跳到主要内容

链上申购与领取 (On-Chain Subscribe & Claim)

本文档介绍如何通过智能合约进行理财产品申购和到期领取。

合约信息

Testnet (Arbitrum Sepolia)

合约地址说明
AXBladeEarn v3.10xA40b4b726980C02F6558fcA5C0a52FaA2E5A0864✅ 当前使用 (修复短期利息计算)
AXBladeEarn v30x26763848400E108bd3095e548a6de2D01dadDCaB旧版本 (短期产品利息为0的bug)
AXBladeEarn v20xe97ec2cb6B735d791ee253f86343782d41F6FA0a旧版本 (durationDays)
Platform USDT0x572E474C3Cf364D085760784F938A1Aa397a8B9b平台统一 USDT,用户需授权给 Earn 合约

Chain ID: 421614 (Arbitrum Sepolia) 合约源码: https://github.com/AXBladeStudio/AXBlade/tree/main/contracts

USDT 授权: 用户申购前需将 USDT (0x572E...) 授权 (approve) 给 Earn 合约 (0xA40b...)

v3.1 版本修复 (2026-01-08)

修复说明
利息计算修复短期产品 (如5分钟) 利息为0的bug
直接计算interest = principal * annualRate * duration / (365 days * BPS)
移除字段合约删除 maxPeriodRateBps 字段,直接计算利息避免中间值截断

v3 版本特性

特性说明
durationSeconds锁定期改为秒,支持灵活时长 (如10分钟、1小时、7天)
动态 APRmaxAnnualRateBps 定义上限,实际利率可为 0% ~ max%
actualTotalInterestdepositReturns(productId, actualTotalInterest) - 指定实际利息
比例分配用户 claim 时按比例分配实际存入的利息

合约 ABI

// 使用 ethers-rs abigen! 生成
const EARN_ABI = [
// 查询函数
'function productCount() external view returns (uint256)',
'function getProductConfig(uint256 productId) external view returns (string name, uint256 annualRateBps, uint256 durationDays, uint256 periodRateBps, uint256 totalQuota, uint256 minAmount, uint256 maxAmountPerUser)',
'function getProductTiming(uint256 productId) external view returns (uint256 subscribeStartTime, uint256 subscribeEndTime, uint256 settleTime, uint256 actualSettleTime)',
'function getProductStats(uint256 productId) external view returns (uint256 subscribedAmount, uint256 totalInterestPaid, uint256 participantCount, uint8 status, address creator)',
'function getSubscription(uint256 productId, address user) external view returns (uint256 amount, uint256 expectedReturn, uint256 actualReturn, uint256 subscribedAt, bool claimed)',
'function balanceOf(address account, uint256 id) external view returns (uint256)',
'function domainSeparator() external view returns (bytes32)',

// 用户操作
'function subscribe(uint256 productId, uint256 amount, uint256 deadline, bytes signature) external',
'function claim(uint256 productId) external',
'function emergencyClaim(uint256 productId) external',

// 管理操作
'function createProduct(string name, uint256 annualRateBps, uint256 durationDays, uint256 totalQuota, uint256 minAmount, uint256 maxAmountPerUser, uint256 subscribeStartTime, uint256 subscribeEndTime) external returns (uint256)',
'function openSubscription(uint256 productId) external',
'function activateProduct(uint256 productId) external',
'function settleProduct(uint256 productId) external',

// 事件
'event ProductCreated(uint256 indexed productId, string name, uint256 annualRateBps, uint256 durationDays, uint256 totalQuota)',
'event ProductStatusChanged(uint256 indexed productId, uint8 oldStatus, uint8 newStatus)',
'event Subscribed(uint256 indexed productId, address indexed user, uint256 amount, uint256 expectedReturn)',
'event ProductSettled(uint256 indexed productId, uint256 totalPrincipal, uint256 totalInterest)',
'event Claimed(uint256 indexed productId, address indexed user, uint256 principal, uint256 interest)'
];

申购流程

Step 1: 获取申购签名

const API_BASE = 'https://api.axblade.io/api/v1';

