Skip to content
markpaper

src/orders/cancel.test.ts

v0.2.0 · 4.8 KB

Download file
import { verifyTypedData } from 'viem';
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
import { describe, expect, it } from 'vitest';
import { parseNonce } from '../signing/nonce.js';
import { subaccountBytes32 } from '../signing/subaccount.js';
import type { Hex } from '../signing/types.js';
import { NadoRejection } from '../transport/errors.js';
import type { ExecuteRequester } from '../transport/types.js';
import { buildCancelOrders, cancelOrdersBody, confirmCancelled, sendCancelOrders, signCancelOrders } from './cancel.js';

const master = privateKeyToAccount(generatePrivateKey());
const signer = privateKeyToAccount(generatePrivateKey());
const sender = subaccountBytes32(master.address);
const endpointAddr = privateKeyToAccount(generatePrivateKey()).address;
const dA = `0x${'1a'.repeat(32)}` as Hex;
const dB = `0x${'2b'.repeat(32)}` as Hex;
const NOW = 1_780_000_000_000; // synthetic round clock

describe('buildCancelOrders / cancelOrdersBody', () => {
  it('builds the tx with a recv_time nonce, dedups digests and stringifies the nonce on the wire', () => {
    const p = buildCancelOrders({
      chainId: 57073,
      endpointAddr,
      sender,
      targets: [
        { productId: 2, digest: dA },
        { productId: 18, digest: dB.toUpperCase().replace('0X', '0x') as Hex },
        { productId: 2, digest: dA },
      ],
      nonce: { nowMs: NOW, random: 7 },
    });
    expect(p.tx.productIds).toEqual([2, 18]);
    expect(p.digests).toEqual([dA, dB]);
    expect(parseNonce(p.tx.nonce).recvTimeMs).toBe(NOW + 60_000);
    expect(p.tx.nonce & 0xfffffn).toBe(7n);
    expect(p.typedData.domain.verifyingContract).toBe(endpointAddr.toLowerCase());
    const body = cancelOrdersBody(p, '0xsig' as Hex);
    expect(body).toEqual({
      cancel_orders: {
        tx: { sender, productIds: [2, 18], digests: [dA, dB], nonce: p.tx.nonce.toString() },
        signature: '0xsig',
      },
    });
  });

  it('refuses malformed digests and empty targets; explicit nonce is honoured', () => {
    expect(() =>
      buildCancelOrders({ chainId: 57073, endpointAddr, sender, targets: [{ productId: 2, digest: '0x12' as Hex }] }),
    ).toThrow(TypeError);
    expect(() => buildCancelOrders({ chainId: 57073, endpointAddr, sender, targets: [] })).toThrow(/nothing to cancel/);
    expect(
      buildCancelOrders({ chainId: 57073, endpointAddr, sender, targets: [{ productId: 2, digest: dA }], nonce: 42n })
        .tx.nonce,
    ).toBe(42n);
  });

  it('signCancelOrders verifies against the endpoint domain', async () => {
    const { prepared, body } = await signCancelOrders(signer, {
      chainId: 57073,
      endpointAddr,
      sender,
      targets: [{ productId: 2, digest: dA }],
    });
    expect(
      await verifyTypedData({
        ...prepared.typedData,
        signature: body.cancel_orders.signature,
        address: signer.address,
      }),
    ).toBe(true);
  });
});

describe('confirmCancelled', () => {
  it('confirms only digests listed in cancelled_orders', () => {
    expect(
      confirmCancelled({ cancelled_orders: [{ digest: dA.toUpperCase().replace('0X', '0x') }] }, [dA, dB]),
    ).toEqual({ ok: false, confirmed: [dA], unconfirmed: [dB] });
    expect(confirmCancelled({ cancelled_orders: [{ digest: dA }, { digest: dB }] }, [dA, dB])).toEqual({
      ok: true,
      confirmed: [dA, dB],
      unconfirmed: [],
    });
    expect(confirmCancelled({}, [dA])).toEqual({ ok: false, confirmed: [], unconfirmed: [dA] });
    expect(confirmCancelled(null, [dA]).ok).toBe(false);
    expect(confirmCancelled({ cancelled_orders: [] }, []).ok).toBe(false);
  });
});

describe('sendCancelOrders', () => {
  const body = cancelOrdersBody(
    buildCancelOrders({ chainId: 57073, endpointAddr, sender, targets: [{ productId: 2, digest: dA }], nonce: 1n }),
    '0x00' as Hex,
  );

  it('sends idempotently and confirms', async () => {
    const seen: unknown[] = [];
    const execute: ExecuteRequester = async <T>(_b: unknown, opts?: unknown) => {
      seen.push(opts);
      return { cancelled_orders: [{ digest: dA }] } as T;
    };
    expect(await sendCancelOrders(execute, body)).toEqual({ ok: true, confirmed: [dA], unconfirmed: [] });
    expect(seen[0]).toMatchObject({ idempotent: true, label: 'cancel_orders' });
  });

  it('2020 OrderNotFound confirms nothing; transport errors propagate', async () => {
    const notFound: ExecuteRequester = async () => {
      throw new NadoRejection({ code: 2020, error: 'OrderNotFound', path: 'execute' });
    };
    expect(await sendCancelOrders(notFound, body)).toEqual({
      ok: false,
      confirmed: [],
      unconfirmed: [dA],
      rejection: { code: 2020, message: 'OrderNotFound' },
    });
    const failing: ExecuteRequester = async () => {
      throw new Error('503');
    };
    await expect(sendCancelOrders(failing, body)).rejects.toThrow('503');
  });
});
All files