Files
nofx/api/handler_hyperliquid_wallet_test.go
tinkle-community 6195803e9e feat: Hyperliquid deposit QR + spot<->perp transfer in the platform
The trading account could only be funded outside NOFX: no way to move
spot ('main wallet') USDC into the perp trading account, and no deposit
address/QR shown anywhere.

- API: /hyperliquid/account now also returns spot USDC (total/available)
  alongside the perp summary; /hyperliquid/submit-exchange accepts a
  validated usdClassTransfer action (positive plain amount, boolean
  toPerp, Mainnet chain) relayed like the existing approval actions
- Web: new HyperliquidFundsPanel in the exchange config modal with a
  deposit tab (wallet address QR + Arbitrum/spot routing notes) and a
  transfer tab (spot<->perp, signed by the user's own wallet via EIP-712
  UsdClassTransfer — user-signed actions derive the account from the
  signature, so the agent key NOFX holds cannot and does not move funds)
- Shared EIP-712/provider helpers extracted from HyperliquidWalletConnect
  into lib/hyperliquidWallet.ts
2026-07-24 14:07:13 +09:00

68 lines
2.1 KiB
Go

package api
import (
"strings"
"testing"
)
func usdClassTransferAction(overrides map[string]any) map[string]any {
action := map[string]any{
"type": "usdClassTransfer",
"signatureChainId": "0x66eee",
"hyperliquidChain": "Mainnet",
"amount": "21.5",
"toPerp": true,
"nonce": float64(1784900000000),
}
for k, v := range overrides {
if v == nil {
delete(action, k)
continue
}
action[k] = v
}
return action
}
func TestValidateUsdClassTransferActionAcceptsValidTransfer(t *testing.T) {
if err := validateUsdClassTransferAction(usdClassTransferAction(nil)); err != nil {
t.Fatalf("expected valid usdClassTransfer to pass, got %v", err)
}
}
func TestValidateUsdClassTransferActionRejectsBadAmounts(t *testing.T) {
cases := map[string]any{
"zero": "0",
"negative": "-5",
"not number": "abc",
"empty": "",
// The SDK's subaccount suffix must not be relayable from the browser.
"subaccount": "21.5 subaccount:0x1234",
}
for name, amount := range cases {
err := validateUsdClassTransferAction(usdClassTransferAction(map[string]any{"amount": amount}))
if err == nil {
t.Fatalf("%s: expected amount %q to be rejected", name, amount)
}
}
}
func TestValidateUsdClassTransferActionRequiresBooleanToPerp(t *testing.T) {
if err := validateUsdClassTransferAction(usdClassTransferAction(map[string]any{"toPerp": nil})); err == nil {
t.Fatal("expected missing toPerp to be rejected")
}
if err := validateUsdClassTransferAction(usdClassTransferAction(map[string]any{"toPerp": "true"})); err == nil {
t.Fatal("expected non-boolean toPerp to be rejected")
}
if err := validateUsdClassTransferAction(usdClassTransferAction(map[string]any{"toPerp": false})); err != nil {
t.Fatalf("expected toPerp=false (perp->spot) to be valid, got %v", err)
}
}
func TestValidateUsdClassTransferActionRequiresMainnetChain(t *testing.T) {
err := validateUsdClassTransferAction(usdClassTransferAction(map[string]any{"hyperliquidChain": "Testnet"}))
if err == nil || !strings.Contains(err.Error(), "hyperliquidChain") {
t.Fatalf("expected Testnet chain to be rejected, got %v", err)
}
}