hyperliquid 1.3.0 → 1.4.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/CHANGELOG.md +11 -0
- data/CLAUDE.md +2 -0
- data/lib/hyperliquid/exchange.rb +160 -0
- data/lib/hyperliquid/signing/eip712.rb +24 -0
- data/lib/hyperliquid/version.rb +1 -1
- data/scripts/test_16_send_to_evm_with_data.rb +79 -0
- data/scripts/test_all.rb +2 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c7969eed04a0b822b7be2c29c3ab57f97f077eaf8ed245bc48cb4916fd6b5836
|
|
4
|
+
data.tar.gz: d37e1acbf31a1e26454071ede7c83fcccf842bf9524835def63a652e77fff1af
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1a7bc66c9acd82975bb1e5929d2f4b77469c5e52b3c9824f7ce5306ad642c1e065531d6f4c20134cdab85c350b2f694736c5a414ee6e7a564d35abc658ed1074
|
|
7
|
+
data.tar.gz: 9898cba2cbf564dff73f950a29423dd5f42ab2fddf62458418d365546893dabdc5f3ddf159f1af31c7e6254ff02aeaa1a77ca0c63f6198931ee291319daaab82
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
## [Ruby Hyperliquid SDK Changelog]
|
|
2
2
|
|
|
3
|
+
## [1.4.0] - 2026-05-05
|
|
4
|
+
|
|
5
|
+
### New Exchange actions
|
|
6
|
+
|
|
7
|
+
- `Exchange#send_to_evm_with_data(destination:, token:, amount:, data:, ...)` — user-signed; transfers Core assets to HyperEVM with arbitrary calldata for `ICoreReceiveWithData` contracts. Adds `SEND_TO_EVM_WITH_DATA_TYPES` (first user-signed action using `bytes`). Includes signature-parity regression specs against captured `eth_account` fixtures to lock down eth gem's `bytes` handling.
|
|
8
|
+
- `Exchange#agent_send_asset(...)` — agent-signed counterpart to `send_asset` (destination must equal the agent's principal).
|
|
9
|
+
- `Exchange#hip3_liquidator_transfer(dex:, usdc:, to_dex:)` — deposit/withdraw to an HIP-3 DEX backstop in 1e-6 quote-token units.
|
|
10
|
+
- `Exchange#borrow_lend(token:, action:, amount:, is_isolated:)` — HIP-2 supply/withdraw/repay/borrow (L1 action). Companion to the four HIP-2 info methods. `amount` is nullable for full-position ops.
|
|
11
|
+
- `Exchange#sub_account_modify(sub_account_user:, name:)` — rename a sub-account (L1 action).
|
|
12
|
+
- `Exchange#link_staking_user(staking_user:, ...)` — link staking + trading accounts for fee discount (user-signed EIP-712). Adds `LINK_STAKING_USER_TYPES`.
|
|
13
|
+
|
|
3
14
|
## [1.3.0] - 2026-04-30
|
|
4
15
|
|
|
5
16
|
### New transport
|
data/CLAUDE.md
CHANGED
|
@@ -63,6 +63,8 @@ The signing chain in `lib/hyperliquid/signing/` must exactly match the official
|
|
|
63
63
|
|
|
64
64
|
Any change to signing must maintain parity with the Python SDK or transactions will be rejected by the exchange.
|
|
65
65
|
|
|
66
|
+
User-signed actions (`usd_send`, `withdraw_from_bridge`, `send_to_evm_with_data`, etc.) use direct EIP-712 typed-data signing with the `HyperliquidSignTransaction` domain (chain ID 421614) — not the phantom-agent flow. Each has a typed-data spec in `Signing::EIP712`. The `eth` gem's typed-data signer handles primitive types (`string`, `uint*`, `address`, `bool`) and dynamic `bytes` correctly — `send_to_evm_with_data` was the first to use `bytes`, and its spec includes a fixture-based signature parity test against `eth_account` to lock that in. When adding new user-signed actions with non-string types, add a similar fixture to catch eth-gem regressions.
|
|
67
|
+
|
|
66
68
|
### Numeric Conversion
|
|
67
69
|
|
|
68
70
|
- **`float_to_wire`** (in Exchange): converts to string with 8-decimal precision, validates rounding tolerance (`1e-12`), normalizes trailing zeros. No scientific notation.
|
data/lib/hyperliquid/exchange.rb
CHANGED
|
@@ -893,6 +893,166 @@ module Hyperliquid
|
|
|
893
893
|
post_action(action, signature, nonce, nil)
|
|
894
894
|
end
|
|
895
895
|
|
|
896
|
+
# Borrow, lend, supply, or withdraw HIP-2 borrow/lend assets (`borrowLend` L1 action).
|
|
897
|
+
# Companion to the four HIP-2 info methods (`borrow_lend_user_state` etc.).
|
|
898
|
+
# @param operation [String] One of 'supply', 'withdraw', 'repay', 'borrow'
|
|
899
|
+
# @param token [Integer] HIP-2 token ID (e.g. 0 for USDC)
|
|
900
|
+
# @param amount [String, Numeric, nil] Amount to operate on; pass nil to use the full position
|
|
901
|
+
# @param vault_address [String, nil] Vault address if acting on behalf of a vault
|
|
902
|
+
# @return [Hash] Exchange response
|
|
903
|
+
def borrow_lend(operation:, token:, amount: nil, vault_address: nil)
|
|
904
|
+
nonce = timestamp_ms
|
|
905
|
+
action = {
|
|
906
|
+
type: 'borrowLend',
|
|
907
|
+
operation: operation,
|
|
908
|
+
token: token,
|
|
909
|
+
amount: amount&.to_s
|
|
910
|
+
}
|
|
911
|
+
signature = @signer.sign_l1_action(
|
|
912
|
+
action, nonce,
|
|
913
|
+
vault_address: vault_address,
|
|
914
|
+
expires_after: @expires_after
|
|
915
|
+
)
|
|
916
|
+
post_action(action, signature, nonce, vault_address)
|
|
917
|
+
end
|
|
918
|
+
|
|
919
|
+
# Rename a sub-account (`subAccountModify` L1 action).
|
|
920
|
+
# @param sub_account_user [String] Sub-account wallet address to rename
|
|
921
|
+
# @param name [String] New sub-account name (1–16 characters)
|
|
922
|
+
# @return [Hash] Exchange response
|
|
923
|
+
def sub_account_modify(sub_account_user:, name:)
|
|
924
|
+
nonce = timestamp_ms
|
|
925
|
+
action = {
|
|
926
|
+
type: 'subAccountModify',
|
|
927
|
+
subAccountUser: sub_account_user,
|
|
928
|
+
name: name
|
|
929
|
+
}
|
|
930
|
+
signature = @signer.sign_l1_action(
|
|
931
|
+
action, nonce,
|
|
932
|
+
expires_after: @expires_after
|
|
933
|
+
)
|
|
934
|
+
post_action(action, signature, nonce, nil)
|
|
935
|
+
end
|
|
936
|
+
|
|
937
|
+
# Link a staking account to a trading account for fee-discount attribution
|
|
938
|
+
# (`linkStakingUser` user-signed action).
|
|
939
|
+
# The trading user initiates with `is_finalize: false`; the staking user finalizes
|
|
940
|
+
# the permanent link with `is_finalize: true`. The `user` field is the *other*
|
|
941
|
+
# account address in each direction.
|
|
942
|
+
# @param user [String] The counterpart account address (staking address when initiating, trading when finalizing)
|
|
943
|
+
# @param is_finalize [Boolean] False = trading user initiates, true = staking user finalizes
|
|
944
|
+
# @return [Hash] Exchange response
|
|
945
|
+
def link_staking_user(user:, is_finalize:)
|
|
946
|
+
nonce = timestamp_ms
|
|
947
|
+
action = {
|
|
948
|
+
type: 'linkStakingUser',
|
|
949
|
+
signatureChainId: '0x66eee',
|
|
950
|
+
hyperliquidChain: Signing::EIP712.hyperliquid_chain(testnet: @testnet),
|
|
951
|
+
user: user,
|
|
952
|
+
isFinalize: is_finalize,
|
|
953
|
+
nonce: nonce
|
|
954
|
+
}
|
|
955
|
+
signature = @signer.sign_user_signed_action(
|
|
956
|
+
{ user: user, isFinalize: is_finalize, nonce: nonce },
|
|
957
|
+
'HyperliquidTransaction:LinkStakingUser',
|
|
958
|
+
Signing::EIP712::LINK_STAKING_USER_TYPES
|
|
959
|
+
)
|
|
960
|
+
post_action(action, signature, nonce, nil)
|
|
961
|
+
end
|
|
962
|
+
|
|
963
|
+
# Move assets between DEX instances on behalf of an agent's principal
|
|
964
|
+
# (`agentSendAsset` L1 action). Unlike `send_asset` (which is user-signed),
|
|
965
|
+
# this is signed by an agent and the destination must equal the agent's
|
|
966
|
+
# principal address. `source_dex`/`destination_dex` accept "" (default USDC
|
|
967
|
+
# perp DEX) and "spot" (spot trading) per protocol convention.
|
|
968
|
+
# @param destination [String] Destination wallet address (must match the agent's principal)
|
|
969
|
+
# @param source_dex [String] Source DEX identifier
|
|
970
|
+
# @param destination_dex [String] Destination DEX identifier
|
|
971
|
+
# @param token [String] Token in "tokenName:tokenId" format
|
|
972
|
+
# @param amount [String, Numeric] Amount to send
|
|
973
|
+
# @param from_sub_account [String] Source sub-account address, or empty string for the principal
|
|
974
|
+
# @return [Hash] Exchange response
|
|
975
|
+
def agent_send_asset(destination:, source_dex:, destination_dex:, token:, amount:,
|
|
976
|
+
from_sub_account: '')
|
|
977
|
+
nonce = timestamp_ms
|
|
978
|
+
action = {
|
|
979
|
+
type: 'agentSendAsset',
|
|
980
|
+
destination: destination,
|
|
981
|
+
sourceDex: source_dex,
|
|
982
|
+
destinationDex: destination_dex,
|
|
983
|
+
token: token,
|
|
984
|
+
amount: amount.to_s,
|
|
985
|
+
fromSubAccount: from_sub_account,
|
|
986
|
+
nonce: nonce
|
|
987
|
+
}
|
|
988
|
+
signature = @signer.sign_l1_action(
|
|
989
|
+
action, nonce,
|
|
990
|
+
expires_after: @expires_after
|
|
991
|
+
)
|
|
992
|
+
post_action(action, signature, nonce, nil)
|
|
993
|
+
end
|
|
994
|
+
|
|
995
|
+
# Deposit to or withdraw from an HIP-3 DEX's backstop liquidator
|
|
996
|
+
# (`hip3LiquidatorTransfer` L1 action). `ntl` is denominated in 1e-6 quote
|
|
997
|
+
# tokens and the protocol requires it to be a multiple of 1_000_000_000
|
|
998
|
+
# (i.e. $1,000 increments).
|
|
999
|
+
# @param dex [String] HIP-3 DEX identifier
|
|
1000
|
+
# @param ntl [Integer] Notional amount in 1e-6 quote tokens (multiple of 1_000_000_000)
|
|
1001
|
+
# @param is_deposit [Boolean] True to deposit into the backstop, false to withdraw
|
|
1002
|
+
# @return [Hash] Exchange response
|
|
1003
|
+
def hip3_liquidator_transfer(dex:, ntl:, is_deposit:)
|
|
1004
|
+
nonce = timestamp_ms
|
|
1005
|
+
action = { type: 'hip3LiquidatorTransfer', dex: dex, ntl: ntl, isDeposit: is_deposit }
|
|
1006
|
+
signature = @signer.sign_l1_action(
|
|
1007
|
+
action, nonce,
|
|
1008
|
+
expires_after: @expires_after
|
|
1009
|
+
)
|
|
1010
|
+
post_action(action, signature, nonce, nil)
|
|
1011
|
+
end
|
|
1012
|
+
|
|
1013
|
+
# Transfer an asset from Core to HyperEVM with an arbitrary calldata payload
|
|
1014
|
+
# (`sendToEvmWithData` user-signed action). Intended for `ICoreReceiveWithData`
|
|
1015
|
+
# contracts that react atomically to the deposit. `data` accepts a hex string
|
|
1016
|
+
# ('0x' or '0x...'); the EIP-712 signer hashes it as `bytes`.
|
|
1017
|
+
# `destination_recipient` is NOT lowercased — `address_encoding` may be
|
|
1018
|
+
# 'base58' for non-EVM target chains, where lowercasing would corrupt the value.
|
|
1019
|
+
# @param token [String] Token symbol (e.g. "USDC")
|
|
1020
|
+
# @param amount [String, Numeric] Amount as UnsignedDecimal (NOT wei); coerced via to_s
|
|
1021
|
+
# @param source_dex [String] Source DEX identifier (e.g. "spot")
|
|
1022
|
+
# @param destination_recipient [String] Recipient address on the destination chain (hex or base58)
|
|
1023
|
+
# @param address_encoding [String] One of 'hex' or 'base58'
|
|
1024
|
+
# @param destination_chain_id [Integer] Target EVM chain id (e.g. 998 for HyperEVM testnet)
|
|
1025
|
+
# @param gas_limit [Integer] Gas limit for the destination EVM call
|
|
1026
|
+
# @param data [String] ABI calldata hex string ('0x' for empty payload)
|
|
1027
|
+
# @return [Hash] Exchange response
|
|
1028
|
+
def send_to_evm_with_data(token:, amount:, source_dex:, destination_recipient:,
|
|
1029
|
+
address_encoding:, destination_chain_id:, gas_limit:, data: '0x')
|
|
1030
|
+
nonce = timestamp_ms
|
|
1031
|
+
action = {
|
|
1032
|
+
type: 'sendToEvmWithData',
|
|
1033
|
+
signatureChainId: '0x66eee',
|
|
1034
|
+
hyperliquidChain: Signing::EIP712.hyperliquid_chain(testnet: @testnet),
|
|
1035
|
+
token: token,
|
|
1036
|
+
amount: amount.to_s,
|
|
1037
|
+
sourceDex: source_dex,
|
|
1038
|
+
destinationRecipient: destination_recipient,
|
|
1039
|
+
addressEncoding: address_encoding,
|
|
1040
|
+
destinationChainId: destination_chain_id.to_i,
|
|
1041
|
+
gasLimit: gas_limit.to_i,
|
|
1042
|
+
data: data,
|
|
1043
|
+
nonce: nonce
|
|
1044
|
+
}
|
|
1045
|
+
signature = @signer.sign_user_signed_action(
|
|
1046
|
+
{ token: token, amount: amount.to_s, sourceDex: source_dex,
|
|
1047
|
+
destinationRecipient: destination_recipient, addressEncoding: address_encoding,
|
|
1048
|
+
destinationChainId: destination_chain_id.to_i, gasLimit: gas_limit.to_i,
|
|
1049
|
+
data: data, nonce: nonce },
|
|
1050
|
+
'HyperliquidTransaction:SendToEvmWithData',
|
|
1051
|
+
Signing::EIP712::SEND_TO_EVM_WITH_DATA_TYPES
|
|
1052
|
+
)
|
|
1053
|
+
post_action(action, signature, nonce, nil)
|
|
1054
|
+
end
|
|
1055
|
+
|
|
896
1056
|
# Clear the asset metadata cache
|
|
897
1057
|
# Call this if metadata has been updated
|
|
898
1058
|
def reload_metadata!
|
|
@@ -121,6 +121,30 @@ module Hyperliquid
|
|
|
121
121
|
]
|
|
122
122
|
}.freeze
|
|
123
123
|
|
|
124
|
+
LINK_STAKING_USER_TYPES = {
|
|
125
|
+
'HyperliquidTransaction:LinkStakingUser': [
|
|
126
|
+
{ name: :hyperliquidChain, type: 'string' },
|
|
127
|
+
{ name: :user, type: 'address' },
|
|
128
|
+
{ name: :isFinalize, type: 'bool' },
|
|
129
|
+
{ name: :nonce, type: 'uint64' }
|
|
130
|
+
]
|
|
131
|
+
}.freeze
|
|
132
|
+
|
|
133
|
+
SEND_TO_EVM_WITH_DATA_TYPES = {
|
|
134
|
+
'HyperliquidTransaction:SendToEvmWithData': [
|
|
135
|
+
{ name: :hyperliquidChain, type: 'string' },
|
|
136
|
+
{ name: :token, type: 'string' },
|
|
137
|
+
{ name: :amount, type: 'string' },
|
|
138
|
+
{ name: :sourceDex, type: 'string' },
|
|
139
|
+
{ name: :destinationRecipient, type: 'string' },
|
|
140
|
+
{ name: :addressEncoding, type: 'string' },
|
|
141
|
+
{ name: :destinationChainId, type: 'uint32' },
|
|
142
|
+
{ name: :gasLimit, type: 'uint64' },
|
|
143
|
+
{ name: :data, type: 'bytes' },
|
|
144
|
+
{ name: :nonce, type: 'uint64' }
|
|
145
|
+
]
|
|
146
|
+
}.freeze
|
|
147
|
+
|
|
124
148
|
class << self
|
|
125
149
|
# Domain for L1 actions (orders, cancels, leverage, etc.)
|
|
126
150
|
# @return [Hash] EIP-712 domain configuration
|
data/lib/hyperliquid/version.rb
CHANGED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Test 16: sendToEvmWithData (user-signed exchange action)
|
|
5
|
+
#
|
|
6
|
+
# Mirrors the TS SDK's `tests/api/exchange/sendToEvmWithData.test.ts`:
|
|
7
|
+
# 1. Top up spot USDC by 2 (via usd_class_transfer perp -> spot).
|
|
8
|
+
# 2. Submit one real sendToEvmWithData action sending 1 USDC to 0x...01
|
|
9
|
+
# on HyperEVM testnet (chain 998) with empty calldata.
|
|
10
|
+
# 3. Assert response is { status: 'ok', response: { type: 'default' } }.
|
|
11
|
+
#
|
|
12
|
+
# This funds-moving test sends 1 testnet USDC to 0x...01 (effectively burned —
|
|
13
|
+
# no contract is there to redirect it). Same bar the TS SDK uses for its own
|
|
14
|
+
# test suite.
|
|
15
|
+
#
|
|
16
|
+
# Skips with a warning if the wallet is on unified account (usd_class_transfer
|
|
17
|
+
# is disabled in that mode and the top-up cannot run).
|
|
18
|
+
#
|
|
19
|
+
# Usage:
|
|
20
|
+
# HYPERLIQUID_PRIVATE_KEY=0x... ruby scripts/test_16_send_to_evm_with_data.rb
|
|
21
|
+
|
|
22
|
+
require_relative 'test_helpers'
|
|
23
|
+
|
|
24
|
+
sdk = build_sdk
|
|
25
|
+
separator('TEST 16: sendToEvmWithData')
|
|
26
|
+
|
|
27
|
+
abstraction = sdk.info.user_abstraction(sdk.exchange.address)
|
|
28
|
+
if abstraction == 'unifiedAccount'
|
|
29
|
+
puts "SKIPPED: Wallet has unified account active (abstraction=#{abstraction.inspect})."
|
|
30
|
+
puts ' usd_class_transfer top-up step is disabled by the exchange when balances are unified.'
|
|
31
|
+
test_passed('Test 16 sendToEvmWithData')
|
|
32
|
+
exit 0
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
puts 'Topping up spot USDC by $2 (perp -> spot)...'
|
|
36
|
+
top_up = sdk.exchange.usd_class_transfer(amount: '2', to_perp: false)
|
|
37
|
+
dump_status(top_up)
|
|
38
|
+
if api_error?(top_up)
|
|
39
|
+
puts red('Top-up failed; aborting before sendToEvmWithData.')
|
|
40
|
+
test_passed('Test 16 sendToEvmWithData')
|
|
41
|
+
exit 1
|
|
42
|
+
end
|
|
43
|
+
puts green('Top-up successful.')
|
|
44
|
+
puts
|
|
45
|
+
|
|
46
|
+
wait_with_countdown(WAIT_SECONDS, 'Waiting before sendToEvmWithData...')
|
|
47
|
+
|
|
48
|
+
puts 'Sending 1 USDC to 0x...01 on HyperEVM testnet (chain 998) with empty calldata...'
|
|
49
|
+
result = sdk.exchange.send_to_evm_with_data(
|
|
50
|
+
token: 'USDC',
|
|
51
|
+
amount: '1',
|
|
52
|
+
source_dex: 'spot',
|
|
53
|
+
destination_recipient: '0x0000000000000000000000000000000000000001',
|
|
54
|
+
address_encoding: 'hex',
|
|
55
|
+
destination_chain_id: 998,
|
|
56
|
+
gas_limit: 200_000,
|
|
57
|
+
data: '0x'
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if api_error?(result)
|
|
61
|
+
puts red("sendToEvmWithData FAILED: #{result.inspect}")
|
|
62
|
+
test_passed('Test 16 sendToEvmWithData')
|
|
63
|
+
exit 1
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
unless result.is_a?(Hash) && result['status'] == 'ok'
|
|
67
|
+
$test_failed = true
|
|
68
|
+
puts red("Unexpected status: #{result.inspect}")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
response_type = result.dig('response', 'type')
|
|
72
|
+
unless response_type == 'default'
|
|
73
|
+
$test_failed = true
|
|
74
|
+
puts red("Expected response.type 'default', got: #{response_type.inspect}")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
puts green("sendToEvmWithData OK: status=ok, response.type=#{response_type}") unless $test_failed
|
|
78
|
+
|
|
79
|
+
test_passed('Test 16 sendToEvmWithData')
|
data/scripts/test_all.rb
CHANGED
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.
|
|
4
|
+
version: 1.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- carter2099
|
|
@@ -130,6 +130,7 @@ files:
|
|
|
130
130
|
- scripts/test_13_ws_l2_book.rb
|
|
131
131
|
- scripts/test_14_ws_candle.rb
|
|
132
132
|
- scripts/test_15_explorer.rb
|
|
133
|
+
- scripts/test_16_send_to_evm_with_data.rb
|
|
133
134
|
- scripts/test_all.rb
|
|
134
135
|
- scripts/test_automated.rb
|
|
135
136
|
- scripts/test_helpers.rb
|