Skip to content

Commit

Permalink
Fix missing invoice retry before showing QR code
Browse files Browse the repository at this point in the history
  • Loading branch information
ekzyis committed Dec 2, 2024
1 parent 8595a2b commit 463b672
Show file tree
Hide file tree
Showing 7 changed files with 49 additions and 29 deletions.
27 changes: 23 additions & 4 deletions api/resolvers/wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion api/typeDefs/wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion components/item-info.js
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ function PaymentInfo ({ item, disableRetry, setDisableRetry }) {
>pending
</span>
)
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
Expand Down
36 changes: 20 additions & 16 deletions components/payment.js
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 =>
<Invoice
id={inv.id}
id={qrInv.id}
modal
description
status='loading'
successVerb='received'
walletError={walletError}
waitFor={waitFor}
onExpired={inv => 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 })
Expand Down
2 changes: 1 addition & 1 deletion components/poll.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export default function Poll ({ item }) {
<span
className='ms-2 fw-bold text-info pointer'
onClick={() => 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
</span>
)
Expand Down
5 changes: 1 addition & 4 deletions components/use-paid-mutation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 ({
Expand Down
4 changes: 2 additions & 2 deletions fragments/wallet.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down

0 comments on commit 463b672

Please sign in to comment.