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>
This commit is contained in:
parent
d1c770dbbc
commit
a44d0daf09
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}`
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 } })
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 })
|
||||
|
|
|
@ -12,6 +12,7 @@ extend type Mutation {
|
|||
|
||||
enum PaymentMethod {
|
||||
FEE_CREDIT
|
||||
ZERO_COST
|
||||
OPTIMISTIC
|
||||
PESSIMISTIC
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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')
|
||||
}
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue