SPB Git

Repository Public

testnet-proof.test.ts 4.4 KB 59 lines typescript

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
Files main
Menu

Theme