兑换端点¶
请求兑换报价¶
身份验证: 无
请求体¶
{
"from_token": "0xDcAEcdd8Db64f4316A11917Ad0162DEBD935285b",
"to_token": "0xd3BdB2CE9cD98566EFc2e2977448c40578371779",
"from_amount": "1000000",
"owner_address": "0x...",
"recipient": "0x...",
"expiration": 1713254400,
"gas_mode": "receive_less"
}
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
from_token | address | 是 | ERC-20 输入代币地址 |
to_token | address | 是 | ERC-20 输出代币地址 |
from_amount | string | 是 | 正 uint256 字符串形式的原始输入金额 |
owner_address | address | 是 | 负责签名并支付兑换的地址 |
recipient | address | 是 | 最终接收输出代币的地址。可以与 owner_address 不同——设置为任意地址即可将输出代币发送给第三方钱包。该值会被锁定在已签名的 Intent 中,合约会强制要求最后一段腿恰好将输出支付到此地址。 |
expiration | integer | 是 | 作为 Intent 截止时间使用的 Unix 时间戳,必须满足 now < expiration <= now + 365 天 - 300 秒 |
gas_mode | string | 否 | receive_less 或 pay_more;默认值为 receive_less |
from_token 和 to_token 不能相同。
如果 from_amount 低于 GET /tokens 给出的代币最低成交门槛,接口会返回 HTTP 400,并在结构化 detail.code 中给出 AMOUNT_BELOW_MIN,同时返回必须满足的原始值和十进制展示值。
响应¶
{
"uuid": "6d0ad60d-c5d5-4b71-b0ca-9e8d2ae1bca4",
"route_params": {
"taker": "0x...",
"inputToken": "0xDcAEcdd8Db64f4316A11917Ad0162DEBD935285b",
"outputToken": "0xd3BdB2CE9cD98566EFc2e2977448c40578371779",
"maxInputAmount": "1000000",
"minOutputAmount": "923450",
"recipient": "0x...",
"initialDepositAmount": "1000000",
"uuid": "6431994229952211760403847975151123456789012345678901234567",
"deadline": 1713254400
},
"fee_breakdown": {
"gas_cost_usd": "0.12",
"gas_cost_from_token": "0.120000"
},
"expires_at": 1713250830,
"permit": {
"permit_supported": true,
"permit_required": true,
"token": "0xDcAEcdd8Db64f4316A11917Ad0162DEBD935285b",
"spender": "0x...",
"owner": "0x...",
"value_raw": "1000000",
"current_allowance_raw": "1000000",
"nonce": 4,
"suggested_deadline": 1713254400,
"domain": {
"name": "USD Coin",
"version": "2",
"chainId": 11155111,
"verifyingContract": "0xDcAEcdd8Db64f4316A11917Ad0162DEBD935285b"
},
"eip712": {
"domain": {
"name": "USD Coin", "version": "2",
"chainId": 11155111,
"verifyingContract": "0xDcAEcdd8Db64f4316A11917Ad0162DEBD935285b"
},
"primaryType": "Permit",
"types": {
"Permit": [
{ "name": "owner", "type": "address" },
{ "name": "spender", "type": "address" },
{ "name": "value", "type": "uint256" },
{ "name": "nonce", "type": "uint256" },
{ "name": "deadline", "type": "uint256" }
]
},
"message": {
"owner": "0x...",
"spender": "0x...",
"value": "1000000",
"nonce": 4,
"deadline": 1713254400
}
}
}
}
响应说明¶
uuid是传给POST /swap的报价记录 ID。route_params.uuid是绑定到签名 Intent 的组合 uint256。expires_at的生命周期很短;当前实现中报价大约只保存 30 秒。fee_breakdown仅覆盖 gas 成本;底层定价输入不属于响应契约的一部分。permit在所有需要从钱包扣款的兑换报价中(即route_params.initialDepositAmount > 0)都会非空。它包含一个eip712子对象,结构与signTypedData(domain, types, message)完全一致,钱包收到报价后可直接签名,无需自行拼装结构。permit.nonce是钱包下一次 EIP-2612 签名的预期 nonce — 它会把同一钱包已被服务器接受但尚未上链的兑换计入,以保证连续两笔兑换在中继器中按序排队,而非争抢同一个链上 nonce。仅initialDepositAmount = 0的纯权益兑换会让permit保持null。permit_supported = false表示输入代币不支持 EIP-2612 — 改为POST /approve流程(该分支不会带eip712字段)。
Gas 模式¶
| 模式 | 行为 |
|---|---|
receive_less | 保持输入金额不变,由 gas 减少输出 |
pay_more | 保持输出目标不变,由输入预算吸收 gas |
Note
只有完整签名通过校验的提交才会消费报价。签名不匹配、缺少必填 permit 字段或 minOutputAmount = 0 都会直接返回 400 而不消耗报价,避免良性错误强制重新报价。如果有并发的有效请求先消费成功,您会收到 410。
Note
某些报价会故意返回 route_params.minOutputAmount = "0",表示在该请求规模下没有可执行路径。这类报价仅供参考,POST /swap 会拒绝它们。
Note
如果 permit.permit_supported 为 false,请改用 GET /config 中的 sor_address 调用 POST /approve,本地签名后通过 POST /tx/send 广播,再提交 POST /swap。
批量请求兑换报价¶
鉴权: 无
在一次往返请求中获取多达 50 个兑换报价。适合做市商或需要同时为多个交易对定价的交易者 —— 把 N 次串行的 /swap/quote 调用合并成一次 HTTP 请求,服务端以受限并发量处理。
请求体¶
{
"quotes": [
{
"from_token": "0xDcAEcdd8Db64f4316A11917Ad0162DEBD935285b",
"to_token": "0xd3BdB2CE9cD98566EFc2e2977448c40578371779",
"from_amount": "1000000",
"owner_address": "0x...",
"recipient": "0x...",
"expiration": 1713254400,
"gas_mode": "receive_less"
}
]
}
| 字段 | 类型 | 必填 | 描述 |
|---|---|---|---|
quotes | array | 是 | 1–50 条报价请求,每条与 POST /swap/quote 请求体格式一致 |
每条报价的字段校验与 POST /swap/quote 完全一致。如果任意一条未通过 Pydantic 校验(无效地址、自交易、错误的 gas_mode 等),整体请求返回 422 且不会进入业务处理。运行时错误(预言机、订单核算、超时)以单条 error 包装返回 —— 整体请求仍返回 200。
响应¶
{
"items": [
{
"ok": true,
"quote": {
"uuid": "6d0ad60d-c5d5-4b71-b0ca-9e8d2ae1bca4",
"route_params": { "...": "..." },
"fee_breakdown": { "gas_cost_usd": "0.12", "gas_cost_from_token": "0.120000" },
"expires_at": 1713250830,
"permit": null
},
"error": null
},
{
"ok": false,
"quote": null,
"error": {
"rejectionCategory": "no_liquidity",
"message": "No liquidity"
}
}
]
}
items[i] 与 quotes[i] 一一对应 —— 顺序相同,长度相同。每条要么是成功报价 (ok: true),要么是带类型的错误包装 (ok: false)。
错误包装¶
rejectionCategory 为字符串。当底层错误携带类型化错误码(与 POST /swap 共用的 code 字段:如 AMOUNT_BELOW_MIN、NO_LIQUIDITY、SLIPPAGE_EXCEEDED、INTENT_DEADLINE_EXPIRED、QUOTE_STALE)时,会被原样转发,使客户端的同一段类型化错误分发逻辑可同时服务两个端点。批量运行时关注的错误使用各自的类别:
rejectionCategory | 触发条件 |
|---|---|
公开类型化错误码(如 AMOUNT_BELOW_MIN、NO_LIQUIDITY、SLIPPAGE_EXCEEDED) | 由定价引擎透传,与 POST /swap 的类型化 error_code 同词汇表。详见上方 Swap 错误包装 中的动作对照表 |
no_liquidity | 定价返回旧版字符串形式的无流动性(小写变体,等价于类型化的 NO_LIQUIDITY) |
invalid_quote | 定价引擎其他未携带类型化错误码的 4xx |
upstream_unavailable | 订单核算不可达 / 5xx |
quote_timeout | 单条报价计算超过服务器端预算(默认 10 秒) |
internal_error | 未处理的服务端错误 |
运维须知¶
- 上限: 每次请求 50 条。超出会在请求校验阶段返回
422。剩余交易对请分批提交。 - 并发: 服务端将单条计算的并发量限制在固定值(默认 20),即使一次提交 50 条,也最多有 20 条同时与订单核算交互。
- 单条超时: 每条报价的计算时间被独立限制,单个慢交易对不会拖累整批次。慢条返回
quote_timeout,其他仍可成功。 - 报价快照: 各交易对独立计算,不存在跨交易对的统一快照。如需跨对原子性定价,本接口不适用。
- 无批次幂等: 每条成功结果都生成新的报价
uuid。重试同样的批次会产出新的 UUID。 - 每条返回的报价 UUID 相互独立 —— 与
POST /swap/quote一样,各自配上签名 Intent 提交到POST /swap。
执行兑换¶
身份验证: 请求体中携带的 EIP-712 Intent 签名
请求体¶
{
"uuid": "6d0ad60d-c5d5-4b71-b0ca-9e8d2ae1bca4",
"signature": "0x...",
"permit_signature": "0x...",
"permit_deadline": 1713254400
}
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
uuid | string | 是 | 由 POST /swap/quote 返回的报价 ID |
signature | hex string | 是 | 对 quote.route_params 的签名 |
permit_signature | hex string | permit != null 时必填 | 把 quote.permit.eip712.{domain, types, message} 直接传给 wallet.signTypedData(...) 得到的 EIP-2612 签名;支持 65 字节与紧凑 64 字节两种十六进制编码。仅在 quote.permit == null(纯权益兑换)时可省略 |
permit_deadline | integer | permit != null 时必填 | 签名时使用的 deadline(即 quote.permit.eip712.message.deadline)。与 permit_signature 成对出现 |
响应¶
{
"success": true,
"trade_id": "85c92fcb-21b9-43ba-bb36-01d7b21eaa8d",
"status": "pending",
"fee_breakdown": {
"gas_cost_usd": "0.12",
"gas_cost_from_token": "0.120000"
}
}
trade_id 是后续可通过 GET /orders/{order_id} 查询,或通过 GET /orders?type=swap 过滤的订单 ID。
fee_breakdown 为可选字段,在部分失败或降级响应路径中可能省略。
要签名的 Intent 类型¶
const types = {
Intent: [
{ name: 'taker', type: 'address' },
{ name: 'inputToken', type: 'address' },
{ name: 'outputToken', type: 'address' },
{ name: 'maxInputAmount', type: 'uint256' },
{ name: 'minOutputAmount', type: 'uint256' },
{ name: 'recipient', type: 'address' },
{ name: 'initialDepositAmount', type: 'uint256' },
{ name: 'uuid', type: 'uint256' },
{ name: 'deadline', type: 'uint48' }
]
};
请严格按返回值原样签名 quote.route_params,不要在客户端重新生成或标准化字段。
错误情况¶
| 状态码 | 原因 |
|---|---|
400 | 请求无效、签名不匹配、缺少必填 permit 字段,或报价不可执行(minOutputAmount = 0) |
409 | "Permit nonce stale; re-quote required" — 报价生成与提交之间,链上 nonce 或服务端队列发生变动(例如同钱包另一笔兑换先一步上链)。报价未被消费;客户端应静默重新报价并重新提交。 |
410 | 报价已过期、不存在,或已被消费 |
429 | 触发钱包级速率限制 |
503 | 服务暂时不可用;请按 Retry-After 重试 |