diff --git a/backend/src/api/bitcoin/bitcoin-core.routes.ts b/backend/src/api/bitcoin/bitcoin-core.routes.ts new file mode 100644 index 000000000..7933dc17b --- /dev/null +++ b/backend/src/api/bitcoin/bitcoin-core.routes.ts @@ -0,0 +1,249 @@ +import { Application, NextFunction, Request, Response } from 'express'; +import logger from '../../logger'; +import bitcoinClient from './bitcoin-client'; + +/** + * Define a set of routes used by the accelerator server + * Those routes are not designed to be public + */ +class BitcoinBackendRoutes { + private static tag = 'BitcoinBackendRoutes'; + + public initRoutes(app: Application) { + app + .get('/api/internal/bitcoin-core/' + 'get-mempool-entry', this.disableCache, this.$getMempoolEntry) + .post('/api/internal/bitcoin-core/' + 'decode-raw-transaction', this.disableCache, this.$decodeRawTransaction) + .get('/api/internal/bitcoin-core/' + 'get-raw-transaction', this.disableCache, this.$getRawTransaction) + .post('/api/internal/bitcoin-core/' + 'send-raw-transaction', this.disableCache, this.$sendRawTransaction) + .post('/api/internal/bitcoin-core/' + 'test-mempool-accept', this.disableCache, this.$testMempoolAccept) + .get('/api/internal/bitcoin-core/' + 'get-mempool-ancestors', this.disableCache, this.$getMempoolAncestors) + .get('/api/internal/bitcoin-core/' + 'get-block', this.disableCache, this.$getBlock) + .get('/api/internal/bitcoin-core/' + 'get-block-hash', this.disableCache, this.$getBlockHash) + .get('/api/internal/bitcoin-core/' + 'get-block-count', this.disableCache, this.$getBlockCount) + ; + } + + /** + * Disable caching for bitcoin core routes + * + * @param req + * @param res + * @param next + */ + private disableCache(req: Request, res: Response, next: NextFunction): void { + res.setHeader('Pragma', 'no-cache'); + res.setHeader('Cache-control', 'private, no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0'); + res.setHeader('expires', -1); + next(); + } + + /** + * Exeption handler to return proper details to the accelerator server + * + * @param e + * @param fnName + * @param res + */ + private static handleException(e: any, fnName: string, res: Response): void { + if (typeof(e.code) === 'number') { + res.status(400).send(JSON.stringify(e, ['code', 'message'])); + } else { + const err = `exception in ${fnName}. ${e}. Details: ${JSON.stringify(e, ['code', 'message'])}`; + logger.err(err, BitcoinBackendRoutes.tag); + res.status(500).send(err); + } + } + + private async $getMempoolEntry(req: Request, res: Response): Promise { + const txid = req.query.txid; + try { + if (typeof(txid) !== 'string' || txid.length !== 64) { + res.status(400).send(`invalid param txid ${txid}. must be a string of 64 char`); + return; + } + const mempoolEntry = await bitcoinClient.getMempoolEntry(txid); + if (!mempoolEntry) { + res.status(404).send(`no mempool entry found for txid ${txid}`); + return; + } + res.status(200).send(mempoolEntry); + } catch (e: any) { + BitcoinBackendRoutes.handleException(e, 'getMempoolEntry', res); + } + } + + private async $decodeRawTransaction(req: Request, res: Response): Promise { + const rawTx = req.body.rawTx; + try { + if (typeof(rawTx) !== 'string') { + res.status(400).send(`invalid param rawTx ${rawTx}. must be a string`); + return; + } + const decodedTx = await bitcoinClient.decodeRawTransaction(rawTx); + if (!decodedTx) { + res.status(400).send(`unable to decode rawTx ${rawTx}`); + return; + } + res.status(200).send(decodedTx); + } catch (e: any) { + BitcoinBackendRoutes.handleException(e, 'decodeRawTransaction', res); + } + } + + private async $getRawTransaction(req: Request, res: Response): Promise { + const txid = req.query.txid; + const verbose = req.query.verbose; + try { + if (typeof(txid) !== 'string' || txid.length !== 64) { + res.status(400).send(`invalid param txid ${txid}. must be a string of 64 char`); + return; + } + if (typeof(verbose) !== 'string') { + res.status(400).send(`invalid param verbose ${verbose}. must be a string representing an integer`); + return; + } + const verboseNumber = parseInt(verbose, 10); + if (typeof(verboseNumber) !== 'number') { + res.status(400).send(`invalid param verbose ${verbose}. must be a valid integer`); + return; + } + + const decodedTx = await bitcoinClient.getRawTransaction(txid, verboseNumber); + if (!decodedTx) { + res.status(400).send(`unable to get raw transaction for txid ${txid}`); + return; + } + res.status(200).send(decodedTx); + } catch (e: any) { + BitcoinBackendRoutes.handleException(e, 'decodeRawTransaction', res); + } + } + + private async $sendRawTransaction(req: Request, res: Response): Promise { + const rawTx = req.body.rawTx; + try { + if (typeof(rawTx) !== 'string') { + res.status(400).send(`invalid param rawTx ${rawTx}. must be a string`); + return; + } + const txHex = await bitcoinClient.sendRawTransaction(rawTx); + if (!txHex) { + res.status(400).send(`unable to send rawTx ${rawTx}`); + return; + } + res.status(200).send(txHex); + } catch (e: any) { + BitcoinBackendRoutes.handleException(e, 'sendRawTransaction', res); + } + } + + private async $testMempoolAccept(req: Request, res: Response): Promise { + const rawTxs = req.body.rawTxs; + try { + if (typeof(rawTxs) !== 'object') { + res.status(400).send(`invalid param rawTxs ${JSON.stringify(rawTxs)}. must be an array of string`); + return; + } + const txHex = await bitcoinClient.testMempoolAccept(rawTxs); + if (typeof(txHex) !== 'object' || txHex.length === 0) { + res.status(400).send(`testmempoolaccept failed for raw txs ${JSON.stringify(rawTxs)}, got an empty result`); + return; + } + res.status(200).send(txHex); + } catch (e: any) { + BitcoinBackendRoutes.handleException(e, 'testMempoolAccept', res); + } + } + + private async $getMempoolAncestors(req: Request, res: Response): Promise { + const txid = req.query.txid; + const verbose = req.query.verbose; + try { + if (typeof(txid) !== 'string' || txid.length !== 64) { + res.status(400).send(`invalid param txid ${txid}. must be a string of 64 char`); + return; + } + if (typeof(verbose) !== 'string' || (verbose !== 'true' && verbose !== 'false')) { + res.status(400).send(`invalid param verbose ${verbose}. must be a string ('true' | 'false')`); + return; + } + + const ancestors = await bitcoinClient.getMempoolAncestors(txid, verbose === 'true' ? true : false); + if (!ancestors) { + res.status(400).send(`unable to get mempool ancestors for txid ${txid}`); + return; + } + res.status(200).send(ancestors); + } catch (e: any) { + BitcoinBackendRoutes.handleException(e, 'getMempoolAncestors', res); + } + } + + private async $getBlock(req: Request, res: Response): Promise { + const blockHash = req.query.hash; + const verbosity = req.query.verbosity; + try { + if (typeof(blockHash) !== 'string' || blockHash.length !== 64) { + res.status(400).send(`invalid param blockHash ${blockHash}. must be a string of 64 char`); + return; + } + if (typeof(verbosity) !== 'string') { + res.status(400).send(`invalid param verbosity ${verbosity}. must be a string representing an integer`); + return; + } + const verbosityNumber = parseInt(verbosity, 10); + if (typeof(verbosityNumber) !== 'number') { + res.status(400).send(`invalid param verbosity ${verbosity}. must be a valid integer`); + return; + } + + const block = await bitcoinClient.getBlock(blockHash, verbosityNumber); + if (!block) { + res.status(400).send(`unable to get block for block hash ${blockHash}`); + return; + } + res.status(200).send(block); + } catch (e: any) { + BitcoinBackendRoutes.handleException(e, 'getBlock', res); + } + } + + private async $getBlockHash(req: Request, res: Response): Promise { + const blockHeight = req.query.height; + try { + if (typeof(blockHeight) !== 'string') { + res.status(400).send(`invalid param blockHeight ${blockHeight}, must be a string representing an integer`); + return; + } + const blockHeightNumber = parseInt(blockHeight, 10); + if (typeof(blockHeightNumber) !== 'number') { + res.status(400).send(`invalid param blockHeight ${blockHeight}. must be a valid integer`); + return; + } + + const block = await bitcoinClient.getBlockHash(blockHeightNumber); + if (!block) { + res.status(400).send(`unable to get block hash for block height ${blockHeightNumber}`); + return; + } + res.status(200).send(block); + } catch (e: any) { + BitcoinBackendRoutes.handleException(e, 'getBlockHash', res); + } + } + + private async $getBlockCount(req: Request, res: Response): Promise { + try { + const count = await bitcoinClient.getBlockCount(); + if (!count) { + res.status(400).send(`unable to get block count`); + return; + } + res.status(200).send(`${count}`); + } catch (e: any) { + BitcoinBackendRoutes.handleException(e, 'getBlockCount', res); + } + } +} + +export default new BitcoinBackendRoutes \ No newline at end of file diff --git a/backend/src/index.ts b/backend/src/index.ts index 44fe87e3a..a7b2ad4df 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -44,6 +44,7 @@ import v8 from 'v8'; import { formatBytes, getBytesUnit } from './utils/format'; import redisCache from './api/redis-cache'; import accelerationApi from './api/services/acceleration'; +import bitcoinCoreRoutes from './api/bitcoin/bitcoin-core.routes'; class Server { private wss: WebSocket.Server | undefined; @@ -282,6 +283,7 @@ class Server { setUpHttpApiRoutes(): void { bitcoinRoutes.initRoutes(this.app); + bitcoinCoreRoutes.initRoutes(this.app); pricesRoutes.initRoutes(this.app); if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && config.MEMPOOL.ENABLED) { statisticsRoutes.initRoutes(this.app); diff --git a/backend/src/rpc-api/commands.ts b/backend/src/rpc-api/commands.ts index 78f5e12f4..ecfb2ed7c 100644 --- a/backend/src/rpc-api/commands.ts +++ b/backend/src/rpc-api/commands.ts @@ -91,4 +91,5 @@ module.exports = { walletPassphraseChange: 'walletpassphrasechange', getTxoutSetinfo: 'gettxoutsetinfo', getIndexInfo: 'getindexinfo', + testMempoolAccept: 'testmempoolaccept', }; diff --git a/contributors/ncois.txt b/contributors/natsee.txt similarity index 91% rename from contributors/ncois.txt rename to contributors/natsee.txt index 04a436613..c391ce823 100644 --- a/contributors/ncois.txt +++ b/contributors/natsee.txt @@ -1,3 +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 November 16, 2023. -Signed: ncois +Signed: natsee diff --git a/frontend/src/app/components/assets/asset-group/asset-group.component.html b/frontend/src/app/components/assets/asset-group/asset-group.component.html index a4c9e9da0..600a8a9bd 100644 --- a/frontend/src/app/components/assets/asset-group/asset-group.component.html +++ b/frontend/src/app/components/assets/asset-group/asset-group.component.html @@ -4,7 +4,7 @@
-

{{ group.name }}

+

{{ group['group'].name }}

Group of {{ group.assets.length | number }} assets
diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts index e225eb758..31b9a6de8 100644 --- a/frontend/src/app/interfaces/node-api.interface.ts +++ b/frontend/src/app/interfaces/node-api.interface.ts @@ -255,6 +255,29 @@ export interface INodesRanking { topByChannels: ITopNodesPerChannels[]; } +export interface INodesStatisticsEntry { + added: string; + avg_base_fee_mtokens: number; + avg_capacity: number; + avg_fee_rate: number; + channel_count: number; + clearnet_nodes: number; + clearnet_tor_nodes: number; + id: number; + med_base_fee_mtokens: number; + med_capacity: number; + med_fee_rate: number; + node_count: number; + tor_nodes: number; + total_capacity: number; + unannounced_nodes: number; +} + +export interface INodesStatistics { + latest: INodesStatisticsEntry; + previous: INodesStatisticsEntry; +} + export interface IOldestNodes { publicKey: string, alias: string, diff --git a/frontend/src/app/lightning/channels-statistics/channels-statistics.component.ts b/frontend/src/app/lightning/channels-statistics/channels-statistics.component.ts index 4059e9420..f2b78f53c 100644 --- a/frontend/src/app/lightning/channels-statistics/channels-statistics.component.ts +++ b/frontend/src/app/lightning/channels-statistics/channels-statistics.component.ts @@ -1,5 +1,6 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; +import { INodesStatistics } from '../../interfaces/node-api.interface'; @Component({ selector: 'app-channels-statistics', @@ -8,7 +9,7 @@ import { Observable } from 'rxjs'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class ChannelsStatisticsComponent implements OnInit { - @Input() statistics$: Observable; + @Input() statistics$: Observable; mode: string = 'avg'; constructor() { } 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 f7d318073..89059185e 100644 --- a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.html +++ b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.html @@ -63,7 +63,7 @@   - + @@ -77,7 +77,7 @@   - + diff --git a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts index 26a67cee6..dece98ddb 100644 --- a/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts +++ b/frontend/src/app/lightning/lightning-dashboard/lightning-dashboard.component.ts @@ -1,7 +1,7 @@ import { AfterViewInit, ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { share } from 'rxjs/operators'; -import { INodesRanking } from '../../interfaces/node-api.interface'; +import { INodesRanking, INodesStatistics } from '../../interfaces/node-api.interface'; import { SeoService } from '../../services/seo.service'; import { StateService } from '../../services/state.service'; import { LightningApiService } from '../lightning-api.service'; @@ -13,7 +13,7 @@ import { LightningApiService } from '../lightning-api.service'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class LightningDashboardComponent implements OnInit, AfterViewInit { - statistics$: Observable; + statistics$: Observable; nodesRanking$: Observable; officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE; diff --git a/frontend/src/app/lightning/node-statistics/node-statistics.component.ts b/frontend/src/app/lightning/node-statistics/node-statistics.component.ts index c42720427..338e17ab8 100644 --- a/frontend/src/app/lightning/node-statistics/node-statistics.component.ts +++ b/frontend/src/app/lightning/node-statistics/node-statistics.component.ts @@ -1,5 +1,6 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; +import { INodesStatistics } from '../../interfaces/node-api.interface'; @Component({ selector: 'app-node-statistics', @@ -8,7 +9,7 @@ import { Observable } from 'rxjs'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class NodeStatisticsComponent implements OnInit { - @Input() statistics$: Observable; + @Input() statistics$: Observable; constructor() { } diff --git a/frontend/src/app/lightning/nodes-ranking/nodes-ranking.component.html b/frontend/src/app/lightning/nodes-ranking/nodes-ranking.component.html index 5bd03941e..51836880d 100644 --- a/frontend/src/app/lightning/nodes-ranking/nodes-ranking.component.html +++ b/frontend/src/app/lightning/nodes-ranking/nodes-ranking.component.html @@ -1,7 +1,7 @@ - + - + diff --git a/frontend/src/app/lightning/nodes-ranking/nodes-ranking.component.ts b/frontend/src/app/lightning/nodes-ranking/nodes-ranking.component.ts index 373751be9..8a1eae3dc 100644 --- a/frontend/src/app/lightning/nodes-ranking/nodes-ranking.component.ts +++ b/frontend/src/app/lightning/nodes-ranking/nodes-ranking.component.ts @@ -1,5 +1,9 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; +import { LightningApiService } from '../lightning-api.service'; +import { share } from 'rxjs/operators'; +import { Observable } from 'rxjs'; +import { INodesStatistics } from '../../interfaces/node-api.interface'; @Component({ selector: 'app-nodes-ranking', @@ -9,10 +13,15 @@ import { ActivatedRoute } from '@angular/router'; }) export class NodesRanking implements OnInit { type: string; + statistics$: Observable; - constructor(private route: ActivatedRoute) {} + constructor( + private route: ActivatedRoute, + private lightningApiService: LightningApiService, + ) {} ngOnInit(): void { + this.statistics$ = this.lightningApiService.getLatestStatistics$().pipe(share()); this.route.data.subscribe(data => { this.type = data.type; }); diff --git a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.html b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.html index 3efbc8594..27af80564 100644 --- a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.html +++ b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.html @@ -16,8 +16,8 @@ Last update Location - - + +
@@ -27,12 +27,14 @@ +  ({{ (node?.capacity / data.statistics.totalCapacity * 100) | number:'1.1-1' }}%) {{ node.channels | number }} +  ({{ (node?.channels / data.statistics.totalChannels * 100) | number:'1.1-1' }}%) diff --git a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.scss b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.scss index 3547c447f..89f144135 100644 --- a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.scss +++ b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.scss @@ -41,6 +41,11 @@ tr, td, th { } } +.capacity-ratio { + font-size: 12px; + color: darkgrey; +} + .fiat { width: 15%; @media (min-width: 768px) and (max-width: 991px) { diff --git a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.ts b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.ts index 054fa2f3c..0b8c03bbd 100644 --- a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.ts +++ b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-capacity/top-nodes-per-capacity.component.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; -import { map, Observable } from 'rxjs'; -import { INodesRanking, ITopNodesPerCapacity } from '../../../interfaces/node-api.interface'; +import { combineLatest, map, Observable } from 'rxjs'; +import { INodesRanking, INodesStatistics, ITopNodesPerCapacity } from '../../../interfaces/node-api.interface'; import { SeoService } from '../../../services/seo.service'; import { StateService } from '../../../services/state.service'; import { GeolocationData } from '../../../shared/components/geolocation/geolocation.component'; @@ -14,9 +14,10 @@ import { LightningApiService } from '../../lightning-api.service'; }) export class TopNodesPerCapacity implements OnInit { @Input() nodes$: Observable; + @Input() statistics$: Observable; @Input() widget: boolean = false; - topNodesPerCapacity$: Observable; + topNodesPerCapacity$: Observable<{ nodes: ITopNodesPerCapacity[]; statistics: { totalCapacity: number; totalChannels?: number; } }>; skeletonRows: number[] = []; currency$: Observable; @@ -39,8 +40,12 @@ export class TopNodesPerCapacity implements OnInit { } if (this.widget === false) { - this.topNodesPerCapacity$ = this.apiService.getTopNodesByCapacity$().pipe( - map((ranking) => { + this.topNodesPerCapacity$ = combineLatest([ + this.apiService.getTopNodesByCapacity$(), + this.statistics$ + ]) + .pipe( + map(([ranking, statistics]) => { for (const i in ranking) { ranking[i].geolocation = { country: ranking[i].country?.en, @@ -49,13 +54,28 @@ export class TopNodesPerCapacity implements OnInit { iso: ranking[i].iso_code, }; } - return ranking; + return { + nodes: ranking, + statistics: { + totalCapacity: statistics.latest.total_capacity, + totalChannels: statistics.latest.channel_count, + } + } }) ); } else { - this.topNodesPerCapacity$ = this.nodes$.pipe( - map((ranking) => { - return ranking.topByCapacity.slice(0, 6); + this.topNodesPerCapacity$ = combineLatest([ + this.nodes$, + this.statistics$ + ]) + .pipe( + map(([ranking, statistics]) => { + return { + nodes: ranking.topByCapacity.slice(0, 6), + statistics: { + totalCapacity: statistics.latest.total_capacity, + } + } }) ); } diff --git a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.html b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.html index 94a887bb3..87f7f1ad2 100644 --- a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.html +++ b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.html @@ -16,8 +16,8 @@ Last update Location - - + +
@@ -27,9 +27,11 @@ {{ node.channels ? (node.channels | number) : '~' }} +  ({{ (node?.channels / data.statistics.totalChannels * 100) | number:'1.1-1' }}%) +  ({{ (node.capacity / data.statistics.totalCapacity * 100) | number:'1.1-1' }}%) diff --git a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.scss b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.scss index a42599d69..63d65bcf8 100644 --- a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.scss +++ b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.scss @@ -44,6 +44,11 @@ tr, td, th { } } +.capacity-ratio { + font-size: 12px; + color: darkgrey; +} + .geolocation { @media (min-width: 768px) and (max-width: 991px) { display: none !important; diff --git a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.ts b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.ts index 3de177cc7..56d55a5d3 100644 --- a/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.ts +++ b/frontend/src/app/lightning/nodes-ranking/top-nodes-per-channels/top-nodes-per-channels.component.ts @@ -1,6 +1,6 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; -import { map, Observable } from 'rxjs'; -import { INodesRanking, ITopNodesPerChannels } from '../../../interfaces/node-api.interface'; +import { combineLatest, map, Observable } from 'rxjs'; +import { INodesRanking, INodesStatistics, ITopNodesPerChannels } from '../../../interfaces/node-api.interface'; import { SeoService } from '../../../services/seo.service'; import { StateService } from '../../../services/state.service'; import { GeolocationData } from '../../../shared/components/geolocation/geolocation.component'; @@ -14,12 +14,13 @@ import { LightningApiService } from '../../lightning-api.service'; }) export class TopNodesPerChannels implements OnInit { @Input() nodes$: Observable; + @Input() statistics$: Observable; @Input() widget: boolean = false; - topNodesPerChannels$: Observable; + topNodesPerChannels$: Observable<{ nodes: ITopNodesPerChannels[]; statistics: { totalChannels: number; totalCapacity?: number; } }>; skeletonRows: number[] = []; currency$: Observable; - + constructor( private apiService: LightningApiService, private stateService: StateService, @@ -37,8 +38,12 @@ export class TopNodesPerChannels implements OnInit { this.seoService.setTitle($localize`:@@c50bf442cf99f6fc5f8b687c460f33234b879869:Connectivity Ranking`); this.seoService.setDescription($localize`:@@meta.description.lightning.ranking.channels:See Lightning nodes with the most channels open along with high-level stats like total node capacity, node age, and more.`); - this.topNodesPerChannels$ = this.apiService.getTopNodesByChannels$().pipe( - map((ranking) => { + this.topNodesPerChannels$ = combineLatest([ + this.apiService.getTopNodesByChannels$(), + this.statistics$ + ]) + .pipe( + map(([ranking, statistics]) => { for (const i in ranking) { ranking[i].geolocation = { country: ranking[i].country?.en, @@ -47,12 +52,22 @@ export class TopNodesPerChannels implements OnInit { iso: ranking[i].iso_code, }; } - return ranking; + return { + nodes: ranking, + statistics: { + totalChannels: statistics.latest.channel_count, + totalCapacity: statistics.latest.total_capacity, + } + } }) ); } else { - this.topNodesPerChannels$ = this.nodes$.pipe( - map((ranking) => { + this.topNodesPerChannels$ = combineLatest([ + this.nodes$, + this.statistics$ + ]) + .pipe( + map(([ranking, statistics]) => { for (const i in ranking.topByChannels) { ranking.topByChannels[i].geolocation = { country: ranking.topByChannels[i].country?.en, @@ -61,7 +76,12 @@ export class TopNodesPerChannels implements OnInit { iso: ranking.topByChannels[i].iso_code, }; } - return ranking.topByChannels.slice(0, 6); + return { + nodes: ranking.topByChannels.slice(0, 6), + statistics: { + totalChannels: statistics.latest.channel_count, + } + } }) ); } diff --git a/frontend/src/app/liquid/liquid-master-page.module.ts b/frontend/src/app/liquid/liquid-master-page.module.ts index 17fa6b8e5..bb6e4cff8 100644 --- a/frontend/src/app/liquid/liquid-master-page.module.ts +++ b/frontend/src/app/liquid/liquid-master-page.module.ts @@ -91,7 +91,7 @@ const routes: Routes = [ }, { path: '**', - redirectTo: 'all' + redirectTo: 'featured' } ] },