Withdrawals

Withdraw crypto from user wallets to external addresses using on-chain execution.

Before You Start

Read the following guides before proceeding:

GuideWhy
Getting StartedPlatform overview and setup
Api BasicsRequired headers and request configuration
AuthenticationHow to obtain access tokens
OnboardingUser and wallet registration
ABI ReferenceSmart contract ABIs

Overview

Crypto withdrawals are executed entirely on-chain without Wirex backend involvement. The user's Smart Wallet submits transactions directly to the blockchain through the ExecutionDelayPolicy module, which enforces a time-lock before funds can be sent.

Withdrawals can be performed using:

  • Wirex SDK (preferred) — handles the two-phase flow automatically
  • ZeroDev SDK — direct interaction with the Smart Wallet and policy contract

How It Works

Withdrawals follow a two-phase pattern enforced by the ExecutionDelayPolicy smart contract:

User App                  Smart Wallet              ExecutionDelayPolicy
   |                           |                              |
   | 1. Initiate withdrawal    |                              |
   |-------------------------->|                              |
   |                           |                              |
   |                           | 2. Call request()            |
   |                           |----------------------------->|
   |                           |                              |
   |                           |      Store with valid_after  |
   |                           |<-----------------------------|
   |                           |                              |
   |   Request tx hash         |                              |
   |<--------------------------|                              |
   |                           |                              |
   |   ═══════ DELAY PERIOD (3 seconds) ═══════               |
   |                           |                              |
   | 3. Execute withdrawal     |                              |
   |-------------------------->|                              |
   |                           |                              |
   |                           | 4. Submit with custom nonce  |
   |                           |----------------------------->|
   |                           |                              |
   |                           |      Verify delay passed     |
   |                           |      Execute transfer        |
   |                           |<-----------------------------|
   |                           |                              |
   |   Execution tx hash       |                              |
   |<--------------------------|                              |

Phase 1: Request

  1. App prepares withdrawal: unwrap unified token (WUSD/WEUR) + transfer base token (USDC/EURC)
  2. App calls ExecutionDelayPolicy.request(wallet, nonce, callData)
  3. Contract stores the operation with valid_after timestamp (current time + delay)
  4. Returns request transaction hash

Phase 2: Execute

  1. App waits until valid_after timestamp passes
  2. App submits the original callData with matching custom nonce
  3. Policy verifies delay has passed
  4. Transaction executes: unwrap + transfer

Delay Period

OperationDelay
Token transfers3 seconds
Account changes24 hours

The delay protects Wirex from fraud attempts through frontrunning transactions.


Using Wirex SDK (Recommended)

Full Transfer (Automatic)

The SDK handles both phases automatically, waiting for the delay period:

import { createSDK } from '@wirexapp/wirexpay-sdk';

const sdk = await createSDK({ /* config */ });

const result = await sdk.crypto.transfer.makeFullErc20Transfer({
  tokenAddress: '0x0774164DC20524Bb239b39D1DC42573C3E4C6976', // WUSD
  recipientAddress: '0xF4D2A0E30C7983EF65DC0DD938BED82C7E3745DB',
  amount: 100,
});

console.log('Request tx:', result.firstPartTxHash);
console.log('Execution tx:', result.finalTxHash);
console.log('Success:', result.success);

Two-Step Transfer (Manual Control)

For more control over timing or UI feedback:

// Step 1: Initiate withdrawal request
const requestTxHash = await sdk.crypto.transfer.makeErc20TransferFirstPart({
  tokenAddress: '0x0774164DC20524Bb239b39D1DC42573C3E4C6976', // WUSD
  recipientAddress: '0xF4D2A0E30C7983EF65DC0DD938BED82C7E3745DB',
  amount: 100,
});

console.log('Request submitted:', requestTxHash);

// Step 2: Fetch pending withdrawal requests
const withdrawals = await sdk.api.withdrawal.getWithdrawalRequests();

const pendingWithdrawal = withdrawals.find(
  w => w.to_address === '0xF4D2A0E30C7983EF65DC0DD938BED82C7E3745DB'
);

// Step 3: Wait for valid_after time
const validAfter = new Date(pendingWithdrawal.valid_after);
const now = new Date();

if (validAfter > now) {
  const waitMs = validAfter.getTime() - now.getTime();
  console.log(`Waiting ${waitMs / 1000} seconds for delay period...`);
  await new Promise(resolve => setTimeout(resolve, waitMs + 1000));
}

// Step 4: Execute the withdrawal
const executionTxHash = await sdk.crypto.transfer.makeErc20TransferFinal(
  pendingWithdrawal.call_data
);

console.log('Withdrawal executed:', executionTxHash);

Using ZeroDev SDK (Direct)

For partners who prefer direct blockchain interaction without the Wirex SDK:

import { getCustomNonceKeyFromString } from '@zerodev/sdk';
import { encodeFunctionData, erc20Abi, keccak256, parseUnits } from 'viem';

const unifiedTokenAbi = [{
  name: 'withdraw',
  type: 'function',
  inputs: [{ type: 'uint256', name: 'amount' }],
  outputs: [],
  stateMutability: 'nonpayable'
}];

const executionDelayPolicyAbi = [{
  name: 'request',
  type: 'function',
  inputs: [
    { type: 'address', name: 'wallet' },
    { type: 'uint256', name: 'nonce' },
    { type: 'bytes', name: 'callData' }
  ],
  outputs: [{ type: 'uint48' }]
}];

