Skip to content

Commit 5cd447b

Browse files
committed
Merge remote-tracking branch 'upstream/master' into bolt12a
2 parents 287e114 + 29c31e3 commit 5cd447b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

70 files changed

+784
-427
lines changed

Diff for: api/paidAction/README.md

+6
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,12 @@ All functions have the following signature: `function(args: Object, context: Obj
194194
- `models`: the current prisma client (for anything that doesn't need to be done atomically with the payment)
195195
- `lnd`: the current lnd client
196196

197+
## Recording Cowboy Credits
198+
199+
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`.
200+
201+
The ONLY exception to this are for the `users` table where we store a stacker's rewards sats and credits balances separately.
202+
197203
## `IMPORTANT: transaction isolation`
198204

199205
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).

Diff for: api/paidAction/boost.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export const anonable = false
55

66
export const paymentMethods = [
77
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
8+
PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
89
PAID_ACTION_PAYMENT_METHODS.OPTIMISTIC
910
]
1011

@@ -67,9 +68,9 @@ export async function onPaid ({ invoice, actId }, { tx }) {
6768
})
6869

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

7576
export async function onFail ({ invoice }, { tx }) {

Diff for: api/paidAction/buyCredits.js

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { PAID_ACTION_PAYMENT_METHODS } from '@/lib/constants'
2+
import { satsToMsats } from '@/lib/format'
3+
4+
export const anonable = false
5+
6+
export const paymentMethods = [
7+
PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
8+
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
9+
]
10+
11+
export async function getCost ({ credits }) {
12+
return satsToMsats(credits)
13+
}
14+
15+
export async function perform ({ credits }, { me, cost, tx }) {
16+
await tx.user.update({
17+
where: { id: me.id },
18+
data: {
19+
mcredits: {
20+
increment: cost
21+
}
22+
}
23+
})
24+
25+
return {
26+
credits
27+
}
28+
}
29+
30+
export async function describe () {
31+
return 'SN: buy fee credits'
32+
}

Diff for: api/paidAction/donate.js

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export const anonable = true
55

66
export const paymentMethods = [
77
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
8+
PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
89
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
910
]
1011

Diff for: api/paidAction/downZap.js

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export const anonable = false
55

66
export const paymentMethods = [
77
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
8+
PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
89
PAID_ACTION_PAYMENT_METHODS.OPTIMISTIC
910
]
1011

Diff for: api/paidAction/index.js

+13-3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import * as TERRITORY_UNARCHIVE from './territoryUnarchive'
1919
import * as DONATE from './donate'
2020
import * as BOOST from './boost'
2121
import * as RECEIVE from './receive'
22+
import * as BUY_CREDITS from './buyCredits'
2223
import * as INVITE_GIFT from './inviteGift'
2324

2425
export const paidActions = {
@@ -34,6 +35,7 @@ export const paidActions = {
3435
TERRITORY_UNARCHIVE,
3536
DONATE,
3637
RECEIVE,
38+
BUY_CREDITS,
3739
INVITE_GIFT
3840
}
3941

@@ -97,7 +99,8 @@ export default async function performPaidAction (actionType, args, incomingConte
9799

98100
// additional payment methods that logged in users can use
99101
if (me) {
100-
if (paymentMethod === PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT) {
102+
if (paymentMethod === PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT ||
103+
paymentMethod === PAID_ACTION_PAYMENT_METHODS.REWARD_SATS) {
101104
try {
102105
return await performNoInvoiceAction(actionType, args, contextWithPaymentMethod)
103106
} catch (e) {
@@ -142,6 +145,13 @@ async function performNoInvoiceAction (actionType, args, incomingContext) {
142145
const context = { ...incomingContext, tx }
143146

144147
if (paymentMethod === 'FEE_CREDIT') {
148+
await tx.user.update({
149+
where: {
150+
id: me?.id ?? USER_ID.anon
151+
},
152+
data: { mcredits: { decrement: cost } }
153+
})
154+
} else if (paymentMethod === PAID_ACTION_PAYMENT_METHODS.REWARD_SATS) {
145155
await tx.user.update({
146156
where: {
147157
id: me?.id ?? USER_ID.anon
@@ -464,11 +474,11 @@ async function createDbInvoice (actionType, args, context) {
464474

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

473483
// the HMAC is only returned during invoice creation
474484
// this makes sure that only the person who created this invoice

Diff for: api/paidAction/inviteGift.js

+3-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import { notifyInvite } from '@/lib/webPush'
55
export const anonable = false
66

77
export const paymentMethods = [
8-
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT
8+
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
9+
PAID_ACTION_PAYMENT_METHODS.REWARD_SATS
910
]
1011

1112
export async function getCost ({ id }, { models, me }) {
@@ -36,7 +37,7 @@ export async function perform ({ id, userId }, { me, cost, tx }) {
3637
}
3738
},
3839
data: {
39-
msats: {
40+
mcredits: {
4041
increment: cost
4142
},
4243
inviteId: id,

Diff for: api/paidAction/itemCreate.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export const anonable = true
88

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

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

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

224225
if (item.parentId) {

Diff for: api/paidAction/itemUpdate.js

+5-4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export const anonable = true
88

99
export const paymentMethods = [
1010
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
11+
PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
1112
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
1213
]
1314

@@ -137,15 +138,15 @@ export async function perform (args, context) {
137138
})
138139

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

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

151152
await performBotBehavior(args, context)

Diff for: api/paidAction/lib/assert.js

+1-47
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
import { BALANCE_LIMIT_MSATS, PAID_ACTION_TERMINAL_STATES, USER_ID, SN_ADMIN_IDS } from '@/lib/constants'
2-
import { msatsToSats, numWithUnits } from '@/lib/format'
1+
import { PAID_ACTION_TERMINAL_STATES, USER_ID } from '@/lib/constants'
32
import { datePivot } from '@/lib/time'
43

54
const MAX_PENDING_PAID_ACTIONS_PER_USER = 100
65
const MAX_PENDING_DIRECT_INVOICES_PER_USER_MINUTES = 10
76
const MAX_PENDING_DIRECT_INVOICES_PER_USER = 100
8-
const USER_IDS_BALANCE_NO_LIMIT = [...SN_ADMIN_IDS, USER_ID.anon, USER_ID.ad]
97

108
export async function assertBelowMaxPendingInvoices (context) {
119
const { models, me } = context
@@ -56,47 +54,3 @@ export async function assertBelowMaxPendingDirectPayments (userId, context) {
5654
throw new Error('Receiver has too many direct payments')
5755
}
5856
}
59-
60-
export async function assertBelowBalanceLimit (context) {
61-
const { me, tx } = context
62-
if (!me || USER_IDS_BALANCE_NO_LIMIT.includes(me.id)) return
63-
64-
// we need to prevent this invoice (and any other pending invoices and withdrawls)
65-
// from causing the user's balance to exceed the balance limit
66-
const pendingInvoices = await tx.invoice.aggregate({
67-
where: {
68-
userId: me.id,
69-
// p2p invoices are never in state PENDING
70-
actionState: 'PENDING',
71-
actionType: 'RECEIVE'
72-
},
73-
_sum: {
74-
msatsRequested: true
75-
}
76-
})
77-
78-
// Get pending withdrawals total
79-
const pendingWithdrawals = await tx.withdrawl.aggregate({
80-
where: {
81-
userId: me.id,
82-
status: null
83-
},
84-
_sum: {
85-
msatsPaying: true,
86-
msatsFeePaying: true
87-
}
88-
})
89-
90-
// Calculate total pending amount
91-
const pendingMsats = (pendingInvoices._sum.msatsRequested ?? 0n) +
92-
((pendingWithdrawals._sum.msatsPaying ?? 0n) + (pendingWithdrawals._sum.msatsFeePaying ?? 0n))
93-
94-
// Check balance limit
95-
if (pendingMsats + me.msats > BALANCE_LIMIT_MSATS) {
96-
throw new Error(
97-
`pending invoices and withdrawals must not cause balance to exceed ${
98-
numWithUnits(msatsToSats(BALANCE_LIMIT_MSATS))
99-
}`
100-
)
101-
}
102-
}

Diff for: api/paidAction/lib/item.js

+4-4
Original file line numberDiff line numberDiff line change
@@ -60,23 +60,23 @@ export async function performBotBehavior ({ text, id }, { me, tx }) {
6060
const deleteAt = getDeleteAt(text)
6161
if (deleteAt) {
6262
await tx.$queryRaw`
63-
INSERT INTO pgboss.job (name, data, startafter, expirein)
63+
INSERT INTO pgboss.job (name, data, startafter, keepuntil)
6464
VALUES (
6565
'deleteItem',
6666
jsonb_build_object('id', ${id}::INTEGER),
6767
${deleteAt}::TIMESTAMP WITH TIME ZONE,
68-
${deleteAt}::TIMESTAMP WITH TIME ZONE - now() + interval '1 minute')`
68+
${deleteAt}::TIMESTAMP WITH TIME ZONE + interval '1 minute')`
6969
}
7070

7171
const remindAt = getRemindAt(text)
7272
if (remindAt) {
7373
await tx.$queryRaw`
74-
INSERT INTO pgboss.job (name, data, startafter, expirein)
74+
INSERT INTO pgboss.job (name, data, startafter, keepuntil)
7575
VALUES (
7676
'reminder',
7777
jsonb_build_object('itemId', ${id}::INTEGER, 'userId', ${userId}::INTEGER),
7878
${remindAt}::TIMESTAMP WITH TIME ZONE,
79-
${remindAt}::TIMESTAMP WITH TIME ZONE - now() + interval '1 minute')`
79+
${remindAt}::TIMESTAMP WITH TIME ZONE + interval '1 minute')`
8080
await tx.reminder.create({
8181
data: {
8282
userId,

Diff for: api/paidAction/pollVote.js

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export const anonable = false
55

66
export const paymentMethods = [
77
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
8+
PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
89
PAID_ACTION_PAYMENT_METHODS.OPTIMISTIC
910
]
1011

Diff for: api/paidAction/receive.js

+6-9
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { PAID_ACTION_PAYMENT_METHODS } from '@/lib/constants'
22
import { toPositiveBigInt, numWithUnits, msatsToSats, satsToMsats } from '@/lib/format'
33
import { notifyDeposit } from '@/lib/webPush'
44
import { getInvoiceableWallets } from '@/wallets/server'
5-
import { assertBelowBalanceLimit } from './lib/assert'
65

76
export const anonable = false
87

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

2422
const wallets = await getInvoiceableWallets(me.id, { models })
2523
if (wallets.length === 0) {
2624
return null
2725
}
2826

27+
if (cost < satsToMsats(me.receiveCreditsBelowSats)) {
28+
return null
29+
}
30+
2931
return me.id
3032
}
3133

@@ -39,7 +41,7 @@ export async function perform ({
3941
lud18Data,
4042
noteStr
4143
}, { me, tx }) {
42-
const invoice = await tx.invoice.update({
44+
return await tx.invoice.update({
4345
where: { id: invoiceId },
4446
data: {
4547
comment,
@@ -48,11 +50,6 @@ export async function perform ({
4850
},
4951
include: { invoiceForward: true }
5052
})
51-
52-
if (!invoice.invoiceForward) {
53-
// if the invoice is not p2p, assert that the user's balance limit is not exceeded
54-
await assertBelowBalanceLimit({ me, tx })
55-
}
5653
}
5754

5855
export async function describe ({ description }, { me, cost, paymentMethod, sybilFeePercent }) {
@@ -73,7 +70,7 @@ export async function onPaid ({ invoice }, { tx }) {
7370
await tx.user.update({
7471
where: { id: invoice.userId },
7572
data: {
76-
msats: {
73+
mcredits: {
7774
increment: invoice.msatsReceived
7875
}
7976
}

Diff for: api/paidAction/territoryBilling.js

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export const anonable = false
66

77
export const paymentMethods = [
88
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
9+
PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
910
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
1011
]
1112

Diff for: api/paidAction/territoryCreate.js

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export const anonable = false
66

77
export const paymentMethods = [
88
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
9+
PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
910
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
1011
]
1112

Diff for: api/paidAction/territoryUnarchive.js

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export const anonable = false
66

77
export const paymentMethods = [
88
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
9+
PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
910
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
1011
]
1112

Diff for: api/paidAction/territoryUpdate.js

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const anonable = false
77

88
export const paymentMethods = [
99
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
10+
PAID_ACTION_PAYMENT_METHODS.REWARD_SATS,
1011
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
1112
]
1213

0 commit comments

Comments
 (0)