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:
parent
4fe1d416de
commit
bb2212d51e
|
@ -47,6 +47,7 @@ PUBLIC_URL=http://localhost:3000
|
|||
LND_CONNECT_ADDRESS=03cc1d0932bb99b0697f5b5e5961b83ab7fd66f1efc4c9f5c7bad66c1bcbe78f02@xhlmkj7mfrl6ejnczfwl2vqik3xim6wzmurc2vlyfoqw2sasaocgpuad.onion:9735
|
||||
NEXTAUTH_SECRET=3_0W_PhDRZVanbeJsZZGIEljexkKoGbL6qGIqSwTjjI
|
||||
JWT_SIGNING_PRIVATE_KEY={"kty":"oct","kid":"FvD__hmeKoKHu2fKjUrWbRKfhjimIM4IKshyrJG4KSM","alg":"HS512","k":"3_0W_PhDRZVanbeJsZZGIEljexkKoGbL6qGIqSwTjjI"}
|
||||
INVOICE_HMAC_KEY=a4c1d9c81edb87b79d28809876a18cf72293eadb39f92f3f4f2f1cfbdf907c91
|
||||
|
||||
# imgproxy
|
||||
NEXT_PUBLIC_IMGPROXY_URL=
|
||||
|
|
|
@ -17,6 +17,7 @@ import { amountSchema, bountySchema, commentSchema, discussionSchema, jobSchema,
|
|||
import { sendUserNotification } from '../webPush'
|
||||
import { proxyImages } from './imgproxy'
|
||||
import { defaultCommentSort } from '../../lib/item'
|
||||
import { createHmac } from './wallet'
|
||||
|
||||
export async function commentFilterClause (me, models) {
|
||||
let clause = ` AND ("Item"."weightedVotes" - "Item"."weightedDownVotes" > -${ITEM_FILTER_THRESHOLD}`
|
||||
|
@ -37,9 +38,17 @@ export async function commentFilterClause (me, models) {
|
|||
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({
|
||||
where: { hash: invoiceHash },
|
||||
where: { hash },
|
||||
include: {
|
||||
user: true
|
||||
}
|
||||
|
@ -590,7 +599,7 @@ export default {
|
|||
if (id) {
|
||||
return await updateItem(parent, { id, data }, { me, models })
|
||||
} 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 }) => {
|
||||
|
@ -601,7 +610,7 @@ export default {
|
|||
if (id) {
|
||||
return await updateItem(parent, { id, data }, { me, models })
|
||||
} 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 }) => {
|
||||
|
@ -616,11 +625,11 @@ export default {
|
|||
}
|
||||
},
|
||||
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
|
||||
const trx = []
|
||||
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
|
||||
trx.push(models.invoice.delete({ where: { hash: invoiceHash } }))
|
||||
}
|
||||
|
@ -707,7 +716,7 @@ export default {
|
|||
},
|
||||
createComment: async (parent, data, { me, models }) => {
|
||||
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
|
||||
const user = await models.user.findUnique({ where: { id: me?.id || ANON_USER_ID } })
|
||||
|
||||
|
@ -740,7 +749,7 @@ export default {
|
|||
|
||||
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
|
||||
if (!me && !invoiceHash) {
|
||||
throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
|
||||
|
@ -749,8 +758,9 @@ export default {
|
|||
await ssValidate(amountSchema, { amount: sats })
|
||||
|
||||
let user = me
|
||||
let invoice
|
||||
if (!me && invoiceHash) {
|
||||
const invoice = await checkInvoice(models, invoiceHash, sats)
|
||||
invoice = await checkInvoice(models, invoiceHash, invoiceHmac, sats)
|
||||
user = invoice.user
|
||||
}
|
||||
|
||||
|
@ -766,8 +776,8 @@ export default {
|
|||
const calls = [
|
||||
models.$queryRaw`SELECT item_act(${Number(id)}::INTEGER, ${user.id}::INTEGER, 'TIP', ${Number(sats)}::INTEGER)`
|
||||
]
|
||||
if (!me && invoiceHash) {
|
||||
calls.push(models.invoice.delete({ where: { hash: invoiceHash } }))
|
||||
if (invoice) {
|
||||
calls.push(models.invoice.delete({ where: { hash: invoice.hash } }))
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
const trx = []
|
||||
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
|
||||
trx.push(models.invoice.delete({ where: { hash: invoiceHash } }))
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { createInvoice, decodePaymentRequest, payViaPaymentRequest } from 'ln-service'
|
||||
import { GraphQLError } from 'graphql'
|
||||
import crypto from 'crypto'
|
||||
import serialize from './serial'
|
||||
import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
|
||||
import lnpr from 'bolt11'
|
||||
|
@ -40,6 +41,11 @@ export async function getInvoice (parent, { id }, { me, models }) {
|
|||
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 {
|
||||
Query: {
|
||||
invoice: getInvoice,
|
||||
|
@ -220,7 +226,12 @@ export default {
|
|||
models.$queryRaw`SELECT * FROM create_invoice(${invoice.id}, ${invoice.request},
|
||||
${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) {
|
||||
console.log(error)
|
||||
throw error
|
||||
|
|
|
@ -26,16 +26,16 @@ export default gql`
|
|||
bookmarkItem(id: ID): Item
|
||||
subscribeItem(id: ID): Item
|
||||
deleteItem(id: ID): Item
|
||||
upsertLink(id: ID, sub: String, title: String!, url: String!, boost: Int, forward: String, invoiceHash: String): Item!
|
||||
upsertDiscussion(id: ID, sub: String, title: String!, text: 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, invoiceHmac: 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,
|
||||
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!
|
||||
createComment(text: String!, parentId: ID!, 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, invoiceHmac: String): Item!
|
||||
updateComment(id: ID!, text: String!): Item!
|
||||
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!
|
||||
}
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ export default gql`
|
|||
confirmedAt: Date
|
||||
satsReceived: Int
|
||||
nostr: JSONObject
|
||||
hmac: String
|
||||
}
|
||||
|
||||
type Withdrawl {
|
||||
|
|
|
@ -53,7 +53,7 @@ export function BountyForm ({
|
|||
|
||||
const submitUpsertBounty = useCallback(
|
||||
// we ignore the invoice since only stackers can post bounties
|
||||
async (_, boost, bounty, values, __) => {
|
||||
async (_, boost, bounty, values, ...__) => {
|
||||
const { error } = await upsertBounty({
|
||||
variables: {
|
||||
sub: item?.subName || sub?.name,
|
||||
|
|
|
@ -29,17 +29,17 @@ export function DiscussionForm ({
|
|||
// const me = useMe()
|
||||
const [upsertDiscussion] = useMutation(
|
||||
gql`
|
||||
mutation upsertDiscussion($sub: String, $id: ID, $title: String!, $text: String, $boost: Int, $forward: String, $invoiceHash: String) {
|
||||
upsertDiscussion(sub: $sub, id: $id, title: $title, text: $text, boost: $boost, forward: $forward, invoiceHash: $invoiceHash) {
|
||||
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, invoiceHmac: $invoiceHmac) {
|
||||
id
|
||||
}
|
||||
}`
|
||||
)
|
||||
|
||||
const submitUpsertDiscussion = useCallback(
|
||||
async (_, boost, values, invoiceHash) => {
|
||||
async (_, boost, values, invoiceHash, invoiceHmac) => {
|
||||
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) {
|
||||
throw new Error({ message: error.toString() })
|
||||
|
|
|
@ -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 body = 'Hi, I successfully paid for <insert action> but the action did not work.'
|
||||
return (
|
||||
|
@ -69,6 +69,10 @@ const Contacts = ({ invoiceHash }) => {
|
|||
<div className='w-100'>
|
||||
<CopyInput type='text' placeholder={invoiceHash} readOnly noForm />
|
||||
</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'>
|
||||
<a
|
||||
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, {
|
||||
pollInterval: 1000,
|
||||
variables: { id }
|
||||
|
@ -130,7 +134,7 @@ const ActionInvoice = ({ id, hash, errorCount, repeat, ...props }) => {
|
|||
<InvoiceStatus variant='failed' status={errorStatus} />
|
||||
{errorCount === 1
|
||||
? <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}
|
||||
|
@ -149,6 +153,7 @@ export const useInvoiceable = (fn, options = defaultOptions) => {
|
|||
createInvoice(amount: $amount) {
|
||||
id
|
||||
hash
|
||||
hmac
|
||||
}
|
||||
}`)
|
||||
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
|
||||
let errorCount = 0
|
||||
const onConfirmation = useCallback(
|
||||
onClose => {
|
||||
(onClose, hmac) => {
|
||||
return async ({ id, satsReceived, hash }) => {
|
||||
await sleep(2000)
|
||||
const repeat = () =>
|
||||
fn(satsReceived, ...fnArgs, hash)
|
||||
fn(satsReceived, ...fnArgs, hash, hmac)
|
||||
.then(onClose)
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
|
@ -171,7 +176,8 @@ export const useInvoiceable = (fn, options = defaultOptions) => {
|
|||
<ActionInvoice
|
||||
id={id}
|
||||
hash={hash}
|
||||
onConfirmation={onConfirmation(onClose)}
|
||||
hmac={hmac}
|
||||
onConfirmation={onConfirmation(onClose, hmac)}
|
||||
successVerb='received'
|
||||
errorCount={errorCount}
|
||||
repeat={repeat}
|
||||
|
@ -191,7 +197,8 @@ export const useInvoiceable = (fn, options = defaultOptions) => {
|
|||
<ActionInvoice
|
||||
id={invoice.id}
|
||||
hash={invoice.hash}
|
||||
onConfirmation={onConfirmation(onClose)}
|
||||
hmac={invoice.hmac}
|
||||
onConfirmation={onConfirmation(onClose, invoice.hmac)}
|
||||
successVerb='received'
|
||||
/>
|
||||
), { keepOpen: true }
|
||||
|
@ -213,7 +220,7 @@ export const useInvoiceable = (fn, options = defaultOptions) => {
|
|||
<FundError
|
||||
onClose={onClose}
|
||||
amount={amount}
|
||||
onPayment={async (_, invoiceHash) => { await fn(amount, ...args, invoiceHash) }}
|
||||
onPayment={async (_, invoiceHash, invoiceHmac) => { await fn(amount, ...args, invoiceHash, invoiceHmac) }}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -47,7 +47,7 @@ export default function ItemAct ({ onClose, itemId, act, strike }) {
|
|||
}, [onClose, itemId])
|
||||
|
||||
const submitAct = useCallback(
|
||||
async (amount, invoiceHash) => {
|
||||
async (amount, invoiceHash, invoiceHmac) => {
|
||||
if (!me) {
|
||||
const storageKey = `TIP-item:${itemId}`
|
||||
const existingAmount = Number(window.localStorage.getItem(storageKey) || '0')
|
||||
|
@ -57,7 +57,8 @@ export default function ItemAct ({ onClose, itemId, act, strike }) {
|
|||
variables: {
|
||||
id: itemId,
|
||||
sats: Number(amount),
|
||||
invoiceHash
|
||||
invoiceHash,
|
||||
invoiceHmac
|
||||
}
|
||||
})
|
||||
await strike()
|
||||
|
|
|
@ -53,7 +53,7 @@ export default function JobForm ({ item, sub }) {
|
|||
|
||||
const submitUpsertJob = useCallback(
|
||||
// we ignore the invoice since only stackers can post jobs
|
||||
async (_, maxBid, stop, start, values, __) => {
|
||||
async (_, maxBid, stop, start, values, ...__) => {
|
||||
let status
|
||||
if (start) {
|
||||
status = 'ACTIVE'
|
||||
|
|
|
@ -67,17 +67,17 @@ export function LinkForm ({ item, sub, editThreshold, children }) {
|
|||
|
||||
const [upsertLink] = useMutation(
|
||||
gql`
|
||||
mutation upsertLink($sub: String, $id: ID, $title: String!, $url: String!, $boost: Int, $forward: String, $invoiceHash: String) {
|
||||
upsertLink(sub: $sub, id: $id, title: $title, url: $url, boost: $boost, forward: $forward, invoiceHash: $invoiceHash) {
|
||||
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, invoiceHmac: $invoiceHmac) {
|
||||
id
|
||||
}
|
||||
}`
|
||||
)
|
||||
|
||||
const submitUpsertLink = useCallback(
|
||||
async (_, boost, title, values, invoiceHash) => {
|
||||
async (_, boost, title, values, invoiceHash, invoiceHmac) => {
|
||||
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) {
|
||||
throw new Error({ message: error.toString() })
|
||||
|
|
|
@ -21,16 +21,16 @@ export function PollForm ({ item, sub, editThreshold, children }) {
|
|||
const [upsertPoll] = useMutation(
|
||||
gql`
|
||||
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,
|
||||
options: $options, boost: $boost, forward: $forward, invoiceHash: $invoiceHash) {
|
||||
options: $options, boost: $boost, forward: $forward, invoiceHash: $invoiceHash, invoiceHmac: $invoiceHmac) {
|
||||
id
|
||||
}
|
||||
}`
|
||||
)
|
||||
|
||||
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 { error } = await upsertPoll({
|
||||
variables: {
|
||||
|
@ -40,7 +40,8 @@ export function PollForm ({ item, sub, editThreshold, children }) {
|
|||
title: title.trim(),
|
||||
options: optionsFiltered,
|
||||
...values,
|
||||
invoiceHash
|
||||
invoiceHash,
|
||||
invoiceHmac
|
||||
}
|
||||
})
|
||||
if (error) {
|
||||
|
|
|
@ -46,8 +46,8 @@ export default function Reply ({ item, onSuccess, replyOpen, children, placehold
|
|||
const [createComment] = useMutation(
|
||||
gql`
|
||||
${COMMENTS}
|
||||
mutation createComment($text: String!, $parentId: ID!, $invoiceHash: String) {
|
||||
createComment(text: $text, parentId: $parentId, invoiceHash: $invoiceHash) {
|
||||
mutation createComment($text: String!, $parentId: ID!, $invoiceHash: String, $invoiceHmac: String) {
|
||||
createComment(text: $text, parentId: $parentId, invoiceHash: $invoiceHash, invoiceHmac: $invoiceHmac) {
|
||||
...CommentFields
|
||||
comments {
|
||||
...CommentsRecursive
|
||||
|
@ -92,8 +92,8 @@ export default function Reply ({ item, onSuccess, replyOpen, children, placehold
|
|||
)
|
||||
|
||||
const submitComment = useCallback(
|
||||
async (_, values, parentId, resetForm, invoiceHash) => {
|
||||
const { error } = await createComment({ variables: { ...values, parentId, invoiceHash } })
|
||||
async (_, values, parentId, resetForm, invoiceHash, invoiceHmac) => {
|
||||
const { error } = await createComment({ variables: { ...values, parentId, invoiceHash, invoiceHmac } })
|
||||
if (error) {
|
||||
throw new Error({ message: error.toString() })
|
||||
}
|
||||
|
|
|
@ -110,8 +110,8 @@ export default function UpVote ({ item, className, pendingSats, setPendingSats }
|
|||
|
||||
const [act] = useMutation(
|
||||
gql`
|
||||
mutation act($id: ID!, $sats: Int!, $invoiceHash: String) {
|
||||
act(id: $id, sats: $sats, invoiceHash: $invoiceHash) {
|
||||
mutation act($id: ID!, $sats: Int!, $invoiceHash: String, $invoiceHmac: String) {
|
||||
act(id: $id, sats: $sats, invoiceHash: $invoiceHash, invoiceHmac: $invoiceHmac) {
|
||||
sats
|
||||
}
|
||||
}`, {
|
||||
|
|
Loading…
Reference in New Issue