EIP-712 订单签名
Conviction 的所有订单都通过 EIP-712 typed data 签署。普通用户无需关心,因为 UI 会自动处理这一过程。本页是面向以程序方式直接挂单的机器人·做市商的参考资料。
#域(EIP-712 Domain Separator)
{
name: 'Conviction CTF Exchange', // 必须 byte-for-byte 完全一致
version: '1',
chainId: 56, // BSC mainnet
verifyingContract: CTF_EXCHANGE_ADDRESS, // 因环境而异
}域 name 一个字符都不能错
即便有 typo,ECDSA 本身也会通过,因此在客户端时看起来正常。但由于与合约一侧的域不同,会在成交时 reverts。
verifyingContract 是 BSC 主网的 CTFExchange 地址。请从服务端响应或环境变量中获取 —— 不要硬编码到客户端。
#Order 结构
| 字段 | 类型 | 含义 |
|---|---|---|
salt | uint256 | 随机标识符(每次重新签名都新生成) |
maker | address | 用户地址。通过 EIP-7702 委托,EOA 即 AA,因此 maker == signer。 |
signer | address | 用户的 EOA —— 与 maker 相同的地址。 |
taker | address | 0x0000...(公开挂单) |
tokenId | uint256 | 结果的 position ID(= outcome 的 position_id) |
makerAmount | uint256 | maker 付出的资产的 wei 数量 |
takerAmount | uint256 | maker 想要收到的资产的 wei 数量 |
expiration | uint256 | 订单过期 unix 秒。0 = 无过期 |
nonce | uint256 | 每次用 CTFExchange.nonces(maker) 重新获取 |
feeRateBps | uint256 | 手续费上限(basis points) |
side | uint8 | BUY = 0,SELL = 1 |
signatureType | uint8 | EOA = 0(用户订单) |
#EIP-712 type 定义
const types = {
Order: [
{ name: 'salt', type: 'uint256' },
{ name: 'maker', type: 'address' },
{ name: 'signer', type: 'address' },
{ name: 'taker', type: 'address' },
{ name: 'tokenId', type: 'uint256' },
{ name: 'makerAmount', type: 'uint256' },
{ name: 'takerAmount', type: 'uint256' },
{ name: 'expiration', type: 'uint256' },
{ name: 'nonce', type: 'uint256' },
{ name: 'feeRateBps', type: 'uint256' },
{ name: 'side', type: 'uint8' },
{ name: 'signatureType', type: 'uint8' },
],
};#单个订单签名示例(viem)
import { parseUnits } from 'viem';
const SHARES = parseUnits('100', 18);
const order = {
salt: BigInt(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)),
maker: eoaAddress, // EIP-7702: EOA 即 AA
signer: eoaAddress, // 相同地址
taker: '0x0000000000000000000000000000000000000000',
tokenId: BigInt(outcome.position_id),
// BUY 100 shares at 0.5 USDT each
makerAmount: parseUnits('50', 18), // 100 × 0.5 = 50 USDT
takerAmount: SHARES, // 100 shares
expiration: BigInt(Math.floor(Date.now() / 1000) + 3600),
nonce: await ctfExchange.read.nonces([eoaAddress]),
feeRateBps: BigInt(market.platformFee),
side: 0n, // BUY
signatureType: 0, // EOA
};
const signature = await walletClient.signTypedData({
account: eoaAddress,
domain,
types: { Order: types.Order },
primaryType: 'Order',
message: order,
});
// POST /orders
await fetch('/api/orders', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
marketId: market.id,
outcomeId: outcome.id,
side: 'buy',
type: 'limit',
price: 0.5,
shares: 100,
userId: user.id,
salt: '0x' + order.salt.toString(16),
signature,
signerAddress: order.signer,
makerAddress: order.maker,
expiration: order.expiration.toString(),
signatureType: 0,
tokenId: order.tokenId.toString(),
makerAmount: order.makerAmount.toString(),
takerAmount: order.takerAmount.toString(),
feeRateBps: market.platformFee,
}),
});#makerAmount / takerAmount 计算规则
所有金额都是 18-decimal wei 整数。
| Side | Maker 付出 | Maker 收到 | makerAmount | takerAmount |
|---|---|---|---|---|
| BUY | USDT | 结果代币 | shares × price(USDT wei) | shares(token wei) |
| SELL | 结果代币 | USDT | shares(token wei) | shares × price(USDT wei) |
price是 0..1 的 float,但在 wei-space 中以price × 1e18用于乘法。- 所有乘法都用 bigint 算术 —— 禁止 JS
Number(1 USDT =10^18,超出安全整数范围)。
#常见错误
以下四点最常见
- 在
maker与signer填入不同地址。 在 EIP-7702 模型中,两者都必须是用户的 EOA 地址,保持一致。 - 缓存 nonce。 每次都用
CTFExchange.nonces(maker)重新获取。一旦调用incrementNonce(),此前 nonce 的所有未成交订单都会 reverts。 - 把 price 当作 percent 或 bps 发送。 载荷中的
price是 0..1 的 float。 - 把 makerAmount / takerAmount 当作人类可读单位发送。 始终是 18-dec wei 的 decimal string。
#签名在成交时校验
- 后端不校验签名 —— 它原样保存已签名的载荷,并在成交时提交到链上。
- 错误的签名在成交时会让链上 ECDSA 复原返回另一个地址而 reverts → 该订单被标记为失败。
- 因此签名步骤必须准确 —— 请避免上述四个错误。
#下一步
- REST → Markets —— 查询
feeRateBps等下单所需的市场数据 - 用户钱包 —— 单一地址(EOA == AA)模型
本页面是否有帮助?