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.
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. UsesubmitterWallet 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 acall 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:
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 signsReceiveWithAuthorization 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 withinmaxHoldDuration().
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.
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 throughblock.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:
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.
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:
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.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 bothREVERT 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.