stacker.news/wallets/payment.js

140 lines
5.0 KiB
JavaScript
Raw Normal View History

2024-11-27 23:31:08 +00:00
import { useCallback } from 'react'
2024-11-28 01:39:20 +00:00
import { useSendWallets } 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'
import {
WalletsNotAvailableError, WalletSenderError, WalletAggregateError, WalletPaymentAggregateError,
WalletNotEnabledError, WalletSendNotConfiguredError, WalletPaymentError, WalletError
} from '@/wallets/errors'
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-28 01:39:20 +00:00
const wallets = useSendWallets()
2024-11-27 23:31:08 +00:00
const sendPayment = useSendPayment()
2024-11-25 00:53:57 +00:00
const invoiceHelper = useInvoice()
return useCallback(async (invoice, { waitFor, update }) => {
2024-11-28 00:35:44 +00:00
let aggregateError = new WalletAggregateError([])
let latestInvoice = invoice
2024-11-25 00:53:57 +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) {
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) {
// 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)
// is there another wallet to try?
2024-11-27 23:31:08 +00:00
const lastAttempt = i === wallets.length - 1
if (!lastAttempt) {
latestInvoice = await invoiceHelper.retry(latestInvoice, { update })
}
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) => {
let updatedInvoice, paid
2024-11-25 00:53:57 +00:00
const interval = setInterval(async () => {
try {
({ invoice: updatedInvoice, check: paid } = await isInvoice(inv, waitFor))
2024-11-25 00:53:57 +00:00
if (paid) {
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`)
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)
}
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 })
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
}