diff --git a/api/resolvers/wallet.js b/api/resolvers/wallet.js
index 41d6867f..89f4ee9c 100644
--- a/api/resolvers/wallet.js
+++ b/api/resolvers/wallet.js
@@ -80,6 +80,18 @@ export default {
return wdrwl
},
+ numBolt11s: async (parent, args, { me, models, lnd }) => {
+ if (!me) {
+ throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
+ }
+
+ return await models.withdrawl.count({
+ where: {
+ userId: me.id,
+ hash: { not: null }
+ }
+ })
+ },
connectAddress: async (parent, args, { lnd }) => {
return process.env.LND_CONNECT_ADDRESS
},
@@ -359,6 +371,23 @@ export default {
}
}))
return inv
+ },
+ dropBolt11: async (parent, { id }, { me, models }) => {
+ if (!me) {
+ throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } })
+ }
+
+ await models.withdrawl.update({
+ where: {
+ userId: me.id,
+ id: Number(id),
+ createdAt: {
+ lte: datePivot(new Date(), { days: -7 })
+ }
+ },
+ data: { bolt11: null, hash: null }
+ })
+ return { id }
}
},
diff --git a/api/typeDefs/user.js b/api/typeDefs/user.js
index 48738475..9880058e 100644
--- a/api/typeDefs/user.js
+++ b/api/typeDefs/user.js
@@ -22,7 +22,7 @@ export default gql`
setName(name: String!): String
setSettings(tipDefault: Int!, turboTipping: Boolean!, fiatCurrency: String!, withdrawMaxFeeDefault: Int!, noteItemSats: Boolean!,
noteEarning: Boolean!, noteAllDescendants: Boolean!, noteMentions: Boolean!, noteDeposits: Boolean!,
- noteInvites: Boolean!, noteJobIndicator: Boolean!, noteCowboyHat: Boolean!, hideInvoiceDesc: Boolean!,
+ noteInvites: Boolean!, noteJobIndicator: Boolean!, noteCowboyHat: Boolean!, hideInvoiceDesc: Boolean!, autoDropBolt11s: Boolean!,
hideFromTopUsers: Boolean!, hideCowboyHat: Boolean!, imgproxyOnly: Boolean!,
wildWestMode: Boolean!, greeterMode: Boolean!, nostrPubkey: String, nostrCrossposting: Boolean, nostrRelays: [String!], hideBookmarks: Boolean!,
noteForwardedSats: Boolean!, hideWalletBalance: Boolean!, hideIsContributor: Boolean!, diagnostics: Boolean!): User
@@ -97,6 +97,7 @@ export default gql`
noteCowboyHat: Boolean!
noteForwardedSats: Boolean!
hideInvoiceDesc: Boolean!
+ autoDropBolt11s: Boolean!
hideFromTopUsers: Boolean!
hideCowboyHat: Boolean!
hideBookmarks: Boolean!
diff --git a/api/typeDefs/wallet.js b/api/typeDefs/wallet.js
index e7fa200f..137c61b8 100644
--- a/api/typeDefs/wallet.js
+++ b/api/typeDefs/wallet.js
@@ -4,6 +4,7 @@ export default gql`
extend type Query {
invoice(id: ID!): Invoice!
withdrawl(id: ID!): Withdrawl!
+ numBolt11s: Int!
connectAddress: String!
walletHistory(cursor: String, inc: String): History
}
@@ -13,6 +14,7 @@ export default gql`
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!
+ dropBolt11(id: ID): Withdrawl
}
type Invoice {
@@ -36,8 +38,8 @@ export default gql`
type Withdrawl {
id: ID!
createdAt: Date!
- hash: String!
- bolt11: String!
+ hash: String
+ bolt11: String
satsPaying: Int!
satsPaid: Int
satsFeePaying: Int!
diff --git a/components/bolt11-info.js b/components/bolt11-info.js
index 4867572e..bdfa7d5f 100644
--- a/components/bolt11-info.js
+++ b/components/bolt11-info.js
@@ -2,8 +2,11 @@ import { decode } from 'bolt11'
import AccordianItem from './accordian-item'
import { CopyInput } from './form'
-export default ({ bolt11, preimage }) => {
- const { tagsObject: { description, payment_hash: paymentHash } } = decode(bolt11)
+export default ({ bolt11, preimage, children }) => {
+ let description, paymentHash
+ if (bolt11) {
+ ({ tagsObject: { description, payment_hash: paymentHash } } = decode(bolt11))
+ }
if (!description && !paymentHash && !preimage) {
return null
}
@@ -41,6 +44,7 @@ export default ({ bolt11, preimage }) => {
noForm
placeholder={preimage}
/>}
+ {children}
>
}
/>
diff --git a/components/delete.js b/components/delete.js
index 3df30089..463c0d04 100644
--- a/components/delete.js
+++ b/components/delete.js
@@ -61,7 +61,7 @@ export default function Delete ({ itemId, children, onDelete, type = 'post' }) {
)
}
-function DeleteConfirm ({ onConfirm, type }) {
+export function DeleteConfirm ({ onConfirm, type }) {
const [error, setError] = useState()
const toaster = useToast()
diff --git a/fragments/users.js b/fragments/users.js
index cff94edc..6033bf9d 100644
--- a/fragments/users.js
+++ b/fragments/users.js
@@ -30,6 +30,7 @@ export const ME = gql`
noteCowboyHat
noteForwardedSats
hideInvoiceDesc
+ autoDropBolt11s
hideFromTopUsers
hideCowboyHat
imgproxyOnly
@@ -60,6 +61,7 @@ export const SETTINGS_FIELDS = gql`
noteCowboyHat
noteForwardedSats
hideInvoiceDesc
+ autoDropBolt11s
hideFromTopUsers
hideCowboyHat
hideBookmarks
@@ -94,14 +96,14 @@ gql`
${SETTINGS_FIELDS}
mutation setSettings($tipDefault: Int!, $turboTipping: Boolean!, $fiatCurrency: String!, $withdrawMaxFeeDefault: Int!, $noteItemSats: Boolean!,
$noteEarning: Boolean!, $noteAllDescendants: Boolean!, $noteMentions: Boolean!, $noteDeposits: Boolean!,
- $noteInvites: Boolean!, $noteJobIndicator: Boolean!, $noteCowboyHat: Boolean!, $hideInvoiceDesc: Boolean!,
+ $noteInvites: Boolean!, $noteJobIndicator: Boolean!, $noteCowboyHat: Boolean!, $hideInvoiceDesc: Boolean!, $autoDropBolt11s: Boolean!,
$hideFromTopUsers: Boolean!, $hideCowboyHat: Boolean!, $imgproxyOnly: Boolean!,
$wildWestMode: Boolean!, $greeterMode: Boolean!, $nostrPubkey: String, $nostrCrossposting: Boolean!, $nostrRelays: [String!], $hideBookmarks: Boolean!,
$noteForwardedSats: Boolean!, $hideWalletBalance: Boolean!, $hideIsContributor: Boolean!, $diagnostics: Boolean!) {
setSettings(tipDefault: $tipDefault, turboTipping: $turboTipping, fiatCurrency: $fiatCurrency, withdrawMaxFeeDefault: $withdrawMaxFeeDefault,
noteItemSats: $noteItemSats, noteEarning: $noteEarning, noteAllDescendants: $noteAllDescendants,
noteMentions: $noteMentions, noteDeposits: $noteDeposits, noteInvites: $noteInvites,
- noteJobIndicator: $noteJobIndicator, noteCowboyHat: $noteCowboyHat, hideInvoiceDesc: $hideInvoiceDesc,
+ noteJobIndicator: $noteJobIndicator, noteCowboyHat: $noteCowboyHat, hideInvoiceDesc: $hideInvoiceDesc, autoDropBolt11s: $autoDropBolt11s,
hideFromTopUsers: $hideFromTopUsers, hideCowboyHat: $hideCowboyHat, imgproxyOnly: $imgproxyOnly,
wildWestMode: $wildWestMode, greeterMode: $greeterMode, nostrPubkey: $nostrPubkey, nostrCrossposting: $nostrCrossposting, nostrRelays: $nostrRelays, hideBookmarks: $hideBookmarks,
noteForwardedSats: $noteForwardedSats, hideWalletBalance: $hideWalletBalance, hideIsContributor: $hideIsContributor, diagnostics: $diagnostics) {
diff --git a/fragments/wallet.js b/fragments/wallet.js
index 477a6803..4956e610 100644
--- a/fragments/wallet.js
+++ b/fragments/wallet.js
@@ -24,6 +24,7 @@ export const WITHDRAWL = gql`
query Withdrawl($id: ID!) {
withdrawl(id: $id) {
id
+ createdAt
bolt11
satsPaid
satsFeePaying
@@ -40,6 +41,7 @@ export const WALLET_HISTORY = gql`
facts {
id
factId
+ bolt11
type
createdAt
sats
diff --git a/lib/apollo.js b/lib/apollo.js
index 59d0b9e6..1cdeec4c 100644
--- a/lib/apollo.js
+++ b/lib/apollo.js
@@ -127,6 +127,12 @@ function getClient (uri) {
}
}
},
+ numBolt11s: {
+ keyArgs: [],
+ merge (existing, incoming) {
+ return incoming
+ }
+ },
walletHistory: {
keyArgs: ['inc'],
merge (existing, incoming) {
diff --git a/lib/constants.js b/lib/constants.js
index f006a947..a6a1dec3 100644
--- a/lib/constants.js
+++ b/lib/constants.js
@@ -79,3 +79,5 @@ export const ITEM_ALLOW_EDITS = [
// FAQ, privacy policy, changelog, content guidelines
349, 76894, 78763, 81862
]
+
+export const INVOICE_RETENTION_DAYS = 7
diff --git a/pages/satistics.js b/pages/satistics.js
index a260728f..e2a57fc2 100644
--- a/pages/satistics.js
+++ b/pages/satistics.js
@@ -115,11 +115,12 @@ function Detail ({ fact }) {
return (
- {(zap &&
nostr zap{zap.content && `: ${zap.content}`}) ||
+ {(!fact.bolt11 &&
invoice deleted) ||
+ (zap &&
nostr zap{zap.content && `: ${zap.content}`}) ||
(fact.description &&
{fact.description})}
{fact.invoiceComment &&
sender says: {fact.invoiceComment}}
- {!fact.invoiceComment && !fact.description &&
no description}
+ {!fact.invoiceComment && !fact.description && fact.bolt11 &&
no description}
diff --git a/pages/settings.js b/pages/settings.js
index 2c85ee26..46fe14fd 100644
--- a/pages/settings.js
+++ b/pages/settings.js
@@ -25,6 +25,7 @@ import { NostrAuth } from '../components/nostr-auth'
import { useToast } from '../components/toast'
import { useLogger } from '../components/logger'
import { useMe } from '../components/me'
+import { INVOICE_RETENTION_DAYS } from '../lib/constants'
export const getServerSideProps = getGetServerSideProps({ query: SETTINGS, authRequired: true })
@@ -74,6 +75,7 @@ export default function Settings ({ ssrData }) {
noteCowboyHat: settings?.noteCowboyHat,
noteForwardedSats: settings?.noteForwardedSats,
hideInvoiceDesc: settings?.hideInvoiceDesc,
+ autoDropBolt11s: settings?.autoDropBolt11s,
hideFromTopUsers: settings?.hideFromTopUsers,
hideCowboyHat: settings?.hideCowboyHat,
imgproxyOnly: settings?.imgproxyOnly,
@@ -236,6 +238,23 @@ export default function Settings ({ ssrData }) {
name='hideInvoiceDesc'
groupClassName='mb-0'
/>
+ autodelete withdrawal invoices
+
+
+ - use this to protect receiver privacy
+ - applies retroactively, cannot be reversed
+ - withdrawal invoices are kept at least {INVOICE_RETENTION_DAYS} days for security and debugging purposes
+ - autodeletions are run a daily basis at night
+
+
+
+ }
+ name='autoDropBolt11s'
+ groupClassName='mb-0'
+ />
hide me from top stackers>}
name='hideFromTopUsers'
@@ -377,6 +396,38 @@ export default function Settings ({ ssrData }) {
)
}
+const DropBolt11sCheckbox = ({ ssrData, ...props }) => {
+ const showModal = useShowModal()
+ const { data } = useQuery(gql`{ numBolt11s }`)
+ const { numBolt11s } = data || ssrData
+
+ return (
+ {
+ if (e.target.checked) {
+ showModal(onClose => {
+ return (
+ <>
+ {numBolt11s} withdrawal invoices will be deleted with this setting.
+ You sure? This is a gone forever kind of delete.
+
+
+
+ >
+ )
+ })
+ }
+ }}
+ {...props}
+ />
+ )
+}
+
function QRLinkButton ({ provider, unlink, status }) {
const showModal = useShowModal()
const text = status ? 'Unlink' : 'Link'
diff --git a/pages/withdrawals/[id].js b/pages/withdrawals/[id].js
index 2b259c9e..9c4d0bde 100644
--- a/pages/withdrawals/[id].js
+++ b/pages/withdrawals/[id].js
@@ -1,4 +1,4 @@
-import { useQuery } from '@apollo/client'
+import { useQuery, useMutation } from '@apollo/client'
import { CenterLayout } from '../../components/layout'
import { CopyInput, Input, InputSkeleton } from '../../components/form'
import InputGroup from 'react-bootstrap/InputGroup'
@@ -6,9 +6,15 @@ import InvoiceStatus from '../../components/invoice-status'
import { useRouter } from 'next/router'
import { WITHDRAWL } from '../../fragments/wallet'
import Link from 'next/link'
-import { SSR } from '../../lib/constants'
+import { SSR, INVOICE_RETENTION_DAYS } from '../../lib/constants'
import { numWithUnits } from '../../lib/format'
import Bolt11Info from '../../components/bolt11-info'
+import { datePivot, timeLeft } from '../../lib/time'
+import { useMe } from '../../components/me'
+import { useToast } from '../../components/toast'
+import { gql } from 'graphql-tag'
+import { useShowModal } from '../../components/modal'
+import { DeleteConfirm } from '../../components/delete'
export default function Withdrawl () {
return (
@@ -87,7 +93,7 @@ function LoadWithdrawl () {
@@ -98,7 +104,70 @@ function LoadWithdrawl () {
/>
-
+
+
+
>
)
}
+
+function PrivacyOption ({ wd }) {
+ if (!wd.bolt11) return
+
+ const me = useMe()
+ const keepUntil = datePivot(new Date(wd.createdAt), { days: INVOICE_RETENTION_DAYS })
+ const oldEnough = new Date() >= keepUntil
+ if (!oldEnough) {
+ return (
+
+ {`this invoice ${me.autoDropBolt11s ? 'will be auto-deleted' : 'can be deleted'} in ${timeLeft(keepUntil)}`}
+
+ )
+ }
+
+ const showModal = useShowModal()
+ const toaster = useToast()
+ const [dropBolt11] = useMutation(
+ gql`
+ mutation dropBolt11($id: ID!) {
+ dropBolt11(id: $id) {
+ id
+ }
+ }`, {
+ update (cache) {
+ cache.modify({
+ id: `Withdrawl:${wd.id}`,
+ fields: {
+ bolt11: () => null,
+ hash: () => null
+ }
+ })
+ }
+ })
+
+ return (
+ {
+ showModal(onClose => {
+ return (
+ {
+ if (me) {
+ try {
+ await dropBolt11({ variables: { id: wd.id } })
+ } catch (err) {
+ console.error(err)
+ toaster.danger('unable to delete invoice')
+ }
+ }
+ onClose()
+ }}
+ />
+ )
+ })
+ }}
+ >delete invoice
+
+ )
+}
diff --git a/prisma/migrations/20231026142112_auto_drop_bolt11s/migration.sql b/prisma/migrations/20231026142112_auto_drop_bolt11s/migration.sql
new file mode 100644
index 00000000..69b4ca94
--- /dev/null
+++ b/prisma/migrations/20231026142112_auto_drop_bolt11s/migration.sql
@@ -0,0 +1,23 @@
+-- AlterTable
+ALTER TABLE "users" ADD COLUMN "autoDropBolt11s" BOOLEAN NOT NULL DEFAULT false;
+ALTER TABLE "Withdrawl" ALTER COLUMN "hash" DROP NOT NULL;
+ALTER TABLE "Withdrawl" ALTER COLUMN "bolt11" DROP NOT NULL;
+
+-- hack ... prisma doesn't know about our other schemas (e.g. pgboss)
+-- and this is only really a problem on their "shadow database"
+-- so we catch the exception it throws and ignore it
+CREATE OR REPLACE FUNCTION create_autodrop_bolt11s_job()
+RETURNS INTEGER
+LANGUAGE plpgsql
+AS $$
+DECLARE
+BEGIN
+ INSERT INTO pgboss.schedule (name, cron, timezone) VALUES ('autoDropBolt11s', '1 1 * * *', 'America/Chicago') ON CONFLICT DO NOTHING;
+ return 0;
+EXCEPTION WHEN OTHERS THEN
+ return 0;
+END;
+$$;
+
+SELECT create_autodrop_bolt11s_job();
+DROP FUNCTION create_autodrop_bolt11s_job();
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 0cc13fc0..ab2babd7 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -50,6 +50,7 @@ model User {
greeterMode Boolean @default(false)
fiatCurrency String @default("USD")
withdrawMaxFeeDefault Int @default(10)
+ autoDropBolt11s Boolean @default(false)
hideFromTopUsers Boolean @default(false)
turboTipping Boolean @default(false)
imgproxyOnly Boolean @default(false)
@@ -493,8 +494,8 @@ model Withdrawl {
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
userId Int
- hash String
- bolt11 String
+ hash String?
+ bolt11 String?
msatsPaying BigInt
msatsPaid BigInt?
msatsFeePaying BigInt
diff --git a/worker/index.js b/worker/index.js
index 401ed46f..94491136 100644
--- a/worker/index.js
+++ b/worker/index.js
@@ -1,7 +1,7 @@
import PgBoss from 'pg-boss'
import nextEnv from '@next/env'
import { PrismaClient } from '@prisma/client'
-import { checkInvoice, checkWithdrawal } from './wallet.js'
+import { checkInvoice, checkWithdrawal, autoDropBolt11s } from './wallet.js'
import { repin } from './repin.js'
import { trust } from './trust.js'
import { auction } from './auction.js'
@@ -57,6 +57,7 @@ async function work () {
await boss.start()
await boss.work('checkInvoice', checkInvoice(args))
await boss.work('checkWithdrawal', checkWithdrawal(args))
+ await boss.work('autoDropBolt11s', autoDropBolt11s(args))
await boss.work('repin-*', repin(args))
await boss.work('trust', trust(args))
await boss.work('timestampItem', timestampItem(args))
diff --git a/worker/wallet.js b/worker/wallet.js
index dd9c14c7..8492290d 100644
--- a/worker/wallet.js
+++ b/worker/wallet.js
@@ -3,6 +3,7 @@ import { getInvoice, getPayment, cancelHodlInvoice } from 'ln-service'
import { datePivot } from '../lib/time.js'
import { sendUserNotification } from '../api/webPush/index.js'
import { msatsToSats, numWithUnits } from '../lib/format'
+import { INVOICE_RETENTION_DAYS } from '../lib/constants'
const walletOptions = { startAfter: 5, retryLimit: 21, retryBackoff: true }
@@ -118,3 +119,20 @@ export function checkWithdrawal ({ boss, models, lnd }) {
}
}
}
+
+export function autoDropBolt11s ({ models }) {
+ return async function () {
+ console.log('deleting invoices')
+ try {
+ await serialize(models, models.$executeRaw`
+ UPDATE "Withdrawl"
+ SET hash = NULL, bolt11 = NULL
+ WHERE "userId" IN (SELECT id FROM users WHERE "autoDropBolt11s")
+ AND now() > created_at + interval '${INVOICE_RETENTION_DAYS} days'
+ AND hash IS NOT NULL;`
+ )
+ } catch (err) {
+ console.log(err)
+ }
+ }
+}