hyperliquid 0.5.0 → 0.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: e4e2d29336f0e921d82f0f7a154676a102a228fd5aa4811b85db890802330865
4
- data.tar.gz: ff28b26c30bd14549def4e1f5adb3193ab812cc4527da50109dd17d5bc333816
3
+ metadata.gz: 324ff762cbf5edcea7fc532f853b7295ffcd34476f6273fdef4e03bcc0f9eaee
4
+ data.tar.gz: 98b1b88b378bb8ae010301c656c833282cd406822fa0c8a7d6c134911b0f2cce
5
5
  SHA512:
6
- metadata.gz: d19f8ddf0b9d7cb16a7791a2d7117725929e956c05a46a27b11cc9b27bb051f0c4d7b1cfc1053e3683a9577ceaacbd96ebd3b06a3541d527f9c0437e5de067b1
7
- data.tar.gz: 8a590a96b1e14b8178e3d628cb8e498d718e0a41963dad0671ead93c06df3d389e2fe2c3082d2a27965eb0adf46370e1508f8a9b0cb2e002444c225e39d8aa79
6
+ metadata.gz: fe9e447e5500312f98e8f8e602071372847963daacfd51475e92d4db5a7bc988546e4cf106479a9c899bcecf113612c2aa9ebe0dd50a5384bd438bb0d6429865
7
+ data.tar.gz: 7cd7d2e048c4bf25e128dd7fd41814621bd95305d613199cb4c41abdf1901fdc4c665635000112aafeb7c395dbf01dae81cba74042557d86643c6ac169e3d201
data/.rubocop.yml CHANGED
@@ -4,6 +4,7 @@ AllCops:
4
4
  SuggestExtensions: false
5
5
  Exclude:
6
6
  - 'test_*.rb' # Exclude ad-hoc integration test scripts
7
+ - 'scripts/**/*' # Exclude integration test scripts
7
8
  - 'vendor/**/*' # Exclude vendored gems (CI bundles here)
8
9
 
9
10
  # Allow longer methods for complex logic
data/CHANGELOG.md CHANGED
@@ -1,8 +1,20 @@
1
1
  ## [Ruby Hyperliquid SDK Changelog]
2
2
 
3
+ ## [0.6.0] - 2026-01-28
4
+
5
+ - Add transfers and account management to Exchange API
6
+ - USD transfers: `usd_send`, `usd_class_transfer`, `withdraw_from_bridge`
7
+ - Spot transfers: `spot_send`, `send_asset`
8
+ - Sub-accounts: `create_sub_account`, `sub_account_transfer`, `sub_account_spot_transfer`
9
+ - Vaults: `vault_transfer`
10
+ - Referrals: `set_referrer`
11
+ - Add user-signed action signing (`sign_user_signed_action`) for EIP-712 typed data with `HyperliquidSignTransaction` domain
12
+ - Add Python SDK parity test vectors for `usd_send`, `withdraw_from_bridge`, `create_sub_account`, and `sub_account_transfer`
13
+ - Reorganize integration tests into individual scripts under `scripts/` for easier debugging
14
+
3
15
  ## [0.5.0] - 2026-01-28
4
16
 
5
- - Add core trading features to Exchange API (Tier 1 parity with Python SDK)
17
+ - Add core trading features to Exchange API
6
18
  - Order modification: `modify_order`, `batch_modify`
7
19
  - Position management: `update_leverage`, `update_isolated_margin`, `market_close`
8
20
  - Dead man's switch: `schedule_cancel`
data/CLAUDE.md CHANGED
@@ -45,16 +45,15 @@ ruby example.rb
45
45
 
46
46
  ### Integration Testing (Testnet)
47
47
  ```bash
48
- # Run the testnet integration test (requires private key)
48
+ # Run all integration tests (requires private key)
49
49
  # Get testnet funds from: https://app.hyperliquid-testnet.xyz
50
- HYPERLIQUID_PRIVATE_KEY=0x... ruby test_integration.rb
50
+ HYPERLIQUID_PRIVATE_KEY=0x... ruby scripts/test_all.rb
51
+
52
+ # Run a single integration test
53
+ HYPERLIQUID_PRIVATE_KEY=0x... ruby scripts/test_08_usd_class_transfer.rb
51
54
  ```
52
55
 
