Skip to main content
What you’ll be able to do
  • Shield funds in a MIP410 pool.
  • Send a shielded payment.
  • Accept or reject an incoming shielded payment.
  • Reclaim funds from an expired payment.
  • Unshield funds to an onchain address.
A shielded payment involves two transactions. Initiation reserves the sender’s input notes in a payment hold. The recipient’s virtual asset service provider (VASP) then accepts or rejects that hold. Acceptance spends the inputs and creates the recipient’s note. Rejection, or reclaim after expiry, releases the inputs for the sender to use again. The client retains the private note data and works with its VASP to prepare proofs. The signing account’s balance in the gas token pays each transaction fee and that may differ from the note owner or the VASP. The roles and authorization guide explains those responsibilities.

Establish the proving and participant prerequisites

Use a configured MIP410 pool, an admitted MIP20 token, and admitted sender and recipient VASPs. Their registry records must name the policy verification keys and messaging keys used by the proof system. The client needs a compatible, publicly obtainable key and note library, circuit artifacts, a prover and VASP messaging and policy-proof services. Before moving value, obtain the toolchain’s version, artifact hashes, installation instructions, witness builder, proof-generation commands and exported request format. Match its verification keys against the pool’s configured circuit IDs. An ordinary EVM wallet and the token SDK do not generate MIP410 proofs. Do not substitute dummy proofs or send private witness material to an untrusted proving service. Use submitterWallet for the signing account that submits transactions and pays gas. Install @metalntwx/metal-ts@0.3.2 and viem@2.57.7 with pnpm. The configured testnet pool is 0x0000000000000000000000000000000000000410.
asset is the verified MIP20 token address supplied by the token configuration. Read metadata and validate base-unit amounts using Direct payment. The private amount must also satisfy 0 < amount < 2^128.

Set up keys, notes and recovery once

Create or restore the customer’s spending key and derive the viewing and nullifier keys using the key derivation specification. The spending key authorizes shielding, initiation and unshielding statements with Grumpkin Schnorr signatures. It is different from the EVM transaction signing key. A VASP prepares proofs without receiving the spending key when the customer’s wallet supplies the required signatures. Retain encrypted, recoverable records for each note opening, its asset, amount, commitment, tree position and ownership keys. Persist proposed recipient and change notes before submitting any transaction that could create them. A public commitment cannot reconstruct a lost note opening. Record these states in the client store: Write transaction records and note state changes atomically. Reserve input notes locally while preparing a proof so concurrent requests cannot select them twice. Keep an unresolved submission pending until its onchain outcome is known. Refresh the VASP and sanctions registries and rebuild their proof paths with the compatible tooling. Verify the rebuilt roots against the pool. Current sanctions roots are mandatory. VASP roots use the documented root windows.

Submit a transaction with its proof

Each transaction below supplies a call object. Simulate and estimate the exact calldata as submitterWallet.account using Estimate and record fees. Set gasLimit from the estimate and use the quoted price as maxFeePerGas, then sign:
Persist the signed transaction with its private transaction record before await signed.send(). Require a successful transaction receipt, the evidence specified below for that transaction and canonical finality before changing spendable note state. Reuse this procedure for each transaction in the lifecycle, with a separate transaction hash.

1. Shield MIP20 funds

Construct a private note for the customer and generate the shield proof. Its public request binds the chain ID, pool contract address, note commitment, asset, amount, depositor, shield timestamp and sanctions root. The timestamp must be no later than the inclusion block and no more than 3,600 seconds old. Preserve the note opening before requesting either signature. The note owner signs the shield statement with the spending key. Separately, the MIP20 depositor signs ReceiveWithAuthorization with to = pool, value = request.amount and nonce = request.noteCommitment. Use the token’s EIP-712 domain and the signing procedure in Authorized payment. Use the note commitment as the authorization nonce for this shielding transaction. The prover produces shieldProof and the corresponding request with the exact ShieldRequest fields. Encode the separate deposit authorization and call the pool:
validAfter, validBefore and depositSignature come from that depositor’s signed authorization. Verify the exact token transfer from depositor to pool, the pool’s Shielded event and appended note commitment. At the verified block, check the backing increase and store the note’s tree position. The note becomes available to spend after the shielding transaction is final.

2. Initiate a payment

