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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 16 additions & 15 deletions hyperliquid/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,27 +39,28 @@ func MakeUniversalRequest[T any](api IAPIService, request any) (*T, error) {
if api.Endpoint() == "/exchange" && api.KeyManager() == nil {
return nil, APIError{Message: "API key not set"}
}

response, err := api.Request(api.Endpoint(), request)
if err != nil {
return nil, err
}

var result T
err = json.Unmarshal(response, &result)
if err == nil {
return &result, nil
}

var errResult map[string]interface{}
err = json.Unmarshal(response, &errResult)
if err != nil {
api.debug("Error json.Unmarshal: %s", err)
var errResult map[string]interface{}
err = json.Unmarshal(response, &errResult)
if err != nil {
api.debug("Error second json.Unmarshal: %s", err)
return nil, APIError{Message: "Unexpected response"}
}
// Check if the result is an error
// Return an APIError if it is
if errResult["status"] == "err" {
return nil, APIError{Message: errResult["response"].(string)}
} else {
return nil, APIError{Message: fmt.Sprintf("Unexpected response: %v", errResult)}
}
api.debug("Error second json.Unmarshal: %s", err)
return nil, APIError{Message: "Unexpected response"}
}
return &result, nil

if errResult["status"] == "err" {
return nil, APIError{Message: errResult["response"].(string)}
}

return nil, APIError{Message: fmt.Sprintf("Unexpected response: %v", errResult)}
}
3 changes: 3 additions & 0 deletions hyperliquid/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ func (client *Client) debug(format string, v ...interface{}) {

// SetPrivateKey sets the private key for the client.
func (client *Client) SetPrivateKey(privateKey string) error {
if strings.HasPrefix(privateKey, "0x") {
privateKey = strings.TrimPrefix(privateKey, "0x") // remove 0x prefix from private key
}
client.privateKey = privateKey
var err error
client.keyManager, err = NewPKeyManager(privateKey)
Expand Down
4 changes: 3 additions & 1 deletion hyperliquid/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ const GLOBAL_DEBUG = false // Defualt debug that is used in all tests

// Execution constants
const DEFAULT_SLIPPAGE = 0.005 // 0.5% default slippage
var SZ_DECIMALS = 2 // Default decimals for size
const SPOT_MAX_DECIMALS = 8 // Default decimals for spot
const PERP_MAX_DECIMALS = 6 // Default decimals for perp
var SZ_DECIMALS = 2 // Default decimals for usdc

// Signing constants
const HYPERLIQUID_CHAIN_ID = 1337
Expand Down
47 changes: 28 additions & 19 deletions hyperliquid/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,22 @@ func OrderWiresToOrderAction(orders []OrderWire, grouping Grouping) PlaceOrderAc
}
}

