From 6a02ea8c5c3eb62010be70aa449d0e8f97c808e8 Mon Sep 17 00:00:00 2001 From: ekzyis Date: Thu, 2 Jan 2025 17:35:54 +0100 Subject: [PATCH 1/2] Allow cancel of own invoices without hmac (#1787) --- api/resolvers/wallet.js | 13 +++++++++++-- api/typeDefs/wallet.js | 2 +- components/use-invoice.js | 4 ---- fragments/wallet.js | 2 +- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/api/resolvers/wallet.js b/api/resolvers/wallet.js index ff1f1782e..24f5e301b 100644 --- a/api/resolvers/wallet.js +++ b/api/resolvers/wallet.js @@ -134,11 +134,13 @@ export async function getWithdrawl (parent, { id }, { me, models, lnd }) { } export function createHmac (hash) { + if (!hash) throw new GqlInputError('hash required to create hmac') const key = Buffer.from(process.env.INVOICE_HMAC_KEY, 'hex') return crypto.createHmac('sha256', key).update(Buffer.from(hash, 'hex')).digest('hex') } export function verifyHmac (hash, hmac) { + if (!hash || !hmac) throw new GqlInputError('hash or hmac missing') const hmac2 = createHmac(hash) if (!timingSafeEqual(Buffer.from(hmac), Buffer.from(hmac2))) { throw new GqlAuthorizationError('bad hmac') @@ -487,8 +489,15 @@ const resolvers = { }, createWithdrawl: createWithdrawal, sendToLnAddr, - cancelInvoice: async (parent, { hash, hmac }, { models, lnd, boss }) => { - verifyHmac(hash, hmac) + cancelInvoice: async (parent, { hash, hmac }, { me, models, lnd, boss }) => { + // stackers can cancel their own invoices without hmac + if (me && !hmac) { + const inv = await models.invoice.findUnique({ where: { hash } }) + if (!inv) throw new GqlInputError('invoice not found') + if (inv.userId !== me.id) throw new GqlInputError('not ur invoice') + } else { + verifyHmac(hash, hmac) + } await finalizeHodlInvoice({ data: { hash }, lnd, models, boss }) return await models.invoice.findFirst({ where: { hash } }) }, diff --git a/api/typeDefs/wallet.js b/api/typeDefs/wallet.js index 932b67bcd..e0da47f1e 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(hash: String!, hmac: String): Invoice! dropBolt11(hash: String!): Boolean removeWallet(id: ID!): Boolean deleteWalletLogs(wallet: String): Boolean diff --git a/components/use-invoice.js b/components/use-invoice.js index cfdb2c6dd..fad49b0a4 100644 --- a/components/use-invoice.js +++ b/components/use-invoice.js @@ -37,10 +37,6 @@ export default function useInvoice () { }, [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 } }) return data.cancelInvoice diff --git a/fragments/wallet.js b/fragments/wallet.js index 6f84f4afd..176575dd2 100644 --- a/fragments/wallet.js +++ b/fragments/wallet.js @@ -225,7 +225,7 @@ export const SET_WALLET_PRIORITY = gql` export const CANCEL_INVOICE = gql` ${INVOICE_FIELDS} - mutation cancelInvoice($hash: String!, $hmac: String!) { + mutation cancelInvoice($hash: String!, $hmac: String) { cancelInvoice(hash: $hash, hmac: $hmac) { ...InvoiceFields } From d53bc09773f2dc736aa80d18e80fb4ceef2dd377 Mon Sep 17 00:00:00 2001 From: ekzyis Date: Thu, 2 Jan 2025 18:53:05 +0100 Subject: [PATCH 2/2] Distinguish invoices cancelled by user (#1785) --- api/resolvers/wallet.js | 4 +-- api/typeDefs/wallet.js | 2 +- components/use-invoice.js | 4 +-- components/use-qr-payment.js | 2 +- fragments/wallet.js | 4 +-- .../migration.sql | 28 +++++++++++++++++++ prisma/schema.prisma | 1 + 7 files changed, 37 insertions(+), 8 deletions(-) create mode 100644 prisma/migrations/20241231223214_invoice_user_cancel/migration.sql diff --git a/api/resolvers/wallet.js b/api/resolvers/wallet.js index 24f5e301b..7e305eec9 100644 --- a/api/resolvers/wallet.js +++ b/api/resolvers/wallet.js @@ -489,7 +489,7 @@ const resolvers = { }, createWithdrawl: createWithdrawal, sendToLnAddr, - cancelInvoice: async (parent, { hash, hmac }, { me, models, lnd, boss }) => { + cancelInvoice: async (parent, { hash, hmac, userCancel }, { me, models, lnd, boss }) => { // stackers can cancel their own invoices without hmac if (me && !hmac) { const inv = await models.invoice.findUnique({ where: { hash } }) @@ -499,7 +499,7 @@ const resolvers = { verifyHmac(hash, hmac) } await finalizeHodlInvoice({ data: { hash }, lnd, models, boss }) - return await models.invoice.findFirst({ where: { hash } }) + return await models.invoice.update({ where: { hash }, data: { userCancel: !!userCancel } }) }, dropBolt11: async (parent, { hash }, { me, models, lnd }) => { if (!me) { diff --git a/api/typeDefs/wallet.js b/api/typeDefs/wallet.js index e0da47f1e..01b12bff2 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(hash: String!, hmac: String, userCancel: Boolean): Invoice! dropBolt11(hash: String!): Boolean removeWallet(id: ID!): Boolean deleteWalletLogs(wallet: String): Boolean diff --git a/components/use-invoice.js b/components/use-invoice.js index fad49b0a4..1cbe94dfd 100644 --- a/components/use-invoice.js +++ b/components/use-invoice.js @@ -36,9 +36,9 @@ export default function useInvoice () { return { invoice: data.invoice, check: that(data.invoice) } }, [client]) - const cancel = useCallback(async ({ hash, hmac }) => { + const cancel = useCallback(async ({ hash, hmac }, { userCancel = false } = {}) => { console.log('canceling invoice:', hash) - const { data } = await cancelInvoice({ variables: { hash, hmac } }) + const { data } = await cancelInvoice({ variables: { hash, hmac, userCancel } }) return data.cancelInvoice }, [cancelInvoice]) diff --git a/components/use-qr-payment.js b/components/use-qr-payment.js index 8d2167d90..dddc53e9c 100644 --- a/components/use-qr-payment.js +++ b/components/use-qr-payment.js @@ -20,7 +20,7 @@ export default function useQrPayment () { let paid const cancelAndReject = async (onClose) => { if (!paid && cancelOnClose) { - const updatedInv = await invoice.cancel(inv) + const updatedInv = await invoice.cancel(inv, { userCancel: true }) reject(new InvoiceCanceledError(updatedInv)) } resolve(inv) diff --git a/fragments/wallet.js b/fragments/wallet.js index 176575dd2..f75d6547e 100644 --- a/fragments/wallet.js +++ b/fragments/wallet.js @@ -225,8 +225,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($hash: String!, $hmac: String, $userCancel: Boolean) { + cancelInvoice(hash: $hash, hmac: $hmac, userCancel: $userCancel) { ...InvoiceFields } } diff --git a/prisma/migrations/20241231223214_invoice_user_cancel/migration.sql b/prisma/migrations/20241231223214_invoice_user_cancel/migration.sql new file mode 100644 index 000000000..950caaa3f --- /dev/null +++ b/prisma/migrations/20241231223214_invoice_user_cancel/migration.sql @@ -0,0 +1,28 @@ +-- AlterTable +ALTER TABLE "Invoice" ADD COLUMN "userCancel" BOOLEAN; + +-- Migrate existing rows +UPDATE "Invoice" SET "userCancel" = false; + +-- Add constraint to ensure consistent cancel state +ALTER TABLE "Invoice" ADD CONSTRAINT "Invoice_cancel" CHECK ( + ("cancelled" = true AND "cancelledAt" IS NOT NULL AND "userCancel" IS NOT NULL) OR + ("cancelled" = false AND "cancelledAt" IS NULL AND "userCancel" IS NULL) +); + +-- Add trigger to set userCancel to false by default when cancelled updated and userCancel not specified +CREATE OR REPLACE FUNCTION invoice_set_user_cancel_default() +RETURNS TRIGGER AS $$ +BEGIN + IF NEW.cancelled AND NEW."userCancel" IS NULL THEN + NEW."userCancel" := false; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER invoice_user_cancel_trigger + BEFORE UPDATE ON "Invoice" + FOR EACH ROW + EXECUTE FUNCTION invoice_set_user_cancel_default(); + diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 59685b931..dc38f489e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -917,6 +917,7 @@ model Invoice { confirmedIndex BigInt? cancelled Boolean @default(false) cancelledAt DateTime? + userCancel Boolean? msatsRequested BigInt msatsReceived BigInt? desc String?