跳转至

身份验证

网络与兼容性

资源 值
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 校验和地址。

Sera 使用两种身份验证模式:

  • API Key:用于账户读取接口和交易构建辅助接口。
  • EIP-712 签名:用于交易、取消、提款和 API Key 管理。

实际使用中,公共工具类端点包括:

  • GET /health、GET /system/time、GET /tokens、GET /markets 和 GET /config
  • POST /swap/quote 与 POST /verify-signature

需要 API Key 的读取与构建类端点包括:

  • GET /orders、GET /orders/{order_id}、GET /fills、GET /fills/{order_id} 与 GET /balances
  • GET /permit/metadata
  • 各类交易构建端点,例如 POST /approve、POST /deposit、POST /tx/send、POST /transfer 和 POST /transfer/send

准备

本页面的每个代码片段都假定下述 import 和常量。选定一种语言后在全页面复用。

import json, time
import requests
from eth_account import Account
from eth_account.messages import encode_typed_data

API           = "https://api.testnet.sera.cx/api/v1"
PRIVATE_KEY   = "0x...你的钱包私钥..."
WALLET        = Account.from_key(PRIVATE_KEY).address

DOMAIN = {
    "name": "Sera",
    "version": "1",
    "chainId": 11155111,
    "verifyingContract": "0x83475A1bD98a8DC2DCd507A747e4DC85da241D6e",
}
import { Wallet, TypedDataDomain, ZeroAddress } from "ethers";

const API         = "https://api.testnet.sera.cx/api/v1";
const PRIVATE_KEY = "0x...你的钱包私钥...";
const signer      = new Wallet(PRIVATE_KEY);
const WALLET      = signer.address;

const DOMAIN: TypedDataDomain = {
  name: "Sera",
  version: "1",
  chainId: 11155111,
  verifyingContract: "0x83475A1bD98a8DC2DCd507A747e4DC85da241D6e",
};

API Key

API Key 用于私有查询、交易辅助接口与自身撤销,不替代交易或签名撤单所需的钱包签名。凭证格式如下:

Authorization: Bearer {api_key}:{api_secret}

端点摘要:

方法 路径 签名位置
POST /api-keys 请求体中的 EIP-712 载荷(action=create)
GET 或 POST /api-keys 或 /api-keys/list 查询参数或请求体中的 EIP-712 载荷(action=list)
DELETE 或 POST /api-keys 或 /api-keys/revoke 查询参数或请求体中的 EIP-712 载荷(action=revoke_<api_key>)
POST /api-keys/revoke-all 请求体中的 EIP-712 载荷(action=revoke_all)
POST /api-keys/self-revoke 使用待撤销 Key 自身的 Bearer 凭据
POST /api-keys/verify 无(待验证的 Key 在请求体中)

创建 API Key

API Key 通过签署 EIP-712 ManageApiKey 消息来创建。

MANAGE_API_KEY_TYPES = {
    "ManageApiKey": [
        {"name": "owner",     "type": "address"},
        {"name": "action",    "type": "string"},
        {"name": "timestamp", "type": "uint256"},
    ]
}

timestamp = int(time.time())
message   = {"owner": WALLET, "action": "create", "timestamp": timestamp}
signable  = encode_typed_data(DOMAIN, MANAGE_API_KEY_TYPES, message)
signature = Account.from_key(PRIVATE_KEY).sign_message(signable).signature.hex()
signature = "0x" + signature.removeprefix("0x")

r = requests.post(
    f"{API}/api-keys",
    headers={"Content-Type": "application/json"},
    data=json.dumps({
        "owner_address": WALLET,
        "action":        "create",
        "timestamp":     timestamp,
        "signature":     "0x" + signature.removeprefix("0x"),
        "label":         "Trading bot",
    }),
    timeout=10,
)
api_key, api_secret = (lambda b: (b["api_key"], b["api_secret"]))(r.json())
const MANAGE_API_KEY_TYPES = {
  ManageApiKey: [
    { name: "owner",     type: "address" },
    { name: "action",    type: "string"  },
    { name: "timestamp", type: "uint256" },
  ],
};

const timestamp = Math.floor(Date.now() / 1000);
const signature = await signer.signTypedData(DOMAIN, MANAGE_API_KEY_TYPES, {
  owner: WALLET, action: "create", timestamp,
});

