fix missed tx + notification info
This commit is contained in:
parent
4c58cab1d6
commit
0b9a2ee3d3
7 changed files with 111 additions and 33 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}`)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,7 +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 }
|
||||||
this.notificationsManager.SendNotification(JSON.stringify(encryptedData), tokens, {
|
const notification: ShockPushNotification = {
|
||||||
|
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!
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue