diff --git a/hyperliquid/api.go b/hyperliquid/api.go index 63f8639..0f13100 100644 --- a/hyperliquid/api.go +++ b/hyperliquid/api.go @@ -39,27 +39,28 @@ func MakeUniversalRequest[T any](api IAPIService, request any) (*T, error) { if api.Endpoint() == "/exchange" && api.KeyManager() == nil { return nil, APIError{Message: "API key not set"} } + response, err := api.Request(api.Endpoint(), request) if err != nil { return nil, err } + var result T err = json.Unmarshal(response, &result) + if err == nil { + return &result, nil + } + + var errResult map[string]interface{} + err = json.Unmarshal(response, &errResult) if err != nil { - api.debug("Error json.Unmarshal: %s", err) - var errResult map[string]interface{} - err = json.Unmarshal(response, &errResult) - if err != nil { - api.debug("Error second json.Unmarshal: %s", err) - return nil, APIError{Message: "Unexpected response"} - } - // Check if the result is an error - // Return an APIError if it is - if errResult["status"] == "err" { - return nil, APIError{Message: errResult["response"].(string)} - } else { - return nil, APIError{Message: fmt.Sprintf("Unexpected response: %v", errResult)} - } + api.debug("Error second json.Unmarshal: %s", err) + return nil, APIError{Message: "Unexpected response"} } - return &result, nil + + if errResult["status"] == "err" { + return nil, APIError{Message: errResult["response"].(string)} + } + + return nil, APIError{Message: fmt.Sprintf("Unexpected response: %v", errResult)} } diff --git a/hyperliquid/client.go b/hyperliquid/client.go index 65ee701..5da47bc 100644 --- a/hyperliquid/client.go +++ b/hyperliquid/client.go @@ -86,6 +86,9 @@ func (client *Client) debug(format string, v ...interface{}) { // SetPrivateKey sets the private key for the client. func (client *Client) SetPrivateKey(privateKey string) error { + if strings.HasPrefix(privateKey, "0x") { + privateKey = strings.TrimPrefix(privateKey, "0x") // remove 0x prefix from private key + } client.privateKey = privateKey var err error client.keyManager, err = NewPKeyManager(privateKey) diff --git a/hyperliquid/consts.go b/hyperliquid/consts.go index bc1a5ee..6df7155 100644 --- a/hyperliquid/consts.go +++ b/hyperliquid/consts.go @@ -4,7 +4,9 @@ const GLOBAL_DEBUG = false // Defualt debug that is used in all tests // Execution constants const DEFAULT_SLIPPAGE = 0.005 // 0.5% default slippage -var SZ_DECIMALS = 2 // Default decimals for size +const SPOT_MAX_DECIMALS = 8 // Default decimals for spot +const PERP_MAX_DECIMALS = 6 // Default decimals for perp +var SZ_DECIMALS = 2 // Default decimals for usdc // Signing constants const HYPERLIQUID_CHAIN_ID = 1337 diff --git a/hyperliquid/convert.go b/hyperliquid/convert.go index 75a282a..50d8536 100644 --- a/hyperliquid/convert.go +++ b/hyperliquid/convert.go @@ -42,13 +42,22 @@ func OrderWiresToOrderAction(orders []OrderWire, grouping Grouping) PlaceOrderAc } } -func OrderRequestToWire(req OrderRequest, meta map[string]AssetInfo) OrderWire { +func OrderRequestToWire(req OrderRequest, meta map[string]AssetInfo, isSpot bool) OrderWire { info := meta[req.Coin] + var assetId, maxDecimals int + if isSpot { + // https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/asset-ids + assetId = info.AssetId + 10000 + maxDecimals = SPOT_MAX_DECIMALS + } else { + assetId = info.AssetId + maxDecimals = PERP_MAX_DECIMALS + } return OrderWire{ - Asset: info.AssetId, + Asset: assetId, IsBuy: req.IsBuy, - LimitPx: FloatToWire(req.LimitPx, nil), - SizePx: FloatToWire(req.Sz, &info.SzDecimals), + LimitPx: FloatToWire(req.LimitPx, maxDecimals, info.SzDecimals), + SizePx: FloatToWire(req.Sz, maxDecimals, info.SzDecimals), ReduceOnly: req.ReduceOnly, OrderType: OrderTypeToWire(req.OrderType), } @@ -75,28 +84,28 @@ func OrderTypeToWire(orderType OrderType) OrderTypeWire { return OrderTypeWire{} } -// Format the float with custom decimal places, default is 6. -// Hyperliquid only allows at most 6 digits. -func FloatToWire(x float64, szDecimals *int) string { +// Format the float with custom decimal places, default is 6 (perp), 8 (spot). +// https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/tick-and-lot-size +func FloatToWire(x float64, maxDecimals int, szDecimals int) string { bigf := big.NewFloat(x) var maxDecSz uint - if szDecimals != nil { - maxDecSz = uint(*szDecimals) + intPart, _ := bigf.Int64() + intSize := len(strconv.FormatInt(intPart, 10)) + if intSize >= maxDecimals { + maxDecSz = 0 } else { - intPart, _ := bigf.Int64() - intSize := len(strconv.FormatInt(intPart, 10)) - if intSize >= 6 { - maxDecSz = 0 - } else { - maxDecSz = uint(6 - intSize) - } + maxDecSz = uint(maxDecimals - intSize) } x, _ = bigf.Float64() rounded := fmt.Sprintf("%.*f", maxDecSz, x) - for strings.HasSuffix(rounded, "0") { - rounded = strings.TrimSuffix(rounded, "0") + if strings.Contains(rounded, ".") { + for strings.HasSuffix(rounded, "0") { + rounded = strings.TrimSuffix(rounded, "0") + } + } + if strings.HasSuffix(rounded, ".") { + rounded = strings.TrimSuffix(rounded, ".") } - rounded = strings.TrimSuffix(rounded, ".") return rounded } diff --git a/hyperliquid/exchange_service.go b/hyperliquid/exchange_service.go index 3b6f573..3a6a053 100644 --- a/hyperliquid/exchange_service.go +++ b/hyperliquid/exchange_service.go @@ -36,6 +36,7 @@ type ExchangeAPI struct { address string baseEndpoint string meta map[string]AssetInfo + spotMeta map[string]AssetInfo } // NewExchangeAPI creates a new default ExchangeAPI. @@ -54,6 +55,14 @@ func NewExchangeAPI(isMainnet bool) *ExchangeAPI { api.debug("Error building meta map: %s", err) } api.meta = meta + + spotMeta, err := api.infoAPI.BuildSpotMetaMap() + if err != nil { + api.SetDebugActive() + api.debug("Error building spot meta map: %s", err) + } + api.spotMeta = spotMeta + return &api } @@ -71,6 +80,17 @@ func (api *ExchangeAPI) SlippagePrice(coin string, isBuy bool, slippage float64) return CalculateSlippage(isBuy, marketPx, slippage) } +// SlippagePriceSpot is a helper function to calculate the slippage price for a spot coin. +func (api *ExchangeAPI) SlippagePriceSpot(coin string, isBuy bool, slippage float64) float64 { + marketPx, err := api.infoAPI.GetSpotMarketPx(coin) + if err != nil { + api.debug("Error getting market price: %s", err) + return 0.0 + } + slippagePrice := CalculateSlippage(isBuy, marketPx, slippage) + return slippagePrice +} + // Open a market order. // Limit order with TIF=IOC and px=market price * (1 +- slippage). // Size determines the amount of the coin to buy/sell. @@ -98,6 +118,34 @@ func (api *ExchangeAPI) MarketOrder(coin string, size float64, slippage *float64 return api.Order(orderRequest, GroupingNa) } +// MarketOrderSpot is a market order for a spot coin. +// It is used to buy/sell a spot coin. +// Limit order with TIF=IOC and px=market price * (1 +- slippage). +// Size determines the amount of the coin to buy/sell. +// +// MarketOrderSpot("HYPE", 0.1, nil) // Buy 0.1 HYPE +// MarketOrderSpot("HYPE", -0.1, nil) // Sell 0.1 HYPE +// MarketOrderSpot("HYPE", 0.1, &slippage) // Buy 0.1 HYPE with slippage +func (api *ExchangeAPI) MarketOrderSpot(coin string, size float64, slippage *float64) (*PlaceOrderResponse, error) { + slpg := GetSlippage(slippage) + isBuy := IsBuy(size) + finalPx := api.SlippagePriceSpot(coin, isBuy, slpg) + orderType := OrderType{ + Limit: &LimitOrderType{ + Tif: TifIoc, + }, + } + orderRequest := OrderRequest{ + Coin: coin, + IsBuy: isBuy, + Sz: math.Abs(size), + LimitPx: finalPx, + OrderType: orderType, + ReduceOnly: false, + } + return api.OrderSpot(orderRequest, GroupingNa) +} + // Open a limit order. // Order type can be Gtc, Ioc, Alo. // Size determines the amount of the coin to buy/sell. @@ -165,15 +213,26 @@ func (api *ExchangeAPI) ClosePosition(coin string) (*PlaceOrderResponse, error) // Place single order func (api *ExchangeAPI) Order(request OrderRequest, grouping Grouping) (*PlaceOrderResponse, error) { - return api.BulkOrders([]OrderRequest{request}, grouping) + return api.BulkOrders([]OrderRequest{request}, grouping, false) +} + +// OrderSpot places a spot order +func (api *ExchangeAPI) OrderSpot(request OrderRequest, grouping Grouping) (*PlaceOrderResponse, error) { + return api.BulkOrders([]OrderRequest{request}, grouping, true) } // Place orders in bulk // https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#place-an-order -func (api *ExchangeAPI) BulkOrders(requests []OrderRequest, grouping Grouping) (*PlaceOrderResponse, error) { +func (api *ExchangeAPI) BulkOrders(requests []OrderRequest, grouping Grouping, isSpot bool) (*PlaceOrderResponse, error) { var wires []OrderWire + var meta map[string]AssetInfo + if isSpot { + meta = api.spotMeta + } else { + meta = api.meta + } for _, req := range requests { - wires = append(wires, OrderRequestToWire(req, api.meta)) + wires = append(wires, OrderRequestToWire(req, meta, isSpot)) } timestamp := GetNonce() action := OrderWiresToOrderAction(wires, grouping) @@ -283,7 +342,7 @@ func (api *ExchangeAPI) Withdraw(destination string, amount float64) (*WithdrawR action := WithdrawAction{ Type: "withdraw3", Destination: destination, - Amount: FloatToWire(amount, &SZ_DECIMALS), + Amount: FloatToWire(amount, PERP_MAX_DECIMALS, SZ_DECIMALS), Time: nonce, } signatureChainID, chainType := api.getChainParams() @@ -315,7 +374,7 @@ func (api *ExchangeAPI) getChainParams() (string, string) { func (api *ExchangeAPI) BuildBulkOrdersEIP712(requests []OrderRequest, grouping Grouping) (apitypes.TypedData, error) { var wires []OrderWire for _, req := range requests { - wires = append(wires, OrderRequestToWire(req, api.meta)) + wires = append(wires, OrderRequestToWire(req, api.meta, false)) } timestamp := GetNonce() action := OrderWiresToOrderAction(wires, grouping) diff --git a/hyperliquid/exchange_test.go b/hyperliquid/exchange_test.go index 4cdb47b..13ec140 100644 --- a/hyperliquid/exchange_test.go +++ b/hyperliquid/exchange_test.go @@ -80,6 +80,7 @@ func TestExchangeAPI_MarketOpen(testing *testing.T) { if totalSize != math.Abs(size) { testing.Errorf("res.Response.Data.Statuses[0].Filled.TotalSz = %v", totalSize) } + time.Sleep(2 * time.Second) // wait to execute order accountState, err := exchangeAPI.infoAPI.GetUserState(exchangeAPI.AccountAddress()) if err != nil { testing.Errorf("GetAccountState() error = %v", err) @@ -157,7 +158,7 @@ func TestExchangeAPI_MarketClose(testing *testing.T) { func TestExchangeAPI_TestWithdraw(testing *testing.T) { exchangeAPI := GetExchangeAPI() - withdrawAmount := 2.0 + withdrawAmount := 10.0 stateBefore, err := exchangeAPI.infoAPI.GetUserState(exchangeAPI.AccountAddress()) if err != nil { testing.Errorf("GetAccountState() error = %v", err) @@ -184,3 +185,18 @@ func TestExchangeAPI_TestWithdraw(testing *testing.T) { testing.Errorf("Balance not updated: %v", stateAfter) } } + +func TestExchageAPI_TestMarketOrderSpot(testing *testing.T) { + exchangeAPI := GetExchangeAPI() + size := 1600.0 + coin := "YEETI" + res, err := exchangeAPI.MarketOrderSpot(coin, size, nil) + if err != nil { + testing.Errorf("MakeOpen() error = %v", err) + } + testing.Logf("MakeOpen() = %v", res) + avgPrice := res.Response.Data.Statuses[0].Filled.AvgPx + if avgPrice == 0 { + testing.Errorf("res.Response.Data.Statuses[0].Filled.AvgPx = %v", avgPrice) + } +} diff --git a/hyperliquid/exchange_types.go b/hyperliquid/exchange_types.go index 69af1c6..e8f4920 100644 --- a/hyperliquid/exchange_types.go +++ b/hyperliquid/exchange_types.go @@ -15,8 +15,10 @@ type ExchangeRequest struct { } type AssetInfo struct { - SzDecimals int - AssetId int + SzDecimals int + WeiDecimals int + AssetId int + SpotName string // for spot asset (e.g. "@107") } type OrderRequest struct { diff --git a/hyperliquid/go.mod b/hyperliquid/go.mod index e9f7d36..7645a30 100644 --- a/hyperliquid/go.mod +++ b/hyperliquid/go.mod @@ -1,27 +1,28 @@ module github.com/Logarithm-Labs/go-hyperliquid/hyperliquid -go 1.21.10 +go 1.23.4 require ( - github.com/ethereum/go-ethereum v1.14.3 + github.com/ethereum/go-ethereum v1.14.12 github.com/sirupsen/logrus v1.9.3 github.com/vmihailenco/msgpack/v5 v5.4.1 ) require ( - github.com/bits-and-blooms/bitset v1.10.0 // indirect - github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect + github.com/bits-and-blooms/bitset v1.13.0 // indirect github.com/consensys/bavard v0.1.13 // indirect github.com/consensys/gnark-crypto v0.12.1 // indirect + github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c // indirect github.com/crate-crypto/go-kzg-4844 v1.0.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect github.com/ethereum/c-kzg-4844 v1.0.0 // indirect - github.com/holiman/uint256 v1.2.4 // indirect + github.com/ethereum/go-verkle v0.1.1-0.20240829091221-dffa7562dbe9 // indirect + github.com/holiman/uint256 v1.3.1 // indirect github.com/mmcloughlin/addchain v0.4.0 // indirect - github.com/supranational/blst v0.3.11 // indirect + github.com/supranational/blst v0.3.13 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - golang.org/x/crypto v0.23.0 // indirect + golang.org/x/crypto v0.22.0 // indirect golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.20.0 // indirect + golang.org/x/sys v0.22.0 // indirect rsc.io/tmplfunc v0.0.3 // indirect ) diff --git a/hyperliquid/go.sum b/hyperliquid/go.sum index 01af282..6d2aabe 100644 --- a/hyperliquid/go.sum +++ b/hyperliquid/go.sum @@ -1,35 +1,17 @@ -github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= -github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= -github.com/VictoriaMetrics/fastcache v1.12.1 h1:i0mICQuojGDL3KblA7wUNlY5lOK6a4bwt3uRKnkZU40= -github.com/VictoriaMetrics/fastcache v1.12.1/go.mod h1:tX04vaqcNoQeGLD+ra5pU5sWkuxnzWhEzLwhP9w653o= -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bits-and-blooms/bitset v1.10.0 h1:ePXTeiPEazB5+opbv5fr8umg2R/1NlzgDsyepwsSr88= -github.com/bits-and-blooms/bitset v1.10.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= -github.com/btcsuite/btcd/btcec/v2 v2.2.0 h1:fzn1qaOt32TuLjFlkzYSsBC35Q3KUjT1SwPxiMSCF5k= -github.com/btcsuite/btcd/btcec/v2 v2.2.0/go.mod h1:U7MHm051Al6XmscBQ0BoNydpOTsFAn707034b5nY8zU= -github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U= -github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cockroachdb/errors v1.11.1 h1:xSEW75zKaKCWzR3OfxXUxgrk/NtT4G1MiOv5lWZazG8= -github.com/cockroachdb/errors v1.11.1/go.mod h1:8MUxA3Gi6b25tYlFEBGLf+D8aISL+M4MIpiWMSNRfxw= -github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= -github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= -github.com/cockroachdb/pebble v1.1.0 h1:pcFh8CdCIt2kmEpK0OIatq67Ln9uGDYY3d5XnE0LJG4= -github.com/cockroachdb/pebble v1.1.0/go.mod h1:sEHm5NOXxyiAoKWhoFxT8xMgd/f3RA6qUqQ1BXKrh2E= -github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30= -github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= -github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= -github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= +github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI= +github.com/VictoriaMetrics/fastcache v1.12.2/go.mod h1:AmC+Nzz1+3G2eCPapF6UcsnkThDcMsQicp4xDukwJYI= +github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE= +github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/consensys/bavard v0.1.13 h1:oLhMLOFGTLdlda/kma4VOJazblc7IM5y5QPd2A/YjhQ= github.com/consensys/bavard v0.1.13/go.mod h1:9ItSMtA/dXMAiL7BG6bqW2m3NdSEObYWoH223nGHukI= github.com/consensys/gnark-crypto v0.12.1 h1:lHH39WuuFgVHONRl3J0LRBtuYdQTumFSDtJF7HpyG8M= github.com/consensys/gnark-crypto v0.12.1/go.mod h1:v2Gy7L/4ZRosZ7Ivs+9SfUDr0f5UlG+EM5t7MPHiLuY= -github.com/crate-crypto/go-ipa v0.0.0-20231025140028-3c0104f4b233 h1:d28BXYi+wUpz1KBmiF9bWrjEMacUEREV6MBi2ODnrfQ= -github.com/crate-crypto/go-ipa v0.0.0-20231025140028-3c0104f4b233/go.mod h1:geZJZH3SzKCqnz5VT0q/DyIG/tvu/dZk+VIfXicupJs= +github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c h1:uQYC5Z1mdLRPrZhHjHxufI8+2UG/i25QG92j0Er9p6I= +github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c/go.mod h1:geZJZH3SzKCqnz5VT0q/DyIG/tvu/dZk+VIfXicupJs= github.com/crate-crypto/go-kzg-4844 v1.0.0 h1:TsSgHwrkTKecKJ4kadtHi4b3xHW5dCFUDFnUp1TsawI= github.com/crate-crypto/go-kzg-4844 v1.0.0/go.mod h1:1kMhvPgI0Ky3yIa+9lFySEBUBXkYxeOi8ZF1sYioxhc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -41,72 +23,44 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= github.com/ethereum/c-kzg-4844 v1.0.0 h1:0X1LBXxaEtYD9xsyj9B9ctQEZIpnvVDeoBx8aHEwTNA= github.com/ethereum/c-kzg-4844 v1.0.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0= -github.com/ethereum/go-ethereum v1.14.3 h1:5zvnAqLtnCZrU9uod1JCvHWJbPMURzYFHfc2eHz4PHA= -github.com/ethereum/go-ethereum v1.14.3/go.mod h1:1STrq471D0BQbCX9He0hUj4bHxX2k6mt5nOQJhDNOJ8= -github.com/gballet/go-verkle v0.1.1-0.20231031103413-a67434b50f46 h1:BAIP2GihuqhwdILrV+7GJel5lyPV3u1+PgzrWLc0TkE= -github.com/gballet/go-verkle v0.1.1-0.20231031103413-a67434b50f46/go.mod h1:QNpY22eby74jVhqH4WhDLDwxc/vqsern6pW+u2kbkpc= -github.com/getsentry/sentry-go v0.18.0 h1:MtBW5H9QgdcJabtZcuJG80BMOwaBpkRDZkxRkNC1sN0= -github.com/getsentry/sentry-go v0.18.0/go.mod h1:Kgon4Mby+FJ7ZWHFUAZgVaIa8sxHtnRJRLTXZr51aKQ= +github.com/ethereum/go-ethereum v1.14.12 h1:8hl57x77HSUo+cXExrURjU/w1VhL+ShCTJrTwcCQSe4= +github.com/ethereum/go-ethereum v1.14.12/go.mod h1:RAC2gVMWJ6FkxSPESfbshrcKpIokgQKsVKmAuqdekDY= +github.com/ethereum/go-verkle v0.1.1-0.20240829091221-dffa7562dbe9 h1:8NfxH2iXvJ60YRB8ChToFTUzl8awsc3cJ8CbLjGIl/A= +github.com/ethereum/go-verkle v0.1.1-0.20240829091221-dffa7562dbe9/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= -github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk= github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk= -github.com/holiman/uint256 v1.2.4 h1:jUc4Nk8fm9jZabQuqr2JzednajVmBpC+oiTiXZJEApU= -github.com/holiman/uint256 v1.2.4/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= -github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw= -github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/holiman/uint256 v1.3.1 h1:JfTzmih28bittyHM8z360dCjIA9dbPIBlcTI6lmctQs= +github.com/holiman/uint256 v1.3.1/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/leanovate/gopter v0.2.9 h1:fQjYxZaynp97ozCzfOyOuAGOU4aU/z37zf/tOujFk7c= github.com/leanovate/gopter v0.2.9/go.mod h1:U2L/78B+KVFIx2VmW6onHJQzXtFb+p5y3y2Sh+Jxxv8= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= -github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY= github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU= github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.12.0 h1:C+UIj/QWtmqY13Arb8kwMt5j34/0Z2iKamrJ+ryC0Gg= -github.com/prometheus/client_golang v1.12.0/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= -github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a h1:CmF68hwI0XsOQ5UwlBopMi2Ow4Pbg32akc4KIVCOm+Y= -github.com/prometheus/client_model v0.2.1-0.20210607210712-147c58e9608a/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= -github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= -github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= -github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU= github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/supranational/blst v0.3.11 h1:LyU6FolezeWAhvQk0k6O/d49jqgO52MSDDfYgbeoEm4= -github.com/supranational/blst v0.3.11/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= -github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= -github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/supranational/blst v0.3.13 h1:AYeSxdOMacwu7FBmpfloBz5pbFXDmJL33RuwnKtmTjk= +github.com/supranational/blst v0.3.13/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= @@ -115,19 +69,15 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ= golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/hyperliquid/info_service.go b/hyperliquid/info_service.go index 144ff22..953e03f 100644 --- a/hyperliquid/info_service.go +++ b/hyperliquid/info_service.go @@ -1,6 +1,8 @@ package hyperliquid import ( + "encoding/json" + "fmt" "strconv" ) @@ -38,16 +40,24 @@ type IInfoAPI interface { type InfoAPI struct { Client baseEndpoint string + spotMeta map[string]AssetInfo } // NewInfoAPI returns a new instance of the InfoAPI struct. // It sets the base endpoint to "/info" and the client to the NewClient function. // The isMainnet parameter is used to set the network type. func NewInfoAPI(isMainnet bool) *InfoAPI { - return &InfoAPI{ + api := InfoAPI{ baseEndpoint: "/info", Client: *NewClient(isMainnet), } + spotMeta, err := api.BuildSpotMetaMap() + if err != nil { + api.SetDebugActive() + api.debug("Error building meta map: %s", err) + } + api.spotMeta = spotMeta + return &api } func (api *InfoAPI) Endpoint() string { @@ -63,6 +73,41 @@ func (api *InfoAPI) GetAllMids() (*map[string]string, error) { return MakeUniversalRequest[map[string]string](api, request) } +// Retrieve spot meta and asset contexts +// https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/spot#retrieve-spot-asset-contexts +func (api *InfoAPI) GetAllSpotPrices() (*map[string]string, error) { + request := InfoRequest{ + Typez: "spotMetaAndAssetCtxs", + } + response, err := MakeUniversalRequest[SpotMetaAndAssetCtxsResponse](api, request) + if err != nil { + return nil, err + } + + marketsData, ok := response[1].([]interface{}) + if !ok { + return nil, fmt.Errorf("invalid markets data format") + } + + result := make(map[string]string) + + marketBytes, err := json.Marshal(marketsData) + if err != nil { + return nil, err + } + + var markets []Market + if err := json.Unmarshal(marketBytes, &markets); err != nil { + return nil, err + } + + for _, market := range markets { + result[market.Coin] = market.MidPx + } + + return &result, nil +} + // Retrieve a user's open orders // https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint#retrieve-a-users-open-orders func (api *InfoAPI) GetOpenOrders(address string) (*[]Order, error) { @@ -148,6 +193,14 @@ func (api *InfoAPI) GetMeta() (*Meta, error) { return MakeUniversalRequest[Meta](api, request) } +// Retrieve spot metadata +func (api *InfoAPI) GetSpotMeta() (*SpotMeta, error) { + request := InfoRequest{ + Typez: "spotMeta", + } + return MakeUniversalRequest[SpotMeta](api, request) +} + // Retrieve user's perpetuals account summary // https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/perpetuals#retrieve-users-perpetuals-account-summary func (api *InfoAPI) GetUserState(address string) (*UserState, error) { @@ -165,6 +218,23 @@ func (api *InfoAPI) GetAccountState() (*UserState, error) { return api.GetUserState(api.AccountAddress()) } +// Retrieve user's spot account summary +// https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/spot#retrieve-a-users-token-balances +func (api *InfoAPI) GetUserStateSpot(address string) (*UserStateSpot, error) { + request := UserStateRequest{ + User: address, + Typez: "spotClearinghouseState", + } + return MakeUniversalRequest[UserStateSpot](api, request) +} + +// Retrieve account's spot account summary +// The same as GetUserStateSpot but user is set to the account address +// Check AccountAddress() or SetAccountAddress() if there is a need to set the account address +func (api *InfoAPI) GetAccountStateSpot() (*UserStateSpot, error) { + return api.GetUserStateSpot(api.AccountAddress()) +} + // Retrieve a user's funding history // https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint/perpetuals#retrieve-a-users-funding-history-or-non-funding-ledger-updates func (api *InfoAPI) GetFundingUpdates(address string, startTime int64, endTime int64) (*[]FundingUpdate, error) { @@ -216,6 +286,11 @@ func (api *InfoAPI) GetHistoricalFundingRates(coin string, startTime int64, endT } // Helper function to get the market price of a given coin +// The coin parameter is the name of the coin +// +// Example: +// +// api.GetMartketPx("BTC") func (api *InfoAPI) GetMartketPx(coin string) (float64, error) { allMids, err := api.GetAllMids() if err != nil { @@ -228,6 +303,25 @@ func (api *InfoAPI) GetMartketPx(coin string) (float64, error) { return parsed, nil } +// GetSpotMarketPx returns the market price of a given spot coin +// The coin parameter is the name of the coin +// +// Example: +// +// api.GetSpotMarketPx("HYPE") +func (api *InfoAPI) GetSpotMarketPx(coin string) (float64, error) { + spotPrices, err := api.GetAllSpotPrices() + if err != nil { + return 0, err + } + spotName := api.spotMeta[coin].SpotName + parsed, err := strconv.ParseFloat((*spotPrices)[spotName], 32) + if err != nil { + return 0, err + } + return parsed, nil +} + // Helper function to get the withdrawals of a given address // By default returns last 90 days func (api *InfoAPI) GetWithdrawals(address string) (*[]Withdrawal, error) { @@ -304,3 +398,44 @@ func (api *InfoAPI) BuildMetaMap() (map[string]AssetInfo, error) { } return metaMap, nil } + +// Helper function to build a map of asset names to asset info +// It is used to get the assetId for a given asset name +func (api *InfoAPI) BuildSpotMetaMap() (map[string]AssetInfo, error) { + spotMeta, err := api.GetSpotMeta() + if err != nil { + return nil, err + } + + tokenMap := make(map[int]struct { + name string + szDecimals int + weiDecimals int + }, len(spotMeta.Tokens)) + + for _, token := range spotMeta.Tokens { + tokenMap[token.Index] = struct { + name string + szDecimals int + weiDecimals int + }{token.Name, token.SzDecimals, token.WeiDecimals} + } + + metaMap := make(map[string]AssetInfo) + for _, universe := range spotMeta.Universe { + for _, tokenId := range universe.Tokens { + if tokenId == 0 { + continue + } + if token, exists := tokenMap[tokenId]; exists { + metaMap[token.name] = AssetInfo{ + SzDecimals: token.szDecimals, + WeiDecimals: token.weiDecimals, + AssetId: universe.Index, + SpotName: universe.Name, + } + } + } + } + return metaMap, nil +} diff --git a/hyperliquid/info_test.go b/hyperliquid/info_test.go index f8bbed0..0df0f91 100644 --- a/hyperliquid/info_test.go +++ b/hyperliquid/info_test.go @@ -10,6 +10,8 @@ func GetInfoAPI() *InfoAPI { if GLOBAL_DEBUG { api.SetDebugActive() } + // It should be active account to pass all tests + // like GetAccountFills, GetAccountWithdrawals, etc. TEST_ADDRESS := os.Getenv("TEST_ADDRESS") if TEST_ADDRESS == "" { panic("Set TEST_ADDRESS in .env file") @@ -294,3 +296,71 @@ func TestInfoAPI_BuildMetaMap(t *testing.T) { } t.Logf("BuildMetaMap() = %v", res) } + +func TestInfoAPI_BuildSpotMetaMap(t *testing.T) { + api := GetInfoAPI() + res, err := api.BuildSpotMetaMap() + if err != nil { + t.Errorf("BuildSpotMetaMap() error = %v", err) + } + if len(res) == 0 { + t.Errorf("BuildSpotMetaMap() = %v, want > %v", res, 0) + } + // check PURR, HYPE in map + if _, ok := res["PURR"]; !ok { + t.Errorf("BuildSpotMetaMap() = %v, want %v", res, "PURR") + } + if _, ok := res["HYPE"]; !ok { + t.Errorf("BuildSpotMetaMap() = %v, want %v", res, "HYPE") + } + t.Logf("map(PURR) = %+v", res["PURR"]) + t.Logf("BuildSpotMetaMap() = %+v", res) +} + +func TestInfoAPI_GetSpotMeta(t *testing.T) { + api := GetInfoAPI() + res, err := api.GetSpotMeta() + if err != nil { + t.Errorf("GetSpotMeta() error = %v", err) + } + if len(res.Tokens) == 0 { + t.Errorf("GetSpotMeta() = %v, want > %v", res, 0) + } + t.Logf("GetSpotMeta() = %v", res) +} + +func TestInfoAPI_GetAllSpotPrices(t *testing.T) { + api := GetInfoAPI() + res, err := api.GetAllSpotPrices() + if err != nil { + t.Errorf("GetAllSpotPrices() error = %v", err) + } + if len(*res) == 0 { + t.Errorf("GetAllSpotPrices() = %v, want > %v", res, 0) + } + t.Logf("GetAllSpotPrices() = %+v", res) +} + +func TestInfoAPI_GetSpotMarketPx(t *testing.T) { + api := GetInfoAPI() + res, err := api.GetSpotMarketPx("HYPE") + if err != nil { + t.Errorf("GetSpotMarketPx() error = %v", err) + } + if res < 0 { + t.Errorf("GetSpotMarketPx() = %v, want > %v", res, 0) + } + t.Logf("GetSpotMarketPx(HYPE) = %v", res) +} + +func TestInfoAPI_GetUserStateSpot(t *testing.T) { + api := GetInfoAPI() + res, err := api.GetAccountStateSpot() + if err != nil { + t.Errorf("GetUserStateSpot() error = %v", err) + } + if len(res.Balances) == 0 { + t.Errorf("GetUserStateSpot() = %v, want > %v", res, 0) + } + t.Logf("GetUserStateSpot() = %+v", res) +} diff --git a/hyperliquid/info_types.go b/hyperliquid/info_types.go index 082d133..bce157b 100644 --- a/hyperliquid/info_types.go +++ b/hyperliquid/info_types.go @@ -54,6 +54,25 @@ type Position struct { } `json:"cumFunding"` } +type UserStateSpot struct { + Balances []SpotAssetPosition `json:"balances"` +} + +type SpotAssetPosition struct { + /* + "coin": "USDC", + "token": 0, + "hold": "0.0", + "total": "14.625485", + "entryNtl": "0.0" + */ + Coin string `json:"coin"` + Token int `json:"token"` + Hold float64 `json:"hold,string"` + Total float64 `json:"total,string"` + EntryNtl float64 `json:"entryNtl,string"` +} + type Order struct { Children []any `json:"children,omitempty"` Cloid string `json:"cloid,omitempty"` @@ -85,6 +104,25 @@ type MarginSummary struct { TotalRawUsd float64 `json:"totalRawUsd,string"` } +type SpotMeta struct { + Universe []struct { + Tokens []int `json:"tokens"` + Name string `json:"name"` + Index int `json:"index"` + IsCanonical bool `json:"isCanonical"` + } `json:"universe"` + Tokens []struct { + Name string `json:"name"` + SzDecimals int `json:"szDecimals"` + WeiDecimals int `json:"weiDecimals"` + Index int `json:"index"` + TokenID string `json:"tokenId"` + IsCanonical bool `json:"isCanonical"` + EvmContract any `json:"evmContract"` + FullName any `json:"fullName"` + } `json:"tokens"` +} + type Meta struct { Universe []Asset `json:"universe"` } @@ -179,3 +217,16 @@ type RatesLimits struct { NRequestsUsed int `json:"nRequestsUsed"` NRequestsCap int `json:"nRequestsCap"` } + +type SpotMetaAndAssetCtxsResponse [2]interface{} // Array of exactly 2 elements + +type Market struct { + PrevDayPx string `json:"prevDayPx,omitempty"` + DayNtlVlm string `json:"dayNtlVlm,omitempty"` + MarkPx string `json:"markPx,omitempty"` + MidPx string `json:"midPx,omitempty"` + CirculatingSupply string `json:"circulatingSupply,omitempty"` + Coin string `json:"coin,omitempty"` + TotalSupply string `json:"totalSupply,omitempty"` + DayBaseVlm string `json:"dayBaseVlm,omitempty"` +}