hyperliquid 1.2.0 → 1.3.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: f6baba398ac6a839785cea71c83f96b7a7e339f200604288f9920f25b5a49ed0
4
- data.tar.gz: 0b1961c8b84802e88730421516d27b58e5e14bb055ac190a82e2c0881c692333
3
+ metadata.gz: 3bb8af9ff28e902372f37910705473b980d432cf4a04485dbe5999e7fa6edc7e
4
+ data.tar.gz: e7176efc9fd95c1662b36ec7b8f5536192d8ce3e41624f9b341eca5e79de079a
5
5
  SHA512:
6
- metadata.gz: a7f8886eede6df8fcde833b490733def438945d115c4d3664d397c037d97c5e785e13793885e984baeaf696f156f1f59f614c8f5d0f384e47cbc7d1b1d85708e
7
- data.tar.gz: 518cdc5f65b6fd3de3c15a96bf6e67049af1c5935e9a67a9e56b01d45d43984506d9e30804ec01a9723fe2ff9e1306b2ee32bbb1959dfead2a4f6b036c2c2d49
6
+ metadata.gz: 79d01ffef7d490e185e191285dab90adcd339b03c8f2936ab8eaccb1fba6bc832a76aa49e2cee3db354608a5ce53e92afda6c6efec5d8b2cafc0c1e9a51f7b44
7
+ data.tar.gz: c313423aab3b257d81b7447c840502025f1a92cf857064c614678be2022b533ce7e85a89bedf0aaedd371c5781425962ee48b8f8c9ac7be98f3940b08c756510
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.4.8
1
+ 3.4.9
data/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  ## [Ruby Hyperliquid SDK Changelog]
2
2
 
3
+ ## [1.3.0] - 2026-04-30
4
+
5
+ ### New transport
6
+
7
+ - Explorer RPC connection (`rpc.hyperliquid.xyz` / `rpc.hyperliquid-testnet.xyz`, endpoint `/explorer`). `Client` gains optional `explorer_base_url:` and `target: :explorer` routing, with a lazily-built second Faraday connection sharing the default retry/timeout config. The SDK wires this up automatically based on `testnet:`.
8
+ - New `Hyperliquid::ConfigurationError` raised when `target: :explorer` is used on a client constructed without an explorer URL.
9
+
10
+ ### New Info endpoints
11
+
12
+ - `Info#tx_details(hash)` — explorer `txDetails` lookup
13
+ - `Info#user_details(user)` — explorer `userDetails` lookup
14
+
15
+ ### New Exchange actions
16
+
17
+ - `Exchange#claim_rewards` — L1 `claimRewards`
18
+ - `Exchange#set_display_name(display_name:)` — L1 `setDisplayName` (max 20 chars; `""` clears)
19
+ - `Exchange#register_referrer(code:)` — L1 `registerReferrer`
20
+ - `Exchange#top_up_isolated_only_margin(coin:, leverage:, vault_address:)` — L1 `topUpIsolatedOnlyMargin`
21
+ - `Exchange#vault_modify(vault_address:, allow_deposits:, always_close_on_withdraw:)` — L1 `vaultModify`
22
+ - `Exchange#vault_distribute(vault_address:, usd:)` — L1 `vaultDistribute`
23
+
3
24
  ## [1.2.0] - 2026-04-27
4
25
 
5
26
  ### New Info endpoints
data/CLAUDE.md CHANGED
@@ -50,6 +50,7 @@ Hyperliquid.new(...)
50
50
 
51
51
  - **Info path**: method builds `{ type: 'someType', ... }` body → `Client` POSTs to `/info` → parsed JSON returned.
52
52
  - **Exchange path**: method builds action payload → `Signer` generates EIP-712 signature over msgpack-encoded action → `Client` POSTs signed payload to `/exchange` → parsed JSON returned.
53
+ - **Explorer RPC path** (`tx_details`, `user_details`): a separate base URL (`rpc.hyperliquid.xyz` / `rpc.hyperliquid-testnet.xyz`) with endpoint `/explorer`. `Client` holds a second Faraday connection for this, built lazily on first use; methods opt in via `client.post(EXPLORER_ENDPOINT, body, target: :explorer)`. The SDK wires this up automatically based on `testnet:`. Calling `target: :explorer` on a `Client` constructed without `explorer_base_url:` raises `ConfigurationError`. Don't add a public connection accessor — `target:` is the contract.
53
54
  - **WebSocket path**: `WS::Client` manages a persistent WSS connection with subscription tracking, automatic reconnection (exp backoff, 30s cap), 50s ping keepalive, and a bounded message queue (1024, drops oldest on overflow). Subscriptions are identified by a canonical key and dispatched via callbacks on a dedicated thread.
