From 9c774d596b46e8933980132f56f9c7a5d1899983 Mon Sep 17 00:00:00 2001 From: SatsAllDay <128755788+SatsAllDay@users.noreply.github.com> Date: Sun, 24 Sep 2023 15:38:37 -0400 Subject: [PATCH] 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 --- api/resolvers/item.js | 5 ++++- api/resolvers/rewards.js | 34 ++++++++++++++++++++++++++++------ api/typeDefs/rewards.js | 2 +- pages/rewards/index.js | 30 +++++++++++++++++------------- 4 files changed, 50 insertions(+), 21 deletions(-) diff --git a/api/resolvers/item.js b/api/resolvers/item.js index 9711cccd..e9786096 100644 --- a/api/resolvers/item.js +++ b/api/resolvers/item.js @@ -38,7 +38,10 @@ export async function commentFilterClause (me, models) { 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) { throw new GraphQLError('hmac required', { extensions: { code: 'BAD_INPUT' } }) } diff --git a/api/resolvers/rewards.js b/api/resolvers/rewards.js index 10a202f3..8d78339f 100644 --- a/api/resolvers/rewards.js +++ b/api/resolvers/rewards.js @@ -1,8 +1,9 @@ import { GraphQLError } from 'graphql' +import { settleHodlInvoice } from 'ln-service' import { amountSchema, ssValidate } from '../../lib/validate' import serialize from './serial' import { ANON_USER_ID } from '../../lib/constants' -import { getItem } from './item' +import { getItem, checkInvoice } from './item' export default { Query: { @@ -102,17 +103,38 @@ export default { } }, Mutation: { - donateToRewards: async (parent, { sats }, { me, models }) => { - if (!me) { - throw new GraphQLError('you must be logged in', { extensions: { code: 'UNAUTHENTICATED' } }) + donateToRewards: async (parent, { sats, hash, hmac }, { me, models, lnd }) => { + if (!me && !hash) { + 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 serialize(models, + const trx = [ models.$queryRawUnsafe( '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 } diff --git a/api/typeDefs/rewards.js b/api/typeDefs/rewards.js index 220ea7bb..90feb30f 100644 --- a/api/typeDefs/rewards.js +++ b/api/typeDefs/rewards.js @@ -7,7 +7,7 @@ export default gql` } extend type Mutation { - donateToRewards(sats: Int!): Int! + donateToRewards(sats: Int!, hash: String, hmac: String): Int! } type Rewards { diff --git a/pages/rewards/index.js b/pages/rewards/index.js index eca4cece..ad5014a0 100644 --- a/pages/rewards/index.js +++ b/pages/rewards/index.js @@ -92,8 +92,8 @@ export function DonateButton () { const toaster = useToast() const [donateToRewards] = useMutation( gql` - mutation donateToRewards($sats: Int!) { - donateToRewards(sats: $sats) + mutation donateToRewards($sats: Int!, $hash: String, $hmac: String) { + donateToRewards(sats: $sats, hash: $hash, hmac: $hmac) }`) return ( @@ -104,24 +104,28 @@ export function DonateButton () { amount: 1000 }} schema={amountSchema} - onSubmit={async ({ amount }) => { - try { - await donateToRewards({ - variables: { - sats: Number(amount) - } - }) - onClose() - toaster.success('donated') - } catch (err) { - console.error(err) + invoiceable + onSubmit={async ({ amount, hash, hmac }) => { + const { error } = await donateToRewards({ + variables: { + sats: Number(amount), + hash, + hmac + } + }) + if (error) { + console.error(error) toaster.danger('failed to donate') + } else { + toaster.success('donated') } + onClose() }} > sats}