Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 58 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ A minimal Go implementation of the Hyperliquid API for E2E testing without requi

## Features

- **No authentication validation** - Accepts all requests without signature verification
- **Signature validation and wallet isolation** - Recovers wallet addresses from ECDSA signatures for proper multi-wallet testing
- **In-memory order state** - Tracks orders for realistic responses
- **WebSocket support** - Real-time order updates and market data (BBO)
- **WebSocket support** - Real-time order updates and market data (BBO) with wallet-based filtering
- **Unlimited requests** - No rate limiting
- **Simple responses** - Returns success for all valid operations

Expand Down Expand Up @@ -110,6 +110,47 @@ ws.onmessage = (event) => {
};
```

## Wallet Isolation

The mock server implements proper wallet isolation to support multi-wallet testing scenarios. Each order is tagged with the wallet address recovered from its ECDSA signature, ensuring that:

- **WebSocket subscriptions** only receive updates for orders belonging to the subscribed wallet
- **Order queries** only return orders owned by the requesting wallet
- **Different wallets** can operate independently without interference

### Signature Recovery

The server recovers wallet addresses from ECDSA signatures using the same EIP-712 signing format as the real Hyperliquid API:

1. Msgpack-encodes the action with sorted keys
2. Appends nonce, vault address (optional), and expiration timestamp (optional)
3. Creates an EIP-712 typed data structure with "Agent" phantom type
4. Recovers the public key from the signature
5. Derives the Ethereum address

This allows the mock server to properly isolate orders by wallet without requiring manual configuration.

### Testing Multi-Wallet Scenarios

```go
// Create two different wallets
privateKey1, _ := crypto.GenerateKey()
privateKey2, _ := crypto.GenerateKey()

wallet1 := crypto.PubkeyToAddress(privateKey1.PublicKey).Hex()
wallet2 := crypto.PubkeyToAddress(privateKey2.PublicKey).Hex()

// Create exchanges for each wallet
exchange1 := hyperliquid.NewExchange(ctx, privateKey1, ts.URL(), nil, "", wallet1, nil)
exchange2 := hyperliquid.NewExchange(ctx, privateKey2, ts.URL(), nil, "", wallet2, nil)

// Orders are automatically isolated by wallet
exchange1.Order(ctx, order1, nil) // Only visible to wallet1
exchange2.Order(ctx, order2, nil) // Only visible to wallet2
```

See `server/integration_test.go` for complete multi-wallet test examples.

### POST /exchange

Handles trading actions:
Expand All @@ -129,7 +170,7 @@ Handles trading actions:
"limit_px": 3000.00,
"reduce_only": false,
"order_type": {"limit": {"tif": "Gtc"}},
"cloid": "0x1234..."
"cloid": "00000000000000000000000000000001"
}]
},
"nonce": 1234567890,
Expand Down Expand Up @@ -188,7 +229,7 @@ Handles queries:
"oid": 1000001,
"timestamp": 1234567890000,
"origSz": "1.5",
"cloid": "0x1234..."
"cloid": "00000000000000000000000000000001"
},
"status": "open",
"statusTimestamp": 1234567890000
Expand Down Expand Up @@ -247,7 +288,10 @@ func TestMyHyperliquidCode(t *testing.T) {
)

// Run your test code
cloid := "my-cloid"
// CLOID must be exactly 32 hex characters (16 bytes)
token := make([]byte, 16)
rand.Read(token)
cloid := hex.EncodeToString(token)
status, err := exchange.Order(ctx, hyperliquid.CreateOrderRequest{
Coin: "ETH",
IsBuy: true,
Expand All @@ -266,7 +310,7 @@ func TestMyHyperliquidCode(t *testing.T) {
}

// Check server state
order, exists := ts.GetOrder("my-cloid")
order, exists := ts.GetOrder(cloid)
if !exists {
t.Fatal("Order not found")
}
Expand Down Expand Up @@ -347,8 +391,8 @@ for _, req := range rawReqs {
Check the server's internal order state:

```go
// Get order by CLOID
order, exists := ts.GetOrder("my-cloid-123")
// Get order by CLOID (must be the exact CLOID used when creating the order)
order, exists := ts.GetOrder("00000000000000000000000000000001")
if exists {
fmt.Println(order.Order.Coin) // "ETH"
fmt.Println(order.Status) // "open"
Expand Down Expand Up @@ -421,7 +465,7 @@ You can also test the mock server manually using curl:
# Health check
curl http://localhost:8080/health

# Create an order
# Create an order (note: CLOID must be 32 hex characters)
curl -X POST http://localhost:8080/exchange \
-H "Content-Type: application/json" \
-d '{
Expand All @@ -432,7 +476,7 @@ curl -X POST http://localhost:8080/exchange \
"is_buy": true,
"sz": 1.0,
"limit_px": 3000.00,
"cloid": "test-order-1"
"cloid": "00000000000000000000000000000001"
}]
},
"nonce": 123,
Expand All @@ -447,12 +491,12 @@ curl -X POST http://localhost:8080/info \