interface WithdrawParams {
  unifiedTokenAddress: `0x${string}`;  // WUSD/WEUR
  baseTokenAddress: `0x${string}`;     // USDC/EURC
  recipientAddress: `0x${string}`;
  amount: string;
  unifiedDecimals: number;  // 18
  baseDecimals: number;     // 6
}

// Phase 1: Create withdrawal request
async function createWithdrawalRequest(
  smartWalletClient: any,
  executionDelayPolicyAddress: `0x${string}`,
  params: WithdrawParams
) {
  // Encode unwrap call (WUSD → USDC)
  const unwrapCallData = encodeFunctionData({
    abi: unifiedTokenAbi,
    functionName: 'withdraw',
    args: [parseUnits(params.amount, params.unifiedDecimals)],
  });

  // Encode transfer call
  const transferCallData = encodeFunctionData({
    abi: erc20Abi,
    functionName: 'transfer',
    args: [
      params.recipientAddress,
      parseUnits(params.amount, params.baseDecimals)
    ],
  });

  // Batch both calls
  const calls = [
    { to: params.unifiedTokenAddress, value: 0n, data: unwrapCallData },
    { to: params.baseTokenAddress, value: 0n, data: transferCallData }
  ];

  const encodedCalls = await smartWalletClient.account.encodeCalls(calls);

  // Generate custom nonce for this operation
  const nonceKey = keccak256(encodedCalls);
  const customNonce = getCustomNonceKeyFromString(nonceKey, '0.7');
  const nonce = await smartWalletClient.account.getNonce({ key: customNonce });

  // Create delay policy request
  const requestCallData = encodeFunctionData({
    abi: executionDelayPolicyAbi,
    functionName: 'request',
    args: [smartWalletClient.account.address, nonce, encodedCalls],
  });

  const requestCalls = await smartWalletClient.account.encodeCalls([{
    to: executionDelayPolicyAddress,
    value: 0n,
    data: requestCallData,
  }]);

  const { hash } = await smartWalletClient.sendUserOperation({
    callData: requestCalls
  });

  return {
    txHash: hash,
    callData: encodedCalls,
    validAfter: new Date(Date.now() + 3 * 1000) // 3 seconds from now
  };
}

// Phase 2: Execute after delay
async function executeWithdrawal(
  smartWalletClient: any,
  callData: `0x${string}`
) {
  const nonceKey = keccak256(callData);
  const customNonce = getCustomNonceKeyFromString(nonceKey, '0.7');
  const nonce = await smartWalletClient.account.getNonce({ key: customNonce });

  const { hash } = await smartWalletClient.sendUserOperation({
    callData: callData,
    nonce: nonce
  });

  return hash;
}

Querying Pending Withdrawals

The Wirex backend monitors on-chain withdrawal requests and provides an API to retrieve them. This helps partners visualize pending requests without maintaining their own blockchain indexer.

GET /api/v1/withdrawal/requests

Headers (S2S example):

  • Authorization: Bearer {access_token}
  • X-User-Address: {user_eoa_address}
  • X-Chain-Id: {chain_id}

For other authentication methods, see Authentication.

Response:

{
  "data": [
    {
      "account_address": "0xA7E41d5680dE394EaA2ed417169DFf56840Fb3EE",
      "token_address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
      "to_address": "0xF4D2A0E30C7983EF65DC0DD938BED82C7E3745DB",
      "amount": 100.0,
      "valid_after": "2024-01-01T12:05:00.000Z",
      "hash": "0x8A7B6C5D4E3F2A1B0C9D8E7F6A5B4C3D2E1F0A9B8C7D6E5F4A3B2C1D0E9F8171",
      "call_data": "3F7D9E1C5A2B804D6E93F7C1A0B5D8E29F4A3B7C6D5E0F1A2B3C4D5E6F7A8B9..."
    }
  ]
}
FieldTypeDescription
account_addressstringUser's Smart Wallet address
token_addressstringBase token being withdrawn (USDC/EURC)
to_addressstringDestination address
amountnumberWithdrawal amount
valid_afterstring (ISO 8601)When withdrawal can be executed
hashstringUnique hash for this request
call_datastringEncoded calldata for execution

Webhook Notification

When withdrawal request is created, Wirex sends a webhook with the pending request details:

Endpoint: POST {your_webhook_base_url}/v2/webhooks/erc-withdrawals

{
  "account_address": "0xA7E41d5680dE394EaA2ed417169DFf56840Fb3EE",
  "token_address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
  "to_address": "0xF4D2A0E30C7983EF65DC0DD938BED82C7E3745DB",
  "amount": 100.0,
  "valid_after": "2024-01-01T12:05:00.000Z",
  "valid_before": "2024-01-02T12:05:00.000Z",
  "hash": "0x8A7B6C5D4E3F2A1B0C9D8E7F6A5B4C3D2E1F0A9B8C7D6E5F4A3B2C1D0E9F8171",
  "call_data": "3F7D9E1C5A2B804D6E93F7C1A0B5D8E29F4A3B7C6D5E0F1A2B3C4D5E6F7A8B9..."
}

Use this webhook to update your UI showing pending withdrawals.


Token Addresses

See Unified Balance for token addresses and decimals.

TokenTypeDecimals
WUSDUnified (source)18
WEURUnified (source)18
USDCBase (destination)6
EURCBase (destination)6