Withdrawals
Withdraw crypto from user wallets to external addresses using on-chain execution.
Before You Start
Read the following guides before proceeding:
| Guide | Why |
|---|---|
| Getting Started | Platform overview and setup |
| Api Basics | Required headers and request configuration |
| Authentication | How to obtain access tokens |
| Onboarding | User and wallet registration |
| ABI Reference | Smart 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
- App prepares withdrawal: unwrap unified token (WUSD/WEUR) + transfer base token (USDC/EURC)
- App calls
ExecutionDelayPolicy.request(wallet, nonce, callData) - Contract stores the operation with
valid_aftertimestamp (current time + delay) - Returns request transaction hash
Phase 2: Execute
- App waits until
valid_aftertimestamp passes - App submits the original callData with matching custom nonce
- Policy verifies delay has passed
- Transaction executes: unwrap + transfer
Delay Period
| Operation | Delay |
|---|---|
| Token transfers | 3 seconds |
| Account changes | 24 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..."
}
]
}| Field | Type | Description |
|---|---|---|
account_address | string | User's Smart Wallet address |
token_address | string | Base token being withdrawn (USDC/EURC) |
to_address | string | Destination address |
amount | number | Withdrawal amount |
valid_after | string (ISO 8601) | When withdrawal can be executed |
hash | string | Unique hash for this request |
call_data | string | Encoded 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.
| Token | Type | Decimals |
|---|---|---|
| WUSD | Unified (source) | 18 |
| WEUR | Unified (source) | 18 |
| USDC | Base (destination) | 6 |
| EURC | Base (destination) | 6 |
Updated 8 days ago
