Skip to content

Commit 00d7290

Browse files
authored
Merge pull request #21 from terwey/tests/exchange-orders-cancel
Initial unit tests for Order Cancel
2 parents fe48a0f + 358455e commit 00d7290

6 files changed

Lines changed: 461 additions & 4 deletions

File tree

actions.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ package hyperliquid
55

66
// CancelOrderWire represents cancel order item wire format
77
type CancelOrderWire struct {
8-
Asset int `json:"a" msgpack:"a"`
9-
OrderID string `json:"o" msgpack:"o"`
8+
Asset int `json:"a" msgpack:"a"`
9+
OrderID int64 `json:"o" msgpack:"o"`
1010
}
1111

1212
// CancelAction represents the cancel action

api.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,28 @@ func (ma *MixedArray) UnmarshalJSON(data []byte) error {
137137
*ma = rawArr
138138
return nil
139139
}
140+
141+
func (ma MixedArray) FirstError() error {
142+
for _, mv := range ma {
143+
if s, ok := mv.String(); ok {
144+
if s == "success" {
145+
continue
146+
}
147+
// any other string? treat as error text
148+
return fmt.Errorf(s)
149+
}
150+
if obj, ok := mv.Object(); ok {
151+
if v, ok := obj["error"]; ok {
152+
if msg, ok := v.(string); ok && msg != "" {
153+
return fmt.Errorf(msg)
154+
}
155+
// stringify unknown error shapes
156+
b, _ := json.Marshal(v)
157+
return fmt.Errorf(string(b))
158+
}
159+
}
160+
// Unknown shape -> generic failure
161+
return fmt.Errorf("cancel failed")
162+
}
163+
return nil
164+
}

api_test.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -654,3 +654,16 @@ func TestMixedValue_IntegrationWithComplexData(t *testing.T) {
654654

655655
assert.Equal(t, mv.Type(), mv2.Type())
656656
}
657+
658+
func TestMixedArray_FirstError(t *testing.T) {
659+
input := `{"status":"ok","response":{"type":"cancel","data":{"statuses":[{"error":"Order was never placed, already canceled, or filled. asset=173"}]}}}`
660+
res := &APIResponse[CancelOrderResponse]{}
661+
662+
err := json.Unmarshal([]byte(input), res)
663+
require.NoError(t, err)
664+
665+
want := "Order was never placed, already canceled, or filled. asset=173"
666+
got := res.Data.Statuses.FirstError()
667+
668+
require.ErrorContains(t, got, want)
669+
}

exchange_orders_cancel.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package hyperliquid
22

33
import (
4-
"strconv"
4+
"fmt"
55

66
"github.com/sonirico/vago/slices"
77
)
@@ -35,7 +35,7 @@ func (e *Exchange) BulkCancel(
3535
cancels := slices.Map(requests, func(req CancelOrderRequest) CancelOrderWire {
3636
return CancelOrderWire{
3737
Asset: e.info.NameToAsset(req.Coin),
38-
OrderID: strconv.FormatInt(req.OrderID, 10),
38+
OrderID: req.OrderID,
3939
}
4040
})
4141

@@ -47,6 +47,18 @@ func (e *Exchange) BulkCancel(
4747
if err = e.executeAction(action, &res); err != nil {
4848
return
4949
}
50+
51+
if res == nil || !res.Ok || res.Status == "err" {
52+
if res != nil && res.Err != "" {
53+
return res, fmt.Errorf(res.Err)
54+
}
55+
return res, fmt.Errorf("cancel failed")
56+
}
57+
58+
if err := res.Data.Statuses.FirstError(); err != nil {
59+
return res, err
60+
}
61+
5062
return
5163
}
5264

exchange_orders_cancel_test.go

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package hyperliquid
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
)
8+
9+
var recordForDebug = false
10+
11+
func TestCancel(t *testing.T) {
12+
type tc struct {
13+
name string
14+
cassetteName string
15+
// If placeFirst is true, we first place a resting order and use its OID.
16+
placeFirst bool
17+
order CreateOrderRequest
18+
coin string
19+
oid int64 // used only when placeFirst == false
20+
// If doubleCancel is true, we attempt to cancel the same OID twice to exercise the error path.
21+
doubleCancel bool
22+
wantErr string
23+
record bool
24+
}
25+
26+
cases := []tc{
27+
{
28+
name: "cancel resting order by oid",
29+
cassetteName: "Cancel",
30+
placeFirst: true,
31+
order: CreateOrderRequest{
32+
Coin: "DOGE",
33+
IsBuy: true,
34+
Size: 45,
35+
Price: 0.12330, // low so it stays resting
36+
OrderType: OrderType{
37+
Limit: &LimitOrderType{Tif: TifGtc},
38+
},
39+
},
40+
coin: "DOGE",
41+
record: recordForDebug,
42+
},
43+
{
44+
name: "double cancel returns error on second attempt",
45+
cassetteName: "Cancel",
46+
placeFirst: true,
47+
order: CreateOrderRequest{
48+
Coin: "DOGE",
49+
IsBuy: true,
50+
Size: 45,
51+
Price: 0.12330,
52+
OrderType: OrderType{
53+
Limit: &LimitOrderType{Tif: TifGtc},
54+
},
55+
},
56+
coin: "DOGE",
57+
doubleCancel: true,
58+
wantErr: "already canceled",
59+
record: recordForDebug,
60+
},
61+
{
62+
name: "cancel non-existent oid",
63+
cassetteName: "Cancel",
64+
placeFirst: false,
65+
coin: "DOGE",
66+
oid: 1,
67+
wantErr: "Order was never placed, already canceled, or filled.",
68+
record: recordForDebug,
69+
},
70+
}
71+
72+
for _, tc := range cases {
73+
t.Run(tc.name, func(tt *testing.T) {
74+
initRecorder(tt, tc.record, tc.cassetteName)
75+
76+
exchange, err := newExchange(
77+
"0x38d55ff1195c57b9dbc8a72c93119500f1fcd47a33f98149faa18d2fc37932fa",
78+
TestnetAPIURL)
79+
require.NoError(t, err)
80+
81+
oid := tc.oid
82+
if tc.placeFirst {
83+
placed, err := exchange.Order(tc.order, nil)
84+
require.NoError(tt, err)
85+
require.NotNil(tt, placed.Resting, "expected resting order so it can be canceled")
86+
oid = placed.Resting.Oid
87+
}
88+
89+
// First cancel
90+
resp, err := exchange.Cancel(tc.coin, oid)
91+
if tc.wantErr != "" && !tc.doubleCancel {
92+
require.Error(tt, err)
93+
require.Contains(tt, err.Error(), tc.wantErr)
94+
return
95+
}
96+
require.NoError(tt, err)
97+
tt.Logf("cancel response: %+v", resp)
98+
99+
// Optional second cancel to test error path
100+
if tc.doubleCancel {
101+
resp2, err2 := exchange.Cancel(tc.coin, oid)
102+
require.Error(tt, err2, "expected error on second cancel")
103+
if tc.wantErr != "" {
104+
require.Contains(tt, err2.Error(), tc.wantErr)
105+
}
106+
tt.Logf("second cancel response: %+v, err: %v", resp2, err2)
107+
}
108+
})
109+
}
110+
}

0 commit comments

Comments
 (0)