hyperliquid 1.9.2 → 2.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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -0
  3. data/README.md +26 -0
  4. data/docs/API.md +374 -152
  5. data/docs/CONFIGURATION.md +25 -11
  6. data/docs/DEVELOPMENT.md +93 -15
  7. data/docs/ERRORS.md +2 -0
  8. data/docs/EXAMPLES.md +253 -2
  9. data/docs/WS.md +78 -20
  10. data/lib/hyperliquid/client.rb +19 -10
  11. data/lib/hyperliquid/exchange.rb +844 -22
  12. data/lib/hyperliquid/info.rb +23 -11
  13. data/lib/hyperliquid/signing/signer.rb +18 -1
  14. data/lib/hyperliquid/version.rb +1 -1
  15. data/lib/hyperliquid/ws/client.rb +192 -78
  16. data/lib/hyperliquid.rb +6 -0
  17. metadata +16 -34
  18. data/.rspec +0 -3
  19. data/.rubocop.yml +0 -49
  20. data/.ruby-version +0 -1
  21. data/CLAUDE.md +0 -108
  22. data/CODE_OF_CONDUCT.md +0 -132
  23. data/Rakefile +0 -12
  24. data/example.rb +0 -161
  25. data/scripts/test_01_spot_market_roundtrip.rb +0 -48
  26. data/scripts/test_02_spot_limit_order.rb +0 -48
  27. data/scripts/test_03_perp_market_roundtrip.rb +0 -58
  28. data/scripts/test_04_perp_limit_order.rb +0 -52
  29. data/scripts/test_05_update_leverage.rb +0 -50
  30. data/scripts/test_06_modify_order.rb +0 -67
  31. data/scripts/test_07_market_close.rb +0 -55
  32. data/scripts/test_08_usd_class_transfer.rb +0 -36
  33. data/scripts/test_09_sub_account_lifecycle.rb +0 -51
  34. data/scripts/test_10_vault.rb +0 -68
  35. data/scripts/test_11_builder_fee.rb +0 -81
  36. data/scripts/test_12_staking.rb +0 -70
  37. data/scripts/test_13_ws_l2_book.rb +0 -83
  38. data/scripts/test_14_ws_candle.rb +0 -81
  39. data/scripts/test_15_explorer.rb +0 -71
  40. data/scripts/test_16_send_to_evm_with_data.rb +0 -79
  41. data/scripts/test_17_create_vault.rb +0 -73
  42. data/scripts/test_18_user_portfolio_margin.rb +0 -72
  43. data/scripts/test_19_spot_user.rb +0 -55
  44. data/scripts/test_20_explorer_ws.rb +0 -92
  45. data/scripts/test_all.rb +0 -96
  46. data/scripts/test_automated.rb +0 -82
  47. data/scripts/test_helpers.rb +0 -238
  48. data/sig/hyperliquid.rbs +0 -4
  49. data/test_integration.rb +0 -17
@@ -3,10 +3,11 @@
3
3
  ## Basic Options
4
4
 
5
5
  ```ruby
6
- # Custom timeout (default: 30 seconds)
6
+ # Custom timeout in seconds (default: 30), applied separately to connecting,
7
+ # sending the request and waiting for the response
7
8
  sdk = Hyperliquid.new(timeout: 60)
8
9
 
9
- # Enable retry logic for handling transient failures (default: disabled)
10
+ # Retry transient failures of read requests (default: disabled; /exchange is never retried)
10
11
  sdk = Hyperliquid.new(retry_enabled: true)
11
12
 
12
13
  # Enable trading with a private key
@@ -36,18 +37,31 @@ sdk.base_url # => "https://api.hyperliquid.xyz"
36
37
  sdk.exchange # => nil if no private_key, Hyperliquid::Exchange instance otherwise
37
38
  ```
38
39
 
40
+ ## Timeout
41
+
42
+ `timeout:` (seconds, default 30) is used for every phase of a request: opening the connection, writing the request and reading the response. Each phase can take up to that long, so a slow response raises `Hyperliquid::TimeoutError` once no data has arrived for `timeout` seconds.
43
+
39
44
  ## Retry Configuration
40
45
 
41
- By default, retry logic is **disabled** for predictable API behavior. When enabled, the SDK will automatically retry requests that fail due to:
46
+ Retries are **disabled** by default. With `retry_enabled: true`, only read requests are retried:
47
+
48
+ - `POST /info`: every `sdk.info` method except `tx_details` and `user_details`
49
+ - `POST /explorer` on the RPC host: `sdk.info.tx_details` and `sdk.info.user_details`
50
+
51
+ **`POST /exchange` is never retried**, whatever the failure. Resending a signed action could execute it twice (for example, place the same order twice), so a failed exchange request always raises after exactly one attempt. Check the result (for example with `open_orders` or `order_status`) before you send the action again.
42
52
 
