Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

TypeScript

wrappers/typescript/mostroCli.ts — Node 18+, no dependencies. It contains complete TypeScript types for every data object of contract 1, usable on their own (for example to type the output of a different runner). Tested with Node 22 (node --experimental-strip-types) against mostro-cli 0.16.3.

import { MostroCli, TransportError } from "./mostroCli.ts";

const cli = new MostroCli({ env: { MOSTRO_PUBKEY: "<node hex>", RELAYS: "wss://relay.example.com" } });

const book = await cli.orderList({ side: "sell", currency: "EUR" });
if (book.status === "ok" && book.data) {
  for (const o of book.data.orders) {
    const sats = BigInt(o.sats_amount);          // decimal strings → BigInt, never Number
    console.log(o.id, sats === 0n ? "market" : sats.toString(), o.payment_method);
  }
}

const inbox = await cli.messageList({ since: 60 });
for (const m of inbox.data?.messages ?? []) {
  if (m.payload.kind === "payment_request") {
    console.log(`${m.next_step}: pay ${m.payload.invoice}`);   // narrowed by kind
  }
}

const orderId = "00000000-0000-0000-0000-000000000001";
try {
  const step = await cli.release(orderId, /* confirmed by the user */ true);
  if (step.status === "uncertain") {
    const info = await cli.orderInfo(orderId);   // check before doing anything else
  }
} catch (e) {
  if (e instanceof TransportError && e.mayHaveActed) { /* treat as uncertain */ }
}

Source

// Minimal TypeScript wrapper for the mostro-cli JSON interface (contract "1").
// Node 18+, no dependencies. Decimal strings stay strings; convert with BigInt.
//
//   import { MostroCli } from "./mostroCli.ts";
//   const cli = new MostroCli({ env: { MOSTRO_PUBKEY: "...", RELAYS: "wss://..." } });
//   const out = await cli.run(["order", "list", "--side", "sell"]);
//   if (out.status === "ok") console.log(out.data.orders.length);

import { execFile } from "node:child_process";

export type Status = "ok" | "error" | "uncertain";
export type Publication = "not_attempted" | "unknown" | "published" | "not_applicable";
export type DaemonAcceptance = "unknown" | "accepted" | "rejected" | "not_applicable";
export type Persistence = "not_attempted" | "applied" | "failed" | "not_applicable";

export type ErrorCode =
  | "invalid_arguments" | "output_not_supported"
  | "configuration_missing" | "configuration_invalid"
  | "relay_connection_failed" | "relay_read_failed" | "read_timeout" | "read_limit_exceeded"
  | "internal_error" | "identity_missing" | "identity_unreadable"
  | "protocol_unsupported" | "pow_required" | "request_rejected" | "reply_timeout"
  | "node_refused" | "reply_malformed" | "order_declined" | "order_not_saved"
  | "store_unwritable" | "order_unknown" | "store_unreadable";

export type WarningCode =
  | "invalid_event_timestamp" | "cleanup_incomplete" | "orders_not_returned"
  | "invalid_order_timestamp" | "local_status_stale" | "trade_keys_limited"
  | "trade_index_not_recorded";

/** Base-10 signed 64-bit integer as a string. Use BigInt(value) for arithmetic. */
export type Decimal = string;
export type Uuid = string;
export type Hex64 = string;
export type Rfc3339 = string;

export interface ErrorInfo { code: ErrorCode; message: string; reason?: string }
export interface Warning { code: WarningCode; message: string }

export interface Snapshot {
  since: Rfc3339; until: null; observed_at: Rfc3339;
  per_relay_limit: 50; max_unique_events: 10000;
  configured_relays: number; completed_relays: number; received_events: number;
  completion_policy: "all_relays_eose"; complete_history: false;
}

export interface Meta {
  publication: Publication; daemon_acceptance: DaemonAcceptance;
  persistence: Persistence; snapshot: Snapshot | null;
}

export interface Envelope<D = unknown> {
  schema_version: "1";
  command: string | null;
  status: Status;
  data: D | null;
  error: ErrorInfo | null;
  warnings: Warning[];
  meta: Meta;
}

