View Source Hyperliquid.Api.ActionEncoder (hyperliquid v0.3.0)

Canonical JSON encoding for exchange actions.

Hyperliquid derives an L1 action's connection id from the msgpack encoding of the action. msgpack preserves field order, so the resulting hash depends on the order in which fields are serialized.

Elixir maps do not preserve insertion order. Keys are stored in term order, and for atom keys that order follows the atom table, which is populated in a different sequence on every BEAM run. Encoding an action directly from a map therefore produced a different connection id on each boot:

0x4dfe89e6ccf2066c47f5a9a935d4c0c82697b240071f83b9593a4b4db11bb7dc
0x5a9622bad83165511b1bfda92b4274f841547cbad8dc47bac0bcf4b29dd5f17a
0x716ee448bc143896bfad2dcda1baec8984550e6276caabb4c5e48ec11975fb28

This module renders an action into Jason.OrderedObject values using the field order fixed by the reference implementations, so the signed preimage is stable across runs and matches what the exchange computes.

Both the signed preimage and the request body must be built from the value returned by canonicalize/1, otherwise the bytes that were hashed and the bytes that were sent can disagree.

Field order

@field_order is a single global ranking applied at every level of the action. A key that does not appear in it sorts after every known key, lexicographically, so unrecognized actions still encode deterministically even when their canonical order is unknown.

The trading-path ranking is taken from the reference Python SDK (hyperliquid/utils/signing.pyorder_wires_to_order_action/3, order_request_to_order_wire/2, order_type_to_wire/1) and verified against it by test/api/action_encoder_test.exs.

Keys for the newer actions (HIP-4 outcomes, gossip priority, agent asset transfers) are ordered to match the field order declared by the nktkas TypeScript SDK's request schemas, which is the order its own canonicalize emits. Those have no published reference hashes to check against, so unlike the trading path they are not verified end to end — they are as good as that schema.

Summary

Functions

Recursively rewrite an action so that every map becomes an order-preserving Jason.OrderedObject in canonical field order.

Encode an action as canonical JSON.

The canonical field ranking, most significant first. Exposed for tests.

Functions

@spec canonicalize(term()) :: term()

Recursively rewrite an action so that every map becomes an order-preserving Jason.OrderedObject in canonical field order.

Lists are walked element-wise. Scalars are returned unchanged.

@spec encode(term()) :: {:ok, String.t()} | {:error, Exception.t()}

Encode an action as canonical JSON.

Returns the same {:ok, iodata} / {:error, reason} shape as Jason.encode/1.

@spec field_order() :: [String.t()]

The canonical field ranking, most significant first. Exposed for tests.