reduce duplication of invoicable code

This commit is contained in:
keyan 2023-09-26 15:15:09 -05:00
parent b740eeb2c4
commit 32847670e2
3 changed files with 121 additions and 192 deletions

View File

@ -1,6 +1,6 @@
import { GraphQLError } from 'graphql' import { GraphQLError } from 'graphql'
import { ensureProtocol, removeTracking } from '../../lib/url' import { ensureProtocol, removeTracking } from '../../lib/url'
import serialize from './serial' import { serializeInvoicable } from './serial'
import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor' import { decodeCursor, LIMIT, nextCursorEncoded } from '../../lib/cursor'
import { getMetadata, metadataRuleSets } from 'page-metadata-parser' import { getMetadata, metadataRuleSets } from 'page-metadata-parser'
import domino from 'domino' import domino from 'domino'
@ -16,8 +16,6 @@ import { advSchema, amountSchema, bountySchema, commentSchema, discussionSchema,
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'
import { settleHodlInvoice } from 'ln-service'
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}`
@ -38,51 +36,6 @@ export async function commentFilterClause (me, models) {
return clause return clause
} }
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' } })
}
const hmac2 = createHmac(hash)
if (hmac !== hmac2) {
throw new GraphQLError('bad hmac', { extensions: { code: 'FORBIDDEN' } })
}
const invoice = await models.invoice.findUnique({
where: { hash },
include: {
user: true
}
})
if (!invoice) {
throw new GraphQLError('invoice not found', { extensions: { code: 'BAD_INPUT' } })
}
const expired = new Date(invoice.expiresAt) <= new Date()
if (expired) {
throw new GraphQLError('invoice expired', { extensions: { code: 'BAD_INPUT' } })
}
if (invoice.confirmedAt) {
throw new GraphQLError('invoice already used', { extensions: { code: 'BAD_INPUT' } })
}
if (invoice.cancelled) {
throw new GraphQLError('invoice was canceled', { extensions: { code: 'BAD_INPUT' } })
}
if (!invoice.msatsReceived) {
throw new GraphQLError('invoice was not paid', { extensions: { code: 'BAD_INPUT' } })
}
if (fee && msatsToSats(invoice.msatsReceived) < fee) {
throw new GraphQLError('invoice amount too low', { extensions: { code: 'BAD_INPUT' } })
}
return invoice
}
async function comments (me, models, id, sort) { async function comments (me, models, id, sort) {
let orderBy let orderBy
switch (sort) { switch (sort) {
@ -714,72 +667,45 @@ export default {
return rItem return rItem
} }
}, },
pollVote: async (parent, { id, hash, hmac }, { me, models }) => { pollVote: async (parent, { id, hash, hmac }, { me, models, lnd }) => {
if (!me) { if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } }) throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
} }
let invoice await serializeInvoicable(
if (hash) { models.$queryRawUnsafe(`${SELECT} FROM poll_vote($1::INTEGER, $2::INTEGER) AS "Item"`, Number(id), Number(me.id)),
invoice = await checkInvoice(models, hash, hmac) { me, models, lnd, hash, hmac }
} )
const trx = [
models.$queryRawUnsafe(`${SELECT} FROM poll_vote($1::INTEGER, $2::INTEGER) AS "Item"`, Number(id), Number(me.id))
]
if (invoice) {
trx.unshift(models.$queryRaw`UPDATE users SET msats = msats + ${invoice.msatsReceived} WHERE id = ${invoice.user.id}`)
trx.push(models.invoice.update({ where: { hash: invoice.hash }, data: { confirmedAt: new Date() } }))
}
await serialize(models, ...trx)
return id return id
}, },
act: async (parent, { id, sats, hash, hmac }, { me, models, lnd }) => { act: async (parent, { id, sats, hash, hmac }, { me, models, lnd }) => {
// need to make sure we are logged in
if (!me && !hash) {
throw new GraphQLError('you must be logged in or pay', { extensions: { code: 'FORBIDDEN' } })
}
await ssValidate(amountSchema, { amount: sats }) await ssValidate(amountSchema, { amount: sats })
let user = me
let invoice
if (hash) {
invoice = await checkInvoice(models, hash, hmac, sats)
if (!me) user = invoice.user
}
// disallow self tips except anons // disallow self tips except anons
if (user.id !== ANON_USER_ID) { if (me) {
const [item] = await models.$queryRawUnsafe(` const [item] = await models.$queryRawUnsafe(`
${SELECT} ${SELECT}
FROM "Item" FROM "Item"
WHERE id = $1 AND "userId" = $2`, Number(id), user.id) WHERE id = $1 AND "userId" = $2`, Number(id), me.id)
if (item) { if (item) {
throw new GraphQLError('cannot zap your self', { extensions: { code: 'BAD_INPUT' } }) throw new GraphQLError('cannot zap your self', { extensions: { code: 'BAD_INPUT' } })
} }
}
// Disallow tips if me is one of the forward user recipients // Disallow tips if me is one of the forward user recipients
const existingForwards = await models.itemForward.findMany({ where: { itemId: Number(id) } }) const existingForwards = await models.itemForward.findMany({ where: { itemId: Number(id) } })
if (existingForwards.some(fwd => Number(fwd.userId) === Number(user.id))) { if (existingForwards.some(fwd => Number(fwd.userId) === Number(me.id))) {
throw new GraphQLError('cannot zap a post for which you are forwarded zaps', { extensions: { code: 'BAD_INPUT' } }) throw new GraphQLError('cannot zap a post for which you are forwarded zaps', { extensions: { code: 'BAD_INPUT' } })
} }
const trx = [
models.$queryRaw`SELECT item_act(${Number(id)}::INTEGER, ${user.id}::INTEGER, 'TIP', ${Number(sats)}::INTEGER)`
]
if (invoice) {
trx.unshift(models.$queryRaw`UPDATE users SET msats = msats + ${invoice.msatsReceived} WHERE id = ${invoice.user.id}`)
trx.push(models.invoice.update({ where: { hash: invoice.hash }, data: { confirmedAt: new Date() } }))
} }
const query = await serialize(models, ...trx) const { item_act: vote } = await serializeInvoicable(
const { item_act: vote } = trx.length > 1 ? query[1][0] : query[0] models.$queryRaw`
SELECT
if (invoice?.isHeld) await settleHodlInvoice({ secret: invoice.preimage, lnd }) item_act(${Number(id)}::INTEGER,
${me?.id || ANON_USER_ID}::INTEGER, 'TIP', ${Number(sats)}::INTEGER)`,
{ me, models, lnd, hash, hmac, enforceFee: sats }
)
const notify = async () => { const notify = async () => {
try { try {
@ -837,17 +763,12 @@ export default {
sats sats
} }
}, },
dontLikeThis: async (parent, { id, sats, hash, hmac }, { me, models }) => { dontLikeThis: async (parent, { id, sats = DONT_LIKE_THIS_COST, hash, hmac }, { me, lnd, models }) => {
// need to make sure we are logged in // need to make sure we are logged in
if (!me) { if (!me) {
throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } }) throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
} }
let invoice
if (hash) {
invoice = await checkInvoice(models, hash, hmac, DONT_LIKE_THIS_COST)
}
// disallow self down votes // disallow self down votes
const [item] = await models.$queryRawUnsafe(` const [item] = await models.$queryRawUnsafe(`
${SELECT} ${SELECT}
@ -857,16 +778,11 @@ export default {
throw new GraphQLError('cannot downvote your self', { extensions: { code: 'BAD_INPUT' } }) throw new GraphQLError('cannot downvote your self', { extensions: { code: 'BAD_INPUT' } })
} }
const trx = [ await serializeInvoicable(
models.$queryRaw`SELECT item_act(${Number(id)}::INTEGER, models.$queryRaw`SELECT item_act(${Number(id)}::INTEGER,
${me.id}::INTEGER, 'DONT_LIKE_THIS', ${sats || DONT_LIKE_THIS_COST}::INTEGER)` ${me.id}::INTEGER, 'DONT_LIKE_THIS', ${sats}::INTEGER)`,
] { me, models, lnd, hash, hmac }
if (invoice) { )
trx.unshift(models.$queryRaw`UPDATE users SET msats = msats + ${invoice.msatsReceived} WHERE id = ${invoice.user.id}`)
trx.push(models.invoice.update({ where: { hash: invoice.hash }, data: { confirmedAt: new Date() } }))
}
await serialize(models, ...trx)
return true return true
} }
@ -1126,6 +1042,7 @@ export const updateItem = async (parent, { sub: subName, forward, options, ...it
throw new GraphQLError('item does not belong to you', { extensions: { code: 'FORBIDDEN' } }) throw new GraphQLError('item does not belong to you', { extensions: { code: 'FORBIDDEN' } })
} }
// in case they lied about their existing boost
await ssValidate(advSchema, { boost: item.boost }, { models, me, existingBoost: old.boost }) await ssValidate(advSchema, { boost: item.boost }, { models, me, existingBoost: old.boost })
// if it's not the FAQ, not their bio, and older than 10 minutes // if it's not the FAQ, not their bio, and older than 10 minutes
@ -1157,29 +1074,16 @@ export const updateItem = async (parent, { sub: subName, forward, options, ...it
item = { subName, userId: me.id, ...item } item = { subName, userId: me.id, ...item }
const fwdUsers = await getForwardUsers(models, forward) const fwdUsers = await getForwardUsers(models, forward)
let invoice item = await serializeInvoicable(
if (hash) {
invoice = await checkInvoice(models, hash, hmac)
}
const trx = [
models.$queryRawUnsafe(`${SELECT} FROM update_item($1::JSONB, $2::JSONB, $3::JSONB) AS "Item"`, models.$queryRawUnsafe(`${SELECT} FROM update_item($1::JSONB, $2::JSONB, $3::JSONB) AS "Item"`,
JSON.stringify(item), JSON.stringify(fwdUsers), JSON.stringify(options)) JSON.stringify(item), JSON.stringify(fwdUsers), JSON.stringify(options)),
] { models, lnd, hash, hmac, me }
if (invoice) { )
trx.unshift(models.$queryRaw`UPDATE users SET msats = msats + ${invoice.msatsReceived} WHERE id = ${invoice.user.id}`)
trx.push(models.invoice.update({ where: { hash: invoice.hash }, data: { confirmedAt: new Date() } }))
}
const query = await serialize(models, ...trx) await createMentions(item, models)
const rItem = trx.length > 1 ? query[1][0] : query[0]
if (invoice?.isHeld) await settleHodlInvoice({ secret: invoice.preimage, lnd }) item.comments = []
return item
await createMentions(rItem, models)
rItem.comments = []
return rItem
} }
export const createItem = async (parent, { forward, options, ...item }, { me, models, lnd, hash, hmac }) => { export const createItem = async (parent, { forward, options, ...item }, { me, models, lnd, hash, hmac }) => {
@ -1189,21 +1093,7 @@ export const createItem = async (parent, { forward, options, ...item }, { me, mo
item.subName = item.sub item.subName = item.sub
delete item.sub delete item.sub
if (!me && !hash) { item.userId = me ? Number(me.id) : ANON_USER_ID
throw new GraphQLError('you must be logged in or pay', { extensions: { code: 'FORBIDDEN' } })
}
let invoice
if (hash) {
// if we are logged in, we don't compare the invoice amount with the fee
// since it's not a fixed amount that we could use here.
// we rely on the query telling us if the balance is too low
const fee = !me ? (item.parentId ? ANON_COMMENT_FEE : ANON_POST_FEE) : undefined
invoice = await checkInvoice(models, hash, hmac, fee)
item.userId = invoice.user.id
}
if (me) {
item.userId = Number(me.id)
}
const fwdUsers = await getForwardUsers(models, forward) const fwdUsers = await getForwardUsers(models, forward)
if (item.text) { if (item.text) {
@ -1215,19 +1105,13 @@ export const createItem = async (parent, { forward, options, ...item }, { me, mo
item.url = await proxyImages(item.url) item.url = await proxyImages(item.url)
} }
const trx = [ const enforceFee = me ? undefined : (item.parentId ? ANON_COMMENT_FEE : (ANON_POST_FEE + (item.boost || 0)))
models.$queryRawUnsafe(`${SELECT} FROM create_item($1::JSONB, $2::JSONB, $3::JSONB, '${spamInterval}'::INTERVAL) AS "Item"`, item = await serializeInvoicable(
JSON.stringify(item), JSON.stringify(fwdUsers), JSON.stringify(options)) models.$queryRawUnsafe(
] `${SELECT} FROM create_item($1::JSONB, $2::JSONB, $3::JSONB, '${spamInterval}'::INTERVAL) AS "Item"`,
if (invoice) { JSON.stringify(item), JSON.stringify(fwdUsers), JSON.stringify(options)),
trx.unshift(models.$queryRaw`UPDATE users SET msats = msats + ${invoice.msatsReceived} WHERE id = ${invoice.user.id}`) { models, lnd, hash, hmac, me, enforceFee }
trx.push(models.invoice.update({ where: { hash: invoice.hash }, data: { confirmedAt: new Date() } })) )
}
const query = await serialize(models, ...trx)
item = trx.length > 1 ? query[1][0] : query[0]
if (invoice?.isHeld) await settleHodlInvoice({ secret: invoice.preimage, lnd })
await createMentions(item, models) await createMentions(item, models)

View File

@ -1,9 +1,8 @@
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 { serializeInvoicable } from './serial'
import { ANON_USER_ID } from '../../lib/constants' import { ANON_USER_ID } from '../../lib/constants'
import { getItem, checkInvoice } from './item' import { getItem } from './item'
export default { export default {
Query: { Query: {
@ -104,37 +103,12 @@ export default {
}, },
Mutation: { Mutation: {
donateToRewards: async (parent, { sats, hash, hmac }, { me, models, lnd }) => { 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 ssValidate(amountSchema, { amount: sats })
const trx = [ await serializeInvoicable(
models.$queryRawUnsafe( models.$queryRaw`SELECT donate(${sats}::INTEGER, ${me?.id || ANON_USER_ID}::INTEGER)`,
'SELECT donate($1::INTEGER, $2::INTEGER)', { models, lnd, hash, hmac, me, enforceFee: sats }
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

@ -1,8 +1,11 @@
import { GraphQLError } from 'graphql' import { GraphQLError } from 'graphql'
import retry from 'async-retry' import retry from 'async-retry'
import Prisma from '@prisma/client' import Prisma from '@prisma/client'
import { settleHodlInvoice } from 'ln-service'
import { createHmac } from './wallet'
import { msatsToSats } from '../../lib/format'
async function serialize (models, ...calls) { export default async function serialize (models, ...calls) {
return await retry(async bail => { return await retry(async bail => {
try { try {
const [, ...result] = await models.$transaction( const [, ...result] = await models.$transaction(
@ -56,4 +59,72 @@ async function serialize (models, ...calls) {
}) })
} }
export default serialize export async function serializeInvoicable (query, { models, lnd, hash, hmac, me, enforceFee }) {
if (!me && !hash) {
throw new Error('you must be logged in or pay')
}
let trx = [query]
let invoice
if (hash) {
invoice = await checkInvoice(models, hash, hmac, enforceFee)
trx = [
models.$queryRaw`UPDATE users SET msats = msats + ${invoice.msatsReceived} WHERE id = ${invoice.user.id}`,
query,
models.invoice.update({ where: { hash: invoice.hash }, data: { confirmedAt: new Date() } })
]
}
const results = await serialize(models, ...trx)
const result = trx.length > 1 ? results[1][0] : results[0]
if (invoice?.isHeld) await settleHodlInvoice({ secret: invoice.preimage, lnd })
return result
}
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' } })
}
const hmac2 = createHmac(hash)
if (hmac !== hmac2) {
throw new GraphQLError('bad hmac', { extensions: { code: 'FORBIDDEN' } })
}
const invoice = await models.invoice.findUnique({
where: { hash },
include: {
user: true
}
})
if (!invoice) {
throw new GraphQLError('invoice not found', { extensions: { code: 'BAD_INPUT' } })
}
const expired = new Date(invoice.expiresAt) <= new Date()
if (expired) {
throw new GraphQLError('invoice expired', { extensions: { code: 'BAD_INPUT' } })
}
if (invoice.confirmedAt) {
throw new GraphQLError('invoice already used', { extensions: { code: 'BAD_INPUT' } })
}
if (invoice.cancelled) {
throw new GraphQLError('invoice was canceled', { extensions: { code: 'BAD_INPUT' } })
}
if (!invoice.msatsReceived) {
throw new GraphQLError('invoice was not paid', { extensions: { code: 'BAD_INPUT' } })
}
if (fee && msatsToSats(invoice.msatsReceived) < fee) {
throw new GraphQLError('invoice amount too low', { extensions: { code: 'BAD_INPUT' } })
}
return invoice
}