export type Side = "buy" | "sell";
export type OrderStatus =
  | "active" | "canceled" | "canceled-by-admin" | "settled-by-admin" | "completed-by-admin"
  | "dispute" | "expired" | "fiat-sent" | "settled-hold-invoice" | "pending" | "success"
  | "waiting-buyer-invoice" | "waiting-payment" | "waiting-taker-bond" | "waiting-maker-bond"
  | "cooperatively-canceled" | "in-progress";

export interface PublicOrder {
  id: Uuid; side: Side; status: OrderStatus; sats_amount: Decimal; fiat_currency: string;
  fiat_amount: Decimal; fiat_amount_min: Decimal | null; fiat_amount_max: Decimal | null;
  payment_method: string; premium_percent: Decimal; created_at: Rfc3339 | null; created_at_unix: Decimal;
}

export interface OrderRecord {
  id: Uuid; side: Side | null; status: OrderStatus | null; sats_amount: Decimal; fiat_currency: string;
  fiat_amount: Decimal; fiat_amount_min: Decimal | null; fiat_amount_max: Decimal | null;
  payment_method: string; premium_percent: Decimal;
  buyer_trade_pubkey: Hex64 | null; seller_trade_pubkey: Hex64 | null;
  created_at: Rfc3339 | null; created_at_unix: Decimal | null;
  expires_at: Rfc3339 | null; expires_at_unix: Decimal | null;
}

export interface ListData {
  node_pubkey: Hex64;
  filters: { side: Side | null; currency: string | null; status: OrderStatus };
  orders: PublicOrder[];
}
export interface OrderInfoData { node_pubkey: Hex64; orders: OrderRecord[]; not_returned: Uuid[] }
export interface OrderNewData {
  node_pubkey: Hex64; order: OrderRecord; next_step: "wait_for_taker" | "pay_bond_invoice";
  payment_request: string | null; trade_index: Decimal; trade_pubkey: Hex64;
}
export interface OrderTakeData {
  node_pubkey: Hex64; order_id: Uuid; side: Side;
  next_step: "add_invoice" | "wait_for_seller_payment" | "pay_hold_invoice" | "pay_bond_invoice";
  order: OrderRecord | null; payment_request: string | null; trade_index: Decimal; trade_pubkey: Hex64;
}
export interface OrderInvoiceData {
  node_pubkey: Hex64; order_id: Uuid;
  result: "waiting_seller_payment" | "payment_accepted" | "invoice_updated";
  order: OrderRecord | null; trade_pubkey: Hex64;
}
export interface BondInvoiceData {
  node_pubkey: Hex64; order_id: Uuid; result: "invoice_accepted";
  counterparty_share_sats: Decimal | null; trade_pubkey: Hex64;
}
export interface StepData {
  node_pubkey: Hex64; order_id: Uuid;
  result: "fiat_sent" | "released" | "canceled" | "cancel_requested" | "cancel_accepted";
  next_step: "wait_for_release" | "wait_for_peer_cancel" | "sync_child_order" | null;
  trade_pubkey: Hex64; next_trade_index: Decimal | null;
}
export interface SyncData {
  node_pubkey: Hex64; since_minutes: Decimal;
  adopted: { order: OrderRecord; trade_index: Decimal; trade_pubkey: Hex64 }[];
  messages_read: Decimal; messages_ignored: Decimal; truncated: boolean;
  keys_read: Decimal; keys_not_read: Decimal;
}
export type MessagePayload =
  | { kind: "none" }
  | { kind: "order"; order: OrderRecord }
  | { kind: "payment_request"; order: OrderRecord | null; invoice: string; amount: Decimal | null }
  | { kind: "bond_payout_request"; order: OrderRecord; slashed_at: Rfc3339 | null; slashed_at_unix: Decimal }
  | { kind: "cant_do"; reason: string | null }
  | { kind: "peer"; counterparty_pubkey: string }
  | { kind: "text"; text: string }
  | { kind: "amount"; amount: Decimal }
  | { kind: "other"; variant: string };
