跳转至

兑换端点

请求兑换报价

POST /swap/quote

身份验证:

请求体

{
  "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_lesspay_more;默认值为 receive_less

from_tokento_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_supportedfalse,请改用 GET /config 中的 sor_address 调用 POST /approve,本地签名后通过 POST /tx/send 广播,再提交 POST /swap

批量请求兑换报价

POST /swap/quote/batch

鉴权:

在一次往返请求中获取多达 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_MINNO_LIQUIDITYSLIPPAGE_EXCEEDEDINTENT_DEADLINE_EXPIREDQUOTE_STALE)时,会被原样转发,使客户端的同一段类型化错误分发逻辑可同时服务两个端点。批量运行时关注的错误使用各自的类别:

rejectionCategory 触发条件
公开类型化错误码(如 AMOUNT_BELOW_MINNO_LIQUIDITYSLIPPAGE_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

执行兑换

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 重试