Merge branch 'master' into clink-manage

This commit is contained in:
boufni95 2025-06-30 18:39:17 +00:00
commit 819cf74d2f
24 changed files with 417 additions and 34 deletions

View file

@ -3,6 +3,7 @@ import { BalanceEvent } from "./build/src/services/storage/entity/BalanceEvent.j
import { ChannelBalanceEvent } from "./build/src/services/storage/entity/ChannelsBalanceEvent.js"
import { ChannelRouting } from "./build/src/services/storage/entity/ChannelRouting.js"
import { RootOperation } from "./build/src/services/storage/entity/RootOperation.js"
import { ChannelEvent } from "./build/src/services/storage/entity/ChannelEvent.js"
import { LndMetrics1703170330183 } from './build/src/services/storage/migrations/1703170330183-lnd_metrics.js'
import { ChannelRouting1709316653538 } from './build/src/services/storage/migrations/1709316653538-channel_routing.js'
import { HtlcCount1724266887195 } from './build/src/services/storage/migrations/1724266887195-htlc_count.js'
@ -11,8 +12,8 @@ import { BalanceEvents1724860966825 } from './build/src/services/storage/migrati
export default new DataSource({
type: "sqlite",
database: "metrics.sqlite",
entities: [BalanceEvent, ChannelBalanceEvent, ChannelRouting, RootOperation],
entities: [BalanceEvent, ChannelBalanceEvent, ChannelRouting, RootOperation, ChannelEvent],
migrations: [LndMetrics1703170330183, ChannelRouting1709316653538, HtlcCount1724266887195, BalanceEvents1724860966825]
});
//npx typeorm migration:generate ./src/services/storage/migrations/root_ops_time -d ./metricsDatasource.js
//npx typeorm migration:generate ./src/services/storage/migrations/channel_events -d ./metricsDatasource.js

View file

