Skip to content
markpaper

src/orders/build.test.ts

v0.2.0 · 8.1 KB

Download file
import { verifyTypedData } from 'viem';
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
import { describe, expect, it } from 'vitest';
import { decimalToX18, X18 } from '../numbers/x18.js';
import { EXPIRATION_NEVER, ReduceOnlyNotTakerError } from '../signing/appendix.js';
import { parseNonce } from '../signing/nonce.js';
import { productVerifyingContract, 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 {
  buildPlaceOrder,
  isIdempotentPlacement,
  orderTypeForMarket,
  placeOrderBody,
  sendPlaceOrder,
  signPlaceOrder,
} from './build.js';

const master = privateKeyToAccount(generatePrivateKey());
const signer = privateKeyToAccount(generatePrivateKey());
const sender = subaccountBytes32(master.address, 'default');
const BTC = { productId: 2, tickX18: X18, lotX18: 50000000000000n };
const CHAIN_ID = 57073;
const NOW = 1_780_000_000_000; // synthetic round clock

describe('orderTypeForMarket', () => {
  it('takers keep their type and reduce-only bit whatever the market says', () => {
    expect(orderTypeForMarket({ intent: 'ioc', reduceOnly: true, postOnlyMarket: true })).toEqual({
      orderType: 'ioc',
      reduceOnly: true,
    });
    expect(orderTypeForMarket({ intent: 'ioc', reduceOnly: false, postOnlyMarket: undefined })).toEqual({
      orderType: 'ioc',
      reduceOnly: false,
    });
    expect(orderTypeForMarket({ intent: 'fok', reduceOnly: true, postOnlyMarket: true })).toEqual({
      orderType: 'fok',
      reduceOnly: true,
    });
  });

  it('resting: POST_ONLY on a fresh post-only market, DEFAULT otherwise, never reduce-only', () => {
    expect(orderTypeForMarket({ intent: 'resting', reduceOnly: true, postOnlyMarket: true })).toEqual({
      orderType: 'post_only',
      reduceOnly: false,
    });
    expect(orderTypeForMarket({ intent: 'resting', reduceOnly: true, postOnlyMarket: false })).toEqual({
      orderType: 'default',
      reduceOnly: false,
    });
    expect(orderTypeForMarket({ intent: 'resting', reduceOnly: false, postOnlyMarket: undefined })).toEqual({
      orderType: 'default',
      reduceOnly: false,
    });
  });
});

describe('buildPlaceOrder', () => {
  const base = {
    product: BTC,
    chainId: CHAIN_ID,
    sender,
    isBuy: false,
    priceX18: 66120n * X18,
    sizeX18: decimalToX18('0.0005'),
  };

  it('produces the knowledge-base payload: signed amount, u64 max expiration, appendix 2561, product domain', () => {
    const p = buildPlaceOrder({
      ...base,
      orderType: 'ioc',
      reduceOnly: true,
      nonce: { nowMs: NOW, tag: 0x321, reduceIntent: true, random: 9 },
    });
    expect(p.productId).toBe(2);
    expect(p.order.amount).toBe(-decimalToX18('0.0005'));
    expect(p.order.expiration).toBe(EXPIRATION_NEVER);
    expect(p.order.appendix).toBe(2561n);
    expect(parseNonce(p.order.nonce)).toMatchObject({
      recvTimeMs: NOW + 60_000,
      tag: 0x321,
      reduceIntent: true,
      random: 9,
    });
    expect(p.typedData.domain.verifyingContract).toBe(productVerifyingContract(2));
    expect(p.typedData.domain.chainId).toBe(CHAIN_ID);
    expect(p.wireOrder).toEqual({
      sender,
      priceX18: '66120000000000000000000',
      amount: '-500000000000000',
      expiration: '18446744073709551615',
      nonce: p.order.nonce.toString(),
      appendix: '2561',
    });
    expect(p.expectedDigest).toMatch(/^0x[0-9a-f]{64}$/);
    const body = placeOrderBody(p, '0xsig' as Hex);
    expect(body).toEqual({ place_order: { product_id: 2, order: p.wireOrder, signature: '0xsig' } });
  });

  it('buy orders carry a positive amount; explicit nonce and expiration are honoured', () => {
    const p = buildPlaceOrder({ ...base, isBuy: true, orderType: 'default', nonce: 12345n, expiration: 99n });
    expect(p.order.amount).toBe(decimalToX18('0.0005'));
    expect(p.order.nonce).toBe(12345n);
    expect(p.order.expiration).toBe(99n);
    expect(p.order.appendix).toBe(1n);
    expect(p.reduceOnly).toBe(false);
  });

  it('refuses reduce-only on resting types, off-grid values, zero sizes and bad senders', () => {
    expect(() => buildPlaceOrder({ ...base, orderType: 'default', reduceOnly: true })).toThrow(ReduceOnlyNotTakerError);
    expect(() => buildPlaceOrder({ ...base, orderType: 'post_only', reduceOnly: true })).toThrow(/2067/);
    expect(() => buildPlaceOrder({ ...base, orderType: 'ioc', priceX18: decimalToX18('66120.5') })).toThrow(
      /not on the tick/,
    );
    expect(() => buildPlaceOrder({ ...base, orderType: 'ioc', sizeX18: decimalToX18('0.00012') })).toThrow(
      /not on the lot/,
    );
    expect(
      buildPlaceOrder({ ...base, orderType: 'ioc', sizeX18: decimalToX18('0.00012'), skipGridCheck: true }).order
        .amount,
    ).toBe(-decimalToX18('0.00012'));
    expect(() => buildPlaceOrder({ ...base, orderType: 'ioc', sizeX18: 0n })).toThrow(/SKIP/);
    expect(() => buildPlaceOrder({ ...base, orderType: 'ioc', priceX18: 0n })).toThrow(RangeError);
    expect(() => buildPlaceOrder({ ...base, orderType: 'ioc', sender: '0x12' as Hex })).toThrow(TypeError);
  });

  it('signPlaceOrder yields a signature that verifies under the product domain', async () => {
    const { prepared, body } = await signPlaceOrder(signer, {
      ...base,
      orderType: 'ioc',
      reduceOnly: true,
      nonce: { nowMs: NOW },
    });
    expect(body.place_order.product_id).toBe(2);
    expect(
      await verifyTypedData({ ...prepared.typedData, signature: body.place_order.signature, address: signer.address }),
    ).toBe(true);
    expect(
      await verifyTypedData({
        ...prepared.typedData,
        domain: { ...prepared.typedData.domain, verifyingContract: productVerifyingContract(18) },
        signature: body.place_order.signature,
        address: signer.address,
      }),
    ).toBe(false);
  });
});

describe('sendPlaceOrder', () => {
  const body = placeOrderBody(
    buildPlaceOrder({
      product: BTC,
      chainId: CHAIN_ID,
      sender,
      isBuy: true,
      priceX18: X18,
      sizeX18: BTC.lotX18,
      orderType: 'ioc',
      nonce: 1n,
    }),
    '0x00' as Hex,
  );
  const digest = `0x${'AB'.repeat(32)}`;

  it('returns the digest (lower-cased) and forwards idempotency / label', async () => {
    const seen: unknown[] = [];
    const execute: ExecuteRequester = async <T>(_b: unknown, opts?: unknown) => {
      seen.push(opts);
      return { digest } as T;
    };
    const out = await sendPlaceOrder(execute, body, { idempotent: true, label: 'order:close' });
    expect(out).toEqual({ digest: digest.toLowerCase() });
    expect(seen[0]).toMatchObject({ idempotent: true, label: 'order:close' });
    await sendPlaceOrder(execute, body);
    expect(seen[1]).toMatchObject({ idempotent: false, label: 'place_order' });
  });

  it('maps a failure envelope and a success without digest to rejections; transport errors propagate', async () => {
    const rejecting: ExecuteRequester = async () => {
      throw new NadoRejection({ code: 2117, error: 'post-only', path: 'execute' });
    };
    expect(await sendPlaceOrder(rejecting, body)).toEqual({ rejection: { code: 2117, message: 'post-only' } });
    const noDigest: ExecuteRequester = async <T>() => ({ digest: '0x12' }) as T;
    expect(await sendPlaceOrder(noDigest, body)).toMatchObject({ rejection: { code: -1 } });
    const failing: ExecuteRequester = async () => {
      throw new Error('timeout');
    };
    await expect(sendPlaceOrder(failing, body)).rejects.toThrow('timeout');
  });
});

describe('isIdempotentPlacement', () => {
  it('only a full reduce-only taker close', () => {
    expect(isIdempotentPlacement({ orderType: 'ioc', reduceOnly: true, fullClose: true })).toBe(true);
    expect(isIdempotentPlacement({ orderType: 'fok', reduceOnly: true, fullClose: true })).toBe(true);
    expect(isIdempotentPlacement({ orderType: 'ioc', reduceOnly: true, fullClose: false })).toBe(false);
    expect(isIdempotentPlacement({ orderType: 'ioc', reduceOnly: false, fullClose: true })).toBe(false);
    expect(isIdempotentPlacement({ orderType: 'default', reduceOnly: false, fullClose: true })).toBe(false);
  });
});
All files