Provide option to clear withdrawal invoices (#591)

* add settings option

* add auto-drop worker

* add manual delete option

* add warning and note

* cleanup

* incorporate most review feedback

* add warning to settings option

* remove debugging tweaks and simplify

* refine auto delete bolt11s

* refine UI

---------

Co-authored-by: rleed <rleed1@pm.me>
Co-authored-by: Keyan <34140557+huumn@users.noreply.github.com>
This commit is contained in:
rleed 2023-11-09 14:50:43 -03:00 committed by GitHub
parent 5b14606b39
commit d86d8b3bac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 229 additions and 17 deletions

View File

@ -80,6 +80,18 @@ export default {
return wdrwl 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 }) => { connectAddress: async (parent, args, { lnd }) => {
return process.env.LND_CONNECT_ADDRESS return process.env.LND_CONNECT_ADDRESS
}, },
@ -359,6 +371,23 @@ export default {
} }
})) }))
return inv 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 }
} }
}, },

View File

@ -22,7 +22,7 @@ export default gql`
setName(name: String!): String setName(name: String!): String
setSettings(tipDefault: Int!, turboTipping: Boolean!, fiatCurrency: String!, withdrawMaxFeeDefault: Int!, noteItemSats: Boolean!, setSettings(tipDefault: Int!, turboTipping: Boolean!, fiatCurrency: String!, withdrawMaxFeeDefault: Int!, noteItemSats: Boolean!,
noteEarning: Boolean!, noteAllDescendants: Boolean!, noteMentions: Boolean!, noteDeposits: 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!, hideFromTopUsers: Boolean!, hideCowboyHat: Boolean!, imgproxyOnly: Boolean!,
wildWestMode: Boolean!, greeterMode: Boolean!, nostrPubkey: String, nostrCrossposting: Boolean, nostrRelays: [String!], hideBookmarks: Boolean!, wildWestMode: Boolean!, greeterMode: Boolean!, nostrPubkey: String, nostrCrossposting: Boolean, nostrRelays: [String!], hideBookmarks: Boolean!,
noteForwardedSats: Boolean!, hideWalletBalance: Boolean!, hideIsContributor: Boolean!, diagnostics: Boolean!): User noteForwardedSats: Boolean!, hideWalletBalance: Boolean!, hideIsContributor: Boolean!, diagnostics: Boolean!): User
@ -97,6 +97,7 @@ export default gql`
noteCowboyHat: Boolean! noteCowboyHat: Boolean!
noteForwardedSats: Boolean! noteForwardedSats: Boolean!
hideInvoiceDesc: Boolean! hideInvoiceDesc: Boolean!
autoDropBolt11s: Boolean!
hideFromTopUsers: Boolean! hideFromTopUsers: Boolean!
hideCowboyHat: Boolean! hideCowboyHat: Boolean!
hideBookmarks: Boolean! hideBookmarks: Boolean!

View File