43
- - Network connectivity issues (connection failed, timeouts)
44
- - Server errors (5xx status codes)
45
- - Rate limiting (429 status codes)
53
+ A read request is retried when:
54
+
55
+ - the response status is 429, 502, 503 or 504
56
+ - the connection fails (`Faraday::ConnectionFailed`) or times out (`Faraday::TimeoutError`)
57
+
58
+ Other statuses (400, 401, 404, 500, …) are not retried and raise their error class immediately.
46
59
 
47
60
  **Retry Settings:**
48
- - Maximum retries: 2
49
- - Base interval: 0.5 seconds
50
- - Backoff factor: 2x (exponential backoff)
51
- - Randomness: ±50% to prevent thundering herd
61
+ - Maximum retries: 2 (at most 3 attempts per read request)
62
+ - Wait before retry *n*: 0.5 s × 2^(n−1), plus a random 0–0.25 s (0.5–0.75 s, then 1.0–1.25 s)
63
+ - A `Retry-After` or `RateLimit-Reset` response header that asks for a longer wait is honoured
64
+
65
+ When the last attempt still fails, the SDK raises the same error a single failure would raise (`RateLimitError` for 429, `ServerError` for 5xx, `NetworkError` or `TimeoutError` for network failures).
52
66
 
53
- **Note:** Retries are disabled by default to avoid unexpected delays in time-sensitive trading applications. Enable only when you want automatic handling of transient failures.
67
+ **Note:** Retries are disabled by default to avoid unexpected delays in time-sensitive trading applications. Enable them only if you want transient read failures handled automatically.
data/docs/DEVELOPMENT.md CHANGED
@@ -11,46 +11,124 @@ bin/setup
11
11
  ## Running Tests
12
12
 
13
13
  ```bash
14
- # Run all unit tests
14
+ # Default gate (what CI runs): unit tests, RuboCop, and Lint+Security on scripts/ and example.rb
15
+ rake
16
+
17
+ # Unit tests only (random order; reproduce a failure with its printed seed)
15
18
  rake spec
19
+ bundle exec rspec --seed 1234
16
20
 
17
- # Run unit tests and linting together (default)
18
- rake
21
+ # Default gate again inside a fresh clone of HEAD with a frozen bundle (run after committing)
22
+ rake verify
23
+
24
+ # Exit 0 and print DOCS_ONLY=yes iff every uncommitted change is docs-only
25
+ rake verify:docs_only
26
+
27
+ # Regenerate the Python-SDK parity fixtures and diff them against spec/fixtures/parity (python3 + network)
28
+ rake parity:check
29
+
30
+ # Run the Ruby 3.3 CI leg locally
31
+ RBENV_VERSION=3.3.12 bundle exec rake
19
32
  ```
20
33
 
34
+ A change is done when `rake` is green, `rake verify` is green on the commit, and `rake integration` reports `INTEGRATION GATE: PASS`. You can skip `rake integration` only when `rake verify:docs_only` prints `DOCS_ONLY=yes`. Unit and lint failures are never waived. See `CLAUDE.md` → Verification.
35
+
21
36
  ### Integration Testing (Testnet)
22
37
 
23
38
  Integration tests live in `scripts/` and execute real trades on testnet. No real funds are at risk. Get testnet funds from https://app.hyperliquid-testnet.xyz.
24
39
 
