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.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -1
- data/CHANGELOG.md +50 -0
- data/README.md +2 -1
- data/docs/API.md +68 -2
- data/docs/EXAMPLES.md +247 -0
- data/docs/WS.md +49 -0
- data/lib/hyperliquid/constants.rb +5 -0
- data/lib/hyperliquid/errors.rb +3 -0
- data/lib/hyperliquid/exchange.rb +203 -19
- data/lib/hyperliquid/info.rb +26 -2
- data/lib/hyperliquid/signing/eip712.rb +37 -0
- data/lib/hyperliquid/signing/signer.rb +6 -0
- data/lib/hyperliquid/version.rb +1 -1
- data/lib/hyperliquid/ws/client.rb +340 -0
- data/lib/hyperliquid.rb +4 -1
- data/scripts/test_03_perp_market_roundtrip.rb +32 -26
- data/scripts/test_05_update_leverage.rb +11 -0
- data/scripts/test_07_market_close.rb +28 -22
- data/scripts/test_08_usd_class_transfer.rb +2 -0
- data/scripts/test_10_vault.rb +68 -0
- data/scripts/test_11_builder_fee.rb +71 -0
- data/scripts/test_12_staking.rb +70 -0
- data/scripts/test_13_ws_l2_book.rb +83 -0
- data/scripts/test_14_ws_candle.rb +81 -0
- data/scripts/test_all.rb +5 -1
- data/scripts/test_helpers.rb +131 -1
- metadata +22 -2
- data/scripts/test_10_vault_transfer.rb +0 -41
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7c9ed1fef8ad29ab90cbc1ba13a6dc7ceed9267ff98ec97ed6748f71e1ff8289
|
|
4
|
+
data.tar.gz: 10268ae8868d6b46a1a91a853c80f5009b7586d8bed449488ec1db2923a6a3ad
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ab9e06936e8a3417d074f9102463d309c5c7527f945a6165a592ff57124772ce946248b9d6582a281071fc0520fd42136bce3a00cf62e60af02fc1cd3e1ecaaa
|
|
7
|
+
data.tar.gz: 665b93a26551b493309a14aa29d4a795d5e701a54bd55ba66bbbaa3fc20cc30eca6b98224e0bd3bfc538ab87884479918a95fccf49eece876347b61d4914b850
|
data/.rubocop.yml
CHANGED
|
@@ -30,10 +30,11 @@ Metrics/ParameterLists:
|
|
|
30
30
|
Exclude:
|
|
31
31
|
- 'lib/hyperliquid/exchange.rb'
|
|
32
32
|
|
|
33
|
-
# Allow higher complexity for order type conversion logic
|
|
33
|
+
# Allow higher complexity for order type conversion logic and WS message handling
|
|
34
34
|
Metrics/CyclomaticComplexity:
|
|
35
35
|
Exclude:
|
|
36
36
|
- 'lib/hyperliquid/exchange.rb'
|
|
37
|
+
- 'lib/hyperliquid/ws/client.rb'
|
|
37
38
|
|
|
38
39
|
Metrics/PerceivedComplexity:
|
|
39
40
|
Exclude:
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,55 @@
|
|
|
1
1
|
## [Ruby Hyperliquid SDK Changelog]
|
|
2
2
|
|
|
3
|
+
## [1.0.0] - 2026-02-03
|
|
4
|
+
|
|
5
|
+
### WebSocket Support
|
|
6
|
+
|
|
7
|
+
- Add real-time WebSocket client (`Hyperliquid::WS::Client`) with managed connection
|
|
8
|
+
- Three-thread architecture: read thread, dispatch thread, ping thread
|
|
9
|
+
- Automatic reconnection with exponential backoff (1s, 2s, 4s, ..., 30s cap)
|
|
10
|
+
- Heartbeat ping every 50 seconds to keep connection alive
|
|
11
|
+
- Bounded message queue (1024) with overflow detection
|
|
12
|
+
- Lifecycle callbacks: `on(:open)`, `on(:close)`, `on(:error)`
|
|
13
|
+
|
|
14
|
+
- Add 9 WebSocket subscription channels
|
|
15
|
+
- `allMids` — mid prices for all coins
|
|
16
|
+
- `l2Book` — level 2 order book updates
|
|
17
|
+
- `trades` — trade feed for a coin
|
|
18
|
+
- `bbo` — best bid/offer for a coin
|
|
19
|
+
- `candle` — candlestick updates
|
|
20
|
+
- `orderUpdates` — order status changes for a user
|
|
21
|
+
- `userEvents` — all events for a user (fills, liquidations, etc.)
|
|
22
|
+
- `userFills` — fill updates for a user
|
|
23
|
+
- `userFundings` — funding payments for a user
|
|
24
|
+
|
|
25
|
+
### HIP-3 Support
|
|
26
|
+
|
|
27
|
+
- Add HIP-3 DEX abstraction Exchange actions
|
|
28
|
+
- `user_dex_abstraction` — enable/disable DEX abstraction for automatic collateral transfers (user-signed)
|
|
29
|
+
- `agent_enable_dex_abstraction` — enable DEX abstraction via agent (L1 action, enable only)
|
|
30
|
+
- Add full HIP-3 trading support
|
|
31
|
+
- Lazy loading of HIP-3 dex asset metadata when trading prefixed coins (e.g., `xyz:GOLD`)
|
|
32
|
+
- Correct HIP-3 asset ID calculation: `100000 + perp_dex_index * 10000 + index_in_meta`
|
|
33
|
+
- `market_order` and `market_close` automatically use dex-specific price endpoints
|
|
34
|
+
- Add `dex:` parameter to `all_mids` Info endpoint for HIP-3 perp dex prices
|
|
35
|
+
|
|
36
|
+
### Info API
|
|
37
|
+
|
|
38
|
+
- Add 3 more Info endpoints
|
|
39
|
+
- `extra_agents` — get authorized agent addresses for a user
|
|
40
|
+
- `user_to_multi_sig_signers` — get multi-sig signer mappings for a user
|
|
41
|
+
- `user_dex_abstraction` — get dex abstraction config for a user
|
|
42
|
+
|
|
43
|
+
## [0.7.0] - 2026-01-30
|
|
44
|
+
|
|
45
|
+
- Add agent, builder & delegation actions to Exchange API
|
|
46
|
+
- `approve_agent` — authorize an agent wallet to trade on behalf of the account
|
|
47
|
+
- `approve_builder_fee` — approve a builder fee rate for a builder address
|
|
48
|
+
- `token_delegate` — delegate or undelegate HYPE tokens to a validator
|
|
49
|
+
- Add builder fee support on order placement
|
|
50
|
+
- Optional `builder:` parameter on `order`, `bulk_orders`, `market_order`, `market_close`
|
|
51
|
+
- Add EIP-712 type definitions for `ApproveAgent`, `ApproveBuilderFee`, and `TokenDelegate`
|
|
52
|
+
|
|
3
53
|
## [0.6.0] - 2026-01-28
|
|
4
54
|
|
|
5
55
|
- Add transfers and account management to Exchange API
|
data/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
A Ruby SDK for interacting with the Hyperliquid decentralized exchange API.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Full-featured SDK with Info API (market data), Exchange API (trading), real-time WebSocket streaming, and HIP-3 builder-deployed perpetuals support.
|
|
10
10
|
|
|
11
11
|
## Installation
|
|
12
12
|
|
|
@@ -54,6 +54,7 @@ exchange = trading_sdk.exchange
|
|
|
54
54
|
|
|
55
55
|
- [API Reference](docs/API.md) - Complete list of available methods
|
|
56
56
|
- [Examples](docs/EXAMPLES.md) - Code examples for Info and Exchange APIs
|
|
57
|
+
- [Web Sockets](docs/WS.md) - Web Sockets implementation
|
|
57
58
|
- [Configuration](docs/CONFIGURATION.md) - SDK configuration options
|
|
58
59
|
- [Error Handling](docs/ERRORS.md) - Error types and handling
|
|
59
60
|
- [Development](docs/DEVELOPMENT.md) - Contributing and running tests
|
data/docs/API.md
CHANGED
|
@@ -6,8 +6,8 @@ Read-only methods for querying market data and user information.
|
|
|
6
6
|
|
|
7
7
|
### General Info
|
|
8
8
|
|
|
9
|
-
- `all_mids()` - Retrieve mids for all coins
|
|
10
|
-
- `open_orders(user)` - Retrieve a user's open orders
|
|
9
|
+
- `all_mids(dex: nil)` - Retrieve mids for all coins (optional dex for HIP-3 perp dexs; spot mids only included with default dex)
|
|
10
|
+
- `open_orders(user, dex: nil)` - Retrieve a user's open orders (optional dex for HIP-3)
|
|
11
11
|
- `frontend_open_orders(user, dex: nil)` - Retrieve a user's open orders with additional frontend info
|
|
12
12
|
- `user_fills(user)` - Retrieve a user's fills
|
|
13
13
|
- `user_fills_by_time(user, start_time, end_time = nil)` - Retrieve a user's fills by time (optional end time)
|
|
@@ -30,6 +30,9 @@ Read-only methods for querying market data and user information.
|
|
|
30
30
|
- `delegator_summary(user)` - Query a user's staking summary
|
|
31
31
|
- `delegator_history(user)` - Query a user's staking history
|
|
32
32
|
- `delegator_rewards(user)` - Query a user's staking rewards
|
|
33
|
+
- `extra_agents(user)` - Get authorized agent addresses for a user
|
|
34
|
+
- `user_to_multi_sig_signers(user)` - Get multi-sig signer mappings for a user
|
|
35
|
+
- `user_dex_abstraction(user)` - Get dex abstraction config for a user
|
|
33
36
|
|
|
34
37
|
### Perpetuals Methods
|
|
35
38
|
|
|
@@ -97,12 +100,27 @@ Read-only methods for querying market data and user information.
|
|
|
97
100
|
- `vault_transfer(vault_address:, is_deposit:, usd:)` - Deposit or withdraw USDC to/from a vault
|
|
98
101
|
- `set_referrer(code:)` - Set referral code
|
|
99
102
|
|
|
103
|
+
### Agent & Builder
|
|
104
|
+
|
|
105
|
+
- `approve_agent(agent_address:, agent_name:)` - Authorize an agent wallet to trade on behalf of this account
|
|
106
|
+
- `approve_builder_fee(builder:, max_fee_rate:)` - Approve a builder fee rate for a builder address
|
|
107
|
+
- `token_delegate(validator:, wei:, is_undelegate:)` - Delegate or undelegate HYPE tokens to a validator
|
|
108
|
+
|
|
109
|
+
### HIP-3 DEX Abstraction
|
|
110
|
+
|
|
111
|
+
HIP-3 DEX abstraction allows automatic collateral transfers when trading on builder-deployed perpetual DEXs.
|
|
112
|
+
|
|
113
|
+
- `user_dex_abstraction(enabled:, user: nil)` - Enable or disable DEX abstraction for an account (user-signed)
|
|
114
|
+
- `agent_enable_dex_abstraction(vault_address: nil)` - Enable DEX abstraction via agent (L1 action, enable only)
|
|
115
|
+
|
|
100
116
|
### Other
|
|
101
117
|
|
|
102
118
|
- `address` - Get the wallet address associated with the private key
|
|
103
119
|
|
|
104
120
|
Order placement and management methods support an optional `vault_address:` parameter for vault trading.
|
|
105
121
|
|
|
122
|
+
Order placement methods (`order`, `bulk_orders`, `market_order`, `market_close`) support an optional `builder:` parameter for builder fee integration. The builder is a Hash with `:b` (builder address) and `:f` (fee in tenths of a basis point).
|
|
123
|
+
|
|
106
124
|
### Order Types
|
|
107
125
|
|
|
108
126
|
- `{ limit: { tif: 'Gtc' } }` - Good-til-canceled (default)
|
|
@@ -126,3 +144,51 @@ Factory methods:
|
|
|
126
144
|
- `Hyperliquid::Cloid.from_str(s)` - Create from hex string
|
|
127
145
|
- `Hyperliquid::Cloid.from_uuid(uuid)` - Create from UUID
|
|
128
146
|
- `Hyperliquid::Cloid.random` - Generate random
|
|
147
|
+
|
|
148
|
+
## WebSocket
|
|
149
|
+
|
|
150
|
+
Real-time data streaming via WebSocket. No private key required.
|
|
151
|
+
|
|
152
|
+
### Connection
|
|
153
|
+
|
|
154
|
+
- `ws.connect` - Connect to the WebSocket server (also called automatically on first `subscribe`)
|
|
155
|
+
- `ws.connected?` - Check if the WebSocket is connected
|
|
156
|
+
- `ws.close` - Disconnect and stop all background threads
|
|
157
|
+
|
|
158
|
+
### Subscriptions
|
|
159
|
+
|
|
160
|
+
- `ws.subscribe(subscription, &callback)` - Subscribe to a channel. Returns a subscription ID.
|
|
161
|
+
- `ws.unsubscribe(id)` - Unsubscribe by subscription ID. Sends unsubscribe to server when the last callback for a channel is removed.
|
|
162
|
+
|
|
163
|
+
### Lifecycle Events
|
|
164
|
+
|
|
165
|
+
- `ws.on(:open, &callback)` - Called when connection is established
|
|
166
|
+
- `ws.on(:close, &callback)` - Called when connection is closed
|
|
167
|
+
- `ws.on(:error, &callback)` - Called on connection error
|
|
168
|
+
|
|
169
|
+
### Monitoring
|
|
170
|
+
|
|
171
|
+
- `ws.dropped_message_count` - Number of messages dropped due to a full internal queue (callbacks too slow)
|
|
172
|
+
|
|
173
|
+
### Available Channels
|
|
174
|
+
|
|
175
|
+
| Channel | Subscription | Description |
|
|
176
|
+
|---------|-------------|-------------|
|
|
177
|
+
| `allMids` | `{ type: 'allMids' }` | Mid prices for all coins |
|
|
178
|
+
| `l2Book` | `{ type: 'l2Book', coin: 'ETH' }` | Level 2 order book updates |
|
|
179
|
+
| `trades` | `{ type: 'trades', coin: 'ETH' }` | Trade feed for a coin |
|
|
180
|
+
| `bbo` | `{ type: 'bbo', coin: 'ETH' }` | Best bid/offer for a coin |
|
|
181
|
+
| `candle` | `{ type: 'candle', coin: 'ETH', interval: '1m' }` | Candlestick updates |
|
|
182
|
+
| `orderUpdates` | `{ type: 'orderUpdates', user: '0x...' }` | Order status changes for a user |
|
|
183
|
+
| `userEvents` | `{ type: 'userEvents', user: '0x...' }` | All events for a user (fills, liquidations, etc.) |
|
|
184
|
+
| `userFills` | `{ type: 'userFills', user: '0x...' }` | Fill updates for a user |
|
|
185
|
+
| `userFundings` | `{ type: 'userFundings', user: '0x...' }` | Funding payments for a user |
|
|
186
|
+
|
|
187
|
+
Candle intervals: `1m`, `3m`, `5m`, `15m`, `30m`, `1h`, `2h`, `4h`, `8h`, `12h`, `1d`, `3d`, `1w`, `1M`
|
|
188
|
+
|
|
189
|
+
### Configuration
|
|
190
|
+
|
|
191
|
+
`Hyperliquid::WS::Client.new` accepts:
|
|
192
|
+
- `testnet:` (Boolean, default: false) - Use testnet WebSocket endpoint
|
|
193
|
+
- `max_queue_size:` (Integer, default: 1024) - Max messages buffered before dropping
|
|
194
|
+
- `reconnect:` (Boolean, default: true) - Auto-reconnect on unexpected disconnect
|
data/docs/EXAMPLES.md
CHANGED
|
@@ -9,12 +9,20 @@
|
|
|
9
9
|
mids = sdk.info.all_mids
|
|
10
10
|
# => { "BTC" => "50000", "ETH" => "3000", ... }
|
|
11
11
|
|
|
12
|
+
# Retrieve mids for a HIP-3 perp dex (e.g., xyz)
|
|
13
|
+
hip3_mids = sdk.info.all_mids(dex: 'xyz')
|
|
14
|
+
# => { "xyz:GOLD" => "2500", "xyz:SILVER" => "30", ... }
|
|
15
|
+
|
|
12
16
|
user_address = "0x..."
|
|
13
17
|
|
|
14
18
|
# Retrieve a user's open orders
|
|
15
19
|
orders = sdk.info.open_orders(user_address)
|
|
16
20
|
# => [{ "coin" => "BTC", "sz" => "0.1", "px" => "50000", "side" => "A" }]
|
|
17
21
|
|
|
22
|
+
# Retrieve a user's open orders on a HIP-3 dex
|
|
23
|
+
hip3_orders = sdk.info.open_orders(user_address, dex: 'xyz')
|
|
24
|
+
# => [{ "coin" => "xyz:GOLD", "sz" => "1.0", ... }]
|
|
25
|
+
|
|
18
26
|
# Retrieve a user's open orders with additional frontend info
|
|
19
27
|
frontend_orders = sdk.info.frontend_open_orders(user_address)
|
|
20
28
|
# => [{ "coin" => "BTC", "isTrigger" => false, ... }]
|
|
@@ -114,6 +122,18 @@ history = sdk.info.delegator_history(user_address)
|
|
|
114
122
|
# Query a user's staking rewards
|
|
115
123
|
rewards = sdk.info.delegator_rewards(user_address)
|
|
116
124
|
# => [{ "time" => 1_736_726_400_073, "source" => "delegation", "totalAmount" => "0.123" }, ...]
|
|
125
|
+
|
|
126
|
+
# Get authorized agent addresses for a user
|
|
127
|
+
agents = sdk.info.extra_agents(user_address)
|
|
128
|
+
# => [{ "address" => "0x...", "name" => "agent1" }, ...]
|
|
129
|
+
|
|
130
|
+
# Get multi-sig signer mappings for a user
|
|
131
|
+
signers = sdk.info.user_to_multi_sig_signers(user_address)
|
|
132
|
+
# => { "signers" => ["0x...", "0x..."], "threshold" => 2 }
|
|
133
|
+
|
|
134
|
+
# Get dex abstraction config for a user
|
|
135
|
+
dex_abstraction = sdk.info.user_dex_abstraction(user_address)
|
|
136
|
+
# => { "enabled" => true }
|
|
117
137
|
```
|
|
118
138
|
|
|
119
139
|
**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"]`.
|
|
@@ -479,6 +499,233 @@ sdk.exchange.vault_transfer(
|
|
|
479
499
|
sdk.exchange.set_referrer(code: 'MY_REFERRAL_CODE')
|
|
480
500
|
```
|
|
481
501
|
|
|
502
|
+
### Agent, Builder & Delegation
|
|
503
|
+
|
|
504
|
+
```ruby
|
|
505
|
+
# Authorize an agent wallet to trade on your behalf
|
|
506
|
+
agent_key = Eth::Key.new
|
|
507
|
+
result = sdk.exchange.approve_agent(
|
|
508
|
+
agent_address: agent_key.address.to_s,
|
|
509
|
+
agent_name: 'my-trading-bot' # optional
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
# Approve a builder fee (required before placing orders with that builder)
|
|
513
|
+
sdk.exchange.approve_builder_fee(
|
|
514
|
+
builder: '0x250F311Ae04D3CEA03443C76340069eD26C47D7D',
|
|
515
|
+
max_fee_rate: '0.01%' # 1 basis point
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
# Place an order with a builder fee
|
|
519
|
+
sdk.exchange.order(
|
|
520
|
+
coin: 'BTC',
|
|
521
|
+
is_buy: true,
|
|
522
|
+
size: '0.01',
|
|
523
|
+
limit_px: '95000',
|
|
524
|
+
builder: { b: '0x250F311Ae04D3CEA03443C76340069eD26C47D7D', f: 10 } # f=10 means 1bp
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
# Builder works with all order methods
|
|
528
|
+
sdk.exchange.bulk_orders(
|
|
529
|
+
orders: [
|
|
530
|
+
{ coin: 'BTC', is_buy: true, size: '0.01', limit_px: '94000' },
|
|
531
|
+
{ coin: 'BTC', is_buy: false, size: '0.01', limit_px: '96000' }
|
|
532
|
+
],
|
|
533
|
+
builder: { b: '0x250F311Ae04D3CEA03443C76340069eD26C47D7D', f: 10 }
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
sdk.exchange.market_order(
|
|
537
|
+
coin: 'BTC',
|
|
538
|
+
is_buy: true,
|
|
539
|
+
size: '0.01',
|
|
540
|
+
builder: { b: '0x250F311Ae04D3CEA03443C76340069eD26C47D7D', f: 10 }
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
# Delegate HYPE tokens to a validator (wei = float * 1e8)
|
|
544
|
+
sdk.exchange.token_delegate(
|
|
545
|
+
validator: '0x...',
|
|
546
|
+
wei: 100_000_000, # 1 HYPE
|
|
547
|
+
is_undelegate: false
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
# Undelegate HYPE tokens
|
|
551
|
+
sdk.exchange.token_delegate(
|
|
552
|
+
validator: '0x...',
|
|
553
|
+
wei: 10_000_000, # 0.1 HYPE
|
|
554
|
+
is_undelegate: true
|
|
555
|
+
)
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
### HIP-3 DEX Abstraction
|
|
559
|
+
|
|
560
|
+
HIP-3 DEX abstraction allows automatic collateral transfers when trading on builder-deployed perpetual DEXs.
|
|
561
|
+
|
|
562
|
+
```ruby
|
|
563
|
+
# Enable DEX abstraction for your account (user-signed action)
|
|
564
|
+
sdk.exchange.user_dex_abstraction(enabled: true)
|
|
565
|
+
|
|
566
|
+
# Disable DEX abstraction
|
|
567
|
+
sdk.exchange.user_dex_abstraction(enabled: false)
|
|
568
|
+
|
|
569
|
+
# Enable for a specific user address
|
|
570
|
+
sdk.exchange.user_dex_abstraction(enabled: true, user: '0x...')
|
|
571
|
+
|
|
572
|
+
# Enable DEX abstraction via agent (L1 action, enable only)
|
|
573
|
+
# Use this when trading as an agent on behalf of another account
|
|
574
|
+
sdk.exchange.agent_enable_dex_abstraction
|
|
575
|
+
|
|
576
|
+
# Enable DEX abstraction for a vault via agent
|
|
577
|
+
sdk.exchange.agent_enable_dex_abstraction(vault_address: '0x...')
|
|
578
|
+
|
|
579
|
+
# Check current DEX abstraction status
|
|
580
|
+
status = sdk.info.user_dex_abstraction(sdk.exchange.address)
|
|
581
|
+
# => { "enabled" => true }
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
## WebSocket
|
|
585
|
+
|
|
586
|
+
### l2Book (Order Book)
|
|
587
|
+
|
|
588
|
+
```ruby
|
|
589
|
+
sdk = Hyperliquid.new(testnet: true)
|
|
590
|
+
|
|
591
|
+
sdk.ws.on(:open) { puts 'Connected!' }
|
|
592
|
+
|
|
593
|
+
sub_id = sdk.ws.subscribe({ type: 'l2Book', coin: 'ETH' }) do |data|
|
|
594
|
+
levels = data['levels']
|
|
595
|
+
best_bid = levels[0]&.first
|
|
596
|
+
best_ask = levels[1]&.first
|
|
597
|
+
puts "ETH bid=#{best_bid['px']} ask=#{best_ask['px']}"
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
sleep 10
|
|
601
|
+
sdk.ws.unsubscribe(sub_id)
|
|
602
|
+
sdk.ws.close
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
### allMids (Mid Prices)
|
|
606
|
+
|
|
607
|
+
```ruby
|
|
608
|
+
sdk = Hyperliquid.new(testnet: true)
|
|
609
|
+
|
|
610
|
+
sdk.ws.subscribe({ type: 'allMids' }) do |data|
|
|
611
|
+
puts "BTC mid: #{data['mids']['BTC']}"
|
|
612
|
+
puts "ETH mid: #{data['mids']['ETH']}"
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
sleep 10
|
|
616
|
+
sdk.ws.close
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
### trades
|
|
620
|
+
|
|
621
|
+
```ruby
|
|
622
|
+
sdk = Hyperliquid.new(testnet: true)
|
|
623
|
+
|
|
624
|
+
sdk.ws.subscribe({ type: 'trades', coin: 'ETH' }) do |trades|
|
|
625
|
+
trades.each do |t|
|
|
626
|
+
side = t['side'] == 'B' ? 'BUY' : 'SELL'
|
|
627
|
+
puts "#{side} #{t['sz']} ETH @ #{t['px']}"
|
|
628
|
+
end
|
|
629
|
+
end
|
|
630
|
+
|
|
631
|
+
sleep 30
|
|
632
|
+
sdk.ws.close
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
### bbo (Best Bid/Offer)
|
|
636
|
+
|
|
637
|
+
```ruby
|
|
638
|
+
sdk = Hyperliquid.new(testnet: true)
|
|
639
|
+
|
|
640
|
+
sdk.ws.subscribe({ type: 'bbo', coin: 'BTC' }) do |data|
|
|
641
|
+
puts "BTC bid=#{data['bid']} ask=#{data['ask']}"
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
sleep 10
|
|
645
|
+
sdk.ws.close
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
### candle (Candlesticks)
|
|
649
|
+
|
|
650
|
+
```ruby
|
|
651
|
+
sdk = Hyperliquid.new(testnet: true)
|
|
652
|
+
|
|
653
|
+
sdk.ws.subscribe({ type: 'candle', coin: 'ETH', interval: '1m' }) do |data|
|
|
654
|
+
puts "ETH 1m candle o=#{data['o']} h=#{data['h']} l=#{data['l']} c=#{data['c']}"
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
sleep 120
|
|
658
|
+
sdk.ws.close
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
### User Channels
|
|
662
|
+
|
|
663
|
+
```ruby
|
|
664
|
+
sdk = Hyperliquid.new(testnet: true, private_key: ENV['HYPERLIQUID_PRIVATE_KEY'])
|
|
665
|
+
user = sdk.exchange.address
|
|
666
|
+
|
|
667
|
+
# Order status changes
|
|
668
|
+
sdk.ws.subscribe({ type: 'orderUpdates', user: user }) do |updates|
|
|
669
|
+
updates.each { |u| puts "Order #{u['order']['oid']}: #{u['status']}" }
|
|
670
|
+
end
|
|
671
|
+
|
|
672
|
+
# Fill notifications
|
|
673
|
+
sdk.ws.subscribe({ type: 'userFills', user: user }) do |data|
|
|
674
|
+
data['fills'].each { |f| puts "Fill: #{f['sz']} #{f['coin']} @ #{f['px']}" }
|
|
675
|
+
end
|
|
676
|
+
|
|
677
|
+
# Funding payments
|
|
678
|
+
sdk.ws.subscribe({ type: 'userFundings', user: user }) do |data|
|
|
679
|
+
puts "Funding update for #{data['user']}"
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
# All user events (fills, liquidations, etc.)
|
|
683
|
+
sdk.ws.subscribe({ type: 'userEvents', user: user }) do |data|
|
|
684
|
+
puts "User event received"
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
sleep 60
|
|
688
|
+
sdk.ws.close
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
### Multiple Subscriptions
|
|
692
|
+
|
|
693
|
+
```ruby
|
|
694
|
+
sdk = Hyperliquid.new(testnet: true)
|
|
695
|
+
|
|
696
|
+
sdk.ws.subscribe({ type: 'l2Book', coin: 'ETH' }) do |data|
|
|
697
|
+
puts "ETH book: #{data['levels'][0]&.first&.dig('px')}"
|
|
698
|
+
end
|
|
699
|
+
|
|
700
|
+
sdk.ws.subscribe({ type: 'l2Book', coin: 'BTC' }) do |data|
|
|
701
|
+
puts "BTC book: #{data['levels'][0]&.first&.dig('px')}"
|
|
702
|
+
end
|
|
703
|
+
|
|
704
|
+
sdk.ws.subscribe({ type: 'trades', coin: 'ETH' }) do |trades|
|
|
705
|
+
puts "ETH trade: #{trades.first['px']}" if trades.any?
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
sleep 10
|
|
709
|
+
sdk.ws.close
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
### Handling Reconnection
|
|
713
|
+
|
|
714
|
+
```ruby
|
|
715
|
+
sdk = Hyperliquid.new(testnet: true)
|
|
716
|
+
|
|
717
|
+
sdk.ws.on(:open) { puts 'Connected (or reconnected)!' }
|
|
718
|
+
sdk.ws.on(:close) { puts 'Connection lost. Reconnecting...' }
|
|
719
|
+
|
|
720
|
+
# Subscriptions are automatically replayed on reconnect
|
|
721
|
+
sdk.ws.subscribe({ type: 'l2Book', coin: 'ETH' }) do |data|
|
|
722
|
+
puts "ETH: #{data['levels'][0]&.first&.dig('px')}"
|
|
723
|
+
end
|
|
724
|
+
|
|
725
|
+
sleep 300
|
|
726
|
+
sdk.ws.close
|
|
727
|
+
```
|
|
728
|
+
|
|
482
729
|
### Client Order IDs (Cloid)
|
|
483
730
|
|
|
484
731
|
```ruby
|
data/docs/WS.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# WebSocket Implementation
|
|
2
|
+
|
|
3
|
+
## Architecture
|
|
4
|
+
|
|
5
|
+
`Hyperliquid::WS::Client` is a managed WebSocket client backed by three background threads:
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
WS Read Thread ──> Bounded Queue (1024) ──> Dispatch Thread ──> User Callbacks
|
|
9
|
+
Ping Thread (every 50s)
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
- **Read thread** (`ws_lite`): receives frames, parses JSON, pushes onto the queue. Never blocks on user code.
|
|
13
|
+
- **Dispatch thread** (`hl-ws-dispatch`): pops messages from the queue and invokes matching callbacks in order. If a callback is slow, only this thread blocks.
|
|
14
|
+
- **Ping thread** (`hl-ws-ping`): sends `{"method":"ping"}` every 50 seconds to keep the connection alive.
|
|
15
|
+
|
|
16
|
+
## Message Flow
|
|
17
|
+
|
|
18
|
+
1. Raw frame arrives on the read thread.
|
|
19
|
+
2. Non-JSON messages (e.g. `"Websocket connection established."`) and `pong` responses are discarded.
|
|
20
|
+
3. A channel identifier is computed from the message (e.g. `l2Book:eth`).
|
|
21
|
+
4. The message is pushed onto the bounded `Queue`. If the queue is full, the message is dropped and a warning is emitted.
|
|
22
|
+
5. The dispatch thread pops the message, looks up callbacks by identifier, and calls each one.
|
|
23
|
+
|
|
24
|
+
## Subscription Routing
|
|
25
|
+
|
|
26
|
+
Subscriptions are keyed by an identifier string derived from the channel type and its parameters:
|
|
27
|
+
|
|
28
|
+
| Channel | Identifier format | Example |
|
|
29
|
+
|----------------|------------------------------------|---------------------|
|
|
30
|
+
| `allMids` | `allMids` | `allMids` |
|
|
31
|
+
| `l2Book` | `l2Book:<coin>` | `l2Book:eth` |
|
|
32
|
+
| `candle` | `candle:<coin>:<interval>` | `candle:eth:1h` |
|
|
33
|
+
| `userEvents` | `userEvents:<user>` | `userEvents:0xabc` |
|
|
34
|
+
|
|
35
|
+
Multiple callbacks can be registered for the same identifier. The server unsubscribe message is only sent when the last callback for an identifier is removed.
|
|
36
|
+
|
|
37
|
+
## Queue Overflow
|
|
38
|
+
|
|
39
|
+
The internal queue is bounded (default 1024 messages). When full, new messages are dropped (oldest retained). Warnings print on the 1st drop and every 100th drop. Monitor via `dropped_message_count`.
|
|
40
|
+
|
|
41
|
+
## Reconnection
|
|
42
|
+
|
|
43
|
+
On unexpected disconnect (when `reconnect: true`, the default), the client spawns a thread that retries with exponential backoff: 1s, 2s, 4s, ..., capped at 30s. On reconnect, all active subscriptions are replayed automatically.
|
|
44
|
+
|
|
45
|
+
## Thread Safety
|
|
46
|
+
|
|
47
|
+
- `@subscriptions` and `@pending_subscriptions` are protected by a `Mutex` as they are accessed across the three threads.
|
|
48
|
+
- Ruby's `Queue` is inherently thread-safe.
|
|
49
|
+
- Callbacks are invoked serially on the dispatch thread (never concurrently).
|
|
@@ -11,6 +11,11 @@ module Hyperliquid
|
|
|
11
11
|
INFO_ENDPOINT = '/info'
|
|
12
12
|
EXCHANGE_ENDPOINT = '/exchange'
|
|
13
13
|
|
|
14
|
+
# WebSocket
|
|
15
|
+
WS_ENDPOINT = '/ws'
|
|
16
|
+
WS_PING_INTERVAL = 50 # seconds between pings
|
|
17
|
+
WS_MAX_QUEUE_SIZE = 1024 # max queued messages before dropping
|
|
18
|
+
|
|
14
19
|
# Request timeouts (seconds)
|
|
15
20
|
DEFAULT_TIMEOUT = 30
|
|
16
21
|
DEFAULT_READ_TIMEOUT = 30
|