跳到主要内容

提现

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 事件,资金已离开金库。
expireddeadline 前未上链;冻结金额已退回 available

申请提现签名

冻结金额、签发释放授权,并返回用户上链需要的全部数据。

需要鉴权

仅支持 JWT。API Key 调用会返回 403 API Key permission denied

Authorization: Bearer <JWT>
POST /spot/withdraw/request

请求体

{ "token": "DF", "amount": "100" }
字段类型必填说明
tokenstringMVP 仅支持 DF
amountstring / 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"
}
字段类型说明
idstring提现记录 UUID(可用于 GET /spot/withdrawals/:id)。
noncenumber按 (用户, 链) 维度单调递增的 nonce,防重放。
signaturestring65 字节、0x 前缀的 EIP-712 签名。直接传给链上 withdraw() 即可。
deadlinenumberUnix 秒。链上调用必须严格在此之前完成。
vault_addressstring调用 withdraw() 的金库合约地址。
chain_idnumber签名作用的网络(97 = BSC Testnet)。
amountstring回显请求的金额。

错误响应

HTTPerror原因
400unsupported token: <symbol>提现了 DF 之外的代币。
400amount below minimum 1低于 SPOT_WITHDRAW_MIN_AMOUNT_DF
400insufficient balance现货 available < amount
400invalid user address鉴权后的地址解析失败(实际场景几乎不会出现)。
403API Key permission denied: withdraws not allowed调用方使用了 API Key。
503spot subsystem disabled服务端 SPOT_ENABLED=false
500internal / 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 参数

参数类型默认说明
statusstring(全部)过滤:signed / confirmed / expired
limitnumber501–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
}
]
字段类型说明
idstring提现记录 UUID。
tokenstringMVP 阶段恒为 DF
amountstring申请金额。
feestring提现手续费(当前恒为 0)。
chain_idnumber目标链。
noncenumberEIP-712 nonce。
statusstringsigned / confirmed / expired
deadlinenumberUnix 秒 — 签名有效期。
tx_hashstring | nullconfirmed 后填充。
block_numbernumber | nullconfirmed 后填充。
requested_atnumberUnix 秒 — 后台签发时间。
confirmed_atnumber | nullUnix 秒 — 监听器观察到链上事件的时间。

查询单笔提现

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)提现。