53
- The integration test executes real trades on testnet:
54
- 1. Spot market roundtrip (buy/sell PURR/USDC)
55
- 2. Spot limit order (place and cancel)
56
- 3. Perp market roundtrip (long/close BTC)
57
- 4. Perp limit order (place short, cancel)
56
+ Integration tests live in `scripts/` as individual files. Each can be run standalone for debugging. `test_integration.rb` at the project root is a convenience wrapper that runs them all.
58
57
 
59
58
  ### Setup
60
59
  ```bash
data/docs/API.md CHANGED
@@ -84,11 +84,24 @@ Read-only methods for querying market data and user information.
84
84
  - `update_leverage(coin:, leverage:, is_cross:, ...)` - Set cross or isolated leverage for a coin
85
85
  - `update_isolated_margin(coin:, amount:, ...)` - Add or remove isolated margin for a position
86
86
 
87
+ ### Transfers & Account Management
88
+
89
+ - `usd_send(amount:, destination:)` - Transfer USDC to another address
90
+ - `spot_send(amount:, destination:, token:)` - Transfer a spot token to another address
91
+ - `usd_class_transfer(amount:, to_perp:)` - Move USDC between perp and spot accounts
92
+ - `withdraw_from_bridge(amount:, destination:)` - Withdraw USDC via the bridge
93
+ - `send_asset(destination:, source_dex:, destination_dex:, token:, amount:)` - Move assets between DEX instances
94
+ - `create_sub_account(name:)` - Create a sub-account
95
+ - `sub_account_transfer(sub_account_user:, is_deposit:, usd:)` - Transfer USDC to/from a sub-account
96
+ - `sub_account_spot_transfer(sub_account_user:, is_deposit:, token:, amount:)` - Transfer spot tokens to/from a sub-account
97
+ - `vault_transfer(vault_address:, is_deposit:, usd:)` - Deposit or withdraw USDC to/from a vault
98
+ - `set_referrer(code:)` - Set referral code
99
+
87
100
  ### Other
88
101
 
89
102
  - `address` - Get the wallet address associated with the private key
90
103
 
91
- All exchange methods support an optional `vault_address:` parameter for vault trading.
104
+ Order placement and management methods support an optional `vault_address:` parameter for vault trading.
92
105
 
93
106
  ### Order Types
94
107
 
data/docs/DEVELOPMENT.md CHANGED
@@ -11,13 +11,42 @@ bin/setup
11
11
  ## Running Tests
12
12
 
13
13
  ```bash
14
- # Run all tests
14
+ # Run all unit tests
15
15
  rake spec
16
16
 
17
- # Run tests and linting together (default)
17
+ # Run unit tests and linting together (default)
18
18
  rake
19
19
  ```
20
20
 
21
+ ### Integration Testing (Testnet)
22
+
23
+ Integration tests live in `scripts/` and execute real trades on testnet. No real funds are at risk. Get testnet funds from https://app.hyperliquid-testnet.xyz.
24
+
25
+ ```bash
26
+ # Run all integration tests
27
+ HYPERLIQUID_PRIVATE_KEY=0x... ruby scripts/test_all.rb
28
+
29
+ # Run a single integration test
30
+ HYPERLIQUID_PRIVATE_KEY=0x... ruby scripts/test_08_usd_class_transfer.rb
31
+ ```
32
+
33
+ The convenience wrapper `ruby test_integration.rb` also runs all tests.
34
+
35
+ Available test scripts:
36
+
37
+ | Script | Description |
38
+ |--------|-------------|
39
+ | `test_01_spot_market_roundtrip.rb` | Buy/sell PURR/USDC at market |
40
+ | `test_02_spot_limit_order.rb` | Place and cancel a spot limit order |
41
+ | `test_03_perp_market_roundtrip.rb` | Long/close BTC at market |
42
+ | `test_04_perp_limit_order.rb` | Place and cancel a perp short |
43
+ | `test_05_update_leverage.rb` | Set cross, isolated, and reset leverage |
44
+ | `test_06_modify_order.rb` | Place, modify, and cancel an order |
45
+ | `test_07_market_close.rb` | Open a position and close via `market_close` |
46
+ | `test_08_usd_class_transfer.rb` | Transfer USDC between perp and spot |
47
+ | `test_09_sub_account_lifecycle.rb` | Create sub-account, deposit, withdraw |
48
+ | `test_10_vault_transfer.rb` | Deposit/withdraw to a vault |
49
+
21
50
  ## Linting
22
51
 
23
52
  ```bash
