即时兑换交易¶
网络与兼容性
| 资源 | 值 |
|---|---|
| API 基础 URL | https://api.testnet.sera.cx/api/v1 |
| 链 | Sepolia(chainId 11155111) |
| Sera 合约 | 0x83475A1bD98a8DC2DCd507A747e4DC85da241D6e |
| Vault 合约 | 0x3c7945840bAE0d7e7f3824Ebccef1962629250F0 |
| SOR 合约 | 0x83c1368110B640A729f3810De5FBe94b99aa5668 |
签名原语。 所有交易型变更都是针对 Sera 域的 EIP-712 类型化数据签名。走 permit 路径的充值使用 ERC-2612 Permit 扩展 — USDC、EURC、EURT 以及多数现代稳定币支持,但并非所有 ERC-20 都支持;签名前请调用 GET /permit/metadata 检查。API key 管理使用 EIP-712 ManageApiKey 载荷。
已验证客户端。 Python eth_account >= 0.10 + requests;TypeScript ethers v6(signer.signTypedData)。已验证支持 EIP-712 类型化数据的浏览器钱包:MetaMask、Rabby、Frame、Coinbase Wallet、Trust、Rainbow。Safe 多签通过 EIP-1271 支持(消息在链上验证,而非通过 ecrecover)。
地址大小写。 读取类端点(/balances、/orders、/fills)将 owner_address 视为大小写敏感 — 请传入小写形式。EIP-712 签名载荷接受 EIP-55 校验和地址。
即时兑换为希望立即以最佳可用价格交换代币的交易者提供即时执行。与限价单不同,即时兑换是 Fill-or-Kill 的 — 要么完全执行,要么被拒绝。
兑换流程¶
sequenceDiagram
participant User
participant API as Sera API
participant Chain as Ethereum
User->>API: POST /swap/quote
API-->>User: Quote (uuid, route_params)
User->>User: Sign route_params (EIP-712)
User->>API: POST /swap (uuid + signature)
API->>Chain: Settlement
Chain-->>API: Confirmation
API-->>User: Success (trade_id) 第 1 步:请求报价¶
import time, requests
quote = requests.post(
"https://api.testnet.sera.cx/api/v1/swap/quote",
json={
"from_token": "0x965d4b...d0e", # USDC address
"to_token": "0xef64d1...40f", # EURC address
"from_amount": "1000000000", # 1000 USDC(6 位小数)
"owner_address": "0xYOUR_WALLET",
"recipient": "0xYOUR_WALLET", # 接收输出代币的地址(可以是另一个钱包)
"expiration": int(time.time()) + 3600,
"gas_mode": "receive_less",
},
timeout=10,
).json()
# quote["uuid"] — 唯一报价标识
# quote["route_params"] — 需要签名的 EIP-712 参数
# quote["fee_breakdown"] — Gas 费用详情(gas_cost_usd、gas_cost_from_token)
const response = await fetch("https://api.testnet.sera.cx/api/v1/swap/quote", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
from_token: "0x965d4b...d0e", // USDC address
to_token: "0xef64d1...40f", // EURC address
from_amount: "1000000000", // 1000 USDC(6 位小数)
owner_address: "0xYOUR_WALLET",
recipient: "0xYOUR_WALLET", // 接收输出代币的地址(可以是另一个钱包)
expiration: Math.floor(Date.now() / 1000) + 3600,
gas_mode: "receive_less",
}),
});
const quote = await response.json();
// quote.uuid — 唯一报价标识
// quote.route_params — 需要签名的 EIP-712 参数
// quote.fee_breakdown — Gas 费用详情(gas_cost_usd、gas_cost_from_token)
报价参数¶
| 参数 | 类型 | 说明 |
|---|---|---|
from_token | address | 输入代币的 ERC-20 地址 |
to_token | address | 输出代币的 ERC-20 地址 |
from_amount | string | 原始代币单位的金额(例如 "1000000000" 表示 1000 USDC) |
owner_address | address | 您的钱包地址 |
recipient | address | 输出代币的接收地址;可以是任意地址——设置为其他钱包即可将兑换输出发送给第三方。 |
expiration | integer | Unix 时间戳截止时间 |
gas_mode | string | "receive_less" 或 "pay_more" |
报价响应¶
响应包含:
uuid— 此报价的唯一标识符(提交时使用)route_params— 需要签名的 EIP-712 Intent 结构体字段fee_breakdown— Gas 费用详情:gas_cost_usd和gas_cost_from_tokenexpires_at— 报价过期时间(报价为一次性使用)
第 2 步:签署报价¶
使用您的钱包通过 EIP-712 类型化数据签名对 route_params 进行签名:
from eth_account import Account
from eth_account.messages import encode_typed_data
DOMAIN = {
"name": "Sera",
"version": "1",
"chainId": 11155111, # Sepolia
"verifyingContract": "0x83475A1bD98a8DC2DCd507A747e4DC85da241D6e",
}
INTENT_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"},
]
}
signable = encode_typed_data(DOMAIN, INTENT_TYPES, quote["route_params"])
signature = Account.from_key(PRIVATE_KEY).sign_message(signable).signature.hex()
import { Wallet } from "ethers";
const DOMAIN = {
name: "Sera",
version: "1",
chainId: 11155111, // Sepolia
verifyingContract: "0x83475A1bD98a8DC2DCd507A747e4DC85da241D6e",
};
const INTENT_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" },
],
};
const signature = await signer.signTypedData(DOMAIN, INTENT_TYPES, quote.route_params);
第 3 步:执行兑换¶
提交已签名的报价:
import json
from pathlib import Path
submit_body = {"uuid": quote["uuid"], "signature": "0x" + signature.removeprefix("0x")}
permit = quote.get("permit") or {}
if permit.get("permit_required") and permit.get("permit_supported"):
typed = permit["eip712"]
permit_message = encode_typed_data(typed["domain"], typed["types"], typed["message"])
permit_sig = Account.from_key(PRIVATE_KEY).sign_message(permit_message).signature.hex()
submit_body["permit_signature"] = "0x" + permit_sig.removeprefix("0x")
submit_body["permit_deadline"] = int(typed["message"]["deadline"])
elif permit.get("permit_required") and int(permit["current_allowance_raw"]) < int(permit["value_raw"]):
raise RuntimeError("Approve the quoted spender, confirm the transaction, then request a fresh quote")
journal = Path("swap-requests")
journal.mkdir(mode=0o700, exist_ok=True)
# hex gives an opaque quote ID a filename without interpreting path separators.
saved_path = journal / (quote["uuid"].encode().hex() + ".json")
with saved_path.open("x", encoding="utf-8") as saved:
json.dump(submit_body, saved)
response = requests.post("https://api.testnet.sera.cx/api/v1/swap",
json=submit_body, timeout=10)
response.raise_for_status()
swap = response.json()
# Accepted is not chain-confirmed: track trade_id via the authenticated order API.
import { mkdirSync, writeFileSync } from "node:fs";
const submitBody: { uuid: string; signature: string; permit_signature?: string; permit_deadline?: number } = {
uuid: quote.uuid, signature,
};
const permit = quote.permit;
if (permit?.permit_required && permit.permit_supported) {
const typed = permit.eip712;
submitBody.permit_signature = await signer.signTypedData(typed.domain, typed.types, typed.message);
submitBody.permit_deadline = Number(typed.message.deadline);
} else if (permit?.permit_required && BigInt(permit.current_allowance_raw) < BigInt(permit.value_raw)) {
throw new Error("Approve the quoted spender, confirm the transaction, then request a fresh quote");
}
mkdirSync("swap-requests", { recursive: true, mode: 0o700 });
const savedPath = `swap-requests/${Buffer.from(quote.uuid).toString("hex")}.json`;
writeFileSync(savedPath, JSON.stringify(submitBody), { flag: "wx", mode: 0o600 });
const result = await fetch("https://api.testnet.sera.cx/api/v1/swap", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify(submitBody),
});
if (!result.ok) throw new Error(`swap: ${result.status}; reconcile saved request`);
const swap = await result.json();
// Accepted is not chain-confirmed: track trade_id via the authenticated order API.
Gas 模式¶
Swap 执行 Gas 包含在报价中,由 relayer 发送结算交易。独立 approve 等用户自行发送的交易仍需网络 Gas,详见费用。
请求报价时,您可以选择 Gas 费用的应用方式:
| 模式 | 行为 |
|---|---|
receive_less | Gas 费用从输出中扣除。您花费的恰好是 from_amount,但收到的略少。 |
pay_more | Gas 费用添加到输入中。您收到完整的报价金额,但花费的略多。 |
报价响应中的 fee_breakdown 显示确切的 Gas 费用,不会有意外。服务器会计算调整后的金额 — 您的前端只需按原样签署 route_params 即可。
多段路由¶
Sera 自动为您的兑换找到最优路由。如果直接交易对不存在或多跳路径能提供更好的定价,兑换会透明地通过中间货币路由。
例如,GBP → SGD 的兑换可能执行为:
- GBP → USD
- USD → SGD
这是原子性的 — 要么所有段都成功,要么全部不执行。
签名价格约束与 MEV¶
报价和路由在链下计算,签名的 maxInputAmount、minOutputAmount、uuid 和 deadline 约束结算,不符合时回滚。这减少了开放式内存池价格发现带来的风险,但不表示免疫排序、服务干扰或其他攻击。请核对签名约束并参阅安全模型。
错误处理¶
分类业务错误可以使用 detail.error_code,其他失败可能返回字符串 detail 或验证数组。Exact Output 使用独立的 detail.code 约定,不能假定所有 4xx 形状相同。
| 结果 | 处理 |
|---|---|
| 200 | 已接受/缓存结果;用 trade_id 查询结算,不能仅凭 HTTP 成功确认成交 |
| 400 / 422 | 查看验证、签名和资金错误;更改已准入请求前先核对 |
| 409 | 可能是提交中、请求内容冲突、报价过旧或准入前 permit nonce 冲突;查看响应并核对原订单 |
| 410 | 报价不存在/过期或保存结果已清理;不能据此证明未执行 |
| 429 | 有 Retry-After 时遵守 |
| 超时 / 503 | 保留身份和完整已签名 body,查询后按需重试原 body |
执行示例在发送前保存请求(TypeScript 使用 Node.js),请保持文件私有。结果未知时,不要重跑报价/签名/生成身份流程来恢复。需要 approve 时,先确认授权再取得新报价;permit 路径除 Intent 外,还需要签署返回的 Permit。只有解决原请求状态后才重新报价,详见完整错误和重试规则。当前报价 API 没有客户端 slippage 参数,不要静默更改返回的签名约束。