diff --git a/src/cli.ts b/src/cli.ts index 196d01a..7b1c42d 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -106,6 +106,40 @@ async function printLightningInvoice(invoice: string): Promise { console.log(`${qr}\nInvoice:\n${invoice}`); } +/** Block until the daemon has minted the paid invoice, then show the credit. */ +async function waitForMintQuote(operationId: string): Promise { + console.log( + "\nWaiting for payment... (Ctrl+C stops waiting; the daemon keeps checking)", + ); + for (;;) { + const result = await callDaemon( + `/wallet/receive/bolt11/${encodeURIComponent(operationId)}`, + ); + const quote = result.output as + | { state?: string; amount?: number; error?: string } + | undefined; + if (quote?.state === "finalized" && quote.error) { + // The mint issued the quote but the wallet could not restore the proofs. + console.error(`Payment reached the mint but could not be credited: ${quote.error}`); + process.exit(1); + } + if (quote?.state === "finalized") { + const balance = await callDaemon("/wallet/balance"); + const balances = + (balance.output as { balances?: Record } | undefined) + ?.balances ?? {}; + const total = Object.values(balances).reduce((sum, sats) => sum + sats, 0); + console.log(`Received ${quote.amount} sat. Balance: ${total} sat`); + return; + } + if (quote?.state === "failed") { + console.error(`Invoice failed: ${quote.error ?? "unknown reason"}`); + process.exit(1); + } + await new Promise((resolve) => setTimeout(resolve, 2000)); + } +} + export function initializeWallet(walletDir = defaultWalletDir()): void { const walletConfig = join(walletDir, "config.json"); @@ -1728,11 +1762,14 @@ program }); const output = result.output as - | { invoice?: string; amount?: number; mintUrl?: string } + | { invoice?: string; operationId?: string; amount?: number; mintUrl?: string } | undefined; if (typeof output?.invoice === "string" && output.invoice) { await printLightningInvoice(output.invoice); + if (typeof output.operationId === "string") { + await waitForMintQuote(output.operationId); + } return; } diff --git a/src/daemon/http/index.ts b/src/daemon/http/index.ts index d6949c3..510d239 100644 --- a/src/daemon/http/index.ts +++ b/src/daemon/http/index.ts @@ -488,8 +488,22 @@ export function createDaemonRequestHandler(deps: { const body = await readJsonBody(req); const amount = getRequiredPositiveNumberField(body, "amount"); const mintUrl = optionalStringField(body, "mintUrl"); - const invoice = await deps.walletClient.receiveBolt11(amount, mintUrl); - return { output: { invoice, amount, mintUrl } }; + const { invoice, operationId } = await deps.walletClient.receiveBolt11( + amount, + mintUrl, + ); + return { output: { invoice, operationId, amount, mintUrl } }; + }); + return; + } + + const mintQuoteMatch = url.pathname.match(/^\/wallet\/receive\/bolt11\/([^/]+)$/); + if (req.method === "GET" && mintQuoteMatch?.[1]) { + const operationId = decodeURIComponent(mintQuoteMatch[1]); + await respond(res, async () => { + const quote = await deps.walletClient.getMintQuote?.(operationId); + if (!quote) throw new CocodHttpError(404, "Mint quote not found"); + return { output: quote }; }); return; } diff --git a/src/daemon/wallet/auto-refill.ts b/src/daemon/wallet/auto-refill.ts index a9848e0..5f3fbae 100644 --- a/src/daemon/wallet/auto-refill.ts +++ b/src/daemon/wallet/auto-refill.ts @@ -104,7 +104,7 @@ export function startAutoRefillLoop( logger.log( `[auto-refill] Creating BOLT-11 invoice for ${config.amount} sats via ${mintUrl}...`, ); - const invoice = await cocod.receiveBolt11(config.amount, mintUrl); + const { invoice } = await cocod.receiveBolt11(config.amount, mintUrl); // Step 2: Pay the invoice via NWC (applesauce) logger.log(`[auto-refill] Paying invoice via NWC...`); diff --git a/src/daemon/wallet/coco-client.ts b/src/daemon/wallet/coco-client.ts index 67acf8d..e0b64de 100644 --- a/src/daemon/wallet/coco-client.ts +++ b/src/daemon/wallet/coco-client.ts @@ -1383,7 +1383,7 @@ export async function createCocoClient( } }, - async receiveBolt11(amount: number, mintUrl?: string): Promise { + async receiveBolt11(amount: number, mintUrl?: string) { await waitForRecovery(); const targetMint = mintUrl ? normalizeMintUrl(mintUrl) @@ -1399,7 +1399,20 @@ export async function createCocoClient( if (!("request" in op)) { throw new Error("mint prepare did not return a payment request"); } - return op.request as string; + return { invoice: op.request as string, operationId: op.id }; + }, + + async getMintQuote(operationId: string) { + const op = await coco.ops.mint.get(operationId); + if (!op || op.state === "init") return null; + return { + operationId: op.id, + state: op.state, + mintState: op.lastObservedRemoteState, + amount: op.amount, + mintUrl: op.mintUrl, + error: op.error, + }; }, async sendCashu(amount: number, mintUrl?: string): Promise { diff --git a/src/daemon/wallet/cocod-client.ts b/src/daemon/wallet/cocod-client.ts index 6623391..d338506 100644 --- a/src/daemon/wallet/cocod-client.ts +++ b/src/daemon/wallet/cocod-client.ts @@ -109,13 +109,29 @@ export class CocodHttpError extends Error { } } +/** Progress of a Lightning top-up created with `receiveBolt11`. */ +export interface MintQuoteStatus { + operationId: string; + state: "pending" | "executing" | "finalized" | "failed"; + /** Last quote state reported by the mint (UNPAID, PAID, ISSUED). */ + mintState?: string; + amount: number; + mintUrl: string; + error?: string; +} + export interface CocodClient { ping(): Promise; getStatus(): Promise; unlock(passphrase: string): Promise; getBalances(): Promise>; receiveCashu(token: string): Promise; - receiveBolt11(amount: number, mintUrl?: string): Promise; + receiveBolt11( + amount: number, + mintUrl?: string, + ): Promise<{ invoice: string; operationId?: string }>; + /** Progress of a top-up, when the wallet tracks mint operations. */ + getMintQuote?(operationId: string): Promise; sendCashu(amount: number, mintUrl?: string): Promise; sendBolt11(invoice: string, mintUrl?: string): Promise; listMints(): Promise; @@ -421,8 +437,8 @@ export function createCocodClient( logger.debug(`[receiveCashu] Full response.output:`, message); return message; }, - async receiveBolt11(amount: number, mintUrl?: string): Promise { - return post("/receive/bolt11", { amount, mintUrl }); + async receiveBolt11(amount: number, mintUrl?: string) { + return { invoice: await post("/receive/bolt11", { amount, mintUrl }) }; }, async sendCashu(amount: number, mintUrl?: string): Promise { return post("/send/cashu", { amount, mintUrl }); diff --git a/src/daemon/wallet/index.ts b/src/daemon/wallet/index.ts index a031a10..a752c0a 100644 --- a/src/daemon/wallet/index.ts +++ b/src/daemon/wallet/index.ts @@ -189,7 +189,7 @@ export async function createWalletAdapter( // Step 2: Create a BOLT-11 invoice via cocod logger.log(`[nwc] Creating ${amount}-sat Lightning invoice via cocod...`); - const invoice = await client.receiveBolt11(amount, mintUrl); + const { invoice } = await client.receiveBolt11(amount, mintUrl); logger.log(`[nwc] Invoice: ${invoice}`); // Step 3: Pay it via NWC