Merge branch 'master' into address-unfurls
This commit is contained in:
		
						commit
						49a545e0c7
					
				@ -15,10 +15,11 @@
 | 
				
			|||||||
    "@typescript-eslint/ban-types": 1,
 | 
					    "@typescript-eslint/ban-types": 1,
 | 
				
			||||||
    "@typescript-eslint/no-empty-function": 1,
 | 
					    "@typescript-eslint/no-empty-function": 1,
 | 
				
			||||||
    "@typescript-eslint/no-explicit-any": 1,
 | 
					    "@typescript-eslint/no-explicit-any": 1,
 | 
				
			||||||
    "@typescript-eslint/no-inferrable-types": 1,
 | 
					    "@typescript-eslint/no-inferrable-types": 0,
 | 
				
			||||||
    "@typescript-eslint/no-namespace": 1,
 | 
					    "@typescript-eslint/no-namespace": 1,
 | 
				
			||||||
    "@typescript-eslint/no-this-alias": 1,
 | 
					    "@typescript-eslint/no-this-alias": 1,
 | 
				
			||||||
    "@typescript-eslint/no-var-requires": 1,
 | 
					    "@typescript-eslint/no-var-requires": 1,
 | 
				
			||||||
 | 
					    "@typescript-eslint/explicit-function-return-type": 1,
 | 
				
			||||||
    "no-console": 1,
 | 
					    "no-console": 1,
 | 
				
			||||||
    "no-constant-condition": 1,
 | 
					    "no-constant-condition": 1,
 | 
				
			||||||
    "no-dupe-else-if": 1,
 | 
					    "no-dupe-else-if": 1,
 | 
				
			||||||
