跳轉至

身份驗證

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

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 訊息建立。

const domain = {
  name: 'Sera',
  version: '1',
  chainId: 11155111,
  verifyingContract: '0xd0fc92d8eF9bE26D7288fCa1D6458f675e72B83a'
};

const 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, types, {
  owner: walletAddress,
  action: 'create',
  timestamp
});

const response = await fetch('https://api.sera.cx/api/v1/api-keys', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    owner_address: walletAddress,
    action: 'create',
    timestamp,
    signature,
    label: 'Trading bot'
  })
});

const { api_key, api_secret } = await response.json();

說明:

  • 簽名中的時間戳必須在伺服器時間前後 5 分鐘內。
  • 一個錢包最多可同時擁有 10 個活躍 API Key。
  • api_secret 只會回傳一次,必須自行安全保存。

列出 API Key

const timestamp = Math.floor(Date.now() / 1000);
const signature = await signer.signTypedData(domain, types, {
  owner: walletAddress,
  action: 'list',
  timestamp
});

const url = new URL('https://api.sera.cx/api/v1/api-keys');
url.searchParams.set('owner_address', walletAddress);
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

const action = `revoke_${apiKeyToRevoke}`;
const timestamp = Math.floor(Date.now() / 1000);
const signature = await signer.signTypedData(domain, types, {
  owner: walletAddress,
  action,
  timestamp
});

const url = new URL('https://api.sera.cx/api/v1/api-keys');
url.searchParams.set('owner_address', walletAddress);
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 次簽名。

const timestamp = Math.floor(Date.now() / 1000);
const signature = await signer.signTypedData(domain, types, {
  owner: walletAddress,
  action: 'revoke_all',
  timestamp
});

const res = await fetch('https://api.sera.cx/api/v1/api-keys/revoke-all', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    owner_address: walletAddress,
    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 加錢包簽名)。

await fetch('https://api.sera.cx/api/v1/api-keys/self-revoke', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': `Bearer ${apiKey}:${apiSecret}`
  },
  body: JSON.stringify({ api_key: apiKey })
});

驗證 API key

驗證 api_key/api_secret 配對是否有效,且不消耗速率限制、不產生副作用。成功時回傳所屬錢包地址。

const res = await fetch('https://api.sera.cx/api/v1/api-keys/verify', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ api_key: apiKey, api_secret: apiSecret })
});

// 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 會依 Sepolia 上的 Sera 合約 domain 驗證簽名:

const domain = {
  name: 'Sera',
  version: '1',
  chainId: 11155111,
  verifyingContract: '0xd0fc92d8eF9bE26D7288fCa1D6458f675e72B83a'
};

請透過 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 位
function uuidStringToBigInt(uuid) {
  return BigInt(`0x${uuid.replace(/-/g, '')}`);
}

function encodeStandaloneUuid(orderId, executorId) {
  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, ...
function encodeVlUuid(orderId, executorId, legId, groupOrderId) {
  const raw = uuidStringToBigInt(orderId);
  const group = uuidStringToBigInt(groupOrderId) >> 16n;
  return ((BigInt(executorId) << 252n) | (raw << 124n) | (group << 12n) | BigInt(legId)).toString();
}

訂單簽名

鏈上簽名的 Order 結構如下:

const 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 取得。

範例:

const orderData = {
  user: walletAddress,
  expiration: Math.floor(Date.now() / 1000) + 86400,
  feeBps: 0,
  recipient: ethers.ZeroAddress,
  fromToken: '0x...',
  toToken: '0x...',
  fromAmount: '1085000000',
  toAmount: '1000000000',
  initialDepositAmount: 0,
  uuid: BigInt(uuidInt)
};

const signature = await signer.signTypedData(domain, types, orderData);

兌換的 Intent 簽名

POST /swap/quote 會回傳 route_params。請依回傳結果原樣簽署。

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' }
  ]
};

const signature = await signer.signTypedData(domain, types, quote.route_params);

取消簽名

CancelOrder

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

const types = {
  CancelOrder: [
    { name: 'owner', type: 'address' },
    { name: 'orderId', type: 'uint256' }
  ]
};

const signature = await signer.signTypedData(domain, types, {
  owner: walletAddress,
  orderId: BigInt(uuidInt)
});

CancelVLBatch

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

提款簽名

const 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 response = await fetch('https://api.sera.cx/api/v1/verify-signature', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    owner_address: walletAddress,
    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) + 86400
  })
});

時鐘同步

請使用 GET /system/time 計算 expirationdeadline,使用 GET /health 讀取產生 uuid_int 所需的最新 executor_id,並使用 GET /config 取得最新的 EIP-712 合約地址。