提现
DF 提现是两步流程:后台先签发 EIP-712 释放授权,再由用户拿这个签名上链调用金库合约把代币提走。
1. POST /spot/withdraw/request
───────────────▶ 后台
│
▼ (原子)
spot_balances: available -= amount, frozen += amount
spot_withdrawals: status = "signed",落库签名 + nonce
│
▼ 返回 { signature, nonce, deadline, vault_address, ... }
2. 用户上链提交
user.wallet.signAndSend(ZtdxSpotVault.withdraw(token, amount, deadline, signature))
│
▼ 金库验签后转出代币
SpotWithdrawal 事件
│
▼ 后台监听器(≥ 20 确认)
spot_withdrawals.status = "confirmed"
spot_balances.frozen -= amount
如果用户在 SPOT_WITHDRAW_NONCE_TTL_SECS(当前 24 小时)内没有上链,回收任务会把这条记录标记为 expired,把冻结金额退回到 available。此时签名也已超过 deadline,链上无法再使用。
状态值
status | 含义 |
|---|---|
signed | 后台已签发释放授权;资金处于冻结,等待上链。 |
confirmed | 已观察到链上 SpotWithdrawal 事件,资金已离开金库。 |
expired | 在 deadline 前未上链;冻结金额已退回 available。 |
申请提现签名
冻结金额、签发释放授权,并返回用户上链需要的全部数据。
需要鉴权
仅支持 JWT。API Key 调用会返回 403 API Key permission denied。
Authorization: Bearer <JWT>
POST /spot/withdraw/request
请求体
{ "token": "DF", "amount": "100" }
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
token | string | 是 | MVP 仅支持 DF。 |
amount | string / number | 是 | 十进制金额,必须 ≥ SPOT_WITHDRAW_MIN_AMOUNT_DF(当前 1)。 |
响应 — 200 OK
{
"id": "9f2a-...-uuid",
"nonce": 42,
"signature": "0x...65 字节 hex...",
"deadline": 1778402000,
"vault_address": "0x4Fe0b354c5865ee9deb979a99030d757ae47664a",
"chain_id": 97,
"amount": "100"
}
| 字段 | 类型 | 说明 |
|---|---|---|
id | string | 提现记录 UUID(可用于 GET /spot/withdrawals/:id)。 |
nonce | number | 按 (用户, 链) 维度单调递增的 nonce,防重放。 |
signature | string | 65 字节、0x 前缀的 EIP-712 签名。直接传给链上 withdraw() 即可。 |
deadline | number | Unix 秒。链上调用必须严格在此之前完成。 |
vault_address | string | 调用 withdraw() 的金库合约地址。 |
chain_id | number | 签名作用的网络(97 = BSC Testnet )。 |
amount | string | 回显请求的金额。 |
错误响应
| HTTP | error | 原因 |
|---|---|---|
400 | unsupported token: <symbol> | 提现了 DF 之外的代币。 |
400 | amount below minimum 1 | 低于 SPOT_WITHDRAW_MIN_AMOUNT_DF。 |
400 | insufficient balance | 现货 available < amount。 |
400 | invalid user address | 鉴权后的地址解析失败(实际场景几乎不会出现)。 |
403 | API Key permission denied: withdraws not allowed | 调用方使用了 API Key。 |
503 | spot subsystem disabled | 服务端 SPOT_ENABLED=false。 |
500 | internal / sign failed / signer unavailable | 服务端错误。 |
链上提交
用户拿响应里的字段调用金库合约的 withdraw:
function withdraw(
address token, // DF 代币地址
uint256 amount, // wei(DF 18 位精度)
uint256 deadline, // 来自响应,Unix 秒
bytes calldata signature
) external;
金库会按下面的 EIP-712 类型化数据验签:
SpotReleaseFunds(address account, address token, uint256 value, uint256 nonce, uint256 deadline)
Domain 为 ("ZTDX Spot Vault", "1", chainId=97, verifyingContract=vaultAddress)。account 字段就等于调用方 msg.sender,所以第三方无法把签名重放到别的收款人头上。
查询我的提现记录
GET /spot/withdrawals
Query 参数
| 参数 | 类型 | 默认 | 说明 |
|---|---|---|---|
status | string | (全部) | 过滤:signed / confirmed / expired。 |
limit | number | 50 | 1–200,服务端 clamp。 |
响应 — 200 OK
[
{
"id": "9f2a-...-uuid",
"token": "DF",
"amount": "100",
"fee": "0",
"chain_id": 97,
"nonce": 42,
"status": "confirmed",
"deadline": 1778402000,
"tx_hash": "0xabc...",
"block_number": 106400000,
"requested_at": 1778315530,
"confirmed_at": 1778316040
}
]
| 字段 | 类型 | 说明 |
|---|---|---|
id | string | 提现记录 UUID。 |
token | string | MVP 阶段恒为 DF。 |
amount | string | 申请金额。 |
fee | string | 提现手续费(当前恒为 0)。 |
chain_id | number | 目标链。 |
nonce | number | EIP-712 nonce。 |
status | string | signed / confirmed / expired。 |
deadline | number | Unix 秒 — 签名有效期。 |
tx_hash | string | null | confirmed 后填充。 |
block_number | number | null | confirmed 后填充。 |
requested_at | number | Unix 秒 — 后台签发时间。 |
confirmed_at | number | null | Unix 秒 — 监听器观察到链上事件的时间。 |
查询单笔提现
GET /spot/withdrawals/:id
返回的 WithdrawalView 字段同上,按当前用户 + id 查询。如果该 id 不属于调用方,返回 404 not found。
代码示例
import requests
BASE = "https://api-sepolia.p99.world/api/v1"
JWT = "your_jwt_token"
# 1. 冻结 + 签名
sig = requests.post(
f"{BASE}/spot/withdraw/request",
headers={"Authorization": f"Bearer {JWT}", "Content-Type": "application/json"},
json={"token": "DF", "amount": "100"},
).json()
print(sig)
# { "id": "...", "nonce": 42, "signature": "0x...", "deadline": 1778402000, ... }
# 2. 链上提交(伪代码;实际用 ethers / web3.py)
# vault.withdraw(DF_TOKEN, parse_wei(sig["amount"], 18), sig["deadline"], sig["signature"])
# 3. 轮询状态
import time
for _ in range(60):
row = requests.get(
f"{BASE}/spot/withdrawals/{sig['id']}",
headers={"Authorization": f"Bearer {JWT}"},
).json()
print(row["status"])
if row["status"] in ("confirmed", "expired"):
break
time.sleep(5)
运维说明
- 每个用户在 每条链上拥有单调递增的 nonce。同一用户的并发提现会在余额行锁(
FOR UPDATE)上串行化。 fee字段当前恒为0;schema 已为后续手续费策略预留这一列。- MVP 不支持原生币(BNB)提现。