hyperliquid 1.5.0 → 1.6.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: 3bf3db383f0da2f70b1d7e98e8901f29fbb4cde940a18859c57c98592c4ffbf3
4
- data.tar.gz: 0055af5f445d241193e27189a585423e3410d296a7df96ba447827299a9b9cd4
3
+ metadata.gz: ea2358a056d774b7c16f5630111fe847204db819d4cdb6c0c9a3c9d601e9977d
4
+ data.tar.gz: cd896dd17790c385765d0484c5d1d57c8589ac4b8b8b238eae624dde135f47b1
5
5
  SHA512:
6
- metadata.gz: 32986974696870940a6ca517f963be23faf84c3dfc7adbfe14f0bcfb5d919118c683ab7f3faa9fd3cfa3249c95c92d89f240cc15917e3e18092331e6141a59de
7
- data.tar.gz: 9a2d495448ba88243b14bfae7f0fe81ddebab3dbc743dce21742b58955196ff9cdd7a6e6a45cea2d958e00cd023748250c923d203258e32d346390638dd4e9de
6
+ metadata.gz: dce41aa6805c512465cba2a66a33a277ad84b6130b06ca80641f45b45ecd8cea6e89d30a1ad592fd30d93f90ef06c4369633ba35df98b6b0429ccd3bfb1377c0
7
+ data.tar.gz: 913c99238363b115273cfd31e2ec3ae8fe61d3de9b2e7367cd0a295b771d75e3b5f5dc050f97d535dfa66b10bae6fcda52dc54835c0e61ffee8b5f14fea2ba2a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  ## [Ruby Hyperliquid SDK Changelog]
2
2
 
3
+ ## [1.6.0] - 2026-05-26
4
+
5
+ ### New Exchange actions
6
+
7
+ - `Exchange#twap_order(asset:, is_buy:, sz:, reduce_only:, minutes:, randomize:, ...)` — L1 action that places a TWAP order with the nested `twap` dict shape (`a`/`b`/`s`/`r`/`m`/`t`). Size via `float_to_wire`; supports `vault_address`.
8
+ - `Exchange#twap_cancel(asset:, twap_id:, ...)` — L1 action that cancels an active TWAP by asset index + twap id; supports `vault_address`.
9
+ - `Exchange#reserve_request_weight(weight:, ...)` — L1 action with a weight-only body; supports `expires_after`, no `vault_address` per TS schema.
10
+ - `Exchange#c_deposit(wei:)` — user-signed action depositing native HYPE to staking. Adds `C_DEPOSIT_TYPES`.
11
+ - `Exchange#c_withdraw(wei:)` — user-signed action withdrawing native HYPE from staking. Adds `C_WITHDRAW_TYPES`.
12
+ - `Exchange#user_portfolio_margin(user:, enabled:)` — user-signed action toggling cross-portfolio-margin mode. Adds `USER_PORTFOLIO_MARGIN_TYPES`.
13
+ - `Exchange#spot_user(opt_out:)` — L1 action toggling spot-dusting opt-out (wraps the wire-shape `toggleSpotDusting` inner object); supports `expires_after`.
14
+
15
+ ### New Info methods
16
+
17
+ - `Info#gossip_priority_auction_status` — returns the current `gossipPriorityAuctionStatus`.
18
+
19
+ ### Tests
20
+
21
+ - New integration scripts `scripts/test_18_user_portfolio_margin.rb` and `scripts/test_19_spot_user.rb` wired into `test_all.rb` (not yet in `test_automated.rb`).
22
+
3
23
  ## [1.5.0] - 2026-05-08
4
24
 
5
25
  ### New Exchange actions
@@ -1110,6 +1110,148 @@ module Hyperliquid
1110
1110
  post_action(action, signature, nonce, nil)
1111
1111
  end
1112
1112
 
