From 789fd9db2b8838cc5dfcc042fce31fbc036eaf6d Mon Sep 17 00:00:00 2001 From: redshift <213178690+1ftredsh@users.noreply.github.com> Date: Sun, 24 May 2026 10:16:16 +0800 Subject: [PATCH] fix(auto-refill): broken backoff and preimage crash causing payment spam Two bugs caused the auto-refill loop to retry every 5s regardless of outcome: 1) Backoff used lastRefillAt which was only set on success. On failure it stayed at 0, so now - 0 < backoffInterval never blocked once the daemon had been running > 30s. Switched to a separate lastAttemptAt that is updated on every attempt (success or failure). 2) preimage.slice() crashed when NWC returned a payment result without a preimage field. This turned successful payments into exceptions, triggering retries and causing the wallet to be charged repeatedly. Guard preimage with an if-check before calling .slice(). --- src/daemon/wallet/auto-refill.ts | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/daemon/wallet/auto-refill.ts b/src/daemon/wallet/auto-refill.ts index 8305942..e7eb1b7 100644 --- a/src/daemon/wallet/auto-refill.ts +++ b/src/daemon/wallet/auto-refill.ts @@ -39,6 +39,7 @@ export function startAutoRefillLoop( intervalMs: number = 5000, ): () => void { let lastRefillAt = 0; + let lastAttemptAt = 0; // tracks last attempt (success or failure) for backoff let running = true; let timeout: ReturnType | null = null; let checkInProgress = false; @@ -73,7 +74,7 @@ export function startAutoRefillLoop( intervalMs * Math.pow(2, consecutiveFailures), 5 * 60 * 1000, // cap at 5 minutes ); - if (now - lastRefillAt < backoffInterval) { + if (now - lastAttemptAt < backoffInterval) { return; } @@ -114,21 +115,30 @@ export function startAutoRefillLoop( logger.log("[auto-refill] Wallet disconnected during refill check"); return; } - const { preimage, fees_paid } = await currentWallet.payInvoice(invoice); + const payment = await currentWallet.payInvoice(invoice); // Step 3: The Cashu mint should automatically detect the paid invoice // and issue tokens. We don't need to explicitly mint here; cocod // handles this on its end when the mint sees the payment. - logger.log( - `[auto-refill] Successfully refilled ${config.amount} sats. Preimage: ${preimage.slice(0, 16)}...`, - ); - if (fees_paid !== undefined) { - logger.log(`[auto-refill] Fees paid: ${fees_paid} msats`); + const preimage = payment.preimage; + if (preimage) { + logger.log( + `[auto-refill] Successfully refilled ${config.amount} sats. Preimage: ${preimage.slice(0, 16)}...`, + ); + } else { + logger.log( + `[auto-refill] Successfully refilled ${config.amount} sats (no preimage returned).`, + ); + } + if (payment.fees_paid !== undefined) { + logger.log(`[auto-refill] Fees paid: ${payment.fees_paid} msats`); } lastRefillAt = now; + lastAttemptAt = now; consecutiveFailures = 0; // reset on success } catch (error) { const message = error instanceof Error ? error.message : String(error); + lastAttemptAt = now; // track for backoff regardless of success/failure consecutiveFailures++; if (isFatalError(message)) {