const response = await fetch(`${API}/api-keys`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    owner_address: WALLET,
    action:        "create",
    timestamp,
    signature,
    label:         "Trading bot",
  }),
});
const { api_key, api_secret } = await response.json();

说明:

  • 签名中的时间戳必须在服务器时间前后 5 分钟内。
  • 一个钱包最多可同时拥有 10 个活跃 API Key。
  • api_secret 只会返回一次,必须自行安全保存。

列出 API Key

timestamp = int(time.time())
message   = {"owner": WALLET, "action": "list", "timestamp": timestamp}
signable  = encode_typed_data(DOMAIN, MANAGE_API_KEY_TYPES, message)
signature = Account.from_key(PRIVATE_KEY).sign_message(signable).signature.hex()
signature = "0x" + signature.removeprefix("0x")

keys = requests.get(
    f"{API}/api-keys",
    params={
        "owner_address": WALLET,
        "action":        "list",
        "timestamp":     timestamp,
        "signature":     "0x" + signature.removeprefix("0x"),
    },
    timeout=10,
).json()
const timestamp = Math.floor(Date.now() / 1000);
const signature = await signer.signTypedData(DOMAIN, MANAGE_API_KEY_TYPES, {
  owner: WALLET, action: "list", timestamp,
});

const url = new URL(`${API}/api-keys`);
url.searchParams.set("owner_address", WALLET);
url.searchParams.set("action",        "list");
url.searchParams.set("timestamp",     String(timestamp));
url.searchParams.set("signature",     signature);

const keys = await fetch(url).then(r => r.json());

如果客户端更适合使用 JSON 请求体而不是签名查询参数,也可以调用 POST /api-keys/list,并在请求体中提交同样的 owner_address、action、timestamp 和 signature 字段。

撤销 API Key

api_key_to_revoke = "sera_..."
action            = f"revoke_{api_key_to_revoke}"
timestamp         = int(time.time())

signable = encode_typed_data(
    DOMAIN, MANAGE_API_KEY_TYPES,
    {"owner": WALLET, "action": action, "timestamp": timestamp},
)
signature = Account.from_key(PRIVATE_KEY).sign_message(signable).signature.hex()
signature = "0x" + signature.removeprefix("0x")

requests.delete(
    f"{API}/api-keys",
    params={
        "owner_address": WALLET,
        "api_key":       api_key_to_revoke,
        "action":        action,
        "timestamp":     timestamp,
        "signature":     "0x" + signature.removeprefix("0x"),
    },
    timeout=10,
)
const apiKeyToRevoke = "sera_...";
const action         = `revoke_${apiKeyToRevoke}`;
const timestamp      = Math.floor(Date.now() / 1000);

const signature = await signer.signTypedData(DOMAIN, MANAGE_API_KEY_TYPES, {
  owner: WALLET, action, timestamp,
});

const url = new URL(`${API}/api-keys`);
url.searchParams.set("owner_address", WALLET);
url.searchParams.set("api_key",       apiKeyToRevoke);
url.searchParams.set("action",        action);
url.searchParams.set("timestamp",     String(timestamp));
url.searchParams.set("signature",     signature);

await fetch(url, { method: "DELETE" });

如果客户端更适合使用 JSON 请求体而不是签名查询参数,也可以调用 POST /api-keys/revoke,并在请求体中提交相同字段以及 api_key。

批量撤销所有 API Key

通过一次签名撤销该钱包下所有有效的 API Key。action 是固定字符串 revoke_all。适合在凭据疑似泄漏、设备丢失或钱包轮换流程中使用 — 一次钱包弹窗代替 N 次签名。

timestamp = int(time.time())
signable  = encode_typed_data(
    DOMAIN, MANAGE_API_KEY_TYPES,
    {"owner": WALLET, "action": "revoke_all", "timestamp": timestamp},
)
signature = Account.from_key(PRIVATE_KEY).sign_message(signable).signature.hex()
signature = "0x" + signature.removeprefix("0x")

