Skip to content

Commit 36dfc17

Browse files
committed
add spot trading support
1 parent 5d88441 commit 36dfc17

File tree

9 files changed

+286
-103
lines changed

9 files changed

+286
-103
lines changed

hyperliquid/api.go

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -39,27 +39,28 @@ func MakeUniversalRequest[T any](api IAPIService, request any) (*T, error) {
3939
if api.Endpoint() == "/exchange" && api.KeyManager() == nil {
4040
return nil, APIError{Message: "API key not set"}
4141
}
42+
4243
response, err := api.Request(api.Endpoint(), request)
4344
if err != nil {
4445
return nil, err
4546
}
47+
4648
var result T
4749
err = json.Unmarshal(response, &result)
50+
if err == nil {
51+
return &result, nil
52+
}
53+
54+
var errResult map[string]interface{}
55+
err = json.Unmarshal(response, &errResult)
4856
if err != nil {
49-
api.debug("Error json.Unmarshal: %s", err)
50-
var errResult map[string]interface{}
51-
err = json.Unmarshal(response, &errResult)
52-
if err != nil {
53-
api.debug("Error second json.Unmarshal: %s", err)
54-
return nil, APIError{Message: "Unexpected response"}
55-
}
56-
// Check if the result is an error
57-
// Return an APIError if it is
58-
if errResult["status"] == "err" {
59-
return nil, APIError{Message: errResult["response"].(string)}
60-
} else {
61-
return nil, APIError{Message: fmt.Sprintf("Unexpected response: %v", errResult)}
62-
}
57+
api.debug("Error second json.Unmarshal: %s", err)
58+
return nil, APIError{Message: "Unexpected response"}
6359
}
64-
return &result, nil
60+
61+
if errResult["status"] == "err" {
62+
return nil, APIError{Message: errResult["response"].(string)}
63+
}
64+
65+
return nil, APIError{Message: fmt.Sprintf("Unexpected response: %v", errResult)}
6566
}

hyperliquid/client.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,9 @@ func (client *Client) debug(format string, v ...interface{}) {
8686

8787
// SetPrivateKey sets the private key for the client.
8888
func (client *Client) SetPrivateKey(privateKey string) error {
89+
if strings.HasPrefix(privateKey, "0x") {
90+
privateKey = strings.TrimPrefix(privateKey, "0x") // remove 0x prefix from private key
91+
}
8992
client.privateKey = privateKey
9093
var err error
9194
client.keyManager, err = NewPKeyManager(privateKey)

hyperliquid/convert.go

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,25 @@ func OrderWiresToOrderAction(orders []OrderWire, grouping Grouping) PlaceOrderAc
4242
}
4343
}
4444

