Repository Public
1// Replay of what scripts/hl-live-testnet.mts recorded on the Hyperliquid TESTNET (unfunded run): the isolated signer's two keys2// at work. For every request that was sent, the signature is re-verified here against OUR hash of the recorded payload, and3// the venue's answer must name the very address we recover — the venue computed the same hash, expiry included.4import { readFileSync } from 'node:fs';5import { describe, expect, it } from 'vitest';6import { buildApproveAgent, l1SignPayload, recoverTypedDataSigner } from '../../src/venues/hyperliquid/signing.ts';78interface Exchange { step: string; request?: { action: any; nonce: number; signature: unknown; vaultAddress: string | null; expiresAfter: number | null }; status?: number; answer: any }9const proof = JSON.parse(readFileSync(new URL('../fixtures/hyperliquid/testnet-live-proof.json', import.meta.url), 'utf8')) as { network: string; account: string; agent: string; exchanges: Exchange[] };10const sent = proof.exchanges.filter((e) => e.request);1112describe('testnet proof of the live signing path (recorded)', () => {13 it('was recorded on testnet with two different keys', () => {14 expect(proof.network).toBe('testnet');15 expect(proof.account).toMatch(/^0x[0-9a-f]{40}$/); expect(proof.agent).toMatch(/^0x[0-9a-f]{40}$/);16 expect(proof.agent).not.toBe(proof.account);17 expect(sent.map((e) => e.request!.action.type)).toEqual(['approveAgent', 'order', 'cancelByCloid', 'updateLeverage']);18 });1920 it('approveAgent: signed by the WALLET key, naming the trading key; the venue recovered the wallet key and asked for a deposit', async () => {21 const e = sent[0]!, a = e.request!.action;22 expect(a.agentAddress).toBe(proof.agent);23 expect(a.agentName).toMatch(/^spw-live valid_until \d{13}$/);24 const td = buildApproveAgent({ network: 'testnet', agentAddress: a.agentAddress, agentName: a.agentName, nonce: a.nonce, signatureChainId: a.signatureChainId }).typedData;25 expect(await recoverTypedDataSigner(td, e.request!.signature)).toBe(proof.account);26 expect(e.status).toBe(200);27 expect(e.answer).toEqual({ status: 'err', response: `Must deposit before performing actions. User: ${proof.account}` });28 // the same refusal, reached through the API's authorization route29 const viaApi = proof.exchanges.find((x) => x.step.startsWith('API authorization/approve'))!;30 expect(viaApi.status).toBe(422);31 expect(viaApi.answer.message).toContain(`Must deposit before performing actions. User: ${proof.account}`);32 });3334 it('order, cancel and leverage change: signed by the TRADING key with a venue expiry; the venue recovered the trading key', async () => {35 for (const e of sent.slice(1)) {36 const r = e.request!;37 expect(r.vaultAddress, e.step).toBeNull();38 expect(r.expiresAfter, e.step).toBe(r.nonce + 90_000); // every signed action dies after 90 s39 const td = l1SignPayload(r.action, { network: 'testnet', nonce: r.nonce, vaultAddress: null, expiresAfter: r.expiresAfter }).typedData;40 expect(await recoverTypedDataSigner(td, r.signature), e.step).toBe(proof.agent);41 expect(e.answer, e.step).toEqual({ status: 'err', response: `User or API Wallet ${proof.agent} does not exist.` });42 expect(JSON.stringify(e.answer)).not.toContain(proof.account); // the wallet key is nowhere in the trading path43 }44 const order = sent[1]!.request!.action.orders[0];45 expect(order).toMatchObject({ a: 3, b: true, r: false, t: { limit: { tif: 'Alo' } } }); // BTC is asset 3 on testnet (pinned table)46 expect(order.c).toMatch(/^0x[0-9a-f]{32}$/);47 });4849 it('the signer refused, with its own exchange price, what it must refuse', () => {50 const refusals = Object.fromEntries(proof.exchanges.filter((e) => e.step.startsWith('signer: ')).map((e) => [e.step.slice(8), String(e.answer)]));51 expect(refusals['order at half the market price (passkey-approved all the same)']).toMatch(/^signer_price: price \d+ is more than 1000 bps away from the signer's own BTC price \d+/);52 expect(refusals['order above the authorized notional']).toMatch(/^signer_policy: order notional/);53 expect(refusals['order on an instrument outside the authorization']).toMatch(/^signer_policy: hyperliquid:ETH is not allowed/);54 expect(refusals['payload differs from the order shown for approval']).toMatch(/^signer_mismatch/);55 expect(refusals['an approveAgent for another address smuggled through /venue/sign']).toMatch(/only be signed through \/venue\/authorize/);56 expect(refusals['a transfer (usdClassTransfer) smuggled through /venue/sign']).toMatch(/not an order, a cancel or a leverage change/);57 expect(refusals['a cancel after revocation']).toMatch(/^signer_revoked/);58 expect(refusals['an order after revocation']).toMatch(/^signer_revoked/);59 });60});61