Merge branch 'master' into admin-swaps

This commit is contained in:
boufni95 2026-01-27 16:10:28 +00:00
commit e71f631993
12 changed files with 169 additions and 46 deletions

View file

@ -14,9 +14,55 @@ docker run -d \
--network host \ --network host \
-p 1776:1776 \ -p 1776:1776 \
-p 1777:1777 \ -p 1777:1777 \
-e DATA_DIR=/app/data \
-v /path/to/local/data:/app/data \ -v /path/to/local/data:/app/data \
-v $HOME/.lnd:/root/.lnd \ -v $HOME/.lnd:/root/.lnd \
ghcr.io/shocknet/lightning-pub:latest ghcr.io/shocknet/lightning-pub:latest
``` ```
Network host is used so the service can reach a local LND via localhost. LND is assumed to be under the users home folder, update these resources as needed. Network host is used so the service can reach a local LND via localhost. LND is assumed to be under the users home folder, update these resources as needed.
## Getting the Connection String
After starting the container, you can get the connection string in several ways:
### Option 1: From the mounted data directory
If `DATA_DIR=/app/data` is set and the volume is mounted correctly, the connection string will be in:
```ssh
cat /path/to/local/data/admin.connect
```
The connection string format is: `nprofile1...:token`
### Option 2: Via API endpoint
Access the wizard endpoint to get connection info:
```ssh
curl http://localhost:1777/wizard/admin_connect_info
```
The response will include the `nprofile` and `admin_token`. Combine them as `nprofile:admin_token` to form the connection string.
### Option 3: From inside the container
If the files aren't in the mounted volume, check inside the container:
```ssh
docker exec lightning-pub cat /app/data/admin.connect
```
Or if `DATA_DIR` wasn't set, check the working directory:
```ssh
docker exec lightning-pub cat /app/admin.connect
```
### Troubleshooting
If `/data` folder is empty:
- Ensure `DATA_DIR=/app/data` environment variable is set in the docker run command
- Check container logs: `docker logs lightning-pub` to see where files are being written
- The logs will show the configured data directory path at startup

View file

@ -14,11 +14,18 @@ export type ClientParams = {
checkResult?: true checkResult?: true
} }
export default (params: ClientParams) => ({ export default (params: ClientParams) => ({
EnrollServicePub: async (request: Types.ServiceNpub): Promise<ResultError | ({ status: 'OK' })> => { EnrollServicePub: async (query: Types.EnrollServicePub_Query): Promise<ResultError | ({ status: 'OK' })> => {
let finalRoute = '/api/admin/service/enroll' let finalRoute = '/api/admin/service/enroll'
const initialQuery = query as Record<string, string | string[]>
const finalQuery: Record<string, string> = {}
for (const key in initialQuery)
if (Array.isArray(initialQuery[key]))
finalQuery[key] = initialQuery[key].join(',')
const q = (new URLSearchParams(finalQuery)).toString()
finalRoute = finalRoute + (q === '' ? '' : '?' + q)
const auth = await params.retrieveAdminAuth() const auth = await params.retrieveAdminAuth()
if (auth === null) throw new Error('retrieveAdminAuth() returned null') if (auth === null) throw new Error('retrieveAdminAuth() returned null')
const { data } = await axios.post(params.baseUrl + finalRoute, request, { headers: { 'authorization': auth } }) const { data } = await axios.get(params.baseUrl + finalRoute, { headers: { 'authorization': auth } })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') { if (data.status === 'OK') {
return data return data

View file

@ -25,7 +25,10 @@ export type NostrAppMethodInputs = SendNotification_Input
export type NostrAppMethodOutputs = SendNotification_Output export type NostrAppMethodOutputs = SendNotification_Output
export type AuthContext = AdminContext | GuestContext | NostrAppContext export type AuthContext = AdminContext | GuestContext | NostrAppContext
export type EnrollServicePub_Input = { rpcName: 'EnrollServicePub', req: ServiceNpub } export type EnrollServicePub_Query = {
pubkey_hex?: string[] | string
}
export type EnrollServicePub_Input = { rpcName: 'EnrollServicePub', query: EnrollServicePub_Query }
export type EnrollServicePub_Output = ResultError | { status: 'OK' } export type EnrollServicePub_Output = ResultError | { status: 'OK' }
export type Health_Input = { rpcName: 'Health' } export type Health_Input = { rpcName: 'Health' }
@ -59,19 +62,27 @@ export const EmptyValidate = (o?: Empty, opts: EmptyOptions = {}, path: string =
} }
export type Notification = { export type Notification = {
body?: string
data: string data: string
recipient_registration_tokens: string[] recipient_registration_tokens: string[]
title?: string
} }
export const NotificationOptionalFields: [] = [] export type NotificationOptionalField = 'body' | 'title'
export const NotificationOptionalFields: NotificationOptionalField[] = ['body', 'title']
export type NotificationOptions = OptionsBaseMessage & { export type NotificationOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: [] checkOptionalsAreSet?: NotificationOptionalField[]
body_CustomCheck?: (v?: string) => boolean
data_CustomCheck?: (v: string) => boolean data_CustomCheck?: (v: string) => boolean
recipient_registration_tokens_CustomCheck?: (v: string[]) => boolean recipient_registration_tokens_CustomCheck?: (v: string[]) => boolean
title_CustomCheck?: (v?: string) => boolean
} }
export const NotificationValidate = (o?: Notification, opts: NotificationOptions = {}, path: string = 'Notification::root.'): Error | null => { export const NotificationValidate = (o?: Notification, opts: NotificationOptions = {}, path: string = 'Notification::root.'): Error | null => {
if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message')
if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null') if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null')
if ((o.body || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('body')) && typeof o.body !== 'string') return new Error(`${path}.body: is not a string`)
if (opts.body_CustomCheck && !opts.body_CustomCheck(o.body)) return new Error(`${path}.body: custom check failed`)
if (typeof o.data !== 'string') return new Error(`${path}.data: is not a string`) if (typeof o.data !== 'string') return new Error(`${path}.data: is not a string`)
if (opts.data_CustomCheck && !opts.data_CustomCheck(o.data)) return new Error(`${path}.data: custom check failed`) if (opts.data_CustomCheck && !opts.data_CustomCheck(o.data)) return new Error(`${path}.data: custom check failed`)
@ -81,6 +92,9 @@ export const NotificationValidate = (o?: Notification, opts: NotificationOptions
} }
if (opts.recipient_registration_tokens_CustomCheck && !opts.recipient_registration_tokens_CustomCheck(o.recipient_registration_tokens)) return new Error(`${path}.recipient_registration_tokens: custom check failed`) if (opts.recipient_registration_tokens_CustomCheck && !opts.recipient_registration_tokens_CustomCheck(o.recipient_registration_tokens)) return new Error(`${path}.recipient_registration_tokens: custom check failed`)
if ((o.title || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('title')) && typeof o.title !== 'string') return new Error(`${path}.title: is not a string`)
if (opts.title_CustomCheck && !opts.title_CustomCheck(o.title)) return new Error(`${path}.title: custom check failed`)
return null return null
} }

View file

@ -3,10 +3,12 @@ import { bytesToHex } from '@noble/hashes/utils'
import { sha256 } from '@noble/hashes/sha256' import { sha256 } from '@noble/hashes/sha256'
import { base64 } from '@scure/base'; import { base64 } from '@scure/base';
import NewClient, { ClientParams } from './autogenerated/http_client.js' import NewClient, { ClientParams } from './autogenerated/http_client.js'
import * as Types from './autogenerated/types.js'
import { ERROR, getLogger } from '../helpers/logger.js' import { ERROR, getLogger } from '../helpers/logger.js'
const utf8Encoder = new TextEncoder() const utf8Encoder = new TextEncoder()
export type PushPair = { pubkey: string, privateKey: string } export type PushPair = { pubkey: string, privateKey: string }
const nip98Kind = 27235 const nip98Kind = 27235
export type ShockPushNotification = { message: string, body: string, title: string }
export class ShockPush { export class ShockPush {
private client: ReturnType<typeof NewClient> private client: ReturnType<typeof NewClient>
private logger: ReturnType<typeof getLogger> private logger: ReturnType<typeof getLogger>
@ -52,8 +54,11 @@ export class ShockPush {
return nip98Header return nip98Header
} }
SendNotification = async (message: string, messagingTokens: string[]) => { SendNotification = async ({ body, message, title }: ShockPushNotification, messagingTokens: string[]) => {
const res = await this.client.SendNotification({ recipient_registration_tokens: messagingTokens, data: message }) const res = await this.client.SendNotification({
recipient_registration_tokens: messagingTokens, data: message,
body, title
})
if (res.status !== 'OK') { if (res.status !== 'OK') {
this.logger(ERROR, `failed to send notification: ${res.status}`) this.logger(ERROR, `failed to send notification: ${res.status}`)
} }

View file

@ -18,9 +18,9 @@ export type BalanceInfo = {
channelsBalance: ChannelBalance[]; channelsBalance: ChannelBalance[];
} }
export type AddressPaidCb = (txOutput: TxOutput, address: string, amount: number, used: 'lnd' | 'provider' | 'internal') => Promise<void> export type AddressPaidCb = (txOutput: TxOutput, address: string, amount: number, used: 'lnd' | 'provider' | 'internal', broadcastHeight?: number) => Promise<void>
export type InvoicePaidCb = (paymentRequest: string, amount: number, used: 'lnd' | 'provider' | 'internal') => Promise<void> export type InvoicePaidCb = (paymentRequest: string, amount: number, used: 'lnd' | 'provider' | 'internal') => Promise<void>
export type NewBlockCb = (height: number) => void export type NewBlockCb = (height: number, skipMetrics?: boolean) => Promise<void>
export type HtlcCb = (event: HtlcEvent) => void export type HtlcCb = (event: HtlcEvent) => void
export type ChannelEventCb = (event: ChannelEventUpdate, channels: Channel[]) => void export type ChannelEventCb = (event: ChannelEventUpdate, channels: Channel[]) => void

View file

@ -66,7 +66,7 @@ export default class {
const appUser = await this.storage.applicationStorage.GetAppUserFromUser(app, user.user_id) const appUser = await this.storage.applicationStorage.GetAppUserFromUser(app, user.user_id)
if (!appUser) { if (!appUser) {
throw new Error(`app user ${ctx.user_id} not found`) // TODO: fix logs doxing throw new Error("app user not found")
} }
const nostrSettings = this.settings.getSettings().nostrRelaySettings const nostrSettings = this.settings.getSettings().nostrRelaySettings
const { max, serviceFeeFloor, serviceFeeBps } = this.applicationManager.paymentManager.GetMaxPayableInvoice(user.balance_sats) const { max, serviceFeeFloor, serviceFeeBps } = this.applicationManager.paymentManager.GetMaxPayableInvoice(user.balance_sats)

View file

@ -31,6 +31,7 @@ import { NotificationsManager } from "./notificationsManager.js"
import { ApplicationUser } from '../storage/entity/ApplicationUser.js' import { ApplicationUser } from '../storage/entity/ApplicationUser.js'
import SettingsManager from './settingsManager.js' import SettingsManager from './settingsManager.js'
import { NostrSettings, AppInfo } from '../nostr/nostrPool.js' import { NostrSettings, AppInfo } from '../nostr/nostrPool.js'
import { ShockPushNotification } from '../ShockPush/index.js'
type UserOperationsSub = { type UserOperationsSub = {
id: string id: string
newIncomingInvoice: (operation: Types.UserOperation) => void newIncomingInvoice: (operation: Types.UserOperation) => void
@ -80,7 +81,7 @@ export default class {
this.liquidityManager = new LiquidityManager(this.settings, this.storage, this.utils, this.liquidityProvider, this.lnd, this.rugPullTracker) this.liquidityManager = new LiquidityManager(this.settings, this.storage, this.utils, this.liquidityProvider, this.lnd, this.rugPullTracker)
this.metricsManager = new MetricsManager(this.storage, this.lnd) this.metricsManager = new MetricsManager(this.storage, this.lnd)
this.paymentManager = new PaymentManager(this.storage, this.metricsManager, this.lnd, adminManager.swaps, this.settings, this.liquidityManager, this.utils, this.addressPaidCb, this.invoicePaidCb) this.paymentManager = new PaymentManager(this.storage, this.metricsManager, this.lnd, adminManager.swaps, this.settings, this.liquidityManager, this.utils, this.addressPaidCb, this.invoicePaidCb, this.newBlockCb)
this.productManager = new ProductManager(this.storage, this.paymentManager, this.settings) this.productManager = new ProductManager(this.storage, this.paymentManager, this.settings)
this.applicationManager = new ApplicationManager(this.storage, this.settings, this.paymentManager) this.applicationManager = new ApplicationManager(this.storage, this.settings, this.paymentManager)
this.appUserManager = new AppUserManager(this.storage, this.settings, this.applicationManager) this.appUserManager = new AppUserManager(this.storage, this.settings, this.applicationManager)
@ -153,18 +154,20 @@ export default class {
this.metricsManager.HtlcCb(e) this.metricsManager.HtlcCb(e)
} }
newBlockCb: NewBlockCb = (height) => { newBlockCb: NewBlockCb = (height, skipMetrics) => {
this.NewBlockHandler(height) return this.NewBlockHandler(height, skipMetrics)
} }
NewBlockHandler = async (height: number) => { NewBlockHandler = async (height: number, skipMetrics?: boolean) => {
let confirmed: (PendingTx & { confs: number; })[] let confirmed: (PendingTx & { confs: number; })[]
let log = getLogger({}) let log = getLogger({})
this.storage.paymentStorage.DeleteExpiredTransactionSwaps(height) this.storage.paymentStorage.DeleteExpiredTransactionSwaps(height)
.catch(err => log(ERROR, "failed to delete expired transaction swaps", err.message || err)) .catch(err => log(ERROR, "failed to delete expired transaction swaps", err.message || err))
try { try {
const balanceEvents = await this.paymentManager.GetLndBalance() const balanceEvents = await this.paymentManager.GetLndBalance()
await this.metricsManager.NewBlockCb(height, balanceEvents) if (!skipMetrics) {
await this.metricsManager.NewBlockCb(height, balanceEvents)
}
confirmed = await this.paymentManager.CheckNewlyConfirmedTxs(height) confirmed = await this.paymentManager.CheckNewlyConfirmedTxs(height)
await this.liquidityManager.onNewBlock() await this.liquidityManager.onNewBlock()
} catch (err: any) { } catch (err: any) {
@ -203,7 +206,7 @@ export default class {
})) }))
} }
addressPaidCb: AddressPaidCb = (txOutput, address, amount, used) => { addressPaidCb: AddressPaidCb = (txOutput, address, amount, used, broadcastHeight) => {
return this.storage.StartTransaction(async tx => { return this.storage.StartTransaction(async tx => {
// On-chain payments not supported when bypass is enabled // On-chain payments not supported when bypass is enabled
if (this.liquidityProvider.getSettings().useOnlyLiquidityProvider) { if (this.liquidityProvider.getSettings().useOnlyLiquidityProvider) {
@ -231,7 +234,8 @@ export default class {
const fee = this.paymentManager.getReceiveServiceFee(Types.UserOperationType.INCOMING_TX, amount, isManagedUser) const fee = this.paymentManager.getReceiveServiceFee(Types.UserOperationType.INCOMING_TX, amount, isManagedUser)
try { try {
// This call will fail if the transaction is already registered // This call will fail if the transaction is already registered
const addedTx = await this.storage.paymentStorage.AddAddressReceivingTransaction(userAddress, txOutput.hash, txOutput.index, amount, fee, internal, blockHeight, tx) const txBroadcastHeight = broadcastHeight ? broadcastHeight : blockHeight
const addedTx = await this.storage.paymentStorage.AddAddressReceivingTransaction(userAddress, txOutput.hash, txOutput.index, amount, fee, internal, txBroadcastHeight, tx)
if (internal) { if (internal) {
const addressData = `${address}:${txOutput.hash}` const addressData = `${address}:${txOutput.hash}`
this.storage.eventsLog.LogEvent({ type: 'address_paid', userId: userAddress.user.user_id, appId: userAddress.linkedApplication.app_id, appUserId: "", balance: userAddress.user.balance_sats, data: addressData, amount }) this.storage.eventsLog.LogEvent({ type: 'address_paid', userId: userAddress.user.user_id, appId: userAddress.linkedApplication.app_id, appUserId: "", balance: userAddress.user.balance_sats, data: addressData, amount })
@ -381,10 +385,36 @@ export default class {
{ operation: op, requestId: "GetLiveUserOperations", status: 'OK', latest_balance: balance } { operation: op, requestId: "GetLiveUserOperations", status: 'OK', latest_balance: balance }
const j = JSON.stringify(message) const j = JSON.stringify(message)
this.utils.nostrSender.Send({ type: 'app', appId: app.app_id }, { type: 'content', content: j, pub: user.nostr_public_key }) this.utils.nostrSender.Send({ type: 'app', appId: app.app_id }, { type: 'content', content: j, pub: user.nostr_public_key })
this.SendEncryptedNotification(app, user, op)
this.SendEncryptedNotification(app, user, op, this.getOperationMessage(op))
} }
async SendEncryptedNotification(app: Application, appUser: ApplicationUser, op: Types.UserOperation) { getOperationMessage = (op: Types.UserOperation) => {
switch (op.type) {
case Types.UserOperationType.INCOMING_TX:
case Types.UserOperationType.INCOMING_INVOICE:
case Types.UserOperationType.INCOMING_USER_TO_USER:
return {
body: "You received a new payment",
title: "Payment Received"
}
case Types.UserOperationType.OUTGOING_TX:
case Types.UserOperationType.OUTGOING_INVOICE:
case Types.UserOperationType.OUTGOING_USER_TO_USER:
return {
body: "You sent a new payment",
title: "Payment Sent"
}
default:
return {
body: "Unknown operation",
title: "Unknown Operation"
}
}
}
async SendEncryptedNotification(app: Application, appUser: ApplicationUser, op: Types.UserOperation, { body, title }: { body: string, title: string }) {
const devices = await this.storage.applicationStorage.GetAppUserDevices(appUser.identifier) const devices = await this.storage.applicationStorage.GetAppUserDevices(appUser.identifier)
if (devices.length === 0 || !app.nostr_public_key || !app.nostr_private_key || !appUser.nostr_public_key) { if (devices.length === 0 || !app.nostr_public_key || !app.nostr_private_key || !appUser.nostr_public_key) {
return return
@ -394,8 +424,12 @@ export default class {
const j = JSON.stringify(op) const j = JSON.stringify(op)
const encrypted = nip44.encrypt(j, ck) const encrypted = nip44.encrypt(j, ck)
const encryptedData: { encrypted: string, app_npub_hex: string } = { encrypted, app_npub_hex: app.nostr_public_key } const encryptedData: { encrypted: string, app_npub_hex: string } = { encrypted, app_npub_hex: app.nostr_public_key }
const notification: ShockPushNotification = {
this.notificationsManager.SendNotification(JSON.stringify(encryptedData), tokens, { message: JSON.stringify(encryptedData),
body,
title
}
await this.notificationsManager.SendNotification(notification, tokens, {
pubkey: app.nostr_public_key!, pubkey: app.nostr_public_key!,
privateKey: app.nostr_private_key! privateKey: app.nostr_private_key!
}) })

View file

@ -1,4 +1,4 @@
import { PushPair, ShockPush } from "../ShockPush/index.js" import { PushPair, ShockPush, ShockPushNotification } from "../ShockPush/index.js"
import { getLogger, PubLogger } from "../helpers/logger.js" import { getLogger, PubLogger } from "../helpers/logger.js"
import SettingsManager from "./settingsManager.js" import SettingsManager from "./settingsManager.js"
@ -21,12 +21,12 @@ export class NotificationsManager {
return newClient return newClient
} }
SendNotification = async (message: string, messagingTokens: string[], pair: PushPair) => { SendNotification = async (notification: ShockPushNotification, messagingTokens: string[], pair: PushPair) => {
if (!this.settings.getSettings().serviceSettings.shockPushBaseUrl) { if (!this.settings.getSettings().serviceSettings.shockPushBaseUrl) {
this.logger("ShockPush is not configured, skipping notification") this.logger("ShockPush is not configured, skipping notification")
return return
} }
const client = this.getClient(pair) const client = this.getClient(pair)
await client.SendNotification(message, messagingTokens) await client.SendNotification(notification, messagingTokens)
} }
} }

View file

@ -6,7 +6,7 @@ import { InboundOptionals, defaultInvoiceExpiry } from '../storage/paymentStorag
import LND from '../lnd/lnd.js' import LND from '../lnd/lnd.js'
import { Application } from '../storage/entity/Application.js' import { Application } from '../storage/entity/Application.js'
import { ERROR, getLogger, PubLogger } from '../helpers/logger.js' import { ERROR, getLogger, PubLogger } from '../helpers/logger.js'
import { AddressPaidCb, InvoicePaidCb } from '../lnd/settings.js' import { AddressPaidCb, InvoicePaidCb, NewBlockCb } from '../lnd/settings.js'
import { UserReceivingInvoice, ZapInfo } from '../storage/entity/UserReceivingInvoice.js' import { UserReceivingInvoice, ZapInfo } from '../storage/entity/UserReceivingInvoice.js'
import { Payment_PaymentStatus } from '../../../proto/lnd/lightning.js' import { Payment_PaymentStatus } from '../../../proto/lnd/lightning.js'
import { Event, verifiedSymbol, verifyEvent } from 'nostr-tools' import { Event, verifiedSymbol, verifyEvent } from 'nostr-tools'
@ -54,6 +54,7 @@ export default class {
lnd: LND lnd: LND
addressPaidCb: AddressPaidCb addressPaidCb: AddressPaidCb
invoicePaidCb: InvoicePaidCb invoicePaidCb: InvoicePaidCb
newBlockCb: NewBlockCb
log = getLogger({ component: "PaymentManager" }) log = getLogger({ component: "PaymentManager" })
watchDog: Watchdog watchDog: Watchdog
liquidityManager: LiquidityManager liquidityManager: LiquidityManager
@ -61,7 +62,7 @@ export default class {
swaps: Swaps swaps: Swaps
invoiceLock: InvoiceLock invoiceLock: InvoiceLock
metrics: Metrics metrics: Metrics
constructor(storage: Storage, metrics: Metrics, lnd: LND, swaps: Swaps, settings: SettingsManager, liquidityManager: LiquidityManager, utils: Utils, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb) { constructor(storage: Storage, metrics: Metrics, lnd: LND, swaps: Swaps, settings: SettingsManager, liquidityManager: LiquidityManager, utils: Utils, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb, newBlockCb: NewBlockCb) {
this.storage = storage this.storage = storage
this.metrics = metrics this.metrics = metrics
this.settings = settings this.settings = settings
@ -72,6 +73,7 @@ export default class {
this.swaps = swaps this.swaps = swaps
this.addressPaidCb = addressPaidCb this.addressPaidCb = addressPaidCb
this.invoicePaidCb = invoicePaidCb this.invoicePaidCb = invoicePaidCb
this.newBlockCb = newBlockCb
this.invoiceLock = new InvoiceLock() this.invoiceLock = new InvoiceLock()
} }
@ -185,24 +187,39 @@ export default class {
} }
try { try {
const { txs, currentHeight, lndPubkey } = await this.getLatestTransactions(log) const { txs, currentHeight, lndPubkey, startHeight } = await this.getLatestTransactions(log)
const recoveredCount = await this.processMissedChainTransactions(txs, log) const recoveredCount = await this.processMissedChainTransactions(txs, log, startHeight)
// Update latest checked height to current block height // Update latest checked height to current block height
await this.storage.liquidityStorage.UpdateLatestCheckedHeight('lnd', lndPubkey, currentHeight) await this.storage.liquidityStorage.UpdateLatestCheckedHeight('lnd', lndPubkey, currentHeight)
if (recoveredCount > 0) { if (recoveredCount > 0) {
log(`processed ${recoveredCount} missed chain tx(s)`) log(`processed ${recoveredCount} missed chain tx(s) triggering new block callback`)
// Call new block callback to process any new transaction that was just added to the database as pending
await this.newBlockCb(currentHeight, true)
} else { } else {
log("no missed chain transactions found") log("no missed chain transactions found")
} }
await this.reprocessStuckPendingTx(log, currentHeight)
} catch (err: any) { } catch (err: any) {
log(ERROR, "failed to check for missed chain transactions:", err.message || err) log(ERROR, "failed to check for missed chain transactions:", err.message || err)
} }
} }
private async getLatestTransactions(log: PubLogger): Promise<{ txs: Transaction[], currentHeight: number, lndPubkey: string }> { reprocessStuckPendingTx = async (log: PubLogger, currentHeight: number) => {
const { incoming } = await this.storage.paymentStorage.GetPendingTransactions()
const found = incoming.find(t => t.broadcast_height < currentHeight - 100)
if (found) {
log("found a possibly stuck pending transaction, reprocessing with full transaction history")
// There is a pending transaction more than 100 blocks old, this is likely a transaction
// that has a broadcast height higher than it actually is, so its not getting picked up when being processed
// by calling new block cb with height of 1, we make sure that even if the transaction has a newer height, it will still be processed
await this.newBlockCb(1, true)
}
}
private async getLatestTransactions(log: PubLogger): Promise<{ txs: Transaction[], currentHeight: number, lndPubkey: string, startHeight: number }> {
const lndInfo = await this.lnd.GetInfo() const lndInfo = await this.lnd.GetInfo()
const lndPubkey = lndInfo.identityPubkey const lndPubkey = lndInfo.identityPubkey
@ -212,10 +229,10 @@ export default class {
const { transactions } = await this.lnd.GetTransactions(startHeight) const { transactions } = await this.lnd.GetTransactions(startHeight)
log(`retrieved ${transactions.length} transactions from LND`) log(`retrieved ${transactions.length} transactions from LND`)
return { txs: transactions, currentHeight: lndInfo.blockHeight, lndPubkey } return { txs: transactions, currentHeight: lndInfo.blockHeight, lndPubkey, startHeight }
} }
private async processMissedChainTransactions(transactions: Transaction[], log: PubLogger): Promise<number> { private async processMissedChainTransactions(transactions: Transaction[], log: PubLogger, startHeight: number): Promise<number> {
let recoveredCount = 0 let recoveredCount = 0
const addresses = await this.lnd.ListAddresses() const addresses = await this.lnd.ListAddresses()
for (const tx of transactions) { for (const tx of transactions) {
@ -231,7 +248,7 @@ export default class {
continue continue
} }
const processed = await this.processUserAddressOutput(output, tx, log) const processed = await this.processUserAddressOutput(output, tx, log, startHeight)
if (processed) { if (processed) {
recoveredCount++ recoveredCount++
} }
@ -272,7 +289,7 @@ export default class {
return true return true
} }
private async processUserAddressOutput(output: OutputDetail, tx: Transaction, log: PubLogger) { private async processUserAddressOutput(output: OutputDetail, tx: Transaction, log: PubLogger, startHeight: number) {
const existingTx = await this.storage.paymentStorage.GetAddressReceivingTransactionOwner( const existingTx = await this.storage.paymentStorage.GetAddressReceivingTransactionOwner(
output.address, output.address,
tx.txHash tx.txHash
@ -285,7 +302,7 @@ export default class {
const amount = Number(output.amount) const amount = Number(output.amount)
const outputIndex = Number(output.outputIndex) const outputIndex = Number(output.outputIndex)
log(`processing missed chain tx: address=${output.address}, txHash=${tx.txHash}, amount=${amount}, outputIndex=${outputIndex}`) log(`processing missed chain tx: address=${output.address}, txHash=${tx.txHash}, amount=${amount}, outputIndex=${outputIndex}`)
this.addressPaidCb({ hash: tx.txHash, index: outputIndex }, output.address, amount, 'lnd') this.addressPaidCb({ hash: tx.txHash, index: outputIndex }, output.address, amount, 'lnd', startHeight)
.catch(err => log(ERROR, "failed to process user address output:", err.message || err)) .catch(err => log(ERROR, "failed to process user address output:", err.message || err))
return true return true
} }

View file

@ -42,7 +42,7 @@ export default class {
async GetUser(userId: string, txId?: string): Promise<User> { async GetUser(userId: string, txId?: string): Promise<User> {
const user = await this.FindUser(userId, txId) const user = await this.FindUser(userId, txId)
if (!user) { if (!user) {
throw new Error(`user ${userId} not found`) // TODO: fix logs doxing throw new Error(`user not found`)
} }
return user return user
} }
@ -50,7 +50,7 @@ export default class {
async UnbanUser(userId: string, txId?: string) { async UnbanUser(userId: string, txId?: string) {
const affected = await this.dbs.Update<User>('User', { user_id: userId }, { locked: false }, txId) const affected = await this.dbs.Update<User>('User', { user_id: userId }, { locked: false }, txId)
if (!affected) { if (!affected) {
throw new Error("unaffected user unlock for " + userId) // TODO: fix logs doxing throw new Error("unaffected user unlock")
} }
} }
@ -58,7 +58,7 @@ export default class {
const user = await this.GetUser(userId, txId) const user = await this.GetUser(userId, txId)
const affected = await this.dbs.Update<User>('User', { user_id: userId }, { balance_sats: 0, locked: true }, txId) const affected = await this.dbs.Update<User>('User', { user_id: userId }, { balance_sats: 0, locked: true }, txId)
if (!affected) { if (!affected) {
throw new Error("unaffected ban user for " + userId) // TODO: fix logs doxing throw new Error("unaffected ban user")
} }
if (user.balance_sats > 0) { if (user.balance_sats > 0) {
this.eventsLog.LogEvent({ type: 'balance_decrement', userId, appId: "", appUserId: "", balance: user.balance_sats, data: 'ban', amount: user.balance_sats }) this.eventsLog.LogEvent({ type: 'balance_decrement', userId, appId: "", appUserId: "", balance: user.balance_sats, data: 'ban', amount: user.balance_sats })
@ -80,7 +80,7 @@ export default class {
const affected = await this.dbs.Increment<User>('User', { user_id: userId }, "balance_sats", increment, txId) const affected = await this.dbs.Increment<User>('User', { user_id: userId }, "balance_sats", increment, txId)
if (!affected) { if (!affected) {
getLogger({ userId: userId, component: "balanceUpdates" })("user unaffected by increment") getLogger({ userId: userId, component: "balanceUpdates" })("user unaffected by increment")
throw new Error("unaffected balance increment for " + userId) // TODO: fix logs doxing throw new Error("unaffected balance increment")
} }
getLogger({ userId: userId, component: "balanceUpdates" })("incremented balance from", user.balance_sats, "sats, by", increment, "sats") getLogger({ userId: userId, component: "balanceUpdates" })("incremented balance from", user.balance_sats, "sats, by", increment, "sats")
this.eventsLog.LogEvent({ type: 'balance_increment', userId, appId: "", appUserId: "", balance: user.balance_sats, data: reason, amount: increment }) this.eventsLog.LogEvent({ type: 'balance_increment', userId, appId: "", appUserId: "", balance: user.balance_sats, data: reason, amount: increment })
@ -105,7 +105,7 @@ export default class {
const affected = await this.dbs.Decrement<User>('User', { user_id: userId }, "balance_sats", decrement, txId) const affected = await this.dbs.Decrement<User>('User', { user_id: userId }, "balance_sats", decrement, txId)
if (!affected) { if (!affected) {
getLogger({ userId: userId, component: "balanceUpdates" })("user unaffected by decrement") getLogger({ userId: userId, component: "balanceUpdates" })("user unaffected by decrement")
throw new Error("unaffected balance decrement for " + userId) // TODO: fix logs doxing throw new Error("unaffected balance decrement")
} }
getLogger({ userId: userId, component: "balanceUpdates" })("decremented balance from", user.balance_sats, "sats, by", decrement, "sats") getLogger({ userId: userId, component: "balanceUpdates" })("decremented balance from", user.balance_sats, "sats, by", decrement, "sats")
this.eventsLog.LogEvent({ type: 'balance_decrement', userId, appId: "", appUserId: "", balance: user.balance_sats, data: reason, amount: decrement }) this.eventsLog.LogEvent({ type: 'balance_decrement', userId, appId: "", appUserId: "", balance: user.balance_sats, data: reason, amount: decrement })

View file

@ -26,8 +26,8 @@ export const setupNetwork = async (): Promise<ChainTools> => {
const lndNodeSettings = LoadLndNodeSettingsFromEnv({}) const lndNodeSettings = LoadLndNodeSettingsFromEnv({})
const secondLndNodeSettings = LoadSecondLndSettingsFromEnv() const secondLndNodeSettings = LoadSecondLndSettingsFromEnv()
const liquiditySettings: LiquiditySettings = { disableLiquidityProvider: true, liquidityProviderPub: "", useOnlyLiquidityProvider: false, providerRelayUrl: "" } const liquiditySettings: LiquiditySettings = { disableLiquidityProvider: true, liquidityProviderPub: "", useOnlyLiquidityProvider: false, providerRelayUrl: "" }
const alice = new LND(() => ({ lndSettings, lndNodeSettings }), new LiquidityProvider(() => liquiditySettings, setupUtils, async () => { }, async () => { }), async () => { }, setupUtils, async () => { }, async () => { }, () => { }, () => { }, () => { }) const alice = new LND(() => ({ lndSettings, lndNodeSettings }), new LiquidityProvider(() => liquiditySettings, setupUtils, async () => { }, async () => { }), async () => { }, setupUtils, async () => { }, async () => { }, async () => { }, () => { }, () => { })
const bob = new LND(() => ({ lndSettings, lndNodeSettings: secondLndNodeSettings }), new LiquidityProvider(() => liquiditySettings, setupUtils, async () => { }, async () => { }), async () => { }, setupUtils, async () => { }, async () => { }, () => { }, () => { }, () => { }) const bob = new LND(() => ({ lndSettings, lndNodeSettings: secondLndNodeSettings }), new LiquidityProvider(() => liquiditySettings, setupUtils, async () => { }, async () => { }), async () => { }, setupUtils, async () => { }, async () => { }, async () => { }, () => { }, () => { })
await tryUntil<void>(async i => { await tryUntil<void>(async i => {
const peers = await alice.ListPeers() const peers = await alice.ListPeers()
if (peers.peers.length > 0) { if (peers.peers.length > 0) {

View file

@ -89,12 +89,12 @@ export const SetupTest = async (d: Describe, chainTools: ChainTools): Promise<Te
const lndSettings = LoadLndSettingsFromEnv({}) const lndSettings = LoadLndSettingsFromEnv({})
const secondLndNodeSettings = LoadSecondLndSettingsFromEnv() const secondLndNodeSettings = LoadSecondLndSettingsFromEnv()
const otherLndSetting = () => ({ lndSettings, lndNodeSettings: secondLndNodeSettings }) const otherLndSetting = () => ({ lndSettings, lndNodeSettings: secondLndNodeSettings })
const externalAccessToOtherLnd = new LND(otherLndSetting, new LiquidityProvider(() => liquiditySettings, extermnalUtils, async () => { }, async () => { }), async () => { }, extermnalUtils, async () => { }, async () => { }, () => { }, () => { }, () => { }) const externalAccessToOtherLnd = new LND(otherLndSetting, new LiquidityProvider(() => liquiditySettings, extermnalUtils, async () => { }, async () => { }), async () => { }, extermnalUtils, async () => { }, async () => { }, async () => { }, () => { }, () => { })
await externalAccessToOtherLnd.Warmup() await externalAccessToOtherLnd.Warmup()
const thirdLndNodeSettings = LoadThirdLndSettingsFromEnv() const thirdLndNodeSettings = LoadThirdLndSettingsFromEnv()
const thirdLndSetting = () => ({ lndSettings, lndNodeSettings: thirdLndNodeSettings }) const thirdLndSetting = () => ({ lndSettings, lndNodeSettings: thirdLndNodeSettings })
const externalAccessToThirdLnd = new LND(thirdLndSetting, new LiquidityProvider(() => liquiditySettings, extermnalUtils, async () => { }, async () => { }), async () => { }, extermnalUtils, async () => { }, async () => { }, () => { }, () => { }, () => { }) const externalAccessToThirdLnd = new LND(thirdLndSetting, new LiquidityProvider(() => liquiditySettings, extermnalUtils, async () => { }, async () => { }), async () => { }, extermnalUtils, async () => { }, async () => { }, async () => { }, () => { }, () => { })
await externalAccessToThirdLnd.Warmup() await externalAccessToThirdLnd.Warmup()