Merge pull request #631 from shocknet/lnd-metrics

lnd metrics
This commit is contained in:
boufni95 2023-12-20 18:09:27 +01:00 committed by GitHub
commit 1b4aaba566
23 changed files with 3850 additions and 2845 deletions

View file

@ -105,9 +105,9 @@ The nostr server will send back a message response, and inside the body there wi
- __User__:
- expected context content
- __app_id__: _string_
- __app_user_id__: _string_
- __user_id__: _string_
- __app_id__: _string_
- __Admin__:
- expected context content
@ -155,6 +155,13 @@ The nostr server will send back a message response, and inside the body there wi
- input: [AppsMetricsRequest](#AppsMetricsRequest)
- output: [AppsMetrics](#AppsMetrics)
- GetLndMetrics
- auth type: __Admin__
- http method: __post__
- http route: __/api/admin/metrics/lnd__
- input: [LndMetricsRequest](#LndMetricsRequest)
- output: [LndMetrics](#LndMetrics)
- Health
- auth type: __Guest__
- http method: __get__
@ -421,42 +428,34 @@ The nostr server will send back a message response, and inside the body there wi
## Messages
### The content of requests and response from the methods
### PayInvoiceResponse
- __preimage__: _string_
- __amount_paid__: _number_
- __operation_id__: _string_
- __service_fee__: _number_
- __network_fee__: _number_
### AuthApp
- __app__: _[Application](#Application)_
- __auth_token__: _string_
### OpenChannelRequest
- __destination__: _string_
- __fundingAmount__: _number_
- __pushAmount__: _number_
- __closeAddress__: _string_
### PayAddressRequest
- __address__: _string_
- __amoutSats__: _number_
- __satsPerVByte__: _number_
### LndGetInfoRequest
- __nodeId__: _number_
### SendAppUserToAppPaymentRequest
- __from_user_identifier__: _string_
### PayInvoiceRequest
- __invoice__: _string_
- __amount__: _number_
### SetMockAppBalanceRequest
- __amount__: _number_
### GetUserOperationsResponse
- __latestOutgoingInvoiceOperations__: _[UserOperations](#UserOperations)_
- __latestIncomingInvoiceOperations__: _[UserOperations](#UserOperations)_
- __latestOutgoingTxOperations__: _[UserOperations](#UserOperations)_
- __latestIncomingTxOperations__: _[UserOperations](#UserOperations)_
- __latestOutgoingUserToUserPayemnts__: _[UserOperations](#UserOperations)_
- __latestIncomingUserToUserPayemnts__: _[UserOperations](#UserOperations)_
### UserOperations
- __fromIndex__: _number_
- __toIndex__: _number_
- __operations__: ARRAY of: _[UserOperation](#UserOperation)_
### AddAppUserRequest
- __identifier__: _string_
- __fail_if_exists__: _boolean_
- __balance__: _number_
### GetAppUserLNURLInfoRequest
- __user_identifier__: _string_
- __base_url_override__: _string_
### DecodeInvoiceResponse
- __amount__: _number_
### AddProductRequest
### Product
- __id__: _string_
- __name__: _string_
- __price_sats__: _number_
@ -471,12 +470,131 @@ The nostr server will send back a message response, and inside the body there wi
- __nostr__: _boolean_
- __batch_size__: _number_
### SetMockInvoiceAsPaidRequest
### LndMetricsRequest
- __from_unix__: _number_ *this field is optional
- __to_unix__: _number_ *this field is optional
### LndNodeMetrics
- __channels_balance_events__: ARRAY of: _[ChannelBalanceEvent](#ChannelBalanceEvent)_
- __chain_balance_events__: ARRAY of: _[ChainBalanceEvent](#ChainBalanceEvent)_
- __routing_events__: ARRAY of: _[RoutingEvent](#RoutingEvent)_
### UsersInfo
- __total__: _number_
- __no_balance__: _number_
- __negative_balance__: _number_
- __always_been_inactive__: _number_
- __balance_avg__: _number_
- __balance_median__: _number_
### NewInvoiceResponse
- __invoice__: _string_
### PayInvoiceResponse
- __preimage__: _string_
- __amount_paid__: _number_
- __operation_id__: _string_
- __service_fee__: _number_
- __network_fee__: _number_
### OpenChannelResponse
- __channelId__: _string_
### LnurlLinkResponse
- __lnurl__: _string_
- __k1__: _string_
### AddAppRequest
- __name__: _string_
- __allow_user_creation__: _boolean_
### Application
- __name__: _string_
- __id__: _string_
- __balance__: _number_
- __npub__: _string_
### SendAppUserToAppPaymentRequest
- __from_user_identifier__: _string_
- __amount__: _number_
### DecodeInvoiceResponse
- __amount__: _number_
### ClosureMigration
- __closes_at_unix__: _number_
### SetMockAppBalanceRequest
- __amount__: _number_
### HandleLnurlPayResponse
- __pr__: _string_
- __routes__: ARRAY of: _[Empty](#Empty)_
### AppsMetrics
- __apps__: ARRAY of: _[AppMetrics](#AppMetrics)_
### OpenChannelRequest
- __destination__: _string_
- __fundingAmount__: _number_
- __pushAmount__: _number_
- __closeAddress__: _string_
### RelaysMigration
- __relays__: ARRAY of: _string_
### GetAppUserRequest
- __user_identifier__: _string_
### PayAppUserInvoiceRequest
- __user_identifier__: _string_
- __invoice__: _string_
- __amount__: _number_
### LndGetInfoResponse
- __alias__: _string_
### SendAppUserToAppUserPaymentRequest
- __from_user_identifier__: _string_
- __to_user_identifier__: _string_
- __amount__: _number_
### NewAddressRequest
- __addressType__: _[AddressType](#AddressType)_
### PayAddressResponse
- __txId__: _string_
- __operation_id__: _string_
- __service_fee__: _number_
- __network_fee__: _number_
### AddProductRequest
- __name__: _string_
- __price_sats__: _number_
### GetProductBuyLinkResponse
- __link__: _string_
### RoutingEvent
- __incoming_channel_id__: _number_
- __incoming_htlc_id__: _number_
- __outgoing_channel_id__: _number_
- __outgoing_htlc_id__: _number_
- __timestamp_ns__: _number_
- __event_type__: _string_
- __incoming_amt_msat__: _number_
- __outgoing_amt_msat__: _number_
- __failure_string__: _string_
- __settled__: _boolean_
- __offchain__: _boolean_
- __forward_fail_event__: _boolean_
### AppUser
- __identifier__: _string_
- __info__: _[UserInfo](#UserInfo)_
- __max_withdrawable__: _number_
### AddAppInvoiceRequest
- __payer_identifier__: _string_
- __http_callback_url__: _string_
- __invoice_req__: _[NewInvoiceRequest](#NewInvoiceRequest)_
### AddAppUserInvoiceRequest
- __receiver_identifier__: _string_
@ -484,50 +602,72 @@ The nostr server will send back a message response, and inside the body there wi
- __http_callback_url__: _string_
- __invoice_req__: _[NewInvoiceRequest](#NewInvoiceRequest)_
### GetProductBuyLinkResponse
- __link__: _string_
### DecodeInvoiceRequest
- __invoice__: _string_
### AppsMetrics
- __apps__: ARRAY of: _[AppMetrics](#AppMetrics)_
### UserOperations
- __fromIndex__: _number_
- __toIndex__: _number_
- __operations__: ARRAY of: _[UserOperation](#UserOperation)_
### MigrationUpdate
- __closure__: _[ClosureMigration](#ClosureMigration)_ *this field is optional
- __relays__: _[RelaysMigration](#RelaysMigration)_ *this field is optional
### EncryptionExchangeRequest
- __publicKey__: _string_
- __deviceId__: _string_
### Empty
### GetUserOperationsRequest
- __latestIncomingInvoice__: _number_
- __latestOutgoingInvoice__: _number_
- __latestIncomingTx__: _number_
- __latestOutgoingTx__: _number_
- __latestIncomingUserToUserPayment__: _number_
- __latestOutgoingUserToUserPayment__: _number_
### AddAppUserRequest
- __identifier__: _string_
- __fail_if_exists__: _boolean_
- __balance__: _number_
### GetUserOperationsResponse
- __latestOutgoingInvoiceOperations__: _[UserOperations](#UserOperations)_
- __latestIncomingInvoiceOperations__: _[UserOperations](#UserOperations)_
- __latestOutgoingTxOperations__: _[UserOperations](#UserOperations)_
- __latestIncomingTxOperations__: _[UserOperations](#UserOperations)_
- __latestOutgoingUserToUserPayemnts__: _[UserOperations](#UserOperations)_
- __latestIncomingUserToUserPayemnts__: _[UserOperations](#UserOperations)_
### AuthApp
### AppMetrics
- __app__: _[Application](#Application)_
- __auth_token__: _string_
- __users__: _[UsersInfo](#UsersInfo)_
- __total_received__: _number_
- __total_spent__: _number_
- __total_available__: _number_
- __unpaid_invoices__: _number_
- __operations__: ARRAY of: _[UserOperation](#UserOperation)_
### AppUser
- __identifier__: _string_
- __info__: _[UserInfo](#UserInfo)_
- __max_withdrawable__: _number_
### ChainBalanceEvent
- __block_height__: _number_
- __confirmed_balance__: _number_
- __unconfirmed_balance__: _number_
- __total_balance__: _number_
### SendAppUserToAppUserPaymentRequest
- __from_user_identifier__: _string_
- __to_user_identifier__: _string_
### AuthAppRequest
- __name__: _string_
- __allow_user_creation__: _boolean_ *this field is optional
### SetMockAppUserBalanceRequest
- __user_identifier__: _string_
- __amount__: _number_
### NewAddressResponse
- __address__: _string_
### DecodeInvoiceRequest
- __invoice__: _string_
### LiveUserOperation
- __operation__: _[UserOperation](#UserOperation)_
### Empty
### ChannelBalanceEvent
- __block_height__: _number_
- __channel_id__: _string_
- __local_balance_sats__: _number_
- __remote_balance_sats__: _number_
### LndMetrics
- __nodes__: ARRAY of: _[LndNodeMetrics](#LndNodeMetrics)_
### LndGetInfoResponse
- __alias__: _string_
### NewInvoiceRequest
- __amountSats__: _number_
- __memo__: _string_
### LnurlWithdrawInfoResponse
- __tag__: _string_
@ -539,91 +679,18 @@ The nostr server will send back a message response, and inside the body there wi
- __balanceCheck__: _string_
- __payLink__: _string_
### HandleLnurlPayResponse
- __pr__: _string_
- __routes__: ARRAY of: _[Empty](#Empty)_
### RelaysMigration
- __relays__: ARRAY of: _string_
### Product
- __id__: _string_
- __name__: _string_
- __price_sats__: _number_
### UsageMetrics
- __metrics__: ARRAY of: _[UsageMetric](#UsageMetric)_
### AppsMetricsRequest
- __from_unix__: _number_ *this field is optional
- __to_unix__: _number_ *this field is optional
- __big_user_sats__: _number_ *this field is optional
- __huge_user_sats__: _number_ *this field is optional
- __include_operations__: _boolean_ *this field is optional
### AppMetrics
- __app_name__: _string_
- __app_id__: _string_
- __app_npub__: _string_
- __app_balance__: _number_
- __total_received__: _number_
- __total_spent__: _number_
- __total_available__: _number_
- __total_users__: _number_
- __total_big_users__: _number_
- __total_huge_users__: _number_
- __unpaid_invoices__: _number_
- __operations__: ARRAY of: _[UserOperation](#UserOperation)_
### AddAppRequest
- __name__: _string_
- __allow_user_creation__: _boolean_
### OpenChannelResponse
- __channelId__: _string_
### PayAddressRequest
- __address__: _string_
- __amoutSats__: _number_
- __satsPerVByte__: _number_
### UserInfo
- __userId__: _string_
- __balance__: _number_
- __max_withdrawable__: _number_
### LiveUserOperation
- __operation__: _[UserOperation](#UserOperation)_
### ClosureMigration
- __closes_at_unix__: _number_
### GetAppUserRequest
- __user_identifier__: _string_
### GetUserOperationsRequest
- __latestIncomingInvoice__: _number_
- __latestOutgoingInvoice__: _number_
- __latestIncomingTx__: _number_
- __latestOutgoingTx__: _number_
- __latestIncomingUserToUserPayment__: _number_
- __latestOutgoingUserToUserPayment__: _number_
### NewInvoiceRequest
- __amountSats__: _number_
- __memo__: _string_
### AddAppInvoiceRequest
- __payer_identifier__: _string_
- __http_callback_url__: _string_
- __invoice_req__: _[NewInvoiceRequest](#NewInvoiceRequest)_
### NewInvoiceResponse
- __invoice__: _string_
### PayInvoiceRequest
- __invoice__: _string_
- __amount__: _number_
### AppsMetricsRequest
- __from_unix__: _number_ *this field is optional
- __to_unix__: _number_ *this field is optional
- __include_operations__: _boolean_ *this field is optional
### LnurlPayInfoResponse
- __tag__: _string_
@ -634,34 +701,6 @@ The nostr server will send back a message response, and inside the body there wi
- __allowsNostr__: _boolean_
- __nostrPubkey__: _string_
### LnurlLinkResponse
- __lnurl__: _string_
- __k1__: _string_
### EncryptionExchangeRequest
- __publicKey__: _string_
- __deviceId__: _string_
### AuthAppRequest
- __name__: _string_
- __allow_user_creation__: _boolean_ *this field is optional
### Application
- __name__: _string_
- __id__: _string_
- __balance__: _number_
- __npub__: _string_
### SetMockAppUserBalanceRequest
- __user_identifier__: _string_
- __amount__: _number_
### PayAddressResponse
- __txId__: _string_
- __operation_id__: _string_
- __service_fee__: _number_
- __network_fee__: _number_
### UserOperation
- __paidAtUnix__: _number_
- __type__: _[UserOperationType](#UserOperationType)_
@ -673,13 +712,20 @@ The nostr server will send back a message response, and inside the body there wi
- __network_fee__: _number_
- __confirmed__: _boolean_
### PayAppUserInvoiceRequest
- __user_identifier__: _string_
### MigrationUpdate
- __closure__: _[ClosureMigration](#ClosureMigration)_ *this field is optional
- __relays__: _[RelaysMigration](#RelaysMigration)_ *this field is optional
### LndGetInfoRequest
- __nodeId__: _number_
### SetMockInvoiceAsPaidRequest
- __invoice__: _string_
- __amount__: _number_
### NewAddressRequest
- __addressType__: _[AddressType](#AddressType)_
### GetAppUserLNURLInfoRequest
- __user_identifier__: _string_
- __base_url_override__: _string_
## Enums
### The enumerators used in the messages

File diff suppressed because it is too large Load diff

View file

@ -139,6 +139,28 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => {
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
})
if (!opts.allowNotImplementedMethods && !methods.GetLndMetrics) throw new Error('method: GetLndMetrics is not implemented')
app.post('/api/admin/metrics/lnd', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'GetLndMetrics', batch: false, nostr: false, batchSize: 0}
const stats: Types.RequestStats = { start:req.startTime || 0n, parse: process.hrtime.bigint(), guard: 0n, validate: 0n, handle: 0n }
let authCtx: Types.AuthContext = {}
try {
if (!methods.GetLndMetrics) throw new Error('method: GetLndMetrics is not implemented')
const authContext = await opts.AdminAuthGuard(req.headers['authorization'])
authCtx = authContext
stats.guard = process.hrtime.bigint()
const request = req.body
const error = Types.LndMetricsRequestValidate(request)
stats.validate = process.hrtime.bigint()
if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authContext }, opts.metricsCallback)
const query = req.query
const params = req.params
const response = await methods.GetLndMetrics({rpcName:'GetLndMetrics', ctx:authContext , req: request})
stats.handle = process.hrtime.bigint()
res.json({status: 'OK', ...response})
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
})
if (!opts.allowNotImplementedMethods && !methods.Health) throw new Error('method: Health is not implemented')
app.get('/api/health', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'Health', batch: false, nostr: false, batchSize: 0}

View file

@ -85,6 +85,20 @@ export default (params: ClientParams) => ({
}
return { status: 'ERROR', reason: 'invalid response' }
},
GetLndMetrics: async (request: Types.LndMetricsRequest): Promise<ResultError | ({ status: 'OK' }& Types.LndMetrics)> => {
const auth = await params.retrieveAdminAuth()
if (auth === null) throw new Error('retrieveAdminAuth() returned null')
let finalRoute = '/api/admin/metrics/lnd'
const { data } = await axios.post(params.baseUrl + finalRoute, request, { headers: { 'authorization': auth } })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') {
const result = data
if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.LndMetricsValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
Health: async (): Promise<ResultError | ({ status: 'OK' })> => {
const auth = await params.retrieveGuestAuth()
if (auth === null) throw new Error('retrieveGuestAuth() returned null')

File diff suppressed because it is too large Load diff

View file

@ -108,6 +108,12 @@ service LightningPub {
option (http_method) = "post";
option (http_route) = "/api/admin/metrics/apps";
}
rpc GetLndMetrics(structs.LndMetricsRequest) returns (structs.LndMetrics) {
option (auth_type) = "Admin";
option (http_method) = "post";
option (http_route) = "/api/admin/metrics/lnd";
}
// </Admin>
// <Guest>

View file

@ -31,26 +31,30 @@ message UsageMetrics {
message AppsMetricsRequest {
optional int64 from_unix = 1;
optional int64 to_unix = 2;
optional int64 big_user_sats = 3;
optional int64 huge_user_sats = 4;
optional bool include_operations = 5;
optional bool include_operations = 3;
}
message UsersInfo {
int64 total = 1;
int64 no_balance = 2;
int64 negative_balance = 3;
int64 always_been_inactive = 4;
int64 balance_avg = 5;
int64 balance_median = 6;
}
message AppMetrics {
string app_name = 1;
string app_id = 2;
string app_npub = 3;
int64 app_balance = 4;
Application app = 1;
UsersInfo users = 2;
int64 total_received = 5;
int64 total_spent = 6;
int64 total_available = 7;
int64 total_users = 8;
int64 total_big_users = 9;
int64 total_huge_users = 10;
int64 unpaid_invoices = 11;
int64 unpaid_invoices = 10;
repeated UserOperation operations = 100;
}
@ -59,6 +63,49 @@ message AppsMetrics {
repeated AppMetrics apps = 1;
}
message LndMetricsRequest {
optional int64 from_unix = 1;
optional int64 to_unix = 2;
}
message RoutingEvent {
int64 incoming_channel_id = 1;
int64 incoming_htlc_id=2;
int64 outgoing_channel_id = 3;
int64 outgoing_htlc_id =4;
int64 timestamp_ns = 5;
string event_type = 6;
int64 incoming_amt_msat = 7;
int64 outgoing_amt_msat = 8;
string failure_string = 9;
bool settled = 10;
bool offchain = 11;
bool forward_fail_event = 12;
}
message ChannelBalanceEvent {
int64 block_height = 1;
string channel_id = 2;
int64 local_balance_sats = 3;
int64 remote_balance_sats = 4;
}
message ChainBalanceEvent {
int64 block_height = 1;
int64 confirmed_balance = 2;
int64 unconfirmed_balance = 3;
int64 total_balance = 4;
}
message LndNodeMetrics {
repeated ChannelBalanceEvent channels_balance_events = 1;
repeated ChainBalanceEvent chain_balance_events = 2;
repeated RoutingEvent routing_events = 3;
}
message LndMetrics {
repeated LndNodeMetrics nodes = 1;
}
message LndGetInfoRequest {
int64 nodeId = 1;
}

View file

@ -5,7 +5,7 @@ import NewLightningHandler, { LightningHandler, LoadLndSettingsFromEnv } from '.
let lnd: LightningHandler
export const ignore = true
export const setup = async () => {
lnd = NewLightningHandler(LoadLndSettingsFromEnv(true), console.log, console.log, console.log)
lnd = NewLightningHandler(LoadLndSettingsFromEnv(true), console.log, console.log, console.log, console.log)
await lnd.Warmup()
}
export const teardown = () => {

View file

@ -1,7 +1,7 @@
import * as Types from '../../../proto/autogenerated/ts/types.js'
import { GetInfoResponse, NewAddressResponse, AddInvoiceResponse, PayReq, Payment, SendCoinsResponse, EstimateFeeResponse, TransactionDetails } from '../../../proto/lnd/lightning.js'
import { EnvMustBeNonEmptyString, EnvMustBeInteger, EnvCanBeBoolean } from '../helpers/envParser.js'
import { AddressPaidCb, DecodedInvoice, Invoice, InvoicePaidCb, LndSettings, NewBlockCb, NodeInfo, PaidInvoice } from './settings.js'
import { AddressPaidCb, BalanceInfo, DecodedInvoice, HtlcCb, Invoice, InvoicePaidCb, LndSettings, NewBlockCb, NodeInfo, PaidInvoice } from './settings.js'
import LND from './lnd.js'
import MockLnd from './mock.js'
import { getLogger } from '../helpers/logger.js'
@ -31,14 +31,15 @@ export interface LightningHandler {
SetMockInvoiceAsPaid(invoice: string, amount: number): Promise<void>
ChannelBalance(): Promise<{ local: number, remote: number }>
GetTransactions(startHeight: number): Promise<TransactionDetails>
GetBalance(): Promise<BalanceInfo>
}
export default (settings: LndSettings, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb, newBlockCb: NewBlockCb): LightningHandler => {
export default (settings: LndSettings, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb, newBlockCb: NewBlockCb, htlcCb: HtlcCb): LightningHandler => {
if (settings.mockLnd) {
getLogger({})("registering mock lnd handler")
return new MockLnd(settings, addressPaidCb, invoicePaidCb, newBlockCb)
} else {
getLogger({})("registering prod lnd handler")
return new LND(settings, addressPaidCb, invoicePaidCb, newBlockCb)
return new LND(settings, addressPaidCb, invoicePaidCb, newBlockCb, htlcCb)
}
}

View file

@ -13,8 +13,9 @@ import { OpenChannelReq } from './openChannelReq.js';
import { AddInvoiceReq } from './addInvoiceReq.js';
import { PayInvoiceReq } from './payInvoiceReq.js';
import { SendCoinsReq } from './sendCoinsReq.js';
import { LndSettings, AddressPaidCb, InvoicePaidCb, NodeInfo, Invoice, DecodedInvoice, PaidInvoice, NewBlockCb } from './settings.js';
import { LndSettings, AddressPaidCb, InvoicePaidCb, NodeInfo, Invoice, DecodedInvoice, PaidInvoice, NewBlockCb, HtlcCb, BalanceInfo } from './settings.js';
import { getLogger } from '../helpers/logger.js';
import { HtlcEvent_EventType } from '../../../proto/lnd/router.js';
const DeadLineMetadata = (deadline = 10 * 1000) => ({ deadline: Date.now() + deadline })
const deadLndRetrySeconds = 5
export default class {
@ -30,12 +31,14 @@ export default class {
addressPaidCb: AddressPaidCb
invoicePaidCb: InvoicePaidCb
newBlockCb: NewBlockCb
htlcCb: HtlcCb
log = getLogger({})
constructor(settings: LndSettings, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb, newBlockCb: NewBlockCb) {
constructor(settings: LndSettings, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb, newBlockCb: NewBlockCb, htlcCb: HtlcCb) {
this.settings = settings
this.addressPaidCb = addressPaidCb
this.invoicePaidCb = invoicePaidCb
this.newBlockCb = newBlockCb
this.htlcCb = htlcCb
const { lndAddr, lndCertPath, lndMacaroonPath } = this.settings
const lndCert = fs.readFileSync(lndCertPath);
const macaroon = fs.readFileSync(lndMacaroonPath).toString('hex');
@ -67,6 +70,7 @@ export default class {
this.SubscribeAddressPaid()
this.SubscribeInvoicePaid()
this.SubscribeNewBlock()
this.SubscribeHtlcEvents()
this.ready = true
}
@ -102,6 +106,19 @@ export default class {
}, deadLndRetrySeconds * 1000)
}
async SubscribeHtlcEvents() {
const stream = this.router.subscribeHtlcEvents({}, { abort: this.abortController.signal })
stream.responses.onMessage(htlc => {
this.htlcCb(htlc)
})
stream.responses.onError(error => {
this.log("Error with subscribeHtlcEvents stream")
})
stream.responses.onComplete(() => {
this.log("subscribeHtlcEvents stream closed")
})
}
async SubscribeNewBlock() {
const { blockHeight } = await this.GetInfo()
const stream = this.chainNotifier.registerBlockEpochNtfn({ height: blockHeight, hash: Buffer.alloc(0) }, { abort: this.abortController.signal })
@ -259,6 +276,20 @@ export default class {
return res.response
}
async GetBalance(): Promise<BalanceInfo> {
const wRes = await this.lightning.walletBalance({}, DeadLineMetadata())
const { confirmedBalance, unconfirmedBalance, totalBalance } = wRes.response
const { response } = await this.lightning.listChannels({
activeOnly: false, inactiveOnly: false, privateOnly: false, publicOnly: false, peer: Buffer.alloc(0)
}, DeadLineMetadata())
const channelsBalance = response.channels.map(c => ({
channelId: c.chanId,
localBalanceSats: Number(c.localBalance),
remoteBalanceSats: Number(c.remoteBalance)
}))
return { confirmedBalance: Number(confirmedBalance), unconfirmedBalance: Number(unconfirmedBalance), totalBalance: Number(totalBalance), channelsBalance }
}
async OpenChannel(destination: string, closeAddress: string, fundingAmount: number, pushSats: number): Promise<string> {
await this.Health()

View file

@ -12,7 +12,7 @@ import { OpenChannelReq } from './openChannelReq.js';
import { AddInvoiceReq } from './addInvoiceReq.js';
import { PayInvoiceReq } from './payInvoiceReq.js';
import { SendCoinsReq } from './sendCoinsReq.js';
import { LndSettings, AddressPaidCb, InvoicePaidCb, NodeInfo, Invoice, DecodedInvoice, PaidInvoice, NewBlockCb } from './settings.js';
import { LndSettings, AddressPaidCb, InvoicePaidCb, NodeInfo, Invoice, DecodedInvoice, PaidInvoice, NewBlockCb, BalanceInfo } from './settings.js';
import { getLogger } from '../helpers/logger.js';
export default class {
@ -115,6 +115,10 @@ export default class {
async GetTransactions(startHeight: number): Promise<TransactionDetails> {
throw new Error("GetTransactions disabled in mock mode")
}
GetBalance(): Promise<BalanceInfo> {
throw new Error("GetBalance disabled in mock mode")
}
}

View file

@ -1,3 +1,5 @@
import { HtlcEvent } from "../../../proto/lnd/router"
export type LndSettings = {
lndAddr: string
lndCertPath: string
@ -10,10 +12,21 @@ type TxOutput = {
hash: string
index: number
}
export type BalanceInfo = {
confirmedBalance: number;
unconfirmedBalance: number;
totalBalance: number;
channelsBalance: {
channelId: string;
localBalanceSats: number;
remoteBalanceSats: number;
}[];
}
export type AddressPaidCb = (txOutput: TxOutput, address: string, amount: number, internal: boolean) => void
export type InvoicePaidCb = (paymentRequest: string, amount: number, internal: boolean) => void
export type NewBlockCb = (height: number) => void
export type HtlcCb = (event: HtlcEvent) => void
export type NodeInfo = {
alias: string

View file

@ -59,7 +59,7 @@ export default class {
this.settings = settings
this.storage = new Storage(settings.storageSettings)
this.metricsManager = new MetricsManager(this.storage)
this.lnd = NewLightningHandler(settings.lndSettings, this.addressPaidCb, this.invoicePaidCb, this.newBlockCb)
this.lnd = NewLightningHandler(settings.lndSettings, this.addressPaidCb, this.invoicePaidCb, this.newBlockCb, this.metricsManager.HtlcCb)
this.paymentManager = new PaymentManager(this.storage, this.lnd, this.settings, this.addressPaidCb, this.invoicePaidCb)
this.productManager = new ProductManager(this.storage, this.paymentManager, this.settings)
@ -78,10 +78,13 @@ export default class {
NewBlockHandler = async (height: number) => {
let confirmed: (PendingTx & { confs: number; })[]
let log = getLogger({})
try {
const balanceEvents = await this.paymentManager.GetLndBalance()
await this.metricsManager.NewBlockCb(height, balanceEvents)
confirmed = await this.paymentManager.CheckPendingTransactions(height)
} catch (err: any) {
log("failed to check transactions after new block", err)
log("failed to check transactions after new block", err.message || err)
return
}
await Promise.all(confirmed.map(async c => {

View file

@ -507,6 +507,10 @@ export default class {
return resolved.filter(t => t !== undefined) as (PendingTx & { confs: number })[]
}
async GetLndBalance() {
return this.lnd.GetBalance()
}
encodeLnurl(base: string) {
if (!base || typeof base !== 'string') {
throw new Error("provided string for lnurl encode is not a string or is an empty string")

View file

@ -1,6 +1,11 @@
import Storage from '../storage/index.js'
import * as Types from '../../../proto/autogenerated/ts/types.js'
import { Application } from '../storage/entity/Application.js'
import { HtlcEvent, HtlcEvent_EventType } from '../../../proto/lnd/router.js'
import { RoutingEvent } from '../storage/entity/RoutingEvent.js'
import { BalanceInfo } from '../lnd/settings.js'
import { BalanceEvent } from '../storage/entity/BalanceEvent.js'
import { ChannelBalanceEvent } from '../storage/entity/ChannelsBalanceEvent.js'
const maxEvents = 100_000
export default class Handler {
storage: Storage
@ -8,6 +13,50 @@ export default class Handler {
constructor(storage: Storage) {
this.storage = storage
}
async HtlcCb(htlc: HtlcEvent) {
const routingEvent: Partial<RoutingEvent> = {}
routingEvent.event_type = HtlcEvent_EventType[htlc.eventType]
routingEvent.incoming_channel_id = Number(htlc.incomingChannelId)
routingEvent.incoming_htlc_id = Number(htlc.incomingHtlcId)
routingEvent.outgoing_channel_id = Number(htlc.outgoingChannelId)
routingEvent.outgoing_htlc_id = Number(htlc.outgoingHtlcId)
routingEvent.timestamp_ns = Number(htlc.timestampNs)
if (htlc.event.oneofKind === 'finalHtlcEvent') {
routingEvent.offchain = htlc.event.finalHtlcEvent.offchain
routingEvent.settled = htlc.event.finalHtlcEvent.settled
} else if (htlc.event.oneofKind === 'forwardEvent') {
const { info } = htlc.event.forwardEvent
routingEvent.incoming_amt_msat = info ? Number(info.incomingAmtMsat) : undefined
routingEvent.outgoing_amt_msat = info ? Number(info.outgoingAmtMsat) : undefined
} else if (htlc.event.oneofKind === 'settleEvent') {
} else if (htlc.event.oneofKind === 'subscribedEvent') {
} else if (htlc.event.oneofKind === 'forwardFailEvent') {
routingEvent.forward_fail_event = true
} else if (htlc.event.oneofKind === 'linkFailEvent') {
routingEvent.failure_string = htlc.event.linkFailEvent.failureString
const { info } = htlc.event.linkFailEvent
routingEvent.incoming_amt_msat = info ? Number(info.incomingAmtMsat) : undefined
routingEvent.outgoing_amt_msat = info ? Number(info.outgoingAmtMsat) : undefined
}
await this.storage.metricsStorage.SaveRoutingEvent(routingEvent)
}
async NewBlockCb(height: number, balanceInfo: BalanceInfo) {
const balanceEvent: Partial<BalanceEvent> = {
block_height: height,
confirmed_chain_balance: balanceInfo.confirmedBalance,
unconfirmed_chain_balance: balanceInfo.unconfirmedBalance,
total_chain_balance: balanceInfo.totalBalance,
}
const channelsEvents: Partial<ChannelBalanceEvent>[] = balanceInfo.channelsBalance.map(c => ({
channel_id: c.channelId,
local_balance_sats: c.localBalanceSats,
remote_balance_sats: c.remoteBalanceSats,
}))
await this.storage.metricsStorage.SaveBalanceEvents(balanceEvent, channelsEvents)
}
AddMetrics(newMetrics: (Types.RequestMetric & { app_id?: string })[]) {
const parsed: Types.UsageMetric[] = newMetrics.map(m => ({
rpc_name: m.rpcName,
@ -44,13 +93,8 @@ export default class Handler {
async GetAppMetrics(req: Types.AppsMetricsRequest, app: Application | null): Promise<Types.AppMetrics> {
const { receivingInvoices, receivingTransactions, outgoingInvoices, outgoingTransactions, receivingAddresses, userToUser } = await this.storage.paymentStorage.GetAppOperations(app, { from: req.from_unix, to: req.to_unix })
const bigUser = req.big_user_sats ? req.big_user_sats : 10_000
const hugeUser = req.big_user_sats ? req.big_user_sats : 500_000
let totalReceived = 0
let totalSpent = 0
let totalAvailable = 0
let totalBigUsers = 0
let totalHugeUsers = 0
let unpaidInvoices = 0
const operations: Types.UserOperation[] = []
receivingInvoices.forEach(i => {
@ -84,32 +128,93 @@ export default class Handler {
const users = await this.storage.applicationStorage.GetApplicationUsers(app, { from: req.from_unix, to: req.to_unix })
users.forEach(u => {
totalAvailable += u.user.balance_sats
if (u.user.balance_sats > bigUser) {
totalBigUsers++
let totalUserWithBalance = 0
let totalUserWithNoBalance = 0
let totalUsersWithNegativeBalance = 0
let totalAlwaysBeenInactive = 0
let balanceSum = 0
let minBalance = Number.MAX_SAFE_INTEGER
let maxBalance = 0
await Promise.all(users.map(async u => {
if (u.user.balance_sats < 0) {
totalUsersWithNegativeBalance++
} else if (u.user.balance_sats === 0) {
const wasActive = await this.storage.paymentStorage.UserHasOutgoingOperation(u.user.user_id)
totalUserWithNoBalance++
if (!wasActive) {
totalAlwaysBeenInactive++
}
if (u.user.balance_sats > hugeUser) {
totalHugeUsers++
} else {
balanceSum += u.user.balance_sats
totalUserWithBalance++
if (u.user.balance_sats < minBalance) {
minBalance = u.user.balance_sats
}
})
if (u.user.balance_sats > maxBalance) {
maxBalance = u.user.balance_sats
}
}
}))
return {
app_name: app ? app.name : "unlinked to app",
app_id: app ? app.app_id : "unlinked",
app_npub: app ? (app.nostr_public_key || "") : "",
app_balance: app ? app.owner.balance_sats : 0,
app: {
name: app ? app.name : "unlinked to app",
id: app ? app.app_id : "unlinked",
npub: app ? (app.nostr_public_key || "") : "",
balance: app ? app.owner.balance_sats : 0,
},
users: {
total: users.length,
always_been_inactive: totalAlwaysBeenInactive,
balance_avg: Math.round(balanceSum / totalUserWithBalance),
balance_median: Math.round((maxBalance + minBalance) / 2),
no_balance: totalUserWithNoBalance,
negative_balance: totalUsersWithNegativeBalance,
},
total_received: totalReceived,
total_spent: totalSpent,
total_available: totalAvailable,
total_users: users.length,
total_big_users: totalBigUsers,
total_huge_users: totalHugeUsers,
total_available: balanceSum,
unpaid_invoices: unpaidInvoices,
operations
}
}
async GetLndMetrics(req: Types.LndMetricsRequest): Promise<Types.LndMetrics> {
const routingEvents = await this.storage.metricsStorage.GetRoutingEvents({ from: req.from_unix, to: req.to_unix })
const { channelsBalanceEvents, chainBalanceEvents } = await this.storage.metricsStorage.GetBalanceEvents({ from: req.from_unix, to: req.to_unix })
return {
nodes: [{
chain_balance_events: chainBalanceEvents.map(e => ({
block_height: e.block_height,
confirmed_balance: e.confirmed_chain_balance,
unconfirmed_balance: e.unconfirmed_chain_balance,
total_balance: e.total_chain_balance
})),
channels_balance_events: channelsBalanceEvents.map(e => ({
block_height: e.balance_event.block_height,
channel_id: e.channel_id,
local_balance_sats: e.local_balance_sats,
remote_balance_sats: e.remote_balance_sats
})),
routing_events: routingEvents.map(e => ({
event_type: e.event_type,
failure_string: e.failure_string || "",
forward_fail_event: e.forward_fail_event || false,
incoming_amt_msat: e.incoming_amt_msat || 0,
incoming_channel_id: e.incoming_channel_id || 0,
incoming_htlc_id: e.incoming_htlc_id || 0,
offchain: e.offchain || false,
outgoing_amt_msat: e.outgoing_amt_msat || 0,
outgoing_channel_id: e.outgoing_channel_id,
outgoing_htlc_id: e.outgoing_htlc_id,
settled: e.settled || false,
timestamp_ns: e.timestamp_ns
}))
}]
}
}
}

View file

@ -10,6 +10,9 @@ export default (mainHandler: Main): Types.ServerMethods => {
GetAppsMetrics: async ({ ctx, req }) => {
return mainHandler.metricsManager.GetAppsMetrics(req)
},
GetLndMetrics: async ({ ctx, req }) => {
return mainHandler.metricsManager.GetLndMetrics(req)
},
EncryptionExchange: async () => { },
Health: async () => { await mainHandler.lnd.Health() },
LndGetInfo: async ({ ctx }) => {

View file

@ -13,6 +13,9 @@ import { Product } from "./entity/Product.js"
import { UserToUserPayment } from "./entity/UserToUserPayment.js"
import { Application } from "./entity/Application.js"
import { ApplicationUser } from "./entity/ApplicationUser.js"
import { RoutingEvent } from "./entity/RoutingEvent.js"
import { BalanceEvent } from "./entity/BalanceEvent.js"
import { ChannelBalanceEvent } from "./entity/ChannelsBalanceEvent.js"
import { getLogger } from "../helpers/logger.js"
export type DbSettings = {
databaseFile: string
@ -29,7 +32,8 @@ export default async (settings: DbSettings) => {
type: "sqlite",
database: settings.databaseFile,
// logging: true,
entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment, UserBasicAuth, UserEphemeralKey, Product, UserToUserPayment, Application, ApplicationUser, UserToUserPayment],
entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment,
UserBasicAuth, UserEphemeralKey, Product, UserToUserPayment, Application, ApplicationUser, UserToUserPayment, RoutingEvent, BalanceEvent, ChannelBalanceEvent],
// synchronize: true,
}).initialize()
const log = getLogger({})

View file

@ -0,0 +1,25 @@
import { Entity, PrimaryGeneratedColumn, Column, Index, Check, CreateDateColumn, UpdateDateColumn } from "typeorm"
@Entity()
export class BalanceEvent {
@PrimaryGeneratedColumn()
serial_id: number
@Column()
block_height: number
@Column()
confirmed_chain_balance: number
@Column()
unconfirmed_chain_balance: number
@Column()
total_chain_balance: number
@CreateDateColumn()
created_at: Date
@UpdateDateColumn()
updated_at: Date
}

View file

@ -0,0 +1,27 @@
import { Entity, PrimaryGeneratedColumn, Column, Index, Check, CreateDateColumn, UpdateDateColumn, ManyToOne, JoinColumn } from "typeorm"
import { BalanceEvent } from "./BalanceEvent.js"
@Entity()
export class ChannelBalanceEvent {
@PrimaryGeneratedColumn()
serial_id: number
@ManyToOne(type => BalanceEvent, { eager: true })
@JoinColumn()
balance_event: BalanceEvent
@Column()
channel_id: string
@Column()
local_balance_sats: number
@Column()
remote_balance_sats: number
@CreateDateColumn()
created_at: Date
@UpdateDateColumn()
updated_at: Date
}

View file

@ -0,0 +1,49 @@
import { Entity, PrimaryGeneratedColumn, Column, Index, Check, CreateDateColumn, UpdateDateColumn } from "typeorm"
@Entity()
export class RoutingEvent {
@PrimaryGeneratedColumn()
serial_id: number
@Column()
incoming_channel_id: number
@Column()
incoming_htlc_id: number
@Column()
outgoing_channel_id: number
@Column()
outgoing_htlc_id: number
@Column()
timestamp_ns: number
@Column()
event_type: string
@Column({ nullable: true })
incoming_amt_msat?: number
@Column({ nullable: true })
outgoing_amt_msat?: number
@Column({ nullable: true })
failure_string?: string
@Column({ nullable: true })
settled?: boolean
@Column({ nullable: true })
offchain?: boolean
@Column({ nullable: true })
forward_fail_event?: boolean
@CreateDateColumn()
created_at: Date
@UpdateDateColumn()
updated_at: Date
}

View file

@ -4,6 +4,7 @@ import ProductStorage from './productStorage.js'
import ApplicationStorage from './applicationStorage.js'
import UserStorage from "./userStorage.js";
import PaymentStorage from "./paymentStorage.js";
import MetricsStorage from "./metricsStorage.js";
export type StorageSettings = {
dbSettings: DbSettings
}
@ -19,6 +20,7 @@ export default class {
applicationStorage: ApplicationStorage
userStorage: UserStorage
paymentStorage: PaymentStorage
metricsStorage: MetricsStorage
pendingTx: boolean
transactionsQueue: { exec: TX, res: () => void, rej: (message: string) => void }[] = []
constructor(settings: StorageSettings) {
@ -30,6 +32,7 @@ export default class {
this.productStorage = new ProductStorage(this.DB)
this.applicationStorage = new ApplicationStorage(this.DB, this.userStorage)
this.paymentStorage = new PaymentStorage(this.DB, this.userStorage)
this.metricsStorage = new MetricsStorage(this.DB)
}
StartTransaction(exec: TX) {

View file

@ -0,0 +1,51 @@
import { Between, DataSource, EntityManager, FindOperator, LessThanOrEqual, MoreThanOrEqual } from "typeorm"
import { RoutingEvent } from "./entity/RoutingEvent.js"
import { BalanceEvent } from "./entity/BalanceEvent.js"
import { ChannelBalanceEvent } from "./entity/ChannelsBalanceEvent.js"
export default class {
DB: DataSource | EntityManager
constructor(DB: DataSource | EntityManager) {
this.DB = DB
}
async SaveRoutingEvent(event: Partial<RoutingEvent>, entityManager = this.DB) {
const entry = entityManager.getRepository(RoutingEvent).create(event)
return entityManager.getRepository(RoutingEvent).save(entry)
}
async SaveBalanceEvents(balanceEvent: Partial<BalanceEvent>, channelBalanceEvents: Partial<ChannelBalanceEvent>[], entityManager = this.DB) {
const blanceEventEntry = entityManager.getRepository(BalanceEvent).create(balanceEvent)
const balanceEntry = await entityManager.getRepository(BalanceEvent).save(blanceEventEntry)
const channelsEntry = entityManager.getRepository(ChannelBalanceEvent).create(channelBalanceEvents.map(e => ({ ...e, balance_event: balanceEntry })))
const channelsEntries = await entityManager.getRepository(ChannelBalanceEvent).save(channelsEntry)
return { balanceEntry, channelsEntries }
}
async GetRoutingEvents({ from, to }: { from?: number, to?: number }, entityManager = this.DB) {
let q: { where: { created_at: FindOperator<Date> } } | {} = {}
if (!!from && !!to) {
q = { where: { created_at: Between<Date>(new Date(from * 1000), new Date(to * 1000)) } }
} else if (!!from) {
q = { where: { created_at: MoreThanOrEqual<Date>(new Date(from * 1000)) } }
} else if (!!to) {
q = { where: { created_at: LessThanOrEqual<Date>(new Date(to * 1000)) } }
}
return entityManager.getRepository(RoutingEvent).find(q)
}
async GetBalanceEvents({ from, to }: { from?: number, to?: number }, entityManager = this.DB) {
let q: { where: { created_at: FindOperator<Date> } } | {} = {}
if (!!from && !!to) {
q = { where: { created_at: Between<Date>(new Date(from * 1000), new Date(to * 1000)) } }
} else if (!!from) {
q = { where: { created_at: MoreThanOrEqual<Date>(new Date(from * 1000)) } }
} else if (!!to) {
q = { where: { created_at: LessThanOrEqual<Date>(new Date(to * 1000)) } }
}
const [chainBalanceEvents, channelsBalanceEvents] = await Promise.all([
entityManager.getRepository(BalanceEvent).find(q),
entityManager.getRepository(ChannelBalanceEvent).find(q),
])
return { chainBalanceEvents, channelsBalanceEvents }
}
}

View file

@ -289,4 +289,12 @@ export default class {
}
}
async UserHasOutgoingOperation(userId: string, entityManager = this.DB) {
const [i, tx, u2u] = await Promise.all([
entityManager.getRepository(UserInvoicePayment).findOne({ where: { user: { user_id: userId } } }),
entityManager.getRepository(UserTransactionPayment).findOne({ where: { user: { user_id: userId } } }),
entityManager.getRepository(UserToUserPayment).findOne({ where: { from_user: { user_id: userId } } }),
])
return !!i || !!tx || !!u2u
}
}