src/orders/cancel.test.ts
v0.2.0 · 4.8 KB
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');
});
});