r = requests.post(
    f"{API}/api-keys/revoke-all",
    headers={"Content-Type": "application/json"},
    data=json.dumps({
        "owner_address": WALLET,
        "action":        "revoke_all",
        "timestamp":     timestamp,
        "signature":     "0x" + signature.removeprefix("0x"),
    }),
    timeout=10,
)
# 200: {"status":"ok","revoked_api_keys":["sera_...","sera_..."],"count":2}
# 200: {"status":"ok","revoked_api_keys":[],"count":0}            # 无有效 Key
# 409: 签名重放(已使用过)— 用新的 timestamp 重新签名
const timestamp = Math.floor(Date.now() / 1000);
const signature = await signer.signTypedData(DOMAIN, MANAGE_API_KEY_TYPES, {
  owner: WALLET, action: "revoke_all", timestamp,
});

const res = await fetch(`${API}/api-keys/revoke-all`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    owner_address: WALLET,
    action:        "revoke_all",
    timestamp,
    signature,
  }),
});
// 200: { "status": "ok", "revoked_api_keys": ["sera_...", "sera_..."], "count": 2 }
// 200: { "status": "ok", "revoked_api_keys": [], "count": 0 }   // 无有效 Key
// 409: 签名重放(已使用过)— 用新的 timestamp 重新签名

自撤销(Bearer 鉴权)

无需钱包签名即可撤销当前正在使用的 API Key。使用 Authorization: Bearer {api_key}:{api_secret} 鉴权;请求体中的 api_key 必须等于 Bearer 中的 api_key(只能撤销自己当前登录使用的 Key — 撤销同一钱包下的其它 Key 仍需用 DELETE /api-keys 加钱包签名)。

requests.post(
    f"{API}/api-keys/self-revoke",
    headers={
        "Content-Type":  "application/json",
        "Authorization": f"Bearer {api_key}:{api_secret}",
    },
    data=json.dumps({"api_key": api_key}),
    timeout=10,
)
await fetch(`${API}/api-keys/self-revoke`, {
  method: "POST",
  headers: {
    "Content-Type":  "application/json",
    "Authorization": `Bearer ${api_key}:${api_secret}`,
  },
  body: JSON.stringify({ api_key }),
});

验证 API Key

校验 api_key/api_secret 对是否有效,且不消耗速率限制、不产生副作用。成功时返回所属钱包地址。

r = requests.post(
    f"{API}/api-keys/verify",
    headers={"Content-Type": "application/json"},
    data=json.dumps({"api_key": api_key, "api_secret": api_secret}),
    timeout=10,
)
# 200: {"valid": True, "owner_address": "0x..."}
# 401: {"detail": "Invalid api_key or api_secret"}
# 503: {"detail": "Service temporarily unavailable; please retry"}(鉴权后端不可达)
const res = await fetch(`${API}/api-keys/verify`, {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ api_key, api_secret }),
});
// 200: { "valid": true, "owner_address": "0x..." }
// 401: { "detail": "Invalid api_key or api_secret" }
// 503: { "detail": "Service temporarily unavailable; please retry" }(鉴权后端不可达)

API Key 字段与返回结果

POST /api-keys 还接受可选的 label(去除两侧空格,最多 64 字符)。成功创建返回 HTTP 200,包含 api_key、仅返回一次的明文 api_secret、可为空的 label 与字符串 create_time。

列表响应为 { "keys": [...] },每项包含整数 id、owner_address、api_key、可为空的 label 和 create_time,不返回 api_secret。单个撤销和自身撤销返回 { "status": "ok" };不存在或已撤销的 key 可返回 404。撤销全部返回 status、revoked_api_keys、count,数量为零也可成功。

创建和撤销全部会以 409 拒绝重复使用已签名的 (owner, action, timestamp);仅改变签名编码不会产生新的授权。创建响应不明确时,先列出 key 再决定是否再次创建;丢失的明文 secret 不能通过列表恢复。超过 key 数量上限也返回 409,需要先撤销已有 key。

EIP-712 域

公共 API 按 Sepolia 上的 Sera 合约域来验证签名(已在 准备 中定义):

DOMAIN = {
    "name": "Sera",
    "version": "1",
    "chainId": 11155111,
    "verifyingContract": "0x83475A1bD98a8DC2DCd507A747e4DC85da241D6e",
}
const DOMAIN = {
  name: "Sera",
  version: "1",
  chainId: 11155111,
  verifyingContract: "0x83475A1bD98a8DC2DCd507A747e4DC85da241D6e",
};

请通过 GET /config 获取当前部署的 chain_id、sera_address、vault_address 和 sor_address,不要把这些值硬编码在客户端中。

