Skip to content

v0.3.1 — user-signed EIP-712 chainId aligned with the reference SDKs

Choose a tag to compare

@github-actions github-actions released this 09 Aug 05:33
· 27 commits to main since this release
71cc522

Fixes the failing usdSend EIP-712 vector carried into v0.3.0 as a known issue.

What was wrong

The test asserted the signature produced with EIP-712 domain chainId 421614 (0x66eee) while the library signed with 42161 (0xa4b1):

domain chainId resulting r
421614 (0x66eee) 0xf777c38e… ← reference SDKs
42161 (0xa4b1) 0xb25362bf… ← what the library emitted

This was not a functional break. The exchange rebuilds the EIP-712 domain from the action's signatureChainId to recover the signer, so only self-consistency matters — and the domain and signatureChainId did agree at 42161. Withdrawals and transfers worked. The library simply used a different convention from the official Python SDK and nktkas, which both use 0x66eee.

What changed

User-signed actions now sign with 0x66eee, so signatures are byte-comparable with both reference SDKs.

The fragility underneath was structural: the chain id lived in the Rust NIF and in a dozen Elixir modules independently. That duplication is how the domain and signatureChainId drifted apart before e0e67bf, which made the API recover the wrong address. Both now read Hyperliquid.Config.signature_chain_id/0.

Adds Hyperliquid.Api.Exchange.UserSigned for the shared domain and signing. The five actions that signed through specialized Rust NIFs (usdSend, withdraw3, spotSend, approveAgent, approveBuilderFee) route through it so configuration reaches them — verified byte-identical to the NIF path for all five before switching.

New tests pin domain ↔ signatureChainId across all five actions, assert the config override propagates to both, and assert the Rust NIF still agrees with the generic path so the two cannot silently diverge.

Both conventions exist in the wild

test/debug/send_asset_debug_test.exs holds payloads captured from the Hyperliquid frontend, which sends "signatureChainId": "0xa4b1" on testnet. So 42161 was not arbitrary — it is what the frontend does.

This release moves from the frontend's convention to the SDKs'. Both are accepted by the exchange. It does change the signing bytes for every user-signed action (withdrawals, USD sends, spot sends, agent approvals), so run a testnet transfer before relying on it in production. Reverting takes one line and no rebuild:

config :hyperliquid, signature_chain_id: 42_161

Upgrade notes

Precompiled NIFs are rebuilt for this release: the Rust chain/1 default moved to 421614 so the standalone Signer.sign_* functions stay in step with Config.signature_chain_id/0. Take the v0.3.1 artifacts if you vendor them.

Testing

280 tests, 25 pre-existing failures (down from 26), SignerUserSignedTest green. Verified by a clean-room build resolving the published artifacts.

The 4 SendAssetDebugTest failures are not a live signal: @api_key is the literal placeholder "YOUR_API_KEY_HERE", so both signing calls error identically and the "different chainIds must differ" assertion fails on equal error tuples. They have never been runnable as checked in.