Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cowboy credits (aka nov-5 (aka jan-3)) #1678

Merged
merged 46 commits into from
Jan 3, 2025
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
e0b99f3
wip adding cowboy credits
huumn Dec 3, 2024
0bd6e9c
invite gift paid action
huumn Dec 4, 2024
1ab9c15
remove balance limit
huumn Dec 4, 2024
bae8c2e
remove p2p zap withdrawal notifications
huumn Dec 4, 2024
25e4e09
Merge branch 'master' into cowboy-credits
huumn Dec 4, 2024
1928a5e
credits typedefs
huumn Dec 4, 2024
89504f0
squash migrations
huumn Dec 4, 2024
9406995
remove wallet limit stuff
huumn Dec 4, 2024
d2185ce
CCs in item detail
huumn Dec 5, 2024
e821dc9
comments with meCredits
huumn Dec 5, 2024
d789d27
Merge branch 'master' into cowboy-credits
huumn Dec 5, 2024
b307ab0
begin including CCs in item stats/notifications
huumn Dec 5, 2024
f83552a
buy credits ui/mutation
huumn Dec 6, 2024
f4e5e70
fix old /settings/wallets paths
huumn Dec 8, 2024
88b0392
bios don't get sats
huumn Dec 8, 2024
26de139
fix settings
huumn Dec 8, 2024
6d22316
make invites work with credits
huumn Dec 8, 2024
0099d76
Merge branch 'master' into cowboy-credits
huumn Dec 9, 2024
bf90bd5
Merge branch 'master' into cowboy-credits
huumn Dec 10, 2024
c076b23
restore migration from master
huumn Dec 11, 2024
0c92d8f
inform backend of send wallets on zap
huumn Dec 11, 2024
78d3514
Merge branch 'master' into cowboy-credits
huumn Dec 12, 2024
34dd419
Merge branch 'master' into cowboy-credits
huumn Dec 14, 2024
94b086d
Merge branch 'master' into cowboy-credits
huumn Dec 14, 2024
f0fab58
satistics header
huumn Dec 14, 2024
dcdd33a
default receive options to true and squash migrations
huumn Dec 15, 2024
5502c27
fix paidAction query
huumn Dec 15, 2024
c99df3d
add nav for credits
huumn Dec 15, 2024
40c078a
Merge branch 'master' into cowboy-credits
huumn Dec 17, 2024
78f1182
fix forever stacked count
huumn Dec 17, 2024
7eb0de2
Merge branch 'master' into cowboy-credits
huumn Dec 21, 2024
37021eb
ek suggested fixes
huumn Dec 22, 2024
e276d86
Merge branch 'master' into cowboy-credits
huumn Dec 28, 2024
a048008
fix lint
huumn Dec 28, 2024
70a912f
Merge branch 'master' into cowboy-credits
huumn Dec 28, 2024
b6f5417
Merge branch 'master' into cowboy-credits
huumn Dec 28, 2024
91fce73
Merge branch 'master' into cowboy-credits
huumn Jan 2, 2025
cc39790
Merge branch 'master' into cowboy-credits
huumn Jan 2, 2025
23a4cea
fix freebies wrt CCs
huumn Jan 2, 2025
5a3cdb0
add back disable freebies
huumn Jan 2, 2025
4de74b4
trigger cowboy hat job on CC depletion
huumn Jan 2, 2025
264f721
fix meMsats+meMcredits
huumn Jan 2, 2025
721e4d7
Update api/paidAction/README.md
huumn Jan 3, 2025
0fa135d
Merge branch 'master' into cowboy-credits
huumn Jan 3, 2025
18164d9
Merge branch 'master' into cowboy-credits
huumn Jan 3, 2025
d62afeb
remove expireBoost migration that doesn't work
huumn Jan 3, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.sats - item.mcredits`.
huumn marked this conversation as resolved.
Show resolved Hide resolved

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
1 change: 1 addition & 0 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
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
12 changes: 11 additions & 1 deletion 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
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
1 change: 1 addition & 0 deletions api/paidAction/itemCreate.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,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 Down
1 change: 1 addition & 0 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
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))
}`
)
}
}
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
Loading