hyperliquid 0.2.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.
@@ -7,6 +7,10 @@ module Hyperliquid
7
7
  @client = client
8
8
  end
9
9
 
10
+ # ============================
11
+ # Info
12
+ # ============================
13
+
10
14
  # Get all market mid prices
11
15
  # @return [Hash] Hash containing mid prices for all markets
12
16
  def all_mids
@@ -15,9 +19,22 @@ module Hyperliquid
15
19
 
16
20
  # Get a user's open orders
17
21
  # @param user [String] Wallet address
22
+ # @param dex [String, nil] Optional perp dex name
18
23
  # @return [Array] Array of open orders for the user
19
- def open_orders(user)
20
- @client.post(Constants::INFO_ENDPOINT, { type: 'openOrders', user: user })
24
+ def open_orders(user, dex: nil)
25
+ body = { type: 'openOrders', user: user }
26
+ body[:dex] = dex if dex
27
+ @client.post(Constants::INFO_ENDPOINT, body)
28
+ end
29
+
30
+ # Get a user's open orders with additional frontend info
31
+ # @param user [String] Wallet address
32
+ # @param dex [String, nil] Optional perp dex name
33
+ # @return [Array]
34
+ def frontend_open_orders(user, dex: nil)
35
+ body = { type: 'frontendOpenOrders', user: user }
36
+ body[:dex] = dex if dex
37
+ @client.post(Constants::INFO_ENDPOINT, body)
21
38
  end
22
39
 
23
40
  # Get a user's fill history
@@ -27,6 +44,24 @@ module Hyperliquid
27
44
  @client.post(Constants::INFO_ENDPOINT, { type: 'userFills', user: user })
28
45
  end
29
46
 
47
+ # Get a user's fills within a time range
48
+ # @param user [String] Wallet address
49
+ # @param start_time [Integer] Start timestamp in milliseconds
50
+ # @param end_time [Integer, nil] Optional end timestamp in milliseconds
51
+ # @return [Array]
52
+ def user_fills_by_time(user, start_time, end_time = nil)
53
+ body = { type: 'userFillsByTime', user: user, startTime: start_time }
54
+ body[:endTime] = end_time if end_time
55
+ @client.post(Constants::INFO_ENDPOINT, body)
56
+ end
57
+
58
+ # Query user rate limits and usage
59
+ # @param user [String] Wallet address
60
+ # @return [Hash]
61
+ def user_rate_limit(user)
62
+ @client.post(Constants::INFO_ENDPOINT, { type: 'userRateLimit', user: user })
63
+ end
64
+
30
65
  # Get order status by order ID
31
66
  # @param user [String] Wallet address
32
67
  # @param oid [Integer] Order ID
@@ -35,23 +70,12 @@ module Hyperliquid
35
70
  @client.post(Constants::INFO_ENDPOINT, { type: 'orderStatus', user: user, oid: oid })
36
71
  end
37
72
 
38
- # Get user's trading state
73
+ # Get order status by client order ID (cloid)
39
74
  # @param user [String] Wallet address
40
- # @return [Hash] User's trading state including positions and balances
41
- def user_state(user)
42
- @client.post(Constants::INFO_ENDPOINT, { type: 'clearinghouseState', user: user })
43
- end
44
-
45
- # Get metadata for all assets
46
- # @return [Hash] Metadata for all tradable assets
47
- def meta
48
- @client.post(Constants::INFO_ENDPOINT, { type: 'meta' })
49
- end
50
-
51
- # Get metadata for all assets including universe info
52
- # @return [Hash] Extended metadata for all assets with universe information
53
- def meta_and_asset_ctxs
54
- @client.post(Constants::INFO_ENDPOINT, { type: 'metaAndAssetCtxs' })
75
+ # @param cloid [String] Client order ID
76
+ # @return [Hash] Order status information
77
+ def order_status_by_cloid(user, cloid)
78
+ @client.post(Constants::INFO_ENDPOINT, { type: 'orderStatus', user: user, cloid: cloid })
55
79
  end
56
80
 
57
81
  # Get L2 order book for a coin
@@ -79,8 +103,221 @@ module Hyperliquid
79
103
  })
80
104
  end
81
105
 
