身份验证¶
网络与兼容性
| 资源 | 值 |
|---|---|
| API 基础 URL | https://api.sera.cx/api/v1 |
| 链 | 以太坊主网(chainId 1) |
| Sera 合约 | 0xB5C50C5D5f038404F85970b7f5B7259C4AC0E198 |
| Vault 合约 | 0xC7d4Fd2638e6630C8C61329878676b88A8A24D43 |
| SOR 合约 | 0xa7A0cf7cd6f043fCA23f29d8ae5aae6b46e11c18 |
签名原语。 所有交易型变更都是针对 Sera 域的 EIP-712 类型化数据签名。走 permit 路径的充值使用 ERC-2612 Permit 扩展 — USDC、EURC 以及许多现代稳定币支持,但并非所有 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 /configPOST /swap/quote与POST /verify-signature
需要 API Key 的读取与构建类端点包括:
GET /orders、GET /orders/{order_id}、GET /fills、GET /fills/{order_id}与GET /balancesGET /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.sera.cx/api/v1"
PRIVATE_KEY = "0x...你的钱包私钥..."
WALLET = Account.from_key(PRIVATE_KEY).address
DOMAIN = {
"name": "Sera",
"version": "1",
"chainId": 1,
"verifyingContract": "0xB5C50C5D5f038404F85970b7f5B7259C4AC0E198",
}
import { Wallet, TypedDataDomain, ZeroAddress } from "ethers";
const API = "https://api.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: 1,
verifyingContract: "0xB5C50C5D5f038404F85970b7f5B7259C4AC0E198",
};
API Key¶
API Key 是只读凭证,格式如下:
端点摘要:
| 方法 | 路径 | 签名位置 |
|---|---|---|
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()
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.lstrip("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()
keys = requests.get(
f"{API}/api-keys",
params={
"owner_address": WALLET,
"action": "list",
"timestamp": timestamp,
"signature": "0x" + signature.lstrip("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()
requests.delete(
f"{API}/api-keys",
params={
"owner_address": WALLET,
"api_key": api_key_to_revoke,
"action": action,
"timestamp": timestamp,
"signature": "0x" + signature.lstrip("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()
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.lstrip("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 加钱包签名)。
验证 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" }(鉴权后端不可达)
EIP-712 域¶
公共 API 按 以太坊主网 Sera 合约域来验证签名(已在 准备 中定义):
请通过 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:
独立限价单¶
普通限价单的规则:
leg_id = 0group_id = order_id的前 112 位
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 0leg_id依次为0, 1, 2, ...
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 获取。
示例:
order_data = {
"user": WALLET,
"expiration": int(time.time()) + 86_400,
"feeBps": 0,
"recipient": "0x0000000000000000000000000000000000000000",
"fromToken": "0x...",
"toToken": "0x...",
"fromAmount": 1_085_000_000,
"toAmount": 1_000_000_000,
"initialDepositAmount": 0,
"uuid": int(uuid_int),
}
signable = encode_typed_data(DOMAIN, ORDER_TYPES, order_data)
signature = Account.from_key(PRIVATE_KEY).sign_message(signable).signature.hex()
const orderData = {
user: WALLET,
expiration: Math.floor(Date.now() / 1000) + 86_400,
feeBps: 0,
recipient: ZeroAddress,
fromToken: "0x...",
toToken: "0x...",
fromAmount: 1_085_000_000n,
toAmount: 1_000_000_000n,
initialDepositAmount: 0n,
uuid: BigInt(uuidInt),
};
const signature = await signer.signTypedData(DOMAIN, ORDER_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()
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()
CancelVLBatch¶
提款签名¶
提交前验证签名¶
r = requests.post(
f"{API}/verify-signature",
headers={"Content-Type": "application/json"},
data=json.dumps({
"owner_address": WALLET,
"side": "bid",
"amount": "1000.0",
"price": "1.085",
"from_address": EURC_ADDRESS,
"to_address": USDC_ADDRESS,
"order_id": "00000000-0000-4000-8000-000000000001",
"uuid_int": "6427948336465191935941739505432058208337171677044006212075520",
"signature": signature,
"expiration": int(time.time()) + 86_400,
}),
timeout=10,
)
const response = await fetch(`${API}/verify-signature`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
owner_address: WALLET,
side: "bid",
amount: "1000.0",
price: "1.085",
from_address: EURC_ADDRESS,
to_address: USDC_ADDRESS,
order_id: "00000000-0000-4000-8000-000000000001",
uuid_int: "6427948336465191935941739505432058208337171677044006212075520",
signature,
expiration: Math.floor(Date.now() / 1000) + 86_400,
}),
});
时钟同步¶
请使用 GET /system/time 计算 expiration 和 deadline,使用 GET /health 读取生成 uuid_int 所需的最新 executor_id,并使用 GET /config 获取最新的 EIP-712 合约地址。