Revert "Merge serializeInvoiceable with serialize (#1040)"

This reverts commit e7e7cbff0a.
This commit is contained in:
keyan 2024-04-08 15:50:39 -05:00
parent e7e7cbff0a
commit 9f79ff1f89
11 changed files with 93 additions and 111 deletions

View File

@ -1,6 +1,6 @@
import { GraphQLError } from 'graphql'
import { ensureProtocol, removeTracking, stripTrailingSlash } from '@/lib/url'
import serialize from './serial'
import serialize, { serializeInvoicable } from './serial'
import { decodeCursor, LIMIT, nextCursorEncoded } from '@/lib/cursor'
import { getMetadata, metadataRuleSets } from 'page-metadata-parser'
import { ruleSet as publicationDateRuleSet } from '@/lib/timedate-scraper'
@ -849,9 +849,9 @@ export default {
throw new GraphQLError('you must be logged in', { extensions: { code: 'FORBIDDEN' } })
}
await serialize(
await serializeInvoicable(
models.$queryRawUnsafe(`${SELECT} FROM poll_vote($1::INTEGER, $2::INTEGER) AS "Item"`, Number(id), Number(me.id)),
{ models, lnd, me, hash, hmac }
{ me, models, lnd, hash, hmac }
)
return id
@ -883,6 +883,7 @@ export default {
if (idempotent) {
await serialize(
models,
models.$queryRaw`
SELECT
item_act(${Number(id)}::INTEGER, ${me.id}::INTEGER, ${act}::"ItemActType",
@ -890,16 +891,15 @@ export default {
FROM "ItemAct"
WHERE act IN ('TIP', 'FEE')
AND "itemId" = ${Number(id)}::INTEGER
AND "userId" = ${me.id}::INTEGER)::INTEGER)`,
{ models }
AND "userId" = ${me.id}::INTEGER)::INTEGER)`
)
} else {
await serialize(
await serializeInvoicable(
models.$queryRaw`
SELECT
item_act(${Number(id)}::INTEGER,
${me?.id || ANON_USER_ID}::INTEGER, ${act}::"ItemActType", ${Number(sats)}::INTEGER)`,
{ models, lnd, me, hash, hmac, fee: sats }
{ me, models, lnd, hash, hmac, enforceFee: sats }
)
}
@ -1284,10 +1284,10 @@ export const updateItem = async (parent, { sub: subName, forward, options, ...it
const uploadIds = uploadIdsFromText(item.text, { models })
const { totalFees: imgFees } = await imageFeesInfo(uploadIds, { models, me })
item = await serialize(
item = await serializeInvoicable(
models.$queryRawUnsafe(`${SELECT} FROM update_item($1::JSONB, $2::JSONB, $3::JSONB, $4::INTEGER[]) AS "Item"`,
JSON.stringify(item), JSON.stringify(fwdUsers), JSON.stringify(options), uploadIds),
{ models, lnd, me, hash, hmac, fee: imgFees }
{ models, lnd, hash, hmac, me, enforceFee: imgFees }
)
await createMentions(item, models)
@ -1320,22 +1320,22 @@ export const createItem = async (parent, { forward, options, ...item }, { me, mo
const uploadIds = uploadIdsFromText(item.text, { models })
const { totalFees: imgFees } = await imageFeesInfo(uploadIds, { models, me })
let fee = 0
let enforceFee = 0
if (!me) {
if (item.parentId) {
fee = ANON_FEE_MULTIPLIER
enforceFee = ANON_FEE_MULTIPLIER
} else {
const sub = await models.sub.findUnique({ where: { name: item.subName } })
fee = sub.baseCost * ANON_FEE_MULTIPLIER + (item.boost || 0)
enforceFee = sub.baseCost * ANON_FEE_MULTIPLIER + (item.boost || 0)
}
}
fee += imgFees
enforceFee += imgFees
item = await serialize(
item = await serializeInvoicable(
models.$queryRawUnsafe(
`${SELECT} FROM create_item($1::JSONB, $2::JSONB, $3::JSONB, '${spamInterval}'::INTERVAL, $4::INTEGER[]) AS "Item"`,
JSON.stringify(item), JSON.stringify(fwdUsers), JSON.stringify(options), uploadIds),
{ models, lnd, me, hash, hmac, fee }
{ models, lnd, hash, hmac, me, enforceFee }
)
await createMentions(item, models)

View File

@ -1,6 +1,6 @@
import { GraphQLError } from 'graphql'
import { amountSchema, ssValidate } from '@/lib/validate'
import serialize from './serial'
import { serializeInvoicable } from './serial'
import { ANON_USER_ID } from '@/lib/constants'
import { getItem } from './item'
import { topUsers } from './user'
@ -168,9 +168,9 @@ export default {
donateToRewards: async (parent, { sats, hash, hmac }, { me, models, lnd }) => {
await ssValidate(amountSchema, { amount: sats })
await serialize(
await serializeInvoicable(
models.$queryRaw`SELECT donate(${sats}::INTEGER, ${me?.id || ANON_USER_ID}::INTEGER)`,
{ models, lnd, me, hash, hmac, fee: sats }
{ models, lnd, hash, hmac, me, enforceFee: sats }
)
return sats

View File

@ -1,5 +1,4 @@
import { GraphQLError } from 'graphql'
import { timingSafeEqual } from 'crypto'
import retry from 'async-retry'
import Prisma from '@prisma/client'
import { settleHodlInvoice } from 'ln-service'
@ -7,30 +6,13 @@ import { createHmac } from './wallet'
import { msatsToSats, numWithUnits } from '@/lib/format'
import { BALANCE_LIMIT_MSATS } from '@/lib/constants'
export default async function serialize (trx, { models, lnd, me, hash, hmac, fee }) {
// wrap first argument in array if not array already
const isArray = Array.isArray(trx)
if (!isArray) trx = [trx]
// conditional queries can be added inline using && syntax
// we filter any falsy value out here
trx = trx.filter(q => !!q)
let invoice
if (hash) {
invoice = await verifyPayment(models, hash, hmac, fee)
trx = [
models.$executeRaw`SELECT confirm_invoice(${hash}, ${invoice.msatsReceived})`,
...trx
]
}
let results = await retry(async bail => {
export default async function serialize (models, ...calls) {
return await retry(async bail => {
try {
const [, ...results] = await models.$transaction(
[models.$executeRaw`SELECT ASSERT_SERIALIZED()`, ...trx],
const [, ...result] = await models.$transaction(
[models.$executeRaw`SELECT ASSERT_SERIALIZED()`, ...calls],
{ isolationLevel: Prisma.TransactionIsolationLevel.Serializable })
return results
return calls.length > 1 ? result : result[0]
} catch (error) {
console.log(error)
// two cases where we get insufficient funds:
@ -82,20 +64,38 @@ export default async function serialize (trx, { models, lnd, me, hash, hmac, fee
maxTimeout: 100,
retries: 10
})
if (hash) {
if (invoice?.isHeld) {
await settleHodlInvoice({ secret: invoice.preimage, lnd })
}
// remove first element since that is the confirmed invoice
results = results.slice(1)
}
// if first argument was not an array, unwrap the result
return isArray ? results : results[0]
}
async function verifyPayment (models, hash, hmac, fee) {
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 = Array.isArray(query) ? query : [query]
let invoice
if (hash) {
invoice = await checkInvoice(models, hash, hmac, enforceFee)
trx = [
models.$executeRaw`SELECT confirm_invoice(${hash}, ${invoice.msatsReceived})`,
...trx
]
}
let results = await serialize(models, ...trx)
if (hash) {
if (invoice?.isHeld) { await settleHodlInvoice({ secret: invoice.preimage, lnd }) }
// remove first element since that is the confirmed invoice
[, ...results] = results
}
// if there is only one result, return it directly, else the array
results = results.flat(2)
return results.length > 1 ? results : results[0]
}
export async function checkInvoice (models, hash, hmac, fee) {
if (!hash) {
throw new GraphQLError('hash required', { extensions: { code: 'BAD_INPUT' } })
}
@ -103,7 +103,7 @@ async function verifyPayment (models, hash, hmac, fee) {
throw new GraphQLError('hmac required', { extensions: { code: 'BAD_INPUT' } })
}
const hmac2 = createHmac(hash)
if (!timingSafeEqual(Buffer.from(hmac), Buffer.from(hmac2))) {
if (hmac !== hmac2) {
throw new GraphQLError('bad hmac', { extensions: { code: 'FORBIDDEN' } })
}

View File

@ -1,5 +1,5 @@
import { GraphQLError } from 'graphql'
import serialize from './serial'
import { serializeInvoicable } from './serial'
import { TERRITORY_COST_MONTHLY, TERRITORY_COST_ONCE, TERRITORY_COST_YEARLY, TERRITORY_PERIOD_COST } from '@/lib/constants'
import { datePivot, whenRange } from '@/lib/time'
import { ssValidate, territorySchema } from '@/lib/validate'
@ -246,9 +246,9 @@ export default {
return sub
}
const results = await serialize(
const results = await serializeInvoicable(
queries,
{ models, lnd, me, hash, hmac, fee: sub.billingCost })
{ models, lnd, hash, hmac, me, enforceFee: sub.billingCost })
return results[1]
},
toggleMuteSub: async (parent, { name }, { me, models }) => {
@ -344,9 +344,8 @@ export default {
const billPaidUntil = nextBilling(new Date(), data.billingType)
const cost = BigInt(1000) * BigInt(billingCost)
const newSub = { ...data, billPaidUntil, billingCost, userId: me.id, status: 'ACTIVE' }
const isTransfer = oldSub.userId !== me.id
await serialize([
await serializeInvoicable([
models.user.update({
where: {
id: me.id
@ -366,11 +365,11 @@ export default {
}
}),
models.sub.update({ where: { name }, data: newSub }),
isTransfer && models.territoryTransfer.create({ data: { subName: name, oldUserId: oldSub.userId, newUserId: me.id } })
],
{ models, lnd, hash, me, hmac, fee: billingCost })
oldSub.userId !== me.id && models.territoryTransfer.create({ data: { subName: name, oldUserId: oldSub.userId, newUserId: me.id } })
].filter(q => !!q),
{ models, lnd, hash, hmac, me, enforceFee: billingCost })
if (isTransfer) notifyTerritoryTransfer({ models, sub: newSub, to: me })
if (oldSub.userId !== me.id) notifyTerritoryTransfer({ models, sub: newSub, to: me })
}
},
Sub: {
@ -418,7 +417,7 @@ async function createSub (parent, data, { me, models, lnd, hash, hmac }) {
const cost = BigInt(1000) * BigInt(billingCost)
try {
const results = await serialize([
const results = await serializeInvoicable([
// bill 'em
models.user.update({
where: {
@ -457,7 +456,7 @@ async function createSub (parent, data, { me, models, lnd, hash, hmac }) {
subName: data.name
}
})
], { models, lnd, me, hash, hmac, fee: billingCost })
], { models, lnd, hash, hmac, me, enforceFee: billingCost })
return results[1]
} catch (error) {
@ -512,7 +511,7 @@ async function updateSub (parent, { oldName, ...data }, { me, models, lnd, hash,
const proratedCost = proratedBillingCost(oldSub, data.billingType)
if (proratedCost > 0) {
const cost = BigInt(1000) * BigInt(proratedCost)
const results = await serialize([
const results = await serializeInvoicable([
models.user.update({
where: {
id: me.id
@ -538,7 +537,7 @@ async function updateSub (parent, { oldName, ...data }, { me, models, lnd, hash,
userId: me.id
}
})
], { models, lnd, me, hash, hmac, fee: proratedCost })
], { models, lnd, hash, hmac, me, enforceFee: proratedCost })
return results[2]
}
}

View File

@ -354,12 +354,10 @@ export default {
expires_at: expiresAt
})
const [inv] = await serialize(
const [inv] = await serialize(models,
models.$queryRaw`SELECT * FROM create_invoice(${invoice.id}, ${hodlInvoice ? invoice.secret : null}::TEXT, ${invoice.request},
${expiresAt}::timestamp, ${amount * 1000}, ${user.id}::INTEGER, ${description}, NULL, NULL,
${invLimit}::INTEGER, ${balanceLimit})`,
{ models }
)
${invLimit}::INTEGER, ${balanceLimit})`)
// the HMAC is only returned during invoice creation
// this makes sure that only the person who created this invoice
@ -380,7 +378,7 @@ export default {
throw new GraphQLError('bad hmac', { extensions: { code: 'FORBIDDEN' } })
}
await cancelHodlInvoice({ id: hash, lnd })
const inv = await serialize(
const inv = await serialize(models,
models.invoice.update({
where: {
hash
@ -388,9 +386,7 @@ export default {
data: {
cancelled: true
}
}),
{ models }
)
}))
return inv
},
dropBolt11: async (parent, { id }, { me, models, lnd }) => {
@ -664,11 +660,9 @@ export async function createWithdrawal (parent, { invoice, maxFee }, { me, model
const user = await models.user.findUnique({ where: { id: me.id } })
// create withdrawl transactionally (id, bolt11, amount, fee)
const [withdrawl] = await serialize(
const [withdrawl] = await serialize(models,
models.$queryRaw`SELECT * FROM create_withdrawl(${decoded.id}, ${invoice},
${Number(decoded.mtokens)}, ${msatsFee}, ${user.name}, ${autoWithdraw})`,
{ models }
)
${Number(decoded.mtokens)}, ${msatsFee}, ${user.name}, ${autoWithdraw})`)
payViaPaymentRequest({
lnd,

View File

@ -80,13 +80,11 @@ export default async ({ query: { username, amount, nostr, comment, payerdata: pa
expires_at: expiresAt
})
await serialize(
await serialize(models,
models.$queryRaw`SELECT * FROM create_invoice(${invoice.id}, NULL, ${invoice.request},
${expiresAt}::timestamp, ${Number(amount)}, ${user.id}::INTEGER, ${noteStr || description},
${comment || null}, ${parsedPayerData || null}::JSONB, ${INV_PENDING_LIMIT}::INTEGER,
${USER_IDS_BALANCE_NO_LIMIT.includes(Number(user.id)) ? 0 : BALANCE_LIMIT_MSATS})`,
{ models }
)
${USER_IDS_BALANCE_NO_LIMIT.includes(Number(user.id)) ? 0 : BALANCE_LIMIT_MSATS})`)
return res.status(200).json({
pr: invoice.request,

View File

@ -36,10 +36,8 @@ export async function getServerSideProps ({ req, res, query: { id, error = null
try {
// attempt to send gift
// catch any errors and just ignore them for now
await serialize(
models.$queryRawUnsafe('SELECT invite_drain($1::INTEGER, $2::TEXT)', session.user.id, id),
{ models }
)
await serialize(models,
models.$queryRawUnsafe('SELECT invite_drain($1::INTEGER, $2::TEXT)', session.user.id, id))
const invite = await models.invite.findUnique({ where: { id } })
notifyInvite(invite.userId)
} catch (e) {

View File

@ -17,6 +17,7 @@ export async function auction ({ models }) {
// for each item, run serialized auction function
items.forEach(async item => {
await serialize(models.$executeRaw`SELECT run_auction(${item.id}::INTEGER)`, { models })
await serialize(models,
models.$executeRaw`SELECT run_auction(${item.id}::INTEGER)`)
})
}

View File

@ -79,11 +79,9 @@ export async function earn ({ name }) {
console.log('stacker', earner.userId, 'earned', earnings, 'proportion', earner.proportion, 'rank', earner.rank, 'type', earner.type)
if (earnings > 0) {
await serialize(
await serialize(models,
models.$executeRaw`SELECT earn(${earner.userId}::INTEGER, ${earnings},
${now}::timestamp without time zone, ${earner.type}::"EarnType", ${earner.id}::INTEGER, ${earner.rank}::INTEGER)`,
{ models }
)
${now}::timestamp without time zone, ${earner.type}::"EarnType", ${earner.id}::INTEGER, ${earner.rank}::INTEGER)`)
const userN = notifications[earner.userId] || {}

View File

@ -34,7 +34,7 @@ export async function territoryBilling ({ data: { subName }, boss, models }) {
try {
const queries = paySubQueries(sub, models)
await serialize(queries, { models })
await serialize(models, ...queries)
} catch (e) {
console.error(e)
await territoryStatusUpdate()
@ -42,7 +42,7 @@ export async function territoryBilling ({ data: { subName }, boss, models }) {
}
export async function territoryRevenue ({ models }) {
await serialize(
await serialize(models,
models.$executeRaw`
WITH revenue AS (
SELECT coalesce(sum(msats), 0) as revenue, "subName", "userId"
@ -69,7 +69,6 @@ export async function territoryRevenue ({ models }) {
)
UPDATE users SET msats = users.msats + "SubActResult".msats
FROM "SubActResult"
WHERE users.id = "SubActResult"."userId"`,
{ models }
WHERE users.id = "SubActResult"."userId"`
)
}

View File

@ -121,10 +121,10 @@ async function checkInvoice ({ data: { hash }, boss, models, lnd }) {
// ALSO: is_confirmed and is_held are mutually exclusive
// that is, a hold invoice will first be is_held but not is_confirmed
// and once it's settled it will be is_confirmed but not is_held
const [[{ confirm_invoice: code }]] = await serialize([
const [[{ confirm_invoice: code }]] = await serialize(models,
models.$queryRaw`SELECT confirm_invoice(${inv.id}, ${Number(inv.received_mtokens)})`,
models.invoice.update({ where: { hash }, data: { confirmedIndex: inv.confirmed_index } })
], { models })
)
// don't send notifications for JIT invoices
if (dbInv.preimage) return
@ -143,7 +143,7 @@ async function checkInvoice ({ data: { hash }, boss, models, lnd }) {
// and without setting the user balance
// those will be set when the invoice is settled by user action
const expiresAt = new Date(Math.min(dbInv.expiresAt, datePivot(new Date(), { seconds: 60 })))
return await serialize([
return await serialize(models,
models.$queryRaw`
INSERT INTO pgboss.job (name, data, retrylimit, retrybackoff, startafter)
VALUES ('finalizeHodlInvoice', jsonb_build_object('hash', ${hash}), 21, true, ${expiresAt})`,
@ -154,12 +154,11 @@ async function checkInvoice ({ data: { hash }, boss, models, lnd }) {
expiresAt,
isHeld: true
}
})
], { models })
}))
}
if (inv.is_canceled) {
return await serialize(
return await serialize(models,
models.invoice.update({
where: {
hash: inv.id
@ -167,8 +166,7 @@ async function checkInvoice ({ data: { hash }, boss, models, lnd }) {
data: {
cancelled: true
}
}), { models }
)
}))
}
}
@ -230,10 +228,8 @@ async function checkWithdrawal ({ data: { hash }, boss, models, lnd }) {
if (wdrwl?.is_confirmed) {
const fee = Number(wdrwl.payment.fee_mtokens)
const paid = Number(wdrwl.payment.mtokens) - fee
const [{ confirm_withdrawl: code }] = await serialize(
models.$queryRaw`SELECT confirm_withdrawl(${dbWdrwl.id}::INTEGER, ${paid}, ${fee})`,
{ models }
)
const [{ confirm_withdrawl: code }] = await serialize(models, models.$queryRaw`
SELECT confirm_withdrawl(${dbWdrwl.id}::INTEGER, ${paid}, ${fee})`)
if (code === 0) {
notifyWithdrawal(dbWdrwl.userId, wdrwl)
}
@ -249,10 +245,9 @@ async function checkWithdrawal ({ data: { hash }, boss, models, lnd }) {
status = 'ROUTE_NOT_FOUND'
}
await serialize(
await serialize(models,
models.$executeRaw`
SELECT reverse_withdrawl(${dbWdrwl.id}::INTEGER, ${status}::"WithdrawlStatus")`,
{ models }
SELECT reverse_withdrawl(${dbWdrwl.id}::INTEGER, ${status}::"WithdrawlStatus")`
)
}
}