跳轉至

身份驗證

網路與相容性

資源
API 基礎 URL https://api.sera.cx/api/v1
Ethereum 主網(chainId 1
Sera 合約 0xB5C50C5D5f038404F85970b7f5B7259C4AC0E198
Vault 合約 0xC7d4Fd2638e6630C8C61329878676b88A8A24D43
SOR 合約 0xa7A0cf7cd6f043fCA23f29d8ae5aae6b46e11c18

簽名原語。 所有交易型變更都是針對 Sera domain 的 EIP-712 類型化資料簽名。走 permit 路徑的充值使用 ERC-2612 Permit 擴充功能 — USDC、EURC 以及許多現代穩定幣支援,但並非所有 ERC-20 都支援;簽名前請呼叫 GET /permit/metadata 檢查。API key 管理使用 EIP-712 ManageApiKey payload。

已驗證客戶端。 Python eth_account >= 0.10 + requests;TypeScript ethers v6(signer.signTypedData)。已驗證支援 EIP-712 類型化資料的瀏覽器錢包:MetaMaskRabbyFrameCoinbase WalletTrustRainbow。Safe 多簽透過 EIP-1271 支援(訊息於鏈上驗證,而非透過 ecrecover)。

地址大小寫。 讀取類端點(/balances/orders/fills)將 owner_address 視為大小寫敏感 — 請傳入小寫形式。EIP-712 簽名 payload 接受 EIP-55 校驗和地址。

Sera 使用兩種身份驗證模式:

  • API Key:用於帳戶讀取端點與交易建構輔助端點。
  • EIP-712 簽名:用於交易、取消、提款與 API Key 管理。

實際使用時,公共工具類端點包括:

  • GET /healthGET /system/timeGET /tokensGET /marketsGET /config
  • POST /swap/quotePOST /verify-signature

需要 API Key 的讀取與建構類端點包括:

  • GET /ordersGET /orders/{order_id}GET /fillsGET /fills/{order_id}GET /balances
  • GET /permit/metadata
  • 各類交易建構端點,例如 POST /approvePOST /depositPOST /tx/sendPOST /transferPOST /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 是唯讀憑證,格式如下:

Authorization: Bearer {api_key}:{api_secret}

端點摘要:

方法 路徑 簽名位置
POST /api-keys 請求主體中的 EIP-712 載荷(action=create
GETPOST /api-keys/api-keys/list 查詢參數或請求主體中的 EIP-712 載荷(action=list
DELETEPOST /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_addressactiontimestampsignature 欄位。

撤銷 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 加錢包簽名)。

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" }(驗證後端不可達)

EIP-712 Domain

公共 API 會依 Ethereum 主網 Sera 合約 domain 驗證簽名(已在 準備 中定義):

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

請透過 GET /config 取得目前部署的 chain_idsera_addressvault_addresssor_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 是市場的計價代幣地址。
  • bidto_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()
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",
    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 計算 expirationdeadline,使用 GET /health 讀取產生 uuid_int 所需的最新 executor_id,並使用 GET /config 取得最新的 EIP-712 合約地址。