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.
- checksums.yaml +4 -4
- data/.rubocop.yml +25 -2
- data/CHANGELOG.md +25 -5
- data/CLAUDE.md +202 -0
- data/README.md +410 -61
- 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/info.rb +257 -19
- 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
data/lib/hyperliquid/info.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
73
|
+
# Get order status by client order ID (cloid)
|
|
39
74
|
# @param user [String] Wallet address
|
|
40
|
-
# @
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
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
|
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
|