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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 324ff762cbf5edcea7fc532f853b7295ffcd34476f6273fdef4e03bcc0f9eaee
4
- data.tar.gz: 98b1b88b378bb8ae010301c656c833282cd406822fa0c8a7d6c134911b0f2cce
3
+ metadata.gz: 7c9ed1fef8ad29ab90cbc1ba13a6dc7ceed9267ff98ec97ed6748f71e1ff8289
4
+ data.tar.gz: 10268ae8868d6b46a1a91a853c80f5009b7586d8bed449488ec1db2923a6a3ad
5
5
  SHA512:
6
- metadata.gz: fe9e447e5500312f98e8f8e602071372847963daacfd51475e92d4db5a7bc988546e4cf106479a9c899bcecf113612c2aa9ebe0dd50a5384bd438bb0d6429865
7
- data.tar.gz: 7cd7d2e048c4bf25e128dd7fd41814621bd95305d613199cb4c41abdf1901fdc4c665635000112aafeb7c395dbf01dae81cba74042557d86643c6ac169e3d201
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
- The SDK supports both read operations (Info API) and authenticated write operations (Exchange API) for trading.
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
@@ -35,4 +35,7 @@ module Hyperliquid
35
35
 
36
36
  # Error for network connectivity issues
37
37
  class NetworkError < Error; end
38
+
39
+ # Error for WebSocket issues
40
+ class WebSocketError < Error; end
38
41
  end