hyperliquid 0.7.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 +40 -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 +103 -7
- data/lib/hyperliquid/info.rb +26 -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: 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,45 @@
|
|
|
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
|
+
|
|
3
43
|
## [0.7.0] - 2026-01-30
|
|
4
44
|
|
|
5
45
|
- 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
|
data/lib/hyperliquid/errors.rb
CHANGED
data/lib/hyperliquid/exchange.rb
CHANGED
|
@@ -119,8 +119,9 @@ module Hyperliquid
|
|
|
119
119
|
# @param builder [Hash, nil] Builder fee config { b: "0xaddress", f: fee_in_tenths_of_bp } (optional)
|
|
120
120
|
# @return [Hash] Order response
|
|
121
121
|
def market_order(coin:, is_buy:, size:, slippage: DEFAULT_SLIPPAGE, vault_address: nil, builder: nil)
|
|
122
|
-
# Get current mid price
|
|
123
|
-
|
|
122
|
+
# Get current mid price (use dex-specific endpoint for HIP-3 assets)
|
|
123
|
+
dex_prefix = extract_dex_prefix(coin)
|
|
124
|
+
mids = dex_prefix ? @info.all_mids(dex: dex_prefix) : @info.all_mids
|
|
124
125
|
mid = mids[coin]&.to_f
|
|
125
126
|
raise ArgumentError, "Unknown asset or no price available: #{coin}" unless mid&.positive?
|
|
126
127
|
|
|
@@ -354,7 +355,8 @@ module Hyperliquid
|
|
|
354
355
|
# @return [Hash] Order response
|
|
355
356
|
def market_close(coin:, size: nil, slippage: DEFAULT_SLIPPAGE, cloid: nil, vault_address: nil, builder: nil)
|
|
356
357
|
address = vault_address || @signer.address
|
|
357
|
-
|
|
358
|
+
dex_prefix = extract_dex_prefix(coin)
|
|
359
|
+
state = dex_prefix ? @info.user_state(address, dex: dex_prefix) : @info.user_state(address)
|
|
358
360
|
|
|
359
361
|
position = state['assetPositions']&.find do |pos|
|
|
360
362
|
pos.dig('position', 'coin') == coin
|
|
@@ -365,7 +367,7 @@ module Hyperliquid
|
|
|
365
367
|
is_buy = szi.negative?
|
|
366
368
|
close_size = size || szi.abs
|
|
367
369
|
|
|
368
|
-
mids = @info.all_mids
|
|
370
|
+
mids = dex_prefix ? @info.all_mids(dex: dex_prefix) : @info.all_mids
|
|
369
371
|
mid = mids[coin]&.to_f
|
|
370
372
|
raise ArgumentError, "Unknown asset or no price available: #{coin}" unless mid&.positive?
|
|
371
373
|
|
|
@@ -650,6 +652,45 @@ module Hyperliquid
|
|
|
650
652
|
post_action(action, signature, nonce, nil)
|
|
651
653
|
end
|
|
652
654
|
|
|
655
|
+
# Enable or disable HIP-3 DEX abstraction for automatic collateral transfers
|
|
656
|
+
# When enabled, collateral is automatically transferred to HIP-3 dexes when trading
|
|
657
|
+
# @param enabled [Boolean] True to enable, false to disable DEX abstraction
|
|
658
|
+
# @param user [String, nil] User address (defaults to signer address)
|
|
659
|
+
# @return [Hash] User DEX abstraction response
|
|
660
|
+
def user_dex_abstraction(enabled:, user: nil)
|
|
661
|
+
nonce = timestamp_ms
|
|
662
|
+
user_address = user || @signer.address
|
|
663
|
+
action = {
|
|
664
|
+
type: 'userDexAbstraction',
|
|
665
|
+
signatureChainId: '0x66eee',
|
|
666
|
+
hyperliquidChain: Signing::EIP712.hyperliquid_chain(testnet: @testnet),
|
|
667
|
+
user: user_address,
|
|
668
|
+
enabled: enabled,
|
|
669
|
+
nonce: nonce
|
|
670
|
+
}
|
|
671
|
+
signature = @signer.sign_user_signed_action(
|
|
672
|
+
{ user: user_address, enabled: enabled, nonce: nonce },
|
|
673
|
+
'HyperliquidTransaction:UserDexAbstraction',
|
|
674
|
+
Signing::EIP712::USER_DEX_ABSTRACTION_TYPES
|
|
675
|
+
)
|
|
676
|
+
post_action(action, signature, nonce, nil)
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
# Enable HIP-3 DEX abstraction via agent (L1 action, enable only)
|
|
680
|
+
# This allows agents to enable DEX abstraction for the account they're trading on behalf of
|
|
681
|
+
# @param vault_address [String, nil] Vault address if trading on behalf of a vault
|
|
682
|
+
# @return [Hash] Agent enable DEX abstraction response
|
|
683
|
+
def agent_enable_dex_abstraction(vault_address: nil)
|
|
684
|
+
nonce = timestamp_ms
|
|
685
|
+
action = { type: 'agentEnableDexAbstraction' }
|
|
686
|
+
signature = @signer.sign_l1_action(
|
|
687
|
+
action, nonce,
|
|
688
|
+
vault_address: vault_address,
|
|
689
|
+
expires_after: @expires_after
|
|
690
|
+
)
|
|
691
|
+
post_action(action, signature, nonce, vault_address)
|
|
692
|
+
end
|
|
693
|
+
|
|
653
694
|
# Clear the asset metadata cache
|
|
654
695
|
# Call this if metadata has been updated
|
|
655
696
|
def reload_metadata!
|
|
@@ -678,26 +719,54 @@ module Hyperliquid
|
|
|
678
719
|
end
|
|
679
720
|
|
|
680
721
|
# Get asset index for a coin symbol
|
|
681
|
-
# @param coin [String] Asset symbol
|
|
722
|
+
# @param coin [String] Asset symbol (supports HIP-3 prefixed names like "xyz:GOLD")
|
|
682
723
|
# @return [Integer] Asset index
|
|
683
724
|
def asset_index(coin)
|
|
684
725
|
load_asset_cache unless @asset_cache
|
|
726
|
+
|
|
727
|
+
# If not found and has a dex prefix (e.g., "xyz:GOLD"), try loading that dex
|
|
728
|
+
unless @asset_cache[:indices][coin]
|
|
729
|
+
dex_prefix = extract_dex_prefix(coin)
|
|
730
|
+
load_hip3_dex_cache(dex_prefix) if dex_prefix && !@loaded_dexes&.include?(dex_prefix)
|
|
731
|
+
end
|
|
732
|
+
|
|
685
733
|
@asset_cache[:indices][coin] || raise(ArgumentError, "Unknown asset: #{coin}")
|
|
686
734
|
end
|
|
687
735
|
|
|
688
736
|
# Get asset metadata for a coin symbol
|
|
689
|
-
# @param coin [String] Asset symbol
|
|
737
|
+
# @param coin [String] Asset symbol (supports HIP-3 prefixed names like "xyz:GOLD")
|
|
690
738
|
# @return [Hash] Asset metadata with :sz_decimals and :is_spot
|
|
691
739
|
def asset_metadata(coin)
|
|
692
740
|
load_asset_cache unless @asset_cache
|
|
741
|
+
|
|
742
|
+
# If not found and has a dex prefix, try loading that dex
|
|
743
|
+
unless @asset_cache[:metadata][coin]
|
|
744
|
+
dex_prefix = extract_dex_prefix(coin)
|
|
745
|
+
load_hip3_dex_cache(dex_prefix) if dex_prefix && !@loaded_dexes&.include?(dex_prefix)
|
|
746
|
+
end
|
|
747
|
+
|
|
693
748
|
@asset_cache[:metadata][coin] || raise(ArgumentError, "Unknown asset: #{coin}")
|
|
694
749
|
end
|
|
695
750
|
|
|
751
|
+
# Extract dex prefix from a coin name (e.g., "xyz:GOLD" -> "xyz")
|
|
752
|
+
# @param coin [String] Coin name
|
|
753
|
+
# @return [String, nil] Dex prefix or nil if no prefix
|
|
754
|
+
def extract_dex_prefix(coin)
|
|
755
|
+
return nil unless coin.include?(':')
|
|
756
|
+
|
|
757
|
+
prefix = coin.split(':').first
|
|
758
|
+
# Spot pairs like "PURR/USDC" don't count as dex prefixes
|
|
759
|
+
return nil if prefix.include?('/')
|
|
760
|
+
|
|
761
|
+
prefix
|
|
762
|
+
end
|
|
763
|
+
|
|
696
764
|
# Load asset metadata from Info API (perps and spot)
|
|
697
765
|
def load_asset_cache
|
|
698
766
|
@asset_cache = { indices: {}, metadata: {} }
|
|
767
|
+
@loaded_dexes = Set.new
|
|
699
768
|
|
|
700
|
-
# Load perpetual assets
|
|
769
|
+
# Load perpetual assets from default dex
|
|
701
770
|
meta = @info.meta
|
|
702
771
|
meta['universe'].each_with_index do |asset, index|
|
|
703
772
|
name = asset['name']
|
|
@@ -721,6 +790,33 @@ module Hyperliquid
|
|
|
721
790
|
end
|
|
722
791
|
end
|
|
723
792
|
|
|
793
|
+
# Load asset metadata for a HIP-3 dex
|
|
794
|
+
# HIP-3 asset IDs use formula: 100000 + perp_dex_index * 10000 + index_in_meta
|
|
795
|
+
# @param dex [String] Dex name (e.g., "xyz")
|
|
796
|
+
def load_hip3_dex_cache(dex)
|
|
797
|
+
return if @loaded_dexes.include?(dex)
|
|
798
|
+
|
|
799
|
+
@loaded_dexes.add(dex)
|
|
800
|
+
|
|
801
|
+
# Get perp_dex_index from perp_dexs list (position in array)
|
|
802
|
+
perp_dexs = @info.perp_dexs
|
|
803
|
+
perp_dex_index = perp_dexs.index { |d| d && d['name'] == dex }
|
|
804
|
+
return unless perp_dex_index
|
|
805
|
+
|
|
806
|
+
meta = @info.meta(dex: dex)
|
|
807
|
+
meta['universe']&.each_with_index do |asset, index|
|
|
808
|
+
name = asset['name']
|
|
809
|
+
# HIP-3 asset ID: 100000 + perp_dex_index * 10000 + index_in_meta
|
|
810
|
+
hip3_asset_id = 100_000 + (perp_dex_index * 10_000) + index
|
|
811
|
+
@asset_cache[:indices][name] = hip3_asset_id
|
|
812
|
+
@asset_cache[:metadata][name] = {
|
|
813
|
+
sz_decimals: asset['szDecimals'],
|
|
814
|
+
is_spot: false,
|
|
815
|
+
dex: dex
|
|
816
|
+
}
|
|
817
|
+
end
|
|
818
|
+
end
|
|
819
|
+
|
|
724
820
|
# Convert float to wire format (string representation)
|
|
725
821
|
# Maintains parity with official Python SDK
|
|
726
822
|
# - 8 decimal precision
|
data/lib/hyperliquid/info.rb
CHANGED
|
@@ -12,9 +12,12 @@ module Hyperliquid
|
|
|
12
12
|
# ============================
|
|
13
13
|
|
|
14
14
|
# Get all market mid prices
|
|
15
|
+
# @param dex [String, nil] Optional perp dex name (defaults to first perp dex; spot mids only included with first perp dex)
|
|
15
16
|
# @return [Hash] Hash containing mid prices for all markets
|
|
16
|
-
def all_mids
|
|
17
|
-
|
|
17
|
+
def all_mids(dex: nil)
|
|
18
|
+
body = { type: 'allMids' }
|
|
19
|
+
body[:dex] = dex if dex
|
|
20
|
+
@client.post(Constants::INFO_ENDPOINT, body)
|
|
18
21
|
end
|
|
19
22
|
|
|
20
23
|
# Get a user's open orders
|
|
@@ -215,6 +218,27 @@ module Hyperliquid
|
|
|
215
218
|
@client.post(Constants::INFO_ENDPOINT, { type: 'delegatorRewards', user: user })
|
|
216
219
|
end
|
|
217
220
|
|
|
221
|
+
# Get authorized agent addresses for a user
|
|
222
|
+
# @param user [String] Wallet address
|
|
223
|
+
# @return [Array] Array of authorized agent addresses
|
|
224
|
+
def extra_agents(user)
|
|
225
|
+
@client.post(Constants::INFO_ENDPOINT, { type: 'extraAgents', user: user })
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# Get multi-sig signer mappings for a user
|
|
229
|
+
# @param user [String] Multi-sig wallet address
|
|
230
|
+
# @return [Hash] Multi-sig signer information
|
|
231
|
+
def user_to_multi_sig_signers(user)
|
|
232
|
+
@client.post(Constants::INFO_ENDPOINT, { type: 'userToMultiSigSigners', user: user })
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Get dex abstraction config for a user
|
|
236
|
+
# @param user [String] Wallet address
|
|
237
|
+
# @return [Hash] Dex abstraction configuration
|
|
238
|
+
def user_dex_abstraction(user)
|
|
239
|
+
@client.post(Constants::INFO_ENDPOINT, { type: 'userDexAbstraction', user: user })
|
|
240
|
+
end
|
|
241
|
+
|
|
218
242
|
# ============================
|
|
219
243
|
# Info: Perpetuals
|
|
220
244
|
# ============================
|