Merge branch 'master' into nymkappa/mega-branch
This commit is contained in:
commit
59e30027a5
249
backend/src/api/bitcoin/bitcoin-core.routes.ts
Normal file
249
backend/src/api/bitcoin/bitcoin-core.routes.ts
Normal file
@ -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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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<void> {
|
||||||
|
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
|
@ -44,6 +44,7 @@ import v8 from 'v8';
|
|||||||
import { formatBytes, getBytesUnit } from './utils/format';
|
import { formatBytes, getBytesUnit } from './utils/format';
|
||||||
import redisCache from './api/redis-cache';
|
import redisCache from './api/redis-cache';
|
||||||
import accelerationApi from './api/services/acceleration';
|
import accelerationApi from './api/services/acceleration';
|
||||||
|
import bitcoinCoreRoutes from './api/bitcoin/bitcoin-core.routes';
|
||||||
|
|
||||||
class Server {
|
class Server {
|
||||||
private wss: WebSocket.Server | undefined;
|
private wss: WebSocket.Server | undefined;
|
||||||
@ -282,6 +283,7 @@ class Server {
|
|||||||
|
|
||||||
setUpHttpApiRoutes(): void {
|
setUpHttpApiRoutes(): void {
|
||||||
bitcoinRoutes.initRoutes(this.app);
|
bitcoinRoutes.initRoutes(this.app);
|
||||||
|
bitcoinCoreRoutes.initRoutes(this.app);
|
||||||
pricesRoutes.initRoutes(this.app);
|
pricesRoutes.initRoutes(this.app);
|
||||||
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && config.MEMPOOL.ENABLED) {
|
if (config.STATISTICS.ENABLED && config.DATABASE.ENABLED && config.MEMPOOL.ENABLED) {
|
||||||
statisticsRoutes.initRoutes(this.app);
|
statisticsRoutes.initRoutes(this.app);
|
||||||
|
@ -91,4 +91,5 @@ module.exports = {
|
|||||||
walletPassphraseChange: 'walletpassphrasechange',
|
walletPassphraseChange: 'walletpassphrasechange',
|
||||||
getTxoutSetinfo: 'gettxoutsetinfo',
|
getTxoutSetinfo: 'gettxoutsetinfo',
|
||||||
getIndexInfo: 'getindexinfo',
|
getIndexInfo: 'getindexinfo',
|
||||||
|
testMempoolAccept: 'testmempoolaccept',
|
||||||
};
|
};
|
||||||
|
@ -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.
|
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
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
<ng-template #title>
|
<ng-template #title>
|
||||||
<div class="main-title">
|
<div class="main-title">
|
||||||
<h2>{{ group.name }}</h2>
|
<h2>{{ group['group'].name }}</h2>
|
||||||
<div class="sub-title" i18n>Group of {{ group.assets.length | number }} assets</div>
|
<div class="sub-title" i18n>Group of {{ group.assets.length | number }} assets</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
@ -255,6 +255,29 @@ export interface INodesRanking {
|
|||||||
topByChannels: ITopNodesPerChannels[];
|
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 {
|
export interface IOldestNodes {
|
||||||
publicKey: string,
|
publicKey: string,
|
||||||
alias: string,
|
alias: string,
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
import { INodesStatistics } from '../../interfaces/node-api.interface';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-channels-statistics',
|
selector: 'app-channels-statistics',
|
||||||
@ -8,7 +9,7 @@ import { Observable } from 'rxjs';
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class ChannelsStatisticsComponent implements OnInit {
|
export class ChannelsStatisticsComponent implements OnInit {
|
||||||
@Input() statistics$: Observable<any>;
|
@Input() statistics$: Observable<INodesStatistics>;
|
||||||
mode: string = 'avg';
|
mode: string = 'avg';
|
||||||
|
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
@ -63,7 +63,7 @@
|
|||||||
<span> </span>
|
<span> </span>
|
||||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: #4a68b9"></fa-icon>
|
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: #4a68b9"></fa-icon>
|
||||||
</a>
|
</a>
|
||||||
<app-top-nodes-per-capacity [nodes$]="nodesRanking$" [widget]="true"></app-top-nodes-per-capacity>
|
<app-top-nodes-per-capacity [nodes$]="nodesRanking$" [statistics$]="statistics$" [widget]="true"></app-top-nodes-per-capacity>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -77,7 +77,7 @@
|
|||||||
<span> </span>
|
<span> </span>
|
||||||
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: #4a68b9"></fa-icon>
|
<fa-icon [icon]="['fas', 'external-link-alt']" [fixedWidth]="true" style="vertical-align: text-top; font-size: 13px; color: #4a68b9"></fa-icon>
|
||||||
</a>
|
</a>
|
||||||
<app-top-nodes-per-channels [nodes$]="nodesRanking$" [widget]="true"></app-top-nodes-per-channels>
|
<app-top-nodes-per-channels [nodes$]="nodesRanking$" [statistics$]="statistics$" [widget]="true"></app-top-nodes-per-channels>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { AfterViewInit, ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
import { AfterViewInit, ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { share } from 'rxjs/operators';
|
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 { SeoService } from '../../services/seo.service';
|
||||||
import { StateService } from '../../services/state.service';
|
import { StateService } from '../../services/state.service';
|
||||||
import { LightningApiService } from '../lightning-api.service';
|
import { LightningApiService } from '../lightning-api.service';
|
||||||
@ -13,7 +13,7 @@ import { LightningApiService } from '../lightning-api.service';
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class LightningDashboardComponent implements OnInit, AfterViewInit {
|
export class LightningDashboardComponent implements OnInit, AfterViewInit {
|
||||||
statistics$: Observable<any>;
|
statistics$: Observable<INodesStatistics>;
|
||||||
nodesRanking$: Observable<INodesRanking>;
|
nodesRanking$: Observable<INodesRanking>;
|
||||||
officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE;
|
officialMempoolSpace = this.stateService.env.OFFICIAL_MEMPOOL_SPACE;
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
import { INodesStatistics } from '../../interfaces/node-api.interface';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-node-statistics',
|
selector: 'app-node-statistics',
|
||||||
@ -8,7 +9,7 @@ import { Observable } from 'rxjs';
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class NodeStatisticsComponent implements OnInit {
|
export class NodeStatisticsComponent implements OnInit {
|
||||||
@Input() statistics$: Observable<any>;
|
@Input() statistics$: Observable<INodesStatistics>;
|
||||||
|
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<app-top-nodes-per-capacity [nodes$]="null" [widget]="false" *ngIf="type === 'capacity'">
|
<app-top-nodes-per-capacity [nodes$]="null" [statistics$]="statistics$" [widget]="false" *ngIf="type === 'capacity'">
|
||||||
</app-top-nodes-per-capacity>
|
</app-top-nodes-per-capacity>
|
||||||
|
|
||||||
<app-top-nodes-per-channels [nodes$]="null" [widget]="false" *ngIf="type === 'channels'">
|
<app-top-nodes-per-channels [nodes$]="null" [statistics$]="statistics$" [widget]="false" *ngIf="type === 'channels'">
|
||||||
</app-top-nodes-per-channels>
|
</app-top-nodes-per-channels>
|
||||||
|
|
||||||
<app-oldest-nodes [widget]="false" *ngIf="type === 'oldest'"></app-oldest-nodes>
|
<app-oldest-nodes [widget]="false" *ngIf="type === 'oldest'"></app-oldest-nodes>
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
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({
|
@Component({
|
||||||
selector: 'app-nodes-ranking',
|
selector: 'app-nodes-ranking',
|
||||||
@ -9,10 +13,15 @@ import { ActivatedRoute } from '@angular/router';
|
|||||||
})
|
})
|
||||||
export class NodesRanking implements OnInit {
|
export class NodesRanking implements OnInit {
|
||||||
type: string;
|
type: string;
|
||||||
|
statistics$: Observable<INodesStatistics>;
|
||||||
|
|
||||||
constructor(private route: ActivatedRoute) {}
|
constructor(
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private lightningApiService: LightningApiService,
|
||||||
|
) {}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
this.statistics$ = this.lightningApiService.getLatestStatistics$().pipe(share());
|
||||||
this.route.data.subscribe(data => {
|
this.route.data.subscribe(data => {
|
||||||
this.type = data.type;
|
this.type = data.type;
|
||||||
});
|
});
|
||||||
|
@ -16,8 +16,8 @@
|
|||||||
<th *ngIf="!widget" class="d-none d-md-table-cell timestamp text-right" i18n="lightning.last_update">Last update</th>
|
<th *ngIf="!widget" class="d-none d-md-table-cell timestamp text-right" i18n="lightning.last_update">Last update</th>
|
||||||
<th *ngIf="!widget" class="d-none d-md-table-cell text-right" i18n="lightning.location">Location</th>
|
<th *ngIf="!widget" class="d-none d-md-table-cell text-right" i18n="lightning.location">Location</th>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody *ngIf="topNodesPerCapacity$ | async as nodes">
|
<tbody *ngIf="topNodesPerCapacity$ | async as data">
|
||||||
<tr *ngFor="let node of nodes;">
|
<tr *ngFor="let node of data.nodes;">
|
||||||
<td class="pool text-left">
|
<td class="pool text-left">
|
||||||
<div class="tooltip-custom d-block w-100">
|
<div class="tooltip-custom d-block w-100">
|
||||||
<a class="link d-block w-100" [routerLink]="['/lightning/node' | relativeUrl, node.publicKey]">
|
<a class="link d-block w-100" [routerLink]="['/lightning/node' | relativeUrl, node.publicKey]">
|
||||||
@ -27,12 +27,14 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
<app-amount [satoshis]="node.capacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
|
<app-amount [satoshis]="node.capacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
|
||||||
|
<span class="capacity-ratio"> ({{ (node?.capacity / data.statistics.totalCapacity * 100) | number:'1.1-1' }}%)</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="d-table-cell fiat text-right" [ngClass]="{'widget': widget}">
|
<td class="d-table-cell fiat text-right" [ngClass]="{'widget': widget}">
|
||||||
<app-fiat [value]="node.capacity"></app-fiat>
|
<app-fiat [value]="node.capacity"></app-fiat>
|
||||||
</td>
|
</td>
|
||||||
<td *ngIf="!widget" class="d-none d-md-table-cell text-right">
|
<td *ngIf="!widget" class="d-none d-md-table-cell text-right">
|
||||||
{{ node.channels | number }}
|
{{ node.channels | number }}
|
||||||
|
<span class="capacity-ratio"> ({{ (node?.channels / data.statistics.totalChannels * 100) | number:'1.1-1' }}%)</span>
|
||||||
</td>
|
</td>
|
||||||
<td *ngIf="!widget" class="d-none d-md-table-cell text-right">
|
<td *ngIf="!widget" class="d-none d-md-table-cell text-right">
|
||||||
<app-timestamp [customFormat]="'yyyy-MM-dd'" [unixTime]="node.firstSeen" [hideTimeSince]="true"></app-timestamp>
|
<app-timestamp [customFormat]="'yyyy-MM-dd'" [unixTime]="node.firstSeen" [hideTimeSince]="true"></app-timestamp>
|
||||||
|
@ -41,6 +41,11 @@ tr, td, th {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.capacity-ratio {
|
||||||
|
font-size: 12px;
|
||||||
|
color: darkgrey;
|
||||||
|
}
|
||||||
|
|
||||||
.fiat {
|
.fiat {
|
||||||
width: 15%;
|
width: 15%;
|
||||||
@media (min-width: 768px) and (max-width: 991px) {
|
@media (min-width: 768px) and (max-width: 991px) {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||||
import { map, Observable } from 'rxjs';
|
import { combineLatest, map, Observable } from 'rxjs';
|
||||||
import { INodesRanking, ITopNodesPerCapacity } from '../../../interfaces/node-api.interface';
|
import { INodesRanking, INodesStatistics, ITopNodesPerCapacity } from '../../../interfaces/node-api.interface';
|
||||||
import { SeoService } from '../../../services/seo.service';
|
import { SeoService } from '../../../services/seo.service';
|
||||||
import { StateService } from '../../../services/state.service';
|
import { StateService } from '../../../services/state.service';
|
||||||
import { GeolocationData } from '../../../shared/components/geolocation/geolocation.component';
|
import { GeolocationData } from '../../../shared/components/geolocation/geolocation.component';
|
||||||
@ -14,9 +14,10 @@ import { LightningApiService } from '../../lightning-api.service';
|
|||||||
})
|
})
|
||||||
export class TopNodesPerCapacity implements OnInit {
|
export class TopNodesPerCapacity implements OnInit {
|
||||||
@Input() nodes$: Observable<INodesRanking>;
|
@Input() nodes$: Observable<INodesRanking>;
|
||||||
|
@Input() statistics$: Observable<INodesStatistics>;
|
||||||
@Input() widget: boolean = false;
|
@Input() widget: boolean = false;
|
||||||
|
|
||||||
topNodesPerCapacity$: Observable<ITopNodesPerCapacity[]>;
|
topNodesPerCapacity$: Observable<{ nodes: ITopNodesPerCapacity[]; statistics: { totalCapacity: number; totalChannels?: number; } }>;
|
||||||
skeletonRows: number[] = [];
|
skeletonRows: number[] = [];
|
||||||
currency$: Observable<string>;
|
currency$: Observable<string>;
|
||||||
|
|
||||||
@ -39,8 +40,12 @@ export class TopNodesPerCapacity implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.widget === false) {
|
if (this.widget === false) {
|
||||||
this.topNodesPerCapacity$ = this.apiService.getTopNodesByCapacity$().pipe(
|
this.topNodesPerCapacity$ = combineLatest([
|
||||||
map((ranking) => {
|
this.apiService.getTopNodesByCapacity$(),
|
||||||
|
this.statistics$
|
||||||
|
])
|
||||||
|
.pipe(
|
||||||
|
map(([ranking, statistics]) => {
|
||||||
for (const i in ranking) {
|
for (const i in ranking) {
|
||||||
ranking[i].geolocation = <GeolocationData>{
|
ranking[i].geolocation = <GeolocationData>{
|
||||||
country: ranking[i].country?.en,
|
country: ranking[i].country?.en,
|
||||||
@ -49,13 +54,28 @@ export class TopNodesPerCapacity implements OnInit {
|
|||||||
iso: ranking[i].iso_code,
|
iso: ranking[i].iso_code,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return ranking;
|
return {
|
||||||
|
nodes: ranking,
|
||||||
|
statistics: {
|
||||||
|
totalCapacity: statistics.latest.total_capacity,
|
||||||
|
totalChannels: statistics.latest.channel_count,
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.topNodesPerCapacity$ = this.nodes$.pipe(
|
this.topNodesPerCapacity$ = combineLatest([
|
||||||
map((ranking) => {
|
this.nodes$,
|
||||||
return ranking.topByCapacity.slice(0, 6);
|
this.statistics$
|
||||||
|
])
|
||||||
|
.pipe(
|
||||||
|
map(([ranking, statistics]) => {
|
||||||
|
return {
|
||||||
|
nodes: ranking.topByCapacity.slice(0, 6),
|
||||||
|
statistics: {
|
||||||
|
totalCapacity: statistics.latest.total_capacity,
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -16,8 +16,8 @@
|
|||||||
<th *ngIf="!widget" class="d-none d-md-table-cell timestamp text-right" i18n="lightning.last_update">Last update</th>
|
<th *ngIf="!widget" class="d-none d-md-table-cell timestamp text-right" i18n="lightning.last_update">Last update</th>
|
||||||
<th class="geolocation d-table-cell text-right" i18n="lightning.location">Location</th>
|
<th class="geolocation d-table-cell text-right" i18n="lightning.location">Location</th>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody *ngIf="topNodesPerChannels$ | async as nodes">
|
<tbody *ngIf="topNodesPerChannels$ | async as data">
|
||||||
<tr *ngFor="let node of nodes;">
|
<tr *ngFor="let node of data.nodes;">
|
||||||
<td class="pool text-left">
|
<td class="pool text-left">
|
||||||
<div class="tooltip-custom d-block w-100">
|
<div class="tooltip-custom d-block w-100">
|
||||||
<a class="link d-block w-100" [routerLink]="['/lightning/node' | relativeUrl, node.publicKey]">
|
<a class="link d-block w-100" [routerLink]="['/lightning/node' | relativeUrl, node.publicKey]">
|
||||||
@ -27,9 +27,11 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
{{ node.channels ? (node.channels | number) : '~' }}
|
{{ node.channels ? (node.channels | number) : '~' }}
|
||||||
|
<span class="capacity-ratio"> ({{ (node?.channels / data.statistics.totalChannels * 100) | number:'1.1-1' }}%)</span>
|
||||||
</td>
|
</td>
|
||||||
<td *ngIf="!widget" class="d-none d-md-table-cell capacity text-right">
|
<td *ngIf="!widget" class="d-none d-md-table-cell capacity text-right">
|
||||||
<app-amount [satoshis]="node.capacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
|
<app-amount [satoshis]="node.capacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
|
||||||
|
<span class="capacity-ratio"> ({{ (node.capacity / data.statistics.totalCapacity * 100) | number:'1.1-1' }}%)</span>
|
||||||
</td>
|
</td>
|
||||||
<td *ngIf="!widget" class="fiat d-none d-md-table-cell text-right">
|
<td *ngIf="!widget" class="fiat d-none d-md-table-cell text-right">
|
||||||
<app-fiat [value]="node.capacity"></app-fiat>
|
<app-fiat [value]="node.capacity"></app-fiat>
|
||||||
|
@ -44,6 +44,11 @@ tr, td, th {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.capacity-ratio {
|
||||||
|
font-size: 12px;
|
||||||
|
color: darkgrey;
|
||||||
|
}
|
||||||
|
|
||||||
.geolocation {
|
.geolocation {
|
||||||
@media (min-width: 768px) and (max-width: 991px) {
|
@media (min-width: 768px) and (max-width: 991px) {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
|
||||||
import { map, Observable } from 'rxjs';
|
import { combineLatest, map, Observable } from 'rxjs';
|
||||||
import { INodesRanking, ITopNodesPerChannels } from '../../../interfaces/node-api.interface';
|
import { INodesRanking, INodesStatistics, ITopNodesPerChannels } from '../../../interfaces/node-api.interface';
|
||||||
import { SeoService } from '../../../services/seo.service';
|
import { SeoService } from '../../../services/seo.service';
|
||||||
import { StateService } from '../../../services/state.service';
|
import { StateService } from '../../../services/state.service';
|
||||||
import { GeolocationData } from '../../../shared/components/geolocation/geolocation.component';
|
import { GeolocationData } from '../../../shared/components/geolocation/geolocation.component';
|
||||||
@ -14,12 +14,13 @@ import { LightningApiService } from '../../lightning-api.service';
|
|||||||
})
|
})
|
||||||
export class TopNodesPerChannels implements OnInit {
|
export class TopNodesPerChannels implements OnInit {
|
||||||
@Input() nodes$: Observable<INodesRanking>;
|
@Input() nodes$: Observable<INodesRanking>;
|
||||||
|
@Input() statistics$: Observable<INodesStatistics>;
|
||||||
@Input() widget: boolean = false;
|
@Input() widget: boolean = false;
|
||||||
|
|
||||||
topNodesPerChannels$: Observable<ITopNodesPerChannels[]>;
|
topNodesPerChannels$: Observable<{ nodes: ITopNodesPerChannels[]; statistics: { totalChannels: number; totalCapacity?: number; } }>;
|
||||||
skeletonRows: number[] = [];
|
skeletonRows: number[] = [];
|
||||||
currency$: Observable<string>;
|
currency$: Observable<string>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private apiService: LightningApiService,
|
private apiService: LightningApiService,
|
||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
@ -37,8 +38,12 @@ export class TopNodesPerChannels implements OnInit {
|
|||||||
this.seoService.setTitle($localize`:@@c50bf442cf99f6fc5f8b687c460f33234b879869:Connectivity Ranking`);
|
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.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(
|
this.topNodesPerChannels$ = combineLatest([
|
||||||
map((ranking) => {
|
this.apiService.getTopNodesByChannels$(),
|
||||||
|
this.statistics$
|
||||||
|
])
|
||||||
|
.pipe(
|
||||||
|
map(([ranking, statistics]) => {
|
||||||
for (const i in ranking) {
|
for (const i in ranking) {
|
||||||
ranking[i].geolocation = <GeolocationData>{
|
ranking[i].geolocation = <GeolocationData>{
|
||||||
country: ranking[i].country?.en,
|
country: ranking[i].country?.en,
|
||||||
@ -47,12 +52,22 @@ export class TopNodesPerChannels implements OnInit {
|
|||||||
iso: ranking[i].iso_code,
|
iso: ranking[i].iso_code,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return ranking;
|
return {
|
||||||
|
nodes: ranking,
|
||||||
|
statistics: {
|
||||||
|
totalChannels: statistics.latest.channel_count,
|
||||||
|
totalCapacity: statistics.latest.total_capacity,
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
this.topNodesPerChannels$ = this.nodes$.pipe(
|
this.topNodesPerChannels$ = combineLatest([
|
||||||
map((ranking) => {
|
this.nodes$,
|
||||||
|
this.statistics$
|
||||||
|
])
|
||||||
|
.pipe(
|
||||||
|
map(([ranking, statistics]) => {
|
||||||
for (const i in ranking.topByChannels) {
|
for (const i in ranking.topByChannels) {
|
||||||
ranking.topByChannels[i].geolocation = <GeolocationData>{
|
ranking.topByChannels[i].geolocation = <GeolocationData>{
|
||||||
country: ranking.topByChannels[i].country?.en,
|
country: ranking.topByChannels[i].country?.en,
|
||||||
@ -61,7 +76,12 @@ export class TopNodesPerChannels implements OnInit {
|
|||||||
iso: ranking.topByChannels[i].iso_code,
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -91,7 +91,7 @@ const routes: Routes = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '**',
|
path: '**',
|
||||||
redirectTo: 'all'
|
redirectTo: 'featured'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user