Compare commits
2 Commits
8c43caed80
...
79ada2ab58
Author | SHA1 | Date |
---|---|---|
ekzyis | 79ada2ab58 | |
Riccardo Balbo | 9c55f1ebe2 |
|
@ -103,6 +103,7 @@ stateDiagram-v2
|
|||
| donations | x | | x | x | x | | |
|
||||
| update posts | x | | x | | x | | x |
|
||||
| update comments | x | | x | | x | | x |
|
||||
| receive | | x | | x | x | x | x |
|
||||
|
||||
## Not-custodial zaps (ie p2p wrapped payments)
|
||||
Zaps, and possibly other future actions, can be performed peer to peer and non-custodially. This means that the payment is made directly from the client to the recipient, without the server taking custody of the funds. Currently, in order to trigger this behavior, the recipient must have a receiving wallet attached and the sender must have insufficient funds in their custodial wallet to perform the requested zap.
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
import { createHodlInvoice, createInvoice, parsePaymentRequest } from 'ln-service'
|
||||
import { datePivot } from '@/lib/time'
|
||||
import { PAID_ACTION_PAYMENT_METHODS, PAID_ACTION_TERMINAL_STATES, USER_ID } from '@/lib/constants'
|
||||
import { PAID_ACTION_PAYMENT_METHODS, USER_ID } from '@/lib/constants'
|
||||
import { createHmac } from '@/api/resolvers/wallet'
|
||||
import { Prisma } from '@prisma/client'
|
||||
import { createWrappedInvoice } from '@/wallets/server'
|
||||
import { assertBelowMaxPendingInvoices } from './lib/assert'
|
||||
|
||||
import * as ITEM_CREATE from './itemCreate'
|
||||
import * as ITEM_UPDATE from './itemUpdate'
|
||||
import * as ZAP from './zap'
|
||||
|
@ -14,7 +17,7 @@ import * as TERRITORY_BILLING from './territoryBilling'
|
|||
import * as TERRITORY_UNARCHIVE from './territoryUnarchive'
|
||||
import * as DONATE from './donate'
|
||||
import * as BOOST from './boost'
|
||||
import { createWrappedInvoice } from 'wallets/server'
|
||||
import * as RECEIVE from './receive'
|
||||
|
||||
export const paidActions = {
|
||||
ITEM_CREATE,
|
||||
|
@ -27,7 +30,8 @@ export const paidActions = {
|
|||
TERRITORY_UPDATE,
|
||||
TERRITORY_BILLING,
|
||||
TERRITORY_UNARCHIVE,
|
||||
DONATE
|
||||
DONATE,
|
||||
RECEIVE
|
||||
}
|
||||
|
||||
export default async function performPaidAction (actionType, args, incomingContext) {
|
||||
|
@ -52,8 +56,7 @@ export default async function performPaidAction (actionType, args, incomingConte
|
|||
}
|
||||
const context = {
|
||||
...contextWithMe,
|
||||
cost: await paidAction.getCost(args, contextWithMe),
|
||||
sybilFeePercent: await paidAction.getSybilFeePercent?.(args, contextWithMe)
|
||||
cost: await paidAction.getCost(args, contextWithMe)
|
||||
}
|
||||
|
||||
// special case for zero cost actions
|
||||
|
@ -183,19 +186,25 @@ async function beginPessimisticAction (actionType, args, 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
|
||||
const { cost, models, lnd, me } = incomingContext
|
||||
const sybilFeePercent = await paidActions[actionType].getSybilFeePercent?.(args, 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)
|
||||
const contextWithSybilFeePercent = {
|
||||
...incomingContext,
|
||||
sybilFeePercent
|
||||
}
|
||||
|
||||
const userId = await paidActions[actionType]?.getInvoiceablePeer?.(args, contextWithSybilFeePercent)
|
||||
if (!userId) {
|
||||
throw new NonInvoiceablePeerError()
|
||||
}
|
||||
|
||||
await assertBelowMaxPendingInvoices(incomingContext)
|
||||
await assertBelowMaxPendingInvoices(contextWithSybilFeePercent)
|
||||
|
||||
const description = await paidActions[actionType].describe(args, incomingContext)
|
||||
const description = await paidActions[actionType].describe(args, contextWithSybilFeePercent)
|
||||
const { invoice, wrappedInvoice, wallet, maxFee } = await createWrappedInvoice(userId, {
|
||||
msats: cost,
|
||||
feePercent: sybilFeePercent,
|
||||
|
@ -204,7 +213,7 @@ async function performP2PAction (actionType, args, incomingContext) {
|
|||
}, { models, me, lnd })
|
||||
|
||||
const context = {
|
||||
...incomingContext,
|
||||
...contextWithSybilFeePercent,
|
||||
invoiceArgs: {
|
||||
bolt11: invoice,
|
||||
wrappedBolt11: wrappedInvoice,
|
||||
|
@ -282,23 +291,6 @@ export async function retryPaidAction (actionType, args, incomingContext) {
|
|||
}
|
||||
|
||||
const INVOICE_EXPIRE_SECS = 600
|
||||
const MAX_PENDING_PAID_ACTIONS_PER_USER = 100
|
||||
|
||||
export async function assertBelowMaxPendingInvoices (context) {
|
||||
const { models, me } = context
|
||||
const pendingInvoices = await models.invoice.count({
|
||||
where: {
|
||||
userId: me?.id ?? USER_ID.anon,
|
||||
actionState: {
|
||||
notIn: PAID_ACTION_TERMINAL_STATES
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (pendingInvoices >= MAX_PENDING_PAID_ACTIONS_PER_USER) {
|
||||
throw new Error('You have too many pending paid actions, cancel some or wait for them to expire')
|
||||
}
|
||||
}
|
||||
|
||||
export class NonInvoiceablePeerError extends Error {
|
||||
constructor () {
|
||||
|
@ -314,6 +306,8 @@ async function createSNInvoice (actionType, args, context) {
|
|||
const action = paidActions[actionType]
|
||||
const createLNDInvoice = optimistic ? createInvoice : createHodlInvoice
|
||||
|
||||
await assertBelowMaxPendingInvoices(context)
|
||||
|
||||
if (cost < 1000n) {
|
||||
// sanity check
|
||||
throw new Error('The cost of the action must be at least 1 sat')
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
import { BALANCE_LIMIT_MSATS, PAID_ACTION_TERMINAL_STATES, USER_ID, SN_ADMIN_IDS } from '@/lib/constants'
|
||||
import { msatsToSats, numWithUnits } from '@/lib/format'
|
||||
|
||||
const MAX_PENDING_PAID_ACTIONS_PER_USER = 100
|
||||
const USER_IDS_BALANCE_NO_LIMIT = [...SN_ADMIN_IDS, USER_ID.anon, USER_ID.ad]
|
||||
|
||||
export async function assertBelowMaxPendingInvoices (context) {
|
||||
const { models, me } = context
|
||||
const pendingInvoices = await models.invoice.count({
|
||||
where: {
|
||||
userId: me?.id ?? USER_ID.anon,
|
||||
actionState: {
|
||||
notIn: PAID_ACTION_TERMINAL_STATES
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (pendingInvoices >= MAX_PENDING_PAID_ACTIONS_PER_USER) {
|
||||
throw new Error('You have too many pending paid actions, cancel some or wait for them to expire')
|
||||
}
|
||||
}
|
||||
|
||||
export async function assertBelowBalanceLimit (context) {
|
||||
const { me, tx } = context
|
||||
if (!me || USER_IDS_BALANCE_NO_LIMIT.includes(me.id)) return
|
||||
|
||||
// we need to prevent this invoice (and any other pending invoices and withdrawls)
|
||||
// from causing the user's balance to exceed the balance limit
|
||||
const pendingInvoices = await tx.invoice.aggregate({
|
||||
where: {
|
||||
userId: me.id,
|
||||
// p2p invoices are never in state PENDING
|
||||
actionState: 'PENDING',
|
||||
actionType: 'RECEIVE'
|
||||
},
|
||||
_sum: {
|
||||
msatsRequested: true
|
||||
}
|
||||
})
|
||||
|
||||
// Get pending withdrawals total
|
||||
const pendingWithdrawals = await tx.withdrawl.aggregate({
|
||||
where: {
|
||||
userId: me.id,
|
||||
status: null
|
||||
},
|
||||
_sum: {
|
||||
msatsPaying: true,
|
||||
msatsFeePaying: true
|
||||
}
|
||||
})
|
||||
|
||||
// Calculate total pending amount
|
||||
const pendingMsats = (pendingInvoices._sum.msatsRequested ?? 0n) +
|
||||
((pendingWithdrawals._sum.msatsPaying ?? 0n) + (pendingWithdrawals._sum.msatsFeePaying ?? 0n))
|
||||
|
||||
// Check balance limit
|
||||
if (pendingMsats + me.msats > BALANCE_LIMIT_MSATS) {
|
||||
throw new Error(
|
||||
`pending invoices and withdrawals must not cause balance to exceed ${
|
||||
numWithUnits(msatsToSats(BALANCE_LIMIT_MSATS))
|
||||
}`
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
import { PAID_ACTION_PAYMENT_METHODS } from '@/lib/constants'
|
||||
import { toPositiveBigInt } from '@/lib/validate'
|
||||
import { notifyDeposit } from '@/lib/webPush'
|
||||
import { numWithUnits, msatsToSats, satsToMsats } from '@/lib/format'
|
||||
import { getInvoiceableWallets } from '@/wallets/server'
|
||||
import { assertBelowBalanceLimit } from './lib/assert'
|
||||
|
||||
export const anonable = false
|
||||
|
||||
export const paymentMethods = [
|
||||
PAID_ACTION_PAYMENT_METHODS.P2P,
|
||||
PAID_ACTION_PAYMENT_METHODS.OPTIMISTIC
|
||||
]
|
||||
|
||||
export async function getCost ({ msats }) {
|
||||
return toPositiveBigInt(msats)
|
||||
}
|
||||
|
||||
export async function getInvoiceablePeer (_, { me, models, cost }) {
|
||||
if (!me?.proxyReceive) return null
|
||||
const wallets = await getInvoiceableWallets(me.id, { models })
|
||||
|
||||
// if the user has any invoiceable wallets and this action will result in their balance
|
||||
// being greater than their desired threshold
|
||||
if (wallets.length > 0 && (cost + me.msats) > satsToMsats(me.autoWithdrawThreshold)) {
|
||||
return me.id
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export async function getSybilFeePercent () {
|
||||
return 10n
|
||||
}
|
||||
|
||||
export async function perform ({
|
||||
invoiceId,
|
||||
comment,
|
||||
lud18Data
|
||||
}, { me, tx }) {
|
||||
const invoice = await tx.invoice.update({
|
||||
where: { id: invoiceId },
|
||||
data: {
|
||||
comment,
|
||||
lud18Data
|
||||
},
|
||||
include: { invoiceForward: true }
|
||||
})
|
||||
|
||||
if (!invoice.invoiceForward) {
|
||||
// if the invoice is not p2p, assert that the user's balance limit is not exceeded
|
||||
await assertBelowBalanceLimit({ me, tx })
|
||||
}
|
||||
}
|
||||
|
||||
export async function describe ({ description }, { me, cost, sybilFeePercent }) {
|
||||
const fee = sybilFeePercent ? cost * BigInt(sybilFeePercent) / 100n : 0n
|
||||
return description ?? `SN: ${me?.name ?? ''} receives ${numWithUnits(msatsToSats(cost - fee))}`
|
||||
}
|
||||
|
||||
export async function onPaid ({ invoice }, { tx }) {
|
||||
if (!invoice) {
|
||||
throw new Error('invoice is required')
|
||||
}
|
||||
|
||||
// P2P lnurlp does not need to update the user's balance
|
||||
if (invoice?.invoiceForward) return
|
||||
|
||||
await tx.user.update({
|
||||
where: { id: invoice.userId },
|
||||
data: {
|
||||
msats: {
|
||||
increment: invoice.msatsReceived
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function nonCriticalSideEffects ({ invoice }, { models }) {
|
||||
await notifyDeposit(invoice.userId, invoice)
|
||||
await models.$executeRaw`
|
||||
INSERT INTO pgboss.job (name, data)
|
||||
VALUES ('nip57', jsonb_build_object('hash', ${invoice.hash}))`
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import { PAID_ACTION_PAYMENT_METHODS, USER_ID } from '@/lib/constants'
|
||||
import { msatsToSats, satsToMsats } from '@/lib/format'
|
||||
import { notifyZapped } from '@/lib/webPush'
|
||||
import { getInvoiceableWallets } from '@/wallets/server'
|
||||
|
||||
export const anonable = true
|
||||
|
||||
|
@ -18,18 +19,13 @@ export async function getCost ({ sats }) {
|
|||
export async function getInvoiceablePeer ({ id }, { models }) {
|
||||
const item = await models.item.findUnique({
|
||||
where: { id: parseInt(id) },
|
||||
include: {
|
||||
itemForwards: true,
|
||||
user: {
|
||||
include: {
|
||||
wallets: true
|
||||
}
|
||||
}
|
||||
}
|
||||
include: { itemForwards: true }
|
||||
})
|
||||
|
||||
const wallets = await getInvoiceableWallets(item.userId, { models })
|
||||
|
||||
// request peer invoice if they have an attached wallet and have not forwarded the item
|
||||
return item.user.wallets.length > 0 && item.itemForwards.length === 0 ? item.userId : null
|
||||
return wallets.length > 0 && item.itemForwards.length === 0 ? item.userId : null
|
||||
}
|
||||
|
||||
export async function getSybilFeePercent () {
|
||||
|
|
|
@ -1033,6 +1033,7 @@ export default {
|
|||
},
|
||||
ItemAct: {
|
||||
invoice: async (itemAct, args, { models }) => {
|
||||
// we never want to fetch the sensitive data full monty in nested resolvers
|
||||
if (itemAct.invoiceId) {
|
||||
return {
|
||||
id: itemAct.invoiceId,
|
||||
|
@ -1282,6 +1283,7 @@ export default {
|
|||
return root
|
||||
},
|
||||
invoice: async (item, args, { models }) => {
|
||||
// we never want to fetch the sensitive data full monty in nested resolvers
|
||||
if (item.invoiceId) {
|
||||
return {
|
||||
id: item.invoiceId,
|
||||
|
|
|
@ -217,14 +217,20 @@ export default {
|
|||
|
||||
if (meFull.noteDeposits) {
|
||||
queries.push(
|
||||
`(SELECT "Invoice".id::text, "Invoice"."confirmedAt" AS "sortTime", FLOOR("msatsReceived" / 1000) as "earnedSats",
|
||||
`(SELECT "Invoice".id::text, "Invoice"."confirmedAt" AS "sortTime",
|
||||
FLOOR("Invoice"."msatsReceived" / 1000) as "earnedSats",
|
||||
'InvoicePaid' AS type
|
||||
FROM "Invoice"
|
||||
WHERE "Invoice"."userId" = $1
|
||||
AND "confirmedAt" IS NOT NULL
|
||||
AND "isHeld" IS NULL
|
||||
AND "actionState" IS NULL
|
||||
AND created_at < $2
|
||||
AND "Invoice"."confirmedAt" IS NOT NULL
|
||||
AND "Invoice"."created_at" < $2
|
||||
AND (
|
||||
("Invoice"."isHeld" IS NULL AND "Invoice"."actionType" IS NULL)
|
||||
OR (
|
||||
"Invoice"."actionType" = 'RECEIVE'
|
||||
AND "Invoice"."actionState" = 'PAID'
|
||||
)
|
||||
)
|
||||
ORDER BY "sortTime" DESC
|
||||
LIMIT ${LIMIT})`
|
||||
)
|
||||
|
@ -232,12 +238,17 @@ export default {
|
|||
|
||||
if (meFull.noteWithdrawals) {
|
||||
queries.push(
|
||||
`(SELECT "Withdrawl".id::text, "Withdrawl".created_at AS "sortTime", FLOOR("msatsPaid" / 1000) as "earnedSats",
|
||||
`(SELECT "Withdrawl".id::text, MAX(COALESCE("Invoice"."confirmedAt", "Withdrawl".created_at)) AS "sortTime",
|
||||
FLOOR(MAX("Withdrawl"."msatsPaid" / 1000)) as "earnedSats",
|
||||
'WithdrawlPaid' AS type
|
||||
FROM "Withdrawl"
|
||||
LEFT JOIN "InvoiceForward" ON "InvoiceForward"."withdrawlId" = "Withdrawl".id
|
||||
LEFT JOIN "Invoice" ON "InvoiceForward"."invoiceId" = "Invoice".id
|
||||
WHERE "Withdrawl"."userId" = $1
|
||||
AND status = 'CONFIRMED'
|
||||
AND created_at < $2
|
||||
AND "Withdrawl".status = 'CONFIRMED'
|
||||
AND "Withdrawl".created_at < $2
|
||||
AND ("InvoiceForward"."id" IS NULL OR "Invoice"."actionType" = 'ZAP')
|
||||
GROUP BY "Withdrawl".id
|
||||
ORDER BY "sortTime" DESC
|
||||
LIMIT ${LIMIT})`
|
||||
)
|
||||
|
|
|
@ -19,6 +19,8 @@ function paidActionType (actionType) {
|
|||
return 'DonatePaidAction'
|
||||
case 'POLL_VOTE':
|
||||
return 'PollVotePaidAction'
|
||||
case 'RECEIVE':
|
||||
return 'ReceivePaidAction'
|
||||
default:
|
||||
throw new Error('Unknown action type')
|
||||
}
|
||||
|
|
|
@ -421,8 +421,16 @@ export default {
|
|||
confirmedAt: {
|
||||
gt: lastChecked
|
||||
},
|
||||
isHeld: null,
|
||||
actionType: null
|
||||
OR: [
|
||||
{
|
||||
isHeld: null,
|
||||
actionType: null
|
||||
},
|
||||
{
|
||||
actionType: 'RECEIVE',
|
||||
actionState: 'PAID'
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
if (invoice) {
|
||||
|
@ -438,7 +446,23 @@ export default {
|
|||
status: 'CONFIRMED',
|
||||
updatedAt: {
|
||||
gt: lastChecked
|
||||
}
|
||||
},
|
||||
OR: [
|
||||
{
|
||||
invoiceForward: {
|
||||
none: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
invoiceForward: {
|
||||
some: {
|
||||
invoice: {
|
||||
actionType: 'ZAP'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
if (wdrwl) {
|
||||
|
@ -922,7 +946,8 @@ export default {
|
|||
createdAt: {
|
||||
gte,
|
||||
lte
|
||||
}
|
||||
},
|
||||
OR: [{ invoiceActionState: 'PAID' }, { invoiceActionState: null }]
|
||||
}
|
||||
})
|
||||
},
|
||||
|
@ -939,7 +964,8 @@ export default {
|
|||
createdAt: {
|
||||
gte,
|
||||
lte
|
||||
}
|
||||
},
|
||||
OR: [{ invoiceActionState: 'PAID' }, { invoiceActionState: null }]
|
||||
}
|
||||
})
|
||||
},
|
||||
|
@ -956,7 +982,8 @@ export default {
|
|||
createdAt: {
|
||||
gte,
|
||||
lte
|
||||
}
|
||||
},
|
||||
OR: [{ invoiceActionState: 'PAID' }, { invoiceActionState: null }]
|
||||
}
|
||||
})
|
||||
},
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {
|
||||
createHodlInvoice, createInvoice, payViaPaymentRequest,
|
||||
payViaPaymentRequest,
|
||||
getInvoice as getInvoiceFromLnd, deletePayment, getPayment,
|
||||
parsePaymentRequest
|
||||
} from 'ln-service'
|
||||
|
@ -7,24 +7,23 @@ import crypto, { timingSafeEqual } from 'crypto'
|
|||
import serialize from './serial'
|
||||
import { decodeCursor, LIMIT, nextCursorEncoded } from '@/lib/cursor'
|
||||
import { SELECT, itemQueryWithMeta } from './item'
|
||||
import { formatMsats, formatSats, msatsToSats, msatsToSatsDecimal } from '@/lib/format'
|
||||
import { formatMsats, formatSats, msatsToSats, msatsToSatsDecimal, satsToMsats } from '@/lib/format'
|
||||
import {
|
||||
ANON_BALANCE_LIMIT_MSATS, ANON_INV_PENDING_LIMIT, USER_ID, BALANCE_LIMIT_MSATS,
|
||||
INVOICE_RETENTION_DAYS, INV_PENDING_LIMIT, USER_IDS_BALANCE_NO_LIMIT, LND_PATHFINDING_TIMEOUT_MS
|
||||
USER_ID, INVOICE_RETENTION_DAYS, LND_PATHFINDING_TIMEOUT_MS
|
||||
} from '@/lib/constants'
|
||||
import { amountSchema, validateSchema, withdrawlSchema, lnAddrSchema } from '@/lib/validate'
|
||||
import { datePivot } from '@/lib/time'
|
||||
import assertGofacYourself from './ofac'
|
||||
import assertApiKeyNotPermitted from './apiKey'
|
||||
import { bolt11Tags } from '@/lib/bolt11'
|
||||
import { finalizeHodlInvoice } from 'worker/wallet'
|
||||
import walletDefs from 'wallets/server'
|
||||
import { finalizeHodlInvoice } from '@/worker/wallet'
|
||||
import walletDefs from '@/wallets/server'
|
||||
import { generateResolverName, generateTypeDefName } from '@/wallets/graphql'
|
||||
import { lnAddrOptions } from '@/lib/lnurl'
|
||||
import { GqlAuthenticationError, GqlAuthorizationError, GqlInputError } from '@/lib/error'
|
||||
import { getNodeSockets, getOurPubkey } from '../lnd'
|
||||
import validateWallet from '@/wallets/validate'
|
||||
import { canReceive } from '@/wallets/common'
|
||||
import performPaidAction from '../paidAction'
|
||||
|
||||
function injectResolvers (resolvers) {
|
||||
console.group('injected GraphQL resolvers:')
|
||||
|
@ -84,9 +83,6 @@ export async function getInvoice (parent, { id }, { me, models, lnd }) {
|
|||
const inv = await models.invoice.findUnique({
|
||||
where: {
|
||||
id: Number(id)
|
||||
},
|
||||
include: {
|
||||
user: true
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -94,29 +90,16 @@ export async function getInvoice (parent, { id }, { me, models, lnd }) {
|
|||
throw new GqlInputError('invoice not found')
|
||||
}
|
||||
|
||||
if (inv.user.id === USER_ID.anon) {
|
||||
if (inv.userId === USER_ID.anon) {
|
||||
return inv
|
||||
}
|
||||
if (!me) {
|
||||
throw new GqlAuthenticationError()
|
||||
}
|
||||
if (inv.user.id !== me.id) {
|
||||
if (inv.userId !== me.id) {
|
||||
throw new GqlInputError('not ur invoice')
|
||||
}
|
||||
|
||||
try {
|
||||
inv.nostr = JSON.parse(inv.desc)
|
||||
} catch (err) {
|
||||
}
|
||||
|
||||
try {
|
||||
if (inv.confirmedAt) {
|
||||
inv.confirmedPreimage = inv.preimage ?? (await getInvoiceFromLnd({ id: inv.hash, lnd })).secret
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('error fetching invoice from LND', err)
|
||||
}
|
||||
|
||||
return inv
|
||||
}
|
||||
|
||||
|
@ -128,10 +111,6 @@ export async function getWithdrawl (parent, { id }, { me, models, lnd }) {
|
|||
const wdrwl = await models.withdrawl.findUnique({
|
||||
where: {
|
||||
id: Number(id)
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
invoiceForward: true
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -139,7 +118,7 @@ export async function getWithdrawl (parent, { id }, { me, models, lnd }) {
|
|||
throw new GqlInputError('withdrawal not found')
|
||||
}
|
||||
|
||||
if (wdrwl.user.id !== me.id) {
|
||||
if (wdrwl.userId !== me.id) {
|
||||
throw new GqlInputError('not ur withdrawal')
|
||||
}
|
||||
|
||||
|
@ -458,50 +437,15 @@ const resolvers = {
|
|||
__resolveType: wallet => wallet.__resolveType
|
||||
},
|
||||
Mutation: {
|
||||
createInvoice: async (parent, { amount, hodlInvoice = false, expireSecs = 3600 }, { me, models, lnd, headers }) => {
|
||||
createInvoice: async (parent, { amount }, { me, models, lnd, headers }) => {
|
||||
await validateSchema(amountSchema, { amount })
|
||||
await assertGofacYourself({ models, headers })
|
||||
|
||||
let expirePivot = { seconds: expireSecs }
|
||||
let invLimit = INV_PENDING_LIMIT
|
||||
let balanceLimit = (hodlInvoice || USER_IDS_BALANCE_NO_LIMIT.includes(Number(me?.id))) ? 0 : BALANCE_LIMIT_MSATS
|
||||
let id = me?.id
|
||||
if (!me) {
|
||||
expirePivot = { seconds: Math.min(expireSecs, 180) }
|
||||
invLimit = ANON_INV_PENDING_LIMIT
|
||||
balanceLimit = ANON_BALANCE_LIMIT_MSATS
|
||||
id = USER_ID.anon
|
||||
}
|
||||
const { invoice } = await performPaidAction('RECEIVE', {
|
||||
msats: satsToMsats(amount)
|
||||
}, { models, lnd, me })
|
||||
|
||||
const user = await models.user.findUnique({ where: { id } })
|
||||
|
||||
const expiresAt = datePivot(new Date(), expirePivot)
|
||||
const description = `Funding @${user.name} on stacker.news`
|
||||
try {
|
||||
const invoice = await (hodlInvoice ? createHodlInvoice : createInvoice)({
|
||||
description: user.hideInvoiceDesc ? undefined : description,
|
||||
lnd,
|
||||
tokens: amount,
|
||||
expires_at: expiresAt
|
||||
})
|
||||
|
||||
const [inv] = await serialize(
|
||||
models.$queryRaw`SELECT * FROM create_invoice(${invoice.id}, ${invoice.secret}::TEXT, ${invoice.request},
|
||||
${expiresAt}::timestamp, ${amount * 1000}, ${user.id}::INTEGER, ${description}, NULL, NULL,
|
||||
${invLimit}::INTEGER, ${balanceLimit})`,
|
||||
{ models }
|
||||
)
|
||||
|
||||
// the HMAC is only returned during invoice creation
|
||||
// this makes sure that only the person who created this invoice
|
||||
// has access to the HMAC
|
||||
const hmac = createHmac(inv.hash)
|
||||
|
||||
return { ...inv, hmac }
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
throw error
|
||||
}
|
||||
return invoice
|
||||
},
|
||||
createWithdrawl: createWithdrawal,
|
||||
sendToLnAddr,
|
||||
|
@ -596,7 +540,15 @@ const resolvers = {
|
|||
satsPaid: w => msatsToSats(w.msatsPaid),
|
||||
satsFeePaying: w => w.invoiceForward?.length > 0 ? 0 : msatsToSats(w.msatsFeePaying),
|
||||
satsFeePaid: w => w.invoiceForward?.length > 0 ? 0 : msatsToSats(w.msatsFeePaid),
|
||||
p2p: w => !!w.invoiceForward?.length,
|
||||
// we never want to fetch the sensitive data full monty in nested resolvers
|
||||
forwardedActionType: async (withdrawl, args, { models }) => {
|
||||
return (await models.invoiceForward.findFirst({
|
||||
where: { withdrawlId: Number(withdrawl.id) },
|
||||
include: {
|
||||
invoice: true
|
||||
}
|
||||
}))?.invoice?.actionType
|
||||
},
|
||||
preimage: async (withdrawl, args, { lnd }) => {
|
||||
try {
|
||||
if (withdrawl.status === 'CONFIRMED') {
|
||||
|
@ -611,6 +563,35 @@ const resolvers = {
|
|||
Invoice: {
|
||||
satsReceived: i => msatsToSats(i.msatsReceived),
|
||||
satsRequested: i => msatsToSats(i.msatsRequested),
|
||||
// we never want to fetch the sensitive data full monty in nested resolvers
|
||||
forwardedSats: async (invoice, args, { models }) => {
|
||||
const msats = (await models.invoiceForward.findUnique({
|
||||
where: { invoiceId: Number(invoice.id) },
|
||||
include: {
|
||||
withdrawl: true
|
||||
}
|
||||
}))?.withdrawl?.msatsPaid
|
||||
return msats ? msatsToSats(msats) : null
|
||||
},
|
||||
nostr: async (invoice, args, { models }) => {
|
||||
try {
|
||||
return JSON.parse(invoice.desc)
|
||||
} catch (err) {
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
confirmedPreimage: async (invoice, args, { lnd }) => {
|
||||
try {
|
||||
if (invoice.confirmedAt) {
|
||||
return invoice.preimage ?? (await getInvoiceFromLnd({ id: invoice.hash, lnd })).secret
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('error fetching invoice from LND', err)
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
item: async (invoice, args, { models, me }) => {
|
||||
if (!invoice.actionId) return null
|
||||
switch (invoice.actionType) {
|
||||
|
@ -896,6 +877,17 @@ export async function createWithdrawal (parent, { invoice, maxFee }, { me, model
|
|||
|
||||
const user = await models.user.findUnique({ where: { id: me.id } })
|
||||
|
||||
// check if there's an invoice with same hash that has an invoiceForward
|
||||
// we can't allow this because it creates two outgoing payments from our node
|
||||
// with the same hash
|
||||
const selfPayment = await models.invoice.findUnique({
|
||||
where: { hash: decoded.id },
|
||||
include: { invoiceForward: true }
|
||||
})
|
||||
if (selfPayment?.invoiceForward) {
|
||||
throw new GqlInputError('SN cannot pay an invoice that SN is proxying')
|
||||
}
|
||||
|
||||
const autoWithdraw = !!wallet?.id
|
||||
// create withdrawl transactionally (id, bolt11, amount, fee)
|
||||
const [withdrawl] = await serialize(
|
||||
|
|
|
@ -107,6 +107,7 @@ export default gql`
|
|||
zapUndos: Int
|
||||
wildWestMode: Boolean!
|
||||
withdrawMaxFeeDefault: Int!
|
||||
proxyReceive: Boolean
|
||||
}
|
||||
|
||||
type AuthMethods {
|
||||
|
@ -185,6 +186,7 @@ export default gql`
|
|||
autoWithdrawMaxFeeTotal: Int
|
||||
vaultKeyHash: String
|
||||
walletsUpdatedAt: Date
|
||||
proxyReceive: Boolean
|
||||
}
|
||||
|
||||
type UserOptional {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { gql } from 'graphql-tag'
|
||||
import { fieldToGqlArg, fieldToGqlArgOptional, generateResolverName, generateTypeDefName } from '@/wallets/graphql'
|
||||
import { isServerField } from '@/wallets/common'
|
||||
import walletDefs from 'wallets/server'
|
||||
import walletDefs from '@/wallets/server'
|
||||
|
||||
function injectTypeDefs (typeDefs) {
|
||||
const injected = [rawTypeDefs(), mutationTypeDefs()]
|
||||
|
@ -74,7 +74,7 @@ const typeDefs = `
|
|||
}
|
||||
|
||||
extend type Mutation {
|
||||
createInvoice(amount: Int!, expireSecs: Int, hodlInvoice: Boolean): Invoice!
|
||||
createInvoice(amount: Int!): Invoice!
|
||||
createWithdrawl(invoice: String!, maxFee: Int!): Withdrawl!
|
||||
sendToLnAddr(addr: String!, amount: Int!, maxFee: Int!, comment: String, identifier: Boolean, name: String, email: String): Withdrawl!
|
||||
cancelInvoice(hash: String!, hmac: String!): Invoice!
|
||||
|
@ -122,6 +122,7 @@ const typeDefs = `
|
|||
actionError: String
|
||||
item: Item
|
||||
itemAct: ItemAct
|
||||
forwardedSats: Int
|
||||
}
|
||||
|
||||
type Withdrawl {
|
||||
|
@ -135,8 +136,8 @@ const typeDefs = `
|
|||
satsFeePaid: Int
|
||||
status: String
|
||||
autoWithdraw: Boolean!
|
||||
p2p: Boolean!
|
||||
preimage: String
|
||||
forwardedActionType: String
|
||||
}
|
||||
|
||||
type Fact {
|
||||
|
|
|
@ -14,6 +14,8 @@ import Item from './item'
|
|||
import { CommentFlat } from './comment'
|
||||
import classNames from 'classnames'
|
||||
import Moon from '@/svgs/moon-fill.svg'
|
||||
import { Badge } from 'react-bootstrap'
|
||||
import styles from './invoice.module.css'
|
||||
|
||||
export default function Invoice ({
|
||||
id, query = INVOICE, modal, onPayment, onCanceled, info, successVerb = 'deposited',
|
||||
|
@ -54,10 +56,27 @@ export default function Invoice ({
|
|||
|
||||
let variant = 'default'
|
||||
let status = 'waiting for you'
|
||||
let sats = invoice.satsRequested
|
||||
if (invoice.forwardedSats) {
|
||||
if (invoice.actionType === 'RECEIVE') {
|
||||
successVerb = 'forwarded'
|
||||
sats = invoice.forwardedSats
|
||||
} else {
|
||||
successVerb = 'zapped'
|
||||
}
|
||||
}
|
||||
|
||||
if (invoice.confirmedAt) {
|
||||
variant = 'confirmed'
|
||||
status = `${numWithUnits(invoice.satsReceived, { abbreviate: false })} ${successVerb}`
|
||||
status = (
|
||||
<>
|
||||
{numWithUnits(sats, { abbreviate: false })}
|
||||
{' '}
|
||||
{successVerb}
|
||||
{' '}
|
||||
{invoice.forwardedSats && <Badge className={styles.badge} bg={null}>p2p</Badge>}
|
||||
</>
|
||||
)
|
||||
useWallet = false
|
||||
} else if (invoice.cancelled) {
|
||||
variant = 'failed'
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
.badge {
|
||||
color: var(--theme-grey) !important;
|
||||
background: var(--theme-clickToContextColor) !important;
|
||||
vertical-align: middle;
|
||||
margin-left: 0.5rem;
|
||||
}
|
|
@ -326,10 +326,10 @@ function NostrZap ({ n }) {
|
|||
)
|
||||
}
|
||||
|
||||
function InvoicePaid ({ n }) {
|
||||
function getPayerSig (lud18Data) {
|
||||
let payerSig
|
||||
if (n.invoice.lud18Data) {
|
||||
const { name, identifier, email, pubkey } = n.invoice.lud18Data
|
||||
if (lud18Data) {
|
||||
const { name, identifier, email, pubkey } = lud18Data
|
||||
const id = identifier || email || pubkey
|
||||
payerSig = '- '
|
||||
if (name) {
|
||||
|
@ -339,10 +339,23 @@ function InvoicePaid ({ n }) {
|
|||
|
||||
if (id) payerSig += id
|
||||
}
|
||||
return payerSig
|
||||
}
|
||||
|
||||
function InvoicePaid ({ n }) {
|
||||
const payerSig = getPayerSig(n.invoice.lud18Data)
|
||||
let actionString = 'desposited to your account'
|
||||
let sats = n.earnedSats
|
||||
if (n.invoice.forwardedSats) {
|
||||
actionString = 'sent directly to your attached wallet'
|
||||
sats = n.invoice.forwardedSats
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='fw-bold text-info'>
|
||||
<Check className='fill-info me-1' />{numWithUnits(n.earnedSats, { abbreviate: false, unitSingular: 'sat was', unitPlural: 'sats were' })} deposited in your account
|
||||
<Check className='fill-info me-1' />{numWithUnits(sats, { abbreviate: false, unitSingular: 'sat was', unitPlural: 'sats were' })} {actionString}
|
||||
<small className='text-muted ms-1 fw-normal' suppressHydrationWarning>{timeSince(new Date(n.sortTime))}</small>
|
||||
{n.invoice.forwardedSats && <Badge className={styles.badge} bg={null}>p2p</Badge>}
|
||||
{n.invoice.comment &&
|
||||
<small className='d-block ms-4 ps-1 mt-1 mb-1 text-muted fw-normal'>
|
||||
<Text>{n.invoice.comment}</Text>
|
||||
|
@ -484,13 +497,17 @@ function Invoicification ({ n: { invoice, sortTime } }) {
|
|||
}
|
||||
|
||||
function WithdrawlPaid ({ n }) {
|
||||
let actionString = n.withdrawl.autoWithdraw ? 'sent to your attached wallet' : 'withdrawn from your account'
|
||||
if (n.withdrawl.forwardedActionType === 'ZAP') {
|
||||
actionString = 'zapped directly to your attached wallet'
|
||||
}
|
||||
return (
|
||||
<div className='fw-bold text-info'>
|
||||
<Check className='fill-info me-1' />{numWithUnits(n.earnedSats + n.withdrawl.satsFeePaid, { abbreviate: false, unitSingular: 'sat was ', unitPlural: 'sats were ' })}
|
||||
{n.withdrawl.p2p || n.withdrawl.autoWithdraw ? 'sent to your attached wallet' : 'withdrawn from your account'}
|
||||
{actionString}
|
||||
<small className='text-muted ms-1 fw-normal' suppressHydrationWarning>{timeSince(new Date(n.sortTime))}</small>
|
||||
{(n.withdrawl.p2p && <Badge className={styles.badge} bg={null}>p2p</Badge>) ||
|
||||
(n.withdrawl.autoWithdraw && <Badge className={styles.badge} bg={null}>autowithdraw</Badge>)}
|
||||
{(n.withdrawl.forwardedActionType === 'ZAP' && <Badge className={styles.badge} bg={null}>p2p</Badge>) ||
|
||||
(n.withdrawl.autoWithdraw && <Badge className={styles.badge} bg={null}>autowithdraw</Badge>)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -174,6 +174,8 @@ export const NOTIFICATIONS = gql`
|
|||
nostr
|
||||
comment
|
||||
lud18Data
|
||||
actionType
|
||||
forwardedSats
|
||||
}
|
||||
}
|
||||
... on Invoicification {
|
||||
|
@ -185,8 +187,8 @@ export const NOTIFICATIONS = gql`
|
|||
earnedSats
|
||||
withdrawl {
|
||||
autoWithdraw
|
||||
p2p
|
||||
satsFeePaid
|
||||
forwardedActionType
|
||||
}
|
||||
}
|
||||
... on Reminder {
|
||||
|
|
|
@ -50,6 +50,7 @@ ${STREAK_FIELDS}
|
|||
disableFreebies
|
||||
vaultKeyHash
|
||||
walletsUpdatedAt
|
||||
proxyReceive
|
||||
}
|
||||
optional {
|
||||
isContributor
|
||||
|
@ -111,6 +112,7 @@ export const SETTINGS_FIELDS = gql`
|
|||
apiKey
|
||||
}
|
||||
apiKeyEnabled
|
||||
proxyReceive
|
||||
}
|
||||
}`
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ export const INVOICE_FIELDS = gql`
|
|||
actionType
|
||||
actionError
|
||||
confirmedPreimage
|
||||
forwardedSats
|
||||
}`
|
||||
|
||||
export const INVOICE_FULL = gql`
|
||||
|
@ -57,6 +58,7 @@ export const WITHDRAWL = gql`
|
|||
status
|
||||
autoWithdraw
|
||||
preimage
|
||||
forwardedActionType
|
||||
}
|
||||
}`
|
||||
|
||||
|
|
|
@ -2,30 +2,9 @@
|
|||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/api/*": [
|
||||
"api/*"
|
||||
],
|
||||
"@/lib/*": [
|
||||
"lib/*"
|
||||
],
|
||||
"@/fragments/*": [
|
||||
"fragments/*"
|
||||
],
|
||||
"@/pages/*": [
|
||||
"pages/*"
|
||||
],
|
||||
"@/components/*": [
|
||||
"components/*"
|
||||
],
|
||||
"@/wallets/*": [
|
||||
"wallets/*"
|
||||
],
|
||||
"@/styles/*": [
|
||||
"styles/*"
|
||||
],
|
||||
"@/svgs/*": [
|
||||
"svgs/*"
|
||||
],
|
||||
"@/*": [
|
||||
"./*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -49,7 +49,8 @@ function getClient (uri) {
|
|||
'ItemActPaidAction',
|
||||
'PollVotePaidAction',
|
||||
'SubPaidAction',
|
||||
'DonatePaidAction'
|
||||
'DonatePaidAction',
|
||||
'ReceivePaidAction'
|
||||
],
|
||||
Notification: [
|
||||
'Reply',
|
||||
|
|
|
@ -59,8 +59,6 @@ export const USER_ID = {
|
|||
}
|
||||
export const SN_ADMIN_IDS = [USER_ID.k00b, USER_ID.ek, USER_ID.sn]
|
||||
export const SN_NO_REWARDS_IDS = [USER_ID.anon, USER_ID.sn, USER_ID.saloon]
|
||||
export const ANON_INV_PENDING_LIMIT = 1000
|
||||
export const ANON_BALANCE_LIMIT_MSATS = 0 // disable
|
||||
export const MAX_POLL_NUM_CHOICES = 10
|
||||
export const MIN_POLL_NUM_CHOICES = 2
|
||||
export const POLL_COST = 1
|
||||
|
@ -80,10 +78,11 @@ export const SSR = typeof window === 'undefined'
|
|||
export const MAX_FORWARDS = 5
|
||||
export const LND_PATHFINDING_TIMEOUT_MS = 30000
|
||||
export const LNURLP_COMMENT_MAX_LENGTH = 1000
|
||||
// https://github.com/lightning/bolts/issues/236
|
||||
export const MAX_INVOICE_DESCRIPTION_LENGTH = 640
|
||||
export const RESERVED_MAX_USER_ID = 615
|
||||
export const GLOBAL_SEED = USER_ID.k00b
|
||||
export const FREEBIE_BASE_COST_THRESHOLD = 10
|
||||
export const USER_IDS_BALANCE_NO_LIMIT = [...SN_ADMIN_IDS, USER_ID.anon, USER_ID.ad]
|
||||
|
||||
// WIP ultimately subject to this list: https://ofac.treasury.gov/sanctions-programs-and-country-information
|
||||
// From lawyers: north korea, cuba, iran, ukraine, syria
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
import models from '@/api/models'
|
||||
import lnd from '@/api/lnd'
|
||||
import { createInvoice } from 'ln-service'
|
||||
import { lnurlPayDescriptionHashForUser, lnurlPayMetadataString, lnurlPayDescriptionHash } from '@/lib/lnurl'
|
||||
import serialize from '@/api/resolvers/serial'
|
||||
import { schnorr } from '@noble/curves/secp256k1'
|
||||
import { createHash } from 'crypto'
|
||||
import { datePivot } from '@/lib/time'
|
||||
import { BALANCE_LIMIT_MSATS, INV_PENDING_LIMIT, LNURLP_COMMENT_MAX_LENGTH, USER_IDS_BALANCE_NO_LIMIT } from '@/lib/constants'
|
||||
import { validateSchema, lud18PayerDataSchema } from '@/lib/validate'
|
||||
import { LNURLP_COMMENT_MAX_LENGTH, MAX_INVOICE_DESCRIPTION_LENGTH } from '@/lib/constants'
|
||||
import { validateSchema, lud18PayerDataSchema, toPositiveBigInt } from '@/lib/validate'
|
||||
import assertGofacYourself from '@/api/resolvers/ofac'
|
||||
import performPaidAction from '@/api/paidAction'
|
||||
|
||||
export default async ({ query: { username, amount, nostr, comment, payerdata: payerData }, headers }, res) => {
|
||||
const user = await models.user.findUnique({ where: { name: username } })
|
||||
|
@ -30,14 +28,16 @@ export default async ({ query: { username, amount, nostr, comment, payerdata: pa
|
|||
// If there is an amount tag, it MUST be equal to the amount query parameter
|
||||
const eventAmount = note.tags?.find(t => t[0] === 'amount')?.[1]
|
||||
if (schnorr.verify(note.sig, note.id, note.pubkey) && hasPTag && hasETag && (!eventAmount || Number(eventAmount) === Number(amount))) {
|
||||
description = user.hideInvoiceDesc ? undefined : 'zap'
|
||||
description = 'zap'
|
||||
descriptionHash = createHash('sha256').update(noteStr).digest('hex')
|
||||
} else {
|
||||
res.status(400).json({ status: 'ERROR', reason: 'invalid NIP-57 note' })
|
||||
return
|
||||
}
|
||||
} else {
|
||||
description = user.hideInvoiceDesc ? undefined : `Funding @${username} on stacker.news`
|
||||
description = `Paying @${username} on stacker.news`
|
||||
description += comment ? `: ${comment}` : '.'
|
||||
description = description.slice(0, MAX_INVOICE_DESCRIPTION_LENGTH)
|
||||
descriptionHash = lnurlPayDescriptionHashForUser(username)
|
||||
}
|
||||
|
||||
|
@ -45,8 +45,11 @@ export default async ({ query: { username, amount, nostr, comment, payerdata: pa
|
|||
return res.status(400).json({ status: 'ERROR', reason: 'amount must be >=1000 msats' })
|
||||
}
|
||||
|
||||
if (comment && comment.length > LNURLP_COMMENT_MAX_LENGTH) {
|
||||
return res.status(400).json({ status: 'ERROR', reason: `comment cannot exceed ${LNURLP_COMMENT_MAX_LENGTH} characters in length` })
|
||||
if (comment?.length > LNURLP_COMMENT_MAX_LENGTH) {
|
||||
return res.status(400).json({
|
||||
status: 'ERROR',
|
||||
reason: `comment cannot exceed ${LNURLP_COMMENT_MAX_LENGTH} characters in length`
|
||||
})
|
||||
}
|
||||
|
||||
let parsedPayerData
|
||||
|
@ -55,7 +58,10 @@ export default async ({ query: { username, amount, nostr, comment, payerdata: pa
|
|||
parsedPayerData = JSON.parse(payerData)
|
||||
} catch (err) {
|
||||
console.error('failed to parse payerdata', err)
|
||||
return res.status(400).json({ status: 'ERROR', reason: 'Invalid JSON supplied for payerdata parameter' })
|
||||
return res.status(400).json({
|
||||
status: 'ERROR',
|
||||
reason: 'Invalid JSON supplied for payerdata parameter'
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
|
@ -71,27 +77,20 @@ export default async ({ query: { username, amount, nostr, comment, payerdata: pa
|
|||
}
|
||||
|
||||
// generate invoice
|
||||
const expiresAt = datePivot(new Date(), { minutes: 5 })
|
||||
const invoice = await createInvoice({
|
||||
const { invoice } = await performPaidAction('RECEIVE', {
|
||||
msats: toPositiveBigInt(amount),
|
||||
description,
|
||||
description_hash: descriptionHash,
|
||||
lnd,
|
||||
mtokens: amount,
|
||||
expires_at: expiresAt
|
||||
})
|
||||
descriptionHash,
|
||||
comment: comment || '',
|
||||
lud18Data: parsedPayerData
|
||||
}, { models, lnd, me: user })
|
||||
|
||||
await serialize(
|
||||
models.$queryRaw`SELECT * FROM create_invoice(${invoice.id}, ${invoice.secret}::TEXT, ${invoice.request},
|
||||
${expiresAt}::timestamp, ${Number(amount)}, ${user.id}::INTEGER, ${noteStr || description},
|
||||
${comment || null}, ${parsedPayerData || null}::JSONB, ${INV_PENDING_LIMIT}::INTEGER,
|
||||
${USER_IDS_BALANCE_NO_LIMIT.includes(Number(user.id)) ? 0 : BALANCE_LIMIT_MSATS})`,
|
||||
{ models }
|
||||
)
|
||||
if (!invoice?.bolt11) throw new Error('could not generate invoice')
|
||||
|
||||
return res.status(200).json({
|
||||
pr: invoice.request,
|
||||
pr: invoice.bolt11,
|
||||
routes: [],
|
||||
verify: `${process.env.NEXT_PUBLIC_URL}/api/lnurlp/${username}/verify/${invoice.id}`
|
||||
verify: `${process.env.NEXT_PUBLIC_URL}/api/lnurlp/${username}/verify/${invoice.hash}`
|
||||
})
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
|
|
|
@ -6,8 +6,13 @@ export default async ({ query: { hash } }, res) => {
|
|||
if (!inv) {
|
||||
return res.status(404).json({ status: 'ERROR', reason: 'not found' })
|
||||
}
|
||||
const settled = inv.confirmedAt
|
||||
return res.status(200).json({ status: 'OK', settled: !!settled, preimage: settled ? inv.preimage : null, pr: inv.bolt11 })
|
||||
const settled = !!inv.confirmedAt
|
||||
return res.status(200).json({
|
||||
status: 'OK',
|
||||
settled,
|
||||
preimage: settled ? inv.preimage : null,
|
||||
pr: inv.bolt11
|
||||
})
|
||||
} catch (err) {
|
||||
console.log('error', err)
|
||||
return res.status(500).json({ status: 'ERROR', reason: 'internal server error' })
|
||||
|
|
|
@ -158,7 +158,8 @@ export default function Settings ({ ssrData }) {
|
|||
hideWalletBalance: settings?.hideWalletBalance,
|
||||
diagnostics: settings?.diagnostics,
|
||||
hideIsContributor: settings?.hideIsContributor,
|
||||
noReferralLinks: settings?.noReferralLinks
|
||||
noReferralLinks: settings?.noReferralLinks,
|
||||
proxyReceive: settings?.proxyReceive
|
||||
}}
|
||||
schema={settingsSchema}
|
||||
onSubmit={async ({
|
||||
|
@ -332,7 +333,22 @@ export default function Settings ({ ssrData }) {
|
|||
label='I find or lose cowboy essentials (e.g. cowboy hat)'
|
||||
name='noteCowboyHat'
|
||||
/>
|
||||
<div className='form-label'>privacy</div>
|
||||
<div className='form-label'>wallet</div>
|
||||
<Checkbox
|
||||
label={
|
||||
<div className='d-flex align-items-center'>proxy deposits to attached wallets
|
||||
<Info>
|
||||
<ul>
|
||||
<li>Forward deposits directly to your attached wallets if they will cause your balance to exceed your auto-withdraw threshold</li>
|
||||
<li>Payments will be wrapped by the SN node to preserve your wallet's privacy</li>
|
||||
<li>This will incur in a 10% fee</li>
|
||||
</ul>
|
||||
</Info>
|
||||
</div>
|
||||
}
|
||||
name='proxyReceive'
|
||||
groupClassName='mb-0'
|
||||
/>
|
||||
<Checkbox
|
||||
label={
|
||||
<div className='d-flex align-items-center'>hide invoice descriptions
|
||||
|
@ -353,6 +369,7 @@ export default function Settings ({ ssrData }) {
|
|||
groupClassName='mb-0'
|
||||
/>
|
||||
<DropBolt11sCheckbox
|
||||
groupClassName='mb-0'
|
||||
ssrData={ssrData}
|
||||
label={
|
||||
<div className='d-flex align-items-center'>autodelete withdrawal invoices
|
||||
|
@ -367,8 +384,12 @@ export default function Settings ({ ssrData }) {
|
|||
</div>
|
||||
}
|
||||
name='autoDropBolt11s'
|
||||
groupClassName='mb-0'
|
||||
/>
|
||||
<Checkbox
|
||||
label={<>hide my wallet balance</>}
|
||||
name='hideWalletBalance'
|
||||
/>
|
||||
<div className='form-label'>privacy</div>
|
||||
<Checkbox
|
||||
label={<>hide me from <Link href='/top/stackers/day'>top stackers</Link></>}
|
||||
name='hideFromTopUsers'
|
||||
|
@ -379,11 +400,6 @@ export default function Settings ({ ssrData }) {
|
|||
name='hideCowboyHat'
|
||||
groupClassName='mb-0'
|
||||
/>
|
||||
<Checkbox
|
||||
label={<>hide my wallet balance</>}
|
||||
name='hideWalletBalance'
|
||||
groupClassName='mb-0'
|
||||
/>
|
||||
<Checkbox
|
||||
label={<>hide my bookmarks from other stackers</>}
|
||||
name='hideBookmarks'
|
||||
|
|
|
@ -16,7 +16,8 @@ import { gql } from 'graphql-tag'
|
|||
import { useShowModal } from '@/components/modal'
|
||||
import { DeleteConfirm } from '@/components/delete'
|
||||
import { getGetServerSideProps } from '@/api/ssrApollo'
|
||||
|
||||
import { Badge } from 'react-bootstrap'
|
||||
import styles from '@/components/invoice.module.css'
|
||||
// force SSR to include CSP nonces
|
||||
export const getServerSideProps = getGetServerSideProps({ query: null })
|
||||
|
||||
|
@ -68,7 +69,11 @@ function LoadWithdrawl () {
|
|||
let variant = 'default'
|
||||
switch (data.withdrawl.status) {
|
||||
case 'CONFIRMED':
|
||||
status = `sent ${numWithUnits(data.withdrawl.satsPaid, { abbreviate: false })} with ${numWithUnits(data.withdrawl.satsFeePaid, { abbreviate: false })} in routing fees`
|
||||
if (data.withdrawl.forwardedActionType) {
|
||||
status = <>{`forwarded ${numWithUnits(data.withdrawl.satsPaid, { abbreviate: false })}`} <Badge className={styles.badge} bg={null}>p2p</Badge></>
|
||||
} else {
|
||||
status = `sent ${numWithUnits(data.withdrawl.satsPaid, { abbreviate: false })} with ${numWithUnits(data.withdrawl.satsFeePaid, { abbreviate: false })} in routing fees`
|
||||
}
|
||||
variant = 'confirmed'
|
||||
break
|
||||
case 'INSUFFICIENT_BALANCE':
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
-- AlterEnum
|
||||
ALTER TYPE "InvoiceActionType" ADD VALUE 'RECEIVE';
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "users" ADD COLUMN "proxyReceive" BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
DROP FUNCTION IF EXISTS create_invoice;
|
||||
|
||||
-- Add unique index for Withdrawl table
|
||||
-- to prevent multiple pending withdrawls with the same hash
|
||||
CREATE UNIQUE INDEX "Withdrawl_hash_key_null_status"
|
||||
ON "Withdrawl" (hash)
|
||||
WHERE status IS NULL OR status = 'CONFIRMED';
|
|
@ -140,6 +140,7 @@ model User {
|
|||
vaultKeyHash String @default("")
|
||||
walletsUpdatedAt DateTime?
|
||||
vaultEntries VaultEntry[] @relation("VaultEntries")
|
||||
proxyReceive Boolean @default(false)
|
||||
|
||||
@@index([photoId])
|
||||
@@index([createdAt], map: "users.created_at_index")
|
||||
|
@ -864,6 +865,7 @@ enum InvoiceActionType {
|
|||
TERRITORY_UPDATE
|
||||
TERRITORY_BILLING
|
||||
TERRITORY_UNARCHIVE
|
||||
RECEIVE
|
||||
}
|
||||
|
||||
enum InvoiceActionState {
|
||||
|
|
|
@ -73,7 +73,7 @@ EOF
|
|||
|
||||
# create client.js
|
||||
cat > wallets/$wallet/client.js <<EOF
|
||||
export * from 'wallets/$wallet'
|
||||
export * from '@/wallets/$wallet'
|
||||
|
||||
export async function testSendPayment (config, { logger }) {
|
||||
$(todo)
|
||||
|
@ -87,7 +87,7 @@ EOF
|
|||
|
||||
# create server.js
|
||||
cat > wallets/$wallet/server.js <<EOF
|
||||
export * from 'wallets/$wallet'
|
||||
export * from '@/wallets/$wallet'
|
||||
|
||||
export async function testCreateInvoice (config) {
|
||||
$(todo)
|
||||
|
|
|
@ -23,13 +23,13 @@ A _server.js_ file is only required for wallets that support receiving by exposi
|
|||
> Every wallet must have a client.js file (even if it does not support paying invoices) because every wallet is imported on the client. This is not the case on the server. On the client, wallets are imported via
|
||||
>
|
||||
> ```js
|
||||
> import wallet from 'wallets/<name>/client'
|
||||
> import wallet from '@/wallets/<name>/client'
|
||||
> ```
|
||||
>
|
||||
> vs
|
||||
>
|
||||
> ```js
|
||||
> import wallet from 'wallets/<name>/server'
|
||||
> import wallet from '@/wallets/<name>/server'
|
||||
> ```
|
||||
>
|
||||
> on the server.
|
||||
|
@ -37,7 +37,7 @@ A _server.js_ file is only required for wallets that support receiving by exposi
|
|||
> To have access to the properties that can be shared between client and server, server.js and client.js always reexport everything in index.js with a line like this:
|
||||
>
|
||||
> ```js
|
||||
> export * from 'wallets/<name>'
|
||||
> export * from '@/wallets/<name>'
|
||||
> ```
|
||||
>
|
||||
> If a wallet does not support paying invoices, this is all that client.js of this wallet does. The reason for this structure is to make sure the client does not import dependencies that can only be imported on the server and would thus break the build.
|
||||
|
@ -181,7 +181,7 @@ The first argument is the [BOLT11 payment request](https://github.com/lightning/
|
|||
>
|
||||
> ```js
|
||||
> // wallets/<wallet>/client.js
|
||||
> export * from 'wallets/<name>'
|
||||
> export * from '@/wallets/<name>'
|
||||
> ```
|
||||
>
|
||||
> where `<name>` is the wallet directory name.
|
||||
|
@ -191,13 +191,13 @@ The first argument is the [BOLT11 payment request](https://github.com/lightning/
|
|||
>
|
||||
> ```diff
|
||||
> // wallets/client.js
|
||||
> import * as nwc from 'wallets/nwc/client'
|
||||
> import * as lnbits from 'wallets/lnbits/client'
|
||||
> import * as lnc from 'wallets/lnc/client'
|
||||
> import * as lnAddr from 'wallets/lightning-address/client'
|
||||
> import * as cln from 'wallets/cln/client'
|
||||
> import * as lnd from 'wallets/lnd/client'
|
||||
> + import * as newWallet from 'wallets/<name>/client'
|
||||
> import * as nwc from '@/wallets/nwc/client'
|
||||
> import * as lnbits from '@/wallets/lnbits/client'
|
||||
> import * as lnc from '@/wallets/lnc/client'
|
||||
> import * as lnAddr from '@/wallets/lightning-address/client'
|
||||
> import * as cln from '@/wallets/cln/client'
|
||||
> import * as lnd from '@/wallets/lnd/client'
|
||||
> + import * as newWallet from '@/wallets/<name>/client'
|
||||
>
|
||||
> - export default [nwc, lnbits, lnc, lnAddr, cln, lnd]
|
||||
> + export default [nwc, lnbits, lnc, lnAddr, cln, lnd, newWallet]
|
||||
|
@ -225,7 +225,7 @@ Again, like `testSendPayment`, the first argument is the wallet configuration th
|
|||
>
|
||||
> ```js
|
||||
> // wallets/<wallet>/server.js
|
||||
> export * from 'wallets/<name>'
|
||||
> export * from '@/wallets/<name>'
|
||||
> ```
|
||||
>
|
||||
> where `<name>` is the wallet directory name.
|
||||
|
@ -235,10 +235,10 @@ Again, like `testSendPayment`, the first argument is the wallet configuration th
|
|||
>
|
||||
> ```diff
|
||||
> // wallets/server.js
|
||||
> import * as lnd from 'wallets/lnd/server'
|
||||
> import * as cln from 'wallets/cln/server'
|
||||
> import * as lnAddr from 'wallets/lightning-address/server'
|
||||
> + import * as newWallet from 'wallets/<name>/client'
|
||||
> import * as lnd from '@/wallets/lnd/server'
|
||||
> import * as cln from '@/wallets/cln/server'
|
||||
> import * as lnAddr from '@/wallets/lightning-address/server'
|
||||
> + import * as newWallet from '@/wallets/<name>/client'
|
||||
>
|
||||
> - export default [lnd, cln, lnAddr]
|
||||
> + export default [lnd, cln, lnAddr, newWallet]
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { getScopes, SCOPE_READ, SCOPE_WRITE, getWallet, request } from 'wallets/blink/common'
|
||||
export * from 'wallets/blink'
|
||||
import { getScopes, SCOPE_READ, SCOPE_WRITE, getWallet, request } from '@/wallets/blink/common'
|
||||
export * from '@/wallets/blink'
|
||||
|
||||
export async function testSendPayment ({ apiKey, currency }, { logger }) {
|
||||
logger.info('trying to fetch ' + currency + ' wallet')
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { string } from '@/lib/yup'
|
||||
import { galoyBlinkDashboardUrl } from 'wallets/blink/common'
|
||||
import { galoyBlinkDashboardUrl } from '@/wallets/blink/common'
|
||||
|
||||
export const name = 'blink'
|
||||
export const walletType = 'BLINK'
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { withTimeout } from '@/lib/time'
|
||||
import { getScopes, SCOPE_READ, SCOPE_RECEIVE, SCOPE_WRITE, getWallet, request } from 'wallets/blink/common'
|
||||
import { getScopes, SCOPE_READ, SCOPE_RECEIVE, SCOPE_WRITE, getWallet, request } from '@/wallets/blink/common'
|
||||
import { msatsToSats } from '@/lib/format'
|
||||
export * from 'wallets/blink'
|
||||
export * from '@/wallets/blink'
|
||||
|
||||
export async function testCreateInvoice ({ apiKeyRecv, currencyRecv }) {
|
||||
const scopes = await getScopes(apiKeyRecv)
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import * as nwc from 'wallets/nwc/client'
|
||||
import * as lnbits from 'wallets/lnbits/client'
|
||||
import * as lnc from 'wallets/lnc/client'
|
||||
import * as lnAddr from 'wallets/lightning-address/client'
|
||||
import * as cln from 'wallets/cln/client'
|
||||
import * as lnd from 'wallets/lnd/client'
|
||||
import * as webln from 'wallets/webln/client'
|
||||
import * as blink from 'wallets/blink/client'
|
||||
import * as phoenixd from 'wallets/phoenixd/client'
|
||||
import * as nwc from '@/wallets/nwc/client'
|
||||
import * as lnbits from '@/wallets/lnbits/client'
|
||||
import * as lnc from '@/wallets/lnc/client'
|
||||
import * as lnAddr from '@/wallets/lightning-address/client'
|
||||
import * as cln from '@/wallets/cln/client'
|
||||
import * as lnd from '@/wallets/lnd/client'
|
||||
import * as webln from '@/wallets/webln/client'
|
||||
import * as blink from '@/wallets/blink/client'
|
||||
import * as phoenixd from '@/wallets/phoenixd/client'
|
||||
|
||||
export default [nwc, lnbits, lnc, lnAddr, cln, lnd, webln, blink, phoenixd]
|
||||
|
|
|
@ -1 +1 @@
|
|||
export * from 'wallets/cln'
|
||||
export * from '@/wallets/cln'
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { createInvoice as clnCreateInvoice } from '@/lib/cln'
|
||||
|
||||
export * from 'wallets/cln'
|
||||
export * from '@/wallets/cln'
|
||||
|
||||
export const testCreateInvoice = async ({ socket, rune, cert }) => {
|
||||
return await createInvoice({ msats: 1000, expiry: 1, description: '' }, { socket, rune, cert })
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import walletDefs from 'wallets/client'
|
||||
import walletDefs from '@/wallets/client'
|
||||
|
||||
export const Status = {
|
||||
Enabled: 'Enabled',
|
||||
|
|
|
@ -7,7 +7,7 @@ import { getStorageKey, getWalletByType, Status, walletPrioritySort, canSend, is
|
|||
import useVault from '@/components/vault/use-vault'
|
||||
import { useWalletLogger } from '@/components/wallet-logger'
|
||||
import { decode as bolt11Decode } from 'bolt11'
|
||||
import walletDefs from 'wallets/client'
|
||||
import walletDefs from '@/wallets/client'
|
||||
import { generateMutation } from './graphql'
|
||||
import { formatSats } from '@/lib/format'
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
export * from 'wallets/lightning-address'
|
||||
export * from '@/wallets/lightning-address'
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { msatsSatsFloor } from '@/lib/format'
|
||||
import { lnAddrOptions } from '@/lib/lnurl'
|
||||
|
||||
export * from 'wallets/lightning-address'
|
||||
export * from '@/wallets/lightning-address'
|
||||
|
||||
export const testCreateInvoice = async ({ address }) => {
|
||||
return await createInvoice({ msats: 1000 }, { address })
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { assertContentTypeJson } from '@/lib/url'
|
||||
|
||||
export * from 'wallets/lnbits'
|
||||
export * from '@/wallets/lnbits'
|
||||
|
||||
export async function testSendPayment ({ url, adminKey, invoiceKey }, { logger }) {
|
||||
logger.info('trying to fetch wallet')
|
||||
|
|
|
@ -3,7 +3,7 @@ import { getAgent } from '@/lib/proxy'
|
|||
import { assertContentTypeJson } from '@/lib/url'
|
||||
import fetch from 'cross-fetch'
|
||||
|
||||
export * from 'wallets/lnbits'
|
||||
export * from '@/wallets/lnbits'
|
||||
|
||||
export async function testCreateInvoice ({ url, invoiceKey }) {
|
||||
return await createInvoice({ msats: 1000, expiry: 1 }, { url, invoiceKey })
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { InvoiceCanceledError, InvoiceExpiredError } from '@/wallets/errors'
|
||||
import { bolt11Tags } from '@/lib/bolt11'
|
||||
import { Mutex } from 'async-mutex'
|
||||
export * from 'wallets/lnc'
|
||||
export * from '@/wallets/lnc'
|
||||
|
||||
async function disconnect (lnc, logger) {
|
||||
if (lnc) {
|
||||
|
|
|
@ -1 +1 @@
|
|||
export * from 'wallets/lnd'
|
||||
export * from '@/wallets/lnd'
|
||||
|
|
|
@ -3,7 +3,7 @@ import { authenticatedLndGrpc } from '@/lib/lnd'
|
|||
import { createInvoice as lndCreateInvoice } from 'ln-service'
|
||||
import { TOR_REGEXP } from '@/lib/url'
|
||||
|
||||
export * from 'wallets/lnd'
|
||||
export * from '@/wallets/lnd'
|
||||
|
||||
export const testCreateInvoice = async ({ cert, macaroon, socket }) => {
|
||||
return await createInvoice({ msats: 1000, expiry: 1 }, { cert, macaroon, socket })
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { nwcCall, supportedMethods } from 'wallets/nwc'
|
||||
export * from 'wallets/nwc'
|
||||
import { nwcCall, supportedMethods } from '@/wallets/nwc'
|
||||
export * from '@/wallets/nwc'
|
||||
|
||||
export async function testSendPayment ({ nwcUrl }, { logger }) {
|
||||
const timeout = 15_000
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { withTimeout } from '@/lib/time'
|
||||
import { nwcCall, supportedMethods } from 'wallets/nwc'
|
||||
export * from 'wallets/nwc'
|
||||
import { nwcCall, supportedMethods } from '@/wallets/nwc'
|
||||
export * from '@/wallets/nwc'
|
||||
|
||||
export async function testCreateInvoice ({ nwcUrlRecv }, { logger }) {
|
||||
const timeout = 15_000
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export * from 'wallets/phoenixd'
|
||||
export * from '@/wallets/phoenixd'
|
||||
|
||||
export async function testSendPayment (config, { logger }) {
|
||||
// TODO:
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { msatsToSats } from '@/lib/format'
|
||||
|
||||
export * from 'wallets/phoenixd'
|
||||
export * from '@/wallets/phoenixd'
|
||||
|
||||
export async function testCreateInvoice ({ url, secondaryPassword }) {
|
||||
return await createInvoice(
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
// import server side wallets
|
||||
import * as lnd from 'wallets/lnd/server'
|
||||
import * as cln from 'wallets/cln/server'
|
||||
import * as lnAddr from 'wallets/lightning-address/server'
|
||||
import * as lnbits from 'wallets/lnbits/server'
|
||||
import * as nwc from 'wallets/nwc/server'
|
||||
import * as phoenixd from 'wallets/phoenixd/server'
|
||||
import * as blink from 'wallets/blink/server'
|
||||
import * as lnd from '@/wallets/lnd/server'
|
||||
import * as cln from '@/wallets/cln/server'
|
||||
import * as lnAddr from '@/wallets/lightning-address/server'
|
||||
import * as lnbits from '@/wallets/lnbits/server'
|
||||
import * as nwc from '@/wallets/nwc/server'
|
||||
import * as phoenixd from '@/wallets/phoenixd/server'
|
||||
import * as blink from '@/wallets/blink/server'
|
||||
|
||||
// we import only the metadata of client side wallets
|
||||
import * as lnc from 'wallets/lnc'
|
||||
import * as webln from 'wallets/webln'
|
||||
import * as lnc from '@/wallets/lnc'
|
||||
import * as webln from '@/wallets/webln'
|
||||
|
||||
import { walletLogger } from '@/api/resolvers/wallet'
|
||||
import walletDefs from 'wallets/server'
|
||||
import walletDefs from '@/wallets/server'
|
||||
import { parsePaymentRequest } from 'ln-service'
|
||||
import { toPositiveBigInt, toPositiveNumber } from '@/lib/validate'
|
||||
import { PAID_ACTION_TERMINAL_STATES } from '@/lib/constants'
|
||||
|
@ -27,28 +27,11 @@ const MAX_PENDING_INVOICES_PER_WALLET = 25
|
|||
|
||||
export async function createInvoice (userId, { msats, description, descriptionHash, expiry = 360 }, { models }) {
|
||||
// get the wallets in order of priority
|
||||
const wallets = await models.wallet.findMany({
|
||||
where: { userId, enabled: true },
|
||||
include: {
|
||||
user: true
|
||||
},
|
||||
orderBy: [
|
||||
{ priority: 'asc' },
|
||||
// use id as tie breaker (older wallet first)
|
||||
{ id: 'asc' }
|
||||
]
|
||||
})
|
||||
const wallets = await getInvoiceableWallets(userId, { models })
|
||||
|
||||
msats = toPositiveNumber(msats)
|
||||
|
||||
for (const wallet of wallets) {
|
||||
const w = walletDefs.find(w => w.walletType === wallet.type)
|
||||
|
||||
const config = wallet.wallet
|
||||
if (!canReceive({ def: w, config })) {
|
||||
continue
|
||||
}
|
||||
|
||||
for (const { def, wallet } of wallets) {
|
||||
const logger = walletLogger({ wallet, models })
|
||||
|
||||
try {
|
||||
|
@ -61,8 +44,8 @@ export async function createInvoice (userId, { msats, description, descriptionHa
|
|||
let invoice
|
||||
try {
|
||||
invoice = await walletCreateInvoice(
|
||||
{ wallet, def },
|
||||
{ msats, description, descriptionHash, expiry },
|
||||
{ ...w, userId, createInvoice: w.createInvoice },
|
||||
{ logger, models })
|
||||
} catch (err) {
|
||||
throw new Error('failed to create invoice: ' + err.message)
|
||||
|
@ -128,37 +111,33 @@ export async function createWrappedInvoice (userId,
|
|||
}
|
||||
}
|
||||
|
||||
async function walletCreateInvoice (
|
||||
{
|
||||
msats,
|
||||
description,
|
||||
descriptionHash,
|
||||
expiry = 360
|
||||
},
|
||||
{
|
||||
userId,
|
||||
walletType,
|
||||
walletField,
|
||||
createInvoice
|
||||
},
|
||||
{ logger, models }) {
|
||||
const wallet = await models.wallet.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
type: walletType
|
||||
},
|
||||
export async function getInvoiceableWallets (userId, { models }) {
|
||||
const wallets = await models.wallet.findMany({
|
||||
where: { userId, enabled: true },
|
||||
include: {
|
||||
[walletField]: true,
|
||||
user: true
|
||||
}
|
||||
},
|
||||
orderBy: [
|
||||
{ priority: 'asc' },
|
||||
// use id as tie breaker (older wallet first)
|
||||
{ id: 'asc' }
|
||||
]
|
||||
})
|
||||
|
||||
const config = wallet[walletField]
|
||||
const walletsWithDefs = wallets.map(wallet => {
|
||||
const w = walletDefs.find(w => w.walletType === wallet.type)
|
||||
return { wallet, def: w }
|
||||
})
|
||||
|
||||
if (!wallet || !config) {
|
||||
throw new Error('wallet not found')
|
||||
}
|
||||
return walletsWithDefs.filter(({ def, wallet }) => canReceive({ def, config: wallet.wallet }))
|
||||
}
|
||||
|
||||
async function walletCreateInvoice ({ wallet, def }, {
|
||||
msats,
|
||||
description,
|
||||
descriptionHash,
|
||||
expiry = 360
|
||||
}, { logger, models }) {
|
||||
// check for pending withdrawals
|
||||
const pendingWithdrawals = await models.withdrawl.count({
|
||||
where: {
|
||||
|
@ -185,14 +164,14 @@ async function walletCreateInvoice (
|
|||
}
|
||||
|
||||
return await withTimeout(
|
||||
createInvoice(
|
||||
def.createInvoice(
|
||||
{
|
||||
msats,
|
||||
description: wallet.user.hideInvoiceDesc ? undefined : description,
|
||||
descriptionHash,
|
||||
expiry
|
||||
},
|
||||
config,
|
||||
wallet.wallet,
|
||||
{ logger }
|
||||
), 10_000)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useEffect } from 'react'
|
||||
import { SSR } from '@/lib/constants'
|
||||
export * from 'wallets/webln'
|
||||
export * from '@/wallets/webln'
|
||||
|
||||
export const sendPayment = async (bolt11) => {
|
||||
if (typeof window.webln === 'undefined') {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { msatsSatsFloor, msatsToSats, satsToMsats } from '@/lib/format'
|
||||
import { createWithdrawal } from '@/api/resolvers/wallet'
|
||||
import { createInvoice } from 'wallets/server'
|
||||
import { createInvoice } from '@/wallets/server'
|
||||
|
||||
export async function autoWithdraw ({ data: { id }, models, lnd }) {
|
||||
const user = await models.user.findUnique({ where: { id } })
|
||||
|
|
|
@ -11,14 +11,18 @@ import {
|
|||
getInvoice, getPayment, parsePaymentRequest,
|
||||
payViaPaymentRequest, settleHodlInvoice
|
||||
} from 'ln-service'
|
||||
import { MIN_SETTLEMENT_CLTV_DELTA } from 'wallets/wrap'
|
||||
import { MIN_SETTLEMENT_CLTV_DELTA } from '@/wallets/wrap'
|
||||
|
||||
// aggressive finalization retry options
|
||||
const FINALIZE_OPTIONS = { retryLimit: 2 ** 31 - 1, retryBackoff: false, retryDelay: 5, priority: 1000 }
|
||||
|
||||
async function transitionInvoice (jobName, { invoiceId, fromState, toState, transition, invoice }, { models, lnd, boss }) {
|
||||
async function transitionInvoice (jobName,
|
||||
{ invoiceId, fromState, toState, transition, invoice, onUnexpectedError },
|
||||
{ models, lnd, boss }
|
||||
) {
|
||||
console.group(`${jobName}: transitioning invoice ${invoiceId} from ${fromState} to ${toState}`)
|
||||
|
||||
let dbInvoice
|
||||
try {
|
||||
const currentDbInvoice = await models.invoice.findUnique({ where: { id: invoiceId } })
|
||||
console.log('invoice is in state', currentDbInvoice.actionState)
|
||||
|
@ -47,7 +51,7 @@ async function transitionInvoice (jobName, { invoiceId, fromState, toState, tran
|
|||
}
|
||||
|
||||
// grab optimistic concurrency lock and the invoice
|
||||
const dbInvoice = await tx.invoice.update({
|
||||
dbInvoice = await tx.invoice.update({
|
||||
include,
|
||||
where: {
|
||||
id: invoiceId,
|
||||
|
@ -100,6 +104,7 @@ async function transitionInvoice (jobName, { invoiceId, fromState, toState, tran
|
|||
}
|
||||
|
||||
console.error('unexpected error', e)
|
||||
onUnexpectedError?.({ error: e, dbInvoice, models, boss })
|
||||
await boss.send(
|
||||
jobName,
|
||||
{ invoiceId },
|
||||
|
@ -110,35 +115,35 @@ async function transitionInvoice (jobName, { invoiceId, fromState, toState, tran
|
|||
}
|
||||
|
||||
async function performPessimisticAction ({ lndInvoice, dbInvoice, tx, models, lnd, boss }) {
|
||||
try {
|
||||
const args = { ...dbInvoice.actionArgs, invoiceId: dbInvoice.id }
|
||||
const context = {
|
||||
tx,
|
||||
cost: BigInt(lndInvoice.received_mtokens),
|
||||
me: dbInvoice.user
|
||||
}
|
||||
const sybilFeePercent = await paidActions[dbInvoice.actionType].getSybilFeePercent?.(args, context)
|
||||
|
||||
const result = await paidActions[dbInvoice.actionType].perform(args, { ...context, sybilFeePercent })
|
||||
await tx.invoice.update({
|
||||
where: { id: dbInvoice.id },
|
||||
data: {
|
||||
actionResult: result,
|
||||
actionError: null
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
// store the error in the invoice, nonblocking and outside of this tx, finalizing immediately
|
||||
models.invoice.update({
|
||||
where: { id: dbInvoice.id },
|
||||
data: {
|
||||
actionError: e.message
|
||||
}
|
||||
}).catch(e => console.error('failed to store action error', e))
|
||||
boss.send('finalizeHodlInvoice', { hash: dbInvoice.hash }, FINALIZE_OPTIONS)
|
||||
.catch(e => console.error('failed to finalize', e))
|
||||
throw e
|
||||
const args = { ...dbInvoice.actionArgs, invoiceId: dbInvoice.id }
|
||||
const context = {
|
||||
tx,
|
||||
cost: BigInt(lndInvoice.received_mtokens),
|
||||
me: dbInvoice.user
|
||||
}
|
||||
const sybilFeePercent = await paidActions[dbInvoice.actionType].getSybilFeePercent?.(args, context)
|
||||
|
||||
const result = await paidActions[dbInvoice.actionType].perform(args, { ...context, sybilFeePercent })
|
||||
await tx.invoice.update({
|
||||
where: { id: dbInvoice.id },
|
||||
data: {
|
||||
actionResult: result,
|
||||
actionError: null
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// if we experience an unexpected error when holding an invoice, we need aggressively attempt to cancel it
|
||||
// store the error in the invoice, nonblocking and outside of this tx, finalizing immediately
|
||||
function onHeldInvoiceError ({ error, dbInvoice, models, boss }) {
|
||||
models.invoice.update({
|
||||
where: { id: dbInvoice.id },
|
||||
data: {
|
||||
actionError: error.message
|
||||
}
|
||||
}).catch(e => console.error('failed to store action error', e))
|
||||
boss.send('finalizeHodlInvoice', { hash: dbInvoice.hash }, FINALIZE_OPTIONS)
|
||||
.catch(e => console.error('failed to finalize', e))
|
||||
}
|
||||
|
||||
export async function paidActionPaid ({ data: { invoiceId, ...args }, models, lnd, boss }) {
|
||||
|
@ -151,9 +156,17 @@ export async function paidActionPaid ({ data: { invoiceId, ...args }, models, ln
|
|||
throw new Error('invoice is not confirmed')
|
||||
}
|
||||
|
||||
await paidActions[dbInvoice.actionType].onPaid?.({ invoice: dbInvoice }, { models, tx, lnd })
|
||||
const updateFields = {
|
||||
confirmedAt: new Date(lndInvoice.confirmed_at),
|
||||
confirmedIndex: lndInvoice.confirmed_index,
|
||||
msatsReceived: BigInt(lndInvoice.received_mtokens)
|
||||
}
|
||||
|
||||
// any paid action is eligible for a cowboy hat streak
|
||||
await paidActions[dbInvoice.actionType].onPaid?.({
|
||||
invoice: { ...dbInvoice, ...updateFields }
|
||||
}, { models, tx, lnd })
|
||||
|
||||
// most paid actions are eligible for a cowboy hat streak
|
||||
await tx.$executeRaw`
|
||||
INSERT INTO pgboss.job (name, data)
|
||||
VALUES ('checkStreak', jsonb_build_object('id', ${dbInvoice.userId}, 'type', 'COWBOY_HAT'))`
|
||||
|
@ -166,11 +179,7 @@ export async function paidActionPaid ({ data: { invoiceId, ...args }, models, ln
|
|||
('checkStreak', jsonb_build_object('id', ${dbInvoice.invoiceForward.withdrawl.userId}, 'type', 'HORSE'))`
|
||||
}
|
||||
|
||||
return {
|
||||
confirmedAt: new Date(lndInvoice.confirmed_at),
|
||||
confirmedIndex: lndInvoice.confirmed_index,
|
||||
msatsReceived: BigInt(lndInvoice.received_mtokens)
|
||||
}
|
||||
return updateFields
|
||||
},
|
||||
...args
|
||||
}, { models, lnd, boss })
|
||||
|
@ -241,6 +250,7 @@ export async function paidActionForwarding ({ data: { invoiceId, ...args }, mode
|
|||
}
|
||||
}
|
||||
},
|
||||
onUnexpectedError: onHeldInvoiceError,
|
||||
...args
|
||||
}, { models, lnd, boss })
|
||||
|
||||
|
@ -410,6 +420,7 @@ export async function paidActionHeld ({ data: { invoiceId, ...args }, models, ln
|
|||
msatsReceived: BigInt(lndInvoice.received_mtokens)
|
||||
}
|
||||
},
|
||||
onUnexpectedError: onHeldInvoiceError,
|
||||
...args
|
||||
}, { models, lnd, boss })
|
||||
}
|
||||
|
|
|
@ -99,7 +99,7 @@ function getStreakQuery (type, userId) {
|
|||
FROM "Invoice"
|
||||
JOIN "InvoiceForward" ON "Invoice".id = "InvoiceForward"."invoiceId"
|
||||
WHERE ("Invoice"."created_at" AT TIME ZONE 'UTC' AT TIME ZONE 'America/Chicago')::date >= ${dayFragment}
|
||||
AND "Invoice"."actionState" = 'PAID'
|
||||
AND "Invoice"."actionState" = 'PAID' AND "Invoice"."actionType" = 'ZAP'
|
||||
${userId ? Prisma.sql`AND "Invoice"."userId" = ${userId}` : Prisma.empty}
|
||||
GROUP BY "Invoice"."userId"
|
||||
HAVING sum(floor("Invoice"."msatsReceived"/1000)) >= ${GUN_STREAK_THRESHOLD}`
|
||||
|
@ -112,7 +112,7 @@ function getStreakQuery (type, userId) {
|
|||
JOIN "InvoiceForward" ON "Withdrawl".id = "InvoiceForward"."withdrawlId"
|
||||
JOIN "Invoice" ON "InvoiceForward"."invoiceId" = "Invoice".id
|
||||
WHERE ("Withdrawl"."created_at" AT TIME ZONE 'UTC' AT TIME ZONE 'America/Chicago')::date >= ${dayFragment}
|
||||
AND "Invoice"."actionState" = 'PAID'
|
||||
AND "Invoice"."actionState" = 'PAID' AND "Invoice"."actionType" = 'ZAP'
|
||||
${userId ? Prisma.sql`AND "Withdrawl"."userId" = ${userId}` : Prisma.empty}
|
||||
GROUP BY "Withdrawl"."userId"
|
||||
HAVING sum(floor("Invoice"."msatsReceived"/1000)) >= ${HORSE_STREAK_THRESHOLD}`
|
||||
|
|
|
@ -144,10 +144,8 @@ export async function checkInvoice ({ data: { hash, invoice }, boss, models, lnd
|
|||
return await paidActionPaid({ data: { invoiceId: dbInv.id, invoice: inv }, models, lnd, boss })
|
||||
}
|
||||
|
||||
// NOTE: confirm invoice prevents double confirmations (idempotent)
|
||||
// ALSO: is_confirmed and is_held are mutually exclusive
|
||||
// that is, a hold invoice will first be is_held but not is_confirmed
|
||||
// and once it's settled it will be is_confirmed but not is_held
|
||||
// XXX we need to keep this to allow production to migrate to new paidAction flow
|
||||
// once all non-paidAction receive invoices are migrated, we can remove this
|
||||
const [[{ confirm_invoice: code }]] = await serialize([
|
||||
models.$queryRaw`SELECT confirm_invoice(${inv.id}, ${Number(inv.received_mtokens)})`,
|
||||
models.invoice.update({ where: { hash }, data: { confirmedIndex: inv.confirmed_index } })
|
||||
|
@ -171,26 +169,6 @@ export async function checkInvoice ({ data: { hash, invoice }, boss, models, lnd
|
|||
}
|
||||
return await paidActionHeld({ data: { invoiceId: dbInv.id, invoice: inv }, models, lnd, boss })
|
||||
}
|
||||
// First query makes sure that after payment, JIT invoices are settled
|
||||
// within 60 seconds or they will be canceled to minimize risk of
|
||||
// force closures or wallets banning us.
|
||||
// Second query is basically confirm_invoice without setting confirmed_at
|
||||
// and without setting the user balance
|
||||
// those will be set when the invoice is settled by user action
|
||||
const expiresAt = new Date(Math.min(dbInv.expiresAt, datePivot(new Date(), { seconds: 60 })))
|
||||
return await serialize([
|
||||
models.$queryRaw`
|
||||
INSERT INTO pgboss.job (name, data, retrylimit, retrybackoff, startafter)
|
||||
VALUES ('finalizeHodlInvoice', jsonb_build_object('hash', ${hash}), 21, true, ${expiresAt})`,
|
||||
models.invoice.update({
|
||||
where: { hash },
|
||||
data: {
|
||||
msatsReceived: Number(inv.received_mtokens),
|
||||
expiresAt,
|
||||
isHeld: true
|
||||
}
|
||||
})
|
||||
], { models })
|
||||
}
|
||||
|
||||
if (inv.is_canceled) {
|
||||
|
|
Loading…
Reference in New Issue