Skip to content

Commit 952e4f6

Browse files
Merge pull request #22 from Logarithm-Labs/fix-prerelease-fixes
Make pre-release updates.
2 parents 0e4c1ff + 21d672f commit 952e4f6

9 files changed

+675
-328
lines changed

hyperliquid/client.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@ func (client *Client) KeyManager() *PKeyManager {
5151
// getAPIURL returns the API URL based on the network type.
5252
func getURL(isMainnet bool) string {
5353
if isMainnet {
54-
return "https://api.hyperliquid.xyz"
54+
return MAINNET_API_URL
5555
} else {
56-
return "https://api.hyperliquid-testnet.xyz"
56+
return TESTNET_API_URL
5757
}
5858
}
5959

@@ -65,6 +65,7 @@ func NewClient(isMainnet bool) *Client {
6565
PadLevelText: true,
6666
})
6767
logger.SetOutput(os.Stdout)
68+
logger.SetLevel(log.DebugLevel)
6869
return &Client{
6970
baseUrl: getURL(isMainnet),
7071
httpClient: http.DefaultClient,
@@ -80,7 +81,7 @@ func NewClient(isMainnet bool) *Client {
8081
// debug prints the debug messages.
8182
func (client *Client) debug(format string, v ...interface{}) {
8283
if client.Debug {
83-
client.Logger.Printf(format, v...)
84+
client.Logger.Debugf(format, v...)
8485
}
8586
}
8687

hyperliquid/consts.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@ package hyperliquid
22

33
const GLOBAL_DEBUG = false // Default debug that is used in all tests
44

5+
// API constants
6+
const MAINNET_API_URL = "https://api.hyperliquid.xyz"
7+
const TESTNET_API_URL = "https://api.hyperliquid-testnet.xyz"
8+
59
// Execution constants
610
const DEFAULT_SLIPPAGE = 0.005 // 0.5% default slippage
711
const SPOT_MAX_DECIMALS = 8 // Default decimals for spot
812
const PERP_MAX_DECIMALS = 6 // Default decimals for perp
9-
var SZ_DECIMALS = 2 // Default decimals for usdc
13+
var USDC_SZ_DECIMALS = 2 // Default decimals for usdc that is used for withdraw
1014

1115
// Signing constants
1216
const HYPERLIQUID_CHAIN_ID = 1337

hyperliquid/convert.go

Lines changed: 88 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,8 @@ func OrderRequestToWire(req OrderRequest, meta map[string]AssetInfo, isSpot bool
5757
return OrderWire{
5858
Asset: assetId,
5959
IsBuy: req.IsBuy,
60-
LimitPx: RoundOrderPrice(req.LimitPx, info.SzDecimals, maxDecimals),
61-
SizePx: RoundOrderSize(req.Sz, info.SzDecimals),
60+
LimitPx: PriceToWire(req.LimitPx, maxDecimals, info.SzDecimals),
61+
SizePx: SizeToWire(req.Sz, info.SzDecimals),
6262
ReduceOnly: req.ReduceOnly,
6363
OrderType: OrderTypeToWire(req.OrderType),
6464
Cloid: req.Cloid,
@@ -81,8 +81,8 @@ func ModifyOrderRequestToWire(req ModifyOrderRequest, meta map[string]AssetInfo,
8181
Order: OrderWire{
8282
Asset: assetId,
8383
IsBuy: req.IsBuy,
84-
LimitPx: RoundOrderPrice(req.LimitPx, info.SzDecimals, maxDecimals),
85-
SizePx: RoundOrderSize(req.Sz, info.SzDecimals),
84+
LimitPx: PriceToWire(req.LimitPx, maxDecimals, info.SzDecimals),
85+
SizePx: SizeToWire(req.Sz, info.SzDecimals),
8686
ReduceOnly: req.ReduceOnly,
8787
OrderType: OrderTypeToWire(req.OrderType),
8888
},
@@ -135,6 +135,90 @@ func FloatToWire(x float64, maxDecimals int, szDecimals int) string {
135135
return rounded
136136
}
137137

138+
// fastPow10 returns 10^exp as a float64. For our purposes exp is small.
139+
func pow10(exp int) float64 {
140+
var res float64 = 1
141+
for i := 0; i < exp; i++ {
142+
res *= 10
143+
}
144+
return res
145+
}
146+
147+
// PriceToWire converts a price value to its string representation per Hyperliquid rules.
148+
// It enforces:
149+
// - At most 5 significant figures,
150+
// - And no more than (maxDecimals - szDecimals) decimal places.
151+
//
152+
// Integer prices are returned as is.
153+
func PriceToWire(x float64, maxDecimals, szDecimals int) string {
154+
// If the price is an integer, return it without decimals.
155+
if x == math.Trunc(x) {
156+
return strconv.FormatInt(int64(x), 10)
157+
}
158+
159+
// Rule 1: The tick rule – maximum decimals allowed is (maxDecimals - szDecimals).
160+
allowedTick := maxDecimals - szDecimals
161+
162+
// Rule 2: The significant figures rule – at most 5 significant digits.
163+
var allowedSig int
164+
if x >= 1 {
165+
// Count digits in the integer part.
166+
digits := int(math.Floor(math.Log10(x))) + 1
167+
allowedSig = 5 - digits
168+
if allowedSig < 0 {
169+
allowedSig = 0
170+
}
171+
} else {
172+
// For x < 1, determine the effective exponent.
173+
exponent := int(math.Ceil(-math.Log10(x)))
174+
allowedSig = 4 + exponent
175+
}
176+
177+
// Final allowed decimals is the minimum of the tick rule and the significant figures rule.
178+
allowedDecimals := allowedTick
179+
if allowedSig < allowedDecimals {
180+
allowedDecimals = allowedSig
181+
}
182+
if allowedDecimals < 0 {
183+
allowedDecimals = 0
184+
}
185+
186+
// Round the price to allowedDecimals decimals.
187+
factor := pow10(allowedDecimals)
188+
rounded := math.Round(x*factor) / factor
189+
190+
// Format the number with fixed precision.
191+
s := strconv.FormatFloat(rounded, 'f', allowedDecimals, 64)
192+
// Only trim trailing zeros if the formatted string contains a decimal point.
193+
if strings.Contains(s, ".") {
194+
s = strings.TrimRight(s, "0")
195+
s = strings.TrimRight(s, ".")
196+
}
197+
return s
198+
}
199+
200+
// SizeToWire converts a size value to its string representation,
201+
// rounding it to exactly szDecimals decimals.
202+
// Integer sizes are returned without decimals.
203+
func SizeToWire(x float64, szDecimals int) string {
204+
// Return integer sizes without decimals.
205+
if szDecimals == 0 {
206+
return strconv.FormatInt(int64(x), 10)
207+
}
208+
// Return integer sizes directly.
209+
if x == math.Trunc(x) {
210+
return strconv.FormatInt(int64(x), 10)
211+
}
212+
213+
// Round the size value to szDecimals decimals.
214+
factor := pow10(szDecimals)
215+
rounded := math.Round(x*factor) / factor
216+
217+
// Format with fixed precision then trim any trailing zeros and the decimal point.
218+
s := strconv.FormatFloat(rounded, 'f', szDecimals, 64)
219+
return strings.TrimRight(strings.TrimRight(s, "0"), ".")
220+
}
221+
138222
// To sign raw messages via EIP-712
139223
func StructToMap(strct any) (res map[string]interface{}, err error) {
140224
a, err := json.Marshal(strct)
@@ -144,38 +228,3 @@ func StructToMap(strct any) (res map[string]interface{}, err error) {
144228
json.Unmarshal(a, &res)
145229
return res, nil
146230
}
147-
148-
// Round the order size to the nearest tick size
149-
func RoundOrderSize(x float64, szDecimals int) string {
150-
newX := math.Round(x*math.Pow10(szDecimals)) / math.Pow10(szDecimals)
151-
// TODO: add rounding
152-
return big.NewFloat(newX).Text('f', szDecimals)
153-
}
154-
155-
// Round the order price to the nearest tick size
156-
func RoundOrderPrice(x float64, szDecimals int, maxDecimals int) string {
157-
maxSignFigures := 5
158-
allowedDecimals := maxDecimals - szDecimals
159-
numberOfDigitsInIntegerPart := len(strconv.Itoa(int(x)))
160-
if numberOfDigitsInIntegerPart >= maxSignFigures {
161-
return RoundOrderSize(x, 0)
162-
}
163-
allowedSignFigures := maxSignFigures - numberOfDigitsInIntegerPart
164-
if x < 1 {
165-
text := RoundOrderSize(x, allowedDecimals)
166-
startSignFigures := false
167-
for i := 2; i < len(text); i++ {
168-
if text[i] == '0' && !startSignFigures {
169-
continue
170-
}
171-
startSignFigures = true
172-
allowedSignFigures--
173-
if allowedSignFigures == 0 {
174-
return text[:i+1]
175-
}
176-
}
177-
return text
178-
} else {
179-
return RoundOrderSize(x, min(allowedSignFigures, allowedDecimals))
180-
}
181-
}

hyperliquid/convert_test.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package hyperliquid
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestConvert_SizeToWire(t *testing.T) {
8+
testCases := []struct {
9+
name string
10+
input float64
11+
szDec int
12+
expected string
13+
}{
14+
{
15+
name: "BTC Size",
16+
input: 0.1,
17+
szDec: 5,
18+
expected: "0.1",
19+
},
20+
{
21+
name: "PNUT Size",
22+
input: 101.22,
23+
szDec: 1,
24+
expected: "101.2",
25+
},
26+
{
27+
name: "ETH Size",
28+
input: 0.1,
29+
szDec: 4,
30+
expected: "0.1",
31+
},
32+
{
33+
name: "ADA Size",
34+
input: 100.123456,
35+
szDec: 0,
36+
expected: "100",
37+
},
38+
{
39+
name: "ETH Size",
40+
input: 1.0,
41+
szDec: 4,
42+
expected: "1",
43+
},
44+
{
45+
name: "ETH Size",
46+
input: 10.0,
47+
szDec: 4,
48+
expected: "10",
49+
},
50+
{
51+
name: "ETH Size",
52+
input: 0.0100,
53+
szDec: 4,
54+
expected: "0.01",
55+
},
56+
{
57+
name: "ETH Size",
58+
input: 0.010000001,
59+
szDec: 4,
60+
expected: "0.01",
61+
},
62+
}
63+
for _, tc := range testCases {
64+
t.Run(tc.name, func(t *testing.T) {
65+
res := SizeToWire(tc.input, tc.szDec)
66+
if res != tc.expected {
67+
t.Errorf("SizeToWire() = %v, want %v", res, tc.expected)
68+
}
69+
})
70+
}
71+
}
72+
73+
func TestConvert_PriceToWire(t *testing.T) {
74+
testCases := []struct {
75+
name string
76+
input float64
77+
maxDec int
78+
szDec int
79+
expected string
80+
}{
81+
{
82+
name: "BTC Price",
83+
input: 105000,
84+
maxDec: 6,
85+
szDec: 5,
86+
expected: "105000",
87+
},
88+
{
89+
name: "BTC Price",
90+
input: 105000.1234,
91+
maxDec: 6,
92+
szDec: 5,
93+
expected: "105000",
94+
},
95+
{
96+
name: "BTC Price",
97+
input: 95001.123456,
98+
maxDec: 6,
99+
szDec: 5,
100+
expected: "95001",
101+
},
102+
}
103+
for _, tc := range testCases {
104+
t.Run(tc.name, func(t *testing.T) {
105+
res := PriceToWire(tc.input, tc.maxDec, tc.szDec)
106+
if res != tc.expected {
107+
t.Errorf("PriceToWire() = %v, want %v", res, tc.expected)
108+
}
109+
})
110+
}
111+
}

0 commit comments

Comments
 (0)