Repository Public
1// The testnet proof recorded on 2026-11-11 by `scripts/hl-testnet.mts proof --record` (fixtures/hyperliquid/testnet-exchange-proof.json):2// a throwaway key generated in memory, an account that never existed on the venue. Replayed here without network.3import { describe, expect, it } from 'vitest';4import { VenueError } from '../../src/index.ts';5import { createHlHttp, createHlTrading, l1SignPayload, recoverTypedDataSigner, type HlUserSignPayload } from '../../src/venues/hyperliquid/index.ts';6import { raw, replay, universe } from './helpers.ts';78const proof = raw('testnet-exchange-proof.json') as { account: string; exchanges: { step: string; request: any; status: number; answer: any; namesSigner: boolean }[] };9const t = universe('testnet');10const trading = (expiresAfterMs: number | null) => createHlTrading({ network: 'testnet', assets: t.assets, expiresAfterMs });11const good = proof.exchanges.filter((x) => !x.step.startsWith('WRONG')), wrong = proof.exchanges.filter((x) => x.step.startsWith('WRONG'));12const l1 = (r: any) => l1SignPayload(r.action, { network: 'testnet', nonce: r.nonce, vaultAddress: r.vaultAddress, expiresAfter: r.expiresAfter }).typedData;1314describe('hyperliquid testnet proof (recorded)', () => {15 it('every correctly signed action recovers to OUR address locally, and the venue named that same address in its refusal', async () => {16 expect(good.map((x) => x.request.action.type)).toEqual(['approveAgent', 'order', 'order', 'cancel', 'cancelByCloid', 'updateLeverage', 'order']);17 for (const x of good) {18 const r = x.request;19 expect(x.status).toBe(200);20 expect(x.answer.status).toBe('err');21 expect(x.answer.response).toContain(proof.account);22 const td = r.action.type === 'approveAgent'23 ? (trading(null).buildApproveAgent({ agentAddress: r.action.agentAddress, agentName: r.action.agentName ?? null, nonce: r.nonce }).sign.payload as HlUserSignPayload).typedData24 : l1(r);25 expect(await recoverTypedDataSigner(td, r.signature)).toBe(proof.account);26 }27 expect(good[0]!.answer.response).toBe(`Must deposit before performing actions. User: ${proof.account}`);28 for (const x of good.slice(1)) expect(x.answer.response).toBe(`User or API Wallet ${proof.account} does not exist.`);29 expect(good[6]!.request.expiresAfter).toBe(good[6]!.request.nonce + 60_000);30 });3132 it('describe() reads each recorded action back', () => {33 const kinds = good.map((x) => {34 const r = x.request, user = r.action.type === 'approveAgent', tr = trading(r.expiresAfter === null ? null : r.expiresAfter - r.nonce);35 const payload = user36 ? tr.buildApproveAgent({ agentAddress: r.action.agentAddress, agentName: r.action.agentName ?? null, nonce: r.nonce }).sign.payload37 : { scheme: 'l1', action: r.action, nonce: r.nonce, vaultAddress: r.vaultAddress, expiresAfter: r.expiresAfter, typedData: l1(r) };38 return tr.describe({ venue: 'hyperliquid', network: 'testnet', kind: user ? 'hl_user' : 'hl_l1', payload }).kind;39 });40 expect(kinds).toEqual(['approve_agent', 'order', 'order', 'cancel', 'cancel', 'leverage', 'order']);41 });4243 it('wrong signatures were refused differently: another address, or no recovery at all', async () => {44 expect(wrong.length).toBe(4);45 for (const x of wrong) { expect(x.answer.status).toBe('err'); expect(x.answer.response).not.toContain(proof.account); }46 expect(wrong[3]!.answer.response).toBe('Unable to recover signer.');47 // the address the venue named is exactly the one our own code recovers from that signature over the submitted payload48 for (const x of wrong.slice(0, 3)) expect(x.answer.response).toBe(`User or API Wallet ${await recoverTypedDataSigner(l1(x.request), x.request.signature)} does not exist.`);49 });5051 it('replayed through submit(): VenueError rejected, with the venue\'s words, and the same request body', async () => {52 const x = good[1]!, r = replay({ exchange: { status: x.status, body: x.answer } }), now = () => x.request.nonce;53 const tr = createHlTrading({ network: 'testnet', assets: t.assets, http: createHlHttp({ network: 'testnet', fetch: r.fetchImpl, now }), now });54 try { await tr.submit(x.request.action, x.request.signature, x.request.nonce); expect.unreachable(); }55 catch (e) { expect(e).toBeInstanceOf(VenueError); expect((e as VenueError).code).toBe('rejected'); expect((e as VenueError).message).toContain('does not exist'); }56 expect(r.calls[0]!.body).toEqual(x.request);57 });58});59