hyperliquid 0.7.0 → 1.0.1
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 +3 -1
- data/CHANGELOG.md +44 -0
- data/README.md +2 -1
- data/docs/API.md +60 -2
- data/docs/EXAMPLES.md +191 -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 +129 -43
- data/lib/hyperliquid/info.rb +27 -2
- data/lib/hyperliquid/signing/eip712.rb +9 -0
- data/lib/hyperliquid/version.rb +1 -1
- data/lib/hyperliquid/ws/client.rb +340 -0
- data/lib/hyperliquid.rb +3 -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 +2 -0
- data/scripts/test_11_builder_fee.rb +1 -0
- data/scripts/test_12_staking.rb +2 -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 +3 -1
- data/scripts/test_helpers.rb +131 -1
- metadata +19 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1e1cb7c8ae1c1b83ab34eaba6f6bf9ce34b620ca1af4500912d62e54d75a95ab
|
|
4
|
+
data.tar.gz: 9d026ad803f2fe0d2983c3dd242ee5d901cf56677e7c32cf756b5da38eb39c68
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 99a8f5cf7847c9236f3b4b48cfb83734ecb6ae8b7679d8d5cc1aef003b1a510df3edefdf9f6481d7bdb4ff7d22546549247b9aa17771e95d58307eb93bdd0448
|
|
7
|
+
data.tar.gz: d649b243e0aab4ae25ffcd3c452878cce1be94dd86dde1aeb151b9478bba1c42fe534ec485f4ddd896e0f358b705d4b063180b26e23fcf7d09d42a9f71ffbc64
|
data/.rubocop.yml
CHANGED
|
@@ -5,6 +5,7 @@ AllCops:
|
|
|
5
5
|
Exclude:
|
|
6
6
|
- 'test_*.rb' # Exclude ad-hoc integration test scripts
|
|
7
7
|
- 'scripts/**/*' # Exclude integration test scripts
|
|
8
|
+
- 'local/**/*' # Exclude local test scripts
|
|
8
9
|
- 'vendor/**/*' # Exclude vendored gems (CI bundles here)
|
|
9
10
|
|
|
10
11
|
# Allow longer methods for complex logic
|
|
@@ -30,10 +31,11 @@ Metrics/ParameterLists:
|
|
|
30
31
|
Exclude:
|
|
31
32
|
- 'lib/hyperliquid/exchange.rb'
|
|
32
33
|
|
|
33
|
-
# Allow higher complexity for order type conversion logic
|
|
34
|
+
# Allow higher complexity for order type conversion logic and WS message handling
|
|
34
35
|
Metrics/CyclomaticComplexity:
|
|
35
36
|
Exclude:
|
|
36
37
|
- 'lib/hyperliquid/exchange.rb'
|
|
38
|
+
- 'lib/hyperliquid/ws/client.rb'
|
|
37
39
|
|
|
38
40
|
Metrics/PerceivedComplexity:
|
|
39
41
|
Exclude:
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
## [Ruby Hyperliquid SDK Changelog]
|
|
2
2
|
|
|
3
|
+
## [1.0.1] - 2026-02-03
|
|
4
|
+
|
|
5
|
+
- Minor refactors to reduce method complexity
|
|
6
|
+
|
|
7
|
+
## [1.0.0] - 2026-02-03
|
|
8
|
+
|
|
9
|
+
### WebSocket Support
|
|
10
|
+
|
|
11
|
+
- Add real-time WebSocket client (`Hyperliquid::WS::Client`) with managed connection
|
|
12
|
+
- Three-thread architecture: read thread, dispatch thread, ping thread
|
|
13
|
+
- Automatic reconnection with exponential backoff (1s, 2s, 4s, ..., 30s cap)
|
|
14
|
+
- Heartbeat ping every 50 seconds to keep connection alive
|
|
15
|
+
- Bounded message queue (1024) with overflow detection
|
|
16
|
+
- Lifecycle callbacks: `on(:open)`, `on(:close)`, `on(:error)`
|
|
17
|
+
|
|
18
|
+
- Add 9 WebSocket subscription channels
|
|
19
|
+
- `allMids` — mid prices for all coins
|
|
20
|
+
- `l2Book` — level 2 order book updates
|
|
21
|
+
- `trades` — trade feed for a coin
|
|
22
|
+
- `bbo` — best bid/offer for a coin
|
|
23
|
+
- `candle` — candlestick updates
|
|
24
|
+
- `orderUpdates` — order status changes for a user
|
|
25
|
+
- `userEvents` — all events for a user (fills, liquidations, etc.)
|
|
26
|
+
- `userFills` — fill updates for a user
|
|
27
|
+
- `userFundings` — funding payments for a user
|
|
28
|
+
|
|
29
|
+
### HIP-3 Support
|
|
30
|
+
|
|
31
|
+
- Add HIP-3 DEX abstraction Exchange actions
|
|
32
|
+
- `user_dex_abstraction` — enable/disable DEX abstraction for automatic collateral transfers (user-signed)
|
|
33
|
+
- `agent_enable_dex_abstraction` — enable DEX abstraction via agent (L1 action, enable only)
|
|
34
|
+
- Add full HIP-3 trading support
|
|
35
|
+
- Lazy loading of HIP-3 dex asset metadata when trading prefixed coins (e.g., `xyz:GOLD`)
|
|
36
|
+
- Correct HIP-3 asset ID calculation: `100000 + perp_dex_index * 10000 + index_in_meta`
|
|
37
|
+
- `market_order` and `market_close` automatically use dex-specific price endpoints
|
|
38
|
+
- Add `dex:` parameter to `all_mids` Info endpoint for HIP-3 perp dex prices
|
|
39
|
+
|
|
40
|
+
### Info API
|
|
41
|
+
|
|
42
|
+
- Add 3 more Info endpoints
|
|
43
|
+
- `extra_agents` — get authorized agent addresses for a user
|
|
44
|
+
- `user_to_multi_sig_signers` — get multi-sig signer mappings for a user
|
|
45
|
+
- `user_dex_abstraction` — get dex abstraction config for a user
|
|
46
|
+
|
|
3
47
|
## [0.7.0] - 2026-01-30
|
|
4
48
|
|
|
5
49
|
- Add agent, builder & delegation actions 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
|
|
|
@@ -103,6 +106,13 @@ Read-only methods for querying market data and user information.
|
|
|
103
106
|
- `approve_builder_fee(builder:, max_fee_rate:)` - Approve a builder fee rate for a builder address
|
|
104
107
|
- `token_delegate(validator:, wei:, is_undelegate:)` - Delegate or undelegate HYPE tokens to a validator
|
|
105
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
|
+
|
|
106
116
|
### Other
|
|
107
117
|
|
|
108
118
|
- `address` - Get the wallet address associated with the private key
|
|
@@ -134,3 +144,51 @@ Factory methods:
|
|
|
134
144
|
- `Hyperliquid::Cloid.from_str(s)` - Create from hex string
|
|
135
145
|
- `Hyperliquid::Cloid.from_uuid(uuid)` - Create from UUID
|
|
136
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"]`.
|
|
@@ -535,6 +555,177 @@ sdk.exchange.token_delegate(
|
|
|
535
555
|
)
|
|
536
556
|
```
|
|
537
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
|
+
|
|
538
729
|
### Client Order IDs (Cloid)
|
|
539
730
|
|
|
540
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
|