async function getSubscribeSignature(jwtToken: string, productId: string, amount: string) {
const response = await fetch(`${API_BASE}/earn/subscribe/prepare`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${jwtToken}`
},
body: JSON.stringify({
product_id: productId,
amount: amount
})
});

return response.json();
}

响应示例:

{
"chain_product_id": 1,
"amount": "500000000",
"deadline": 1767766918,
"signature": "0x...",
"contract_address": "0xA40b4b726980C02F6558fcA5C0a52FaA2E5A0864",
"user_address": "0x6538469807e019e05c9ec4bd158b12afb1da50f3"
}

Step 2: 授权 USDT

import { ethers } from 'ethers';

// 主合约 (Platform USDT)
const USDT_ADDRESS = '0x572E474C3Cf364D085760784F938A1Aa397a8B9b';
const EARN_CONTRACT = '0xA40b4b726980C02F6558fcA5C0a52FaA2E5A0864'; // v3.1

async function approveUSDT(signer: ethers.Signer, amount: bigint) {
const usdt = new ethers.Contract(USDT_ADDRESS, [
'function approve(address,uint256) returns (bool)',
'function allowance(address,address) view returns (uint256)'
], signer);

const userAddress = await signer.getAddress();
const allowance = await usdt.allowance(userAddress, EARN_CONTRACT);

if (allowance < amount) {
const tx = await usdt.approve(EARN_CONTRACT, ethers.MaxUint256);
await tx.wait();
console.log('USDT approved');
}
}

Step 3: 调用合约申购

async function subscribe(
signer: ethers.Signer,
productId: number,
amount: bigint,
deadline: number,
signature: string
) {
const contract = new ethers.Contract(EARN_CONTRACT, [
'function subscribe(uint256,uint256,uint256,bytes) external'
], signer);

const tx = await contract.subscribe(productId, amount, deadline, signature);
const receipt = await tx.wait();

console.log('Subscribe successful:', receipt.hash);
return receipt;
}

完整申购示例

import { ethers } from 'ethers';

const API_BASE = 'https://api.8a27.xyz/api/v1';
// 主合约 (Platform USDT)
const USDT_ADDRESS = '0x572E474C3Cf364D085760784F938A1Aa397a8B9b';
const EARN_CONTRACT = '0xA40b4b726980C02F6558fcA5C0a52FaA2E5A0864'; // v3.1

async function subscribeToProduct(
signer: ethers.Signer,
jwtToken: string,
productId: string,
amountUsdt: string
) {
// 1. 获取申购签名
const signatureResponse = await fetch(`${API_BASE}/earn/subscribe/prepare`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${jwtToken}`
},
body: JSON.stringify({ product_id: productId, amount: amountUsdt })
});

if (!signatureResponse.ok) {
const error = await signatureResponse.json();
throw new Error(error.message || 'Failed to get signature');
}

const {
chain_product_id: chainProductId,
amount,
deadline,
signature,
contract_address
} = await signatureResponse.json();

// 2. 授权 USDT
const usdt = new ethers.Contract(USDT_ADDRESS, [
'function approve(address,uint256) returns (bool)',
'function allowance(address,address) view returns (uint256)'
], signer);

const userAddress = await signer.getAddress();
const amountBigInt = BigInt(amount);
const allowance = await usdt.allowance(userAddress, contract_address);

if (allowance < amountBigInt) {
console.log('Approving USDT...');
const approveTx = await usdt.approve(contract_address, ethers.MaxUint256);
await approveTx.wait();
}

// 3. 调用合约申购
const contract = new ethers.Contract(contract_address, [
'function subscribe(uint256,uint256,uint256,bytes) external'
], signer);

console.log('Subscribing...');
const tx = await contract.subscribe(chainProductId, amountBigInt, deadline, signature);
const receipt = await tx.wait();

console.log('Subscribe successful!');
console.log('Transaction:', receipt.hash);
console.log('Amount:', ethers.formatUnits(amount, 6), 'USDT');

return receipt;
}

领取流程

产品结算后,用户可以领取本金+利息。

普通领取 (claim)

async function claimReturns(signer: ethers.Signer, productId: number) {
const contract = new ethers.Contract(EARN_CONTRACT, [
'function claim(uint256) external',
'function getSubscription(uint256,address) view returns (uint256,uint256,uint256,uint256,bool)',
'function getProductStats(uint256) view returns (uint256,uint256,uint256,uint8,address)'
], signer);

const userAddress = await signer.getAddress();

// 检查产品状态 (status: 3 = Settled)
const [,,,status,] = await contract.getProductStats(productId);
if (status !== 3) {
throw new Error('Product not settled yet');
}

// 检查申购记录
const [amount, expectedReturn, actualReturn, subscribedAt, claimed] =
await contract.getSubscription(productId, userAddress);

if (amount === 0n) {
throw new Error('No subscription found');
}

if (claimed) {
throw new Error('Already claimed');
}

// 领取
const tx = await contract.claim(productId);
const receipt = await tx.wait();

console.log('Claim successful!');
console.log('Transaction:', receipt.hash);
console.log('Principal:', ethers.formatUnits(amount, 6), 'USDT');
console.log('Interest:', ethers.formatUnits(actualReturn - amount, 6), 'USDT');

return receipt;
}

紧急赎回 (emergencyClaim)

当产品被取消时,用户可以紧急赎回本金。

