ekzyis fd8510d59f Use payment hash instead of invoice id as proof of payment
Our invoice IDs can be enumerated.
So there is a - even though very rare - chance that an attacker could find a paid invoice which is not used yet and use it for himself.
Random payment hashes prevent this.

Also, since we delete invoices after use, using database IDs as proof of payments are not suitable.
If a user tells us an invoice ID after we deleted it, we can no longer tell if the invoice was paid or not since the LN node only knows about payment hashes but nothing about the database IDs.
2023-07-30 23:45:07 +02:00

168 lines
5.3 KiB
JavaScript

import { Form, MarkdownInput, SubmitButton } from '../components/form'
import { gql, useMutation } from '@apollo/client'
import styles from './reply.module.css'
import { COMMENTS } from '../fragments/comments'
import { useMe } from './me'
import { useEffect, useState, useRef, useCallback } from 'react'
import Link from 'next/link'
import FeeButton from './fee-button'
import { commentsViewedAfterComment } from '../lib/new-comments'
import { commentSchema } from '../lib/validate'
import Info from './info'
import { useAnonymous } from '../lib/anonymous'
import { ANON_COMMENT_FEE } from '../lib/constants'
export function ReplyOnAnotherPage ({ parentId }) {
return (
<Link href={`/items/${parentId}`} className={`${styles.replyButtons} text-muted`}>
reply on another page
</Link>
)
}
function FreebieDialog () {
return (
<div className='text-muted'>
you have no sats, so this one is on us
<Info>
<ul className='fw-bold'>
<li>Free comments have limited visibility and are listed at the bottom of the comment section until other stackers zap them.</li>
<li>Free comments will not cover comments that cost more than 1 sat.</li>
<li>To get fully visibile and unrestricted comments right away, fund your account with a few sats or earn some on Stacker News.</li>
</ul>
</Info>
</div>
)
}
export default function Reply ({ item, onSuccess, replyOpen, children, placeholder }) {
const [reply, setReply] = useState(replyOpen)
const me = useMe()
const parentId = item.id
useEffect(() => {
setReply(replyOpen || !!window.localStorage.getItem('reply-' + parentId + '-' + 'text'))
}, [])
const [createComment] = useMutation(
gql`
${COMMENTS}
mutation createComment($text: String!, $parentId: ID!, $invoiceHash: String) {
createComment(text: $text, parentId: $parentId, invoiceHash: $invoiceHash) {
...CommentFields
comments {
...CommentsRecursive
}
}
}`, {
update (cache, { data: { createComment } }) {
cache.modify({
id: `Item:${parentId}`,
fields: {
comments (existingCommentRefs = []) {
const newCommentRef = cache.writeFragment({
data: createComment,
fragment: COMMENTS,
fragmentName: 'CommentsRecursive'
})
return [newCommentRef, ...existingCommentRefs]
}
}
})
const ancestors = item.path.split('.')
// update all ancestors
ancestors.forEach(id => {
cache.modify({
id: `Item:${id}`,
fields: {
ncomments (existingNComments = 0) {
return existingNComments + 1
}
}
})
})
// so that we don't see indicator for our own comments, we record this comments as the latest time
// but we also have record num comments, in case someone else commented when we did
const root = ancestors[0]
commentsViewedAfterComment(root, createComment.createdAt)
}
}
)
const submitComment = useCallback(
async (_, values, parentId, resetForm, invoiceHash) => {
const { error } = await createComment({ variables: { ...values, parentId, invoiceHash } })
if (error) {
throw new Error({ message: error.toString() })
}
resetForm({ text: '' })
setReply(replyOpen || false)
}, [createComment, setReply])
const anonCreateComment = useAnonymous(submitComment)
const replyInput = useRef(null)
useEffect(() => {
if (replyInput.current && reply && !replyOpen) replyInput.current.focus()
}, [reply])
return (
<div>
{replyOpen
? <div className={styles.replyButtons} />
: (
<div className={styles.replyButtons}>
<div
onClick={() => setReply(!reply)}
>
{reply ? 'cancel' : 'reply'}
</div>
{/* HACK if we need more items, we should probably do a comment toolbar */}
{children}
</div>)}
{reply &&
<div className={styles.reply}>
<Form
initial={{
text: ''
}}
schema={commentSchema}
onSubmit={async (values, { resetForm }) => {
await anonCreateComment(ANON_COMMENT_FEE, values, parentId, resetForm)
}}
storageKeyPrefix={'reply-' + parentId}
>
<MarkdownInput
name='text'
minRows={6}
autoFocus={!replyOpen}
required
placeholder={placeholder}
hint={me?.sats < 1 && <FreebieDialog />}
innerRef={replyInput}
/>
{reply &&
<div className='mt-1'>
<FeeButton
baseFee={1} parentId={parentId} text='reply'
ChildButton={SubmitButton} variant='secondary' alwaysShow
/>
</div>}
</Form>
</div>}
</div>
)
}
export function ReplySkeleton () {
return (
<div className={`${styles.reply} ${styles.skeleton}`}>
<div className={`${styles.input} clouds`} />
<div className={`${styles.button} clouds`} />
</div>
)
}