Skip to content

Commit d0beee2

Browse files
committed
fix: msgpack field ordering
1 parent 3003166 commit d0beee2

10 files changed

Lines changed: 526 additions & 229 deletions

actions.go

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
package hyperliquid
2+
3+
// Action structs with deterministic field ordering for consistent MessagePack serialization
4+
// The order of fields in these structs is critical for signature generation
5+
6+
// CancelOrderWire represents cancel order item wire format
7+
type CancelOrderWire struct {
8+
Asset int `json:"a" msgpack:"a"`
9+
OrderID string `json:"o" msgpack:"o"`
10+
}
11+
12+
// CancelAction represents the cancel action
13+
type CancelAction struct {
14+
Type string `json:"type" msgpack:"type"`
15+
Cancels []CancelOrderWire `json:"cancels" msgpack:"cancels"`
16+
}
17+
18+
// CancelByCloidWire represents cancel by cloid item wire format
19+
type CancelByCloidWire struct {
20+
Asset int `json:"a" msgpack:"a"`
21+
OrderID string `json:"cloid" msgpack:"cloid"`
22+
}
23+
24+
// CancelByCloidAction represents the cancel by cloid action
25+
type CancelByCloidAction struct {
26+
Type string `json:"type" msgpack:"type"`
27+
Cancels []CancelByCloidWire `json:"cancels" msgpack:"cancels"`
28+
}
29+
30+
// UsdClassTransferAction represents USD class transfer
31+
type UsdClassTransferAction struct {
32+
Type string `json:"type" msgpack:"type"`
33+
Amount string `json:"amount" msgpack:"amount"`
34+
ToPerp bool `json:"toPerp" msgpack:"toPerp"`
35+
Nonce int64 `json:"nonce" msgpack:"nonce"`
36+
}
37+
38+
// SpotTransferAction represents spot transfer
39+
type SpotTransferAction struct {
40+
Type string `json:"type" msgpack:"type"`
41+
Destination string `json:"destination" msgpack:"destination"`
42+
Amount string `json:"amount" msgpack:"amount"`
43+
Token string `json:"token" msgpack:"token"`
44+
Time int64 `json:"time" msgpack:"time"`
45+
}
46+
47+
// UsdTransferAction represents USD transfer
48+
type UsdTransferAction struct {
49+
Type string `json:"type" msgpack:"type"`
50+
Destination string `json:"destination" msgpack:"destination"`
51+
Amount string `json:"amount" msgpack:"amount"`
52+
Time int64 `json:"time" msgpack:"time"`
53+
}
54+
55+
// SubAccountTransferAction represents sub-account transfer
56+
type SubAccountTransferAction struct {
57+
Type string `json:"type" msgpack:"type"`
58+
SubAccountUser string `json:"subAccountUser" msgpack:"subAccountUser"`
59+
IsDeposit bool `json:"isDeposit" msgpack:"isDeposit"`
60+
Usd int `json:"usd" msgpack:"usd"`
61+
}
62+
63+
// VaultUsdTransferAction represents vault USD transfer
64+
type VaultUsdTransferAction struct {
65+
Type string `json:"type" msgpack:"type"`
66+
VaultAddress string `json:"vaultAddress" msgpack:"vaultAddress"`
67+
IsDeposit bool `json:"isDeposit" msgpack:"isDeposit"`
68+
Usd int `json:"usd" msgpack:"usd"`
69+
}
70+
71+
// UpdateLeverageAction represents leverage update
72+
type UpdateLeverageAction struct {
73+
Type string `json:"type" msgpack:"type"`
74+
Asset int `json:"asset" msgpack:"asset"`
75+
Leverage map[string]any `json:"leverage" msgpack:"leverage"`
76+
}
77+
78+
// UpdateIsolatedMarginAction represents isolated margin update
79+
type UpdateIsolatedMarginAction struct {
80+
Type string `json:"type" msgpack:"type"`
81+
Asset int `json:"asset" msgpack:"asset"`
82+
IsBuy bool `json:"isBuy" msgpack:"isBuy"`
83+
Ntli float64 `json:"ntli" msgpack:"ntli"`
84+
}
85+
86+
// OrderWire represents the wire format for orders with deterministic field ordering
87+
type OrderWire struct {
88+
Asset int `json:"a" msgpack:"a"`
89+
IsBuy bool `json:"b" msgpack:"b"`
90+
LimitPx string `json:"p" msgpack:"p"`
91+
Size string `json:"s" msgpack:"s"`
92+
ReduceOnly bool `json:"r" msgpack:"r"`
93+
OrderType map[string]any `json:"t" msgpack:"t"`
94+
Cloid *string `json:"c,omitempty" msgpack:"c,omitempty"`
95+
}
96+
97+
// OrderAction represents the order action with deterministic field ordering
98+
type OrderAction struct {
99+
Type string `json:"type" msgpack:"type"`
100+
Orders []OrderWire `json:"orders" msgpack:"orders"`
101+
Grouping string `json:"grouping" msgpack:"grouping"`
102+
Builder *BuilderInfo `json:"builder,omitempty" msgpack:"builder,omitempty"`
103+
}
104+
105+
// ModifyAction represents a single order modification
106+
type ModifyAction struct {
107+
Type string `json:"type" msgpack:"type"`
108+
Oid any `json:"oid" msgpack:"oid"`
109+
Order OrderWire `json:"order" msgpack:"order"`
110+
}
111+
112+
// BatchModifyAction represents multiple order modifications
113+
type BatchModifyAction struct {
114+
Type string `json:"type" msgpack:"type"`
115+
Modifies []ModifyAction `json:"modifies" msgpack:"modifies"`
116+
}
117+
118+
// PerpDexClassTransferAction represents perp dex class transfer
119+
type PerpDexClassTransferAction struct {
120+
Type string `json:"type" msgpack:"type"`
121+
Dex string `json:"dex" msgpack:"dex"`
122+
Token string `json:"token" msgpack:"token"`
123+
Amount float64 `json:"amount" msgpack:"amount"`
124+
ToPerp bool `json:"toPerp" msgpack:"toPerp"`
125+
}
126+
127+
// SubAccountSpotTransferAction represents sub-account spot transfer
128+
type SubAccountSpotTransferAction struct {
129+
Type string `json:"type" msgpack:"type"`
130+
SubAccountUser string `json:"subAccountUser" msgpack:"subAccountUser"`
131+
IsDeposit bool `json:"isDeposit" msgpack:"isDeposit"`
132+
Token string `json:"token" msgpack:"token"`
133+
Amount float64 `json:"amount" msgpack:"amount"`
134+
}
135+
136+
// ScheduleCancelAction represents schedule cancel action
137+
type ScheduleCancelAction struct {
138+
Type string `json:"type" msgpack:"type"`
139+
Time *int64 `json:"time,omitempty" msgpack:"time,omitempty"`
140+
}
141+
142+
// SetReferrerAction represents set referrer action
143+
type SetReferrerAction struct {
144+
Type string `json:"type" msgpack:"type"`
145+
Code string `json:"code" msgpack:"code"`
146+
}
147+
148+
// CreateSubAccountAction represents create sub-account action
149+
type CreateSubAccountAction struct {
150+
Type string `json:"type" msgpack:"type"`
151+
Name string `json:"name" msgpack:"name"`
152+
}
153+
154+
// UseBigBlocksAction represents use big blocks action
155+
type UseBigBlocksAction struct {
156+
Type string `json:"type" msgpack:"type"`
157+
UsingBigBlocks bool `json:"usingBigBlocks" msgpack:"usingBigBlocks"`
158+
}
159+
160+
// TokenDelegateAction represents token delegate action
161+
type TokenDelegateAction struct {
162+
Type string `json:"type" msgpack:"type"`
163+
Validator string `json:"validator" msgpack:"validator"`
164+
Wei int `json:"wei" msgpack:"wei"`
165+
IsUndelegate bool `json:"isUndelegate" msgpack:"isUndelegate"`
166+
Nonce int64 `json:"nonce" msgpack:"nonce"`
167+
}
168+
169+
// WithdrawFromBridgeAction represents withdraw from bridge action
170+
type WithdrawFromBridgeAction struct {
171+
Type string `json:"type" msgpack:"type"`
172+
Destination string `json:"destination" msgpack:"destination"`
173+
Amount string `json:"amount" msgpack:"amount"`
174+
Time int64 `json:"time" msgpack:"time"`
175+
}
176+
177+
// ApproveAgentAction represents approve agent action
178+
type ApproveAgentAction struct {
179+
Type string `json:"type" msgpack:"type"`
180+
AgentAddress string `json:"agentAddress" msgpack:"agentAddress"`
181+
AgentName *string `json:"agentName,omitempty" msgpack:"agentName,omitempty"`
182+
Nonce int64 `json:"nonce" msgpack:"nonce"`
183+
}
184+
185+
// ApproveBuilderFeeAction represents approve builder fee action
186+
type ApproveBuilderFeeAction struct {
187+
Type string `json:"type" msgpack:"type"`
188+
Builder string `json:"builder" msgpack:"builder"`
189+
MaxFeeRate string `json:"maxFeeRate" msgpack:"maxFeeRate"`
190+
Nonce int64 `json:"nonce" msgpack:"nonce"`
191+
}
192+
193+
// ConvertToMultiSigUserAction represents convert to multi-sig user action
194+
type ConvertToMultiSigUserAction struct {
195+
Type string `json:"type" msgpack:"type"`
196+
Signers string `json:"signers" msgpack:"signers"`
197+
Nonce int64 `json:"nonce" msgpack:"nonce"`
198+
}
199+
200+
// MultiSigAction represents multi-signature action
201+
type MultiSigAction struct {
202+
Type string `json:"type" msgpack:"type"`
203+
Action map[string]any `json:"action" msgpack:"action"`
204+
Signers []string `json:"signers" msgpack:"signers"`
205+
Signatures []string `json:"signatures" msgpack:"signatures"`
206+
}

