hyperliquid-python-sdk 的 utils 层之所以存在,是因为 Hyperliquid 的交易所端点不接受用于交易操作的 bearer-token JSON 请求体。相反,客户端会构建一个类型化操作,对其进行确定性哈希,然后使用 API 钱包的私钥通过 EIP-712 风格的类型化数据对该哈希进行签名,并将签名、nonce 和 vault 地址附加到请求中。utils 模块提供了操作哈希辅助函数、签名构造以及浮点数到传输格式的格式化辅助函数,以确保签名字节与传输字节完全一致。浮点数格式化是一个正确性隐患:如果客户端签名一个十进制字符串但传输另一个,重建的哈希就会不同,交易所会拒绝该请求。本文解释了该机制,提供了用于下单和多操作信封的可运行 Python 示例,并将故障模式映射到诊断步骤。
为什么 Hyperliquid 需要客户端签名层
Hyperliquid 的交易所 API 不是传统的 REST API,后者通过 bearer token 授权 JSON 请求体。根据 Hyperliquid API 文档,交易操作必须由注册到账户的 API 钱包签名。交易所会验证请求是否由该钱包授权,这正是 API 钱包可以在不持有提现权限的情况下进行交易的机制。
因此,签名层位于应用程序逻辑和 HTTP 传输之间。它接收结构化操作,生成该操作的确定性哈希,将哈希作为基于某个域(domain)的 EIP-712 类型化数据进行签名,并将结果与 nonce 和可选的 vault 地址一起封装。hyperliquid-python-sdk 在其 utils 模块中暴露了这一点,以便调用者无需重新实现哈希和编码规则。
这种分离也是为什么关于“hyperliquid utils github”的文章不应变成安装说明。重要的产物是协议层面的心智模型:操作、确定性哈希、类型化数据签名、nonce/vault 信封、POST。该模型可跨 Python、Rust 和 Go SDK 以及 CCXT 集成通用。
- 交易所端点多路复用类型化操作,而不是为每个操作暴露一个路由。
- API 钱包密钥授权交易操作,但与账户的提现权限不同。
- 签名负载必须与服务器重建的负载字节完全一致。
utils 模块的三项职责
utils 层有三个不同的任务,将它们混为一谈是集成错误的常见来源。首先,它对操作负载进行确定性哈希,以便签名字节与服务器重建的精确负载匹配。其次,它根据类型化数据构造签名,遵循 EIP-712 中描述的结构化哈希和签名方案。第三,它将浮点数格式化为具有固定小数位数的传输字符串。
浮点数格式化辅助函数并非装饰性的。在哈希之前,价格和数量必须按照每种资产的正确小数位数转换为字符串。如果客户端签名“1.2345678901”但传输“1.23456789”,哈希就会不同,交易所会拒绝订单或错误定价。utils 格式化辅助函数以及每种币种的数量和价格小数规则必须在签名边界的两侧一致使用。
地址和密钥处理是第四个隐含职责。API 钱包私钥应从环境变量或密钥管理器加载,绝不能提交到源代码管理中,并且 SDK 的钱包对象应是进程中唯一存放密钥的地方。OnFinality Learn 中心 有关于端点选择和错误处理的相关材料,可作为这篇以签名为重点的文章的补充。
- 操作哈希:操作负载的确定性序列化。
- 签名构造:基于 Hyperliquid 域的类型化数据签名。
- 浮点数格式化:价格和数量的固定小数位字符串化。
- 密钥处理:从环境变量或密钥管理器加载,进程内单一钱包对象。
操作哈希与字节一致性要求
哈希是在操作负载被规范化为传输表示后计算的。这意味着数值字段必须已经是具有正确小数位数的字符串,字段顺序必须遵循 SDK 的编码器,并且可选字段(如 vault 地址)必须一致地存在或不存在。服务器从请求体重建相同的负载并重新计算哈希;任何差异都会导致签名或负载哈希不匹配。
在实践中,最安全的模式是构建一次操作,使用 utils 辅助函数格式化一次,然后对哈希和传输使用同一个对象。不要为了显示而格式化,然后再为传输重新格式化,也不要让 JSON 序列化器在这两个步骤之间重新排序或重新量化字段。
Hyperliquid API 错误处理与订单拒绝 文章介绍了交易所如何暴露这些不匹配,但根本原因几乎总是客户端的格式化或排序差异。
- 在哈希之前将数值字段规范化为字符串。
- 对签名和传输使用同一个格式化后的操作对象。
- 避免使用会重新排序或重新量化字段的中间序列化器。
基于 Hyperliquid 域的 EIP-712 类型化数据签名
签名是基于类型化数据生成的,而不是基于 JSON 请求体的原始哈希。类型化数据结构包括域分隔符和操作类型,这将签名绑定到 Hyperliquid 交易所,并防止在不同域上重放。EIP-712 规范 定义了 SDK 遵循的哈希和签名规则。
附加到请求的信封包含签名组件、nonce 和可选的 vault 地址。Nonce 按签名者跟踪,每个新操作必须大于签名者最近 100 个 nonce 中的最小值,并且从未使用过,因此每个签名者使用单调递增计数器是实用规则;即使签名本身有效,重放或过期的 nonce 也会被拒绝。vault 地址字段仅在代表 vault 执行操作时才有意义。
由于类型化数据结构是协议层面的,它在不同客户端之间是相同的。由 Python SDK 生成的签名和由 Rust 或 Go SDK 对相同操作和 nonce 生成的签名应能相同地验证。这就是为什么心智模型可以通用,以及为什么最好将 utils 模块理解为协议的一种实现,而不是 Python 特有的便利工具。
- 域分隔符将签名绑定到 Hyperliquid 交易所。
- Nonce 必须按签名者单调递增。
- Vault 地址是可选的,仅在代表 vault 操作时设置。
可运行示例:签名并提交订单,然后读回
下面的示例停留在 utils 层,而不是隐藏在 Exchange.order 后面:它构建传输订单,将其转换为签名操作,使用 sign_l1_action 进行哈希和签名,并打印发送到交易所端点的精确负载。它假设 API 钱包私钥和主账户地址在环境变量中。这里的端点是占位符;请从 Hyperliquid RPC 端点(RPC Assistant) 页面或您自己的基础设施中选择一个。
有两个细节比样板代码更重要。首先,打印的负载就是必须发送的负载:用不同的浮点表示重新序列化订单会改变 msgpack 字节,从而改变哈希。其次,签名字段 p 和 s 是由 float_to_wire 生成的字符串,而不是浮点数,因为 msgpack 对整数和浮点数的编码方式不同,交易所会从传输的字符串重建操作。要确认往返,请使用 info 端点按 oid 读回订单,并将报告的价格和数量与您签名的字符串进行比较。
import json, os
from eth_account import Account
from hyperliquid.utils.constants import MAINNET_API_URL
from hyperliquid.utils.signing import (
float_to_wire, get_timestamp_ms, order_request_to_order_wire,
order_wires_to_order_action, sign_l1_action,
)
acct = Account.from_key(os.environ["HL_API_WALLET_PRIVATE_KEY"]) # API wallet
# 1) Build the action exactly as the signed payload will carry it.
req = {"coin": "BTC", "is_buy": True, "sz": 0.001, "limit_px": 60000.0,
"order_type": {"limit": {"tif": "Gtc"}}, "reduce_only": False}
order = order_request_to_order_wire(req, asset=0) # p and s become strings
action = order_wires_to_order_action([order], grouping="na")
# 2) Sign: msgpack(action) + 8-byte nonce + vault flag -> keccak -> EIP-712 "Agent".
nonce = get_timestamp_ms()
signature = sign_l1_action(acct, action, None, nonce, None, True)
# 3) Transmit exactly the strings that were signed.
payload = {"action": action, "nonce": nonce, "signature": signature, "vaultAddress": None}
print(json.dumps(payload, indent=2))
# POST as JSON to MAINNET_API_URL + "/exchange" with your HTTP client.
print("signed p:", repr(action["orders"][0]["p"]),
"signed s:", repr(action["orders"][0]["s"]),
"float_to_wire(60000.0):", repr(float_to_wire(60000.0)))跨操作类型复用同一个签名信封
交易所端点多路复用类型化操作,因此相同的签名路径适用于订单、取消和杠杆更新。每种操作的字段集不同,但信封结构相同:操作、nonce、签名和可选的 vault 地址。在代码中看到这一点使抽象变得具体,并显示每种操作的字段在哪里不同。
下面的示例通过同一个 exchange 对象发出取消和更新杠杆请求。每次调用内部都会构建操作、格式化、哈希、签名并附加新的 nonce。每次调用都会用各自的 nonce 重新签名新操作,因此读者应观察到每种操作类型不需要单独的签名代码;utils 层处理编码差异。
import os
from hyperliquid.exchange import Exchange
from hyperliquid.utils import constants
from eth_account import Account
api_key = os.environ["HL_API_WALLET_PRIVATE_KEY"]
account = Account.from_key(api_key)
account_address = os.environ["HL_ACCOUNT_ADDRESS"]
exchange = Exchange(account, constants.MAINNET_API_URL, account_address=account_address)
# Place an order to obtain an oid.
order_result = exchange.order(
"ETH", True, 0.01, 3000.0, {"limit": {"tif": "Gtc"}}, reduce_only=False
)
print("order_result:", order_result)
# Cancel the order using the same signing path.
if order_result.get("status") == "ok":
oid = order_result["response"]["data"]["statuses"][0]["resting"]["oid"]
cancel_result = exchange.cancel("ETH", oid)
print("cancel_result:", cancel_result)
# Update leverage for the same asset; a different action type, same envelope.
leverage_result = exchange.update_leverage(5, "ETH", is_cross=True)
print("leverage_result:", leverage_result)
浮点数格式化作为正确性隐患
浮点数格式化是负载哈希不匹配的最常见来源。每种资产都有自己的数量和价格小数规则,传输表示必须遵守这些规则。如果客户端签名的值的小数位数超过资产允许的范围,或者舍入方式与传输值不同,服务器会重建不同的负载,签名检查就会失败。
utils 辅助函数的存在就是为了集中这种格式化。对参与签名操作的每个数值字段使用它们,并且在格式化后不要应用额外的舍入。如果您的应用程序需要以不同方式显示价格,请将显示格式化与签名负载分开。
一个有用的诊断方法是在签名前立即记录格式化后的操作,并将其与传输层捕获的请求体进行比较。如果两者在任何数值字符串上不同,则不匹配在客户端,重试无法解决。
- 对所有签名的数值字段使用 SDK 的格式化辅助函数。
- 格式化后不要重新舍入。
- 记录格式化后的操作并与传输的请求体进行比较。
针对您自己的端点测量往返
由于端点行为因提供商而异,唯一可靠的测量是您针对自己的端点进行的测量。下表是一个模板:用您环境中的观察结果填充它,而不是依赖已发布的数字。目标是区分签名失败与传输或风险引擎失败。
重复运行相同的签名操作,并记录每次尝试的结果类别。如果失败集中在签名或负载哈希错误上,则问题在签名层。如果失败集中在 nonce 错误上,则问题是排序。如果失败集中在保证金或风险响应上,则签名层正常工作,问题在于账户状态。
- 结果表列:尝试、操作类型、结果类别、错误代码、格式化负载哈希、传输负载哈希。
- 结果类别:已接受、签名错误、负载哈希不匹配、nonce 错误、风险引擎拒绝、传输错误。
- 对每次失败的尝试比较格式化和传输的哈希。
故障模式及区分它们的诊断步骤
签名和地址验证错误通常意味着错误的 API 钱包正在签名,或者签名域与环境不匹配。确认 API 钱包地址已注册到账户,并且 SDK 指向正确的网络。Hyperliquid API 文档 描述了注册模型。
负载哈希不匹配几乎总是追溯到浮点数格式化或字段排序。将格式化后的操作与传输的请求体进行比较。Nonce 重用或排序问题即使签名有效也会产生错误;检查 nonce 是否按签名者单调递增,以及并发请求是否共享 nonce。
保证金不足拒绝是风险引擎响应,不是签名错误。签名已验证,操作已解析;账户只是缺少保证金。Hyperliquid API 错误处理与订单拒绝 文章介绍了响应形状,Hyperliquid API 速率限制指南 介绍了可能伪装成传输失败的节流行为。
- 签名错误:验证 API 钱包注册和签名域。
- 负载哈希不匹配:比较格式化和传输的数值字符串。
- Nonce 错误:强制每个签名者的 nonce 单调递增。
- 风险引擎拒绝:检查账户保证金和仓位状态。
与 CCXT 以及 Rust 和 Go SDK 的关系
哈希和类型化数据结构是协议层面的,在不同客户端之间相同。CCXT 集成以及 Rust 和 Go SDK 实现相同的操作到哈希到签名的管道,因此即使 API 表面不同,心智模型也可以通用。这就是为什么最好将 utils 层作为协议实现来研究,而不是作为 Python 特有的实用工具。
在比较实现时,关注三件事:每个客户端如何规范化数值字段、如何构造域分隔符以及如何管理 nonce。这三个方面的差异解释了大多数跨客户端差异。Hyperliquid WebSocket 交易和成交频道 和 Hyperliquid WebSocket ping/pong 心跳检测 文章介绍了流式传输方面,它使用与签名交易所操作不同的认证路径。
- 比较跨客户端的数值规范化。
- 比较域分隔符构造。
- 比较 nonce 管理策略。
局限性、权衡与后续步骤
utils 层并不能消除理解协议的需要。它集中了格式化和签名,但无法阻止调用者在签名后重新格式化值,或在并发请求之间共享 nonce。一旦操作被哈希,就应将其视为不可变。
提供商特定的行为,包括端点可用性和速率限制,因提供商而异,应根据您自己的测量进行验证。RPC 定价 和 API 服务 页面描述了 OnFinality 的产品,Hyperliquid RPC 端点(RPC Assistant) 页面有助于端点选择。有关网络层面的背景,请参阅 Hyperliquid 网络页面。
作为下一步,请对您的客户端进行插桩,记录每个请求的格式化操作、计算的哈希和传输的请求体,然后针对您的端点运行结果表练习。这种做法将签名失败从模糊的错误转变为可诊断的事件。
- 哈希后将格式化操作视为不可变。
- 用您自己的测量验证提供商行为。
- 对格式化操作、哈希和传输请求体进行日志记录。