25
40
  ```bash
26
- # Run all integration tests
27
- HYPERLIQUID_PRIVATE_KEY=0x... ruby scripts/test_all.rb
41
+ # Run the integration gate (exits 2 when HYPERLIQUID_PRIVATE_KEY is unset)
42
+ HYPERLIQUID_PRIVATE_KEY=0x... rake integration
43
+
44
+ # Same runner without rake
45
+ HYPERLIQUID_PRIVATE_KEY=0x... ruby -rbundler/setup scripts/test_all.rb
28
46
 
29
47
  # Run a single integration test
30
- HYPERLIQUID_PRIVATE_KEY=0x... ruby scripts/test_08_usd_class_transfer.rb
48
+ HYPERLIQUID_PRIVATE_KEY=0x... ruby -rbundler/setup scripts/test_08_usd_class_transfer.rb
49
+
50
+ # Read-only wallet precondition check (the runner's pre-flight and post-flight)
51
+ HYPERLIQUID_PRIVATE_KEY=0x... ruby -rbundler/setup scripts/testnet_wallet_check.rb --assert
52
+
53
+ # Switch to standard abstraction and rebalance (manual only, never from a runner)
54
+ HYPERLIQUID_PRIVATE_KEY=0x... ruby -rbundler/setup scripts/testnet_wallet_check.rb --fix
55
+ ```
56
+
57
+ `scripts/test_all.rb` is the only runner.
58
+ - **Discovery.** It runs every `scripts/test_[0-9][0-9]_*.rb` it finds by glob, in sorted order, so a new script is picked up automatically. Deliberate exclusions go in its `OPT_OUT` hash, with a reason.
59
+ - **Lock.** It holds a `flock` on `tmp/integration.lock`, so only one gate runs at a time.
60
+ - **Wallet checks.** It runs `testnet_wallet_check.rb --assert` before the first script and after the last. A violation fails the gate as `precondition` or `leak`.
61
+ - **Process.** Each script runs under `ruby -rbundler/setup`, against the locked gems, in its own process group. The private key is redacted from the output.
62
+ - **Timeouts.** Each script has a timeout of `HL_SCRIPT_TIMEOUT` seconds (default 300) and is then killed (TIMEOUT). The whole run has a budget of `HL_GATE_BUDGET` seconds (default 2400); scripts left when it runs out are NOT_RUN.
63
+
64
+ Each script reports its result through its exit code:
65
+
66
+ | Status | Exit | Meaning | Blocks the gate |
67
+ |--------|------|---------|-----------------|
68
+ | PASS | 0 | Verified (for a rejection wire check: the pinned rejection arrived) | no |
69
+ | FAIL | 1 (or any other unlisted code) | Something is wrong | yes |
70
+ | INCONCLUSIVE | 75 | The environment gave no evidence either way | no (yes with `HL_GATE_STRICT=1`) |
71
+ | SKIPPED | 77 | The script deliberately tested nothing | no (yes with `HL_GATE_STRICT=1`) |
72
+ | GUARDED | 78 | Wallet/testnet state forced a weaker check that still proves signing | no (yes with `HL_GATE_STRICT=1`) |
73
+ | TIMEOUT | — | Exceeded `HL_SCRIPT_TIMEOUT` and was killed | yes |
74
+ | NOT_RUN | — | `HL_GATE_BUDGET` ran out before the script started | yes |
75
+
76
+ The runner's last stdout line is always:
77
+
78
+ ```text
79
+ INTEGRATION GATE: PASS|FAIL total=N pass=a guarded=b skipped=c inconclusive=d fail=e timeout=f not_run=g
31
80
  ```
32
81
 
33
- The convenience wrapper `ruby test_integration.rb` also runs all tests.
82
+ A JSON summary with per-script status, exit code, duration and `RESULT` line goes to `HL_GATE_SUMMARY`, default `tmp/integration-summary.json`. Report every INCONCLUSIVE, SKIPPED and GUARDED script by name.
83
+
84
+ Every script is safe to run unattended in its default mode, and a new script must be too: read-only, or rejection-only with a fail-closed guard. Destructive or locking variants are opt-in CLI args on individual scripts and are never run by the runner:
85
+
86
+ ```bash
87
+ ruby -rbundler/setup scripts/test_10_vault.rb deposit|withdraw # move $10 into / out of the test vault
88
+ ruby -rbundler/setup scripts/test_12_staking.rb delegate|undelegate # 0.1 HYPE, 1-day delegation lockup
89
+ ruby -rbundler/setup scripts/test_16_send_to_evm_with_data.rb live # burns 1 testnet USDC
90
+ ruby -rbundler/setup scripts/test_17_create_vault.rb live # locks $100 in a new vault
91
+ ```
34
92
 
35
- Available test scripts:
93
+ Available test scripts (every `scripts/test_NN_*.rb` has exactly one row here; `spec/call_surface_spec.rb` enforces it):
36
94
 
37
95
  | Script | Description |
38
96
  |--------|-------------|
39
97
  | `test_01_spot_market_roundtrip.rb` | Buy/sell PURR/USDC at market |
40
- | `test_02_spot_limit_order.rb` | Place and cancel a spot limit order |
98
+ | `test_02_spot_limit_order.rb` | Place a resting spot limit buy below market and cancel it |
41
99
  | `test_03_perp_market_roundtrip.rb` | Long/close BTC at market |