54
55
 
55
56
  ### Signing (Python SDK Parity)
@@ -21,17 +21,24 @@ module Hyperliquid
21
21
  }.freeze
22
22
 
23
23
  # Initialize a new HTTP client
24
- # @param base_url [String] The base URL for the API
24
+ # @param base_url [String] The base URL for the default API (info/exchange)
25
25
  # @param timeout [Integer] Request timeout in seconds (default: Constants::DEFAULT_TIMEOUT)
26
26
  # @param retry_enabled [Boolean] Whether to enable retry logic (default: false)
27
- def initialize(base_url:, timeout: Constants::DEFAULT_TIMEOUT, retry_enabled: false)
27
+ # @param explorer_base_url [String, nil] Optional base URL for the explorer RPC (used by
28
+ # tx_details / user_details). When nil, calls with target: :explorer raise ConfigurationError.
29
+ def initialize(base_url:, timeout: Constants::DEFAULT_TIMEOUT, retry_enabled: false,
30
+ explorer_base_url: nil)
28
31
  @retry_enabled = retry_enabled
29
- @connection = build_connection(base_url, timeout)
32
+ @timeout = timeout
33
+ @explorer_base_url = explorer_base_url
34
+ @connection = build_connection(base_url)
35
+ @explorer_connection = nil
30
36
  end
31
37
 
32
38
  # Make a POST request to the API
33
39
  # @param endpoint [String] The API endpoint to make the request to
34
40
  # @param body [Hash] The request body as a hash (default: {})
41
+ # @param target [Symbol] Which connection to use; :default (info/exchange) or :explorer (RPC)
35
42
  # @return [Hash, String] The parsed JSON response or raw response body
36
43
  # @raise [NetworkError] When connection fails
37
44
  # @raise [TimeoutError] When request times out
@@ -41,8 +48,10 @@ module Hyperliquid
41
48
  # @raise [RateLimitError] When API returns 429 status
42
49
  # @raise [ServerError] When API returns 5xx status
43
50
  # @raise [ClientError] When API returns unexpected status
44
- def post(endpoint, body = {})
45
- response = @connection.post(endpoint) do |req|
51
+ # @raise [ConfigurationError] When target: :explorer is requested but no explorer_base_url was configured
52
+ def post(endpoint, body = {}, target: :default)
53
+ connection = connection_for(target)
54
+ response = connection.post(endpoint) do |req|
46
55
  req.headers['Content-Type'] = 'application/json'
47
56
  req.body = body.to_json unless body.empty?
48
57
  end
@@ -60,9 +69,24 @@ module Hyperliquid
60
69
 
61
70
  private
62
71
 
63
- def build_connection(base_url, timeout)
72
+ def connection_for(target)
73
+ case target
74
+ when :default
75
+ @connection
76
+ when :explorer
77
+ unless @explorer_base_url
78
+ raise ConfigurationError,
79
+ 'Explorer RPC URL not configured; pass explorer_base_url: when constructing the Client'
80
+ end
81
+ @explorer_connection ||= build_connection(@explorer_base_url)
82
+ else
83
+ raise ArgumentError, "Unknown post target: #{target.inspect} (expected :default or :explorer)"
84
+ end
85
+ end
86
+
87
+ def build_connection(base_url)
64
88
  Faraday.new(url: base_url) do |conn|
65
- conn.options.timeout = timeout
89
+ conn.options.timeout = @timeout
66
90
  conn.options.read_timeout = Constants::DEFAULT_READ_TIMEOUT
67
91
  conn.request :retry, DEFAULT_RETRY_OPTIONS if @retry_enabled
68
92
  end
@@ -7,9 +7,14 @@ module Hyperliquid
7
7
  MAINNET_API_URL = 'https://api.hyperliquid.xyz'
8
8
  TESTNET_API_URL = 'https://api.hyperliquid-testnet.xyz'
