From 463b672d8ef5adb247cfa22b497ef3fd0b087313 Mon Sep 17 00:00:00 2001 From: ekzyis Date: Mon, 2 Dec 2024 03:27:12 +0100 Subject: [PATCH 1/3] Fix missing invoice retry before showing QR code --- api/resolvers/wallet.js | 27 +++++++++++++++++++++---- api/typeDefs/wallet.js | 2 +- components/item-info.js | 2 +- components/payment.js | 36 ++++++++++++++++++--------------- components/poll.js | 2 +- components/use-paid-mutation.js | 5 +---- fragments/wallet.js | 4 ++-- 7 files changed, 49 insertions(+), 29 deletions(-) diff --git a/api/resolvers/wallet.js b/api/resolvers/wallet.js index b0f9f7798..4b0d24e39 100644 --- a/api/resolvers/wallet.js +++ b/api/resolvers/wallet.js @@ -131,10 +131,15 @@ export function createHmac (hash) { } export function verifyHmac (hash, hmac) { + if (!hmac) { + throw new GqlInputError('hmac required') + } + const hmac2 = createHmac(hash) if (!timingSafeEqual(Buffer.from(hmac), Buffer.from(hmac2))) { throw new GqlAuthorizationError('bad hmac') } + return true } @@ -479,10 +484,24 @@ const resolvers = { }, createWithdrawl: createWithdrawal, sendToLnAddr, - cancelInvoice: async (parent, { hash, hmac }, { models, lnd, boss }) => { - verifyHmac(hash, hmac) - await finalizeHodlInvoice({ data: { hash }, lnd, models, boss }) - return await models.invoice.findFirst({ where: { hash } }) + cancelInvoice: async (parent, { id, hash, hmac }, { me, models, lnd, boss }) => { + if (!id && !hash) { + throw new GqlInputError('id or hash required') + } + + const inv = await models.invoice.findFirst({ + where: id + ? { id: Number(id) } + : { hash } + }) + + // owners can always cancel their own invoices, anons can only cancel with valid hmac + if (!me || inv.userId !== me.id) { + verifyHmac(inv.hash, hmac) + } + + await finalizeHodlInvoice({ data: { hash: inv.hash }, lnd, models, boss }) + return await models.invoice.findFirst({ where: { id: inv.id } }) }, dropBolt11: async (parent, { hash }, { me, models, lnd }) => { if (!me) { diff --git a/api/typeDefs/wallet.js b/api/typeDefs/wallet.js index 97f4f69e8..bccbbacf7 100644 --- a/api/typeDefs/wallet.js +++ b/api/typeDefs/wallet.js @@ -78,7 +78,7 @@ const typeDefs = ` createInvoice(amount: Int!): InvoiceOrDirect! createWithdrawl(invoice: String!, maxFee: Int!): Withdrawl! sendToLnAddr(addr: String!, amount: Int!, maxFee: Int!, comment: String, identifier: Boolean, name: String, email: String): Withdrawl! - cancelInvoice(hash: String!, hmac: String!): Invoice! + cancelInvoice(id: ID, hash: String, hmac: String): Invoice! dropBolt11(hash: String!): Boolean removeWallet(id: ID!): Boolean deleteWalletLogs(wallet: String): Boolean diff --git a/components/item-info.js b/components/item-info.js index 515993f88..c250ef60a 100644 --- a/components/item-info.js +++ b/components/item-info.js @@ -300,7 +300,7 @@ function PaymentInfo ({ item, disableRetry, setDisableRetry }) { >pending ) - onClick = () => waitForQrPayment({ id: item.invoice?.id }, null, { cancelOnClose: false }).catch(console.error) + onClick = () => waitForQrPayment({ id: item.invoice?.id }, null, { cancelOnClose: false, retry: true }).catch(console.error) } } else { return null diff --git a/components/payment.js b/components/payment.js index f7e19af63..953bda4f6 100644 --- a/components/payment.js +++ b/components/payment.js @@ -37,18 +37,14 @@ export const useInvoice = () => { return { invoice: data.invoice, check: that(data.invoice) } }, [client]) - const cancel = useCallback(async ({ hash, hmac }) => { - if (!hash || !hmac) { - throw new Error('missing hash or hmac') - } - - console.log('canceling invoice:', hash) - const { data } = await cancelInvoice({ variables: { hash, hmac } }) + const cancel = useCallback(async ({ id, hash, hmac }) => { + console.log('canceling invoice:', id || hash) + const { data } = await cancelInvoice({ variables: { id, hash, hmac } }) return data.cancelInvoice }, [cancelInvoice]) - const retry = useCallback(async ({ id, hash, hmac }, { update }) => { - console.log('retrying invoice:', hash) + const retry = useCallback(async ({ id, hash, hmac }, { update } = {}) => { + console.log('retrying invoice:', id || hash) const { data, error } = await retryPaidAction({ variables: { invoiceId: Number(id) }, update }) if (error) throw error @@ -70,30 +66,38 @@ export const useQrPayment = () => { keepOpen = true, cancelOnClose = true, persistOnNavigate = false, - waitFor = inv => inv?.satsReceived > 0 + waitFor = inv => inv?.satsReceived > 0, + retry = false } = {} ) => { + let qrInv = inv + + if (retry) { + await invoice.cancel(inv) + qrInv = await invoice.retry(inv) + } + return await new Promise((resolve, reject) => { let paid const cancelAndReject = async (onClose) => { if (!paid && cancelOnClose) { - const updatedInv = await invoice.cancel(inv).catch(console.error) + const updatedInv = await invoice.cancel(qrInv).catch(console.error) reject(new InvoiceCanceledError(updatedInv)) } - resolve(inv) + resolve(qrInv) } showModal(onClose => reject(new InvoiceExpiredError(inv))} - onCanceled={inv => { onClose(); reject(new InvoiceCanceledError(inv, inv?.actionError)) }} - onPayment={(inv) => { paid = true; onClose(); resolve(inv) }} + onExpired={expiredInv => reject(new InvoiceExpiredError(expiredInv))} + onCanceled={canceledInv => { onClose(); reject(new InvoiceCanceledError(canceledInv, canceledInv?.actionError)) }} + onPayment={(paidInv) => { paid = true; onClose(); resolve(paidInv) }} poll />, { keepOpen, persistOnNavigate, onClose: cancelAndReject }) diff --git a/components/poll.js b/components/poll.js index dc694f801..d0a3657f2 100644 --- a/components/poll.js +++ b/components/poll.js @@ -52,7 +52,7 @@ export default function Poll ({ item }) { waitForQrPayment( - { id: parseInt(item.poll.meInvoiceId) }, null, { cancelOnClose: false }).catch(console.error)} + { id: parseInt(item.poll.meInvoiceId) }, null, { cancelOnClose: false, retry: true }).catch(console.error)} >vote pending ) diff --git a/components/use-paid-mutation.js b/components/use-paid-mutation.js index d3b1bf7f0..e2dcb90f9 100644 --- a/components/use-paid-mutation.js +++ b/components/use-paid-mutation.js @@ -62,10 +62,7 @@ export function usePaidMutation (mutation, } const paymentAttempted = walletError instanceof WalletPaymentError - if (paymentAttempted) { - walletInvoice = await invoiceHelper.retry(walletInvoice, { update: updateOnFallback }) - } - return await waitForQrPayment(walletInvoice, walletError, { persistOnNavigate, waitFor }) + return await waitForQrPayment(walletInvoice, walletError, { persistOnNavigate, waitFor, retry: paymentAttempted }) }, [waitForWalletPayment, waitForQrPayment, invoiceHelper]) const innerMutate = useCallback(async ({ diff --git a/fragments/wallet.js b/fragments/wallet.js index 7fe3fc492..c05fe0050 100644 --- a/fragments/wallet.js +++ b/fragments/wallet.js @@ -224,8 +224,8 @@ export const SET_WALLET_PRIORITY = gql` export const CANCEL_INVOICE = gql` ${INVOICE_FIELDS} - mutation cancelInvoice($hash: String!, $hmac: String!) { - cancelInvoice(hash: $hash, hmac: $hmac) { + mutation cancelInvoice($id: ID, $hash: String, $hmac: String) { + cancelInvoice(id: $id, hash: $hash, hmac: $hmac) { ...InvoiceFields } } From 9940588be5246233bc00208854d9d865b1728c37 Mon Sep 17 00:00:00 2001 From: ekzyis Date: Mon, 2 Dec 2024 03:35:34 +0100 Subject: [PATCH 2/3] Retry QR invoices by default --- components/item-info.js | 2 +- components/payment.js | 2 +- components/poll.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/components/item-info.js b/components/item-info.js index c250ef60a..515993f88 100644 --- a/components/item-info.js +++ b/components/item-info.js @@ -300,7 +300,7 @@ function PaymentInfo ({ item, disableRetry, setDisableRetry }) { >pending ) - onClick = () => waitForQrPayment({ id: item.invoice?.id }, null, { cancelOnClose: false, retry: true }).catch(console.error) + onClick = () => waitForQrPayment({ id: item.invoice?.id }, null, { cancelOnClose: false }).catch(console.error) } } else { return null diff --git a/components/payment.js b/components/payment.js index 953bda4f6..646c385c4 100644 --- a/components/payment.js +++ b/components/payment.js @@ -67,7 +67,7 @@ export const useQrPayment = () => { cancelOnClose = true, persistOnNavigate = false, waitFor = inv => inv?.satsReceived > 0, - retry = false + retry = true } = {} ) => { let qrInv = inv diff --git a/components/poll.js b/components/poll.js index d0a3657f2..dc694f801 100644 --- a/components/poll.js +++ b/components/poll.js @@ -52,7 +52,7 @@ export default function Poll ({ item }) { waitForQrPayment( - { id: parseInt(item.poll.meInvoiceId) }, null, { cancelOnClose: false, retry: true }).catch(console.error)} + { id: parseInt(item.poll.meInvoiceId) }, null, { cancelOnClose: false }).catch(console.error)} >vote pending ) From e805331c4cd743a100e7753eccda7f2000b5f3be Mon Sep 17 00:00:00 2001 From: ekzyis Date: Mon, 2 Dec 2024 13:09:32 +0100 Subject: [PATCH 3/3] Fix pending shown after QR invoice canceled --- components/item-info.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/item-info.js b/components/item-info.js index 515993f88..a1270132a 100644 --- a/components/item-info.js +++ b/components/item-info.js @@ -300,7 +300,7 @@ function PaymentInfo ({ item, disableRetry, setDisableRetry }) { >pending ) - onClick = () => waitForQrPayment({ id: item.invoice?.id }, null, { cancelOnClose: false }).catch(console.error) + onClick = () => waitForQrPayment({ id: item.invoice?.id }).catch(console.error) } } else { return null