## Limitations

- **No signature validation** - All requests are accepted regardless of signature
- **No real order matching** - Orders are just stored in memory
- **No real order matching** - Orders are just stored in memory, no actual trading logic
- **Limited WebSocket subscriptions** - Only orderUpdates and l2Book/bbo (no trades, candles, etc.)
- **Simplified responses** - Some fields may be omitted or simplified
- **Simplified responses** - Some fields may be omitted or simplified compared to production API
- **No persistence** - All state is lost when the server restarts
- **No rate limiting** - Unlimited requests accepted
- **Testnet behavior** - Signature recovery uses testnet EIP-712 domain (chainId: 1337)

## Development

Expand All @@ -476,6 +520,7 @@ hyperliquid-mock/
├── server.go # HTTP server setup
├── handlers.go # Request handlers
├── handlers_test.go # Handler unit tests
├── signature_recovery.go # ECDSA signature recovery for wallet isolation
├── websocket.go # WebSocket connection & subscription management
├── websocket_example_test.go # WebSocket usage examples
├── types.go # Request/response types
Expand Down
11 changes: 7 additions & 4 deletions WEBSOCKET.md
Original file line number Diff line number Diff line change
Expand Up @@ -324,17 +324,20 @@ This implementation follows the official Hyperliquid WebSocket API specification
- `/upstream-api-docs/subscriptions.md`

Key differences from production API:
- User tracking simplified (uses placeholder in mock)
- BBO updates are manual/on-demand (vs. real-time market data)
- Limited to orderUpdates and l2Book/bbo subscriptions
- No authentication required
- Uses testnet EIP-712 domain for signature verification (chainId: 1337)

## Implemented Features

- **Wallet-based order filtering** - Orders are automatically isolated by wallet address
- **Signature recovery** - ECDSA signature verification extracts wallet addresses
- **Real-time order updates** - WebSocket broadcasts filtered by wallet ownership

## Future Enhancements

Potential additions (not currently in scope):
- Additional subscription types (trades, candles, userFills, etc.)
- Automatic periodic BBO updates
- User-based order filtering
- WebSocket authentication
- Connection heartbeat/ping-pong
- Reconnection handling
13 changes: 10 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,38 @@ module github.com/recomma/hyperliquid-mock
go 1.25.0

require (
github.com/ethereum/go-ethereum v1.16.5
github.com/sonirico/go-hyperliquid v0.16.1
github.com/ethereum/go-ethereum v1.16.7
github.com/gorilla/websocket v1.5.3
github.com/hashicorp/go-msgpack v0.5.5
github.com/sonirico/go-hyperliquid v0.21.0
)

