Add invoice HMAC

This prevents entities which know the invoice hash (like all LN nodes on the payment path) from using the invoice hash on SN.

Only the user which created the invoice knows the HMAC and thus can use the invoice hash.
This commit is contained in:
ekzyis 2023-08-10 07:10:05 +02:00
parent 4fe1d416de
commit bb2212d51e
14 changed files with 81 additions and 49 deletions

View File

@ -47,6 +47,7 @@ PUBLIC_URL=http://localhost:3000
LND_CONNECT_ADDRESS=03cc1d0932bb99b0697f5b5e5961b83ab7fd66f1efc4c9f5c7bad66c1bcbe78f02@xhlmkj7mfrl6ejnczfwl2vqik3xim6wzmurc2vlyfoqw2sasaocgpuad.onion:9735 LND_CONNECT_ADDRESS=03cc1d0932bb99b0697f5b5e5961b83ab7fd66f1efc4c9f5c7bad66c1bcbe78f02@xhlmkj7mfrl6ejnczfwl2vqik3xim6wzmurc2vlyfoqw2sasaocgpuad.onion:9735
NEXTAUTH_SECRET=3_0W_PhDRZVanbeJsZZGIEljexkKoGbL6qGIqSwTjjI NEXTAUTH_SECRET=3_0W_PhDRZVanbeJsZZGIEljexkKoGbL6qGIqSwTjjI
JWT_SIGNING_PRIVATE_KEY={"kty":"oct","kid":"FvD__hmeKoKHu2fKjUrWbRKfhjimIM4IKshyrJG4KSM","alg":"HS512","k":"3_0W_PhDRZVanbeJsZZGIEljexkKoGbL6qGIqSwTjjI"} JWT_SIGNING_PRIVATE_KEY={"kty":"oct","kid":"FvD__hmeKoKHu2fKjUrWbRKfhjimIM4IKshyrJG4KSM","alg":"HS512","k":"3_0W_PhDRZVanbeJsZZGIEljexkKoGbL6qGIqSwTjjI"}
INVOICE_HMAC_KEY=a4c1d9c81edb87b79d28809876a18cf72293eadb39f92f3f4f2f1cfbdf907c91
# imgproxy # imgproxy
NEXT_PUBLIC_IMGPROXY_URL= NEXT_PUBLIC_IMGPROXY_URL=

View File

