Reshield
A reshield turns a shielded position in one asset into a shielded position in another. On-chain it is a withdraw-and-swap whose aggregator route ends not at a recipient but at a contractCalls step that deposits the swapped output back into the Entrypoint. The result is a new note with no on-chain link to the one you spent, and the funds never sit in a wallet in between.
Reshielding is honest about one thing the shorthand hides: the funds do leave the pool. They exit, trade, and return within one transaction. The privacy claim is about what an observer can link, not about the funds staying shielded throughout.
Same-chain reshield
- Estimate.
estimateReshieldAmountqueries the relayer fee and the aggregator, reads the output token'svettingFeeBPSfrom the Entrypoint, and returns the note value the deposit will carry plustotalDepositCost(the deposit value and its vetting fee). - Prove the deposit. Build a standard deposit proof for the output token with
prepareDeposit({ tokenId: outputToken, value: estimate.noteValue }). This is the note you will hold afterwards. - Quote.
prepareReshieldQuotefetches the aggregator'scontractCallsquote (swap, then a call toEntrypoint.depositwith your deposit proof and note data), and commits the routing with the relayer. Because the deposit calldata is inside the aggregator calldata, and that is inside the proof-bound routing, the deposit is as fixed as the swap. - Relay.
relayReshieldproves the withdrawal and submits. RelaySwaps executes the swap; the aggregator's post-swap call deposits; the pool inserts your new commitment.
const estimate = await session.estimateReshieldAmount({
relayerInfo,
amount: spendAmount, // Hex, in the input token
tokenId: usdcAddress,
outputToken: usdtAddress,
});
const depositResult = await session.prepareDeposit({
tokenId: usdtAddress,
value: estimate.noteValue,
});
const quote = await session.prepareReshieldQuote({
relayerInfo,
amount: spendAmount,
tokenId: usdcAddress,
outputToken: usdtAddress,
depositResult,
totalDepositCost: estimate.totalDepositCost,
});
const receipt = await session.relayReshield({
inputCommitments: [noteToSpend.commitment],
amount: spendAmount,
tokenId: usdcAddress,
outputToken: usdtAddress,
swapCalldata: quote.swapCalldata,
selectedQuote: quote.selectedQuote,
swapTarget: quote.swapTarget,
approvalAddress: quote.approvalAddress,
dustRecipient: quote.dustRecipient,
depositNote: quote.depositNote, // persisted as PENDING once mined
});
When outputToken is a yield share, the embedded call is PPRouter.depositExactShares (or depositExactSharesNative for a native-mode router) instead of Entrypoint.deposit: the swap buys the wrapper's underlying, and the aggregator approves the router. Configure the session's yield deployments so the SDK can resolve the router.
Cross-chain reshield
The same three calls with a destination: estimateReshieldCrosschainAmount, prepareReshieldCrosschainQuote, and relayReshieldCrosschain, each taking toChainId. The withdrawal happens on the source chain, the aggregator bridges and swaps, and the deposit lands in the destination chain's Entrypoint. Two things change:
- The deposit proof is destination-scoped. Build
depositResultagainst the destination chain's pool and Entrypoint (a second session, or the destination interactors), because that is where the commitment will be inserted. Share deposits on the destination reserve a larger rate headroom (CROSS_CHAIN_DEPOSIT_RATE_HEADROOM_PPM), since a bridge leg takes minutes rather than seconds. - Dust can be rescued to a stealth address. A bridge delivers slightly more or less than quoted. Opt in with
dustRescue: { recipientIdentifier }and the leftover on the destination is deposited for a one-time stealth address derived from your meta-address, with the discovery material packed into the deposit's ownnoteDatarather than a public announcement. The identifier rides in the routing'sdustRescuefield, so it is proof-bound even thoughRelaySwapsnever reads it.
Cross-chain is not a single moment. The source transaction confirms, then the bridge leg completes on its own schedule; the SDK's swap status provider tracks it.
Constraints
- Two providers. Reshield needs
withSwapAndDepositQuoteProvider(...)for thecontractCallsquote in addition towithSwapQuoteProvider(...).LiFiSwapQuoteProviderimplements both. - The vetting fee is read on-chain. It is whatever
Entrypoint.assets(outputToken).vettingFeeBPSsays at quote time and is not caller-supplied, becauseEntrypoint.depositaccepts nothing else. - No recipient, no floor. The routing carries a zero
recipient, so the on-chain output check does not run; a failed or short delivery makes the embeddedEntrypoint.depositrevert, which fails the whole transaction. The contract also rejects anynativeGasorminOutputAmounton such a routing, so those fields cannot be set by mistake. - The new note is
PENDING. Like any deposit it awaits ASP attestation before it is spendable; the ASP sees a fresh deposit from RelaySwaps' aggregator leg, not from you. - Cross-chain needs both chains. Source-chain relayer with swaps enabled, destination-chain Entrypoint interactor, and matching yield targets on the destination if you are landing as a share.
Behind the scenes
estimateReshieldAmount, prepareReshieldQuote, relayReshield; or the ...Crosschain trio. Plus prepareDeposit for the output note.RelaySwaps.execute (or .executeUnwrapped when spending a yield share), whose aggregator call ends in Entrypoint.deposit or PPRouter.depositExactShares on the destination.POST /v1/quote/evm/{chainId}/swap and POST /v1/relay/evm/{chainId}/relay-swap, as for a plain withdraw-and-swap.transact_NxM proof for the spend and a deposit proof for the new note, the second embedded in the aggregator calldata the first commits to.What's next
- Withdraw and swap: the same route delivered to a public address.
- Swap and deposit: enter the pool from any wallet token.
- Stealth addresses: where cross-chain dust lands.