Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions examples/meta_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package examples

import (
"testing"

"github.com/joho/godotenv"
"github.com/sonirico/go-hyperliquid"
)

func TestMetaAndAssetCtxs(t *testing.T) {
godotenv.Overload()
info := hyperliquid.NewInfo(hyperliquid.MainnetAPIURL, true, nil, nil)
meta, err := info.MetaAndAssetCtxs()
if err != nil {
t.Fatalf("Failed to get meta: %v", err)
}

if meta.Meta.Universe == nil {
t.Error("Expected non-nil universe")
}

if meta.Meta.MarginTables == nil {
t.Error("Expected non-nil margin tables")
}

if meta.Ctxs == nil {
t.Error("Expected non-nil contexts")
}

if len(meta.Meta.Universe) == 0 {
t.Error("Expected at least one asset in universe")
}

if meta.Meta.Universe[0].Name == "" {
t.Error("Expected name to be non-empty")
}

if len(meta.Meta.MarginTables) == 0 {
t.Error("Expected at least one margin table")
}

if meta.Meta.MarginTables[0].ID < 0 {
t.Error("Expected ID to be non-negative")
}

if len(meta.Meta.MarginTables[0].MarginTiers) == 0 {
t.Error("Expected at least one margin tier")
}

if len(meta.Ctxs) == 0 {
t.Error("Expected at least one context")
}

if meta.Ctxs[0].MarkPx == "" {
t.Error("Expected mark price to be non-empty")
}
}

func TestSpotMetaAndAssetCtxs(t *testing.T) {
godotenv.Overload()

info := hyperliquid.NewInfo(hyperliquid.MainnetAPIURL, true, nil, nil)
spotMeta, err := info.SpotMetaAndAssetCtxs()
if err != nil {
t.Fatalf("Failed to get spot meta: %v", err)
}

if spotMeta.Meta.Universe == nil {
t.Error("Expected non-nil universe")
}

if spotMeta.Meta.Tokens == nil {
t.Error("Expected non-nil tokens")
}

if spotMeta.Ctxs == nil {
t.Error("Expected non-nil contexts")
}

if len(spotMeta.Meta.Universe) == 0 {
t.Error("Expected at least one asset in universe")
}

if spotMeta.Meta.Universe[0].Name == "" {
t.Error("Expected name to be non-empty")
}

if len(spotMeta.Meta.Tokens) == 0 {
t.Error("Expected at least one token")
}

if spotMeta.Meta.Tokens[0].Name == "" {
t.Error("Expected name to be non-empty")
}

if len(spotMeta.Ctxs) == 0 {
t.Error("Expected at least one context")
}

if spotMeta.Ctxs[0].Coin == "" {
t.Error("Expected coin to be non-empty")
}
}
131 changes: 119 additions & 12 deletions info.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,58 @@ func NewInfo(baseURL string, skipWS bool, meta *Meta, spotMeta *SpotMeta) *Info
return info
}

func parseMetaResponse(resp []byte) (*Meta, error) {
var meta map[string]json.RawMessage
if err := json.Unmarshal(resp, &meta); err != nil {
return nil, fmt.Errorf("failed to unmarshal meta response: %w", err)
}

var universe []AssetInfo
if err := json.Unmarshal(meta["universe"], &universe); err != nil {
return nil, fmt.Errorf("failed to unmarshal universe: %w", err)
}

var marginTables [][]any
if err := json.Unmarshal(meta["marginTables"], &marginTables); err != nil {
return nil, fmt.Errorf("failed to unmarshal margin tables: %w", err)
}

marginTablesResult := make([]MarginTable, len(marginTables))
for i, marginTable := range marginTables {
id := marginTable[0].(float64)
tableBytes, err := json.Marshal(marginTable[1])
if err != nil {
return nil, fmt.Errorf("failed to marshal margin table data: %w", err)
}

var marginTableData map[string]any
if err := json.Unmarshal(tableBytes, &marginTableData); err != nil {
return nil, fmt.Errorf("failed to unmarshal margin table data: %w", err)
}

marginTiersBytes, err := json.Marshal(marginTableData["marginTiers"])
if err != nil {
return nil, fmt.Errorf("failed to marshal margin tiers: %w", err)
}

var marginTiers []MarginTier
if err := json.Unmarshal(marginTiersBytes, &marginTiers); err != nil {
return nil, fmt.Errorf("failed to unmarshal margin tiers: %w", err)
}

marginTablesResult[i] = MarginTable{
ID: int(id),
Description: marginTableData["description"].(string),
MarginTiers: marginTiers,
}
}

return &Meta{
Universe: universe,
MarginTables: marginTablesResult,
}, nil
}

