Skip to main content

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.
1

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.
2

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.
3

(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.'
  )
}
4

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'.
5

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