@ -28,6 +29,8 @@
 | 
				
			|||||||
    "no-useless-catch": 1,
 | 
					    "no-useless-catch": 1,
 | 
				
			||||||
    "no-var": 1,
 | 
					    "no-var": 1,
 | 
				
			||||||
    "prefer-const": 1,
 | 
					    "prefer-const": 1,
 | 
				
			||||||
    "prefer-rest-params": 1
 | 
					    "prefer-rest-params": 1,
 | 
				
			||||||
 | 
					    "quotes": [1, "single", { "allowTemplateLiterals": true }],
 | 
				
			||||||
 | 
					    "semi": 1
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -9,6 +9,7 @@ export interface AbstractBitcoinApi {
 | 
				
			|||||||
  $getBlockHash(height: number): Promise<string>;
 | 
					  $getBlockHash(height: number): Promise<string>;
 | 
				
			||||||
  $getBlockHeader(hash: string): Promise<string>;
 | 
					  $getBlockHeader(hash: string): Promise<string>;
 | 
				
			||||||
  $getBlock(hash: string): Promise<IEsploraApi.Block>;
 | 
					  $getBlock(hash: string): Promise<IEsploraApi.Block>;
 | 
				
			||||||
 | 
					  $getRawBlock(hash: string): Promise<string>;
 | 
				
			||||||
  $getAddress(address: string): Promise<IEsploraApi.Address>;
 | 
					  $getAddress(address: string): Promise<IEsploraApi.Address>;
 | 
				
			||||||
  $getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
 | 
					  $getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
 | 
				
			||||||
  $getAddressPrefix(prefix: string): string[];
 | 
					  $getAddressPrefix(prefix: string): string[];
 | 
				
			||||||
 | 
				
			|||||||
@ -77,7 +77,8 @@ class BitcoinApi implements AbstractBitcoinApi {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  $getRawBlock(hash: string): Promise<string> {
 | 
					  $getRawBlock(hash: string): Promise<string> {
 | 
				
			||||||
    return this.bitcoindClient.getBlock(hash, 0);
 | 
					    return this.bitcoindClient.getBlock(hash, 0)
 | 
				
			||||||
 | 
					      .then((raw: string) => Buffer.from(raw, "hex"));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  $getBlockHash(height: number): Promise<string> {
 | 
					  $getBlockHash(height: number): Promise<string> {
 | 
				
			||||||
 | 
				
			|||||||
@ -103,9 +103,10 @@ class BitcoinRoutes {
 | 
				
			|||||||
          .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/header', this.getBlockHeader)
 | 
					          .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/header', this.getBlockHeader)
 | 
				
			||||||
          .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight)
 | 
					          .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/height', this.getBlockTipHeight)
 | 
				
			||||||
          .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/hash', this.getBlockTipHash)
 | 
					          .get(config.MEMPOOL.API_URL_PREFIX + 'blocks/tip/hash', this.getBlockTipHash)
 | 
				
			||||||
 | 
					          .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/raw', this.getRawBlock)
 | 
				
			||||||
 | 
					          .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txids', this.getTxIdsForBlock)
 | 
				
			||||||
          .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txs', this.getBlockTransactions)
 | 
					          .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txs', this.getBlockTransactions)
 | 
				
			||||||
          .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txs/:index', this.getBlockTransactions)
 | 
					          .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txs/:index', this.getBlockTransactions)
 | 
				
			||||||
          .get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txids', this.getTxIdsForBlock)
 | 
					 | 
				
			||||||
          .get(config.MEMPOOL.API_URL_PREFIX + 'block-height/:height', this.getBlockHeight)
 | 
					          .get(config.MEMPOOL.API_URL_PREFIX + 'block-height/:height', this.getBlockHeight)
 | 
				
			||||||
          .get(config.MEMPOOL.API_URL_PREFIX + 'address/:address', this.getAddress)
 | 
					          .get(config.MEMPOOL.API_URL_PREFIX + 'address/:address', this.getAddress)
 | 
				
			||||||
          .get(config.MEMPOOL.API_URL_PREFIX + 'address/:address/txs', this.getAddressTransactions)
 | 
					          .get(config.MEMPOOL.API_URL_PREFIX + 'address/:address/txs', this.getAddressTransactions)
 | 
				
			||||||
@ -470,6 +471,16 @@ class BitcoinRoutes {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private async getRawBlock(req: Request, res: Response) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const result = await bitcoinApi.$getRawBlock(req.params.hash);
 | 
				
			||||||
 | 
					      res.setHeader('content-type', 'application/octet-stream');
 | 
				
			||||||
 | 
					      res.send(result);
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      res.status(500).send(e instanceof Error ? e.message : e);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private async getTxIdsForBlock(req: Request, res: Response) {
 | 
					  private async getTxIdsForBlock(req: Request, res: Response) {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const result = await bitcoinApi.$getTxIdsForBlock(req.params.hash);
 | 
					      const result = await bitcoinApi.$getTxIdsForBlock(req.params.hash);
 | 
				
			||||||
 | 
				
			|||||||
@ -50,6 +50,11 @@ class ElectrsApi implements AbstractBitcoinApi {
 | 
				
			|||||||
      .then((response) => response.data);
 | 
					      .then((response) => response.data);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  $getRawBlock(hash: string): Promise<string> {
 | 
				
			||||||
 | 
					    return axios.get<string>(config.ESPLORA.REST_API_URL + '/block/' + hash + "/raw", this.axiosConfig)
 | 
				
			||||||
 | 
					      .then((response) => response.data);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  $getAddress(address: string): Promise<IEsploraApi.Address> {
 | 
					  $getAddress(address: string): Promise<IEsploraApi.Address> {
 | 
				
			||||||
    throw new Error('Method getAddress not implemented.');
 | 
					    throw new Error('Method getAddress not implemented.');
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -9,8 +9,8 @@ class DatabaseMigration {
 | 
				
			|||||||
  private statisticsAddedIndexed = false;
 | 
					  private statisticsAddedIndexed = false;
 | 
				
			||||||
  private uniqueLogs: string[] = [];
 | 
					  private uniqueLogs: string[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private blocksTruncatedMessage = `'blocks' table has been truncated. Re-indexing from scratch.`;
 | 
					  private blocksTruncatedMessage = `'blocks' table has been truncated.`;
 | 
				
			||||||
  private hashratesTruncatedMessage = `'hashrates' table has been truncated. Re-indexing from scratch.`;
 | 
					  private hashratesTruncatedMessage = `'hashrates' table has been truncated.`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Avoid printing multiple time the same message
 | 
					   * Avoid printing multiple time the same message
 | 
				
			||||||
@ -256,7 +256,9 @@ class DatabaseMigration {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (databaseSchemaVersion < 26 && isBitcoin === true) {
 | 
					    if (databaseSchemaVersion < 26 && isBitcoin === true) {
 | 
				
			||||||
      this.uniqueLog(logger.notice, `'lightning_stats' table has been truncated. Will re-generate historical data from scratch.`);
 | 
					      if (config.LIGHTNING.ENABLED) {
 | 
				
			||||||
 | 
					        this.uniqueLog(logger.notice, `'lightning_stats' table has been truncated.`);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
      await this.$executeQuery(`TRUNCATE lightning_stats`);
 | 
					      await this.$executeQuery(`TRUNCATE lightning_stats`);
 | 
				
			||||||
      await this.$executeQuery('ALTER TABLE `lightning_stats` ADD tor_nodes int(11) NOT NULL DEFAULT "0"');
 | 
					      await this.$executeQuery('ALTER TABLE `lightning_stats` ADD tor_nodes int(11) NOT NULL DEFAULT "0"');
 | 
				
			||||||
      await this.$executeQuery('ALTER TABLE `lightning_stats` ADD clearnet_nodes int(11) NOT NULL DEFAULT "0"');
 | 
					      await this.$executeQuery('ALTER TABLE `lightning_stats` ADD clearnet_nodes int(11) NOT NULL DEFAULT "0"');
 | 
				
			||||||
@ -273,6 +275,9 @@ class DatabaseMigration {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    if (databaseSchemaVersion < 28 && isBitcoin === true) {
 | 
					    if (databaseSchemaVersion < 28 && isBitcoin === true) {
 | 
				
			||||||
 | 
					      if (config.LIGHTNING.ENABLED) {
 | 
				
			||||||
 | 
					        this.uniqueLog(logger.notice, `'lightning_stats' and 'node_stats' tables have been truncated.`);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
      await this.$executeQuery(`TRUNCATE lightning_stats`);
 | 
					      await this.$executeQuery(`TRUNCATE lightning_stats`);
 | 
				
			||||||
      await this.$executeQuery(`TRUNCATE node_stats`);
 | 
					      await this.$executeQuery(`TRUNCATE node_stats`);
 | 
				
			||||||
      await this.$executeQuery(`ALTER TABLE lightning_stats MODIFY added DATE`);
 | 
					      await this.$executeQuery(`ALTER TABLE lightning_stats MODIFY added DATE`);
 | 
				
			||||||
 | 
				
			|||||||
@ -5,25 +5,26 @@ class NodesApi {
 | 
				
			|||||||
  public async $getNode(public_key: string): Promise<any> {
 | 
					  public async $getNode(public_key: string): Promise<any> {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const query = `
 | 
					      const query = `
 | 
				
			||||||
        SELECT nodes.*, geo_names_as.names as as_organization, geo_names_city.names as city,
 | 
					        SELECT nodes.*, geo_names_iso.names as iso_code, geo_names_as.names as as_organization, geo_names_city.names as city,
 | 
				
			||||||
        geo_names_country.names as country, geo_names_subdivision.names as subdivision,
 | 
					        geo_names_country.names as country, geo_names_subdivision.names as subdivision,
 | 
				
			||||||
          (SELECT Count(*)
 | 
					          (SELECT Count(*)
 | 
				
			||||||
          FROM channels
 | 
					          FROM channels
 | 
				
			||||||
          WHERE channels.status = 2 AND ( channels.node1_public_key = ? OR channels.node2_public_key = ? )) AS channel_closed_count,
 | 
					          WHERE channels.status = 2 AND ( channels.node1_public_key = ? OR channels.node2_public_key = ? )) AS channel_closed_count,
 | 
				
			||||||
          (SELECT Count(*)
 | 
					          (SELECT Count(*)
 | 
				
			||||||
          FROM channels
 | 
					          FROM channels
 | 
				
			||||||
          WHERE channels.status < 2 AND ( channels.node1_public_key = ? OR channels.node2_public_key = ? )) AS channel_active_count,
 | 
					          WHERE channels.status = 1 AND ( channels.node1_public_key = ? OR channels.node2_public_key = ? )) AS channel_active_count,
 | 
				
			||||||
          (SELECT Sum(capacity)
 | 
					          (SELECT Sum(capacity)
 | 
				
			||||||
          FROM channels
 | 
					          FROM channels
 | 
				
			||||||
          WHERE channels.status < 2 AND ( channels.node1_public_key = ? OR channels.node2_public_key = ? )) AS capacity,
 | 
					          WHERE channels.status = 1 AND ( channels.node1_public_key = ? OR channels.node2_public_key = ? )) AS capacity,
 | 
				
			||||||
          (SELECT Avg(capacity)
 | 
					          (SELECT Avg(capacity)
 | 
				
			||||||
          FROM channels
 | 
					          FROM channels
 | 
				
			||||||
          WHERE status < 2 AND ( node1_public_key = ? OR node2_public_key = ? )) AS channels_capacity_avg
 | 
					          WHERE status = 1 AND ( node1_public_key = ? OR node2_public_key = ? )) AS channels_capacity_avg
 | 
				
			||||||
        FROM nodes
 | 
					        FROM nodes
 | 
				
			||||||
        LEFT JOIN geo_names geo_names_as on geo_names_as.id = as_number
 | 
					        LEFT JOIN geo_names geo_names_as on geo_names_as.id = as_number
 | 
				
			||||||
        LEFT JOIN geo_names geo_names_city on geo_names_city.id = city_id
 | 
					        LEFT JOIN geo_names geo_names_city on geo_names_city.id = city_id
 | 
				
			||||||
        LEFT JOIN geo_names geo_names_subdivision on geo_names_subdivision.id = subdivision_id
 | 
					        LEFT JOIN geo_names geo_names_subdivision on geo_names_subdivision.id = subdivision_id
 | 
				
			||||||
        LEFT JOIN geo_names geo_names_country on geo_names_country.id = country_id
 | 
					        LEFT JOIN geo_names geo_names_country on geo_names_country.id = country_id
 | 
				
			||||||
 | 
					        LEFT JOIN geo_names geo_names_iso ON geo_names_iso.id = nodes.country_id AND geo_names_iso.type = 'country_iso_code'
 | 
				
			||||||
        WHERE public_key = ?
 | 
					        WHERE public_key = ?
 | 
				
			||||||
      `;
 | 
					      `;
 | 
				
			||||||
      const [rows]: any = await DB.query(query, [public_key, public_key, public_key, public_key, public_key, public_key, public_key, public_key, public_key]);
 | 
					      const [rows]: any = await DB.query(query, [public_key, public_key, public_key, public_key, public_key, public_key, public_key, public_key, public_key]);
 | 
				
			||||||
@ -97,29 +98,59 @@ class NodesApi {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public async $getNodesISP() {
 | 
					  public async $getNodesISP(groupBy: string, showTor: boolean) {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      let query = `SELECT GROUP_CONCAT(DISTINCT(nodes.as_number)) as ispId, geo_names.names as names, COUNT(DISTINCT nodes.public_key) as nodesCount, SUM(capacity) as capacity
 | 
					      const orderBy = groupBy === 'capacity' ? `CAST(SUM(capacity) as INT)` : `COUNT(DISTINCT nodes.public_key)`;
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      // Clearnet
 | 
				
			||||||
 | 
					      let query = `SELECT GROUP_CONCAT(DISTINCT(nodes.as_number)) as ispId, geo_names.names as names,
 | 
				
			||||||
 | 
					          COUNT(DISTINCT nodes.public_key) as nodesCount, CAST(SUM(capacity) as INT) as capacity
 | 
				
			||||||
        FROM nodes
 | 
					        FROM nodes
 | 
				
			||||||
        JOIN geo_names ON geo_names.id = nodes.as_number
 | 
					        JOIN geo_names ON geo_names.id = nodes.as_number
 | 
				
			||||||
        JOIN channels ON channels.node1_public_key = nodes.public_key OR channels.node2_public_key = nodes.public_key
 | 
					        JOIN channels ON channels.node1_public_key = nodes.public_key OR channels.node2_public_key = nodes.public_key
 | 
				
			||||||
        GROUP BY geo_names.names
 | 
					        GROUP BY geo_names.names
 | 
				
			||||||
        ORDER BY COUNT(DISTINCT nodes.public_key) DESC
 | 
					        ORDER BY ${orderBy} DESC
 | 
				
			||||||
      `;
 | 
					      `;      
 | 
				
			||||||
      const [nodesCountPerAS]: any = await DB.query(query);
 | 
					      const [nodesCountPerAS]: any = await DB.query(query);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      query = `SELECT COUNT(*) as total FROM nodes WHERE as_number IS NOT NULL`;
 | 
					      let total = 0;
 | 
				
			||||||
      const [nodesWithAS]: any = await DB.query(query);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const nodesPerAs: any[] = [];
 | 
					      const nodesPerAs: any[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      for (const asGroup of nodesCountPerAS) {
 | 
				
			||||||
 | 
					        if (groupBy === 'capacity') {
 | 
				
			||||||
 | 
					          total += asGroup.capacity;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          total += asGroup.nodesCount;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // Tor
 | 
				
			||||||
 | 
					      if (showTor) {
 | 
				
			||||||
 | 
					        query = `SELECT COUNT(DISTINCT nodes.public_key) as nodesCount, CAST(SUM(capacity) as INT) as capacity
 | 
				
			||||||
 | 
					          FROM nodes
 | 
				
			||||||
 | 
					          JOIN channels ON channels.node1_public_key = nodes.public_key OR channels.node2_public_key = nodes.public_key
 | 
				
			||||||
 | 
					          ORDER BY ${orderBy} DESC
 | 
				
			||||||
 | 
					        `;      
 | 
				
			||||||
 | 
					        const [nodesCountTor]: any = await DB.query(query);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        total += groupBy === 'capacity' ? nodesCountTor[0].capacity : nodesCountTor[0].nodesCount;
 | 
				
			||||||
 | 
					        nodesPerAs.push({
 | 
				
			||||||
 | 
					          ispId: null,
 | 
				
			||||||
 | 
					          name: 'Tor',
 | 
				
			||||||
 | 
					          count: nodesCountTor[0].nodesCount,
 | 
				
			||||||
 | 
					          share: Math.floor((groupBy === 'capacity' ? nodesCountTor[0].capacity : nodesCountTor[0].nodesCount) / total * 10000) / 100,
 | 
				
			||||||
 | 
					          capacity: nodesCountTor[0].capacity,
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      for (const as of nodesCountPerAS) {
 | 
					      for (const as of nodesCountPerAS) {
 | 
				
			||||||
        nodesPerAs.push({
 | 
					        nodesPerAs.push({
 | 
				
			||||||
          ispId: as.ispId,
 | 
					          ispId: as.ispId,
 | 
				
			||||||
          name: JSON.parse(as.names),
 | 
					          name: JSON.parse(as.names),
 | 
				
			||||||
          count: as.nodesCount,
 | 
					          count: as.nodesCount,
 | 
				
			||||||
          share: Math.floor(as.nodesCount / nodesWithAS[0].total * 10000) / 100,
 | 
					          share: Math.floor((groupBy === 'capacity' ? as.capacity : as.nodesCount) / total * 10000) / 100,
 | 
				
			||||||
          capacity: as.capacity,
 | 
					          capacity: as.capacity,
 | 
				
			||||||
        })
 | 
					        });
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return nodesPerAs;
 | 
					      return nodesPerAs;
 | 
				
			||||||
 | 
				
			|||||||
@ -9,10 +9,10 @@ class NodesRoutes {
 | 
				
			|||||||
  public initRoutes(app: Application) {
 | 
					  public initRoutes(app: Application) {
 | 
				
			||||||
    app
 | 
					    app
 | 
				
			||||||
      .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/isp/:isp', this.$getNodesPerISP)
 | 
					 | 
				
			||||||
      .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/top', this.$getTopNodes)
 | 
					      .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/top', this.$getTopNodes)
 | 
				
			||||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/isp', this.$getNodesISP)
 | 
					      .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/isp-ranking', this.$getISPRanking)
 | 
				
			||||||
 | 
					      .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/isp/:isp', this.$getNodesPerISP)
 | 
				
			||||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/countries', this.$getNodesCountries)
 | 
					      .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/countries', this.$getNodesCountries)
 | 
				
			||||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key/statistics', this.$getHistoricalNodeStats)
 | 
					      .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key/statistics', this.$getHistoricalNodeStats)
 | 
				
			||||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key', this.$getNode)
 | 
					      .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key', this.$getNode)
 | 
				
			||||||
@ -63,9 +63,18 @@ class NodesRoutes {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private async $getNodesISP(req: Request, res: Response) {
 | 
					  private async $getISPRanking(req: Request, res: Response): Promise<void> {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const nodesPerAs = await nodesApi.$getNodesISP();
 | 
					      const groupBy = req.query.groupBy as string;
 | 
				
			||||||
 | 
					      const showTor = req.query.showTor as string === 'true' ? true : false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (!['capacity', 'node-count'].includes(groupBy)) {
 | 
				
			||||||
 | 
					        res.status(400).send(`groupBy must be one of 'capacity' or 'node-count'`);
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const nodesPerAs = await nodesApi.$getNodesISP(groupBy, showTor);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      res.header('Pragma', 'public');
 | 
					      res.header('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());
 | 
				
			||||||
 | 
				
			|||||||
@ -13,7 +13,7 @@ class StatisticsApi {
 | 
				
			|||||||
      query += ` WHERE added BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
 | 
					      query += ` WHERE added BETWEEN DATE_SUB(NOW(), INTERVAL ${interval}) AND NOW()`;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    query += ` ORDER BY id DESC`;
 | 
					    query += ` ORDER BY added DESC`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const [rows]: any = await DB.query(query);
 | 
					      const [rows]: any = await DB.query(query);
 | 
				
			||||||
@ -26,8 +26,8 @@ class StatisticsApi {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  public async $getLatestStatistics(): Promise<any> {
 | 
					  public async $getLatestStatistics(): Promise<any> {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const [rows]: any = await DB.query(`SELECT * FROM lightning_stats ORDER BY id DESC LIMIT 1`);
 | 
					      const [rows]: any = await DB.query(`SELECT * FROM lightning_stats ORDER BY added DESC LIMIT 1`);
 | 
				
			||||||
      const [rows2]: any = await DB.query(`SELECT * FROM lightning_stats ORDER BY id DESC LIMIT 1 OFFSET 7`);
 | 
					      const [rows2]: any = await DB.query(`SELECT * FROM lightning_stats ORDER BY added DESC LIMIT 1 OFFSET 7`);
 | 
				
			||||||
      return {
 | 
					      return {
 | 
				
			||||||
        latest: rows[0],
 | 
					        latest: rows[0],
 | 
				
			||||||
        previous: rows2[0],
 | 
					        previous: rows2[0],
 | 
				
			||||||
 | 
				
			|||||||
@ -38,11 +38,13 @@ class NodeSyncService {
 | 
				
			|||||||
        await $lookupNodeLocation();
 | 
					        await $lookupNodeLocation();
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await this.$setChannelsInactive();
 | 
					      const graphChannelsIds: string[] = [];
 | 
				
			||||||
 | 
					 | 
				
			||||||
      for (const channel of networkGraph.channels) {
 | 
					      for (const channel of networkGraph.channels) {
 | 
				
			||||||
        await this.$saveChannel(channel);
 | 
					        await this.$saveChannel(channel);
 | 
				
			||||||
 | 
					        graphChannelsIds.push(channel.id);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      await this.$setChannelsInactive(graphChannelsIds);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      logger.info(`Channels updated.`);
 | 
					      logger.info(`Channels updated.`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await this.$findInactiveNodesAndChannels();
 | 
					      await this.$findInactiveNodesAndChannels();
 | 
				
			||||||
@ -106,7 +108,22 @@ class NodeSyncService {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      // @ts-ignore
 | 
					      // @ts-ignore
 | 
				
			||||||
      const [channels]: [ILightningApi.Channel[]] = await DB.query(`SELECT channels.id FROM channels WHERE channels.status = 1 AND ((SELECT COUNT(*) FROM nodes WHERE nodes.public_key = channels.node1_public_key) = 0 OR (SELECT COUNT(*) FROM nodes WHERE nodes.public_key = channels.node2_public_key) = 0)`);
 | 
					      const [channels]: [ILightningApi.Channel[]] = await DB.query(`
 | 
				
			||||||
 | 
					        SELECT channels.id
 | 
				
			||||||
 | 
					        FROM channels
 | 
				
			||||||
 | 
					        WHERE channels.status = 1
 | 
				
			||||||
 | 
					        AND (
 | 
				
			||||||
 | 
					          (
 | 
				
			||||||
 | 
					            SELECT COUNT(*)
 | 
				
			||||||
 | 
					            FROM nodes
 | 
				
			||||||
 | 
					            WHERE nodes.public_key = channels.node1_public_key
 | 
				
			||||||
 | 
					          ) = 0
 | 
				
			||||||
 | 
					        OR (
 | 
				
			||||||
 | 
					            SELECT COUNT(*)
 | 
				
			||||||
 | 
					            FROM nodes
 | 
				
			||||||
 | 
					            WHERE nodes.public_key = channels.node2_public_key
 | 
				
			||||||
 | 
					          ) = 0)
 | 
				
			||||||
 | 
					        `);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      for (const channel of channels) {
 | 
					      for (const channel of channels) {
 | 
				
			||||||
        await this.$updateChannelStatus(channel.id, 0);
 | 
					        await this.$updateChannelStatus(channel.id, 0);
 | 
				
			||||||
@ -356,9 +373,16 @@ class NodeSyncService {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private async $setChannelsInactive(): Promise<void> {
 | 
					  private async $setChannelsInactive(graphChannelsIds: string[]): Promise<void> {
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      await DB.query(`UPDATE channels SET status = 0 WHERE status = 1`);
 | 
					      await DB.query(`
 | 
				
			||||||
 | 
					        UPDATE channels
 | 
				
			||||||
 | 
					        SET status = 0
 | 
				
			||||||
 | 
					        WHERE short_id NOT IN (
 | 
				
			||||||
 | 
					          ${graphChannelsIds.map(id => `"${id}"`).join(',')}
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        AND status != 2
 | 
				
			||||||
 | 
					      `);
 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
      logger.err('$setChannelsInactive() error: ' + (e instanceof Error ? e.message : e));
 | 
					      logger.err('$setChannelsInactive() error: ' + (e instanceof Error ? e.message : e));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -141,7 +141,22 @@ class LightningStatsUpdater {
 | 
				
			|||||||
    try {
 | 
					    try {
 | 
				
			||||||
      logger.info(`Running daily node stats update...`);
 | 
					      logger.info(`Running daily node stats update...`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const query = `SELECT nodes.public_key, c1.channels_count_left, c2.channels_count_right, c1.channels_capacity_left, c2.channels_capacity_right FROM nodes LEFT JOIN (SELECT node1_public_key, COUNT(id) AS channels_count_left, SUM(capacity) AS channels_capacity_left FROM channels WHERE channels.status < 2 GROUP BY node1_public_key) c1 ON c1.node1_public_key = nodes.public_key LEFT JOIN (SELECT node2_public_key, COUNT(id) AS channels_count_right, SUM(capacity) AS channels_capacity_right FROM channels WHERE channels.status < 2 GROUP BY node2_public_key) c2 ON c2.node2_public_key = nodes.public_key`;
 | 
					      const query = `
 | 
				
			||||||
 | 
					        SELECT nodes.public_key, c1.channels_count_left, c2.channels_count_right, c1.channels_capacity_left,
 | 
				
			||||||
 | 
					          c2.channels_capacity_right
 | 
				
			||||||
 | 
					        FROM nodes
 | 
				
			||||||
 | 
					        LEFT JOIN (
 | 
				
			||||||
 | 
					          SELECT node1_public_key, COUNT(id) AS channels_count_left, SUM(capacity) AS channels_capacity_left
 | 
				
			||||||
 | 
					          FROM channels
 | 
				
			||||||
 | 
					          WHERE channels.status = 1
 | 
				
			||||||
 | 
					          GROUP BY node1_public_key
 | 
				
			||||||
 | 
					        ) c1 ON c1.node1_public_key = nodes.public_key
 | 
				
			||||||
 | 
					        LEFT JOIN (
 | 
				
			||||||
 | 
					          SELECT node2_public_key, COUNT(id) AS channels_count_right, SUM(capacity) AS channels_capacity_right
 | 
				
			||||||
 | 
					          FROM channels WHERE channels.status = 1 GROUP BY node2_public_key
 | 
				
			||||||
 | 
					        ) c2 ON c2.node2_public_key = nodes.public_key
 | 
				
			||||||
 | 
					      `;
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
      const [nodes]: any = await DB.query(query);
 | 
					      const [nodes]: any = await DB.query(query);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      for (const node of nodes) {
 | 
					      for (const node of nodes) {
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										3
									
								
								contributors/oleonardolima.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								contributors/oleonardolima.txt
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					I hereby accept the terms of the Contributor License Agreement in the CONTRIBUTING.md file of the mempool/mempool git repository as of July 25, 2022.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Signed: oleonardolima
 | 
				
			||||||
@ -14,10 +14,11 @@
 | 
				
			|||||||
    "@typescript-eslint/ban-types": 1,
 | 
					    "@typescript-eslint/ban-types": 1,
 | 
				
			||||||
    "@typescript-eslint/no-empty-function": 1,
 | 
					    "@typescript-eslint/no-empty-function": 1,
 | 
				
			||||||
    "@typescript-eslint/no-explicit-any": 1,
 | 
					    "@typescript-eslint/no-explicit-any": 1,
 | 
				
			||||||
    "@typescript-eslint/no-inferrable-types": 1,
 | 
					    "@typescript-eslint/no-inferrable-types": 0,
 | 
				
			||||||
    "@typescript-eslint/no-namespace": 1,
 | 
					    "@typescript-eslint/no-namespace": 1,
 | 
				
			||||||
    "@typescript-eslint/no-this-alias": 1,
 | 
					    "@typescript-eslint/no-this-alias": 1,
 | 
				
			||||||
    "@typescript-eslint/no-var-requires": 1,
 | 
					    "@typescript-eslint/no-var-requires": 1,
 | 
				
			||||||
 | 
					    "@typescript-eslint/explicit-function-return-type": 1,
 | 
				
			||||||
    "no-case-declarations": 1,
 | 
					    "no-case-declarations": 1,
 | 
				
			||||||
    "no-console": 1,
 | 
					    "no-console": 1,
 | 
				
			||||||
    "no-constant-condition": 1,
 | 
					    "no-constant-condition": 1,
 | 
				
			||||||
@ -29,6 +30,8 @@
 | 
				
			|||||||
    "no-useless-catch": 1,
 | 
					    "no-useless-catch": 1,
 | 
				
			||||||
    "no-var": 1,
 | 
					    "no-var": 1,
 | 
				
			||||||
    "prefer-const": 1,
 | 
					    "prefer-const": 1,
 | 
				
			||||||
    "prefer-rest-params": 1
 | 
					    "prefer-rest-params": 1,
 | 
				
			||||||
 | 
					    "quotes": [1, "single", { "allowTemplateLiterals": true }],
 | 
				
			||||||
 | 
					    "semi": 1
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -5,9 +5,9 @@ const P2SH_P2WSH_COST  = 35 * 4; // the WU cost for the non-witness part of P2SH
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export function calcSegwitFeeGains(tx: Transaction) {
 | 
					export function calcSegwitFeeGains(tx: Transaction) {
 | 
				
			||||||
  // calculated in weight units
 | 
					  // calculated in weight units
 | 
				
			||||||
  let realizedBech32Gains = 0;
 | 
					  let realizedSegwitGains = 0;
 | 
				
			||||||
  let potentialBech32Gains = 0;
 | 
					  let potentialSegwitGains = 0;
 | 
				
			||||||
  let potentialP2shGains = 0;
 | 
					  let potentialP2shSegwitGains = 0;
 | 
				
			||||||
  let potentialTaprootGains = 0;
 | 
					  let potentialTaprootGains = 0;
 | 
				
			||||||
  let realizedTaprootGains = 0;
 | 
					  let realizedTaprootGains = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -24,31 +24,33 @@ export function calcSegwitFeeGains(tx: Transaction) {
 | 
				
			|||||||
    const isP2tr         = vin.prevout.scriptpubkey_type === 'v1_p2tr';
 | 
					    const isP2tr         = vin.prevout.scriptpubkey_type === 'v1_p2tr';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const op = vin.scriptsig ? vin.scriptsig_asm.split(' ')[0] : null;
 | 
					    const op = vin.scriptsig ? vin.scriptsig_asm.split(' ')[0] : null;
 | 
				
			||||||
    const isP2sh2Wpkh = isP2sh && !!vin.witness && op === 'OP_PUSHBYTES_22';
 | 
					    const isP2shP2Wpkh = isP2sh && !!vin.witness && op === 'OP_PUSHBYTES_22';
 | 
				
			||||||
    const isP2sh2Wsh  = isP2sh && !!vin.witness && op === 'OP_PUSHBYTES_34';
 | 
					    const isP2shP2Wsh  = isP2sh && !!vin.witness && op === 'OP_PUSHBYTES_34';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    switch (true) {
 | 
					    switch (true) {
 | 
				
			||||||
      // Native Segwit - P2WPKH/P2WSH (Bech32)
 | 
					      // Native Segwit - P2WPKH/P2WSH/P2TR
 | 
				
			||||||
      case isP2wpkh:
 | 
					      case isP2wpkh:
 | 
				
			||||||
      case isP2wsh:
 | 
					      case isP2wsh:
 | 
				
			||||||
      case isP2tr:
 | 
					      case isP2tr:
 | 
				
			||||||
        // maximal gains: the scriptSig is moved entirely to the witness part
 | 
					        // maximal gains: the scriptSig is moved entirely to the witness part
 | 
				
			||||||
        realizedBech32Gains += witnessSize(vin) * 3;
 | 
					        // if taproot is used savings are 42 WU higher because it produces smaller signatures and doesn't require a pubkey in the witness
 | 
				
			||||||
 | 
					        // this number is explained above `realizedTaprootGains += 42;`
 | 
				
			||||||
 | 
					        realizedSegwitGains += (witnessSize(vin) + (isP2tr ? 42 : 0)) * 3;
 | 
				
			||||||
        // XXX P2WSH output creation is more expensive, should we take this into consideration?
 | 
					        // XXX P2WSH output creation is more expensive, should we take this into consideration?
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // Backward compatible Segwit - P2SH-P2WPKH
 | 
					      // Backward compatible Segwit - P2SH-P2WPKH
 | 
				
			||||||
      case isP2sh2Wpkh:
 | 
					      case isP2shP2Wpkh:
 | 
				
			||||||
        // the scriptSig is moved to the witness, but we have extra 21 extra non-witness bytes (48 WU)
 | 
					        // the scriptSig is moved to the witness, but we have extra 21 extra non-witness bytes (84 WU)
 | 
				
			||||||
        realizedBech32Gains += witnessSize(vin) * 3 - P2SH_P2WPKH_COST;
 | 
					        realizedSegwitGains += witnessSize(vin) * 3 - P2SH_P2WPKH_COST;
 | 
				
			||||||
        potentialBech32Gains += P2SH_P2WPKH_COST;
 | 
					        potentialSegwitGains += P2SH_P2WPKH_COST;
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // Backward compatible Segwit - P2SH-P2WSH
 | 
					      // Backward compatible Segwit - P2SH-P2WSH
 | 
				
			||||||
      case isP2sh2Wsh:
 | 
					      case isP2shP2Wsh:
 | 
				
			||||||
        // the scriptSig is moved to the witness, but we have extra 35 extra non-witness bytes
 | 
					        // the scriptSig is moved to the witness, but we have extra 35 extra non-witness bytes (140 WU)
 | 
				
			||||||
        realizedBech32Gains += witnessSize(vin) * 3 - P2SH_P2WSH_COST;
 | 
					        realizedSegwitGains += witnessSize(vin) * 3 - P2SH_P2WSH_COST;
 | 
				
			||||||
        potentialBech32Gains += P2SH_P2WSH_COST;
 | 
					        potentialSegwitGains += P2SH_P2WSH_COST;
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      // Non-segwit P2PKH/P2SH/P2PK/bare multisig
 | 
					      // Non-segwit P2PKH/P2SH/P2PK/bare multisig
 | 
				
			||||||
@ -56,9 +58,13 @@ export function calcSegwitFeeGains(tx: Transaction) {
 | 
				
			|||||||
      case isP2sh:
 | 
					      case isP2sh:
 | 
				
			||||||
      case isP2pk:
 | 
					      case isP2pk:
 | 
				
			||||||
      case isBareMultisig: {
 | 
					      case isBareMultisig: {
 | 
				
			||||||
        const fullGains = scriptSigSize(vin) * 3;
 | 
					        let fullGains = scriptSigSize(vin) * 3;
 | 
				
			||||||
        potentialBech32Gains += fullGains;
 | 
					        if (isBareMultisig) {
 | 
				
			||||||
        potentialP2shGains += fullGains - (isP2pkh ? P2SH_P2WPKH_COST : P2SH_P2WSH_COST);
 | 
					          // a _bare_ multisig has the keys in the output script, but P2SH and P2WSH require them to be in the scriptSig/scriptWitness
 | 
				
			||||||
 | 
					          fullGains -= vin.prevout.scriptpubkey.length / 2;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        potentialSegwitGains += fullGains;
 | 
				
			||||||
 | 
					        potentialP2shSegwitGains += fullGains - (isP2pkh ? P2SH_P2WPKH_COST : P2SH_P2WSH_COST);
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@ -79,11 +85,11 @@ export function calcSegwitFeeGains(tx: Transaction) {
 | 
				
			|||||||
        // TODO maybe add some complex scripts that are specified somewhere, so that size is known, such as lightning scripts
 | 
					        // TODO maybe add some complex scripts that are specified somewhere, so that size is known, such as lightning scripts
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      const script = isP2sh2Wsh || isP2wsh ? vin.inner_witnessscript_asm : vin.inner_redeemscript_asm;
 | 
					      const script = isP2shP2Wsh || isP2wsh ? vin.inner_witnessscript_asm : vin.inner_redeemscript_asm;
 | 
				
			||||||
      let replacementSize: number;
 | 
					      let replacementSize: number;
 | 
				
			||||||
      if (
 | 
					      if (
 | 
				
			||||||
        // single sig
 | 
					        // single sig
 | 
				
			||||||
        isP2pk || isP2pkh || isP2wpkh || isP2sh2Wpkh ||
 | 
					        isP2pk || isP2pkh || isP2wpkh || isP2shP2Wpkh ||
 | 
				
			||||||
        // multisig
 | 
					        // multisig
 | 
				
			||||||
        isBareMultisig || parseMultisigScript(script)
 | 
					        isBareMultisig || parseMultisigScript(script)
 | 
				
			||||||
      ) {
 | 
					      ) {
 | 
				
			||||||
@ -105,11 +111,11 @@ export function calcSegwitFeeGains(tx: Transaction) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  // returned as percentage of the total tx weight
 | 
					  // returned as percentage of the total tx weight
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    realizedBech32Gains: realizedBech32Gains / (tx.weight + realizedBech32Gains), // percent of the pre-segwit tx size
 | 
					    realizedSegwitGains: realizedSegwitGains / (tx.weight + realizedSegwitGains), // percent of the pre-segwit tx size
 | 
				
			||||||
    potentialBech32Gains: potentialBech32Gains / tx.weight,
 | 
					    potentialSegwitGains: potentialSegwitGains / tx.weight,
 | 
				
			||||||
    potentialP2shGains: potentialP2shGains / tx.weight,
 | 
					    potentialP2shSegwitGains: potentialP2shSegwitGains / tx.weight,
 | 
				
			||||||
    potentialTaprootGains: potentialTaprootGains / tx.weight,
 | 
					    potentialTaprootGains: potentialTaprootGains / tx.weight,
 | 
				
			||||||
    realizedTaprootGains: realizedTaprootGains / tx.weight
 | 
					    realizedTaprootGains: realizedTaprootGains / (tx.weight + realizedTaprootGains)
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -188,12 +194,12 @@ export function moveDec(num: number, n: number) {
 | 
				
			|||||||
  return neg + (int || '0') + (frac.length ? '.' + frac : '');
 | 
					  return neg + (int || '0') + (frac.length ? '.' + frac : '');
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function zeros(n) {
 | 
					function zeros(n: number) {
 | 
				
			||||||
  return new Array(n + 1).join('0');
 | 
					  return new Array(n + 1).join('0');
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Formats a number for display. Treats the number as a string to avoid rounding errors.
 | 
					// Formats a number for display. Treats the number as a string to avoid rounding errors.
 | 
				
			||||||
export const formatNumber = (s, precision = null) => {
 | 
					export const formatNumber = (s: number | string, precision: number | null = null) => {
 | 
				
			||||||
  let [ whole, dec ] = s.toString().split('.');
 | 
					  let [ whole, dec ] = s.toString().split('.');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // divide numbers into groups of three separated with a thin space (U+202F, "NARROW NO-BREAK SPACE"),
 | 
					  // divide numbers into groups of three separated with a thin space (U+202F, "NARROW NO-BREAK SPACE"),
 | 
				
			||||||
@ -219,27 +225,27 @@ const witnessSize = (vin: Vin) => vin.witness ? vin.witness.reduce((S, w) => S +
 | 
				
			|||||||
const scriptSigSize = (vin: Vin) => vin.scriptsig ? vin.scriptsig.length / 2 : 0;
 | 
					const scriptSigSize = (vin: Vin) => vin.scriptsig ? vin.scriptsig.length / 2 : 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Power of ten wrapper
 | 
					// Power of ten wrapper
 | 
				
			||||||
export function selectPowerOfTen(val: number) {
 | 
					export function selectPowerOfTen(val: number): { divider: number, unit: string } {
 | 
				
			||||||
  const powerOfTen = {
 | 
					  const powerOfTen = {
 | 
				
			||||||
    exa: Math.pow(10, 18),
 | 
					    exa: Math.pow(10, 18),
 | 
				
			||||||
    peta: Math.pow(10, 15),
 | 
					    peta: Math.pow(10, 15),
 | 
				
			||||||
    terra: Math.pow(10, 12),
 | 
					    tera: Math.pow(10, 12),
 | 
				
			||||||
    giga: Math.pow(10, 9),
 | 
					    giga: Math.pow(10, 9),
 | 
				
			||||||
    mega: Math.pow(10, 6),
 | 
					    mega: Math.pow(10, 6),
 | 
				
			||||||
    kilo: Math.pow(10, 3),
 | 
					    kilo: Math.pow(10, 3),
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let selectedPowerOfTen;
 | 
					  let selectedPowerOfTen: { divider: number, unit: string };
 | 
				
			||||||
  if (val < powerOfTen.kilo) {
 | 
					  if (val < powerOfTen.kilo) {
 | 
				
			||||||
    selectedPowerOfTen = { divider: 1, unit: '' }; // no scaling
 | 
					    selectedPowerOfTen = { divider: 1, unit: '' }; // no scaling
 | 
				
			||||||
  } else if (val < powerOfTen.mega) {
 | 
					  } else if (val < powerOfTen.mega) {
 | 
				
			||||||
    selectedPowerOfTen = { divider: powerOfTen.kilo, unit: 'k' };
 | 
					    selectedPowerOfTen = { divider: powerOfTen.kilo, unit: 'k' };
 | 
				
			||||||
  } else if (val < powerOfTen.giga) {
 | 
					  } else if (val < powerOfTen.giga) {
 | 
				
			||||||
    selectedPowerOfTen = { divider: powerOfTen.mega, unit: 'M' };
 | 
					    selectedPowerOfTen = { divider: powerOfTen.mega, unit: 'M' };
 | 
				
			||||||
  } else if (val < powerOfTen.terra) {
 | 
					  } else if (val < powerOfTen.tera) {
 | 
				
			||||||
    selectedPowerOfTen = { divider: powerOfTen.giga, unit: 'G' };
 | 
					    selectedPowerOfTen = { divider: powerOfTen.giga, unit: 'G' };
 | 
				
			||||||
  } else if (val < powerOfTen.peta) {
 | 
					  } else if (val < powerOfTen.peta) {
 | 
				
			||||||
    selectedPowerOfTen = { divider: powerOfTen.terra, unit: 'T' };
 | 
					    selectedPowerOfTen = { divider: powerOfTen.tera, unit: 'T' };
 | 
				
			||||||
  } else if (val < powerOfTen.exa) {
 | 
					  } else if (val < powerOfTen.exa) {
 | 
				
			||||||
    selectedPowerOfTen = { divider: powerOfTen.peta, unit: 'P' };
 | 
					    selectedPowerOfTen = { divider: powerOfTen.peta, unit: 'P' };
 | 
				
			||||||
  } else {
 | 
					  } else {
 | 
				
			||||||
 | 
				
			|||||||
@ -8,8 +8,8 @@
 | 
				
			|||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </ng-template>
 | 
					  </ng-template>
 | 
				
			||||||
    <ng-container *ngIf="{ val: connectionState$ | async } as connectionState">
 | 
					    <ng-container *ngIf="{ val: connectionState$ | async } as connectionState">
 | 
				
			||||||
      <img *ngIf="!officialMempoolSpace" src="/resources/mempool-logo.png" height="35" width="140" class="logo" [ngStyle]="{'opacity': connectionState.val === 2 ? 1 : 0.5 }" alt="The Mempool Open Source Project logo">
 | 
					      <img *ngIf="!officialMempoolSpace" src="/resources/mempool-logo.png" class="mempool-logo" [ngStyle]="{'opacity': connectionState.val === 2 ? 1 : 0.5 }" alt="The Mempool Open Source Project logo">
 | 
				
			||||||
      <app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" style="width: 140px; height: 35px" width="500" height="126" viewBox="0 0 500 126"></app-svg-images>
 | 
					      <app-svg-images *ngIf="officialMempoolSpace" name="officialMempoolSpace" viewBox="0 0 500 126"></app-svg-images>
 | 
				
			||||||
      <div class="connection-badge">
 | 
					      <div class="connection-badge">
 | 
				
			||||||
        <div class="badge badge-warning" *ngIf="connectionState.val === 0" i18n="master-page.offline">Offline</div>
 | 
					        <div class="badge badge-warning" *ngIf="connectionState.val === 0" i18n="master-page.offline">Offline</div>
 | 
				
			||||||
        <div class="badge badge-warning" *ngIf="connectionState.val === 1" i18n="master-page.reconnecting">Reconnecting...</div>
 | 
					        <div class="badge badge-warning" *ngIf="connectionState.val === 1" i18n="master-page.reconnecting">Reconnecting...</div>
 | 
				
			||||||
 | 
				
			|||||||
@ -78,6 +78,7 @@ li.nav-item {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
.navbar-brand {
 | 
					.navbar-brand {
 | 
				
			||||||
  position: relative;
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  height: 65px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
nav {
 | 
					nav {
 | 
				
			||||||
@ -86,7 +87,7 @@ nav {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
.connection-badge {
 | 
					.connection-badge {
 | 
				
			||||||
  position: absolute;
 | 
					  position: absolute;
 | 
				
			||||||
  top: 13px;
 | 
					  top: 22px;
 | 
				
			||||||
  width: 100%;
 | 
					  width: 100%;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -150,6 +151,7 @@ nav {
 | 
				
			|||||||
  width: 140px;
 | 
					  width: 140px;
 | 
				
			||||||
  margin-right: 15px;
 | 
					  margin-right: 15px;
 | 
				
			||||||
  text-align: center;
 | 
					  text-align: center;
 | 
				
			||||||
 | 
					  align-self: center;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.logo-holder {
 | 
					.logo-holder {
 | 
				
			||||||
@ -161,3 +163,9 @@ nav {
 | 
				
			|||||||
  flex-direction: row;
 | 
					  flex-direction: row;
 | 
				
			||||||
  display: flex;
 | 
					  display: flex;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.mempool-logo, app-svg-images {
 | 
				
			||||||
 | 
					  align-self: center;
 | 
				
			||||||
 | 
					  width: 140px;
 | 
				
			||||||
 | 
					  height: 35px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,12 +1,12 @@
 | 
				
			|||||||
<span *ngIf="segwitGains.realizedBech32Gains && !segwitGains.potentialBech32Gains; else segwitTwo" class="badge badge-success mr-1" i18n-ngbTooltip="ngbTooltip about segwit gains" ngbTooltip="This transaction saved {{ segwitGains.realizedBech32Gains * 100 | number:  '1.0-0' }}% on fees by using native SegWit-Bech32" placement="bottom" i18n="tx-features.tag.segwit|SegWit">SegWit</span>
 | 
					<span *ngIf="segwitGains.realizedSegwitGains && !segwitGains.potentialSegwitGains; else segwitTwo" class="badge badge-success mr-1" i18n-ngbTooltip="ngbTooltip about segwit gains" ngbTooltip="This transaction saved {{ segwitGains.realizedSegwitGains * 100 | number:  '1.0-0' }}% on fees by using native SegWit" placement="bottom" i18n="tx-features.tag.segwit|SegWit">SegWit</span>
 | 
				
			||||||
<ng-template #segwitTwo>
 | 
					<ng-template #segwitTwo>
 | 
				
			||||||
  <span *ngIf="segwitGains.realizedBech32Gains && segwitGains.potentialBech32Gains; else potentialP2shGains" class="badge badge-warning mr-1" i18n-ngbTooltip="ngbTooltip about double segwit gains" ngbTooltip="This transaction saved {{ segwitGains.realizedBech32Gains * 100 | number:  '1.0-0' }}% on fees by using SegWit and could save {{ segwitGains.potentialBech32Gains * 100 | number : '1.0-0' }}% more by fully upgrading to native SegWit-Bech32" placement="bottom" i18n="tx-features.tag.segwit|SegWit">SegWit</span>
 | 
					  <span *ngIf="segwitGains.realizedSegwitGains && segwitGains.potentialSegwitGains; else potentialP2shSegwitGains" class="badge badge-warning mr-1" i18n-ngbTooltip="ngbTooltip about double segwit gains" ngbTooltip="This transaction saved {{ segwitGains.realizedSegwitGains * 100 | number:  '1.0-0' }}% on fees by using SegWit and could save {{ segwitGains.potentialSegwitGains * 100 | number : '1.0-0' }}% more by fully upgrading to native SegWit" placement="bottom" i18n="tx-features.tag.segwit|SegWit">SegWit</span>
 | 
				
			||||||
  <ng-template #potentialP2shGains>
 | 
					  <ng-template #potentialP2shSegwitGains>
 | 
				
			||||||
    <span *ngIf="segwitGains.potentialP2shGains" class="badge badge-danger mr-1" i18n-ngbTooltip="ngbTooltip about missed out gains" ngbTooltip="This transaction could save {{ segwitGains.potentialBech32Gains * 100 | number : '1.0-0' }}% on fees by upgrading to native SegWit-Bech32 or {{ segwitGains.potentialP2shGains * 100 | number:  '1.0-0' }}% by upgrading to SegWit-P2SH" placement="bottom"><del i18n="tx-features.tag.segwit|SegWit">SegWit</del></span>
 | 
					    <span *ngIf="segwitGains.potentialP2shSegwitGains" class="badge badge-danger mr-1" i18n-ngbTooltip="ngbTooltip about missed out gains" ngbTooltip="This transaction could save {{ segwitGains.potentialSegwitGains * 100 | number : '1.0-0' }}% on fees by upgrading to native SegWit or {{ segwitGains.potentialP2shSegwitGains * 100 | number:  '1.0-0' }}% by upgrading to SegWit-P2SH" placement="bottom"><del i18n="tx-features.tag.segwit|SegWit">SegWit</del></span>
 | 
				
			||||||
  </ng-template>
 | 
					  </ng-template>
 | 
				
			||||||
</ng-template>
 | 
					</ng-template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<span *ngIf="segwitGains.realizedTaprootGains && !segwitGains.potentialTaprootGains; else notFullyTaproot" class="badge badge-success mr-1" i18n-ngbTooltip="Tooltip about privacy and fees saved with taproot" ngbTooltip="This transaction uses Taproot and the user's thereby increased privacy and saved at least {{ segwitGains.realizedTaprootGains * 100 | number: '1.0-0' }}% on fees" placement="bottom" i18n="tx-features.tag.taproot|Taproot">Taproot</span>
 | 
					<span *ngIf="segwitGains.realizedTaprootGains && !segwitGains.potentialTaprootGains; else notFullyTaproot" class="badge badge-success mr-1" i18n-ngbTooltip="Tooltip about privacy and fees saved with taproot" ngbTooltip="This transaction uses Taproot and thereby increased the user's privacy and saved at least {{ segwitGains.realizedTaprootGains * 100 | number: '1.0-0' }}% on fees" placement="bottom" i18n="tx-features.tag.taproot|Taproot">Taproot</span>
 | 
				
			||||||
<ng-template #notFullyTaproot>
 | 
					<ng-template #notFullyTaproot>
 | 
				
			||||||
  <span *ngIf="segwitGains.realizedTaprootGains && segwitGains.potentialTaprootGains; else noTaproot" class="badge badge-warning mr-1" i18n-ngbTooltip="Tooltip about privacy and more fees that could be saved with more taproot" ngbTooltip="This transaction uses Taproot and thereby increased the user's privacy and already saved at least {{ segwitGains.realizedTaprootGains * 100 | number: '1.0-0' }}% on fees, but could save an additional {{ segwitGains.potentialTaprootGains * 100 | number: '1.0-0' }}% by fully using Taproot" placement="bottom" i18n="tx-features.tag.taproot|Taproot">Taproot</span>
 | 
					  <span *ngIf="segwitGains.realizedTaprootGains && segwitGains.potentialTaprootGains; else noTaproot" class="badge badge-warning mr-1" i18n-ngbTooltip="Tooltip about privacy and more fees that could be saved with more taproot" ngbTooltip="This transaction uses Taproot and thereby increased the user's privacy and already saved at least {{ segwitGains.realizedTaprootGains * 100 | number: '1.0-0' }}% on fees, but could save an additional {{ segwitGains.potentialTaprootGains * 100 | number: '1.0-0' }}% by fully using Taproot" placement="bottom" i18n="tx-features.tag.taproot|Taproot">Taproot</span>
 | 
				
			||||||
  <ng-template #noTaproot>
 | 
					  <ng-template #noTaproot>
 | 
				
			||||||
 | 
				
			|||||||
@ -12,9 +12,9 @@ export class TxFeaturesComponent implements OnChanges {
 | 
				
			|||||||
  @Input() tx: Transaction;
 | 
					  @Input() tx: Transaction;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  segwitGains = {
 | 
					  segwitGains = {
 | 
				
			||||||
    realizedBech32Gains: 0,
 | 
					    realizedSegwitGains: 0,
 | 
				
			||||||
    potentialBech32Gains: 0,
 | 
					    potentialSegwitGains: 0,
 | 
				
			||||||
    potentialP2shGains: 0,
 | 
					    potentialP2shSegwitGains: 0,
 | 
				
			||||||
    potentialTaprootGains: 0,
 | 
					    potentialTaprootGains: 0,
 | 
				
			||||||
    realizedTaprootGains: 0
 | 
					    realizedTaprootGains: 0
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
				
			|||||||
@ -169,9 +169,6 @@ export class NodeStatisticsChartComponent implements OnInit {
 | 
				
			|||||||
      },
 | 
					      },
 | 
				
			||||||
      yAxis: data.channels.length === 0 ? undefined : [
 | 
					      yAxis: data.channels.length === 0 ? undefined : [
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          min: (value) => {
 | 
					 | 
				
			||||||
            return value.min * 0.9;
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          type: 'value',
 | 
					          type: 'value',
 | 
				
			||||||
          axisLabel: {
 | 
					          axisLabel: {
 | 
				
			||||||
            color: 'rgb(110, 112, 121)',
 | 
					            color: 'rgb(110, 112, 121)',
 | 
				
			||||||
@ -188,9 +185,6 @@ export class NodeStatisticsChartComponent implements OnInit {
 | 
				
			|||||||
          },
 | 
					          },
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          min: (value) => {
 | 
					 | 
				
			||||||
            return value.min * 0.9;
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          type: 'value',
 | 
					          type: 'value',
 | 
				
			||||||
          position: 'right',
 | 
					          position: 'right',
 | 
				
			||||||
          axisLabel: {
 | 
					          axisLabel: {
 | 
				
			||||||
@ -225,15 +219,6 @@ export class NodeStatisticsChartComponent implements OnInit {
 | 
				
			|||||||
              opacity: 1,
 | 
					              opacity: 1,
 | 
				
			||||||
              width: 1,
 | 
					              width: 1,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            data: [{
 | 
					 | 
				
			||||||
              yAxis: 1,
 | 
					 | 
				
			||||||
              label: {
 | 
					 | 
				
			||||||
                position: 'end',
 | 
					 | 
				
			||||||
                show: true,
 | 
					 | 
				
			||||||
                color: '#ffffff',
 | 
					 | 
				
			||||||
                formatter: `1 MB`
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
            }],
 | 
					 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
 | 
				
			|||||||
@ -43,11 +43,23 @@
 | 
				
			|||||||
            </tr>
 | 
					            </tr>
 | 
				
			||||||
            <tr *ngIf="node.country && node.city && node.subdivision">
 | 
					            <tr *ngIf="node.country && node.city && node.subdivision">
 | 
				
			||||||
              <td i18n="location">Location</td>
 | 
					              <td i18n="location">Location</td>
 | 
				
			||||||
              <td>{{ node.city.en }}, {{ node.subdivision.en }}<br>{{ node.country.en }}</td>
 | 
					              <td>
 | 
				
			||||||
 | 
					                <span>{{ node.city.en }}, {{ node.subdivision.en }}</span>
 | 
				
			||||||
 | 
					                <br>
 | 
				
			||||||
 | 
					                <a class="d-flex align-items-center" [routerLink]="['/lightning/nodes/country' | relativeUrl, node.iso_code]">
 | 
				
			||||||
 | 
					                  <span class="link">{{ node.country.en }}</span>
 | 
				
			||||||
 | 
					                   
 | 
				
			||||||
 | 
					                  <span class="flag">{{ node.flag }}</span>
 | 
				
			||||||
 | 
					                </a>
 | 
				
			||||||
 | 
					              </td>
 | 
				
			||||||
            </tr>
 | 
					            </tr>
 | 
				
			||||||
            <tr *ngIf="node.country && !node.city">
 | 
					            <tr *ngIf="node.country && !node.city">
 | 
				
			||||||
              <td i18n="location">Location</td>
 | 
					              <td i18n="location">Location</td>
 | 
				
			||||||
              <td>{{ node.country.en }}</td>
 | 
					              <td>
 | 
				
			||||||
 | 
					                <a [routerLink]="['/lightning/nodes/country' | relativeUrl, node.iso_code]">
 | 
				
			||||||
 | 
					                  {{ node.country.en }} {{ node.flag }}
 | 
				
			||||||
 | 
					                </a>
 | 
				
			||||||
 | 
					              </td>
 | 
				
			||||||
            </tr>
 | 
					            </tr>
 | 
				
			||||||
          </tbody>
 | 
					          </tbody>
 | 
				
			||||||
        </table>
 | 
					        </table>
 | 
				
			||||||
@ -77,7 +89,9 @@
 | 
				
			|||||||
            <tr *ngIf="node.country">
 | 
					            <tr *ngIf="node.country">
 | 
				
			||||||
              <td i18n="isp">ISP</td>
 | 
					              <td i18n="isp">ISP</td>
 | 
				
			||||||
              <td>
 | 
					              <td>
 | 
				
			||||||
                {{ node.as_organization }} [ASN {{node.as_number}}]
 | 
					                <a [routerLink]="['/lightning/nodes/isp' | relativeUrl, node.as_number]">
 | 
				
			||||||
 | 
					                  {{ node.as_organization }} [ASN {{node.as_number}}]
 | 
				
			||||||
 | 
					                </a>                
 | 
				
			||||||
              </td>
 | 
					              </td>
 | 
				
			||||||
            </tr>
 | 
					            </tr>
 | 
				
			||||||
          </tbody>
 | 
					          </tbody>
 | 
				
			||||||
@ -126,13 +140,8 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  <div class="d-flex justify-content-between" *ngIf="!error">
 | 
					  <div class="d-flex justify-content-between" *ngIf="!error">
 | 
				
			||||||
    <h2>Channels ({{ channelsListStatus === 'open' ? node.channel_active_count : node.channel_closed_count }})</h2>
 | 
					    <h2>Channels ({{ channelsListStatus === 'open' ? node.channel_active_count : node.channel_closed_count }})</h2>
 | 
				
			||||||
    <div class="d-flex align-items-center justify-content-end">
 | 
					    <div class="d-flex justify-content-end">
 | 
				
			||||||
      <span style="margin-bottom: 0.5rem">List</span> 
 | 
					      <app-toggle [textLeft]="'List'" [textRight]="'Map'" (toggleStatusChanged)="channelsListModeChange($event)"></app-toggle>
 | 
				
			||||||
      <label class="switch">
 | 
					 | 
				
			||||||
        <input type="checkbox" (change)="channelsListModeChange($event)">
 | 
					 | 
				
			||||||
        <span class="slider round"></span>
 | 
					 | 
				
			||||||
      </label>
 | 
					 | 
				
			||||||
       <span style="margin-bottom: 0.5rem">Map</span>
 | 
					 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -56,67 +56,4 @@ app-fiat {
 | 
				
			|||||||
    display: inline-block;
 | 
					    display: inline-block;
 | 
				
			||||||
    margin-left: 10px;
 | 
					    margin-left: 10px;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
 /* The switch - the box around the slider */
 | 
					 | 
				
			||||||
 .switch {
 | 
					 | 
				
			||||||
  position: relative;
 | 
					 | 
				
			||||||
  display: inline-block;
 | 
					 | 
				
			||||||
  width: 30px;
 | 
					 | 
				
			||||||
  height: 17px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/* Hide default HTML checkbox */
 | 
					 | 
				
			||||||
.switch input {
 | 
					 | 
				
			||||||
  opacity: 0;
 | 
					 | 
				
			||||||
  width: 0;
 | 
					 | 
				
			||||||
  height: 0;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/* The slider */
 | 
					 | 
				
			||||||
.slider {
 | 
					 | 
				
			||||||
  position: absolute;
 | 
					 | 
				
			||||||
  cursor: pointer;
 | 
					 | 
				
			||||||
  top: 0;
 | 
					 | 
				
			||||||
  left: 0;
 | 
					 | 
				
			||||||
  right: 0;
 | 
					 | 
				
			||||||
  bottom: 0;
 | 
					 | 
				
			||||||
  background-color: #ccc;
 | 
					 | 
				
			||||||
  -webkit-transition: .4s;
 | 
					 | 
				
			||||||
  transition: .4s;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.slider:before {
 | 
					 | 
				
			||||||
  position: absolute;
 | 
					 | 
				
			||||||
  content: "";
 | 
					 | 
				
			||||||
  height: 13px;
 | 
					 | 
				
			||||||
  width: 13px;
 | 
					 | 
				
			||||||
  left: 2px;
 | 
					 | 
				
			||||||
  bottom: 2px;
 | 
					 | 
				
			||||||
  background-color: white;
 | 
					 | 
				
			||||||
  -webkit-transition: .4s;
 | 
					 | 
				
			||||||
  transition: .4s;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
input:checked + .slider {
 | 
					 | 
				
			||||||
  background-color: #2196F3;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
input:focus + .slider {
 | 
					 | 
				
			||||||
  box-shadow: 0 0 1px #2196F3;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
input:checked + .slider:before {
 | 
					 | 
				
			||||||
  -webkit-transform: translateX(13px);
 | 
					 | 
				
			||||||
  -ms-transform: translateX(13px);
 | 
					 | 
				
			||||||
  transform: translateX(13px);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/* Rounded sliders */
 | 
					 | 
				
			||||||
.slider.round {
 | 
					 | 
				
			||||||
  border-radius: 17px;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.slider.round:before {
 | 
					 | 
				
			||||||
  border-radius: 50%;
 | 
					 | 
				
			||||||
} 
 | 
					 | 
				
			||||||
@ -3,6 +3,7 @@ import { ActivatedRoute, ParamMap } from '@angular/router';
 | 
				
			|||||||
import { Observable } from 'rxjs';
 | 
					import { Observable } from 'rxjs';
 | 
				
			||||||
import { catchError, map, switchMap } from 'rxjs/operators';
 | 
					import { catchError, map, switchMap } from 'rxjs/operators';
 | 
				
			||||||
import { SeoService } from 'src/app/services/seo.service';
 | 
					import { SeoService } from 'src/app/services/seo.service';
 | 
				
			||||||
 | 
					import { getFlagEmoji } from 'src/app/shared/graphs.utils';
 | 
				
			||||||
import { LightningApiService } from '../lightning-api.service';
 | 
					import { LightningApiService } from '../lightning-api.service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Component({
 | 
					@Component({
 | 
				
			||||||
@ -51,6 +52,7 @@ export class NodeComponent implements OnInit {
 | 
				
			|||||||
            } else if (socket.indexOf('onion') > -1) {
 | 
					            } else if (socket.indexOf('onion') > -1) {
 | 
				
			||||||
              label = 'Tor';
 | 
					              label = 'Tor';
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					            node.flag = getFlagEmoji(node.iso_code);
 | 
				
			||||||
            socketsObject.push({
 | 
					            socketsObject.push({
 | 
				
			||||||
              label: label,
 | 
					              label: label,
 | 
				
			||||||
              socket: node.public_key + '@' + socket,
 | 
					              socket: node.public_key + '@' + socket,
 | 
				
			||||||
@ -73,8 +75,8 @@ export class NodeComponent implements OnInit {
 | 
				
			|||||||
    this.selectedSocketIndex = index;
 | 
					    this.selectedSocketIndex = index;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  channelsListModeChange(e) {
 | 
					  channelsListModeChange(toggle) {
 | 
				
			||||||
    if (e.target.checked === true) {
 | 
					    if (toggle === true) {
 | 
				
			||||||
      this.channelsListMode = 'map';
 | 
					      this.channelsListMode = 'map';
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      this.channelsListMode = 'list';
 | 
					      this.channelsListMode = 'list';
 | 
				
			||||||
 | 
				
			|||||||
@ -59,7 +59,7 @@ export class NodesNetworksChartComponent implements OnInit {
 | 
				
			|||||||
    let firstRun = true;
 | 
					    let firstRun = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (this.widget) {
 | 
					    if (this.widget) {
 | 
				
			||||||
      this.miningWindowPreference = '1y';
 | 
					      this.miningWindowPreference = '3y';
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      this.seoService.setTitle($localize`Lightning nodes per network`);
 | 
					      this.seoService.setTitle($localize`Lightning nodes per network`);
 | 
				
			||||||
      this.miningWindowPreference = this.miningService.getDefaultTimespan('all');
 | 
					      this.miningWindowPreference = this.miningService.getDefaultTimespan('all');
 | 
				
			||||||
 | 
				
			|||||||
@ -7,7 +7,9 @@
 | 
				
			|||||||
        <fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
 | 
					        <fa-icon [icon]="['fas', 'download']" [fixedWidth]="true"></fa-icon>
 | 
				
			||||||
      </button>
 | 
					      </button>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <small style="color: #ffffff66" i18n="lightning.tor-nodes-excluded">(Tor nodes excluded)</small>
 | 
					    <small class="d-block" style="color: #ffffff66; min-height: 25px" i18n="lightning.tor-nodes-excluded">
 | 
				
			||||||
 | 
					      <span *ngIf="!(showTorObservable$ | async)">(Tor nodes excluded)</span>
 | 
				
			||||||
 | 
					    </small>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <div class="container pb-lg-0 bottom-padding">
 | 
					  <div class="container pb-lg-0 bottom-padding">
 | 
				
			||||||
@ -21,6 +23,11 @@
 | 
				
			|||||||
      <div class="spinner-border text-light"></div>
 | 
					      <div class="spinner-border text-light"></div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="d-flex toggle">
 | 
				
			||||||
 | 
					      <app-toggle [textLeft]="'Show Tor'" [textRight]="" (toggleStatusChanged)="onTorToggleStatusChanged($event)"></app-toggle>
 | 
				
			||||||
 | 
					      <app-toggle [textLeft]="'Nodes'" [textRight]="'Capacity'" (toggleStatusChanged)="onGroupToggleStatusChanged($event)"></app-toggle>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <table class="table table-borderless text-center m-auto" style="max-width: 900px">
 | 
					    <table class="table table-borderless text-center m-auto" style="max-width: 900px">
 | 
				
			||||||
      <thead>
 | 
					      <thead>
 | 
				
			||||||
        <tr>
 | 
					        <tr>
 | 
				
			||||||
@ -34,8 +41,9 @@
 | 
				
			|||||||
      <tbody [attr.data-cy]="'pools-table'" *ngIf="(nodesPerAsObservable$ | async) as asList">
 | 
					      <tbody [attr.data-cy]="'pools-table'" *ngIf="(nodesPerAsObservable$ | async) as asList">
 | 
				
			||||||
        <tr *ngFor="let asEntry of asList">
 | 
					        <tr *ngFor="let asEntry of asList">
 | 
				
			||||||
          <td class="rank text-left pl-0">{{ asEntry.rank }}</td>
 | 
					          <td class="rank text-left pl-0">{{ asEntry.rank }}</td>
 | 
				
			||||||
          <td class="name text-left text-truncate" style="max-width: 250px">
 | 
					          <td class="name text-left text-truncate">
 | 
				
			||||||
          <a [routerLink]="[('/lightning/nodes/isp/' + asEntry.ispId) | relativeUrl]">{{ asEntry.name }}</a>
 | 
					            <a *ngIf="asEntry.ispId" [routerLink]="[('/lightning/nodes/isp/' + asEntry.ispId) | relativeUrl]">{{ asEntry.name }}</a>
 | 
				
			||||||
 | 
					            <span *ngIf="!asEntry.ispId">{{ asEntry.name }}</span>
 | 
				
			||||||
          </td>
 | 
					          </td>
 | 
				
			||||||
          <td class="share text-right">{{ asEntry.share }}%</td>
 | 
					          <td class="share text-right">{{ asEntry.share }}%</td>
 | 
				
			||||||
          <td class="nodes text-right">{{ asEntry.count }}</td>
 | 
					          <td class="nodes text-right">{{ asEntry.count }}</td>
 | 
				
			||||||
 | 
				
			|||||||
@ -45,7 +45,7 @@
 | 
				
			|||||||
.name {
 | 
					.name {
 | 
				
			||||||
  width: 25%;
 | 
					  width: 25%;
 | 
				
			||||||
  @media (max-width: 576px) {
 | 
					  @media (max-width: 576px) {
 | 
				
			||||||
    width: 80%;
 | 
					    width: 70%;
 | 
				
			||||||
    max-width: 150px;
 | 
					    max-width: 150px;
 | 
				
			||||||
    padding-left: 0;
 | 
					    padding-left: 0;
 | 
				
			||||||
    padding-right: 0;
 | 
					    padding-right: 0;
 | 
				
			||||||
@ -69,7 +69,17 @@
 | 
				
			|||||||
.capacity {
 | 
					.capacity {
 | 
				
			||||||
  width: 20%;
 | 
					  width: 20%;
 | 
				
			||||||
  @media (max-width: 576px) {
 | 
					  @media (max-width: 576px) {
 | 
				
			||||||
    width: 10%;
 | 
					    width: 20%;
 | 
				
			||||||
    max-width: 100px;
 | 
					    max-width: 100px;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.toggle {
 | 
				
			||||||
 | 
					  justify-content: space-between;
 | 
				
			||||||
 | 
					  padding-top: 15px;
 | 
				
			||||||
 | 
					  @media (min-width: 576px) {
 | 
				
			||||||
 | 
					    padding-bottom: 15px;
 | 
				
			||||||
 | 
					    padding-left: 105px;
 | 
				
			||||||
 | 
					    padding-right: 105px;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@ -1,7 +1,7 @@
 | 
				
			|||||||
import { ChangeDetectionStrategy, Component, OnInit, HostBinding, NgZone } from '@angular/core';
 | 
					import { ChangeDetectionStrategy, Component, OnInit, HostBinding, NgZone } from '@angular/core';
 | 
				
			||||||
import { Router } from '@angular/router';
 | 
					import { Router } from '@angular/router';
 | 
				
			||||||
import { EChartsOption, PieSeriesOption } from 'echarts';
 | 
					import { EChartsOption, PieSeriesOption } from 'echarts';
 | 
				
			||||||
import { map, Observable, share, tap } from 'rxjs';
 | 
					import { combineLatest, map, Observable, share, Subject, switchMap, tap } from 'rxjs';
 | 
				
			||||||
import { chartColors } from 'src/app/app.constants';
 | 
					import { chartColors } from 'src/app/app.constants';
 | 
				
			||||||
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';
 | 
				
			||||||
@ -17,19 +17,20 @@ import { RelativeUrlPipe } from 'src/app/shared/pipes/relative-url/relative-url.
 | 
				
			|||||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
					  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
export class NodesPerISPChartComponent implements OnInit {
 | 
					export class NodesPerISPChartComponent implements OnInit {
 | 
				
			||||||
  miningWindowPreference: string;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  isLoading = true;
 | 
					  isLoading = true;
 | 
				
			||||||
  chartOptions: EChartsOption = {};
 | 
					  chartOptions: EChartsOption = {};
 | 
				
			||||||
  chartInitOptions = {
 | 
					  chartInitOptions = {
 | 
				
			||||||
    renderer: 'svg',
 | 
					    renderer: 'svg',
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
  timespan = '';
 | 
					  timespan = '';
 | 
				
			||||||
  chartInstance: any = undefined;
 | 
					  chartInstance = undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @HostBinding('attr.dir') dir = 'ltr';
 | 
					  @HostBinding('attr.dir') dir = 'ltr';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  nodesPerAsObservable$: Observable<any>;
 | 
					  nodesPerAsObservable$: Observable<any>;
 | 
				
			||||||
 | 
					  showTorObservable$: Observable<boolean>;
 | 
				
			||||||
 | 
					  groupBySubject = new Subject<boolean>();
 | 
				
			||||||
 | 
					  showTorSubject = new Subject<boolean>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(
 | 
					  constructor(
 | 
				
			||||||
    private apiService: ApiService,
 | 
					    private apiService: ApiService,
 | 
				
			||||||
@ -44,23 +45,32 @@ export class NodesPerISPChartComponent implements OnInit {
 | 
				
			|||||||
  ngOnInit(): void {
 | 
					  ngOnInit(): void {
 | 
				
			||||||
    this.seoService.setTitle($localize`Lightning nodes per ISP`);
 | 
					    this.seoService.setTitle($localize`Lightning nodes per ISP`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.nodesPerAsObservable$ = this.apiService.getNodesPerAs()
 | 
					    this.showTorObservable$ = this.showTorSubject.asObservable();
 | 
				
			||||||
 | 
					    this.nodesPerAsObservable$ = combineLatest([this.groupBySubject, this.showTorSubject])
 | 
				
			||||||
      .pipe(
 | 
					      .pipe(
 | 
				
			||||||
        tap(data => {
 | 
					        switchMap((selectedFilters) => {
 | 
				
			||||||
          this.isLoading = false;
 | 
					          return this.apiService.getNodesPerAs(
 | 
				
			||||||
          this.prepareChartOptions(data);
 | 
					            selectedFilters[0] ? 'capacity' : 'node-count',
 | 
				
			||||||
        }),
 | 
					            selectedFilters[1] // Show Tor nodes
 | 
				
			||||||
        map(data => {
 | 
					          )
 | 
				
			||||||
          for (let i = 0; i < data.length; ++i) {
 | 
					            .pipe(
 | 
				
			||||||
            data[i].rank = i + 1;
 | 
					              tap(data => {
 | 
				
			||||||
          }
 | 
					                this.isLoading = false;
 | 
				
			||||||
          return data.slice(0, 100);
 | 
					                this.prepareChartOptions(data);
 | 
				
			||||||
 | 
					              }),
 | 
				
			||||||
 | 
					              map(data => {
 | 
				
			||||||
 | 
					                for (let i = 0; i < data.length; ++i) {
 | 
				
			||||||
 | 
					                  data[i].rank = i + 1;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                return data.slice(0, 100);
 | 
				
			||||||
 | 
					              })
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
        }),
 | 
					        }),
 | 
				
			||||||
        share()
 | 
					        share()
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  generateChartSerieData(as) {
 | 
					  generateChartSerieData(as): PieSeriesOption[] {
 | 
				
			||||||
    const shareThreshold = this.isMobile() ? 2 : 0.5;
 | 
					    const shareThreshold = this.isMobile() ? 2 : 0.5;
 | 
				
			||||||
    const data: object[] = [];
 | 
					    const data: object[] = [];
 | 
				
			||||||
    let totalShareOther = 0;
 | 
					    let totalShareOther = 0;
 | 
				
			||||||
@ -78,6 +88,9 @@ export class NodesPerISPChartComponent implements OnInit {
 | 
				
			|||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      data.push({
 | 
					      data.push({
 | 
				
			||||||
 | 
					        itemStyle: {
 | 
				
			||||||
 | 
					          color: as.ispId === null ? '#7D4698' : undefined,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
        value: as.share,
 | 
					        value: as.share,
 | 
				
			||||||
        name: as.name + (this.isMobile() ? `` : ` (${as.share}%)`),
 | 
					        name: as.name + (this.isMobile() ? `` : ` (${as.share}%)`),
 | 
				
			||||||
        label: {
 | 
					        label: {
 | 
				
			||||||
@ -138,14 +151,14 @@ export class NodesPerISPChartComponent implements OnInit {
 | 
				
			|||||||
    return data;
 | 
					    return data;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  prepareChartOptions(as) {
 | 
					  prepareChartOptions(as): void {
 | 
				
			||||||
    let pieSize = ['20%', '80%']; // Desktop
 | 
					    let pieSize = ['20%', '80%']; // Desktop
 | 
				
			||||||
    if (this.isMobile()) {
 | 
					    if (this.isMobile()) {
 | 
				
			||||||
      pieSize = ['15%', '60%'];
 | 
					      pieSize = ['15%', '60%'];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.chartOptions = {
 | 
					    this.chartOptions = {
 | 
				
			||||||
      color: chartColors,
 | 
					      color: chartColors.slice(3),
 | 
				
			||||||
      tooltip: {
 | 
					      tooltip: {
 | 
				
			||||||
        trigger: 'item',
 | 
					        trigger: 'item',
 | 
				
			||||||
        textStyle: {
 | 
					        textStyle: {
 | 
				
			||||||
@ -191,18 +204,18 @@ export class NodesPerISPChartComponent implements OnInit {
 | 
				
			|||||||
    };
 | 
					    };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  isMobile() {
 | 
					  isMobile(): boolean {
 | 
				
			||||||
    return (window.innerWidth <= 767.98);
 | 
					    return (window.innerWidth <= 767.98);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onChartInit(ec) {
 | 
					  onChartInit(ec): void {
 | 
				
			||||||
    if (this.chartInstance !== undefined) {
 | 
					    if (this.chartInstance !== undefined) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    this.chartInstance = ec;
 | 
					    this.chartInstance = ec;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.chartInstance.on('click', (e) => {
 | 
					    this.chartInstance.on('click', (e) => {
 | 
				
			||||||
      if (e.data.data === 9999) { // "Other"
 | 
					      if (e.data.data === 9999 || e.data.data === null) { // "Other" or Tor
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      this.zone.run(() => {
 | 
					      this.zone.run(() => {
 | 
				
			||||||
@ -212,7 +225,7 @@ export class NodesPerISPChartComponent implements OnInit {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  onSaveChart() {
 | 
					  onSaveChart(): void {
 | 
				
			||||||
    const now = new Date();
 | 
					    const now = new Date();
 | 
				
			||||||
    this.chartOptions.backgroundColor = '#11131f';
 | 
					    this.chartOptions.backgroundColor = '#11131f';
 | 
				
			||||||
    this.chartInstance.setOption(this.chartOptions);
 | 
					    this.chartInstance.setOption(this.chartOptions);
 | 
				
			||||||
@ -224,8 +237,12 @@ export class NodesPerISPChartComponent implements OnInit {
 | 
				
			|||||||
    this.chartInstance.setOption(this.chartOptions);
 | 
					    this.chartInstance.setOption(this.chartOptions);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  isEllipsisActive(e) {
 | 
					  onTorToggleStatusChanged(e): void {
 | 
				
			||||||
    return (e.offsetWidth < e.scrollWidth);
 | 
					    this.showTorSubject.next(e);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onGroupToggleStatusChanged(e): void {
 | 
				
			||||||
 | 
					    this.groupBySubject.next(e);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -58,7 +58,7 @@ export class LightningStatisticsChartComponent implements OnInit {
 | 
				
			|||||||
    let firstRun = true;
 | 
					    let firstRun = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (this.widget) {
 | 
					    if (this.widget) {
 | 
				
			||||||
      this.miningWindowPreference = '1y';
 | 
					      this.miningWindowPreference = '3y';
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      this.seoService.setTitle($localize`Channels and Capacity`);
 | 
					      this.seoService.setTitle($localize`Channels and Capacity`);
 | 
				
			||||||
      this.miningWindowPreference = this.miningService.getDefaultTimespan('all');
 | 
					      this.miningWindowPreference = this.miningService.getDefaultTimespan('all');
 | 
				
			||||||
 | 
				
			|||||||
@ -255,8 +255,9 @@ export class ApiService {
 | 
				
			|||||||
    return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/search', { params });
 | 
					    return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/search', { params });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getNodesPerAs(): Observable<any> {
 | 
					  getNodesPerAs(groupBy: 'capacity' | 'node-count', showTorNodes: boolean): Observable<any> {
 | 
				
			||||||
    return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/isp');
 | 
					    return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/nodes/isp-ranking'
 | 
				
			||||||
 | 
					      + `?groupBy=${groupBy}&showTor=${showTorNodes}`);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getNodeForCountry$(country: string): Observable<any> {
 | 
					  getNodeForCountry$(country: string): Observable<any> {
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					<div class="d-flex align-items-center">
 | 
				
			||||||
 | 
					  <span style="margin-bottom: 0.5rem">{{ textLeft }}</span> 
 | 
				
			||||||
 | 
					  <label class="switch">
 | 
				
			||||||
 | 
					    <input type="checkbox" (change)="onToggleStatusChanged($event)">
 | 
				
			||||||
 | 
					    <span class="slider round"></span>
 | 
				
			||||||
 | 
					  </label>
 | 
				
			||||||
 | 
					   <span style="margin-bottom: 0.5rem">{{ textRight }}</span>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
@ -0,0 +1,62 @@
 | 
				
			|||||||
 | 
					/* The switch - the box around the slider */
 | 
				
			||||||
 | 
					.switch {
 | 
				
			||||||
 | 
					  position: relative;
 | 
				
			||||||
 | 
					  display: inline-block;
 | 
				
			||||||
 | 
					  width: 30px;
 | 
				
			||||||
 | 
					  height: 17px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Hide default HTML checkbox */
 | 
				
			||||||
 | 
					.switch input {
 | 
				
			||||||
 | 
					  opacity: 0;
 | 
				
			||||||
 | 
					  width: 0;
 | 
				
			||||||
 | 
					  height: 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* The slider */
 | 
				
			||||||
 | 
					.slider {
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					  top: 0;
 | 
				
			||||||
 | 
					  left: 0;
 | 
				
			||||||
 | 
					  right: 0;
 | 
				
			||||||
 | 
					  bottom: 0;
 | 
				
			||||||
 | 
					  background-color: #ccc;
 | 
				
			||||||
 | 
					  -webkit-transition: .4s;
 | 
				
			||||||
 | 
					  transition: .4s;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.slider:before {
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  content: "";
 | 
				
			||||||
 | 
					  height: 13px;
 | 
				
			||||||
 | 
					  width: 13px;
 | 
				
			||||||
 | 
					  left: 2px;
 | 
				
			||||||
 | 
					  bottom: 2px;
 | 
				
			||||||
 | 
					  background-color: white;
 | 
				
			||||||
 | 
					  -webkit-transition: .4s;
 | 
				
			||||||
 | 
					  transition: .4s;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					input:checked+.slider {
 | 
				
			||||||
 | 
					  background-color: #2196F3;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					input:focus+.slider {
 | 
				
			||||||
 | 
					  box-shadow: 0 0 1px #2196F3;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					input:checked+.slider:before {
 | 
				
			||||||
 | 
					  -webkit-transform: translateX(13px);
 | 
				
			||||||
 | 
					  -ms-transform: translateX(13px);
 | 
				
			||||||
 | 
					  transform: translateX(13px);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/* Rounded sliders */
 | 
				
			||||||
 | 
					.slider.round {
 | 
				
			||||||
 | 
					  border-radius: 17px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.slider.round:before {
 | 
				
			||||||
 | 
					  border-radius: 50%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					import { Component, Input, Output, ChangeDetectionStrategy, EventEmitter, AfterViewInit } from '@angular/core';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@Component({
 | 
				
			||||||
 | 
					  selector: 'app-toggle',
 | 
				
			||||||
 | 
					  templateUrl: './toggle.component.html',
 | 
				
			||||||
 | 
					  styleUrls: ['./toggle.component.scss'],
 | 
				
			||||||
 | 
					  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
 | 
					export class ToggleComponent implements AfterViewInit {
 | 
				
			||||||
 | 
					  @Output() toggleStatusChanged = new EventEmitter<boolean>();
 | 
				
			||||||
 | 
					  @Input() textLeft: string;
 | 
				
			||||||
 | 
					  @Input() textRight: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ngAfterViewInit(): void {
 | 
				
			||||||
 | 
					    this.toggleStatusChanged.emit(false);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onToggleStatusChanged(e): void {
 | 
				
			||||||
 | 
					    this.toggleStatusChanged.emit(e.target.checked);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -92,6 +92,9 @@ export function detectWebGL() {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function getFlagEmoji(countryCode) {
 | 
					export function getFlagEmoji(countryCode) {
 | 
				
			||||||
 | 
					  if (!countryCode) {
 | 
				
			||||||
 | 
					    return '';
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
  const codePoints = countryCode
 | 
					  const codePoints = countryCode
 | 
				
			||||||
    .toUpperCase()
 | 
					    .toUpperCase()
 | 
				
			||||||
    .split('')
 | 
					    .split('')
 | 
				
			||||||
 | 
				
			|||||||
@ -81,6 +81,7 @@ import { ChangeComponent } from '../components/change/change.component';
 | 
				
			|||||||
import { SatsComponent } from './components/sats/sats.component';
 | 
					import { SatsComponent } from './components/sats/sats.component';
 | 
				
			||||||
import { SearchResultsComponent } from '../components/search-form/search-results/search-results.component';
 | 
					import { SearchResultsComponent } from '../components/search-form/search-results/search-results.component';
 | 
				
			||||||
import { TimestampComponent } from './components/timestamp/timestamp.component';
 | 
					import { TimestampComponent } from './components/timestamp/timestamp.component';
 | 
				
			||||||
 | 
					import { ToggleComponent } from './components/toggle/toggle.component';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@NgModule({
 | 
					@NgModule({
 | 
				
			||||||
  declarations: [
 | 
					  declarations: [
 | 
				
			||||||
@ -156,6 +157,7 @@ import { TimestampComponent } from './components/timestamp/timestamp.component';
 | 
				
			|||||||
    SatsComponent,
 | 
					    SatsComponent,
 | 
				
			||||||
    SearchResultsComponent,
 | 
					    SearchResultsComponent,
 | 
				
			||||||
    TimestampComponent,
 | 
					    TimestampComponent,
 | 
				
			||||||
 | 
					    ToggleComponent,
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
  imports: [
 | 
					  imports: [
 | 
				
			||||||
    CommonModule,
 | 
					    CommonModule,
 | 
				
			||||||
@ -258,6 +260,7 @@ import { TimestampComponent } from './components/timestamp/timestamp.component';
 | 
				
			|||||||
    SatsComponent,
 | 
					    SatsComponent,
 | 
				
			||||||
    SearchResultsComponent,
 | 
					    SearchResultsComponent,
 | 
				
			||||||
    TimestampComponent,
 | 
					    TimestampComponent,
 | 
				
			||||||
 | 
					    ToggleComponent,
 | 
				
			||||||
  ]
 | 
					  ]
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
export class SharedModule {
 | 
					export class SharedModule {
 | 
				
			||||||
 | 
				
			|||||||
@ -18,12 +18,18 @@ whitelist=2401:b140::/32
 | 
				
			|||||||
#uacomment=@wiz
 | 
					#uacomment=@wiz
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[main]
 | 
					[main]
 | 
				
			||||||
bind=0.0.0.0:8333
 | 
					 | 
				
			||||||
bind=[::]:8333
 | 
					 | 
				
			||||||
rpcbind=127.0.0.1:8332
 | 
					rpcbind=127.0.0.1:8332
 | 
				
			||||||
rpcbind=[::1]:8332
 | 
					rpcbind=[::1]:8332
 | 
				
			||||||
zmqpubrawblock=tcp://127.0.0.1:18332
 | 
					bind=0.0.0.0:8333
 | 
				
			||||||
zmqpubrawtx=tcp://127.0.0.1:18333
 | 
					bind=[::]:8333
 | 
				
			||||||
 | 
					zmqpubrawblock=tcp://127.0.0.1:8334
 | 
				
			||||||
 | 
					zmqpubrawtx=tcp://127.0.0.1:8335
 | 
				
			||||||
 | 
					#addnode=[2401:b140:1::92:201]:8333
 | 
				
			||||||
 | 
					#addnode=[2401:b140:1::92:202]:8333
 | 
				
			||||||
 | 
					#addnode=[2401:b140:1::92:203]:8333
 | 
				
			||||||
 | 
					#addnode=[2401:b140:1::92:204]:8333
 | 
				
			||||||
 | 
					#addnode=[2401:b140:1::92:205]:8333
 | 
				
			||||||
 | 
					#addnode=[2401:b140:1::92:206]:8333
 | 
				
			||||||
#addnode=[2401:b140:2::92:201]:8333
 | 
					#addnode=[2401:b140:2::92:201]:8333
 | 
				
			||||||
#addnode=[2401:b140:2::92:202]:8333
 | 
					#addnode=[2401:b140:2::92:202]:8333
 | 
				
			||||||
#addnode=[2401:b140:2::92:203]:8333
 | 
					#addnode=[2401:b140:2::92:203]:8333
 | 
				
			||||||
@ -33,10 +39,18 @@ zmqpubrawtx=tcp://127.0.0.1:18333
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[test]
 | 
					[test]
 | 
				
			||||||
daemon=1
 | 
					daemon=1
 | 
				
			||||||
bind=0.0.0.0:18333
 | 
					 | 
				
			||||||
bind=[::]:18333
 | 
					 | 
				
			||||||
rpcbind=127.0.0.1:18332
 | 
					rpcbind=127.0.0.1:18332
 | 
				
			||||||
rpcbind=[::1]:18332
 | 
					rpcbind=[::1]:18332
 | 
				
			||||||
 | 
					bind=0.0.0.0:18333
 | 
				
			||||||
 | 
					bind=[::]:18333
 | 
				
			||||||
 | 
					zmqpubrawblock=tcp://127.0.0.1:18334
 | 
				
			||||||
 | 
					zmqpubrawtx=tcp://127.0.0.1:18335
 | 
				
			||||||
 | 
					#addnode=[2401:b140:1::92:201]:18333
 | 
				
			||||||
 | 
					#addnode=[2401:b140:1::92:202]:18333
 | 
				
			||||||
 | 
					#addnode=[2401:b140:1::92:203]:18333
 | 
				
			||||||
 | 
					#addnode=[2401:b140:1::92:204]:18333
 | 
				
			||||||
 | 
					#addnode=[2401:b140:1::92:205]:18333
 | 
				
			||||||
 | 
					#addnode=[2401:b140:1::92:206]:18333
 | 
				
			||||||
#addnode=[2401:b140:2::92:201]:18333
 | 
					#addnode=[2401:b140:2::92:201]:18333
 | 
				
			||||||
#addnode=[2401:b140:2::92:202]:18333
 | 
					#addnode=[2401:b140:2::92:202]:18333
 | 
				
			||||||
#addnode=[2401:b140:2::92:203]:18333
 | 
					#addnode=[2401:b140:2::92:203]:18333
 | 
				
			||||||
@ -46,10 +60,18 @@ rpcbind=[::1]:18332
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[signet]
 | 
					[signet]
 | 
				
			||||||
daemon=1
 | 
					daemon=1
 | 
				
			||||||
bind=0.0.0.0:38333
 | 
					 | 
				
			||||||
bind=[::]:38333
 | 
					 | 
				
			||||||
rpcbind=127.0.0.1:38332
 | 
					rpcbind=127.0.0.1:38332
 | 
				
			||||||
rpcbind=[::1]:38332
 | 
					rpcbind=[::1]:38332
 | 
				
			||||||
 | 
					bind=0.0.0.0:38333
 | 
				
			||||||
 | 
					bind=[::]:38333
 | 
				
			||||||
 | 
					zmqpubrawblock=tcp://127.0.0.1:38334
 | 
				
			||||||
 | 
					zmqpubrawtx=tcp://127.0.0.1:38335
 | 
				
			||||||
 | 
					#addnode=[2401:b140:1::92:201]:38333
 | 
				
			||||||
 | 
					#addnode=[2401:b140:1::92:202]:38333
 | 
				
			||||||
 | 
					#addnode=[2401:b140:1::92:203]:38333
 | 
				
			||||||
 | 
					#addnode=[2401:b140:1::92:204]:38333
 | 
				
			||||||
 | 
					#addnode=[2401:b140:1::92:205]:38333
 | 
				
			||||||
 | 
					#addnode=[2401:b140:1::92:206]:38333
 | 
				
			||||||
#addnode=[2401:b140:2::92:201]:38333
 | 
					#addnode=[2401:b140:2::92:201]:38333
 | 
				
			||||||
#addnode=[2401:b140:2::92:202]:38333
 | 
					#addnode=[2401:b140:2::92:202]:38333
 | 
				
			||||||
#addnode=[2401:b140:2::92:203]:38333
 | 
					#addnode=[2401:b140:2::92:203]:38333
 | 
				
			||||||
 | 
				
			|||||||
@ -218,6 +218,21 @@ MYSQL_HOME=/mysql
 | 
				
			|||||||
MYSQL_USER=mysql
 | 
					MYSQL_USER=mysql
 | 
				
			||||||
MYSQL_GROUP=mysql
 | 
					MYSQL_GROUP=mysql
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# mempool mysql user/password
 | 
				
			||||||
 | 
					MEMPOOL_MAINNET_USER='mempool'
 | 
				
			||||||
 | 
					MEMPOOL_TESTNET_USER='mempool_testnet'
 | 
				
			||||||
 | 
					MEMPOOL_SIGNET_USER='mempool_signet'
 | 
				
			||||||
 | 
					MEMPOOL_LIQUID_USER='mempool_liquid'
 | 
				
			||||||
 | 
					MEMPOOL_LIQUIDTESTNET_USER='mempool_liquidtestnet'
 | 
				
			||||||
 | 
					MEMPOOL_BISQ_USER='mempool_bisq'
 | 
				
			||||||
 | 
					# generate random hex string
 | 
				
			||||||
 | 
					MEMPOOL_MAINNET_PASS=$(head -150 /dev/urandom | ${MD5} | awk '{print $1}')
 | 
				
			||||||
 | 
					MEMPOOL_TESTNET_PASS=$(head -150 /dev/urandom | ${MD5} | awk '{print $1}')
 | 
				
			||||||
 | 
					MEMPOOL_SIGNET_PASS=$(head -150 /dev/urandom | ${MD5} | awk '{print $1}')
 | 
				
			||||||
 | 
					MEMPOOL_LIQUID_PASS=$(head -150 /dev/urandom | ${MD5} | awk '{print $1}')
 | 
				
			||||||
 | 
					MEMPOOL_LIQUIDTESTNET_PASS=$(head -150 /dev/urandom | ${MD5} | awk '{print $1}')
 | 
				
			||||||
 | 
					MEMPOOL_BISQ_PASS=$(head -150 /dev/urandom | ${MD5} | awk '{print $1}')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# mempool data folder and user/group
 | 
					# mempool data folder and user/group
 | 
				
			||||||
MEMPOOL_HOME=/mempool
 | 
					MEMPOOL_HOME=/mempool
 | 
				
			||||||
MEMPOOL_USER=mempool
 | 
					MEMPOOL_USER=mempool
 | 
				
			||||||
@ -1513,22 +1528,38 @@ esac
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
mysql << _EOF_
 | 
					mysql << _EOF_
 | 
				
			||||||
create database mempool;
 | 
					create database mempool;
 | 
				
			||||||
grant all on mempool.* to 'mempool'@'localhost' identified by 'mempool';
 | 
					grant all on mempool.* to '${MEMPOOL_MAINNET_USER}'@'localhost' identified by '${MEMPOOL_MAINNET_PASS}';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
create database mempool_testnet;
 | 
					create database mempool_testnet;
 | 
				
			||||||
grant all on mempool_testnet.* to 'mempool_testnet'@'localhost' identified by 'mempool_testnet';
 | 
					grant all on mempool_testnet.* to '${MEMPOOL_TESTNET_USER}'@'localhost' identified by '${MEMPOOL_TESTNET_PASS}';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
create database mempool_signet;
 | 
					create database mempool_signet;
 | 
				
			||||||
grant all on mempool_signet.* to 'mempool_signet'@'localhost' identified by 'mempool_signet';
 | 
					grant all on mempool_signet.* to '${MEMPOOL_SIGNET_USER}'@'localhost' identified by '${MEMPOOL_SIGNET_PASS}';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
create database mempool_liquid;
 | 
					create database mempool_liquid;
 | 
				
			||||||
grant all on mempool_liquid.* to 'mempool_liquid'@'localhost' identified by 'mempool_liquid';
 | 
					grant all on mempool_liquid.* to '${MEMPOOL_LIQUID_USER}'@'localhost' identified by '${MEMPOOL_LIQUID_PASS}';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
create database mempool_liquidtestnet;
 | 
					create database mempool_liquidtestnet;
 | 
				
			||||||
grant all on mempool_liquidtestnet.* to 'mempool_liquidtestnet'@'localhost' identified by 'mempool_liquidtestnet';
 | 
					grant all on mempool_liquidtestnet.* to '${MEMPOOL_LIQUIDTESTNET_USER}'@'localhost' identified by '${MEMPOOL_LIQUIDTESTNET_PASS}';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
create database mempool_bisq;
 | 
					create database mempool_bisq;
 | 
				
			||||||
grant all on mempool_bisq.* to 'mempool_bisq'@'localhost' identified by 'mempool_bisq';
 | 
					grant all on mempool_bisq.* to '${MEMPOOL_BISQ_USER}'@'localhost' identified by '${MEMPOOL_BISQ_PASS}';
 | 
				
			||||||
 | 
					_EOF_
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					echo "[*] save MySQL credentials"
 | 
				
			||||||
 | 
					cat > ${MEMPOOL_HOME}/mysql_credentials << _EOF_
 | 
				
			||||||
 | 
					declare -x MEMPOOL_MAINNET_USER="${MEMPOOL_MAINNET_USER}"
 | 
				
			||||||
 | 
					declare -x MEMPOOL_MAINNET_PASS="${MEMPOOL_MAINNET_PASS}"
 | 
				
			||||||
 | 
					declare -x MEMPOOL_TESTNET_USER="${MEMPOOL_TESTNET_USER}"
 | 
				
			||||||
 | 
					declare -x MEMPOOL_TESTNET_PASS="${MEMPOOL_TESTNET_PASS}"
 | 
				
			||||||
 | 
					declare -x MEMPOOL_SIGNET_USER="${MEMPOOL_SIGNET_USER}"
 | 
				
			||||||
 | 
					declare -x MEMPOOL_SIGNET_PASS="${MEMPOOL_SIGNET_PASS}"
 | 
				
			||||||
 | 
					declare -x MEMPOOL_LIQUID_USER="${MEMPOOL_LIQUID_USER}"
 | 
				
			||||||
 | 
					declare -x MEMPOOL_LIQUID_PASS="${MEMPOOL_LIQUID_PASS}"
 | 
				
			||||||
 | 
					declare -x MEMPOOL_LIQUIDTESTNET_USER="${MEMPOOL_LIQUIDTESTNET_USER}"
 | 
				
			||||||
 | 
					declare -x MEMPOOL_LIQUIDTESTNET_PASS="${MEMPOOL_LIQUIDTESTNET_PASS}"
 | 
				
			||||||
 | 
					declare -x MEMPOOL_BISQ_USER="${MEMPOOL_BISQ_USER}"
 | 
				
			||||||
 | 
					declare -x MEMPOOL_BISQ_PASS="${MEMPOOL_BISQ_PASS}"
 | 
				
			||||||
_EOF_
 | 
					_EOF_
 | 
				
			||||||
 | 
					
 | 
				
			||||||
##### nginx
 | 
					##### nginx
 | 
				
			||||||
 | 
				
			|||||||
@ -11,6 +11,9 @@ BITCOIN_RPC_PASS=$(grep '^rpcpassword' /bitcoin/bitcoin.conf | cut -d '=' -f2)
 | 
				
			|||||||
ELEMENTS_RPC_USER=$(grep '^rpcuser' /elements/elements.conf | cut -d '=' -f2)
 | 
					ELEMENTS_RPC_USER=$(grep '^rpcuser' /elements/elements.conf | cut -d '=' -f2)
 | 
				
			||||||
ELEMENTS_RPC_PASS=$(grep '^rpcpassword' /elements/elements.conf | cut -d '=' -f2)
 | 
					ELEMENTS_RPC_PASS=$(grep '^rpcpassword' /elements/elements.conf | cut -d '=' -f2)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# get mysql credentials
 | 
				
			||||||
 | 
					. /mempool/mysql_credentials
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if [ -f "${LOCKFILE}" ];then
 | 
					if [ -f "${LOCKFILE}" ];then
 | 
				
			||||||
    echo "upgrade already running? check lockfile ${LOCKFILE}"
 | 
					    echo "upgrade already running? check lockfile ${LOCKFILE}"
 | 
				
			||||||
    exit 1
 | 
					    exit 1
 | 
				
			||||||
@ -73,6 +76,18 @@ build_backend()
 | 
				
			|||||||
	-e "s!__BITCOIN_RPC_PASS__!${BITCOIN_RPC_PASS}!" \
 | 
						-e "s!__BITCOIN_RPC_PASS__!${BITCOIN_RPC_PASS}!" \
 | 
				
			||||||
	-e "s!__ELEMENTS_RPC_USER__!${ELEMENTS_RPC_USER}!" \
 | 
						-e "s!__ELEMENTS_RPC_USER__!${ELEMENTS_RPC_USER}!" \
 | 
				
			||||||
	-e "s!__ELEMENTS_RPC_PASS__!${ELEMENTS_RPC_PASS}!" \
 | 
						-e "s!__ELEMENTS_RPC_PASS__!${ELEMENTS_RPC_PASS}!" \
 | 
				
			||||||
 | 
					        -e "s!__MEMPOOL_MAINNET_USER__!${MEMPOOL_MAINNET_USER}!" \
 | 
				
			||||||
 | 
					        -e "s!__MEMPOOL_MAINNET_PASS__!${MEMPOOL_MAINNET_PASS}!" \
 | 
				
			||||||
 | 
					        -e "s!__MEMPOOL_TESTNET_USER__!${MEMPOOL_TESTNET_USER}!" \
 | 
				
			||||||
 | 
					        -e "s!__MEMPOOL_TESTNET_PASS__!${MEMPOOL_TESTNET_PASS}!" \
 | 
				
			||||||
 | 
					        -e "s!__MEMPOOL_SIGNET_USER__!${MEMPOOL_SIGNET_USER}!" \
 | 
				
			||||||
 | 
					        -e "s!__MEMPOOL_SIGNET_PASS__!${MEMPOOL_SIGNET_PASS}!" \
 | 
				
			||||||
 | 
					        -e "s!__MEMPOOL_LIQUID_USER__!${MEMPOOL_LIQUID_USER}!" \
 | 
				
			||||||
 | 
					        -e "s!__MEMPOOL_LIQUID_PASS__!${MEMPOOL_LIQUID_PASS}!" \
 | 
				
			||||||
 | 
					        -e "s!__MEMPOOL_LIQUIDTESTNET_USER__!${LIQUIDTESTNET_USER}!" \
 | 
				
			||||||
 | 
					        -e "s!__MEMPOOL_LIQUIDTESTNET_PASS__!${MEMPOOL_LIQUIDTESTNET_PASS}!" \
 | 
				
			||||||
 | 
					        -e "s!__MEMPOOL_BISQ_USER__!${MEMPOOL_BISQ_USER}!" \
 | 
				
			||||||
 | 
					        -e "s!__MEMPOOL_BISQ_PASS__!${MEMPOOL_BISQ_PASS}!" \
 | 
				
			||||||
	"mempool-config.json"
 | 
						"mempool-config.json"
 | 
				
			||||||
    fi
 | 
					    fi
 | 
				
			||||||
    npm install --omit=dev --omit=optional || exit 1
 | 
					    npm install --omit=dev --omit=optional || exit 1
 | 
				
			||||||
 | 
				
			|||||||
@ -21,8 +21,8 @@
 | 
				
			|||||||
    "ENABLED": false,
 | 
					    "ENABLED": false,
 | 
				
			||||||
    "HOST": "127.0.0.1",
 | 
					    "HOST": "127.0.0.1",
 | 
				
			||||||
    "PORT": 3306,
 | 
					    "PORT": 3306,
 | 
				
			||||||
    "USERNAME": "mempool_bisq",
 | 
					    "USERNAME": "__MEMPOOL_BISQ_USER__",
 | 
				
			||||||
    "PASSWORD": "mempool_bisq",
 | 
					    "PASSWORD": "__MEMPOOL_BISQ_PASS__",
 | 
				
			||||||
    "DATABASE": "mempool_bisq"
 | 
					    "DATABASE": "mempool_bisq"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "STATISTICS": {
 | 
					  "STATISTICS": {
 | 
				
			||||||
 | 
				
			|||||||
@ -28,8 +28,8 @@
 | 
				
			|||||||
    "ENABLED": true,
 | 
					    "ENABLED": true,
 | 
				
			||||||
    "HOST": "127.0.0.1",
 | 
					    "HOST": "127.0.0.1",
 | 
				
			||||||
    "PORT": 3306,
 | 
					    "PORT": 3306,
 | 
				
			||||||
    "USERNAME": "mempool_liquid",
 | 
					    "USERNAME": "__MEMPOOL_LIQUID_USER__",
 | 
				
			||||||
    "PASSWORD": "mempool_liquid",
 | 
					    "PASSWORD": "__MEMPOOL_LIQUID_PASS__",
 | 
				
			||||||
    "DATABASE": "mempool_liquid"
 | 
					    "DATABASE": "mempool_liquid"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "STATISTICS": {
 | 
					  "STATISTICS": {
 | 
				
			||||||
 | 
				
			|||||||
@ -28,8 +28,8 @@
 | 
				
			|||||||
    "ENABLED": true,
 | 
					    "ENABLED": true,
 | 
				
			||||||
    "HOST": "127.0.0.1",
 | 
					    "HOST": "127.0.0.1",
 | 
				
			||||||
    "PORT": 3306,
 | 
					    "PORT": 3306,
 | 
				
			||||||
    "USERNAME": "mempool_liquidtestnet",
 | 
					    "USERNAME": "__MEMPOOL_LIQUIDTESTNET_USER__",
 | 
				
			||||||
    "PASSWORD": "mempool_liquidtestnet",
 | 
					    "PASSWORD": "__MEMPOOL_LIQUIDTESTNET_PASS__",
 | 
				
			||||||
    "DATABASE": "mempool_liquidtestnet"
 | 
					    "DATABASE": "mempool_liquidtestnet"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "STATISTICS": {
 | 
					  "STATISTICS": {
 | 
				
			||||||
 | 
				
			|||||||
@ -32,8 +32,8 @@
 | 
				
			|||||||
    "ENABLED": true,
 | 
					    "ENABLED": true,
 | 
				
			||||||
    "HOST": "127.0.0.1",
 | 
					    "HOST": "127.0.0.1",
 | 
				
			||||||
    "PORT": 3306,
 | 
					    "PORT": 3306,
 | 
				
			||||||
    "USERNAME": "mempool",
 | 
					    "USERNAME": "__MEMPOOL_MAINNET_USER__",
 | 
				
			||||||
    "PASSWORD": "mempool",
 | 
					    "PASSWORD": "__MEMPOOL_MAINNET_PASS__",
 | 
				
			||||||
    "DATABASE": "mempool"
 | 
					    "DATABASE": "mempool"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "STATISTICS": {
 | 
					  "STATISTICS": {
 | 
				
			||||||
 | 
				
			|||||||
@ -24,8 +24,8 @@
 | 
				
			|||||||
    "ENABLED": true,
 | 
					    "ENABLED": true,
 | 
				
			||||||
    "HOST": "127.0.0.1",
 | 
					    "HOST": "127.0.0.1",
 | 
				
			||||||
    "PORT": 3306,
 | 
					    "PORT": 3306,
 | 
				
			||||||
    "USERNAME": "mempool_signet",
 | 
					    "USERNAME": "__MEMPOOL_SIGNET_USER__",
 | 
				
			||||||
    "PASSWORD": "mempool_signet",
 | 
					    "PASSWORD": "__MEMPOOL_SIGNET_PASS__",
 | 
				
			||||||
    "DATABASE": "mempool_signet"
 | 
					    "DATABASE": "mempool_signet"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "STATISTICS": {
 | 
					  "STATISTICS": {
 | 
				
			||||||
 | 
				
			|||||||
@ -24,8 +24,8 @@
 | 
				
			|||||||
    "ENABLED": true,
 | 
					    "ENABLED": true,
 | 
				
			||||||
    "HOST": "127.0.0.1",
 | 
					    "HOST": "127.0.0.1",
 | 
				
			||||||
    "PORT": 3306,
 | 
					    "PORT": 3306,
 | 
				
			||||||
    "USERNAME": "mempool_testnet",
 | 
					    "USERNAME": "__MEMPOOL_TESTNET_USER__",
 | 
				
			||||||
    "PASSWORD": "mempool_testnet",
 | 
					    "PASSWORD": "__MEMPOOL_TESTNET_PASS__",
 | 
				
			||||||
    "DATABASE": "mempool_testnet"
 | 
					    "DATABASE": "mempool_testnet"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "STATISTICS": {
 | 
					  "STATISTICS": {
 | 
				
			||||||
 | 
				
			|||||||
@ -9,6 +9,22 @@ Some additional server configuration is required to properly route requests (see
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
## Setup
 | 
					## Setup
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 0. Install deps
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					For Linux, in addition to NodeJS/npm you'll need at least:
 | 
				
			||||||
 | 
					* nginx
 | 
				
			||||||
 | 
					* cups
 | 
				
			||||||
 | 
					* chromium-bsu
 | 
				
			||||||
 | 
					* libatk1.0
 | 
				
			||||||
 | 
					* libatk-bridge2.0
 | 
				
			||||||
 | 
					* libxkbcommon-dev
 | 
				
			||||||
 | 
					* libxcomposite-dev
 | 
				
			||||||
 | 
					* libxdamage-dev
 | 
				
			||||||
 | 
					* libxrandr-dev
 | 
				
			||||||
 | 
					* libgbm-dev
 | 
				
			||||||
 | 
					* libpango1.0-dev
 | 
				
			||||||
 | 
					* libasound-dev
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### 1. Clone Mempool Repository
 | 
					### 1. Clone Mempool Repository
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Get the latest Mempool code:
 | 
					Get the latest Mempool code:
 | 
				
			||||||
 | 
				
			|||||||
@ -4,7 +4,7 @@
 | 
				
			|||||||
  "description": "Renderer for mempool open graph link preview images",
 | 
					  "description": "Renderer for mempool open graph link preview images",
 | 
				
			||||||
  "repository": {
 | 
					  "repository": {
 | 
				
			||||||
    "type": "git",
 | 
					    "type": "git",
 | 
				
			||||||
    "url": "git+https://github.com/mononaut/mempool-unfurl"
 | 
					    "url": "git+https://github.com/mempool/mempool"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "main": "index.ts",
 | 
					  "main": "index.ts",
 | 
				
			||||||
  "scripts": {
 | 
					  "scripts": {
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user