9
9
 
10
+ # Explorer RPC URLs (used by Info#tx_details and Info#user_details)
11
+ MAINNET_RPC_URL = 'https://rpc.hyperliquid.xyz'
12
+ TESTNET_RPC_URL = 'https://rpc.hyperliquid-testnet.xyz'
13
+
10
14
  # API endpoints
11
15
  INFO_ENDPOINT = '/info'
12
16
  EXCHANGE_ENDPOINT = '/exchange'
17
+ EXPLORER_ENDPOINT = '/explorer'
13
18
 
14
19
  # WebSocket
15
20
  WS_ENDPOINT = '/ws'
@@ -38,4 +38,8 @@ module Hyperliquid
38
38
 
39
39
  # Error for WebSocket issues
40
40
  class WebSocketError < Error; end
41
+
42
+ # Error for SDK configuration issues (e.g. calling an explorer-only method
43
+ # on a Client that wasn't configured with an explorer_base_url)
44
+ class ConfigurationError < Error; end
41
45
  end
@@ -793,6 +793,106 @@ module Hyperliquid
793
793
  post_action(action, signature, nonce, nil)
794
794
  end
795
795
 
796
+ # Claim accrued referral-program rewards (`claimRewards` L1 action).
797
+ # @return [Hash] Exchange response
798
+ def claim_rewards
799
+ nonce = timestamp_ms
800
+ action = { type: 'claimRewards' }
801
+ signature = @signer.sign_l1_action(
802
+ action, nonce,
803
+ expires_after: @expires_after
804
+ )
805
+ post_action(action, signature, nonce, nil)
806
+ end
807
+
808
+ # Set the leaderboard display name (`setDisplayName` L1 action).
809
+ # Pass an empty string to remove the existing display name.
810
+ # @param display_name [String] Display name (max 20 characters)
811
+ # @return [Hash] Exchange response
812
+ def set_display_name(display_name:)
813
+ nonce = timestamp_ms
814
+ action = { type: 'setDisplayName', displayName: display_name }
815
+ signature = @signer.sign_l1_action(
816
+ action, nonce,
817
+ expires_after: @expires_after
818
+ )
819
+ post_action(action, signature, nonce, nil)
820
+ end
821
+
822
+ # Register a new referral code for this account (`registerReferrer` L1 action).
823
+ # Distinct from `set_referrer`, which records a referrer the account was referred *by*.
824
+ # @param code [String] Referral code to create (1–20 characters)
825
+ # @return [Hash] Exchange response
826
+ def register_referrer(code:)
827
+ nonce = timestamp_ms
828
+ action = { type: 'registerReferrer', code: code }
829
+ signature = @signer.sign_l1_action(
830
+ action, nonce,
831
+ expires_after: @expires_after
832
+ )
833
+ post_action(action, signature, nonce, nil)
834
+ end
835
+
836
+ # Top up isolated margin to target a specific leverage (`topUpIsolatedOnlyMargin` L1 action).
837
+ # @param coin [String] Asset symbol (perps only)
838
+ # @param leverage [String, Numeric] Target leverage (sent as float string per protocol)
839
+ # @param vault_address [String, nil] Vault address if acting on behalf of a vault
840
+ # @return [Hash] Exchange response
841
+ def top_up_isolated_only_margin(coin:, leverage:, vault_address: nil)
842
+ nonce = timestamp_ms
843
+ action = {
844
+ type: 'topUpIsolatedOnlyMargin',
845
+ asset: asset_index(coin),
846
+ leverage: leverage.to_s
847
+ }
848
+ signature = @signer.sign_l1_action(
849
+ action, nonce,
850
+ vault_address: vault_address,
851
+ expires_after: @expires_after
852
+ )
853
+ post_action(action, signature, nonce, vault_address)
854
+ end
855
+
856
+ # Modify a vault's configuration (`vaultModify` L1 action).
857
+ # Only the vault leader may submit this. Either flag may be omitted (sent as null).
858
+ # @param vault_address [String] Vault address being modified
859
+ # @param allow_deposits [Boolean, nil] Allow follower deposits (nil = unchanged)
860
+ # @param always_close_on_withdraw [Boolean, nil] Always close positions on withdrawal (nil = unchanged)
861
+ # @return [Hash] Exchange response
862
+ def vault_modify(vault_address:, allow_deposits: nil, always_close_on_withdraw: nil)
863
+ nonce = timestamp_ms
864
+ action = {
865
+ type: 'vaultModify',
866
+ vaultAddress: vault_address,
867
+ allowDeposits: allow_deposits,
868
+ alwaysCloseOnWithdraw: always_close_on_withdraw
869
+ }
870
+ signature = @signer.sign_l1_action(
871
+ action, nonce,
872
+ expires_after: @expires_after
873
+ )
874
+ post_action(action, signature, nonce, nil)
875
+ end
876
+
877
+ # Distribute funds from a vault to followers (`vaultDistribute` L1 action).
878
+ # Only the vault leader may submit this. Pass `usd: 0` to close the vault.
879
+ # @param vault_address [String] Vault address
880
+ # @param usd [Numeric] USD amount to distribute (scaled to integer cents-of-cents internally)
881
+ # @return [Hash] Exchange response
882
+ def vault_distribute(vault_address:, usd:)
883
+ nonce = timestamp_ms
884
+ action = {
885
+ type: 'vaultDistribute',
886
+ vaultAddress: vault_address,
887
+ usd: float_to_usd_int(usd)
888
+ }
889
+ signature = @signer.sign_l1_action(
890
+ action, nonce,
891
+ expires_after: @expires_after
892
+ )
893
+ post_action(action, signature, nonce, nil)
894
+ end
895
+
796
896
  # Clear the asset metadata cache
