hyperliquid 0.5.0 → 0.7.0
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 +1 -0
- data/CHANGELOG.md +23 -1
- data/CLAUDE.md +6 -7
- data/docs/API.md +22 -1
- data/docs/DEVELOPMENT.md +31 -17
- data/docs/EXAMPLES.md +147 -0
- data/lib/hyperliquid/exchange.rb +290 -7
- data/lib/hyperliquid/signing/eip712.rb +101 -0
- data/lib/hyperliquid/signing/signer.rb +33 -2
- data/lib/hyperliquid/version.rb +1 -1
- data/lib/hyperliquid.rb +1 -0
- data/scripts/test_01_spot_market_roundtrip.rb +48 -0
- data/scripts/test_02_spot_limit_order.rb +48 -0
- data/scripts/test_03_perp_market_roundtrip.rb +52 -0
- data/scripts/test_04_perp_limit_order.rb +52 -0
- data/scripts/test_05_update_leverage.rb +39 -0
- data/scripts/test_06_modify_order.rb +67 -0
- data/scripts/test_07_market_close.rb +49 -0
- data/scripts/test_08_usd_class_transfer.rb +23 -0
- data/scripts/test_09_sub_account_lifecycle.rb +51 -0
- data/scripts/test_10_vault.rb +66 -0
- data/scripts/test_11_builder_fee.rb +70 -0
- data/scripts/test_12_staking.rb +68 -0
- data/scripts/test_all.rb +88 -0
- data/scripts/test_helpers.rb +100 -0
- data/test_integration.rb +8 -367
- metadata +15 -1
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Test 3: Perp Market Roundtrip (BTC Long)
|
|
5
|
+
# Open a long BTC position, then close it.
|
|
6
|
+
|
|
7
|
+
require_relative 'test_helpers'
|
|
8
|
+
|
|
9
|
+
sdk = build_sdk
|
|
10
|
+
separator('TEST 3: Perp Market Roundtrip (BTC Long)')
|
|
11
|
+
|
|
12
|
+
perp_coin = 'BTC'
|
|
13
|
+
mids = sdk.info.all_mids
|
|
14
|
+
btc_price = mids[perp_coin]&.to_f
|
|
15
|
+
|
|
16
|
+
if btc_price&.positive?
|
|
17
|
+
meta = sdk.info.meta
|
|
18
|
+
btc_meta = meta['universe'].find { |a| a['name'] == perp_coin }
|
|
19
|
+
sz_decimals = btc_meta['szDecimals']
|
|
20
|
+
|
|
21
|
+
perp_size = (20.0 / btc_price).ceil(sz_decimals)
|
|
22
|
+
|
|
23
|
+
puts "#{perp_coin} mid: $#{btc_price.round(2)}"
|
|
24
|
+
puts "Size: #{perp_size} BTC (~$#{(perp_size * btc_price).round(2)})"
|
|
25
|
+
puts "Slippage: #{(PERP_SLIPPAGE * 100).to_i}%"
|
|
26
|
+
puts
|
|
27
|
+
|
|
28
|
+
puts 'Opening LONG position (market buy)...'
|
|
29
|
+
result = sdk.exchange.market_order(
|
|
30
|
+
coin: perp_coin,
|
|
31
|
+
is_buy: true,
|
|
32
|
+
size: perp_size,
|
|
33
|
+
slippage: PERP_SLIPPAGE
|
|
34
|
+
)
|
|
35
|
+
check_result(result, 'Long open')
|
|
36
|
+
|
|
37
|
+
wait_with_countdown(WAIT_SECONDS, 'Position open. Waiting before close...')
|
|
38
|
+
|
|
39
|
+
puts 'Closing LONG position (market sell)...'
|
|
40
|
+
result = sdk.exchange.market_order(
|
|
41
|
+
coin: perp_coin,
|
|
42
|
+
is_buy: false,
|
|
43
|
+
size: perp_size,
|
|
44
|
+
slippage: PERP_SLIPPAGE
|
|
45
|
+
)
|
|
46
|
+
check_result(result, 'Long close')
|
|
47
|
+
else
|
|
48
|
+
puts red("SKIPPED: Could not get #{perp_coin} price")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
test_passed('Test 3 Perp Market Roundtrip')
|
|
52
|
+
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Test 4: Perp Limit Order (Short, then Cancel)
|
|
5
|
+
# Place a limit sell well above market, then cancel it.
|
|
6
|
+
|
|
7
|
+
require_relative 'test_helpers'
|
|
8
|
+
|
|
9
|
+
sdk = build_sdk
|
|
10
|
+
separator('TEST 4: Perp Limit Order (Short, then Cancel)')
|
|
11
|
+
|
|
12
|
+
perp_coin = 'BTC'
|
|
13
|
+
mids = sdk.info.all_mids
|
|
14
|
+
btc_price = mids[perp_coin]&.to_f
|
|
15
|
+
|
|
16
|
+
if btc_price&.positive?
|
|
17
|
+
meta = sdk.info.meta
|
|
18
|
+
btc_meta = meta['universe'].find { |a| a['name'] == perp_coin }
|
|
19
|
+
sz_decimals = btc_meta['szDecimals']
|
|
20
|
+
|
|
21
|
+
limit_price = (btc_price * 1.50).round(0).to_i
|
|
22
|
+
perp_size = (20.0 / btc_price).ceil(sz_decimals)
|
|
23
|
+
|
|
24
|
+
puts "#{perp_coin} mid: $#{btc_price.round(2)}"
|
|
25
|
+
puts "Limit price: $#{limit_price} (50% above mid - won't fill)"
|
|
26
|
+
puts "Size: #{perp_size} BTC"
|
|
27
|
+
puts
|
|
28
|
+
|
|
29
|
+
puts 'Placing limit SELL order (short)...'
|
|
30
|
+
result = sdk.exchange.order(
|
|
31
|
+
coin: perp_coin,
|
|
32
|
+
is_buy: false,
|
|
33
|
+
size: perp_size,
|
|
34
|
+
limit_px: limit_price,
|
|
35
|
+
order_type: { limit: { tif: 'Gtc' } },
|
|
36
|
+
reduce_only: false
|
|
37
|
+
)
|
|
38
|
+
oid = check_result(result, 'Limit short')
|
|
39
|
+
|
|
40
|
+
if oid.is_a?(Integer)
|
|
41
|
+
wait_with_countdown(WAIT_SECONDS, 'Order resting. Waiting before cancel...')
|
|
42
|
+
|
|
43
|
+
puts "Canceling order #{oid}..."
|
|
44
|
+
result = sdk.exchange.cancel(coin: perp_coin, oid: oid)
|
|
45
|
+
check_result(result, 'Cancel')
|
|
46
|
+
end
|
|
47
|
+
else
|
|
48
|
+
puts red("SKIPPED: Could not get #{perp_coin} price")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
test_passed('Test 4 Perp Limit Order')
|
|
52
|
+
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Test 5: Update Leverage (BTC)
|
|
5
|
+
# Set cross, isolated, then reset leverage.
|
|
6
|
+
|
|
7
|
+
require_relative 'test_helpers'
|
|
8
|
+
|
|
9
|
+
sdk = build_sdk
|
|
10
|
+
separator('TEST 5: Update Leverage (BTC)')
|
|
11
|
+
|
|
12
|
+
perp_coin = 'BTC'
|
|
13
|
+
mids = sdk.info.all_mids
|
|
14
|
+
btc_price = mids[perp_coin]&.to_f
|
|
15
|
+
|
|
16
|
+
if btc_price&.positive?
|
|
17
|
+
puts 'Setting BTC to 5x cross leverage...'
|
|
18
|
+
result = sdk.exchange.update_leverage(coin: perp_coin, leverage: 5, is_cross: true)
|
|
19
|
+
api_error?(result) || puts(green('5x cross leverage set'))
|
|
20
|
+
puts
|
|
21
|
+
|
|
22
|
+
wait_with_countdown(WAIT_SECONDS, 'Waiting before next leverage update...')
|
|
23
|
+
|
|
24
|
+
puts 'Setting BTC to 3x isolated leverage...'
|
|
25
|
+
result = sdk.exchange.update_leverage(coin: perp_coin, leverage: 3, is_cross: false)
|
|
26
|
+
api_error?(result) || puts(green('3x isolated leverage set'))
|
|
27
|
+
puts
|
|
28
|
+
|
|
29
|
+
wait_with_countdown(WAIT_SECONDS, 'Waiting before resetting leverage...')
|
|
30
|
+
|
|
31
|
+
puts 'Resetting BTC to 1x cross leverage...'
|
|
32
|
+
result = sdk.exchange.update_leverage(coin: perp_coin, leverage: 1, is_cross: true)
|
|
33
|
+
api_error?(result) || puts(green('1x cross leverage set'))
|
|
34
|
+
else
|
|
35
|
+
puts red("SKIPPED: Could not get #{perp_coin} price")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
test_passed('Test 5 Update Leverage')
|
|
39
|
+
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Test 6: Modify Order (BTC)
|
|
5
|
+
# Place a limit buy, modify its price, then cancel.
|
|
6
|
+
|
|
7
|
+
require_relative 'test_helpers'
|
|
8
|
+
|
|
9
|
+
sdk = build_sdk
|
|
10
|
+
separator('TEST 6: Modify Order (BTC)')
|
|
11
|
+
|
|
12
|
+
perp_coin = 'BTC'
|
|
13
|
+
mids = sdk.info.all_mids
|
|
14
|
+
btc_price = mids[perp_coin]&.to_f
|
|
15
|
+
|
|
16
|
+
if btc_price&.positive?
|
|
17
|
+
meta = sdk.info.meta
|
|
18
|
+
btc_meta = meta['universe'].find { |a| a['name'] == perp_coin }
|
|
19
|
+
sz_decimals = btc_meta['szDecimals']
|
|
20
|
+
|
|
21
|
+
original_price = (btc_price * 0.50).round(0).to_i
|
|
22
|
+
modified_price = (btc_price * 0.51).round(0).to_i
|
|
23
|
+
perp_size = (20.0 / btc_price).ceil(sz_decimals)
|
|
24
|
+
|
|
25
|
+
puts "#{perp_coin} mid: $#{btc_price.round(2)}"
|
|
26
|
+
puts "Original limit: $#{original_price} (50% below mid)"
|
|
27
|
+
puts "Modified limit: $#{modified_price} (49% below mid)"
|
|
28
|
+
puts "Size: #{perp_size} BTC"
|
|
29
|
+
puts
|
|
30
|
+
|
|
31
|
+
puts 'Placing limit BUY order...'
|
|
32
|
+
result = sdk.exchange.order(
|
|
33
|
+
coin: perp_coin,
|
|
34
|
+
is_buy: true,
|
|
35
|
+
size: perp_size,
|
|
36
|
+
limit_px: original_price,
|
|
37
|
+
order_type: { limit: { tif: 'Gtc' } }
|
|
38
|
+
)
|
|
39
|
+
oid = check_result(result, 'Limit buy')
|
|
40
|
+
|
|
41
|
+
if oid.is_a?(Integer)
|
|
42
|
+
wait_with_countdown(WAIT_SECONDS, 'Order resting. Waiting before modify...')
|
|
43
|
+
|
|
44
|
+
puts "Modifying order #{oid} (price: $#{original_price} -> $#{modified_price})..."
|
|
45
|
+
result = sdk.exchange.modify_order(
|
|
46
|
+
oid: oid,
|
|
47
|
+
coin: perp_coin,
|
|
48
|
+
is_buy: true,
|
|
49
|
+
size: perp_size,
|
|
50
|
+
limit_px: modified_price
|
|
51
|
+
)
|
|
52
|
+
new_oid = check_result(result, 'Modify')
|
|
53
|
+
new_oid = oid unless new_oid.is_a?(Integer)
|
|
54
|
+
puts
|
|
55
|
+
|
|
56
|
+
wait_with_countdown(WAIT_SECONDS, 'Waiting before cancel...')
|
|
57
|
+
|
|
58
|
+
puts "Canceling modified order #{new_oid}..."
|
|
59
|
+
result = sdk.exchange.cancel(coin: perp_coin, oid: new_oid)
|
|
60
|
+
check_result(result, 'Cancel')
|
|
61
|
+
end
|
|
62
|
+
else
|
|
63
|
+
puts red("SKIPPED: Could not get #{perp_coin} price")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
test_passed('Test 6 Modify Order')
|
|
67
|
+
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Test 7: Market Close (BTC)
|
|
5
|
+
# Open a long position, then close it using market_close (auto-detect size).
|
|
6
|
+
|
|
7
|
+
require_relative 'test_helpers'
|
|
8
|
+
|
|
9
|
+
sdk = build_sdk
|
|
10
|
+
separator('TEST 7: Market Close (BTC)')
|
|
11
|
+
|
|
12
|
+
perp_coin = 'BTC'
|
|
13
|
+
mids = sdk.info.all_mids
|
|
14
|
+
btc_price = mids[perp_coin]&.to_f
|
|
15
|
+
|
|
16
|
+
if btc_price&.positive?
|
|
17
|
+
meta = sdk.info.meta
|
|
18
|
+
btc_meta = meta['universe'].find { |a| a['name'] == perp_coin }
|
|
19
|
+
sz_decimals = btc_meta['szDecimals']
|
|
20
|
+
|
|
21
|
+
perp_size = (20.0 / btc_price).ceil(sz_decimals)
|
|
22
|
+
|
|
23
|
+
puts "#{perp_coin} mid: $#{btc_price.round(2)}"
|
|
24
|
+
puts "Size: #{perp_size} BTC"
|
|
25
|
+
puts
|
|
26
|
+
|
|
27
|
+
puts 'Opening LONG position (market buy)...'
|
|
28
|
+
result = sdk.exchange.market_order(
|
|
29
|
+
coin: perp_coin,
|
|
30
|
+
is_buy: true,
|
|
31
|
+
size: perp_size,
|
|
32
|
+
slippage: PERP_SLIPPAGE
|
|
33
|
+
)
|
|
34
|
+
check_result(result, 'Long open')
|
|
35
|
+
|
|
36
|
+
wait_with_countdown(WAIT_SECONDS, 'Position open. Waiting before market_close...')
|
|
37
|
+
|
|
38
|
+
puts 'Closing position using market_close (auto-detect size)...'
|
|
39
|
+
result = sdk.exchange.market_close(
|
|
40
|
+
coin: perp_coin,
|
|
41
|
+
slippage: PERP_SLIPPAGE
|
|
42
|
+
)
|
|
43
|
+
check_result(result, 'Market close')
|
|
44
|
+
else
|
|
45
|
+
puts red("SKIPPED: Could not get #{perp_coin} price")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
test_passed('Test 7 Market Close')
|
|
49
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Test 8: USD Class Transfer (Perp <-> Spot)
|
|
5
|
+
# Transfer $10 from perp to spot, then back.
|
|
6
|
+
|
|
7
|
+
require_relative 'test_helpers'
|
|
8
|
+
|
|
9
|
+
sdk = build_sdk
|
|
10
|
+
separator('TEST 8: USD Class Transfer (Perp <-> Spot)')
|
|
11
|
+
|
|
12
|
+
puts 'Transferring $10 from perp to spot...'
|
|
13
|
+
result = sdk.exchange.usd_class_transfer(amount: '10', to_perp: false)
|
|
14
|
+
api_error?(result) || puts(green('Transfer to spot successful!'))
|
|
15
|
+
puts
|
|
16
|
+
|
|
17
|
+
wait_with_countdown(WAIT_SECONDS, 'Waiting before transferring back...')
|
|
18
|
+
|
|
19
|
+
puts 'Transferring $10 from spot to perp...'
|
|
20
|
+
result = sdk.exchange.usd_class_transfer(amount: '10', to_perp: true)
|
|
21
|
+
api_error?(result) || puts(green('Transfer to perp successful!'))
|
|
22
|
+
|
|
23
|
+
test_passed('Test 8 USD Class Transfer')
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Test 9: Sub Account Lifecycle
|
|
5
|
+
# Create a sub-account, deposit $10, then withdraw $10.
|
|
6
|
+
|
|
7
|
+
require_relative 'test_helpers'
|
|
8
|
+
|
|
9
|
+
sdk = build_sdk
|
|
10
|
+
separator('TEST 9: Sub Account Lifecycle')
|
|
11
|
+
|
|
12
|
+
puts 'Creating sub-account "ruby-sdk-test"...'
|
|
13
|
+
result = sdk.exchange.create_sub_account(name: 'ruby-sdk-test')
|
|
14
|
+
|
|
15
|
+
if api_error?(result)
|
|
16
|
+
exit 1
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
puts green('Sub-account created!')
|
|
20
|
+
puts
|
|
21
|
+
|
|
22
|
+
sub_account_address = result.dig('response', 'data', 'subAccountUser')
|
|
23
|
+
if sub_account_address
|
|
24
|
+
puts "Sub-account address: #{sub_account_address}"
|
|
25
|
+
puts
|
|
26
|
+
|
|
27
|
+
wait_with_countdown(WAIT_SECONDS, 'Waiting before deposit...')
|
|
28
|
+
|
|
29
|
+
puts 'Depositing $10 to sub-account...'
|
|
30
|
+
result = sdk.exchange.sub_account_transfer(
|
|
31
|
+
sub_account_user: sub_account_address,
|
|
32
|
+
is_deposit: true,
|
|
33
|
+
usd: 10
|
|
34
|
+
)
|
|
35
|
+
api_error?(result) || puts(green('Deposit successful!'))
|
|
36
|
+
puts
|
|
37
|
+
|
|
38
|
+
wait_with_countdown(WAIT_SECONDS, 'Waiting before withdrawal...')
|
|
39
|
+
|
|
40
|
+
puts 'Withdrawing $10 from sub-account...'
|
|
41
|
+
result = sdk.exchange.sub_account_transfer(
|
|
42
|
+
sub_account_user: sub_account_address,
|
|
43
|
+
is_deposit: false,
|
|
44
|
+
usd: 10
|
|
45
|
+
)
|
|
46
|
+
api_error?(result) || puts(green('Withdrawal successful!'))
|
|
47
|
+
else
|
|
48
|
+
puts red('SKIPPED: Could not extract sub-account address from response')
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
test_passed('Test 9 Sub Account Lifecycle')
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Test 10: Vault Status / Deposit / Withdraw
|
|
5
|
+
#
|
|
6
|
+
# Default: Show vault status (equity, entry date, unlock date)
|
|
7
|
+
# Options: ruby test_10_vault_transfer.rb deposit
|
|
8
|
+
# ruby test_10_vault_transfer.rb withdraw
|
|
9
|
+
|
|
10
|
+
require_relative 'test_helpers'
|
|
11
|
+
|
|
12
|
+
sdk = build_sdk
|
|
13
|
+
separator('TEST 10: Vault Status / Deposit / Withdraw')
|
|
14
|
+
|
|
15
|
+
vault_addr = '0xa15099a30bbf2e68942d6f4c43d70d04faeab0a0'
|
|
16
|
+
action = ARGV[0] # nil, "deposit", or "withdraw"
|
|
17
|
+
|
|
18
|
+
puts "Vault: #{vault_addr}"
|
|
19
|
+
puts
|
|
20
|
+
|
|
21
|
+
vault = sdk.info.vault_details(vault_addr, sdk.exchange.address)
|
|
22
|
+
follower = vault['followerState']
|
|
23
|
+
|
|
24
|
+
if follower
|
|
25
|
+
equity = follower['vaultEquity']
|
|
26
|
+
entry_time = follower['vaultEntryTime']
|
|
27
|
+
lockup_until = follower['lockupUntil']
|
|
28
|
+
|
|
29
|
+
puts "Vault equity: $#{equity}"
|
|
30
|
+
puts "Entry date: #{Time.at(entry_time / 1000.0).utc}" if entry_time
|
|
31
|
+
puts "Unlock date: #{Time.at(lockup_until / 1000.0).utc}" if lockup_until
|
|
32
|
+
else
|
|
33
|
+
puts 'No position in this vault.'
|
|
34
|
+
end
|
|
35
|
+
puts
|
|
36
|
+
|
|
37
|
+
case action
|
|
38
|
+
when 'deposit'
|
|
39
|
+
puts 'Depositing $10 to vault...'
|
|
40
|
+
result = sdk.exchange.vault_transfer(
|
|
41
|
+
vault_address: vault_addr,
|
|
42
|
+
is_deposit: true,
|
|
43
|
+
usd: 10
|
|
44
|
+
)
|
|
45
|
+
api_error?(result) || puts(green('Vault deposit successful!'))
|
|
46
|
+
when 'withdraw'
|
|
47
|
+
equity_f = follower&.dig('vaultEquity')&.to_f || 0
|
|
48
|
+
if equity_f > 1
|
|
49
|
+
withdraw_amount = equity_f.floor
|
|
50
|
+
puts "Withdrawing $#{withdraw_amount} from vault..."
|
|
51
|
+
result = sdk.exchange.vault_transfer(
|
|
52
|
+
vault_address: vault_addr,
|
|
53
|
+
is_deposit: false,
|
|
54
|
+
usd: withdraw_amount
|
|
55
|
+
)
|
|
56
|
+
api_error?(result) || puts(green('Vault withdrawal successful!'))
|
|
57
|
+
else
|
|
58
|
+
puts red("Insufficient vault equity to withdraw ($#{equity_f})")
|
|
59
|
+
end
|
|
60
|
+
else
|
|
61
|
+
puts 'Pass "deposit" or "withdraw" as an argument to perform a transfer.'
|
|
62
|
+
puts ' ruby scripts/test_10_vault_transfer.rb deposit'
|
|
63
|
+
puts ' ruby scripts/test_10_vault_transfer.rb withdraw'
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
test_passed('Test 10 Vault Status')
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Test 11: Builder Fee (Approve + Order with Builder)
|
|
5
|
+
# Approve a builder fee, then place an order with builder param, then cancel.
|
|
6
|
+
|
|
7
|
+
require_relative 'test_helpers'
|
|
8
|
+
|
|
9
|
+
sdk = build_sdk
|
|
10
|
+
separator('TEST 11: Builder Fee (Approve + Order with Builder)')
|
|
11
|
+
|
|
12
|
+
builder_address = '0x250F311Ae04D3CEA03443C76340069eD26C47D7D'
|
|
13
|
+
max_fee_rate = '0.01%'
|
|
14
|
+
perp_coin = 'BTC'
|
|
15
|
+
|
|
16
|
+
# Step 1: Approve builder fee
|
|
17
|
+
puts "Approving builder fee for #{builder_address} (max #{max_fee_rate})..."
|
|
18
|
+
result = sdk.exchange.approve_builder_fee(builder: builder_address, max_fee_rate: max_fee_rate)
|
|
19
|
+
api_error?(result) || puts(green('Builder fee approved'))
|
|
20
|
+
puts
|
|
21
|
+
|
|
22
|
+
wait_with_countdown(WAIT_SECONDS, 'Waiting before placing order with builder...')
|
|
23
|
+
|
|
24
|
+
# Step 2: Verify approval via Info API
|
|
25
|
+
puts 'Checking builder fee approval...'
|
|
26
|
+
approval = sdk.info.max_builder_fee(sdk.exchange.address, builder_address)
|
|
27
|
+
puts "Max builder fee: #{approval.inspect}"
|
|
28
|
+
puts
|
|
29
|
+
|
|
30
|
+
# Step 3: Place order with builder param
|
|
31
|
+
mids = sdk.info.all_mids
|
|
32
|
+
btc_price = mids[perp_coin]&.to_f
|
|
33
|
+
|
|
34
|
+
if btc_price&.positive?
|
|
35
|
+
meta = sdk.info.meta
|
|
36
|
+
btc_meta = meta['universe'].find { |a| a['name'] == perp_coin }
|
|
37
|
+
sz_decimals = btc_meta['szDecimals']
|
|
38
|
+
|
|
39
|
+
limit_price = (btc_price * 1.50).round(0).to_i
|
|
40
|
+
perp_size = (20.0 / btc_price).ceil(sz_decimals)
|
|
41
|
+
|
|
42
|
+
puts "#{perp_coin} mid: $#{btc_price.round(2)}"
|
|
43
|
+
puts "Limit price: $#{limit_price} (50% above mid - won't fill)"
|
|
44
|
+
puts "Size: #{perp_size} BTC"
|
|
45
|
+
puts "Builder: #{builder_address} (fee: 10 = 1bp)"
|
|
46
|
+
puts
|
|
47
|
+
|
|
48
|
+
puts 'Placing limit SELL order with builder fee...'
|
|
49
|
+
result = sdk.exchange.order(
|
|
50
|
+
coin: perp_coin,
|
|
51
|
+
is_buy: false,
|
|
52
|
+
size: perp_size,
|
|
53
|
+
limit_px: limit_price,
|
|
54
|
+
order_type: { limit: { tif: 'Gtc' } },
|
|
55
|
+
builder: { b: builder_address, f: 10 }
|
|
56
|
+
)
|
|
57
|
+
oid = check_result(result, 'Limit short with builder')
|
|
58
|
+
|
|
59
|
+
if oid.is_a?(Integer)
|
|
60
|
+
wait_with_countdown(WAIT_SECONDS, 'Order resting. Waiting before cancel...')
|
|
61
|
+
|
|
62
|
+
puts "Canceling order #{oid}..."
|
|
63
|
+
result = sdk.exchange.cancel(coin: perp_coin, oid: oid)
|
|
64
|
+
check_result(result, 'Cancel')
|
|
65
|
+
end
|
|
66
|
+
else
|
|
67
|
+
puts red("SKIPPED: Could not get #{perp_coin} price")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
test_passed('Test 11 Builder Fee')
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Test 12: Staking Status / Delegate / Undelegate
|
|
5
|
+
#
|
|
6
|
+
# Default: Show staking summary and delegations
|
|
7
|
+
# Options: ruby test_12_staking.rb delegate
|
|
8
|
+
# ruby test_12_staking.rb undelegate
|
|
9
|
+
|
|
10
|
+
require_relative 'test_helpers'
|
|
11
|
+
|
|
12
|
+
sdk = build_sdk
|
|
13
|
+
separator('TEST 12: Staking Status / Delegate / Undelegate')
|
|
14
|
+
|
|
15
|
+
validator = '0x946bf3135c7d15e4462b510f74b6e304aabb5b21'
|
|
16
|
+
action = ARGV[0] # nil, "delegate", or "undelegate"
|
|
17
|
+
delegate_amount = 10_000_000 # 0.1 HYPE (wei = float * 1e8)
|
|
18
|
+
|
|
19
|
+
puts "Validator: #{validator}"
|
|
20
|
+
puts
|
|
21
|
+
|
|
22
|
+
# Show staking summary
|
|
23
|
+
summary = sdk.info.delegator_summary(sdk.exchange.address)
|
|
24
|
+
puts "Staking summary:"
|
|
25
|
+
puts " Delegated: #{summary['delegated']}" if summary['delegated']
|
|
26
|
+
puts " Undelegatable: #{summary['undelegatable']}" if summary['undelegatable']
|
|
27
|
+
puts " Total pending: #{summary['totalPending']}" if summary['totalPending']
|
|
28
|
+
puts " N delegations: #{summary['nDelegations']}" if summary['nDelegations']
|
|
29
|
+
puts
|
|
30
|
+
|
|
31
|
+
# Show delegations for this validator
|
|
32
|
+
delegations = sdk.info.delegations(sdk.exchange.address)
|
|
33
|
+
validator_delegation = delegations&.find { |d| d['validator']&.downcase == validator.downcase }
|
|
34
|
+
|
|
35
|
+
if validator_delegation
|
|
36
|
+
puts "Delegation to #{validator}:"
|
|
37
|
+
puts " Amount: #{validator_delegation['amount']}"
|
|
38
|
+
lockup = validator_delegation['lockedUntilTimestamp']
|
|
39
|
+
puts " Locked until: #{Time.at(lockup / 1000.0).utc}" if lockup
|
|
40
|
+
else
|
|
41
|
+
puts "No active delegation to #{validator}."
|
|
42
|
+
end
|
|
43
|
+
puts
|
|
44
|
+
|
|
45
|
+
case action
|
|
46
|
+
when 'delegate'
|
|
47
|
+
puts "Delegating 0.1 HYPE to #{validator}..."
|
|
48
|
+
result = sdk.exchange.token_delegate(
|
|
49
|
+
validator: validator,
|
|
50
|
+
wei: delegate_amount,
|
|
51
|
+
is_undelegate: false
|
|
52
|
+
)
|
|
53
|
+
api_error?(result) || puts(green('Delegation successful!'))
|
|
54
|
+
when 'undelegate'
|
|
55
|
+
puts "Undelegating 0.1 HYPE from #{validator}..."
|
|
56
|
+
result = sdk.exchange.token_delegate(
|
|
57
|
+
validator: validator,
|
|
58
|
+
wei: delegate_amount,
|
|
59
|
+
is_undelegate: true
|
|
60
|
+
)
|
|
61
|
+
api_error?(result) || puts(green('Undelegation successful!'))
|
|
62
|
+
else
|
|
63
|
+
puts 'Pass "delegate" or "undelegate" as an argument to perform an action.'
|
|
64
|
+
puts ' ruby scripts/test_12_staking.rb delegate'
|
|
65
|
+
puts ' ruby scripts/test_12_staking.rb undelegate'
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
test_passed('Test 12 Staking')
|
data/scripts/test_all.rb
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Hyperliquid Ruby SDK - Testnet Integration Tests (Runner)
|
|
5
|
+
#
|
|
6
|
+
# Runs all integration test scripts in order.
|
|
7
|
+
# Each script can also be run individually for debugging.
|
|
8
|
+
#
|
|
9
|
+
# Prerequisites:
|
|
10
|
+
# - Testnet wallet with USDC balance
|
|
11
|
+
# - Get testnet funds from: https://app.hyperliquid-testnet.xyz
|
|
12
|
+
#
|
|
13
|
+
# Usage:
|
|
14
|
+
# HYPERLIQUID_PRIVATE_KEY=0x... ruby scripts/test_all.rb
|
|
15
|
+
#
|
|
16
|
+
# Run a single test:
|
|
17
|
+
# HYPERLIQUID_PRIVATE_KEY=0x... ruby scripts/test_08_usd_class_transfer.rb
|
|
18
|
+
#
|
|
19
|
+
# Note: These scripts execute real trades on testnet. No real funds are at risk.
|
|
20
|
+
|
|
21
|
+
SCRIPTS = [
|
|
22
|
+
'test_01_spot_market_roundtrip.rb',
|
|
23
|
+
'test_02_spot_limit_order.rb',
|
|
24
|
+
'test_03_perp_market_roundtrip.rb',
|
|
25
|
+
'test_04_perp_limit_order.rb',
|
|
26
|
+
'test_05_update_leverage.rb',
|
|
27
|
+
'test_06_modify_order.rb',
|
|
28
|
+
'test_07_market_close.rb',
|
|
29
|
+
'test_08_usd_class_transfer.rb',
|
|
30
|
+
'test_09_sub_account_lifecycle.rb',
|
|
31
|
+
'test_10_vault.rb',
|
|
32
|
+
'test_11_builder_fee.rb',
|
|
33
|
+
'test_12_staking.rb'
|
|
34
|
+
].freeze
|
|
35
|
+
|
|
36
|
+
def green(text)
|
|
37
|
+
"\e[32m#{text}\e[0m"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def red(text)
|
|
41
|
+
"\e[31m#{text}\e[0m"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
unless ENV['HYPERLIQUID_PRIVATE_KEY']
|
|
45
|
+
puts red('Error: Set HYPERLIQUID_PRIVATE_KEY environment variable')
|
|
46
|
+
puts 'Usage: HYPERLIQUID_PRIVATE_KEY=0x... ruby scripts/test_all.rb'
|
|
47
|
+
exit 1
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
scripts_dir = __dir__
|
|
51
|
+
passed = []
|
|
52
|
+
failed = []
|
|
53
|
+
|
|
54
|
+
SCRIPTS.each do |script|
|
|
55
|
+
path = File.join(scripts_dir, script)
|
|
56
|
+
puts
|
|
57
|
+
puts '#' * 60
|
|
58
|
+
puts "# Running: #{script}"
|
|
59
|
+
puts '#' * 60
|
|
60
|
+
|
|
61
|
+
success = system(RbConfig.ruby, path)
|
|
62
|
+
|
|
63
|
+
if success
|
|
64
|
+
passed << script
|
|
65
|
+
else
|
|
66
|
+
failed << script
|
|
67
|
+
puts red("!!! #{script} exited with error !!!")
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
puts
|
|
72
|
+
puts '=' * 60
|
|
73
|
+
puts 'INTEGRATION TEST SUMMARY'
|
|
74
|
+
puts '=' * 60
|
|
75
|
+
puts
|
|
76
|
+
puts "Passed: #{passed.length}/#{SCRIPTS.length}"
|
|
77
|
+
passed.each { |s| puts green(" [PASS] #{s}") }
|
|
78
|
+
if failed.any?
|
|
79
|
+
puts
|
|
80
|
+
puts "Failed: #{failed.length}/#{SCRIPTS.length}"
|
|
81
|
+
failed.each { |s| puts red(" [FAIL] #{s}") }
|
|
82
|
+
end
|
|
83
|
+
puts
|
|
84
|
+
puts 'Check your testnet wallet for trade history:'
|
|
85
|
+
puts 'https://app.hyperliquid-testnet.xyz'
|
|
86
|
+
puts
|
|
87
|
+
|
|
88
|
+
exit(failed.empty? ? 0 : 1)
|