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 ### Boolean flags
- `anonable`: can be performed anonymously - `anonable`: can be performed anonymously
- `supportsPessimism`: supports a pessimistic payment flow
### Payment methods - `supportsOptimism`: supports an optimistic payment flow
- `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 ### Functions

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { createHodlInvoice, createInvoice, parsePaymentRequest } from 'ln-service' import { createHodlInvoice, createInvoice, parsePaymentRequest } from 'ln-service'
import { datePivot } from '@/lib/time' 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 { createHmac } from '@/api/resolvers/wallet'
import { Prisma } from '@prisma/client' import { Prisma } from '@prisma/client'
import * as ITEM_CREATE from './itemCreate' import * as ITEM_CREATE from './itemCreate'
@ -30,9 +30,9 @@ export const paidActions = {
DONATE DONATE
} }
export default async function performPaidAction (actionType, args, incomingContext) { export default async function performPaidAction (actionType, args, context) {
try { try {
const { me, models, forcePaymentMethod } = incomingContext const { me, models, forceFeeCredits } = context
const paidAction = paidActions[actionType] const paidAction = paidActions[actionType]
console.group('performPaidAction', actionType, args) console.group('performPaidAction', actionType, args)
@ -41,71 +41,52 @@ export default async function performPaidAction (actionType, args, incomingConte
throw new Error(`Invalid action type ${actionType}`) throw new Error(`Invalid action type ${actionType}`)
} }
if (!me && !paidAction.anonable) { context.me = me ? await models.user.findUnique({ where: { id: me.id } }) : undefined
throw new Error('You must be logged in to perform this action') context.cost = await paidAction.getCost(args, context)
} context.sybilFeePercent = await paidAction.getSybilFeePercent?.(args, context)
// treat context as immutable if (!me) {
const contextWithMe = { if (!paidAction.anonable) {
...incomingContext, throw new Error('You must be logged in to perform this action')
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
} }
// payment methods that anonymous users can use if (context.cost > 0) {
if (paymentMethod === PAID_ACTION_PAYMENT_METHODS.P2P) { console.log('we are anon so can only perform pessimistic action that require payment')
try { return await performPessimisticAction(actionType, args, context)
return await performP2PAction(actionType, args, context) }
} catch (e) { }
if (e instanceof NonInvoiceablePeerError) {
console.log('peer cannot be invoiced, skipping') const isRich = context.cost <= (context.me?.msats ?? 0)
continue if (isRich) {
} try {
console.error(`${paymentMethod} action failed`, e) 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 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) { } catch (e) {
console.error('performPaidAction failed', e) console.error('performPaidAction failed', e)
throw e throw e
@ -114,48 +95,50 @@ export default async function performPaidAction (actionType, args, incomingConte
} }
} }
async function performNoInvoiceAction (actionType, args, incomingContext) { async function performFeeCreditAction (actionType, args, context) {
const { me, models, cost, paymentMethod } = incomingContext const { me, models, cost } = context
const action = paidActions[actionType] const action = paidActions[actionType]
const result = await models.$transaction(async tx => { const result = await models.$transaction(async tx => {
const context = { ...incomingContext, tx } context.tx = tx
if (paymentMethod === 'FEE_CREDIT') { await tx.user.update({
await tx.user.update({ where: {
where: { id: me?.id ?? USER_ID.anon
id: me?.id ?? USER_ID.anon },
}, data: {
data: { msats: { decrement: cost } } msats: {
}) decrement: cost
} }
}
})
const result = await action.perform(args, context) const result = await action.perform(args, context)
await action.onPaid?.(result, context) await action.onPaid?.(result, context)
return { return {
result, result,
paymentMethod paymentMethod: 'FEE_CREDIT'
} }
}, { isolationLevel: Prisma.TransactionIsolationLevel.ReadCommitted }) }, { isolationLevel: Prisma.TransactionIsolationLevel.ReadCommitted })
// run non critical side effects in the background // run non critical side effects in the background
// after the transaction has been committed // after the transaction has been committed
action.nonCriticalSideEffects?.(result.result, incomingContext).catch(console.error) action.nonCriticalSideEffects?.(result.result, context).catch(console.error)
return result return result
} }
async function performOptimisticAction (actionType, args, incomingContext) { async function performOptimisticAction (actionType, args, context) {
const { models, invoiceArgs: incomingInvoiceArgs } = incomingContext const { models } = context
const action = paidActions[actionType] const action = paidActions[actionType]
const optimisticContext = { ...incomingContext, optimistic: true } context.optimistic = true
const invoiceArgs = incomingInvoiceArgs ?? await createSNInvoice(actionType, args, optimisticContext) const invoiceArgs = await createLightningInvoice(actionType, args, context)
return await models.$transaction(async tx => { 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 { return {
invoice, invoice,
@ -165,61 +148,23 @@ async function performOptimisticAction (actionType, args, incomingContext) {
}, { isolationLevel: Prisma.TransactionIsolationLevel.ReadCommitted }) }, { isolationLevel: Prisma.TransactionIsolationLevel.ReadCommitted })
} }
async function beginPessimisticAction (actionType, args, context) { async function performPessimisticAction (actionType, args, context) {
const action = paidActions[actionType] 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`) throw new Error(`This action ${actionType} does not support pessimistic invoicing`)
} }
// just create the invoice and complete action when it's paid // 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 { return {
invoice: await createDbInvoice(actionType, args, { ...context, invoiceArgs }), invoice: await createDbInvoice(actionType, args, context, invoiceArgs),
paymentMethod: 'PESSIMISTIC' paymentMethod: 'PESSIMISTIC'
} }
} }
async function performP2PAction (actionType, args, incomingContext) { export async function retryPaidAction (actionType, args, context) {
// if the action has an invoiceable peer, we'll create a peer invoice const { models, me } = context
// 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 const { invoice: failedInvoice } = args
console.log('retryPaidAction', actionType, 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}`) 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}`) 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}`) throw new Error(`retryPaidAction - missing invoice ${actionType}`)
} }
const { msatsRequested, actionId } = failedInvoice context.optimistic = true
const retryContext = { context.me = await models.user.findUnique({ where: { id: me.id } })
...incomingContext,
optimistic: true,
me: await models.user.findUnique({ where: { id: me.id } }),
cost: BigInt(msatsRequested),
actionId
}
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 => { 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 // update the old invoice to RETRYING, so that it's not confused with FAILED
await tx.invoice.update({ await tx.invoice.update({
@ -271,7 +213,7 @@ export async function retryPaidAction (actionType, args, incomingContext) {
}) })
// create a new invoice // create a new invoice
const invoice = await createDbInvoice(actionType, args, context) const invoice = await createDbInvoice(actionType, args, context, invoiceArgs)
return { return {
result: await action.retry({ invoiceId: failedInvoice.id, newInvoiceId: invoice.id }, context), 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 INVOICE_EXPIRE_SECS = 600
const MAX_PENDING_PAID_ACTIONS_PER_USER = 100 const MAX_PENDING_PAID_ACTIONS_PER_USER = 100
export async function assertBelowMaxPendingInvoices (context) { export async function createLightningInvoice (actionType, args, context) {
const { models, me } = 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({ const pendingInvoices = await models.invoice.count({
where: { where: {
userId: me?.id ?? USER_ID.anon, userId: me?.id ?? USER_ID.anon,
actionState: { actionState: {
// not in a terminal state. Note: null isn't counted by prisma
notIn: PAID_ACTION_TERMINAL_STATES notIn: PAID_ACTION_TERMINAL_STATES
} }
} }
}) })
console.log('pending paid actions', pendingInvoices)
if (pendingInvoices >= MAX_PENDING_PAID_ACTIONS_PER_USER) { 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') throw new Error('You have too many pending paid actions, cancel some or wait for them to expire')
} }
}
export class NonInvoiceablePeerError extends Error { const userId = await paidActions[actionType]?.getInvoiceablePeer?.(args, context)
constructor () { if (userId) {
super('non invoiceable peer') try {
this.name = 'NonInvoiceablePeerError' 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 // 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 } return { bolt11: invoice.request, preimage: invoice.secret }
} }
async function createDbInvoice (actionType, args, context) { async function createDbInvoice (actionType, args, context,
const { me, models, tx, cost, optimistic, actionId, invoiceArgs } = context { bolt11, wrappedBolt11, preimage, wallet, maxFee }) {
const { bolt11, wrappedBolt11, preimage, wallet, maxFee } = invoiceArgs const { me, models, tx, cost, optimistic, actionId } = context
const db = tx ?? models const db = tx ?? models
if (cost < 1000n) { 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 { notifyItemMention, notifyItemParents, notifyMention, notifyTerritorySubscribers, notifyUserSubscribers } from '@/lib/webPush'
import { getItemMentions, getMentions, performBotBehavior } from './lib/item' import { getItemMentions, getMentions, performBotBehavior } from './lib/item'
import { msatsToSats, satsToMsats } from '@/lib/format' import { msatsToSats, satsToMsats } from '@/lib/format'
export const anonable = true export const anonable = true
export const supportsPessimism = true
export const paymentMethods = [ export const supportsOptimism = true
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 }) { export async function getCost ({ subName, parentId, uploadIds, boost = 0, bio }, { models, me }) {
const sub = (parentId || bio) ? null : await models.sub.findUnique({ where: { name: subName } }) 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 { uploadFees } from '../resolvers/upload'
import { getItemMentions, getMentions, performBotBehavior } from './lib/item' import { getItemMentions, getMentions, performBotBehavior } from './lib/item'
import { notifyItemMention, notifyMention } from '@/lib/webPush' import { notifyItemMention, notifyMention } from '@/lib/webPush'
import { satsToMsats } from '@/lib/format' import { satsToMsats } from '@/lib/format'
export const anonable = true export const anonable = true
export const supportsPessimism = true
export const paymentMethods = [ export const supportsOptimism = false
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
]
export async function getCost ({ id, boost = 0, uploadIds, bio }, { me, models }) { export async function getCost ({ id, boost = 0, uploadIds, bio }, { me, models }) {
// the only reason updating items costs anything is when it has new uploads // 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' import { satsToMsats } from '@/lib/format'
export const anonable = false export const anonable = false
export const supportsPessimism = true
export const paymentMethods = [ export const supportsOptimism = true
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
PAID_ACTION_PAYMENT_METHODS.OPTIMISTIC
]
export async function getCost ({ id }, { me, models }) { export async function getCost ({ id }, { me, models }) {
const pollOption = await models.pollOption.findUnique({ 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 { satsToMsats } from '@/lib/format'
import { nextBilling } from '@/lib/territory' import { nextBilling } from '@/lib/territory'
export const anonable = false export const anonable = false
export const supportsPessimism = true
export const paymentMethods = [ export const supportsOptimism = false
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
]
export async function getCost ({ name }, { models }) { export async function getCost ({ name }, { models }) {
const sub = await models.sub.findUnique({ 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 { satsToMsats } from '@/lib/format'
import { nextBilling } from '@/lib/territory' import { nextBilling } from '@/lib/territory'
export const anonable = false export const anonable = false
export const supportsPessimism = true
export const paymentMethods = [ export const supportsOptimism = false
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
]
export async function getCost ({ billingType }) { export async function getCost ({ billingType }) {
return satsToMsats(TERRITORY_PERIOD_COST(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 { satsToMsats } from '@/lib/format'
import { nextBilling } from '@/lib/territory' import { nextBilling } from '@/lib/territory'
export const anonable = false export const anonable = false
export const supportsPessimism = true
export const paymentMethods = [ export const supportsOptimism = false
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
]
export async function getCost ({ billingType }) { export async function getCost ({ billingType }) {
return satsToMsats(TERRITORY_PERIOD_COST(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 { satsToMsats } from '@/lib/format'
import { proratedBillingCost } from '@/lib/territory' import { proratedBillingCost } from '@/lib/territory'
import { datePivot } from '@/lib/time' import { datePivot } from '@/lib/time'
export const anonable = false export const anonable = false
export const supportsPessimism = true
export const paymentMethods = [ export const supportsOptimism = false
PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT,
PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC
]
export async function getCost ({ oldName, billingType }, { models }) { export async function getCost ({ oldName, billingType }, { models }) {
const oldSub = await models.sub.findUnique({ 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 { msatsToSats, satsToMsats } from '@/lib/format'
import { notifyZapped } from '@/lib/webPush' import { notifyZapped } from '@/lib/webPush'
export const anonable = true export const anonable = true
export const supportsPessimism = true
export const paymentMethods = [ export const supportsOptimism = true
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 }) { export async function getCost ({ sats }) {
return satsToMsats(sats) return satsToMsats(sats)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,12 +3,6 @@
export const DEFAULT_SUBS = ['bitcoin', 'nostr', 'tech', 'meta', 'jobs'] export const DEFAULT_SUBS = ['bitcoin', 'nostr', 'tech', 'meta', 'jobs']
export const DEFAULT_SUBS_NO_JOBS = DEFAULT_SUBS.filter(s => s !== '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 PAID_ACTION_TERMINAL_STATES = ['FAILED', 'PAID', 'RETRYING']
export const NOFOLLOW_LIMIT = 1000 export const NOFOLLOW_LIMIT = 1000
export const UNKNOWN_LINK_REL = 'noreferrer nofollow noopener' 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 }) { async function performPessimisticAction ({ lndInvoice, dbInvoice, tx, models, lnd, boss }) {
try { try {
const args = { ...dbInvoice.actionArgs, invoiceId: dbInvoice.id } const args = { ...dbInvoice.actionArgs, invoiceId: dbInvoice.id }
const context = { const context = { tx, cost: BigInt(lndInvoice.received_mtokens) }
tx, context.sybilFeePercent = await paidActions[dbInvoice.actionType].getSybilFeePercent?.(args, context)
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, sybilFeePercent }) const result = await paidActions[dbInvoice.actionType].perform(args, context)
await tx.invoice.update({ await tx.invoice.update({
where: { id: dbInvoice.id }, where: { id: dbInvoice.id },
data: { data: {
@ -285,12 +281,9 @@ export async function paidActionForwarded ({ data: { invoiceId, withdrawal, ...a
// settle the invoice, allowing us to transition to PAID // settle the invoice, allowing us to transition to PAID
await settleHodlInvoice({ secret: payment.secret, lnd }) 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 }) const logger = walletLogger({ wallet: dbInvoice.invoiceForward.wallet, models })
logger.ok( logger.ok(
`↙ payment received: ${formatSats(msatsToSats(received))}`, `↙ payment received: ${formatSats(msatsToSats(payment.mtokens))}`,
{ {
bolt11, bolt11,
preimage: payment.secret preimage: payment.secret

View File

@ -1,7 +1,6 @@
import lnd from '@/api/lnd' import lnd from '@/api/lnd'
import performPaidAction from '@/api/paidAction' import performPaidAction from '@/api/paidAction'
import serialize from '@/api/resolvers/serial' import serialize from '@/api/resolvers/serial'
import { PAID_ACTION_PAYMENT_METHODS } from '@/lib/constants'
import { nextBillingWithGrace } from '@/lib/territory' import { nextBillingWithGrace } from '@/lib/territory'
import { datePivot } from '@/lib/time' import { datePivot } from '@/lib/time'
@ -37,12 +36,7 @@ export async function territoryBilling ({ data: { subName }, boss, models }) {
try { try {
const { result } = await performPaidAction('TERRITORY_BILLING', const { result } = await performPaidAction('TERRITORY_BILLING',
{ name: subName }, { { name: subName }, { models, me: sub.user, lnd, forceFeeCredits: true })
models,
me: sub.user,
lnd,
forcePaymentMethod: PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT
})
if (!result) { if (!result) {
throw new Error('not enough fee credits to auto-renew territory') 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) notifyWithdrawal(dbWdrwl.userId, wdrwl)
const { request: bolt11, secret: preimage } = wdrwl.payment const { request: bolt11, secret: preimage } = wdrwl.payment
logger?.ok( logger?.ok(
`↙ payment received: ${formatSats(msatsToSats(paid))}`, `↙ payment received: ${formatSats(msatsToSats(Number(wdrwl.payment.mtokens)))}`,
{ {
bolt11, bolt11,
preimage, preimage,

View File

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