45-
func OrderRequestToWire(req OrderRequest, meta map[string]AssetInfo) OrderWire {
46-
info := meta[req.Coin]
45+
func OrderRequestToWire(req OrderRequest, meta map[string]AssetInfo, isSpot bool) OrderWire {
46+
47+
var info AssetInfo
48+
for _, v := range meta {
49+
if v.SpotName == req.Coin {
50+
info = v
51+
break
52+
}
53+
}
54+
55+
var assetId int
56+
if isSpot {
57+
assetId = info.AssetId + 10000
58+
} else {
59+
assetId = info.AssetId
60+
}
61+
4762
return OrderWire{
48-
Asset: info.AssetId,
63+
Asset: assetId,
4964
IsBuy: req.IsBuy,
5065
LimitPx: FloatToWire(req.LimitPx, nil),
5166
SizePx: FloatToWire(req.Sz, &info.SzDecimals),

hyperliquid/exchange_service.go

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ type ExchangeAPI struct {
3636
address string
3737
baseEndpoint string
3838
meta map[string]AssetInfo
39+
spotMeta map[string]AssetInfo
3940
}
4041

4142
// NewExchangeAPI creates a new default ExchangeAPI.
@@ -54,6 +55,14 @@ func NewExchangeAPI(isMainnet bool) *ExchangeAPI {
5455
api.debug("Error building meta map: %s", err)
5556
}
5657
api.meta = meta
58+
59+
spotMeta, err := api.infoAPI.BuildSpotMetaMap()
60+
if err != nil {
61+
api.SetDebugActive()
62+
api.debug("Error building spot meta map: %s", err)
63+
}
64+
api.spotMeta = spotMeta
65+
5766
return &api
5867
}
5968

@@ -71,6 +80,17 @@ func (api *ExchangeAPI) SlippagePrice(coin string, isBuy bool, slippage float64)
7180
return CalculateSlippage(isBuy, marketPx, slippage)
7281
}
7382

83+
// SlippagePriceSpot is a helper function to calculate the slippage price for a spot coin.
84+
func (api *ExchangeAPI) SlippagePriceSpot(coin string, isBuy bool, slippage float64) float64 {
85+
marketPx, err := api.infoAPI.GetSpotMarketPx(coin)
86+
if err != nil {
87+
api.debug("Error getting market price: %s", err)
88+
return 0.0
89+
}
90+
slippagePrice := CalculateSlippage(isBuy, marketPx, slippage)
91+
return slippagePrice
92+
}
93+
7494
// Open a market order.
7595
// Limit order with TIF=IOC and px=market price * (1 +- slippage).
7696
// Size determines the amount of the coin to buy/sell.
@@ -98,6 +118,29 @@ func (api *ExchangeAPI) MarketOrder(coin string, size float64, slippage *float64
98118
return api.Order(orderRequest, GroupingNa)
99119
}
100120

121+
// MarketOrderSpot is a market order for a spot coin.
122+
// It is used to buy/sell a spot coin.
123+
func (api *ExchangeAPI) MarketOrderSpot(coin string, size float64, slippage *float64) (*PlaceOrderResponse, error) {
124+
spotName := api.spotMeta[coin].SpotName
125+
slpg := GetSlippage(slippage)
126+
isBuy := IsBuy(size)
127+
finalPx := api.SlippagePriceSpot(coin, isBuy, slpg)
128+
orderType := OrderType{
129+
Limit: &LimitOrderType{
130+
Tif: TifIoc,
131+
},
132+
}
133+
orderRequest := OrderRequest{
134+
Coin: spotName,
135+
IsBuy: isBuy,
136+
Sz: math.Abs(size),
137+
LimitPx: finalPx,
138+
OrderType: orderType,
139+
ReduceOnly: false,
140+
}
141+
return api.OrderSpot(orderRequest, GroupingNa)
142+
}
143+
101144
// Open a limit order.
102145
// Order type can be Gtc, Ioc, Alo.
103146
// Size determines the amount of the coin to buy/sell.
@@ -168,12 +211,39 @@ func (api *ExchangeAPI) Order(request OrderRequest, grouping Grouping) (*PlaceOr
168211
return api.BulkOrders([]OrderRequest{request}, grouping)
169212
}
170213

214+
// OrderSpot places a spot order
215+
func (api *ExchangeAPI) OrderSpot(request OrderRequest, grouping Grouping) (*PlaceOrderResponse, error) {
216+
return api.BulkOrdersSpot([]OrderRequest{request}, grouping)
217+
}
218+
171219
// Place orders in bulk
172220
// https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#place-an-order
173221
func (api *ExchangeAPI) BulkOrders(requests []OrderRequest, grouping Grouping) (*PlaceOrderResponse, error) {
174222
var wires []OrderWire
175223
for _, req := range requests {
176-
wires = append(wires, OrderRequestToWire(req, api.meta))
224+
wires = append(wires, OrderRequestToWire(req, api.meta, false))
225+
}
226+
timestamp := GetNonce()
227+
action := OrderWiresToOrderAction(wires, grouping)
228+
v, r, s, err := api.SignL1Action(action, timestamp)
229+
if err != nil {
230+
api.debug("Error signing L1 action: %s", err)
231+
return nil, err
232+
}
233+
request := ExchangeRequest{
234+
Action: action,
235+
Nonce: timestamp,
236+
Signature: ToTypedSig(r, s, v),
237+
VaultAddress: nil,
238+
}
239+
return MakeUniversalRequest[PlaceOrderResponse](api, request)
240+
}
241+
242+
// BulkOrdersSpot places spot orders
243+
func (api *ExchangeAPI) BulkOrdersSpot(requests []OrderRequest, grouping Grouping) (*PlaceOrderResponse, error) {
244+
var wires []OrderWire
245+
for _, req := range requests {
246+
wires = append(wires, OrderRequestToWire(req, api.spotMeta, true))
177247
}
178248
timestamp := GetNonce()
179249
action := OrderWiresToOrderAction(wires, grouping)
@@ -315,7 +385,7 @@ func (api *ExchangeAPI) getChainParams() (string, string) {
315385
func (api *ExchangeAPI) BuildBulkOrdersEIP712(requests []OrderRequest, grouping Grouping) (apitypes.TypedData, error) {
316386
var wires []OrderWire
317387
for _, req := range requests {
318-
wires = append(wires, OrderRequestToWire(req, api.meta))
388+
wires = append(wires, OrderRequestToWire(req, api.meta, false))
319389
}
320390
timestamp := GetNonce()
321391
action := OrderWiresToOrderAction(wires, grouping)

hyperliquid/exchange_types.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@ type ExchangeRequest struct {
1515
}
1616

1717
type AssetInfo struct {
18-
SzDecimals int
19-
AssetId int
18+
SzDecimals int
19+
WeiDecimals int
20+
AssetId int
21+
SpotName string // for spot asset (e.g. "@107")
2022
}
2123

2224
type OrderRequest struct {

hyperliquid/go.mod

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,28 @@
11
module github.com/Logarithm-Labs/go-hyperliquid/hyperliquid
22

3-
go 1.21.10
3+
go 1.23.4
44

55
require (
6-
github.com/ethereum/go-ethereum v1.14.3
6+
github.com/ethereum/go-ethereum v1.14.12
77
github.com/sirupsen/logrus v1.9.3
88
github.com/vmihailenco/msgpack/v5 v5.4.1
99
)
1010

1111
require (
12-
github.com/bits-and-blooms/bitset v1.10.0 // indirect
13-
github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect
12+
github.com/bits-and-blooms/bitset v1.13.0 // indirect
1413
github.com/consensys/bavard v0.1.13 // indirect
1514
github.com/consensys/gnark-crypto v0.12.1 // indirect
15+
github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c // indirect
1616
github.com/crate-crypto/go-kzg-4844 v1.0.0 // indirect
1717
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
1818
github.com/ethereum/c-kzg-4844 v1.0.0 // indirect
19-
github.com/holiman/uint256 v1.2.4 // indirect
19+
github.com/ethereum/go-verkle v0.1.1-0.20240829091221-dffa7562dbe9 // indirect
20+
github.com/holiman/uint256 v1.3.1 // indirect
2021
github.com/mmcloughlin/addchain v0.4.0 // indirect
21-
github.com/supranational/blst v0.3.11 // indirect
22+
github.com/supranational/blst v0.3.13 // indirect
2223
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
23-
golang.org/x/crypto v0.23.0 // indirect
24+
golang.org/x/crypto v0.22.0 // indirect
2425
golang.org/x/sync v0.7.0 // indirect
25-
golang.org/x/sys v0.20.0 // indirect
26+
golang.org/x/sys v0.22.0 // indirect
2627
rsc.io/tmplfunc v0.0.3 // indirect
2728
)

0 commit comments

Comments
 (0)