func (i *Info) Meta() (*Meta, error) {
resp, err := i.client.post("/info", map[string]any{
"type": "meta",
Expand All @@ -95,12 +147,7 @@ func (i *Info) Meta() (*Meta, error) {
return nil, fmt.Errorf("failed to fetch meta: %w", err)
}

var meta Meta
if err := json.Unmarshal(resp, &meta); err != nil {
return nil, fmt.Errorf("failed to unmarshal meta response: %w", err)
}

return &meta, nil
return parseMetaResponse(resp)
}

func (i *Info) SpotMeta() (*SpotMeta, error) {
Expand Down Expand Up @@ -232,34 +279,94 @@ func (i *Info) UserFillsByTime(address string, startTime int64, endTime *int64)
return result, nil
}

func (i *Info) MetaAndAssetCtxs() (map[string]any, error) {
func (i *Info) MetaAndAssetCtxs() (*MetaAndAssetCtxs, error) {
resp, err := i.client.post("/info", map[string]any{
"type": "metaAndAssetCtxs",
})
if err != nil {
return nil, fmt.Errorf("failed to fetch meta and asset contexts: %w", err)
}

var result map[string]any
var result []any
if err := json.Unmarshal(resp, &result); err != nil {
return nil, fmt.Errorf("failed to unmarshal meta and asset contexts: %w", err)
}
return result, nil

if len(result) < 2 {
return nil, fmt.Errorf("expected at least 2 elements in response, got %d", len(result))
}

metaBytes, err := json.Marshal(result[0])
if err != nil {
return nil, fmt.Errorf("failed to marshal meta data: %w", err)
}

meta, err := parseMetaResponse(metaBytes)
if err != nil {
return nil, fmt.Errorf("failed to parse meta: %w", err)
}

ctxsBytes, err := json.Marshal(result[1])
if err != nil {
return nil, fmt.Errorf("failed to marshal ctxs data: %w", err)
}

var ctxs []AssetCtx
if err := json.Unmarshal(ctxsBytes, &ctxs); err != nil {
return nil, fmt.Errorf("failed to unmarshal ctxs: %w", err)
}

metaAndAssetCtxs := &MetaAndAssetCtxs{
Meta: *meta,
Ctxs: ctxs,
}

return metaAndAssetCtxs, nil
}

func (i *Info) SpotMetaAndAssetCtxs() (map[string]any, error) {
func (i *Info) SpotMetaAndAssetCtxs() (*SpotMetaAndAssetCtxs, error) {
resp, err := i.client.post("/info", map[string]any{
"type": "spotMetaAndAssetCtxs",
})
if err != nil {
return nil, fmt.Errorf("failed to fetch spot meta and asset contexts: %w", err)
}

var result map[string]any
var result []any
if err := json.Unmarshal(resp, &result); err != nil {
return nil, fmt.Errorf("failed to unmarshal spot meta and asset contexts: %w", err)
}
return result, nil

if len(result) < 2 {
return nil, fmt.Errorf("expected at least 2 elements in response, got %d", len(result))
}

// Unmarshal the first element (SpotMeta)
metaBytes, err := json.Marshal(result[0])
if err != nil {
return nil, fmt.Errorf("failed to marshal meta data: %w", err)
}

var meta SpotMeta
if err := json.Unmarshal(metaBytes, &meta); err != nil {
return nil, fmt.Errorf("failed to unmarshal meta: %w", err)
}

// Unmarshal the second element ([]SpotAssetCtx)
ctxsBytes, err := json.Marshal(result[1])
if err != nil {
return nil, fmt.Errorf("failed to marshal ctxs data: %w", err)
}

var ctxs []SpotAssetCtx
if err := json.Unmarshal(ctxsBytes, &ctxs); err != nil {
return nil, fmt.Errorf("failed to unmarshal ctxs: %w", err)
}

return &SpotMetaAndAssetCtxs{
Meta: meta,
Ctxs: ctxs,
}, nil
}

func (i *Info) FundingHistory(
Expand Down
39 changes: 38 additions & 1 deletion types.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,39 @@ type AssetInfo struct {
SzDecimals int `json:"szDecimals"`
}

type MarginTier struct {
LowerBound string `json:"lowerBound"`
MaxLeverage int `json:"maxLeverage"`
}

type MarginTable struct {
ID int
Description string `json:"description"`
MarginTiers []MarginTier `json:"marginTiers"`
}

type Meta struct {
Universe []AssetInfo `json:"universe"`
Universe []AssetInfo `json:"universe"`
MarginTables []MarginTable `json:"marginTables"`
}

type AssetCtx struct {
Funding string `json:"funding"`
OpenInterest string `json:"openInterest"`
PrevDayPx string `json:"prevDayPx"`
DayNtlVlm string `json:"dayNtlVlm"`
Premium string `json:"premium"`
OraclePx string `json:"oraclePx"`
MarkPx string `json:"markPx"`
MidPx string `json:"midPx,omitempty"`
ImpactPxs []string `json:"impactPxs"`
DayBaseVlm string `json:"dayBaseVlm,omitempty"`
}

// This type has no JSON annotation because it cannot be directly unmarshalled from the response
type MetaAndAssetCtxs struct {
Meta
Ctxs []AssetCtx
}

type SpotAssetInfo struct {
Expand Down Expand Up @@ -75,6 +106,12 @@ type SpotAssetCtx struct {
Coin string `json:"coin"`
}

// This type has no JSON annotation because it cannot be directly unmarshalled from the response
type SpotMetaAndAssetCtxs struct {
Meta SpotMeta
Ctxs []SpotAssetCtx
}

// WsMsg represents a WebSocket message with a channel and data payload.
type WsMsg struct {
Channel string `json:"channel"`
Expand Down
Loading
Loading