Compare commits
6 Commits
b0207a2906
...
707d7bdf8b
Author | SHA1 | Date |
---|---|---|
soxa | 707d7bdf8b | |
Riccardo Balbo | e05989d371 | |
Riccardo Balbo | 6630899e79 | |
ekzyis | a032da57b9 | |
Keyan | 0bff478d39 | |
ekzyis | 8b5e13236b |
|
@ -1,7 +1,9 @@
|
||||||
import { cachedFetcher } from '@/lib/fetch'
|
import { cachedFetcher } from '@/lib/fetch'
|
||||||
import { toPositiveNumber } from '@/lib/validate'
|
import { toPositiveNumber } from '@/lib/format'
|
||||||
import { authenticatedLndGrpc } from '@/lib/lnd'
|
import { authenticatedLndGrpc } from '@/lib/lnd'
|
||||||
import { getIdentity, getHeight, getWalletInfo, getNode } from 'ln-service'
|
import { getIdentity, getHeight, getWalletInfo, getNode, getPayment } from 'ln-service'
|
||||||
|
import { datePivot } from '@/lib/time'
|
||||||
|
import { LND_PATHFINDING_TIMEOUT_MS } from '@/lib/constants'
|
||||||
|
|
||||||
const lnd = global.lnd || authenticatedLndGrpc({
|
const lnd = global.lnd || authenticatedLndGrpc({
|
||||||
cert: process.env.LND_CERT,
|
cert: process.env.LND_CERT,
|
||||||
|
@ -88,22 +90,22 @@ export function getPaymentFailureStatus (withdrawal) {
|
||||||
throw new Error('withdrawal is not failed')
|
throw new Error('withdrawal is not failed')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (withdrawal?.failed.is_insufficient_balance) {
|
if (withdrawal?.failed?.is_insufficient_balance) {
|
||||||
return {
|
return {
|
||||||
status: 'INSUFFICIENT_BALANCE',
|
status: 'INSUFFICIENT_BALANCE',
|
||||||
message: 'you didn\'t have enough sats'
|
message: 'you didn\'t have enough sats'
|
||||||
}
|
}
|
||||||
} else if (withdrawal?.failed.is_invalid_payment) {
|
} else if (withdrawal?.failed?.is_invalid_payment) {
|
||||||
return {
|
return {
|
||||||
status: 'INVALID_PAYMENT',
|
status: 'INVALID_PAYMENT',
|
||||||
message: 'invalid payment'
|
message: 'invalid payment'
|
||||||
}
|
}
|
||||||
} else if (withdrawal?.failed.is_pathfinding_timeout) {
|
} else if (withdrawal?.failed?.is_pathfinding_timeout) {
|
||||||
return {
|
return {
|
||||||
status: 'PATHFINDING_TIMEOUT',
|
status: 'PATHFINDING_TIMEOUT',
|
||||||
message: 'no route found'
|
message: 'no route found'
|
||||||
}
|
}
|
||||||
} else if (withdrawal?.failed.is_route_not_found) {
|
} else if (withdrawal?.failed?.is_route_not_found) {
|
||||||
return {
|
return {
|
||||||
status: 'ROUTE_NOT_FOUND',
|
status: 'ROUTE_NOT_FOUND',
|
||||||
message: 'no route found'
|
message: 'no route found'
|
||||||
|
@ -160,4 +162,18 @@ export const getNodeSockets = cachedFetcher(async function fetchNodeSockets ({ l
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
export async function getPaymentOrNotSent ({ id, lnd, createdAt }) {
|
||||||
|
try {
|
||||||
|
return await getPayment({ id, lnd })
|
||||||
|
} catch (err) {
|
||||||
|
if (err[1] === 'SentPaymentNotFound' &&
|
||||||
|
createdAt < datePivot(new Date(), { milliseconds: -LND_PATHFINDING_TIMEOUT_MS * 2 })) {
|
||||||
|
// if the payment is older than 2x timeout, but not found in LND, we can assume it errored before lnd stored it
|
||||||
|
return { notSent: true, is_failed: true }
|
||||||
|
} else {
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default lnd
|
export default lnd
|
||||||
|
|
|
@ -3,8 +3,8 @@ import { datePivot } from '@/lib/time'
|
||||||
import { PAID_ACTION_PAYMENT_METHODS, USER_ID } from '@/lib/constants'
|
import { PAID_ACTION_PAYMENT_METHODS, USER_ID } from '@/lib/constants'
|
||||||
import { createHmac } from '@/api/resolvers/wallet'
|
import { createHmac } from '@/api/resolvers/wallet'
|
||||||
import { Prisma } from '@prisma/client'
|
import { Prisma } from '@prisma/client'
|
||||||
import { createWrappedInvoice } from '@/wallets/server'
|
import { createWrappedInvoice, createInvoice as createUserInvoice } from '@/wallets/server'
|
||||||
import { assertBelowMaxPendingInvoices } from './lib/assert'
|
import { assertBelowMaxPendingInvoices, assertBelowMaxPendingDirectPayments } from './lib/assert'
|
||||||
|
|
||||||
import * as ITEM_CREATE from './itemCreate'
|
import * as ITEM_CREATE from './itemCreate'
|
||||||
import * as ITEM_UPDATE from './itemUpdate'
|
import * as ITEM_UPDATE from './itemUpdate'
|
||||||
|
@ -106,6 +106,17 @@ export default async function performPaidAction (actionType, args, incomingConte
|
||||||
}
|
}
|
||||||
} else if (paymentMethod === PAID_ACTION_PAYMENT_METHODS.OPTIMISTIC) {
|
} else if (paymentMethod === PAID_ACTION_PAYMENT_METHODS.OPTIMISTIC) {
|
||||||
return await performOptimisticAction(actionType, args, contextWithPaymentMethod)
|
return await performOptimisticAction(actionType, args, contextWithPaymentMethod)
|
||||||
|
} else if (paymentMethod === PAID_ACTION_PAYMENT_METHODS.DIRECT) {
|
||||||
|
try {
|
||||||
|
return await performDirectAction(actionType, args, contextWithPaymentMethod)
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof NonInvoiceablePeerError) {
|
||||||
|
console.log('peer cannot be invoiced, skipping')
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
console.error(`${paymentMethod} action failed`, e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -229,6 +240,55 @@ async function performP2PAction (actionType, args, incomingContext) {
|
||||||
: await beginPessimisticAction(actionType, args, context)
|
: await beginPessimisticAction(actionType, args, context)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// we don't need to use the module for perform-ing outside actions
|
||||||
|
// because we can't track the state of outside invoices we aren't paid/paying
|
||||||
|
async function performDirectAction (actionType, args, incomingContext) {
|
||||||
|
const { models, lnd, cost } = incomingContext
|
||||||
|
const { comment, lud18Data, noteStr, description: actionDescription } = args
|
||||||
|
|
||||||
|
const userId = await paidActions[actionType]?.getInvoiceablePeer?.(args, incomingContext)
|
||||||
|
if (!userId) {
|
||||||
|
throw new NonInvoiceablePeerError()
|
||||||
|
}
|
||||||
|
|
||||||
|
let invoiceObject
|
||||||
|
|
||||||
|
try {
|
||||||
|
await assertBelowMaxPendingDirectPayments(userId, incomingContext)
|
||||||
|
|
||||||
|
const description = actionDescription ?? await paidActions[actionType].describe(args, incomingContext)
|
||||||
|
invoiceObject = await createUserInvoice(userId, {
|
||||||
|
msats: cost,
|
||||||
|
description,
|
||||||
|
expiry: INVOICE_EXPIRE_SECS
|
||||||
|
}, { models, lnd })
|
||||||
|
} catch (e) {
|
||||||
|
console.error('failed to create outside invoice', e)
|
||||||
|
throw new NonInvoiceablePeerError()
|
||||||
|
}
|
||||||
|
|
||||||
|
const { invoice, wallet } = invoiceObject
|
||||||
|
const hash = parsePaymentRequest({ request: invoice }).id
|
||||||
|
|
||||||
|
const payment = await models.directPayment.create({
|
||||||
|
data: {
|
||||||
|
comment,
|
||||||
|
lud18Data,
|
||||||
|
desc: noteStr,
|
||||||
|
bolt11: invoice,
|
||||||
|
msats: cost,
|
||||||
|
hash,
|
||||||
|
walletId: wallet.id,
|
||||||
|
receiverId: userId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
invoice: payment,
|
||||||
|
paymentMethod: 'DIRECT'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function retryPaidAction (actionType, args, incomingContext) {
|
export async function retryPaidAction (actionType, args, incomingContext) {
|
||||||
const { models, me } = incomingContext
|
const { models, me } = incomingContext
|
||||||
const { invoice: failedInvoice } = args
|
const { invoice: failedInvoice } = args
|
||||||
|
@ -256,7 +316,7 @@ export async function retryPaidAction (actionType, args, incomingContext) {
|
||||||
throw new Error(`retryPaidAction - missing invoice ${actionType}`)
|
throw new Error(`retryPaidAction - missing invoice ${actionType}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { msatsRequested, actionId } = failedInvoice
|
const { msatsRequested, actionId, actionArgs } = failedInvoice
|
||||||
const retryContext = {
|
const retryContext = {
|
||||||
...incomingContext,
|
...incomingContext,
|
||||||
optimistic: true,
|
optimistic: true,
|
||||||
|
@ -265,7 +325,7 @@ export async function retryPaidAction (actionType, args, incomingContext) {
|
||||||
actionId
|
actionId
|
||||||
}
|
}
|
||||||
|
|
||||||
const invoiceArgs = await createSNInvoice(actionType, args, retryContext)
|
const invoiceArgs = await createSNInvoice(actionType, actionArgs, retryContext)
|
||||||
|
|
||||||
return await models.$transaction(async tx => {
|
return await models.$transaction(async tx => {
|
||||||
const context = { ...retryContext, tx, invoiceArgs }
|
const context = { ...retryContext, tx, invoiceArgs }
|
||||||
|
@ -282,7 +342,7 @@ export async function retryPaidAction (actionType, args, incomingContext) {
|
||||||
})
|
})
|
||||||
|
|
||||||
// create a new invoice
|
// create a new invoice
|
||||||
const invoice = await createDbInvoice(actionType, args, context)
|
const invoice = await createDbInvoice(actionType, actionArgs, context)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
result: await action.retry({ invoiceId: failedInvoice.id, newInvoiceId: invoice.id }, context),
|
result: await action.retry({ invoiceId: failedInvoice.id, newInvoiceId: invoice.id }, context),
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
import { BALANCE_LIMIT_MSATS, PAID_ACTION_TERMINAL_STATES, USER_ID, SN_ADMIN_IDS } from '@/lib/constants'
|
import { BALANCE_LIMIT_MSATS, PAID_ACTION_TERMINAL_STATES, USER_ID, SN_ADMIN_IDS } from '@/lib/constants'
|
||||||
import { msatsToSats, numWithUnits } from '@/lib/format'
|
import { msatsToSats, numWithUnits } from '@/lib/format'
|
||||||
|
import { datePivot } from '@/lib/time'
|
||||||
|
|
||||||
const MAX_PENDING_PAID_ACTIONS_PER_USER = 100
|
const MAX_PENDING_PAID_ACTIONS_PER_USER = 100
|
||||||
|
const MAX_PENDING_DIRECT_INVOICES_PER_USER_MINUTES = 10
|
||||||
|
const MAX_PENDING_DIRECT_INVOICES_PER_USER = 100
|
||||||
const USER_IDS_BALANCE_NO_LIMIT = [...SN_ADMIN_IDS, USER_ID.anon, USER_ID.ad]
|
const USER_IDS_BALANCE_NO_LIMIT = [...SN_ADMIN_IDS, USER_ID.anon, USER_ID.ad]
|
||||||
|
|
||||||
export async function assertBelowMaxPendingInvoices (context) {
|
export async function assertBelowMaxPendingInvoices (context) {
|
||||||
|
@ -20,6 +23,40 @@ export async function assertBelowMaxPendingInvoices (context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function assertBelowMaxPendingDirectPayments (userId, context) {
|
||||||
|
const { models, me } = context
|
||||||
|
|
||||||
|
if (me?.id !== userId) {
|
||||||
|
const pendingSenderInvoices = await models.directPayment.count({
|
||||||
|
where: {
|
||||||
|
senderId: me?.id ?? USER_ID.anon,
|
||||||
|
createdAt: {
|
||||||
|
gt: datePivot(new Date(), { minutes: -MAX_PENDING_DIRECT_INVOICES_PER_USER_MINUTES })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (pendingSenderInvoices >= MAX_PENDING_DIRECT_INVOICES_PER_USER) {
|
||||||
|
throw new Error('You\'ve sent too many direct payments')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!userId) return
|
||||||
|
|
||||||
|
const pendingReceiverInvoices = await models.directPayment.count({
|
||||||
|
where: {
|
||||||
|
receiverId: userId,
|
||||||
|
createdAt: {
|
||||||
|
gt: datePivot(new Date(), { minutes: -MAX_PENDING_DIRECT_INVOICES_PER_USER_MINUTES })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (pendingReceiverInvoices >= MAX_PENDING_DIRECT_INVOICES_PER_USER) {
|
||||||
|
throw new Error('Receiver has too many direct payments')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function assertBelowBalanceLimit (context) {
|
export async function assertBelowBalanceLimit (context) {
|
||||||
const { me, tx } = context
|
const { me, tx } = context
|
||||||
if (!me || USER_IDS_BALANCE_NO_LIMIT.includes(me.id)) return
|
if (!me || USER_IDS_BALANCE_NO_LIMIT.includes(me.id)) return
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { PAID_ACTION_PAYMENT_METHODS } from '@/lib/constants'
|
import { PAID_ACTION_PAYMENT_METHODS } from '@/lib/constants'
|
||||||
import { toPositiveBigInt } from '@/lib/validate'
|
import { toPositiveBigInt, numWithUnits, msatsToSats, satsToMsats } from '@/lib/format'
|
||||||
import { notifyDeposit } from '@/lib/webPush'
|
import { notifyDeposit } from '@/lib/webPush'
|
||||||
import { numWithUnits, msatsToSats, satsToMsats } from '@/lib/format'
|
|
||||||
import { getInvoiceableWallets } from '@/wallets/server'
|
import { getInvoiceableWallets } from '@/wallets/server'
|
||||||
import { assertBelowBalanceLimit } from './lib/assert'
|
import { assertBelowBalanceLimit } from './lib/assert'
|
||||||
|
|
||||||
|
@ -9,6 +8,7 @@ export const anonable = false
|
||||||
|
|
||||||
export const paymentMethods = [
|
export const paymentMethods = [
|
||||||
PAID_ACTION_PAYMENT_METHODS.P2P,
|
PAID_ACTION_PAYMENT_METHODS.P2P,
|
||||||
|
PAID_ACTION_PAYMENT_METHODS.DIRECT,
|
||||||
PAID_ACTION_PAYMENT_METHODS.OPTIMISTIC
|
PAID_ACTION_PAYMENT_METHODS.OPTIMISTIC
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -16,17 +16,17 @@ export async function getCost ({ msats }) {
|
||||||
return toPositiveBigInt(msats)
|
return toPositiveBigInt(msats)
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getInvoiceablePeer (_, { me, models, cost }) {
|
export async function getInvoiceablePeer (_, { me, models, cost, paymentMethod }) {
|
||||||
if (!me?.proxyReceive) return null
|
if (paymentMethod === PAID_ACTION_PAYMENT_METHODS.P2P && !me?.proxyReceive) return null
|
||||||
const wallets = await getInvoiceableWallets(me.id, { models })
|
if (paymentMethod === PAID_ACTION_PAYMENT_METHODS.DIRECT && !me?.directReceive) return null
|
||||||
|
if ((cost + me.msats) <= satsToMsats(me.autoWithdrawThreshold)) return null
|
||||||
|
|
||||||
// if the user has any invoiceable wallets and this action will result in their balance
|
const wallets = await getInvoiceableWallets(me.id, { models })
|
||||||
// being greater than their desired threshold
|
if (wallets.length === 0) {
|
||||||
if (wallets.length > 0 && (cost + me.msats) > satsToMsats(me.autoWithdrawThreshold)) {
|
return null
|
||||||
return me.id
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null
|
return me.id
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getSybilFeePercent () {
|
export async function getSybilFeePercent () {
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
import { LND_PATHFINDING_TIMEOUT_MS } from '@/lib/constants'
|
||||||
|
import { msatsToSats, satsToMsats, toPositiveBigInt } from '@/lib/format'
|
||||||
|
import { Prisma } from '@prisma/client'
|
||||||
|
import { parsePaymentRequest, payViaPaymentRequest } from 'ln-service'
|
||||||
|
|
||||||
|
// paying actions are completely distinct from paid actions
|
||||||
|
// and there's only one paying action: send
|
||||||
|
// ... still we want the api to at least be similar
|
||||||
|
export default async function performPayingAction ({ bolt11, maxFee, walletId }, { me, models, lnd }) {
|
||||||
|
try {
|
||||||
|
console.group('performPayingAction', `${bolt11.slice(0, 10)}...`, maxFee, walletId)
|
||||||
|
|
||||||
|
if (!me) {
|
||||||
|
throw new Error('You must be logged in to perform this action')
|
||||||
|
}
|
||||||
|
|
||||||
|
const decoded = await parsePaymentRequest({ request: bolt11 })
|
||||||
|
const cost = toPositiveBigInt(toPositiveBigInt(decoded.mtokens) + satsToMsats(maxFee))
|
||||||
|
|
||||||
|
console.log('cost', cost)
|
||||||
|
|
||||||
|
const withdrawal = await models.$transaction(async tx => {
|
||||||
|
await tx.user.update({
|
||||||
|
where: {
|
||||||
|
id: me.id
|
||||||
|
},
|
||||||
|
data: { msats: { decrement: cost } }
|
||||||
|
})
|
||||||
|
|
||||||
|
return await tx.withdrawl.create({
|
||||||
|
data: {
|
||||||
|
hash: decoded.id,
|
||||||
|
bolt11,
|
||||||
|
msatsPaying: toPositiveBigInt(decoded.mtokens),
|
||||||
|
msatsFeePaying: satsToMsats(maxFee),
|
||||||
|
userId: me.id,
|
||||||
|
walletId,
|
||||||
|
autoWithdraw: !!walletId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, { isolationLevel: Prisma.TransactionIsolationLevel.ReadCommitted })
|
||||||
|
|
||||||
|
payViaPaymentRequest({
|
||||||
|
lnd,
|
||||||
|
request: withdrawal.bolt11,
|
||||||
|
max_fee: msatsToSats(withdrawal.msatsFeePaying),
|
||||||
|
pathfinding_timeout: LND_PATHFINDING_TIMEOUT_MS
|
||||||
|
}).catch(console.error)
|
||||||
|
|
||||||
|
return withdrawal
|
||||||
|
} catch (e) {
|
||||||
|
if (e.message.includes('\\"users\\" violates check constraint \\"msats_positive\\"')) {
|
||||||
|
throw new Error('insufficient funds')
|
||||||
|
}
|
||||||
|
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
|
||||||
|
throw new Error('you cannot withdraw to the same invoice twice')
|
||||||
|
}
|
||||||
|
console.error('performPayingAction failed', e)
|
||||||
|
throw e
|
||||||
|
} finally {
|
||||||
|
console.groupEnd()
|
||||||
|
}
|
||||||
|
}
|
|
@ -440,29 +440,37 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.noteWithdrawals) {
|
if (user.noteWithdrawals) {
|
||||||
|
const p2pZap = await models.invoice.findFirst({
|
||||||
|
where: {
|
||||||
|
confirmedAt: {
|
||||||
|
gt: lastChecked
|
||||||
|
},
|
||||||
|
invoiceForward: {
|
||||||
|
withdrawl: {
|
||||||
|
userId: me.id,
|
||||||
|
status: 'CONFIRMED',
|
||||||
|
updatedAt: {
|
||||||
|
gt: lastChecked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (p2pZap) {
|
||||||
|
foundNotes()
|
||||||
|
return true
|
||||||
|
}
|
||||||
const wdrwl = await models.withdrawl.findFirst({
|
const wdrwl = await models.withdrawl.findFirst({
|
||||||
where: {
|
where: {
|
||||||
userId: me.id,
|
userId: me.id,
|
||||||
status: 'CONFIRMED',
|
status: 'CONFIRMED',
|
||||||
|
hash: {
|
||||||
|
not: null
|
||||||
|
},
|
||||||
updatedAt: {
|
updatedAt: {
|
||||||
gt: lastChecked
|
gt: lastChecked
|
||||||
},
|
},
|
||||||
OR: [
|
invoiceForward: { is: null }
|
||||||
{
|
|
||||||
invoiceForward: {
|
|
||||||
none: {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
invoiceForward: {
|
|
||||||
some: {
|
|
||||||
invoice: {
|
|
||||||
actionType: 'ZAP'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if (wdrwl) {
|
if (wdrwl) {
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
import {
|
import {
|
||||||
payViaPaymentRequest,
|
|
||||||
getInvoice as getInvoiceFromLnd, deletePayment, getPayment,
|
getInvoice as getInvoiceFromLnd, deletePayment, getPayment,
|
||||||
parsePaymentRequest
|
parsePaymentRequest
|
||||||
} from 'ln-service'
|
} from 'ln-service'
|
||||||
import crypto, { timingSafeEqual } from 'crypto'
|
import crypto, { timingSafeEqual } from 'crypto'
|
||||||
import serialize from './serial'
|
|
||||||
import { decodeCursor, LIMIT, nextCursorEncoded } from '@/lib/cursor'
|
import { decodeCursor, LIMIT, nextCursorEncoded } from '@/lib/cursor'
|
||||||
import { SELECT, itemQueryWithMeta } from './item'
|
import { SELECT, itemQueryWithMeta } from './item'
|
||||||
import { formatMsats, formatSats, msatsToSats, msatsToSatsDecimal, satsToMsats } from '@/lib/format'
|
import { formatMsats, msatsToSats, msatsToSatsDecimal, satsToMsats } from '@/lib/format'
|
||||||
import {
|
import {
|
||||||
USER_ID, INVOICE_RETENTION_DAYS, LND_PATHFINDING_TIMEOUT_MS
|
USER_ID, INVOICE_RETENTION_DAYS,
|
||||||
|
PAID_ACTION_PAYMENT_METHODS
|
||||||
} from '@/lib/constants'
|
} from '@/lib/constants'
|
||||||
import { amountSchema, validateSchema, withdrawlSchema, lnAddrSchema } from '@/lib/validate'
|
import { amountSchema, validateSchema, withdrawlSchema, lnAddrSchema } from '@/lib/validate'
|
||||||
import assertGofacYourself from './ofac'
|
import assertGofacYourself from './ofac'
|
||||||
|
@ -24,6 +23,7 @@ import { getNodeSockets, getOurPubkey } from '../lnd'
|
||||||
import validateWallet from '@/wallets/validate'
|
import validateWallet from '@/wallets/validate'
|
||||||
import { canReceive } from '@/wallets/common'
|
import { canReceive } from '@/wallets/common'
|
||||||
import performPaidAction from '../paidAction'
|
import performPaidAction from '../paidAction'
|
||||||
|
import performPayingAction from '../payingAction'
|
||||||
|
|
||||||
function injectResolvers (resolvers) {
|
function injectResolvers (resolvers) {
|
||||||
console.group('injected GraphQL resolvers:')
|
console.group('injected GraphQL resolvers:')
|
||||||
|
@ -190,6 +190,18 @@ const resolvers = {
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
withdrawl: getWithdrawl,
|
withdrawl: getWithdrawl,
|
||||||
|
direct: async (parent, { id }, { me, models }) => {
|
||||||
|
if (!me) {
|
||||||
|
throw new GqlAuthenticationError()
|
||||||
|
}
|
||||||
|
|
||||||
|
return await models.directPayment.findUnique({
|
||||||
|
where: {
|
||||||
|
id: Number(id),
|
||||||
|
receiverId: me.id
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
numBolt11s: async (parent, args, { me, models, lnd }) => {
|
numBolt11s: async (parent, args, { me, models, lnd }) => {
|
||||||
if (!me) {
|
if (!me) {
|
||||||
throw new GqlAuthenticationError()
|
throw new GqlAuthenticationError()
|
||||||
|
@ -251,6 +263,17 @@ const resolvers = {
|
||||||
AND "Withdrawl".created_at <= $2
|
AND "Withdrawl".created_at <= $2
|
||||||
GROUP BY "Withdrawl".id)`
|
GROUP BY "Withdrawl".id)`
|
||||||
)
|
)
|
||||||
|
queries.push(
|
||||||
|
`(SELECT id, created_at as "createdAt", msats, 'direct' as type,
|
||||||
|
jsonb_build_object(
|
||||||
|
'bolt11', bolt11,
|
||||||
|
'description', "desc",
|
||||||
|
'invoiceComment', comment,
|
||||||
|
'invoicePayerData', "lud18Data") as other
|
||||||
|
FROM "DirectPayment"
|
||||||
|
WHERE "DirectPayment"."receiverId" = $1
|
||||||
|
AND "DirectPayment".created_at <= $2)`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (include.has('stacked')) {
|
if (include.has('stacked')) {
|
||||||
|
@ -436,33 +459,32 @@ const resolvers = {
|
||||||
WalletDetails: {
|
WalletDetails: {
|
||||||
__resolveType: wallet => wallet.__resolveType
|
__resolveType: wallet => wallet.__resolveType
|
||||||
},
|
},
|
||||||
|
InvoiceOrDirect: {
|
||||||
|
__resolveType: invoiceOrDirect => invoiceOrDirect.__resolveType
|
||||||
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
createInvoice: async (parent, { amount }, { me, models, lnd, headers }) => {
|
createInvoice: async (parent, { amount }, { me, models, lnd, headers }) => {
|
||||||
await validateSchema(amountSchema, { amount })
|
await validateSchema(amountSchema, { amount })
|
||||||
await assertGofacYourself({ models, headers })
|
await assertGofacYourself({ models, headers })
|
||||||
|
|
||||||
const { invoice } = await performPaidAction('RECEIVE', {
|
const { invoice, paymentMethod } = await performPaidAction('RECEIVE', {
|
||||||
msats: satsToMsats(amount)
|
msats: satsToMsats(amount)
|
||||||
}, { models, lnd, me })
|
}, { models, lnd, me })
|
||||||
|
|
||||||
return invoice
|
return {
|
||||||
|
...invoice,
|
||||||
|
__resolveType:
|
||||||
|
paymentMethod === PAID_ACTION_PAYMENT_METHODS.DIRECT ? 'Direct' : 'Invoice'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
createWithdrawl: createWithdrawal,
|
createWithdrawl: createWithdrawal,
|
||||||
sendToLnAddr,
|
sendToLnAddr,
|
||||||
cancelInvoice: async (parent, { hash, hmac }, { models, lnd, boss }) => {
|
cancelInvoice: async (parent, { hash, hmac }, { models, lnd, boss }) => {
|
||||||
verifyHmac(hash, hmac)
|
verifyHmac(hash, hmac)
|
||||||
const dbInv = await finalizeHodlInvoice({ data: { hash }, lnd, models, boss })
|
await finalizeHodlInvoice({ data: { hash }, lnd, models, boss })
|
||||||
|
|
||||||
if (dbInv?.invoiceForward) {
|
|
||||||
const { wallet, bolt11 } = dbInv.invoiceForward
|
|
||||||
const logger = walletLogger({ wallet, models })
|
|
||||||
const decoded = await parsePaymentRequest({ request: bolt11 })
|
|
||||||
logger.info(`invoice for ${formatSats(msatsToSats(decoded.mtokens))} canceled by payer`, { bolt11 })
|
|
||||||
}
|
|
||||||
|
|
||||||
return await models.invoice.findFirst({ where: { hash } })
|
return await models.invoice.findFirst({ where: { hash } })
|
||||||
},
|
},
|
||||||
dropBolt11: async (parent, { id }, { me, models, lnd }) => {
|
dropBolt11: async (parent, { hash }, { me, models, lnd }) => {
|
||||||
if (!me) {
|
if (!me) {
|
||||||
throw new GqlAuthenticationError()
|
throw new GqlAuthenticationError()
|
||||||
}
|
}
|
||||||
|
@ -470,20 +492,20 @@ const resolvers = {
|
||||||
const retention = `${INVOICE_RETENTION_DAYS} days`
|
const retention = `${INVOICE_RETENTION_DAYS} days`
|
||||||
|
|
||||||
const [invoice] = await models.$queryRaw`
|
const [invoice] = await models.$queryRaw`
|
||||||
WITH to_be_updated AS (
|
WITH to_be_updated AS (
|
||||||
SELECT id, hash, bolt11
|
SELECT id, hash, bolt11
|
||||||
FROM "Withdrawl"
|
FROM "Withdrawl"
|
||||||
WHERE "userId" = ${me.id}
|
WHERE "userId" = ${me.id}
|
||||||
AND id = ${Number(id)}
|
AND hash = ${hash}
|
||||||
AND now() > created_at + ${retention}::INTERVAL
|
AND now() > created_at + ${retention}::INTERVAL
|
||||||
AND hash IS NOT NULL
|
AND hash IS NOT NULL
|
||||||
AND status IS NOT NULL
|
AND status IS NOT NULL
|
||||||
), updated_rows AS (
|
), updated_rows AS (
|
||||||
UPDATE "Withdrawl"
|
UPDATE "Withdrawl"
|
||||||
SET hash = NULL, bolt11 = NULL, preimage = NULL
|
SET hash = NULL, bolt11 = NULL, preimage = NULL
|
||||||
FROM to_be_updated
|
FROM to_be_updated
|
||||||
WHERE "Withdrawl".id = to_be_updated.id)
|
WHERE "Withdrawl".id = to_be_updated.id)
|
||||||
SELECT * FROM to_be_updated;`
|
SELECT * FROM to_be_updated;`
|
||||||
|
|
||||||
if (invoice) {
|
if (invoice) {
|
||||||
try {
|
try {
|
||||||
|
@ -497,7 +519,16 @@ const resolvers = {
|
||||||
throw new GqlInputError('failed to drop bolt11 from lnd')
|
throw new GqlInputError('failed to drop bolt11 from lnd')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { id }
|
|
||||||
|
await models.$queryRaw`
|
||||||
|
UPDATE "DirectPayment"
|
||||||
|
SET hash = NULL, bolt11 = NULL, preimage = NULL
|
||||||
|
WHERE "receiverId" = ${me.id}
|
||||||
|
AND hash = ${hash}
|
||||||
|
AND now() > created_at + ${retention}::INTERVAL
|
||||||
|
AND hash IS NOT NULL`
|
||||||
|
|
||||||
|
return true
|
||||||
},
|
},
|
||||||
setWalletPriority: async (parent, { id, priority }, { me, models }) => {
|
setWalletPriority: async (parent, { id, priority }, { me, models }) => {
|
||||||
if (!me) {
|
if (!me) {
|
||||||
|
@ -538,11 +569,11 @@ const resolvers = {
|
||||||
Withdrawl: {
|
Withdrawl: {
|
||||||
satsPaying: w => msatsToSats(w.msatsPaying),
|
satsPaying: w => msatsToSats(w.msatsPaying),
|
||||||
satsPaid: w => msatsToSats(w.msatsPaid),
|
satsPaid: w => msatsToSats(w.msatsPaid),
|
||||||
satsFeePaying: w => w.invoiceForward?.length > 0 ? 0 : msatsToSats(w.msatsFeePaying),
|
satsFeePaying: w => w.invoiceForward ? 0 : msatsToSats(w.msatsFeePaying),
|
||||||
satsFeePaid: w => w.invoiceForward?.length > 0 ? 0 : msatsToSats(w.msatsFeePaid),
|
satsFeePaid: w => w.invoiceForward ? 0 : msatsToSats(w.msatsFeePaid),
|
||||||
// we never want to fetch the sensitive data full monty in nested resolvers
|
// we never want to fetch the sensitive data full monty in nested resolvers
|
||||||
forwardedActionType: async (withdrawl, args, { models }) => {
|
forwardedActionType: async (withdrawl, args, { models }) => {
|
||||||
return (await models.invoiceForward.findFirst({
|
return (await models.invoiceForward.findUnique({
|
||||||
where: { withdrawlId: Number(withdrawl.id) },
|
where: { withdrawlId: Number(withdrawl.id) },
|
||||||
include: {
|
include: {
|
||||||
invoice: true
|
invoice: true
|
||||||
|
@ -551,7 +582,7 @@ const resolvers = {
|
||||||
},
|
},
|
||||||
preimage: async (withdrawl, args, { lnd }) => {
|
preimage: async (withdrawl, args, { lnd }) => {
|
||||||
try {
|
try {
|
||||||
if (withdrawl.status === 'CONFIRMED') {
|
if (withdrawl.status === 'CONFIRMED' && withdrawl.hash) {
|
||||||
return withdrawl.preimage ?? (await getPayment({ id: withdrawl.hash, lnd })).payment.secret
|
return withdrawl.preimage ?? (await getPayment({ id: withdrawl.hash, lnd })).payment.secret
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -559,6 +590,17 @@ const resolvers = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
Direct: {
|
||||||
|
nostr: async (direct, args, { models }) => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(direct.desc)
|
||||||
|
} catch (err) {
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
sats: direct => msatsToSats(direct.msats)
|
||||||
|
},
|
||||||
|
|
||||||
Invoice: {
|
Invoice: {
|
||||||
satsReceived: i => msatsToSats(i.msatsReceived),
|
satsReceived: i => msatsToSats(i.msatsReceived),
|
||||||
|
@ -876,10 +918,6 @@ export async function createWithdrawal (parent, { invoice, maxFee }, { me, model
|
||||||
throw new GqlInputError('invoice amount is too large')
|
throw new GqlInputError('invoice amount is too large')
|
||||||
}
|
}
|
||||||
|
|
||||||
const msatsFee = Number(maxFee) * 1000
|
|
||||||
|
|
||||||
const user = await models.user.findUnique({ where: { id: me.id } })
|
|
||||||
|
|
||||||
// check if there's an invoice with same hash that has an invoiceForward
|
// 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
|
// we can't allow this because it creates two outgoing payments from our node
|
||||||
// with the same hash
|
// with the same hash
|
||||||
|
@ -891,23 +929,7 @@ export async function createWithdrawal (parent, { invoice, maxFee }, { me, model
|
||||||
throw new GqlInputError('SN cannot pay an invoice that SN is proxying')
|
throw new GqlInputError('SN cannot pay an invoice that SN is proxying')
|
||||||
}
|
}
|
||||||
|
|
||||||
const autoWithdraw = !!wallet?.id
|
return await performPayingAction({ bolt11: invoice, maxFee, walletId: wallet?.id }, { me, models, lnd })
|
||||||
// create withdrawl transactionally (id, bolt11, amount, fee)
|
|
||||||
const [withdrawl] = await serialize(
|
|
||||||
models.$queryRaw`SELECT * FROM create_withdrawl(${decoded.id}, ${invoice},
|
|
||||||
${Number(decoded.mtokens)}, ${msatsFee}, ${user.name}, ${autoWithdraw}, ${wallet?.id}::INTEGER)`,
|
|
||||||
{ models }
|
|
||||||
)
|
|
||||||
|
|
||||||
payViaPaymentRequest({
|
|
||||||
lnd,
|
|
||||||
request: invoice,
|
|
||||||
// can't use max_fee_mtokens https://github.com/alexbosworth/ln-service/issues/141
|
|
||||||
max_fee: Number(maxFee),
|
|
||||||
pathfinding_timeout: LND_PATHFINDING_TIMEOUT_MS
|
|
||||||
}).catch(console.error)
|
|
||||||
|
|
||||||
return withdrawl
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sendToLnAddr (parent, { addr, amount, maxFee, comment, ...payer },
|
export async function sendToLnAddr (parent, { addr, amount, maxFee, comment, ...payer },
|
||||||
|
|
|
@ -108,6 +108,7 @@ export default gql`
|
||||||
wildWestMode: Boolean!
|
wildWestMode: Boolean!
|
||||||
withdrawMaxFeeDefault: Int!
|
withdrawMaxFeeDefault: Int!
|
||||||
proxyReceive: Boolean
|
proxyReceive: Boolean
|
||||||
|
directReceive: Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthMethods {
|
type AuthMethods {
|
||||||
|
@ -187,6 +188,7 @@ export default gql`
|
||||||
vaultKeyHash: String
|
vaultKeyHash: String
|
||||||
walletsUpdatedAt: Date
|
walletsUpdatedAt: Date
|
||||||
proxyReceive: Boolean
|
proxyReceive: Boolean
|
||||||
|
directReceive: Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserOptional {
|
type UserOptional {
|
||||||
|
|
|
@ -64,6 +64,7 @@ const typeDefs = `
|
||||||
extend type Query {
|
extend type Query {
|
||||||
invoice(id: ID!): Invoice!
|
invoice(id: ID!): Invoice!
|
||||||
withdrawl(id: ID!): Withdrawl!
|
withdrawl(id: ID!): Withdrawl!
|
||||||
|
direct(id: ID!): Direct!
|
||||||
numBolt11s: Int!
|
numBolt11s: Int!
|
||||||
connectAddress: String!
|
connectAddress: String!
|
||||||
walletHistory(cursor: String, inc: String): History
|
walletHistory(cursor: String, inc: String): History
|
||||||
|
@ -74,16 +75,20 @@ const typeDefs = `
|
||||||
}
|
}
|
||||||
|
|
||||||
extend type Mutation {
|
extend type Mutation {
|
||||||
createInvoice(amount: Int!): Invoice!
|
createInvoice(amount: Int!): InvoiceOrDirect!
|
||||||
createWithdrawl(invoice: String!, maxFee: Int!): Withdrawl!
|
createWithdrawl(invoice: String!, maxFee: Int!): Withdrawl!
|
||||||
sendToLnAddr(addr: String!, amount: Int!, maxFee: Int!, comment: String, identifier: Boolean, name: String, email: String): Withdrawl!
|
sendToLnAddr(addr: String!, amount: Int!, maxFee: Int!, comment: String, identifier: Boolean, name: String, email: String): Withdrawl!
|
||||||
cancelInvoice(hash: String!, hmac: String!): Invoice!
|
cancelInvoice(hash: String!, hmac: String!): Invoice!
|
||||||
dropBolt11(id: ID): Withdrawl
|
dropBolt11(hash: String!): Boolean
|
||||||
removeWallet(id: ID!): Boolean
|
removeWallet(id: ID!): Boolean
|
||||||
deleteWalletLogs(wallet: String): Boolean
|
deleteWalletLogs(wallet: String): Boolean
|
||||||
setWalletPriority(id: ID!, priority: Int!): Boolean
|
setWalletPriority(id: ID!, priority: Int!): Boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface InvoiceOrDirect {
|
||||||
|
id: ID!
|
||||||
|
}
|
||||||
|
|
||||||
type Wallet {
|
type Wallet {
|
||||||
id: ID!
|
id: ID!
|
||||||
createdAt: Date!
|
createdAt: Date!
|
||||||
|
@ -101,7 +106,7 @@ const typeDefs = `
|
||||||
autoWithdrawMaxFeeTotal: Int!
|
autoWithdrawMaxFeeTotal: Int!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Invoice {
|
type Invoice implements InvoiceOrDirect {
|
||||||
id: ID!
|
id: ID!
|
||||||
createdAt: Date!
|
createdAt: Date!
|
||||||
hash: String!
|
hash: String!
|
||||||
|
@ -141,6 +146,18 @@ const typeDefs = `
|
||||||
forwardedActionType: String
|
forwardedActionType: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Direct implements InvoiceOrDirect {
|
||||||
|
id: ID!
|
||||||
|
createdAt: Date!
|
||||||
|
bolt11: String
|
||||||
|
hash: String
|
||||||
|
sats: Int
|
||||||
|
preimage: String
|
||||||
|
nostr: JSONObject
|
||||||
|
comment: String
|
||||||
|
lud18Data: JSONObject
|
||||||
|
}
|
||||||
|
|
||||||
type Fact {
|
type Fact {
|
||||||
id: ID!
|
id: ID!
|
||||||
createdAt: Date!
|
createdAt: Date!
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { InputGroup } from 'react-bootstrap'
|
||||||
import { Input } from './form'
|
import { Input } from './form'
|
||||||
import { useMe } from './me'
|
import { useMe } from './me'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { isNumber } from '@/lib/validate'
|
import { isNumber } from '@/lib/format'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
|
||||||
function autoWithdrawThreshold ({ me }) {
|
function autoWithdrawThreshold ({ me }) {
|
||||||
|
|
|
@ -360,12 +360,22 @@ export function MarkdownInput ({ label, topLevel, groupClassName, onChange, onKe
|
||||||
onUpload={file => {
|
onUpload={file => {
|
||||||
const uploadMarker = `![Uploading ${file.name}…]()`
|
const uploadMarker = `![Uploading ${file.name}…]()`
|
||||||
const text = innerRef.current.value
|
const text = innerRef.current.value
|
||||||
const cursorPosition = innerRef.current.selectionStart || text.length
|
const cursorPosition = innerRef.current.selectionStart
|
||||||
let preMarker = text.slice(0, cursorPosition)
|
let preMarker = text.slice(0, cursorPosition)
|
||||||
const postMarker = text.slice(cursorPosition)
|
let postMarker = text.slice(cursorPosition)
|
||||||
// when uploading multiple files at once, we want to make sure the upload markers are separated by blank lines
|
// when uploading multiple files at once, we want to make sure the upload markers are separated by blank lines
|
||||||
if (preMarker && !/\n+\s*$/.test(preMarker)) {
|
if (preMarker) {
|
||||||
preMarker += '\n\n'
|
// Count existing newlines at the end of preMarker
|
||||||
|
const existingNewlines = preMarker.match(/[\n]+$/)?.[0].length || 0
|
||||||
|
// Add only the needed newlines to reach 2
|
||||||
|
preMarker += '\n'.repeat(Math.max(0, 2 - existingNewlines))
|
||||||
|
}
|
||||||
|
// if there's text after the cursor, we want to make sure the upload marker is separated by a blank line
|
||||||
|
if (postMarker) {
|
||||||
|
// Count existing newlines at the start of postMarker
|
||||||
|
const existingNewlines = postMarker.match(/^[\n]*/)?.[0].length || 0
|
||||||
|
// Add only the needed newlines to reach 2
|
||||||
|
postMarker = '\n'.repeat(Math.max(0, 2 - existingNewlines)) + postMarker
|
||||||
}
|
}
|
||||||
const newText = preMarker + uploadMarker + postMarker
|
const newText = preMarker + uploadMarker + postMarker
|
||||||
helpers.setValue(newText)
|
helpers.setValue(newText)
|
||||||
|
|
|
@ -103,7 +103,7 @@ export default function Invoice ({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const { nostr, comment, lud18Data, bolt11, confirmedPreimage } = invoice
|
const { bolt11, confirmedPreimage } = invoice
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -120,36 +120,7 @@ export default function Invoice ({
|
||||||
{!modal &&
|
{!modal &&
|
||||||
<>
|
<>
|
||||||
{info && <div className='text-muted fst-italic text-center'>{info}</div>}
|
{info && <div className='text-muted fst-italic text-center'>{info}</div>}
|
||||||
<div className='w-100'>
|
<InvoiceExtras {...invoice} />
|
||||||
{nostr
|
|
||||||
? <AccordianItem
|
|
||||||
header='Nostr Zap Request'
|
|
||||||
body={
|
|
||||||
<pre>
|
|
||||||
<code>
|
|
||||||
{JSON.stringify(nostr, null, 2)}
|
|
||||||
</code>
|
|
||||||
</pre>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
: null}
|
|
||||||
</div>
|
|
||||||
{lud18Data &&
|
|
||||||
<div className='w-100'>
|
|
||||||
<AccordianItem
|
|
||||||
header='sender information'
|
|
||||||
body={<PayerData data={lud18Data} className='text-muted ms-3' />}
|
|
||||||
className='mb-3'
|
|
||||||
/>
|
|
||||||
</div>}
|
|
||||||
{comment &&
|
|
||||||
<div className='w-100'>
|
|
||||||
<AccordianItem
|
|
||||||
header='sender comments'
|
|
||||||
body={<span className='text-muted ms-3'>{comment}</span>}
|
|
||||||
className='mb-3'
|
|
||||||
/>
|
|
||||||
</div>}
|
|
||||||
<Bolt11Info bolt11={bolt11} preimage={confirmedPreimage} />
|
<Bolt11Info bolt11={bolt11} preimage={confirmedPreimage} />
|
||||||
{invoice?.item && <ActionInfo invoice={invoice} />}
|
{invoice?.item && <ActionInfo invoice={invoice} />}
|
||||||
</>}
|
</>}
|
||||||
|
@ -157,6 +128,43 @@ export default function Invoice ({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function InvoiceExtras ({ nostr, lud18Data, comment }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className='w-100'>
|
||||||
|
{nostr
|
||||||
|
? <AccordianItem
|
||||||
|
header='Nostr Zap Request'
|
||||||
|
body={
|
||||||
|
<pre>
|
||||||
|
<code>
|
||||||
|
{JSON.stringify(nostr, null, 2)}
|
||||||
|
</code>
|
||||||
|
</pre>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
|
{lud18Data &&
|
||||||
|
<div className='w-100'>
|
||||||
|
<AccordianItem
|
||||||
|
header='sender information'
|
||||||
|
body={<PayerData data={lud18Data} className='text-muted ms-3' />}
|
||||||
|
className='mb-3'
|
||||||
|
/>
|
||||||
|
</div>}
|
||||||
|
{comment &&
|
||||||
|
<div className='w-100'>
|
||||||
|
<AccordianItem
|
||||||
|
header='sender comments'
|
||||||
|
body={<span className='text-muted ms-3'>{comment}</span>}
|
||||||
|
className='mb-3'
|
||||||
|
/>
|
||||||
|
</div>}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function ActionInfo ({ invoice }) {
|
function ActionInfo ({ invoice }) {
|
||||||
if (!invoice.actionType) return null
|
if (!invoice.actionType) return null
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import { gql, useApolloClient, useMutation } from '@apollo/client'
|
import { gql, useApolloClient, useMutation } from '@apollo/client'
|
||||||
import { useWallet } from '@/wallets/index'
|
import { useWallet } from '@/wallets/index'
|
||||||
import { FAST_POLL_INTERVAL, JIT_INVOICE_TIMEOUT_MS } from '@/lib/constants'
|
import { FAST_POLL_INTERVAL } from '@/lib/constants'
|
||||||
import { INVOICE } from '@/fragments/wallet'
|
import { INVOICE } from '@/fragments/wallet'
|
||||||
import Invoice from '@/components/invoice'
|
import Invoice from '@/components/invoice'
|
||||||
import { useShowModal } from './modal'
|
import { useShowModal } from './modal'
|
||||||
|
@ -10,17 +10,6 @@ import { InvoiceCanceledError, NoAttachedWalletError, InvoiceExpiredError } from
|
||||||
export const useInvoice = () => {
|
export const useInvoice = () => {
|
||||||
const client = useApolloClient()
|
const client = useApolloClient()
|
||||||
|
|
||||||
const [createInvoice] = useMutation(gql`
|
|
||||||
mutation createInvoice($amount: Int!, $expireSecs: Int!) {
|
|
||||||
createInvoice(amount: $amount, hodlInvoice: true, expireSecs: $expireSecs) {
|
|
||||||
id
|
|
||||||
bolt11
|
|
||||||
hash
|
|
||||||
hmac
|
|
||||||
expiresAt
|
|
||||||
satsRequested
|
|
||||||
}
|
|
||||||
}`)
|
|
||||||
const [cancelInvoice] = useMutation(gql`
|
const [cancelInvoice] = useMutation(gql`
|
||||||
mutation cancelInvoice($hash: String!, $hmac: String!) {
|
mutation cancelInvoice($hash: String!, $hmac: String!) {
|
||||||
cancelInvoice(hash: $hash, hmac: $hmac) {
|
cancelInvoice(hash: $hash, hmac: $hmac) {
|
||||||
|
@ -29,15 +18,6 @@ export const useInvoice = () => {
|
||||||
}
|
}
|
||||||
`)
|
`)
|
||||||
|
|
||||||
const create = useCallback(async amount => {
|
|
||||||
const { data, error } = await createInvoice({ variables: { amount, expireSecs: JIT_INVOICE_TIMEOUT_MS / 1000 } })
|
|
||||||
if (error) {
|
|
||||||
throw error
|
|
||||||
}
|
|
||||||
const invoice = data.createInvoice
|
|
||||||
return invoice
|
|
||||||
}, [createInvoice])
|
|
||||||
|
|
||||||
const isInvoice = useCallback(async ({ id }, that) => {
|
const isInvoice = useCallback(async ({ id }, that) => {
|
||||||
const { data, error } = await client.query({ query: INVOICE, fetchPolicy: 'network-only', variables: { id } })
|
const { data, error } = await client.query({ query: INVOICE, fetchPolicy: 'network-only', variables: { id } })
|
||||||
if (error) {
|
if (error) {
|
||||||
|
@ -73,7 +53,7 @@ export const useInvoice = () => {
|
||||||
return inv
|
return inv
|
||||||
}, [cancelInvoice])
|
}, [cancelInvoice])
|
||||||
|
|
||||||
return { create, cancel, isInvoice }
|
return { cancel, isInvoice }
|
||||||
}
|
}
|
||||||
|
|
||||||
const invoiceController = (id, isInvoice) => {
|
const invoiceController = (id, isInvoice) => {
|
||||||
|
|
|
@ -25,12 +25,10 @@ export default function WalletCard ({ wallet, draggable, onDragStart, onDragEnte
|
||||||
onDragEnter={onDragEnter}
|
onDragEnter={onDragEnter}
|
||||||
onDragEnd={onDragEnd}
|
onDragEnd={onDragEnd}
|
||||||
>
|
>
|
||||||
<div className={styles.cardMeta}>
|
<div className={styles.indicators}>
|
||||||
<div className={styles.indicators}>
|
{status.any !== Status.Disabled && <DraggableIcon className={styles.drag} width={16} height={16} />}
|
||||||
{status.any !== Status.Disabled && <DraggableIcon className={styles.drag} width={16} height={16} />}
|
{support.recv && <RecvIcon className={`${styles.indicator} ${statusToClass(status.recv)}`} />}
|
||||||
{support.recv && <RecvIcon className={`${styles.indicator} ${statusToClass(status.recv)}`} />}
|
{support.send && <SendIcon className={`${styles.indicator} ${statusToClass(status.send)}`} />}
|
||||||
{support.send && <SendIcon className={`${styles.indicator} ${statusToClass(status.send)}`} />}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<Card.Body
|
<Card.Body
|
||||||
// we attach touch listener only to card body to not interfere with wallet link
|
// we attach touch listener only to card body to not interfere with wallet link
|
||||||
|
@ -42,8 +40,8 @@ export default function WalletCard ({ wallet, draggable, onDragStart, onDragEnte
|
||||||
>
|
>
|
||||||
<div className='d-flex text-center align-items-center h-100'>
|
<div className='d-flex text-center align-items-center h-100'>
|
||||||
{image
|
{image
|
||||||
? <img width='100%' {...image} />
|
? <img className={styles.walletLogo} {...image} />
|
||||||
: <Card.Title className='w-100 justify-content-center align-items-center'>{wallet.def.card.title}</Card.Title>}
|
: <Card.Title className={styles.walletLogo}>{wallet.def.card.title}</Card.Title>}
|
||||||
</div>
|
</div>
|
||||||
</Card.Body>
|
</Card.Body>
|
||||||
<Link href={`/settings/wallets/${wallet.def.name}`}>
|
<Link href={`/settings/wallets/${wallet.def.name}`}>
|
||||||
|
|
|
@ -170,7 +170,7 @@ export function useWalletLogger (wallet, setLogs) {
|
||||||
const decoded = bolt11Decode(context.bolt11)
|
const decoded = bolt11Decode(context.bolt11)
|
||||||
context = {
|
context = {
|
||||||
...context,
|
...context,
|
||||||
amount: formatMsats(Number(decoded.millisatoshis)),
|
amount: formatMsats(decoded.millisatoshis),
|
||||||
payment_hash: decoded.tagsObject.payment_hash,
|
payment_hash: decoded.tagsObject.payment_hash,
|
||||||
description: decoded.tagsObject.description,
|
description: decoded.tagsObject.description,
|
||||||
created_at: new Date(decoded.timestamp * 1000).toISOString(),
|
created_at: new Date(decoded.timestamp * 1000).toISOString(),
|
||||||
|
|
|
@ -51,6 +51,7 @@ ${STREAK_FIELDS}
|
||||||
vaultKeyHash
|
vaultKeyHash
|
||||||
walletsUpdatedAt
|
walletsUpdatedAt
|
||||||
proxyReceive
|
proxyReceive
|
||||||
|
directReceive
|
||||||
}
|
}
|
||||||
optional {
|
optional {
|
||||||
isContributor
|
isContributor
|
||||||
|
@ -113,6 +114,7 @@ export const SETTINGS_FIELDS = gql`
|
||||||
}
|
}
|
||||||
apiKeyEnabled
|
apiKeyEnabled
|
||||||
proxyReceive
|
proxyReceive
|
||||||
|
directReceive
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
|
|
|
@ -53,6 +53,7 @@ export const WITHDRAWL = gql`
|
||||||
id
|
id
|
||||||
createdAt
|
createdAt
|
||||||
bolt11
|
bolt11
|
||||||
|
hash
|
||||||
satsPaid
|
satsPaid
|
||||||
satsFeePaying
|
satsFeePaying
|
||||||
satsFeePaid
|
satsFeePaid
|
||||||
|
@ -63,6 +64,21 @@ export const WITHDRAWL = gql`
|
||||||
}
|
}
|
||||||
}`
|
}`
|
||||||
|
|
||||||
|
export const DIRECT = gql`
|
||||||
|
query Direct($id: ID!) {
|
||||||
|
direct(id: $id) {
|
||||||
|
id
|
||||||
|
createdAt
|
||||||
|
bolt11
|
||||||
|
hash
|
||||||
|
sats
|
||||||
|
preimage
|
||||||
|
comment
|
||||||
|
lud18Data
|
||||||
|
nostr
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
export const WALLET_HISTORY = gql`
|
export const WALLET_HISTORY = gql`
|
||||||
${ITEM_FULL_FIELDS}
|
${ITEM_FULL_FIELDS}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ export const PAID_ACTION_PAYMENT_METHODS = {
|
||||||
FEE_CREDIT: 'FEE_CREDIT',
|
FEE_CREDIT: 'FEE_CREDIT',
|
||||||
PESSIMISTIC: 'PESSIMISTIC',
|
PESSIMISTIC: 'PESSIMISTIC',
|
||||||
OPTIMISTIC: 'OPTIMISTIC',
|
OPTIMISTIC: 'OPTIMISTIC',
|
||||||
|
DIRECT: 'DIRECT',
|
||||||
P2P: 'P2P'
|
P2P: 'P2P'
|
||||||
}
|
}
|
||||||
export const PAID_ACTION_TERMINAL_STATES = ['FAILED', 'PAID', 'RETRYING']
|
export const PAID_ACTION_TERMINAL_STATES = ['FAILED', 'PAID', 'RETRYING']
|
||||||
|
|
|
@ -73,7 +73,7 @@ export const msatsToSatsDecimal = msats => {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const formatSats = (sats) => numWithUnits(sats, { unitSingular: 'sat', unitPlural: 'sats', abbreviate: false })
|
export const formatSats = (sats) => numWithUnits(sats, { unitSingular: 'sat', unitPlural: 'sats', abbreviate: false })
|
||||||
export const formatMsats = (msats) => numWithUnits(msats, { unitSingular: 'msat', unitPlural: 'msats', abbreviate: false })
|
export const formatMsats = (msats) => numWithUnits(toPositiveNumber(msats), { unitSingular: 'msat', unitPlural: 'msats', abbreviate: false })
|
||||||
|
|
||||||
export const hexToB64 = hexstring => {
|
export const hexToB64 = hexstring => {
|
||||||
return btoa(hexstring.match(/\w{2}/g).map(function (a) {
|
return btoa(hexstring.match(/\w{2}/g).map(function (a) {
|
||||||
|
@ -128,3 +128,79 @@ export function giveOrdinalSuffix (i) {
|
||||||
}
|
}
|
||||||
return i + 'th'
|
return i + 'th'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check if something is _really_ a number.
|
||||||
|
// returns true for every number in this range: [-Infinity, ..., 0, ..., Infinity]
|
||||||
|
export const isNumber = x => typeof x === 'number' && !Number.isNaN(x)
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {any | bigint} x
|
||||||
|
* @param {number} min
|
||||||
|
* @param {number} max
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
export const toNumber = (x, min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER) => {
|
||||||
|
if (typeof x === 'undefined') {
|
||||||
|
throw new Error('value is required')
|
||||||
|
}
|
||||||
|
if (typeof x === 'bigint') {
|
||||||
|
if (x < BigInt(min) || x > BigInt(max)) {
|
||||||
|
throw new Error(`value ${x} must be between ${min} and ${max}`)
|
||||||
|
}
|
||||||
|
return Number(x)
|
||||||
|
} else {
|
||||||
|
const n = Number(x)
|
||||||
|
if (isNumber(n)) {
|
||||||
|
if (x < min || x > max) {
|
||||||
|
throw new Error(`value ${x} must be between ${min} and ${max}`)
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error(`value ${x} is not a number`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any | bigint} x
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
export const toPositiveNumber = (x) => toNumber(x, 0)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any} x
|
||||||
|
* @param {bigint | number} [min]
|
||||||
|
* @param {bigint | number} [max]
|
||||||
|
* @returns {bigint}
|
||||||
|
*/
|
||||||
|
export const toBigInt = (x, min, max) => {
|
||||||
|
if (typeof x === 'undefined') throw new Error('value is required')
|
||||||
|
|
||||||
|
const n = BigInt(x)
|
||||||
|
if (min !== undefined && n < BigInt(min)) {
|
||||||
|
throw new Error(`value ${x} must be at least ${min}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (max !== undefined && n > BigInt(max)) {
|
||||||
|
throw new Error(`value ${x} must be at most ${max}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number|bigint} x
|
||||||
|
* @returns {bigint}
|
||||||
|
*/
|
||||||
|
export const toPositiveBigInt = (x) => {
|
||||||
|
return toBigInt(x, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number|bigint} x
|
||||||
|
* @returns {number|bigint}
|
||||||
|
*/
|
||||||
|
export const toPositive = (x) => {
|
||||||
|
if (typeof x === 'bigint') return toPositiveBigInt(x)
|
||||||
|
return toPositiveNumber(x)
|
||||||
|
}
|
||||||
|
|
|
@ -513,79 +513,3 @@ export const deviceSyncSchema = object().shape({
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// check if something is _really_ a number.
|
|
||||||
// returns true for every number in this range: [-Infinity, ..., 0, ..., Infinity]
|
|
||||||
export const isNumber = x => typeof x === 'number' && !Number.isNaN(x)
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {any | bigint} x
|
|
||||||
* @param {number} min
|
|
||||||
* @param {number} max
|
|
||||||
* @returns {number}
|
|
||||||
*/
|
|
||||||
export const toNumber = (x, min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER) => {
|
|
||||||
if (typeof x === 'undefined') {
|
|
||||||
throw new Error('value is required')
|
|
||||||
}
|
|
||||||
if (typeof x === 'bigint') {
|
|
||||||
if (x < BigInt(min) || x > BigInt(max)) {
|
|
||||||
throw new Error(`value ${x} must be between ${min} and ${max}`)
|
|
||||||
}
|
|
||||||
return Number(x)
|
|
||||||
} else {
|
|
||||||
const n = Number(x)
|
|
||||||
if (isNumber(n)) {
|
|
||||||
if (x < min || x > max) {
|
|
||||||
throw new Error(`value ${x} must be between ${min} and ${max}`)
|
|
||||||
}
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new Error(`value ${x} is not a number`)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {any | bigint} x
|
|
||||||
* @returns {number}
|
|
||||||
*/
|
|
||||||
export const toPositiveNumber = (x) => toNumber(x, 0)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {any} x
|
|
||||||
* @param {bigint | number} [min]
|
|
||||||
* @param {bigint | number} [max]
|
|
||||||
* @returns {bigint}
|
|
||||||
*/
|
|
||||||
export const toBigInt = (x, min, max) => {
|
|
||||||
if (typeof x === 'undefined') throw new Error('value is required')
|
|
||||||
|
|
||||||
const n = BigInt(x)
|
|
||||||
if (min !== undefined && n < BigInt(min)) {
|
|
||||||
throw new Error(`value ${x} must be at least ${min}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (max !== undefined && n > BigInt(max)) {
|
|
||||||
throw new Error(`value ${x} must be at most ${max}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return n
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {number|bigint} x
|
|
||||||
* @returns {bigint}
|
|
||||||
*/
|
|
||||||
export const toPositiveBigInt = (x) => {
|
|
||||||
return toBigInt(x, 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {number|bigint} x
|
|
||||||
* @returns {number|bigint}
|
|
||||||
*/
|
|
||||||
export const toPositive = (x) => {
|
|
||||||
if (typeof x === 'bigint') return toPositiveBigInt(x)
|
|
||||||
return toPositiveNumber(x)
|
|
||||||
}
|
|
||||||
|
|
|
@ -350,12 +350,12 @@ export async function notifyDeposit (userId, invoice) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function notifyWithdrawal (userId, wdrwl) {
|
export async function notifyWithdrawal (wdrwl) {
|
||||||
try {
|
try {
|
||||||
await sendUserNotification(userId, {
|
await sendUserNotification(wdrwl.userId, {
|
||||||
title: `${numWithUnits(msatsToSats(wdrwl.payment.mtokens), { abbreviate: false, unitSingular: 'sat was', unitPlural: 'sats were' })} withdrawn from your account`,
|
title: `${numWithUnits(msatsToSats(wdrwl.msatsPaid), { abbreviate: false, unitSingular: 'sat was', unitPlural: 'sats were' })} withdrawn from your account`,
|
||||||
tag: 'WITHDRAWAL',
|
tag: 'WITHDRAWAL',
|
||||||
data: { sats: msatsToSats(wdrwl.payment.mtokens) }
|
data: { sats: msatsToSats(wdrwl.msatsPaid) }
|
||||||
})
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { lnurlPayDescriptionHashForUser, lnurlPayMetadataString, lnurlPayDescrip
|
||||||
import { schnorr } from '@noble/curves/secp256k1'
|
import { schnorr } from '@noble/curves/secp256k1'
|
||||||
import { createHash } from 'crypto'
|
import { createHash } from 'crypto'
|
||||||
import { LNURLP_COMMENT_MAX_LENGTH, MAX_INVOICE_DESCRIPTION_LENGTH } from '@/lib/constants'
|
import { LNURLP_COMMENT_MAX_LENGTH, MAX_INVOICE_DESCRIPTION_LENGTH } from '@/lib/constants'
|
||||||
import { validateSchema, lud18PayerDataSchema, toPositiveBigInt } from '@/lib/validate'
|
import { validateSchema, lud18PayerDataSchema, toPositiveBigInt } from '@/lib/format'
|
||||||
import assertGofacYourself from '@/api/resolvers/ofac'
|
import assertGofacYourself from '@/api/resolvers/ofac'
|
||||||
import performPaidAction from '@/api/paidAction'
|
import performPaidAction from '@/api/paidAction'
|
||||||
|
|
||||||
|
@ -91,7 +91,7 @@ export default async ({ query: { username, amount, nostr, comment, payerdata: pa
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
pr: invoice.bolt11,
|
pr: invoice.bolt11,
|
||||||
routes: [],
|
routes: [],
|
||||||
verify: `${process.env.NEXT_PUBLIC_URL}/api/lnurlp/${username}/verify/${invoice.hash}`
|
verify: invoice.hash ? `${process.env.NEXT_PUBLIC_URL}/api/lnurlp/${username}/verify/${invoice.hash}` : undefined
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
import { useQuery } from '@apollo/client'
|
||||||
|
import { CenterLayout } from '@/components/layout'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import { DIRECT } from '@/fragments/wallet'
|
||||||
|
import { SSR, FAST_POLL_INTERVAL } from '@/lib/constants'
|
||||||
|
import Bolt11Info from '@/components/bolt11-info'
|
||||||
|
import { getGetServerSideProps } from '@/api/ssrApollo'
|
||||||
|
import { PrivacyOption } from '../withdrawals/[id]'
|
||||||
|
import { InvoiceExtras } from '@/components/invoice'
|
||||||
|
import { numWithUnits } from '@/lib/format'
|
||||||
|
import Qr, { QrSkeleton } from '@/components/qr'
|
||||||
|
// force SSR to include CSP nonces
|
||||||
|
export const getServerSideProps = getGetServerSideProps({ query: null })
|
||||||
|
|
||||||
|
export default function Direct () {
|
||||||
|
return (
|
||||||
|
<CenterLayout>
|
||||||
|
<LoadDirect />
|
||||||
|
</CenterLayout>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DirectSkeleton ({ status }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className='w-100 form-group'>
|
||||||
|
<QrSkeleton status={status} />
|
||||||
|
</div>
|
||||||
|
<div className='w-100 mt-3'>
|
||||||
|
<Bolt11Info />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function LoadDirect () {
|
||||||
|
const router = useRouter()
|
||||||
|
const { loading, error, data } = useQuery(DIRECT, SSR
|
||||||
|
? {}
|
||||||
|
: {
|
||||||
|
variables: { id: router.query.id },
|
||||||
|
pollInterval: FAST_POLL_INTERVAL,
|
||||||
|
nextFetchPolicy: 'cache-and-network'
|
||||||
|
})
|
||||||
|
if (error) return <div>error</div>
|
||||||
|
if (!data || loading) {
|
||||||
|
return <DirectSkeleton status='loading' />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Qr
|
||||||
|
value={data.direct.bolt11}
|
||||||
|
description={numWithUnits(data.direct.sats, { abbreviate: false })}
|
||||||
|
statusVariant='pending' status='direct payment to attached wallet'
|
||||||
|
/>
|
||||||
|
<div className='w-100 mt-3'>
|
||||||
|
<InvoiceExtras {...data.direct} />
|
||||||
|
<Bolt11Info bolt11={data.direct.bolt11} preimage={data.direct.preimage} />
|
||||||
|
<div className='w-100 mt-3'>
|
||||||
|
<PrivacyOption payment={data.direct} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -24,7 +24,7 @@ export const getServerSideProps = getGetServerSideProps({ query: WALLET_HISTORY,
|
||||||
|
|
||||||
function satusClass (status) {
|
function satusClass (status) {
|
||||||
if (!status) {
|
if (!status) {
|
||||||
return ''
|
return 'text-reset'
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (status) {
|
switch (status) {
|
||||||
|
|
|
@ -159,7 +159,8 @@ export default function Settings ({ ssrData }) {
|
||||||
diagnostics: settings?.diagnostics,
|
diagnostics: settings?.diagnostics,
|
||||||
hideIsContributor: settings?.hideIsContributor,
|
hideIsContributor: settings?.hideIsContributor,
|
||||||
noReferralLinks: settings?.noReferralLinks,
|
noReferralLinks: settings?.noReferralLinks,
|
||||||
proxyReceive: settings?.proxyReceive
|
proxyReceive: settings?.proxyReceive,
|
||||||
|
directReceive: settings?.directReceive
|
||||||
}}
|
}}
|
||||||
schema={settingsSchema}
|
schema={settingsSchema}
|
||||||
onSubmit={async ({
|
onSubmit={async ({
|
||||||
|
@ -339,7 +340,7 @@ export default function Settings ({ ssrData }) {
|
||||||
<div className='d-flex align-items-center'>proxy deposits to attached wallets
|
<div className='d-flex align-items-center'>proxy deposits to attached wallets
|
||||||
<Info>
|
<Info>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Forward deposits directly to your attached wallets if they will cause your balance to exceed your auto-withdraw threshold</li>
|
<li>Forward deposits directly to your attached wallets if they 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>Payments will be wrapped by the SN node to preserve your wallet's privacy</li>
|
||||||
<li>This will incur in a 10% fee</li>
|
<li>This will incur in a 10% fee</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -349,6 +350,22 @@ export default function Settings ({ ssrData }) {
|
||||||
name='proxyReceive'
|
name='proxyReceive'
|
||||||
groupClassName='mb-0'
|
groupClassName='mb-0'
|
||||||
/>
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label={
|
||||||
|
<div className='d-flex align-items-center'>directly deposit to attached wallets
|
||||||
|
<Info>
|
||||||
|
<ul>
|
||||||
|
<li>Directly deposit to your attached wallets if they cause your balance to exceed your auto-withdraw threshold</li>
|
||||||
|
<li>Senders will be able to see your wallet's lightning node public key</li>
|
||||||
|
<li>If 'proxy deposits' is also checked, it will take precedence and direct deposits will only be used as a fallback</li>
|
||||||
|
<li>Because we can't determine if a payment succeeds, you won't be notified about direct deposits</li>
|
||||||
|
</ul>
|
||||||
|
</Info>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
name='directReceive'
|
||||||
|
groupClassName='mb-0'
|
||||||
|
/>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
label={
|
label={
|
||||||
<div className='d-flex align-items-center'>hide invoice descriptions
|
<div className='d-flex align-items-center'>hide invoice descriptions
|
||||||
|
|
|
@ -19,6 +19,7 @@ import validateWallet from '@/wallets/validate'
|
||||||
import { ValidationError } from 'yup'
|
import { ValidationError } from 'yup'
|
||||||
import { useFormikContext } from 'formik'
|
import { useFormikContext } from 'formik'
|
||||||
import { useWalletImage } from '@/components/wallet-image'
|
import { useWalletImage } from '@/components/wallet-image'
|
||||||
|
import styles from '@/styles/wallet.module.css'
|
||||||
|
|
||||||
export const getServerSideProps = getGetServerSideProps({ authRequired: true })
|
export const getServerSideProps = getGetServerSideProps({ authRequired: true })
|
||||||
|
|
||||||
|
@ -72,7 +73,7 @@ export default function WalletSettings () {
|
||||||
return (
|
return (
|
||||||
<CenterLayout>
|
<CenterLayout>
|
||||||
{image
|
{image
|
||||||
? <img {...image} className='pb-3 px-2 mw-100' />
|
? <img {...image} className={styles.walletBanner} />
|
||||||
: <h2 className='pb-2'>{wallet.def.card.title}</h2>}
|
: <h2 className='pb-2'>{wallet.def.card.title}</h2>}
|
||||||
<h6 className='text-muted text-center pb-3'><Text>{wallet.def.card.subtitle}</Text></h6>
|
<h6 className='text-muted text-center pb-3'><Text>{wallet.def.card.subtitle}</Text></h6>
|
||||||
<Form
|
<Form
|
||||||
|
|
|
@ -7,6 +7,11 @@ import { useCallback, useState } from 'react'
|
||||||
import { useIsClient } from '@/components/use-client'
|
import { useIsClient } from '@/components/use-client'
|
||||||
import WalletCard from '@/components/wallet-card'
|
import WalletCard from '@/components/wallet-card'
|
||||||
import { useToast } from '@/components/toast'
|
import { useToast } from '@/components/toast'
|
||||||
|
import BootstrapForm from 'react-bootstrap/Form'
|
||||||
|
import RecvIcon from '@/svgs/arrow-left-down-line.svg'
|
||||||
|
import SendIcon from '@/svgs/arrow-right-up-line.svg'
|
||||||
|
import { useRouter } from 'next/router'
|
||||||
|
import { supportsReceive, supportsSend } from '@/wallets/common'
|
||||||
|
|
||||||
export const getServerSideProps = getGetServerSideProps({ authRequired: true })
|
export const getServerSideProps = getGetServerSideProps({ authRequired: true })
|
||||||
|
|
||||||
|
@ -17,6 +22,12 @@ export default function Wallet ({ ssrData }) {
|
||||||
const [sourceIndex, setSourceIndex] = useState(null)
|
const [sourceIndex, setSourceIndex] = useState(null)
|
||||||
const [targetIndex, setTargetIndex] = useState(null)
|
const [targetIndex, setTargetIndex] = useState(null)
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const [filter, setFilter] = useState({
|
||||||
|
send: router.query.send === 'true',
|
||||||
|
receive: router.query.receive === 'true'
|
||||||
|
})
|
||||||
|
|
||||||
const reorder = useCallback(async (sourceIndex, targetIndex) => {
|
const reorder = useCallback(async (sourceIndex, targetIndex) => {
|
||||||
const newOrder = [...wallets.filter(w => w.config?.enabled)]
|
const newOrder = [...wallets.filter(w => w.config?.enabled)]
|
||||||
const [source] = newOrder.splice(sourceIndex, 1)
|
const [source] = newOrder.splice(sourceIndex, 1)
|
||||||
|
@ -65,6 +76,13 @@ export default function Wallet ({ ssrData }) {
|
||||||
}
|
}
|
||||||
}, [sourceIndex, reorder, onReorderError])
|
}, [sourceIndex, reorder, onReorderError])
|
||||||
|
|
||||||
|
const onFilterChange = useCallback((key) => {
|
||||||
|
return e => {
|
||||||
|
setFilter(old => ({ ...old, [key]: e.target.checked }))
|
||||||
|
router.replace({ query: { ...router.query, [key]: e.target.checked } }, undefined, { shallow: true })
|
||||||
|
}
|
||||||
|
}, [router])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<div className='py-5 w-100'>
|
<div className='py-5 w-100'>
|
||||||
|
@ -76,33 +94,52 @@ export default function Wallet ({ ssrData }) {
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.walletGrid} onDragEnd={onDragEnd}>
|
<div className={styles.walletGrid} onDragEnd={onDragEnd}>
|
||||||
{wallets.map((w, i) => {
|
<div className={styles.walletFilters}>
|
||||||
const draggable = isClient && w.config?.enabled
|
<BootstrapForm.Check
|
||||||
|
inline
|
||||||
|
label={<span><RecvIcon width={16} height={16} /> receive</span>}
|
||||||
|
onChange={onFilterChange('receive')}
|
||||||
|
checked={filter.receive}
|
||||||
|
/>
|
||||||
|
<BootstrapForm.Check
|
||||||
|
inline
|
||||||
|
label={<span><SendIcon width={16} height={16} /> send</span>}
|
||||||
|
onChange={onFilterChange('send')}
|
||||||
|
checked={filter.send}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{wallets
|
||||||
|
.filter(w => {
|
||||||
|
return (!filter.send || (filter.send && supportsSend(w))) &&
|
||||||
|
(!filter.receive || (filter.receive && supportsReceive(w)))
|
||||||
|
})
|
||||||
|
.map((w, i) => {
|
||||||
|
const draggable = isClient && w.config?.enabled
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={w.def.name}
|
key={w.def.name}
|
||||||
className={
|
className={
|
||||||
!draggable
|
!draggable
|
||||||
? ''
|
? ''
|
||||||
: (`${sourceIndex === i ? styles.drag : ''} ${draggable && targetIndex === i ? styles.drop : ''}`)
|
: (`${sourceIndex === i ? styles.drag : ''} ${draggable && targetIndex === i ? styles.drop : ''}`)
|
||||||
}
|
}
|
||||||
suppressHydrationWarning
|
suppressHydrationWarning
|
||||||
>
|
>
|
||||||
<WalletCard
|
<WalletCard
|
||||||
wallet={w}
|
wallet={w}
|
||||||
draggable={draggable}
|
draggable={draggable}
|
||||||
onDragStart={draggable ? onDragStart(i) : undefined}
|
onDragStart={draggable ? onDragStart(i) : undefined}
|
||||||
onTouchStart={draggable ? onTouchStart(i) : undefined}
|
onTouchStart={draggable ? onTouchStart(i) : undefined}
|
||||||
onDragEnter={draggable ? onDragEnter(i) : undefined}
|
onDragEnter={draggable ? onDragEnter(i) : undefined}
|
||||||
sourceIndex={sourceIndex}
|
sourceIndex={sourceIndex}
|
||||||
targetIndex={targetIndex}
|
targetIndex={targetIndex}
|
||||||
index={i}
|
index={i}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -114,6 +114,7 @@ export function FundForm () {
|
||||||
const [createInvoice, { called, error }] = useMutation(gql`
|
const [createInvoice, { called, error }] = useMutation(gql`
|
||||||
mutation createInvoice($amount: Int!) {
|
mutation createInvoice($amount: Int!) {
|
||||||
createInvoice(amount: $amount) {
|
createInvoice(amount: $amount) {
|
||||||
|
__typename
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
}`)
|
}`)
|
||||||
|
@ -147,7 +148,11 @@ export function FundForm () {
|
||||||
schema={amountSchema}
|
schema={amountSchema}
|
||||||
onSubmit={async ({ amount }) => {
|
onSubmit={async ({ amount }) => {
|
||||||
const { data } = await createInvoice({ variables: { amount: Number(amount) } })
|
const { data } = await createInvoice({ variables: { amount: Number(amount) } })
|
||||||
router.push(`/invoices/${data.createInvoice.id}`)
|
if (data.createInvoice.__typename === 'Direct') {
|
||||||
|
router.push(`/directs/${data.createInvoice.id}`)
|
||||||
|
} else {
|
||||||
|
router.push(`/invoices/${data.createInvoice.id}`)
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
|
|
|
@ -118,18 +118,18 @@ function LoadWithdrawl () {
|
||||||
<InvoiceStatus variant={variant} status={status} />
|
<InvoiceStatus variant={variant} status={status} />
|
||||||
<div className='w-100 mt-3'>
|
<div className='w-100 mt-3'>
|
||||||
<Bolt11Info bolt11={data.withdrawl.bolt11} preimage={data.withdrawl.preimage}>
|
<Bolt11Info bolt11={data.withdrawl.bolt11} preimage={data.withdrawl.preimage}>
|
||||||
<PrivacyOption wd={data.withdrawl} />
|
<PrivacyOption payment={data.withdrawl} />
|
||||||
</Bolt11Info>
|
</Bolt11Info>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function PrivacyOption ({ wd }) {
|
export function PrivacyOption ({ payment }) {
|
||||||
if (!wd.bolt11) return
|
if (!payment.bolt11) return
|
||||||
|
|
||||||
const { me } = useMe()
|
const { me } = useMe()
|
||||||
const keepUntil = datePivot(new Date(wd.createdAt), { days: INVOICE_RETENTION_DAYS })
|
const keepUntil = datePivot(new Date(payment.createdAt), { days: INVOICE_RETENTION_DAYS })
|
||||||
const oldEnough = new Date() >= keepUntil
|
const oldEnough = new Date() >= keepUntil
|
||||||
if (!oldEnough) {
|
if (!oldEnough) {
|
||||||
return (
|
return (
|
||||||
|
@ -143,19 +143,19 @@ function PrivacyOption ({ wd }) {
|
||||||
const toaster = useToast()
|
const toaster = useToast()
|
||||||
const [dropBolt11] = useMutation(
|
const [dropBolt11] = useMutation(
|
||||||
gql`
|
gql`
|
||||||
mutation dropBolt11($id: ID!) {
|
mutation dropBolt11($hash: String!) {
|
||||||
dropBolt11(id: $id) {
|
dropBolt11(hash: $hash)
|
||||||
id
|
|
||||||
}
|
|
||||||
}`, {
|
}`, {
|
||||||
update (cache) {
|
update (cache, { data }) {
|
||||||
cache.modify({
|
if (data.dropBolt11) {
|
||||||
id: `Withdrawl:${wd.id}`,
|
cache.modify({
|
||||||
fields: {
|
id: `${payment.__typename}:${payment.id}`,
|
||||||
bolt11: () => null,
|
fields: {
|
||||||
hash: () => null
|
bolt11: () => null,
|
||||||
}
|
hash: () => null
|
||||||
})
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -169,7 +169,7 @@ function PrivacyOption ({ wd }) {
|
||||||
onConfirm={async () => {
|
onConfirm={async () => {
|
||||||
if (me) {
|
if (me) {
|
||||||
try {
|
try {
|
||||||
await dropBolt11({ variables: { id: wd.id } })
|
await dropBolt11({ variables: { hash: payment.hash } })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toaster.danger('unable to delete invoice: ' + err.message || err.toString?.())
|
toaster.danger('unable to delete invoice: ' + err.message || err.toString?.())
|
||||||
throw err
|
throw err
|
||||||
|
|
|
@ -0,0 +1,53 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "users" ADD COLUMN "directReceive" BOOLEAN NOT NULL DEFAULT false;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "DirectPayment" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"senderId" INTEGER,
|
||||||
|
"receiverId" INTEGER,
|
||||||
|
"preimage" TEXT,
|
||||||
|
"bolt11" TEXT,
|
||||||
|
"walletId" INTEGER,
|
||||||
|
"comment" TEXT,
|
||||||
|
"desc" TEXT,
|
||||||
|
"lud18Data" JSONB,
|
||||||
|
"msats" BIGINT NOT NULL,
|
||||||
|
"hash" TEXT,
|
||||||
|
CONSTRAINT "DirectPayment_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "DirectPayment_preimage_key" ON "DirectPayment"("preimage");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "DirectPayment_created_at_idx" ON "DirectPayment"("created_at");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "DirectPayment_senderId_idx" ON "DirectPayment"("senderId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "DirectPayment_receiverId_idx" ON "DirectPayment"("receiverId");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "DirectPayment" ADD CONSTRAINT "DirectPayment_senderId_fkey" FOREIGN KEY ("senderId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "DirectPayment" ADD CONSTRAINT "DirectPayment_receiverId_fkey" FOREIGN KEY ("receiverId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "DirectPayment" ADD CONSTRAINT "DirectPayment_walletId_fkey" FOREIGN KEY ("walletId") REFERENCES "Wallet"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- drop dead functions replaced by paid/paying action state machines
|
||||||
|
DROP FUNCTION IF EXISTS confirm_invoice;
|
||||||
|
DROP FUNCTION IF EXISTS create_withdrawl;
|
||||||
|
DROP FUNCTION IF EXISTS confirm_withdrawl;
|
||||||
|
DROP FUNCTION IF EXISTS reverse_withdrawl;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "InvoiceForward_withdrawlId_key" ON "InvoiceForward"("withdrawlId");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "DirectPayment_hash_key" ON "DirectPayment"("hash");
|
|
@ -141,6 +141,9 @@ model User {
|
||||||
walletsUpdatedAt DateTime?
|
walletsUpdatedAt DateTime?
|
||||||
vaultEntries VaultEntry[] @relation("VaultEntries")
|
vaultEntries VaultEntry[] @relation("VaultEntries")
|
||||||
proxyReceive Boolean @default(false)
|
proxyReceive Boolean @default(false)
|
||||||
|
directReceive Boolean @default(false)
|
||||||
|
DirectPaymentReceived DirectPayment[] @relation("DirectPaymentReceived")
|
||||||
|
DirectPaymentSent DirectPayment[] @relation("DirectPaymentSent")
|
||||||
|
|
||||||
@@index([photoId])
|
@@index([photoId])
|
||||||
@@index([createdAt], map: "users.created_at_index")
|
@@index([createdAt], map: "users.created_at_index")
|
||||||
|
@ -217,6 +220,7 @@ model Wallet {
|
||||||
vaultEntries VaultEntry[] @relation("VaultEntries")
|
vaultEntries VaultEntry[] @relation("VaultEntries")
|
||||||
withdrawals Withdrawl[]
|
withdrawals Withdrawl[]
|
||||||
InvoiceForward InvoiceForward[]
|
InvoiceForward InvoiceForward[]
|
||||||
|
DirectPayment DirectPayment[]
|
||||||
|
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
@@index([priority])
|
@@index([priority])
|
||||||
|
@ -940,6 +944,29 @@ model Invoice {
|
||||||
@@index([actionState])
|
@@index([actionState])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model DirectPayment {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
updatedAt DateTime @default(now()) @updatedAt @map("updated_at")
|
||||||
|
senderId Int?
|
||||||
|
receiverId Int?
|
||||||
|
preimage String? @unique
|
||||||
|
bolt11 String?
|
||||||
|
hash String? @unique
|
||||||
|
desc String?
|
||||||
|
comment String?
|
||||||
|
lud18Data Json?
|
||||||
|
msats BigInt
|
||||||
|
walletId Int?
|
||||||
|
sender User? @relation("DirectPaymentSent", fields: [senderId], references: [id], onDelete: Cascade)
|
||||||
|
receiver User? @relation("DirectPaymentReceived", fields: [receiverId], references: [id], onDelete: Cascade)
|
||||||
|
wallet Wallet? @relation(fields: [walletId], references: [id], onDelete: SetNull)
|
||||||
|
|
||||||
|
@@index([createdAt])
|
||||||
|
@@index([senderId])
|
||||||
|
@@index([receiverId])
|
||||||
|
}
|
||||||
|
|
||||||
model InvoiceForward {
|
model InvoiceForward {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
createdAt DateTime @default(now()) @map("created_at")
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
@ -954,7 +981,7 @@ model InvoiceForward {
|
||||||
|
|
||||||
// we get these values when the outgoing invoice is settled
|
// we get these values when the outgoing invoice is settled
|
||||||
invoiceId Int @unique
|
invoiceId Int @unique
|
||||||
withdrawlId Int?
|
withdrawlId Int? @unique
|
||||||
|
|
||||||
invoice Invoice @relation(fields: [invoiceId], references: [id], onDelete: Cascade)
|
invoice Invoice @relation(fields: [invoiceId], references: [id], onDelete: Cascade)
|
||||||
wallet Wallet @relation(fields: [walletId], references: [id], onDelete: Cascade)
|
wallet Wallet @relation(fields: [walletId], references: [id], onDelete: Cascade)
|
||||||
|
@ -982,7 +1009,7 @@ model Withdrawl {
|
||||||
walletId Int?
|
walletId Int?
|
||||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||||
wallet Wallet? @relation(fields: [walletId], references: [id], onDelete: SetNull)
|
wallet Wallet? @relation(fields: [walletId], references: [id], onDelete: SetNull)
|
||||||
invoiceForward InvoiceForward[]
|
invoiceForward InvoiceForward?
|
||||||
|
|
||||||
@@index([createdAt], map: "Withdrawl.created_at_index")
|
@@index([createdAt], map: "Withdrawl.created_at_index")
|
||||||
@@index([userId], map: "Withdrawl.userId_index")
|
@@index([userId], map: "Withdrawl.userId_index")
|
||||||
|
|
2
sndev
2
sndev
|
@ -261,7 +261,7 @@ sndev__withdraw() {
|
||||||
if [ "$1" = "--cln" ]; then
|
if [ "$1" = "--cln" ]; then
|
||||||
shift
|
shift
|
||||||
label=$(date +%s)
|
label=$(date +%s)
|
||||||
sndev__cli -t cln invoice "$1" "$label" sndev | jq -j '.bolt11'; echo
|
sndev__cli -t cln invoice "$1"sats "$label" sndev | jq -j '.bolt11'; echo
|
||||||
else
|
else
|
||||||
sndev__cli lnd addinvoice --amt "$@" | jq -j '.payment_request'; echo
|
sndev__cli lnd addinvoice --amt "$@" | jq -j '.payment_request'; echo
|
||||||
fi
|
fi
|
||||||
|
|
|
@ -1,10 +1,34 @@
|
||||||
.walletGrid {
|
.walletGrid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
grid-template-columns: repeat(auto-fill, 160px);
|
||||||
grid-gap: 20px;
|
grid-gap: 20px;
|
||||||
justify-items: center;
|
padding: 20px 0;
|
||||||
align-items: center;
|
justify-content: center;
|
||||||
margin-top: 3rem;
|
}
|
||||||
|
|
||||||
|
@media (max-width: 440px) {
|
||||||
|
.walletGrid {
|
||||||
|
grid-template-columns: repeat(auto-fill, 140px);
|
||||||
|
grid-gap: 15px;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
width: 140px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 330px) {
|
||||||
|
.walletGrid {
|
||||||
|
grid-template-columns: 100%;
|
||||||
|
}
|
||||||
|
.walletGrid > * {
|
||||||
|
justify-self: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.walletFilters {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
margin-left: 0.2rem;
|
||||||
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.drag {
|
.drag {
|
||||||
|
@ -17,18 +41,34 @@
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
width: 160px;
|
width: 160px;
|
||||||
height: 180px;
|
max-width: 100%;
|
||||||
|
aspect-ratio: 160 / 180;
|
||||||
}
|
}
|
||||||
|
|
||||||
.indicators {
|
.indicators {
|
||||||
position: absolute;
|
display: flex;
|
||||||
width: 100%;
|
|
||||||
display: grid;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: end;
|
column-gap: 0.2rem;
|
||||||
padding: 10px 10px 0 10px;
|
margin-left: auto;
|
||||||
grid-gap: 0.2rem;
|
padding: 10px;
|
||||||
grid-auto-flow: column;
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.walletLogo {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 40%;
|
||||||
|
margin: auto;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.walletBanner {
|
||||||
|
max-width: min(256px, 100vw);
|
||||||
|
max-height: 100px;
|
||||||
|
padding: 0 15px 1rem 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge {
|
.badge {
|
||||||
|
|
|
@ -3,106 +3,109 @@ import { bolt11Tags } from '@/lib/bolt11'
|
||||||
import { Mutex } from 'async-mutex'
|
import { Mutex } from 'async-mutex'
|
||||||
export * from '@/wallets/lnc'
|
export * from '@/wallets/lnc'
|
||||||
|
|
||||||
async function disconnect (lnc, logger) {
|
const mutex = new Mutex()
|
||||||
if (lnc) {
|
const serverHost = 'mailbox.terminal.lightning.today:443'
|
||||||
try {
|
|
||||||
lnc.disconnect()
|
|
||||||
logger.info('disconnecting...')
|
|
||||||
// wait for lnc to disconnect before releasing the mutex
|
|
||||||
await new Promise((resolve, reject) => {
|
|
||||||
let counter = 0
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
if (lnc?.isConnected) {
|
|
||||||
if (counter++ > 100) {
|
|
||||||
logger.error('failed to disconnect from lnc')
|
|
||||||
clearInterval(interval)
|
|
||||||
reject(new Error('failed to disconnect from lnc'))
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
clearInterval(interval)
|
|
||||||
resolve()
|
|
||||||
})
|
|
||||||
}, 50)
|
|
||||||
logger.info('disconnected')
|
|
||||||
} catch (err) {
|
|
||||||
logger.error('failed to disconnect from lnc: ' + err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function testSendPayment (credentials, { logger }) {
|
export async function testSendPayment (credentials, { logger }) {
|
||||||
let lnc
|
const lnc = await getLNC(credentials, { logger })
|
||||||
try {
|
logger?.info('validating permissions ...')
|
||||||
lnc = await getLNC(credentials)
|
await validateNarrowPerms(lnc)
|
||||||
|
logger?.info('permissions ok')
|
||||||
logger.info('connecting ...')
|
return lnc.credentials.credentials
|
||||||
await lnc.connect()
|
|
||||||
logger.info('connected')
|
|
||||||
|
|
||||||
logger.info('validating permissions ...')
|
|
||||||
await validateNarrowPerms(lnc)
|
|
||||||
logger.info('permissions ok')
|
|
||||||
|
|
||||||
return lnc.credentials.credentials
|
|
||||||
} finally {
|
|
||||||
await disconnect(lnc, logger)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mutex = new Mutex()
|
|
||||||
|
|
||||||
export async function sendPayment (bolt11, credentials, { logger }) {
|
export async function sendPayment (bolt11, credentials, { logger }) {
|
||||||
const hash = bolt11Tags(bolt11).payment_hash
|
const hash = bolt11Tags(bolt11).payment_hash
|
||||||
|
|
||||||
return await mutex.runExclusive(async () => {
|
return await mutex.runExclusive(async () => {
|
||||||
let lnc
|
|
||||||
try {
|
try {
|
||||||
lnc = await getLNC(credentials)
|
const lnc = await getLNC(credentials, { logger })
|
||||||
|
const { paymentError, paymentPreimage: preimage } = await lnc.lnd.lightning.sendPaymentSync({ payment_request: bolt11 })
|
||||||
await lnc.connect()
|
|
||||||
const { paymentError, paymentPreimage: preimage } =
|
|
||||||
await lnc.lnd.lightning.sendPaymentSync({ payment_request: bolt11 })
|
|
||||||
|
|
||||||
if (paymentError) throw new Error(paymentError)
|
if (paymentError) throw new Error(paymentError)
|
||||||
if (!preimage) throw new Error('No preimage in response')
|
if (!preimage) throw new Error('No preimage in response')
|
||||||
|
|
||||||
return preimage
|
return preimage
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const msg = err.message || err.toString?.()
|
const msg = err.message || err.toString?.()
|
||||||
if (msg.includes('invoice expired')) {
|
if (msg.includes('invoice expired')) throw new InvoiceExpiredError(hash)
|
||||||
throw new InvoiceExpiredError(hash)
|
if (msg.includes('canceled')) throw new InvoiceCanceledError(hash)
|
||||||
}
|
|
||||||
if (msg.includes('canceled')) {
|
|
||||||
throw new InvoiceCanceledError(hash)
|
|
||||||
}
|
|
||||||
throw err
|
throw err
|
||||||
} finally {
|
|
||||||
await disconnect(lnc, logger)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getLNC (credentials = {}) {
|
async function disconnectLNC (lnc, { logger } = {}) {
|
||||||
const serverHost = 'mailbox.terminal.lightning.today:443'
|
try {
|
||||||
// XXX we MUST reuse the same instance of LNC because it references a global Go object
|
if (!lnc?.isConnected) return
|
||||||
// that holds closures to the first LNC instance it's created with
|
lnc.disconnect()
|
||||||
if (window.lnc) {
|
logger?.info('disconnecting...')
|
||||||
window.lnc.credentials.credentials = {
|
// wait for lnc to disconnect
|
||||||
...window.lnc.credentials.credentials,
|
await new Promise((resolve, reject) => {
|
||||||
|
let counter = 0
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
if (lnc?.isConnected) {
|
||||||
|
if (counter++ > 100) {
|
||||||
|
logger?.error('failed to disconnect from lnc')
|
||||||
|
clearInterval(interval)
|
||||||
|
reject(new Error('failed to disconnect from lnc'))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
clearInterval(interval)
|
||||||
|
resolve()
|
||||||
|
})
|
||||||
|
}, 50)
|
||||||
|
logger?.info('disconnected')
|
||||||
|
} catch (err) {
|
||||||
|
logger?.error('failed to disconnect from lnc: ' + err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getLNC (credentials = {}, { logger } = {}) {
|
||||||
|
if (window.snLncKillerTimeout) clearTimeout(window.snLncKillerTimeout)
|
||||||
|
|
||||||
|
if (!window.snLnc) { // create new instance
|
||||||
|
const { default: LNC } = await import('@lightninglabs/lnc-web')
|
||||||
|
window.snLnc = new LNC({
|
||||||
|
credentialStore: new LncCredentialStore({
|
||||||
|
...credentials,
|
||||||
|
serverHost
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
window.addEventListener('beforeunload', () => {
|
||||||
|
// try to disconnect gracefully when the page is closed
|
||||||
|
disconnectLNC(window.snLnc, { logger })
|
||||||
|
})
|
||||||
|
} else if (JSON.stringify(window.snLncCredentials ?? {}) !== JSON.stringify(credentials)) {
|
||||||
|
console.log('LNC instance has new credentials')
|
||||||
|
// disconnect and update credentials if they've changed
|
||||||
|
await disconnectLNC(window.snLnc, { logger })
|
||||||
|
// XXX we MUST reuse the same instance of LNC because it references a global Go object
|
||||||
|
// that holds closures to the first LNC instance it's created with
|
||||||
|
window.snLnc.credentials.credentials = {
|
||||||
|
...window.snLnc.credentials.credentials,
|
||||||
...credentials,
|
...credentials,
|
||||||
serverHost
|
serverHost
|
||||||
}
|
}
|
||||||
return window.lnc
|
|
||||||
}
|
}
|
||||||
const { default: LNC } = await import('@lightninglabs/lnc-web')
|
|
||||||
window.lnc = new LNC({
|
if (!window.snLnc.isConnected) {
|
||||||
credentialStore: new LncCredentialStore({
|
logger?.info('connecting ...')
|
||||||
...credentials,
|
await window.snLnc.connect()
|
||||||
serverHost
|
logger?.info('connected')
|
||||||
|
}
|
||||||
|
|
||||||
|
window.snLncCredentials = {
|
||||||
|
...credentials
|
||||||
|
}
|
||||||
|
|
||||||
|
window.snLncKillerTimeout = setTimeout(() => {
|
||||||
|
logger?.info('disconnecting from lnc due to inactivity ...')
|
||||||
|
mutex.runExclusive(async () => {
|
||||||
|
await disconnectLNC(window.snLnc, { logger })
|
||||||
})
|
})
|
||||||
})
|
}, 4000)
|
||||||
return window.lnc
|
|
||||||
|
return window.snLnc
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateNarrowPerms (lnc) {
|
function validateNarrowPerms (lnc) {
|
||||||
|
|
|
@ -14,11 +14,10 @@ import * as webln from '@/wallets/webln'
|
||||||
import { walletLogger } from '@/api/resolvers/wallet'
|
import { walletLogger } from '@/api/resolvers/wallet'
|
||||||
import walletDefs from '@/wallets/server'
|
import walletDefs from '@/wallets/server'
|
||||||
import { parsePaymentRequest } from 'ln-service'
|
import { parsePaymentRequest } from 'ln-service'
|
||||||
import { toPositiveBigInt, toPositiveNumber } from '@/lib/validate'
|
import { toPositiveBigInt, toPositiveNumber, formatMsats, formatSats, msatsToSats } from '@/lib/format'
|
||||||
import { PAID_ACTION_TERMINAL_STATES } from '@/lib/constants'
|
import { PAID_ACTION_TERMINAL_STATES } from '@/lib/constants'
|
||||||
import { withTimeout } from '@/lib/time'
|
import { withTimeout } from '@/lib/time'
|
||||||
import { canReceive } from './common'
|
import { canReceive } from './common'
|
||||||
import { formatMsats, formatSats, msatsToSats } from '@/lib/format'
|
|
||||||
import wrapInvoice from './wrap'
|
import wrapInvoice from './wrap'
|
||||||
|
|
||||||
export default [lnd, cln, lnAddr, lnbits, nwc, phoenixd, blink, lnc, webln]
|
export default [lnd, cln, lnAddr, lnbits, nwc, phoenixd, blink, lnc, webln]
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { createHodlInvoice, parsePaymentRequest } from 'ln-service'
|
import { createHodlInvoice, parsePaymentRequest } from 'ln-service'
|
||||||
import { estimateRouteFee, getBlockHeight } from '../api/lnd'
|
import { estimateRouteFee, getBlockHeight } from '../api/lnd'
|
||||||
import { toBigInt, toPositiveBigInt, toPositiveNumber } from '@/lib/validate'
|
import { toBigInt, toPositiveBigInt, toPositiveNumber } from '@/lib/format'
|
||||||
|
|
||||||
const MIN_OUTGOING_MSATS = BigInt(900) // the minimum msats we'll allow for the outgoing invoice
|
const MIN_OUTGOING_MSATS = BigInt(900) // the minimum msats we'll allow for the outgoing invoice
|
||||||
const MAX_OUTGOING_MSATS = BigInt(900_000_000) // the maximum msats we'll allow for the outgoing invoice
|
const MAX_OUTGOING_MSATS = BigInt(900_000_000) // the maximum msats we'll allow for the outgoing invoice
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { deletePayment } from 'ln-service'
|
||||||
|
import { INVOICE_RETENTION_DAYS } from '@/lib/constants'
|
||||||
|
|
||||||
|
export async function autoDropBolt11s ({ models, lnd }) {
|
||||||
|
const retention = `${INVOICE_RETENTION_DAYS} days`
|
||||||
|
|
||||||
|
// This query will update the withdrawls and return what the hash and bol11 values were before the update
|
||||||
|
const invoices = await models.$queryRaw`
|
||||||
|
WITH to_be_updated AS (
|
||||||
|
SELECT id, hash, bolt11
|
||||||
|
FROM "Withdrawl"
|
||||||
|
WHERE "userId" IN (SELECT id FROM users WHERE "autoDropBolt11s")
|
||||||
|
AND now() > created_at + ${retention}::INTERVAL
|
||||||
|
AND hash IS NOT NULL
|
||||||
|
AND status IS NOT NULL
|
||||||
|
), updated_rows AS (
|
||||||
|
UPDATE "Withdrawl"
|
||||||
|
SET hash = NULL, bolt11 = NULL, preimage = NULL
|
||||||
|
FROM to_be_updated
|
||||||
|
WHERE "Withdrawl".id = to_be_updated.id)
|
||||||
|
SELECT * FROM to_be_updated;`
|
||||||
|
|
||||||
|
if (invoices.length > 0) {
|
||||||
|
for (const invoice of invoices) {
|
||||||
|
try {
|
||||||
|
await deletePayment({ id: invoice.hash, lnd })
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error removing invoice with hash ${invoice.hash}:`, error)
|
||||||
|
await models.withdrawl.update({
|
||||||
|
where: { id: invoice.id },
|
||||||
|
data: { hash: invoice.hash, bolt11: invoice.bolt11, preimage: invoice.preimage }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await models.$queryRaw`
|
||||||
|
UPDATE "DirectPayment"
|
||||||
|
SET hash = NULL, bolt11 = NULL, preimage = NULL
|
||||||
|
WHERE "receiverId" IN (SELECT id FROM users WHERE "autoDropBolt11s")
|
||||||
|
AND now() > created_at + ${retention}::INTERVAL
|
||||||
|
AND hash IS NOT NULL`
|
||||||
|
}
|
|
@ -3,7 +3,7 @@ import './loadenv'
|
||||||
import PgBoss from 'pg-boss'
|
import PgBoss from 'pg-boss'
|
||||||
import createPrisma from '@/lib/create-prisma'
|
import createPrisma from '@/lib/create-prisma'
|
||||||
import {
|
import {
|
||||||
autoDropBolt11s, checkInvoice, checkPendingDeposits, checkPendingWithdrawals,
|
checkInvoice, checkPendingDeposits, checkPendingWithdrawals,
|
||||||
checkWithdrawal,
|
checkWithdrawal,
|
||||||
finalizeHodlInvoice, subscribeToWallet
|
finalizeHodlInvoice, subscribeToWallet
|
||||||
} from './wallet'
|
} from './wallet'
|
||||||
|
@ -35,6 +35,8 @@ import { thisDay } from './thisDay'
|
||||||
import { isServiceEnabled } from '@/lib/sndev'
|
import { isServiceEnabled } from '@/lib/sndev'
|
||||||
import { payWeeklyPostBounty, weeklyPost } from './weeklyPosts'
|
import { payWeeklyPostBounty, weeklyPost } from './weeklyPosts'
|
||||||
import { expireBoost } from './expireBoost'
|
import { expireBoost } from './expireBoost'
|
||||||
|
import { payingActionConfirmed, payingActionFailed } from './payingAction'
|
||||||
|
import { autoDropBolt11s } from './autoDropBolt11'
|
||||||
|
|
||||||
async function work () {
|
async function work () {
|
||||||
const boss = new PgBoss(process.env.DATABASE_URL)
|
const boss = new PgBoss(process.env.DATABASE_URL)
|
||||||
|
@ -102,6 +104,9 @@ async function work () {
|
||||||
await boss.work('paidActionCanceling', jobWrapper(paidActionCanceling))
|
await boss.work('paidActionCanceling', jobWrapper(paidActionCanceling))
|
||||||
await boss.work('paidActionFailed', jobWrapper(paidActionFailed))
|
await boss.work('paidActionFailed', jobWrapper(paidActionFailed))
|
||||||
await boss.work('paidActionPaid', jobWrapper(paidActionPaid))
|
await boss.work('paidActionPaid', jobWrapper(paidActionPaid))
|
||||||
|
// payingAction jobs
|
||||||
|
await boss.work('payingActionFailed', jobWrapper(payingActionFailed))
|
||||||
|
await boss.work('payingActionConfirmed', jobWrapper(payingActionConfirmed))
|
||||||
}
|
}
|
||||||
if (isServiceEnabled('search')) {
|
if (isServiceEnabled('search')) {
|
||||||
await boss.work('indexItem', jobWrapper(indexItem))
|
await boss.work('indexItem', jobWrapper(indexItem))
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
import { getPaymentFailureStatus, hodlInvoiceCltvDetails } from '@/api/lnd'
|
import { getPaymentFailureStatus, hodlInvoiceCltvDetails, getPaymentOrNotSent } from '@/api/lnd'
|
||||||
import { paidActions } from '@/api/paidAction'
|
import { paidActions } from '@/api/paidAction'
|
||||||
import { walletLogger } from '@/api/resolvers/wallet'
|
import { walletLogger } from '@/api/resolvers/wallet'
|
||||||
import { LND_PATHFINDING_TIMEOUT_MS, PAID_ACTION_TERMINAL_STATES } from '@/lib/constants'
|
import { LND_PATHFINDING_TIMEOUT_MS, PAID_ACTION_TERMINAL_STATES } from '@/lib/constants'
|
||||||
import { formatMsats, formatSats, msatsToSats } from '@/lib/format'
|
import { formatMsats, formatSats, msatsToSats, toPositiveNumber } from '@/lib/format'
|
||||||
import { datePivot } from '@/lib/time'
|
import { datePivot } from '@/lib/time'
|
||||||
import { toPositiveNumber } from '@/lib/validate'
|
|
||||||
import { Prisma } from '@prisma/client'
|
import { Prisma } from '@prisma/client'
|
||||||
import {
|
import {
|
||||||
cancelHodlInvoice,
|
cancelHodlInvoice,
|
||||||
getInvoice, getPayment, parsePaymentRequest,
|
getInvoice, parsePaymentRequest,
|
||||||
payViaPaymentRequest, settleHodlInvoice
|
payViaPaymentRequest, settleHodlInvoice
|
||||||
} from 'ln-service'
|
} from 'ln-service'
|
||||||
import { MIN_SETTLEMENT_CLTV_DELTA } from '@/wallets/wrap'
|
import { MIN_SETTLEMENT_CLTV_DELTA } from '@/wallets/wrap'
|
||||||
|
@ -114,7 +113,7 @@ async function transitionInvoice (jobName,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function performPessimisticAction ({ lndInvoice, dbInvoice, tx, models, lnd, boss }) {
|
async function performPessimisticAction ({ lndInvoice, dbInvoice, tx, lnd, boss }) {
|
||||||
const args = { ...dbInvoice.actionArgs, invoiceId: dbInvoice.id }
|
const args = { ...dbInvoice.actionArgs, invoiceId: dbInvoice.id }
|
||||||
const context = {
|
const context = {
|
||||||
tx,
|
tx,
|
||||||
|
@ -278,7 +277,7 @@ export async function paidActionForwarding ({ data: { invoiceId, ...args }, mode
|
||||||
|
|
||||||
// this finalizes the forward by settling the incoming invoice after the outgoing payment is confirmed
|
// this finalizes the forward by settling the incoming invoice after the outgoing payment is confirmed
|
||||||
export async function paidActionForwarded ({ data: { invoiceId, withdrawal, ...args }, models, lnd, boss }) {
|
export async function paidActionForwarded ({ data: { invoiceId, withdrawal, ...args }, models, lnd, boss }) {
|
||||||
return await transitionInvoice('paidActionForwarded', {
|
const transitionedInvoice = await transitionInvoice('paidActionForwarded', {
|
||||||
invoiceId,
|
invoiceId,
|
||||||
fromState: 'FORWARDING',
|
fromState: 'FORWARDING',
|
||||||
toState: 'FORWARDED',
|
toState: 'FORWARDED',
|
||||||
|
@ -287,8 +286,9 @@ export async function paidActionForwarded ({ data: { invoiceId, withdrawal, ...a
|
||||||
throw new Error('invoice is not held')
|
throw new Error('invoice is not held')
|
||||||
}
|
}
|
||||||
|
|
||||||
const { bolt11, hash, msatsPaying } = dbInvoice.invoiceForward.withdrawl
|
const { hash, msatsPaying, createdAt } = dbInvoice.invoiceForward.withdrawl
|
||||||
const { payment, is_confirmed: isConfirmed } = withdrawal ?? await getPayment({ id: hash, lnd })
|
const { payment, is_confirmed: isConfirmed } = withdrawal ??
|
||||||
|
await getPaymentOrNotSent({ id: hash, lnd, createdAt })
|
||||||
if (!isConfirmed) {
|
if (!isConfirmed) {
|
||||||
throw new Error('payment is not confirmed')
|
throw new Error('payment is not confirmed')
|
||||||
}
|
}
|
||||||
|
@ -296,20 +296,6 @@ export async function paidActionForwarded ({ data: { invoiceId, withdrawal, ...a
|
||||||
// settle the invoice, allowing us to transition to PAID
|
// settle the invoice, allowing us to transition to PAID
|
||||||
await settleHodlInvoice({ secret: payment.secret, lnd })
|
await settleHodlInvoice({ secret: payment.secret, lnd })
|
||||||
|
|
||||||
// the amount we paid includes the fee so we need to subtract it to get the amount received
|
|
||||||
const received = Number(payment.mtokens) - Number(payment.fee_mtokens)
|
|
||||||
|
|
||||||
const logger = walletLogger({ wallet: dbInvoice.invoiceForward.wallet, models })
|
|
||||||
logger.ok(
|
|
||||||
`↙ payment received: ${formatSats(msatsToSats(received))}`,
|
|
||||||
{
|
|
||||||
bolt11,
|
|
||||||
preimage: payment.secret
|
|
||||||
// we could show the outgoing fee that we paid from the incoming amount to the receiver
|
|
||||||
// but we don't since it might look like the receiver paid the fee but that's not the case.
|
|
||||||
// fee: formatMsats(Number(payment.fee_mtokens))
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
preimage: payment.secret,
|
preimage: payment.secret,
|
||||||
invoiceForward: {
|
invoiceForward: {
|
||||||
|
@ -328,11 +314,31 @@ export async function paidActionForwarded ({ data: { invoiceId, withdrawal, ...a
|
||||||
},
|
},
|
||||||
...args
|
...args
|
||||||
}, { models, lnd, boss })
|
}, { models, lnd, boss })
|
||||||
|
|
||||||
|
if (transitionedInvoice) {
|
||||||
|
const { bolt11, msatsPaid, msatsFeePaid } = transitionedInvoice.invoiceForward.withdrawl
|
||||||
|
// the amount we paid includes the fee so we need to subtract it to get the amount received
|
||||||
|
const received = Number(msatsPaid) - Number(msatsFeePaid)
|
||||||
|
|
||||||
|
const logger = walletLogger({ wallet: transitionedInvoice.invoiceForward.wallet, models })
|
||||||
|
logger.ok(
|
||||||
|
`↙ payment received: ${formatSats(msatsToSats(received))}`,
|
||||||
|
{
|
||||||
|
bolt11,
|
||||||
|
preimage: transitionedInvoice.preimage
|
||||||
|
// we could show the outgoing fee that we paid from the incoming amount to the receiver
|
||||||
|
// but we don't since it might look like the receiver paid the fee but that's not the case.
|
||||||
|
// fee: formatMsats(msatsFeePaid)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return transitionedInvoice
|
||||||
}
|
}
|
||||||
|
|
||||||
// when the pending forward fails, we need to cancel the incoming invoice
|
// when the pending forward fails, we need to cancel the incoming invoice
|
||||||
export async function paidActionFailedForward ({ data: { invoiceId, withdrawal: pWithdrawal, ...args }, models, lnd, boss }) {
|
export async function paidActionFailedForward ({ data: { invoiceId, withdrawal: pWithdrawal, ...args }, models, lnd, boss }) {
|
||||||
return await transitionInvoice('paidActionFailedForward', {
|
let message
|
||||||
|
const transitionedInvoice = await transitionInvoice('paidActionFailedForward', {
|
||||||
invoiceId,
|
invoiceId,
|
||||||
fromState: 'FORWARDING',
|
fromState: 'FORWARDING',
|
||||||
toState: 'FAILED_FORWARD',
|
toState: 'FAILED_FORWARD',
|
||||||
|
@ -341,21 +347,10 @@ export async function paidActionFailedForward ({ data: { invoiceId, withdrawal:
|
||||||
throw new Error('invoice is not held')
|
throw new Error('invoice is not held')
|
||||||
}
|
}
|
||||||
|
|
||||||
let withdrawal
|
const { hash, createdAt } = dbInvoice.invoiceForward.withdrawl
|
||||||
let notSent = false
|
const withdrawal = pWithdrawal ?? await getPaymentOrNotSent({ id: hash, lnd, createdAt })
|
||||||
try {
|
|
||||||
withdrawal = pWithdrawal ?? await getPayment({ id: dbInvoice.invoiceForward.withdrawl.hash, lnd })
|
|
||||||
} catch (err) {
|
|
||||||
if (err[1] === 'SentPaymentNotFound' &&
|
|
||||||
dbInvoice.invoiceForward.withdrawl.createdAt < datePivot(new Date(), { milliseconds: -LND_PATHFINDING_TIMEOUT_MS * 2 })) {
|
|
||||||
// if the payment is older than 2x timeout, but not found in LND, we can assume it errored before lnd stored it
|
|
||||||
notSent = true
|
|
||||||
} else {
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(withdrawal?.is_failed || notSent)) {
|
if (!(withdrawal?.is_failed || withdrawal?.notSent)) {
|
||||||
throw new Error('payment has not failed')
|
throw new Error('payment has not failed')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -363,14 +358,8 @@ export async function paidActionFailedForward ({ data: { invoiceId, withdrawal:
|
||||||
// which once it does succeed will ensure we will try to cancel the held invoice until it actually cancels
|
// which once it does succeed will ensure we will try to cancel the held invoice until it actually cancels
|
||||||
await boss.send('finalizeHodlInvoice', { hash: dbInvoice.hash }, FINALIZE_OPTIONS)
|
await boss.send('finalizeHodlInvoice', { hash: dbInvoice.hash }, FINALIZE_OPTIONS)
|
||||||
|
|
||||||
const { status, message } = getPaymentFailureStatus(withdrawal)
|
const { status, message: failureMessage } = getPaymentFailureStatus(withdrawal)
|
||||||
const { bolt11, msatsFeePaying } = dbInvoice.invoiceForward.withdrawl
|
message = failureMessage
|
||||||
const logger = walletLogger({ wallet: dbInvoice.invoiceForward.wallet, models })
|
|
||||||
logger.warn(
|
|
||||||
`incoming payment failed: ${message}`, {
|
|
||||||
bolt11,
|
|
||||||
max_fee: formatMsats(Number(msatsFeePaying))
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
invoiceForward: {
|
invoiceForward: {
|
||||||
|
@ -386,6 +375,18 @@ export async function paidActionFailedForward ({ data: { invoiceId, withdrawal:
|
||||||
},
|
},
|
||||||
...args
|
...args
|
||||||
}, { models, lnd, boss })
|
}, { models, lnd, boss })
|
||||||
|
|
||||||
|
if (transitionedInvoice) {
|
||||||
|
const { bolt11, msatsFeePaying } = transitionedInvoice.invoiceForward.withdrawl
|
||||||
|
const logger = walletLogger({ wallet: transitionedInvoice.invoiceForward.wallet, models })
|
||||||
|
logger.warn(
|
||||||
|
`incoming payment failed: ${message}`, {
|
||||||
|
bolt11,
|
||||||
|
max_fee: formatMsats(msatsFeePaying)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return transitionedInvoice
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function paidActionHeld ({ data: { invoiceId, ...args }, models, lnd, boss }) {
|
export async function paidActionHeld ({ data: { invoiceId, ...args }, models, lnd, boss }) {
|
||||||
|
@ -427,7 +428,7 @@ export async function paidActionHeld ({ data: { invoiceId, ...args }, models, ln
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function paidActionCanceling ({ data: { invoiceId, ...args }, models, lnd, boss }) {
|
export async function paidActionCanceling ({ data: { invoiceId, ...args }, models, lnd, boss }) {
|
||||||
return await transitionInvoice('paidActionCanceling', {
|
const transitionedInvoice = await transitionInvoice('paidActionCanceling', {
|
||||||
invoiceId,
|
invoiceId,
|
||||||
fromState: ['HELD', 'PENDING', 'PENDING_HELD', 'FAILED_FORWARD'],
|
fromState: ['HELD', 'PENDING', 'PENDING_HELD', 'FAILED_FORWARD'],
|
||||||
toState: 'CANCELING',
|
toState: 'CANCELING',
|
||||||
|
@ -440,6 +441,17 @@ export async function paidActionCanceling ({ data: { invoiceId, ...args }, model
|
||||||
},
|
},
|
||||||
...args
|
...args
|
||||||
}, { models, lnd, boss })
|
}, { models, lnd, boss })
|
||||||
|
|
||||||
|
if (transitionedInvoice) {
|
||||||
|
if (transitionedInvoice.invoiceForward) {
|
||||||
|
const { wallet, bolt11 } = transitionedInvoice.invoiceForward
|
||||||
|
const logger = walletLogger({ wallet, models })
|
||||||
|
const decoded = await parsePaymentRequest({ request: bolt11 })
|
||||||
|
logger.info(`invoice for ${formatSats(msatsToSats(decoded.mtokens))} canceled by payer`, { bolt11 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return transitionedInvoice
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function paidActionFailed ({ data: { invoiceId, ...args }, models, lnd, boss }) {
|
export async function paidActionFailed ({ data: { invoiceId, ...args }, models, lnd, boss }) {
|
||||||
|
|
|
@ -0,0 +1,176 @@
|
||||||
|
import { getPaymentFailureStatus, getPaymentOrNotSent } from '@/api/lnd'
|
||||||
|
import { walletLogger } from '@/api/resolvers/wallet'
|
||||||
|
import { formatMsats, formatSats, msatsToSats, toPositiveBigInt } from '@/lib/format'
|
||||||
|
import { datePivot } from '@/lib/time'
|
||||||
|
import { notifyWithdrawal } from '@/lib/webPush'
|
||||||
|
import { Prisma } from '@prisma/client'
|
||||||
|
|
||||||
|
async function transitionWithdrawal (jobName,
|
||||||
|
{ withdrawalId, toStatus, transition, withdrawal, onUnexpectedError },
|
||||||
|
{ models, lnd, boss }
|
||||||
|
) {
|
||||||
|
console.group(`${jobName}: transitioning withdrawal ${withdrawalId} from null to ${toStatus}`)
|
||||||
|
|
||||||
|
let dbWithdrawal
|
||||||
|
try {
|
||||||
|
const currentDbWithdrawal = await models.withdrawl.findUnique({ where: { id: withdrawalId } })
|
||||||
|
console.log('withdrawal has status', currentDbWithdrawal.status)
|
||||||
|
|
||||||
|
if (currentDbWithdrawal.status) {
|
||||||
|
console.log('withdrawal is already has a terminal status, skipping transition')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { hash, createdAt } = currentDbWithdrawal
|
||||||
|
const lndWithdrawal = withdrawal ?? await getPaymentOrNotSent({ id: hash, lnd, createdAt })
|
||||||
|
|
||||||
|
const transitionedWithdrawal = await models.$transaction(async tx => {
|
||||||
|
// grab optimistic concurrency lock and the withdrawal
|
||||||
|
dbWithdrawal = await tx.withdrawl.update({
|
||||||
|
include: {
|
||||||
|
wallet: true
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
id: withdrawalId,
|
||||||
|
status: null
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
status: toStatus
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// our own optimistic concurrency check
|
||||||
|
if (!dbWithdrawal) {
|
||||||
|
console.log('record not found in our own concurrency check, assuming concurrent worker transitioned it')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await transition({ lndWithdrawal, dbWithdrawal, tx })
|
||||||
|
if (data) {
|
||||||
|
return await tx.withdrawl.update({
|
||||||
|
include: {
|
||||||
|
wallet: true
|
||||||
|
},
|
||||||
|
where: { id: dbWithdrawal.id },
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return dbWithdrawal
|
||||||
|
}, {
|
||||||
|
isolationLevel: Prisma.TransactionIsolationLevel.ReadCommitted,
|
||||||
|
// we only need to do this because we settleHodlInvoice inside the transaction
|
||||||
|
// ... and it's prone to timing out
|
||||||
|
timeout: 60000
|
||||||
|
})
|
||||||
|
|
||||||
|
if (transitionedWithdrawal) {
|
||||||
|
console.log('transition succeeded')
|
||||||
|
return transitionedWithdrawal
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
||||||
|
if (e.code === 'P2025') {
|
||||||
|
console.log('record not found, assuming concurrent worker transitioned it')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (e.code === 'P2034') {
|
||||||
|
console.log('write conflict, assuming concurrent worker is transitioning it')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error('unexpected error', e)
|
||||||
|
onUnexpectedError?.({ error: e, dbWithdrawal, models, boss })
|
||||||
|
await boss.send(
|
||||||
|
jobName,
|
||||||
|
{ withdrawalId },
|
||||||
|
{ startAfter: datePivot(new Date(), { seconds: 30 }), priority: 1000 })
|
||||||
|
} finally {
|
||||||
|
console.groupEnd()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function payingActionConfirmed ({ data: args, models, lnd, boss }) {
|
||||||
|
const transitionedWithdrawal = await transitionWithdrawal('payingActionConfirmed', {
|
||||||
|
toStatus: 'CONFIRMED',
|
||||||
|
...args,
|
||||||
|
transition: async ({ dbWithdrawal, lndWithdrawal, tx }) => {
|
||||||
|
if (!lndWithdrawal?.is_confirmed) {
|
||||||
|
throw new Error('withdrawal is not confirmed')
|
||||||
|
}
|
||||||
|
|
||||||
|
const msatsFeePaid = toPositiveBigInt(lndWithdrawal.payment.fee_mtokens)
|
||||||
|
const msatsPaid = toPositiveBigInt(lndWithdrawal.payment.mtokens) - msatsFeePaid
|
||||||
|
|
||||||
|
console.log(`withdrawal confirmed paying ${msatsToSats(msatsPaid)} sats with ${msatsToSats(msatsFeePaid)} fee`)
|
||||||
|
|
||||||
|
await tx.user.update({
|
||||||
|
where: { id: dbWithdrawal.userId },
|
||||||
|
data: { msats: { increment: dbWithdrawal.msatsFeePaying - msatsFeePaid } }
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(`user refunded ${msatsToSats(dbWithdrawal.msatsFeePaying - msatsFeePaid)} sats`)
|
||||||
|
|
||||||
|
return {
|
||||||
|
msatsFeePaid,
|
||||||
|
msatsPaid,
|
||||||
|
preimage: lndWithdrawal.payment.secret
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, { models, lnd, boss })
|
||||||
|
|
||||||
|
if (transitionedWithdrawal) {
|
||||||
|
await notifyWithdrawal(transitionedWithdrawal)
|
||||||
|
|
||||||
|
const logger = walletLogger({ models, wallet: transitionedWithdrawal.wallet })
|
||||||
|
logger?.ok(
|
||||||
|
`↙ payment received: ${formatSats(msatsToSats(transitionedWithdrawal.msatsPaid))}`,
|
||||||
|
{
|
||||||
|
bolt11: transitionedWithdrawal.bolt11,
|
||||||
|
preimage: transitionedWithdrawal.preimage,
|
||||||
|
fee: formatMsats(transitionedWithdrawal.msatsFeePaid)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function payingActionFailed ({ data: args, models, lnd, boss }) {
|
||||||
|
let message
|
||||||
|
const transitionedWithdrawal = await transitionWithdrawal('payingActionFailed', {
|
||||||
|
toStatus: 'UNKNOWN_FAILURE',
|
||||||
|
...args,
|
||||||
|
transition: async ({ dbWithdrawal, lndWithdrawal, tx }) => {
|
||||||
|
if (!lndWithdrawal?.is_failed) {
|
||||||
|
throw new Error('withdrawal is not failed')
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`withdrawal failed paying ${msatsToSats(dbWithdrawal.msatsPaying)} sats with ${msatsToSats(dbWithdrawal.msatsFeePaying)} fee`)
|
||||||
|
|
||||||
|
await tx.user.update({
|
||||||
|
where: { id: dbWithdrawal.userId },
|
||||||
|
data: { msats: { increment: dbWithdrawal.msatsFeePaying + dbWithdrawal.msatsPaying } }
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(`user refunded ${msatsToSats(dbWithdrawal.msatsFeePaying + dbWithdrawal.msatsPaying)} sats`)
|
||||||
|
|
||||||
|
// update to particular status
|
||||||
|
const { status, message: failureMessage } = getPaymentFailureStatus(lndWithdrawal)
|
||||||
|
message = failureMessage
|
||||||
|
|
||||||
|
console.log('withdrawal failed with status', status)
|
||||||
|
return {
|
||||||
|
status
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, { models, lnd, boss })
|
||||||
|
|
||||||
|
if (transitionedWithdrawal) {
|
||||||
|
const logger = walletLogger({ models, wallet: transitionedWithdrawal.wallet })
|
||||||
|
logger?.error(
|
||||||
|
`incoming payment failed: ${message}`,
|
||||||
|
{
|
||||||
|
bolt11: transitionedWithdrawal.bolt11,
|
||||||
|
max_fee: formatMsats(transitionedWithdrawal.msatsFeePaying)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
160
worker/wallet.js
160
worker/wallet.js
|
@ -1,11 +1,9 @@
|
||||||
import serialize from '@/api/resolvers/serial'
|
|
||||||
import {
|
import {
|
||||||
getInvoice, getPayment, cancelHodlInvoice, deletePayment,
|
getInvoice,
|
||||||
subscribeToInvoices, subscribeToPayments, subscribeToInvoice
|
subscribeToInvoices, subscribeToPayments, subscribeToInvoice
|
||||||
} from 'ln-service'
|
} from 'ln-service'
|
||||||
import { notifyDeposit, notifyWithdrawal } from '@/lib/webPush'
|
import { getPaymentOrNotSent } from '@/api/lnd'
|
||||||
import { INVOICE_RETENTION_DAYS, LND_PATHFINDING_TIMEOUT_MS } from '@/lib/constants'
|
import { sleep } from '@/lib/time'
|
||||||
import { datePivot, sleep } from '@/lib/time'
|
|
||||||
import retry from 'async-retry'
|
import retry from 'async-retry'
|
||||||
import {
|
import {
|
||||||
paidActionPaid, paidActionForwarded,
|
paidActionPaid, paidActionForwarded,
|
||||||
|
@ -13,9 +11,7 @@ import {
|
||||||
paidActionForwarding,
|
paidActionForwarding,
|
||||||
paidActionCanceling
|
paidActionCanceling
|
||||||
} from './paidAction'
|
} from './paidAction'
|
||||||
import { getPaymentFailureStatus } from '@/api/lnd/index.js'
|
import { payingActionConfirmed, payingActionFailed } from './payingAction'
|
||||||
import { walletLogger } from '@/api/resolvers/wallet.js'
|
|
||||||
import { formatMsats, formatSats, msatsToSats } from '@/lib/format.js'
|
|
||||||
|
|
||||||
export async function subscribeToWallet (args) {
|
export async function subscribeToWallet (args) {
|
||||||
await subscribeToDeposits(args)
|
await subscribeToDeposits(args)
|
||||||
|
@ -143,19 +139,6 @@ export async function checkInvoice ({ data: { hash, invoice }, boss, models, lnd
|
||||||
if (dbInv.actionType) {
|
if (dbInv.actionType) {
|
||||||
return await paidActionPaid({ data: { invoiceId: dbInv.id, invoice: inv }, models, lnd, boss })
|
return await paidActionPaid({ data: { invoiceId: dbInv.id, invoice: inv }, models, lnd, boss })
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 } })
|
|
||||||
], { models })
|
|
||||||
|
|
||||||
if (code === 0) {
|
|
||||||
notifyDeposit(dbInv.userId, { comment: dbInv.comment, ...inv })
|
|
||||||
}
|
|
||||||
|
|
||||||
return await boss.send('nip57', { hash })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inv.is_held) {
|
if (inv.is_held) {
|
||||||
|
@ -175,18 +158,6 @@ export async function checkInvoice ({ data: { hash, invoice }, boss, models, lnd
|
||||||
if (dbInv.actionType) {
|
if (dbInv.actionType) {
|
||||||
return await paidActionFailed({ data: { invoiceId: dbInv.id, invoice: inv }, models, lnd, boss })
|
return await paidActionFailed({ data: { invoiceId: dbInv.id, invoice: inv }, models, lnd, boss })
|
||||||
}
|
}
|
||||||
|
|
||||||
return await serialize(
|
|
||||||
models.invoice.update({
|
|
||||||
where: {
|
|
||||||
hash: inv.id
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
cancelled: true,
|
|
||||||
cancelledAt: new Date()
|
|
||||||
}
|
|
||||||
}), { models }
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -235,13 +206,12 @@ export async function checkWithdrawal ({ data: { hash, withdrawal, invoice }, bo
|
||||||
hash,
|
hash,
|
||||||
OR: [
|
OR: [
|
||||||
{ status: null },
|
{ status: null },
|
||||||
{ invoiceForward: { some: { } } }
|
{ invoiceForward: { isNot: null } }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
wallet: true,
|
wallet: true,
|
||||||
invoiceForward: {
|
invoiceForward: {
|
||||||
orderBy: { createdAt: 'desc' },
|
|
||||||
include: {
|
include: {
|
||||||
invoice: true
|
invoice: true
|
||||||
}
|
}
|
||||||
|
@ -252,103 +222,20 @@ export async function checkWithdrawal ({ data: { hash, withdrawal, invoice }, bo
|
||||||
// nothing to do if the withdrawl is already recorded and it isn't an invoiceForward
|
// nothing to do if the withdrawl is already recorded and it isn't an invoiceForward
|
||||||
if (!dbWdrwl) return
|
if (!dbWdrwl) return
|
||||||
|
|
||||||
let wdrwl
|
const wdrwl = withdrawal ?? await getPaymentOrNotSent({ id: hash, lnd, createdAt: dbWdrwl.createdAt })
|
||||||
let notSent = false
|
|
||||||
try {
|
|
||||||
wdrwl = withdrawal ?? await getPayment({ id: hash, lnd })
|
|
||||||
} catch (err) {
|
|
||||||
if (err[1] === 'SentPaymentNotFound' &&
|
|
||||||
dbWdrwl.createdAt < datePivot(new Date(), { milliseconds: -LND_PATHFINDING_TIMEOUT_MS * 2 })) {
|
|
||||||
// if the payment is older than 2x timeout, but not found in LND, we can assume it errored before lnd stored it
|
|
||||||
notSent = true
|
|
||||||
} else {
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const logger = walletLogger({ models, wallet: dbWdrwl.wallet })
|
|
||||||
|
|
||||||
if (wdrwl?.is_confirmed) {
|
if (wdrwl?.is_confirmed) {
|
||||||
if (dbWdrwl.invoiceForward.length > 0) {
|
if (dbWdrwl.invoiceForward) {
|
||||||
return await paidActionForwarded({ data: { invoiceId: dbWdrwl.invoiceForward[0].invoice.id, withdrawal: wdrwl, invoice }, models, lnd, boss })
|
return await paidActionForwarded({ data: { invoiceId: dbWdrwl.invoiceForward.invoice.id, withdrawal: wdrwl, invoice }, models, lnd, boss })
|
||||||
}
|
}
|
||||||
|
|
||||||
const fee = Number(wdrwl.payment.fee_mtokens)
|
return await payingActionConfirmed({ data: { withdrawalId: dbWdrwl.id, withdrawal: wdrwl }, models, lnd, boss })
|
||||||
const paid = Number(wdrwl.payment.mtokens) - fee
|
} else if (wdrwl?.is_failed || wdrwl?.notSent) {
|
||||||
const [[{ confirm_withdrawl: code }]] = await serialize([
|
if (dbWdrwl.invoiceForward) {
|
||||||
models.$queryRaw`SELECT confirm_withdrawl(${dbWdrwl.id}::INTEGER, ${paid}, ${fee})`,
|
return await paidActionFailedForward({ data: { invoiceId: dbWdrwl.invoiceForward.invoice.id, withdrawal: wdrwl, invoice }, models, lnd, boss })
|
||||||
models.withdrawl.update({
|
|
||||||
where: { id: dbWdrwl.id },
|
|
||||||
data: {
|
|
||||||
preimage: wdrwl.payment.secret
|
|
||||||
}
|
|
||||||
})
|
|
||||||
], { models })
|
|
||||||
if (code === 0) {
|
|
||||||
notifyWithdrawal(dbWdrwl.userId, wdrwl)
|
|
||||||
|
|
||||||
const { request: bolt11, secret: preimage } = wdrwl.payment
|
|
||||||
|
|
||||||
logger?.ok(
|
|
||||||
`↙ payment received: ${formatSats(msatsToSats(paid))}`,
|
|
||||||
{
|
|
||||||
bolt11,
|
|
||||||
preimage,
|
|
||||||
fee: formatMsats(fee)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else if (wdrwl?.is_failed || notSent) {
|
|
||||||
if (dbWdrwl.invoiceForward.length > 0) {
|
|
||||||
return await paidActionFailedForward({ data: { invoiceId: dbWdrwl.invoiceForward[0].invoice.id, withdrawal: wdrwl, invoice }, models, lnd, boss })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { message, status } = getPaymentFailureStatus(wdrwl)
|
return await payingActionFailed({ data: { withdrawalId: dbWdrwl.id, withdrawal: wdrwl }, models, lnd, boss })
|
||||||
await serialize(
|
|
||||||
models.$queryRaw`
|
|
||||||
SELECT reverse_withdrawl(${dbWdrwl.id}::INTEGER, ${status}::"WithdrawlStatus")`,
|
|
||||||
{ models }
|
|
||||||
)
|
|
||||||
|
|
||||||
logger?.error(
|
|
||||||
`incoming payment failed: ${message}`,
|
|
||||||
{
|
|
||||||
bolt11: wdrwl.payment.request,
|
|
||||||
max_fee: formatMsats(dbWdrwl.msatsFeePaying)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function autoDropBolt11s ({ models, lnd }) {
|
|
||||||
const retention = `${INVOICE_RETENTION_DAYS} days`
|
|
||||||
|
|
||||||
// This query will update the withdrawls and return what the hash and bol11 values were before the update
|
|
||||||
const invoices = await models.$queryRaw`
|
|
||||||
WITH to_be_updated AS (
|
|
||||||
SELECT id, hash, bolt11
|
|
||||||
FROM "Withdrawl"
|
|
||||||
WHERE "userId" IN (SELECT id FROM users WHERE "autoDropBolt11s")
|
|
||||||
AND now() > created_at + ${retention}::INTERVAL
|
|
||||||
AND hash IS NOT NULL
|
|
||||||
AND status IS NOT NULL
|
|
||||||
), updated_rows AS (
|
|
||||||
UPDATE "Withdrawl"
|
|
||||||
SET hash = NULL, bolt11 = NULL, preimage = NULL
|
|
||||||
FROM to_be_updated
|
|
||||||
WHERE "Withdrawl".id = to_be_updated.id)
|
|
||||||
SELECT * FROM to_be_updated;`
|
|
||||||
|
|
||||||
if (invoices.length > 0) {
|
|
||||||
for (const invoice of invoices) {
|
|
||||||
try {
|
|
||||||
await deletePayment({ id: invoice.hash, lnd })
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error removing invoice with hash ${invoice.hash}:`, error)
|
|
||||||
await models.withdrawl.update({
|
|
||||||
where: { id: invoice.id },
|
|
||||||
data: { hash: invoice.hash, bolt11: invoice.bolt11, preimage: invoice.preimage }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -360,33 +247,16 @@ export async function finalizeHodlInvoice ({ data: { hash }, models, lnd, boss,
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const dbInv = await models.invoice.findUnique({
|
const dbInv = await models.invoice.findUnique({ where: { hash } })
|
||||||
where: { hash },
|
|
||||||
include: {
|
|
||||||
invoiceForward: {
|
|
||||||
include: {
|
|
||||||
withdrawl: true,
|
|
||||||
wallet: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (!dbInv) {
|
if (!dbInv) {
|
||||||
console.log('invoice not found in database', hash)
|
console.log('invoice not found in database', hash)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// if this is an actionType we need to cancel conditionally
|
await paidActionCanceling({ data: { invoiceId: dbInv.id, invoice: inv }, models, lnd, boss })
|
||||||
if (dbInv.actionType) {
|
|
||||||
await paidActionCanceling({ data: { invoiceId: dbInv.id, invoice: inv }, models, lnd, boss })
|
|
||||||
} else {
|
|
||||||
await cancelHodlInvoice({ id: hash, lnd })
|
|
||||||
}
|
|
||||||
|
|
||||||
// sync LND invoice status with invoice status in database
|
// sync LND invoice status with invoice status in database
|
||||||
await checkInvoice({ data: { hash }, models, lnd, boss })
|
await checkInvoice({ data: { hash }, models, lnd, boss })
|
||||||
|
|
||||||
return dbInv
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkPendingDeposits (args) {
|
export async function checkPendingDeposits (args) {
|
||||||
|
|
Loading…
Reference in New Issue