api.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010

1111
// Pool of parsers to avoid allocations
1212
var parserPool = sync.Pool{
13-
New: func() interface{} {
13+
New: func() any {
1414
return &fastjson.Parser{}
1515
},
1616
}

api_test.go

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -241,9 +241,13 @@ func TestMixedValue_Array(t *testing.T) {
241241
wantOk bool
242242
}{
243243
{
244-
name: "valid array",
245-
mv: MixedValue(`[1,"hello",true]`),
246-
want: []json.RawMessage{json.RawMessage("1"), json.RawMessage(`"hello"`), json.RawMessage("true")},
244+
name: "valid array",
245+
mv: MixedValue(`[1,"hello",true]`),
246+
want: []json.RawMessage{
247+
json.RawMessage("1"),
248+
json.RawMessage(`"hello"`),
249+
json.RawMessage("true"),
250+
},
247251
wantOk: true,
248252
},
249253
{
@@ -253,9 +257,12 @@ func TestMixedValue_Array(t *testing.T) {
253257
wantOk: true,
254258
},
255259
{
256-
name: "array with objects",
257-
mv: MixedValue(`[{"key":"value"},{"num":42}]`),
258-
want: []json.RawMessage{json.RawMessage(`{"key":"value"}`), json.RawMessage(`{"num":42}`)},
260+
name: "array with objects",
261+
mv: MixedValue(`[{"key":"value"},{"num":42}]`),
262+
want: []json.RawMessage{
263+
json.RawMessage(`{"key":"value"}`),
264+
json.RawMessage(`{"num":42}`),
265+
},
259266
wantOk: true,
260267
},
261268
{
@@ -618,12 +625,12 @@ func TestMixedValue_IntegrationWithComplexData(t *testing.T) {
618625

619626
// Test parsing to a struct
620627
type ComplexStruct struct {
621-
StringField string `json:"string_field"`
622-
NumberField int `json:"number_field"`
623-
BooleanField bool `json:"boolean_field"`
624-
NullField *string `json:"null_field"`
625-
ObjectField map[string]interface{} `json:"object_field"`
626-
ArrayField []interface{} `json:"array_field"`
628+
StringField string `json:"string_field"`
629+
NumberField int `json:"number_field"`
630+
BooleanField bool `json:"boolean_field"`
631+
NullField *string `json:"null_field"`
632+
ObjectField map[string]any `json:"object_field"`
633+
ArrayField []any `json:"array_field"`
627634
}
628635

629636
var result ComplexStruct
@@ -634,7 +641,7 @@ func TestMixedValue_IntegrationWithComplexData(t *testing.T) {
634641
assert.Equal(t, 42, result.NumberField)
635642
assert.Equal(t, true, result.BooleanField)
636643
assert.Nil(t, result.NullField)
637-
assert.Equal(t, map[string]interface{}{"nested": "value"}, result.ObjectField)
644+
assert.Equal(t, map[string]any{"nested": "value"}, result.ObjectField)
638645
assert.Len(t, result.ArrayField, 5)
639646

640647
// Test round-trip marshaling

exchange.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ func NewExchange(
3232
}
3333

3434
// executeAction executes an action and unmarshals the response into the given result
35-
func (e *Exchange) executeAction(action map[string]any, result any) error {
35+
func (e *Exchange) executeAction(action any, result any) error {
3636
timestamp := time.Now().UnixMilli()
3737

3838
sig, err := SignL1Action(
@@ -60,7 +60,7 @@ func (e *Exchange) executeAction(action map[string]any, result any) error {
6060
}
6161

6262
func (e *Exchange) postAction(
63-
action map[string]any,
63+
action any,
6464
signature SignatureResult,
6565
nonce int64,
6666
) ([]byte, error) {
@@ -70,10 +70,17 @@ func (e *Exchange) postAction(
7070
"signature": signature,
7171
}
7272

73-
if action["type"] != "usdClassTransfer" {
74-
payload["vaultAddress"] = e.vault
73+
// Handle vault address based on action type
74+
if actionMap, ok := action.(map[string]any); ok {
75+
if actionMap["type"] != "usdClassTransfer" {
76+
payload["vaultAddress"] = e.vault
77+
} else {
78+
payload["vaultAddress"] = nil
79+
}
7580
} else {
76-
payload["vaultAddress"] = nil
81+
// For struct types, we need to use reflection or type assertion
82+
// For now, assume it's not usdClassTransfer
83+
payload["vaultAddress"] = e.vault
7784
}
7885

7986
// Add expiration time if set

0 commit comments

Comments
 (0)