Compare commits

..

No commits in common. "b7fc0e0e74391990ddeff7a63b9ac620d42f1405" and "d1c770dbbc832cb28ad5e301fc11ae0e2bc2f8b9" have entirely different histories.

24 changed files with 184 additions and 268 deletions

View File

@ -139,14 +139,8 @@ Each paid action is implemented in its own file in the `paidAction` directory. E
### Boolean flags
- `anonable`: can be performed anonymously
### 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
- `supportsPessimism`: supports a pessimistic payment flow
- `supportsOptimism`: supports an optimistic payment flow
### Functions

View File

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

View File

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

View File

@ -1,12 +1,8 @@
import { PAID_ACTION_PAYMENT_METHODS } from '@/lib/constants'
import { msatsToSats, satsToMsats } from '@/lib/format'
export const anonable = false
export const paymentMethods = [
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
PAID_ACTION_PAYMENT_METHODS.OPTIMISTIC
]
export const supportsPessimism = false
export const supportsOptimism = true
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_PAYMENT_METHODS, PAID_ACTION_TERMINAL_STATES, USER_ID } from '@/lib/constants'
import { 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, incomingContext) {
export default async function performPaidAction (actionType, args, context) {
try {
const { me, models, forcePaymentMethod } = incomingContext
const { me, models, forceFeeCredits } = context
const paidAction = paidActions[actionType]
console.group('performPaidAction', actionType, args)
@ -41,71 +41,52 @@ export default async function performPaidAction (actionType, args, incomingConte
throw new Error(`Invalid action type ${actionType}`)
}
if (!me && !paidAction.anonable) {
throw new Error('You must be logged in to perform this action')
}
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)
// 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)
}
// 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
if (!me) {
if (!paidAction.anonable) {
throw new Error('You must be logged in to perform this action')
}
// 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)
if (context.cost > 0) {
console.log('we are anon so can only perform pessimistic action that require payment')
return await performPessimisticAction(actionType, args, context)
}
}
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)
// if we fail with fee credits, but not because of insufficient funds, bail
if (!e.message.includes('\\"users\\" violates check constraint \\"msats_positive\\"')) {
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)
}
}
}
throw new Error('No working payment method found')
// 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)
} catch (e) {
console.error('performPaidAction failed', e)
throw e
@ -114,48 +95,50 @@ export default async function performPaidAction (actionType, args, incomingConte
}
}
async function performNoInvoiceAction (actionType, args, incomingContext) {
const { me, models, cost, paymentMethod } = incomingContext
async function performFeeCreditAction (actionType, args, context) {
const { me, models, cost } = context
const action = paidActions[actionType]
const result = await models.$transaction(async tx => {
const context = { ...incomingContext, tx }
context.tx = tx
if (paymentMethod === 'FEE_CREDIT') {
await tx.user.update({
where: {
id: me?.id ?? USER_ID.anon
},
data: { msats: { decrement: cost } }
})
}
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
paymentMethod: 'FEE_CREDIT'
}
}, { isolationLevel: Prisma.TransactionIsolationLevel.ReadCommitted })
// run non critical side effects in the background
// after the transaction has been committed
action.nonCriticalSideEffects?.(result.result, incomingContext).catch(console.error)
action.nonCriticalSideEffects?.(result.result, context).catch(console.error)
return result
}
async function performOptimisticAction (actionType, args, incomingContext) {
const { models, invoiceArgs: incomingInvoiceArgs } = incomingContext
async function performOptimisticAction (actionType, args, context) {
const { models } = context
const action = paidActions[actionType]
const optimisticContext = { ...incomingContext, optimistic: true }
const invoiceArgs = incomingInvoiceArgs ?? await createSNInvoice(actionType, args, optimisticContext)
context.optimistic = true
const invoiceArgs = await createLightningInvoice(actionType, args, context)
return await models.$transaction(async tx => {
const context = { ...optimisticContext, tx, invoiceArgs }
context.tx = tx
const invoice = await createDbInvoice(actionType, args, context)
const invoice = await createDbInvoice(actionType, args, context, invoiceArgs)
return {
invoice,
@ -165,61 +148,23 @@ async function performOptimisticAction (actionType, args, incomingContext) {
}, { isolationLevel: Prisma.TransactionIsolationLevel.ReadCommitted })
}
async function beginPessimisticAction (actionType, args, context) {
async function performPessimisticAction (actionType, args, context) {
const action = paidActions[actionType]
if (!action.paymentMethods.includes(PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC)) {
if (!action.supportsPessimism) {
throw new Error(`This action ${actionType} does not support pessimistic invoicing`)
}
// just create the invoice and complete action when it's paid
const invoiceArgs = context.invoiceArgs ?? await createSNInvoice(actionType, args, context)
const invoiceArgs = await createLightningInvoice(actionType, args, context)
return {
invoice: await createDbInvoice(actionType, args, { ...context, invoiceArgs }),
invoice: await createDbInvoice(actionType, args, context, invoiceArgs),
paymentMethod: 'PESSIMISTIC'
}
}
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
export async function retryPaidAction (actionType, args, context) {
const { models, me } = context
const { invoice: failedInvoice } = args
console.log('retryPaidAction', actionType, args)
@ -233,7 +178,7 @@ export async function retryPaidAction (actionType, args, incomingContext) {
throw new Error(`retryPaidAction - must be logged in ${actionType}`)
}
if (!action.paymentMethods.includes(PAID_ACTION_PAYMENT_METHODS.OPTIMISTIC)) {
if (!action.supportsOptimism) {
throw new Error(`retryPaidAction - action does not support optimism ${actionType}`)
}
@ -245,19 +190,16 @@ export async function retryPaidAction (actionType, args, incomingContext) {
throw new Error(`retryPaidAction - missing invoice ${actionType}`)
}
const { msatsRequested, actionId } = failedInvoice
const retryContext = {
...incomingContext,
optimistic: true,
me: await models.user.findUnique({ where: { id: me.id } }),
cost: BigInt(msatsRequested),
actionId
}
context.optimistic = true
context.me = await models.user.findUnique({ where: { id: me.id } })
const invoiceArgs = await createSNInvoice(actionType, args, retryContext)
const { msatsRequested, actionId } = failedInvoice
context.cost = BigInt(msatsRequested)
context.actionId = actionId
const invoiceArgs = await createSNInvoice(actionType, args, context)
return await models.$transaction(async tx => {
const context = { ...retryContext, tx, invoiceArgs }
context.tx = tx
// update the old invoice to RETRYING, so that it's not confused with FAILED
await tx.invoice.update({
@ -271,7 +213,7 @@ export async function retryPaidAction (actionType, args, incomingContext) {
})
// create a new invoice
const invoice = await createDbInvoice(actionType, args, context)
const invoice = await createDbInvoice(actionType, args, context, invoiceArgs)
return {
result: await action.retry({ invoiceId: failedInvoice.id, newInvoiceId: invoice.id }, context),
@ -284,27 +226,55 @@ export async function retryPaidAction (actionType, args, incomingContext) {
const INVOICE_EXPIRE_SECS = 600
const MAX_PENDING_PAID_ACTIONS_PER_USER = 100
export async function assertBelowMaxPendingInvoices (context) {
const { models, me } = context
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
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')
}
}
export class NonInvoiceablePeerError extends Error {
constructor () {
super('non invoiceable peer')
this.name = 'NonInvoiceablePeerError'
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)
}
}
return await createSNInvoice(actionType, args, context)
}
// we seperate the invoice creation into two functions because
@ -329,10 +299,9 @@ async function createSNInvoice (actionType, args, context) {
return { bolt11: invoice.request, preimage: invoice.secret }
}
async function createDbInvoice (actionType, args, context) {
const { me, models, tx, cost, optimistic, actionId, invoiceArgs } = context
const { bolt11, wrappedBolt11, preimage, wallet, maxFee } = invoiceArgs
async function createDbInvoice (actionType, args, context,
{ bolt11, wrappedBolt11, preimage, wallet, maxFee }) {
const { me, models, tx, cost, optimistic, actionId } = context
const db = tx ?? models
if (cost < 1000n) {

View File

@ -1,15 +1,11 @@
import { ANON_ITEM_SPAM_INTERVAL, ITEM_SPAM_INTERVAL, PAID_ACTION_PAYMENT_METHODS, USER_ID } from '@/lib/constants'
import { ANON_ITEM_SPAM_INTERVAL, ITEM_SPAM_INTERVAL, 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 paymentMethods = [
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
PAID_ACTION_PAYMENT_METHODS.OPTIMISTIC,
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
]
export const supportsPessimism = true
export const supportsOptimism = true
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,15 +1,12 @@
import { PAID_ACTION_PAYMENT_METHODS, USER_ID } from '@/lib/constants'
import { 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 paymentMethods = [
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
]
export const supportsPessimism = true
export const supportsOptimism = false
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,12 +1,8 @@
import { PAID_ACTION_PAYMENT_METHODS } from '@/lib/constants'
import { satsToMsats } from '@/lib/format'
export const anonable = false
export const paymentMethods = [
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
PAID_ACTION_PAYMENT_METHODS.OPTIMISTIC
]
export const supportsPessimism = true
export const supportsOptimism = true
export async function getCost ({ id }, { me, models }) {
const pollOption = await models.pollOption.findUnique({

View File

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

View File

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

View File

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

View File

@ -1,14 +1,11 @@
import { PAID_ACTION_PAYMENT_METHODS, TERRITORY_PERIOD_COST } from '@/lib/constants'
import { 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 paymentMethods = [
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
]
export const supportsPessimism = true
export const supportsOptimism = false
export async function getCost ({ oldName, billingType }, { models }) {
const oldSub = await models.sub.findUnique({

View File

@ -1,15 +1,10 @@
import { PAID_ACTION_PAYMENT_METHODS, USER_ID } from '@/lib/constants'
import { USER_ID } from '@/lib/constants'
import { msatsToSats, satsToMsats } from '@/lib/format'
import { notifyZapped } from '@/lib/webPush'
export const anonable = 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 const supportsPessimism = true
export const supportsOptimism = true
export async function getCost ({ sats }) {
return satsToMsats(sats)

View File

@ -535,7 +535,6 @@ 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,7 +12,6 @@ extend type Mutation {
enum PaymentMethod {
FEE_CREDIT
ZERO_COST
OPTIMISTIC
PESSIMISTIC
}

View File

@ -509,7 +509,6 @@ 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 => uniqueSort([...prevLogs, ...result.data]))
_setLogs(prevLogs => [...prevLogs, ...result.data])
setHasMore(result.hasMore)
setPage(prevPage => prevPage + 1)
setLoading(false)

View File

@ -3,12 +3,6 @@
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,14 +112,10 @@ 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),
me: dbInvoice.user
}
const sybilFeePercent = await paidActions[dbInvoice.actionType].getSybilFeePercent?.(args, context)
const context = { tx, cost: BigInt(lndInvoice.received_mtokens) }
context.sybilFeePercent = await paidActions[dbInvoice.actionType].getSybilFeePercent?.(args, context)
const result = await paidActions[dbInvoice.actionType].perform(args, { ...context, sybilFeePercent })
const result = await paidActions[dbInvoice.actionType].perform(args, context)
await tx.invoice.update({
where: { id: dbInvoice.id },
data: {
@ -285,12 +281,9 @@ 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(received))}`,
`↙ payment received: ${formatSats(msatsToSats(payment.mtokens))}`,
{
bolt11,
preimage: payment.secret

View File

@ -1,7 +1,6 @@
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'
@ -37,12 +36,7 @@ export async function territoryBilling ({ data: { subName }, boss, models }) {
try {
const { result } = await performPaidAction('TERRITORY_BILLING',
{ name: subName }, {
models,
me: sub.user,
lnd,
forcePaymentMethod: PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT
})
{ name: subName }, { models, me: sub.user, lnd, forceFeeCredits: true })
if (!result) {
throw new Error('not enough fee credits to auto-renew territory')
}

View File

@ -309,9 +309,8 @@ 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(paid))}`,
`↙ payment received: ${formatSats(msatsToSats(Number(wdrwl.payment.mtokens)))}`,
{
bolt11,
preimage,

View File

@ -1,17 +1,12 @@
import performPaidAction from '@/api/paidAction'
import { PAID_ACTION_PAYMENT_METHODS, USER_ID } from '@/lib/constants'
import { 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,
forcePaymentMethod: PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT
})
{ models, me: { id: USER_ID.sn }, lnd, forceFeeCredits: true })
}
export async function weeklyPost (args) {
@ -52,10 +47,5 @@ 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,
forcePaymentMethod: PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT
})
{ models, me: { id: USER_ID.sn }, lnd, forceFeeCredits: true })
}