fix: NWC payment timeout/error now rejects properly (BUG-F4)

- Timeout and WebSocket errors reject with proper Error objects
- Caller catches and falls through to poll-based confirmation
- Preimage undefined check prevents calling confirm with no preimage

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-12 23:10:50 +00:00
co-authored by Claude Opus 4.6
parent acecc79d04
commit 74cb5cc728
2 changed files with 61 additions and 12 deletions
@@ -0,0 +1,43 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
// Test the NWC timeout/error behavior in isolation
// Full component test would require too many mocks, so we test the promise pattern
describe('NWC payment timeout handling', () => {
beforeEach(() => {
vi.useFakeTimers()
})
afterEach(() => {
vi.useRealTimers()
})
it('timeout produces proper rejection error', async () => {
// Simulate the payViaNWC timeout pattern
const payPromise = new Promise<string | undefined>((_, reject) => {
setTimeout(() => {
reject(new Error('NWC payment timed out'))
}, 30_000)
})
vi.advanceTimersByTime(30_000)
await expect(payPromise).rejects.toThrow('NWC payment timed out')
})
it('websocket error produces proper rejection', async () => {
const payPromise = new Promise<string | undefined>((_, reject) => {
// Simulate immediate ws error
queueMicrotask(() => reject(new Error('NWC WebSocket error')))
})
await expect(payPromise).rejects.toThrow('NWC WebSocket error')
})
it('undefined preimage is treated as failure', () => {
const preimage: string | undefined = undefined
// The fixed code checks: if (preimage) { ...confirm... }
// So undefined preimage falls through to polling
expect(!preimage).toBe(true)
})
})
+18 -12
View File
@@ -173,16 +173,22 @@ export function useWallet() {
}
if (nwcUrl && nwcValid) {
paymentStatus.value = 'paying'
const preimage = await payViaNWC(nwcUrl, bolt11)
// Tell server payment is confirmed (skip lookup_invoice polling)
await authFetch(`/api/payments/confirm/${paymentId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ preimage, pubkey: pubkey.value }),
})
paymentStatus.value = 'confirmed'
return paymentId
try {
const preimage = await payViaNWC(nwcUrl, bolt11)
if (preimage) {
await authFetch(`/api/payments/confirm/${paymentId}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ preimage, pubkey: pubkey.value }),
})
paymentStatus.value = 'confirmed'
return paymentId
}
} catch (err) {
console.warn('[Wallet] NWC payment failed, falling back to polling:', err)
}
// NWC failed or returned no preimage — fall through to polling
paymentStatus.value = 'pending'
}
// No NWC — poll for confirmation (manual payment / QR code flow)
@@ -268,7 +274,7 @@ async function payViaNWC(nwcUrl: string, bolt11: string): Promise<string | undef
const ws = new WebSocket(nwc.relay)
const timeout = setTimeout(() => {
ws.close()
resolve(undefined)
reject(new Error('NWC payment timed out'))
}, 30_000)
ws.onopen = () => {
@@ -309,7 +315,7 @@ async function payViaNWC(nwcUrl: string, bolt11: string): Promise<string | undef
ws.onerror = () => {
clearTimeout(timeout)
resolve(undefined)
reject(new Error('NWC WebSocket error'))
}
})
}