Merge branch 'master' into master

This commit is contained in:
Felipe Knorr Kuhn 2022-09-08 10:03:36 -07:00 committed by GitHub
commit aa51484b0b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
66 changed files with 3182 additions and 419 deletions

View File

@ -110,6 +110,11 @@ Run the Mempool backend:
``` ```
npm run start npm run start
```
You can also set env var `MEMPOOL_CONFIG_FILE` to specify a custom config file location:
```
MEMPOOL_CONFIG_FILE=/path/to/mempool-config.json npm run start
``` ```
When it's running, you should see output like this: When it's running, you should see output like this:

View File

@ -22,7 +22,10 @@
"main": "index.ts", "main": "index.ts",
"scripts": { "scripts": {
"tsc": "./node_modules/typescript/bin/tsc -p tsconfig.build.json", "tsc": "./node_modules/typescript/bin/tsc -p tsconfig.build.json",
"build": "npm run tsc", "build": "npm run tsc && npm run create-resources",
"create-resources": "cp ./src/tasks/price-feeds/mtgox-weekly.json ./dist/tasks && node dist/api/fetch-version.js",
"package": "npm run build && rm -rf package && mv dist package && mv node_modules package && npm run package-rm-build-deps",
"package-rm-build-deps": "(cd package/node_modules; rm -r typescript @typescript-eslint)",
"start": "node --max-old-space-size=2048 dist/index.js", "start": "node --max-old-space-size=2048 dist/index.js",
"start-production": "node --max-old-space-size=4096 dist/index.js", "start-production": "node --max-old-space-size=4096 dist/index.js",
"test": "./node_modules/.bin/jest --coverage", "test": "./node_modules/.bin/jest --coverage",

View File

@ -1,60 +1,37 @@
import * as fs from 'fs'; import fs from 'fs';
import * as os from 'os'; import path from 'path';
import logger from '../logger'; import os from 'os';
import { IBackendInfo } from '../mempool.interfaces'; import { IBackendInfo } from '../mempool.interfaces';
const { spawnSync } = require('child_process');
class BackendInfo { class BackendInfo {
private gitCommitHash = ''; private backendInfo: IBackendInfo;
private hostname = '';
private version = '';
constructor() { constructor() {
this.setLatestCommitHash(); // This file is created by ./fetch-version.ts during building
this.setVersion(); const versionFile = path.join(__dirname, 'version.json')
this.hostname = os.hostname(); var versionInfo;
} if (fs.existsSync(versionFile)) {
versionInfo = JSON.parse(fs.readFileSync(versionFile).toString());
public getBackendInfo(): IBackendInfo { } else {
return { // Use dummy values if `versionFile` doesn't exist (e.g., during testing)
hostname: this.hostname, versionInfo = {
gitCommit: this.gitCommitHash, version: '?',
version: this.version, gitCommit: '?'
};
}
this.backendInfo = {
hostname: os.hostname(),
version: versionInfo.version,
gitCommit: versionInfo.gitCommit
}; };
} }
public getBackendInfo(): IBackendInfo {
return this.backendInfo;
}
public getShortCommitHash() { public getShortCommitHash() {
return this.gitCommitHash.slice(0, 7); return this.backendInfo.gitCommit.slice(0, 7);
}
private setLatestCommitHash(): void {
//TODO: share this logic with `generate-config.js`
if (process.env.DOCKER_COMMIT_HASH) {
this.gitCommitHash = process.env.DOCKER_COMMIT_HASH;
} else {
try {
const gitRevParse = spawnSync('git', ['rev-parse', '--short', 'HEAD']);
if (!gitRevParse.error) {
const output = gitRevParse.stdout.toString('utf-8').replace(/[\n\r\s]+$/, '');
this.gitCommitHash = output ? output : '?';
} else if (gitRevParse.error.code === 'ENOENT') {
console.log('git not found, cannot parse git hash');
this.gitCommitHash = '?';
}
} catch (e: any) {
console.log('Could not load git commit info: ' + e.message);
this.gitCommitHash = '?';
}
}
}
private setVersion(): void {
try {
const packageJson = fs.readFileSync('package.json').toString();
this.version = JSON.parse(packageJson).version;
} catch (e) {
throw new Error(e instanceof Error ? e.message : 'Error');
}
} }
} }

View File

@ -39,7 +39,8 @@ class ChannelsApi {
FROM channels FROM channels
JOIN nodes AS nodes_1 on nodes_1.public_key = channels.node1_public_key JOIN nodes AS nodes_1 on nodes_1.public_key = channels.node1_public_key
JOIN nodes AS nodes_2 on nodes_2.public_key = channels.node2_public_key JOIN nodes AS nodes_2 on nodes_2.public_key = channels.node2_public_key
WHERE nodes_1.latitude IS NOT NULL AND nodes_1.longitude IS NOT NULL WHERE channels.status = 1
AND nodes_1.latitude IS NOT NULL AND nodes_1.longitude IS NOT NULL
AND nodes_2.latitude IS NOT NULL AND nodes_2.longitude IS NOT NULL AND nodes_2.latitude IS NOT NULL AND nodes_2.longitude IS NOT NULL
`; `;
@ -374,6 +375,7 @@ class ChannelsApi {
'transaction_vout': channel.transaction_vout, 'transaction_vout': channel.transaction_vout,
'closing_transaction_id': channel.closing_transaction_id, 'closing_transaction_id': channel.closing_transaction_id,
'closing_reason': channel.closing_reason, 'closing_reason': channel.closing_reason,
'closing_date': channel.closing_date,
'updated_at': channel.updated_at, 'updated_at': channel.updated_at,
'created': channel.created, 'created': channel.created,
'status': channel.status, 'status': channel.status,

View File

@ -5,6 +5,49 @@ import { ILightningApi } from '../lightning/lightning-api.interface';
import { ITopNodesPerCapacity, ITopNodesPerChannels } from '../../mempool.interfaces'; import { ITopNodesPerCapacity, ITopNodesPerChannels } from '../../mempool.interfaces';
class NodesApi { class NodesApi {
public async $getWorldNodes(): Promise<any> {
try {
let query = `
SELECT nodes.public_key as publicKey, IF(nodes.alias = '', SUBSTRING(nodes.public_key, 1, 20), alias) as alias,
CAST(COALESCE(nodes.capacity, 0) as INT) as capacity,
CAST(COALESCE(nodes.channels, 0) as INT) as channels,
nodes.longitude, nodes.latitude,
geo_names_country.names as country, geo_names_iso.names as isoCode
FROM nodes
LEFT 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_iso ON geo_names_iso.id = nodes.country_id AND geo_names_iso.type = 'country_iso_code'
WHERE status = 1 AND nodes.as_number IS NOT NULL
ORDER BY capacity
`;
const [nodes]: any[] = await DB.query(query);
for (let i = 0; i < nodes.length; ++i) {
nodes[i].country = JSON.parse(nodes[i].country);
}
query = `
SELECT MAX(nodes.capacity) as maxLiquidity, MAX(nodes.channels) as maxChannels
FROM nodes
WHERE status = 1 AND nodes.as_number IS NOT NULL
`;
const [maximums]: any[] = await DB.query(query);
return {
maxLiquidity: maximums[0].maxLiquidity,
maxChannels: maximums[0].maxChannels,
nodes: nodes.map(node => [
node.longitude, node.latitude,
node.publicKey, node.alias, node.capacity, node.channels,
node.country, node.isoCode
])
};
} catch (e) {
logger.err(`Can't get world nodes list. Reason: ${e instanceof Error ? e.message : e}`);
}
}
public async $getNode(public_key: string): Promise<any> { public async $getNode(public_key: string): Promise<any> {
try { try {
// General info // General info
@ -133,10 +176,13 @@ class NodesApi {
CAST(COALESCE(nodes.capacity, 0) as INT) as capacity, CAST(COALESCE(nodes.capacity, 0) as INT) as capacity,
CAST(COALESCE(nodes.channels, 0) as INT) as channels, CAST(COALESCE(nodes.channels, 0) as INT) as channels,
UNIX_TIMESTAMP(nodes.first_seen) as firstSeen, UNIX_TIMESTAMP(nodes.updated_at) as updatedAt, UNIX_TIMESTAMP(nodes.first_seen) as firstSeen, UNIX_TIMESTAMP(nodes.updated_at) as updatedAt,
geo_names_city.names as city, geo_names_country.names as country geo_names_city.names as city, geo_names_country.names as country,
geo_names_iso.names as iso_code, geo_names_subdivision.names as subdivision
FROM nodes FROM nodes
LEFT 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_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' LEFT JOIN geo_names geo_names_city ON geo_names_city.id = nodes.city_id AND geo_names_city.type = 'city'
LEFT JOIN geo_names geo_names_iso ON geo_names_iso.id = nodes.country_id AND geo_names_iso.type = 'country_iso_code'
LEFT JOIN geo_names geo_names_subdivision on geo_names_subdivision.id = nodes.subdivision_id AND geo_names_subdivision.type = 'division'
ORDER BY capacity DESC ORDER BY capacity DESC
LIMIT 100 LIMIT 100
`; `;
@ -175,10 +221,13 @@ class NodesApi {
CAST(COALESCE(nodes.channels, 0) as INT) as channels, CAST(COALESCE(nodes.channels, 0) as INT) as channels,
CAST(COALESCE(nodes.capacity, 0) as INT) as capacity, CAST(COALESCE(nodes.capacity, 0) as INT) as capacity,
UNIX_TIMESTAMP(nodes.first_seen) as firstSeen, UNIX_TIMESTAMP(nodes.updated_at) as updatedAt, UNIX_TIMESTAMP(nodes.first_seen) as firstSeen, UNIX_TIMESTAMP(nodes.updated_at) as updatedAt,
geo_names_city.names as city, geo_names_country.names as country geo_names_city.names as city, geo_names_country.names as country,
geo_names_iso.names as iso_code, geo_names_subdivision.names as subdivision
FROM nodes FROM nodes
LEFT 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_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' LEFT JOIN geo_names geo_names_city ON geo_names_city.id = nodes.city_id AND geo_names_city.type = 'city'
LEFT JOIN geo_names geo_names_iso ON geo_names_iso.id = nodes.country_id AND geo_names_iso.type = 'country_iso_code'
LEFT JOIN geo_names geo_names_subdivision on geo_names_subdivision.id = nodes.subdivision_id AND geo_names_subdivision.type = 'division'
ORDER BY channels DESC ORDER BY channels DESC
LIMIT 100 LIMIT 100
`; `;
@ -221,11 +270,14 @@ class NodesApi {
CAST(COALESCE(node_stats.channels, 0) as INT) as channels, CAST(COALESCE(node_stats.channels, 0) as INT) as channels,
CAST(COALESCE(node_stats.capacity, 0) as INT) as capacity, CAST(COALESCE(node_stats.capacity, 0) as INT) as capacity,
UNIX_TIMESTAMP(nodes.first_seen) as firstSeen, UNIX_TIMESTAMP(nodes.updated_at) as updatedAt, UNIX_TIMESTAMP(nodes.first_seen) as firstSeen, UNIX_TIMESTAMP(nodes.updated_at) as updatedAt,
geo_names_city.names as city, geo_names_country.names as country geo_names_city.names as city, geo_names_country.names as country,
geo_names_iso.names as iso_code, geo_names_subdivision.names as subdivision
FROM node_stats FROM node_stats
RIGHT JOIN nodes ON nodes.public_key = node_stats.public_key RIGHT JOIN nodes ON nodes.public_key = node_stats.public_key
LEFT 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_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' LEFT JOIN geo_names geo_names_city ON geo_names_city.id = nodes.city_id AND geo_names_city.type = 'city'
LEFT JOIN geo_names geo_names_iso ON geo_names_iso.id = nodes.country_id AND geo_names_iso.type = 'country_iso_code'
LEFT JOIN geo_names geo_names_subdivision on geo_names_subdivision.id = nodes.subdivision_id AND geo_names_subdivision.type = 'division'
WHERE added = FROM_UNIXTIME(${latestDate}) WHERE added = FROM_UNIXTIME(${latestDate})
ORDER BY first_seen ORDER BY first_seen
LIMIT 100 LIMIT 100

View File

@ -9,6 +9,7 @@ class NodesRoutes {
public initRoutes(app: Application) { public initRoutes(app: Application) {
app app
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/world', this.$getWorldNodes)
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/country/:country', this.$getNodesPerCountry) .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/country/:country', this.$getNodesPerCountry)
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/search/:search', this.$searchNode) .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/search/:search', this.$searchNode)
.get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/isp-ranking', this.$getISPRanking) .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/isp-ranking', this.$getISPRanking)
@ -115,7 +116,6 @@ class NodesRoutes {
private async $getISPRanking(req: Request, res: Response): Promise<void> { private async $getISPRanking(req: Request, res: Response): Promise<void> {
try { try {
const nodesPerAs = await nodesApi.$getNodesISPRanking(); const nodesPerAs = await nodesApi.$getNodesISPRanking();
res.header('Pragma', 'public'); res.header('Pragma', 'public');
res.header('Cache-control', 'public'); res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString()); res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
@ -125,6 +125,18 @@ class NodesRoutes {
} }
} }
private async $getWorldNodes(req: Request, res: Response) {
try {
const worldNodes = await nodesApi.$getWorldNodes();
res.header('Pragma', 'public');
res.header('Cache-control', 'public');
res.setHeader('Expires', new Date(Date.now() + 1000 * 600).toUTCString());
res.json(worldNodes);
} catch (e) {
res.status(500).send(e instanceof Error ? e.message : e);
}
}
private async $getNodesPerCountry(req: Request, res: Response) { private async $getNodesPerCountry(req: Request, res: Response) {
try { try {
const [country]: any[] = await DB.query( const [country]: any[] = await DB.query(

View File

@ -0,0 +1,37 @@
import fs from 'fs';
import path from "path";
const { spawnSync } = require('child_process');
function getVersion(): string {
const packageJson = fs.readFileSync('package.json').toString();
return JSON.parse(packageJson).version;
}
function getGitCommit(): string {
if (process.env.MEMPOOL_COMMIT_HASH) {
return process.env.MEMPOOL_COMMIT_HASH;
} else {
const gitRevParse = spawnSync('git', ['rev-parse', '--short', 'HEAD']);
if (!gitRevParse.error) {
const output = gitRevParse.stdout.toString('utf-8').replace(/[\n\r\s]+$/, '');
if (output) {
return output;
} else {
console.log('Could not fetch git commit: No repo available');
}
} else if (gitRevParse.error.code === 'ENOENT') {
console.log('Could not fetch git commit: Command `git` is unavailable');
}
}
return '?';
}
const versionInfo = {
version: getVersion(),
gitCommit: getGitCommit()
}
fs.writeFileSync(
path.join(__dirname, 'version.json'),
JSON.stringify(versionInfo, null, 2) + "\n"
);

View File

@ -1,4 +1,6 @@
const configFile = require('../mempool-config.json'); const configFromFile = require(
process.env.MEMPOOL_CONFIG_FILE ? process.env.MEMPOOL_CONFIG_FILE : '../mempool-config.json'
);
interface IConfig { interface IConfig {
MEMPOOL: { MEMPOOL: {
@ -249,7 +251,7 @@ class Config implements IConfig {
MAXMIND: IConfig['MAXMIND']; MAXMIND: IConfig['MAXMIND'];
constructor() { constructor() {
const configs = this.merge(configFile, defaults); const configs = this.merge(configFromFile, defaults);
this.MEMPOOL = configs.MEMPOOL; this.MEMPOOL = configs.MEMPOOL;
this.ESPLORA = configs.ESPLORA; this.ESPLORA = configs.ESPLORA;
this.ELECTRUM = configs.ELECTRUM; this.ELECTRUM = configs.ELECTRUM;

View File

@ -12,9 +12,11 @@ import { ResultSetHeader } from 'mysql2';
import fundingTxFetcher from './sync-tasks/funding-tx-fetcher'; import fundingTxFetcher from './sync-tasks/funding-tx-fetcher';
import NodesSocketsRepository from '../../repositories/NodesSocketsRepository'; import NodesSocketsRepository from '../../repositories/NodesSocketsRepository';
import { Common } from '../../api/common'; import { Common } from '../../api/common';
import blocks from '../../api/blocks';
class NetworkSyncService { class NetworkSyncService {
loggerTimer = 0; loggerTimer = 0;
closedChannelsScanBlock = 0;
constructor() {} constructor() {}
@ -240,10 +242,22 @@ class NetworkSyncService {
} }
private async $scanForClosedChannels(): Promise<void> { private async $scanForClosedChannels(): Promise<void> {
if (this.closedChannelsScanBlock === blocks.getCurrentBlockHeight()) {
logger.debug(`We've already scan closed channels for this block, skipping.`);
return;
}
let progress = 0; let progress = 0;
try { try {
logger.info(`Starting closed channels scan`); let log = `Starting closed channels scan`;
if (this.closedChannelsScanBlock > 0) {
log += `. Last scan was at block ${this.closedChannelsScanBlock}`;
} else {
log += ` for the first time`;
}
logger.info(log);
const channels = await channelsApi.$getChannelsByStatus([0, 1]); const channels = await channelsApi.$getChannelsByStatus([0, 1]);
for (const channel of channels) { for (const channel of channels) {
const spendingTx = await bitcoinApi.$getOutspend(channel.transaction_id, channel.transaction_vout); const spendingTx = await bitcoinApi.$getOutspend(channel.transaction_id, channel.transaction_vout);
@ -263,7 +277,9 @@ class NetworkSyncService {
this.loggerTimer = new Date().getTime() / 1000; this.loggerTimer = new Date().getTime() / 1000;
} }
} }
logger.info(`Closed channels scan complete.`);
this.closedChannelsScanBlock = blocks.getCurrentBlockHeight();
logger.info(`Closed channels scan completed at block ${this.closedChannelsScanBlock}`);
} catch (e) { } catch (e) {
logger.err('$scanForClosedChannels() error: ' + (e instanceof Error ? e.message : e)); logger.err('$scanForClosedChannels() error: ' + (e instanceof Error ? e.message : e));
} }

View File

@ -1,4 +1,5 @@
import * as fs from 'fs'; import * as fs from 'fs';
import path from "path";
import { Common } from '../api/common'; import { Common } from '../api/common';
import config from '../config'; import config from '../config';
import logger from '../logger'; import logger from '../logger';
@ -159,7 +160,7 @@ class PriceUpdater {
const existingPriceTimes = await PricesRepository.$getPricesTimes(); const existingPriceTimes = await PricesRepository.$getPricesTimes();
// Insert MtGox weekly prices // Insert MtGox weekly prices
const pricesJson: any[] = JSON.parse(fs.readFileSync('./src/tasks/price-feeds/mtgox-weekly.json').toString()); const pricesJson: any[] = JSON.parse(fs.readFileSync(path.join(__dirname, 'mtgox-weekly.json')).toString());
const prices = this.getEmptyPricesObj(); const prices = this.getEmptyPricesObj();
let insertedCount: number = 0; let insertedCount: number = 0;
for (const price of pricesJson) { for (const price of pricesJson) {

View File

@ -1,7 +1,7 @@
FROM node:16.16.0-buster-slim AS builder FROM node:16.16.0-buster-slim AS builder
ARG commitHash ARG commitHash
ENV DOCKER_COMMIT_HASH=${commitHash} ENV MEMPOOL_COMMIT_HASH=${commitHash}
WORKDIR /build WORKDIR /build
COPY . . COPY . .
@ -9,18 +9,15 @@ COPY . .
RUN apt-get update RUN apt-get update
RUN apt-get install -y build-essential python3 pkg-config RUN apt-get install -y build-essential python3 pkg-config
RUN npm install --omit=dev --omit=optional RUN npm install --omit=dev --omit=optional
RUN npm run build RUN npm run package
FROM node:16.16.0-buster-slim FROM node:16.16.0-buster-slim
WORKDIR /backend WORKDIR /backend
COPY --from=builder /build/ . RUN chown 1000:1000 ./
COPY --from=builder --chown=1000:1000 /build/package ./package/
RUN chmod +x /backend/start.sh COPY --from=builder --chown=1000:1000 /build/mempool-config.json /build/start.sh /build/wait-for-it.sh ./
RUN chmod +x /backend/wait-for-it.sh
RUN chown -R 1000:1000 /backend && chmod -R 755 /backend
USER 1000 USER 1000

2
docker/backend/start.sh Normal file → Executable file
View File

@ -205,4 +205,4 @@ sed -i "s!__LND_REST_API_URL__!${__LND_REST_API_URL__}!g" mempool-config.json
# CLN # CLN
sed -i "s!__CLIGHTNING_SOCKET__!${__CLIGHTNING_SOCKET__}!g" mempool-config.json sed -i "s!__CLIGHTNING_SOCKET__!${__CLIGHTNING_SOCKET__}!g" mempool-config.json
node /backend/dist/index.js node /backend/package/index.js

0
docker/backend/wait-for-it.sh Normal file → Executable file
View File

View File

@ -1,10 +1,7 @@
#!/bin/sh #!/bin/sh
#backend #backend
gitMaster="\.\.\/\.git\/refs\/heads\/master"
git ls-remote https://github.com/mempool/mempool.git "$1^{}" | awk '{ print $1}' > ./backend/master
cp ./docker/backend/* ./backend/ cp ./docker/backend/* ./backend/
sed -i "s/${gitMaster}/master/g" ./backend/src/api/backend-info.ts
#frontend #frontend
localhostIP="127.0.0.1" localhostIP="127.0.0.1"

View File

@ -1,12 +1,12 @@
<div class="box preview-box" *ngIf="address && !error"> <div class="box preview-box" *ngIf="address && !error">
<h2 class="preview-header" i18n="shared.address">Address</h2>
<div class="row"> <div class="row">
<div class="col-md"> <div class="col-md">
<div class="title-address"> <div class="row d-flex justify-content-between">
<h1 i18n="shared.address">Address</h1> <div class="title-wrapper">
<h1 class="title truncated"><span class="first">{{addressString.slice(0,-4)}}</span><span class="last-four">{{addressString.slice(-4)}}</span></h1>
</div>
</div> </div>
<a [routerLink]="['/address/' | relativeUrl, addressString]" class="address-link" >
<span class="truncated-address">{{addressString.slice(0,-4)}}</span><span class="last-four">{{addressString.slice(-4)}}</span>
</a>
<table class="table table-borderless table-striped"> <table class="table table-borderless table-striped">
<tbody> <tbody>
<tr *ngIf="addressInfo && addressInfo.unconfidential"> <tr *ngIf="addressInfo && addressInfo.unconfidential">

View File

@ -1,6 +1,5 @@
h1 { .title-wrapper {
font-size: 52px; padding: 0 15px;
margin: 0;
} }
.qr-wrapper { .qr-wrapper {
@ -23,27 +22,9 @@ h1 {
.table { .table {
font-size: 32px; font-size: 32px;
margin-top: 48px;
::ng-deep .symbol { ::ng-deep .symbol {
font-size: 24px; font-size: 24px;
} }
} }
.address-link {
font-size: 24px;
margin-bottom: 0.5em;
display: flex;
flex-direction: row;
align-items: baseline;
.truncated-address {
text-overflow: ellipsis;
overflow: hidden;
max-width: calc(640px - 4em);
display: inline-block;
white-space: nowrap;
}
.last-four {
display: inline-block;
white-space: nowrap;
}
}

View File

@ -19,6 +19,7 @@ import { AddressInformation } from 'src/app/interfaces/node-api.interface';
export class AddressPreviewComponent implements OnInit, OnDestroy { export class AddressPreviewComponent implements OnInit, OnDestroy {
network = ''; network = '';
rawAddress: string;
address: Address; address: Address;
addressString: string; addressString: string;
isLoadingAddress = true; isLoadingAddress = true;
@ -55,7 +56,8 @@ export class AddressPreviewComponent implements OnInit, OnDestroy {
this.mainSubscription = this.route.paramMap this.mainSubscription = this.route.paramMap
.pipe( .pipe(
switchMap((params: ParamMap) => { switchMap((params: ParamMap) => {
this.openGraphService.waitFor('address-data'); this.rawAddress = params.get('id') || '';
this.openGraphService.waitFor('address-data-' + this.rawAddress);
this.error = undefined; this.error = undefined;
this.isLoadingAddress = true; this.isLoadingAddress = true;
this.loadedConfirmedTxCount = 0; this.loadedConfirmedTxCount = 0;
@ -73,7 +75,7 @@ export class AddressPreviewComponent implements OnInit, OnDestroy {
this.isLoadingAddress = false; this.isLoadingAddress = false;
this.error = err; this.error = err;
console.log(err); console.log(err);
this.openGraphService.fail('address-data'); this.openGraphService.fail('address-data-' + this.rawAddress);
return of(null); return of(null);
}) })
); );
@ -91,7 +93,7 @@ export class AddressPreviewComponent implements OnInit, OnDestroy {
this.address = address; this.address = address;
this.updateChainStats(); this.updateChainStats();
this.isLoadingAddress = false; this.isLoadingAddress = false;
this.openGraphService.waitOver('address-data'); this.openGraphService.waitOver('address-data-' + this.rawAddress);
}) })
) )
.subscribe(() => {}, .subscribe(() => {},
@ -99,7 +101,7 @@ export class AddressPreviewComponent implements OnInit, OnDestroy {
console.log(error); console.log(error);
this.error = error; this.error = error;
this.isLoadingAddress = false; this.isLoadingAddress = false;
this.openGraphService.fail('address-data'); this.openGraphService.fail('address-data-' + this.rawAddress);
} }
); );
} }

View File

@ -4,7 +4,6 @@ import { map } from 'rxjs/operators';
import { moveDec } from 'src/app/bitcoin.utils'; import { moveDec } from 'src/app/bitcoin.utils';
import { AssetsService } from 'src/app/services/assets.service'; import { AssetsService } from 'src/app/services/assets.service';
import { ElectrsApiService } from 'src/app/services/electrs-api.service'; import { ElectrsApiService } from 'src/app/services/electrs-api.service';
import { formatNumber } from '@angular/common';
import { environment } from 'src/environments/environment'; import { environment } from 'src/environments/environment';
@Component({ @Component({

View File

@ -1,19 +1,16 @@
<div class="box preview-box" *ngIf="!error"> <div class="box preview-box" *ngIf="!error">
<h2 class="preview-header" i18n="shared.block-title">Block</h2>
<div class="row"> <div class="row">
<div class="col-sm"> <div class="col-sm">
<h1 class="block-title"> <div class="row d-flex justify-content-between">
<ng-template [ngIf]="blockHeight === 0"><ng-container i18n="@@2303359202781425764">Genesis</ng-container> <div class="title-wrapper">
<span class="next-previous-blocks"> <h1 class="title">
<a [routerLink]="['/block/' | relativeUrl, blockHash]">{{ blockHeight }}</a> <ng-template [ngIf]="blockHeight === 0"><ng-container i18n="@@2303359202781425764">Genesis</ng-container></ng-template>
</span> <ng-template [ngIf]="blockHeight" i18n="shared.block-title">{{ blockHeight }}</ng-template>
</ng-template> </h1>
<ng-template [ngIf]="blockHeight" i18n="shared.block-title">Block <ng-container *ngTemplateOutlet="blockTemplateContent"></ng-container></ng-template> </div>
<ng-template #blockTemplateContent> </div>
<span class="next-previous-blocks"> <a class="subtitle truncated" [routerLink]="['/block/' | relativeUrl, blockHash]"><span class="first">{{blockHash.slice(0,-4)}}</span><span class="last-four">{{blockHash.slice(-4)}}</span></a>
<a [routerLink]="['/block/' | relativeUrl, blockHash]">{{ blockHeight }}</a>
</span>
</ng-template>
</h1>
<table class="table table-borderless table-striped"> <table class="table table-borderless table-striped">
<tbody> <tbody>
<!-- <tr> <!-- <tr>

View File

@ -1,14 +1,10 @@
.block-title {
margin-bottom: 48px;
font-size: 52px;
::ng-deep .next-previous-blocks {
font-size: 52px;
}
}
.table { .table {
font-size: 32px; font-size: 32px;
margin-top: 6px;
}
.title-wrapper {
padding-left: 15px;
} }
.chart-container { .chart-container {

View File

@ -20,6 +20,7 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
block: BlockExtended; block: BlockExtended;
blockHeight: number; blockHeight: number;
blockHash: string; blockHash: string;
rawId: string;
isLoadingBlock = true; isLoadingBlock = true;
strippedTransactions: TransactionStripped[]; strippedTransactions: TransactionStripped[];
overviewTransitionDirection: string; overviewTransitionDirection: string;
@ -48,8 +49,9 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
const block$ = this.route.paramMap.pipe( const block$ = this.route.paramMap.pipe(
switchMap((params: ParamMap) => { switchMap((params: ParamMap) => {
this.openGraphService.waitFor('block-viz'); this.rawId = params.get('id') || '';
this.openGraphService.waitFor('block-data'); this.openGraphService.waitFor('block-viz-' + this.rawId);
this.openGraphService.waitFor('block-data-' + this.rawId);
const blockHash: string = params.get('id') || ''; const blockHash: string = params.get('id') || '';
this.block = undefined; this.block = undefined;
@ -80,8 +82,8 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
}), }),
catchError((err) => { catchError((err) => {
this.error = err; this.error = err;
this.openGraphService.fail('block-data'); this.openGraphService.fail('block-data-' + this.rawId);
this.openGraphService.fail('block-viz'); this.openGraphService.fail('block-viz-' + this.rawId);
return of(null); return of(null);
}), }),
); );
@ -103,7 +105,7 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
this.isLoadingOverview = true; this.isLoadingOverview = true;
this.overviewError = null; this.overviewError = null;
this.openGraphService.waitOver('block-data'); this.openGraphService.waitOver('block-data-' + this.rawId);
}), }),
throttleTime(50, asyncScheduler, { leading: true, trailing: true }), throttleTime(50, asyncScheduler, { leading: true, trailing: true }),
shareReplay(1) shareReplay(1)
@ -116,7 +118,7 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
.pipe( .pipe(
catchError((err) => { catchError((err) => {
this.overviewError = err; this.overviewError = err;
this.openGraphService.fail('block-viz'); this.openGraphService.fail('block-viz-' + this.rawId);
return of([]); return of([]);
}), }),
switchMap((transactions) => { switchMap((transactions) => {
@ -136,8 +138,8 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
(error) => { (error) => {
this.error = error; this.error = error;
this.isLoadingOverview = false; this.isLoadingOverview = false;
this.openGraphService.fail('block-viz'); this.openGraphService.fail('block-viz-' + this.rawId);
this.openGraphService.fail('block-data'); this.openGraphService.fail('block-data-' + this.rawId);
if (this.blockGraph) { if (this.blockGraph) {
this.blockGraph.destroy(); this.blockGraph.destroy();
} }
@ -163,6 +165,6 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
} }
onGraphReady(): void { onGraphReady(): void {
this.openGraphService.waitOver('block-viz'); this.openGraphService.waitOver('block-viz-' + this.rawId);
} }
} }

View File

@ -7,12 +7,12 @@
</span> </span>
<div [ngSwitch]="network.val"> <div [ngSwitch]="network.val">
<span *ngSwitchCase="'signet'" class="network signet"><app-svg-images name="signet" width="35" height="35" viewBox="0 0 65 65" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Signet <ng-template [ngIf]="(lightning$ | async)">Lightning</ng-template></span> <span *ngSwitchCase="'signet'" class="network signet"><app-svg-images name="signet" width="35" height="35" viewBox="0 0 65 65" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Signet</span>
<span *ngSwitchCase="'testnet'" class="network testnet"><app-svg-images name="testnet" width="35" height="35" viewBox="0 0 65 65" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Testnet <ng-template [ngIf]="(lightning$ | async)">Lightning</ng-template></span> <span *ngSwitchCase="'testnet'" class="network testnet"><app-svg-images name="testnet" width="35" height="35" viewBox="0 0 65 65" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Testnet</span>
<span *ngSwitchCase="'bisq'" class="network bisq"><app-svg-images name="bisq" width="35" height="35" viewBox="0 0 75 75" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Bisq</span> <span *ngSwitchCase="'bisq'" class="network bisq"><app-svg-images name="bisq" width="35" height="35" viewBox="0 0 75 75" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Bisq</span>
<span *ngSwitchCase="'liquid'" class="network liquid"><app-svg-images name="liquid" width="35" height="35" viewBox="0 0 125 125" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Liquid</span> <span *ngSwitchCase="'liquid'" class="network liquid"><app-svg-images name="liquid" width="35" height="35" viewBox="0 0 125 125" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Liquid</span>
<span *ngSwitchCase="'liquidtestnet'" class="network liquidtestnet"><app-svg-images name="liquidtestnet" width="35" height="35" viewBox="0 0 125 125" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Liquid Testnet</span> <span *ngSwitchCase="'liquidtestnet'" class="network liquidtestnet"><app-svg-images name="liquidtestnet" width="35" height="35" viewBox="0 0 125 125" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Liquid Testnet</span>
<span *ngSwitchDefault class="network mainnet"><app-svg-images name="bitcoin" width="35" height="35" viewBox="0 0 65 65" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Mainnet <ng-template [ngIf]="(lightning$ | async)">Lightning</ng-template></span> <span *ngSwitchDefault class="network mainnet"><app-svg-images name="bitcoin" width="35" height="35" viewBox="0 0 65 65" style="width: 40px; height: 48px;" class="mainnet mr-1"></app-svg-images> Mainnet</span>
</div> </div>
</header> </header>
<router-outlet></router-outlet> <router-outlet></router-outlet>

View File

@ -33,4 +33,66 @@
justify-content: flex-start; justify-content: flex-start;
align-items: center; align-items: center;
} }
::ng-deep .preview-header {
position: absolute;
top: -80px;
left: 0;
right: 0;
width: 100%;
padding: 0 220px;
text-align: center;
overflow: hidden;
text-overflow: ellipsis;
z-index: 101;
line-height: 80px;
text-transform: capitalize;
}
::ng-deep .title {
font-size: 52px;
}
::ng-deep .subtitle {
font-size: 28px;
}
::ng-deep .title, ::ng-deep .subtitle {
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin: 0;
display: inline-block;
&.truncated {
text-overflow: unset;
display: flex;
flex-direction: row;
align-items: baseline;
.first {
flex-grow: 1;
flex-shrink: 1;
overflow: hidden;
text-overflow: ellipsis;
margin-right: -2px;
}
.last-four {
flex-shrink: 0;
flex-grow: 0;
}
}
}
::ng-deep .title-wrapper {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
margin: 0;
width: 0;
flex-grow: 1;
flex-shrink: 1;
}
} }

View File

@ -1,10 +1,9 @@
<div class="box preview-box" *ngIf="tx && !error"> <div class="box preview-box" *ngIf="tx && !error">
<h2 class="preview-header" i18n="shared.transaction">Transaction</h2>
<div class="page-title"> <div class="row d-flex justify-content-between full-width-row">
<h1 i18n="shared.transaction">Transaction</h1> <div class="title-wrapper">
<a class="tx-link" [routerLink]="['/tx/' | relativeUrl, txId]"> <h1 class="title truncated"><span class="first">{{txId.slice(0,-4)}}</span><span class="last-four">{{txId.slice(-4)}}</span></h1>
<span class="truncated">{{txId.slice(0,-4)}}</span><span class="last-four">{{txId.slice(-4)}}</span> </div>
</a>
<div *ngIf="network !== 'liquid' && network !== 'liquidtestnet'" class="features"> <div *ngIf="network !== 'liquid' && network !== 'liquidtestnet'" class="features">
<app-tx-features [tx]="tx"></app-tx-features> <app-tx-features [tx]="tx"></app-tx-features>
<span *ngIf="cpfpInfo && cpfpInfo.bestDescendant" class="badge badge-primary mr-1"> <span *ngIf="cpfpInfo && cpfpInfo.bestDescendant" class="badge badge-primary mr-1">
@ -15,7 +14,6 @@
</span> </span>
</div> </div>
</div> </div>
<div class="top-data row"> <div class="top-data row">
<span class="field col-sm-4 text-left"> <span class="field col-sm-4 text-left">
<ng-template [ngIf]="isLiquid && haveBlindedOutputValues(tx)" [ngIfElse]="defaultAmount" i18n="shared.confidential">Confidential</ng-template> <ng-template [ngIf]="isLiquid && haveBlindedOutputValues(tx)" [ngIfElse]="defaultAmount" i18n="shared.confidential">Confidential</ng-template>
@ -23,7 +21,7 @@
<app-amount [satoshis]="totalValue"></app-amount> <app-amount [satoshis]="totalValue"></app-amount>
</ng-template> </ng-template>
</span> </span>
<span class="field col-sm-4 text-center">&lrm;{{ (tx.status.confirmed ? tx.status.block_time : transactionTime) * 1000 | date:'yyyy-MM-dd HH:mm' }}</span> <span class="field col-sm-4 text-center"><ng-container *ngIf="transactionTime > 0">&lrm;{{ transactionTime * 1000 | date:'yyyy-MM-dd HH:mm' }}</ng-container></span>
<span class="field col-sm-4 text-right"><span class="label" i18n="transaction.fee|Transaction fee">Fee </span>{{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></span> <span class="field col-sm-4 text-right"><span class="label" i18n="transaction.fee|Transaction fee">Fee </span>{{ tx.fee | number }} <span class="symbol" i18n="shared.sat|sat">sat</span></span>
</div> </div>

View File

@ -26,56 +26,9 @@
margin-top: 0px; margin-top: 0px;
} }
.page-title { .features {
display: flex; font-size: 24px;
flex-direction: row; margin-left: 1em;
justify-content: space-between;
align-items: baseline;
margin-bottom: 2px;
max-width: 100%;
h1 {
font-size: 52px;
margin: 0;
line-height: 1;
}
.features {
font-size: 24px;
}
& > * {
flex-grow: 0;
flex-shrink: 0;
}
.tx-link {
flex-grow: 1;
flex-shrink: 1;
margin: 0 1em;
overflow: hidden;
white-space: nowrap;
display: flex;
flex-direction: row;
align-items: baseline;
.truncated {
flex-grow: 1;
flex-shrink: 1;
overflow: hidden;
text-overflow: ellipsis;
margin-right: -2px;
}
.last-four {
flex-shrink: 0;
flex-grow: 0;
}
}
.features {
align-self: center;
}
} }
.top-data { .top-data {

View File

@ -92,15 +92,16 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
txFeePerVSize: this.tx.effectiveFeePerVsize, txFeePerVSize: this.tx.effectiveFeePerVsize,
}); });
this.cpfpInfo = cpfpInfo; this.cpfpInfo = cpfpInfo;
this.openGraphService.waitOver('cpfp-data'); this.openGraphService.waitOver('cpfp-data-' + this.txId);
}); });
this.subscription = this.route.paramMap this.subscription = this.route.paramMap
.pipe( .pipe(
switchMap((params: ParamMap) => { switchMap((params: ParamMap) => {
this.openGraphService.waitFor('tx-data');
const urlMatch = (params.get('id') || '').split(':'); const urlMatch = (params.get('id') || '').split(':');
this.txId = urlMatch[0]; this.txId = urlMatch[0];
this.openGraphService.waitFor('tx-data-' + this.txId);
this.openGraphService.waitFor('tx-time-' + this.txId);
this.seoService.setTitle( this.seoService.setTitle(
$localize`:@@bisq.transaction.browser-title:Transaction: ${this.txId}:INTERPOLATION:` $localize`:@@bisq.transaction.browser-title:Transaction: ${this.txId}:INTERPOLATION:`
); );
@ -149,7 +150,7 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
) )
.subscribe((tx: Transaction) => { .subscribe((tx: Transaction) => {
if (!tx) { if (!tx) {
this.openGraphService.fail('tx-data'); this.openGraphService.fail('tx-data-' + this.txId);
return; return;
} }
@ -164,8 +165,12 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
this.opReturns = this.getOpReturns(this.tx); this.opReturns = this.getOpReturns(this.tx);
this.extraData = this.chooseExtraData(); this.extraData = this.chooseExtraData();
if (!tx.status.confirmed && tx.firstSeen) { if (tx.status.confirmed) {
this.transactionTime = tx.status.block_time;
this.openGraphService.waitOver('tx-time-' + this.txId);
} else if (!tx.status.confirmed && tx.firstSeen) {
this.transactionTime = tx.firstSeen; this.transactionTime = tx.firstSeen;
this.openGraphService.waitOver('tx-time-' + this.txId);
} else { } else {
this.getTransactionTime(); this.getTransactionTime();
} }
@ -177,15 +182,15 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
bestDescendant: tx.bestDescendant, bestDescendant: tx.bestDescendant,
}; };
} else { } else {
this.openGraphService.waitFor('cpfp-data'); this.openGraphService.waitFor('cpfp-data-' + this.txId);
this.fetchCpfp$.next(this.tx.txid); this.fetchCpfp$.next(this.tx.txid);
} }
} }
this.openGraphService.waitOver('tx-data'); this.openGraphService.waitOver('tx-data-' + this.txId);
}, },
(error) => { (error) => {
this.openGraphService.fail('tx-data'); this.openGraphService.fail('tx-data-' + this.txId);
this.error = error; this.error = error;
this.isLoadingTx = false; this.isLoadingTx = false;
} }
@ -193,7 +198,6 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
} }
getTransactionTime() { getTransactionTime() {
this.openGraphService.waitFor('tx-time');
this.apiService this.apiService
.getTransactionTimes$([this.tx.txid]) .getTransactionTimes$([this.tx.txid])
.pipe( .pipe(
@ -203,7 +207,7 @@ export class TransactionPreviewComponent implements OnInit, OnDestroy {
) )
.subscribe((transactionTimes) => { .subscribe((transactionTimes) => {
this.transactionTime = transactionTimes[0]; this.transactionTime = transactionTimes[0];
this.openGraphService.waitOver('tx-time'); this.openGraphService.waitOver('tx-time-' + this.txId);
}); });
} }

File diff suppressed because it is too large Load Diff

View File

@ -161,6 +161,9 @@ export interface ITopNodesPerChannels {
updatedAt?: number, updatedAt?: number,
city?: any, city?: any,
country?: any, country?: any,
subdivision?: any,
iso_code?: string,
geolocation?: any;
} }
export interface ITopNodesPerCapacity { export interface ITopNodesPerCapacity {
@ -172,6 +175,9 @@ export interface ITopNodesPerCapacity {
updatedAt?: number, updatedAt?: number,
city?: any, city?: any,
country?: any, country?: any,
subdivision?: any,
iso_code?: string,
geolocation?: any;
} }
export interface INodesRanking { export interface INodesRanking {
@ -188,6 +194,9 @@ export interface IOldestNodes {
updatedAt?: number, updatedAt?: number,
city?: any, city?: any,
country?: any, country?: any,
subdivision?: any,
iso_code?: string,
geolocation?: any;
} }
export interface IChannel { export interface IChannel {

View File

@ -19,31 +19,31 @@
<tr> <tr>
<td i18n="address.total-sent">Fee rate</td> <td i18n="address.total-sent">Fee rate</td>
<td> <td>
{{ channel.fee_rate }} <span class="symbol">ppm ({{ channel.fee_rate / 10000 | number }}%)</span> {{ channel.fee_rate ?? '-' }} <span class="symbol">ppm ({{ channel.fee_rate / 10000 | number }}%)</span>
</td> </td>
</tr> </tr>
<tr> <tr>
<td i18n="address.total-sent">Base fee</td> <td i18n="address.total-sent">Base fee</td>
<td> <td>
<app-sats [satoshis]="channel.base_fee_mtokens / 1000" digitsInfo="1.0-2"></app-sats> <app-sats [valueOverride]="!channel.base_fee_mtokens ? '- ' : undefined" [satoshis]="channel.base_fee_mtokens / 1000" digitsInfo="1.0-2"></app-sats>
</td> </td>
</tr> </tr>
<tr> <tr>
<td i18n="address.total-sent">Min HTLC</td> <td i18n="address.total-sent">Min HTLC</td>
<td> <td>
<app-sats [satoshis]="channel.min_htlc_mtokens / 1000"></app-sats> <app-sats [valueOverride]="!channel.min_htlc_mtokens ? '- ' : undefined" [satoshis]="channel.min_htlc_mtokens / 1000"></app-sats>
</td> </td>
</tr> </tr>
<tr> <tr>
<td i18n="address.total-sent">Max HTLC</td> <td i18n="address.total-sent">Max HTLC</td>
<td> <td>
<app-sats [satoshis]="channel.max_htlc_mtokens / 1000"></app-sats> <app-sats [valueOverride]="!channel.max_htlc_mtokens ? '- ' : undefined" [satoshis]="channel.max_htlc_mtokens / 1000"></app-sats>
</td> </td>
</tr> </tr>
<tr> <tr>
<td i18n="address.total-sent">Timelock delta</td> <td i18n="address.total-sent">Timelock delta</td>
<td> <td>
<ng-container *ngTemplateOutlet="blocksPlural; context: {$implicit: channel.cltv_delta }"></ng-container> <ng-container *ngTemplateOutlet="blocksPlural; context: {$implicit: channel.cltv_delta ?? '-' }"></ng-container>
</td> </td>
</tr> </tr>
</tbody> </tbody>

View File

@ -1,9 +1,9 @@
<div class="box preview-box" *ngIf="(channel$ | async) as channel"> <div class="box preview-box" *ngIf="(channel$ | async) as channel">
<h2 class="preview-header" i18n="lightning.channel">lightning channel</h2>
<div class="row d-flex justify-content-between full-width-row"> <div class="row d-flex justify-content-between full-width-row">
<h1 class="title"> <div class="title-wrapper">
<span i18n="lightning.channel">Channel</span>: <h1 class="title">{{ channel.short_id }}</h1>
<a [routerLink]="['/lightning/channel' | relativeUrl, channel.id]"> {{ channel.short_id }}</a> </div>
</h1>
<div class="badges mb-2"> <div class="badges mb-2">
<span class="badge rounded-pill badge-secondary" *ngIf="channel.status === 0">Inactive</span> <span class="badge rounded-pill badge-secondary" *ngIf="channel.status === 0">Inactive</span>
<span class="badge rounded-pill badge-success" *ngIf="channel.status === 1">Active</span> <span class="badge rounded-pill badge-success" *ngIf="channel.status === 1">Active</span>
@ -12,20 +12,11 @@
<app-closing-type [type]="channel.closing_reason" *ngIf="channel.status === 2"></app-closing-type> <app-closing-type [type]="channel.closing_reason" *ngIf="channel.status === 2"></app-closing-type>
</div> </div>
</div> </div>
<div class="row d-flex justify-content-between full-width-row nodes">
<span class="node left">
{{ channel.node_left.alias || '?' }}
</span>
<fa-icon class="between-arrow" [icon]="['fas', 'arrow-right-arrow-left']" [fixedWidth]="true" title="channel between"></fa-icon>
<span class="node right">
{{ channel.node_right.alias || '?' }}
</span>
</div>
<div class="row"> <div class="row">
<div class="col-md"> <div class="col-md">
<a class="subtitle" [routerLink]="['/lightning/channel' | relativeUrl, channel.id]">{{ channel.id }}</a>
<table class="table table-borderless table-striped"> <table class="table table-borderless table-striped">
<tbody> <tbody>
<tr></tr>
<tr> <tr>
<td i18n="channel.created">Created</td> <td i18n="channel.created">Created</td>
<td>{{ channel.created | date:'yyyy-MM-dd HH:mm' }}</td> <td>{{ channel.created | date:'yyyy-MM-dd HH:mm' }}</td>
@ -61,6 +52,15 @@
<app-nodes-channels-map *ngIf="!error" [style]="'channelpage'" [channel]="channelGeo" [fitContainer]="true" [placeholder]="true" (readyEvent)="onMapReady()"></app-nodes-channels-map> <app-nodes-channels-map *ngIf="!error" [style]="'channelpage'" [channel]="channelGeo" [fitContainer]="true" [placeholder]="true" (readyEvent)="onMapReady()"></app-nodes-channels-map>
</div> </div>
</div> </div>
<div class="row d-flex justify-content-between full-width-row nodes">
<span class="node left">
{{ channel.node_left.alias || '?' }}
</span>
<fa-icon class="between-arrow" [icon]="['fas', 'arrow-right-arrow-left']" [fixedWidth]="true" title="channel between"></fa-icon>
<span class="node right">
{{ channel.node_right.alias || '?' }}
</span>
</div>
</div> </div>
<ng-template [ngIf]="error"> <ng-template [ngIf]="error">

View File

@ -1,15 +1,17 @@
.title {
font-size: 52px;
margin: 0;
}
.table { .table {
font-size: 32px; font-size: 32px;
margin-top: 36px; margin-top: 10px;
} }
.badges { .badges {
font-size: 28px; font-size: 28px;
flex-shrink: 0;
flex-grow: 0;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: baseline;
justify-content: flex-end;
::ng-deep .badge { ::ng-deep .badge {
margin-left: 0.5em; margin-left: 0.5em;
@ -23,11 +25,12 @@
.full-width-row { .full-width-row {
padding-left: 15px; padding-left: 15px;
padding-right: 15px; padding-right: 15px;
flex-wrap: nowrap;
}
&:nth-child(even) { .row.nodes {
background: #181b2d; background: #181b2d;
margin: 15px 0; margin: 15px 0 0;
}
} }
.nodes { .nodes {
@ -46,7 +49,7 @@
min-width: 470px; min-width: 470px;
padding: 0; padding: 0;
background: #181b2d; background: #181b2d;
max-height: 470px; max-height: 350px;
overflow: hidden; overflow: hidden;
} }

View File

@ -16,6 +16,7 @@ export class ChannelPreviewComponent implements OnInit {
channel$: Observable<any>; channel$: Observable<any>;
error: any = null; error: any = null;
channelGeo: number[] = []; channelGeo: number[] = [];
shortId: string;
constructor( constructor(
private lightningApiService: LightningApiService, private lightningApiService: LightningApiService,
@ -28,8 +29,9 @@ export class ChannelPreviewComponent implements OnInit {
this.channel$ = this.activatedRoute.paramMap this.channel$ = this.activatedRoute.paramMap
.pipe( .pipe(
switchMap((params: ParamMap) => { switchMap((params: ParamMap) => {
this.openGraphService.waitFor('channel-map'); this.shortId = params.get('short_id') || '';
this.openGraphService.waitFor('channel-data'); this.openGraphService.waitFor('channel-map-' + this.shortId);
this.openGraphService.waitFor('channel-data-' + this.shortId);
this.error = null; this.error = null;
this.seoService.setTitle(`Channel: ${params.get('short_id')}`); this.seoService.setTitle(`Channel: ${params.get('short_id')}`);
return this.lightningApiService.getChannel$(params.get('short_id')) return this.lightningApiService.getChannel$(params.get('short_id'))
@ -48,12 +50,12 @@ export class ChannelPreviewComponent implements OnInit {
data.node_right.longitude, data.node_right.latitude, data.node_right.longitude, data.node_right.latitude,
]; ];
} }
this.openGraphService.waitOver('channel-data'); this.openGraphService.waitOver('channel-data-' + this.shortId);
}), }),
catchError((err) => { catchError((err) => {
this.error = err; this.error = err;
this.openGraphService.fail('channel-map'); this.openGraphService.fail('channel-map-' + this.shortId);
this.openGraphService.fail('channel-data'); this.openGraphService.fail('channel-data-' + this.shortId);
return of(null); return of(null);
}) })
); );
@ -62,6 +64,6 @@ export class ChannelPreviewComponent implements OnInit {
} }
onMapReady() { onMapReady() {
this.openGraphService.waitOver('channel-map'); this.openGraphService.waitOver('channel-map-' + this.shortId);
} }
} }

View File

@ -25,13 +25,17 @@
<table class="table table-borderless table-striped"> <table class="table table-borderless table-striped">
<tbody> <tbody>
<tr> <tr>
<td i18n="address.total-sent">Created</td> <td i18n="lightning.created">Created</td>
<td><app-timestamp [dateString]="channel.created"></app-timestamp></td> <td><app-timestamp [dateString]="channel.created"></app-timestamp></td>
</tr> </tr>
<tr> <tr *ngIf="channel.status !== 2">
<td i18n="address.total-sent">Last update</td> <td i18n="lightning.last-update">Last update</td>
<td><app-timestamp [dateString]="channel.updated_at"></app-timestamp></td> <td><app-timestamp [dateString]="channel.updated_at"></app-timestamp></td>
</tr> </tr>
<tr *ngIf="channel.status === 2">
<td i18n="lightning.closing_date">Closing date</td>
<td><app-timestamp [dateString]="channel.closing_date"></app-timestamp></td>
</tr>
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ActivatedRoute, ParamMap } from '@angular/router'; import { ActivatedRoute, ParamMap } from '@angular/router';
import { Observable, of, zip } from 'rxjs'; import { Observable, of, zip } from 'rxjs';
import { catchError, map, shareReplay, switchMap } from 'rxjs/operators'; import { catchError, map, shareReplay, switchMap, tap } from 'rxjs/operators';
import { IChannel } from 'src/app/interfaces/node-api.interface'; import { IChannel } from 'src/app/interfaces/node-api.interface';
import { ElectrsApiService } from 'src/app/services/electrs-api.service'; import { ElectrsApiService } from 'src/app/services/electrs-api.service';
import { SeoService } from 'src/app/services/seo.service'; import { SeoService } from 'src/app/services/seo.service';
@ -31,9 +31,11 @@ export class ChannelComponent implements OnInit {
.pipe( .pipe(
switchMap((params: ParamMap) => { switchMap((params: ParamMap) => {
this.error = null; this.error = null;
this.seoService.setTitle(`Channel: ${params.get('short_id')}`);
return this.lightningApiService.getChannel$(params.get('short_id')) return this.lightningApiService.getChannel$(params.get('short_id'))
.pipe( .pipe(
tap((value) => {
this.seoService.setTitle(`Channel: ${value.short_id}`);
}),
catchError((err) => { catchError((err) => {
this.error = err; this.error = err;
return of(null); return of(null);

View File

@ -35,7 +35,8 @@
<th class="alias text-left" i18n="nodes.alias">Node Alias</th> <th class="alias text-left" i18n="nodes.alias">Node Alias</th>
<th class="alias text-left d-none d-md-table-cell" i18n="channels.transaction">&nbsp;</th> <th class="alias text-left d-none d-md-table-cell" i18n="channels.transaction">&nbsp;</th>
<th class="alias text-left d-none d-md-table-cell" i18n="status">Status</th> <th class="alias text-left d-none d-md-table-cell" i18n="status">Status</th>
<th class="channels text-left d-none d-md-table-cell" i18n="channels.rate">Fee Rate</th> <th *ngIf="status !== 'closed'" class="channels text-left d-none d-md-table-cell" i18n="channels.rate">Fee Rate</th>
<th *ngIf="status === 'closed'" class="channels text-left d-none d-md-table-cell" i18n="channels.closing_date">Closing date</th>
<th class="capacity text-right d-none d-md-table-cell" i18n="nodes.capacity">Capacity</th> <th class="capacity text-right d-none d-md-table-cell" i18n="nodes.capacity">Capacity</th>
<th class="capacity text-right" i18n="channels.id">Channel ID</th> <th class="capacity text-right" i18n="channels.id">Channel ID</th>
</thead> </thead>
@ -71,9 +72,12 @@
</ng-template> </ng-template>
</ng-template> </ng-template>
</td> </td>
<td class="capacity text-left d-none d-md-table-cell"> <td *ngIf="status !== 'closed'" class="capacity text-left d-none d-md-table-cell">
{{ channel.fee_rate }} <span class="symbol">ppm ({{ channel.fee_rate / 10000 | number }}%)</span> {{ channel.fee_rate }} <span class="symbol">ppm ({{ channel.fee_rate / 10000 | number }}%)</span>
</td> </td>
<td *ngIf="status === 'closed'" class="capacity text-left d-none d-md-table-cell">
<app-timestamp [unixTime]="channel.closing_date"></app-timestamp>
</td>
<td class="capacity text-right d-none d-md-table-cell"> <td class="capacity text-right d-none d-md-table-cell">
<app-amount *ngIf="channel.capacity > 100000000; else smallchannel" [satoshis]="channel.capacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount> <app-amount *ngIf="channel.capacity > 100000000; else smallchannel" [satoshis]="channel.capacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
<ng-template #smallchannel> <ng-template #smallchannel>

View File

@ -1,15 +1,17 @@
<div class="box preview-box" *ngIf="(node$ | async) as node"> <div class="box preview-box" *ngIf="(node$ | async) as node">
<h2 class="preview-header" i18n="lightning.node">lightning node</h2>
<div class="row d-flex justify-content-between full-width-row"> <div class="row d-flex justify-content-between full-width-row">
<h1 class="title"> <h1 class="title"></h1>
<span i18n="lightning.node">Node</span>: <div class="title-wrapper">
<a [routerLink]="['/lightning/node' | relativeUrl, node.id]"> {{ node.alias }}</a> <h1 class="title">{{ node.alias }}</h1>
</h1> </div>
<div class="badges mb-2"> <div class="badges mb-2">
<span class="badge rounded-pill badge-success" *ngFor="let socketType of socketTypes">{{ socketType }}</span> <span class="badge rounded-pill badge-success" *ngFor="let socketType of socketTypes">{{ socketType }}</span>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md"> <div class="col-md">
<a class="subtitle" [routerLink]="['/lightning/node' | relativeUrl, node.public_key]">{{ node.public_key }}</a>
<table class="table table-borderless table-striped"> <table class="table table-borderless table-striped">
<tbody> <tbody>
<tr> <tr>

View File

@ -1,15 +1,17 @@
.title {
font-size: 52px;
margin-bottom: 0;
}
.table { .table {
margin-top: 48px; margin-top: 6px;
font-size: 32px; font-size: 32px;
} }
.badges { .badges {
font-size: 28px; font-size: 28px;
flex-shrink: 0;
flex-grow: 0;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: baseline;
justify-content: flex-end;
::ng-deep .badge { ::ng-deep .badge {
margin-left: 0.5em; margin-left: 0.5em;
@ -20,14 +22,14 @@
flex-grow: 0; flex-grow: 0;
flex-shrink: 0; flex-shrink: 0;
width: 470px; width: 470px;
height: 390px; height: 408px;
min-width: 470px; min-width: 470px;
min-height: 390px; min-height: 408px;
max-height: 390px; max-height: 408px;
padding: 0; padding: 0;
background: #181b2d; background: #181b2d;
overflow: hidden; overflow: hidden;
margin-top: 18px; margin-top: 6px;
} }
.row { .row {
@ -36,6 +38,7 @@
.full-width-row { .full-width-row {
padding-left: 15px; padding-left: 15px;
flex-wrap: nowrap;
} }
::ng-deep .symbol { ::ng-deep .symbol {

View File

@ -42,9 +42,9 @@ export class NodePreviewComponent implements OnInit {
this.node$ = this.activatedRoute.paramMap this.node$ = this.activatedRoute.paramMap
.pipe( .pipe(
switchMap((params: ParamMap) => { switchMap((params: ParamMap) => {
this.openGraphService.waitFor('node-map'); this.publicKey = params.get('public_key');
this.openGraphService.waitFor('node-data'); this.openGraphService.waitFor('node-map-' + this.publicKey);
this.publicKey = params.get('public_key'); this.openGraphService.waitFor('node-data-' + this.publicKey);
return this.lightningApiService.getNode$(params.get('public_key')); return this.lightningApiService.getNode$(params.get('public_key'));
}), }),
map((node) => { map((node) => {
@ -75,14 +75,14 @@ export class NodePreviewComponent implements OnInit {
this.socketTypes = Object.keys(socketTypesMap); this.socketTypes = Object.keys(socketTypesMap);
node.avgCapacity = node.capacity / Math.max(1, node.active_channel_count); node.avgCapacity = node.capacity / Math.max(1, node.active_channel_count);
this.openGraphService.waitOver('node-data'); this.openGraphService.waitOver('node-data-' + this.publicKey);
return node; return node;
}), }),
catchError(err => { catchError(err => {
this.error = err; this.error = err;
this.openGraphService.fail('node-map'); this.openGraphService.fail('node-map-' + this.publicKey);
this.openGraphService.fail('node-data'); this.openGraphService.fail('node-data-' + this.publicKey);
return [{ return [{
alias: this.publicKey, alias: this.publicKey,
public_key: this.publicKey, public_key: this.publicKey,
@ -100,6 +100,6 @@ export class NodePreviewComponent implements OnInit {
} }
onMapReady() { onMapReady() {
this.openGraphService.waitOver('node-map'); this.openGraphService.waitOver('node-map-' + this.publicKey);
} }
} }

View File

@ -120,7 +120,7 @@
</div> </div>
<div *ngIf="!error"> <div *ngIf="!error">
<div class="row" *ngIf="node.as_number"> <div class="row" *ngIf="node.as_number && node.active_channel_count">
<div class="col-sm"> <div class="col-sm">
<app-nodes-channels-map [style]="'nodepage'" [publicKey]="node.public_key" [hasLocation]="!!node.as_number"></app-nodes-channels-map> <app-nodes-channels-map [style]="'nodepage'" [publicKey]="node.public_key" [hasLocation]="!!node.as_number"></app-nodes-channels-map>
</div> </div>
@ -128,7 +128,7 @@
<app-node-statistics-chart [publicKey]="node.public_key"></app-node-statistics-chart> <app-node-statistics-chart [publicKey]="node.public_key"></app-node-statistics-chart>
</div> </div>
</div> </div>
<div *ngIf="!node.as_number"> <div *ngIf="!node.as_number || !node.active_channel_count">
<app-node-statistics-chart [publicKey]="node.public_key"></app-node-statistics-chart> <app-node-statistics-chart [publicKey]="node.public_key"></app-node-statistics-chart>
</div> </div>

View File

@ -2,10 +2,7 @@
<div class="card-header"> <div class="card-header">
<div class="d-flex d-md-block align-items-baseline" style="margin-bottom: -5px"> <div class="d-flex d-md-block align-items-baseline" style="margin-bottom: -5px">
<span i18n="lightning.nodes-heatmap">Lightning nodes world heat map</span> <span i18n="lightning.nodes-world-map">Lightning nodes world map</span>
<button class="btn p-0 pl-2" style="margin: 0 0 4px 0px">
<fa-icon [icon]="['fas', 'download']" [fixedWidth]="true" (click)="onSaveChart()"></fa-icon>
</button>
</div> </div>
<small style="color: #ffffff66" i18n="lightning.tor-nodes-excluded">(Tor nodes excluded)</small> <small style="color: #ffffff66" i18n="lightning.tor-nodes-excluded">(Tor nodes excluded)</small>
</div> </div>

View File

@ -1,14 +1,15 @@
import { ChangeDetectionStrategy, Component, NgZone, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, Inject, LOCALE_ID, NgZone, OnDestroy, OnInit } from '@angular/core';
import { mempoolFeeColors } from 'src/app/app.constants';
import { SeoService } from 'src/app/services/seo.service'; import { SeoService } from 'src/app/services/seo.service';
import { ApiService } from 'src/app/services/api.service'; import { ApiService } from 'src/app/services/api.service';
import { combineLatest, Observable, tap } from 'rxjs'; import { Observable, tap, zip } from 'rxjs';
import { AssetsService } from 'src/app/services/assets.service'; import { AssetsService } from 'src/app/services/assets.service';
import { EChartsOption, registerMap } from 'echarts'; import { EChartsOption, registerMap } from 'echarts';
import { download } from 'src/app/shared/graphs.utils'; import { lerpColor } from 'src/app/shared/graphs.utils';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe'; import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.pipe';
import { StateService } from 'src/app/services/state.service'; import { StateService } from 'src/app/services/state.service';
import { AmountShortenerPipe } from 'src/app/shared/pipes/amount-shortener.pipe';
import { getFlagEmoji } from 'src/app/shared/common.utils';
@Component({ @Component({
selector: 'app-nodes-map', selector: 'app-nodes-map',
@ -16,7 +17,7 @@ import { StateService } from 'src/app/services/state.service';
styleUrls: ['./nodes-map.component.scss'], styleUrls: ['./nodes-map.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class NodesMap implements OnInit, OnDestroy { export class NodesMap implements OnInit {
observable$: Observable<any>; observable$: Observable<any>;
chartInstance = undefined; chartInstance = undefined;
@ -26,44 +27,52 @@ export class NodesMap implements OnInit, OnDestroy {
}; };
constructor( constructor(
@Inject(LOCALE_ID) public locale: string,
private seoService: SeoService, private seoService: SeoService,
private apiService: ApiService, private apiService: ApiService,
private stateService: StateService, private stateService: StateService,
private assetsService: AssetsService, private assetsService: AssetsService,
private router: Router, private router: Router,
private zone: NgZone, private zone: NgZone,
private amountShortenerPipe: AmountShortenerPipe
) { ) {
} }
ngOnDestroy(): void {}
ngOnInit(): void { ngOnInit(): void {
this.seoService.setTitle($localize`Lightning nodes world map`); this.seoService.setTitle($localize`Lightning nodes world map`);
this.observable$ = combineLatest([ this.observable$ = zip(
this.assetsService.getWorldMapJson$, this.assetsService.getWorldMapJson$,
this.apiService.getNodesPerCountry() this.apiService.getWorldNodes$()
]).pipe(tap((data) => { ).pipe(tap((data) => {
registerMap('world', data[0]); registerMap('world', data[0]);
const countries = []; const nodes: any[] = [];
let max = 0; console.log(data[1].nodes[0]);
for (const country of data[1]) { for (const node of data[1].nodes) {
countries.push({ // We add a bit of noise so nodes at the same location are not all
name: country.name.en, // on top of each other
value: country.count, const random = Math.random() * 2 * Math.PI;
iso: country.iso.toLowerCase(), const random2 = Math.random() * 0.01;
}); nodes.push([
max = Math.max(max, country.count); node[0] + random2 * Math.cos(random),
node[1] + random2 * Math.sin(random),
node[4], // Liquidity
node[3], // Alias
node[2], // Public key
node[5], // Channels
node[6].en, // Country
node[7], // ISO Code
]);
} }
this.prepareChartOptions(countries, max); this.prepareChartOptions(nodes, data[1].maxLiquidity);
})); }));
} }
prepareChartOptions(countries, max) { prepareChartOptions(nodes, maxLiquidity) {
let title: object; let title: object;
if (countries.length === 0) { if (nodes.length === 0) {
title = { title = {
textStyle: { textStyle: {
color: 'grey', color: 'grey',
@ -76,53 +85,80 @@ export class NodesMap implements OnInit, OnDestroy {
} }
this.chartOptions = { this.chartOptions = {
title: countries.length === 0 ? title : undefined, silent: false,
tooltip: { title: title ?? undefined,
backgroundColor: 'rgba(17, 19, 31, 1)', tooltip: {},
borderRadius: 4, geo: {
shadowColor: 'rgba(0, 0, 0, 0.5)', animation: false,
textStyle: { silent: true,
color: '#b1b1b1', center: [0, 5],
zoom: 1.3,
tooltip: {
show: false
}, },
borderColor: '#000', map: 'world',
formatter: function(country) { roam: true,
if (country.data === undefined) { itemStyle: {
return `<b style="color: white">${country.name}<br>0 nodes</b><br>`; borderColor: 'black',
} else { color: '#272b3f'
return `<b style="color: white">${country.data.name}<br>${country.data.value} nodes</b><br>`; },
} scaleLimit: {
min: 1.3,
max: 100000,
},
emphasis: {
disabled: true,
} }
}, },
visualMap: { series: [
left: 'right', {
show: true, large: false,
min: 1, type: 'scatter',
max: max, data: nodes,
text: ['High', 'Low'], coordinateSystem: 'geo',
calculable: true, geoIndex: 0,
textStyle: { progressive: 500,
color: 'white', symbolSize: function (params) {
}, return 10 * Math.pow(params[2] / maxLiquidity, 0.2) + 3;
inRange: { },
color: mempoolFeeColors.map(color => `#${color}`), tooltip: {
}, trigger: 'item',
}, show: true,
series: { backgroundColor: 'rgba(17, 19, 31, 1)',
type: 'map', borderRadius: 4,
map: 'world', shadowColor: 'rgba(0, 0, 0, 0.5)',
emphasis: { textStyle: {
label: { color: '#b1b1b1',
show: false, align: 'left',
},
borderColor: '#000',
formatter: (value) => {
const data = value.data;
const alias = data[3].length > 0 ? data[3] : data[4].slice(0, 20);
const liquidity = data[2] >= 100000000 ?
`${this.amountShortenerPipe.transform(data[2] / 100000000)} BTC` :
`${this.amountShortenerPipe.transform(data[2], 2)} sats`;
return `
<b style="color: white">${alias}</b><br>
${liquidity}<br>
${data[5]} channels<br>
${getFlagEmoji(data[7])} ${data[6]}
`;
}
}, },
itemStyle: { itemStyle: {
areaColor: '#FDD835', color: function (params) {
} return `${lerpColor('#1E88E5', '#D81B60', Math.pow(params.data[2] / maxLiquidity, 0.2))}`;
},
opacity: 1,
borderColor: 'black',
borderWidth: 0,
},
blendMode: 'lighter',
zlevel: 2,
}, },
data: countries, ]
itemStyle: {
areaColor: '#5A6A6D'
},
}
}; };
} }
@ -134,30 +170,16 @@ export class NodesMap implements OnInit, OnDestroy {
this.chartInstance = ec; this.chartInstance = ec;
this.chartInstance.on('click', (e) => { this.chartInstance.on('click', (e) => {
if (e.data && e.data.value > 0) { if (e.data) {
this.zone.run(() => { this.zone.run(() => {
const url = new RelativeUrlPipe(this.stateService).transform(`/lightning/nodes/country/${e.data.iso}`); const url = new RelativeUrlPipe(this.stateService).transform(`/lightning/node/${e.data[4]}`);
this.router.navigate([url]); this.router.navigate([url]);
}); });
} }
}); });
}
onSaveChart() { this.chartInstance.on('georoam', (e) => {
// @ts-ignore this.chartInstance.resize();
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);
} }
} }

View File

@ -46,7 +46,7 @@
<td class="text-right capacity"> <td class="text-right capacity">
<app-amount *ngIf="country.capacity > 100000000; else smallchannel" [satoshis]="country.capacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount> <app-amount *ngIf="country.capacity > 100000000; else smallchannel" [satoshis]="country.capacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
<ng-template #smallchannel> <ng-template #smallchannel>
{{ country.capacity | amountShortener: 1 }} {{ country.capacity ?? 0 | amountShortener: 1 }}
<span class="sats" i18n="shared.sats">sats</span> <span class="sats" i18n="shared.sats">sats</span>
</ng-template> </ng-template>
</td> </td>

View File

@ -45,7 +45,7 @@ export class NodesPerCountryChartComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.seoService.setTitle($localize`Lightning nodes per country`); this.seoService.setTitle($localize`Lightning nodes per country`);
this.nodesPerCountryObservable$ = this.apiService.getNodesPerCountry() this.nodesPerCountryObservable$ = this.apiService.getNodesPerCountry$()
.pipe( .pipe(
map(data => { map(data => {
for (let i = 0; i < data.length; ++i) { for (let i = 0; i < data.length; ++i) {

View File

@ -6,15 +6,17 @@
<div style="min-height: 295px"> <div style="min-height: 295px">
<table class="table table-borderless"> <table class="table table-borderless">
<thead> <thead>
<th class="alias text-left" i18n="lightning.alias">Alias</th> <th class="alias text-left" i18n="lightning.alias">Alias</th>
<th class="timestamp-first text-left" i18n="lightning.first_seen">First seen</th> <th class="timestamp-first text-left" i18n="lightning.first_seen">First seen</th>
<th class="timestamp-update text-left" i18n="lightning.last_update">Last update</th> <th class="timestamp-update text-left" i18n="lightning.last_update">Last update</th>
<th class="capacity text-right" i18n="lightning.capacity">Capacity</th> <th class="capacity text-right" i18n="lightning.capacity">Capacity</th>
<th class="channels text-right" i18n="lightning.channels">Channels</th> <th class="channels text-right" i18n="lightning.channels">Channels</th>
<th class="city text-right" i18n="lightning.city">City</th> <th class="city text-right" i18n="lightning.location">Location</th>
</thead> </thead>
<tbody *ngIf="nodes$ | async as nodes">
<tbody *ngIf="nodes$ | async as nodes; else skeleton">
<tr *ngFor="let node of nodes; let i= index; trackBy: trackByPublicKey"> <tr *ngFor="let node of nodes; let i= index; trackBy: trackByPublicKey">
<td class="alias text-left text-truncate"> <td class="alias text-left text-truncate">
<a [routerLink]="['/lightning/node/' | relativeUrl, node.public_key]">{{ node.alias }}</a> <a [routerLink]="['/lightning/node/' | relativeUrl, node.public_key]">{{ node.alias }}</a>
@ -39,6 +41,32 @@
<app-geolocation [data]="node.geolocation" [type]="'list-country'"></app-geolocation> <app-geolocation [data]="node.geolocation" [type]="'list-country'"></app-geolocation>
</td> </td>
</tbody> </tbody>
<ng-template #skeleton>
<tbody>
<tr *ngFor="let item of skeletonLines">
<td class="alias text-left text-truncate">
<span class="skeleton-loader"></span>
</td>
<td class="timestamp-first text-left">
<span class="skeleton-loader"></span>
</td>
<td class="timestamp-update text-left">
<span class="skeleton-loader"></span>
</td>
<td class="capacity text-right">
<span class="skeleton-loader"></span>
</td>
<td class="channels text-right">
<span class="skeleton-loader"></span>
</td>
<td class="city text-right text-truncate">
<span class="skeleton-loader"></span>
</td>
</tr>
</tbody>
</ng-template>
</table> </table>
</div> </div>

View File

@ -16,11 +16,17 @@ export class NodesPerCountry implements OnInit {
nodes$: Observable<any>; nodes$: Observable<any>;
country: {name: string, flag: string}; country: {name: string, flag: string};
skeletonLines: number[] = [];
constructor( constructor(
private apiService: ApiService, private apiService: ApiService,
private seoService: SeoService, private seoService: SeoService,
private route: ActivatedRoute, private route: ActivatedRoute,
) { } ) {
for (let i = 0; i < 20; ++i) {
this.skeletonLines.push(i);
}
}
ngOnInit(): void { ngOnInit(): void {
this.nodes$ = this.apiService.getNodeForCountry$(this.route.snapshot.params.country) this.nodes$ = this.apiService.getNodeForCountry$(this.route.snapshot.params.country)

View File

@ -47,7 +47,9 @@ export class NodesPerISPChartComponent implements OnInit {
} }
ngOnInit(): void { ngOnInit(): void {
this.seoService.setTitle($localize`Lightning nodes per ISP`); if (!this.widget) {
this.seoService.setTitle($localize`Lightning nodes per ISP`);
}
this.nodesPerAsObservable$ = combineLatest([ this.nodesPerAsObservable$ = combineLatest([
this.sortBySubject.pipe(startWith(true)), this.sortBySubject.pipe(startWith(true)),
@ -105,7 +107,7 @@ export class NodesPerISPChartComponent implements OnInit {
} }
generateChartSerieData(ispRanking): PieSeriesOption[] { generateChartSerieData(ispRanking): PieSeriesOption[] {
let shareThreshold = 0.5; let shareThreshold = 0.4;
if (this.widget && isMobile() || isMobile()) { if (this.widget && isMobile() || isMobile()) {
shareThreshold = 1; shareThreshold = 1;
} else if (this.widget) { } else if (this.widget) {

View File

@ -3,15 +3,17 @@
<div style="min-height: 295px"> <div style="min-height: 295px">
<table class="table table-borderless"> <table class="table table-borderless">
<thead> <thead>
<th class="alias text-left" i18n="lightning.alias">Alias</th> <th class="alias text-left" i18n="lightning.alias">Alias</th>
<th class="timestamp-first text-left" i18n="lightning.first_seen">First seen</th> <th class="timestamp-first text-left" i18n="lightning.first_seen">First seen</th>
<th class="timestamp-update text-left" i18n="lightning.last_update">Last update</th> <th class="timestamp-update text-left" i18n="lightning.last_update">Last update</th>
<th class="capacity text-right" i18n="lightning.capacity">Capacity</th> <th class="capacity text-right" i18n="lightning.capacity">Capacity</th>
<th class="channels text-right" i18n="lightning.channels">Channels</th> <th class="channels text-right" i18n="lightning.channels">Channels</th>
<th class="city text-right" i18n="lightning.city">City</th> <th class="city text-right" i18n="lightning.location">Location</th>
</thead> </thead>
<tbody *ngIf="nodes$ | async as nodes">
<tbody *ngIf="nodes$ | async as nodes; else skeleton">
<tr *ngFor="let node of nodes; let i= index; trackBy: trackByPublicKey"> <tr *ngFor="let node of nodes; let i= index; trackBy: trackByPublicKey">
<td class="alias text-left text-truncate"> <td class="alias text-left text-truncate">
<a [routerLink]="['/lightning/node/' | relativeUrl, node.public_key]">{{ node.alias }}</a> <a [routerLink]="['/lightning/node/' | relativeUrl, node.public_key]">{{ node.alias }}</a>
@ -36,6 +38,32 @@
<app-geolocation [data]="node.geolocation" [type]="'list-isp'"></app-geolocation> <app-geolocation [data]="node.geolocation" [type]="'list-isp'"></app-geolocation>
</td> </td>
</tbody> </tbody>
<ng-template #skeleton>
<tbody>
<tr *ngFor="let item of skeletonLines">
<td class="alias text-left text-truncate">
<span class="skeleton-loader"></span>
</td>
<td class="timestamp-first text-left">
<span class="skeleton-loader"></span>
</td>
<td class="timestamp-update text-left">
<span class="skeleton-loader"></span>
</td>
<td class="capacity text-right">
<span class="skeleton-loader"></span>
</td>
<td class="channels text-right">
<span class="skeleton-loader"></span>
</td>
<td class="city text-right text-truncate">
<span class="skeleton-loader"></span>
</td>
</tr>
</tbody>
</ng-template>
</table> </table>
</div> </div>

View File

@ -15,11 +15,17 @@ export class NodesPerISP implements OnInit {
nodes$: Observable<any>; nodes$: Observable<any>;
isp: {name: string, id: number}; isp: {name: string, id: number};
skeletonLines: number[] = [];
constructor( constructor(
private apiService: ApiService, private apiService: ApiService,
private seoService: SeoService, private seoService: SeoService,
private route: ActivatedRoute, private route: ActivatedRoute,
) { } ) {
for (let i = 0; i < 20; ++i) {
this.skeletonLines.push(i);
}
}
ngOnInit(): void { ngOnInit(): void {
this.nodes$ = this.apiService.getNodeForISP$(this.route.snapshot.params.isp) this.nodes$ = this.apiService.getNodeForISP$(this.route.snapshot.params.isp)

View File

@ -9,7 +9,7 @@
<th class="rank"></th> <th class="rank"></th>
<th class="alias text-left" i18n="nodes.alias">Alias</th> <th class="alias text-left" i18n="nodes.alias">Alias</th>
<th class="timestamp-first text-right" i18n="lightning.first_seen">First seen</th> <th class="timestamp-first text-right" i18n="lightning.first_seen">First seen</th>
<th *ngIf="!widget" class="capacity text-right" i18n="node.capacity">Capacity</th> <th *ngIf="!widget" class="capacity text-right" i18n="node.liquidity">Liquidity</th>
<th *ngIf="!widget" class="channels text-right" i18n="lightning.channels">Channels</th> <th *ngIf="!widget" class="channels text-right" i18n="lightning.channels">Channels</th>
<th *ngIf="!widget" class="timestamp-update text-left" i18n="lightning.last_update">Last update</th> <th *ngIf="!widget" class="timestamp-update text-left" i18n="lightning.last_update">Last update</th>
<th *ngIf="!widget" class="location text-right" i18n="lightning.location">Location</th> <th *ngIf="!widget" class="location text-right" i18n="lightning.location">Location</th>
@ -35,7 +35,7 @@
<app-timestamp [customFormat]="'yyyy-MM-dd'" [unixTime]="node.updatedAt"></app-timestamp> <app-timestamp [customFormat]="'yyyy-MM-dd'" [unixTime]="node.updatedAt"></app-timestamp>
</td> </td>
<td *ngIf="!widget" class="location text-right text-truncate"> <td *ngIf="!widget" class="location text-right text-truncate">
{{ node?.city?.en ?? '-' }} <app-geolocation [data]="node.geolocation" [type]="'list-isp'"></app-geolocation>
</td> </td>
</tr> </tr>
</tbody> </tbody>

View File

@ -1,5 +1,7 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { map, Observable } from 'rxjs'; import { map, Observable } from 'rxjs';
import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component';
import { SeoService } from 'src/app/services/seo.service';
import { IOldestNodes } from '../../../interfaces/node-api.interface'; import { IOldestNodes } from '../../../interfaces/node-api.interface';
import { LightningApiService } from '../../lightning-api.service'; import { LightningApiService } from '../../lightning-api.service';
@ -15,19 +17,38 @@ export class OldestNodes implements OnInit {
oldestNodes$: Observable<IOldestNodes[]>; oldestNodes$: Observable<IOldestNodes[]>;
skeletonRows: number[] = []; skeletonRows: number[] = [];
constructor(private apiService: LightningApiService) {} constructor(
private apiService: LightningApiService,
private seoService: SeoService
) {}
ngOnInit(): void { ngOnInit(): void {
if (!this.widget) {
this.seoService.setTitle($localize`Oldest lightning nodes`);
}
for (let i = 1; i <= (this.widget ? 10 : 100); ++i) { for (let i = 1; i <= (this.widget ? 10 : 100); ++i) {
this.skeletonRows.push(i); this.skeletonRows.push(i);
} }
if (this.widget === false) { if (this.widget === false) {
this.oldestNodes$ = this.apiService.getOldestNodes$(); this.oldestNodes$ = this.apiService.getOldestNodes$().pipe(
map((ranking) => {
for (const i in ranking) {
ranking[i].geolocation = <GeolocationData>{
country: ranking[i].country?.en,
city: ranking[i].city?.en,
subdivision: ranking[i].subdivision?.en,
iso: ranking[i].iso_code,
};
}
return ranking;
})
);
} else { } else {
this.oldestNodes$ = this.apiService.getOldestNodes$().pipe( this.oldestNodes$ = this.apiService.getOldestNodes$().pipe(
map((nodes: IOldestNodes[]) => { map((nodes: IOldestNodes[]) => {
return nodes.slice(0, 10); return nodes.slice(0, 7);
}) })
); );
} }

View File

@ -8,7 +8,7 @@
<thead> <thead>
<th class="rank"></th> <th class="rank"></th>
<th class="alias text-left" i18n="nodes.alias">Alias</th> <th class="alias text-left" i18n="nodes.alias">Alias</th>
<th class="capacity text-right" i18n="node.capacity">Capacity</th> <th class="capacity text-right" i18n="node.liquidity">Liquidity</th>
<th *ngIf="!widget" class="channels text-right" i18n="lightning.channels">Channels</th> <th *ngIf="!widget" class="channels text-right" i18n="lightning.channels">Channels</th>
<th *ngIf="!widget" class="timestamp-first text-left" i18n="lightning.first_seen">First seen</th> <th *ngIf="!widget" class="timestamp-first text-left" i18n="lightning.first_seen">First seen</th>
<th *ngIf="!widget" class="timestamp-update text-left" i18n="lightning.last_update">Last update</th> <th *ngIf="!widget" class="timestamp-update text-left" i18n="lightning.last_update">Last update</th>
@ -35,7 +35,7 @@
<app-timestamp [customFormat]="'yyyy-MM-dd'" [unixTime]="node.updatedAt"></app-timestamp> <app-timestamp [customFormat]="'yyyy-MM-dd'" [unixTime]="node.updatedAt"></app-timestamp>
</td> </td>
<td *ngIf="!widget" class="location text-right text-truncate"> <td *ngIf="!widget" class="location text-right text-truncate">
{{ node?.city?.en ?? '-' }} <app-geolocation [data]="node.geolocation" [type]="'list-isp'"></app-geolocation>
</td> </td>
</tr> </tr>
</tbody> </tbody>

View File

@ -1,7 +1,9 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { map, Observable } from 'rxjs'; import { map, Observable } from 'rxjs';
import { INodesRanking, ITopNodesPerCapacity } from 'src/app/interfaces/node-api.interface'; import { INodesRanking, ITopNodesPerCapacity } from 'src/app/interfaces/node-api.interface';
import { SeoService } from 'src/app/services/seo.service';
import { isMobile } from 'src/app/shared/common.utils'; import { isMobile } from 'src/app/shared/common.utils';
import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component';
import { LightningApiService } from '../../lightning-api.service'; import { LightningApiService } from '../../lightning-api.service';
@Component({ @Component({
@ -17,15 +19,34 @@ export class TopNodesPerCapacity implements OnInit {
topNodesPerCapacity$: Observable<ITopNodesPerCapacity[]>; topNodesPerCapacity$: Observable<ITopNodesPerCapacity[]>;
skeletonRows: number[] = []; skeletonRows: number[] = [];
constructor(private apiService: LightningApiService) {} constructor(
private apiService: LightningApiService,
private seoService: SeoService
) {}
ngOnInit(): void { ngOnInit(): void {
if (!this.widget) {
this.seoService.setTitle($localize`Liquidity Ranking`);
}
for (let i = 1; i <= (this.widget ? (isMobile() ? 8 : 7) : 100); ++i) { for (let i = 1; i <= (this.widget ? (isMobile() ? 8 : 7) : 100); ++i) {
this.skeletonRows.push(i); this.skeletonRows.push(i);
} }
if (this.widget === false) { if (this.widget === false) {
this.topNodesPerCapacity$ = this.apiService.getTopNodesByCapacity$(); this.topNodesPerCapacity$ = this.apiService.getTopNodesByCapacity$().pipe(
map((ranking) => {
for (const i in ranking) {
ranking[i].geolocation = <GeolocationData>{
country: ranking[i].country?.en,
city: ranking[i].city?.en,
subdivision: ranking[i].subdivision?.en,
iso: ranking[i].iso_code,
};
}
return ranking;
})
);
} else { } else {
this.topNodesPerCapacity$ = this.nodes$.pipe( this.topNodesPerCapacity$ = this.nodes$.pipe(
map((ranking) => { map((ranking) => {

View File

@ -9,7 +9,7 @@
<th class="rank"></th> <th class="rank"></th>
<th class="alias text-left" i18n="nodes.alias">Alias</th> <th class="alias text-left" i18n="nodes.alias">Alias</th>
<th class="channels text-right" i18n="node.channels">Channels</th> <th class="channels text-right" i18n="node.channels">Channels</th>
<th *ngIf="!widget" class="capacity text-right" i18n="lightning.capacity">Capacity</th> <th *ngIf="!widget" class="capacity text-right" i18n="lightning.liquidity">Liquidity</th>
<th *ngIf="!widget" class="timestamp-first text-left" i18n="lightning.first_seen">First seen</th> <th *ngIf="!widget" class="timestamp-first text-left" i18n="lightning.first_seen">First seen</th>
<th *ngIf="!widget" class="timestamp-update text-left" i18n="lightning.last_update">Last update</th> <th *ngIf="!widget" class="timestamp-update text-left" i18n="lightning.last_update">Last update</th>
<th *ngIf="!widget" class="location text-right" i18n="lightning.location">Location</th> <th *ngIf="!widget" class="location text-right" i18n="lightning.location">Location</th>
@ -35,9 +35,9 @@
<app-timestamp [customFormat]="'yyyy-MM-dd'" [unixTime]="node.updatedAt"></app-timestamp> <app-timestamp [customFormat]="'yyyy-MM-dd'" [unixTime]="node.updatedAt"></app-timestamp>
</td> </td>
<td *ngIf="!widget" class="location text-right text-truncate"> <td *ngIf="!widget" class="location text-right text-truncate">
{{ node?.city?.en ?? '-' }} <app-geolocation [data]="node.geolocation" [type]="'list-isp'"></app-geolocation>
</td> </td>
</tr> </tr>
</tbody> </tbody>
<ng-template #skeleton> <ng-template #skeleton>
<tbody> <tbody>

View File

@ -1,7 +1,9 @@
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
import { map, Observable } from 'rxjs'; import { map, Observable } from 'rxjs';
import { INodesRanking, ITopNodesPerChannels } from 'src/app/interfaces/node-api.interface'; import { INodesRanking, ITopNodesPerChannels } from 'src/app/interfaces/node-api.interface';
import { SeoService } from 'src/app/services/seo.service';
import { isMobile } from 'src/app/shared/common.utils'; import { isMobile } from 'src/app/shared/common.utils';
import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component';
import { LightningApiService } from '../../lightning-api.service'; import { LightningApiService } from '../../lightning-api.service';
@Component({ @Component({
@ -17,15 +19,34 @@ export class TopNodesPerChannels implements OnInit {
topNodesPerChannels$: Observable<ITopNodesPerChannels[]>; topNodesPerChannels$: Observable<ITopNodesPerChannels[]>;
skeletonRows: number[] = []; skeletonRows: number[] = [];
constructor(private apiService: LightningApiService) {} constructor(
private apiService: LightningApiService,
private seoService: SeoService
) {}
ngOnInit(): void { ngOnInit(): void {
if (!this.widget) {
this.seoService.setTitle($localize`Connectivity Ranking`);
}
for (let i = 1; i <= (this.widget ? (isMobile() ? 8 : 7) : 100); ++i) { for (let i = 1; i <= (this.widget ? (isMobile() ? 8 : 7) : 100); ++i) {
this.skeletonRows.push(i); this.skeletonRows.push(i);
} }
if (this.widget === false) { if (this.widget === false) {
this.topNodesPerChannels$ = this.apiService.getTopNodesByChannels$(); this.topNodesPerChannels$ = this.apiService.getTopNodesByChannels$().pipe(
map((ranking) => {
for (const i in ranking) {
ranking[i].geolocation = <GeolocationData>{
country: ranking[i].country?.en,
city: ranking[i].city?.en,
subdivision: ranking[i].subdivision?.en,
iso: ranking[i].iso_code,
};
}
return ranking;
})
);
} else { } else {
this.topNodesPerChannels$ = this.nodes$.pipe( this.topNodesPerChannels$ = this.nodes$.pipe(
map((ranking) => { map((ranking) => {

View File

@ -267,10 +267,14 @@ export class ApiService {
return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/isp/' + isp); return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/isp/' + isp);
} }
getNodesPerCountry(): Observable<any> { getNodesPerCountry$(): Observable<any> {
return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/countries'); return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/countries');
} }
getWorldNodes$(): Observable<any> {
return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/world');
}
getChannelsGeo$(publicKey?: string, style?: 'graph' | 'nodepage' | 'widget' | 'channelpage'): Observable<any> { getChannelsGeo$(publicKey?: string, style?: 'graph' | 'nodepage' | 'widget' | 'channelpage'): Observable<any> {
return this.httpClient.get<any[]>( return this.httpClient.get<any[]>(
this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/channels-geo' + this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/channels-geo' +

View File

@ -83,13 +83,13 @@ export class OpenGraphService {
waitOver(event) { waitOver(event) {
if (this.previewLoadingEvents[event]) { if (this.previewLoadingEvents[event]) {
this.previewLoadingEvents[event]--; this.previewLoadingEvents[event]--;
if (this.previewLoadingEvents[event] === 0) { if (this.previewLoadingEvents[event] === 0 && this.previewLoadingCount > 0) {
delete this.previewLoadingEvents[event] delete this.previewLoadingEvents[event]
this.previewLoadingCount--; this.previewLoadingCount--;
} }
} if (this.previewLoadingCount === 0) {
if (this.previewLoadingCount === 0) { this.metaService.updateTag({ property: 'og:preview:ready', content: 'ready'});
this.metaService.updateTag({ property: 'og:preview:ready', content: 'ready'}); }
} }
} }

View File

@ -1,5 +1,8 @@
&lrm;{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis | number : digitsInfo }} <span *ngIf="valueOverride !== undefined">{{ valueOverride }}</span>
<span class="symbol"><ng-template [ngIf]="network === 'liquid'">L-</ng-template> <span *ngIf="valueOverride === undefined">&lrm;{{ addPlus && satoshis >= 0 ? '+' : '' }}{{ satoshis | number : digitsInfo }} </span>
<ng-template [ngIf]="network === 'liquidtestnet'">tL-</ng-template> <span class="symbol">
<ng-template [ngIf]="network === 'testnet'">t-</ng-template> <ng-template [ngIf]="network === 'liquid'">L-</ng-template>
<ng-template [ngIf]="network === 'signet'">s-</ng-template>sats</span> <ng-template [ngIf]="network === 'liquidtestnet'">tL-</ng-template>
<ng-template [ngIf]="network === 'testnet'">t-</ng-template>
<ng-template [ngIf]="network === 'signet'">s-</ng-template>sats
</span>

View File

@ -11,6 +11,7 @@ export class SatsComponent implements OnInit {
@Input() satoshis: number; @Input() satoshis: number;
@Input() digitsInfo = '1.0-0'; @Input() digitsInfo = '1.0-0';
@Input() addPlus = false; @Input() addPlus = false;
@Input() valueOverride: string | undefined = undefined;
network = ''; network = '';
stateSubscription: Subscription; stateSubscription: Subscription;

View File

@ -1,4 +1,7 @@
&lrm;{{ seconds * 1000 | date: customFormat ?? 'yyyy-MM-dd HH:mm' }} <span *ngIf="seconds === undefined">-</span>
<div class="lg-inline"> <span *ngIf="seconds !== undefined">
<i class="symbol">(<app-time-since [time]="seconds" [fastRender]="true"></app-time-since>)</i> &lrm;{{ seconds * 1000 | date: customFormat ?? 'yyyy-MM-dd HH:mm' }}
</div> <div class="lg-inline">
<i class="symbol">(<app-time-since [time]="seconds" [fastRender]="true"></app-time-since>)</i>
</div>
</span>

View File

@ -11,15 +11,13 @@ export class TimestampComponent implements OnChanges {
@Input() dateString: string; @Input() dateString: string;
@Input() customFormat: string; @Input() customFormat: string;
seconds: number; seconds: number | undefined = undefined;
constructor() { }
ngOnChanges(): void { ngOnChanges(): void {
if (this.unixTime) { if (this.unixTime) {
this.seconds = this.unixTime; this.seconds = this.unixTime;
} else if (this.dateString) { } else if (this.dateString) {
this.seconds = new Date(this.dateString).getTime() / 1000 this.seconds = new Date(this.dateString).getTime() / 1000;
} }
} }

View File

@ -89,7 +89,7 @@ body {
.preview-box { .preview-box {
min-height: 520px; min-height: 520px;
padding: 1.5rem 3rem; padding: 1rem 3rem 1.5rem;
} }
@media (max-width: 767.98px) { @media (max-width: 767.98px) {

View File

@ -1009,7 +1009,6 @@ osSudo "${MEMPOOL_USER}" git clone --branch "${MEMPOOL_REPO_BRANCH}" "${MEMPOOL_
osSudo "${MEMPOOL_USER}" ln -s mempool/production/mempool-build-all upgrade osSudo "${MEMPOOL_USER}" ln -s mempool/production/mempool-build-all upgrade
osSudo "${MEMPOOL_USER}" ln -s mempool/production/mempool-kill-all stop osSudo "${MEMPOOL_USER}" ln -s mempool/production/mempool-kill-all stop
osSudo "${MEMPOOL_USER}" ln -s mempool/production/mempool-start-all start osSudo "${MEMPOOL_USER}" ln -s mempool/production/mempool-start-all start
osSudo "${MEMPOOL_USER}" ln -s mempool/production/mempool-restart-all restart
case $OS in case $OS in

View File

@ -1,8 +0,0 @@
#!/usr/bin/env zsh
HOSTNAME=$(hostname)
echo restarting mempool backends | wall
echo "${HOSTNAME} restarted mempool backends" | /usr/local/bin/keybase chat send --nonblock --channel general mempool.ops
ps uaxw|grep 'dist/index'|grep -v grep|grep -v services|awk '{print $2}'|xargs -n 1 kill
exit 0

View File

@ -87,6 +87,10 @@ export default class ReusablePage extends ConcurrencyImplementation {
page.repairRequested = true; page.repairRequested = true;
}); });
await page.goto(defaultUrl, { waitUntil: "load" }); await page.goto(defaultUrl, { waitUntil: "load" });
await Promise.race([
page.waitForSelector('meta[property="og:preview:ready"]', { timeout: config.PUPPETEER.RENDER_TIMEOUT || 3000 }).then(() => true),
page.waitForSelector('meta[property="og:preview:fail"]', { timeout: config.PUPPETEER.RENDER_TIMEOUT || 3000 }).then(() => false)
])
page.free = true; page.free = true;
return page return page
} }

View File

@ -100,14 +100,13 @@ class Server {
} }
} }
const waitForReady = await page.$('meta[property="og:preview:loading"]'); // wait for preview component to initialize
let success = true; await page.waitForSelector('meta[property="og:preview:loading"]', { timeout: config.PUPPETEER.RENDER_TIMEOUT || 3000 })
if (waitForReady != null) { let success = false;
success = await Promise.race([ success = await Promise.race([
page.waitForSelector('meta[property="og:preview:ready"]', { timeout: config.PUPPETEER.RENDER_TIMEOUT || 3000 }).then(() => true), page.waitForSelector('meta[property="og:preview:ready"]', { timeout: config.PUPPETEER.RENDER_TIMEOUT || 3000 }).then(() => true),
page.waitForSelector('meta[property="og:preview:fail"]', { timeout: config.PUPPETEER.RENDER_TIMEOUT || 3000 }).then(() => false) page.waitForSelector('meta[property="og:preview:fail"]', { timeout: config.PUPPETEER.RENDER_TIMEOUT || 3000 }).then(() => false)
]) ])
}
if (success) { if (success) {
const screenshot = await page.screenshot(); const screenshot = await page.screenshot();
return screenshot; return screenshot;

View File

@ -56,8 +56,7 @@ const routes = {
const networks = { const networks = {
bitcoin: { bitcoin: {
fallbackImg: '/resources/mempool-space-preview.png', fallbackImg: '/resources/previews/dashboard.png',
staticImg: '/resources/previews/dashboard.png',
routes: { routes: {
...routes // all routes supported ...routes // all routes supported
} }