170 lines
6.3 KiB
JavaScript
170 lines
6.3 KiB
JavaScript
import { useCallback } from 'react'
|
|
import { useSendWallets } from '@/wallets'
|
|
import { formatSats } from '@/lib/format'
|
|
import useInvoice from '@/components/use-invoice'
|
|
import { FAST_POLL_INTERVAL, WALLET_SEND_PAYMENT_TIMEOUT_MS } from '@/lib/constants'
|
|
import {
|
|
WalletsNotAvailableError, WalletSenderError, WalletAggregateError, WalletPaymentAggregateError,
|
|
WalletNotEnabledError, WalletSendNotConfiguredError, WalletPaymentError, WalletError, WalletReceiverError
|
|
} from '@/wallets/errors'
|
|
import { canSend } from './common'
|
|
import { useWalletLoggerFactory } from './logger'
|
|
import { timeoutSignal, withTimeout } from '@/lib/time'
|
|
|
|
export function useWalletPayment () {
|
|
const wallets = useSendWallets()
|
|
const sendPayment = useSendPayment()
|
|
const loggerFactory = useWalletLoggerFactory()
|
|
const invoiceHelper = useInvoice()
|
|
|
|
return useCallback(async (invoice, { waitFor, updateOnFallback }) => {
|
|
let aggregateError = new WalletAggregateError([])
|
|
let latestInvoice = invoice
|
|
|
|
// throw a special error that caller can handle separately if no payment was attempted
|
|
if (wallets.length === 0) {
|
|
throw new WalletsNotAvailableError()
|
|
}
|
|
|
|
for (let i = 0; i < wallets.length; i++) {
|
|
const wallet = wallets[i]
|
|
const logger = loggerFactory(wallet)
|
|
|
|
const { bolt11 } = latestInvoice
|
|
const controller = invoiceController(latestInvoice, invoiceHelper.isInvoice)
|
|
|
|
const walletPromise = sendPayment(wallet, logger, latestInvoice)
|
|
const pollPromise = controller.wait(waitFor)
|
|
|
|
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.
|
|
walletPromise.catch(reject)
|
|
pollPromise.then(resolve).catch(reject)
|
|
})
|
|
} catch (err) {
|
|
let paymentError = err
|
|
const message = `payment failed: ${paymentError.reason ?? paymentError.message}`
|
|
|
|
if (!(paymentError instanceof WalletError)) {
|
|
// payment failed for some reason unrelated to wallets (ie invoice expired or was canceled).
|
|
// bail out of attempting wallets.
|
|
logger.error(message, { bolt11 })
|
|
throw paymentError
|
|
}
|
|
|
|
// at this point, paymentError is always a wallet error,
|
|
// we just need to distinguish between receiver and sender errors
|
|
|
|
try {
|
|
// we need to poll one more time to check for failed forwards since sender wallet errors
|
|
// can be caused by them which we want to handle as receiver errors, not sender errors.
|
|
await invoiceHelper.isInvoice(latestInvoice, waitFor)
|
|
} catch (err) {
|
|
if (err instanceof WalletError) {
|
|
paymentError = err
|
|
}
|
|
}
|
|
|
|
if (paymentError instanceof WalletReceiverError) {
|
|
// if payment failed because of the receiver, use the same wallet again
|
|
// and log this as info, not error
|
|
logger.info('failed to forward payment to receiver, retrying with new invoice', { bolt11 })
|
|
i -= 1
|
|
} else if (paymentError instanceof WalletPaymentError) {
|
|
// only log payment errors, not configuration errors
|
|
logger.error(message, { bolt11 })
|
|
}
|
|
|
|
if (paymentError instanceof WalletPaymentError) {
|
|
// if a payment was attempted, cancel invoice to make sure it cannot be paid later and create new invoice to retry.
|
|
await invoiceHelper.cancel(latestInvoice)
|
|
}
|
|
|
|
// only create a new invoice if we will try to pay with a wallet again
|
|
const retry = paymentError instanceof WalletReceiverError || i < wallets.length - 1
|
|
if (retry) {
|
|
latestInvoice = await invoiceHelper.retry(latestInvoice, { update: updateOnFallback })
|
|
}
|
|
|
|
aggregateError = new WalletAggregateError([aggregateError, paymentError], latestInvoice)
|
|
|
|
continue
|
|
} finally {
|
|
controller.stop()
|
|
}
|
|
}
|
|
|
|
// if we reach this line, no wallet payment succeeded
|
|
throw new WalletPaymentAggregateError([aggregateError], latestInvoice)
|
|
}, [wallets, invoiceHelper, sendPayment])
|
|
}
|
|
|
|
function invoiceController (inv, isInvoice) {
|
|
const controller = new AbortController()
|
|
const signal = controller.signal
|
|
controller.wait = async (waitFor = inv => inv?.actionState === 'PAID') => {
|
|
return await new Promise((resolve, reject) => {
|
|
let updatedInvoice, paid
|
|
const interval = setInterval(async () => {
|
|
try {
|
|
({ invoice: updatedInvoice, check: paid } = await isInvoice(inv, waitFor))
|
|
if (paid) {
|
|
resolve(updatedInvoice)
|
|
clearInterval(interval)
|
|
signal.removeEventListener('abort', abort)
|
|
} else {
|
|
console.info(`invoice #${inv.id}: waiting for payment ...`)
|
|
}
|
|
} catch (err) {
|
|
reject(err)
|
|
clearInterval(interval)
|
|
signal.removeEventListener('abort', abort)
|
|
}
|
|
}, FAST_POLL_INTERVAL)
|
|
|
|
const abort = () => {
|
|
console.info(`invoice #${inv.id}: stopped waiting`)
|
|
resolve(updatedInvoice)
|
|
clearInterval(interval)
|
|
signal.removeEventListener('abort', abort)
|
|
}
|
|
signal.addEventListener('abort', abort)
|
|
})
|
|
}
|
|
|
|
controller.stop = () => controller.abort()
|
|
|
|
return controller
|
|
}
|
|
|
|
function useSendPayment () {
|
|
return useCallback(async (wallet, logger, invoice) => {
|
|
if (!wallet.config.enabled) {
|
|
throw new WalletNotEnabledError(wallet.def.name)
|
|
}
|
|
|
|
if (!canSend(wallet)) {
|
|
throw new WalletSendNotConfiguredError(wallet.def.name)
|
|
}
|
|
|
|
const { bolt11, satsRequested } = invoice
|
|
|
|
logger.info(`↗ sending payment: ${formatSats(satsRequested)}`, { bolt11 })
|
|
try {
|
|
const preimage = await withTimeout(
|
|
wallet.def.sendPayment(bolt11, wallet.config, {
|
|
logger,
|
|
signal: timeoutSignal(WALLET_SEND_PAYMENT_TIMEOUT_MS)
|
|
}),
|
|
WALLET_SEND_PAYMENT_TIMEOUT_MS)
|
|
logger.ok(`↗ payment sent: ${formatSats(satsRequested)}`, { bolt11, preimage })
|
|
} catch (err) {
|
|
// we don't log the error here since we want to handle receiver errors separately
|
|
const message = err.message || err.toString?.()
|
|
throw new WalletSenderError(wallet.def.name, invoice, message)
|
|
}
|
|
}, [])
|
|
}
|