require (
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect
github.com/armon/go-radix v1.0.0 // indirect
github.com/bits-and-blooms/bitset v1.24.0 // indirect
github.com/consensys/gnark-crypto v0.19.0 // indirect
github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
github.com/elastic/go-sysinfo v1.15.4 // indirect
github.com/elastic/go-windows v1.0.2 // indirect
github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect
github.com/ethereum/go-verkle v0.2.2 // indirect
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
github.com/holiman/uint256 v1.3.2 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.9.1 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/procfs v0.17.0 // indirect
github.com/rs/zerolog v1.34.0 // indirect
github.com/sonirico/vago v0.9.0 // indirect
github.com/sonirico/vago/lol v0.0.0-20250901170347-2d1d82c510bd // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/supranational/blst v0.3.16 // indirect
github.com/valyala/fastjson v1.6.4 // indirect
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
Expand All @@ -40,5 +46,6 @@ require (
golang.org/x/net v0.43.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
howett.net/plist v1.0.1 // indirect
)
14 changes: 12 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU=
github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA=
github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8=
github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
Expand Down Expand Up @@ -27,6 +29,8 @@ github.com/ethereum/c-kzg-4844/v2 v2.1.5 h1:aVtoLK5xwJ6c5RiqO8g8ptJ5KU+2Hdquf6G3
github.com/ethereum/c-kzg-4844/v2 v2.1.5/go.mod h1:u59hRTTah4Co6i9fDWtiCjTrblJv0UwsqZKCc0GfgUs=
github.com/ethereum/go-ethereum v1.16.5 h1:GZI995PZkzP7ySCxEFaOPzS8+bd8NldE//1qvQDQpe0=
github.com/ethereum/go-ethereum v1.16.5/go.mod h1:kId9vOtlYg3PZk9VwKbGlQmSACB5ESPTBGT+M9zjmok=
github.com/ethereum/go-ethereum v1.16.7 h1:qeM4TvbrWK0UC0tgkZ7NiRsmBGwsjqc64BHo20U59UQ=
github.com/ethereum/go-ethereum v1.16.7/go.mod h1:Fs6QebQbavneQTYcA39PEKv2+zIjX7rPUZ14DER46wk=
github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8=
github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk=
github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY=
Expand All @@ -42,11 +46,17 @@ github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
github.com/hashicorp/go-msgpack v0.5.5 h1:i9R9JSrqIz0QVLz3sz+i3YJdT7TTSLcfLLzJi9aZTuI=
github.com/hashicorp/go-msgpack v0.5.5/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA=
github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
Expand Down Expand Up @@ -85,8 +95,8 @@ github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1:Bn1aCHHRnjv4Bl16T8rcaFjYSrGrIZvpiGO6P3Q4GpU=
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/sonirico/go-hyperliquid v0.16.1 h1:s7IvY8BNSm0zhvgt+fEWykAEufIeZN6jZjPCs20qOj8=
github.com/sonirico/go-hyperliquid v0.16.1/go.mod h1:m3AY4P0ngPt7pRqovV1pt4E5dkzDLoPAJ7Hr7UHf4tY=
github.com/sonirico/go-hyperliquid v0.21.0 h1:IXoZEcGiVL/h4TgJrOuMaj9ptM+e7vW4q+MYJesVG5U=
github.com/sonirico/go-hyperliquid v0.21.0/go.mod h1:sH51Vsu+tPUwc95TL2MoQ8YXSewLWBEJirgzo7sZx6w=
github.com/sonirico/vago v0.9.0 h1:DF2OWW2Aaf1xPZmnFv79kBrHmjKX3mVvMbP08vERlKo=
github.com/sonirico/vago v0.9.0/go.mod h1:fZxV1RzMe2eaZokbbDvuyoOzG3YapzqRQoOiD9VyJH0=
github.com/sonirico/vago/lol v0.0.0-20250901170347-2d1d82c510bd h1:rbvNORW8/0AtH/8W/SUwUykbuh2SeQBrNgFLqYpGTWY=
Expand Down
Loading