42
- | `test_04_perp_limit_order.rb` | Place and cancel a perp short |
43
- | `test_05_update_leverage.rb` | Set cross, isolated, and reset leverage |
100
+ | `test_04_perp_limit_order.rb` | Place a resting perp limit short above market and cancel it |
101
+ | `test_05_update_leverage.rb` | Set cross, isolated, and reset BTC leverage (requires no BTC position or orders) |
44
102
  | `test_06_modify_order.rb` | Place, modify, and cancel an order |
45
103
  | `test_07_market_close.rb` | Open a position and close via `market_close` |
46
- | `test_08_usd_class_transfer.rb` | Transfer USDC between perp and spot |
47
- | `test_09_sub_account_lifecycle.rb` | Create sub-account, deposit, withdraw |
48
- | `test_10_vault_transfer.rb` | Deposit/withdraw to a vault |
104
+ | `test_08_usd_class_transfer.rb` | Transfer USDC perp → spot → perp (GUARDED rejection wire check on a unified wallet) |
105
+ | `test_09_sub_account_lifecycle.rb` | Create (or reuse) a sub-account, deposit, withdraw (volume-gate rejection is a wire check) |
106
+ | `test_10_vault.rb` | Vault status (`deposit`/`withdraw` opt-in) |
107
+ | `test_11_builder_fee.rb` | Approve a builder fee, place an order with a builder, cancel (GUARDED if the builder is ineligible) |
108
+ | `test_12_staking.rb` | Staking status and undelegate rejection wire check (`delegate`/`undelegate` opt-in) |
109
+ | `test_13_ws_l2_book.rb` | WebSocket `l2Book` subscription (top of book; no key) |
110
+ | `test_14_ws_candle.rb` | WebSocket `candle` subscriptions with coin/interval routing checks (INCONCLUSIVE when testnet ETH has no trades) |
111
+ | `test_15_explorer.rb` | Explorer RPC `user_details` and `tx_details` (SKIPPED when the wallet has no txs) |
112
+ | `test_16_send_to_evm_with_data.rb` | Send USDC to HyperEVM with calldata (default: rejection wire check; `live` sends 1 USDC) |
113
+ | `test_17_create_vault.rb` | Create a vault (default: rejection wire check; `live` seeds a real vault with $100) |
114
+ | `test_18_user_portfolio_margin.rb` | Portfolio-margin toggle (rejection wire check; restores the original mode) |
115
+ | `test_19_spot_user.rb` | Opt out of then back into spot dusting |
116
+ | `test_20_explorer_ws.rb` | Explorer WebSocket block stream (3 blocks; INCONCLUSIVE on 1–2) |
117
+ | `test_21_ws_channel_parity.rb` | Subscribe to the parity WS channels (read-only) and check each delivers its own user/dex/coin |
118
+ | `test_22_outcome_deploy.rb` | HIP-4 outcome deployer actions (structured-rejection wire check) |
119
+ | `test_23_ws_fast_asset_ctxs.rb` | Subscribe to the compressed `fastAssetCtxs` channel (read-only) and check the decoded snapshot + deltas |
120
+ | `test_24_perp_deploy_wire.rb` | Send `perpDeploy` actions to a nonexistent dex; expect a structured rejection (proves signing) |
121
+ | `test_25_hip3_star.rb` | Read `user_star_state`; send a HIP-3* `star` action to a live star dex; expect a structured rejection (proves signing; SKIPPED when no star dex exists) |
122
+ | `test_26_spot_deploy_rejection.rb` | spotDeploy structured-rejection wire check on token 0 with a throwaway-key control (no state change) |
123
+ | `test_27_validator_actions.rb` | Validator-operator actions (`validator_l1_stream`, `c_signer_*`, `c_validator_change_profile`, `c_validator_unregister`) — rejection-only wire check behind a fail-closed non-validator safety guard; `c_validator_register` is never sent |
124
+
125
+ Each rejection wire check (`test_09`, `test_12`, `test_16`–`test_18`, `test_22`, `test_24`–`test_27`) also sends the same call with a never-funded throwaway key. It passes only when the wallet's rejection differs from that control's "does not exist" / "must deposit" rejection, which proves the server recovered this wallet as the signer.
49
126
 
50
127
  ## Linting
51
128
 
52
129
  ```bash
53
- rake rubocop
130
+ rake rubocop # lib/, spec/, config files
131
+ rake rubocop:scripts # Lint + Security departments only, on scripts/ and example.rb
54
132
  ```
55
133
 
56
134
  ## Interactive Console