过期时间规则

所有已签名订单请求都必须带 expiration,而兑换报价请求也使用同一套有界未来时间戳来生成后续签名 Intent。

  • expiration 必须严格大于当前时间。
  • expiration 最多只能比当前服务器时间晚 365 天减去 300 秒的时钟偏差保护。
  • 缺失、为 0、已过期或过远的值都会在进入撮合或结算前直接被拒绝。

请使用 GET /system/time 推导这些时间戳,并预留一点客户端缓冲,不要卡在边界时间上签名。

订单请求中的 UUID 绑定

限价单现在同时携带两个关联标识符:

  • order_id:人类可读的 UUID4 字符串。
  • uuid_int:嵌入链上签名 Order.uuid 字段中的十进制 uint256。

API 会校验这两个标识符是否编码的是同一个订单。请先从 GET /health 读取当前 executor_id,然后按照下面的组合布局生成 uuid_int:

[255:252] executor_id | [251:124] full UUID4 bits | [123:12] group_id | [11:0] leg_id

独立限价单

普通限价单的规则:

  • leg_id = 0
  • group_id = order_id 的前 112 位
import uuid as _uuid

def uuid_string_to_int(s: str) -> int:
    return int(_uuid.UUID(s))

def encode_standalone_uuid(order_id: str, executor_id: int) -> str:
    raw   = uuid_string_to_int(order_id)
    group = raw >> 16
    return str((executor_id << 252) | (raw << 124) | (group << 12))
function uuidStringToBigInt(uuid: string): bigint {
  return BigInt(`0x${uuid.replace(/-/g, "")}`);
}

function encodeStandaloneUuid(orderId: string, executorId: number): string {
  const raw   = uuidStringToBigInt(orderId);
  const group = raw >> 16n;
  return ((BigInt(executorId) << 252n) | (raw << 124n) | (group << 12n)).toString();
}

有效示例:

{
  "order_id": "00000000-0000-4000-8000-000000000001",
  "uuid_int": "6427948336465191935941739505432058208337171677044006212075520"
}

虚拟流动性批次

对于 VL 批次:

  • 所有同组订单共享同一个 group_id
  • group_id 来自 order 0
  • leg_id 依次为 0, 1, 2, ...
def encode_vl_uuid(order_id: str, executor_id: int,
                  leg_id: int, group_order_id: str) -> str:
    raw   = uuid_string_to_int(order_id)
    group = uuid_string_to_int(group_order_id) >> 16
    return str((executor_id << 252) | (raw << 124) | (group << 12) | leg_id)
function encodeVlUuid(
  orderId: string, executorId: number,
  legId: number, groupOrderId: string,
): string {
  const raw   = uuidStringToBigInt(orderId);
  const group = uuidStringToBigInt(groupOrderId) >> 16n;
  return ((BigInt(executorId) << 252n) | (raw << 124n) | (group << 12n) | BigInt(legId)).toString();
}

订单签名

链上签名的 Order 结构如下:

ORDER_TYPES = {
    "Order": [
        {"name": "user",                 "type": "address"},
        {"name": "expiration",           "type": "uint48"},
        {"name": "feeBps",               "type": "uint48"},
        {"name": "recipient",            "type": "address"},
        {"name": "fromToken",            "type": "address"},
        {"name": "toToken",              "type": "address"},
        {"name": "fromAmount",           "type": "uint256"},
        {"name": "toAmount",             "type": "uint256"},
        {"name": "initialDepositAmount", "type": "uint256"},
        {"name": "uuid",                 "type": "uint256"},
    ]
}
const ORDER_TYPES = {
  Order: [
    { name: "user",                 type: "address" },
    { name: "expiration",           type: "uint48"  },
    { name: "feeBps",               type: "uint48"  },
    { name: "recipient",            type: "address" },
    { name: "fromToken",            type: "address" },
    { name: "toToken",              type: "address" },
    { name: "fromAmount",           type: "uint256" },
    { name: "toAmount",             type: "uint256" },
    { name: "initialDepositAmount", type: "uint256" },
    { name: "uuid",                 type: "uint256" },
  ],
};

在 POST /orders 请求中,使用交易对身份字段,而不是直接的"支出/接收"字段:

  • from_address 是市场的基础代币地址。
  • to_address 是市场的报价代币地址。
  • bid 用 to_address 买入 from_address。
  • ask 卖出 from_address,换取 to_address。

