Recover failed Lightning file attempts without blocking other payment methods
This commit is contained in:
@@ -56,6 +56,7 @@ describe('Lightning file delivery recovery', () => {
|
||||
expect(vm.viewerUrl).toBe('/api/peer-content/peer.onion/paid-file')
|
||||
expect(vm.viewerMime).toBe('video/mp4')
|
||||
expect(vi.mocked(rpcClient.call).mock.calls.find(([v]) => v.method === 'content.download-peer-paid')![0].params).toMatchObject({ cache_only: true, method: 'cashu' })
|
||||
expect(vi.mocked(rpcClient.call).mock.calls.find(([v]) => v.method === 'content.download-peer-paid')![0].maxRetries).toBe(1)
|
||||
wrapper.unmount()
|
||||
})
|
||||
it('does not issue a duplicate ecash purchase while delivery is pending', async () => {
|
||||
@@ -133,4 +134,80 @@ describe('Lightning file delivery recovery', () => {
|
||||
expect(rpcClient.payLightningInvoice).not.toHaveBeenCalled()
|
||||
save.mockRestore(); wrapper.unmount()
|
||||
})
|
||||
it('reopens ecash after a returned terminal failure while retaining failed-attempt history', async () => {
|
||||
vi.mocked(rpcClient.payLightningInvoice).mockResolvedValue({ status: 'failed', failure_reason: 'Insufficient channel balance' } as never)
|
||||
const original = vi.mocked(rpcClient.call).getMockImplementation()!
|
||||
vi.mocked(rpcClient.call).mockImplementation(async args => args.method === 'wallet.ecash-balance' ? { cashu_sats: 10 } : original(args))
|
||||
const { wrapper, vm } = await open()
|
||||
vm.openPayModal({ ...item, access: { paid: { price_sats: 5, accepted: ['lightning', 'ecash'] } } })
|
||||
await vm.payWithLightning(); await flushPromises()
|
||||
expect(vm.hasBlockingLightningReceipt).toBe(false)
|
||||
expect(JSON.parse(localStorage.getItem(receiptKey)!)).toMatchObject({ state: 'failed', payment_hash: hash })
|
||||
expect(wrapper.text()).toContain('Pay from this node’s ecash wallet')
|
||||
await vm.prepareEcashPay()
|
||||
expect(vm.ecashPlan.chosen).toBe('cashu')
|
||||
expect(rpcClient.payLightningInvoice).toHaveBeenCalledTimes(1)
|
||||
expect(download).not.toHaveBeenCalled()
|
||||
wrapper.unmount()
|
||||
})
|
||||
it('resolves an old-backend exception using definitive LND state', async () => {
|
||||
vi.mocked(rpcClient.payLightningInvoice).mockRejectedValue(new Error('Payment failed: Insufficient channel balance'))
|
||||
const original = vi.mocked(rpcClient.call).getMockImplementation()!
|
||||
vi.mocked(rpcClient.call).mockImplementation(async args => args.method === 'lnd.paymentstatus' ? { status: 'failed', failure_reason: 'Insufficient channel balance' } : original(args))
|
||||
const { wrapper, vm } = await open(); await vm.payWithLightning()
|
||||
expect(vm.hasBlockingLightningReceipt).toBe(false)
|
||||
expect(JSON.parse(localStorage.getItem(receiptKey)!)).toMatchObject({ state: 'failed' })
|
||||
expect(vi.mocked(rpcClient.call).mock.calls.find(([v]) => v.method === 'lnd.paymentstatus')![0].params).toEqual({ payment_hash: hash })
|
||||
expect(download).not.toHaveBeenCalled()
|
||||
wrapper.unmount()
|
||||
})
|
||||
it('resolves an already stuck receipt without another payment or invoice', async () => {
|
||||
localStorage.setItem(receiptKey, JSON.stringify({ bolt11: 'ln-test', payment_hash: hash, price_sats: 5 }))
|
||||
const original = vi.mocked(rpcClient.call).getMockImplementation()!
|
||||
vi.mocked(rpcClient.call).mockImplementation(async args => args.method === 'lnd.paymentstatus' ? { status: 'failed', failure_reason: 'No route' } : original(args))
|
||||
const { wrapper, vm } = await open(); await vm.payWithLightning()
|
||||
expect(vm.hasBlockingLightningReceipt).toBe(false)
|
||||
expect(rpcClient.payLightningInvoice).not.toHaveBeenCalled()
|
||||
expect(vi.mocked(rpcClient.call).mock.calls.some(([v]) => v.method === 'content.request-invoice')).toBe(false)
|
||||
wrapper.unmount()
|
||||
})
|
||||
it('keeps ambiguous attempts recoverable and prevents another payment method', async () => {
|
||||
vi.mocked(rpcClient.payLightningInvoice).mockRejectedValue(new Error('Connection lost'))
|
||||
const original = vi.mocked(rpcClient.call).getMockImplementation()!
|
||||
vi.mocked(rpcClient.call).mockImplementation(async args => args.method === 'lnd.paymentstatus' ? { status: 'unknown' } : original(args))
|
||||
const { wrapper, vm } = await open(); await vm.payWithLightning()
|
||||
expect(vm.hasBlockingLightningReceipt).toBe(true)
|
||||
expect(JSON.parse(localStorage.getItem(receiptKey)!)).toMatchObject({ state: 'pending' })
|
||||
await vm.prepareEcashPay()
|
||||
vm.ecashPlan = { cashu: 10, fedimint: 0, ark: 0, total: 10, chosen: 'cashu' }
|
||||
await vm.confirmEcashPay(); await vm.payOnchain()
|
||||
expect(vi.mocked(rpcClient.call).mock.calls.some(([v]) => ['content.download-peer-paid', 'content.request-onchain', 'lnd.sendcoins'].includes(v.method))).toBe(false)
|
||||
expect(rpcClient.payLightningInvoice).toHaveBeenCalledTimes(1)
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('returns known in-flight recovery promptly without a long delivery call or another payment', async () => {
|
||||
localStorage.setItem(receiptKey, JSON.stringify({ bolt11: 'saved', payment_hash: hash, price_sats: 5, state: 'pending' }))
|
||||
const original = vi.mocked(rpcClient.call).getMockImplementation()!
|
||||
vi.mocked(rpcClient.call).mockImplementation(async args => args.method === 'lnd.paymentstatus' ? { status: 'pending' } : original(args))
|
||||
const { wrapper, vm } = await open()
|
||||
await vm.payWithLightning()
|
||||
expect(vm.lnPaying).toBe(false)
|
||||
expect(vm.lnError).toContain('close this window')
|
||||
expect(vi.mocked(rpcClient.call).mock.calls.some(([c]) => ['content.request-invoice', 'content.download-peer-invoice'].includes(c.method))).toBe(false)
|
||||
expect(rpcClient.payLightningInvoice).not.toHaveBeenCalled()
|
||||
vm.closePayModal()
|
||||
expect(JSON.parse(localStorage.getItem(receiptKey)!)).toMatchObject({ state: 'pending' })
|
||||
wrapper.unmount()
|
||||
})
|
||||
it('requests a fresh QR invoice after a confirmed failed local attempt', async () => {
|
||||
localStorage.setItem(receiptKey, JSON.stringify({ bolt11: 'failed-old', payment_hash: hash, price_sats: 5, state: 'failed' }))
|
||||
const { wrapper, vm } = await open()
|
||||
await vm.payWithInvoice()
|
||||
expect(vi.mocked(rpcClient.call).mock.calls.filter(([c]) => c.method === 'content.request-invoice')).toHaveLength(1)
|
||||
expect(JSON.parse(localStorage.getItem(receiptKey)!)).toMatchObject({ bolt11: 'ln-test', state: 'pending' })
|
||||
expect(rpcClient.payLightningInvoice).not.toHaveBeenCalled()
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user