data/docs/ERRORS.md CHANGED
@@ -36,3 +36,5 @@ end
36
36
  | `Hyperliquid::RateLimitError` | 429 errors |
37
37
  | `Hyperliquid::NetworkError` | Connection issues |
38
38
  | `Hyperliquid::TimeoutError` | Request timeouts |
39
+ | `Hyperliquid::WebSocketError` | Unsupported WebSocket subscription type |
40
+ | `Hyperliquid::ConfigurationError` | Missing configuration, e.g. explorer endpoint not set |
data/docs/EXAMPLES.md CHANGED
@@ -134,6 +134,20 @@ signers = sdk.info.user_to_multi_sig_signers(user_address)
134
134
  # Get dex abstraction config for a user
135
135
  dex_abstraction = sdk.info.user_dex_abstraction(user_address)
136
136
  # => { "enabled" => true }
137
+
138
+ # Fills in a time range, newest first, with partial fills of one crossing order merged
139
+ fills = sdk.info.user_fills_by_time(user_address, start_time_ms, nil, aggregate_by_time: true, reversed: true)
140
+
141
+ # Recent trades and a block by height
142
+ sdk.info.recent_trades('BTC')
143
+ sdk.info.block_details(123_456)
144
+
145
+ # Vaults created in the last 2 hours, and vaults a user leads
146
+ sdk.info.vault_summaries
147
+ sdk.info.leading_vaults(user_address)
148
+
149
+ # Margin table (dex: '' is the main dex)
150
+ sdk.info.margin_table(1, dex: '')
137
151
  ```
138
152
 
139
153
  **Note:** `l2_book` and `candles_snapshot` work for both Perpetuals and Spot. For spot, use `"{BASE}/USDC"` when available (e.g., `"PURR/USDC"`). Otherwise, use the index alias `"@{index}"` from `spot_meta["universe"]`.
@@ -183,6 +197,29 @@ oi_capped = sdk.info.perps_at_open_interest_cap
183
197
  auction = sdk.info.perp_deploy_auction_status
184
198
  # => { "startTimeSeconds" => ..., "durationSeconds" => ..., "startGas" => "500.0", ... }
185
199
 
200
+ # Deploy a HIP-3 perp dex (deployer wallet only; mirrors the Python SDK's examples/perp_deploy.py).
201
+ # The first registration on a new dex passes `schema:` and pays the deploy-auction gas.
202
+ # `auction` comes from perp_deploy_auction_status above; `max_gas: nil` bids its currentGas.
203
+ sdk.exchange.perp_deploy_register_asset2(
204
+ dex: "test", coin: "test:TEST0", sz_decimals: 2, oracle_px: "10.0", margin_table_id: 10,
205
+ margin_mode: "noCross",
206
+ max_gas: 1_000_000_000_000, # native-token wei: 10k HYPE
207
+ schema: { full_name: "test dex", collateral_token: 0, oracle_updater: sdk.exchange.address }
208
+ )
209
+ # => { "status" => "ok", "response" => { "type" => "default" } }
210
+
211
+ # Push oracle prices (at most once every 2.5s); coins are full "dex:COIN" names.
212
+ # A real oracle updater runs this continuously.
213
+ 3.times do
214
+ sdk.exchange.perp_deploy_set_oracle(
215
+ dex: "test",
216
+ oracle_pxs: { "test:TEST0" => "12.0" },
217
+ all_mark_pxs: [{ "test:TEST0" => "12.1" }],
218
+ external_perp_pxs: { "test:TEST0" => "12.0" }
219
+ )
220
+ sleep 3
221
+ end
222
+
186
223
  # Retrieve User's Active Asset Data
187
224
  aad = sdk.info.active_asset_data(user_address, "APT")
188
225
  # => { "user" => user_address, "coin" => "APT", "leverage" => { "type" => "cross", "value" => 3 }, ... }
@@ -220,6 +257,31 @@ details = sdk.info.token_details("0x00000000000000000000000000000000")
220
257
  # => { "name" => "TEST", "maxSupply" => "...", "midPx" => "...", ... }
221
258
  ```
222
259
 
