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.
@@ -16,11 +16,13 @@ module Hyperliquid
16
16
  # @param client [Hyperliquid::Client] HTTP client
17
17
  # @param signer [Hyperliquid::Signing::Signer] EIP-712 signer
18
18
  # @param info [Hyperliquid::Info] Info API client for metadata
19
+ # @param testnet [Boolean] Whether targeting testnet (default: false)
19
20
  # @param expires_after [Integer, nil] Optional global expiration timestamp
20
- def initialize(client:, signer:, info:, expires_after: nil)
21
+ def initialize(client:, signer:, info:, testnet: false, expires_after: nil)
21
22
  @client = client
22
23
  @signer = signer
23
24
  @info = info
25
+ @testnet = testnet
24
26
  @expires_after = expires_after
25
27
  @asset_cache = nil
26
28
  end
@@ -40,9 +42,10 @@ module Hyperliquid
40
42
  # @param reduce_only [Boolean] Reduce-only flag (default: false)
41
43
  # @param cloid [Cloid, String, nil] Client order ID (optional)
42
44
  # @param vault_address [String, nil] Vault address for vault trading (optional)
45
+ # @param builder [Hash, nil] Builder fee config { b: "0xaddress", f: fee_in_tenths_of_bp } (optional)
43
46
  # @return [Hash] Order response
44
47
  def order(coin:, is_buy:, size:, limit_px:, order_type: { limit: { tif: 'Gtc' } },
45
- reduce_only: false, cloid: nil, vault_address: nil)
48
+ reduce_only: false, cloid: nil, vault_address: nil, builder: nil)
46
49
  nonce = timestamp_ms
47
50
 
48
51
  order_wire = build_order_wire(
@@ -60,6 +63,7 @@ module Hyperliquid
60
63
  orders: [order_wire],
61
64
  grouping: 'na'
62
65
  }
66
+ action[:builder] = normalize_builder(builder) if builder
63
67
 
64
68
  signature = @signer.sign_l1_action(
65
69
  action, nonce,
@@ -74,8 +78,9 @@ module Hyperliquid
74
78
  # :coin, :is_buy, :size, :limit_px, :order_type, :reduce_only, :cloid
75
79
  # @param grouping [String] Order grouping ("na", "normalTpsl", "positionTpsl")
76
80
  # @param vault_address [String, nil] Vault address for vault trading (optional)
81
+ # @param builder [Hash, nil] Builder fee config { b: "0xaddress", f: fee_in_tenths_of_bp } (optional)
77
82
  # @return [Hash] Bulk order response
78
- def bulk_orders(orders:, grouping: 'na', vault_address: nil)
83
+ def bulk_orders(orders:, grouping: 'na', vault_address: nil, builder: nil)
79
84
  nonce = timestamp_ms
80
85
 
81
86
  order_wires = orders.map do |o|
@@ -95,6 +100,7 @@ module Hyperliquid
95
100
  orders: order_wires,
96
101
  grouping: grouping
97
102
  }
103
+ action[:builder] = normalize_builder(builder) if builder
98
104
 
99
105
  signature = @signer.sign_l1_action(
100
106
  action, nonce,
@@ -110,10 +116,12 @@ module Hyperliquid
110
116
  # @param size [String, Numeric] Order size
111
117
  # @param slippage [Float] Slippage tolerance (default: 0.05 = 5%)
112
118
  # @param vault_address [String, nil] Vault address for vault trading (optional)
119
+ # @param builder [Hash, nil] Builder fee config { b: "0xaddress", f: fee_in_tenths_of_bp } (optional)
113
120
  # @return [Hash] Order response
114
- def market_order(coin:, is_buy:, size:, slippage: DEFAULT_SLIPPAGE, vault_address: nil)
115
- # Get current mid price
116
- mids = @info.all_mids
121
+ def market_order(coin:, is_buy:, size:, slippage: DEFAULT_SLIPPAGE, vault_address: nil, builder: nil)
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
117
125
  mid = mids[coin]&.to_f
118
126
  raise ArgumentError, "Unknown asset or no price available: #{coin}" unless mid&.positive?
119
127
 
@@ -126,7 +134,8 @@ module Hyperliquid
126
134
  size: size,
127
135
  limit_px: slippage_price,
128
136
  order_type: { limit: { tif: 'Ioc' } },
129
- vault_address: vault_address
137
+ vault_address: vault_address,
138
+ builder: builder
130
139
  )
131
140
  end
