跳到主要内容

申购与赎回

申购流程

申购过程涉及后端 API 调用和链上合约交互:

┌──────────┐ ① GET /earn/products ┌──────────────┐
│ 前端/用户 │ ───────────────────────▶ │ 公开接口 │
│ │ ◀─────────────────────── │ │
│ │ 产品列表 └──────────────┘
│ │
│ │ ② POST /earn/subscribe/prepare
│ │ ───────────────────────▶ ┌──────────────┐
│ │ ◀─────────────────────── │ 受保护接口 │
│ │ EIP-712 签名 └──────────────┘
│ │
│ │ ③ approve USDT
│ │ ───────────────────────▶ ┌──────────────┐
│ │ │ USDT 合约 │
│ │ ④ 调用 subscribe() └──────────────┘
│ │ ───────────────────────▶ ┌──────────────┐
│ │ │ Earn 合约 │
└──────────┘ └──────┬───────┘
│ ⑤ Subscribed 事件

┌──────────────┐
│ 后端服务 │
│ 创建申购记录 │
│ 铸造 NFT │
└──────────────┘

第一步:获取 EIP-712 域信息

GET /earn/domain

响应

{
"types": {
"EIP712Domain": [
{ "name": "name", "type": "string" },
{ "name": "version", "type": "string" },
{ "name": "chainId", "type": "uint256" },
{ "name": "verifyingContract", "type": "address" }
],
"Subscribe": [
{ "name": "user", "type": "address" },
{ "name": "productId", "type": "uint256" },
{ "name": "amount", "type": "uint256" },
{ "name": "deadline", "type": "uint256" }
]
},
"primaryType": "Subscribe",
"domain": {
"name": "ZTDX Earn",
"version": "1",
"chainId": 421614,
"verifyingContract": "0x436dcB8a2478D636a6cC678AE7A2E4c5449cB3ba"
}
}

第二步:浏览产品

GET /earn/products

查询参数

参数类型必填说明
statusstring按状态过滤:created / subscribing / active / settled / ended / cancelled
pagenumber页码,默认 1
page_sizenumber每页条数,默认 20

示例

GET /earn/products?status=subscribing&page=1&page_size=10

响应

{
"products": [
{
"id": "123204f6-5bde-4434-890a-8ca241f66067",
"chain_product_id": 1769524310,
"name": "USDT Earn 900% APY",
"description": null,
"annual_rate": "900.00%",
"period_rate": "0.00%",
"annual_rate_bps": 90000,
"period_rate_bps": 0,
"duration_seconds": 300,
"total_quota": "500000000.00",
"subscribed_amount": "500000000.00",
"available_quota": "0.00",
"subscription_rate": "100.00%",
"min_amount": "100000000.00",
"max_amount_per_user": "500000000.00",
"status": "subscribing",
"subscriber_count": 1,
"subscribe_start_time": "2026-01-27T14:33:00Z",
"subscribe_end_time": "2026-01-27T14:38:00Z",
"settle_time": "2026-01-27T14:43:00Z",
"is_subscribing": true,
"is_sold_out": false,
"time_remaining_seconds": 180
}
],
"total": 1,
"page": 1,
"page_size": 20
}

