From 4f5bfdcca6256ca2ccd8f0da4ee19c8ebb0f26e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20A=2EP?= <53834183+Jossec101@users.noreply.github.com> Date: Tue, 21 Apr 2026 16:38:45 +0200 Subject: [PATCH 1/6] Add Grafana monitoring and improve Lightning Network setup - Add Grafana service with comprehensive LN monitoring dashboards - Update LND nodes to v0.20.1-beta for better performance - Add payment generation script for testing channel activity - Configure inbound fee policies on Bob node for routing optimization - Make network routing-friendly as bob intermediary --- Tiltfile | 6 + docker-compose.yml | 1 + docker/bitcoin/.justfile | 15 + docker/bitcoin/docker-compose.polar.yml | 2 +- docker/bitcoin/setup.sh | 8 +- docker/grafana/docker-compose.yml | 21 + .../provisioning/dashboards/dashboards.yml | 11 + .../provisioning/dashboards/routing.json | 3785 +++++++++++++++++ .../datasources/nodeguard-postgres.yml | 28 + 9 files changed, 3873 insertions(+), 4 deletions(-) create mode 100644 docker/grafana/docker-compose.yml create mode 100644 docker/grafana/provisioning/dashboards/dashboards.yml create mode 100644 docker/grafana/provisioning/dashboards/routing.json create mode 100644 docker/grafana/provisioning/datasources/nodeguard-postgres.yml diff --git a/Tiltfile b/Tiltfile index 1d064ac9..64a44181 100644 --- a/Tiltfile +++ b/Tiltfile @@ -41,6 +41,10 @@ labels = { '40swapd-carol', '40swap-backend', ], + + 'grafana': [ + 'grafana', + ], } for (label, services) in labels.items(): @@ -50,5 +54,7 @@ for (label, services) in labels.items(): dc_resource(s, auto_init=False, labels = [label]) elif label == '40swap': dc_resource(s, auto_init=False, labels = [label]) + elif label == 'grafana': + dc_resource(s, auto_init=False, labels = [label]) else: dc_resource(s, labels = [label]) \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index cde8e67c..91cbac56 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,3 +6,4 @@ include: - ./docker/loop/docker-compose.yml - ./docker/mempool/docker-compose.yml - ./docker/40swap/docker-compose.yml + - ./docker/grafana/docker-compose.yml diff --git a/docker/bitcoin/.justfile b/docker/bitcoin/.justfile index f6ebeee3..e10c8105 100644 --- a/docker/bitcoin/.justfile +++ b/docker/bitcoin/.justfile @@ -128,3 +128,18 @@ addinvoice amount: payinvoice invoice: just lncli payinvoice {{invoice}} -f +# Generate payments from Alice to Carol via Bob every 5s +generate-payments: + #!/usr/bin/env bash + echo "Generating payments: Alice → Bob → Carol every 5s (Ctrl+C to stop)" + while true; do + INVOICE=$(docker exec polar-n1-carol lncli --network regtest -lnddir /root/.lnd/ addinvoice --amt 1000 | jq -r .payment_request) + if [ -n "$INVOICE" ] && [ "$INVOICE" != "null" ]; then + echo "$(date): Paying 1000 sats Alice → Carol via Bob" + docker exec polar-n1-alice lncli --network regtest -lnddir /root/.lnd/ payinvoice --force "$INVOICE" + else + echo "$(date): Failed to create invoice on Carol, retrying..." + fi + sleep 5 + done + diff --git a/docker/bitcoin/docker-compose.polar.yml b/docker/bitcoin/docker-compose.polar.yml index 06abe926..6aab82d0 100644 --- a/docker/bitcoin/docker-compose.polar.yml +++ b/docker/bitcoin/docker-compose.polar.yml @@ -2,7 +2,7 @@ name: polar x-lnd-node: &lnd-node profiles: [polar] - image: lightninglabs/lndinit:v0.1.28-beta-lnd-v0.19.0-beta + image: lightninglabs/lndinit:v0.1.34-beta-lnd-v0.20.1-beta environment: USERID: ${USERID:-1000} GROUPID: ${GROUPID:-1000} diff --git a/docker/bitcoin/setup.sh b/docker/bitcoin/setup.sh index 4a71a815..34af8cb8 100755 --- a/docker/bitcoin/setup.sh +++ b/docker/bitcoin/setup.sh @@ -76,8 +76,10 @@ lncli $ALICE openchannel --connect $BOB:9735 $BOB_PUBKEY --local_amt 16000000 -- echo "Opening a channel from Bob to Carol" lncli $BOB openchannel --connect $CAROL:9735 $CAROL_PUBKEY --local_amt 16000000 --push_amt 8000000 -echo "Opening a channel from Carol to Alice" -lncli $CAROL openchannel --connect $ALICE:9735 $ALICE_PUBKEY --local_amt 16000000 --push_amt 8000000 - echo "Confirming channels" bitcoin_cli -generate 6 > /dev/null + +echo "Setting inbound fees on Bob's channels" + + +lncli $BOB updatechanpolicy --base_fee_msat 0 --fee_rate_ppm 1000 --time_lock_delta 40 --inbound_fee_rate_ppm -1000 --inbound_base_fee_msat 0 \ No newline at end of file diff --git a/docker/grafana/docker-compose.yml b/docker/grafana/docker-compose.yml new file mode 100644 index 00000000..f1678757 --- /dev/null +++ b/docker/grafana/docker-compose.yml @@ -0,0 +1,21 @@ +name: grafana + +services: + grafana: + image: grafana/grafana:latest + container_name: grafana + restart: unless-stopped + ports: + - "3000:3000" + depends_on: + postgres: + condition: service_healthy + environment: + GF_SECURITY_ADMIN_USER: admin + GF_SECURITY_ADMIN_PASSWORD: admin + volumes: + - grafana_data:/var/lib/grafana + - ./provisioning:/etc/grafana/provisioning + +volumes: + grafana_data: diff --git a/docker/grafana/provisioning/dashboards/dashboards.yml b/docker/grafana/provisioning/dashboards/dashboards.yml new file mode 100644 index 00000000..7c9df516 --- /dev/null +++ b/docker/grafana/provisioning/dashboards/dashboards.yml @@ -0,0 +1,11 @@ +apiVersion: 1 + +providers: + - name: 'NodeGuard Dashboards' + orgId: 1 + folder: '' + type: file + disableDeletion: false + editable: true + options: + path: /etc/grafana/provisioning/dashboards \ No newline at end of file diff --git a/docker/grafana/provisioning/dashboards/routing.json b/docker/grafana/provisioning/dashboards/routing.json new file mode 100644 index 00000000..d8b91dec --- /dev/null +++ b/docker/grafana/provisioning/dashboards/routing.json @@ -0,0 +1,3785 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "links": [], + "panels": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 8, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "currencyBTC" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 4, + "x": 0, + "y": 0 + }, + "id": 3, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": true + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.4.2", + "targets": [ + { + "dataset": "fundsmanager", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "editorMode": "builder", + "format": "table", + "hide": true, + "rawSql": "SELECT SUM(\"FeeMsat\") FROM \"ForwardingHtlcEvents\" WHERE ($__timeFilter(\"CreationDatetime\") AND \"Outcome\" = 1 AND \"ManagedNodePubKey\" IN ($node)) LIMIT 50 ", + "refId": "A", + "sql": { + "columns": [ + { + "name": "SUM", + "parameters": [ + { + "name": "\"FeeMsat\"", + "type": "functionParameter" + } + ], + "type": "function" + } + ], + "groupBy": [], + "limit": 50, + "whereJsonTree": { + "children1": [ + { + "id": "bab889bb-89ab-4cde-b012-319d68a9f5ad", + "properties": { + "field": "\"CreationDatetime\"", + "fieldSrc": "field", + "operator": "macros", + "value": [ + "timeFilter" + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "datetime" + ] + }, + "type": "rule" + }, + { + "id": "898bb9ba-0123-4456-b89a-b19d6c62b32c", + "properties": { + "field": "\"Outcome\"", + "fieldSrc": "field", + "operator": "equal", + "value": [ + 1 + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "number" + ] + }, + "type": "rule" + }, + { + "id": "b9bb88bb-89ab-4cde-b012-319d6e02759b", + "properties": { + "field": "\"ManagedNodePubKey\"", + "fieldSrc": "field", + "operator": "select_any_in", + "value": [ + "$node" + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "text" + ] + }, + "type": "rule" + } + ], + "id": "9ba98b98-0123-4456-b89a-b19d68a8bc01", + "type": "group" + }, + "whereString": "($__timeFilter(\"CreationDatetime\") AND \"Outcome\" = 1 AND \"ManagedNodePubKey\" IN ($node))" + }, + "table": "\"ForwardingHtlcEvents\"" + }, + { + "datasource": { + "name": "Expression", + "type": "__expr__", + "uid": "__expr__" + }, + "expression": "$A/100000000000", + "refId": "Node", + "type": "math" + } + ], + "title": "Total P&L (Routing)", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 8, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "currencyBTC" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 20, + "x": 4, + "y": 0 + }, + "id": 1, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": true + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.4.2", + "targets": [ + { + "dataset": "fundsmanager", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "editorMode": "builder", + "format": "table", + "hide": true, + "rawSql": "SELECT SUM(\"FeeMsat\"), \"ManagedNodeName\" FROM \"ForwardingHtlcEvents\" WHERE ($__timeFilter(\"CreationDatetime\") AND \"Outcome\" = 1 AND \"ManagedNodePubKey\" IN ($node)) GROUP BY \"ManagedNodeName\" LIMIT 50 ", + "refId": "A", + "sql": { + "columns": [ + { + "name": "SUM", + "parameters": [ + { + "name": "\"FeeMsat\"", + "type": "functionParameter" + } + ], + "type": "function" + }, + { + "parameters": [ + { + "name": "\"ManagedNodeName\"", + "type": "functionParameter" + } + ], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "name": "\"ManagedNodeName\"", + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50, + "whereJsonTree": { + "children1": [ + { + "id": "bab889bb-89ab-4cde-b012-319d68a9f5ad", + "properties": { + "field": "\"CreationDatetime\"", + "fieldSrc": "field", + "operator": "macros", + "value": [ + "timeFilter" + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "datetime" + ] + }, + "type": "rule" + }, + { + "id": "898bb9ba-0123-4456-b89a-b19d6c62b32c", + "properties": { + "field": "\"Outcome\"", + "fieldSrc": "field", + "operator": "equal", + "value": [ + 1 + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "number" + ] + }, + "type": "rule" + }, + { + "id": "aa9bbb99-cdef-4012-b456-719d6e03b38e", + "properties": { + "field": "\"ManagedNodePubKey\"", + "fieldSrc": "field", + "operator": "select_any_in", + "value": [ + "$node" + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "text" + ] + }, + "type": "rule" + } + ], + "id": "9ba98b98-0123-4456-b89a-b19d68a8bc01", + "type": "group" + }, + "whereString": "($__timeFilter(\"CreationDatetime\") AND \"Outcome\" = 1 AND \"ManagedNodePubKey\" IN ($node))" + }, + "table": "\"ForwardingHtlcEvents\"" + }, + { + "datasource": { + "name": "Expression", + "type": "__expr__", + "uid": "__expr__" + }, + "expression": "$A/100000000000", + "refId": "Node", + "type": "math" + } + ], + "title": "Total P&L per node (Routing)", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 8, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "currencyBTC" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 4, + "x": 0, + "y": 7 + }, + "id": 4, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": true + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.4.2", + "targets": [ + { + "dataset": "fundsmanager", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "editorMode": "builder", + "format": "table", + "hide": true, + "rawSql": "SELECT SUM(\"FeeMsat\") FROM \"ForwardingHtlcEvents\" WHERE ($__timeFilter(\"CreationDatetime\") AND \"Outcome\" = 2 AND \"ManagedNodePubKey\" IN ($node) AND \"WireFailureCode\" = 15) LIMIT 50 ", + "refId": "A", + "sql": { + "columns": [ + { + "name": "SUM", + "parameters": [ + { + "name": "\"FeeMsat\"", + "type": "functionParameter" + } + ], + "type": "function" + } + ], + "groupBy": [], + "limit": 50, + "whereJsonTree": { + "children1": [ + { + "id": "bab889bb-89ab-4cde-b012-319d68a9f5ad", + "properties": { + "field": "\"CreationDatetime\"", + "fieldSrc": "field", + "operator": "macros", + "value": [ + "timeFilter" + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "datetime" + ] + }, + "type": "rule" + }, + { + "id": "898bb9ba-0123-4456-b89a-b19d6c62b32c", + "properties": { + "field": "\"Outcome\"", + "fieldSrc": "field", + "operator": "equal", + "value": [ + 2 + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "number" + ] + }, + "type": "rule" + }, + { + "id": "989bbaba-89ab-4cde-b012-319d6e021913", + "properties": { + "field": "\"ManagedNodePubKey\"", + "fieldSrc": "field", + "operator": "select_any_in", + "value": [ + "$node" + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "text" + ] + }, + "type": "rule" + }, + { + "id": "a8b89a8a-0123-4456-b89a-b19d8b54b557", + "properties": { + "field": "\"WireFailureCode\"", + "fieldSrc": "field", + "operator": "equal", + "value": [ + 15 + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "number" + ] + }, + "type": "rule" + } + ], + "id": "9ba98b98-0123-4456-b89a-b19d68a8bc01", + "type": "group" + }, + "whereString": "($__timeFilter(\"CreationDatetime\") AND \"Outcome\" = 2 AND \"ManagedNodePubKey\" IN ($node) AND \"WireFailureCode\" = 15)" + }, + "table": "\"ForwardingHtlcEvents\"" + }, + { + "datasource": { + "name": "Expression", + "type": "__expr__", + "uid": "__expr__" + }, + "expression": "$A/100000000000", + "refId": "Node", + "type": "math" + } + ], + "title": "Total Potential Missed fees (Routing)", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 8, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "currencyBTC" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 20, + "x": 4, + "y": 7 + }, + "id": 2, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": true + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.4.2", + "targets": [ + { + "dataset": "fundsmanager", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "editorMode": "builder", + "format": "table", + "hide": true, + "rawSql": "SELECT SUM(\"FeeMsat\"), \"ManagedNodeName\" FROM \"ForwardingHtlcEvents\" WHERE ($__timeFilter(\"CreationDatetime\") AND \"Outcome\" = 2 AND \"ManagedNodePubKey\" IN ($node) AND \"WireFailureCode\" = 15) GROUP BY \"ManagedNodeName\" LIMIT 50 ", + "refId": "A", + "sql": { + "columns": [ + { + "name": "SUM", + "parameters": [ + { + "name": "\"FeeMsat\"", + "type": "functionParameter" + } + ], + "type": "function" + }, + { + "parameters": [ + { + "name": "\"ManagedNodeName\"", + "type": "functionParameter" + } + ], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "name": "\"ManagedNodeName\"", + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50, + "whereJsonTree": { + "children1": [ + { + "id": "bab889bb-89ab-4cde-b012-319d68a9f5ad", + "properties": { + "field": "\"CreationDatetime\"", + "fieldSrc": "field", + "operator": "macros", + "value": [ + "timeFilter" + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "datetime" + ] + }, + "type": "rule" + }, + { + "id": "898bb9ba-0123-4456-b89a-b19d6c62b32c", + "properties": { + "field": "\"Outcome\"", + "fieldSrc": "field", + "operator": "equal", + "value": [ + 2 + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "number" + ] + }, + "type": "rule" + }, + { + "id": "8b8988ba-cdef-4012-b456-719d6e04bd82", + "properties": { + "field": "\"ManagedNodePubKey\"", + "fieldSrc": "field", + "operator": "select_any_in", + "value": [ + "$node" + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "text" + ] + }, + "type": "rule" + }, + { + "id": "8988aaab-cdef-4012-b456-719d8b58d45d", + "properties": { + "field": "\"WireFailureCode\"", + "fieldSrc": "field", + "operator": "equal", + "value": [ + 15 + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "number" + ] + }, + "type": "rule" + } + ], + "id": "9ba98b98-0123-4456-b89a-b19d68a8bc01", + "type": "group" + }, + "whereString": "($__timeFilter(\"CreationDatetime\") AND \"Outcome\" = 2 AND \"ManagedNodePubKey\" IN ($node) AND \"WireFailureCode\" = 15)" + }, + "table": "\"ForwardingHtlcEvents\"" + }, + { + "datasource": { + "name": "Expression", + "type": "__expr__", + "uid": "__expr__" + }, + "expression": "$A/100000000000", + "refId": "Node", + "type": "math" + } + ], + "title": "Total Missed Fees per Node (Routing)", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": 0 + }, + { + "color": "green", + "value": 50 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 4, + "x": 0, + "y": 14 + }, + "id": 5, + "options": { + "minVizHeight": 75, + "minVizWidth": 75, + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": true + }, + "showThresholdLabels": false, + "showThresholdMarkers": true, + "sizing": "auto" + }, + "pluginVersion": "12.4.2", + "targets": [ + { + "dataset": "fundsmanager", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "editorMode": "builder", + "format": "table", + "hide": true, + "rawSql": "SELECT SUM(\"FeeMsat\") FROM \"ForwardingHtlcEvents\" WHERE ($__timeFilter(\"CreationDatetime\") AND \"Outcome\" = 1 AND \"ManagedNodePubKey\" IN ($node)) LIMIT 50 ", + "refId": "A", + "sql": { + "columns": [ + { + "name": "SUM", + "parameters": [ + { + "name": "\"FeeMsat\"", + "type": "functionParameter" + } + ], + "type": "function" + } + ], + "groupBy": [], + "limit": 50, + "whereJsonTree": { + "children1": [ + { + "id": "bab889bb-89ab-4cde-b012-319d68a9f5ad", + "properties": { + "field": "\"CreationDatetime\"", + "fieldSrc": "field", + "operator": "macros", + "value": [ + "timeFilter" + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "datetime" + ] + }, + "type": "rule" + }, + { + "id": "898bb9ba-0123-4456-b89a-b19d6c62b32c", + "properties": { + "field": "\"Outcome\"", + "fieldSrc": "field", + "operator": "equal", + "value": [ + 1 + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "number" + ] + }, + "type": "rule" + }, + { + "id": "babbab9b-89ab-4cde-b012-319d6e01b4ee", + "properties": { + "field": "\"ManagedNodePubKey\"", + "fieldSrc": "field", + "operator": "select_any_in", + "value": [ + "$node" + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "text" + ] + }, + "type": "rule" + } + ], + "id": "9ba98b98-0123-4456-b89a-b19d68a8bc01", + "type": "group" + }, + "whereString": "($__timeFilter(\"CreationDatetime\") AND \"Outcome\" = 1 AND \"ManagedNodePubKey\" IN ($node))" + }, + "table": "\"ForwardingHtlcEvents\"" + }, + { + "dataset": "fundsmanager", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "editorMode": "builder", + "format": "table", + "hide": true, + "rawSql": "SELECT SUM(\"FeeMsat\") FROM \"ForwardingHtlcEvents\" WHERE ($__timeFilter(\"CreationDatetime\") AND \"Outcome\" BETWEEN 1 AND 2 AND \"ManagedNodePubKey\" IN ($node) AND \"WireFailureCode\" = 15) LIMIT 50 ", + "refId": "B", + "sql": { + "columns": [ + { + "name": "SUM", + "parameters": [ + { + "name": "\"FeeMsat\"", + "type": "functionParameter" + } + ], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50, + "whereJsonTree": { + "children1": [ + { + "id": "bab889bb-89ab-4cde-b012-319d68a9f5ad", + "properties": { + "field": "\"CreationDatetime\"", + "fieldSrc": "field", + "operator": "macros", + "value": [ + "timeFilter" + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "datetime" + ] + }, + "type": "rule" + }, + { + "id": "baaa88b9-89ab-4cde-b012-319d6c6df7e3", + "properties": { + "field": "\"Outcome\"", + "fieldSrc": "field", + "operator": "between", + "value": [ + 1, + 2 + ], + "valueError": [ + null, + null + ], + "valueSrc": [ + "value", + "value" + ], + "valueType": [ + "number", + "number" + ] + }, + "type": "rule" + }, + { + "id": "9b889a99-cdef-4012-b456-719d6e02e3d5", + "properties": { + "field": "\"ManagedNodePubKey\"", + "fieldSrc": "field", + "operator": "select_any_in", + "value": [ + "$node" + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "text" + ] + }, + "type": "rule" + }, + { + "id": "aa9999ba-cdef-4012-b456-719d8b56e86e", + "properties": { + "field": "\"WireFailureCode\"", + "fieldSrc": "field", + "operator": "equal", + "value": [ + 15 + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "number" + ] + }, + "type": "rule" + } + ], + "id": "9ba98b98-0123-4456-b89a-b19d68a8bc01", + "type": "group" + }, + "whereString": "($__timeFilter(\"CreationDatetime\") AND \"Outcome\" BETWEEN 1 AND 2 AND \"ManagedNodePubKey\" IN ($node) AND \"WireFailureCode\" = 15)" + }, + "table": "\"ForwardingHtlcEvents\"" + }, + { + "datasource": { + "name": "Expression", + "type": "__expr__", + "uid": "__expr__" + }, + "expression": "($A/$B)*100", + "refId": "C", + "type": "math" + } + ], + "title": "% Routed of total", + "type": "gauge" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "fieldMinMax": false, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": 0 + }, + { + "color": "green", + "value": 50 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 20, + "x": 4, + "y": 14 + }, + "id": 6, + "interval": "30m", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.4.2", + "targets": [ + { + "dataset": "fundsmanager", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "editorMode": "builder", + "format": "table", + "hide": true, + "rawQuery": true, + "rawSql": "SELECT $__timeGroup(\"EventTimestamp\",$__interval), SUM(\"FeeMsat\") FROM \"ForwardingHtlcEvents\" WHERE ($__timeFilter(\"CreationDatetime\") AND \"Outcome\" = 2 AND \"ManagedNodePubKey\" IN ($node)) GROUP BY $__timeGroup(\"EventTimestamp\", $__interval, 0) ORDER BY $__timeGroup(\"EventTimestamp\", $__interval, 0) ", + "refId": "A", + "sql": { + "columns": [ + { + "name": "$__timeGroup", + "parameters": [ + { + "name": "\"EventTimestamp\"", + "type": "functionParameter" + }, + { + "name": "$__interval", + "type": "functionParameter" + } + ], + "type": "function" + }, + { + "name": "SUM", + "parameters": [ + { + "name": "\"FeeMsat\"", + "type": "functionParameter" + } + ], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "name": "$__timeGroup(\"EventTimestamp\", $__interval, 0)", + "type": "string" + }, + "type": "groupBy" + } + ], + "orderBy": { + "property": { + "name": "$__timeGroup(\"EventTimestamp\", $__interval, 0)", + "type": "string" + }, + "type": "property" + }, + "whereJsonTree": { + "children1": [ + { + "id": "bab889bb-89ab-4cde-b012-319d68a9f5ad", + "properties": { + "field": "\"CreationDatetime\"", + "fieldSrc": "field", + "operator": "macros", + "value": [ + "timeFilter" + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "datetime" + ] + }, + "type": "rule" + }, + { + "id": "898bb9ba-0123-4456-b89a-b19d6c62b32c", + "properties": { + "field": "\"Outcome\"", + "fieldSrc": "field", + "operator": "equal", + "value": [ + 2 + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "number" + ] + }, + "type": "rule" + }, + { + "id": "bb988889-4567-489a-bcde-f19d6e00972c", + "properties": { + "field": "\"ManagedNodePubKey\"", + "fieldSrc": "field", + "operator": "select_any_in", + "value": [ + "$node" + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "text" + ] + }, + "type": "rule" + } + ], + "id": "9ba98b98-0123-4456-b89a-b19d68a8bc01", + "type": "group" + }, + "whereString": "($__timeFilter(\"CreationDatetime\") AND \"Outcome\" = 2 AND \"ManagedNodePubKey\" IN ($node))" + }, + "table": "\"ForwardingHtlcEvents\"" + }, + { + "dataset": "fundsmanager", + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "editorMode": "builder", + "format": "table", + "hide": true, + "rawQuery": true, + "rawSql": "SELECT $__timeGroup(\"EventTimestamp\",$__interval), SUM(\"FeeMsat\") FROM \"ForwardingHtlcEvents\" WHERE ($__timeFilter(\"CreationDatetime\") AND \"Outcome\" BETWEEN 1 AND 2 AND \"ManagedNodePubKey\" IN ($node)) GROUP BY $__timeGroup(\"EventTimestamp\", $__interval, 0) ORDER BY $__timeGroup(\"EventTimestamp\", $__interval, 0) ", + "refId": "B", + "sql": { + "columns": [ + { + "name": "$__timeGroup", + "parameters": [ + { + "name": "\"EventTimestamp\"", + "type": "functionParameter" + }, + { + "name": "$__interval", + "type": "functionParameter" + } + ], + "type": "function" + }, + { + "name": "SUM", + "parameters": [ + { + "name": "\"FeeMsat\"", + "type": "functionParameter" + } + ], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "name": "$__timeGroup(\"EventTimestamp\", $__interval, 0)", + "type": "string" + }, + "type": "groupBy" + } + ], + "orderBy": { + "property": { + "name": "$__timeGroup(\"EventTimestamp\", $__interval, 0)", + "type": "string" + }, + "type": "property" + }, + "whereJsonTree": { + "children1": [ + { + "id": "bab889bb-89ab-4cde-b012-319d68a9f5ad", + "properties": { + "field": "\"CreationDatetime\"", + "fieldSrc": "field", + "operator": "macros", + "value": [ + "timeFilter" + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "datetime" + ] + }, + "type": "rule" + }, + { + "id": "898bb9ba-0123-4456-b89a-b19d6c62b32c", + "properties": { + "field": "\"Outcome\"", + "fieldSrc": "field", + "operator": "between", + "value": [ + 1, + 2 + ], + "valueSrc": [ + "value", + "value" + ], + "valueType": [ + "number", + "number" + ] + }, + "type": "rule" + }, + { + "id": "9b8bba99-0123-4456-b89a-b19d6e0f9072", + "properties": { + "field": "\"ManagedNodePubKey\"", + "fieldSrc": "field", + "operator": "select_any_in", + "value": [ + "$node" + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "text" + ] + }, + "type": "rule" + } + ], + "id": "9ba98b98-0123-4456-b89a-b19d68a8bc01", + "type": "group" + }, + "whereString": "($__timeFilter(\"CreationDatetime\") AND \"Outcome\" BETWEEN 1 AND 2 AND \"ManagedNodePubKey\" IN ($node))" + }, + "table": "\"ForwardingHtlcEvents\"" + }, + { + "datasource": { + "name": "Expression", + "type": "__expr__", + "uid": "__expr__" + }, + "expression": "(1-$A/$B)", + "refId": "%", + "type": "math" + } + ], + "title": "% Routed of total over time", + "type": "timeseries" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": 0 + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "currencyBTC" + }, + "overrides": [ + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "24h Rolling Avg" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": true, + "viz": true + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 21 + }, + "id": 18, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.4.2", + "targets": [ + { + "dataset": "fundsmanager", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\n $__timeGroup(\"CreationDatetime\", '1h', 0) AS \"time\",\n \n -- The raw profit for that specific hour:\n SUM(\"FeeMsat\") / 1e11 AS \"Hourly Profit\",\n\n -- The 24-hour rolling average (averages the last 24 one-hour buckets):\n AVG(SUM(\"FeeMsat\") / 1e11) OVER (\n ORDER BY $__timeGroup(\"CreationDatetime\", '1h', 0)\n ROWS BETWEEN 23 PRECEDING AND CURRENT ROW\n ) AS \"24h Rolling Avg\"\n\nFROM \"ForwardingHtlcEvents\"\nWHERE\n $__timeFilter(\"CreationDatetime\")\n AND \"Outcome\" = 1\n AND \"ManagedNodePubKey\" IN ($node)\nGROUP BY 1\nORDER BY 1", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Profit over time", + "type": "timeseries" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "barWidthFactor": 0.6, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "insertNulls": false, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "showValues": false, + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": 0 + }, + { + "color": "#dd606e", + "value": 80 + } + ] + }, + "unit": "currencyBTC" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 29 + }, + "id": 19, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.4.2", + "targets": [ + { + "dataset": "fundsmanager", + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT\n $__timeGroup(\"CreationDatetime\", '1h', 0) AS \"time\",\n \n -- The raw profit for that specific hour:\n SUM(\"FeeMsat\") / 1e11 AS \"Missed hourly fee\",\n\n -- The 24-hour rolling average (averages the last 24 one-hour buckets):\n AVG(SUM(\"FeeMsat\") / 1e11) OVER (\n ORDER BY $__timeGroup(\"CreationDatetime\", '1h', 0)\n ROWS BETWEEN 23 PRECEDING AND CURRENT ROW\n ) AS \"24h Rolling Avg\"\n\nFROM \"ForwardingHtlcEvents\"\nWHERE\n $__timeFilter(\"CreationDatetime\")\n AND \"Outcome\" = 2\n AND \"ManagedNodePubKey\" IN ($node)\n AND \"WireFailureCode\" = 15\nGROUP BY 1\nORDER BY 1", + "refId": "A", + "sql": { + "columns": [ + { + "parameters": [], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50 + } + } + ], + "title": "Missed fee over time", + "type": "timeseries" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [], + "unit": "currencyBTC" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 37 + }, + "id": 9, + "options": { + "displayLabels": [], + "legend": { + "displayMode": "table", + "placement": "right", + "showLegend": true, + "values": [ + "percent", + "value" + ] + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "/^sum / 100000000000$/", + "values": true + }, + "sort": "desc", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.4.2", + "targets": [ + { + "dataset": "fundsmanager", + "editorMode": "builder", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT SUM(\"FeeMsat\"), \"IncomingPeerAlias\" FROM \"ForwardingHtlcEvents\" WHERE (\"Outcome\" = 1 AND $__timeFilter(\"EventTimestamp\") AND \"ManagedNodePubKey\" IN ($node)) GROUP BY \"IncomingPeerAlias\" ORDER BY SUM(\"FeeMsat\") DESC LIMIT 500 ", + "refId": "A", + "sql": { + "columns": [ + { + "name": "SUM", + "parameters": [ + { + "name": "\"FeeMsat\"", + "type": "functionParameter" + } + ], + "type": "function" + }, + { + "parameters": [ + { + "name": "\"IncomingPeerAlias\"", + "type": "functionParameter" + } + ], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "name": "\"IncomingPeerAlias\"", + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 500, + "orderBy": { + "property": { + "name": "SUM(\"FeeMsat\")", + "type": "string" + }, + "type": "property" + }, + "orderByDirection": "DESC", + "whereJsonTree": { + "children1": [ + { + "id": "b899889b-cdef-4012-b456-719d6cbd7329", + "properties": { + "field": "\"Outcome\"", + "fieldSrc": "field", + "operator": "equal", + "value": [ + 1 + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "number" + ] + }, + "type": "rule" + }, + { + "id": "aaa89ba8-89ab-4cde-b012-319d6d60ec02", + "properties": { + "field": "\"EventTimestamp\"", + "fieldSrc": "field", + "operator": "macros", + "value": [ + "timeFilter" + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "datetime" + ] + }, + "type": "rule" + }, + { + "id": "8ba98b99-cdef-4012-b456-719d6e0c80a5", + "properties": { + "field": "\"ManagedNodePubKey\"", + "fieldSrc": "field", + "operator": "select_any_in", + "value": [ + "$node" + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "text" + ] + }, + "type": "rule" + } + ], + "id": "9ba98b98-0123-4456-b89a-b19d68a8bc01", + "type": "group" + }, + "whereString": "(\"Outcome\" = 1 AND $__timeFilter(\"EventTimestamp\") AND \"ManagedNodePubKey\" IN ($node))" + }, + "table": "\"ForwardingHtlcEvents\"" + } + ], + "title": "Routed fee distribution (From)", + "transformations": [ + { + "id": "calculateField", + "options": { + "binary": { + "left": { + "matcher": { + "id": "byName", + "options": "sum" + } + }, + "operator": "/", + "right": { + "fixed": "100000000000" + } + }, + "mode": "binary", + "reduce": { + "reducer": "sum" + }, + "replaceFields": false + } + }, + { + "id": "organize", + "options": { + "excludeByName": { + "sum": true + }, + "includeByName": {}, + "indexByName": {}, + "renameByName": {} + } + } + ], + "type": "piechart" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [], + "unit": "currencyBTC" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 37 + }, + "id": 13, + "options": { + "displayLabels": [], + "legend": { + "displayMode": "table", + "placement": "right", + "showLegend": true, + "values": [ + "percent", + "value" + ] + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "/^sum / 100000000000$/", + "values": true + }, + "sort": "desc", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.4.2", + "targets": [ + { + "dataset": "fundsmanager", + "editorMode": "builder", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT SUM(\"FeeMsat\"), \"OutgoingPeerAlias\" FROM \"ForwardingHtlcEvents\" WHERE (\"Outcome\" = 1 AND $__timeFilter(\"EventTimestamp\") AND \"ManagedNodePubKey\" IN ($node)) GROUP BY \"OutgoingPeerAlias\" ORDER BY SUM(\"FeeMsat\") DESC LIMIT 500 ", + "refId": "A", + "sql": { + "columns": [ + { + "name": "SUM", + "parameters": [ + { + "name": "\"FeeMsat\"", + "type": "functionParameter" + } + ], + "type": "function" + }, + { + "parameters": [ + { + "name": "\"OutgoingPeerAlias\"", + "type": "functionParameter" + } + ], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "name": "\"OutgoingPeerAlias\"", + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 500, + "orderBy": { + "property": { + "name": "SUM(\"FeeMsat\")", + "type": "string" + }, + "type": "property" + }, + "orderByDirection": "DESC", + "whereJsonTree": { + "children1": [ + { + "id": "b899889b-cdef-4012-b456-719d6cbd7329", + "properties": { + "field": "\"Outcome\"", + "fieldSrc": "field", + "operator": "equal", + "value": [ + 1 + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "number" + ] + }, + "type": "rule" + }, + { + "id": "aaa89ba8-89ab-4cde-b012-319d6d60ec02", + "properties": { + "field": "\"EventTimestamp\"", + "fieldSrc": "field", + "operator": "macros", + "value": [ + "timeFilter" + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "datetime" + ] + }, + "type": "rule" + }, + { + "id": "8a8ba9a8-cdef-4012-b456-719d6e0d9a7c", + "properties": { + "field": "\"ManagedNodePubKey\"", + "fieldSrc": "field", + "operator": "select_any_in", + "value": [ + "$node" + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "text" + ] + }, + "type": "rule" + } + ], + "id": "9ba98b98-0123-4456-b89a-b19d68a8bc01", + "type": "group" + }, + "whereString": "(\"Outcome\" = 1 AND $__timeFilter(\"EventTimestamp\") AND \"ManagedNodePubKey\" IN ($node))" + }, + "table": "\"ForwardingHtlcEvents\"" + } + ], + "title": "Routed fee distribution (To)", + "transformations": [ + { + "id": "calculateField", + "options": { + "binary": { + "left": { + "matcher": { + "id": "byName", + "options": "sum" + } + }, + "operator": "/", + "right": { + "fixed": "100000000000" + } + }, + "mode": "binary", + "reduce": { + "reducer": "sum" + }, + "replaceFields": false + } + }, + { + "id": "organize", + "options": { + "excludeByName": { + "sum": true + }, + "includeByName": {}, + "indexByName": {}, + "renameByName": {} + } + } + ], + "type": "piechart" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [], + "unit": "currencyBTC" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 45 + }, + "id": 14, + "options": { + "displayLabels": [], + "legend": { + "displayMode": "table", + "placement": "right", + "showLegend": true, + "values": [ + "percent", + "value" + ] + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "/^sum / 100000000000$/", + "limit": 100, + "values": true + }, + "sort": "desc", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.4.2", + "targets": [ + { + "dataset": "fundsmanager", + "editorMode": "builder", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT SUM(\"FeeMsat\"), \"IncomingPeerAlias\" FROM \"ForwardingHtlcEvents\" WHERE (\"Outcome\" = 2 AND $__timeFilter(\"EventTimestamp\") AND \"ManagedNodePubKey\" IN ($node)) GROUP BY \"IncomingPeerAlias\" ORDER BY SUM(\"FeeMsat\") DESC LIMIT 100 ", + "refId": "A", + "sql": { + "columns": [ + { + "name": "SUM", + "parameters": [ + { + "name": "\"FeeMsat\"", + "type": "functionParameter" + } + ], + "type": "function" + }, + { + "parameters": [ + { + "name": "\"IncomingPeerAlias\"", + "type": "functionParameter" + } + ], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "name": "\"IncomingPeerAlias\"", + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 100, + "orderBy": { + "property": { + "name": "SUM(\"FeeMsat\")", + "type": "string" + }, + "type": "property" + }, + "orderByDirection": "DESC", + "whereJsonTree": { + "children1": [ + { + "id": "b899889b-cdef-4012-b456-719d6cbd7329", + "properties": { + "field": "\"Outcome\"", + "fieldSrc": "field", + "operator": "equal", + "value": [ + 2 + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "number" + ] + }, + "type": "rule" + }, + { + "id": "aaa89ba8-89ab-4cde-b012-319d6d60ec02", + "properties": { + "field": "\"EventTimestamp\"", + "fieldSrc": "field", + "operator": "macros", + "value": [ + "timeFilter" + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "datetime" + ] + }, + "type": "rule" + }, + { + "id": "b98aba98-cdef-4012-b456-719d6e0e2960", + "properties": { + "field": "\"ManagedNodePubKey\"", + "fieldSrc": "field", + "operator": "select_any_in", + "value": [ + "$node" + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "text" + ] + }, + "type": "rule" + } + ], + "id": "9ba98b98-0123-4456-b89a-b19d68a8bc01", + "type": "group" + }, + "whereString": "(\"Outcome\" = 2 AND $__timeFilter(\"EventTimestamp\") AND \"ManagedNodePubKey\" IN ($node))" + }, + "table": "\"ForwardingHtlcEvents\"" + } + ], + "title": "Missed fee distribution (From)", + "transformations": [ + { + "id": "calculateField", + "options": { + "binary": { + "left": { + "matcher": { + "id": "byName", + "options": "sum" + } + }, + "operator": "/", + "right": { + "fixed": "100000000000" + } + }, + "mode": "binary", + "reduce": { + "reducer": "sum" + }, + "replaceFields": false + } + }, + { + "id": "organize", + "options": { + "excludeByName": { + "IncomingPeerAlias": false, + "sum": true, + "sum / 100000000000": false + }, + "includeByName": {}, + "indexByName": {}, + "orderByMode": "manual", + "renameByName": {} + } + } + ], + "type": "piechart" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [], + "unit": "currencyBTC" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 45 + }, + "id": 15, + "options": { + "displayLabels": [], + "legend": { + "displayMode": "table", + "placement": "right", + "showLegend": true, + "values": [ + "percent", + "value" + ] + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "/^sum / 100000000000$/", + "values": true + }, + "sort": "desc", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.4.2", + "targets": [ + { + "dataset": "fundsmanager", + "editorMode": "builder", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT SUM(\"FeeMsat\"), \"OutgoingPeerAlias\" FROM \"ForwardingHtlcEvents\" WHERE (\"Outcome\" = 2 AND $__timeFilter(\"EventTimestamp\") AND \"ManagedNodePubKey\" IN ($node)) GROUP BY \"OutgoingPeerAlias\" ORDER BY SUM(\"FeeMsat\") DESC LIMIT 500 ", + "refId": "A", + "sql": { + "columns": [ + { + "name": "SUM", + "parameters": [ + { + "name": "\"FeeMsat\"", + "type": "functionParameter" + } + ], + "type": "function" + }, + { + "parameters": [ + { + "name": "\"OutgoingPeerAlias\"", + "type": "functionParameter" + } + ], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "name": "\"OutgoingPeerAlias\"", + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 500, + "orderBy": { + "property": { + "name": "SUM(\"FeeMsat\")", + "type": "string" + }, + "type": "property" + }, + "orderByDirection": "DESC", + "whereJsonTree": { + "children1": [ + { + "id": "b899889b-cdef-4012-b456-719d6cbd7329", + "properties": { + "field": "\"Outcome\"", + "fieldSrc": "field", + "operator": "equal", + "value": [ + 2 + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "number" + ] + }, + "type": "rule" + }, + { + "id": "aaa89ba8-89ab-4cde-b012-319d6d60ec02", + "properties": { + "field": "\"EventTimestamp\"", + "fieldSrc": "field", + "operator": "macros", + "value": [ + "timeFilter" + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "datetime" + ] + }, + "type": "rule" + }, + { + "id": "ba9b9b9a-cdef-4012-b456-719d6e0eb039", + "properties": { + "field": "\"ManagedNodePubKey\"", + "fieldSrc": "field", + "operator": "select_any_in", + "value": [ + "$node" + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "text" + ] + }, + "type": "rule" + } + ], + "id": "9ba98b98-0123-4456-b89a-b19d68a8bc01", + "type": "group" + }, + "whereString": "(\"Outcome\" = 2 AND $__timeFilter(\"EventTimestamp\") AND \"ManagedNodePubKey\" IN ($node))" + }, + "table": "\"ForwardingHtlcEvents\"" + } + ], + "title": "Missed fee distribution (To)", + "transformations": [ + { + "id": "calculateField", + "options": { + "binary": { + "left": { + "matcher": { + "id": "byName", + "options": "sum" + } + }, + "operator": "/", + "right": { + "fixed": "100000000000" + } + }, + "mode": "binary", + "reduce": { + "reducer": "sum" + }, + "replaceFields": false + } + }, + { + "id": "organize", + "options": { + "excludeByName": { + "sum": true + }, + "includeByName": {}, + "indexByName": {}, + "renameByName": {} + } + } + ], + "type": "piechart" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [], + "unit": "currencyBTC" + }, + "overrides": [ + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "sum / 100000000000" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": true, + "viz": true + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 53 + }, + "id": 8, + "options": { + "displayLabels": [], + "legend": { + "displayMode": "table", + "placement": "right", + "showLegend": true, + "values": [ + "percent", + "value" + ] + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "/^sum / 100000000000$/", + "values": true + }, + "sort": "desc", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.4.2", + "targets": [ + { + "dataset": "fundsmanager", + "editorMode": "builder", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT SUM(\"FeeMsat\"), \"IncomingPeerAlias\", \"OutgoingPeerAlias\" FROM \"ForwardingHtlcEvents\" WHERE (\"Outcome\" = 1 AND $__timeFilter(\"EventTimestamp\") AND \"ManagedNodePubKey\" IN ($node)) GROUP BY \"IncomingPeerAlias\", \"OutgoingPeerAlias\" ORDER BY SUM(\"FeeMsat\") DESC LIMIT 500 ", + "refId": "A", + "sql": { + "columns": [ + { + "name": "SUM", + "parameters": [ + { + "name": "\"FeeMsat\"", + "type": "functionParameter" + } + ], + "type": "function" + }, + { + "parameters": [ + { + "name": "\"IncomingPeerAlias\"", + "type": "functionParameter" + } + ], + "type": "function" + }, + { + "parameters": [ + { + "name": "\"OutgoingPeerAlias\"", + "type": "functionParameter" + } + ], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "name": "\"IncomingPeerAlias\"", + "type": "string" + }, + "type": "groupBy" + }, + { + "property": { + "name": "\"OutgoingPeerAlias\"", + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 500, + "orderBy": { + "property": { + "name": "SUM(\"FeeMsat\")", + "type": "string" + }, + "type": "property" + }, + "orderByDirection": "DESC", + "whereJsonTree": { + "children1": [ + { + "id": "b899889b-cdef-4012-b456-719d6cbd7329", + "properties": { + "field": "\"Outcome\"", + "fieldSrc": "field", + "operator": "equal", + "value": [ + 1 + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "number" + ] + }, + "type": "rule" + }, + { + "id": "aaa89ba8-89ab-4cde-b012-319d6d60ec02", + "properties": { + "field": "\"EventTimestamp\"", + "fieldSrc": "field", + "operator": "macros", + "value": [ + "timeFilter" + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "datetime" + ] + }, + "type": "rule" + }, + { + "id": "9b8a889a-0123-4456-b89a-b19d6e16b184", + "properties": { + "field": "\"ManagedNodePubKey\"", + "fieldSrc": "field", + "operator": "select_any_in", + "value": [ + "$node" + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "text" + ] + }, + "type": "rule" + } + ], + "id": "9ba98b98-0123-4456-b89a-b19d68a8bc01", + "type": "group" + }, + "whereString": "(\"Outcome\" = 1 AND $__timeFilter(\"EventTimestamp\") AND \"ManagedNodePubKey\" IN ($node))" + }, + "table": "\"ForwardingHtlcEvents\"" + } + ], + "title": "Routed fee distribution (From->To)", + "transformations": [ + { + "id": "calculateField", + "options": { + "alias": "", + "binary": { + "left": { + "matcher": { + "id": "byName", + "options": "sum" + } + }, + "operator": "/", + "right": { + "fixed": "100000000000" + } + }, + "mode": "binary", + "reduce": { + "reducer": "sum" + }, + "replaceFields": false + } + }, + { + "id": "organize", + "options": { + "excludeByName": { + "sum": true + }, + "includeByName": {}, + "indexByName": {}, + "renameByName": {} + } + } + ], + "type": "piechart" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [], + "unit": "currencyBTC" + }, + "overrides": [ + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "sum / 100000000000", + "Kraken 🐙⚡ bfx-lnd0" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": true, + "viz": true + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 53 + }, + "id": 16, + "options": { + "displayLabels": [], + "legend": { + "displayMode": "table", + "placement": "right", + "showLegend": true, + "values": [ + "percent", + "value" + ] + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "/^sum / 100000000000$/", + "values": true + }, + "sort": "desc", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.4.2", + "targets": [ + { + "dataset": "fundsmanager", + "editorMode": "builder", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT SUM(\"FeeMsat\"), \"IncomingPeerAlias\", \"OutgoingPeerAlias\" FROM \"ForwardingHtlcEvents\" WHERE (\"Outcome\" = 2 AND $__timeFilter(\"EventTimestamp\") AND \"ManagedNodePubKey\" IN ($node)) GROUP BY \"IncomingPeerAlias\", \"OutgoingPeerAlias\" ORDER BY SUM(\"FeeMsat\") DESC LIMIT 500 ", + "refId": "A", + "sql": { + "columns": [ + { + "name": "SUM", + "parameters": [ + { + "name": "\"FeeMsat\"", + "type": "functionParameter" + } + ], + "type": "function" + }, + { + "parameters": [ + { + "name": "\"IncomingPeerAlias\"", + "type": "functionParameter" + } + ], + "type": "function" + }, + { + "parameters": [ + { + "name": "\"OutgoingPeerAlias\"", + "type": "functionParameter" + } + ], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "name": "\"IncomingPeerAlias\"", + "type": "string" + }, + "type": "groupBy" + }, + { + "property": { + "name": "\"OutgoingPeerAlias\"", + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 500, + "orderBy": { + "property": { + "name": "SUM(\"FeeMsat\")", + "type": "string" + }, + "type": "property" + }, + "orderByDirection": "DESC", + "whereJsonTree": { + "children1": [ + { + "id": "b899889b-cdef-4012-b456-719d6cbd7329", + "properties": { + "field": "\"Outcome\"", + "fieldSrc": "field", + "operator": "equal", + "value": [ + 2 + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "number" + ] + }, + "type": "rule" + }, + { + "id": "aaa89ba8-89ab-4cde-b012-319d6d60ec02", + "properties": { + "field": "\"EventTimestamp\"", + "fieldSrc": "field", + "operator": "macros", + "value": [ + "timeFilter" + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "datetime" + ] + }, + "type": "rule" + }, + { + "id": "bbb99aba-0123-4456-b89a-b19d6e17cc23", + "properties": { + "field": "\"ManagedNodePubKey\"", + "fieldSrc": "field", + "operator": "select_any_in", + "value": [ + "$node" + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "text" + ] + }, + "type": "rule" + } + ], + "id": "9ba98b98-0123-4456-b89a-b19d68a8bc01", + "type": "group" + }, + "whereString": "(\"Outcome\" = 2 AND $__timeFilter(\"EventTimestamp\") AND \"ManagedNodePubKey\" IN ($node))" + }, + "table": "\"ForwardingHtlcEvents\"" + } + ], + "title": "Missed fee distribution (From->To)", + "transformations": [ + { + "id": "calculateField", + "options": { + "alias": "", + "binary": { + "left": { + "matcher": { + "id": "byName", + "options": "sum" + } + }, + "operator": "/", + "right": { + "fixed": "100000000000" + } + }, + "mode": "binary", + "reduce": { + "reducer": "sum" + }, + "replaceFields": false + } + }, + { + "id": "organize", + "options": { + "excludeByName": { + "sum": true + }, + "includeByName": {}, + "indexByName": {}, + "renameByName": {} + } + } + ], + "type": "piechart" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [] + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 61 + }, + "id": 21, + "options": { + "displayLabels": [ + "percent" + ], + "legend": { + "displayMode": "list", + "placement": "right", + "showLegend": true + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [], + "fields": "", + "values": true + }, + "sort": "desc", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.4.2", + "targets": [ + { + "dataset": "fundsmanager", + "editorMode": "builder", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT COUNT(\"CreationDatetime\"), \"IncomingPeerAlias\", \"OutgoingPeerAlias\" FROM \"ForwardingHtlcEvents\" WHERE (\"Outcome\" = 1 AND $__timeFilter(\"EventTimestamp\") AND \"ManagedNodePubKey\" IN ($node)) GROUP BY \"IncomingPeerAlias\", \"OutgoingPeerAlias\" ORDER BY COUNT(\"CreationDatetime\") DESC LIMIT 500 ", + "refId": "A", + "sql": { + "columns": [ + { + "name": "COUNT", + "parameters": [ + { + "name": "\"CreationDatetime\"", + "type": "functionParameter" + } + ], + "type": "function" + }, + { + "parameters": [ + { + "name": "\"IncomingPeerAlias\"", + "type": "functionParameter" + } + ], + "type": "function" + }, + { + "parameters": [ + { + "name": "\"OutgoingPeerAlias\"", + "type": "functionParameter" + } + ], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "name": "\"IncomingPeerAlias\"", + "type": "string" + }, + "type": "groupBy" + }, + { + "property": { + "name": "\"OutgoingPeerAlias\"", + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 500, + "orderBy": { + "property": { + "name": "COUNT(\"CreationDatetime\")", + "type": "string" + }, + "type": "property" + }, + "orderByDirection": "DESC", + "whereJsonTree": { + "children1": [ + { + "id": "b899889b-cdef-4012-b456-719d6cbd7329", + "properties": { + "field": "\"Outcome\"", + "fieldSrc": "field", + "operator": "equal", + "value": [ + 1 + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "number" + ] + }, + "type": "rule" + }, + { + "id": "aaa89ba8-89ab-4cde-b012-319d6d60ec02", + "properties": { + "field": "\"EventTimestamp\"", + "fieldSrc": "field", + "operator": "macros", + "value": [ + "timeFilter" + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "datetime" + ] + }, + "type": "rule" + }, + { + "id": "8ba98b99-cdef-4012-b456-719d6e0c80a5", + "properties": { + "field": "\"ManagedNodePubKey\"", + "fieldSrc": "field", + "operator": "select_any_in", + "value": [ + "$node" + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "text" + ] + }, + "type": "rule" + } + ], + "id": "9ba98b98-0123-4456-b89a-b19d68a8bc01", + "type": "group" + }, + "whereString": "(\"Outcome\" = 1 AND $__timeFilter(\"EventTimestamp\") AND \"ManagedNodePubKey\" IN ($node))" + }, + "table": "\"ForwardingHtlcEvents\"" + } + ], + "title": "Routed HTLC counted distribution (From->To)", + "type": "piechart" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [] + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 61 + }, + "id": 22, + "options": { + "displayLabels": [ + "percent" + ], + "legend": { + "displayMode": "list", + "placement": "right", + "showLegend": true + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [], + "fields": "", + "values": true + }, + "sort": "desc", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.4.2", + "targets": [ + { + "dataset": "fundsmanager", + "editorMode": "builder", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT COUNT(\"CreationDatetime\"), \"IncomingPeerAlias\", \"OutgoingPeerAlias\" FROM \"ForwardingHtlcEvents\" WHERE (\"Outcome\" = 2 AND $__timeFilter(\"EventTimestamp\") AND \"ManagedNodePubKey\" IN ($node)) GROUP BY \"IncomingPeerAlias\", \"OutgoingPeerAlias\" ORDER BY COUNT(\"CreationDatetime\") DESC LIMIT 500 ", + "refId": "A", + "sql": { + "columns": [ + { + "name": "COUNT", + "parameters": [ + { + "name": "\"CreationDatetime\"", + "type": "functionParameter" + } + ], + "type": "function" + }, + { + "parameters": [ + { + "name": "\"IncomingPeerAlias\"", + "type": "functionParameter" + } + ], + "type": "function" + }, + { + "parameters": [ + { + "name": "\"OutgoingPeerAlias\"", + "type": "functionParameter" + } + ], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "name": "\"IncomingPeerAlias\"", + "type": "string" + }, + "type": "groupBy" + }, + { + "property": { + "name": "\"OutgoingPeerAlias\"", + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 500, + "orderBy": { + "property": { + "name": "COUNT(\"CreationDatetime\")", + "type": "string" + }, + "type": "property" + }, + "orderByDirection": "DESC", + "whereJsonTree": { + "children1": [ + { + "id": "b899889b-cdef-4012-b456-719d6cbd7329", + "properties": { + "field": "\"Outcome\"", + "fieldSrc": "field", + "operator": "equal", + "value": [ + 2 + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "number" + ] + }, + "type": "rule" + }, + { + "id": "aaa89ba8-89ab-4cde-b012-319d6d60ec02", + "properties": { + "field": "\"EventTimestamp\"", + "fieldSrc": "field", + "operator": "macros", + "value": [ + "timeFilter" + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "datetime" + ] + }, + "type": "rule" + }, + { + "id": "8ba98b99-cdef-4012-b456-719d6e0c80a5", + "properties": { + "field": "\"ManagedNodePubKey\"", + "fieldSrc": "field", + "operator": "select_any_in", + "value": [ + "$node" + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "text" + ] + }, + "type": "rule" + } + ], + "id": "9ba98b98-0123-4456-b89a-b19d68a8bc01", + "type": "group" + }, + "whereString": "(\"Outcome\" = 2 AND $__timeFilter(\"EventTimestamp\") AND \"ManagedNodePubKey\" IN ($node))" + }, + "table": "\"ForwardingHtlcEvents\"" + } + ], + "title": "Missed HTLC counted distribution (From->To)", + "type": "piechart" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + }, + "mappings": [] + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 69 + }, + "id": 17, + "options": { + "legend": { + "displayMode": "list", + "placement": "right", + "showLegend": true, + "values": [ + "percent", + "value" + ] + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": true + }, + "sort": "desc", + "tooltip": { + "hideZeros": false, + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "12.4.2", + "targets": [ + { + "dataset": "fundsmanager", + "editorMode": "builder", + "format": "table", + "rawSql": "SELECT COUNT(\"FailureString\"), \"FailureString\" FROM \"ForwardingHtlcEvents\" WHERE ($__timeFilter(\"EventTimestamp\") AND \"ManagedNodePubKey\" IN ($node)) GROUP BY \"FailureString\" LIMIT 50 ", + "refId": "A", + "sql": { + "columns": [ + { + "name": "COUNT", + "parameters": [ + { + "name": "\"FailureString\"", + "type": "functionParameter" + } + ], + "type": "function" + }, + { + "parameters": [ + { + "name": "\"FailureString\"", + "type": "functionParameter" + } + ], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "name": "\"FailureString\"", + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50, + "whereJsonTree": { + "children1": [ + { + "id": "9ab899ba-cdef-4012-b456-719d73491530", + "properties": { + "field": "\"EventTimestamp\"", + "fieldSrc": "field", + "operator": "macros", + "value": [ + "timeFilter" + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "datetime" + ] + }, + "type": "rule" + }, + { + "id": "8abab889-89ab-4cde-b012-319d734a9da7", + "properties": { + "field": "\"ManagedNodePubKey\"", + "fieldSrc": "field", + "operator": "select_any_in", + "value": [ + "$node" + ], + "valueError": [ + null + ], + "valueSrc": [ + "value" + ], + "valueType": [ + "text" + ] + }, + "type": "rule" + } + ], + "id": "988b8bb9-0123-4456-b89a-b19d6d718135", + "type": "group" + }, + "whereString": "($__timeFilter(\"EventTimestamp\") AND \"ManagedNodePubKey\" IN ($node))" + }, + "table": "\"ForwardingHtlcEvents\"" + } + ], + "title": "Failure string ", + "type": "piechart" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": 0 + }, + { + "color": "green", + "value": 1 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 0, + "y": 102 + }, + "id": 30, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": true + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.4.2", + "targets": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT COUNT(*) AS value FROM \"Nodes\" WHERE \"ChannelAdminMacaroon\" IS NOT NULL", + "refId": "A" + } + ], + "title": "Local Managed Nodes", + "type": "stat" + } + ], + "preload": false, + "refresh": "1m", + "schemaVersion": 42, + "tags": [], + "templating": { + "list": [ + { + "current": { + "selected": true, + "text": "NodeGuard PostgreSQL", + "value": "nodeguard-postgres" + }, + "hide": 0, + "includeAll": false, + "label": "Datasource", + "multi": false, + "name": "datasource", + "options": [], + "query": "grafana-postgresql-datasource", + "refresh": 1, + "regex": "", + "type": "datasource" + }, + { + "allowCustomValue": false, + "current": { + "text": "All", + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "definition": "SELECT \"Name\", \"PubKey\" FROM \"Nodes\" WHERE \"ChannelAdminMacaroon\" IS NOT NULL LIMIT 50 ", + "includeAll": true, + "label": "Node", + "multi": true, + "name": "node", + "options": [], + "query": { + "dataset": "fundsmanager", + "editorMode": "builder", + "format": "table", + "meta": { + "textField": "Name", + "valueField": "PubKey" + }, + "query": "SELECT \"Name\", \"PubKey\" FROM \"Nodes\" WHERE \"ChannelAdminMacaroon\" IS NOT NULL LIMIT 50 ", + "rawSql": "SELECT \"Name\", \"PubKey\" FROM \"Nodes\" WHERE \"ChannelAdminMacaroon\" IS NOT NULL LIMIT 50 ", + "refId": "SQLVariableQueryEditor-VariableQuery", + "sql": { + "columns": [ + { + "parameters": [ + { + "name": "\"Name\"", + "type": "functionParameter" + } + ], + "type": "function" + }, + { + "parameters": [ + { + "name": "\"PubKey\"", + "type": "functionParameter" + } + ], + "type": "function" + } + ], + "groupBy": [ + { + "property": { + "type": "string" + }, + "type": "groupBy" + } + ], + "limit": 50, + "whereJsonTree": { + "children1": [ + { + "id": "9b8988b9-0123-4456-b89a-b19d6dff19f6", + "properties": { + "field": "\"ChannelAdminMacaroon\"", + "fieldSrc": "field", + "operator": "is_not_null", + "value": [], + "valueSrc": [], + "valueType": [] + }, + "type": "rule" + } + ], + "id": "988b8bb9-0123-4456-b89a-b19d6d718135", + "type": "group" + }, + "whereString": "\"ChannelAdminMacaroon\" IS NOT NULL" + }, + "table": "\"Nodes\"" + }, + "refresh": 1, + "regex": "", + "regexApplyTo": "value", + "type": "query" + } + ] + }, + "time": { + "from": "2026-04-06T22:00:00.000Z", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "Routing", + "uid": "ad4n8c6", + "version": 1, + "weekStart": "" +} \ No newline at end of file diff --git a/docker/grafana/provisioning/datasources/nodeguard-postgres.yml b/docker/grafana/provisioning/datasources/nodeguard-postgres.yml new file mode 100644 index 00000000..9e087ad1 --- /dev/null +++ b/docker/grafana/provisioning/datasources/nodeguard-postgres.yml @@ -0,0 +1,28 @@ +apiVersion: 1 + +datasources: + - name: NodeGuard PostgreSQL + type: postgres + uid: nodeguard-postgres + url: host.docker.internal:55432 + user: view + jsonData: + database: fundsmanager + sslmode: disable + postgresVersion: 1600 + timescaledb: false + secureJsonData: {} + isDefault: true + editable: true + - name: NodeGuard PostgreSQL Local + type: postgres + uid: nodeguard-postgres-local + url: postgres:5432 + user: postgres + jsonData: + database: nodeguard + sslmode: disable + postgresVersion: 1600 + timescaledb: false + secureJsonData: {} + editable: true From bcc656b564d5457bc6415fa15e70723b5d6b54f4 Mon Sep 17 00:00:00 2001 From: Marcos Date: Thu, 30 Apr 2026 17:58:15 +0200 Subject: [PATCH 2/6] feat: add ClosedAt column to Channels table --- src/Data/Models/Channel.cs | 6 + src/Jobs/ChannelMonitorJob.cs | 1 + src/Jobs/NodeChannelSubscribeJob.cs | 1 + ...60430155542_AddChannelClosedAt.Designer.cs | 1621 +++++++++++++++++ .../20260430155542_AddChannelClosedAt.cs | 29 + .../ApplicationDbContextModelSnapshot.cs | 3 + src/Services/LightningService.cs | 2 + 7 files changed, 1663 insertions(+) create mode 100644 src/Migrations/20260430155542_AddChannelClosedAt.Designer.cs create mode 100644 src/Migrations/20260430155542_AddChannelClosedAt.cs diff --git a/src/Data/Models/Channel.cs b/src/Data/Models/Channel.cs index 263d0f96..30c78dbb 100644 --- a/src/Data/Models/Channel.cs +++ b/src/Data/Models/Channel.cs @@ -47,6 +47,12 @@ public enum ChannelStatus public ChannelStatus Status { get; set; } + /// + /// Timestamp at which the channel transitioned to . + /// Null while the channel is open or for channels closed before this column existed. + /// + public DateTimeOffset? ClosedAt { get; set; } + /// /// Indicates if this channel was created by NodeGuard /// diff --git a/src/Jobs/ChannelMonitorJob.cs b/src/Jobs/ChannelMonitorJob.cs index a27aea90..11680344 100644 --- a/src/Jobs/ChannelMonitorJob.cs +++ b/src/Jobs/ChannelMonitorJob.cs @@ -206,6 +206,7 @@ public async Task MarkClosedChannelsAsClosed(Node source, List? channel if (channel == null) { openChannel.Status = ChannelStatus.Closed; + openChannel.ClosedAt = DateTimeOffset.UtcNow; await dbContext.SaveChangesAsync(); } } diff --git a/src/Jobs/NodeChannelSubscribeJob.cs b/src/Jobs/NodeChannelSubscribeJob.cs index 146a7981..bcae7221 100644 --- a/src/Jobs/NodeChannelSubscribeJob.cs +++ b/src/Jobs/NodeChannelSubscribeJob.cs @@ -166,6 +166,7 @@ public async Task NodeUpdateManagement(ChannelEventUpdate channelEventUpdate, No else { channelToClose.Status = Channel.ChannelStatus.Closed; + channelToClose.ClosedAt = DateTimeOffset.UtcNow; var updateChannel = _channelRepository.Update(channelToClose); if (!updateChannel.Item1) { diff --git a/src/Migrations/20260430155542_AddChannelClosedAt.Designer.cs b/src/Migrations/20260430155542_AddChannelClosedAt.Designer.cs new file mode 100644 index 00000000..e156058f --- /dev/null +++ b/src/Migrations/20260430155542_AddChannelClosedAt.Designer.cs @@ -0,0 +1,1621 @@ +// +using System; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NodeGuard.Data; +using NodeGuard.Helpers; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace NodeGuard.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20260430155542_AddChannelClosedAt")] + partial class AddChannelClosedAt + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.1") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("ApplicationUserNode", b => + { + b.Property("NodesId") + .HasColumnType("integer"); + + b.Property("UsersId") + .HasColumnType("text"); + + b.HasKey("NodesId", "UsersId"); + + b.HasIndex("UsersId"); + + b.ToTable("ApplicationUserNode"); + }); + + modelBuilder.Entity("ChannelOperationRequestFMUTXO", b => + { + b.Property("ChannelOperationRequestsId") + .HasColumnType("integer"); + + b.Property("UtxosId") + .HasColumnType("integer"); + + b.HasKey("ChannelOperationRequestsId", "UtxosId"); + + b.HasIndex("UtxosId"); + + b.ToTable("ChannelOperationRequestFMUTXO"); + }); + + modelBuilder.Entity("FMUTXOWalletWithdrawalRequest", b => + { + b.Property("UTXOsId") + .HasColumnType("integer"); + + b.Property("WalletWithdrawalRequestsId") + .HasColumnType("integer"); + + b.HasKey("UTXOsId", "WalletWithdrawalRequestsId"); + + b.HasIndex("WalletWithdrawalRequestsId"); + + b.ToTable("FMUTXOWalletWithdrawalRequest"); + }); + + modelBuilder.Entity("KeyWallet", b => + { + b.Property("KeysId") + .HasColumnType("integer"); + + b.Property("WalletsId") + .HasColumnType("integer"); + + b.HasKey("KeysId", "WalletsId"); + + b.HasIndex("WalletsId"); + + b.ToTable("KeyWallet"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("RoleId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUser", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.Property("AccessFailedCount") + .HasColumnType("integer"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("text"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(21) + .HasColumnType("character varying(21)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("boolean"); + + b.Property("LockoutEnabled") + .HasColumnType("boolean"); + + b.Property("LockoutEnd") + .HasColumnType("timestamp with time zone"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PasswordHash") + .HasColumnType("text"); + + b.Property("PhoneNumber") + .HasColumnType("text"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("boolean"); + + b.Property("SecurityStamp") + .HasColumnType("text"); + + b.Property("TwoFactorEnabled") + .HasColumnType("boolean"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex"); + + b.ToTable("AspNetUsers", (string)null); + + b.HasDiscriminator().HasValue("IdentityUser"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("text"); + + b.Property("ClaimValue") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ProviderKey") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("ProviderDisplayName") + .HasColumnType("text"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("RoleId") + .HasColumnType("text"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("text"); + + b.Property("LoginProvider") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Name") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.APIToken", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationDatetime") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatorId") + .IsRequired() + .HasColumnType("text"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("IsBlocked") + .HasColumnType("boolean"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("TokenHash") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdateDatetime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CreatorId"); + + b.ToTable("ApiTokens"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.AuditLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ActionType") + .HasColumnType("integer"); + + b.Property("Details") + .HasColumnType("text"); + + b.Property("EventType") + .HasColumnType("integer"); + + b.Property("IpAddress") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("ObjectAffected") + .HasColumnType("integer"); + + b.Property("ObjectId") + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasMaxLength(450) + .HasColumnType("character varying(450)"); + + b.Property("Username") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("Id"); + + b.ToTable("AuditLogs"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.Channel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BtcCloseAddress") + .HasColumnType("text"); + + b.Property("ChanId") + .HasColumnType("numeric(20,0)"); + + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedByNodeGuard") + .HasColumnType("boolean"); + + b.Property("CreationDatetime") + .HasColumnType("timestamp with time zone"); + + b.Property("DestinationNodeId") + .HasColumnType("integer"); + + b.Property("FundingTx") + .IsRequired() + .HasColumnType("text"); + + b.Property("FundingTxOutputIndex") + .HasColumnType("bigint"); + + b.Property("IsAutomatedLiquidityEnabled") + .HasColumnType("boolean"); + + b.Property("IsPrivate") + .HasColumnType("boolean"); + + b.Property("SatsAmount") + .HasColumnType("bigint"); + + b.Property("SourceNodeId") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("UpdateDatetime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("DestinationNodeId"); + + b.HasIndex("SourceNodeId"); + + b.ToTable("Channels"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.ChannelOperationRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AmountCryptoUnit") + .HasColumnType("integer"); + + b.Property("Changeless") + .HasColumnType("boolean"); + + b.Property("ChannelId") + .HasColumnType("integer"); + + b.Property("ClosingReason") + .HasColumnType("text"); + + b.Property("CreationDatetime") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("DestNodeId") + .HasColumnType("integer"); + + b.Property("FeeRate") + .HasColumnType("numeric"); + + b.Property("IsChannelPrivate") + .HasColumnType("boolean"); + + b.Property("MempoolRecommendedFeesType") + .HasColumnType("integer"); + + b.Property("RequestType") + .HasColumnType("integer"); + + b.Property("SatsAmount") + .HasColumnType("bigint"); + + b.Property("SourceNodeId") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property>("StatusLogs") + .HasColumnType("jsonb"); + + b.Property("TxId") + .HasColumnType("text"); + + b.Property("UpdateDatetime") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("text"); + + b.Property("WalletId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ChannelId"); + + b.HasIndex("DestNodeId"); + + b.HasIndex("SourceNodeId"); + + b.HasIndex("UserId"); + + b.HasIndex("WalletId"); + + b.ToTable("ChannelOperationRequests"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.ChannelOperationRequestPSBT", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChannelOperationRequestId") + .HasColumnType("integer"); + + b.Property("CreationDatetime") + .HasColumnType("timestamp with time zone"); + + b.Property("IsFinalisedPSBT") + .HasColumnType("boolean"); + + b.Property("IsInternalWalletPSBT") + .HasColumnType("boolean"); + + b.Property("IsTemplatePSBT") + .HasColumnType("boolean"); + + b.Property("PSBT") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdateDatetime") + .HasColumnType("timestamp with time zone"); + + b.Property("UserSignerId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ChannelOperationRequestId"); + + b.HasIndex("UserSignerId"); + + b.ToTable("ChannelOperationRequestPSBTs"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.FMUTXO", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationDatetime") + .HasColumnType("timestamp with time zone"); + + b.Property("OutputIndex") + .HasColumnType("bigint"); + + b.Property("SatsAmount") + .HasColumnType("bigint"); + + b.Property("TxId") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdateDatetime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("FMUTXOs"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.ForwardingHtlcEvent", b => + { + b.Property("ManagedNodePubKey") + .HasColumnType("text"); + + b.Property("IncomingChannelId") + .HasColumnType("numeric(20,0)"); + + b.Property("OutgoingChannelId") + .HasColumnType("numeric(20,0)"); + + b.Property("IncomingHtlcId") + .HasColumnType("numeric(20,0)"); + + b.Property("OutgoingHtlcId") + .HasColumnType("numeric(20,0)"); + + b.Property("CreationDatetime") + .HasColumnType("timestamp with time zone"); + + b.Property("EventCase") + .HasColumnType("integer"); + + b.Property("EventTimestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("EventType") + .HasColumnType("integer"); + + b.Property("FailureDetail") + .HasColumnType("integer"); + + b.Property("FailureString") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("FeeMsat") + .HasColumnType("bigint"); + + b.Property("GrossFeeMsat") + .HasColumnType("bigint"); + + b.Property("InboundFeeMsat") + .HasColumnType("bigint"); + + b.Property("InboundFeePpm") + .HasColumnType("bigint"); + + b.Property("IncomingAmountMsat") + .HasColumnType("numeric(20,0)"); + + b.Property("IncomingPeerAlias") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IncomingTimelock") + .HasColumnType("bigint"); + + b.Property("ManagedNodeName") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Outcome") + .HasColumnType("integer"); + + b.Property("OutgoingAmountMsat") + .HasColumnType("numeric(20,0)"); + + b.Property("OutgoingPeerAlias") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("OutgoingTimelock") + .HasColumnType("bigint"); + + b.Property("RoutingFeePpm") + .HasColumnType("bigint"); + + b.Property("UpdateDatetime") + .HasColumnType("timestamp with time zone"); + + b.Property("WireFailureCode") + .HasColumnType("integer"); + + b.HasKey("ManagedNodePubKey", "IncomingChannelId", "OutgoingChannelId", "IncomingHtlcId", "OutgoingHtlcId"); + + b.HasIndex("CreationDatetime"); + + b.HasIndex("EventTimestamp"); + + b.ToTable("ForwardingHtlcEvents"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.InternalWallet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationDatetime") + .HasColumnType("timestamp with time zone"); + + b.Property("DerivationPath") + .IsRequired() + .HasColumnType("text"); + + b.Property("MasterFingerprint") + .HasColumnType("text"); + + b.Property("MnemonicString") + .HasColumnType("text"); + + b.Property("UpdateDatetime") + .HasColumnType("timestamp with time zone"); + + b.Property("XPUB") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("InternalWallets"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.Key", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationDatetime") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("InternalWalletId") + .HasColumnType("integer"); + + b.Property("IsArchived") + .HasColumnType("boolean"); + + b.Property("IsBIP39ImportedKey") + .HasColumnType("boolean"); + + b.Property("IsCompromised") + .HasColumnType("boolean"); + + b.Property("MasterFingerprint") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Path") + .HasColumnType("text"); + + b.Property("UpdateDatetime") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("text"); + + b.Property("XPUB") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("InternalWalletId"); + + b.HasIndex("UserId"); + + b.ToTable("Keys"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.LiquidityRule", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChannelId") + .HasColumnType("integer"); + + b.Property("CreationDatetime") + .HasColumnType("timestamp with time zone"); + + b.Property("IsReverseSwapWalletRule") + .HasColumnType("boolean"); + + b.Property("MinimumLocalBalance") + .HasColumnType("numeric"); + + b.Property("MinimumRemoteBalance") + .HasColumnType("numeric"); + + b.Property("NodeId") + .HasColumnType("integer"); + + b.Property("RebalanceTarget") + .HasColumnType("numeric"); + + b.Property("ReverseSwapAddress") + .HasColumnType("text"); + + b.Property("ReverseSwapWalletId") + .HasColumnType("integer"); + + b.Property("SwapWalletId") + .HasColumnType("integer"); + + b.Property("UpdateDatetime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ChannelId") + .IsUnique(); + + b.HasIndex("NodeId"); + + b.HasIndex("ReverseSwapWalletId"); + + b.HasIndex("SwapWalletId"); + + b.ToTable("LiquidityRules"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.Node", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AutoLiquidityManagementEnabled") + .HasColumnType("boolean"); + + b.Property("AutosweepEnabled") + .HasColumnType("boolean"); + + b.Property("ChannelAdminMacaroon") + .HasColumnType("text"); + + b.Property("CreationDatetime") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Endpoint") + .HasColumnType("text"); + + b.Property("FortySwapEndpoint") + .HasColumnType("text"); + + b.Property("FortySwapWeight") + .HasColumnType("integer"); + + b.Property("FundsDestinationWalletId") + .HasColumnType("integer"); + + b.Property("IsNodeDisabled") + .HasColumnType("boolean"); + + b.Property("LoopSwapWeight") + .HasColumnType("integer"); + + b.Property("LoopdCert") + .HasColumnType("text"); + + b.Property("LoopdEndpoint") + .HasColumnType("text"); + + b.Property("LoopdMacaroon") + .HasColumnType("text"); + + b.Property("MaxSwapRoutingFeeRatio") + .HasColumnType("numeric"); + + b.Property("MaxSwapsInFlight") + .HasColumnType("integer"); + + b.Property("MinimumBalanceThresholdSats") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PubKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("SwapBudgetRefreshInterval") + .HasColumnType("interval"); + + b.Property("SwapBudgetSats") + .HasColumnType("bigint"); + + b.Property("SwapBudgetStartDatetime") + .HasColumnType("timestamp with time zone"); + + b.Property("SwapMaxAmountSats") + .HasColumnType("bigint"); + + b.Property("SwapMinAmountSats") + .HasColumnType("bigint"); + + b.Property("UpdateDatetime") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("FundsDestinationWalletId"); + + b.HasIndex("PubKey") + .IsUnique(); + + b.ToTable("Nodes"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.SwapOut", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationDatetime") + .HasColumnType("timestamp with time zone"); + + b.Property("DestinationWalletId") + .HasColumnType("integer"); + + b.Property("ErrorDetails") + .HasColumnType("text"); + + b.Property("IsManual") + .HasColumnType("boolean"); + + b.Property("LightningFeeSats") + .HasColumnType("bigint"); + + b.Property("NodeId") + .HasColumnType("integer"); + + b.Property("OnChainFeeSats") + .HasColumnType("bigint"); + + b.Property("Provider") + .HasColumnType("integer"); + + b.Property("ProviderId") + .HasColumnType("text"); + + b.Property("SatsAmount") + .HasColumnType("bigint"); + + b.Property("ServiceFeeSats") + .HasColumnType("bigint"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TxId") + .HasColumnType("text"); + + b.Property("UpdateDatetime") + .HasColumnType("timestamp with time zone"); + + b.Property("UserRequestorId") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("DestinationWalletId"); + + b.HasIndex("NodeId"); + + b.HasIndex("UserRequestorId"); + + b.ToTable("SwapOuts"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.UTXOTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationDatetime") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("Outpoint") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdateDatetime") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Key", "Outpoint") + .IsUnique(); + + b.ToTable("UTXOTags"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.Wallet", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BIP39Seedphrase") + .HasColumnType("text"); + + b.Property("CreationDatetime") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("ImportedOutputDescriptor") + .HasColumnType("text"); + + b.Property("InternalWalletId") + .HasColumnType("integer"); + + b.Property("InternalWalletMasterFingerprint") + .HasColumnType("text"); + + b.Property("InternalWalletSubDerivationPath") + .HasColumnType("text"); + + b.Property("IsArchived") + .HasColumnType("boolean"); + + b.Property("IsBIP39Imported") + .HasColumnType("boolean"); + + b.Property("IsCompromised") + .HasColumnType("boolean"); + + b.Property("IsFinalised") + .HasColumnType("boolean"); + + b.Property("IsHotWallet") + .HasColumnType("boolean"); + + b.Property("IsUnSortedMultiSig") + .HasColumnType("boolean"); + + b.Property("MofN") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReferenceId") + .HasColumnType("text"); + + b.Property("UpdateDatetime") + .HasColumnType("timestamp with time zone"); + + b.Property("WalletAddressType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("InternalWalletId"); + + b.HasIndex("InternalWalletSubDerivationPath", "InternalWalletMasterFingerprint") + .IsUnique(); + + b.ToTable("Wallets"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.WalletWithdrawalRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BumpingWalletWithdrawalRequestId") + .HasColumnType("integer"); + + b.Property("Changeless") + .HasColumnType("boolean"); + + b.Property("CreationDatetime") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomFeeRate") + .HasColumnType("numeric"); + + b.Property("Description") + .IsRequired() + .HasColumnType("text"); + + b.Property("MempoolRecommendedFeesType") + .HasColumnType("integer"); + + b.Property("ReferenceId") + .HasColumnType("text"); + + b.Property("RejectCancelDescription") + .HasColumnType("text"); + + b.Property("RequestMetadata") + .HasColumnType("text"); + + b.Property("Status") + .HasColumnType("integer"); + + b.Property("TxId") + .HasColumnType("text"); + + b.Property("UpdateDatetime") + .HasColumnType("timestamp with time zone"); + + b.Property("UserRequestorId") + .HasColumnType("text"); + + b.Property("WalletId") + .HasColumnType("integer"); + + b.Property("WithdrawAllFunds") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("BumpingWalletWithdrawalRequestId"); + + b.HasIndex("UserRequestorId"); + + b.HasIndex("WalletId"); + + b.ToTable("WalletWithdrawalRequests"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.WalletWithdrawalRequestDestination", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Address") + .IsRequired() + .HasColumnType("text"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("CreationDatetime") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdateDatetime") + .HasColumnType("timestamp with time zone"); + + b.Property("WalletWithdrawalRequestId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("WalletWithdrawalRequestId"); + + b.ToTable("WalletWithdrawalRequestDestinations"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.WalletWithdrawalRequestPSBT", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationDatetime") + .HasColumnType("timestamp with time zone"); + + b.Property("IsFinalisedPSBT") + .HasColumnType("boolean"); + + b.Property("IsInternalWalletPSBT") + .HasColumnType("boolean"); + + b.Property("IsTemplatePSBT") + .HasColumnType("boolean"); + + b.Property("PSBT") + .IsRequired() + .HasColumnType("text"); + + b.Property("SignerId") + .HasColumnType("text"); + + b.Property("UpdateDatetime") + .HasColumnType("timestamp with time zone"); + + b.Property("WalletWithdrawalRequestId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SignerId"); + + b.HasIndex("WalletWithdrawalRequestId"); + + b.ToTable("WalletWithdrawalRequestPSBTs"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.ApplicationUser", b => + { + b.HasBaseType("Microsoft.AspNetCore.Identity.IdentityUser"); + + b.HasDiscriminator().HasValue("ApplicationUser"); + }); + + modelBuilder.Entity("ApplicationUserNode", b => + { + b.HasOne("NodeGuard.Data.Models.Node", null) + .WithMany() + .HasForeignKey("NodesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("NodeGuard.Data.Models.ApplicationUser", null) + .WithMany() + .HasForeignKey("UsersId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("ChannelOperationRequestFMUTXO", b => + { + b.HasOne("NodeGuard.Data.Models.ChannelOperationRequest", null) + .WithMany() + .HasForeignKey("ChannelOperationRequestsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("NodeGuard.Data.Models.FMUTXO", null) + .WithMany() + .HasForeignKey("UtxosId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("FMUTXOWalletWithdrawalRequest", b => + { + b.HasOne("NodeGuard.Data.Models.FMUTXO", null) + .WithMany() + .HasForeignKey("UTXOsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("NodeGuard.Data.Models.WalletWithdrawalRequest", null) + .WithMany() + .HasForeignKey("WalletWithdrawalRequestsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("KeyWallet", b => + { + b.HasOne("NodeGuard.Data.Models.Key", null) + .WithMany() + .HasForeignKey("KeysId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("NodeGuard.Data.Models.Wallet", null) + .WithMany() + .HasForeignKey("WalletsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("Microsoft.AspNetCore.Identity.IdentityUser", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.APIToken", b => + { + b.HasOne("NodeGuard.Data.Models.ApplicationUser", "Creator") + .WithMany() + .HasForeignKey("CreatorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Creator"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.Channel", b => + { + b.HasOne("NodeGuard.Data.Models.Node", "DestinationNode") + .WithMany() + .HasForeignKey("DestinationNodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("NodeGuard.Data.Models.Node", "SourceNode") + .WithMany() + .HasForeignKey("SourceNodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("DestinationNode"); + + b.Navigation("SourceNode"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.ChannelOperationRequest", b => + { + b.HasOne("NodeGuard.Data.Models.Channel", "Channel") + .WithMany("ChannelOperationRequests") + .HasForeignKey("ChannelId"); + + b.HasOne("NodeGuard.Data.Models.Node", "DestNode") + .WithMany("ChannelOperationRequestsAsDestination") + .HasForeignKey("DestNodeId"); + + b.HasOne("NodeGuard.Data.Models.Node", "SourceNode") + .WithMany("ChannelOperationRequestsAsSource") + .HasForeignKey("SourceNodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("NodeGuard.Data.Models.ApplicationUser", "User") + .WithMany("ChannelOperationRequests") + .HasForeignKey("UserId"); + + b.HasOne("NodeGuard.Data.Models.Wallet", "Wallet") + .WithMany("ChannelOperationRequestsAsSource") + .HasForeignKey("WalletId"); + + b.Navigation("Channel"); + + b.Navigation("DestNode"); + + b.Navigation("SourceNode"); + + b.Navigation("User"); + + b.Navigation("Wallet"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.ChannelOperationRequestPSBT", b => + { + b.HasOne("NodeGuard.Data.Models.ChannelOperationRequest", "ChannelOperationRequest") + .WithMany("ChannelOperationRequestPsbts") + .HasForeignKey("ChannelOperationRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("NodeGuard.Data.Models.ApplicationUser", "UserSigner") + .WithMany() + .HasForeignKey("UserSignerId"); + + b.Navigation("ChannelOperationRequest"); + + b.Navigation("UserSigner"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.Key", b => + { + b.HasOne("NodeGuard.Data.Models.InternalWallet", "InternalWallet") + .WithMany() + .HasForeignKey("InternalWalletId"); + + b.HasOne("NodeGuard.Data.Models.ApplicationUser", "User") + .WithMany("Keys") + .HasForeignKey("UserId"); + + b.Navigation("InternalWallet"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.LiquidityRule", b => + { + b.HasOne("NodeGuard.Data.Models.Channel", "Channel") + .WithMany("LiquidityRules") + .HasForeignKey("ChannelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("NodeGuard.Data.Models.Node", "Node") + .WithMany() + .HasForeignKey("NodeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("NodeGuard.Data.Models.Wallet", "ReverseSwapWallet") + .WithMany("LiquidityRulesAsReverseSwapWallet") + .HasForeignKey("ReverseSwapWalletId"); + + b.HasOne("NodeGuard.Data.Models.Wallet", "SwapWallet") + .WithMany("LiquidityRulesAsSwapWallet") + .HasForeignKey("SwapWalletId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Channel"); + + b.Navigation("Node"); + + b.Navigation("ReverseSwapWallet"); + + b.Navigation("SwapWallet"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.Node", b => + { + b.HasOne("NodeGuard.Data.Models.Wallet", "FundsDestinationWallet") + .WithMany() + .HasForeignKey("FundsDestinationWalletId"); + + b.Navigation("FundsDestinationWallet"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.SwapOut", b => + { + b.HasOne("NodeGuard.Data.Models.Wallet", "DestinationWallet") + .WithMany("SwapOuts") + .HasForeignKey("DestinationWalletId"); + + b.HasOne("NodeGuard.Data.Models.Node", "Node") + .WithMany("SwapOuts") + .HasForeignKey("NodeId"); + + b.HasOne("NodeGuard.Data.Models.ApplicationUser", "UserRequestor") + .WithMany() + .HasForeignKey("UserRequestorId"); + + b.Navigation("DestinationWallet"); + + b.Navigation("Node"); + + b.Navigation("UserRequestor"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.Wallet", b => + { + b.HasOne("NodeGuard.Data.Models.InternalWallet", "InternalWallet") + .WithMany() + .HasForeignKey("InternalWalletId"); + + b.Navigation("InternalWallet"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.WalletWithdrawalRequest", b => + { + b.HasOne("NodeGuard.Data.Models.WalletWithdrawalRequest", "BumpingWalletWithdrawalRequest") + .WithMany() + .HasForeignKey("BumpingWalletWithdrawalRequestId"); + + b.HasOne("NodeGuard.Data.Models.ApplicationUser", "UserRequestor") + .WithMany("WalletWithdrawalRequests") + .HasForeignKey("UserRequestorId"); + + b.HasOne("NodeGuard.Data.Models.Wallet", "Wallet") + .WithMany() + .HasForeignKey("WalletId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("BumpingWalletWithdrawalRequest"); + + b.Navigation("UserRequestor"); + + b.Navigation("Wallet"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.WalletWithdrawalRequestDestination", b => + { + b.HasOne("NodeGuard.Data.Models.WalletWithdrawalRequest", "WalletWithdrawalRequest") + .WithMany("WalletWithdrawalRequestDestinations") + .HasForeignKey("WalletWithdrawalRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("WalletWithdrawalRequest"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.WalletWithdrawalRequestPSBT", b => + { + b.HasOne("NodeGuard.Data.Models.ApplicationUser", "Signer") + .WithMany() + .HasForeignKey("SignerId"); + + b.HasOne("NodeGuard.Data.Models.WalletWithdrawalRequest", "WalletWithdrawalRequest") + .WithMany("WalletWithdrawalRequestPSBTs") + .HasForeignKey("WalletWithdrawalRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Signer"); + + b.Navigation("WalletWithdrawalRequest"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.Channel", b => + { + b.Navigation("ChannelOperationRequests"); + + b.Navigation("LiquidityRules"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.ChannelOperationRequest", b => + { + b.Navigation("ChannelOperationRequestPsbts"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.Node", b => + { + b.Navigation("ChannelOperationRequestsAsDestination"); + + b.Navigation("ChannelOperationRequestsAsSource"); + + b.Navigation("SwapOuts"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.Wallet", b => + { + b.Navigation("ChannelOperationRequestsAsSource"); + + b.Navigation("LiquidityRulesAsReverseSwapWallet"); + + b.Navigation("LiquidityRulesAsSwapWallet"); + + b.Navigation("SwapOuts"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.WalletWithdrawalRequest", b => + { + b.Navigation("WalletWithdrawalRequestDestinations"); + + b.Navigation("WalletWithdrawalRequestPSBTs"); + }); + + modelBuilder.Entity("NodeGuard.Data.Models.ApplicationUser", b => + { + b.Navigation("ChannelOperationRequests"); + + b.Navigation("Keys"); + + b.Navigation("WalletWithdrawalRequests"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Migrations/20260430155542_AddChannelClosedAt.cs b/src/Migrations/20260430155542_AddChannelClosedAt.cs new file mode 100644 index 00000000..3b707008 --- /dev/null +++ b/src/Migrations/20260430155542_AddChannelClosedAt.cs @@ -0,0 +1,29 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace NodeGuard.Migrations +{ + /// + public partial class AddChannelClosedAt : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ClosedAt", + table: "Channels", + type: "timestamp with time zone", + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ClosedAt", + table: "Channels"); + } + } +} diff --git a/src/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Migrations/ApplicationDbContextModelSnapshot.cs index 3ec01885..83cfe77d 100644 --- a/src/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Migrations/ApplicationDbContextModelSnapshot.cs @@ -390,6 +390,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("ChanId") .HasColumnType("numeric(20,0)"); + b.Property("ClosedAt") + .HasColumnType("timestamp with time zone"); + b.Property("CreatedByNodeGuard") .HasColumnType("boolean"); diff --git a/src/Services/LightningService.cs b/src/Services/LightningService.cs index 81cfeaea..2aa882d3 100644 --- a/src/Services/LightningService.cs +++ b/src/Services/LightningService.cs @@ -1301,6 +1301,7 @@ public async Task CloseChannel(ChannelOperationRequest channelOperationRequest, } channel.Status = Channel.ChannelStatus.Closed; + channel.ClosedAt = DateTimeOffset.UtcNow; var updateChannelResult = _channelRepository.Update(channel); @@ -1331,6 +1332,7 @@ public async Task CloseChannel(ChannelOperationRequest channelOperationRequest, if (channel != null) { channel.Status = Channel.ChannelStatus.Closed; + channel.ClosedAt = DateTimeOffset.UtcNow; _channelRepository.Update(channel); _logger.LogInformation( From ba3d4102327c85d68f85ad00666f3b10eec063b2 Mon Sep 17 00:00:00 2001 From: Marcos Date: Thu, 30 Apr 2026 17:59:17 +0200 Subject: [PATCH 3/6] fix: set update datetime when updating channel --- src/Data/Repositories/ChannelRepository.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Data/Repositories/ChannelRepository.cs b/src/Data/Repositories/ChannelRepository.cs index d31b8824..2a8a7716 100644 --- a/src/Data/Repositories/ChannelRepository.cs +++ b/src/Data/Repositories/ChannelRepository.cs @@ -186,6 +186,8 @@ public async Task> GetAll() { using var applicationDbContext = _dbContextFactory.CreateDbContext(); + type.SetUpdateDatetime(); + //Automapper to avoid creation of entities type = _mapper.Map(type); @@ -340,6 +342,7 @@ public async Task> GetAllManagedByUserNodes(string loggedUserId) } channel.Status = Channel.ChannelStatus.Closed; + channel.ClosedAt = DateTimeOffset.UtcNow; var markAsClosed = _repository.Update(channel, applicationDbContext); From fface70a487368e28229171e3bce2944c84062d8 Mon Sep 17 00:00:00 2001 From: Marcos Date: Thu, 30 Apr 2026 20:13:05 +0200 Subject: [PATCH 4/6] feat: new graphs and panels in grafana --- .../provisioning/dashboards/routing.json | 1587 ++++++++++++++++- 1 file changed, 1568 insertions(+), 19 deletions(-) diff --git a/docker/grafana/provisioning/dashboards/routing.json b/docker/grafana/provisioning/dashboards/routing.json index d8b91dec..1461baf6 100644 --- a/docker/grafana/provisioning/dashboards/routing.json +++ b/docker/grafana/provisioning/dashboards/routing.json @@ -3619,29 +3619,1486 @@ }, "overrides": [] }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 69 + }, + "id": 30, + "options": { + "colorMode": "value", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": true + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.4.2", + "targets": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT COUNT(*) AS value FROM \"Nodes\" WHERE \"ChannelAdminMacaroon\" IS NOT NULL", + "refId": "A" + } + ], + "title": "Local Managed Nodes", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "description": "Settled forwarding revenue grouped by directional channel pair (incoming → outgoing) for the selected window. Direction is preserved.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Fee (BTC)" + }, + "properties": [ + { + "id": "unit", + "value": "currencyBTC" + }, + { + "id": "decimals", + "value": 8 + }, + { + "id": "custom.cellOptions", + "value": { + "type": "color-background-solid", + "mode": "gradient" + } + }, + { + "id": "color", + "value": { + "mode": "continuous-GrYlRd" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 0, + "y": 108 + }, + "id": 100, + "options": { + "showHeader": true, + "footer": { + "show": true, + "reducer": [ + "sum" + ], + "fields": [ + "HTLCs Settled", + "Fee (BTC)" + ] + } + }, + "pluginVersion": "12.4.2", + "targets": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "WITH channel_status AS (SELECT DISTINCT ON (\"ChanId\") \"ChanId\", CASE \"Status\" WHEN 1 THEN 'Open' WHEN 2 THEN 'Closed' ELSE 'Unknown' END AS status FROM \"Channels\" ORDER BY \"ChanId\", \"CreationDatetime\" DESC), pair_fees AS (SELECT \"ManagedNodeName\", \"IncomingChannelId\", \"OutgoingChannelId\", COALESCE(MAX(\"IncomingPeerAlias\"), '?') || ' -> ' || COALESCE(MAX(\"OutgoingPeerAlias\"), '?') AS pair_label, COUNT(*) AS htlcs, SUM(\"FeeMsat\") AS fee_msat FROM \"ForwardingHtlcEvents\" WHERE $__timeFilter(\"EventTimestamp\") AND \"Outcome\" = 1 AND \"ManagedNodePubKey\" IN ($node) AND \"FeeMsat\" IS NOT NULL GROUP BY \"ManagedNodeName\", \"IncomingChannelId\", \"OutgoingChannelId\") SELECT pf.\"ManagedNodeName\" AS \"Node\", pf.pair_label AS \"Channel Pair\", COALESCE(cs_in.status, '?') AS \"Incoming Status\", COALESCE(cs_out.status, '?') AS \"Outgoing Status\", pf.htlcs AS \"HTLCs Settled\", (pf.fee_msat / 100000000000.0)::numeric(20,8) AS \"Fee (BTC)\" FROM pair_fees pf LEFT JOIN channel_status cs_in ON cs_in.\"ChanId\" = pf.\"IncomingChannelId\" LEFT JOIN channel_status cs_out ON cs_out.\"ChanId\" = pf.\"OutgoingChannelId\" ORDER BY pf.fee_msat DESC LIMIT 50", + "refId": "A" + } + ], + "title": "Top Channel Pairs by Settled Fee Revenue", + "type": "table" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "description": "Settled forwarding revenue grouped by directional peer pair (incoming alias → outgoing alias) for the selected window. Direction is preserved.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Fee (BTC)" + }, + "properties": [ + { + "id": "unit", + "value": "currencyBTC" + }, + { + "id": "decimals", + "value": 8 + }, + { + "id": "custom.cellOptions", + "value": { + "type": "color-background-solid", + "mode": "gradient" + } + }, + { + "id": "color", + "value": { + "mode": "continuous-GrYlRd" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 108 + }, + "id": 101, + "options": { + "showHeader": true, + "footer": { + "show": true, + "reducer": [ + "sum" + ], + "fields": [ + "HTLCs settled", + "Fee (BTC)" + ] + } + }, + "pluginVersion": "12.4.2", + "targets": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT \"ManagedNodeName\" AS \"Node\", \"IncomingPeerAlias\" AS \"Incoming Peer\", \"OutgoingPeerAlias\" AS \"Outgoing Peer\", COUNT(*) AS \"HTLCs settled\", (SUM(\"FeeMsat\") / 100000000000.0)::numeric(20,8) AS \"Fee (BTC)\" FROM \"ForwardingHtlcEvents\" WHERE $__timeFilter(\"EventTimestamp\") AND \"Outcome\" = 1 AND \"ManagedNodePubKey\" IN ($node) AND \"FeeMsat\" IS NOT NULL GROUP BY \"ManagedNodeName\", \"IncomingPeerAlias\", \"OutgoingPeerAlias\" ORDER BY 5 DESC LIMIT 50", + "refId": "A" + } + ], + "title": "Top Peer Pairs by Settled Fee Revenue", + "type": "table" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "description": "Settled fee revenue (sats) over time, bucketed by Grafana's auto-interval and stacked by managed node.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "bars", + "fillOpacity": 80, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "currencyBTC", + "decimals": 8 + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 118 + }, + "id": 102, + "options": { + "legend": { + "calcs": [ + "sum" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "12.4.2", + "targets": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "editorMode": "code", + "format": "time_series", + "rawQuery": true, + "rawSql": "SELECT $__timeGroup(\"EventTimestamp\", $__interval, 0) AS \"time\", \"ManagedNodeName\" AS metric, (SUM(\"FeeMsat\") / 100000000000.0)::numeric(20,8) AS value FROM \"ForwardingHtlcEvents\" WHERE $__timeFilter(\"EventTimestamp\") AND \"Outcome\" = 1 AND \"ManagedNodePubKey\" IN ($node) AND \"FeeMsat\" IS NOT NULL GROUP BY 1, \"ManagedNodeName\" ORDER BY 1", + "refId": "A" + } + ], + "title": "Settled Fee Revenue Over Time", + "type": "timeseries" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "description": "Annualized yield on liquidity from NodeGuard-opened channels, net of swap costs. Numerator is fee_sats minus the pair's pro-rata share of completed swap-out costs on the same managed node (allocated by fee share). Denominator is each qualifying channel's SatsAmount prorated by the fraction of the window it was open: opened at Channel.CreationDatetime; close time is Channel.ClosedAt when Status = Closed. Channels not opened by NodeGuard contribute revenue but never liquidity. APR can be negative when swap costs exceed routing revenue. APR is null when the pair has no qualifying liquidity.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": ".+\\(BTC\\)$" + }, + "properties": [ + { + "id": "unit", + "value": "currencyBTC" + }, + { + "id": "decimals", + "value": 8 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "APR" + }, + "properties": [ + { + "id": "unit", + "value": "percentunit" + }, + { + "id": "decimals", + "value": 2 + }, + { + "id": "custom.cellOptions", + "value": { + "type": "color-background-solid", + "mode": "gradient" + } + }, + { + "id": "color", + "value": { + "mode": "continuous-GrYlRd" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Fee (BTC)" + }, + "properties": [ + { + "id": "unit", + "value": "currencyBTC" + }, + { + "id": "decimals", + "value": 8 + }, + { + "id": "custom.cellOptions", + "value": { + "type": "color-background-solid", + "mode": "gradient" + } + }, + { + "id": "color", + "value": { + "mode": "continuous-GrYlRd" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 127 + }, + "id": 103, + "options": { + "showHeader": true, + "footer": { + "show": true, + "reducer": [ + "sum" + ], + "fields": [ + "HTLCs settled", + "Fee (BTC)", + "Swap Cost (BTC)", + "Net Revenue (BTC)", + "Qualifying Liquidity (BTC)" + ] + } + }, + "pluginVersion": "12.4.2", + "targets": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "WITH bounds AS (SELECT $__timeFrom()::timestamptz AS window_start, $__timeTo()::timestamptz AS window_end, GREATEST(EXTRACT(EPOCH FROM ($__timeTo()::timestamptz - $__timeFrom()::timestamptz)) / 86400.0, 0.000001) AS window_days), pair_fees AS (SELECT \"ManagedNodePubKey\", MAX(\"ManagedNodeName\") AS managed_node_name, \"IncomingChannelId\", \"OutgoingChannelId\", COALESCE(MAX(\"IncomingPeerAlias\"), '?') || ' -> ' || COALESCE(MAX(\"OutgoingPeerAlias\"), '?') AS pair_label, COUNT(*) AS settled_count, ROUND(SUM(\"FeeMsat\") / 1000.0)::bigint AS fee_sats FROM \"ForwardingHtlcEvents\" WHERE $__timeFilter(\"EventTimestamp\") AND \"Outcome\" = 1 AND \"ManagedNodePubKey\" IN ($node) AND \"FeeMsat\" IS NOT NULL GROUP BY \"ManagedNodePubKey\", \"IncomingChannelId\", \"OutgoingChannelId\"), channel_status AS (SELECT DISTINCT ON (\"ChanId\") \"ChanId\", CASE \"Status\" WHEN 1 THEN 'Open' WHEN 2 THEN 'Closed' ELSE 'Unknown' END AS status FROM \"Channels\" ORDER BY \"ChanId\", \"CreationDatetime\" DESC), qualifying_channels AS (SELECT c.\"ChanId\", c.\"SatsAmount\", GREATEST(c.\"CreationDatetime\", b.window_start) AS open_start, LEAST(CASE WHEN c.\"Status\" = 2 THEN COALESCE((to_jsonb(c) ->> 'ClosedAt')::timestamptz, c.\"UpdateDatetime\") ELSE b.window_end END, b.window_end) AS open_end, b.window_days FROM \"Channels\" c CROSS JOIN bounds b WHERE (c.\"CreatedByNodeGuard\" = TRUE OR $liquidityScope = 0)), prorated AS (SELECT \"ChanId\", \"SatsAmount\" * GREATEST(EXTRACT(EPOCH FROM (open_end - open_start)) / 86400.0, 0.0) / window_days AS prorated_sats FROM qualifying_channels), swap_costs AS (SELECT n.\"PubKey\" AS managed_node_pubkey, $applySwapCosts * SUM(COALESCE(s.\"ServiceFeeSats\", 0) + COALESCE(s.\"LightningFeeSats\", 0) + COALESCE(s.\"OnChainFeeSats\", 0)) AS swap_cost_sats FROM \"SwapOuts\" s JOIN \"Nodes\" n ON n.\"Id\" = s.\"NodeId\" CROSS JOIN bounds b WHERE s.\"Status\" = 1 AND s.\"CreationDatetime\" >= b.window_start AND s.\"CreationDatetime\" < b.window_end AND n.\"PubKey\" IN ($node) GROUP BY n.\"PubKey\"), node_fees AS (SELECT \"ManagedNodePubKey\", SUM(fee_sats) AS node_total_fee_sats FROM pair_fees GROUP BY \"ManagedNodePubKey\"), pair_with_liquidity AS (SELECT pf.\"ManagedNodePubKey\", pf.managed_node_name, pf.pair_label, pf.\"IncomingChannelId\", pf.\"OutgoingChannelId\", pf.settled_count, pf.fee_sats, CASE WHEN nf.node_total_fee_sats > 0 THEN ROUND(COALESCE(sc.swap_cost_sats, 0)::numeric * pf.fee_sats / nf.node_total_fee_sats)::bigint ELSE 0 END AS allocated_swap_cost_sats, COALESCE(p_in.prorated_sats, 0) + COALESCE(p_out.prorated_sats, 0) - CASE WHEN pf.\"IncomingChannelId\" = pf.\"OutgoingChannelId\" THEN COALESCE(p_in.prorated_sats, 0) ELSE 0 END AS qualifying_liquidity_sats, COALESCE(cs_in.status, '?') AS incoming_status, COALESCE(cs_out.status, '?') AS outgoing_status FROM pair_fees pf LEFT JOIN prorated p_in ON p_in.\"ChanId\" = pf.\"IncomingChannelId\" LEFT JOIN prorated p_out ON p_out.\"ChanId\" = pf.\"OutgoingChannelId\" LEFT JOIN swap_costs sc ON sc.managed_node_pubkey = pf.\"ManagedNodePubKey\" LEFT JOIN node_fees nf ON nf.\"ManagedNodePubKey\" = pf.\"ManagedNodePubKey\" LEFT JOIN channel_status cs_in ON cs_in.\"ChanId\" = pf.\"IncomingChannelId\" LEFT JOIN channel_status cs_out ON cs_out.\"ChanId\" = pf.\"OutgoingChannelId\") SELECT managed_node_name AS \"Node\", pair_label AS \"Channel Pair\", incoming_status AS \"Incoming Status\", outgoing_status AS \"Outgoing Status\", settled_count AS \"HTLCs settled\", (fee_sats / 100000000.0)::numeric(20,8) AS \"Fee (BTC)\", (allocated_swap_cost_sats / 100000000.0)::numeric(20,8) AS \"Swap Cost (BTC)\", ((fee_sats - allocated_swap_cost_sats) / 100000000.0)::numeric(20,8) AS \"Net Revenue (BTC)\", (qualifying_liquidity_sats / 100000000.0)::numeric(20,8) AS \"Qualifying Liquidity (BTC)\", CASE WHEN qualifying_liquidity_sats > 0 THEN ((fee_sats - allocated_swap_cost_sats)::numeric / qualifying_liquidity_sats) * (365.0 / (SELECT window_days FROM bounds))::numeric ELSE NULL END AS \"APR\" FROM pair_with_liquidity ORDER BY fee_sats DESC LIMIT 50", + "refId": "A" + } + ], + "title": "NodeGuard-Liquidity APR by Channel Pair", + "type": "table" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "description": "Annualized yield rolled up to the directional peer pair (incoming alias → outgoing alias), net of swap costs. Numerator is fee_sats minus the pair's pro-rata share of the managed node's completed swap-out costs (allocated by fee share). Denominator is the union of NodeGuard-opened channels that routed inside the pair during the window, each prorated by its open fraction and counted once even if a single channel appears on both sides. Close time uses Channel.ClosedAt when Status = Closed. APR can be negative; null when the pair has no qualifying liquidity.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": ".+\\(BTC\\)$" + }, + "properties": [ + { + "id": "unit", + "value": "currencyBTC" + }, + { + "id": "decimals", + "value": 8 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "APR" + }, + "properties": [ + { + "id": "unit", + "value": "percentunit" + }, + { + "id": "decimals", + "value": 2 + }, + { + "id": "custom.cellOptions", + "value": { + "type": "color-background-solid", + "mode": "gradient" + } + }, + { + "id": "color", + "value": { + "mode": "continuous-GrYlRd" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Fee (BTC)" + }, + "properties": [ + { + "id": "unit", + "value": "currencyBTC" + }, + { + "id": "decimals", + "value": 8 + }, + { + "id": "custom.cellOptions", + "value": { + "type": "color-background-solid", + "mode": "gradient" + } + }, + { + "id": "color", + "value": { + "mode": "continuous-GrYlRd" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 137 + }, + "id": 104, + "options": { + "showHeader": true, + "footer": { + "show": true, + "reducer": [ + "sum" + ], + "fields": [ + "HTLCs settled", + "Fee (BTC)", + "Swap Cost (BTC)", + "Net Revenue (BTC)", + "Qualifying Liquidity (BTC)" + ] + } + }, + "pluginVersion": "12.4.2", + "targets": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "WITH bounds AS (SELECT $__timeFrom()::timestamptz AS window_start, $__timeTo()::timestamptz AS window_end, GREATEST(EXTRACT(EPOCH FROM ($__timeTo()::timestamptz - $__timeFrom()::timestamptz)) / 86400.0, 0.000001) AS window_days), peer_pair_events AS (SELECT \"ManagedNodePubKey\", MAX(\"ManagedNodeName\") AS managed_node_name, \"IncomingPeerAlias\" AS incoming_peer, \"OutgoingPeerAlias\" AS outgoing_peer, COUNT(*) AS settled_count, ROUND(SUM(\"FeeMsat\") / 1000.0)::bigint AS fee_sats FROM \"ForwardingHtlcEvents\" WHERE $__timeFilter(\"EventTimestamp\") AND \"Outcome\" = 1 AND \"ManagedNodePubKey\" IN ($node) AND \"FeeMsat\" IS NOT NULL GROUP BY \"ManagedNodePubKey\", \"IncomingPeerAlias\", \"OutgoingPeerAlias\"), peer_pair_channels AS (SELECT DISTINCT f.\"ManagedNodePubKey\", f.\"IncomingPeerAlias\" AS incoming_peer, f.\"OutgoingPeerAlias\" AS outgoing_peer, u.chan_id FROM \"ForwardingHtlcEvents\" f CROSS JOIN LATERAL (VALUES (f.\"IncomingChannelId\"), (f.\"OutgoingChannelId\")) AS u(chan_id) WHERE $__timeFilter(f.\"EventTimestamp\") AND f.\"Outcome\" = 1 AND f.\"ManagedNodePubKey\" IN ($node) AND f.\"FeeMsat\" IS NOT NULL), qualifying_channels AS (SELECT c.\"ChanId\", c.\"SatsAmount\", GREATEST(c.\"CreationDatetime\", b.window_start) AS open_start, LEAST(CASE WHEN c.\"Status\" = 2 THEN COALESCE((to_jsonb(c) ->> 'ClosedAt')::timestamptz, c.\"UpdateDatetime\") ELSE b.window_end END, b.window_end) AS open_end, b.window_days FROM \"Channels\" c CROSS JOIN bounds b WHERE (c.\"CreatedByNodeGuard\" = TRUE OR $liquidityScope = 0)), prorated AS (SELECT \"ChanId\", \"SatsAmount\" * GREATEST(EXTRACT(EPOCH FROM (open_end - open_start)) / 86400.0, 0.0) / window_days AS prorated_sats FROM qualifying_channels), peer_pair_liquidity AS (SELECT ppc.\"ManagedNodePubKey\", ppc.incoming_peer, ppc.outgoing_peer, SUM(p.prorated_sats) AS qualifying_liquidity_sats FROM peer_pair_channels ppc JOIN prorated p ON p.\"ChanId\" = ppc.chan_id GROUP BY ppc.\"ManagedNodePubKey\", ppc.incoming_peer, ppc.outgoing_peer), swap_costs AS (SELECT n.\"PubKey\" AS managed_node_pubkey, $applySwapCosts * SUM(COALESCE(s.\"ServiceFeeSats\", 0) + COALESCE(s.\"LightningFeeSats\", 0) + COALESCE(s.\"OnChainFeeSats\", 0)) AS swap_cost_sats FROM \"SwapOuts\" s JOIN \"Nodes\" n ON n.\"Id\" = s.\"NodeId\" CROSS JOIN bounds b WHERE s.\"Status\" = 1 AND s.\"CreationDatetime\" >= b.window_start AND s.\"CreationDatetime\" < b.window_end AND n.\"PubKey\" IN ($node) GROUP BY n.\"PubKey\"), node_fees AS (SELECT \"ManagedNodePubKey\", SUM(fee_sats) AS node_total_fee_sats FROM peer_pair_events GROUP BY \"ManagedNodePubKey\"), peer_pair_with_alloc AS (SELECT ppe.\"ManagedNodePubKey\", ppe.managed_node_name, ppe.incoming_peer, ppe.outgoing_peer, ppe.settled_count, ppe.fee_sats, CASE WHEN nf.node_total_fee_sats > 0 THEN ROUND(COALESCE(sc.swap_cost_sats, 0)::numeric * ppe.fee_sats / nf.node_total_fee_sats)::bigint ELSE 0 END AS allocated_swap_cost_sats, COALESCE(ppl.qualifying_liquidity_sats, 0) AS qualifying_liquidity_sats FROM peer_pair_events ppe LEFT JOIN peer_pair_liquidity ppl ON ppl.\"ManagedNodePubKey\" = ppe.\"ManagedNodePubKey\" AND ppl.incoming_peer IS NOT DISTINCT FROM ppe.incoming_peer AND ppl.outgoing_peer IS NOT DISTINCT FROM ppe.outgoing_peer LEFT JOIN swap_costs sc ON sc.managed_node_pubkey = ppe.\"ManagedNodePubKey\" LEFT JOIN node_fees nf ON nf.\"ManagedNodePubKey\" = ppe.\"ManagedNodePubKey\") SELECT managed_node_name AS \"Node\", (COALESCE(incoming_peer, '?') || ' -> ' || COALESCE(outgoing_peer, '?')) AS \"Peer Pair\", settled_count AS \"HTLCs settled\", (fee_sats / 100000000.0)::numeric(20,8) AS \"Fee (BTC)\", (allocated_swap_cost_sats / 100000000.0)::numeric(20,8) AS \"Swap Cost (BTC)\", ((fee_sats - allocated_swap_cost_sats) / 100000000.0)::numeric(20,8) AS \"Net Revenue (BTC)\", (qualifying_liquidity_sats / 100000000.0)::numeric(20,8) AS \"Qualifying Liquidity (BTC)\", CASE WHEN qualifying_liquidity_sats > 0 THEN ((fee_sats - allocated_swap_cost_sats)::numeric / qualifying_liquidity_sats) * (365.0 / (SELECT window_days FROM bounds))::numeric ELSE NULL END AS \"APR\" FROM peer_pair_with_alloc ORDER BY fee_sats DESC LIMIT 50", + "refId": "A" + } + ], + "title": "NodeGuard-Liquidity APR by Peer Pair", + "type": "table" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "description": "Combined annualized yield across all settled forwards in the window, net of swap costs. Numerator is total fee_sats minus the sum of completed swap-out costs (Service + Lightning + OnChain fees) on the selected nodes in the window. Denominator is the sum of prorated SatsAmount across every distinct NodeGuard-opened channel that routed at least one settled forward in the window (each counted once total). APR can be negative.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "orange", + "value": 0.02 + }, + { + "color": "yellow", + "value": 0.05 + }, + { + "color": "green", + "value": 0.08 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 100 + }, + "id": 105, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.4.2", + "targets": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "WITH bounds AS (SELECT $__timeFrom()::timestamptz AS window_start, $__timeTo()::timestamptz AS window_end, GREATEST(EXTRACT(EPOCH FROM ($__timeTo()::timestamptz - $__timeFrom()::timestamptz)) / 86400.0, 0.000001) AS window_days), total_fees AS (SELECT COALESCE(ROUND(SUM(\"FeeMsat\") / 1000.0)::bigint, 0) AS fee_sats FROM \"ForwardingHtlcEvents\" WHERE $__timeFilter(\"EventTimestamp\") AND \"Outcome\" = 1 AND \"ManagedNodePubKey\" IN ($node) AND \"FeeMsat\" IS NOT NULL), total_swap_costs AS (SELECT $applySwapCosts * COALESCE(SUM(COALESCE(s.\"ServiceFeeSats\", 0) + COALESCE(s.\"LightningFeeSats\", 0) + COALESCE(s.\"OnChainFeeSats\", 0)), 0) AS swap_cost_sats FROM \"SwapOuts\" s JOIN \"Nodes\" n ON n.\"Id\" = s.\"NodeId\" CROSS JOIN bounds b WHERE s.\"Status\" = 1 AND s.\"CreationDatetime\" >= b.window_start AND s.\"CreationDatetime\" < b.window_end AND n.\"PubKey\" IN ($node)), participating_chans AS (SELECT DISTINCT u.chan_id FROM \"ForwardingHtlcEvents\" f CROSS JOIN LATERAL (VALUES (f.\"IncomingChannelId\"), (f.\"OutgoingChannelId\")) AS u(chan_id) WHERE $__timeFilter(f.\"EventTimestamp\") AND f.\"Outcome\" = 1 AND f.\"ManagedNodePubKey\" IN ($node) AND f.\"FeeMsat\" IS NOT NULL), qualifying_liquidity AS (SELECT COALESCE(SUM(c.\"SatsAmount\" * GREATEST(EXTRACT(EPOCH FROM (LEAST(CASE WHEN c.\"Status\" = 2 THEN COALESCE((to_jsonb(c) ->> 'ClosedAt')::timestamptz, c.\"UpdateDatetime\") ELSE b.window_end END, b.window_end) - GREATEST(c.\"CreationDatetime\", b.window_start))) / 86400.0, 0.0) / b.window_days), 0) AS liquidity_sats FROM \"Channels\" c CROSS JOIN bounds b JOIN participating_chans p ON p.chan_id = c.\"ChanId\" WHERE (c.\"CreatedByNodeGuard\" = TRUE OR $liquidityScope = 0)) SELECT CASE WHEN ql.liquidity_sats > 0 THEN ((tf.fee_sats - tsc.swap_cost_sats)::numeric / ql.liquidity_sats) * (365.0 / b.window_days)::numeric ELSE NULL END AS \"APR\" FROM total_fees tf, total_swap_costs tsc, qualifying_liquidity ql, bounds b", + "refId": "A" + } + ], + "title": "Combined NodeGuard-Liquidity APR", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "description": "Period yield as a percentage — the actual return on NodeGuard liquidity for the selected window, NOT annualized. Equal to (settled fee revenue − completed swap-out costs) / qualifying_liquidity. The Combined APR panel takes this same value and multiplies by 365/window_days to annualize. Respects the Apply Swap Costs toggle. Can be negative.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 4, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "green", + "value": 0 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 100 + }, + "id": 109, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "percentChangeColorMode": "standard", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showPercentChange": false, + "textMode": "auto", + "wideLayout": true + }, + "pluginVersion": "12.4.2", + "targets": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "WITH bounds AS (SELECT $__timeFrom()::timestamptz AS window_start, $__timeTo()::timestamptz AS window_end), total_fees AS (SELECT COALESCE(ROUND(SUM(\"FeeMsat\") / 1000.0)::bigint, 0) AS fee_sats FROM \"ForwardingHtlcEvents\" WHERE $__timeFilter(\"EventTimestamp\") AND \"Outcome\" = 1 AND \"ManagedNodePubKey\" IN ($node) AND \"FeeMsat\" IS NOT NULL), total_swap_costs AS (SELECT $applySwapCosts * COALESCE(SUM(COALESCE(s.\"ServiceFeeSats\", 0) + COALESCE(s.\"LightningFeeSats\", 0) + COALESCE(s.\"OnChainFeeSats\", 0)), 0) AS swap_cost_sats FROM \"SwapOuts\" s JOIN \"Nodes\" n ON n.\"Id\" = s.\"NodeId\" CROSS JOIN bounds b WHERE s.\"Status\" = 1 AND s.\"CreationDatetime\" >= b.window_start AND s.\"CreationDatetime\" < b.window_end AND n.\"PubKey\" IN ($node)), participating_chans AS (SELECT DISTINCT u.chan_id FROM \"ForwardingHtlcEvents\" f CROSS JOIN LATERAL (VALUES (f.\"IncomingChannelId\"), (f.\"OutgoingChannelId\")) AS u(chan_id) WHERE $__timeFilter(f.\"EventTimestamp\") AND f.\"Outcome\" = 1 AND f.\"ManagedNodePubKey\" IN ($node) AND f.\"FeeMsat\" IS NOT NULL), qualifying_liquidity AS (SELECT COALESCE(SUM(c.\"SatsAmount\" * GREATEST(EXTRACT(EPOCH FROM (LEAST(CASE WHEN c.\"Status\" = 2 THEN COALESCE((to_jsonb(c) ->> 'ClosedAt')::timestamptz, c.\"UpdateDatetime\") ELSE b.window_end END, b.window_end) - GREATEST(c.\"CreationDatetime\", b.window_start))) / 86400.0, 0.0) / GREATEST(EXTRACT(EPOCH FROM (b.window_end - b.window_start)) / 86400.0, 0.000001)), 0) AS liquidity_sats FROM \"Channels\" c CROSS JOIN bounds b JOIN participating_chans p ON p.chan_id = c.\"ChanId\" WHERE (c.\"CreatedByNodeGuard\" = TRUE OR $liquidityScope = 0)) SELECT CASE WHEN ql.liquidity_sats > 0 THEN ((tf.fee_sats - tsc.swap_cost_sats)::numeric / ql.liquidity_sats) ELSE NULL END AS \"Raw Yield\" FROM total_fees tf, total_swap_costs tsc, qualifying_liquidity ql", + "refId": "A" + } + ], + "title": "Combined Raw Yield", + "type": "stat" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "description": "Per-node annualized yield, net of that node's completed swap-out costs. APR = ((node_fee_sats - node_swap_cost_sats) / node_qualifying_liquidity_sats) * (365 / window_days). A node's qualifying liquidity is the sum of prorated SatsAmount of NodeGuard-opened channels that routed at least one settled forward for that node in the window, each counted once. APR can be negative.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "red", + "value": null + }, + { + "color": "orange", + "value": 0.02 + }, + { + "color": "yellow", + "value": 0.05 + }, + { + "color": "green", + "value": 0.08 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, "gridPos": { "h": 7, - "w": 6, + "w": 8, + "x": 16, + "y": 100 + }, + "id": 107, + "options": { + "displayMode": "gradient", + "minVizHeight": 16, + "minVizWidth": 8, + "namePlacement": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": true + }, + "showUnfilled": true, + "sizing": "auto", + "valueMode": "color" + }, + "pluginVersion": "12.4.2", + "targets": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "WITH bounds AS (SELECT $__timeFrom()::timestamptz AS window_start, $__timeTo()::timestamptz AS window_end, GREATEST(EXTRACT(EPOCH FROM ($__timeTo()::timestamptz - $__timeFrom()::timestamptz)) / 86400.0, 0.000001) AS window_days), node_fees AS (SELECT \"ManagedNodePubKey\", MAX(\"ManagedNodeName\") AS managed_node_name, ROUND(SUM(\"FeeMsat\") / 1000.0)::bigint AS fee_sats FROM \"ForwardingHtlcEvents\" WHERE $__timeFilter(\"EventTimestamp\") AND \"Outcome\" = 1 AND \"ManagedNodePubKey\" IN ($node) AND \"FeeMsat\" IS NOT NULL GROUP BY \"ManagedNodePubKey\"), node_swap_costs AS (SELECT n.\"PubKey\" AS managed_node_pubkey, $applySwapCosts * SUM(COALESCE(s.\"ServiceFeeSats\", 0) + COALESCE(s.\"LightningFeeSats\", 0) + COALESCE(s.\"OnChainFeeSats\", 0)) AS swap_cost_sats FROM \"SwapOuts\" s JOIN \"Nodes\" n ON n.\"Id\" = s.\"NodeId\" WHERE s.\"Status\" = 1 AND $__timeFilter(s.\"CreationDatetime\") AND n.\"PubKey\" IN ($node) GROUP BY n.\"PubKey\"), node_participating_chans AS (SELECT DISTINCT f.\"ManagedNodePubKey\", u.chan_id FROM \"ForwardingHtlcEvents\" f CROSS JOIN LATERAL (VALUES (f.\"IncomingChannelId\"), (f.\"OutgoingChannelId\")) AS u(chan_id) WHERE $__timeFilter(f.\"EventTimestamp\") AND f.\"Outcome\" = 1 AND f.\"ManagedNodePubKey\" IN ($node) AND f.\"FeeMsat\" IS NOT NULL), node_liquidity AS (SELECT npc.\"ManagedNodePubKey\", SUM(c.\"SatsAmount\" * GREATEST(EXTRACT(EPOCH FROM (LEAST(CASE WHEN c.\"Status\" = 2 THEN COALESCE((to_jsonb(c) ->> 'ClosedAt')::timestamptz, c.\"UpdateDatetime\") ELSE b.window_end END, b.window_end) - GREATEST(c.\"CreationDatetime\", b.window_start))) / 86400.0, 0.0) / b.window_days) AS liquidity_sats FROM node_participating_chans npc JOIN \"Channels\" c ON c.\"ChanId\" = npc.chan_id CROSS JOIN bounds b WHERE (c.\"CreatedByNodeGuard\" = TRUE OR $liquidityScope = 0) GROUP BY npc.\"ManagedNodePubKey\") SELECT nf.managed_node_name AS \"Node\", CASE WHEN nl.liquidity_sats > 0 THEN ((nf.fee_sats - COALESCE(nsc.swap_cost_sats, 0))::numeric / nl.liquidity_sats) * (365.0 / b.window_days)::numeric ELSE NULL END AS \"APR\" FROM node_fees nf LEFT JOIN node_liquidity nl ON nl.\"ManagedNodePubKey\" = nf.\"ManagedNodePubKey\" LEFT JOIN node_swap_costs nsc ON nsc.managed_node_pubkey = nf.\"ManagedNodePubKey\" CROSS JOIN bounds b ORDER BY \"APR\" DESC NULLS LAST", + "refId": "A" + } + ], + "title": "NodeGuard-Liquidity APR by Node", + "type": "bargauge" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "description": "Diagnostic view of swap-out records that drive the swap-cost subtraction in the APR panels above. Includes Pending and Failed swaps for visibility, but only Completed swaps with non-null NodeId are subtracted from APR. Columns expose ServiceFee + LightningFee + OnChainFee components so you can verify the Total Fees figure that flows into the APR numerator.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": ".+\\(BTC\\)$" + }, + "properties": [ + { + "id": "unit", + "value": "currencyBTC" + }, + { + "id": "decimals", + "value": 8 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Status" + }, + "properties": [ + { + "id": "mappings", + "value": [ + { + "type": "value", + "options": { + "Completed": { + "color": "green", + "index": 0 + }, + "Pending": { + "color": "yellow", + "index": 1 + }, + "Failed": { + "color": "red", + "index": 2 + } + } + } + ] + }, + { + "id": "custom.cellOptions", + "value": { + "type": "color-text" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total Fees (BTC)" + }, + "properties": [ + { + "id": "custom.cellOptions", + "value": { + "type": "color-background-solid", + "mode": "gradient" + } + }, + { + "id": "color", + "value": { + "mode": "continuous-GrYlRd" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 168 + }, + "id": 108, + "options": { + "showHeader": true, + "footer": { + "show": true, + "reducer": [ + "sum", + "count" + ], + "fields": [ + "Amount (BTC)", + "Total Fees (BTC)" + ] + } + }, + "pluginVersion": "12.4.2", + "targets": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "SELECT s.\"CreationDatetime\" AS \"Date\", COALESCE(n.\"Name\", '(no node)') AS \"Node\", CASE s.\"Provider\" WHEN 0 THEN 'Loop' WHEN 1 THEN '40swap' ELSE '?' END AS \"Provider\", CASE s.\"Status\" WHEN 0 THEN 'Pending' WHEN 1 THEN 'Completed' WHEN 2 THEN 'Failed' ELSE '?' END AS \"Status\", CASE WHEN s.\"IsManual\" THEN 'Manual' ELSE 'Auto' END AS \"Type\", (s.\"SatsAmount\" / 100000000.0)::numeric(20,8) AS \"Amount (BTC)\", ((COALESCE(s.\"ServiceFeeSats\", 0) + COALESCE(s.\"LightningFeeSats\", 0) + COALESCE(s.\"OnChainFeeSats\", 0)) / 100000000.0)::numeric(20,8) AS \"Total Fees (BTC)\" FROM \"SwapOuts\" s LEFT JOIN \"Nodes\" n ON n.\"Id\" = s.\"NodeId\" WHERE $__timeFilter(s.\"CreationDatetime\") AND (s.\"NodeId\" IS NULL OR n.\"PubKey\" IN ($node)) ORDER BY s.\"CreationDatetime\" DESC", + "refId": "A" + } + ], + "title": "Recent Swaps (Diagnostic)", + "type": "table" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "description": "Annualized yield attributed to each individual NodeGuard channel. Each settled forward credits BOTH its incoming and outgoing channel with the full fee (full-attribution, same convention as the existing 'Routed fee distribution From/To' panels). Denominator is this channel's prorated SatsAmount alone. Allocated swap cost is the channel's pro-rata share of its managed node's total swap cost (by fee share within the node, double-credit basis). APR can be negative; null when the channel has no qualifying liquidity (i.e. not opened by NodeGuard).", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": ".+\\(BTC\\)$" + }, + "properties": [ + { + "id": "unit", + "value": "currencyBTC" + }, + { + "id": "decimals", + "value": 8 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "APR" + }, + "properties": [ + { + "id": "unit", + "value": "percentunit" + }, + { + "id": "decimals", + "value": 2 + }, + { + "id": "custom.cellOptions", + "value": { + "type": "color-background-solid", + "mode": "gradient" + } + }, + { + "id": "color", + "value": { + "mode": "continuous-GrYlRd" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Fee (BTC)" + }, + "properties": [ + { + "id": "unit", + "value": "currencyBTC" + }, + { + "id": "decimals", + "value": 8 + }, + { + "id": "custom.cellOptions", + "value": { + "type": "color-background-solid", + "mode": "gradient" + } + }, + { + "id": "color", + "value": { + "mode": "continuous-GrYlRd" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 147 + }, + "id": 110, + "options": { + "showHeader": true, + "footer": { + "show": true, + "reducer": [ + "sum" + ], + "fields": [ + "HTLC Settled", + "Fee (BTC)", + "Swap Cost (BTC)", + "Net Revenue (BTC)", + "Qualifying Liquidity (BTC)" + ] + } + }, + "pluginVersion": "12.4.2", + "targets": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "WITH bounds AS (SELECT $__timeFrom()::timestamptz AS window_start, $__timeTo()::timestamptz AS window_end, GREATEST(EXTRACT(EPOCH FROM ($__timeTo()::timestamptz - $__timeFrom()::timestamptz)) / 86400.0, 0.000001) AS window_days), channel_events AS (SELECT f.\"ManagedNodePubKey\", MAX(f.\"ManagedNodeName\") AS managed_node_name, u.chan_id, MAX(u.peer_alias) AS peer_alias, COUNT(*) AS settled_count, ROUND(SUM(f.\"FeeMsat\") / 1000.0)::bigint AS fee_sats FROM \"ForwardingHtlcEvents\" f CROSS JOIN LATERAL (VALUES (f.\"IncomingChannelId\", f.\"IncomingPeerAlias\"), (f.\"OutgoingChannelId\", f.\"OutgoingPeerAlias\")) AS u(chan_id, peer_alias) WHERE $__timeFilter(f.\"EventTimestamp\") AND f.\"Outcome\" = 1 AND f.\"ManagedNodePubKey\" IN ($node) AND f.\"FeeMsat\" IS NOT NULL GROUP BY f.\"ManagedNodePubKey\", u.chan_id), channel_status AS (SELECT DISTINCT ON (\"ChanId\") \"ChanId\", CASE \"Status\" WHEN 1 THEN 'Open' WHEN 2 THEN 'Closed' ELSE 'Unknown' END AS status FROM \"Channels\" ORDER BY \"ChanId\", \"CreationDatetime\" DESC), qualifying_channels AS (SELECT c.\"ChanId\", c.\"SatsAmount\", GREATEST(c.\"CreationDatetime\", b.window_start) AS open_start, LEAST(CASE WHEN c.\"Status\" = 2 THEN COALESCE((to_jsonb(c) ->> 'ClosedAt')::timestamptz, c.\"UpdateDatetime\") ELSE b.window_end END, b.window_end) AS open_end, b.window_days FROM \"Channels\" c CROSS JOIN bounds b WHERE (c.\"CreatedByNodeGuard\" = TRUE OR $liquidityScope = 0)), prorated AS (SELECT \"ChanId\", \"SatsAmount\" * GREATEST(EXTRACT(EPOCH FROM (open_end - open_start)) / 86400.0, 0.0) / window_days AS prorated_sats FROM qualifying_channels), all_channel_sizes AS (SELECT \"ChanId\", MAX(\"SatsAmount\") AS sats_amount FROM \"Channels\" GROUP BY \"ChanId\"), swap_costs AS (SELECT n.\"PubKey\" AS managed_node_pubkey, $applySwapCosts * SUM(COALESCE(s.\"ServiceFeeSats\", 0) + COALESCE(s.\"LightningFeeSats\", 0) + COALESCE(s.\"OnChainFeeSats\", 0)) AS swap_cost_sats FROM \"SwapOuts\" s JOIN \"Nodes\" n ON n.\"Id\" = s.\"NodeId\" CROSS JOIN bounds b WHERE s.\"Status\" = 1 AND s.\"CreationDatetime\" >= b.window_start AND s.\"CreationDatetime\" < b.window_end AND n.\"PubKey\" IN ($node) GROUP BY n.\"PubKey\"), node_fees AS (SELECT \"ManagedNodePubKey\", SUM(fee_sats) AS node_total_fee_sats FROM channel_events GROUP BY \"ManagedNodePubKey\"), channels_with_meta AS (SELECT ce.\"ManagedNodePubKey\", ce.managed_node_name, ce.chan_id, ce.peer_alias, ce.settled_count, ce.fee_sats, COALESCE(acs.sats_amount, 0) AS channel_size_sats, COALESCE(p.prorated_sats, 0) AS qualifying_liquidity_sats, CASE WHEN nf.node_total_fee_sats > 0 THEN ROUND(COALESCE(sc.swap_cost_sats, 0)::numeric * ce.fee_sats / nf.node_total_fee_sats)::bigint ELSE 0 END AS allocated_swap_cost_sats, COALESCE(cs.status, '?') AS status FROM channel_events ce LEFT JOIN prorated p ON p.\"ChanId\" = ce.chan_id LEFT JOIN all_channel_sizes acs ON acs.\"ChanId\" = ce.chan_id LEFT JOIN swap_costs sc ON sc.managed_node_pubkey = ce.\"ManagedNodePubKey\" LEFT JOIN node_fees nf ON nf.\"ManagedNodePubKey\" = ce.\"ManagedNodePubKey\" LEFT JOIN channel_status cs ON cs.\"ChanId\" = ce.chan_id) SELECT managed_node_name AS \"Node\", COALESCE(peer_alias, '?') AS \"Channel (peer)\", chan_id::text AS \"Chan ID\", status AS \"Status\", (channel_size_sats / 100000000.0)::numeric(20,8) AS \"Channel Size (BTC)\", settled_count AS \"HTLC Settled\", (fee_sats / 100000000.0)::numeric(20,8) AS \"Fee (BTC)\", (allocated_swap_cost_sats / 100000000.0)::numeric(20,8) AS \"Swap Cost (BTC)\", ((fee_sats - allocated_swap_cost_sats) / 100000000.0)::numeric(20,8) AS \"Net Revenue (BTC)\", (qualifying_liquidity_sats / 100000000.0)::numeric(20,8) AS \"Qualifying Liquidity (BTC)\", CASE WHEN qualifying_liquidity_sats > 0 THEN ((fee_sats - allocated_swap_cost_sats)::numeric / qualifying_liquidity_sats) * (365.0 / (SELECT window_days FROM bounds))::numeric ELSE NULL END AS \"APR\" FROM channels_with_meta ORDER BY fee_sats DESC LIMIT 50", + "refId": "A" + } + ], + "title": "NodeGuard-Liquidity APR by Channel", + "type": "table" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "description": "Annualized yield attributed to each individual peer (by alias). Each settled forward credits BOTH the incoming and outgoing peer with the full fee (full-attribution). Denominator is the union of NodeGuard-opened channels associated with the peer, each prorated by its open fraction in the window and counted once. Allocated swap cost is the peer's pro-rata share of its managed node's swap cost. APR can be negative; null when the peer has no qualifying NodeGuard liquidity.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "cellOptions": { + "type": "auto" + }, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": ".+\\(BTC\\)$" + }, + "properties": [ + { + "id": "unit", + "value": "currencyBTC" + }, + { + "id": "decimals", + "value": 8 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "APR" + }, + "properties": [ + { + "id": "unit", + "value": "percentunit" + }, + { + "id": "decimals", + "value": 2 + }, + { + "id": "custom.cellOptions", + "value": { + "type": "color-background-solid", + "mode": "gradient" + } + }, + { + "id": "color", + "value": { + "mode": "continuous-GrYlRd" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Fee (BTC)" + }, + "properties": [ + { + "id": "unit", + "value": "currencyBTC" + }, + { + "id": "decimals", + "value": 8 + }, + { + "id": "custom.cellOptions", + "value": { + "type": "color-background-solid", + "mode": "gradient" + } + }, + { + "id": "color", + "value": { + "mode": "continuous-GrYlRd" + } + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 24, "x": 0, - "y": 102 + "y": 157 }, - "id": 30, + "id": 111, "options": { - "colorMode": "value", - "graphMode": "none", - "justifyMode": "auto", - "orientation": "auto", - "percentChangeColorMode": "standard", - "reduceOptions": { + "showHeader": true, + "footer": { + "show": true, + "reducer": [ + "sum" + ], + "fields": [ + "HTLC Settled", + "Fee (BTC)", + "Swap Cost (BTC)", + "Net Revenue (BTC)", + "Qualifying Liquidity (BTC)" + ] + } + }, + "pluginVersion": "12.4.2", + "targets": [ + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "editorMode": "code", + "format": "table", + "rawQuery": true, + "rawSql": "WITH bounds AS (SELECT $__timeFrom()::timestamptz AS window_start, $__timeTo()::timestamptz AS window_end, GREATEST(EXTRACT(EPOCH FROM ($__timeTo()::timestamptz - $__timeFrom()::timestamptz)) / 86400.0, 0.000001) AS window_days), peer_events AS (SELECT f.\"ManagedNodePubKey\", MAX(f.\"ManagedNodeName\") AS managed_node_name, u.peer_alias, COUNT(*) AS settled_count, ROUND(SUM(f.\"FeeMsat\") / 1000.0)::bigint AS fee_sats FROM \"ForwardingHtlcEvents\" f CROSS JOIN LATERAL (VALUES (f.\"IncomingPeerAlias\"), (f.\"OutgoingPeerAlias\")) AS u(peer_alias) WHERE $__timeFilter(f.\"EventTimestamp\") AND f.\"Outcome\" = 1 AND f.\"ManagedNodePubKey\" IN ($node) AND f.\"FeeMsat\" IS NOT NULL GROUP BY f.\"ManagedNodePubKey\", u.peer_alias), peer_chans AS (SELECT DISTINCT f.\"ManagedNodePubKey\", u.peer_alias, u.chan_id FROM \"ForwardingHtlcEvents\" f CROSS JOIN LATERAL (VALUES (f.\"IncomingPeerAlias\", f.\"IncomingChannelId\"), (f.\"OutgoingPeerAlias\", f.\"OutgoingChannelId\")) AS u(peer_alias, chan_id) WHERE $__timeFilter(f.\"EventTimestamp\") AND f.\"Outcome\" = 1 AND f.\"ManagedNodePubKey\" IN ($node) AND f.\"FeeMsat\" IS NOT NULL), qualifying_channels AS (SELECT c.\"ChanId\", c.\"SatsAmount\", GREATEST(c.\"CreationDatetime\", b.window_start) AS open_start, LEAST(CASE WHEN c.\"Status\" = 2 THEN COALESCE((to_jsonb(c) ->> 'ClosedAt')::timestamptz, c.\"UpdateDatetime\") ELSE b.window_end END, b.window_end) AS open_end, b.window_days FROM \"Channels\" c CROSS JOIN bounds b WHERE (c.\"CreatedByNodeGuard\" = TRUE OR $liquidityScope = 0)), prorated AS (SELECT \"ChanId\", \"SatsAmount\" * GREATEST(EXTRACT(EPOCH FROM (open_end - open_start)) / 86400.0, 0.0) / window_days AS prorated_sats FROM qualifying_channels), peer_liquidity AS (SELECT pc.\"ManagedNodePubKey\", pc.peer_alias, SUM(p.prorated_sats) AS qualifying_liquidity_sats FROM peer_chans pc JOIN prorated p ON p.\"ChanId\" = pc.chan_id GROUP BY pc.\"ManagedNodePubKey\", pc.peer_alias), swap_costs AS (SELECT n.\"PubKey\" AS managed_node_pubkey, $applySwapCosts * SUM(COALESCE(s.\"ServiceFeeSats\", 0) + COALESCE(s.\"LightningFeeSats\", 0) + COALESCE(s.\"OnChainFeeSats\", 0)) AS swap_cost_sats FROM \"SwapOuts\" s JOIN \"Nodes\" n ON n.\"Id\" = s.\"NodeId\" CROSS JOIN bounds b WHERE s.\"Status\" = 1 AND s.\"CreationDatetime\" >= b.window_start AND s.\"CreationDatetime\" < b.window_end AND n.\"PubKey\" IN ($node) GROUP BY n.\"PubKey\"), node_fees AS (SELECT \"ManagedNodePubKey\", SUM(fee_sats) AS node_total_fee_sats FROM peer_events GROUP BY \"ManagedNodePubKey\") SELECT pe.managed_node_name AS \"Node\", COALESCE(pe.peer_alias, '?') AS \"Peer\", pe.settled_count AS \"HTLC Settled\", (pe.fee_sats / 100000000.0)::numeric(20,8) AS \"Fee (BTC)\", (CASE WHEN nf.node_total_fee_sats > 0 THEN ROUND(COALESCE(sc.swap_cost_sats, 0)::numeric * pe.fee_sats / nf.node_total_fee_sats)::bigint ELSE 0 END / 100000000.0)::numeric(20,8) AS \"Swap Cost (BTC)\", ((pe.fee_sats - CASE WHEN nf.node_total_fee_sats > 0 THEN ROUND(COALESCE(sc.swap_cost_sats, 0)::numeric * pe.fee_sats / nf.node_total_fee_sats)::bigint ELSE 0 END) / 100000000.0)::numeric(20,8) AS \"Net Revenue (BTC)\", (COALESCE(pl.qualifying_liquidity_sats, 0) / 100000000.0)::numeric(20,8) AS \"Qualifying Liquidity (BTC)\", CASE WHEN pl.qualifying_liquidity_sats > 0 THEN ((pe.fee_sats - CASE WHEN nf.node_total_fee_sats > 0 THEN ROUND(COALESCE(sc.swap_cost_sats, 0)::numeric * pe.fee_sats / nf.node_total_fee_sats)::bigint ELSE 0 END)::numeric / pl.qualifying_liquidity_sats) * (365.0 / (SELECT window_days FROM bounds))::numeric ELSE NULL END AS \"APR\" FROM peer_events pe LEFT JOIN peer_liquidity pl ON pl.\"ManagedNodePubKey\" = pe.\"ManagedNodePubKey\" AND pl.peer_alias IS NOT DISTINCT FROM pe.peer_alias LEFT JOIN swap_costs sc ON sc.managed_node_pubkey = pe.\"ManagedNodePubKey\" LEFT JOIN node_fees nf ON nf.\"ManagedNodePubKey\" = pe.\"ManagedNodePubKey\" ORDER BY pe.fee_sats DESC LIMIT 50", + "refId": "A" + } + ], + "title": "NodeGuard-Liquidity APR by Peer", + "type": "table" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 99 + }, + "id": 200, + "panels": [], + "title": "Yield — Headlines", + "type": "row" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 107 + }, + "id": 201, + "panels": [], + "title": "Yield — Routing Activity", + "type": "row" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 126 + }, + "id": 202, + "panels": [], + "title": "Yield — APR Breakdowns", + "type": "row" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 167 + }, + "id": 203, + "panels": [], + "title": "Yield — Swap Diagnostics", + "type": "row" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 178 + }, + "id": 204, + "panels": [], + "title": "Yield — APR Trends", + "type": "row" + }, + { + "datasource": { + "type": "grafana-postgresql-datasource", + "uid": "$datasource" + }, + "description": "Rolling APR (left axis, percent) and rolling-average Qualifying Liquidity (right axis, BTC) per managed node, in the style of \"Profit over time\". Hourly buckets; each plotted point is the rolling APR over the last $aprWindow ending that hour, paired with the average qualifying liquidity deployed over the same rolling window. Designed for self-comparison within a node: visually spot whether adding/removing liquidity tracks with APR moving up or down. Honors $applySwapCosts and $liquidityScope. APR can be negative.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisBorderShow": false, + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": ".* APR$" + }, + "properties": [ + { + "id": "unit", + "value": "percentunit" + }, + { + "id": "decimals", + "value": 2 + }, + { + "id": "custom.axisPlacement", + "value": "left" + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": ".* Liquidity \\(BTC\\)$" + }, + "properties": [ + { + "id": "unit", + "value": "currencyBTC" + }, + { + "id": "decimals", + "value": 8 + }, + { + "id": "custom.axisPlacement", + "value": "right" + }, + { + "id": "custom.lineStyle", + "value": { + "fill": "dash", + "dash": [ + 10, + 10 + ] + } + } + ] + } + ] + }, + "gridPos": { + "h": 9, + "w": 24, + "x": 0, + "y": 179 + }, + "id": 111, + "options": { + "legend": { "calcs": [ + "mean", "lastNotNull" ], - "fields": "", - "values": true + "displayMode": "table", + "placement": "bottom", + "showLegend": true }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true + "tooltip": { + "mode": "multi", + "sort": "desc" + } }, "pluginVersion": "12.4.2", "targets": [ @@ -3651,14 +5108,14 @@ "uid": "$datasource" }, "editorMode": "code", - "format": "table", + "format": "time_series", "rawQuery": true, - "rawSql": "SELECT COUNT(*) AS value FROM \"Nodes\" WHERE \"ChannelAdminMacaroon\" IS NOT NULL", + "rawSql": "WITH days AS (SELECT generate_series(date_trunc('hour', $__timeFrom()::timestamptz), $__timeTo()::timestamptz, INTERVAL '1 hour') AS bucket_start), day_buckets AS (SELECT bucket_start, bucket_start + INTERVAL '1 hour' AS bucket_end FROM days), nodes AS (SELECT \"ManagedNodePubKey\" AS pubkey, MAX(\"ManagedNodeName\") AS node_name FROM \"ForwardingHtlcEvents\" WHERE $__timeFilter(\"EventTimestamp\") AND \"ManagedNodePubKey\" IN ($node) GROUP BY 1), participating_node_chans AS (SELECT DISTINCT f.\"ManagedNodePubKey\" AS pubkey, u.chan_id FROM \"ForwardingHtlcEvents\" f CROSS JOIN LATERAL (VALUES (f.\"IncomingChannelId\"), (f.\"OutgoingChannelId\")) u(chan_id) WHERE $__timeFilter(f.\"EventTimestamp\") AND f.\"Outcome\" = 1 AND f.\"FeeMsat\" IS NOT NULL AND f.\"ManagedNodePubKey\" IN ($node)), day_node_grid AS (SELECT db.bucket_start, db.bucket_end, n.pubkey, n.node_name FROM day_buckets db CROSS JOIN nodes n), day_fees AS (SELECT g.bucket_start, g.pubkey, g.node_name, COALESCE(ROUND(SUM(f.\"FeeMsat\") / 1000.0)::bigint, 0) AS fee_sats FROM day_node_grid g LEFT JOIN \"ForwardingHtlcEvents\" f ON f.\"EventTimestamp\" >= g.bucket_start AND f.\"EventTimestamp\" < g.bucket_end AND f.\"Outcome\" = 1 AND f.\"FeeMsat\" IS NOT NULL AND f.\"ManagedNodePubKey\" = g.pubkey GROUP BY g.bucket_start, g.pubkey, g.node_name), day_swap_costs AS (SELECT g.bucket_start, g.pubkey, $applySwapCosts * COALESCE(SUM(COALESCE(s.\"ServiceFeeSats\", 0) + COALESCE(s.\"LightningFeeSats\", 0) + COALESCE(s.\"OnChainFeeSats\", 0)), 0) AS swap_cost_sats FROM day_node_grid g LEFT JOIN \"Nodes\" n ON n.\"PubKey\" = g.pubkey LEFT JOIN \"SwapOuts\" s ON s.\"NodeId\" = n.\"Id\" AND s.\"Status\" = 1 AND s.\"CreationDatetime\" >= g.bucket_start AND s.\"CreationDatetime\" < g.bucket_end GROUP BY g.bucket_start, g.pubkey), day_liquidity AS (SELECT g.bucket_start, g.pubkey, COALESCE(SUM(c.\"SatsAmount\" * GREATEST(EXTRACT(EPOCH FROM (LEAST(CASE WHEN c.\"Status\" = 2 THEN COALESCE((to_jsonb(c) ->> 'ClosedAt')::timestamptz, c.\"UpdateDatetime\") ELSE g.bucket_end END, g.bucket_end) - GREATEST(c.\"CreationDatetime\", g.bucket_start))) / 86400.0, 0.0)), 0) AS sat_days FROM day_node_grid g LEFT JOIN participating_node_chans pc ON pc.pubkey = g.pubkey LEFT JOIN \"Channels\" c ON c.\"ChanId\" = pc.chan_id AND (c.\"CreatedByNodeGuard\" = TRUE OR $liquidityScope = 0) GROUP BY g.bucket_start, g.pubkey), rolling AS (SELECT df.bucket_start, df.pubkey, df.node_name, SUM(df.fee_sats) OVER w AS r_fee, SUM(COALESCE(dsc.swap_cost_sats, 0)) OVER w AS r_swap, SUM(COALESCE(dl.sat_days, 0)) OVER w AS r_satdays, COUNT(*) OVER w AS r_count FROM day_fees df LEFT JOIN day_swap_costs dsc ON dsc.bucket_start = df.bucket_start AND dsc.pubkey = df.pubkey LEFT JOIN day_liquidity dl ON dl.bucket_start = df.bucket_start AND dl.pubkey = df.pubkey WINDOW w AS (PARTITION BY df.pubkey ORDER BY df.bucket_start ROWS BETWEEN (CASE '$aprWindow' WHEN 'day' THEN 23 WHEN 'week' THEN 167 ELSE 719 END) PRECEDING AND CURRENT ROW)) SELECT t AS \"time\", m AS metric, v AS value FROM (SELECT bucket_start AS t, node_name || ' APR' AS m, CASE WHEN r_satdays > 0 THEN (r_fee - r_swap)::numeric * 365.0 / r_satdays ELSE NULL END AS v FROM rolling UNION ALL SELECT bucket_start AS t, node_name || ' Liquidity (BTC)' AS m, CASE WHEN r_count > 0 THEN (r_satdays * 24.0 / r_count / 100000000.0)::numeric(20,8) ELSE NULL END AS v FROM rolling) sub ORDER BY t, m", "refId": "A" } ], - "title": "Local Managed Nodes", - "type": "stat" + "title": "APR & Qualifying Liquidity over time by managed node", + "type": "timeseries" } ], "preload": false, @@ -3769,6 +5226,98 @@ "regex": "", "regexApplyTo": "value", "type": "query" + }, + { + "current": { + "selected": true, + "text": "Yes", + "value": "1" + }, + "description": "When set to Yes, APR panels subtract completed swap-out costs (Service + Lightning + OnChain fees) from the routing-revenue numerator. When No, APR is gross routing yield only.", + "hide": 0, + "includeAll": false, + "label": "Apply Swap Costs", + "multi": false, + "name": "applySwapCosts", + "options": [ + { + "selected": true, + "text": "Yes", + "value": "1" + }, + { + "selected": false, + "text": "No", + "value": "0" + } + ], + "query": "Yes : 1, No : 0", + "queryValue": "", + "skipUrlSync": false, + "type": "custom" + }, + { + "current": { + "selected": true, + "text": "NodeGuard only", + "value": "1" + }, + "description": "Liquidity denominator scope. NodeGuard only: only channels opened by NodeGuard (Channel.CreatedByNodeGuard = TRUE) contribute to the qualifying-liquidity sum. All channels: every channel that participated in routing in the window contributes liquidity, regardless of who opened it.", + "hide": 0, + "includeAll": false, + "label": "Liquidity Scope", + "multi": false, + "name": "liquidityScope", + "options": [ + { + "selected": true, + "text": "NodeGuard only", + "value": "1" + }, + { + "selected": false, + "text": "All channels", + "value": "0" + } + ], + "query": "NodeGuard only : 1, All channels : 0", + "queryValue": "", + "skipUrlSync": false, + "type": "custom" + }, + { + "current": { + "selected": true, + "text": "Day", + "value": "day" + }, + "description": "Bucket length used by the APR Trends panels. Each non-overlapping bucket spans this length and APR is annualized to 365 days. Calendar-aligned (day = UTC day, week = ISO week, month = calendar month).", + "hide": 0, + "includeAll": false, + "label": "APR Window", + "multi": false, + "name": "aprWindow", + "options": [ + { + "selected": true, + "text": "Day", + "value": "day" + }, + { + "selected": false, + "text": "Week", + "value": "week" + }, + { + "selected": false, + "text": "Month", + "value": "month" + } + ], + "query": "Day : day, Week : week, Month : month", + "queryValue": "", + "skipUrlSync": false, + "type": "custom" } ] }, @@ -3782,4 +5331,4 @@ "uid": "ad4n8c6", "version": 1, "weekStart": "" -} \ No newline at end of file +} From 487d9d9cc812a198f444ab72ddc5331b54b0b431 Mon Sep 17 00:00:00 2001 From: Marcos Date: Tue, 5 May 2026 13:27:31 +0200 Subject: [PATCH 5/6] fix: remove local managed nodes --- .../provisioning/dashboards/routing.json | 69 ------------------- 1 file changed, 69 deletions(-) diff --git a/docker/grafana/provisioning/dashboards/routing.json b/docker/grafana/provisioning/dashboards/routing.json index 1461baf6..72b95f0e 100644 --- a/docker/grafana/provisioning/dashboards/routing.json +++ b/docker/grafana/provisioning/dashboards/routing.json @@ -3591,75 +3591,6 @@ "title": "Failure string ", "type": "piechart" }, - { - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "$datasource" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "thresholds" - }, - "decimals": 0, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "red", - "value": 0 - }, - { - "color": "green", - "value": 1 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 69 - }, - "id": 30, - "options": { - "colorMode": "value", - "graphMode": "none", - "justifyMode": "auto", - "orientation": "auto", - "percentChangeColorMode": "standard", - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": true - }, - "showPercentChange": false, - "textMode": "auto", - "wideLayout": true - }, - "pluginVersion": "12.4.2", - "targets": [ - { - "datasource": { - "type": "grafana-postgresql-datasource", - "uid": "$datasource" - }, - "editorMode": "code", - "format": "table", - "rawQuery": true, - "rawSql": "SELECT COUNT(*) AS value FROM \"Nodes\" WHERE \"ChannelAdminMacaroon\" IS NOT NULL", - "refId": "A" - } - ], - "title": "Local Managed Nodes", - "type": "stat" - }, { "datasource": { "type": "grafana-postgresql-datasource", From d19c89089d108bf10cc32e5a3f57f67843bed96a Mon Sep 17 00:00:00 2001 From: Marcos Date: Tue, 5 May 2026 13:37:18 +0200 Subject: [PATCH 6/6] fix: remove swap costs from graphs, fix infinite APR bug and order by APR --- .../provisioning/dashboards/routing.json | 26 +++++++------------ 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/docker/grafana/provisioning/dashboards/routing.json b/docker/grafana/provisioning/dashboards/routing.json index 72b95f0e..b3965d5c 100644 --- a/docker/grafana/provisioning/dashboards/routing.json +++ b/docker/grafana/provisioning/dashboards/routing.json @@ -3887,7 +3887,7 @@ "type": "grafana-postgresql-datasource", "uid": "$datasource" }, - "description": "Annualized yield on liquidity from NodeGuard-opened channels, net of swap costs. Numerator is fee_sats minus the pair's pro-rata share of completed swap-out costs on the same managed node (allocated by fee share). Denominator is each qualifying channel's SatsAmount prorated by the fraction of the window it was open: opened at Channel.CreationDatetime; close time is Channel.ClosedAt when Status = Closed. Channels not opened by NodeGuard contribute revenue but never liquidity. APR can be negative when swap costs exceed routing revenue. APR is null when the pair has no qualifying liquidity.", + "description": "Annualized yield by directional channel pair (incoming -> outgoing) on NodeGuard-opened liquidity. Numerator is settled forwarding fees for the pair; swap costs are NOT subtracted at this granularity because swaps rebalance node-level liquidity, not specific channel pairs (per-pair allocation would be an accounting convention, not a causal attribution). Denominator is each qualifying channel's SatsAmount prorated by the fraction of the window it was open (CreationDatetime to ClosedAt, or UpdateDatetime fallback when Status = Closed). Channels not opened by NodeGuard contribute revenue but never liquidity. APR is null when the pair has no qualifying liquidity. For APR net of swap costs, see the 'NodeGuard-Liquidity APR by Node' or 'Combined NodeGuard-Liquidity APR' panels.", "fieldConfig": { "defaults": { "color": { @@ -4005,8 +4005,6 @@ "fields": [ "HTLCs settled", "Fee (BTC)", - "Swap Cost (BTC)", - "Net Revenue (BTC)", "Qualifying Liquidity (BTC)" ] } @@ -4021,7 +4019,7 @@ "editorMode": "code", "format": "table", "rawQuery": true, - "rawSql": "WITH bounds AS (SELECT $__timeFrom()::timestamptz AS window_start, $__timeTo()::timestamptz AS window_end, GREATEST(EXTRACT(EPOCH FROM ($__timeTo()::timestamptz - $__timeFrom()::timestamptz)) / 86400.0, 0.000001) AS window_days), pair_fees AS (SELECT \"ManagedNodePubKey\", MAX(\"ManagedNodeName\") AS managed_node_name, \"IncomingChannelId\", \"OutgoingChannelId\", COALESCE(MAX(\"IncomingPeerAlias\"), '?') || ' -> ' || COALESCE(MAX(\"OutgoingPeerAlias\"), '?') AS pair_label, COUNT(*) AS settled_count, ROUND(SUM(\"FeeMsat\") / 1000.0)::bigint AS fee_sats FROM \"ForwardingHtlcEvents\" WHERE $__timeFilter(\"EventTimestamp\") AND \"Outcome\" = 1 AND \"ManagedNodePubKey\" IN ($node) AND \"FeeMsat\" IS NOT NULL GROUP BY \"ManagedNodePubKey\", \"IncomingChannelId\", \"OutgoingChannelId\"), channel_status AS (SELECT DISTINCT ON (\"ChanId\") \"ChanId\", CASE \"Status\" WHEN 1 THEN 'Open' WHEN 2 THEN 'Closed' ELSE 'Unknown' END AS status FROM \"Channels\" ORDER BY \"ChanId\", \"CreationDatetime\" DESC), qualifying_channels AS (SELECT c.\"ChanId\", c.\"SatsAmount\", GREATEST(c.\"CreationDatetime\", b.window_start) AS open_start, LEAST(CASE WHEN c.\"Status\" = 2 THEN COALESCE((to_jsonb(c) ->> 'ClosedAt')::timestamptz, c.\"UpdateDatetime\") ELSE b.window_end END, b.window_end) AS open_end, b.window_days FROM \"Channels\" c CROSS JOIN bounds b WHERE (c.\"CreatedByNodeGuard\" = TRUE OR $liquidityScope = 0)), prorated AS (SELECT \"ChanId\", \"SatsAmount\" * GREATEST(EXTRACT(EPOCH FROM (open_end - open_start)) / 86400.0, 0.0) / window_days AS prorated_sats FROM qualifying_channels), swap_costs AS (SELECT n.\"PubKey\" AS managed_node_pubkey, $applySwapCosts * SUM(COALESCE(s.\"ServiceFeeSats\", 0) + COALESCE(s.\"LightningFeeSats\", 0) + COALESCE(s.\"OnChainFeeSats\", 0)) AS swap_cost_sats FROM \"SwapOuts\" s JOIN \"Nodes\" n ON n.\"Id\" = s.\"NodeId\" CROSS JOIN bounds b WHERE s.\"Status\" = 1 AND s.\"CreationDatetime\" >= b.window_start AND s.\"CreationDatetime\" < b.window_end AND n.\"PubKey\" IN ($node) GROUP BY n.\"PubKey\"), node_fees AS (SELECT \"ManagedNodePubKey\", SUM(fee_sats) AS node_total_fee_sats FROM pair_fees GROUP BY \"ManagedNodePubKey\"), pair_with_liquidity AS (SELECT pf.\"ManagedNodePubKey\", pf.managed_node_name, pf.pair_label, pf.\"IncomingChannelId\", pf.\"OutgoingChannelId\", pf.settled_count, pf.fee_sats, CASE WHEN nf.node_total_fee_sats > 0 THEN ROUND(COALESCE(sc.swap_cost_sats, 0)::numeric * pf.fee_sats / nf.node_total_fee_sats)::bigint ELSE 0 END AS allocated_swap_cost_sats, COALESCE(p_in.prorated_sats, 0) + COALESCE(p_out.prorated_sats, 0) - CASE WHEN pf.\"IncomingChannelId\" = pf.\"OutgoingChannelId\" THEN COALESCE(p_in.prorated_sats, 0) ELSE 0 END AS qualifying_liquidity_sats, COALESCE(cs_in.status, '?') AS incoming_status, COALESCE(cs_out.status, '?') AS outgoing_status FROM pair_fees pf LEFT JOIN prorated p_in ON p_in.\"ChanId\" = pf.\"IncomingChannelId\" LEFT JOIN prorated p_out ON p_out.\"ChanId\" = pf.\"OutgoingChannelId\" LEFT JOIN swap_costs sc ON sc.managed_node_pubkey = pf.\"ManagedNodePubKey\" LEFT JOIN node_fees nf ON nf.\"ManagedNodePubKey\" = pf.\"ManagedNodePubKey\" LEFT JOIN channel_status cs_in ON cs_in.\"ChanId\" = pf.\"IncomingChannelId\" LEFT JOIN channel_status cs_out ON cs_out.\"ChanId\" = pf.\"OutgoingChannelId\") SELECT managed_node_name AS \"Node\", pair_label AS \"Channel Pair\", incoming_status AS \"Incoming Status\", outgoing_status AS \"Outgoing Status\", settled_count AS \"HTLCs settled\", (fee_sats / 100000000.0)::numeric(20,8) AS \"Fee (BTC)\", (allocated_swap_cost_sats / 100000000.0)::numeric(20,8) AS \"Swap Cost (BTC)\", ((fee_sats - allocated_swap_cost_sats) / 100000000.0)::numeric(20,8) AS \"Net Revenue (BTC)\", (qualifying_liquidity_sats / 100000000.0)::numeric(20,8) AS \"Qualifying Liquidity (BTC)\", CASE WHEN qualifying_liquidity_sats > 0 THEN ((fee_sats - allocated_swap_cost_sats)::numeric / qualifying_liquidity_sats) * (365.0 / (SELECT window_days FROM bounds))::numeric ELSE NULL END AS \"APR\" FROM pair_with_liquidity ORDER BY fee_sats DESC LIMIT 50", + "rawSql": "WITH bounds AS (SELECT $__timeFrom()::timestamptz AS window_start, $__timeTo()::timestamptz AS window_end, GREATEST(EXTRACT(EPOCH FROM ($__timeTo()::timestamptz - $__timeFrom()::timestamptz)) / 86400.0, 0.000001) AS window_days), pair_fees AS (SELECT \"ManagedNodePubKey\", MAX(\"ManagedNodeName\") AS managed_node_name, \"IncomingChannelId\", \"OutgoingChannelId\", COALESCE(MAX(\"IncomingPeerAlias\"), '?') || ' -> ' || COALESCE(MAX(\"OutgoingPeerAlias\"), '?') AS pair_label, COUNT(*) AS settled_count, ROUND(SUM(\"FeeMsat\") / 1000.0)::bigint AS fee_sats FROM \"ForwardingHtlcEvents\" WHERE $__timeFilter(\"EventTimestamp\") AND \"Outcome\" = 1 AND \"ManagedNodePubKey\" IN ($node) AND \"FeeMsat\" IS NOT NULL GROUP BY \"ManagedNodePubKey\", \"IncomingChannelId\", \"OutgoingChannelId\"), channel_status AS (SELECT DISTINCT ON (\"ChanId\") \"ChanId\", CASE \"Status\" WHEN 1 THEN 'Open' WHEN 2 THEN 'Closed' ELSE 'Unknown' END AS status FROM \"Channels\" ORDER BY \"ChanId\", \"CreationDatetime\" DESC), qualifying_channels AS (SELECT c.\"ChanId\", c.\"SatsAmount\", GREATEST(c.\"CreationDatetime\", b.window_start) AS open_start, LEAST(CASE WHEN c.\"Status\" = 2 THEN COALESCE((to_jsonb(c) ->> 'ClosedAt')::timestamptz, c.\"UpdateDatetime\") ELSE b.window_end END, b.window_end) AS open_end, b.window_days FROM \"Channels\" c CROSS JOIN bounds b WHERE (c.\"CreatedByNodeGuard\" = TRUE OR $liquidityScope = 0)), prorated AS (SELECT \"ChanId\", \"SatsAmount\" * GREATEST(EXTRACT(EPOCH FROM (open_end - open_start)) / 86400.0, 0.0) / window_days AS prorated_sats FROM qualifying_channels), pair_with_liquidity AS (SELECT pf.\"ManagedNodePubKey\", pf.managed_node_name, pf.pair_label, pf.\"IncomingChannelId\", pf.\"OutgoingChannelId\", pf.settled_count, pf.fee_sats, COALESCE(p_in.prorated_sats, 0) + COALESCE(p_out.prorated_sats, 0) - CASE WHEN pf.\"IncomingChannelId\" = pf.\"OutgoingChannelId\" THEN COALESCE(p_in.prorated_sats, 0) ELSE 0 END AS qualifying_liquidity_sats, COALESCE(cs_in.status, '?') AS incoming_status, COALESCE(cs_out.status, '?') AS outgoing_status FROM pair_fees pf LEFT JOIN prorated p_in ON p_in.\"ChanId\" = pf.\"IncomingChannelId\" LEFT JOIN prorated p_out ON p_out.\"ChanId\" = pf.\"OutgoingChannelId\" LEFT JOIN channel_status cs_in ON cs_in.\"ChanId\" = pf.\"IncomingChannelId\" LEFT JOIN channel_status cs_out ON cs_out.\"ChanId\" = pf.\"OutgoingChannelId\") SELECT managed_node_name AS \"Node\", pair_label AS \"Channel Pair\", incoming_status AS \"Incoming Status\", outgoing_status AS \"Outgoing Status\", settled_count AS \"HTLCs settled\", (fee_sats / 100000000.0)::numeric(20,8) AS \"Fee (BTC)\", (qualifying_liquidity_sats / 100000000.0)::numeric(20,8) AS \"Qualifying Liquidity (BTC)\", CASE WHEN qualifying_liquidity_sats >= 1 THEN (fee_sats::numeric / qualifying_liquidity_sats) * (365.0 / (SELECT window_days FROM bounds))::numeric ELSE NULL END AS \"APR\" FROM pair_with_liquidity ORDER BY \"APR\" DESC NULLS LAST LIMIT 50", "refId": "A" } ], @@ -4033,7 +4031,7 @@ "type": "grafana-postgresql-datasource", "uid": "$datasource" }, - "description": "Annualized yield rolled up to the directional peer pair (incoming alias → outgoing alias), net of swap costs. Numerator is fee_sats minus the pair's pro-rata share of the managed node's completed swap-out costs (allocated by fee share). Denominator is the union of NodeGuard-opened channels that routed inside the pair during the window, each prorated by its open fraction and counted once even if a single channel appears on both sides. Close time uses Channel.ClosedAt when Status = Closed. APR can be negative; null when the pair has no qualifying liquidity.", + "description": "Annualized yield by directional peer pair (incoming alias -> outgoing alias) on NodeGuard-opened liquidity. Numerator is settled forwarding fees; swap costs are NOT subtracted at peer-pair granularity because swaps rebalance node-level liquidity, not peer-specific flow. Denominator is the union of NodeGuard-opened channels that routed inside the pair during the window, each prorated by its open fraction and counted once even if a single channel appears on both sides. For APR net of swap costs, see the node-level panels.", "fieldConfig": { "defaults": { "color": { @@ -4151,8 +4149,6 @@ "fields": [ "HTLCs settled", "Fee (BTC)", - "Swap Cost (BTC)", - "Net Revenue (BTC)", "Qualifying Liquidity (BTC)" ] } @@ -4167,7 +4163,7 @@ "editorMode": "code", "format": "table", "rawQuery": true, - "rawSql": "WITH bounds AS (SELECT $__timeFrom()::timestamptz AS window_start, $__timeTo()::timestamptz AS window_end, GREATEST(EXTRACT(EPOCH FROM ($__timeTo()::timestamptz - $__timeFrom()::timestamptz)) / 86400.0, 0.000001) AS window_days), peer_pair_events AS (SELECT \"ManagedNodePubKey\", MAX(\"ManagedNodeName\") AS managed_node_name, \"IncomingPeerAlias\" AS incoming_peer, \"OutgoingPeerAlias\" AS outgoing_peer, COUNT(*) AS settled_count, ROUND(SUM(\"FeeMsat\") / 1000.0)::bigint AS fee_sats FROM \"ForwardingHtlcEvents\" WHERE $__timeFilter(\"EventTimestamp\") AND \"Outcome\" = 1 AND \"ManagedNodePubKey\" IN ($node) AND \"FeeMsat\" IS NOT NULL GROUP BY \"ManagedNodePubKey\", \"IncomingPeerAlias\", \"OutgoingPeerAlias\"), peer_pair_channels AS (SELECT DISTINCT f.\"ManagedNodePubKey\", f.\"IncomingPeerAlias\" AS incoming_peer, f.\"OutgoingPeerAlias\" AS outgoing_peer, u.chan_id FROM \"ForwardingHtlcEvents\" f CROSS JOIN LATERAL (VALUES (f.\"IncomingChannelId\"), (f.\"OutgoingChannelId\")) AS u(chan_id) WHERE $__timeFilter(f.\"EventTimestamp\") AND f.\"Outcome\" = 1 AND f.\"ManagedNodePubKey\" IN ($node) AND f.\"FeeMsat\" IS NOT NULL), qualifying_channels AS (SELECT c.\"ChanId\", c.\"SatsAmount\", GREATEST(c.\"CreationDatetime\", b.window_start) AS open_start, LEAST(CASE WHEN c.\"Status\" = 2 THEN COALESCE((to_jsonb(c) ->> 'ClosedAt')::timestamptz, c.\"UpdateDatetime\") ELSE b.window_end END, b.window_end) AS open_end, b.window_days FROM \"Channels\" c CROSS JOIN bounds b WHERE (c.\"CreatedByNodeGuard\" = TRUE OR $liquidityScope = 0)), prorated AS (SELECT \"ChanId\", \"SatsAmount\" * GREATEST(EXTRACT(EPOCH FROM (open_end - open_start)) / 86400.0, 0.0) / window_days AS prorated_sats FROM qualifying_channels), peer_pair_liquidity AS (SELECT ppc.\"ManagedNodePubKey\", ppc.incoming_peer, ppc.outgoing_peer, SUM(p.prorated_sats) AS qualifying_liquidity_sats FROM peer_pair_channels ppc JOIN prorated p ON p.\"ChanId\" = ppc.chan_id GROUP BY ppc.\"ManagedNodePubKey\", ppc.incoming_peer, ppc.outgoing_peer), swap_costs AS (SELECT n.\"PubKey\" AS managed_node_pubkey, $applySwapCosts * SUM(COALESCE(s.\"ServiceFeeSats\", 0) + COALESCE(s.\"LightningFeeSats\", 0) + COALESCE(s.\"OnChainFeeSats\", 0)) AS swap_cost_sats FROM \"SwapOuts\" s JOIN \"Nodes\" n ON n.\"Id\" = s.\"NodeId\" CROSS JOIN bounds b WHERE s.\"Status\" = 1 AND s.\"CreationDatetime\" >= b.window_start AND s.\"CreationDatetime\" < b.window_end AND n.\"PubKey\" IN ($node) GROUP BY n.\"PubKey\"), node_fees AS (SELECT \"ManagedNodePubKey\", SUM(fee_sats) AS node_total_fee_sats FROM peer_pair_events GROUP BY \"ManagedNodePubKey\"), peer_pair_with_alloc AS (SELECT ppe.\"ManagedNodePubKey\", ppe.managed_node_name, ppe.incoming_peer, ppe.outgoing_peer, ppe.settled_count, ppe.fee_sats, CASE WHEN nf.node_total_fee_sats > 0 THEN ROUND(COALESCE(sc.swap_cost_sats, 0)::numeric * ppe.fee_sats / nf.node_total_fee_sats)::bigint ELSE 0 END AS allocated_swap_cost_sats, COALESCE(ppl.qualifying_liquidity_sats, 0) AS qualifying_liquidity_sats FROM peer_pair_events ppe LEFT JOIN peer_pair_liquidity ppl ON ppl.\"ManagedNodePubKey\" = ppe.\"ManagedNodePubKey\" AND ppl.incoming_peer IS NOT DISTINCT FROM ppe.incoming_peer AND ppl.outgoing_peer IS NOT DISTINCT FROM ppe.outgoing_peer LEFT JOIN swap_costs sc ON sc.managed_node_pubkey = ppe.\"ManagedNodePubKey\" LEFT JOIN node_fees nf ON nf.\"ManagedNodePubKey\" = ppe.\"ManagedNodePubKey\") SELECT managed_node_name AS \"Node\", (COALESCE(incoming_peer, '?') || ' -> ' || COALESCE(outgoing_peer, '?')) AS \"Peer Pair\", settled_count AS \"HTLCs settled\", (fee_sats / 100000000.0)::numeric(20,8) AS \"Fee (BTC)\", (allocated_swap_cost_sats / 100000000.0)::numeric(20,8) AS \"Swap Cost (BTC)\", ((fee_sats - allocated_swap_cost_sats) / 100000000.0)::numeric(20,8) AS \"Net Revenue (BTC)\", (qualifying_liquidity_sats / 100000000.0)::numeric(20,8) AS \"Qualifying Liquidity (BTC)\", CASE WHEN qualifying_liquidity_sats > 0 THEN ((fee_sats - allocated_swap_cost_sats)::numeric / qualifying_liquidity_sats) * (365.0 / (SELECT window_days FROM bounds))::numeric ELSE NULL END AS \"APR\" FROM peer_pair_with_alloc ORDER BY fee_sats DESC LIMIT 50", + "rawSql": "WITH bounds AS (SELECT $__timeFrom()::timestamptz AS window_start, $__timeTo()::timestamptz AS window_end, GREATEST(EXTRACT(EPOCH FROM ($__timeTo()::timestamptz - $__timeFrom()::timestamptz)) / 86400.0, 0.000001) AS window_days), peer_pair_events AS (SELECT \"ManagedNodePubKey\", MAX(\"ManagedNodeName\") AS managed_node_name, \"IncomingPeerAlias\" AS incoming_peer, \"OutgoingPeerAlias\" AS outgoing_peer, COUNT(*) AS settled_count, ROUND(SUM(\"FeeMsat\") / 1000.0)::bigint AS fee_sats FROM \"ForwardingHtlcEvents\" WHERE $__timeFilter(\"EventTimestamp\") AND \"Outcome\" = 1 AND \"ManagedNodePubKey\" IN ($node) AND \"FeeMsat\" IS NOT NULL GROUP BY \"ManagedNodePubKey\", \"IncomingPeerAlias\", \"OutgoingPeerAlias\"), peer_pair_channels AS (SELECT DISTINCT f.\"ManagedNodePubKey\", f.\"IncomingPeerAlias\" AS incoming_peer, f.\"OutgoingPeerAlias\" AS outgoing_peer, u.chan_id FROM \"ForwardingHtlcEvents\" f CROSS JOIN LATERAL (VALUES (f.\"IncomingChannelId\"), (f.\"OutgoingChannelId\")) AS u(chan_id) WHERE $__timeFilter(f.\"EventTimestamp\") AND f.\"Outcome\" = 1 AND f.\"ManagedNodePubKey\" IN ($node) AND f.\"FeeMsat\" IS NOT NULL), qualifying_channels AS (SELECT c.\"ChanId\", c.\"SatsAmount\", GREATEST(c.\"CreationDatetime\", b.window_start) AS open_start, LEAST(CASE WHEN c.\"Status\" = 2 THEN COALESCE((to_jsonb(c) ->> 'ClosedAt')::timestamptz, c.\"UpdateDatetime\") ELSE b.window_end END, b.window_end) AS open_end, b.window_days FROM \"Channels\" c CROSS JOIN bounds b WHERE (c.\"CreatedByNodeGuard\" = TRUE OR $liquidityScope = 0)), prorated AS (SELECT \"ChanId\", \"SatsAmount\" * GREATEST(EXTRACT(EPOCH FROM (open_end - open_start)) / 86400.0, 0.0) / window_days AS prorated_sats FROM qualifying_channels), peer_pair_liquidity AS (SELECT ppc.\"ManagedNodePubKey\", ppc.incoming_peer, ppc.outgoing_peer, SUM(p.prorated_sats) AS qualifying_liquidity_sats FROM peer_pair_channels ppc JOIN prorated p ON p.\"ChanId\" = ppc.chan_id GROUP BY ppc.\"ManagedNodePubKey\", ppc.incoming_peer, ppc.outgoing_peer), peer_pair_with_alloc AS (SELECT ppe.\"ManagedNodePubKey\", ppe.managed_node_name, ppe.incoming_peer, ppe.outgoing_peer, ppe.settled_count, ppe.fee_sats, COALESCE(ppl.qualifying_liquidity_sats, 0) AS qualifying_liquidity_sats FROM peer_pair_events ppe LEFT JOIN peer_pair_liquidity ppl ON ppl.\"ManagedNodePubKey\" = ppe.\"ManagedNodePubKey\" AND ppl.incoming_peer IS NOT DISTINCT FROM ppe.incoming_peer AND ppl.outgoing_peer IS NOT DISTINCT FROM ppe.outgoing_peer) SELECT managed_node_name AS \"Node\", (COALESCE(incoming_peer, '?') || ' -> ' || COALESCE(outgoing_peer, '?')) AS \"Peer Pair\", settled_count AS \"HTLCs settled\", (fee_sats / 100000000.0)::numeric(20,8) AS \"Fee (BTC)\", (qualifying_liquidity_sats / 100000000.0)::numeric(20,8) AS \"Qualifying Liquidity (BTC)\", CASE WHEN qualifying_liquidity_sats >= 1 THEN (fee_sats::numeric / qualifying_liquidity_sats) * (365.0 / (SELECT window_days FROM bounds))::numeric ELSE NULL END AS \"APR\" FROM peer_pair_with_alloc ORDER BY \"APR\" DESC NULLS LAST LIMIT 50", "refId": "A" } ], @@ -4551,7 +4547,7 @@ "type": "grafana-postgresql-datasource", "uid": "$datasource" }, - "description": "Annualized yield attributed to each individual NodeGuard channel. Each settled forward credits BOTH its incoming and outgoing channel with the full fee (full-attribution, same convention as the existing 'Routed fee distribution From/To' panels). Denominator is this channel's prorated SatsAmount alone. Allocated swap cost is the channel's pro-rata share of its managed node's total swap cost (by fee share within the node, double-credit basis). APR can be negative; null when the channel has no qualifying liquidity (i.e. not opened by NodeGuard).", + "description": "Annualized yield attributed to each individual NodeGuard channel. Each settled forward credits BOTH its incoming and outgoing channel with the full fee (full-attribution, same convention as 'Routed fee distribution From/To'). Numerator is settled forwarding fees; swap costs are NOT subtracted at per-channel granularity (swaps rebalance the whole node, not a single channel). Denominator is this channel's prorated SatsAmount alone. APR is null when the channel has no qualifying liquidity (i.e. not opened by NodeGuard). For APR net of swap costs, see the node-level panels.", "fieldConfig": { "defaults": { "color": { @@ -4669,8 +4665,6 @@ "fields": [ "HTLC Settled", "Fee (BTC)", - "Swap Cost (BTC)", - "Net Revenue (BTC)", "Qualifying Liquidity (BTC)" ] } @@ -4685,7 +4679,7 @@ "editorMode": "code", "format": "table", "rawQuery": true, - "rawSql": "WITH bounds AS (SELECT $__timeFrom()::timestamptz AS window_start, $__timeTo()::timestamptz AS window_end, GREATEST(EXTRACT(EPOCH FROM ($__timeTo()::timestamptz - $__timeFrom()::timestamptz)) / 86400.0, 0.000001) AS window_days), channel_events AS (SELECT f.\"ManagedNodePubKey\", MAX(f.\"ManagedNodeName\") AS managed_node_name, u.chan_id, MAX(u.peer_alias) AS peer_alias, COUNT(*) AS settled_count, ROUND(SUM(f.\"FeeMsat\") / 1000.0)::bigint AS fee_sats FROM \"ForwardingHtlcEvents\" f CROSS JOIN LATERAL (VALUES (f.\"IncomingChannelId\", f.\"IncomingPeerAlias\"), (f.\"OutgoingChannelId\", f.\"OutgoingPeerAlias\")) AS u(chan_id, peer_alias) WHERE $__timeFilter(f.\"EventTimestamp\") AND f.\"Outcome\" = 1 AND f.\"ManagedNodePubKey\" IN ($node) AND f.\"FeeMsat\" IS NOT NULL GROUP BY f.\"ManagedNodePubKey\", u.chan_id), channel_status AS (SELECT DISTINCT ON (\"ChanId\") \"ChanId\", CASE \"Status\" WHEN 1 THEN 'Open' WHEN 2 THEN 'Closed' ELSE 'Unknown' END AS status FROM \"Channels\" ORDER BY \"ChanId\", \"CreationDatetime\" DESC), qualifying_channels AS (SELECT c.\"ChanId\", c.\"SatsAmount\", GREATEST(c.\"CreationDatetime\", b.window_start) AS open_start, LEAST(CASE WHEN c.\"Status\" = 2 THEN COALESCE((to_jsonb(c) ->> 'ClosedAt')::timestamptz, c.\"UpdateDatetime\") ELSE b.window_end END, b.window_end) AS open_end, b.window_days FROM \"Channels\" c CROSS JOIN bounds b WHERE (c.\"CreatedByNodeGuard\" = TRUE OR $liquidityScope = 0)), prorated AS (SELECT \"ChanId\", \"SatsAmount\" * GREATEST(EXTRACT(EPOCH FROM (open_end - open_start)) / 86400.0, 0.0) / window_days AS prorated_sats FROM qualifying_channels), all_channel_sizes AS (SELECT \"ChanId\", MAX(\"SatsAmount\") AS sats_amount FROM \"Channels\" GROUP BY \"ChanId\"), swap_costs AS (SELECT n.\"PubKey\" AS managed_node_pubkey, $applySwapCosts * SUM(COALESCE(s.\"ServiceFeeSats\", 0) + COALESCE(s.\"LightningFeeSats\", 0) + COALESCE(s.\"OnChainFeeSats\", 0)) AS swap_cost_sats FROM \"SwapOuts\" s JOIN \"Nodes\" n ON n.\"Id\" = s.\"NodeId\" CROSS JOIN bounds b WHERE s.\"Status\" = 1 AND s.\"CreationDatetime\" >= b.window_start AND s.\"CreationDatetime\" < b.window_end AND n.\"PubKey\" IN ($node) GROUP BY n.\"PubKey\"), node_fees AS (SELECT \"ManagedNodePubKey\", SUM(fee_sats) AS node_total_fee_sats FROM channel_events GROUP BY \"ManagedNodePubKey\"), channels_with_meta AS (SELECT ce.\"ManagedNodePubKey\", ce.managed_node_name, ce.chan_id, ce.peer_alias, ce.settled_count, ce.fee_sats, COALESCE(acs.sats_amount, 0) AS channel_size_sats, COALESCE(p.prorated_sats, 0) AS qualifying_liquidity_sats, CASE WHEN nf.node_total_fee_sats > 0 THEN ROUND(COALESCE(sc.swap_cost_sats, 0)::numeric * ce.fee_sats / nf.node_total_fee_sats)::bigint ELSE 0 END AS allocated_swap_cost_sats, COALESCE(cs.status, '?') AS status FROM channel_events ce LEFT JOIN prorated p ON p.\"ChanId\" = ce.chan_id LEFT JOIN all_channel_sizes acs ON acs.\"ChanId\" = ce.chan_id LEFT JOIN swap_costs sc ON sc.managed_node_pubkey = ce.\"ManagedNodePubKey\" LEFT JOIN node_fees nf ON nf.\"ManagedNodePubKey\" = ce.\"ManagedNodePubKey\" LEFT JOIN channel_status cs ON cs.\"ChanId\" = ce.chan_id) SELECT managed_node_name AS \"Node\", COALESCE(peer_alias, '?') AS \"Channel (peer)\", chan_id::text AS \"Chan ID\", status AS \"Status\", (channel_size_sats / 100000000.0)::numeric(20,8) AS \"Channel Size (BTC)\", settled_count AS \"HTLC Settled\", (fee_sats / 100000000.0)::numeric(20,8) AS \"Fee (BTC)\", (allocated_swap_cost_sats / 100000000.0)::numeric(20,8) AS \"Swap Cost (BTC)\", ((fee_sats - allocated_swap_cost_sats) / 100000000.0)::numeric(20,8) AS \"Net Revenue (BTC)\", (qualifying_liquidity_sats / 100000000.0)::numeric(20,8) AS \"Qualifying Liquidity (BTC)\", CASE WHEN qualifying_liquidity_sats > 0 THEN ((fee_sats - allocated_swap_cost_sats)::numeric / qualifying_liquidity_sats) * (365.0 / (SELECT window_days FROM bounds))::numeric ELSE NULL END AS \"APR\" FROM channels_with_meta ORDER BY fee_sats DESC LIMIT 50", + "rawSql": "WITH bounds AS (SELECT $__timeFrom()::timestamptz AS window_start, $__timeTo()::timestamptz AS window_end, GREATEST(EXTRACT(EPOCH FROM ($__timeTo()::timestamptz - $__timeFrom()::timestamptz)) / 86400.0, 0.000001) AS window_days), channel_events AS (SELECT f.\"ManagedNodePubKey\", MAX(f.\"ManagedNodeName\") AS managed_node_name, u.chan_id, MAX(u.peer_alias) AS peer_alias, COUNT(*) AS settled_count, ROUND(SUM(f.\"FeeMsat\") / 1000.0)::bigint AS fee_sats FROM \"ForwardingHtlcEvents\" f CROSS JOIN LATERAL (VALUES (f.\"IncomingChannelId\", f.\"IncomingPeerAlias\"), (f.\"OutgoingChannelId\", f.\"OutgoingPeerAlias\")) AS u(chan_id, peer_alias) WHERE $__timeFilter(f.\"EventTimestamp\") AND f.\"Outcome\" = 1 AND f.\"ManagedNodePubKey\" IN ($node) AND f.\"FeeMsat\" IS NOT NULL GROUP BY f.\"ManagedNodePubKey\", u.chan_id), channel_status AS (SELECT DISTINCT ON (\"ChanId\") \"ChanId\", CASE \"Status\" WHEN 1 THEN 'Open' WHEN 2 THEN 'Closed' ELSE 'Unknown' END AS status FROM \"Channels\" ORDER BY \"ChanId\", \"CreationDatetime\" DESC), qualifying_channels AS (SELECT c.\"ChanId\", c.\"SatsAmount\", GREATEST(c.\"CreationDatetime\", b.window_start) AS open_start, LEAST(CASE WHEN c.\"Status\" = 2 THEN COALESCE((to_jsonb(c) ->> 'ClosedAt')::timestamptz, c.\"UpdateDatetime\") ELSE b.window_end END, b.window_end) AS open_end, b.window_days FROM \"Channels\" c CROSS JOIN bounds b WHERE (c.\"CreatedByNodeGuard\" = TRUE OR $liquidityScope = 0)), prorated AS (SELECT \"ChanId\", \"SatsAmount\" * GREATEST(EXTRACT(EPOCH FROM (open_end - open_start)) / 86400.0, 0.0) / window_days AS prorated_sats FROM qualifying_channels), all_channel_sizes AS (SELECT \"ChanId\", MAX(\"SatsAmount\") AS sats_amount FROM \"Channels\" GROUP BY \"ChanId\"), channels_with_meta AS (SELECT ce.\"ManagedNodePubKey\", ce.managed_node_name, ce.chan_id, ce.peer_alias, ce.settled_count, ce.fee_sats, COALESCE(acs.sats_amount, 0) AS channel_size_sats, COALESCE(p.prorated_sats, 0) AS qualifying_liquidity_sats, COALESCE(cs.status, '?') AS status FROM channel_events ce LEFT JOIN prorated p ON p.\"ChanId\" = ce.chan_id LEFT JOIN all_channel_sizes acs ON acs.\"ChanId\" = ce.chan_id LEFT JOIN channel_status cs ON cs.\"ChanId\" = ce.chan_id) SELECT managed_node_name AS \"Node\", COALESCE(peer_alias, '?') AS \"Channel (peer)\", chan_id::text AS \"Chan ID\", status AS \"Status\", (channel_size_sats / 100000000.0)::numeric(20,8) AS \"Channel Size (BTC)\", settled_count AS \"HTLC Settled\", (fee_sats / 100000000.0)::numeric(20,8) AS \"Fee (BTC)\", (qualifying_liquidity_sats / 100000000.0)::numeric(20,8) AS \"Qualifying Liquidity (BTC)\", CASE WHEN qualifying_liquidity_sats >= 1 THEN (fee_sats::numeric / qualifying_liquidity_sats) * (365.0 / (SELECT window_days FROM bounds))::numeric ELSE NULL END AS \"APR\" FROM channels_with_meta ORDER BY \"APR\" DESC NULLS LAST LIMIT 50", "refId": "A" } ], @@ -4697,7 +4691,7 @@ "type": "grafana-postgresql-datasource", "uid": "$datasource" }, - "description": "Annualized yield attributed to each individual peer (by alias). Each settled forward credits BOTH the incoming and outgoing peer with the full fee (full-attribution). Denominator is the union of NodeGuard-opened channels associated with the peer, each prorated by its open fraction in the window and counted once. Allocated swap cost is the peer's pro-rata share of its managed node's swap cost. APR can be negative; null when the peer has no qualifying NodeGuard liquidity.", + "description": "Annualized yield attributed to each individual peer (by alias). Each settled forward credits BOTH the incoming and outgoing peer with the full fee (full-attribution). Numerator is settled forwarding fees; swap costs are NOT subtracted at per-peer granularity. Denominator is the union of NodeGuard-opened channels associated with the peer, each prorated by its open fraction in the window and counted once. APR is null when the peer has no qualifying NodeGuard liquidity. For APR net of swap costs, see the node-level panels.", "fieldConfig": { "defaults": { "color": { @@ -4815,8 +4809,6 @@ "fields": [ "HTLC Settled", "Fee (BTC)", - "Swap Cost (BTC)", - "Net Revenue (BTC)", "Qualifying Liquidity (BTC)" ] } @@ -4831,7 +4823,7 @@ "editorMode": "code", "format": "table", "rawQuery": true, - "rawSql": "WITH bounds AS (SELECT $__timeFrom()::timestamptz AS window_start, $__timeTo()::timestamptz AS window_end, GREATEST(EXTRACT(EPOCH FROM ($__timeTo()::timestamptz - $__timeFrom()::timestamptz)) / 86400.0, 0.000001) AS window_days), peer_events AS (SELECT f.\"ManagedNodePubKey\", MAX(f.\"ManagedNodeName\") AS managed_node_name, u.peer_alias, COUNT(*) AS settled_count, ROUND(SUM(f.\"FeeMsat\") / 1000.0)::bigint AS fee_sats FROM \"ForwardingHtlcEvents\" f CROSS JOIN LATERAL (VALUES (f.\"IncomingPeerAlias\"), (f.\"OutgoingPeerAlias\")) AS u(peer_alias) WHERE $__timeFilter(f.\"EventTimestamp\") AND f.\"Outcome\" = 1 AND f.\"ManagedNodePubKey\" IN ($node) AND f.\"FeeMsat\" IS NOT NULL GROUP BY f.\"ManagedNodePubKey\", u.peer_alias), peer_chans AS (SELECT DISTINCT f.\"ManagedNodePubKey\", u.peer_alias, u.chan_id FROM \"ForwardingHtlcEvents\" f CROSS JOIN LATERAL (VALUES (f.\"IncomingPeerAlias\", f.\"IncomingChannelId\"), (f.\"OutgoingPeerAlias\", f.\"OutgoingChannelId\")) AS u(peer_alias, chan_id) WHERE $__timeFilter(f.\"EventTimestamp\") AND f.\"Outcome\" = 1 AND f.\"ManagedNodePubKey\" IN ($node) AND f.\"FeeMsat\" IS NOT NULL), qualifying_channels AS (SELECT c.\"ChanId\", c.\"SatsAmount\", GREATEST(c.\"CreationDatetime\", b.window_start) AS open_start, LEAST(CASE WHEN c.\"Status\" = 2 THEN COALESCE((to_jsonb(c) ->> 'ClosedAt')::timestamptz, c.\"UpdateDatetime\") ELSE b.window_end END, b.window_end) AS open_end, b.window_days FROM \"Channels\" c CROSS JOIN bounds b WHERE (c.\"CreatedByNodeGuard\" = TRUE OR $liquidityScope = 0)), prorated AS (SELECT \"ChanId\", \"SatsAmount\" * GREATEST(EXTRACT(EPOCH FROM (open_end - open_start)) / 86400.0, 0.0) / window_days AS prorated_sats FROM qualifying_channels), peer_liquidity AS (SELECT pc.\"ManagedNodePubKey\", pc.peer_alias, SUM(p.prorated_sats) AS qualifying_liquidity_sats FROM peer_chans pc JOIN prorated p ON p.\"ChanId\" = pc.chan_id GROUP BY pc.\"ManagedNodePubKey\", pc.peer_alias), swap_costs AS (SELECT n.\"PubKey\" AS managed_node_pubkey, $applySwapCosts * SUM(COALESCE(s.\"ServiceFeeSats\", 0) + COALESCE(s.\"LightningFeeSats\", 0) + COALESCE(s.\"OnChainFeeSats\", 0)) AS swap_cost_sats FROM \"SwapOuts\" s JOIN \"Nodes\" n ON n.\"Id\" = s.\"NodeId\" CROSS JOIN bounds b WHERE s.\"Status\" = 1 AND s.\"CreationDatetime\" >= b.window_start AND s.\"CreationDatetime\" < b.window_end AND n.\"PubKey\" IN ($node) GROUP BY n.\"PubKey\"), node_fees AS (SELECT \"ManagedNodePubKey\", SUM(fee_sats) AS node_total_fee_sats FROM peer_events GROUP BY \"ManagedNodePubKey\") SELECT pe.managed_node_name AS \"Node\", COALESCE(pe.peer_alias, '?') AS \"Peer\", pe.settled_count AS \"HTLC Settled\", (pe.fee_sats / 100000000.0)::numeric(20,8) AS \"Fee (BTC)\", (CASE WHEN nf.node_total_fee_sats > 0 THEN ROUND(COALESCE(sc.swap_cost_sats, 0)::numeric * pe.fee_sats / nf.node_total_fee_sats)::bigint ELSE 0 END / 100000000.0)::numeric(20,8) AS \"Swap Cost (BTC)\", ((pe.fee_sats - CASE WHEN nf.node_total_fee_sats > 0 THEN ROUND(COALESCE(sc.swap_cost_sats, 0)::numeric * pe.fee_sats / nf.node_total_fee_sats)::bigint ELSE 0 END) / 100000000.0)::numeric(20,8) AS \"Net Revenue (BTC)\", (COALESCE(pl.qualifying_liquidity_sats, 0) / 100000000.0)::numeric(20,8) AS \"Qualifying Liquidity (BTC)\", CASE WHEN pl.qualifying_liquidity_sats > 0 THEN ((pe.fee_sats - CASE WHEN nf.node_total_fee_sats > 0 THEN ROUND(COALESCE(sc.swap_cost_sats, 0)::numeric * pe.fee_sats / nf.node_total_fee_sats)::bigint ELSE 0 END)::numeric / pl.qualifying_liquidity_sats) * (365.0 / (SELECT window_days FROM bounds))::numeric ELSE NULL END AS \"APR\" FROM peer_events pe LEFT JOIN peer_liquidity pl ON pl.\"ManagedNodePubKey\" = pe.\"ManagedNodePubKey\" AND pl.peer_alias IS NOT DISTINCT FROM pe.peer_alias LEFT JOIN swap_costs sc ON sc.managed_node_pubkey = pe.\"ManagedNodePubKey\" LEFT JOIN node_fees nf ON nf.\"ManagedNodePubKey\" = pe.\"ManagedNodePubKey\" ORDER BY pe.fee_sats DESC LIMIT 50", + "rawSql": "WITH bounds AS (SELECT $__timeFrom()::timestamptz AS window_start, $__timeTo()::timestamptz AS window_end, GREATEST(EXTRACT(EPOCH FROM ($__timeTo()::timestamptz - $__timeFrom()::timestamptz)) / 86400.0, 0.000001) AS window_days), peer_events AS (SELECT f.\"ManagedNodePubKey\", MAX(f.\"ManagedNodeName\") AS managed_node_name, u.peer_alias, COUNT(*) AS settled_count, ROUND(SUM(f.\"FeeMsat\") / 1000.0)::bigint AS fee_sats FROM \"ForwardingHtlcEvents\" f CROSS JOIN LATERAL (VALUES (f.\"IncomingPeerAlias\"), (f.\"OutgoingPeerAlias\")) AS u(peer_alias) WHERE $__timeFilter(f.\"EventTimestamp\") AND f.\"Outcome\" = 1 AND f.\"ManagedNodePubKey\" IN ($node) AND f.\"FeeMsat\" IS NOT NULL GROUP BY f.\"ManagedNodePubKey\", u.peer_alias), peer_chans AS (SELECT DISTINCT f.\"ManagedNodePubKey\", u.peer_alias, u.chan_id FROM \"ForwardingHtlcEvents\" f CROSS JOIN LATERAL (VALUES (f.\"IncomingPeerAlias\", f.\"IncomingChannelId\"), (f.\"OutgoingPeerAlias\", f.\"OutgoingChannelId\")) AS u(peer_alias, chan_id) WHERE $__timeFilter(f.\"EventTimestamp\") AND f.\"Outcome\" = 1 AND f.\"ManagedNodePubKey\" IN ($node) AND f.\"FeeMsat\" IS NOT NULL), qualifying_channels AS (SELECT c.\"ChanId\", c.\"SatsAmount\", GREATEST(c.\"CreationDatetime\", b.window_start) AS open_start, LEAST(CASE WHEN c.\"Status\" = 2 THEN COALESCE((to_jsonb(c) ->> 'ClosedAt')::timestamptz, c.\"UpdateDatetime\") ELSE b.window_end END, b.window_end) AS open_end, b.window_days FROM \"Channels\" c CROSS JOIN bounds b WHERE (c.\"CreatedByNodeGuard\" = TRUE OR $liquidityScope = 0)), prorated AS (SELECT \"ChanId\", \"SatsAmount\" * GREATEST(EXTRACT(EPOCH FROM (open_end - open_start)) / 86400.0, 0.0) / window_days AS prorated_sats FROM qualifying_channels), peer_liquidity AS (SELECT pc.\"ManagedNodePubKey\", pc.peer_alias, SUM(p.prorated_sats) AS qualifying_liquidity_sats FROM peer_chans pc JOIN prorated p ON p.\"ChanId\" = pc.chan_id GROUP BY pc.\"ManagedNodePubKey\", pc.peer_alias) SELECT pe.managed_node_name AS \"Node\", COALESCE(pe.peer_alias, '?') AS \"Peer\", pe.settled_count AS \"HTLC Settled\", (pe.fee_sats / 100000000.0)::numeric(20,8) AS \"Fee (BTC)\", (COALESCE(pl.qualifying_liquidity_sats, 0) / 100000000.0)::numeric(20,8) AS \"Qualifying Liquidity (BTC)\", CASE WHEN pl.qualifying_liquidity_sats >= 1 THEN (pe.fee_sats::numeric / pl.qualifying_liquidity_sats) * (365.0 / (SELECT window_days FROM bounds))::numeric ELSE NULL END AS \"APR\" FROM peer_events pe LEFT JOIN peer_liquidity pl ON pl.\"ManagedNodePubKey\" = pe.\"ManagedNodePubKey\" AND pl.peer_alias IS NOT DISTINCT FROM pe.peer_alias ORDER BY \"APR\" DESC NULLS LAST LIMIT 50", "refId": "A" } ], @@ -5015,7 +5007,7 @@ "x": 0, "y": 179 }, - "id": 111, + "id": 113, "options": { "legend": { "calcs": [