132
141
 
@@ -342,10 +351,12 @@ module Hyperliquid
342
351
  # @param slippage [Float] Slippage tolerance (default: 5%)
343
352
  # @param cloid [Cloid, String, nil] Client order ID (optional)
344
353
  # @param vault_address [String, nil] Vault address for vault trading (optional)
354
+ # @param builder [Hash, nil] Builder fee config { b: "0xaddress", f: fee_in_tenths_of_bp } (optional)
345
355
  # @return [Hash] Order response
346
- def market_close(coin:, size: nil, slippage: DEFAULT_SLIPPAGE, cloid: nil, vault_address: nil)
356
+ def market_close(coin:, size: nil, slippage: DEFAULT_SLIPPAGE, cloid: nil, vault_address: nil, builder: nil)
347
357
  address = vault_address || @signer.address
348
- 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)
349
360
 
350
361
  position = state['assetPositions']&.find do |pos|
351
362
  pos.dig('position', 'coin') == coin
@@ -356,7 +367,7 @@ module Hyperliquid
356
367
  is_buy = szi.negative?
357
368
  close_size = size || szi.abs
358
369
 
359
- mids = @info.all_mids
370
+ mids = dex_prefix ? @info.all_mids(dex: dex_prefix) : @info.all_mids
360
371
  mid = mids[coin]&.to_f
361
372
  raise ArgumentError, "Unknown asset or no price available: #{coin}" unless mid&.positive?
362
373
 
@@ -370,7 +381,8 @@ module Hyperliquid
370
381
  order_type: { limit: { tif: 'Ioc' } },
371
382
  reduce_only: true,
372
383
  cloid: cloid,
373
- vault_address: vault_address
384
+ vault_address: vault_address,
385
+ builder: builder
374
386
  )
375
387
  end
376
388
 
@@ -383,7 +395,7 @@ module Hyperliquid
383
395
  action = {
384
396
  type: 'usdSend',
385
397
  signatureChainId: '0x66eee',
386
- hyperliquidChain: Signing::EIP712.hyperliquid_chain(testnet: @signer.instance_variable_get(:@testnet)),
398
+ hyperliquidChain: Signing::EIP712.hyperliquid_chain(testnet: @testnet),
387
399
  destination: destination,
388
400
  amount: amount.to_s,
389
401
  time: nonce
@@ -406,7 +418,7 @@ module Hyperliquid
406
418
  action = {
407
419
  type: 'spotSend',
408
420
  signatureChainId: '0x66eee',
409
- hyperliquidChain: Signing::EIP712.hyperliquid_chain(testnet: @signer.instance_variable_get(:@testnet)),
421
+ hyperliquidChain: Signing::EIP712.hyperliquid_chain(testnet: @testnet),
410
422
  destination: destination,
411
423
  token: token,
412
424
  amount: amount.to_s,
@@ -429,7 +441,7 @@ module Hyperliquid
429
441
  action = {
430
442
  type: 'usdClassTransfer',
431
443
  signatureChainId: '0x66eee',
432
- hyperliquidChain: Signing::EIP712.hyperliquid_chain(testnet: @signer.instance_variable_get(:@testnet)),
444
+ hyperliquidChain: Signing::EIP712.hyperliquid_chain(testnet: @testnet),
433
445
  amount: amount.to_s,
434
446
  toPerp: to_perp,
435
447
  nonce: nonce
@@ -451,7 +463,7 @@ module Hyperliquid
451
463
  action = {
452
464
  type: 'withdraw3',
453
465
  signatureChainId: '0x66eee',
454
- hyperliquidChain: Signing::EIP712.hyperliquid_chain(testnet: @signer.instance_variable_get(:@testnet)),
466
+ hyperliquidChain: Signing::EIP712.hyperliquid_chain(testnet: @testnet),
455
467
  destination: destination,
456
468
  amount: amount.to_s,
457
469
  time: nonce
@@ -476,7 +488,7 @@ module Hyperliquid
476
488
  action = {
477
489
  type: 'sendAsset',
478
490
  signatureChainId: '0x66eee',
479
- hyperliquidChain: Signing::EIP712.hyperliquid_chain(testnet: @signer.instance_variable_get(:@testnet)),
491
+ hyperliquidChain: Signing::EIP712.hyperliquid_chain(testnet: @testnet),
480
492
  destination: destination,
481
493
  sourceDex: source_dex,
482
494
  destinationDex: destination_dex,
@@ -569,6 +581,116 @@ module Hyperliquid
569
581
  post_action(action, signature, nonce, nil)
570
582
  end
571
583
 
584
+ # Authorize an agent wallet to trade on behalf of this account
585
+ # @param agent_address [String] Agent's Ethereum address
586
+ # @param agent_name [String, nil] Optional agent name (omitted from action if nil)
587
+ # @return [Hash] Approve agent response
588
+ def approve_agent(agent_address:, agent_name: nil)
589
+ nonce = timestamp_ms
590
+ action = {
591
+ type: 'approveAgent',
592
+ signatureChainId: '0x66eee',
593
+ hyperliquidChain: Signing::EIP712.hyperliquid_chain(testnet: @testnet),
594
+ agentAddress: agent_address,
595
+ nonce: nonce
596
+ }
597
+ # agentName is always included in the signed message (empty string if nil),
598
+ # but only included in the posted action if a name was provided (matches Python SDK)
599
+ action[:agentName] = agent_name if agent_name
600
+ signature = @signer.sign_user_signed_action(
601
+ { agentAddress: agent_address, agentName: agent_name || '', nonce: nonce },
602
+ 'HyperliquidTransaction:ApproveAgent',
603
+ Signing::EIP712::APPROVE_AGENT_TYPES
604
+ )
605
+ post_action(action, signature, nonce, nil)
606
+ end
607
+
608
+ # Approve a builder fee rate for a builder address
609
+ # Users must approve a builder before orders with that builder can be placed.
610
+ # @param builder [String] Builder's Ethereum address
611
+ # @param max_fee_rate [String] Maximum fee rate (e.g., "0.01%" for 1 basis point)
612
+ # @return [Hash] Approve builder fee response
613
+ def approve_builder_fee(builder:, max_fee_rate:)
614
+ nonce = timestamp_ms
615
+ action = {
616
+ type: 'approveBuilderFee',
617
+ signatureChainId: '0x66eee',
618
+ hyperliquidChain: Signing::EIP712.hyperliquid_chain(testnet: @testnet),
619
+ maxFeeRate: max_fee_rate,
620
+ builder: builder,
621
+ nonce: nonce
622
+ }
623
+ signature = @signer.sign_user_signed_action(
624
+ { maxFeeRate: max_fee_rate, builder: builder, nonce: nonce },
625
+ 'HyperliquidTransaction:ApproveBuilderFee',
626
+ Signing::EIP712::APPROVE_BUILDER_FEE_TYPES
627
+ )
628
+ post_action(action, signature, nonce, nil)
629
+ end
630
+
631
+ # Delegate or undelegate HYPE tokens to a validator
632
+ # @param validator [String] Validator's Ethereum address
633
+ # @param wei [Integer] Amount as float * 1e8 (e.g., 1 HYPE = 100_000_000)
634
+ # @param is_undelegate [Boolean] True to undelegate, false to delegate
635
+ # @return [Hash] Token delegate response
636
+ def token_delegate(validator:, wei:, is_undelegate:)
637
+ nonce = timestamp_ms
638
+ action = {
639
+ type: 'tokenDelegate',
640
+ signatureChainId: '0x66eee',
641
+ hyperliquidChain: Signing::EIP712.hyperliquid_chain(testnet: @testnet),
642
+ validator: validator,
643
+ wei: wei,
644
+ isUndelegate: is_undelegate,
645
+ nonce: nonce
646
+ }
647
+ signature = @signer.sign_user_signed_action(
648
+ { validator: validator, wei: wei, isUndelegate: is_undelegate, nonce: nonce },
649
+ 'HyperliquidTransaction:TokenDelegate',
650
+ Signing::EIP712::TOKEN_DELEGATE_TYPES
651
+ )
652
+ post_action(action, signature, nonce, nil)
653
+ end
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
+
572
694
  # Clear the asset metadata cache
573
695
  # Call this if metadata has been updated
574
696
  def reload_metadata!
@@ -597,26 +719,54 @@ module Hyperliquid
597
719
  end
598
720
 
599
721
  # Get asset index for a coin symbol
600
- # @param coin [String] Asset symbol
722
+ # @param coin [String] Asset symbol (supports HIP-3 prefixed names like "xyz:GOLD")
601
723
  # @return [Integer] Asset index
602
724
  def asset_index(coin)
603
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
+
604
733
  @asset_cache[:indices][coin] || raise(ArgumentError, "Unknown asset: #{coin}")
605
734
  end
606
735
 
607
736
  # Get asset metadata for a coin symbol
608
- # @param coin [String] Asset symbol
737
+ # @param coin [String] Asset symbol (supports HIP-3 prefixed names like "xyz:GOLD")
609
738
  # @return [Hash] Asset metadata with :sz_decimals and :is_spot
610
739
  def asset_metadata(coin)
611
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
+
612
748
  @asset_cache[:metadata][coin] || raise(ArgumentError, "Unknown asset: #{coin}")
613
749
  end
614
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
+
615
764
  # Load asset metadata from Info API (perps and spot)
616
765
  def load_asset_cache
617
766
  @asset_cache = { indices: {}, metadata: {} }
767
+ @loaded_dexes = Set.new
618
768
 
619
- # Load perpetual assets
769
+ # Load perpetual assets from default dex
620
770
  meta = @info.meta
621
771
  meta['universe'].each_with_index do |asset, index|
622
772
  name = asset['name']
@@ -640,6 +790,33 @@ module Hyperliquid
640
790
  end
641
791
  end
642
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
+
643
820
  # Convert float to wire format (string representation)
644
821
  # Maintains parity with official Python SDK
645
822
  # - 8 decimal precision
@@ -745,6 +922,13 @@ module Hyperliquid
745
922
  end
746
923
  end
747
924
 
925
+ # Normalize builder fee config for inclusion in action payload
926
+ # @param builder [Hash] Builder config with :b (address) and :f (fee)
927
+ # @return [Hash] Normalized builder config with lowercased address
928
+ def normalize_builder(builder)
929
+ { b: builder[:b].downcase, f: builder[:f] }
930
+ end
931
+
748
932
  # Convert order type to wire format
749
933
  # @param order_type [Hash] Order type configuration
750
934
  # @return [Hash] Wire format order type
@@ -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
  # ============================
@@ -67,6 +67,43 @@ module Hyperliquid
67
67
  ]
68
68
  }.freeze
69
69
 
70
+ APPROVE_AGENT_TYPES = {
71
+ 'HyperliquidTransaction:ApproveAgent': [
72
+ { name: :hyperliquidChain, type: 'string' },
73
+ { name: :agentAddress, type: 'address' },
74
+ { name: :agentName, type: 'string' },
75
+ { name: :nonce, type: 'uint64' }
76
+ ]
77
+ }.freeze
78
+
79
+ APPROVE_BUILDER_FEE_TYPES = {
80
+ 'HyperliquidTransaction:ApproveBuilderFee': [
81
+ { name: :hyperliquidChain, type: 'string' },
82
+ { name: :maxFeeRate, type: 'string' },
83
+ { name: :builder, type: 'address' },
84
+ { name: :nonce, type: 'uint64' }
85
+ ]
86
+ }.freeze
87
+
88
+ TOKEN_DELEGATE_TYPES = {
89
+ 'HyperliquidTransaction:TokenDelegate': [
90
+ { name: :hyperliquidChain, type: 'string' },
91
+ { name: :validator, type: 'address' },
92
+ { name: :wei, type: 'uint64' },
93
+ { name: :isUndelegate, type: 'bool' },
94
+ { name: :nonce, type: 'uint64' }
95
+ ]
96
+ }.freeze
97
+
98
+ USER_DEX_ABSTRACTION_TYPES = {
99
+ 'HyperliquidTransaction:UserDexAbstraction': [
100
+ { name: :hyperliquidChain, type: 'string' },
101
+ { name: :user, type: 'address' },
102
+ { name: :enabled, type: 'bool' },
103
+ { name: :nonce, type: 'uint64' }
104
+ ]
105
+ }.freeze
106
+
70
107
  class << self
71
108
  # Domain for L1 actions (orders, cancels, leverage, etc.)
72
109
  # @return [Hash] EIP-712 domain configuration
@@ -22,6 +22,12 @@ module Hyperliquid
22
22
  @key.address.to_s
23
23
  end
24
24
 
25
+ # Whether the signer targets testnet
26
+ # @return [Boolean]
27
+ def testnet?
28
+ @testnet
29
+ end
30
+
25
31
  # Sign an L1 action (orders, cancels, leverage updates, etc.)
26
32
  # @param action [Hash] The action payload to sign
27
33
  # @param nonce [Integer] Timestamp in milliseconds
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hyperliquid
4
- VERSION = '0.6.0'
4
+ VERSION = '1.0.0'
5
5
  end