106
+ # Check builder fee approval
107
+ # @param user [String] Wallet address
108
+ # @param builder [String] Builder address
109
+ # @return [Hash]
110
+ def max_builder_fee(user, builder)
111
+ @client.post(Constants::INFO_ENDPOINT, { type: 'maxBuilderFee', user: user, builder: builder })
112
+ end
113
+
114
+ # Retrieve a user's historical orders
115
+ # @param user [String] Wallet address
116
+ # @param start_time [Integer, nil] Optional start timestamp in milliseconds
117
+ # @param end_time [Integer, nil] Optional end timestamp in milliseconds
118
+ # @return [Array]
119
+ def historical_orders(user, start_time = nil, end_time = nil)
120
+ body = { type: 'historicalOrders', user: user }
121
+ body[:startTime] = start_time if start_time
122
+ body[:endTime] = end_time if end_time
123
+ @client.post(Constants::INFO_ENDPOINT, body)
124
+ end
125
+
126
+ # Retrieve a user's TWAP slice fills
127
+ # @param user [String] Wallet address
128
+ # @param start_time [Integer, nil] Optional start timestamp in milliseconds
129
+ # @param end_time [Integer, nil] Optional end timestamp in milliseconds
130
+ # @return [Array]
131
+ def user_twap_slice_fills(user, start_time = nil, end_time = nil)
132
+ body = { type: 'userTwapSliceFills', user: user }
133
+ body[:startTime] = start_time if start_time
134
+ body[:endTime] = end_time if end_time
135
+ @client.post(Constants::INFO_ENDPOINT, body)
136
+ end
137
+
138
+ # Retrieve a user's subaccounts
139
+ # @param user [String]
140
+ # @return [Array]
141
+ def user_subaccounts(user)
142
+ @client.post(Constants::INFO_ENDPOINT, { type: 'subaccounts', user: user })
143
+ end
144
+
145
+ # Retrieve details for a vault
146
+ # @param vault_address [String] Vault address
147
+ # @param user [String, nil] Optional wallet address
148
+ # @return [Hash]
149
+ def vault_details(vault_address, user = nil)
150
+ body = { type: 'vaultDetails', vaultAddress: vault_address }
151
+ body[:user] = user if user
152
+ @client.post(Constants::INFO_ENDPOINT, body)
153
+ end
154
+
155
+ # Retrieve a user's vault deposits
156
+ # @param user [String] Wallet address
157
+ # @return [Array]
158
+ def user_vault_equities(user)
159
+ @client.post(Constants::INFO_ENDPOINT, { type: 'userVaultEquities', user: user })
160
+ end
161
+
162
+ # Query a user's role
163
+ # @param user [String]
164
+ # @return [Hash]
165
+ def user_role(user)
166
+ @client.post(Constants::INFO_ENDPOINT, { type: 'userRole', user: user })
167
+ end
168
+
169
+ # Query a user's portfolio time series
170
+ # @param user [String]
171
+ # @return [Array]
172
+ def portfolio(user)
173
+ @client.post(Constants::INFO_ENDPOINT, { type: 'portfolio', user: user })
174
+ end
175
+
176
+ # Query a user's referral information
177
+ # @param user [String]
178
+ # @return [Hash]
179
+ def referral(user)
180
+ @client.post(Constants::INFO_ENDPOINT, { type: 'referral', user: user })
181
+ end
182
+
183
+ # Query a user's effective fee rates and schedule
184
+ # @param user [String]
185
+ # @return [Hash]
186
+ def user_fees(user)
187
+ @client.post(Constants::INFO_ENDPOINT, { type: 'userFees', user: user })
188
+ end
189
+
190
+ # Query a user's staking delegations
191
+ # @param user [String]
192
+ # @return [Array]
193
+ def delegations(user)
194
+ @client.post(Constants::INFO_ENDPOINT, { type: 'delegations', user: user })
195
+ end
196
+
197
+ # Query a user's staking summary
198
+ # @param user [String]
199
+ # @return [Hash]
200
+ def delegator_summary(user)
201
+ @client.post(Constants::INFO_ENDPOINT, { type: 'delegatorSummary', user: user })
202
+ end
203
+
204
+ # Query a user's staking history
205
+ # @param user [String]
206
+ # @return [Array]
207
+ def delegator_history(user)
208
+ @client.post(Constants::INFO_ENDPOINT, { type: 'delegatorHistory', user: user })
209
+ end
210
+
211
+ # Query a user's staking rewards
212
+ # @param user [String]
213
+ # @return [Array]
214
+ def delegator_rewards(user)
215
+ @client.post(Constants::INFO_ENDPOINT, { type: 'delegatorRewards', user: user })
216
+ end
217
+
218
+ # ============================
219
+ # Info: Perpetuals
220
+ # ============================
221
+
222
+ # Retrieve all perpetual dexs
223
+ # @return [Array]
224
+ def perp_dexs
225
+ @client.post(Constants::INFO_ENDPOINT, { type: 'perpDexs' })
226
+ end
227
+
228
+ # Get metadata for all assets
229
+ # @return [Hash] Metadata for all tradable assets
230
+ # @param dex [String, nil] Optional perp dex name (defaults to first perp dex when not provided)
231
+ def meta(dex: nil)
232
+ body = { type: 'meta' }
233
+ body[:dex] = dex if dex
234
+ @client.post(Constants::INFO_ENDPOINT, body)
235
+ end
236
+
237
+ # Get metadata for all assets including universe info
238
+ # @return [Hash] Extended metadata for all assets with universe information
239
+ def meta_and_asset_ctxs
240
+ @client.post(Constants::INFO_ENDPOINT, { type: 'metaAndAssetCtxs' })
241
+ end
242
+
243
+ # Get user's trading state
244
+ # @param user [String] Wallet address
245
+ # @param dex [String, nil] Optional perp dex name
246
+ # @return [Hash] User's trading state including positions and balances
247
+ def user_state(user, dex: nil)
248
+ body = { type: 'clearinghouseState', user: user }
249
+ body[:dex] = dex if dex
250
+ @client.post(Constants::INFO_ENDPOINT, body)
251
+ end
252
+
253
+ # Retrieve predicted funding rates for different venues
254
+ # @return [Array]
255
+ def predicted_fundings
256
+ @client.post(Constants::INFO_ENDPOINT, { type: 'predictedFundings' })
257
+ end
258
+
259
+ # Query perps at open interest caps
260
+ # @return [Array]
261
+ def perps_at_open_interest_cap
262
+ @client.post(Constants::INFO_ENDPOINT, { type: 'perpsAtOpenInterestCap' })
263
+ end
264
+
265
+ # Retrieve information about the Perp Deploy Auction
266
+ # @return [Hash]
267
+ def perp_deploy_auction_status
268
+ @client.post(Constants::INFO_ENDPOINT, { type: 'perpDeployAuctionStatus' })
269
+ end
270
+
271
+ # Retrieve User's Active Asset Data
272
+ # @param user [String]
273
+ # @param coin [String]
274
+ # @return [Hash]
275
+ def active_asset_data(user, coin)
276
+ @client.post(Constants::INFO_ENDPOINT, { type: 'activeAssetData', user: user, coin: coin })
277
+ end
278
+
279
+ # Retrieve Builder-Deployed Perp Market Limits
280
+ # @param dex [String]
281
+ # @return [Hash]
282
+ def perp_dex_limits(dex)
283
+ @client.post(Constants::INFO_ENDPOINT, { type: 'perpDexLimits', dex: dex })
284
+ end
285
+
286
+ # Retrieve a user's funding history
287
+ # @param user [String]
288
+ # @param start_time [Integer]
289
+ # @param end_time [Integer, nil]
290
+ # @return [Array]
291
+ def user_funding(user, start_time, end_time = nil)
292
+ body = { type: 'userFunding', user: user, startTime: start_time }
293
+ body[:endTime] = end_time if end_time
294
+ @client.post(Constants::INFO_ENDPOINT, body)
295
+ end
296
+
297
+ # Retrieve a user's non-funding ledger updates
298
+ # @param user [String]
299
+ # @param start_time [Integer]
300
+ # @param end_time [Integer, nil]
301
+ # @return [Array]
302
+ def user_non_funding_ledger_updates(user, start_time, end_time = nil)
303
+ body = { type: 'userNonFundingLedgerUpdates', user: user, startTime: start_time }
304
+ body[:endTime] = end_time if end_time
305
+ @client.post(Constants::INFO_ENDPOINT, body)
306
+ end
307
+
308
+ # Retrieve historical funding rates
309
+ # @param coin [String]
310
+ # @param start_time [Integer]
311
+ # @param end_time [Integer, nil]
312
+ # @return [Array]
313
+ def funding_history(coin, start_time, end_time = nil)
314
+ body = { type: 'fundingHistory', coin: coin, startTime: start_time }
315
+ body[:endTime] = end_time if end_time
316
+ @client.post(Constants::INFO_ENDPOINT, body)
317
+ end
318
+
82
319
  # ============================
83
- # Spot-specific info endpoints
320
+ # Info: Spot
84
321
  # ============================
85
322
 
86
323
  # Get spot metadata
@@ -122,4 +359,5 @@ module Hyperliquid
122
359
  @client.post(Constants::INFO_ENDPOINT, { type: 'tokenDetails', tokenId: token_id })
123
360
  end
124
361
  end
362
+ # rubocop:enable Metrics/ClassLength
125
363
  end
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hyperliquid
4
- VERSION = '0.2.0'
4
+ VERSION = '0.4.0'
5
5
  end
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 read-only access to Hyperliquid's decentralized exchange API
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
- SDK.new(testnet: testnet, timeout: timeout, retry_enabled: retry_enabled)
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
- def initialize(testnet: false, timeout: Constants::DEFAULT_TIMEOUT, retry_enabled: false)
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