1113
+ # Toggle cross-portfolio-margin mode for a user (`userPortfolioMargin` user-signed action).
1114
+ # The `user` address is lowercased to match the Python SDK and protocol expectations.
1115
+ # @param user [String] Wallet address whose portfolio-margin mode is being toggled
1116
+ # @param enabled [Boolean] True to enable cross-portfolio margin, false to disable
1117
+ # @return [Hash] Exchange response
1118
+ def user_portfolio_margin(user:, enabled:)
1119
+ nonce = timestamp_ms
1120
+ user_lower = user.downcase
1121
+ action = {
1122
+ type: 'userPortfolioMargin',
1123
+ signatureChainId: '0x66eee',
1124
+ hyperliquidChain: Signing::EIP712.hyperliquid_chain(testnet: @testnet),
1125
+ user: user_lower,
1126
+ enabled: enabled,
1127
+ nonce: nonce
1128
+ }
1129
+ signature = @signer.sign_user_signed_action(
1130
+ { user: user_lower, enabled: enabled, nonce: nonce },
1131
+ 'HyperliquidTransaction:UserPortfolioMargin',
1132
+ Signing::EIP712::USER_PORTFOLIO_MARGIN_TYPES
1133
+ )
1134
+ post_action(action, signature, nonce, nil)
1135
+ end
1136
+
1137
+ # Deposit native HYPE from the user's spot account into staking (`cDeposit` user-signed action).
1138
+ # @param wei [Integer] Amount of wei to deposit into staking (float * 1e8)
1139
+ # @return [Hash] Exchange response
1140
+ def c_deposit(wei:)
1141
+ nonce = timestamp_ms
1142
+ wei_int = wei.to_i
1143
+ action = {
1144
+ type: 'cDeposit',
1145
+ signatureChainId: '0x66eee',
1146
+ hyperliquidChain: Signing::EIP712.hyperliquid_chain(testnet: @testnet),
1147
+ wei: wei_int,
1148
+ nonce: nonce
1149
+ }
1150
+ signature = @signer.sign_user_signed_action(
1151
+ { wei: wei_int, nonce: nonce },
1152
+ 'HyperliquidTransaction:CDeposit',
1153
+ Signing::EIP712::C_DEPOSIT_TYPES
1154
+ )
1155
+ post_action(action, signature, nonce, nil)
1156
+ end
1157
+
1158
+ # Withdraw native HYPE from staking back into the user's spot account (`cWithdraw` user-signed action).
1159
+ # @param wei [Integer] Amount of wei to withdraw from staking (float * 1e8)
1160
+ # @return [Hash] Exchange response
1161
+ def c_withdraw(wei:)
1162
+ nonce = timestamp_ms
1163
+ wei_int = wei.to_i
1164
+ action = {
1165
+ type: 'cWithdraw',
1166
+ signatureChainId: '0x66eee',
1167
+ hyperliquidChain: Signing::EIP712.hyperliquid_chain(testnet: @testnet),
1168
+ wei: wei_int,
1169
+ nonce: nonce
1170
+ }
1171
+ signature = @signer.sign_user_signed_action(
1172
+ { wei: wei_int, nonce: nonce },
1173
+ 'HyperliquidTransaction:CWithdraw',
1174
+ Signing::EIP712::C_WITHDRAW_TYPES
1175
+ )
1176
+ post_action(action, signature, nonce, nil)
1177
+ end
1178
+
1179
+ # Place a TWAP order (`twapOrder` L1 action). The order is sliced over `minutes`
1180
+ # minutes (5..1440). When `randomize` is true the protocol randomizes the timing
1181
+ # of individual child orders. `size` is denominated in base currency units.
1182
+ # @param coin [String] Asset symbol
1183
+ # @param is_buy [Boolean] True for buy/long, false for sell/short
1184
+ # @param size [String, Numeric] Total order size (base currency units)
1185
+ # @param reduce_only [Boolean] Reduce-only flag
1186
+ # @param minutes [Integer] TWAP duration in minutes (5..1440)
1187
+ # @param randomize [Boolean] Randomize order timing
1188
+ # @param vault_address [String, nil] Vault address if acting on behalf of a vault
1189
+ # @return [Hash] Exchange response — on success `response.data.status.running.twapId`
1190
+ def twap_order(coin:, is_buy:, size:, reduce_only:, minutes:, randomize:, vault_address: nil)
1191
+ nonce = timestamp_ms
1192
+ action = {
1193
+ type: 'twapOrder',
1194
+ twap: {
1195
+ a: asset_index(coin),
1196
+ b: is_buy,
1197
+ s: float_to_wire(size),
1198
+ r: reduce_only,
1199
+ m: minutes,
1200
+ t: randomize
1201
+ }
1202
+ }
1203
+ signature = @signer.sign_l1_action(
1204
+ action, nonce,
1205
+ vault_address: vault_address,
1206
+ expires_after: @expires_after
1207
+ )
1208
+ post_action(action, signature, nonce, vault_address)
1209
+ end
1210
+
1211
+ # Cancel a TWAP order by id (`twapCancel` L1 action).
1212
+ # @param coin [String] Asset symbol the TWAP applies to
1213
+ # @param twap_id [Integer] TWAP id returned by `twap_order`
1214
+ # @param vault_address [String, nil] Vault address if acting on behalf of a vault
1215
+ # @return [Hash] Exchange response
1216
+ def twap_cancel(coin:, twap_id:, vault_address: nil)
1217
+ nonce = timestamp_ms
1218
+ action = { type: 'twapCancel', a: asset_index(coin), t: twap_id }
1219
+ signature = @signer.sign_l1_action(
1220
+ action, nonce,
1221
+ vault_address: vault_address,
1222
+ expires_after: @expires_after
1223
+ )
1224
+ post_action(action, signature, nonce, vault_address)
1225
+ end
1226
+
1227
+ # Reserve additional rate-limited actions for a fee (`reserveRequestWeight` L1 action).
1228
+ # @param weight [Integer] Amount of request weight to reserve
1229
+ # @return [Hash] Exchange response
1230
+ def reserve_request_weight(weight:)
1231
+ nonce = timestamp_ms
1232
+ action = { type: 'reserveRequestWeight', weight: weight }
1233
+ signature = @signer.sign_l1_action(
1234
+ action, nonce,
1235
+ expires_after: @expires_after
1236
+ )
1237
+ post_action(action, signature, nonce, nil)
1238
+ end
1239
+
1240
+ # Opt in or out of spot dusting (`spotUser` L1 action).
1241
+ # Spot dusting is the protocol's automatic conversion of small spot balances.
1242
+ # Despite the generic action name, this method exclusively toggles that opt-out flag.
1243
+ # @param opt_out [Boolean] True to opt out of spot dusting, false to opt in
1244
+ # @return [Hash] Exchange response
1245
+ def spot_user(opt_out:)
1246
+ nonce = timestamp_ms
1247
+ action = { type: 'spotUser', toggleSpotDusting: { optOut: opt_out } }
1248
+ signature = @signer.sign_l1_action(
1249
+ action, nonce,
1250
+ expires_after: @expires_after
1251
+ )
1252
+ post_action(action, signature, nonce, nil)
1253
+ end
1254
+
1113
1255
  # Clear the asset metadata cache
