hyperliquid 0.3.0 → 0.4.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 +21 -2
- data/CHANGELOG.md +24 -4
- data/CLAUDE.md +202 -0
- data/README.md +184 -7
- data/example.rb +131 -28
- data/lib/hyperliquid/cloid.rb +102 -0
- data/lib/hyperliquid/constants.rb +1 -0
- data/lib/hyperliquid/exchange.rb +410 -0
- data/lib/hyperliquid/signing/eip712.rb +56 -0
- data/lib/hyperliquid/signing/signer.rb +122 -0
- data/lib/hyperliquid/version.rb +1 -1
- data/lib/hyperliquid.rb +33 -5
- data/test_integration.rb +255 -0
- metadata +35 -1
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hyperliquid
|
|
4
|
+
module Signing
|
|
5
|
+
# EIP-712 domain and type definitions for Hyperliquid
|
|
6
|
+
# These values are defined by the Hyperliquid protocol and must match exactly
|
|
7
|
+
class EIP712
|
|
8
|
+
# Hyperliquid L1 chain ID (same for mainnet and testnet)
|
|
9
|
+
L1_CHAIN_ID = 1337
|
|
10
|
+
|
|
11
|
+
# Source identifier for phantom agent
|
|
12
|
+
MAINNET_SOURCE = 'a'
|
|
13
|
+
TESTNET_SOURCE = 'b'
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
# Domain for L1 actions (orders, cancels, leverage, etc.)
|
|
17
|
+
# @return [Hash] EIP-712 domain configuration
|
|
18
|
+
def l1_action_domain
|
|
19
|
+
{
|
|
20
|
+
name: 'Exchange',
|
|
21
|
+
version: '1',
|
|
22
|
+
chainId: L1_CHAIN_ID,
|
|
23
|
+
verifyingContract: '0x0000000000000000000000000000000000000000'
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# EIP-712 domain type definition
|
|
28
|
+
# @return [Array<Hash>] Domain type fields
|
|
29
|
+
def domain_type
|
|
30
|
+
[
|
|
31
|
+
{ name: :name, type: 'string' },
|
|
32
|
+
{ name: :version, type: 'string' },
|
|
33
|
+
{ name: :chainId, type: 'uint256' },
|
|
34
|
+
{ name: :verifyingContract, type: 'address' }
|
|
35
|
+
]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Agent type for phantom agent signing
|
|
39
|
+
# @return [Array<Hash>] Agent type fields
|
|
40
|
+
def agent_type
|
|
41
|
+
[
|
|
42
|
+
{ name: :source, type: 'string' },
|
|
43
|
+
{ name: :connectionId, type: 'bytes32' }
|
|
44
|
+
]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Get source identifier for phantom agent
|
|
48
|
+
# @param testnet [Boolean] Whether testnet
|
|
49
|
+
# @return [String] Source identifier ('a' for mainnet, 'b' for testnet)
|
|
50
|
+
def source(testnet:)
|
|
51
|
+
testnet ? TESTNET_SOURCE : MAINNET_SOURCE
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'eth'
|
|
4
|
+
require 'msgpack'
|
|
5
|
+
|
|
6
|
+
module Hyperliquid
|
|
7
|
+
module Signing
|
|
8
|
+
# EIP-712 signature generation for Hyperliquid exchange operations
|
|
9
|
+
# Implements the phantom agent signing scheme used by Hyperliquid
|
|
10
|
+
class Signer
|
|
11
|
+
# Initialize a new signer
|
|
12
|
+
# @param private_key [String] Ethereum private key (hex string with or without 0x prefix)
|
|
13
|
+
# @param testnet [Boolean] Whether to sign for testnet (default: false)
|
|
14
|
+
def initialize(private_key:, testnet: false)
|
|
15
|
+
@testnet = testnet
|
|
16
|
+
@key = Eth::Key.new(priv: normalize_private_key(private_key))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Get the wallet address
|
|
20
|
+
# @return [String] Checksummed Ethereum address
|
|
21
|
+
def address
|
|
22
|
+
@key.address.to_s
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Sign an L1 action (orders, cancels, leverage updates, etc.)
|
|
26
|
+
# @param action [Hash] The action payload to sign
|
|
27
|
+
# @param nonce [Integer] Timestamp in milliseconds
|
|
28
|
+
# @param vault_address [String, nil] Optional vault address for vault trading
|
|
29
|
+
# @param expires_after [Integer, nil] Optional expiration timestamp in milliseconds
|
|
30
|
+
# @return [Hash] Signature with :r, :s, :v components
|
|
31
|
+
def sign_l1_action(action, nonce, vault_address: nil, expires_after: nil)
|
|
32
|
+
phantom_agent = construct_phantom_agent(action, nonce, vault_address, expires_after)
|
|
33
|
+
|
|
34
|
+
typed_data = {
|
|
35
|
+
types: {
|
|
36
|
+
EIP712Domain: EIP712.domain_type,
|
|
37
|
+
Agent: EIP712.agent_type
|
|
38
|
+
},
|
|
39
|
+
primaryType: 'Agent',
|
|
40
|
+
domain: EIP712.l1_action_domain,
|
|
41
|
+
message: phantom_agent
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
sign_typed_data(typed_data)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
# Normalize private key format
|
|
50
|
+
# @param key [String] Private key with or without 0x prefix
|
|
51
|
+
# @return [String] Private key with 0x prefix
|
|
52
|
+
def normalize_private_key(key)
|
|
53
|
+
key.start_with?('0x') ? key : "0x#{key}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Construct the phantom agent for signing
|
|
57
|
+
# Maintains parity with Python SDK
|
|
58
|
+
# @param action [Hash] Action payload
|
|
59
|
+
# @param nonce [Integer] Nonce timestamp
|
|
60
|
+
# @param vault_address [String, nil] Optional vault address
|
|
61
|
+
# @param expires_after [Integer, nil] Optional expiration timestamp
|
|
62
|
+
# @return [Hash] Phantom agent with source and connectionId
|
|
63
|
+
def construct_phantom_agent(action, nonce, vault_address, expires_after)
|
|
64
|
+
# Compute action hash
|
|
65
|
+
# Maintains parity with Python SDK
|
|
66
|
+
# data = msgpack(action) + nonce(8 bytes BE) + vault_flag + [vault_addr] + [expires_flag + expires_after]
|
|
67
|
+
# - Note: expires_flag is only included if expires_after exists. A bit odd but that's what the
|
|
68
|
+
# Python SDK does.
|
|
69
|
+
data = action.to_msgpack
|
|
70
|
+
data += [nonce].pack('Q>') # 8-byte big-endian uint64
|
|
71
|
+
|
|
72
|
+
if vault_address.nil?
|
|
73
|
+
data += "\x00" # no vault flag
|
|
74
|
+
else
|
|
75
|
+
data += "\x01" # has vault flag
|
|
76
|
+
data += address_to_bytes(vault_address.downcase)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
unless expires_after.nil?
|
|
80
|
+
data += "\x00" # expiration flag
|
|
81
|
+
data += [expires_after].pack('Q>') # 8-byte big-endian uint64
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
connection_id = Eth::Util.keccak256(data)
|
|
85
|
+
|
|
86
|
+
{
|
|
87
|
+
source: EIP712.source(testnet: @testnet),
|
|
88
|
+
connectionId: bin_to_hex(connection_id)
|
|
89
|
+
}
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Convert hex address to 20-byte binary
|
|
93
|
+
# @param address [String] Ethereum address with 0x prefix
|
|
94
|
+
# @return [String] 20-byte binary representation
|
|
95
|
+
def address_to_bytes(address)
|
|
96
|
+
[address.sub(/\A0x/i, '')].pack('H*')
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Sign EIP-712 typed data using eth gem's built-in method
|
|
100
|
+
# @param typed_data [Hash] Complete EIP-712 structure
|
|
101
|
+
# @return [Hash] Signature components :r, :s, :v
|
|
102
|
+
def sign_typed_data(typed_data)
|
|
103
|
+
signature = @key.sign_typed_data(typed_data)
|
|
104
|
+
|
|
105
|
+
# Parse signature hex string into components
|
|
106
|
+
# Format: r (64 hex chars) + s (64 hex chars) + v (2 hex chars)
|
|
107
|
+
{
|
|
108
|
+
r: "0x#{signature[0, 64]}",
|
|
109
|
+
s: "0x#{signature[64, 64]}",
|
|
110
|
+
v: signature[128, 2].to_i(16)
|
|
111
|
+
}
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Convert binary data to hex string with 0x prefix
|
|
115
|
+
# @param bin [String] Binary data
|
|
116
|
+
# @return [String] Hex string with 0x prefix
|
|
117
|
+
def bin_to_hex(bin)
|
|
118
|
+
"0x#{bin.unpack1('H*')}"
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
data/lib/hyperliquid/version.rb
CHANGED
data/lib/hyperliquid.rb
CHANGED
|
@@ -5,33 +5,61 @@ require_relative 'hyperliquid/constants'
|
|
|
5
5
|
require_relative 'hyperliquid/errors'
|
|
6
6
|
require_relative 'hyperliquid/client'
|
|
7
7
|
require_relative 'hyperliquid/info'
|
|
8
|
+
require_relative 'hyperliquid/cloid'
|
|
9
|
+
require_relative 'hyperliquid/signing/eip712'
|
|
10
|
+
require_relative 'hyperliquid/signing/signer'
|
|
11
|
+
require_relative 'hyperliquid/exchange'
|
|
8
12
|
|
|
9
13
|
# Ruby SDK for Hyperliquid API
|
|
10
|
-
# Provides
|
|
14
|
+
# Provides access to Hyperliquid's decentralized exchange API
|
|
15
|
+
# including both read (Info) and write (Exchange) operations
|
|
11
16
|
module Hyperliquid
|
|
12
17
|
# Create a new SDK instance
|
|
13
18
|
# @param testnet [Boolean] Whether to use testnet (default: false for mainnet)
|
|
14
19
|
# @param timeout [Integer] Request timeout in seconds (default: 30)
|
|
15
20
|
# @param retry_enabled [Boolean] Whether to enable retry logic (default: false)
|
|
21
|
+
# @param private_key [String, nil] Ethereum private key for exchange operations (optional)
|
|
22
|
+
# @param expires_after [Integer, nil] Global order expiration timestamp in ms (optional)
|
|
16
23
|
# @return [Hyperliquid::SDK] A new SDK instance
|
|
17
|
-
def self.new(testnet: false, timeout: Constants::DEFAULT_TIMEOUT, retry_enabled: false
|
|
18
|
-
|
|
24
|
+
def self.new(testnet: false, timeout: Constants::DEFAULT_TIMEOUT, retry_enabled: false,
|
|
25
|
+
private_key: nil, expires_after: nil)
|
|
26
|
+
SDK.new(
|
|
27
|
+
testnet: testnet,
|
|
28
|
+
timeout: timeout,
|
|
29
|
+
retry_enabled: retry_enabled,
|
|
30
|
+
private_key: private_key,
|
|
31
|
+
expires_after: expires_after
|
|
32
|
+
)
|
|
19
33
|
end
|
|
20
34
|
|
|
21
35
|
# Main SDK class
|
|
22
36
|
class SDK
|
|
23
|
-
attr_reader :info
|
|
37
|
+
attr_reader :info, :exchange
|
|
24
38
|
|
|
25
39
|
# Initialize the SDK
|
|
26
40
|
# @param testnet [Boolean] Whether to use testnet (default: false for mainnet)
|
|
27
41
|
# @param timeout [Integer] Request timeout in seconds
|
|
28
42
|
# @param retry_enabled [Boolean] Whether to enable retry logic (default: false)
|
|
29
|
-
|
|
43
|
+
# @param private_key [String, nil] Ethereum private key for exchange operations (optional)
|
|
44
|
+
# @param expires_after [Integer, nil] Global order expiration timestamp in ms (optional)
|
|
45
|
+
def initialize(testnet: false, timeout: Constants::DEFAULT_TIMEOUT, retry_enabled: false,
|
|
46
|
+
private_key: nil, expires_after: nil)
|
|
30
47
|
base_url = testnet ? Constants::TESTNET_API_URL : Constants::MAINNET_API_URL
|
|
31
48
|
client = Client.new(base_url: base_url, timeout: timeout, retry_enabled: retry_enabled)
|
|
32
49
|
|
|
33
50
|
@info = Info.new(client)
|
|
34
51
|
@testnet = testnet
|
|
52
|
+
@exchange = nil
|
|
53
|
+
|
|
54
|
+
return unless private_key
|
|
55
|
+
|
|
56
|
+
signer = Signing::Signer.new(private_key: private_key, testnet: testnet)
|
|
57
|
+
@exchange = Exchange.new(
|
|
58
|
+
client: client,
|
|
59
|
+
signer: signer,
|
|
60
|
+
info: @info,
|
|
61
|
+
expires_after: expires_after
|
|
62
|
+
)
|
|
35
63
|
end
|
|
36
64
|
|
|
37
65
|
# Check if using testnet
|
data/test_integration.rb
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Hyperliquid Ruby SDK - Testnet Integration Test
|
|
5
|
+
#
|
|
6
|
+
# This script tests the Exchange API against the live testnet:
|
|
7
|
+
# 1. Spot market roundtrip (buy PURR, sell PURR)
|
|
8
|
+
# 2. Spot limit order (place and cancel)
|
|
9
|
+
# 3. Perp market roundtrip (long BTC, close position)
|
|
10
|
+
# 4. Perp limit order (place short, cancel)
|
|
11
|
+
#
|
|
12
|
+
# Prerequisites:
|
|
13
|
+
# - Testnet wallet with USDC balance
|
|
14
|
+
# - Get testnet funds from: https://app.hyperliquid-testnet.xyz
|
|
15
|
+
#
|
|
16
|
+
# Usage:
|
|
17
|
+
# HYPERLIQUID_PRIVATE_KEY=0x... ruby test_integration.rb
|
|
18
|
+
#
|
|
19
|
+
# Note: This script executes real trades on testnet. No real funds are at risk.
|
|
20
|
+
|
|
21
|
+
require_relative 'lib/hyperliquid'
|
|
22
|
+
require 'json'
|
|
23
|
+
|
|
24
|
+
WAIT_SECONDS = 10
|
|
25
|
+
SPOT_SLIPPAGE = 0.40 # 40% for illiquid testnet spot markets
|
|
26
|
+
PERP_SLIPPAGE = 0.05 # 5% for perp markets
|
|
27
|
+
|
|
28
|
+
def separator(title)
|
|
29
|
+
puts
|
|
30
|
+
puts '=' * 60
|
|
31
|
+
puts title
|
|
32
|
+
puts '=' * 60
|
|
33
|
+
puts
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def wait_with_countdown(seconds, message)
|
|
37
|
+
puts message
|
|
38
|
+
seconds.downto(1) do |i|
|
|
39
|
+
print "\r #{i} seconds remaining... "
|
|
40
|
+
sleep 1
|
|
41
|
+
end
|
|
42
|
+
puts "\r Done! "
|
|
43
|
+
puts
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def check_result(result, operation)
|
|
47
|
+
status = result.dig('response', 'data', 'statuses', 0)
|
|
48
|
+
|
|
49
|
+
if status.is_a?(Hash) && status['error']
|
|
50
|
+
puts "FAILED: #{status['error']}"
|
|
51
|
+
return false
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
if status.is_a?(Hash) && status['resting']
|
|
55
|
+
puts "Order resting with OID: #{status['resting']['oid']}"
|
|
56
|
+
return status['resting']['oid']
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
if status == 'success' || (status.is_a?(Hash) && status['filled'])
|
|
60
|
+
puts "#{operation} successful!"
|
|
61
|
+
return true
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
puts "Result: #{status.inspect}"
|
|
65
|
+
true
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# --- Main Script ---
|
|
69
|
+
|
|
70
|
+
private_key = ENV['HYPERLIQUID_PRIVATE_KEY']
|
|
71
|
+
unless private_key
|
|
72
|
+
puts 'Error: Set HYPERLIQUID_PRIVATE_KEY environment variable'
|
|
73
|
+
puts 'Usage: HYPERLIQUID_PRIVATE_KEY=0x... ruby test_integration.rb'
|
|
74
|
+
exit 1
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
sdk = Hyperliquid.new(
|
|
78
|
+
testnet: true,
|
|
79
|
+
private_key: private_key
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
puts 'Hyperliquid Ruby SDK - Testnet Integration Test'
|
|
83
|
+
puts '=' * 60
|
|
84
|
+
puts "Wallet: #{sdk.exchange.address}"
|
|
85
|
+
puts 'Network: Testnet'
|
|
86
|
+
puts "Testnet UI: https://app.hyperliquid-testnet.xyz"
|
|
87
|
+
|
|
88
|
+
# ============================================================
|
|
89
|
+
# TEST 1: Spot Market Roundtrip (PURR/USDC)
|
|
90
|
+
# ============================================================
|
|
91
|
+
separator('TEST 1: Spot Market Roundtrip (PURR/USDC)')
|
|
92
|
+
|
|
93
|
+
spot_coin = 'PURR/USDC'
|
|
94
|
+
spot_size = 5 # PURR has 0 decimals
|
|
95
|
+
|
|
96
|
+
mids = sdk.info.all_mids
|
|
97
|
+
spot_price = mids[spot_coin]&.to_f
|
|
98
|
+
|
|
99
|
+
if spot_price&.positive?
|
|
100
|
+
puts "#{spot_coin} mid: $#{spot_price}"
|
|
101
|
+
puts "Size: #{spot_size} PURR (~$#{(spot_size * spot_price).round(2)})"
|
|
102
|
+
puts "Slippage: #{(SPOT_SLIPPAGE * 100).to_i}%"
|
|
103
|
+
puts
|
|
104
|
+
|
|
105
|
+
# Buy
|
|
106
|
+
puts 'Placing market BUY...'
|
|
107
|
+
result = sdk.exchange.market_order(
|
|
108
|
+
coin: spot_coin,
|
|
109
|
+
is_buy: true,
|
|
110
|
+
size: spot_size,
|
|
111
|
+
slippage: SPOT_SLIPPAGE
|
|
112
|
+
)
|
|
113
|
+
check_result(result, 'Buy')
|
|
114
|
+
|
|
115
|
+
wait_with_countdown(WAIT_SECONDS, 'Waiting before sell...')
|
|
116
|
+
|
|
117
|
+
# Sell
|
|
118
|
+
puts 'Placing market SELL...'
|
|
119
|
+
result = sdk.exchange.market_order(
|
|
120
|
+
coin: spot_coin,
|
|
121
|
+
is_buy: false,
|
|
122
|
+
size: spot_size,
|
|
123
|
+
slippage: SPOT_SLIPPAGE
|
|
124
|
+
)
|
|
125
|
+
check_result(result, 'Sell')
|
|
126
|
+
else
|
|
127
|
+
puts "SKIPPED: Could not get #{spot_coin} price"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# ============================================================
|
|
131
|
+
# TEST 2: Spot Limit Order (Place and Cancel)
|
|
132
|
+
# ============================================================
|
|
133
|
+
separator('TEST 2: Spot Limit Order (Place and Cancel)')
|
|
134
|
+
|
|
135
|
+
if spot_price&.positive?
|
|
136
|
+
# Place limit buy well below market (won't fill)
|
|
137
|
+
limit_price = (spot_price * 0.50).round(2) # 50% below mid
|
|
138
|
+
puts "#{spot_coin} mid: $#{spot_price}"
|
|
139
|
+
puts "Limit price: $#{limit_price} (50% below mid - won't fill)"
|
|
140
|
+
puts "Size: #{spot_size} PURR"
|
|
141
|
+
puts
|
|
142
|
+
|
|
143
|
+
puts 'Placing limit BUY order...'
|
|
144
|
+
result = sdk.exchange.order(
|
|
145
|
+
coin: spot_coin,
|
|
146
|
+
is_buy: true,
|
|
147
|
+
size: spot_size,
|
|
148
|
+
limit_px: limit_price,
|
|
149
|
+
order_type: { limit: { tif: 'Gtc' } },
|
|
150
|
+
reduce_only: false
|
|
151
|
+
)
|
|
152
|
+
oid = check_result(result, 'Limit order')
|
|
153
|
+
|
|
154
|
+
if oid.is_a?(Integer)
|
|
155
|
+
wait_with_countdown(WAIT_SECONDS, 'Order resting. Waiting before cancel...')
|
|
156
|
+
|
|
157
|
+
puts "Canceling order #{oid}..."
|
|
158
|
+
result = sdk.exchange.cancel(coin: spot_coin, oid: oid)
|
|
159
|
+
check_result(result, 'Cancel')
|
|
160
|
+
end
|
|
161
|
+
else
|
|
162
|
+
puts "SKIPPED: Could not get #{spot_coin} price"
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# ============================================================
|
|
166
|
+
# TEST 3: Perp Market Roundtrip (BTC Long)
|
|
167
|
+
# ============================================================
|
|
168
|
+
separator('TEST 3: Perp Market Roundtrip (BTC Long)')
|
|
169
|
+
|
|
170
|
+
perp_coin = 'BTC'
|
|
171
|
+
btc_price = mids[perp_coin]&.to_f
|
|
172
|
+
|
|
173
|
+
if btc_price&.positive?
|
|
174
|
+
# Get BTC metadata for size precision
|
|
175
|
+
meta = sdk.info.meta
|
|
176
|
+
btc_meta = meta['universe'].find { |a| a['name'] == perp_coin }
|
|
177
|
+
sz_decimals = btc_meta['szDecimals']
|
|
178
|
+
|
|
179
|
+
# Calculate size for ~$20 notional
|
|
180
|
+
perp_size = (20.0 / btc_price).ceil(sz_decimals)
|
|
181
|
+
|
|
182
|
+
puts "#{perp_coin} mid: $#{btc_price.round(2)}"
|
|
183
|
+
puts "Size: #{perp_size} BTC (~$#{(perp_size * btc_price).round(2)})"
|
|
184
|
+
puts "Slippage: #{(PERP_SLIPPAGE * 100).to_i}%"
|
|
185
|
+
puts
|
|
186
|
+
|
|
187
|
+
# Open long
|
|
188
|
+
puts 'Opening LONG position (market buy)...'
|
|
189
|
+
result = sdk.exchange.market_order(
|
|
190
|
+
coin: perp_coin,
|
|
191
|
+
is_buy: true,
|
|
192
|
+
size: perp_size,
|
|
193
|
+
slippage: PERP_SLIPPAGE
|
|
194
|
+
)
|
|
195
|
+
check_result(result, 'Long open')
|
|
196
|
+
|
|
197
|
+
wait_with_countdown(WAIT_SECONDS, 'Position open. Waiting before close...')
|
|
198
|
+
|
|
199
|
+
# Close long (sell to close)
|
|
200
|
+
puts 'Closing LONG position (market sell)...'
|
|
201
|
+
result = sdk.exchange.market_order(
|
|
202
|
+
coin: perp_coin,
|
|
203
|
+
is_buy: false,
|
|
204
|
+
size: perp_size,
|
|
205
|
+
slippage: PERP_SLIPPAGE
|
|
206
|
+
)
|
|
207
|
+
check_result(result, 'Long close')
|
|
208
|
+
else
|
|
209
|
+
puts "SKIPPED: Could not get #{perp_coin} price"
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# ============================================================
|
|
213
|
+
# TEST 4: Perp Limit Order (Short, then Cancel)
|
|
214
|
+
# ============================================================
|
|
215
|
+
separator('TEST 4: Perp Limit Order (Short, then Cancel)')
|
|
216
|
+
|
|
217
|
+
if btc_price&.positive?
|
|
218
|
+
# Place limit sell well above market (won't fill)
|
|
219
|
+
limit_price = (btc_price * 1.50).round(0).to_i # 50% above mid, whole number tick
|
|
220
|
+
perp_size = (20.0 / btc_price).ceil(sz_decimals)
|
|
221
|
+
|
|
222
|
+
puts "#{perp_coin} mid: $#{btc_price.round(2)}"
|
|
223
|
+
puts "Limit price: $#{limit_price} (50% above mid - won't fill)"
|
|
224
|
+
puts "Size: #{perp_size} BTC"
|
|
225
|
+
puts
|
|
226
|
+
|
|
227
|
+
puts 'Placing limit SELL order (short)...'
|
|
228
|
+
result = sdk.exchange.order(
|
|
229
|
+
coin: perp_coin,
|
|
230
|
+
is_buy: false,
|
|
231
|
+
size: perp_size,
|
|
232
|
+
limit_px: limit_price,
|
|
233
|
+
order_type: { limit: { tif: 'Gtc' } },
|
|
234
|
+
reduce_only: false
|
|
235
|
+
)
|
|
236
|
+
oid = check_result(result, 'Limit short')
|
|
237
|
+
|
|
238
|
+
if oid.is_a?(Integer)
|
|
239
|
+
wait_with_countdown(WAIT_SECONDS, 'Order resting. Waiting before cancel...')
|
|
240
|
+
|
|
241
|
+
puts "Canceling order #{oid}..."
|
|
242
|
+
result = sdk.exchange.cancel(coin: perp_coin, oid: oid)
|
|
243
|
+
check_result(result, 'Cancel')
|
|
244
|
+
end
|
|
245
|
+
else
|
|
246
|
+
puts "SKIPPED: Could not get #{perp_coin} price"
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# ============================================================
|
|
250
|
+
# Summary
|
|
251
|
+
# ============================================================
|
|
252
|
+
separator('INTEGRATION TEST COMPLETE')
|
|
253
|
+
puts 'All tests executed. Check your testnet wallet for trade history:'
|
|
254
|
+
puts 'https://app.hyperliquid-testnet.xyz'
|
|
255
|
+
puts
|
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: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- carter2099
|
|
@@ -9,6 +9,20 @@ bindir: exe
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: eth
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.5'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.5'
|
|
12
26
|
- !ruby/object:Gem::Dependency
|
|
13
27
|
name: faraday
|
|
14
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -37,6 +51,20 @@ dependencies:
|
|
|
37
51
|
- - "~>"
|
|
38
52
|
- !ruby/object:Gem::Version
|
|
39
53
|
version: '2.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: msgpack
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '1.7'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - "~>"
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '1.7'
|
|
40
68
|
description: A Ruby SDK for interacting with Hyperliquid's decentralized exchange
|
|
41
69
|
API
|
|
42
70
|
email:
|
|
@@ -49,6 +77,7 @@ files:
|
|
|
49
77
|
- ".rubocop.yml"
|
|
50
78
|
- ".ruby-version"
|
|
51
79
|
- CHANGELOG.md
|
|
80
|
+
- CLAUDE.md
|
|
52
81
|
- CODE_OF_CONDUCT.md
|
|
53
82
|
- LICENSE.txt
|
|
54
83
|
- README.md
|
|
@@ -56,11 +85,16 @@ files:
|
|
|
56
85
|
- example.rb
|
|
57
86
|
- lib/hyperliquid.rb
|
|
58
87
|
- lib/hyperliquid/client.rb
|
|
88
|
+
- lib/hyperliquid/cloid.rb
|
|
59
89
|
- lib/hyperliquid/constants.rb
|
|
60
90
|
- lib/hyperliquid/errors.rb
|
|
91
|
+
- lib/hyperliquid/exchange.rb
|
|
61
92
|
- lib/hyperliquid/info.rb
|
|
93
|
+
- lib/hyperliquid/signing/eip712.rb
|
|
94
|
+
- lib/hyperliquid/signing/signer.rb
|
|
62
95
|
- lib/hyperliquid/version.rb
|
|
63
96
|
- sig/hyperliquid.rbs
|
|
97
|
+
- test_integration.rb
|
|
64
98
|
homepage: https://github.com/carter2099/hyperliquid
|
|
65
99
|
licenses:
|
|
66
100
|
- MIT
|