diff --git a/go.mod b/go.mod index e4db1955b5..1fee261cfe 100644 --- a/go.mod +++ b/go.mod @@ -14,15 +14,16 @@ require ( github.com/cenkalti/backoff/v4 v4.2.0 github.com/cheggaaa/pb/v3 v3.0.8 github.com/codingconcepts/env v0.0.0-20200821220118-a8fbf8d84482 + github.com/ethereum/go-ethereum v1.16.3 github.com/evanphx/json-patch/v5 v5.6.0 github.com/fatih/camelcase v1.0.0 - github.com/fatih/color v1.14.1 + github.com/fatih/color v1.16.0 github.com/gertd/go-pluralize v0.2.1 github.com/gin-contrib/cors v1.7.2 github.com/gin-gonic/gin v1.10.0 github.com/go-redis/redis/v8 v8.11.5 github.com/go-sql-driver/mysql v1.8.1 - github.com/gofrs/flock v0.8.1 + github.com/gofrs/flock v0.12.1 github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 github.com/grafana/pyroscope-go v1.2.0 @@ -32,6 +33,7 @@ require ( github.com/joho/godotenv v1.3.0 github.com/leekchan/accounting v0.0.0-20191218023648-17a4ce5f94d4 github.com/mattn/go-shellwords v1.0.12 + github.com/mitchellh/mapstructure v1.5.0 github.com/muesli/clusters v0.0.0-20180605185049-a07a36e67d36 github.com/muesli/kmeans v0.3.0 github.com/pkg/errors v0.9.1 @@ -43,11 +45,12 @@ require ( github.com/sajari/regression v1.0.1 github.com/sirupsen/logrus v1.9.3 github.com/slack-go/slack v0.17.2 - github.com/spf13/cobra v1.8.0 - github.com/spf13/pflag v1.0.5 + github.com/spf13/cobra v1.8.1 + github.com/spf13/pflag v1.0.6 github.com/spf13/viper v1.18.2 github.com/stretchr/testify v1.10.0 github.com/valyala/fastjson v1.5.1 + github.com/vmihailenco/msgpack/v5 v5.4.1 github.com/wcharczuk/go-chart/v2 v2.1.2 github.com/x-cray/logrus-prefixed-formatter v0.5.2 github.com/zserge/lorca v0.1.9 @@ -55,7 +58,7 @@ require ( go.uber.org/multierr v1.11.0 golang.org/x/oauth2 v0.22.0 golang.org/x/sync v0.18.0 - golang.org/x/time v0.6.0 + golang.org/x/time v0.9.0 gonum.org/v1/gonum v0.8.2 google.golang.org/api v0.194.0 google.golang.org/grpc v1.65.0 @@ -73,6 +76,7 @@ require ( github.com/VividCortex/ewma v1.1.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bitly/go-simplejson v0.5.1 // indirect + github.com/bits-and-blooms/bitset v1.20.0 // indirect github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect github.com/bytedance/sonic v1.12.1 // indirect github.com/bytedance/sonic/loader v0.2.0 // indirect @@ -80,10 +84,16 @@ require ( github.com/cloudwego/base64x v0.1.4 // indirect github.com/cloudwego/iasm v0.2.0 // indirect github.com/cockroachdb/apd v1.1.0 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect + github.com/consensys/gnark-crypto v0.18.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect + github.com/crate-crypto/go-eth-kzg v1.3.0 // indirect + github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect github.com/denisenkom/go-mssqldb v0.12.3 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/ethereum/c-kzg-4844/v2 v2.1.0 // indirect + github.com/ethereum/go-verkle v0.2.2 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gabriel-vasile/mimetype v1.4.5 // indirect @@ -93,7 +103,7 @@ require ( github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.22.0 // indirect - github.com/goccy/go-json v0.10.3 // indirect + github.com/goccy/go-json v0.10.4 // indirect github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect github.com/golang-sql/sqlexp v0.1.0 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect @@ -103,6 +113,7 @@ require ( github.com/googleapis/gax-go/v2 v2.13.0 // indirect github.com/grafana/pyroscope-go/godeltaprof v0.1.8 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/holiman/uint256 v1.3.2 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -118,7 +129,6 @@ require ( github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mattn/go-sqlite3 v1.14.23 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect @@ -136,8 +146,10 @@ require ( github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect + github.com/supranational/blst v0.3.14 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/ziutek/mymysql v1.5.4 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect diff --git a/go.sum b/go.sum index 3361760c24..a8b1297ceb 100644 --- a/go.sum +++ b/go.sum @@ -31,6 +31,10 @@ github.com/Masterminds/squirrel v1.5.3 h1:YPpoceAcxuzIljlr5iWpNKaql7hLeG1KLSrhvd github.com/Masterminds/squirrel v1.5.3/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= +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.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI= +github.com/VictoriaMetrics/fastcache v1.12.2/go.mod h1:AmC+Nzz1+3G2eCPapF6UcsnkThDcMsQicp4xDukwJYI= github.com/VividCortex/ewma v1.1.1 h1:MnEK4VOv6n0RSY4vtRe3h11qjxL3+t0B8yOL8iMXdcM= github.com/VividCortex/ewma v1.1.1/go.mod h1:2Tkkvm3sRDVXaiyucHiACn4cqf7DpdyLvmxzcbUokwA= github.com/adshao/go-binance/v2 v2.8.9 h1:NX+4u/LgEmrjTS7OMWU+9ZgfHKFM61RPhnr9/SqWPhc= @@ -52,6 +56,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bitly/go-simplejson v0.5.1 h1:xgwPbetQScXt1gh9BmoJ6j9JMr3TElvuIyjR8pgdoow= github.com/bitly/go-simplejson v0.5.1/go.mod h1:YOPVLzCfwK14b4Sff3oP1AmGhI9T9Vsg84etUnlyp+Q= +github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU= +github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI= github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= @@ -88,6 +94,8 @@ github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/codingconcepts/env v0.0.0-20200821220118-a8fbf8d84482 h1:5/aEFreBh9hH/0G+33xtczJCvMaulqsm9nDuu2BZUEo= github.com/codingconcepts/env v0.0.0-20200821220118-a8fbf8d84482/go.mod h1:TM9ug+H/2cI3EjyIDr5xKCkFGyNE59URgH1wu5NyU8E= +github.com/consensys/gnark-crypto v0.18.0 h1:vIye/FqI50VeAr0B3dx+YjeIvmc3LWz4yEfbWBpTUf0= +github.com/consensys/gnark-crypto v0.18.0/go.mod h1:L3mXGFTe1ZN+RSJ+CLjUt9x7PNdx8ubaYfDROyp2Z8c= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -95,8 +103,13 @@ github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7 github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= -github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/crate-crypto/go-eth-kzg v1.3.0 h1:05GrhASN9kDAidaFJOda6A4BEvgvuXbazXg/0E3OOdI= +github.com/crate-crypto/go-eth-kzg v1.3.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= +github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg= +github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/cznic/cc v0.0.0-20181122101902-d673e9b70d4d/go.mod h1:m3fD/V+XTB35Kh9zw6dzjMY+We0Q7PMf6LLIC4vuG9k= github.com/cznic/golex v0.0.0-20181122101858-9c343928389c/go.mod h1:+bmmJDNmKlhWNG+gwWCkaBoTy39Fs+bzRxVBzoTQbIc= @@ -107,6 +120,10 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/decred/dcrd/crypto/blake256 v1.0.0 h1:/8DMNYp9SGi5f0w7uCm6d6M4OU2rGFK09Y2A4Xv7EE0= +github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 h1:YLtO71vCjJRCBcrPMtQ9nqBsqpA1m5sE92cU+pd5Mcc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/denisenkom/go-mssqldb v0.12.3 h1:pBSGx9Tq67pBOTLmxNuirNTeB8Vjmf886Kx+8Y+8shw= github.com/denisenkom/go-mssqldb v0.12.3/go.mod h1:k0mtMFOnU+AihqFxPMiF05rtiDrorD1Vrm1KEz5hxDo= @@ -115,21 +132,31 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= +github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= +github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= +github.com/ethereum/c-kzg-4844/v2 v2.1.0 h1:gQropX9YFBhl3g4HYhwE70zq3IHFRgbbNPw0Shwzf5w= +github.com/ethereum/c-kzg-4844/v2 v2.1.0/go.mod h1:TC48kOKjJKPbN7C++qIgt0TJzZ70QznYR7Ob+WXl57E= +github.com/ethereum/go-ethereum v1.16.3 h1:nDoBSrmsrPbrDIVLTkDQCy1U9KdHN+F2PzvMbDoS42Q= +github.com/ethereum/go-ethereum v1.16.3/go.mod h1:Lrsc6bt9Gm9RyvhfFK53vboCia8kpF9nv+2Ukntnl+8= +github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8= +github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk= github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= -github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w= -github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY= +github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= @@ -157,6 +184,8 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +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/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -173,10 +202,10 @@ github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqw github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= -github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= -github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= -github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= +github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= @@ -211,6 +240,8 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +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/gonum/blas v0.0.0-20181208220705-f22b278b28ac/go.mod h1:P32wAyui1PQ58Oce/KYkOqQv8cVw1zAapXOl+dRFGbc= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -278,6 +309,8 @@ github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2p github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/heroku/rollrus v0.2.0 h1:b3AgcXJKFJNUwbQOC2S69/+mxuTpe4laznem9VJdPEo= github.com/heroku/rollrus v0.2.0/go.mod h1:B3MwEcr9nmf4xj0Sr5l9eSht7wLKMa1C+9ajgAU79ek= +github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= +github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= @@ -370,6 +403,8 @@ github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= +github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= +github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= github.com/leekchan/accounting v0.0.0-20191218023648-17a4ce5f94d4 h1:KZzDAtJ7ZLm0zSWVhN/zgyB8Ksx5H+P9irwbTcJ9FwI= github.com/leekchan/accounting v0.0.0-20191218023648-17a4ce5f94d4/go.mod h1:3timm6YPhY3YDaGxl0q3eaflX0eoSx3FXn7ckHe4tO0= github.com/leesper/go_rng v0.0.0-20171009123644-5344a9259b21/go.mod h1:N0SVk0uhy+E1PZ3C9ctsPRlvOPAFPkCNlcPBDkt0N3U= @@ -418,6 +453,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5 github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= +github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -445,6 +482,8 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +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/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= @@ -495,8 +534,8 @@ github.com/robfig/cron/v3 v3.0.0 h1:kQ6Cb7aHOHTSzNVNEhmp8EcWKLb4CbiMW9h9VyIhO4E= github.com/robfig/cron/v3 v3.0.0/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rollbar/rollbar-go v1.0.2/go.mod h1:AcFs5f0I+c71bpHlXNNDbOWJiKwjFDtISeXco0L5PKQ= github.com/rollbar/rollbar-go v1.4.5 h1:Z+5yGaZdB7MFv7t759KUR3VEkGdwHjo7Avvf3ApHTVI= github.com/rollbar/rollbar-go v1.4.5/go.mod h1:kLQ9gP3WCRGrvJmF0ueO3wK9xWocej8GRX98D8sa39w= @@ -515,6 +554,8 @@ github.com/sajari/regression v1.0.1 h1:iTVc6ZACGCkoXC+8NdqH5tIreslDTT/bXxT6OmHR5 github.com/sajari/regression v1.0.1/go.mod h1:NeG/XTW1lYfGY7YV/Z0nYDV/RGh3wxwd1yW46835flM= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +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/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= @@ -540,12 +581,13 @@ github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkU github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= -github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= -github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= +github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= @@ -572,6 +614,12 @@ github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/supranational/blst v0.3.14 h1:xNMoHRJOTwMn63ip6qoWJ2Ymgvj7E2b9jY2FAwY+qRo= +github.com/supranational/blst v0.3.14/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= +github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= @@ -579,6 +627,10 @@ github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65E github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/valyala/fastjson v1.5.1 h1:SXaQZVSwLjZOVhDEhjiCcDtnX0Feu7Z7A1+C5atpoHM= github.com/valyala/fastjson v1.5.1/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +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= github.com/wcharczuk/go-chart/v2 v2.1.0/go.mod h1:yx7MvAVNcP/kN9lKXM/NTce4au4DFN99j6i1OwDclNA= github.com/wcharczuk/go-chart/v2 v2.1.2 h1:Y17/oYNuXwZg6TFag06qe8sBajwwsuvPiJJXcUcLL6E= github.com/wcharczuk/go-chart/v2 v2.1.2/go.mod h1:Zi4hbaqlWpYajnXB2K22IUYVXRXaLfSGNNR7P4ukyyQ= @@ -793,8 +845,8 @@ golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= -golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -915,6 +967,7 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/exchange/factory.go b/pkg/exchange/factory.go index 8c874771f5..13c90a141f 100644 --- a/pkg/exchange/factory.go +++ b/pkg/exchange/factory.go @@ -10,6 +10,7 @@ import ( "github.com/c9s/bbgo/pkg/exchange/bitget" "github.com/c9s/bbgo/pkg/exchange/bybit" "github.com/c9s/bbgo/pkg/exchange/coinbase" + "github.com/c9s/bbgo/pkg/exchange/hyperliquid" "github.com/c9s/bbgo/pkg/exchange/kucoin" "github.com/c9s/bbgo/pkg/exchange/max" "github.com/c9s/bbgo/pkg/exchange/okex" @@ -17,11 +18,13 @@ import ( ) const ( - OptionKeyAPIKey = "API_KEY" - OptionKeyAPISecret = "API_SECRET" - OptionKeyAPIPassphrase = "API_PASSPHRASE" - OptionKeyAPIPrivateKey = "API_PRIVATE_KEy" - OptionKeyAPISubAccount = "API_SUB_ACCOUNT" // for exchanges like Coinbase Pro which support sub accounts + OptionKeyAPIKey = "API_KEY" + OptionKeyAPISecret = "API_SECRET" + OptionKeyAPIPassphrase = "API_PASSPHRASE" + OptionKeyAPIPrivateKey = "API_PRIVATE_KEY" + OptionKeyAPISubAccount = "API_SUB_ACCOUNT" // for exchanges like Coinbase Pro which support sub accounts + OptionKeyAPIMainAccount = "API_MAIN_ACCOUNT" // for exchanges like hyperliquid which main accounts + OptionKeyAPIVaultAccount = "API_VAULT_ACCOUNT" // for exchanges like hyperliquid which support vault accounts ) // Options is a map of exchange options used to initialize an exchange @@ -88,6 +91,12 @@ var factories = map[types.ExchangeName]Factory{ return coinbase.New(options[OptionKeyAPIKey], options[OptionKeyAPISecret], options[OptionKeyAPIPassphrase], 0), nil }, }, + types.ExchangeHyperliquid: { + EnvLoader: DefaultEnvVarLoader, + Constructor: func(options Options) (types.Exchange, error) { + return hyperliquid.New(options[OptionKeyAPIPrivateKey], options[OptionKeyAPIMainAccount], options[OptionKeyAPIVaultAccount]), nil + }, + }, } func Register(name types.ExchangeName, factory Factory) { diff --git a/pkg/exchange/hyperliquid/Makefile b/pkg/exchange/hyperliquid/Makefile new file mode 100644 index 0000000000..920905cc85 --- /dev/null +++ b/pkg/exchange/hyperliquid/Makefile @@ -0,0 +1,7 @@ +generate: symbols go-generate + +symbols: + @echo "Generate symbols.go" && go run ./gensymbols.go + +go-generate: + @echo "Running \`go generate\`" && go generate ./... \ No newline at end of file diff --git a/pkg/exchange/hyperliquid/convert.go b/pkg/exchange/hyperliquid/convert.go new file mode 100644 index 0000000000..cd4259fcd5 --- /dev/null +++ b/pkg/exchange/hyperliquid/convert.go @@ -0,0 +1,213 @@ +package hyperliquid + +import ( + "fmt" + "math" + "strconv" + "strings" + + "github.com/c9s/bbgo/pkg/exchange/hyperliquid/hyperapi" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +func toGlobalSpotMarket(s hyperapi.UniverseMeta, tokens []hyperapi.TokenMeta) types.Market { + base, quote := tokens[s.Tokens[0]], tokens[s.Tokens[1]] + tickSize := fixedpoint.NewFromFloat(math.Pow10(-quote.SzDecimals)) + stepSize := fixedpoint.NewFromFloat(math.Pow10(-base.SzDecimals)) + + return types.Market{ + Exchange: types.ExchangeHyperliquid, + Symbol: base.Name + quote.Name, + LocalSymbol: base.Name + "@" + strconv.Itoa(s.Index), + BaseCurrency: base.Name, + QuoteCurrency: quote.Name, + TickSize: tickSize, + StepSize: stepSize, + MinPrice: fixedpoint.Zero, // not used + MaxPrice: fixedpoint.Zero, // not used + MinNotional: stepSize.Mul(tickSize), + MinAmount: stepSize, + MinQuantity: stepSize, + MaxQuantity: fixedpoint.NewFromFloat(1e9), + PricePrecision: quote.SzDecimals, + VolumePrecision: base.SzDecimals, + } +} + +func toLocalSpotSymbol(symbol string) (string, int) { + if s, ok := spotSymbolSyncMap.Load(symbol); ok { + if localSymbol, ok := s.(string); ok { + at := strings.LastIndexByte(localSymbol, '@') + if at < 0 || at+1 >= len(localSymbol) { + log.Errorf("invalid local symbol format %q for %s", localSymbol, symbol) + return symbol, -1 + } + + if asset, err := strconv.Atoi(localSymbol[at+1:]); err == nil { + return localSymbol[:at], asset + 1000 + } + } + + log.Errorf("failed to convert symbol %s to local symbol and asset, but found in spotSymbolSyncMap", symbol) + } + + log.Errorf("failed to look up local symbol and asset from %s", symbol) + return symbol, -1 +} + +func toLocalFuturesSymbol(symbol string) (string, int) { + if s, ok := futuresSymbolSyncMap.Load(symbol); ok { + if localSymbol, ok := s.(string); ok { + at := strings.LastIndexByte(localSymbol, '@') + if at < 0 || at+1 >= len(localSymbol) { + log.Errorf("invalid local symbol format %q for %s", localSymbol, symbol) + return symbol, -1 + } + + if asset, err := strconv.Atoi(localSymbol[at+1:]); err == nil { + return localSymbol[:at], asset + } + } + + log.Errorf("failed to convert symbol %s to local symbol and asset, but found in futuresSymbolSyncMaps", symbol) + } + + log.Errorf("failed to look up local symbol and asset from %s", symbol) + return symbol, -1 +} + +func toGlobalBalance(account *hyperapi.Account) types.BalanceMap { + balances := make(types.BalanceMap) + for _, b := range account.Balances { + available := b.Total.Sub(b.Hold) + balances[b.Coin] = types.Balance{ + Currency: b.Coin, + Available: available, + Locked: b.Hold, + NetAsset: b.Total, + MaxWithdrawAmount: available, + } + } + return balances +} + +func toGlobalFuturesAccountInfo(rawAccount *hyperapi.FuturesAccount) *types.FuturesAccount { + account := &types.FuturesAccount{ + Assets: make(types.FuturesAssetMap), + Positions: make(types.FuturesPositionMap), + } + account.TotalMarginBalance = rawAccount.MarginSummary.AccountValue + account.TotalWalletBalance = rawAccount.MarginSummary.TotalRawUsd + account.TotalInitialMargin = rawAccount.MarginSummary.TotalMarginUsed + account.TotalMaintMargin = rawAccount.CrossMaintenanceMarginUsed + account.AvailableBalance = rawAccount.Withdrawable + + for _, asset := range rawAccount.AssetPositions { + p := asset.Position + symbol := p.Coin + QuoteCurrency + positionSide := types.PositionLong + if p.Szi.Sign() < 0 { + positionSide = types.PositionShort + } + + posKey := types.NewPositionKey(symbol, positionSide) + account.Positions[posKey] = types.FuturesPosition{ + Symbol: symbol, + PositionSide: positionSide, + BaseCurrency: p.Coin, + QuoteCurrency: QuoteCurrency, + Base: p.Szi.Abs(), + Quote: p.PositionValue, + Isolated: p.Leverage.Type == "isolated", + PositionRisk: toGlobalPositionRisk(asset.Position), + UpdateTime: rawAccount.Time, + } + } + + return account +} + +func toGlobalPositionRisk(p hyperapi.FuturesPosition) *types.PositionRisk { + markPrice := p.EntryPx.Add(p.UnrealizedPnl.Div(p.Szi)) + side := types.PositionLong + if p.Szi.Sign() < 0 { + side = types.PositionShort + } + return &types.PositionRisk{ + Leverage: p.Leverage.Value, + Symbol: p.Coin + QuoteCurrency, + EntryPrice: p.EntryPx, + LiquidationPrice: p.LiquidationPx, + PositionAmount: p.Szi, + UnrealizedPnL: p.UnrealizedPnl, + InitialMargin: p.MarginUsed, + MarkPrice: markPrice, + PositionInitialMargin: p.MarginUsed, + Notional: p.PositionValue, + PositionSide: side, + } +} + +func toLocalInterval(interval types.Interval) (string, error) { + if _, ok := SupportedIntervals[interval]; !ok { + return "", fmt.Errorf("interval %s is not supported", interval) + } + + in, ok := localInterval[interval] + if !ok { + return "", fmt.Errorf("interval %s is not supported, got local interval %s", interval, in) + } + + return in, nil +} + +func kLineToGlobal(k hyperapi.KLine, interval types.Interval, symbol string) types.KLine { + return types.KLine{ + Exchange: types.ExchangeHyperliquid, + Symbol: symbol, + StartTime: types.Time(k.StartTime), + EndTime: types.Time(k.EndTime), + Interval: interval, + Open: k.OpenPrice, + Close: k.ClosePrice, + High: k.HighestPrice, + Low: k.LowestPrice, + Volume: k.Volume, + NumberOfTrades: k.Trades, + QuoteVolume: fixedpoint.Zero, // not supported + TakerBuyBaseAssetVolume: fixedpoint.Zero, // not supported + TakerBuyQuoteAssetVolume: fixedpoint.Zero, // not supported + LastTradeID: 0, // not supported + Closed: true, + } +} + +func toGlobalOrder(order hyperapi.OpenOrder, isFutures bool) types.Order { + // TODO: implement time in force and order type + return types.Order{ + SubmitOrder: types.SubmitOrder{ + Symbol: order.Coin + QuoteCurrency, + Price: order.LimitPx, + Quantity: order.Sz, + Side: toGlobalSide(order.Side), + Type: types.OrderType(order.OrderType), + TimeInForce: types.TimeInForceGTC, + }, + Exchange: types.ExchangeHyperliquid, + OrderID: uint64(order.Oid), + CreationTime: types.Time(order.Timestamp), + UpdateTime: types.Time(order.Timestamp), + IsFutures: isFutures, + } +} + +func toGlobalSide(side string) types.SideType { + switch side { + case "B": + return types.SideTypeBuy + case "A": + return types.SideTypeSell + } + return types.SideType(side) +} diff --git a/pkg/exchange/hyperliquid/convert_test.go b/pkg/exchange/hyperliquid/convert_test.go new file mode 100644 index 0000000000..b1045cd575 --- /dev/null +++ b/pkg/exchange/hyperliquid/convert_test.go @@ -0,0 +1,79 @@ +package hyperliquid + +import "testing" + +func TestToLocalSpotSymbol(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + key string + stored interface{} + wantSymbol string + wantIndex int + }{ + {name: "success", key: "UNITTEST_SUCCESS", stored: "BTC@123", wantSymbol: "BTC", wantIndex: 1123}, + {name: "invalid-format-missing-at", key: "UNITTEST_BADFMT", stored: "123", wantSymbol: "UNITTEST_BADFMT", wantIndex: -1}, + {name: "non-numeric", key: "UNITTEST_NAN", stored: "@abc", wantSymbol: "UNITTEST_NAN", wantIndex: -1}, + {name: "invalid-type", key: "UNITTEST_BADTYPE", stored: 123, wantSymbol: "UNITTEST_BADTYPE", wantIndex: -1}, + {name: "missing-key", key: "UNITTEST_MISSING", stored: nil, wantSymbol: "UNITTEST_MISSING", wantIndex: -1}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + // Setup + spotSymbolSyncMap.Delete(tc.key) + if tc.stored != nil { + spotSymbolSyncMap.Store(tc.key, tc.stored) + } + t.Cleanup(func() { spotSymbolSyncMap.Delete(tc.key) }) + + // Execute + gotSymbol, gotIndex := toLocalSpotSymbol(tc.key) + + // Verify + if gotSymbol != tc.wantSymbol || gotIndex != tc.wantIndex { + t.Fatalf("toLocalSpotAsset(%q) = (%q, %d), want (%q, %d)", tc.key, gotSymbol, gotIndex, tc.wantSymbol, tc.wantIndex) + } + }) + } +} + +func TestToLocalFuturesSymbol(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + key string + stored interface{} + wantSymbol string + wantIndex int + }{ + {name: "success", key: "FUT_SUCCESS", stored: "ETH@987", wantSymbol: "ETH", wantIndex: 987}, + {name: "invalid-format-missing-at", key: "FUT_BADFMT", stored: "987", wantSymbol: "FUT_BADFMT", wantIndex: -1}, + {name: "non-numeric", key: "FUT_NAN", stored: "@abc", wantSymbol: "FUT_NAN", wantIndex: -1}, + {name: "invalid-type", key: "FUT_BADTYPE", stored: 123, wantSymbol: "FUT_BADTYPE", wantIndex: -1}, + {name: "missing-key", key: "FUT_MISSING", stored: nil, wantSymbol: "FUT_MISSING", wantIndex: -1}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + // Setup + futuresSymbolSyncMap.Delete(tc.key) + if tc.stored != nil { + futuresSymbolSyncMap.Store(tc.key, tc.stored) + } + t.Cleanup(func() { futuresSymbolSyncMap.Delete(tc.key) }) + + // Execute + gotSymbol, gotIndex := toLocalFuturesSymbol(tc.key) + + // Verify + if gotSymbol != tc.wantSymbol || gotIndex != tc.wantIndex { + t.Fatalf("toLocalFuturesAsset(%q) = (%q, %d), want (%q, %d)", tc.key, gotSymbol, gotIndex, tc.wantSymbol, tc.wantIndex) + } + }) + } +} diff --git a/pkg/exchange/hyperliquid/exchange.go b/pkg/exchange/hyperliquid/exchange.go new file mode 100644 index 0000000000..5f789a4f15 --- /dev/null +++ b/pkg/exchange/hyperliquid/exchange.go @@ -0,0 +1,444 @@ +package hyperliquid + +import ( + "context" + "encoding/json" + "fmt" + "regexp" + "sync" + "time" + + "github.com/c9s/bbgo/pkg/exchange/hyperliquid/hyperapi" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" + "github.com/sirupsen/logrus" + "go.uber.org/multierr" + "golang.org/x/time/rate" +) + +const ( + HYPE = "HYPE" + + ID types.ExchangeName = "hyperliquid" +) + +// REST requests share an aggregated weight limit of 1200 per minute. +var restSharedLimiter = rate.NewLimiter(rate.Every(50*time.Millisecond), 1) + +// clientOrderIdRegex is 128-bit hex string starting with 0x and followed by 32 hex characters +var clientOrderIdRegex = regexp.MustCompile(`^0x[0-9a-fA-F]{32}$`) + +var log = logrus.WithFields(logrus.Fields{ + "exchange": ID, +}) + +type Exchange struct { + types.FuturesSettings + + secret string + + client *hyperapi.Client +} + +func New(secret, accountAddress, vaultAddress string) *Exchange { + client := hyperapi.NewClient() + if len(secret) > 0 { + client.Auth(secret, accountAddress) + } + + if len(vaultAddress) > 0 { + client.SetVaultAddress(vaultAddress) + } + return &Exchange{ + secret: secret, + client: client, + } +} + +func (e *Exchange) Name() types.ExchangeName { + return types.ExchangeHyperliquid +} + +func (e *Exchange) PlatformFeeCurrency() string { + return HYPE +} + +func (e *Exchange) Initialize(ctx context.Context) error { + if err := e.syncSymbolsToMap(ctx); err != nil { + return err + } + + return nil +} + +func (e *Exchange) QueryMarkets(ctx context.Context) (types.MarketMap, error) { + if err := restSharedLimiter.Wait(ctx); err != nil { + return nil, fmt.Errorf("markets rate limiter wait error: %w", err) + } + + if e.IsFutures { + return e.queryFuturesMarkets(ctx) + } + + meta, err := e.client.NewSpotGetMetaRequest().Do(ctx) + if err != nil { + return nil, err + } + + markets := types.MarketMap{} + for _, s := range meta.Universe { + market := toGlobalSpotMarket(s, meta.Tokens) + markets.Add(market) + } + + return markets, nil +} + +func (e *Exchange) QueryAccount(ctx context.Context) (*types.Account, error) { + if e.IsFutures { + return e.queryFuturesAccount(ctx) + } + + balances, err := e.querySpotAccountBalance(ctx) + if err != nil { + return nil, err + } + + account := types.NewAccount() + account.UpdateBalances(toGlobalBalance(balances)) + + return account, nil +} + +func (e *Exchange) QueryAccountBalances(ctx context.Context) (types.BalanceMap, error) { + balances, err := e.querySpotAccountBalance(ctx) + if err != nil { + return nil, err + } + + return toGlobalBalance(balances), nil +} + +func (e *Exchange) querySpotAccountBalance(ctx context.Context) (*hyperapi.Account, error) { + if err := restSharedLimiter.Wait(ctx); err != nil { + return nil, fmt.Errorf("account rate limiter wait error: %w", err) + } + + return e.client.NewGetAccountBalanceRequest().User(e.client.UserAddress()).Do(ctx) +} + +func (e *Exchange) SubmitOrder(ctx context.Context, order types.SubmitOrder) (createdOrder *types.Order, err error) { + // Validate order parameters + if len(order.Market.Symbol) == 0 { + return nil, fmt.Errorf("order.Market.Symbol is required: %+v", order) + } + if order.Quantity.IsZero() { + return nil, fmt.Errorf("order.Quantity is required: %+v", order) + } + + // Build order request + _, assetIdx := e.getLocalSymbol(order.Symbol) + reqOrder := hyperapi.Order{ + Asset: assetIdx, + IsBuy: order.Side == types.SideTypeBuy, + Size: order.Quantity.String(), + Price: order.Price.String(), + ReduceOnly: order.ReduceOnly, + } + + // Validate and set client order ID if provided + if len(order.ClientOrderID) > 0 { + if !clientOrderIdRegex.MatchString(order.ClientOrderID) { + return nil, fmt.Errorf("client order id should be a 128-bit hex string starting with 0x and followed by 32 hex characters: %s", order.ClientOrderID) + } + reqOrder.ClientOrderID = &order.ClientOrderID + } + + // Set order type and time in force + switch order.Type { + case types.OrderTypeLimit, types.OrderTypeLimitMaker: + tif := hyperapi.TimeInForceGTC + switch order.TimeInForce { + case types.TimeInForceIOC: + tif = hyperapi.TimeInForceIOC + case types.TimeInForceALO: + tif = hyperapi.TimeInForceALO + } + reqOrder.OrderType = hyperapi.OrderType{Limit: &hyperapi.LimitOrderType{Tif: tif}} + + case types.OrderTypeMarket: + reqOrder.OrderType = hyperapi.OrderType{ + Trigger: &hyperapi.TriggerOrderType{ + IsMarket: true, + TriggerPx: "0", + }, + } + + case types.OrderTypeStopLimit: + reqOrder.OrderType = hyperapi.OrderType{ + Trigger: &hyperapi.TriggerOrderType{ + IsMarket: false, + TriggerPx: order.StopPrice.String(), + Tpsl: hyperapi.StopLoss, + }, + } + + case types.OrderTypeStopMarket, types.OrderTypeTakeProfitMarket: + tpsl := hyperapi.StopLoss + if order.Type == types.OrderTypeTakeProfitMarket { + tpsl = hyperapi.TakeProfit + } + reqOrder.OrderType = hyperapi.OrderType{ + Trigger: &hyperapi.TriggerOrderType{ + IsMarket: true, + TriggerPx: order.StopPrice.String(), + Tpsl: tpsl, + }, + } + + default: + return nil, fmt.Errorf("unsupported order type: %v", order.Type) + } + + if err := restSharedLimiter.Wait(ctx); err != nil { + return nil, fmt.Errorf("submit order rate limiter wait error: %w", err) + } + + // Submit order to exchange + resp, err := e.client.NewPlaceOrderRequest().Orders([]hyperapi.Order{reqOrder}).Do(ctx) + if err != nil { + return nil, fmt.Errorf("failed to place order: %w", err) + } + + // Validate response + if resp == nil || len(resp.Statuses) == 0 { + return nil, fmt.Errorf("invalid order response: resp=%v, statuses=%d", resp != nil, len(resp.Statuses)) + } + + status := resp.Statuses[0] + if status.Error != "" { + return nil, fmt.Errorf("order submission error: %s", status.Error) + } + + // Extract order information from response + var ( + orderID int + orderStatus = types.OrderStatusNew + executedQuantity = fixedpoint.Zero + ) + + if status.Resting != nil { + orderID = status.Resting.Oid + } else if status.Filled != nil { + orderID = status.Filled.Oid + orderStatus = types.OrderStatusFilled + executedQuantity = status.Filled.TotalSz + } + + // Build and return order object + timeNow := time.Now() + return &types.Order{ + SubmitOrder: order, + Exchange: types.ExchangeHyperliquid, + OrderID: uint64(orderID), + Status: orderStatus, + ExecutedQuantity: executedQuantity, + IsWorking: true, + CreationTime: types.Time(timeNow), + UpdateTime: types.Time(timeNow), + }, nil +} + +func (e *Exchange) QueryOpenOrders(ctx context.Context, symbol string) (orders []types.Order, err error) { + if err := restSharedLimiter.Wait(ctx); err != nil { + return nil, fmt.Errorf("query open orders rate limiter wait error: %w", err) + } + + resp, err := e.client.NewGetOpenOrdersRequest().User(e.client.UserAddress()).Do(ctx) + if err != nil { + return nil, fmt.Errorf("failed to query open orders: %w", err) + } + + localSymbol, _ := e.getLocalSymbol(symbol) + for _, order := range resp { + if order.Coin != localSymbol { + continue + } + orders = append(orders, toGlobalOrder(order, e.IsFutures)) + } + + return orders, nil +} + +func (e *Exchange) CancelOrders(ctx context.Context, orders ...types.Order) error { + if len(orders) == 0 { + return fmt.Errorf("orders are required") + } + + canceledOrders := make([]hyperapi.CancelOrder, 0) + for _, order := range orders { + _, assetIdx := e.getLocalSymbol(order.Symbol) + canceledOrders = append(canceledOrders, hyperapi.CancelOrder{ + Asset: assetIdx, + OrderId: int(order.OrderID), + }) + } + + if err := restSharedLimiter.Wait(ctx); err != nil { + return fmt.Errorf("cancel order rate limiter wait error: %w", err) + } + + resp, err := e.client.NewCancelOrderRequest().CancelOrders(canceledOrders).Do(ctx) + if err != nil { + return fmt.Errorf("failed to cancel orders: %w", err) + } + + var errs error + if resp != nil && len(resp.Statuses) > 0 { + var statusError struct{ Error string } + for _, status := range resp.Statuses { + if err := json.Unmarshal(status, &statusError); err != nil { + continue + } + if statusError.Error != "" { + errs = multierr.Append(errs, fmt.Errorf("%s", statusError.Error)) + } + } + } + + return errs +} + +func (e *Exchange) NewStream() types.Stream { + return NewStream(e.client, e) +} + +func (e *Exchange) QueryTicker(ctx context.Context, symbol string) (*types.Ticker, error) { + tickers, err := e.QueryTickers(ctx, symbol) + if err != nil { + return nil, err + } + + t, ok := tickers[symbol] + if !ok { + return nil, fmt.Errorf("ticker not found for symbol: %s", symbol) + } + return &t, nil +} + +func (e *Exchange) QueryTickers(ctx context.Context, symbol ...string) (map[string]types.Ticker, error) { + if err := restSharedLimiter.Wait(ctx); err != nil { + return nil, fmt.Errorf("query ticker rate limiter wait error: %w", err) + } + + _, err := e.client.NewSpotGetMetaAndAssetCtxsRequest().Do(ctx) + if err != nil { + return nil, err + } + + result := make(map[string]types.Ticker, len(symbol)) + // TODO + + return result, nil +} + +func (e *Exchange) QueryKLines(ctx context.Context, symbol string, interval types.Interval, options types.KLineQueryOptions) ([]types.KLine, error) { + if err := restSharedLimiter.Wait(ctx); err != nil { + return nil, fmt.Errorf("query k line rate limiter wait error: %w", err) + } + + localSymbol, _ := e.getLocalSymbol(symbol) + intervalParam, err := toLocalInterval(interval) + if err != nil { + return nil, fmt.Errorf("failed to get interval: %w", err) + } + + candleReq := hyperapi.CandleRequest{ + Interval: intervalParam, + Coin: localSymbol, + } + + if options.StartTime != nil { + candleReq.StartTime = options.StartTime.UnixMilli() + } + + if options.EndTime != nil { + candleReq.EndTime = options.EndTime.UnixMilli() + } + + candles, err := e.client.NewGetCandlesRequest().CandleRequest(candleReq).Do(ctx) + if err != nil { + return nil, err + } + var kLines []types.KLine + for _, candle := range candles { + kLines = append(kLines, kLineToGlobal(candle, interval, symbol)) + } + + return kLines, nil +} + +// DefaultFeeRates returns the hyperliquid base fee schedule +// See futures fee at: https://hyperliquid.gitbook.io/hyperliquid-docs/trading/fees +func (e *Exchange) DefaultFeeRates() types.ExchangeFee { + if e.IsFutures { + return types.ExchangeFee{ + MakerFeeRate: fixedpoint.NewFromFloat(0.01 * 0.0150), // 0.0150% + TakerFeeRate: fixedpoint.NewFromFloat(0.01 * 0.0450), // 0.0450% + } + } + + return types.ExchangeFee{ + MakerFeeRate: fixedpoint.NewFromFloat(0.01 * 0.040), // 0.040% + TakerFeeRate: fixedpoint.NewFromFloat(0.01 * 0.070), // 0.070% + } +} + +func (e *Exchange) SupportedInterval() map[types.Interval]int { + return SupportedIntervals +} +func (e *Exchange) IsSupportedInterval(interval types.Interval) bool { + _, ok := SupportedIntervals[interval] + return ok +} + +func (e *Exchange) syncSymbolsToMap(ctx context.Context) error { + markets, err := e.QueryMarkets(ctx) + if err != nil { + return err + } + + symbolMap := func() *sync.Map { + if e.IsFutures { + return &futuresSymbolSyncMap + } + return &spotSymbolSyncMap + }() + + // Mark all valid symbols + existing := make(map[string]struct{}, len(markets)) + for symbol, market := range markets { + symbolMap.Store(symbol, market.LocalSymbol) + existing[symbol] = struct{}{} + } + + // Remove outdated symbols + symbolMap.Range(func(key, _ interface{}) bool { + if symbol, ok := key.(string); !ok || existing[symbol] == struct{}{} { + return true + } else if _, found := existing[symbol]; !found { + symbolMap.Delete(symbol) + } + return true + }) + + return nil +} + +func (e *Exchange) getLocalSymbol(symbol string) (string, int) { + if e.IsFutures { + return toLocalFuturesSymbol(symbol) + } + + return toLocalSpotSymbol(symbol) +} diff --git a/pkg/exchange/hyperliquid/exchange_test.go b/pkg/exchange/hyperliquid/exchange_test.go new file mode 100644 index 0000000000..da76b6f1a3 --- /dev/null +++ b/pkg/exchange/hyperliquid/exchange_test.go @@ -0,0 +1,507 @@ +package hyperliquid + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "strconv" + "testing" + + "github.com/c9s/bbgo/pkg/exchange/hyperliquid/hyperapi" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/testing/httptesting" + . "github.com/c9s/bbgo/pkg/testing/testhelper" + "github.com/c9s/bbgo/pkg/testutil" + "github.com/c9s/bbgo/pkg/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func newMockTestExchange(t *testing.T, isFutures bool) (*Exchange, *httptesting.MockTransport) { + t.Helper() + + privateKey, err := crypto.GenerateKey() + require.NoError(t, err) + + ex := New(fmt.Sprintf("%x", crypto.FromECDSA(privateKey)), crypto.PubkeyToAddress(privateKey.PublicKey).Hex(), "") + ex.IsFutures = isFutures + + transport := &httptesting.MockTransport{} + ex.client.HttpClient.Transport = transport + + return ex, transport +} + +func TestQueryMarkets(t *testing.T) { + t.Run("succeeds querying spot markets", func(t *testing.T) { + ex, transport := newMockTestExchange(t, false) + + spotMeta, err := os.ReadFile("./testdata/spotMeta.json") + require.NoError(t, err) + + transport.POST("/info", func(req *http.Request) (*http.Response, error) { + return httptesting.BuildResponseString(http.StatusOK, string(spotMeta)), nil + }) + + markets, err := ex.QueryMarkets(context.Background()) + assert.NoError(t, err) + assert.NotNil(t, markets) + assert.Greater(t, len(markets), 0) + + // Verify specific market data + purrUsdcMarket, exists := markets["PURRUSDC"] + assert.True(t, exists, "PURRUSDC market should exist") + assert.Equal(t, types.ExchangeHyperliquid, purrUsdcMarket.Exchange) + assert.Equal(t, "PURRUSDC", purrUsdcMarket.Symbol) + assert.Equal(t, "PURR", purrUsdcMarket.BaseCurrency) + assert.Equal(t, "USDC", purrUsdcMarket.QuoteCurrency) + assert.Equal(t, "PURR@0", purrUsdcMarket.LocalSymbol) + assert.Equal(t, 8, purrUsdcMarket.PricePrecision) + assert.Equal(t, 0, purrUsdcMarket.VolumePrecision) + }) + + t.Run("succeeds querying preps markets", func(t *testing.T) { + ex, transport := newMockTestExchange(t, true) + + perpsMeta, err := os.ReadFile("./testdata/perpsMeta.json") + require.NoError(t, err) + + transport.POST("/info", func(req *http.Request) (*http.Response, error) { + return httptesting.BuildResponseString(http.StatusOK, string(perpsMeta)), nil + }) + + markets, err := ex.QueryMarkets(context.Background()) + assert.NoError(t, err) + assert.NotNil(t, markets) + assert.Greater(t, len(markets), 0) + + purrUsdcMarket, exists := markets["ETHUSDC"] + assert.True(t, exists, "ETHUSDC market should exist") + assert.Equal(t, "ETH", purrUsdcMarket.BaseCurrency) + assert.Equal(t, "USDC", purrUsdcMarket.QuoteCurrency) + }) + +} + +func TestQueryAccount(t *testing.T) { + t.Run("succeeds querying spot account", func(t *testing.T) { + ex, transport := newMockTestExchange(t, false) + + spotMeta, err := os.ReadFile("./testdata/spotClearinghouseState.json") + require.NoError(t, err) + + transport.POST("/info", func(req *http.Request) (*http.Response, error) { + return httptesting.BuildResponseString(http.StatusOK, string(spotMeta)), nil + }) + + account, err := ex.QueryAccount(context.Background()) + assert.NoError(t, err) + assert.NotNil(t, account) + + // Verify account balances + balances := account.Balances() + assert.NotNil(t, balances) + assert.Greater(t, len(balances), 0) + + // Verify specific balance data from test data + usdcBalance, exists := balances["USDC"] + assert.True(t, exists, "USDC balance should exist") + assert.Equal(t, "USDC", usdcBalance.Currency) + assert.Equal(t, "1808443.14193129", usdcBalance.NetAsset.String()) + assert.Equal(t, "1808443.14193129", usdcBalance.Available.String()) + assert.Equal(t, 0.0, usdcBalance.Locked.Float64()) + + hypeBalance, exists := balances["HYPE"] + assert.True(t, exists, "HYPE balance should exist") + assert.Equal(t, "HYPE", hypeBalance.Currency) + assert.Equal(t, "503808.46025243", hypeBalance.NetAsset.String()) + assert.Equal(t, "503778.46025243", hypeBalance.Available.String()) + assert.Equal(t, 30.0, hypeBalance.Locked.Float64()) + + // Verify account type is spot (default) + assert.Equal(t, types.AccountTypeSpot, account.AccountType) + }) + + t.Run("succeeds querying futures account", func(t *testing.T) { + ex, transport := newMockTestExchange(t, true) + + clearinghouseState, err := os.ReadFile("./testdata/clearinghouseState.json") + require.NoError(t, err) + + transport.POST("/info", func(req *http.Request) (*http.Response, error) { + return httptesting.BuildResponseString(http.StatusOK, string(clearinghouseState)), nil + }) + + account, err := ex.QueryAccount(context.Background()) + assert.NoError(t, err) + assert.NotNil(t, account) + + // Verify account type is futures + assert.Equal(t, types.AccountTypeFutures, account.AccountType) + + // Verify futures info is initialized + assert.NotNil(t, account.FuturesInfo) + assert.NotNil(t, account.FuturesInfo.Assets) + assert.NotNil(t, account.FuturesInfo.Positions) + + futuresInfo := account.FuturesInfo + + // Verify futures account summary fields + // Use InDelta for floating point comparisons to handle platform-specific precision differences + const delta = 1e-6 // Allow 0.000001 difference for large numbers + assert.InDelta(t, 121968206.668798998, futuresInfo.TotalMarginBalance.Float64(), delta) + assert.InDelta(t, 46255052.1990050003, futuresInfo.TotalInitialMargin.Float64(), delta) + assert.InDelta(t, 12598833.5037019998, futuresInfo.TotalMaintMargin.Float64(), delta) + assert.InDelta(t, 75713154.4697940052, futuresInfo.AvailableBalance.Float64(), delta) + + // Verify positions count + assert.Equal(t, 11, len(futuresInfo.Positions)) + + // Verify BTC short position + btcPosKey := types.NewPositionKey("BTCUSDC", types.PositionShort) + btcPos, exists := futuresInfo.Positions[btcPosKey] + assert.True(t, exists, "BTC short position should exist") + assert.Equal(t, "BTCUSDC", btcPos.Symbol) + assert.Equal(t, types.PositionShort, btcPos.PositionSide) + assert.Equal(t, "BTC", btcPos.BaseCurrency) + assert.Equal(t, "USDC", btcPos.QuoteCurrency) + assert.Equal(t, "1186.74032", btcPos.Base.String()) + assert.InDelta(t, 120498051.8718400002, btcPos.Quote.Float64(), delta) + assert.False(t, btcPos.Isolated, "BTC position should be cross margin") + assert.NotNil(t, btcPos.PositionRisk) + assert.Equal(t, "10", btcPos.PositionRisk.Leverage.String()) + assert.Equal(t, "111616.8", btcPos.PositionRisk.EntryPrice.String()) + assert.InDelta(t, 191751.8562150183, btcPos.PositionRisk.LiquidationPrice.Float64(), delta) + assert.InDelta(t, 11962126.6084020007, btcPos.PositionRisk.UnrealizedPnL.Float64(), delta) + assert.InDelta(t, 120498051.8718400002, btcPos.PositionRisk.Notional.Float64(), delta) + + // Verify ETH short position + ethPosKey := types.NewPositionKey("ETHUSDC", types.PositionShort) + ethPos, exists := futuresInfo.Positions[ethPosKey] + assert.True(t, exists, "ETH short position should exist") + assert.Equal(t, "ETHUSDC", ethPos.Symbol) + assert.Equal(t, types.PositionShort, ethPos.PositionSide) + assert.Equal(t, "ETH", ethPos.BaseCurrency) + assert.Equal(t, "51446.7322", ethPos.Base.String()) + assert.InDelta(t, 170664244.7270599902, ethPos.Quote.Float64(), delta) + assert.False(t, ethPos.Isolated, "ETH position should be cross margin") + assert.NotNil(t, ethPos.PositionRisk) + assert.Equal(t, "10", ethPos.PositionRisk.Leverage.String()) + assert.Equal(t, "3527.73", ethPos.PositionRisk.EntryPrice.String()) + assert.InDelta(t, 5374.5993530082, ethPos.PositionRisk.LiquidationPrice.Float64(), delta) + + // Verify ASTER long position (the only long position in test data) + asterPosKey := types.NewPositionKey("ASTERUSDC", types.PositionLong) + asterPos, exists := futuresInfo.Positions[asterPosKey] + assert.True(t, exists, "ASTER long position should exist") + assert.Equal(t, "ASTERUSDC", asterPos.Symbol) + assert.Equal(t, types.PositionLong, asterPos.PositionSide) + assert.Equal(t, "ASTER", asterPos.BaseCurrency) + assert.Equal(t, "3058645", asterPos.Base.String()) + assert.Equal(t, "3148263.2985", asterPos.Quote.String()) + assert.False(t, asterPos.Isolated, "ASTER position should be cross margin") + assert.NotNil(t, asterPos.PositionRisk) + assert.Equal(t, "3", asterPos.PositionRisk.Leverage.String()) + assert.Equal(t, "0.968318", asterPos.PositionRisk.EntryPrice.String()) + }) + +} + +func TestQueryAccountBalances(t *testing.T) { + t.Run("succeeds querying spot account balances", func(t *testing.T) { + ex, transport := newMockTestExchange(t, false) + + spotMeta, err := os.ReadFile("./testdata/spotClearinghouseState.json") + require.NoError(t, err) + + transport.POST("/info", func(req *http.Request) (*http.Response, error) { + return httptesting.BuildResponseString(http.StatusOK, string(spotMeta)), nil + }) + + balances, err := ex.QueryAccountBalances(context.Background()) + assert.NoError(t, err) + assert.NotNil(t, balances) + assert.Greater(t, len(balances), 0) + + // Verify USDC balance + usdcBalance, exists := balances["USDC"] + assert.True(t, exists, "USDC balance should exist") + assert.Equal(t, "USDC", usdcBalance.Currency) + assert.Equal(t, "1808443.14193129", usdcBalance.NetAsset.String()) + assert.Equal(t, "1808443.14193129", usdcBalance.Available.String()) + assert.Equal(t, 0.0, usdcBalance.Locked.Float64()) + assert.Equal(t, "1808443.14193129", usdcBalance.MaxWithdrawAmount.String()) + + // Verify HYPE balance + hypeBalance, exists := balances["HYPE"] + assert.True(t, exists, "HYPE balance should exist") + assert.Equal(t, "HYPE", hypeBalance.Currency) + assert.Equal(t, "503808.46025243", hypeBalance.NetAsset.String()) + assert.Equal(t, "503778.46025243", hypeBalance.Available.String()) + assert.Equal(t, 30.0, hypeBalance.Locked.Float64()) + assert.Equal(t, "503778.46025243", hypeBalance.MaxWithdrawAmount.String()) + + // Verify LATINA balance + latinaBalance, exists := balances["LATINA"] + assert.True(t, exists, "LATINA balance should exist") + assert.Equal(t, "LATINA", latinaBalance.Currency) + assert.Equal(t, "45658.0657", latinaBalance.NetAsset.String()) + assert.Equal(t, "45658.0657", latinaBalance.Available.String()) + assert.Equal(t, 0.0, latinaBalance.Locked.Float64()) + + // Verify UBTC balance (zero balance) + ubtcBalance, exists := balances["UBTC"] + assert.True(t, exists, "UBTC balance should exist") + assert.Equal(t, "UBTC", ubtcBalance.Currency) + assert.Equal(t, "0", ubtcBalance.NetAsset.String()) + assert.Equal(t, "0", ubtcBalance.Available.String()) + assert.Equal(t, 0.0, ubtcBalance.Locked.Float64()) + }) +} + +func TestQueryKLines(t *testing.T) { + t.Run("succeeds querying futures klines without time bounds", func(t *testing.T) { + ex, transport := newMockTestExchange(t, true) + + candles, err := os.ReadFile("./testdata/candles.json") + require.NoError(t, err) + + transport.POST("/info", func(req *http.Request) (*http.Response, error) { + body, err := io.ReadAll(req.Body) + require.NoError(t, err) + + var payload map[string]any + require.NoError(t, json.Unmarshal(body, &payload)) + + reqType, ok := payload["type"].(string) + require.True(t, ok) + assert.Equal(t, "candleSnapshot", reqType) + + reqPayload, ok := payload["req"].(map[string]any) + require.True(t, ok) + + assert.Equal(t, "BTC", reqPayload["coin"]) + assert.Equal(t, "1h", reqPayload["interval"]) + _, hasStart := reqPayload["startTime"] + assert.True(t, hasStart) + _, hasEnd := reqPayload["endTime"] + assert.False(t, hasEnd) + + return httptesting.BuildResponseString(http.StatusOK, string(candles)), nil + }) + + kLines, err := ex.QueryKLines(context.Background(), "BTCUSDC", types.Interval1h, types.KLineQueryOptions{}) + require.NoError(t, err) + require.Len(t, kLines, 3) + + first := kLines[0] + assert.Equal(t, types.ExchangeHyperliquid, first.Exchange) + assert.Equal(t, "BTCUSDC", first.Symbol) + assert.Equal(t, types.Interval1h, first.Interval) + assert.True(t, first.Closed) + assert.Equal(t, uint64(14946), first.NumberOfTrades) + + assert.Equal(t, int64(1762776000000), first.StartTime.Time().UnixMilli()) + assert.Equal(t, int64(1762779599999), first.EndTime.Time().UnixMilli()) + assert.Equal(t, fixedpoint.MustNewFromString("106211.0"), first.Open) + assert.Equal(t, fixedpoint.MustNewFromString("105968.0"), first.Close) + assert.Equal(t, fixedpoint.MustNewFromString("106248.0"), first.High) + assert.Equal(t, fixedpoint.MustNewFromString("105886.0"), first.Low) + assert.Equal(t, fixedpoint.MustNewFromString("736.72125"), first.Volume) + }) +} + +func TestMockExchange_SubmitOrder(t *testing.T) { + + t.Run("places limit order and returns resting response", func(t *testing.T) { + ex, transport := newMockTestExchange(t, true) + ctx := context.Background() + + quantity := fixedpoint.MustNewFromString("0.5") + price := fixedpoint.MustNewFromString("100000") + + transport.POST("/exchange", func(req *http.Request) (*http.Response, error) { + body, err := io.ReadAll(req.Body) + require.NoError(t, err) + + var payload struct { + Action struct { + Type string `json:"type"` + Orders []hyperapi.Order `json:"orders"` + Grouping string `json:"grouping"` + } `json:"action"` + } + + require.NoError(t, json.Unmarshal(body, &payload)) + require.Len(t, payload.Action.Orders, 1) + + submitted := payload.Action.Orders[0] + assert.Equal(t, 0, submitted.Asset) + assert.True(t, submitted.IsBuy) + assert.Equal(t, quantity.String(), submitted.Size) + assert.Equal(t, price.String(), submitted.Price) + assert.False(t, submitted.ReduceOnly) + assert.Equal(t, hyperapi.TimeInForceGTC, submitted.OrderType.Limit.Tif) + + return httptesting.BuildResponseJson(http.StatusOK, map[string]any{ + "status": "success", + "response": map[string]any{ + "type": "order", + "data": map[string]any{ + "statuses": []any{ + map[string]any{ + "resting": map[string]any{"oid": 123456}, + }, + }, + }, + }, + }), nil + }) + + order := types.SubmitOrder{ + Symbol: "BTCUSDC", + Market: types.Market{Symbol: "BTCUSDC"}, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimit, + Quantity: quantity, + Price: price, + TimeInForce: types.TimeInForceGTC, + } + + createdOrder, err := ex.SubmitOrder(ctx, order) + require.NoError(t, err) + require.NotNil(t, createdOrder) + + assert.Equal(t, uint64(123456), createdOrder.OrderID) + assert.Equal(t, types.OrderStatusNew, createdOrder.Status) + assert.True(t, createdOrder.IsWorking) + assert.True(t, createdOrder.ExecutedQuantity.IsZero()) + }) + + t.Run("returns filled status when order is fully executed", func(t *testing.T) { + ex, transport := newMockTestExchange(t, true) + ctx := context.Background() + + quantity := fixedpoint.MustNewFromString("1.25") + + transport.POST("/exchange", func(req *http.Request) (*http.Response, error) { + body, err := io.ReadAll(req.Body) + require.NoError(t, err) + + var payload struct { + Action struct { + Type string `json:"type"` + Orders []hyperapi.Order `json:"orders"` + } `json:"action"` + } + + require.NoError(t, json.Unmarshal(body, &payload)) + require.Len(t, payload.Action.Orders, 1) + + submitted := payload.Action.Orders[0] + assert.True(t, submitted.OrderType.Trigger.IsMarket) + assert.Equal(t, "0", submitted.OrderType.Trigger.TriggerPx) + + return httptesting.BuildResponseJson(http.StatusOK, map[string]any{ + "status": "success", + "response": map[string]any{ + "type": "order", + "data": map[string]any{ + "statuses": []any{ + map[string]any{ + "filled": map[string]any{ + "oid": 888, + "totalSz": quantity.String(), + "avgPx": "105000", + }, + }, + }, + }, + }, + }), nil + }) + + order := types.SubmitOrder{ + Symbol: "BTCUSDC", + Market: types.Market{Symbol: "BTCUSDC"}, + Side: types.SideTypeSell, + Type: types.OrderTypeMarket, + Quantity: quantity, + Price: fixedpoint.Zero, + } + + createdOrder, err := ex.SubmitOrder(ctx, order) + require.NoError(t, err) + require.NotNil(t, createdOrder) + + assert.Equal(t, uint64(888), createdOrder.OrderID) + assert.Equal(t, types.OrderStatusFilled, createdOrder.Status) + assert.Equal(t, quantity.String(), createdOrder.ExecutedQuantity.String()) + }) + + t.Run("rejects invalid client order id", func(t *testing.T) { + ex, _ := newMockTestExchange(t, true) + ctx := context.Background() + + order := types.SubmitOrder{ + Symbol: "BTCUSDC", + Market: types.Market{Symbol: "BTCUSDC"}, + Side: types.SideTypeBuy, + Type: types.OrderTypeLimit, + Quantity: fixedpoint.MustNewFromString("0.1"), + Price: fixedpoint.MustNewFromString("100000"), + ClientOrderID: "invalidID", + } + + createdOrder, err := ex.SubmitOrder(ctx, order) + require.Error(t, err) + assert.Nil(t, createdOrder) + assert.Contains(t, err.Error(), "client order id") + }) +} + +func TestExchange_SubmitOrder(t *testing.T) { + if b, _ := strconv.ParseBool(os.Getenv("CI")); b { + t.Skip("skip test for CI") + } + + privateKey, mainAccount, vaultAccount, ok := testutil.IntegrationTestWithPrivateKeyConfigured(t, "HYPERLIQUID") + if !ok { + t.SkipNow() + } + + ctx := context.Background() + ex := New(privateKey, mainAccount, vaultAccount) + hyperapi.TestNet = true + ex.IsFutures = true + + account, err := ex.QueryAccount(ctx) + require.NoError(t, err) + require.NotNil(t, account) + + t.Logf("account: %+v", account) + + markets, err := ex.QueryMarkets(ctx) + assert.NoError(t, err) + + market, ok := markets["BTCUSDC"] + if assert.True(t, ok) { + t.Logf("market: %+v", market) + createdOrder, err := ex.SubmitOrder(ctx, types.SubmitOrder{ + Symbol: "BTCUSDC", + Type: types.OrderTypeLimit, + Price: Number(84925), + Quantity: fixedpoint.MustNewFromString("0.001"), + Side: types.SideTypeBuy, + Market: market, + }) + if assert.NoError(t, err) { + t.Error(err.Error()) + t.Logf("createdOrder: %+v", createdOrder) + } + } +} diff --git a/pkg/exchange/hyperliquid/futures.go b/pkg/exchange/hyperliquid/futures.go new file mode 100644 index 0000000000..da3991f2d1 --- /dev/null +++ b/pkg/exchange/hyperliquid/futures.go @@ -0,0 +1,60 @@ +package hyperliquid + +import ( + "context" + "fmt" + "math" + "strconv" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +const QuoteCurrency = "USDC" + +func (e *Exchange) queryFuturesMarkets(ctx context.Context) (types.MarketMap, error) { + meta, err := e.client.NewFuturesGetMetaRequest().Do(ctx) + if err != nil { + return nil, err + } + + markets := types.MarketMap{} + for i, u := range meta.Universe { + stepSize := fixedpoint.NewFromFloat(1 / math.Pow10(u.SzDecimals)) + tickSize := fixedpoint.NewFromFloat(1 / math.Pow10(8)) + markets.Add(types.Market{ + Exchange: types.ExchangeHyperliquid, + Symbol: u.Name + QuoteCurrency, + LocalSymbol: u.Name + "@" + strconv.Itoa(i), + BaseCurrency: u.Name, + QuoteCurrency: QuoteCurrency, + PricePrecision: 8, + VolumePrecision: u.SzDecimals, + StepSize: stepSize, + TickSize: tickSize, + MinNotional: stepSize.Mul(tickSize), + MinAmount: stepSize, + MinQuantity: stepSize, + MaxQuantity: fixedpoint.NewFromFloat(1e9), + }) + } + + return markets, nil +} + +func (e *Exchange) queryFuturesAccount(ctx context.Context) (*types.Account, error) { + if err := restSharedLimiter.Wait(ctx); err != nil { + return nil, fmt.Errorf("account rate limiter wait error: %w", err) + } + + futuresAccount, err := e.client.NewFuturesGetAccountBalanceRequest().User(e.client.UserAddress()).Do(ctx) + if err != nil { + return nil, err + } + + account := types.NewAccount() + account.AccountType = types.AccountTypeFutures + account.FuturesInfo = toGlobalFuturesAccountInfo(futuresAccount) + + return account, nil +} diff --git a/pkg/exchange/hyperliquid/gensymbols.go b/pkg/exchange/hyperliquid/gensymbols.go new file mode 100644 index 0000000000..30b6686d70 --- /dev/null +++ b/pkg/exchange/hyperliquid/gensymbols.go @@ -0,0 +1,71 @@ +//go:build ignore +// +build ignore + +package main + +import ( + "context" + "log" + "os" + "strconv" + "text/template" + + "github.com/c9s/bbgo/pkg/exchange/hyperliquid" + "github.com/c9s/bbgo/pkg/exchange/hyperliquid/hyperapi" +) + +var packageTemplate = template.Must(template.New("").Parse(`// Code generated by go generate; DO NOT EDIT. +package hyperliquid + +var spotSymbolMap = map[string]string{ +{{- range $k, $v := .spot }} + {{ printf "%q" $k }}: {{ printf "%q" $v }}, +{{- end }} +} + +var futuresSymbolMap = map[string]string{ +{{- range $k, $v := .futures }} + {{ printf "%q" $k }}: {{ printf "%q" $v }}, +{{- end }} +} + +`)) + +func main() { + ctx := context.Background() + client := hyperapi.NewClient() + + spotMeta, err := client.NewSpotGetMetaRequest().Do(ctx) + if err != nil { + log.Fatal(err) + } + + spotSymbolMap := map[string]string{} + tokens := spotMeta.Tokens + for _, s := range spotMeta.Universe { + base, quote := tokens[s.Tokens[0]], tokens[s.Tokens[1]] + spotSymbolMap[base.Name+quote.Name] = base.Name + "@" + strconv.Itoa(s.Index) + } + + futuresMeta, err := client.NewFuturesGetMetaRequest().Do(ctx) + if err != nil { + log.Fatal(err) + } + + futuresSymbolMap := map[string]string{} + for i, s := range futuresMeta.Universe { + futuresSymbolMap[s.Name+hyperliquid.QuoteCurrency] = s.Name + "@" + strconv.Itoa(i) + } + + f, err := os.Create("symbols.go") + if err != nil { + log.Fatal(err) + } + + defer f.Close() + + err = packageTemplate.Execute(f, map[string]map[string]string{"spot": spotSymbolMap, "futures": futuresSymbolMap}) + if err != nil { + log.Fatal(err) + } +} diff --git a/pkg/exchange/hyperliquid/hyperapi/actions.go b/pkg/exchange/hyperliquid/hyperapi/actions.go new file mode 100644 index 0000000000..168327ccd6 --- /dev/null +++ b/pkg/exchange/hyperliquid/hyperapi/actions.go @@ -0,0 +1,38 @@ +package hyperapi + +type OrderWire struct { + Asset int `mapstructure:"a" msgpack:"a"` + IsBuy bool `mapstructure:"b" msgpack:"b"` + LimitPx string `mapstructure:"p" msgpack:"p"` + Size string `mapstructure:"s" msgpack:"s"` + ReduceOnly bool `mapstructure:"r" msgpack:"r"` + OrderType OrderWireType `mapstructure:"t" msgpack:"t"` + Cloid *string `mapstructure:"c,omitempty" msgpack:"c,omitempty"` +} + +type OrderWireType struct { + Limit *OrderWireTypeLimit `mapstructure:"limit,omitempty" msgpack:"limit,omitempty"` + Trigger *OrderWireTypeTrigger `mapstructure:"trigger,omitempty" msgpack:"trigger,omitempty"` +} + +type OrderWireTypeLimit struct { + Tif TimeInForce `mapstructure:"tif,string" msgpack:"tif"` +} + +type OrderWireTypeTrigger struct { + IsMarket bool `mapstructure:"isMarket" msgpack:"isMarket"` + TriggerPx string `mapstructure:"triggerPx" msgpack:"triggerPx"` + Tpsl Tpsl `mapstructure:"tpsl" msgpack:"tpsl"` +} + +type OrderWireBuilderInfo struct { + Builder string `mapstructure:"b" msgpack:"b"` + Fee int `mapstructure:"f" msgpack:"f"` +} + +type OrderAction struct { + Type string `mapstructure:"type" msgpack:"type"` + Orders []OrderWire `mapstructure:"orders" msgpack:"orders"` + Grouping string `mapstructure:"grouping" msgpack:"grouping"` + Builder *OrderWireBuilderInfo `mapstructure:"builder,omitempty" msgpack:"builder,omitempty"` +} diff --git a/pkg/exchange/hyperliquid/hyperapi/actions_test.go b/pkg/exchange/hyperliquid/hyperapi/actions_test.go new file mode 100644 index 0000000000..23d5d396f8 --- /dev/null +++ b/pkg/exchange/hyperliquid/hyperapi/actions_test.go @@ -0,0 +1,75 @@ +package hyperapi + +import ( + "encoding/hex" + "testing" + + "github.com/vmihailenco/msgpack/v5" +) + +// import msgpack + +// # Create order_wire dict matching the Go test case +// # Python dict preserves insertion order (Python 3.7+) +// # Order: a, b, p, s, r, t (and optionally c) +// order_wire = { +// "a": 0, +// "b": True, +// "p": "100000", +// "s": "0.001", +// "r": False, +// "t": { +// "limit": { +// "tif": "Gtc" +// } +// } +// } + +// # Serialize with msgpack +// # Use use_bin_type=False to match the behavior expected by Go +// packed = msgpack.packb(order_wire, use_bin_type=False) + +// # Convert to hex string +// pythonExpectedHex = packed.hex() +func Test_Msgpack_OrderAction(t *testing.T) { + orderTypeNew := OrderWireType{ + Limit: &OrderWireTypeLimit{ + Tif: TimeInForceGTC, + }, + } + + newOrder := OrderWire{ + Asset: 0, + IsBuy: true, + LimitPx: "100000", + Size: "0.001", + ReduceOnly: false, + OrderType: orderTypeNew, + Cloid: nil, // No cloid for this test + } + + // Serialize with msgpack + newBytes, err := msgpack.Marshal(newOrder) + if err != nil { + t.Fatalf("Failed to encode: %v", err) + } + + goHex := hex.EncodeToString(newBytes) + + // Expected output from Python SDK for equivalent order_wire + // Python code: {"a": 0, "b": True, "p": "100000", "s": "0.001", "r": False, "t": {"limit": {"tif": "Gtc"}}} + pythonExpectedHex := "86a16100a162c3a170a6313030303030a173a5302e303031a172c2a17481a56c696d697481a3746966a3477463" + + t.Logf("Go msgpack output: %s", goHex) + t.Logf("Python expected: %s", pythonExpectedHex) + + if goHex != pythonExpectedHex { + t.Errorf( + "Msgpack output does NOT match Python SDK!\nGot: %s\nExpected: %s", + goHex, + pythonExpectedHex, + ) + } else { + t.Logf("โœ“ Msgpack field ordering matches Python SDK!") + } +} diff --git a/pkg/exchange/hyperliquid/hyperapi/cancel_order_request.go b/pkg/exchange/hyperliquid/hyperapi/cancel_order_request.go new file mode 100644 index 0000000000..08f317de27 --- /dev/null +++ b/pkg/exchange/hyperliquid/hyperapi/cancel_order_request.go @@ -0,0 +1,35 @@ +package hyperapi + +import ( + "encoding/json" + + "github.com/c9s/requestgen" +) + +//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Response.Data +//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Response.Data + +type CancelResponse struct { + Statuses []json.RawMessage `json:"statuses"` +} + +type CancelOrder struct { + Asset int `json:"a"` + OrderId int `json:"o"` +} + +//go:generate PostRequest -url "/exchange" -type CancelOrderRequest -responseDataType CancelResponse +type CancelOrderRequest struct { + client requestgen.AuthenticatedAPIClient + + metaType ReqTypeInfo `param:"type" default:"cancel" validValues:"cancel"` + + cancelOrders []CancelOrder `param:"cancels,required"` +} + +func (c *Client) NewCancelOrderRequest() *CancelOrderRequest { + return &CancelOrderRequest{ + client: c, + metaType: ReqCancelOrder, + } +} diff --git a/pkg/exchange/hyperliquid/hyperapi/cancel_order_request_requestgen.go b/pkg/exchange/hyperliquid/hyperapi/cancel_order_request_requestgen.go new file mode 100644 index 0000000000..d8c9ae3c44 --- /dev/null +++ b/pkg/exchange/hyperliquid/hyperapi/cancel_order_request_requestgen.go @@ -0,0 +1,219 @@ +// Code generated by "requestgen -method POST -responseType .APIResponse -responseDataField Response.Data -url /exchange -type CancelOrderRequest -responseDataType CancelResponse"; DO NOT EDIT. + +package hyperapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +func (c *CancelOrderRequest) MetaType(metaType ReqTypeInfo) *CancelOrderRequest { + c.metaType = metaType + return c +} + +func (c *CancelOrderRequest) CancelOrders(cancelOrders []CancelOrder) *CancelOrderRequest { + c.cancelOrders = cancelOrders + return c +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (c *CancelOrderRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + if c.isVarSlice(_v) { + c.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (c *CancelOrderRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check metaType field -> json key type + metaType := c.metaType + + // TEMPLATE check-required + if len(metaType) == 0 { + metaType = "cancel" + } + // END TEMPLATE check-required + + // TEMPLATE check-valid-values + switch metaType { + case "cancel": + params["type"] = metaType + + default: + return nil, fmt.Errorf("type value %v is invalid", metaType) + + } + // END TEMPLATE check-valid-values + + // assign parameter of metaType + params["type"] = metaType + // check cancelOrders field -> json key cancels + cancelOrders := c.cancelOrders + + // TEMPLATE check-required + // END TEMPLATE check-required + + // assign parameter of cancelOrders + params["cancels"] = cancelOrders + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (c *CancelOrderRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := c.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if c.isVarSlice(_v) { + c.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (c *CancelOrderRequest) GetParametersJSON() ([]byte, error) { + params, err := c.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (c *CancelOrderRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (c *CancelOrderRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (c *CancelOrderRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (c *CancelOrderRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (c *CancelOrderRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := c.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +// GetPath returns the request path of the API +func (c *CancelOrderRequest) GetPath() string { + return "/exchange" +} + +// Do generates the request object and send the request object to the API endpoint +func (c *CancelOrderRequest) Do(ctx context.Context) (*CancelResponse, error) { + + params, err := c.GetParameters() + if err != nil { + return nil, err + } + query := url.Values{} + + var apiURL string + + apiURL = c.GetPath() + + req, err := c.client.NewAuthenticatedRequest(ctx, "POST", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := c.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse APIResponse + + type responseUnmarshaler interface { + Unmarshal(data []byte) error + } + + if unmarshaler, ok := interface{}(&apiResponse).(responseUnmarshaler); ok { + if err := unmarshaler.Unmarshal(response.Body); err != nil { + return nil, err + } + } else { + // The line below checks the content type, however, some API server might not send the correct content type header, + // Hence, this is commented for backward compatibility + // response.IsJSON() + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + } + + type responseValidator interface { + Validate() error + } + + if validator, ok := interface{}(&apiResponse).(responseValidator); ok { + if err := validator.Validate(); err != nil { + return nil, err + } + } + var data CancelResponse + if err := json.Unmarshal(apiResponse.Response.Data, &data); err != nil { + return nil, err + } + return &data, nil +} diff --git a/pkg/exchange/hyperliquid/hyperapi/client.go b/pkg/exchange/hyperliquid/hyperapi/client.go new file mode 100644 index 0000000000..0c5afaf931 --- /dev/null +++ b/pkg/exchange/hyperliquid/hyperapi/client.go @@ -0,0 +1,323 @@ +package hyperapi + +import ( + "bytes" + "context" + "crypto/ecdsa" + "encoding/binary" + "encoding/json" + "errors" + "fmt" + "math/big" + "net/http" + "net/url" + "time" + + "github.com/c9s/bbgo/pkg/nonce" + "github.com/c9s/requestgen" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/signer/core/apitypes" + "github.com/mitchellh/mapstructure" + "github.com/vmihailenco/msgpack/v5" +) + +const ( + defaultHTTPTimeout = 15 * time.Second + ProductionURL = "https://api.hyperliquid.xyz" + TestNetURL = "https://api.hyperliquid-testnet.xyz" +) + +var ( + ErrInvalidSignature = errors.New("invalid signature") + ErrInvalidAccountAddress = errors.New("invalid account address") + TestNet = false +) + +type Client struct { + requestgen.BaseAPIClient + + apiSecret, account string + vaultAddress string + privateKey *ecdsa.PrivateKey + + nonce *nonce.MillisecondNonce +} + +func NewClient() *Client { + u, err := url.Parse(getAPIEndpoint()) + if err != nil { + panic(err) + } + + return &Client{ + BaseAPIClient: requestgen.BaseAPIClient{ + BaseURL: u, + HttpClient: &http.Client{ + Timeout: defaultHTTPTimeout, + }, + }, + nonce: nonce.NewMillisecondNonce(time.Now()), + } +} + +func (c *Client) Auth(secret, account string) { + c.apiSecret = secret + + privateKey, err := crypto.HexToECDSA(c.apiSecret) + if err != nil { + panic(err) + } + c.privateKey = privateKey + + if !common.IsHexAddress(account) { + panic(fmt.Errorf("%w: %s", ErrInvalidAccountAddress, account)) + } + c.account = account +} + +func (c *Client) SetVaultAddress(address string) { + c.vaultAddress = address +} + +// NewRequest create new API request. Relative url can be provided in refURL. +func (c *Client) NewRequest( + ctx context.Context, method, refPath string, params url.Values, payload interface{}, +) (*http.Request, error) { + req, err := c.BaseAPIClient.NewRequest(ctx, method, refPath, params, payload) + if req != nil { + req.Header.Set("Content-Type", "application/json") + req.Header.Add("Accept", "application/json") + } + + return req, err +} + +// NewAuthenticatedRequest creates new http request for authenticated routes. +func (c *Client) NewAuthenticatedRequest( + ctx context.Context, method, refURL string, params url.Values, payload interface{}, +) (*http.Request, error) { + body, err := c.buildPayload(payload, c.vaultAddress, c.nonce.GetInt64()) + if err != nil { + return nil, err + } + rel, err := url.Parse(refURL) + if err != nil { + return nil, err + } + + pathURL := c.BaseURL.ResolveReference(rel) + rawQuery := params.Encode() + if rawQuery != "" { + pathURL.RawQuery = rawQuery + } + + req, err := http.NewRequestWithContext(ctx, method, pathURL.String(), bytes.NewReader(body)) + if err != nil { + return nil, err + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Add("Accept", "application/json") + return req, nil +} + +func (c *Client) SignL1Action(action any, nonce int64, expiresAfter *int64) (SignatureResult, error) { + hash, err := c.buildActionHash(action, uint64(nonce), c.vaultAddress, expiresAfter) + if err != nil { + return SignatureResult{}, err + } + + phantomAgent := c.PhantomAgent(hash) + chainId := math.HexOrDecimal256(*big.NewInt(1337)) + return c.sign(apitypes.TypedData{ + Domain: apitypes.TypedDataDomain{ + ChainId: &chainId, + Name: "Exchange", + Version: "1", + VerifyingContract: "0x0000000000000000000000000000000000000000", + }, + Types: apitypes.Types{ + "Agent": []apitypes.Type{ + {Name: "source", Type: "string"}, + {Name: "connectionId", Type: "bytes32"}, + }, + "EIP712Domain": []apitypes.Type{ + {Name: "name", Type: "string"}, + {Name: "version", Type: "string"}, + {Name: "chainId", Type: "uint256"}, + {Name: "verifyingContract", Type: "address"}, + }, + }, + PrimaryType: "Agent", + Message: phantomAgent, + }, c.privateKey) +} + +func (c *Client) PhantomAgent(hash []byte) map[string]any { + source := "b" // testnet + if !TestNet { + source = "a" // mainnet + } + + return map[string]any{ + "source": source, + "connectionId": hash, + } +} + +func (c *Client) UserAddress() string { + return c.account +} + +func (c *Client) sign(typedData apitypes.TypedData, privateKey *ecdsa.PrivateKey) (SignatureResult, error) { + // Create EIP-712 hash + hash, _, err := apitypes.TypedDataAndHash(typedData) + if err != nil { + return SignatureResult{}, fmt.Errorf("failed to hash typed data: %w", err) + } + + signature, err := crypto.Sign(hash, privateKey) + if err != nil { + return SignatureResult{}, fmt.Errorf("failed to sign message: %w", err) + } + + // Extract r, s, v components + r := signature[:32] + s := signature[32:64] + v := int(signature[64]) + 27 + + return SignatureResult{ + R: hexutil.Encode(r), + S: hexutil.Encode(s), + V: v, + }, nil +} + +// buildActionData constructs the data for action hashing +func (c *Client) buildActionHash(action any, nonce uint64, vaultAddress string, expiresAfter *int64) ([]byte, error) { + data, err := c.convertToAction(action) + if err != nil { + return nil, err + } + + // Append nonce + data = appendUint64(data, nonce) + + // Append vault address flag and address if present + if vaultAddress == "" { + data = append(data, 0x00) + } else { + data = append(data, 0x01) + data = append(data, common.HexToAddress(vaultAddress).Bytes()...) + } + + // Append expiration if provided + if expiresAfter != nil { + if *expiresAfter < 0 { + return nil, fmt.Errorf("expiresAfter cannot be negative: %d", *expiresAfter) + } + data = append(data, 0x00) + data = appendUint64(data, uint64(*expiresAfter)) + } + + return crypto.Keccak256(data), nil +} + +func (c *Client) convertToAction(action any) ([]byte, error) { + // Convert action to map[string]any + actionMap, ok := action.(map[string]any) + if !ok { + return nil, fmt.Errorf("action is not a map[string]any") + } + + actionType, ok := actionMap["type"] + if !ok { + return nil, fmt.Errorf("action missing required 'type' field") + } + + switch actionType { + case ReqSubmitOrder: + var orderAction OrderAction + return encodeActionToMsgpack(actionMap, &orderAction) + // Add more action types here as needed + // case "usdClassTransfer": + // var transferAction TransferAction + // return encodeActionToMsgpack(actionMap, &transferAction) + default: + return nil, fmt.Errorf("action type %v is not supported", actionType) + } +} + +func (c *Client) buildPayload(action any, vaultAddress string, nonce int64) ([]byte, error) { + signature, err := c.SignL1Action(action, nonce, nil) + if err != nil { + return nil, err + } + + // Marshal action to JSON + payload := map[string]any{ + "action": action, + "nonce": nonce, + "signature": signature, + } + + if vaultAddress != "" { + // Handle vault address based on action type + if actionMap, ok := action.(map[string]any); ok { + if actionMap["type"] != "usdClassTransfer" { + payload["vaultAddress"] = vaultAddress + } else { + payload["vaultAddress"] = nil + } + } else { + // For struct types, we need to use reflection or type assertion + // For now, assume it's not usdClassTransfer + payload["vaultAddress"] = vaultAddress + } + } + + return json.Marshal(payload) +} + +// encodeActionToMsgpack is a generic helper function that normalizes and encodes +// an action map to msgpack format using the provided target type. +func encodeActionToMsgpack(actionMap map[string]any, target any) ([]byte, error) { + // Normalize nested map structures through JSON serialization/deserialization + // This ensures mapstructure can correctly decode nested map[string]any structures + data, err := json.Marshal(actionMap) + if err != nil { + return nil, fmt.Errorf("failed to marshal action map: %w", err) + } + + var normalizedMap map[string]any + if err := json.Unmarshal(data, &normalizedMap); err != nil { + return nil, fmt.Errorf("failed to unmarshal normalized action map: %w", err) + } + + if err := mapstructure.Decode(normalizedMap, target); err != nil { + return nil, fmt.Errorf("failed to decode action: %w", err) + } + + data, err = msgpack.Marshal(target) + if err != nil { + return nil, fmt.Errorf("failed to marshal action to msgpack: %w", err) + } + return data, nil +} + +// appendUint64 appends a uint64 as 8 bytes in big-endian format +func appendUint64(data []byte, value uint64) []byte { + bytes := make([]byte, 8) + binary.BigEndian.PutUint64(bytes, value) + return append(data, bytes...) +} + +func getAPIEndpoint() string { + if TestNet { + return TestNetURL + } + return ProductionURL +} diff --git a/pkg/exchange/hyperliquid/hyperapi/client_test.go b/pkg/exchange/hyperliquid/hyperapi/client_test.go new file mode 100644 index 0000000000..7736926b51 --- /dev/null +++ b/pkg/exchange/hyperliquid/hyperapi/client_test.go @@ -0,0 +1,527 @@ +package hyperapi + +import ( + "context" + "encoding/binary" + "math/big" + "net/url" + "testing" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/signer/core/apitypes" + "github.com/vmihailenco/msgpack/v5" +) + +func TestNewClient(t *testing.T) { + // Test default client + client := NewClient() + if client == nil { + t.Error("NewClient should return a valid client") + } +} + +func TestBuildActionData(t *testing.T) { + client := NewClient() + + action := map[string]interface{}{ + "type": ReqSubmitOrder, + "data": "test", + } + + hash, err := client.buildActionHash(action, 12345, "", nil) + if err != nil { + t.Errorf("buildActionData should not return error: %v", err) + } + if len(hash) == 0 { + t.Error("buildActionData should return non-empty data") + } +} + +func TestPhantomAgent(t *testing.T) { + // Test production client phantom agent + originalTestNet := TestNet + TestNet = false + defer func() { TestNet = originalTestNet }() + + client := NewClient() + agent := client.PhantomAgent([]byte("test")) + if agent["source"] != "a" { + t.Errorf("Expected production source 'a', got %s", agent["source"]) + } + + // Test testnet client phantom agent + TestNet = true + client = NewClient() + agent = client.PhantomAgent([]byte("test")) + if agent["source"] != "b" { + t.Errorf("Expected testnet source 'b', got %s", agent["source"]) + } +} + +func TestAuth(t *testing.T) { + client := NewClient() + + // Test with valid private key + validPrivateKey, _ := crypto.GenerateKey() + validPrivateKeyHex := hexutil.Encode(crypto.FromECDSA(validPrivateKey))[2:] // Remove 0x prefix + account := crypto.PubkeyToAddress(validPrivateKey.PublicKey).Hex() + + // Should not panic with valid key + func() { + defer func() { + if r := recover(); r != nil { + t.Errorf("Auth should not panic with valid private key: %v", r) + } + }() + client.Auth(validPrivateKeyHex, account) + }() + + // Verify private key was set + if client.privateKey == nil { + t.Error("Private key should be set after Auth") + } + + // Test with invalid private key + client2 := NewClient() + func() { + defer func() { + if r := recover(); r == nil { + t.Error("Auth should panic with invalid private key") + } + }() + client2.Auth("invalid_private_key", "") + }() +} + +func TestSetVaultAddress(t *testing.T) { + client := NewClient() + + // Test setting vault address + vaultAddr := "0x1234567890123456789012345678901234567890" + client.SetVaultAddress(vaultAddr) + + if client.vaultAddress != vaultAddr { + t.Errorf("Expected vault address %s, got %s", vaultAddr, client.vaultAddress) + } +} + +func TestSingL1Action(t *testing.T) { + client := NewClient() + + // Generate a valid private key for testing + privateKey, _ := crypto.GenerateKey() + privateKeyHex := hexutil.Encode(crypto.FromECDSA(privateKey))[2:] // Remove 0x prefix + account := crypto.PubkeyToAddress(privateKey.PublicKey).Hex() + client.Auth(privateKeyHex, account) + + action := map[string]interface{}{ + "type": ReqSubmitOrder, + "data": "test", + } + + // Test signing without expiration + signature, err := client.SignL1Action(action, 12345, nil) + if err != nil { + t.Errorf("SignL1Action should not return error: %v", err) + } + + // Verify signature components + if signature.R == "" || signature.S == "" || signature.V == 0 { + t.Error("Signature should have valid R, S, V components") + } + + // Test signing with expiration + expiresAfter := int64(3600) + signature2, err := client.SignL1Action(action, 12345, &expiresAfter) + if err != nil { + t.Errorf("SignL1Action with expiration should not return error: %v", err) + } + + // Signatures should be different + if signature.R == signature2.R && signature.S == signature2.S { + t.Error("Signatures with different expiration should be different") + } +} + +func TestSign(t *testing.T) { + client := NewClient() + + // Generate a valid private key for testing + privateKey, _ := crypto.GenerateKey() + privateKeyHex := hexutil.Encode(crypto.FromECDSA(privateKey))[2:] // Remove 0x prefix + account := crypto.PubkeyToAddress(privateKey.PublicKey).Hex() + client.Auth(privateKeyHex, account) + + // Create test typed data + typedData := apitypes.TypedData{ + Domain: apitypes.TypedDataDomain{ + Name: "Test", + Version: "1", + ChainId: (*math.HexOrDecimal256)(big.NewInt(1)), + VerifyingContract: "0x0000000000000000000000000000000000000000", + }, + Types: apitypes.Types{ + "Test": []apitypes.Type{ + {Name: "test", Type: "string"}, + }, + "EIP712Domain": []apitypes.Type{ + {Name: "name", Type: "string"}, + {Name: "version", Type: "string"}, + {Name: "chainId", Type: "uint256"}, + {Name: "verifyingContract", Type: "address"}, + }, + }, + PrimaryType: "Test", + Message: map[string]interface{}{ + "test": "value", + }, + } + + signature, err := client.sign(typedData, privateKey) + if err != nil { + t.Errorf("sign should not return error: %v", err) + } + + // Verify signature components + if signature.R == "" || signature.S == "" || signature.V == 0 { + t.Error("Signature should have valid R, S, V components") + } +} + +func TestBuildActionDataWithVault(t *testing.T) { + client := NewClient() + client.SetVaultAddress("0x1234567890123456789012345678901234567890") + + action := map[string]interface{}{ + "type": ReqSubmitOrder, + "data": "test", + } + + // Test with vault address + hash, err := client.buildActionHash(action, 12345, "", nil) + if err != nil { + t.Errorf("buildActionData with vault should not return error: %v", err) + } + if len(hash) == 0 { + t.Error("buildActionData should return non-empty data") + } + + // Test with expiration + expiresAfter := int64(3600) + hash2, err := client.buildActionHash(action, 12345, "", &expiresAfter) + if err != nil { + t.Errorf("buildActionData with expiration should not return error: %v", err) + } + if len(hash2) == 0 { + t.Error("buildActionData with expiration should return non-empty data") + } + + // Data with expiration should be different + if string(hash) == string(hash2) { + t.Error("Data with expiration should be different from data without expiration") + } +} + +func TestNewAuthenticatedRequest(t *testing.T) { + client := NewClient() + + // Generate a valid private key for testing + privateKey, _ := crypto.GenerateKey() + privateKeyHex := hexutil.Encode(crypto.FromECDSA(privateKey))[2:] // Remove 0x prefix + account := crypto.PubkeyToAddress(privateKey.PublicKey).Hex() + client.Auth(privateKeyHex, account) + + action := map[string]interface{}{ + "type": ReqSubmitOrder, + "data": "test", + } + + ctx := context.Background() + params := url.Values{} + params.Set("test", "value") + + req, err := client.NewAuthenticatedRequest(ctx, "POST", "/test", params, action) + if err != nil { + t.Errorf("NewAuthenticatedRequest should not return error: %v", err) + } + + if req == nil { + t.Error("NewAuthenticatedRequest should return a valid request") + return + } + + // Verify request headers + if req.Header.Get("Content-Type") != "application/json" { + t.Error("Request should have correct Content-Type header") + } + if req.Header.Get("Accept") != "application/json" { + t.Error("Request should have correct Accept header") + } +} + +func TestBuildPayload(t *testing.T) { + client := NewClient() + + // Generate a valid private key for testing + privateKey, _ := crypto.GenerateKey() + privateKeyHex := hexutil.Encode(crypto.FromECDSA(privateKey))[2:] // Remove 0x prefix + account := crypto.PubkeyToAddress(privateKey.PublicKey).Hex() + client.Auth(privateKeyHex, account) + + // Test with regular action + action := map[string]interface{}{ + "type": ReqSubmitOrder, + "data": "test", + } + + payload, err := client.buildPayload(action, client.vaultAddress, client.nonce.GetInt64()) + if err != nil { + t.Errorf("castPayload should not return error: %v", err) + } + + if len(payload) == 0 { + t.Error("castPayload should return non-empty payload") + } + + // TODO add test case + // Test with usdClassTransfer action (should not include vault address) + // client.SetVaultAddress("0x1234567890123456789012345678901234567890") + // usdTransferAction := map[string]interface{}{ + // "type": "usdClassTransfer", + // "data": "test", + // } + + // payload2, err := client.buildPayload(usdTransferAction, client.vaultAddress, client.nonce.GetInt64()) + // if err != nil { + // t.Errorf("castPayload with usdClassTransfer should not return error: %v", err) + // } + + // if len(payload2) == 0 { + // t.Error("castPayload with usdClassTransfer should return non-empty payload") + // } +} + +func TestAppendUint64(t *testing.T) { + // Test appendUint64 function + data := []byte("test") + value := uint64(12345) + + result := appendUint64(data, value) + + // Should be 8 bytes longer + if len(result) != len(data)+8 { + t.Errorf("Expected length %d, got %d", len(data)+8, len(result)) + } + + // Verify the appended value + appendedValue := binary.BigEndian.Uint64(result[len(data):]) + if appendedValue != value { + t.Errorf("Expected value %d, got %d", value, appendedValue) + } +} + +func TestGetAPIEndpoint(t *testing.T) { + // Test production endpoint + originalTestNet := TestNet + TestNet = false + defer func() { TestNet = originalTestNet }() + + endpoint := getAPIEndpoint() + if endpoint != ProductionURL { + t.Errorf("Expected production URL %s, got %s", ProductionURL, endpoint) + } + + // Test testnet endpoint + TestNet = true + endpoint = getAPIEndpoint() + if endpoint != TestNetURL { + t.Errorf("Expected testnet URL %s, got %s", TestNetURL, endpoint) + } +} + +func TestConvertToAction(t *testing.T) { + client := NewClient() + + t.Run("OrderAction", func(t *testing.T) { + // Test order action conversion + action := map[string]any{ + "type": ReqSubmitOrder, + "orders": []map[string]any{ + { + "a": 0, + "b": true, + "p": "100000", + "s": "0.001", + "r": false, + "t": map[string]any{ + "limit": map[string]any{ + "tif": "Gtc", + }, + }, + }, + }, + "grouping": "na", + } + + packed, err := client.convertToAction(action) + if err != nil { + t.Fatalf("convertToAction should not return error: %v", err) + } + + if len(packed) == 0 { + t.Error("convertToAction should return non-empty packed data") + } + + // Verify it's valid msgpack + var decoded OrderAction + err = msgpack.Unmarshal(packed, &decoded) + if err != nil { + t.Fatalf("Failed to unmarshal packed data: %v", err) + } + + if decoded.Type != "order" { + t.Errorf("Expected type 'order', got %s", decoded.Type) + } + + if len(decoded.Orders) != 1 { + t.Errorf("Expected 1 order, got %d", len(decoded.Orders)) + } + + if decoded.Orders[0].Asset != 0 { + t.Errorf("Expected asset 0, got %d", decoded.Orders[0].Asset) + } + + if !decoded.Orders[0].IsBuy { + t.Error("Expected IsBuy to be true") + } + + if decoded.Orders[0].LimitPx != "100000" { + t.Errorf("Expected LimitPx '100000', got %s", decoded.Orders[0].LimitPx) + } + }) + + t.Run("UnsupportedActionType", func(t *testing.T) { + action := map[string]any{ + "type": "unsupported", + } + + _, err := client.convertToAction(action) + if err == nil { + t.Error("convertToAction should return error for unsupported action type") + } + }) + + t.Run("InvalidActionType", func(t *testing.T) { + // Test with non-map action + _, err := client.convertToAction("not a map") + if err == nil { + t.Error("convertToAction should return error for non-map action") + } + }) +} + +// TestSignatureAddressRecovery verifies that the address recovered from signature matches the original address +func TestSignatureAddressRecovery(t *testing.T) { + client := NewClient() + + // Generate a valid private key for testing + privateKey, err := crypto.GenerateKey() + if err != nil { + t.Fatalf("Failed to generate private key: %v", err) + } + + privateKeyHex := hexutil.Encode(crypto.FromECDSA(privateKey))[2:] // Remove 0x prefix + account := crypto.PubkeyToAddress(privateKey.PublicKey).Hex() + + client.Auth(privateKeyHex, account) + + // Get the original address from the private key + originalAddress := client.UserAddress() + if originalAddress == "" { + t.Fatal("UserAddress should return a valid address") + } + + // Create a test action + action := map[string]any{ + "type": ReqSubmitOrder, + "orders": []map[string]any{ + { + "a": 0, + "b": true, + "p": "100000", + "s": "0.001", + "r": false, + "t": map[string]any{ + "limit": map[string]any{ + "tif": "Gtc", + }, + }, + }, + }, + "grouping": "na", + } + + nonce := int64(1234567890) + + // Sign the action + signature, err := client.SignL1Action(action, nonce, nil) + if err != nil { + t.Fatalf("SignL1Action should not return error: %v", err) + } + + // Rebuild the action data to get the hash (same as in SignL1Action) + hash, err := client.buildActionHash(action, uint64(nonce), client.vaultAddress, nil) + if err != nil { + t.Fatalf("buildActionData should not return error: %v", err) + } + + // Rebuild the PhantomAgent (same as in SignL1Action) + phantomAgent := client.PhantomAgent(hash) + chainId := math.HexOrDecimal256(*big.NewInt(1337)) + + // Rebuild the typed data (same as in SignL1Action) + typedData := apitypes.TypedData{ + Domain: apitypes.TypedDataDomain{ + ChainId: &chainId, + Name: "Exchange", + Version: "1", + VerifyingContract: "0x0000000000000000000000000000000000000000", + }, + Types: apitypes.Types{ + "Agent": []apitypes.Type{ + {Name: "source", Type: "string"}, + {Name: "connectionId", Type: "bytes32"}, + }, + "EIP712Domain": []apitypes.Type{ + {Name: "name", Type: "string"}, + {Name: "version", Type: "string"}, + {Name: "chainId", Type: "uint256"}, + {Name: "verifyingContract", Type: "address"}, + }, + }, + PrimaryType: "Agent", + Message: phantomAgent, + } + + recoverHash, _, err := apitypes.TypedDataAndHash(typedData) + if err != nil { + t.Fatalf("TypedDataAndHash should not return error: %v", err) + } + + sigBytes := make([]byte, 65) + copy(sigBytes[:32], hexutil.MustDecode(signature.R)) + copy(sigBytes[32:64], hexutil.MustDecode(signature.S)) + sigBytes[64] = byte(signature.V - 27) + + pub, _ := crypto.SigToPub(recoverHash, sigBytes) + recoveredAddress := crypto.PubkeyToAddress(*pub).Hex() + + // Verify that recovered address matches original address + if recoveredAddress != originalAddress { + t.Errorf("Recovered address %s does not match original address %s", recoveredAddress, originalAddress) + } else { + t.Logf("Successfully verified: recovered address %s matches original address %s", recoveredAddress, originalAddress) + } +} diff --git a/pkg/exchange/hyperliquid/hyperapi/futures_get_account_balance_request.go b/pkg/exchange/hyperliquid/hyperapi/futures_get_account_balance_request.go new file mode 100644 index 0000000000..a4e625979f --- /dev/null +++ b/pkg/exchange/hyperliquid/hyperapi/futures_get_account_balance_request.go @@ -0,0 +1,69 @@ +package hyperapi + +import ( + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/requestgen" +) + +type FuturesAccount struct { + AssetPositions []FuturesAssetPosition `json:"assetPositions"` + CrossMaintenanceMarginUsed fixedpoint.Value `json:"crossMaintenanceMarginUsed"` + CrossMarginSummary FuturesMarginSummary `json:"crossMarginSummary"` + MarginSummary FuturesMarginSummary `json:"marginSummary"` + Time int64 `json:"time"` + Withdrawable fixedpoint.Value `json:"withdrawable"` +} + +type FuturesAssetPosition struct { + Position FuturesPosition `json:"position"` + Type string `json:"type"` +} + +type FuturesPosition struct { + Coin string `json:"coin"` + CumFunding FuturesCumFunding `json:"cumFunding"` + EntryPx fixedpoint.Value `json:"entryPx"` + Leverage FuturesLeverage `json:"leverage"` + LiquidationPx fixedpoint.Value `json:"liquidationPx"` + MarginUsed fixedpoint.Value `json:"marginUsed"` + MaxLeverage int `json:"maxLeverage"` + PositionValue fixedpoint.Value `json:"positionValue"` + ReturnOnEquity fixedpoint.Value `json:"returnOnEquity"` + Szi fixedpoint.Value `json:"szi"` + UnrealizedPnl fixedpoint.Value `json:"unrealizedPnl"` +} + +type FuturesCumFunding struct { + AllTime fixedpoint.Value `json:"allTime"` + SinceChange fixedpoint.Value `json:"sinceChange"` + SinceOpen fixedpoint.Value `json:"sinceOpen"` +} + +type FuturesLeverage struct { + RawUsd fixedpoint.Value `json:"rawUsd"` + Type string `json:"type"` + Value fixedpoint.Value `json:"value"` +} + +type FuturesMarginSummary struct { + AccountValue fixedpoint.Value `json:"accountValue"` + TotalMarginUsed fixedpoint.Value `json:"totalMarginUsed"` + TotalNtlPos fixedpoint.Value `json:"totalNtlPos"` + TotalRawUsd fixedpoint.Value `json:"totalRawUsd"` +} + +//go:generate requestgen -method POST -url "/info" -type FuturesGetAccountBalanceRequest -responseType FuturesAccount +type FuturesGetAccountBalanceRequest struct { + client requestgen.APIClient + + user string `param:"user,required"` + dex *string `param:"dex"` + metaType ReqTypeInfo `param:"type" default:"clearinghouseState" validValues:"clearinghouseState"` +} + +func (c *Client) NewFuturesGetAccountBalanceRequest() *FuturesGetAccountBalanceRequest { + return &FuturesGetAccountBalanceRequest{ + client: c, + metaType: ReqFuturesClearinghouseState, + } +} diff --git a/pkg/exchange/hyperliquid/hyperapi/futures_get_account_balance_request_requestgen.go b/pkg/exchange/hyperliquid/hyperapi/futures_get_account_balance_request_requestgen.go new file mode 100644 index 0000000000..f341a1acd0 --- /dev/null +++ b/pkg/exchange/hyperliquid/hyperapi/futures_get_account_balance_request_requestgen.go @@ -0,0 +1,236 @@ +// Code generated by "requestgen -method POST -url /info -type FuturesGetAccountBalanceRequest -responseType FuturesAccount"; DO NOT EDIT. + +package hyperapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +func (f *FuturesGetAccountBalanceRequest) User(user string) *FuturesGetAccountBalanceRequest { + f.user = user + return f +} + +func (f *FuturesGetAccountBalanceRequest) Dex(dex string) *FuturesGetAccountBalanceRequest { + f.dex = &dex + return f +} + +func (f *FuturesGetAccountBalanceRequest) MetaType(metaType ReqTypeInfo) *FuturesGetAccountBalanceRequest { + f.metaType = metaType + return f +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (f *FuturesGetAccountBalanceRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + if f.isVarSlice(_v) { + f.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (f *FuturesGetAccountBalanceRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check user field -> json key user + user := f.user + + // TEMPLATE check-required + if len(user) == 0 { + return nil, fmt.Errorf("user is required, empty string given") + } + // END TEMPLATE check-required + + // assign parameter of user + params["user"] = user + // check dex field -> json key dex + if f.dex != nil { + dex := *f.dex + + // TEMPLATE check-required + if len(dex) == 0 { + } + // END TEMPLATE check-required + + // assign parameter of dex + params["dex"] = dex + } else { + } + // check metaType field -> json key type + metaType := f.metaType + + // TEMPLATE check-required + if len(metaType) == 0 { + metaType = "clearinghouseState" + } + // END TEMPLATE check-required + + // TEMPLATE check-valid-values + switch metaType { + case "clearinghouseState": + params["type"] = metaType + + default: + return nil, fmt.Errorf("type value %v is invalid", metaType) + + } + // END TEMPLATE check-valid-values + + // assign parameter of metaType + params["type"] = metaType + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (f *FuturesGetAccountBalanceRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := f.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if f.isVarSlice(_v) { + f.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (f *FuturesGetAccountBalanceRequest) GetParametersJSON() ([]byte, error) { + params, err := f.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (f *FuturesGetAccountBalanceRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (f *FuturesGetAccountBalanceRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (f *FuturesGetAccountBalanceRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (f *FuturesGetAccountBalanceRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (f *FuturesGetAccountBalanceRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := f.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +// GetPath returns the request path of the API +func (f *FuturesGetAccountBalanceRequest) GetPath() string { + return "/info" +} + +// Do generates the request object and send the request object to the API endpoint +func (f *FuturesGetAccountBalanceRequest) Do(ctx context.Context) (*FuturesAccount, error) { + + params, err := f.GetParameters() + if err != nil { + return nil, err + } + query := url.Values{} + + var apiURL string + + apiURL = f.GetPath() + + req, err := f.client.NewRequest(ctx, "POST", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := f.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse FuturesAccount + + type responseUnmarshaler interface { + Unmarshal(data []byte) error + } + + if unmarshaler, ok := interface{}(&apiResponse).(responseUnmarshaler); ok { + if err := unmarshaler.Unmarshal(response.Body); err != nil { + return nil, err + } + } else { + // The line below checks the content type, however, some API server might not send the correct content type header, + // Hence, this is commented for backward compatibility + // response.IsJSON() + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + } + + type responseValidator interface { + Validate() error + } + + if validator, ok := interface{}(&apiResponse).(responseValidator); ok { + if err := validator.Validate(); err != nil { + return nil, err + } + } + return &apiResponse, nil +} diff --git a/pkg/exchange/hyperliquid/hyperapi/futures_get_meta_and_asset_ctxs_request copy.go b/pkg/exchange/hyperliquid/hyperapi/futures_get_meta_and_asset_ctxs_request copy.go new file mode 100644 index 0000000000..635cc7e64c --- /dev/null +++ b/pkg/exchange/hyperliquid/hyperapi/futures_get_meta_and_asset_ctxs_request copy.go @@ -0,0 +1,61 @@ +package hyperapi + +import ( + "encoding/json" + "fmt" + + "github.com/c9s/requestgen" +) + +type FuturesMetaAndAssetCtxsResponse struct { + Meta FuturesGetMetaResponse + AssetCtxs []FuturesAssetContext +} + +type FuturesAssetContext struct { + DayNotionalVolume string `json:"dayNtlVlm"` + Funding string `json:"funding"` + ImpactPrices [2]string `json:"impactPxs"` + MarkPrice string `json:"markPx"` + MidPrice string `json:"midPx"` + OpenInterest string `json:"openInterest"` + OraclePrice string `json:"oraclePx"` + Premium string `json:"premium"` + PrevDayPrice string `json:"prevDayPx"` +} + +func (r *FuturesMetaAndAssetCtxsResponse) UnmarshalJSON(data []byte) error { + var payload []json.RawMessage + if err := json.Unmarshal(data, &payload); err != nil { + return err + } + + if len(payload) != 2 { + return fmt.Errorf("unexpected metaAndAssetCtxs payload length %d", len(payload)) + } + + if err := json.Unmarshal(payload[0], &r.Meta); err != nil { + return fmt.Errorf("unmarshal meta block: %w", err) + } + + if err := json.Unmarshal(payload[1], &r.AssetCtxs); err != nil { + return fmt.Errorf("unmarshal asset contexts: %w", err) + } + + return nil +} + +//go:generate requestgen -method POST -url "/info" -type FuturesGetMetaAndAssetCtxsRequest -responseType FuturesMetaAndAssetCtxsResponse + +type FuturesGetMetaAndAssetCtxsRequest struct { + client requestgen.APIClient + + metaType ReqTypeInfo `param:"type" default:"metaAndAssetCtxs" validValues:"metaAndAssetCtxs"` +} + +func (c *Client) NewFuturesGetMetaAndAssetCtxsRequest() *FuturesGetMetaAndAssetCtxsRequest { + return &FuturesGetMetaAndAssetCtxsRequest{ + client: c, + metaType: ReqFuturesMetaAndAssetCtxs, + } +} diff --git a/pkg/exchange/hyperliquid/hyperapi/futures_get_meta_and_asset_ctxs_request_requestgen.go b/pkg/exchange/hyperliquid/hyperapi/futures_get_meta_and_asset_ctxs_request_requestgen.go new file mode 100644 index 0000000000..7d1e031d04 --- /dev/null +++ b/pkg/exchange/hyperliquid/hyperapi/futures_get_meta_and_asset_ctxs_request_requestgen.go @@ -0,0 +1,202 @@ +// Code generated by "requestgen -method POST -url /info -type FuturesGetMetaAndAssetCtxsRequest -responseType FuturesMetaAndAssetCtxsResponse"; DO NOT EDIT. + +package hyperapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +func (f *FuturesGetMetaAndAssetCtxsRequest) MetaType(metaType ReqTypeInfo) *FuturesGetMetaAndAssetCtxsRequest { + f.metaType = metaType + return f +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (f *FuturesGetMetaAndAssetCtxsRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + if f.isVarSlice(_v) { + f.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (f *FuturesGetMetaAndAssetCtxsRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check metaType field -> json key type + metaType := f.metaType + + // TEMPLATE check-required + if len(metaType) == 0 { + metaType = "metaAndAssetCtxs" + } + // END TEMPLATE check-required + + // TEMPLATE check-valid-values + switch metaType { + case "metaAndAssetCtxs": + params["type"] = metaType + + default: + return nil, fmt.Errorf("type value %v is invalid", metaType) + + } + // END TEMPLATE check-valid-values + + // assign parameter of metaType + params["type"] = metaType + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (f *FuturesGetMetaAndAssetCtxsRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := f.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if f.isVarSlice(_v) { + f.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (f *FuturesGetMetaAndAssetCtxsRequest) GetParametersJSON() ([]byte, error) { + params, err := f.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (f *FuturesGetMetaAndAssetCtxsRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (f *FuturesGetMetaAndAssetCtxsRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (f *FuturesGetMetaAndAssetCtxsRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (f *FuturesGetMetaAndAssetCtxsRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (f *FuturesGetMetaAndAssetCtxsRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := f.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +// GetPath returns the request path of the API +func (f *FuturesGetMetaAndAssetCtxsRequest) GetPath() string { + return "/info" +} + +// Do generates the request object and send the request object to the API endpoint +func (f *FuturesGetMetaAndAssetCtxsRequest) Do(ctx context.Context) (*FuturesMetaAndAssetCtxsResponse, error) { + + params, err := f.GetParameters() + if err != nil { + return nil, err + } + query := url.Values{} + + var apiURL string + + apiURL = f.GetPath() + + req, err := f.client.NewRequest(ctx, "POST", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := f.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse FuturesMetaAndAssetCtxsResponse + + type responseUnmarshaler interface { + Unmarshal(data []byte) error + } + + if unmarshaler, ok := interface{}(&apiResponse).(responseUnmarshaler); ok { + if err := unmarshaler.Unmarshal(response.Body); err != nil { + return nil, err + } + } else { + // The line below checks the content type, however, some API server might not send the correct content type header, + // Hence, this is commented for backward compatibility + // response.IsJSON() + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + } + + type responseValidator interface { + Validate() error + } + + if validator, ok := interface{}(&apiResponse).(responseValidator); ok { + if err := validator.Validate(); err != nil { + return nil, err + } + } + return &apiResponse, nil +} diff --git a/pkg/exchange/hyperliquid/hyperapi/futures_get_meta_request.go b/pkg/exchange/hyperliquid/hyperapi/futures_get_meta_request.go new file mode 100644 index 0000000000..5bf49022f5 --- /dev/null +++ b/pkg/exchange/hyperliquid/hyperapi/futures_get_meta_request.go @@ -0,0 +1,31 @@ +package hyperapi + +import ( + "github.com/c9s/requestgen" +) + +type FuturesGetMetaResponse struct { + Universe []PerpMeta `json:"universe"` // universe +} + +type PerpMeta struct { + Name string `json:"name"` + SzDecimals int `json:"szDecimals"` + MaxLeverage int `json:"maxLeverage"` + OnlyIsolated bool `json:"onlyIsolated"` + IsDelisted bool `json:"isDelisted"` +} + +//go:generate requestgen -method POST -url "/info" -type FuturesGetMetaRequest -responseType FuturesGetMetaResponse +type FuturesGetMetaRequest struct { + client requestgen.APIClient + + metaType ReqTypeInfo `param:"type" default:"meta" validValues:"meta"` +} + +func (c *Client) NewFuturesGetMetaRequest() *FuturesGetMetaRequest { + return &FuturesGetMetaRequest{ + client: c, + metaType: ReqMeta, + } +} diff --git a/pkg/exchange/hyperliquid/hyperapi/futures_get_meta_request_requestgen.go b/pkg/exchange/hyperliquid/hyperapi/futures_get_meta_request_requestgen.go new file mode 100644 index 0000000000..bff847166b --- /dev/null +++ b/pkg/exchange/hyperliquid/hyperapi/futures_get_meta_request_requestgen.go @@ -0,0 +1,202 @@ +// Code generated by "requestgen -method POST -url /info -type FuturesGetMetaRequest -responseType FuturesGetMetaResponse"; DO NOT EDIT. + +package hyperapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +func (f *FuturesGetMetaRequest) MetaType(metaType ReqTypeInfo) *FuturesGetMetaRequest { + f.metaType = metaType + return f +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (f *FuturesGetMetaRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + if f.isVarSlice(_v) { + f.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (f *FuturesGetMetaRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check metaType field -> json key type + metaType := f.metaType + + // TEMPLATE check-required + if len(metaType) == 0 { + metaType = "meta" + } + // END TEMPLATE check-required + + // TEMPLATE check-valid-values + switch metaType { + case "meta": + params["type"] = metaType + + default: + return nil, fmt.Errorf("type value %v is invalid", metaType) + + } + // END TEMPLATE check-valid-values + + // assign parameter of metaType + params["type"] = metaType + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (f *FuturesGetMetaRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := f.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if f.isVarSlice(_v) { + f.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (f *FuturesGetMetaRequest) GetParametersJSON() ([]byte, error) { + params, err := f.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (f *FuturesGetMetaRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (f *FuturesGetMetaRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (f *FuturesGetMetaRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (f *FuturesGetMetaRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (f *FuturesGetMetaRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := f.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +// GetPath returns the request path of the API +func (f *FuturesGetMetaRequest) GetPath() string { + return "/info" +} + +// Do generates the request object and send the request object to the API endpoint +func (f *FuturesGetMetaRequest) Do(ctx context.Context) (*FuturesGetMetaResponse, error) { + + params, err := f.GetParameters() + if err != nil { + return nil, err + } + query := url.Values{} + + var apiURL string + + apiURL = f.GetPath() + + req, err := f.client.NewRequest(ctx, "POST", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := f.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse FuturesGetMetaResponse + + type responseUnmarshaler interface { + Unmarshal(data []byte) error + } + + if unmarshaler, ok := interface{}(&apiResponse).(responseUnmarshaler); ok { + if err := unmarshaler.Unmarshal(response.Body); err != nil { + return nil, err + } + } else { + // The line below checks the content type, however, some API server might not send the correct content type header, + // Hence, this is commented for backward compatibility + // response.IsJSON() + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + } + + type responseValidator interface { + Validate() error + } + + if validator, ok := interface{}(&apiResponse).(responseValidator); ok { + if err := validator.Validate(); err != nil { + return nil, err + } + } + return &apiResponse, nil +} diff --git a/pkg/exchange/hyperliquid/hyperapi/get_account_balance_request.go b/pkg/exchange/hyperliquid/hyperapi/get_account_balance_request.go new file mode 100644 index 0000000000..7afb7d02f3 --- /dev/null +++ b/pkg/exchange/hyperliquid/hyperapi/get_account_balance_request.go @@ -0,0 +1,34 @@ +package hyperapi + +import ( + "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/fixedpoint" +) + +type Balance struct { + Coin string `json:"coin"` + Token int64 `json:"token"` + Hold fixedpoint.Value `json:"hold"` + Total fixedpoint.Value `json:"total"` + EntryNtl fixedpoint.Value `json:"entryNtl"` +} + +type Account struct { + Balances []Balance `json:"balances"` +} + +//go:generate requestgen -method POST -url "/info" -type GetAccountBalanceRequest -responseType Account +type GetAccountBalanceRequest struct { + client requestgen.APIClient + + user string `param:"user,required"` + metaType ReqTypeInfo `param:"type" default:"spotClearinghouseState" validValues:"spotClearinghouseState"` +} + +func (c *Client) NewGetAccountBalanceRequest() *GetAccountBalanceRequest { + return &GetAccountBalanceRequest{ + client: c, + metaType: ReqSpotClearinghouseState, + } +} diff --git a/pkg/exchange/hyperliquid/hyperapi/get_account_balance_request_requestgen.go b/pkg/exchange/hyperliquid/hyperapi/get_account_balance_request_requestgen.go new file mode 100644 index 0000000000..cc1bdf124d --- /dev/null +++ b/pkg/exchange/hyperliquid/hyperapi/get_account_balance_request_requestgen.go @@ -0,0 +1,218 @@ +// Code generated by "requestgen -method POST -url /info -type GetAccountBalanceRequest -responseType Account"; DO NOT EDIT. + +package hyperapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +func (g *GetAccountBalanceRequest) User(user string) *GetAccountBalanceRequest { + g.user = user + return g +} + +func (g *GetAccountBalanceRequest) MetaType(metaType ReqTypeInfo) *GetAccountBalanceRequest { + g.metaType = metaType + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetAccountBalanceRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetAccountBalanceRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check user field -> json key user + user := g.user + + // TEMPLATE check-required + if len(user) == 0 { + return nil, fmt.Errorf("user is required, empty string given") + } + // END TEMPLATE check-required + + // assign parameter of user + params["user"] = user + // check metaType field -> json key type + metaType := g.metaType + + // TEMPLATE check-required + if len(metaType) == 0 { + metaType = "spotClearinghouseState" + } + // END TEMPLATE check-required + + // TEMPLATE check-valid-values + switch metaType { + case "spotClearinghouseState": + params["type"] = metaType + + default: + return nil, fmt.Errorf("type value %v is invalid", metaType) + + } + // END TEMPLATE check-valid-values + + // assign parameter of metaType + params["type"] = metaType + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetAccountBalanceRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetAccountBalanceRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetAccountBalanceRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetAccountBalanceRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetAccountBalanceRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetAccountBalanceRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetAccountBalanceRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +// GetPath returns the request path of the API +func (g *GetAccountBalanceRequest) GetPath() string { + return "/info" +} + +// Do generates the request object and send the request object to the API endpoint +func (g *GetAccountBalanceRequest) Do(ctx context.Context) (*Account, error) { + + params, err := g.GetParameters() + if err != nil { + return nil, err + } + query := url.Values{} + + var apiURL string + + apiURL = g.GetPath() + + req, err := g.client.NewRequest(ctx, "POST", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse Account + + type responseUnmarshaler interface { + Unmarshal(data []byte) error + } + + if unmarshaler, ok := interface{}(&apiResponse).(responseUnmarshaler); ok { + if err := unmarshaler.Unmarshal(response.Body); err != nil { + return nil, err + } + } else { + // The line below checks the content type, however, some API server might not send the correct content type header, + // Hence, this is commented for backward compatibility + // response.IsJSON() + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + } + + type responseValidator interface { + Validate() error + } + + if validator, ok := interface{}(&apiResponse).(responseValidator); ok { + if err := validator.Validate(); err != nil { + return nil, err + } + } + return &apiResponse, nil +} diff --git a/pkg/exchange/hyperliquid/hyperapi/get_candles_request.go b/pkg/exchange/hyperliquid/hyperapi/get_candles_request.go new file mode 100644 index 0000000000..4d1c8d7687 --- /dev/null +++ b/pkg/exchange/hyperliquid/hyperapi/get_candles_request.go @@ -0,0 +1,44 @@ +package hyperapi + +import ( + "github.com/c9s/requestgen" + + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +type KLine struct { + OpenPrice fixedpoint.Value `json:"o"` + HighestPrice fixedpoint.Value `json:"h"` + LowestPrice fixedpoint.Value `json:"l"` + ClosePrice fixedpoint.Value `json:"c"` + Volume fixedpoint.Value `json:"v"` + Interval string `json:"i"` + Symbol string `json:"s"` + Trades uint64 `json:"n"` + StartTime types.MillisecondTimestamp `json:"t"` + EndTime types.MillisecondTimestamp `json:"T"` +} + +type CandleRequest struct { + Coin string `json:"coin"` + Interval string `json:"interval"` + StartTime int64 `json:"startTime"` + EndTime int64 `json:"endTime,omitempty"` +} + +//go:generate requestgen -method POST -url "/info" -type GetCandlesRequest -responseType []KLine +type GetCandlesRequest struct { + client requestgen.APIClient + + metaType ReqTypeInfo `param:"type" default:"candleSnapshot" validValues:"candleSnapshot"` + + candleRequest CandleRequest `param:"req,required"` +} + +func (c *Client) NewGetCandlesRequest() *GetCandlesRequest { + return &GetCandlesRequest{ + client: c, + metaType: ReqCandleSnapshot, + } +} diff --git a/pkg/exchange/hyperliquid/hyperapi/get_candles_request_requestgen.go b/pkg/exchange/hyperliquid/hyperapi/get_candles_request_requestgen.go new file mode 100644 index 0000000000..03d4ecec1d --- /dev/null +++ b/pkg/exchange/hyperliquid/hyperapi/get_candles_request_requestgen.go @@ -0,0 +1,215 @@ +// Code generated by "requestgen -method POST -url /info -type GetCandlesRequest -responseType []KLine"; DO NOT EDIT. + +package hyperapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +func (g *GetCandlesRequest) MetaType(metaType ReqTypeInfo) *GetCandlesRequest { + g.metaType = metaType + return g +} + +func (g *GetCandlesRequest) CandleRequest(candleRequest CandleRequest) *GetCandlesRequest { + g.candleRequest = candleRequest + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetCandlesRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetCandlesRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check metaType field -> json key type + metaType := g.metaType + + // TEMPLATE check-required + if len(metaType) == 0 { + metaType = "candleSnapshot" + } + // END TEMPLATE check-required + + // TEMPLATE check-valid-values + switch metaType { + case "candleSnapshot": + params["type"] = metaType + + default: + return nil, fmt.Errorf("type value %v is invalid", metaType) + + } + // END TEMPLATE check-valid-values + + // assign parameter of metaType + params["type"] = metaType + // check candleRequest field -> json key req + candleRequest := g.candleRequest + + // TEMPLATE check-required + // END TEMPLATE check-required + + // assign parameter of candleRequest + params["req"] = candleRequest + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetCandlesRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetCandlesRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetCandlesRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetCandlesRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetCandlesRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetCandlesRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetCandlesRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +// GetPath returns the request path of the API +func (g *GetCandlesRequest) GetPath() string { + return "/info" +} + +// Do generates the request object and send the request object to the API endpoint +func (g *GetCandlesRequest) Do(ctx context.Context) ([]KLine, error) { + + params, err := g.GetParameters() + if err != nil { + return nil, err + } + query := url.Values{} + + var apiURL string + + apiURL = g.GetPath() + + req, err := g.client.NewRequest(ctx, "POST", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse []KLine + + type responseUnmarshaler interface { + Unmarshal(data []byte) error + } + + if unmarshaler, ok := interface{}(&apiResponse).(responseUnmarshaler); ok { + if err := unmarshaler.Unmarshal(response.Body); err != nil { + return nil, err + } + } else { + // The line below checks the content type, however, some API server might not send the correct content type header, + // Hence, this is commented for backward compatibility + // response.IsJSON() + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + } + + type responseValidator interface { + Validate() error + } + + if validator, ok := interface{}(&apiResponse).(responseValidator); ok { + if err := validator.Validate(); err != nil { + return nil, err + } + } + return apiResponse, nil +} diff --git a/pkg/exchange/hyperliquid/hyperapi/get_historical_orders_request.go b/pkg/exchange/hyperliquid/hyperapi/get_historical_orders_request.go new file mode 100644 index 0000000000..8929d24dfd --- /dev/null +++ b/pkg/exchange/hyperliquid/hyperapi/get_historical_orders_request.go @@ -0,0 +1,49 @@ +package hyperapi + +import ( + "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/requestgen" +) + +//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Response.Data +//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Response.Data + +type HistoricalOrder struct { + Coin string `json:"coin"` + Side string `json:"side"` + LimitPx string `json:"limitPx"` + Sz string `json:"sz"` + Oid int64 `json:"oid"` + Timestamp types.MillisecondTimestamp `json:"timestamp"` + TriggerCondition string `json:"triggerCondition"` + IsTrigger bool `json:"isTrigger"` + TriggerPx string `json:"triggerPx"` + Children []any `json:"children"` + IsPositionTpsl bool `json:"isPositionTpsl"` + ReduceOnly bool `json:"reduceOnly"` + OrderType string `json:"orderType"` + OrigSz string `json:"origSz"` + Tif string `json:"tif"` + Cloid *string `json:"cloid"` +} + +type HistoricalOrdersResponse struct { + Order HistoricalOrder `json:"order"` + Status string `json:"status"` + StatusTimestamp types.MillisecondTimestamp `json:"statusTimestamp"` +} + +//go:generate PostRequest -url "/info" -type GetHistoricalOrdersRequest -responseDataType []HistoricalOrdersResponse +type GetHistoricalOrdersRequest struct { + client requestgen.APIClient + + user string `param:"user,required"` + metaType ReqTypeInfo `param:"type" default:"historicalOrders" validValues:"historicalOrders"` +} + +func (c *Client) NewGetHistoricalOrdersRequest() *GetHistoricalOrdersRequest { + return &GetHistoricalOrdersRequest{ + client: c, + metaType: ReqHistoricalOrders, + } +} diff --git a/pkg/exchange/hyperliquid/hyperapi/get_historical_orders_request_requestgen.go b/pkg/exchange/hyperliquid/hyperapi/get_historical_orders_request_requestgen.go new file mode 100644 index 0000000000..8837872f81 --- /dev/null +++ b/pkg/exchange/hyperliquid/hyperapi/get_historical_orders_request_requestgen.go @@ -0,0 +1,222 @@ +// Code generated by "requestgen -method POST -responseType .APIResponse -responseDataField Response.Data -url /info -type GetHistoricalOrdersRequest -responseDataType []HistoricalOrdersResponse"; DO NOT EDIT. + +package hyperapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +func (g *GetHistoricalOrdersRequest) User(user string) *GetHistoricalOrdersRequest { + g.user = user + return g +} + +func (g *GetHistoricalOrdersRequest) MetaType(metaType ReqTypeInfo) *GetHistoricalOrdersRequest { + g.metaType = metaType + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetHistoricalOrdersRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetHistoricalOrdersRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check user field -> json key user + user := g.user + + // TEMPLATE check-required + if len(user) == 0 { + return nil, fmt.Errorf("user is required, empty string given") + } + // END TEMPLATE check-required + + // assign parameter of user + params["user"] = user + // check metaType field -> json key type + metaType := g.metaType + + // TEMPLATE check-required + if len(metaType) == 0 { + metaType = "historicalOrders" + } + // END TEMPLATE check-required + + // TEMPLATE check-valid-values + switch metaType { + case "historicalOrders": + params["type"] = metaType + + default: + return nil, fmt.Errorf("type value %v is invalid", metaType) + + } + // END TEMPLATE check-valid-values + + // assign parameter of metaType + params["type"] = metaType + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetHistoricalOrdersRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetHistoricalOrdersRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetHistoricalOrdersRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetHistoricalOrdersRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetHistoricalOrdersRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetHistoricalOrdersRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetHistoricalOrdersRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +// GetPath returns the request path of the API +func (g *GetHistoricalOrdersRequest) GetPath() string { + return "/info" +} + +// Do generates the request object and send the request object to the API endpoint +func (g *GetHistoricalOrdersRequest) Do(ctx context.Context) ([]HistoricalOrdersResponse, error) { + + params, err := g.GetParameters() + if err != nil { + return nil, err + } + query := url.Values{} + + var apiURL string + + apiURL = g.GetPath() + + req, err := g.client.NewRequest(ctx, "POST", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse APIResponse + + type responseUnmarshaler interface { + Unmarshal(data []byte) error + } + + if unmarshaler, ok := interface{}(&apiResponse).(responseUnmarshaler); ok { + if err := unmarshaler.Unmarshal(response.Body); err != nil { + return nil, err + } + } else { + // The line below checks the content type, however, some API server might not send the correct content type header, + // Hence, this is commented for backward compatibility + // response.IsJSON() + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + } + + type responseValidator interface { + Validate() error + } + + if validator, ok := interface{}(&apiResponse).(responseValidator); ok { + if err := validator.Validate(); err != nil { + return nil, err + } + } + var data []HistoricalOrdersResponse + if err := json.Unmarshal(apiResponse.Response.Data, &data); err != nil { + return nil, err + } + return data, nil +} diff --git a/pkg/exchange/hyperliquid/hyperapi/get_open_orders_request.go b/pkg/exchange/hyperliquid/hyperapi/get_open_orders_request.go new file mode 100644 index 0000000000..a92b9cf7c8 --- /dev/null +++ b/pkg/exchange/hyperliquid/hyperapi/get_open_orders_request.go @@ -0,0 +1,43 @@ +package hyperapi + +import ( + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/requestgen" +) + +//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Response.Data +//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Response.Data + +type OpenOrder struct { + Coin string `json:"coin"` + IsPositionTpsl bool `json:"isPositionTpsl"` + IsTrigger bool `json:"isTrigger"` + LimitPx fixedpoint.Value `json:"limitPx"` + Oid int64 `json:"oid"` + OrderType string `json:"orderType"` + OrigSz fixedpoint.Value `json:"origSz"` + ReduceOnly bool `json:"reduceOnly"` + Side string `json:"side"` + Sz fixedpoint.Value `json:"sz"` + Timestamp types.MillisecondTimestamp `json:"timestamp"` + TriggerCondition string `json:"triggerCondition"` + TriggerPx fixedpoint.Value `json:"triggerPx"` +} + +//go:generate PostRequest -url "/info" -type GetOpenOrdersRequest -responseDataType []OpenOrder +type GetOpenOrdersRequest struct { + client requestgen.APIClient + + user string `param:"user,required"` + dex *string `param:"dex"` + + metaType ReqTypeInfo `param:"type" default:"frontendOpenOrders" validValues:"frontendOpenOrders"` +} + +func (c *Client) NewGetOpenOrdersRequest() *GetOpenOrdersRequest { + return &GetOpenOrdersRequest{ + client: c, + metaType: ReqFrontendOpenOrders, + } +} diff --git a/pkg/exchange/hyperliquid/hyperapi/get_open_orders_request_requestgen.go b/pkg/exchange/hyperliquid/hyperapi/get_open_orders_request_requestgen.go new file mode 100644 index 0000000000..ef69bc0601 --- /dev/null +++ b/pkg/exchange/hyperliquid/hyperapi/get_open_orders_request_requestgen.go @@ -0,0 +1,240 @@ +// Code generated by "requestgen -method POST -responseType .APIResponse -responseDataField Response.Data -url /info -type GetOpenOrdersRequest -responseDataType []OpenOrder"; DO NOT EDIT. + +package hyperapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +func (g *GetOpenOrdersRequest) User(user string) *GetOpenOrdersRequest { + g.user = user + return g +} + +func (g *GetOpenOrdersRequest) Dex(dex string) *GetOpenOrdersRequest { + g.dex = &dex + return g +} + +func (g *GetOpenOrdersRequest) MetaType(metaType ReqTypeInfo) *GetOpenOrdersRequest { + g.metaType = metaType + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetOpenOrdersRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetOpenOrdersRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check user field -> json key user + user := g.user + + // TEMPLATE check-required + if len(user) == 0 { + return nil, fmt.Errorf("user is required, empty string given") + } + // END TEMPLATE check-required + + // assign parameter of user + params["user"] = user + // check dex field -> json key dex + if g.dex != nil { + dex := *g.dex + + // TEMPLATE check-required + if len(dex) == 0 { + } + // END TEMPLATE check-required + + // assign parameter of dex + params["dex"] = dex + } else { + } + // check metaType field -> json key type + metaType := g.metaType + + // TEMPLATE check-required + if len(metaType) == 0 { + metaType = "frontendOpenOrders" + } + // END TEMPLATE check-required + + // TEMPLATE check-valid-values + switch metaType { + case "frontendOpenOrders": + params["type"] = metaType + + default: + return nil, fmt.Errorf("type value %v is invalid", metaType) + + } + // END TEMPLATE check-valid-values + + // assign parameter of metaType + params["type"] = metaType + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetOpenOrdersRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetOpenOrdersRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetOpenOrdersRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetOpenOrdersRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetOpenOrdersRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetOpenOrdersRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetOpenOrdersRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +// GetPath returns the request path of the API +func (g *GetOpenOrdersRequest) GetPath() string { + return "/info" +} + +// Do generates the request object and send the request object to the API endpoint +func (g *GetOpenOrdersRequest) Do(ctx context.Context) ([]OpenOrder, error) { + + params, err := g.GetParameters() + if err != nil { + return nil, err + } + query := url.Values{} + + var apiURL string + + apiURL = g.GetPath() + + req, err := g.client.NewRequest(ctx, "POST", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse APIResponse + + type responseUnmarshaler interface { + Unmarshal(data []byte) error + } + + if unmarshaler, ok := interface{}(&apiResponse).(responseUnmarshaler); ok { + if err := unmarshaler.Unmarshal(response.Body); err != nil { + return nil, err + } + } else { + // The line below checks the content type, however, some API server might not send the correct content type header, + // Hence, this is commented for backward compatibility + // response.IsJSON() + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + } + + type responseValidator interface { + Validate() error + } + + if validator, ok := interface{}(&apiResponse).(responseValidator); ok { + if err := validator.Validate(); err != nil { + return nil, err + } + } + var data []OpenOrder + if err := json.Unmarshal(apiResponse.Response.Data, &data); err != nil { + return nil, err + } + return data, nil +} diff --git a/pkg/exchange/hyperliquid/hyperapi/get_trades_history_request.go b/pkg/exchange/hyperliquid/hyperapi/get_trades_history_request.go new file mode 100644 index 0000000000..6d8f071d10 --- /dev/null +++ b/pkg/exchange/hyperliquid/hyperapi/get_trades_history_request.go @@ -0,0 +1,47 @@ +package hyperapi + +import ( + "time" + + "github.com/c9s/bbgo/pkg/types" + "github.com/c9s/requestgen" +) + +//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Response.Data +//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Response.Data + +type Trade struct { + ClosedPnl string `json:"closedPnl,omitempty"` + Coin string `json:"coin"` + Crossed bool `json:"crossed"` + Dir string `json:"dir,omitempty"` + Hash string `json:"hash"` + Oid int64 `json:"oid"` + Px string `json:"px"` + Side string `json:"side"` + StartPosition string `json:"startPosition,omitempty"` + Sz string `json:"sz"` + Time types.MillisecondTimestamp `json:"time"` + Fee string `json:"fee,omitempty"` + FeeToken string `json:"feeToken,omitempty"` + BuilderFee string `json:"builderFee,omitempty"` + Tid int64 `json:"tid,omitempty"` +} + +//go:generate PostRequest -url "/info" -type GetTradesHistoryRequest -responseDataType []Trade +type GetTradesHistoryRequest struct { + client requestgen.APIClient + + user string `param:"user,required"` + startTime *time.Time `param:"startTime,milliseconds"` + endTime *time.Time `param:"endTime,milliseconds"` + aggregateByTime *bool `param:"aggregateByTime"` + metaType ReqTypeInfo `param:"type" default:"userFills" validValues:"userFills,userFillsByTime"` +} + +func (c *Client) NewGetTradesHistoryRequest() *GetTradesHistoryRequest { + return &GetTradesHistoryRequest{ + client: c, + metaType: ReqUserFills, + } +} diff --git a/pkg/exchange/hyperliquid/hyperapi/get_trades_history_request_requestgen.go b/pkg/exchange/hyperliquid/hyperapi/get_trades_history_request_requestgen.go new file mode 100644 index 0000000000..24f33d86bf --- /dev/null +++ b/pkg/exchange/hyperliquid/hyperapi/get_trades_history_request_requestgen.go @@ -0,0 +1,274 @@ +// Code generated by "requestgen -method POST -responseType .APIResponse -responseDataField Response.Data -url /info -type GetTradesHistoryRequest -responseDataType []Trade"; DO NOT EDIT. + +package hyperapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" + "strconv" + "time" +) + +func (g *GetTradesHistoryRequest) User(user string) *GetTradesHistoryRequest { + g.user = user + return g +} + +func (g *GetTradesHistoryRequest) StartTime(startTime time.Time) *GetTradesHistoryRequest { + g.startTime = &startTime + return g +} + +func (g *GetTradesHistoryRequest) EndTime(endTime time.Time) *GetTradesHistoryRequest { + g.endTime = &endTime + return g +} + +func (g *GetTradesHistoryRequest) AggregateByTime(aggregateByTime bool) *GetTradesHistoryRequest { + g.aggregateByTime = &aggregateByTime + return g +} + +func (g *GetTradesHistoryRequest) MetaType(metaType ReqTypeInfo) *GetTradesHistoryRequest { + g.metaType = metaType + return g +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (g *GetTradesHistoryRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (g *GetTradesHistoryRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check user field -> json key user + user := g.user + + // TEMPLATE check-required + if len(user) == 0 { + return nil, fmt.Errorf("user is required, empty string given") + } + // END TEMPLATE check-required + + // assign parameter of user + params["user"] = user + // check startTime field -> json key startTime + if g.startTime != nil { + startTime := *g.startTime + + // TEMPLATE check-required + // END TEMPLATE check-required + + // assign parameter of startTime + // convert time.Time to milliseconds time stamp + params["startTime"] = strconv.FormatInt(startTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check endTime field -> json key endTime + if g.endTime != nil { + endTime := *g.endTime + + // TEMPLATE check-required + // END TEMPLATE check-required + + // assign parameter of endTime + // convert time.Time to milliseconds time stamp + params["endTime"] = strconv.FormatInt(endTime.UnixNano()/int64(time.Millisecond), 10) + } else { + } + // check aggregateByTime field -> json key aggregateByTime + if g.aggregateByTime != nil { + aggregateByTime := *g.aggregateByTime + + // TEMPLATE check-required + // END TEMPLATE check-required + + // assign parameter of aggregateByTime + params["aggregateByTime"] = aggregateByTime + } else { + } + // check metaType field -> json key type + metaType := g.metaType + + // TEMPLATE check-required + if len(metaType) == 0 { + metaType = "userFills" + } + // END TEMPLATE check-required + + // TEMPLATE check-valid-values + switch metaType { + case "userFills", "userFillsByTime": + params["type"] = metaType + + default: + return nil, fmt.Errorf("type value %v is invalid", metaType) + + } + // END TEMPLATE check-valid-values + + // assign parameter of metaType + params["type"] = metaType + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (g *GetTradesHistoryRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := g.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if g.isVarSlice(_v) { + g.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (g *GetTradesHistoryRequest) GetParametersJSON() ([]byte, error) { + params, err := g.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (g *GetTradesHistoryRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (g *GetTradesHistoryRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (g *GetTradesHistoryRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (g *GetTradesHistoryRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (g *GetTradesHistoryRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := g.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +// GetPath returns the request path of the API +func (g *GetTradesHistoryRequest) GetPath() string { + return "/info" +} + +// Do generates the request object and send the request object to the API endpoint +func (g *GetTradesHistoryRequest) Do(ctx context.Context) ([]Trade, error) { + + params, err := g.GetParameters() + if err != nil { + return nil, err + } + query := url.Values{} + + var apiURL string + + apiURL = g.GetPath() + + req, err := g.client.NewRequest(ctx, "POST", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := g.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse APIResponse + + type responseUnmarshaler interface { + Unmarshal(data []byte) error + } + + if unmarshaler, ok := interface{}(&apiResponse).(responseUnmarshaler); ok { + if err := unmarshaler.Unmarshal(response.Body); err != nil { + return nil, err + } + } else { + // The line below checks the content type, however, some API server might not send the correct content type header, + // Hence, this is commented for backward compatibility + // response.IsJSON() + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + } + + type responseValidator interface { + Validate() error + } + + if validator, ok := interface{}(&apiResponse).(responseValidator); ok { + if err := validator.Validate(); err != nil { + return nil, err + } + } + var data []Trade + if err := json.Unmarshal(apiResponse.Response.Data, &data); err != nil { + return nil, err + } + return data, nil +} diff --git a/pkg/exchange/hyperliquid/hyperapi/place_order_request.go b/pkg/exchange/hyperliquid/hyperapi/place_order_request.go new file mode 100644 index 0000000000..68d3740de8 --- /dev/null +++ b/pkg/exchange/hyperliquid/hyperapi/place_order_request.go @@ -0,0 +1,73 @@ +package hyperapi + +import ( + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/requestgen" +) + +//go:generate -command GetRequest requestgen -method GET -responseType .APIResponse -responseDataField Response.Data +//go:generate -command PostRequest requestgen -method POST -responseType .APIResponse -responseDataField Response.Data + +type OrderResponse struct { + Statuses []struct { + Resting *struct { + Oid int `json:"oid"` + } `json:"resting,omitempty"` + Error string `json:"error,omitempty"` + Filled *struct { + TotalSz fixedpoint.Value `json:"totalSz"` + AvgPx fixedpoint.Value `json:"avgPx"` + Oid int `json:"oid"` + } `json:"filled,omitempty"` + } `json:"statuses"` +} + +type Order struct { + Asset int `json:"a"` + IsBuy bool `json:"b"` + Price string `json:"p"` + Size string `json:"s"` + ReduceOnly bool `json:"r"` + OrderType OrderType `json:"t"` + ClientOrderID *string `json:"c,omitempty"` +} + +type OrderType struct { + Limit *LimitOrderType `json:"limit,omitempty"` + Trigger *TriggerOrderType `json:"trigger,omitempty"` +} + +type LimitOrderType struct { + Tif TimeInForce `json:"tif,omitempty" validate:"Alo,Ioc,Gtc"` +} + +type TriggerOrderType struct { + IsMarket bool `json:"isMarket"` + TriggerPx string `json:"triggerPx,omitempty"` + Tpsl Tpsl `json:"tpsl,omitempty" validate:"tp,sl"` +} + +type BuilderInfo struct { + Builder string `json:"b"` + Fee int `json:"f"` +} + +//go:generate PostRequest -url "/exchange" -type PlaceOrderRequest -responseDataType OrderResponse +type PlaceOrderRequest struct { + client requestgen.AuthenticatedAPIClient + + metaType ReqTypeInfo `param:"type" default:"order" validValues:"order"` + + orders []Order `param:"orders,required"` + + grouping Grouping `param:"grouping" default:"na" validValues:"na,normalTpsl,positionTpsl"` + + builder *BuilderInfo `param:"builder,omitempty"` +} + +func (c *Client) NewPlaceOrderRequest() *PlaceOrderRequest { + return &PlaceOrderRequest{ + client: c, + metaType: ReqSubmitOrder, + } +} diff --git a/pkg/exchange/hyperliquid/hyperapi/place_order_request_requestgen.go b/pkg/exchange/hyperliquid/hyperapi/place_order_request_requestgen.go new file mode 100644 index 0000000000..5b56c2f5c5 --- /dev/null +++ b/pkg/exchange/hyperliquid/hyperapi/place_order_request_requestgen.go @@ -0,0 +1,262 @@ +// Code generated by "requestgen -method POST -responseType .APIResponse -responseDataField Response.Data -url /exchange -type PlaceOrderRequest -responseDataType OrderResponse"; DO NOT EDIT. + +package hyperapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +func (p *PlaceOrderRequest) MetaType(metaType ReqTypeInfo) *PlaceOrderRequest { + p.metaType = metaType + return p +} + +func (p *PlaceOrderRequest) Orders(orders []Order) *PlaceOrderRequest { + p.orders = orders + return p +} + +func (p *PlaceOrderRequest) Grouping(grouping Grouping) *PlaceOrderRequest { + p.grouping = grouping + return p +} + +func (p *PlaceOrderRequest) Builder(builder BuilderInfo) *PlaceOrderRequest { + p.builder = &builder + return p +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (p *PlaceOrderRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + if p.isVarSlice(_v) { + p.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (p *PlaceOrderRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check metaType field -> json key type + metaType := p.metaType + + // TEMPLATE check-required + if len(metaType) == 0 { + metaType = "order" + } + // END TEMPLATE check-required + + // TEMPLATE check-valid-values + switch metaType { + case "order": + params["type"] = metaType + + default: + return nil, fmt.Errorf("type value %v is invalid", metaType) + + } + // END TEMPLATE check-valid-values + + // assign parameter of metaType + params["type"] = metaType + // check orders field -> json key orders + orders := p.orders + + // TEMPLATE check-required + // END TEMPLATE check-required + + // assign parameter of orders + params["orders"] = orders + // check grouping field -> json key grouping + grouping := p.grouping + + // TEMPLATE check-required + if len(grouping) == 0 { + grouping = "na" + } + // END TEMPLATE check-required + + // TEMPLATE check-valid-values + switch grouping { + case "na", "normalTpsl", "positionTpsl": + params["grouping"] = grouping + + default: + return nil, fmt.Errorf("grouping value %v is invalid", grouping) + + } + // END TEMPLATE check-valid-values + + // assign parameter of grouping + params["grouping"] = grouping + // check builder field -> json key builder + if p.builder != nil { + builder := *p.builder + + // TEMPLATE check-required + // END TEMPLATE check-required + + // assign parameter of builder + params["builder"] = builder + } else { + } + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (p *PlaceOrderRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := p.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if p.isVarSlice(_v) { + p.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (p *PlaceOrderRequest) GetParametersJSON() ([]byte, error) { + params, err := p.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (p *PlaceOrderRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (p *PlaceOrderRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (p *PlaceOrderRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (p *PlaceOrderRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (p *PlaceOrderRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := p.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +// GetPath returns the request path of the API +func (p *PlaceOrderRequest) GetPath() string { + return "/exchange" +} + +// Do generates the request object and send the request object to the API endpoint +func (p *PlaceOrderRequest) Do(ctx context.Context) (*OrderResponse, error) { + + params, err := p.GetParameters() + if err != nil { + return nil, err + } + query := url.Values{} + + var apiURL string + + apiURL = p.GetPath() + + req, err := p.client.NewAuthenticatedRequest(ctx, "POST", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := p.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse APIResponse + + type responseUnmarshaler interface { + Unmarshal(data []byte) error + } + + if unmarshaler, ok := interface{}(&apiResponse).(responseUnmarshaler); ok { + if err := unmarshaler.Unmarshal(response.Body); err != nil { + return nil, err + } + } else { + // The line below checks the content type, however, some API server might not send the correct content type header, + // Hence, this is commented for backward compatibility + // response.IsJSON() + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + } + + type responseValidator interface { + Validate() error + } + + if validator, ok := interface{}(&apiResponse).(responseValidator); ok { + if err := validator.Validate(); err != nil { + return nil, err + } + } + var data OrderResponse + if err := json.Unmarshal(apiResponse.Response.Data, &data); err != nil { + return nil, err + } + return &data, nil +} diff --git a/pkg/exchange/hyperliquid/hyperapi/spot_get_meta_and_asset_ctxs_request.go b/pkg/exchange/hyperliquid/hyperapi/spot_get_meta_and_asset_ctxs_request.go new file mode 100644 index 0000000000..00312e3e9c --- /dev/null +++ b/pkg/exchange/hyperliquid/hyperapi/spot_get_meta_and_asset_ctxs_request.go @@ -0,0 +1,56 @@ +package hyperapi + +import ( + "encoding/json" + "fmt" + + "github.com/c9s/requestgen" +) + +type SpotMetaAndAssetCtxsResponse struct { + Meta SpotGetMetaResponse + AssetCtxs []SpotAssetContext +} + +type SpotAssetContext struct { + DayNotionalVolume string `json:"dayNtlVlm"` + MarkPrice string `json:"markPx"` + MidPrice string `json:"midPx"` + PrevDayPrice string `json:"prevDayPx"` +} + +func (r *SpotMetaAndAssetCtxsResponse) UnmarshalJSON(data []byte) error { + var payload []json.RawMessage + if err := json.Unmarshal(data, &payload); err != nil { + return err + } + + if len(payload) != 2 { + return fmt.Errorf("unexpected spotMetaAndAssetCtxs payload length %d", len(payload)) + } + + if err := json.Unmarshal(payload[0], &r.Meta); err != nil { + return fmt.Errorf("unmarshal meta block: %w", err) + } + + if err := json.Unmarshal(payload[1], &r.AssetCtxs); err != nil { + return fmt.Errorf("unmarshal asset contexts: %w", err) + } + + return nil +} + +//go:generate requestgen -method POST -url "/info" -type SpotGetMetaAndAssetCtxsRequest -responseType SpotMetaAndAssetCtxsResponse + +type SpotGetMetaAndAssetCtxsRequest struct { + client requestgen.APIClient + + metaType ReqTypeInfo `param:"type" default:"spotMetaAndAssetCtxs" validValues:"spotMetaAndAssetCtxs"` +} + +func (c *Client) NewSpotGetMetaAndAssetCtxsRequest() *SpotGetMetaAndAssetCtxsRequest { + return &SpotGetMetaAndAssetCtxsRequest{ + client: c, + metaType: ReqSpotMetaAndAssetCtxs, + } +} diff --git a/pkg/exchange/hyperliquid/hyperapi/spot_get_meta_and_asset_ctxs_request_requestgen.go b/pkg/exchange/hyperliquid/hyperapi/spot_get_meta_and_asset_ctxs_request_requestgen.go new file mode 100644 index 0000000000..9650fd7c6a --- /dev/null +++ b/pkg/exchange/hyperliquid/hyperapi/spot_get_meta_and_asset_ctxs_request_requestgen.go @@ -0,0 +1,202 @@ +// Code generated by "requestgen -method POST -url /info -type SpotGetMetaAndAssetCtxsRequest -responseType SpotMetaAndAssetCtxsResponse"; DO NOT EDIT. + +package hyperapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +func (s *SpotGetMetaAndAssetCtxsRequest) MetaType(metaType ReqTypeInfo) *SpotGetMetaAndAssetCtxsRequest { + s.metaType = metaType + return s +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (s *SpotGetMetaAndAssetCtxsRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + if s.isVarSlice(_v) { + s.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (s *SpotGetMetaAndAssetCtxsRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check metaType field -> json key type + metaType := s.metaType + + // TEMPLATE check-required + if len(metaType) == 0 { + metaType = "spotMetaAndAssetCtxs" + } + // END TEMPLATE check-required + + // TEMPLATE check-valid-values + switch metaType { + case "spotMetaAndAssetCtxs": + params["type"] = metaType + + default: + return nil, fmt.Errorf("type value %v is invalid", metaType) + + } + // END TEMPLATE check-valid-values + + // assign parameter of metaType + params["type"] = metaType + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (s *SpotGetMetaAndAssetCtxsRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := s.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if s.isVarSlice(_v) { + s.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (s *SpotGetMetaAndAssetCtxsRequest) GetParametersJSON() ([]byte, error) { + params, err := s.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (s *SpotGetMetaAndAssetCtxsRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (s *SpotGetMetaAndAssetCtxsRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (s *SpotGetMetaAndAssetCtxsRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (s *SpotGetMetaAndAssetCtxsRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (s *SpotGetMetaAndAssetCtxsRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := s.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +// GetPath returns the request path of the API +func (s *SpotGetMetaAndAssetCtxsRequest) GetPath() string { + return "/info" +} + +// Do generates the request object and send the request object to the API endpoint +func (s *SpotGetMetaAndAssetCtxsRequest) Do(ctx context.Context) (*SpotMetaAndAssetCtxsResponse, error) { + + params, err := s.GetParameters() + if err != nil { + return nil, err + } + query := url.Values{} + + var apiURL string + + apiURL = s.GetPath() + + req, err := s.client.NewRequest(ctx, "POST", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := s.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse SpotMetaAndAssetCtxsResponse + + type responseUnmarshaler interface { + Unmarshal(data []byte) error + } + + if unmarshaler, ok := interface{}(&apiResponse).(responseUnmarshaler); ok { + if err := unmarshaler.Unmarshal(response.Body); err != nil { + return nil, err + } + } else { + // The line below checks the content type, however, some API server might not send the correct content type header, + // Hence, this is commented for backward compatibility + // response.IsJSON() + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + } + + type responseValidator interface { + Validate() error + } + + if validator, ok := interface{}(&apiResponse).(responseValidator); ok { + if err := validator.Validate(); err != nil { + return nil, err + } + } + return &apiResponse, nil +} diff --git a/pkg/exchange/hyperliquid/hyperapi/spot_get_meta_request.go b/pkg/exchange/hyperliquid/hyperapi/spot_get_meta_request.go new file mode 100644 index 0000000000..bef63c67f9 --- /dev/null +++ b/pkg/exchange/hyperliquid/hyperapi/spot_get_meta_request.go @@ -0,0 +1,42 @@ +package hyperapi + +import ( + "github.com/c9s/requestgen" +) + +type SpotGetMetaResponse struct { + Tokens []TokenMeta `json:"tokens"` + Universe []UniverseMeta `json:"universe"` +} + +type TokenMeta 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"` +} + +type UniverseMeta struct { + Name string `json:"name"` + Tokens [2]int `json:"tokens"` + Index int `json:"index"` + IsCanonical bool `json:"isCanonical"` +} + +//go:generate requestgen -method POST -url "/info" -type SpotGetMetaRequest -responseType SpotGetMetaResponse +type SpotGetMetaRequest struct { + client requestgen.APIClient + + metaType ReqTypeInfo `param:"type" default:"spotMeta" validValues:"spotMeta"` +} + +func (c *Client) NewSpotGetMetaRequest() *SpotGetMetaRequest { + return &SpotGetMetaRequest{ + client: c, + metaType: ReqSpotMeta, + } +} diff --git a/pkg/exchange/hyperliquid/hyperapi/spot_get_meta_request_requestgen.go b/pkg/exchange/hyperliquid/hyperapi/spot_get_meta_request_requestgen.go new file mode 100644 index 0000000000..b97a63e866 --- /dev/null +++ b/pkg/exchange/hyperliquid/hyperapi/spot_get_meta_request_requestgen.go @@ -0,0 +1,202 @@ +// Code generated by "requestgen -method POST -url /info -type SpotGetMetaRequest -responseType SpotGetMetaResponse"; DO NOT EDIT. + +package hyperapi + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "reflect" + "regexp" +) + +func (s *SpotGetMetaRequest) MetaType(metaType ReqTypeInfo) *SpotGetMetaRequest { + s.metaType = metaType + return s +} + +// GetQueryParameters builds and checks the query parameters and returns url.Values +func (s *SpotGetMetaRequest) GetQueryParameters() (url.Values, error) { + var params = map[string]interface{}{} + + query := url.Values{} + for _k, _v := range params { + if s.isVarSlice(_v) { + s.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParameters builds and checks the parameters and return the result in a map object +func (s *SpotGetMetaRequest) GetParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + // check metaType field -> json key type + metaType := s.metaType + + // TEMPLATE check-required + if len(metaType) == 0 { + metaType = "spotMeta" + } + // END TEMPLATE check-required + + // TEMPLATE check-valid-values + switch metaType { + case "spotMeta": + params["type"] = metaType + + default: + return nil, fmt.Errorf("type value %v is invalid", metaType) + + } + // END TEMPLATE check-valid-values + + // assign parameter of metaType + params["type"] = metaType + + return params, nil +} + +// GetParametersQuery converts the parameters from GetParameters into the url.Values format +func (s *SpotGetMetaRequest) GetParametersQuery() (url.Values, error) { + query := url.Values{} + + params, err := s.GetParameters() + if err != nil { + return query, err + } + + for _k, _v := range params { + if s.isVarSlice(_v) { + s.iterateSlice(_v, func(it interface{}) { + query.Add(_k+"[]", fmt.Sprintf("%v", it)) + }) + } else { + query.Add(_k, fmt.Sprintf("%v", _v)) + } + } + + return query, nil +} + +// GetParametersJSON converts the parameters from GetParameters into the JSON format +func (s *SpotGetMetaRequest) GetParametersJSON() ([]byte, error) { + params, err := s.GetParameters() + if err != nil { + return nil, err + } + + return json.Marshal(params) +} + +// GetSlugParameters builds and checks the slug parameters and return the result in a map object +func (s *SpotGetMetaRequest) GetSlugParameters() (map[string]interface{}, error) { + var params = map[string]interface{}{} + + return params, nil +} + +func (s *SpotGetMetaRequest) applySlugsToUrl(url string, slugs map[string]string) string { + for _k, _v := range slugs { + needleRE := regexp.MustCompile(":" + _k + "\\b") + url = needleRE.ReplaceAllString(url, _v) + } + + return url +} + +func (s *SpotGetMetaRequest) iterateSlice(slice interface{}, _f func(it interface{})) { + sliceValue := reflect.ValueOf(slice) + for _i := 0; _i < sliceValue.Len(); _i++ { + it := sliceValue.Index(_i).Interface() + _f(it) + } +} + +func (s *SpotGetMetaRequest) isVarSlice(_v interface{}) bool { + rt := reflect.TypeOf(_v) + switch rt.Kind() { + case reflect.Slice: + return true + } + return false +} + +func (s *SpotGetMetaRequest) GetSlugsMap() (map[string]string, error) { + slugs := map[string]string{} + params, err := s.GetSlugParameters() + if err != nil { + return slugs, nil + } + + for _k, _v := range params { + slugs[_k] = fmt.Sprintf("%v", _v) + } + + return slugs, nil +} + +// GetPath returns the request path of the API +func (s *SpotGetMetaRequest) GetPath() string { + return "/info" +} + +// Do generates the request object and send the request object to the API endpoint +func (s *SpotGetMetaRequest) Do(ctx context.Context) (*SpotGetMetaResponse, error) { + + params, err := s.GetParameters() + if err != nil { + return nil, err + } + query := url.Values{} + + var apiURL string + + apiURL = s.GetPath() + + req, err := s.client.NewRequest(ctx, "POST", apiURL, query, params) + if err != nil { + return nil, err + } + + response, err := s.client.SendRequest(req) + if err != nil { + return nil, err + } + + var apiResponse SpotGetMetaResponse + + type responseUnmarshaler interface { + Unmarshal(data []byte) error + } + + if unmarshaler, ok := interface{}(&apiResponse).(responseUnmarshaler); ok { + if err := unmarshaler.Unmarshal(response.Body); err != nil { + return nil, err + } + } else { + // The line below checks the content type, however, some API server might not send the correct content type header, + // Hence, this is commented for backward compatibility + // response.IsJSON() + if err := response.DecodeJSON(&apiResponse); err != nil { + return nil, err + } + } + + type responseValidator interface { + Validate() error + } + + if validator, ok := interface{}(&apiResponse).(responseValidator); ok { + if err := validator.Validate(); err != nil { + return nil, err + } + } + return &apiResponse, nil +} diff --git a/pkg/exchange/hyperliquid/hyperapi/types.go b/pkg/exchange/hyperliquid/hyperapi/types.go new file mode 100644 index 0000000000..16378c44ff --- /dev/null +++ b/pkg/exchange/hyperliquid/hyperapi/types.go @@ -0,0 +1,60 @@ +package hyperapi + +import ( + "encoding/json" +) + +type SignatureResult struct { + R string `json:"r"` + S string `json:"s"` + V int `json:"v"` +} + +type ReqTypeInfo string + +const ( + ReqMeta ReqTypeInfo = "meta" + ReqSpotMeta ReqTypeInfo = "spotMeta" + ReqSubmitOrder ReqTypeInfo = "order" + ReqCancelOrder ReqTypeInfo = "cancel" + ReqCandleSnapshot ReqTypeInfo = "candleSnapshot" + ReqFrontendOpenOrders ReqTypeInfo = "frontendOpenOrders" + ReqUserFills ReqTypeInfo = "userFills" + ReqUserFillsByTime ReqTypeInfo = "userFillsByTime" + ReqHistoricalOrders ReqTypeInfo = "historicalOrders" + ReqSpotClearinghouseState ReqTypeInfo = "spotClearinghouseState" + ReqFuturesClearinghouseState ReqTypeInfo = "clearinghouseState" + ReqSpotMetaAndAssetCtxs ReqTypeInfo = "spotMetaAndAssetCtxs" + ReqFuturesMetaAndAssetCtxs ReqTypeInfo = "metaAndAssetCtxs" +) + +type TimeInForce string + +const ( + TimeInForceALO TimeInForce = "Alo" + TimeInForceIOC TimeInForce = "Ioc" + TimeInForceGTC TimeInForce = "Gtc" +) + +type Grouping string + +const ( + GroupingNA Grouping = "na" + GroupingNormalTpsl Grouping = "normalTpsl" + GroupingPositionTpls Grouping = "positionTpsl" +) + +type Tpsl string // Advanced order type + +const ( + TakeProfit Tpsl = "tp" + StopLoss Tpsl = "sl" +) + +type APIResponse struct { + Status string `json:"status"` + Response struct { + Type string `json:"type"` + Data json.RawMessage `json:"data"` + } `json:"response"` +} diff --git a/pkg/exchange/hyperliquid/stream.go b/pkg/exchange/hyperliquid/stream.go new file mode 100644 index 0000000000..b3b3652387 --- /dev/null +++ b/pkg/exchange/hyperliquid/stream.go @@ -0,0 +1,24 @@ +package hyperliquid + +import ( + "github.com/c9s/bbgo/pkg/exchange/hyperliquid/hyperapi" + "github.com/c9s/bbgo/pkg/types" +) + +// Stream represents the Hyperliquid websocket stream. +// +//go:generate callbackgen -type Stream +type Stream struct { + types.StandardStream + + client *hyperapi.Client + exchange *Exchange +} + +func NewStream(client *hyperapi.Client, ex *Exchange) *Stream { + + return &Stream{ + client: client, + exchange: ex, + } +} diff --git a/pkg/exchange/hyperliquid/stream_callbacks.go b/pkg/exchange/hyperliquid/stream_callbacks.go new file mode 100644 index 0000000000..67216b3592 --- /dev/null +++ b/pkg/exchange/hyperliquid/stream_callbacks.go @@ -0,0 +1,5 @@ +// Code generated by "callbackgen -type Stream"; DO NOT EDIT. + +package hyperliquid + +import () diff --git a/pkg/exchange/hyperliquid/symbols.go b/pkg/exchange/hyperliquid/symbols.go new file mode 100644 index 0000000000..2938a0d939 --- /dev/null +++ b/pkg/exchange/hyperliquid/symbols.go @@ -0,0 +1,462 @@ +// Code generated by go generate; DO NOT EDIT. +package hyperliquid + +var spotSymbolMap = map[string]string{ + "AAVE0USDC": "AAVE0@227", + "ADHDUSDC": "ADHD@40", + "ANONUSDC": "ANON@174", + "ANSEMUSDC": "ANSEM@18", + "ANTUSDC": "ANT@55", + "ANZUSDC": "ANZ@192", + "APUUSDC": "APU@196", + "ARIUSDC": "ARI@53", + "ASIUSDC": "ASI@36", + "ATEHUNUSDC": "ATEHUN@51", + "AUTISTUSDC": "AUTIST@94", + "AVAX0USDC": "AVAX0@226", + "BAGSUSDC": "BAGS@17", + "BAYCUSDC": "BAYC@211", + "BEATSUSDC": "BEATS@130", + "BERAUSDC": "BERA@117", + "BIDUSDC": "BID@33", + "BIGBENUSDC": "BIGBEN@25", + "BOZOUSDC": "BOZO@77", + "BRIDGEUSDC": "BRIDGE@219", + "BUBZUSDC": "BUBZ@119", + "BUDDYUSDC": "BUDDY@160", + "BUSSYUSDC": "BUSSY@82", + "BZECUSDC": "BZEC@248", + "CAPPYUSDC": "CAPPY@7", + "CATBALUSDC": "CATBAL@59", + "CATNIPUSDC": "CATNIP@26", + "CATUSDC": "CAT@126", + "CHEFUSDC": "CHEF@108", + "CHINAUSDC": "CHINA@68", + "CINDYUSDC": "CINDY@67", + "COOKUSDC": "COOK@172", + "COPEUSDC": "COPE@104", + "COZYUSDC": "COZY@52", + "CZUSDC": "CZ@16", + "DBLNUSDC": "DBLN@203", + "DEFINUSDC": "DEFIN@146", + "DEPINUSDC": "DEPIN@128", + "DIABLOUSDC": "DIABLO@164", + "DROPUSDC": "DROP@46", + "EARTHUSDC": "EARTH@99", + "EXUSDC": "EX@199", + "FARMEDUSDC": "FARMED@30", + "FARMUSDC": "FARM@123", + "FATCATUSDC": "FATCAT@83", + "FEITUSDC": "FEIT@90", + "FEUSDUSDC": "FEUSD@153", + "FIUSDC": "FI@247", + "FLASKUSDC": "FLASK@124", + "FLRUSDC": "FLR@225", + "FLYUSDC": "FLY@137", + "FRACUSDC": "FRAC@50", + "FRCTUSDC": "FRCT@175", + "FRIEDUSDC": "FRIED@70", + "FRUDOUSDC": "FRUDO@91", + "FUCKYUSDC": "FUCKY@15", + "FUNDUSDC": "FUND@163", + "FUNUSDC": "FUN@41", + "GENESYUSDC": "GENESY@118", + "GMEOWUSDC": "GMEOW@10", + "GODUSDC": "GOD@141", + "GPTUSDC": "GPT@31", + "GUESSUSDC": "GUESS@61", + "GUPUSDC": "GUP@29", + "GUSDC": "G@76", + "HAPPYUSDC": "HAPPY@22", + "HBOOSTUSDC": "HBOOST@27", + "HEADUSDC": "HEAD@143", + "HFUNUSDC": "HFUN@1", + "HGODUSDC": "HGOD@96", + "HODLUSDC": "HODL@34", + "HOLDUSDC": "HOLD@115", + "HOPEUSDC": "HOPE@81", + "HOPUSDC": "HOP@102", + "HORSYUSDC": "HORSY@183", + "HPENGUUSDC": "HPENGU@184", + "HPEPEUSDC": "HPEPE@44", + "HPUMPUSDC": "HPUMP@64", + "HPYHUSDC": "HPYH@105", + "HREKTUSDC": "HREKT@201", + "HUSDC": "H@133", + "HWAVEUSDC": "HWAVE@241", + "HWTRUSDC": "HWTR@140", + "HYENAUSDC": "HYENA@127", + "HYPEUSDC": "HYPE@107", + "HYPEUSDH": "HYPE@232", + "HYPEUSDT0": "HYPE@207", + "ILIENSUSDC": "ILIENS@14", + "ISLANDUSDC": "ISLAND@185", + "IZECUSDC": "IZEC@246", + "JEETUSDC": "JEET@45", + "JEFFUSDC": "JEFF@4", + "JPEGUSDC": "JPEG@147", + "KEIUSDC": "KEI@236", + "KITTENUSDC": "KITTEN@240", + "KITTYUSDC": "KITTY@202", + "KOBEUSDC": "KOBE@21", + "LADYUSDC": "LADY@42", + "LATINAUSDC": "LATINA@195", + "LAUNCHUSDC": "LAUNCH@122", + "LICKOUSDC": "LICKO@187", + "LICKUSDC": "LICK@2", + "LINK0USDC": "LINK0@213", + "LINK0USDT0": "LINK0@212", + "LIQDUSDC": "LIQD@132", + "LIQUIDUSDC": "LIQUID@97", + "LORAUSDC": "LORA@58", + "LQNAUSDC": "LQNA@86", + "LTHREEUSDC": "LTHREE@208", + "LUCKYUSDC": "LUCKY@103", + "MAGAUSDC": "MAGA@95", + "MANLETUSDC": "MANLET@3", + "MAXIUSDC": "MAXI@62", + "MBAPPEUSDC": "MBAPPE@47", + "MEOWUSDC": "MEOW@112", + "MOGUSDC": "MOG@43", + "MONADUSDC": "MONAD@80", + "MONUSDC": "MON@129", + "MUNCHUSDC": "MUNCH@116", + "NASDAQUSDC": "NASDAQ@87", + "NBTUSDC": "NBT@245", + "NEIROUSDC": "NEIRO@113", + "NFTUSDC": "NFT@56", + "NIGGOUSDC": "NIGGO@101", + "NMTDUSDC": "NMTD@63", + "NOCEXUSDC": "NOCEX@72", + "OMNIXUSDC": "OMNIX@74", + "ORAUSDC": "ORA@131", + "OTTIUSDC": "OTTI@179", + "PANDAUSDC": "PANDA@38", + "PEARUSDC": "PEAR@114", + "PEGUSDC": "PEG@170", + "PENISUSDC": "PENIS@165", + "PEPEUSDC": "PEPE@11", + "PERPUSDC": "PERP@176", + "PICKLUSDC": "PICKL@120", + "PIGEONUSDC": "PIGEON@65", + "PILLUSDC": "PILL@39", + "PIPUSDC": "PIP@85", + "POINTSUSDC": "POINTS@8", + "PRFIUSDC": "PRFI@161", + "PUMPUSDC": "PUMP@20", + "PUPUSDC": "PUP@223", + "PURROUSDC": "PURRO@177", + "PURRPSUSDC": "PURRPS@32", + "PURRUSDC": "PURR@0", + "QUANTUSDC": "QUANT@155", + "RAGEUSDC": "RAGE@49", + "RANKUSDC": "RANK@73", + "RATUSDC": "RAT@157", + "REIUSDC": "REI@216", + "RETARDUSDC": "RETARD@111", + "REXUSDC": "REX@218", + "RICHUSDC": "RICH@57", + "RIPUSDC": "RIP@75", + "RISEUSDC": "RISE@66", + "RUBUSDC": "RUB@173", + "RUGUSDC": "RUG@13", + "RZRUSDC": "RZR@217", + "SCHIZOUSDC": "SCHIZO@23", + "SELLUSDC": "SELL@24", + "SENTUSDC": "SENT@135", + "SHEEPUSDC": "SHEEP@121", + "SHOEUSDC": "SHOE@79", + "SHREKUSDC": "SHREK@84", + "SIXUSDC": "SIX@5", + "SLAYUSDC": "SLAY@215", + "SOLVUSDC": "SOLV@136", + "SOVRNUSDC": "SOVRN@139", + "SPHUSDC": "SPH@78", + "STACKUSDC": "STACK@69", + "STARUSDC": "STAR@134", + "STEELUSDC": "STEEL@110", + "STLOOPUSDC": "STLOOP@190", + "STRICTUSDC": "STRICT@93", + "SUCKYUSDC": "SUCKY@28", + "SWAPUSDC": "SWAP@242", + "SYLVIUSDC": "SYLVI@89", + "TATEUSDC": "TATE@19", + "TESTUSDC": "TEST@48", + "THBILLUSDC": "THBILL@205", + "TILTUSDC": "TILT@158", + "TIMEUSDC": "TIME@138", + "TJIFUSDC": "TJIF@60", + "TRADEUSDC": "TRADE@191", + "TRENDUSDC": "TREND@159", + "TRUMPUSDC": "TRUMP@9", + "UBONKUSDC": "UBONK@194", + "UBTCUSDC": "UBTC@142", + "UBTCUSDH": "UBTC@234", + "UDZUSDC": "UDZ@228", + "UENAUSDC": "UENA@206", + "UETHUSDC": "UETH@151", + "UETHUSDH": "UETH@235", + "UFARTUSDC": "UFART@162", + "UMOGUSDC": "UMOG@200", + "UMONUSDC": "UMON@243", + "UPHLUSDC": "UPHL@231", + "UPUMPUSDC": "UPUMP@188", + "UPUSDC": "UP@100", + "USDEUSDC": "USDE@150", + "USDHLUSDC": "USDHL@180", + "USDHUSDC": "USDH@230", + "USDT0USDC": "USDT0@166", + "USDXLUSDC": "USDXL@152", + "USDXLUSDT0": "USDXL@244", + "USHUSDC": "USH@171", + "USOLUSDC": "USOL@156", + "USPYXUSDC": "USPYX@189", + "USRUSDC": "USR@178", + "UUUSPXUSDC": "UUUSPX@193", + "UWLDUSDC": "UWLD@224", + "UXPLUSDC": "UXPL@210", + "UXPLUSDH": "UXPL@233", + "VAPORUSDC": "VAPOR@37", + "VAULTUSDC": "VAULT@125", + "VEGASUSDC": "VEGAS@35", + "VIZNUSDC": "VIZN@92", + "VORTXUSDC": "VORTX@145", + "VSNUSDC": "VSN@204", + "WAGMIUSDC": "WAGMI@6", + "WARUSDC": "WAR@237", + "WASHUSDC": "WASH@54", + "WHYPIUSDC": "WHYPI@148", + "WMNTUSDC": "WMNT@214", + "WOULDUSDC": "WOULD@198", + "WOWUSDC": "WOW@109", + "XAUT0USDC": "XAUT0@182", + "XAUT0USDT0": "XAUT0@209", + "XIONUSDC": "XION@238", + "XULIANUSDC": "XULIAN@12", + "YAPUSDC": "YAP@106", + "YEETIUSDC": "YEETI@88", +} + +var futuresSymbolMap = map[string]string{ + "0GUSDC": "0G@210", + "2ZUSDC": "2Z@213", + "AAVEUSDC": "AAVE@28", + "ACEUSDC": "ACE@96", + "ADAUSDC": "ADA@65", + "AI16ZUSDC": "AI16Z@166", + "AIUSDC": "AI@115", + "AIXBTUSDC": "AIXBT@167", + "ALGOUSDC": "ALGO@158", + "ALTUSDC": "ALT@107", + "ANIMEUSDC": "ANIME@176", + "APEUSDC": "APE@8", + "APEXUSDC": "APEX@212", + "APTUSDC": "APT@27", + "ARBUSDC": "ARB@11", + "ARKUSDC": "ARK@55", + "ARUSDC": "AR@117", + "ASTERUSDC": "ASTER@207", + "ATOMUSDC": "ATOM@2", + "AVAXUSDC": "AVAX@6", + "AVNTUSDC": "AVNT@208", + "BABYUSDC": "BABY@189", + "BADGERUSDC": "BADGER@77", + "BANANAUSDC": "BANANA@49", + "BCHUSDC": "BCH@26", + "BERAUSDC": "BERA@180", + "BIGTIMEUSDC": "BIGTIME@59", + "BIOUSDC": "BIO@169", + "BLASTUSDC": "BLAST@137", + "BLURUSDC": "BLUR@62", + "BLZUSDC": "BLZ@47", + "BNBUSDC": "BNB@7", + "BNTUSDC": "BNT@56", + "BOMEUSDC": "BOME@120", + "BRETTUSDC": "BRETT@134", + "BSVUSDC": "BSV@64", + "BTCUSDC": "BTC@0", + "CAKEUSDC": "CAKE@99", + "CANTOUSDC": "CANTO@57", + "CATIUSDC": "CATI@143", + "CCUSDC": "CC@218", + "CELOUSDC": "CELO@144", + "CFXUSDC": "CFX@21", + "CHILLGUYUSDC": "CHILLGUY@155", + "COMPUSDC": "COMP@29", + "CRVUSDC": "CRV@16", + "CYBERUSDC": "CYBER@45", + "DOGEUSDC": "DOGE@12", + "DOODUSDC": "DOOD@194", + "DOTUSDC": "DOT@48", + "DYDXUSDC": "DYDX@4", + "DYMUSDC": "DYM@109", + "EIGENUSDC": "EIGEN@130", + "ENAUSDC": "ENA@122", + "ENSUSDC": "ENS@101", + "ETCUSDC": "ETC@102", + "ETHFIUSDC": "ETHFI@121", + "ETHUSDC": "ETH@1", + "FARTCOINUSDC": "FARTCOIN@165", + "FETUSDC": "FET@72", + "FILUSDC": "FIL@80", + "FRIENDUSDC": "FRIEND@43", + "FTMUSDC": "FTM@22", + "FTTUSDC": "FTT@51", + "FXSUSDC": "FXS@32", + "GALAUSDC": "GALA@93", + "GASUSDC": "GAS@69", + "GMTUSDC": "GMT@86", + "GMXUSDC": "GMX@23", + "GOATUSDC": "GOAT@149", + "GRASSUSDC": "GRASS@151", + "GRIFFAINUSDC": "GRIFFAIN@170", + "HBARUSDC": "HBAR@127", + "HEMIUSDC": "HEMI@211", + "HMSTRUSDC": "HMSTR@145", + "HPOSUSDC": "HPOS@33", + "HYPERUSDC": "HYPER@191", + "HYPEUSDC": "HYPE@159", + "ICPUSDC": "ICP@219", + "ILVUSDC": "ILV@83", + "IMXUSDC": "IMX@84", + "INITUSDC": "INIT@193", + "INJUSDC": "INJ@13", + "IOTAUSDC": "IOTA@157", + "IOUSDC": "IO@135", + "IPUSDC": "IP@183", + "JELLYUSDC": "JELLY@179", + "JTOUSDC": "JTO@94", + "JUPUSDC": "JUP@90", + "KAITOUSDC": "KAITO@185", + "KASUSDC": "KAS@60", + "LAUNCHCOINUSDC": "LAUNCHCOIN@195", + "LAYERUSDC": "LAYER@182", + "LDOUSDC": "LDO@17", + "LINEAUSDC": "LINEA@205", + "LINKUSDC": "LINK@18", + "LISTAUSDC": "LISTA@138", + "LOOMUSDC": "LOOM@52", + "LTCUSDC": "LTC@10", + "MANTAUSDC": "MANTA@104", + "MATICUSDC": "MATIC@3", + "MAVIAUSDC": "MAVIA@110", + "MAVUSDC": "MAV@97", + "MEGAUSDC": "MEGA@217", + "MELANIAUSDC": "MELANIA@175", + "MEMEUSDC": "MEME@75", + "MERLUSDC": "MERL@126", + "METUSDC": "MET@216", + "MEUSDC": "ME@160", + "MEWUSDC": "MEW@139", + "MINAUSDC": "MINA@67", + "MKRUSDC": "MKR@30", + "MNTUSDC": "MNT@123", + "MONUSDC": "MON@215", + "MOODENGUSDC": "MOODENG@150", + "MORPHOUSDC": "MORPHO@173", + "MOVEUSDC": "MOVE@161", + "MYROUSDC": "MYRO@118", + "NEARUSDC": "NEAR@74", + "NEIROETHUSDC": "NEIROETH@147", + "NEOUSDC": "NEO@78", + "NFTIUSDC": "NFTI@89", + "NILUSDC": "NIL@186", + "NOTUSDC": "NOT@132", + "NTRNUSDC": "NTRN@95", + "NXPCUSDC": "NXPC@196", + "OGNUSDC": "OGN@53", + "OMNIUSDC": "OMNI@129", + "OMUSDC": "OM@184", + "ONDOUSDC": "ONDO@106", + "OPUSDC": "OP@9", + "ORBSUSDC": "ORBS@61", + "ORDIUSDC": "ORDI@76", + "OXUSDC": "OX@42", + "PANDORAUSDC": "PANDORA@112", + "PAXGUSDC": "PAXG@187", + "PENDLEUSDC": "PENDLE@70", + "PENGUUSDC": "PENGU@163", + "PEOPLEUSDC": "PEOPLE@100", + "PIXELUSDC": "PIXEL@114", + "PNUTUSDC": "PNUT@153", + "POLUSDC": "POL@142", + "POLYXUSDC": "POLYX@68", + "POPCATUSDC": "POPCAT@128", + "PROMPTUSDC": "PROMPT@188", + "PROVEUSDC": "PROVE@201", + "PUMPUSDC": "PUMP@200", + "PURRUSDC": "PURR@152", + "PYTHUSDC": "PYTH@81", + "RDNTUSDC": "RDNT@54", + "RENDERUSDC": "RENDER@140", + "REQUSDC": "REQ@58", + "RESOLVUSDC": "RESOLV@198", + "REZUSDC": "REZ@131", + "RLBUSDC": "RLB@34", + "RNDRUSDC": "RNDR@20", + "RSRUSDC": "RSR@92", + "RUNEUSDC": "RUNE@41", + "SAGAUSDC": "SAGA@125", + "SANDUSDC": "SAND@156", + "SCRUSDC": "SCR@146", + "SEIUSDC": "SEI@40", + "SHIAUSDC": "SHIA@44", + "SKYUSDC": "SKY@206", + "SNXUSDC": "SNX@24", + "SOLUSDC": "SOL@5", + "SOPHUSDC": "SOPH@197", + "SPXUSDC": "SPX@171", + "STBLUSDC": "STBL@209", + "STGUSDC": "STG@71", + "STRAXUSDC": "STRAX@73", + "STRKUSDC": "STRK@113", + "STXUSDC": "STX@19", + "SUIUSDC": "SUI@14", + "SUPERUSDC": "SUPER@87", + "SUSDC": "S@172", + "SUSHIUSDC": "SUSHI@82", + "SYRUPUSDC": "SYRUP@199", + "TAOUSDC": "TAO@116", + "TIAUSDC": "TIA@63", + "TNSRUSDC": "TNSR@124", + "TONUSDC": "TON@66", + "TRBUSDC": "TRB@50", + "TRUMPUSDC": "TRUMP@174", + "TRXUSDC": "TRX@37", + "TSTUSDC": "TST@181", + "TURBOUSDC": "TURBO@133", + "UMAUSDC": "UMA@105", + "UNIBOTUSDC": "UNIBOT@35", + "UNIUSDC": "UNI@39", + "USTCUSDC": "USTC@88", + "USUALUSDC": "USUAL@164", + "VINEUSDC": "VINE@177", + "VIRTUALUSDC": "VIRTUAL@162", + "VVVUSDC": "VVV@178", + "WCTUSDC": "WCT@190", + "WIFUSDC": "WIF@98", + "WLDUSDC": "WLD@31", + "WLFIUSDC": "WLFI@204", + "WUSDC": "W@111", + "XAIUSDC": "XAI@103", + "XLMUSDC": "XLM@154", + "XPLUSDC": "XPL@203", + "XRPUSDC": "XRP@25", + "YGGUSDC": "YGG@36", + "YZYUSDC": "YZY@202", + "ZECUSDC": "ZEC@214", + "ZENUSDC": "ZEN@79", + "ZEREBROUSDC": "ZEREBRO@168", + "ZETAUSDC": "ZETA@108", + "ZKUSDC": "ZK@136", + "ZORAUSDC": "ZORA@192", + "ZROUSDC": "ZRO@46", + "kBONKUSDC": "kBONK@85", + "kDOGSUSDC": "kDOGS@141", + "kFLOKIUSDC": "kFLOKI@119", + "kLUNCUSDC": "kLUNC@91", + "kNEIROUSDC": "kNEIRO@148", + "kPEPEUSDC": "kPEPE@15", + "kSHIBUSDC": "kSHIB@38", +} + diff --git a/pkg/exchange/hyperliquid/testdata/candles.json b/pkg/exchange/hyperliquid/testdata/candles.json new file mode 100644 index 0000000000..a89cdc208f --- /dev/null +++ b/pkg/exchange/hyperliquid/testdata/candles.json @@ -0,0 +1 @@ +[{"t":1762776000000,"T":1762779599999,"s":"BTC","i":"1h","o":"106211.0","c":"105968.0","h":"106248.0","l":"105886.0","v":"736.72125","n":14946},{"t":1762779600000,"T":1762783199999,"s":"BTC","i":"1h","o":"105969.0","c":"106577.0","h":"106628.0","l":"105942.0","v":"1714.28609","n":26465},{"t":1762783200000,"T":1762786799999,"s":"BTC","i":"1h","o":"106578.0","c":"105450.0","h":"106578.0","l":"105378.0","v":"2138.28354","n":27231}] \ No newline at end of file diff --git a/pkg/exchange/hyperliquid/testdata/clearinghouseState.json b/pkg/exchange/hyperliquid/testdata/clearinghouseState.json new file mode 100644 index 0000000000..262c36377b --- /dev/null +++ b/pkg/exchange/hyperliquid/testdata/clearinghouseState.json @@ -0,0 +1 @@ +{"marginSummary":{"accountValue":"121968206.668798998","totalNtlPos":"381765315.7634580135","totalRawUsd":"497436995.8352569938","totalMarginUsed":"46255052.1990050003"},"crossMarginSummary":{"accountValue":"121968206.668798998","totalNtlPos":"381765315.7634580135","totalRawUsd":"497436995.8352569938","totalMarginUsed":"46255052.1990050003"},"crossMaintenanceMarginUsed":"12598833.5037019998","withdrawable":"75713154.4697940052","assetPositions":[{"type":"oneWay","position":{"coin":"BTC","szi":"-1186.74032","leverage":{"type":"cross","value":10},"entryPx":"111616.8","positionValue":"120498051.8718400002","unrealizedPnl":"11962126.6084020007","returnOnEquity":"0.9030734177","liquidationPx":"191751.8562150183","marginUsed":"12049805.1871840004","maxLeverage":40,"cumFunding":{"allTime":"-15758256.6353799999","sinceOpen":"-15758256.6353799999","sinceChange":"-5139.340506"}}},{"type":"oneWay","position":{"coin":"ETH","szi":"-51446.7322","leverage":{"type":"cross","value":10},"entryPx":"3527.73","positionValue":"170664244.7270599902","unrealizedPnl":"10826171.0458439998","returnOnEquity":"0.5965147526","liquidationPx":"5374.5993530082","marginUsed":"17066424.4727060013","maxLeverage":15,"cumFunding":{"allTime":"-12673932.0224830005","sinceOpen":"-12673932.0224830005","sinceChange":"-173.909597"}}},{"type":"oneWay","position":{"coin":"SOL","szi":"-65657.43","leverage":{"type":"cross","value":10},"entryPx":"194.8025","positionValue":"10274074.6464000009","unrealizedPnl":"2516158.846802","returnOnEquity":"1.9672501273","liquidationPx":"1764.5745236211","marginUsed":"1027407.46464","maxLeverage":20,"cumFunding":{"allTime":"-2691176.0796099999","sinceOpen":"-2691176.0796099999","sinceChange":"3408.28492"}}},{"type":"oneWay","position":{"coin":"DOGE","szi":"-32337.0","leverage":{"type":"cross","value":10},"entryPx":"0.237133","positionValue":"5261.55327","unrealizedPnl":"2406.623292","returnOnEquity":"3.1384557626","liquidationPx":"3102.9723282373","marginUsed":"526.155327","maxLeverage":10,"cumFunding":{"allTime":"-4554.609844","sinceOpen":"-4554.609844","sinceChange":"3.530526"}}},{"type":"oneWay","position":{"coin":"INJ","szi":"-36877.3","leverage":{"type":"cross","value":3},"entryPx":"13.05957","positionValue":"238231.04573","unrealizedPnl":"243370.71087","returnOnEquity":"1.5160080349","liquidationPx":"2706.0134249405","marginUsed":"79410.348576","maxLeverage":10,"cumFunding":{"allTime":"-553.225455","sinceOpen":"-553.225455","sinceChange":"419.824208"}}},{"type":"oneWay","position":{"coin":"SUI","szi":"-737411.0","leverage":{"type":"cross","value":10},"entryPx":"3.87517","positionValue":"1465678.1036","unrealizedPnl":"1391921.5109590001","returnOnEquity":"4.870946594","liquidationPx":"137.9622085351","marginUsed":"146567.81036","maxLeverage":10,"cumFunding":{"allTime":"-117677.90807","sinceOpen":"-117677.90807","sinceChange":"-140.793879"}}},{"type":"oneWay","position":{"coin":"HYPE","szi":"-1630802.6899999999","leverage":{"type":"cross","value":5},"entryPx":"42.1486","positionValue":"64070976.0847200006","unrealizedPnl":"4665162.9988240004","returnOnEquity":"0.3393529998","liquidationPx":"100.2559536875","marginUsed":"12814195.2169439998","maxLeverage":5,"cumFunding":{"allTime":"-5104958.9617560003","sinceOpen":"-5104958.9617560003","sinceChange":"-3166.631505"}}},{"type":"oneWay","position":{"coin":"FARTCOIN","szi":"-9797563.6999999993","leverage":{"type":"cross","value":10},"entryPx":"0.73591","positionValue":"2588124.4269920001","unrealizedPnl":"4622046.2150400002","returnOnEquity":"6.4104532951","liquidationPx":"10.4930449387","marginUsed":"258812.442699","maxLeverage":10,"cumFunding":{"allTime":"-150051.788229","sinceOpen":"-135276.823358","sinceChange":"-1895.874993"}}},{"type":"oneWay","position":{"coin":"PUMP","szi":"-1005077786.0","leverage":{"type":"cross","value":5},"entryPx":"0.006521","positionValue":"3754970.6084960001","unrealizedPnl":"2799724.5257410002","returnOnEquity":"2.1356634202","liquidationPx":"0.1033950659","marginUsed":"750994.121699","maxLeverage":10,"cumFunding":{"allTime":"-246376.25479","sinceOpen":"-246376.25479","sinceChange":"-280.203121"}}},{"type":"oneWay","position":{"coin":"XPL","szi":"-19501945.0","leverage":{"type":"cross","value":5},"entryPx":"0.794719","positionValue":"5057439.3968500001","unrealizedPnl":"10441131.331487","returnOnEquity":"3.3684174865","liquidationPx":"5.3576267533","marginUsed":"1011487.87937","maxLeverage":5,"cumFunding":{"allTime":"-1182499.1109480001","sinceOpen":"-929085.984461","sinceChange":"-2961.540967"}}},{"type":"oneWay","position":{"coin":"ASTER","szi":"3058645.0","leverage":{"type":"cross","value":3},"entryPx":"0.968318","positionValue":"3148263.2985","unrealizedPnl":"186520.393275","returnOnEquity":"0.1889296937","liquidationPx":null,"marginUsed":"1049421.0995","maxLeverage":5,"cumFunding":{"allTime":"-563012.138468","sinceOpen":"-5220.295642","sinceChange":"-199.771919"}}}],"time":1762478386421} \ No newline at end of file diff --git a/pkg/exchange/hyperliquid/testdata/frontendOpenOrders.json b/pkg/exchange/hyperliquid/testdata/frontendOpenOrders.json new file mode 100644 index 0000000000..60148a5b71 --- /dev/null +++ b/pkg/exchange/hyperliquid/testdata/frontendOpenOrders.json @@ -0,0 +1 @@ +[{"coin":"BTC","side":"B","limitPx":"94777.0","sz":"0.10543","oid":235572566321,"timestamp":1763167879930,"triggerCondition":"N/A","isTrigger":false,"triggerPx":"0.0","children":[],"isPositionTpsl":false,"reduceOnly":false,"orderType":"Limit","origSz":"0.10543","tif":"Gtc","cloid":"0x00000000000000000000000637000698"},{"coin":"WLFI","side":"B","limitPx":"0.10447","sz":"2624.0","oid":194029229960,"timestamp":1760131688558,"triggerCondition":"N/A","isTrigger":false,"triggerPx":"0.0","children":[],"isPositionTpsl":false,"reduceOnly":false,"orderType":"Limit","origSz":"12760.0","tif":"Gtc","cloid":"0x00000000000000000000001261000016"}] \ No newline at end of file diff --git a/pkg/exchange/hyperliquid/testdata/perpsMeta.json b/pkg/exchange/hyperliquid/testdata/perpsMeta.json new file mode 100644 index 0000000000..2bac10d127 --- /dev/null +++ b/pkg/exchange/hyperliquid/testdata/perpsMeta.json @@ -0,0 +1 @@ +{"universe":[{"szDecimals":5,"name":"BTC","maxLeverage":40,"marginTableId":56},{"szDecimals":4,"name":"ETH","maxLeverage":25,"marginTableId":55},{"szDecimals":2,"name":"ATOM","maxLeverage":5,"marginTableId":5},{"szDecimals":1,"name":"MATIC","maxLeverage":20,"marginTableId":20,"isDelisted":true},{"szDecimals":1,"name":"DYDX","maxLeverage":5,"marginTableId":5},{"szDecimals":2,"name":"SOL","maxLeverage":20,"marginTableId":54},{"szDecimals":2,"name":"AVAX","maxLeverage":10,"marginTableId":52},{"szDecimals":3,"name":"BNB","maxLeverage":10,"marginTableId":51},{"szDecimals":1,"name":"APE","maxLeverage":5,"marginTableId":5},{"szDecimals":1,"name":"OP","maxLeverage":10,"marginTableId":51},{"szDecimals":2,"name":"LTC","maxLeverage":10,"marginTableId":52},{"szDecimals":1,"name":"ARB","maxLeverage":10,"marginTableId":51},{"szDecimals":0,"name":"DOGE","maxLeverage":10,"marginTableId":52},{"szDecimals":1,"name":"INJ","maxLeverage":10,"marginTableId":51},{"szDecimals":1,"name":"SUI","maxLeverage":10,"marginTableId":52},{"szDecimals":0,"name":"kPEPE","maxLeverage":10,"marginTableId":52},{"szDecimals":1,"name":"CRV","maxLeverage":10,"marginTableId":52},{"szDecimals":1,"name":"LDO","maxLeverage":10,"marginTableId":51},{"szDecimals":1,"name":"LINK","maxLeverage":10,"marginTableId":52},{"szDecimals":1,"name":"STX","maxLeverage":5,"marginTableId":5},{"szDecimals":1,"name":"RNDR","maxLeverage":20,"marginTableId":20,"isDelisted":true},{"szDecimals":0,"name":"CFX","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"FTM","maxLeverage":10,"marginTableId":10,"isDelisted":true},{"szDecimals":2,"name":"GMX","maxLeverage":5,"marginTableId":5},{"szDecimals":1,"name":"SNX","maxLeverage":3,"marginTableId":3},{"szDecimals":0,"name":"XRP","maxLeverage":20,"marginTableId":53},{"szDecimals":3,"name":"BCH","maxLeverage":10,"marginTableId":52},{"szDecimals":2,"name":"APT","maxLeverage":10,"marginTableId":52},{"szDecimals":2,"name":"AAVE","maxLeverage":10,"marginTableId":52},{"szDecimals":2,"name":"COMP","maxLeverage":5,"marginTableId":5},{"szDecimals":4,"name":"MKR","maxLeverage":10,"marginTableId":51,"isDelisted":true},{"szDecimals":1,"name":"WLD","maxLeverage":10,"marginTableId":52},{"szDecimals":1,"name":"FXS","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"HPOS","maxLeverage":3,"marginTableId":3,"onlyIsolated":true,"isDelisted":true},{"szDecimals":0,"name":"RLB","maxLeverage":3,"marginTableId":3,"onlyIsolated":true,"isDelisted":true},{"szDecimals":3,"name":"UNIBOT","maxLeverage":3,"marginTableId":3,"onlyIsolated":true,"isDelisted":true},{"szDecimals":0,"name":"YGG","maxLeverage":3,"marginTableId":3},{"szDecimals":0,"name":"TRX","maxLeverage":10,"marginTableId":51},{"szDecimals":0,"name":"kSHIB","maxLeverage":10,"marginTableId":51},{"szDecimals":1,"name":"UNI","maxLeverage":10,"marginTableId":52},{"szDecimals":0,"name":"SEI","maxLeverage":10,"marginTableId":51},{"szDecimals":1,"name":"RUNE","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"OX","maxLeverage":3,"marginTableId":3,"onlyIsolated":true,"isDelisted":true},{"szDecimals":1,"name":"FRIEND","maxLeverage":3,"marginTableId":3,"onlyIsolated":true,"isDelisted":true},{"szDecimals":0,"name":"SHIA","maxLeverage":3,"marginTableId":3,"onlyIsolated":true,"isDelisted":true},{"szDecimals":1,"name":"CYBER","maxLeverage":3,"marginTableId":3,"isDelisted":true},{"szDecimals":1,"name":"ZRO","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"BLZ","maxLeverage":5,"marginTableId":5,"isDelisted":true},{"szDecimals":1,"name":"DOT","maxLeverage":10,"marginTableId":51},{"szDecimals":1,"name":"BANANA","maxLeverage":3,"marginTableId":3},{"szDecimals":2,"name":"TRB","maxLeverage":3,"marginTableId":3},{"szDecimals":1,"name":"FTT","maxLeverage":3,"marginTableId":3},{"szDecimals":0,"name":"LOOM","maxLeverage":10,"marginTableId":10,"isDelisted":true},{"szDecimals":0,"name":"OGN","maxLeverage":3,"marginTableId":3,"isDelisted":true},{"szDecimals":0,"name":"RDNT","maxLeverage":5,"marginTableId":5,"isDelisted":true},{"szDecimals":0,"name":"ARK","maxLeverage":3,"marginTableId":3},{"szDecimals":0,"name":"BNT","maxLeverage":3,"marginTableId":3,"isDelisted":true},{"szDecimals":0,"name":"CANTO","maxLeverage":5,"marginTableId":5,"isDelisted":true},{"szDecimals":0,"name":"REQ","maxLeverage":3,"marginTableId":3,"isDelisted":true},{"szDecimals":0,"name":"BIGTIME","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"KAS","maxLeverage":3,"marginTableId":3},{"szDecimals":0,"name":"ORBS","maxLeverage":3,"marginTableId":3,"isDelisted":true},{"szDecimals":0,"name":"BLUR","maxLeverage":5,"marginTableId":5},{"szDecimals":1,"name":"TIA","maxLeverage":10,"marginTableId":52},{"szDecimals":2,"name":"BSV","maxLeverage":3,"marginTableId":3},{"szDecimals":0,"name":"ADA","maxLeverage":10,"marginTableId":52},{"szDecimals":1,"name":"TON","maxLeverage":10,"marginTableId":51},{"szDecimals":0,"name":"MINA","maxLeverage":3,"marginTableId":3},{"szDecimals":0,"name":"POLYX","maxLeverage":3,"marginTableId":3},{"szDecimals":1,"name":"GAS","maxLeverage":3,"marginTableId":3},{"szDecimals":0,"name":"PENDLE","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"STG","maxLeverage":3,"marginTableId":3,"isDelisted":true},{"szDecimals":0,"name":"FET","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"STRAX","maxLeverage":10,"marginTableId":10,"isDelisted":true},{"szDecimals":1,"name":"NEAR","maxLeverage":10,"marginTableId":52},{"szDecimals":0,"name":"MEME","maxLeverage":5,"marginTableId":5},{"szDecimals":2,"name":"ORDI","maxLeverage":5,"marginTableId":5},{"szDecimals":1,"name":"BADGER","maxLeverage":5,"marginTableId":5,"isDelisted":true},{"szDecimals":2,"name":"NEO","maxLeverage":5,"marginTableId":5},{"szDecimals":2,"name":"ZEN","maxLeverage":5,"marginTableId":5},{"szDecimals":1,"name":"FIL","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"PYTH","maxLeverage":5,"marginTableId":5},{"szDecimals":1,"name":"SUSHI","maxLeverage":5,"marginTableId":5},{"szDecimals":2,"name":"ILV","maxLeverage":3,"marginTableId":3,"isDelisted":true},{"szDecimals":1,"name":"IMX","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"kBONK","maxLeverage":10,"marginTableId":52},{"szDecimals":0,"name":"GMT","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"SUPER","maxLeverage":3,"marginTableId":3},{"szDecimals":0,"name":"USTC","maxLeverage":3,"marginTableId":3},{"szDecimals":1,"name":"NFTI","maxLeverage":3,"marginTableId":3,"onlyIsolated":true,"isDelisted":true},{"szDecimals":0,"name":"JUP","maxLeverage":10,"marginTableId":51},{"szDecimals":0,"name":"kLUNC","maxLeverage":3,"marginTableId":3},{"szDecimals":0,"name":"RSR","maxLeverage":3,"marginTableId":3},{"szDecimals":0,"name":"GALA","maxLeverage":3,"marginTableId":3},{"szDecimals":0,"name":"JTO","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"NTRN","maxLeverage":3,"marginTableId":3,"isDelisted":true},{"szDecimals":2,"name":"ACE","maxLeverage":3,"marginTableId":3},{"szDecimals":0,"name":"MAV","maxLeverage":3,"marginTableId":3},{"szDecimals":0,"name":"WIF","maxLeverage":10,"marginTableId":52},{"szDecimals":1,"name":"CAKE","maxLeverage":3,"marginTableId":3},{"szDecimals":0,"name":"PEOPLE","maxLeverage":3,"marginTableId":3},{"szDecimals":2,"name":"ENS","maxLeverage":5,"marginTableId":5},{"szDecimals":2,"name":"ETC","maxLeverage":5,"marginTableId":5},{"szDecimals":1,"name":"XAI","maxLeverage":3,"marginTableId":3},{"szDecimals":1,"name":"MANTA","maxLeverage":3,"marginTableId":3},{"szDecimals":1,"name":"UMA","maxLeverage":3,"marginTableId":3},{"szDecimals":0,"name":"ONDO","maxLeverage":10,"marginTableId":51},{"szDecimals":0,"name":"ALT","maxLeverage":5,"marginTableId":5},{"szDecimals":1,"name":"ZETA","maxLeverage":3,"marginTableId":3},{"szDecimals":1,"name":"DYM","maxLeverage":3,"marginTableId":3},{"szDecimals":1,"name":"MAVIA","maxLeverage":3,"marginTableId":3},{"szDecimals":1,"name":"W","maxLeverage":5,"marginTableId":5},{"szDecimals":5,"name":"PANDORA","maxLeverage":3,"marginTableId":3,"onlyIsolated":true,"isDelisted":true},{"szDecimals":1,"name":"STRK","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"PIXEL","maxLeverage":3,"marginTableId":3,"isDelisted":true},{"szDecimals":1,"name":"AI","maxLeverage":3,"marginTableId":3,"isDelisted":true},{"szDecimals":3,"name":"TAO","maxLeverage":5,"marginTableId":5},{"szDecimals":2,"name":"AR","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"MYRO","maxLeverage":3,"marginTableId":3,"isDelisted":true},{"szDecimals":0,"name":"kFLOKI","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"BOME","maxLeverage":3,"marginTableId":3},{"szDecimals":1,"name":"ETHFI","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"ENA","maxLeverage":10,"marginTableId":52},{"szDecimals":1,"name":"MNT","maxLeverage":5,"marginTableId":5},{"szDecimals":1,"name":"TNSR","maxLeverage":3,"marginTableId":3},{"szDecimals":1,"name":"SAGA","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"MERL","maxLeverage":3,"marginTableId":3},{"szDecimals":0,"name":"HBAR","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"POPCAT","maxLeverage":10,"marginTableId":52},{"szDecimals":2,"name":"OMNI","maxLeverage":3,"marginTableId":3,"isDelisted":true},{"szDecimals":2,"name":"EIGEN","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"REZ","maxLeverage":3,"marginTableId":3},{"szDecimals":0,"name":"NOT","maxLeverage":3,"marginTableId":3},{"szDecimals":0,"name":"TURBO","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"BRETT","maxLeverage":5,"marginTableId":5},{"szDecimals":1,"name":"IO","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"ZK","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"BLAST","maxLeverage":3,"marginTableId":3},{"szDecimals":0,"name":"LISTA","maxLeverage":3,"marginTableId":3,"isDelisted":true},{"szDecimals":0,"name":"MEW","maxLeverage":5,"marginTableId":5},{"szDecimals":1,"name":"RENDER","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"kDOGS","maxLeverage":3,"marginTableId":3,"isDelisted":true},{"szDecimals":0,"name":"POL","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"CATI","maxLeverage":3,"marginTableId":3,"isDelisted":true},{"szDecimals":0,"name":"CELO","maxLeverage":3,"marginTableId":3},{"szDecimals":0,"name":"HMSTR","maxLeverage":3,"marginTableId":3},{"szDecimals":1,"name":"SCR","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"NEIROETH","maxLeverage":5,"marginTableId":5,"isDelisted":true},{"szDecimals":1,"name":"kNEIRO","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"GOAT","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"MOODENG","maxLeverage":5,"marginTableId":5},{"szDecimals":1,"name":"GRASS","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"PURR","maxLeverage":3,"marginTableId":3},{"szDecimals":1,"name":"PNUT","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"XLM","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"CHILLGUY","maxLeverage":3,"marginTableId":3},{"szDecimals":0,"name":"SAND","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"IOTA","maxLeverage":3,"marginTableId":3},{"szDecimals":0,"name":"ALGO","maxLeverage":5,"marginTableId":5},{"szDecimals":2,"name":"HYPE","maxLeverage":10,"marginTableId":52},{"szDecimals":1,"name":"ME","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"MOVE","maxLeverage":5,"marginTableId":5},{"szDecimals":1,"name":"VIRTUAL","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"PENGU","maxLeverage":5,"marginTableId":5},{"szDecimals":1,"name":"USUAL","maxLeverage":5,"marginTableId":5},{"szDecimals":1,"name":"FARTCOIN","maxLeverage":10,"marginTableId":52},{"szDecimals":1,"name":"AI16Z","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"AIXBT","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"ZEREBRO","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"BIO","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"GRIFFAIN","maxLeverage":5,"marginTableId":5},{"szDecimals":1,"name":"SPX","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"S","maxLeverage":5,"marginTableId":5},{"szDecimals":1,"name":"MORPHO","maxLeverage":5,"marginTableId":5},{"szDecimals":1,"name":"TRUMP","maxLeverage":10,"marginTableId":52},{"szDecimals":1,"name":"MELANIA","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"ANIME","maxLeverage":3,"marginTableId":3},{"szDecimals":0,"name":"VINE","maxLeverage":5,"marginTableId":5},{"szDecimals":2,"name":"VVV","maxLeverage":3,"marginTableId":3},{"szDecimals":0,"name":"JELLY","maxLeverage":3,"marginTableId":3,"isDelisted":true},{"szDecimals":1,"name":"BERA","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"TST","maxLeverage":3,"marginTableId":3},{"szDecimals":0,"name":"LAYER","maxLeverage":3,"marginTableId":3},{"szDecimals":1,"name":"IP","maxLeverage":3,"marginTableId":3},{"szDecimals":1,"name":"OM","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"KAITO","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"NIL","maxLeverage":3,"marginTableId":3},{"szDecimals":3,"name":"PAXG","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"PROMPT","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"BABY","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"WCT","maxLeverage":3,"marginTableId":3},{"szDecimals":0,"name":"HYPER","maxLeverage":3,"marginTableId":3},{"szDecimals":0,"name":"ZORA","maxLeverage":3,"marginTableId":3},{"szDecimals":0,"name":"INIT","maxLeverage":3,"marginTableId":3},{"szDecimals":0,"name":"DOOD","maxLeverage":3,"marginTableId":3},{"szDecimals":0,"name":"LAUNCHCOIN","maxLeverage":3,"marginTableId":3},{"szDecimals":0,"name":"NXPC","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"SOPH","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"RESOLV","maxLeverage":3,"marginTableId":3},{"szDecimals":0,"name":"SYRUP","maxLeverage":3,"marginTableId":3},{"szDecimals":0,"name":"PUMP","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"PROVE","maxLeverage":3,"marginTableId":3},{"szDecimals":0,"name":"YZY","maxLeverage":3,"marginTableId":3},{"szDecimals":0,"name":"XPL","maxLeverage":10,"marginTableId":51},{"szDecimals":0,"name":"WLFI","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"LINEA","maxLeverage":3,"marginTableId":3},{"szDecimals":0,"name":"SKY","maxLeverage":3,"marginTableId":3},{"szDecimals":0,"name":"ASTER","maxLeverage":3,"marginTableId":3},{"szDecimals":0,"name":"AVNT","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"STBL","maxLeverage":3,"marginTableId":3},{"szDecimals":0,"name":"0G","maxLeverage":3,"marginTableId":3},{"szDecimals":0,"name":"HEMI","maxLeverage":3,"marginTableId":3},{"szDecimals":0,"name":"APEX","maxLeverage":3,"marginTableId":3},{"szDecimals":0,"name":"2Z","maxLeverage":3,"marginTableId":3},{"szDecimals":2,"name":"ZEC","maxLeverage":5,"marginTableId":5},{"szDecimals":0,"name":"MON","maxLeverage":3,"marginTableId":3,"onlyIsolated":true},{"szDecimals":0,"name":"MET","maxLeverage":3,"marginTableId":3,"onlyIsolated":true}],"marginTables":[[50,{"description":"","marginTiers":[{"lowerBound":"0.0","maxLeverage":50}]}],[51,{"description":"tiered 10x","marginTiers":[{"lowerBound":"0.0","maxLeverage":10},{"lowerBound":"3000000.0","maxLeverage":5}]}],[52,{"description":"tiered 10x (2)","marginTiers":[{"lowerBound":"0.0","maxLeverage":10},{"lowerBound":"20000000.0","maxLeverage":5}]}],[53,{"description":"tiered 20x","marginTiers":[{"lowerBound":"0.0","maxLeverage":20},{"lowerBound":"40000000.0","maxLeverage":10}]}],[54,{"description":"tiered 20x (2)","marginTiers":[{"lowerBound":"0.0","maxLeverage":20},{"lowerBound":"70000000.0","maxLeverage":10}]}],[55,{"description":"tiered 25x","marginTiers":[{"lowerBound":"0.0","maxLeverage":25},{"lowerBound":"100000000.0","maxLeverage":15}]}],[56,{"description":"tiered 40x","marginTiers":[{"lowerBound":"0.0","maxLeverage":40},{"lowerBound":"150000000.0","maxLeverage":20}]}]],"collateralToken":0} \ No newline at end of file diff --git a/pkg/exchange/hyperliquid/testdata/spotClearinghouseState.json b/pkg/exchange/hyperliquid/testdata/spotClearinghouseState.json new file mode 100644 index 0000000000..383f0197ae --- /dev/null +++ b/pkg/exchange/hyperliquid/testdata/spotClearinghouseState.json @@ -0,0 +1 @@ +{"balances":[{"coin":"USDC","token":0,"total":"1808443.14193129","hold":"0.0","entryNtl":"0.0"},{"coin":"HYPE","token":150,"total":"503808.46025243","hold":"30.0","entryNtl":"22408631.1612854116"},{"coin":"UBTC","token":197,"total":"0.0","hold":"0.0","entryNtl":"0.0"},{"coin":"LATINA","token":223,"total":"45658.0657","hold":"0.0","entryNtl":"0.0"},{"coin":"FUND","token":232,"total":"18.69","hold":"0.0","entryNtl":"14.0446005"},{"coin":"USDT0","token":268,"total":"7.99944001","hold":"0.0","entryNtl":"8.01251937"},{"coin":"UFART","token":269,"total":"9818483.1515529994","hold":"0.0","entryNtl":"7230474.5692744898"},{"coin":"LICKO","token":307,"total":"35585.49249208","hold":"0.0","entryNtl":"0.21351295"}]} \ No newline at end of file diff --git a/pkg/exchange/hyperliquid/testdata/spotMeta.json b/pkg/exchange/hyperliquid/testdata/spotMeta.json new file mode 100644 index 0000000000..869539a63e --- /dev/null +++ b/pkg/exchange/hyperliquid/testdata/spotMeta.json @@ -0,0 +1 @@ +{"universe":[{"tokens":[1,0],"name":"PURR/USDC","index":0,"isCanonical":true},{"tokens":[2,0],"name":"@1","index":1,"isCanonical":false},{"tokens":[3,0],"name":"@2","index":2,"isCanonical":false},{"tokens":[4,0],"name":"@3","index":3,"isCanonical":false},{"tokens":[5,0],"name":"@4","index":4,"isCanonical":false},{"tokens":[6,0],"name":"@5","index":5,"isCanonical":false},{"tokens":[7,0],"name":"@6","index":6,"isCanonical":false},{"tokens":[8,0],"name":"@7","index":7,"isCanonical":false},{"tokens":[9,0],"name":"@8","index":8,"isCanonical":false},{"tokens":[10,0],"name":"@9","index":9,"isCanonical":false},{"tokens":[11,0],"name":"@10","index":10,"isCanonical":false},{"tokens":[12,0],"name":"@11","index":11,"isCanonical":false},{"tokens":[13,0],"name":"@12","index":12,"isCanonical":false},{"tokens":[14,0],"name":"@13","index":13,"isCanonical":false},{"tokens":[15,0],"name":"@14","index":14,"isCanonical":false},{"tokens":[16,0],"name":"@15","index":15,"isCanonical":false},{"tokens":[17,0],"name":"@16","index":16,"isCanonical":false},{"tokens":[18,0],"name":"@17","index":17,"isCanonical":false},{"tokens":[19,0],"name":"@18","index":18,"isCanonical":false},{"tokens":[20,0],"name":"@19","index":19,"isCanonical":false},{"tokens":[26,0],"name":"@20","index":20,"isCanonical":false},{"tokens":[24,0],"name":"@21","index":21,"isCanonical":false},{"tokens":[29,0],"name":"@22","index":22,"isCanonical":false},{"tokens":[27,0],"name":"@23","index":23,"isCanonical":false},{"tokens":[30,0],"name":"@24","index":24,"isCanonical":false},{"tokens":[23,0],"name":"@25","index":25,"isCanonical":false},{"tokens":[28,0],"name":"@26","index":26,"isCanonical":false},{"tokens":[31,0],"name":"@27","index":27,"isCanonical":false},{"tokens":[22,0],"name":"@28","index":28,"isCanonical":false},{"tokens":[34,0],"name":"@29","index":29,"isCanonical":false},{"tokens":[32,0],"name":"@30","index":30,"isCanonical":false},{"tokens":[35,0],"name":"@31","index":31,"isCanonical":false},{"tokens":[33,0],"name":"@32","index":32,"isCanonical":false},{"tokens":[37,0],"name":"@33","index":33,"isCanonical":false},{"tokens":[38,0],"name":"@34","index":34,"isCanonical":false},{"tokens":[25,0],"name":"@35","index":35,"isCanonical":false},{"tokens":[40,0],"name":"@36","index":36,"isCanonical":false},{"tokens":[42,0],"name":"@37","index":37,"isCanonical":false},{"tokens":[36,0],"name":"@38","index":38,"isCanonical":false},{"tokens":[45,0],"name":"@39","index":39,"isCanonical":false},{"tokens":[46,0],"name":"@40","index":40,"isCanonical":false},{"tokens":[21,0],"name":"@41","index":41,"isCanonical":false},{"tokens":[47,0],"name":"@42","index":42,"isCanonical":false},{"tokens":[52,0],"name":"@43","index":43,"isCanonical":false},{"tokens":[49,0],"name":"@44","index":44,"isCanonical":false},{"tokens":[55,0],"name":"@45","index":45,"isCanonical":false},{"tokens":[56,0],"name":"@46","index":46,"isCanonical":false},{"tokens":[50,0],"name":"@47","index":47,"isCanonical":false},{"tokens":[59,0],"name":"@48","index":48,"isCanonical":false},{"tokens":[39,0],"name":"@49","index":49,"isCanonical":false},{"tokens":[63,0],"name":"@50","index":50,"isCanonical":false},{"tokens":[64,0],"name":"@51","index":51,"isCanonical":false},{"tokens":[66,0],"name":"@52","index":52,"isCanonical":false},{"tokens":[58,0],"name":"@53","index":53,"isCanonical":false},{"tokens":[67,0],"name":"@54","index":54,"isCanonical":false},{"tokens":[62,0],"name":"@55","index":55,"isCanonical":false},{"tokens":[69,0],"name":"@56","index":56,"isCanonical":false},{"tokens":[72,0],"name":"@57","index":57,"isCanonical":false},{"tokens":[75,0],"name":"@58","index":58,"isCanonical":false},{"tokens":[76,0],"name":"@59","index":59,"isCanonical":false},{"tokens":[77,0],"name":"@60","index":60,"isCanonical":false},{"tokens":[74,0],"name":"@61","index":61,"isCanonical":false},{"tokens":[84,0],"name":"@62","index":62,"isCanonical":false},{"tokens":[92,0],"name":"@63","index":63,"isCanonical":false},{"tokens":[93,0],"name":"@64","index":64,"isCanonical":false},{"tokens":[94,0],"name":"@65","index":65,"isCanonical":false},{"tokens":[97,0],"name":"@66","index":66,"isCanonical":false},{"tokens":[99,0],"name":"@67","index":67,"isCanonical":false},{"tokens":[100,0],"name":"@68","index":68,"isCanonical":false},{"tokens":[101,0],"name":"@69","index":69,"isCanonical":false},{"tokens":[102,0],"name":"@70","index":70,"isCanonical":false},{"tokens":[105,0],"name":"@72","index":72,"isCanonical":false},{"tokens":[107,0],"name":"@73","index":73,"isCanonical":false},{"tokens":[53,0],"name":"@74","index":74,"isCanonical":false},{"tokens":[114,0],"name":"@75","index":75,"isCanonical":false},{"tokens":[110,0],"name":"@76","index":76,"isCanonical":false},{"tokens":[113,0],"name":"@77","index":77,"isCanonical":false},{"tokens":[118,0],"name":"@78","index":78,"isCanonical":false},{"tokens":[123,0],"name":"@79","index":79,"isCanonical":false},{"tokens":[112,0],"name":"@80","index":80,"isCanonical":false},{"tokens":[122,0],"name":"@81","index":81,"isCanonical":false},{"tokens":[125,0],"name":"@82","index":82,"isCanonical":false},{"tokens":[126,0],"name":"@83","index":83,"isCanonical":false},{"tokens":[104,0],"name":"@84","index":84,"isCanonical":false},{"tokens":[127,0],"name":"@85","index":85,"isCanonical":false},{"tokens":[129,0],"name":"@86","index":86,"isCanonical":false},{"tokens":[130,0],"name":"@87","index":87,"isCanonical":false},{"tokens":[128,0],"name":"@88","index":88,"isCanonical":false},{"tokens":[131,0],"name":"@89","index":89,"isCanonical":false},{"tokens":[132,0],"name":"@90","index":90,"isCanonical":false},{"tokens":[134,0],"name":"@91","index":91,"isCanonical":false},{"tokens":[135,0],"name":"@92","index":92,"isCanonical":false},{"tokens":[133,0],"name":"@93","index":93,"isCanonical":false},{"tokens":[136,0],"name":"@94","index":94,"isCanonical":false},{"tokens":[51,0],"name":"@95","index":95,"isCanonical":false},{"tokens":[137,0],"name":"@96","index":96,"isCanonical":false},{"tokens":[138,0],"name":"@97","index":97,"isCanonical":false},{"tokens":[141,0],"name":"@99","index":99,"isCanonical":false},{"tokens":[115,0],"name":"@100","index":100,"isCanonical":false},{"tokens":[142,0],"name":"@101","index":101,"isCanonical":false},{"tokens":[144,0],"name":"@102","index":102,"isCanonical":false},{"tokens":[143,0],"name":"@103","index":103,"isCanonical":false},{"tokens":[147,0],"name":"@104","index":104,"isCanonical":false},{"tokens":[148,0],"name":"@105","index":105,"isCanonical":false},{"tokens":[149,0],"name":"@106","index":106,"isCanonical":false},{"tokens":[150,0],"name":"@107","index":107,"isCanonical":false},{"tokens":[139,0],"name":"@108","index":108,"isCanonical":false},{"tokens":[98,0],"name":"@109","index":109,"isCanonical":false},{"tokens":[151,0],"name":"@110","index":110,"isCanonical":false},{"tokens":[152,0],"name":"@111","index":111,"isCanonical":false},{"tokens":[61,0],"name":"@112","index":112,"isCanonical":false},{"tokens":[78,0],"name":"@113","index":113,"isCanonical":false},{"tokens":[71,0],"name":"@114","index":114,"isCanonical":false},{"tokens":[153,0],"name":"@115","index":115,"isCanonical":false},{"tokens":[146,0],"name":"@116","index":116,"isCanonical":false},{"tokens":[80,0],"name":"@117","index":117,"isCanonical":false},{"tokens":[156,0],"name":"@118","index":118,"isCanonical":false},{"tokens":[158,0],"name":"@119","index":119,"isCanonical":false},{"tokens":[103,0],"name":"@120","index":120,"isCanonical":false},{"tokens":[159,0],"name":"@121","index":121,"isCanonical":false},{"tokens":[95,0],"name":"@122","index":122,"isCanonical":false},{"tokens":[161,0],"name":"@123","index":123,"isCanonical":false},{"tokens":[162,0],"name":"@124","index":124,"isCanonical":false},{"tokens":[106,0],"name":"@125","index":125,"isCanonical":false},{"tokens":[48,0],"name":"@126","index":126,"isCanonical":false},{"tokens":[168,0],"name":"@127","index":127,"isCanonical":false},{"tokens":[174,0],"name":"@128","index":128,"isCanonical":false},{"tokens":[164,0],"name":"@129","index":129,"isCanonical":false},{"tokens":[176,0],"name":"@130","index":130,"isCanonical":false},{"tokens":[177,0],"name":"@131","index":131,"isCanonical":false},{"tokens":[178,0],"name":"@132","index":132,"isCanonical":false},{"tokens":[109,0],"name":"@133","index":133,"isCanonical":false},{"tokens":[154,0],"name":"@134","index":134,"isCanonical":false},{"tokens":[186,0],"name":"@135","index":135,"isCanonical":false},{"tokens":[157,0],"name":"@136","index":136,"isCanonical":false},{"tokens":[187,0],"name":"@137","index":137,"isCanonical":false},{"tokens":[79,0],"name":"@138","index":138,"isCanonical":false},{"tokens":[163,0],"name":"@139","index":139,"isCanonical":false},{"tokens":[189,0],"name":"@140","index":140,"isCanonical":false},{"tokens":[165,0],"name":"@141","index":141,"isCanonical":false},{"tokens":[197,0],"name":"@142","index":142,"isCanonical":false},{"tokens":[199,0],"name":"@143","index":143,"isCanonical":false},{"tokens":[204,0],"name":"@145","index":145,"isCanonical":false},{"tokens":[206,0],"name":"@146","index":146,"isCanonical":false},{"tokens":[196,0],"name":"@147","index":147,"isCanonical":false},{"tokens":[217,0],"name":"@148","index":148,"isCanonical":false},{"tokens":[235,0],"name":"@150","index":150,"isCanonical":false},{"tokens":[221,0],"name":"@151","index":151,"isCanonical":false},{"tokens":[239,0],"name":"@152","index":152,"isCanonical":false},{"tokens":[241,0],"name":"@153","index":153,"isCanonical":false},{"tokens":[251,0],"name":"@155","index":155,"isCanonical":false},{"tokens":[254,0],"name":"@156","index":156,"isCanonical":false},{"tokens":[253,0],"name":"@157","index":157,"isCanonical":false},{"tokens":[209,0],"name":"@158","index":158,"isCanonical":false},{"tokens":[257,0],"name":"@159","index":159,"isCanonical":false},{"tokens":[222,0],"name":"@160","index":160,"isCanonical":false},{"tokens":[202,0],"name":"@161","index":161,"isCanonical":false},{"tokens":[269,0],"name":"@162","index":162,"isCanonical":false},{"tokens":[232,0],"name":"@163","index":163,"isCanonical":false},{"tokens":[271,0],"name":"@164","index":164,"isCanonical":false},{"tokens":[272,0],"name":"@165","index":165,"isCanonical":false},{"tokens":[268,0],"name":"@166","index":166,"isCanonical":false},{"tokens":[192,0],"name":"@170","index":170,"isCanonical":false},{"tokens":[240,0],"name":"@171","index":171,"isCanonical":false},{"tokens":[242,0],"name":"@172","index":172,"isCanonical":false},{"tokens":[277,0],"name":"@173","index":173,"isCanonical":false},{"tokens":[218,0],"name":"@174","index":174,"isCanonical":false},{"tokens":[65,0],"name":"@175","index":175,"isCanonical":false},{"tokens":[289,0],"name":"@176","index":176,"isCanonical":false},{"tokens":[255,0],"name":"@177","index":177,"isCanonical":false},{"tokens":[248,0],"name":"@178","index":178,"isCanonical":false},{"tokens":[219,0],"name":"@179","index":179,"isCanonical":false},{"tokens":[291,0],"name":"@180","index":180,"isCanonical":false},{"tokens":[297,0],"name":"@182","index":182,"isCanonical":false},{"tokens":[220,0],"name":"@183","index":183,"isCanonical":false},{"tokens":[292,0],"name":"@184","index":184,"isCanonical":false},{"tokens":[183,0],"name":"@185","index":185,"isCanonical":false},{"tokens":[307,0],"name":"@187","index":187,"isCanonical":false},{"tokens":[299,0],"name":"@188","index":188,"isCanonical":false},{"tokens":[312,0],"name":"@189","index":189,"isCanonical":false},{"tokens":[304,0],"name":"@190","index":190,"isCanonical":false},{"tokens":[281,0],"name":"@191","index":191,"isCanonical":false},{"tokens":[170,0],"name":"@192","index":192,"isCanonical":false},{"tokens":[319,0],"name":"@193","index":193,"isCanonical":false},{"tokens":[320,0],"name":"@194","index":194,"isCanonical":false},{"tokens":[223,0],"name":"@195","index":195,"isCanonical":false},{"tokens":[207,0],"name":"@196","index":196,"isCanonical":false},{"tokens":[318,0],"name":"@198","index":198,"isCanonical":false},{"tokens":[316,0],"name":"@199","index":199,"isCanonical":false},{"tokens":[326,0],"name":"@200","index":200,"isCanonical":false},{"tokens":[317,0],"name":"@201","index":201,"isCanonical":false},{"tokens":[256,0],"name":"@202","index":202,"isCanonical":false},{"tokens":[332,0],"name":"@203","index":203,"isCanonical":false},{"tokens":[325,0],"name":"@204","index":204,"isCanonical":false},{"tokens":[323,0],"name":"@205","index":205,"isCanonical":false},{"tokens":[338,0],"name":"@206","index":206,"isCanonical":false},{"tokens":[150,268],"name":"@207","index":207,"isCanonical":false},{"tokens":[264,0],"name":"@208","index":208,"isCanonical":false},{"tokens":[297,268],"name":"@209","index":209,"isCanonical":false},{"tokens":[343,0],"name":"@210","index":210,"isCanonical":false},{"tokens":[330,0],"name":"@211","index":211,"isCanonical":false},{"tokens":[341,268],"name":"@212","index":212,"isCanonical":false},{"tokens":[341,0],"name":"@213","index":213,"isCanonical":false},{"tokens":[246,0],"name":"@214","index":214,"isCanonical":false},{"tokens":[237,0],"name":"@215","index":215,"isCanonical":false},{"tokens":[203,0],"name":"@216","index":216,"isCanonical":false},{"tokens":[348,0],"name":"@217","index":217,"isCanonical":false},{"tokens":[340,0],"name":"@218","index":218,"isCanonical":false},{"tokens":[73,0],"name":"@219","index":219,"isCanonical":false},{"tokens":[356,0],"name":"@223","index":223,"isCanonical":false},{"tokens":[358,0],"name":"@224","index":224,"isCanonical":false},{"tokens":[351,0],"name":"@225","index":225,"isCanonical":false},{"tokens":[347,0],"name":"@226","index":226,"isCanonical":false},{"tokens":[346,0],"name":"@227","index":227,"isCanonical":false},{"tokens":[361,0],"name":"@228","index":228,"isCanonical":false},{"tokens":[360,0],"name":"@230","index":230,"isCanonical":false},{"tokens":[364,0],"name":"@231","index":231,"isCanonical":false},{"tokens":[150,360],"name":"@232","index":232,"isCanonical":false},{"tokens":[343,360],"name":"@233","index":233,"isCanonical":false},{"tokens":[197,360],"name":"@234","index":234,"isCanonical":false},{"tokens":[221,360],"name":"@235","index":235,"isCanonical":false},{"tokens":[335,0],"name":"@236","index":236,"isCanonical":false}],"tokens":[{"name":"USDC","szDecimals":8,"weiDecimals":8,"index":0,"tokenId":"0x6d1e7cde53ba9467b783cb7c530ce054","isCanonical":true,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"PURR","szDecimals":0,"weiDecimals":5,"index":1,"tokenId":"0xc1fb593aeffbeb02f85e0308e9956a90","isCanonical":true,"evmContract":{"address":"0x9b498c3c8a0b8cd8ba1d9851d40d186f1872b44e","evm_extra_wei_decimals":13},"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"HFUN","szDecimals":2,"weiDecimals":8,"index":2,"tokenId":"0xbaf265ef389da684513d98d68edf4eae","isCanonical":false,"evmContract":{"address":"0xa320d9f65ec992eff38622c63627856382db726c","evm_extra_wei_decimals":10},"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"LICK","szDecimals":0,"weiDecimals":5,"index":3,"tokenId":"0xba3aaf468f793d9b42fd3328e24f1de9","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"MANLET","szDecimals":0,"weiDecimals":5,"index":4,"tokenId":"0xe9ced9225d2a69ccc8d6a5b224524b99","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"JEFF","szDecimals":0,"weiDecimals":5,"index":5,"tokenId":"0xfcf28885456bf7e7cbe5b7a25407c5bc","isCanonical":false,"evmContract":{"address":"0x52e444545fbe9e5972a7a371299522f7871aec1f","evm_extra_wei_decimals":13},"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"SIX","szDecimals":2,"weiDecimals":8,"index":6,"tokenId":"0x50a9391b4a40caffbe8b16303b95a0c1","isCanonical":false,"evmContract":{"address":"0x41de34fc45a770ebcd50200be93f080b4b05151f","evm_extra_wei_decimals":10},"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"WAGMI","szDecimals":2,"weiDecimals":8,"index":7,"tokenId":"0x649efea44690cf88d464f512bc7e2818","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"CAPPY","szDecimals":0,"weiDecimals":5,"index":8,"tokenId":"0x3f8abf62220007cc7ab6d33ef2963d88","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"POINTS","szDecimals":0,"weiDecimals":5,"index":9,"tokenId":"0xbb03842e1f71ed27ed8fa012b29affd4","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"TRUMP","szDecimals":2,"weiDecimals":7,"index":10,"tokenId":"0x368cb581f0d51e21aa19996d38ffdf6f","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"GMEOW","szDecimals":0,"weiDecimals":8,"index":11,"tokenId":"0x07615193eaa63d1da6feda6e0ac9e014","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"PEPE","szDecimals":2,"weiDecimals":7,"index":12,"tokenId":"0x79b6e1596ea0deb2e6912ff8392c9325","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"XULIAN","szDecimals":0,"weiDecimals":5,"index":13,"tokenId":"0x6cc648be7e4c38a8c7fcd8bfa6714127","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"RUG","szDecimals":0,"weiDecimals":5,"index":14,"tokenId":"0x4978f3f49f30776d9d7397b873223c2d","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"ILIENS","szDecimals":0,"weiDecimals":5,"index":15,"tokenId":"0xa74984ea379be6d899c1bf54db923604","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"FUCKY","szDecimals":2,"weiDecimals":8,"index":16,"tokenId":"0x7de5b7a8c115edf0174333446ba0ea78","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"CZ","szDecimals":2,"weiDecimals":7,"index":17,"tokenId":"0x3b5ff6cb91f71032578b53960090adfb","isCanonical":false,"evmContract":{"address":"0xfdd1834677a50e61fe13e6bce49c8144e333fcbe","evm_extra_wei_decimals":11},"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"BAGS","szDecimals":0,"weiDecimals":5,"index":18,"tokenId":"0x979978fd8cb07141f97dcab921ba697a","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"ANSEM","szDecimals":0,"weiDecimals":5,"index":19,"tokenId":"0xa96cfac10eaecba151f646c5cb4c5507","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"TATE","szDecimals":0,"weiDecimals":5,"index":20,"tokenId":"0xfba416cad5d8944e954deb6bfb2a8672","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"FUN","szDecimals":1,"weiDecimals":6,"index":21,"tokenId":"0x3dc9f93c39ddd9f0182ad1e584bae0d4","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"SUCKY","szDecimals":0,"weiDecimals":5,"index":22,"tokenId":"0xfd2ac85551ac85d3f04369e296ed8cd3","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"BIGBEN","szDecimals":2,"weiDecimals":8,"index":23,"tokenId":"0x231f2a687770b13fe12adb1f339ff722","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"KOBE","szDecimals":0,"weiDecimals":5,"index":24,"tokenId":"0x0d2556646326733d86c3fc4c2fa22ad4","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"VEGAS","szDecimals":2,"weiDecimals":8,"index":25,"tokenId":"0xb693d596cd02f5f38e532e647bb43b69","isCanonical":false,"evmContract":{"address":"0xb09158c8297acee00b900dc1f8715df46b7246a6","evm_extra_wei_decimals":10},"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"PUMP","szDecimals":0,"weiDecimals":5,"index":26,"tokenId":"0xefa7e286b99ea49ce6a21d21bb41636f","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"SCHIZO","szDecimals":2,"weiDecimals":8,"index":27,"tokenId":"0xe140201197ced74f8884a698190da7aa","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"CATNIP","szDecimals":2,"weiDecimals":8,"index":28,"tokenId":"0x8bb39ef7881404c5d019a07b1cf21725","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"HAPPY","szDecimals":2,"weiDecimals":8,"index":29,"tokenId":"0x3d06abbbc860b7e97feb1478c21c5cd6","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"SELL","szDecimals":0,"weiDecimals":5,"index":30,"tokenId":"0x18bce4e1865f0535689b3c4c71e86a84","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"HBOOST","szDecimals":0,"weiDecimals":5,"index":31,"tokenId":"0xe292ab8f7d9ddd078ebbcbc4d328665c","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"FARMED","szDecimals":2,"weiDecimals":7,"index":32,"tokenId":"0x002911b408c1ae4ddc144e756cebdc17","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"PURRPS","szDecimals":0,"weiDecimals":5,"index":33,"tokenId":"0x1254a7b73720e7e9d53cf822a8603fbf","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"GUP","szDecimals":2,"weiDecimals":8,"index":34,"tokenId":"0x5a9a44de6fbdb012284eec608c1cf9b8","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"GPT","szDecimals":2,"weiDecimals":7,"index":35,"tokenId":"0x2f9f45766eba7b62f30082f3d4ecc77c","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"PANDA","szDecimals":0,"weiDecimals":5,"index":36,"tokenId":"0x92588529ba00e2d2bec07c12c804a580","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"BID","szDecimals":0,"weiDecimals":5,"index":37,"tokenId":"0x21c5fd69973e1bee64c92af9eb47420a","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"HODL","szDecimals":2,"weiDecimals":8,"index":38,"tokenId":"0x33b1ee8db7496b484db887a7b748d20f","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"RAGE","szDecimals":2,"weiDecimals":8,"index":39,"tokenId":"0x0ae968c613d445364de97769f1afb16b","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"ASI","szDecimals":2,"weiDecimals":7,"index":40,"tokenId":"0x8811d8fc24b9f4ace2c0bd21996d5899","isCanonical":false,"evmContract":null,"fullName":"Artificial Superintelligence","deployerTradingFeeShare":"0.0"},{"name":"LEAP","szDecimals":2,"weiDecimals":8,"index":41,"tokenId":"0xd2f2e10dbfbd43d6147f8784358f5dc4","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"VAPOR","szDecimals":0,"weiDecimals":5,"index":42,"tokenId":"0xdb5190bea4b6ab178da0162420c93a73","isCanonical":false,"evmContract":null,"fullName":"Hypervapor","deployerTradingFeeShare":"0.0"},{"name":"PVP","szDecimals":0,"weiDecimals":5,"index":43,"tokenId":"0x42825e552b0242aa26d641f5cb6aff56","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"X","szDecimals":2,"weiDecimals":7,"index":44,"tokenId":"0x54198368f92dcfbe88e3c2090526690a","isCanonical":false,"evmContract":null,"fullName":"X","deployerTradingFeeShare":"0.0"},{"name":"PILL","szDecimals":2,"weiDecimals":8,"index":45,"tokenId":"0xd56f8732f1cae4fc2db2e48863c25d81","isCanonical":false,"evmContract":null,"fullName":"Take the Hyperliquid pill.","deployerTradingFeeShare":"0.0"},{"name":"ADHD","szDecimals":0,"weiDecimals":5,"index":46,"tokenId":"0xba13ed91905624d50318d54651ad3bab","isCanonical":false,"evmContract":{"address":"0xe3d5f45d97fee83b48c85e00c8359a2e07d68fee","evm_extra_wei_decimals":13},"fullName":"Attention Deficit Hyperliquid Disorder","deployerTradingFeeShare":"0.0"},{"name":"LADY","szDecimals":2,"weiDecimals":7,"index":47,"tokenId":"0x71378e1ba5403eb35ad4e1af2048c6e3","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"CAT","szDecimals":2,"weiDecimals":7,"index":48,"tokenId":"0x7631702302e6c7fbf69d369e0bcf45d6","isCanonical":false,"evmContract":{"address":"0x04d02cb2e963b4490ee02b1925223d04f9d83fc6","evm_extra_wei_decimals":11},"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"HPEPE","szDecimals":0,"weiDecimals":5,"index":49,"tokenId":"0x501ebeddf15e7433cb84d7cc396cdfa5","isCanonical":false,"evmContract":null,"fullName":"HypurrPepe","deployerTradingFeeShare":"0.0"},{"name":"MBAPPE","szDecimals":2,"weiDecimals":7,"index":50,"tokenId":"0x03e1ad7d4d6d82b3da89ac8d35cc917b","isCanonical":false,"evmContract":null,"fullName":"MBENIVINI","deployerTradingFeeShare":"0.0"},{"name":"MAGA","szDecimals":2,"weiDecimals":7,"index":51,"tokenId":"0x9b91002773083d9292b8cb02dacb7e79","isCanonical":false,"evmContract":null,"fullName":"MAGA","deployerTradingFeeShare":"0.0"},{"name":"MOG","szDecimals":2,"weiDecimals":8,"index":52,"tokenId":"0xbe7772099df97ec90f6ff9c4caf670d1","isCanonical":false,"evmContract":null,"fullName":"THE INTERNETS FIRST CULTURE COIN ON HYPERLIQUID.","deployerTradingFeeShare":"0.0"},{"name":"OMNIX","szDecimals":1,"weiDecimals":6,"index":53,"tokenId":"0x216a1428334ac9e908e76a02a1812416","isCanonical":false,"evmContract":{"address":"0x45ec8f63fe934c0213476cfb5870835e61dd11fa","evm_extra_wei_decimals":12},"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"COKE","szDecimals":2,"weiDecimals":8,"index":54,"tokenId":"0x2f0f48f1d1698fd483a0f6f8ebe9782e","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"JEET","szDecimals":0,"weiDecimals":5,"index":55,"tokenId":"0x15fa03f87df338eb7d858da86498ea0e","isCanonical":false,"evmContract":null,"fullName":"โ€œDEVS ARE FROM USAโ€","deployerTradingFeeShare":"0.0"},{"name":"DROP","szDecimals":2,"weiDecimals":7,"index":56,"tokenId":"0x6c30da3dd84bd4f59642fd73fcd46ad6","isCanonical":false,"evmContract":null,"fullName":"Airdrop","deployerTradingFeeShare":"0.0"},{"name":"CBD","szDecimals":0,"weiDecimals":5,"index":57,"tokenId":"0xb7bf6dd48ffeb66c22524e065abe6733","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"ARI","szDecimals":2,"weiDecimals":8,"index":58,"tokenId":"0xb0027f425afd8f8d7bdab3d0e22c7e54","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"TEST","szDecimals":0,"weiDecimals":5,"index":59,"tokenId":"0x677482db6e19ad72108bb042b190709e","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"KING","szDecimals":1,"weiDecimals":6,"index":60,"tokenId":"0x4b5aea9c6624f354fdfe28274a363f27","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"MEOW","szDecimals":1,"weiDecimals":6,"index":61,"tokenId":"0xcb820fb2715cad58356916847960a73c","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"ANT","szDecimals":2,"weiDecimals":8,"index":62,"tokenId":"0x8a7ade54439539fd1268ca60a528396a","isCanonical":false,"evmContract":null,"fullName":"ANT POWER","deployerTradingFeeShare":"0.0"},{"name":"FRAC","szDecimals":1,"weiDecimals":6,"index":63,"tokenId":"0x58fb407b5fce2b90348cdf090d6dd3d4","isCanonical":false,"evmContract":null,"fullName":"Fractality","deployerTradingFeeShare":"0.0"},{"name":"ATEHUN","szDecimals":2,"weiDecimals":8,"index":64,"tokenId":"0xbebb35d03b83a302a2aa06dbaa67dd9f","isCanonical":false,"evmContract":null,"fullName":"The official meme for the $800/pt HL retard","deployerTradingFeeShare":"0.0"},{"name":"FRCT","szDecimals":1,"weiDecimals":6,"index":65,"tokenId":"0xfebf76468dd13f240281e4a8ef11932d","isCanonical":false,"evmContract":null,"fullName":"Fractality","deployerTradingFeeShare":"0.0"},{"name":"COZY","szDecimals":2,"weiDecimals":8,"index":66,"tokenId":"0xd998e1032a1fabfeaf6cf11ae7ddec2a","isCanonical":false,"evmContract":null,"fullName":"Cozy at the beach","deployerTradingFeeShare":"0.0"},{"name":"WASH","szDecimals":2,"weiDecimals":8,"index":67,"tokenId":"0x2e348026257e41c936119bf69d9c313f","isCanonical":false,"evmContract":null,"fullName":"Don't wash the cat ๐Ÿ™€๐Ÿงฝ๐Ÿšฟ","deployerTradingFeeShare":"0.0"},{"name":"POP","szDecimals":1,"weiDecimals":6,"index":68,"tokenId":"0x99ffa3c6ae342dcd532c8431bf4f3474","isCanonical":false,"evmContract":null,"fullName":"POP Coin","deployerTradingFeeShare":"0.0"},{"name":"NFT","szDecimals":2,"weiDecimals":8,"index":69,"tokenId":"0x5c621972ab84c077a1056d77f509a30f","isCanonical":false,"evmContract":null,"fullName":"First NFT collection on HL","deployerTradingFeeShare":"0.0"},{"name":"JEFE","szDecimals":1,"weiDecimals":6,"index":70,"tokenId":"0xe711c656aaadefe8f15579ca22679acd","isCanonical":false,"evmContract":null,"fullName":"Hyperlitquid","deployerTradingFeeShare":"0.0"},{"name":"PEAR","szDecimals":1,"weiDecimals":6,"index":71,"tokenId":"0x1ad748b576a84f6eb6541389bb53d164","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"RICH","szDecimals":2,"weiDecimals":8,"index":72,"tokenId":"0x263db50b9946a0701ccba65b52928dab","isCanonical":false,"evmContract":null,"fullName":"Rich Chinese guy with cigarette","deployerTradingFeeShare":"0.0"},{"name":"BRIDGE","szDecimals":2,"weiDecimals":7,"index":73,"tokenId":"0xc196d2b7598991bd0425fc959bc1a40f","isCanonical":false,"evmContract":{"address":"0x29dbf86a8c48ea4331e28b3c1eae824a2a45996a","evm_extra_wei_decimals":11},"fullName":"HyBridge","deployerTradingFeeShare":"0.0"},{"name":"GUESS","szDecimals":2,"weiDecimals":7,"index":74,"tokenId":"0x8211bcb445c7c006059cfcccb4f1a1b8","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"LORA","szDecimals":2,"weiDecimals":8,"index":75,"tokenId":"0xc6d6fb326749974a028425d9db7681c4","isCanonical":false,"evmContract":null,"fullName":"Lora","deployerTradingFeeShare":"0.0"},{"name":"CATBAL","szDecimals":2,"weiDecimals":8,"index":76,"tokenId":"0x38348c17e1a18559bbda232d22007695","isCanonical":false,"evmContract":{"address":"0x11735dbd0b97cfa7accf47d005673ba185f7fd49","evm_extra_wei_decimals":10},"fullName":"Password? Paw paw time.","deployerTradingFeeShare":"0.0"},{"name":"TJIF","szDecimals":2,"weiDecimals":8,"index":77,"tokenId":"0xa167acb332328faa26eda9ec38770eec","isCanonical":false,"evmContract":null,"fullName":"Thanks Jeff I'm Free","deployerTradingFeeShare":"0.0"},{"name":"NEIRO","szDecimals":1,"weiDecimals":6,"index":78,"tokenId":"0x0d04cae66af89099270d2931709afb32","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"TIME","szDecimals":1,"weiDecimals":8,"index":79,"tokenId":"0xae3f9675b5825ab20850ce6229c4bdbd","isCanonical":false,"evmContract":{"address":"0x266a2491f782eb03b369760889fff8785efb3e46","evm_extra_wei_decimals":10},"fullName":"Timeswap","deployerTradingFeeShare":"0.0"},{"name":"BERA","szDecimals":1,"weiDecimals":6,"index":80,"tokenId":"0x0b6ae68f39bfd088744374daa99db226","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"ANGY","szDecimals":0,"weiDecimals":8,"index":81,"tokenId":"0x56236a47fbbcc41ea8043207bfa0118d","isCanonical":false,"evmContract":null,"fullName":"notokmeiangy","deployerTradingFeeShare":"0.0"},{"name":"DOG","szDecimals":2,"weiDecimals":7,"index":82,"tokenId":"0xb37dc8f9819f5c7be5393c2241d28d28","isCanonical":false,"evmContract":null,"fullName":"DOG","deployerTradingFeeShare":"0.0"},{"name":"MOON","szDecimals":2,"weiDecimals":8,"index":83,"tokenId":"0xeff9411a23ab28ad69f67fbd9759d491","isCanonical":false,"evmContract":null,"fullName":"๐ŸŒ• 2themoon's retirement funds ๐Ÿ’ธ","deployerTradingFeeShare":"0.0"},{"name":"MAXI","szDecimals":2,"weiDecimals":8,"index":84,"tokenId":"0xed660d18ef6a87bc3dc1b3eeeb94fe21","isCanonical":false,"evmContract":null,"fullName":"ALL MY FRIENDS LOVE MAX FIEGE.","deployerTradingFeeShare":"0.0"},{"name":"PAIN","szDecimals":2,"weiDecimals":8,"index":85,"tokenId":"0x6e99a27fd1de4723848e32d7106aa59f","isCanonical":false,"evmContract":null,"fullName":"PAINCOIN","deployerTradingFeeShare":"0.0"},{"name":"VDO","szDecimals":2,"weiDecimals":8,"index":86,"tokenId":"0x2364d4a94a2a9f89a53b976fa8cadc80","isCanonical":false,"evmContract":null,"fullName":"ValiDAO","deployerTradingFeeShare":"0.0"},{"name":"HQ","szDecimals":1,"weiDecimals":6,"index":87,"tokenId":"0xf6f58625ba95ecde953f4aafc7af53fd","isCanonical":false,"evmContract":null,"fullName":"HyperQuant","deployerTradingFeeShare":"0.0"},{"name":"EUR","szDecimals":1,"weiDecimals":6,"index":88,"tokenId":"0xd125b605fa732968610dd4962e9a7fb9","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"REGARD","szDecimals":1,"weiDecimals":8,"index":89,"tokenId":"0x86df9bf70b2d72f2b168508f90026469","isCanonical":false,"evmContract":null,"fullName":"Regarded","deployerTradingFeeShare":"0.0"},{"name":"YUM","szDecimals":2,"weiDecimals":8,"index":90,"tokenId":"0x6bc45d95a04a7fba29fdb3ecbeeb053d","isCanonical":false,"evmContract":null,"fullName":"Yum.","deployerTradingFeeShare":"0.0"},{"name":"REKT","szDecimals":0,"weiDecimals":5,"index":91,"tokenId":"0x4626a44a785134109cae9ea21f33d9a0","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"NMTD","szDecimals":2,"weiDecimals":8,"index":92,"tokenId":"0x22bafa9342b3ac5762056d944a2f6b2a","isCanonical":false,"evmContract":null,"fullName":"Token supporting Pascal Bridge","deployerTradingFeeShare":"0.0"},{"name":"HPUMP","szDecimals":2,"weiDecimals":8,"index":93,"tokenId":"0xaa6f295683beb4906a9d21accbe5e604","isCanonical":false,"evmContract":null,"fullName":"HPUMP","deployerTradingFeeShare":"0.0"},{"name":"PIGEON","szDecimals":2,"weiDecimals":8,"index":94,"tokenId":"0xb901e2dfb2dbe1902e03be3c1e3e72ce","isCanonical":false,"evmContract":null,"fullName":"It's just a pigeon","deployerTradingFeeShare":"0.0"},{"name":"LAUNCH","szDecimals":2,"weiDecimals":8,"index":95,"tokenId":"0xa82aa458b2c146e97733d0c05374aa0e","isCanonical":false,"evmContract":{"address":"0x4533c3002660117a56fa44ed3c4bf4b6a8e0b0a2","evm_extra_wei_decimals":10},"fullName":"Hyperlauncher","deployerTradingFeeShare":"0.0"},{"name":"IRL","szDecimals":1,"weiDecimals":6,"index":96,"tokenId":"0x2ef25ace7c11f77f0de071b15e8b0bc0","isCanonical":false,"evmContract":null,"fullName":"Integra","deployerTradingFeeShare":"0.0"},{"name":"RISE","szDecimals":2,"weiDecimals":8,"index":97,"tokenId":"0x51973ef5533887afcace6b26de5f19dd","isCanonical":false,"evmContract":null,"fullName":"Phoenix","deployerTradingFeeShare":"0.0"},{"name":"WOW","szDecimals":0,"weiDecimals":5,"index":98,"tokenId":"0xc81b9a04884f4a8107f734c4d88f811a","isCanonical":false,"evmContract":{"address":"0x93f12d8931e97cf41d5934bb5d302aa0e2623305","evm_extra_wei_decimals":13},"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"CINDY","szDecimals":2,"weiDecimals":8,"index":99,"tokenId":"0xf00cf2c0647e1ce1ee5c1274b4650e40","isCanonical":false,"evmContract":null,"fullName":"Hyperliquid is a CEX with a couple of extra steps","deployerTradingFeeShare":"0.0"},{"name":"CHINA","szDecimals":2,"weiDecimals":8,"index":100,"tokenId":"0x650ba53f534d00834002ca10c4792d06","isCanonical":false,"evmContract":null,"fullName":"SUPPORT CHINA TO ONBOARD MORE CHINESE","deployerTradingFeeShare":"0.0"},{"name":"STACK","szDecimals":2,"weiDecimals":8,"index":101,"tokenId":"0x9728448fd10546a8527b21298a9b41e4","isCanonical":false,"evmContract":null,"fullName":"Just STACK.","deployerTradingFeeShare":"0.0"},{"name":"FRIED","szDecimals":2,"weiDecimals":8,"index":102,"tokenId":"0xbd859b02096be7a23dd1527269be10a0","isCanonical":false,"evmContract":null,"fullName":"Hypurrfried","deployerTradingFeeShare":"0.0"},{"name":"PICKL","szDecimals":2,"weiDecimals":7,"index":103,"tokenId":"0x1a69662de430e900be096c00ea28b810","isCanonical":false,"evmContract":null,"fullName":"PICKL","deployerTradingFeeShare":"0.0"},{"name":"SHREK","szDecimals":2,"weiDecimals":8,"index":104,"tokenId":"0x550ff22831954938f2be76028b15eb18","isCanonical":false,"evmContract":null,"fullName":"Swamp trades, ogre-sized gains!","deployerTradingFeeShare":"0.0"},{"name":"NOCEX","szDecimals":2,"weiDecimals":8,"index":105,"tokenId":"0x5901399de6f487a6f7b11b1487811c86","isCanonical":false,"evmContract":null,"fullName":"NO CEX","deployerTradingFeeShare":"0.0"},{"name":"VAULT","szDecimals":2,"weiDecimals":8,"index":106,"tokenId":"0x288553d859333b90ede88640b4279598","isCanonical":false,"evmContract":null,"fullName":"Vault","deployerTradingFeeShare":"0.0"},{"name":"RANK","szDecimals":2,"weiDecimals":8,"index":107,"tokenId":"0xd0a824284314e8ceb8cf3c3b31ddf0bd","isCanonical":false,"evmContract":null,"fullName":"!rank","deployerTradingFeeShare":"0.0"},{"name":"L","szDecimals":2,"weiDecimals":7,"index":108,"tokenId":"0xdb7d7cd808bdad7cedf44b5864c3866c","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"H","szDecimals":2,"weiDecimals":7,"index":109,"tokenId":"0x52a110041682dbf38277c80a72eecba6","isCanonical":false,"evmContract":{"address":"0x78c3791ea49a7c6f41e87ba96c7d09a493febb1e","evm_extra_wei_decimals":11},"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"G","szDecimals":2,"weiDecimals":7,"index":110,"tokenId":"0x7ee78dd38408381c2b5b979b7383e2e1","isCanonical":false,"evmContract":null,"fullName":"G","deployerTradingFeeShare":"0.0"},{"name":"SCAM","szDecimals":2,"weiDecimals":8,"index":111,"tokenId":"0x425e14eb167930e81390c8091bc2d8b7","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"MONAD","szDecimals":1,"weiDecimals":8,"index":112,"tokenId":"0x3413da62384685cda2528d4ba085a055","isCanonical":false,"evmContract":null,"fullName":"Monad","deployerTradingFeeShare":"0.0"},{"name":"BOZO","szDecimals":2,"weiDecimals":7,"index":113,"tokenId":"0x2030eb01e794e9f7e94b9e6b82a08bfb","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"RIP","szDecimals":2,"weiDecimals":8,"index":114,"tokenId":"0xc20825d6e56b3bf1aa15d1f80471b166","isCanonical":false,"evmContract":null,"fullName":"Rest in peace HL SPOT","deployerTradingFeeShare":"0.0"},{"name":"UP","szDecimals":2,"weiDecimals":7,"index":115,"tokenId":"0xaa60905298f022ad8efab6d4348bc61b","isCanonical":false,"evmContract":null,"fullName":"UP","deployerTradingFeeShare":"0.0"},{"name":"GIGA","szDecimals":2,"weiDecimals":7,"index":116,"tokenId":"0xe536ad7c82d63a53aafe6ae9f19c7e03","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"FELIX","szDecimals":0,"weiDecimals":8,"index":117,"tokenId":"0x601138020f6eeb930be04fbc31379f08","isCanonical":false,"evmContract":null,"fullName":"Felix","deployerTradingFeeShare":"0.0"},{"name":"SPH","szDecimals":2,"weiDecimals":8,"index":118,"tokenId":"0xaced5d1ad2c5ce7b7c789abfb7955b27","isCanonical":false,"evmContract":{"address":"0xd2fe47eed2d52725d9e3ae6df45593837f57c1a2","evm_extra_wei_decimals":10},"fullName":"SPH800","deployerTradingFeeShare":"0.0"},{"name":"STHYPE","szDecimals":2,"weiDecimals":8,"index":119,"tokenId":"0x244d19a8576a492f44ee19622f4104e4","isCanonical":false,"evmContract":null,"fullName":"Staked HYPE","deployerTradingFeeShare":"0.0"},{"name":"HPL","szDecimals":2,"weiDecimals":8,"index":120,"tokenId":"0x5e887f0c6c3deec190c36186bf23369f","isCanonical":false,"evmContract":null,"fullName":"HyperLend","deployerTradingFeeShare":"0.0"},{"name":"KHYPE","szDecimals":2,"weiDecimals":8,"index":121,"tokenId":"0xbc8a22f25703a03101630ce6b09f4baa","isCanonical":false,"evmContract":null,"fullName":"Kinetiq Staked HYPE","deployerTradingFeeShare":"0.0"},{"name":"HOPE","szDecimals":0,"weiDecimals":5,"index":122,"tokenId":"0xe6ac9d5a7cf91cdd9fdf34fcbbdd58e9","isCanonical":false,"evmContract":{"address":"0x869ac826b78bc1d9501014994196e41025b5224b","evm_extra_wei_decimals":0},"fullName":"Purr $HOPE for the $HYPE! No planned utility.","deployerTradingFeeShare":"0.0"},{"name":"SHOE","szDecimals":2,"weiDecimals":8,"index":123,"tokenId":"0x706aae9a576b939bc8ef1656f9fe9ed0","isCanonical":false,"evmContract":null,"fullName":"Shoe Mario","deployerTradingFeeShare":"0.0"},{"name":"KNTQ","szDecimals":2,"weiDecimals":8,"index":124,"tokenId":"0xbd31bd605c0a1b82c72aae3587f9061f","isCanonical":false,"evmContract":null,"fullName":"Kinetiq","deployerTradingFeeShare":"0.0"},{"name":"BUSSY","szDecimals":2,"weiDecimals":8,"index":125,"tokenId":"0xd27ab3aa48f744ca0c469902a13c8656","isCanonical":false,"evmContract":null,"fullName":"Bozoussy","deployerTradingFeeShare":"0.0"},{"name":"FATCAT","szDecimals":2,"weiDecimals":8,"index":126,"tokenId":"0x1b00cbe4c41fc38e2701df2d75dad892","isCanonical":false,"evmContract":null,"fullName":"Fat Cat in Catbal","deployerTradingFeeShare":"0.0"},{"name":"PIP","szDecimals":2,"weiDecimals":8,"index":127,"tokenId":"0xe85f43e1f91e3c8cdf3acbd7e0855b8e","isCanonical":false,"evmContract":{"address":"0x1bee6762f0b522c606dc2ffb106c0bb391b2e309","evm_extra_wei_decimals":10},"fullName":"a tiny pip in a big hypurrrliquid puddle ๐ŸŒŠ","deployerTradingFeeShare":"0.0"},{"name":"YEETI","szDecimals":0,"weiDecimals":5,"index":128,"tokenId":"0x2150c49b1b981b88349dae535d607ce2","isCanonical":false,"evmContract":{"address":"0x7280cc1f369ab574c35cb8a8d0885e9486e3b733","evm_extra_wei_decimals":13},"fullName":"YEETI ๆถฒไฝ“","deployerTradingFeeShare":"0.0"},{"name":"LQNA","szDecimals":2,"weiDecimals":8,"index":129,"tokenId":"0x0dd6980f71e51e6f218a9b7d53c837f6","isCanonical":false,"evmContract":{"address":"0xa94676f34f6a2764c7fde5611b53834b71f228ec","evm_extra_wei_decimals":10},"fullName":"The Queen of Hyperliquid.","deployerTradingFeeShare":"0.0"},{"name":"NASDAQ","szDecimals":2,"weiDecimals":8,"index":130,"tokenId":"0x6a53c88cfa761cd324b0ab4cd50dc2d5","isCanonical":false,"evmContract":null,"fullName":"Professional traders unite in DEX eco.","deployerTradingFeeShare":"0.0"},{"name":"SYLVI","szDecimals":2,"weiDecimals":8,"index":131,"tokenId":"0x21c7f844092376ba85aa3875180e194b","isCanonical":false,"evmContract":{"address":"0x769a227178f37da50371200eb93aa71a214ca874","evm_extra_wei_decimals":10},"fullName":"Sylvanian Families","deployerTradingFeeShare":"0.0"},{"name":"FEIT","szDecimals":2,"weiDecimals":8,"index":132,"tokenId":"0x0c29857bd79a8e17437b19f9eaa322a4","isCanonical":false,"evmContract":null,"fullName":"Fortuneโ€™s path begins with $FEIT","deployerTradingFeeShare":"0.0"},{"name":"STRICT","szDecimals":0,"weiDecimals":5,"index":133,"tokenId":"0x83a38493d264bf3f5b1f47c8ebd67eea","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"FRUDO","szDecimals":2,"weiDecimals":8,"index":134,"tokenId":"0xb3d30d38259b99d4a6daaf72ded7bf34","isCanonical":false,"evmContract":null,"fullName":"Frudo is here to lead us into a new bold era.","deployerTradingFeeShare":"0.0"},{"name":"VIZN","szDecimals":2,"weiDecimals":8,"index":135,"tokenId":"0x7e640c7d4d529e332c89ac86075a1855","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"AUTIST","szDecimals":2,"weiDecimals":8,"index":136,"tokenId":"0x7b659352508b25500406b4cd7fdc4f8c","isCanonical":false,"evmContract":null,"fullName":"Hyperautism levels of autism!","deployerTradingFeeShare":"0.0"},{"name":"HGOD","szDecimals":2,"weiDecimals":8,"index":137,"tokenId":"0xd84e1b57dad6c106c3eb8842905d773c","isCanonical":false,"evmContract":null,"fullName":"The Goddess of Hyperliquid","deployerTradingFeeShare":"0.0"},{"name":"LIQUID","szDecimals":2,"weiDecimals":8,"index":138,"tokenId":"0x8e821d72325ab1effd3193c3ac5f9c4b","isCanonical":false,"evmContract":null,"fullName":"$LIQUID is the Hyperliquid GOD.","deployerTradingFeeShare":"0.0"},{"name":"CHEF","szDecimals":2,"weiDecimals":8,"index":139,"tokenId":"0xb59b1334dfefe55b4d6895bdec3ed16c","isCanonical":false,"evmContract":null,"fullName":"CHEF: The Nourisher of Hyperliquid's Powers","deployerTradingFeeShare":"0.0"},{"name":"AZUR","szDecimals":2,"weiDecimals":7,"index":140,"tokenId":"0x14167e36345f2652e90b44ed1a015782","isCanonical":false,"evmContract":null,"fullName":"Azuro Protocol","deployerTradingFeeShare":"0.0"},{"name":"EARTH","szDecimals":2,"weiDecimals":8,"index":141,"tokenId":"0xc9a3be0e82dde365b038c34674ed4046","isCanonical":false,"evmContract":null,"fullName":"We have only one","deployerTradingFeeShare":"0.0"},{"name":"NIGGO","szDecimals":2,"weiDecimals":8,"index":142,"tokenId":"0x2e73765c9718421ca426a2bb2d7243b0","isCanonical":false,"evmContract":{"address":"0xc1631903081b19f0b7117f34192c7db48960989c","evm_extra_wei_decimals":10},"fullName":"Finding Niggo in the vast hyperliquid ocean","deployerTradingFeeShare":"0.0"},{"name":"LUCKY","szDecimals":0,"weiDecimals":5,"index":143,"tokenId":"0x6558e9071cbb754b7ee56568a9f5d80a","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"HOP","szDecimals":2,"weiDecimals":8,"index":144,"tokenId":"0x9b2cff929d260e8d5015fa188167be74","isCanonical":false,"evmContract":null,"fullName":"Hopurr","deployerTradingFeeShare":"0.0"},{"name":"FAN","szDecimals":0,"weiDecimals":5,"index":145,"tokenId":"0x1fedb171e381fce7051832b8bc72ef34","isCanonical":false,"evmContract":null,"fullName":"fan.fun","deployerTradingFeeShare":"0.0"},{"name":"MUNCH","szDecimals":0,"weiDecimals":5,"index":146,"tokenId":"0x26f2bebc98ec2a579febeb5d7b7e3456","isCanonical":false,"evmContract":null,"fullName":"๐Ÿฉ Staying Hungry We Keep ๐Ÿฉn Munching ๐Ÿฉ","deployerTradingFeeShare":"0.0"},{"name":"COPE","szDecimals":2,"weiDecimals":8,"index":147,"tokenId":"0x53184ac4c2f5d4729f4e0c0077af63e3","isCanonical":false,"evmContract":null,"fullName":"Cope Hard Token","deployerTradingFeeShare":"0.0"},{"name":"HPYH","szDecimals":2,"weiDecimals":8,"index":148,"tokenId":"0x18a98375ce42a0f1e40d0d666eb69e15","isCanonical":false,"evmContract":null,"fullName":"quant turned memecoin trader trying to make a buck","deployerTradingFeeShare":"0.0"},{"name":"YAP","szDecimals":2,"weiDecimals":8,"index":149,"tokenId":"0x7555ea01e1100621bcbf5f9ce3351a69","isCanonical":false,"evmContract":null,"fullName":"Yield Acceleration Protocol","deployerTradingFeeShare":"0.0"},{"name":"HYPE","szDecimals":2,"weiDecimals":8,"index":150,"tokenId":"0x0d01dc56dcaaca66ad901c959b4011ec","isCanonical":false,"evmContract":null,"fullName":"Hyperliquid","deployerTradingFeeShare":"0.0"},{"name":"STEEL","szDecimals":2,"weiDecimals":8,"index":151,"tokenId":"0x7a176eea76f7c1337134d143d2f4939e","isCanonical":false,"evmContract":null,"fullName":"Steel Panties Token","deployerTradingFeeShare":"0.0"},{"name":"RETARD","szDecimals":2,"weiDecimals":8,"index":152,"tokenId":"0x888b2490ddbb2a176fcd0da1e53d44c5","isCanonical":false,"evmContract":null,"fullName":"Really Effective Trading And Research Device","deployerTradingFeeShare":"0.0"},{"name":"HOLD","szDecimals":2,"weiDecimals":8,"index":153,"tokenId":"0xdaf550b8afca818dadb7949937528bc6","isCanonical":false,"evmContract":null,"fullName":"HyperGold","deployerTradingFeeShare":"0.0"},{"name":"STAR","szDecimals":2,"weiDecimals":8,"index":154,"tokenId":"0x68aa39b6a6d8abe04ba97d3953fea816","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"WATAR","szDecimals":2,"weiDecimals":8,"index":155,"tokenId":"0xc48ddbfec007fa7443259f6b1598797c","isCanonical":false,"evmContract":null,"fullName":"be watar, my fren","deployerTradingFeeShare":"0.0"},{"name":"GENESY","szDecimals":2,"weiDecimals":8,"index":156,"tokenId":"0xc8469678541042226507fd880a105523","isCanonical":false,"evmContract":{"address":"0x6f7e96c0267cd22fe04346af21f8c6ff54372939","evm_extra_wei_decimals":10},"fullName":"First P2E on Hyperliquid, optimize and earn.","deployerTradingFeeShare":"0.0"},{"name":"SOLV","szDecimals":2,"weiDecimals":8,"index":157,"tokenId":"0xcf548188e24a0ce3dc3b281242baafd9","isCanonical":false,"evmContract":null,"fullName":"Solv Protocol","deployerTradingFeeShare":"0.0"},{"name":"BUBZ","szDecimals":2,"weiDecimals":8,"index":158,"tokenId":"0x9563495f7d4c195047fc891e2622673b","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"SHEEP","szDecimals":2,"weiDecimals":8,"index":159,"tokenId":"0xd99348c20362c8513e2808ac24adc777","isCanonical":false,"evmContract":{"address":"0x27542a8d608ee16322fae739c46e6e6ae6d2828a","evm_extra_wei_decimals":10},"fullName":"The blue sheep on HyperLiquiq Don't Miss It","deployerTradingFeeShare":"0.0"},{"name":"HYFI","szDecimals":2,"weiDecimals":8,"index":160,"tokenId":"0x1c351bfdefb12591e9329adb38cdcbae","isCanonical":false,"evmContract":null,"fullName":"HypurrFi","deployerTradingFeeShare":"0.0"},{"name":"FARM","szDecimals":2,"weiDecimals":8,"index":161,"tokenId":"0x11c83b5cfeb6419a74a85a5aaba0511d","isCanonical":false,"evmContract":{"address":"0x3880c8cf09a5454dc9cb851b08c785e55504f63c","evm_extra_wei_decimals":10},"fullName":"World's 1st GenAI AI Agent Game","deployerTradingFeeShare":"0.0"},{"name":"FLASK","szDecimals":2,"weiDecimals":8,"index":162,"tokenId":"0x7edce821fc9e272192e73420ac9ad319","isCanonical":false,"evmContract":null,"fullName":"The flask containing the legendary hyperliquid.","deployerTradingFeeShare":"0.0"},{"name":"SOVRN","szDecimals":2,"weiDecimals":8,"index":163,"tokenId":"0xc6aef97252f0081eb8246e24f24c5fe7","isCanonical":false,"evmContract":null,"fullName":"Sovrun","deployerTradingFeeShare":"0.0"},{"name":"MON","szDecimals":2,"weiDecimals":8,"index":164,"tokenId":"0x622cf551933f19f9136303dcab56488c","isCanonical":false,"evmContract":null,"fullName":"MON","deployerTradingFeeShare":"0.0"},{"name":"GOD","szDecimals":2,"weiDecimals":8,"index":165,"tokenId":"0x5e525c76aa6e1f0a80fd8a0e71b77c45","isCanonical":false,"evmContract":null,"fullName":"GOD","deployerTradingFeeShare":"0.0"},{"name":"CREAM","szDecimals":2,"weiDecimals":8,"index":166,"tokenId":"0x45b7ba9b61a02d94c19e606d99b7a263","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"ANIME","szDecimals":2,"weiDecimals":8,"index":167,"tokenId":"0x0d5012952afd6e4d76baa0a73d16a937","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"HYENA","szDecimals":2,"weiDecimals":8,"index":168,"tokenId":"0xf1b242b9725266c3c634909fb575ef10","isCanonical":false,"evmContract":null,"fullName":"Hyena - The Predator on HyperLiquid","deployerTradingFeeShare":"0.0"},{"name":"ETHC","szDecimals":2,"weiDecimals":8,"index":169,"tokenId":"0x8d3aeb9873bfbe64eb0554e6fde38a8b","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"ANZ","szDecimals":2,"weiDecimals":8,"index":170,"tokenId":"0xd50882aa1d890629d02b2a616dde56e5","isCanonical":false,"evmContract":{"address":"0xdf3dee6d24d4f369e0e290533d47ef9cf44abbdb","evm_extra_wei_decimals":10},"fullName":"Anzen Token","deployerTradingFeeShare":"0.0"},{"name":"GAME","szDecimals":2,"weiDecimals":8,"index":171,"tokenId":"0x323f0986ba5b7f6e8e968e3d072bcd3e","isCanonical":false,"evmContract":null,"fullName":"GAME by Virtuals","deployerTradingFeeShare":"0.0"},{"name":"RIFT","szDecimals":2,"weiDecimals":8,"index":172,"tokenId":"0x0b983d0a300f75b148a6a96ed4c6f793","isCanonical":false,"evmContract":null,"fullName":"RIFT","deployerTradingFeeShare":"0.0"},{"name":"SWELL","szDecimals":2,"weiDecimals":8,"index":173,"tokenId":"0xc2fb8b0d924e6fffe98dd85e6f18f362","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"DEPIN","szDecimals":2,"weiDecimals":8,"index":174,"tokenId":"0x934a7e00c1b047c2d27967663f58befe","isCanonical":false,"evmContract":{"address":"0xa085221178b4cb7e40415bca547e76e223e6d137","evm_extra_wei_decimals":10},"fullName":"DEPIN DEGN DEVICE OFFICIAL TOKEN","deployerTradingFeeShare":"0.0"},{"name":"ROUTE","szDecimals":2,"weiDecimals":8,"index":175,"tokenId":"0xd633138efb8cd5bcd1e5f7686abcb4fc","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"BEATS","szDecimals":2,"weiDecimals":8,"index":176,"tokenId":"0x74051103aba9e726e0ad877ed3e49c38","isCanonical":false,"evmContract":{"address":"0x710a6c044d23949ba3b98ce13d762503c9708ba3","evm_extra_wei_decimals":10},"fullName":"BEATS AI - the agentic music creation lab","deployerTradingFeeShare":"0.0"},{"name":"ORA","szDecimals":2,"weiDecimals":8,"index":177,"tokenId":"0xd7a5b9b760fd758d2e3f6f3ecd2ae5bb","isCanonical":false,"evmContract":null,"fullName":"ORA Coin","deployerTradingFeeShare":"0.0"},{"name":"LIQD","szDecimals":2,"weiDecimals":8,"index":178,"tokenId":"0xa043053570d42d6f553896820dfd42b6","isCanonical":false,"evmContract":{"address":"0x1ecd15865d7f8019d546f76d095d9c93cc34edfa","evm_extra_wei_decimals":10},"fullName":"LiquidLaunch","deployerTradingFeeShare":"0.0"},{"name":"XBG","szDecimals":2,"weiDecimals":8,"index":179,"tokenId":"0x6f61f3baeca70d913bc619e3e5d559e3","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"HETU","szDecimals":2,"weiDecimals":8,"index":180,"tokenId":"0x9d417e6c0904ad3185f7dfa4c82896bc","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"GG","szDecimals":0,"weiDecimals":8,"index":181,"tokenId":"0x13eaa23f20948f2a43ae24d0630ba5e2","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"WOOL","szDecimals":0,"weiDecimals":8,"index":182,"tokenId":"0x718a34a10ea60c49aa486d5e0a66a1ce","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"ISLAND","szDecimals":2,"weiDecimals":8,"index":183,"tokenId":"0x7663c492f7cfdbb149486af546ee7f9a","isCanonical":false,"evmContract":{"address":"0x1f170f59849bf2e11a051e9da1213d672baa8748","evm_extra_wei_decimals":10},"fullName":"Nifty Island","deployerTradingFeeShare":"0.0"},{"name":"SIPHER","szDecimals":2,"weiDecimals":8,"index":184,"tokenId":"0x9b84608875b23c69b83b048480d7a66c","isCanonical":false,"evmContract":null,"fullName":"Sipher","deployerTradingFeeShare":"0.0"},{"name":"DBR","szDecimals":1,"weiDecimals":6,"index":185,"tokenId":"0x1df7fcdc9af97d44310f6bb8f346b0a8","isCanonical":false,"evmContract":null,"fullName":"deBridge","deployerTradingFeeShare":"0.0"},{"name":"SENT","szDecimals":2,"weiDecimals":8,"index":186,"tokenId":"0x2ceb1907ce5c0c98b86e733d15aea34b","isCanonical":false,"evmContract":{"address":"0xed912f61368be50835ad7696f67d106b0cd08fe2","evm_extra_wei_decimals":10},"fullName":"https://x.com/Sentiient_AI","deployerTradingFeeShare":"0.0"},{"name":"FLY","szDecimals":2,"weiDecimals":8,"index":187,"tokenId":"0x55db85bd0938b111d0f78a98173fd99a","isCanonical":false,"evmContract":{"address":"0x3f244819a8359145a8e7cf0272955e4918a50627","evm_extra_wei_decimals":10},"fullName":"HyperFly","deployerTradingFeeShare":"0.0"},{"name":"SSS","szDecimals":2,"weiDecimals":8,"index":188,"tokenId":"0x7bd8248ebc4bd9ed1898fcf3f61986d9","isCanonical":false,"evmContract":null,"fullName":"Smart Stables System","deployerTradingFeeShare":"0.0"},{"name":"HWTR","szDecimals":2,"weiDecimals":8,"index":189,"tokenId":"0xad426cf28a66dc0c8f3b931018ba9845","isCanonical":false,"evmContract":{"address":"0x5804bf271d9e691611eea1267b24c1f3d0723639","evm_extra_wei_decimals":10},"fullName":"First AI investment DAO on Hyperliquid","deployerTradingFeeShare":"0.0"},{"name":"NEKO","szDecimals":2,"weiDecimals":8,"index":190,"tokenId":"0x7079200f1b1511a6d344d399fb9bf984","isCanonical":false,"evmContract":null,"fullName":"Neko is Omo Protocol's debut AI Agent","deployerTradingFeeShare":"0.0"},{"name":"UNIT","szDecimals":2,"weiDecimals":8,"index":191,"tokenId":"0x83cb6431830d27338b9afb6150023856","isCanonical":false,"evmContract":null,"fullName":"Unit","deployerTradingFeeShare":"0.0"},{"name":"PEG","szDecimals":0,"weiDecimals":5,"index":192,"tokenId":"0x63142af22ec99122436a82c47b8ef2fa","isCanonical":false,"evmContract":{"address":"0x28245ab01298eaef7933bc90d35bd9dbca5c89db","evm_extra_wei_decimals":13},"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"NEURAL","szDecimals":2,"weiDecimals":8,"index":193,"tokenId":"0x9c3cb39ba8fbd523f9ef02795634cf82","isCanonical":false,"evmContract":null,"fullName":"NeuralAI","deployerTradingFeeShare":"0.0"},{"name":"PIE","szDecimals":2,"weiDecimals":8,"index":194,"tokenId":"0xd45d3412640d43f763d94295ac7fb751","isCanonical":false,"evmContract":null,"fullName":"PIE","deployerTradingFeeShare":"0.0"},{"name":"DRIVE","szDecimals":2,"weiDecimals":8,"index":195,"tokenId":"0x55ac10c740e99c4542175ee946c822b1","isCanonical":false,"evmContract":null,"fullName":"Hyperdrive","deployerTradingFeeShare":"0.0"},{"name":"JPEG","szDecimals":2,"weiDecimals":8,"index":196,"tokenId":"0xc1918b90ec9dd64ed6d226600e9f91c7","isCanonical":false,"evmContract":{"address":"0xc11579f984d07af75b0164ac458583a0d39d619a","evm_extra_wei_decimals":10},"fullName":"JPEG","deployerTradingFeeShare":"1.0"},{"name":"UBTC","szDecimals":5,"weiDecimals":10,"index":197,"tokenId":"0x8f254b963e8468305d409b33aa137c67","isCanonical":false,"evmContract":{"address":"0x9fdbda0a5e284c32744d2f17ee5c74b284993463","evm_extra_wei_decimals":-2},"fullName":"Unit Bitcoin","deployerTradingFeeShare":"1.0"},{"name":"TGE","szDecimals":2,"weiDecimals":8,"index":198,"tokenId":"0x4a34dc4e210b7d224620f89a57f76497","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"1.0"},{"name":"HEAD","szDecimals":2,"weiDecimals":8,"index":199,"tokenId":"0x8a1c119f82fc528492b8a353c053a82e","isCanonical":false,"evmContract":{"address":"0xa0999b04c23e8b73961a97b463067d9bea5b953c","evm_extra_wei_decimals":10},"fullName":"Head to Head","deployerTradingFeeShare":"1.0"},{"name":"EDGE","szDecimals":2,"weiDecimals":8,"index":200,"tokenId":"0x7ef7da504e078de1e22637eed68922a7","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"1.0"},{"name":"GEN","szDecimals":0,"weiDecimals":5,"index":201,"tokenId":"0xc74cbf22461b784e1b78f8d1c68b9c31","isCanonical":false,"evmContract":null,"fullName":"Generative AI","deployerTradingFeeShare":"1.0"},{"name":"PRFI","szDecimals":2,"weiDecimals":8,"index":202,"tokenId":"0x592e638c27be0c84f7c2a4aad919b855","isCanonical":false,"evmContract":{"address":"0x7bbcf1b600565ae023a1806ef637af4739de3255","evm_extra_wei_decimals":10},"fullName":"PrimeFi","deployerTradingFeeShare":"1.0"},{"name":"REI","szDecimals":2,"weiDecimals":8,"index":203,"tokenId":"0x228f5c718414182956b11c3ea5380b35","isCanonical":false,"evmContract":{"address":"0xa4907f0e6bf36341516ba3d9d2524ea92f521e65","evm_extra_wei_decimals":10},"fullName":"REI","deployerTradingFeeShare":"1.0"},{"name":"VORTX","szDecimals":2,"weiDecimals":8,"index":204,"tokenId":"0xce98fc675b7de6fa9776d32e00c56717","isCanonical":false,"evmContract":{"address":"0xc12b4dd5268322ddbe3d6f65ebb1ce37a9951315","evm_extra_wei_decimals":10},"fullName":"Interact with HL on Discord, Twitter and web UI","deployerTradingFeeShare":"1.0"},{"name":"HAR","szDecimals":2,"weiDecimals":8,"index":205,"tokenId":"0x9325025f805731935c1df7f97e654cda","isCanonical":false,"evmContract":null,"fullName":"Harmonix Finance","deployerTradingFeeShare":"1.0"},{"name":"DEFIN","szDecimals":2,"weiDecimals":8,"index":206,"tokenId":"0x1c1c2e504b6306e6ab6c0377959dc2fb","isCanonical":false,"evmContract":null,"fullName":"Research & trading agents powered Defi trading hub","deployerTradingFeeShare":"1.0"},{"name":"APU","szDecimals":2,"weiDecimals":8,"index":207,"tokenId":"0xf20a5f3285ec49d45360fa28d95d6ba3","isCanonical":false,"evmContract":{"address":"0xcbb8e1a8d5f9ee59c5a331f6c8556cca8e01cf10","evm_extra_wei_decimals":10},"fullName":"Apu Apustaja","deployerTradingFeeShare":"1.0"},{"name":"MOCA","szDecimals":2,"weiDecimals":8,"index":208,"tokenId":"0x0b431f75530ae2c7f68ad00f04d7778a","isCanonical":false,"evmContract":null,"fullName":"Moca Network","deployerTradingFeeShare":"1.0"},{"name":"TILT","szDecimals":2,"weiDecimals":8,"index":209,"tokenId":"0x346d925b4592c989ff2433dd13a4ec1a","isCanonical":false,"evmContract":null,"fullName":"Tilt.Fun","deployerTradingFeeShare":"1.0"},{"name":"SPR","szDecimals":2,"weiDecimals":8,"index":210,"tokenId":"0x3b68a2d22a7efa9cd1ef20706a8658b6","isCanonical":false,"evmContract":null,"fullName":"Supurr App","deployerTradingFeeShare":"1.0"},{"name":"HIPPO","szDecimals":1,"weiDecimals":6,"index":211,"tokenId":"0x42b217dcbcc3fd263935f755696070fe","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"1.0"},{"name":"HANA","szDecimals":2,"weiDecimals":8,"index":212,"tokenId":"0x661492e8a1f4202ed4294ef3c96c9be6","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"1.0"},{"name":"SWAP","szDecimals":2,"weiDecimals":8,"index":213,"tokenId":"0x1684cb225aa305ea5be44f2a79214e7e","isCanonical":false,"evmContract":null,"fullName":"HyperSwap","deployerTradingFeeShare":"1.0"},{"name":"MORE","szDecimals":0,"weiDecimals":5,"index":214,"tokenId":"0xdce99e9dc0c51e09bd36920b929771eb","isCanonical":false,"evmContract":null,"fullName":"Moonveil","deployerTradingFeeShare":"1.0"},{"name":"LOOT","szDecimals":0,"weiDecimals":7,"index":215,"tokenId":"0xaebc7b19a35ec0a726962772aacc9e7a","isCanonical":false,"evmContract":null,"fullName":"LiquidLoot","deployerTradingFeeShare":"1.0"},{"name":"LUMI","szDecimals":0,"weiDecimals":6,"index":216,"tokenId":"0x4c06e1d30e9116751344a0fb9627b0aa","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"1.0"},{"name":"WHYPI","szDecimals":2,"weiDecimals":8,"index":217,"tokenId":"0x8c6028c318586b2e69cd1848a9ca6434","isCanonical":false,"evmContract":null,"fullName":"HYPI HRC-20","deployerTradingFeeShare":"1.0"},{"name":"ANON","szDecimals":2,"weiDecimals":8,"index":218,"tokenId":"0x5b56ac883c628df1fe24d463b3ac82ac","isCanonical":false,"evmContract":{"address":"0x79bbf4508b1391af3a0f4b30bb5fc4aa9ab0e07c","evm_extra_wei_decimals":10},"fullName":"HeyAnon","deployerTradingFeeShare":"1.0"},{"name":"OTTI","szDecimals":2,"weiDecimals":8,"index":219,"tokenId":"0x2d7b2c50be8a8f2d16c20cbae448109f","isCanonical":false,"evmContract":{"address":"0xe45b3fc1f0032532e3d8a3c141cd258bd3eed368","evm_extra_wei_decimals":10},"fullName":"Odd Otties: Cute otters will overtake hyperliquid","deployerTradingFeeShare":"1.0"},{"name":"HORSY","szDecimals":2,"weiDecimals":8,"index":220,"tokenId":"0x259267575e6da365e8d6292a7640e675","isCanonical":false,"evmContract":{"address":"0xd918e114a473ebb3c7688d9a7373838582cb0539","evm_extra_wei_decimals":10},"fullName":"$HORSY","deployerTradingFeeShare":"1.0"},{"name":"UETH","szDecimals":4,"weiDecimals":9,"index":221,"tokenId":"0xe1edd30daaf5caac3fe63569e24748da","isCanonical":false,"evmContract":{"address":"0xbe6727b535545c67d5caa73dea54865b92cf7907","evm_extra_wei_decimals":9},"fullName":"Unit Ethereum","deployerTradingFeeShare":"1.0"},{"name":"BUDDY","szDecimals":2,"weiDecimals":8,"index":222,"tokenId":"0x84ecb2340931f6b153ff10bcfafb6726","isCanonical":false,"evmContract":{"address":"0x47bb061c0204af921f43dc73c7d7768d2672ddee","evm_extra_wei_decimals":-2},"fullName":"alright buddy","deployerTradingFeeShare":"1.0"},{"name":"LATINA","szDecimals":2,"weiDecimals":8,"index":223,"tokenId":"0x9774dccfb6ea1667ea499babe9de388d","isCanonical":false,"evmContract":{"address":"0xe7eaa46c2ac8470d622ada1538fede6242cebe53","evm_extra_wei_decimals":10},"fullName":null,"deployerTradingFeeShare":"0.0"},{"name":"WLFI","szDecimals":2,"weiDecimals":8,"index":224,"tokenId":"0x675abb1741c0153bfb51c4298ac9e4b7","isCanonical":false,"evmContract":null,"fullName":"World Liberty Financial","deployerTradingFeeShare":"0.0"},{"name":"AIR","szDecimals":2,"weiDecimals":8,"index":225,"tokenId":"0xd04d518a78fdda67e06c7ce42c9ac9e0","isCanonical":false,"evmContract":null,"fullName":"AIR","deployerTradingFeeShare":"1.0"},{"name":"K","szDecimals":2,"weiDecimals":8,"index":226,"tokenId":"0x02cd8d640cf9be38bf9335213f227976","isCanonical":false,"evmContract":null,"fullName":"Kinto","deployerTradingFeeShare":"0.01"},{"name":"PLUME","szDecimals":2,"weiDecimals":8,"index":227,"tokenId":"0x8a008852ff36d4303690fb168d6cc542","isCanonical":false,"evmContract":{"address":"0xcc0f1a2a451fb7bb0c97602e890ef0fe8b6b2e15","evm_extra_wei_decimals":10},"fullName":"Plume","deployerTradingFeeShare":"1.0"},{"name":"LIM","szDecimals":2,"weiDecimals":8,"index":228,"tokenId":"0xb4215c475856b8487e68e2fbdf314133","isCanonical":false,"evmContract":null,"fullName":"Liminal","deployerTradingFeeShare":"1.0"},{"name":"HUSD","szDecimals":1,"weiDecimals":6,"index":229,"tokenId":"0x2d8e9750853bde71a1262a73c0025f99","isCanonical":false,"evmContract":null,"fullName":"Halo USD","deployerTradingFeeShare":"1.0"},{"name":"DHYPE","szDecimals":2,"weiDecimals":8,"index":230,"tokenId":"0xd526b265e85aa057b7a8c45383e0fb75","isCanonical":false,"evmContract":null,"fullName":"Hyperdrive Staked HYPE","deployerTradingFeeShare":"1.0"},{"name":"EDA","szDecimals":0,"weiDecimals":5,"index":231,"tokenId":"0x7577b44c0c4eab39341cba1370681a96","isCanonical":false,"evmContract":null,"fullName":"EDAMAME","deployerTradingFeeShare":"1.0"},{"name":"FUND","szDecimals":2,"weiDecimals":8,"index":232,"tokenId":"0xb50907c7421090108069db4681949f19","isCanonical":false,"evmContract":{"address":"0x89b0fe193887d731a2b48a5d87a1185214482ad5","evm_extra_wei_decimals":10},"fullName":"HL Fund","deployerTradingFeeShare":"1.0"},{"name":"EXP","szDecimals":2,"weiDecimals":8,"index":233,"tokenId":"0xf5a960c12ff925c3b7e1b34ca46ad147","isCanonical":false,"evmContract":null,"fullName":"Exponents","deployerTradingFeeShare":"1.0"},{"name":"SENTI","szDecimals":1,"weiDecimals":8,"index":234,"tokenId":"0xfc2d496a192e696d134dfbc974de1732","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"1.0"},{"name":"USDE","szDecimals":2,"weiDecimals":8,"index":235,"tokenId":"0x2e6d84f2d7ca82e6581e03523e4389f7","isCanonical":false,"evmContract":{"address":"0x5d3a1ff2b6bab83b63cd9ad0787074081a52ef34","evm_extra_wei_decimals":10},"fullName":"USDe","deployerTradingFeeShare":"0.0"},{"name":"HIP","szDecimals":2,"weiDecimals":8,"index":236,"tokenId":"0xdbc8946c19ab6d89d1ce64d08a5914f7","isCanonical":false,"evmContract":null,"fullName":"HyppieLiquid","deployerTradingFeeShare":"1.0"},{"name":"SLAY","szDecimals":1,"weiDecimals":6,"index":237,"tokenId":"0xf9dadbe873e83783630c7089320f361a","isCanonical":false,"evmContract":{"address":"0x9efdfc72c1bc90b36f8aae35247e62310c58eacf","evm_extra_wei_decimals":0},"fullName":"SatLayer","deployerTradingFeeShare":"1.0"},{"name":"B","szDecimals":2,"weiDecimals":8,"index":238,"tokenId":"0x8bf4496242db4851312e0d5895bf1504","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"1.0"},{"name":"USDXL","szDecimals":2,"weiDecimals":7,"index":239,"tokenId":"0xf448c3cad413cdf0feb1746d7b057967","isCanonical":false,"evmContract":{"address":"0xca79db4b49f608ef54a5cb813fbed3a6387bc645","evm_extra_wei_decimals":11},"fullName":"Last USD","deployerTradingFeeShare":"1.0"},{"name":"USH","szDecimals":2,"weiDecimals":8,"index":240,"tokenId":"0x97697ab0da91ab1c7db063b52a93bdbb","isCanonical":false,"evmContract":{"address":"0x8ff0dd9f9c40a0d76ef1bcfaf5f98c1610c74bd8","evm_extra_wei_decimals":10},"fullName":null,"deployerTradingFeeShare":"1.0"},{"name":"FEUSD","szDecimals":2,"weiDecimals":8,"index":241,"tokenId":"0x88102bea0bbad5f301f6e9e4dacdf979","isCanonical":false,"evmContract":{"address":"0x02c6a2fa58cc01a18b8d9e00ea48d65e4df26c70","evm_extra_wei_decimals":10},"fullName":"Felix USD","deployerTradingFeeShare":"1.0"},{"name":"COOK","szDecimals":2,"weiDecimals":8,"index":242,"tokenId":"0x2e2c62335d06f2a44d703c66501ce4d6","isCanonical":false,"evmContract":{"address":"0x9f0c013016e8656bc256f948cd4b79ab25c7b94d","evm_extra_wei_decimals":10},"fullName":"mETH Protocol","deployerTradingFeeShare":"1.0"},{"name":"OKI","szDecimals":2,"weiDecimals":8,"index":243,"tokenId":"0xc19bf809d69f87f6b0e8bcf840814e94","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"1.0"},{"name":"BORG","szDecimals":2,"weiDecimals":8,"index":244,"tokenId":"0x69c7d7659a582b35187d364977931245","isCanonical":false,"evmContract":null,"fullName":"SwissBorg","deployerTradingFeeShare":"1.0"},{"name":"SIGN","szDecimals":2,"weiDecimals":8,"index":245,"tokenId":"0xee794f997a8c9c83c1e23fcc7731369a","isCanonical":false,"evmContract":null,"fullName":"Sign","deployerTradingFeeShare":"1.0"},{"name":"WMNT","szDecimals":2,"weiDecimals":8,"index":246,"tokenId":"0x08e0dbf94cc66a239e00baefdd9ac2f6","isCanonical":false,"evmContract":{"address":"0x36721e62edea413dc5195c4ca9c5a7eb175feb6b","evm_extra_wei_decimals":10},"fullName":"Mantle","deployerTradingFeeShare":"1.0"},{"name":"KITTEN","szDecimals":2,"weiDecimals":8,"index":247,"tokenId":"0x3bd699153fef2806ffe90f3175d916db","isCanonical":false,"evmContract":null,"fullName":"Kittenswap","deployerTradingFeeShare":"1.0"},{"name":"USR","szDecimals":2,"weiDecimals":8,"index":248,"tokenId":"0x44759ebad788666b5ab35f4e20cea722","isCanonical":false,"evmContract":{"address":"0x0ad339d66bf4aed5ce31c64bc37b3244b6394a77","evm_extra_wei_decimals":10},"fullName":"Resolv USR","deployerTradingFeeShare":"1.0"},{"name":"LEND","szDecimals":2,"weiDecimals":8,"index":249,"tokenId":"0x6cf1d9a259f3797734535a7820d58b51","isCanonical":false,"evmContract":null,"fullName":"Sentiment","deployerTradingFeeShare":"1.0"},{"name":"CATH","szDecimals":2,"weiDecimals":8,"index":250,"tokenId":"0x8e1cc390b36f72e372af7dc9949cca3c","isCanonical":false,"evmContract":null,"fullName":"CATHENA AI","deployerTradingFeeShare":"1.0"},{"name":"QUANT","szDecimals":2,"weiDecimals":8,"index":251,"tokenId":"0x68c563790c975869358031d0e42e49a1","isCanonical":false,"evmContract":{"address":"0xe443d488a8988262f35b921b36f1c8f5b2fa38e1","evm_extra_wei_decimals":10},"fullName":"HypurrQuant","deployerTradingFeeShare":"1.0"},{"name":"ONLYUP","szDecimals":0,"weiDecimals":5,"index":252,"tokenId":"0xfadb70f442b61aaf3896ae79c3e1b044","isCanonical":false,"evmContract":null,"fullName":"DOUBLEUP","deployerTradingFeeShare":"1.0"},{"name":"RAT","szDecimals":2,"weiDecimals":8,"index":253,"tokenId":"0xd7f58d3c744fbc0100f7d86f3e283d11","isCanonical":false,"evmContract":{"address":"0xfdc42f844f3aa19f8fc0b2d67d3276cf1efdc4ca","evm_extra_wei_decimals":10},"fullName":"Rat Race Combat: Skill, Gems, Crypto. ๐Ÿ€๐Ÿ’Ž","deployerTradingFeeShare":"1.0"},{"name":"USOL","szDecimals":3,"weiDecimals":8,"index":254,"tokenId":"0x49b67c39f5566535de22b29b0e51e685","isCanonical":false,"evmContract":{"address":"0x068f321fa8fb9f0d135f290ef6a3e2813e1c8a29","evm_extra_wei_decimals":1},"fullName":"Unit Solana","deployerTradingFeeShare":"1.0"},{"name":"PURRO","szDecimals":2,"weiDecimals":8,"index":255,"tokenId":"0xaea9bf1dcd2a5de3cd70260ada52c4fa","isCanonical":false,"evmContract":{"address":"0x713f4c66221c3920578675dfc45cfa71b5f1f307","evm_extra_wei_decimals":10},"fullName":"Purro","deployerTradingFeeShare":"1.0"},{"name":"KITTY","szDecimals":2,"weiDecimals":8,"index":256,"tokenId":"0x29337e3b3bb954244d4b384b5b3c9c24","isCanonical":false,"evmContract":{"address":"0x04d64b58941d8699f574fb77cb9069971a2097b8","evm_extra_wei_decimals":10},"fullName":"A fully on-chain game built on Hyperliquid.","deployerTradingFeeShare":"1.0"},{"name":"TREND","szDecimals":2,"weiDecimals":7,"index":257,"tokenId":"0x34fc4f2dac4e69240efefd86729d6627","isCanonical":false,"evmContract":null,"fullName":"Trend Protocol","deployerTradingFeeShare":"1.0"},{"name":"COIN","szDecimals":2,"weiDecimals":8,"index":258,"tokenId":"0x3e3a2bdcd2db04b313384e1c42eaa7ae","isCanonical":false,"evmContract":null,"fullName":"W","deployerTradingFeeShare":"1.0"},{"name":"STORM","szDecimals":0,"weiDecimals":5,"index":259,"tokenId":"0x3d9b9406190361a09bc46ec965f2f087","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"1.0"},{"name":"RISK","szDecimals":2,"weiDecimals":8,"index":260,"tokenId":"0x210ba34d337330d07443d1dc488bf811","isCanonical":false,"evmContract":null,"fullName":"RISK","deployerTradingFeeShare":"1.0"},{"name":"PLAY","szDecimals":2,"weiDecimals":8,"index":261,"tokenId":"0xcdd43c66ff2c2967bbda9fe3bb385d9f","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"1.0"},{"name":"NET","szDecimals":2,"weiDecimals":8,"index":262,"tokenId":"0xf79028d078d6180d4617a72ec4bef202","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"1.0"},{"name":"LOOP","szDecimals":2,"weiDecimals":8,"index":263,"tokenId":"0xc77768ff84182dbf7157412f544529aa","isCanonical":false,"evmContract":null,"fullName":"Looping Collective","deployerTradingFeeShare":"1.0"},{"name":"LTHREE","szDecimals":2,"weiDecimals":8,"index":264,"tokenId":"0x3d1cf3daf5faca4bccf7853e11383fa4","isCanonical":false,"evmContract":{"address":"0x46777c76dbbe40fabb2aab99e33ce20058e76c59","evm_extra_wei_decimals":10},"fullName":"Layer3","deployerTradingFeeShare":"1.0"},{"name":"WELL","szDecimals":2,"weiDecimals":8,"index":265,"tokenId":"0x74ac699fbf35540ac5407e5d49f5a044","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"1.0"},{"name":"WAVE","szDecimals":0,"weiDecimals":5,"index":266,"tokenId":"0x6beb0b9433ea24b5932f87e7d44b4c51","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"1.0"},{"name":"ZAP","szDecimals":2,"weiDecimals":8,"index":267,"tokenId":"0x67e79f5a7be4eb7ab0da1581be098062","isCanonical":false,"evmContract":null,"fullName":"HyperZap","deployerTradingFeeShare":"1.0"},{"name":"USDT0","szDecimals":2,"weiDecimals":8,"index":268,"tokenId":"0x25faedc3f054130dbb4e4203aca63567","isCanonical":false,"evmContract":{"address":"0xb8ce59fc3717ada4c02eadf9682a9e934f625ebb","evm_extra_wei_decimals":-2},"fullName":"USDT0","deployerTradingFeeShare":"0.0"},{"name":"UFART","szDecimals":1,"weiDecimals":6,"index":269,"tokenId":"0x7650808198966e4285687d3deb556ccc","isCanonical":false,"evmContract":{"address":"0x3b4575e689ded21caad31d64c4df1f10f3b2cedf","evm_extra_wei_decimals":0},"fullName":"Unit Fartcoin","deployerTradingFeeShare":"1.0"},{"name":"BAPE","szDecimals":2,"weiDecimals":8,"index":270,"tokenId":"0x52d448847d2b754285d0cdce8a0bbee9","isCanonical":false,"evmContract":{"address":"0xab11329560fa9c9c860bb21a9342215a1265bbb0","evm_extra_wei_decimals":10},"fullName":"ApeCoin","deployerTradingFeeShare":"1.0"},{"name":"DIABLO","szDecimals":2,"weiDecimals":8,"index":271,"tokenId":"0x9348a089d6fd81ec48699e0ec949480b","isCanonical":false,"evmContract":null,"fullName":"Diablo โ€“ The little demon of HyperLiquid","deployerTradingFeeShare":"1.0"},{"name":"PENIS","szDecimals":2,"weiDecimals":8,"index":272,"tokenId":"0xf86eb43cc683110ce33d7a86f674cfbd","isCanonical":false,"evmContract":{"address":"0xf0c82d188ee54958813e7ac650e119135fc35e94","evm_extra_wei_decimals":10},"fullName":"Purr's Elite Network of Interactive Systems","deployerTradingFeeShare":"1.0"},{"name":"LBS","szDecimals":2,"weiDecimals":8,"index":273,"tokenId":"0x3834dd888f9766e2fe8907d54c4c90ad","isCanonical":false,"evmContract":null,"fullName":"Lootbase","deployerTradingFeeShare":"1.0"},{"name":"EBISU","szDecimals":2,"weiDecimals":8,"index":274,"tokenId":"0x6240595cb860815b6f1e36b3b77d1c6a","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"1.0"},{"name":"DEFI","szDecimals":2,"weiDecimals":8,"index":275,"tokenId":"0xd815fbce8df4daca2c2e22ee8851d65b","isCanonical":false,"evmContract":null,"fullName":"DeFi Dollar","deployerTradingFeeShare":"1.0"},{"name":"END","szDecimals":2,"weiDecimals":8,"index":276,"tokenId":"0x15dcde35463dd2fd241a8e2fe82e48a8","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"1.0"},{"name":"RUB","szDecimals":5,"weiDecimals":10,"index":277,"tokenId":"0x0921e26b2409bdbb47989ce5692f595f","isCanonical":false,"evmContract":{"address":"0x7dcffcb06b40344eeced2d1cbf096b299fe4b405","evm_extra_wei_decimals":8},"fullName":"Reverse Unit Bias","deployerTradingFeeShare":"1.0"},{"name":"ASF","szDecimals":2,"weiDecimals":8,"index":278,"tokenId":"0x0692dac3b63105879d1b9cf14a57d30c","isCanonical":false,"evmContract":null,"fullName":"Asymmetry Finance","deployerTradingFeeShare":"1.0"},{"name":"BEAT","szDecimals":2,"weiDecimals":8,"index":279,"tokenId":"0xfb41962c0778da46c4cb67482019b0cd","isCanonical":false,"evmContract":null,"fullName":"Hyperbeat","deployerTradingFeeShare":"1.0"},{"name":"VAL","szDecimals":2,"weiDecimals":8,"index":280,"tokenId":"0xab665a1c43d3c04c7021032a56bf1b04","isCanonical":false,"evmContract":null,"fullName":"VALANTIS","deployerTradingFeeShare":"1.0"},{"name":"TRADE","szDecimals":2,"weiDecimals":8,"index":281,"tokenId":"0xcb6bac63eddb234edf8d694301fcdbd3","isCanonical":false,"evmContract":null,"fullName":"Trade.fun","deployerTradingFeeShare":"1.0"},{"name":"URBIT","szDecimals":0,"weiDecimals":5,"index":282,"tokenId":"0xea5feba80278099b4f7be0bc554e9b78","isCanonical":false,"evmContract":null,"fullName":"Urbit Address Space","deployerTradingFeeShare":"1.0"},{"name":"TREE","szDecimals":2,"weiDecimals":8,"index":283,"tokenId":"0xdecbff6efe00db0fb0e80be2b2e06dab","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"1.0"},{"name":"FI","szDecimals":2,"weiDecimals":8,"index":284,"tokenId":"0xba02a3914ecdddb7e398112733823a84","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"1.0"},{"name":"STAKE","szDecimals":2,"weiDecimals":8,"index":285,"tokenId":"0xf228badd8bb9b73985267556f06aa38e","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"1.0"},{"name":"NQ","szDecimals":2,"weiDecimals":8,"index":286,"tokenId":"0xcfe4b91486b3fe0377e38433494b688b","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"1.0"},{"name":"BLND","szDecimals":2,"weiDecimals":8,"index":287,"tokenId":"0x1e7da953f0ba78bf06d2e330bb466199","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"1.0"},{"name":"WAR","szDecimals":2,"weiDecimals":8,"index":288,"tokenId":"0xda37ed48536836994807abb934062694","isCanonical":false,"evmContract":null,"fullName":"Hyperliquid vs FUD","deployerTradingFeeShare":"1.0"},{"name":"PERP","szDecimals":2,"weiDecimals":8,"index":289,"tokenId":"0x27e2e115b3067cb20f18a44b067ade73","isCanonical":false,"evmContract":{"address":"0xd2567ee20d75e8b74b44875173054365f6eb5052","evm_extra_wei_decimals":-2},"fullName":"Perpcoin","deployerTradingFeeShare":"0.5"},{"name":"DNDX","szDecimals":2,"weiDecimals":8,"index":290,"tokenId":"0x61548910ed6b5e4e1f136907737dd499","isCanonical":false,"evmContract":null,"fullName":"Decentralized Nasdaq","deployerTradingFeeShare":"1.0"},{"name":"USDHL","szDecimals":2,"weiDecimals":7,"index":291,"tokenId":"0xd289c79872a9eace15cc4cadb030661f","isCanonical":false,"evmContract":{"address":"0xb50a96253abdf803d85efcdce07ad8becbc52bd5","evm_extra_wei_decimals":-1},"fullName":"Hyper USD","deployerTradingFeeShare":"1.0"},{"name":"HPENGU","szDecimals":2,"weiDecimals":8,"index":292,"tokenId":"0x1765b5a9ec8fc3cdbc209c63cac68e86","isCanonical":false,"evmContract":{"address":"0xfa44c2634ff17cbe26dc3007d36bd61c79068c14","evm_extra_wei_decimals":10},"fullName":"Pudgy Penguins","deployerTradingFeeShare":"1.0"},{"name":"PAIR","szDecimals":2,"weiDecimals":7,"index":293,"tokenId":"0xa29869e9df2d282afa5e2c1a6ca091ba","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"1.0"},{"name":"HOLY","szDecimals":2,"weiDecimals":7,"index":294,"tokenId":"0xa1cb5a48e9dbce0771f3f501729c44af","isCanonical":false,"evmContract":null,"fullName":"Holy Liquid","deployerTradingFeeShare":"1.0"},{"name":"SING","szDecimals":2,"weiDecimals":8,"index":295,"tokenId":"0x8a35738573d89ee4a3a4d7e8e46851c0","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"0.5"},{"name":"JOFF","szDecimals":2,"weiDecimals":8,"index":296,"tokenId":"0x8ecbaadbaf3f59a7e9f97af2688d479d","isCanonical":false,"evmContract":null,"fullName":"bildin a purrty gud el 1","deployerTradingFeeShare":"1.0"},{"name":"XAUT0","szDecimals":2,"weiDecimals":8,"index":297,"tokenId":"0xfd61ec89811ba3cf2ae12d0ed8ef1afd","isCanonical":false,"evmContract":{"address":"0xf4d9235269a96aadafc9adae454a0618ebe37949","evm_extra_wei_decimals":-2},"fullName":"XAUT0","deployerTradingFeeShare":"1.0"},{"name":"HY","szDecimals":2,"weiDecimals":8,"index":298,"tokenId":"0xe0c5df65121e4aa8bcef413fdfd43fbd","isCanonical":false,"evmContract":null,"fullName":"HyDAO","deployerTradingFeeShare":"0.0"},{"name":"UPUMP","szDecimals":0,"weiDecimals":6,"index":299,"tokenId":"0x544e60f98a36d7b22c0fb5824b84f795","isCanonical":false,"evmContract":{"address":"0x27ec642013bcb3d80ca3706599d3cda04f6f4452","evm_extra_wei_decimals":0},"fullName":"Unit Pump Fun","deployerTradingFeeShare":"1.0"},{"name":"XP","szDecimals":2,"weiDecimals":8,"index":300,"tokenId":"0x64c26b5cb4bdf2ce7f38c3ccfaed90cd","isCanonical":false,"evmContract":null,"fullName":"XP","deployerTradingFeeShare":"1.0"},{"name":"HWAVE","szDecimals":2,"weiDecimals":8,"index":301,"tokenId":"0x0a036982bc089f1f9da1a664a698948d","isCanonical":false,"evmContract":null,"fullName":"Hyperwave","deployerTradingFeeShare":"1.0"},{"name":"WHLP","szDecimals":2,"weiDecimals":8,"index":302,"tokenId":"0x75dfa57dcf7949c37bc7947b121a3dda","isCanonical":false,"evmContract":null,"fullName":"Wrapped HLP","deployerTradingFeeShare":"1.0"},{"name":"USDY","szDecimals":2,"weiDecimals":8,"index":303,"tokenId":"0x36ce8f49a71509ec6d2c1c946ed5b845","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"1.0"},{"name":"STLOOP","szDecimals":2,"weiDecimals":8,"index":304,"tokenId":"0xc41d4a266072f72005ba7bc56374fa84","isCanonical":false,"evmContract":{"address":"0x502ee789b448aa692901fe27ab03174c90f07dd1","evm_extra_wei_decimals":10},"fullName":"Staked LOOP","deployerTradingFeeShare":"1.0"},{"name":"HWHLP","szDecimals":2,"weiDecimals":8,"index":305,"tokenId":"0x27ef6e651e5dde63988ba030657f155f","isCanonical":false,"evmContract":null,"fullName":"Hyperwave HLP","deployerTradingFeeShare":"1.0"},{"name":"SSR","szDecimals":1,"weiDecimals":6,"index":306,"tokenId":"0xad96194aea68cf8416366deb310f13a4","isCanonical":false,"evmContract":null,"fullName":"STRATEGIC SOLANA RESERVE","deployerTradingFeeShare":"1.0"},{"name":"LICKO","szDecimals":2,"weiDecimals":8,"index":307,"tokenId":"0xaf5f46cd9a9fc7f7028e1028b0c5182b","isCanonical":false,"evmContract":null,"fullName":"LICKO","deployerTradingFeeShare":"1.0"},{"name":"THLP","szDecimals":0,"weiDecimals":5,"index":308,"tokenId":"0x4d10c3c44a3113fddbf81cb52a9eec83","isCanonical":false,"evmContract":null,"fullName":"Tokenized Hyperliquidity Provider","deployerTradingFeeShare":"1.0"},{"name":"DUSD","szDecimals":2,"weiDecimals":8,"index":309,"tokenId":"0x38714be3e734fb386dc1ba1d095d7916","isCanonical":false,"evmContract":null,"fullName":"DeFi Dollar","deployerTradingFeeShare":"0.0"},{"name":"HWHYPE","szDecimals":2,"weiDecimals":8,"index":310,"tokenId":"0xbbb85f3bb755eda63ccb5a4e6e254357","isCanonical":false,"evmContract":null,"fullName":"Hyperwave HYPE","deployerTradingFeeShare":"1.0"},{"name":"MONEY","szDecimals":2,"weiDecimals":7,"index":311,"tokenId":"0x56d1e8ab006c7cb8538cec106055184c","isCanonical":false,"evmContract":null,"fullName":"HYPEMAN","deployerTradingFeeShare":"1.0"},{"name":"USPYX","szDecimals":2,"weiDecimals":8,"index":312,"tokenId":"0xc0fe8a28e4fbcfb7c6cb298c8ae0f206","isCanonical":false,"evmContract":null,"fullName":"Unit SP500 xStock","deployerTradingFeeShare":"1.0"},{"name":"WQWQ","szDecimals":2,"weiDecimals":8,"index":313,"tokenId":"0x2e460c1d64da65af87114102c8a18e9c","isCanonical":false,"evmContract":null,"fullName":"qwqwqw","deployerTradingFeeShare":"1.0"},{"name":"BLUAI","szDecimals":2,"weiDecimals":8,"index":314,"tokenId":"0xb874bd0e4379299dcd3936a75a077f35","isCanonical":false,"evmContract":null,"fullName":"Bluwhale AI Token","deployerTradingFeeShare":"1.0"},{"name":"FITTED","szDecimals":2,"weiDecimals":8,"index":315,"tokenId":"0x8cc1664bf6f230efc42035bbb30a4e7d","isCanonical":false,"evmContract":null,"fullName":"FITCOIN","deployerTradingFeeShare":"1.0"},{"name":"EX","szDecimals":1,"weiDecimals":6,"index":316,"tokenId":"0x92c54320963722f8c76be61a8e655fb9","isCanonical":false,"evmContract":null,"fullName":"Yolo ex","deployerTradingFeeShare":"1.0"},{"name":"HREKT","szDecimals":0,"weiDecimals":5,"index":317,"tokenId":"0x84354e8267d96a9a5a665ce1533e21e1","isCanonical":false,"evmContract":{"address":"0xc12e26ab94d1e62f18373cf25e43afa7b2af1cdc","evm_extra_wei_decimals":13},"fullName":"Rekt","deployerTradingFeeShare":"1.0"},{"name":"WOULD","szDecimals":1,"weiDecimals":6,"index":318,"tokenId":"0x7f33d750065fe8ee53ac8d8195808e4c","isCanonical":false,"evmContract":{"address":"0xf855572485de4c8bc33905f58d0c21cf4df3d6c7","evm_extra_wei_decimals":12},"fullName":"would","deployerTradingFeeShare":"1.0"},{"name":"UUUSPX","szDecimals":1,"weiDecimals":8,"index":319,"tokenId":"0x2ff71b802a6788a052c7f1a58ec863af","isCanonical":false,"evmContract":null,"fullName":"Unit SPX6900","deployerTradingFeeShare":"1.0"},{"name":"UBONK","szDecimals":0,"weiDecimals":5,"index":320,"tokenId":"0xb113d34e351cf195733c98442530c099","isCanonical":false,"evmContract":null,"fullName":"Unit Bonk","deployerTradingFeeShare":"1.0"},{"name":"VOL","szDecimals":2,"weiDecimals":8,"index":321,"tokenId":"0x0a350cf931ca6e8b1f15c8ff40b68ae2","isCanonical":false,"evmContract":null,"fullName":"Volmex","deployerTradingFeeShare":"1.0"},{"name":"WILD","szDecimals":2,"weiDecimals":8,"index":322,"tokenId":"0x31200a6a0474f01b109dbb45dd4b0b35","isCanonical":false,"evmContract":null,"fullName":"Wildmeta","deployerTradingFeeShare":"1.0"},{"name":"THBILL","szDecimals":2,"weiDecimals":8,"index":323,"tokenId":"0x3d4d3796a3d34c8693d087ad266950e5","isCanonical":false,"evmContract":{"address":"0xfdd22ce6d1f66bc0ec89b20bf16ccb6670f55a5a","evm_extra_wei_decimals":-2},"fullName":"Theo Short Duration US Treasury Fund","deployerTradingFeeShare":"1.0"},{"name":"BERRIE","szDecimals":2,"weiDecimals":8,"index":324,"tokenId":"0x67230d3abe2373714d30934edad26162","isCanonical":false,"evmContract":null,"fullName":"Berr.ie","deployerTradingFeeShare":"0.78"},{"name":"VSN","szDecimals":2,"weiDecimals":8,"index":325,"tokenId":"0xf9969f85c507bf85c1a516f0ecc8478f","isCanonical":false,"evmContract":{"address":"0x31185950db028ecfc70df6a35a4b552462a35773","evm_extra_wei_decimals":10},"fullName":"Vision","deployerTradingFeeShare":"1.0"},{"name":"UMOG","szDecimals":1,"weiDecimals":6,"index":326,"tokenId":"0xe839668425f85cc7d6ed2fb1edee3e88","isCanonical":false,"evmContract":null,"fullName":"Unit Mog Coin (m)","deployerTradingFeeShare":"1.0"},{"name":"BALT","szDecimals":2,"weiDecimals":8,"index":327,"tokenId":"0xebe9c3c1c24d30d2436694b64273b4e8","isCanonical":false,"evmContract":null,"fullName":"AltLayer Token","deployerTradingFeeShare":"1.0"},{"name":"TOSHI","szDecimals":0,"weiDecimals":8,"index":328,"tokenId":"0x18ecab0b67760ccb8a85edc52bba0ea9","isCanonical":false,"evmContract":null,"fullName":"Toshi","deployerTradingFeeShare":"1.0"},{"name":"HUSDE","szDecimals":2,"weiDecimals":8,"index":329,"tokenId":"0x576a5a12ff24d3b6bdec80737707f493","isCanonical":false,"evmContract":null,"fullName":"Hyper USDe","deployerTradingFeeShare":"1.0"},{"name":"BAYC","szDecimals":2,"weiDecimals":8,"index":330,"tokenId":"0x72c8258401943a45698be4062a45dac9","isCanonical":false,"evmContract":{"address":"0x23a5594ec077475cc466367d0b05222da206b906","evm_extra_wei_decimals":10},"fullName":null,"deployerTradingFeeShare":"1.0"},{"name":"YUMA","szDecimals":1,"weiDecimals":6,"index":331,"tokenId":"0xb8692d675d9414f9a77f57f24266f098","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"1.0"},{"name":"DBLN","szDecimals":2,"weiDecimals":8,"index":332,"tokenId":"0x85a17122bf4711227397feeab9862975","isCanonical":false,"evmContract":{"address":"0x5e29cb5120692a813bf166f2dd7b140b4dbf61b9","evm_extra_wei_decimals":-2},"fullName":"Doubloon","deployerTradingFeeShare":"1.0"},{"name":"RYSK","szDecimals":2,"weiDecimals":8,"index":333,"tokenId":"0x6fbecac28f85b141d9e38be88e45281d","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"1.0"},{"name":"AURA","szDecimals":1,"weiDecimals":6,"index":334,"tokenId":"0xc69352e2d17a3d56230caf6984eabcdc","isCanonical":false,"evmContract":null,"fullName":"Unit AURA","deployerTradingFeeShare":"1.0"},{"name":"KEI","szDecimals":2,"weiDecimals":8,"index":335,"tokenId":"0xebe075b89fd49398cce2f786906ac6b3","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"1.0"},{"name":"BULL","szDecimals":1,"weiDecimals":6,"index":336,"tokenId":"0x6415fc21938afec00553c79866a9e172","isCanonical":false,"evmContract":null,"fullName":"Bullpen","deployerTradingFeeShare":"1.0"},{"name":"DEXARI","szDecimals":2,"weiDecimals":8,"index":337,"tokenId":"0xf7bfc122ddd90eddb723bd7dc2bf4f2d","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"1.0"},{"name":"UENA","szDecimals":1,"weiDecimals":6,"index":338,"tokenId":"0x593494b6af79172fa983a0cf1c88e0e0","isCanonical":false,"evmContract":null,"fullName":"Unit Ethena","deployerTradingFeeShare":"1.0"},{"name":"BASED","szDecimals":1,"weiDecimals":6,"index":339,"tokenId":"0xbe946bd7862245615d6729ec5b67cc7e","isCanonical":false,"evmContract":null,"fullName":"Based Token","deployerTradingFeeShare":"1.0"},{"name":"REX","szDecimals":1,"weiDecimals":8,"index":340,"tokenId":"0x6d7473f5efcb0989ccddd3e123ddb1d7","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"1.0"},{"name":"LINK0","szDecimals":2,"weiDecimals":8,"index":341,"tokenId":"0xb99e5be3b0a18c37940a7ab0c204b328","isCanonical":false,"evmContract":{"address":"0x9a12cb8869498d8826567437abea27be1c2ba9ab","evm_extra_wei_decimals":10},"fullName":"LayerZero powered LINK deployed by HyBridge","deployerTradingFeeShare":"1.0"},{"name":"PKMN","szDecimals":5,"weiDecimals":10,"index":342,"tokenId":"0x826900f7ecb54fb386573f4610bcf421","isCanonical":false,"evmContract":null,"fullName":"PKMN","deployerTradingFeeShare":"1.0"},{"name":"UXPL","szDecimals":1,"weiDecimals":6,"index":343,"tokenId":"0x2c54c60600e1d786b2dfc139a38a5a99","isCanonical":false,"evmContract":{"address":"0x33af3c2540ba72054e044efe504867b39ae421f5","evm_extra_wei_decimals":12},"fullName":"Unit Plasma","deployerTradingFeeShare":"1.0"},{"name":"TRX0","szDecimals":2,"weiDecimals":8,"index":344,"tokenId":"0xc829e0ed85ed339680740c3cf170515b","isCanonical":false,"evmContract":null,"fullName":"$TRX on Hyperliquid. Deployed by HyBridge.","deployerTradingFeeShare":"1.0"},{"name":"STRD","szDecimals":2,"weiDecimals":8,"index":345,"tokenId":"0x545f09adaa44427db0ec7ac958c6c1bc","isCanonical":false,"evmContract":null,"fullName":"Stride","deployerTradingFeeShare":"1.0"},{"name":"AAVE0","szDecimals":2,"weiDecimals":8,"index":346,"tokenId":"0x49259bc8e57d4a039fd41f9b40d1a3a6","isCanonical":false,"evmContract":{"address":"0xaa3871e64ca8c09fbdded327b1269224643cb83c","evm_extra_wei_decimals":10},"fullName":"AAVE spot asset deployed by HyBridge","deployerTradingFeeShare":"1.0"},{"name":"AVAX0","szDecimals":2,"weiDecimals":8,"index":347,"tokenId":"0x22b40040969d98471f8c57a721a9327d","isCanonical":false,"evmContract":{"address":"0xba63418b8e247df4b3f97aa39bef2b6804664054","evm_extra_wei_decimals":10},"fullName":"$AVAX on Hyperliquid. Deployed by HyBridge","deployerTradingFeeShare":"1.0"},{"name":"RZR","szDecimals":2,"weiDecimals":7,"index":348,"tokenId":"0x6492582d90449b9f8913fcbf85104cce","isCanonical":false,"evmContract":{"address":"0xb4444468e444f89e1c2cac2f1d3ee7e336cbd1f5","evm_extra_wei_decimals":11},"fullName":"Rezerve Money","deployerTradingFeeShare":"1.0"},{"name":"PEPE0","szDecimals":2,"weiDecimals":8,"index":349,"tokenId":"0xbacf4ae7974d3899ae867e6a06546594","isCanonical":false,"evmContract":null,"fullName":"$PEPE0 on Hyperliquid. Deployed by HyBridge.","deployerTradingFeeShare":"1.0"},{"name":"SDEX","szDecimals":0,"weiDecimals":8,"index":350,"tokenId":"0x10c169c8f6e91b15e2a6e03e703f8ab4","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"1.0"},{"name":"FLR","szDecimals":2,"weiDecimals":8,"index":351,"tokenId":"0x41b511438587b456ca47322b97a27fb7","isCanonical":false,"evmContract":{"address":"0x579292c42356dd9907dfb2c25e59bbdedb5b8dc9","evm_extra_wei_decimals":10},"fullName":"Flare","deployerTradingFeeShare":"1.0"},{"name":"USDG","szDecimals":2,"weiDecimals":8,"index":352,"tokenId":"0xae87b5246dd9f377b14bbadcf3c72131","isCanonical":false,"evmContract":null,"fullName":"USDG0","deployerTradingFeeShare":"1.0"},{"name":"OG","szDecimals":2,"weiDecimals":8,"index":353,"tokenId":"0xe807efe6ea8c7ed6aa0e96c729bd4d89","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"1.0"},{"name":"RED","szDecimals":2,"weiDecimals":8,"index":354,"tokenId":"0xf7b182df2fea90a5c1a654d475195796","isCanonical":false,"evmContract":null,"fullName":"RedStone","deployerTradingFeeShare":"1.0"},{"name":"CASH","szDecimals":2,"weiDecimals":8,"index":355,"tokenId":"0xd275d730933f351f9aa7ae02929e77a6","isCanonical":false,"evmContract":null,"fullName":"CASH","deployerTradingFeeShare":"1.0"},{"name":"PUP","szDecimals":2,"weiDecimals":8,"index":356,"tokenId":"0x0bff82cce36b779c3f7d3d678e3a740d","isCanonical":false,"evmContract":{"address":"0x876e7f2f30935118a654fc0e1f807afc49efe500","evm_extra_wei_decimals":10},"fullName":"Pup Token","deployerTradingFeeShare":"1.0"},{"name":"IXRP","szDecimals":2,"weiDecimals":7,"index":357,"tokenId":"0x53053c80e0dfbce58e3f45993bd2b20e","isCanonical":false,"evmContract":null,"fullName":"iXRP","deployerTradingFeeShare":"1.0"},{"name":"UWLD","szDecimals":1,"weiDecimals":6,"index":358,"tokenId":"0x571aa6d5261fb8f28e21a9cdb2e1c646","isCanonical":false,"evmContract":null,"fullName":"Unit Worldcoin","deployerTradingFeeShare":"1.0"},{"name":"NATIVE","szDecimals":2,"weiDecimals":8,"index":359,"tokenId":"0xe662f08111f77587581b55dc7fb823d1","isCanonical":false,"evmContract":null,"fullName":"Native Markets","deployerTradingFeeShare":"1.0"},{"name":"USDH","szDecimals":2,"weiDecimals":8,"index":360,"tokenId":"0x54e00a5988577cb0b0c9ab0cb6ef7f4b","isCanonical":false,"evmContract":{"address":"0x111111a1a0667d36bd57c0a9f569b98057111111","evm_extra_wei_decimals":-2},"fullName":"USDH","deployerTradingFeeShare":"0.0"},{"name":"UDZ","szDecimals":2,"weiDecimals":8,"index":361,"tokenId":"0x9cd8fd4cae61e63a10ba7615780ee520","isCanonical":false,"evmContract":null,"fullName":"Unit DoubleZero","deployerTradingFeeShare":"1.0"},{"name":"DURIAN","szDecimals":0,"weiDecimals":5,"index":362,"tokenId":"0xb2464a5bf71e1fa8e5cbfd6d4a26fc81","isCanonical":false,"evmContract":null,"fullName":"DURIAN","deployerTradingFeeShare":"1.0"},{"name":"CRCT","szDecimals":2,"weiDecimals":8,"index":363,"tokenId":"0x39ed94302b0aaee0c640ef98711942e2","isCanonical":false,"evmContract":null,"fullName":"Circuit","deployerTradingFeeShare":"1.0"},{"name":"UPHL","szDecimals":2,"weiDecimals":8,"index":364,"tokenId":"0xca30bc8e4e2029e53d3a52867df2a7d1","isCanonical":false,"evmContract":{"address":"0x9c3bcc457862dd6d8907ba11f8c7fe7e43a8c08b","evm_extra_wei_decimals":10},"fullName":"Upheaval Token","deployerTradingFeeShare":"1.0"},{"name":"UDOGE","szDecimals":2,"weiDecimals":9,"index":365,"tokenId":"0xd5e0a2d2aa8a05e73e4ef1caeea2f62a","isCanonical":false,"evmContract":null,"fullName":"Unit Dogecoin","deployerTradingFeeShare":"1.0"},{"name":"SFC","szDecimals":2,"weiDecimals":8,"index":366,"tokenId":"0x5f54bd9b4000f450ca33d55da766b758","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"1.0"},{"name":"FXRP","szDecimals":1,"weiDecimals":6,"index":367,"tokenId":"0x7624bce759ceb7eb1579fb18839d5fa6","isCanonical":false,"evmContract":null,"fullName":"FXRP","deployerTradingFeeShare":"1.0"},{"name":"WXRP","szDecimals":1,"weiDecimals":6,"index":368,"tokenId":"0xde64ca50987c1e366e5d462b78f0499f","isCanonical":false,"evmContract":null,"fullName":"wXRP","deployerTradingFeeShare":"1.0"},{"name":"MHYPE","szDecimals":2,"weiDecimals":8,"index":369,"tokenId":"0xbc536c5210073afb8dac4b7a5faed86b","isCanonical":false,"evmContract":null,"fullName":"Max Hype","deployerTradingFeeShare":"1.0"},{"name":"FLOW","szDecimals":2,"weiDecimals":8,"index":370,"tokenId":"0x3e6b0906781a6f82b40a22f2868fe5e1","isCanonical":false,"evmContract":null,"fullName":"Flowdesk","deployerTradingFeeShare":"1.0"},{"name":"VNTLS","szDecimals":2,"weiDecimals":8,"index":371,"tokenId":"0xc89b770d03bbd03457713866543329dc","isCanonical":false,"evmContract":null,"fullName":"Ventuals","deployerTradingFeeShare":"1.0"},{"name":"TANSSI","szDecimals":2,"weiDecimals":8,"index":372,"tokenId":"0xb64e181726c52cfccc50b59fc57dfe29","isCanonical":false,"evmContract":null,"fullName":"Tanssi","deployerTradingFeeShare":"1.0"},{"name":"XION","szDecimals":1,"weiDecimals":6,"index":373,"tokenId":"0x23fc959643503f65a253fdfb4d34742f","isCanonical":false,"evmContract":null,"fullName":"XION","deployerTradingFeeShare":"1.0"},{"name":"SEDA","szDecimals":2,"weiDecimals":8,"index":374,"tokenId":"0x83a0bf54708a0236a49f7301677ffdc2","isCanonical":false,"evmContract":null,"fullName":"SEDA","deployerTradingFeeShare":"1.0"},{"name":"WUSDN","szDecimals":2,"weiDecimals":8,"index":375,"tokenId":"0xc62a55d6081b5879fcdf2ace9160c0ba","isCanonical":false,"evmContract":null,"fullName":null,"deployerTradingFeeShare":"1.0"},{"name":"IZEC","szDecimals":2,"weiDecimals":8,"index":376,"tokenId":"0x2bfad83b63d172b0b74279bd1f282d20","isCanonical":false,"evmContract":null,"fullName":"iZEC","deployerTradingFeeShare":"1.0"},{"name":"USDN","szDecimals":1,"weiDecimals":6,"index":377,"tokenId":"0x92ec40275450da1c84f0f50887c40233","isCanonical":false,"evmContract":null,"fullName":"Noble Dollar","deployerTradingFeeShare":"1.0"}]} \ No newline at end of file diff --git a/pkg/exchange/hyperliquid/types.go b/pkg/exchange/hyperliquid/types.go new file mode 100644 index 0000000000..9e65743ad8 --- /dev/null +++ b/pkg/exchange/hyperliquid/types.go @@ -0,0 +1,56 @@ +package hyperliquid + +import ( + "sync" + + "github.com/c9s/bbgo/pkg/types" +) + +var ( + SupportedIntervals = map[types.Interval]int{ + types.Interval1m: 1 * 60, + types.Interval3m: 3 * 60, + types.Interval5m: 5 * 60, + types.Interval15m: 15 * 60, + types.Interval30m: 30 * 60, + types.Interval1h: 60 * 60, + types.Interval2h: 60 * 60 * 2, + types.Interval4h: 60 * 60 * 4, + types.Interval8h: 60 * 60 * 8, + types.Interval12h: 60 * 60 * 12, + types.Interval1d: 60 * 60 * 24, + types.Interval3d: 60 * 60 * 24 * 3, + types.Interval1w: 60 * 60 * 24 * 7, + types.Interval1mo: 60 * 60 * 24 * 30, + } + + localInterval = map[types.Interval]string{ + types.Interval1m: "1m", + types.Interval3m: "3m", + types.Interval5m: "5m", + types.Interval15m: "15m", + types.Interval30m: "30m", + types.Interval1h: "1h", + types.Interval2h: "2h", + types.Interval4h: "4h", + types.Interval8h: "8h", + types.Interval12h: "12h", + types.Interval1d: "1d", + types.Interval3d: "3d", + types.Interval1w: "1w", + types.Interval1mo: "1M", + } +) + +var spotSymbolSyncMap sync.Map +var futuresSymbolSyncMap sync.Map + +func init() { + for key, val := range spotSymbolMap { + spotSymbolSyncMap.Store(key, val) + } + + for key, val := range futuresSymbolMap { + futuresSymbolSyncMap.Store(key, val) + } +} diff --git a/pkg/testutil/auth.go b/pkg/testutil/auth.go index fede5fb8a9..991230492c 100644 --- a/pkg/testutil/auth.go +++ b/pkg/testutil/auth.go @@ -40,3 +40,16 @@ func IntegrationTestWithPassphraseConfigured(t *testing.T, prefix string) (key, return key, secret, passphrase, ok } + +func IntegrationTestWithPrivateKeyConfigured(t *testing.T, prefix string) (privateKey, account, vaultAccount string, ok bool) { + var hasKey bool + privateKey, hasKey = os.LookupEnv(prefix + "_API_PRIVATE_KEY") + accountAddr, hasAccount := os.LookupEnv(prefix + "_API_MAIN_ACCOUNT") + vaultAccount, _ = os.LookupEnv(prefix + "_API_VAULT_ACCOUNT") + ok = hasKey && hasAccount && os.Getenv("TEST_"+prefix) == "1" + if ok { + t.Logf(prefix+" api integration test enabled, privateKey = %s, vaultAccount = %s", maskSecret(privateKey), maskSecret(vaultAccount)) + } + + return privateKey, accountAddr, vaultAccount, ok +} diff --git a/pkg/types/exchange.go b/pkg/types/exchange.go index 84c4521be1..4d48ee09a2 100644 --- a/pkg/types/exchange.go +++ b/pkg/types/exchange.go @@ -17,26 +17,28 @@ const DateFormat = "2006-01-02" type ExchangeName string const ( - ExchangeMax ExchangeName = "max" - ExchangeBinance ExchangeName = "binance" - ExchangeOKEx ExchangeName = "okex" - ExchangeKucoin ExchangeName = "kucoin" - ExchangeBitget ExchangeName = "bitget" - ExchangeBacktest ExchangeName = "backtest" - ExchangeBybit ExchangeName = "bybit" - ExchangeCoinBase ExchangeName = "coinbase" - ExchangeBitfinex ExchangeName = "bitfinex" + ExchangeMax ExchangeName = "max" + ExchangeBinance ExchangeName = "binance" + ExchangeOKEx ExchangeName = "okex" + ExchangeKucoin ExchangeName = "kucoin" + ExchangeBitget ExchangeName = "bitget" + ExchangeBacktest ExchangeName = "backtest" + ExchangeBybit ExchangeName = "bybit" + ExchangeCoinBase ExchangeName = "coinbase" + ExchangeBitfinex ExchangeName = "bitfinex" + ExchangeHyperliquid ExchangeName = "hyperliquid" ) var SupportedExchanges = map[ExchangeName]struct{}{ - ExchangeMax: struct{}{}, - ExchangeBinance: struct{}{}, - ExchangeOKEx: struct{}{}, - ExchangeKucoin: struct{}{}, - ExchangeBitget: struct{}{}, - ExchangeBybit: struct{}{}, - ExchangeCoinBase: struct{}{}, - ExchangeBitfinex: struct{}{}, + ExchangeMax: struct{}{}, + ExchangeBinance: struct{}{}, + ExchangeOKEx: struct{}{}, + ExchangeKucoin: struct{}{}, + ExchangeBitget: struct{}{}, + ExchangeBybit: struct{}{}, + ExchangeCoinBase: struct{}{}, + ExchangeBitfinex: struct{}{}, + ExchangeHyperliquid: struct{}{}, // note: we are not using "backtest" } diff --git a/pkg/types/interval.go b/pkg/types/interval.go index 689da68e71..aa79ee2cb3 100644 --- a/pkg/types/interval.go +++ b/pkg/types/interval.go @@ -104,6 +104,7 @@ var Interval1h = Interval("1h") var Interval2h = Interval("2h") var Interval4h = Interval("4h") var Interval6h = Interval("6h") +var Interval8h = Interval("8h") var Interval12h = Interval("12h") var Interval1d = Interval("1d") var Interval3d = Interval("3d") diff --git a/pkg/types/order.go b/pkg/types/order.go index b29ea325e5..9b8144cc61 100644 --- a/pkg/types/order.go +++ b/pkg/types/order.go @@ -37,6 +37,7 @@ var ( TimeInForceIOC TimeInForce = "IOC" TimeInForceFOK TimeInForce = "FOK" TimeInForceGTT TimeInForce = "GTT" // for coinbase exchange api + TimeInForceALO TimeInForce = "ALO" // for hyperliquid exchange api ) // MarginOrderSideEffectType define side effect type for orders