Merge branch 'master' into fix-liquid-cb-previews
This commit is contained in:
		
						commit
						c92fcd20f7
					
				@ -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:
 | 
				
			||||||
 | 
				
			|||||||
@ -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",
 | 
				
			||||||
 | 
				
			|||||||
@ -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');
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -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,
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
@ -382,12 +434,14 @@ class NodesApi {
 | 
				
			|||||||
        SELECT nodes.public_key, CAST(COALESCE(nodes.capacity, 0) as INT) as capacity, CAST(COALESCE(nodes.channels, 0) as INT) as channels,
 | 
					        SELECT nodes.public_key, CAST(COALESCE(nodes.capacity, 0) as INT) as capacity, CAST(COALESCE(nodes.channels, 0) as INT) as channels,
 | 
				
			||||||
          nodes.alias, UNIX_TIMESTAMP(nodes.first_seen) as first_seen, UNIX_TIMESTAMP(nodes.updated_at) as updated_at,
 | 
					          nodes.alias, UNIX_TIMESTAMP(nodes.first_seen) as first_seen, UNIX_TIMESTAMP(nodes.updated_at) as updated_at,
 | 
				
			||||||
          geo_names_city.names as city, geo_names_country.names as country,
 | 
					          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
 | 
					          geo_names_iso.names as iso_code, geo_names_subdivision.names as subdivision,
 | 
				
			||||||
 | 
					          nodes.longitude, nodes.latitude, nodes.as_number, geo_names_isp.names as isp
 | 
				
			||||||
        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_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'
 | 
					        LEFT JOIN geo_names geo_names_subdivision on geo_names_subdivision.id = nodes.subdivision_id AND geo_names_subdivision.type = 'division'
 | 
				
			||||||
 | 
					        LEFT JOIN geo_names geo_names_isp on geo_names_isp.id = nodes.as_number AND geo_names_isp.type = 'as_organization'
 | 
				
			||||||
        WHERE geo_names_country.id = ?
 | 
					        WHERE geo_names_country.id = ?
 | 
				
			||||||
        ORDER BY capacity DESC
 | 
					        ORDER BY capacity DESC
 | 
				
			||||||
      `;
 | 
					      `;
 | 
				
			||||||
@ -397,6 +451,7 @@ class NodesApi {
 | 
				
			|||||||
        rows[i].country = JSON.parse(rows[i].country);
 | 
					        rows[i].country = JSON.parse(rows[i].country);
 | 
				
			||||||
        rows[i].city = JSON.parse(rows[i].city);
 | 
					        rows[i].city = JSON.parse(rows[i].city);
 | 
				
			||||||
        rows[i].subdivision = JSON.parse(rows[i].subdivision);
 | 
					        rows[i].subdivision = JSON.parse(rows[i].subdivision);
 | 
				
			||||||
 | 
					        rows[i].isp = JSON.parse(rows[i].isp);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      return rows;
 | 
					      return rows;
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
@ -411,7 +466,8 @@ class NodesApi {
 | 
				
			|||||||
        SELECT nodes.public_key, CAST(COALESCE(nodes.capacity, 0) as INT) as capacity, CAST(COALESCE(nodes.channels, 0) as INT) as channels,
 | 
					        SELECT nodes.public_key, CAST(COALESCE(nodes.capacity, 0) as INT) as capacity, CAST(COALESCE(nodes.channels, 0) as INT) as channels,
 | 
				
			||||||
          nodes.alias, UNIX_TIMESTAMP(nodes.first_seen) as first_seen, UNIX_TIMESTAMP(nodes.updated_at) as updated_at,
 | 
					          nodes.alias, UNIX_TIMESTAMP(nodes.first_seen) as first_seen, UNIX_TIMESTAMP(nodes.updated_at) as updated_at,
 | 
				
			||||||
          geo_names_city.names as city, geo_names_country.names as country,
 | 
					          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
 | 
					          geo_names_iso.names as iso_code, geo_names_subdivision.names as subdivision,
 | 
				
			||||||
 | 
					          nodes.longitude, nodes.latitude
 | 
				
			||||||
        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'
 | 
				
			||||||
 | 
				
			|||||||
@ -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(
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										37
									
								
								backend/src/api/fetch-version.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								backend/src/api/fetch-version.ts
									
									
									
									
									
										Normal 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"
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
@ -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;
 | 
				
			||||||
 | 
				
			|||||||
@ -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));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -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) {
 | 
				
			||||||
 | 
				
			|||||||
@ -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
									
								
							
							
						
						
									
										2
									
								
								docker/backend/start.sh
									
									
									
									
									
										
										
										Normal file → Executable 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!__CLN_SOCKET__!${__CLN_SOCKET__}!g" mempool-config.json
 | 
					sed -i "s!__CLN_SOCKET__!${__CLN_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
									
								
							
							
						
						
									
										0
									
								
								docker/backend/wait-for-it.sh
									
									
									
									
									
										
										
										Normal file → Executable 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"
 | 
				
			||||||
 | 
				
			|||||||
@ -170,6 +170,10 @@
 | 
				
			|||||||
          },
 | 
					          },
 | 
				
			||||||
          "configurations": {
 | 
					          "configurations": {
 | 
				
			||||||
            "production": {
 | 
					            "production": {
 | 
				
			||||||
 | 
					              "assets": [
 | 
				
			||||||
 | 
					                "src/favicon.ico",
 | 
				
			||||||
 | 
					                "src/robots.txt"
 | 
				
			||||||
 | 
					              ],
 | 
				
			||||||
              "fileReplacements": [
 | 
					              "fileReplacements": [
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                  "replace": "src/environments/environment.ts",
 | 
					                  "replace": "src/environments/environment.ts",
 | 
				
			||||||
 | 
				
			|||||||
@ -34,7 +34,7 @@
 | 
				
			|||||||
    "start:local-staging": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-staging",
 | 
					    "start:local-staging": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c local-staging",
 | 
				
			||||||
    "start:mixed": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c mixed",
 | 
					    "start:mixed": "npm run generate-config && npm run sync-assets-dev && npm run ng -- serve -c mixed",
 | 
				
			||||||
    "build": "npm run generate-config && npm run ng -- build --configuration production --localize && npm run sync-assets && npm run build-mempool.js",
 | 
					    "build": "npm run generate-config && npm run ng -- build --configuration production --localize && npm run sync-assets && npm run build-mempool.js",
 | 
				
			||||||
    "sync-assets": "node sync-assets.js && rsync -av ./dist/mempool/browser/en-US/resources ./dist/mempool/browser/resources",
 | 
					    "sync-assets": "rsync -av ./src/resources ./dist/mempool/browser && node sync-assets.js",
 | 
				
			||||||
    "sync-assets-dev": "node sync-assets.js dev",
 | 
					    "sync-assets-dev": "node sync-assets.js dev",
 | 
				
			||||||
    "generate-config": "node generate-config.js",
 | 
					    "generate-config": "node generate-config.js",
 | 
				
			||||||
    "build-mempool.js": "npm run build-mempool-js && npm run build-mempool-liquid-js && npm run build-mempool-bisq-js",
 | 
					    "build-mempool.js": "npm run build-mempool-js && npm run build-mempool-liquid-js && npm run build-mempool-bisq-js",
 | 
				
			||||||
 | 
				
			|||||||
@ -187,8 +187,8 @@
 | 
				
			|||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <div class="selfhosted-integrations-sponsor">
 | 
					  <div class="community-integrations-sponsor">
 | 
				
			||||||
    <h3 i18n="about.self-hosted-integrations">Self-Hosted Integrations</h3>
 | 
					    <h3 i18n="about.community-integrations">Community Integrations</h3>
 | 
				
			||||||
    <div class="wrapper">
 | 
					    <div class="wrapper">
 | 
				
			||||||
      <a href="https://github.com/getumbrel/umbrel" target="_blank" title="Umbrel">
 | 
					      <a href="https://github.com/getumbrel/umbrel" target="_blank" title="Umbrel">
 | 
				
			||||||
        <img class="image" src="/resources/profile/umbrel.png" />
 | 
					        <img class="image" src="/resources/profile/umbrel.png" />
 | 
				
			||||||
@ -218,18 +218,24 @@
 | 
				
			|||||||
        <img class="image" src="/resources/profile/start9.png" />
 | 
					        <img class="image" src="/resources/profile/start9.png" />
 | 
				
			||||||
        <span>EmbassyOS</span>
 | 
					        <span>EmbassyOS</span>
 | 
				
			||||||
      </a>
 | 
					      </a>
 | 
				
			||||||
    </div>
 | 
					      <a href="https://github.com/btcpayserver/btcpayserver" target="_blank" title="BTCPay Server">
 | 
				
			||||||
  </div>
 | 
					        <img class="image" src="/resources/profile/btcpayserver.svg" />
 | 
				
			||||||
 | 
					        <span>BTCPay</span>
 | 
				
			||||||
  <div class="community-integrations-sponsor">
 | 
					      </a>
 | 
				
			||||||
    <h3 i18n="about.wallet-integrations">Wallet Integrations</h3>
 | 
					 | 
				
			||||||
    <div class="wrapper">
 | 
					 | 
				
			||||||
      <a href="https://github.com/bisq-network/bisq" target="_blank" title="Bisq">
 | 
					      <a href="https://github.com/bisq-network/bisq" target="_blank" title="Bisq">
 | 
				
			||||||
        <img class="image" src="/resources/profile/bisq_network.png" />
 | 
					        <img class="image" src="/resources/profile/bisq_network.png" />
 | 
				
			||||||
        <span>Bisq</span>
 | 
					        <span>Bisq</span>
 | 
				
			||||||
      </a>
 | 
					      </a>
 | 
				
			||||||
 | 
					      <a href="https://github.com/BlueWallet/BlueWallet" target="_blank" title="BlueWallet">
 | 
				
			||||||
 | 
					        <img class="image" src="/resources/profile/bluewallet.png" />
 | 
				
			||||||
 | 
					        <span>BlueWallet</span>
 | 
				
			||||||
 | 
					      </a>
 | 
				
			||||||
 | 
					      <a href="https://github.com/muun/apollo" target="_blank" title="Muun Wallet">
 | 
				
			||||||
 | 
					        <img class="image" src="/resources/profile/muun.png" />
 | 
				
			||||||
 | 
					        <span>Muun</span>
 | 
				
			||||||
 | 
					      </a>
 | 
				
			||||||
      <a href="https://github.com/spesmilo/electrum" target="_blank" title="Electrum Wallet">
 | 
					      <a href="https://github.com/spesmilo/electrum" target="_blank" title="Electrum Wallet">
 | 
				
			||||||
        <img class="image" src="/resources/profile/electrum.jpg" />
 | 
					        <img class="image" src="/resources/profile/electrum.png" />
 | 
				
			||||||
        <span>Electrum</span>
 | 
					        <span>Electrum</span>
 | 
				
			||||||
      </a>
 | 
					      </a>
 | 
				
			||||||
      <a href="https://github.com/cryptoadvance/specter-desktop" target="_blank" title="Specter Wallet">
 | 
					      <a href="https://github.com/cryptoadvance/specter-desktop" target="_blank" title="Specter Wallet">
 | 
				
			||||||
@ -244,18 +250,14 @@
 | 
				
			|||||||
        <img class="image" src="/resources/profile/phoenix.jpg" />
 | 
					        <img class="image" src="/resources/profile/phoenix.jpg" />
 | 
				
			||||||
        <span>Phoenix</span>
 | 
					        <span>Phoenix</span>
 | 
				
			||||||
      </a>
 | 
					      </a>
 | 
				
			||||||
 | 
					      <a href="https://github.com/lnbits/lnbits-legend" target="_blank" title="LNbits">
 | 
				
			||||||
 | 
					        <img class="image" src="/resources/profile/lnbits.svg" />
 | 
				
			||||||
 | 
					        <span>LNBits</span>
 | 
				
			||||||
 | 
					      </a>
 | 
				
			||||||
      <a href="https://github.com/layer2tech/mercury-wallet" target="_blank" title="Mercury Wallet">
 | 
					      <a href="https://github.com/layer2tech/mercury-wallet" target="_blank" title="Mercury Wallet">
 | 
				
			||||||
        <img class="image" src="/resources/profile/mercury.svg" />
 | 
					        <img class="image" src="/resources/profile/mercury.svg" />
 | 
				
			||||||
        <span>Mercury</span>
 | 
					        <span>Mercury</span>
 | 
				
			||||||
      </a>
 | 
					      </a>
 | 
				
			||||||
      <a href="https://github.com/muun/apollo" target="_blank" title="Muun Wallet">
 | 
					 | 
				
			||||||
        <img class="image" src="/resources/profile/muun.png" />
 | 
					 | 
				
			||||||
        <span>Muun</span>
 | 
					 | 
				
			||||||
      </a>
 | 
					 | 
				
			||||||
      <a href="https://github.com/BlueWallet/BlueWallet" target="_blank" title="BlueWallet">
 | 
					 | 
				
			||||||
        <img class="image" src="/resources/profile/bluewallet.png" />
 | 
					 | 
				
			||||||
        <span>BlueWallet</span>
 | 
					 | 
				
			||||||
      </a>
 | 
					 | 
				
			||||||
      <a href="https://github.com/hsjoberg/blixt-wallet" target="_blank" title="Blixt Wallet">
 | 
					      <a href="https://github.com/hsjoberg/blixt-wallet" target="_blank" title="Blixt Wallet">
 | 
				
			||||||
        <img class="image" src="/resources/profile/blixt.png" />
 | 
					        <img class="image" src="/resources/profile/blixt.png" />
 | 
				
			||||||
        <span>Blixt</span>
 | 
					        <span>Blixt</span>
 | 
				
			||||||
 | 
				
			|||||||
@ -43,7 +43,6 @@
 | 
				
			|||||||
  .alliances,
 | 
					  .alliances,
 | 
				
			||||||
  .enterprise-sponsor,
 | 
					  .enterprise-sponsor,
 | 
				
			||||||
  .community-integrations-sponsor,
 | 
					  .community-integrations-sponsor,
 | 
				
			||||||
  .selfhosted-integrations-sponsor,
 | 
					 | 
				
			||||||
  .maintainers {
 | 
					  .maintainers {
 | 
				
			||||||
    margin-top: 68px;
 | 
					    margin-top: 68px;
 | 
				
			||||||
    margin-bottom: 68px;
 | 
					    margin-bottom: 68px;
 | 
				
			||||||
@ -117,7 +116,6 @@
 | 
				
			|||||||
  .community-sponsor,
 | 
					  .community-sponsor,
 | 
				
			||||||
  .project-translators,
 | 
					  .project-translators,
 | 
				
			||||||
  .community-integrations-sponsor,
 | 
					  .community-integrations-sponsor,
 | 
				
			||||||
  .selfhosted-integrations-sponsor,
 | 
					 | 
				
			||||||
  .maintainers {
 | 
					  .maintainers {
 | 
				
			||||||
    .wrapper {
 | 
					    .wrapper {
 | 
				
			||||||
      display: inline-block;
 | 
					      display: inline-block;
 | 
				
			||||||
@ -193,6 +191,6 @@
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.community-integrations-sponsor {
 | 
					.community-integrations-sponsor {
 | 
				
			||||||
  max-width: 830px;
 | 
					  max-width: 970px;
 | 
				
			||||||
  margin: auto;
 | 
					  margin: auto;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -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({
 | 
				
			||||||
 | 
				
			|||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -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 {
 | 
				
			||||||
 | 
				
			|||||||
@ -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>
 | 
				
			||||||
 | 
				
			|||||||
@ -16,7 +16,8 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  <div class="clearfix"></div>
 | 
					  <div class="clearfix"></div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <app-nodes-channels-map *ngIf="!error && (channelGeo$ | async) as channelGeo" [style]="'channelpage'" [channel]="channelGeo"></app-nodes-channels-map>
 | 
					  <app-nodes-channels-map *ngIf="!error && (channelGeo$ | async) as channelGeo" [style]="'channelpage'"
 | 
				
			||||||
 | 
					    [channel]="channelGeo"></app-nodes-channels-map>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <div class="box">
 | 
					  <div class="box">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -25,13 +26,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>
 | 
				
			||||||
@ -47,38 +52,57 @@
 | 
				
			|||||||
          </table>
 | 
					          </table>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div class="w-100 d-block d-md-none"></div>
 | 
				
			||||||
    </div>
 | 
					      <div class="col-md">
 | 
				
			||||||
 | 
					        <table class="table table-borderless table-striped">
 | 
				
			||||||
    <br>
 | 
					          <tbody>
 | 
				
			||||||
 | 
					            <tr>
 | 
				
			||||||
    <div class="row row-cols-1 row-cols-md-2">
 | 
					              <td i18n="address.total-received">Capacity</td>
 | 
				
			||||||
      <div class="col">
 | 
					              <td>
 | 
				
			||||||
        <app-channel-box [channel]="channel.node_left"></app-channel-box>
 | 
					                <app-sats [satoshis]="channel.capacity"></app-sats>
 | 
				
			||||||
 | 
					                <app-fiat [value]="channel.capacity" digitsInfo="1.0-0"></app-fiat>
 | 
				
			||||||
 | 
					              </td>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					          </tbody>
 | 
				
			||||||
 | 
					        </table>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <div class="col">
 | 
					 | 
				
			||||||
        <app-channel-box [channel]="channel.node_right"></app-channel-box>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    <br>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <ng-container *ngIf="transactions$ | async as transactions">
 | 
					  </div>
 | 
				
			||||||
      <ng-template [ngIf]="transactions[0]">
 | 
					
 | 
				
			||||||
        <div class="d-flex">
 | 
					  <br>
 | 
				
			||||||
          <h3>Opening transaction</h3>
 | 
					
 | 
				
			||||||
          <button type="button" class="btn btn-outline-info details-button btn-sm" (click)="txList1.toggleDetails()" i18n="transaction.details|Transaction Details">Details</button>
 | 
					  <div class="row row-cols-1 row-cols-md-2">
 | 
				
			||||||
        </div>
 | 
					    <div class="col">
 | 
				
			||||||
        <app-transactions-list #txList1 [transactions]="[transactions[0]]" [showConfirmations]="true" [rowLimit]="5"></app-transactions-list>
 | 
					      <app-channel-box [channel]="channel.node_left"></app-channel-box>
 | 
				
			||||||
      </ng-template>
 | 
					    </div>
 | 
				
			||||||
      <ng-template [ngIf]="transactions[1]">
 | 
					    <div class="col">
 | 
				
			||||||
        <div class="closing-header d-flex">
 | 
					      <app-channel-box [channel]="channel.node_right"></app-channel-box>
 | 
				
			||||||
          <h3 style="margin: 0;">Closing transaction</h3>  <app-closing-type [type]="channel.closing_reason"></app-closing-type>
 | 
					    </div>
 | 
				
			||||||
          <button type="button" class="btn btn-outline-info details-button btn-sm" (click)="txList2.toggleDetails()" i18n="transaction.details|Transaction Details">Details</button>
 | 
					  </div>
 | 
				
			||||||
        </div>
 | 
					
 | 
				
			||||||
        <app-transactions-list #txList2 [transactions]="[transactions[1]]" [showConfirmations]="true" [rowLimit]="5"></app-transactions-list>
 | 
					  <br>
 | 
				
			||||||
      </ng-template>
 | 
					
 | 
				
			||||||
    </ng-container>
 | 
					  <ng-container *ngIf="transactions$ | async as transactions">
 | 
				
			||||||
 | 
					    <ng-template [ngIf]="transactions[0]">
 | 
				
			||||||
 | 
					      <div class="d-flex">
 | 
				
			||||||
 | 
					        <h3>Opening transaction</h3>
 | 
				
			||||||
 | 
					        <button type="button" class="btn btn-outline-info details-button btn-sm" (click)="txList1.toggleDetails()"
 | 
				
			||||||
 | 
					          i18n="transaction.details|Transaction Details">Details</button>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <app-transactions-list #txList1 [transactions]="[transactions[0]]" [showConfirmations]="true" [rowLimit]="5">
 | 
				
			||||||
 | 
					      </app-transactions-list>
 | 
				
			||||||
 | 
					    </ng-template>
 | 
				
			||||||
 | 
					    <ng-template [ngIf]="transactions[1]">
 | 
				
			||||||
 | 
					      <div class="closing-header d-flex">
 | 
				
			||||||
 | 
					        <h3 style="margin: 0;">Closing transaction</h3>  <app-closing-type [type]="channel.closing_reason">
 | 
				
			||||||
 | 
					        </app-closing-type>
 | 
				
			||||||
 | 
					        <button type="button" class="btn btn-outline-info details-button btn-sm" (click)="txList2.toggleDetails()"
 | 
				
			||||||
 | 
					          i18n="transaction.details|Transaction Details">Details</button>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <app-transactions-list #txList2 [transactions]="[transactions[1]]" [showConfirmations]="true" [rowLimit]="5">
 | 
				
			||||||
 | 
					      </app-transactions-list>
 | 
				
			||||||
 | 
					    </ng-template>
 | 
				
			||||||
 | 
					  </ng-container>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -104,7 +128,7 @@
 | 
				
			|||||||
    <div class="badges mb-2">
 | 
					    <div class="badges mb-2">
 | 
				
			||||||
      <span class="skeleton-loader" style="width: 50px; height: 22px; margin-top: 5px;"></span>
 | 
					      <span class="skeleton-loader" style="width: 50px; height: 22px; margin-top: 5px;"></span>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  
 | 
					
 | 
				
			||||||
    <div class="clearfix"></div>
 | 
					    <div class="clearfix"></div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div style="height: 413px;  padding: 15px;">
 | 
					    <div style="height: 413px;  padding: 15px;">
 | 
				
			||||||
@ -148,4 +172,4 @@
 | 
				
			|||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
</ng-template>
 | 
					</ng-template>
 | 
				
			||||||
 | 
				
			|||||||
@ -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);
 | 
				
			||||||
 | 
				
			|||||||
@ -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"> </th>
 | 
					    <th class="alias text-left d-none d-md-table-cell" i18n="channels.transaction"> </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>
 | 
				
			||||||
 | 
				
			|||||||
@ -21,7 +21,7 @@ export class LightningDashboardComponent implements OnInit {
 | 
				
			|||||||
  ) { }
 | 
					  ) { }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ngOnInit(): void {
 | 
					  ngOnInit(): void {
 | 
				
			||||||
    this.seoService.setTitle($localize`Lightning Dashboard`);
 | 
					    this.seoService.setTitle($localize`Lightning Network`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.nodesRanking$ = this.lightningApiService.getNodesRanking$().pipe(share());
 | 
					    this.nodesRanking$ = this.lightningApiService.getNodesRanking$().pipe(share());
 | 
				
			||||||
    this.statistics$ = this.lightningApiService.getLatestStatistics$().pipe(share());
 | 
					    this.statistics$ = this.lightningApiService.getLatestStatistics$().pipe(share());
 | 
				
			||||||
 | 
				
			|||||||
@ -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>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,16 +1,13 @@
 | 
				
			|||||||
<div class="full-container">
 | 
					<div class="full-container" [class]="widget ? 'widget' : ''">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <div class="card-header">
 | 
					  <div *ngIf="!widget" 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>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <div *ngIf="observable$ | async" class="chart" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
 | 
					  <div *ngIf="observable$ | async" class="chart" [class]="widget ? 'widget' : ''" echarts [initOpts]="chartInitOptions" [options]="chartOptions"
 | 
				
			||||||
    (chartInit)="onChartInit($event)">
 | 
					    (chartInit)="onChartInit($event)">
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -16,6 +16,11 @@
 | 
				
			|||||||
    padding-bottom: 100px;
 | 
					    padding-bottom: 100px;
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					.full-container.widget {
 | 
				
			||||||
 | 
					  min-height: 240px;
 | 
				
			||||||
 | 
					  height: 240px;
 | 
				
			||||||
 | 
					  padding: 0px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.chart {
 | 
					.chart {
 | 
				
			||||||
  width: 100%;
 | 
					  width: 100%;
 | 
				
			||||||
@ -38,3 +43,6 @@
 | 
				
			|||||||
    padding-bottom: 55px;
 | 
					    padding-bottom: 55px;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					.chart.widget {
 | 
				
			||||||
 | 
					  padding: 0px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,14 +1,15 @@
 | 
				
			|||||||
import { ChangeDetectionStrategy, Component, NgZone, OnDestroy, OnInit } from '@angular/core';
 | 
					import { ChangeDetectionStrategy, Component, Inject, Input, 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,11 @@ 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 {
 | 
				
			||||||
 | 
					  @Input() widget: boolean = false;
 | 
				
			||||||
 | 
					  @Input() nodes: any[] | undefined = undefined;
 | 
				
			||||||
 | 
					  @Input() type: 'none' | 'isp' | 'country' = 'none';
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
  observable$: Observable<any>;
 | 
					  observable$: Observable<any>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  chartInstance = undefined;
 | 
					  chartInstance = undefined;
 | 
				
			||||||
@ -26,44 +31,88 @@ 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.nodes ? [this.nodes] : this.apiService.getWorldNodes$()
 | 
				
			||||||
    ]).pipe(tap((data) => {
 | 
					    ).pipe(tap((data) => {
 | 
				
			||||||
      registerMap('world', data[0]);
 | 
					      registerMap('world', data[0]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const countries = [];
 | 
					      let maxLiquidity = data[1].maxLiquidity;
 | 
				
			||||||
      let max = 0;
 | 
					      let inputNodes: any[] = data[1].nodes;
 | 
				
			||||||
      for (const country of data[1]) {
 | 
					      let mapCenter: number[] = [0, 5];
 | 
				
			||||||
        countries.push({
 | 
					      if (this.type === 'country') {
 | 
				
			||||||
          name: country.name.en,
 | 
					        mapCenter = [0, 0];
 | 
				
			||||||
          value: country.count,
 | 
					      } else if (this.type === 'isp') {
 | 
				
			||||||
          iso: country.iso.toLowerCase(),
 | 
					        mapCenter = [0, 10];
 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
        max = Math.max(max, country.count);
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      this.prepareChartOptions(countries, max);
 | 
					      let mapZoom = 1.3;
 | 
				
			||||||
 | 
					      if (!inputNodes) {
 | 
				
			||||||
 | 
					        inputNodes = [];
 | 
				
			||||||
 | 
					        for (const node of data[1]) {
 | 
				
			||||||
 | 
					          if (this.type === 'country') {
 | 
				
			||||||
 | 
					            mapCenter[0] += node.longitude;
 | 
				
			||||||
 | 
					            mapCenter[1] += node.latitude;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          inputNodes.push([
 | 
				
			||||||
 | 
					            node.longitude,
 | 
				
			||||||
 | 
					            node.latitude,
 | 
				
			||||||
 | 
					            node.public_key,
 | 
				
			||||||
 | 
					            node.alias,
 | 
				
			||||||
 | 
					            node.capacity,
 | 
				
			||||||
 | 
					            node.channels,
 | 
				
			||||||
 | 
					            node.country,
 | 
				
			||||||
 | 
					            node.iso_code,
 | 
				
			||||||
 | 
					          ]);
 | 
				
			||||||
 | 
					          maxLiquidity = Math.max(maxLiquidity ?? 0, node.capacity);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (this.type === 'country') {
 | 
				
			||||||
 | 
					          mapCenter[0] /= data[1].length;
 | 
				
			||||||
 | 
					          mapCenter[1] /= data[1].length;
 | 
				
			||||||
 | 
					          mapZoom = 6;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const nodes: any[] = [];
 | 
				
			||||||
 | 
					      for (const node of inputNodes) {
 | 
				
			||||||
 | 
					        // We add a bit of noise so nodes at the same location are not all
 | 
				
			||||||
 | 
					        // on top of each other
 | 
				
			||||||
 | 
					        const random = Math.random() * 2 * Math.PI;
 | 
				
			||||||
 | 
					        const random2 = Math.random() * 0.01;
 | 
				
			||||||
 | 
					        nodes.push([
 | 
				
			||||||
 | 
					          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
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      maxLiquidity = Math.max(1, maxLiquidity);
 | 
				
			||||||
 | 
					      this.prepareChartOptions(nodes, maxLiquidity, mapCenter, mapZoom);
 | 
				
			||||||
    }));
 | 
					    }));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  prepareChartOptions(countries, max) {
 | 
					  prepareChartOptions(nodes, maxLiquidity, mapCenter, mapZoom) {
 | 
				
			||||||
    let title: object;
 | 
					    let title: object;
 | 
				
			||||||
    if (countries.length === 0) {
 | 
					    if (nodes.length === 0) {
 | 
				
			||||||
      title = {
 | 
					      title = {
 | 
				
			||||||
        textStyle: {
 | 
					        textStyle: {
 | 
				
			||||||
          color: 'grey',
 | 
					          color: 'grey',
 | 
				
			||||||
@ -76,53 +125,82 @@ 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: mapCenter,
 | 
				
			||||||
 | 
					        zoom: mapZoom,
 | 
				
			||||||
 | 
					        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: {
 | 
				
			||||||
        },
 | 
					            position: function(point, params, dom, rect, size) {
 | 
				
			||||||
      },
 | 
					              return point;
 | 
				
			||||||
      series: {
 | 
					            },
 | 
				
			||||||
        type: 'map',
 | 
					            trigger: 'item',
 | 
				
			||||||
        map: 'world',
 | 
					            show: true,
 | 
				
			||||||
        emphasis: {
 | 
					            backgroundColor: 'rgba(17, 19, 31, 1)',
 | 
				
			||||||
          label: {
 | 
					            borderRadius: 0,
 | 
				
			||||||
            show: false,
 | 
					            shadowColor: 'rgba(0, 0, 0, 0.5)',
 | 
				
			||||||
 | 
					            textStyle: {
 | 
				
			||||||
 | 
					              color: '#b1b1b1',
 | 
				
			||||||
 | 
					              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,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          zlevel: 2,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        data: countries,
 | 
					      ]
 | 
				
			||||||
        itemStyle: {
 | 
					 | 
				
			||||||
          areaColor: '#5A6A6D'
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -134,30 +212,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);
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -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>
 | 
				
			||||||
 | 
				
			|||||||
@ -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) {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,21 +1,71 @@
 | 
				
			|||||||
<div class="container-xl full-height" style="min-height: 335px">
 | 
					<div class="container-xl full-height" style="min-height: 335px">
 | 
				
			||||||
  <h1 class="float-left" i18n="lightning.nodes-in-country">
 | 
					  <h1 i18n="lightning.nodes-in-country">
 | 
				
			||||||
    <span>Lightning nodes in {{ country?.name }}</span>
 | 
					    <span>Lightning nodes in {{ country?.name }}</span>
 | 
				
			||||||
    <span style="font-size: 50px; vertical-align:sub;"> {{ country?.flag }}</span>
 | 
					    <span style="font-size: 50px; vertical-align:sub;"> {{ country?.flag }}</span>
 | 
				
			||||||
  </h1>
 | 
					  </h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <div class="box">
 | 
				
			||||||
 | 
					    <div class="row" *ngIf="nodes$ | async as countryNodes">
 | 
				
			||||||
 | 
					      <div class="col-12 col-md-6">
 | 
				
			||||||
 | 
					        <table class="table table-borderless table-striped">
 | 
				
			||||||
 | 
					          <tbody>
 | 
				
			||||||
 | 
					            <tr>
 | 
				
			||||||
 | 
					              <td i18n="lightning.node-count">Nodes</td>
 | 
				
			||||||
 | 
					              <td>{{ countryNodes.nodes.length }}</td>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					            <tr>
 | 
				
			||||||
 | 
					              <td i18n="lightning.liquidity">Liquidity</td>
 | 
				
			||||||
 | 
					              <td>
 | 
				
			||||||
 | 
					                <app-amount *ngIf="countryNodes.sumLiquidity > 100000000; else smallnode" [satoshis]="countryNodes.sumLiquidity" [digitsInfo]="'1.2-2'" [noFiat]="false"></app-amount>
 | 
				
			||||||
 | 
					                <ng-template #smallnode>
 | 
				
			||||||
 | 
					                  {{ countryNodes.sumLiquidity | amountShortener: 1 }}
 | 
				
			||||||
 | 
					                  <span class="sats" i18n="shared.sats">sats</span>
 | 
				
			||||||
 | 
					                </ng-template>
 | 
				
			||||||
 | 
					                <span class="d-none d-md-inline-block"> </span>
 | 
				
			||||||
 | 
					                <span class="d-block d-md-none"></span>
 | 
				
			||||||
 | 
					                <app-fiat [value]="countryNodes.sumLiquidity" digitsInfo="1.0-0"></app-fiat>
 | 
				
			||||||
 | 
					              </td>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					            <tr>
 | 
				
			||||||
 | 
					              <td i18n="lightning.channels">Channels</td>
 | 
				
			||||||
 | 
					              <td>{{ countryNodes.sumChannels }}</td>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					            <tr>
 | 
				
			||||||
 | 
					              <td i18n="lightning.isp-count">ISP Count</td>
 | 
				
			||||||
 | 
					              <td>{{ countryNodes.ispCount }}</td>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					            <tr>
 | 
				
			||||||
 | 
					              <td i18n="lightning.top-isp">Top ISP</td>
 | 
				
			||||||
 | 
					              <td class="text-truncate">
 | 
				
			||||||
 | 
					                <a class="d-block text-wrap" [routerLink]="['/lightning/nodes/isp' | relativeUrl, countryNodes.topIsp.id]">
 | 
				
			||||||
 | 
					                  {{ countryNodes.topIsp.name }} [ASN {{ countryNodes.topIsp.id }}]
 | 
				
			||||||
 | 
					                </a>
 | 
				
			||||||
 | 
					              </td>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					          </tbody>
 | 
				
			||||||
 | 
					        </table>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div class="col-12 col-md-6 p-3 p-md-0 pr-md-3">
 | 
				
			||||||
 | 
					        <div style="background-color: #181b2d">
 | 
				
			||||||
 | 
					          <app-nodes-map [widget]="true" [nodes]="countryNodes.nodes" type="country"></app-nodes-map>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <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 countryNodes; else skeleton">
 | 
				
			||||||
        <tr *ngFor="let node of nodes; let i= index; trackBy: trackByPublicKey">
 | 
					        <tr *ngFor="let node of countryNodes.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>
 | 
				
			||||||
          </td>
 | 
					          </td>
 | 
				
			||||||
@ -39,6 +89,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>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,6 @@
 | 
				
			|||||||
import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
 | 
					import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core';
 | 
				
			||||||
import { ActivatedRoute } from '@angular/router';
 | 
					import { ActivatedRoute } from '@angular/router';
 | 
				
			||||||
import { map, Observable } from 'rxjs';
 | 
					import { map, Observable, share } from 'rxjs';
 | 
				
			||||||
import { ApiService } from 'src/app/services/api.service';
 | 
					import { ApiService } from 'src/app/services/api.service';
 | 
				
			||||||
import { SeoService } from 'src/app/services/seo.service';
 | 
					import { SeoService } from 'src/app/services/seo.service';
 | 
				
			||||||
import { getFlagEmoji } from 'src/app/shared/common.utils';
 | 
					import { getFlagEmoji } from 'src/app/shared/common.utils';
 | 
				
			||||||
@ -16,16 +16,24 @@ 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)
 | 
				
			||||||
      .pipe(
 | 
					      .pipe(
 | 
				
			||||||
        map(response => {
 | 
					        map(response => {
 | 
				
			||||||
 | 
					          this.seoService.setTitle($localize`Lightning nodes in ${response.country.en}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          this.country = {
 | 
					          this.country = {
 | 
				
			||||||
            name: response.country.en,
 | 
					            name: response.country.en,
 | 
				
			||||||
            flag: getFlagEmoji(this.route.snapshot.params.country)
 | 
					            flag: getFlagEmoji(this.route.snapshot.params.country)
 | 
				
			||||||
@ -39,14 +47,50 @@ export class NodesPerCountry implements OnInit {
 | 
				
			|||||||
              iso: response.nodes[i].iso_code,
 | 
					              iso: response.nodes[i].iso_code,
 | 
				
			||||||
            };
 | 
					            };
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
          this.seoService.setTitle($localize`Lightning nodes in ${this.country.name}`);
 | 
					          const sumLiquidity = response.nodes.reduce((partialSum, a) => partialSum + a.capacity, 0);
 | 
				
			||||||
          return response.nodes;
 | 
					          const sumChannels = response.nodes.reduce((partialSum, a) => partialSum + a.channels, 0);
 | 
				
			||||||
        })
 | 
					          const isps = {};
 | 
				
			||||||
 | 
					          const topIsp = {
 | 
				
			||||||
 | 
					            count: 0,
 | 
				
			||||||
 | 
					            id: '',
 | 
				
			||||||
 | 
					            name: '',
 | 
				
			||||||
 | 
					          };
 | 
				
			||||||
 | 
					          for (const node of response.nodes) {
 | 
				
			||||||
 | 
					            if (!node.isp) {
 | 
				
			||||||
 | 
					              continue;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if (!isps[node.isp]) {
 | 
				
			||||||
 | 
					              isps[node.isp] = {
 | 
				
			||||||
 | 
					                count: 0,
 | 
				
			||||||
 | 
					                asns: [],
 | 
				
			||||||
 | 
					              };
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if (isps[node.isp].asns.indexOf(node.as_number) === -1) {
 | 
				
			||||||
 | 
					              isps[node.isp].asns.push(node.as_number);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            isps[node.isp].count++;
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            if (isps[node.isp].count > topIsp.count) {
 | 
				
			||||||
 | 
					              topIsp.count = isps[node.isp].count;
 | 
				
			||||||
 | 
					              topIsp.id = isps[node.isp].asns.join(',');
 | 
				
			||||||
 | 
					              topIsp.name = node.isp;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          return {
 | 
				
			||||||
 | 
					            nodes: response.nodes,
 | 
				
			||||||
 | 
					            sumLiquidity: sumLiquidity,
 | 
				
			||||||
 | 
					            sumChannels: sumChannels,
 | 
				
			||||||
 | 
					            topIsp: topIsp,
 | 
				
			||||||
 | 
					            ispCount: Object.keys(isps).length
 | 
				
			||||||
 | 
					          };
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					        share()
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  trackByPublicKey(index: number, node: any) {
 | 
					  trackByPublicKey(index: number, node: any): string {
 | 
				
			||||||
    return node.public_key;
 | 
					    return node.public_key;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -10,14 +10,14 @@
 | 
				
			|||||||
        </p>
 | 
					        </p>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <div class="item">
 | 
					      <div class="item">
 | 
				
			||||||
        <h5 class="card-title d-inline-block">Unknown capacity</h5>
 | 
					        <h5 class="card-title d-inline-block" i18n="lightning.unknown-capacity">Unknown capacity</h5>
 | 
				
			||||||
        <p class="card-text" i18n-ngbTooltip="lightning.unknown-capacity-desc"
 | 
					        <p class="card-text" i18n-ngbTooltip="lightning.unknown-capacity-desc"
 | 
				
			||||||
        ngbTooltip="How much liquidity is running on nodes which ISP was not identifiable" placement="bottom">
 | 
					        ngbTooltip="How much liquidity is running on nodes which ISP was not identifiable" placement="bottom">
 | 
				
			||||||
          <app-amount [satoshis]="stats.unknownCapacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
 | 
					          <app-amount [satoshis]="stats.unknownCapacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
 | 
				
			||||||
        </p>
 | 
					        </p>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <div class="item">
 | 
					      <div class="item">
 | 
				
			||||||
        <h5 class="card-title d-inline-block">Tor capacity</h5>
 | 
					        <h5 class="card-title d-inline-block" i18n="lightning.tor-capacity">Tor capacity</h5>
 | 
				
			||||||
        <p class="card-text" i18n-ngbTooltip="lightning.tor-capacity-desc"
 | 
					        <p class="card-text" i18n-ngbTooltip="lightning.tor-capacity-desc"
 | 
				
			||||||
        ngbTooltip="How much liquidity is running on nodes advertising only Tor addresses" placement="bottom">
 | 
					        ngbTooltip="How much liquidity is running on nodes advertising only Tor addresses" placement="bottom">
 | 
				
			||||||
          <app-amount [satoshis]="stats.torCapacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
 | 
					          <app-amount [satoshis]="stats.torCapacity" [digitsInfo]="'1.2-2'" [noFiat]="true"></app-amount>
 | 
				
			||||||
@ -80,19 +80,19 @@
 | 
				
			|||||||
<ng-template #loadingReward>
 | 
					<ng-template #loadingReward>
 | 
				
			||||||
  <div class="pool-distribution">
 | 
					  <div class="pool-distribution">
 | 
				
			||||||
    <div class="item">
 | 
					    <div class="item">
 | 
				
			||||||
      <h5 class="card-title" i18n="lightning.tagged-isp">Tagged ISPs</h5>
 | 
					      <h5 class="card-title d-inline-block" i18n="lightning.clearnet-capacity">Clearnet capacity</h5>
 | 
				
			||||||
      <p class="card-text">
 | 
					      <p class="card-text">
 | 
				
			||||||
        <span class="skeleton-loader skeleton-loader-big"></span>
 | 
					        <span class="skeleton-loader skeleton-loader-big"></span>
 | 
				
			||||||
      </p>
 | 
					      </p>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <div class="item">
 | 
					    <div class="item">
 | 
				
			||||||
      <h5 class="card-title" i18n="lightning.tagged-capacity">Tagged capacity</h5>
 | 
					      <h5 class="card-title d-inline-block" i18n="lightning.unknown-capacity">Unknown capacity</h5>
 | 
				
			||||||
      <p class="card-text">
 | 
					      <p class="card-text">
 | 
				
			||||||
        <span class="skeleton-loader skeleton-loader-big"></span>
 | 
					        <span class="skeleton-loader skeleton-loader-big"></span>
 | 
				
			||||||
      </p>
 | 
					      </p>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <div class="item">
 | 
					    <div class="item">
 | 
				
			||||||
      <h5 class="card-title" i18n="lightning.tagged-nodes">Tagged nodes</h5>
 | 
					      <h5 class="card-title d-inline-block" i18n="lightning.tor-capacity">Tor capacity</h5>
 | 
				
			||||||
      <p class="card-text">
 | 
					      <p class="card-text">
 | 
				
			||||||
        <span class="skeleton-loader skeleton-loader-big"></span>
 | 
					        <span class="skeleton-loader skeleton-loader-big"></span>
 | 
				
			||||||
      </p>
 | 
					      </p>
 | 
				
			||||||
 | 
				
			|||||||
@ -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) {
 | 
				
			||||||
@ -132,9 +134,6 @@ export class NodesPerISPChartComponent implements OnInit {
 | 
				
			|||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      data.push({
 | 
					      data.push({
 | 
				
			||||||
        itemStyle: {
 | 
					 | 
				
			||||||
          color: isp[0] === null ? '#7D4698' : undefined,
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        value: this.sortBy === 'capacity' ? isp[7] : isp[6],
 | 
					        value: this.sortBy === 'capacity' ? isp[7] : isp[6],
 | 
				
			||||||
        name: isp[1].replace('&', '') + (isMobile() || this.widget ? `` : ` (${this.sortBy === 'capacity' ? isp[7] : isp[6]}%)`),
 | 
					        name: isp[1].replace('&', '') + (isMobile() || this.widget ? `` : ` (${this.sortBy === 'capacity' ? isp[7] : isp[6]}%)`),
 | 
				
			||||||
        label: {
 | 
					        label: {
 | 
				
			||||||
@ -204,7 +203,7 @@ export class NodesPerISPChartComponent implements OnInit {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.chartOptions = {
 | 
					    this.chartOptions = {
 | 
				
			||||||
      color: chartColors.slice(3),
 | 
					      color: chartColors.filter((color) => color != '#5E35B1'), // Remove color that looks like Tor
 | 
				
			||||||
      tooltip: {
 | 
					      tooltip: {
 | 
				
			||||||
        trigger: 'item',
 | 
					        trigger: 'item',
 | 
				
			||||||
        textStyle: {
 | 
					        textStyle: {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,18 +1,68 @@
 | 
				
			|||||||
<div class="container-xl full-height" style="min-height: 335px">
 | 
					<div class="container-xl full-height" style="min-height: 335px">
 | 
				
			||||||
  <h1 class="float-left" i18n="lightning.nodes-for-isp">Lightning nodes on ISP: {{ isp?.name }} [AS {{isp?.id}}]</h1>
 | 
					  <h1 i18n="lightning.nodes-for-isp">Lightning nodes on ISP: {{ isp?.name }}</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <div class="box">
 | 
				
			||||||
 | 
					    <div class="row" *ngIf="nodes$ | async as ispNodes">
 | 
				
			||||||
 | 
					      <div class="col-12 col-md-6">
 | 
				
			||||||
 | 
					        <table class="table table-borderless table-striped">
 | 
				
			||||||
 | 
					          <tbody>
 | 
				
			||||||
 | 
					            <tr>
 | 
				
			||||||
 | 
					              <td i18n="lightning.asn">ASN</td>
 | 
				
			||||||
 | 
					              <td>{{ isp?.id }}</td>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					            <tr>
 | 
				
			||||||
 | 
					              <td i18n="lightning.node-count">Nodes</td>
 | 
				
			||||||
 | 
					              <td>{{ ispNodes.nodes.length }}</td>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					            <tr>
 | 
				
			||||||
 | 
					              <td i18n="lightning.liquidity">Liquidity</td>
 | 
				
			||||||
 | 
					              <td>
 | 
				
			||||||
 | 
					                <app-amount *ngIf="ispNodes.sumLiquidity > 100000000; else smallnode" [satoshis]="ispNodes.sumLiquidity" [digitsInfo]="'1.2-2'" [noFiat]="false"></app-amount>
 | 
				
			||||||
 | 
					                <ng-template #smallnode>
 | 
				
			||||||
 | 
					                  {{ ispNodes.sumLiquidity | amountShortener: 1 }}
 | 
				
			||||||
 | 
					                  <span class="sats" i18n="shared.sats">sats</span>
 | 
				
			||||||
 | 
					                </ng-template>
 | 
				
			||||||
 | 
					                <span class="d-none d-md-inline-block"> </span>
 | 
				
			||||||
 | 
					                <span class="d-block d-md-none"></span>
 | 
				
			||||||
 | 
					                <app-fiat [value]="ispNodes.sumLiquidity" digitsInfo="1.0-0"></app-fiat>
 | 
				
			||||||
 | 
					              </td>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					            <tr>
 | 
				
			||||||
 | 
					              <td i18n="lightning.channels">Channels</td>
 | 
				
			||||||
 | 
					              <td>{{ ispNodes.sumChannels }}</td>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					            <tr>
 | 
				
			||||||
 | 
					              <td i18n="lightning.top-country">Top country</td>
 | 
				
			||||||
 | 
					              <td class="text-truncate">
 | 
				
			||||||
 | 
					                <a class="d-block text-wrap" [routerLink]="['/lightning/nodes/country' | relativeUrl, ispNodes.topCountry.iso]">
 | 
				
			||||||
 | 
					                  <span class="">{{ ispNodes.topCountry.country }} {{ ispNodes.topCountry.flag }}</span>
 | 
				
			||||||
 | 
					                </a>
 | 
				
			||||||
 | 
					              </td>
 | 
				
			||||||
 | 
					            </tr>
 | 
				
			||||||
 | 
					          </tbody>
 | 
				
			||||||
 | 
					        </table>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div class="col-12 col-md-6 p-3 p-md-0 pr-md-3">
 | 
				
			||||||
 | 
					        <div style="background-color: #181b2d">
 | 
				
			||||||
 | 
					          <app-nodes-map [widget]="true" [nodes]="ispNodes.nodes" type="isp"></app-nodes-map>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <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 ispNodes; else skeleton">
 | 
				
			||||||
        <tr *ngFor="let node of nodes; let i= index; trackBy: trackByPublicKey">
 | 
					        <tr *ngFor="let node of ispNodes.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>
 | 
				
			||||||
          </td>
 | 
					          </td>
 | 
				
			||||||
@ -36,6 +86,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>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -59,4 +59,4 @@
 | 
				
			|||||||
  @media (max-width: 576px) {
 | 
					  @media (max-width: 576px) {
 | 
				
			||||||
    display: none
 | 
					    display: none
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,8 +1,9 @@
 | 
				
			|||||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
 | 
					import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
 | 
				
			||||||
import { ActivatedRoute } from '@angular/router';
 | 
					import { ActivatedRoute } from '@angular/router';
 | 
				
			||||||
import { map, Observable } from 'rxjs';
 | 
					import { map, Observable, share } from 'rxjs';
 | 
				
			||||||
import { ApiService } from 'src/app/services/api.service';
 | 
					import { ApiService } from 'src/app/services/api.service';
 | 
				
			||||||
import { SeoService } from 'src/app/services/seo.service';
 | 
					import { SeoService } from 'src/app/services/seo.service';
 | 
				
			||||||
 | 
					import { getFlagEmoji } from 'src/app/shared/common.utils';
 | 
				
			||||||
import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component';
 | 
					import { GeolocationData } from 'src/app/shared/components/geolocation/geolocation.component';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Component({
 | 
					@Component({
 | 
				
			||||||
@ -15,11 +16,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)
 | 
				
			||||||
@ -27,7 +34,7 @@ export class NodesPerISP implements OnInit {
 | 
				
			|||||||
        map(response => {
 | 
					        map(response => {
 | 
				
			||||||
          this.isp = {
 | 
					          this.isp = {
 | 
				
			||||||
            name: response.isp,
 | 
					            name: response.isp,
 | 
				
			||||||
            id: this.route.snapshot.params.isp
 | 
					            id: this.route.snapshot.params.isp.split(',').join(', ')
 | 
				
			||||||
          };
 | 
					          };
 | 
				
			||||||
          this.seoService.setTitle($localize`Lightning nodes on ISP: ${response.isp} [AS${this.route.snapshot.params.isp}]`);
 | 
					          this.seoService.setTitle($localize`Lightning nodes on ISP: ${response.isp} [AS${this.route.snapshot.params.isp}]`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -40,12 +47,40 @@ export class NodesPerISP implements OnInit {
 | 
				
			|||||||
            };
 | 
					            };
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          return response.nodes;
 | 
					          const sumLiquidity = response.nodes.reduce((partialSum, a) => partialSum + a.capacity, 0);
 | 
				
			||||||
        })
 | 
					          const sumChannels = response.nodes.reduce((partialSum, a) => partialSum + a.channels, 0);
 | 
				
			||||||
 | 
					          const countries = {};
 | 
				
			||||||
 | 
					          const topCountry = {
 | 
				
			||||||
 | 
					            count: 0,
 | 
				
			||||||
 | 
					            country: '',
 | 
				
			||||||
 | 
					            iso: '',
 | 
				
			||||||
 | 
					            flag: '',
 | 
				
			||||||
 | 
					          };
 | 
				
			||||||
 | 
					          for (const node of response.nodes) {
 | 
				
			||||||
 | 
					            if (!node.geolocation.iso) {
 | 
				
			||||||
 | 
					              continue;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            countries[node.geolocation.iso] = countries[node.geolocation.iso] ?? 0 + 1;
 | 
				
			||||||
 | 
					            if (countries[node.geolocation.iso] > topCountry.count) {
 | 
				
			||||||
 | 
					              topCountry.count = countries[node.geolocation.iso];
 | 
				
			||||||
 | 
					              topCountry.country = node.geolocation.country;
 | 
				
			||||||
 | 
					              topCountry.iso = node.geolocation.iso;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          topCountry.flag = getFlagEmoji(topCountry.iso);
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					          return {
 | 
				
			||||||
 | 
					            nodes: response.nodes,
 | 
				
			||||||
 | 
					            sumLiquidity: sumLiquidity,
 | 
				
			||||||
 | 
					            sumChannels: sumChannels,
 | 
				
			||||||
 | 
					            topCountry: topCountry,
 | 
				
			||||||
 | 
					          };
 | 
				
			||||||
 | 
					        }),
 | 
				
			||||||
 | 
					        share()
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  trackByPublicKey(index: number, node: any) {
 | 
					  trackByPublicKey(index: number, node: any): string {
 | 
				
			||||||
    return node.public_key;
 | 
					    return node.public_key;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -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>
 | 
				
			||||||
 | 
				
			|||||||
@ -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);
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -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>
 | 
				
			||||||
 | 
				
			|||||||
@ -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) => {
 | 
				
			||||||
 | 
				
			|||||||
@ -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>
 | 
				
			||||||
 | 
				
			|||||||
@ -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) => {
 | 
				
			||||||
 | 
				
			|||||||
@ -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' +
 | 
				
			||||||
 | 
				
			|||||||
@ -1,5 +1,8 @@
 | 
				
			|||||||
‎{{ 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">‎{{ 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>
 | 
				
			||||||
@ -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;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,7 @@
 | 
				
			|||||||
‎{{ 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>
 | 
					  ‎{{ 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>
 | 
				
			||||||
 | 
				
			|||||||
@ -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;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										1
									
								
								frontend/src/resources/profile/btcpayserver.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								frontend/src/resources/profile/btcpayserver.svg
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 105.46 188.47"><defs><style>.cls-1{fill:#cedc21;}.cls-2{fill:#51b13e;}.cls-3{fill:#1e7a44;}.cls-4{fill:#fff;}</style></defs><title>BTCPayServer</title><path class="cls-1" d="M117.24,247.32a11.06,11.06,0,0,1-11-11.06V69.91a11.06,11.06,0,1,1,22.11,0V236.26A11.06,11.06,0,0,1,117.24,247.32Z" transform="translate(-106.19 -58.85)"/><path class="cls-2" d="M117.25,247.32a11.06,11.06,0,0,1-4.75-21l66.66-31.64L110.69,144.2a11.05,11.05,0,1,1,13.11-17.8l83.35,61.41a11,11,0,0,1-1.82,18.88L122,246.25A10.94,10.94,0,0,1,117.25,247.32Z" transform="translate(-106.19 -58.85)"/><path class="cls-1" d="M117.25,181.93a11.05,11.05,0,0,1-6.56-20l68.47-50.45L112.5,79.89a11.05,11.05,0,0,1,9.48-20l83.35,39.56a11.05,11.05,0,0,1,1.82,18.89L123.8,179.78A11,11,0,0,1,117.25,181.93Z" transform="translate(-106.19 -58.85)"/><polygon class="cls-3" points="22.11 70.86 22.11 117.61 53.82 94.25 22.11 70.86"/><rect class="cls-4" y="51.26" width="22.11" height="53.89"/><path class="cls-1" d="M128.3,69.91a11.06,11.06,0,1,0-22.11,0V209H128.3Z" transform="translate(-106.19 -58.85)"/></svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 1.1 KiB  | 
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 19 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								frontend/src/resources/profile/electrum.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								frontend/src/resources/profile/electrum.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 170 KiB  | 
							
								
								
									
										1
									
								
								frontend/src/resources/profile/lnbits.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								frontend/src/resources/profile/lnbits.svg
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 30"><defs><path id="a" d="M33.2619 148.1667h154.2143v68.7917H33.2619z"/></defs><g fill="#1f2234" aria-label="LNbits" font-family="sans-serif" font-size=".3095" font-weight="400" letter-spacing=".0031" style="line-height:1.25;white-space:pre;shape-inside:url(#a)" transform="matrix(72.4607 0 0 72.4607 -2399.2814 -10741.3589)"><g transform="matrix(.00244 0 0 .00244 33.0708 148.1594)"><circle cx="101.2976" cy="116.4167" r="84.6667" fill="#673ab7" fill-rule="evenodd"/><path fill="#eee" d="M79.1105 71.9667v49.0613h13.3803v40.141l31.2208-53.5213h-17.8404l17.8404-35.681z"/></g><g fill="#eee" font-family="roboto"/></g></svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 680 B  | 
@ -4,7 +4,7 @@ var fs = require('fs');
 | 
				
			|||||||
const CONFIG_FILE_NAME = 'mempool-frontend-config.json';
 | 
					const CONFIG_FILE_NAME = 'mempool-frontend-config.json';
 | 
				
			||||||
let configContent = {};
 | 
					let configContent = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var PATH = 'dist/mempool/browser/en-US/resources/';
 | 
					var PATH = 'dist/mempool/browser/resources/';
 | 
				
			||||||
if (process.argv[2] && process.argv[2] === 'dev') {
 | 
					if (process.argv[2] && process.argv[2] === 'dev') {
 | 
				
			||||||
  PATH = 'src/resources/';
 | 
					  PATH = 'src/resources/';
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -18,7 +18,7 @@
 | 
				
			|||||||
		expires 10m;
 | 
							expires 10m;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	location /resources {
 | 
						location /resources {
 | 
				
			||||||
		try_files /$lang/$uri /$lang/$uri/ $uri $uri/ /en-US/$uri @index-redirect;
 | 
							try_files $uri @index-redirect;
 | 
				
			||||||
		expires 1h;
 | 
							expires 1h;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	location @index-redirect {
 | 
						location @index-redirect {
 | 
				
			||||||
@ -27,10 +27,6 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	# location block using regex are matched in order
 | 
						# location block using regex are matched in order
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	# used to rewrite resources from /<lang>/ to /en-US/
 | 
					 | 
				
			||||||
	location ~ ^/(ar|bg|bs|ca|cs|da|de|et|el|es|eo|eu|fa|fr|gl|ko|hr|id|it|he|ka|lv|lt|hu|mk|ms|nl|ja|nb|nn|pl|pt|pt-BR|ro|ru|sk|sl|sr|sh|fi|sv|th|tr|uk|vi|zh|hi)/resources/ {
 | 
					 | 
				
			||||||
		rewrite ^/[a-zA-Z-]*/resources/(.*) /en-US/resources/$1;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	# used for cookie override
 | 
						# used for cookie override
 | 
				
			||||||
	location ~ ^/(ar|bg|bs|ca|cs|da|de|et|el|es|eo|eu|fa|fr|gl|ko|hr|id|it|he|ka|lv|lt|hu|mk|ms|nl|ja|nb|nn|pl|pt|pt-BR|ro|ru|sk|sl|sr|sh|fi|sv|th|tr|uk|vi|zh|hi)/ {
 | 
						location ~ ^/(ar|bg|bs|ca|cs|da|de|et|el|es|eo|eu|fa|fr|gl|ko|hr|id|it|he|ka|lv|lt|hu|mk|ms|nl|ja|nb|nn|pl|pt|pt-BR|ro|ru|sk|sl|sr|sh|fi|sv|th|tr|uk|vi|zh|hi)/ {
 | 
				
			||||||
		try_files $uri $uri/ /$1/index.html =404;
 | 
							try_files $uri $uri/ /$1/index.html =404;
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
				
			||||||
 | 
				
			|||||||
@ -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
 | 
					 | 
				
			||||||
@ -5,5 +5,5 @@
 | 
				
			|||||||
37 13 * * * sleep 30 ; /mempool/mempool.space/backup >/dev/null 2>&1 &
 | 
					37 13 * * * sleep 30 ; /mempool/mempool.space/backup >/dev/null 2>&1 &
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# hourly liquid asset update
 | 
					# hourly liquid asset update
 | 
				
			||||||
6 * * * * cd $HOME/liquid/frontend && npm run sync-assets && rsync -av $HOME/liquid/frontend/dist/mempool/browser/en-US/resources/assets* $HOME/public_html/liquid/en-US/resources/ >/dev/null 2>&1
 | 
					6 * * * * cd $HOME/liquid/frontend && npm run sync-assets && rsync -av $HOME/liquid/frontend/dist/mempool/browser/resources/assets* $HOME/public_html/liquid/resources/ >/dev/null 2>&1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -58,12 +58,6 @@ location = / {
 | 
				
			|||||||
	expires 5m;
 | 
						expires 5m;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# used to rewrite resources from /<lang>/ to /en-US/
 | 
					 | 
				
			||||||
# cache /resources/** for 1 week since they don't change often
 | 
					 | 
				
			||||||
location ~ ^/[a-z][a-z]/resources/(.*) {
 | 
					 | 
				
			||||||
	try_files $uri /en-US/resources/$1 =404;
 | 
					 | 
				
			||||||
	expires 1w;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
# cache /<lang>/main.f40e91d908a068a2.js forever since they never change
 | 
					# cache /<lang>/main.f40e91d908a068a2.js forever since they never change
 | 
				
			||||||
location ~ ^/([a-z][a-z])/(.+\..+\.(js|css)) {
 | 
					location ~ ^/([a-z][a-z])/(.+\..+\.(js|css)) {
 | 
				
			||||||
	try_files $uri =404;
 | 
						try_files $uri =404;
 | 
				
			||||||
@ -84,7 +78,7 @@ location ~ ^/([a-z][a-z])/ {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# cache /resources/** for 1 week since they don't change often
 | 
					# cache /resources/** for 1 week since they don't change often
 | 
				
			||||||
location /resources {
 | 
					location /resources {
 | 
				
			||||||
	try_files $uri /en-US/$uri /en-US/index.html;
 | 
						try_files $uri /en-US/index.html;
 | 
				
			||||||
	expires 1w;
 | 
						expires 1w;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
# cache /main.f40e91d908a068a2.js forever since they never change
 | 
					# cache /main.f40e91d908a068a2.js forever since they never change
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user