func OrderRequestToWire(req OrderRequest, meta map[string]AssetInfo) OrderWire {
func OrderRequestToWire(req OrderRequest, meta map[string]AssetInfo, isSpot bool) OrderWire {
info := meta[req.Coin]
var assetId, maxDecimals int
if isSpot {
// https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/asset-ids
assetId = info.AssetId + 10000
maxDecimals = SPOT_MAX_DECIMALS
} else {
assetId = info.AssetId
maxDecimals = PERP_MAX_DECIMALS
}
return OrderWire{
Asset: info.AssetId,
Asset: assetId,
IsBuy: req.IsBuy,
LimitPx: FloatToWire(req.LimitPx, nil),
SizePx: FloatToWire(req.Sz, &info.SzDecimals),
LimitPx: FloatToWire(req.LimitPx, maxDecimals, info.SzDecimals),
SizePx: FloatToWire(req.Sz, maxDecimals, info.SzDecimals),
ReduceOnly: req.ReduceOnly,
OrderType: OrderTypeToWire(req.OrderType),
}
Expand All @@ -75,28 +84,28 @@ func OrderTypeToWire(orderType OrderType) OrderTypeWire {
return OrderTypeWire{}
}

// Format the float with custom decimal places, default is 6.
// Hyperliquid only allows at most 6 digits.
func FloatToWire(x float64, szDecimals *int) string {
// Format the float with custom decimal places, default is 6 (perp), 8 (spot).
// https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/tick-and-lot-size
func FloatToWire(x float64, maxDecimals int, szDecimals int) string {
bigf := big.NewFloat(x)
var maxDecSz uint
if szDecimals != nil {
maxDecSz = uint(*szDecimals)
intPart, _ := bigf.Int64()
intSize := len(strconv.FormatInt(intPart, 10))
if intSize >= maxDecimals {
maxDecSz = 0
} else {
intPart, _ := bigf.Int64()
intSize := len(strconv.FormatInt(intPart, 10))
if intSize >= 6 {
maxDecSz = 0
} else {
maxDecSz = uint(6 - intSize)
}
maxDecSz = uint(maxDecimals - intSize)
}
x, _ = bigf.Float64()
rounded := fmt.Sprintf("%.*f", maxDecSz, x)
for strings.HasSuffix(rounded, "0") {
rounded = strings.TrimSuffix(rounded, "0")
if strings.Contains(rounded, ".") {
for strings.HasSuffix(rounded, "0") {
rounded = strings.TrimSuffix(rounded, "0")
}
}
if strings.HasSuffix(rounded, ".") {
rounded = strings.TrimSuffix(rounded, ".")
}
rounded = strings.TrimSuffix(rounded, ".")
return rounded
}

Expand Down
69 changes: 64 additions & 5 deletions hyperliquid/exchange_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ type ExchangeAPI struct {
address string
baseEndpoint string
meta map[string]AssetInfo
spotMeta map[string]AssetInfo
}

// NewExchangeAPI creates a new default ExchangeAPI.
Expand All @@ -54,6 +55,14 @@ func NewExchangeAPI(isMainnet bool) *ExchangeAPI {
api.debug("Error building meta map: %s", err)
}
api.meta = meta

spotMeta, err := api.infoAPI.BuildSpotMetaMap()
if err != nil {
api.SetDebugActive()
api.debug("Error building spot meta map: %s", err)
}
api.spotMeta = spotMeta

return &api
}

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

// SlippagePriceSpot is a helper function to calculate the slippage price for a spot coin.
func (api *ExchangeAPI) SlippagePriceSpot(coin string, isBuy bool, slippage float64) float64 {
marketPx, err := api.infoAPI.GetSpotMarketPx(coin)
if err != nil {
api.debug("Error getting market price: %s", err)
return 0.0
}
slippagePrice := CalculateSlippage(isBuy, marketPx, slippage)
return slippagePrice
}

// Open a market order.
// Limit order with TIF=IOC and px=market price * (1 +- slippage).
// Size determines the amount of the coin to buy/sell.
Expand Down Expand Up @@ -98,6 +118,34 @@ func (api *ExchangeAPI) MarketOrder(coin string, size float64, slippage *float64
return api.Order(orderRequest, GroupingNa)
}

// MarketOrderSpot is a market order for a spot coin.
// It is used to buy/sell a spot coin.
// Limit order with TIF=IOC and px=market price * (1 +- slippage).
// Size determines the amount of the coin to buy/sell.
//
// MarketOrderSpot("HYPE", 0.1, nil) // Buy 0.1 HYPE
// MarketOrderSpot("HYPE", -0.1, nil) // Sell 0.1 HYPE
// MarketOrderSpot("HYPE", 0.1, &slippage) // Buy 0.1 HYPE with slippage
func (api *ExchangeAPI) MarketOrderSpot(coin string, size float64, slippage *float64) (*PlaceOrderResponse, error) {
slpg := GetSlippage(slippage)
isBuy := IsBuy(size)
finalPx := api.SlippagePriceSpot(coin, isBuy, slpg)
orderType := OrderType{
Limit: &LimitOrderType{
Tif: TifIoc,
},
}
orderRequest := OrderRequest{
Coin: coin,
IsBuy: isBuy,
Sz: math.Abs(size),
LimitPx: finalPx,
OrderType: orderType,
ReduceOnly: false,
}
return api.OrderSpot(orderRequest, GroupingNa)
}

// Open a limit order.
// Order type can be Gtc, Ioc, Alo.
// Size determines the amount of the coin to buy/sell.
Expand Down Expand Up @@ -165,15 +213,26 @@ func (api *ExchangeAPI) ClosePosition(coin string) (*PlaceOrderResponse, error)

// Place single order
func (api *ExchangeAPI) Order(request OrderRequest, grouping Grouping) (*PlaceOrderResponse, error) {
return api.BulkOrders([]OrderRequest{request}, grouping)
return api.BulkOrders([]OrderRequest{request}, grouping, false)
}

// OrderSpot places a spot order
func (api *ExchangeAPI) OrderSpot(request OrderRequest, grouping Grouping) (*PlaceOrderResponse, error) {
return api.BulkOrders([]OrderRequest{request}, grouping, true)
}

// Place orders in bulk
// https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#place-an-order
func (api *ExchangeAPI) BulkOrders(requests []OrderRequest, grouping Grouping) (*PlaceOrderResponse, error) {
func (api *ExchangeAPI) BulkOrders(requests []OrderRequest, grouping Grouping, isSpot bool) (*PlaceOrderResponse, error) {
var wires []OrderWire
var meta map[string]AssetInfo
if isSpot {
meta = api.spotMeta
} else {
meta = api.meta
}
for _, req := range requests {
wires = append(wires, OrderRequestToWire(req, api.meta))
wires = append(wires, OrderRequestToWire(req, meta, isSpot))
}
timestamp := GetNonce()
action := OrderWiresToOrderAction(wires, grouping)
Expand Down Expand Up @@ -283,7 +342,7 @@ func (api *ExchangeAPI) Withdraw(destination string, amount float64) (*WithdrawR
action := WithdrawAction{
Type: "withdraw3",
Destination: destination,
Amount: FloatToWire(amount, &SZ_DECIMALS),
Amount: FloatToWire(amount, PERP_MAX_DECIMALS, SZ_DECIMALS),
Time: nonce,
}
signatureChainID, chainType := api.getChainParams()
Expand Down Expand Up @@ -315,7 +374,7 @@ func (api *ExchangeAPI) getChainParams() (string, string) {
func (api *ExchangeAPI) BuildBulkOrdersEIP712(requests []OrderRequest, grouping Grouping) (apitypes.TypedData, error) {
var wires []OrderWire
for _, req := range requests {
wires = append(wires, OrderRequestToWire(req, api.meta))
wires = append(wires, OrderRequestToWire(req, api.meta, false))
}
timestamp := GetNonce()
action := OrderWiresToOrderAction(wires, grouping)
Expand Down
18 changes: 17 additions & 1 deletion hyperliquid/exchange_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ func TestExchangeAPI_MarketOpen(testing *testing.T) {
if totalSize != math.Abs(size) {
testing.Errorf("res.Response.Data.Statuses[0].Filled.TotalSz = %v", totalSize)
}
time.Sleep(2 * time.Second) // wait to execute order
accountState, err := exchangeAPI.infoAPI.GetUserState(exchangeAPI.AccountAddress())
if err != nil {
testing.Errorf("GetAccountState() error = %v", err)
Expand Down Expand Up @@ -157,7 +158,7 @@ func TestExchangeAPI_MarketClose(testing *testing.T) {

func TestExchangeAPI_TestWithdraw(testing *testing.T) {
exchangeAPI := GetExchangeAPI()
withdrawAmount := 2.0
withdrawAmount := 10.0
stateBefore, err := exchangeAPI.infoAPI.GetUserState(exchangeAPI.AccountAddress())
if err != nil {
testing.Errorf("GetAccountState() error = %v", err)
Expand All @@ -184,3 +185,18 @@ func TestExchangeAPI_TestWithdraw(testing *testing.T) {
testing.Errorf("Balance not updated: %v", stateAfter)
}
}

func TestExchageAPI_TestMarketOrderSpot(testing *testing.T) {
exchangeAPI := GetExchangeAPI()
size := 1600.0
coin := "YEETI"
res, err := exchangeAPI.MarketOrderSpot(coin, size, nil)
if err != nil {
testing.Errorf("MakeOpen() error = %v", err)
}
testing.Logf("MakeOpen() = %v", res)
avgPrice := res.Response.Data.Statuses[0].Filled.AvgPx
if avgPrice == 0 {
testing.Errorf("res.Response.Data.Statuses[0].Filled.AvgPx = %v", avgPrice)
}
}
6 changes: 4 additions & 2 deletions hyperliquid/exchange_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ type ExchangeRequest struct {
}

type AssetInfo struct {
SzDecimals int
AssetId int
SzDecimals int
WeiDecimals int
AssetId int
SpotName string // for spot asset (e.g. "@107")
}

type OrderRequest struct {
Expand Down
17 changes: 9 additions & 8 deletions hyperliquid/go.mod
Original file line number Diff line number Diff line change
@@ -1,27 +1,28 @@
module github.com/Logarithm-Labs/go-hyperliquid/hyperliquid

go 1.21.10
go 1.23.4

require (
github.com/ethereum/go-ethereum v1.14.3
github.com/ethereum/go-ethereum v1.14.12
github.com/sirupsen/logrus v1.9.3
github.com/vmihailenco/msgpack/v5 v5.4.1
)

require (
github.com/bits-and-blooms/bitset v1.10.0 // indirect
github.com/btcsuite/btcd/btcec/v2 v2.2.0 // indirect
github.com/bits-and-blooms/bitset v1.13.0 // indirect
github.com/consensys/bavard v0.1.13 // indirect
github.com/consensys/gnark-crypto v0.12.1 // indirect
github.com/crate-crypto/go-ipa v0.0.0-20240223125850-b1e8a79f509c // indirect
github.com/crate-crypto/go-kzg-4844 v1.0.0 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
github.com/ethereum/c-kzg-4844 v1.0.0 // indirect
github.com/holiman/uint256 v1.2.4 // indirect
github.com/ethereum/go-verkle v0.1.1-0.20240829091221-dffa7562dbe9 // indirect
github.com/holiman/uint256 v1.3.1 // indirect
github.com/mmcloughlin/addchain v0.4.0 // indirect
github.com/supranational/blst v0.3.11 // indirect
github.com/supranational/blst v0.3.13 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/crypto v0.22.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/sys v0.22.0 // indirect
rsc.io/tmplfunc v0.0.3 // indirect
)
Loading