Skip to content
markpaper

src/signing/types.ts

v0.2.0 · 4.6 KB

Download file
// EIP-712 domain and typed-data definitions of the Nado gateway.
//
// Domain: `{ name: 'Nado', version: '0.0.1', chainId, verifyingContract }`. For `place_order` the
// verifying contract is `address(productId)` (the product id written as a 20-byte address); for every
// other execute (`cancel_orders`, `link_signer`, ...) it is `endpoint_addr` from the `contracts` query.
// Domains are separated: a signature made under product 2 does not verify under product 18 (checked with
// viem `verifyTypedData`; orders signed this way executed on mainnet, 2026-07-24).

/** `0x`-prefixed hex string. */
export type Hex = `0x${string}`;
/** 20-byte address as lower-case hex. */
export type Address = `0x${string}`;

export const NADO_DOMAIN_NAME = 'Nado';
export const NADO_DOMAIN_VERSION = '0.0.1';

/** Typed-data domain of every Nado execute. */
export interface NadoDomain {
  readonly name: typeof NADO_DOMAIN_NAME;
  readonly version: typeof NADO_DOMAIN_VERSION;
  readonly chainId: number;
  readonly verifyingContract: Address;
}

/** Builds the domain for a given chain id and verifying contract. */
export function nadoDomain(chainId: number, verifyingContract: Address): NadoDomain {
  if (!Number.isSafeInteger(chainId) || chainId <= 0)
    throw new RangeError(`chainId must be a positive integer, got ${String(chainId)}`);
  return { name: NADO_DOMAIN_NAME, version: NADO_DOMAIN_VERSION, chainId, verifyingContract };
}

/** `Order` struct of `place_order`. `amount` is signed: positive = buy, negative = sell. */
export const ORDER_TYPES = {
  Order: [
    { name: 'sender', type: 'bytes32' },
    { name: 'priceX18', type: 'int128' },
    { name: 'amount', type: 'int128' },
    { name: 'expiration', type: 'uint64' },
    { name: 'nonce', type: 'uint64' },
    { name: 'appendix', type: 'uint128' },
  ],
} as const;

/** `Cancellation` struct of `cancel_orders` (by digests). */
export const CANCELLATION_TYPES = {
  Cancellation: [
    { name: 'sender', type: 'bytes32' },
    { name: 'productIds', type: 'uint32[]' },
    { name: 'digests', type: 'bytes32[]' },
    { name: 'nonce', type: 'uint64' },
  ],
} as const;

/**
 * `CancellationProducts` struct (cancel every order on the listed products with one signature).
 *
 * @experimental Present in the signing schema, not verified live; the execute body shape and the
 * confirmation response were not observed.
 */
export const CANCELLATION_PRODUCTS_TYPES = {
  CancellationProducts: [
    { name: 'sender', type: 'bytes32' },
    { name: 'productIds', type: 'uint32[]' },
    { name: 'nonce', type: 'uint64' },
  ],
} as const;

/** `LinkSigner` struct of `link_signer` (signed by the MASTER wallet with the `tx_nonce` from `nonces`). */
export const LINK_SIGNER_TYPES = {
  LinkSigner: [
    { name: 'sender', type: 'bytes32' },
    { name: 'signer', type: 'bytes32' },
    { name: 'nonce', type: 'uint64' },
  ],
} as const;

/** Order message with bigint fields (what gets signed). */
export interface OrderMessage {
  readonly sender: Hex;
  readonly priceX18: bigint;
  readonly amount: bigint;
  readonly expiration: bigint;
  readonly nonce: bigint;
  readonly appendix: bigint;
}

/** Cancellation message with bigint nonce. */
export interface CancellationMessage {
  readonly sender: Hex;
  readonly productIds: readonly number[];
  readonly digests: readonly Hex[];
  readonly nonce: bigint;
}

/** CancellationProducts message. @experimental */
export interface CancellationProductsMessage {
  readonly sender: Hex;
  readonly productIds: readonly number[];
  readonly nonce: bigint;
}

/** LinkSigner message: `signer` is bytes32 (address + 12 zero bytes), `nonce` is the `tx_nonce`. */
export interface LinkSignerMessage {
  readonly sender: Hex;
  readonly signer: Hex;
  readonly nonce: bigint;
}

/** Order message as it goes into the JSON body: every bigint as a decimal string. */
export interface WireOrder {
  readonly sender: Hex;
  readonly priceX18: string;
  readonly amount: string;
  readonly expiration: string;
  readonly nonce: string;
  readonly appendix: string;
}

/**
 * Minimal signer contract: a viem `LocalAccount` (`privateKeyToAccount(...)`) satisfies it directly. For a
 * `WalletClient` pass `(p) => client.signTypedData({ ...p, account })`. The kit never holds keys.
 */
export interface TypedDataSigner {
  signTypedData(parameters: {
    domain: NadoDomain;
    types: Record<string, readonly { name: string; type: string }[]>;
    primaryType: string;
    message: Record<string, unknown>;
  }): Promise<Hex>;
}

/** Either a {@link TypedDataSigner} or a bare `signTypedData` function. */
export type SignerLike = TypedDataSigner | TypedDataSigner['signTypedData'];
All files