client.tradingWithdraws creates durable, signed withdrawal intents out of the Trading venue.
Every method is authenticated and account-scoped, so each input accepts an optional account field
("main", "active", or { subaccountId }). See Account scoping for how the default resolves.
Each intent is a signed message, not a fire-and-forget call. The SDK embeds a deadline (five
minutes from now on the device clock, unless you pass deadline), a random nonce, and an
idempotency key into the payload, then signs it. Because the payload is signed, an intent is
durable: submit it once, and it settles even if your process restarts.
Signing
Every withdrawal needs a signature over its canonical EIP-191 text message. You have two options, passed alongside the withdrawal input:
walletSigner: hand the SDK an EOA wallet signer and it builds the canonical message, asks the wallet to sign it withpersonal_sign, and submits the wallet-authorized intent.payloadSignature: supply a signature you precomputed yourself (for example from an API-key signer or custody infrastructure). The SDK submits the backend-authorized intent as-is.
Provide exactly one. If neither is present, the call throws a ValidationError.
import type { TradingWithdrawWalletSigner } from "@polyester/sdk";
const walletSigner: TradingWithdrawWalletSigner = {
signerWallet: owner.address,
accountId: me.accountId,
signMessage: (message) => owner.signMessage({ message }),
};Methods
| Method | Summary |
|---|---|
validateDestination | Check a destination chain and address before you sign anything. |
createToFunding | Withdraw from Trading to Funding, staying on Polyester. |
createToExternalChain | Withdraw from Trading out to an external chain via the Zipper. |
prepareToFunding | Sign a Trading-to-Funding intent once, submit it when you are ready. |
prepareToExternalChain | Sign an external-chain intent once, submit it when you are ready. |
validateDestination(input, options?)
Preflights a destination before you build or sign an intent. It takes a destinationChainId and a destinationAddress, and answers whether that pair is a legitimate withdrawal target.
const check = await client.tradingWithdraws.validateDestination({
destinationChainId: 1,
destinationAddress: "0xabc...",
});
if (!check.valid) {
console.error(check.code, check.message);
return;
}
// Use the canonical form the backend resolved, not the string you typed.
const destinationAddress = check.canonicalDestinationAddress;The result is a discriminated union on valid:
type ValidateWithdrawDestinationResult =
| {
valid: true;
code: "valid";
message: string;
canonicalDestinationAddress: string;
}
| {
valid: false;
code:
| "invalid_address"
| "unsupported_chain"
| "polyester_smart_account"
| "token_contract"
| "denylisted_address"
| "unspecified";
message: string;
canonicalDestinationAddress: string;
};| Code | Meaning |
|---|---|
valid | The address is a legitimate destination on that chain. |
invalid_address | The address is malformed for that chain. |
unsupported_chain | Withdrawals to that chain are not supported. |
polyester_smart_account | The address is a Polyester smart account; use an internal transfer. |
token_contract | The address is a token contract, so funds sent there would be stranded. |
denylisted_address | The address is blocked for that chain family. |
unspecified | The backend returned a code this SDK version does not have a label for. |
createToExternalChain rather than treating a valid result as an approval.createToFunding(input, options?)
Moves funds from the Trading venue to Funding. The destination stays on Polyester, so nothing leaves
the platform. Resolves to a CreateTradingWithdrawResult carrying the intentId.
const result = await client.tradingWithdraws.createToFunding({
assetId: 2,
quantity: "500.00",
idempotencyKey: crypto.randomUUID(),
walletSigner,
});
console.log(result.intentId);CreateTradingWithdrawToFundingInput
| Field | Type | Required | Notes |
|---|---|---|---|
assetId | number | yes | Integer from 1 through 4,294,967,295. |
quantity | decimal string | yes | Amount to withdraw. Strict precision. |
idempotencyKey | string | yes | Reuse it to retry safely. |
destinationAddress | string | no | Optional Funding destination address. |
deadline | Date | no | Signed expiry. Defaults to 5 minutes. |
walletSigner | TradingWithdrawWalletSigner | one signer | SDK signs the EIP-191 message. |
payloadSignature | Uint8Array | one signer | A signature you precomputed. |
account | AccountScope | no | Scope override. |
createToExternalChain(input, options?)
Withdraws from the Trading venue out to an external chain. The funds leave Polyester via the Zipper. The amount is gross (fees are taken from it), and you specify the destination network and address.
const result = await client.tradingWithdraws.createToExternalChain({
assetId: 2,
quantity: "500.00", // gross amount
destinationChainId: 1,
destinationAddress: "0xabc...",
idempotencyKey: crypto.randomUUID(),
walletSigner,
});
console.log(result.intentId);CreateTradingWithdrawToExternalChainInput
| Field | Type | Required | Notes |
|---|---|---|---|
assetId | number | yes | Integer from 1 through 4,294,967,295. |
quantity | decimal string | yes | Gross amount to withdraw. Strict precision. |
destinationChainId | number | yes | Destination network chain id. |
destinationAddress | string | yes | Destination address on that network. |
idempotencyKey | string | yes | Reuse it to retry safely. |
deadline | Date | no | Signed expiry. Defaults to 5 minutes. |
walletSigner | TradingWithdrawWalletSigner | one signer | SDK signs the EIP-191 message. |
payloadSignature | Uint8Array | one signer | A signature you precomputed. |
account | AccountScope | no | Scope override. |
When quantity has more decimal places than the destination network's token supports, the backend
rejects the withdrawal before reserving any funds, with detail.service === "withdraw" and detail.code === "AMOUNT_EXCEEDS_ROUTE_PRECISION". Round the amount down to the token's decimals
before signing.
prepareToFunding(input) and prepareToExternalChain(input)
Both create methods are a prepare-then-submit pair collapsed into one call. Use the prepare variants when submission can be interrupted, most often by an MFA step-up: they build and sign the
payload once and hand back a PreparedTradingWithdraw whose submit resends that exact payload and
signature. Only transport options change between attempts until the deadline is near (see below).
import { isFreshStepUpRequiredError } from "@polyester/sdk";
const prepared = await client.tradingWithdraws.prepareToFunding({
assetId: 2,
quantity: "500.00",
idempotencyKey: crypto.randomUUID(),
walletSigner,
});
try {
await prepared.submit();
} catch (error) {
if (!isFreshStepUpRequiredError(error)) throw error;
// Satisfy the step-up, then resubmit the same signed payload.
await prepared.submit({ stepUpToken });
}They take the same inputs as their create counterparts.
import type { CreateTradingWithdrawResult, PolyesterMutationOptions } from "@polyester/sdk";
interface PreparedTradingWithdraw {
readonly deadline: Date; // when the backend stops accepting the current signature
isExpired: () => boolean; // true once the deadline is less than a minute away
submit: (options?: PolyesterMutationOptions) => Promise<CreateTradingWithdrawResult>;
}What submit does near the deadline depends on how the withdrawal was signed:
walletSigner: onceisExpired()is true,submitsigns a fresh deadline and nonce with the same idempotency key before sending, so a resubmit after a slow MFA step-up still goes through.deadlinethen reflects the new signature. The wallet is asked to sign again only in this case.payloadSignature: the SDK cannot sign again. After the deadline passes,submitthrowsWithdrawDeadlineExpiredErrorwithout sending. Prepare the withdrawal again (same input, same idempotency key) before resubmitting.
WithdrawDeadlineExpiredError (code WITHDRAW_DEADLINE_EXPIRED) is a ValidationError subclass
exported from @polyester/sdk. The backend's own expired-deadline rejection maps to it too.
CreateTradingWithdrawResult
Every submission path returns the same shape:
interface CreateTradingWithdrawResult {
intentId: string;
}Track the intent from there through the lifecycle stream. A trading withdrawal that fails after
admission surfaces its cause as a lifecycle reason: trading_withdraw_policy_denied, trading_withdraw_contract_reverted, or trading_withdraw_execution_failed.
Related
- Zipper for the cross-chain bridge that carries an external-chain withdrawal.
- Lifecycle to follow an intent from signed to settled.
- Guard signer for co-signing.
- Auto-withdraw for scheduled withdrawals to whitelisted addresses.
- Deposits and withdrawals guide for the end-to-end walkthrough.
- Requests and idempotency for retry semantics and idempotency keys.