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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +40 -0
- data/README.md +26 -0
- data/docs/API.md +374 -152
- data/docs/CONFIGURATION.md +25 -11
- data/docs/DEVELOPMENT.md +93 -15
- data/docs/ERRORS.md +2 -0
- data/docs/EXAMPLES.md +253 -2
- data/docs/WS.md +78 -20
- data/lib/hyperliquid/client.rb +19 -10
- data/lib/hyperliquid/exchange.rb +844 -22
- data/lib/hyperliquid/info.rb +23 -11
- data/lib/hyperliquid/signing/signer.rb +18 -1
- data/lib/hyperliquid/version.rb +1 -1
- data/lib/hyperliquid/ws/client.rb +192 -78
- data/lib/hyperliquid.rb +6 -0
- metadata +16 -34
- data/.rspec +0 -3
- data/.rubocop.yml +0 -49
- data/.ruby-version +0 -1
- data/CLAUDE.md +0 -108
- data/CODE_OF_CONDUCT.md +0 -132
- data/Rakefile +0 -12
- data/example.rb +0 -161
- data/scripts/test_01_spot_market_roundtrip.rb +0 -48
- data/scripts/test_02_spot_limit_order.rb +0 -48
- data/scripts/test_03_perp_market_roundtrip.rb +0 -58
- data/scripts/test_04_perp_limit_order.rb +0 -52
- data/scripts/test_05_update_leverage.rb +0 -50
- data/scripts/test_06_modify_order.rb +0 -67
- data/scripts/test_07_market_close.rb +0 -55
- data/scripts/test_08_usd_class_transfer.rb +0 -36
- data/scripts/test_09_sub_account_lifecycle.rb +0 -51
- data/scripts/test_10_vault.rb +0 -68
- data/scripts/test_11_builder_fee.rb +0 -81
- data/scripts/test_12_staking.rb +0 -70
- data/scripts/test_13_ws_l2_book.rb +0 -83
- data/scripts/test_14_ws_candle.rb +0 -81
- data/scripts/test_15_explorer.rb +0 -71
- data/scripts/test_16_send_to_evm_with_data.rb +0 -79
- data/scripts/test_17_create_vault.rb +0 -73
- data/scripts/test_18_user_portfolio_margin.rb +0 -72
- data/scripts/test_19_spot_user.rb +0 -55
- data/scripts/test_20_explorer_ws.rb +0 -92
- data/scripts/test_all.rb +0 -96
- data/scripts/test_automated.rb +0 -82
- data/scripts/test_helpers.rb +0 -238
- data/sig/hyperliquid.rbs +0 -4
- data/test_integration.rb +0 -17
data/docs/CONFIGURATION.md
CHANGED
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
## Basic Options
|
|
4
4
|
|
|
5
5
|
```ruby
|
|
6
|
-
# Custom timeout (default: 30
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
-
|
|
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
|
-
-
|
|
50
|
-
-
|
|
51
|
-
|
|
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
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
|
27
|
-
HYPERLIQUID_PRIVATE_KEY=0x...
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
47
|
-
| `test_09_sub_account_lifecycle.rb` | Create sub-account, deposit, withdraw |
|
|
48
|
-
| `
|
|
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
|
-
#
|
|
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
|