产品对象字段(ProductDetail

字段类型说明
idstring产品 UUID
chain_product_idnumber链上产品 ID
namestring产品名称
descriptionstring | null产品描述
annual_ratestring年化收益率(百分比字符串)
period_ratestring期间收益率(百分比字符串)
annual_rate_bpsnumber年化利率(基点,1% = 100)
period_rate_bpsnumber期间利率(基点)
duration_secondsnumber锁仓时长(秒)
total_quotastring总申购上限(Wei 格式)
subscribed_amountstring已申购总量(Wei 格式)
available_quotastring剩余可申购额度(Wei 格式)
subscription_ratestring申购进度百分比
min_amountstring单次最低申购额(Wei 格式)
max_amount_per_userstring单用户最高申购额(Wei 格式)
statusstring产品状态
subscriber_countnumber申购人数
subscribe_start_timestring申购开始时间(ISO 8601)
subscribe_end_timestring申购截止时间(ISO 8601)
settle_timestring结算时间(ISO 8601)
is_subscribingboolean当前是否在申购期
is_sold_outboolean是否已售罄
time_remaining_secondsnumber | null申购期剩余秒数(仅在申购期内有值)

第三步:获取产品详情

GET /earn/products/:id
参数类型说明
idstring产品 UUID 或 chain_product_id(整数字符串)

返回 ProductDetail 对象(字段同产品列表)。

第四步:申购准备(获取签名)

需要认证

此接口需在 HTTP Header 中携带有效 JWT Token:

Authorization: Bearer <JWT_TOKEN>
POST /earn/subscribe/prepare

请求体

字段类型必填说明
product_idstring产品 UUID
amountstring申购金额(人类可读 USDT,如 "100" = 100 USDT)
{
"product_id": "123204f6-5bde-4434-890a-8ca241f66067",
"amount": "100"
}

响应

字段类型说明
chain_product_idnumber链上产品 ID
amountstring申购金额(Wei 格式,用于合约调用)
deadlinenumber签名过期时间(Unix 时间戳,30 分钟后)
signaturestringEIP-712 后端签名
contract_addressstring理财合约地址
user_addressstring用户钱包地址
{
"chain_product_id": 1769524310,
"amount": "100000000",
"deadline": 1772006400,
"signature": "0x1a2b3c...",
"contract_address": "0x436dcB8a2478D636a6cC678AE7A2E4c5449cB3ba",
"user_address": "0x29F721B203A9fC9c5DDe35A739d8b8E0E4605489"
}

申购限制

条件错误码
产品非 subscribing 状态SUBSCRIPTION_CLOSED
当前时间不在申购窗口内SUBSCRIPTION_CLOSED
金额低于最低限额AMOUNT_TOO_LOW
超出单用户最高限额AMOUNT_TOO_HIGH
剩余额度不足QUOTA_EXCEEDED
金额格式无效INVALID_AMOUNT
金额为零或负数INVALID_AMOUNT

第五步:链上申购

获取签名后,客户端需执行两笔链上交易:

// 第一步:授权理财合约使用 USDT
await usdtContract.approve(contract_address, amount); // amount 为 Wei

// 第二步:调用 Earn 合约的 subscribe
await earnContract.subscribe(
chain_product_id, // 链上产品 ID
amount, // Wei 金额(来自响应)
deadline, // 来自响应
signature // 来自响应
);
利息计算示例

900% APY,锁仓 300 秒(≈5 分钟),申购 100 USDT:

利息 = 100 × 9.00 × (300 / 31536000) ≈ 0.0000855 USDT


赎回(领取)

产品进入 settled 状态后,用户可直接在链上领取本金 + 利息,无需调用后端接口。

领取本息

// 产品到期后领取(NFT 自动销毁)
await earnContract.claim(chain_product_id);

后端监听 Claimed 事件后自动更新申购记录:

  • nft_statusredeemed
  • claimedtrue
  • 记录 claim_tx_hash

紧急退款

当产品处于 cancelled 状态时,用户可取回本金:

// 产品取消时的紧急退款
await earnContract.emergencyClaim(chain_product_id);
备注

领取和紧急退款均为纯链上操作。后端通过事件监听自动同步状态(轮询间隔约 12 秒)。申购记录可能需要 12–30 秒才能反映最新状态。


错误码

错误码HTTP 状态码说明
SUBSCRIPTION_CLOSED400产品未处于申购期(状态或时间窗口)
AMOUNT_TOO_LOW400金额低于产品最低申购额
AMOUNT_TOO_HIGH400超出单用户最大申购限额
QUOTA_EXCEEDED400产品剩余额度不足
INVALID_AMOUNT400金额格式错误或为零/负数
PRODUCT_NOT_FOUND404产品 ID 不存在
UNAUTHORIZED401未认证或 Token 无效

错误响应格式

{
"error": "Product is not open for subscription",
"code": "SUBSCRIPTION_CLOSED"
}

代码示例

Python — Browse Products (Public)

import requests

BASE_URL = "https://api.ztdx.io"

# Get EIP-712 domain info
resp = requests.get(f"{BASE_URL}/earn/domain")
print("Domain:", resp.json())

# List subscribing products
resp = requests.get(f"{BASE_URL}/earn/products", params={"status": "subscribing", "page": 1, "page_size": 10})
data = resp.json()
for p in data["products"]:
print(f" {p['name']} — APY: {p['annual_rate']}, Quota: {int(p['available_quota']) / 1e6:.2f} USDT")

# Get product details
product_id = data["products"][0]["id"] if data["products"] else "example-uuid"
resp = requests.get(f"{BASE_URL}/earn/products/{product_id}")
print("Product detail:", resp.json())

Python — Prepare Subscription (JWT Required)

import requests

BASE_URL = "https://api.ztdx.io"
JWT_TOKEN = "your_jwt_token"

resp = requests.post(
f"{BASE_URL}/earn/subscribe/prepare",
headers={"Authorization": f"Bearer {JWT_TOKEN}", "Content-Type": "application/json"},
json={"product_id": "123204f6-5bde-4434-890a-8ca241f66067", "amount": "100"},
)
data = resp.json()
print(f"Chain Product ID: {data['chain_product_id']}")
print(f"Amount (Wei): {data['amount']}, Deadline: {data['deadline']}")
print(f"Signature: {data['signature']}")
# Use this signature to call earnContract.subscribe() on-chain

Error Codes

Error CodeHTTP StatusDescription
SUBSCRIPTION_CLOSED400Product not in subscription period (status or time window)
AMOUNT_TOO_LOW400Amount below product minimum
AMOUNT_TOO_HIGH400Exceeds per-user maximum limit
QUOTA_EXCEEDED400Insufficient remaining product quota
INVALID_AMOUNT400Invalid amount format, zero, or negative
PRODUCT_NOT_FOUND404Product ID does not exist
UNAUTHORIZED401Not authenticated or invalid token

Error Response Format

{
"error": "Product is not open for subscription",
"code": "SUBSCRIPTION_CLOSED"
}