@ -4,6 +4,7 @@ export default gql`
extend type Query { extend type Query {
invoice(id: ID!): Invoice! invoice(id: ID!): Invoice!
withdrawl(id: ID!): Withdrawl! withdrawl(id: ID!): Withdrawl!
numBolt11s: Int!
connectAddress: String! connectAddress: String!
walletHistory(cursor: String, inc: String): History walletHistory(cursor: String, inc: String): History
} }
@ -13,6 +14,7 @@ export default gql`
createWithdrawl(invoice: String!, maxFee: Int!): Withdrawl! createWithdrawl(invoice: String!, maxFee: Int!): Withdrawl!
sendToLnAddr(addr: String!, amount: Int!, maxFee: Int!, comment: String, identifier: Boolean, name: String, email: String): 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(id: ID): Withdrawl
} }
type Invoice { type Invoice {
@ -36,8 +38,8 @@ export default gql`
type Withdrawl { type Withdrawl {
id: ID! id: ID!
createdAt: Date! createdAt: Date!
hash: String! hash: String
bolt11: String! bolt11: String
satsPaying: Int! satsPaying: Int!
satsPaid: Int satsPaid: Int
satsFeePaying: Int! satsFeePaying: Int!

View File

@ -2,8 +2,11 @@ import { decode } from 'bolt11'
import AccordianItem from './accordian-item' import AccordianItem from './accordian-item'
import { CopyInput } from './form' import { CopyInput } from './form'
export default ({ bolt11, preimage }) => { export default ({ bolt11, preimage, children }) => {
const { tagsObject: { description, payment_hash: paymentHash } } = decode(bolt11) let description, paymentHash
if (bolt11) {
({ tagsObject: { description, payment_hash: paymentHash } } = decode(bolt11))
}
if (!description && !paymentHash && !preimage) { if (!description && !paymentHash && !preimage) {
return null return null
} }
@ -41,6 +44,7 @@ export default ({ bolt11, preimage }) => {
noForm noForm
placeholder={preimage} placeholder={preimage}
/>} />}
{children}
</> </>
} }
/> />

View File

@ -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 [error, setError] = useState()
const toaster = useToast() const toaster = useToast()

View File

@ -30,6 +30,7 @@ export const ME = gql`
noteCowboyHat noteCowboyHat
noteForwardedSats noteForwardedSats
hideInvoiceDesc hideInvoiceDesc
autoDropBolt11s
hideFromTopUsers hideFromTopUsers
hideCowboyHat hideCowboyHat
imgproxyOnly imgproxyOnly
@ -60,6 +61,7 @@ export const SETTINGS_FIELDS = gql`
noteCowboyHat noteCowboyHat
noteForwardedSats noteForwardedSats
hideInvoiceDesc hideInvoiceDesc
autoDropBolt11s
hideFromTopUsers hideFromTopUsers
hideCowboyHat hideCowboyHat
hideBookmarks hideBookmarks
@ -94,14 +96,14 @@ gql`
${SETTINGS_FIELDS} ${SETTINGS_FIELDS}
mutation setSettings($tipDefault: Int!, $turboTipping: Boolean!, $fiatCurrency: String!, $withdrawMaxFeeDefault: Int!, $noteItemSats: Boolean!, mutation setSettings($tipDefault: Int!, $turboTipping: Boolean!, $fiatCurrency: String!, $withdrawMaxFeeDefault: Int!, $noteItemSats: Boolean!,
$noteEarning: Boolean!, $noteAllDescendants: Boolean!, $noteMentions: Boolean!, $noteDeposits: 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!, $hideFromTopUsers: Boolean!, $hideCowboyHat: Boolean!, $imgproxyOnly: Boolean!,
$wildWestMode: Boolean!, $greeterMode: Boolean!, $nostrPubkey: String, $nostrCrossposting: Boolean!, $nostrRelays: [String!], $hideBookmarks: Boolean!, $wildWestMode: Boolean!, $greeterMode: Boolean!, $nostrPubkey: String, $nostrCrossposting: Boolean!, $nostrRelays: [String!], $hideBookmarks: Boolean!,
$noteForwardedSats: Boolean!, $hideWalletBalance: Boolean!, $hideIsContributor: Boolean!, $diagnostics: Boolean!) { $noteForwardedSats: Boolean!, $hideWalletBalance: Boolean!, $hideIsContributor: Boolean!, $diagnostics: Boolean!) {
setSettings(tipDefault: $tipDefault, turboTipping: $turboTipping, fiatCurrency: $fiatCurrency, withdrawMaxFeeDefault: $withdrawMaxFeeDefault, setSettings(tipDefault: $tipDefault, turboTipping: $turboTipping, fiatCurrency: $fiatCurrency, withdrawMaxFeeDefault: $withdrawMaxFeeDefault,
noteItemSats: $noteItemSats, noteEarning: $noteEarning, noteAllDescendants: $noteAllDescendants, noteItemSats: $noteItemSats, noteEarning: $noteEarning, noteAllDescendants: $noteAllDescendants,
noteMentions: $noteMentions, noteDeposits: $noteDeposits, noteInvites: $noteInvites, 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, hideFromTopUsers: $hideFromTopUsers, hideCowboyHat: $hideCowboyHat, imgproxyOnly: $imgproxyOnly,
wildWestMode: $wildWestMode, greeterMode: $greeterMode, nostrPubkey: $nostrPubkey, nostrCrossposting: $nostrCrossposting, nostrRelays: $nostrRelays, hideBookmarks: $hideBookmarks, wildWestMode: $wildWestMode, greeterMode: $greeterMode, nostrPubkey: $nostrPubkey, nostrCrossposting: $nostrCrossposting, nostrRelays: $nostrRelays, hideBookmarks: $hideBookmarks,
noteForwardedSats: $noteForwardedSats, hideWalletBalance: $hideWalletBalance, hideIsContributor: $hideIsContributor, diagnostics: $diagnostics) { noteForwardedSats: $noteForwardedSats, hideWalletBalance: $hideWalletBalance, hideIsContributor: $hideIsContributor, diagnostics: $diagnostics) {

View File

@ -24,6 +24,7 @@ export const WITHDRAWL = gql`
query Withdrawl($id: ID!) { query Withdrawl($id: ID!) {
withdrawl(id: $id) { withdrawl(id: $id) {
id id
createdAt
bolt11 bolt11
satsPaid satsPaid
satsFeePaying satsFeePaying
@ -40,6 +41,7 @@ export const WALLET_HISTORY = gql`
facts { facts {
id id
factId factId
bolt11
type type
createdAt createdAt
sats sats

View File

@ -127,6 +127,12 @@ function getClient (uri) {
} }
} }
}, },
numBolt11s: {
keyArgs: [],
merge (existing, incoming) {
return incoming
}
},
walletHistory: { walletHistory: {
keyArgs: ['inc'], keyArgs: ['inc'],
merge (existing, incoming) { merge (existing, incoming) {

View File

@ -79,3 +79,5 @@ export const ITEM_ALLOW_EDITS = [
// FAQ, privacy policy, changelog, content guidelines // FAQ, privacy policy, changelog, content guidelines
349, 76894, 78763, 81862 349, 76894, 78763, 81862
] ]
export const INVOICE_RETENTION_DAYS = 7

View File

@ -115,11 +115,12 @@ function Detail ({ fact }) {
return ( return (
<div className='px-3'> <div className='px-3'>
<Link className={satusClass(fact.status)} href={`/${fact.type}s/${fact.factId}`}> <Link className={satusClass(fact.status)} href={`/${fact.type}s/${fact.factId}`}>
{(zap && <span className='d-block'>nostr zap{zap.content && `: ${zap.content}`}</span>) || {(!fact.bolt11 && <span className='d-block text-muted fw-bold fst-italic'>invoice deleted</span>) ||
(zap && <span className='d-block'>nostr zap{zap.content && `: ${zap.content}`}</span>) ||
(fact.description && <span className='d-block'>{fact.description}</span>)} (fact.description && <span className='d-block'>{fact.description}</span>)}
<PayerData data={fact.invoicePayerData} className='text-muted' header /> <PayerData data={fact.invoicePayerData} className='text-muted' header />
{fact.invoiceComment && <small className='text-muted'><b>sender says:</b> {fact.invoiceComment}</small>} {fact.invoiceComment && <small className='text-muted'><b>sender says:</b> {fact.invoiceComment}</small>}
{!fact.invoiceComment && !fact.description && <span className='d-block'>no description</span>} {!fact.invoiceComment && !fact.description && fact.bolt11 && <span className='d-block'>no description</span>}
<Satus status={fact.status} /> <Satus status={fact.status} />
</Link> </Link>
</div> </div>

View File

@ -25,6 +25,7 @@ import { NostrAuth } from '../components/nostr-auth'
import { useToast } from '../components/toast' import { useToast } from '../components/toast'
import { useLogger } from '../components/logger' import { useLogger } from '../components/logger'
import { useMe } from '../components/me' import { useMe } from '../components/me'
import { INVOICE_RETENTION_DAYS } from '../lib/constants'
export const getServerSideProps = getGetServerSideProps({ query: SETTINGS, authRequired: true }) export const getServerSideProps = getGetServerSideProps({ query: SETTINGS, authRequired: true })
@ -74,6 +75,7 @@ export default function Settings ({ ssrData }) {
noteCowboyHat: settings?.noteCowboyHat, noteCowboyHat: settings?.noteCowboyHat,
noteForwardedSats: settings?.noteForwardedSats, noteForwardedSats: settings?.noteForwardedSats,
hideInvoiceDesc: settings?.hideInvoiceDesc, hideInvoiceDesc: settings?.hideInvoiceDesc,
autoDropBolt11s: settings?.autoDropBolt11s,
hideFromTopUsers: settings?.hideFromTopUsers, hideFromTopUsers: settings?.hideFromTopUsers,
hideCowboyHat: settings?.hideCowboyHat, hideCowboyHat: settings?.hideCowboyHat,
imgproxyOnly: settings?.imgproxyOnly, imgproxyOnly: settings?.imgproxyOnly,
@ -236,6 +238,23 @@ export default function Settings ({ ssrData }) {
name='hideInvoiceDesc' name='hideInvoiceDesc'
groupClassName='mb-0' groupClassName='mb-0'
/> />
<DropBolt11sCheckbox
ssrData={ssrData}
label={
<div className='d-flex align-items-center'>autodelete withdrawal invoices
<Info>
<ul className='fw-bold'>
<li>use this to protect receiver privacy</li>
<li>applies retroactively, cannot be reversed</li>
<li>withdrawal invoices are kept at least {INVOICE_RETENTION_DAYS} days for security and debugging purposes</li>
<li>autodeletions are run a daily basis at night</li>
</ul>
</Info>
</div>
}
name='autoDropBolt11s'
groupClassName='mb-0'
/>
<Checkbox <Checkbox
label={<>hide me from <Link href='/top/stackers/day'>top stackers</Link></>} label={<>hide me from <Link href='/top/stackers/day'>top stackers</Link></>}
name='hideFromTopUsers' 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 (
<Checkbox
onClick={e => {
if (e.target.checked) {
showModal(onClose => {
return (
<>
<p className='fw-bolder'>{numBolt11s} withdrawal invoices will be deleted with this setting.</p>
<p className='fw-bolder'>You sure? This is a gone forever kind of delete.</p>
<div className='d-flex justify-content-end'>
<Button
variant='danger' onClick={async () => {
await onClose()
}}
>I am sure
</Button>
</div>
</>
)
})
}
}}
{...props}
/>
)
}
function QRLinkButton ({ provider, unlink, status }) { function QRLinkButton ({ provider, unlink, status }) {
const showModal = useShowModal() const showModal = useShowModal()
const text = status ? 'Unlink' : 'Link' const text = status ? 'Unlink' : 'Link'

View File

@ -1,4 +1,4 @@
import { useQuery } from '@apollo/client' import { useQuery, useMutation } from '@apollo/client'
import { CenterLayout } from '../../components/layout' import { CenterLayout } from '../../components/layout'
import { CopyInput, Input, InputSkeleton } from '../../components/form' import { CopyInput, Input, InputSkeleton } from '../../components/form'
import InputGroup from 'react-bootstrap/InputGroup' import InputGroup from 'react-bootstrap/InputGroup'
@ -6,9 +6,15 @@ import InvoiceStatus from '../../components/invoice-status'
import { useRouter } from 'next/router' import { useRouter } from 'next/router'
import { WITHDRAWL } from '../../fragments/wallet' import { WITHDRAWL } from '../../fragments/wallet'
import Link from 'next/link' import Link from 'next/link'
import { SSR } from '../../lib/constants' import { SSR, INVOICE_RETENTION_DAYS } from '../../lib/constants'
import { numWithUnits } from '../../lib/format' import { numWithUnits } from '../../lib/format'
import Bolt11Info from '../../components/bolt11-info' 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 () { export default function Withdrawl () {
return ( return (
@ -87,7 +93,7 @@ function LoadWithdrawl () {
<div className='w-100'> <div className='w-100'>
<CopyInput <CopyInput
label='invoice' type='text' label='invoice' type='text'
placeholder={data.withdrawl.bolt11} readOnly noForm placeholder={data.withdrawl.bolt11 || 'deleted'} readOnly noForm
/> />
</div> </div>
<div className='w-100'> <div className='w-100'>
@ -98,7 +104,70 @@ function LoadWithdrawl () {
/> />
</div> </div>
<InvoiceStatus variant={variant} status={status} /> <InvoiceStatus variant={variant} status={status} />
<Bolt11Info bolt11={data.withdrawl.bolt11} /> <Bolt11Info bolt11={data.withdrawl.bolt11}>
<PrivacyOption wd={data.withdrawl} />
</Bolt11Info>
</> </>
) )
} }
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 (
<span className='text-muted fst-italic'>
{`this invoice ${me.autoDropBolt11s ? 'will be auto-deleted' : 'can be deleted'} in ${timeLeft(keepUntil)}`}
</span>
)
}
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 (
<span
className='btn btn-md btn-danger' onClick={() => {
showModal(onClose => {
return (
<DeleteConfirm
type='invoice'
onConfirm={async () => {
if (me) {
try {
await dropBolt11({ variables: { id: wd.id } })
} catch (err) {
console.error(err)
toaster.danger('unable to delete invoice')
}
}
onClose()
}}
/>
)
})
}}
>delete invoice
</span>
)
}

View File

@ -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();

View File

@ -50,6 +50,7 @@ model User {
greeterMode Boolean @default(false) greeterMode Boolean @default(false)
fiatCurrency String @default("USD") fiatCurrency String @default("USD")
withdrawMaxFeeDefault Int @default(10) withdrawMaxFeeDefault Int @default(10)
autoDropBolt11s Boolean @default(false)
hideFromTopUsers Boolean @default(false) hideFromTopUsers Boolean @default(false)
turboTipping Boolean @default(false) turboTipping Boolean @default(false)
imgproxyOnly Boolean @default(false) imgproxyOnly Boolean @default(false)
@ -493,8 +494,8 @@ model Withdrawl {
createdAt DateTime @default(now()) @map("created_at") createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @default(now()) @updatedAt @map("updated_at") updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
userId Int userId Int
hash String hash String?
bolt11 String bolt11 String?
msatsPaying BigInt msatsPaying BigInt
msatsPaid BigInt? msatsPaid BigInt?
msatsFeePaying BigInt msatsFeePaying BigInt

View File

@ -1,7 +1,7 @@
import PgBoss from 'pg-boss' import PgBoss from 'pg-boss'
import nextEnv from '@next/env' import nextEnv from '@next/env'
import { PrismaClient } from '@prisma/client' import { PrismaClient } from '@prisma/client'
import { checkInvoice, checkWithdrawal } from './wallet.js' import { checkInvoice, checkWithdrawal, autoDropBolt11s } from './wallet.js'
import { repin } from './repin.js' import { repin } from './repin.js'
import { trust } from './trust.js' import { trust } from './trust.js'
import { auction } from './auction.js' import { auction } from './auction.js'
@ -57,6 +57,7 @@ async function work () {
await boss.start() await boss.start()
await boss.work('checkInvoice', checkInvoice(args)) await boss.work('checkInvoice', checkInvoice(args))
await boss.work('checkWithdrawal', checkWithdrawal(args)) await boss.work('checkWithdrawal', checkWithdrawal(args))
await boss.work('autoDropBolt11s', autoDropBolt11s(args))
await boss.work('repin-*', repin(args)) await boss.work('repin-*', repin(args))
await boss.work('trust', trust(args)) await boss.work('trust', trust(args))
await boss.work('timestampItem', timestampItem(args)) await boss.work('timestampItem', timestampItem(args))

View File

@ -3,6 +3,7 @@ import { getInvoice, getPayment, cancelHodlInvoice } from 'ln-service'
import { datePivot } from '../lib/time.js' import { datePivot } from '../lib/time.js'
import { sendUserNotification } from '../api/webPush/index.js' import { sendUserNotification } from '../api/webPush/index.js'
import { msatsToSats, numWithUnits } from '../lib/format' import { msatsToSats, numWithUnits } from '../lib/format'
import { INVOICE_RETENTION_DAYS } from '../lib/constants'
const walletOptions = { startAfter: 5, retryLimit: 21, retryBackoff: true } 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)
}
}
}