Obtain the recipient VASP’s signed payment request. Refresh its registry record, verify its messaging signature and refuse an expired request. The request supplies the recipient’s diversified address, VASP identity and record thresholds. These offchain checks happen before constructing a payment hold. Select one or two unlocked, unspent notes of the payment asset. Compute the recipient note, any change note, nullifiers and input lock IDs. Pad each input array to exactly two elements, with zero in an unused slot. Compute the payment effect hash and commitment binding the recipient VASP using the proving toolchain. Choose a deadline strictly after current chain time and within maxHoldDuration(). Prepare the private payment data, commit to it, and encrypt it in a payment envelope for the recipient VASP. The sender proof binds the plaintext to that commitment. Generate the sender proof using the spending-key signature and the sender VASP’s phase-0 policy-wrapper proof. The sender proof specification defines those bindings.
The prover exports initiationRequest, senderProof and senderPolicyProof for the same chain ID, pool contract address, roots and payment effect. Verify HoldCreated for holdId and compare holdOf(holdId) with the saved payment effect hash, recipientFiBindingCommitment, deadline and payload commitment. Its state must be Held, numeric value 1. Each real inputLocked must be true, while the inputs’ nullifiers remain unused. Initiation appends neither recipient nor change notes. Mark the local inputs reserved after finality and deliver the encrypted payment envelope through the configured VASP messaging service. A finalized hold still needs resolution.

3. Accept or reject the incoming payment

The recipient VASP retrieves the finalized live hold, authenticates and decrypts the payment envelope, checks its commitment and applies its policy. It generates a recipient proof for that exact hold and a policy-wrapper proof using accepted registration and sanctions roots. Resolution is allowed through block.timestamp == deadline. To accept, use decision Accept = 0, wrapper phase 1, and the resolution effect’s two nullifiers, recipient note commitment and optional change note commitment:
The proof toolchain must export roots, resolutionEffect and both proofs for the stored payment effect. Verify HoldAccepted, the expected appended commitments, both real nullifiers becoming used and state Accepted = 2. Input locks remain permanently set because those notes have been spent. After finality, mark sender inputs spent and add the recipient and change notes to their owners’ stores using the preserved private openings. To reject, generate decision Reject = 1 and wrapper phase 2. Rejection reveals neither nullifiers nor proposed note commitments. Its recipient proof uses zero for those four resolution effect fields.
Verify HoldRejected, state Rejected = 3, released input locks, unchanged nullifiers and no new note commitments. After finality, restore the original input notes to the sender’s available set. An invalid or unauthorized resolution reverts and leaves the hold unchanged.

4. Reclaim an expired hold

When chain time passes the deadline, holdOf reports Expired = 4. Expiry alone leaves the input locks in place. Any submitter whose signing account has a sufficient balance in the gas token may call reclaimExpired without a proof or spending-key signature:
Verify HoldReclaimed, state Reclaimed = 5, released input locks and unchanged nullifiers. Restore the original notes only after finality. Reclaim remains available while the pool is paused. Accepted, rejected and reclaimed holds are terminal and cannot be resolved again.

5. Unshield to the authorized destination

Select unlocked, unspent notes backed by public MIP20 and a controlled public destination. Construct the unshield proof with the asset, amount, destination, the note tree’s Merkle root, two nullifiers, two input lock IDs, optional change note and current sanctions root. The spending-key signature binds the destination, so a submitter cannot change it.
Generate unshieldRequest and unshieldProof together for the authorized statement. Screen the exact pool-to-destination credit using Receive policy before submitting. Reconcile one of these outcomes after checking canonical finality: A policy refusal normally reverts unshielding. A destination configured for QUARANTINE can instead produce the third outcome. An Unshielded event alone does not prove destination credit. Do not reuse consumed notes or generate a replacement unshielding transaction for quarantined funds. Reconcile the separate policy claim using its recovery authority and verify its eventual delivery.

Recover and test the complete lifecycle

Reconcile uncertain transactions by their saved transaction hash and exact note or hold identity. Rebuilding a proof against a fresh root does not authorize a second payment. First establish whether the original transaction succeeded. Retain old attempts and recover from their finalized events before choosing new notes or signing replacement statements. Exercise acceptance, rejection and post-expiry reclaim on separate holds. Test replay, locked-input reuse, stale sanctions roots, altered destination and proof failure. Verify that reverted transactions leave notes and balances unchanged. Also test unshield refusal in both REVERT and QUARANTINE modes. The latter must consume the notes without recording destination credit. Fixtures can test client handling, but cannot establish live deployment or proof compatibility. Restore the key and note store in a fresh client and reconstruct its finalized state before claiming recovery works.