260
+ ### HIP-2 Borrow/Lend
261
+
262
+ ```ruby
263
+ sdk.info.all_borrow_lend_reserve_states
264
+ sdk.info.borrow_lend_reserve_state(0) # token index 0 = USDC
265
+ sdk.info.borrow_lend_user_state(user_address)
266
+ sdk.info.user_borrow_lend_interest(user_address, start_time_ms)
267
+ ```
268
+
269
+ ### HIP-4 Outcomes
270
+
271
+ ```ruby
272
+ sdk.info.outcome_meta
273
+ sdk.info.outcome_templates
274
+ sdk.info.settled_outcome(outcome: 5) # nil until settled
275
+ ```
276
+
277
+ ### Explorer RPC
278
+
279
+ ```ruby
280
+ # Routed to rpc.hyperliquid.xyz/explorer (rpc.hyperliquid-testnet.xyz with testnet: true)
281
+ txs = sdk.info.user_details(user_address)
282
+ sdk.info.tx_details('0x' + 'ab' * 32) # a 66-character transaction hash
283
+ ```
284
+
223
285
  ## Exchange API (Trading)
224
286
 
225
287
  ### Basic Orders
@@ -356,7 +418,7 @@ sdk.exchange.market_close(coin: 'BTC', size: 0.01, slippage: 0.03)
356
418
  cancel_time = (Time.now.to_f * 1000).to_i + 60_000 # 60 seconds from now
357
419
  sdk.exchange.schedule_cancel(time: cancel_time)
358
420
 
359
- # Activate schedule cancel without specifying a time (server default)
421
+ # Remove the scheduled cancel
360
422
  sdk.exchange.schedule_cancel
361
423
  ```
362
424
 
@@ -408,6 +470,34 @@ sdk.exchange.order(
408
470
  )
409
471
  ```
410
472
 
473
+ ### TWAP and Trailing Stop Orders
474
+
475
+ ```ruby
476
+ # Buy 1 ETH over 30 minutes
477
+ result = sdk.exchange.twap_order(coin: 'ETH', is_buy: true, size: 1, reduce_only: false,
478
+ minutes: 30, randomize: true)
479
+ twap_id = result.dig('response', 'data', 'status', 'running', 'twapId')
480
+ sdk.exchange.twap_cancel(coin: 'ETH', twap_id: twap_id)
481
+
482
+ # Reduce-only trailing sell of 0.01 BTC, 1.5% retracement, active once the price reaches 100000
483
+ sdk.exchange.trailing_stop(coin: 'BTC', is_buy: false, size: 0.01, reduce_only: true,
484
+ retracement: { pct: '1.5%' }, activation_px: '100000')
485
+ ```
486
+
487
+ ### Fast Cancel, Always-Place Modify, Order Expiry
488
+
489
+ ```ruby
490
+ sdk.exchange.cancel(coin: 'BTC', oid: 123, fast: true)
491
+ sdk.exchange.modify_order(oid: 123, coin: 'BTC', is_buy: true, size: 0.01, limit_px: 95_000,
492
+ always_place: true)
493
+
494
+ # Reject L1 actions that reach the exchange after this time (ms); nil clears it.
495
+ # Leave it nil around user-signed actions such as usd_send.
496
+ sdk.exchange.expires_after = (Time.now.to_f * 1000).to_i + 10_000
497
+ sdk.exchange.order(coin: 'BTC', is_buy: true, size: 0.01, limit_px: 90_000)
498
+ sdk.exchange.expires_after = nil
499
+ ```
500
+
411
501
  ### Transfers & Account Management
412
502
 
413
503
  ```ruby
@@ -421,7 +511,7 @@ sdk.exchange.usd_send(
421
511
  sdk.exchange.spot_send(
422
512
  amount: '50',
423
513
  destination: '0x...',
424
- token: 'PURR'
514
+ token: 'PURR:0xc4bf3f870c0e9465323c0b6ed28096c2' # testnet PURR; mainnet is PURR:0xc1fb593aeffbeb02f85e0308e9956a90 (see info.spot_meta)
425
515
  )
426
516
 
427
517
  # Move USDC between perp and spot accounts
@@ -442,6 +532,16 @@ sdk.exchange.send_asset(
442
532
  token: 'USDC',
443
533
  amount: '100'
444
534
  )
535
+
536
+ # Move USDC between perp and spot for a sub-account
537
+ sdk.exchange.usd_class_transfer(amount: 10, to_perp: true, sub_account: '0x...')
538
+
539
+ # Send USDC to HyperEVM with calldata for an ICoreReceiveWithData contract
540
+ sdk.exchange.send_to_evm_with_data(
541
+ token: 'USDC', amount: '5', source_dex: 'spot',
542
+ destination_recipient: '0x...', address_encoding: 'hex',
543
+ destination_chain_id: 998, gas_limit: 200_000, data: '0x'
544
+ )
445
545
  ```
446
546
 
447
547
  ### Sub-Account Management
