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
59 changes: 59 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: CI
on:
push:
branches:
- main
pull_request:

env:
GO_VERSION: 1.24.5

jobs:
ci:
name: ${{ matrix.job }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
job: [lint, test]
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}

- name: Lint
if: matrix.job == 'lint'
uses: golangci/golangci-lint-action@v8
with:
args: --timeout=5m

- name: Test
if: matrix.job == 'test'
env:
TESTCOVERAGE_THRESHOLD: 0
run: |
set -euo pipefail
echo "Running tests..."
go test -p 1 -v -coverpkg=./... -coverprofile=profile.cov ./... | tee test.log
go tool cover -func profile.cov

echo "------------------"
if grep -q "FAIL:" test.log; then
echo "--- SOME TESTCASES FAILED"
else
echo "--- ALL TESTCASES PASSED"
fi

echo "Threshold: ${TESTCOVERAGE_THRESHOLD}%"
totalCoverage=$(go tool cover -func=profile.cov | awk '/total:/ {print substr($3, 1, length($3)-1)}')
echo "Current test coverage: ${totalCoverage}%"
if awk -v total="$totalCoverage" -v threshold="$TESTCOVERAGE_THRESHOLD" 'BEGIN {exit !(total + 0 >= threshold + 0)}'; then
echo "OK"
else
echo "Current test coverage is below threshold. Please add more unit tests."
exit 1
fi
32 changes: 32 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
version: "2"

run:
timeout: 5m

linters:
default: none
enable:
- errcheck
- govet
- ineffassign
- revive
- staticcheck
exclusions:
paths:
- ".*_test\\.go"
settings:
revive:
confidence: 0.8

formatters:
enable:
- gofmt
- goimports
settings:
goimports:
local-prefixes:
- github.com/SwellNetwork/hyperliquid-go-sdk

issues:
max-issues-per-linter: 0
max-same-issues: 0
3 changes: 3 additions & 0 deletions http_client_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
//go:build integration
// +build integration

package hyperliquid

import (
Expand Down
22 changes: 22 additions & 0 deletions http_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package hyperliquid

import "time"

type HTTPClientConfig struct {
BaseURL string
Timeout time.Duration
}

func DefaultMainnetHTTPClientConfig() HTTPClientConfig {
return HTTPClientConfig{
BaseURL: "https://api.hyperliquid.xyz",
Timeout: 3 * time.Second,
}
}

func DefaultTestnetHTTPClientConfig() HTTPClientConfig {
return HTTPClientConfig{
BaseURL: "https://api.hyperliquid-testnet.xyz",
Timeout: 3 * time.Second,
}
}
File renamed without changes.
21 changes: 21 additions & 0 deletions funding.go → http_info_perpetuals.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,24 @@ func (c *HTTPClient) FundingHistory(ctx context.Context, params FundingHistoryPa

return result, nil
}

func (c *HTTPClient) MetaAndAssetCtxs(ctx context.Context) (*MetaAndAssetCtx, error) {
body := map[string]any{"type": InfoTypeMetaAndAssetCtxs}

var result metaAndAssetCtxsResult

resp, err := c.client.R().
SetContext(ctx).
SetBody(body).
SetResult(&result).
Post(PathInfo)
if err != nil {
return nil, fmt.Errorf("meta and asset ctxs request failed: %w", err)
}

if resp.IsError() {
return nil, fmt.Errorf("meta and asset ctxs request failed: status %d: %s", resp.StatusCode(), resp.String())
}

return transformMetaAndAssetCtxsResult(result)
}
26 changes: 0 additions & 26 deletions transform.go → http_info_perpetuals_transform.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,29 +88,3 @@ func transformMetaAndAssetCtxsResult(result metaAndAssetCtxsResult) (*MetaAndAss
Ctxs: ctxs,
}, nil
}

func transformSpotMetaAndAssetCtxsResult(result spotMetaAndAssetCtxsResult) (*SpotMetaAndAssetCtx, error) {
meta := SpotMeta{
Universe: make([]SpotAssetInfo, len(result.Meta.Universe)),
Tokens: result.Meta.Tokens,
}

for i, uni := range result.Meta.Universe {
meta.Universe[i] = SpotAssetInfo{
Tokens: append([]int(nil), uni.Tokens...),
Name: uni.Name,
Index: uni.Index,
IsCanonical: uni.IsCanonical,
}
}

ctxs := make([]SpotAssetCtx, len(result.Assets))
for i, asset := range result.Assets {
ctxs[i] = SpotAssetCtx(asset)
}

return &SpotMetaAndAssetCtx{
Meta: meta,
Ctxs: ctxs,
}, nil
}
156 changes: 156 additions & 0 deletions http_info_perpetuals_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package hyperliquid

import (
"fmt"

"github.com/goccy/go-json"
)

// Predicted fundings

type (
predictedFundingsResult []predictedFundingsCoin

predictedFundingsCoin struct {
Coin string
Exchanges []predictedFundingsExchange
}

predictedFundingsExchange struct {
Exchange string
Details predictedFundingDetails
}

predictedFundingDetails struct {
FundingRate string `json:"fundingRate"`
NextFundingTime int64 `json:"nextFundingTime"`
FundingIntervalHours int64 `json:"fundingIntervalHours"`
}
)