async function emergencyClaim(signer: ethers.Signer, productId: number) {
const contract = new ethers.Contract(EARN_CONTRACT, [
'function emergencyClaim(uint256) external',
'function getProductStats(uint256) view returns (uint256,uint256,uint256,uint8,address)'
], signer);

// 检查产品状态 (status: 4 = Cancelled)
const [,,,status,] = await contract.getProductStats(productId);
if (status !== 4) {
throw new Error('Product not cancelled, use claim() instead');
}

const tx = await contract.emergencyClaim(productId);
const receipt = await tx.wait();

console.log('Emergency claim successful!');
return receipt;
}

使用 Cast (Foundry) 命令行

查询产品配置

cast call --rpc-url "https://sepolia-rollup.arbitrum.io/rpc" \
"0xA40b4b726980C02F6558fcA5C0a52FaA2E5A0864" \
"getProductConfig(uint256)(string,uint256,uint256,uint256,uint256,uint256,uint256)" \
"1"

查询产品状态

cast call --rpc-url "https://sepolia-rollup.arbitrum.io/rpc" \
"0xA40b4b726980C02F6558fcA5C0a52FaA2E5A0864" \
"getProductStats(uint256)(uint256,uint256,uint256,uint8,address)" \
"1"

查询申购记录

cast call --rpc-url "https://sepolia-rollup.arbitrum.io/rpc" \
"0xA40b4b726980C02F6558fcA5C0a52FaA2E5A0864" \
"getSubscription(uint256,address)(uint256,uint256,uint256,uint256,bool)" \
"1" "0xYourAddress"

执行申购

cast send --rpc-url "https://sepolia-rollup.arbitrum.io/rpc" \
--private-key "0x..." \
"0xA40b4b726980C02F6558fcA5C0a52FaA2E5A0864" \
"subscribe(uint256,uint256,uint256,bytes)" \
"1" "500000000" "1767766918" "0x..."

执行领取

cast send --rpc-url "https://sepolia-rollup.arbitrum.io/rpc" \
--private-key "0x..." \
"0xA40b4b726980C02F6558fcA5C0a52FaA2E5A0864" \
"claim(uint256)" \
"1"

事件监听

const EARN_CONTRACT = '0xA40b4b726980C02F6558fcA5C0a52FaA2E5A0864';  // v3.1

const contract = new ethers.Contract(EARN_CONTRACT, [
'event Subscribed(uint256 indexed productId, address indexed user, uint256 amount, uint256 expectedReturn)',
'event Claimed(uint256 indexed productId, address indexed user, uint256 principal, uint256 interest)',
'event ProductStatusChanged(uint256 indexed productId, uint8 oldStatus, uint8 newStatus)'
], provider);

// 监听申购事件
contract.on('Subscribed', (productId, user, amount, expectedReturn) => {
console.log(`User ${user} subscribed ${ethers.formatUnits(amount, 6)} USDT to product ${productId}`);
console.log(`Expected return: ${ethers.formatUnits(expectedReturn, 6)} USDT`);
});

// 监听领取事件
contract.on('Claimed', (productId, user, principal, interest) => {
console.log(`User ${user} claimed from product ${productId}`);
console.log(`Principal: ${ethers.formatUnits(principal, 6)} USDT`);
console.log(`Interest: ${ethers.formatUnits(interest, 6)} USDT`);
});

// 监听产品状态变化
contract.on('ProductStatusChanged', (productId, oldStatus, newStatus) => {
const statusNames = ['Created', 'Subscribing', 'Active', 'Settled', 'Cancelled'];
console.log(`Product ${productId} status: ${statusNames[oldStatus]} -> ${statusNames[newStatus]}`);
});

产品状态枚举

状态说明
0Created已创建,等待开放申购
1Subscribing申购中
2Active进行中(申购结束)
3Settled已结算,可领取
4Cancelled已取消,可紧急赎回

常见错误及解决方案

错误原因解决方案
InvalidSignature()签名验证失败检查签名参数是否正确
SignatureAlreadyUsed()签名已被使用重新获取申购签名
DeadlineExceeded()签名已过期重新获取申购签名
ProductNotSubscribing()产品未在申购期等待申购期开始或选择其他产品
QuotaExceeded()产品额度已满减少申购金额或选择其他产品
UserLimitExceeded()超出个人限额减少申购金额
BelowMinAmount()低于最小申购金额增加申购金额
ProductNotSettled()产品未结算等待管理员结算产品
AlreadyClaimed()已经领取过无需重复操作
NoSubscription()无申购记录确认是否有申购该产品
ProductNotCancelled()产品未取消使用 claim() 而非 emergencyClaim()

测试网资源

合约地址 (Arbiscan)

合约链接
AXBladeEarnhttps://sepolia.arbiscan.io/address/0xA40b4b726980C02F6558fcA5C0a52FaA2E5A0864
Platform USDThttps://sepolia.arbiscan.io/address/0x572E474C3Cf364D085760784F938A1Aa397a8B9b