hyperliquid 0.6.0 → 1.0.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.
@@ -0,0 +1,340 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ws_lite'
4
+ require 'json'
5
+
6
+ module Hyperliquid
7
+ module WS
8
+ # Managed WebSocket client for subscribing to real-time data channels
9
+ class Client
10
+ attr_reader :dropped_message_count
11
+
12
+ def initialize(testnet: false, max_queue_size: Constants::WS_MAX_QUEUE_SIZE, reconnect: true)
13
+ base_url = testnet ? Constants::TESTNET_API_URL : Constants::MAINNET_API_URL
14
+ @url = base_url.sub(%r{^https?://}, 'wss://') + Constants::WS_ENDPOINT
15
+ @max_queue_size = max_queue_size
16
+ @reconnect_enabled = reconnect
17
+
18
+ @subscriptions = {} # identifier => [{ id:, callback: }]
19
+ @subscription_msgs = {} # subscription_id => { subscription:, identifier: }
20
+ @next_id = 0
21
+ @mutex = Mutex.new
22
+ @queue = Queue.new
23
+ @dropped_message_count = 0
24
+
25
+ @ws = nil
26
+ @connected = false
27
+ @closing = false
28
+ @dispatch_thread = nil
29
+ @ping_thread = nil
30
+ @pending_subscriptions = []
31
+
32
+ @lifecycle_callbacks = {}
33
+ @reconnect_attempts = 0
34
+ @connection_id = 0
35
+ end
36
+
37
+ def connect
38
+ @closing = false
39
+ @reconnect_attempts = 0
40
+ establish_connection
41
+ start_dispatch_thread
42
+ start_ping_thread
43
+ self
44
+ end
45
+
46
+ def subscribe(subscription, &callback)
47
+ raise ArgumentError, 'Block required for subscribe' unless block_given?
48
+
49
+ identifier = subscription_identifier(subscription)
50
+ sub_id = nil
51
+
52
+ @mutex.synchronize do
53
+ sub_id = @next_id
54
+ @next_id += 1
55
+
56
+ @subscriptions[identifier] ||= []
57
+ @subscriptions[identifier] << { id: sub_id, callback: callback }
58
+ @subscription_msgs[sub_id] = { subscription: subscription, identifier: identifier }
59
+ end
60
+
61
+ if @connected
62
+ send_subscribe(subscription)
63
+ else
64
+ @mutex.synchronize { @pending_subscriptions << subscription }
65
+ connect unless @ws
66
+ end
67
+
68
+ sub_id
69
+ end
70
+
71
+ def unsubscribe(subscription_id)
72
+ sub_msg = nil
73
+ should_send = false
74
+
75
+ @mutex.synchronize do
76
+ sub_msg = @subscription_msgs.delete(subscription_id)
77
+ return unless sub_msg
78
+
79
+ identifier = sub_msg[:identifier]
80
+ callbacks = @subscriptions[identifier]
81
+ return unless callbacks
82
+
83
+ callbacks.reject! { |entry| entry[:id] == subscription_id }
84
+
85
+ if callbacks.empty?
86
+ @subscriptions.delete(identifier)
87
+ should_send = true
88
+ end
89
+ end
90
+
91
+ send_unsubscribe(sub_msg[:subscription]) if should_send && @connected
92
+ end
93
+
94
+ def close
95
+ @closing = true
96
+ @connected = false
97
+
98
+ @ping_thread&.kill
99
+ @ping_thread = nil
100
+
101
+ @queue&.close if @queue.respond_to?(:close)
102
+ @dispatch_thread&.join(5)
103
+ @dispatch_thread = nil
104
+
105
+ @ws&.close
106
+ @ws = nil
107
+ end
108
+
109
+ def connected?
110
+ @connected
111
+ end
112
+
113
+ def on(event, &callback)
114
+ @lifecycle_callbacks[event] = callback
115
+ end
116
+
117
+ private
118
+
119
+ def establish_connection
120
+ client = self
121
+ url = @url
122
+ @connection_id += 1
123
+ active_id = @connection_id
124
+
125
+ @ws = ::WSLite.connect(url) do |ws|
126
+ ws.on :open do
127
+ next if client.send(:stale_connection?, active_id)
128
+
129
+ client.send(:handle_open)
130
+ end
131
+
132
+ ws.on :message do |msg|
133
+ next if client.send(:stale_connection?, active_id)
134
+
135
+ client.send(:handle_message, msg.data)
136
+ end
137
+
138
+ ws.on :error do |e|
139
+ next if client.send(:stale_connection?, active_id)
140
+
141
+ client.send(:handle_error, e)
142
+ end
143
+
144
+ ws.on :close do |e|
145
+ next if client.send(:stale_connection?, active_id)
146
+
147
+ client.send(:handle_close, e)
148
+ end
149
+ end
150
+ end
151
+
152
+ def stale_connection?(id)
153
+ id != @connection_id
154
+ end
155
+
156
+ def handle_open
157
+ @connected = true
158
+ @reconnect_attempts = 0
159
+ flush_pending_subscriptions
160
+ replay_subscriptions
161
+ @lifecycle_callbacks[:open]&.call
162
+ end
163
+
164
+ def handle_message(raw)
165
+ return if raw.nil? || raw.empty?
166
+
167
+ return if raw.start_with?('Websocket connection established')
168
+
169
+ data = parse_json(raw)
170
+ return unless data
171
+
172
+ channel = data['channel']
173
+ return if channel == 'pong'
174
+ return unless channel
175
+
176
+ identifier = compute_identifier(channel, data['data'])
177
+ return unless identifier
178
+
179
+ enqueue_message(identifier, data['data'])
180
+ end
181
+
182
+ def handle_error(error)
183
+ @lifecycle_callbacks[:error]&.call(error)
184
+ end
185
+
186
+ def handle_close(_event)
187
+ was_connected = @connected
188
+ @connected = false
189
+ @lifecycle_callbacks[:close]&.call
190
+
191
+ attempt_reconnect if was_connected && !@closing && @reconnect_enabled
192
+ end
193
+
194
+ def parse_json(raw)
195
+ JSON.parse(raw)
196
+ rescue JSON::ParserError => e
197
+ warn "[Hyperliquid::WS] Failed to parse message: #{e.message}"
198
+ nil
199
+ end
200
+
201
+ def compute_identifier(channel, data)
202
+ case channel
203
+ when 'l2Book' then "l2Book:#{data['coin'].downcase}"
204
+ when 'trades' then data.is_a?(Array) && data[0] ? "trades:#{data[0]['coin'].downcase}" : nil
205
+ when 'bbo' then "bbo:#{data['coin'].downcase}"
206
+ when 'candle' then "candle:#{data['s'].downcase}:#{data['i']}"
207
+ when 'allMids' then 'allMids'
208
+ when 'orderUpdates' then 'orderUpdates'
209
+ when 'userEvents' then "userEvents:#{data['user'].downcase}"
210
+ when 'userFills' then "userFills:#{data['user'].downcase}"
211
+ when 'userFundings' then "userFundings:#{data['user'].downcase}"
212
+ end
213
+ end
214
+
215
+ def subscription_identifier(subscription)
216
+ type = sub_field(subscription, 'type')
217
+ case type
218
+ when 'l2Book' then "l2Book:#{sub_field(subscription, 'coin').downcase}"
219
+ when 'trades' then "trades:#{sub_field(subscription, 'coin').downcase}"
220
+ when 'bbo' then "bbo:#{sub_field(subscription, 'coin').downcase}"
221
+ when 'candle'
222
+ "candle:#{sub_field(subscription, 'coin').downcase}:#{sub_field(subscription, 'interval')}"
223
+ when 'allMids' then 'allMids'
224
+ when 'orderUpdates' then 'orderUpdates'
225
+ when 'userEvents' then "userEvents:#{sub_field(subscription, 'user').downcase}"
226
+ when 'userFills' then "userFills:#{sub_field(subscription, 'user').downcase}"
227
+ when 'userFundings' then "userFundings:#{sub_field(subscription, 'user').downcase}"
228
+ else
229
+ raise Hyperliquid::WebSocketError, "Unsupported subscription type: #{type}"
230
+ end
231
+ end
232
+
233
+ def sub_field(subscription, key)
234
+ subscription[key.to_sym] || subscription[key]
235
+ end
236
+
237
+ def enqueue_message(identifier, data)
238
+ @mutex.synchronize do
239
+ if @queue.size >= @max_queue_size
240
+ @dropped_message_count += 1
241
+ if @dropped_message_count == 1 || (@dropped_message_count % 100).zero?
242
+ warn "[Hyperliquid::WS] Queue full (#{@max_queue_size}). " \
243
+ "Dropped #{@dropped_message_count} message(s). Callbacks may be too slow."
244
+ end
245
+ return
246
+ end
247
+ end
248
+ @queue.push({ identifier: identifier, data: data })
249
+ end
250
+
251
+ def start_dispatch_thread
252
+ @dispatch_thread = Thread.new do
253
+ loop do
254
+ msg = begin
255
+ @queue.pop
256
+ rescue ClosedQueueError
257
+ break
258
+ end
259
+ break if msg.nil?
260
+
261
+ callbacks = @mutex.synchronize { @subscriptions[msg[:identifier]]&.dup }
262
+ next unless callbacks
263
+
264
+ callbacks.each do |entry|
265
+ entry[:callback].call(msg[:data])
266
+ rescue StandardError => e
267
+ warn "[Hyperliquid::WS] Callback error: #{e.message}"
268
+ end
269
+ end
270
+ end
271
+ @dispatch_thread.name = 'hl-ws-dispatch'
272
+ end
273
+
274
+ def start_ping_thread
275
+ @ping_thread = Thread.new do
276
+ loop do
277
+ sleep Constants::WS_PING_INTERVAL
278
+ break if @closing
279
+
280
+ send_json({ method: 'ping' }) if @connected
281
+ end
282
+ end
283
+ @ping_thread.name = 'hl-ws-ping'
284
+ @ping_thread.report_on_exception = false
285
+ end
286
+
287
+ def flush_pending_subscriptions
288
+ pending = @mutex.synchronize do
289
+ subs = @pending_subscriptions.dup
290
+ @pending_subscriptions.clear
291
+ subs
292
+ end
293
+
294
+ pending.each { |sub| send_subscribe(sub) }
295
+ end
296
+
297
+ def send_subscribe(subscription)
298
+ send_json({ method: 'subscribe', subscription: subscription })
299
+ end
300
+
301
+ def send_unsubscribe(subscription)
302
+ send_json({ method: 'unsubscribe', subscription: subscription })
303
+ end
304
+
305
+ def send_json(hash)
306
+ @ws&.send(JSON.generate(hash))
307
+ rescue StandardError => e
308
+ warn "[Hyperliquid::WS] Send error: #{e.message}"
309
+ end
310
+
311
+ def attempt_reconnect
312
+ Thread.new do
313
+ loop do
314
+ break if @closing
315
+
316
+ delay = [2**@reconnect_attempts, 30].min
317
+ @reconnect_attempts += 1
318
+ sleep delay
319
+
320
+ break if @closing
321
+
322
+ begin
323
+ establish_connection
324
+ break
325
+ rescue StandardError => e
326
+ warn "[Hyperliquid::WS] Reconnect failed: #{e.message}"
327
+ end
328
+ end
329
+ end
330
+ end
331
+
332
+ def replay_subscriptions
333
+ subs = @mutex.synchronize do
334
+ @subscription_msgs.values.map { |v| v[:subscription] }.uniq
335
+ end
336
+ subs.each { |sub| send_subscribe(sub) }
337
+ end
338
+ end
339
+ end
340
+ end
data/lib/hyperliquid.rb CHANGED
@@ -9,6 +9,7 @@ require_relative 'hyperliquid/cloid'
9
9
  require_relative 'hyperliquid/signing/eip712'
10
10
  require_relative 'hyperliquid/signing/signer'
11
11
  require_relative 'hyperliquid/exchange'
12
+ require_relative 'hyperliquid/ws/client'
12
13
 
13
14
  # Ruby SDK for Hyperliquid API
14
15
  # Provides access to Hyperliquid's decentralized exchange API
@@ -34,7 +35,7 @@ module Hyperliquid
34
35
 
35
36
  # Main SDK class
36
37
  class SDK
37
- attr_reader :info, :exchange
38
+ attr_reader :info, :exchange, :ws
38
39
 
39
40
  # Initialize the SDK
40
41
  # @param testnet [Boolean] Whether to use testnet (default: false for mainnet)
@@ -50,6 +51,7 @@ module Hyperliquid
50
51
  @info = Info.new(client)
51
52
  @testnet = testnet
52
53
  @exchange = nil
54
+ @ws = WS::Client.new(testnet: testnet)
53
55
 
54
56
  return unless private_key
55
57
 
@@ -58,6 +60,7 @@ module Hyperliquid
58
60
  client: client,
59
61
  signer: signer,
60
62
  info: @info,
63
+ testnet: testnet,
61
64
  expires_after: expires_after
62
65
  )
63
66
  end
@@ -1,49 +1,55 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- # Test 3: Perp Market Roundtrip (BTC Long)
5
- # Open a long BTC position, then close it.
4
+ # Test 3: Perp Market Roundtrip (Long)
5
+ # Open a long perp position, then close it.
6
6
 
7
7
  require_relative 'test_helpers'
8
8
 
9
9
  sdk = build_sdk
10
- separator('TEST 3: Perp Market Roundtrip (BTC Long)')
10
+ perp_coin = 'ETH'
11
+ separator("TEST 3: Perp Market Roundtrip (#{perp_coin} Long)")
11
12
 
12
- perp_coin = 'BTC'
13
13
  mids = sdk.info.all_mids
14
- btc_price = mids[perp_coin]&.to_f
14
+ perp_coin_price = mids[perp_coin]&.to_f
15
15
 
16
- if btc_price&.positive?
16
+ if perp_coin_price&.positive?
17
17
  meta = sdk.info.meta
18
- btc_meta = meta['universe'].find { |a| a['name'] == perp_coin }
19
- sz_decimals = btc_meta['szDecimals']
18
+ perp_coin_meta = meta['universe'].find { |a| a['name'] == perp_coin }
19
+ sz_decimals = perp_coin_meta['szDecimals']
20
20
 
21
- perp_size = (20.0 / btc_price).ceil(sz_decimals)
21
+ perp_size = (20.0 / perp_coin_price).ceil(sz_decimals)
22
22
 
23
- puts "#{perp_coin} mid: $#{btc_price.round(2)}"
24
- puts "Size: #{perp_size} BTC (~$#{(perp_size * btc_price).round(2)})"
25
- puts "Slippage: #{(PERP_SLIPPAGE * 100).to_i}%"
23
+ puts "#{perp_coin} mid: $#{perp_coin_price.round(2)}"
24
+ puts "Size: #{perp_size} #{perp_coin} (~$#{(perp_size * perp_coin_price).round(2)})"
25
+ puts "Slippage: #{(PERP_SLIPPAGE * 100).to_i}% (with retry up to #{((PERP_SLIPPAGE + ORACLE_SLIPPAGE_INCREMENT * (ORACLE_RETRY_ATTEMPTS - 1)) * 100).to_i}%)"
26
26
  puts
27
27
 
28
28
  puts 'Opening LONG position (market buy)...'
29
- result = sdk.exchange.market_order(
29
+ result = market_order_with_retry(
30
+ sdk,
30
31
  coin: perp_coin,
31
32
  is_buy: true,
32
33
  size: perp_size,
33
- slippage: PERP_SLIPPAGE
34
+ base_slippage: PERP_SLIPPAGE
34
35
  )
35
- check_result(result, 'Long open')
36
-
37
- wait_with_countdown(WAIT_SECONDS, 'Position open. Waiting before close...')
38
-
39
- puts 'Closing LONG position (market sell)...'
40
- result = sdk.exchange.market_order(
41
- coin: perp_coin,
42
- is_buy: false,
43
- size: perp_size,
44
- slippage: PERP_SLIPPAGE
45
- )
46
- check_result(result, 'Long close')
36
+ open_success = check_result(result, 'Long open')
37
+
38
+ if open_success
39
+ wait_with_countdown(WAIT_SECONDS, 'Position open. Waiting before close...')
40
+
41
+ puts 'Closing LONG position (market sell)...'
42
+ result = market_order_with_retry(
43
+ sdk,
44
+ coin: perp_coin,
45
+ is_buy: false,
46
+ size: perp_size,
47
+ base_slippage: PERP_SLIPPAGE
48
+ )
49
+ check_result(result, 'Long close')
50
+ else
51
+ puts red('Skipping close - position was not opened')
52
+ end
47
53
  else
48
54
  puts red("SKIPPED: Could not get #{perp_coin} price")
49
55
  end
@@ -3,6 +3,7 @@
3
3
 
4
4
  # Test 5: Update Leverage (BTC)
5
5
  # Set cross, isolated, then reset leverage.
6
+ # Requires no open BTC position (cannot switch leverage type with open position).
6
7
 
7
8
  require_relative 'test_helpers'
8
9
 
@@ -14,8 +15,16 @@ mids = sdk.info.all_mids
14
15
  btc_price = mids[perp_coin]&.to_f
15
16
 
16
17
  if btc_price&.positive?
18
+ # Check for open position - cannot switch leverage type with open position
19
+ unless check_position_and_prompt(sdk, perp_coin, timeout: 10)
20
+ puts
21
+ puts green('Test 5 Update Leverage skipped (open position).')
22
+ exit 0
23
+ end
24
+
17
25
  puts 'Setting BTC to 5x cross leverage...'
18
26
  result = sdk.exchange.update_leverage(coin: perp_coin, leverage: 5, is_cross: true)
27
+ dump_status(result)
19
28
  api_error?(result) || puts(green('5x cross leverage set'))
20
29
  puts
21
30
 
@@ -23,6 +32,7 @@ if btc_price&.positive?
23
32
 
24
33
  puts 'Setting BTC to 3x isolated leverage...'
25
34
  result = sdk.exchange.update_leverage(coin: perp_coin, leverage: 3, is_cross: false)
35
+ dump_status(result)
26
36
  api_error?(result) || puts(green('3x isolated leverage set'))
27
37
  puts
28
38
 
@@ -30,6 +40,7 @@ if btc_price&.positive?
30
40
 
31
41
  puts 'Resetting BTC to 1x cross leverage...'
32
42
  result = sdk.exchange.update_leverage(coin: perp_coin, leverage: 1, is_cross: true)
43
+ dump_status(result)
33
44
  api_error?(result) || puts(green('1x cross leverage set'))
34
45
  else
35
46
  puts red("SKIPPED: Could not get #{perp_coin} price")
@@ -1,46 +1,52 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- # Test 7: Market Close (BTC)
4
+ # Test 7: Market Close (PERP)
5
5
  # Open a long position, then close it using market_close (auto-detect size).
6
6
 
7
7
  require_relative 'test_helpers'
8
8
 
9
9
  sdk = build_sdk
10
- separator('TEST 7: Market Close (BTC)')
10
+ perp_coin = 'ETH'
11
+ separator("TEST 7: Market Close (#{perp_coin})")
11
12
 
12
- perp_coin = 'BTC'
13
13
  mids = sdk.info.all_mids
14
- btc_price = mids[perp_coin]&.to_f
14
+ perp_coin_price = mids[perp_coin]&.to_f
15
15
 
16
- if btc_price&.positive?
16
+ if perp_coin_price&.positive?
17
17
  meta = sdk.info.meta
18
- btc_meta = meta['universe'].find { |a| a['name'] == perp_coin }
19
- sz_decimals = btc_meta['szDecimals']
18
+ perp_coin_meta = meta['universe'].find { |a| a['name'] == perp_coin }
19
+ sz_decimals = perp_coin_meta['szDecimals']
20
20
 
21
- perp_size = (20.0 / btc_price).ceil(sz_decimals)
21
+ perp_size = (20.0 / perp_coin_price).ceil(sz_decimals)
22
22
 
23
- puts "#{perp_coin} mid: $#{btc_price.round(2)}"
24
- puts "Size: #{perp_size} BTC"
23
+ puts "#{perp_coin} mid: $#{perp_coin_price.round(2)}"
24
+ puts "Size: #{perp_size} #{perp_coin}"
25
+ puts "Slippage: #{(PERP_SLIPPAGE * 100).to_i}% (with retry up to #{((PERP_SLIPPAGE + ORACLE_SLIPPAGE_INCREMENT * (ORACLE_RETRY_ATTEMPTS - 1)) * 100).to_i}%)"
25
26
  puts
26
27
 
27
28
  puts 'Opening LONG position (market buy)...'
28
- result = sdk.exchange.market_order(
29
+ result = market_order_with_retry(
30
+ sdk,
29
31
  coin: perp_coin,
30
32
  is_buy: true,
31
33
  size: perp_size,
32
- slippage: PERP_SLIPPAGE
34
+ base_slippage: PERP_SLIPPAGE
33
35
  )
34
- check_result(result, 'Long open')
35
-
36
- wait_with_countdown(WAIT_SECONDS, 'Position open. Waiting before market_close...')
37
-
38
- puts 'Closing position using market_close (auto-detect size)...'
39
- result = sdk.exchange.market_close(
40
- coin: perp_coin,
41
- slippage: PERP_SLIPPAGE
42
- )
43
- check_result(result, 'Market close')
36
+ open_success = check_result(result, 'Long open')
37
+
38
+ if open_success
39
+ wait_with_countdown(WAIT_SECONDS, 'Position open. Waiting before market_close...')
40
+
41
+ puts 'Closing position using market_close (auto-detect size)...'
42
+ result = sdk.exchange.market_close(
43
+ coin: perp_coin,
44
+ slippage: PERP_SLIPPAGE + ORACLE_SLIPPAGE_INCREMENT # Use higher slippage for close
45
+ )
46
+ check_result(result, 'Market close')
47
+ else
48
+ puts red('Skipping market_close - position was not opened')
49
+ end
44
50
  else
45
51
  puts red("SKIPPED: Could not get #{perp_coin} price")
46
52
  end
@@ -11,6 +11,7 @@ separator('TEST 8: USD Class Transfer (Perp <-> Spot)')
11
11
 
12
12
  puts 'Transferring $10 from perp to spot...'
13
13
  result = sdk.exchange.usd_class_transfer(amount: '10', to_perp: false)
14
+ dump_status(result)
14
15
  api_error?(result) || puts(green('Transfer to spot successful!'))
15
16
  puts
16
17
 
@@ -18,6 +19,7 @@ wait_with_countdown(WAIT_SECONDS, 'Waiting before transferring back...')
18
19
 
19
20
  puts 'Transferring $10 from spot to perp...'
20
21
  result = sdk.exchange.usd_class_transfer(amount: '10', to_perp: true)
22
+ dump_status(result)
21
23
  api_error?(result) || puts(green('Transfer to perp successful!'))
22
24
 
23
25
  test_passed('Test 8 USD Class Transfer')
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Test 10: Vault Status / Deposit / Withdraw
5
+ #
6
+ # Default: Show vault status (equity, entry date, unlock date)
7
+ # Options: ruby test_10_vault_transfer.rb deposit
8
+ # ruby test_10_vault_transfer.rb withdraw
9
+
10
+ require_relative 'test_helpers'
11
+
12
+ sdk = build_sdk
13
+ separator('TEST 10: Vault Status / Deposit / Withdraw')
14
+
15
+ vault_addr = '0xa15099a30bbf2e68942d6f4c43d70d04faeab0a0'
16
+ action = ARGV[0] # nil, "deposit", or "withdraw"
17
+
18
+ puts "Vault: #{vault_addr}"
19
+ puts
20
+
21
+ vault = sdk.info.vault_details(vault_addr, sdk.exchange.address)
22
+ follower = vault['followerState']
23
+
24
+ if follower
25
+ equity = follower['vaultEquity']
26
+ entry_time = follower['vaultEntryTime']
27
+ lockup_until = follower['lockupUntil']
28
+
29
+ puts "Vault equity: $#{equity}"
30
+ puts "Entry date: #{Time.at(entry_time / 1000.0).utc}" if entry_time
31
+ puts "Unlock date: #{Time.at(lockup_until / 1000.0).utc}" if lockup_until
32
+ else
33
+ puts 'No position in this vault.'
34
+ end
35
+ puts
36
+
37
+ case action
38
+ when 'deposit'
39
+ puts 'Depositing $10 to vault...'
40
+ result = sdk.exchange.vault_transfer(
41
+ vault_address: vault_addr,
42
+ is_deposit: true,
43
+ usd: 10
44
+ )
45
+ dump_status(result)
46
+ api_error?(result) || puts(green('Vault deposit successful!'))
47
+ when 'withdraw'
48
+ equity_f = follower&.dig('vaultEquity')&.to_f || 0
49
+ if equity_f > 1
50
+ withdraw_amount = equity_f.floor
51
+ puts "Withdrawing $#{withdraw_amount} from vault..."
52
+ result = sdk.exchange.vault_transfer(
53
+ vault_address: vault_addr,
54
+ is_deposit: false,
55
+ usd: withdraw_amount
56
+ )
57
+ dump_status(result)
58
+ api_error?(result) || puts(green('Vault withdrawal successful!'))
59
+ else
60
+ puts red("Insufficient vault equity to withdraw ($#{equity_f})")
61
+ end
62
+ else
63
+ puts 'Pass "deposit" or "withdraw" as an argument to perform a transfer.'
64
+ puts ' ruby scripts/test_10_vault_transfer.rb deposit'
65
+ puts ' ruby scripts/test_10_vault_transfer.rb withdraw'
66
+ end
67
+
68
+ test_passed('Test 10 Vault Status')