@@ -490,8 +590,54 @@ sdk.exchange.vault_transfer(
490
590
  is_deposit: false,
491
591
  usd: 50
492
592
  )
593
+
594
+ # Create a vault (at least 100 USD), then manage it as leader
595
+ sdk.exchange.create_vault(name: 'My Vault', description: 'Trend following on BTC', initial_usd: 100)
596
+ sdk.exchange.vault_modify(vault_address: '0x...', allow_deposits: false)
597
+ sdk.exchange.vault_distribute(vault_address: '0x...', usd: 25)
598
+ ```
599
+
600
+ ### HIP-2 Borrow/Lend and HIP-4 Outcomes
601
+
602
+ ```ruby
603
+ sdk.exchange.borrow_lend(operation: 'supply', token: 0, amount: '100')
604
+ sdk.exchange.borrow_lend(operation: 'withdraw', token: 0) # amount nil = full position
605
+
606
+ sdk.exchange.split_outcome(outcome: 5, amount: '10') # 10 quote -> 10 Yes + 10 No
607
+ sdk.exchange.merge_outcome(outcome: 5) # merge the maximum back
493
608
  ```
494
609
 
610
+ ### Multi-Sig
611
+
612
+ ```ruby
613
+ # 1. Once: turn the multi-sig account into a 2-of-3 multi-sig user
614
+ owner = Hyperliquid.new(testnet: true, private_key: ENV['MULTI_SIG_OWNER_KEY'])
615
+ owner.exchange.convert_to_multi_sig_user(authorized_users: [addr_a, addr_b, addr_c], threshold: 2)
616
+
617
+ # 2. Each co-signer signs the same inner action and nonce, with the submitter as outer_signer
618
+ multi_sig_user = owner.exchange.address
619
+ submitter = Hyperliquid.new(testnet: true, private_key: ENV['SIGNER_A_KEY'])
620
+ nonce = (Time.now.to_f * 1000).to_i
621
+ inner_action = {
622
+ type: 'order',
623
+ orders: [{ a: 4, b: true, p: '1100', s: '0.2', r: false, t: { limit: { tif: 'Gtc' } } }],
624
+ grouping: 'na'
625
+ }
626
+ signatures = [ENV['SIGNER_A_KEY'], ENV['SIGNER_B_KEY']].map do |key|
627
+ Hyperliquid::Signing::MultiSig.sign_as_co_signer_l1(
628
+ signer: Hyperliquid::Signing::Signer.new(private_key: key, testnet: true),
629
+ inner_action: inner_action, multi_sig_user: multi_sig_user,
630
+ outer_signer: submitter.exchange.address, nonce: nonce
631
+ )
632
+ end
633
+
634
+ # 3. The submitter posts the envelope with the same nonce
635
+ submitter.exchange.multi_sig(multi_sig_user: multi_sig_user, inner_action: inner_action,
636
+ signatures: signatures, nonce: nonce)
637
+ ```
638
+
639
+ The inner action is the wire-format action body (here an order for asset 4). For a user-signed inner action, co-signers call `sign_as_co_signer_user_signed` with its EIP-712 primary type and type constant, e.g. `primary_type: 'HyperliquidTransaction:SendAsset', sign_types: Hyperliquid::Signing::EIP712::SEND_ASSET_TYPES`.
640
+
495
641
  ### Referral
496
642
 
497
643
  ```ruby
@@ -581,6 +727,39 @@ status = sdk.info.user_dex_abstraction(sdk.exchange.address)
581
727
  # => { "enabled" => true }
582
728
  ```
583
729
 
730
+ ### HIP-4 Outcome Deployment
731
+
732
+ For an activated outcome deployer (or a sub-deployer acting for its venue). Responses come back unmodified, so check `status`.
733
+
734
+ ```ruby
735
+ venue = 'abc'
736
+
737
+ # How many outcomes can this venue still deploy?
738
+ limits = sdk.info.outcome_deployer_limits(venue)
739
+ # => { "nDailyOutcomesRemaining" => 10, "nActiveOutcomesRemaining" => 90 }
740
+
741
+ # Deploy a standalone Yes/No outcome; keywords are sorted by the SDK
742
+ result = sdk.exchange.register_standalone_outcome_from_template(
743
+ venue: venue,
744
+ template_id: 'binaryPrice',
745
+ keyword_to_value: { perp: 'BTC', priceDescription: 'the Hyperliquid BTC perp trade',
746
+ seconds: 90, threshold: 84_793, time: '20261002-1900' },
747
+ deployer_fee_scale: '1'
748
+ )
749
+ raise result['response'].to_s unless result['status'] == 'ok'
750
+
751
+ # Settle it later: name, description and side names must match outcome_meta exactly
752
+ outcome = sdk.info.outcome_meta['outcomes'].reverse.find { |o| o['venue'] == venue }
753
+ sdk.exchange.settle_outcome(
754
+ venue: venue,
755
+ outcome: outcome['outcome'],
756
+ settle_fraction: '1', # first side (Yes) pays out in full
757
+ name: outcome['name'],
758
+ description: outcome['description'],
759
+ side_names: outcome['sideSpecs'].map { |s| s['name'] }
760
+ )
761
+ ```
762
+
584
763
  ## WebSocket
585
764
 
586
765
  ### l2Book (Order Book)
@@ -688,6 +867,60 @@ sleep 60
688
867
  sdk.ws.close
689
868
  ```
