Skip to content
markpaper

src/signer/runtime.test.ts

v0.1.0 · 13.7 KB

Download file
// Offline optional runtime coverage; no keys, accounts, or response data are sourced from a live user.
import { randomBytes } from 'node:crypto';
import { mkdtempSync, rmdirSync, unlinkSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { beforeAll, describe, expect, it } from 'vitest';
import { base58Decode, base58Encode } from '../address/base58.js';
import { delegate, exchangeKeys, globalBytes, headerBytes, market, owner } from '../testing/fixtures.js';

let S: Record<string, any>;
beforeAll(async () => {
  const runtimePath = '../../signer/' + 'sidecar.mjs';
  S = await import(runtimePath);
});
function env() {
  return {
    PHX_SIGNER_BOT_PUBKEY: delegate,
    PHX_SIGNER_AUTHORITY: owner,
    PHX_SIGNER_TOKEN: base58Encode(randomBytes(32)),
    PHX_SIGNER_RPC_URL: 'https://rpc.example.invalid',
    PHX_SIGNER_CLIENT_ORDER_NAMESPACE: 'example',
    PHX_SIGNER_PORT: '9000',
    PHX_SIGNER_PRIORITY_FEE_MIN: '0',
    PHX_SIGNER_PRIORITY_FEE_MAX: '500',
    PHX_SIGNER_IOC_SLOT_OFFSET_MAX: '10',
    PHX_SIGNER_INTENT_TTL_MS: '60000',
    PHX_SIGNER_INTENT_TOMBSTONE_MS: '120000',
    PHX_SIGNER_COLLATERAL_MAX_AGE_MS: '10000',
    PHX_SIGNER_COLLATERAL_REFRESH_MS: '1000',
    PHX_SIGNER_CU_ORDER: '100000',
    PHX_SIGNER_CU_CANCEL: '100000',
    PHX_SIGNER_CU_CANCEL_PER_ID: '0',
    PHX_SIGNER_PRIORITY_FEE_PERCENTILE: '50',
    PHX_SIGNER_IOC_SLOT_OFFSET: '5',
    PHX_SIGNER_HTTP_WAIT_MS: '100',
    PHX_SIGNER_MAX_INFLIGHT: '4',
    PHX_SIGNER_RPC_TIMEOUT_MS: '1000',
    PHX_SIGNER_CONFIRM_POLL_MS: '1',
    PHX_SIGNER_REBROADCAST_MS: '100',
    PHX_SIGNER_CONFIRM_MAX_MS: '200',
    PHX_SIGNER_EXPIRY_MARGIN_BLOCKS: '2',
    PHX_SIGNER_EXPIRY_RECHECK_MS: '0',
    PHX_SIGNER_RESOLVE_MS: '1000',
    PHX_SIGNER_META_REFRESH_MS: '100000',
    PHX_SIGNER_HEADER_SHARE_MS: '0',
    PHX_SIGNER_HEADER_HEARTBEAT_MS: '100000',
    PHX_SIGNER_COLLATERAL_LAZY_MAX_AGE_MS: '0',
    PHX_SIGNER_SOL_REFRESH_MS: '100000',
    PHX_SIGNER_SOL_MAX_AGE_MS: '200000',
    PHX_SIGNER_GLOBAL_CONFIG_TTL_MS: '0',
    PHX_SIGNER_MIN_SOL_LAMPORTS: '0',
  };
}
const req = () => ({
  op: 'order',
  intentId: 'example-intent-1',
  symbol: 'BTC',
  kind: 'ioc',
  side: 'bid',
  priceInTicks: 800n,
  numBaseLots: 3n,
  reduceOnly: false,
  lastValidSlotOffset: 5,
});
describe('optional signing runtime', () => {
  it('requires caller policy and defaults to readonly', () => {
    const e = env();
    const c = S.parseConfig(e);
    expect(c.readonly).toBe(true);
    expect(c.clientOrderNamespace).toBe('example');
    delete (e as Record<string, string>).PHX_SIGNER_PORT;
    expect(() => S.parseConfig(e)).toThrow('PHX_SIGNER_PORT');
  });
  it('requires a durable journal before accepting write mode', () => {
    expect(() => S.parseConfig({ ...env(), PHX_SIGNER_READONLY: 'false' })).toThrow();
    expect(
      S.parseConfig({ ...env(), PHX_SIGNER_READONLY: 'false', PHX_SIGNER_JOURNAL: 'intent-journal.jsonl' }).readonly,
    ).toBe(false);
  });
  it('refuses programmatic write mode without an open journal or matching signer', () => {
    const config = S.parseConfig({
      ...env(),
      PHX_SIGNER_READONLY: 'false',
      PHX_SIGNER_JOURNAL: 'intent-journal.jsonl',
    });
    const signer = { address: delegate, signTransactions: async () => [] };
    expect(() => S.createSidecar({ config, signer, rpc: {}, api: {} })).toThrow('open durable');
    const journal = { load: () => [], append: () => {}, rewrite: () => {}, close: () => {} };
    expect(() => S.createSidecar({ config, signer: { ...signer, address: owner }, journal, rpc: {}, api: {} })).toThrow(
      'matching',
    );
  });
  it('enforces independent owner/delegate and authenticated loopback binding', () => {
    expect(() => S.parseConfig({ ...env(), PHX_SIGNER_AUTHORITY: delegate })).toThrow();
    expect(() => S.parseConfig({ ...env(), PHX_SIGNER_BIND: '0.0.0.0' })).toThrow();
    expect(() => S.parseConfig({ ...env(), PHX_SIGNER_TOKEN: 'small' })).toThrow();
  });
  it('uses exact tick/lot strings and makes IoC partial-fill rather than FOK', () => {
    const c = S.parseConfig(env());
    const r = S.normalizeOrderRequest(
      {
        intentId: 'example-intent-1',
        symbol: 'BTC',
        kind: 'ioc',
        side: 'bid',
        reduceOnly: false,
        priceInTicks: '800',
        numBaseLots: '3',
      },
      c,
    );
    const p = S.buildOrderPacket(r, { lastValidSlot: 20n });
    expect(p.minBaseLotsToFill).toBe(0n);
    expect(p.minQuoteLotsToFill).toBe(0n);
    expect(p.cancelExisting).toBe(false);
    expect(p.lastValidSlot).toBe(20n);
    expect(p.priceInTicks).toBe(800n);
  });
  it('rejects numeric u64 values before instruction construction', () => {
    const c = S.parseConfig(env());
    expect(() => S.normalizeOrderRequest({ ...req(), priceInTicks: 800, numBaseLots: '3' }, c)).toThrow();
    expect(() => S.parseU64(900, 'ticks')).toThrow();
  });
  it('makes the public on-chain client id namespace explicit', () => {
    expect(() => S.clientOrderIdFor('example-intent-1')).toThrow();
    expect(S.clientOrderIdFor('example-intent-1', 'one')).not.toBe(S.clientOrderIdFor('example-intent-1', 'two'));
  });
  it('preserves PostOnly slide=false and reduce-only flags', () => {
    const p = S.buildOrderPacket({ ...req(), kind: 'postOnly', reduceOnly: true });
    expect(p.slide).toBe(false);
    expect(p.cancelExisting).toBe(false);
    expect(p.orderFlags).toBe(128);
  });
  it('builds protocol instructions with explicit mainnet addresses', () => {
    const ix = S.buildPhoenixInstruction(req(), {
      keys: exchangeKeys,
      market: market(),
      signer: delegate,
      traderAccount: owner,
      lastValidSlot: 20n,
      clientOrderNamespace: 'example',
    });
    expect(String(ix.programAddress)).toBe(S.PROGRAM_ADDRESS);
    expect(ix.data.length).toBeGreaterThan(0);
  });
  it('checks cancel count and transaction wire size', () => {
    const c = S.parseConfig(env());
    const ids = Array.from({ length: 31 }, (_, i) => ({ priceInTicks: String(i + 1), seq: String(i + 1) }));
    expect(() => S.normalizeCancelRequest({ intentId: 'example-cancel-1', symbol: 'BTC', ids }, c)).toThrow();
  });
  it('decodes return data from the matching engine side', () => {
    const data = new Uint8Array(64);
    const d = new DataView(data.buffer);
    d.setBigUint64(16, 600n, true);
    d.setBigUint64(40, 2n, true);
    const r = S.decodeCpiResponse(data, 'bid');
    expect(r.filledBaseLots).toBe('2');
    expect(r.filledQuoteLots).toBe('600');
  });
  it('fails closed on inconsistent return data', () => {
    const data = new Uint8Array(64);
    new DataView(data.buffer).setBigUint64(24, 1n, true);
    expect(() => S.decodeCpiResponse(data, 'bid')).toThrow();
    expect(() => S.decodeCpiResponse(new Uint8Array(20), 'bid')).toThrow();
    const missing = new Uint8Array(64);
    new DataView(missing.buffer).setBigUint64(16, 600n, true);
    expect(() => S.decodeCpiResponse(missing, 'bid')).toThrow();
  });
  it('classifies paid no-op cancels and explicit rejection logs', () => {
    const p = S.parsePhoenixLogs(['Program log: cancel requested for cold trader; operation is a no-op']);
    expect(p).toHaveProperty('coldNoop', true);
    const r = S.parsePhoenixLogs(['Program log: PostOnly order would cross the book']);
    expect(JSON.stringify(r)).toContain('PostOnly');
  });
  it('retains durable journal records without embedding identities in fixtures', () => {
    const dir = mkdtempSync(join(tmpdir(), 'phx-kit-journal-'));
    try {
      const j = S.openJournal(join(dir, 'journal.jsonl'));
      j.append({ v: 1, t: 'used', intentId: 'example-intent-1', ts: 1 });
      expect(j.load()).toHaveLength(1);
      j.close();
      const read = S.openJournal(join(dir, 'journal.jsonl'));
      expect(read.load()[0].intentId).toBe('example-intent-1');
      read.close();
    } finally {
      unlinkSync(join(dir, 'journal.jsonl'));
      rmdirSync(dir);
    }
  });
  it('refuses corrupt or unreadable journals rather than forgetting spent intents', () => {
    const dir = mkdtempSync(join(tmpdir(), 'phx-kit-journal-'));
    try {
      const file = join(dir, 'journal.jsonl');
      const j = S.openJournal(file);
      writeFileSync(file, '{broken}\n');
      expect(() => j.load()).toThrow('corrupt');
      j.close();
      const broken = S.openJournal(file, {
        fsImpl: {
          mkdirSync: () => {},
          openSync: () => 1,
          readFileSync: () => {
            throw new Error('unreadable');
          },
          closeSync: () => {},
        },
      });
      expect(() => broken.load()).toThrow('unreadable');
      broken.close();
    } finally {
      unlinkSync(join(dir, 'journal.jsonl'));
      rmdirSync(dir);
    }
  });
  it('refuses recovery from structurally invalid journal records', async () => {
    const journal = { load: () => [{}], append: () => {}, rewrite: () => {}, close: () => {} };
    const sc = S.createSidecar({ config: S.parseConfig(env()), journal, rpc: {}, api: {}, log: () => {} });
    await expect(sc.start()).rejects.toThrow('invalid record');
    await sc.stop({ graceMs: 0 });
  });
  it('reads synthesized on-chain metadata without writes in readonly mode', async () => {
    const c = S.parseConfig(env());
    let sends = 0;
    const rpc = {
      getAccountInfo: async (a: string) => ({
        context: { slot: 100 },
        value: {
          owner: S.PROGRAM_ADDRESS,
          lamports: 1,
          data: [
            Buffer.from(a === S.GLOBAL_CONFIGURATION ? globalBytes() : headerBytes()).toString('base64'),
            'base64',
          ],
        },
      }),
      getBalance: async () => ({ context: { slot: 100 }, value: 50000 }),
      sendTransaction: async () => {
        sends++;
        throw new Error('No writes allowed');
      },
    };
    const sc = S.createSidecar({
      config: c,
      rpc,
      api: { getExchange: async () => ({ keys: exchangeKeys, markets: [market()] }) },
      log: () => {},
    });
    try {
      await sc.start();
      const h = await sc.health();
      expect(h.readonly).toBe(true);
      expect(h.bindingOk).toBe(true);
      await expect(
        sc.submit('order', {
          intentId: 'example-intent-2',
          symbol: 'BTC',
          kind: 'limit',
          side: 'bid',
          priceInTicks: '800',
          numBaseLots: '1',
          reduceOnly: false,
        }),
      ).rejects.toMatchObject({ status: 403 });
      expect(sends).toBe(0);
    } finally {
      await sc.stop({ graceMs: 0 });
    }
  });
  it('signs once per intent, journals before send, and refuses a changed fingerprint', async () => {
    const kitPath = '@solana/' + 'kit';
    const solana = await import(kitPath);
    const kp = await solana.generateKeyPairSigner();
    let signs = 0;
    const signer = {
      ...kp,
      signTransactions: async (txs: unknown, config: unknown) => {
        signs++;
        return kp.signTransactions(txs, config);
      },
    };
    const cfg = S.parseConfig({
      ...env(),
      PHX_SIGNER_BOT_PUBKEY: String(kp.address),
      PHX_SIGNER_READONLY: 'false',
      PHX_SIGNER_JOURNAL: 'test-journal',
      PHX_SIGNER_HTTP_WAIT_MS: '1000',
    });
    const hdr = headerBytes();
    hdr.set(base58Decode(String(kp.address))!, 120);
    const events: string[] = [];
    const wires: string[] = [];
    const recs: unknown[] = [];
    const journal = {
      load: () => [],
      append: (r: unknown) => {
        events.push('journal');
        recs.push(r);
      },
      rewrite: () => {},
      close: () => {},
    };
    const cpi = new Uint8Array(64);
    const dv = new DataView(cpi.buffer);
    dv.setBigUint64(16, 500n, true);
    dv.setBigUint64(40, 1n, true);
    const rpc = {
      getAccountInfo: async (a: string) => ({
        context: { slot: 100 },
        value: {
          owner: S.PROGRAM_ADDRESS,
          lamports: 1,
          data: [Buffer.from(a === S.GLOBAL_CONFIGURATION ? globalBytes() : hdr).toString('base64'), 'base64'],
        },
      }),
      getBalance: async () => ({ context: { slot: 100 }, value: 50000 }),
      getSlot: async () => 100,
      getLatestBlockhash: async () => ({ blockhash: base58Encode(randomBytes(32)), lastValidBlockHeight: 200 }),
      getRecentPrioritizationFees: async () => [],
      getBlockHeight: async () => 100,
      sendTransaction: async (wire: string) => {
        events.push('send');
        wires.push(wire);
        return base58Encode(Buffer.from(wire, 'base64').subarray(1, 65));
      },
      getSignatureStatus: async () => ({ slot: 101, confirmationStatus: 'confirmed', err: null }),
      getTransaction: async () => ({
        slot: 101,
        meta: {
          err: null,
          logMessages: [],
          innerInstructions: [],
          returnData: { programId: S.PROGRAM_ADDRESS, data: [Buffer.from(cpi).toString('base64'), 'base64'] },
        },
      }),
    };
    const sc = S.createSidecar({
      config: cfg,
      signer,
      rpc,
      journal,
      api: { getExchange: async () => ({ keys: exchangeKeys, markets: [market()] }) },
      log: () => {},
    });
    try {
      await sc.start();
      const body = {
        intentId: 'example-unique-intent',
        symbol: 'BTC',
        kind: 'ioc',
        side: 'bid',
        priceInTicks: '800',
        numBaseLots: '1',
        reduceOnly: false,
      };
      const a = await sc.submit('order', body);
      const b = await sc.submit('order', body);
      expect(a.signature).toBe(b.signature);
      expect(a.status).toBe('confirmed');
      expect(signs).toBe(1);
      expect(wires).toHaveLength(1);
      expect(events.indexOf('journal')).toBeLessThan(events.indexOf('send'));
      await expect(sc.submit('order', { ...body, numBaseLots: '2' })).rejects.toMatchObject({ status: 409 });
      expect(recs.length).toBeGreaterThan(0);
    } finally {
      await sc.stop({ graceMs: 0 });
    }
  });
});
All files