Skip to content

Commit

Permalink
Merge branch 'master' into daily_sats_summary_notifications
Browse files Browse the repository at this point in the history
  • Loading branch information
Soxasora authored Jan 7, 2025
2 parents b23e0bb + b37a12b commit a789e7d
Show file tree
Hide file tree
Showing 71 changed files with 787 additions and 430 deletions.
6 changes: 6 additions & 0 deletions api/paidAction/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,12 @@ All functions have the following signature: `function(args: Object, context: Obj
- `models`: the current prisma client (for anything that doesn't need to be done atomically with the payment)
- `lnd`: the current lnd client

## Recording Cowboy Credits

To avoid adding sats and credits together everywhere to show an aggregate sat value, in most cases we denormalize a `sats` field that carries the "sats value", the combined sats + credits of something, and a `credits` field that carries only the earned `credits`. For example, the `Item` table has an `msats` field that carries the sum of the `mcredits` and `msats` earned and a `mcredits` field that carries the value of the `mcredits` earned. So, the sats value an item earned is `item.msats` BUT the real sats earned is `item.msats - item.mcredits`.

The ONLY exception to this are for the `users` table where we store a stacker's rewards sats and credits balances separately.

## `IMPORTANT: transaction isolation`

We use a `read committed` isolation level for actions. This means paid actions need to be mindful of concurrency issues. Specifically, reading data from the database and then writing it back in `read committed` is a common source of consistency bugs (aka serialization anamolies).
Expand Down
5 changes: 3 additions & 2 deletions api/paidAction/boost.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const anonable = false

export const paymentMethods = [
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
PAID_ACTION_PAYMENT_METHODS.OPTIMISTIC
]

Expand Down Expand Up @@ -67,9 +68,9 @@ export async function onPaid ({ invoice, actId }, { tx }) {
})

await tx.$executeRaw`
INSERT INTO pgboss.job (name, data, retrylimit, retrybackoff, startafter, expirein)
INSERT INTO pgboss.job (name, data, retrylimit, retrybackoff, startafter, keepuntil)
VALUES ('expireBoost', jsonb_build_object('id', ${itemAct.itemId}::INTEGER), 21, true,
now() + interval '30 days', interval '40 days')`
now() + interval '30 days', now() + interval '40 days')`
}

