Skip to content

Exchange API: User role detection, automatic vault/subAccount enabling and support #21

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
39 changes: 32 additions & 7 deletions hyperliquid/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ type IClient interface {
IAPIService
SetPrivateKey(privateKey string) error
SetAccountAddress(address string)
SetVaultAddress(address string)
SetUserRole(role Role)
AccountAddress() string
VaultAddress() string
SetDebugActive()
IsMainnet() bool
}
Expand All @@ -33,14 +36,16 @@ type IClient interface {
// the network type, the private key, and the logger.
// The debug method prints the debug messages.
type Client struct {
baseUrl string // Base URL of the HyperLiquid API
baseURL string // Base URL of the HyperLiquid API
privateKey string // Private key for the client
defualtAddress string // Default address for the client
defaultAddress string // Default address for the client
isMainnet bool // Network type
Debug bool // Debug mode
httpClient *http.Client // HTTP client
keyManager *PKeyManager // Private key manager
Logger *log.Logger // Logger for debug messages
role Role // Role of the client,
vaultAddress string // Vault address
}

// Returns the private key manager connected to the API.
Expand All @@ -60,19 +65,21 @@ func getURL(isMainnet bool) string {
// NewClient returns a new instance of the Client struct.
func NewClient(isMainnet bool) *Client {
logger := log.New()
logger.SetLevel(log.DebugLevel)
logger.SetFormatter(&log.TextFormatter{
FullTimestamp: true,
PadLevelText: true,
ForceColors: true,
})
logger.SetOutput(os.Stdout)
logger.SetLevel(log.DebugLevel)
return &Client{
baseUrl: getURL(isMainnet),
baseURL: getURL(isMainnet),
httpClient: http.DefaultClient,
Debug: false,
isMainnet: isMainnet,
privateKey: "",
defualtAddress: "",
defaultAddress: "",
Logger: logger,
keyManager: nil,
}
Expand Down Expand Up @@ -100,12 +107,30 @@ func (client *Client) SetPrivateKey(privateKey string) error {
// In case you use PKeyManager from API section https://app.hyperliquid.xyz/API
// Then you can use this method to set the address.
func (client *Client) SetAccountAddress(address string) {
client.defualtAddress = address
client.defaultAddress = address
}

// Returns the public address connected to the API.
func (client *Client) AccountAddress() string {
return client.defualtAddress
return client.defaultAddress
}

// VaultAddress returns the vault address for the client.
func (client *Client) VaultAddress() string {
return client.vaultAddress
}

// SetVaultAddress sets the vault address for the client.
func (client *Client) SetVaultAddress(vaultAddress string) {
client.vaultAddress = vaultAddress
}

// SetUserRole sets the user role for the client.
func (client *Client) SetUserRole(role Role) {
client.role = role
if role.IsVaultOrSubAccount() {
client.vaultAddress = client.AccountAddress()
}
}

// IsMainnet returns true if the client is connected to the mainnet.
Expand All @@ -121,7 +146,7 @@ func (client *Client) SetDebugActive() {
// Request sends a POST request to the HyperLiquid API.
func (client *Client) Request(endpoint string, payload any) ([]byte, error) {
endpoint = strings.TrimPrefix(endpoint, "/") // Remove leading slash if present
url := fmt.Sprintf("%s/%s", client.baseUrl, endpoint)
url := fmt.Sprintf("%s/%s", client.baseURL, endpoint)
client.debug("Request to %s", url)
jsonPayload, err := json.Marshal(payload)
if err != nil {
Expand Down
131 changes: 96 additions & 35 deletions hyperliquid/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,21 @@ func HexToBytes(addr string) []byte {
return b
}
}
func HexToInt(hexString string) (*big.Int, error) {
value := new(big.Int)
if len(hexString) > 1 && hexString[:2] == "0x" {
hexString = hexString[2:]
}
_, success := value.SetString(hexString, 16)
if !success {
return nil, fmt.Errorf("invalid hexadecimal string: %s", hexString)
}
return value, nil
}

func IntToHex(value *big.Int) string {
return "0x" + value.Text(16)
}

func OrderWiresToOrderAction(orders []OrderWire, grouping Grouping) PlaceOrderAction {
return PlaceOrderAction{
Expand All @@ -43,19 +58,35 @@ func OrderWiresToOrderAction(orders []OrderWire, grouping Grouping) PlaceOrderAc
}
}

func OrderRequestToWire(req OrderRequest, meta map[string]AssetInfo, isSpot bool) OrderWire {
func (req *OrderRequest) isSpot() bool {
return strings.ContainsAny(req.Coin, "@-")
}

// ToWire (OrderRequest) converts an OrderRequest to an OrderWire using the provided metadata.
// https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/asset-ids
func (req *OrderRequest) ToWireMeta(meta map[string]AssetInfo) 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
return req.ToWire(info)
}

// ToModifyWire converts an OrderRequest to a ModifyOrderWire using the provided AssetInfo.
func (req *OrderRequest) ToModifyWire(info AssetInfo) ModifyOrderWire {
return ModifyOrderWire{
OrderID: *req.OrderID,
Order: req.ToWire(info),
}
}

// ToWire converts an OrderRequest to an OrderWire using the provided AssetInfo.
func (req *OrderRequest) ToWire(info AssetInfo) OrderWire {
var assetID = info.AssetID
var maxDecimals = PERP_MAX_DECIMALS
if req.isSpot() {
assetID = info.AssetID + 10000 // https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/asset-ids
maxDecimals = SPOT_MAX_DECIMALS
} else {
assetId = info.AssetId
maxDecimals = PERP_MAX_DECIMALS
}
return OrderWire{
Asset: assetId,
Asset: assetID,
IsBuy: req.IsBuy,
LimitPx: PriceToWire(req.LimitPx, maxDecimals, info.SzDecimals),
SizePx: SizeToWire(req.Sz, info.SzDecimals),
Expand All @@ -65,30 +96,7 @@ func OrderRequestToWire(req OrderRequest, meta map[string]AssetInfo, isSpot bool
}
}

func ModifyOrderRequestToWire(req ModifyOrderRequest, meta map[string]AssetInfo, isSpot bool) ModifyOrderWire {
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 ModifyOrderWire{
OrderId: req.OrderId,
Order: OrderWire{
Asset: assetId,
IsBuy: req.IsBuy,
LimitPx: PriceToWire(req.LimitPx, maxDecimals, info.SzDecimals),
SizePx: SizeToWire(req.Sz, info.SzDecimals),
ReduceOnly: req.ReduceOnly,
OrderType: OrderTypeToWire(req.OrderType),
},
}
}

// OrderTypeToWire converts an OrderType to an OrderTypeWire.
func OrderTypeToWire(orderType OrderType) OrderTypeWire {
if orderType.Limit != nil {
return OrderTypeWire{
Expand All @@ -110,8 +118,26 @@ func OrderTypeToWire(orderType OrderType) OrderTypeWire {
return OrderTypeWire{}
}

// 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
/**
* FloatToWire converts a float64 to a string representation following Hyperliquid's decimal rules.
* FloatToWire converts a float64 to a string representation following Hyperliquid's decimal rules.
*
* The conversion adheres to market-specific decimal place constraints:
* - Perpetual markets: Maximum 6 decimal places
* - Spot markets: Maximum 8 decimal places
*
* The function dynamically adjusts decimal precision based on:
* 1. Integer part magnitude
* 2. Maximum allowed decimals (maxDecimals)
* 3. Size decimal precision (szDecimals)
*
* Output formatting:
* - Removes trailing zeros
* - Trims unnecessary decimal points
* - Maintains tick size precision requirements
*
* @see 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
Expand Down Expand Up @@ -228,3 +254,38 @@ func StructToMap(strct any) (res map[string]interface{}, err error) {
json.Unmarshal(a, &res)
return res, nil
}

// RoundOrderSize rounds the order size to the nearest tick size
func RoundOrderSize(x float64, szDecimals int) string {
newX := math.Round(x*math.Pow10(szDecimals)) / math.Pow10(szDecimals)
// TODO: add rounding
return big.NewFloat(newX).Text('f', szDecimals)
}

// RoundOrderPrice rounds the order price to the nearest tick size
func RoundOrderPrice(x float64, szDecimals int, maxDecimals int) string {
maxSignFigures := 5
allowedDecimals := maxDecimals - szDecimals
numberOfDigitsInIntegerPart := len(strconv.Itoa(int(x)))
if numberOfDigitsInIntegerPart >= maxSignFigures {
return RoundOrderSize(x, 0)
}
allowedSignFigures := maxSignFigures - numberOfDigitsInIntegerPart
if x >= 1 {
return RoundOrderSize(x, min(allowedSignFigures, allowedDecimals))
}

text := RoundOrderSize(x, allowedDecimals)
startSignFigures := false
for i := 2; i < len(text); i++ {
if text[i] == '0' && !startSignFigures {
continue
}
startSignFigures = true
allowedSignFigures--
if allowedSignFigures == 0 {
return text[:i+1]
}
}
return text
}
Loading