代理授权
为做市商提供的多密钥签名支持。
概述
代理授权允许专用签名密钥(代理)代表交易钱包下单和撤单。这在以下场景中非常有用:
- 安全性:将交易钱包密钥保存在冷存储中,使用热密钥进行签名
- 运营:多名团队成员可以使用不同的密钥进行签名
- 自动化:自动化系统可以使用专用密钥进行签名
两种授权模型
Hypercall 根据产品的不同,有两套独立的委托系统:
| 产品 | 委托类型 | 存储 | 设置方式 |
|---|---|---|---|
| 期权(REST API) | 代理授权 | 链下(数据库) | POST /approve-agent |
| 永续合约(链上指令) | API 钱包 | 链上(Exchange 合约) | hc_update_api_wallet 指令 |
这两套系统相互独立。为期权交易批准的代理不会自动获得永续合约的授权,反之亦然。
期权:代理授权(链下)
代理代表交易钱包签署 PlaceOrder 和 CancelOrder EIP-712 消息。API 服务器在转发到引擎之前,会根据其数据库验证授权。
永续合约:API 钱包(链上)
API 钱包使用 HypercallAgentSign EIP-712 域签署永续合约指令。Exchange 合约通过 isApiWalletActive() 在链上验证授权。完整的指令签名参考请参阅 EIP-712 签名。
要添加或移除 API 钱包,请提交由账户管理钱包签名的 hc_update_api_wallet 指令。
这与 Hyperliquid 的 API 钱包模型相同。Hyperliquid 在 Nonces and API wallets 中将其记录为 API 钱包(也称为代理钱包),并在 Exchange endpoint 中记录了 approveAgent。
Nonce 重放保护
Nonce 跟踪遵循 Hyperliquid nonce 模型。所有已签名的操作(订单、代理批准/撤销、QP 握手)共享每个签名者的 nonce 空间。引擎为每个签名者地址存储 100 个最大的 nonce。新 nonce 满足以下条件时会被接受:
nonce > min(stored_set)—— 必须大于已存储集合中最小的 nonce!stored_set.contains(nonce)—— 不得重复nonce必须在服务器时间戳的 (T - 2 天, T + 1 天) 范围内
允许乱序的 nonce(例如,先使用 nonce 105 再使用 102,只要两者都大于集合的最小值即可)。集合的上限为 100 条,因此在添加新条目时最旧的条目会被淘汰。这可以防止单个过大的 nonce 导致账户失效。
Nonce 签名者是签署 EIP-712 消息的地址:订单为 API 钱包,代理授权为恢复出的签名者,握手为 QP 钱包。客户端应使用 Date.now()(毫秒纪元时间)作为 nonce 种子并单调递增。
期权代理授权
直接签名
如果 signer == wallet,订单始终被授权(自签名)。
代理签名
如果 signer != wallet,签名者必须是已授权的代理:
- 代理必须存在于引擎持有的授权状态中
- 授权不得已过期
- 引擎快照和引擎日志是重启后的持久化恢复来源
批准代理
端点:POST /approve-agent
请求:ApproveAgentRequest
{
"agent": "0x...",
"nonce": 1,
"signature": "0x..."
}
签名:
- 钱包所有者签署
ApproveAgent消息 - 钱包地址从恢复出的签名中推导得出
- 代理为请求中的
agent字段
EIP-712 结构体:
struct ApproveAgent {
address agent;
uint64 nonce;
}
响应:ApproveAgentResponse
{
"success": true,
"error": null
}
注意事项:
- 代理授权是持久性的(存储在数据库中)
- 除非设置了
expires_at(尚未实现),否则授权不会过期
撤销代理
端点:DELETE /revoke-agent
请求:RevokeAgentRequest
{
"agent": "0x...",
"nonce": 2,
"signature": "0x..."
}
签名:
- 钱包所有者签署
RevokeAgent消息 - 钱包地址从恢复出的签名中推导得出
EIP-712 结构体:
struct RevokeAgent {
address agent;
uint64 nonce;
}
响应:RevokeAgentResponse
注意事项:
- 向引擎持有的授权状态应用一条记入日志的
RevokeAgent命令 - 交易 API 的授权检查读取后端状态,并对交易请求强制执行撤销。Trollbox 发帖功能可能会将代理授权的肯定结果缓存较长时间,因此最近被撤销的代理在该缓存过期或尽力而为的失效通知到达相关边缘节点之前,仍可能可以发帖。此缓存不会授权任何交易 API 的变更操作。
- 撤销后,代理无法再为该钱包签名
获取已授权代理
端点:GET /authorized-agents?wallet=...
查询参数:
wallet(必填)
响应:
{
"agents": [
"0x...",
"0x..."
]
}
注意事项:
- 仅返回处于活跃状态且未过期的代理
- 按
created_at DESC排序
代理使用
使用代理签署订单
代理获得批准后:
- 使用代理钱包签署订单:使用代理钱包签署
PlaceOrder/CancelOrder消息 - 设置 wallet 字段:将
wallet字段设置为交易钱包地址(而非代理地址) - 中间件验证:中间件验证代理是否已获得该钱包的授权
示例:
// Agent wallet signs the order
const agentSigner = new ethers.Wallet(agentPrivateKey);
const message = {
wallet: "0x...", // Trading wallet (not agent)
symbol: "BTC-20250131-100000-C",
side: "Buy",
size: "0.1",
price: "100.0",
tif: "gtc",
clientId: "mm-1",
nonce: 1
};
// Sign with agent wallet
const signature = await agentSigner._signTypedData(domain, types, message);
// Send request
const response = await fetch('/order', {
method: 'POST',
body: JSON.stringify({
...message,
signature
})
});
使用代理批量下单
批量端点按条目逐一验证代理授权:
POST /bulk_order中的每个订单可以由不同的代理签署- 代理授权按条目逐一检查
- 如果任何条目的代理授权失败,该条目会在
BulkOrderResult中返回错误
授权检查
中间件(单笔订单)
端点:
POST /orderDELETE /orderDELETE /order_cloid
检查:signature_and_agent_middleware 验证:
- 签名恢复成功
- 签名者已获授权(signer == wallet 或代理已获授权)
处理程序(批量订单)
端点:
POST /bulk_orderDELETE /bulk_orderDELETE /bulk_order_cloid
检查:在处理程序中按条目逐一验证:
- 按条目逐一进行签名恢复
- 按条目逐一进行代理授权检查
授权存储
代理授权是由引擎持有的状态。ApproveAgent 和 RevokeAgent 命令会记入日志,通过引擎重放恢复,并通过读取快照暴露给 API 检查使用。
授权逻辑
签名者授权采用显式 OR 逻辑:
- 直接签名:
signer == wallet。 - 代理签名:引擎快照中包含
wallet_address == <wallet>且agent_address == <signer>的授权记录,并且expires_at不存在或处于未来时间。
过期
expires_at 由引擎快照授权检查强制执行。expires_at < NOW() 的代理将被视为未授权。
安全注意事项
- 代理密钥安全:保护代理私钥(使用硬件钱包或安全的密钥管理系统)
- 定期审计:通过
GET /authorized-agents定期审查已授权的代理 - 撤销未使用的代理:撤销不再需要的代理
- 过期设置:当批准路径提供非默认过期时间时,请使用
expires_at进行临时代理授权
最佳实践
- 将代理用于自动化:将交易钱包密钥保存在冷存储中,为自动化系统使用代理
- 限制代理范围:仅批准需要访问权限的代理
- 监控代理使用情况:跟踪哪些代理在下单
- 及时撤销:在不再需要时立即撤销代理
常见问题
「Unauthorized: signer not authorized for wallet」
原因:代理未获批准,或授权已过期/已撤销。
解决方案:通过 POST /approve-agent 批准代理,或直接使用钱包签名。
代理授权不生效
原因:
- 代理不存在于引擎持有的授权状态中
- 授权已被撤销
- 授权已过期
解决方案:通过 GET /authorized-agents?wallet=... 检查代理状态。