Skip to content

Withdraw ​

A withdrawal spends shielded notes and pays a public EVM address. The recipient and the gross amount are visible on chain.

To an ERC-20 balance ​

ts
const 
tx
= await
wallet
.
withdraw
({
asset
: "USDC",
gross
: "1000", // what leaves the pool; or `net: "…"` for what arrives
recipient
: "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
autoConsolidate
: true,
onPhase
: (
phase
) =>
console
.
log
(
phase
), // "preparing" | "consolidating" | "proving" | "submitting" | "confirmed"
});
tx
.
onLadder
;
OptionDescription
assetregistry id, token address, or symbol
gross or netexactly one — see Gross or net
recipientEVM address receiving the tokens
nativeunwrap to the native coin — see To native ETH
feeAsset, maxFee, selection, autoConsolidate, deadline, signal, onPhase, opIdas for Transfer

Change is returned as new notes. When the asset has a withdrawal ladder, change is split into ladder denominations — see Denominations.

Gross or net ​

The protocol fee is deducted from what leaves the pool. Name the side you mean:

FieldYou nameThe SDK computes
grosspublicOut, the amount published on chainwhat the recipient receives: gross − fee
netwhat the recipient receivesthe smallest gross that delivers at least that
ts
import { 
formatAmount
} from "@lelantos-org/sdk";
// A payment of exactly 250 USDC to an exchange deposit address. const
r
= await
wallet
.
withdraw
({
asset
: "USDC",
net
: {
baseUnits
: 250_000_000n },
recipient
});
formatAmount
(
r
.
gross
.
amount
,
r
.
asset
); // e.g. "250.625" — published on chain
r
.
net
.
baseUnits
; // ≥ 250_000_000n, exact
r
.
fees
.
protocol
; // Money in USDC, or null at 0 bps
r
.
fees
.
relayer
; // Money in the fee asset, or null — never inside gross or net

A net withdrawal rarely lands on a denomination, so it publishes a distinctive amount; r.onLadder is then false. Surface that to the user. See Denominations.

Relayer fee is separate

Neither gross nor net includes the relayer's fee. It is paid from notes, in feeAsset, and reported as fees.relayer.

Previewing ​

previewWithdraw computes what a withdrawal would publish, cost, and deliver. It does no proving or submitting, so it can run on every keystroke.

ts
const 
p
= await
wallet
.
previewWithdraw
({
asset
: "USDC",
gross
: "1000" });
p
.
publicOut
; // gross in circuit units — what the chain sees
p
.
netFormatted
; // "997.5" — what the recipient gets
p
.
onLadder
; // whether this gross blends with other users' withdrawals
p
.
suggestion
; // nearest denomination, when it does not

Without a wallet, withdrawNetFor(publicOut, asset) and grossForNet from @lelantos-org/sdk/protocol apply the same arithmetic. Do not reimplement it: yield-bearing and plain assets round at different steps, and the results differ by up to one unit.

Reading the result ​

FieldMeaning
grosspublicOut (Money, amount exact)
netdelivered to recipient (Money, baseUnits exact)
fees.protocol, fees.relayerMoney or null
onLadderwhether gross is a denomination; false makes the withdrawal linkable
nativeunwrapped to the native coin
recipient, spent, change, txHash, opIdas for a transfer

To native ETH ​

ts
if (
wallet
.
capabilities
.
nativeWithdraw
) {
await
wallet
.
withdraw
({
asset
: "WETH", // must be the chain's registered wrapped coin
gross
: "0.5",
recipient
,
native
: true,
feeAsset
: "USDC", // any accepted fee asset works on the native path too
}); }

native: true unshields WETH and unwraps it in the same transaction through the NativeAdapter contract. It needs no EOA of your own — the relayer broadcasts it — but it needs a known NativeAdapter address. Without one it rejects UNSUPPORTED_OPERATION before any fee is quoted.

Withdrawing the maximum ​

balance().total is not the maximum withdrawable amount: notes can be reserved, dust, or recently received, and one spend can use only nIn notes. spendableMax applies the spend's own rules, and with kind it also reserves the relayer fee:

ts
import { 
circuitAmount
} from "@lelantos-org/sdk";
import {
largestAtMost
} from "@lelantos-org/sdk/protocol";
const
usdc
= await
wallet
.
asset
("USDC");
const {
max
} = await
wallet
.
spendableMax
(
usdc
.
id
, {
kind
: "withdraw" });
// `max` is the largest `gross` one spend can cover, after the fee. Either withdraw it all… if (
max
> 0n) await
wallet
.
withdraw
({
asset
:
usdc
.
id
,
gross
:
max
,
recipient
});
// …or the largest denomination at or below it, which blends in where `max` rarely does. const
denomination
=
largestAtMost
(
max
,
usdc
.
ladder
);
if (
denomination
!==
undefined
) {
await
wallet
.
withdraw
({
asset
:
usdc
.
id
,
gross
:
circuitAmount
(
denomination
),
recipient
});
}

Pass the same feeAsset, native, and selection to spendableMax that the withdrawal will use. For a denomination picker, use wallet.withdrawDenominations().

Next ​