diff --git a/api/paidAction/boost.js b/api/paidAction/boost.js index af96b4c83..cb374cead 100644 --- a/api/paidAction/boost.js +++ b/api/paidAction/boost.js @@ -33,12 +33,12 @@ export async function perform ({ invoiceId, sats, id: itemId, ...args }, { me, c } export async function retry ({ invoiceId, newInvoiceId }, { tx, cost }) { - await tx.itemAct.updateMany({ where: { invoiceId }, data: { invoiceId: newInvoiceId, invoiceActionState: 'PENDING' } }) const [{ id, path }] = await tx.$queryRaw` SELECT "Item".id, ltree2text(path) as path FROM "Item" JOIN "ItemAct" ON "Item".id = "ItemAct"."itemId" - WHERE "ItemAct"."invoiceId" = ${newInvoiceId}::INTEGER` + WHERE "ItemAct"."invoiceId" = ${invoiceId}::INTEGER` + await tx.itemAct.updateMany({ where: { invoiceId }, data: { invoiceId: newInvoiceId, invoiceActionState: 'PENDING' } }) return { id, sats: msatsToSats(cost), act: 'BOOST', path } } diff --git a/api/paidAction/downZap.js b/api/paidAction/downZap.js index 4266fbfa6..4075e7e42 100644 --- a/api/paidAction/downZap.js +++ b/api/paidAction/downZap.js @@ -34,12 +34,12 @@ export async function perform ({ invoiceId, sats, id: itemId }, { me, cost, tx } } export async function retry ({ invoiceId, newInvoiceId }, { tx, cost }) { - await tx.itemAct.updateMany({ where: { invoiceId }, data: { invoiceId: newInvoiceId, invoiceActionState: 'PENDING' } }) const [{ id, path }] = await tx.$queryRaw` SELECT "Item".id, ltree2text(path) as path FROM "Item" JOIN "ItemAct" ON "Item".id = "ItemAct"."itemId" - WHERE "ItemAct"."invoiceId" = ${newInvoiceId}::INTEGER` + WHERE "ItemAct"."invoiceId" = ${invoiceId}::INTEGER` + await tx.itemAct.updateMany({ where: { invoiceId }, data: { invoiceId: newInvoiceId, invoiceActionState: 'PENDING' } }) return { id, sats: msatsToSats(cost), act: 'DONT_LIKE_THIS', path } } diff --git a/api/paidAction/index.js b/api/paidAction/index.js index 9397a50c5..df072979f 100644 --- a/api/paidAction/index.js +++ b/api/paidAction/index.js @@ -34,9 +34,9 @@ export const paidActions = { RECEIVE } -export default async function performPaidAction (actionType, args, incomingContext) { +export default async function performPaidAction (actionType, args, { ...context }) { try { - const { me, models, forcePaymentMethod } = incomingContext + const { models } = context const paidAction = paidActions[actionType] console.group('performPaidAction', actionType, args) @@ -45,248 +45,265 @@ export default async function performPaidAction (actionType, args, incomingConte throw new Error(`Invalid action type ${actionType}`) } - if (!me && !paidAction.anonable) { + // add context properties + context.me = context.me ? await models.user.findUnique({ where: { id: context.me.id } }) : undefined + context.cost = await paidAction.getCost(args, context) + context.sybilFeePercent = await paidAction.getSybilFeePercent?.(args, context) + context.attempt ??= 0 // how many times the client thinks it has tried + context.forceInternal ??= false // use only internal payment methods + context.prioritizeInternal ??= false // prefer internal payment methods + context.description = context.me?.hideInvoiceDesc ? undefined : await paidAction.describe?.(args, context) + context.supportedPaymentMethods = paidAction.paymentMethods ?? await paidAction.getPaymentMethods?.(args, context) ?? [] + + const { + me, + forceInternal, + cost, + prioritizeInternal + } = context + + if (!me && !paidAction.anonable) { // action is not allowed for anons throw new Error('You must be logged in to perform this action') } - // 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) + if (cost === 0n) { // special case for zero cost actions + console.log('performing zero cost action') + return await performNoInvoiceAction(actionType, args, { ...context, paymentMethod: PAID_ACTION_PAYMENT_METHODS.ZERO_COST }) } - // 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' }) + // sort and filter supported payment methods + if (forceInternal) { + // we keep only the payment methods that qualify as internal payments + if (!me) { + throw new Error('user must be logged in to use internal payments') + } + const forcedPaymentMethods = [] + // reset the supported payment methods to only include internal methods + // that are supported by the action + if (context.supportedPaymentMethods.includes(PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT)) { + forcedPaymentMethods.push(PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT) + } + // TODO: add reward sats + // ... + if (forcedPaymentMethods.length === 0) { + throw new Error('action does not support internal payments') + } + context.supportedPaymentMethods = forcedPaymentMethods + } else if (prioritizeInternal) { + // prefer internal payment methods over the others (if they are supported) + const priority = { + [PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT]: -2 + // add other internal methods here + } + context.supportedPaymentMethods = context.supportedPaymentMethods.sort((a, b) => { + return priority[a] - priority[b] + }) } - for (const paymentMethod of paidAction.paymentMethods) { - console.log(`considering payment method ${paymentMethod}`) + const { supportedPaymentMethods } = context - if (forcePaymentMethod && - paymentMethod !== forcePaymentMethod) { - console.log('skipping payment method', paymentMethod, 'because forcePaymentMethod is set to', forcePaymentMethod) - continue - } + for (const paymentMethod of supportedPaymentMethods) { + console.log(`trying payment method ${paymentMethod}`) - // 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 + // p2p can fail for various reasons, if it does, we should try another payment method + console.error('paid action failed with P2P payment method, try another one', e) + continue } - } 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 { + try { + switch (paymentMethod) { + case PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT: { + if (!me || (me.msats ?? 0n) < cost) break // if anon or low balance skip 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) + // TODO: add reward sats + // ... + case PAID_ACTION_PAYMENT_METHODS.OPTIMISTIC: { + if (!me) break // anons are not optimistic + return await performOptimisticAction(actionType, args, context) + } + case PAID_ACTION_PAYMENT_METHODS.PESSIMISTIC: { + return await beginPessimisticAction(actionType, args, context) + } + } + } catch (e) { + console.error('performPaidAction failed with internal payment method', e) + // if we fail for reasons unrelated to balance, we should throw to fail the action + if (!e.message.includes('\\"users\\" violates check constraint \\"msats_positive\\"')) { + throw e } } } - throw new Error('No working payment method found') - } catch (e) { - console.error('performPaidAction failed', e) - throw e + // if we reach this point, no payment method succeeded + throw new Error('no payment method succeeded') } finally { console.groupEnd() } } -async function performNoInvoiceAction (actionType, args, incomingContext) { - const { me, models, cost, paymentMethod } = incomingContext +async function performNoInvoiceAction (actionType, args, { ...context }) { + const { me, models, cost, paymentMethod } = context const action = paidActions[actionType] - const result = await models.$transaction(async tx => { - const context = { ...incomingContext, tx } + const run = async tx => { + context.tx = tx - if (paymentMethod === 'FEE_CREDIT') { + if (paymentMethod === PAID_ACTION_PAYMENT_METHODS.FEE_CREDIT) { await tx.user.update({ where: { id: me?.id ?? USER_ID.anon }, data: { msats: { decrement: cost } } }) - } + } // add other internal methods here - const result = await action.perform(args, context) + const result = await performAction(null, action, args, context) await action.onPaid?.(result, context) return { result, - paymentMethod + paymentMethod, + retriable: false } - }, { isolationLevel: Prisma.TransactionIsolationLevel.ReadCommitted }) - + } + // if this is nested into another transaction (eg for retryPaidAction), use the parent transaction + const result = context.tx ? await run(context.tx) : await models.$transaction(run, { 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) - - return await models.$transaction(async tx => { - const context = { ...optimisticContext, tx, invoiceArgs } + context.optimistic = true + // create the invoice and perform the action immediately( invoiceArgs could be passed in by the p2p method) + const invoiceArgs = context.invoiceArgs ?? await createSNInvoice(context) - const invoice = await createDbInvoice(actionType, args, context) + const run = async tx => { + context.tx = tx + const invoice = await createDbInvoice(actionType, args, { ...context, invoiceArgs }) + const result = await performAction(invoice, action, args, context) return { invoice, - result: await action.perform?.({ invoiceId: invoice.id, ...args }, context), - paymentMethod: 'OPTIMISTIC' + result, + paymentMethod: 'OPTIMISTIC', + retriable: false } - }, { isolationLevel: Prisma.TransactionIsolationLevel.ReadCommitted }) -} - -async function beginPessimisticAction (actionType, args, context) { - const action = paidActions[actionType] - - 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 = context.invoiceArgs ?? await createSNInvoice(actionType, args, context) + // if this is nested into another transaction (eg for retryPaidAction), use the parent transaction + return context.tx ? await run(context.tx) : await models.$transaction(run, { isolationLevel: Prisma.TransactionIsolationLevel.ReadCommitted }) +} + +async function beginPessimisticAction (actionType, args, { ...context }) { + // just create the invoice and complete action when it's paid (invoiceArgs could be passed in by the p2p method) + const invoiceArgs = context.invoiceArgs ?? await createSNInvoice(context) return { invoice: await createDbInvoice(actionType, args, { ...context, invoiceArgs }), - paymentMethod: 'PESSIMISTIC' + paymentMethod: 'PESSIMISTIC', + retriable: false } } -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, me } = incomingContext - const sybilFeePercent = await paidActions[actionType].getSybilFeePercent?.(args, incomingContext) +async function performP2PAction (actionType, args, { ...context }) { + const { cost, models, lnd, sybilFeePercent, me, supportedPaymentMethods, description, attempt } = context if (!sybilFeePercent) { throw new Error('sybil fee percent is not set for an invoiceable peer action') } - const contextWithSybilFeePercent = { - ...incomingContext, - sybilFeePercent - } - - const userId = await paidActions[actionType]?.getInvoiceablePeer?.(args, contextWithSybilFeePercent) + const userId = await paidActions[actionType]?.getInvoiceablePeer?.(args, context) if (!userId) { throw new NonInvoiceablePeerError() } - await assertBelowMaxPendingInvoices(contextWithSybilFeePercent) + await assertBelowMaxPendingInvoices(context) + + // optimistic only if logged in and the action supports optimism + const optimistic = (me && supportedPaymentMethods.includes(PAID_ACTION_PAYMENT_METHODS.OPTIMISTIC)) - const description = await paidActions[actionType].describe(args, contextWithSybilFeePercent) - const { invoice, wrappedInvoice, wallet, maxFee } = await createWrappedInvoice(userId, { + const { invoice, wrappedInvoice, wallet, maxFee, retriable } = await createWrappedInvoice(userId, { msats: cost, feePercent: sybilFeePercent, description, - expiry: INVOICE_EXPIRE_SECS + expiry: INVOICE_EXPIRE_SECS, + walletOffset: attempt }, { models, me, lnd }) - const context = { - ...contextWithSybilFeePercent, - invoiceArgs: { - bolt11: invoice, - wrappedBolt11: wrappedInvoice, - wallet, - maxFee - } + context.invoiceArgs = { + bolt11: invoice, + wrappedBolt11: wrappedInvoice, + wallet, + maxFee } - return me - ? await performOptimisticAction(actionType, args, context) - : await beginPessimisticAction(actionType, args, context) + return { + retriable, + ...(optimistic + ? 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) - - const action = paidActions[actionType] - if (!action) { - throw new Error(`retryPaidAction - invalid action type ${actionType}`) - } +export async function retryPaidAction ({ invoiceId, forceInternal, attempt, prioritizeInternal }, { ...context }) { + const { models, me } = context if (!me) { - throw new Error(`retryPaidAction - must be logged in ${actionType}`) - } - - if (!action.paymentMethods.includes(PAID_ACTION_PAYMENT_METHODS.OPTIMISTIC)) { - throw new Error(`retryPaidAction - action does not support optimism ${actionType}`) + // otherwise every anon could retry retry other anons' actions + throw new Error('user must be logged in to retry paid actions') } - if (!action.retry) { - throw new Error(`retryPaidAction - action does not support retrying ${actionType}`) - } + const failedInvoice = await models.invoice.findUnique({ where: { id: invoiceId, userId: me.id } }) if (!failedInvoice) { - throw new Error(`retryPaidAction - missing invoice ${actionType}`) + throw new Error('invoice not found') } - const { msatsRequested, actionId } = failedInvoice - const retryContext = { - ...incomingContext, - optimistic: true, - me: await models.user.findUnique({ where: { id: me.id } }), - cost: BigInt(msatsRequested), - actionId + if (failedInvoice.actionState !== 'FAILED') { + // you should cancel the invoice before retrying the action! + throw new Error(`actions is not in a retriable state: ${failedInvoice.actionState}`) } - const invoiceArgs = await createSNInvoice(actionType, args, retryContext) + const actionType = failedInvoice.actionType - return await models.$transaction(async tx => { - const context = { ...retryContext, tx, invoiceArgs } - - // update the old invoice to RETRYING, so that it's not confused with FAILED - await tx.invoice.update({ - where: { - id: failedInvoice.id, - actionState: 'FAILED' - }, - data: { - actionState: 'RETRYING' - } - }) + const paidAction = paidActions[actionType] + if (!paidAction) { + throw new Error(`invalid action type ${actionType}`) + } - // create a new invoice - const invoice = await createDbInvoice(actionType, args, context) + const { msatsRequested, actionId, actionArgs } = failedInvoice + context.cost = msatsRequested + context.actionId = actionId + context.retryForInvoice = failedInvoice + context.forceInternal = forceInternal + context.attempt = attempt + context.prioritizeInternal = prioritizeInternal - return { - result: await action.retry({ invoiceId: failedInvoice.id, newInvoiceId: invoice.id }, context), - invoice, - paymentMethod: 'OPTIMISTIC' + return await models.$transaction(async tx => { + context.tx = tx + const supportRetrying = paidAction.retry + if (supportRetrying) { + // update the old invoice to RETRYING, so that it's not confused with FAILED + await tx.invoice.update({ + where: { + id: failedInvoice.id, + actionState: 'FAILED' + }, + data: { + actionState: 'RETRYING' + } + }) } + return await performPaidAction(actionType, actionArgs, context) }, { isolationLevel: Prisma.TransactionIsolationLevel.ReadCommitted }) } @@ -301,9 +318,9 @@ export class NonInvoiceablePeerError extends Error { // we seperate the invoice creation into two functions because // because if lnd is slow, it'll timeout the interactive tx -async function createSNInvoice (actionType, args, context) { - const { me, lnd, cost, optimistic } = context - const action = paidActions[actionType] +async function createSNInvoice (context) { + const { lnd, cost, optimistic, description } = context + const createLNDInvoice = optimistic ? createInvoice : createHodlInvoice await assertBelowMaxPendingInvoices(context) @@ -315,7 +332,7 @@ async function createSNInvoice (actionType, args, context) { const expiresAt = datePivot(new Date(), { seconds: INVOICE_EXPIRE_SECS }) const invoice = await createLNDInvoice({ - description: me?.hideInvoiceDesc ? undefined : await action.describe(args, context), + description, lnd, mtokens: String(cost), expires_at: expiresAt @@ -388,3 +405,12 @@ async function createDbInvoice (actionType, args, context) { return invoice } + +async function performAction (dbInvoice, paidAction, args, { ...context }) { + const { retryForInvoice } = context + if (retryForInvoice && paidAction.retry) { + return await paidAction.retry({ invoiceId: retryForInvoice.id, newInvoiceId: dbInvoice?.id }, context) + } else { + return await paidAction.perform?.({ invoiceId: dbInvoice?.id, ...args }, context) + } +} diff --git a/api/paidAction/itemCreate.js b/api/paidAction/itemCreate.js index 4b2b8bb9e..2fd0c0715 100644 --- a/api/paidAction/itemCreate.js +++ b/api/paidAction/itemCreate.js @@ -148,13 +148,15 @@ export async function perform (args, context) { } export async function retry ({ invoiceId, newInvoiceId }, { tx }) { + const res = (await tx.$queryRaw` + SELECT *, ltree2text(path) AS path, created_at AS "createdAt", updated_at AS "updatedAt" + FROM "Item" WHERE "invoiceId" = ${invoiceId}::INTEGER` + )[0] + res.invoiceId = newInvoiceId await tx.itemAct.updateMany({ where: { invoiceId }, data: { invoiceId: newInvoiceId, invoiceActionState: 'PENDING' } }) await tx.item.updateMany({ where: { invoiceId }, data: { invoiceId: newInvoiceId, invoiceActionState: 'PENDING' } }) await tx.upload.updateMany({ where: { invoiceId }, data: { invoiceId: newInvoiceId, invoiceActionState: 'PENDING' } }) - return (await tx.$queryRaw` - SELECT *, ltree2text(path) AS path, created_at AS "createdAt", updated_at AS "updatedAt" - FROM "Item" WHERE "invoiceId" = ${newInvoiceId}::INTEGER` - )[0] + return res } export async function onPaid ({ invoice, id }, context) { diff --git a/api/paidAction/pollVote.js b/api/paidAction/pollVote.js index c63ecef2e..31f2be337 100644 --- a/api/paidAction/pollVote.js +++ b/api/paidAction/pollVote.js @@ -41,11 +41,10 @@ export async function perform ({ invoiceId, id }, { me, cost, tx }) { } export async function retry ({ invoiceId, newInvoiceId }, { tx }) { + const { pollOptionId } = await tx.pollVote.findFirst({ where: { invoiceId } }) await tx.itemAct.updateMany({ where: { invoiceId }, data: { invoiceId: newInvoiceId, invoiceActionState: 'PENDING' } }) await tx.pollBlindVote.updateMany({ where: { invoiceId }, data: { invoiceId: newInvoiceId, invoiceActionState: 'PENDING' } }) await tx.pollVote.updateMany({ where: { invoiceId }, data: { invoiceId: newInvoiceId, invoiceActionState: 'PENDING' } }) - - const { pollOptionId } = await tx.pollVote.findFirst({ where: { invoiceId: newInvoiceId } }) return { id: pollOptionId } } diff --git a/api/paidAction/zap.js b/api/paidAction/zap.js index 51ac29b06..527553db2 100644 --- a/api/paidAction/zap.js +++ b/api/paidAction/zap.js @@ -60,12 +60,12 @@ export async function perform ({ invoiceId, sats, id: itemId, ...args }, { me, c } export async function retry ({ invoiceId, newInvoiceId }, { tx, cost }) { - await tx.itemAct.updateMany({ where: { invoiceId }, data: { invoiceId: newInvoiceId, invoiceActionState: 'PENDING' } }) const [{ id, path }] = await tx.$queryRaw` SELECT "Item".id, ltree2text(path) as path FROM "Item" JOIN "ItemAct" ON "Item".id = "ItemAct"."itemId" - WHERE "ItemAct"."invoiceId" = ${newInvoiceId}::INTEGER` + WHERE "ItemAct"."invoiceId" = ${invoiceId}::INTEGER` + await tx.itemAct.updateMany({ where: { invoiceId }, data: { invoiceId: newInvoiceId, invoiceActionState: 'PENDING' } }) return { id, sats: msatsToSats(cost), act: 'TIP', path } } diff --git a/api/resolvers/notifications.js b/api/resolvers/notifications.js index a7af37bc5..23aec8a6f 100644 --- a/api/resolvers/notifications.js +++ b/api/resolvers/notifications.js @@ -357,6 +357,11 @@ export default { "Invoice"."actionType" = 'POLL_VOTE' OR "Invoice"."actionType" = 'BOOST' ) + AND EXISTS ( + SELECT 1 + FROM "ItemAct" + WHERE "ItemAct"."invoiceId" = "Invoice".id + ) ORDER BY "sortTime" DESC LIMIT ${LIMIT})` ) diff --git a/api/resolvers/paidAction.js b/api/resolvers/paidAction.js index 3fc20c4e5..dabab1c66 100644 --- a/api/resolvers/paidAction.js +++ b/api/resolvers/paidAction.js @@ -33,6 +33,13 @@ export default { where: { id: invoiceId, userId: me?.id ?? USER_ID.anon + }, + include: { + invoiceForward: { + include: { + withdrawl: true + } + } } }) if (!invoice) { @@ -48,28 +55,11 @@ export default { } }, Mutation: { - retryPaidAction: async (parent, { invoiceId }, { models, me, lnd }) => { - if (!me) { - throw new Error('You must be logged in') - } - - const invoice = await models.invoice.findUnique({ where: { id: invoiceId, userId: me.id } }) - if (!invoice) { - throw new Error('Invoice not found') - } - - if (invoice.actionState !== 'FAILED') { - if (invoice.actionState === 'PAID') { - throw new Error('Invoice is already paid') - } - throw new Error(`Invoice is not in failed state: ${invoice.actionState}`) - } - - const result = await retryPaidAction(invoice.actionType, { invoice }, { models, me, lnd }) - + retryPaidAction: async (parent, { invoiceId, forceInternal, prioritizeInternal, attempt }, { models, me, lnd }) => { + const result = await retryPaidAction({ invoiceId, forceInternal, prioritizeInternal, attempt }, { models, me, lnd }) return { ...result, - type: paidActionType(invoice.actionType) + type: paidActionType(result.invoice.actionType) } } }, diff --git a/api/typeDefs/paidAction.js b/api/typeDefs/paidAction.js index 56dd74323..212c8213a 100644 --- a/api/typeDefs/paidAction.js +++ b/api/typeDefs/paidAction.js @@ -7,7 +7,7 @@ extend type Query { } extend type Mutation { - retryPaidAction(invoiceId: Int!): PaidAction! + retryPaidAction(invoiceId: Int!, forceInternal: Boolean, attempt: Int, prioritizeInternal: Boolean): PaidAction! } enum PaymentMethod { @@ -20,36 +20,42 @@ enum PaymentMethod { interface PaidAction { invoice: Invoice paymentMethod: PaymentMethod! + retriable: Boolean } type ItemPaidAction implements PaidAction { result: Item invoice: Invoice paymentMethod: PaymentMethod! + retriable: Boolean } type ItemActPaidAction implements PaidAction { result: ItemActResult invoice: Invoice paymentMethod: PaymentMethod! + retriable: Boolean } type PollVotePaidAction implements PaidAction { result: PollVoteResult invoice: Invoice paymentMethod: PaymentMethod! + retriable: Boolean } type SubPaidAction implements PaidAction { result: Sub invoice: Invoice paymentMethod: PaymentMethod! + retriable: Boolean } type DonatePaidAction implements PaidAction { result: DonateResult invoice: Invoice paymentMethod: PaymentMethod! + retriable: Boolean } ` diff --git a/api/typeDefs/wallet.js b/api/typeDefs/wallet.js index a7b32ad55..6e555d828 100644 --- a/api/typeDefs/wallet.js +++ b/api/typeDefs/wallet.js @@ -123,6 +123,15 @@ const typeDefs = ` item: Item itemAct: ItemAct forwardedSats: Int + invoiceForward: InvoiceForwardStatus + } + + type InvoiceForwardStatus { + withdrawl: WithdrawlStatus + } + + type WithdrawlStatus { + status: String } type Withdrawl { diff --git a/components/item-act.js b/components/item-act.js index 36d5a0c77..bbb87b373 100644 --- a/components/item-act.js +++ b/components/item-act.js @@ -272,7 +272,7 @@ export function useZap () { const sats = nextTip(meSats, { ...me?.privates }) const variables = { id: item.id, sats, act: 'TIP' } - const optimisticResponse = { act: { __typename: 'ItemActPaidAction', result: { path: item.path, ...variables } } } + const optimisticResponse = { act: { __typename: 'ItemActPaidAction', result: { path: item.path, ...variables }, retriable: true } } try { await abortSignal.pause({ me, amount: sats }) diff --git a/components/pay-bounty.js b/components/pay-bounty.js index 35c5ebe60..83d0bd6c6 100644 --- a/components/pay-bounty.js +++ b/components/pay-bounty.js @@ -50,7 +50,7 @@ export default function PayBounty ({ children, item }) { const variables = { id: item.id, sats: root.bounty, act: 'TIP' } const act = useAct({ variables, - optimisticResponse: { act: { __typename: 'ItemActPaidAction', result: { ...variables, path: item.path } } }, + optimisticResponse: { act: { __typename: 'ItemActPaidAction', result: { ...variables, path: item.path }, retriable: true } }, ...payBountyCacheMods }) diff --git a/components/payment.js b/components/payment.js index 175ca2b3b..526a23122 100644 --- a/components/payment.js +++ b/components/payment.js @@ -112,9 +112,10 @@ const invoiceController = (id, isInvoice) => { export const useWalletPayment = () => { const invoice = useInvoice() - const wallet = useWallet() + const defaultWallet = useWallet() - const waitForWalletPayment = useCallback(async ({ id, bolt11 }, waitFor) => { + const waitForWalletPayment = useCallback(async ({ id, bolt11 }, waitFor, wallet) => { + wallet ??= defaultWallet if (!wallet) { throw new NoAttachedWalletError() } @@ -134,7 +135,7 @@ export const useWalletPayment = () => { } finally { controller.stop() } - }, [wallet, invoice]) + }, [defaultWallet, invoice]) return waitForWalletPayment } diff --git a/components/poll.js b/components/poll.js index dc694f801..264ef8e16 100644 --- a/components/poll.js +++ b/components/poll.js @@ -23,7 +23,7 @@ export default function Poll ({ item }) { onClick={me ? async () => { const variables = { id: v.id } - const optimisticResponse = { pollVote: { __typename: 'PollVotePaidAction', result: { id: v.id } } } + const optimisticResponse = { pollVote: { __typename: 'PollVotePaidAction', result: { id: v.id }, retriable: true } } try { const { error } = await pollVote({ variables, diff --git a/components/qr.js b/components/qr.js index 23757ba33..837468ad8 100644 --- a/components/qr.js +++ b/components/qr.js @@ -2,7 +2,7 @@ import { QRCodeSVG } from 'qrcode.react' import { CopyInput, InputSkeleton } from './form' import InvoiceStatus from './invoice-status' import { useEffect } from 'react' -import { useWallet } from '@/wallets/index' +import { useEnabledWallets } from '@/wallets/index' import Bolt11Info from './bolt11-info' export const qrImageSettings = { @@ -16,20 +16,23 @@ export const qrImageSettings = { export default function Qr ({ asIs, value, useWallet: automated, statusVariant, description, status }) { const qrValue = asIs ? value : 'lightning:' + value.toUpperCase() - const wallet = useWallet() + const senderWallets = useEnabledWallets() useEffect(() => { async function effect () { - if (automated && wallet) { - try { - await wallet.sendPayment(value) - } catch (e) { - console.log(e?.message) + if (automated && senderWallets.length > 0) { + for (const wallet of senderWallets) { + try { + await wallet.sendPayment(value) + break + } catch (e) { + console.log(e?.message) + } } } } effect() - }, [wallet]) + }, [senderWallets]) return ( <> diff --git a/components/use-paid-mutation.js b/components/use-paid-mutation.js index 765508b5f..e913577e0 100644 --- a/components/use-paid-mutation.js +++ b/components/use-paid-mutation.js @@ -2,8 +2,10 @@ import { useApolloClient, useLazyQuery, useMutation } from '@apollo/client' import { useCallback, useState } from 'react' import { useInvoice, useQrPayment, useWalletPayment } from './payment' import { InvoiceCanceledError, InvoiceExpiredError } from '@/wallets/errors' -import { GET_PAID_ACTION } from '@/fragments/paidAction' +import { GET_PAID_ACTION, RETRY_PAID_ACTION } from '@/fragments/paidAction' +import { useEnabledWallets } from '@/wallets/index' +import { useMe } from './me' /* this is just like useMutation with a few changes: 1. pays an invoice returned by the mutation @@ -23,39 +25,144 @@ export function usePaidMutation (mutation, const [getPaidAction] = useLazyQuery(GET_PAID_ACTION, { fetchPolicy: 'network-only' }) + const [retryPaidAction] = useMutation(RETRY_PAID_ACTION) const waitForWalletPayment = useWalletPayment() const invoiceHelper = useInvoice() const waitForQrPayment = useQrPayment() const client = useApolloClient() // innerResult is used to store/control the result of the mutation when innerMutate runs const [innerResult, setInnerResult] = useState(result) + const { me } = useMe() - const waitForPayment = useCallback(async (invoice, { alwaysShowQROnFailure = false, persistOnNavigate = false, waitFor }) => { - let walletError - const start = Date.now() + const senderWallets = useEnabledWallets().map(w => { + return { ...w, failed: false } + }) + + const addPayError = (e, rest) => ({ + ...rest, + payError: e, + error: e instanceof InvoiceCanceledError && e.actionError ? e : undefined + }) + + const waitForActionPayment = useCallback(async (invoice, { alwaysShowQROnFailure = false, persistOnNavigate = false, waitFor }, originalResponse, action) => { + const walletErrors = [] + let response = originalResponse + let invoiceUsed = false + + const cancelInvoice = async () => { + try { + invoiceUsed = true + await invoiceHelper.cancel(invoice) + console.log('old invoice canceled') + } catch (err) { + console.error('could not cancel old invoice', err) + } + } + + // ensures every invoice is used only once + const refreshInvoice = async (attempt = 0) => { + if (invoiceUsed) { + await cancelInvoice() + const retry = await retryPaidAction({ variables: { invoiceId: parseInt(invoice.id), attempt } }) + response = retry.data?.retryPaidAction + invoice = response?.invoice + invoiceUsed = true + } else invoiceUsed = true + } + + // if anon we go straight to qr code + if (!me) { + await refreshInvoice() + await waitForQrPayment(invoice, null, { persistOnNavigate, waitFor }) + return { invoice, response } + } + + const paymentAttemptStartTime = Date.now() + // we try with attached wallets + let attempt = 0 + while (true) { + await refreshInvoice(attempt) + if (!invoice) return { invoice, response } + + // first non failed sender wallet + const senderWallet = senderWallets.find(w => !w.failed) + if (!senderWallet) { + console.log('no sender wallet available') + break + } + + try { + console.log('trying to pay with wallet', senderWallet.def.name) + await waitForWalletPayment(invoice, waitFor, senderWallet) + console.log('paid with wallet', senderWallet.def.name) + return { invoice, response } + } catch (err) { + if (err instanceof InvoiceCanceledError || err instanceof InvoiceExpiredError) { + // bail early if the invoice was canceled or expired + throw err + } + walletErrors.push(err) + // get action data + const { data: paidActionData } = await getPaidAction({ variables: { invoiceId: parseInt(invoice.id) } }) + const hasWithdrawl = !!paidActionData.invoice?.invoiceForward?.withdrawl + if (hasWithdrawl) { + // SN received the payment but couldn't forward it, we try another receiver with the same sender + if (!response.retriable) { // we are out of receivers + console.log('the receiver wallet failed to receive the payment, but we exhausted all options') + break + } + console.log('the receiver wallet failed to receive the payment, will try another one') + attempt++ + } else { + // SN didn't receive the payment, so the sender must have failed + senderWallet.failed = true + console.log('the sender wallet failed to pay the invoice', senderWallet.def.name) + } + } + } + + // we try an internal payment try { - return await waitForWalletPayment(invoice, waitFor) - } catch (err) { - if ( - (!alwaysShowQROnFailure && Date.now() - start > 1000) || - err instanceof InvoiceCanceledError || - err instanceof InvoiceExpiredError) { - // bail since qr code payment will also fail - // also bail if the payment took more than 1 second - // and cancel the invoice if it's not already canceled so it can be retried - invoiceHelper.cancel(invoice).catch(console.error) - throw err + console.log('could not pay with any wallet... will try with an internal payment...') + await cancelInvoice() + const retry = await retryPaidAction({ variables: { invoiceId: parseInt(invoice.id), prioritizeInternal: true } }) + response = retry.data?.retryPaidAction + invoice = response?.invoice + if (!invoice) { + return { response } + } else { + // if the internal payment returned an invoice, it means it failed + // maybe the user doesn't have enough credits. + invoiceUsed = false } - walletError = err + } catch (err) { + console.log('could not pay with internal payment... will fallback to another method') + walletErrors.push(err) } - return await waitForQrPayment(invoice, walletError, { persistOnNavigate, waitFor }) - }, [waitForWalletPayment, waitForQrPayment, invoiceHelper]) + + // last resort, show qr code or fail + + // we don't show the qr if too much time has passed from the payment attempt, this prevents + // very slow payments from resulting in a qr codes being shown unexpectedly during the user navigation + const failedEarly = paymentAttemptStartTime - Date.now() < 1000 + if (alwaysShowQROnFailure || failedEarly) { + console.log('show qr code for manual payment') + await refreshInvoice(attempt) + await waitForQrPayment(invoice, walletErrors[walletErrors.length - 1], { persistOnNavigate, waitFor }) + } else { + console.log('we are out of options, we will throw the errors') + cancelInvoice().catch(console.error) + throw new Error(walletErrors.map(e => e.message).join('\n')) + } + + return { invoice, response } + }, [waitForWalletPayment, waitForQrPayment, invoiceHelper, senderWallets]) const innerMutate = useCallback(async ({ onCompleted: innerOnCompleted, ...innerOptions } = {}) => { innerOptions.optimisticResponse = addOptimisticResponseExtras(innerOptions.optimisticResponse) - let { data, ...rest } = await mutate(innerOptions) + let { data, ...rest } = await mutate({ ...innerOptions }) // use the most inner callbacks/options if they exist const { @@ -75,62 +182,52 @@ export function usePaidMutation (mutation, if (invoice) { // adds payError, escalating to a normal error if the invoice is not canceled or // has an actionError - const addPayError = (e, rest) => ({ - ...rest, - payError: e, - error: e instanceof InvoiceCanceledError && e.actionError ? e : undefined - }) + const wait = response?.paymentMethod !== 'OPTIMISTIC' || forceWaitForPayment + const alwaysShowQROnFailure = options.alwaysShowQROnFailure ?? innerOptions.alwaysShowQROnFailure ?? wait // should we wait for the invoice to be paid? - if (response?.paymentMethod === 'OPTIMISTIC' && !forceWaitForPayment) { + if (!wait) { // onCompleted is called before the invoice is paid for optimistic updates ourOnCompleted?.(data) - // don't wait to pay the invoice - waitForPayment(invoice, { persistOnNavigate, waitFor }).then(() => { - onPaid?.(client.cache, { data }) - }).catch(e => { - console.error('usePaidMutation: failed to pay invoice', e) - // onPayError is called after the invoice fails to pay - // useful for updating invoiceActionState to FAILED - onPayError?.(e, client.cache, { data }) - setInnerResult(r => addPayError(e, r)) - }) } else { - // the action is pessimistic - try { - // wait for the invoice to be paid - await waitForPayment(invoice, { alwaysShowQROnFailure: true, persistOnNavigate, waitFor }) - if (!response.result) { - // if the mutation didn't return any data, ie pessimistic, we need to fetch it - const { data: { paidAction } } = await getPaidAction({ variables: { invoiceId: parseInt(invoice.id) } }) - // create new data object - // ( hmac is only returned on invoice creation so we need to add it back to the data ) - data = { - [Object.keys(data)[0]]: { - ...paidAction, - invoice: { ...paidAction.invoice, hmac: invoice.hmac } - } + setInnerResult({ data, ...rest }) + } + // don't wait to pay the invoice + const p = waitForActionPayment(invoice, { alwaysShowQROnFailure, persistOnNavigate, waitFor }, response, innerOptions).then(async ({ invoice, response }) => { + if (!response.result) { // supposedly this is never the case for optimistic actions + // if the mutation didn't return any data, ie pessimistic, we need to fetch it + const { data: { paidAction } } = await getPaidAction({ variables: { invoiceId: parseInt(invoice.id) } }) + // create new data object + // ( hmac is only returned on invoice creation so we need to add it back to the data ) + data = { + [Object.keys(data)[0]]: { + ...paidAction, + invoice: { ...paidAction.invoice, hmac: invoice.hmac } } - // we need to run update functions on mutations now that we have the data - update?.(client.cache, { data }) } - ourOnCompleted?.(data) - onPaid?.(client.cache, { data }) - } catch (e) { - console.error('usePaidMutation: failed to pay invoice', e) - onPayError?.(e, client.cache, { data }) - rest = addPayError(e, rest) + // we need to run update functions on mutations now that we have the data + update?.(client.cache, { data }) } - } + if (wait) ourOnCompleted?.(data) + onPaid?.(client.cache, { data }) + setInnerResult({ data, ...rest }) + }).catch(e => { + console.error('usePaidMutation: failed to pay invoice', e) + // onPayError is called after the invoice fails to pay + // useful for updating invoiceActionState to FAILED + onPayError?.(e, client.cache, { data }) + setInnerResult(r => addPayError(e, r)) + }) + + if (wait) await p } else { // fee credits paid for it ourOnCompleted?.(data) onPaid?.(client.cache, { data }) } - setInnerResult({ data, ...rest }) return { data, ...rest } - }, [mutate, options, waitForPayment, onCompleted, client.cache, getPaidAction, setInnerResult]) + }, [mutate, options, waitForActionPayment, onCompleted, client.cache, getPaidAction, setInnerResult]) return [innerMutate, innerResult] } @@ -139,7 +236,7 @@ export function usePaidMutation (mutation, function addOptimisticResponseExtras (optimisticResponse) { if (!optimisticResponse) return optimisticResponse const key = Object.keys(optimisticResponse)[0] - optimisticResponse[key] = { invoice: null, paymentMethod: 'OPTIMISTIC', ...optimisticResponse[key] } + optimisticResponse[key] = { invoice: null, paymentMethod: 'OPTIMISTIC', retriable: true, ...optimisticResponse[key] } return optimisticResponse } diff --git a/fragments/paidAction.js b/fragments/paidAction.js index c47fa7005..576eea20c 100644 --- a/fragments/paidAction.js +++ b/fragments/paidAction.js @@ -13,6 +13,7 @@ export const PAID_ACTION = gql` ...InvoiceFields } paymentMethod + retriable }` const ITEM_PAID_ACTION_FIELDS = gql` @@ -88,8 +89,10 @@ export const RETRY_PAID_ACTION = gql` ${PAID_ACTION} ${ITEM_PAID_ACTION_FIELDS} ${ITEM_ACT_PAID_ACTION_FIELDS} - mutation retryPaidAction($invoiceId: Int!) { - retryPaidAction(invoiceId: $invoiceId) { + ${SUB_FULL_FIELDS} + + mutation retryPaidAction($invoiceId: Int!, $forceInternal: Boolean, $attempt: Int, $prioritizeInternal: Boolean) { + retryPaidAction(invoiceId: $invoiceId, forceInternal: $forceInternal, attempt: $attempt, prioritizeInternal: $prioritizeInternal) { __typename ...PaidActionFields ... on ItemPaidAction { @@ -103,6 +106,16 @@ export const RETRY_PAID_ACTION = gql` id } } + ... on SubPaidAction { + result { + ...SubFullFields + } + } + ... on DonatePaidAction { + result { + sats + } + } } }` diff --git a/fragments/wallet.js b/fragments/wallet.js index fe1f543b3..0705eb256 100644 --- a/fragments/wallet.js +++ b/fragments/wallet.js @@ -3,6 +3,7 @@ import { ITEM_FULL_FIELDS } from './items' import { VAULT_ENTRY_FIELDS } from './vault' export const INVOICE_FIELDS = gql` + fragment InvoiceFields on Invoice { id hash @@ -21,6 +22,11 @@ export const INVOICE_FIELDS = gql` actionType actionError confirmedPreimage + invoiceForward { + withdrawl { + status + } + } forwardedSats }` diff --git a/lib/constants.js b/lib/constants.js index 4e734355b..5950eae9c 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -7,7 +7,8 @@ export const PAID_ACTION_PAYMENT_METHODS = { FEE_CREDIT: 'FEE_CREDIT', PESSIMISTIC: 'PESSIMISTIC', OPTIMISTIC: 'OPTIMISTIC', - P2P: 'P2P' + P2P: 'P2P', + ZERO_COST: 'ZERO_COST' } export const PAID_ACTION_TERMINAL_STATES = ['FAILED', 'PAID', 'RETRYING'] export const NOFOLLOW_LIMIT = 1000 diff --git a/wallets/index.js b/wallets/index.js index 719e230f6..50095480a 100644 --- a/wallets/index.js +++ b/wallets/index.js @@ -215,6 +215,15 @@ export function useWallets () { return useContext(WalletsContext) } +export function useEnabledWallets () { + const { wallets } = useContext(WalletsContext) + // walletDefs shouldn't change on rerender, so it should be safe + return wallets + .map(w => useWallet(w.def.name)) + .filter(w => !w.def.isAvailable || w.def.isAvailable()) + .filter(w => w.config?.enabled && canSend(w)) +} + export function useWallet (name) { const { wallets } = useWallets() diff --git a/wallets/server.js b/wallets/server.js index 9ee2e71ef..6f77c6742 100644 --- a/wallets/server.js +++ b/wallets/server.js @@ -14,7 +14,7 @@ import * as webln from '@/wallets/webln' import { walletLogger } from '@/api/resolvers/wallet' import walletDefs from '@/wallets/server' import { parsePaymentRequest } from 'ln-service' -import { toPositiveBigInt, toPositiveNumber } from '@/lib/validate' +import { toNumber, toPositiveBigInt, toPositiveNumber } from '@/lib/validate' import { PAID_ACTION_TERMINAL_STATES } from '@/lib/constants' import { withTimeout } from '@/lib/time' import { canReceive } from './common' @@ -25,27 +25,41 @@ export default [lnd, cln, lnAddr, lnbits, nwc, phoenixd, blink, lnc, webln] const MAX_PENDING_INVOICES_PER_WALLET = 25 -export async function createInvoice (userId, { msats, description, descriptionHash, expiry = 360 }, { models }) { +export async function createInvoice (userId, { msats, description, descriptionHash, expiry = 360, wrap = false, feePercent, walletOffset = 0 }, { models, me, lnd }) { // get the wallets in order of priority const wallets = await getInvoiceableWallets(userId, { models }) - msats = toPositiveNumber(msats) + msats = toPositiveBigInt(msats) + + let innerMsats = msats + if (wrap) { + if (!feePercent) throw new Error('feePercent is required for wrapped invoices') + innerMsats = msats * (100n - feePercent) / 100n + } + + const offset = toNumber(Math.min(walletOffset, wallets.length), 0, wallets.length) + for (let i = offset; i < wallets.length; i++) { + const { def, wallet } = wallets[i] + + const config = wallet.wallet + if (!canReceive({ def, config })) { + continue + } - for (const { def, wallet } of wallets) { const logger = walletLogger({ wallet, models }) try { logger.info( `↙ incoming payment: ${formatSats(msatsToSats(msats))}`, { - amount: formatMsats(msats) - }) + amount: formatMsats(toNumber(msats)) + }) // TODO add fee info? let invoice try { invoice = await walletCreateInvoice( { wallet, def }, - { msats, description, descriptionHash, expiry }, + { msats: innerMsats, description, descriptionHash, expiry }, { logger, models }) } catch (err) { throw new Error('failed to create invoice: ' + err.message) @@ -57,21 +71,35 @@ export async function createInvoice (userId, { msats, description, descriptionHa bolt11: invoice }) - if (BigInt(bolt11.mtokens) !== BigInt(msats)) { - if (BigInt(bolt11.mtokens) > BigInt(msats)) { + if (BigInt(bolt11.mtokens) !== msats) { + if (BigInt(bolt11.mtokens) > msats) { throw new Error('invoice invalid: amount too big') } if (BigInt(bolt11.mtokens) === 0n) { throw new Error('invoice invalid: amount is 0 msats') } - if (BigInt(msats) - BigInt(bolt11.mtokens) >= 1000n) { + if (innerMsats - BigInt(bolt11.mtokens) >= 1000n) { throw new Error('invoice invalid: amount too small') } logger.warn('wallet does not support msats') } - return { invoice, wallet, logger } + let wrappedInvoice + let maxFee + + if (wrap) { + const wrappedInvoiceData = + await wrapInvoice( + { bolt11: invoice, feePercent }, + { msats, description, descriptionHash }, + { me, lnd } + ) + wrappedInvoice = wrappedInvoiceData.invoice.request + maxFee = wrappedInvoiceData.maxFee + } + + return { invoice, wallet, logger, wrappedInvoice, maxFee, retriable: i < wallets.length - 1 } } catch (err) { logger.error(err.message) } @@ -81,34 +109,18 @@ export async function createInvoice (userId, { msats, description, descriptionHa } export async function createWrappedInvoice (userId, - { msats, feePercent, description, descriptionHash, expiry = 360 }, + { msats, feePercent, description, descriptionHash, expiry = 360, walletOffset = 0 }, { models, me, lnd }) { - let logger, bolt11 - try { - const { invoice, wallet } = await createInvoice(userId, { - // this is the amount the stacker will receive, the other (feePercent)% is our fee - msats: toPositiveBigInt(msats) * (100n - feePercent) / 100n, - description, - descriptionHash, - expiry - }, { models }) - - logger = walletLogger({ wallet, models }) - bolt11 = invoice - - const { invoice: wrappedInvoice, maxFee } = - await wrapInvoice({ bolt11, feePercent }, { msats, description, descriptionHash }, { me, lnd }) - - return { - invoice, - wrappedInvoice: wrappedInvoice.request, - wallet, - maxFee - } - } catch (e) { - logger?.error('invalid invoice: ' + e.message, { bolt11 }) - throw e - } + return await createInvoice(userId, { + // this is the amount the stacker will receive, the other (feePercent)% is our fee + msats, + feePercent, + wrap: true, + description, + descriptionHash, + expiry, + walletOffset + }, { models, me, lnd }) } export async function getInvoiceableWallets (userId, { models }) { @@ -132,12 +144,15 @@ export async function getInvoiceableWallets (userId, { models }) { return walletsWithDefs.filter(({ def, wallet }) => canReceive({ def, config: wallet.wallet })) } -async function walletCreateInvoice ({ wallet, def }, { - msats, - description, - descriptionHash, - expiry = 360 -}, { logger, models }) { +async function walletCreateInvoice ( + { wallet, def }, + { + msats, + description, + descriptionHash, + expiry = 360 + }, + { logger, models }) { // check for pending withdrawals const pendingWithdrawals = await models.withdrawl.count({ where: { @@ -166,7 +181,7 @@ async function walletCreateInvoice ({ wallet, def }, { return await withTimeout( def.createInvoice( { - msats, + msats: toPositiveNumber(msats), // TODO: should probably make the wallet interface work with bigints description: wallet.user.hideInvoiceDesc ? undefined : description, descriptionHash, expiry diff --git a/worker/territory.js b/worker/territory.js index e687b6125..87c2e4cf5 100644 --- a/worker/territory.js +++ b/worker/territory.js @@ -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, forceInternal: true }) if (!result) { throw new Error('not enough fee credits to auto-renew territory') } diff --git a/worker/weeklyPosts.js b/worker/weeklyPosts.js index 275b23bab..5b764b7f5 100644 --- a/worker/weeklyPosts.js +++ b/worker/weeklyPosts.js @@ -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, forceInternal: 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, forceInternal: true }) }