这些地址请通过 GET /tokens 获取,展示用交易对标签请通过 GET /markets 获取。

示例:

health_response = requests.get(f"{API}/health", timeout=10)
health_response.raise_for_status()
order_id = str(_uuid.uuid4())
uuid_int = encode_standalone_uuid(order_id, int(health_response.json()["executor_id"]))
market_response = requests.get(f"{API}/markets", timeout=10)
market_response.raise_for_status()
market = next(m for m in market_response.json()["markets"]
              if {m["base_symbol"], m["quote_symbol"]} == {"USDC", "EURC"})
signed_order_request = {
    "owner_address": WALLET, "side": "bid", "amount": "1000", "price": "1",
    "from_address": market["base_address"], "to_address": market["quote_address"],
    "order_id": order_id, "uuid_int": str(uuid_int),
    "expiration": int(time.time()) + 3600,
}
preview_response = requests.post(f"{API}/orders/preview",
    json={**signed_order_request, "order_type": "limit"}, timeout=10)
preview_response.raise_for_status()
preview = preview_response.json()
signed_order_request.update(amount=preview["normalized_amount"], price=preview["normalized_price"])
order_data = preview["eip712_order"]
signable = encode_typed_data(DOMAIN, preview["eip712_types"], order_data)
raw_signature = Account.from_key(PRIVATE_KEY).sign_message(signable).signature.hex()
order_signature = "0x" + raw_signature.removeprefix("0x")
const healthResponse = await fetch(`${API}/health`);
if (!healthResponse.ok) throw new Error(`health: ${healthResponse.status}`);
const orderId = crypto.randomUUID();
const uuidInt = encodeStandaloneUuid(orderId, (await healthResponse.json()).executor_id);
const marketsResponse = await fetch(`${API}/markets`);
if (!marketsResponse.ok) throw new Error(`markets: ${marketsResponse.status}`);
const market = (await marketsResponse.json()).markets.find((m: any) =>
  [m.base_symbol, m.quote_symbol].sort().join("/") === "EURC/USDC");
if (!market) throw new Error("No active USDC/EURC market");
const signedOrderRequest = {
  owner_address: WALLET, side: "bid", amount: "1000", price: "1",
  from_address: market.base_address, to_address: market.quote_address,
  order_id: orderId, uuid_int: uuidInt.toString(),
  expiration: Math.floor(Date.now() / 1000) + 3600,
};
const previewResponse = await fetch(`${API}/orders/preview`, {
  method: "POST", headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ ...signedOrderRequest, order_type: "limit" }),
});
if (!previewResponse.ok) throw new Error(`preview: ${previewResponse.status}`);
const preview = await previewResponse.json();
signedOrderRequest.amount = preview.normalized_amount;
signedOrderRequest.price = preview.normalized_price;
const orderData = preview.eip712_order;
const orderSignature = await signer.signTypedData(DOMAIN, preview.eip712_types, orderData);

兑换的 Intent 签名

POST /swap/quote 会返回 route_params。请按返回结果原样签名。

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()
signature = "0x" + signature.removeprefix("0x")
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);

取消签名

CancelOrder

CancelOrder.orderId 使用的是组合 uuid_int,不是 UUID 字符串。

CANCEL_ORDER_TYPES = {
    "CancelOrder": [
        {"name": "owner",   "type": "address"},
        {"name": "orderId", "type": "uint256"},
    ]
}
signable  = encode_typed_data(
    DOMAIN, CANCEL_ORDER_TYPES,
    {"owner": WALLET, "orderId": int(uuid_int)},
)
signature = Account.from_key(PRIVATE_KEY).sign_message(signable).signature.hex()
signature = "0x" + signature.removeprefix("0x")
const CANCEL_ORDER_TYPES = {
  CancelOrder: [
    { name: "owner",   type: "address" },
    { name: "orderId", type: "uint256" },
  ],
};
const signature = await signer.signTypedData(DOMAIN, CANCEL_ORDER_TYPES, {
  owner: WALLET, orderId: BigInt(uuidInt),
});

CancelVLBatch

