Repository Public
1// Replays the testnet proof of the withdrawal signature (scripts/hl-withdraw-testnet.mts --send --record, 2026-11-11): the real2// isolated signer signed `withdraw3` with a throwaway wallet key; the venue refused the unfunded account BY NAME, i.e. it3// recovered our address from the signature over its own hash of the action. No network here.4import { describe, expect, it } from 'vitest';5import { buildWithdraw3TypedDataOnly, recoverTypedDataSigner } from '../../src/venues/hyperliquid/index.ts';6import { raw } from './helpers.ts';78describe('testnet proof: withdraw3 signed by the wallet key', () => {9 const p = raw('testnet-withdraw-proof.json') as { network: 'testnet'; endpoint: string; account: string; request: { action: { type: string; hyperliquidChain: string; signatureChainId: string; destination: string; amount: string; time: number }; nonce: number; signature: { r: string; s: string; v: number }; vaultAddress: null; expiresAfter: null }; status: number; answer: { status: string; response: string } };10 it('was sent to the testnet endpoint only, as a withdrawal to the signing address itself', () => {11 expect(p.endpoint).toBe('https://api.hyperliquid-testnet.xyz/exchange');12 expect(p.request.action).toEqual({ type: 'withdraw3', hyperliquidChain: 'Testnet', signatureChainId: '0x66eee', destination: p.account, amount: p.request.action.amount, time: p.request.nonce });13 expect([p.request.vaultAddress, p.request.expiresAfter]).toEqual([null, null]);14 });15 it('our typed data recovers the account from the recorded signature, and the venue named that same account', async () => {16 const a = p.request.action;17 const td = buildWithdraw3TypedDataOnly({ network: 'testnet', destination: a.destination, amount: a.amount, time: a.time }).typedData;18 expect(await recoverTypedDataSigner(td, p.request.signature)).toBe(p.account);19 expect(p.status).toBe(200);20 expect(p.answer).toEqual({ status: 'err', response: `Must deposit before performing actions. User: ${p.account}` });21 // the same signature read as a Mainnet action is someone else's22 const main = buildWithdraw3TypedDataOnly({ network: 'mainnet', destination: a.destination, amount: a.amount, time: a.time }).typedData;23 expect(await recoverTypedDataSigner(main, p.request.signature)).not.toBe(p.account);24 });25});26