@@ -37,18 +66,3 @@ This opens an interactive prompt with the SDK loaded for experimentation.
37
66
  ```bash
38
67
  ruby example.rb
39
68
  ```
40
-
41
- ## Integration Testing (Testnet)
42
-
43
- For real trading tests on testnet:
44
-
45
- ```bash
46
- # Get testnet funds from: https://app.hyperliquid-testnet.xyz
47
- HYPERLIQUID_PRIVATE_KEY=0x... ruby test_integration.rb
48
- ```
49
-
50
- The integration test executes real trades on testnet:
51
- 1. Spot market roundtrip (buy/sell PURR/USDC)
52
- 2. Spot limit order (place and cancel)
53
- 3. Perp market roundtrip (long/close BTC)
54
- 4. Perp limit order (place short, cancel)
data/docs/EXAMPLES.md CHANGED
@@ -388,6 +388,97 @@ sdk.exchange.order(
388
388
  )
389
389
  ```
390
390
 
391
+ ### Transfers & Account Management
392
+
393
+ ```ruby
394
+ # Transfer USDC to another address
395
+ sdk.exchange.usd_send(
396
+ amount: '100',
397
+ destination: '0x...'
398
+ )
399
+
400
+ # Transfer a spot token to another address
401
+ sdk.exchange.spot_send(
402
+ amount: '50',
403
+ destination: '0x...',
404
+ token: 'PURR'
405
+ )
406
+
407
+ # Move USDC between perp and spot accounts
408
+ sdk.exchange.usd_class_transfer(amount: '100', to_perp: false) # perp -> spot
409
+ sdk.exchange.usd_class_transfer(amount: '100', to_perp: true) # spot -> perp
410
+
411
+ # Withdraw USDC via the bridge
412
+ sdk.exchange.withdraw_from_bridge(
413
+ amount: '100',
414
+ destination: '0x...'
415
+ )
416
+
417
+ # Move assets between DEX instances
418
+ sdk.exchange.send_asset(
419
+ destination: '0x...',
420
+ source_dex: 'dex1',
421
+ destination_dex: 'dex2',
422
+ token: 'USDC',
423
+ amount: '100'
424
+ )
425
+ ```
426
+
427
+ ### Sub-Account Management
428
+
429
+ ```ruby
430
+ # Create a sub-account
431
+ result = sdk.exchange.create_sub_account(name: 'my-sub-account')
432
+ sub_address = result.dig('response', 'data', 'subAccountUser')
433
+
434
+ # Deposit USDC into a sub-account
435
+ sdk.exchange.sub_account_transfer(
436
+ sub_account_user: sub_address,
437
+ is_deposit: true,
438
+ usd: 100
439
+ )
440
+
441
+ # Withdraw USDC from a sub-account
442
+ sdk.exchange.sub_account_transfer(
443
+ sub_account_user: sub_address,
444
+ is_deposit: false,
445
+ usd: 50
446
+ )
447
+
448
+ # Transfer spot tokens to a sub-account
449
+ sdk.exchange.sub_account_spot_transfer(
450
+ sub_account_user: sub_address,
451
+ is_deposit: true,
452
+ token: 'PURR',
453
+ amount: '10'
454
+ )
455
+ ```
456
+
457
+ ### Vault Operations
458
+
459
+ ```ruby
460
+ # Deposit USDC into a vault
461
+ sdk.exchange.vault_transfer(
462
+ vault_address: '0x...',
463
+ is_deposit: true,
464
+ usd: 100
465
+ )
466
+
467
+ # Withdraw USDC from a vault
468
+ sdk.exchange.vault_transfer(
469
+ vault_address: '0x...',
470
+ is_deposit: false,
471
+ usd: 50
472
+ )
473
+ ```
474
+
475
+ ### Referral
476
+
477
+ ```ruby
478
+ # Set a referral code
479
+ sdk.exchange.set_referrer(code: 'MY_REFERRAL_CODE')
480
+ ```
481
+
391
482
  ### Client Order IDs (Cloid)
392
483
 
393
484
  ```ruby
@@ -374,6 +374,201 @@ module Hyperliquid
374
374
  )
375
375
  end
376
376
 
