diff --git a/src/services/main/init.ts b/src/services/main/init.ts index 776e989a..dd3051e8 100644 --- a/src/services/main/init.ts +++ b/src/services/main/init.ts @@ -72,7 +72,8 @@ export const initMainHandler = async (log: PubLogger, settingsManager: SettingsM if (stop) { return } - await mainHandler.paymentManager.checkPendingPayments() + await mainHandler.paymentManager.checkPaymentStatus() + await mainHandler.paymentManager.checkMissedChainTxs() await mainHandler.paymentManager.CleanupOldUnpaidInvoices() await mainHandler.appUserManager.CleanupInactiveUsers() await mainHandler.appUserManager.CleanupNeverActiveUsers() diff --git a/src/services/main/paymentManager.ts b/src/services/main/paymentManager.ts index e5cb9bdf..1c67afe8 100644 --- a/src/services/main/paymentManager.ts +++ b/src/services/main/paymentManager.ts @@ -75,11 +75,11 @@ export default class { this.swaps.Stop() } - checkPendingPayments = async () => { - const log = getLogger({ component: 'pendingPaymentsOnStart' }) + checkPaymentStatus = async () => { + const log = getLogger({ component: 'checkPaymentStatus' }) const pendingPayments = await this.storage.paymentStorage.GetPendingPayments() for (const p of pendingPayments) { - log("checking state of payment: ", p.invoice) + log("checking status of payment: ", p.invoice) if (p.internal) { log("found pending internal payment", p.serial_id) } else if (p.liquidityProvider) { @@ -170,6 +170,171 @@ export default class { } } + checkMissedChainTxs = async () => { + const log = getLogger({ component: 'checkMissedChainTxs' }) + + // Skip chain tx check when bypass is enabled + if (this.liquidityManager.settings.getSettings().liquiditySettings.useOnlyLiquidityProvider) { + log("USE_ONLY_LIQUIDITY_PROVIDER enabled, skipping chain tx check") + return + } + + log("checking for missed confirmed chain transactions...") + + // Get transactions from LND (only includes transactions relevant to the wallet) + // Use startHeight 0 to check all transactions + const startHeight = 0 + + try { + const { transactions } = await this.lnd.GetTransactions(startHeight) + log(`retrieved ${transactions.length} transactions from LND`) + + let recoveredCount = 0 + + for (const tx of transactions) { + // Only process confirmed transactions (at least 1 confirmation) + // Pending transactions (0 confirmations) are handled by SubscribeAddressPaid() + // and can be dropped from mempool, so we don't process them here + if (tx.numConfirmations === 0) { + continue + } + + // Check each output detail for addresses we care about + if (!tx.outputDetails || tx.outputDetails.length === 0) { + continue + } + + for (const output of tx.outputDetails) { + // Only process outputs that are our addresses and have an address + if (!output.address || !output.isOurAddress) { + continue + } + + // Check if this address belongs to a user (lazy lookup - only for addresses in transactions) + const userAddress = await this.storage.paymentStorage.GetAddressOwner(output.address) + if (!userAddress) { + // Not a user address, skip + continue + } + + // Check if we already have this transaction in the database + const existingTx = await this.storage.paymentStorage.GetAddressReceivingTransactionOwner( + output.address, + tx.txHash + ) + + if (existingTx) { + // Transaction already recorded, skip + continue + } + + // This is a missed transaction - process it + const amount = Number(output.amount) + const outputIndex = Number(output.outputIndex) + log(`processing missed chain tx: address=${output.address}, txHash=${tx.txHash}, amount=${amount}, outputIndex=${outputIndex}`) + + try { + await this.storage.StartTransaction(async dbTx => { + if (!userAddress.linkedApplication) { + log(ERROR, "found address with no linked application during recovery:", output.address) + return + } + + const isManagedUser = userAddress.user.user_id !== userAddress.linkedApplication.owner.user_id + const fee = this.getReceiveServiceFee(Types.UserOperationType.INCOMING_TX, amount, isManagedUser) + const blockHeight = tx.blockHeight || 0 + + // Add the transaction record + const addedTx = await this.storage.paymentStorage.AddAddressReceivingTransaction( + userAddress, + tx.txHash, + outputIndex, + amount, + fee, + false, // not internal + blockHeight, + dbTx + ) + + // Update balance + const addressData = `${output.address}:${tx.txHash}` + 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 + }) + + await this.storage.userStorage.IncrementUserBalance( + userAddress.user.user_id, + amount - fee, + addressData, + dbTx + ) + + if (fee > 0) { + await this.storage.userStorage.IncrementUserBalance( + userAddress.linkedApplication.owner.user_id, + fee, + 'fees', + dbTx + ) + } + + // Send operation to Nostr + const operationId = `${Types.UserOperationType.INCOMING_TX}-${addedTx.serial_id}` + const op = { + amount, + paidAtUnix: tx.timeStamp ? Number(tx.timeStamp) : Math.floor(Date.now() / 1000), + inbound: true, + type: Types.UserOperationType.INCOMING_TX, + identifier: userAddress.address, + operationId, + network_fee: 0, + service_fee: fee, + confirmed: tx.numConfirmations > 0, + tx_hash: tx.txHash, + internal: false + } + + // Note: sendOperationToNostr is not available here, but the operation will be synced + // when the user next syncs their operations + + this.utils.stateBundler.AddTxPoint( + 'addressWasPaid', + amount, + { used: 'lnd', from: 'system', timeDiscount: true }, + userAddress.linkedApplication.app_id + ) + + recoveredCount++ + log(`successfully processed missed chain tx: address=${output.address}, txHash=${tx.txHash}, amount=${amount}`) + }, "process missed chain tx") + } catch (err: any) { + log(ERROR, `failed to process missed chain tx for address=${output.address}, txHash=${tx.txHash}:`, err.message || err) + this.utils.stateBundler.AddTxPointFailed( + 'addressWasPaid', + amount, + { used: 'lnd', from: 'system' }, + userAddress.linkedApplication?.app_id + ) + } + } + } + + if (recoveredCount > 0) { + log(`processed ${recoveredCount} missed chain tx(s)`) + } else { + log("no missed chain transactions found") + } + } catch (err: any) { + log(ERROR, "failed to check for missed chain transactions:", err.message || err) + } + } + getReceiveServiceFee = (action: Types.UserOperationType, amount: number, managedUser: boolean): number => { switch (action) { case Types.UserOperationType.INCOMING_TX: