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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 67b19ec4599a62be35288947fdc8ce2a98c6a0f012f4bdb055a58bd9e4a37849
4
- data.tar.gz: 58470f99ebf59a1ba95936816c887210d5937240c55ea1b6e85f7d68b0d82a37
3
+ metadata.gz: 7c9ed1fef8ad29ab90cbc1ba13a6dc7ceed9267ff98ec97ed6748f71e1ff8289
4
+ data.tar.gz: 10268ae8868d6b46a1a91a853c80f5009b7586d8bed449488ec1db2923a6a3ad
5
5
  SHA512:
6
- metadata.gz: fe341138c6085166b2ddd6e87fd7458554e178c4361e9ad16ed7ab1d239333c821dc0556d7f86d9080ec2002c3c36f26b3f32282acc2b6ccc010b85f83c83abe
7
- data.tar.gz: caaefe028d2a4727c01daeea7241290af00c8676d73c2371ce360279f4dc0c18337c1daa398ab968416ee2516f02d08882b0556d91b5e5e78d318bfbd2d7531f
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
- 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
 
@@ -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
@@ -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
@@ -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
- mids = @info.all_mids
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
- state = @info.user_state(address)
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
@@ -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
- @client.post(Constants::INFO_ENDPOINT, { type: 'allMids' })
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
  # ============================