From 5e8514757884114a1f0651ccb8e6955b2ba99695 Mon Sep 17 00:00:00 2001 From: ekzyis Date: Sat, 15 Feb 2025 03:01:14 +0100 Subject: [PATCH] Fix receiver fallback on caller error (#1907) * Rename to createUserInvoice * Fix no receiver fallback on wrap, direct or autowithdrawal error * Fix missing error logs for direct payments --- api/paidAction/index.js | 63 +++++++++++++++++++++++------------------ wallets/server.js | 54 +++++++++++++++++------------------ worker/autowithdraw.js | 24 +++++++++------- 3 files changed, 76 insertions(+), 65 deletions(-) diff --git a/api/paidAction/index.js b/api/paidAction/index.js index 8feabc30..cd627987 100644 --- a/api/paidAction/index.js +++ b/api/paidAction/index.js @@ -3,7 +3,7 @@ import { datePivot } from '@/lib/time' import { PAID_ACTION_PAYMENT_METHODS, USER_ID } from '@/lib/constants' import { createHmac } from '@/api/resolvers/wallet' import { Prisma } from '@prisma/client' -import { createWrappedInvoice, createInvoice as createUserInvoice } from '@/wallets/server' +import { createWrappedInvoice, createUserInvoice } from '@/wallets/server' import { assertBelowMaxPendingInvoices, assertBelowMaxPendingDirectPayments } from './lib/assert' import * as ITEM_CREATE from './itemCreate' @@ -264,42 +264,51 @@ async function performDirectAction (actionType, args, incomingContext) { throw new NonInvoiceablePeerError() } - let invoiceObject - try { await assertBelowMaxPendingDirectPayments(userId, incomingContext) const description = actionDescription ?? await paidActions[actionType].describe(args, incomingContext) - invoiceObject = await createUserInvoice(userId, { + + for await (const { invoice, logger, wallet } of createUserInvoice(userId, { msats: cost, description, expiry: INVOICE_EXPIRE_SECS - }, { models, lnd }) - } catch (e) { - console.error('failed to create outside invoice', e) - throw new NonInvoiceablePeerError() - } + }, { models, lnd })) { + let hash + try { + hash = parsePaymentRequest({ request: invoice }).id + } catch (e) { + console.error('failed to parse invoice', e) + logger?.error('failed to parse invoice: ' + e.message, { bolt11: invoice }) + continue + } - const { invoice, wallet } = invoiceObject - const hash = parsePaymentRequest({ request: invoice }).id - - const payment = await models.directPayment.create({ - data: { - comment, - lud18Data, - desc: noteStr, - bolt11: invoice, - msats: cost, - hash, - walletId: wallet.id, - receiverId: userId + try { + return { + invoice: await models.directPayment.create({ + data: { + comment, + lud18Data, + desc: noteStr, + bolt11: invoice, + msats: cost, + hash, + walletId: wallet.id, + receiverId: userId + } + }), + paymentMethod: 'DIRECT' + } + } catch (e) { + console.error('failed to create direct payment', e) + logger?.error('failed to create direct payment: ' + e.message, { bolt11: invoice }) + } } - }) - - return { - invoice: payment, - paymentMethod: 'DIRECT' + } catch (e) { + console.error('failed to create user invoice', e) } + + throw new NonInvoiceablePeerError() } export async function retryPaidAction (actionType, args, incomingContext) { diff --git a/wallets/server.js b/wallets/server.js index 58c00c11..4a937763 100644 --- a/wallets/server.js +++ b/wallets/server.js @@ -24,7 +24,7 @@ export default [lnd, cln, lnAddr, lnbits, nwc, phoenixd, blink, lnc, webln] const MAX_PENDING_INVOICES_PER_WALLET = 25 -export async function createInvoice (userId, { msats, description, descriptionHash, expiry = 360 }, { paymentAttempt, predecessorId, models }) { +export async function * createUserInvoice (userId, { msats, description, descriptionHash, expiry = 360 }, { paymentAttempt, predecessorId, models }) { // get the wallets in order of priority const wallets = await getInvoiceableWallets(userId, { paymentAttempt, @@ -72,44 +72,42 @@ export async function createInvoice (userId, { msats, description, descriptionHa } } - return { invoice, wallet, logger } + yield { invoice, wallet, logger } } catch (err) { + console.error('failed to create user invoice:', err) logger.error(err.message, { status: true }) } } - - throw new Error('no wallet to receive available') } export async function createWrappedInvoice (userId, { msats, feePercent, description, descriptionHash, expiry = 360 }, { paymentAttempt, predecessorId, models, me, lnd }) { - let logger, bolt11 - try { - const { invoice, wallet } = await createInvoice(userId, { - // this is the amount the stacker will receive, the other (feePercent)% is our fee - msats: toPositiveBigInt(msats) * (100n - feePercent) / 100n, - description, - descriptionHash, - expiry - }, { paymentAttempt, predecessorId, models }) - - logger = walletLogger({ wallet, models }) - bolt11 = invoice - - const { invoice: wrappedInvoice, maxFee } = - await wrapInvoice({ bolt11, feePercent }, { msats, description, descriptionHash }, { me, lnd }) - - return { - invoice, - wrappedInvoice: wrappedInvoice.request, - wallet, - maxFee + // loop over all receiver wallet invoices until we successfully wrapped one + for await (const { invoice, logger, wallet } of createUserInvoice(userId, { + // this is the amount the stacker will receive, the other (feePercent)% is our fee + msats: toPositiveBigInt(msats) * (100n - feePercent) / 100n, + description, + descriptionHash, + expiry + }, { paymentAttempt, predecessorId, models })) { + let bolt11 + try { + bolt11 = invoice + const { invoice: wrappedInvoice, maxFee } = await wrapInvoice({ bolt11, feePercent }, { msats, description, descriptionHash }, { me, lnd }) + return { + invoice, + wrappedInvoice: wrappedInvoice.request, + wallet, + maxFee + } + } catch (e) { + console.error('failed to wrap invoice:', e) + logger?.error('failed to wrap invoice: ' + e.message, { bolt11 }) } - } catch (e) { - logger?.error('invalid invoice: ' + e.message, { bolt11 }) - throw e } + + throw new Error('no wallet to receive available') } export async function getInvoiceableWallets (userId, { paymentAttempt, predecessorId, models }) { diff --git a/worker/autowithdraw.js b/worker/autowithdraw.js index c2105d02..b1bc59da 100644 --- a/worker/autowithdraw.js +++ b/worker/autowithdraw.js @@ -1,6 +1,6 @@ import { msatsSatsFloor, msatsToSats, satsToMsats } from '@/lib/format' import { createWithdrawal } from '@/api/resolvers/wallet' -import { createInvoice } from '@/wallets/server' +import { createUserInvoice } from '@/wallets/server' export async function autoWithdraw ({ data: { id }, models, lnd }) { const user = await models.user.findUnique({ where: { id } }) @@ -42,14 +42,18 @@ export async function autoWithdraw ({ data: { id }, models, lnd }) { if (pendingOrFailed.exists) return - const { invoice, wallet, logger } = await createInvoice(id, { msats, description: 'SN: autowithdrawal', expiry: 360 }, { models }) - - try { - return await createWithdrawal(null, - { invoice, maxFee: msatsToSats(maxFeeMsats) }, - { me: { id }, models, lnd, wallet, logger }) - } catch (err) { - logger.error(`incoming payment failed: ${err}`, { bolt11: invoice }) - throw err + for await (const { invoice, wallet, logger } of createUserInvoice(id, { + msats, + description: 'SN: autowithdrawal', + expiry: 360 + }, { models })) { + try { + return await createWithdrawal(null, + { invoice, maxFee: msatsToSats(maxFeeMsats) }, + { me: { id }, models, lnd, wallet, logger }) + } catch (err) { + console.error('failed to create autowithdrawal:', err) + logger?.error(`incoming payment failed: ${err}`, { bolt11: invoice }) + } } }