提现流程
概述
DF 提现采用两步流程:
- 后端签名 — 调用
POST /spot/withdraw/request。后端预留资金(在spot_balances中冻结),并签署 EIP-712 类型化数据SpotReleaseFunds消息。 - 用户提交链上交易 — 调用方将返回的
signature、deadline和amount传入 BSC 上的ZtdxSpotVault.withdraw()。金库验证签名后将 DF 转账给调用方。
在第二步完成之前,资金不会离开金库。用户可在 deadline (保留) 窗口内自行决定何时(或是否)执行链上步骤。
生命周期
1. POST /spot/withdraw/request
──────────────▶ 后端
│(原子数据库事务)
├─ spot_balances: available -= amount, frozen += amount
└─ spot_withdrawals: status = "signed",存储 nonce + signature
│
▼ 返回 { id, signature, nonce, deadline, vault_address, ... }
2. 用户提交链上交易
──────────────▶ ZtdxSpotVault.withdraw(token, amount, deadline, signature)
│ 金库验证 EIP-712 签名
│ 金库将 DF 转账给 msg.sender
└─ 触发 SpotWithdrawal(account, token, amount)
3. 后端监听器(≥ 20 个确认)
├─ spot_withdrawals.status = "confirmed"
└─ spot_balances.frozen -= amount
若用户在 deadline (保留) 之前未提交链上交易,过期回收任务将把该记录标记为 expired 并将资金返还至 available。详见下方 过期回收任务。
EIP-712 域与主类型
后端使用以下 EIP-712 类型化数据域进行签名:
| 字段 | 值 |
|---|---|
name | "ZTDX Spot Vault" |
version | "1" |
chainId | 97(BSC 测试网) |
verifyingContract | 0x4Fe0b354c5865ee9deb979a99030d757ae47664a |
主类型 — SpotReleaseFunds:
struct SpotReleaseFunds {
address account; // 调用方地址(链上的 msg.sender)
address token; // DF 代币地址
uint256 value; // 以 wei 为单位的金额(DF 有 18 位小数)
uint256 nonce; // 每(账户、链)单调递增的 nonce
uint256 deadline; // Unix 秒;交易必须在此时间前执行
}
account 字段绑定至已认证用户的地址。第三方无法将该签名重放到不同的收款人——金库会校验 msg.sender == account。
金库 ABI(withdraw 函数)
function withdraw(
address token, // DF 代币地址
uint256 amount, // wei
uint256 deadline, // Unix 秒(来自响应)
bytes calldata signature // 65 字节、0x 前缀的 EIP-712 签名(来自响应)
) external;
提现状态
status | 含义 |
|---|---|
signed | 后端已签署 EIP-712 类型化数据释放授权(已签名)。资金已冻结,等待链上提交。 |
confirmed | 已观测到链上 SpotWithdrawal 事件(≥ 20 个 BSC 确认,已确认)。资金已离开金库。 |
expired | 签名 deadline (保留) 在链上提交前已过期(已过期)。资金已返还至 available。 |
另见:枚举 → 提现状态。
过期回收任务
后台回收任务扫描 deadline (保留) 已过期的 signed 状态提现记录。发现此类记录后:
- 将
spot_withdrawals.status设置为expired。 - 将
spot_balances.frozen减去对应金额,同时将spot_balances.available增加相同金额。
回收任务的执行间隔由 SPOT_WITHDRAW_NONCE_TTL_SECS 控制(当前为 86 400 秒 = 24 小时)。deadline (保留) 已过期的签名在链上也会被金库拒绝,因此 expired 状态的记录在回收任务运行之前同样无法提交。
代码示例:完整端到端提现
import time
import requests
from web3 import Web3
BASE_URL = "https://api-sepolia.p99.world/api/v1"
JWT = "your_jwt_token"
BSC_RPC = "https://data-seed-prebsc-1-s1.binance.org:8545"
PRIVATE_KEY = "0x..." # 调用方私钥
DF_TOKEN = Web3.to_checksum_address("0x8063a43ed88397c1B10DA23dcC60ba1E7A0Bf555")
DF_DECIMALS = 18
# ── 第一步:申请提现签名 ───────────────────────────────────────────────────────
resp = requests.post(
f"{BASE_URL}/spot/withdraw/request",
headers={"Authorization": f"Bearer {JWT}", "Content-Type": "application/json"},
json={"token": "DF", "amount": "100"},
timeout=5,
)
resp.raise_for_status()
sig_data = resp.json()
withdrawal_id = sig_data["id"]
signature = sig_data["signature"]
deadline = sig_data["deadline"]
vault_address = Web3.to_checksum_address(sig_data["vault_address"])
amount_decimal = sig_data["amount"]
print(f"提现 ID : {withdrawal_id}")
print(f"截止时间 : {deadline} (约 {deadline - int(time.time())} 秒后过期)")
# ── 第二步:调用链上 vault.withdraw() ─────────────────────────────────────────
VAULT_ABI = [
{
"name": "withdraw",
"type": "function",
"inputs": [
{"name": "token", "type": "address"},
{"name": "amount", "type": "uint256"},
{"name": "deadline", "type": "uint256"},
{"name": "signature", "type": "bytes"},
],
"outputs": [],
"stateMutability": "nonpayable",
}
]
w3 = Web3(Web3.HTTPProvider(BSC_RPC))
account = w3.eth.account.from_key(PRIVATE_KEY)
vault = w3.eth.contract(address=vault_address, abi=VAULT_ABI)
amount_wei = int(float(amount_decimal) * 10 ** DF_DECIMALS)
tx = vault.functions.withdraw(
DF_TOKEN,
amount_wei,
deadline,
bytes.fromhex(signature.removeprefix("0x")),
).build_transaction({
"from": account.address,
"nonce": w3.eth.get_transaction_count(account.address),
"gas": 200_000,
"gasPrice": w3.to_wei("5", "gwei"),
"chainId": 97,
})
signed_tx = w3.eth.account.sign_transaction(tx, PRIVATE_KEY)
tx_hash = w3.eth.send_raw_transaction(signed_tx.rawTransaction)
print(f"链上交易 : {tx_hash.hex()}")
# ── 第三步:轮询 GET /spot/withdrawals/:id 直至确认 ──────────────────────────
for attempt in range(60): # 最多轮询 5 分钟
row = requests.get(
f"{BASE_URL}/spot/withdrawals/{withdrawal_id}",
headers={"Authorization": f"Bearer {JWT}"},
timeout=5,
).json()
print(f" 第 {attempt + 1}/60 次尝试 — status={row['status']}")
if row["status"] in ("confirmed", "expired"):
break
time.sleep(5)
if row["status"] == "confirmed":
print(f"提现已确认!block={row['block_number']} tx={row['tx_hash']}")
else:
print(f"提现最终状态:status={row['status']}")
注意:
amount_wei必须与后端签名时使用的 wei 精确一致(即amount_decimal * 10**18)。任何不一致均会导致金库拒绝该签名。
另见:POST /spot/withdraw/request、GET /spot/withdrawals、GET /spot/withdrawals/:id、通用信息。