797
897
  # Call this if metadata has been updated
798
898
  def reload_metadata!
@@ -631,6 +631,32 @@ module Hyperliquid
631
631
  body[:endTime] = end_time if end_time
632
632
  @client.post(Constants::INFO_ENDPOINT, body)
633
633
  end
634
+
635
+ # ============================
636
+ # Info: Explorer RPC
637
+ # ============================
638
+ # The methods below are routed via a separate base URL (the explorer RPC) rather
639
+ # than the canonical /info endpoint. They require the Client to have been
640
+ # constructed with explorer_base_url:; the SDK does this automatically based on
641
+ # the testnet: flag.
642
+
643
+ # Retrieve transaction details by transaction hash
644
+ # @param hash [String] Transaction hash (66-char 0x-prefixed hex)
645
+ # @return [Hash] Hash with type ('txDetails') and tx (transaction details)
646
+ def tx_details(hash)
647
+ @client.post(Constants::EXPLORER_ENDPOINT, { type: 'txDetails', hash: hash }, target: :explorer)
648
+ end
649
+
650
+ # Retrieve a user's transaction history from the explorer
651
+ # @param user [String] Wallet address
652
+ # @return [Hash] Hash with type ('userDetails') and txs (Array of transaction details)
653
+ def user_details(user)
654
+ @client.post(
655
+ Constants::EXPLORER_ENDPOINT,
656
+ { type: 'userDetails', user: user.downcase },
657
+ target: :explorer
658
+ )
659
+ end
634
660
  end
635
661
  # rubocop:enable Metrics/ClassLength
636
662
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hyperliquid
4
- VERSION = '1.2.0'
4
+ VERSION = '1.3.0'
5
5
  end
data/lib/hyperliquid.rb CHANGED
@@ -46,7 +46,13 @@ module Hyperliquid
46
46
  def initialize(testnet: false, timeout: Constants::DEFAULT_TIMEOUT, retry_enabled: false,
47
47
  private_key: nil, expires_after: nil)
48
48
  base_url = testnet ? Constants::TESTNET_API_URL : Constants::MAINNET_API_URL
49
- client = Client.new(base_url: base_url, timeout: timeout, retry_enabled: retry_enabled)
49
+ explorer_base_url = testnet ? Constants::TESTNET_RPC_URL : Constants::MAINNET_RPC_URL
50
+ client = Client.new(
51
+ base_url: base_url,
52
+ timeout: timeout,
53
+ retry_enabled: retry_enabled,
54
+ explorer_base_url: explorer_base_url
55
+ )
50
56
 
51
57
  @info = Info.new(client)