func (p *predictedFundingsCoin) UnmarshalJSON(data []byte) error {
var payload [2]json.RawMessage
if err := json.Unmarshal(data, &payload); err != nil {
return fmt.Errorf("decode predicted fundings coin: %w", err)
}

if err := json.Unmarshal(payload[0], &p.Coin); err != nil {
return err
}

return json.Unmarshal(payload[1], &p.Exchanges)
}

func (p *predictedFundingsExchange) UnmarshalJSON(data []byte) error {
var payload [2]json.RawMessage
if err := json.Unmarshal(data, &payload); err != nil {
return fmt.Errorf("decode predicted fundings exchange: %w", err)
}

if err := json.Unmarshal(payload[0], &p.Exchange); err != nil {
return err
}

return json.Unmarshal(payload[1], &p.Details)
}

// Perp meta & asset ctxs

type (
metaAndAssetCtxsResult struct {
Meta metaAndAssetCtxMeta
Assets []assetCtxPayload
}

metaAndAssetCtxMeta struct {
Universe []metaAndAssetCtxUniverse `json:"universe"`
MarginTables marginTableMap `json:"marginTables"`
CollateralToken int `json:"collateralToken"`
}

metaAndAssetCtxUniverse struct {
SzDecimals int `json:"szDecimals"`
Name string `json:"name"`
MaxLeverage int `json:"maxLeverage"`
MarginTableID int `json:"marginTableId"`
OnlyIsolated bool `json:"onlyIsolated,omitempty"`
IsDelisted bool `json:"isDelisted,omitempty"`
}

marginTableMap map[int]marginTableDetails

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

marginTableDetails struct {
Description string `json:"description"`
MarginTiers []marginTierPayload `json:"marginTiers"`
}

assetCtxPayload 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"`
ImpactPxs []string `json:"impactPxs"`
DayBaseVlm string `json:"dayBaseVlm"`
}
)

func (m *metaAndAssetCtxsResult) UnmarshalJSON(data []byte) error {
var payload [2]json.RawMessage
if err := json.Unmarshal(data, &payload); err != nil {
return err
}

if err := json.Unmarshal(payload[0], &m.Meta); err != nil {
return fmt.Errorf("decode meta section: %w", err)
}

if err := json.Unmarshal(payload[1], &m.Assets); err != nil {
return fmt.Errorf("decode asset ctx section: %w", err)
}

return nil
}

func (m *marginTableMap) UnmarshalJSON(data []byte) error {
var entries []json.RawMessage
if err := json.Unmarshal(data, &entries); err != nil {
return err
}

if len(entries) == 0 {
*m = nil
return nil
}

result := make(marginTableMap, len(entries))
for _, raw := range entries {
var payload [2]json.RawMessage
if err := json.Unmarshal(raw, &payload); err != nil {
return err
}

var id int
if err := json.Unmarshal(payload[0], &id); err != nil {
return err
}

var details marginTableDetails
if err := json.Unmarshal(payload[1], &details); err != nil {
return err
}

result[id] = details
}

*m = result
return nil
}
28 changes: 28 additions & 0 deletions http_info_spot.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package hyperliquid

import (
"context"
"fmt"
)

func (c *HTTPClient) SpotMetaAndAssetCtxs(ctx context.Context) (*SpotMetaAndAssetCtx, error) {
body := map[string]any{"type": InfoTypeSpotMetaAndAssetCtxs}

var result spotMetaAndAssetCtxsResult

resp, err := c.client.R().
SetContext(ctx).
SetBody(body).
SetResult(&result).
Post(PathInfo)
if err != nil {
return nil, fmt.Errorf("spot meta and asset ctxs request failed: %w", err)
}

if resp.IsError() {
return nil, fmt.Errorf("spot meta and asset ctxs request failed: status %d: %s", resp.StatusCode(), resp.String())
}

return transformSpotMetaAndAssetCtxsResult(result)

}
27 changes: 27 additions & 0 deletions http_info_spot_transform.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package hyperliquid

func transformSpotMetaAndAssetCtxsResult(result spotMetaAndAssetCtxsResult) (*SpotMetaAndAssetCtx, error) {
meta := SpotMeta{
Universe: make([]SpotAssetInfo, len(result.Meta.Universe)),
Tokens: result.Meta.Tokens,
}

for i, uni := range result.Meta.Universe {
meta.Universe[i] = SpotAssetInfo{
Tokens: append([]int(nil), uni.Tokens...),
Name: uni.Name,
Index: uni.Index,
IsCanonical: uni.IsCanonical,
}
}

ctxs := make([]SpotAssetCtx, len(result.Assets))
for i, asset := range result.Assets {
ctxs[i] = SpotAssetCtx(asset)
}

return &SpotMetaAndAssetCtx{
Meta: meta,
Ctxs: ctxs,
}, nil
}
Loading