Support JIT invoicing on donations to rewards pool (#515)

* Support JIT invoicing on donations to rewards pool

Now you can just-in-time fund your account to donate to SN's reward pool.

You can also donate without an account via the @anon account, also using
JIT invoices.

* Ensure donate amount is numeric

* Explicit error checking for hash being required for invoice validation

* let donation exceptions bubble for jit funding

---------

Co-authored-by: keyan <keyan.kousha+huumn@gmail.com>
This commit is contained in:
SatsAllDay 2023-09-24 15:38:37 -04:00 committed by GitHub
parent 9f333518c1
commit 9c774d596b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 50 additions and 21 deletions

View File

@ -38,7 +38,10 @@ export async function commentFilterClause (me, models) {
return clause return clause
} }
async function checkInvoice (models, hash, hmac, fee) { export async function checkInvoice (models, hash, hmac, fee) {
if (!hash) {
throw new GraphQLError('hash required', { extensions: { code: 'BAD_INPUT' } })
}
if (!hmac) { if (!hmac) {
throw new GraphQLError('hmac required', { extensions: { code: 'BAD_INPUT' } }) throw new GraphQLError('hmac required', { extensions: { code: 'BAD_INPUT' } })
} }

View File

@ -1,8 +1,9 @@
import { GraphQLError } from 'graphql' import { GraphQLError } from 'graphql'
import { settleHodlInvoice } from 'ln-service'
import { amountSchema, ssValidate } from '../../lib/validate' import { amountSchema, ssValidate } from '../../lib/validate'
import serialize from './serial' import serialize from './serial'
import { ANON_USER_ID } from '../../lib/constants' import { ANON_USER_ID } from '../../lib/constants'
import { getItem } from './item' import { getItem, checkInvoice } from './item'
export default { export default {
Query: { Query: {
@ -102,17 +103,38 @@ export default {
} }
}, },
Mutation: { Mutation: {
donateToRewards: async (parent, { sats }, { me, models }) => { donateToRewards: async (parent, { sats, hash, hmac }, { me, models, lnd }) => {
if (!me) { if (!me && !hash) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } }) throw new GraphQLError('you must be logged in or pay', { extensions: { code: 'FORBIDDEN' } })
}
let user
if (me) {
user = me
}
let invoice
if (hash) {
invoice = await checkInvoice(models, hash, hmac, sats)
user = invoice.user
} }
await ssValidate(amountSchema, { amount: sats }) await ssValidate(amountSchema, { amount: sats })
await serialize(models, const trx = [
models.$queryRawUnsafe( models.$queryRawUnsafe(
'SELECT donate($1::INTEGER, $2::INTEGER)', 'SELECT donate($1::INTEGER, $2::INTEGER)',
sats, Number(me.id))) sats, Number(user.id))
]
if (invoice) {
trx.unshift(models.$queryRaw`UPDATE users SET msats = msats + ${invoice.msatsReceived} WHERE id = ${user.id}`)
trx.push(models.invoice.update({ where: { hash: invoice.hash }, data: { confirmedAt: new Date() } }))
}
await serialize(models, ...trx)
if (invoice?.isHeld) {
await settleHodlInvoice({ secret: invoice.preimage, lnd })
}
return sats return sats
} }

View File

@ -7,7 +7,7 @@ export default gql`
} }
extend type Mutation { extend type Mutation {
donateToRewards(sats: Int!): Int! donateToRewards(sats: Int!, hash: String, hmac: String): Int!
} }
type Rewards { type Rewards {

View File

@ -92,8 +92,8 @@ export function DonateButton () {
const toaster = useToast() const toaster = useToast()
const [donateToRewards] = useMutation( const [donateToRewards] = useMutation(
gql` gql`
mutation donateToRewards($sats: Int!) { mutation donateToRewards($sats: Int!, $hash: String, $hmac: String) {
donateToRewards(sats: $sats) donateToRewards(sats: $sats, hash: $hash, hmac: $hmac)
}`) }`)
return ( return (
@ -104,24 +104,28 @@ export function DonateButton () {
amount: 1000 amount: 1000
}} }}
schema={amountSchema} schema={amountSchema}
onSubmit={async ({ amount }) => { invoiceable
try { onSubmit={async ({ amount, hash, hmac }) => {
await donateToRewards({ const { error } = await donateToRewards({
variables: { variables: {
sats: Number(amount) sats: Number(amount),
hash,
hmac
} }
}) })
onClose() if (error) {
toaster.success('donated') console.error(error)
} catch (err) {
console.error(err)
toaster.danger('failed to donate') toaster.danger('failed to donate')
} else {
toaster.success('donated')
} }
onClose()
}} }}
> >
<Input <Input
label='amount' label='amount'
name='amount' name='amount'
type='number'
required required
autoFocus autoFocus
append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>} append={<InputGroup.Text className='text-monospace'>sats</InputGroup.Text>}