52
58
  @testnet = testnet
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Test 15: Explorer RPC (tx_details + user_details)
5
+ #
6
+ # Calls the explorer RPC for the wallet's recent transactions:
7
+ # 1. user_details(wallet_address) -> list of recent txs
8
+ # 2. tx_details(first tx hash) -> full details of one tx
9
+ #
10
+ # Requires HYPERLIQUID_PRIVATE_KEY (used to derive wallet address; no signing).
11
+ # A wallet with no testnet activity at all will produce a soft warning rather
12
+ # than fail — explorer testnet endpoint occasionally has its own state quirks.
13
+ #
14
+ # Usage:
15
+ # HYPERLIQUID_PRIVATE_KEY=0x... ruby scripts/test_15_explorer.rb
16
+
17
+ require_relative 'test_helpers'
18
+
19
+ separator('TEST 15: Explorer RPC (tx_details + user_details)')
20
+
21
+ sdk = build_sdk
22
+
23
+ begin
24
+ user_details_resp = sdk.info.user_details(sdk.exchange.address)
25
+ rescue Hyperliquid::Error => e
26
+ puts red("user_details FAILED: #{e.class}: #{e.message}")
27
+ exit 1
28
+ end
29
+
30
+ unless user_details_resp.is_a?(Hash) && user_details_resp['type'] == 'userDetails'
31
+ puts red("user_details: unexpected response shape: #{user_details_resp.inspect}")
32
+ exit 1
33
+ end
34
+
35
+ txs = user_details_resp['txs'] || []
36
+ puts green("user_details OK: #{txs.length} txs returned")
37
+
38
+ if txs.empty?
39
+ puts 'No txs on this wallet — skipping tx_details lookup.'
40
+ test_passed('Test 15 explorer RPC')
41
+ exit 0
42
+ end
43
+
44
+ first_hash = txs.first['hash']
45
+ unless first_hash.is_a?(String) && first_hash.start_with?('0x')
46
+ puts red("First tx hash missing or malformed: #{first_hash.inspect}")
47
+ exit 1
48
+ end
49
+
50
+ puts "Looking up tx_details for #{first_hash}..."
51
+ begin
52
+ tx_resp = sdk.info.tx_details(first_hash)
53
+ rescue Hyperliquid::Error => e
54
+ puts red("tx_details FAILED: #{e.class}: #{e.message}")
55
+ exit 1
56
+ end
57
+
58
+ unless tx_resp.is_a?(Hash) && tx_resp['type'] == 'txDetails'
59
+ puts red("tx_details: unexpected response shape: #{tx_resp.inspect}")
60
+ exit 1
61
+ end
62
+
63
+ tx = tx_resp['tx']
64
+ unless tx.is_a?(Hash) && tx['hash'] == first_hash
65
+ puts red("tx_details: hash mismatch or missing tx: #{tx.inspect}")
66
+ exit 1
67
+ end
68
+
69
+ puts green("tx_details OK: action=#{tx.dig('action', 'type')} time=#{tx['time']}")
70
+
71
+ test_passed('Test 15 explorer RPC')
data/scripts/test_all.rb CHANGED
@@ -32,7 +32,8 @@ SCRIPTS = [
32
32
  'test_11_builder_fee.rb',
33
33
  'test_12_staking.rb',
34
34
  'test_13_ws_l2_book.rb',
35
- 'test_14_ws_candle.rb'
35
+ 'test_14_ws_candle.rb',
36
+ 'test_15_explorer.rb'
36
37
  ].freeze
37
38
 
38
39
  def green(text)
@@ -22,7 +22,8 @@ SCRIPTS = [
22
22
  'test_10_vault.rb',
23
23
  'test_11_builder_fee.rb',
24
24
  'test_13_ws_l2_book.rb',
25
- 'test_14_ws_candle.rb'
25
+ 'test_14_ws_candle.rb',
26
+ 'test_15_explorer.rb'
26
27
  ].freeze
27
28
 
28
29
  def green(text)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hyperliquid
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - carter2099
@@ -129,6 +129,7 @@ files:
129
129
  - scripts/test_12_staking.rb
130
130
  - scripts/test_13_ws_l2_book.rb
131
131
  - scripts/test_14_ws_candle.rb
132
+ - scripts/test_15_explorer.rb
132
133
  - scripts/test_all.rb
133
134
  - scripts/test_automated.rb
134
135
  - scripts/test_helpers.rb