export async function onFail ({ invoice }, { tx }) {
Expand Down
32 changes: 32 additions & 0 deletions api/paidAction/buyCredits.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { PAID_ACTION_PAYMENT_METHODS } from '@/lib/constants'
import { satsToMsats } from '@/lib/format'

export const anonable = false

export const paymentMethods = [
PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
]

export async function getCost ({ credits }) {
return satsToMsats(credits)
}

export async function perform ({ credits }, { me, cost, tx }) {
await tx.user.update({
where: { id: me.id },
data: {
mcredits: {
increment: cost
}
}
})

return {
credits
}
}

export async function describe () {
return 'SN: buy fee credits'
}
1 change: 1 addition & 0 deletions api/paidAction/donate.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const anonable = true

export const paymentMethods = [
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
]

Expand Down
1 change: 1 addition & 0 deletions api/paidAction/downZap.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const anonable = false

export const paymentMethods = [
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
PAID_ACTION_PAYMENT_METHODS.OPTIMISTIC
]

Expand Down
16 changes: 13 additions & 3 deletions api/paidAction/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import * as TERRITORY_UNARCHIVE from './territoryUnarchive'
import * as DONATE from './donate'
import * as BOOST from './boost'
import * as RECEIVE from './receive'
import * as BUY_CREDITS from './buyCredits'
import * as INVITE_GIFT from './inviteGift'

export const paidActions = {
Expand All @@ -33,6 +34,7 @@ export const paidActions = {
TERRITORY_UNARCHIVE,
DONATE,
RECEIVE,
BUY_CREDITS,
INVITE_GIFT
}

Expand Down Expand Up @@ -96,7 +98,8 @@ export default async function performPaidAction (actionType, args, incomingConte

// additional payment methods that logged in users can use
if (me) {
if (paymentMethod === PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT) {
if (paymentMethod === PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT ||
paymentMethod === PAID_ACTION_PAYMENT_METHODS.REWARD_SATS) {
try {
return await performNoInvoiceAction(actionType, args, contextWithPaymentMethod)
} catch (e) {
Expand Down Expand Up @@ -141,6 +144,13 @@ async function performNoInvoiceAction (actionType, args, incomingContext) {
const context = { ...incomingContext, tx }

if (paymentMethod === 'FEE_CREDIT') {
await tx.user.update({
where: {
id: me?.id ?? USER_ID.anon
},
data: { mcredits: { decrement: cost } }
})
} else if (paymentMethod === PAID_ACTION_PAYMENT_METHODS.REWARD_SATS) {
await tx.user.update({
where: {
id: me?.id ?? USER_ID.anon
Expand Down Expand Up @@ -461,11 +471,11 @@ async function createDbInvoice (actionType, args, context) {

// insert a job to check the invoice after it's set to expire
await db.$executeRaw`
INSERT INTO pgboss.job (name, data, retrylimit, retrybackoff, startafter, expirein, priority)
INSERT INTO pgboss.job (name, data, retrylimit, retrybackoff, startafter, keepuntil, priority)
VALUES ('checkInvoice',
jsonb_build_object('hash', ${invoice.hash}::TEXT), 21, true,
${expiresAt}::TIMESTAMP WITH TIME ZONE,
${expiresAt}::TIMESTAMP WITH TIME ZONE - now() + interval '10m', 100)`
${expiresAt}::TIMESTAMP WITH TIME ZONE + interval '10m', 100)`

// the HMAC is only returned during invoice creation
// this makes sure that only the person who created this invoice
Expand Down
5 changes: 3 additions & 2 deletions api/paidAction/inviteGift.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import { notifyInvite } from '@/lib/webPush'
export const anonable = false

export const paymentMethods = [
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
PAID_ACTION_PAYMENT_METHODS.REWARD_SATS
]

export async function getCost ({ id }, { models, me }) {
Expand Down Expand Up @@ -36,7 +37,7 @@ export async function perform ({ id, userId }, { me, cost, tx }) {
}
},
data: {
msats: {
mcredits: {
increment: cost
},
inviteId: id,
Expand Down
7 changes: 4 additions & 3 deletions api/paidAction/itemCreate.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const anonable = true

export const paymentMethods = [
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
PAID_ACTION_PAYMENT_METHODS.OPTIMISTIC,
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
]
Expand All @@ -29,7 +30,7 @@ export async function getCost ({ subName, parentId, uploadIds, boost = 0, bio },
// sub allows freebies (or is a bio or a comment), cost is less than baseCost, not anon,
// cost must be greater than user's balance, and user has not disabled freebies
const freebie = (parentId || bio) && cost <= baseCost && !!me &&
cost > me?.msats && !me?.disableFreebies
me?.msats < cost && !me?.disableFreebies && me?.mcredits < cost

return freebie ? BigInt(0) : BigInt(cost)
}
Expand Down Expand Up @@ -216,9 +217,9 @@ export async function onPaid ({ invoice, id }, context) {

if (item.boost > 0) {
await tx.$executeRaw`
INSERT INTO pgboss.job (name, data, retrylimit, retrybackoff, startafter, expirein)
INSERT INTO pgboss.job (name, data, retrylimit, retrybackoff, startafter, keepuntil)
VALUES ('expireBoost', jsonb_build_object('id', ${item.id}::INTEGER), 21, true,
now() + interval '30 days', interval '40 days')`
now() + interval '30 days', now() + interval '40 days')`
}

if (item.parentId) {
Expand Down
9 changes: 5 additions & 4 deletions api/paidAction/itemUpdate.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export const anonable = true

export const paymentMethods = [
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
]

Expand Down Expand Up @@ -137,15 +138,15 @@ export async function perform (args, context) {
})

await tx.$executeRaw`
INSERT INTO pgboss.job (name, data, retrylimit, retrybackoff, startafter, expirein)
INSERT INTO pgboss.job (name, data, retrylimit, retrybackoff, startafter, keepuntil)
VALUES ('imgproxy', jsonb_build_object('id', ${id}::INTEGER), 21, true,
now() + interval '5 seconds', interval '1 day')`
now() + interval '5 seconds', now() + interval '1 day')`

if (newBoost > 0) {
await tx.$executeRaw`
INSERT INTO pgboss.job (name, data, retrylimit, retrybackoff, startafter, expirein)
INSERT INTO pgboss.job (name, data, retrylimit, retrybackoff, startafter, keepuntil)
VALUES ('expireBoost', jsonb_build_object('id', ${id}::INTEGER), 21, true,
now() + interval '30 days', interval '40 days')`
now() + interval '30 days', now() + interval '40 days')`
}

await performBotBehavior(args, context)
Expand Down
48 changes: 1 addition & 47 deletions api/paidAction/lib/assert.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { BALANCE_LIMIT_MSATS, PAID_ACTION_TERMINAL_STATES, USER_ID, SN_ADMIN_IDS } from '@/lib/constants'
import { msatsToSats, numWithUnits } from '@/lib/format'
import { PAID_ACTION_TERMINAL_STATES, USER_ID } from '@/lib/constants'
import { datePivot } from '@/lib/time'

const MAX_PENDING_PAID_ACTIONS_PER_USER = 100
const MAX_PENDING_DIRECT_INVOICES_PER_USER_MINUTES = 10
const MAX_PENDING_DIRECT_INVOICES_PER_USER = 100
const USER_IDS_BALANCE_NO_LIMIT = [...SN_ADMIN_IDS, USER_ID.anon, USER_ID.ad]

export async function assertBelowMaxPendingInvoices (context) {
const { models, me } = context
Expand Down Expand Up @@ -56,47 +54,3 @@ export async function assertBelowMaxPendingDirectPayments (userId, context) {
throw new Error('Receiver has too many direct payments')
}
}

export async function assertBelowBalanceLimit (context) {
const { me, tx } = context
if (!me || USER_IDS_BALANCE_NO_LIMIT.includes(me.id)) return

// we need to prevent this invoice (and any other pending invoices and withdrawls)
// from causing the user's balance to exceed the balance limit
const pendingInvoices = await tx.invoice.aggregate({
where: {
userId: me.id,
// p2p invoices are never in state PENDING
actionState: 'PENDING',
actionType: 'RECEIVE'
},
_sum: {
msatsRequested: true
}
})

// Get pending withdrawals total
const pendingWithdrawals = await tx.withdrawl.aggregate({
where: {
userId: me.id,
status: null
},
_sum: {
msatsPaying: true,
msatsFeePaying: true
}
})

// Calculate total pending amount
const pendingMsats = (pendingInvoices._sum.msatsRequested ?? 0n) +
((pendingWithdrawals._sum.msatsPaying ?? 0n) + (pendingWithdrawals._sum.msatsFeePaying ?? 0n))

// Check balance limit
if (pendingMsats + me.msats > BALANCE_LIMIT_MSATS) {
throw new Error(
`pending invoices and withdrawals must not cause balance to exceed ${
numWithUnits(msatsToSats(BALANCE_LIMIT_MSATS))
}`
)
}
}
8 changes: 4 additions & 4 deletions api/paidAction/lib/item.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,23 +60,23 @@ export async function performBotBehavior ({ text, id }, { me, tx }) {
const deleteAt = getDeleteAt(text)
if (deleteAt) {
await tx.$queryRaw`
INSERT INTO pgboss.job (name, data, startafter, expirein)
INSERT INTO pgboss.job (name, data, startafter, keepuntil)
VALUES (
'deleteItem',
jsonb_build_object('id', ${id}::INTEGER),
${deleteAt}::TIMESTAMP WITH TIME ZONE,
${deleteAt}::TIMESTAMP WITH TIME ZONE - now() + interval '1 minute')`
${deleteAt}::TIMESTAMP WITH TIME ZONE + interval '1 minute')`
}

const remindAt = getRemindAt(text)
if (remindAt) {
await tx.$queryRaw`
INSERT INTO pgboss.job (name, data, startafter, expirein)
INSERT INTO pgboss.job (name, data, startafter, keepuntil)
VALUES (
'reminder',
jsonb_build_object('itemId', ${id}::INTEGER, 'userId', ${userId}::INTEGER),
${remindAt}::TIMESTAMP WITH TIME ZONE,
${remindAt}::TIMESTAMP WITH TIME ZONE - now() + interval '1 minute')`
${remindAt}::TIMESTAMP WITH TIME ZONE + interval '1 minute')`
await tx.reminder.create({
data: {
userId,
Expand Down
1 change: 1 addition & 0 deletions api/paidAction/pollVote.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const anonable = false

export const paymentMethods = [
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
PAID_ACTION_PAYMENT_METHODS.OPTIMISTIC
]

Expand Down
15 changes: 6 additions & 9 deletions api/paidAction/receive.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { PAID_ACTION_PAYMENT_METHODS } from '@/lib/constants'
import { toPositiveBigInt, numWithUnits, msatsToSats, satsToMsats } from '@/lib/format'
import { notifyDeposit } from '@/lib/webPush'
import { getInvoiceableWallets } from '@/wallets/server'
import { assertBelowBalanceLimit } from './lib/assert'

export const anonable = false

Expand All @@ -19,13 +18,16 @@ export async function getCost ({ msats }) {
export async function getInvoiceablePeer (_, { me, models, cost, paymentMethod }) {
if (paymentMethod === PAID_ACTION_PAYMENT_METHODS.P2P && !me?.proxyReceive) return null
if (paymentMethod === PAID_ACTION_PAYMENT_METHODS.DIRECT && !me?.directReceive) return null
if ((cost + me.msats) <= satsToMsats(me.autoWithdrawThreshold)) return null

const wallets = await getInvoiceableWallets(me.id, { models })
if (wallets.length === 0) {
return null
}

if (cost < satsToMsats(me.receiveCreditsBelowSats)) {
return null
}

return me.id
}

Expand All @@ -39,7 +41,7 @@ export async function perform ({
lud18Data,
noteStr
}, { me, tx }) {
const invoice = await tx.invoice.update({
return await tx.invoice.update({
where: { id: invoiceId },
data: {
comment,
Expand All @@ -48,11 +50,6 @@ export async function perform ({
},
include: { invoiceForward: true }
})

if (!invoice.invoiceForward) {
// if the invoice is not p2p, assert that the user's balance limit is not exceeded
await assertBelowBalanceLimit({ me, tx })
}
}

export async function describe ({ description }, { me, cost, paymentMethod, sybilFeePercent }) {
Expand All @@ -73,7 +70,7 @@ export async function onPaid ({ invoice }, { tx }) {
await tx.user.update({
where: { id: invoice.userId },
data: {
msats: {
mcredits: {
increment: invoice.msatsReceived
}
}
Expand Down
1 change: 1 addition & 0 deletions api/paidAction/territoryBilling.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const anonable = false

export const paymentMethods = [
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
]

Expand Down
1 change: 1 addition & 0 deletions api/paidAction/territoryCreate.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const anonable = false

export const paymentMethods = [
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
]

Expand Down
1 change: 1 addition & 0 deletions api/paidAction/territoryUnarchive.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const anonable = false

export const paymentMethods = [
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
]

Expand Down
1 change: 1 addition & 0 deletions api/paidAction/territoryUpdate.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const anonable = false

export const paymentMethods = [
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
]

Expand Down
Loading

0 comments on commit a789e7d

Please sign in to comment.