690
869
 
870
+ ### clearinghouseState (per dex)
871
+
872
+ ```ruby
873
+ sdk = Hyperliquid.new(testnet: true)
874
+ user = '0x...'
875
+
876
+ # Main dex (omit dex:) and a HIP-3 dex are separate subscriptions
877
+ sdk.ws.subscribe({ type: 'clearinghouseState', user: user }) do |data|
878
+ puts "main dex account value: #{data['clearinghouseState']['marginSummary']['accountValue']}"
879
+ end
880
+
881
+ sdk.ws.subscribe({ type: 'clearinghouseState', user: user, dex: 'xyz' }) do |data|
882
+ puts "#{data['dex']} positions: #{data['clearinghouseState']['assetPositions'].length}"
883
+ end
884
+
885
+ sleep 10
886
+ sdk.ws.close
887
+ ```
888
+
889
+ ### activeAssetCtx (Asset Context)
890
+
891
+ ```ruby
892
+ sdk = Hyperliquid.new(testnet: true)
893
+
894
+ # Perp, HIP-3 ('xyz:XYZ100') or spot ('PURR/USDC', '@107') coins
895
+ sdk.ws.subscribe({ type: 'activeAssetCtx', coin: 'BTC' }) do |data|
896
+ puts "#{data['coin']} mark=#{data['ctx']['markPx']} funding=#{data['ctx']['funding']}"
897
+ end
898
+
899
+ sdk.ws.subscribe({ type: 'activeAssetCtx', coin: 'PURR/USDC' }) do |data|
900
+ puts "#{data['coin']} mark=#{data['ctx']['markPx']}"
901
+ end
902
+
903
+ sleep 10
904
+ sdk.ws.close
905
+ ```
906
+
907
+ ### fastAssetCtxs (mark/mid prices, compressed)
908
+
909
+ ```ruby
910
+ sdk = Hyperliquid.new(testnet: true)
911
+ cache = {}
912
+
913
+ # First frame: snapshot of every coin. Later frames: only changed coins/fields.
914
+ # Frames arrive base64 + raw DEFLATE; the SDK decodes them before the callback.
915
+ sdk.ws.subscribe({ type: 'fastAssetCtxs' }) do |ctxs|
916
+ cache.merge!(ctxs) { |_coin, old, new| old.merge(new) }
917
+ puts "BTC mark=#{cache.dig('BTC', 'markPx')} mid=#{cache.dig('BTC', 'midPx')} (#{cache.size} coins)"
918
+ end
919
+
920
+ sleep 10
921
+ sdk.ws.close
922
+ ```
923
+
691
924
  ### Multiple Subscriptions
692
925
 
693
926
  ```ruby
@@ -709,6 +942,24 @@ sleep 10
709
942
  sdk.ws.close
710
943
  ```
711
944
 
945
+ ### Explorer Blocks and Transactions
946
+
947
+ ```ruby
948
+ sdk = Hyperliquid.new(testnet: true)
949
+
950
+ # Separate connection to rpc.hyperliquid(-testnet).xyz/ws; each message is an Array
951
+ sdk.ws.subscribe_explorer_block do |blocks|
952
+ blocks.each { |b| puts "block #{b['height']} with #{b['numTxs']} txs" }
953
+ end
954
+
955
+ sdk.ws.subscribe_explorer_txs do |txs|
956
+ txs.each { |tx| puts "#{tx['user']} #{tx.dig('action', 'type')} #{tx['hash']}" }
957
+ end
958
+
959
+ sleep 10
960
+ sdk.ws.close
961
+ ```
962
+
712
963
  ### Handling Reconnection
713
964
 
714
965
  ```ruby