export interface MessageRecord {
  event_id: Hex64; created_at: Rfc3339 | null; created_at_unix: Decimal;
  order_id: Uuid | null; request_id: Decimal | null; action: string;
  next_step: "pay_bond_invoice" | "add_invoice" | "pay_hold_invoice" | "add_bond_invoice" | null;
  trade_index: Decimal | null; trade_pubkey: Hex64; payload: MessagePayload;
}
export interface MessageListData {
  node_pubkey: Hex64; since_minutes: Decimal; messages: MessageRecord[];
  truncated: boolean; keys_read: Decimal; keys_not_read: Decimal;
}

/** Thrown when there is no usable envelope. `mayHaveActed` => treat like "uncertain". */
export class TransportError extends Error {
  mayHaveActed: boolean;
  stderr: string;
  constructor(message: string, mayHaveActed: boolean, stderr = "") {
    super(message);
    this.mayHaveActed = mayHaveActed;
    this.stderr = stderr;
  }
}

export interface Options { binary?: string; env?: Record<string, string>; timeoutMs?: number }

export class MostroCli {
  private binary: string;
  private env: NodeJS.ProcessEnv;
  private timeoutMs: number;

  constructor(options: Options = {}) {
    this.binary = options.binary ?? "mostro-cli";
    this.env = { ...process.env, ...(options.env ?? {}) };
    this.timeoutMs = options.timeoutMs ?? 45_000;
  }

  /** Runs `mostro-cli --output json ...args`. `write` marks commands that publish a change. */
  run<D = unknown>(args: string[], write = false): Promise<Envelope<D> & { exitCode: number }> {
    const argv = ["--output", "json", ...args];
    return new Promise((resolve, reject) => {
      const child = execFile(this.binary, argv, { env: this.env, timeout: this.timeoutMs, maxBuffer: 16 << 20 },
        (error, stdout, stderr) => {
          const exitCode = typeof child.exitCode === "number" ? child.exitCode : -1;
          if (error && (error as NodeJS.ErrnoException).code === "ENOENT") {
            return reject(new TransportError(`cannot start ${this.binary}`, false));
          }
          if (error && error.killed) return reject(new TransportError("timed out", write, stderr));
          const text = stdout.trim();
          if (!text) return reject(new TransportError("empty stdout", write && exitCode === 1, stderr));
          let envelope: Envelope<D>;
          try {
            envelope = JSON.parse(text);
          } catch {
            return reject(new TransportError("stdout is not one JSON document", write, stderr));
          }
          if (envelope.schema_version !== "1") {
            return reject(new Error(`unsupported schema_version ${String(envelope.schema_version)}`));
          }
          resolve({ ...envelope, exitCode });
        });
      child.stdin?.end(); // the CLI never reads stdin in JSON mode
    });
  }

  orderList(f: { side?: Side; currency?: string; status?: OrderStatus } = {}) {
    return this.run<ListData>(["order", "list", ...opt("--side", f.side), ...opt("--currency", f.currency),
      ...opt("--status", f.status)]);
  }
  orderInfo(...ids: Uuid[]) {
    return this.run<OrderInfoData>(["order", "info", ...ids.flatMap((id) => opt("--order", id))]);
  }
  messageList(f: { since?: number; order?: Uuid } = {}) {
    return this.run<MessageListData>(["message", "list", ...opt("--since", f.since), ...opt("--order", f.order)]);
  }
  orderSync(since?: number) {
    return this.run<SyncData>(["order", "sync", ...opt("--since", since)]);
  }
  /** Every write needs the user's consent; it is passed as --yes. */
  write<D>(args: string[], confirmed: boolean) {
    if (!confirmed) throw new Error("write commands need the user's confirmation (--yes)");
    return this.run<D>([...args, "--yes"], true);
  }
  fiatSent(order: Uuid, confirmed: boolean) { return this.write<StepData>(["order", "fiat-sent", ...opt("--order", order)], confirmed); }
  release(order: Uuid, confirmed: boolean) { return this.write<StepData>(["order", "release", ...opt("--order", order)], confirmed); }
  cancel(order: Uuid, confirmed: boolean) { return this.write<StepData>(["order", "cancel", ...opt("--order", order)], confirmed); }
}

// Attached form, so a value starting with "-" is never parsed as an option.
function opt(flag: string, value: string | number | undefined): string[] {
  return value === undefined ? [] : [`${flag}=${value}`];
}