From fc57effd5c60ea9124598fec068cc47ac32ce061 Mon Sep 17 00:00:00 2001 From: hunicus <93150691+hunicus@users.noreply.github.com> Date: Tue, 13 Sep 2022 06:44:34 -0400 Subject: [PATCH 1/8] Add python example for websocket api docs --- .../src/app/docs/api-docs/api-docs-data.ts | 34 +++++++++++++++++++ .../code-template.component.html | 7 ++++ .../code-template/code-template.component.ts | 4 +++ 3 files changed, 45 insertions(+) diff --git a/frontend/src/app/docs/api-docs/api-docs-data.ts b/frontend/src/app/docs/api-docs/api-docs-data.ts index 7ea01584e..17fc3051b 100644 --- a/frontend/src/app/docs/api-docs/api-docs-data.ts +++ b/frontend/src/app/docs/api-docs/api-docs-data.ts @@ -62,6 +62,40 @@ console.log(res["mempool-blocks"]); } }); `, + python: `import websocket +import _thread +import time +import rel +import json + +rel.safe_read() + +def on_message(ws, message): + print(json.loads(message)) + +def on_error(ws, error): + print(error) + +def on_close(ws, close_status_code, close_msg): + print("### closed ###") + +def on_open(ws): + message = { "action": "init" } + ws.send(json.dumps(message)) + message = { "action": "want", "data": ['blocks', 'stats', 'mempool-blocks', 'live-2h-chart', 'watch-mempool'] } + ws.send(json.dumps(message)) + +if __name__ == "__main__": + ws = websocket.WebSocketApp("wss://mempool.space/api/v1/ws", + on_open=on_open, + on_message=on_message, + on_error=on_error, + on_close=on_close) + + ws.run_forever(dispatcher=rel) # Set dispatcher to automatic reconnection + rel.signal(2, rel.abort) # Keyboard Interrupt + rel.dispatch() + `, }, codeSampleMainnet: emptyCodeSample, codeSampleTestnet: emptyCodeSample, diff --git a/frontend/src/app/docs/code-template/code-template.component.html b/frontend/src/app/docs/code-template/code-template.component.html index 373f7daf6..8fac91114 100644 --- a/frontend/src/app/docs/code-template/code-template.component.html +++ b/frontend/src/app/docs/code-template/code-template.component.html @@ -30,6 +30,13 @@
+
  • + Python + +
    Code Example
    +
    +
    +
  • diff --git a/frontend/src/app/docs/code-template/code-template.component.ts b/frontend/src/app/docs/code-template/code-template.component.ts index 1875e175a..961e3cb24 100644 --- a/frontend/src/app/docs/code-template/code-template.component.ts +++ b/frontend/src/app/docs/code-template/code-template.component.ts @@ -287,6 +287,10 @@ yarn add @mempool/liquid.js`; return code.codeSampleMainnet.response; } + wrapPythonTemplate(code: any) { + return ( ( this.network === 'testnet' || this.network === 'signet' ) ? ( code.codeTemplate.python.replace( "wss://mempool.space/api/v1/ws", "wss://mempool.space/" + this.network + "/api/v1/ws" ) ) : code.codeTemplate.python ); + } + replaceJSPlaceholder(text: string, code: any) { for (let index = 0; index < code.length; index++) { const textReplace = code[index]; From 19a86cbd596bb7586f03e391053a24de77d409aa Mon Sep 17 00:00:00 2001 From: hunicus <93150691+hunicus@users.noreply.github.com> Date: Tue, 13 Sep 2022 07:14:03 -0400 Subject: [PATCH 2/8] Fix spacing on ws commonjs api example --- .../src/app/docs/api-docs/api-docs-data.ts | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/frontend/src/app/docs/api-docs/api-docs-data.ts b/frontend/src/app/docs/api-docs/api-docs-data.ts index 7ea01584e..35cdb76ca 100644 --- a/frontend/src/app/docs/api-docs/api-docs-data.ts +++ b/frontend/src/app/docs/api-docs/api-docs-data.ts @@ -17,27 +17,27 @@ export const wsApiDocsData = { codeTemplate: { curl: `/api/v1/ws`, commonJS: ` - const { %{0}: { websocket } } = mempoolJS(); + const { %{0}: { websocket } } = mempoolJS(); - const ws = websocket.initClient({ - options: ['blocks', 'stats', 'mempool-blocks', 'live-2h-chart'], - }); + const ws = websocket.initClient({ + options: ['blocks', 'stats', 'mempool-blocks', 'live-2h-chart'], + }); - ws.addEventListener('message', function incoming({data}) { - const res = JSON.parse(data.toString()); - if (res.block) { - document.getElementById("result-blocks").textContent = JSON.stringify(res.block, undefined, 2); - } - if (res.mempoolInfo) { - document.getElementById("result-mempool-info").textContent = JSON.stringify(res.mempoolInfo, undefined, 2); - } - if (res.transactions) { - document.getElementById("result-transactions").textContent = JSON.stringify(res.transactions, undefined, 2); - } - if (res["mempool-blocks"]) { - document.getElementById("result-mempool-blocks").textContent = JSON.stringify(res["mempool-blocks"], undefined, 2); - } - }); + ws.addEventListener('message', function incoming({data}) { + const res = JSON.parse(data.toString()); + if (res.block) { + document.getElementById("result-blocks").textContent = JSON.stringify(res.block, undefined, 2); + } + if (res.mempoolInfo) { + document.getElementById("result-mempool-info").textContent = JSON.stringify(res.mempoolInfo, undefined, 2); + } + if (res.transactions) { + document.getElementById("result-transactions").textContent = JSON.stringify(res.transactions, undefined, 2); + } + if (res["mempool-blocks"]) { + document.getElementById("result-mempool-blocks").textContent = JSON.stringify(res["mempool-blocks"], undefined, 2); + } + }); `, esModule: ` const { %{0}: { websocket } } = mempoolJS(); From 7014ac2335b0c1329e184a6508d9e2cdf49fc3f3 Mon Sep 17 00:00:00 2001 From: hunicus <93150691+hunicus@users.noreply.github.com> Date: Tue, 13 Sep 2022 07:16:39 -0400 Subject: [PATCH 3/8] Fix spacing on ws esmodule api example --- .../src/app/docs/api-docs/api-docs-data.ts | 38 +++++++++---------- .../code-template/code-template.component.ts | 1 + 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/frontend/src/app/docs/api-docs/api-docs-data.ts b/frontend/src/app/docs/api-docs/api-docs-data.ts index 35cdb76ca..4a95c5015 100644 --- a/frontend/src/app/docs/api-docs/api-docs-data.ts +++ b/frontend/src/app/docs/api-docs/api-docs-data.ts @@ -40,27 +40,27 @@ export const wsApiDocsData = { }); `, esModule: ` -const { %{0}: { websocket } } = mempoolJS(); + const { %{0}: { websocket } } = mempoolJS(); -const ws = websocket.initServer({ -options: ["blocks", "stats", "mempool-blocks", "live-2h-chart"], -}); + const ws = websocket.initServer({ + options: ["blocks", "stats", "mempool-blocks", "live-2h-chart"], + }); -ws.on("message", function incoming(data) { -const res = JSON.parse(data.toString()); -if (res.block) { -console.log(res.block); -} -if (res.mempoolInfo) { -console.log(res.mempoolInfo); -} -if (res.transactions) { -console.log(res.transactions); -} -if (res["mempool-blocks"]) { -console.log(res["mempool-blocks"]); -} -}); + ws.on("message", function incoming(data) { + const res = JSON.parse(data.toString()); + if (res.block) { + console.log(res.block); + } + if (res.mempoolInfo) { + console.log(res.mempoolInfo); + } + if (res.transactions) { + console.log(res.transactions); + } + if (res["mempool-blocks"]) { + console.log(res["mempool-blocks"]); + } + }); `, }, codeSampleMainnet: emptyCodeSample, diff --git a/frontend/src/app/docs/code-template/code-template.component.ts b/frontend/src/app/docs/code-template/code-template.component.ts index 1875e175a..b9ec5972f 100644 --- a/frontend/src/app/docs/code-template/code-template.component.ts +++ b/frontend/src/app/docs/code-template/code-template.component.ts @@ -152,6 +152,7 @@ export class CodeTemplateComponent implements OnInit { const init = async () => { ${codeText} }; + init();`; } } From 1ead34d42d5ec3753b6a2d6c34c7564e2555d7a7 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Tue, 6 Sep 2022 12:05:23 +0200 Subject: [PATCH 4/8] Show tor+clearnet node series in chart --- backend/src/api/explorer/statistics.api.ts | 3 +- .../nodes-networks-chart.component.ts | 37 ++++++++++++++----- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/backend/src/api/explorer/statistics.api.ts b/backend/src/api/explorer/statistics.api.ts index 558ee86fd..cab8bfc29 100644 --- a/backend/src/api/explorer/statistics.api.ts +++ b/backend/src/api/explorer/statistics.api.ts @@ -6,7 +6,8 @@ class StatisticsApi { public async $getStatistics(interval: string | null = null): Promise { interval = Common.getSqlInterval(interval); - let query = `SELECT UNIX_TIMESTAMP(added) AS added, channel_count, total_capacity, tor_nodes, clearnet_nodes, unannounced_nodes + let query = `SELECT UNIX_TIMESTAMP(added) AS added, channel_count, total_capacity, + tor_nodes, clearnet_nodes, unannounced_nodes, clearnet_tor_nodes FROM lightning_stats`; if (interval) { diff --git a/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts b/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts index a6278658a..1d87b7929 100644 --- a/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts +++ b/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts @@ -89,10 +89,11 @@ export class NodesNetworksChartComponent implements OnInit { tor_nodes: data.map(val => [val.added * 1000, val.tor_nodes]), clearnet_nodes: data.map(val => [val.added * 1000, val.clearnet_nodes]), unannounced_nodes: data.map(val => [val.added * 1000, val.unannounced_nodes]), + clearnet_tor_nodes: data.map(val => [val.added * 1000, val.clearnet_tor_nodes]), }; let maxYAxis = 0; for (const day of data) { - maxYAxis = Math.max(maxYAxis, day.tor_nodes + day.clearnet_nodes + day.unannounced_nodes); + maxYAxis = Math.max(maxYAxis, day.tor_nodes + day.clearnet_nodes + day.unannounced_nodes + day.clearnet_tor_nodes); } maxYAxis = Math.ceil(maxYAxis / 3000) * 3000; this.prepareChartOptions(chartData, maxYAxis); @@ -163,13 +164,16 @@ export class NodesNetworksChartComponent implements OnInit { const date = new Date(ticks[0].data[0]).toLocaleDateString(this.locale, { year: 'numeric', month: 'short', day: 'numeric' }); let tooltip = `${date}
    `; - for (const tick of ticks.reverse()) { + console.log(ticks); + for (const tick of ticks) { if (tick.seriesIndex === 0) { // Tor tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.0-0')}`; } else if (tick.seriesIndex === 1) { // Clearnet tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.0-0')}`; } else if (tick.seriesIndex === 2) { // Unannounced tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.0-0')}`; + } else if (tick.seriesIndex === 3) { // Tor + Clearnet + tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.0-0')}`; } tooltip += `
    `; total += tick.data[1]; @@ -189,14 +193,6 @@ export class NodesNetworksChartComponent implements OnInit { legend: this.widget || data.tor_nodes.length === 0 ? undefined : { padding: 10, data: [ - { - name: $localize`Total`, - inactiveColor: 'rgb(110, 112, 121)', - textStyle: { - color: 'white', - }, - icon: 'roundRect', - }, { name: $localize`Tor`, inactiveColor: 'rgb(110, 112, 121)', @@ -323,6 +319,27 @@ export class NodesNetworksChartComponent implements OnInit { ]), smooth: false, }, + { + zlevel: 1, + yAxisIndex: 1, + name: $localize`Clearnet & Tor`, + showSymbol: false, + symbol: 'none', + data: data.clearnet_tor_nodes, + type: 'line', + lineStyle: { + width: 2, + }, + areaStyle: { + opacity: 0.5, + }, + stack: 'Total', + color: new graphic.LinearGradient(0, 0.75, 0, 1, [ + { offset: 0, color: '#be7d4c' }, + { offset: 1, color: '#be7d4cAA' }, + ]), + smooth: false, + }, { zlevel: 1, yAxisIndex: 1, From 0f218ced47fa230e2ed748e89314cf052469c732 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Thu, 8 Sep 2022 19:03:37 +0200 Subject: [PATCH 5/8] Fix legend --- .../nodes-networks-chart.component.ts | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts b/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts index 1d87b7929..ba75661fe 100644 --- a/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts +++ b/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts @@ -209,6 +209,14 @@ export class NodesNetworksChartComponent implements OnInit { }, icon: 'roundRect', }, + { + name: $localize`Clearnet & Tor`, + inactiveColor: 'rgb(110, 112, 121)', + textStyle: { + color: 'white', + }, + icon: 'roundRect', + }, { name: $localize`Unannounced`, inactiveColor: 'rgb(110, 112, 121)', @@ -219,10 +227,10 @@ export class NodesNetworksChartComponent implements OnInit { }, ], selected: this.widget ? undefined : JSON.parse(this.storageService.getValue('nodes_networks_legend')) ?? { - 'Total': true, - 'Tor': true, - 'Clearnet': true, - 'Unannounced': true, + '$localize`Tor`': true, + '$localize`Clearnet`': true, + '$localize`Clearnet & Tor`': true, + '$localize`Unannounced`': true, } }, yAxis: data.tor_nodes.length === 0 ? undefined : [ From b7b1dfdeb5091f14ccdc85634a158348a5e16e70 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Thu, 22 Sep 2022 16:09:26 +0200 Subject: [PATCH 6/8] Change naming in networks line chart + Fix y axis scaling --- .../nodes-networks-chart.component.ts | 221 ++++++++++-------- 1 file changed, 121 insertions(+), 100 deletions(-) diff --git a/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts b/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts index ba75661fe..c1647cd25 100644 --- a/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts +++ b/frontend/src/app/lightning/nodes-networks-chart/nodes-networks-chart.component.ts @@ -1,5 +1,5 @@ import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit, HostBinding } from '@angular/core'; -import { EChartsOption, graphic} from 'echarts'; +import { EChartsOption, graphic, LineSeriesOption} from 'echarts'; import { Observable } from 'rxjs'; import { map, share, startWith, switchMap, tap } from 'rxjs/operators'; import { formatNumber } from '@angular/common'; @@ -135,6 +135,94 @@ export class NodesNetworksChartComponent implements OnInit { }; } + const series: LineSeriesOption[] = [ + { + zlevel: 1, + yAxisIndex: 0, + name: $localize`Unknown`, + showSymbol: false, + symbol: 'none', + data: data.unannounced_nodes, + type: 'line', + lineStyle: { + width: 2, + }, + areaStyle: { + opacity: 0.5, + }, + stack: 'Total', + color: new graphic.LinearGradient(0, 0.75, 0, 1, [ + { offset: 0, color: '#D81B60' }, + { offset: 1, color: '#D81B60AA' }, + ]), + + smooth: false, + }, + { + zlevel: 1, + yAxisIndex: 0, + name: $localize`Reachable on Clearnet Only`, + showSymbol: false, + symbol: 'none', + data: data.clearnet_nodes, + type: 'line', + lineStyle: { + width: 2, + }, + areaStyle: { + opacity: 0.5, + }, + stack: 'Total', + color: new graphic.LinearGradient(0, 0.75, 0, 1, [ + { offset: 0, color: '#FFB300' }, + { offset: 1, color: '#FFB300AA' }, + ]), + smooth: false, + }, + { + zlevel: 1, + yAxisIndex: 0, + name: $localize`Reachable on Clearnet and Darknet`, + showSymbol: false, + symbol: 'none', + data: data.clearnet_tor_nodes, + type: 'line', + lineStyle: { + width: 2, + }, + areaStyle: { + opacity: 0.5, + }, + stack: 'Total', + color: new graphic.LinearGradient(0, 0.75, 0, 1, [ + { offset: 0, color: '#be7d4c' }, + { offset: 1, color: '#be7d4cAA' }, + ]), + smooth: false, + }, + { + zlevel: 1, + yAxisIndex: 0, + name: $localize`Reachable on Darknet Only`, + showSymbol: false, + symbol: 'none', + data: data.tor_nodes, + type: 'line', + lineStyle: { + width: 2, + }, + areaStyle: { + opacity: 0.5, + }, + stack: 'Total', + color: new graphic.LinearGradient(0, 0.75, 0, 1, [ + { offset: 0, color: '#7D4698' }, + { offset: 1, color: '#7D4698AA' }, + ]), + smooth: false, + }, + ]; + this.chartOptions = { title: title, animation: false, @@ -164,8 +252,10 @@ export class NodesNetworksChartComponent implements OnInit { const date = new Date(ticks[0].data[0]).toLocaleDateString(this.locale, { year: 'numeric', month: 'short', day: 'numeric' }); let tooltip = `${date}
    `; - console.log(ticks); - for (const tick of ticks) { + for (const tick of ticks.reverse()) { + if (tick.seriesName.indexOf('ignored') !== -1) { + continue; + } if (tick.seriesIndex === 0) { // Tor tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.0-0')}`; } else if (tick.seriesIndex === 1) { // Clearnet @@ -194,7 +284,7 @@ export class NodesNetworksChartComponent implements OnInit { padding: 10, data: [ { - name: $localize`Tor`, + name: $localize`Reachable on Darknet Only`, inactiveColor: 'rgb(110, 112, 121)', textStyle: { color: 'white', @@ -202,7 +292,7 @@ export class NodesNetworksChartComponent implements OnInit { icon: 'roundRect', }, { - name: $localize`Clearnet`, + name: $localize`Reachable on Clearnet and Darknet`, inactiveColor: 'rgb(110, 112, 121)', textStyle: { color: 'white', @@ -210,7 +300,7 @@ export class NodesNetworksChartComponent implements OnInit { icon: 'roundRect', }, { - name: $localize`Clearnet & Tor`, + name: $localize`Reachable on Clearnet Only`, inactiveColor: 'rgb(110, 112, 121)', textStyle: { color: 'white', @@ -218,7 +308,7 @@ export class NodesNetworksChartComponent implements OnInit { icon: 'roundRect', }, { - name: $localize`Unannounced`, + name: $localize`Unknown`, inactiveColor: 'rgb(110, 112, 121)', textStyle: { color: 'white', @@ -227,10 +317,10 @@ export class NodesNetworksChartComponent implements OnInit { }, ], selected: this.widget ? undefined : JSON.parse(this.storageService.getValue('nodes_networks_legend')) ?? { - '$localize`Tor`': true, - '$localize`Clearnet`': true, - '$localize`Clearnet & Tor`': true, - '$localize`Unannounced`': true, + '$localize`Reachable on Darknet Only`': true, + '$localize`Reachable on Clearnet Only`': true, + '$localize`Reachable on Clearnet and Darknet`': true, + '$localize`Unknown`': true, } }, yAxis: data.tor_nodes.length === 0 ? undefined : [ @@ -254,7 +344,6 @@ export class NodesNetworksChartComponent implements OnInit { opacity: 0.25, }, }, - max: maxYAxis, min: 0, interval: 3000, }, @@ -278,98 +367,25 @@ export class NodesNetworksChartComponent implements OnInit { opacity: 0.25, }, }, - max: maxYAxis, min: 0, interval: 3000, } ], - series: data.tor_nodes.length === 0 ? [] : [ - { - zlevel: 1, - yAxisIndex: 0, - name: $localize`Unannounced`, - showSymbol: false, - symbol: 'none', - data: data.unannounced_nodes, - type: 'line', - lineStyle: { - width: 2, - }, - areaStyle: { - opacity: 0.5, - }, - stack: 'Total', - color: new graphic.LinearGradient(0, 0.75, 0, 1, [ - { offset: 0, color: '#D81B60' }, - { offset: 1, color: '#D81B60AA' }, - ]), - - smooth: false, - }, - { - zlevel: 1, - yAxisIndex: 0, - name: $localize`Clearnet`, - showSymbol: false, - symbol: 'none', - data: data.clearnet_nodes, - type: 'line', - lineStyle: { - width: 2, - }, - areaStyle: { - opacity: 0.5, - }, - stack: 'Total', - color: new graphic.LinearGradient(0, 0.75, 0, 1, [ - { offset: 0, color: '#FFB300' }, - { offset: 1, color: '#FFB300AA' }, - ]), - smooth: false, - }, - { - zlevel: 1, - yAxisIndex: 1, - name: $localize`Clearnet & Tor`, - showSymbol: false, - symbol: 'none', - data: data.clearnet_tor_nodes, - type: 'line', - lineStyle: { - width: 2, - }, - areaStyle: { - opacity: 0.5, - }, - stack: 'Total', - color: new graphic.LinearGradient(0, 0.75, 0, 1, [ - { offset: 0, color: '#be7d4c' }, - { offset: 1, color: '#be7d4cAA' }, - ]), - smooth: false, - }, - { - zlevel: 1, - yAxisIndex: 1, - name: $localize`Tor`, - showSymbol: false, - symbol: 'none', - data: data.tor_nodes, - type: 'line', - lineStyle: { - width: 2, - }, - areaStyle: { - opacity: 0.5, - }, - stack: 'Total', - color: new graphic.LinearGradient(0, 0.75, 0, 1, [ - { offset: 0, color: '#7D4698' }, - { offset: 1, color: '#7D4698AA' }, - ]), - smooth: false, - }, - ], + series: data.tor_nodes.length === 0 ? [] : series.concat(series.map((serie) => { + // We create dummy duplicated series so when we use the data zoom, the y axis + // both scales properly + const invisibleSerie = {...serie}; + invisibleSerie.name = 'ignored' + Math.random().toString(); + invisibleSerie.stack = 'ignored'; + invisibleSerie.yAxisIndex = 1; + invisibleSerie.lineStyle = { + opacity: 0, + }; + invisibleSerie.areaStyle = { + opacity: 0, + }; + return invisibleSerie; + })), dataZoom: this.widget ? null : [{ type: 'inside', realtime: true, @@ -396,6 +412,11 @@ export class NodesNetworksChartComponent implements OnInit { }, }], }; + + if (isMobile()) { + // @ts-ignore + this.chartOptions.legend.left = 50; + } } onChartInit(ec): void { From 6df731af58595bb89fd22035016b412777a91fd6 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Thu, 22 Sep 2022 18:35:16 +0200 Subject: [PATCH 7/8] Only show tor badge in node page if actually running on tor only --- frontend/src/app/lightning/node/node.component.html | 5 ++++- frontend/src/app/lightning/node/node.component.ts | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/lightning/node/node.component.html b/frontend/src/app/lightning/node/node.component.html index 0c8451d44..441eeb78e 100644 --- a/frontend/src/app/lightning/node/node.component.html +++ b/frontend/src/app/lightning/node/node.component.html @@ -85,9 +85,12 @@ {{ node.as_organization }} [ASN {{node.as_number}}] - + Exclusively on Tor + + Unknown + diff --git a/frontend/src/app/lightning/node/node.component.ts b/frontend/src/app/lightning/node/node.component.ts index 1bf100af4..cbfa66c89 100644 --- a/frontend/src/app/lightning/node/node.component.ts +++ b/frontend/src/app/lightning/node/node.component.ts @@ -22,6 +22,8 @@ export class NodeComponent implements OnInit { error: Error; publicKey: string; channelListLoading = false; + clearnetSocketCount = 0; + torSocketCount = 0; constructor( private lightningApiService: LightningApiService, @@ -47,10 +49,13 @@ export class NodeComponent implements OnInit { let label = ''; if (socket.match(/(?:[0-9]{1,3}\.){3}[0-9]{1,3}/)) { label = 'IPv4'; + this.clearnetSocketCount++; } else if (socket.indexOf('[') > -1) { label = 'IPv6'; + this.clearnetSocketCount++; } else if (socket.indexOf('onion') > -1) { label = 'Tor'; + this.torSocketCount++; } socketsObject.push({ label: label, From 409e5a335f777e30da17ba647da0e64301931ca3 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Fri, 23 Sep 2022 19:03:21 +0000 Subject: [PATCH 8/8] Improve tx flow diagram drawing algorithm --- .../transaction/transaction.component.html | 12 +- .../transaction/transaction.component.ts | 4 +- .../tx-bowtie-graph.component.ts | 148 ++++++++++++------ 3 files changed, 116 insertions(+), 48 deletions(-) diff --git a/frontend/src/app/components/transaction/transaction.component.html b/frontend/src/app/components/transaction/transaction.component.html index f25e2a012..81d3778f8 100644 --- a/frontend/src/app/components/transaction/transaction.component.html +++ b/frontend/src/app/components/transaction/transaction.component.html @@ -191,12 +191,20 @@
    -

    Diagram

    +

    Flow

    - + +
    diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts index 948eecdb1..3c29f210d 100644 --- a/frontend/src/app/components/transaction/transaction.component.ts +++ b/frontend/src/app/components/transaction/transaction.component.ts @@ -50,7 +50,9 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { graphExpanded: boolean = false; graphWidth: number = 1000; graphHeight: number = 360; + inOutLimit: number = 150; maxInOut: number = 0; + tooltipPosition: { x: number, y: number }; @ViewChild('graphContainer') @@ -298,7 +300,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy { } setupGraph() { - this.maxInOut = Math.min(250, Math.max(this.tx?.vin?.length || 1, this.tx?.vout?.length + 1 || 1)); + this.maxInOut = Math.min(this.inOutLimit, Math.max(this.tx?.vin?.length || 1, this.tx?.vout?.length + 1 || 1)); this.graphHeight = Math.min(360, this.maxInOut * 80); } diff --git a/frontend/src/app/components/tx-bowtie-graph/tx-bowtie-graph.component.ts b/frontend/src/app/components/tx-bowtie-graph/tx-bowtie-graph.component.ts index 78a865b89..16e2736f7 100644 --- a/frontend/src/app/components/tx-bowtie-graph/tx-bowtie-graph.component.ts +++ b/frontend/src/app/components/tx-bowtie-graph/tx-bowtie-graph.component.ts @@ -19,8 +19,6 @@ interface Xput { confidential?: boolean; } -const lineLimit = 250; - @Component({ selector: 'tx-bowtie-graph', templateUrl: './tx-bowtie-graph.component.html', @@ -31,7 +29,8 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges { @Input() network: string; @Input() width = 1200; @Input() height = 600; - @Input() combinedWeight = 100; + @Input() lineLimit = 250; + @Input() maxCombinedWeight = 100; @Input() minWeight = 2; // @Input() maxStrands = 24; // number of inputs/outputs to keep fully on-screen. @Input() tooltip = false; @@ -42,6 +41,7 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges { outputs: SvgLine[]; middle: SvgLine; midWidth: number; + combinedWeight: number; isLiquid: boolean = false; hoverLine: Xput | void = null; tooltipPosition = { x: 0, y: 0 }; @@ -62,20 +62,19 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges { gradient: string[] = ['#105fb0', '#105fb0']; ngOnInit(): void { - this.isLiquid = (this.network === 'liquid' || this.network === 'liquidtestnet'); - this.gradient = this.gradientColors[this.network]; - this.midWidth = Math.min(50, Math.ceil(this.width / 20)); this.initGraph(); } ngOnChanges(): void { - this.isLiquid = (this.network === 'liquid' || this.network === 'liquidtestnet'); - this.gradient = this.gradientColors[this.network]; - this.midWidth = Math.min(50, Math.ceil(this.width / 20)); this.initGraph(); } initGraph(): void { + this.isLiquid = (this.network === 'liquid' || this.network === 'liquidtestnet'); + this.gradient = this.gradientColors[this.network]; + this.midWidth = Math.min(10, Math.ceil(this.width / 100)); + this.combinedWeight = Math.min(this.maxCombinedWeight, Math.floor((this.width - (2 * this.midWidth)) / 6)); + const totalValue = this.calcTotalValue(this.tx); let voutWithFee = this.tx.vout.map(v => { return { @@ -103,19 +102,19 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges { } as Xput; }); - if (truncatedInputs.length > lineLimit) { - const valueOfRest = truncatedInputs.slice(lineLimit).reduce((r, v) => { + if (truncatedInputs.length > this.lineLimit) { + const valueOfRest = truncatedInputs.slice(this.lineLimit).reduce((r, v) => { return r + (v.value || 0); }, 0); - truncatedInputs = truncatedInputs.slice(0, lineLimit); - truncatedInputs.push({ type: 'input', value: valueOfRest, rest: this.tx.vin.length - lineLimit }); + truncatedInputs = truncatedInputs.slice(0, this.lineLimit); + truncatedInputs.push({ type: 'input', value: valueOfRest, rest: this.tx.vin.length - this.lineLimit }); } - if (voutWithFee.length > lineLimit) { - const valueOfRest = voutWithFee.slice(lineLimit).reduce((r, v) => { + if (voutWithFee.length > this.lineLimit) { + const valueOfRest = voutWithFee.slice(this.lineLimit).reduce((r, v) => { return r + (v.value || 0); }, 0); - voutWithFee = voutWithFee.slice(0, lineLimit); - voutWithFee.push({ type: 'output', value: valueOfRest, rest: outputCount - lineLimit }); + voutWithFee = voutWithFee.slice(0, this.lineLimit); + voutWithFee.push({ type: 'output', value: valueOfRest, rest: outputCount - this.lineLimit }); } this.inputData = truncatedInputs; @@ -126,7 +125,7 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges { this.middle = { path: `M ${(this.width / 2) - this.midWidth} ${(this.height / 2) + 0.5} L ${(this.width / 2) + this.midWidth} ${(this.height / 2) + 0.5}`, - style: `stroke-width: ${this.combinedWeight + 0.5}; stroke: ${this.gradient[1]}` + style: `stroke-width: ${this.combinedWeight + 1}; stroke: ${this.gradient[1]}` }; } @@ -157,7 +156,7 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges { initLines(side: 'in' | 'out', xputs: Xput[], total: number, maxVisibleStrands: number): SvgLine[] { if (!total) { - const weights = xputs.map((put): number => this.combinedWeight / xputs.length); + const weights = xputs.map((put) => this.combinedWeight / xputs.length); return this.linesFromWeights(side, xputs, weights, maxVisibleStrands); } else { let unknownCount = 0; @@ -171,19 +170,26 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges { }); const unknownShare = unknownTotal / unknownCount; // conceptual weights - const weights = xputs.map((put): number => this.combinedWeight * (put.value == null ? unknownShare : put.value as number) / total); + const weights = xputs.map((put) => this.combinedWeight * (put.value == null ? unknownShare : put.value as number) / total); return this.linesFromWeights(side, xputs, weights, maxVisibleStrands); } } - linesFromWeights(side: 'in' | 'out', xputs: Xput[], weights: number[], maxVisibleStrands: number) { - const lines = []; - // actual displayed line thicknesses - const minWeights = weights.map((w) => Math.max(this.minWeight - 1, w) + 1); + linesFromWeights(side: 'in' | 'out', xputs: Xput[], weights: number[], maxVisibleStrands: number): SvgLine[] { + const lineParams = weights.map((w) => { + return { + weight: w, + thickness: Math.max(this.minWeight - 1, w) + 1, + offset: 0, + innerY: 0, + outerY: 0, + }; + }); const visibleStrands = Math.min(maxVisibleStrands, xputs.length); - const visibleWeight = minWeights.slice(0, visibleStrands).reduce((acc, v) => v + acc, 0); + const visibleWeight = lineParams.slice(0, visibleStrands).reduce((acc, v) => v.thickness + acc, 0); const gaps = visibleStrands - 1; + // bounds of the middle segment const innerTop = (this.height / 2) - (this.combinedWeight / 2); const innerBottom = innerTop + this.combinedWeight; // tracks the visual bottom of the endpoints of the previous line @@ -192,39 +198,91 @@ export class TxBowtieGraphComponent implements OnInit, OnChanges { // gap between strands const spacing = (this.height - visibleWeight) / gaps; - for (let i = 0; i < xputs.length; i++) { - const weight = weights[i]; - const minWeight = minWeights[i]; + // curve adjustments to prevent overlaps + let offset = 0; + let minOffset = 0; + let maxOffset = 0; + let lastWeight = 0; + let pad = 0; + lineParams.forEach((line, i) => { // set the vertical position of the (center of the) outer side of the line - let outer = lastOuter + (minWeight / 2); - const inner = Math.min(innerBottom + (minWeight / 2), Math.max(innerTop + (minWeight / 2), lastInner + (weight / 2))); + line.outerY = lastOuter + (line.thickness / 2); + line.innerY = Math.min(innerBottom + (line.thickness / 2), Math.max(innerTop + (line.thickness / 2), lastInner + (line.weight / 2))); // special case to center single input/outputs if (xputs.length === 1) { - outer = (this.height / 2); + line.outerY = (this.height / 2); } - lastOuter += minWeight + spacing; - lastInner += weight; - lines.push({ - path: this.makePath(side, outer, inner, minWeight), - style: this.makeStyle(minWeight, xputs[i].type), - class: xputs[i].type - }); - } + lastOuter += line.thickness + spacing; + lastInner += line.weight; - return lines; + // calculate conservative lower bound of the amount of horizontal offset + // required to prevent this line overlapping its neighbor + + if (this.tooltip || !xputs[i].rest) { + const w = (this.width - Math.max(lastWeight, line.weight)) / 2; // approximate horizontal width of the curved section of the line + const y1 = line.outerY; + const y2 = line.innerY; + const t = (lastWeight + line.weight) / 2; // distance between center of this line and center of previous line + + // slope of the inflection point of the bezier curve + const dx = 0.75 * w; + const dy = 1.5 * (y2 - y1); + const a = Math.atan2(dy, dx); + + // parallel curves should be separated by >=t at the inflection point to prevent overlap + // vertical offset is always = t, contributing tCos(a) + // horizontal offset h will contribute hSin(a) + // tCos(a) + hSin(a) >= t + // h >= t(1 - cos(a)) / sin(a) + if (Math.sin(a) !== 0) { + // (absolute value clamped to t for sanity) + offset += Math.max(Math.min(t * (1 - Math.cos(a)) / Math.sin(a), t), -t); + } + + line.offset = offset; + minOffset = Math.min(minOffset, offset); + maxOffset = Math.max(maxOffset, offset); + pad = Math.max(pad, line.thickness / 2); + lastWeight = line.weight; + } else { + // skip the offsets for consolidated lines in unfurls, since these *should* overlap a little + } + }); + + // normalize offsets + lineParams.forEach((line) => { + line.offset -= minOffset; + }); + maxOffset -= minOffset; + + return lineParams.map((line, i) => { + return { + path: this.makePath(side, line.outerY, line.innerY, line.thickness, line.offset, pad + maxOffset), + style: this.makeStyle(line.thickness, xputs[i].type), + class: xputs[i].type + }; + }); } - makePath(side: 'in' | 'out', outer: number, inner: number, weight: number): string { - const start = side === 'in' ? (weight * 0.5) : this.width - (weight * 0.5); - const center = this.width / 2 + (side === 'in' ? -(this.midWidth * 0.9) : (this.midWidth * 0.9) ); - const midpoint = (start + center) / 2; + makePath(side: 'in' | 'out', outer: number, inner: number, weight: number, offset: number, pad: number): string { + const start = (weight * 0.5); + const curveStart = Math.max(start + 1, pad - offset); + const end = this.width / 2 - (this.midWidth * 0.9) + 1; + const curveEnd = end - offset - 10; + const midpoint = (curveStart + curveEnd) / 2; + // correct for svg horizontal gradient bug if (Math.round(outer) === Math.round(inner)) { outer -= 1; } - return `M ${start} ${outer} C ${midpoint} ${outer}, ${midpoint} ${inner}, ${center} ${inner}`; + + if (side === 'in') { + return `M ${start} ${outer} L ${curveStart} ${outer} C ${midpoint} ${outer}, ${midpoint} ${inner}, ${curveEnd} ${inner} L ${end} ${inner}`; + } else { // mirrored in y-axis for the right hand side + return `M ${this.width - start} ${outer} L ${this.width - curveStart} ${outer} C ${this.width - midpoint} ${outer}, ${this.width - midpoint} ${inner}, ${this.width - curveEnd} ${inner} L ${this.width - end} ${inner}`; + } } makeStyle(minWeight, type): string {