hyperliquid 0.7.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -1
- data/CHANGELOG.md +44 -0
- data/README.md +2 -1
- data/docs/API.md +60 -2
- data/docs/EXAMPLES.md +191 -0
- data/docs/WS.md +49 -0
- data/lib/hyperliquid/constants.rb +5 -0
- data/lib/hyperliquid/errors.rb +3 -0
- data/lib/hyperliquid/exchange.rb +129 -43
- data/lib/hyperliquid/info.rb +27 -2
- data/lib/hyperliquid/signing/eip712.rb +9 -0
- data/lib/hyperliquid/version.rb +1 -1
- data/lib/hyperliquid/ws/client.rb +340 -0
- data/lib/hyperliquid.rb +3 -1
- data/scripts/test_03_perp_market_roundtrip.rb +32 -26
- data/scripts/test_05_update_leverage.rb +11 -0
- data/scripts/test_07_market_close.rb +28 -22
- data/scripts/test_08_usd_class_transfer.rb +2 -0
- data/scripts/test_10_vault.rb +2 -0
- data/scripts/test_11_builder_fee.rb +1 -0
- data/scripts/test_12_staking.rb +2 -0
- data/scripts/test_13_ws_l2_book.rb +83 -0
- data/scripts/test_14_ws_candle.rb +81 -0
- data/scripts/test_all.rb +3 -1
- data/scripts/test_helpers.rb +131 -1
- metadata +19 -1
|
@@ -1,49 +1,55 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
# Test 3: Perp Market Roundtrip (
|
|
5
|
-
# Open a long
|
|
4
|
+
# Test 3: Perp Market Roundtrip (Long)
|
|
5
|
+
# Open a long perp position, then close it.
|
|
6
6
|
|
|
7
7
|
require_relative 'test_helpers'
|
|
8
8
|
|
|
9
9
|
sdk = build_sdk
|
|
10
|
-
|
|
10
|
+
perp_coin = 'ETH'
|
|
11
|
+
separator("TEST 3: Perp Market Roundtrip (#{perp_coin} Long)")
|
|
11
12
|
|
|
12
|
-
perp_coin = 'BTC'
|
|
13
13
|
mids = sdk.info.all_mids
|
|
14
|
-
|
|
14
|
+
perp_coin_price = mids[perp_coin]&.to_f
|
|
15
15
|
|
|
16
|
-
if
|
|
16
|
+
if perp_coin_price&.positive?
|
|
17
17
|
meta = sdk.info.meta
|
|
18
|
-
|
|
19
|
-
sz_decimals =
|
|
18
|
+
perp_coin_meta = meta['universe'].find { |a| a['name'] == perp_coin }
|
|
19
|
+
sz_decimals = perp_coin_meta['szDecimals']
|
|
20
20
|
|
|
21
|
-
perp_size = (20.0 /
|
|
21
|
+
perp_size = (20.0 / perp_coin_price).ceil(sz_decimals)
|
|
22
22
|
|
|
23
|
-
puts "#{perp_coin} mid: $#{
|
|
24
|
-
puts "Size: #{perp_size}
|
|
25
|
-
puts "Slippage: #{(PERP_SLIPPAGE * 100).to_i}%"
|
|
23
|
+
puts "#{perp_coin} mid: $#{perp_coin_price.round(2)}"
|
|
24
|
+
puts "Size: #{perp_size} #{perp_coin} (~$#{(perp_size * perp_coin_price).round(2)})"
|
|
25
|
+
puts "Slippage: #{(PERP_SLIPPAGE * 100).to_i}% (with retry up to #{((PERP_SLIPPAGE + ORACLE_SLIPPAGE_INCREMENT * (ORACLE_RETRY_ATTEMPTS - 1)) * 100).to_i}%)"
|
|
26
26
|
puts
|
|
27
27
|
|
|
28
28
|
puts 'Opening LONG position (market buy)...'
|
|
29
|
-
result =
|
|
29
|
+
result = market_order_with_retry(
|
|
30
|
+
sdk,
|
|
30
31
|
coin: perp_coin,
|
|
31
32
|
is_buy: true,
|
|
32
33
|
size: perp_size,
|
|
33
|
-
|
|
34
|
+
base_slippage: PERP_SLIPPAGE
|
|
34
35
|
)
|
|
35
|
-
check_result(result, 'Long open')
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
36
|
+
open_success = check_result(result, 'Long open')
|
|
37
|
+
|
|
38
|
+
if open_success
|
|
39
|
+
wait_with_countdown(WAIT_SECONDS, 'Position open. Waiting before close...')
|
|
40
|
+
|
|
41
|
+
puts 'Closing LONG position (market sell)...'
|
|
42
|
+
result = market_order_with_retry(
|
|
43
|
+
sdk,
|
|
44
|
+
coin: perp_coin,
|
|
45
|
+
is_buy: false,
|
|
46
|
+
size: perp_size,
|
|
47
|
+
base_slippage: PERP_SLIPPAGE
|
|
48
|
+
)
|
|
49
|
+
check_result(result, 'Long close')
|
|
50
|
+
else
|
|
51
|
+
puts red('Skipping close - position was not opened')
|
|
52
|
+
end
|
|
47
53
|
else
|
|
48
54
|
puts red("SKIPPED: Could not get #{perp_coin} price")
|
|
49
55
|
end
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
# Test 5: Update Leverage (BTC)
|
|
5
5
|
# Set cross, isolated, then reset leverage.
|
|
6
|
+
# Requires no open BTC position (cannot switch leverage type with open position).
|
|
6
7
|
|
|
7
8
|
require_relative 'test_helpers'
|
|
8
9
|
|
|
@@ -14,8 +15,16 @@ mids = sdk.info.all_mids
|
|
|
14
15
|
btc_price = mids[perp_coin]&.to_f
|
|
15
16
|
|
|
16
17
|
if btc_price&.positive?
|
|
18
|
+
# Check for open position - cannot switch leverage type with open position
|
|
19
|
+
unless check_position_and_prompt(sdk, perp_coin, timeout: 10)
|
|
20
|
+
puts
|
|
21
|
+
puts green('Test 5 Update Leverage skipped (open position).')
|
|
22
|
+
exit 0
|
|
23
|
+
end
|
|
24
|
+
|
|
17
25
|
puts 'Setting BTC to 5x cross leverage...'
|
|
18
26
|
result = sdk.exchange.update_leverage(coin: perp_coin, leverage: 5, is_cross: true)
|
|
27
|
+
dump_status(result)
|
|
19
28
|
api_error?(result) || puts(green('5x cross leverage set'))
|
|
20
29
|
puts
|
|
21
30
|
|
|
@@ -23,6 +32,7 @@ if btc_price&.positive?
|
|
|
23
32
|
|
|
24
33
|
puts 'Setting BTC to 3x isolated leverage...'
|
|
25
34
|
result = sdk.exchange.update_leverage(coin: perp_coin, leverage: 3, is_cross: false)
|
|
35
|
+
dump_status(result)
|
|
26
36
|
api_error?(result) || puts(green('3x isolated leverage set'))
|
|
27
37
|
puts
|
|
28
38
|
|
|
@@ -30,6 +40,7 @@ if btc_price&.positive?
|
|
|
30
40
|
|
|
31
41
|
puts 'Resetting BTC to 1x cross leverage...'
|
|
32
42
|
result = sdk.exchange.update_leverage(coin: perp_coin, leverage: 1, is_cross: true)
|
|
43
|
+
dump_status(result)
|
|
33
44
|
api_error?(result) || puts(green('1x cross leverage set'))
|
|
34
45
|
else
|
|
35
46
|
puts red("SKIPPED: Could not get #{perp_coin} price")
|
|
@@ -1,46 +1,52 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
# Test 7: Market Close (
|
|
4
|
+
# Test 7: Market Close (PERP)
|
|
5
5
|
# Open a long position, then close it using market_close (auto-detect size).
|
|
6
6
|
|
|
7
7
|
require_relative 'test_helpers'
|
|
8
8
|
|
|
9
9
|
sdk = build_sdk
|
|
10
|
-
|
|
10
|
+
perp_coin = 'ETH'
|
|
11
|
+
separator("TEST 7: Market Close (#{perp_coin})")
|
|
11
12
|
|
|
12
|
-
perp_coin = 'BTC'
|
|
13
13
|
mids = sdk.info.all_mids
|
|
14
|
-
|
|
14
|
+
perp_coin_price = mids[perp_coin]&.to_f
|
|
15
15
|
|
|
16
|
-
if
|
|
16
|
+
if perp_coin_price&.positive?
|
|
17
17
|
meta = sdk.info.meta
|
|
18
|
-
|
|
19
|
-
sz_decimals =
|
|
18
|
+
perp_coin_meta = meta['universe'].find { |a| a['name'] == perp_coin }
|
|
19
|
+
sz_decimals = perp_coin_meta['szDecimals']
|
|
20
20
|
|
|
21
|
-
perp_size = (20.0 /
|
|
21
|
+
perp_size = (20.0 / perp_coin_price).ceil(sz_decimals)
|
|
22
22
|
|
|
23
|
-
puts "#{perp_coin} mid: $#{
|
|
24
|
-
puts "Size: #{perp_size}
|
|
23
|
+
puts "#{perp_coin} mid: $#{perp_coin_price.round(2)}"
|
|
24
|
+
puts "Size: #{perp_size} #{perp_coin}"
|
|
25
|
+
puts "Slippage: #{(PERP_SLIPPAGE * 100).to_i}% (with retry up to #{((PERP_SLIPPAGE + ORACLE_SLIPPAGE_INCREMENT * (ORACLE_RETRY_ATTEMPTS - 1)) * 100).to_i}%)"
|
|
25
26
|
puts
|
|
26
27
|
|
|
27
28
|
puts 'Opening LONG position (market buy)...'
|
|
28
|
-
result =
|
|
29
|
+
result = market_order_with_retry(
|
|
30
|
+
sdk,
|
|
29
31
|
coin: perp_coin,
|
|
30
32
|
is_buy: true,
|
|
31
33
|
size: perp_size,
|
|
32
|
-
|
|
34
|
+
base_slippage: PERP_SLIPPAGE
|
|
33
35
|
)
|
|
34
|
-
check_result(result, 'Long open')
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
36
|
+
open_success = check_result(result, 'Long open')
|
|
37
|
+
|
|
38
|
+
if open_success
|
|
39
|
+
wait_with_countdown(WAIT_SECONDS, 'Position open. Waiting before market_close...')
|
|
40
|
+
|
|
41
|
+
puts 'Closing position using market_close (auto-detect size)...'
|
|
42
|
+
result = sdk.exchange.market_close(
|
|
43
|
+
coin: perp_coin,
|
|
44
|
+
slippage: PERP_SLIPPAGE + ORACLE_SLIPPAGE_INCREMENT # Use higher slippage for close
|
|
45
|
+
)
|
|
46
|
+
check_result(result, 'Market close')
|
|
47
|
+
else
|
|
48
|
+
puts red('Skipping market_close - position was not opened')
|
|
49
|
+
end
|
|
44
50
|
else
|
|
45
51
|
puts red("SKIPPED: Could not get #{perp_coin} price")
|
|
46
52
|
end
|
|
@@ -11,6 +11,7 @@ separator('TEST 8: USD Class Transfer (Perp <-> Spot)')
|
|
|
11
11
|
|
|
12
12
|
puts 'Transferring $10 from perp to spot...'
|
|
13
13
|
result = sdk.exchange.usd_class_transfer(amount: '10', to_perp: false)
|
|
14
|
+
dump_status(result)
|
|
14
15
|
api_error?(result) || puts(green('Transfer to spot successful!'))
|
|
15
16
|
puts
|
|
16
17
|
|
|
@@ -18,6 +19,7 @@ wait_with_countdown(WAIT_SECONDS, 'Waiting before transferring back...')
|
|
|
18
19
|
|
|
19
20
|
puts 'Transferring $10 from spot to perp...'
|
|
20
21
|
result = sdk.exchange.usd_class_transfer(amount: '10', to_perp: true)
|
|
22
|
+
dump_status(result)
|
|
21
23
|
api_error?(result) || puts(green('Transfer to perp successful!'))
|
|
22
24
|
|
|
23
25
|
test_passed('Test 8 USD Class Transfer')
|
data/scripts/test_10_vault.rb
CHANGED
|
@@ -42,6 +42,7 @@ when 'deposit'
|
|
|
42
42
|
is_deposit: true,
|
|
43
43
|
usd: 10
|
|
44
44
|
)
|
|
45
|
+
dump_status(result)
|
|
45
46
|
api_error?(result) || puts(green('Vault deposit successful!'))
|
|
46
47
|
when 'withdraw'
|
|
47
48
|
equity_f = follower&.dig('vaultEquity')&.to_f || 0
|
|
@@ -53,6 +54,7 @@ when 'withdraw'
|
|
|
53
54
|
is_deposit: false,
|
|
54
55
|
usd: withdraw_amount
|
|
55
56
|
)
|
|
57
|
+
dump_status(result)
|
|
56
58
|
api_error?(result) || puts(green('Vault withdrawal successful!'))
|
|
57
59
|
else
|
|
58
60
|
puts red("Insufficient vault equity to withdraw ($#{equity_f})")
|
|
@@ -16,6 +16,7 @@ perp_coin = 'BTC'
|
|
|
16
16
|
# Step 1: Approve builder fee
|
|
17
17
|
puts "Approving builder fee for #{builder_address} (max #{max_fee_rate})..."
|
|
18
18
|
result = sdk.exchange.approve_builder_fee(builder: builder_address, max_fee_rate: max_fee_rate)
|
|
19
|
+
dump_status(result)
|
|
19
20
|
api_error?(result) || puts(green('Builder fee approved'))
|
|
20
21
|
puts
|
|
21
22
|
|
data/scripts/test_12_staking.rb
CHANGED
|
@@ -50,6 +50,7 @@ when 'delegate'
|
|
|
50
50
|
wei: delegate_amount,
|
|
51
51
|
is_undelegate: false
|
|
52
52
|
)
|
|
53
|
+
dump_status(result)
|
|
53
54
|
api_error?(result) || puts(green('Delegation successful!'))
|
|
54
55
|
when 'undelegate'
|
|
55
56
|
puts "Undelegating 0.1 HYPE from #{validator}..."
|
|
@@ -58,6 +59,7 @@ when 'undelegate'
|
|
|
58
59
|
wei: delegate_amount,
|
|
59
60
|
is_undelegate: true
|
|
60
61
|
)
|
|
62
|
+
dump_status(result)
|
|
61
63
|
api_error?(result) || puts(green('Undelegation successful!'))
|
|
62
64
|
else
|
|
63
65
|
puts 'Pass "delegate" or "undelegate" as an argument to perform an action.'
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Test 13: WebSocket l2Book Subscription
|
|
5
|
+
#
|
|
6
|
+
# Subscribes to ETH perp l2Book on testnet, prints 3 updates
|
|
7
|
+
# with top-of-book (best bid/ask), then cleanly disconnects.
|
|
8
|
+
#
|
|
9
|
+
# No private key required (read-only WebSocket).
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# ruby scripts/test_13_ws_l2_book.rb
|
|
13
|
+
|
|
14
|
+
require_relative '../lib/hyperliquid'
|
|
15
|
+
|
|
16
|
+
def green(text)
|
|
17
|
+
"\e[32m#{text}\e[0m"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def red(text)
|
|
21
|
+
"\e[31m#{text}\e[0m"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
puts
|
|
25
|
+
puts '=' * 60
|
|
26
|
+
puts 'TEST 13: WebSocket l2Book Subscription'
|
|
27
|
+
puts '=' * 60
|
|
28
|
+
puts
|
|
29
|
+
puts 'Network: Testnet'
|
|
30
|
+
puts 'Subscribing to ETH l2Book (3 updates, then disconnect)'
|
|
31
|
+
puts
|
|
32
|
+
|
|
33
|
+
sdk = Hyperliquid.new(testnet: true)
|
|
34
|
+
updates = []
|
|
35
|
+
mutex = Mutex.new
|
|
36
|
+
done = ConditionVariable.new
|
|
37
|
+
|
|
38
|
+
sdk.ws.on(:open) { puts 'WebSocket connected.' }
|
|
39
|
+
sdk.ws.on(:error) { |e| puts red("WebSocket error: #{e}") }
|
|
40
|
+
|
|
41
|
+
sdk.ws.subscribe({ type: 'l2Book', coin: 'ETH' }) do |data|
|
|
42
|
+
mutex.synchronize do
|
|
43
|
+
next if updates.length >= 3
|
|
44
|
+
|
|
45
|
+
levels = data['levels']
|
|
46
|
+
bids = levels&.dig(0) || []
|
|
47
|
+
asks = levels&.dig(1) || []
|
|
48
|
+
best_bid = bids.first
|
|
49
|
+
best_ask = asks.first
|
|
50
|
+
|
|
51
|
+
update_num = updates.length + 1
|
|
52
|
+
puts "Update #{update_num}/3:"
|
|
53
|
+
puts " Best bid: #{best_bid ? "#{best_bid['px']} (#{best_bid['sz']})" : 'n/a'}"
|
|
54
|
+
puts " Best ask: #{best_ask ? "#{best_ask['px']} (#{best_ask['sz']})" : 'n/a'}"
|
|
55
|
+
puts " Bid levels: #{bids.length}, Ask levels: #{asks.length}"
|
|
56
|
+
puts
|
|
57
|
+
|
|
58
|
+
updates << data
|
|
59
|
+
done.signal if updates.length >= 3
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Wait for 3 updates or timeout after 30 seconds
|
|
64
|
+
success = false
|
|
65
|
+
mutex.synchronize do
|
|
66
|
+
deadline = Time.now + 30
|
|
67
|
+
while updates.length < 3 && Time.now < deadline
|
|
68
|
+
remaining = deadline - Time.now
|
|
69
|
+
break if remaining <= 0
|
|
70
|
+
|
|
71
|
+
done.wait(mutex, remaining)
|
|
72
|
+
end
|
|
73
|
+
success = updates.length >= 3
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
sdk.ws.close
|
|
77
|
+
|
|
78
|
+
if success
|
|
79
|
+
puts green('Test 13 WebSocket l2Book passed!')
|
|
80
|
+
else
|
|
81
|
+
puts red("Test 13 FAILED: only received #{updates.length}/3 updates within 30s")
|
|
82
|
+
exit 1
|
|
83
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Test 14: WebSocket candle Subscription
|
|
5
|
+
#
|
|
6
|
+
# Subscribes to ETH 1m candles on testnet, prints 3 updates
|
|
7
|
+
# with OHLCV data, then cleanly disconnects.
|
|
8
|
+
#
|
|
9
|
+
# No private key required (read-only WebSocket).
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# ruby scripts/test_14_ws_candle.rb
|
|
13
|
+
|
|
14
|
+
require_relative '../lib/hyperliquid'
|
|
15
|
+
|
|
16
|
+
def green(text)
|
|
17
|
+
"\e[32m#{text}\e[0m"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def red(text)
|
|
21
|
+
"\e[31m#{text}\e[0m"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
puts
|
|
25
|
+
puts '=' * 60
|
|
26
|
+
puts 'TEST 14: WebSocket candle Subscription'
|
|
27
|
+
puts '=' * 60
|
|
28
|
+
puts
|
|
29
|
+
puts 'Network: Testnet'
|
|
30
|
+
puts 'Subscribing to ETH 1m candles (3 updates, then disconnect)'
|
|
31
|
+
puts
|
|
32
|
+
|
|
33
|
+
sdk = Hyperliquid.new(testnet: true)
|
|
34
|
+
updates = []
|
|
35
|
+
mutex = Mutex.new
|
|
36
|
+
done = ConditionVariable.new
|
|
37
|
+
|
|
38
|
+
sdk.ws.on(:open) { puts 'WebSocket connected.' }
|
|
39
|
+
sdk.ws.on(:error) { |e| puts red("WebSocket error: #{e}") }
|
|
40
|
+
|
|
41
|
+
sdk.ws.subscribe({ type: 'candle', coin: 'ETH', interval: '1m' }) do |data|
|
|
42
|
+
mutex.synchronize do
|
|
43
|
+
next if updates.length >= 3
|
|
44
|
+
|
|
45
|
+
update_num = updates.length + 1
|
|
46
|
+
puts "Update #{update_num}/3:"
|
|
47
|
+
puts " Symbol: #{data['s']}"
|
|
48
|
+
puts " Interval: #{data['i']}"
|
|
49
|
+
puts " Open: #{data['o']}"
|
|
50
|
+
puts " High: #{data['h']}"
|
|
51
|
+
puts " Low: #{data['l']}"
|
|
52
|
+
puts " Close: #{data['c']}"
|
|
53
|
+
puts " Volume: #{data['v']}"
|
|
54
|
+
puts
|
|
55
|
+
|
|
56
|
+
updates << data
|
|
57
|
+
done.signal if updates.length >= 3
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Wait for 3 updates or timeout after 90 seconds (candles can be slow)
|
|
62
|
+
success = false
|
|
63
|
+
mutex.synchronize do
|
|
64
|
+
deadline = Time.now + 90
|
|
65
|
+
while updates.length < 3 && Time.now < deadline
|
|
66
|
+
remaining = deadline - Time.now
|
|
67
|
+
break if remaining <= 0
|
|
68
|
+
|
|
69
|
+
done.wait(mutex, remaining)
|
|
70
|
+
end
|
|
71
|
+
success = updates.length >= 3
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
sdk.ws.close
|
|
75
|
+
|
|
76
|
+
if success
|
|
77
|
+
puts green('Test 14 WebSocket candle passed!')
|
|
78
|
+
else
|
|
79
|
+
puts red("Test 14 FAILED: only received #{updates.length}/3 updates within 90s")
|
|
80
|
+
exit 1
|
|
81
|
+
end
|
data/scripts/test_all.rb
CHANGED
data/scripts/test_helpers.rb
CHANGED
|
@@ -5,7 +5,9 @@ require 'json'
|
|
|
5
5
|
|
|
6
6
|
WAIT_SECONDS = 3
|
|
7
7
|
SPOT_SLIPPAGE = 0.40 # 40% for illiquid testnet spot markets
|
|
8
|
-
PERP_SLIPPAGE = 0.
|
|
8
|
+
PERP_SLIPPAGE = 0.15
|
|
9
|
+
ORACLE_RETRY_ATTEMPTS = 3
|
|
10
|
+
ORACLE_SLIPPAGE_INCREMENT = 0.10 # Increase slippage by 10% on each retry
|
|
9
11
|
|
|
10
12
|
def green(text)
|
|
11
13
|
"\e[32m#{text}\e[0m"
|
|
@@ -43,7 +45,19 @@ def api_error?(result)
|
|
|
43
45
|
true
|
|
44
46
|
end
|
|
45
47
|
|
|
48
|
+
# Extract and display the status from an API response
|
|
49
|
+
def dump_status(result)
|
|
50
|
+
return unless result.is_a?(Hash)
|
|
51
|
+
|
|
52
|
+
status = result.dig('response', 'data', 'statuses', 0)
|
|
53
|
+
return unless status
|
|
54
|
+
|
|
55
|
+
puts " API status: #{status.inspect}"
|
|
56
|
+
end
|
|
57
|
+
|
|
46
58
|
def check_result(result, operation)
|
|
59
|
+
dump_status(result)
|
|
60
|
+
|
|
47
61
|
return false if api_error?(result)
|
|
48
62
|
|
|
49
63
|
status = result.dig('response', 'data', 'statuses', 0)
|
|
@@ -98,3 +112,119 @@ def build_sdk
|
|
|
98
112
|
|
|
99
113
|
sdk
|
|
100
114
|
end
|
|
115
|
+
|
|
116
|
+
# Check if result has "Price too far from oracle" error
|
|
117
|
+
def oracle_error?(result)
|
|
118
|
+
return false unless result.is_a?(Hash)
|
|
119
|
+
|
|
120
|
+
if result['status'] == 'err' && result['response'].to_s.include?('Price too far from oracle')
|
|
121
|
+
return true
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
status = result.dig('response', 'data', 'statuses', 0)
|
|
125
|
+
status.is_a?(Hash) && status['error'].to_s.include?('Price too far from oracle')
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Execute a market order with retry logic for oracle errors
|
|
129
|
+
def market_order_with_retry(sdk, coin:, is_buy:, size:, base_slippage:)
|
|
130
|
+
slippage = base_slippage
|
|
131
|
+
|
|
132
|
+
ORACLE_RETRY_ATTEMPTS.times do |attempt|
|
|
133
|
+
result = sdk.exchange.market_order(
|
|
134
|
+
coin: coin,
|
|
135
|
+
is_buy: is_buy,
|
|
136
|
+
size: size,
|
|
137
|
+
slippage: slippage
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
unless oracle_error?(result)
|
|
141
|
+
return result
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
if attempt < ORACLE_RETRY_ATTEMPTS - 1
|
|
145
|
+
slippage += ORACLE_SLIPPAGE_INCREMENT
|
|
146
|
+
puts red("Oracle price error. Retrying with #{(slippage * 100).to_i}% slippage (attempt #{attempt + 2}/#{ORACLE_RETRY_ATTEMPTS})...")
|
|
147
|
+
sleep 1
|
|
148
|
+
else
|
|
149
|
+
return result
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Get position for a coin, returns nil if no position
|
|
155
|
+
def get_position(sdk, coin)
|
|
156
|
+
state = sdk.info.user_state(sdk.exchange.address)
|
|
157
|
+
positions = state['assetPositions'] || []
|
|
158
|
+
positions.find { |p| p.dig('position', 'coin') == coin }
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Check for open position and prompt user to clean up
|
|
162
|
+
# Returns true if test should continue, false if should skip
|
|
163
|
+
def check_position_and_prompt(sdk, coin, timeout: 10)
|
|
164
|
+
position = get_position(sdk, coin)
|
|
165
|
+
return true unless position
|
|
166
|
+
|
|
167
|
+
size = position.dig('position', 'szi').to_f
|
|
168
|
+
return true if size.zero?
|
|
169
|
+
|
|
170
|
+
puts red("WARNING: Open #{coin} position detected (size: #{size})")
|
|
171
|
+
puts "This test requires no open #{coin} position."
|
|
172
|
+
puts
|
|
173
|
+
puts "Options:"
|
|
174
|
+
puts " [c] Close the position and continue"
|
|
175
|
+
puts " [s] Skip this test"
|
|
176
|
+
puts
|
|
177
|
+
print "Choice (auto-skip in #{timeout}s): "
|
|
178
|
+
$stdout.flush
|
|
179
|
+
|
|
180
|
+
# Non-blocking read with timeout
|
|
181
|
+
require 'io/wait'
|
|
182
|
+
choice = nil
|
|
183
|
+
start = Time.now
|
|
184
|
+
loop do
|
|
185
|
+
if $stdin.ready?
|
|
186
|
+
choice = $stdin.gets&.strip&.downcase
|
|
187
|
+
break
|
|
188
|
+
end
|
|
189
|
+
elapsed = Time.now - start
|
|
190
|
+
remaining = (timeout - elapsed).ceil
|
|
191
|
+
if elapsed >= timeout
|
|
192
|
+
puts
|
|
193
|
+
puts "No response, skipping test..."
|
|
194
|
+
return false
|
|
195
|
+
end
|
|
196
|
+
print "\rChoice (auto-skip in #{remaining}s): "
|
|
197
|
+
$stdout.flush
|
|
198
|
+
sleep 0.5
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
case choice
|
|
202
|
+
when 'c'
|
|
203
|
+
puts "Closing #{coin} position..."
|
|
204
|
+
result = sdk.exchange.market_close(coin: coin, slippage: PERP_SLIPPAGE + 0.20)
|
|
205
|
+
dump_status(result)
|
|
206
|
+
if api_error?(result)
|
|
207
|
+
puts red("Failed to close position. Skipping test.")
|
|
208
|
+
return false
|
|
209
|
+
end
|
|
210
|
+
puts green("Position closed. Waiting for settlement...")
|
|
211
|
+
|
|
212
|
+
# Wait and verify position is actually closed
|
|
213
|
+
5.times do |i|
|
|
214
|
+
sleep 2
|
|
215
|
+
position = get_position(sdk, coin)
|
|
216
|
+
remaining = position&.dig('position', 'szi').to_f.abs
|
|
217
|
+
if remaining < 0.000001
|
|
218
|
+
puts green("Position fully settled.")
|
|
219
|
+
return true
|
|
220
|
+
end
|
|
221
|
+
puts " Still settling... (#{remaining} remaining)"
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
puts red("Position did not fully close. Skipping test.")
|
|
225
|
+
false
|
|
226
|
+
else
|
|
227
|
+
puts "Skipping test."
|
|
228
|
+
false
|
|
229
|
+
end
|
|
230
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hyperliquid
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 1.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- carter2099
|
|
@@ -65,6 +65,20 @@ dependencies:
|
|
|
65
65
|
- - "~>"
|
|
66
66
|
- !ruby/object:Gem::Version
|
|
67
67
|
version: '1.7'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: ws_lite
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - "~>"
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: 1.0.0
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - "~>"
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: 1.0.0
|
|
68
82
|
description: A Ruby SDK for interacting with Hyperliquid's decentralized exchange
|
|
69
83
|
API
|
|
70
84
|
email:
|
|
@@ -88,6 +102,7 @@ files:
|
|
|
88
102
|
- docs/DEVELOPMENT.md
|
|
89
103
|
- docs/ERRORS.md
|
|
90
104
|
- docs/EXAMPLES.md
|
|
105
|
+
- docs/WS.md
|
|
91
106
|
- example.rb
|
|
92
107
|
- lib/hyperliquid.rb
|
|
93
108
|
- lib/hyperliquid/client.rb
|
|
@@ -99,6 +114,7 @@ files:
|
|
|
99
114
|
- lib/hyperliquid/signing/eip712.rb
|
|
100
115
|
- lib/hyperliquid/signing/signer.rb
|
|
101
116
|
- lib/hyperliquid/version.rb
|
|
117
|
+
- lib/hyperliquid/ws/client.rb
|
|
102
118
|
- scripts/test_01_spot_market_roundtrip.rb
|
|
103
119
|
- scripts/test_02_spot_limit_order.rb
|
|
104
120
|
- scripts/test_03_perp_market_roundtrip.rb
|
|
@@ -111,6 +127,8 @@ files:
|
|
|
111
127
|
- scripts/test_10_vault.rb
|
|
112
128
|
- scripts/test_11_builder_fee.rb
|
|
113
129
|
- scripts/test_12_staking.rb
|
|
130
|
+
- scripts/test_13_ws_l2_book.rb
|
|
131
|
+
- scripts/test_14_ws_candle.rb
|
|
114
132
|
- scripts/test_all.rb
|
|
115
133
|
- scripts/test_helpers.rb
|
|
116
134
|
- sig/hyperliquid.rbs
|