hyperliquid-rb 0.1.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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +460 -0
- data/lib/hyperliquid/cloid.rb +48 -0
- data/lib/hyperliquid/constants.rb +107 -0
- data/lib/hyperliquid/error.rb +27 -0
- data/lib/hyperliquid/exchange.rb +840 -0
- data/lib/hyperliquid/info.rb +486 -0
- data/lib/hyperliquid/signer.rb +147 -0
- data/lib/hyperliquid/transport.rb +69 -0
- data/lib/hyperliquid/utils.rb +78 -0
- data/lib/hyperliquid/version.rb +5 -0
- data/lib/hyperliquid/websocket_manager.rb +197 -0
- data/lib/hyperliquid.rb +15 -0
- metadata +114 -0
|
@@ -0,0 +1,840 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
|
|
6
|
+
module Hyperliquid
|
|
7
|
+
class Exchange
|
|
8
|
+
DEFAULT_SLIPPAGE = 0.05
|
|
9
|
+
|
|
10
|
+
attr_reader :info, :signer
|
|
11
|
+
attr_accessor :expires_after
|
|
12
|
+
|
|
13
|
+
# @param private_key [String] hex private key
|
|
14
|
+
# @param base_url [String] API URL (defaults to mainnet)
|
|
15
|
+
# @param vault_address [String, nil] default vault address for all operations
|
|
16
|
+
# @param account_address [String, nil] address to use for queries (e.g. when trading as agent)
|
|
17
|
+
def initialize(private_key:, base_url: MAINNET_URL, vault_address: nil, account_address: nil, skip_ws: false)
|
|
18
|
+
@signer = Signer.new(private_key: private_key, base_url: base_url)
|
|
19
|
+
@info = Info.new(base_url: base_url, skip_ws: skip_ws)
|
|
20
|
+
@transport = Transport.new(base_url: base_url)
|
|
21
|
+
@vault_address = vault_address
|
|
22
|
+
@account_address = account_address
|
|
23
|
+
@expires_after = nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def address
|
|
27
|
+
@signer.address
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Set expiration for subsequent actions (millisecond timestamp).
|
|
31
|
+
# Not supported on user-signed actions.
|
|
32
|
+
# @param expires_after [Integer, nil] timestamp in ms, or nil to clear
|
|
33
|
+
def set_expires_after(expires_after)
|
|
34
|
+
@expires_after = expires_after
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# ---- Orders ----
|
|
38
|
+
|
|
39
|
+
# Place a single order.
|
|
40
|
+
# @param coin [String] e.g. "ETH"
|
|
41
|
+
# @param is_buy [Boolean]
|
|
42
|
+
# @param sz [Float] order size
|
|
43
|
+
# @param limit_px [Float] limit price
|
|
44
|
+
# @param order_type [Hash] e.g. { limit: { "tif" => "Gtc" } } or { trigger: { ... } }
|
|
45
|
+
# @param reduce_only [Boolean]
|
|
46
|
+
# @param cloid [Cloid, nil] client order ID
|
|
47
|
+
# @param builder [Hash, nil] e.g. { "b" => "0x...", "f" => 10 }
|
|
48
|
+
# @param vault_address [String, nil] override default vault
|
|
49
|
+
def order(coin, is_buy:, sz:, limit_px:, order_type:, reduce_only: false, cloid: nil, builder: nil,
|
|
50
|
+
vault_address: nil)
|
|
51
|
+
order_request = {
|
|
52
|
+
coin: coin, is_buy: is_buy, sz: sz, limit_px: limit_px,
|
|
53
|
+
order_type: order_type, reduce_only: reduce_only, cloid: cloid
|
|
54
|
+
}
|
|
55
|
+
bulk_orders([order_request], builder: builder, vault_address: vault_address)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Place multiple orders at once.
|
|
59
|
+
# @param order_requests [Array<Hash>] array of order request hashes
|
|
60
|
+
# @param grouping [String] "na", "normalTpsl", or "positionTpsl"
|
|
61
|
+
# @param builder [Hash, nil]
|
|
62
|
+
# @param vault_address [String, nil]
|
|
63
|
+
def bulk_orders(order_requests, grouping: "na", builder: nil, vault_address: nil)
|
|
64
|
+
wires = order_requests.map do |req|
|
|
65
|
+
asset = @info.name_to_asset(req[:coin])
|
|
66
|
+
Utils.order_request_to_order_wire(req, asset)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
action = { "type" => "order", "orders" => wires, "grouping" => grouping }
|
|
70
|
+
action["builder"] = { "b" => builder["b"].downcase, "f" => builder["f"] } if builder
|
|
71
|
+
|
|
72
|
+
vault = vault_address || @vault_address
|
|
73
|
+
post_action(action, vault_address: vault)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Market open: buy/sell at market with slippage tolerance.
|
|
77
|
+
# @param coin [String]
|
|
78
|
+
# @param is_buy [Boolean]
|
|
79
|
+
# @param sz [Float] size
|
|
80
|
+
# @param px [Float, nil] reference price (default: mid price)
|
|
81
|
+
# @param slippage [Float] slippage as decimal (default 0.05 = 5%)
|
|
82
|
+
# @param cloid [Cloid, nil]
|
|
83
|
+
# @param builder [Hash, nil]
|
|
84
|
+
def market_open(coin, is_buy:, sz:, px: nil, slippage: DEFAULT_SLIPPAGE, cloid: nil, builder: nil)
|
|
85
|
+
px = slippage_price(coin, is_buy, slippage, px)
|
|
86
|
+
order(coin, is_buy: is_buy, sz: sz, limit_px: px,
|
|
87
|
+
order_type: { limit: { "tif" => "Ioc" } }, cloid: cloid, builder: builder)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Market close: close entire position (or partial) at market.
|
|
91
|
+
# @param coin [String]
|
|
92
|
+
# @param sz [Float, nil] size to close (default: entire position)
|
|
93
|
+
# @param px [Float, nil] reference price (default: mid price)
|
|
94
|
+
# @param slippage [Float]
|
|
95
|
+
# @param cloid [Cloid, nil]
|
|
96
|
+
# @param builder [Hash, nil]
|
|
97
|
+
def market_close(coin, sz: nil, px: nil, slippage: DEFAULT_SLIPPAGE, cloid: nil, builder: nil)
|
|
98
|
+
addr = @account_address || @vault_address || address
|
|
99
|
+
state = @info.user_state(addr)
|
|
100
|
+
position = state["assetPositions"]&.find { |p| p["position"]["coin"] == coin }
|
|
101
|
+
raise Error, "No open position for #{coin}" unless position
|
|
102
|
+
|
|
103
|
+
szi = position["position"]["szi"].to_f
|
|
104
|
+
is_buy = szi < 0 # close short = buy, close long = sell
|
|
105
|
+
sz ||= szi.abs
|
|
106
|
+
|
|
107
|
+
px = slippage_price(coin, is_buy, slippage, px)
|
|
108
|
+
order(coin, is_buy: is_buy, sz: sz, limit_px: px,
|
|
109
|
+
order_type: { limit: { "tif" => "Ioc" } }, reduce_only: true, cloid: cloid, builder: builder)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# ---- Modify Orders ----
|
|
113
|
+
|
|
114
|
+
# Modify a single order.
|
|
115
|
+
# @param oid [Integer, Cloid] order ID or client order ID to modify
|
|
116
|
+
# @param coin [String]
|
|
117
|
+
# @param is_buy [Boolean]
|
|
118
|
+
# @param sz [Float]
|
|
119
|
+
# @param limit_px [Float]
|
|
120
|
+
# @param order_type [Hash]
|
|
121
|
+
# @param reduce_only [Boolean]
|
|
122
|
+
# @param cloid [Cloid, nil]
|
|
123
|
+
def modify_order(oid, coin:, is_buy:, sz:, limit_px:, order_type:, reduce_only: false, cloid: nil)
|
|
124
|
+
order_request = {
|
|
125
|
+
coin: coin, is_buy: is_buy, sz: sz, limit_px: limit_px,
|
|
126
|
+
order_type: order_type, reduce_only: reduce_only, cloid: cloid
|
|
127
|
+
}
|
|
128
|
+
bulk_modify_orders([{ oid: oid, order: order_request }])
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Modify multiple orders at once.
|
|
132
|
+
# @param modifications [Array<Hash>] each with :oid and :order keys
|
|
133
|
+
def bulk_modify_orders(modifications)
|
|
134
|
+
wires = modifications.map do |mod|
|
|
135
|
+
asset = @info.name_to_asset(mod[:order][:coin])
|
|
136
|
+
oid_val = mod[:oid].is_a?(Cloid) ? mod[:oid].to_raw : mod[:oid]
|
|
137
|
+
{
|
|
138
|
+
"oid" => oid_val,
|
|
139
|
+
"order" => Utils.order_request_to_order_wire(mod[:order], asset)
|
|
140
|
+
}
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
action = { "type" => "batchModify", "modifies" => wires }
|
|
144
|
+
post_action(action)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# ---- Cancel Orders ----
|
|
148
|
+
|
|
149
|
+
# Cancel a single order by order ID.
|
|
150
|
+
# @param coin [String]
|
|
151
|
+
# @param oid [Integer]
|
|
152
|
+
def cancel(coin, oid)
|
|
153
|
+
bulk_cancel([{ coin: coin, oid: oid }])
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Cancel a single order by client order ID.
|
|
157
|
+
# @param coin [String]
|
|
158
|
+
# @param cloid [Cloid]
|
|
159
|
+
def cancel_by_cloid(coin, cloid)
|
|
160
|
+
bulk_cancel_by_cloid([{ coin: coin, cloid: cloid }])
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Cancel multiple orders by order ID.
|
|
164
|
+
# @param cancels [Array<Hash>] each with :coin and :oid keys
|
|
165
|
+
def bulk_cancel(cancels)
|
|
166
|
+
cancel_wires = cancels.map do |c|
|
|
167
|
+
asset = @info.name_to_asset(c[:coin])
|
|
168
|
+
{ "a" => asset, "o" => c[:oid] }
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
action = { "type" => "cancel", "cancels" => cancel_wires }
|
|
172
|
+
post_action(action)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Cancel multiple orders by client order ID.
|
|
176
|
+
# @param cancels [Array<Hash>] each with :coin and :cloid keys
|
|
177
|
+
def bulk_cancel_by_cloid(cancels)
|
|
178
|
+
cancel_wires = cancels.map do |c|
|
|
179
|
+
asset = @info.name_to_asset(c[:coin])
|
|
180
|
+
{ "asset" => asset, "cloid" => c[:cloid].to_s }
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
action = { "type" => "cancelByCloid", "cancels" => cancel_wires }
|
|
184
|
+
post_action(action)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Schedule cancel-all after a delay.
|
|
188
|
+
# @param time [Integer, nil] timestamp in ms when to cancel. nil to clear.
|
|
189
|
+
def schedule_cancel(time: nil)
|
|
190
|
+
action = { "type" => "scheduleCancel" }
|
|
191
|
+
action["time"] = time if time
|
|
192
|
+
post_action(action)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# ---- TWAP ----
|
|
196
|
+
|
|
197
|
+
# Place a TWAP order.
|
|
198
|
+
# @param coin [String]
|
|
199
|
+
# @param is_buy [Boolean]
|
|
200
|
+
# @param sz [Float]
|
|
201
|
+
# @param reduce_only [Boolean]
|
|
202
|
+
# @param minutes [Integer] duration
|
|
203
|
+
# @param randomize [Boolean] randomize execution
|
|
204
|
+
def twap_order(coin, is_buy:, sz:, minutes:, reduce_only: false, randomize: true)
|
|
205
|
+
asset = @info.name_to_asset(coin)
|
|
206
|
+
action = {
|
|
207
|
+
"type" => "twapOrder",
|
|
208
|
+
"twap" => {
|
|
209
|
+
"a" => asset,
|
|
210
|
+
"b" => is_buy,
|
|
211
|
+
"s" => Utils.float_to_wire(sz),
|
|
212
|
+
"r" => reduce_only,
|
|
213
|
+
"m" => minutes,
|
|
214
|
+
"t" => randomize
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
post_action(action)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Cancel a TWAP order.
|
|
221
|
+
# @param coin [String]
|
|
222
|
+
# @param twap_id [Integer]
|
|
223
|
+
def twap_cancel(coin, twap_id:)
|
|
224
|
+
asset = @info.name_to_asset(coin)
|
|
225
|
+
action = { "type" => "twapCancel", "a" => asset, "t" => twap_id }
|
|
226
|
+
post_action(action)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# ---- Account ----
|
|
230
|
+
|
|
231
|
+
# Update leverage for a coin.
|
|
232
|
+
# @param coin [String]
|
|
233
|
+
# @param leverage [Integer]
|
|
234
|
+
# @param is_cross [Boolean]
|
|
235
|
+
def update_leverage(coin, leverage:, is_cross: true)
|
|
236
|
+
asset = @info.name_to_asset(coin)
|
|
237
|
+
action = {
|
|
238
|
+
"type" => "updateLeverage",
|
|
239
|
+
"asset" => asset,
|
|
240
|
+
"isCross" => is_cross,
|
|
241
|
+
"leverage" => leverage
|
|
242
|
+
}
|
|
243
|
+
post_action(action)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Update isolated margin for a coin.
|
|
247
|
+
# @param coin [String]
|
|
248
|
+
# @param is_buy [Boolean]
|
|
249
|
+
# @param amount [Float] USD amount (positive = add, negative = remove)
|
|
250
|
+
def update_isolated_margin(coin, is_buy:, amount:)
|
|
251
|
+
asset = @info.name_to_asset(coin)
|
|
252
|
+
action = {
|
|
253
|
+
"type" => "updateIsolatedMargin",
|
|
254
|
+
"asset" => asset,
|
|
255
|
+
"isBuy" => is_buy,
|
|
256
|
+
"ntli" => Utils.float_to_usd_int(amount)
|
|
257
|
+
}
|
|
258
|
+
post_action(action)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Set referrer code.
|
|
262
|
+
# @param code [String] referral code
|
|
263
|
+
def set_referrer(code)
|
|
264
|
+
action = { "type" => "setReferrer", "code" => code }
|
|
265
|
+
post_action(action, vault_address: nil)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# ---- Transfers (user-signed actions) ----
|
|
269
|
+
|
|
270
|
+
# Transfer USD to another address.
|
|
271
|
+
# @param destination [String] recipient address
|
|
272
|
+
# @param amount [Float] USD amount
|
|
273
|
+
def usd_transfer(destination, amount:)
|
|
274
|
+
action = {
|
|
275
|
+
"type" => "usdSend",
|
|
276
|
+
"destination" => destination,
|
|
277
|
+
"amount" => amount.to_s,
|
|
278
|
+
"time" => timestamp_ms
|
|
279
|
+
}
|
|
280
|
+
post_user_signed_action(action, primary_type: "UsdSend")
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Transfer spot tokens to another address.
|
|
284
|
+
# @param destination [String] recipient address
|
|
285
|
+
# @param token [String] token identifier
|
|
286
|
+
# @param amount [Float]
|
|
287
|
+
def spot_transfer(destination, token:, amount:)
|
|
288
|
+
action = {
|
|
289
|
+
"type" => "spotSend",
|
|
290
|
+
"destination" => destination,
|
|
291
|
+
"token" => token,
|
|
292
|
+
"amount" => amount.to_s,
|
|
293
|
+
"time" => timestamp_ms
|
|
294
|
+
}
|
|
295
|
+
post_user_signed_action(action, primary_type: "SpotSend")
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
# Withdraw USDC from bridge to L1.
|
|
299
|
+
# @param destination [String] recipient address
|
|
300
|
+
# @param amount [Float] USDC amount
|
|
301
|
+
def withdraw_from_bridge(destination, amount:)
|
|
302
|
+
action = {
|
|
303
|
+
"type" => "withdraw3",
|
|
304
|
+
"destination" => destination,
|
|
305
|
+
"amount" => amount.to_s,
|
|
306
|
+
"time" => timestamp_ms
|
|
307
|
+
}
|
|
308
|
+
post_user_signed_action(action, primary_type: "Withdraw")
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Transfer between perp and spot.
|
|
312
|
+
# @param amount [Float] USD amount
|
|
313
|
+
# @param to_perp [Boolean] true = spot->perp, false = perp->spot
|
|
314
|
+
def usd_class_transfer(amount:, to_perp:)
|
|
315
|
+
ts = timestamp_ms
|
|
316
|
+
str_amount = amount.to_s
|
|
317
|
+
str_amount += " subaccount:#{@vault_address}" if @vault_address
|
|
318
|
+
|
|
319
|
+
action = {
|
|
320
|
+
"type" => "usdClassTransfer",
|
|
321
|
+
"amount" => str_amount,
|
|
322
|
+
"toPerp" => to_perp,
|
|
323
|
+
"nonce" => ts
|
|
324
|
+
}
|
|
325
|
+
post_user_signed_action(action, primary_type: "UsdClassTransfer")
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# Send asset between dexes.
|
|
329
|
+
# @param destination [String] recipient address
|
|
330
|
+
# @param source_dex [String] source dex identifier
|
|
331
|
+
# @param destination_dex [String] destination dex identifier
|
|
332
|
+
# @param token [String] token identifier
|
|
333
|
+
# @param amount [Float] amount to send
|
|
334
|
+
def send_asset(destination, source_dex:, destination_dex:, token:, amount:)
|
|
335
|
+
ts = timestamp_ms
|
|
336
|
+
action = {
|
|
337
|
+
"type" => "sendAsset",
|
|
338
|
+
"destination" => destination,
|
|
339
|
+
"sourceDex" => source_dex,
|
|
340
|
+
"destinationDex" => destination_dex,
|
|
341
|
+
"token" => token,
|
|
342
|
+
"amount" => amount.to_s,
|
|
343
|
+
"fromSubAccount" => @vault_address || "",
|
|
344
|
+
"nonce" => ts
|
|
345
|
+
}
|
|
346
|
+
post_user_signed_action(action, primary_type: "SendAsset")
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# ---- Agent / Builder ----
|
|
350
|
+
|
|
351
|
+
# Approve an agent to trade on your behalf.
|
|
352
|
+
# Generates a new agent key and returns [response, agent_private_key].
|
|
353
|
+
# @param name [String, nil] agent name
|
|
354
|
+
# @return [Array] [response, agent_private_key_hex]
|
|
355
|
+
def approve_agent(name: nil)
|
|
356
|
+
agent_key = "0x#{SecureRandom.hex(32)}"
|
|
357
|
+
agent_account = Eth::Key.new(priv: agent_key.delete_prefix("0x"))
|
|
358
|
+
ts = timestamp_ms
|
|
359
|
+
|
|
360
|
+
action = {
|
|
361
|
+
"type" => "approveAgent",
|
|
362
|
+
"agentAddress" => agent_account.address.to_s,
|
|
363
|
+
"agentName" => name || "",
|
|
364
|
+
"nonce" => ts
|
|
365
|
+
}
|
|
366
|
+
sig = @signer.sign_user_signed_action(
|
|
367
|
+
action,
|
|
368
|
+
primary_type: "ApproveAgent",
|
|
369
|
+
payload_types: USER_SIGNED_TYPES["ApproveAgent"]
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
# Remove agentName from action if no name provided (matches Python SDK behavior)
|
|
373
|
+
action.delete("agentName") if name.nil?
|
|
374
|
+
|
|
375
|
+
payload = { action: action, nonce: ts, signature: sig }
|
|
376
|
+
result = @transport.post_exchange(payload)
|
|
377
|
+
|
|
378
|
+
[result, agent_key]
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
# Approve a builder fee.
|
|
382
|
+
# @param builder [String] builder address
|
|
383
|
+
# @param max_fee_rate [String] max fee rate
|
|
384
|
+
def approve_builder_fee(builder:, max_fee_rate:)
|
|
385
|
+
action = {
|
|
386
|
+
"type" => "approveBuilderFee",
|
|
387
|
+
"maxFeeRate" => max_fee_rate,
|
|
388
|
+
"builder" => builder,
|
|
389
|
+
"nonce" => timestamp_ms
|
|
390
|
+
}
|
|
391
|
+
post_user_signed_action(action, primary_type: "ApproveBuilderFee")
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
# ---- Sub-accounts ----
|
|
395
|
+
|
|
396
|
+
# Create a sub-account.
|
|
397
|
+
# @param name [String] sub-account name
|
|
398
|
+
def create_sub_account(name:)
|
|
399
|
+
action = { "type" => "createSubAccount", "name" => name }
|
|
400
|
+
post_action(action, vault_address: nil)
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# Transfer USD to/from a sub-account.
|
|
404
|
+
# @param sub_account_user [String] sub-account address
|
|
405
|
+
# @param is_deposit [Boolean] true = deposit into sub, false = withdraw
|
|
406
|
+
# @param usd [Integer] USD amount (raw integer)
|
|
407
|
+
def sub_account_transfer(sub_account_user:, is_deposit:, usd:)
|
|
408
|
+
action = {
|
|
409
|
+
"type" => "subAccountTransfer",
|
|
410
|
+
"subAccountUser" => sub_account_user,
|
|
411
|
+
"isDeposit" => is_deposit,
|
|
412
|
+
"usd" => usd
|
|
413
|
+
}
|
|
414
|
+
post_action(action, vault_address: nil)
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
# Transfer spot tokens to/from a sub-account.
|
|
418
|
+
# @param sub_account_user [String]
|
|
419
|
+
# @param is_deposit [Boolean]
|
|
420
|
+
# @param token [String]
|
|
421
|
+
# @param amount [Float]
|
|
422
|
+
def sub_account_spot_transfer(sub_account_user:, is_deposit:, token:, amount:)
|
|
423
|
+
action = {
|
|
424
|
+
"type" => "subAccountSpotTransfer",
|
|
425
|
+
"subAccountUser" => sub_account_user,
|
|
426
|
+
"isDeposit" => is_deposit,
|
|
427
|
+
"token" => token,
|
|
428
|
+
"amount" => amount.to_s
|
|
429
|
+
}
|
|
430
|
+
post_action(action, vault_address: nil)
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
# ---- Vault ----
|
|
434
|
+
|
|
435
|
+
# Transfer USD to/from a vault.
|
|
436
|
+
# @param vault_address [String] vault address
|
|
437
|
+
# @param is_deposit [Boolean]
|
|
438
|
+
# @param usd [Integer] amount
|
|
439
|
+
def vault_usd_transfer(vault_address:, is_deposit:, usd:)
|
|
440
|
+
action = {
|
|
441
|
+
"type" => "vaultTransfer",
|
|
442
|
+
"vaultAddress" => vault_address,
|
|
443
|
+
"isDeposit" => is_deposit,
|
|
444
|
+
"usd" => usd
|
|
445
|
+
}
|
|
446
|
+
post_action(action, vault_address: nil)
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
# ---- Staking ----
|
|
450
|
+
|
|
451
|
+
# Delegate or undelegate tokens.
|
|
452
|
+
# @param validator [String] validator address
|
|
453
|
+
# @param wei [Integer] amount in wei
|
|
454
|
+
# @param is_undelegate [Boolean]
|
|
455
|
+
def token_delegate(validator:, wei:, is_undelegate: false)
|
|
456
|
+
action = {
|
|
457
|
+
"type" => "tokenDelegate",
|
|
458
|
+
"validator" => validator,
|
|
459
|
+
"wei" => wei,
|
|
460
|
+
"isUndelegate" => is_undelegate,
|
|
461
|
+
"nonce" => timestamp_ms
|
|
462
|
+
}
|
|
463
|
+
post_user_signed_action(action, primary_type: "TokenDelegate")
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
# ---- Multi-sig ----
|
|
467
|
+
|
|
468
|
+
# Convert account to multi-sig.
|
|
469
|
+
# @param authorized_users [Array<String>] list of authorized signer addresses
|
|
470
|
+
# @param threshold [Integer] number of signatures required
|
|
471
|
+
def convert_to_multi_sig_user(authorized_users:, threshold:)
|
|
472
|
+
sorted_users = authorized_users.sort
|
|
473
|
+
signers = { "authorizedUsers" => sorted_users, "threshold" => threshold }
|
|
474
|
+
action = {
|
|
475
|
+
"type" => "convertToMultiSigUser",
|
|
476
|
+
"signers" => JSON.generate(signers),
|
|
477
|
+
"nonce" => timestamp_ms
|
|
478
|
+
}
|
|
479
|
+
post_user_signed_action(action, primary_type: "ConvertToMultiSigUser")
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
# Execute a multi-sig action.
|
|
483
|
+
# @param multi_sig_user [String] multi-sig user address
|
|
484
|
+
# @param inner_action [Hash] the action to execute
|
|
485
|
+
# @param signatures [Array] existing signatures
|
|
486
|
+
# @param nonce [Integer] nonce
|
|
487
|
+
# @param vault_address [String, nil]
|
|
488
|
+
def multi_sig(multi_sig_user, inner_action, signatures, nonce, vault_address: nil)
|
|
489
|
+
multi_sig_action = {
|
|
490
|
+
"type" => "multiSig",
|
|
491
|
+
"signatureChainId" => "0x66eee",
|
|
492
|
+
"signatures" => signatures,
|
|
493
|
+
"payload" => {
|
|
494
|
+
"multiSigUser" => multi_sig_user.downcase,
|
|
495
|
+
"outerSigner" => @signer.address.downcase,
|
|
496
|
+
"action" => inner_action
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
sig = @signer.sign_multi_sig_action(multi_sig_action, vault_address: vault_address, nonce: nonce,
|
|
501
|
+
expires_after: @expires_after)
|
|
502
|
+
|
|
503
|
+
payload = {
|
|
504
|
+
action: multi_sig_action,
|
|
505
|
+
nonce: nonce,
|
|
506
|
+
signature: sig,
|
|
507
|
+
vaultAddress: vault_address,
|
|
508
|
+
expiresAfter: @expires_after
|
|
509
|
+
}
|
|
510
|
+
@transport.post_exchange(payload)
|
|
511
|
+
end
|
|
512
|
+
|
|
513
|
+
# ---- Spot Deploy ----
|
|
514
|
+
|
|
515
|
+
# Register a new spot token.
|
|
516
|
+
def spot_deploy_register_token(token_name:, sz_decimals:, wei_decimals:, max_gas:, full_name:)
|
|
517
|
+
action = {
|
|
518
|
+
"type" => "spotDeploy",
|
|
519
|
+
"registerToken2" => {
|
|
520
|
+
"spec" => { "name" => token_name, "szDecimals" => sz_decimals, "weiDecimals" => wei_decimals },
|
|
521
|
+
"maxGas" => max_gas,
|
|
522
|
+
"fullName" => full_name
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
post_action(action, vault_address: nil)
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
# User genesis for a spot token.
|
|
529
|
+
def spot_deploy_user_genesis(token:, user_and_wei:, existing_token_and_wei:)
|
|
530
|
+
action = {
|
|
531
|
+
"type" => "spotDeploy",
|
|
532
|
+
"userGenesis" => {
|
|
533
|
+
"token" => token,
|
|
534
|
+
"userAndWei" => user_and_wei.map { |user, wei| [user.downcase, wei] },
|
|
535
|
+
"existingTokenAndWei" => existing_token_and_wei
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
post_action(action, vault_address: nil)
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
# Enable freeze privilege for a spot token.
|
|
542
|
+
def spot_deploy_enable_freeze_privilege(token)
|
|
543
|
+
spot_deploy_token_action("enableFreezePrivilege", token)
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
# Freeze/unfreeze a user for a spot token.
|
|
547
|
+
def spot_deploy_freeze_user(token, user:, freeze:)
|
|
548
|
+
action = {
|
|
549
|
+
"type" => "spotDeploy",
|
|
550
|
+
"freezeUser" => { "token" => token, "user" => user.downcase, "freeze" => freeze }
|
|
551
|
+
}
|
|
552
|
+
post_action(action, vault_address: nil)
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
# Revoke freeze privilege for a spot token.
|
|
556
|
+
def spot_deploy_revoke_freeze_privilege(token)
|
|
557
|
+
spot_deploy_token_action("revokeFreezePrivilege", token)
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
# Enable quote token for a spot token.
|
|
561
|
+
def spot_deploy_enable_quote_token(token)
|
|
562
|
+
spot_deploy_token_action("enableQuoteToken", token)
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
# Run genesis for a spot token.
|
|
566
|
+
def spot_deploy_genesis(token, max_supply:, no_hyperliquidity: false)
|
|
567
|
+
genesis = { "token" => token, "maxSupply" => max_supply }
|
|
568
|
+
genesis["noHyperliquidity"] = true if no_hyperliquidity
|
|
569
|
+
action = { "type" => "spotDeploy", "genesis" => genesis }
|
|
570
|
+
post_action(action, vault_address: nil)
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
# Register a spot trading pair.
|
|
574
|
+
def spot_deploy_register_spot(base_token:, quote_token:)
|
|
575
|
+
action = {
|
|
576
|
+
"type" => "spotDeploy",
|
|
577
|
+
"registerSpot" => { "tokens" => [base_token, quote_token] }
|
|
578
|
+
}
|
|
579
|
+
post_action(action, vault_address: nil)
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
# Register hyperliquidity for a spot pair.
|
|
583
|
+
def spot_deploy_register_hyperliquidity(spot, start_px:, order_sz:, n_orders:, n_seeded_levels: nil)
|
|
584
|
+
register = {
|
|
585
|
+
"spot" => spot,
|
|
586
|
+
"startPx" => start_px.to_s,
|
|
587
|
+
"orderSz" => order_sz.to_s,
|
|
588
|
+
"nOrders" => n_orders
|
|
589
|
+
}
|
|
590
|
+
register["nSeededLevels"] = n_seeded_levels if n_seeded_levels
|
|
591
|
+
action = { "type" => "spotDeploy", "registerHyperliquidity" => register }
|
|
592
|
+
post_action(action, vault_address: nil)
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
# Set deployer trading fee share for a spot token.
|
|
596
|
+
def spot_deploy_set_deployer_trading_fee_share(token, share:)
|
|
597
|
+
action = {
|
|
598
|
+
"type" => "spotDeploy",
|
|
599
|
+
"setDeployerTradingFeeShare" => { "token" => token, "share" => share }
|
|
600
|
+
}
|
|
601
|
+
post_action(action, vault_address: nil)
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
# ---- Perp Deploy ----
|
|
605
|
+
|
|
606
|
+
# Register a new perp asset.
|
|
607
|
+
def perp_deploy_register_asset(dex:, coin:, sz_decimals:, oracle_px:, margin_table_id:,
|
|
608
|
+
only_isolated:, max_gas: nil, schema: nil)
|
|
609
|
+
schema_wire = nil
|
|
610
|
+
if schema
|
|
611
|
+
schema_wire = {
|
|
612
|
+
"fullName" => schema[:full_name],
|
|
613
|
+
"collateralToken" => schema[:collateral_token],
|
|
614
|
+
"oracleUpdater" => schema[:oracle_updater]&.downcase
|
|
615
|
+
}
|
|
616
|
+
end
|
|
617
|
+
action = {
|
|
618
|
+
"type" => "perpDeploy",
|
|
619
|
+
"registerAsset" => {
|
|
620
|
+
"maxGas" => max_gas,
|
|
621
|
+
"assetRequest" => {
|
|
622
|
+
"coin" => coin,
|
|
623
|
+
"szDecimals" => sz_decimals,
|
|
624
|
+
"oraclePx" => oracle_px,
|
|
625
|
+
"marginTableId" => margin_table_id,
|
|
626
|
+
"onlyIsolated" => only_isolated
|
|
627
|
+
},
|
|
628
|
+
"dex" => dex,
|
|
629
|
+
"schema" => schema_wire
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
post_action(action, vault_address: nil)
|
|
633
|
+
end
|
|
634
|
+
|
|
635
|
+
# Set oracle prices for a perp dex.
|
|
636
|
+
def perp_deploy_set_oracle(dex:, oracle_pxs:, all_mark_pxs:, external_perp_pxs:)
|
|
637
|
+
action = {
|
|
638
|
+
"type" => "perpDeploy",
|
|
639
|
+
"setOracle" => {
|
|
640
|
+
"dex" => dex,
|
|
641
|
+
"oraclePxs" => oracle_pxs.sort.to_a,
|
|
642
|
+
"markPxs" => all_mark_pxs.map { |m| m.sort.to_a },
|
|
643
|
+
"externalPerpPxs" => external_perp_pxs.sort.to_a
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
post_action(action, vault_address: nil)
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
# ---- C-Signer ----
|
|
650
|
+
|
|
651
|
+
# Unjail self as c-signer.
|
|
652
|
+
def c_signer_unjail_self
|
|
653
|
+
c_signer_action("unjailSelf")
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
# Jail self as c-signer.
|
|
657
|
+
def c_signer_jail_self
|
|
658
|
+
c_signer_action("jailSelf")
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
# ---- C-Validator ----
|
|
662
|
+
|
|
663
|
+
# Register as a validator.
|
|
664
|
+
def c_validator_register(node_ip:, name:, description:, delegations_disabled:, commission_bps:,
|
|
665
|
+
signer:, unjailed:, initial_wei:)
|
|
666
|
+
action = {
|
|
667
|
+
"type" => "CValidatorAction",
|
|
668
|
+
"register" => {
|
|
669
|
+
"profile" => {
|
|
670
|
+
"node_ip" => { "Ip" => node_ip },
|
|
671
|
+
"name" => name,
|
|
672
|
+
"description" => description,
|
|
673
|
+
"delegations_disabled" => delegations_disabled,
|
|
674
|
+
"commission_bps" => commission_bps,
|
|
675
|
+
"signer" => signer
|
|
676
|
+
},
|
|
677
|
+
"unjailed" => unjailed,
|
|
678
|
+
"initial_wei" => initial_wei
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
post_action(action, vault_address: nil)
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
# Change validator profile.
|
|
685
|
+
def c_validator_change_profile(unjailed:, node_ip: nil, name: nil, description: nil,
|
|
686
|
+
disable_delegations: nil, commission_bps: nil, signer: nil)
|
|
687
|
+
action = {
|
|
688
|
+
"type" => "CValidatorAction",
|
|
689
|
+
"changeProfile" => {
|
|
690
|
+
"node_ip" => node_ip ? { "Ip" => node_ip } : nil,
|
|
691
|
+
"name" => name,
|
|
692
|
+
"description" => description,
|
|
693
|
+
"unjailed" => unjailed,
|
|
694
|
+
"disable_delegations" => disable_delegations,
|
|
695
|
+
"commission_bps" => commission_bps,
|
|
696
|
+
"signer" => signer
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
post_action(action, vault_address: nil)
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
# Unregister as a validator.
|
|
703
|
+
def c_validator_unregister
|
|
704
|
+
action = { "type" => "CValidatorAction", "unregister" => nil }
|
|
705
|
+
post_action(action, vault_address: nil)
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
# ---- EVM / Blocks ----
|
|
709
|
+
|
|
710
|
+
# Enable or disable big blocks.
|
|
711
|
+
# @param enable [Boolean]
|
|
712
|
+
def use_big_blocks(enable)
|
|
713
|
+
action = { "type" => "evmUserModify", "usingBigBlocks" => enable }
|
|
714
|
+
post_action(action, vault_address: nil)
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
# ---- Dex Abstraction ----
|
|
718
|
+
|
|
719
|
+
# Enable dex abstraction for an agent.
|
|
720
|
+
def agent_enable_dex_abstraction
|
|
721
|
+
action = { "type" => "agentEnableDexAbstraction" }
|
|
722
|
+
post_action(action)
|
|
723
|
+
end
|
|
724
|
+
|
|
725
|
+
# Set abstraction level for an agent.
|
|
726
|
+
# @param abstraction [String] "u", "p", or "i"
|
|
727
|
+
def agent_set_abstraction(abstraction)
|
|
728
|
+
action = { "type" => "agentSetAbstraction", "abstraction" => abstraction }
|
|
729
|
+
post_action(action)
|
|
730
|
+
end
|
|
731
|
+
|
|
732
|
+
# Set dex abstraction for a user (user-signed action).
|
|
733
|
+
# @param user [String] user address
|
|
734
|
+
# @param enabled [Boolean]
|
|
735
|
+
def user_dex_abstraction(user, enabled:)
|
|
736
|
+
action = {
|
|
737
|
+
"type" => "userDexAbstraction",
|
|
738
|
+
"user" => user.downcase,
|
|
739
|
+
"enabled" => enabled,
|
|
740
|
+
"nonce" => timestamp_ms
|
|
741
|
+
}
|
|
742
|
+
post_user_signed_action(action, primary_type: "UserDexAbstraction")
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
# Set user abstraction level (user-signed action).
|
|
746
|
+
# @param user [String] user address
|
|
747
|
+
# @param abstraction [String] "unifiedAccount", "portfolioMargin", or "disabled"
|
|
748
|
+
def user_set_abstraction(user, abstraction:)
|
|
749
|
+
action = {
|
|
750
|
+
"type" => "userSetAbstraction",
|
|
751
|
+
"user" => user.downcase,
|
|
752
|
+
"abstraction" => abstraction,
|
|
753
|
+
"nonce" => timestamp_ms
|
|
754
|
+
}
|
|
755
|
+
post_user_signed_action(action, primary_type: "UserSetAbstraction")
|
|
756
|
+
end
|
|
757
|
+
|
|
758
|
+
# ---- Noop ----
|
|
759
|
+
|
|
760
|
+
# Send a no-op action (useful for testing signing).
|
|
761
|
+
# @param nonce [Integer]
|
|
762
|
+
def noop(nonce)
|
|
763
|
+
action = { "type" => "noop" }
|
|
764
|
+
post_action(action, nonce: nonce)
|
|
765
|
+
end
|
|
766
|
+
|
|
767
|
+
private
|
|
768
|
+
|
|
769
|
+
def post_action(action, vault_address: :default, nonce: nil)
|
|
770
|
+
vault = vault_address == :default ? @vault_address : vault_address
|
|
771
|
+
# usdClassTransfer and sendAsset don't include vaultAddress
|
|
772
|
+
vault = nil if %w[usdClassTransfer sendAsset].include?(action["type"])
|
|
773
|
+
|
|
774
|
+
nonce ||= timestamp_ms
|
|
775
|
+
sig = @signer.sign_l1_action(action, nonce: nonce, vault_address: vault, expires_after: @expires_after)
|
|
776
|
+
|
|
777
|
+
payload = { action: action, nonce: nonce, signature: sig }
|
|
778
|
+
payload[:vaultAddress] = vault if vault
|
|
779
|
+
payload[:expiresAfter] = @expires_after if @expires_after
|
|
780
|
+
|
|
781
|
+
@transport.post_exchange(payload)
|
|
782
|
+
end
|
|
783
|
+
|
|
784
|
+
def post_user_signed_action(action, primary_type:)
|
|
785
|
+
payload_types = USER_SIGNED_TYPES[primary_type]
|
|
786
|
+
raise Error, "Unknown user-signed action type: #{primary_type}" unless payload_types
|
|
787
|
+
|
|
788
|
+
sig = @signer.sign_user_signed_action(
|
|
789
|
+
action,
|
|
790
|
+
primary_type: primary_type,
|
|
791
|
+
payload_types: payload_types
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
payload = { action: action, nonce: timestamp_ms, signature: sig }
|
|
795
|
+
@transport.post_exchange(payload)
|
|
796
|
+
end
|
|
797
|
+
|
|
798
|
+
def slippage_price(coin, is_buy, slippage, px = nil)
|
|
799
|
+
unless px
|
|
800
|
+
full_coin = @info.name_to_coin(coin)
|
|
801
|
+
dex = full_coin.include?(":") ? full_coin.split(":")[0] : ""
|
|
802
|
+
mids = @info.all_mids(dex: dex)
|
|
803
|
+
mid = mids[full_coin]
|
|
804
|
+
raise Error, "No mid price for #{coin}" unless mid
|
|
805
|
+
|
|
806
|
+
px = mid.to_f
|
|
807
|
+
end
|
|
808
|
+
|
|
809
|
+
asset = @info.name_to_asset(coin)
|
|
810
|
+
is_spot = asset >= 10_000
|
|
811
|
+
|
|
812
|
+
px = if is_buy
|
|
813
|
+
px * (1 + slippage)
|
|
814
|
+
else
|
|
815
|
+
[px * (1 - slippage), 0].max
|
|
816
|
+
end
|
|
817
|
+
|
|
818
|
+
# Round to 5 significant figures, with appropriate decimal places
|
|
819
|
+
sz_decimals = @info.asset_to_sz_decimals(asset) || 0
|
|
820
|
+
max_decimals = is_spot ? 8 : 6
|
|
821
|
+
decimal_places = [max_decimals - sz_decimals, 0].max
|
|
822
|
+
rounded = format("%.5g", px).to_f
|
|
823
|
+
rounded.round(decimal_places)
|
|
824
|
+
end
|
|
825
|
+
|
|
826
|
+
def spot_deploy_token_action(variant, token)
|
|
827
|
+
action = { "type" => "spotDeploy", variant => { "token" => token } }
|
|
828
|
+
post_action(action, vault_address: nil)
|
|
829
|
+
end
|
|
830
|
+
|
|
831
|
+
def c_signer_action(variant)
|
|
832
|
+
action = { "type" => "CSignerAction", variant => nil }
|
|
833
|
+
post_action(action, vault_address: nil)
|
|
834
|
+
end
|
|
835
|
+
|
|
836
|
+
def timestamp_ms
|
|
837
|
+
(Time.now.to_f * 1000).to_i
|
|
838
|
+
end
|
|
839
|
+
end
|
|
840
|
+
end
|