From 156ce22474b68b03cdab076f116f6f360e2ca7fc Mon Sep 17 00:00:00 2001 From: jackyu66git Date: Mon, 4 May 2026 16:21:57 +0800 Subject: [PATCH] feat: HL EVM signing via sonirico/go-hyperliquid SDK - testnet orders work --- .env | 6 +- exchange-monitor.log | 19 +++++- exchange/hyperliquid_trade.go | 99 +++++++++------------------- hl_helper/package-lock.json | 118 ---------------------------------- hl_helper/package.json | 6 -- hl_helper/sign.js | 94 --------------------------- hl_helper/test_order.js | 86 ------------------------- hl_helper/test_order2.js | 110 ------------------------------- 8 files changed, 52 insertions(+), 486 deletions(-) delete mode 100644 hl_helper/package-lock.json delete mode 100644 hl_helper/package.json delete mode 100644 hl_helper/sign.js delete mode 100644 hl_helper/test_order.js delete mode 100644 hl_helper/test_order2.js diff --git a/.env b/.env index 3cec780..8df24df 100644 --- a/.env +++ b/.env @@ -7,6 +7,8 @@ BITGET_API_SECRET=e914e1...d620 BITGET_PASSPHRASE=Yoyousoft007 # HyperLiquid testnet API (EVM wallet) -# 主账号地址(主网有 $13.65) +# Private key = API钱包私钥 +# Address = 主账号地址(有999 USDC现货) HL_PRIVATE_KEY=0xcb4e58b6c0921fd4aa8e49cd8a9f06b98b4e6b9cdf7c60c63ba1416856bb0b2a -HL_ADDRESS=0x601B09803A46F640C9416C124A7DB858a615F5fF +HL_ADDRESS=0xA834b6d3Fa1D8A55ea8e502685ef5cbD2b2D3343 +HL_API_ADDRESS=0x601B09803A46F640C9416C124A7DB858a615F5fF diff --git a/exchange-monitor.log b/exchange-monitor.log index 81e53b6..791b0ca 100644 --- a/exchange-monitor.log +++ b/exchange-monitor.log @@ -263,5 +263,20 @@ 2026/05/04 16:14:51 main.go:165: [Status] 1956 prices / 1951 coins connected 2026/05/04 16:15:21 main.go:165: [Status] 1958 prices / 1953 coins connected 2026/05/04 16:15:51 main.go:165: [Status] 1958 prices / 1953 coins connected -5:21 main.go:165: [Status] 1958 prices / 1953 coins connected -2026/05/04 16:15:51 main.go:165: [Status] 1958 prices / 1953 coins connected +2026/05/04 16:16:21 main.go:165: [Status] 1958 prices / 1953 coins connected +2026/05/04 16:16:51 main.go:165: [Status] 1958 prices / 1953 coins connected +2026/05/04 16:17:21 main.go:165: [Status] 1958 prices / 1953 coins connected +2026/05/04 16:17:51 main.go:165: [Status] 1958 prices / 1953 coins connected +2026/05/04 16:18:21 main.go:165: [Status] 1958 prices / 1953 coins connected +2026/05/04 16:18:51 main.go:165: [Status] 1958 prices / 1953 coins connected +2026/05/04 16:19:21 main.go:165: [Status] 1958 prices / 1953 coins connected +2026/05/04 16:19:37 connector.go:126: [HyperLiquid WS] Read error: websocket: close 1000 (normal): Expired +2026/05/04 16:19:37 connector.go:52: [HyperLiquid WS] Connecting... (attempt 1) +2026/05/04 16:19:38 hyperliquid.go:32: [HL WS] Connected +2026/05/04 16:19:51 main.go:165: [Status] 1958 prices / 1953 coins connected +2026/05/04 16:20:21 main.go:165: [Status] 1958 prices / 1953 coins connected +2026/05/04 16:20:51 main.go:165: [Status] 1958 prices / 1953 coins connected +2026/05/04 16:21:21 main.go:165: [Status] 1958 prices / 1953 coins connected +2026/05/04 16:21:51 main.go:165: [Status] 1958 prices / 1953 coins connected +1:21 main.go:165: [Status] 1958 prices / 1953 coins connected +2026/05/04 16:21:51 main.go:165: [Status] 1958 prices / 1953 coins connected diff --git a/exchange/hyperliquid_trade.go b/exchange/hyperliquid_trade.go index e94d6d5..dc5579e 100644 --- a/exchange/hyperliquid_trade.go +++ b/exchange/hyperliquid_trade.go @@ -21,7 +21,7 @@ type HyperLiquidTrade struct { exchange *hl.Exchange info *hl.Info privateKey *ecdsa.PrivateKey - mainAddress string // main account address + mainAddress string nonceMu sync.Mutex lastNonce int64 configured bool @@ -32,7 +32,6 @@ func NewHyperLiquidTrade(privateKeyHex, mainAddress, apiAddress string) (*HyperL return &HyperLiquidTrade{}, nil } - // Parse ECDSA private key (supports 0x prefix) keyHex := strings.TrimPrefix(privateKeyHex, "0x") keyBytes, err := hex.DecodeString(keyHex) if err != nil { @@ -44,24 +43,20 @@ func NewHyperLiquidTrade(privateKeyHex, mainAddress, apiAddress string) (*HyperL return nil, fmt.Errorf("convert to ECDSA: %w", err) } + // Initialize SDK Info (auto-fetches meta + spotMeta) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + info := hl.NewInfo(ctx, hl.TestnetAPIURL, true, nil, nil, nil) + t := &HyperLiquidTrade{ privateKey: privKey, mainAddress: mainAddress, + info: info, configured: true, } - // Initialize SDK info (fetch meta for exchange) - ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) - defer cancel() - - t.info = hl.NewInfo(ctx, hl.TestnetAPIURL, true, nil, nil, nil) - if t.info == nil { - return nil, fmt.Errorf("failed to create HL Info") - } - - // Wait briefly for meta to be fetched (Info fetches meta in constructor) - time.Sleep(2 * time.Second) - + // Initialize exchange lazily on first order return t, nil } @@ -69,53 +64,37 @@ func (h *HyperLiquidTrade) initExchange() error { if h.exchange != nil { return nil } - if !h.configured { return fmt.Errorf("HyperLiquid not configured") } - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() - meta, err := h.fetchMeta(ctx) + meta, err := h.info.Meta(ctx) if err != nil { return fmt.Errorf("fetch meta: %w", err) } + spotMeta, err := h.info.SpotMeta(ctx) + if err != nil { + return fmt.Errorf("fetch spot meta: %w", err) + } + h.exchange = hl.NewExchange( ctx, h.privateKey, hl.TestnetAPIURL, meta, - "", // vault - h.mainAddress, // account address - nil, // spot meta - nil, // perp dex + "", + h.mainAddress, + spotMeta, + nil, ) return nil } -func (h *HyperLiquidTrade) fetchMeta(ctx context.Context) (*hl.Meta, error) { - // Use info endpoint to get meta - resp, err := h.postInfo(ctx, map[string]any{"type": "meta"}) - if err != nil { - return nil, err - } - - // Parse as Meta struct - var meta hl.Meta - if err := json.Unmarshal(resp, &meta); err != nil { - return nil, fmt.Errorf("parse meta: %w", err) - } - return &meta, nil -} - -func (h *HyperLiquidTrade) postInfo(ctx context.Context, payload map[string]any) ([]byte, error) { - // Simple HTTP POST to info endpoint - return nil, fmt.Errorf("not implemented via SDK - use Info directly") -} - func (h *HyperLiquidTrade) IsConfigured() bool { return h.configured } @@ -131,58 +110,42 @@ func (h *HyperLiquidTrade) PlaceMarketOrder(coin, side, sz string) (string, erro } isBuy := side == "buy" - - // Parse size from string to float64 size, err := strconv.ParseFloat(sz, 64) if err != nil { return "", fmt.Errorf("parse size %s: %w", sz, err) } - // Get current price for slippage calculation + // Get current price for slippage ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() - allMids, err := h.fetchAllMids(ctx) + mids, err := h.info.AllMids(ctx) if err != nil { - return "", fmt.Errorf("fetch prices: %w", err) + return "", fmt.Errorf("fetch mids: %w", err) } - priceStr, ok := allMids[coin] + priceStr, ok := mids[coin] if !ok { - return "", fmt.Errorf("coin %s not found in allMids", coin) + return "", fmt.Errorf("coin %s not found", coin) } midPx, _ := strconv.ParseFloat(priceStr, 64) - // Slippage price: buy = ask (mid * 1.02), sell = bid (mid * 0.98) - var limitPx float64 - if isBuy { - limitPx = midPx * 2.0 // aggressive buy - } else { - limitPx = midPx * 0.5 // aggressive sell + // Aggressive IOC: buy above market, sell below + limitPx := midPx * 2.0 + if !isBuy { + limitPx = midPx * 0.5 } result, err := h.exchange.MarketOpen(ctx, coin, isBuy, size, &limitPx, 0.05, nil, nil) if err != nil { - return "", fmt.Errorf("market order: %w", err) + return "", fmt.Errorf("market open: %w", err) } - // Serialize response + // Marshal response respJSON, _ := json.Marshal(result) return string(respJSON), nil } -func (h *HyperLiquidTrade) fetchAllMids(ctx context.Context) (map[string]string, error) { - // Try to get allMids via the SDK's Info if available - if h.info != nil { - mids, err := h.info.AllMids(ctx) - if err != nil { - return nil, err - } - return mids, nil - } - return nil, fmt.Errorf("info not initialized") -} - // GetHLSize calculates size for a given USD amount on HyperLiquid. func GetHLSize(coin string, amountUSD, price float64) string { sz := amountUSD / price diff --git a/hl_helper/package-lock.json b/hl_helper/package-lock.json deleted file mode 100644 index 7ee62e7..0000000 --- a/hl_helper/package-lock.json +++ /dev/null @@ -1,118 +0,0 @@ -{ - "name": "hl_helper", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "dependencies": { - "@msgpack/msgpack": "^3.0.0", - "ethers": "^6.0.0" - } - }, - "node_modules/@adraffy/ens-normalize": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", - "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==" - }, - "node_modules/@msgpack/msgpack": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.3.tgz", - "integrity": "sha512-47XIizs9XZXvuJgoaJUIE2lFoID8ugvc0jzSHP+Ptfk8nTbnR8g788wv48N03Kx0UkAv559HWRQ3yzOgzlRNUA==", - "engines": { - "node": ">= 18" - } - }, - "node_modules/@noble/curves": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", - "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", - "dependencies": { - "@noble/hashes": "1.3.2" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@noble/hashes": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", - "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@types/node": { - "version": "22.7.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", - "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", - "dependencies": { - "undici-types": "~6.19.2" - } - }, - "node_modules/aes-js": { - "version": "4.0.0-beta.5", - "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", - "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==" - }, - "node_modules/ethers": { - "version": "6.16.0", - "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.16.0.tgz", - "integrity": "sha512-U1wulmetNymijEhpSEQ7Ct/P/Jw9/e7R1j5XIbPRydgV2DjLVMsULDlNksq3RQnFgKoLlZf88ijYtWEXcPa07A==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/ethers-io/" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ], - "dependencies": { - "@adraffy/ens-normalize": "1.10.1", - "@noble/curves": "1.2.0", - "@noble/hashes": "1.3.2", - "@types/node": "22.7.5", - "aes-js": "4.0.0-beta.5", - "tslib": "2.7.0", - "ws": "8.17.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tslib": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", - "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==" - }, - "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" - }, - "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - } - } -} diff --git a/hl_helper/package.json b/hl_helper/package.json deleted file mode 100644 index 17fdc06..0000000 --- a/hl_helper/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "dependencies": { - "ethers": "^6.0.0", - "@msgpack/msgpack": "^3.0.0" - } -} diff --git a/hl_helper/sign.js b/hl_helper/sign.js deleted file mode 100644 index ab56525..0000000 --- a/hl_helper/sign.js +++ /dev/null @@ -1,94 +0,0 @@ -const https = require('https'); - -// Read args: [action_json, nonce, private_key, address, isMainnet] -const [actionJson, nonceStr, privKey, address, isMainnetStr] = process.argv.slice(2); - -if (!actionJson || !nonceStr || !privKey) { - console.log(JSON.stringify({ error: "Usage: node hl_sign.js '' [address] [isMainnet]" })); - process.exit(1); -} - -const action = JSON.parse(actionJson); -const nonce = parseInt(nonceStr); -const isMainnet = isMainnetStr === 'true'; - -// ---- Build the EIP-712 typed data ---- -const { ethers } = require('ethers'); - -async function main() { - const wallet = new ethers.Wallet(privKey); - const apiAddr = wallet.address; - - // 1. Normalize action (remove trailing zeros) - let normalizedAction = JSON.parse(JSON.stringify(action)); - function normalize(obj) { - if (!obj || typeof obj !== 'object') return; - if (Array.isArray(obj)) { - for (const item of obj) normalize(item); - } else { - for (const key of Object.keys(obj)) { - if ((key === 'p' || key === 's') && typeof obj[key] === 'string') { - obj[key] = obj[key].replace(/\.?0+$/, '').replace(/^-0$/, '0'); - } else { - normalize(obj[key]); - } - } - } - } - normalize(normalizedAction); - - // 2. msgpack encode the action - const { encode } = require('@msgpack/msgpack'); - const msgBytes = encode(normalizedAction); - - // 3. Build hash: msgpack + nonce(8 bytes big endian) + flag(1 byte) - const totalLen = msgBytes.length + 8 + 1; - const data = new Uint8Array(totalLen); - data.set(msgBytes, 0); - const view = new DataView(data.buffer); - view.setBigUint64(msgBytes.length, BigInt(nonce), false); - view.setUint8(msgBytes.length + 8, 0); // no vault address - - const hash = ethers.keccak256(data); - - // 4. EIP-712 signing - const source = isMainnet ? 'a' : 'b'; - const domain = { - name: 'Exchange', - version: '1', - chainId: 1337, - verifyingContract: '0x0000000000000000000000000000000000000000', - }; - const types = { - Agent: [ - { name: 'source', type: 'string' }, - { name: 'connectionId', type: 'bytes32' }, - ], - }; - const message = { source, connectionId: hash }; - - const signature = await wallet.signTypedData(domain, types, message); - const sig = ethers.Signature.from(signature); - - // 5. Build result - const result = { - action: action, - nonce: nonce, - signature: { - r: sig.r, - s: sig.s, - v: sig.v, - }, - }; - - // If address is provided and != wallet.address, it's the main account - // The SDK automatically handles this via the accountAddress constructor param - // For order placement via exchange API, we just send the signature - - console.log(JSON.stringify(result)); -} - -main().catch(err => { - console.log(JSON.stringify({ error: err.message })); - process.exit(1); -}); diff --git a/hl_helper/test_order.js b/hl_helper/test_order.js deleted file mode 100644 index cc9c880..0000000 --- a/hl_helper/test_order.js +++ /dev/null @@ -1,86 +0,0 @@ -const https = require('https'); -const { ethers } = require('ethers'); -const { encode } = require('@msgpack/msgpack'); - -const host = "api.hyperliquid-testnet.xyz"; -const privKey = "0xcb4e58b6c0921fd4aa8e49cd8a9f06b98b4e6b9cdf7c60c63ba1416856bb0b2a"; -const wallet = new ethers.Wallet(privKey); - -async function api(method, path, payload) { - return new Promise((resolve, reject) => { - const data = JSON.stringify(payload); - const opts = { hostname: host, path, method, headers: { 'Content-Type': 'application/json' } }; - const req = https.request(opts, res => { - let body = ''; - res.on('data', c => body += c); - res.on('end', () => { try { resolve(JSON.parse(body)); } catch(e) { resolve(body); } }); - }); - req.on('error', reject); - req.write(data); req.end(); - }); -} - -async function sign(action) { - const nonce = Date.now() * 1000000 + Math.floor(Math.random() * 1000000); - - // Normalize - function normalize(obj) { - if (!obj || typeof obj !== 'object') return; - if (Array.isArray(obj)) { for (const i of obj) normalize(i); } - else { for (const k of Object.keys(obj)) { - if ((k === 'p' || k === 's') && typeof obj[k] === 'string') - obj[k] = obj[k].replace(/\.?0+$/, '').replace(/^-0$/, '0'); - else normalize(obj[k]); - }} - } - const act = JSON.parse(JSON.stringify(action)); - normalize(act); - - const msgBytes = encode(act); - const buf = new Uint8Array(msgBytes.length + 8 + 1); - buf.set(msgBytes, 0); - new DataView(buf.buffer).setBigUint64(msgBytes.length, BigInt(nonce), false); - buf[msgBytes.length + 8] = 0; - const hash = ethers.keccak256(buf); - - const sig = ethers.Signature.from(await wallet.signTypedData( - { name: 'Exchange', version: '1', chainId: 1337, verifyingContract: '0x0000000000000000000000000000000000000000' }, - { Agent: [{ name: 'source', type: 'string' }, { name: 'connectionId', type: 'bytes32' }] }, - { source: 'b', connectionId: hash } - )); - - return { action, nonce, signature: { r: sig.r, s: sig.s, v: sig.v } }; -} - -async function main() { - // Get meta to find asset index - const meta = await api('POST', '/info', { type: 'meta' }); - const btcIdx = meta.universe.findIndex(u => u.name === 'BTC'); - console.log(`BTC asset index: ${btcIdx}`); - - // Get mid price for BTC - const mids = await api('POST', '/info', { type: 'allMids' }); - const btcMid = parseFloat(mids['BTC']); - console.log(`BTC mid: ${btcMid}`); - - // Place a small market buy order (IOC, high limit price) - const orderAction = { - type: 'order', - orders: [{ - a: btcIdx, - b: true, // buy - p: (btcMid * 2).toFixed(0), - s: '0.001', - r: false, - t: { limit: { tif: 'Ioc' } } - }], - grouping: 'na' - }; - - const signed = await sign(orderAction); - console.log('\nSending order...'); - const result = await api('POST', '/exchange', signed); - console.log(JSON.stringify(result, null, 2)); -} - -main().catch(console.error); diff --git a/hl_helper/test_order2.js b/hl_helper/test_order2.js deleted file mode 100644 index b36ee25..0000000 --- a/hl_helper/test_order2.js +++ /dev/null @@ -1,110 +0,0 @@ -const https = require('https'); -const { Wallet, keccak256, Signature, verifyTypedData } = require('ethers'); -const { encode } = require('@msgpack/msgpack'); - -const host = "api.hyperliquid-testnet.xyz"; -const privKey = "0xcb4e58b6c0921fd4aa8e49cd8a9f06b98b4e6b9cdf7c60c63ba1416856bb0b2a"; -const wallet = new Wallet(privKey); -console.log("Wallet address:", wallet.address); - -async function api(method, path, payload) { - return new Promise((resolve, reject) => { - const data = JSON.stringify(payload); - const req = https.request({ hostname: host, path, method, headers: { 'Content-Type': 'application/json' } }, res => { - let body = ''; - res.on('data', c => body += c); - res.on('end', () => { try { resolve(JSON.parse(body)); } catch(e) { resolve(body); } }); - }); - req.on('error', reject); - req.write(data); req.end(); - }); -} - -function floatToWire(x) { - const rounded = x.toFixed(8); - let normalized = rounded.replace(/\.?0+$/, ''); - if (normalized === '-0') normalized = '0'; - return normalized; -} - -async function main() { - // Get meta - const meta = await api('POST', '/info', { type: 'meta' }); - const universe = meta.universe; - const btc = universe.find(u => u.name === 'BTC'); - const btcIdx = universe.indexOf(btc); - console.log(`BTC index: ${btcIdx}`); - - const mids = await api('POST', '/info', { type: 'allMids' }); - const mid = parseFloat(mids['BTC']); - const limitPx = mid * 2; - const sz = 0.001; - - // Build exactly like floatToWire does - const pxStr = floatToWire(limitPx); - const szStr = floatToWire(sz); - console.log(`pxStr: ${pxStr}, szStr: ${szStr}`); - - // Build order wire EXACTLY like SDK - const orderWire = { - a: btcIdx, - b: true, - p: pxStr, - s: szStr, - r: false, - t: { limit: { tif: 'Ioc' } } - }; - console.log("Order wire:", JSON.stringify(orderWire)); - - // Sign step by step - const nonce = Date.now() * 1000000 + Math.floor(Math.random() * 1000000); - - // Normalize trailing zeros - function normalize(obj) { - if (!obj || typeof obj !== 'object') return; - if (Array.isArray(obj)) { for (const i of obj) normalize(i); } - else { for (const k of Object.keys(obj)) { - if ((k === 'p' || k === 's') && typeof obj[k] === 'string') - obj[k] = obj[k].replace(/\.?0+$/, '').replace(/^-0$/, '0'); - else normalize(obj[k]); - }} - } - - const action = { type: 'order', orders: [JSON.parse(JSON.stringify(orderWire))], grouping: 'na' }; - normalize(action); // Apply to full action with orders array - console.log("Normalized action:", JSON.stringify(action)); - - const msgBytes = encode(action); - console.log(`Msgpack bytes: ${Buffer.from(msgBytes).toString('hex')}`); - - const buf = new Uint8Array(msgBytes.length + 8 + 1); - buf.set(msgBytes, 0); - new DataView(buf.buffer).setBigUint64(msgBytes.length, BigInt(nonce), false); - buf[msgBytes.length + 8] = 0; - const hash = keccak256(buf); - console.log(`Hash: ${hash}`); - - // Sign and verify - const sig = Signature.from(await wallet.signTypedData( - { name: 'Exchange', version: '1', chainId: 1337, verifyingContract: '0x0000000000000000000000000000000000000000' }, - { Agent: [{ name: 'source', type: 'string' }, { name: 'connectionId', type: 'bytes32' }] }, - { source: 'b', connectionId: hash } - )); - - // Verify the signature recovers to our wallet - const domain = { name: 'Exchange', version: '1', chainId: 1337, verifyingContract: '0x0000000000000000000000000000000000000000' }; - const types = { Agent: [{ name: 'source', type: 'string' }, { name: 'connectionId', type: 'bytes32' }] }; - const message = { source: 'b', connectionId: hash }; - const recoveredAddr = verifyTypedData(domain, types, message, sig); - console.log(`Recovered address: ${recoveredAddr}`); - console.log(`Wallet address: ${wallet.address.toLowerCase()}`); - console.log(`Match: ${recoveredAddr.toLowerCase() === wallet.address.toLowerCase()}`); - - // Send order - const payload = { action, nonce, signature: { r: sig.r, s: sig.s, v: sig.v } }; - console.log('\nSending order...'); - const result = await api('POST', '/exchange', payload); - console.log(JSON.stringify(result, null, 2)); -} - -main().catch(console.error);