andreashuber69/lightning-node-operator

View on GitHub
src/NodeStats.ts

Summary

Maintainability
A
3 hrs
Test Coverage
// https://github.com/andreashuber69/lightning-node-operator/develop/README.md
import type { IChannelStats } from "./ChannelStats.js";
import { ChannelStats } from "./ChannelStats.js";
import type { INodeInfo } from "./info/NodeInfo.js";

export class NodeStats {
    public static get(
        {
            channels: { data: channels },
            closedChannels: { data: closedChannels },
            nodes: { data: nodes },
            forwards: { data: forwards, days },
            payments: { data: payments },
            transactions: { data: transactions },
        }: INodeInfo,
    ): INodeStats {
        const nodesMap = new Map(nodes.map((n) => [n.id, n]));
        const transactionsMap = new Map(transactions.map((t) => [t.id, t.created_at]));

        const channelsImpl = new Map(channels.map(
            // eslint-disable-next-line @typescript-eslint/naming-convention
            ({ id, partner_public_key, transaction_id, ...rest }) => {
                const node = nodesMap.get(id);

                const partnerFeeRate = node?.channels?.
                    find((c) => c.id === id)?.policies?.
                    find((p) => p.public_key === partner_public_key)?.
                    fee_rate;

                // We only see confirmed transactions. Should we see a channel even though its funding transactions has
                // not been confirmed yet, we take the current time as opening time.
                const openedAt = transactionsMap.get(transaction_id) ?? new Date().toISOString();
                return [id, new ChannelStats({ id, partnerAlias: node?.alias, partnerFeeRate, openedAt, ...rest })];
            },
        ));

        const closedChannelIds = new Set(closedChannels.map((c) => c.id));

        for (const forward of forwards) {
            // eslint-disable-next-line @typescript-eslint/naming-convention
            const { created_at, incoming_channel, outgoing_channel } = forward;
            const { rawTokens, fee } = this.getTokens(forward);
            const inChannel = channelsImpl.get(incoming_channel);
            const outChannel = channelsImpl.get(outgoing_channel);
            inChannel?.addInForward(created_at, -rawTokens - fee, fee, outChannel);
            outChannel?.addOutForward(created_at, rawTokens, fee, inChannel);
        }

        // eslint-disable-next-line @typescript-eslint/naming-convention
        for (const { attempts, confirmed_at } of payments) {
            // eslint-disable-next-line @typescript-eslint/naming-convention
            for (const { is_confirmed, route } of attempts) {
                if (is_confirmed) {
                    const { rawTokens, fee } = this.getTokens(route);
                    const outId = route.hops.at(0)?.channel ?? "";
                    const outChannel = channelsImpl.get(outId);
                    const inId = route.hops.at(-1)?.channel ?? "";
                    const inChannel = channelsImpl.get(inId);

                    // Make sure that rebalances are recognized as such even if one of the involved channels has been
                    // closed.
                    if ((outChannel ?? closedChannelIds.has(outId)) && (inChannel ?? closedChannelIds.has(inId))) {
                        outChannel?.addOutRebalance(confirmed_at, rawTokens, fee);
                        inChannel?.addInRebalance(confirmed_at, -rawTokens + fee, fee);
                    } else {
                        outChannel?.addPayment(confirmed_at, rawTokens);
                        inChannel?.addPayment(confirmed_at, -rawTokens + fee);
                    }
                }
            }
        }

        return new NodeStats(channelsImpl, days);
    }

    // eslint-disable-next-line @typescript-eslint/naming-convention
    private static getTokens({ fee_mtokens, mtokens }: { readonly fee_mtokens: string; readonly mtokens: string }) {
        return { rawTokens: Number(mtokens) / 1000, fee: Number(fee_mtokens) / 1000 };
    }

    private constructor(public readonly channels: ReadonlyMap<string, IChannelStats>, public readonly days: number) {}
}

/** See {@linkcode NodeStats}. */
export type INodeStats = Pick<NodeStats, "channels" | "days">;