377
+ # Transfer USDC to another address
378
+ # @param amount [String, Numeric] Amount to send
379
+ # @param destination [String] Destination wallet address
380
+ # @return [Hash] Transfer response
381
+ def usd_send(amount:, destination:)
382
+ nonce = timestamp_ms
383
+ action = {
384
+ type: 'usdSend',
385
+ signatureChainId: '0x66eee',
386
+ hyperliquidChain: Signing::EIP712.hyperliquid_chain(testnet: @signer.instance_variable_get(:@testnet)),
387
+ destination: destination,
388
+ amount: amount.to_s,
389
+ time: nonce
390
+ }
391
+ signature = @signer.sign_user_signed_action(
392
+ { destination: destination, amount: amount.to_s, time: nonce },
393
+ 'HyperliquidTransaction:UsdSend',
394
+ Signing::EIP712::USD_SEND_TYPES
395
+ )
396
+ post_action(action, signature, nonce, nil)
397
+ end
398
+
399
+ # Transfer a spot token to another address
400
+ # @param amount [String, Numeric] Amount to send
401
+ # @param destination [String] Destination wallet address
402
+ # @param token [String] Token identifier
403
+ # @return [Hash] Transfer response
404
+ def spot_send(amount:, destination:, token:)
405
+ nonce = timestamp_ms
406
+ action = {
407
+ type: 'spotSend',
408
+ signatureChainId: '0x66eee',
409
+ hyperliquidChain: Signing::EIP712.hyperliquid_chain(testnet: @signer.instance_variable_get(:@testnet)),
410
+ destination: destination,
411
+ token: token,
412
+ amount: amount.to_s,
413
+ time: nonce
414
+ }
415
+ signature = @signer.sign_user_signed_action(
416
+ { destination: destination, token: token, amount: amount.to_s, time: nonce },
417
+ 'HyperliquidTransaction:SpotSend',
418
+ Signing::EIP712::SPOT_SEND_TYPES
419
+ )
420
+ post_action(action, signature, nonce, nil)
421
+ end
422
+
423
+ # Move USDC between perp and spot accounts
424
+ # @param amount [String, Numeric] Amount to transfer
425
+ # @param to_perp [Boolean] True to move to perp, false to move to spot
426
+ # @return [Hash] Transfer response
427
+ def usd_class_transfer(amount:, to_perp:)
428
+ nonce = timestamp_ms
429
+ action = {
430
+ type: 'usdClassTransfer',
431
+ signatureChainId: '0x66eee',
432
+ hyperliquidChain: Signing::EIP712.hyperliquid_chain(testnet: @signer.instance_variable_get(:@testnet)),
433
+ amount: amount.to_s,
434
+ toPerp: to_perp,
435
+ nonce: nonce
436
+ }
437
+ signature = @signer.sign_user_signed_action(
438
+ { amount: amount.to_s, toPerp: to_perp, nonce: nonce },
439
+ 'HyperliquidTransaction:UsdClassTransfer',
440
+ Signing::EIP712::USD_CLASS_TRANSFER_TYPES
441
+ )
442
+ post_action(action, signature, nonce, nil)
443
+ end
444
+
445
+ # Withdraw USDC via the bridge
446
+ # @param amount [String, Numeric] Amount to withdraw
447
+ # @param destination [String] Destination wallet address
448
+ # @return [Hash] Withdrawal response
449
+ def withdraw_from_bridge(amount:, destination:)
450
+ nonce = timestamp_ms
451
+ action = {
452
+ type: 'withdraw3',
453
+ signatureChainId: '0x66eee',
454
+ hyperliquidChain: Signing::EIP712.hyperliquid_chain(testnet: @signer.instance_variable_get(:@testnet)),
455
+ destination: destination,
456
+ amount: amount.to_s,
457
+ time: nonce
458
+ }
459
+ signature = @signer.sign_user_signed_action(
460
+ { destination: destination, amount: amount.to_s, time: nonce },
461
+ 'HyperliquidTransaction:Withdraw',
462
+ Signing::EIP712::WITHDRAW_TYPES
463
+ )
464
+ post_action(action, signature, nonce, nil)
465
+ end
466
+
467
+ # Move assets between DEX instances
468
+ # @param destination [String] Destination wallet address
469
+ # @param source_dex [String] Source DEX identifier
470
+ # @param destination_dex [String] Destination DEX identifier
471
+ # @param token [String] Token identifier
472
+ # @param amount [String, Numeric] Amount to send
473
+ # @return [Hash] Transfer response
474
+ def send_asset(destination:, source_dex:, destination_dex:, token:, amount:)
475
+ nonce = timestamp_ms
476
+ action = {
477
+ type: 'sendAsset',
478
+ signatureChainId: '0x66eee',
479
+ hyperliquidChain: Signing::EIP712.hyperliquid_chain(testnet: @signer.instance_variable_get(:@testnet)),
480
+ destination: destination,
481
+ sourceDex: source_dex,
482
+ destinationDex: destination_dex,
483
+ token: token,
484
+ amount: amount.to_s,
485
+ fromSubAccount: '',
486
+ nonce: nonce
487
+ }
488
+ signature = @signer.sign_user_signed_action(
489
+ {
490
+ destination: destination, sourceDex: source_dex, destinationDex: destination_dex,
491
+ token: token, amount: amount.to_s, fromSubAccount: '', nonce: nonce
492
+ },
493
+ 'HyperliquidTransaction:SendAsset',
494
+ Signing::EIP712::SEND_ASSET_TYPES
495
+ )
496
+ post_action(action, signature, nonce, nil)
497
+ end
498
+
499
+ # Create a sub-account
500
+ # @param name [String] Sub-account name
501
+ # @return [Hash] Creation response
502
+ def create_sub_account(name:)
503
+ nonce = timestamp_ms
504
+ action = { type: 'createSubAccount', name: name }
505
+ signature = @signer.sign_l1_action(action, nonce)
506
+ post_action(action, signature, nonce, nil)
507
+ end
508
+
509
+ # Transfer USDC to/from a sub-account
510
+ # @param sub_account_user [String] Sub-account wallet address
511
+ # @param is_deposit [Boolean] True to deposit into sub-account, false to withdraw
512
+ # @param usd [Numeric] Amount in USD
513
+ # @return [Hash] Transfer response
514
+ def sub_account_transfer(sub_account_user:, is_deposit:, usd:)
515
+ nonce = timestamp_ms
516
+ action = {
517
+ type: 'subAccountTransfer',
518
+ subAccountUser: sub_account_user,
519
+ isDeposit: is_deposit,
520
+ usd: float_to_usd_int(usd)
521
+ }
522
+ signature = @signer.sign_l1_action(action, nonce)
523
+ post_action(action, signature, nonce, nil)
524
+ end
525
+
526
+ # Transfer spot tokens to/from a sub-account
527
+ # @param sub_account_user [String] Sub-account wallet address
528
+ # @param is_deposit [Boolean] True to deposit into sub-account, false to withdraw
529
+ # @param token [String] Token identifier
530
+ # @param amount [String, Numeric] Amount to transfer
531
+ # @return [Hash] Transfer response
532
+ def sub_account_spot_transfer(sub_account_user:, is_deposit:, token:, amount:)
533
+ nonce = timestamp_ms
534
+ action = {
535
+ type: 'subAccountSpotTransfer',
536
+ subAccountUser: sub_account_user,
537
+ isDeposit: is_deposit,
538
+ token: token,
539
+ amount: amount.to_s
540
+ }
541
+ signature = @signer.sign_l1_action(action, nonce)
542
+ post_action(action, signature, nonce, nil)
543
+ end
544
+
545
+ # Deposit or withdraw USDC to/from a vault
546
+ # @param vault_address [String] Vault wallet address
547
+ # @param is_deposit [Boolean] True to deposit, false to withdraw
548
+ # @param usd [Numeric] Amount in USD
549
+ # @return [Hash] Vault transfer response
550
+ def vault_transfer(vault_address:, is_deposit:, usd:)
551
+ nonce = timestamp_ms
552
+ action = {
553
+ type: 'vaultTransfer',
554
+ vaultAddress: vault_address,
555
+ isDeposit: is_deposit,
556
+ usd: float_to_usd_int(usd)
557
+ }
558
+ signature = @signer.sign_l1_action(action, nonce)
559
+ post_action(action, signature, nonce, nil)
560
+ end
561
+
562
+ # Set referral code
563
+ # @param code [String] Referral code
564
+ # @return [Hash] Set referrer response
565
+ def set_referrer(code:)
566
+ nonce = timestamp_ms
567
+ action = { type: 'setReferrer', code: code }
568
+ signature = @signer.sign_l1_action(action, nonce)
569
+ post_action(action, signature, nonce, nil)
570
+ end
571
+
377
572
  # Clear the asset metadata cache
378
573
  # Call this if metadata has been updated
379
574
  def reload_metadata!
@@ -12,6 +12,61 @@ module Hyperliquid
12
12
  MAINNET_SOURCE = 'a'
13
13
  TESTNET_SOURCE = 'b'
14
14
 
15
+ # Chain ID for user-signed actions (Arbitrum Sepolia: 0x66eee = 421614)
16
+ USER_SIGNED_CHAIN_ID = 421_614
17
+
18
+ # EIP-712 type definitions for user-signed actions
19
+
20
+ USD_SEND_TYPES = {
21
+ 'HyperliquidTransaction:UsdSend': [
22
+ { name: :hyperliquidChain, type: 'string' },
23
+ { name: :destination, type: 'string' },
24
+ { name: :amount, type: 'string' },
25
+ { name: :time, type: 'uint64' }
26
+ ]
27
+ }.freeze
28
+
29
+ SPOT_SEND_TYPES = {
30
+ 'HyperliquidTransaction:SpotSend': [
31
+ { name: :hyperliquidChain, type: 'string' },
32
+ { name: :destination, type: 'string' },
33
+ { name: :token, type: 'string' },
34
+ { name: :amount, type: 'string' },
35
+ { name: :time, type: 'uint64' }
36
+ ]
37
+ }.freeze
38
+
39
+ USD_CLASS_TRANSFER_TYPES = {
40
+ 'HyperliquidTransaction:UsdClassTransfer': [
41
+ { name: :hyperliquidChain, type: 'string' },
42
+ { name: :amount, type: 'string' },
43
+ { name: :toPerp, type: 'bool' },
44
+ { name: :nonce, type: 'uint64' }
45
+ ]
46
+ }.freeze
47
+
48
+ WITHDRAW_TYPES = {
49
+ 'HyperliquidTransaction:Withdraw': [
50
+ { name: :hyperliquidChain, type: 'string' },
51
+ { name: :destination, type: 'string' },
52
+ { name: :amount, type: 'string' },
53
+ { name: :time, type: 'uint64' }
54
+ ]
55
+ }.freeze
56
+
57
+ SEND_ASSET_TYPES = {
58
+ 'HyperliquidTransaction:SendAsset': [
59
+ { name: :hyperliquidChain, type: 'string' },
60
+ { name: :destination, type: 'string' },
61
+ { name: :sourceDex, type: 'string' },
62
+ { name: :destinationDex, type: 'string' },
63
+ { name: :token, type: 'string' },
64
+ { name: :amount, type: 'string' },
65
+ { name: :fromSubAccount, type: 'string' },
66
+ { name: :nonce, type: 'uint64' }
67
+ ]
68
+ }.freeze
69
+
15
70
  class << self
16
71
  # Domain for L1 actions (orders, cancels, leverage, etc.)
17
72
  # @return [Hash] EIP-712 domain configuration
@@ -24,6 +79,17 @@ module Hyperliquid
24
79
  }
25
80
  end
26
81
 
82
+ # Domain for user-signed actions (transfers, withdrawals, etc.)
83
+ # @return [Hash] EIP-712 domain configuration
84
+ def user_signed_domain
85
+ {
86
+ name: 'HyperliquidSignTransaction',
87
+ version: '1',
88
+ chainId: USER_SIGNED_CHAIN_ID,
89
+ verifyingContract: '0x0000000000000000000000000000000000000000'
90
+ }
91
+ end
92
+
27
93
  # EIP-712 domain type definition
28
94
  # @return [Array<Hash>] Domain type fields
29
95
  def domain_type
@@ -50,6 +116,13 @@ module Hyperliquid
50
116
  def source(testnet:)
51
117
  testnet ? TESTNET_SOURCE : MAINNET_SOURCE
52
118
  end
119
+
120
+ # Get hyperliquid chain name for user-signed actions
121
+ # @param testnet [Boolean] Whether testnet
122
+ # @return [String] "Mainnet" or "Testnet"
123
+ def hyperliquid_chain(testnet:)
124
+ testnet ? 'Testnet' : 'Mainnet'
125
+ end
53
126
  end
