* Use same naming scheme between ln containers and env vars * Add router_lnd container * Only open channels to router_lnd * Use 1sat base fee and 0ppm fee rate * Add script to test routing * Also fund router_lnd wallet * Receiver fallbacks * Rename to predecessorId * Remove useless wallet table join * Missing renaming to predecessor * Fix payment stuck on sender error We want to await the invoice poll promise so we can check for receiver errors, but in case of sender errors, the promise will never settle. * Don't log failed forwards as sender errors * fix check for receiver error --------- Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com> Co-authored-by: k00b <k00b@stacker.news>
166 lines
6.2 KiB
JavaScript
166 lines
6.2 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 } 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 { 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 always await the poll promise here 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.
|
|
// but we don't wait forever because real sender errors will cause the poll promise to never settle.
|
|
await withTimeout(pollPromise, FAST_POLL_INTERVAL * 2.5)
|
|
} 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 wallet.def.sendPayment(bolt11, wallet.config, { logger })
|
|
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)
|
|
}
|
|
}, [])
|
|
}
|