> ## Documentation Index
> Fetch the complete documentation index at: https://docs.metalntwx.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Shielded payment

> Integrate shielding, payment acceptance or rejection, expiry recovery, and unshielding with a configured MIP410 pool

> [!NOTE]
>
> **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](https://docs.metalntwx.com/learn/mip410/roles-and-authorization.md) 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](https://docs.metalntwx.com/reference/privacy/circuit-registry.md). 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`.

```typescript theme={null}
import { parseAbi } from "viem"

const pool = "0x0000000000000000000000000000000000000410" as const
const stateAbi = parseAbi([
  "function paused() view returns (bool)",
  "function assetAdmitted(address asset) view returns (bool)",
  "function circuitId(uint8 circuit) view returns (bytes32)",
  "function registrationRoot() view returns (bytes32)",
  "function sanctionsRoot() view returns (bytes32)",
  "function maxHoldDuration() view returns (uint64)",
  "function backingOf(address asset) view returns (uint256)",
  "function inputLocked(bytes32 inputLockId) view returns (bool)",
  "function nullifierUsed(bytes32 nullifier) view returns (bool)",
  "function holdOf(bytes32 holdId) view returns ((uint8 state, bytes32 paymentEffectHash, bytes32 recipientFiBindingCommitment, uint64 deadline, bytes32 payloadCommitment))",
])
if (await client.getChainId() !== 63825) throw new Error("Wrong network")
if (!await client.getCode({ address: pool })) throw new Error("Pool absent")
if (await client.readContract({ address: pool, abi: stateAbi,
  functionName: "paused" })) throw new Error("Pool paused")
if (!await client.readContract({ address: pool, abi: stateAbi,
  functionName: "assetAdmitted", args: [asset] })) {
  throw new Error("Asset not admitted for shielding")
}
```

`asset` is the verified MIP20 token address supplied by the token configuration.
Read metadata and validate base-unit amounts using
[Direct payment](https://docs.metalntwx.com/build/payments/direct-payment.md#read-metadata).
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](https://docs.metalntwx.com/reference/privacy/key-derivation.md).
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](https://docs.metalntwx.com/reference/privacy/shielded-note.md), 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:

| Record | Required data |
| - | - |
| Note | Private opening, commitment, tree index, asset, owner and spend status |
| Transaction | Chain ID, pool contract address, function, exact request bound by the proof, signed transaction hash and bytes |
| Payment | Hold ID, input note IDs and input lock IDs, payment effect, deadline, payload commitment and counterparty request |
| Completion | Transaction receipt and canonical finalized block, exact events, resulting note and lock changes |

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](https://docs.metalntwx.com/reference/assets-and-payments/mip410.md#registry-state)
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](https://docs.metalntwx.com/reference/assets-and-payments/mip410.md#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](https://docs.metalntwx.com/build/prepare/estimate-and-record-fees.md). Set `gasLimit`
from the estimate and use the quoted `price` as `maxFeePerGas`, then sign:

```typescript theme={null}
import { signMip20Contract } from "@metalntwx/metal-ts"

const signed = await signMip20Contract({
  publicClient: client, walletClient: submitterWallet, ...call,
  gasToken: "0x000000000000000000000000000000000000b5d5",
  gasLimit, gasFallback: gasLimit, maxFeePerGas,
})
```

[Persist the signed
transaction](https://docs.metalntwx.com/build/payments/verify-and-reconcile-payments.md#save-outgoing-attempts-before-submission)
with its private transaction record before `await signed.send()`. Require a
successful transaction receipt, the evidence specified below for that
transaction and [canonical finality](https://docs.metalntwx.com/reference/network-fundamentals/settlement.md#finality-check)
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](https://docs.metalntwx.com/reference/privacy/shield-circuit.md). 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](https://docs.metalntwx.com/build/payments/authorized-payment.md#obtain-and-check-the-signature).
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](https://docs.metalntwx.com/reference/assets-and-payments/mip410.md#core-types). Encode the separate
deposit authorization and call the pool:

```typescript theme={null}
import { encodeAbiParameters } from "viem"

const depositAuthorization = encodeAbiParameters([
  { type: "uint256" }, { type: "uint256" }, { type: "bytes" },
], [validAfter, validBefore, depositSignature])
const shieldAbi = parseAbi([
  "function shield((address asset, address depositor, uint256 amount, bytes32 noteCommitment, uint64 shieldTimestamp, bytes32 sanctionsRoot) request, bytes depositAuthorization, bytes shieldProof) returns (bytes32)",
])
const call = { address: pool, abi: shieldAbi, functionName: "shield",
  args: [request, depositAuthorization, shieldProof] } as const
```

`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](https://docs.metalntwx.com/reference/assets-and-payments/mip410.md#payment-requests). 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](https://docs.metalntwx.com/reference/privacy/transfer-circuit.md) defines those bindings.

```typescript theme={null}
const initiationAbi = parseAbi([
  "function initiatePayment((bytes32 commitmentRoot, bytes32[] inputLockIds, bytes32 paymentEffectHash, bytes32 recipientFiBindingCommitment, uint256 nonce, uint64 deadline, bytes32 payloadCommitment, bytes32 registrationRoot, bytes32 sanctionsRoot) request, bytes senderProof, bytes policyProof) returns (bytes32)",
])
const call = { address: pool, abi: initiationAbi, functionName: "initiatePayment",
  args: [initiationRequest, senderProof, senderPolicyProof] } as const
const holdId = initiationRequest.paymentEffectHash
```

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:

```typescript theme={null}
const acceptAbi = parseAbi([
  "function acceptPayment(bytes32 holdId, (bytes32 registrationRoot, bytes32 sanctionsRoot) roots, (bytes32[] nullifiers, bytes32 recipientNoteCommitment, bytes32 changeNoteCommitment) resolutionEffect, bytes recipientProof, bytes policyProof)",
])
const call = { address: pool, abi: acceptAbi, functionName: "acceptPayment",
  args: [holdId, roots, resolutionEffect, recipientProof, recipientPolicyProof] } as const
```

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.

```typescript theme={null}
const rejectAbi = parseAbi([
  "function rejectPayment(bytes32 holdId, (bytes32 registrationRoot, bytes32 sanctionsRoot) roots, bytes recipientProof, bytes policyProof)",
])
const call = { address: pool, abi: rejectAbi, functionName: "rejectPayment",
  args: [holdId, roots, rejectionProof, rejectionPolicyProof] } as const
```

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:

```typescript theme={null}
const reclaimAbi = parseAbi(["function reclaimExpired(bytes32 holdId)"])
const call = { address: pool, abi: reclaimAbi, functionName: "reclaimExpired",
  args: [holdId] } as const
```

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](https://docs.metalntwx.com/reference/privacy/unshield-circuit.md)
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.

```typescript theme={null}
const unshieldAbi = parseAbi([
  "function unshield((address asset, uint256 amount, address destination, bytes32 commitmentRoot, bytes32[] inputLockIds, bytes32[] nullifiers, bytes32 changeNoteCommitment, bytes32 sanctionsRoot) request, bytes unshieldProof)",
])
const call = { address: pool, abi: unshieldAbi, functionName: "unshield",
  args: [unshieldRequest, unshieldProof] } as const
```

Generate `unshieldRequest` and `unshieldProof` together for the authorized
statement. Screen the exact pool-to-destination credit using
[Receive policy](https://docs.metalntwx.com/reference/compliance/receive-policy.md) before submitting. Reconcile one of
these outcomes after checking canonical finality:

| Outcome | Required state and application result |
| - | - |
| Destination credited | Verify `Unshielded`, the exact token `Transfer(pool, destination, amount)`, backing reduction, consumed real nullifiers and any appended change note. Mark the inputs spent, make change available and record delivery. |
| Transaction reverted | The nullifiers, change commitment and backing change roll back. Reconcile the original notes as unspent before attempting another unshielding transaction. Record the transaction fee separately. |
| Credit quarantined | Verify `Unshielded`, the exact token `Transfer(pool, ReceivePolicy, amount)` and matching quarantine receipt naming the pool as originator and the intended destination in `receiver`. Backing decreases and real input nullifiers are consumed. Mark inputs spent, make any change available, save the full quarantine receipt and leave delivery unresolved. |

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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.