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().
This commit is contained in:
redshift
2026-05-24 10:16:16 +08:00
parent 99244a8bdb
commit 789fd9db2b
+17 -7
View File
@@ -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<typeof setInterval> | 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)) {