充值与提现 (Deposit & Withdraw)
ZTDX 采用链上结算机制,充值和提现均通过智能合约完成,确保资金安全和透明。
概述
充值流程
1. 准备充值
↓
2. 向 Vault 合约转账
↓
3. 等待区块确认(2-5个区块)
↓
4. 余额自动更新
提现流程
1. 发起提现请求(获取后端签名)
↓
2. 调用 Vault 合约的 withdraw() 方法
↓
3. 等待交易确认
↓
4. 资产转入钱包
充值 (Deposit)
准备充值
获取充值所需的合约地址和参数信息。
接口信息
- Method:
POST - Path:
/api/v1/deposit/prepare - Authentication: 需要 JWT Token
- Content-Type:
application/json
请求参数
| 参数 | 类型 | 必须 | 描述 |
|---|---|---|---|
| token | string | 是 | 资产名称(如 USDT) |
| amount | string | 是 | 充值数量(Decimal 字符串) |
请求示例
{
"token": "USDT",
"amount": "1000.00"
}
响应示例
{
"vault_address": "0x1234567890abcdef1234567890abcdef12345678",
"token_address": "0xdac17f958d2ee523a2206206994597c13d831ec7",
"token_symbol": "USDT",
"token_decimals": 6,
"amount": "1000.00",
"amount_wei": "1000000000",
"estimated_gas": 100000,
"gas_price_gwei": "0.1"
}
响应字段说明
| 字段 | 类型 | 描述 |
|---|---|---|
| vault_address | string | Vault 合约地址(接收地址) |
| token_address | string | 代币合约地址 |
| token_symbol | string | 代币符号 |
| token_decimals | number | 代币小数位数 |
| amount | string | 充值数量(Decimal 字符串) |
| amount_wei | string | 充值数量(Wei,最小单位) |
| estimated_gas | number | 预估 Gas 用量 |
| gas_price_gwei | string | 当前 Gas 价格(Gwei) |
前端充值示例
async function deposit(token, amount) {
// 1. 准备充值
const prepareResponse = await fetch('/api/v1/deposit/prepare', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${jwtToken}`
},
body: JSON.stringify({ token, amount })
});
const {
vault_address,
token_address,
amount_wei
} = await prepareResponse.json();
// 2. 调用 ERC20 合约的 transfer 方法
const tokenContract = new ethers.Contract(
token_address,
['function transfer(address to, uint256 amount) returns (bool)'],
signer
);
const tx = await tokenContract.transfer(vault_address, amount_wei);
// 3. 等待确认
await tx.wait();
console.log('充值成功!交易哈希:', tx.hash);
return tx.hash;
}
充值历史
查询用户的充值记录。
接口信息
- Method:
GET - Path:
/api/v1/deposit/history - Authentication: 需要 JWT Token
Query 参数
| 参数 | 类型 | 必须 | 描述 |
|---|---|---|---|
| limit | number | 否 | 返回数量(默认 50) |
| offset | number | 否 | 偏移量(分页) |
响应示例
{
"deposits": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"user_address": "0x742d35cc6634c0532925a3b844bc9e7595f0beb",
"token": "USDT",
"token_address": "0xdac17f958d2ee523a2206206994597c13d831ec7",
"amount": "1000.00",
"tx_hash": "0xabc123def456...",
"block_number": 12345678,
"confirmations": 5,
"status": "completed",
"created_at": 1704067200000,
"confirmed_at": 1704067500000
}
],
"total": 1,
"limit": 50,
"offset": 0
}
Deposit 对象字段
| 字段 | 类型 | 描述 |
|---|---|---|
| id | string | 充值记录 UUID |
| user_address | string | 用户地址 |
| token | string | 资产符号 |
| token_address | string | 代币合约地址 |
| amount | string | 充值数量(Decimal 字符串) |
| tx_hash | string | 交易哈希 |
| block_number | number | 区块高度 |
| confirmations | number | 确认数 |
| status | string | 状态(见下表) |
| created_at | number | 创建时间(毫秒级时间戳) |
| confirmed_at | number | null | 确认时间(毫秒级时间戳) |
充值状态
| 状态 | 说明 | 可操作 |
|---|---|---|
| pending | 待确认(等待区块确认) | 否 |
| completed | 已完成(余额已到账) | 否 |
| failed | 失败(交易回滚) | 否 |
提现 (Withdraw)
发起提现请求
创建提现请求并获取后端签名,用于调用链上合约。
接口信息
- Method:
POST - Path:
/api/v1/withdraw/request - Authentication: 需要 JWT Token
- Content-Type:
application/json
请求参数
| 参数 | 类型 | 必须 | 描述 |
|---|---|---|---|
| token | string | 是 | 资产名称(如 USDT) |
| amount | string | 是 | 提现数量(Decimal 字符串) |
请求示例
{
"token": "USDT",
"amount": "500.00"
}
响应示例
{
"id": "660e8400-e29b-41d4-a716-446655440000",
"user_address": "0x742d35cc6634c0532925a3b844bc9e7595f0beb",
"token": "USDT",
"token_address": "0xdac17f958d2ee523a2206206994597c13d831ec7",
"amount": "500.00",
"amount_wei": "500000000",
"nonce": 1,
"expiry": 1704153600,
"backend_signature": "0xabcdef123456...",
"vault_address": "0x1234567890abcdef...",
"status": "pending",
"created_at": 1704067200000
}
响应字段说明
| 字段 | 类型 | 描述 |
|---|---|---|
| id | string | 提现请求 UUID |
| user_address | string | 用户地址 |
| token | string | 资产符号 |
| token_address | string | 代币合约地址 |
| amount | string | 提现数量(Decimal 字符串) |
| amount_wei | string | 提现数量(Wei) |
| nonce | number | 用户的提现 nonce(链上验证) |
| expiry | number | 签名过期时间(秒级时间戳,通常 24 小时后) |
| backend_signature | string | 后端签名(用于合约验证) |
| vault_address | string | Vault 合约地址 |
| status | string | 提现状态 |
| created_at | number | 创建时间(毫秒级时间戳) |
前端提现示例
async function withdraw(token, amount) {
// 1. 发起提现请求
const response = await fetch('/api/v1/withdraw/request', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${jwtToken}`
},
body: JSON.stringify({ token, amount })
});
const {
id: withdrawId,
amount_wei,
nonce,
expiry,
backend_signature,
vault_address,
token_address
} = await response.json();
// 2. 调用 Vault 合约的 withdraw 方法
const vaultContract = new ethers.Contract(
vault_address,
[
'function withdraw(address token, uint256 amount, uint256 nonce, uint256 expiry, bytes signature) external'
],
signer
);
const tx = await vaultContract.withdraw(
token_address,
amount_wei,
nonce,
expiry,
backend_signature
);
// 3. 等待交易确认
await tx.wait();
// 4. 通知后端交易已确认
await fetch(`/api/v1/withdraw/${withdrawId}/confirm`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${jwtToken}`
},
body: JSON.stringify({ tx_hash: tx.hash })
});
return tx.hash;
}
查询提现详情
查询单个提现请求的详细信息。
接口信息
- Method:
GET - Path:
/api/v1/withdraw/:id - Authentication: 需要 JWT Token
路径参数
| 参数 | 类型 | 必须 | 描述 |
|---|---|---|---|
| id | string | 是 | 提现记录 UUID |
响应示例
{
"id": "660e8400-e29b-41d4-a716-446655440000",
"user_address": "0x742d35cc6634c0532925a3b844bc9e7595f0beb",
"token": "USDT",
"token_address": "0xdac17f958d2ee523a2206206994597c13d831ec7",
"amount": "500.00",
"amount_wei": "500000000",
"nonce": 1,
"expiry": 1704153600,
"backend_signature": "0xabcdef123456...",
"tx_hash": "0xdef456abc123...",
"status": "completed",
"created_at": 1704067200000,
"confirmed_at": 1704067500000
}
提现历史
查询用户的提现记录列表。
接口信息
- Method:
GET - Path:
/api/v1/withdraw/history - Authentication: 需要 JWT Token
Query 参数
| 参数 | 类型 | 必须 | 描述 |
|---|---|---|---|
| limit | number | 否 | 返回数量(默认 50) |
| offset | number | 否 | 偏移量(分页) |
| status | string | 否 | 过滤状态(可选) |
响应示例
{
"withdrawals": [
{
"id": "660e8400-e29b-41d4-a716-446655440000",
"user_address": "0x742d35cc6634c0532925a3b844bc9e7595f0beb",
"token": "USDT",
"amount": "500.00",
"nonce": 1,
"expiry": 1704153600,
"backend_signature": "0xabcdef...",
"tx_hash": "0xdef456...",
"status": "completed",
"created_at": 1704067200000,
"confirmed_at": 1704067500000
}
],
"total": 1,
"limit": 50,
"offset": 0
}
取消提现
取消尚未确认的提现请求。只能取消状态为 pending 的提现。
接口信息
- Method:
DELETE - Path:
/api/v1/withdraw/:id/cancel - Authentication: 需要 JWT Token
路径参数
| 参数 | 类型 | 必须 | 描述 |
|---|---|---|---|
| id | string | 是 | 提现记录 UUID |
响应示例
{
"success": true,
"message": "提现已取消,资金已退回",
"withdrawal": {
"id": "660e8400-e29b-41d4-a716-446655440000",
"status": "cancelled",
"cancelled_at": 1704067300000
}
}
注意
- 只能取消状态为
pending的提现 - 已经在链上确认的提现无法取消
- 签名过期的提现会 自动取消
确认提现
提现交易上链后,通知后端更新状态。
接口信息
- Method:
POST - Path:
/api/v1/withdraw/:id/confirm - Authentication: 需要 JWT Token
- Content-Type:
application/json
路径参数
| 参数 | 类型 | 必须 | 描述 |
|---|---|---|---|
| id | string | 是 | 提现记录 UUID |
请求参数
| 参数 | 类型 | 必须 | 描述 |
|---|---|---|---|
| tx_hash | string | 是 | 链上交易哈希 |
请求示例
{
"tx_hash": "0xdef456abc123789..."
}
响应示例
{
"success": true,
"message": "提现已确认",
"withdrawal": {
"id": "660e8400-e29b-41d4-a716-446655440000",
"tx_hash": "0xdef456abc123789...",
"status": "processing",
"confirmed_at": 1704067500000
}
}
提现状态说明
| 状态 | 描述 | 可取消 |
|---|---|---|
| pending | 等待用户调用合约 | ✅ 是 |
| processing | 交易已提交,等待确认 | ❌ 否 |
| completed | 提现成功 | ❌ 否 |
| failed | 提现失败(资金退回) | ❌ 否 |
| cancelled | 已取消 | ❌ 否 |
| expired | 签名已过期(资金退回) | ❌ 否 |
智能合约 ABI
Vault 合约
deposit
// 用户无需直接调用,只需向 Vault 转账即可
withdraw
function withdraw(
address token,
uint256 amount,
uint256 nonce,
uint256 expiry,
bytes calldata signature
) external
参数说明:
token: 代币合约地址amount: 提现数量(Wei)nonce: 用户的提现 nonceexpiry: 签名过期时间signature: 后端签名
限制和费用
充值
- 最小充值: 10 USDT
- Gas 费用: 由用户支付(通常 0.1-0.5 USD)
- 确认时间: 2-5 个区块(约 30-150 秒)
- 限制: 无充值次数限制
提现
- 最小提现: 10 USDT
- Gas 费用: 由用户支付
- 签名有效期: 24 小时
- 限制: 每24小时最多 10 次提现
最佳实践
1. 检查余额
// 提现前检查可用余额
const { available } = await getBalance('USDT');
if (parseFloat(available) < parseFloat(withdrawAmount)) {
alert('可用余额不足');
return;
}
2. 处理签名过期
// 检查签名是否过期
const now = Math.floor(Date.now() / 1000);
if (withdrawal.expiry < now) {
// 取消过期的提现
await cancelWithdrawal(withdrawal.id);
// 重新发起提现
const newWithdrawal = await requestWithdraw(token, amount);
}
3. 监控交易状态
// 等待交易确认并更新状态
const tx = await vaultContract.withdraw(...);
// 监听事件
tx.wait().then(receipt => {
console.log('提现成功!');
confirmWithdrawal(withdrawalId, tx.hash);
}).catch(error => {
console.error('提现失败:', error);
// 资金会自动退回
});
4. 错误处理
try {
const txHash = await withdraw('USDT', '500');
alert('提现成功!交易哈希: ' + txHash);
} catch (error) {
if (error.code === 'INSUFFICIENT_BALANCE') {
alert('余额不足');
} else if (error.code === 'SIGNATURE_EXPIRED') {
alert('签名已过期,请重新发起提现');
} else if (error.code === 'USER_REJECTED') {
alert('用户取消了交易');
} else {
alert('提现失败: ' + error.message);
}
}
常见问题
Q: 充值需要多久到账?
A: 通常需要 2-5 个区块确认,约 30-150 秒。Arbitrum 网络速度较快,一般 1 分钟内即可到账。
Q: 为什么提现需 要后端签名?
A: 后端签名用于验证提现请求的合法性,防止未授权提现。这是混合模式(链下订单 + 链上结算)的安全保障。
Q: 签名过期后资金会丢失吗?
A: 不会。签名过期后,提现状态会自动变为 expired,资金会退回到您的可用余额。
Q: 可以取消已提交的提现吗?
A: 如果提现状态为 pending(尚未链上确认),可以取消。一旦链上确认(processing 或 completed),则无法取消。
Q: Gas 费用如何计算?
A: Gas 费用由链上网络决定。通常:
- 充值(ERC20 transfer): ~100,000 gas
- 提现(Vault withdraw): ~150,000 gas
- Arbitrum 上 gas 费用通常低于 1 USD