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:
		
							parent
							
								
									5b14606b39
								
							
						
					
					
						commit
						d86d8b3bac
					
				@ -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 }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -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!
 | 
				
			||||||
 | 
				
			|||||||
@ -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!
 | 
				
			||||||
 | 
				
			|||||||
@ -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}
 | 
				
			||||||
          </>
 | 
					          </>
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
      />
 | 
					      />
 | 
				
			||||||
 | 
				
			|||||||
@ -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()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -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) {
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
 | 
				
			|||||||
@ -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) {
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
 | 
				
			|||||||
@ -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>
 | 
				
			||||||
 | 
				
			|||||||
@ -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'
 | 
				
			||||||
 | 
				
			|||||||
@ -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>
 | 
				
			||||||
 | 
					  )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -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();
 | 
				
			||||||
@ -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
 | 
				
			||||||
 | 
				
			|||||||
@ -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))
 | 
				
			||||||
 | 
				
			|||||||
@ -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)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user