2024-11-27 23:31:08 +00:00
|
|
|
import { useCallback } from 'react'
|
|
|
|
import { useWalletsWithPayments } from '@/wallets'
|
2024-11-25 00:53:57 +00:00
|
|
|
import { formatSats } from '@/lib/format'
|
|
|
|
import { useInvoice } from '@/components/payment'
|
|
|
|
import { FAST_POLL_INTERVAL } from '@/lib/constants'
|
2024-11-26 04:01:23 +00:00
|
|
|
import {
|
2024-11-27 17:00:35 +00:00
|
|
|
WalletsNotAvailableError, WalletSenderError, WalletAggregateError, WalletPaymentAggregateError,
|
|
|
|
WalletNotEnabledError, WalletSendNotConfiguredError, WalletPaymentError, WalletError
|
2024-11-26 04:01:23 +00:00
|
|
|
} from '@/wallets/errors'
|
2024-11-26 03:27:44 +00:00
|
|
|
import { canSend } from './common'
|
2024-11-27 23:31:08 +00:00
|
|
|
import { useWalletLoggerFactory } from './logger'
|
2024-11-25 00:53:57 +00:00
|
|
|
|
|
|
|
export function useWalletPayment () {
|
2024-11-27 23:31:08 +00:00
|
|
|
const wallets = useWalletsWithPayments()
|
|
|
|
const sendPayment = useSendPayment()
|
2024-11-25 00:53:57 +00:00
|
|
|
const invoiceHelper = useInvoice()
|
|
|
|
|
2024-11-27 23:31:08 +00:00
|
|
|
return useCallback(async (invoice, { waitFor }) => {
|
2024-11-28 00:35:44 +00:00
|
|
|
let aggregateError = new WalletAggregateError([])
|
|
|
|
let latestInvoice = invoice
|
2024-11-25 00:53:57 +00:00
|
|
|
|
2024-11-27 16:55:27 +00:00
|
|
|
// throw a special error that caller can handle separately if no payment was attempted
|
2024-11-28 00:35:44 +00:00
|
|
|
if (wallets.length === 0) {
|
2024-11-27 16:55:27 +00:00
|
|
|
throw new WalletsNotAvailableError()
|
|
|
|
}
|
|
|
|
|
2024-11-27 23:31:08 +00:00
|
|
|
for (const [i, wallet] of wallets.entries()) {
|
2024-11-28 00:35:44 +00:00
|
|
|
const controller = invoiceController(latestInvoice, invoiceHelper.isInvoice)
|
2024-11-25 00:53:57 +00:00
|
|
|
try {
|
|
|
|
return await new Promise((resolve, reject) => {
|
|
|
|
// can't await wallet payments since we might pay hold invoices and thus payments might not settle immediately.
|
|
|
|
// that's why we separately check if we received the payment with the invoice controller.
|
2024-11-28 00:35:44 +00:00
|
|
|
sendPayment(wallet, latestInvoice).catch(reject)
|
2024-11-25 00:53:57 +00:00
|
|
|
controller.wait(waitFor)
|
|
|
|
.then(resolve)
|
|
|
|
.catch(reject)
|
|
|
|
})
|
|
|
|
} catch (err) {
|
2024-11-26 07:43:05 +00:00
|
|
|
// cancel invoice to make sure it cannot be paid later and create new invoice to retry.
|
2024-11-26 02:29:28 +00:00
|
|
|
// we only need to do this if payment was attempted which is not the case if the wallet is not enabled.
|
2024-11-28 00:35:44 +00:00
|
|
|
if (err instanceof WalletPaymentError) {
|
|
|
|
await invoiceHelper.cancel(latestInvoice)
|
2024-11-27 15:52:21 +00:00
|
|
|
|
|
|
|
// is there another wallet to try?
|
2024-11-27 23:31:08 +00:00
|
|
|
const lastAttempt = i === wallets.length - 1
|
2024-11-27 15:52:21 +00:00
|
|
|
if (!lastAttempt) {
|
2024-11-28 00:35:44 +00:00
|
|
|
latestInvoice = await invoiceHelper.retry(latestInvoice)
|
2024-11-27 15:52:21 +00:00
|
|
|
}
|
2024-11-25 00:53:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: receiver fallbacks
|
|
|
|
//
|
|
|
|
// if payment failed because of the receiver, we should use the same wallet again.
|
|
|
|
// if (err instanceof ReceiverError) { ... }
|
|
|
|
|
|
|
|
// try next wallet if the payment failed because of the wallet
|
|
|
|
// and not because it expired or was canceled
|
2024-11-28 00:35:44 +00:00
|
|
|
if (err instanceof WalletError) {
|
|
|
|
aggregateError = new WalletAggregateError([aggregateError, err], latestInvoice)
|
2024-11-25 00:53:57 +00:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
// payment failed not because of the sender or receiver wallet. bail out of attemping wallets.
|
|
|
|
throw err
|
|
|
|
} finally {
|
|
|
|
controller.stop()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// if we reach this line, no wallet payment succeeded
|
2024-11-28 00:35:44 +00:00
|
|
|
throw new WalletPaymentAggregateError([aggregateError], latestInvoice)
|
2024-11-27 23:31:08 +00:00
|
|
|
}, [wallets, invoiceHelper, sendPayment])
|
2024-11-25 00:53:57 +00:00
|
|
|
}
|
|
|
|
|
2024-11-27 23:31:08 +00:00
|
|
|
function invoiceController (inv, isInvoice) {
|
2024-11-25 00:53:57 +00:00
|
|
|
const controller = new AbortController()
|
|
|
|
const signal = controller.signal
|
|
|
|
controller.wait = async (waitFor = inv => inv?.actionState === 'PAID') => {
|
|
|
|
return await new Promise((resolve, reject) => {
|
2024-11-27 18:09:56 +00:00
|
|
|
let updatedInvoice, paid
|
2024-11-25 00:53:57 +00:00
|
|
|
const interval = setInterval(async () => {
|
|
|
|
try {
|
2024-11-27 18:09:56 +00:00
|
|
|
({ invoice: updatedInvoice, check: paid } = await isInvoice(inv, waitFor))
|
2024-11-25 00:53:57 +00:00
|
|
|
if (paid) {
|
2024-11-27 18:09:56 +00:00
|
|
|
resolve(updatedInvoice)
|
2024-11-25 00:53:57 +00:00
|
|
|
clearInterval(interval)
|
|
|
|
signal.removeEventListener('abort', abort)
|
|
|
|
} else {
|
2024-11-26 09:05:00 +00:00
|
|
|
console.info(`invoice #${inv.id}: waiting for payment ...`)
|
2024-11-25 00:53:57 +00:00
|
|
|
}
|
|
|
|
} catch (err) {
|
|
|
|
reject(err)
|
|
|
|
clearInterval(interval)
|
|
|
|
signal.removeEventListener('abort', abort)
|
|
|
|
}
|
|
|
|
}, FAST_POLL_INTERVAL)
|
|
|
|
|
|
|
|
const abort = () => {
|
2024-11-26 09:05:00 +00:00
|
|
|
console.info(`invoice #${inv.id}: stopped waiting`)
|
2024-11-27 18:09:56 +00:00
|
|
|
resolve(updatedInvoice)
|
2024-11-25 00:53:57 +00:00
|
|
|
clearInterval(interval)
|
|
|
|
signal.removeEventListener('abort', abort)
|
|
|
|
}
|
|
|
|
signal.addEventListener('abort', abort)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
controller.stop = () => controller.abort()
|
|
|
|
|
|
|
|
return controller
|
|
|
|
}
|
|
|
|
|
2024-11-27 23:31:08 +00:00
|
|
|
function useSendPayment () {
|
|
|
|
const factory = useWalletLoggerFactory()
|
|
|
|
|
|
|
|
return useCallback(async (wallet, invoice) => {
|
|
|
|
const logger = factory(wallet)
|
|
|
|
|
2024-11-25 00:53:57 +00:00
|
|
|
if (!wallet.config.enabled) {
|
|
|
|
throw new WalletNotEnabledError(wallet.def.name)
|
|
|
|
}
|
|
|
|
|
2024-11-26 03:27:44 +00:00
|
|
|
if (!canSend(wallet)) {
|
|
|
|
throw new WalletSendNotConfiguredError(wallet.def.name)
|
|
|
|
}
|
|
|
|
|
2024-11-26 02:29:28 +00:00
|
|
|
const { bolt11, satsRequested } = invoice
|
|
|
|
|
|
|
|
logger.info(`↗ sending payment: ${formatSats(satsRequested)}`, { bolt11 })
|
2024-11-25 00:53:57 +00:00
|
|
|
try {
|
|
|
|
const preimage = await wallet.def.sendPayment(bolt11, wallet.config, { logger })
|
2024-11-26 02:29:28 +00:00
|
|
|
logger.ok(`↗ payment sent: ${formatSats(satsRequested)}`, { bolt11, preimage })
|
2024-11-25 00:53:57 +00:00
|
|
|
} catch (err) {
|
|
|
|
const message = err.message || err.toString?.()
|
|
|
|
logger.error(`payment failed: ${message}`, { bolt11 })
|
2024-11-26 04:01:23 +00:00
|
|
|
throw new WalletSenderError(wallet.def.name, invoice, message)
|
2024-11-25 00:53:57 +00:00
|
|
|
}
|
2024-11-27 23:31:08 +00:00
|
|
|
}, [factory])
|
2024-11-25 00:53:57 +00:00
|
|
|
}
|