@ -133,6 +133,11 @@ The nostr server will send back a message response, and inside the body there wi
- This methods has an __empty__ __request__ body
- output: [LiveUserOperation](#LiveUserOperation)
- GetLndForwardingMetrics
- auth type: __Metrics__
- input: [LndMetricsRequest](#LndMetricsRequest)
- output: [LndForwardingMetrics](#LndForwardingMetrics)
- GetLndMetrics
- auth type: __Metrics__
- input: [LndMetricsRequest](#LndMetricsRequest)
@ -567,6 +572,13 @@ The nostr server will send back a message response, and inside the body there wi
- This methods has an __empty__ __request__ body
- output: [LiveUserOperation](#LiveUserOperation)
- GetLndForwardingMetrics
- auth type: __Metrics__
- http method: __post__
- http route: __/api/reports/lnd/forwarding__
- input: [LndMetricsRequest](#LndMetricsRequest)
- output: [LndForwardingMetrics](#LndForwardingMetrics)
- GetLndMetrics
- auth type: __Metrics__
- http method: __post__
@ -1210,6 +1222,18 @@ The nostr server will send back a message response, and inside the body there wi
### LndChannels
- __open_channels__: ARRAY of: _[OpenChannel](#OpenChannel)_
### LndForwardingEvent
- __amt_in__: _number_
- __amt_out__: _number_
- __at_unix__: _number_
- __chan_id_in__: _string_
- __chan_id_out__: _string_
- __fee__: _number_
### LndForwardingMetrics
- __events__: ARRAY of: _[LndForwardingEvent](#LndForwardingEvent)_
- __total_fees__: _number_
### LndGetInfoRequest
- __nodeId__: _number_
@ -1316,6 +1340,7 @@ The nostr server will send back a message response, and inside the body there wi
- __capacity__: _number_
- __channel_id__: _string_
- __channel_point__: _string_
- __inactive_since_unix__: _number_
- __label__: _string_
- __lifetime__: _number_
- __local_balance__: _number_

View file

@ -85,6 +85,7 @@ type Client struct {
GetLNURLChannelLink func() (*LnurlLinkResponse, error)
GetLiveDebitRequests func() (*LiveDebitRequest, error)
GetLiveUserOperations func() (*LiveUserOperation, error)
GetLndForwardingMetrics func(req LndMetricsRequest) (*LndForwardingMetrics, error)
GetLndMetrics func(req LndMetricsRequest) (*LndMetrics, error)
GetLnurlPayInfo func(query GetLnurlPayInfo_Query) (*LnurlPayInfoResponse, error)
GetLnurlPayLink func() (*LnurlLinkResponse, error)
@ -906,6 +907,35 @@ func NewClient(params ClientParams) *Client {
},
// server streaming method: GetLiveDebitRequests not implemented
// server streaming method: GetLiveUserOperations not implemented
GetLndForwardingMetrics: func(req LndMetricsRequest) (*LndForwardingMetrics, error) {
auth, err := params.RetrieveMetricsAuth()
if err != nil {
return nil, err
}
finalRoute := "/api/reports/lnd/forwarding"
body, err := json.Marshal(req)
if err != nil {
return nil, err
}
resBody, err := doPostRequest(params.BaseURL+finalRoute, body, auth)
if err != nil {
return nil, err
}
result := ResultError{}
err = json.Unmarshal(resBody, &result)
if err != nil {
return nil, err
}
if result.Status == "ERROR" {
return nil, fmt.Errorf(result.Reason)
}
res := LndForwardingMetrics{}
err = json.Unmarshal(resBody, &res)
if err != nil {
return nil, err
}
return &res, nil
},
GetLndMetrics: func(req LndMetricsRequest) (*LndMetrics, error) {
auth, err := params.RetrieveMetricsAuth()
if err != nil {

View file

@ -361,6 +361,18 @@ type LiveUserOperation struct {
type LndChannels struct {
Open_channels []OpenChannel `json:"open_channels"`
}
type LndForwardingEvent struct {
Amt_in int64 `json:"amt_in"`
Amt_out int64 `json:"amt_out"`
At_unix int64 `json:"at_unix"`
Chan_id_in string `json:"chan_id_in"`
Chan_id_out string `json:"chan_id_out"`
Fee int64 `json:"fee"`
}
type LndForwardingMetrics struct {
Events []LndForwardingEvent `json:"events"`
Total_fees int64 `json:"total_fees"`
}
type LndGetInfoRequest struct {
Nodeid int64 `json:"nodeId"`
}
@ -467,6 +479,7 @@ type OpenChannel struct {
Capacity int64 `json:"capacity"`
Channel_id string `json:"channel_id"`
Channel_point string `json:"channel_point"`
Inactive_since_unix int64 `json:"inactive_since_unix"`
Label string `json:"label"`
Lifetime int64 `json:"lifetime"`
Local_balance int64 `json:"local_balance"`

View file

@ -996,6 +996,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.GetLndForwardingMetrics) throw new Error('method: GetLndForwardingMetrics is not implemented')
app.post('/api/reports/lnd/forwarding', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'GetLndForwardingMetrics', batch: false, nostr: false, batchSize: 0}
const stats: Types.RequestStats = { startMs:req.startTimeMs || 0, start:req.startTime || 0n, parse: process.hrtime.bigint(), guard: 0n, validate: 0n, handle: 0n }
let authCtx: Types.AuthContext = {}
try {
if (!methods.GetLndForwardingMetrics) throw new Error('method: GetLndForwardingMetrics is not implemented')
const authContext = await opts.MetricsAuthGuard(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.GetLndForwardingMetrics({rpcName:'GetLndForwardingMetrics', 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.GetLndMetrics) throw new Error('method: GetLndMetrics is not implemented')
app.post('/api/reports/lnd', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'GetLndMetrics', batch: false, nostr: false, batchSize: 0}

View file

@ -404,6 +404,20 @@ export default (params: ClientParams) => ({
},
GetLiveDebitRequests: async (cb: (v:ResultError | ({ status: 'OK' }& Types.LiveDebitRequest)) => void): Promise<void> => { throw new Error('http streams are not supported')},
GetLiveUserOperations: async (cb: (v:ResultError | ({ status: 'OK' }& Types.LiveUserOperation)) => void): Promise<void> => { throw new Error('http streams are not supported')},
GetLndForwardingMetrics: async (request: Types.LndMetricsRequest): Promise<ResultError | ({ status: 'OK' }& Types.LndForwardingMetrics)> => {
const auth = await params.retrieveMetricsAuth()
if (auth === null) throw new Error('retrieveMetricsAuth() returned null')
let finalRoute = '/api/reports/lnd/forwarding'
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.LndForwardingMetricsValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
GetLndMetrics: async (request: Types.LndMetricsRequest): Promise<ResultError | ({ status: 'OK' }& Types.LndMetrics)> => {
const auth = await params.retrieveMetricsAuth()
if (auth === null) throw new Error('retrieveMetricsAuth() returned null')

View file

@ -349,6 +349,21 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ
return cb({ status: 'ERROR', reason: 'invalid response' })
})
},
GetLndForwardingMetrics: async (request: Types.LndMetricsRequest): Promise<ResultError | ({ status: 'OK' }& Types.LndForwardingMetrics)> => {
const auth = await params.retrieveNostrMetricsAuth()
if (auth === null) throw new Error('retrieveNostrMetricsAuth() returned null')
const nostrRequest: NostrRequest = {}
nostrRequest.body = request
const data = await send(params.pubDestination, {rpcName:'GetLndForwardingMetrics',authIdentifier:auth, ...nostrRequest })
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.LndForwardingMetricsValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
GetLndMetrics: async (request: Types.LndMetricsRequest): Promise<ResultError | ({ status: 'OK' }& Types.LndMetrics)> => {
const auth = await params.retrieveNostrMetricsAuth()
if (auth === null) throw new Error('retrieveNostrMetricsAuth() returned null')

View file

@ -741,6 +741,22 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => {
}})
}catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
break
case 'GetLndForwardingMetrics':
try {
if (!methods.GetLndForwardingMetrics) throw new Error('method: GetLndForwardingMetrics is not implemented')
const authContext = await opts.NostrMetricsAuthGuard(req.appId, req.authIdentifier)
stats.guard = process.hrtime.bigint()
authCtx = authContext
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, ...authCtx }, opts.metricsCallback)
const response = await methods.GetLndForwardingMetrics({rpcName:'GetLndForwardingMetrics', ctx:authContext , req: request})
stats.handle = process.hrtime.bigint()
res({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 }
break
case 'GetLndMetrics':
try {
if (!methods.GetLndMetrics) throw new Error('method: GetLndMetrics is not implemented')

View file

@ -28,8 +28,8 @@ export type MetricsContext = {
app_id: string
operator_id: string
}
export type MetricsMethodInputs = GetAppsMetrics_Input | GetBundleMetrics_Input | GetErrorStats_Input | GetLndMetrics_Input | GetProvidersDisruption_Input | GetSingleBundleMetrics_Input | GetSingleUsageMetrics_Input | GetUsageMetrics_Input | PingSubProcesses_Input | ResetMetricsStorages_Input | SubmitWebRtcMessage_Input | ZipMetricsStorages_Input
export type MetricsMethodOutputs = GetAppsMetrics_Output | GetBundleMetrics_Output | GetErrorStats_Output | GetLndMetrics_Output | GetProvidersDisruption_Output | GetSingleBundleMetrics_Output | GetSingleUsageMetrics_Output | GetUsageMetrics_Output | PingSubProcesses_Output | ResetMetricsStorages_Output | SubmitWebRtcMessage_Output | ZipMetricsStorages_Output
export type MetricsMethodInputs = GetAppsMetrics_Input | GetBundleMetrics_Input | GetErrorStats_Input | GetLndForwardingMetrics_Input | GetLndMetrics_Input | GetProvidersDisruption_Input | GetSingleBundleMetrics_Input | GetSingleUsageMetrics_Input | GetUsageMetrics_Input | PingSubProcesses_Input | ResetMetricsStorages_Input | SubmitWebRtcMessage_Input | ZipMetricsStorages_Input
export type MetricsMethodOutputs = GetAppsMetrics_Output | GetBundleMetrics_Output | GetErrorStats_Output | GetLndForwardingMetrics_Output | GetLndMetrics_Output | GetProvidersDisruption_Output | GetSingleBundleMetrics_Output | GetSingleUsageMetrics_Output | GetUsageMetrics_Output | PingSubProcesses_Output | ResetMetricsStorages_Output | SubmitWebRtcMessage_Output | ZipMetricsStorages_Output
export type UserContext = {
app_id: string
app_user_id: string
@ -132,6 +132,9 @@ export type GetLiveDebitRequests_Output = ResultError | { status: 'OK' }
export type GetLiveUserOperations_Input = {rpcName:'GetLiveUserOperations', cb:(res: LiveUserOperation, err:Error|null)=> void}
export type GetLiveUserOperations_Output = ResultError | { status: 'OK' }
export type GetLndForwardingMetrics_Input = {rpcName:'GetLndForwardingMetrics', req: LndMetricsRequest}
export type GetLndForwardingMetrics_Output = ResultError | ({ status: 'OK' } & LndForwardingMetrics)
export type GetLndMetrics_Input = {rpcName:'GetLndMetrics', req: LndMetricsRequest}
export type GetLndMetrics_Output = ResultError | ({ status: 'OK' } & LndMetrics)
@ -338,6 +341,7 @@ export type ServerMethods = {
GetLNURLChannelLink?: (req: GetLNURLChannelLink_Input & {ctx: UserContext }) => Promise<LnurlLinkResponse>
GetLiveDebitRequests?: (req: GetLiveDebitRequests_Input & {ctx: UserContext }) => Promise<void>
GetLiveUserOperations?: (req: GetLiveUserOperations_Input & {ctx: UserContext }) => Promise<void>
GetLndForwardingMetrics?: (req: GetLndForwardingMetrics_Input & {ctx: MetricsContext }) => Promise<LndForwardingMetrics>
GetLndMetrics?: (req: GetLndMetrics_Input & {ctx: MetricsContext }) => Promise<LndMetrics>
GetLnurlPayInfo?: (req: GetLnurlPayInfo_Input & {ctx: GuestContext }) => Promise<LnurlPayInfoResponse>
GetLnurlPayLink?: (req: GetLnurlPayLink_Input & {ctx: UserContext }) => Promise<LnurlLinkResponse>
@ -2049,6 +2053,77 @@ export const LndChannelsValidate = (o?: LndChannels, opts: LndChannelsOptions =
return null
}
export type LndForwardingEvent = {
amt_in: number
amt_out: number
at_unix: number
chan_id_in: string
chan_id_out: string
fee: number
}
export const LndForwardingEventOptionalFields: [] = []
export type LndForwardingEventOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
amt_in_CustomCheck?: (v: number) => boolean
amt_out_CustomCheck?: (v: number) => boolean
at_unix_CustomCheck?: (v: number) => boolean
chan_id_in_CustomCheck?: (v: string) => boolean
chan_id_out_CustomCheck?: (v: string) => boolean
fee_CustomCheck?: (v: number) => boolean
}
export const LndForwardingEventValidate = (o?: LndForwardingEvent, opts: LndForwardingEventOptions = {}, path: string = 'LndForwardingEvent::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 (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null')
if (typeof o.amt_in !== 'number') return new Error(`${path}.amt_in: is not a number`)
if (opts.amt_in_CustomCheck && !opts.amt_in_CustomCheck(o.amt_in)) return new Error(`${path}.amt_in: custom check failed`)
if (typeof o.amt_out !== 'number') return new Error(`${path}.amt_out: is not a number`)
if (opts.amt_out_CustomCheck && !opts.amt_out_CustomCheck(o.amt_out)) return new Error(`${path}.amt_out: custom check failed`)
if (typeof o.at_unix !== 'number') return new Error(`${path}.at_unix: is not a number`)
if (opts.at_unix_CustomCheck && !opts.at_unix_CustomCheck(o.at_unix)) return new Error(`${path}.at_unix: custom check failed`)
if (typeof o.chan_id_in !== 'string') return new Error(`${path}.chan_id_in: is not a string`)
if (opts.chan_id_in_CustomCheck && !opts.chan_id_in_CustomCheck(o.chan_id_in)) return new Error(`${path}.chan_id_in: custom check failed`)
if (typeof o.chan_id_out !== 'string') return new Error(`${path}.chan_id_out: is not a string`)
if (opts.chan_id_out_CustomCheck && !opts.chan_id_out_CustomCheck(o.chan_id_out)) return new Error(`${path}.chan_id_out: custom check failed`)
if (typeof o.fee !== 'number') return new Error(`${path}.fee: is not a number`)
if (opts.fee_CustomCheck && !opts.fee_CustomCheck(o.fee)) return new Error(`${path}.fee: custom check failed`)
return null
}
export type LndForwardingMetrics = {
events: LndForwardingEvent[]
total_fees: number
}
export const LndForwardingMetricsOptionalFields: [] = []
export type LndForwardingMetricsOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
events_ItemOptions?: LndForwardingEventOptions
events_CustomCheck?: (v: LndForwardingEvent[]) => boolean
total_fees_CustomCheck?: (v: number) => boolean
}
export const LndForwardingMetricsValidate = (o?: LndForwardingMetrics, opts: LndForwardingMetricsOptions = {}, path: string = 'LndForwardingMetrics::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 (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null')
if (!Array.isArray(o.events)) return new Error(`${path}.events: is not an array`)
for (let index = 0; index < o.events.length; index++) {
const eventsErr = LndForwardingEventValidate(o.events[index], opts.events_ItemOptions, `${path}.events[${index}]`)
if (eventsErr !== null) return eventsErr
}
if (opts.events_CustomCheck && !opts.events_CustomCheck(o.events)) return new Error(`${path}.events: custom check failed`)
if (typeof o.total_fees !== 'number') return new Error(`${path}.total_fees: is not a number`)
if (opts.total_fees_CustomCheck && !opts.total_fees_CustomCheck(o.total_fees)) return new Error(`${path}.total_fees: custom check failed`)
return null
}
export type LndGetInfoRequest = {
nodeId: number
}
@ -2676,6 +2751,7 @@ export type OpenChannel = {
capacity: number
channel_id: string
channel_point: string
inactive_since_unix: number
label: string
lifetime: number
local_balance: number
@ -2690,6 +2766,7 @@ export type OpenChannelOptions = OptionsBaseMessage & {
capacity_CustomCheck?: (v: number) => boolean
channel_id_CustomCheck?: (v: string) => boolean
channel_point_CustomCheck?: (v: string) => boolean
inactive_since_unix_CustomCheck?: (v: number) => boolean
label_CustomCheck?: (v: string) => boolean
lifetime_CustomCheck?: (v: number) => boolean
local_balance_CustomCheck?: (v: number) => boolean
@ -2712,6 +2789,9 @@ export const OpenChannelValidate = (o?: OpenChannel, opts: OpenChannelOptions =
if (typeof o.channel_point !== 'string') return new Error(`${path}.channel_point: is not a string`)
if (opts.channel_point_CustomCheck && !opts.channel_point_CustomCheck(o.channel_point)) return new Error(`${path}.channel_point: custom check failed`)
if (typeof o.inactive_since_unix !== 'number') return new Error(`${path}.inactive_since_unix: is not a number`)
if (opts.inactive_since_unix_CustomCheck && !opts.inactive_since_unix_CustomCheck(o.inactive_since_unix)) return new Error(`${path}.inactive_since_unix: custom check failed`)
if (typeof o.label !== 'string') return new Error(`${path}.label: is not a string`)
if (opts.label_CustomCheck && !opts.label_CustomCheck(o.label)) return new Error(`${path}.label: custom check failed`)

View file

@ -221,6 +221,15 @@ service LightningPub {
option (http_route) = "/api/reports/lnd";
option (nostr) = true;
}
rpc GetLndForwardingMetrics(structs.LndMetricsRequest) returns (structs.LndForwardingMetrics) {
option (auth_type) = "Metrics";
option (http_method) = "post";
option (http_route) = "/api/reports/lnd/forwarding";
option (nostr) = true;
}
rpc SubmitWebRtcMessage(structs.WebRtcMessage) returns (structs.WebRtcAnswer) {
option (auth_type) = "Metrics";
option (http_method) = "post";

View file

@ -184,12 +184,13 @@ message OpenChannel {
string channel_id = 1;
int64 capacity = 2;
bool active = 3;
int64 lifetime =4 ;
int64 lifetime =4;
int64 local_balance=5;
int64 remote_balance = 6;
string label = 7;
string channel_point = 8;
optional ChannelPolicy policy = 9;
int64 inactive_since_unix = 10;
}
message ClosedChannel {
string channel_id = 1;
@ -213,6 +214,20 @@ message RootOperation {
int64 created_at_unix = 4;
}
message LndForwardingEvent {
string chan_id_in = 1;
string chan_id_out = 2;
int64 amt_in = 3;
int64 amt_out = 4;
int64 fee = 5;
int64 at_unix = 6;
}
message LndForwardingMetrics {
int64 total_fees = 1;
repeated LndForwardingEvent events = 2;
}
message LndNodeMetrics {
repeated GraphPoint chain_balance = 1;
repeated GraphPoint channel_balance = 2;

View file

@ -8,12 +8,12 @@ import { LightningClient } from '../../../proto/lnd/lightning.client.js'
import { InvoicesClient } from '../../../proto/lnd/invoices.client.js'
import { RouterClient } from '../../../proto/lnd/router.client.js'
import { ChainNotifierClient } from '../../../proto/lnd/chainnotifier.client.js'
import { GetInfoResponse, AddressType, NewAddressResponse, AddInvoiceResponse, Invoice_InvoiceState, PayReq, Payment_PaymentStatus, Payment, PaymentFailureReason, SendCoinsResponse, EstimateFeeResponse, ChannelBalanceResponse, TransactionDetails, ListChannelsResponse, ClosedChannelsResponse, PendingChannelsResponse, ForwardingHistoryResponse, CoinSelectionStrategy, OpenStatusUpdate, CloseStatusUpdate, PendingUpdate } from '../../../proto/lnd/lightning.js'
import { GetInfoResponse, AddressType, NewAddressResponse, AddInvoiceResponse, Invoice_InvoiceState, PayReq, Payment_PaymentStatus, Payment, PaymentFailureReason, SendCoinsResponse, EstimateFeeResponse, ChannelBalanceResponse, TransactionDetails, ListChannelsResponse, ClosedChannelsResponse, PendingChannelsResponse, ForwardingHistoryResponse, CoinSelectionStrategy, OpenStatusUpdate, CloseStatusUpdate, PendingUpdate, ChannelEventUpdate_UpdateType } from '../../../proto/lnd/lightning.js'
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, HtlcCb, BalanceInfo } from './settings.js';
import { LndSettings, AddressPaidCb, InvoicePaidCb, NodeInfo, Invoice, DecodedInvoice, PaidInvoice, NewBlockCb, HtlcCb, BalanceInfo, ChannelEventCb } from './settings.js';
import { ERROR, getLogger } from '../helpers/logger.js';
import { HtlcEvent_EventType } from '../../../proto/lnd/router.js';
import { LiquidityProvider, LiquidityRequest } from '../main/liquidityProvider.js';
@ -38,17 +38,19 @@ export default class {
invoicePaidCb: InvoicePaidCb
newBlockCb: NewBlockCb
htlcCb: HtlcCb
channelEventCb: ChannelEventCb
log = getLogger({ component: 'lndManager' })
outgoingOpsLocked = false
liquidProvider: LiquidityProvider
utils: Utils
constructor(settings: LndSettings, liquidProvider: LiquidityProvider, utils: Utils, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb, newBlockCb: NewBlockCb, htlcCb: HtlcCb) {
constructor(settings: LndSettings, liquidProvider: LiquidityProvider, utils: Utils, addressPaidCb: AddressPaidCb, invoicePaidCb: InvoicePaidCb, newBlockCb: NewBlockCb, htlcCb: HtlcCb, channelEventCb: ChannelEventCb) {
this.settings = settings
this.utils = utils
this.addressPaidCb = addressPaidCb
this.invoicePaidCb = invoicePaidCb
this.newBlockCb = newBlockCb
this.htlcCb = htlcCb
this.channelEventCb = channelEventCb
const { lndAddr, lndCertPath, lndMacaroonPath } = this.settings.mainNode
const lndCert = fs.readFileSync(lndCertPath);
const macaroon = fs.readFileSync(lndMacaroonPath).toString('hex');
@ -97,6 +99,7 @@ export default class {
this.SubscribeInvoicePaid()
this.SubscribeNewBlock()
this.SubscribeHtlcEvents()
this.SubscribeChannelEvents()
const now = Date.now()
return new Promise<void>((res, rej) => {
const interval = setInterval(async () => {
@ -168,6 +171,20 @@ export default class {
}, deadLndRetrySeconds * 1000)
}
async SubscribeChannelEvents() {
const stream = this.lightning.subscribeChannelEvents({}, { abort: this.abortController.signal })
stream.responses.onMessage(async channel => {
const channels = await this.ListChannels()
this.channelEventCb(channel, channels.channels)
})
stream.responses.onError(error => {
this.log("Error with subscribeChannelEvents stream")
})
stream.responses.onComplete(() => {
this.log("subscribeChannelEvents stream closed")
})
}
async SubscribeHtlcEvents() {
const stream = this.router.subscribeHtlcEvents({}, { abort: this.abortController.signal })
stream.responses.onMessage(htlc => {
@ -448,8 +465,8 @@ export default class {
return { confirmedBalance: Number(confirmedBalance), unconfirmedBalance: Number(unconfirmedBalance), totalBalance: Number(totalBalance), channelsBalance }
}
async GetForwardingHistory(indexOffset: number, startTime = 0): Promise<ForwardingHistoryResponse> {
const { response } = await this.lightning.forwardingHistory({ indexOffset, numMaxEvents: 0, startTime: BigInt(startTime), endTime: 0n, peerAliasLookup: false }, DeadLineMetadata())
async GetForwardingHistory(indexOffset: number, startTime = 0, endTime = 0): Promise<ForwardingHistoryResponse> {
const { response } = await this.lightning.forwardingHistory({ indexOffset, numMaxEvents: 0, startTime: BigInt(startTime), endTime: BigInt(endTime), peerAliasLookup: false }, DeadLineMetadata())
return response
}

View file

@ -1,3 +1,4 @@
import { Channel, ChannelEventUpdate } from "../../../proto/lnd/lightning"
import { HtlcEvent } from "../../../proto/lnd/router"
export type NodeSettings = {
lndAddr: string
@ -35,6 +36,7 @@ export type AddressPaidCb = (txOutput: TxOutput, address: string, amount: number
export type InvoicePaidCb = (paymentRequest: string, amount: number, used: 'lnd' | 'provider' | 'internal') => Promise<void>
export type NewBlockCb = (height: number) => void
export type HtlcCb = (event: HtlcEvent) => void
export type ChannelEventCb = (event: ChannelEventUpdate, channels: Channel[]) => void
export type NodeInfo = {
alias: string

View file

@ -160,6 +160,7 @@ export class AdminManager {
ListChannels = async (): Promise<Types.LndChannels> => {
const { channels } = await this.lnd.ListChannels(true)
const { identityPubkey } = await this.lnd.GetInfo()
const activity = await this.storage.metricsStorage.GetChannelsActivity()
const openChannels = await Promise.all(channels.map(async c => {
const info = await this.lnd.GetChannelInfo(c.chanId)
const policies = [{ pub: info.node1Pub, policy: info.node1Policy }, { pub: info.node2Pub, policy: info.node2Policy }]
@ -182,6 +183,7 @@ export class AdminManager {
label: c.peerAlias || c.remotePubkey,
lifetime: Number(c.lifetime),
policy,
inactive_since_unix: activity[c.chanId] || 0
}
}))
return {

View file

@ -6,7 +6,7 @@ import ApplicationManager from './applicationManager.js'
import PaymentManager, { PendingTx } from './paymentManager.js'
import { MainSettings } from './settings.js'
import LND from "../lnd/lnd.js"
import { AddressPaidCb, HtlcCb, InvoicePaidCb, NewBlockCb } from "../lnd/settings.js"
import { AddressPaidCb, ChannelEventCb, HtlcCb, InvoicePaidCb, NewBlockCb } from "../lnd/settings.js"
import { ERROR, getLogger, PubLogger } from "../helpers/logger.js"
import AppUserManager from "./appUserManager.js"
import { Application } from '../storage/entity/Application.js'
@ -68,7 +68,7 @@ export default class {
const updateProviderBalance = (b: number) => this.storage.liquidityStorage.IncrementTrackedProviderBalance('lnPub', settings.liquiditySettings.liquidityProviderPub, b)
this.liquidityProvider = new LiquidityProvider(settings.liquiditySettings.liquidityProviderPub, this.utils, this.invoicePaidCb, updateProviderBalance)
this.rugPullTracker = new RugPullTracker(this.storage, this.liquidityProvider)
this.lnd = new LND(settings.lndSettings, this.liquidityProvider, this.utils, this.addressPaidCb, this.invoicePaidCb, this.newBlockCb, this.htlcCb)
this.lnd = new LND(settings.lndSettings, this.liquidityProvider, this.utils, this.addressPaidCb, this.invoicePaidCb, this.newBlockCb, this.htlcCb, this.channelEventCb)
this.liquidityManager = new LiquidityManager(this.settings.liquiditySettings, this.storage, this.utils, this.liquidityProvider, this.lnd, this.rugPullTracker)
this.metricsManager = new MetricsManager(this.storage, this.lnd)
@ -137,6 +137,10 @@ export default class {
}
}
channelEventCb: ChannelEventCb = (e, channels) => {
this.metricsManager.ChannelEventCb(e, channels)
}
htlcCb: HtlcCb = (e) => {
this.metricsManager.HtlcCb(e)
}

View file

@ -3,6 +3,7 @@ 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 { Channel, ChannelEventUpdate, ChannelPoint } from '../../../proto/lnd/lightning.js'
import { BalanceInfo } from '../lnd/settings.js'
import { BalanceEvent } from '../storage/entity/BalanceEvent.js'
import { ChannelBalanceEvent } from '../storage/entity/ChannelsBalanceEvent.js'
@ -13,7 +14,6 @@ import { getLogger } from '../helpers/logger.js'
import { encodeTLV, usageMetricsToTlv } from '../helpers/tlv.js'
import { ChannelCloseSummary_ClosureType } from '../../../proto/lnd/lightning.js'
export default class Handler {
storage: Storage
@ -46,6 +46,29 @@ export default class Handler {
}
}
async ChannelEventCb(event: ChannelEventUpdate, channels: Channel[]) {
if (event.channel.oneofKind === 'inactiveChannel') {
const channel = this.getRelevantChannel(event.channel.inactiveChannel, channels)
if (!channel) {
return
}
await this.storage.metricsStorage.FlagInactiveChannel(channel.chanId)
return
} else if (event.channel.oneofKind === 'activeChannel') {
const channel = this.getRelevantChannel(event.channel.activeChannel, channels)
if (!channel) {
return
}
await this.storage.metricsStorage.FlagActiveChannel(channel.chanId)
return
}
}
getRelevantChannel(c: ChannelPoint, channels: Channel[]) {
const point = `${c.fundingTxid}:${c.outputIndex}`
return channels.find(c => c.channelPoint === point)
}
async HtlcCb(htlc: HtlcEvent) {
@ -309,14 +332,30 @@ export default class Handler {
}
async GetLndForwardingMetrics(req: Types.LndMetricsRequest): Promise<Types.LndForwardingMetrics> {
const fwEvents = await this.lnd.GetForwardingHistory(0, req.from_unix, req.to_unix)
let totalFees = 0
const events: Types.LndForwardingEvent[] = fwEvents.forwardingEvents.map(e => {
totalFees += Number(e.fee)
return {
chan_id_in: e.chanIdIn, chan_id_out: e.chanIdOut, amt_in: Number(e.amtIn), amt_out: Number(e.amtOut), fee: Number(e.fee), at_unix: Number(e.timestampNs)
}
})
return {
total_fees: totalFees,
events: events
}
}
async GetLndMetrics(req: Types.LndMetricsRequest): Promise<Types.LndMetrics> {
const [chansInfo, pendingChansInfo, closedChansInfo, routing, rootOps] = await Promise.all([
const [chansInfo, pendingChansInfo, closedChansInfo, routing, rootOps, channelsActivity] = await Promise.all([
this.GetChannelsInfo(),
this.GetPendingChannelsInfo(),
this.lnd.ListClosedChannels(),
this.storage.metricsStorage.GetChannelRouting({ from: req.from_unix, to: req.to_unix }),
this.storage.metricsStorage.GetRootOperations({ from: req.from_unix, to: req.to_unix })
this.storage.metricsStorage.GetRootOperations({ from: req.from_unix, to: req.to_unix }),
this.storage.metricsStorage.GetChannelsActivity()
])
const { openChannels, totalActive, totalInactive } = chansInfo
const { totalPendingOpen, totalPendingClose } = pendingChansInfo
@ -364,7 +403,7 @@ export default class Handler {
offline_channels: totalInactive,
online_channels: totalActive,
closed_channels: closed,
open_channels: openChannels.map(c => ({ channel_point: c.channelPoint, active: c.active, capacity: Number(c.capacity), channel_id: c.chanId, lifetime: Number(c.lifetime), local_balance: Number(c.localBalance), remote_balance: Number(c.remoteBalance), label: c.peerAlias })),
open_channels: openChannels.map(c => ({ channel_point: c.channelPoint, active: c.active, capacity: Number(c.capacity), channel_id: c.chanId, lifetime: Number(c.lifetime), local_balance: Number(c.localBalance), remote_balance: Number(c.remoteBalance), label: c.peerAlias, inactive_since_unix: channelsActivity[c.chanId] || 0 })),
forwarding_events: totalEvents,
forwarding_fees: totalFees,
root_ops: rootOps.map(r => ({ amount: r.operation_amount, created_at_unix: r.at_unix || 0, op_id: r.operation_identifier, op_type: mapRootOpType(r.operation_type) })),

View file

@ -46,6 +46,9 @@ export default (mainHandler: Main): Types.ServerMethods => {
GetLndMetrics: async ({ ctx, req }) => {
return mainHandler.metricsManager.GetLndMetrics(req)
},
GetLndForwardingMetrics: async ({ ctx, req }) => {
return mainHandler.metricsManager.GetLndForwardingMetrics(req)
},
ResetMetricsStorages: async ({ ctx }) => {
return mainHandler.utils.tlvStorageFactory.ResetStorages()
},
@ -85,8 +88,7 @@ export default (mainHandler: Main): Types.ServerMethods => {
},
CloseChannel: async ({ ctx, req }) => {
const err = Types.CloseChannelRequestValidate(req, {
funding_txid_CustomCheck: chanId => chanId !== '',
sat_per_v_byte_CustomCheck: spv => spv > 0
funding_txid_CustomCheck: chanId => chanId !== ''
})
if (err != null) throw new Error(err.message)
return mainHandler.adminManager.CloseChannel(req)

View file

@ -25,6 +25,7 @@ import { DebitAccess } from "../entity/DebitAccess.js"
import { RootOperation } from "../entity/RootOperation.js"
import { UserOffer } from "../entity/UserOffer.js"
import { ManagementGrant } from "../entity/ManagementGrant.js"
import { ChannelEvent } from "../entity/ChannelEvent.js"
export type DbSettings = {
@ -76,7 +77,8 @@ export const MetricsDbEntities = {
'BalanceEvent': BalanceEvent,
'ChannelBalanceEvent': ChannelBalanceEvent,
'ChannelRouting': ChannelRouting,
'RootOperation': RootOperation
'RootOperation': RootOperation,
'ChannelEvent': ChannelEvent
}
export type MetricsDbNames = keyof typeof MetricsDbEntities
export const MetricsDbEntitiesNames = Object.keys(MetricsDbEntities)

View file

@ -0,0 +1,22 @@
import { Entity, PrimaryGeneratedColumn, Column, Index, Check, CreateDateColumn, UpdateDateColumn } from "typeorm"
@Entity()
export class ChannelEvent {
@PrimaryGeneratedColumn()
serial_id: number
@Column()
channel_id: string
@Column()
event_type: 'activity'
@Column({ default: 0 })
inactive_since_unix: number
@CreateDateColumn()
created_at: Date
@UpdateDateColumn()
updated_at: Date
}

View file

@ -8,6 +8,8 @@ import { ChannelRouting } from "./entity/ChannelRouting.js";
import { RootOperation } from "./entity/RootOperation.js";
import { StorageInterface } from "./db/storageInterface.js";
import { Utils } from "../helpers/utilsWrapper.js";
import { Channel, ChannelEventUpdate } from "../../../proto/lnd/lightning.js";
import { ChannelEvent } from "./entity/ChannelEvent.js";
export default class {
//DB: DataSource | EntityManager
settings: StorageSettings
@ -27,6 +29,42 @@ export default class {
//return executedMigrations;
}
async FlagActiveChannel(chanId: string) {
const existing = await this.dbs.FindOne<ChannelEvent>('ChannelEvent', { where: { channel_id: chanId, event_type: 'activity' } })
if (!existing) {
await this.dbs.CreateAndSave<ChannelEvent>('ChannelEvent', { channel_id: chanId, event_type: 'activity', inactive_since_unix: 0 })
return
}
if (existing.inactive_since_unix > 0) {
await this.dbs.Update<ChannelEvent>('ChannelEvent', existing.serial_id, { inactive_since_unix: 0 })
return
}
return
}
async FlagInactiveChannel(chanId: string) {
const existing = await this.dbs.FindOne<ChannelEvent>('ChannelEvent', { where: { channel_id: chanId, event_type: 'activity' } })
if (!existing) {
await this.dbs.CreateAndSave<ChannelEvent>('ChannelEvent', { channel_id: chanId, event_type: 'activity', inactive_since_unix: Math.floor(Date.now() / 1000) })
return
}
if (existing.inactive_since_unix > 0) {
return
}
await this.dbs.Update<ChannelEvent>('ChannelEvent', existing.serial_id, { inactive_since_unix: Math.floor(Date.now() / 1000) })
return
}
async GetChannelsActivity(): Promise<Record<string, number>> {
const events = await this.dbs.Find<ChannelEvent>('ChannelEvent', { where: { event_type: 'activity' } })
const activityMap: Record<string, number> = {}
events.forEach(e => {
activityMap[e.channel_id] = e.inactive_since_unix
})
return activityMap
}
async SaveBalanceEvents(balanceEvent: Partial<BalanceEvent>, channelBalanceEvents: Partial<ChannelBalanceEvent>[]) {
//const blanceEventEntry = this.DB.getRepository(BalanceEvent).create(balanceEvent)
//const balanceEntry = await this.txQueue.PushToQueue<BalanceEvent>({ exec: async db => db.getRepository(BalanceEvent).save(blanceEventEntry), dbTx: false })

View file

@ -0,0 +1,14 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class ChannelEvents1750777346411 implements MigrationInterface {
name = 'ChannelEvents1750777346411'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "channel_event" ("serial_id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "channel_id" varchar NOT NULL, "event_type" varchar NOT NULL, "inactive_since_unix" integer NOT NULL DEFAULT (0), "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')))`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE "channel_event"`);
}
}

View file

@ -16,9 +16,10 @@ import { UserCbUrl1727112281043 } from './1727112281043-user_cb_url.js'
import { RootOps1732566440447 } from './1732566440447-root_ops.js'
import { UserOffer1733502626042 } from './1733502626042-user_offer.js'
import { RootOpsTime1745428134124 } from './1745428134124-root_ops_time.js'
import { ChannelEvents1750777346411 } from './1750777346411-channel_events.js'
import { ManagementGrant1751307732346 } from './1751307732346-management_grant.js'
export const allMigrations = [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189, TrackedProvider1720814323679, CreateInviteTokenTable1721751414878, PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264, DebitToPub1727105758354, UserCbUrl1727112281043, UserOffer1733502626042, ManagementGrant1751307732346]
export const allMetricsMigrations = [LndMetrics1703170330183, ChannelRouting1709316653538, HtlcCount1724266887195, BalanceEvents1724860966825, RootOps1732566440447, RootOpsTime1745428134124]
export const allMetricsMigrations = [LndMetrics1703170330183, ChannelRouting1709316653538, HtlcCount1724266887195, BalanceEvents1724860966825, RootOps1732566440447, RootOpsTime1745428134124, ChannelEvents1750777346411]
/* export const TypeOrmMigrationRunner = async (log: PubLogger, storageManager: Storage, settings: DbSettings, arg: string | undefined): Promise<boolean> => {
await connectAndMigrate(log, storageManager, allMigrations, allMetricsMigrations)
return false

View file

@ -14,8 +14,8 @@ export const setupNetwork = async (): Promise<ChainTools> => {
await core.InitAddress()
await core.Mine(1)
const setupUtils = new Utils({ dataDir: settings.storageSettings.dataDir, allowResetMetricsStorages: settings.allowResetMetricsStorages })
const alice = new LND(settings.lndSettings, new LiquidityProvider("", setupUtils, async () => { }, async () => { }), setupUtils, async () => { }, async () => { }, () => { }, () => { })
const bob = new LND({ ...settings.lndSettings, mainNode: settings.lndSettings.otherNode }, new LiquidityProvider("", setupUtils, async () => { }, async () => { }), setupUtils, async () => { }, async () => { }, () => { }, () => { })
const alice = new LND(settings.lndSettings, new LiquidityProvider("", setupUtils, async () => { }, async () => { }), setupUtils, async () => { }, async () => { }, () => { }, () => { }, () => { })
const bob = new LND({ ...settings.lndSettings, mainNode: settings.lndSettings.otherNode }, new LiquidityProvider("", setupUtils, async () => { }, async () => { }), setupUtils, async () => { }, async () => { }, () => { }, () => { }, () => { })
await tryUntil<void>(async i => {
const peers = await alice.ListPeers()
if (peers.peers.length > 0) {

View file

@ -78,11 +78,11 @@ export const SetupTest = async (d: Describe, chainTools: ChainTools): Promise<Te
await externalAccessToMainLnd.Warmup() */
const otherLndSetting = { ...settings.lndSettings, mainNode: settings.lndSettings.otherNode }
const externalAccessToOtherLnd = new LND(otherLndSetting, new LiquidityProvider("", extermnalUtils, async () => { }, async () => { }), extermnalUtils, async () => { }, async () => { }, () => { }, () => { })
const externalAccessToOtherLnd = new LND(otherLndSetting, new LiquidityProvider("", extermnalUtils, async () => { }, async () => { }), extermnalUtils, async () => { }, async () => { }, () => { }, () => { }, () => { })
await externalAccessToOtherLnd.Warmup()
const thirdLndSetting = { ...settings.lndSettings, mainNode: settings.lndSettings.thirdNode }
const externalAccessToThirdLnd = new LND(thirdLndSetting, new LiquidityProvider("", extermnalUtils, async () => { }, async () => { }), extermnalUtils, async () => { }, async () => { }, () => { }, () => { })
const externalAccessToThirdLnd = new LND(thirdLndSetting, new LiquidityProvider("", extermnalUtils, async () => { }, async () => { }), extermnalUtils, async () => { }, async () => { }, () => { }, () => { }, () => { })
await externalAccessToThirdLnd.Warmup()