watchdog v0
This commit is contained in:
parent
d83f1e6074
commit
f20abdd5f4
9 changed files with 79 additions and 0 deletions
|
|
@ -44,3 +44,5 @@ MIGRATE_DB=false
|
||||||
RECORD_PERFORMANCE=true
|
RECORD_PERFORMANCE=true
|
||||||
SKIP_SANITY_CHECK=false
|
SKIP_SANITY_CHECK=false
|
||||||
DISABLE_EXTERNAL_PAYMENTS=false
|
DISABLE_EXTERNAL_PAYMENTS=false
|
||||||
|
WATCHDOG_MAX_DIFF_BPS=100
|
||||||
|
WATCHDOG_MAX_DIFF_SATS=10000
|
||||||
|
|
@ -38,6 +38,8 @@ export interface LightningHandler {
|
||||||
GetForwardingHistory(indexOffset: number): Promise<{ fee: number, chanIdIn: string, chanIdOut: string, timestampNs: number, offset: number }[]>
|
GetForwardingHistory(indexOffset: number): Promise<{ fee: number, chanIdIn: string, chanIdOut: string, timestampNs: number, offset: number }[]>
|
||||||
GetAllPaidInvoices(max: number): Promise<ListInvoiceResponse>
|
GetAllPaidInvoices(max: number): Promise<ListInvoiceResponse>
|
||||||
GetAllPayments(max: number): Promise<ListPaymentsResponse>
|
GetAllPayments(max: number): Promise<ListPaymentsResponse>
|
||||||
|
LockOutgoingOperations(): void
|
||||||
|
UnlockOutgoingOperations(): void
|
||||||
}
|
}
|
||||||
|
|
||||||
export default (settings: LndSettings, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb, newBlockCb: NewBlockCb, htlcCb: HtlcCb): LightningHandler => {
|
export default (settings: LndSettings, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb, newBlockCb: NewBlockCb, htlcCb: HtlcCb): LightningHandler => {
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ export default class {
|
||||||
newBlockCb: NewBlockCb
|
newBlockCb: NewBlockCb
|
||||||
htlcCb: HtlcCb
|
htlcCb: HtlcCb
|
||||||
log = getLogger({ appName: 'lndManager' })
|
log = getLogger({ appName: 'lndManager' })
|
||||||
|
outgoingOpsLocked = false
|
||||||
constructor(settings: LndSettings, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb, newBlockCb: NewBlockCb, htlcCb: HtlcCb) {
|
constructor(settings: LndSettings, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb, newBlockCb: NewBlockCb, htlcCb: HtlcCb) {
|
||||||
this.settings = settings
|
this.settings = settings
|
||||||
this.addressPaidCb = addressPaidCb
|
this.addressPaidCb = addressPaidCb
|
||||||
|
|
@ -60,6 +61,14 @@ export default class {
|
||||||
this.router = new RouterClient(transport)
|
this.router = new RouterClient(transport)
|
||||||
this.chainNotifier = new ChainNotifierClient(transport)
|
this.chainNotifier = new ChainNotifierClient(transport)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LockOutgoingOperations(): void {
|
||||||
|
this.outgoingOpsLocked = true
|
||||||
|
}
|
||||||
|
UnlockOutgoingOperations(): void {
|
||||||
|
this.outgoingOpsLocked = false
|
||||||
|
}
|
||||||
|
|
||||||
SetMockInvoiceAsPaid(invoice: string, amount: number): Promise<void> {
|
SetMockInvoiceAsPaid(invoice: string, amount: number): Promise<void> {
|
||||||
throw new Error("SetMockInvoiceAsPaid only available in mock mode")
|
throw new Error("SetMockInvoiceAsPaid only available in mock mode")
|
||||||
}
|
}
|
||||||
|
|
@ -251,6 +260,10 @@ export default class {
|
||||||
return { local: r.localBalance ? Number(r.localBalance.sat) : 0, remote: r.remoteBalance ? Number(r.remoteBalance.sat) : 0 }
|
return { local: r.localBalance ? Number(r.localBalance.sat) : 0, remote: r.remoteBalance ? Number(r.remoteBalance.sat) : 0 }
|
||||||
}
|
}
|
||||||
async PayInvoice(invoice: string, amount: number, feeLimit: number): Promise<PaidInvoice> {
|
async PayInvoice(invoice: string, amount: number, feeLimit: number): Promise<PaidInvoice> {
|
||||||
|
if (this.outgoingOpsLocked) {
|
||||||
|
this.log("outgoing ops locked, rejecting payment request")
|
||||||
|
throw new Error("lnd node is currently out of sync")
|
||||||
|
}
|
||||||
await this.Health()
|
await this.Health()
|
||||||
this.log("paying invoice", invoice, "for", amount, "sats")
|
this.log("paying invoice", invoice, "for", amount, "sats")
|
||||||
const abortController = new AbortController()
|
const abortController = new AbortController()
|
||||||
|
|
@ -287,6 +300,10 @@ export default class {
|
||||||
}
|
}
|
||||||
|
|
||||||
async PayAddress(address: string, amount: number, satPerVByte: number, label = ""): Promise<SendCoinsResponse> {
|
async PayAddress(address: string, amount: number, satPerVByte: number, label = ""): Promise<SendCoinsResponse> {
|
||||||
|
if (this.outgoingOpsLocked) {
|
||||||
|
this.log("outgoing ops locked, rejecting payment request")
|
||||||
|
throw new Error("lnd node is currently out of sync")
|
||||||
|
}
|
||||||
await this.Health()
|
await this.Health()
|
||||||
this.log("sending chain TX for", amount, "sats", "to", address)
|
this.log("sending chain TX for", amount, "sats", "to", address)
|
||||||
const res = await this.lightning.sendCoins(SendCoinsReq(address, amount, satPerVByte, label), DeadLineMetadata())
|
const res = await this.lightning.sendCoins(SendCoinsReq(address, amount, satPerVByte, label), DeadLineMetadata())
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,12 @@ export default class {
|
||||||
async GetAllPayments(max: number): Promise<ListPaymentsResponse> {
|
async GetAllPayments(max: number): Promise<ListPaymentsResponse> {
|
||||||
throw new Error("not implemented")
|
throw new Error("not implemented")
|
||||||
}
|
}
|
||||||
|
LockOutgoingOperations() {
|
||||||
|
throw new Error("not implemented")
|
||||||
|
}
|
||||||
|
UnlockOutgoingOperations() {
|
||||||
|
throw new Error("not implemented")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
34
src/services/lnd/watchdog.ts
Normal file
34
src/services/lnd/watchdog.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { EnvMustBeInteger } from "../helpers/envParser.js";
|
||||||
|
import { getLogger } from "../helpers/logger.js";
|
||||||
|
import { LightningHandler } from "./index.js";
|
||||||
|
export type WatchdogSettings = {
|
||||||
|
maxDiffBps: number
|
||||||
|
maxDiffSats: number
|
||||||
|
}
|
||||||
|
export const LoadWatchdogSettingsFromEnv = (test = false): WatchdogSettings => {
|
||||||
|
return {
|
||||||
|
maxDiffBps: EnvMustBeInteger("WATCHDOG_MAX_DIFF_BPS"),
|
||||||
|
maxDiffSats: EnvMustBeInteger("WATCHDOG_MAX_DIFF_SATS")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export class Watchdog {
|
||||||
|
lnd: LightningHandler;
|
||||||
|
settings: WatchdogSettings;
|
||||||
|
log = getLogger({ appName: "watchdog" })
|
||||||
|
constructor(settings: WatchdogSettings, lnd: LightningHandler) {
|
||||||
|
this.lnd = lnd;
|
||||||
|
this.settings = settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
PaymentRequested = async (totalUsersBalance: number) => {
|
||||||
|
this.log("Payment requested, checking balance")
|
||||||
|
const { channelsBalance, confirmedBalance } = await this.lnd.GetBalance()
|
||||||
|
const totalLndBalance = confirmedBalance + channelsBalance.reduce((acc, { localBalanceSats }) => acc + localBalanceSats, 0)
|
||||||
|
const diffSats = Math.abs(totalLndBalance - totalUsersBalance)
|
||||||
|
const diffBps = (diffSats / Math.max(totalLndBalance, totalUsersBalance)) * 10_000
|
||||||
|
if (diffSats > this.settings.maxDiffSats || diffBps > this.settings.maxDiffBps) {
|
||||||
|
this.log(`LND balance ${totalLndBalance} is too different from users balance ${totalUsersBalance}`)
|
||||||
|
this.lnd.LockOutgoingOperations()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,8 +17,10 @@ import { UnsignedEvent } from '../nostr/tools/event.js'
|
||||||
import { NostrSend } from '../nostr/handler.js'
|
import { NostrSend } from '../nostr/handler.js'
|
||||||
import MetricsManager from '../metrics/index.js'
|
import MetricsManager from '../metrics/index.js'
|
||||||
import EventsLogManager, { LoggedEvent } from '../storage/eventsLog.js'
|
import EventsLogManager, { LoggedEvent } from '../storage/eventsLog.js'
|
||||||
|
import { LoadWatchdogSettingsFromEnv } from '../lnd/watchdog.js'
|
||||||
export const LoadMainSettingsFromEnv = (test = false): MainSettings => {
|
export const LoadMainSettingsFromEnv = (test = false): MainSettings => {
|
||||||
return {
|
return {
|
||||||
|
watchDogSettings: LoadWatchdogSettingsFromEnv(test),
|
||||||
lndSettings: LoadLndSettingsFromEnv(test),
|
lndSettings: LoadLndSettingsFromEnv(test),
|
||||||
storageSettings: LoadStorageSettingsFromEnv(test),
|
storageSettings: LoadStorageSettingsFromEnv(test),
|
||||||
jwtSecret: EnvMustBeNonEmptyString("JWT_SECRET"),
|
jwtSecret: EnvMustBeNonEmptyString("JWT_SECRET"),
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import { SendCoinsResponse } from '../../../proto/lnd/lightning.js'
|
||||||
import { Event, verifiedSymbol, verifySignature } from '../nostr/tools/event.js'
|
import { Event, verifiedSymbol, verifySignature } from '../nostr/tools/event.js'
|
||||||
import { AddressReceivingTransaction } from '../storage/entity/AddressReceivingTransaction.js'
|
import { AddressReceivingTransaction } from '../storage/entity/AddressReceivingTransaction.js'
|
||||||
import { UserTransactionPayment } from '../storage/entity/UserTransactionPayment.js'
|
import { UserTransactionPayment } from '../storage/entity/UserTransactionPayment.js'
|
||||||
|
import { Watchdog } from '../lnd/watchdog.js'
|
||||||
interface UserOperationInfo {
|
interface UserOperationInfo {
|
||||||
serial_id: number
|
serial_id: number
|
||||||
paid_amount: number
|
paid_amount: number
|
||||||
|
|
@ -45,10 +46,12 @@ export default class {
|
||||||
addressPaidCb: AddressPaidCb
|
addressPaidCb: AddressPaidCb
|
||||||
invoicePaidCb: InvoicePaidCb
|
invoicePaidCb: InvoicePaidCb
|
||||||
log = getLogger({ appName: "PaymentManager" })
|
log = getLogger({ appName: "PaymentManager" })
|
||||||
|
watchDog: Watchdog
|
||||||
constructor(storage: Storage, lnd: LightningHandler, settings: MainSettings, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb) {
|
constructor(storage: Storage, lnd: LightningHandler, settings: MainSettings, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb) {
|
||||||
this.storage = storage
|
this.storage = storage
|
||||||
this.settings = settings
|
this.settings = settings
|
||||||
this.lnd = lnd
|
this.lnd = lnd
|
||||||
|
this.watchDog = new Watchdog(settings.watchDogSettings, lnd)
|
||||||
this.addressPaidCb = addressPaidCb
|
this.addressPaidCb = addressPaidCb
|
||||||
this.invoicePaidCb = invoicePaidCb
|
this.invoicePaidCb = invoicePaidCb
|
||||||
}
|
}
|
||||||
|
|
@ -134,8 +137,14 @@ export default class {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async WatchdogCheck() {
|
||||||
|
const total = await this.storage.paymentStorage.GetTotalUsersBalance()
|
||||||
|
await this.watchDog.PaymentRequested(total || 0)
|
||||||
|
}
|
||||||
|
|
||||||
async PayInvoice(userId: string, req: Types.PayInvoiceRequest, linkedApplication: Application): Promise<Types.PayInvoiceResponse> {
|
async PayInvoice(userId: string, req: Types.PayInvoiceRequest, linkedApplication: Application): Promise<Types.PayInvoiceResponse> {
|
||||||
this.log("paying invoice", req.invoice, "for user", userId, "with amount", req.amount)
|
this.log("paying invoice", req.invoice, "for user", userId, "with amount", req.amount)
|
||||||
|
this.WatchdogCheck()
|
||||||
const decoded = await this.lnd.DecodeInvoice(req.invoice)
|
const decoded = await this.lnd.DecodeInvoice(req.invoice)
|
||||||
if (decoded.numSatoshis !== 0 && req.amount !== 0) {
|
if (decoded.numSatoshis !== 0 && req.amount !== 0) {
|
||||||
throw new Error("invoice has value, do not provide amount the the request")
|
throw new Error("invoice has value, do not provide amount the the request")
|
||||||
|
|
@ -192,6 +201,7 @@ export default class {
|
||||||
|
|
||||||
async PayAddress(ctx: Types.UserContext, req: Types.PayAddressRequest): Promise<Types.PayAddressResponse> {
|
async PayAddress(ctx: Types.UserContext, req: Types.PayAddressRequest): Promise<Types.PayAddressResponse> {
|
||||||
throw new Error("address payment currently disabled, use Lightning instead")
|
throw new Error("address payment currently disabled, use Lightning instead")
|
||||||
|
this.WatchdogCheck()
|
||||||
const { blockHeight } = await this.lnd.GetInfo()
|
const { blockHeight } = await this.lnd.GetInfo()
|
||||||
const app = await this.storage.applicationStorage.GetApplication(ctx.app_id)
|
const app = await this.storage.applicationStorage.GetApplication(ctx.app_id)
|
||||||
const serviceFee = this.getServiceFee(Types.UserOperationType.OUTGOING_TX, req.amoutSats, false)
|
const serviceFee = this.getServiceFee(Types.UserOperationType.OUTGOING_TX, req.amoutSats, false)
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
import { StorageSettings } from '../storage/index.js'
|
import { StorageSettings } from '../storage/index.js'
|
||||||
import { LndSettings } from '../lnd/settings.js'
|
import { LndSettings } from '../lnd/settings.js'
|
||||||
|
import { WatchdogSettings } from '../lnd/watchdog.js'
|
||||||
export type MainSettings = {
|
export type MainSettings = {
|
||||||
storageSettings: StorageSettings,
|
storageSettings: StorageSettings,
|
||||||
lndSettings: LndSettings,
|
lndSettings: LndSettings,
|
||||||
|
watchDogSettings: WatchdogSettings,
|
||||||
jwtSecret: string
|
jwtSecret: string
|
||||||
incomingTxFee: number
|
incomingTxFee: number
|
||||||
outgoingTxFee: number
|
outgoingTxFee: number
|
||||||
|
|
|
||||||
|
|
@ -360,4 +360,8 @@ export default class {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async GetTotalUsersBalance(entityManager = this.DB) {
|
||||||
|
return entityManager.getRepository(User).sum("balance_sats")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue