From 8bacb4c6a2c471beaf9dda0cbfcc55da5eac3249 Mon Sep 17 00:00:00 2001 From: "transifex-integration[bot]" <43880903+transifex-integration[bot]@users.noreply.github.com> Date: Fri, 15 Jul 2022 14:25:36 +0000 Subject: [PATCH 001/105] Translate /frontend/src/locale/messages.xlf in pl review completed for the source file '/frontend/src/locale/messages.xlf' on the 'pl' language. --- frontend/src/locale/messages.pl.xlf | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/frontend/src/locale/messages.pl.xlf b/frontend/src/locale/messages.pl.xlf index 67e48fcf7..a2be50b95 100644 --- a/frontend/src/locale/messages.pl.xlf +++ b/frontend/src/locale/messages.pl.xlf @@ -1997,6 +1997,7 @@ At block: + W bloku: src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.ts 188 @@ -2020,6 +2021,7 @@ Around block: + W okolicu bloku: src/app/components/block-fee-rates-graph/block-fee-rates-graph.component.ts 190 @@ -2234,6 +2236,7 @@ Block Prediction Accuracy + Dokładność prognoz bloków src/app/components/block-prediction-graph/block-prediction-graph.component.html 5,7 @@ -2250,6 +2253,7 @@ Match rate + Częstość trafień src/app/components/block-prediction-graph/block-prediction-graph.component.ts 176,174 @@ -2867,6 +2871,7 @@ Usually places your transaction in between the second and third mempool blocks + Zazwyczaj umieszcza Twoją transakcje między drugim a trzecim blokiem w mempool src/app/components/fees-box/fees-box.component.html 8,9 @@ -2888,6 +2893,7 @@ Usually places your transaction in between the first and second mempool blocks + Zazwyczaj umieszcza Twoją transakcje między pierwszym a drugim blokiem w mempool src/app/components/fees-box/fees-box.component.html 9,10 @@ -3072,6 +3078,7 @@ Hashrate (MA) + Prędkość haszowania (MA) src/app/components/hashrate-chart/hashrate-chart.component.ts 288,287 @@ -3244,6 +3251,7 @@ Pools luck (1 week) + Szczęście kolektywu (1 tydzień) src/app/components/pool-ranking/pool-ranking.component.html 9 @@ -3252,6 +3260,7 @@ Pools luck + Szczęście kolektywu src/app/components/pool-ranking/pool-ranking.component.html 9,11 @@ -3260,6 +3269,7 @@ The overall luck of all mining pools over the past week. A luck bigger than 100% means the average block time for the current epoch is less than 10 minutes. + Ogólne szczęście wszystkich kolektywów wydobywczych w ciągu ostatniego tygodnia. Szczęście większe niż 100% oznacza, że średni czas bloku dla danej epoki jest mniejszy niż 10 minut. src/app/components/pool-ranking/pool-ranking.component.html 11,15 @@ -3268,6 +3278,7 @@ Pools count (1w) + Liczba kolektywów (1t) src/app/components/pool-ranking/pool-ranking.component.html 17 @@ -3276,6 +3287,7 @@ Pools count + Liczba kolektywów src/app/components/pool-ranking/pool-ranking.component.html 17,19 @@ -3284,6 +3296,7 @@ How many unique pools found at least one block over the past week. + Ile unikatowych kolektywów znalazło conajmniej jeden blok w ciągu ostatniego tygodnia. src/app/components/pool-ranking/pool-ranking.component.html 19,23 @@ -3309,6 +3322,7 @@ The number of blocks found over the past week. + Liczba bloków znalezionych w ciągu ostatniego tygodnia. src/app/components/pool-ranking/pool-ranking.component.html 27,31 @@ -4465,6 +4479,7 @@ REST API service + Usługa REST API src/app/docs/api-docs/api-docs.component.html 34,35 From a6535277ef9a7243edeb38deaf246d4cb0ede04b Mon Sep 17 00:00:00 2001 From: Mononaut Date: Tue, 12 Jul 2022 22:04:20 +0000 Subject: [PATCH 002/105] Add open graph block preview page --- frontend/src/app/app-routing.module.ts | 15 ++- .../block/block-preview.component.html | 92 +++++++++++++++++++ .../block/block-preview.component.scss | 7 ++ .../block/block-preview.component.ts | 11 +++ .../master-page-preview.component.html | 21 +++++ .../master-page-preview.component.scss | 35 +++++++ .../master-page-preview.component.ts | 25 +++++ frontend/src/app/shared/shared.module.ts | 5 + 8 files changed, 209 insertions(+), 2 deletions(-) create mode 100644 frontend/src/app/components/block/block-preview.component.html create mode 100644 frontend/src/app/components/block/block-preview.component.scss create mode 100644 frontend/src/app/components/block/block-preview.component.ts create mode 100644 frontend/src/app/components/master-page-preview/master-page-preview.component.html create mode 100644 frontend/src/app/components/master-page-preview/master-page-preview.component.scss create mode 100644 frontend/src/app/components/master-page-preview/master-page-preview.component.ts diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 1a658c44b..c9f7e19d4 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -4,8 +4,10 @@ import { StartComponent } from './components/start/start.component'; import { TransactionComponent } from './components/transaction/transaction.component'; import { BlockComponent } from './components/block/block.component'; import { BlockAuditComponent } from './components/block-audit/block-audit.component'; +import { BlockPreviewComponent } from './components/block/block-preview.component'; import { AddressComponent } from './components/address/address.component'; import { MasterPageComponent } from './components/master-page/master-page.component'; +import { MasterPagePreviewComponent } from './components/master-page-preview/master-page-preview.component'; import { AboutComponent } from './components/about/about.component'; import { StatusViewComponent } from './components/status-view/status-view.component'; import { TermsOfServiceComponent } from './components/terms-of-service/terms-of-service.component'; @@ -23,7 +25,7 @@ import { AssetComponent } from './components/asset/asset.component'; import { AssetsNavComponent } from './components/assets/assets-nav/assets-nav.component'; let routes: Routes = [ - { + { path: 'testnet', children: [ { @@ -315,6 +317,16 @@ let routes: Routes = [ }, ], }, + { + path: 'preview', + component: MasterPagePreviewComponent, + children: [ + { + path: 'block/:id', + component: BlockPreviewComponent + }, + ], + }, { path: 'status', component: StatusViewComponent @@ -576,4 +588,3 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { })], }) export class AppRoutingModule { } - diff --git a/frontend/src/app/components/block/block-preview.component.html b/frontend/src/app/components/block/block-preview.component.html new file mode 100644 index 000000000..5cfc31c70 --- /dev/null +++ b/frontend/src/app/components/block/block-preview.component.html @@ -0,0 +1,92 @@ +
+
+
+

+ Genesis + + {{ blockHeight }} + + + Block + + + {{ blockHeight }} + + +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Timestamp + {{ block?.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }} +
Size
Weight
Median fee~{{ block?.extras?.medianFee | number:'1.0-0' }} sat/vB
Total fees + + + +
Subsidy + fees: + +
Miner + + {{ block?.extras.pool.name }} + + + + {{ block?.extras.pool.name }} + +
+
+
+ +
+
+
diff --git a/frontend/src/app/components/block/block-preview.component.scss b/frontend/src/app/components/block/block-preview.component.scss new file mode 100644 index 000000000..fb51413e3 --- /dev/null +++ b/frontend/src/app/components/block/block-preview.component.scss @@ -0,0 +1,7 @@ +.box { + padding: 2rem 6rem; +} + +.block-title { + margin-bottom: 0.5em; +} diff --git a/frontend/src/app/components/block/block-preview.component.ts b/frontend/src/app/components/block/block-preview.component.ts new file mode 100644 index 000000000..bab4e0489 --- /dev/null +++ b/frontend/src/app/components/block/block-preview.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; +import { BlockComponent } from './block.component'; + +@Component({ + selector: 'app-block-preview', + templateUrl: './block-preview.component.html', + styleUrls: ['./block.component.scss', './block-preview.component.scss'] +}) +export class BlockPreviewComponent extends BlockComponent { + +} diff --git a/frontend/src/app/components/master-page-preview/master-page-preview.component.html b/frontend/src/app/components/master-page-preview/master-page-preview.component.html new file mode 100644 index 000000000..6c2e45242 --- /dev/null +++ b/frontend/src/app/components/master-page-preview/master-page-preview.component.html @@ -0,0 +1,21 @@ + +
+ + +
+ + + + + +
+ logo Signet + testnet logo Testnet + bisq logo Bisq + liquid mainnet logo Liquid + liquid testnet logo Liquid Testnet + bitcoin logo Mainnet +
+
+
+
diff --git a/frontend/src/app/components/master-page-preview/master-page-preview.component.scss b/frontend/src/app/components/master-page-preview/master-page-preview.component.scss new file mode 100644 index 000000000..eebb9a75b --- /dev/null +++ b/frontend/src/app/components/master-page-preview/master-page-preview.component.scss @@ -0,0 +1,35 @@ +.preview-wrapper { + position: relative; + display: block; + margin: auto; + max-width: 1024px; + max-height: 512px; + padding-bottom: 64px; + + footer { + position: absolute; + left: 0; + right: 0; + bottom: 0; + z-index: 100; + min-height: 64px; + padding: 0rem 2rem; + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + background: #11131f; + text-align: start; + } + + .footer-brand { + width: 60%; + } + + .network { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: center; + } +} diff --git a/frontend/src/app/components/master-page-preview/master-page-preview.component.ts b/frontend/src/app/components/master-page-preview/master-page-preview.component.ts new file mode 100644 index 000000000..9678aa32d --- /dev/null +++ b/frontend/src/app/components/master-page-preview/master-page-preview.component.ts @@ -0,0 +1,25 @@ +import { Component, OnInit } from '@angular/core'; +import { StateService } from '../../services/state.service'; +import { Observable, merge, of } from 'rxjs'; +import { LanguageService } from 'src/app/services/language.service'; + +@Component({ + selector: 'app-master-page-preview', + templateUrl: './master-page-preview.component.html', + styleUrls: ['./master-page-preview.component.scss'], +}) +export class MasterPagePreviewComponent implements OnInit { + network$: Observable; + officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE; + urlLanguage: string; + + constructor( + public stateService: StateService, + private languageService: LanguageService, + ) { } + + ngOnInit() { + this.network$ = merge(of(''), this.stateService.networkChanged$); + this.urlLanguage = this.languageService.getLanguageForUrl(); + } +} diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index cec162ad9..cd087a3c4 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -7,6 +7,7 @@ import { faFilter, faAngleDown, faAngleUp, faAngleRight, faAngleLeft, faBolt, fa faFileAlt, faRedoAlt, faArrowAltCircleRight, faExternalLinkAlt, faBook, faListUl, faDownload, faQrcode } from '@fortawesome/free-solid-svg-icons'; import { InfiniteScrollModule } from 'ngx-infinite-scroll'; import { MasterPageComponent } from '../components/master-page/master-page.component'; +import { MasterPagePreviewComponent } from '../components/master-page-preview/master-page-preview.component'; import { BisqMasterPageComponent } from '../components/bisq-master-page/bisq-master-page.component'; import { LiquidMasterPageComponent } from '../components/liquid-master-page/liquid-master-page.component'; import { AboutComponent } from '../components/about/about.component'; @@ -44,6 +45,7 @@ import { StartComponent } from '../components/start/start.component'; import { TransactionComponent } from '../components/transaction/transaction.component'; import { TransactionsListComponent } from '../components/transactions-list/transactions-list.component'; import { BlockComponent } from '../components/block/block.component'; +import { BlockPreviewComponent } from '../components/block/block-preview.component'; import { BlockAuditComponent } from '../components/block-audit/block-audit.component'; import { BlockOverviewGraphComponent } from '../components/block-overview-graph/block-overview-graph.component'; import { BlockOverviewTooltipComponent } from '../components/block-overview-tooltip/block-overview-tooltip.component'; @@ -110,11 +112,13 @@ import { TimestampComponent } from './components/timestamp/timestamp.component'; AmountComponent, AboutComponent, MasterPageComponent, + MasterPagePreviewComponent, BisqMasterPageComponent, LiquidMasterPageComponent, StartComponent, TransactionComponent, BlockComponent, + BlockPreviewComponent, BlockAuditComponent, BlockOverviewGraphComponent, BlockOverviewTooltipComponent, @@ -215,6 +219,7 @@ import { TimestampComponent } from './components/timestamp/timestamp.component'; StartComponent, TransactionComponent, BlockComponent, + BlockPreviewComponent, BlockAuditComponent, BlockOverviewGraphComponent, BlockOverviewTooltipComponent, From 4d25c00a9c1a8bd19babef1d57f611491ad9b66b Mon Sep 17 00:00:00 2001 From: nymkappa Date: Sat, 23 Jul 2022 09:57:30 +0200 Subject: [PATCH 003/105] Fix wrong i18n key --- frontend/src/app/components/graphs/graphs.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/components/graphs/graphs.component.html b/frontend/src/app/components/graphs/graphs.component.html index 938d3e817..d6f9694d0 100644 --- a/frontend/src/app/components/graphs/graphs.component.html +++ b/frontend/src/app/components/graphs/graphs.component.html @@ -37,7 +37,7 @@ Lightning nodes per ISP Lightning nodes per country + i18n="lightning.nodes-per-country">Lightning nodes per country Lightning nodes world map Date: Sat, 23 Jul 2022 10:18:36 +0200 Subject: [PATCH 004/105] Make flags clickable --- .../nodes-per-country-chart.component.html | 8 ++++---- .../nodes-per-country-chart.component.scss | 12 ++++++++++++ 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/lightning/nodes-per-country-chart/nodes-per-country-chart.component.html b/frontend/src/app/lightning/nodes-per-country-chart/nodes-per-country-chart.component.html index abc0e306c..d3e8686b0 100644 --- a/frontend/src/app/lightning/nodes-per-country-chart/nodes-per-country-chart.component.html +++ b/frontend/src/app/lightning/nodes-per-country-chart/nodes-per-country-chart.component.html @@ -35,11 +35,11 @@ {{ country.rank }} - + {{ country.name.en }} + {{ country.share }}% {{ country.count }} diff --git a/frontend/src/app/lightning/nodes-per-country-chart/nodes-per-country-chart.component.scss b/frontend/src/app/lightning/nodes-per-country-chart/nodes-per-country-chart.component.scss index c2c94cac0..97a3e76f6 100644 --- a/frontend/src/app/lightning/nodes-per-country-chart/nodes-per-country-chart.component.scss +++ b/frontend/src/app/lightning/nodes-per-country-chart/nodes-per-country-chart.component.scss @@ -79,3 +79,15 @@ max-width: 100px; } } + +a { + text-decoration: none; +} + +a:hover .link { + text-decoration: underline; +} + +.flag { + font-size: 20px; +} From a59cc6cb552d12839404bf2fdecf40b4319b422f Mon Sep 17 00:00:00 2001 From: nymkappa Date: Sat, 23 Jul 2022 14:23:47 +0200 Subject: [PATCH 005/105] Show LN map on the LN dashboard --- .../lightning-dashboard.component.html | 2 ++ .../nodes-channels-map.component.html | 4 ++-- .../nodes-channels-map.component.scss | 22 ++++++++++++++--- .../nodes-channels-map.component.ts | 24 +++++++++---------- 4 files changed, 34 insertions(+), 18 deletions(-) diff --git a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.html b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.html index 056799014..a7a685b89 100644 --- a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.html +++ b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.html @@ -1,3 +1,5 @@ + +
diff --git a/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.html b/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.html index df831608a..e41bbd4bb 100644 --- a/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.html +++ b/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.html @@ -1,6 +1,6 @@ -
+
-
+
Lightning nodes channels world map -
- -
+
+ +
+
+ +
+
- - {{ node.socketsObject[selectedSocketIndex].label }} - - - -
+ + {{ node.socketsObject[selectedSocketIndex].label }} + + + + +
-
+
- + + +
+ +
+

Channels ({{ channelsListStatus === 'open' ? node.channel_active_count : node.channel_closed_count }})

+
+ List  + +  Map +
+
+ + + + -
- -
-

Channels ({{ channelsListStatus === 'open' ? node.channel_active_count : node.channel_closed_count }})

-
- List  - -  Map -
-
- - - -
-
+
\ No newline at end of file diff --git a/frontend/src/app/lightning/node/node.component.ts b/frontend/src/app/lightning/node/node.component.ts index f75ab6c95..c70983b54 100644 --- a/frontend/src/app/lightning/node/node.component.ts +++ b/frontend/src/app/lightning/node/node.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ActivatedRoute, ParamMap } from '@angular/router'; import { Observable } from 'rxjs'; -import { map, switchMap } from 'rxjs/operators'; +import { catchError, map, switchMap } from 'rxjs/operators'; import { SeoService } from 'src/app/services/seo.service'; import { LightningApiService } from '../lightning-api.service'; @@ -19,6 +19,8 @@ export class NodeComponent implements OnInit { qrCodeVisible = false; channelsListMode = 'list'; channelsListStatus: string; + error: Error; + publicKey: string; constructor( private lightningApiService: LightningApiService, @@ -30,6 +32,7 @@ export class NodeComponent implements OnInit { this.node$ = this.activatedRoute.paramMap .pipe( switchMap((params: ParamMap) => { + this.publicKey = params.get('public_key'); return this.lightningApiService.getNode$(params.get('public_key')); }), map((node) => { @@ -56,6 +59,13 @@ export class NodeComponent implements OnInit { node.socketsObject = socketsObject; return node; }), + catchError(err => { + this.error = err; + return [{ + alias: this.publicKey, + public_key: this.publicKey, + }]; + }) ); } From 311acc016854e084c6fd8982df9ba5f12a0a3d7d Mon Sep 17 00:00:00 2001 From: nymkappa Date: Sun, 24 Jul 2022 15:08:48 +0200 Subject: [PATCH 015/105] Remove duplicated nodes from the world map --- .../nodes-channels-map.component.ts | 34 +++++++++++-------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts b/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts index fd9956d38..7963cf544 100644 --- a/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts +++ b/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts @@ -57,20 +57,26 @@ export class NodesChannelsMap implements OnInit, OnDestroy { const channelsLoc = []; const nodes = []; + const nodesPubkeys = {}; for (const channel of data[1]) { channelsLoc.push([[channel[2], channel[3]], [channel[6], channel[7]]]); - nodes.push({ - publicKey: channel[0], - name: channel[1], - value: [channel[2], channel[3]], - }); - nodes.push({ - publicKey: channel[4], - name: channel[5], - value: [channel[6], channel[7]], - }); + if (!nodesPubkeys[channel[0]]) { + nodes.push({ + publicKey: channel[0], + name: channel[1], + value: [channel[2], channel[3]], + }); + nodesPubkeys[channel[0]] = true; + } + if (!nodesPubkeys[channel[4]]) { + nodes.push({ + publicKey: channel[4], + name: channel[5], + value: [channel[6], channel[7]], + }); + nodesPubkeys[channel[4]] = true; + } } - this.prepareChartOptions(nodes, channelsLoc); })); }) @@ -100,7 +106,7 @@ export class NodesChannelsMap implements OnInit, OnDestroy { postEffect: { enable: true, bloom: { - intensity: this.style === 'nodepage' ? 0.1 : 0.01, + intensity: 0.1, } }, viewControl: { @@ -113,10 +119,10 @@ export class NodesChannelsMap implements OnInit, OnDestroy { zoomSensivity: 0.5, }, itemStyle: { - color: '#FFFFFF', + color: 'white', opacity: 0.02, borderWidth: 1, - borderColor: '#00000050', + borderColor: 'black', }, regionHeight: 0.01, }, From ed36554f768074aa2231008a1e2754679ae8d03d Mon Sep 17 00:00:00 2001 From: Antoni Spaanderman <56turtle56@gmail.com> Date: Sun, 24 Jul 2022 18:44:27 +0200 Subject: [PATCH 016/105] move parseMultisigScript to bitcoin.util.ts --- frontend/src/app/bitcoin.utils.ts | 40 ++++++++++++++++++- .../address-labels.component.ts | 39 +----------------- 2 files changed, 40 insertions(+), 39 deletions(-) diff --git a/frontend/src/app/bitcoin.utils.ts b/frontend/src/app/bitcoin.utils.ts index e5cdde87f..d1f0d6553 100644 --- a/frontend/src/app/bitcoin.utils.ts +++ b/frontend/src/app/bitcoin.utils.ts @@ -1,5 +1,4 @@ import { Transaction, Vin } from './interfaces/electrs.interface'; -import { parseMultisigScript } from './components/address-labels/address-labels.component'; const P2SH_P2WPKH_COST = 21 * 4; // the WU cost for the non-witness part of P2SH-P2WPKH const P2SH_P2WSH_COST = 35 * 4; // the WU cost for the non-witness part of P2SH-P2WSH @@ -114,6 +113,45 @@ export function calcSegwitFeeGains(tx: Transaction) { }; } +/** extracts m and n from a multisig script (asm), returns nothing if it is not a multisig script */ +export function parseMultisigScript(script: string): void | { m: number, n: number } { + if (!script) { + return; + } + const ops = script.split(' '); + if (ops.length < 3 || ops.pop() !== 'OP_CHECKMULTISIG') { + return; + } + const opN = ops.pop(); + if (!opN.startsWith('OP_PUSHNUM_')) { + return; + } + const n = parseInt(opN.match(/[0-9]+/)[0], 10); + if (ops.length < n * 2 + 1) { + return; + } + // pop n public keys + for (let i = 0; i < n; i++) { + if (!/^0((2|3)\w{64}|4\w{128})$/.test(ops.pop())) { + return; + } + if (!/^OP_PUSHBYTES_(33|65)$/.test(ops.pop())) { + return; + } + } + const opM = ops.pop(); + if (!opM.startsWith('OP_PUSHNUM_')) { + return; + } + const m = parseInt(opM.match(/[0-9]+/)[0], 10); + + if (ops.length) { + return; + } + + return { m, n }; +} + // https://github.com/shesek/move-decimal-point export function moveDec(num: number, n: number) { let frac, int, neg, ref; diff --git a/frontend/src/app/components/address-labels/address-labels.component.ts b/frontend/src/app/components/address-labels/address-labels.component.ts index 6dc10f6e9..331114ff4 100644 --- a/frontend/src/app/components/address-labels/address-labels.component.ts +++ b/frontend/src/app/components/address-labels/address-labels.component.ts @@ -1,6 +1,7 @@ import { Component, ChangeDetectionStrategy, Input, OnChanges } from '@angular/core'; import { Vin, Vout } from '../../interfaces/electrs.interface'; import { StateService } from 'src/app/services/state.service'; +import { parseMultisigScript } from 'src/app/bitcoin.utils'; @Component({ selector: 'app-address-labels', @@ -109,41 +110,3 @@ export class AddressLabelsComponent implements OnChanges { this.detectMultisig(this.vout.scriptpubkey_asm); } } - -export function parseMultisigScript(script: string): void | { m: number, n: number } { - if (!script) { - return; - } - const ops = script.split(' '); - if (ops.length < 3 || ops.pop() !== 'OP_CHECKMULTISIG') { - return; - } - const opN = ops.pop(); - if (!opN.startsWith('OP_PUSHNUM_')) { - return; - } - const n = parseInt(opN.match(/[0-9]+/)[0], 10); - if (ops.length < n * 2 + 1) { - return; - } - // pop n public keys - for (let i = 0; i < n; i++) { - if (!/^0((2|3)\w{64}|4\w{128})$/.test(ops.pop())) { - return; - } - if (!/^OP_PUSHBYTES_(33|65)$/.test(ops.pop())) { - return; - } - } - const opM = ops.pop(); - if (!opM.startsWith('OP_PUSHNUM_')) { - return; - } - const m = parseInt(opM.match(/[0-9]+/)[0], 10); - - if (ops.length) { - return; - } - - return { m, n }; -} From 8d52f08de5702acd1a98611ba42cfa7fce346849 Mon Sep 17 00:00:00 2001 From: Antoni Spaanderman <49868160+antonilol@users.noreply.github.com> Date: Sun, 24 Jul 2022 18:44:53 +0200 Subject: [PATCH 017/105] Update frontend/src/app/bitcoin.utils.ts triple equals Co-authored-by: softsimon --- frontend/src/app/bitcoin.utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/bitcoin.utils.ts b/frontend/src/app/bitcoin.utils.ts index d1f0d6553..b2ea1d7a9 100644 --- a/frontend/src/app/bitcoin.utils.ts +++ b/frontend/src/app/bitcoin.utils.ts @@ -64,7 +64,7 @@ export function calcSegwitFeeGains(tx: Transaction) { } if (isP2tr) { - if (vin.witness.length == 1) { + if (vin.witness.length === 1) { // key path spend // we don't know if this was a multisig or single sig (the goal of taproot :)), // so calculate fee savings by comparing to the cheapest single sig input type: P2WPKH and say "saved at least ...%" From 1d2d6e5945547797453f38ae252b4772f4225ead Mon Sep 17 00:00:00 2001 From: Antoni Spaanderman <56turtle56@gmail.com> Date: Sun, 24 Jul 2022 19:39:13 +0200 Subject: [PATCH 018/105] add taproot badge with only privacy tooltip if no fees can be saved --- .../components/tx-features/tx-features.component.html | 9 ++++++--- .../app/components/tx-features/tx-features.component.ts | 2 ++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/components/tx-features/tx-features.component.html b/frontend/src/app/components/tx-features/tx-features.component.html index 49a1fa90f..16dbb66f4 100644 --- a/frontend/src/app/components/tx-features/tx-features.component.html +++ b/frontend/src/app/components/tx-features/tx-features.component.html @@ -6,11 +6,14 @@ -Taproot +Taproot - Taproot + Taproot - Taproot + Taproot + + Taproot + diff --git a/frontend/src/app/components/tx-features/tx-features.component.ts b/frontend/src/app/components/tx-features/tx-features.component.ts index ce25bb097..f73d8ae8a 100644 --- a/frontend/src/app/components/tx-features/tx-features.component.ts +++ b/frontend/src/app/components/tx-features/tx-features.component.ts @@ -19,6 +19,7 @@ export class TxFeaturesComponent implements OnChanges { realizedTaprootGains: 0 }; isRbfTransaction: boolean; + isTaproot: boolean; constructor() { } @@ -28,5 +29,6 @@ export class TxFeaturesComponent implements OnChanges { } this.segwitGains = calcSegwitFeeGains(this.tx); this.isRbfTransaction = this.tx.vin.some((v) => v.sequence < 0xfffffffe); + this.isTaproot = this.tx.vin.some((v) => v.prevout && v.prevout.scriptpubkey_type === 'v1_p2tr'); } } From f08c0faf0fc15c91779920b856e87e995e5bdbd4 Mon Sep 17 00:00:00 2001 From: softsimon Date: Sun, 24 Jul 2022 23:01:31 +0200 Subject: [PATCH 019/105] Fix for mempool logo jumping with various sizes of enterprise logo --- .../components/master-page/master-page.component.html | 2 +- .../components/master-page/master-page.component.scss | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/master-page/master-page.component.html b/frontend/src/app/components/master-page/master-page.component.html index 7be41f233..1458e762b 100644 --- a/frontend/src/app/components/master-page/master-page.component.html +++ b/frontend/src/app/components/master-page/master-page.component.html @@ -9,7 +9,7 @@ - +
Offline
Reconnecting...
diff --git a/frontend/src/app/components/master-page/master-page.component.scss b/frontend/src/app/components/master-page/master-page.component.scss index f251215fb..0f7e56828 100644 --- a/frontend/src/app/components/master-page/master-page.component.scss +++ b/frontend/src/app/components/master-page/master-page.component.scss @@ -78,6 +78,7 @@ li.nav-item { .navbar-brand { position: relative; + height: 65px; } nav { @@ -86,7 +87,7 @@ nav { .connection-badge { position: absolute; - top: 13px; + top: 22px; width: 100%; } @@ -150,6 +151,7 @@ nav { width: 140px; margin-right: 15px; text-align: center; + align-self: center; } .logo-holder { @@ -161,3 +163,9 @@ nav { flex-direction: row; display: flex; } + +app-svg-images { + align-self: center; + width: 140px; + height: 35px; +} From 6e77275d021b1258330a212081004bf3c0334ce5 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 24 Jul 2022 21:16:57 +0000 Subject: [PATCH 020/105] Add Open Graph link unfurler service --- unfurler/.editorconfig | 17 + unfurler/.eslintignore | 2 + unfurler/.eslintrc | 33 + unfurler/.gitignore | 38 + unfurler/.prettierignore | 2 + unfurler/.prettierrc | 6 + unfurler/README.md | 91 + unfurler/config.sample.json | 14 + unfurler/package-lock.json | 4391 ++++++++++++++++++++++++++++++++ unfurler/package.json | 33 + unfurler/puppeteer.config.json | 46 + unfurler/src/config.ts | 55 + unfurler/src/index.ts | 125 + unfurler/tsconfig.json | 24 + 14 files changed, 4877 insertions(+) create mode 100644 unfurler/.editorconfig create mode 100644 unfurler/.eslintignore create mode 100644 unfurler/.eslintrc create mode 100644 unfurler/.gitignore create mode 100644 unfurler/.prettierignore create mode 100644 unfurler/.prettierrc create mode 100644 unfurler/README.md create mode 100644 unfurler/config.sample.json create mode 100644 unfurler/package-lock.json create mode 100644 unfurler/package.json create mode 100644 unfurler/puppeteer.config.json create mode 100644 unfurler/src/config.ts create mode 100644 unfurler/src/index.ts create mode 100644 unfurler/tsconfig.json diff --git a/unfurler/.editorconfig b/unfurler/.editorconfig new file mode 100644 index 000000000..9787413cd --- /dev/null +++ b/unfurler/.editorconfig @@ -0,0 +1,17 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single + +[*.md] +max_line_length = off +trim_trailing_whitespace = false + diff --git a/unfurler/.eslintignore b/unfurler/.eslintignore new file mode 100644 index 000000000..76add878f --- /dev/null +++ b/unfurler/.eslintignore @@ -0,0 +1,2 @@ +node_modules +dist \ No newline at end of file diff --git a/unfurler/.eslintrc b/unfurler/.eslintrc new file mode 100644 index 000000000..d8f453c51 --- /dev/null +++ b/unfurler/.eslintrc @@ -0,0 +1,33 @@ +{ + "root": true, + "parser": "@typescript-eslint/parser", + "plugins": [ + "@typescript-eslint" + ], + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended", + "prettier" + ], + "rules": { + "@typescript-eslint/ban-ts-comment": 1, + "@typescript-eslint/ban-types": 1, + "@typescript-eslint/no-empty-function": 1, + "@typescript-eslint/no-explicit-any": 1, + "@typescript-eslint/no-inferrable-types": 1, + "@typescript-eslint/no-namespace": 1, + "@typescript-eslint/no-this-alias": 1, + "@typescript-eslint/no-var-requires": 1, + "no-console": 1, + "no-constant-condition": 1, + "no-dupe-else-if": 1, + "no-empty": 1, + "no-prototype-builtins": 1, + "no-self-assign": 1, + "no-useless-catch": 1, + "no-var": 1, + "prefer-const": 1, + "prefer-rest-params": 1 + } +} diff --git a/unfurler/.gitignore b/unfurler/.gitignore new file mode 100644 index 000000000..6ef7a8dd4 --- /dev/null +++ b/unfurler/.gitignore @@ -0,0 +1,38 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# production config and external assets +config.json + +# compiled output +/dist +/tmp + +# dependencies +/node_modules + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# misc +/.sass-cache +/connect.lock +/libpeerconnection.log +npm-debug.log +testem.log +/typings + +#System Files +.DS_Store +Thumbs.db diff --git a/unfurler/.prettierignore b/unfurler/.prettierignore new file mode 100644 index 000000000..d5f19d89b --- /dev/null +++ b/unfurler/.prettierignore @@ -0,0 +1,2 @@ +node_modules +package-lock.json diff --git a/unfurler/.prettierrc b/unfurler/.prettierrc new file mode 100644 index 000000000..b8039f843 --- /dev/null +++ b/unfurler/.prettierrc @@ -0,0 +1,6 @@ +{ + "endOfLine": "lf", + "printWidth": 80, + "tabWidth": 2, + "trailingComma": "es5" +} diff --git a/unfurler/README.md b/unfurler/README.md new file mode 100644 index 000000000..71b4ae2fc --- /dev/null +++ b/unfurler/README.md @@ -0,0 +1,91 @@ +# Mempool Link Unfurler Service + +This is a standalone nodejs service which implements the [Open Graph protocol](https://ogp.me/) for Mempool instances. It performs two main tasks: + +1. Serving Open Graph html meta tags to social media link crawler bots. +2. Rendering link preview images for social media sharing. + +Some additional server configuration is required to properly route requests (see section 4 below). + +## Setup + +### 1. Clone Mempool Repository + +Get the latest Mempool code: + +``` +git clone https://github.com/mempool/mempool +cd mempool +``` + +Check out the latest release: + +``` +latestrelease=$(curl -s https://api.github.com/repos/mempool/mempool/releases/latest|grep tag_name|head -1|cut -d '"' -f4) +git checkout $latestrelease +``` + +### 2. Prepare the Mempool Unfurler + +#### Install + +Install dependencies with `npm` and build the backend: + +``` +cd unfurler +npm install +``` + +The npm install may fail if your system does not support automatic installation of Chromium for Puppeteer. In that case, manually install Puppeteer without Chromium first: +``` +PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true npm install puppeteer +npm install +``` + +#### Configure + +In the `unfurler` folder, make a copy of the sample config file: + +``` +cp config.sample.json config.json +``` + +Edit `config.json` as needed: + +| variable | usage | +|---|---| +| SERVER.HOST | the host where **this** service will be served | +| SERVER.HTTP_PORT | the port on which **this** service should run | +| MEMPOOL.HTTP_HOST | the host where **the Mempool frontend** is being served | +| MEMPOOL.HTTP_PORT | the port on which **the Mempool frontend** is running (or `null`) | +| PUPPETEER.CLUSTER_SIZE | the maximum number of Chromium browser instances to run in parallel, for rendering link previews | +| PUPPETEER.EXEC_PATH | (optional) an absolute path to the Chromium browser executable, e.g. `/usr/local/bin/chrome`. Only required when using a manual installation of Chromium | + +#### Build + +``` +npm run build +``` + +### 3. Run the Mempool Unfurler + +``` +npm run start +``` + +### 4. Server configuration + +To enable social media link previews, the system serving the Mempool frontend should detect requests from social media crawler bots and proxy those requests to this service instead. + +Precise implementation is left as an exercise to the reader, but the following snippet may be of some help for Nginx users: +```Nginx +map $http_user_agent $crawler { + default 0; + ~*facebookexternalhit 1; + ~*twitterbot 1; + ~*slackbot 1; + ~*redditbot 1; + ~*linkedinbot 1; + ~*pinterestbot 1; +} +``` diff --git a/unfurler/config.sample.json b/unfurler/config.sample.json new file mode 100644 index 000000000..02f2b78f0 --- /dev/null +++ b/unfurler/config.sample.json @@ -0,0 +1,14 @@ +{ + "SERVER": { + "HOST": "http://localhost", + "HTTP_PORT": 4201 + }, + "MEMPOOL": { + "HTTP_HOST": "http://localhost", + "HTTP_PORT": 4200 + }, + "PUPPETEER": { + "CLUSTER_SIZE": 2, + "EXEC_PATH": "/usr/local/bin/chrome" // optional + } +} diff --git a/unfurler/package-lock.json b/unfurler/package-lock.json new file mode 100644 index 000000000..3da33c69f --- /dev/null +++ b/unfurler/package-lock.json @@ -0,0 +1,4391 @@ +{ + "name": "mempool-unfurl", + "version": "0.0.1", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "mempool-unfurl", + "version": "0.0.1", + "dependencies": { + "@types/node": "^16.11.41", + "express": "^4.18.0", + "puppeteer": "^15.3.2", + "puppeteer-cluster": "^0.23.0", + "typescript": "~4.7.4" + }, + "devDependencies": { + "@typescript-eslint/eslint-plugin": "^5.30.5", + "@typescript-eslint/parser": "^5.30.5", + "eslint": "^8.19.0", + "eslint-config-prettier": "^8.5.0", + "prettier": "^2.7.1" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz", + "integrity": "sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.3.2", + "globals": "^13.15.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", + "integrity": "sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "dev": true + }, + "node_modules/@types/node": { + "version": "16.11.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.43.tgz", + "integrity": "sha512-GqWykok+3uocgfAJM8imbozrqLnPyTrpFlrryURQlw1EesPUCx5XxTiucWDSFF9/NUEXDuD4bnvHm8xfVGWTpQ==" + }, + "node_modules/@types/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.30.6.tgz", + "integrity": "sha512-J4zYMIhgrx4MgnZrSDD7sEnQp7FmhKNOaqaOpaoQ/SfdMfRB/0yvK74hTnvH+VQxndZynqs5/Hn4t+2/j9bADg==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.30.6", + "@typescript-eslint/type-utils": "5.30.6", + "@typescript-eslint/utils": "5.30.6", + "debug": "^4.3.4", + "functional-red-black-tree": "^1.0.1", + "ignore": "^5.2.0", + "regexpp": "^3.2.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.30.6.tgz", + "integrity": "sha512-gfF9lZjT0p2ZSdxO70Xbw8w9sPPJGfAdjK7WikEjB3fcUI/yr9maUVEdqigBjKincUYNKOmf7QBMiTf719kbrA==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.30.6", + "@typescript-eslint/types": "5.30.6", + "@typescript-eslint/typescript-estree": "5.30.6", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.30.6.tgz", + "integrity": "sha512-Hkq5PhLgtVoW1obkqYH0i4iELctEKixkhWLPTYs55doGUKCASvkjOXOd/pisVeLdO24ZX9D6yymJ/twqpJiG3g==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.30.6", + "@typescript-eslint/visitor-keys": "5.30.6" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.30.6.tgz", + "integrity": "sha512-GFVVzs2j0QPpM+NTDMXtNmJKlF842lkZKDSanIxf+ArJsGeZUIaeT4jGg+gAgHt7AcQSFwW7htzF/rbAh2jaVA==", + "dev": true, + "dependencies": { + "@typescript-eslint/utils": "5.30.6", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.30.6.tgz", + "integrity": "sha512-HdnP8HioL1F7CwVmT4RaaMX57RrfqsOMclZc08wGMiDYJBsLGBM7JwXM4cZJmbWLzIR/pXg1kkrBBVpxTOwfUg==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.30.6.tgz", + "integrity": "sha512-Z7TgPoeYUm06smfEfYF0RBkpF8csMyVnqQbLYiGgmUSTaSXTP57bt8f0UFXstbGxKIreTwQCujtaH0LY9w9B+A==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.30.6", + "@typescript-eslint/visitor-keys": "5.30.6", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.30.6.tgz", + "integrity": "sha512-xFBLc/esUbLOJLk9jKv0E9gD/OH966M40aY9jJ8GiqpSkP2xOV908cokJqqhVd85WoIvHVHYXxSFE4cCSDzVvA==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "@typescript-eslint/scope-manager": "5.30.6", + "@typescript-eslint/types": "5.30.6", + "@typescript-eslint/typescript-estree": "5.30.6", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.30.6.tgz", + "integrity": "sha512-41OiCjdL2mCaSDi2SvYbzFLlqqlm5v1ZW9Ym55wXKL/Rx6OOB1IbuFGo71Fj6Xy90gJDFTlgOS+vbmtGHPTQQA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.30.6", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", + "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", + "integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.10.3", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "engines": { + "node": "*" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/cross-fetch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", + "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "dependencies": { + "node-fetch": "2.6.7" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1011705", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1011705.tgz", + "integrity": "sha512-OKvTvu9n3swmgYshvsyVHYX0+aPzCoYUnyXUacfQMmFtBtBKewV/gT4I9jkAbpTqtTi2E4S9MXLlvzBDUlqg0Q==" + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.19.0.tgz", + "integrity": "sha512-SXOPj3x9VKvPe81TjjUJCYlV4oJjQw68Uek+AM0X4p+33dj2HY5bpTZOgnQHcG2eAm1mtCU9uNMnJi7exU/kYw==", + "dev": true, + "dependencies": { + "@eslint/eslintrc": "^1.3.0", + "@humanwhocodes/config-array": "^0.9.2", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.1.1", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.2", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^6.0.1", + "globals": "^13.15.0", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "regexpp": "^3.2.0", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz", + "integrity": "sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==", + "dev": true, + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", + "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/espree": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.2.tgz", + "integrity": "sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA==", + "dev": true, + "dependencies": { + "acorn": "^8.7.1", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.1.tgz", + "integrity": "sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.0", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.10.3", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", + "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.6.tgz", + "integrity": "sha512-0sQoMh9s0BYsm+12Huy/rkKxVu4R1+r96YX5cG44rHV0pQ6iC3Q+mkoMFaGWObMFYQxCVT+ssG1ksneA2MI9KQ==", + "dev": true + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "dev": true + }, + "node_modules/get-intrinsic": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz", + "integrity": "sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==", + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.16.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.16.0.tgz", + "integrity": "sha512-A1lrQfpNF+McdPOnnFqY3kSN0AFTy485bTi1bkLk4mVPODIUEcSfhHgRqA+QdXPksrSTTztYXx37NFV+GpGk3Q==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", + "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/puppeteer": { + "version": "15.3.2", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-15.3.2.tgz", + "integrity": "sha512-6z4fTHCHTpG3Yu7zqP0mLfCmkNkgw5KSUfLAwuBabz9Pkqoe0Z08hqUx5GNxhhMgEo4YVOSPBshePA6zliznWQ==", + "hasInstallScript": true, + "dependencies": { + "cross-fetch": "3.1.5", + "debug": "4.3.4", + "devtools-protocol": "0.0.1011705", + "extract-zip": "2.0.1", + "https-proxy-agent": "5.0.1", + "pkg-dir": "4.2.0", + "progress": "2.0.3", + "proxy-from-env": "1.1.0", + "rimraf": "3.0.2", + "tar-fs": "2.1.1", + "unbzip2-stream": "1.4.3", + "ws": "8.8.0" + }, + "engines": { + "node": ">=14.1.0" + } + }, + "node_modules/puppeteer-cluster": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/puppeteer-cluster/-/puppeteer-cluster-0.23.0.tgz", + "integrity": "sha512-108terIWDzPrQopmoYSPd5yDoy3FGJ2dNnoGMkGYPs6xtkdhgaECwpfZkzaRToMQPZibUOz0/dSSGgPEdXEhkQ==", + "dependencies": { + "debug": "^4.3.3" + }, + "peerDependencies": { + "puppeteer": ">=1.5.0" + } + }, + "node_modules/qs": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", + "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", + "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/v8-compile-cache": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "dev": true + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/ws": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.8.0.tgz", + "integrity": "sha512-JDAgSYQ1ksuwqfChJusw1LSJ8BizJ2e/vVu5Lxjq3YvNJNlROv1ui4i+c/kUUrPheBvQl4c5UbERhTwKa6QBJQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + } + }, + "dependencies": { + "@eslint/eslintrc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.0.tgz", + "integrity": "sha512-UWW0TMTmk2d7hLcWD1/e2g5HDM/HQ3csaLSqXCfqwh4uNDuNqlaKWXmEsL4Cs41Z0KnILNvwbHAah3C2yt06kw==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.3.2", + "globals": "^13.15.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + } + }, + "@humanwhocodes/config-array": { + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", + "integrity": "sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + } + }, + "@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@types/json-schema": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "dev": true + }, + "@types/node": { + "version": "16.11.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.43.tgz", + "integrity": "sha512-GqWykok+3uocgfAJM8imbozrqLnPyTrpFlrryURQlw1EesPUCx5XxTiucWDSFF9/NUEXDuD4bnvHm8xfVGWTpQ==" + }, + "@types/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==", + "optional": true, + "requires": { + "@types/node": "*" + } + }, + "@typescript-eslint/eslint-plugin": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.30.6.tgz", + "integrity": "sha512-J4zYMIhgrx4MgnZrSDD7sEnQp7FmhKNOaqaOpaoQ/SfdMfRB/0yvK74hTnvH+VQxndZynqs5/Hn4t+2/j9bADg==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "5.30.6", + "@typescript-eslint/type-utils": "5.30.6", + "@typescript-eslint/utils": "5.30.6", + "debug": "^4.3.4", + "functional-red-black-tree": "^1.0.1", + "ignore": "^5.2.0", + "regexpp": "^3.2.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/parser": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.30.6.tgz", + "integrity": "sha512-gfF9lZjT0p2ZSdxO70Xbw8w9sPPJGfAdjK7WikEjB3fcUI/yr9maUVEdqigBjKincUYNKOmf7QBMiTf719kbrA==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "5.30.6", + "@typescript-eslint/types": "5.30.6", + "@typescript-eslint/typescript-estree": "5.30.6", + "debug": "^4.3.4" + } + }, + "@typescript-eslint/scope-manager": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.30.6.tgz", + "integrity": "sha512-Hkq5PhLgtVoW1obkqYH0i4iELctEKixkhWLPTYs55doGUKCASvkjOXOd/pisVeLdO24ZX9D6yymJ/twqpJiG3g==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.30.6", + "@typescript-eslint/visitor-keys": "5.30.6" + } + }, + "@typescript-eslint/type-utils": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.30.6.tgz", + "integrity": "sha512-GFVVzs2j0QPpM+NTDMXtNmJKlF842lkZKDSanIxf+ArJsGeZUIaeT4jGg+gAgHt7AcQSFwW7htzF/rbAh2jaVA==", + "dev": true, + "requires": { + "@typescript-eslint/utils": "5.30.6", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/types": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.30.6.tgz", + "integrity": "sha512-HdnP8HioL1F7CwVmT4RaaMX57RrfqsOMclZc08wGMiDYJBsLGBM7JwXM4cZJmbWLzIR/pXg1kkrBBVpxTOwfUg==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.30.6.tgz", + "integrity": "sha512-Z7TgPoeYUm06smfEfYF0RBkpF8csMyVnqQbLYiGgmUSTaSXTP57bt8f0UFXstbGxKIreTwQCujtaH0LY9w9B+A==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.30.6", + "@typescript-eslint/visitor-keys": "5.30.6", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/utils": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.30.6.tgz", + "integrity": "sha512-xFBLc/esUbLOJLk9jKv0E9gD/OH966M40aY9jJ8GiqpSkP2xOV908cokJqqhVd85WoIvHVHYXxSFE4cCSDzVvA==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "@typescript-eslint/scope-manager": "5.30.6", + "@typescript-eslint/types": "5.30.6", + "@typescript-eslint/typescript-estree": "5.30.6", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "5.30.6", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.30.6.tgz", + "integrity": "sha512-41OiCjdL2mCaSDi2SvYbzFLlqqlm5v1ZW9Ym55wXKL/Rx6OOB1IbuFGo71Fj6Xy90gJDFTlgOS+vbmtGHPTQQA==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.30.6", + "eslint-visitor-keys": "^3.3.0" + } + }, + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + } + }, + "acorn": { + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz", + "integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==", + "dev": true + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "requires": {} + }, + "agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "requires": { + "debug": "4" + } + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "body-parser": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", + "integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==", + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.10.3", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==" + }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "requires": { + "safe-buffer": "5.2.1" + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, + "cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==" + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "cross-fetch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", + "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "requires": { + "node-fetch": "2.6.7" + } + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" + }, + "devtools-protocol": { + "version": "0.0.1011705", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1011705.tgz", + "integrity": "sha512-OKvTvu9n3swmgYshvsyVHYX0+aPzCoYUnyXUacfQMmFtBtBKewV/gT4I9jkAbpTqtTi2E4S9MXLlvzBDUlqg0Q==" + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "requires": { + "once": "^1.4.0" + } + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "eslint": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.19.0.tgz", + "integrity": "sha512-SXOPj3x9VKvPe81TjjUJCYlV4oJjQw68Uek+AM0X4p+33dj2HY5bpTZOgnQHcG2eAm1mtCU9uNMnJi7exU/kYw==", + "dev": true, + "requires": { + "@eslint/eslintrc": "^1.3.0", + "@humanwhocodes/config-array": "^0.9.2", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.1.1", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.2", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "functional-red-black-tree": "^1.0.1", + "glob-parent": "^6.0.1", + "globals": "^13.15.0", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "regexpp": "^3.2.0", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0", + "v8-compile-cache": "^2.0.3" + }, + "dependencies": { + "eslint-scope": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", + "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + } + } + }, + "eslint-config-prettier": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz", + "integrity": "sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==", + "dev": true, + "requires": {} + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^2.0.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true + } + } + }, + "eslint-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "dev": true + }, + "espree": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.3.2.tgz", + "integrity": "sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA==", + "dev": true, + "requires": { + "acorn": "^8.7.1", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.3.0" + } + }, + "esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + } + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + }, + "express": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.1.tgz", + "integrity": "sha512-zZBcOX9TfehHQhtupq57OF8lFZ3UZi08Y97dwFCkD8p9d/d2Y3M+ykKcwaMDEL+4qyUolgBDX6AblpR3fL212Q==", + "requires": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.0", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.10.3", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, + "extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "requires": { + "@types/yauzl": "^2.9.1", + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + } + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-glob": { + "version": "3.2.11", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.11.tgz", + "integrity": "sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "requires": { + "pend": "~1.2.0" + } + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.6.tgz", + "integrity": "sha512-0sQoMh9s0BYsm+12Huy/rkKxVu4R1+r96YX5cG44rHV0pQ6iC3Q+mkoMFaGWObMFYQxCVT+ssG1ksneA2MI9KQ==", + "dev": true + }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" + }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" + }, + "functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "dev": true + }, + "get-intrinsic": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.2.tgz", + "integrity": "sha512-Jfm3OyCxHh9DJyc28qGk+JmfkpO41A4XkneDSujN9MDXrm4oDKdHvndhZ2dN94+ERNfkYJWDclW6k2L/ZGHjXA==", + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + } + }, + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "requires": { + "pump": "^3.0.0" + } + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "globals": { + "version": "13.16.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.16.0.tgz", + "integrity": "sha512-A1lrQfpNF+McdPOnnFqY3kSN0AFTy485bTi1bkLk4mVPODIUEcSfhHgRqA+QdXPksrSTTztYXx37NFV+GpGk3Q==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==" + }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, + "https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "requires": { + "agent-base": "6", + "debug": "4" + } + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "ignore": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "requires": { + "p-locate": "^4.1.0" + } + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==" + }, + "merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==" + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" + }, + "node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "requires": { + "whatwg-url": "^5.0.0" + } + }, + "object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==" + }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "requires": { + "ee-first": "1.1.1" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "requires": { + "wrappy": "1" + } + }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "requires": { + "find-up": "^4.0.0" + } + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "prettier": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", + "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", + "dev": true + }, + "progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==" + }, + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + } + }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "puppeteer": { + "version": "15.3.2", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-15.3.2.tgz", + "integrity": "sha512-6z4fTHCHTpG3Yu7zqP0mLfCmkNkgw5KSUfLAwuBabz9Pkqoe0Z08hqUx5GNxhhMgEo4YVOSPBshePA6zliznWQ==", + "requires": { + "cross-fetch": "3.1.5", + "debug": "4.3.4", + "devtools-protocol": "0.0.1011705", + "extract-zip": "2.0.1", + "https-proxy-agent": "5.0.1", + "pkg-dir": "4.2.0", + "progress": "2.0.3", + "proxy-from-env": "1.1.0", + "rimraf": "3.0.2", + "tar-fs": "2.1.1", + "unbzip2-stream": "1.4.3", + "ws": "8.8.0" + } + }, + "puppeteer-cluster": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/puppeteer-cluster/-/puppeteer-cluster-0.23.0.tgz", + "integrity": "sha512-108terIWDzPrQopmoYSPd5yDoy3FGJ2dNnoGMkGYPs6xtkdhgaECwpfZkzaRToMQPZibUOz0/dSSGgPEdXEhkQ==", + "requires": { + "debug": "^4.3.3" + } + }, + "qs": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", + "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", + "requires": { + "side-channel": "^1.0.4" + } + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "requires": { + "glob": "^7.1.3" + } + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "semver": { + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", + "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "requires": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + } + } + }, + "serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "requires": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + } + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "requires": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "requires": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" + }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typescript": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", + "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==" + }, + "unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "requires": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" + }, + "v8-compile-cache": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", + "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", + "dev": true + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "ws": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.8.0.tgz", + "integrity": "sha512-JDAgSYQ1ksuwqfChJusw1LSJ8BizJ2e/vVu5Lxjq3YvNJNlROv1ui4i+c/kUUrPheBvQl4c5UbERhTwKa6QBJQ==", + "requires": {} + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "requires": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + } + } +} diff --git a/unfurler/package.json b/unfurler/package.json new file mode 100644 index 000000000..432c604e3 --- /dev/null +++ b/unfurler/package.json @@ -0,0 +1,33 @@ +{ + "name": "mempool-unfurl", + "version": "0.0.1", + "description": "Renderer for mempool open graph link preview images", + "repository": { + "type": "git", + "url": "git+https://github.com/mononaut/mempool-unfurl" + }, + "main": "index.ts", + "scripts": { + "tsc": "./node_modules/typescript/bin/tsc", + "build": "npm run tsc", + "start": "node --max-old-space-size=2048 dist/index.js", + "start-production": "node --max-old-space-size=4096 dist/index.js", + "lint": "./node_modules/.bin/eslint . --ext .ts", + "lint:fix": "./node_modules/.bin/eslint . --ext .ts --fix", + "prettier": "./node_modules/.bin/prettier --write \"src/**/*.{js,ts}\"" + }, + "dependencies": { + "@types/node": "^16.11.41", + "express": "^4.18.0", + "puppeteer": "^15.3.2", + "puppeteer-cluster": "^0.23.0", + "typescript": "~4.7.4" + }, + "devDependencies": { + "@typescript-eslint/eslint-plugin": "^5.30.5", + "@typescript-eslint/parser": "^5.30.5", + "eslint": "^8.19.0", + "eslint-config-prettier": "^8.5.0", + "prettier": "^2.7.1" + } +} diff --git a/unfurler/puppeteer.config.json b/unfurler/puppeteer.config.json new file mode 100644 index 000000000..346deb1b7 --- /dev/null +++ b/unfurler/puppeteer.config.json @@ -0,0 +1,46 @@ +{ + "headless": true, + "defaultViewport": { + "width": 1024, + "height": 512 + }, + "args": [ + "--window-size=1024,512", + "--autoplay-policy=user-gesture-required", + "--disable-background-networking", + "--disable-background-timer-throttling", + "--disable-backgrounding-occluded-windows", + "--disable-breakpad", + "--disable-client-side-phishing-detection", + "--disable-component-update", + "--disable-default-apps", + "--disable-dev-shm-usage", + "--disable-domain-reliability", + "--disable-extensions", + "--disable-features=AudioServiceOutOfProcess", + "--disable-hang-monitor", + "--disable-ipc-flooding-protection", + "--disable-notifications", + "--disable-offer-store-unmasked-wallet-cards", + "--disable-popup-blocking", + "--disable-print-preview", + "--disable-prompt-on-repost", + "--disable-renderer-backgrounding", + "--disable-setuid-sandbox", + "--disable-speech-api", + "--disable-sync", + "--hide-scrollbars", + "--metrics-recording-only", + "--mute-audio", + "--no-default-browser-check", + "--no-first-run", + "--no-pings", + "--no-sandbox", + "--no-zygote", + "--password-store=basic", + "--use-mock-keychain", + "--ignore-gpu-blacklist", + "--ignore-gpu-blocklist", + "--use-gl=swiftshader" + ] +} diff --git a/unfurler/src/config.ts b/unfurler/src/config.ts new file mode 100644 index 000000000..1df60ce98 --- /dev/null +++ b/unfurler/src/config.ts @@ -0,0 +1,55 @@ +const configFile = require('../config.json'); + +interface IConfig { + SERVER: { + HOST: 'http://localhost'; + HTTP_PORT: number; + }; + MEMPOOL: { + HTTP_HOST: string; + HTTP_PORT: number; + }; + PUPPETEER: { + CLUSTER_SIZE: number; + EXEC_PATH?: string; + }; +} + +const defaults: IConfig = { + 'SERVER': { + 'HOST': 'http://localhost', + 'HTTP_PORT': 4201, + }, + 'MEMPOOL': { + 'HTTP_HOST': 'http://localhost', + 'HTTP_PORT': 4200, + }, + 'PUPPETEER': { + 'CLUSTER_SIZE': 1, + }, +}; + +class Config implements IConfig { + SERVER: IConfig['SERVER']; + MEMPOOL: IConfig['MEMPOOL']; + PUPPETEER: IConfig['PUPPETEER']; + + constructor() { + const configs = this.merge(configFile, defaults); + this.SERVER = configs.SERVER; + this.MEMPOOL = configs.MEMPOOL; + this.PUPPETEER = configs.PUPPETEER; + } + + merge = (...objects: object[]): IConfig => { + // @ts-ignore + return objects.reduce((prev, next) => { + Object.keys(prev).forEach(key => { + next[key] = { ...next[key], ...prev[key] }; + }); + return next; + }); + } +} + +export default new Config(); diff --git a/unfurler/src/index.ts b/unfurler/src/index.ts new file mode 100644 index 000000000..8d0011a44 --- /dev/null +++ b/unfurler/src/index.ts @@ -0,0 +1,125 @@ +import express from "express"; +import { Application, Request, Response, NextFunction } from 'express'; +import * as http from 'http'; +import config from './config'; +import { Cluster } from 'puppeteer-cluster'; +const puppeteerConfig = require('../puppeteer.config.json'); + +if (config.PUPPETEER.EXEC_PATH) { + puppeteerConfig.executablePath = config.PUPPETEER.EXEC_PATH; +} + +class Server { + private server: http.Server | undefined; + private app: Application; + cluster?: Cluster; + mempoolHost: string; + + constructor() { + this.app = express(); + this.mempoolHost = config.MEMPOOL.HTTP_HOST + (config.MEMPOOL.HTTP_PORT ? ':' + config.MEMPOOL.HTTP_PORT : ''); + this.startServer(); + } + + async startServer() { + this.app + .use((req: Request, res: Response, next: NextFunction) => { + res.setHeader('Access-Control-Allow-Origin', '*'); + next(); + }) + .use(express.urlencoded({ extended: true })) + .use(express.text()) + ; + + this.cluster = await Cluster.launch({ + concurrency: Cluster.CONCURRENCY_CONTEXT, + maxConcurrency: config.PUPPETEER.CLUSTER_SIZE, + puppeteerOptions: puppeteerConfig, + }); + await this.cluster?.task(async (args) => { return this.renderPreviewTask(args) }); + + this.setUpRoutes(); + + this.server = http.createServer(this.app); + + this.server.listen(config.SERVER.HTTP_PORT, () => { + console.log(`Mempool Unfurl Server is running on port ${config.SERVER.HTTP_PORT}`); + }); + } + + setUpRoutes() { + this.app.get('/render*', async (req, res) => { return this.renderPreview(req, res) }) + this.app.get('*', (req, res) => { return this.renderHTML(req, res) }) + } + + async renderPreviewTask({ page, data: url }) { + await page.goto(url, { waitUntil: "domcontentloaded" }); + await page.evaluate(async () => { + // wait for all images to finish loading + const imgs = Array.from(document.querySelectorAll("img")); + await Promise.all([ + document.fonts.ready, + ...imgs.map((img) => { + if (img.complete) { + if (img.naturalHeight !== 0) return; + throw new Error("Image failed to load"); + } + return new Promise((resolve, reject) => { + img.addEventListener("load", resolve); + img.addEventListener("error", reject); + }); + }), + ]); + }); + return page.screenshot(); + } + + async renderPreview(req, res) { + try { + const img = await this.cluster?.execute(this.mempoolHost + req.params[0]); + + res.contentType('image/png'); + res.send(img); + } catch (e) { + console.log(e); + res.status(500).send(e instanceof Error ? e.message : e); + } + } + + renderHTML(req, res) { + let lang = ''; + let path = req.originalUrl + // extract the language setting (if any) + const parts = path.split(/^\/(ar|bg|bs|ca|cs|da|de|et|el|es|eo|eu|fa|fr|gl|ko|hr|id|it|he|ka|lv|lt|hu|mk|ms|nl|ja|nb|nn|pl|pt|pt-BR|ro|ru|sk|sl|sr|sh|fi|sv|th|tr|uk|vi|zh|hi)\//) + if (parts.length > 1) { + lang = "/" + parts[1]; + path = "/" + parts[2]; + } + const ogImageUrl = config.SERVER.HOST + '/render' + lang + "/preview" + path; + res.send(` + + + + + mempool - Bitcoin Explorer + + + + + + + + + + + + + + + + + `); + } +} + +const server = new Server(); diff --git a/unfurler/tsconfig.json b/unfurler/tsconfig.json new file mode 100644 index 000000000..6aa69dc08 --- /dev/null +++ b/unfurler/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "types": ["node"], + "module": "commonjs", + "target": "esnext", + "lib": ["es2019", "dom"], + "strict": true, + "noImplicitAny": false, + "sourceMap": false, + "outDir": "dist", + "moduleResolution": "node", + "typeRoots": [ + "node_modules/@types" + ], + "allowSyntheticDefaultImports": true, + "esModuleInterop": true + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "dist/**" + ] +} \ No newline at end of file From 754ae231d1d3d7eb47ac0da7d4501cc7e076703e Mon Sep 17 00:00:00 2001 From: Leonardo Lima Date: Mon, 25 Jul 2022 14:07:45 -0300 Subject: [PATCH 021/105] chore: accept the CLA for @oleonardolima --- contributors/oleonardolima.txt | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 contributors/oleonardolima.txt diff --git a/contributors/oleonardolima.txt b/contributors/oleonardolima.txt new file mode 100644 index 000000000..79adbcb78 --- /dev/null +++ b/contributors/oleonardolima.txt @@ -0,0 +1,3 @@ +I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of July 25, 2022. + +Signed: oleonardolima From 7f4a25baa0ce2c80cf5c728f5994282e6159acdb Mon Sep 17 00:00:00 2001 From: Leonardo Lima Date: Mon, 25 Jul 2022 14:54:00 -0300 Subject: [PATCH 022/105] feat: add /block/:hash/raw api route --- .../src/api/bitcoin/bitcoin-api-abstract-factory.ts | 1 + backend/src/api/bitcoin/bitcoin.routes.ts | 13 ++++++++++++- backend/src/api/bitcoin/esplora-api.ts | 5 +++++ frontend/src/app/docs/api-docs/api-docs-data.ts | 2 +- 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts index f3f7011dd..358bd29e4 100644 --- a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts +++ b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts @@ -9,6 +9,7 @@ export interface AbstractBitcoinApi { $getBlockHash(height: number): Promise; $getBlockHeader(hash: string): Promise; $getBlock(hash: string): Promise; + $getRawBlock(hash: string): Promise; $getAddress(address: string): Promise; $getAddressTransactions(address: string, lastSeenTxId: string): Promise; $getAddressPrefix(prefix: string): string[]; diff --git a/backend/src/api/bitcoin/bitcoin.routes.ts b/backend/src/api/bitcoin/bitcoin.routes.ts index a04a78117..ffdcd0c26 100644 --- a/backend/src/api/bitcoin/bitcoin.routes.ts +++ b/backend/src/api/bitcoin/bitcoin.routes.ts @@ -103,9 +103,10 @@ class BitcoinRoutes { .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/header', this.getBlockHeader) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight) .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/hash', this.getBlockTipHash) + .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/raw', this.getRawBlock) + .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txids', this.getTxIdsForBlock) .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txs', this.getBlockTransactions) .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txs/:index', this.getBlockTransactions) - .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txids', this.getTxIdsForBlock) .get(config.MEMPOOL.API_URL_PREFIX + 'block-height/:height', this.getBlockHeight) .get(config.MEMPOOL.API_URL_PREFIX + 'address/:address', this.getAddress) .get(config.MEMPOOL.API_URL_PREFIX + 'address/:address/txs', this.getAddressTransactions) @@ -470,6 +471,16 @@ class BitcoinRoutes { } } + private async getRawBlock(req: Request, res: Response) { + try { + const result = await bitcoinApi.$getRawBlock(req.params.hash); + res.setHeader('content-type', 'text/plain'); + res.send(result); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + private async getTxIdsForBlock(req: Request, res: Response) { try { const result = await bitcoinApi.$getTxIdsForBlock(req.params.hash); diff --git a/backend/src/api/bitcoin/esplora-api.ts b/backend/src/api/bitcoin/esplora-api.ts index e8eee343a..ebaf2f6a0 100644 --- a/backend/src/api/bitcoin/esplora-api.ts +++ b/backend/src/api/bitcoin/esplora-api.ts @@ -50,6 +50,11 @@ class ElectrsApi implements AbstractBitcoinApi { .then((response) => response.data); } + $getRawBlock(hash: string): Promise { + return axios.get(config.ESPLORA.REST_API_URL + '/block/' + hash + "/raw", this.axiosConfig) + .then((response) => response.data); + } + $getAddress(address: string): Promise { throw new Error('Method getAddress not implemented.'); } 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 f8f0e23b8..80aab5f15 100644 --- a/frontend/src/app/docs/api-docs/api-docs-data.ts +++ b/frontend/src/app/docs/api-docs/api-docs-data.ts @@ -2070,7 +2070,7 @@ export const restApiDocsData = [ fragment: "get-block-raw", title: "GET Block Raw", description: { - default: "Returns the raw block representation in binary." + default: "Returns the raw block representation in binary for Esplora backend, or hex for Bitcoin Core RPC backend." }, urlString: "/block/:hash/raw", showConditions: bitcoinNetworks.concat(liquidNetworks), From acc44e22f0e99e9bfee420fa610383c1aa96fb43 Mon Sep 17 00:00:00 2001 From: softsimon Date: Tue, 26 Jul 2022 01:01:55 +0200 Subject: [PATCH 023/105] A couple of new eslint rules --- backend/.eslintrc | 7 +++++-- frontend/.eslintrc | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/backend/.eslintrc b/backend/.eslintrc index d8f453c51..3029ebab6 100644 --- a/backend/.eslintrc +++ b/backend/.eslintrc @@ -15,10 +15,11 @@ "@typescript-eslint/ban-types": 1, "@typescript-eslint/no-empty-function": 1, "@typescript-eslint/no-explicit-any": 1, - "@typescript-eslint/no-inferrable-types": 1, + "@typescript-eslint/no-inferrable-types": 0, "@typescript-eslint/no-namespace": 1, "@typescript-eslint/no-this-alias": 1, "@typescript-eslint/no-var-requires": 1, + "@typescript-eslint/explicit-function-return-type": 1, "no-console": 1, "no-constant-condition": 1, "no-dupe-else-if": 1, @@ -28,6 +29,8 @@ "no-useless-catch": 1, "no-var": 1, "prefer-const": 1, - "prefer-rest-params": 1 + "prefer-rest-params": 1, + "quotes": [1, "single", { "allowTemplateLiterals": true }], + "semi": 1 } } diff --git a/frontend/.eslintrc b/frontend/.eslintrc index d0fce56f0..4dbcf98d9 100644 --- a/frontend/.eslintrc +++ b/frontend/.eslintrc @@ -14,10 +14,11 @@ "@typescript-eslint/ban-types": 1, "@typescript-eslint/no-empty-function": 1, "@typescript-eslint/no-explicit-any": 1, - "@typescript-eslint/no-inferrable-types": 1, + "@typescript-eslint/no-inferrable-types": 0, "@typescript-eslint/no-namespace": 1, "@typescript-eslint/no-this-alias": 1, "@typescript-eslint/no-var-requires": 1, + "@typescript-eslint/explicit-function-return-type": 1, "no-case-declarations": 1, "no-console": 1, "no-constant-condition": 1, @@ -29,6 +30,8 @@ "no-useless-catch": 1, "no-var": 1, "prefer-const": 1, - "prefer-rest-params": 1 + "prefer-rest-params": 1, + "quotes": [1, "single", { "allowTemplateLiterals": true }], + "semi": 1 } } From afd49bfa8921050b878a8d53591baa9954b3cae8 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Tue, 26 Jul 2022 12:26:44 +0200 Subject: [PATCH 024/105] [Node page] Add link to node list per country/isp in node page --- backend/src/api/explorer/nodes.api.ts | 3 ++- .../app/lightning/node/node.component.html | 20 ++++++++++++++++--- .../src/app/lightning/node/node.component.ts | 2 ++ frontend/src/app/shared/graphs.utils.ts | 3 +++ 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/backend/src/api/explorer/nodes.api.ts b/backend/src/api/explorer/nodes.api.ts index ec8ee35fb..9899e20fc 100644 --- a/backend/src/api/explorer/nodes.api.ts +++ b/backend/src/api/explorer/nodes.api.ts @@ -5,7 +5,7 @@ class NodesApi { public async $getNode(public_key: string): Promise { try { const query = ` - SELECT nodes.*, geo_names_as.names as as_organization, geo_names_city.names as city, + SELECT nodes.*, geo_names_iso.names as iso_code, geo_names_as.names as as_organization, geo_names_city.names as city, geo_names_country.names as country, geo_names_subdivision.names as subdivision, (SELECT Count(*) FROM channels @@ -24,6 +24,7 @@ class NodesApi { LEFT JOIN geo_names geo_names_city on geo_names_city.id = city_id LEFT JOIN geo_names geo_names_subdivision on geo_names_subdivision.id = subdivision_id LEFT JOIN geo_names geo_names_country on geo_names_country.id = country_id + LEFT JOIN geo_names geo_names_iso ON geo_names_iso.id = nodes.country_id AND geo_names_iso.type = 'country_iso_code' WHERE public_key = ? `; const [rows]: any = await DB.query(query, [public_key, public_key, public_key, public_key, public_key, public_key, public_key, public_key, public_key]); diff --git a/frontend/src/app/lightning/node/node.component.html b/frontend/src/app/lightning/node/node.component.html index e2132bca5..774f0aaab 100644 --- a/frontend/src/app/lightning/node/node.component.html +++ b/frontend/src/app/lightning/node/node.component.html @@ -43,11 +43,23 @@ Location - {{ node.city.en }}, {{ node.subdivision.en }}
{{ node.country.en }} + + {{ node.city.en }}, {{ node.subdivision.en }} +
+ + {{ node.country.en }} +   + {{ node.flag }} + + Location - {{ node.country.en }} + + + {{ node.country.en }} {{ node.flag }} + + @@ -77,7 +89,9 @@ ISP - {{ node.as_organization }} [ASN {{node.as_number}}] + + {{ node.as_organization }} [ASN {{node.as_number}}] + diff --git a/frontend/src/app/lightning/node/node.component.ts b/frontend/src/app/lightning/node/node.component.ts index c70983b54..c9971a4cb 100644 --- a/frontend/src/app/lightning/node/node.component.ts +++ b/frontend/src/app/lightning/node/node.component.ts @@ -3,6 +3,7 @@ import { ActivatedRoute, ParamMap } from '@angular/router'; import { Observable } from 'rxjs'; import { catchError, map, switchMap } from 'rxjs/operators'; import { SeoService } from 'src/app/services/seo.service'; +import { getFlagEmoji } from 'src/app/shared/graphs.utils'; import { LightningApiService } from '../lightning-api.service'; @Component({ @@ -51,6 +52,7 @@ export class NodeComponent implements OnInit { } else if (socket.indexOf('onion') > -1) { label = 'Tor'; } + node.flag = getFlagEmoji(node.iso_code); socketsObject.push({ label: label, socket: node.public_key + '@' + socket, diff --git a/frontend/src/app/shared/graphs.utils.ts b/frontend/src/app/shared/graphs.utils.ts index 6909e6fac..019ca49e4 100644 --- a/frontend/src/app/shared/graphs.utils.ts +++ b/frontend/src/app/shared/graphs.utils.ts @@ -92,6 +92,9 @@ export function detectWebGL() { } export function getFlagEmoji(countryCode) { + if (!countryCode) { + return ''; + } const codePoints = countryCode .toUpperCase() .split('') From f3c8c34bb0f1bfc9e952ffdcf175d32db07022df Mon Sep 17 00:00:00 2001 From: nymkappa Date: Tue, 26 Jul 2022 14:10:09 +0200 Subject: [PATCH 025/105] Remove node chart 1mb line and fix y axis --- .../node-statistics-chart.component.ts | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/frontend/src/app/lightning/node-statistics-chart/node-statistics-chart.component.ts b/frontend/src/app/lightning/node-statistics-chart/node-statistics-chart.component.ts index 15997d3c3..962059c9d 100644 --- a/frontend/src/app/lightning/node-statistics-chart/node-statistics-chart.component.ts +++ b/frontend/src/app/lightning/node-statistics-chart/node-statistics-chart.component.ts @@ -169,9 +169,6 @@ export class NodeStatisticsChartComponent implements OnInit { }, yAxis: data.channels.length === 0 ? undefined : [ { - min: (value) => { - return value.min * 0.9; - }, type: 'value', axisLabel: { color: 'rgb(110, 112, 121)', @@ -188,9 +185,6 @@ export class NodeStatisticsChartComponent implements OnInit { }, }, { - min: (value) => { - return value.min * 0.9; - }, type: 'value', position: 'right', axisLabel: { @@ -225,15 +219,6 @@ export class NodeStatisticsChartComponent implements OnInit { opacity: 1, width: 1, }, - data: [{ - yAxis: 1, - label: { - position: 'end', - show: true, - color: '#ffffff', - formatter: `1 MB` - } - }], } }, { From 8872c13de83e2013c0d456037bceed9cf4a5b4ea Mon Sep 17 00:00:00 2001 From: nymkappa Date: Tue, 26 Jul 2022 15:18:42 +0200 Subject: [PATCH 026/105] Show 3y charts by default on dashboard to get more interesting charts --- .../nodes-networks-chart/nodes-networks-chart.component.ts | 2 +- .../statistics-chart/lightning-statistics-chart.component.ts | 2 +- 2 files changed, 2 insertions(+), 2 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 735d4868f..c292d09f7 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 @@ -59,7 +59,7 @@ export class NodesNetworksChartComponent implements OnInit { let firstRun = true; if (this.widget) { - this.miningWindowPreference = '1y'; + this.miningWindowPreference = '3y'; } else { this.seoService.setTitle($localize`Lightning nodes per network`); this.miningWindowPreference = this.miningService.getDefaultTimespan('all'); diff --git a/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.ts b/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.ts index d95205542..1727d1f68 100644 --- a/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.ts +++ b/frontend/src/app/lightning/statistics-chart/lightning-statistics-chart.component.ts @@ -58,7 +58,7 @@ export class LightningStatisticsChartComponent implements OnInit { let firstRun = true; if (this.widget) { - this.miningWindowPreference = '1y'; + this.miningWindowPreference = '3y'; } else { this.seoService.setTitle($localize`Channels and Capacity`); this.miningWindowPreference = this.miningService.getDefaultTimespan('all'); From 857728a4c8d278a473bcd020698c3ba7c7971f40 Mon Sep 17 00:00:00 2001 From: Antoni Spaanderman <56turtle56@gmail.com> Date: Tue, 26 Jul 2022 16:29:42 +0200 Subject: [PATCH 027/105] Address @Xekyo's comments on https://github.com/mempool/mempool/pull/2167 + minor fixes, variable name consistency and ts types --- frontend/src/app/bitcoin.utils.ts | 68 ++++++++++--------- .../tx-features/tx-features.component.html | 10 +-- .../tx-features/tx-features.component.ts | 6 +- 3 files changed, 45 insertions(+), 39 deletions(-) diff --git a/frontend/src/app/bitcoin.utils.ts b/frontend/src/app/bitcoin.utils.ts index b2ea1d7a9..25432565c 100644 --- a/frontend/src/app/bitcoin.utils.ts +++ b/frontend/src/app/bitcoin.utils.ts @@ -5,9 +5,9 @@ const P2SH_P2WSH_COST = 35 * 4; // the WU cost for the non-witness part of P2SH export function calcSegwitFeeGains(tx: Transaction) { // calculated in weight units - let realizedBech32Gains = 0; - let potentialBech32Gains = 0; - let potentialP2shGains = 0; + let realizedSegwitGains = 0; + let potentialSegwitGains = 0; + let potentialP2shSegwitGains = 0; let potentialTaprootGains = 0; let realizedTaprootGains = 0; @@ -24,31 +24,33 @@ export function calcSegwitFeeGains(tx: Transaction) { const isP2tr = vin.prevout.scriptpubkey_type === 'v1_p2tr'; const op = vin.scriptsig ? vin.scriptsig_asm.split(' ')[0] : null; - const isP2sh2Wpkh = isP2sh && !!vin.witness && op === 'OP_PUSHBYTES_22'; - const isP2sh2Wsh = isP2sh && !!vin.witness && op === 'OP_PUSHBYTES_34'; + const isP2shP2Wpkh = isP2sh && !!vin.witness && op === 'OP_PUSHBYTES_22'; + const isP2shP2Wsh = isP2sh && !!vin.witness && op === 'OP_PUSHBYTES_34'; switch (true) { - // Native Segwit - P2WPKH/P2WSH (Bech32) + // Native Segwit - P2WPKH/P2WSH/P2TR case isP2wpkh: case isP2wsh: case isP2tr: // maximal gains: the scriptSig is moved entirely to the witness part - realizedBech32Gains += witnessSize(vin) * 3; + // if taproot is used savings are 42 WU higher because it produces smaller signatures and doesn't require a pubkey in the witness + // this number is explained above `realizedTaprootGains += 42;` + realizedSegwitGains += (witnessSize(vin) + (isP2tr ? 42 : 0)) * 3; // XXX P2WSH output creation is more expensive, should we take this into consideration? break; // Backward compatible Segwit - P2SH-P2WPKH - case isP2sh2Wpkh: - // the scriptSig is moved to the witness, but we have extra 21 extra non-witness bytes (48 WU) - realizedBech32Gains += witnessSize(vin) * 3 - P2SH_P2WPKH_COST; - potentialBech32Gains += P2SH_P2WPKH_COST; + case isP2shP2Wpkh: + // the scriptSig is moved to the witness, but we have extra 21 extra non-witness bytes (84 WU) + realizedSegwitGains += witnessSize(vin) * 3 - P2SH_P2WPKH_COST; + potentialSegwitGains += P2SH_P2WPKH_COST; break; // Backward compatible Segwit - P2SH-P2WSH - case isP2sh2Wsh: - // the scriptSig is moved to the witness, but we have extra 35 extra non-witness bytes - realizedBech32Gains += witnessSize(vin) * 3 - P2SH_P2WSH_COST; - potentialBech32Gains += P2SH_P2WSH_COST; + case isP2shP2Wsh: + // the scriptSig is moved to the witness, but we have extra 35 extra non-witness bytes (140 WU) + realizedSegwitGains += witnessSize(vin) * 3 - P2SH_P2WSH_COST; + potentialSegwitGains += P2SH_P2WSH_COST; break; // Non-segwit P2PKH/P2SH/P2PK/bare multisig @@ -56,9 +58,13 @@ export function calcSegwitFeeGains(tx: Transaction) { case isP2sh: case isP2pk: case isBareMultisig: { - const fullGains = scriptSigSize(vin) * 3; - potentialBech32Gains += fullGains; - potentialP2shGains += fullGains - (isP2pkh ? P2SH_P2WPKH_COST : P2SH_P2WSH_COST); + let fullGains = scriptSigSize(vin) * 3; + if (isBareMultisig) { + // a _bare_ multisig has the keys in the output script, but P2SH and P2WSH require them to be in the scriptSig/scriptWitness + fullGains -= vin.prevout.scriptpubkey.length / 2; + } + potentialSegwitGains += fullGains; + potentialP2shSegwitGains += fullGains - (isP2pkh ? P2SH_P2WPKH_COST : P2SH_P2WSH_COST); break; } } @@ -79,11 +85,11 @@ export function calcSegwitFeeGains(tx: Transaction) { // TODO maybe add some complex scripts that are specified somewhere, so that size is known, such as lightning scripts } } else { - const script = isP2sh2Wsh || isP2wsh ? vin.inner_witnessscript_asm : vin.inner_redeemscript_asm; + const script = isP2shP2Wsh || isP2wsh ? vin.inner_witnessscript_asm : vin.inner_redeemscript_asm; let replacementSize: number; if ( // single sig - isP2pk || isP2pkh || isP2wpkh || isP2sh2Wpkh || + isP2pk || isP2pkh || isP2wpkh || isP2shP2Wpkh || // multisig isBareMultisig || parseMultisigScript(script) ) { @@ -105,11 +111,11 @@ export function calcSegwitFeeGains(tx: Transaction) { // returned as percentage of the total tx weight return { - realizedBech32Gains: realizedBech32Gains / (tx.weight + realizedBech32Gains), // percent of the pre-segwit tx size - potentialBech32Gains: potentialBech32Gains / tx.weight, - potentialP2shGains: potentialP2shGains / tx.weight, + realizedSegwitGains: realizedSegwitGains / (tx.weight + realizedSegwitGains), // percent of the pre-segwit tx size + potentialSegwitGains: potentialSegwitGains / tx.weight, + potentialP2shSegwitGains: potentialP2shSegwitGains / tx.weight, potentialTaprootGains: potentialTaprootGains / tx.weight, - realizedTaprootGains: realizedTaprootGains / tx.weight + realizedTaprootGains: realizedTaprootGains / (tx.weight + realizedTaprootGains) }; } @@ -188,12 +194,12 @@ export function moveDec(num: number, n: number) { return neg + (int || '0') + (frac.length ? '.' + frac : ''); } -function zeros(n) { +function zeros(n: number) { return new Array(n + 1).join('0'); } // Formats a number for display. Treats the number as a string to avoid rounding errors. -export const formatNumber = (s, precision = null) => { +export const formatNumber = (s: number | string, precision: number | null = null) => { let [ whole, dec ] = s.toString().split('.'); // divide numbers into groups of three separated with a thin space (U+202F, "NARROW NO-BREAK SPACE"), @@ -219,27 +225,27 @@ const witnessSize = (vin: Vin) => vin.witness ? vin.witness.reduce((S, w) => S + const scriptSigSize = (vin: Vin) => vin.scriptsig ? vin.scriptsig.length / 2 : 0; // Power of ten wrapper -export function selectPowerOfTen(val: number) { +export function selectPowerOfTen(val: number): { divider: number, unit: string } { const powerOfTen = { exa: Math.pow(10, 18), peta: Math.pow(10, 15), - terra: Math.pow(10, 12), + tera: Math.pow(10, 12), giga: Math.pow(10, 9), mega: Math.pow(10, 6), kilo: Math.pow(10, 3), }; - let selectedPowerOfTen; + let selectedPowerOfTen: { divider: number, unit: string }; if (val < powerOfTen.kilo) { selectedPowerOfTen = { divider: 1, unit: '' }; // no scaling } else if (val < powerOfTen.mega) { selectedPowerOfTen = { divider: powerOfTen.kilo, unit: 'k' }; } else if (val < powerOfTen.giga) { selectedPowerOfTen = { divider: powerOfTen.mega, unit: 'M' }; - } else if (val < powerOfTen.terra) { + } else if (val < powerOfTen.tera) { selectedPowerOfTen = { divider: powerOfTen.giga, unit: 'G' }; } else if (val < powerOfTen.peta) { - selectedPowerOfTen = { divider: powerOfTen.terra, unit: 'T' }; + selectedPowerOfTen = { divider: powerOfTen.tera, unit: 'T' }; } else if (val < powerOfTen.exa) { selectedPowerOfTen = { divider: powerOfTen.peta, unit: 'P' }; } else { diff --git a/frontend/src/app/components/tx-features/tx-features.component.html b/frontend/src/app/components/tx-features/tx-features.component.html index 16dbb66f4..e3569de8d 100644 --- a/frontend/src/app/components/tx-features/tx-features.component.html +++ b/frontend/src/app/components/tx-features/tx-features.component.html @@ -1,12 +1,12 @@ -SegWit +SegWit - SegWit - - SegWit + SegWit + + SegWit -Taproot +Taproot Taproot diff --git a/frontend/src/app/components/tx-features/tx-features.component.ts b/frontend/src/app/components/tx-features/tx-features.component.ts index f73d8ae8a..4c0611971 100644 --- a/frontend/src/app/components/tx-features/tx-features.component.ts +++ b/frontend/src/app/components/tx-features/tx-features.component.ts @@ -12,9 +12,9 @@ export class TxFeaturesComponent implements OnChanges { @Input() tx: Transaction; segwitGains = { - realizedBech32Gains: 0, - potentialBech32Gains: 0, - potentialP2shGains: 0, + realizedSegwitGains: 0, + potentialSegwitGains: 0, + potentialP2shSegwitGains: 0, potentialTaprootGains: 0, realizedTaprootGains: 0 }; From 5a4fc9479328264583fda576611d5169ab62d7b7 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Tue, 26 Jul 2022 17:32:43 +0200 Subject: [PATCH 028/105] Order lightning_stats by added timestamp instead of id --- backend/src/api/explorer/statistics.api.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/src/api/explorer/statistics.api.ts b/backend/src/api/explorer/statistics.api.ts index d29bf1ed4..c2e23848f 100644 --- a/backend/src/api/explorer/statistics.api.ts +++ b/backend/src/api/explorer/statistics.api.ts @@ -13,7 +13,7 @@ class StatisticsApi { query += ` WHERE added BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`; } - query += ` ORDER BY id DESC`; + query += ` ORDER BY added DESC`; try { const [rows]: any = await DB.query(query); @@ -26,8 +26,8 @@ class StatisticsApi { public async $getLatestStatistics(): Promise { try { - const [rows]: any = await DB.query(`SELECT * FROM lightning_stats ORDER BY id DESC LIMIT 1`); - const [rows2]: any = await DB.query(`SELECT * FROM lightning_stats ORDER BY id DESC LIMIT 1 OFFSET 7`); + const [rows]: any = await DB.query(`SELECT * FROM lightning_stats ORDER BY added DESC LIMIT 1`); + const [rows2]: any = await DB.query(`SELECT * FROM lightning_stats ORDER BY added DESC LIMIT 1 OFFSET 7`); return { latest: rows[0], previous: rows2[0], From f381891d21b164b4c2ac586a36dd1b03e90093d4 Mon Sep 17 00:00:00 2001 From: softsimon Date: Tue, 26 Jul 2022 21:35:19 +0200 Subject: [PATCH 029/105] Fix for mempool logo not being centered vertically --- .../src/app/components/master-page/master-page.component.html | 2 +- .../src/app/components/master-page/master-page.component.scss | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/master-page/master-page.component.html b/frontend/src/app/components/master-page/master-page.component.html index 1458e762b..f152cb7b3 100644 --- a/frontend/src/app/components/master-page/master-page.component.html +++ b/frontend/src/app/components/master-page/master-page.component.html @@ -8,7 +8,7 @@
- +
Offline
diff --git a/frontend/src/app/components/master-page/master-page.component.scss b/frontend/src/app/components/master-page/master-page.component.scss index 0f7e56828..c6a9aaeff 100644 --- a/frontend/src/app/components/master-page/master-page.component.scss +++ b/frontend/src/app/components/master-page/master-page.component.scss @@ -164,7 +164,7 @@ nav { display: flex; } -app-svg-images { +.mempool-logo, app-svg-images { align-self: center; width: 140px; height: 35px; From 91f2e3d10e1fa6d7a75a2aa887a0abe236651859 Mon Sep 17 00:00:00 2001 From: Stephan Oeste Date: Tue, 26 Jul 2022 22:07:46 +0200 Subject: [PATCH 030/105] Add random generated mysql passwords on prod install --- production/install | 43 +++++++++++++++++--- production/mempool-build-all | 15 +++++++ production/mempool-config.bisq.json | 4 +- production/mempool-config.liquid.json | 4 +- production/mempool-config.liquidtestnet.json | 4 +- production/mempool-config.mainnet.json | 4 +- production/mempool-config.signet.json | 4 +- production/mempool-config.testnet.json | 4 +- 8 files changed, 64 insertions(+), 18 deletions(-) diff --git a/production/install b/production/install index fb3aa9281..e9b24bafa 100755 --- a/production/install +++ b/production/install @@ -218,6 +218,21 @@ MYSQL_HOME=/mysql MYSQL_USER=mysql MYSQL_GROUP=mysql +# mempool mysql user/password +MEMPOOL_MAINNET_USER='mempool' +MEMPOOL_TESTNET_USER='mempool_testnet' +MEMPOOL_SIGNET_USER='mempool_signet' +MEMPOOL_LIQUID_USER='mempool_liquid' +MEMPOOL_LIQUIDTESTNET_USER='mempool_liquidtestnet' +MEMPOOL_BISQ_USER='mempool_bisq' +# generate random hex string +MEMPOOL_MAINNET_PASS=$(head -150 /dev/urandom | ${MD5} | awk '{print $1}') +MEMPOOL_TESTNET_PASS=$(head -150 /dev/urandom | ${MD5} | awk '{print $1}') +MEMPOOL_SIGNET_PASS=$(head -150 /dev/urandom | ${MD5} | awk '{print $1}') +MEMPOOL_LIQUID_PASS=$(head -150 /dev/urandom | ${MD5} | awk '{print $1}') +MEMPOOL_LIQUIDTESTNET_PASS=$(head -150 /dev/urandom | ${MD5} | awk '{print $1}') +MEMPOOL_BISQ_PASS=$(head -150 /dev/urandom | ${MD5} | awk '{print $1}') + # mempool data folder and user/group MEMPOOL_HOME=/mempool MEMPOOL_USER=mempool @@ -1513,22 +1528,38 @@ esac mysql << _EOF_ create database mempool; -grant all on mempool.* to 'mempool'@'localhost' identified by 'mempool'; +grant all on mempool.* to '${MEMPOOL_MAINNET_USER}'@'localhost' identified by '${MEMPOOL_MAINNET_PASS}'; create database mempool_testnet; -grant all on mempool_testnet.* to 'mempool_testnet'@'localhost' identified by 'mempool_testnet'; +grant all on mempool_testnet.* to '${MEMPOOL_TESTNET_USER}'@'localhost' identified by '${MEMPOOL_TESTNET_PASS}'; create database mempool_signet; -grant all on mempool_signet.* to 'mempool_signet'@'localhost' identified by 'mempool_signet'; +grant all on mempool_signet.* to '${MEMPOOL_SIGNET_USER}'@'localhost' identified by '${MEMPOOL_SIGNET_PASS}'; create database mempool_liquid; -grant all on mempool_liquid.* to 'mempool_liquid'@'localhost' identified by 'mempool_liquid'; +grant all on mempool_liquid.* to '${MEMPOOL_LIQUID_USER}'@'localhost' identified by '${MEMPOOL_LIQUID_PASS}'; create database mempool_liquidtestnet; -grant all on mempool_liquidtestnet.* to 'mempool_liquidtestnet'@'localhost' identified by 'mempool_liquidtestnet'; +grant all on mempool_liquidtestnet.* to '${MEMPOOL_LIQUIDTESTNET_USER}'@'localhost' identified by '${MEMPOOL_LIQUIDTESTNET_PASS}'; create database mempool_bisq; -grant all on mempool_bisq.* to 'mempool_bisq'@'localhost' identified by 'mempool_bisq'; +grant all on mempool_bisq.* to '${MEMPOOL_BISQ_USER}'@'localhost' identified by '${MEMPOOL_BISQ_PASS}'; +_EOF_ + +echo "[*] save MySQL credentials" +cat > ${MEMPOOL_HOME}/mysql_credentials << _EOF_ +declare -x MEMPOOL_MAINNET_USER="${MEMPOOL_MAINNET_USER}" +declare -x MEMPOOL_MAINNET_PASS="${MEMPOOL_MAINNET_PASS}" +declare -x MEMPOOL_TESTNET_USER="${MEMPOOL_TESTNET_USER}" +declare -x MEMPOOL_TESTNET_PASS="${MEMPOOL_TESTNET_PASS}" +declare -x MEMPOOL_SIGNET_USER="${MEMPOOL_SIGNET_USER}" +declare -x MEMPOOL_SIGNET_PASS="${MEMPOOL_SIGNET_PASS}" +declare -x MEMPOOL_LIQUID_USER="${MEMPOOL_LIQUID_USER}" +declare -x MEMPOOL_LIQUID_PASS="${MEMPOOL_LIQUID_PASS}" +declare -x MEMPOOL_LIQUIDTESTNET_USER="${MEMPOOL_LIQUIDTESTNET_USER}" +declare -x MEMPOOL_LIQUIDTESTNET_PASS="${MEMPOOL_LIQUIDTESTNET_PASS}" +declare -x MEMPOOL_BISQ_USER="${MEMPOOL_BISQ_USER}" +declare -x MEMPOOL_BISQ_PASS="${MEMPOOL_BISQ_PASS}" _EOF_ ##### nginx diff --git a/production/mempool-build-all b/production/mempool-build-all index 5ac25f7e4..c0e9a2c2a 100755 --- a/production/mempool-build-all +++ b/production/mempool-build-all @@ -11,6 +11,9 @@ BITCOIN_RPC_PASS=$(grep '^rpcpassword' /bitcoin/bitcoin.conf | cut -d '=' -f2) ELEMENTS_RPC_USER=$(grep '^rpcuser' /elements/elements.conf | cut -d '=' -f2) ELEMENTS_RPC_PASS=$(grep '^rpcpassword' /elements/elements.conf | cut -d '=' -f2) +# get mysql credentials +. /mempool/mysql_credentials + if [ -f "${LOCKFILE}" ];then echo "upgrade already running? check lockfile ${LOCKFILE}" exit 1 @@ -73,6 +76,18 @@ build_backend() -e "s!__BITCOIN_RPC_PASS__!${BITCOIN_RPC_PASS}!" \ -e "s!__ELEMENTS_RPC_USER__!${ELEMENTS_RPC_USER}!" \ -e "s!__ELEMENTS_RPC_PASS__!${ELEMENTS_RPC_PASS}!" \ + -e "s!__MEMPOOL_MAINNET_USER__!${MEMPOOL_MAINNET_USER}!" \ + -e "s!__MEMPOOL_MAINNET_PASS__!${MEMPOOL_MAINNET_PASS}!" \ + -e "s!__MEMPOOL_TESTNET_USER__!${MEMPOOL_TESTNET_USER}!" \ + -e "s!__MEMPOOL_TESTNET_PASS__!${MEMPOOL_TESTNET_PASS}!" \ + -e "s!__MEMPOOL_SIGNET_USER__!${MEMPOOL_SIGNET_USER}!" \ + -e "s!__MEMPOOL_SIGNET_PASS__!${MEMPOOL_SIGNET_PASS}!" \ + -e "s!__MEMPOOL_LIQUID_USER__!${MEMPOOL_LIQUID_USER}!" \ + -e "s!__MEMPOOL_LIQUID_PASS__!${MEMPOOL_LIQUID_PASS}!" \ + -e "s!__MEMPOOL_LIQUIDTESTNET_USER__!${LIQUIDTESTNET_USER}!" \ + -e "s!__MEMPOOL_LIQUIDTESTNET_PASS__!${MEMPOOL_LIQUIDTESTNET_PASS}!" \ + -e "s!__MEMPOOL_BISQ_USER__!${MEMPOOL_BISQ_USER}!" \ + -e "s!__MEMPOOL_BISQ_PASS__!${MEMPOOL_BISQ_PASS}!" \ "mempool-config.json" fi npm install --omit=dev --omit=optional || exit 1 diff --git a/production/mempool-config.bisq.json b/production/mempool-config.bisq.json index 1e91be930..599711764 100644 --- a/production/mempool-config.bisq.json +++ b/production/mempool-config.bisq.json @@ -21,8 +21,8 @@ "ENABLED": false, "HOST": "127.0.0.1", "PORT": 3306, - "USERNAME": "mempool_bisq", - "PASSWORD": "mempool_bisq", + "USERNAME": "__MEMPOOL_BISQ_USER__", + "PASSWORD": "__MEMPOOL_BISQ_PASS__", "DATABASE": "mempool_bisq" }, "STATISTICS": { diff --git a/production/mempool-config.liquid.json b/production/mempool-config.liquid.json index 70ab56625..11ad8ffcd 100644 --- a/production/mempool-config.liquid.json +++ b/production/mempool-config.liquid.json @@ -28,8 +28,8 @@ "ENABLED": true, "HOST": "127.0.0.1", "PORT": 3306, - "USERNAME": "mempool_liquid", - "PASSWORD": "mempool_liquid", + "USERNAME": "__MEMPOOL_LIQUID_USER__", + "PASSWORD": "__MEMPOOL_LIQUID_PASS__", "DATABASE": "mempool_liquid" }, "STATISTICS": { diff --git a/production/mempool-config.liquidtestnet.json b/production/mempool-config.liquidtestnet.json index b3c4dfaaf..7769bfb53 100644 --- a/production/mempool-config.liquidtestnet.json +++ b/production/mempool-config.liquidtestnet.json @@ -28,8 +28,8 @@ "ENABLED": true, "HOST": "127.0.0.1", "PORT": 3306, - "USERNAME": "mempool_liquidtestnet", - "PASSWORD": "mempool_liquidtestnet", + "USERNAME": "__MEMPOOL_LIQUIDTESTNET_USER__", + "PASSWORD": "__MEMPOOL_LIQUIDTESTNET_PASS__", "DATABASE": "mempool_liquidtestnet" }, "STATISTICS": { diff --git a/production/mempool-config.mainnet.json b/production/mempool-config.mainnet.json index 4575afdbe..06a14d223 100644 --- a/production/mempool-config.mainnet.json +++ b/production/mempool-config.mainnet.json @@ -32,8 +32,8 @@ "ENABLED": true, "HOST": "127.0.0.1", "PORT": 3306, - "USERNAME": "mempool", - "PASSWORD": "mempool", + "USERNAME": "__MEMPOOL_MAINNET_USER__", + "PASSWORD": "__MEMPOOL_MAINNET_PASS__", "DATABASE": "mempool" }, "STATISTICS": { diff --git a/production/mempool-config.signet.json b/production/mempool-config.signet.json index c1333f45a..f42c4dc50 100644 --- a/production/mempool-config.signet.json +++ b/production/mempool-config.signet.json @@ -24,8 +24,8 @@ "ENABLED": true, "HOST": "127.0.0.1", "PORT": 3306, - "USERNAME": "mempool_signet", - "PASSWORD": "mempool_signet", + "USERNAME": "__MEMPOOL_SIGNET_USER__", + "PASSWORD": "__MEMPOOL_SIGNET_PASS__", "DATABASE": "mempool_signet" }, "STATISTICS": { diff --git a/production/mempool-config.testnet.json b/production/mempool-config.testnet.json index 79190c2de..cc63f93bf 100644 --- a/production/mempool-config.testnet.json +++ b/production/mempool-config.testnet.json @@ -24,8 +24,8 @@ "ENABLED": true, "HOST": "127.0.0.1", "PORT": 3306, - "USERNAME": "mempool_testnet", - "PASSWORD": "mempool_testnet", + "USERNAME": "__MEMPOOL_TESTNET_USER__", + "PASSWORD": "__MEMPOOL_TESTNET_PASS__", "DATABASE": "mempool_testnet" }, "STATISTICS": { From 3b5ef6253659e65d1c2bf21994aea1ae34640af9 Mon Sep 17 00:00:00 2001 From: wiz Date: Wed, 27 Jul 2022 00:47:46 +0200 Subject: [PATCH 031/105] Fix zmq ports in prod bitcoin.conf --- production/bitcoin.conf | 38 ++++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/production/bitcoin.conf b/production/bitcoin.conf index 4cb95eacc..0fa1e943f 100644 --- a/production/bitcoin.conf +++ b/production/bitcoin.conf @@ -18,12 +18,18 @@ whitelist=2401:b140::/32 #uacomment=@wiz [main] -bind=0.0.0.0:8333 -bind=[::]:8333 rpcbind=127.0.0.1:8332 rpcbind=[::1]:8332 -zmqpubrawblock=tcp://127.0.0.1:18332 -zmqpubrawtx=tcp://127.0.0.1:18333 +bind=0.0.0.0:8333 +bind=[::]:8333 +zmqpubrawblock=tcp://127.0.0.1:8334 +zmqpubrawtx=tcp://127.0.0.1:8335 +#addnode=[2401:b140:1::92:201]:8333 +#addnode=[2401:b140:1::92:202]:8333 +#addnode=[2401:b140:1::92:203]:8333 +#addnode=[2401:b140:1::92:204]:8333 +#addnode=[2401:b140:1::92:205]:8333 +#addnode=[2401:b140:1::92:206]:8333 #addnode=[2401:b140:2::92:201]:8333 #addnode=[2401:b140:2::92:202]:8333 #addnode=[2401:b140:2::92:203]:8333 @@ -33,10 +39,18 @@ zmqpubrawtx=tcp://127.0.0.1:18333 [test] daemon=1 -bind=0.0.0.0:18333 -bind=[::]:18333 rpcbind=127.0.0.1:18332 rpcbind=[::1]:18332 +bind=0.0.0.0:18333 +bind=[::]:18333 +zmqpubrawblock=tcp://127.0.0.1:18334 +zmqpubrawtx=tcp://127.0.0.1:18335 +#addnode=[2401:b140:1::92:201]:18333 +#addnode=[2401:b140:1::92:202]:18333 +#addnode=[2401:b140:1::92:203]:18333 +#addnode=[2401:b140:1::92:204]:18333 +#addnode=[2401:b140:1::92:205]:18333 +#addnode=[2401:b140:1::92:206]:18333 #addnode=[2401:b140:2::92:201]:18333 #addnode=[2401:b140:2::92:202]:18333 #addnode=[2401:b140:2::92:203]:18333 @@ -46,10 +60,18 @@ rpcbind=[::1]:18332 [signet] daemon=1 -bind=0.0.0.0:38333 -bind=[::]:38333 rpcbind=127.0.0.1:38332 rpcbind=[::1]:38332 +bind=0.0.0.0:38333 +bind=[::]:38333 +zmqpubrawblock=tcp://127.0.0.1:38334 +zmqpubrawtx=tcp://127.0.0.1:38335 +#addnode=[2401:b140:1::92:201]:38333 +#addnode=[2401:b140:1::92:202]:38333 +#addnode=[2401:b140:1::92:203]:38333 +#addnode=[2401:b140:1::92:204]:38333 +#addnode=[2401:b140:1::92:205]:38333 +#addnode=[2401:b140:1::92:206]:38333 #addnode=[2401:b140:2::92:201]:38333 #addnode=[2401:b140:2::92:202]:38333 #addnode=[2401:b140:2::92:203]:38333 From ad9c72230cc3fc4031e12e032c96db625981ec92 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Tue, 26 Jul 2022 20:47:03 +0000 Subject: [PATCH 032/105] Set opengraph tags directly in the front end --- frontend/src/app/app-routing.module.ts | 5 +- frontend/src/app/app.module.ts | 2 + .../src/app/components/app/app.component.ts | 2 + .../src/app/services/opengraph.service.ts | 61 +++++++++++++ frontend/src/app/services/seo.service.ts | 2 + unfurler/src/index.ts | 91 ++++++++----------- 6 files changed, 109 insertions(+), 54 deletions(-) create mode 100644 frontend/src/app/services/opengraph.service.ts diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index c9f7e19d4..3489869ec 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -290,7 +290,10 @@ let routes: Routes = [ children: [ { path: ':id', - component: BlockComponent + component: BlockComponent, + data: { + ogImage: true + } }, ], }, diff --git a/frontend/src/app/app.module.ts b/frontend/src/app/app.module.ts index 97c8f9957..b6b8859f6 100644 --- a/frontend/src/app/app.module.ts +++ b/frontend/src/app/app.module.ts @@ -10,6 +10,7 @@ import { EnterpriseService } from './services/enterprise.service'; import { WebsocketService } from './services/websocket.service'; import { AudioService } from './services/audio.service'; import { SeoService } from './services/seo.service'; +import { OpenGraphService } from './services/opengraph.service'; import { SharedModule } from './shared/shared.module'; import { StorageService } from './services/storage.service'; import { HttpCacheInterceptor } from './services/http-cache.interceptor'; @@ -36,6 +37,7 @@ import { CapAddressPipe } from './shared/pipes/cap-address-pipe/cap-address-pipe WebsocketService, AudioService, SeoService, + OpenGraphService, StorageService, EnterpriseService, LanguageService, diff --git a/frontend/src/app/components/app/app.component.ts b/frontend/src/app/components/app/app.component.ts index e060fae54..c96489454 100644 --- a/frontend/src/app/components/app/app.component.ts +++ b/frontend/src/app/components/app/app.component.ts @@ -2,6 +2,7 @@ import { Location } from '@angular/common'; import { Component, HostListener, OnInit, Inject, LOCALE_ID, HostBinding } from '@angular/core'; import { Router, NavigationEnd } from '@angular/router'; import { StateService } from 'src/app/services/state.service'; +import { OpenGraphService } from 'src/app/services/opengraph.service'; import { NgbTooltipConfig } from '@ng-bootstrap/ng-bootstrap'; @Component({ @@ -16,6 +17,7 @@ export class AppComponent implements OnInit { constructor( public router: Router, private stateService: StateService, + private openGraphService: OpenGraphService, private location: Location, tooltipConfig: NgbTooltipConfig, @Inject(LOCALE_ID) private locale: string, diff --git a/frontend/src/app/services/opengraph.service.ts b/frontend/src/app/services/opengraph.service.ts new file mode 100644 index 000000000..48064fdea --- /dev/null +++ b/frontend/src/app/services/opengraph.service.ts @@ -0,0 +1,61 @@ +import { Injectable } from '@angular/core'; +import { Meta } from '@angular/platform-browser'; +import { Router, ActivatedRoute, NavigationEnd } from '@angular/router'; +import { filter, map, switchMap } from 'rxjs/operators'; +import { combineLatest } from 'rxjs'; +import { StateService } from './state.service'; +import { LanguageService } from './language.service'; + +@Injectable({ + providedIn: 'root' +}) +export class OpenGraphService { + network = ''; + defaultImageUrl = ''; + + constructor( + private metaService: Meta, + private stateService: StateService, + private LanguageService: LanguageService, + private router: Router, + private activatedRoute: ActivatedRoute, + ) { + // save og:image tag from original template + const initialOgImageTag = metaService.getTag("property='og:image'"); + this.defaultImageUrl = initialOgImageTag?.content || 'https://mempool.space/resources/mempool-space-preview.png'; + this.router.events.pipe( + filter(event => event instanceof NavigationEnd), + map(() => this.activatedRoute), + map(route => { + while (route.firstChild) route = route.firstChild; + return route; + }), + filter(route => route.outlet === 'primary'), + switchMap(route => route.data), + ).subscribe((data) => { + if (data.ogImage) { + this.setOgImage(); + } else { + this.clearOgImage(); + } + }); + } + + setOgImage() { + const lang = this.LanguageService.getLanguage(); + const ogImageUrl = `${window.location.protocol}//${window.location.host}/render/${lang}/preview${this.router.url}`; + this.metaService.updateTag({ property: 'og:image', content: ogImageUrl }); + this.metaService.updateTag({ property: 'twitter:image:src', content: ogImageUrl }); + this.metaService.updateTag({ property: 'og:image:type', content: 'image/png' }); + this.metaService.updateTag({ property: 'og:image:width', content: '1024' }); + this.metaService.updateTag({ property: 'og:image:height', content: '512' }); + } + + clearOgImage() { + this.metaService.updateTag({ property: 'og:image', content: this.defaultImageUrl }); + this.metaService.updateTag({ property: 'twitter:image:src', content: this.defaultImageUrl }); + this.metaService.updateTag({ property: 'og:image:type', content: 'image/png' }); + this.metaService.updateTag({ property: 'og:image:width', content: '1000' }); + this.metaService.updateTag({ property: 'og:image:height', content: '500' }); + } +} diff --git a/frontend/src/app/services/seo.service.ts b/frontend/src/app/services/seo.service.ts index 772c0410a..af96dc81b 100644 --- a/frontend/src/app/services/seo.service.ts +++ b/frontend/src/app/services/seo.service.ts @@ -20,11 +20,13 @@ export class SeoService { setTitle(newTitle: string): void { this.titleService.setTitle(newTitle + ' - ' + this.getTitle()); this.metaService.updateTag({ property: 'og:title', content: newTitle}); + this.metaService.updateTag({ property: 'twitter:description', content: newTitle}); } resetTitle(): void { this.titleService.setTitle(this.getTitle()); this.metaService.updateTag({ property: 'og:title', content: this.getTitle()}); + this.metaService.updateTag({ property: 'twitter:description', content: this.getTitle()}); } setEnterpriseTitle(title: string) { diff --git a/unfurler/src/index.ts b/unfurler/src/index.ts index 8d0011a44..31e4d423b 100644 --- a/unfurler/src/index.ts +++ b/unfurler/src/index.ts @@ -36,7 +36,7 @@ class Server { maxConcurrency: config.PUPPETEER.CLUSTER_SIZE, puppeteerOptions: puppeteerConfig, }); - await this.cluster?.task(async (args) => { return this.renderPreviewTask(args) }); + await this.cluster?.task(async (args) => { return this.clusterTask(args) }); this.setUpRoutes(); @@ -52,31 +52,40 @@ class Server { this.app.get('*', (req, res) => { return this.renderHTML(req, res) }) } - async renderPreviewTask({ page, data: url }) { + async clusterTask({ page, data: { url, action } }) { await page.goto(url, { waitUntil: "domcontentloaded" }); - await page.evaluate(async () => { - // wait for all images to finish loading - const imgs = Array.from(document.querySelectorAll("img")); - await Promise.all([ - document.fonts.ready, - ...imgs.map((img) => { - if (img.complete) { - if (img.naturalHeight !== 0) return; - throw new Error("Image failed to load"); - } - return new Promise((resolve, reject) => { - img.addEventListener("load", resolve); - img.addEventListener("error", reject); - }); - }), - ]); - }); - return page.screenshot(); + switch (action) { + case 'screenshot': { + await page.evaluate(async () => { + // wait for all images to finish loading + const imgs = Array.from(document.querySelectorAll("img")); + await Promise.all([ + document.fonts.ready, + ...imgs.map((img) => { + if (img.complete) { + if (img.naturalHeight !== 0) return; + throw new Error("Image failed to load"); + } + return new Promise((resolve, reject) => { + img.addEventListener("load", resolve); + img.addEventListener("error", reject); + }); + }), + ]); + }); + return page.screenshot(); + } break; + default: { + return page.content(); + } + } } async renderPreview(req, res) { try { - const img = await this.cluster?.execute(this.mempoolHost + req.params[0]); + // strip default language code for compatibility + const path = req.params[0].replace('/en/', '/'); + const img = await this.cluster?.execute({ url: this.mempoolHost + path, action: 'screenshot' }); res.contentType('image/png'); res.send(img); @@ -86,39 +95,15 @@ class Server { } } - renderHTML(req, res) { - let lang = ''; - let path = req.originalUrl - // extract the language setting (if any) - const parts = path.split(/^\/(ar|bg|bs|ca|cs|da|de|et|el|es|eo|eu|fa|fr|gl|ko|hr|id|it|he|ka|lv|lt|hu|mk|ms|nl|ja|nb|nn|pl|pt|pt-BR|ro|ru|sk|sl|sr|sh|fi|sv|th|tr|uk|vi|zh|hi)\//) - if (parts.length > 1) { - lang = "/" + parts[1]; - path = "/" + parts[2]; + async renderHTML(req, res) { + try { + let html = await this.cluster?.execute({ url: this.mempoolHost + req.params[0], action: 'html' }); + + res.send(html) + } catch (e) { + console.log(e); + res.status(500).send(e instanceof Error ? e.message : e); } - const ogImageUrl = config.SERVER.HOST + '/render' + lang + "/preview" + path; - res.send(` - - - - - mempool - Bitcoin Explorer - - - - - - - - - - - - - - - - - `); } } From debfd8ed3861d647d53fe3a04297183aee65c167 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Tue, 26 Jul 2022 21:21:15 +0000 Subject: [PATCH 033/105] Add block link previews for other networks --- frontend/src/app/app-routing.module.ts | 42 +++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 3489869ec..28faa9595 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -87,7 +87,10 @@ let routes: Routes = [ children: [ { path: ':id', - component: BlockComponent + component: BlockComponent, + data: { + ogImage: true + } }, ], }, @@ -190,7 +193,10 @@ let routes: Routes = [ children: [ { path: ':id', - component: BlockComponent + component: BlockComponent, + data: { + ogImage: true + } }, ], }, @@ -328,6 +334,14 @@ let routes: Routes = [ path: 'block/:id', component: BlockPreviewComponent }, + { + path: 'testnet/block/:id', + component: BlockPreviewComponent + }, + { + path: 'signet/block/:id', + component: BlockPreviewComponent + }, ], }, { @@ -419,7 +433,10 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { children: [ { path: ':id', - component: BlockComponent + component: BlockComponent, + data: { + ogImage: true + } }, ], }, @@ -523,7 +540,10 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { children: [ { path: ':id', - component: BlockComponent + component: BlockComponent, + data: { + ogImage: true + } }, ], }, @@ -563,6 +583,20 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { }, ], }, + { + path: 'preview', + component: MasterPagePreviewComponent, + children: [ + { + path: 'block/:id', + component: BlockPreviewComponent + }, + { + path: 'testnet/block/:id', + component: BlockPreviewComponent + }, + ], + }, { path: 'status', component: StatusViewComponent From 62258a07b48dbd93ac92eaee654beaa337a5f1ee Mon Sep 17 00:00:00 2001 From: Mononaut Date: Tue, 26 Jul 2022 21:50:38 +0000 Subject: [PATCH 034/105] Improve block link preview legibility --- .../block/block-preview.component.html | 10 -------- .../block/block-preview.component.scss | 24 +++++++++++++++++-- .../master-page-preview.component.scss | 1 + 3 files changed, 23 insertions(+), 12 deletions(-) diff --git a/frontend/src/app/components/block/block-preview.component.html b/frontend/src/app/components/block/block-preview.component.html index 5cfc31c70..c47ea236e 100644 --- a/frontend/src/app/components/block/block-preview.component.html +++ b/frontend/src/app/components/block/block-preview.component.html @@ -26,10 +26,6 @@ {{ block?.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }} - - Size - - Weight @@ -51,12 +47,6 @@ - - Subsidy + fees: - - - - Miner diff --git a/frontend/src/app/components/block/block-preview.component.scss b/frontend/src/app/components/block/block-preview.component.scss index fb51413e3..6099f5d47 100644 --- a/frontend/src/app/components/block/block-preview.component.scss +++ b/frontend/src/app/components/block/block-preview.component.scss @@ -1,7 +1,27 @@ .box { - padding: 2rem 6rem; + padding: 2rem 3rem; } .block-title { - margin-bottom: 0.5em; + margin-bottom: 0.75em; + font-size: 42px; + + ::ng-deep .next-previous-blocks { + font-size: 42px; + } +} + +.table { + font-size: 24px; +} + +.chart-container { + flex-grow: 0; + flex-shrink: 0; + width: 420px; + min-width: 420px; +} + +::ng-deep .symbol { + font-size: 18px; } diff --git a/frontend/src/app/components/master-page-preview/master-page-preview.component.scss b/frontend/src/app/components/master-page-preview/master-page-preview.component.scss index eebb9a75b..0384e0f86 100644 --- a/frontend/src/app/components/master-page-preview/master-page-preview.component.scss +++ b/frontend/src/app/components/master-page-preview/master-page-preview.component.scss @@ -20,6 +20,7 @@ align-items: center; background: #11131f; text-align: start; + font-size: 1.2em; } .footer-brand { From 7b094d9a34b655f64066f33a0d206813c6fc97df Mon Sep 17 00:00:00 2001 From: Mononaut Date: Wed, 27 Jul 2022 00:55:17 +0000 Subject: [PATCH 035/105] Add open graph link titles --- frontend/src/app/services/seo.service.ts | 4 ++-- unfurler/src/index.ts | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/services/seo.service.ts b/frontend/src/app/services/seo.service.ts index af96dc81b..01ed7ae8c 100644 --- a/frontend/src/app/services/seo.service.ts +++ b/frontend/src/app/services/seo.service.ts @@ -20,13 +20,13 @@ export class SeoService { setTitle(newTitle: string): void { this.titleService.setTitle(newTitle + ' - ' + this.getTitle()); this.metaService.updateTag({ property: 'og:title', content: newTitle}); - this.metaService.updateTag({ property: 'twitter:description', content: newTitle}); + this.metaService.updateTag({ property: 'twitter:title', content: newTitle}); } resetTitle(): void { this.titleService.setTitle(this.getTitle()); this.metaService.updateTag({ property: 'og:title', content: this.getTitle()}); - this.metaService.updateTag({ property: 'twitter:description', content: this.getTitle()}); + this.metaService.updateTag({ property: 'twitter:title', content: this.getTitle()}); } setEnterpriseTitle(title: string) { diff --git a/unfurler/src/index.ts b/unfurler/src/index.ts index 31e4d423b..998beb1eb 100644 --- a/unfurler/src/index.ts +++ b/unfurler/src/index.ts @@ -76,6 +76,11 @@ class Server { return page.screenshot(); } break; default: { + try { + await page.waitForSelector('meta[property="og:title"', { timeout: 5000 }) + } catch (e) { + // probably timed out + } return page.content(); } } @@ -96,6 +101,14 @@ class Server { } async renderHTML(req, res) { + // drop requests for static files + const path = req.params[0]; + const match = path.match(/\.[\w]+$/); + if (match?.length && match[0] !== '.html') { + res.status(404).send(); + return + } + try { let html = await this.cluster?.execute({ url: this.mempoolHost + req.params[0], action: 'html' }); From 992e3fa7b993ab895cfd67723fb9fa2cb7f27832 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Wed, 27 Jul 2022 10:48:27 +0200 Subject: [PATCH 036/105] Create shared toggle component to re-use --- .../app/lightning/node/node.component.html | 9 +-- .../app/lightning/node/node.component.scss | 65 +------------------ .../src/app/lightning/node/node.component.ts | 4 +- .../components/toggle/toggle.component.html | 8 +++ .../components/toggle/toggle.component.scss | 62 ++++++++++++++++++ .../components/toggle/toggle.component.ts | 17 +++++ frontend/src/app/shared/shared.module.ts | 3 + 7 files changed, 94 insertions(+), 74 deletions(-) create mode 100644 frontend/src/app/shared/components/toggle/toggle.component.html create mode 100644 frontend/src/app/shared/components/toggle/toggle.component.scss create mode 100644 frontend/src/app/shared/components/toggle/toggle.component.ts diff --git a/frontend/src/app/lightning/node/node.component.html b/frontend/src/app/lightning/node/node.component.html index 774f0aaab..af6fca655 100644 --- a/frontend/src/app/lightning/node/node.component.html +++ b/frontend/src/app/lightning/node/node.component.html @@ -140,14 +140,7 @@

Channels ({{ channelsListStatus === 'open' ? node.channel_active_count : node.channel_closed_count }})

-
- List  - -  Map -
+
diff --git a/frontend/src/app/lightning/node/node.component.scss b/frontend/src/app/lightning/node/node.component.scss index 0bdb263a8..2b171416f 100644 --- a/frontend/src/app/lightning/node/node.component.scss +++ b/frontend/src/app/lightning/node/node.component.scss @@ -56,67 +56,4 @@ app-fiat { display: inline-block; margin-left: 10px; } -} - - /* The switch - the box around the slider */ - .switch { - position: relative; - display: inline-block; - width: 30px; - height: 17px; -} - -/* Hide default HTML checkbox */ -.switch input { - opacity: 0; - width: 0; - height: 0; -} - -/* The slider */ -.slider { - position: absolute; - cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; - background-color: #ccc; - -webkit-transition: .4s; - transition: .4s; -} - -.slider:before { - position: absolute; - content: ""; - height: 13px; - width: 13px; - left: 2px; - bottom: 2px; - background-color: white; - -webkit-transition: .4s; - transition: .4s; -} - -input:checked + .slider { - background-color: #2196F3; -} - -input:focus + .slider { - box-shadow: 0 0 1px #2196F3; -} - -input:checked + .slider:before { - -webkit-transform: translateX(13px); - -ms-transform: translateX(13px); - transform: translateX(13px); -} - -/* Rounded sliders */ -.slider.round { - border-radius: 17px; -} - -.slider.round:before { - border-radius: 50%; -} \ No newline at end of file +} \ No newline at end of file diff --git a/frontend/src/app/lightning/node/node.component.ts b/frontend/src/app/lightning/node/node.component.ts index c9971a4cb..a8d487938 100644 --- a/frontend/src/app/lightning/node/node.component.ts +++ b/frontend/src/app/lightning/node/node.component.ts @@ -75,8 +75,8 @@ export class NodeComponent implements OnInit { this.selectedSocketIndex = index; } - channelsListModeChange(e) { - if (e.target.checked === true) { + channelsListModeChange(toggle) { + if (toggle === true) { this.channelsListMode = 'map'; } else { this.channelsListMode = 'list'; diff --git a/frontend/src/app/shared/components/toggle/toggle.component.html b/frontend/src/app/shared/components/toggle/toggle.component.html new file mode 100644 index 000000000..6f8889501 --- /dev/null +++ b/frontend/src/app/shared/components/toggle/toggle.component.html @@ -0,0 +1,8 @@ +
+ {{ textLeft }}  + +  {{ textRight }} +
diff --git a/frontend/src/app/shared/components/toggle/toggle.component.scss b/frontend/src/app/shared/components/toggle/toggle.component.scss new file mode 100644 index 000000000..a9c221290 --- /dev/null +++ b/frontend/src/app/shared/components/toggle/toggle.component.scss @@ -0,0 +1,62 @@ +/* The switch - the box around the slider */ +.switch { + position: relative; + display: inline-block; + width: 30px; + height: 17px; +} + +/* Hide default HTML checkbox */ +.switch input { + opacity: 0; + width: 0; + height: 0; +} + +/* The slider */ +.slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + -webkit-transition: .4s; + transition: .4s; +} + +.slider:before { + position: absolute; + content: ""; + height: 13px; + width: 13px; + left: 2px; + bottom: 2px; + background-color: white; + -webkit-transition: .4s; + transition: .4s; +} + +input:checked+.slider { + background-color: #2196F3; +} + +input:focus+.slider { + box-shadow: 0 0 1px #2196F3; +} + +input:checked+.slider:before { + -webkit-transform: translateX(13px); + -ms-transform: translateX(13px); + transform: translateX(13px); +} + +/* Rounded sliders */ +.slider.round { + border-radius: 17px; +} + +.slider.round:before { + border-radius: 50%; +} \ No newline at end of file diff --git a/frontend/src/app/shared/components/toggle/toggle.component.ts b/frontend/src/app/shared/components/toggle/toggle.component.ts new file mode 100644 index 000000000..a69957366 --- /dev/null +++ b/frontend/src/app/shared/components/toggle/toggle.component.ts @@ -0,0 +1,17 @@ +import { Component, Input, Output, ChangeDetectionStrategy, EventEmitter } from '@angular/core'; + +@Component({ + selector: 'app-toggle', + templateUrl: './toggle.component.html', + styleUrls: ['./toggle.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ToggleComponent { + @Output() toggleStatusChanged = new EventEmitter(); + @Input() textLeft: string; + @Input() textRight: string; + + onToggleStatusChanged(e): void { + this.toggleStatusChanged.emit(e.target.checked); + } +} diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index cd087a3c4..df071033e 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -80,6 +80,7 @@ import { ChangeComponent } from '../components/change/change.component'; import { SatsComponent } from './components/sats/sats.component'; import { SearchResultsComponent } from '../components/search-form/search-results/search-results.component'; import { TimestampComponent } from './components/timestamp/timestamp.component'; +import { ToggleComponent } from './components/toggle/toggle.component'; @NgModule({ declarations: [ @@ -154,6 +155,7 @@ import { TimestampComponent } from './components/timestamp/timestamp.component'; SatsComponent, SearchResultsComponent, TimestampComponent, + ToggleComponent, ], imports: [ CommonModule, @@ -255,6 +257,7 @@ import { TimestampComponent } from './components/timestamp/timestamp.component'; SatsComponent, SearchResultsComponent, TimestampComponent, + ToggleComponent, ] }) export class SharedModule { From 91c39b57fe614ff38a3f7ac21f8106b10524fdf9 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Wed, 27 Jul 2022 13:20:54 +0200 Subject: [PATCH 037/105] [LN ISP chart] Adds toogle to order by nodes/capacity and show/hide Tor --- backend/src/api/explorer/nodes.api.ts | 48 ++++++++++++--- backend/src/api/explorer/nodes.routes.ts | 17 ++++-- .../app/lightning/node/node.component.html | 4 +- .../nodes-per-isp-chart.component.html | 10 ++- .../nodes-per-isp-chart.component.scss | 14 ++++- .../nodes-per-isp-chart.component.ts | 61 ++++++++++++------- frontend/src/app/services/api.service.ts | 5 +- .../components/toggle/toggle.component.html | 2 +- .../components/toggle/toggle.component.ts | 8 ++- 9 files changed, 123 insertions(+), 46 deletions(-) diff --git a/backend/src/api/explorer/nodes.api.ts b/backend/src/api/explorer/nodes.api.ts index 9899e20fc..517ab82f6 100644 --- a/backend/src/api/explorer/nodes.api.ts +++ b/backend/src/api/explorer/nodes.api.ts @@ -98,29 +98,59 @@ class NodesApi { } } - public async $getNodesISP() { + public async $getNodesISP(groupBy: string, showTor: boolean) { try { - let query = `SELECT GROUP_CONCAT(DISTINCT(nodes.as_number)) as ispId, geo_names.names as names, COUNT(DISTINCT nodes.public_key) as nodesCount, SUM(capacity) as capacity + const orderBy = groupBy === 'capacity' ? `CAST(SUM(capacity) as INT)` : `COUNT(DISTINCT nodes.public_key)`; + + // Clearnet + let query = `SELECT GROUP_CONCAT(DISTINCT(nodes.as_number)) as ispId, geo_names.names as names, + COUNT(DISTINCT nodes.public_key) as nodesCount, CAST(SUM(capacity) as INT) as capacity FROM nodes JOIN geo_names ON geo_names.id = nodes.as_number JOIN channels ON channels.node1_public_key = nodes.public_key OR channels.node2_public_key = nodes.public_key GROUP BY geo_names.names - ORDER BY COUNT(DISTINCT nodes.public_key) DESC - `; + ORDER BY ${orderBy} DESC + `; const [nodesCountPerAS]: any = await DB.query(query); - query = `SELECT COUNT(*) as total FROM nodes WHERE as_number IS NOT NULL`; - const [nodesWithAS]: any = await DB.query(query); - + let total = 0; const nodesPerAs: any[] = []; + + for (const asGroup of nodesCountPerAS) { + if (groupBy === 'capacity') { + total += asGroup.capacity; + } else { + total += asGroup.nodesCount; + } + } + + // Tor + if (showTor) { + query = `SELECT COUNT(DISTINCT nodes.public_key) as nodesCount, CAST(SUM(capacity) as INT) as capacity + FROM nodes + JOIN channels ON channels.node1_public_key = nodes.public_key OR channels.node2_public_key = nodes.public_key + ORDER BY ${orderBy} DESC + `; + const [nodesCountTor]: any = await DB.query(query); + + total += groupBy === 'capacity' ? nodesCountTor[0].capacity : nodesCountTor[0].nodesCount; + nodesPerAs.push({ + ispId: null, + name: 'Tor', + count: nodesCountTor[0].nodesCount, + share: Math.floor((groupBy === 'capacity' ? nodesCountTor[0].capacity : nodesCountTor[0].nodesCount) / total * 10000) / 100, + capacity: nodesCountTor[0].capacity, + }); + } + for (const as of nodesCountPerAS) { nodesPerAs.push({ ispId: as.ispId, name: JSON.parse(as.names), count: as.nodesCount, - share: Math.floor(as.nodesCount / nodesWithAS[0].total * 10000) / 100, + share: Math.floor((groupBy === 'capacity' ? as.capacity : as.nodesCount) / total * 10000) / 100, capacity: as.capacity, - }) + }); } return nodesPerAs; diff --git a/backend/src/api/explorer/nodes.routes.ts b/backend/src/api/explorer/nodes.routes.ts index bbc8efb5a..83e3c393e 100644 --- a/backend/src/api/explorer/nodes.routes.ts +++ b/backend/src/api/explorer/nodes.routes.ts @@ -9,10 +9,10 @@ class NodesRoutes { public initRoutes(app: Application) { app .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/country/:country', this.$getNodesPerCountry) - .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/isp/:isp', this.$getNodesPerISP) .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/search/:search', this.$searchNode) .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/top', this.$getTopNodes) - .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/isp', this.$getNodesISP) + .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/isp-ranking', this.$getISPRanking) + .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/isp/:isp', this.$getNodesPerISP) .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/countries', this.$getNodesCountries) .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key/statistics', this.$getHistoricalNodeStats) .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key', this.$getNode) @@ -63,9 +63,18 @@ class NodesRoutes { } } - private async $getNodesISP(req: Request, res: Response) { + private async $getISPRanking(req: Request, res: Response): Promise { try { - const nodesPerAs = await nodesApi.$getNodesISP(); + const groupBy = req.query.groupBy as string; + const showTor = req.query.showTor as string === 'true' ? true : false; + + if (!['capacity', 'node-count'].includes(groupBy)) { + res.status(400).send(`groupBy must be one of 'capacity' or 'node-count'`); + return; + } + + const nodesPerAs = await nodesApi.$getNodesISP(groupBy, showTor); + res.header('Pragma', 'public'); res.header('Cache-control', 'public'); res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString()); diff --git a/frontend/src/app/lightning/node/node.component.html b/frontend/src/app/lightning/node/node.component.html index af6fca655..cb0e5ed43 100644 --- a/frontend/src/app/lightning/node/node.component.html +++ b/frontend/src/app/lightning/node/node.component.html @@ -140,7 +140,9 @@

Channels ({{ channelsListStatus === 'open' ? node.channel_active_count : node.channel_closed_count }})

- +
+ +
diff --git a/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.html b/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.html index 28d314b9c..a80b8665e 100644 --- a/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.html +++ b/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.html @@ -21,6 +21,11 @@
+
+ + +
+ @@ -34,8 +39,9 @@ - diff --git a/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.scss b/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.scss index 8e9a9903b..10ad39372 100644 --- a/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.scss +++ b/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.scss @@ -45,7 +45,7 @@ .name { width: 25%; @media (max-width: 576px) { - width: 80%; + width: 70%; max-width: 150px; padding-left: 0; padding-right: 0; @@ -69,7 +69,17 @@ .capacity { width: 20%; @media (max-width: 576px) { - width: 10%; + width: 20%; max-width: 100px; } +} + +.toggle { + justify-content: space-between; + padding-top: 15px; + @media (min-width: 576px) { + padding-bottom: 15px; + padding-left: 105px; + padding-right: 105px; + } } \ No newline at end of file diff --git a/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.ts b/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.ts index 63665f69a..f6d876e6b 100644 --- a/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.ts +++ b/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectionStrategy, Component, OnInit, HostBinding, NgZone } from '@angular/core'; import { Router } from '@angular/router'; import { EChartsOption, PieSeriesOption } from 'echarts'; -import { map, Observable, share, tap } from 'rxjs'; +import { combineLatest, map, Observable, share, Subject, switchMap, tap } from 'rxjs'; import { chartColors } from 'src/app/app.constants'; import { ApiService } from 'src/app/services/api.service'; import { SeoService } from 'src/app/services/seo.service'; @@ -17,19 +17,19 @@ import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url. changeDetection: ChangeDetectionStrategy.OnPush, }) export class NodesPerISPChartComponent implements OnInit { - miningWindowPreference: string; - isLoading = true; chartOptions: EChartsOption = {}; chartInitOptions = { renderer: 'svg', }; timespan = ''; - chartInstance: any = undefined; + chartInstance = undefined; @HostBinding('attr.dir') dir = 'ltr'; nodesPerAsObservable$: Observable; + groupBySubject = new Subject(); + showTorSubject = new Subject(); constructor( private apiService: ApiService, @@ -44,23 +44,31 @@ export class NodesPerISPChartComponent implements OnInit { ngOnInit(): void { this.seoService.setTitle($localize`Lightning nodes per ISP`); - this.nodesPerAsObservable$ = this.apiService.getNodesPerAs() + this.nodesPerAsObservable$ = combineLatest([this.groupBySubject, this.showTorSubject]) .pipe( - tap(data => { - this.isLoading = false; - this.prepareChartOptions(data); - }), - map(data => { - for (let i = 0; i < data.length; ++i) { - data[i].rank = i + 1; - } - return data.slice(0, 100); + switchMap((selectedFilters) => { + return this.apiService.getNodesPerAs( + selectedFilters[0] ? 'capacity' : 'node-count', + selectedFilters[1] // Show Tor nodes + ) + .pipe( + tap(data => { + this.isLoading = false; + this.prepareChartOptions(data); + }), + map(data => { + for (let i = 0; i < data.length; ++i) { + data[i].rank = i + 1; + } + return data.slice(0, 100); + }) + ); }), share() ); } - generateChartSerieData(as) { + generateChartSerieData(as): PieSeriesOption[] { const shareThreshold = this.isMobile() ? 2 : 0.5; const data: object[] = []; let totalShareOther = 0; @@ -78,6 +86,9 @@ export class NodesPerISPChartComponent implements OnInit { return; } data.push({ + itemStyle: { + color: as.ispId === null ? '#7D4698' : undefined, + }, value: as.share, name: as.name + (this.isMobile() ? `` : ` (${as.share}%)`), label: { @@ -138,14 +149,14 @@ export class NodesPerISPChartComponent implements OnInit { return data; } - prepareChartOptions(as) { + prepareChartOptions(as): void { let pieSize = ['20%', '80%']; // Desktop if (this.isMobile()) { pieSize = ['15%', '60%']; } this.chartOptions = { - color: chartColors, + color: chartColors.slice(3), tooltip: { trigger: 'item', textStyle: { @@ -191,18 +202,18 @@ export class NodesPerISPChartComponent implements OnInit { }; } - isMobile() { + isMobile(): boolean { return (window.innerWidth <= 767.98); } - onChartInit(ec) { + onChartInit(ec): void { if (this.chartInstance !== undefined) { return; } this.chartInstance = ec; this.chartInstance.on('click', (e) => { - if (e.data.data === 9999) { // "Other" + if (e.data.data === 9999 || e.data.data === null) { // "Other" or Tor return; } this.zone.run(() => { @@ -212,7 +223,7 @@ export class NodesPerISPChartComponent implements OnInit { }); } - onSaveChart() { + onSaveChart(): void { const now = new Date(); this.chartOptions.backgroundColor = '#11131f'; this.chartInstance.setOption(this.chartOptions); @@ -224,8 +235,12 @@ export class NodesPerISPChartComponent implements OnInit { this.chartInstance.setOption(this.chartOptions); } - isEllipsisActive(e) { - return (e.offsetWidth < e.scrollWidth); + onTorToggleStatusChanged(e): void { + this.showTorSubject.next(e); + } + + onGroupToggleStatusChanged(e): void { + this.groupBySubject.next(e); } } diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index fdb2714bd..844451574 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -255,8 +255,9 @@ export class ApiService { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/search', { params }); } - getNodesPerAs(): Observable { - return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/isp'); + getNodesPerAs(groupBy: 'capacity' | 'node-count', showTorNodes: boolean): Observable { + return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/isp-ranking' + + `?groupBy=${groupBy}&showTor=${showTorNodes}`); } getNodeForCountry$(country: string): Observable { diff --git a/frontend/src/app/shared/components/toggle/toggle.component.html b/frontend/src/app/shared/components/toggle/toggle.component.html index 6f8889501..dac33c9d8 100644 --- a/frontend/src/app/shared/components/toggle/toggle.component.html +++ b/frontend/src/app/shared/components/toggle/toggle.component.html @@ -1,4 +1,4 @@ -
+
{{ textLeft }} 
- (Tor nodes excluded) + + (Tor nodes excluded) +
diff --git a/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.ts b/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.ts index f6d876e6b..6b9d41e74 100644 --- a/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.ts +++ b/frontend/src/app/lightning/nodes-per-isp-chart/nodes-per-isp-chart.component.ts @@ -28,6 +28,7 @@ export class NodesPerISPChartComponent implements OnInit { @HostBinding('attr.dir') dir = 'ltr'; nodesPerAsObservable$: Observable; + showTorObservable$: Observable; groupBySubject = new Subject(); showTorSubject = new Subject(); @@ -44,6 +45,7 @@ export class NodesPerISPChartComponent implements OnInit { ngOnInit(): void { this.seoService.setTitle($localize`Lightning nodes per ISP`); + this.showTorObservable$ = this.showTorSubject.asObservable(); this.nodesPerAsObservable$ = combineLatest([this.groupBySubject, this.showTorSubject]) .pipe( switchMap((selectedFilters) => { From d532ab93eac0f49248581095ea40402546b63ee3 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Wed, 27 Jul 2022 16:19:47 +0200 Subject: [PATCH 039/105] [LN stats] Ony consider channel stats = 1 for stats calculation --- backend/src/api/explorer/nodes.api.ts | 6 +++--- .../tasks/lightning/stats-updater.service.ts | 17 ++++++++++++++++- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/backend/src/api/explorer/nodes.api.ts b/backend/src/api/explorer/nodes.api.ts index 9899e20fc..dc44a04a1 100644 --- a/backend/src/api/explorer/nodes.api.ts +++ b/backend/src/api/explorer/nodes.api.ts @@ -12,13 +12,13 @@ class NodesApi { WHERE channels.status = 2 AND ( channels.node1_public_key = ? OR channels.node2_public_key = ? )) AS channel_closed_count, (SELECT Count(*) FROM channels - WHERE channels.status < 2 AND ( channels.node1_public_key = ? OR channels.node2_public_key = ? )) AS channel_active_count, + WHERE channels.status = 1 AND ( channels.node1_public_key = ? OR channels.node2_public_key = ? )) AS channel_active_count, (SELECT Sum(capacity) FROM channels - WHERE channels.status < 2 AND ( channels.node1_public_key = ? OR channels.node2_public_key = ? )) AS capacity, + WHERE channels.status = 1 AND ( channels.node1_public_key = ? OR channels.node2_public_key = ? )) AS capacity, (SELECT Avg(capacity) FROM channels - WHERE status < 2 AND ( node1_public_key = ? OR node2_public_key = ? )) AS channels_capacity_avg + WHERE status = 1 AND ( node1_public_key = ? OR node2_public_key = ? )) AS channels_capacity_avg FROM nodes LEFT JOIN geo_names geo_names_as on geo_names_as.id = as_number LEFT JOIN geo_names geo_names_city on geo_names_city.id = city_id diff --git a/backend/src/tasks/lightning/stats-updater.service.ts b/backend/src/tasks/lightning/stats-updater.service.ts index c56e8a015..f30da9e96 100644 --- a/backend/src/tasks/lightning/stats-updater.service.ts +++ b/backend/src/tasks/lightning/stats-updater.service.ts @@ -141,7 +141,22 @@ class LightningStatsUpdater { try { logger.info(`Running daily node stats update...`); - const query = `SELECT nodes.public_key, c1.channels_count_left, c2.channels_count_right, c1.channels_capacity_left, c2.channels_capacity_right FROM nodes LEFT JOIN (SELECT node1_public_key, COUNT(id) AS channels_count_left, SUM(capacity) AS channels_capacity_left FROM channels WHERE channels.status < 2 GROUP BY node1_public_key) c1 ON c1.node1_public_key = nodes.public_key LEFT JOIN (SELECT node2_public_key, COUNT(id) AS channels_count_right, SUM(capacity) AS channels_capacity_right FROM channels WHERE channels.status < 2 GROUP BY node2_public_key) c2 ON c2.node2_public_key = nodes.public_key`; + const query = ` + SELECT nodes.public_key, c1.channels_count_left, c2.channels_count_right, c1.channels_capacity_left, + c2.channels_capacity_right + FROM nodes + LEFT JOIN ( + SELECT node1_public_key, COUNT(id) AS channels_count_left, SUM(capacity) AS channels_capacity_left + FROM channels + WHERE channels.status = 1 + GROUP BY node1_public_key + ) c1 ON c1.node1_public_key = nodes.public_key + LEFT JOIN ( + SELECT node2_public_key, COUNT(id) AS channels_count_right, SUM(capacity) AS channels_capacity_right + FROM channels WHERE channels.status = 1 GROUP BY node2_public_key + ) c2 ON c2.node2_public_key = nodes.public_key + `; + const [nodes]: any = await DB.query(query); for (const node of nodes) { From 54d2d178fc6877d091d6c16ce9e8cbffa5b72f2f Mon Sep 17 00:00:00 2001 From: nymkappa Date: Wed, 27 Jul 2022 17:21:24 +0200 Subject: [PATCH 040/105] Don't set all channels to inactive when the updater runs --- .../src/tasks/lightning/node-sync.service.ts | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/backend/src/tasks/lightning/node-sync.service.ts b/backend/src/tasks/lightning/node-sync.service.ts index 3b2eb18e2..dd2c5868e 100644 --- a/backend/src/tasks/lightning/node-sync.service.ts +++ b/backend/src/tasks/lightning/node-sync.service.ts @@ -38,11 +38,13 @@ class NodeSyncService { await $lookupNodeLocation(); } - await this.$setChannelsInactive(); - + const graphChannelsIds: string[] = []; for (const channel of networkGraph.channels) { await this.$saveChannel(channel); + graphChannelsIds.push(channel.id); } + await this.$setChannelsInactive(graphChannelsIds); + logger.info(`Channels updated.`); await this.$findInactiveNodesAndChannels(); @@ -106,7 +108,22 @@ class NodeSyncService { try { // @ts-ignore - const [channels]: [ILightningApi.Channel[]] = await DB.query(`SELECT channels.id FROM channels WHERE channels.status = 1 AND ((SELECT COUNT(*) FROM nodes WHERE nodes.public_key = channels.node1_public_key) = 0 OR (SELECT COUNT(*) FROM nodes WHERE nodes.public_key = channels.node2_public_key) = 0)`); + const [channels]: [ILightningApi.Channel[]] = await DB.query(` + SELECT channels.id + FROM channels + WHERE channels.status = 1 + AND ( + ( + SELECT COUNT(*) + FROM nodes + WHERE nodes.public_key = channels.node1_public_key + ) = 0 + OR ( + SELECT COUNT(*) + FROM nodes + WHERE nodes.public_key = channels.node2_public_key + ) = 0) + `); for (const channel of channels) { await this.$updateChannelStatus(channel.id, 0); @@ -356,9 +373,15 @@ class NodeSyncService { } } - private async $setChannelsInactive(): Promise { + private async $setChannelsInactive(graphChannelsIds: string[]): Promise { try { - await DB.query(`UPDATE channels SET status = 0 WHERE status = 1`); + await DB.query(` + UPDATE channels + SET status = 0 + WHERE short_id NOT IN ( + ${graphChannelsIds.map(id => `"${id}"`).join(',')} + ) + `); } catch (e) { logger.err('$setChannelsInactive() error: ' + (e instanceof Error ? e.message : e)); } From f69a3beff18140df75d753d21c1998036ebadc33 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Wed, 27 Jul 2022 17:24:31 +0200 Subject: [PATCH 041/105] Don't mark closed channels as inactive --- backend/src/tasks/lightning/node-sync.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/src/tasks/lightning/node-sync.service.ts b/backend/src/tasks/lightning/node-sync.service.ts index dd2c5868e..f45473aba 100644 --- a/backend/src/tasks/lightning/node-sync.service.ts +++ b/backend/src/tasks/lightning/node-sync.service.ts @@ -381,6 +381,7 @@ class NodeSyncService { WHERE short_id NOT IN ( ${graphChannelsIds.map(id => `"${id}"`).join(',')} ) + AND status != 2 `); } catch (e) { logger.err('$setChannelsInactive() error: ' + (e instanceof Error ? e.message : e)); From ace33e39ca6a24957e129c1e5e5f6b25684cce43 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Wed, 27 Jul 2022 18:13:37 +0000 Subject: [PATCH 042/105] Add address link previews --- frontend/src/app/app-routing.module.ts | 51 +++++++- .../address/address-preview.component.html | 55 +++++++++ .../address/address-preview.component.scss | 46 +++++++ .../address/address-preview.component.ts | 116 ++++++++++++++++++ .../block/block-preview.component.scss | 4 - .../src/app/services/opengraph.service.ts | 10 ++ frontend/src/app/shared/shared.module.ts | 3 + frontend/src/styles.scss | 5 + unfurler/src/index.ts | 14 ++- 9 files changed, 293 insertions(+), 11 deletions(-) create mode 100644 frontend/src/app/components/address/address-preview.component.html create mode 100644 frontend/src/app/components/address/address-preview.component.scss create mode 100644 frontend/src/app/components/address/address-preview.component.ts diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 28faa9595..000a0f177 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -6,6 +6,7 @@ import { BlockComponent } from './components/block/block.component'; import { BlockAuditComponent } from './components/block-audit/block-audit.component'; import { BlockPreviewComponent } from './components/block/block-preview.component'; import { AddressComponent } from './components/address/address.component'; +import { AddressPreviewComponent } from './components/address/address-preview.component'; import { MasterPageComponent } from './components/master-page/master-page.component'; import { MasterPagePreviewComponent } from './components/master-page-preview/master-page-preview.component'; import { AboutComponent } from './components/about/about.component'; @@ -69,7 +70,10 @@ let routes: Routes = [ { path: 'address/:id', children: [], - component: AddressComponent + component: AddressComponent, + data: { + ogImage: true + } }, { path: 'tx', @@ -175,7 +179,10 @@ let routes: Routes = [ { path: 'address/:id', children: [], - component: AddressComponent + component: AddressComponent, + data: { + ogImage: true + } }, { path: 'tx', @@ -278,7 +285,10 @@ let routes: Routes = [ { path: 'address/:id', children: [], - component: AddressComponent + component: AddressComponent, + data: { + ogImage: true + } }, { path: 'tx', @@ -342,6 +352,21 @@ let routes: Routes = [ path: 'signet/block/:id', component: BlockPreviewComponent }, + { + path: 'address/:id', + children: [], + component: AddressPreviewComponent + }, + { + path: 'testnet/address/:id', + children: [], + component: AddressPreviewComponent + }, + { + path: 'signet/address/:id', + children: [], + component: AddressPreviewComponent + }, ], }, { @@ -415,7 +440,10 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { { path: 'address/:id', children: [], - component: AddressComponent + component: AddressComponent, + data: { + ogImage: true + } }, { path: 'tx', @@ -522,7 +550,10 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { { path: 'address/:id', children: [], - component: AddressComponent + component: AddressComponent, + data: { + ogImage: true + } }, { path: 'tx', @@ -595,6 +626,16 @@ if (browserWindowEnv && browserWindowEnv.BASE_MODULE === 'liquid') { path: 'testnet/block/:id', component: BlockPreviewComponent }, + { + path: 'address/:id', + children: [], + component: AddressPreviewComponent + }, + { + path: 'testnet/address/:id', + children: [], + component: AddressPreviewComponent + }, ], }, { diff --git a/frontend/src/app/components/address/address-preview.component.html b/frontend/src/app/components/address/address-preview.component.html new file mode 100644 index 000000000..bc73d064b --- /dev/null +++ b/frontend/src/app/components/address/address-preview.component.html @@ -0,0 +1,55 @@ +
{{ asEntry.rank }} - {{ asEntry.name }} + + {{ asEntry.name }} + {{ asEntry.name }} {{ asEntry.count }}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Unconfidential + {{ addressInfo.unconfidential | shortenString : 14 }} + {{ addressInfo.unconfidential }} +
Total received
Total sent
Balance
Transactions{{ txCount | number }}
Unspent TXOs{{ totalUnspent | number }}
+
+
+
+
+ +
+
+
+
+ + + Confidential + diff --git a/frontend/src/app/components/address/address-preview.component.scss b/frontend/src/app/components/address/address-preview.component.scss new file mode 100644 index 000000000..f286c6ca1 --- /dev/null +++ b/frontend/src/app/components/address/address-preview.component.scss @@ -0,0 +1,46 @@ +h1 { + font-size: 42px; + margin: 0; +} + +.qr-wrapper { + background-color: #FFF; + padding: 10px; + padding-bottom: 5px; + display: inline-block; +} + +.qrcode-col { + width: 420px; + min-width: 420px; + flex-grow: 0; + flex-shrink: 0; + text-align: center; +} + +.table { + font-size: 24px; + + ::ng-deep .symbol { + font-size: 18px; + } +} + +.address-link { + font-size: 20px; + margin-bottom: 0.5em; + display: flex; + flex-direction: row; + align-items: baseline; + .truncated-address { + text-overflow: ellipsis; + overflow: hidden; + max-width: calc(505px - 4em); + display: inline-block; + white-space: nowrap; + } + .last-four { + display: inline-block; + white-space: nowrap; + } +} diff --git a/frontend/src/app/components/address/address-preview.component.ts b/frontend/src/app/components/address/address-preview.component.ts new file mode 100644 index 000000000..c661c29db --- /dev/null +++ b/frontend/src/app/components/address/address-preview.component.ts @@ -0,0 +1,116 @@ +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { ActivatedRoute, ParamMap } from '@angular/router'; +import { ElectrsApiService } from '../../services/electrs-api.service'; +import { switchMap, filter, catchError, map, tap } from 'rxjs/operators'; +import { Address, Transaction } from '../../interfaces/electrs.interface'; +import { StateService } from 'src/app/services/state.service'; +import { OpenGraphService } from 'src/app/services/opengraph.service'; +import { AudioService } from 'src/app/services/audio.service'; +import { ApiService } from 'src/app/services/api.service'; +import { of, merge, Subscription, Observable } from 'rxjs'; +import { SeoService } from 'src/app/services/seo.service'; +import { AddressInformation } from 'src/app/interfaces/node-api.interface'; + +@Component({ + selector: 'app-address-preview', + templateUrl: './address-preview.component.html', + styleUrls: ['./address-preview.component.scss'] +}) +export class AddressPreviewComponent implements OnInit, OnDestroy { + network = ''; + + address: Address; + addressString: string; + isLoadingAddress = true; + error: any; + mainSubscription: Subscription; + addressLoadingStatus$: Observable; + addressInfo: null | AddressInformation = null; + + totalConfirmedTxCount = 0; + loadedConfirmedTxCount = 0; + txCount = 0; + received = 0; + sent = 0; + totalUnspent = 0; + + constructor( + private route: ActivatedRoute, + private electrsApiService: ElectrsApiService, + private stateService: StateService, + private apiService: ApiService, + private seoService: SeoService, + private openGraphService: OpenGraphService, + ) { } + + ngOnInit() { + this.openGraphService.setPreviewLoading(); + this.stateService.networkChanged$.subscribe((network) => this.network = network); + + this.addressLoadingStatus$ = this.route.paramMap + .pipe( + switchMap(() => this.stateService.loadingIndicators$), + map((indicators) => indicators['address-' + this.addressString] !== undefined ? indicators['address-' + this.addressString] : 0) + ); + + this.mainSubscription = this.route.paramMap + .pipe( + switchMap((params: ParamMap) => { + this.error = undefined; + this.isLoadingAddress = true; + this.loadedConfirmedTxCount = 0; + this.address = null; + this.addressInfo = null; + this.addressString = params.get('id') || ''; + if (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}$/.test(this.addressString)) { + this.addressString = this.addressString.toLowerCase(); + } + this.seoService.setTitle($localize`:@@address.component.browser-title:Address: ${this.addressString}:INTERPOLATION:`); + + return this.electrsApiService.getAddress$(this.addressString) + .pipe( + catchError((err) => { + this.isLoadingAddress = false; + this.error = err; + console.log(err); + return of(null); + }) + ); + }) + ) + .pipe( + filter((address) => !!address), + tap((address: Address) => { + if ((this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet') && /^([m-zA-HJ-NP-Z1-9]{26,35}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,100}|[a-km-zA-HJ-NP-Z1-9]{80})$/.test(address.address)) { + this.apiService.validateAddress$(address.address) + .subscribe((addressInfo) => { + this.addressInfo = addressInfo; + }); + } + this.address = address; + this.updateChainStats(); + this.isLoadingAddress = false; + this.openGraphService.setPreviewReady(); + }) + ) + .subscribe(() => {}, + (error) => { + console.log(error); + this.error = error; + this.isLoadingAddress = false; + } + ); + } + + updateChainStats() { + this.received = this.address.chain_stats.funded_txo_sum + this.address.mempool_stats.funded_txo_sum; + this.sent = this.address.chain_stats.spent_txo_sum + this.address.mempool_stats.spent_txo_sum; + this.txCount = this.address.chain_stats.tx_count + this.address.mempool_stats.tx_count; + this.totalConfirmedTxCount = this.address.chain_stats.tx_count; + this.totalUnspent = this.address.chain_stats.funded_txo_count - this.address.chain_stats.spent_txo_count; + } + + ngOnDestroy() { + this.mainSubscription.unsubscribe(); + } +} diff --git a/frontend/src/app/components/block/block-preview.component.scss b/frontend/src/app/components/block/block-preview.component.scss index 6099f5d47..f2049a1d3 100644 --- a/frontend/src/app/components/block/block-preview.component.scss +++ b/frontend/src/app/components/block/block-preview.component.scss @@ -1,7 +1,3 @@ -.box { - padding: 2rem 3rem; -} - .block-title { margin-bottom: 0.75em; font-size: 42px; diff --git a/frontend/src/app/services/opengraph.service.ts b/frontend/src/app/services/opengraph.service.ts index 48064fdea..ad62a889c 100644 --- a/frontend/src/app/services/opengraph.service.ts +++ b/frontend/src/app/services/opengraph.service.ts @@ -58,4 +58,14 @@ export class OpenGraphService { this.metaService.updateTag({ property: 'og:image:width', content: '1000' }); this.metaService.updateTag({ property: 'og:image:height', content: '500' }); } + + /// signal that the unfurler should wait for a 'ready' signal before taking a screenshot + setPreviewLoading() { + this.metaService.updateTag({ property: 'og:loading', content: 'loading'}); + } + + // signal to the unfurler that the page is ready for a screenshot + setPreviewReady() { + this.metaService.updateTag({ property: 'og:ready', content: 'ready'}); + } } diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index cd087a3c4..fc7acec54 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -50,6 +50,7 @@ import { BlockAuditComponent } from '../components/block-audit/block-audit.compo import { BlockOverviewGraphComponent } from '../components/block-overview-graph/block-overview-graph.component'; import { BlockOverviewTooltipComponent } from '../components/block-overview-tooltip/block-overview-tooltip.component'; import { AddressComponent } from '../components/address/address.component'; +import { AddressPreviewComponent } from '../components/address/address-preview.component'; import { SearchFormComponent } from '../components/search-form/search-form.component'; import { AddressLabelsComponent } from '../components/address-labels/address-labels.component'; import { FooterComponent } from '../components/footer/footer.component'; @@ -124,6 +125,7 @@ import { TimestampComponent } from './components/timestamp/timestamp.component'; BlockOverviewTooltipComponent, TransactionsListComponent, AddressComponent, + AddressPreviewComponent, SearchFormComponent, TimeSpanComponent, AddressLabelsComponent, @@ -225,6 +227,7 @@ import { TimestampComponent } from './components/timestamp/timestamp.component'; BlockOverviewTooltipComponent, TransactionsListComponent, AddressComponent, + AddressPreviewComponent, SearchFormComponent, TimeSpanComponent, AddressLabelsComponent, diff --git a/frontend/src/styles.scss b/frontend/src/styles.scss index d5e8dde28..da4bdcffe 100644 --- a/frontend/src/styles.scss +++ b/frontend/src/styles.scss @@ -87,6 +87,11 @@ body { box-shadow: 0.125rem 0.125rem 0.25rem rgba(0,0,0,0.075); } +.preview-box { + min-height: 512px; + padding: 2rem 3rem; +} + @media (max-width: 767.98px) { .box { padding: 0.75rem; diff --git a/unfurler/src/index.ts b/unfurler/src/index.ts index 998beb1eb..49815fcb1 100644 --- a/unfurler/src/index.ts +++ b/unfurler/src/index.ts @@ -53,7 +53,7 @@ class Server { } async clusterTask({ page, data: { url, action } }) { - await page.goto(url, { waitUntil: "domcontentloaded" }); + await page.goto(url, { waitUntil: "networkidle0" }); switch (action) { case 'screenshot': { await page.evaluate(async () => { @@ -73,11 +73,21 @@ class Server { }), ]); }); + const waitForReady = await page.$('meta[property="og:loading"]'); + const alreadyReady = await page.$('meta[property="og:ready"]'); + if (waitForReady != null && alreadyReady == null) { + try { + await page.waitForSelector('meta[property="og:ready]"', { timeout: 10000 }); + } catch (e) { + // probably timed out + } + } return page.screenshot(); } break; default: { try { - await page.waitForSelector('meta[property="og:title"', { timeout: 5000 }) + await page.waitForSelector('meta[property="og:title"]', { timeout: 10000 }) + const tag = await page.$('meta[property="og:title"]'); } catch (e) { // probably timed out } From 6d4ee30a0e9335422a8966815ed40bc57bdfa97d Mon Sep 17 00:00:00 2001 From: Leonardo Lima Date: Wed, 27 Jul 2022 17:12:33 -0300 Subject: [PATCH 043/105] feat: parse rpc full block from hex to binary representation --- backend/src/api/bitcoin/bitcoin-api.ts | 3 ++- frontend/src/app/docs/api-docs/api-docs-data.ts | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/src/api/bitcoin/bitcoin-api.ts b/backend/src/api/bitcoin/bitcoin-api.ts index 3152954c1..ebde5cc07 100644 --- a/backend/src/api/bitcoin/bitcoin-api.ts +++ b/backend/src/api/bitcoin/bitcoin-api.ts @@ -77,7 +77,8 @@ class BitcoinApi implements AbstractBitcoinApi { } $getRawBlock(hash: string): Promise { - return this.bitcoindClient.getBlock(hash, 0); + return this.bitcoindClient.getBlock(hash, 0) + .then((raw: string) => Buffer.from(raw, "hex")); } $getBlockHash(height: number): Promise { 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 80aab5f15..f8f0e23b8 100644 --- a/frontend/src/app/docs/api-docs/api-docs-data.ts +++ b/frontend/src/app/docs/api-docs/api-docs-data.ts @@ -2070,7 +2070,7 @@ export const restApiDocsData = [ fragment: "get-block-raw", title: "GET Block Raw", description: { - default: "Returns the raw block representation in binary for Esplora backend, or hex for Bitcoin Core RPC backend." + default: "Returns the raw block representation in binary." }, urlString: "/block/:hash/raw", showConditions: bitcoinNetworks.concat(liquidNetworks), From 26bc7c5d9a32349f76b2b32c4b9a21c7203a4e81 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Wed, 27 Jul 2022 22:53:09 +0200 Subject: [PATCH 044/105] Silence LN db truncation messages is CONFIG.LIGHTNING.ENABLED = false --- backend/src/api/database-migration.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/backend/src/api/database-migration.ts b/backend/src/api/database-migration.ts index d9be6e1e7..8bb4c4abf 100644 --- a/backend/src/api/database-migration.ts +++ b/backend/src/api/database-migration.ts @@ -256,7 +256,9 @@ class DatabaseMigration { } if (databaseSchemaVersion < 26 && isBitcoin === true) { - this.uniqueLog(logger.notice, `'lightning_stats' table has been truncated. Will re-generate historical data from scratch.`); + if (config.LIGHTNING.ENABLED) { + this.uniqueLog(logger.notice, `'lightning_stats' table has been truncated. Will re-generate historical data from scratch.`); + } await this.$executeQuery(`TRUNCATE lightning_stats`); await this.$executeQuery('ALTER TABLE `lightning_stats` ADD tor_nodes int(11) NOT NULL DEFAULT "0"'); await this.$executeQuery('ALTER TABLE `lightning_stats` ADD clearnet_nodes int(11) NOT NULL DEFAULT "0"'); @@ -273,6 +275,9 @@ class DatabaseMigration { } if (databaseSchemaVersion < 28 && isBitcoin === true) { + if (config.LIGHTNING.ENABLED) { + this.uniqueLog(logger.notice, `'lightning_stats' and 'node_stats' tables have been truncated. Will re-generate historical data from scratch.`); + } await this.$executeQuery(`TRUNCATE lightning_stats`); await this.$executeQuery(`TRUNCATE node_stats`); await this.$executeQuery(`ALTER TABLE lightning_stats MODIFY added DATE`); From a90b9434f072eac58c7e2fff8598569c990a35c7 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Wed, 27 Jul 2022 23:01:57 +0200 Subject: [PATCH 045/105] Remove useless notice message content --- backend/src/api/database-migration.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/src/api/database-migration.ts b/backend/src/api/database-migration.ts index 8bb4c4abf..d26bfd6cc 100644 --- a/backend/src/api/database-migration.ts +++ b/backend/src/api/database-migration.ts @@ -9,8 +9,8 @@ class DatabaseMigration { private statisticsAddedIndexed = false; private uniqueLogs: string[] = []; - private blocksTruncatedMessage = `'blocks' table has been truncated. Re-indexing from scratch.`; - private hashratesTruncatedMessage = `'hashrates' table has been truncated. Re-indexing from scratch.`; + private blocksTruncatedMessage = `'blocks' table has been truncated.`; + private hashratesTruncatedMessage = `'hashrates' table has been truncated.`; /** * Avoid printing multiple time the same message @@ -257,7 +257,7 @@ class DatabaseMigration { if (databaseSchemaVersion < 26 && isBitcoin === true) { if (config.LIGHTNING.ENABLED) { - this.uniqueLog(logger.notice, `'lightning_stats' table has been truncated. Will re-generate historical data from scratch.`); + this.uniqueLog(logger.notice, `'lightning_stats' table has been truncated.`); } await this.$executeQuery(`TRUNCATE lightning_stats`); await this.$executeQuery('ALTER TABLE `lightning_stats` ADD tor_nodes int(11) NOT NULL DEFAULT "0"'); @@ -276,7 +276,7 @@ class DatabaseMigration { if (databaseSchemaVersion < 28 && isBitcoin === true) { if (config.LIGHTNING.ENABLED) { - this.uniqueLog(logger.notice, `'lightning_stats' and 'node_stats' tables have been truncated. Will re-generate historical data from scratch.`); + this.uniqueLog(logger.notice, `'lightning_stats' and 'node_stats' tables have been truncated.`); } await this.$executeQuery(`TRUNCATE lightning_stats`); await this.$executeQuery(`TRUNCATE node_stats`); From 14a7ee32ffadd3ca809bb14b225474b8c4f1f9be Mon Sep 17 00:00:00 2001 From: softsimon Date: Wed, 27 Jul 2022 23:33:18 +0200 Subject: [PATCH 046/105] Update backend/src/api/bitcoin/bitcoin.routes.ts --- backend/src/api/bitcoin/bitcoin.routes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/api/bitcoin/bitcoin.routes.ts b/backend/src/api/bitcoin/bitcoin.routes.ts index ffdcd0c26..66bcb2569 100644 --- a/backend/src/api/bitcoin/bitcoin.routes.ts +++ b/backend/src/api/bitcoin/bitcoin.routes.ts @@ -474,7 +474,7 @@ class BitcoinRoutes { private async getRawBlock(req: Request, res: Response) { try { const result = await bitcoinApi.$getRawBlock(req.params.hash); - res.setHeader('content-type', 'text/plain'); + res.setHeader('content-type', 'application/octet-stream'); res.send(result); } catch (e) { res.status(500).send(e instanceof Error ? e.message : e); From a50eb035ffb39d598cb8eac7f5e4a6c1d85d3710 Mon Sep 17 00:00:00 2001 From: wiz Date: Thu, 28 Jul 2022 03:22:26 +0200 Subject: [PATCH 047/105] Add linux deps to unfurler README --- unfurler/README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/unfurler/README.md b/unfurler/README.md index 71b4ae2fc..3a14081d7 100644 --- a/unfurler/README.md +++ b/unfurler/README.md @@ -9,6 +9,22 @@ Some additional server configuration is required to properly route requests (see ## Setup +### 0. Install deps + +For Linux, in addition to NodeJS/npm you'll need at least: +* nginx +* cups +* chromium-bsu +* libatk1.0 +* libatk-bridge2.0 +* libxkbcommon-dev +* libxcomposite-dev +* libxdamage-dev +* libxrandr-dev +* libgbm-dev +* libpango1.0-dev +* libasound-dev + ### 1. Clone Mempool Repository Get the latest Mempool code: From ac3eadb670620d17b91a45077d48123907fa1837 Mon Sep 17 00:00:00 2001 From: wiz Date: Thu, 28 Jul 2022 03:34:14 +0200 Subject: [PATCH 048/105] Update git URL in unfurl/package.json --- unfurler/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unfurler/package.json b/unfurler/package.json index 432c604e3..0d6d938d6 100644 --- a/unfurler/package.json +++ b/unfurler/package.json @@ -4,7 +4,7 @@ "description": "Renderer for mempool open graph link preview images", "repository": { "type": "git", - "url": "git+https://github.com/mononaut/mempool-unfurl" + "url": "git+https://github.com/mempool/mempool" }, "main": "index.ts", "scripts": { From d7cb6dd366782e04c4e1520482a42e7c97616d9d Mon Sep 17 00:00:00 2001 From: nymkappa Date: Thu, 28 Jul 2022 06:52:08 +0200 Subject: [PATCH 049/105] Reduce mobile menus padding --- .../app/components/master-page/master-page.component.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frontend/src/app/components/master-page/master-page.component.scss b/frontend/src/app/components/master-page/master-page.component.scss index c6a9aaeff..607e4abf8 100644 --- a/frontend/src/app/components/master-page/master-page.component.scss +++ b/frontend/src/app/components/master-page/master-page.component.scss @@ -15,6 +15,11 @@ li.nav-item { margin: auto 10px; padding-left: 10px; padding-right: 10px; + @media (max-width: 992px) { + margin: auto 7px; + padding-left: 8px; + padding-right: 8px; + } } @media (min-width: 992px) { From eaebc4a667f766ba38b3a8fa1df821e9ea32face Mon Sep 17 00:00:00 2001 From: nymkappa Date: Thu, 28 Jul 2022 07:45:37 +0200 Subject: [PATCH 050/105] Update mouse UX on LN map in dashboard (wip) --- .../nodes-channels-map.component.scss | 6 +--- .../nodes-channels-map.component.ts | 29 +++++++++++++++---- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.scss b/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.scss index 060032151..7914a5364 100644 --- a/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.scss +++ b/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.scss @@ -36,6 +36,7 @@ .widget > .chart { -webkit-mask: linear-gradient(180deg, #11131f00 0%, #11131fff 20%); + min-height: 250px; } .chart { @@ -43,23 +44,18 @@ width: 100%; height: 100%; padding-right: 10px; - @media (max-width: 992px) { padding-bottom: 25px; } - @media (max-width: 829px) { padding-bottom: 50px; } - @media (max-width: 767px) { padding-bottom: 25px; } - @media (max-width: 629px) { padding-bottom: 55px; } - @media (max-width: 567px) { padding-bottom: 55px; } diff --git a/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts b/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts index 7963cf544..16accda94 100644 --- a/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts +++ b/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, Component, Input, NgZone, OnDestroy, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, HostListener, Input, NgZone, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { SeoService } from 'src/app/services/seo.service'; import { ApiService } from 'src/app/services/api.service'; import { Observable, switchMap, tap, zip } from 'rxjs'; @@ -26,7 +26,7 @@ export class NodesChannelsMap implements OnInit, OnDestroy { chartOptions: EChartsOption = {}; chartInitOptions = { renderer: 'canvas', - }; + }; constructor( private seoService: SeoService, @@ -98,6 +98,7 @@ export class NodesChannelsMap implements OnInit, OnDestroy { } this.chartOptions = { + silent: true, title: title ?? undefined, geo3D: { map: 'world', @@ -110,9 +111,10 @@ export class NodesChannelsMap implements OnInit, OnDestroy { } }, viewControl: { - center: this.style === 'widget' ? [0, 0, -1] : undefined, - minDistance: 0.1, - distance: this.style === 'widget' ? 45 : 60, + center: this.style === 'widget' ? [0, 0, -10] : undefined, + minDistance: this.style === 'widget' ? 22 : 0.1, + maxDistance: this.style === 'widget' ? 22 : 60, + distance: this.style === 'widget' ? 22 : 60, alpha: 90, panMouseButton: 'left', rotateMouseButton: undefined, @@ -167,6 +169,14 @@ export class NodesChannelsMap implements OnInit, OnDestroy { }; } + @HostListener('window:wheel', ['$event']) + onWindowScroll(e): void { + // Not very smooth when using the mouse + if (this.style === 'widget' && e.target.tagName === 'CANVAS') { + window.scrollBy({left: 0, top: e.deltaY, behavior: 'auto'}); + } + } + onChartInit(ec) { if (this.chartInstance !== undefined) { return; @@ -174,6 +184,15 @@ export class NodesChannelsMap implements OnInit, OnDestroy { this.chartInstance = ec; + if (this.style === 'widget') { + this.chartInstance.getZr().on('click', (e) => { + this.zone.run(() => { + const url = new RelativeUrlPipe(this.stateService).transform(`/graphs/lightning/nodes-channels-map`); + this.router.navigate([url]); + }); + }); + } + this.chartInstance.on('click', (e) => { if (e.data && e.data.publicKey) { this.zone.run(() => { From 6a72962fb1948b130d5bfe4e8ed224478b6d4cfc Mon Sep 17 00:00:00 2001 From: nymkappa Date: Thu, 28 Jul 2022 07:50:58 +0200 Subject: [PATCH 051/105] Remove useless view more links that does not link to anywhere --- .../lightning-dashboard/lightning-dashboard.component.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.html b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.html index 35464a186..999183e09 100644 --- a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.html +++ b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.html @@ -53,7 +53,7 @@
Top Capacity Nodes
- +
@@ -63,7 +63,7 @@
Most Connected Nodes
- +
From 1008b00abc685fc3b6ec18ed51cf63e95ec025af Mon Sep 17 00:00:00 2001 From: nymkappa Date: Thu, 28 Jul 2022 10:01:04 +0200 Subject: [PATCH 052/105] Convert nodes per network chart to stack style --- backend/src/api/explorer/statistics.api.ts | 2 +- .../nodes-networks-chart.component.ts | 70 +++++++------------ 2 files changed, 27 insertions(+), 45 deletions(-) diff --git a/backend/src/api/explorer/statistics.api.ts b/backend/src/api/explorer/statistics.api.ts index c2e23848f..7bf3d9107 100644 --- a/backend/src/api/explorer/statistics.api.ts +++ b/backend/src/api/explorer/statistics.api.ts @@ -6,7 +6,7 @@ class StatisticsApi { public async $getStatistics(interval: string | null = null): Promise { interval = Common.getSqlInterval(interval); - let query = `SELECT UNIX_TIMESTAMP(added) AS added, channel_count, node_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 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 c292d09f7..70d02de28 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 @@ -83,7 +83,6 @@ export class NodesNetworksChartComponent implements OnInit { tap((response) => { const data = response.body; this.prepareChartOptions({ - node_count: data.map(val => [val.added * 1000, val.node_count]), 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]), @@ -103,7 +102,7 @@ export class NodesNetworksChartComponent implements OnInit { prepareChartOptions(data) { let title: object; - if (data.node_count.length === 0) { + if (data.tor_nodes.length === 0) { title = { textStyle: { color: 'grey', @@ -145,33 +144,34 @@ export class NodesNetworksChartComponent implements OnInit { }, borderColor: '#000', formatter: (ticks) => { + let total = 0; 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) { - if (tick.seriesIndex === 0) { // Total + for (const tick of ticks.reverse()) { + if (tick.seriesIndex === 0) { // Tor tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.0-0')}`; - } else if (tick.seriesIndex === 1) { // Tor + } 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) { // Clearnet - tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.0-0')}`; - } else if (tick.seriesIndex === 3) { // Unannounced + } else if (tick.seriesIndex === 2) { // Unannounced tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data[1], this.locale, '1.0-0')}`; } tooltip += `
`; + total += tick.data[1]; } + tooltip += `Total: ${formatNumber(total, this.locale, '1.0-0')} nodes`; return tooltip; } }, - xAxis: data.node_count.length === 0 ? undefined : { + xAxis: data.tor_nodes.length === 0 ? undefined : { type: 'time', splitNumber: (this.isMobile() || this.widget) ? 5 : 10, axisLabel: { hideOverlap: true, } }, - legend: data.node_count.length === 0 ? undefined : { + legend: data.tor_nodes.length === 0 ? undefined : { padding: 10, data: [ { @@ -214,7 +214,7 @@ export class NodesNetworksChartComponent implements OnInit { 'Unannounced': true, } }, - yAxis: data.node_count.length === 0 ? undefined : [ + yAxis: data.tor_nodes.length === 0 ? undefined : [ { type: 'value', position: 'left', @@ -236,45 +236,23 @@ export class NodesNetworksChartComponent implements OnInit { }, } ], - series: data.node_count.length === 0 ? [] : [ - { - zlevel: 1, - name: $localize`Total`, - showSymbol: false, - symbol: 'none', - data: data.node_count, - type: 'line', - lineStyle: { - width: 2, - }, - markLine: { - silent: true, - symbol: 'none', - lineStyle: { - type: 'solid', - color: '#ffffff66', - opacity: 1, - width: 1, - }, - }, - areaStyle: { - opacity: 0.25, - }, - }, + series: data.tor_nodes.length === 0 ? [] : [ { zlevel: 1, yAxisIndex: 0, - name: $localize`Tor`, + name: $localize`Unannounced`, showSymbol: false, symbol: 'none', - data: data.tor_nodes, + data: data.unannounced_nodes, type: 'line', lineStyle: { width: 2, }, areaStyle: { - opacity: 0.25, + opacity: 0.5, }, + stack: 'Total', + color: '#FDD835', }, { zlevel: 1, @@ -288,24 +266,28 @@ export class NodesNetworksChartComponent implements OnInit { width: 2, }, areaStyle: { - opacity: 0.25, + opacity: 0.5, }, + stack: 'Total', + color: '#00ACC1', }, { zlevel: 1, yAxisIndex: 0, - name: $localize`Unannounced`, + name: $localize`Tor`, showSymbol: false, symbol: 'none', - data: data.unannounced_nodes, + data: data.tor_nodes, type: 'line', lineStyle: { width: 2, }, areaStyle: { - opacity: 0.25, + opacity: 0.5, }, - } + stack: 'Total', + color: '#7D4698', + }, ], dataZoom: this.widget ? null : [{ type: 'inside', From c9157c974fa4f22fea845c24be175abc0efb2b7f Mon Sep 17 00:00:00 2001 From: Stephan Oeste Date: Fri, 29 Jul 2022 00:30:32 +0200 Subject: [PATCH 053/105] Add options for components to be installed in prod install script --- production/install | 399 ++++++++++++++++++++++++--------------------- 1 file changed, 216 insertions(+), 183 deletions(-) diff --git a/production/install b/production/install index e9b24bafa..729ff33e0 100755 --- a/production/install +++ b/production/install @@ -668,129 +668,131 @@ ext4CreateDir() # does bitcoin exist? -########### -## dialog # -########### -# -#: ${DIALOG=dialog} -# -#: ${DIALOG_OK=0} -#: ${DIALOG_CANCEL=1} -#: ${DIALOG_HELP=2} -#: ${DIALOG_EXTRA=3} -#: ${DIALOG_ITEM_HELP=4} -#: ${DIALOG_ESC=255} -# -#: ${SIG_OFFNE=0} -#: ${SIG_HUP=1} -#: ${SIG_INT=2} -#: ${SIG_QUIT=3} -#: ${SIG_KILL=9} -#: ${SIG_TERM=15} -# -#input=`tempfile 2>/dev/null` || input=/tmp/input$$ -#output=`tempfile 2>/dev/null` || output=/tmp/test$$ -#trap "rm -f $input $output" $SIG_OFFNE $SIG_HUP $SIG_INT $SIG_TRAP $SIG_TERM -# -#DIALOG_ERROR=254 -#export DIALOG_ERROR -# -#backtitle="Mempool Fullnode Installer" -#title="Mempool Fullnode Installer" -#returncode=0 -# -################## -## dialog part 1 # -################## -# -#$CUT >$input <<-EOF -#Tor:Enable Tor v3 HS Onion:ON -#Certbot:Enable HTTPS using Certbot:ON -#Mainnet:Enable Bitcoin Mainnet:ON -#Mainnet-Minfee:Enable Bitcoin Mainnet Minfee:ON -#Testnet:Enable Bitcoin Testnet:ON -#Liquid:Enable Elements Liquid:ON -#Bisq:Enable Bisq:ON -#Lightmode:Enable Electrs Lightmode to save disk space:ON -#Smalldisk:Disable Electrs Compaction to save disk space:ON -#Firewall:Enable Firewall:ON -#EOF -# -#cat $input | sed -e 's/^/"/' -e 's/:/" "/g' -e 's/$/"/' >$output -#cat $output >$input -# -#$DIALOG --backtitle "${backtitle}" \ -# --title "${title}" "$@" \ -# --checklist "Toggle the features below to configure your fullnode:\n" \ -# 20 80 10 \ -# --file $input 2> $output -# -#retval=$? -# -#tempfile=$output -#if [ $retval != $DIALOG_OK ];then -# echo "Installation aborted." -# exit 1 -#fi -# -#if grep Tor $tempfile >/dev/null 2>&1;then -# TOR_INSTALL=ON -#else -# TOR_INSTALL=OFF -#fi -# -#if grep Certbot $tempfile >/dev/null 2>&1;then -# CERTBOT_INSTALL=ON -#else -# CERTBOT_INSTALL=OFF -#fi -# -#if grep Mainnet $tempfile >/dev/null 2>&1;then -# BITCOIN_MAINNET_ENABLE=ON -#else -# BITCOIN_MAINNET_ENABLE=OFF -#fi -# -#if grep Mainnet-Minfee $tempfile >/dev/null 2>&1;then -# BITCOIN_MAINNET_MINFEE_ENABLE=ON -#else -# BITCOIN_MAINNET_MINFEE_ENABLE=OFF -#fi -# -#if grep Testnet $tempfile >/dev/null 2>&1;then -# BITCOIN_TESTNET_ENABLE=ON -#else -# BITCOIN_TESTNET_ENABLE=OFF -#fi -# -#if grep Liquid $tempfile >/dev/null 2>&1;then -# ELEMENTS_INSTALL=ON -# ELEMENTS_LIQUID_ENABLE=ON -#else -# ELEMENTS_INSTALL=OFF -# ELEMENTS_LIQUID_ENABLE=OFF -#fi -# -#if grep Bisq $tempfile >/dev/null 2>&1;then -# BISQ_INSTALL=ON -# BISQ_MAINNET_ENABLE=ON -#else -# BISQ_INSTALL=OFF -# BISQ_MAINNET_ENABLE=OFF -#fi -# -#if grep Lightmode $tempfile >/dev/null 2>&1;then -# BITCOIN_ELECTRS_LIGHT_MODE=ON -#else -# BITCOIN_ELECTRS_LIGHT_MODE=OFF -#fi -# -#if grep Smalldisk $tempfile >/dev/null 2>&1;then -# BITCOIN_ELECTRS_LIGHT_MODE=ON -#else -# BITCOIN_ELECTRS_LIGHT_MODE=OFF -#fi -# +########## +# dialog # +########## + +: ${DIALOG=dialog} + +: ${DIALOG_OK=0} +: ${DIALOG_CANCEL=1} +: ${DIALOG_HELP=2} +: ${DIALOG_EXTRA=3} +: ${DIALOG_ITEM_HELP=4} +: ${DIALOG_ESC=255} + +: ${SIG_OFFNE=0} +: ${SIG_HUP=1} +: ${SIG_INT=2} +: ${SIG_QUIT=3} +: ${SIG_KILL=9} +: ${SIG_TERM=15} + +input=`tempfile 2>/dev/null` || input=/tmp/input$$ +output=`tempfile 2>/dev/null` || output=/tmp/test$$ +trap "rm -f $input $output" $SIG_OFFNE $SIG_HUP $SIG_INT $SIG_TRAP $SIG_TERM + +DIALOG_ERROR=254 +export DIALOG_ERROR + +backtitle="Mempool Fullnode Installer" +title="Mempool Fullnode Installer" +returncode=0 + +################# +# dialog part 1 # +################# + +$CUT >$input <<-EOF +Tor:Enable Tor v3 HS Onion:ON +Mainnet:Enable Bitcoin Mainnet:ON +Mainnet-Minfee:Enable Bitcoin Mainnet Minfee:ON +Testnet:Enable Bitcoin Testnet:ON +Signet:Enable Bitcoin Signet:ON +Liquid:Enable Elements Liquid:ON +Liquidtestnet:Enable Elements Liquidtestnet:ON +Bisq:Enable Bisq:ON +EOF + +cat $input | sed -e 's/^/"/' -e 's/:/" "/g' -e 's/$/"/' >$output +cat $output >$input + +$DIALOG --backtitle "${backtitle}" \ + --title "${title}" "$@" \ + --checklist "Toggle the features below to configure your fullnode:\n" \ + 20 80 10 \ + --file $input 2> $output + +retval=$? + +tempfile=$output +if [ $retval != $DIALOG_OK ];then + echo "Installation aborted." + exit 1 +fi + +if grep Tor $tempfile >/dev/null 2>&1;then + TOR_INSTALL=ON +else + TOR_INSTALL=OFF +fi + +if grep Mainnet $tempfile >/dev/null 2>&1;then + BITCOIN_MAINNET_ENABLE=ON +else + BITCOIN_MAINNET_ENABLE=OFF +fi + +if grep Mainnet-Minfee $tempfile >/dev/null 2>&1;then + BITCOIN_MAINNET_MINFEE_ENABLE=ON +else + BITCOIN_MAINNET_MINFEE_ENABLE=OFF +fi + +if grep Testnet $tempfile >/dev/null 2>&1;then + BITCOIN_TESTNET_ENABLE=ON +else + BITCOIN_TESTNET_ENABLE=OFF +fi + +if grep Signet $tempfile >/dev/null 2>&1;then + BITCOIN_SIGNET_ENABLE=ON +else + BITCOIN_SIGNET_ENABLE=OFF +fi + +if [ "${BITCOIN_MAINNET_ENABLE}" = ON -o "${BITCOIN_MAINNET_MINFEE_ENABLE}" = ON -o "${BITCOIN_TESTNET_ENABLE}" = ON -o "${BITCOIN_SIGNET_ENABLE}" = ON ];then + BITCOIN_INSTALL=ON +else + BITCOIN_INSTALL=OFF +fi + +if grep Liquid $tempfile >/dev/null 2>&1;then + ELEMENTS_LIQUID_ENABLE=ON +else + ELEMENTS_LIQUID_ENABLE=OFF +fi + +if grep Liquidtestnet $tempfile >/dev/null 2>&1;then + ELEMENTS_LIQUIDTESTNET_ENABLE=ON +else + ELEMENTS_LIQUIDTESTNET_ENABLE=OFF +fi + +if [ "${ELEMENTS_LIQUID_ENABLE}" = ON -o "${ELEMENTS_LIQUIDTESTNET_ENABLE}" = ON ];then + ELEMENTS_INSTALL=ON +else + ELEMENTS_INSTALL=OFF +fi + +if grep Bisq $tempfile >/dev/null 2>&1;then + BISQ_INSTALL=ON + BISQ_MAINNET_ENABLE=ON +else + BISQ_INSTALL=OFF + BISQ_MAINNET_ENABLE=OFF +fi + ################## ## dialog part 2 # ################## @@ -969,12 +971,20 @@ if [ "${BITCOIN_INSTALL}" = ON ];then echo "[*] Creating Bitcoin user with Tor access" osGroupCreate "${BITCOIN_GROUP}" - osUserCreate "${BITCOIN_USER}" "${BITCOIN_HOME}" "${BITCOIN_GROUP}" "${TOR_GROUP}" + if [ "${TOR_INSTALL}" = ON ];then + osUserCreate "${BITCOIN_USER}" "${BITCOIN_HOME}" "${BITCOIN_GROUP}" "${TOR_GROUP}" + else + osUserCreate "${BITCOIN_USER}" "${BITCOIN_HOME}" "${BITCOIN_GROUP}" + fi osSudo "${ROOT_USER}" chsh -s `which zsh` "${BITCOIN_USER}" echo "[*] Creating Bitcoin minfee user with Tor access" osGroupCreate "${MINFEE_GROUP}" - osUserCreate "${MINFEE_USER}" "${MINFEE_HOME}" "${MINFEE_GROUP}" "${TOR_GROUP}" + if [ "${TOR_INSTALL}" = ON ];then + osUserCreate "${MINFEE_USER}" "${MINFEE_HOME}" "${MINFEE_GROUP}" "${TOR_GROUP}" + else + osUserCreate "${MINFEE_USER}" "${MINFEE_HOME}" "${MINFEE_GROUP}" + fi osSudo "${ROOT_USER}" chown -R "${MINFEE_USER}:${MINFEE_GROUP}" "${MINFEE_HOME}" osSudo "${ROOT_USER}" chsh -s `which zsh` "${MINFEE_USER}" osSudo "${MINFEE_USER}" touch "${MINFEE_HOME}/.zshrc" @@ -1022,7 +1032,11 @@ if [ "${ELEMENTS_INSTALL}" = ON ];then echo "[*] Creating Elements user with Tor access" osGroupCreate "${ELEMENTS_GROUP}" - osUserCreate "${ELEMENTS_USER}" "${ELEMENTS_HOME}" "${ELEMENTS_GROUP}" "${TOR_GROUP}" + if [ "${TOR_INSTALL}" = ON ];then + osUserCreate "${ELEMENTS_USER}" "${ELEMENTS_HOME}" "${ELEMENTS_GROUP}" "${TOR_GROUP}" + else + osUserCreate "${ELEMENTS_USER}" "${ELEMENTS_HOME}" "${ELEMENTS_GROUP}" + fi osSudo "${ROOT_USER}" chsh -s `which zsh` "${ELEMENTS_USER}" echo "[*] Creating Elements data folder" @@ -1063,9 +1077,15 @@ fi echo "[*] Creating Bitcoin Electrs data folder" osSudo "${ROOT_USER}" mkdir -p "${BITCOIN_ELECTRS_HOME}" osSudo "${ROOT_USER}" chown -R "${BITCOIN_USER}:${BITCOIN_GROUP}" "${BITCOIN_ELECTRS_HOME}" -osSudo "${ROOT_USER}" chown -R "${BITCOIN_USER}:${BITCOIN_GROUP}" "${ELECTRS_MAINNET_DATA}" -osSudo "${ROOT_USER}" chown -R "${BITCOIN_USER}:${BITCOIN_GROUP}" "${ELECTRS_TESTNET_DATA}" -osSudo "${ROOT_USER}" chown -R "${BITCOIN_USER}:${BITCOIN_GROUP}" "${ELECTRS_SIGNET_DATA}" +if [ "${BITCOIN_MAINNET_ENABLE}" = ON ];then + osSudo "${ROOT_USER}" chown -R "${BITCOIN_USER}:${BITCOIN_GROUP}" "${ELECTRS_MAINNET_DATA}" +fi +if [ "${BITCOIN_TESTNET_ENABLE}" = ON ];then + osSudo "${ROOT_USER}" chown -R "${BITCOIN_USER}:${BITCOIN_GROUP}" "${ELECTRS_TESTNET_DATA}" +fi +if [ "${BITCOIN_SIGNET_ENABLE}" = ON ];then + osSudo "${ROOT_USER}" chown -R "${BITCOIN_USER}:${BITCOIN_GROUP}" "${ELECTRS_SIGNET_DATA}" +fi echo "[*] Cloning Bitcoin Electrs repo from ${BITCOIN_ELECTRS_REPO_URL}" osSudo "${BITCOIN_USER}" git config --global advice.detachedHead false @@ -1105,43 +1125,46 @@ osSudo "${BITCOIN_USER}" sh -c "cd ${BITCOIN_ELECTRS_HOME} && cargo run --releas # Liquid -> Electrs installation # ################################## -echo "[*] Creating Liquid Electrs data folder" -osSudo "${ROOT_USER}" mkdir -p "${ELEMENTS_ELECTRS_HOME}" -osSudo "${ROOT_USER}" chown -R "${ELEMENTS_USER}:${ELEMENTS_GROUP}" "${ELEMENTS_HOME}" -osSudo "${ROOT_USER}" chown -R "${ELEMENTS_USER}:${ELEMENTS_GROUP}" "${ELEMENTS_ELECTRS_HOME}" -osSudo "${ROOT_USER}" chown -R "${ELEMENTS_USER}:${ELEMENTS_GROUP}" "${ELECTRS_LIQUID_DATA}" -osSudo "${ROOT_USER}" chown -R "${ELEMENTS_USER}:${ELEMENTS_GROUP}" "${ELECTRS_LIQUIDTESTNET_DATA}" - -echo "[*] Cloning Liquid Electrs repo from ${ELEMENTS_ELECTRS_REPO_URL}" -osSudo "${ELEMENTS_USER}" git config --global advice.detachedHead false -osSudo "${ELEMENTS_USER}" git clone --branch "${ELEMENTS_ELECTRS_REPO_BRANCH}" "${ELEMENTS_ELECTRS_REPO_URL}" "${ELEMENTS_HOME}/${ELEMENTS_ELECTRS_REPO_NAME}" - -echo "[*] Checking out Liquid Electrs ${ELEMENTS_ELECTRS_LATEST_RELEASE}" -osSudo "${ELEMENTS_USER}" sh -c "cd ${ELEMENTS_HOME}/${ELEMENTS_ELECTRS_REPO_NAME} && git checkout ${ELEMENTS_ELECTRS_LATEST_RELEASE}" - -echo "[*] Cloning Liquid Asset Registry repo from ${LIQUID_ASSET_REGISTRY_DB_URL}" -osSudo "${ELEMENTS_USER}" git config --global advice.detachedHead false -osSudo "${ELEMENTS_USER}" git clone "${LIQUID_ASSET_REGISTRY_DB_URL}" "${ELEMENTS_HOME}/${LIQUID_ASSET_REGISTRY_DB_NAME}" - -echo "[*] Cloning Liquid Asset Registry testnet repo from ${LIQUIDTESTNET_ASSET_REGISTRY_DB_URL}" -osSudo "${ELEMENTS_USER}" git config --global advice.detachedHead false -osSudo "${ELEMENTS_USER}" git clone "${LIQUIDTESTNET_ASSET_REGISTRY_DB_URL}" "${ELEMENTS_HOME}/${LIQUIDTESTNET_ASSET_REGISTRY_DB_NAME}" - -echo "[*] Building Liquid Electrs release binary" -osSudo "${ELEMENTS_USER}" sh -c "cd ${ELEMENTS_ELECTRS_HOME} && cargo run --release --features liquid --bin electrs -- --network liquid --version" || true - -case $OS in - FreeBSD) - echo "[*] Patching Liquid Electrs code for FreeBSD" - osSudo "${ELEMENTS_USER}" sh -c "cd \"${ELEMENTS_HOME}/.cargo/registry/src/github.com-1ecc6299db9ec823/sysconf-0.3.4\" && patch -p1 < \"${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/freebsd/sysconf.patch\"" - ;; - Debian) - ;; -esac - -echo "[*] Building Liquid Electrs release binary" -osSudo "${ELEMENTS_USER}" sh -c "cd ${ELEMENTS_ELECTRS_HOME} && cargo run --release --features liquid --bin electrs -- --network liquid --version" || true +if [ "${ELEMENTS_INSTALL}" = ON ;then + echo "[*] Creating Liquid Electrs data folder" + osSudo "${ROOT_USER}" mkdir -p "${ELEMENTS_ELECTRS_HOME}" + osSudo "${ROOT_USER}" chown -R "${ELEMENTS_USER}:${ELEMENTS_GROUP}" "${ELEMENTS_HOME}" + osSudo "${ROOT_USER}" chown -R "${ELEMENTS_USER}:${ELEMENTS_GROUP}" "${ELEMENTS_ELECTRS_HOME}" + osSudo "${ROOT_USER}" chown -R "${ELEMENTS_USER}:${ELEMENTS_GROUP}" "${ELECTRS_LIQUID_DATA}" + osSudo "${ROOT_USER}" chown -R "${ELEMENTS_USER}:${ELEMENTS_GROUP}" "${ELECTRS_LIQUIDTESTNET_DATA}" + + echo "[*] Cloning Liquid Electrs repo from ${ELEMENTS_ELECTRS_REPO_URL}" + osSudo "${ELEMENTS_USER}" git config --global advice.detachedHead false + osSudo "${ELEMENTS_USER}" git clone --branch "${ELEMENTS_ELECTRS_REPO_BRANCH}" "${ELEMENTS_ELECTRS_REPO_URL}" "${ELEMENTS_HOME}/${ELEMENTS_ELECTRS_REPO_NAME}" + + echo "[*] Checking out Liquid Electrs ${ELEMENTS_ELECTRS_LATEST_RELEASE}" + osSudo "${ELEMENTS_USER}" sh -c "cd ${ELEMENTS_HOME}/${ELEMENTS_ELECTRS_REPO_NAME} && git checkout ${ELEMENTS_ELECTRS_LATEST_RELEASE}" + + echo "[*] Cloning Liquid Asset Registry repo from ${LIQUID_ASSET_REGISTRY_DB_URL}" + osSudo "${ELEMENTS_USER}" git config --global advice.detachedHead false + osSudo "${ELEMENTS_USER}" git clone "${LIQUID_ASSET_REGISTRY_DB_URL}" "${ELEMENTS_HOME}/${LIQUID_ASSET_REGISTRY_DB_NAME}" + + echo "[*] Cloning Liquid Asset Registry testnet repo from ${LIQUIDTESTNET_ASSET_REGISTRY_DB_URL}" + osSudo "${ELEMENTS_USER}" git config --global advice.detachedHead false + osSudo "${ELEMENTS_USER}" git clone "${LIQUIDTESTNET_ASSET_REGISTRY_DB_URL}" "${ELEMENTS_HOME}/${LIQUIDTESTNET_ASSET_REGISTRY_DB_NAME}" + + echo "[*] Building Liquid Electrs release binary" + osSudo "${ELEMENTS_USER}" sh -c "cd ${ELEMENTS_ELECTRS_HOME} && cargo run --release --features liquid --bin electrs -- --network liquid --version" || true + + case $OS in + FreeBSD) + echo "[*] Patching Liquid Electrs code for FreeBSD" + osSudo "${ELEMENTS_USER}" sh -c "cd \"${ELEMENTS_HOME}/.cargo/registry/src/github.com-1ecc6299db9ec823/sysconf-0.3.4\" && patch -p1 < \"${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/freebsd/sysconf.patch\"" + ;; + Debian) + ;; + esac + + echo "[*] Building Liquid Electrs release binary" + osSudo "${ELEMENTS_USER}" sh -c "cd ${ELEMENTS_ELECTRS_HOME} && cargo run --release --features liquid --bin electrs -- --network liquid --version" || true +fi + ##################### # Bisq installation # ##################### @@ -1150,7 +1173,11 @@ if [ "${BISQ_INSTALL}" = ON ];then echo "[*] Creating Bisq user with Tor access" osGroupCreate "${BISQ_GROUP}" - osUserCreate "${BISQ_USER}" "${BISQ_HOME}" "${BISQ_GROUP}" "${TOR_GROUP}" + if [ "${TOR_INSTALL}" = ON ];then + osUserCreate "${BISQ_USER}" "${BISQ_HOME}" "${BISQ_GROUP}" "${TOR_GROUP}" + else + osUserCreate "${BISQ_USER}" "${BISQ_HOME}" "${BISQ_GROUP}" + fi osSudo "${ROOT_USER}" chsh -s `which zsh` "${BISQ_USER}" echo "[*] Creating Bisq data folder" @@ -1435,7 +1462,9 @@ case $OS in echo "[*] Installing Electrs Signet Cronjob" crontab_bitcoin+="@reboot sleep 90 ; screen -dmS signet /bitcoin/electrs/electrs-start-signet\n" fi - echo "${crontab_bitcoin}" | crontab -u "${BITCOIN_USER}" - + if [ "${BITCOIN_MAINNET_ENABLE}" = ON -o "${BITCOIN_TESTNET_ENABLE}" = ON -o "${BITCOIN_SIGNET_ENABLE}" = ON ];then + echo "${crontab_bitcoin}" | crontab -u "${BITCOIN_USER}" - + fi crontab_elements=() if [ "${ELEMENTS_LIQUID_ENABLE}" = ON ];then @@ -1446,7 +1475,9 @@ case $OS in echo "[*] Installing Liquid Asset Testnet Cronjob" crontab_elements+="6 * * * * cd $HOME/asset_registry_testnet_db && git pull origin master >/dev/null 2>&1\n" fi - echo "${crontab_elements}" | crontab -u "${ELEMENTS_USER}" - + if [ "${ELEMENTS_LIQUID_ENABLE}" = ON -o "${ELEMENTS_LIQUIDTESTNET_ENABLE}" = ON ];then + echo "${crontab_elements}" | crontab -u "${ELEMENTS_USER}" - + fi ;; esac @@ -1459,7 +1490,7 @@ fi ##### Mempool -> Bitcoin Mainnet instance -if [ "${BITCOIN_MAINNET_ENABLE}" = ON ];then +if [ "${BITCOIN_MAINNET_ENABLE}" = ON -o "${BITCOIN_TESTNET_ENABLE}" = ON -o "${BITCOIN_SIGNET_ENABLE}" = ON ];then echo "[*] Creating Mempool instance for Bitcoin Mainnet" osSudo "${MEMPOOL_USER}" git config --global advice.detachedHead false osSudo "${MEMPOOL_USER}" git clone --branch "${MEMPOOL_REPO_BRANCH}" "${MEMPOOL_REPO_URL}" "${MEMPOOL_HOME}/mainnet" @@ -1564,12 +1595,6 @@ _EOF_ ##### nginx - -echo "[*] Read tor v3 onion hostnames" -NGINX_MEMPOOL_ONION=$(cat "${TOR_RESOURCES}/mempool/hostname") -NGINX_BISQ_ONION=$(cat "${TOR_RESOURCES}/bisq/hostname") -NGINX_LIQUID_ONION=$(cat "${TOR_RESOURCES}/liquid/hostname") - echo "[*] Adding Nginx configuration" osSudo "${ROOT_USER}" install -c -o "${ROOT_USER}" -g "${ROOT_GROUP}" -m 644 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/nginx/nginx.conf" "${NGINX_CONFIGURATION}" mkdir -p /var/cache/nginx/services /var/cache/nginx/api @@ -1577,9 +1602,15 @@ chown ${NGINX_USER}: /var/cache/nginx/services /var/cache/nginx/api ln -s /mempool/mempool /etc/nginx/mempool osSudo "${ROOT_USER}" sed -i.orig "s!__NGINX_USER__!${NGINX_USER}!" "${NGINX_CONFIGURATION}" osSudo "${ROOT_USER}" sed -i.orig "s!__NGINX_ETC_FOLDER__!${NGINX_ETC_FOLDER}!" "${NGINX_CONFIGURATION}" -osSudo "${ROOT_USER}" sed -i.orig "s!__NGINX_MEMPOOL_ONION__!${NGINX_MEMPOOL_ONION%.onion}!" "${NGINX_CONFIGURATION}" -osSudo "${ROOT_USER}" sed -i.orig "s!__NGINX_BISQ_ONION__!${NGINX_BISQ_ONION%.onion}!" "${NGINX_CONFIGURATION}" -osSudo "${ROOT_USER}" sed -i.orig "s!__NGINX_LIQUID_ONION__!${NGINX_LIQUID_ONIONi%.onion}!" "${NGINX_CONFIGURATION}" +if [ "${TOR_INSTALL}" = ON ];then +echo "[*] Read tor v3 onion hostnames" + NGINX_MEMPOOL_ONION=$(cat "${TOR_RESOURCES}/mempool/hostname") + NGINX_BISQ_ONION=$(cat "${TOR_RESOURCES}/bisq/hostname") + NGINX_LIQUID_ONION=$(cat "${TOR_RESOURCES}/liquid/hostname") + osSudo "${ROOT_USER}" sed -i.orig "s!__NGINX_MEMPOOL_ONION__!${NGINX_MEMPOOL_ONION%.onion}!" "${NGINX_CONFIGURATION}" + osSudo "${ROOT_USER}" sed -i.orig "s!__NGINX_BISQ_ONION__!${NGINX_BISQ_ONION%.onion}!" "${NGINX_CONFIGURATION}" + osSudo "${ROOT_USER}" sed -i.orig "s!__NGINX_LIQUID_ONION__!${NGINX_LIQUID_ONIONi%.onion}!" "${NGINX_CONFIGURATION}" +fi echo "[*] Restarting Nginx" osSudo "${ROOT_USER}" service nginx restart @@ -1642,7 +1673,7 @@ esac ##### Build Mempool echo "[*] Build Mempool" -osSudo "${MEMPOOL_USER}" sh -c "cd ${MEMPOOL_HOME} && ./upgrade" +osSudo "${MEMPOOL_USER}" sh -c "cd ${MEMPOOL_HOME} && ./upgrade" || true @@ -1727,10 +1758,12 @@ case $OS in ;; Debian) - echo "This are the generated Tor addresses:" - echo "${NGINX_MEMPOOL_ONION}" - echo "${NGINX_BISQ_ONION}" - echo "${NGINX_LIQUID_ONION}" + if [ "${TOR_INSTALL}" = ON ];then + echo "This are the generated Tor addresses:" + echo "${NGINX_MEMPOOL_ONION}" + echo "${NGINX_BISQ_ONION}" + echo "${NGINX_LIQUID_ONION}" + fi ;; esac From 0127eed3bfbbd34632114bee1963b011bac46eec Mon Sep 17 00:00:00 2001 From: Stephan Oeste Date: Fri, 29 Jul 2022 20:13:48 +0200 Subject: [PATCH 054/105] Add Unfurl to the prod installer --- production/install | 173 +++++++++++++++++++------- production/mempool-config.unfurl.json | 13 ++ production/unfurl-build | 62 +++++++++ production/unfurl-kill | 2 + production/unfurl-start | 6 + 5 files changed, 211 insertions(+), 45 deletions(-) create mode 100644 production/mempool-config.unfurl.json create mode 100755 production/unfurl-build create mode 100755 production/unfurl-kill create mode 100755 production/unfurl-start diff --git a/production/install b/production/install index 729ff33e0..053ffef70 100755 --- a/production/install +++ b/production/install @@ -39,6 +39,9 @@ BITCOIN_INSTALL=ON BISQ_INSTALL=ON ELEMENTS_INSTALL=ON +# install UNFURL +UNFURL_INSTALL=ON + # configure 4 network instances BITCOIN_MAINNET_ENABLE=ON BITCOIN_MAINNET_MINFEE_ENABLE=ON @@ -48,6 +51,9 @@ BISQ_MAINNET_ENABLE=ON ELEMENTS_LIQUID_ENABLE=ON ELEMENTS_LIQUIDTESTNET_ENABLE=ON +# install Electrs +ELECTRS_INSTALL=ON + # enable lightmode and disable compaction to fit on 1TB SSD drive BITCOIN_ELECTRS_LIGHT_MODE=ON BITCOIN_ELECTRS_COMPACTION=OFF @@ -278,6 +284,12 @@ BISQ_GROUP=bisq # bisq home folder, needs about 1GB BISQ_HOME=/bisq +# Unfurl user/group +UNFURL_USER=unfurl +UNFURL_GROUP=unfurl +# Unfurl home folder +UNFURL_HOME=/unfurl + # liquid user/group ELEMENTS_USER=elements ELEMENTS_GROUP=elements @@ -315,6 +327,13 @@ BISQ_REPO_BRANCH=master BISQ_LATEST_RELEASE=master echo -n '.' +UNFURL_REPO_URL=https://github.com/mempool/mempool +UNFURL_REPO_NAME=unfurl +UNFURL_REPO_BRANCH=master +#UNFURL_LATEST_RELEASE=$(curl -s https://api.github.com/repos/mempool/mempool/releases/latest|grep tag_name|head -1|cut -d '"' -f4) +UNFURL_LATEST_RELEASE=master +echo -n '.' + ELEMENTS_REPO_URL=https://github.com/ElementsProject/elements ELEMENTS_REPO_NAME=elements ELEMENTS_REPO_BRANCH=master @@ -351,6 +370,10 @@ DEBIAN_PKG+=(libboost-system-dev libboost-filesystem-dev libboost-chrono-dev lib DEBIAN_PKG+=(nodejs npm mariadb-server nginx-core python3-certbot-nginx rsync ufw) DEBIAN_PKG+=(geoipupdate) +DEBIAN_UNFURL_PKG=() +DEBIAN_UNFURL_PKG+=(cups chromium-bsu libatk1.0 libatk-bridge2.0 libxkbcommon-dev libxcomposite-dev) +DEBIAN_UNFURL_PKG+=(libxdamage-dev libxrandr-dev libgbm-dev libpango1.0-dev libasound-dev) + # packages needed for mempool ecosystem FREEBSD_PKG=() FREEBSD_PKG+=(zsh sudo git screen curl wget calc neovim) @@ -712,6 +735,7 @@ Signet:Enable Bitcoin Signet:ON Liquid:Enable Elements Liquid:ON Liquidtestnet:Enable Elements Liquidtestnet:ON Bisq:Enable Bisq:ON +Unfurl:Enable Unfurl:ON EOF cat $input | sed -e 's/^/"/' -e 's/:/" "/g' -e 's/$/"/' >$output @@ -785,6 +809,12 @@ else ELEMENTS_INSTALL=OFF fi +if [ "${BITCOIN_INSTALL}" = ON -o "${ELEMENTS_INSTALL}" = ON ];then + ELECTRS_INSTALL=ON +else + ELECTRS_INSTALL=OFF +fi + if grep Bisq $tempfile >/dev/null 2>&1;then BISQ_INSTALL=ON BISQ_MAINNET_ENABLE=ON @@ -793,6 +823,12 @@ else BISQ_MAINNET_ENABLE=OFF fi +if grep Unfurl $tempfile >/dev/null 2>&1;then + UNFURL_INSTALL=ON +else + UNFURL_INSTALL=OFF +fi + ################## ## dialog part 2 # ################## @@ -1074,52 +1110,55 @@ fi # Bitcoin -> Electrs installation # ################################### -echo "[*] Creating Bitcoin Electrs data folder" -osSudo "${ROOT_USER}" mkdir -p "${BITCOIN_ELECTRS_HOME}" -osSudo "${ROOT_USER}" chown -R "${BITCOIN_USER}:${BITCOIN_GROUP}" "${BITCOIN_ELECTRS_HOME}" -if [ "${BITCOIN_MAINNET_ENABLE}" = ON ];then - osSudo "${ROOT_USER}" chown -R "${BITCOIN_USER}:${BITCOIN_GROUP}" "${ELECTRS_MAINNET_DATA}" +if [ "${ELECTRS_INSTALL}" = ON ];then + + echo "[*] Creating Bitcoin Electrs data folder" + osSudo "${ROOT_USER}" mkdir -p "${BITCOIN_ELECTRS_HOME}" + osSudo "${ROOT_USER}" chown -R "${BITCOIN_USER}:${BITCOIN_GROUP}" "${BITCOIN_ELECTRS_HOME}" + if [ "${BITCOIN_MAINNET_ENABLE}" = ON ];then + osSudo "${ROOT_USER}" chown -R "${BITCOIN_USER}:${BITCOIN_GROUP}" "${ELECTRS_MAINNET_DATA}" + fi + if [ "${BITCOIN_TESTNET_ENABLE}" = ON ];then + osSudo "${ROOT_USER}" chown -R "${BITCOIN_USER}:${BITCOIN_GROUP}" "${ELECTRS_TESTNET_DATA}" + fi + if [ "${BITCOIN_SIGNET_ENABLE}" = ON ];then + osSudo "${ROOT_USER}" chown -R "${BITCOIN_USER}:${BITCOIN_GROUP}" "${ELECTRS_SIGNET_DATA}" + fi + + echo "[*] Cloning Bitcoin Electrs repo from ${BITCOIN_ELECTRS_REPO_URL}" + osSudo "${BITCOIN_USER}" git config --global advice.detachedHead false + osSudo "${BITCOIN_USER}" git clone --branch "${BITCOIN_ELECTRS_REPO_BRANCH}" "${BITCOIN_ELECTRS_REPO_URL}" "${BITCOIN_HOME}/${BITCOIN_ELECTRS_REPO_NAME}" + + echo "[*] Checking out Electrs ${BITCOIN_ELECTRS_LATEST_RELEASE}" + osSudo "${BITCOIN_USER}" sh -c "cd ${BITCOIN_HOME}/${BITCOIN_ELECTRS_REPO_NAME} && git checkout ${BITCOIN_ELECTRS_LATEST_RELEASE}" + + case $OS in + FreeBSD) + echo "[*] Installing Rust from pkg install" + ;; + Debian) + echo "[*] Installing Rust from rustup.rs" + osSudo "${BITCOIN_USER}" sh -c "cd ${BITCOIN_ELECTRS_HOME} && curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y" + ;; + esac + + echo "[*] Building Bitcoin Electrs release binary" + osSudo "${BITCOIN_USER}" sh -c "cd ${BITCOIN_ELECTRS_HOME} && cargo run --release --bin electrs -- --version" || true + + case $OS in + FreeBSD) + echo "[*] Patching Bitcoin Electrs code for FreeBSD" + osSudo "${BITCOIN_USER}" sh -c "cd \"${BITCOIN_HOME}/.cargo/registry/src/github.com-1ecc6299db9ec823/sysconf-0.3.4\" && patch -p1 < \"${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/freebsd/sysconf.patch\"" + osSudo "${BITCOIN_USER}" sh -c "cd \"${BITCOIN_ELECTRS_HOME}/src/new_index/\" && sed -i.bak -e s/Snappy/None/ db.rs && rm db.rs.bak" + osSudo "${BITCOIN_USER}" sh -c "cd \"${BITCOIN_ELECTRS_HOME}/src/bin/\" && sed -i.bak -e 's/from_secs(5)/from_secs(1)/' electrs.rs && rm electrs.rs.bak" + ;; + Debian) + ;; + esac + + echo "[*] Building Bitcoin Electrs release binary" + osSudo "${BITCOIN_USER}" sh -c "cd ${BITCOIN_ELECTRS_HOME} && cargo run --release --bin electrs -- --version" fi -if [ "${BITCOIN_TESTNET_ENABLE}" = ON ];then - osSudo "${ROOT_USER}" chown -R "${BITCOIN_USER}:${BITCOIN_GROUP}" "${ELECTRS_TESTNET_DATA}" -fi -if [ "${BITCOIN_SIGNET_ENABLE}" = ON ];then - osSudo "${ROOT_USER}" chown -R "${BITCOIN_USER}:${BITCOIN_GROUP}" "${ELECTRS_SIGNET_DATA}" -fi - -echo "[*] Cloning Bitcoin Electrs repo from ${BITCOIN_ELECTRS_REPO_URL}" -osSudo "${BITCOIN_USER}" git config --global advice.detachedHead false -osSudo "${BITCOIN_USER}" git clone --branch "${BITCOIN_ELECTRS_REPO_BRANCH}" "${BITCOIN_ELECTRS_REPO_URL}" "${BITCOIN_HOME}/${BITCOIN_ELECTRS_REPO_NAME}" - -echo "[*] Checking out Electrs ${BITCOIN_ELECTRS_LATEST_RELEASE}" -osSudo "${BITCOIN_USER}" sh -c "cd ${BITCOIN_HOME}/${BITCOIN_ELECTRS_REPO_NAME} && git checkout ${BITCOIN_ELECTRS_LATEST_RELEASE}" - -case $OS in - FreeBSD) - echo "[*] Installing Rust from pkg install" - ;; - Debian) - echo "[*] Installing Rust from rustup.rs" - osSudo "${BITCOIN_USER}" sh -c "cd ${BITCOIN_ELECTRS_HOME} && curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y" - ;; -esac - -echo "[*] Building Bitcoin Electrs release binary" -osSudo "${BITCOIN_USER}" sh -c "cd ${BITCOIN_ELECTRS_HOME} && cargo run --release --bin electrs -- --version" || true - -case $OS in - FreeBSD) - echo "[*] Patching Bitcoin Electrs code for FreeBSD" - osSudo "${BITCOIN_USER}" sh -c "cd \"${BITCOIN_HOME}/.cargo/registry/src/github.com-1ecc6299db9ec823/sysconf-0.3.4\" && patch -p1 < \"${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/freebsd/sysconf.patch\"" - osSudo "${BITCOIN_USER}" sh -c "cd \"${BITCOIN_ELECTRS_HOME}/src/new_index/\" && sed -i.bak -e s/Snappy/None/ db.rs && rm db.rs.bak" - osSudo "${BITCOIN_USER}" sh -c "cd \"${BITCOIN_ELECTRS_HOME}/src/bin/\" && sed -i.bak -e 's/from_secs(5)/from_secs(1)/' electrs.rs && rm electrs.rs.bak" - ;; - Debian) - ;; -esac - -echo "[*] Building Bitcoin Electrs release binary" -osSudo "${BITCOIN_USER}" sh -c "cd ${BITCOIN_ELECTRS_HOME} && cargo run --release --bin electrs -- --version" ################################## # Liquid -> Electrs installation # @@ -1246,6 +1285,50 @@ if [ "${BISQ_INSTALL}" = ON ];then esac fi +####################### +# Unfurl installation # +####################### + +if [ "${UNFURL_INSTALL}" = ON ];then + + echo "[*] Creating Unfurl user" + osGroupCreate "${UNFURL_GROUP}" + osUserCreate "${UNFURL_USER}" "${UNFURL_HOME}" "${UNFURL_GROUP}" + osSudo "${ROOT_USER}" chsh -s `which zsh` "${UNFURL_USER}" + + echo "[*] Creating Unfurl folder" + osSudo "${ROOT_USER}" mkdir -p "${UNFURL_HOME}" + osSudo "${ROOT_USER}" chown -R "${UNFURL_USER}:${UNFURL_GROUP}" "${UNFURL_HOME}" + osSudo "${UNFURL_USER}" touch "${UNFURL_HOME}/.zshrc" + + echo "[*] Insalling Unfurl source" + case $OS in + + FreeBSD) + echo "[*] FIXME: Unfurl must be installed manually on FreeBSD" + ;; + + Debian) + echo "[*] Installing packages for Unfurl" + osPackageInstall ${DEBIAN_UNFURL_PKG[@]} + echo "[*] Cloning Mempool (Unfurl) repo from ${UNFURL_REPO_URL}" + osSudo "${UNFURL_USER}" git config --global pull.rebase true + osSudo "${UNFURL_USER}" git config --global advice.detachedHead false + osSudo "${UNFURL_USER}" git clone --branch "${UNFURL_REPO_BRANCH}" "${UNFURL_REPO_URL}" "${UNFURL_HOME}/${UNFURL_REPO_NAME}" + osSudo "${UNFURL_USER}" ln -s unfurl/production/unfurl-build upgrade + osSudo "${UNFURL_USER}" ln -s unfurl/production/unfurl-kill stop + osSudo "${UNFURL_USER}" ln -s unfurl/production/unfurl-start start + echo "[*] Installing nvm.sh from GitHub" + osSudo "${UNFURL_USER}" sh -c 'curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | zsh' + + echo "[*] Building NodeJS via nvm.sh" + osSudo "${UNFURL_USER}" zsh -c 'source ~/.zshrc ; nvm install v16.16.0 --shared-zlib' + + ;; + esac + +fi + ################################ # Bitcoin instance for Mainnet # ################################ diff --git a/production/mempool-config.unfurl.json b/production/mempool-config.unfurl.json new file mode 100644 index 000000000..5cf67d5ac --- /dev/null +++ b/production/mempool-config.unfurl.json @@ -0,0 +1,13 @@ +{ + "SERVER": { + "HOST": "https://mempool.space", + "HTTP_PORT": 8001 + }, + "MEMPOOL": { + "HTTP_HOST": "https://mempool.space", + "HTTP_PORT": 443 + }, + "PUPPETEER": { + "CLUSTER_SIZE": 8 + } +} diff --git a/production/unfurl-build b/production/unfurl-build new file mode 100755 index 000000000..5b838e0ae --- /dev/null +++ b/production/unfurl-build @@ -0,0 +1,62 @@ +#!/usr/bin/env zsh +PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin:$HOME/bin +HOSTNAME=$(hostname) +LOCATION=$(hostname|cut -d . -f2) +LOCKFILE="${HOME}/lock" +REF=$(echo "${1:=origin/master}"|sed -e 's!:!/!') + +if [ -f "${LOCKFILE}" ];then + echo "upgrade already running? check lockfile ${LOCKFILE}" + exit 1 +fi + +# on exit, remove lockfile but preserve exit code +trap "rv=\$?; rm -f "${LOCKFILE}"; exit \$rv" INT TERM EXIT + +# create lockfile +touch "${LOCKFILE}" + +# notify logged in users +echo "Upgrading unfurler to ${REF}" | wall + +update_repo() +{ + echo "[*] Upgrading unfurler to ${REF}" + cd "$HOME/unfurl/unfurler" || exit 1 + + git fetch origin || exit 1 + for remote in origin;do + git remote add "${remote}" "https://github.com/${remote}/mempool" >/dev/null 2>&1 + git fetch "${remote}" || exit 1 + done + + if [ $(git tag -l "${REF}") ];then + git reset --hard "tags/${REF}" || exit 1 + elif [ $(git branch -r -l "origin/${REF}") ];then + git reset --hard "origin/${REF}" || exit 1 + else + git reset --hard "${REF}" || exit 1 + fi + export HASH=$(git rev-parse HEAD) +} + +build_backend() +{ + echo "[*] Building backend for unfurler" + [ -z "${HASH}" ] && exit 1 + cd "$HOME/unfurl/unfurler" || exit 1 + if [ ! -e "config.json" ];then + cp "${HOME}/unfurl/production/mempool-config.unfurl.json" "config.json" + fi + npm install || exit 1 + npm run build || exit 1 +} + +update_repo +build_backend + +# notify everyone +echo "${HOSTNAME} unfurl updated to \`${REF}\` @ \`${HASH}\`" | /usr/local/bin/keybase chat send --nonblock --channel general mempool.dev +echo "${HOSTNAME} unfurl updated to \`${REF}\` @ \`${HASH}\`" | /usr/local/bin/keybase chat send --nonblock --channel general "mempool.ops.${LOCATION}" + +exit 0 diff --git a/production/unfurl-kill b/production/unfurl-kill new file mode 100755 index 000000000..ae48552c2 --- /dev/null +++ b/production/unfurl-kill @@ -0,0 +1,2 @@ +#!/usr/bin/env zsh +killall sh node diff --git a/production/unfurl-start b/production/unfurl-start new file mode 100755 index 000000000..29b5ddf3e --- /dev/null +++ b/production/unfurl-start @@ -0,0 +1,6 @@ +#!/usr/bin/env zsh +export NVM_DIR="$HOME/.nvm" +source "$NVM_DIR/nvm.sh" + +cd "${HOME}/unfurl/unfurler/" && \ +screen -dmS "unfurl" sh -c 'while true;do npm run start-production;sleep 1;done' From 16d45736ed7cb9165d19a0a481d644b4212bef1a Mon Sep 17 00:00:00 2001 From: Stephan Oeste Date: Sat, 30 Jul 2022 13:58:41 +0200 Subject: [PATCH 055/105] Fix tor config for FreeBSD on prod installer --- production/install | 39 +++++++++++++++++++++++++++++++-------- production/torrc | 8 -------- 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/production/install b/production/install index 729ff33e0..4cb37e6af 100755 --- a/production/install +++ b/production/install @@ -178,7 +178,7 @@ case $OS in ROOT_GROUP=wheel ROOT_HOME=/root TOR_HOME=/var/db/tor - TOR_CONFIGURATION=/usr/local/etc/tor/torrc + TOR_CONFIGURATION=/var/db/tor/torrc TOR_RESOURCES=/var/db/tor TOR_PKG=tor TOR_USER=_tor @@ -277,6 +277,8 @@ BISQ_USER=bisq BISQ_GROUP=bisq # bisq home folder, needs about 1GB BISQ_HOME=/bisq +# tor HS folder +BISQ_TOR_HS=bisq # liquid user/group ELEMENTS_USER=elements @@ -287,6 +289,8 @@ ELEMENTS_HOME=/elements ELECTRS_HOME=/electrs # elements electrs source/binaries ELEMENTS_ELECTRS_HOME=${ELEMENTS_HOME}/electrs +# tor HS folder +LIQUID_TOR_HS=liquid # minfee user/group MINFEE_USER=minfee @@ -941,14 +945,33 @@ if [ "${TOR_INSTALL}" = ON ];then echo "[*] Installing Tor base configuration" osSudo "${ROOT_USER}" install -c -m 644 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/torrc" "${TOR_HOME}/torrc" + osSudo "${ROOT_USER}" sed -i.orig "s!__TOR_RESOURCES__!${TOR_RESOURCES}!" "${TOR_CONFIGURATION}" - echo "[*] Adding Tor HS configuration" - if ! grep "${MEMPOOL_TOR_HS}" /etc/tor/torrc >/dev/null 2>&1;then - osSudo "${ROOT_USER}" /bin/sh -c "echo HiddenServiceDir ${TOR_RESOURCES}/${MEMPOOL_TOR_HS}/ >> ${TOR_CONFIGURATION}" - osSudo "${ROOT_USER}" /bin/sh -c "echo HiddenServicePort 80 127.0.0.1:81 >> ${TOR_CONFIGURATION}" - osSudo "${ROOT_USER}" /bin/sh -c "echo HiddenServiceVersion 3 >> ${TOR_CONFIGURATION}" - else - osSudo "${ROOT_USER}" sed -i.orig "s!__TOR_RESOURCES__!${TOR_RESOURCES}!" "${TOR_CONFIGURATION}" + echo "[*] Adding Tor HS configuration for Mempool" + if [ "${MEMPOOL_ENABLE}" = "ON" ];then + if ! grep "${MEMPOOL_TOR_HS}" "${TOR_CONFIGURATION}" >/dev/null 2>&1;then + osSudo "${ROOT_USER}" /bin/sh -c "echo HiddenServiceDir ${TOR_RESOURCES}/${MEMPOOL_TOR_HS}/ >> ${TOR_CONFIGURATION}" + osSudo "${ROOT_USER}" /bin/sh -c "echo HiddenServicePort 80 127.0.0.1:81 >> ${TOR_CONFIGURATION}" + osSudo "${ROOT_USER}" /bin/sh -c "echo HiddenServiceVersion 3 >> ${TOR_CONFIGURATION}" + fi + fi + + echo "[*] Adding Tor HS configuration for Bisq" + if [ "${BISQ_ENABLE}" = "ON" ];then + if ! grep "${BISQ_TOR_HS}" "${TOR_CONFIGURATION}" >/dev/null 2>&1;then + osSudo "${ROOT_USER}" /bin/sh -c "echo HiddenServiceDir ${TOR_RESOURCES}/${BISQ_TOR_HS}/ >> ${TOR_CONFIGURATION}" + osSudo "${ROOT_USER}" /bin/sh -c "echo HiddenServicePort 80 127.0.0.1:82 >> ${TOR_CONFIGURATION}" + osSudo "${ROOT_USER}" /bin/sh -c "echo HiddenServiceVersion 3 >> ${TOR_CONFIGURATION}" + fi + fi + + echo "[*] Adding Tor HS configuration for Liquid" + if [ "${LIQUID_ENABLE}" = "ON" ];then + if ! grep "${LIQUID_TOR_HS}" "${TOR_CONFIGURATION}" >/dev/null 2>&1;then + osSudo "${ROOT_USER}" /bin/sh -c "echo HiddenServiceDir ${TOR_RESOURCES}/${LIQUID_TOR_HS}/ >> ${TOR_CONFIGURATION}" + osSudo "${ROOT_USER}" /bin/sh -c "echo HiddenServicePort 80 127.0.0.1:83 >> ${TOR_CONFIGURATION}" + osSudo "${ROOT_USER}" /bin/sh -c "echo HiddenServiceVersion 3 >> ${TOR_CONFIGURATION}" + fi fi case $OS in diff --git a/production/torrc b/production/torrc index 454cafba0..344ebc6e4 100644 --- a/production/torrc +++ b/production/torrc @@ -13,11 +13,3 @@ CookieAuthFileGroupReadable 1 HiddenServiceDir __TOR_RESOURCES__/mempool HiddenServicePort 80 127.0.0.1:81 HiddenServiceVersion 3 - -HiddenServiceDir __TOR_RESOURCES__/bisq -HiddenServicePort 80 127.0.0.1:82 -HiddenServiceVersion 3 - -HiddenServiceDir __TOR_RESOURCES__/liquid -HiddenServicePort 80 127.0.0.1:83 -HiddenServiceVersion 3 From 230333c4bdec13819b9ac20ec44774aecc67d717 Mon Sep 17 00:00:00 2001 From: wiz Date: Sat, 30 Jul 2022 15:25:02 +0200 Subject: [PATCH 056/105] Separate electrs into bitcoin electrs and elements electrs --- production/install | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/production/install b/production/install index 053ffef70..6022a9a5e 100755 --- a/production/install +++ b/production/install @@ -51,12 +51,11 @@ BISQ_MAINNET_ENABLE=ON ELEMENTS_LIQUID_ENABLE=ON ELEMENTS_LIQUIDTESTNET_ENABLE=ON -# install Electrs -ELECTRS_INSTALL=ON - # enable lightmode and disable compaction to fit on 1TB SSD drive +BITCOIN_ELECTRS_INSTALL=ON BITCOIN_ELECTRS_LIGHT_MODE=ON BITCOIN_ELECTRS_COMPACTION=OFF +ELEMENTS_ELECTRS_INSTALL=ON ELEMENTS_ELECTRS_LIGHT_MODE=ON ELEMENTS_ELECTRS_COMPACTION=OFF @@ -809,10 +808,16 @@ else ELEMENTS_INSTALL=OFF fi -if [ "${BITCOIN_INSTALL}" = ON -o "${ELEMENTS_INSTALL}" = ON ];then - ELECTRS_INSTALL=ON +if [ "${BITCOIN_MAINNET_ENABLE}" = ON -o "${BITCOIN_TESTNET_ENABLE}" = ON -o "${BITCOIN_SIGNET_ENABLE}" = ON ];then + BITCOIN_ELECTRS_INSTALL=ON else - ELECTRS_INSTALL=OFF + BITCOIN_ELECTRS_INSTALL=OFF +fi + +if [ "${ELEMENTS_LIQUID_ENABLE}" = ON -o "${ELEMENTS_LIQUIDTESTNET_ENABLE}" = ON ];then + ELEMENTS_ELECTRS_INSTALL=ON +else + ELEMENTS_ELECTRS_INSTALL=OFF fi if grep Bisq $tempfile >/dev/null 2>&1;then @@ -1110,7 +1115,7 @@ fi # Bitcoin -> Electrs installation # ################################### -if [ "${ELECTRS_INSTALL}" = ON ];then +if [ "${BITCOIN_ELECTRS_INSTALL}" = ON ];then echo "[*] Creating Bitcoin Electrs data folder" osSudo "${ROOT_USER}" mkdir -p "${BITCOIN_ELECTRS_HOME}" @@ -1164,14 +1169,18 @@ fi # Liquid -> Electrs installation # ################################## -if [ "${ELEMENTS_INSTALL}" = ON ;then +if [ "${ELEMENTS_ELECTRS_INSTALL}" = ON ];then echo "[*] Creating Liquid Electrs data folder" osSudo "${ROOT_USER}" mkdir -p "${ELEMENTS_ELECTRS_HOME}" osSudo "${ROOT_USER}" chown -R "${ELEMENTS_USER}:${ELEMENTS_GROUP}" "${ELEMENTS_HOME}" osSudo "${ROOT_USER}" chown -R "${ELEMENTS_USER}:${ELEMENTS_GROUP}" "${ELEMENTS_ELECTRS_HOME}" - osSudo "${ROOT_USER}" chown -R "${ELEMENTS_USER}:${ELEMENTS_GROUP}" "${ELECTRS_LIQUID_DATA}" - osSudo "${ROOT_USER}" chown -R "${ELEMENTS_USER}:${ELEMENTS_GROUP}" "${ELECTRS_LIQUIDTESTNET_DATA}" + if [ "${ELEMENTS_LIQUID_ENABLE}" = ON ];then + osSudo "${ROOT_USER}" chown -R "${ELEMENTS_USER}:${ELEMENTS_GROUP}" "${ELECTRS_LIQUID_DATA}" + fi + if [ "${ELEMENTS_LIQUIDTESTNET_ENABLE}" = ON ];then + osSudo "${ROOT_USER}" chown -R "${ELEMENTS_USER}:${ELEMENTS_GROUP}" "${ELECTRS_LIQUIDTESTNET_DATA}" + fi echo "[*] Cloning Liquid Electrs repo from ${ELEMENTS_ELECTRS_REPO_URL}" osSudo "${ELEMENTS_USER}" git config --global advice.detachedHead false From c9c5d471ee84d2389f9a9ccf691e37812fe2f2d9 Mon Sep 17 00:00:00 2001 From: wiz Date: Sat, 30 Jul 2022 15:30:55 +0200 Subject: [PATCH 057/105] Fix FreeBSD path for torrc --- production/install | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/production/install b/production/install index 44488add4..8612e0f98 100755 --- a/production/install +++ b/production/install @@ -183,7 +183,7 @@ case $OS in ROOT_GROUP=wheel ROOT_HOME=/root TOR_HOME=/var/db/tor - TOR_CONFIGURATION=/var/db/tor/torrc + TOR_CONFIGURATION=/usr/local/etc/tor/torrc TOR_RESOURCES=/var/db/tor TOR_PKG=tor TOR_USER=_tor From cf38d9cf3a67ba2d72a7c91ece7002a0b7a85898 Mon Sep 17 00:00:00 2001 From: wiz Date: Sat, 30 Jul 2022 15:32:51 +0200 Subject: [PATCH 058/105] Remove TOR_HOME variable in prod/install --- production/install | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/production/install b/production/install index 8612e0f98..94c9936bf 100755 --- a/production/install +++ b/production/install @@ -182,7 +182,6 @@ case $OS in ROOT_USER=root ROOT_GROUP=wheel ROOT_HOME=/root - TOR_HOME=/var/db/tor TOR_CONFIGURATION=/usr/local/etc/tor/torrc TOR_RESOURCES=/var/db/tor TOR_PKG=tor @@ -198,7 +197,6 @@ case $OS in ROOT_USER=root ROOT_GROUP=root ROOT_HOME=/root - TOR_HOME=/etc/tor TOR_CONFIGURATION=/etc/tor/torrc TOR_RESOURCES=/var/lib/tor TOR_PKG=tor @@ -985,7 +983,7 @@ if [ "${TOR_INSTALL}" = ON ];then osPackageInstall "${TOR_PKG}" echo "[*] Installing Tor base configuration" - osSudo "${ROOT_USER}" install -c -m 644 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/torrc" "${TOR_HOME}/torrc" + osSudo "${ROOT_USER}" install -c -m 644 "${MEMPOOL_HOME}/${MEMPOOL_REPO_NAME}/production/torrc" "${TOR_CONFIGURATION}" osSudo "${ROOT_USER}" sed -i.orig "s!__TOR_RESOURCES__!${TOR_RESOURCES}!" "${TOR_CONFIGURATION}" echo "[*] Adding Tor HS configuration for Mempool" From 5264738e5f4210e8bebe623d9b701992a1b8cd46 Mon Sep 17 00:00:00 2001 From: Antoni Spaanderman <56turtle56@gmail.com> Date: Fri, 29 Jul 2022 20:53:19 +0200 Subject: [PATCH 059/105] use lnd rest api --- backend/mempool-config.sample.json | 4 +- backend/package-lock.json | 892 +----------------- backend/package.json | 5 +- .../api/lightning/lightning-api.interface.ts | 104 +- backend/src/api/lightning/lnd/lnd-api.ts | 46 +- backend/src/config.ts | 4 +- .../src/tasks/lightning/node-sync.service.ts | 115 ++- .../tasks/lightning/stats-updater.service.ts | 25 +- 8 files changed, 197 insertions(+), 998 deletions(-) diff --git a/backend/mempool-config.sample.json b/backend/mempool-config.sample.json index b544a3f9b..312d9d18d 100644 --- a/backend/mempool-config.sample.json +++ b/backend/mempool-config.sample.json @@ -79,8 +79,8 @@ }, "LND": { "TLS_CERT_PATH": "tls.cert", - "MACAROON_PATH": "admin.macaroon", - "SOCKET": "localhost:10009" + "MACAROON_PATH": "readonly.macaroon", + "REST_API_URL": "https://localhost:8080" }, "SOCKS5PROXY": { "ENABLED": false, diff --git a/backend/package-lock.json b/backend/package-lock.json index e724ac35b..b23a7f874 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -13,10 +13,8 @@ "@types/node": "^16.11.41", "axios": "~0.27.2", "bitcoinjs-lib": "6.0.1", - "bolt07": "^1.8.1", "crypto-js": "^4.0.0", "express": "^4.18.0", - "lightning": "^5.16.3", "maxmind": "^4.3.6", "mysql2": "2.3.3", "node-worker-threads-pool": "^1.5.1", @@ -97,36 +95,6 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "node_modules/@grpc/grpc-js": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.6.7.tgz", - "integrity": "sha512-eBM03pu9hd3VqDQG+kHahiG1x80RGkkqqRb1Pchcwqej/KkAH95gAvKs6laqaHCycYaPK+TKuNQnOz9UXYA8qw==", - "dependencies": { - "@grpc/proto-loader": "^0.6.4", - "@types/node": ">=12.12.47" - }, - "engines": { - "node": "^8.13.0 || >=10.10.0" - } - }, - "node_modules/@grpc/proto-loader": { - "version": "0.6.13", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.13.tgz", - "integrity": "sha512-FjxPYDRTn6Ec3V0arm1FtSpmP6V50wuph2yILpyvTKzjc76oDdoihXqM1DzOW5ubvCC8GivfCnNtfaRE8myJ7g==", - "dependencies": { - "@types/long": "^4.0.1", - "lodash.camelcase": "^4.3.0", - "long": "^4.0.0", - "protobufjs": "^6.11.3", - "yargs": "^16.2.0" - }, - "bin": { - "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/@humanwhocodes/config-array": { "version": "0.9.5", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", @@ -213,74 +181,16 @@ "node": ">= 8" } }, - "node_modules/@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" - }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" - }, - "node_modules/@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" - }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", - "dependencies": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" - }, - "node_modules/@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" - }, "node_modules/@types/body-parser": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", "integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==", + "dev": true, "dependencies": { "@types/connect": "*", "@types/node": "*" } }, - "node_modules/@types/caseless": { - "version": "0.12.2", - "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", - "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==" - }, "node_modules/@types/compression": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.7.2.tgz", @@ -294,6 +204,7 @@ "version": "3.4.34", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz", "integrity": "sha512-ePPA/JuI+X0vb+gSWlPKOY0NdNAie/rPUqX2GUPpbZwiKTkSPhjXWuee47E4MtE54QVzGCQMQkAL6JhV2E1+cQ==", + "dev": true, "dependencies": { "@types/node": "*" } @@ -308,6 +219,7 @@ "version": "4.17.13", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", + "dev": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.18", @@ -319,6 +231,7 @@ "version": "4.17.28", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz", "integrity": "sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig==", + "dev": true, "dependencies": { "@types/node": "*", "@types/qs": "*", @@ -331,15 +244,11 @@ "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "dev": true }, - "node_modules/@types/long": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", - "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" - }, "node_modules/@types/mime": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz", - "integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==" + "integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==", + "dev": true }, "node_modules/@types/node": { "version": "16.11.41", @@ -349,55 +258,30 @@ "node_modules/@types/qs": { "version": "6.9.7", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", + "dev": true }, "node_modules/@types/range-parser": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", - "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" - }, - "node_modules/@types/request": { - "version": "2.48.8", - "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.8.tgz", - "integrity": "sha512-whjk1EDJPcAR2kYHRbFl/lKeeKYTi05A15K9bnLInCVroNDCtXce57xKdI0/rQaA3K+6q0eFyUBPmqfSndUZdQ==", - "dependencies": { - "@types/caseless": "*", - "@types/node": "*", - "@types/tough-cookie": "*", - "form-data": "^2.5.0" - } - }, - "node_modules/@types/request/node_modules/form-data": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", - "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 0.12" - } + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", + "dev": true }, "node_modules/@types/serve-static": { "version": "1.13.8", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.8.tgz", "integrity": "sha512-MoJhSQreaVoL+/hurAZzIm8wafFR6ajiTM1m4A0kv6AGeVBl4r4pOV8bGFrjjq1sGxDTnCoF8i22o0/aE5XCyA==", + "dev": true, "dependencies": { "@types/mime": "*", "@types/node": "*" } }, - "node_modules/@types/tough-cookie": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz", - "integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==" - }, "node_modules/@types/ws": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==", + "dev": true, "dependencies": { "@types/node": "*" } @@ -838,6 +722,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, "engines": { "node": ">=8" } @@ -846,6 +731,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -870,24 +756,6 @@ "node": ">=8" } }, - "node_modules/async": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", - "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" - }, - "node_modules/asyncjs-util": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/asyncjs-util/-/asyncjs-util-1.2.9.tgz", - "integrity": "sha512-U9imS8ehJA6DPNdBdvoLcIRDFh7yzI9J93CC8/2obk8gUSIy8KKhmCqYe+3NlISJhxLLi8aWmVL1Gkb3dz1xhg==", - "dependencies": { - "async": "3.2.3" - } - }, - "node_modules/asyncjs-util/node_modules/async": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", - "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==" - }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -929,19 +797,6 @@ "node": ">=8.0.0" } }, - "node_modules/bip66": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/bip66/-/bip66-1.1.5.tgz", - "integrity": "sha512-nemMHz95EmS38a26XbbdxIYj5csHd3RMP3H5bwQknX0WYHF01qhpufP42mLOwVICuH2JmhIhXiWs89MfUGL7Xw==", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/bitcoin-ops": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/bitcoin-ops/-/bitcoin-ops-1.4.1.tgz", - "integrity": "sha512-pef6gxZFztEhaE9RY9HmWVmiIHqCb2OyS4HPKkpc6CIiiOa3Qmuoylxc5P2EkU3w+5eTSifI9SEZC88idAIGow==" - }, "node_modules/bitcoinjs-lib": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.0.1.tgz", @@ -959,11 +814,6 @@ "node": ">=8.0.0" } }, - "node_modules/bn.js": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", - "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" - }, "node_modules/body-parser": { "version": "1.20.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", @@ -987,22 +837,6 @@ "npm": "1.2.8000 || >= 1.4.16" } }, - "node_modules/bolt07": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/bolt07/-/bolt07-1.8.2.tgz", - "integrity": "sha512-jq1b/ZdMambhh+yi+pm+1PJBAnlYvQYljaBgSajvVAINHrHg32ovCBra8d0ADE3BAoj6G/tK7OSV4t/yT9A+/g==", - "dependencies": { - "bn.js": "5.2.1" - } - }, - "node_modules/bolt09": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/bolt09/-/bolt09-0.2.3.tgz", - "integrity": "sha512-xEt5GE6pXB8wMIWHAoyF28k0Yt2rFqIou1LCyIeNadAOQhu/F7GTjZwreFwLl07YYkhOH23avewRt5PD8JnKKg==", - "engines": { - "node": ">=12.20.0" - } - }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1072,17 +906,6 @@ "node": ">=6" } }, - "node_modules/cbor": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/cbor/-/cbor-8.1.0.tgz", - "integrity": "sha512-DwGjNW9omn6EwP70aXsn7FQJx5kO12tX0bZkaTjzdVFM6/7nhA4t0EENocKGx6D2Bch9PE2KzCUf5SceBdeijg==", - "dependencies": { - "nofilter": "^3.1.0" - }, - "engines": { - "node": ">=12.19" - } - }, "node_modules/cipher-base": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", @@ -1092,20 +915,11 @@ "safe-buffer": "^5.0.1" } }, - "node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -1116,7 +930,8 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "node_modules/combined-stream": { "version": "1.0.8", @@ -1269,29 +1084,11 @@ "node": ">=6.0.0" } }, - "node_modules/ecpair": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ecpair/-/ecpair-2.0.1.tgz", - "integrity": "sha512-iT3wztQMeE/nDTlfnAg8dAFUfBS7Tq2BXzq3ae6L+pWgFU0fQ3l0woTzdTBrJV3OxBjxbzjq8EQhAbEmJNWFSw==", - "dependencies": { - "randombytes": "^2.1.0", - "typeforce": "^1.18.0", - "wif": "^2.0.6" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, "node_modules/encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -1300,14 +1097,6 @@ "node": ">= 0.8" } }, - "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "engines": { - "node": ">=6" - } - }, "node_modules/escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -1849,14 +1638,6 @@ "is-property": "^1.0.2" } }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, "node_modules/get-intrinsic": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", @@ -2047,22 +1828,6 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, - "node_modules/invoices": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/invoices/-/invoices-2.0.7.tgz", - "integrity": "sha512-2kpjok/83zOTnb4tbV+RbJz7LuGVzj/GZ+jwsC7FxMqwLAf4Sf6OESNM3uuamX9oeFRo44Vip3wn1aX+9D2m8w==", - "dependencies": { - "bech32": "2.0.0", - "bitcoinjs-lib": "6.0.1", - "bn.js": "5.2.1", - "bolt07": "1.8.2", - "bolt09": "0.2.3", - "tiny-secp256k1": "2.2.1" - }, - "engines": { - "node": ">=12.20.0" - } - }, "node_modules/ip": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", @@ -2085,14 +1850,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "engines": { - "node": ">=8" - } - }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -2150,57 +1907,6 @@ "node": ">= 0.8.0" } }, - "node_modules/lightning": { - "version": "5.16.3", - "resolved": "https://registry.npmjs.org/lightning/-/lightning-5.16.3.tgz", - "integrity": "sha512-ghban3KbqkbzahwIp4NAtuhc8xIurVcCXAd7tV6qGkFYKZAy9loIvFrhZqoWF4A4jnaKbRnJPCaxzJ8JwPl3EA==", - "dependencies": { - "@grpc/grpc-js": "1.6.7", - "@grpc/proto-loader": "0.6.13", - "@types/express": "4.17.13", - "@types/node": "17.0.41", - "@types/request": "2.48.8", - "@types/ws": "8.5.3", - "async": "3.2.4", - "asyncjs-util": "1.2.9", - "bitcoinjs-lib": "6.0.1", - "bn.js": "5.2.1", - "body-parser": "1.20.0", - "bolt07": "1.8.2", - "bolt09": "0.2.3", - "cbor": "8.1.0", - "ecpair": "2.0.1", - "express": "4.18.1", - "invoices": "2.0.7", - "psbt": "2.6.0", - "tiny-secp256k1": "2.2.1", - "type-fest": "2.13.0" - }, - "engines": { - "node": ">=12.20" - } - }, - "node_modules/lightning/node_modules/@types/node": { - "version": "17.0.41", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.41.tgz", - "integrity": "sha512-xA6drNNeqb5YyV5fO3OAEsnXLfO7uF0whiOfPTz5AeDo8KeZFmODKnvwPymMNO8qE/an8pVY/O50tig2SQCrGw==" - }, - "node_modules/lightning/node_modules/type-fest": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.13.0.tgz", - "integrity": "sha512-lPfAm42MxE4/456+QyIaaVBAwgpJb6xZ8PRu09utnhPdWwcyj9vgy6Sq0Z5yNbJ21EdxB5dRU/Qg8bsyAMtlcw==", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -2418,14 +2124,6 @@ "resolved": "https://registry.npmjs.org/node-worker-threads-pool/-/node-worker-threads-pool-1.5.1.tgz", "integrity": "sha512-7TXAhpMm+jO4MfESxYLtMGSnJWv+itdNHMdaFmeZuPXxwFGU90mtEB42BciUULXOUAxYBfXILAuvrSG3rQZ7mw==" }, - "node_modules/nofilter": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-3.1.0.tgz", - "integrity": "sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==", - "engines": { - "node": ">=12.19" - } - }, "node_modules/object-inspect": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", @@ -2559,31 +2257,6 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/protobufjs": { - "version": "6.11.3", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.3.tgz", - "integrity": "sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg==", - "hasInstallScript": true, - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/long": "^4.0.1", - "@types/node": ">=13.7.0", - "long": "^4.0.0" - }, - "bin": { - "pbjs": "bin/pbjs", - "pbts": "bin/pbts" - } - }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -2596,22 +2269,6 @@ "node": ">= 0.10" } }, - "node_modules/psbt": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/psbt/-/psbt-2.6.0.tgz", - "integrity": "sha512-z2ca00AMwZ6PfVETQNvXRumZdRwGuQzApIH/hKNp2o6Qo8N8TW7Ug2V+aSH2w/eC1b/bOOMZIE57V3jYN+kB4A==", - "dependencies": { - "bip66": "1.1.5", - "bitcoin-ops": "1.4.1", - "bitcoinjs-lib": "6.0.1", - "bn.js": "5.2.1", - "pushdata-bitcoin": "1.0.1", - "varuint-bitcoin": "1.1.2" - }, - "engines": { - "node": ">=12.20" - } - }, "node_modules/pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", @@ -2626,14 +2283,6 @@ "node": ">=6" } }, - "node_modules/pushdata-bitcoin": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/pushdata-bitcoin/-/pushdata-bitcoin-1.0.1.tgz", - "integrity": "sha512-hw7rcYTJRAl4olM8Owe8x0fBuJJ+WGbMhQuLWOXEMN3PxPCKQHRkhfL+XG0+iXUmSHjkMmb3Ba55Mt21cZc9kQ==", - "dependencies": { - "bitcoin-ops": "^1.3.0" - } - }, "node_modules/qs": { "version": "6.10.3", "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", @@ -2668,14 +2317,6 @@ } ] }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -2723,14 +2364,6 @@ "url": "https://github.com/sponsors/mysticatea" } }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3008,23 +2641,11 @@ "safe-buffer": "~5.2.0" } }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "dependencies": { "ansi-regex": "^5.0.1" }, @@ -3058,17 +2679,6 @@ "node": ">=6" } }, - "node_modules/tiny-secp256k1": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tiny-secp256k1/-/tiny-secp256k1-2.2.1.tgz", - "integrity": "sha512-/U4xfVqnVxJXN4YVsru0E6t5wVncu2uunB8+RVR40fYUxkKYUPS10f+ePQZgFBoE/Jbf9H1NBveupF2VmB58Ng==", - "dependencies": { - "uint8array-tools": "0.0.7" - }, - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -3148,14 +2758,6 @@ "node": ">=4.2.0" } }, - "node_modules/uint8array-tools": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.7.tgz", - "integrity": "sha512-vrrNZJiusLWoFWBqz5Y5KMCgP9W9hnjZHzZiZRT8oNAkq3d5Z5Oe76jAvVVSRh4U8GGR90N2X1dWtrhvx6L8UQ==", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -3240,22 +2842,6 @@ "node": ">=0.10.0" } }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -3282,43 +2868,10 @@ } } }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "engines": { - "node": ">=10" - } - }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, - "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "engines": { - "node": ">=10" - } } }, "dependencies": { @@ -3371,27 +2924,6 @@ } } }, - "@grpc/grpc-js": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.6.7.tgz", - "integrity": "sha512-eBM03pu9hd3VqDQG+kHahiG1x80RGkkqqRb1Pchcwqej/KkAH95gAvKs6laqaHCycYaPK+TKuNQnOz9UXYA8qw==", - "requires": { - "@grpc/proto-loader": "^0.6.4", - "@types/node": ">=12.12.47" - } - }, - "@grpc/proto-loader": { - "version": "0.6.13", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.13.tgz", - "integrity": "sha512-FjxPYDRTn6Ec3V0arm1FtSpmP6V50wuph2yILpyvTKzjc76oDdoihXqM1DzOW5ubvCC8GivfCnNtfaRE8myJ7g==", - "requires": { - "@types/long": "^4.0.1", - "lodash.camelcase": "^4.3.0", - "long": "^4.0.0", - "protobufjs": "^6.11.3", - "yargs": "^16.2.0" - } - }, "@humanwhocodes/config-array": { "version": "0.9.5", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.9.5.tgz", @@ -3457,74 +2989,16 @@ "fastq": "^1.6.0" } }, - "@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" - }, - "@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" - }, - "@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" - }, - "@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" - }, - "@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", - "requires": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" - }, - "@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" - }, - "@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" - }, - "@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" - }, - "@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" - }, "@types/body-parser": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.0.tgz", "integrity": "sha512-W98JrE0j2K78swW4ukqMleo8R7h/pFETjM2DQ90MF6XK2i4LO4W3gQ71Lt4w3bfm2EvVSyWHplECvB5sK22yFQ==", + "dev": true, "requires": { "@types/connect": "*", "@types/node": "*" } }, - "@types/caseless": { - "version": "0.12.2", - "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", - "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==" - }, "@types/compression": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.7.2.tgz", @@ -3538,6 +3012,7 @@ "version": "3.4.34", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.34.tgz", "integrity": "sha512-ePPA/JuI+X0vb+gSWlPKOY0NdNAie/rPUqX2GUPpbZwiKTkSPhjXWuee47E4MtE54QVzGCQMQkAL6JhV2E1+cQ==", + "dev": true, "requires": { "@types/node": "*" } @@ -3552,6 +3027,7 @@ "version": "4.17.13", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", + "dev": true, "requires": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.18", @@ -3563,6 +3039,7 @@ "version": "4.17.28", "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.28.tgz", "integrity": "sha512-P1BJAEAW3E2DJUlkgq4tOL3RyMunoWXqbSCygWo5ZIWTjUgN1YnaXWW4VWl/oc8vs/XoYibEGBKP0uZyF4AHig==", + "dev": true, "requires": { "@types/node": "*", "@types/qs": "*", @@ -3575,15 +3052,11 @@ "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "dev": true }, - "@types/long": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", - "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" - }, "@types/mime": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.3.tgz", - "integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==" + "integrity": "sha512-Jus9s4CDbqwocc5pOAnh8ShfrnMcPHuJYzVcSUU7lrh8Ni5HuIqX3oilL86p3dlTrk0LzHRCgA/GQ7uNCw6l2Q==", + "dev": true }, "@types/node": { "version": "16.11.41", @@ -3593,54 +3066,30 @@ "@types/qs": { "version": "6.9.7", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", + "dev": true }, "@types/range-parser": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", - "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" - }, - "@types/request": { - "version": "2.48.8", - "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.8.tgz", - "integrity": "sha512-whjk1EDJPcAR2kYHRbFl/lKeeKYTi05A15K9bnLInCVroNDCtXce57xKdI0/rQaA3K+6q0eFyUBPmqfSndUZdQ==", - "requires": { - "@types/caseless": "*", - "@types/node": "*", - "@types/tough-cookie": "*", - "form-data": "^2.5.0" - }, - "dependencies": { - "form-data": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", - "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - } - } + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", + "dev": true }, "@types/serve-static": { "version": "1.13.8", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.8.tgz", "integrity": "sha512-MoJhSQreaVoL+/hurAZzIm8wafFR6ajiTM1m4A0kv6AGeVBl4r4pOV8bGFrjjq1sGxDTnCoF8i22o0/aE5XCyA==", + "dev": true, "requires": { "@types/mime": "*", "@types/node": "*" } }, - "@types/tough-cookie": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz", - "integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==" - }, "@types/ws": { "version": "8.5.3", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==", + "dev": true, "requires": { "@types/node": "*" } @@ -3913,12 +3362,14 @@ "ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "requires": { "color-convert": "^2.0.1" } @@ -3934,26 +3385,6 @@ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true }, - "async": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", - "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" - }, - "asyncjs-util": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/asyncjs-util/-/asyncjs-util-1.2.9.tgz", - "integrity": "sha512-U9imS8ehJA6DPNdBdvoLcIRDFh7yzI9J93CC8/2obk8gUSIy8KKhmCqYe+3NlISJhxLLi8aWmVL1Gkb3dz1xhg==", - "requires": { - "async": "3.2.3" - }, - "dependencies": { - "async": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.3.tgz", - "integrity": "sha512-spZRyzKL5l5BZQrr/6m/SqFdBN0q3OCI0f9rjfBzCMBIP4p75P620rR3gTmaksNOhmzgdxcaxdNfMy6anrbM0g==" - } - } - }, "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -3992,19 +3423,6 @@ "resolved": "https://registry.npmjs.org/bip174/-/bip174-2.0.1.tgz", "integrity": "sha512-i3X26uKJOkDTAalYAp0Er+qGMDhrbbh2o93/xiPyAN2s25KrClSpe3VXo/7mNJoqA5qfko8rLS2l3RWZgYmjKQ==" }, - "bip66": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/bip66/-/bip66-1.1.5.tgz", - "integrity": "sha512-nemMHz95EmS38a26XbbdxIYj5csHd3RMP3H5bwQknX0WYHF01qhpufP42mLOwVICuH2JmhIhXiWs89MfUGL7Xw==", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "bitcoin-ops": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/bitcoin-ops/-/bitcoin-ops-1.4.1.tgz", - "integrity": "sha512-pef6gxZFztEhaE9RY9HmWVmiIHqCb2OyS4HPKkpc6CIiiOa3Qmuoylxc5P2EkU3w+5eTSifI9SEZC88idAIGow==" - }, "bitcoinjs-lib": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/bitcoinjs-lib/-/bitcoinjs-lib-6.0.1.tgz", @@ -4019,11 +3437,6 @@ "wif": "^2.0.1" } }, - "bn.js": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", - "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" - }, "body-parser": { "version": "1.20.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", @@ -4043,19 +3456,6 @@ "unpipe": "1.0.0" } }, - "bolt07": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/bolt07/-/bolt07-1.8.2.tgz", - "integrity": "sha512-jq1b/ZdMambhh+yi+pm+1PJBAnlYvQYljaBgSajvVAINHrHg32ovCBra8d0ADE3BAoj6G/tK7OSV4t/yT9A+/g==", - "requires": { - "bn.js": "5.2.1" - } - }, - "bolt09": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/bolt09/-/bolt09-0.2.3.tgz", - "integrity": "sha512-xEt5GE6pXB8wMIWHAoyF28k0Yt2rFqIou1LCyIeNadAOQhu/F7GTjZwreFwLl07YYkhOH23avewRt5PD8JnKKg==" - }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -4113,14 +3513,6 @@ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true }, - "cbor": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/cbor/-/cbor-8.1.0.tgz", - "integrity": "sha512-DwGjNW9omn6EwP70aXsn7FQJx5kO12tX0bZkaTjzdVFM6/7nhA4t0EENocKGx6D2Bch9PE2KzCUf5SceBdeijg==", - "requires": { - "nofilter": "^3.1.0" - } - }, "cipher-base": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", @@ -4130,20 +3522,11 @@ "safe-buffer": "^5.0.1" } }, - "cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "requires": { "color-name": "~1.1.4" } @@ -4151,7 +3534,8 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "combined-stream": { "version": "1.0.8", @@ -4270,36 +3654,16 @@ "esutils": "^2.0.2" } }, - "ecpair": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ecpair/-/ecpair-2.0.1.tgz", - "integrity": "sha512-iT3wztQMeE/nDTlfnAg8dAFUfBS7Tq2BXzq3ae6L+pWgFU0fQ3l0woTzdTBrJV3OxBjxbzjq8EQhAbEmJNWFSw==", - "requires": { - "randombytes": "^2.1.0", - "typeforce": "^1.18.0", - "wif": "^2.0.6" - } - }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" }, - "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" - }, "escape-html": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", @@ -4716,11 +4080,6 @@ "is-property": "^1.0.2" } }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" - }, "get-intrinsic": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", @@ -4857,19 +4216,6 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, - "invoices": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/invoices/-/invoices-2.0.7.tgz", - "integrity": "sha512-2kpjok/83zOTnb4tbV+RbJz7LuGVzj/GZ+jwsC7FxMqwLAf4Sf6OESNM3uuamX9oeFRo44Vip3wn1aX+9D2m8w==", - "requires": { - "bech32": "2.0.0", - "bitcoinjs-lib": "6.0.1", - "bn.js": "5.2.1", - "bolt07": "1.8.2", - "bolt09": "0.2.3", - "tiny-secp256k1": "2.2.1" - } - }, "ip": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", @@ -4886,11 +4232,6 @@ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" - }, "is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -4939,50 +4280,6 @@ "type-check": "~0.4.0" } }, - "lightning": { - "version": "5.16.3", - "resolved": "https://registry.npmjs.org/lightning/-/lightning-5.16.3.tgz", - "integrity": "sha512-ghban3KbqkbzahwIp4NAtuhc8xIurVcCXAd7tV6qGkFYKZAy9loIvFrhZqoWF4A4jnaKbRnJPCaxzJ8JwPl3EA==", - "requires": { - "@grpc/grpc-js": "1.6.7", - "@grpc/proto-loader": "0.6.13", - "@types/express": "4.17.13", - "@types/node": "17.0.41", - "@types/request": "2.48.8", - "@types/ws": "8.5.3", - "async": "3.2.4", - "asyncjs-util": "1.2.9", - "bitcoinjs-lib": "6.0.1", - "bn.js": "5.2.1", - "body-parser": "1.20.0", - "bolt07": "1.8.2", - "bolt09": "0.2.3", - "cbor": "8.1.0", - "ecpair": "2.0.1", - "express": "4.18.1", - "invoices": "2.0.7", - "psbt": "2.6.0", - "tiny-secp256k1": "2.2.1", - "type-fest": "2.13.0" - }, - "dependencies": { - "@types/node": { - "version": "17.0.41", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.41.tgz", - "integrity": "sha512-xA6drNNeqb5YyV5fO3OAEsnXLfO7uF0whiOfPTz5AeDo8KeZFmODKnvwPymMNO8qE/an8pVY/O50tig2SQCrGw==" - }, - "type-fest": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.13.0.tgz", - "integrity": "sha512-lPfAm42MxE4/456+QyIaaVBAwgpJb6xZ8PRu09utnhPdWwcyj9vgy6Sq0Z5yNbJ21EdxB5dRU/Qg8bsyAMtlcw==" - } - } - }, - "lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" - }, "lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -5154,11 +4451,6 @@ "resolved": "https://registry.npmjs.org/node-worker-threads-pool/-/node-worker-threads-pool-1.5.1.tgz", "integrity": "sha512-7TXAhpMm+jO4MfESxYLtMGSnJWv+itdNHMdaFmeZuPXxwFGU90mtEB42BciUULXOUAxYBfXILAuvrSG3rQZ7mw==" }, - "nofilter": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/nofilter/-/nofilter-3.1.0.tgz", - "integrity": "sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==" - }, "object-inspect": { "version": "1.12.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz", @@ -5250,26 +4542,6 @@ "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", "dev": true }, - "protobufjs": { - "version": "6.11.3", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.3.tgz", - "integrity": "sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg==", - "requires": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/long": "^4.0.1", - "@types/node": ">=13.7.0", - "long": "^4.0.0" - } - }, "proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -5279,19 +4551,6 @@ "ipaddr.js": "1.9.1" } }, - "psbt": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/psbt/-/psbt-2.6.0.tgz", - "integrity": "sha512-z2ca00AMwZ6PfVETQNvXRumZdRwGuQzApIH/hKNp2o6Qo8N8TW7Ug2V+aSH2w/eC1b/bOOMZIE57V3jYN+kB4A==", - "requires": { - "bip66": "1.1.5", - "bitcoin-ops": "1.4.1", - "bitcoinjs-lib": "6.0.1", - "bn.js": "5.2.1", - "pushdata-bitcoin": "1.0.1", - "varuint-bitcoin": "1.1.2" - } - }, "pseudomap": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", @@ -5303,14 +4562,6 @@ "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", "dev": true }, - "pushdata-bitcoin": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/pushdata-bitcoin/-/pushdata-bitcoin-1.0.1.tgz", - "integrity": "sha512-hw7rcYTJRAl4olM8Owe8x0fBuJJ+WGbMhQuLWOXEMN3PxPCKQHRkhfL+XG0+iXUmSHjkMmb3Ba55Mt21cZc9kQ==", - "requires": { - "bitcoin-ops": "^1.3.0" - } - }, "qs": { "version": "6.10.3", "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", @@ -5325,14 +4576,6 @@ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true }, - "randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "requires": { - "safe-buffer": "^5.1.0" - } - }, "range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -5365,11 +4608,6 @@ "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", "dev": true }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" - }, "resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -5564,20 +4802,11 @@ "safe-buffer": "~5.2.0" } }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, "strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "requires": { "ansi-regex": "^5.0.1" } @@ -5599,14 +4828,6 @@ "resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-8.0.2.tgz", "integrity": "sha512-ApGvZ6vVvTNdsmt676grvCkUCGwzG9IqXma5Z07xJgiC5L7akUMof5U8G2JTI9Rz/ovtVhJBlY6mNhEvtjzOIg==" }, - "tiny-secp256k1": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tiny-secp256k1/-/tiny-secp256k1-2.2.1.tgz", - "integrity": "sha512-/U4xfVqnVxJXN4YVsru0E6t5wVncu2uunB8+RVR40fYUxkKYUPS10f+ePQZgFBoE/Jbf9H1NBveupF2VmB58Ng==", - "requires": { - "uint8array-tools": "0.0.7" - } - }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -5661,11 +4882,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==" }, - "uint8array-tools": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/uint8array-tools/-/uint8array-tools-0.0.7.tgz", - "integrity": "sha512-vrrNZJiusLWoFWBqz5Y5KMCgP9W9hnjZHzZiZRT8oNAkq3d5Z5Oe76jAvVVSRh4U8GGR90N2X1dWtrhvx6L8UQ==" - }, "unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -5732,16 +4948,6 @@ "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", "dev": true }, - "wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -5754,34 +4960,10 @@ "integrity": "sha512-JDAgSYQ1ksuwqfChJusw1LSJ8BizJ2e/vVu5Lxjq3YvNJNlROv1ui4i+c/kUUrPheBvQl4c5UbERhTwKa6QBJQ==", "requires": {} }, - "y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" - }, "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - }, - "yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "requires": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - } - }, - "yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==" } } } diff --git a/backend/package.json b/backend/package.json index b8930d6e5..6345e89da 100644 --- a/backend/package.json +++ b/backend/package.json @@ -16,7 +16,8 @@ "mempool", "blockchain", "explorer", - "liquid" + "liquid", + "lightning" ], "main": "index.ts", "scripts": { @@ -34,10 +35,8 @@ "@types/node": "^16.11.41", "axios": "~0.27.2", "bitcoinjs-lib": "6.0.1", - "bolt07": "^1.8.1", "crypto-js": "^4.0.0", "express": "^4.18.0", - "lightning": "^5.16.3", "maxmind": "^4.3.6", "mysql2": "2.3.3", "node-worker-threads-pool": "^1.5.1", diff --git a/backend/src/api/lightning/lightning-api.interface.ts b/backend/src/api/lightning/lightning-api.interface.ts index 9b83b5473..283f34a5a 100644 --- a/backend/src/api/lightning/lightning-api.interface.ts +++ b/backend/src/api/lightning/lightning-api.interface.ts @@ -1,71 +1,85 @@ export namespace ILightningApi { export interface NetworkInfo { - average_channel_size: number; - channel_count: number; - max_channel_size: number; - median_channel_size: number; - min_channel_size: number; - node_count: number; - not_recently_updated_policy_count: number; - total_capacity: number; + graph_diameter: number; + avg_out_degree: number; + max_out_degree: number; + num_nodes: number; + num_channels: number; + total_network_capacity: string; + avg_channel_size: number; + min_channel_size: string; + max_channel_size: string; + median_channel_size_sat: string; + num_zombie_chans: string; } export interface NetworkGraph { - channels: Channel[]; nodes: Node[]; + edges: Channel[]; } export interface Channel { - id: string; - capacity: number; - policies: Policy[]; - transaction_id: string; - transaction_vout: number; - updated_at?: string; + channel_id: string; + chan_point: string; + last_update: number; + node1_pub: string; + node2_pub: string; + capacity: string; + node1_policy: RoutingPolicy | null; + node2_policy: RoutingPolicy | null; } - interface Policy { - public_key: string; - base_fee_mtokens?: string; - cltv_delta?: number; - fee_rate?: number; - is_disabled?: boolean; - max_htlc_mtokens?: string; - min_htlc_mtokens?: string; - updated_at?: string; + export interface RoutingPolicy { + time_lock_delta: number; + min_htlc: string; + fee_base_msat: string; + fee_rate_milli_msat: string; + disabled: boolean; + max_htlc_msat: string; + last_update: number; } export interface Node { + last_update: number; + pub_key: string; alias: string; + addresses: { + network: string; + addr: string; + }[]; color: string; - features: Feature[]; - public_key: string; - sockets: string[]; - updated_at?: string; + features: { [key: number]: Feature }; } export interface Info { - chains: string[]; - color: string; - active_channels_count: number; + identity_pubkey: string; alias: string; - current_block_hash: string; - current_block_height: number; - features: Feature[]; - is_synced_to_chain: boolean; - is_synced_to_graph: boolean; - latest_block_at: string; - peers_count: number; - pending_channels_count: number; - public_key: string; - uris: any[]; + num_pending_channels: number; + num_active_channels: number; + num_peers: number; + block_height: number; + block_hash: string; + synced_to_chain: boolean; + testnet: boolean; + uris: string[]; + best_header_timestamp: string; version: string; + num_inactive_channels: number; + chains: { + chain: string; + network: string; + }[]; + color: string; + synced_to_graph: boolean; + features: { [key: number]: Feature }; + commit_hash: string; + /** Available on LND since v0.15.0-beta */ + require_htlc_interceptor?: boolean; } - + export interface Feature { - bit: number; - is_known: boolean; + name: string; is_required: boolean; - type?: string; + is_known: boolean; } } diff --git a/backend/src/api/lightning/lnd/lnd-api.ts b/backend/src/api/lightning/lnd/lnd-api.ts index 19d98744d..1480f9b8f 100644 --- a/backend/src/api/lightning/lnd/lnd-api.ts +++ b/backend/src/api/lightning/lnd/lnd-api.ts @@ -1,44 +1,40 @@ +import axios, { AxiosRequestConfig } from 'axios'; +import { Agent } from 'https'; +import * as fs from 'fs'; import { AbstractLightningApi } from '../lightning-api-abstract-factory'; import { ILightningApi } from '../lightning-api.interface'; -import * as fs from 'fs'; -import { authenticatedLndGrpc, getWalletInfo, getNetworkGraph, getNetworkInfo } from 'lightning'; import config from '../../../config'; -import logger from '../../../logger'; class LndApi implements AbstractLightningApi { - private lnd: any; + axiosConfig: AxiosRequestConfig = {}; + constructor() { - if (!config.LIGHTNING.ENABLED) { - return; - } - try { - const tls = fs.readFileSync(config.LND.TLS_CERT_PATH).toString('base64'); - const macaroon = fs.readFileSync(config.LND.MACAROON_PATH).toString('base64'); - - const { lnd } = authenticatedLndGrpc({ - cert: tls, - macaroon: macaroon, - socket: config.LND.SOCKET, - }); - - this.lnd = lnd; - } catch (e) { - logger.err('Could not initiate the LND service handler: ' + (e instanceof Error ? e.message : e)); - process.exit(1); + if (config.LIGHTNING.ENABLED) { + this.axiosConfig = { + headers: { + 'Grpc-Metadata-macaroon': fs.readFileSync(config.LND.MACAROON_PATH).toString('hex') + }, + httpsAgent: new Agent({ + ca: fs.readFileSync(config.LND.TLS_CERT_PATH) + }), + timeout: 10000 + }; } } async $getNetworkInfo(): Promise { - return await getNetworkInfo({ lnd: this.lnd }); + return axios.get(config.LND.REST_API_URL + '/v1/graph/info', this.axiosConfig) + .then((response) => response.data); } async $getInfo(): Promise { - // @ts-ignore - return await getWalletInfo({ lnd: this.lnd }); + return axios.get(config.LND.REST_API_URL + '/v1/getinfo', this.axiosConfig) + .then((response) => response.data); } async $getNetworkGraph(): Promise { - return await getNetworkGraph({ lnd: this.lnd }); + return axios.get(config.LND.REST_API_URL + '/v1/graph', this.axiosConfig) + .then((response) => response.data); } } diff --git a/backend/src/config.ts b/backend/src/config.ts index 0e3382517..5560a25a7 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -35,7 +35,7 @@ interface IConfig { LND: { TLS_CERT_PATH: string; MACAROON_PATH: string; - SOCKET: string; + REST_API_URL: string; }; ELECTRUM: { HOST: string; @@ -182,7 +182,7 @@ const defaults: IConfig = { 'LND': { 'TLS_CERT_PATH': '', 'MACAROON_PATH': '', - 'SOCKET': 'localhost:10009', + 'REST_API_URL': 'https://localhost:8080', }, 'SOCKS5PROXY': { 'ENABLED': false, diff --git a/backend/src/tasks/lightning/node-sync.service.ts b/backend/src/tasks/lightning/node-sync.service.ts index f45473aba..10cd2d744 100644 --- a/backend/src/tasks/lightning/node-sync.service.ts +++ b/backend/src/tasks/lightning/node-sync.service.ts @@ -1,4 +1,3 @@ -import { chanNumber } from 'bolt07'; import DB from '../../database'; import logger from '../../logger'; import channelsApi from '../../api/explorer/channels.api'; @@ -39,9 +38,9 @@ class NodeSyncService { } const graphChannelsIds: string[] = []; - for (const channel of networkGraph.channels) { + for (const channel of networkGraph.edges) { await this.$saveChannel(channel); - graphChannelsIds.push(channel.id); + graphChannelsIds.push(channel.channel_id); } await this.$setChannelsInactive(graphChannelsIds); @@ -56,7 +55,7 @@ class NodeSyncService { } } catch (e) { - logger.err('$updateNodes() error: ' + (e instanceof Error ? e.message : e)); + logger.err('$runUpdater() error: ' + (e instanceof Error ? e.message : e)); } } @@ -107,8 +106,7 @@ class NodeSyncService { logger.info(`Running inactive channels scan...`); try { - // @ts-ignore - const [channels]: [ILightningApi.Channel[]] = await DB.query(` + const [channels]: [{ id: string }[]] = await DB.query(` SELECT channels.id FROM channels WHERE channels.status = 1 @@ -266,7 +264,10 @@ class NodeSyncService { } private async $saveChannel(channel: ILightningApi.Channel): Promise { - const fromChannel = chanNumber({ channel: channel.id }).number; + const [ txid, vout ] = channel.chan_point.split(':'); + + const policy1: Partial = channel.node1_policy || {}; + const policy2: Partial = channel.node2_policy || {}; try { const query = `INSERT INTO channels @@ -319,55 +320,55 @@ class NodeSyncService { ;`; await DB.query(query, [ - fromChannel, - channel.id, + channel.channel_id, + this.toShortId(channel.channel_id), channel.capacity, - channel.transaction_id, - channel.transaction_vout, - channel.updated_at ? this.utcDateToMysql(channel.updated_at) : 0, - channel.policies[0].public_key, - channel.policies[0].base_fee_mtokens, - channel.policies[0].cltv_delta, - channel.policies[0].fee_rate, - channel.policies[0].is_disabled, - channel.policies[0].max_htlc_mtokens, - channel.policies[0].min_htlc_mtokens, - channel.policies[0].updated_at ? this.utcDateToMysql(channel.policies[0].updated_at) : 0, - channel.policies[1].public_key, - channel.policies[1].base_fee_mtokens, - channel.policies[1].cltv_delta, - channel.policies[1].fee_rate, - channel.policies[1].is_disabled, - channel.policies[1].max_htlc_mtokens, - channel.policies[1].min_htlc_mtokens, - channel.policies[1].updated_at ? this.utcDateToMysql(channel.policies[1].updated_at) : 0, + txid, + vout, + this.utcDateToMysql(channel.last_update), + channel.node1_pub, + policy1.fee_base_msat, + policy1.time_lock_delta, + policy1.fee_rate_milli_msat, + policy1.disabled, + policy1.max_htlc_msat, + policy1.min_htlc, + this.utcDateToMysql(policy1.last_update), + channel.node2_pub, + policy2.fee_base_msat, + policy2.time_lock_delta, + policy2.fee_rate_milli_msat, + policy2.disabled, + policy2.max_htlc_msat, + policy2.min_htlc, + this.utcDateToMysql(policy2.last_update), channel.capacity, - channel.updated_at ? this.utcDateToMysql(channel.updated_at) : 0, - channel.policies[0].public_key, - channel.policies[0].base_fee_mtokens, - channel.policies[0].cltv_delta, - channel.policies[0].fee_rate, - channel.policies[0].is_disabled, - channel.policies[0].max_htlc_mtokens, - channel.policies[0].min_htlc_mtokens, - channel.policies[0].updated_at ? this.utcDateToMysql(channel.policies[0].updated_at) : 0, - channel.policies[1].public_key, - channel.policies[1].base_fee_mtokens, - channel.policies[1].cltv_delta, - channel.policies[1].fee_rate, - channel.policies[1].is_disabled, - channel.policies[1].max_htlc_mtokens, - channel.policies[1].min_htlc_mtokens, - channel.policies[1].updated_at ? this.utcDateToMysql(channel.policies[1].updated_at) : 0, + this.utcDateToMysql(channel.last_update), + channel.node1_pub, + policy1.fee_base_msat, + policy1.time_lock_delta, + policy1.fee_rate_milli_msat, + policy1.disabled, + policy1.max_htlc_msat, + policy1.min_htlc, + this.utcDateToMysql(policy1.last_update), + channel.node2_pub, + policy2.fee_base_msat, + policy2.time_lock_delta, + policy2.fee_rate_milli_msat, + policy2.disabled, + policy2.max_htlc_msat, + policy2.min_htlc, + this.utcDateToMysql(policy2.last_update) ]); } catch (e) { logger.err('$saveChannel() error: ' + (e instanceof Error ? e.message : e)); } } - private async $updateChannelStatus(channelShortId: string, status: number): Promise { + private async $updateChannelStatus(channelId: string, status: number): Promise { try { - await DB.query(`UPDATE channels SET status = ? WHERE id = ?`, [status, channelShortId]); + await DB.query(`UPDATE channels SET status = ? WHERE id = ?`, [status, channelId]); } catch (e) { logger.err('$updateChannelStatus() error: ' + (e instanceof Error ? e.message : e)); } @@ -390,8 +391,8 @@ class NodeSyncService { private async $saveNode(node: ILightningApi.Node): Promise { try { - const updatedAt = node.updated_at ? this.utcDateToMysql(node.updated_at) : '0000-00-00 00:00:00'; - const sockets = node.sockets.join(','); + const updatedAt = this.utcDateToMysql(node.last_update); + const sockets = node.addresses.map(a => a.addr).join(','); const query = `INSERT INTO nodes( public_key, first_seen, @@ -403,7 +404,7 @@ class NodeSyncService { VALUES (?, NOW(), ?, ?, ?, ?) ON DUPLICATE KEY UPDATE updated_at = ?, alias = ?, color = ?, sockets = ?;`; await DB.query(query, [ - node.public_key, + node.pub_key, updatedAt, node.alias, node.color, @@ -418,8 +419,18 @@ class NodeSyncService { } } - private utcDateToMysql(dateString: string): string { - const d = new Date(Date.parse(dateString)); + /** Decodes a channel id returned by lnd as uint64 to a short channel id */ + private toShortId(id: string): string { + const n = BigInt(id); + return [ + n >> 40n, // nth block + (n >> 16n) & 0xffffffn, // nth tx of the block + n & 0xffffn // nth output of the tx + ].join('x'); + } + + private utcDateToMysql(date?: number): string { + const d = new Date((date || 0) * 1000); return d.toISOString().split('T')[0] + ' ' + d.toTimeString().split(' ')[0]; } } diff --git a/backend/src/tasks/lightning/stats-updater.service.ts b/backend/src/tasks/lightning/stats-updater.service.ts index f30da9e96..0a3ade614 100644 --- a/backend/src/tasks/lightning/stats-updater.service.ts +++ b/backend/src/tasks/lightning/stats-updater.service.ts @@ -52,7 +52,7 @@ class LightningStatsUpdater { private async $lightningIsSynced(): Promise { const nodeInfo = await lightningApi.$getInfo(); - return nodeInfo.is_synced_to_chain && nodeInfo.is_synced_to_graph; + return nodeInfo.synced_to_chain && nodeInfo.synced_to_graph; } private async $runTasks(): Promise { @@ -66,13 +66,13 @@ class LightningStatsUpdater { private async $logLightningStatsDaily() { try { - logger.info(`Running lightning daily stats log...`); + logger.info(`Running lightning daily stats log...`); const networkGraph = await lightningApi.$getNetworkGraph(); let total_capacity = 0; - for (const channel of networkGraph.channels) { + for (const channel of networkGraph.edges) { if (channel.capacity) { - total_capacity += channel.capacity; + total_capacity += parseInt(channel.capacity); } } @@ -80,20 +80,17 @@ class LightningStatsUpdater { let torNodes = 0; let unannouncedNodes = 0; for (const node of networkGraph.nodes) { - let isUnnanounced = true; - for (const socket of node.sockets) { - const hasOnion = socket.indexOf('.onion') !== -1; + for (const socket of node.addresses) { + const hasOnion = socket.addr.indexOf('.onion') !== -1; if (hasOnion) { torNodes++; - isUnnanounced = false; } - const hasClearnet = [4, 6].includes(net.isIP(socket.split(':')[0])); + const hasClearnet = [4, 6].includes(net.isIP(socket.addr.split(':')[0])); if (hasClearnet) { clearnetNodes++; - isUnnanounced = false; } } - if (isUnnanounced) { + if (node.addresses.length === 0) { unannouncedNodes++; } } @@ -118,7 +115,7 @@ class LightningStatsUpdater { VALUES (NOW() - INTERVAL 1 DAY, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`; await DB.query(query, [ - networkGraph.channels.length, + networkGraph.edges.length, networkGraph.nodes.length, total_capacity, torNodes, @@ -292,7 +289,7 @@ class LightningStatsUpdater { for (const node of nodes) { const [channels]: any = await DB.query(`SELECT capacity, created, closing_date FROM channels WHERE node1_public_key = ? OR node2_public_key = ? ORDER BY created ASC`, [node.public_key, node.public_key]); - + const date: Date = new Date(this.hardCodedStartTime); const currentDate = new Date(); this.setDateMidnight(currentDate); @@ -322,7 +319,7 @@ class LightningStatsUpdater { lastTotalCapacity = totalCapacity; lastChannelsCount = channelsCount; - + const query = `INSERT INTO node_stats( public_key, added, From 778d27ccb15ebf8e5536bb3bf2f07a51b1328cb9 Mon Sep 17 00:00:00 2001 From: softsimon Date: Sun, 31 Jul 2022 23:25:28 +0200 Subject: [PATCH 060/105] Redirect with path --- frontend/src/app/services/enterprise.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/services/enterprise.service.ts b/frontend/src/app/services/enterprise.service.ts index be34576f9..b125739d6 100644 --- a/frontend/src/app/services/enterprise.service.ts +++ b/frontend/src/app/services/enterprise.service.ts @@ -49,7 +49,7 @@ export class EnterpriseService { }, (error) => { if (error.status === 404) { - window.location.href = 'https://mempool.space'; + window.location.href = 'https://mempool.space' + window.location.pathname; } }); } From ee68c2e4c43a101ebbff07b26693062d13561ab7 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Mon, 1 Aug 2022 09:59:20 +0200 Subject: [PATCH 061/105] Fix missing pub key, capacity and channel count for node lists --- backend/src/api/explorer/nodes.api.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/src/api/explorer/nodes.api.ts b/backend/src/api/explorer/nodes.api.ts index 55b0ba5cb..f05c4cf0a 100644 --- a/backend/src/api/explorer/nodes.api.ts +++ b/backend/src/api/explorer/nodes.api.ts @@ -163,8 +163,8 @@ class NodesApi { public async $getNodesPerCountry(countryId: string) { try { const query = ` - SELECT node_stats.public_key, node_stats.capacity, node_stats.channels, nodes.alias, - UNIX_TIMESTAMP(nodes.first_seen) as first_seen, UNIX_TIMESTAMP(nodes.updated_at) as updated_at, + SELECT nodes.public_key, CAST(COALESCE(node_stats.capacity, 0) as INT) as capacity, CAST(COALESCE(node_stats.channels, 0) as INT) as channels, + nodes.alias, UNIX_TIMESTAMP(nodes.first_seen) as first_seen, UNIX_TIMESTAMP(nodes.updated_at) as updated_at, geo_names_city.names as city FROM node_stats JOIN ( @@ -172,7 +172,7 @@ class NodesApi { FROM node_stats GROUP BY public_key ) as b ON b.public_key = node_stats.public_key AND b.last_added = node_stats.added - JOIN nodes ON nodes.public_key = node_stats.public_key + RIGHT JOIN nodes ON nodes.public_key = node_stats.public_key JOIN geo_names geo_names_country ON geo_names_country.id = nodes.country_id AND geo_names_country.type = 'country' LEFT JOIN geo_names geo_names_city ON geo_names_city.id = nodes.city_id AND geo_names_city.type = 'city' WHERE geo_names_country.id = ? @@ -193,8 +193,8 @@ class NodesApi { public async $getNodesPerISP(ISPId: string) { try { const query = ` - SELECT node_stats.public_key, node_stats.capacity, node_stats.channels, nodes.alias, - UNIX_TIMESTAMP(nodes.first_seen) as first_seen, UNIX_TIMESTAMP(nodes.updated_at) as updated_at, + SELECT nodes.public_key, CAST(COALESCE(node_stats.capacity, 0) as INT) as capacity, CAST(COALESCE(node_stats.channels, 0) as INT) as channels, + nodes.alias, UNIX_TIMESTAMP(nodes.first_seen) as first_seen, UNIX_TIMESTAMP(nodes.updated_at) as updated_at, geo_names_city.names as city, geo_names_country.names as country FROM node_stats JOIN ( From e72dfd713b475134cb0c474b7108338cf13278b5 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Thu, 28 Jul 2022 15:07:50 +0200 Subject: [PATCH 062/105] Re-enabled channels world map click event --- .../nodes-channels-map/nodes-channels-map.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts b/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts index 16accda94..61f7afe85 100644 --- a/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts +++ b/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts @@ -98,7 +98,7 @@ export class NodesChannelsMap implements OnInit, OnDestroy { } this.chartOptions = { - silent: true, + silent: this.style === 'widget' ? true : false, title: title ?? undefined, geo3D: { map: 'world', From 5603fea5901ae444d131d3387df9a1398d326e6c Mon Sep 17 00:00:00 2001 From: nymkappa Date: Mon, 1 Aug 2022 18:41:31 +0200 Subject: [PATCH 063/105] Set default values when pubkey, capacity and channels are missing from top nodes --- backend/src/api/explorer/nodes.api.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/backend/src/api/explorer/nodes.api.ts b/backend/src/api/explorer/nodes.api.ts index f05c4cf0a..4c7028136 100644 --- a/backend/src/api/explorer/nodes.api.ts +++ b/backend/src/api/explorer/nodes.api.ts @@ -66,7 +66,15 @@ class NodesApi { public async $getTopCapacityNodes(): Promise { try { - const query = `SELECT nodes.*, node_stats.capacity, node_stats.channels FROM nodes LEFT JOIN node_stats ON node_stats.public_key = nodes.public_key ORDER BY node_stats.added DESC, node_stats.capacity DESC LIMIT 10`; + const query = ` + SELECT IF(nodes.alias = '', SUBSTRING(nodes.public_key, 1, 20), alias) as alias, nodes.public_key, + CAST(COALESCE(node_stats.capacity, 0) as INT) as capacity, + CAST(COALESCE(node_stats.channels, 0) as INT) as channels + FROM nodes + LEFT JOIN node_stats ON node_stats.public_key = nodes.public_key + ORDER BY node_stats.added DESC, node_stats.capacity DESC + LIMIT 10 + `; const [rows]: any = await DB.query(query); return rows; } catch (e) { @@ -77,7 +85,15 @@ class NodesApi { public async $getTopChannelsNodes(): Promise { try { - const query = `SELECT nodes.*, node_stats.capacity, node_stats.channels FROM nodes LEFT JOIN node_stats ON node_stats.public_key = nodes.public_key ORDER BY node_stats.added DESC, node_stats.channels DESC LIMIT 10`; + const query = ` + SELECT IF(nodes.alias = '', SUBSTRING(nodes.public_key, 1, 20), alias) as alias, nodes.public_key, + CAST(COALESCE(node_stats.capacity, 0) as INT) as capacity, + CAST(COALESCE(node_stats.channels, 0) as INT) as channels + FROM nodes + LEFT JOIN node_stats + ON node_stats.public_key = nodes.public_key + ORDER BY node_stats.added DESC, node_stats.channels DESC + LIMIT 10`; const [rows]: any = await DB.query(query); return rows; } catch (e) { From edba9692060ba55d913fe9df736399dd065a1c18 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Mon, 1 Aug 2022 18:55:35 +0200 Subject: [PATCH 064/105] Fix UX interaction with channels map --- .../nodes-channels-map/nodes-channels-map.component.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts b/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts index 61f7afe85..c71ff88ad 100644 --- a/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts +++ b/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts @@ -112,13 +112,15 @@ export class NodesChannelsMap implements OnInit, OnDestroy { }, viewControl: { center: this.style === 'widget' ? [0, 0, -10] : undefined, - minDistance: this.style === 'widget' ? 22 : 0.1, - maxDistance: this.style === 'widget' ? 22 : 60, + minDistance: 1, + maxDistance: 60, distance: this.style === 'widget' ? 22 : 60, alpha: 90, - panMouseButton: 'left', + rotateSensitivity: 0, + panSensitivity: this.style === 'widget' ? 0 : 1, + zoomSensitivity: this.style === 'widget' ? 0 : 0.5, + panMouseButton: this.style === 'widget' ? null : 'left', rotateMouseButton: undefined, - zoomSensivity: 0.5, }, itemStyle: { color: 'white', From 6236a12f1e29bb04cab7c65ca632c401f7dea55e Mon Sep 17 00:00:00 2001 From: softsimon Date: Mon, 1 Aug 2022 20:08:53 +0200 Subject: [PATCH 065/105] Limit matomo to mempool.space --- frontend/src/app/services/enterprise.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/services/enterprise.service.ts b/frontend/src/app/services/enterprise.service.ts index b125739d6..41a6194a1 100644 --- a/frontend/src/app/services/enterprise.service.ts +++ b/frontend/src/app/services/enterprise.service.ts @@ -24,7 +24,7 @@ export class EnterpriseService { this.subdomain = subdomain; this.fetchSubdomainInfo(); this.disableSubnetworks(); - } else { + } else if (document.location.hostname === 'mempool.space') { this.insertMatomo(); } } From 0f2608aef4a9fba3af4da66c97c32d5dead96a8b Mon Sep 17 00:00:00 2001 From: wiz Date: Tue, 2 Aug 2022 00:07:35 +0200 Subject: [PATCH 066/105] [ops] Fix syslog permissions for /var/log/mempool --- production/newsyslog-mempool-backend.conf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/production/newsyslog-mempool-backend.conf b/production/newsyslog-mempool-backend.conf index 5c96da47a..df0ae9c47 100644 --- a/production/newsyslog-mempool-backend.conf +++ b/production/newsyslog-mempool-backend.conf @@ -1,2 +1,2 @@ -/var/log/mempool 640 10 * @T00 C -/var/log/mempool.debug 640 10 * @T00 C +/var/log/mempool 644 10 * @T00 C +/var/log/mempool.debug 644 10 * @T00 C From 5a2c29fc0e38d2e75db412f9d991a060b3d12624 Mon Sep 17 00:00:00 2001 From: wiz Date: Tue, 2 Aug 2022 01:00:06 +0200 Subject: [PATCH 067/105] [ops] Fix cron jobs to update liquid assets hourly --- production/elements.crontab | 8 ++++++-- production/mempool.crontab | 11 ++++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/production/elements.crontab b/production/elements.crontab index 4459a8c5b..5ba8151a3 100644 --- a/production/elements.crontab +++ b/production/elements.crontab @@ -1,6 +1,10 @@ +# start elements on reboot @reboot sleep 60 ; /usr/local/bin/elementsd -chain=liquidv1 >/dev/null 2>&1 @reboot sleep 60 ; /usr/local/bin/elementsd -chain=liquidtestnet >/dev/null 2>&1 + +# start electrs on reboot @reboot sleep 90 ; screen -dmS liquidv1 /elements/electrs/electrs-start-liquid @reboot sleep 90 ; screen -dmS liquidtestnet /elements/electrs/electrs-start-liquidtestnet -6 * * * * cd $HOME/asset_registry_db && git pull origin master >/dev/null 2>&1 -6 * * * * cd $HOME/asset_registry_testnet_db && git pull origin master >/dev/null 2>&1 + +# hourly asset update and electrs restart +6 * * * * cd $HOME/asset_registry_db && git pull --quiet origin master && cd $HOME/asset_registry_testnet_db && git pull --quiet origin master && killall electrs diff --git a/production/mempool.crontab b/production/mempool.crontab index d953feac4..08639362f 100644 --- a/production/mempool.crontab +++ b/production/mempool.crontab @@ -1,3 +1,12 @@ +# start on reboot @reboot sleep 10 ; $HOME/start -37 13 * * * sleep 30 ; /mempool/mempool.space/backup >/dev/null 2>&1 & + +# start cache warmer on reboot @reboot sleep 180 ; /mempool/mempool/production/nginx-cache-warmer >/dev/null 2>&1 & + +# daily backup +37 13 * * * sleep 30 ; /mempool/mempool.space/backup >/dev/null 2>&1 & + +# hourly liquid asset update +6 * * * * cd $HOME/liquid/frontend && npm run sync-assets && rsync -av $HOME/liquid/frontend/dist/mempool/browser/en-US/resources/assets* $HOME/public_html/liquid/en-US/resources/ >/dev/null 2>&1 + From 068f7392dd076befa138506317c36b4925aad348 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Mon, 1 Aug 2022 17:25:44 +0200 Subject: [PATCH 068/105] Import LN historical statistics (network wide + per node) --- backend/package-lock.json | 38 +++ backend/package.json | 1 + backend/src/api/database-migration.ts | 6 +- backend/src/config.ts | 4 +- .../tasks/lightning/stats-updater.service.ts | 183 +---------- .../sync-tasks/funding-tx-fetcher.ts | 104 +++++++ .../lightning/sync-tasks/stats-importer.ts | 287 ++++++++++++++++++ 7 files changed, 440 insertions(+), 183 deletions(-) create mode 100644 backend/src/tasks/lightning/sync-tasks/funding-tx-fetcher.ts create mode 100644 backend/src/tasks/lightning/sync-tasks/stats-importer.ts diff --git a/backend/package-lock.json b/backend/package-lock.json index b23a7f874..968cb953b 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -31,6 +31,7 @@ "@typescript-eslint/parser": "^5.30.5", "eslint": "^8.19.0", "eslint-config-prettier": "^8.5.0", + "fast-xml-parser": "^4.0.9", "prettier": "^2.7.1" } }, @@ -1496,6 +1497,22 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fast-xml-parser": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.0.9.tgz", + "integrity": "sha512-4G8EzDg2Nb1Qurs3f7BpFV4+jpMVsdgLVuG1Uv8O2OHJfVCg7gcA53obuKbmVqzd4Y7YXVBK05oJG7hzGIdyzg==", + "dev": true, + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + }, + "funding": { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + }, "node_modules/fastq": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", @@ -2665,6 +2682,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", + "dev": true + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -3973,6 +3996,15 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "fast-xml-parser": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.0.9.tgz", + "integrity": "sha512-4G8EzDg2Nb1Qurs3f7BpFV4+jpMVsdgLVuG1Uv8O2OHJfVCg7gcA53obuKbmVqzd4Y7YXVBK05oJG7hzGIdyzg==", + "dev": true, + "requires": { + "strnum": "^1.0.5" + } + }, "fastq": { "version": "1.13.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", @@ -4817,6 +4849,12 @@ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", "dev": true }, + "strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", + "dev": true + }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", diff --git a/backend/package.json b/backend/package.json index 6345e89da..750380156 100644 --- a/backend/package.json +++ b/backend/package.json @@ -53,6 +53,7 @@ "@typescript-eslint/parser": "^5.30.5", "eslint": "^8.19.0", "eslint-config-prettier": "^8.5.0", + "fast-xml-parser": "^4.0.9", "prettier": "^2.7.1" } } diff --git a/backend/src/api/database-migration.ts b/backend/src/api/database-migration.ts index d26bfd6cc..816efc7cc 100644 --- a/backend/src/api/database-migration.ts +++ b/backend/src/api/database-migration.ts @@ -4,7 +4,7 @@ import logger from '../logger'; import { Common } from './common'; class DatabaseMigration { - private static currentVersion = 33; + private static currentVersion = 34; private queryTimeout = 120000; private statisticsAddedIndexed = false; private uniqueLogs: string[] = []; @@ -311,6 +311,10 @@ class DatabaseMigration { if (databaseSchemaVersion < 33 && isBitcoin == true) { await this.$executeQuery('ALTER TABLE `geo_names` CHANGE `type` `type` enum("city","country","division","continent","as_organization", "country_iso_code") NOT NULL'); } + + if (databaseSchemaVersion < 34 && isBitcoin == true) { + await this.$executeQuery('ALTER TABLE `lightning_stats` ADD clearnet_tor_nodes int(11) NOT NULL DEFAULT "0"'); + } } /** diff --git a/backend/src/config.ts b/backend/src/config.ts index 5560a25a7..d480e6c51 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -31,6 +31,7 @@ interface IConfig { LIGHTNING: { ENABLED: boolean; BACKEND: 'lnd' | 'cln' | 'ldk'; + TOPOLOGY_FOLDER: string; }; LND: { TLS_CERT_PATH: string; @@ -177,7 +178,8 @@ const defaults: IConfig = { }, 'LIGHTNING': { 'ENABLED': false, - 'BACKEND': 'lnd' + 'BACKEND': 'lnd', + 'TOPOLOGY_FOLDER': '', }, 'LND': { 'TLS_CERT_PATH': '', diff --git a/backend/src/tasks/lightning/stats-updater.service.ts b/backend/src/tasks/lightning/stats-updater.service.ts index 0a3ade614..c48b683cd 100644 --- a/backend/src/tasks/lightning/stats-updater.service.ts +++ b/backend/src/tasks/lightning/stats-updater.service.ts @@ -3,7 +3,7 @@ import DB from '../../database'; import logger from '../../logger'; import lightningApi from '../../api/lightning/lightning-api-factory'; import channelsApi from '../../api/explorer/channels.api'; -import * as net from 'net'; +import { isIP } from 'net'; class LightningStatsUpdater { hardCodedStartTime = '2018-01-12'; @@ -28,9 +28,6 @@ class LightningStatsUpdater { return; } - await this.$populateHistoricalStatistics(); - await this.$populateHistoricalNodeStatistics(); - setTimeout(() => { this.$runTasks(); }, this.timeUntilMidnight()); @@ -85,7 +82,7 @@ class LightningStatsUpdater { if (hasOnion) { torNodes++; } - const hasClearnet = [4, 6].includes(net.isIP(socket.addr.split(':')[0])); + const hasClearnet = [4, 6].includes(isIP(socket.split(':')[0])); if (hasClearnet) { clearnetNodes++; } @@ -167,182 +164,6 @@ class LightningStatsUpdater { logger.err('$logNodeStatsDaily() error: ' + (e instanceof Error ? e.message : e)); } } - - // We only run this on first launch - private async $populateHistoricalStatistics() { - try { - const [rows]: any = await DB.query(`SELECT COUNT(*) FROM lightning_stats`); - // Only run if table is empty - if (rows[0]['COUNT(*)'] > 0) { - return; - } - logger.info(`Running historical stats population...`); - - const [channels]: any = await DB.query(`SELECT capacity, created, closing_date FROM channels ORDER BY created ASC`); - const [nodes]: any = await DB.query(`SELECT first_seen, sockets FROM nodes ORDER BY first_seen ASC`); - - const date: Date = new Date(this.hardCodedStartTime); - const currentDate = new Date(); - this.setDateMidnight(currentDate); - - while (date < currentDate) { - let totalCapacity = 0; - let channelsCount = 0; - - for (const channel of channels) { - if (new Date(channel.created) > date) { - break; - } - if (channel.closing_date === null || new Date(channel.closing_date) > date) { - totalCapacity += channel.capacity; - channelsCount++; - } - } - - let nodeCount = 0; - let clearnetNodes = 0; - let torNodes = 0; - let unannouncedNodes = 0; - - for (const node of nodes) { - if (new Date(node.first_seen) > date) { - break; - } - nodeCount++; - - const sockets = node.sockets.split(','); - let isUnnanounced = true; - for (const socket of sockets) { - const hasOnion = socket.indexOf('.onion') !== -1; - if (hasOnion) { - torNodes++; - isUnnanounced = false; - } - const hasClearnet = [4, 6].includes(net.isIP(socket.substring(0, socket.lastIndexOf(':')))); - if (hasClearnet) { - clearnetNodes++; - isUnnanounced = false; - } - } - if (isUnnanounced) { - unannouncedNodes++; - } - } - - const query = `INSERT INTO lightning_stats( - added, - channel_count, - node_count, - total_capacity, - tor_nodes, - clearnet_nodes, - unannounced_nodes, - avg_capacity, - avg_fee_rate, - avg_base_fee_mtokens, - med_capacity, - med_fee_rate, - med_base_fee_mtokens - ) - VALUES (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`; - - const rowTimestamp = date.getTime() / 1000; // Save timestamp for the row insertion down below - - date.setUTCDate(date.getUTCDate() + 1); - - // Last iteration, save channels stats - const channelStats = (date >= currentDate ? await channelsApi.$getChannelsStats() : undefined); - - await DB.query(query, [ - rowTimestamp, - channelsCount, - nodeCount, - totalCapacity, - torNodes, - clearnetNodes, - unannouncedNodes, - channelStats?.avgCapacity ?? 0, - channelStats?.avgFeeRate ?? 0, - channelStats?.avgBaseFee ?? 0, - channelStats?.medianCapacity ?? 0, - channelStats?.medianFeeRate ?? 0, - channelStats?.medianBaseFee ?? 0, - ]); - } - - logger.info('Historical stats populated.'); - } catch (e) { - logger.err('$populateHistoricalData() error: ' + (e instanceof Error ? e.message : e)); - } - } - - private async $populateHistoricalNodeStatistics() { - try { - const [rows]: any = await DB.query(`SELECT COUNT(*) FROM node_stats`); - // Only run if table is empty - if (rows[0]['COUNT(*)'] > 0) { - return; - } - logger.info(`Running historical node stats population...`); - - const [nodes]: any = await DB.query(`SELECT public_key, first_seen, alias FROM nodes ORDER BY first_seen ASC`); - - for (const node of nodes) { - const [channels]: any = await DB.query(`SELECT capacity, created, closing_date FROM channels WHERE node1_public_key = ? OR node2_public_key = ? ORDER BY created ASC`, [node.public_key, node.public_key]); - - const date: Date = new Date(this.hardCodedStartTime); - const currentDate = new Date(); - this.setDateMidnight(currentDate); - - let lastTotalCapacity = 0; - let lastChannelsCount = 0; - - while (date < currentDate) { - let totalCapacity = 0; - let channelsCount = 0; - for (const channel of channels) { - if (new Date(channel.created) > date) { - break; - } - if (channel.closing_date !== null && new Date(channel.closing_date) < date) { - date.setUTCDate(date.getUTCDate() + 1); - continue; - } - totalCapacity += channel.capacity; - channelsCount++; - } - - if (lastTotalCapacity === totalCapacity && lastChannelsCount === channelsCount) { - date.setUTCDate(date.getUTCDate() + 1); - continue; - } - - lastTotalCapacity = totalCapacity; - lastChannelsCount = channelsCount; - - const query = `INSERT INTO node_stats( - public_key, - added, - capacity, - channels - ) - VALUES (?, FROM_UNIXTIME(?), ?, ?)`; - - await DB.query(query, [ - node.public_key, - date.getTime() / 1000, - totalCapacity, - channelsCount, - ]); - date.setUTCDate(date.getUTCDate() + 1); - } - logger.debug('Updated node_stats for: ' + node.alias); - } - logger.info('Historical stats populated.'); - } catch (e) { - logger.err('$populateHistoricalNodeData() error: ' + (e instanceof Error ? e.message : e)); - } - } } export default new LightningStatsUpdater(); diff --git a/backend/src/tasks/lightning/sync-tasks/funding-tx-fetcher.ts b/backend/src/tasks/lightning/sync-tasks/funding-tx-fetcher.ts new file mode 100644 index 000000000..b9407c44d --- /dev/null +++ b/backend/src/tasks/lightning/sync-tasks/funding-tx-fetcher.ts @@ -0,0 +1,104 @@ +import { existsSync, readFileSync, writeFileSync } from 'fs'; +import bitcoinClient from '../../../api/bitcoin/bitcoin-client'; +import config from '../../../config'; +import logger from '../../../logger'; + +const BLOCKS_CACHE_MAX_SIZE = 100; +const CACHE_FILE_NAME = config.MEMPOOL.CACHE_DIR + '/ln-funding-txs-cache.json'; + +class FundingTxFetcher { + private running = false; + private blocksCache = {}; + private channelNewlyProcessed = 0; + public fundingTxCache = {}; + + async $fetchChannelsFundingTxs(channelIds: string[]): Promise { + if (this.running) { + return; + } + this.running = true; + + // Load funding tx disk cache + if (Object.keys(this.fundingTxCache).length === 0 && existsSync(CACHE_FILE_NAME)) { + try { + this.fundingTxCache = JSON.parse(readFileSync(CACHE_FILE_NAME, 'utf-8')); + } catch (e) { + logger.err(`Unable to parse channels funding txs disk cache. Starting from scratch`); + this.fundingTxCache = {}; + } + logger.debug(`Imported ${Object.keys(this.fundingTxCache).length} funding tx amount from the disk cache`); + } + + const globalTimer = new Date().getTime() / 1000; + let cacheTimer = new Date().getTime() / 1000; + let loggerTimer = new Date().getTime() / 1000; + let channelProcessed = 0; + this.channelNewlyProcessed = 0; + for (const channelId of channelIds) { + await this.$fetchChannelOpenTx(channelId); + ++channelProcessed; + + let elapsedSeconds = Math.round((new Date().getTime() / 1000) - loggerTimer); + if (elapsedSeconds > 10) { + elapsedSeconds = Math.round((new Date().getTime() / 1000) - globalTimer); + logger.debug(`Indexing channels funding tx ${channelProcessed + 1} of ${channelIds.length} ` + + `(${Math.floor(channelProcessed / channelIds.length * 10000) / 100}%) | ` + + `elapsed: ${elapsedSeconds} seconds` + ); + loggerTimer = new Date().getTime() / 1000; + } + + elapsedSeconds = Math.round((new Date().getTime() / 1000) - cacheTimer); + if (elapsedSeconds > 60) { + logger.debug(`Saving ${Object.keys(this.fundingTxCache).length} funding txs cache into disk`); + writeFileSync(CACHE_FILE_NAME, JSON.stringify(this.fundingTxCache)); + cacheTimer = new Date().getTime() / 1000; + } + } + + if (this.channelNewlyProcessed > 0) { + logger.info(`Indexed ${this.channelNewlyProcessed} additional channels funding tx`); + logger.debug(`Saving ${Object.keys(this.fundingTxCache).length} funding txs cache into disk`); + writeFileSync(CACHE_FILE_NAME, JSON.stringify(this.fundingTxCache)); + } + + this.running = false; + } + + public async $fetchChannelOpenTx(channelId: string): Promise { + if (this.fundingTxCache[channelId]) { + return this.fundingTxCache[channelId]; + } + + const parts = channelId.split('x'); + const blockHeight = parts[0]; + const txIdx = parts[1]; + const outputIdx = parts[2]; + + let block = this.blocksCache[blockHeight]; + if (!block) { + const blockHash = await bitcoinClient.getBlockHash(parseInt(blockHeight, 10)); + block = await bitcoinClient.getBlock(blockHash, 2); + this.blocksCache[block.height] = block; + } + + const blocksCacheHashes = Object.keys(this.blocksCache).sort(); + if (blocksCacheHashes.length > BLOCKS_CACHE_MAX_SIZE) { + for (let i = 0; i < 10; ++i) { + delete this.blocksCache[blocksCacheHashes[i]]; + } + } + + this.fundingTxCache[channelId] = { + timestamp: block.time, + txid: block.tx[txIdx].txid, + value: block.tx[txIdx].vout[outputIdx].value, + }; + + ++this.channelNewlyProcessed; + + return this.fundingTxCache[channelId]; + } +} + +export default new FundingTxFetcher; \ No newline at end of file diff --git a/backend/src/tasks/lightning/sync-tasks/stats-importer.ts b/backend/src/tasks/lightning/sync-tasks/stats-importer.ts new file mode 100644 index 000000000..a0a256457 --- /dev/null +++ b/backend/src/tasks/lightning/sync-tasks/stats-importer.ts @@ -0,0 +1,287 @@ +import DB from '../../../database'; +import { readdirSync, readFileSync } from 'fs'; +import { XMLParser } from 'fast-xml-parser'; +import logger from '../../../logger'; +import fundingTxFetcher from './funding-tx-fetcher'; +import config from '../../../config'; + +interface Node { + id: string; + timestamp: number; + features: string; + rgb_color: string; + alias: string; + addresses: string; + out_degree: number; + in_degree: number; +} + +interface Channel { + scid: string; + source: string; + destination: string; + timestamp: number; + features: string; + fee_base_msat: number; + fee_proportional_millionths: number; + htlc_minimim_msat: number; + cltv_expiry_delta: number; + htlc_maximum_msat: number; +} + +const topologiesFolder = config.LIGHTNING.TOPOLOGY_FOLDER; +const parser = new XMLParser(); + +let latestNodeCount = 1; // Ignore gap in the data + +async function $run(): Promise { + // const [channels]: any[] = await DB.query('SELECT short_id from channels;'); + // logger.info('Caching funding txs for currently existing channels'); + // await fundingTxFetcher.$fetchChannelsFundingTxs(channels.map(channel => channel.short_id)); + + await $importHistoricalLightningStats(); +} + +/** + * Parse the file content into XML, and return a list of nodes and channels + */ +function parseFile(fileContent): any { + const graph = parser.parse(fileContent); + if (Object.keys(graph).length === 0) { + return null; + } + + const nodes: Node[] = []; + const channels: Channel[] = []; + + // If there is only one entry, the parser does not return an array, so we override this + if (!Array.isArray(graph.graphml.graph.node)) { + graph.graphml.graph.node = [graph.graphml.graph.node]; + } + if (!Array.isArray(graph.graphml.graph.edge)) { + graph.graphml.graph.edge = [graph.graphml.graph.edge]; + } + + for (const node of graph.graphml.graph.node) { + if (!node.data) { + continue; + } + nodes.push({ + id: node.data[0], + timestamp: node.data[1], + features: node.data[2], + rgb_color: node.data[3], + alias: node.data[4], + addresses: node.data[5], + out_degree: node.data[6], + in_degree: node.data[7], + }); + } + + for (const channel of graph.graphml.graph.edge) { + if (!channel.data) { + continue; + } + channels.push({ + scid: channel.data[0], + source: channel.data[1], + destination: channel.data[2], + timestamp: channel.data[3], + features: channel.data[4], + fee_base_msat: channel.data[5], + fee_proportional_millionths: channel.data[6], + htlc_minimim_msat: channel.data[7], + cltv_expiry_delta: channel.data[8], + htlc_maximum_msat: channel.data[9], + }); + } + + return { + nodes: nodes, + channels: channels, + }; +} + +/** + * Generate LN network stats for one day + */ +async function computeNetworkStats(timestamp: number, networkGraph): Promise { + // Node counts and network shares + let clearnetNodes = 0; + let torNodes = 0; + let clearnetTorNodes = 0; + let unannouncedNodes = 0; + + for (const node of networkGraph.nodes) { + let hasOnion = false; + let hasClearnet = false; + let isUnnanounced = true; + + const sockets = node.addresses.split(','); + for (const socket of sockets) { + hasOnion = hasOnion || (socket.indexOf('torv3://') !== -1); + hasClearnet = hasClearnet || (socket.indexOf('ipv4://') !== -1 || socket.indexOf('ipv6://') !== -1); + } + if (hasOnion && hasClearnet) { + clearnetTorNodes++; + isUnnanounced = false; + } else if (hasOnion) { + torNodes++; + isUnnanounced = false; + } else if (hasClearnet) { + clearnetNodes++; + isUnnanounced = false; + } + if (isUnnanounced) { + unannouncedNodes++; + } + } + + // Channels and node historical stats + const nodeStats = {}; + let capacity = 0; + let avgFeeRate = 0; + let avgBaseFee = 0; + const capacities: number[] = []; + const feeRates: number[] = []; + const baseFees: number[] = []; + for (const channel of networkGraph.channels) { + const tx = await fundingTxFetcher.$fetchChannelOpenTx(channel.scid.slice(0, -2)); + if (!tx) { + logger.err(`Unable to fetch funding tx for channel ${channel.scid}. Capacity and creation date will stay unknown.`); + continue; + } + + if (!nodeStats[channel.source]) { + nodeStats[channel.source] = { + capacity: 0, + channels: 0, + }; + } + if (!nodeStats[channel.destination]) { + nodeStats[channel.destination] = { + capacity: 0, + channels: 0, + }; + } + + nodeStats[channel.source].capacity += Math.round(tx.value * 100000000); + nodeStats[channel.source].channels++; + nodeStats[channel.destination].capacity += Math.round(tx.value * 100000000); + nodeStats[channel.destination].channels++; + + capacity += Math.round(tx.value * 100000000); + avgFeeRate += channel.fee_proportional_millionths; + avgBaseFee += channel.fee_base_msat; + capacities.push(Math.round(tx.value * 100000000)); + feeRates.push(channel.fee_proportional_millionths); + baseFees.push(channel.fee_base_msat); + } + + avgFeeRate /= networkGraph.channels.length; + avgBaseFee /= networkGraph.channels.length; + const medCapacity = capacities.sort((a, b) => b - a)[Math.round(capacities.length / 2 - 1)]; + const medFeeRate = feeRates.sort((a, b) => b - a)[Math.round(feeRates.length / 2 - 1)]; + const medBaseFee = baseFees.sort((a, b) => b - a)[Math.round(baseFees.length / 2 - 1)]; + + let query = `INSERT INTO lightning_stats( + added, + channel_count, + node_count, + total_capacity, + tor_nodes, + clearnet_nodes, + unannounced_nodes, + clearnet_tor_nodes, + avg_capacity, + avg_fee_rate, + avg_base_fee_mtokens, + med_capacity, + med_fee_rate, + med_base_fee_mtokens + ) + VALUES (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`; + + await DB.query(query, [ + timestamp, + networkGraph.channels.length, + networkGraph.nodes.length, + capacity, + torNodes, + clearnetNodes, + unannouncedNodes, + clearnetTorNodes, + Math.round(capacity / networkGraph.channels.length), + avgFeeRate, + avgBaseFee, + medCapacity, + medFeeRate, + medBaseFee, + ]); + + for (const public_key of Object.keys(nodeStats)) { + query = `INSERT INTO node_stats( + public_key, + added, + capacity, + channels + ) + VALUES (?, FROM_UNIXTIME(?), ?, ?)`; + + await DB.query(query, [ + public_key, + timestamp, + nodeStats[public_key].capacity, + nodeStats[public_key].channels, + ]); + } +} + +export async function $importHistoricalLightningStats(): Promise { + const fileList = readdirSync(topologiesFolder); + fileList.sort().reverse(); + + const [rows]: any[] = await DB.query('SELECT UNIX_TIMESTAMP(added) as added FROM lightning_stats'); + const existingStatsTimestamps = {}; + for (const row of rows) { + existingStatsTimestamps[row.added] = true; + } + + for (const filename of fileList) { + const timestamp = parseInt(filename.split('_')[1], 10); + const fileContent = readFileSync(`${topologiesFolder}/${filename}`, 'utf8'); + + const graph = parseFile(fileContent); + if (!graph) { + continue; + } + + // Ignore drop of more than 90% of the node count as it's probably a missing data point + const diffRatio = graph.nodes.length / latestNodeCount; + if (diffRatio < 0.90) { + continue; + } + latestNodeCount = graph.nodes.length; + + // Stats exist already, don't calculate/insert them + if (existingStatsTimestamps[timestamp] === true) { + continue; + } + + logger.debug(`Processing ${topologiesFolder}/${filename}`); + + const datestr = `${new Date(timestamp * 1000).toUTCString()} (${timestamp})`; + logger.debug(`${datestr}: Found ${graph.nodes.length} nodes and ${graph.channels.length} channels`); + + // Cache funding txs + logger.debug(`Caching funding txs for ${datestr}`); + await fundingTxFetcher.$fetchChannelsFundingTxs(graph.channels.map(channel => channel.scid.slice(0, -2))); + + logger.debug(`Generating LN network stats for ${datestr}`); + await computeNetworkStats(timestamp, graph); + } + + logger.info(`Lightning network stats historical import completed`); +} + +$run().then(() => process.exit(0)); \ No newline at end of file From 6ea171e45a4f9c7823410180e81852dd2edc7b20 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Mon, 1 Aug 2022 17:48:04 +0200 Subject: [PATCH 069/105] Integrate LN stats importer into the main process --- backend/src/index.ts | 10 +- .../tasks/lightning/stats-updater.service.ts | 5 +- .../lightning/sync-tasks/stats-importer.ts | 472 +++++++++--------- 3 files changed, 246 insertions(+), 241 deletions(-) diff --git a/backend/src/index.ts b/backend/src/index.ts index b7159afaf..fa80fb2ad 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -29,11 +29,11 @@ import channelsRoutes from './api/explorer/channels.routes'; import generalLightningRoutes from './api/explorer/general.routes'; import lightningStatsUpdater from './tasks/lightning/stats-updater.service'; import nodeSyncService from './tasks/lightning/node-sync.service'; -import statisticsRoutes from "./api/statistics/statistics.routes"; -import miningRoutes from "./api/mining/mining-routes"; -import bisqRoutes from "./api/bisq/bisq.routes"; -import liquidRoutes from "./api/liquid/liquid.routes"; -import bitcoinRoutes from "./api/bitcoin/bitcoin.routes"; +import statisticsRoutes from './api/statistics/statistics.routes'; +import miningRoutes from './api/mining/mining-routes'; +import bisqRoutes from './api/bisq/bisq.routes'; +import liquidRoutes from './api/liquid/liquid.routes'; +import bitcoinRoutes from './api/bitcoin/bitcoin.routes'; class Server { private wss: WebSocket.Server | undefined; diff --git a/backend/src/tasks/lightning/stats-updater.service.ts b/backend/src/tasks/lightning/stats-updater.service.ts index c48b683cd..c5ca55cd8 100644 --- a/backend/src/tasks/lightning/stats-updater.service.ts +++ b/backend/src/tasks/lightning/stats-updater.service.ts @@ -4,11 +4,12 @@ import logger from '../../logger'; import lightningApi from '../../api/lightning/lightning-api-factory'; import channelsApi from '../../api/explorer/channels.api'; import { isIP } from 'net'; +import LightningStatsImporter from './sync-tasks/stats-importer'; class LightningStatsUpdater { hardCodedStartTime = '2018-01-12'; - public async $startService() { + public async $startService(): Promise { logger.info('Starting Lightning Stats service'); let isInSync = false; let error: any; @@ -28,6 +29,8 @@ class LightningStatsUpdater { return; } + LightningStatsImporter.$run(); + setTimeout(() => { this.$runTasks(); }, this.timeUntilMidnight()); diff --git a/backend/src/tasks/lightning/sync-tasks/stats-importer.ts b/backend/src/tasks/lightning/sync-tasks/stats-importer.ts index a0a256457..9dd5751b9 100644 --- a/backend/src/tasks/lightning/sync-tasks/stats-importer.ts +++ b/backend/src/tasks/lightning/sync-tasks/stats-importer.ts @@ -29,259 +29,261 @@ interface Channel { htlc_maximum_msat: number; } -const topologiesFolder = config.LIGHTNING.TOPOLOGY_FOLDER; -const parser = new XMLParser(); +class LightningStatsImporter { + topologiesFolder = config.LIGHTNING.TOPOLOGY_FOLDER; + parser = new XMLParser(); -let latestNodeCount = 1; // Ignore gap in the data + latestNodeCount = 1; // Ignore gap in the data -async function $run(): Promise { - // const [channels]: any[] = await DB.query('SELECT short_id from channels;'); - // logger.info('Caching funding txs for currently existing channels'); - // await fundingTxFetcher.$fetchChannelsFundingTxs(channels.map(channel => channel.short_id)); - - await $importHistoricalLightningStats(); -} - -/** - * Parse the file content into XML, and return a list of nodes and channels - */ -function parseFile(fileContent): any { - const graph = parser.parse(fileContent); - if (Object.keys(graph).length === 0) { - return null; + async $run(): Promise { + // const [channels]: any[] = await DB.query('SELECT short_id from channels;'); + // logger.info('Caching funding txs for currently existing channels'); + // await fundingTxFetcher.$fetchChannelsFundingTxs(channels.map(channel => channel.short_id)); + + await this.$importHistoricalLightningStats(); } - const nodes: Node[] = []; - const channels: Channel[] = []; + /** + * Parse the file content into XML, and return a list of nodes and channels + */ + parseFile(fileContent): any { + const graph = this.parser.parse(fileContent); + if (Object.keys(graph).length === 0) { + return null; + } - // If there is only one entry, the parser does not return an array, so we override this - if (!Array.isArray(graph.graphml.graph.node)) { - graph.graphml.graph.node = [graph.graphml.graph.node]; - } - if (!Array.isArray(graph.graphml.graph.edge)) { - graph.graphml.graph.edge = [graph.graphml.graph.edge]; + const nodes: Node[] = []; + const channels: Channel[] = []; + + // If there is only one entry, the parser does not return an array, so we override this + if (!Array.isArray(graph.graphml.graph.node)) { + graph.graphml.graph.node = [graph.graphml.graph.node]; + } + if (!Array.isArray(graph.graphml.graph.edge)) { + graph.graphml.graph.edge = [graph.graphml.graph.edge]; + } + + for (const node of graph.graphml.graph.node) { + if (!node.data) { + continue; + } + nodes.push({ + id: node.data[0], + timestamp: node.data[1], + features: node.data[2], + rgb_color: node.data[3], + alias: node.data[4], + addresses: node.data[5], + out_degree: node.data[6], + in_degree: node.data[7], + }); + } + + for (const channel of graph.graphml.graph.edge) { + if (!channel.data) { + continue; + } + channels.push({ + scid: channel.data[0], + source: channel.data[1], + destination: channel.data[2], + timestamp: channel.data[3], + features: channel.data[4], + fee_base_msat: channel.data[5], + fee_proportional_millionths: channel.data[6], + htlc_minimim_msat: channel.data[7], + cltv_expiry_delta: channel.data[8], + htlc_maximum_msat: channel.data[9], + }); + } + + return { + nodes: nodes, + channels: channels, + }; } - for (const node of graph.graphml.graph.node) { - if (!node.data) { - continue; - } - nodes.push({ - id: node.data[0], - timestamp: node.data[1], - features: node.data[2], - rgb_color: node.data[3], - alias: node.data[4], - addresses: node.data[5], - out_degree: node.data[6], - in_degree: node.data[7], - }); - } + /** + * Generate LN network stats for one day + */ + async computeNetworkStats(timestamp: number, networkGraph): Promise { + // Node counts and network shares + let clearnetNodes = 0; + let torNodes = 0; + let clearnetTorNodes = 0; + let unannouncedNodes = 0; - for (const channel of graph.graphml.graph.edge) { - if (!channel.data) { - continue; - } - channels.push({ - scid: channel.data[0], - source: channel.data[1], - destination: channel.data[2], - timestamp: channel.data[3], - features: channel.data[4], - fee_base_msat: channel.data[5], - fee_proportional_millionths: channel.data[6], - htlc_minimim_msat: channel.data[7], - cltv_expiry_delta: channel.data[8], - htlc_maximum_msat: channel.data[9], - }); - } + for (const node of networkGraph.nodes) { + let hasOnion = false; + let hasClearnet = false; + let isUnnanounced = true; - return { - nodes: nodes, - channels: channels, - }; -} - -/** - * Generate LN network stats for one day - */ -async function computeNetworkStats(timestamp: number, networkGraph): Promise { - // Node counts and network shares - let clearnetNodes = 0; - let torNodes = 0; - let clearnetTorNodes = 0; - let unannouncedNodes = 0; - - for (const node of networkGraph.nodes) { - let hasOnion = false; - let hasClearnet = false; - let isUnnanounced = true; - - const sockets = node.addresses.split(','); - for (const socket of sockets) { - hasOnion = hasOnion || (socket.indexOf('torv3://') !== -1); - hasClearnet = hasClearnet || (socket.indexOf('ipv4://') !== -1 || socket.indexOf('ipv6://') !== -1); - } - if (hasOnion && hasClearnet) { - clearnetTorNodes++; - isUnnanounced = false; - } else if (hasOnion) { - torNodes++; - isUnnanounced = false; - } else if (hasClearnet) { - clearnetNodes++; - isUnnanounced = false; - } - if (isUnnanounced) { - unannouncedNodes++; - } - } - - // Channels and node historical stats - const nodeStats = {}; - let capacity = 0; - let avgFeeRate = 0; - let avgBaseFee = 0; - const capacities: number[] = []; - const feeRates: number[] = []; - const baseFees: number[] = []; - for (const channel of networkGraph.channels) { - const tx = await fundingTxFetcher.$fetchChannelOpenTx(channel.scid.slice(0, -2)); - if (!tx) { - logger.err(`Unable to fetch funding tx for channel ${channel.scid}. Capacity and creation date will stay unknown.`); - continue; + const sockets = node.addresses.split(','); + for (const socket of sockets) { + hasOnion = hasOnion || (socket.indexOf('torv3://') !== -1); + hasClearnet = hasClearnet || (socket.indexOf('ipv4://') !== -1 || socket.indexOf('ipv6://') !== -1); + } + if (hasOnion && hasClearnet) { + clearnetTorNodes++; + isUnnanounced = false; + } else if (hasOnion) { + torNodes++; + isUnnanounced = false; + } else if (hasClearnet) { + clearnetNodes++; + isUnnanounced = false; + } + if (isUnnanounced) { + unannouncedNodes++; + } } - if (!nodeStats[channel.source]) { - nodeStats[channel.source] = { - capacity: 0, - channels: 0, - }; - } - if (!nodeStats[channel.destination]) { - nodeStats[channel.destination] = { - capacity: 0, - channels: 0, - }; + // Channels and node historical stats + const nodeStats = {}; + let capacity = 0; + let avgFeeRate = 0; + let avgBaseFee = 0; + const capacities: number[] = []; + const feeRates: number[] = []; + const baseFees: number[] = []; + for (const channel of networkGraph.channels) { + const tx = await fundingTxFetcher.$fetchChannelOpenTx(channel.scid.slice(0, -2)); + if (!tx) { + logger.err(`Unable to fetch funding tx for channel ${channel.scid}. Capacity and creation date will stay unknown.`); + continue; + } + + if (!nodeStats[channel.source]) { + nodeStats[channel.source] = { + capacity: 0, + channels: 0, + }; + } + if (!nodeStats[channel.destination]) { + nodeStats[channel.destination] = { + capacity: 0, + channels: 0, + }; + } + + nodeStats[channel.source].capacity += Math.round(tx.value * 100000000); + nodeStats[channel.source].channels++; + nodeStats[channel.destination].capacity += Math.round(tx.value * 100000000); + nodeStats[channel.destination].channels++; + + capacity += Math.round(tx.value * 100000000); + avgFeeRate += channel.fee_proportional_millionths; + avgBaseFee += channel.fee_base_msat; + capacities.push(Math.round(tx.value * 100000000)); + feeRates.push(channel.fee_proportional_millionths); + baseFees.push(channel.fee_base_msat); } - nodeStats[channel.source].capacity += Math.round(tx.value * 100000000); - nodeStats[channel.source].channels++; - nodeStats[channel.destination].capacity += Math.round(tx.value * 100000000); - nodeStats[channel.destination].channels++; - - capacity += Math.round(tx.value * 100000000); - avgFeeRate += channel.fee_proportional_millionths; - avgBaseFee += channel.fee_base_msat; - capacities.push(Math.round(tx.value * 100000000)); - feeRates.push(channel.fee_proportional_millionths); - baseFees.push(channel.fee_base_msat); - } - - avgFeeRate /= networkGraph.channels.length; - avgBaseFee /= networkGraph.channels.length; - const medCapacity = capacities.sort((a, b) => b - a)[Math.round(capacities.length / 2 - 1)]; - const medFeeRate = feeRates.sort((a, b) => b - a)[Math.round(feeRates.length / 2 - 1)]; - const medBaseFee = baseFees.sort((a, b) => b - a)[Math.round(baseFees.length / 2 - 1)]; - - let query = `INSERT INTO lightning_stats( - added, - channel_count, - node_count, - total_capacity, - tor_nodes, - clearnet_nodes, - unannounced_nodes, - clearnet_tor_nodes, - avg_capacity, - avg_fee_rate, - avg_base_fee_mtokens, - med_capacity, - med_fee_rate, - med_base_fee_mtokens - ) - VALUES (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`; - - await DB.query(query, [ - timestamp, - networkGraph.channels.length, - networkGraph.nodes.length, - capacity, - torNodes, - clearnetNodes, - unannouncedNodes, - clearnetTorNodes, - Math.round(capacity / networkGraph.channels.length), - avgFeeRate, - avgBaseFee, - medCapacity, - medFeeRate, - medBaseFee, - ]); - - for (const public_key of Object.keys(nodeStats)) { - query = `INSERT INTO node_stats( - public_key, + avgFeeRate /= networkGraph.channels.length; + avgBaseFee /= networkGraph.channels.length; + const medCapacity = capacities.sort((a, b) => b - a)[Math.round(capacities.length / 2 - 1)]; + const medFeeRate = feeRates.sort((a, b) => b - a)[Math.round(feeRates.length / 2 - 1)]; + const medBaseFee = baseFees.sort((a, b) => b - a)[Math.round(baseFees.length / 2 - 1)]; + + let query = `INSERT INTO lightning_stats( added, - capacity, - channels + channel_count, + node_count, + total_capacity, + tor_nodes, + clearnet_nodes, + unannounced_nodes, + clearnet_tor_nodes, + avg_capacity, + avg_fee_rate, + avg_base_fee_mtokens, + med_capacity, + med_fee_rate, + med_base_fee_mtokens ) - VALUES (?, FROM_UNIXTIME(?), ?, ?)`; - + VALUES (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`; + await DB.query(query, [ - public_key, timestamp, - nodeStats[public_key].capacity, - nodeStats[public_key].channels, + networkGraph.channels.length, + networkGraph.nodes.length, + capacity, + torNodes, + clearnetNodes, + unannouncedNodes, + clearnetTorNodes, + Math.round(capacity / networkGraph.channels.length), + avgFeeRate, + avgBaseFee, + medCapacity, + medFeeRate, + medBaseFee, ]); + + for (const public_key of Object.keys(nodeStats)) { + query = `INSERT INTO node_stats( + public_key, + added, + capacity, + channels + ) + VALUES (?, FROM_UNIXTIME(?), ?, ?)`; + + await DB.query(query, [ + public_key, + timestamp, + nodeStats[public_key].capacity, + nodeStats[public_key].channels, + ]); + } + } + + async $importHistoricalLightningStats(): Promise { + const fileList = readdirSync(this.topologiesFolder); + fileList.sort().reverse(); + + const [rows]: any[] = await DB.query('SELECT UNIX_TIMESTAMP(added) as added FROM lightning_stats'); + const existingStatsTimestamps = {}; + for (const row of rows) { + existingStatsTimestamps[row.added] = true; + } + + for (const filename of fileList) { + const timestamp = parseInt(filename.split('_')[1], 10); + const fileContent = readFileSync(`${this.topologiesFolder}/${filename}`, 'utf8'); + + const graph = this.parseFile(fileContent); + if (!graph) { + continue; + } + + // Ignore drop of more than 90% of the node count as it's probably a missing data point + const diffRatio = graph.nodes.length / this.latestNodeCount; + if (diffRatio < 0.90) { + continue; + } + this.latestNodeCount = graph.nodes.length; + + // Stats exist already, don't calculate/insert them + if (existingStatsTimestamps[timestamp] === true) { + continue; + } + + logger.debug(`Processing ${this.topologiesFolder}/${filename}`); + + const datestr = `${new Date(timestamp * 1000).toUTCString()} (${timestamp})`; + logger.debug(`${datestr}: Found ${graph.nodes.length} nodes and ${graph.channels.length} channels`); + + // Cache funding txs + logger.debug(`Caching funding txs for ${datestr}`); + await fundingTxFetcher.$fetchChannelsFundingTxs(graph.channels.map(channel => channel.scid.slice(0, -2))); + + logger.debug(`Generating LN network stats for ${datestr}`); + await this.computeNetworkStats(timestamp, graph); + } + + logger.info(`Lightning network stats historical import completed`); } } -export async function $importHistoricalLightningStats(): Promise { - const fileList = readdirSync(topologiesFolder); - fileList.sort().reverse(); - - const [rows]: any[] = await DB.query('SELECT UNIX_TIMESTAMP(added) as added FROM lightning_stats'); - const existingStatsTimestamps = {}; - for (const row of rows) { - existingStatsTimestamps[row.added] = true; - } - - for (const filename of fileList) { - const timestamp = parseInt(filename.split('_')[1], 10); - const fileContent = readFileSync(`${topologiesFolder}/${filename}`, 'utf8'); - - const graph = parseFile(fileContent); - if (!graph) { - continue; - } - - // Ignore drop of more than 90% of the node count as it's probably a missing data point - const diffRatio = graph.nodes.length / latestNodeCount; - if (diffRatio < 0.90) { - continue; - } - latestNodeCount = graph.nodes.length; - - // Stats exist already, don't calculate/insert them - if (existingStatsTimestamps[timestamp] === true) { - continue; - } - - logger.debug(`Processing ${topologiesFolder}/${filename}`); - - const datestr = `${new Date(timestamp * 1000).toUTCString()} (${timestamp})`; - logger.debug(`${datestr}: Found ${graph.nodes.length} nodes and ${graph.channels.length} channels`); - - // Cache funding txs - logger.debug(`Caching funding txs for ${datestr}`); - await fundingTxFetcher.$fetchChannelsFundingTxs(graph.channels.map(channel => channel.scid.slice(0, -2))); - - logger.debug(`Generating LN network stats for ${datestr}`); - await computeNetworkStats(timestamp, graph); - } - - logger.info(`Lightning network stats historical import completed`); -} - -$run().then(() => process.exit(0)); \ No newline at end of file +export default new LightningStatsImporter; \ No newline at end of file From 5eed515bdb2a4d8595b3fe8ec1042f99affcd1ac Mon Sep 17 00:00:00 2001 From: nymkappa Date: Mon, 1 Aug 2022 18:21:45 +0200 Subject: [PATCH 070/105] Re-use LN stats importer code to log daily LN stats --- .../tasks/lightning/stats-updater.service.ts | 111 ++---------------- .../lightning/sync-tasks/stats-importer.ts | 4 +- 2 files changed, 11 insertions(+), 104 deletions(-) diff --git a/backend/src/tasks/lightning/stats-updater.service.ts b/backend/src/tasks/lightning/stats-updater.service.ts index c5ca55cd8..d093892bb 100644 --- a/backend/src/tasks/lightning/stats-updater.service.ts +++ b/backend/src/tasks/lightning/stats-updater.service.ts @@ -56,116 +56,21 @@ class LightningStatsUpdater { } private async $runTasks(): Promise { - await this.$logLightningStatsDaily(); - await this.$logNodeStatsDaily(); + await this.$logStatsDaily(); setTimeout(() => { this.$runTasks(); }, this.timeUntilMidnight()); } - private async $logLightningStatsDaily() { - try { - logger.info(`Running lightning daily stats log...`); + private async $logStatsDaily(): Promise { + const date = new Date(); + this.setDateMidnight(date); + date.setUTCHours(24); - const networkGraph = await lightningApi.$getNetworkGraph(); - let total_capacity = 0; - for (const channel of networkGraph.edges) { - if (channel.capacity) { - total_capacity += parseInt(channel.capacity); - } - } - - let clearnetNodes = 0; - let torNodes = 0; - let unannouncedNodes = 0; - for (const node of networkGraph.nodes) { - for (const socket of node.addresses) { - const hasOnion = socket.addr.indexOf('.onion') !== -1; - if (hasOnion) { - torNodes++; - } - const hasClearnet = [4, 6].includes(isIP(socket.split(':')[0])); - if (hasClearnet) { - clearnetNodes++; - } - } - if (node.addresses.length === 0) { - unannouncedNodes++; - } - } - - const channelStats = await channelsApi.$getChannelsStats(); - - const query = `INSERT INTO lightning_stats( - added, - channel_count, - node_count, - total_capacity, - tor_nodes, - clearnet_nodes, - unannounced_nodes, - avg_capacity, - avg_fee_rate, - avg_base_fee_mtokens, - med_capacity, - med_fee_rate, - med_base_fee_mtokens - ) - VALUES (NOW() - INTERVAL 1 DAY, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`; - - await DB.query(query, [ - networkGraph.edges.length, - networkGraph.nodes.length, - total_capacity, - torNodes, - clearnetNodes, - unannouncedNodes, - channelStats.avgCapacity, - channelStats.avgFeeRate, - channelStats.avgBaseFee, - channelStats.medianCapacity, - channelStats.medianFeeRate, - channelStats.medianBaseFee, - ]); - logger.info(`Lightning daily stats done.`); - } catch (e) { - logger.err('$logLightningStatsDaily() error: ' + (e instanceof Error ? e.message : e)); - } - } - - private async $logNodeStatsDaily() { - try { - logger.info(`Running daily node stats update...`); - - const query = ` - SELECT nodes.public_key, c1.channels_count_left, c2.channels_count_right, c1.channels_capacity_left, - c2.channels_capacity_right - FROM nodes - LEFT JOIN ( - SELECT node1_public_key, COUNT(id) AS channels_count_left, SUM(capacity) AS channels_capacity_left - FROM channels - WHERE channels.status = 1 - GROUP BY node1_public_key - ) c1 ON c1.node1_public_key = nodes.public_key - LEFT JOIN ( - SELECT node2_public_key, COUNT(id) AS channels_count_right, SUM(capacity) AS channels_capacity_right - FROM channels WHERE channels.status = 1 GROUP BY node2_public_key - ) c2 ON c2.node2_public_key = nodes.public_key - `; - - const [nodes]: any = await DB.query(query); - - for (const node of nodes) { - await DB.query( - `INSERT INTO node_stats(public_key, added, capacity, channels) VALUES (?, NOW() - INTERVAL 1 DAY, ?, ?)`, - [node.public_key, (parseInt(node.channels_capacity_left || 0, 10)) + (parseInt(node.channels_capacity_right || 0, 10)), - node.channels_count_left + node.channels_count_right]); - } - logger.info('Daily node stats has updated.'); - } catch (e) { - logger.err('$logNodeStatsDaily() error: ' + (e instanceof Error ? e.message : e)); - } + logger.info(`Running lightning daily stats log...`); + const networkGraph = await lightningApi.$getNetworkGraph(); + LightningStatsImporter.computeNetworkStats(date.getTime(), networkGraph); } } diff --git a/backend/src/tasks/lightning/sync-tasks/stats-importer.ts b/backend/src/tasks/lightning/sync-tasks/stats-importer.ts index 9dd5751b9..f6d70df7d 100644 --- a/backend/src/tasks/lightning/sync-tasks/stats-importer.ts +++ b/backend/src/tasks/lightning/sync-tasks/stats-importer.ts @@ -36,6 +36,8 @@ class LightningStatsImporter { latestNodeCount = 1; // Ignore gap in the data async $run(): Promise { + logger.info(`Importing historical lightning stats`); + // const [channels]: any[] = await DB.query('SELECT short_id from channels;'); // logger.info('Caching funding txs for currently existing channels'); // await fundingTxFetcher.$fetchChannelsFundingTxs(channels.map(channel => channel.short_id)); @@ -106,7 +108,7 @@ class LightningStatsImporter { /** * Generate LN network stats for one day */ - async computeNetworkStats(timestamp: number, networkGraph): Promise { + public async computeNetworkStats(timestamp: number, networkGraph): Promise { // Node counts and network shares let clearnetNodes = 0; let torNodes = 0; From 8a93a0fcf3eca2f458de6d66b26b395cb56d9768 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Mon, 1 Aug 2022 19:50:55 +0200 Subject: [PATCH 071/105] We don't need a synced node to import historical data --- .../tasks/lightning/stats-updater.service.ts | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/backend/src/tasks/lightning/stats-updater.service.ts b/backend/src/tasks/lightning/stats-updater.service.ts index d093892bb..f364629b9 100644 --- a/backend/src/tasks/lightning/stats-updater.service.ts +++ b/backend/src/tasks/lightning/stats-updater.service.ts @@ -1,9 +1,6 @@ - import DB from '../../database'; import logger from '../../logger'; import lightningApi from '../../api/lightning/lightning-api-factory'; -import channelsApi from '../../api/explorer/channels.api'; -import { isIP } from 'net'; import LightningStatsImporter from './sync-tasks/stats-importer'; class LightningStatsUpdater { @@ -11,23 +8,6 @@ class LightningStatsUpdater { public async $startService(): Promise { logger.info('Starting Lightning Stats service'); - let isInSync = false; - let error: any; - try { - error = null; - isInSync = await this.$lightningIsSynced(); - } catch (e) { - error = e; - } - if (!isInSync) { - if (error) { - logger.warn('Was not able to fetch Lightning Node status: ' + (error instanceof Error ? error.message : error) + '. Retrying in 1 minute...'); - } else { - logger.notice('The Lightning graph is not yet in sync. Retrying in 1 minute...'); - } - setTimeout(() => this.$startService(), 60 * 1000); - return; - } LightningStatsImporter.$run(); @@ -50,11 +30,6 @@ class LightningStatsUpdater { date.setUTCMilliseconds(0); } - private async $lightningIsSynced(): Promise { - const nodeInfo = await lightningApi.$getInfo(); - return nodeInfo.synced_to_chain && nodeInfo.synced_to_graph; - } - private async $runTasks(): Promise { await this.$logStatsDaily(); From 2c07b7f4fbaa647f79298d095dff93bf43289829 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Tue, 2 Aug 2022 12:19:57 +0200 Subject: [PATCH 072/105] Make sure to not count channels twice --- .../tasks/lightning/stats-updater.service.ts | 1 - .../sync-tasks/funding-tx-fetcher.ts | 34 ++++++++-- .../lightning/sync-tasks/stats-importer.ts | 65 +++++++++++-------- 3 files changed, 66 insertions(+), 34 deletions(-) diff --git a/backend/src/tasks/lightning/stats-updater.service.ts b/backend/src/tasks/lightning/stats-updater.service.ts index f364629b9..5701ef22a 100644 --- a/backend/src/tasks/lightning/stats-updater.service.ts +++ b/backend/src/tasks/lightning/stats-updater.service.ts @@ -1,4 +1,3 @@ -import DB from '../../database'; import logger from '../../logger'; import lightningApi from '../../api/lightning/lightning-api-factory'; import LightningStatsImporter from './sync-tasks/stats-importer'; diff --git a/backend/src/tasks/lightning/sync-tasks/funding-tx-fetcher.ts b/backend/src/tasks/lightning/sync-tasks/funding-tx-fetcher.ts index b9407c44d..4068de8f1 100644 --- a/backend/src/tasks/lightning/sync-tasks/funding-tx-fetcher.ts +++ b/backend/src/tasks/lightning/sync-tasks/funding-tx-fetcher.ts @@ -1,8 +1,11 @@ -import { existsSync, readFileSync, writeFileSync } from 'fs'; +import { existsSync, promises } from 'fs'; import bitcoinClient from '../../../api/bitcoin/bitcoin-client'; import config from '../../../config'; +import DB from '../../../database'; import logger from '../../../logger'; +const fsPromises = promises; + const BLOCKS_CACHE_MAX_SIZE = 100; const CACHE_FILE_NAME = config.MEMPOOL.CACHE_DIR + '/ln-funding-txs-cache.json'; @@ -21,7 +24,7 @@ class FundingTxFetcher { // Load funding tx disk cache if (Object.keys(this.fundingTxCache).length === 0 && existsSync(CACHE_FILE_NAME)) { try { - this.fundingTxCache = JSON.parse(readFileSync(CACHE_FILE_NAME, 'utf-8')); + this.fundingTxCache = JSON.parse(await fsPromises.readFile(CACHE_FILE_NAME, 'utf-8')); } catch (e) { logger.err(`Unable to parse channels funding txs disk cache. Starting from scratch`); this.fundingTxCache = {}; @@ -51,7 +54,7 @@ class FundingTxFetcher { elapsedSeconds = Math.round((new Date().getTime() / 1000) - cacheTimer); if (elapsedSeconds > 60) { logger.debug(`Saving ${Object.keys(this.fundingTxCache).length} funding txs cache into disk`); - writeFileSync(CACHE_FILE_NAME, JSON.stringify(this.fundingTxCache)); + fsPromises.writeFile(CACHE_FILE_NAME, JSON.stringify(this.fundingTxCache)); cacheTimer = new Date().getTime() / 1000; } } @@ -59,7 +62,7 @@ class FundingTxFetcher { if (this.channelNewlyProcessed > 0) { logger.info(`Indexed ${this.channelNewlyProcessed} additional channels funding tx`); logger.debug(`Saving ${Object.keys(this.fundingTxCache).length} funding txs cache into disk`); - writeFileSync(CACHE_FILE_NAME, JSON.stringify(this.fundingTxCache)); + fsPromises.writeFile(CACHE_FILE_NAME, JSON.stringify(this.fundingTxCache)); } this.running = false; @@ -76,13 +79,30 @@ class FundingTxFetcher { const outputIdx = parts[2]; let block = this.blocksCache[blockHeight]; + // Check if we have the block in the `blocks_summaries` table to avoid calling core + if (!block) { + const [rows] = await DB.query(` + SELECT UNIX_TIMESTAMP(blocks.blockTimestamp) AS time, blocks_summaries.transactions AS tx + FROM blocks_summaries + JOIN blocks ON blocks.hash = blocks_summaries.id + WHERE blocks_summaries.height = ${blockHeight} + `); + block = rows[0] ?? null; + if (block) { + block.tx = JSON.parse(block.tx); + if (block.tx.length === 0) { + block = null; + } + } + } + // Fetch it from core if (!block) { const blockHash = await bitcoinClient.getBlockHash(parseInt(blockHeight, 10)); block = await bitcoinClient.getBlock(blockHash, 2); - this.blocksCache[block.height] = block; } + this.blocksCache[block.height] = block; - const blocksCacheHashes = Object.keys(this.blocksCache).sort(); + const blocksCacheHashes = Object.keys(this.blocksCache).sort((a, b) => parseInt(b) - parseInt(a)).reverse(); if (blocksCacheHashes.length > BLOCKS_CACHE_MAX_SIZE) { for (let i = 0; i < 10; ++i) { delete this.blocksCache[blocksCacheHashes[i]]; @@ -92,7 +112,7 @@ class FundingTxFetcher { this.fundingTxCache[channelId] = { timestamp: block.time, txid: block.tx[txIdx].txid, - value: block.tx[txIdx].vout[outputIdx].value, + value: block.tx[txIdx].value / 100000000 ?? block.tx[txIdx].vout[outputIdx].value, }; ++this.channelNewlyProcessed; diff --git a/backend/src/tasks/lightning/sync-tasks/stats-importer.ts b/backend/src/tasks/lightning/sync-tasks/stats-importer.ts index f6d70df7d..8482b558c 100644 --- a/backend/src/tasks/lightning/sync-tasks/stats-importer.ts +++ b/backend/src/tasks/lightning/sync-tasks/stats-importer.ts @@ -1,10 +1,12 @@ import DB from '../../../database'; -import { readdirSync, readFileSync } from 'fs'; +import { promises } from 'fs'; import { XMLParser } from 'fast-xml-parser'; import logger from '../../../logger'; import fundingTxFetcher from './funding-tx-fetcher'; import config from '../../../config'; +const fsPromises = promises; + interface Node { id: string; timestamp: number; @@ -33,14 +35,12 @@ class LightningStatsImporter { topologiesFolder = config.LIGHTNING.TOPOLOGY_FOLDER; parser = new XMLParser(); - latestNodeCount = 1; // Ignore gap in the data - async $run(): Promise { logger.info(`Importing historical lightning stats`); - // const [channels]: any[] = await DB.query('SELECT short_id from channels;'); - // logger.info('Caching funding txs for currently existing channels'); - // await fundingTxFetcher.$fetchChannelsFundingTxs(channels.map(channel => channel.short_id)); + const [channels]: any[] = await DB.query('SELECT short_id from channels;'); + logger.info('Caching funding txs for currently existing channels'); + await fundingTxFetcher.$fetchChannelsFundingTxs(channels.map(channel => channel.short_id)); await this.$importHistoricalLightningStats(); } @@ -148,6 +148,8 @@ class LightningStatsImporter { const capacities: number[] = []; const feeRates: number[] = []; const baseFees: number[] = []; + const alreadyCountedChannels = {}; + for (const channel of networkGraph.channels) { const tx = await fundingTxFetcher.$fetchChannelOpenTx(channel.scid.slice(0, -2)); if (!tx) { @@ -173,10 +175,14 @@ class LightningStatsImporter { nodeStats[channel.destination].capacity += Math.round(tx.value * 100000000); nodeStats[channel.destination].channels++; - capacity += Math.round(tx.value * 100000000); + if (!alreadyCountedChannels[channel.scid.slice(0, -2)]) { + capacity += Math.round(tx.value * 100000000); + capacities.push(Math.round(tx.value * 100000000)); + alreadyCountedChannels[channel.scid.slice(0, -2)] = true; + } + avgFeeRate += channel.fee_proportional_millionths; avgBaseFee += channel.fee_base_msat; - capacities.push(Math.round(tx.value * 100000000)); feeRates.push(channel.fee_proportional_millionths); baseFees.push(channel.fee_base_msat); } @@ -186,6 +192,7 @@ class LightningStatsImporter { const medCapacity = capacities.sort((a, b) => b - a)[Math.round(capacities.length / 2 - 1)]; const medFeeRate = feeRates.sort((a, b) => b - a)[Math.round(feeRates.length / 2 - 1)]; const medBaseFee = baseFees.sort((a, b) => b - a)[Math.round(baseFees.length / 2 - 1)]; + const avgCapacity = Math.round(capacity / capacities.length); let query = `INSERT INTO lightning_stats( added, @@ -207,14 +214,14 @@ class LightningStatsImporter { await DB.query(query, [ timestamp, - networkGraph.channels.length, + capacities.length, networkGraph.nodes.length, capacity, torNodes, clearnetNodes, unannouncedNodes, clearnetTorNodes, - Math.round(capacity / networkGraph.channels.length), + avgCapacity, avgFeeRate, avgBaseFee, medCapacity, @@ -241,10 +248,10 @@ class LightningStatsImporter { } async $importHistoricalLightningStats(): Promise { - const fileList = readdirSync(this.topologiesFolder); + const fileList = await fsPromises.readdir(this.topologiesFolder); fileList.sort().reverse(); - const [rows]: any[] = await DB.query('SELECT UNIX_TIMESTAMP(added) as added FROM lightning_stats'); + const [rows]: any[] = await DB.query('SELECT UNIX_TIMESTAMP(added) AS added FROM lightning_stats'); const existingStatsTimestamps = {}; for (const row of rows) { existingStatsTimestamps[row.added] = true; @@ -252,26 +259,30 @@ class LightningStatsImporter { for (const filename of fileList) { const timestamp = parseInt(filename.split('_')[1], 10); - const fileContent = readFileSync(`${this.topologiesFolder}/${filename}`, 'utf8'); - - const graph = this.parseFile(fileContent); - if (!graph) { - continue; - } - - // Ignore drop of more than 90% of the node count as it's probably a missing data point - const diffRatio = graph.nodes.length / this.latestNodeCount; - if (diffRatio < 0.90) { - continue; - } - this.latestNodeCount = graph.nodes.length; // Stats exist already, don't calculate/insert them - if (existingStatsTimestamps[timestamp] === true) { + if (existingStatsTimestamps[timestamp] !== undefined) { continue; } logger.debug(`Processing ${this.topologiesFolder}/${filename}`); + const fileContent = await fsPromises.readFile(`${this.topologiesFolder}/${filename}`, 'utf8'); + + let graph; + if (filename.indexOf('.json') !== -1) { + try { + graph = JSON.parse(fileContent); + } catch (e) { + logger.debug(`Invalid topology file, cannot parse the content`); + } + } else { + graph = this.parseFile(fileContent); + if (!graph) { + logger.debug(`Invalid topology file, cannot parse the content`); + continue; + } + await fsPromises.writeFile(`${this.topologiesFolder}/${filename}.json`, JSON.stringify(graph)); + } const datestr = `${new Date(timestamp * 1000).toUTCString()} (${timestamp})`; logger.debug(`${datestr}: Found ${graph.nodes.length} nodes and ${graph.channels.length} channels`); @@ -282,6 +293,8 @@ class LightningStatsImporter { logger.debug(`Generating LN network stats for ${datestr}`); await this.computeNetworkStats(timestamp, graph); + + existingStatsTimestamps[timestamp] = true; } logger.info(`Lightning network stats historical import completed`); From 5205d924862039bb2494972c70c51a7131ef07b4 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Tue, 2 Aug 2022 13:03:32 +0200 Subject: [PATCH 073/105] Remove buggy tx vout value fetching and improve performances --- .../sync-tasks/funding-tx-fetcher.ts | 27 ++++++------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/backend/src/tasks/lightning/sync-tasks/funding-tx-fetcher.ts b/backend/src/tasks/lightning/sync-tasks/funding-tx-fetcher.ts index 4068de8f1..9da721876 100644 --- a/backend/src/tasks/lightning/sync-tasks/funding-tx-fetcher.ts +++ b/backend/src/tasks/lightning/sync-tasks/funding-tx-fetcher.ts @@ -1,4 +1,5 @@ import { existsSync, promises } from 'fs'; +import bitcoinApiFactory from '../../../api/bitcoin/bitcoin-api-factory'; import bitcoinClient from '../../../api/bitcoin/bitcoin-client'; import config from '../../../config'; import DB from '../../../database'; @@ -79,26 +80,10 @@ class FundingTxFetcher { const outputIdx = parts[2]; let block = this.blocksCache[blockHeight]; - // Check if we have the block in the `blocks_summaries` table to avoid calling core - if (!block) { - const [rows] = await DB.query(` - SELECT UNIX_TIMESTAMP(blocks.blockTimestamp) AS time, blocks_summaries.transactions AS tx - FROM blocks_summaries - JOIN blocks ON blocks.hash = blocks_summaries.id - WHERE blocks_summaries.height = ${blockHeight} - `); - block = rows[0] ?? null; - if (block) { - block.tx = JSON.parse(block.tx); - if (block.tx.length === 0) { - block = null; - } - } - } // Fetch it from core if (!block) { const blockHash = await bitcoinClient.getBlockHash(parseInt(blockHeight, 10)); - block = await bitcoinClient.getBlock(blockHash, 2); + block = await bitcoinClient.getBlock(blockHash, 1); } this.blocksCache[block.height] = block; @@ -109,10 +94,14 @@ class FundingTxFetcher { } } + const txid = block.tx[txIdx]; + const rawTx = await bitcoinClient.getRawTransaction(txid); + const tx = await bitcoinClient.decodeRawTransaction(rawTx); + this.fundingTxCache[channelId] = { timestamp: block.time, - txid: block.tx[txIdx].txid, - value: block.tx[txIdx].value / 100000000 ?? block.tx[txIdx].vout[outputIdx].value, + txid: txid, + value: tx.vout[outputIdx].value, }; ++this.channelNewlyProcessed; From 2bddd97e856f7c214a27cd4c39012bf986e3a2f6 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Tue, 2 Aug 2022 15:02:24 +0200 Subject: [PATCH 074/105] Reduce massive gaps in the imported historical LN data --- .../tasks/lightning/sync-tasks/stats-importer.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/backend/src/tasks/lightning/sync-tasks/stats-importer.ts b/backend/src/tasks/lightning/sync-tasks/stats-importer.ts index 8482b558c..4f7c5ca04 100644 --- a/backend/src/tasks/lightning/sync-tasks/stats-importer.ts +++ b/backend/src/tasks/lightning/sync-tasks/stats-importer.ts @@ -248,6 +248,8 @@ class LightningStatsImporter { } async $importHistoricalLightningStats(): Promise { + let latestNodeCount = 1; + const fileList = await fsPromises.readdir(this.topologiesFolder); fileList.sort().reverse(); @@ -284,6 +286,17 @@ class LightningStatsImporter { await fsPromises.writeFile(`${this.topologiesFolder}/${filename}.json`, JSON.stringify(graph)); } + if (timestamp > 1556316000) { + // "No, the reason most likely is just that I started collection in 2019, + // so what I had before that is just the survivors from before, which weren't that many" + const diffRatio = graph.nodes.length / latestNodeCount; + if (diffRatio < 0.9) { + // Ignore drop of more than 90% of the node count as it's probably a missing data point + continue; + } + } + latestNodeCount = graph.nodes.length; + const datestr = `${new Date(timestamp * 1000).toUTCString()} (${timestamp})`; logger.debug(`${datestr}: Found ${graph.nodes.length} nodes and ${graph.channels.length} channels`); From 71abbae05da3f312b05b351dedc903f28e47559d Mon Sep 17 00:00:00 2001 From: nymkappa Date: Tue, 2 Aug 2022 15:58:29 +0200 Subject: [PATCH 075/105] Small cleanup --- .../src/tasks/lightning/sync-tasks/stats-importer.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/backend/src/tasks/lightning/sync-tasks/stats-importer.ts b/backend/src/tasks/lightning/sync-tasks/stats-importer.ts index 4f7c5ca04..5c6a6c5a2 100644 --- a/backend/src/tasks/lightning/sync-tasks/stats-importer.ts +++ b/backend/src/tasks/lightning/sync-tasks/stats-importer.ts @@ -151,9 +151,11 @@ class LightningStatsImporter { const alreadyCountedChannels = {}; for (const channel of networkGraph.channels) { - const tx = await fundingTxFetcher.$fetchChannelOpenTx(channel.scid.slice(0, -2)); + const short_id = channel.scid.slice(0, -2); + + const tx = await fundingTxFetcher.$fetchChannelOpenTx(short_id); if (!tx) { - logger.err(`Unable to fetch funding tx for channel ${channel.scid}. Capacity and creation date will stay unknown.`); + logger.err(`Unable to fetch funding tx for channel ${short_id}. Capacity and creation date is unknown. Skipping channel.`); continue; } @@ -175,10 +177,10 @@ class LightningStatsImporter { nodeStats[channel.destination].capacity += Math.round(tx.value * 100000000); nodeStats[channel.destination].channels++; - if (!alreadyCountedChannels[channel.scid.slice(0, -2)]) { + if (!alreadyCountedChannels[short_id]) { capacity += Math.round(tx.value * 100000000); capacities.push(Math.round(tx.value * 100000000)); - alreadyCountedChannels[channel.scid.slice(0, -2)] = true; + alreadyCountedChannels[short_id] = true; } avgFeeRate += channel.fee_proportional_millionths; From b630816b2ed0aeff545803f7172723f0f76a9abd Mon Sep 17 00:00:00 2001 From: nymkappa Date: Tue, 2 Aug 2022 17:56:46 +0200 Subject: [PATCH 076/105] Don't insert gapped gossip data upon restart --- backend/src/tasks/lightning/sync-tasks/stats-importer.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/src/tasks/lightning/sync-tasks/stats-importer.ts b/backend/src/tasks/lightning/sync-tasks/stats-importer.ts index 5c6a6c5a2..f99529e02 100644 --- a/backend/src/tasks/lightning/sync-tasks/stats-importer.ts +++ b/backend/src/tasks/lightning/sync-tasks/stats-importer.ts @@ -255,10 +255,10 @@ class LightningStatsImporter { const fileList = await fsPromises.readdir(this.topologiesFolder); fileList.sort().reverse(); - const [rows]: any[] = await DB.query('SELECT UNIX_TIMESTAMP(added) AS added FROM lightning_stats'); + const [rows]: any[] = await DB.query('SELECT UNIX_TIMESTAMP(added) as added, node_count FROM lightning_stats'); const existingStatsTimestamps = {}; for (const row of rows) { - existingStatsTimestamps[row.added] = true; + existingStatsTimestamps[row.added] = rows[0]; } for (const filename of fileList) { @@ -266,6 +266,7 @@ class LightningStatsImporter { // Stats exist already, don't calculate/insert them if (existingStatsTimestamps[timestamp] !== undefined) { + latestNodeCount = existingStatsTimestamps[timestamp].node_count; continue; } From 822a677624d8ae279f981dd7bdd0ac9d1b03cd8c Mon Sep 17 00:00:00 2001 From: nymkappa Date: Tue, 2 Aug 2022 18:15:34 +0200 Subject: [PATCH 077/105] Ignore channels fee rate > 5000ppm or base fee > 5000 in stats --- .../lightning/sync-tasks/stats-importer.ts | 144 ++++++++++-------- 1 file changed, 77 insertions(+), 67 deletions(-) diff --git a/backend/src/tasks/lightning/sync-tasks/stats-importer.ts b/backend/src/tasks/lightning/sync-tasks/stats-importer.ts index f99529e02..91e67f77d 100644 --- a/backend/src/tasks/lightning/sync-tasks/stats-importer.ts +++ b/backend/src/tasks/lightning/sync-tasks/stats-importer.ts @@ -45,70 +45,10 @@ class LightningStatsImporter { await this.$importHistoricalLightningStats(); } - /** - * Parse the file content into XML, and return a list of nodes and channels - */ - parseFile(fileContent): any { - const graph = this.parser.parse(fileContent); - if (Object.keys(graph).length === 0) { - return null; - } - - const nodes: Node[] = []; - const channels: Channel[] = []; - - // If there is only one entry, the parser does not return an array, so we override this - if (!Array.isArray(graph.graphml.graph.node)) { - graph.graphml.graph.node = [graph.graphml.graph.node]; - } - if (!Array.isArray(graph.graphml.graph.edge)) { - graph.graphml.graph.edge = [graph.graphml.graph.edge]; - } - - for (const node of graph.graphml.graph.node) { - if (!node.data) { - continue; - } - nodes.push({ - id: node.data[0], - timestamp: node.data[1], - features: node.data[2], - rgb_color: node.data[3], - alias: node.data[4], - addresses: node.data[5], - out_degree: node.data[6], - in_degree: node.data[7], - }); - } - - for (const channel of graph.graphml.graph.edge) { - if (!channel.data) { - continue; - } - channels.push({ - scid: channel.data[0], - source: channel.data[1], - destination: channel.data[2], - timestamp: channel.data[3], - features: channel.data[4], - fee_base_msat: channel.data[5], - fee_proportional_millionths: channel.data[6], - htlc_minimim_msat: channel.data[7], - cltv_expiry_delta: channel.data[8], - htlc_maximum_msat: channel.data[9], - }); - } - - return { - nodes: nodes, - channels: channels, - }; - } - /** * Generate LN network stats for one day */ - public async computeNetworkStats(timestamp: number, networkGraph): Promise { + public async computeNetworkStats(timestamp: number, networkGraph): Promise { // Node counts and network shares let clearnetNodes = 0; let torNodes = 0; @@ -183,10 +123,15 @@ class LightningStatsImporter { alreadyCountedChannels[short_id] = true; } - avgFeeRate += channel.fee_proportional_millionths; - avgBaseFee += channel.fee_base_msat; - feeRates.push(channel.fee_proportional_millionths); - baseFees.push(channel.fee_base_msat); + if (channel.fee_proportional_millionths < 5000) { + avgFeeRate += channel.fee_proportional_millionths; + feeRates.push(channel.fee_proportional_millionths); + } + + if (channel.fee_base_msat < 5000) { + avgBaseFee += channel.fee_base_msat; + baseFees.push(channel.fee_base_msat); + } } avgFeeRate /= networkGraph.channels.length; @@ -247,6 +192,11 @@ class LightningStatsImporter { nodeStats[public_key].channels, ]); } + + return { + added: timestamp, + node_count: networkGraph.nodes.length + }; } async $importHistoricalLightningStats(): Promise { @@ -308,13 +258,73 @@ class LightningStatsImporter { await fundingTxFetcher.$fetchChannelsFundingTxs(graph.channels.map(channel => channel.scid.slice(0, -2))); logger.debug(`Generating LN network stats for ${datestr}`); - await this.computeNetworkStats(timestamp, graph); + const stat = await this.computeNetworkStats(timestamp, graph); - existingStatsTimestamps[timestamp] = true; + existingStatsTimestamps[timestamp] = stat; } logger.info(`Lightning network stats historical import completed`); } + + /** + * Parse the file content into XML, and return a list of nodes and channels + */ + private parseFile(fileContent): any { + const graph = this.parser.parse(fileContent); + if (Object.keys(graph).length === 0) { + return null; + } + + const nodes: Node[] = []; + const channels: Channel[] = []; + + // If there is only one entry, the parser does not return an array, so we override this + if (!Array.isArray(graph.graphml.graph.node)) { + graph.graphml.graph.node = [graph.graphml.graph.node]; + } + if (!Array.isArray(graph.graphml.graph.edge)) { + graph.graphml.graph.edge = [graph.graphml.graph.edge]; + } + + for (const node of graph.graphml.graph.node) { + if (!node.data) { + continue; + } + nodes.push({ + id: node.data[0], + timestamp: node.data[1], + features: node.data[2], + rgb_color: node.data[3], + alias: node.data[4], + addresses: node.data[5], + out_degree: node.data[6], + in_degree: node.data[7], + }); + } + + for (const channel of graph.graphml.graph.edge) { + if (!channel.data) { + continue; + } + channels.push({ + scid: channel.data[0], + source: channel.data[1], + destination: channel.data[2], + timestamp: channel.data[3], + features: channel.data[4], + fee_base_msat: channel.data[5], + fee_proportional_millionths: channel.data[6], + htlc_minimim_msat: channel.data[7], + cltv_expiry_delta: channel.data[8], + htlc_maximum_msat: channel.data[9], + }); + } + + return { + nodes: nodes, + channels: channels, + }; + } } export default new LightningStatsImporter; \ No newline at end of file From 2a43452f9e10eb188b71515bacbed0c8d5d5a8ee Mon Sep 17 00:00:00 2001 From: nymkappa Date: Tue, 2 Aug 2022 18:26:07 +0200 Subject: [PATCH 078/105] Rewrite queries to get top nodes by channels and capacity --- backend/src/api/explorer/nodes.api.ts | 40 +++++++++++++++------------ 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/backend/src/api/explorer/nodes.api.ts b/backend/src/api/explorer/nodes.api.ts index 4c7028136..96da7d1d5 100644 --- a/backend/src/api/explorer/nodes.api.ts +++ b/backend/src/api/explorer/nodes.api.ts @@ -66,16 +66,19 @@ class NodesApi { public async $getTopCapacityNodes(): Promise { try { + let [rows]: any[] = await DB.query('SELECT UNIX_TIMESTAMP(MAX(added)) as maxAdded FROM node_stats'); + const latestDate = rows[0].maxAdded; + const query = ` - SELECT IF(nodes.alias = '', SUBSTRING(nodes.public_key, 1, 20), alias) as alias, nodes.public_key, - CAST(COALESCE(node_stats.capacity, 0) as INT) as capacity, - CAST(COALESCE(node_stats.channels, 0) as INT) as channels - FROM nodes - LEFT JOIN node_stats ON node_stats.public_key = nodes.public_key - ORDER BY node_stats.added DESC, node_stats.capacity DESC - LIMIT 10 + SELECT nodes.public_key, nodes.alias, node_stats.capacity, node_stats.channels + FROM node_stats + JOIN nodes ON nodes.public_key = node_stats.public_key + WHERE added = FROM_UNIXTIME(${latestDate}) + ORDER BY capacity DESC + LIMIT 10; `; - const [rows]: any = await DB.query(query); + [rows] = await DB.query(query); + return rows; } catch (e) { logger.err('$getTopCapacityNodes error: ' + (e instanceof Error ? e.message : e)); @@ -85,16 +88,19 @@ class NodesApi { public async $getTopChannelsNodes(): Promise { try { + let [rows]: any[] = await DB.query('SELECT UNIX_TIMESTAMP(MAX(added)) as maxAdded FROM node_stats'); + const latestDate = rows[0].maxAdded; + const query = ` - SELECT IF(nodes.alias = '', SUBSTRING(nodes.public_key, 1, 20), alias) as alias, nodes.public_key, - CAST(COALESCE(node_stats.capacity, 0) as INT) as capacity, - CAST(COALESCE(node_stats.channels, 0) as INT) as channels - FROM nodes - LEFT JOIN node_stats - ON node_stats.public_key = nodes.public_key - ORDER BY node_stats.added DESC, node_stats.channels DESC - LIMIT 10`; - const [rows]: any = await DB.query(query); + SELECT nodes.public_key, nodes.alias, node_stats.capacity, node_stats.channels + FROM node_stats + JOIN nodes ON nodes.public_key = node_stats.public_key + WHERE added = FROM_UNIXTIME(${latestDate}) + ORDER BY channels DESC + LIMIT 10; + `; + [rows] = await DB.query(query); + return rows; } catch (e) { logger.err('$getTopChannelsNodes error: ' + (e instanceof Error ? e.message : e)); From fefed3c88890d6cf26ac7f302a1b5c2534e1b4db Mon Sep 17 00:00:00 2001 From: nymkappa Date: Fri, 29 Jul 2022 08:08:22 +0200 Subject: [PATCH 079/105] Create CLightningClient class --- backend/src/rpc-api/core-lightning/jsonrpc.ts | 249 ++++++++++++++++++ 1 file changed, 249 insertions(+) create mode 100644 backend/src/rpc-api/core-lightning/jsonrpc.ts diff --git a/backend/src/rpc-api/core-lightning/jsonrpc.ts b/backend/src/rpc-api/core-lightning/jsonrpc.ts new file mode 100644 index 000000000..037dfff75 --- /dev/null +++ b/backend/src/rpc-api/core-lightning/jsonrpc.ts @@ -0,0 +1,249 @@ +'use strict'; + +const methods = [ + 'addgossip', + 'autocleaninvoice', + 'check', + 'checkmessage', + 'close', + 'connect', + 'createinvoice', + 'createinvoicerequest', + 'createoffer', + 'createonion', + 'decode', + 'decodepay', + 'delexpiredinvoice', + 'delinvoice', + 'delpay', + 'dev-listaddrs', + 'dev-rescan-outputs', + 'disableoffer', + 'disconnect', + 'estimatefees', + 'feerates', + 'fetchinvoice', + 'fundchannel', + 'fundchannel_cancel', + 'fundchannel_complete', + 'fundchannel_start', + 'fundpsbt', + 'getchaininfo', + 'getinfo', + 'getlog', + 'getrawblockbyheight', + 'getroute', + 'getsharedsecret', + 'getutxout', + 'help', + 'invoice', + 'keysend', + 'legacypay', + 'listchannels', + 'listconfigs', + 'listforwards', + 'listfunds', + 'listinvoices', + 'listnodes', + 'listoffers', + 'listpays', + 'listpeers', + 'listsendpays', + 'listtransactions', + 'multifundchannel', + 'multiwithdraw', + 'newaddr', + 'notifications', + 'offer', + 'offerout', + 'openchannel_abort', + 'openchannel_bump', + 'openchannel_init', + 'openchannel_signed', + 'openchannel_update', + 'pay', + 'payersign', + 'paystatus', + 'ping', + 'plugin', + 'reserveinputs', + 'sendinvoice', + 'sendonion', + 'sendonionmessage', + 'sendpay', + 'sendpsbt', + 'sendrawtransaction', + 'setchannelfee', + 'signmessage', + 'signpsbt', + 'stop', + 'txdiscard', + 'txprepare', + 'txsend', + 'unreserveinputs', + 'utxopsbt', + 'waitanyinvoice', + 'waitblockheight', + 'waitinvoice', + 'waitsendpay', + 'withdraw' +]; + + +import EventEmitter from 'events'; +import { existsSync, statSync } from 'fs'; +import { createConnection, Socket } from 'net'; +import { homedir } from 'os'; +import path from 'path'; +import { createInterface, Interface } from 'readline'; +import logger from '../../logger'; + +class LightningError extends Error { + type: string = 'lightning'; + message: string = 'lightning-client error'; + + constructor(error) { + super(); + this.type = error.type; + this.message = error.message; + } +} + +const defaultRpcPath = path.join(homedir(), '.lightning') + , fStat = (...p) => statSync(path.join(...p)) + , fExists = (...p) => existsSync(path.join(...p)) + +class CLightningClient extends EventEmitter { + private rpcPath: string; + private reconnectWait: number; + private reconnectTimeout; + private reqcount: number; + private client: Socket; + private rl: Interface; + private clientConnectionPromise: Promise; + + constructor(rpcPath = defaultRpcPath) { + if (!path.isAbsolute(rpcPath)) { + throw new Error('The rpcPath must be an absolute path'); + } + + if (!fExists(rpcPath) || !fStat(rpcPath).isSocket()) { + // network directory provided, use the lightning-rpc within in + if (fExists(rpcPath, 'lightning-rpc')) { + rpcPath = path.join(rpcPath, 'lightning-rpc'); + } + + // main data directory provided, default to using the bitcoin mainnet subdirectory + // to be removed in v0.2.0 + else if (fExists(rpcPath, 'bitcoin', 'lightning-rpc')) { + logger.warn(`[CLightningClient] ${rpcPath}/lightning-rpc is missing, using the bitcoin mainnet subdirectory at ${rpcPath}/bitcoin instead.`) + logger.warn(`[CLightningClient] specifying the main lightning data directory is deprecated, please specify the network directory explicitly.\n`) + rpcPath = path.join(rpcPath, 'bitcoin', 'lightning-rpc') + } + } + + logger.debug(`[CLightningClient] Connecting to ${rpcPath}`); + + super(); + this.rpcPath = rpcPath; + this.reconnectWait = 0.5; + this.reconnectTimeout = null; + this.reqcount = 0; + + const _self = this; + + this.client = createConnection(rpcPath); + this.rl = createInterface({ input: this.client }) + + this.clientConnectionPromise = new Promise(resolve => { + _self.client.on('connect', () => { + logger.debug(`[CLightningClient] Lightning client connected`); + _self.reconnectWait = 1; + resolve(); + }); + + _self.client.on('end', () => { + logger.err('[CLightningClient] Lightning client connection closed, reconnecting'); + _self.increaseWaitTime(); + _self.reconnect(); + }); + + _self.client.on('error', error => { + logger.err(`[CLightningClient] Lightning client connection error: ${error}`); + _self.emit('error', error); + _self.increaseWaitTime(); + _self.reconnect(); + }); + }); + + this.rl.on('line', line => { + line = line.trim(); + if (!line) { + return; + } + const data = JSON.parse(line); + logger.debug(`[CLightningClient] #${data.id} <-- ${JSON.stringify(data.error || data.result)}`); + _self.emit('res:' + data.id, data); + }); + } + + increaseWaitTime(): void { + if (this.reconnectWait >= 16) { + this.reconnectWait = 16; + } else { + this.reconnectWait *= 2; + } + } + + reconnect(): void { + const _self = this; + + if (this.reconnectTimeout) { + return; + } + + this.reconnectTimeout = setTimeout(() => { + logger.debug('[CLightningClient] Trying to reconnect...'); + + _self.client.connect(_self.rpcPath); + _self.reconnectTimeout = null; + }, this.reconnectWait * 1000); + } + + call(method, args = []): Promise { + const _self = this; + + const callInt = ++this.reqcount; + const sendObj = { + jsonrpc: '2.0', + method, + params: args, + id: '' + callInt + }; + + logger.debug(`[CLightningClient] #${callInt} --> ${method} ${args}`); + + // Wait for the client to connect + return this.clientConnectionPromise + .then(() => new Promise((resolve, reject) => { + // Wait for a response + this.once('res:' + callInt, res => res.error == null + ? resolve(res.result) + : reject(new LightningError(res.error)) + ); + + // Send the command + _self.client.write(JSON.stringify(sendObj)); + })); + } +} + +const protify = s => s.replace(/-([a-z])/g, m => m[1].toUpperCase()); + +methods.forEach(k => { + CLightningClient.prototype[protify(k)] = function (...args: any) { + return this.call(k, args); + }; +}); + +export default new CLightningClient(); From ae006579a642b5e93fd44444406a64521249b628 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Fri, 29 Jul 2022 16:33:07 +0200 Subject: [PATCH 080/105] Wrote some utility functions to convert clightning output to our db schema --- .../lightning/clightning/clightning-client.ts | 4 + .../clightning/clightning-convert.ts | 95 +++++++++++++++++++ .../lightning/clightning}/jsonrpc.ts | 12 +-- backend/src/config.ts | 8 ++ 4 files changed, 113 insertions(+), 6 deletions(-) create mode 100644 backend/src/api/lightning/clightning/clightning-client.ts create mode 100644 backend/src/api/lightning/clightning/clightning-convert.ts rename backend/src/{rpc-api/core-lightning => api/lightning/clightning}/jsonrpc.ts (94%) diff --git a/backend/src/api/lightning/clightning/clightning-client.ts b/backend/src/api/lightning/clightning/clightning-client.ts new file mode 100644 index 000000000..2b974bca0 --- /dev/null +++ b/backend/src/api/lightning/clightning/clightning-client.ts @@ -0,0 +1,4 @@ +import config from '../../../config'; +import CLightningClient from './jsonrpc'; + +export default new CLightningClient(config.CLIGHTNING.SOCKET); diff --git a/backend/src/api/lightning/clightning/clightning-convert.ts b/backend/src/api/lightning/clightning/clightning-convert.ts new file mode 100644 index 000000000..34ef6f942 --- /dev/null +++ b/backend/src/api/lightning/clightning/clightning-convert.ts @@ -0,0 +1,95 @@ +import logger from "../../../logger"; +import { ILightningApi } from "../lightning-api.interface"; + +export function convertNode(clNode: any): ILightningApi.Node { + return { + alias: clNode.alias ?? '', + color: `#${clNode.color ?? ''}`, + features: [], // TODO parse and return clNode.feature + public_key: clNode.nodeid, + sockets: clNode.addresses?.map(addr => `${addr.address}:${addr.port}`) ?? [], + updated_at: new Date((clNode?.last_timestamp ?? 0) * 1000).toUTCString(), + }; +} + +export function convertAndmergeBidirectionalChannels(clChannels: any[]): ILightningApi.Channel[] { + const consolidatedChannelList: ILightningApi.Channel[] = []; + const clChannelsDict = {}; + const clChannelsDictCount = {}; + + for (const clChannel of clChannels) { + if (!clChannelsDict[clChannel.short_channel_id]) { + clChannelsDict[clChannel.short_channel_id] = clChannel; + clChannelsDictCount[clChannel.short_channel_id] = 1; + } else { + consolidatedChannelList.push( + buildBidirectionalChannel(clChannel, clChannelsDict[clChannel.short_channel_id]) + ); + delete clChannelsDict[clChannel.short_channel_id]; + clChannelsDictCount[clChannel.short_channel_id]++; + } + } + const bidirectionalChannelsCount = consolidatedChannelList.length; + + for (const short_channel_id of Object.keys(clChannelsDict)) { + consolidatedChannelList.push(buildUnidirectionalChannel(clChannelsDict[short_channel_id])); + } + const unidirectionalChannelsCount = consolidatedChannelList.length - bidirectionalChannelsCount; + + logger.debug(`clightning knows ${clChannels.length} channels. ` + + `We found ${bidirectionalChannelsCount} bidirectional channels ` + + `and ${unidirectionalChannelsCount} unidirectional channels.`); + + return consolidatedChannelList; +} + +function buildBidirectionalChannel(clChannelA: any, clChannelB: any): ILightningApi.Channel { + const lastUpdate = Math.max(clChannelA.last_update ?? 0, clChannelB.last_update ?? 0); + + return { + id: clChannelA.short_channel_id, + capacity: clChannelA.satoshis, + transaction_id: '', // TODO + transaction_vout: 0, // TODO + updated_at: new Date(lastUpdate * 1000).toUTCString(), + policies: [ + convertPolicy(clChannelA), + convertPolicy(clChannelB) + ] + }; +} + +function buildUnidirectionalChannel(clChannel: any): ILightningApi.Channel { + return { + id: clChannel.short_channel_id, + capacity: clChannel.satoshis, + policies: [convertPolicy(clChannel), getEmptyPolicy()], + transaction_id: '', // TODO + transaction_vout: 0, // TODO + updated_at: new Date((clChannel.last_update ?? 0) * 1000).toUTCString(), + }; +} + +function convertPolicy(clChannel: any): ILightningApi.Policy { + return { + public_key: clChannel.source, + base_fee_mtokens: clChannel.base_fee_millisatoshi, + fee_rate: clChannel.fee_per_millionth, + is_disabled: !clChannel.active, + max_htlc_mtokens: clChannel.htlc_maximum_msat.slice(0, -4), + min_htlc_mtokens: clChannel.htlc_minimum_msat.slice(0, -4), + updated_at: new Date((clChannel.last_update ?? 0) * 1000).toUTCString(), + }; +} + +function getEmptyPolicy(): ILightningApi.Policy { + return { + public_key: 'null', + base_fee_mtokens: '0', + fee_rate: 0, + is_disabled: true, + max_htlc_mtokens: '0', + min_htlc_mtokens: '0', + updated_at: new Date(0).toUTCString(), + }; +} diff --git a/backend/src/rpc-api/core-lightning/jsonrpc.ts b/backend/src/api/lightning/clightning/jsonrpc.ts similarity index 94% rename from backend/src/rpc-api/core-lightning/jsonrpc.ts rename to backend/src/api/lightning/clightning/jsonrpc.ts index 037dfff75..d0b187a54 100644 --- a/backend/src/rpc-api/core-lightning/jsonrpc.ts +++ b/backend/src/api/lightning/clightning/jsonrpc.ts @@ -1,3 +1,5 @@ +// Imported from https://github.com/shesek/lightning-client-js + 'use strict'; const methods = [ @@ -96,7 +98,7 @@ import { createConnection, Socket } from 'net'; import { homedir } from 'os'; import path from 'path'; import { createInterface, Interface } from 'readline'; -import logger from '../../logger'; +import logger from '../../../logger'; class LightningError extends Error { type: string = 'lightning'; @@ -113,7 +115,7 @@ const defaultRpcPath = path.join(homedir(), '.lightning') , fStat = (...p) => statSync(path.join(...p)) , fExists = (...p) => existsSync(path.join(...p)) -class CLightningClient extends EventEmitter { +export default class CLightningClient extends EventEmitter { private rpcPath: string; private reconnectWait: number; private reconnectTimeout; @@ -182,7 +184,7 @@ class CLightningClient extends EventEmitter { return; } const data = JSON.parse(line); - logger.debug(`[CLightningClient] #${data.id} <-- ${JSON.stringify(data.error || data.result)}`); + // logger.debug(`[CLightningClient] #${data.id} <-- ${JSON.stringify(data.error || data.result)}`); _self.emit('res:' + data.id, data); }); } @@ -210,7 +212,7 @@ class CLightningClient extends EventEmitter { }, this.reconnectWait * 1000); } - call(method, args = []): Promise { + call(method, args = []): Promise { const _self = this; const callInt = ++this.reqcount; @@ -245,5 +247,3 @@ methods.forEach(k => { return this.call(k, args); }; }); - -export default new CLightningClient(); diff --git a/backend/src/config.ts b/backend/src/config.ts index d480e6c51..b42a45ab2 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -38,6 +38,9 @@ interface IConfig { MACAROON_PATH: string; REST_API_URL: string; }; + CLIGHTNING: { + SOCKET: string; + }; ELECTRUM: { HOST: string; PORT: number; @@ -186,6 +189,9 @@ const defaults: IConfig = { 'MACAROON_PATH': '', 'REST_API_URL': 'https://localhost:8080', }, + 'CLIGHTNING': { + 'SOCKET': '', + }, 'SOCKS5PROXY': { 'ENABLED': false, 'USE_ONION': true, @@ -226,6 +232,7 @@ class Config implements IConfig { BISQ: IConfig['BISQ']; LIGHTNING: IConfig['LIGHTNING']; LND: IConfig['LND']; + CLIGHTNING: IConfig['CLIGHTNING']; SOCKS5PROXY: IConfig['SOCKS5PROXY']; PRICE_DATA_SERVER: IConfig['PRICE_DATA_SERVER']; EXTERNAL_DATA_SERVER: IConfig['EXTERNAL_DATA_SERVER']; @@ -244,6 +251,7 @@ class Config implements IConfig { this.BISQ = configs.BISQ; this.LIGHTNING = configs.LIGHTNING; this.LND = configs.LND; + this.CLIGHTNING = configs.CLIGHTNING; this.SOCKS5PROXY = configs.SOCKS5PROXY; this.PRICE_DATA_SERVER = configs.PRICE_DATA_SERVER; this.EXTERNAL_DATA_SERVER = configs.EXTERNAL_DATA_SERVER; From 1871111b4ebeb05399ba2bffd07d50723d676f68 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Fri, 29 Jul 2022 17:41:09 +0200 Subject: [PATCH 081/105] Delete historical generation code --- .../lightning/clightning/clightning-client.ts | 265 +++++++++++++++++- .../clightning/clightning-convert.ts | 44 +-- .../src/api/lightning/clightning/jsonrpc.ts | 249 ---------------- .../lightning-api-abstract-factory.ts | 2 - .../api/lightning/lightning-api-factory.ts | 5 +- .../src/tasks/lightning/node-sync.service.ts | 2 +- 6 files changed, 295 insertions(+), 272 deletions(-) delete mode 100644 backend/src/api/lightning/clightning/jsonrpc.ts diff --git a/backend/src/api/lightning/clightning/clightning-client.ts b/backend/src/api/lightning/clightning/clightning-client.ts index 2b974bca0..629092d03 100644 --- a/backend/src/api/lightning/clightning/clightning-client.ts +++ b/backend/src/api/lightning/clightning/clightning-client.ts @@ -1,4 +1,263 @@ -import config from '../../../config'; -import CLightningClient from './jsonrpc'; +// Imported from https://github.com/shesek/lightning-client-js -export default new CLightningClient(config.CLIGHTNING.SOCKET); +'use strict'; + +const methods = [ + 'addgossip', + 'autocleaninvoice', + 'check', + 'checkmessage', + 'close', + 'connect', + 'createinvoice', + 'createinvoicerequest', + 'createoffer', + 'createonion', + 'decode', + 'decodepay', + 'delexpiredinvoice', + 'delinvoice', + 'delpay', + 'dev-listaddrs', + 'dev-rescan-outputs', + 'disableoffer', + 'disconnect', + 'estimatefees', + 'feerates', + 'fetchinvoice', + 'fundchannel', + 'fundchannel_cancel', + 'fundchannel_complete', + 'fundchannel_start', + 'fundpsbt', + 'getchaininfo', + 'getinfo', + 'getlog', + 'getrawblockbyheight', + 'getroute', + 'getsharedsecret', + 'getutxout', + 'help', + 'invoice', + 'keysend', + 'legacypay', + 'listchannels', + 'listconfigs', + 'listforwards', + 'listfunds', + 'listinvoices', + 'listnodes', + 'listoffers', + 'listpays', + 'listpeers', + 'listsendpays', + 'listtransactions', + 'multifundchannel', + 'multiwithdraw', + 'newaddr', + 'notifications', + 'offer', + 'offerout', + 'openchannel_abort', + 'openchannel_bump', + 'openchannel_init', + 'openchannel_signed', + 'openchannel_update', + 'pay', + 'payersign', + 'paystatus', + 'ping', + 'plugin', + 'reserveinputs', + 'sendinvoice', + 'sendonion', + 'sendonionmessage', + 'sendpay', + 'sendpsbt', + 'sendrawtransaction', + 'setchannelfee', + 'signmessage', + 'signpsbt', + 'stop', + 'txdiscard', + 'txprepare', + 'txsend', + 'unreserveinputs', + 'utxopsbt', + 'waitanyinvoice', + 'waitblockheight', + 'waitinvoice', + 'waitsendpay', + 'withdraw' +]; + + +import EventEmitter from 'events'; +import { existsSync, statSync } from 'fs'; +import { createConnection, Socket } from 'net'; +import { homedir } from 'os'; +import path from 'path'; +import { createInterface, Interface } from 'readline'; +import logger from '../../../logger'; +import { AbstractLightningApi } from '../lightning-api-abstract-factory'; +import { ILightningApi } from '../lightning-api.interface'; +import { convertAndmergeBidirectionalChannels, convertNode } from './clightning-convert'; + +class LightningError extends Error { + type: string = 'lightning'; + message: string = 'lightning-client error'; + + constructor(error) { + super(); + this.type = error.type; + this.message = error.message; + } +} + +const defaultRpcPath = path.join(homedir(), '.lightning') + , fStat = (...p) => statSync(path.join(...p)) + , fExists = (...p) => existsSync(path.join(...p)) + +export default class CLightningClient extends EventEmitter implements AbstractLightningApi { + private rpcPath: string; + private reconnectWait: number; + private reconnectTimeout; + private reqcount: number; + private client: Socket; + private rl: Interface; + private clientConnectionPromise: Promise; + + constructor(rpcPath = defaultRpcPath) { + if (!path.isAbsolute(rpcPath)) { + throw new Error('The rpcPath must be an absolute path'); + } + + if (!fExists(rpcPath) || !fStat(rpcPath).isSocket()) { + // network directory provided, use the lightning-rpc within in + if (fExists(rpcPath, 'lightning-rpc')) { + rpcPath = path.join(rpcPath, 'lightning-rpc'); + } + + // main data directory provided, default to using the bitcoin mainnet subdirectory + // to be removed in v0.2.0 + else if (fExists(rpcPath, 'bitcoin', 'lightning-rpc')) { + logger.warn(`[CLightningClient] ${rpcPath}/lightning-rpc is missing, using the bitcoin mainnet subdirectory at ${rpcPath}/bitcoin instead.`) + logger.warn(`[CLightningClient] specifying the main lightning data directory is deprecated, please specify the network directory explicitly.\n`) + rpcPath = path.join(rpcPath, 'bitcoin', 'lightning-rpc') + } + } + + logger.debug(`[CLightningClient] Connecting to ${rpcPath}`); + + super(); + this.rpcPath = rpcPath; + this.reconnectWait = 0.5; + this.reconnectTimeout = null; + this.reqcount = 0; + + const _self = this; + + this.client = createConnection(rpcPath); + this.rl = createInterface({ input: this.client }) + + this.clientConnectionPromise = new Promise(resolve => { + _self.client.on('connect', () => { + logger.info(`[CLightningClient] Lightning client connected`); + _self.reconnectWait = 1; + resolve(); + }); + + _self.client.on('end', () => { + logger.err('[CLightningClient] Lightning client connection closed, reconnecting'); + _self.increaseWaitTime(); + _self.reconnect(); + }); + + _self.client.on('error', error => { + logger.err(`[CLightningClient] Lightning client connection error: ${error}`); + _self.emit('error', error); + _self.increaseWaitTime(); + _self.reconnect(); + }); + }); + + this.rl.on('line', line => { + line = line.trim(); + if (!line) { + return; + } + const data = JSON.parse(line); + // logger.debug(`[CLightningClient] #${data.id} <-- ${JSON.stringify(data.error || data.result)}`); + _self.emit('res:' + data.id, data); + }); + } + + increaseWaitTime(): void { + if (this.reconnectWait >= 16) { + this.reconnectWait = 16; + } else { + this.reconnectWait *= 2; + } + } + + reconnect(): void { + const _self = this; + + if (this.reconnectTimeout) { + return; + } + + this.reconnectTimeout = setTimeout(() => { + logger.debug('[CLightningClient] Trying to reconnect...'); + + _self.client.connect(_self.rpcPath); + _self.reconnectTimeout = null; + }, this.reconnectWait * 1000); + } + + call(method, args = []): Promise { + const _self = this; + + const callInt = ++this.reqcount; + const sendObj = { + jsonrpc: '2.0', + method, + params: args, + id: '' + callInt + }; + + // logger.debug(`[CLightningClient] #${callInt} --> ${method} ${args}`); + + // Wait for the client to connect + return this.clientConnectionPromise + .then(() => new Promise((resolve, reject) => { + // Wait for a response + this.once('res:' + callInt, res => res.error == null + ? resolve(res.result) + : reject(new LightningError(res.error)) + ); + + // Send the command + _self.client.write(JSON.stringify(sendObj)); + })); + } + + async $getNetworkGraph(): Promise { + const listnodes: any[] = await this.call('listnodes'); + const listchannels: any[] = await this.call('listchannels'); + const channelsList = convertAndmergeBidirectionalChannels(listchannels['channels']); + + return { + nodes: listnodes['nodes'].map(node => convertNode(node)), + channels: channelsList, + }; + } +} + +const protify = s => s.replace(/-([a-z])/g, m => m[1].toUpperCase()); + +methods.forEach(k => { + CLightningClient.prototype[protify(k)] = function (...args: any) { + return this.call(k, args); + }; +}); diff --git a/backend/src/api/lightning/clightning/clightning-convert.ts b/backend/src/api/lightning/clightning/clightning-convert.ts index 34ef6f942..8ceec3b7e 100644 --- a/backend/src/api/lightning/clightning/clightning-convert.ts +++ b/backend/src/api/lightning/clightning/clightning-convert.ts @@ -1,6 +1,8 @@ -import logger from "../../../logger"; -import { ILightningApi } from "../lightning-api.interface"; +import { ILightningApi } from '../lightning-api.interface'; +/** + * Convert a clightning "listnode" entry to a lnd node entry + */ export function convertNode(clNode: any): ILightningApi.Node { return { alias: clNode.alias ?? '', @@ -12,7 +14,10 @@ export function convertNode(clNode: any): ILightningApi.Node { }; } -export function convertAndmergeBidirectionalChannels(clChannels: any[]): ILightningApi.Channel[] { +/** + * Convert clightning "listchannels" response to lnd "describegraph.channels" format + */ + export function convertAndmergeBidirectionalChannels(clChannels: any[]): ILightningApi.Channel[] { const consolidatedChannelList: ILightningApi.Channel[] = []; const clChannelsDict = {}; const clChannelsDictCount = {}; @@ -23,27 +28,24 @@ export function convertAndmergeBidirectionalChannels(clChannels: any[]): ILightn clChannelsDictCount[clChannel.short_channel_id] = 1; } else { consolidatedChannelList.push( - buildBidirectionalChannel(clChannel, clChannelsDict[clChannel.short_channel_id]) + buildFullChannel(clChannel, clChannelsDict[clChannel.short_channel_id]) ); delete clChannelsDict[clChannel.short_channel_id]; clChannelsDictCount[clChannel.short_channel_id]++; } } - const bidirectionalChannelsCount = consolidatedChannelList.length; - for (const short_channel_id of Object.keys(clChannelsDict)) { - consolidatedChannelList.push(buildUnidirectionalChannel(clChannelsDict[short_channel_id])); + consolidatedChannelList.push(buildIncompleteChannel(clChannelsDict[short_channel_id])); } - const unidirectionalChannelsCount = consolidatedChannelList.length - bidirectionalChannelsCount; - - logger.debug(`clightning knows ${clChannels.length} channels. ` + - `We found ${bidirectionalChannelsCount} bidirectional channels ` + - `and ${unidirectionalChannelsCount} unidirectional channels.`); return consolidatedChannelList; } -function buildBidirectionalChannel(clChannelA: any, clChannelB: any): ILightningApi.Channel { +/** + * Convert two clightning "getchannels" entries into a full a lnd "describegraph.channels" format + * In this case, clightning knows the channel policy for both nodes + */ +function buildFullChannel(clChannelA: any, clChannelB: any): ILightningApi.Channel { const lastUpdate = Math.max(clChannelA.last_update ?? 0, clChannelB.last_update ?? 0); return { @@ -59,7 +61,11 @@ function buildBidirectionalChannel(clChannelA: any, clChannelB: any): ILightning }; } -function buildUnidirectionalChannel(clChannel: any): ILightningApi.Channel { +/** + * Convert one clightning "getchannels" entry into a full a lnd "describegraph.channels" format + * In this case, clightning knows the channel policy of only one node + */ + function buildIncompleteChannel(clChannel: any): ILightningApi.Channel { return { id: clChannel.short_channel_id, capacity: clChannel.satoshis, @@ -70,7 +76,10 @@ function buildUnidirectionalChannel(clChannel: any): ILightningApi.Channel { }; } -function convertPolicy(clChannel: any): ILightningApi.Policy { +/** + * Convert a clightning "listnode" response to a lnd channel policy format + */ + function convertPolicy(clChannel: any): ILightningApi.Policy { return { public_key: clChannel.source, base_fee_mtokens: clChannel.base_fee_millisatoshi, @@ -82,7 +91,10 @@ function convertPolicy(clChannel: any): ILightningApi.Policy { }; } -function getEmptyPolicy(): ILightningApi.Policy { +/** + * Create an empty channel policy in lnd format + */ + function getEmptyPolicy(): ILightningApi.Policy { return { public_key: 'null', base_fee_mtokens: '0', diff --git a/backend/src/api/lightning/clightning/jsonrpc.ts b/backend/src/api/lightning/clightning/jsonrpc.ts deleted file mode 100644 index d0b187a54..000000000 --- a/backend/src/api/lightning/clightning/jsonrpc.ts +++ /dev/null @@ -1,249 +0,0 @@ -// Imported from https://github.com/shesek/lightning-client-js - -'use strict'; - -const methods = [ - 'addgossip', - 'autocleaninvoice', - 'check', - 'checkmessage', - 'close', - 'connect', - 'createinvoice', - 'createinvoicerequest', - 'createoffer', - 'createonion', - 'decode', - 'decodepay', - 'delexpiredinvoice', - 'delinvoice', - 'delpay', - 'dev-listaddrs', - 'dev-rescan-outputs', - 'disableoffer', - 'disconnect', - 'estimatefees', - 'feerates', - 'fetchinvoice', - 'fundchannel', - 'fundchannel_cancel', - 'fundchannel_complete', - 'fundchannel_start', - 'fundpsbt', - 'getchaininfo', - 'getinfo', - 'getlog', - 'getrawblockbyheight', - 'getroute', - 'getsharedsecret', - 'getutxout', - 'help', - 'invoice', - 'keysend', - 'legacypay', - 'listchannels', - 'listconfigs', - 'listforwards', - 'listfunds', - 'listinvoices', - 'listnodes', - 'listoffers', - 'listpays', - 'listpeers', - 'listsendpays', - 'listtransactions', - 'multifundchannel', - 'multiwithdraw', - 'newaddr', - 'notifications', - 'offer', - 'offerout', - 'openchannel_abort', - 'openchannel_bump', - 'openchannel_init', - 'openchannel_signed', - 'openchannel_update', - 'pay', - 'payersign', - 'paystatus', - 'ping', - 'plugin', - 'reserveinputs', - 'sendinvoice', - 'sendonion', - 'sendonionmessage', - 'sendpay', - 'sendpsbt', - 'sendrawtransaction', - 'setchannelfee', - 'signmessage', - 'signpsbt', - 'stop', - 'txdiscard', - 'txprepare', - 'txsend', - 'unreserveinputs', - 'utxopsbt', - 'waitanyinvoice', - 'waitblockheight', - 'waitinvoice', - 'waitsendpay', - 'withdraw' -]; - - -import EventEmitter from 'events'; -import { existsSync, statSync } from 'fs'; -import { createConnection, Socket } from 'net'; -import { homedir } from 'os'; -import path from 'path'; -import { createInterface, Interface } from 'readline'; -import logger from '../../../logger'; - -class LightningError extends Error { - type: string = 'lightning'; - message: string = 'lightning-client error'; - - constructor(error) { - super(); - this.type = error.type; - this.message = error.message; - } -} - -const defaultRpcPath = path.join(homedir(), '.lightning') - , fStat = (...p) => statSync(path.join(...p)) - , fExists = (...p) => existsSync(path.join(...p)) - -export default class CLightningClient extends EventEmitter { - private rpcPath: string; - private reconnectWait: number; - private reconnectTimeout; - private reqcount: number; - private client: Socket; - private rl: Interface; - private clientConnectionPromise: Promise; - - constructor(rpcPath = defaultRpcPath) { - if (!path.isAbsolute(rpcPath)) { - throw new Error('The rpcPath must be an absolute path'); - } - - if (!fExists(rpcPath) || !fStat(rpcPath).isSocket()) { - // network directory provided, use the lightning-rpc within in - if (fExists(rpcPath, 'lightning-rpc')) { - rpcPath = path.join(rpcPath, 'lightning-rpc'); - } - - // main data directory provided, default to using the bitcoin mainnet subdirectory - // to be removed in v0.2.0 - else if (fExists(rpcPath, 'bitcoin', 'lightning-rpc')) { - logger.warn(`[CLightningClient] ${rpcPath}/lightning-rpc is missing, using the bitcoin mainnet subdirectory at ${rpcPath}/bitcoin instead.`) - logger.warn(`[CLightningClient] specifying the main lightning data directory is deprecated, please specify the network directory explicitly.\n`) - rpcPath = path.join(rpcPath, 'bitcoin', 'lightning-rpc') - } - } - - logger.debug(`[CLightningClient] Connecting to ${rpcPath}`); - - super(); - this.rpcPath = rpcPath; - this.reconnectWait = 0.5; - this.reconnectTimeout = null; - this.reqcount = 0; - - const _self = this; - - this.client = createConnection(rpcPath); - this.rl = createInterface({ input: this.client }) - - this.clientConnectionPromise = new Promise(resolve => { - _self.client.on('connect', () => { - logger.debug(`[CLightningClient] Lightning client connected`); - _self.reconnectWait = 1; - resolve(); - }); - - _self.client.on('end', () => { - logger.err('[CLightningClient] Lightning client connection closed, reconnecting'); - _self.increaseWaitTime(); - _self.reconnect(); - }); - - _self.client.on('error', error => { - logger.err(`[CLightningClient] Lightning client connection error: ${error}`); - _self.emit('error', error); - _self.increaseWaitTime(); - _self.reconnect(); - }); - }); - - this.rl.on('line', line => { - line = line.trim(); - if (!line) { - return; - } - const data = JSON.parse(line); - // logger.debug(`[CLightningClient] #${data.id} <-- ${JSON.stringify(data.error || data.result)}`); - _self.emit('res:' + data.id, data); - }); - } - - increaseWaitTime(): void { - if (this.reconnectWait >= 16) { - this.reconnectWait = 16; - } else { - this.reconnectWait *= 2; - } - } - - reconnect(): void { - const _self = this; - - if (this.reconnectTimeout) { - return; - } - - this.reconnectTimeout = setTimeout(() => { - logger.debug('[CLightningClient] Trying to reconnect...'); - - _self.client.connect(_self.rpcPath); - _self.reconnectTimeout = null; - }, this.reconnectWait * 1000); - } - - call(method, args = []): Promise { - const _self = this; - - const callInt = ++this.reqcount; - const sendObj = { - jsonrpc: '2.0', - method, - params: args, - id: '' + callInt - }; - - logger.debug(`[CLightningClient] #${callInt} --> ${method} ${args}`); - - // Wait for the client to connect - return this.clientConnectionPromise - .then(() => new Promise((resolve, reject) => { - // Wait for a response - this.once('res:' + callInt, res => res.error == null - ? resolve(res.result) - : reject(new LightningError(res.error)) - ); - - // Send the command - _self.client.write(JSON.stringify(sendObj)); - })); - } -} - -const protify = s => s.replace(/-([a-z])/g, m => m[1].toUpperCase()); - -methods.forEach(k => { - CLightningClient.prototype[protify(k)] = function (...args: any) { - return this.call(k, args); - }; -}); diff --git a/backend/src/api/lightning/lightning-api-abstract-factory.ts b/backend/src/api/lightning/lightning-api-abstract-factory.ts index 026568c6d..e6691b0a4 100644 --- a/backend/src/api/lightning/lightning-api-abstract-factory.ts +++ b/backend/src/api/lightning/lightning-api-abstract-factory.ts @@ -1,7 +1,5 @@ import { ILightningApi } from './lightning-api.interface'; export interface AbstractLightningApi { - $getNetworkInfo(): Promise; $getNetworkGraph(): Promise; - $getInfo(): Promise; } diff --git a/backend/src/api/lightning/lightning-api-factory.ts b/backend/src/api/lightning/lightning-api-factory.ts index ab551095c..fdadd8230 100644 --- a/backend/src/api/lightning/lightning-api-factory.ts +++ b/backend/src/api/lightning/lightning-api-factory.ts @@ -1,9 +1,12 @@ import config from '../../config'; +import CLightningClient from './clightning/clightning-client'; import { AbstractLightningApi } from './lightning-api-abstract-factory'; import LndApi from './lnd/lnd-api'; function lightningApiFactory(): AbstractLightningApi { - switch (config.LIGHTNING.BACKEND) { + switch (config.LIGHTNING.ENABLED === true && config.LIGHTNING.BACKEND) { + case 'cln': + return new CLightningClient(config.CLIGHTNING.SOCKET); case 'lnd': default: return new LndApi(); diff --git a/backend/src/tasks/lightning/node-sync.service.ts b/backend/src/tasks/lightning/node-sync.service.ts index 10cd2d744..d3367d51c 100644 --- a/backend/src/tasks/lightning/node-sync.service.ts +++ b/backend/src/tasks/lightning/node-sync.service.ts @@ -5,9 +5,9 @@ import bitcoinClient from '../../api/bitcoin/bitcoin-client'; import bitcoinApi from '../../api/bitcoin/bitcoin-api-factory'; import config from '../../config'; import { IEsploraApi } from '../../api/bitcoin/esplora-api.interface'; -import lightningApi from '../../api/lightning/lightning-api-factory'; import { ILightningApi } from '../../api/lightning/lightning-api.interface'; import { $lookupNodeLocation } from './sync-tasks/node-locations'; +import lightningApi from '../../api/lightning/lightning-api-factory'; class NodeSyncService { constructor() {} From 918a4282378e9a3d72c1256823345e68a5c6b2d4 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Mon, 1 Aug 2022 19:42:33 +0200 Subject: [PATCH 082/105] Rebased using the update lightning interfaces --- .../lightning/clightning/clightning-client.ts | 2 +- .../clightning/clightning-convert.ts | 80 +++++++++---------- .../src/tasks/lightning/node-sync.service.ts | 24 ++++-- 3 files changed, 57 insertions(+), 49 deletions(-) diff --git a/backend/src/api/lightning/clightning/clightning-client.ts b/backend/src/api/lightning/clightning/clightning-client.ts index 629092d03..f5643ed01 100644 --- a/backend/src/api/lightning/clightning/clightning-client.ts +++ b/backend/src/api/lightning/clightning/clightning-client.ts @@ -249,7 +249,7 @@ export default class CLightningClient extends EventEmitter implements AbstractLi return { nodes: listnodes['nodes'].map(node => convertNode(node)), - channels: channelsList, + edges: channelsList, }; } } diff --git a/backend/src/api/lightning/clightning/clightning-convert.ts b/backend/src/api/lightning/clightning/clightning-convert.ts index 8ceec3b7e..008094bf5 100644 --- a/backend/src/api/lightning/clightning/clightning-convert.ts +++ b/backend/src/api/lightning/clightning/clightning-convert.ts @@ -8,14 +8,19 @@ export function convertNode(clNode: any): ILightningApi.Node { alias: clNode.alias ?? '', color: `#${clNode.color ?? ''}`, features: [], // TODO parse and return clNode.feature - public_key: clNode.nodeid, - sockets: clNode.addresses?.map(addr => `${addr.address}:${addr.port}`) ?? [], - updated_at: new Date((clNode?.last_timestamp ?? 0) * 1000).toUTCString(), + pub_key: clNode.nodeid, + addresses: clNode.addresses?.map((addr) => { + return { + network: addr.type, + addr: `${addr.address}:${addr.port}` + }; + }), + last_update: clNode?.last_timestamp ?? 0, }; } /** - * Convert clightning "listchannels" response to lnd "describegraph.channels" format + * Convert clightning "listchannels" response to lnd "describegraph.edges" format */ export function convertAndmergeBidirectionalChannels(clChannels: any[]): ILightningApi.Channel[] { const consolidatedChannelList: ILightningApi.Channel[] = []; @@ -41,67 +46,58 @@ export function convertNode(clNode: any): ILightningApi.Node { return consolidatedChannelList; } +export function convertChannelId(channelId): string { + const s = channelId.split('x').map(part => parseInt(part)); + return BigInt((s[0] << 40) | (s[1] << 16) | s[2]).toString(); +} + /** - * Convert two clightning "getchannels" entries into a full a lnd "describegraph.channels" format + * Convert two clightning "getchannels" entries into a full a lnd "describegraph.edges" format * In this case, clightning knows the channel policy for both nodes */ function buildFullChannel(clChannelA: any, clChannelB: any): ILightningApi.Channel { const lastUpdate = Math.max(clChannelA.last_update ?? 0, clChannelB.last_update ?? 0); return { - id: clChannelA.short_channel_id, + channel_id: clChannelA.short_channel_id, capacity: clChannelA.satoshis, - transaction_id: '', // TODO - transaction_vout: 0, // TODO - updated_at: new Date(lastUpdate * 1000).toUTCString(), - policies: [ - convertPolicy(clChannelA), - convertPolicy(clChannelB) - ] + last_update: lastUpdate, + node1_policy: convertPolicy(clChannelA), + node2_policy: convertPolicy(clChannelB), + chan_point: ':0', // TODO + node1_pub: clChannelA.source, + node2_pub: clChannelB.source, }; } /** - * Convert one clightning "getchannels" entry into a full a lnd "describegraph.channels" format + * Convert one clightning "getchannels" entry into a full a lnd "describegraph.edges" format * In this case, clightning knows the channel policy of only one node */ function buildIncompleteChannel(clChannel: any): ILightningApi.Channel { return { - id: clChannel.short_channel_id, + channel_id: clChannel.short_channel_id, capacity: clChannel.satoshis, - policies: [convertPolicy(clChannel), getEmptyPolicy()], - transaction_id: '', // TODO - transaction_vout: 0, // TODO - updated_at: new Date((clChannel.last_update ?? 0) * 1000).toUTCString(), + last_update: clChannel.last_update ?? 0, + node1_policy: convertPolicy(clChannel), + node2_policy: null, + chan_point: ':0', // TODO + node1_pub: clChannel.source, + node2_pub: clChannel.destination, }; } /** * Convert a clightning "listnode" response to a lnd channel policy format */ - function convertPolicy(clChannel: any): ILightningApi.Policy { + function convertPolicy(clChannel: any): ILightningApi.RoutingPolicy { return { - public_key: clChannel.source, - base_fee_mtokens: clChannel.base_fee_millisatoshi, - fee_rate: clChannel.fee_per_millionth, - is_disabled: !clChannel.active, - max_htlc_mtokens: clChannel.htlc_maximum_msat.slice(0, -4), - min_htlc_mtokens: clChannel.htlc_minimum_msat.slice(0, -4), - updated_at: new Date((clChannel.last_update ?? 0) * 1000).toUTCString(), - }; -} - -/** - * Create an empty channel policy in lnd format - */ - function getEmptyPolicy(): ILightningApi.Policy { - return { - public_key: 'null', - base_fee_mtokens: '0', - fee_rate: 0, - is_disabled: true, - max_htlc_mtokens: '0', - min_htlc_mtokens: '0', - updated_at: new Date(0).toUTCString(), + time_lock_delta: 0, // TODO + min_htlc: clChannel.htlc_minimum_msat.slice(0, -4), + max_htlc_msat: clChannel.htlc_maximum_msat.slice(0, -4), + fee_base_msat: clChannel.base_fee_millisatoshi, + fee_rate_milli_msat: clChannel.fee_per_millionth, + disabled: !clChannel.active, + last_update: clChannel.last_update ?? 0, }; } diff --git a/backend/src/tasks/lightning/node-sync.service.ts b/backend/src/tasks/lightning/node-sync.service.ts index d3367d51c..863ee30da 100644 --- a/backend/src/tasks/lightning/node-sync.service.ts +++ b/backend/src/tasks/lightning/node-sync.service.ts @@ -8,6 +8,7 @@ import { IEsploraApi } from '../../api/bitcoin/esplora-api.interface'; import { ILightningApi } from '../../api/lightning/lightning-api.interface'; import { $lookupNodeLocation } from './sync-tasks/node-locations'; import lightningApi from '../../api/lightning/lightning-api-factory'; +import { convertChannelId } from '../../api/lightning/clightning/clightning-convert'; class NodeSyncService { constructor() {} @@ -320,7 +321,7 @@ class NodeSyncService { ;`; await DB.query(query, [ - channel.channel_id, + this.toIntegerId(channel.channel_id), this.toShortId(channel.channel_id), channel.capacity, txid, @@ -391,8 +392,7 @@ class NodeSyncService { private async $saveNode(node: ILightningApi.Node): Promise { try { - const updatedAt = this.utcDateToMysql(node.last_update); - const sockets = node.addresses.map(a => a.addr).join(','); + const sockets = (node.addresses?.map(a => a.addr).join(',')) ?? ''; const query = `INSERT INTO nodes( public_key, first_seen, @@ -401,15 +401,16 @@ class NodeSyncService { color, sockets ) - VALUES (?, NOW(), ?, ?, ?, ?) ON DUPLICATE KEY UPDATE updated_at = ?, alias = ?, color = ?, sockets = ?;`; + VALUES (?, NOW(), FROM_UNIXTIME(?), ?, ?, ?) + ON DUPLICATE KEY UPDATE updated_at = FROM_UNIXTIME(?), alias = ?, color = ?, sockets = ?`; await DB.query(query, [ node.pub_key, - updatedAt, + node.last_update, node.alias, node.color, sockets, - updatedAt, + node.last_update, node.alias, node.color, sockets, @@ -419,8 +420,19 @@ class NodeSyncService { } } + private toIntegerId(id: string): string { + if (config.LIGHTNING.BACKEND === 'lnd') { + return id; + } + return convertChannelId(id); + } + /** Decodes a channel id returned by lnd as uint64 to a short channel id */ private toShortId(id: string): string { + if (config.LIGHTNING.BACKEND === 'cln') { + return id; + } + const n = BigInt(id); return [ n >> 40n, // nth block From 06a404d9baf531d43a2a7c0791f49f6a842db11c Mon Sep 17 00:00:00 2001 From: nymkappa Date: Tue, 2 Aug 2022 16:18:19 +0200 Subject: [PATCH 083/105] Don't run the ln network update if the graph is emtpy --- backend/src/index.ts | 6 +++--- ...node-sync.service.ts => network-sync.service.ts} | 13 +++++++++++-- 2 files changed, 14 insertions(+), 5 deletions(-) rename backend/src/tasks/lightning/{node-sync.service.ts => network-sync.service.ts} (97%) diff --git a/backend/src/index.ts b/backend/src/index.ts index fa80fb2ad..0f7cc7aa7 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -28,7 +28,7 @@ import nodesRoutes from './api/explorer/nodes.routes'; import channelsRoutes from './api/explorer/channels.routes'; import generalLightningRoutes from './api/explorer/general.routes'; import lightningStatsUpdater from './tasks/lightning/stats-updater.service'; -import nodeSyncService from './tasks/lightning/node-sync.service'; +import networkSyncService from './tasks/lightning/network-sync.service'; import statisticsRoutes from './api/statistics/statistics.routes'; import miningRoutes from './api/mining/mining-routes'; import bisqRoutes from './api/bisq/bisq.routes'; @@ -136,8 +136,8 @@ class Server { } if (config.LIGHTNING.ENABLED) { - nodeSyncService.$startService() - .then(() => lightningStatsUpdater.$startService()); + networkSyncService.$startService() + .then(() => lightningStatsUpdater.$startService()); } this.server.listen(config.MEMPOOL.HTTP_PORT, () => { diff --git a/backend/src/tasks/lightning/node-sync.service.ts b/backend/src/tasks/lightning/network-sync.service.ts similarity index 97% rename from backend/src/tasks/lightning/node-sync.service.ts rename to backend/src/tasks/lightning/network-sync.service.ts index 863ee30da..826664cf4 100644 --- a/backend/src/tasks/lightning/node-sync.service.ts +++ b/backend/src/tasks/lightning/network-sync.service.ts @@ -10,7 +10,7 @@ import { $lookupNodeLocation } from './sync-tasks/node-locations'; import lightningApi from '../../api/lightning/lightning-api-factory'; import { convertChannelId } from '../../api/lightning/clightning/clightning-convert'; -class NodeSyncService { +class NetworkSyncService { constructor() {} public async $startService() { @@ -28,6 +28,11 @@ class NodeSyncService { logger.info(`Updating nodes and channels...`); const networkGraph = await lightningApi.$getNetworkGraph(); + if (networkGraph.nodes.length === 0 || networkGraph.edges.length === 0) { + logger.info(`LN Network graph is empty, retrying in 10 seconds`); + setTimeout(this.$runUpdater, 10000); + return; + } for (const node of networkGraph.nodes) { await this.$saveNode(node); @@ -376,6 +381,10 @@ class NodeSyncService { } private async $setChannelsInactive(graphChannelsIds: string[]): Promise { + if (graphChannelsIds.length === 0) { + return; + } + try { await DB.query(` UPDATE channels @@ -447,4 +456,4 @@ class NodeSyncService { } } -export default new NodeSyncService(); +export default new NetworkSyncService(); From d9077412e2f46d234ea95f695e9a87e82f0a90a9 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Tue, 2 Aug 2022 16:39:34 +0200 Subject: [PATCH 084/105] Fetch funding tx for clightning channels --- .../lightning/clightning/clightning-client.ts | 2 +- .../clightning/clightning-convert.ts | 25 +++++++++++++------ backend/src/index.ts | 4 ++- .../sync-tasks/funding-tx-fetcher.ts | 16 ++++++------ 4 files changed, 29 insertions(+), 18 deletions(-) diff --git a/backend/src/api/lightning/clightning/clightning-client.ts b/backend/src/api/lightning/clightning/clightning-client.ts index f5643ed01..15f472f2e 100644 --- a/backend/src/api/lightning/clightning/clightning-client.ts +++ b/backend/src/api/lightning/clightning/clightning-client.ts @@ -245,7 +245,7 @@ export default class CLightningClient extends EventEmitter implements AbstractLi async $getNetworkGraph(): Promise { const listnodes: any[] = await this.call('listnodes'); const listchannels: any[] = await this.call('listchannels'); - const channelsList = convertAndmergeBidirectionalChannels(listchannels['channels']); + const channelsList = await convertAndmergeBidirectionalChannels(listchannels['channels']); return { nodes: listnodes['nodes'].map(node => convertNode(node)), diff --git a/backend/src/api/lightning/clightning/clightning-convert.ts b/backend/src/api/lightning/clightning/clightning-convert.ts index 008094bf5..1a267bc65 100644 --- a/backend/src/api/lightning/clightning/clightning-convert.ts +++ b/backend/src/api/lightning/clightning/clightning-convert.ts @@ -1,4 +1,5 @@ import { ILightningApi } from '../lightning-api.interface'; +import FundingTxFetcher from '../../../tasks/lightning/sync-tasks/funding-tx-fetcher'; /** * Convert a clightning "listnode" entry to a lnd node entry @@ -22,7 +23,7 @@ export function convertNode(clNode: any): ILightningApi.Node { /** * Convert clightning "listchannels" response to lnd "describegraph.edges" format */ - export function convertAndmergeBidirectionalChannels(clChannels: any[]): ILightningApi.Channel[] { + export async function convertAndmergeBidirectionalChannels(clChannels: any[]): Promise { const consolidatedChannelList: ILightningApi.Channel[] = []; const clChannelsDict = {}; const clChannelsDictCount = {}; @@ -33,14 +34,14 @@ export function convertNode(clNode: any): ILightningApi.Node { clChannelsDictCount[clChannel.short_channel_id] = 1; } else { consolidatedChannelList.push( - buildFullChannel(clChannel, clChannelsDict[clChannel.short_channel_id]) + await buildFullChannel(clChannel, clChannelsDict[clChannel.short_channel_id]) ); delete clChannelsDict[clChannel.short_channel_id]; clChannelsDictCount[clChannel.short_channel_id]++; } } for (const short_channel_id of Object.keys(clChannelsDict)) { - consolidatedChannelList.push(buildIncompleteChannel(clChannelsDict[short_channel_id])); + consolidatedChannelList.push(await buildIncompleteChannel(clChannelsDict[short_channel_id])); } return consolidatedChannelList; @@ -55,16 +56,20 @@ export function convertChannelId(channelId): string { * Convert two clightning "getchannels" entries into a full a lnd "describegraph.edges" format * In this case, clightning knows the channel policy for both nodes */ -function buildFullChannel(clChannelA: any, clChannelB: any): ILightningApi.Channel { +async function buildFullChannel(clChannelA: any, clChannelB: any): Promise { const lastUpdate = Math.max(clChannelA.last_update ?? 0, clChannelB.last_update ?? 0); - + + const tx = await FundingTxFetcher.$fetchChannelOpenTx(clChannelA.short_channel_id); + const parts = clChannelA.short_channel_id.split('x'); + const outputIdx = parts[2]; + return { channel_id: clChannelA.short_channel_id, capacity: clChannelA.satoshis, last_update: lastUpdate, node1_policy: convertPolicy(clChannelA), node2_policy: convertPolicy(clChannelB), - chan_point: ':0', // TODO + chan_point: `${tx.txid}:${outputIdx}`, node1_pub: clChannelA.source, node2_pub: clChannelB.source, }; @@ -74,14 +79,18 @@ function buildFullChannel(clChannelA: any, clChannelB: any): ILightningApi.Chann * Convert one clightning "getchannels" entry into a full a lnd "describegraph.edges" format * In this case, clightning knows the channel policy of only one node */ - function buildIncompleteChannel(clChannel: any): ILightningApi.Channel { + async function buildIncompleteChannel(clChannel: any): Promise { + const tx = await FundingTxFetcher.$fetchChannelOpenTx(clChannel.short_channel_id); + const parts = clChannel.short_channel_id.split('x'); + const outputIdx = parts[2]; + return { channel_id: clChannel.short_channel_id, capacity: clChannel.satoshis, last_update: clChannel.last_update ?? 0, node1_policy: convertPolicy(clChannel), node2_policy: null, - chan_point: ':0', // TODO + chan_point: `${tx.txid}:${outputIdx}`, node1_pub: clChannel.source, node2_pub: clChannel.destination, }; diff --git a/backend/src/index.ts b/backend/src/index.ts index 0f7cc7aa7..976ec12df 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -34,6 +34,7 @@ import miningRoutes from './api/mining/mining-routes'; import bisqRoutes from './api/bisq/bisq.routes'; import liquidRoutes from './api/liquid/liquid.routes'; import bitcoinRoutes from './api/bitcoin/bitcoin.routes'; +import fundingTxFetcher from "./tasks/lightning/sync-tasks/funding-tx-fetcher"; class Server { private wss: WebSocket.Server | undefined; @@ -136,7 +137,8 @@ class Server { } if (config.LIGHTNING.ENABLED) { - networkSyncService.$startService() + fundingTxFetcher.$init() + .then(() => networkSyncService.$startService()) .then(() => lightningStatsUpdater.$startService()); } diff --git a/backend/src/tasks/lightning/sync-tasks/funding-tx-fetcher.ts b/backend/src/tasks/lightning/sync-tasks/funding-tx-fetcher.ts index 9da721876..926d20c91 100644 --- a/backend/src/tasks/lightning/sync-tasks/funding-tx-fetcher.ts +++ b/backend/src/tasks/lightning/sync-tasks/funding-tx-fetcher.ts @@ -1,8 +1,6 @@ import { existsSync, promises } from 'fs'; -import bitcoinApiFactory from '../../../api/bitcoin/bitcoin-api-factory'; import bitcoinClient from '../../../api/bitcoin/bitcoin-client'; import config from '../../../config'; -import DB from '../../../database'; import logger from '../../../logger'; const fsPromises = promises; @@ -16,12 +14,7 @@ class FundingTxFetcher { private channelNewlyProcessed = 0; public fundingTxCache = {}; - async $fetchChannelsFundingTxs(channelIds: string[]): Promise { - if (this.running) { - return; - } - this.running = true; - + async $init(): Promise { // Load funding tx disk cache if (Object.keys(this.fundingTxCache).length === 0 && existsSync(CACHE_FILE_NAME)) { try { @@ -32,6 +25,13 @@ class FundingTxFetcher { } logger.debug(`Imported ${Object.keys(this.fundingTxCache).length} funding tx amount from the disk cache`); } + } + + async $fetchChannelsFundingTxs(channelIds: string[]): Promise { + if (this.running) { + return; + } + this.running = true; const globalTimer = new Date().getTime() / 1000; let cacheTimer = new Date().getTime() / 1000; From 9bdd574559d7221b385f44f9ab47edbdd72e4fdc Mon Sep 17 00:00:00 2001 From: wiz Date: Tue, 2 Aug 2022 21:49:53 +0200 Subject: [PATCH 085/105] Move fast-xml-parser from devDeps to deps --- backend/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/package.json b/backend/package.json index 750380156..47694ecf8 100644 --- a/backend/package.json +++ b/backend/package.json @@ -37,6 +37,7 @@ "bitcoinjs-lib": "6.0.1", "crypto-js": "^4.0.0", "express": "^4.18.0", + "fast-xml-parser": "^4.0.9", "maxmind": "^4.3.6", "mysql2": "2.3.3", "node-worker-threads-pool": "^1.5.1", @@ -53,7 +54,6 @@ "@typescript-eslint/parser": "^5.30.5", "eslint": "^8.19.0", "eslint-config-prettier": "^8.5.0", - "fast-xml-parser": "^4.0.9", "prettier": "^2.7.1" } } From f61a04ec6082ef351889b8fc8dc2955d841e2979 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Wed, 3 Aug 2022 12:13:55 +0200 Subject: [PATCH 086/105] Fix daily LN stats crash --- .../clightning/clightning-convert.ts | 33 ++++- .../tasks/lightning/stats-updater.service.ts | 10 +- .../sync-tasks/funding-tx-fetcher.ts | 2 +- .../lightning/sync-tasks/stats-importer.ts | 132 ++++++++++++------ 4 files changed, 123 insertions(+), 54 deletions(-) diff --git a/backend/src/api/lightning/clightning/clightning-convert.ts b/backend/src/api/lightning/clightning/clightning-convert.ts index 1a267bc65..75c8ec20c 100644 --- a/backend/src/api/lightning/clightning/clightning-convert.ts +++ b/backend/src/api/lightning/clightning/clightning-convert.ts @@ -1,5 +1,6 @@ import { ILightningApi } from '../lightning-api.interface'; import FundingTxFetcher from '../../../tasks/lightning/sync-tasks/funding-tx-fetcher'; +import logger from '../../../logger'; /** * Convert a clightning "listnode" entry to a lnd node entry @@ -23,12 +24,17 @@ export function convertNode(clNode: any): ILightningApi.Node { /** * Convert clightning "listchannels" response to lnd "describegraph.edges" format */ - export async function convertAndmergeBidirectionalChannels(clChannels: any[]): Promise { +export async function convertAndmergeBidirectionalChannels(clChannels: any[]): Promise { + logger.info('Converting clightning nodes and channels to lnd graph format'); + + let loggerTimer = new Date().getTime() / 1000; + let channelProcessed = 0; + const consolidatedChannelList: ILightningApi.Channel[] = []; const clChannelsDict = {}; const clChannelsDictCount = {}; - for (const clChannel of clChannels) { + for (const clChannel of clChannels) { if (!clChannelsDict[clChannel.short_channel_id]) { clChannelsDict[clChannel.short_channel_id] = clChannel; clChannelsDictCount[clChannel.short_channel_id] = 1; @@ -39,9 +45,26 @@ export function convertNode(clNode: any): ILightningApi.Node { delete clChannelsDict[clChannel.short_channel_id]; clChannelsDictCount[clChannel.short_channel_id]++; } + + const elapsedSeconds = Math.round((new Date().getTime() / 1000) - loggerTimer); + if (elapsedSeconds > 10) { + logger.info(`Building complete channels from clightning output. Channels processed: ${channelProcessed + 1} of ${clChannels.length}`); + loggerTimer = new Date().getTime() / 1000; + } + + ++channelProcessed; } - for (const short_channel_id of Object.keys(clChannelsDict)) { + + channelProcessed = 0; + const keys = Object.keys(clChannelsDict); + for (const short_channel_id of keys) { consolidatedChannelList.push(await buildIncompleteChannel(clChannelsDict[short_channel_id])); + + const elapsedSeconds = Math.round((new Date().getTime() / 1000) - loggerTimer); + if (elapsedSeconds > 10) { + logger.info(`Building partial channels from clightning output. Channels processed: ${channelProcessed + 1} of ${keys.length}`); + loggerTimer = new Date().getTime() / 1000; + } } return consolidatedChannelList; @@ -79,7 +102,7 @@ async function buildFullChannel(clChannelA: any, clChannelB: any): Promise { +async function buildIncompleteChannel(clChannel: any): Promise { const tx = await FundingTxFetcher.$fetchChannelOpenTx(clChannel.short_channel_id); const parts = clChannel.short_channel_id.split('x'); const outputIdx = parts[2]; @@ -99,7 +122,7 @@ async function buildFullChannel(clChannelA: any, clChannelB: any): Promise { this.$runTasks(); }, this.timeUntilMidnight()); @@ -42,9 +43,14 @@ class LightningStatsUpdater { this.setDateMidnight(date); date.setUTCHours(24); + const [rows] = await DB.query(`SELECT UNIX_TIMESTAMP(MAX(added)) as lastAdded from lightning_stats`); + if ((rows[0].lastAdded ?? 0) === date.getTime() / 1000) { + return; + } + logger.info(`Running lightning daily stats log...`); const networkGraph = await lightningApi.$getNetworkGraph(); - LightningStatsImporter.computeNetworkStats(date.getTime(), networkGraph); + LightningStatsImporter.computeNetworkStats(date.getTime() / 1000, networkGraph); } } diff --git a/backend/src/tasks/lightning/sync-tasks/funding-tx-fetcher.ts b/backend/src/tasks/lightning/sync-tasks/funding-tx-fetcher.ts index 926d20c91..6ca72aef7 100644 --- a/backend/src/tasks/lightning/sync-tasks/funding-tx-fetcher.ts +++ b/backend/src/tasks/lightning/sync-tasks/funding-tx-fetcher.ts @@ -45,7 +45,7 @@ class FundingTxFetcher { let elapsedSeconds = Math.round((new Date().getTime() / 1000) - loggerTimer); if (elapsedSeconds > 10) { elapsedSeconds = Math.round((new Date().getTime() / 1000) - globalTimer); - logger.debug(`Indexing channels funding tx ${channelProcessed + 1} of ${channelIds.length} ` + + logger.info(`Indexing channels funding tx ${channelProcessed + 1} of ${channelIds.length} ` + `(${Math.floor(channelProcessed / channelIds.length * 10000) / 100}%) | ` + `elapsed: ${elapsedSeconds} seconds` ); diff --git a/backend/src/tasks/lightning/sync-tasks/stats-importer.ts b/backend/src/tasks/lightning/sync-tasks/stats-importer.ts index 91e67f77d..d9c441498 100644 --- a/backend/src/tasks/lightning/sync-tasks/stats-importer.ts +++ b/backend/src/tasks/lightning/sync-tasks/stats-importer.ts @@ -13,19 +13,19 @@ interface Node { features: string; rgb_color: string; alias: string; - addresses: string; + addresses: unknown[]; out_degree: number; in_degree: number; } interface Channel { - scid: string; - source: string; - destination: string; + channel_id: string; + node1_pub: string; + node2_pub: string; timestamp: number; features: string; fee_base_msat: number; - fee_proportional_millionths: number; + fee_rate_milli_msat: number; htlc_minimim_msat: number; cltv_expiry_delta: number; htlc_maximum_msat: number; @@ -60,10 +60,9 @@ class LightningStatsImporter { let hasClearnet = false; let isUnnanounced = true; - const sockets = node.addresses.split(','); - for (const socket of sockets) { - hasOnion = hasOnion || (socket.indexOf('torv3://') !== -1); - hasClearnet = hasClearnet || (socket.indexOf('ipv4://') !== -1 || socket.indexOf('ipv6://') !== -1); + for (const socket of (node.addresses ?? [])) { + hasOnion = hasOnion || ['torv2', 'torv3'].includes(socket.network); + hasClearnet = hasClearnet || ['ipv4', 'ipv6'].includes(socket.network); } if (hasOnion && hasClearnet) { clearnetTorNodes++; @@ -90,8 +89,11 @@ class LightningStatsImporter { const baseFees: number[] = []; const alreadyCountedChannels = {}; - for (const channel of networkGraph.channels) { - const short_id = channel.scid.slice(0, -2); + for (const channel of networkGraph.edges) { + let short_id = channel.channel_id; + if (short_id.indexOf('/') !== -1) { + short_id = short_id.slice(0, -2); + } const tx = await fundingTxFetcher.$fetchChannelOpenTx(short_id); if (!tx) { @@ -99,23 +101,23 @@ class LightningStatsImporter { continue; } - if (!nodeStats[channel.source]) { - nodeStats[channel.source] = { + if (!nodeStats[channel.node1_pub]) { + nodeStats[channel.node1_pub] = { capacity: 0, channels: 0, }; } - if (!nodeStats[channel.destination]) { - nodeStats[channel.destination] = { + if (!nodeStats[channel.node2_pub]) { + nodeStats[channel.node2_pub] = { capacity: 0, channels: 0, }; } - nodeStats[channel.source].capacity += Math.round(tx.value * 100000000); - nodeStats[channel.source].channels++; - nodeStats[channel.destination].capacity += Math.round(tx.value * 100000000); - nodeStats[channel.destination].channels++; + nodeStats[channel.node1_pub].capacity += Math.round(tx.value * 100000000); + nodeStats[channel.node1_pub].channels++; + nodeStats[channel.node2_pub].capacity += Math.round(tx.value * 100000000); + nodeStats[channel.node2_pub].channels++; if (!alreadyCountedChannels[short_id]) { capacity += Math.round(tx.value * 100000000); @@ -123,19 +125,31 @@ class LightningStatsImporter { alreadyCountedChannels[short_id] = true; } - if (channel.fee_proportional_millionths < 5000) { - avgFeeRate += channel.fee_proportional_millionths; - feeRates.push(channel.fee_proportional_millionths); - } - - if (channel.fee_base_msat < 5000) { - avgBaseFee += channel.fee_base_msat; - baseFees.push(channel.fee_base_msat); + if (channel.node1_policy !== undefined) { // Coming from the node + for (const policy of [channel.node1_policy, channel.node2_policy]) { + if (policy && policy.fee_rate_milli_msat < 5000) { + avgFeeRate += policy.fee_rate_milli_msat; + feeRates.push(policy.fee_rate_milli_msat); + } + if (policy && policy.fee_base_msat < 5000) { + avgBaseFee += policy.fee_base_msat; + baseFees.push(policy.fee_base_msat); + } + } + } else { // Coming from the historical import + if (channel.fee_rate_milli_msat < 5000) { + avgFeeRate += channel.fee_rate_milli_msat; + feeRates.push(channel.fee_rate_milli_msat); + } + if (channel.fee_base_msat < 5000) { + avgBaseFee += channel.fee_base_msat; + baseFees.push(channel.fee_base_msat); + } } } - avgFeeRate /= networkGraph.channels.length; - avgBaseFee /= networkGraph.channels.length; + avgFeeRate /= networkGraph.edges.length; + avgBaseFee /= networkGraph.edges.length; const medCapacity = capacities.sort((a, b) => b - a)[Math.round(capacities.length / 2 - 1)]; const medFeeRate = feeRates.sort((a, b) => b - a)[Math.round(feeRates.length / 2 - 1)]; const medBaseFee = baseFees.sort((a, b) => b - a)[Math.round(baseFees.length / 2 - 1)]; @@ -203,15 +217,28 @@ class LightningStatsImporter { let latestNodeCount = 1; const fileList = await fsPromises.readdir(this.topologiesFolder); + // Insert history from the most recent to the oldest + // This also put the .json cached files first fileList.sort().reverse(); - const [rows]: any[] = await DB.query('SELECT UNIX_TIMESTAMP(added) as added, node_count FROM lightning_stats'); + const [rows]: any[] = await DB.query(` + SELECT UNIX_TIMESTAMP(added) AS added, node_count + FROM lightning_stats + ORDER BY added DESC + `); const existingStatsTimestamps = {}; for (const row of rows) { - existingStatsTimestamps[row.added] = rows[0]; + existingStatsTimestamps[row.added] = row; } + // For logging purpose + let processed = 10; + let totalProcessed = -1; + for (const filename of fileList) { + processed++; + totalProcessed++; + const timestamp = parseInt(filename.split('_')[1], 10); // Stats exist already, don't calculate/insert them @@ -220,7 +247,7 @@ class LightningStatsImporter { continue; } - logger.debug(`Processing ${this.topologiesFolder}/${filename}`); + logger.debug(`Reading ${this.topologiesFolder}/${filename}`); const fileContent = await fsPromises.readFile(`${this.topologiesFolder}/${filename}`, 'utf8'); let graph; @@ -228,12 +255,13 @@ class LightningStatsImporter { try { graph = JSON.parse(fileContent); } catch (e) { - logger.debug(`Invalid topology file, cannot parse the content`); + logger.debug(`Invalid topology file ${this.topologiesFolder}/${filename}, cannot parse the content`); + continue; } } else { graph = this.parseFile(fileContent); if (!graph) { - logger.debug(`Invalid topology file, cannot parse the content`); + logger.debug(`Invalid topology file ${this.topologiesFolder}/${filename}, cannot parse the content`); continue; } await fsPromises.writeFile(`${this.topologiesFolder}/${filename}.json`, JSON.stringify(graph)); @@ -245,19 +273,22 @@ class LightningStatsImporter { const diffRatio = graph.nodes.length / latestNodeCount; if (diffRatio < 0.9) { // Ignore drop of more than 90% of the node count as it's probably a missing data point + logger.debug(`Nodes count diff ratio threshold reached, ignore the data for this day ${graph.nodes.length} nodes vs ${latestNodeCount}`); continue; } } latestNodeCount = graph.nodes.length; const datestr = `${new Date(timestamp * 1000).toUTCString()} (${timestamp})`; - logger.debug(`${datestr}: Found ${graph.nodes.length} nodes and ${graph.channels.length} channels`); + logger.debug(`${datestr}: Found ${graph.nodes.length} nodes and ${graph.edges.length} channels`); - // Cache funding txs - logger.debug(`Caching funding txs for ${datestr}`); - await fundingTxFetcher.$fetchChannelsFundingTxs(graph.channels.map(channel => channel.scid.slice(0, -2))); - - logger.debug(`Generating LN network stats for ${datestr}`); + if (processed > 10) { + logger.info(`Generating LN network stats for ${datestr}. Processed ${totalProcessed}/${fileList.length} files`); + processed = 0; + } else { + logger.debug(`Generating LN network stats for ${datestr}. Processed ${totalProcessed}/${fileList.length} files`); + } + await fundingTxFetcher.$fetchChannelsFundingTxs(graph.edges.map(channel => channel.channel_id.slice(0, -2))); const stat = await this.computeNetworkStats(timestamp, graph); existingStatsTimestamps[timestamp] = stat; @@ -290,13 +321,22 @@ class LightningStatsImporter { if (!node.data) { continue; } + const addresses: unknown[] = []; + const sockets = node.data[5].split(','); + for (const socket of sockets) { + const parts = socket.split('://'); + addresses.push({ + network: parts[0], + addr: parts[1], + }); + } nodes.push({ id: node.data[0], timestamp: node.data[1], features: node.data[2], rgb_color: node.data[3], alias: node.data[4], - addresses: node.data[5], + addresses: addresses, out_degree: node.data[6], in_degree: node.data[7], }); @@ -307,13 +347,13 @@ class LightningStatsImporter { continue; } channels.push({ - scid: channel.data[0], - source: channel.data[1], - destination: channel.data[2], + channel_id: channel.data[0], + node1_pub: channel.data[1], + node2_pub: channel.data[2], timestamp: channel.data[3], features: channel.data[4], fee_base_msat: channel.data[5], - fee_proportional_millionths: channel.data[6], + fee_rate_milli_msat: channel.data[6], htlc_minimim_msat: channel.data[7], cltv_expiry_delta: channel.data[8], htlc_maximum_msat: channel.data[9], @@ -322,7 +362,7 @@ class LightningStatsImporter { return { nodes: nodes, - channels: channels, + edges: channels, }; } } From 3c9403ba6d7dacbd1f569db2fe7c7da704838af9 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Wed, 3 Aug 2022 12:43:41 +0200 Subject: [PATCH 087/105] When LN backend crashed, catch the error and restart after 1 minute --- backend/src/index.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/backend/src/index.ts b/backend/src/index.ts index 976ec12df..683f964f0 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -137,9 +137,7 @@ class Server { } if (config.LIGHTNING.ENABLED) { - fundingTxFetcher.$init() - .then(() => networkSyncService.$startService()) - .then(() => lightningStatsUpdater.$startService()); + this.$runLightningBackend(); } this.server.listen(config.MEMPOOL.HTTP_PORT, () => { @@ -185,6 +183,18 @@ class Server { } } + async $runLightningBackend() { + try { + await fundingTxFetcher.$init(); + await networkSyncService.$startService(); + await lightningStatsUpdater.$startService(); + } catch(e) { + logger.err(`Lightning backend crashed. Restarting in 1 minute. Reason: ${(e instanceof Error ? e.message : e)}`); + await Common.sleep$(1000 * 60); + this.$runLightningBackend(); + }; +} + setUpWebsocketHandling() { if (this.wss) { websocketHandler.setWebsocketServer(this.wss); From 7b73d1c4dce197beebc64a6738acac83a4ba49a3 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Thu, 4 Aug 2022 11:30:32 +0200 Subject: [PATCH 088/105] Fix node page and display real time data --- backend/src/api/explorer/channels.api.ts | 66 +++++++++++-- backend/src/api/explorer/channels.routes.ts | 6 +- backend/src/api/explorer/nodes.api.ts | 96 ++++++++++++++----- backend/src/api/explorer/nodes.routes.ts | 6 ++ .../channels-list.component.html | 38 +++++--- .../channels-list.component.scss | 8 +- .../channels-list/channels-list.component.ts | 42 +++++--- .../app/lightning/node/node.component.html | 23 ++--- .../src/app/lightning/node/node.component.ts | 10 +- 9 files changed, 218 insertions(+), 77 deletions(-) diff --git a/backend/src/api/explorer/channels.api.ts b/backend/src/api/explorer/channels.api.ts index 79aeebb97..9928cc85b 100644 --- a/backend/src/api/explorer/channels.api.ts +++ b/backend/src/api/explorer/channels.api.ts @@ -1,5 +1,6 @@ import logger from '../../logger'; import DB from '../../database'; +import nodesApi from './nodes.api'; class ChannelsApi { public async $getAllChannels(): Promise { @@ -181,15 +182,57 @@ class ChannelsApi { public async $getChannelsForNode(public_key: string, index: number, length: number, status: string): Promise { try { - // Default active and inactive channels - let statusQuery = '< 2'; - // Closed channels only - if (status === 'closed') { - statusQuery = '= 2'; + let channelStatusFilter; + if (status === 'open') { + channelStatusFilter = '< 2'; + } else if (status === 'closed') { + channelStatusFilter = '= 2'; } - const query = `SELECT n1.alias AS alias_left, n2.alias AS alias_right, channels.*, ns1.channels AS channels_left, ns1.capacity AS capacity_left, ns2.channels AS channels_right, ns2.capacity AS capacity_right FROM channels LEFT JOIN nodes AS n1 ON n1.public_key = channels.node1_public_key LEFT JOIN nodes AS n2 ON n2.public_key = channels.node2_public_key LEFT JOIN node_stats AS ns1 ON ns1.public_key = channels.node1_public_key LEFT JOIN node_stats AS ns2 ON ns2.public_key = channels.node2_public_key WHERE (ns1.id = (SELECT MAX(id) FROM node_stats WHERE public_key = channels.node1_public_key) AND ns2.id = (SELECT MAX(id) FROM node_stats WHERE public_key = channels.node2_public_key)) AND (node1_public_key = ? OR node2_public_key = ?) AND status ${statusQuery} ORDER BY channels.capacity DESC LIMIT ?, ?`; - const [rows]: any = await DB.query(query, [public_key, public_key, index, length]); - const channels = rows.map((row) => this.convertChannel(row)); + + // Channels originating from node + let query = ` + SELECT node2.alias, node2.public_key, channels.status, channels.node1_fee_rate, + channels.capacity, channels.short_id, channels.id + FROM channels + JOIN nodes AS node2 ON node2.public_key = channels.node2_public_key + WHERE node1_public_key = ? AND channels.status ${channelStatusFilter} + `; + const [channelsFromNode]: any = await DB.query(query, [public_key, index, length]); + + // Channels incoming to node + query = ` + SELECT node1.alias, node1.public_key, channels.status, channels.node2_fee_rate, + channels.capacity, channels.short_id, channels.id + FROM channels + JOIN nodes AS node1 ON node1.public_key = channels.node1_public_key + WHERE node2_public_key = ? AND channels.status ${channelStatusFilter} + `; + const [channelsToNode]: any = await DB.query(query, [public_key, index, length]); + + let allChannels = channelsFromNode.concat(channelsToNode); + allChannels.sort((a, b) => { + return b.capacity - a.capacity; + }); + allChannels = allChannels.slice(index, index + length); + + const channels: any[] = [] + for (const row of allChannels) { + const activeChannelsStats: any = await nodesApi.$getActiveChannelsStats(row.public_key); + channels.push({ + status: row.status, + capacity: row.capacity ?? 0, + short_id: row.short_id, + id: row.id, + fee_rate: row.node1_fee_rate ?? row.node2_fee_rate ?? 0, + node: { + alias: row.alias.length > 0 ? row.alias : row.public_key.slice(0, 20), + public_key: row.public_key, + channels: activeChannelsStats.active_channel_count ?? 0, + capacity: activeChannelsStats.capacity ?? 0, + } + }); + } + return channels; } catch (e) { logger.err('$getChannelsForNode error: ' + (e instanceof Error ? e.message : e)); @@ -205,7 +248,12 @@ class ChannelsApi { if (status === 'closed') { statusQuery = '= 2'; } - const query = `SELECT COUNT(*) AS count FROM channels WHERE (node1_public_key = ? OR node2_public_key = ?) AND status ${statusQuery}`; + const query = ` + SELECT COUNT(*) AS count + FROM channels + WHERE (node1_public_key = ? OR node2_public_key = ?) + AND status ${statusQuery} + `; const [rows]: any = await DB.query(query, [public_key, public_key]); return rows[0]['count']; } catch (e) { diff --git a/backend/src/api/explorer/channels.routes.ts b/backend/src/api/explorer/channels.routes.ts index 495eec789..bbb075aa6 100644 --- a/backend/src/api/explorer/channels.routes.ts +++ b/backend/src/api/explorer/channels.routes.ts @@ -46,9 +46,11 @@ class ChannelsRoutes { } const index = parseInt(typeof req.query.index === 'string' ? req.query.index : '0', 10) || 0; const status: string = typeof req.query.status === 'string' ? req.query.status : ''; - const length = 25; - const channels = await channelsApi.$getChannelsForNode(req.query.public_key, index, length, status); + const channels = await channelsApi.$getChannelsForNode(req.query.public_key, index, 10, status); const channelsCount = await channelsApi.$getChannelsCountForNode(req.query.public_key, status); + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.header('X-Total-Count', channelsCount.toString()); res.json(channels); } catch (e) { diff --git a/backend/src/api/explorer/nodes.api.ts b/backend/src/api/explorer/nodes.api.ts index 96da7d1d5..6fba07449 100644 --- a/backend/src/api/explorer/nodes.api.ts +++ b/backend/src/api/explorer/nodes.api.ts @@ -4,21 +4,13 @@ import DB from '../../database'; class NodesApi { public async $getNode(public_key: string): Promise { try { - const query = ` - SELECT nodes.*, geo_names_iso.names as iso_code, geo_names_as.names as as_organization, geo_names_city.names as city, - geo_names_country.names as country, geo_names_subdivision.names as subdivision, - (SELECT Count(*) - FROM channels - WHERE channels.status = 2 AND ( channels.node1_public_key = ? OR channels.node2_public_key = ? )) AS channel_closed_count, - (SELECT Count(*) - FROM channels - WHERE channels.status = 1 AND ( channels.node1_public_key = ? OR channels.node2_public_key = ? )) AS channel_active_count, - (SELECT Sum(capacity) - FROM channels - WHERE channels.status = 1 AND ( channels.node1_public_key = ? OR channels.node2_public_key = ? )) AS capacity, - (SELECT Avg(capacity) - FROM channels - WHERE status = 1 AND ( node1_public_key = ? OR node2_public_key = ? )) AS channels_capacity_avg + // General info + let query = ` + SELECT public_key, alias, UNIX_TIMESTAMP(first_seen) AS first_seen, + UNIX_TIMESTAMP(updated_at) AS updated_at, color, sockets as sockets, + as_number, city_id, country_id, subdivision_id, longitude, latitude, + geo_names_iso.names as iso_code, geo_names_as.names as as_organization, geo_names_city.names as city, + geo_names_country.names as country, geo_names_subdivision.names as subdivision FROM nodes LEFT JOIN geo_names geo_names_as on geo_names_as.id = as_number LEFT JOIN geo_names geo_names_city on geo_names_city.id = city_id @@ -27,21 +19,70 @@ class NodesApi { LEFT JOIN geo_names geo_names_iso ON geo_names_iso.id = nodes.country_id AND geo_names_iso.type = 'country_iso_code' WHERE public_key = ? `; - const [rows]: any = await DB.query(query, [public_key, public_key, public_key, public_key, public_key, public_key, public_key, public_key, public_key]); - if (rows.length > 0) { - rows[0].as_organization = JSON.parse(rows[0].as_organization); - rows[0].subdivision = JSON.parse(rows[0].subdivision); - rows[0].city = JSON.parse(rows[0].city); - rows[0].country = JSON.parse(rows[0].country); - return rows[0]; + let [rows]: any[] = await DB.query(query, [public_key]); + if (rows.length === 0) { + throw new Error(`This node does not exist, or our node is not seeing it yet`); } - return null; + + const node = rows[0]; + node.as_organization = JSON.parse(node.as_organization); + node.subdivision = JSON.parse(node.subdivision); + node.city = JSON.parse(node.city); + node.country = JSON.parse(node.country); + + // Active channels and capacity + const activeChannelsStats: any = await this.$getActiveChannelsStats(public_key); + node.active_channel_count = activeChannelsStats.active_channel_count ?? 0; + node.capacity = activeChannelsStats.capacity ?? 0; + + // Opened channels count + query = ` + SELECT count(short_id) as opened_channel_count + FROM channels + WHERE status != 2 AND (channels.node1_public_key = ? OR channels.node2_public_key = ?) + `; + [rows] = await DB.query(query, [public_key, public_key]); + node.opened_channel_count = 0; + if (rows.length > 0) { + node.opened_channel_count = rows[0].opened_channel_count; + } + + // Closed channels count + query = ` + SELECT count(short_id) as closed_channel_count + FROM channels + WHERE status = 2 AND (channels.node1_public_key = ? OR channels.node2_public_key = ?) + `; + [rows] = await DB.query(query, [public_key, public_key]); + node.closed_channel_count = 0; + if (rows.length > 0) { + node.closed_channel_count = rows[0].closed_channel_count; + } + + return node; } catch (e) { - logger.err('$getNode error: ' + (e instanceof Error ? e.message : e)); + logger.err(`Cannot get node information for ${public_key}. Reason: ${(e instanceof Error ? e.message : e)}`); throw e; } } + public async $getActiveChannelsStats(node_public_key: string): Promise { + const query = ` + SELECT count(short_id) as active_channel_count, sum(capacity) as capacity + FROM channels + WHERE status = 1 AND (channels.node1_public_key = ? OR channels.node2_public_key = ?) + `; + const [rows]: any[] = await DB.query(query, [node_public_key, node_public_key]); + if (rows.length > 0) { + return { + active_channel_count: rows[0].active_channel_count, + capacity: rows[0].capacity + }; + } else { + return null; + } + } + public async $getAllNodes(): Promise { try { const query = `SELECT * FROM nodes`; @@ -55,7 +96,12 @@ class NodesApi { public async $getNodeStats(public_key: string): Promise { try { - const query = `SELECT UNIX_TIMESTAMP(added) AS added, capacity, channels FROM node_stats WHERE public_key = ? ORDER BY added DESC`; + const query = ` + SELECT UNIX_TIMESTAMP(added) AS added, capacity, channels + FROM node_stats + WHERE public_key = ? + ORDER BY added DESC + `; const [rows]: any = await DB.query(query, [public_key]); return rows; } catch (e) { diff --git a/backend/src/api/explorer/nodes.routes.ts b/backend/src/api/explorer/nodes.routes.ts index 83e3c393e..a850b6a09 100644 --- a/backend/src/api/explorer/nodes.routes.ts +++ b/backend/src/api/explorer/nodes.routes.ts @@ -35,6 +35,9 @@ class NodesRoutes { res.status(404).send('Node not found'); return; } + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(node); } catch (e) { res.status(500).send(e instanceof Error ? e.message : e); @@ -44,6 +47,9 @@ class NodesRoutes { private async $getHistoricalNodeStats(req: Request, res: Response) { try { const statistics = await nodesApi.$getNodeStats(req.params.public_key); + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); res.json(statistics); } catch (e) { res.status(500).send(e instanceof Error ? e.message : e); diff --git a/frontend/src/app/lightning/channels-list/channels-list.component.html b/frontend/src/app/lightning/channels-list/channels-list.component.html index 82283f689..b95cddf8d 100644 --- a/frontend/src/app/lightning/channels-list/channels-list.component.html +++ b/frontend/src/app/lightning/channels-list/channels-list.component.html @@ -2,24 +2,24 @@
- +
- +
- +
No channels to display
@@ -30,7 +30,7 @@ - + @@ -42,31 +42,41 @@
{{ node.alias || '?' }}
+ + + {{ channel.capacity | amountShortener: 1 }} + sats + + diff --git a/frontend/src/app/lightning/channels-list/channels-list.component.scss b/frontend/src/app/lightning/channels-list/channels-list.component.scss index 35a6ce0bc..ba7b0a3b5 100644 --- a/frontend/src/app/lightning/channels-list/channels-list.component.scss +++ b/frontend/src/app/lightning/channels-list/channels-list.component.scss @@ -1,3 +1,9 @@ .second-line { font-size: 12px; -} \ No newline at end of file +} + +.sats { + color: #ffffff66; + font-size: 12px; + top: 0px; +} diff --git a/frontend/src/app/lightning/channels-list/channels-list.component.ts b/frontend/src/app/lightning/channels-list/channels-list.component.ts index 4060d36da..6172a4a99 100644 --- a/frontend/src/app/lightning/channels-list/channels-list.component.ts +++ b/frontend/src/app/lightning/channels-list/channels-list.component.ts @@ -1,7 +1,8 @@ import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, OnInit, Output } from '@angular/core'; import { FormBuilder, FormGroup } from '@angular/forms'; -import { BehaviorSubject, combineLatest, merge, Observable, of } from 'rxjs'; -import { map, startWith, switchMap } from 'rxjs/operators'; +import { BehaviorSubject, merge, Observable } from 'rxjs'; +import { map, switchMap, tap } from 'rxjs/operators'; +import { isMobile } from 'src/app/shared/common.utils'; import { LightningApiService } from '../lightning-api.service'; @Component({ @@ -18,11 +19,13 @@ export class ChannelsListComponent implements OnInit, OnChanges { // @ts-ignore paginationSize: 'sm' | 'lg' = 'md'; paginationMaxSize = 10; - itemsPerPage = 25; + itemsPerPage = 10; page = 1; channelsPage$ = new BehaviorSubject(1); channelStatusForm: FormGroup; defaultStatus = 'open'; + status = 'open'; + publicKeySize = 25; constructor( private lightningApiService: LightningApiService, @@ -31,9 +34,12 @@ export class ChannelsListComponent implements OnInit, OnChanges { this.channelStatusForm = this.formBuilder.group({ status: [this.defaultStatus], }); + if (isMobile()) { + this.publicKeySize = 12; + } } - ngOnInit() { + ngOnInit(): void { if (document.body.clientWidth < 670) { this.paginationSize = 'sm'; this.paginationMaxSize = 3; @@ -41,28 +47,36 @@ export class ChannelsListComponent implements OnInit, OnChanges { } ngOnChanges(): void { - this.channelStatusForm.get('status').setValue(this.defaultStatus, { emitEvent: false }) - this.channelsStatusChangedEvent.emit(this.defaultStatus); + this.channelStatusForm.get('status').setValue(this.defaultStatus, { emitEvent: false }); + this.channelsPage$.next(1); - this.channels$ = combineLatest([ + this.channels$ = merge( this.channelsPage$, - this.channelStatusForm.get('status').valueChanges.pipe(startWith(this.defaultStatus)) - ]) + this.channelStatusForm.get('status').valueChanges, + ) .pipe( - switchMap(([page, status]) => { - this.channelsStatusChangedEvent.emit(status); - return this.lightningApiService.getChannelsByNodeId$(this.publicKey, (page -1) * this.itemsPerPage, status); + tap((val) => { + if (typeof val === 'string') { + this.status = val; + this.page = 1; + } else if (typeof val === 'number') { + this.page = val; + } + }), + switchMap(() => { + this.channelsStatusChangedEvent.emit(this.status); + return this.lightningApiService.getChannelsByNodeId$(this.publicKey, (this.page - 1) * this.itemsPerPage, this.status); }), map((response) => { return { channels: response.body, - totalItems: parseInt(response.headers.get('x-total-count'), 10) + totalItems: parseInt(response.headers.get('x-total-count'), 10) + 1 }; }), ); } - pageChange(page: number) { + pageChange(page: number): void { this.channelsPage$.next(page); } diff --git a/frontend/src/app/lightning/node/node.component.html b/frontend/src/app/lightning/node/node.component.html index cb0e5ed43..ac50ed51b 100644 --- a/frontend/src/app/lightning/node/node.component.html +++ b/frontend/src/app/lightning/node/node.component.html @@ -2,8 +2,9 @@ @@ -22,23 +23,23 @@
Node Alias  StatusStatus Fee Rate Capacity Channel ID
{{ node.channels }} channels
-
+
+ + + {{ node.capacity | amountShortener: 1 }} + sats + +
- Inactive - Active + Inactive + Active - Closed + Closed - {{ node.fee_rate }} ppm ({{ node.fee_rate / 10000 | number }}%) + {{ channel.fee_rate }} ppm ({{ channel.fee_rate / 10000 | number }}%) - - {{ channel.short_id }}
- + - + - + @@ -71,13 +72,13 @@ @@ -139,7 +140,7 @@
-

Channels ({{ channelsListStatus === 'open' ? node.channel_active_count : node.channel_closed_count }})

+

All channels ({{ channelsListStatus === 'open' ? node.opened_channel_count : node.closed_channel_count }})

diff --git a/frontend/src/app/lightning/node/node.component.ts b/frontend/src/app/lightning/node/node.component.ts index a8d487938..6f9358090 100644 --- a/frontend/src/app/lightning/node/node.component.ts +++ b/frontend/src/app/lightning/node/node.component.ts @@ -5,6 +5,7 @@ import { catchError, map, switchMap } from 'rxjs/operators'; import { SeoService } from 'src/app/services/seo.service'; import { getFlagEmoji } from 'src/app/shared/graphs.utils'; import { LightningApiService } from '../lightning-api.service'; +import { isMobile } from '../../shared/common.utils'; @Component({ selector: 'app-node', @@ -23,11 +24,17 @@ export class NodeComponent implements OnInit { error: Error; publicKey: string; + publicKeySize = 99; + constructor( private lightningApiService: LightningApiService, private activatedRoute: ActivatedRoute, private seoService: SeoService, - ) { } + ) { + if (isMobile()) { + this.publicKeySize = 12; + } + } ngOnInit(): void { this.node$ = this.activatedRoute.paramMap @@ -59,6 +66,7 @@ export class NodeComponent implements OnInit { }); } node.socketsObject = socketsObject; + node.avgCapacity = node.capacity / Math.max(1, node.active_channel_count); return node; }), catchError(err => { From 22f15926c86f2b20a47dc5271c9a7914f3b17756 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Thu, 4 Aug 2022 12:49:07 +0200 Subject: [PATCH 089/105] Add missing file --- frontend/src/app/shared/common.utils.ts | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 frontend/src/app/shared/common.utils.ts diff --git a/frontend/src/app/shared/common.utils.ts b/frontend/src/app/shared/common.utils.ts new file mode 100644 index 000000000..419c1665d --- /dev/null +++ b/frontend/src/app/shared/common.utils.ts @@ -0,0 +1,3 @@ +export function isMobile() { + return (window.innerWidth <= 767.98); +} From fab4f9202761173bb214e3e41ab737daa667c7e5 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Thu, 4 Aug 2022 13:05:15 +0200 Subject: [PATCH 090/105] Gracefully attempt to reconnect to cln upon error --- .../api/lightning/clightning/clightning-client.ts | 15 ++++++++++++--- .../src/tasks/lightning/network-sync.service.ts | 6 ++++-- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/backend/src/api/lightning/clightning/clightning-client.ts b/backend/src/api/lightning/clightning/clightning-client.ts index 15f472f2e..0535e0881 100644 --- a/backend/src/api/lightning/clightning/clightning-client.ts +++ b/backend/src/api/lightning/clightning/clightning-client.ts @@ -157,8 +157,18 @@ export default class CLightningClient extends EventEmitter implements AbstractLi const _self = this; - this.client = createConnection(rpcPath); - this.rl = createInterface({ input: this.client }) + this.client = createConnection(rpcPath).on( + 'error', () => { + _self.increaseWaitTime(); + _self.reconnect(); + } + ); + this.rl = createInterface({ input: this.client }).on( + 'error', () => { + _self.increaseWaitTime(); + _self.reconnect(); + } + ); this.clientConnectionPromise = new Promise(resolve => { _self.client.on('connect', () => { @@ -175,7 +185,6 @@ export default class CLightningClient extends EventEmitter implements AbstractLi _self.client.on('error', error => { logger.err(`[CLightningClient] Lightning client connection error: ${error}`); - _self.emit('error', error); _self.increaseWaitTime(); _self.reconnect(); }); diff --git a/backend/src/tasks/lightning/network-sync.service.ts b/backend/src/tasks/lightning/network-sync.service.ts index 826664cf4..5af6aef25 100644 --- a/backend/src/tasks/lightning/network-sync.service.ts +++ b/backend/src/tasks/lightning/network-sync.service.ts @@ -9,6 +9,7 @@ import { ILightningApi } from '../../api/lightning/lightning-api.interface'; import { $lookupNodeLocation } from './sync-tasks/node-locations'; import lightningApi from '../../api/lightning/lightning-api-factory'; import { convertChannelId } from '../../api/lightning/clightning/clightning-convert'; +import { Common } from '../../api/common'; class NetworkSyncService { constructor() {} @@ -23,14 +24,15 @@ class NetworkSyncService { }, 1000 * 60 * 60); } - private async $runUpdater() { + private async $runUpdater(): Promise { try { logger.info(`Updating nodes and channels...`); const networkGraph = await lightningApi.$getNetworkGraph(); if (networkGraph.nodes.length === 0 || networkGraph.edges.length === 0) { logger.info(`LN Network graph is empty, retrying in 10 seconds`); - setTimeout(this.$runUpdater, 10000); + await Common.sleep$(10000); + this.$runUpdater(); return; } From 24f35dddbdb3156ade06e2c67c39ec6a5b4a8fcb Mon Sep 17 00:00:00 2001 From: nymkappa Date: Thu, 4 Aug 2022 13:11:24 +0200 Subject: [PATCH 091/105] Re-applied missing fix from https://github.com/mempool/mempool/pull/2233 --- backend/src/api/explorer/nodes.api.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/api/explorer/nodes.api.ts b/backend/src/api/explorer/nodes.api.ts index 96da7d1d5..d6984da45 100644 --- a/backend/src/api/explorer/nodes.api.ts +++ b/backend/src/api/explorer/nodes.api.ts @@ -70,7 +70,7 @@ class NodesApi { const latestDate = rows[0].maxAdded; const query = ` - SELECT nodes.public_key, nodes.alias, node_stats.capacity, node_stats.channels + SELECT nodes.public_key, IF(nodes.alias = '', SUBSTRING(nodes.public_key, 1, 20), alias) as alias, node_stats.capacity, node_stats.channels FROM node_stats JOIN nodes ON nodes.public_key = node_stats.public_key WHERE added = FROM_UNIXTIME(${latestDate}) @@ -92,7 +92,7 @@ class NodesApi { const latestDate = rows[0].maxAdded; const query = ` - SELECT nodes.public_key, nodes.alias, node_stats.capacity, node_stats.channels + SELECT nodes.public_key, IF(nodes.alias = '', SUBSTRING(nodes.public_key, 1, 20), alias) as alias, node_stats.capacity, node_stats.channels FROM node_stats JOIN nodes ON nodes.public_key = node_stats.public_key WHERE added = FROM_UNIXTIME(${latestDate}) From 1bed687e76fc4c600962dea3d6cc3b860e1ccc39 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Thu, 4 Aug 2022 13:11:24 +0200 Subject: [PATCH 092/105] Re-applied missing fix from https://github.com/mempool/mempool/pull/2233 --- backend/src/api/explorer/nodes.api.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/api/explorer/nodes.api.ts b/backend/src/api/explorer/nodes.api.ts index 96da7d1d5..d6984da45 100644 --- a/backend/src/api/explorer/nodes.api.ts +++ b/backend/src/api/explorer/nodes.api.ts @@ -70,7 +70,7 @@ class NodesApi { const latestDate = rows[0].maxAdded; const query = ` - SELECT nodes.public_key, nodes.alias, node_stats.capacity, node_stats.channels + SELECT nodes.public_key, IF(nodes.alias = '', SUBSTRING(nodes.public_key, 1, 20), alias) as alias, node_stats.capacity, node_stats.channels FROM node_stats JOIN nodes ON nodes.public_key = node_stats.public_key WHERE added = FROM_UNIXTIME(${latestDate}) @@ -92,7 +92,7 @@ class NodesApi { const latestDate = rows[0].maxAdded; const query = ` - SELECT nodes.public_key, nodes.alias, node_stats.capacity, node_stats.channels + SELECT nodes.public_key, IF(nodes.alias = '', SUBSTRING(nodes.public_key, 1, 20), alias) as alias, node_stats.capacity, node_stats.channels FROM node_stats JOIN nodes ON nodes.public_key = node_stats.public_key WHERE added = FROM_UNIXTIME(${latestDate}) From 6317d0867a5aa4189953c1cf74719f1849b1c991 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Thu, 4 Aug 2022 18:27:36 +0200 Subject: [PATCH 093/105] Run node stats every 10 minutes, only keep the latest entry per day --- backend/src/config.ts | 2 + .../tasks/lightning/stats-updater.service.ts | 29 ++--- .../lightning/sync-tasks/stats-importer.ts | 102 ++++++++++++------ 3 files changed, 80 insertions(+), 53 deletions(-) diff --git a/backend/src/config.ts b/backend/src/config.ts index b42a45ab2..d4dfc9edd 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -32,6 +32,7 @@ interface IConfig { ENABLED: boolean; BACKEND: 'lnd' | 'cln' | 'ldk'; TOPOLOGY_FOLDER: string; + NODE_STATS_REFRESH_INTERVAL: number; }; LND: { TLS_CERT_PATH: string; @@ -183,6 +184,7 @@ const defaults: IConfig = { 'ENABLED': false, 'BACKEND': 'lnd', 'TOPOLOGY_FOLDER': '', + 'NODE_STATS_REFRESH_INTERVAL': 600, }, 'LND': { 'TLS_CERT_PATH': '', diff --git a/backend/src/tasks/lightning/stats-updater.service.ts b/backend/src/tasks/lightning/stats-updater.service.ts index 237cacd72..0fd147eef 100644 --- a/backend/src/tasks/lightning/stats-updater.service.ts +++ b/backend/src/tasks/lightning/stats-updater.service.ts @@ -2,25 +2,14 @@ import DB from '../../database'; import logger from '../../logger'; import lightningApi from '../../api/lightning/lightning-api-factory'; import LightningStatsImporter from './sync-tasks/stats-importer'; +import config from '../../config'; class LightningStatsUpdater { - hardCodedStartTime = '2018-01-12'; - public async $startService(): Promise { logger.info('Starting Lightning Stats service'); - LightningStatsImporter.$run(); - - setTimeout(() => { - this.$runTasks(); - }, this.timeUntilMidnight()); - } - - private timeUntilMidnight(): number { - const date = new Date(); - this.setDateMidnight(date); - date.setUTCHours(24); - return date.getTime() - new Date().getTime(); + // LightningStatsImporter.$run(); + this.$runTasks(); } private setDateMidnight(date: Date): void { @@ -35,20 +24,18 @@ class LightningStatsUpdater { setTimeout(() => { this.$runTasks(); - }, this.timeUntilMidnight()); + }, 1000 * config.LIGHTNING.NODE_STATS_REFRESH_INTERVAL); } + /** + * Update the latest entry for each node every config.LIGHTNING.NODE_STATS_REFRESH_INTERVAL seconds + */ private async $logStatsDaily(): Promise { const date = new Date(); this.setDateMidnight(date); date.setUTCHours(24); - const [rows] = await DB.query(`SELECT UNIX_TIMESTAMP(MAX(added)) as lastAdded from lightning_stats`); - if ((rows[0].lastAdded ?? 0) === date.getTime() / 1000) { - return; - } - - logger.info(`Running lightning daily stats log...`); + logger.info(`Updating latest node stats`); const networkGraph = await lightningApi.$getNetworkGraph(); LightningStatsImporter.computeNetworkStats(date.getTime() / 1000, networkGraph); } diff --git a/backend/src/tasks/lightning/sync-tasks/stats-importer.ts b/backend/src/tasks/lightning/sync-tasks/stats-importer.ts index d9c441498..ba4adc71c 100644 --- a/backend/src/tasks/lightning/sync-tasks/stats-importer.ts +++ b/backend/src/tasks/lightning/sync-tasks/stats-importer.ts @@ -41,7 +41,7 @@ class LightningStatsImporter { const [channels]: any[] = await DB.query('SELECT short_id from channels;'); logger.info('Caching funding txs for currently existing channels'); await fundingTxFetcher.$fetchChannelsFundingTxs(channels.map(channel => channel.short_id)); - + await this.$importHistoricalLightningStats(); } @@ -114,15 +114,15 @@ class LightningStatsImporter { }; } - nodeStats[channel.node1_pub].capacity += Math.round(tx.value * 100000000); - nodeStats[channel.node1_pub].channels++; - nodeStats[channel.node2_pub].capacity += Math.round(tx.value * 100000000); - nodeStats[channel.node2_pub].channels++; - if (!alreadyCountedChannels[short_id]) { capacity += Math.round(tx.value * 100000000); capacities.push(Math.round(tx.value * 100000000)); alreadyCountedChannels[short_id] = true; + + nodeStats[channel.node1_pub].capacity += Math.round(tx.value * 100000000); + nodeStats[channel.node1_pub].channels++; + nodeStats[channel.node2_pub].capacity += Math.round(tx.value * 100000000); + nodeStats[channel.node2_pub].channels++; } if (channel.node1_policy !== undefined) { // Coming from the node @@ -154,24 +154,40 @@ class LightningStatsImporter { const medFeeRate = feeRates.sort((a, b) => b - a)[Math.round(feeRates.length / 2 - 1)]; const medBaseFee = baseFees.sort((a, b) => b - a)[Math.round(baseFees.length / 2 - 1)]; const avgCapacity = Math.round(capacity / capacities.length); - + let query = `INSERT INTO lightning_stats( - added, - channel_count, - node_count, - total_capacity, - tor_nodes, - clearnet_nodes, - unannounced_nodes, - clearnet_tor_nodes, - avg_capacity, - avg_fee_rate, - avg_base_fee_mtokens, - med_capacity, - med_fee_rate, - med_base_fee_mtokens - ) - VALUES (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`; + added, + channel_count, + node_count, + total_capacity, + tor_nodes, + clearnet_nodes, + unannounced_nodes, + clearnet_tor_nodes, + avg_capacity, + avg_fee_rate, + avg_base_fee_mtokens, + med_capacity, + med_fee_rate, + med_base_fee_mtokens + ) + VALUES (FROM_UNIXTIME(?), ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + added = FROM_UNIXTIME(?), + channel_count = ?, + node_count = ?, + total_capacity = ?, + tor_nodes = ?, + clearnet_nodes = ?, + unannounced_nodes = ?, + clearnet_tor_nodes = ?, + avg_capacity = ?, + avg_fee_rate = ?, + avg_base_fee_mtokens = ?, + med_capacity = ?, + med_fee_rate = ?, + med_base_fee_mtokens = ? + `; await DB.query(query, [ timestamp, @@ -188,22 +204,44 @@ class LightningStatsImporter { medCapacity, medFeeRate, medBaseFee, + timestamp, + capacities.length, + networkGraph.nodes.length, + capacity, + torNodes, + clearnetNodes, + unannouncedNodes, + clearnetTorNodes, + avgCapacity, + avgFeeRate, + avgBaseFee, + medCapacity, + medFeeRate, + medBaseFee, ]); for (const public_key of Object.keys(nodeStats)) { query = `INSERT INTO node_stats( - public_key, - added, - capacity, - channels - ) - VALUES (?, FROM_UNIXTIME(?), ?, ?)`; - + public_key, + added, + capacity, + channels + ) + VALUES (?, FROM_UNIXTIME(?), ?, ?) + ON DUPLICATE KEY UPDATE + added = FROM_UNIXTIME(?), + capacity = ?, + channels = ? + `; + await DB.query(query, [ public_key, timestamp, nodeStats[public_key].capacity, nodeStats[public_key].channels, + timestamp, + nodeStats[public_key].capacity, + nodeStats[public_key].channels, ]); } @@ -278,7 +316,7 @@ class LightningStatsImporter { } } latestNodeCount = graph.nodes.length; - + const datestr = `${new Date(timestamp * 1000).toUTCString()} (${timestamp})`; logger.debug(`${datestr}: Found ${graph.nodes.length} nodes and ${graph.edges.length} channels`); @@ -367,4 +405,4 @@ class LightningStatsImporter { } } -export default new LightningStatsImporter; \ No newline at end of file +export default new LightningStatsImporter; From cb9db0c4928fa3c20798f00b33f8ca9f93bf43b1 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Fri, 5 Aug 2022 10:11:29 +0200 Subject: [PATCH 094/105] Always show channels map in node page - auto zoom on the node --- .../app/lightning/node/node.component.html | 20 +++------ .../src/app/lightning/node/node.component.ts | 9 ---- .../nodes-channels-map.component.html | 3 -- .../nodes-channels-map.component.scss | 3 +- .../nodes-channels-map.component.ts | 41 +++++++++---------- 5 files changed, 27 insertions(+), 49 deletions(-) diff --git a/frontend/src/app/lightning/node/node.component.html b/frontend/src/app/lightning/node/node.component.html index ac50ed51b..de6c816f0 100644 --- a/frontend/src/app/lightning/node/node.component.html +++ b/frontend/src/app/lightning/node/node.component.html @@ -102,9 +102,7 @@
-
- -
+
-
- + -
-
-

All channels ({{ channelsListStatus === 'open' ? node.opened_channel_count : node.closed_channel_count }})

-
- -
+

Channels ({{ channelsListStatus === 'open' ? node.opened_channel_count : node.closed_channel_count }})

- - -
-
\ No newline at end of file +
diff --git a/frontend/src/app/lightning/node/node.component.ts b/frontend/src/app/lightning/node/node.component.ts index 6f9358090..a81849388 100644 --- a/frontend/src/app/lightning/node/node.component.ts +++ b/frontend/src/app/lightning/node/node.component.ts @@ -19,7 +19,6 @@ export class NodeComponent implements OnInit { publicKey$: Observable; selectedSocketIndex = 0; qrCodeVisible = false; - channelsListMode = 'list'; channelsListStatus: string; error: Error; publicKey: string; @@ -83,14 +82,6 @@ export class NodeComponent implements OnInit { this.selectedSocketIndex = index; } - channelsListModeChange(toggle) { - if (toggle === true) { - this.channelsListMode = 'map'; - } else { - this.channelsListMode = 'list'; - } - } - onChannelsListStatusChanged(e) { this.channelsListStatus = e; } diff --git a/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.html b/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.html index 1208a906a..5ccb9f3bc 100644 --- a/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.html +++ b/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.html @@ -3,9 +3,6 @@
Lightning nodes channels world map -
(Tor nodes excluded)
diff --git a/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.scss b/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.scss index 7914a5364..7e6b9f050 100644 --- a/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.scss +++ b/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.scss @@ -20,7 +20,8 @@ } .full-container.nodepage { - margin-top: 50px; + margin-top: 25px; + margin-bottom: 25px; } .full-container.widget { diff --git a/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts b/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts index c71ff88ad..e0063858b 100644 --- a/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts +++ b/frontend/src/app/lightning/nodes-channels-map/nodes-channels-map.component.ts @@ -3,7 +3,6 @@ import { SeoService } from 'src/app/services/seo.service'; import { ApiService } from 'src/app/services/api.service'; import { Observable, switchMap, tap, zip } from 'rxjs'; import { AssetsService } from 'src/app/services/assets.service'; -import { download } from 'src/app/shared/graphs.utils'; import { ActivatedRoute, ParamMap, Router } from '@angular/router'; import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe'; import { StateService } from 'src/app/services/state.service'; @@ -21,6 +20,7 @@ export class NodesChannelsMap implements OnInit, OnDestroy { @Input() publicKey: string | undefined; observable$: Observable; + center: number[] | undefined = undefined; chartInstance = undefined; chartOptions: EChartsOption = {}; @@ -42,6 +42,8 @@ export class NodesChannelsMap implements OnInit, OnDestroy { ngOnDestroy(): void {} ngOnInit(): void { + this.center = this.style === 'widget' ? [0, 0, -10] : undefined; + if (this.style === 'graph') { this.seoService.setTitle($localize`Lightning nodes channels world map`); } @@ -52,13 +54,21 @@ export class NodesChannelsMap implements OnInit, OnDestroy { return zip( this.assetsService.getWorldMapJson$, this.apiService.getChannelsGeo$(params.get('public_key') ?? undefined), + [params.get('public_key') ?? undefined] ).pipe(tap((data) => { registerMap('world', data[0]); const channelsLoc = []; const nodes = []; const nodesPubkeys = {}; + let thisNodeGPS: number[] | undefined = undefined; for (const channel of data[1]) { + if (!thisNodeGPS && data[2] === channel[0]) { + thisNodeGPS = [channel[2], channel[3]]; + } else if (!thisNodeGPS && data[2] === channel[4]) { + thisNodeGPS = [channel[6], channel[7]]; + } + channelsLoc.push([[channel[2], channel[3]], [channel[6], channel[7]]]); if (!nodesPubkeys[channel[0]]) { nodes.push({ @@ -77,6 +87,13 @@ export class NodesChannelsMap implements OnInit, OnDestroy { nodesPubkeys[channel[4]] = true; } } + if (this.style === 'nodepage' && thisNodeGPS) { + // 1ML 0217890e3aad8d35bc054f43acc00084b25229ecff0ab68debd82883ad65ee8266 + // New York GPS [-74.0068, 40.7123] + // Map center [-20.55, 0, -9.85] + this.center = [thisNodeGPS[0] * -20.55 / -74.0068, 0, thisNodeGPS[1] * -9.85 / 40.7123]; + } + this.prepareChartOptions(nodes, channelsLoc); })); }) @@ -111,10 +128,10 @@ export class NodesChannelsMap implements OnInit, OnDestroy { } }, viewControl: { - center: this.style === 'widget' ? [0, 0, -10] : undefined, + center: this.center, minDistance: 1, maxDistance: 60, - distance: this.style === 'widget' ? 22 : 60, + distance: this.style === 'widget' ? 22 : this.style === 'nodepage' ? 22 : 60, alpha: 90, rotateSensitivity: 0, panSensitivity: this.style === 'widget' ? 0 : 1, @@ -204,22 +221,4 @@ export class NodesChannelsMap implements OnInit, OnDestroy { } }); } - - onSaveChart() { - // @ts-ignore - const prevBottom = this.chartOptions.grid.bottom; - const now = new Date(); - // @ts-ignore - this.chartOptions.grid.bottom = 30; - this.chartOptions.backgroundColor = '#11131f'; - this.chartInstance.setOption(this.chartOptions); - download(this.chartInstance.getDataURL({ - pixelRatio: 2, - excludeComponents: ['dataZoom'], - }), `lightning-nodes-heatmap-clearnet-${Math.round(now.getTime() / 1000)}.svg`); - // @ts-ignore - this.chartOptions.grid.bottom = prevBottom; - this.chartOptions.backgroundColor = 'none'; - this.chartInstance.setOption(this.chartOptions); - } } From bd69cee11817394dcb7bcc672b4df4c1cde0b023 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Fri, 5 Aug 2022 12:32:20 +0200 Subject: [PATCH 095/105] Make sure lightning stats are not duplicated in db --- backend/src/api/database-migration.ts | 7 ++++++- backend/src/tasks/lightning/stats-updater.service.ts | 6 +++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/backend/src/api/database-migration.ts b/backend/src/api/database-migration.ts index 816efc7cc..19f523eb3 100644 --- a/backend/src/api/database-migration.ts +++ b/backend/src/api/database-migration.ts @@ -4,7 +4,7 @@ import logger from '../logger'; import { Common } from './common'; class DatabaseMigration { - private static currentVersion = 34; + private static currentVersion = 35; private queryTimeout = 120000; private statisticsAddedIndexed = false; private uniqueLogs: string[] = []; @@ -315,6 +315,11 @@ class DatabaseMigration { if (databaseSchemaVersion < 34 && isBitcoin == true) { await this.$executeQuery('ALTER TABLE `lightning_stats` ADD clearnet_tor_nodes int(11) NOT NULL DEFAULT "0"'); } + + if (databaseSchemaVersion < 35 && isBitcoin == true) { + await this.$executeQuery('DELETE from `lightning_stats` WHERE added > "2021-09-19"'); + await this.$executeQuery('ALTER TABLE `lightning_stats` ADD CONSTRAINT added_unique UNIQUE (added);'); + } } /** diff --git a/backend/src/tasks/lightning/stats-updater.service.ts b/backend/src/tasks/lightning/stats-updater.service.ts index 0fd147eef..fbbef4021 100644 --- a/backend/src/tasks/lightning/stats-updater.service.ts +++ b/backend/src/tasks/lightning/stats-updater.service.ts @@ -8,8 +8,8 @@ class LightningStatsUpdater { public async $startService(): Promise { logger.info('Starting Lightning Stats service'); - // LightningStatsImporter.$run(); - this.$runTasks(); + await this.$runTasks(); + LightningStatsImporter.$run(); } private setDateMidnight(date: Date): void { @@ -35,7 +35,7 @@ class LightningStatsUpdater { this.setDateMidnight(date); date.setUTCHours(24); - logger.info(`Updating latest node stats`); + logger.info(`Updating latest networks stats`); const networkGraph = await lightningApi.$getNetworkGraph(); LightningStatsImporter.computeNetworkStats(date.getTime() / 1000, networkGraph); } From b0b0beca228d1bad4fb633a9f8deb04ef89addc7 Mon Sep 17 00:00:00 2001 From: nymkappa Date: Fri, 5 Aug 2022 12:43:26 +0200 Subject: [PATCH 096/105] Don't insert stats in the future --- backend/src/tasks/lightning/stats-updater.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/src/tasks/lightning/stats-updater.service.ts b/backend/src/tasks/lightning/stats-updater.service.ts index 0fd147eef..8fea9eb30 100644 --- a/backend/src/tasks/lightning/stats-updater.service.ts +++ b/backend/src/tasks/lightning/stats-updater.service.ts @@ -33,7 +33,6 @@ class LightningStatsUpdater { private async $logStatsDaily(): Promise { const date = new Date(); this.setDateMidnight(date); - date.setUTCHours(24); logger.info(`Updating latest node stats`); const networkGraph = await lightningApi.$getNetworkGraph(); From 1af641101ebb544d6302f4fc98be3ca5be4f8c06 Mon Sep 17 00:00:00 2001 From: softsimon Date: Sat, 6 Aug 2022 04:25:21 +0400 Subject: [PATCH 097/105] Right align mempool logo on mobile with dual logos --- .../app/components/master-page/master-page.component.html | 2 +- .../app/components/master-page/master-page.component.scss | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/components/master-page/master-page.component.html b/frontend/src/app/components/master-page/master-page.component.html index f152cb7b3..39acf122d 100644 --- a/frontend/src/app/components/master-page/master-page.component.html +++ b/frontend/src/app/components/master-page/master-page.component.html @@ -1,7 +1,7 @@
Total capacityActive capacity
Total channelsActive channels - {{ node.channel_active_count }} + {{ node.active_channel_count }}
Average channel sizeAverage channel size - - + +
First seen - +
Last update - +