Documentation Index
Fetch the complete documentation index at: https://developers.paxoslabs.com/llms.txt
Use this file to discover all available pages before exploring further.
Withdrawals redeem vault share tokens for the underlying wantAsset through the WithdrawQueue contract. Unlike deposits, there is no inline-permit path in 1.0.0: the share token must have an ERC-20 approve(WithdrawQueue, amount) allowance before submitOrder can pull the shares.
Throughout this guide, client refers to a singleton AmplifyClient created on the server — see Project setup for the wiring. All amounts are base-units decimal strings.
Withdrawals have no permit path. For the share token, permit.authorize always returns method: 'approval' or 'already_approved' — never 'permit'. Approve the WithdrawQueue before calling submitOrder.
Check the share-token allowance
Call permit.authorize with tokenAddress set to the vault address (the share token is the vault contract). The response tells you whether you need to send an approve transaction or whether sufficient allowance already exists.const auth = await client.permit.authorize({
vaultAddress: '0xbbbb000000000000000000000000000000000001',
tokenAddress: '0xbbbb000000000000000000000000000000000001', // share token = vault
amount: shareAmount, // base-units decimal string
userAddress,
chainId: 1,
})
Two possible response shapes:
auth.method === 'already_approved' — skip to Step 3.
auth.method === 'approval' — submit auth.approvalTransaction.encoded to the share-token address (the vault address). The spender baked into the calldata is the WithdrawQueue address.
Approve the WithdrawQueue (if needed)
For the approval branch, just forward the returned transaction:import { useSendTransaction, usePublicClient } from 'wagmi'
const { sendTransactionAsync } = useSendTransaction()
const publicClient = usePublicClient()
if (auth.method === 'approval') {
const approvalHash = await sendTransactionAsync({
to: vaultAddress, // share token = vault contract
data: auth.approvalTransaction.encoded as Hex,
chainId,
})
await publicClient!.waitForTransactionReceipt({ hash: approvalHash })
}
The spender in the approve call is always the WithdrawQueue address, never the vault address. Using the vault address as spender will cause submitOrder to panic with 0x11 when it tries to transferFrom the shares.
(Optional) Estimate the fee
client.withdraw.calculateFee returns the fee the user will pay for a given offer amount. Use it both to render a preview in your UI and to short-circuit submissions that would revert on-chain.const fee = await client.withdraw.calculateFee({
offerAmount: shareAmount, // base-units decimal string of shares to redeem
wantAsset: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
vaultAddress: '0xbbbb000000000000000000000000000000000001',
chainId: 1,
})
// fee = {
// feeAmount, // base-units decimal string of `wantAsset`
// offerFeePercentage: { bps, percentage },
// flatFee,
// }
if (BigInt(fee.feeAmount) >= BigInt(shareAmount)) {
throw new Error(
'Amount is too small — fees would consume the entire withdrawal. Increase the amount and retry.'
)
}
Prepare the withdrawal
const prepared = await client.withdraw.prepareWithdrawal({
vaultAddress: '0xbbbb000000000000000000000000000000000001',
wantAsset: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
shareAmount, // base units of the share token
userAddress,
chainId: 1,
// Optional: override default depositor / receiver / refund receiver
// intendedDepositor,
// receiver,
// refundReceiver,
})
// prepared.transaction = { to: WithdrawQueue, data: submitOrder calldata, value: '0' }
Required fields:
vaultAddress — BoringVault contract address.
wantAsset — ERC-20 the user wants to receive on settlement.
shareAmount — vault shares to redeem (base-units decimal string).
userAddress — wallet submitting the order. Also the default intendedDepositor, receiver, and refundReceiver.
chainId — EVM chain ID.
Optional fields:
intendedDepositor — on-chain SubmitOrderParams.intendedDepositor. Defaults to userAddress.
receiver — address that receives wantAsset on settlement. Defaults to userAddress.
refundReceiver — address that receives refunded shares if the order is cancelled. Defaults to userAddress.
responseFormat — 'encoded' (default), 'full', or 'structured'.
Submit the transaction
import type { Address, Hex } from 'viem'
const tx = prepared.transaction
const withdrawHash = await sendTransactionAsync({
to: tx.to as Address,
data: tx.data as Hex,
value: BigInt(tx.value),
chainId,
})
await publicClient!.waitForTransactionReceipt({ hash: withdrawHash })
End-to-end example
import { AmplifyClient } from '@paxoslabs/amplify-sdk'
import type { Address, Hex } from 'viem'
import {
createPublicClient,
createWalletClient,
custom,
http,
} from 'viem'
import { mainnet } from 'viem/chains'
const client = new AmplifyClient({
apiKey: process.env.PAXOS_LABS_API_KEY!,
})
async function withdraw({
userAddress,
vaultAddress,
wantAsset,
shareAmount,
chainId,
}: {
userAddress: Address
vaultAddress: Address
wantAsset: Address
shareAmount: string
chainId: number
}) {
const publicClient = createPublicClient({ chain: mainnet, transport: http() })
const walletClient = createWalletClient({
account: userAddress,
chain: mainnet,
transport: custom(window.ethereum),
})
// 1. Check the share-token allowance
const auth = await client.permit.authorize({
vaultAddress,
tokenAddress: vaultAddress, // share token = vault contract
amount: shareAmount,
userAddress,
chainId,
})
// 2. Approve the WithdrawQueue if needed
if (auth.method === 'approval') {
const hash = await walletClient.sendTransaction({
to: vaultAddress, // share token = vault contract
data: auth.approvalTransaction.encoded as Hex,
})
await publicClient.waitForTransactionReceipt({ hash })
}
// 3. Prepare
const prepared = await client.withdraw.prepareWithdrawal({
vaultAddress,
wantAsset,
shareAmount,
userAddress,
chainId,
})
// 4. Submit
const tx = prepared.transaction
const withdrawHash = await walletClient.sendTransaction({
to: tx.to as Address,
data: tx.data as Hex,
value: BigInt(tx.value),
})
await publicClient.waitForTransactionReceipt({ hash: withdrawHash })
return withdrawHash
}
Listing a user’s withdrawals
client.withdraw.listRequests supports cursor pagination and an AIP-160-style filter string. Available filter keys: status (PENDING, COMPLETE, PENDING_REFUND, REFUNDED), chainId, wantAssetAddress, vaultAddress, userAddress, receiverAddress, refundReceiverAddress, orderIndex, isSubmittedViaSignature, isForceProcessed, isMarkedForRefund, isMarkedForRefundByUser, didOrderFailTransfer.
const firstPage = await client.withdraw.listRequests({
filter: `userAddress=${userAddress} AND vaultAddress=${VAULT} AND status=PENDING`,
pageSize: 25,
})
// firstPage = { withdrawalRequests, nextPageToken }
for (const r of firstPage.withdrawalRequests) {
// r.orderIndex, r.orderAmount, r.wantAssetAddress, r.status, ...
}
// Paginate by passing the previous nextPageToken back in.
let pageToken = firstPage.nextPageToken
while (pageToken) {
const next = await client.withdraw.listRequests({
filter: `userAddress=${userAddress} AND vaultAddress=${VAULT}`,
pageSize: 25,
pageToken,
})
// ...consume next.withdrawalRequests
pageToken = next.nextPageToken
}
Cancelling a pending order
orderIndex comes from a row returned by listRequests. Cancel returns calldata for the WithdrawQueue.cancel (or equivalent) call; submit it the same way as the deposit/withdrawal flows above.
import type { Address, Hex } from 'viem'
const prepared = await client.withdraw.cancel({
vaultAddress: '0xbbbb000000000000000000000000000000000001',
orderIndex: '42', // decimal string, from listRequests
chainId: 1,
})
const tx = prepared.transaction
const cancelHash = await sendTransactionAsync({
to: tx.to as Address,
data: tx.data as Hex,
value: BigInt(tx.value),
chainId,
})
await publicClient!.waitForTransactionReceipt({ hash: cancelHash })
Converting share amounts
import { parseUnits } from 'viem'
// Read the share-token decimals live from the vault contract.
const decimals = await publicClient.readContract({
address: VAULT,
abi: [{ type: 'function', name: 'decimals', inputs: [], outputs: [{ type: 'uint8' }], stateMutability: 'view' }],
functionName: 'decimals',
})
const shareAmount = parseUnits(userInput, decimals).toString()
Error handling
import { AmplifyError, AmplifyTimeoutError } from '@paxoslabs/amplify-sdk'
try {
await client.withdraw.prepareWithdrawal({ /* ... */ })
} catch (err) {
if (err instanceof AmplifyTimeoutError) {
// Retry, surface a timeout UI, etc.
} else if (err instanceof AmplifyError) {
// err.statusCode number — HTTP status (e.g. 400, 401, 500)
// err.body unknown — parsed backend error body (when JSON)
// err.message string — human-readable summary
// err.rawResponse RawResponse — the original Response
console.error('Amplify error', err.statusCode, err.message)
} else {
throw err
}
}
When surfacing errors to the browser, log err.body and err.rawResponse server-side and return a generic message to the client.
Next steps