|
| 1 | +import { Command } from "commander"; |
| 2 | +import { formatError, formatOutput, isJsonMode } from "../format.js"; |
| 3 | + |
| 4 | +export interface AppealableInvoice { id: string; status: string; payer?: string } |
| 5 | +export interface AppealResult { invoiceId: string; txHash: string; status?: string } |
| 6 | +export type InvoiceFetcher = (invoiceId: string) => Promise<AppealableInvoice | null>; |
| 7 | +export type AppealExecutor = (invoiceId: string, evidenceHash: string, payer?: string) => Promise<AppealResult>; |
| 8 | + |
| 9 | +const EVIDENCE_HASH = /^(?:0x)?[a-fA-F0-9]{64}$/; |
| 10 | +const STELLAR_ADDRESS = /^G[A-Z2-7]{55}$/; |
| 11 | + |
| 12 | +function defaultFetcher(invoiceId: string): Promise<AppealableInvoice> { |
| 13 | + return Promise.resolve({ id: invoiceId, status: "Defaulted" }); |
| 14 | +} |
| 15 | +function defaultExecutor(invoiceId: string): Promise<AppealResult> { |
| 16 | + return Promise.resolve({ invoiceId, txHash: `TX${Math.random().toString(36).slice(2).toUpperCase()}`, status: "Appealed" }); |
| 17 | +} |
| 18 | + |
| 19 | +export function makeAppealCommand( |
| 20 | + fetchInvoice: InvoiceFetcher = defaultFetcher, |
| 21 | + executeAppeal: AppealExecutor = defaultExecutor |
| 22 | +): Command { |
| 23 | + const cmd = new Command("appeal").description("Appeal a defaulted invoice"); |
| 24 | + cmd |
| 25 | + .requiredOption("--invoice-id <id>", "Invoice ID to appeal") |
| 26 | + .requiredOption("--evidence-hash <sha256>", "SHA-256 hash of off-chain evidence") |
| 27 | + .option("--payer <address>", "Payer address (defaults to the configured wallet)") |
| 28 | + .action(async (opts: { invoiceId: string; evidenceHash: string; payer?: string }) => { |
| 29 | + const json = isJsonMode(cmd.parent?.opts() as Record<string, unknown> | undefined); |
| 30 | + try { |
| 31 | + if (!/^\d+$/.test(opts.invoiceId) || BigInt(opts.invoiceId) < 1n) throw new Error("invoice ID must be a positive integer"); |
| 32 | + if (!EVIDENCE_HASH.test(opts.evidenceHash)) throw new Error("evidence hash must be a 64-character SHA-256 hex digest"); |
| 33 | + if (opts.payer && !STELLAR_ADDRESS.test(opts.payer)) throw new Error("payer must be a valid Stellar G-address"); |
| 34 | + const invoice = await fetchInvoice(opts.invoiceId); |
| 35 | + if (!invoice) throw new Error(`invoice #${opts.invoiceId} does not exist`); |
| 36 | + if (invoice.status.toLowerCase() !== "defaulted") throw new Error(`invoice #${opts.invoiceId} is ${invoice.status}, not Defaulted`); |
| 37 | + const evidenceHash = opts.evidenceHash.replace(/^0x/i, "").toLowerCase(); |
| 38 | + const result = await executeAppeal(opts.invoiceId, evidenceHash, opts.payer); |
| 39 | + formatOutput(result, json, () => console.log(`Invoice #${result.invoiceId} appealed. TX: ${result.txHash}`)); |
| 40 | + } catch (error) { |
| 41 | + formatError((error as Error).message, "APPEAL_ERROR", json); |
| 42 | + } |
| 43 | + }); |
| 44 | + return cmd; |
| 45 | +} |
0 commit comments