@ -17,6 +17,7 @@ import { amountSchema, bountySchema, commentSchema, discussionSchema, jobSchema,
import { sendUserNotification } from '../webPush' import { sendUserNotification } from '../webPush'
import { proxyImages } from './imgproxy' import { proxyImages } from './imgproxy'
import { defaultCommentSort } from '../../lib/item' import { defaultCommentSort } from '../../lib/item'
import { createHmac } from './wallet'
export async function commentFilterClause (me, models) { export async function commentFilterClause (me, models) {
let clause = ` AND ("Item"."weightedVotes" - "Item"."weightedDownVotes" > -${ITEM_FILTER_THRESHOLD}` let clause = ` AND ("Item"."weightedVotes" - "Item"."weightedDownVotes" > -${ITEM_FILTER_THRESHOLD}`
@ -37,9 +38,17 @@ export async function commentFilterClause (me, models) {
return clause return clause
} }
async function checkInvoice (models, invoiceHash, fee) { async function checkInvoice (models, hash, hmac, fee) {
if (!hmac) {
throw new GraphQLError('hmac required', { extensions: { code: 'BAD_INPUT' } })
}
const hmac2 = createHmac(hash)
if (hmac !== hmac2) {
throw new GraphQLError('bad hmac', { extensions: { code: 'FORBIDDEN' } })
}
const invoice = await models.invoice.findUnique({ const invoice = await models.invoice.findUnique({
where: { hash: invoiceHash }, where: { hash },
include: { include: {
user: true user: true
} }
@ -590,7 +599,7 @@ export default {
if (id) { if (id) {
return await updateItem(parent, { id, data }, { me, models }) return await updateItem(parent, { id, data }, { me, models })
} else { } else {
return await createItem(parent, data, { me, models, invoiceHash: args.invoiceHash }) return await createItem(parent, data, { me, models, invoiceHash: args.invoiceHash, invoiceHmac: args.invoiceHmac })
} }
}, },
upsertDiscussion: async (parent, args, { me, models }) => { upsertDiscussion: async (parent, args, { me, models }) => {
@ -601,7 +610,7 @@ export default {
if (id) { if (id) {
return await updateItem(parent, { id, data }, { me, models }) return await updateItem(parent, { id, data }, { me, models })
} else { } else {
return await createItem(parent, data, { me, models, invoiceHash: args.invoiceHash }) return await createItem(parent, data, { me, models, invoiceHash: args.invoiceHash, invoiceHmac: args.invoiceHmac })
} }
}, },
upsertBounty: async (parent, args, { me, models }) => { upsertBounty: async (parent, args, { me, models }) => {
@ -616,11 +625,11 @@ export default {
} }
}, },
upsertPoll: async (parent, { id, ...data }, { me, models }) => { upsertPoll: async (parent, { id, ...data }, { me, models }) => {
const { sub, forward, boost, title, text, options, invoiceHash } = data const { sub, forward, boost, title, text, options, invoiceHash, invoiceHmac } = data
let author = me let author = me
const trx = [] const trx = []
if (!me && invoiceHash) { if (!me && invoiceHash) {
const invoice = await checkInvoice(models, invoiceHash, ANON_POST_FEE) const invoice = await checkInvoice(models, invoiceHash, invoiceHmac, ANON_POST_FEE)
author = invoice.user author = invoice.user
trx.push(models.invoice.delete({ where: { hash: invoiceHash } })) trx.push(models.invoice.delete({ where: { hash: invoiceHash } }))
} }
@ -707,7 +716,7 @@ export default {
}, },
createComment: async (parent, data, { me, models }) => { createComment: async (parent, data, { me, models }) => {
await ssValidate(commentSchema, data) await ssValidate(commentSchema, data)
const item = await createItem(parent, data, { me, models, invoiceHash: data.invoiceHash }) const item = await createItem(parent, data, { me, models, invoiceHash: data.invoiceHash, invoiceHmac: data.invoiceHmac })
// fetch user to get up-to-date name // fetch user to get up-to-date name
const user = await models.user.findUnique({ where: { id: me?.id || ANON_USER_ID } }) const user = await models.user.findUnique({ where: { id: me?.id || ANON_USER_ID } })
@ -740,7 +749,7 @@ export default {
return id return id
}, },
act: async (parent, { id, sats, invoiceHash }, { me, models }) => { act: async (parent, { id, sats, invoiceHash, invoiceHmac }, { me, models }) => {
// need to make sure we are logged in // need to make sure we are logged in
if (!me && !invoiceHash) { if (!me && !invoiceHash) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } }) throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
@ -749,8 +758,9 @@ export default {
await ssValidate(amountSchema, { amount: sats }) await ssValidate(amountSchema, { amount: sats })
let user = me let user = me
let invoice
if (!me && invoiceHash) { if (!me && invoiceHash) {
const invoice = await checkInvoice(models, invoiceHash, sats) invoice = await checkInvoice(models, invoiceHash, invoiceHmac, sats)
user = invoice.user user = invoice.user
} }
@ -766,8 +776,8 @@ export default {
const calls = [ const calls = [
models.$queryRaw`SELECT item_act(${Number(id)}::INTEGER, ${user.id}::INTEGER, 'TIP', ${Number(sats)}::INTEGER)` models.$queryRaw`SELECT item_act(${Number(id)}::INTEGER, ${user.id}::INTEGER, 'TIP', ${Number(sats)}::INTEGER)`
] ]
if (!me && invoiceHash) { if (invoice) {
calls.push(models.invoice.delete({ where: { hash: invoiceHash } })) calls.push(models.invoice.delete({ where: { hash: invoice.hash } }))
} }
const [{ item_act: vote }] = await serialize(models, ...calls) const [{ item_act: vote }] = await serialize(models, ...calls)
@ -1093,11 +1103,11 @@ export const updateItem = async (parent, { id, data: { sub, title, url, text, bo
return item return item
} }
const createItem = async (parent, { sub, title, url, text, boost, forward, bounty, parentId }, { me, models, invoiceHash }) => { const createItem = async (parent, { sub, title, url, text, boost, forward, bounty, parentId }, { me, models, invoiceHash, invoiceHmac }) => {
let author = me let author = me
const trx = [] const trx = []
if (!me && invoiceHash) { if (!me && invoiceHash) {
const invoice = await checkInvoice(models, invoiceHash, parentId ? ANON_COMMENT_FEE : ANON_POST_FEE) const invoice = await checkInvoice(models, invoiceHash, invoiceHmac, parentId ? ANON_COMMENT_FEE : ANON_POST_FEE)
author = invoice.user author = invoice.user
trx.push(models.invoice.delete({ where: { hash: invoiceHash } })) trx.push(models.invoice.delete({ where: { hash: invoiceHash } }))
} }

View File

@ -1,5 +1,6 @@
import { createInvoice, decodePaymentRequest, payViaPaymentRequest } from 'ln-service' import { createInvoice, decodePaymentRequest, payViaPaymentRequest } from 'ln-service'
import { GraphQLError } from 'graphql' import { GraphQLError } from 'graphql'
import crypto from 'crypto'
import serialize from './serial' import serialize from './serial'
import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor' import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
import lnpr from 'bolt11' import lnpr from 'bolt11'
@ -40,6 +41,11 @@ export async function getInvoice (parent, { id }, { me, models }) {
return inv return inv
} }
export function createHmac (hash) {
const key = Buffer.from(process.env.INVOICE_HMAC_KEY, 'hex')
return crypto.createHmac('sha256', key).update(Buffer.from(hash, 'hex')).digest('hex')
}
export default { export default {
Query: { Query: {
invoice: getInvoice, invoice: getInvoice,
@ -220,7 +226,12 @@ export default {
models.$queryRaw`SELECT * FROM create_invoice(${invoice.id}, ${invoice.request}, models.$queryRaw`SELECT * FROM create_invoice(${invoice.id}, ${invoice.request},
${expiresAt}::timestamp, ${amount * 1000}, ${user.id}::INTEGER, ${description})`) ${expiresAt}::timestamp, ${amount * 1000}, ${user.id}::INTEGER, ${description})`)
return inv // the HMAC is only returned during invoice creation
// this makes sure that only the person who created this invoice
// has access to the HMAC
const hmac = createHmac(inv.hash)
return { ...inv, hmac }
} catch (error) { } catch (error) {
console.log(error) console.log(error)
throw error throw error

View File

@ -26,16 +26,16 @@ export default gql`
bookmarkItem(id: ID): Item bookmarkItem(id: ID): Item
subscribeItem(id: ID): Item subscribeItem(id: ID): Item
deleteItem(id: ID): Item deleteItem(id: ID): Item
upsertLink(id: ID, sub: String, title: String!, url: String!, boost: Int, forward: String, invoiceHash: String): Item! upsertLink(id: ID, sub: String, title: String!, url: String!, boost: Int, forward: String, invoiceHash: String, invoiceHmac: String): Item!
upsertDiscussion(id: ID, sub: String, title: String!, text: String, boost: Int, forward: String, invoiceHash: String): Item! upsertDiscussion(id: ID, sub: String, title: String!, text: String, boost: Int, forward: String, invoiceHash: String, invoiceHmac: String): Item!
upsertBounty(id: ID, sub: String, title: String!, text: String, bounty: Int!, boost: Int, forward: String): Item! upsertBounty(id: ID, sub: String, title: String!, text: String, bounty: Int!, boost: Int, forward: String): Item!
upsertJob(id: ID, sub: String!, title: String!, company: String!, location: String, remote: Boolean, upsertJob(id: ID, sub: String!, title: String!, company: String!, location: String, remote: Boolean,
text: String!, url: String!, maxBid: Int!, status: String, logo: Int): Item! text: String!, url: String!, maxBid: Int!, status: String, logo: Int): Item!
upsertPoll(id: ID, sub: String, title: String!, text: String, options: [String!]!, boost: Int, forward: String, invoiceHash: String): Item! upsertPoll(id: ID, sub: String, title: String!, text: String, options: [String!]!, boost: Int, forward: String, invoiceHash: String, invoiceHmac: String): Item!
createComment(text: String!, parentId: ID!, invoiceHash: String): Item! createComment(text: String!, parentId: ID!, invoiceHash: String, invoiceHmac: String): Item!
updateComment(id: ID!, text: String!): Item! updateComment(id: ID!, text: String!): Item!
dontLikeThis(id: ID!): Boolean! dontLikeThis(id: ID!): Boolean!
act(id: ID!, sats: Int, invoiceHash: String): ItemActResult! act(id: ID!, sats: Int, invoiceHash: String, invoiceHmac: String): ItemActResult!
pollVote(id: ID!): ID! pollVote(id: ID!): ID!
} }

View File

@ -24,6 +24,7 @@ export default gql`
confirmedAt: Date confirmedAt: Date
satsReceived: Int satsReceived: Int
nostr: JSONObject nostr: JSONObject
hmac: String
} }
type Withdrawl { type Withdrawl {

View File

@ -53,7 +53,7 @@ export function BountyForm ({
const submitUpsertBounty = useCallback( const submitUpsertBounty = useCallback(
// we ignore the invoice since only stackers can post bounties // we ignore the invoice since only stackers can post bounties
async (_, boost, bounty, values, __) => { async (_, boost, bounty, values, ...__) => {
const { error } = await upsertBounty({ const { error } = await upsertBounty({
variables: { variables: {
sub: item?.subName || sub?.name, sub: item?.subName || sub?.name,

View File

@ -29,17 +29,17 @@ export function DiscussionForm ({
// const me = useMe() // const me = useMe()
const [upsertDiscussion] = useMutation( const [upsertDiscussion] = useMutation(
gql` gql`
mutation upsertDiscussion($sub: String, $id: ID, $title: String!, $text: String, $boost: Int, $forward: String, $invoiceHash: String) { mutation upsertDiscussion($sub: String, $id: ID, $title: String!, $text: String, $boost: Int, $forward: String, $invoiceHash: String, $invoiceHmac: String) {
upsertDiscussion(sub: $sub, id: $id, title: $title, text: $text, boost: $boost, forward: $forward, invoiceHash: $invoiceHash) { upsertDiscussion(sub: $sub, id: $id, title: $title, text: $text, boost: $boost, forward: $forward, invoiceHash: $invoiceHash, invoiceHmac: $invoiceHmac) {
id id
} }
}` }`
) )
const submitUpsertDiscussion = useCallback( const submitUpsertDiscussion = useCallback(
async (_, boost, values, invoiceHash) => { async (_, boost, values, invoiceHash, invoiceHmac) => {
const { error } = await upsertDiscussion({ const { error } = await upsertDiscussion({
variables: { sub: item?.subName || sub?.name, id: item?.id, boost: boost ? Number(boost) : undefined, ...values, invoiceHash } variables: { sub: item?.subName || sub?.name, id: item?.id, boost: boost ? Number(boost) : undefined, ...values, invoiceHash, invoiceHmac }
}) })
if (error) { if (error) {
throw new Error({ message: error.toString() }) throw new Error({ message: error.toString() })

View File

@ -60,7 +60,7 @@ export function Invoice ({ invoice, onConfirmation, successVerb }) {
) )
} }
const Contacts = ({ invoiceHash }) => { const Contacts = ({ invoiceHash, invoiceHmac }) => {
const subject = `Support request for payment hash: ${invoiceHash}` const subject = `Support request for payment hash: ${invoiceHash}`
const body = 'Hi, I successfully paid for <insert action> but the action did not work.' const body = 'Hi, I successfully paid for <insert action> but the action did not work.'
return ( return (
@ -69,6 +69,10 @@ const Contacts = ({ invoiceHash }) => {
<div className='w-100'> <div className='w-100'>
<CopyInput type='text' placeholder={invoiceHash} readOnly noForm /> <CopyInput type='text' placeholder={invoiceHash} readOnly noForm />
</div> </div>
<span>Payment HMAC</span>
<div className='w-100'>
<CopyInput type='text' placeholder={invoiceHmac} readOnly noForm />
</div>
<div className='d-flex flex-row justify-content-center'> <div className='d-flex flex-row justify-content-center'>
<a <a
href={`mailto:kk@stacker.news?subject=${subject}&body=${body}`} className='nav-link p-0 d-inline-flex' href={`mailto:kk@stacker.news?subject=${subject}&body=${body}`} className='nav-link p-0 d-inline-flex'
@ -102,7 +106,7 @@ const Contacts = ({ invoiceHash }) => {
) )
} }
const ActionInvoice = ({ id, hash, errorCount, repeat, ...props }) => { const ActionInvoice = ({ id, hash, hmac, errorCount, repeat, ...props }) => {
const { data, loading, error } = useQuery(INVOICE, { const { data, loading, error } = useQuery(INVOICE, {
pollInterval: 1000, pollInterval: 1000,
variables: { id } variables: { id }
@ -130,7 +134,7 @@ const ActionInvoice = ({ id, hash, errorCount, repeat, ...props }) => {
<InvoiceStatus variant='failed' status={errorStatus} /> <InvoiceStatus variant='failed' status={errorStatus} />
{errorCount === 1 {errorCount === 1
? <div className='d-flex flex-row mt-3 justify-content-center'><Button variant='info' onClick={repeat}>Retry</Button></div> ? <div className='d-flex flex-row mt-3 justify-content-center'><Button variant='info' onClick={repeat}>Retry</Button></div>
: <Contacts invoiceHash={hash} />} : <Contacts invoiceHash={hash} invoiceHmac={hmac} />}
</> </>
) )
: null} : null}
@ -149,6 +153,7 @@ export const useInvoiceable = (fn, options = defaultOptions) => {
createInvoice(amount: $amount) { createInvoice(amount: $amount) {
id id
hash hash
hmac
} }
}`) }`)
const showModal = useShowModal() const showModal = useShowModal()
@ -157,11 +162,11 @@ export const useInvoiceable = (fn, options = defaultOptions) => {
// fix for bug where `showModal` runs the code for two modals and thus executes `onConfirmation` twice // fix for bug where `showModal` runs the code for two modals and thus executes `onConfirmation` twice
let errorCount = 0 let errorCount = 0
const onConfirmation = useCallback( const onConfirmation = useCallback(
onClose => { (onClose, hmac) => {
return async ({ id, satsReceived, hash }) => { return async ({ id, satsReceived, hash }) => {
await sleep(2000) await sleep(2000)
const repeat = () => const repeat = () =>
fn(satsReceived, ...fnArgs, hash) fn(satsReceived, ...fnArgs, hash, hmac)
.then(onClose) .then(onClose)
.catch((error) => { .catch((error) => {
console.error(error) console.error(error)
@ -171,7 +176,8 @@ export const useInvoiceable = (fn, options = defaultOptions) => {
<ActionInvoice <ActionInvoice
id={id} id={id}
hash={hash} hash={hash}
onConfirmation={onConfirmation(onClose)} hmac={hmac}
onConfirmation={onConfirmation(onClose, hmac)}
successVerb='received' successVerb='received'
errorCount={errorCount} errorCount={errorCount}
repeat={repeat} repeat={repeat}
@ -191,7 +197,8 @@ export const useInvoiceable = (fn, options = defaultOptions) => {
<ActionInvoice <ActionInvoice
id={invoice.id} id={invoice.id}
hash={invoice.hash} hash={invoice.hash}
onConfirmation={onConfirmation(onClose)} hmac={invoice.hmac}
onConfirmation={onConfirmation(onClose, invoice.hmac)}
successVerb='received' successVerb='received'
/> />
), { keepOpen: true } ), { keepOpen: true }
@ -213,7 +220,7 @@ export const useInvoiceable = (fn, options = defaultOptions) => {
<FundError <FundError
onClose={onClose} onClose={onClose}
amount={amount} amount={amount}
onPayment={async (_, invoiceHash) => { await fn(amount, ...args, invoiceHash) }} onPayment={async (_, invoiceHash, invoiceHmac) => { await fn(amount, ...args, invoiceHash, invoiceHmac) }}
/> />
) )
}) })

View File

@ -47,7 +47,7 @@ export default function ItemAct ({ onClose, itemId, act, strike }) {
}, [onClose, itemId]) }, [onClose, itemId])
const submitAct = useCallback( const submitAct = useCallback(
async (amount, invoiceHash) => { async (amount, invoiceHash, invoiceHmac) => {
if (!me) { if (!me) {
const storageKey = `TIP-item:${itemId}` const storageKey = `TIP-item:${itemId}`
const existingAmount = Number(window.localStorage.getItem(storageKey) || '0') const existingAmount = Number(window.localStorage.getItem(storageKey) || '0')
@ -57,7 +57,8 @@ export default function ItemAct ({ onClose, itemId, act, strike }) {
variables: { variables: {
id: itemId, id: itemId,
sats: Number(amount), sats: Number(amount),
invoiceHash invoiceHash,
invoiceHmac
} }
}) })
await strike() await strike()

View File

@ -53,7 +53,7 @@ export default function JobForm ({ item, sub }) {
const submitUpsertJob = useCallback( const submitUpsertJob = useCallback(
// we ignore the invoice since only stackers can post jobs // we ignore the invoice since only stackers can post jobs
async (_, maxBid, stop, start, values, __) => { async (_, maxBid, stop, start, values, ...__) => {
let status let status
if (start) { if (start) {
status = 'ACTIVE' status = 'ACTIVE'

View File

@ -67,17 +67,17 @@ export function LinkForm ({ item, sub, editThreshold, children }) {
const [upsertLink] = useMutation( const [upsertLink] = useMutation(
gql` gql`
mutation upsertLink($sub: String, $id: ID, $title: String!, $url: String!, $boost: Int, $forward: String, $invoiceHash: String) { mutation upsertLink($sub: String, $id: ID, $title: String!, $url: String!, $boost: Int, $forward: String, $invoiceHash: String, $invoiceHmac: String) {
upsertLink(sub: $sub, id: $id, title: $title, url: $url, boost: $boost, forward: $forward, invoiceHash: $invoiceHash) { upsertLink(sub: $sub, id: $id, title: $title, url: $url, boost: $boost, forward: $forward, invoiceHash: $invoiceHash, invoiceHmac: $invoiceHmac) {
id id
} }
}` }`
) )
const submitUpsertLink = useCallback( const submitUpsertLink = useCallback(
async (_, boost, title, values, invoiceHash) => { async (_, boost, title, values, invoiceHash, invoiceHmac) => {
const { error } = await upsertLink({ const { error } = await upsertLink({
variables: { sub: item?.subName || sub?.name, id: item?.id, boost: boost ? Number(boost) : undefined, title: title.trim(), invoiceHash, ...values } variables: { sub: item?.subName || sub?.name, id: item?.id, boost: boost ? Number(boost) : undefined, title: title.trim(), invoiceHash, invoiceHmac, ...values }
}) })
if (error) { if (error) {
throw new Error({ message: error.toString() }) throw new Error({ message: error.toString() })

View File

@ -21,16 +21,16 @@ export function PollForm ({ item, sub, editThreshold, children }) {
const [upsertPoll] = useMutation( const [upsertPoll] = useMutation(
gql` gql`
mutation upsertPoll($sub: String, $id: ID, $title: String!, $text: String, mutation upsertPoll($sub: String, $id: ID, $title: String!, $text: String,
$options: [String!]!, $boost: Int, $forward: String, $invoiceHash: String) { $options: [String!]!, $boost: Int, $forward: String, $invoiceHash: String, $invoiceHmac: String) {
upsertPoll(sub: $sub, id: $id, title: $title, text: $text, upsertPoll(sub: $sub, id: $id, title: $title, text: $text,
options: $options, boost: $boost, forward: $forward, invoiceHash: $invoiceHash) { options: $options, boost: $boost, forward: $forward, invoiceHash: $invoiceHash, invoiceHmac: $invoiceHmac) {
id id
} }
}` }`
) )
const submitUpsertPoll = useCallback( const submitUpsertPoll = useCallback(
async (_, boost, title, options, values, invoiceHash) => { async (_, boost, title, options, values, invoiceHash, invoiceHmac) => {
const optionsFiltered = options.slice(initialOptions?.length).filter(word => word.trim().length > 0) const optionsFiltered = options.slice(initialOptions?.length).filter(word => word.trim().length > 0)
const { error } = await upsertPoll({ const { error } = await upsertPoll({
variables: { variables: {
@ -40,7 +40,8 @@ export function PollForm ({ item, sub, editThreshold, children }) {
title: title.trim(), title: title.trim(),
options: optionsFiltered, options: optionsFiltered,
...values, ...values,
invoiceHash invoiceHash,
invoiceHmac
} }
}) })
if (error) { if (error) {

View File

@ -46,8 +46,8 @@ export default function Reply ({ item, onSuccess, replyOpen, children, placehold
const [createComment] = useMutation( const [createComment] = useMutation(
gql` gql`
${COMMENTS} ${COMMENTS}
mutation createComment($text: String!, $parentId: ID!, $invoiceHash: String) { mutation createComment($text: String!, $parentId: ID!, $invoiceHash: String, $invoiceHmac: String) {
createComment(text: $text, parentId: $parentId, invoiceHash: $invoiceHash) { createComment(text: $text, parentId: $parentId, invoiceHash: $invoiceHash, invoiceHmac: $invoiceHmac) {
...CommentFields ...CommentFields
comments { comments {
...CommentsRecursive ...CommentsRecursive
@ -92,8 +92,8 @@ export default function Reply ({ item, onSuccess, replyOpen, children, placehold
) )
const submitComment = useCallback( const submitComment = useCallback(
async (_, values, parentId, resetForm, invoiceHash) => { async (_, values, parentId, resetForm, invoiceHash, invoiceHmac) => {
const { error } = await createComment({ variables: { ...values, parentId, invoiceHash } }) const { error } = await createComment({ variables: { ...values, parentId, invoiceHash, invoiceHmac } })
if (error) { if (error) {
throw new Error({ message: error.toString() }) throw new Error({ message: error.toString() })
} }

View File

@ -110,8 +110,8 @@ export default function UpVote ({ item, className, pendingSats, setPendingSats }
const [act] = useMutation( const [act] = useMutation(
gql` gql`
mutation act($id: ID!, $sats: Int!, $invoiceHash: String) { mutation act($id: ID!, $sats: Int!, $invoiceHash: String, $invoiceHmac: String) {
act(id: $id, sats: $sats, invoiceHash: $invoiceHash) { act(id: $id, sats: $sats, invoiceHash: $invoiceHash, invoiceHmac: $invoiceHmac) {
sats sats
} }
}`, { }`, {