CANCEL_VL_BATCH_TYPES = {
    "CancelVLBatch": [
        {"name": "owner",     "type": "address"},
        {"name": "vlBatchId", "type": "string"},
    ]
}
const CANCEL_VL_BATCH_TYPES = {
  CancelVLBatch: [
    { name: "owner",     type: "address" },
    { name: "vlBatchId", type: "string"  },
  ],
};

提款签名

WITHDRAW_TYPES = {
    "WithdrawIntent": [
        {"name": "user",      "type": "address"},
        {"name": "tokens",    "type": "address[]"},
        {"name": "amounts",   "type": "uint256[]"},
        {"name": "recipient", "type": "address"},
        {"name": "deadline",  "type": "uint256"},
        {"name": "uuid",      "type": "uint256"},
    ]
}
const WITHDRAW_TYPES = {
  WithdrawIntent: [
    { name: "user",      type: "address"   },
    { name: "tokens",    type: "address[]" },
    { name: "amounts",   type: "uint256[]" },
    { name: "recipient", type: "address"   },
    { name: "deadline",  type: "uint256"   },
    { name: "uuid",      type: "uint256"   },
  ],
};

提交前验证签名

r = requests.post(f"{API}/verify-signature",
    json={**signed_order_request, "signature": order_signature}, timeout=10)
r.raise_for_status()
print(r.json()["valid"])
const response = await fetch(`${API}/verify-signature`, {
  method: "POST", headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ ...signedOrderRequest, signature: orderSignature }),
});
if (!response.ok) throw new Error(`verify: ${response.status}`);
console.log((await response.json()).valid);

时钟同步

请使用 GET /system/time 计算 expiration 和 deadline,使用 GET /health 读取生成 uuid_int 所需的最新 executor_id,并使用 GET /config 获取最新的 EIP-712 合约地址。

CancelAll

POST /orders/cancel-all 同时要求 API Key 和此签名消息。请使用 GET /config 返回的当前 Sera EIP-712 域。

import secrets
from eth_account import Account
from eth_account.messages import encode_typed_data

CANCEL_ALL_TYPES = {
    "CancelAll": [
        {"name": "owner", "type": "address"},
        {"name": "nonce", "type": "uint256"},
        {"name": "deadline", "type": "uint256"},
    ]
}
nonce = secrets.randbits(256) or 1
deadline = int(time.time()) + 300
message = {"owner": WALLET, "nonce": nonce, "deadline": deadline}
signable = encode_typed_data(DOMAIN, CANCEL_ALL_TYPES, message)
signed = Account.from_key(PRIVATE_KEY).sign_message(signable).signature.hex()
signature = "0x" + signed.removeprefix("0x")
const CANCEL_ALL_TYPES = {
  CancelAll: [
    { name: "owner", type: "address" },
    { name: "nonce", type: "uint256" },
    { name: "deadline", type: "uint256" },
  ],
};
const nonce = BigInt("0x" + crypto.randomUUID().replace(/-/g, ""));
const deadline = Math.floor(Date.now() / 1000) + 300;
const signature = await signer.signTypedData(DOMAIN, CANCEL_ALL_TYPES, {
  owner: WALLET,
  nonce: BigInt(nonce),
  deadline: BigInt(deadline),
});

每次独立取消使用新的正 uint256 nonce。在请求体中传入其十进制字符串和整数 deadline,并在查询参数中传入一致的小写所有者地址。重用已消费 nonce 返回 409。单笔订单与 VL 取消仍使用原有签名类型。

普通下单可通过预览获取 eip712_order、eip712_types 并原样签名。Exact Output 使用现有 Intent 类型,以及其自身返回的 signing_domain 和已保存的 quote_uuid。

签名编码

下单、VL 下单、Swap/Exact Output 执行与 /verify-signature 请使用钱包返回的标准 65 字节 ECDSA 签名。下单、VL 下单、普通 Swap 执行与 /verify-signature 接受 65–8192 字节;Exact Output 执行接受 65–4096 字节。底层代码虽可识别 64 字节 EIP-2098 签名,上述意图签名字段会拒绝它。Permit 字段另行接受 64/65 字节编码;Exact Output 的 Permit 签名上限为 4096 字节。合约钱包签名必须同时满足所在路由的长度检查和 ERC-1271 验证。

Python 格式化签名时,使用 removeprefix("0x") 仅移除字面前缀。lstrip("0x") 还会删除开头的零数字,可能破坏有效签名;上面的示例会保留这些字节。