54
127
  end
55
128
  end
@@ -44,6 +44,31 @@ module Hyperliquid
44
44
  sign_typed_data(typed_data)
45
45
  end
46
46
 
47
+ # Sign a user-signed action (transfers, withdrawals, etc.)
48
+ # Uses direct EIP-712 typed data signing with HyperliquidSignTransaction domain
49
+ # @param action [Hash] The action message to sign (will have chain fields injected)
50
+ # @param primary_type [String] EIP-712 primary type (e.g., "HyperliquidTransaction:UsdSend")
51
+ # @param sign_types [Hash] EIP-712 type definitions for the action
52
+ # @return [Hash] Signature with :r, :s, :v components
53
+ def sign_user_signed_action(action, primary_type, sign_types)
54
+ # Inject chain fields into a copy of the action
55
+ message = action.merge(
56
+ hyperliquidChain: EIP712.hyperliquid_chain(testnet: @testnet),
57
+ signatureChainId: '0x66eee'
58
+ )
59
+
60
+ typed_data = {
61
+ types: {
62
+ EIP712Domain: EIP712.domain_type
63
+ }.merge(sign_types),
64
+ primaryType: primary_type,
65
+ domain: EIP712.user_signed_domain,
66
+ message: message
67
+ }
68
+
69
+ sign_typed_data(typed_data)
70
+ end
71
+
47
72
  private
48
73
 
49
74
  # Normalize private key format
@@ -54,7 +79,7 @@ module Hyperliquid
54
79
  end
55
80
 
56
81
  # Construct the phantom agent for signing
57
- # Maintains parity with Python SDK
82
+ # Maintains parity with official Python SDK
58
83
  # @param action [Hash] Action payload
59
84
  # @param nonce [Integer] Nonce timestamp
60
85
  # @param vault_address [String, nil] Optional vault address
@@ -62,7 +87,7 @@ module Hyperliquid
62
87
  # @return [Hash] Phantom agent with source and connectionId
63
88
  def construct_phantom_agent(action, nonce, vault_address, expires_after)
64
89
  # Compute action hash
65
- # Maintains parity with Python SDK
90
+ # Maintains parity with official Python SDK
66
91
  # data = msgpack(action) + nonce(8 bytes BE) + vault_flag + [vault_addr] + [expires_flag + expires_after]
67
92
  # - Note: expires_flag is only included if expires_after exists. A bit odd but that's what the
68
93
  # Python SDK does.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hyperliquid
4
- VERSION = '0.5.0'
4
+ VERSION = '0.6.0'
5
5
  end
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Test 1: Spot Market Roundtrip (PURR/USDC)
5
+ # Buy and sell PURR at market price.
6
+
7
+ require_relative 'test_helpers'
8
+
9
+ sdk = build_sdk
10
+ separator('TEST 1: Spot Market Roundtrip (PURR/USDC)')
11
+
12
+ spot_coin = 'PURR/USDC'
13
+ spot_size = 5
14
+
15
+ mids = sdk.info.all_mids
16
+ spot_price = mids[spot_coin]&.to_f
17
+
18
+ if spot_price&.positive?
19
+ puts "#{spot_coin} mid: $#{spot_price}"
20
+ puts "Size: #{spot_size} PURR (~$#{(spot_size * spot_price).round(2)})"
21
+ puts "Slippage: #{(SPOT_SLIPPAGE * 100).to_i}%"
22
+ puts
23
+
24
+ puts 'Placing market BUY...'
25
+ result = sdk.exchange.market_order(
26
+ coin: spot_coin,
27
+ is_buy: true,
28
+ size: spot_size,
29
+ slippage: SPOT_SLIPPAGE
30
+ )
31
+ check_result(result, 'Buy')
32
+
33
+ wait_with_countdown(WAIT_SECONDS, 'Waiting before sell...')
34
+
35
+ puts 'Placing market SELL...'
36
+ result = sdk.exchange.market_order(
37
+ coin: spot_coin,
38
+ is_buy: false,
39
+ size: spot_size,
40
+ slippage: SPOT_SLIPPAGE
41
+ )
42
+ check_result(result, 'Sell')
43
+ else
44
+ puts red("SKIPPED: Could not get #{spot_coin} price")
45
+ end
46
+
47
+ test_passed('Test 1 Spot Market Roundtrip')
48
+