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..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 diff --git a/components/payment.js b/components/payment.js index f7e19af63..646c385c4 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 = true } = {} ) => { + 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/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 } }