1114
1256
  # Call this if metadata has been updated
1115
1257
  def reload_metadata!
@@ -359,6 +359,13 @@ module Hyperliquid
359
359
  @client.post(Constants::INFO_ENDPOINT, { type: 'gossipRootIps' })
360
360
  end
361
361
 
362
+ # Retrieve gossip priority auction status (previous winners and current auctions)
363
+ # @return [Array] Two-element tuple: [0] array of previous-winner IPs per slot (String or nil),
364
+ # [1] array of current auction statuses per slot (same shape as perp_deploy_auction_status)
365
+ def gossip_priority_auction_status
366
+ @client.post(Constants::INFO_ENDPOINT, { type: 'gossipPriorityAuctionStatus' })
367
+ end
368
+
362
369
  # Retrieve a user's legal verification status
363
370
  # @param user [String] Wallet address
364
371
  # @return [Hash] Keys: ipAllowed (Boolean), acceptedTerms (Boolean), userAllowed (Boolean)
@@ -138,6 +138,31 @@ module Hyperliquid
138
138
  ]
139
139
  }.freeze
140
140
 
141
+ USER_PORTFOLIO_MARGIN_TYPES = {
142
+ 'HyperliquidTransaction:UserPortfolioMargin': [
143
+ { name: :hyperliquidChain, type: 'string' },
144
+ { name: :user, type: 'address' },
145
+ { name: :enabled, type: 'bool' },
146
+ { name: :nonce, type: 'uint64' }
147
+ ]
148
+ }.freeze
149
+
150
+ C_DEPOSIT_TYPES = {
151
+ 'HyperliquidTransaction:CDeposit': [
152
+ { name: :hyperliquidChain, type: 'string' },
153
+ { name: :wei, type: 'uint64' },
154
+ { name: :nonce, type: 'uint64' }
155
+ ]
156
+ }.freeze
157
+
158
+ C_WITHDRAW_TYPES = {
159
+ 'HyperliquidTransaction:CWithdraw': [
160
+ { name: :hyperliquidChain, type: 'string' },
161
+ { name: :wei, type: 'uint64' },
162
+ { name: :nonce, type: 'uint64' }
163
+ ]
164
+ }.freeze
165
+
141
166
  SEND_TO_EVM_WITH_DATA_TYPES = {
142
167
  'HyperliquidTransaction:SendToEvmWithData': [
143
168
  { name: :hyperliquidChain, type: 'string' },
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hyperliquid
4
- VERSION = '1.5.0'
4
+ VERSION = '1.6.0'
5
5
  end
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Test 18: userPortfolioMargin (user-signed exchange action)
5
+ #
6
+ # Toggles cross-portfolio-margin mode for the calling wallet. Sends two actions:
7
+ # enable, then disable, leaving the wallet in its original state.
8
+ #
9
+ # WARNING: Toggling portfolio margin alters margining math on existing perp positions.
10
+ # Run only on a wallet without significant open exposure on testnet.
11
+ #
12
+ # Usage:
13
+ # HYPERLIQUID_PRIVATE_KEY=0x... ruby scripts/test_18_user_portfolio_margin.rb
14
+
15
+ require_relative 'test_helpers'
16
+
17
+ sdk = build_sdk
18
+ separator('TEST 18: userPortfolioMargin')
19
+
20
+ user = sdk.exchange.address
21
+ puts "Wallet: #{user}"
22
+ puts
23
+
24
+ puts 'Enabling portfolio margin...'
25
+ result = sdk.exchange.user_portfolio_margin(user: user, enabled: true)
26
+
27
+ # The protocol enforces a $10k account value or $5m volume threshold for portfolio
28
+ # margin eligibility. The agent testnet wallet typically meets neither, so this
29
+ # precondition failure is not an SDK bug — downgrade to warning and exit cleanly,
30
+ # matching the test_08 / test_11 pattern.
31
+ if result.is_a?(Hash) && result['status'] == 'err' &&
32
+ result['response'].to_s.include?('Portfolio margin requires')
33
+ puts red("WARNING: #{result['response']}")
34
+ puts ' Skipping — this is a testnet precondition, not an SDK failure.'
35
+ puts ' The action correctly serialized and signed (server returned a structured'
36
+ puts ' rejection, not a signing error).'
37
+ test_passed('Test 18 user_portfolio_margin')
38
+ exit 0
39
+ end
40
+
41
+ if api_error?(result)
42
+ puts red("user_portfolio_margin (enable) FAILED: #{result.inspect}")
43
+ test_passed('Test 18 user_portfolio_margin')
44
+ exit 1
45
+ end
46
+
47
+ unless result.is_a?(Hash) && result['status'] == 'ok' && result.dig('response', 'type') == 'default'
48
+ $test_failed = true
49
+ puts red("Unexpected enable response: #{result.inspect}")
50
+ end
51
+
52
+ puts green("Enable OK: #{result.inspect}") unless $test_failed
53
+
54
+ wait_with_countdown(WAIT_SECONDS, 'Settling before toggle back...')
55
+
56
+ puts 'Disabling portfolio margin...'
57
+ result = sdk.exchange.user_portfolio_margin(user: user, enabled: false)
58
+
59
+ if api_error?(result)
60
+ puts red("user_portfolio_margin (disable) FAILED: #{result.inspect}")
61
+ test_passed('Test 18 user_portfolio_margin')
62
+ exit 1
63
+ end
64
+
65
+ unless result.is_a?(Hash) && result['status'] == 'ok' && result.dig('response', 'type') == 'default'
66
+ $test_failed = true
67
+ puts red("Unexpected disable response: #{result.inspect}")
68
+ end
69
+
70
+ puts green("Disable OK: #{result.inspect}") unless $test_failed
71
+
72
+ test_passed('Test 18 user_portfolio_margin')
@@ -0,0 +1,55 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Test 19: spotUser (L1 exchange action)
5
+ #
6
+ # Toggles spot-dusting opt-out for the calling wallet. Sends two actions:
7
+ # opt_out: true, then opt_out: false, leaving the wallet in its original state
8
+ # (spot dusting opted-in by default).
9
+ #
10
+ # Usage:
11
+ # HYPERLIQUID_PRIVATE_KEY=0x... ruby scripts/test_19_spot_user.rb
12
+
13
+ require_relative 'test_helpers'
14
+
15
+ sdk = build_sdk
16
+ separator('TEST 19: spotUser (spot-dusting opt-out)')
17
+
18
+ puts "Wallet: #{sdk.exchange.address}"
19
+ puts
20
+
21
+ puts 'Opting out of spot dusting...'
22
+ result = sdk.exchange.spot_user(opt_out: true)
23
+
24
+ if api_error?(result)
25
+ puts red("spot_user (opt_out: true) FAILED: #{result.inspect}")
26
+ test_passed('Test 19 spot_user')
27
+ exit 1
28
+ end
29
+
30
+ unless result.is_a?(Hash) && result['status'] == 'ok' && result.dig('response', 'type') == 'default'
31
+ $test_failed = true
32
+ puts red("Unexpected opt-out response: #{result.inspect}")
33
+ end
34
+
35
+ puts green("Opt-out OK: #{result.inspect}") unless $test_failed
36
+
37
+ wait_with_countdown(WAIT_SECONDS, 'Settling before toggle back...')
38
+
39
+ puts 'Opting back in to spot dusting...'
40
+ result = sdk.exchange.spot_user(opt_out: false)
41
+
42
+ if api_error?(result)
43
+ puts red("spot_user (opt_out: false) FAILED: #{result.inspect}")
44
+ test_passed('Test 19 spot_user')
45
+ exit 1
46
+ end
47
+
48
+ unless result.is_a?(Hash) && result['status'] == 'ok' && result.dig('response', 'type') == 'default'
49
+ $test_failed = true
50
+ puts red("Unexpected opt-in response: #{result.inspect}")
51
+ end
52
+
53
+ puts green("Opt-in OK: #{result.inspect}") unless $test_failed
54
+
55
+ test_passed('Test 19 spot_user')
data/scripts/test_all.rb CHANGED
@@ -35,7 +35,9 @@ SCRIPTS = [
35
35
  'test_14_ws_candle.rb',
36
36
  'test_15_explorer.rb',
37
37
  'test_16_send_to_evm_with_data.rb',
38
- 'test_17_create_vault.rb'
38
+ 'test_17_create_vault.rb',
39
+ 'test_18_user_portfolio_margin.rb',
40
+ 'test_19_spot_user.rb'
39
41
  ].freeze
40
42
 
41
43
  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.5.0
4
+ version: 1.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - carter2099
@@ -133,6 +133,8 @@ files:
133
133
  - scripts/test_15_explorer.rb
134
134
  - scripts/test_16_send_to_evm_with_data.rb
135
135
  - scripts/test_17_create_vault.rb
136
+ - scripts/test_18_user_portfolio_margin.rb
137
+ - scripts/test_19_spot_user.rb
136
138
  - scripts/test_all.rb
137
139
  - scripts/test_automated.rb
138
140
  - scripts/test_helpers.rb