Compare commits

...

4 Commits

Author SHA1 Message Date
ekzyis b7fc0e0e74
Fix fee added to received amount (#1582) 2024-11-12 19:50:15 -06:00
ekzyis 08c29b9f10
Fix missing sorting of wallet logs (#1581) 2024-11-12 19:48:21 -06:00
ekzyis 3044c2e98f
Fix outlawed can show up on front page (#1565)
If an item has a lot of comments, it gets ranked higher.

When an item is now downzapped, it can become outlawed but still show up on the front page because of the comments.

This fixes it by filtering outlawed items out instead of relying on the ranking to make them not show up.
2024-11-12 19:47:14 -06:00
Keyan a44d0daf09
paid action payment methods as an array (#1584)
* introduce fee credits & allow paid actions to specify payment method priority

* fix merge issue

* express supported paid action payment methods as an array

* log force payment method skipping methods

* fix stuff

* immutable context

* immutable paidAction context and other fixes

---------

Co-authored-by: Riccardo Balbo <riccardo0blb@gmail.com>
2024-11-12 19:00:51 -06:00
24 changed files with 268 additions and 184 deletions

View File

@ -139,8 +139,14 @@ Each paid action is implemented in its own file in the `paidAction` directory. E
### Boolean flags
- `anonable`: can be performed anonymously
- `supportsPessimism`: supports a pessimistic payment flow
- `supportsOptimism`: supports an optimistic payment flow
### Payment methods
- `paymentMethods`: an array of payment methods that the action supports ordered from most preferred to least preferred
- P2P: a p2p payment made directly from the client to the recipient
- after wrapping the invoice, anonymous users will follow a PESSIMISTIC flow to pay the invoice and logged in users will follow an OPTIMISTIC flow
- FEE_CREDIT: a payment made from the user's fee credit balance
- OPTIMISTIC: an optimistic payment flow
- PESSIMISTIC: a pessimistic payment flow
### Functions

View File

@ -1,8 +1,12 @@
import { PAID_ACTION_PAYMENT_METHODS } from '@/lib/constants'
import { msatsToSats, satsToMsats } from '@/lib/format'
export const anonable = false
export const supportsPessimism = false
export const supportsOptimism = true
export const paymentMethods = [
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
PAID_ACTION_PAYMENT_METHODS.OPTIMISTIC
]
export async function getCost ({ sats }) {
return satsToMsats(sats)

View File

@ -1,26 +0,0 @@
// XXX we don't use this yet ...
// it's just showing that even buying credits
// can eventually be a paid action
import { USER_ID } from '@/lib/constants'
import { satsToMsats } from '@/lib/format'
export const anonable = false
export const supportsPessimism = false
export const supportsOptimism = true
export async function getCost ({ amount }) {
return satsToMsats(amount)
}
export async function onPaid ({ invoice }, { tx }) {
return await tx.users.update({
where: { id: invoice.userId },
data: { balance: { increment: invoice.msatsReceived } }
})
}
export async function describe ({ amount }, { models, me }) {
const user = await models.user.findUnique({ where: { id: me?.id ?? USER_ID.anon } })
return `SN: buying credits for @${user.name}`
}

View File

@ -1,9 +1,12 @@
import { USER_ID } from '@/lib/constants'
import { PAID_ACTION_PAYMENT_METHODS, USER_ID } from '@/lib/constants'
import { satsToMsats } from '@/lib/format'
export const anonable = true
export const supportsPessimism = true
export const supportsOptimism = false
export const paymentMethods = [
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
]
export async function getCost ({ sats }) {
return satsToMsats(sats)

View File

@ -1,8 +1,12 @@
import { PAID_ACTION_PAYMENT_METHODS } from '@/lib/constants'
import { msatsToSats, satsToMsats } from '@/lib/format'
export const anonable = false
export const supportsPessimism = false
export const supportsOptimism = true
export const paymentMethods = [
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
PAID_ACTION_PAYMENT_METHODS.OPTIMISTIC
]
export async function getCost ({ sats }) {
return satsToMsats(sats)

View File

@ -1,6 +1,6 @@
import { createHodlInvoice, createInvoice, parsePaymentRequest } from 'ln-service'
import { datePivot } from '@/lib/time'
import { PAID_ACTION_TERMINAL_STATES, USER_ID } from '@/lib/constants'
import { PAID_ACTION_PAYMENT_METHODS, PAID_ACTION_TERMINAL_STATES, USER_ID } from '@/lib/constants'
import { createHmac } from '@/api/resolvers/wallet'
import { Prisma } from '@prisma/client'
import * as ITEM_CREATE from './itemCreate'
@ -30,9 +30,9 @@ export const paidActions = {
DONATE
}
export default async function performPaidAction (actionType, args, context) {
export default async function performPaidAction (actionType, args, incomingContext) {
try {
const { me, models, forceFeeCredits } = context
const { me, models, forcePaymentMethod } = incomingContext
const paidAction = paidActions[actionType]
console.group('performPaidAction', actionType, args)
@ -41,52 +41,71 @@ export default async function performPaidAction (actionType, args, context) {
throw new Error(`Invalid action type ${actionType}`)
}
context.me = me ? await models.user.findUnique({ where: { id: me.id } }) : undefined
context.cost = await paidAction.getCost(args, context)
context.sybilFeePercent = await paidAction.getSybilFeePercent?.(args, context)
if (!me) {
if (!paidAction.anonable) {
throw new Error('You must be logged in to perform this action')
}
if (context.cost > 0) {
console.log('we are anon so can only perform pessimistic action that require payment')
return await performPessimisticAction(actionType, args, context)
}
if (!me && !paidAction.anonable) {
throw new Error('You must be logged in to perform this action')
}
const isRich = context.cost <= (context.me?.msats ?? 0)
if (isRich) {
try {
console.log('enough fee credits available, performing fee credit action')
return await performFeeCreditAction(actionType, args, context)
} catch (e) {
console.error('fee credit action failed', e)
// treat context as immutable
const contextWithMe = {
...incomingContext,
me: me ? await models.user.findUnique({ where: { id: me.id } }) : undefined
}
const context = {
...contextWithMe,
cost: await paidAction.getCost(args, contextWithMe),
sybilFeePercent: await paidAction.getSybilFeePercent?.(args, contextWithMe)
}
// if we fail with fee credits, but not because of insufficient funds, bail
if (!e.message.includes('\\"users\\" violates check constraint \\"msats_positive\\"')) {
// special case for zero cost actions
if (context.cost === 0n) {
console.log('performing zero cost action')
return await performNoInvoiceAction(actionType, args, { ...context, paymentMethod: 'ZERO_COST' })
}
for (const paymentMethod of paidAction.paymentMethods) {
console.log(`considering payment method ${paymentMethod}`)
if (forcePaymentMethod &&
paymentMethod !== forcePaymentMethod) {
console.log('skipping payment method', paymentMethod, 'because forcePaymentMethod is set to', forcePaymentMethod)
continue
}
// payment methods that anonymous users can use
if (paymentMethod === PAID_ACTION_PAYMENT_METHODS.P2P) {
try {
return await performP2PAction(actionType, args, context)
} catch (e) {
if (e instanceof NonInvoiceablePeerError) {
console.log('peer cannot be invoiced, skipping')
continue
}
console.error(`${paymentMethod} action failed`, e)
throw e
}
} else if (paymentMethod === PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC) {
return await beginPessimisticAction(actionType, args, context)
}
// additionalpayment methods that logged in users can use
if (me) {
if (paymentMethod === PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT) {
try {
return await performNoInvoiceAction(actionType, args, { ...context, paymentMethod })
} catch (e) {
// if we fail with fee credits or reward sats, but not because of insufficient funds, bail
console.error(`${paymentMethod} action failed`, e)
if (!e.message.includes('\\"users\\" violates check constraint \\"msats_positive\\"')) {
throw e
}
}
} else if (paymentMethod === PAID_ACTION_PAYMENT_METHODS.OPTIMISTIC) {
return await performOptimisticAction(actionType, args, context)
}
}
}
// this is set if the worker executes a paid action in behalf of a user.
// in that case, only payment via fee credits is possible
// since there is no client to which we could send an invoice.
// example: automated territory billing
if (forceFeeCredits) {
throw new Error('forceFeeCredits is set, but user does not have enough fee credits')
}
// if we fail to do the action with fee credits, we should fall back to optimistic
if (paidAction.supportsOptimism) {
console.log('performing optimistic action')
return await performOptimisticAction(actionType, args, context)
}
console.error('action does not support optimism and fee credits failed, performing pessimistic action')
return await performPessimisticAction(actionType, args, context)
throw new Error('No working payment method found')
} catch (e) {
console.error('performPaidAction failed', e)
throw e
@ -95,50 +114,48 @@ export default async function performPaidAction (actionType, args, context) {
}
}
async function performFeeCreditAction (actionType, args, context) {
const { me, models, cost } = context
async function performNoInvoiceAction (actionType, args, incomingContext) {
const { me, models, cost, paymentMethod } = incomingContext
const action = paidActions[actionType]
const result = await models.$transaction(async tx => {
context.tx = tx
const context = { ...incomingContext, tx }
await tx.user.update({
where: {
id: me?.id ?? USER_ID.anon
},
data: {
msats: {
decrement: cost
}
}
})
if (paymentMethod === 'FEE_CREDIT') {
await tx.user.update({
where: {
id: me?.id ?? USER_ID.anon
},
data: { msats: { decrement: cost } }
})
}
const result = await action.perform(args, context)
await action.onPaid?.(result, context)
return {
result,
paymentMethod: 'FEE_CREDIT'
paymentMethod
}
}, { isolationLevel: Prisma.TransactionIsolationLevel.ReadCommitted })
// run non critical side effects in the background
// after the transaction has been committed
action.nonCriticalSideEffects?.(result.result, context).catch(console.error)
action.nonCriticalSideEffects?.(result.result, incomingContext).catch(console.error)
return result
}
async function performOptimisticAction (actionType, args, context) {
const { models } = context
async function performOptimisticAction (actionType, args, incomingContext) {
const { models, invoiceArgs: incomingInvoiceArgs } = incomingContext
const action = paidActions[actionType]
context.optimistic = true
const invoiceArgs = await createLightningInvoice(actionType, args, context)
const optimisticContext = { ...incomingContext, optimistic: true }
const invoiceArgs = incomingInvoiceArgs ?? await createSNInvoice(actionType, args, optimisticContext)
return await models.$transaction(async tx => {
context.tx = tx
const context = { ...optimisticContext, tx, invoiceArgs }
const invoice = await createDbInvoice(actionType, args, context, invoiceArgs)
const invoice = await createDbInvoice(actionType, args, context)
return {
invoice,
@ -148,23 +165,61 @@ async function performOptimisticAction (actionType, args, context) {
}, { isolationLevel: Prisma.TransactionIsolationLevel.ReadCommitted })
}
async function performPessimisticAction (actionType, args, context) {
async function beginPessimisticAction (actionType, args, context) {
const action = paidActions[actionType]
if (!action.supportsPessimism) {
if (!action.paymentMethods.includes(PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC)) {
throw new Error(`This action ${actionType} does not support pessimistic invoicing`)
}
// just create the invoice and complete action when it's paid
const invoiceArgs = await createLightningInvoice(actionType, args, context)
const invoiceArgs = context.invoiceArgs ?? await createSNInvoice(actionType, args, context)
return {
invoice: await createDbInvoice(actionType, args, context, invoiceArgs),
invoice: await createDbInvoice(actionType, args, { ...context, invoiceArgs }),
paymentMethod: 'PESSIMISTIC'
}
}
export async function retryPaidAction (actionType, args, context) {
const { models, me } = context
async function performP2PAction (actionType, args, incomingContext) {
// if the action has an invoiceable peer, we'll create a peer invoice
// wrap it, and return the wrapped invoice
const { cost, models, lnd, sybilFeePercent, me } = incomingContext
if (!sybilFeePercent) {
throw new Error('sybil fee percent is not set for an invoiceable peer action')
}
const userId = await paidActions[actionType]?.getInvoiceablePeer?.(args, incomingContext)
if (!userId) {
throw new NonInvoiceablePeerError()
}
await assertBelowMaxPendingInvoices(incomingContext)
const description = await paidActions[actionType].describe(args, incomingContext)
const { invoice, wrappedInvoice, wallet, maxFee } = await createWrappedInvoice(userId, {
msats: cost,
feePercent: sybilFeePercent,
description,
expiry: INVOICE_EXPIRE_SECS
}, { models, me, lnd })
const context = {
...incomingContext,
invoiceArgs: {
bolt11: invoice,
wrappedBolt11: wrappedInvoice,
wallet,
maxFee
}
}
return me
? await performOptimisticAction(actionType, args, context)
: await beginPessimisticAction(actionType, args, context)
}
export async function retryPaidAction (actionType, args, incomingContext) {
const { models, me } = incomingContext
const { invoice: failedInvoice } = args
console.log('retryPaidAction', actionType, args)
@ -178,7 +233,7 @@ export async function retryPaidAction (actionType, args, context) {
throw new Error(`retryPaidAction - must be logged in ${actionType}`)
}
if (!action.supportsOptimism) {
if (!action.paymentMethods.includes(PAID_ACTION_PAYMENT_METHODS.OPTIMISTIC)) {
throw new Error(`retryPaidAction - action does not support optimism ${actionType}`)
}
@ -190,16 +245,19 @@ export async function retryPaidAction (actionType, args, context) {
throw new Error(`retryPaidAction - missing invoice ${actionType}`)
}
context.optimistic = true
context.me = await models.user.findUnique({ where: { id: me.id } })
const { msatsRequested, actionId } = failedInvoice
context.cost = BigInt(msatsRequested)
context.actionId = actionId
const invoiceArgs = await createSNInvoice(actionType, args, context)
const retryContext = {
...incomingContext,
optimistic: true,
me: await models.user.findUnique({ where: { id: me.id } }),
cost: BigInt(msatsRequested),
actionId
}
const invoiceArgs = await createSNInvoice(actionType, args, retryContext)
return await models.$transaction(async tx => {
context.tx = tx
const context = { ...retryContext, tx, invoiceArgs }
// update the old invoice to RETRYING, so that it's not confused with FAILED
await tx.invoice.update({
@ -213,7 +271,7 @@ export async function retryPaidAction (actionType, args, context) {
})
// create a new invoice
const invoice = await createDbInvoice(actionType, args, context, invoiceArgs)
const invoice = await createDbInvoice(actionType, args, context)
return {
result: await action.retry({ invoiceId: failedInvoice.id, newInvoiceId: invoice.id }, context),
@ -226,55 +284,27 @@ export async function retryPaidAction (actionType, args, context) {
const INVOICE_EXPIRE_SECS = 600
const MAX_PENDING_PAID_ACTIONS_PER_USER = 100
export async function createLightningInvoice (actionType, args, context) {
// if the action has an invoiceable peer, we'll create a peer invoice
// wrap it, and return the wrapped invoice
const { cost, models, lnd, sybilFeePercent, me } = context
// count pending invoices and bail if we're over the limit
export async function assertBelowMaxPendingInvoices (context) {
const { models, me } = context
const pendingInvoices = await models.invoice.count({
where: {
userId: me?.id ?? USER_ID.anon,
actionState: {
// not in a terminal state. Note: null isn't counted by prisma
notIn: PAID_ACTION_TERMINAL_STATES
}
}
})
console.log('pending paid actions', pendingInvoices)
if (pendingInvoices >= MAX_PENDING_PAID_ACTIONS_PER_USER) {
throw new Error('You have too many pending paid actions, cancel some or wait for them to expire')
}
}
const userId = await paidActions[actionType]?.getInvoiceablePeer?.(args, context)
if (userId) {
try {
if (!sybilFeePercent) {
throw new Error('sybil fee percent is not set for an invoiceable peer action')
}
const description = await paidActions[actionType].describe(args, context)
const { invoice, wrappedInvoice, wallet, maxFee } = await createWrappedInvoice(userId, {
msats: cost,
feePercent: sybilFeePercent,
description,
expiry: INVOICE_EXPIRE_SECS
}, { models, me, lnd })
return {
bolt11: invoice,
wrappedBolt11: wrappedInvoice,
wallet,
maxFee
}
} catch (e) {
console.error('failed to create stacker invoice, falling back to SN invoice', e)
}
export class NonInvoiceablePeerError extends Error {
constructor () {
super('non invoiceable peer')
this.name = 'NonInvoiceablePeerError'
}
return await createSNInvoice(actionType, args, context)
}
// we seperate the invoice creation into two functions because
@ -299,9 +329,10 @@ async function createSNInvoice (actionType, args, context) {
return { bolt11: invoice.request, preimage: invoice.secret }
}
async function createDbInvoice (actionType, args, context,
{ bolt11, wrappedBolt11, preimage, wallet, maxFee }) {
const { me, models, tx, cost, optimistic, actionId } = context
async function createDbInvoice (actionType, args, context) {
const { me, models, tx, cost, optimistic, actionId, invoiceArgs } = context
const { bolt11, wrappedBolt11, preimage, wallet, maxFee } = invoiceArgs
const db = tx ?? models
if (cost < 1000n) {

View File

@ -1,11 +1,15 @@
import { ANON_ITEM_SPAM_INTERVAL, ITEM_SPAM_INTERVAL, USER_ID } from '@/lib/constants'
import { ANON_ITEM_SPAM_INTERVAL, ITEM_SPAM_INTERVAL, PAID_ACTION_PAYMENT_METHODS, USER_ID } from '@/lib/constants'
import { notifyItemMention, notifyItemParents, notifyMention, notifyTerritorySubscribers, notifyUserSubscribers } from '@/lib/webPush'
import { getItemMentions, getMentions, performBotBehavior } from './lib/item'
import { msatsToSats, satsToMsats } from '@/lib/format'
export const anonable = true
export const supportsPessimism = true
export const supportsOptimism = true
export const paymentMethods = [
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
PAID_ACTION_PAYMENT_METHODS.OPTIMISTIC,
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
]
export async function getCost ({ subName, parentId, uploadIds, boost = 0, bio }, { models, me }) {
const sub = (parentId || bio) ? null : await models.sub.findUnique({ where: { name: subName } })

View File

@ -1,12 +1,15 @@
import { USER_ID } from '@/lib/constants'
import { PAID_ACTION_PAYMENT_METHODS, USER_ID } from '@/lib/constants'
import { uploadFees } from '../resolvers/upload'
import { getItemMentions, getMentions, performBotBehavior } from './lib/item'
import { notifyItemMention, notifyMention } from '@/lib/webPush'
import { satsToMsats } from '@/lib/format'
export const anonable = true
export const supportsPessimism = true
export const supportsOptimism = false
export const paymentMethods = [
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
]
export async function getCost ({ id, boost = 0, uploadIds, bio }, { me, models }) {
// the only reason updating items costs anything is when it has new uploads

View File

@ -1,8 +1,12 @@
import { PAID_ACTION_PAYMENT_METHODS } from '@/lib/constants'
import { satsToMsats } from '@/lib/format'
export const anonable = false
export const supportsPessimism = true
export const supportsOptimism = true
export const paymentMethods = [
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
PAID_ACTION_PAYMENT_METHODS.OPTIMISTIC
]
export async function getCost ({ id }, { me, models }) {
const pollOption = await models.pollOption.findUnique({

View File

@ -1,10 +1,13 @@
import { TERRITORY_PERIOD_COST } from '@/lib/constants'
import { PAID_ACTION_PAYMENT_METHODS, TERRITORY_PERIOD_COST } from '@/lib/constants'
import { satsToMsats } from '@/lib/format'
import { nextBilling } from '@/lib/territory'
export const anonable = false
export const supportsPessimism = true
export const supportsOptimism = false
export const paymentMethods = [
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
]
export async function getCost ({ name }, { models }) {
const sub = await models.sub.findUnique({

View File

@ -1,9 +1,13 @@
import { TERRITORY_PERIOD_COST } from '@/lib/constants'
import { PAID_ACTION_PAYMENT_METHODS, TERRITORY_PERIOD_COST } from '@/lib/constants'
import { satsToMsats } from '@/lib/format'
import { nextBilling } from '@/lib/territory'
export const anonable = false
export const supportsPessimism = true
export const supportsOptimism = false
export const paymentMethods = [
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
]
export async function getCost ({ billingType }) {
return satsToMsats(TERRITORY_PERIOD_COST(billingType))

View File

@ -1,10 +1,13 @@
import { TERRITORY_PERIOD_COST } from '@/lib/constants'
import { PAID_ACTION_PAYMENT_METHODS, TERRITORY_PERIOD_COST } from '@/lib/constants'
import { satsToMsats } from '@/lib/format'
import { nextBilling } from '@/lib/territory'
export const anonable = false
export const supportsPessimism = true
export const supportsOptimism = false
export const paymentMethods = [
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
]
export async function getCost ({ billingType }) {
return satsToMsats(TERRITORY_PERIOD_COST(billingType))

View File

@ -1,11 +1,14 @@
import { TERRITORY_PERIOD_COST } from '@/lib/constants'
import { PAID_ACTION_PAYMENT_METHODS, TERRITORY_PERIOD_COST } from '@/lib/constants'
import { satsToMsats } from '@/lib/format'
import { proratedBillingCost } from '@/lib/territory'
import { datePivot } from '@/lib/time'
export const anonable = false
export const supportsPessimism = true
export const supportsOptimism = false
export const paymentMethods = [
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
]
export async function getCost ({ oldName, billingType }, { models }) {
const oldSub = await models.sub.findUnique({

View File

@ -1,10 +1,15 @@
import { USER_ID } from '@/lib/constants'
import { PAID_ACTION_PAYMENT_METHODS, USER_ID } from '@/lib/constants'
import { msatsToSats, satsToMsats } from '@/lib/format'
import { notifyZapped } from '@/lib/webPush'
export const anonable = true
export const supportsPessimism = true
export const supportsOptimism = true
export const paymentMethods = [
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
PAID_ACTION_PAYMENT_METHODS.P2P,
PAID_ACTION_PAYMENT_METHODS.OPTIMISTIC,
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
]
export async function getCost ({ sats }) {
return satsToMsats(sats)

View File

@ -535,6 +535,7 @@ export default {
'"Item".bio = false',
ad ? `"Item".id <> ${ad.id}` : '',
activeOrMine(me),
await filterClause(me, models, type),
subClause(sub, 3, 'Item', me, showNsfw),
muteClause(me))}
ORDER BY rank DESC

View File

@ -509,7 +509,7 @@ const resolvers = {
verifyHmac(hash, hmac)
const dbInv = await finalizeHodlInvoice({ data: { hash }, lnd, models, boss })
if (dbInv.invoiceForward) {
if (dbInv?.invoiceForward) {
const { wallet, bolt11 } = dbInv.invoiceForward
const logger = walletLogger({ wallet, models })
const decoded = await parsePaymentRequest({ request: bolt11 })

View File

@ -12,6 +12,7 @@ extend type Mutation {
enum PaymentMethod {
FEE_CREDIT
ZERO_COST
OPTIMISTIC
PESSIMISTIC
}

View File

@ -509,6 +509,7 @@ function InputInner ({
if (storageKey) {
window.localStorage.setItem(storageKey, overrideValue)
}
onChange && onChange(formik, { target: { value: overrideValue } })
} else if (storageKey) {
const draft = window.localStorage.getItem(storageKey)
if (draft) {

View File

@ -290,7 +290,7 @@ export function useWalletLogs (wallet, initialPage = 1, logsPerPage = 10) {
if (hasMore) {
setLoading(true)
const result = await loadLogsPage(page + 1, logsPerPage, wallet?.def)
_setLogs(prevLogs => [...prevLogs, ...result.data])
_setLogs(prevLogs => uniqueSort([...prevLogs, ...result.data]))
setHasMore(result.hasMore)
setPage(prevPage => prevPage + 1)
setLoading(false)

View File

@ -3,6 +3,12 @@
export const DEFAULT_SUBS = ['bitcoin', 'nostr', 'tech', 'meta', 'jobs']
export const DEFAULT_SUBS_NO_JOBS = DEFAULT_SUBS.filter(s => s !== 'jobs')
export const PAID_ACTION_PAYMENT_METHODS = {
FEE_CREDIT: 'FEE_CREDIT',
PESSIMISTIC: 'PESSIMISTIC',
OPTIMISTIC: 'OPTIMISTIC',
P2P: 'P2P'
}
export const PAID_ACTION_TERMINAL_STATES = ['FAILED', 'PAID', 'RETRYING']
export const NOFOLLOW_LIMIT = 1000
export const UNKNOWN_LINK_REL = 'noreferrer nofollow noopener'

View File

@ -112,10 +112,14 @@ async function transitionInvoice (jobName, { invoiceId, fromState, toState, tran
async function performPessimisticAction ({ lndInvoice, dbInvoice, tx, models, lnd, boss }) {
try {
const args = { ...dbInvoice.actionArgs, invoiceId: dbInvoice.id }
const context = { tx, cost: BigInt(lndInvoice.received_mtokens) }
context.sybilFeePercent = await paidActions[dbInvoice.actionType].getSybilFeePercent?.(args, context)
const context = {
tx,
cost: BigInt(lndInvoice.received_mtokens),
me: dbInvoice.user
}
const sybilFeePercent = await paidActions[dbInvoice.actionType].getSybilFeePercent?.(args, context)
const result = await paidActions[dbInvoice.actionType].perform(args, context)
const result = await paidActions[dbInvoice.actionType].perform(args, { ...context, sybilFeePercent })
await tx.invoice.update({
where: { id: dbInvoice.id },
data: {
@ -281,9 +285,12 @@ export async function paidActionForwarded ({ data: { invoiceId, withdrawal, ...a
// settle the invoice, allowing us to transition to PAID
await settleHodlInvoice({ secret: payment.secret, lnd })
// the amount we paid includes the fee so we need to subtract it to get the amount received
const received = Number(payment.mtokens) - Number(payment.fee_mtokens)
const logger = walletLogger({ wallet: dbInvoice.invoiceForward.wallet, models })
logger.ok(
`↙ payment received: ${formatSats(msatsToSats(payment.mtokens))}`,
`↙ payment received: ${formatSats(msatsToSats(received))}`,
{
bolt11,
preimage: payment.secret

View File

@ -1,6 +1,7 @@
import lnd from '@/api/lnd'
import performPaidAction from '@/api/paidAction'
import serialize from '@/api/resolvers/serial'
import { PAID_ACTION_PAYMENT_METHODS } from '@/lib/constants'
import { nextBillingWithGrace } from '@/lib/territory'
import { datePivot } from '@/lib/time'
@ -36,7 +37,12 @@ export async function territoryBilling ({ data: { subName }, boss, models }) {
try {
const { result } = await performPaidAction('TERRITORY_BILLING',
{ name: subName }, { models, me: sub.user, lnd, forceFeeCredits: true })
{ name: subName }, {
models,
me: sub.user,
lnd,
forcePaymentMethod: PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT
})
if (!result) {
throw new Error('not enough fee credits to auto-renew territory')
}

View File

@ -309,8 +309,9 @@ export async function checkWithdrawal ({ data: { hash, withdrawal, invoice }, bo
notifyWithdrawal(dbWdrwl.userId, wdrwl)
const { request: bolt11, secret: preimage } = wdrwl.payment
logger?.ok(
`↙ payment received: ${formatSats(msatsToSats(Number(wdrwl.payment.mtokens)))}`,
`↙ payment received: ${formatSats(msatsToSats(paid))}`,
{
bolt11,
preimage,

View File

@ -1,12 +1,17 @@
import performPaidAction from '@/api/paidAction'
import { USER_ID } from '@/lib/constants'
import { PAID_ACTION_PAYMENT_METHODS, USER_ID } from '@/lib/constants'
import { datePivot } from '@/lib/time'
import gql from 'graphql-tag'
export async function autoPost ({ data: item, models, apollo, lnd, boss }) {
return await performPaidAction('ITEM_CREATE',
{ ...item, subName: 'meta', userId: USER_ID.sn, apiKey: true },
{ models, me: { id: USER_ID.sn }, lnd, forceFeeCredits: true })
{
models,
me: { id: USER_ID.sn },
lnd,
forcePaymentMethod: PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT
})
}
export async function weeklyPost (args) {
@ -47,5 +52,10 @@ export async function payWeeklyPostBounty ({ data: { id }, models, apollo, lnd }
await performPaidAction('ZAP',
{ id: winner.id, sats: item.bounty },
{ models, me: { id: USER_ID.sn }, lnd, forceFeeCredits: true })
{
models,
me: { id: USER_ID.sn },
lnd,
forcePaymentMethod: PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT
})
}