Merge Lightning backend into Mempool backend
This commit is contained in:
		
							parent
							
								
									b5995e0331
								
							
						
					
					
						commit
						d1cfdd5931
					
				@ -66,6 +66,15 @@
 | 
			
		||||
    "ENABLED": false,
 | 
			
		||||
    "DATA_PATH": "/bisq/statsnode-data/btc_mainnet/db"
 | 
			
		||||
  },
 | 
			
		||||
  "LIGHTNING": {
 | 
			
		||||
    "ENABLED": false,
 | 
			
		||||
    "BACKEND": "lnd"
 | 
			
		||||
  },
 | 
			
		||||
  "LND_NODE_AUTH": {
 | 
			
		||||
    "TLS_CERT_PATH": "tls.cert",
 | 
			
		||||
    "MACAROON_PATH": "admin.macaroon",
 | 
			
		||||
    "SOCKET": "localhost:10009"
 | 
			
		||||
  },
 | 
			
		||||
  "SOCKS5PROXY": {
 | 
			
		||||
    "ENABLED": false,
 | 
			
		||||
    "USE_ONION": true,
 | 
			
		||||
 | 
			
		||||
@ -37,6 +37,7 @@
 | 
			
		||||
    "bolt07": "^1.8.1",
 | 
			
		||||
    "crypto-js": "^4.0.0",
 | 
			
		||||
    "express": "^4.18.0",
 | 
			
		||||
    "ln-service": "^53.17.4",
 | 
			
		||||
    "mysql2": "2.3.3",
 | 
			
		||||
    "node-worker-threads-pool": "^1.5.1",
 | 
			
		||||
    "socks-proxy-agent": "~7.0.0",
 | 
			
		||||
 | 
			
		||||
@ -13,6 +13,7 @@ export interface AbstractBitcoinApi {
 | 
			
		||||
  $getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
 | 
			
		||||
  $getAddressPrefix(prefix: string): string[];
 | 
			
		||||
  $sendRawTransaction(rawTransaction: string): Promise<string>;
 | 
			
		||||
  $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend>;
 | 
			
		||||
  $getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>;
 | 
			
		||||
  $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -130,6 +130,16 @@ class BitcoinApi implements AbstractBitcoinApi {
 | 
			
		||||
    return this.bitcoindClient.sendRawTransaction(rawTransaction);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
 | 
			
		||||
    const txOut = await this.bitcoindClient.getTxOut(txId, vout, false);
 | 
			
		||||
    return {
 | 
			
		||||
      spent: txOut === null,
 | 
			
		||||
      status: {
 | 
			
		||||
        confirmed: true,
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async $getOutspends(txId: string): Promise<IEsploraApi.Outspend[]> {
 | 
			
		||||
    const outSpends: IEsploraApi.Outspend[] = [];
 | 
			
		||||
    const tx = await this.$getRawTransaction(txId, true, false);
 | 
			
		||||
@ -195,7 +205,9 @@ class BitcoinApi implements AbstractBitcoinApi {
 | 
			
		||||
        sequence: vin.sequence,
 | 
			
		||||
        txid: vin.txid || '',
 | 
			
		||||
        vout: vin.vout || 0,
 | 
			
		||||
        witness: vin.txinwitness,
 | 
			
		||||
        witness: vin.txinwitness || [],
 | 
			
		||||
        inner_redeemscript_asm: '',
 | 
			
		||||
        inner_witnessscript_asm: '',
 | 
			
		||||
      };
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -25,10 +25,10 @@ export namespace IEsploraApi {
 | 
			
		||||
    is_coinbase: boolean;
 | 
			
		||||
    scriptsig: string;
 | 
			
		||||
    scriptsig_asm: string;
 | 
			
		||||
    inner_redeemscript_asm?: string;
 | 
			
		||||
    inner_witnessscript_asm?: string;
 | 
			
		||||
    inner_redeemscript_asm: string;
 | 
			
		||||
    inner_witnessscript_asm: string;
 | 
			
		||||
    sequence: any;
 | 
			
		||||
    witness?: string[];
 | 
			
		||||
    witness: string[];
 | 
			
		||||
    prevout: Vout | null;
 | 
			
		||||
    // Elements
 | 
			
		||||
    is_pegin?: boolean;
 | 
			
		||||
 | 
			
		||||
@ -66,6 +66,11 @@ class ElectrsApi implements AbstractBitcoinApi {
 | 
			
		||||
    throw new Error('Method not implemented.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
 | 
			
		||||
    return axios.get<IEsploraApi.Outspend>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspend/' + vout, this.axiosConfig)
 | 
			
		||||
      .then((response) => response.data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getOutspends(txId: string): Promise<IEsploraApi.Outspend[]> {
 | 
			
		||||
    return axios.get<IEsploraApi.Outspend[]>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspends', this.axiosConfig)
 | 
			
		||||
      .then((response) => response.data);
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,7 @@ import logger from '../logger';
 | 
			
		||||
import { Common } from './common';
 | 
			
		||||
 | 
			
		||||
class DatabaseMigration {
 | 
			
		||||
  private static currentVersion = 24;
 | 
			
		||||
  private static currentVersion = 25;
 | 
			
		||||
  private queryTimeout = 120000;
 | 
			
		||||
  private statisticsAddedIndexed = false;
 | 
			
		||||
  private uniqueLogs: string[] = [];
 | 
			
		||||
@ -248,6 +248,15 @@ class DatabaseMigration {
 | 
			
		||||
        await this.$executeQuery('DROP TABLE IF EXISTS `blocks_audits`');
 | 
			
		||||
        await this.$executeQuery(this.getCreateBlocksAuditsTableQuery(), await this.$checkIfTableExists('blocks_audits'));
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (databaseSchemaVersion < 25 && isBitcoin === true) {
 | 
			
		||||
        await this.$executeQuery(`INSERT INTO state VALUES('last_node_stats', 0, '1970-01-01');`);
 | 
			
		||||
        await this.$executeQuery(this.getCreateLightningStatisticsQuery(), await this.$checkIfTableExists('lightning_stats'));
 | 
			
		||||
        await this.$executeQuery(this.getCreateNodesQuery(), await this.$checkIfTableExists('nodes'));
 | 
			
		||||
        await this.$executeQuery(this.getCreateChannelsQuery(), await this.$checkIfTableExists('channels'));
 | 
			
		||||
        await this.$executeQuery(this.getCreateNodesStatsQuery(), await this.$checkIfTableExists('node_stats'));
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
@ -569,6 +578,82 @@ class DatabaseMigration {
 | 
			
		||||
      adjustment float NOT NULL,
 | 
			
		||||
      PRIMARY KEY (height),
 | 
			
		||||
      INDEX (time)
 | 
			
		||||
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private getCreateLightningStatisticsQuery(): string {
 | 
			
		||||
    return `CREATE TABLE IF NOT EXISTS lightning_stats (
 | 
			
		||||
      id int(11) NOT NULL AUTO_INCREMENT,
 | 
			
		||||
      added datetime NOT NULL,
 | 
			
		||||
      channel_count int(11) NOT NULL,
 | 
			
		||||
      node_count int(11) NOT NULL,
 | 
			
		||||
      total_capacity double unsigned NOT NULL,
 | 
			
		||||
      PRIMARY KEY (id)
 | 
			
		||||
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private getCreateNodesQuery(): string {
 | 
			
		||||
    return `CREATE TABLE IF NOT EXISTS nodes (
 | 
			
		||||
      public_key varchar(66) NOT NULL,
 | 
			
		||||
      first_seen datetime NOT NULL,
 | 
			
		||||
      updated_at datetime NOT NULL,
 | 
			
		||||
      alias varchar(200) CHARACTER SET utf8mb4 NOT NULL,
 | 
			
		||||
      color varchar(200) NOT NULL,
 | 
			
		||||
      sockets text DEFAULT NULL,
 | 
			
		||||
      PRIMARY KEY (public_key),
 | 
			
		||||
      KEY alias (alias(10))
 | 
			
		||||
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private getCreateChannelsQuery(): string {
 | 
			
		||||
    return `CREATE TABLE IF NOT EXISTS channels (
 | 
			
		||||
      id bigint(11) unsigned NOT NULL,
 | 
			
		||||
      short_id varchar(15) NOT NULL DEFAULT '',
 | 
			
		||||
      capacity bigint(20) unsigned NOT NULL,
 | 
			
		||||
      transaction_id varchar(64) NOT NULL,
 | 
			
		||||
      transaction_vout int(11) NOT NULL,
 | 
			
		||||
      updated_at datetime DEFAULT NULL,
 | 
			
		||||
      created datetime DEFAULT NULL,
 | 
			
		||||
      status int(11) NOT NULL DEFAULT 0,
 | 
			
		||||
      closing_transaction_id varchar(64) DEFAULT NULL,
 | 
			
		||||
      closing_date datetime DEFAULT NULL,
 | 
			
		||||
      closing_reason int(11) DEFAULT NULL,
 | 
			
		||||
      node1_public_key varchar(66) NOT NULL,
 | 
			
		||||
      node1_base_fee_mtokens bigint(20) unsigned DEFAULT NULL,
 | 
			
		||||
      node1_cltv_delta int(11) DEFAULT NULL,
 | 
			
		||||
      node1_fee_rate bigint(11) DEFAULT NULL,
 | 
			
		||||
      node1_is_disabled tinyint(1) DEFAULT NULL,
 | 
			
		||||
      node1_max_htlc_mtokens bigint(20) unsigned DEFAULT NULL,
 | 
			
		||||
      node1_min_htlc_mtokens bigint(20) DEFAULT NULL,
 | 
			
		||||
      node1_updated_at datetime DEFAULT NULL,
 | 
			
		||||
      node2_public_key varchar(66) NOT NULL,
 | 
			
		||||
      node2_base_fee_mtokens bigint(20) unsigned DEFAULT NULL,
 | 
			
		||||
      node2_cltv_delta int(11) DEFAULT NULL,
 | 
			
		||||
      node2_fee_rate bigint(11) DEFAULT NULL,
 | 
			
		||||
      node2_is_disabled tinyint(1) DEFAULT NULL,
 | 
			
		||||
      node2_max_htlc_mtokens bigint(20) unsigned DEFAULT NULL,
 | 
			
		||||
      node2_min_htlc_mtokens bigint(20) unsigned DEFAULT NULL,
 | 
			
		||||
      node2_updated_at datetime DEFAULT NULL,
 | 
			
		||||
      PRIMARY KEY (id),
 | 
			
		||||
      KEY node1_public_key (node1_public_key),
 | 
			
		||||
      KEY node2_public_key (node2_public_key),
 | 
			
		||||
      KEY status (status),
 | 
			
		||||
      KEY short_id (short_id),
 | 
			
		||||
      KEY transaction_id (transaction_id),
 | 
			
		||||
      KEY closing_transaction_id (closing_transaction_id)
 | 
			
		||||
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private getCreateNodesStatsQuery(): string {
 | 
			
		||||
    return `CREATE TABLE IF NOT EXISTS node_stats (
 | 
			
		||||
      id int(11) unsigned NOT NULL AUTO_INCREMENT,
 | 
			
		||||
      public_key varchar(66) NOT NULL DEFAULT '',
 | 
			
		||||
      added date NOT NULL,
 | 
			
		||||
      capacity bigint(20) unsigned NOT NULL DEFAULT 0,
 | 
			
		||||
      channels int(11) unsigned NOT NULL DEFAULT 0,
 | 
			
		||||
      PRIMARY KEY (id),
 | 
			
		||||
      UNIQUE KEY added (added,public_key),
 | 
			
		||||
      KEY public_key (public_key)
 | 
			
		||||
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -38,7 +38,7 @@ class ChannelsApi {
 | 
			
		||||
 | 
			
		||||
  public async $getClosedChannelsWithoutReason(): Promise<any[]> {
 | 
			
		||||
    try {
 | 
			
		||||
      const query = `SELECT * FROM channels WHERE status = 2 AND closing_reason IS NULL`;
 | 
			
		||||
      const query = `SELECT * FROM channels WHERE status = 2 AND closing_reason IS NULL AND closing_transaction_id != ''`;
 | 
			
		||||
      const [rows]: any = await DB.query(query);
 | 
			
		||||
      return rows;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
@ -1,16 +1,16 @@
 | 
			
		||||
import config from '../../config';
 | 
			
		||||
import { Express, Request, Response } from 'express';
 | 
			
		||||
import { Application, Request, Response } from 'express';
 | 
			
		||||
import channelsApi from './channels.api';
 | 
			
		||||
 | 
			
		||||
class ChannelsRoutes {
 | 
			
		||||
  constructor() { }
 | 
			
		||||
 | 
			
		||||
  public initRoutes(app: Express) {
 | 
			
		||||
  public initRoutes(app: Application) {
 | 
			
		||||
    app
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'channels/txids', this.$getChannelsByTransactionIds)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'channels/search/:search', this.$searchChannelsById)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'channels/:short_id', this.$getChannel)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'channels', this.$getChannelsForNode)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/channels/txids', this.$getChannelsByTransactionIds)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/channels/search/:search', this.$searchChannelsById)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/channels/:short_id', this.$getChannel)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/channels', this.$getChannelsForNode)
 | 
			
		||||
    ;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -1,16 +1,17 @@
 | 
			
		||||
import config from '../../config';
 | 
			
		||||
import { Express, Request, Response } from 'express';
 | 
			
		||||
import { Application, Request, Response } from 'express';
 | 
			
		||||
import nodesApi from './nodes.api';
 | 
			
		||||
import channelsApi from './channels.api';
 | 
			
		||||
import statisticsApi from './statistics.api';
 | 
			
		||||
class GeneralRoutes {
 | 
			
		||||
class GeneralLightningRoutes {
 | 
			
		||||
  constructor() { }
 | 
			
		||||
 | 
			
		||||
  public initRoutes(app: Express) {
 | 
			
		||||
  public initRoutes(app: Application) {
 | 
			
		||||
    app
 | 
			
		||||
    .get(config.MEMPOOL.API_URL_PREFIX + 'search', this.$searchNodesAndChannels)
 | 
			
		||||
    .get(config.MEMPOOL.API_URL_PREFIX + 'statistics', this.$getStatistics)
 | 
			
		||||
  ;
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/search', this.$searchNodesAndChannels)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/statistics/latest', this.$getGeneralStats)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/statistics', this.$getStatistics)
 | 
			
		||||
    ;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $searchNodesAndChannels(req: Request, res: Response) {
 | 
			
		||||
@ -38,6 +39,15 @@ class GeneralRoutes {
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $getGeneralStats(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const statistics = await statisticsApi.$getLatestStatistics();
 | 
			
		||||
      res.json(statistics);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new GeneralRoutes();
 | 
			
		||||
export default new GeneralLightningRoutes();
 | 
			
		||||
@ -46,20 +46,6 @@ class NodesApi {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getLatestStatistics(): Promise<any> {
 | 
			
		||||
    try {
 | 
			
		||||
      const [rows]: any = await DB.query(`SELECT * FROM statistics ORDER BY id DESC LIMIT 1`);
 | 
			
		||||
      const [rows2]: any = await DB.query(`SELECT * FROM statistics ORDER BY id DESC LIMIT 1 OFFSET 72`);
 | 
			
		||||
      return {
 | 
			
		||||
        latest: rows[0],
 | 
			
		||||
        previous: rows2[0],
 | 
			
		||||
      };
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err('$getLatestStatistics error: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $searchNodeByPublicKeyOrAlias(search: string) {
 | 
			
		||||
    try {
 | 
			
		||||
      const searchStripped = search.replace('%', '') + '%';
 | 
			
		||||
@ -1,17 +1,16 @@
 | 
			
		||||
import config from '../../config';
 | 
			
		||||
import { Express, Request, Response } from 'express';
 | 
			
		||||
import { Application, Request, Response } from 'express';
 | 
			
		||||
import nodesApi from './nodes.api';
 | 
			
		||||
class NodesRoutes {
 | 
			
		||||
  constructor() { }
 | 
			
		||||
 | 
			
		||||
  public initRoutes(app: Express) {
 | 
			
		||||
  public initRoutes(app: Application) {
 | 
			
		||||
    app
 | 
			
		||||
    .get(config.MEMPOOL.API_URL_PREFIX + 'statistics/latest', this.$getGeneralStats)
 | 
			
		||||
    .get(config.MEMPOOL.API_URL_PREFIX + 'nodes/search/:search', this.$searchNode)
 | 
			
		||||
    .get(config.MEMPOOL.API_URL_PREFIX + 'nodes/top', this.$getTopNodes)
 | 
			
		||||
    .get(config.MEMPOOL.API_URL_PREFIX + 'nodes/:public_key/statistics', this.$getHistoricalNodeStats)
 | 
			
		||||
    .get(config.MEMPOOL.API_URL_PREFIX + 'nodes/:public_key', this.$getNode)
 | 
			
		||||
  ;
 | 
			
		||||
      .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/:public_key/statistics', this.$getHistoricalNodeStats)
 | 
			
		||||
      .get(config.MEMPOOL.API_URL_PREFIX + 'lightning/nodes/:public_key', this.$getNode)
 | 
			
		||||
    ;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $searchNode(req: Request, res: Response) {
 | 
			
		||||
@ -45,15 +44,6 @@ class NodesRoutes {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $getGeneralStats(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const statistics = await nodesApi.$getLatestStatistics();
 | 
			
		||||
      res.json(statistics);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      res.status(500).send(e instanceof Error ? e.message : e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $getTopNodes(req: Request, res: Response) {
 | 
			
		||||
    try {
 | 
			
		||||
      const topCapacityNodes = await nodesApi.$getTopCapacityNodes();
 | 
			
		||||
							
								
								
									
										32
									
								
								backend/src/api/explorer/statistics.api.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								backend/src/api/explorer/statistics.api.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,32 @@
 | 
			
		||||
import logger from '../../logger';
 | 
			
		||||
import DB from '../../database';
 | 
			
		||||
 | 
			
		||||
class StatisticsApi {
 | 
			
		||||
  public async $getStatistics(): Promise<any> {
 | 
			
		||||
    try {
 | 
			
		||||
      const query = `SELECT UNIX_TIMESTAMP(added) AS added, channel_count, node_count, total_capacity FROM lightning_stats ORDER BY id DESC`;
 | 
			
		||||
      const [rows]: any = await DB.query(query);
 | 
			
		||||
      return rows;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err('$getStatistics error: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getLatestStatistics(): Promise<any> {
 | 
			
		||||
    try {
 | 
			
		||||
      const [rows]: any = await DB.query(`SELECT * FROM lightning_stats ORDER BY id DESC LIMIT 1`);
 | 
			
		||||
      const [rows2]: any = await DB.query(`SELECT * FROM lightning_stats ORDER BY id DESC LIMIT 1 OFFSET 72`);
 | 
			
		||||
      return {
 | 
			
		||||
        latest: rows[0],
 | 
			
		||||
        previous: rows2[0],
 | 
			
		||||
      };
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err('$getLatestStatistics error: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new StatisticsApi();
 | 
			
		||||
@ -3,7 +3,7 @@ import { AbstractLightningApi } from './lightning-api-abstract-factory';
 | 
			
		||||
import LndApi from './lnd/lnd-api';
 | 
			
		||||
 | 
			
		||||
function lightningApiFactory(): AbstractLightningApi {
 | 
			
		||||
  switch (config.MEMPOOL.BACKEND) {
 | 
			
		||||
  switch (config.LIGHTNING.BACKEND) {
 | 
			
		||||
    case 'lnd':
 | 
			
		||||
    default:
 | 
			
		||||
      return new LndApi();
 | 
			
		||||
@ -8,14 +8,17 @@ import logger from '../../../logger';
 | 
			
		||||
class LndApi implements AbstractLightningApi {
 | 
			
		||||
  private lnd: any;
 | 
			
		||||
  constructor() {
 | 
			
		||||
    if (!config.LIGHTNING.ENABLED) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    try {
 | 
			
		||||
      const tls = fs.readFileSync(config.LN_NODE_AUTH.TLS_CERT_PATH).toString('base64');
 | 
			
		||||
      const macaroon = fs.readFileSync(config.LN_NODE_AUTH.MACAROON_PATH).toString('base64');
 | 
			
		||||
      const tls = fs.readFileSync(config.LND_NODE_AUTH.TLS_CERT_PATH).toString('base64');
 | 
			
		||||
      const macaroon = fs.readFileSync(config.LND_NODE_AUTH.MACAROON_PATH).toString('base64');
 | 
			
		||||
 | 
			
		||||
      const { lnd } = lnService.authenticatedLndGrpc({
 | 
			
		||||
        cert: tls,
 | 
			
		||||
        macaroon: macaroon,
 | 
			
		||||
        socket: config.LN_NODE_AUTH.SOCKET,
 | 
			
		||||
        socket: config.LND_NODE_AUTH.SOCKET,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      this.lnd = lnd;
 | 
			
		||||
@ -27,6 +27,15 @@ interface IConfig {
 | 
			
		||||
  ESPLORA: {
 | 
			
		||||
    REST_API_URL: string;
 | 
			
		||||
  };
 | 
			
		||||
  LIGHTNING: {
 | 
			
		||||
    ENABLED: boolean;
 | 
			
		||||
    BACKEND: 'lnd' | 'cln' | 'ldk';
 | 
			
		||||
  };
 | 
			
		||||
  LND_NODE_AUTH: {
 | 
			
		||||
    TLS_CERT_PATH: string;
 | 
			
		||||
    MACAROON_PATH: string;
 | 
			
		||||
    SOCKET: string;
 | 
			
		||||
  };
 | 
			
		||||
  ELECTRUM: {
 | 
			
		||||
    HOST: string;
 | 
			
		||||
    PORT: number;
 | 
			
		||||
@ -158,6 +167,15 @@ const defaults: IConfig = {
 | 
			
		||||
    'ENABLED': false,
 | 
			
		||||
    'DATA_PATH': '/bisq/statsnode-data/btc_mainnet/db'
 | 
			
		||||
  },
 | 
			
		||||
  'LIGHTNING': {
 | 
			
		||||
    'ENABLED': false,
 | 
			
		||||
    'BACKEND': 'lnd'
 | 
			
		||||
  },
 | 
			
		||||
  'LND_NODE_AUTH': {
 | 
			
		||||
    'TLS_CERT_PATH': '',
 | 
			
		||||
    'MACAROON_PATH': '',
 | 
			
		||||
    'SOCKET': 'localhost:10009',
 | 
			
		||||
  },
 | 
			
		||||
  'SOCKS5PROXY': {
 | 
			
		||||
    'ENABLED': false,
 | 
			
		||||
    'USE_ONION': true,
 | 
			
		||||
@ -166,11 +184,11 @@ const defaults: IConfig = {
 | 
			
		||||
    'USERNAME': '',
 | 
			
		||||
    'PASSWORD': ''
 | 
			
		||||
  },
 | 
			
		||||
  "PRICE_DATA_SERVER": {
 | 
			
		||||
  'PRICE_DATA_SERVER': {
 | 
			
		||||
    'TOR_URL': 'http://wizpriceje6q5tdrxkyiazsgu7irquiqjy2dptezqhrtu7l2qelqktid.onion/getAllMarketPrices',
 | 
			
		||||
    'CLEARNET_URL': 'https://price.bisq.wiz.biz/getAllMarketPrices'
 | 
			
		||||
  },
 | 
			
		||||
  "EXTERNAL_DATA_SERVER": {
 | 
			
		||||
  'EXTERNAL_DATA_SERVER': {
 | 
			
		||||
    'MEMPOOL_API': 'https://mempool.space/api/v1',
 | 
			
		||||
    'MEMPOOL_ONION': 'http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/api/v1',
 | 
			
		||||
    'LIQUID_API': 'https://liquid.network/api/v1',
 | 
			
		||||
@ -190,6 +208,8 @@ class Config implements IConfig {
 | 
			
		||||
  SYSLOG: IConfig['SYSLOG'];
 | 
			
		||||
  STATISTICS: IConfig['STATISTICS'];
 | 
			
		||||
  BISQ: IConfig['BISQ'];
 | 
			
		||||
  LIGHTNING: IConfig['LIGHTNING'];
 | 
			
		||||
  LND_NODE_AUTH: IConfig['LND_NODE_AUTH'];
 | 
			
		||||
  SOCKS5PROXY: IConfig['SOCKS5PROXY'];
 | 
			
		||||
  PRICE_DATA_SERVER: IConfig['PRICE_DATA_SERVER'];
 | 
			
		||||
  EXTERNAL_DATA_SERVER: IConfig['EXTERNAL_DATA_SERVER'];
 | 
			
		||||
@ -205,6 +225,8 @@ class Config implements IConfig {
 | 
			
		||||
    this.SYSLOG = configs.SYSLOG;
 | 
			
		||||
    this.STATISTICS = configs.STATISTICS;
 | 
			
		||||
    this.BISQ = configs.BISQ;
 | 
			
		||||
    this.LIGHTNING = configs.LIGHTNING;
 | 
			
		||||
    this.LND_NODE_AUTH = configs.LND_NODE_AUTH;
 | 
			
		||||
    this.SOCKS5PROXY = configs.SOCKS5PROXY;
 | 
			
		||||
    this.PRICE_DATA_SERVER = configs.PRICE_DATA_SERVER;
 | 
			
		||||
    this.EXTERNAL_DATA_SERVER = configs.EXTERNAL_DATA_SERVER;
 | 
			
		||||
 | 
			
		||||
@ -29,6 +29,11 @@ import poolsUpdater from './tasks/pools-updater';
 | 
			
		||||
import indexer from './indexer';
 | 
			
		||||
import priceUpdater from './tasks/price-updater';
 | 
			
		||||
import BlocksAuditsRepository from './repositories/BlocksAuditsRepository';
 | 
			
		||||
import nodeSyncService from './tasks/lightning/node-sync.service';
 | 
			
		||||
import lightningStatsUpdater from './tasks/lightning/stats-updater.service';
 | 
			
		||||
import nodesRoutes from './api/explorer/nodes.routes';
 | 
			
		||||
import channelsRoutes from './api/explorer/channels.routes';
 | 
			
		||||
import generalLightningRoutes from './api/explorer/general.routes';
 | 
			
		||||
 | 
			
		||||
class Server {
 | 
			
		||||
  private wss: WebSocket.Server | undefined;
 | 
			
		||||
@ -130,6 +135,13 @@ class Server {
 | 
			
		||||
      bisqMarkets.startBisqService();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (config.LIGHTNING.ENABLED) {
 | 
			
		||||
      nodeSyncService.$startService()
 | 
			
		||||
        .then(() => {
 | 
			
		||||
          lightningStatsUpdater.$startService();
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.server.listen(config.MEMPOOL.HTTP_PORT, () => {
 | 
			
		||||
      if (worker) {
 | 
			
		||||
        logger.info(`Mempool Server worker #${process.pid} started`);
 | 
			
		||||
@ -362,6 +374,12 @@ class Server {
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'liquid/pegs/month', routes.$getElementsPegsByMonth)
 | 
			
		||||
        ;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (config.LIGHTNING.ENABLED) {
 | 
			
		||||
      generalLightningRoutes.initRoutes(this.app);
 | 
			
		||||
      nodesRoutes.initRoutes(this.app);
 | 
			
		||||
      channelsRoutes.initRoutes(this.app);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,13 +1,13 @@
 | 
			
		||||
import { chanNumber } from 'bolt07';
 | 
			
		||||
import DB from '../database';
 | 
			
		||||
import logger from '../logger';
 | 
			
		||||
import lightningApi from '../api/lightning/lightning-api-factory';
 | 
			
		||||
import { ILightningApi } from '../api/lightning/lightning-api.interface';
 | 
			
		||||
import channelsApi from '../api/explorer/channels.api';
 | 
			
		||||
import bitcoinClient from '../api/bitcoin/bitcoin-client';
 | 
			
		||||
import bitcoinApi from '../api/bitcoin/bitcoin-api-factory';
 | 
			
		||||
import config from '../config';
 | 
			
		||||
import { IEsploraApi } from '../api/bitcoin/esplora-api.interface';
 | 
			
		||||
import DB from '../../database';
 | 
			
		||||
import logger from '../../logger';
 | 
			
		||||
import channelsApi from '../../api/explorer/channels.api';
 | 
			
		||||
import bitcoinClient from '../../api/bitcoin/bitcoin-client';
 | 
			
		||||
import bitcoinApi from '../../api/bitcoin/bitcoin-api-factory';
 | 
			
		||||
import config from '../../config';
 | 
			
		||||
import { IEsploraApi } from '../../api/bitcoin/esplora-api.interface';
 | 
			
		||||
import lightningApi from '../../api/lightning/lightning-api-factory';
 | 
			
		||||
import { ILightningApi } from '../../api/lightning/lightning-api.interface';
 | 
			
		||||
 | 
			
		||||
class NodeSyncService {
 | 
			
		||||
  constructor() {}
 | 
			
		||||
@ -15,43 +15,36 @@ class NodeSyncService {
 | 
			
		||||
  public async $startService() {
 | 
			
		||||
    logger.info('Starting node sync service');
 | 
			
		||||
 | 
			
		||||
    await this.$updateNodes();
 | 
			
		||||
    await this.$runUpdater();
 | 
			
		||||
 | 
			
		||||
    setInterval(async () => {
 | 
			
		||||
      await this.$updateNodes();
 | 
			
		||||
      await this.$runUpdater();
 | 
			
		||||
    }, 1000 * 60 * 60);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $updateNodes() {
 | 
			
		||||
  private async $runUpdater() {
 | 
			
		||||
    try {
 | 
			
		||||
      logger.info(`Updating nodes and channels...`);
 | 
			
		||||
 | 
			
		||||
      const networkGraph = await lightningApi.$getNetworkGraph();
 | 
			
		||||
 | 
			
		||||
      for (const node of networkGraph.nodes) {
 | 
			
		||||
        await this.$saveNode(node);
 | 
			
		||||
      }
 | 
			
		||||
      logger.debug(`Nodes updated`);
 | 
			
		||||
      logger.info(`Nodes updated.`);
 | 
			
		||||
 | 
			
		||||
      await this.$setChannelsInactive();
 | 
			
		||||
 | 
			
		||||
      for (const channel of networkGraph.channels) {
 | 
			
		||||
        await this.$saveChannel(channel);
 | 
			
		||||
      }
 | 
			
		||||
      logger.debug(`Channels updated`);
 | 
			
		||||
      logger.info(`Channels updated.`);
 | 
			
		||||
 | 
			
		||||
      await this.$findInactiveNodesAndChannels();
 | 
			
		||||
      logger.debug(`Inactive channels scan complete`);
 | 
			
		||||
 | 
			
		||||
      await this.$lookUpCreationDateFromChain();
 | 
			
		||||
      logger.debug(`Channel creation dates scan complete`);
 | 
			
		||||
 | 
			
		||||
      await this.$updateNodeFirstSeen();
 | 
			
		||||
      logger.debug(`Node first seen dates scan complete`);
 | 
			
		||||
 | 
			
		||||
      await this.$scanForClosedChannels();
 | 
			
		||||
      logger.debug(`Closed channels scan complete`);
 | 
			
		||||
 | 
			
		||||
      await this.$runClosedChannelsForensics();
 | 
			
		||||
      logger.debug(`Closed channels forensics scan complete`);
 | 
			
		||||
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err('$updateNodes() error: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
@ -80,18 +73,21 @@ class NodeSyncService {
 | 
			
		||||
          await DB.query(query, params);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      logger.info(`Node first seen dates scan complete.`);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err('$updateNodeFirstSeen() error: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $lookUpCreationDateFromChain() {
 | 
			
		||||
    logger.info(`Running channel creation date lookup...`);
 | 
			
		||||
    try {
 | 
			
		||||
      const channels = await channelsApi.$getChannelsWithoutCreatedDate();
 | 
			
		||||
      for (const channel of channels) {
 | 
			
		||||
        const transaction = await bitcoinClient.getRawTransaction(channel.transaction_id, 1);
 | 
			
		||||
        await DB.query(`UPDATE channels SET created = FROM_UNIXTIME(?) WHERE channels.id = ?`, [transaction.blocktime, channel.id]);
 | 
			
		||||
      }
 | 
			
		||||
      logger.info(`Channel creation dates scan complete.`);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err('$setCreationDateFromChain() error: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
    }
 | 
			
		||||
@ -99,6 +95,8 @@ class NodeSyncService {
 | 
			
		||||
 | 
			
		||||
  // Looking for channels whos nodes are inactive
 | 
			
		||||
  private async $findInactiveNodesAndChannels(): Promise<void> {
 | 
			
		||||
    logger.info(`Running inactive channels scan...`);
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      // @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)`);
 | 
			
		||||
@ -106,6 +104,7 @@ class NodeSyncService {
 | 
			
		||||
      for (const channel of channels) {
 | 
			
		||||
        await this.$updateChannelStatus(channel.id, 0);
 | 
			
		||||
      }
 | 
			
		||||
      logger.info(`Inactive channels scan complete.`);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err('$findInactiveNodesAndChannels() error: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
    }
 | 
			
		||||
@ -113,6 +112,7 @@ class NodeSyncService {
 | 
			
		||||
 | 
			
		||||
  private async $scanForClosedChannels(): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      logger.info(`Starting closed channels scan...`);
 | 
			
		||||
      const channels = await channelsApi.$getChannelsByStatus(0);
 | 
			
		||||
      for (const channel of channels) {
 | 
			
		||||
        const spendingTx = await bitcoinApi.$getOutspend(channel.transaction_id, channel.transaction_vout);
 | 
			
		||||
@ -125,6 +125,7 @@ class NodeSyncService {
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      logger.info(`Closed channels scan complete.`);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err('$scanForClosedChannels() error: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
    }
 | 
			
		||||
@ -140,8 +141,8 @@ class NodeSyncService {
 | 
			
		||||
    if (!config.ESPLORA.REST_API_URL) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      logger.info(`Started running closed channel forensics...`);
 | 
			
		||||
      const channels = await channelsApi.$getClosedChannelsWithoutReason();
 | 
			
		||||
      for (const channel of channels) {
 | 
			
		||||
        let reason = 0;
 | 
			
		||||
@ -186,6 +187,7 @@ class NodeSyncService {
 | 
			
		||||
          await DB.query(`UPDATE channels SET closing_reason = ? WHERE id = ?`, [reason, channel.id]);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      logger.info(`Closed channels forensics scan complete.`);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err('$runClosedChannelsForensics() error: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
    }
 | 
			
		||||
@ -1,7 +1,6 @@
 | 
			
		||||
 | 
			
		||||
import DB from '../database';
 | 
			
		||||
import logger from '../logger';
 | 
			
		||||
import lightningApi from '../api/lightning/lightning-api-factory';
 | 
			
		||||
import logger from "../../logger";
 | 
			
		||||
import DB from "../../database";
 | 
			
		||||
import lightningApi from "../../api/lightning/lightning-api-factory";
 | 
			
		||||
 | 
			
		||||
class LightningStatsUpdater {
 | 
			
		||||
  constructor() {}
 | 
			
		||||
@ -29,6 +28,8 @@ class LightningStatsUpdater {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $logNodeStatsDaily() {
 | 
			
		||||
    logger.info(`Running daily node stats update...`);
 | 
			
		||||
 | 
			
		||||
    const currentDate = new Date().toISOString().split('T')[0];
 | 
			
		||||
    try {
 | 
			
		||||
      const [state]: any = await DB.query(`SELECT string FROM state WHERE name = 'last_node_stats'`);
 | 
			
		||||
@ -52,7 +53,7 @@ class LightningStatsUpdater {
 | 
			
		||||
            node.channels_count_left + node.channels_count_right]);
 | 
			
		||||
      }
 | 
			
		||||
      await DB.query(`UPDATE state SET string = ? WHERE name = 'last_node_stats'`, [currentDate]);
 | 
			
		||||
      logger.debug('Daily node stats has updated.');
 | 
			
		||||
      logger.info('Daily node stats has updated.');
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err('$logNodeStatsDaily() error: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
    }
 | 
			
		||||
@ -60,9 +61,11 @@ class LightningStatsUpdater {
 | 
			
		||||
 | 
			
		||||
  // We only run this on first launch
 | 
			
		||||
  private async $populateHistoricalData() {
 | 
			
		||||
    logger.info(`Running historical stats population...`);
 | 
			
		||||
 | 
			
		||||
    const startTime = '2018-01-13';
 | 
			
		||||
    try {
 | 
			
		||||
      const [rows]: any = await DB.query(`SELECT COUNT(*) FROM statistics`);
 | 
			
		||||
      const [rows]: any = await DB.query(`SELECT COUNT(*) FROM lightning_stats`);
 | 
			
		||||
      // Only store once per day
 | 
			
		||||
      if (rows[0]['COUNT(*)'] > 0) {
 | 
			
		||||
        return;
 | 
			
		||||
@ -86,7 +89,7 @@ class LightningStatsUpdater {
 | 
			
		||||
          channelsCount++;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const query = `INSERT INTO statistics(
 | 
			
		||||
        const query = `INSERT INTO lightning_stats(
 | 
			
		||||
          added,
 | 
			
		||||
          channel_count,
 | 
			
		||||
          node_count,
 | 
			
		||||
@ -117,7 +120,7 @@ class LightningStatsUpdater {
 | 
			
		||||
          nodeCount++;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const query = `UPDATE statistics SET node_count = ? WHERE added = FROM_UNIXTIME(?)`;
 | 
			
		||||
        const query = `UPDATE lightning_stats SET node_count = ? WHERE added = FROM_UNIXTIME(?)`;
 | 
			
		||||
 | 
			
		||||
        await DB.query(query, [
 | 
			
		||||
          nodeCount,
 | 
			
		||||
@ -128,13 +131,15 @@ class LightningStatsUpdater {
 | 
			
		||||
        date.setDate(date.getDate() + 1);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      logger.debug('Historical stats populated.');
 | 
			
		||||
      logger.info('Historical stats populated.');
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err('$populateHistoricalData() error: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $logLightningStatsDaily() {
 | 
			
		||||
    logger.info(`Running lightning daily stats log...`);
 | 
			
		||||
 | 
			
		||||
    const currentDate = new Date().toISOString().split('T')[0];
 | 
			
		||||
    try {
 | 
			
		||||
      const [state]: any = await DB.query(`SELECT string FROM state WHERE name = 'last_node_stats'`);
 | 
			
		||||
@ -151,7 +156,7 @@ class LightningStatsUpdater {
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const query = `INSERT INTO statistics(
 | 
			
		||||
      const query = `INSERT INTO lightning_stats(
 | 
			
		||||
          added,
 | 
			
		||||
          channel_count,
 | 
			
		||||
          node_count,
 | 
			
		||||
@ -164,8 +169,9 @@ class LightningStatsUpdater {
 | 
			
		||||
        networkGraph.nodes.length,
 | 
			
		||||
        total_capacity,
 | 
			
		||||
      ]);
 | 
			
		||||
      logger.info(`Lightning daily stats done.`);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err('$logLightningStats() error: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
      logger.err('$logLightningStatsDaily() error: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -103,13 +103,13 @@ if (configContent && configContent.BASE_MODULE === 'bisq') {
 | 
			
		||||
 | 
			
		||||
PROXY_CONFIG.push(...[
 | 
			
		||||
  {
 | 
			
		||||
    context: ['/lightning/api/v1/**'],
 | 
			
		||||
    target: `http://localhost:8899`,
 | 
			
		||||
    context: ['/testnet/api/v1/lightning/**'],
 | 
			
		||||
    target: `http://localhost:8999`,
 | 
			
		||||
    secure: false,
 | 
			
		||||
    changeOrigin: true,
 | 
			
		||||
    proxyTimeout: 30000,
 | 
			
		||||
    pathRewrite: {
 | 
			
		||||
        "^/lightning/api": "/api"
 | 
			
		||||
        "^/testnet": ""
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
 | 
			
		||||
@ -1,23 +1,33 @@
 | 
			
		||||
import { Injectable } from '@angular/core';
 | 
			
		||||
import { HttpClient, HttpParams } from '@angular/common/http';
 | 
			
		||||
import { Observable } from 'rxjs';
 | 
			
		||||
 | 
			
		||||
const API_BASE_URL = '/lightning/api/v1';
 | 
			
		||||
import { StateService } from '../services/state.service';
 | 
			
		||||
 | 
			
		||||
@Injectable({
 | 
			
		||||
  providedIn: 'root'
 | 
			
		||||
})
 | 
			
		||||
export class LightningApiService {
 | 
			
		||||
  private apiBasePath = ''; // network path is /testnet, etc. or '' for mainnet
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private httpClient: HttpClient,
 | 
			
		||||
  ) { }
 | 
			
		||||
    private stateService: StateService,
 | 
			
		||||
  ) {
 | 
			
		||||
    this.apiBasePath = ''; // assume mainnet by default
 | 
			
		||||
    this.stateService.networkChanged$.subscribe((network) => {
 | 
			
		||||
      if (network === 'bisq' && !this.stateService.env.BISQ_SEPARATE_BACKEND) {
 | 
			
		||||
        network = '';
 | 
			
		||||
      }
 | 
			
		||||
      this.apiBasePath = network ? '/' + network : '';
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getNode$(publicKey: string): Observable<any> {
 | 
			
		||||
    return this.httpClient.get<any>(API_BASE_URL + '/nodes/' + publicKey);
 | 
			
		||||
    return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getChannel$(shortId: string): Observable<any> {
 | 
			
		||||
    return this.httpClient.get<any>(API_BASE_URL + '/channels/' + shortId);
 | 
			
		||||
    return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/channels/' + shortId);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getChannelsByNodeId$(publicKey: string, index: number = 0, status = 'open'): Observable<any> {
 | 
			
		||||
@ -27,22 +37,22 @@ export class LightningApiService {
 | 
			
		||||
      .set('status', status)
 | 
			
		||||
    ;
 | 
			
		||||
 | 
			
		||||
    return this.httpClient.get<any>(API_BASE_URL + '/channels', { params, observe: 'response' });
 | 
			
		||||
    return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/channels', { params, observe: 'response' });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getLatestStatistics$(): Observable<any> {
 | 
			
		||||
    return this.httpClient.get<any>(API_BASE_URL + '/statistics/latest');
 | 
			
		||||
    return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/statistics/latest');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  listNodeStats$(publicKey: string): Observable<any> {
 | 
			
		||||
    return this.httpClient.get<any>(API_BASE_URL + '/nodes/' + publicKey + '/statistics');
 | 
			
		||||
    return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/nodes/' + publicKey + '/statistics');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  listTopNodes$(): Observable<any> {
 | 
			
		||||
    return this.httpClient.get<any>(API_BASE_URL + '/nodes/top');
 | 
			
		||||
    return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/nodes/top');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  listStatistics$(): Observable<any> {
 | 
			
		||||
    return this.httpClient.get<any>(API_BASE_URL + '/statistics');
 | 
			
		||||
    return this.httpClient.get<any>(this.apiBasePath + '/api/v1/lightning/statistics');
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -237,12 +237,12 @@ export class ApiService {
 | 
			
		||||
    txIds.forEach((txId: string) => {
 | 
			
		||||
      params = params.append('txId[]', txId);
 | 
			
		||||
    });
 | 
			
		||||
    return this.httpClient.get<{ inputs: any[], outputs: any[] }>(this.apiBaseUrl + this.apiBasePath + '/lightning/api/v1/channels/txids/', { params });
 | 
			
		||||
    return this.httpClient.get<{ inputs: any[], outputs: any[] }>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/channels/txids/', { params });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  lightningSearch$(searchText: string): Observable<any[]> {
 | 
			
		||||
    let params = new HttpParams().set('searchText', searchText);
 | 
			
		||||
    return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/lightning/api/v1/search', { params });
 | 
			
		||||
    return this.httpClient.get<any[]>(this.apiBaseUrl + this.apiBasePath + '/api/v1/lightning/search', { params });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										48
									
								
								lightning-backend/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										48
									
								
								lightning-backend/.gitignore
									
									
									
									
										vendored
									
									
								
							@ -1,48 +0,0 @@
 | 
			
		||||
# See http://help.github.com/ignore-files/ for more about ignoring files.
 | 
			
		||||
 | 
			
		||||
# production config and external assets
 | 
			
		||||
*.json
 | 
			
		||||
!mempool-config.sample.json
 | 
			
		||||
!package.json
 | 
			
		||||
!package-lock.json
 | 
			
		||||
!tslint.json
 | 
			
		||||
!tsconfig.json
 | 
			
		||||
 | 
			
		||||
# compiled output
 | 
			
		||||
/dist
 | 
			
		||||
/tmp
 | 
			
		||||
 | 
			
		||||
# dependencies
 | 
			
		||||
/node_modules
 | 
			
		||||
 | 
			
		||||
# IDEs and editors
 | 
			
		||||
/.idea
 | 
			
		||||
.project
 | 
			
		||||
.classpath
 | 
			
		||||
.c9/
 | 
			
		||||
*.launch
 | 
			
		||||
.settings/
 | 
			
		||||
 | 
			
		||||
# IDE - VSCode
 | 
			
		||||
.vscode/*
 | 
			
		||||
!.vscode/settings.json
 | 
			
		||||
!.vscode/tasks.json
 | 
			
		||||
!.vscode/launch.json
 | 
			
		||||
!.vscode/extensions.json
 | 
			
		||||
 | 
			
		||||
# misc
 | 
			
		||||
/.sass-cache
 | 
			
		||||
/connect.lock
 | 
			
		||||
/coverage/*
 | 
			
		||||
/libpeerconnection.log
 | 
			
		||||
npm-debug.log
 | 
			
		||||
testem.log
 | 
			
		||||
/typings
 | 
			
		||||
 | 
			
		||||
# e2e
 | 
			
		||||
/e2e/*.js
 | 
			
		||||
/e2e/*.map
 | 
			
		||||
 | 
			
		||||
#System Files
 | 
			
		||||
.DS_Store
 | 
			
		||||
Thumbs.db
 | 
			
		||||
@ -1,38 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "MEMPOOL": {
 | 
			
		||||
    "NETWORK": "mainnet",
 | 
			
		||||
    "BACKEND": "lnd",
 | 
			
		||||
    "HTTP_PORT": 8899,
 | 
			
		||||
    "API_URL_PREFIX": "/api/v1/",
 | 
			
		||||
    "STDOUT_LOG_MIN_PRIORITY": "debug"
 | 
			
		||||
  },
 | 
			
		||||
  "ESPLORA": {
 | 
			
		||||
    "REST_API_URL": ""
 | 
			
		||||
  },
 | 
			
		||||
  "SYSLOG": {
 | 
			
		||||
    "ENABLED": false,
 | 
			
		||||
    "HOST": "127.0.0.1",
 | 
			
		||||
    "PORT": 514,
 | 
			
		||||
    "MIN_PRIORITY": "info",
 | 
			
		||||
    "FACILITY": "local7"
 | 
			
		||||
  },
 | 
			
		||||
  "LN_NODE_AUTH": {
 | 
			
		||||
    "TLS_CERT_PATH": "",
 | 
			
		||||
    "MACAROON_PATH": "",
 | 
			
		||||
    "SOCKET": "localhost:10009"
 | 
			
		||||
  },
 | 
			
		||||
  "CORE_RPC": {
 | 
			
		||||
    "HOST": "127.0.0.1",
 | 
			
		||||
    "PORT": 8332,
 | 
			
		||||
    "USERNAME": "mempool",
 | 
			
		||||
    "PASSWORD": "mempool"
 | 
			
		||||
  },
 | 
			
		||||
  "DATABASE": {
 | 
			
		||||
    "HOST": "127.0.0.1",
 | 
			
		||||
    "PORT": 3306,
 | 
			
		||||
    "SOCKET": "/var/run/mysql/mysql.sock",
 | 
			
		||||
    "DATABASE": "lightning",
 | 
			
		||||
    "USERNAME": "root",
 | 
			
		||||
    "PASSWORD": "root"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										3291
									
								
								lightning-backend/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										3291
									
								
								lightning-backend/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -1,25 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "lightning-backend",
 | 
			
		||||
  "version": "1.0.0",
 | 
			
		||||
  "description": "Backend for the Mempool Lightning Explorer",
 | 
			
		||||
  "license": "AGPL-3.0",
 | 
			
		||||
  "main": "index.ts",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "test": "echo \"Error: no test specified\" && exit 1",
 | 
			
		||||
    "tsc": "./node_modules/typescript/bin/tsc",
 | 
			
		||||
    "build": "npm run tsc",
 | 
			
		||||
    "start": "node --max-old-space-size=2048 dist/index.js"
 | 
			
		||||
  },
 | 
			
		||||
  "author": "",
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@types/express": "^4.17.13",
 | 
			
		||||
    "@types/node": "^17.0.24"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "axios": "^0.27.2",
 | 
			
		||||
    "express": "^4.17.3",
 | 
			
		||||
    "ln-service": "^53.11.0",
 | 
			
		||||
    "mysql2": "^2.3.3",
 | 
			
		||||
    "typescript": "^4.6.3"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,25 +0,0 @@
 | 
			
		||||
import { IEsploraApi } from './esplora-api.interface';
 | 
			
		||||
 | 
			
		||||
export interface AbstractBitcoinApi {
 | 
			
		||||
  $getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]>;
 | 
			
		||||
  $getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean, lazyPrevouts?: boolean): Promise<IEsploraApi.Transaction>;
 | 
			
		||||
  $getBlockHeightTip(): Promise<number>;
 | 
			
		||||
  $getTxIdsForBlock(hash: string): Promise<string[]>;
 | 
			
		||||
  $getBlockHash(height: number): Promise<string>;
 | 
			
		||||
  $getBlockHeader(hash: string): Promise<string>;
 | 
			
		||||
  $getBlock(hash: string): Promise<IEsploraApi.Block>;
 | 
			
		||||
  $getAddress(address: string): Promise<IEsploraApi.Address>;
 | 
			
		||||
  $getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
 | 
			
		||||
  $getAddressPrefix(prefix: string): string[];
 | 
			
		||||
  $sendRawTransaction(rawTransaction: string): Promise<string>;
 | 
			
		||||
  $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend>;
 | 
			
		||||
  $getOutspends(txId: string): Promise<IEsploraApi.Outspend[]>;
 | 
			
		||||
  $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]>;
 | 
			
		||||
}
 | 
			
		||||
export interface BitcoinRpcCredentials {
 | 
			
		||||
  host: string;
 | 
			
		||||
  port: number;
 | 
			
		||||
  user: string;
 | 
			
		||||
  pass: string;
 | 
			
		||||
  timeout: number;
 | 
			
		||||
}
 | 
			
		||||
@ -1,15 +0,0 @@
 | 
			
		||||
import config from '../../config';
 | 
			
		||||
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
 | 
			
		||||
import EsploraApi from './esplora-api';
 | 
			
		||||
import BitcoinApi from './bitcoin-api';
 | 
			
		||||
import bitcoinClient from './bitcoin-client';
 | 
			
		||||
 | 
			
		||||
function bitcoinApiFactory(): AbstractBitcoinApi {
 | 
			
		||||
  if (config.ESPLORA.REST_API_URL) {
 | 
			
		||||
    return new EsploraApi();
 | 
			
		||||
  } else {
 | 
			
		||||
    return new BitcoinApi(bitcoinClient);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default bitcoinApiFactory();
 | 
			
		||||
@ -1,175 +0,0 @@
 | 
			
		||||
export namespace IBitcoinApi {
 | 
			
		||||
  export interface MempoolInfo {
 | 
			
		||||
    loaded: boolean;                 //  (boolean) True if the mempool is fully loaded
 | 
			
		||||
    size: number;                    //  (numeric) Current tx count
 | 
			
		||||
    bytes: number;                   //  (numeric) Sum of all virtual transaction sizes as defined in BIP 141.
 | 
			
		||||
    usage: number;                   //  (numeric) Total memory usage for the mempool
 | 
			
		||||
    total_fee: number;               //  (numeric) Total fees of transactions in the mempool
 | 
			
		||||
    maxmempool: number;              //  (numeric) Maximum memory usage for the mempool
 | 
			
		||||
    mempoolminfee: number;           //  (numeric) Minimum fee rate in BTC/kB for tx to be accepted.
 | 
			
		||||
    minrelaytxfee: number;           //  (numeric) Current minimum relay fee for transactions
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface RawMempool { [txId: string]: MempoolEntry; }
 | 
			
		||||
 | 
			
		||||
  export interface MempoolEntry {
 | 
			
		||||
    vsize: number;                   //  (numeric) virtual transaction size as defined in BIP 141.
 | 
			
		||||
    weight: number;                  //  (numeric) transaction weight as defined in BIP 141.
 | 
			
		||||
    time: number;                    //  (numeric) local time transaction entered pool in seconds since 1 Jan 1970 GMT
 | 
			
		||||
    height: number;                  //  (numeric) block height when transaction entered pool
 | 
			
		||||
    descendantcount: number;         //  (numeric) number of in-mempool descendant transactions (including this one)
 | 
			
		||||
    descendantsize: number;          //  (numeric) virtual transaction size of in-mempool descendants (including this one)
 | 
			
		||||
    ancestorcount: number;           //  (numeric) number of in-mempool ancestor transactions (including this one)
 | 
			
		||||
    ancestorsize: number;            //  (numeric) virtual transaction size of in-mempool ancestors (including this one)
 | 
			
		||||
    wtxid: string;                   //  (string) hash of serialized transactionumber; including witness data
 | 
			
		||||
    fees: {
 | 
			
		||||
      base: number;                  //  (numeric) transaction fee in BTC
 | 
			
		||||
      modified: number;              //  (numeric) transaction fee with fee deltas used for mining priority in BTC
 | 
			
		||||
      ancestor: number;              //  (numeric) modified fees (see above) of in-mempool ancestors (including this one) in BTC
 | 
			
		||||
      descendant: number;            //  (numeric) modified fees (see above) of in-mempool descendants (including this one) in BTC
 | 
			
		||||
    };
 | 
			
		||||
    depends: string[];               //  (string) parent transaction id
 | 
			
		||||
    spentby: string[];               //  (array) unconfirmed transactions spending outputs from this transaction
 | 
			
		||||
    'bip125-replaceable': boolean;   //  (boolean) Whether this transaction could be replaced due to BIP125 (replace-by-fee)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface Block {
 | 
			
		||||
    hash: string;                    //  (string) the block hash (same as provided)
 | 
			
		||||
    confirmations: number;           //  (numeric) The number of confirmations, or -1 if the block is not on the main chain
 | 
			
		||||
    size: number;                    //  (numeric) The block size
 | 
			
		||||
    strippedsize: number;            //  (numeric) The block size excluding witness data
 | 
			
		||||
    weight: number;                  //  (numeric) The block weight as defined in BIP 141
 | 
			
		||||
    height: number;                  //  (numeric) The block height or index
 | 
			
		||||
    version: number;                 //  (numeric) The block version
 | 
			
		||||
    versionHex: string;              //  (string) The block version formatted in hexadecimal
 | 
			
		||||
    merkleroot: string;              //  (string) The merkle root
 | 
			
		||||
    tx: Transaction[];
 | 
			
		||||
    time: number;                    //  (numeric) The block time expressed in UNIX epoch time
 | 
			
		||||
    mediantime: number;              //  (numeric) The median block time expressed in UNIX epoch time
 | 
			
		||||
    nonce: number;                   //  (numeric) The nonce
 | 
			
		||||
    bits: string;                    //  (string) The bits
 | 
			
		||||
    difficulty: number;              //  (numeric) The difficulty
 | 
			
		||||
    chainwork: string;               //  (string) Expected number of hashes required to produce the chain up to this block (in hex)
 | 
			
		||||
    nTx: number;                     //  (numeric) The number of transactions in the block
 | 
			
		||||
    previousblockhash: string;       //  (string) The hash of the previous block
 | 
			
		||||
    nextblockhash: string;           //  (string) The hash of the next block
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface Transaction {
 | 
			
		||||
    in_active_chain: boolean;        //  (boolean) Whether specified block is in the active chain or not
 | 
			
		||||
    hex: string;                     //  (string) The serialized, hex-encoded data for 'txid'
 | 
			
		||||
    txid: string;                    //  (string) The transaction id (same as provided)
 | 
			
		||||
    hash: string;                    //  (string) The transaction hash (differs from txid for witness transactions)
 | 
			
		||||
    size: number;                    //  (numeric) The serialized transaction size
 | 
			
		||||
    vsize: number;                   //  (numeric) The virtual transaction size (differs from size for witness transactions)
 | 
			
		||||
    weight: number;                  //  (numeric) The transaction's weight (between vsize*4-3 and vsize*4)
 | 
			
		||||
    version: number;                 //  (numeric) The version
 | 
			
		||||
    locktime: number;                //  (numeric) The lock time
 | 
			
		||||
    vin: Vin[];
 | 
			
		||||
    vout: Vout[];
 | 
			
		||||
    blockhash: string;               //  (string) the block hash
 | 
			
		||||
    confirmations: number;           //  (numeric) The confirmations
 | 
			
		||||
    blocktime: number;               //  (numeric) The block time expressed in UNIX epoch time
 | 
			
		||||
    time: number;                    //  (numeric) Same as blocktime
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface VerboseBlock extends Block {
 | 
			
		||||
    tx: VerboseTransaction[];        // The transactions in the format of the getrawtransaction RPC. Different from verbosity = 1 "tx" result
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface VerboseTransaction extends Transaction {
 | 
			
		||||
    fee?: number;                   //  (numeric) The transaction fee in BTC, omitted if block undo data is not available
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface Vin {
 | 
			
		||||
    txid?: string;                   //  (string) The transaction id
 | 
			
		||||
    vout?: number;                   //  (string)
 | 
			
		||||
    scriptSig?: {                    //  (json object) The script
 | 
			
		||||
      asm: string;                   //  (string) asm
 | 
			
		||||
      hex: string;                   //  (string) hex
 | 
			
		||||
    };
 | 
			
		||||
    sequence: number;                //  (numeric) The script sequence number
 | 
			
		||||
    txinwitness?: string[];          //  (string) hex-encoded witness data
 | 
			
		||||
    coinbase?: string;
 | 
			
		||||
    is_pegin?: boolean;              //  (boolean) Elements peg-in
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface Vout {
 | 
			
		||||
    value: number;                   //  (numeric) The value in BTC
 | 
			
		||||
    n: number;                       //  (numeric) index
 | 
			
		||||
    asset?: string;                  //  (string) Elements asset id
 | 
			
		||||
    scriptPubKey: {                  //  (json object)
 | 
			
		||||
      asm: string;                   //  (string) the asm
 | 
			
		||||
      hex: string;                   //  (string) the hex
 | 
			
		||||
      reqSigs?: number;              //  (numeric) The required sigs
 | 
			
		||||
      type: string;                  //  (string) The type, eg 'pubkeyhash'
 | 
			
		||||
      address?: string;              //  (string) bitcoin address
 | 
			
		||||
      addresses?: string[];           //  (string) bitcoin addresses
 | 
			
		||||
      pegout_chain?: string;         //  (string) Elements peg-out chain
 | 
			
		||||
      pegout_addresses?: string[];   //  (string) Elements peg-out addresses
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface AddressInformation {
 | 
			
		||||
    isvalid: boolean;                //  (boolean) If the address is valid or not. If not, this is the only property returned.
 | 
			
		||||
    isvalid_parent?: boolean;        //  (boolean) Elements only
 | 
			
		||||
    address: string;                 //  (string) The bitcoin address validated
 | 
			
		||||
    scriptPubKey: string;            //  (string) The hex-encoded scriptPubKey generated by the address
 | 
			
		||||
    isscript: boolean;               //  (boolean) If the key is a script
 | 
			
		||||
    iswitness: boolean;              //  (boolean) If the address is a witness
 | 
			
		||||
    witness_version?: number;        //  (numeric, optional) The version number of the witness program
 | 
			
		||||
    witness_program: string;         //  (string, optional) The hex value of the witness program
 | 
			
		||||
    confidential_key?: string;       //  (string) Elements only
 | 
			
		||||
    unconfidential?: string;         //  (string) Elements only
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface ChainTips {
 | 
			
		||||
    height: number;                  //  (numeric) height of the chain tip
 | 
			
		||||
    hash: string;                    //  (string) block hash of the tip
 | 
			
		||||
    branchlen: number;               //  (numeric) zero for main chain, otherwise length of branch connecting the tip to the main chain
 | 
			
		||||
    status: 'invalid' | 'headers-only' | 'valid-headers' | 'valid-fork' | 'active';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface BlockchainInfo {
 | 
			
		||||
    chain: number;                   // (string) current network name as defined in BIP70 (main, test, regtest)
 | 
			
		||||
    blocks: number;                  // (numeric) the current number of blocks processed in the server
 | 
			
		||||
    headers: number;                 // (numeric) the current number of headers we have validated
 | 
			
		||||
    bestblockhash: string,           // (string) the hash of the currently best block
 | 
			
		||||
    difficulty: number;              // (numeric) the current difficulty
 | 
			
		||||
    mediantime: number;              // (numeric) median time for the current best block
 | 
			
		||||
    verificationprogress: number;    // (numeric) estimate of verification progress [0..1]
 | 
			
		||||
    initialblockdownload: boolean;   // (bool) (debug information) estimate of whether this node is in Initial Block Download mode.
 | 
			
		||||
    chainwork: string                // (string) total amount of work in active chain, in hexadecimal
 | 
			
		||||
    size_on_disk: number;            // (numeric) the estimated size of the block and undo files on disk
 | 
			
		||||
    pruned: number;                  // (boolean) if the blocks are subject to pruning
 | 
			
		||||
    pruneheight: number;             // (numeric) lowest-height complete block stored (only present if pruning is enabled)
 | 
			
		||||
    automatic_pruning: number;       // (boolean) whether automatic pruning is enabled (only present if pruning is enabled)
 | 
			
		||||
    prune_target_size: number;       // (numeric) the target size used by pruning (only present if automatic pruning is enabled)
 | 
			
		||||
    softforks: SoftFork[];           // (array) status of softforks in progress
 | 
			
		||||
    bip9_softforks: { [name: string]: Bip9SoftForks[] } // (object) status of BIP9 softforks in progress
 | 
			
		||||
    warnings: string;                // (string) any network and blockchain warnings.
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  interface SoftFork {
 | 
			
		||||
    id: string;                      // (string) name of softfork
 | 
			
		||||
    version: number;                 // (numeric) block version
 | 
			
		||||
    reject: {                        // (object) progress toward rejecting pre-softfork blocks
 | 
			
		||||
      status: boolean;               // (boolean) true if threshold reached
 | 
			
		||||
    },
 | 
			
		||||
  }
 | 
			
		||||
  interface Bip9SoftForks {
 | 
			
		||||
    status: number;                  // (string) one of defined, started, locked_in, active, failed
 | 
			
		||||
    bit: number;                     // (numeric) the bit (0-28) in the block version field used to signal this softfork (only for started status)
 | 
			
		||||
    startTime: number;               // (numeric) the minimum median time past of a block at which the bit gains its meaning
 | 
			
		||||
    timeout: number;                 // (numeric) the median time past of a block at which the deployment is considered failed if not yet locked in
 | 
			
		||||
    since: number;                   // (numeric) height of the first block to which the status applies
 | 
			
		||||
    statistics: {                    // (object) numeric statistics about BIP9 signalling for a softfork (only for started status)
 | 
			
		||||
      period: number;                // (numeric) the length in blocks of the BIP9 signalling period 
 | 
			
		||||
      threshold: number;             // (numeric) the number of blocks with the version bit set required to activate the feature 
 | 
			
		||||
      elapsed: number;               // (numeric) the number of blocks elapsed since the beginning of the current period 
 | 
			
		||||
      count: number;                 // (numeric) the number of blocks with the version bit set in the current period 
 | 
			
		||||
      possible: boolean;             // (boolean) returns false if there are not enough blocks left in this period to pass activation threshold 
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -1,313 +0,0 @@
 | 
			
		||||
import * as bitcoinjs from 'bitcoinjs-lib';
 | 
			
		||||
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
 | 
			
		||||
import { IBitcoinApi } from './bitcoin-api.interface';
 | 
			
		||||
import { IEsploraApi } from './esplora-api.interface';
 | 
			
		||||
 | 
			
		||||
class BitcoinApi implements AbstractBitcoinApi {
 | 
			
		||||
  protected bitcoindClient: any;
 | 
			
		||||
 | 
			
		||||
  constructor(bitcoinClient: any) {
 | 
			
		||||
    this.bitcoindClient = bitcoinClient;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getAddressPrefix(prefix: string): string[] {
 | 
			
		||||
    throw new Error('Method not implemented.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getBlock(hash: string): Promise<IEsploraApi.Block> {
 | 
			
		||||
    throw new Error('Method not implemented.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getRawTransaction(txId: string, skipConversion = false, addPrevout = false, lazyPrevouts = false): Promise<IEsploraApi.Transaction> {
 | 
			
		||||
    return this.bitcoindClient.getRawTransaction(txId, true)
 | 
			
		||||
      .then((transaction: IBitcoinApi.Transaction) => {
 | 
			
		||||
        if (skipConversion) {
 | 
			
		||||
          transaction.vout.forEach((vout) => {
 | 
			
		||||
            vout.value = Math.round(vout.value * 100000000);
 | 
			
		||||
          });
 | 
			
		||||
          return transaction;
 | 
			
		||||
        }
 | 
			
		||||
        return this.$convertTransaction(transaction, addPrevout, lazyPrevouts);
 | 
			
		||||
      })
 | 
			
		||||
      .catch((e: Error) => {
 | 
			
		||||
        throw e;
 | 
			
		||||
      });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getBlockHeightTip(): Promise<number> {
 | 
			
		||||
    return this.bitcoindClient.getChainTips()
 | 
			
		||||
      .then((result: IBitcoinApi.ChainTips[]) => {
 | 
			
		||||
        return result.find(tip => tip.status === 'active')!.height;
 | 
			
		||||
      });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getTxIdsForBlock(hash: string): Promise<string[]> {
 | 
			
		||||
    return this.bitcoindClient.getBlock(hash, 1)
 | 
			
		||||
      .then((rpcBlock: IBitcoinApi.Block) => rpcBlock.tx);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getRawBlock(hash: string): Promise<string> {
 | 
			
		||||
    return this.bitcoindClient.getBlock(hash, 0);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getBlockHash(height: number): Promise<string> {
 | 
			
		||||
    return this.bitcoindClient.getBlockHash(height);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getBlockHeader(hash: string): Promise<string> {
 | 
			
		||||
    return this.bitcoindClient.getBlockHeader(hash, false);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getAddress(address: string): Promise<IEsploraApi.Address> {
 | 
			
		||||
    throw new Error('Method getAddress not supported by the Bitcoin RPC API.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]> {
 | 
			
		||||
    throw new Error('Method getAddressTransactions not supported by the Bitcoin RPC API.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]> {
 | 
			
		||||
    return this.bitcoindClient.getRawMemPool();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $sendRawTransaction(rawTransaction: string): Promise<string> {
 | 
			
		||||
    return this.bitcoindClient.sendRawTransaction(rawTransaction);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
 | 
			
		||||
    const txOut = await this.bitcoindClient.getTxOut(txId, vout, false);
 | 
			
		||||
    return {
 | 
			
		||||
      spent: txOut === null,
 | 
			
		||||
      status: {
 | 
			
		||||
        confirmed: true,
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async $getOutspends(txId: string): Promise<IEsploraApi.Outspend[]> {
 | 
			
		||||
    const outSpends: IEsploraApi.Outspend[] = [];
 | 
			
		||||
    const tx = await this.$getRawTransaction(txId, true, false);
 | 
			
		||||
    for (let i = 0; i < tx.vout.length; i++) {
 | 
			
		||||
      if (tx.status && tx.status.block_height === 0) {
 | 
			
		||||
        outSpends.push({
 | 
			
		||||
          spent: false
 | 
			
		||||
        });
 | 
			
		||||
      } else {
 | 
			
		||||
        const txOut = await this.bitcoindClient.getTxOut(txId, i);
 | 
			
		||||
        outSpends.push({
 | 
			
		||||
          spent: txOut === null,
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return outSpends;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]> {
 | 
			
		||||
    const outspends: IEsploraApi.Outspend[][] = [];
 | 
			
		||||
    for (const tx of txId) {
 | 
			
		||||
      const outspend = await this.$getOutspends(tx);
 | 
			
		||||
      outspends.push(outspend);
 | 
			
		||||
    }
 | 
			
		||||
    return outspends;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getEstimatedHashrate(blockHeight: number): Promise<number> {
 | 
			
		||||
    // 120 is the default block span in Core
 | 
			
		||||
    return this.bitcoindClient.getNetworkHashPs(120, blockHeight);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected async $convertTransaction(transaction: IBitcoinApi.Transaction, addPrevout: boolean, lazyPrevouts = false): Promise<IEsploraApi.Transaction> {
 | 
			
		||||
    let esploraTransaction: IEsploraApi.Transaction = {
 | 
			
		||||
      txid: transaction.txid,
 | 
			
		||||
      version: transaction.version,
 | 
			
		||||
      locktime: transaction.locktime,
 | 
			
		||||
      size: transaction.size,
 | 
			
		||||
      weight: transaction.weight,
 | 
			
		||||
      fee: 0,
 | 
			
		||||
      vin: [],
 | 
			
		||||
      vout: [],
 | 
			
		||||
      status: { confirmed: false },
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    esploraTransaction.vout = transaction.vout.map((vout) => {
 | 
			
		||||
      return {
 | 
			
		||||
        value: Math.round(vout.value * 100000000),
 | 
			
		||||
        scriptpubkey: vout.scriptPubKey.hex,
 | 
			
		||||
        scriptpubkey_address: vout.scriptPubKey && vout.scriptPubKey.address ? vout.scriptPubKey.address
 | 
			
		||||
          : vout.scriptPubKey.addresses ? vout.scriptPubKey.addresses[0] : '',
 | 
			
		||||
        scriptpubkey_asm: vout.scriptPubKey.asm ? this.convertScriptSigAsm(vout.scriptPubKey.hex) : '',
 | 
			
		||||
        scriptpubkey_type: this.translateScriptPubKeyType(vout.scriptPubKey.type),
 | 
			
		||||
      };
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // @ts-ignore
 | 
			
		||||
    esploraTransaction.vin = transaction.vin.map((vin) => {
 | 
			
		||||
      return {
 | 
			
		||||
        is_coinbase: !!vin.coinbase,
 | 
			
		||||
        prevout: null,
 | 
			
		||||
        scriptsig: vin.scriptSig && vin.scriptSig.hex || vin.coinbase || '',
 | 
			
		||||
        scriptsig_asm: vin.scriptSig && this.convertScriptSigAsm(vin.scriptSig.hex) || '',
 | 
			
		||||
        sequence: vin.sequence,
 | 
			
		||||
        txid: vin.txid || '',
 | 
			
		||||
        vout: vin.vout || 0,
 | 
			
		||||
        witness: vin.txinwitness,
 | 
			
		||||
      };
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (transaction.confirmations) {
 | 
			
		||||
      esploraTransaction.status = {
 | 
			
		||||
        confirmed: true,
 | 
			
		||||
        block_height: -1,
 | 
			
		||||
        block_hash: transaction.blockhash,
 | 
			
		||||
        block_time: transaction.blocktime,
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (addPrevout) {
 | 
			
		||||
      esploraTransaction = await this.$calculateFeeFromInputs(esploraTransaction, false, lazyPrevouts);
 | 
			
		||||
    } else if (!transaction.confirmations) {
 | 
			
		||||
      // esploraTransaction = await this.$appendMempoolFeeData(esploraTransaction);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return esploraTransaction;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private translateScriptPubKeyType(outputType: string): string {
 | 
			
		||||
    const map = {
 | 
			
		||||
      'pubkey': 'p2pk',
 | 
			
		||||
      'pubkeyhash': 'p2pkh',
 | 
			
		||||
      'scripthash': 'p2sh',
 | 
			
		||||
      'witness_v0_keyhash': 'v0_p2wpkh',
 | 
			
		||||
      'witness_v0_scripthash': 'v0_p2wsh',
 | 
			
		||||
      'witness_v1_taproot': 'v1_p2tr',
 | 
			
		||||
      'nonstandard': 'nonstandard',
 | 
			
		||||
      'multisig': 'multisig',
 | 
			
		||||
      'nulldata': 'op_return'
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    if (map[outputType]) {
 | 
			
		||||
      return map[outputType];
 | 
			
		||||
    } else {
 | 
			
		||||
      return 'unknown';
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $calculateFeeFromInputs(transaction: IEsploraApi.Transaction, addPrevout: boolean, lazyPrevouts: boolean): Promise<IEsploraApi.Transaction> {
 | 
			
		||||
    if (transaction.vin[0].is_coinbase) {
 | 
			
		||||
      transaction.fee = 0;
 | 
			
		||||
      return transaction;
 | 
			
		||||
    }
 | 
			
		||||
    let totalIn = 0;
 | 
			
		||||
 | 
			
		||||
    for (let i = 0; i < transaction.vin.length; i++) {
 | 
			
		||||
      if (lazyPrevouts && i > 12) {
 | 
			
		||||
        transaction.vin[i].lazy = true;
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
      const innerTx = await this.$getRawTransaction(transaction.vin[i].txid, false, false);
 | 
			
		||||
      transaction.vin[i].prevout = innerTx.vout[transaction.vin[i].vout];
 | 
			
		||||
      this.addInnerScriptsToVin(transaction.vin[i]);
 | 
			
		||||
      totalIn += innerTx.vout[transaction.vin[i].vout].value;
 | 
			
		||||
    }
 | 
			
		||||
    if (lazyPrevouts && transaction.vin.length > 12) {
 | 
			
		||||
      transaction.fee = -1;
 | 
			
		||||
    } else {
 | 
			
		||||
      const totalOut = transaction.vout.reduce((p, output) => p + output.value, 0);
 | 
			
		||||
      transaction.fee = parseFloat((totalIn - totalOut).toFixed(8));
 | 
			
		||||
    }
 | 
			
		||||
    return transaction;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private convertScriptSigAsm(hex: string): string {
 | 
			
		||||
    const buf = Buffer.from(hex, 'hex');
 | 
			
		||||
 | 
			
		||||
    const b: string[] = [];
 | 
			
		||||
 | 
			
		||||
    let i = 0;
 | 
			
		||||
    while (i < buf.length) {
 | 
			
		||||
      const op = buf[i];
 | 
			
		||||
      if (op >= 0x01 && op <= 0x4e) {
 | 
			
		||||
        i++;
 | 
			
		||||
        let push: number;
 | 
			
		||||
        if (op === 0x4c) {
 | 
			
		||||
          push = buf.readUInt8(i);
 | 
			
		||||
          b.push('OP_PUSHDATA1');
 | 
			
		||||
          i += 1;
 | 
			
		||||
        } else if (op === 0x4d) {
 | 
			
		||||
          push = buf.readUInt16LE(i);
 | 
			
		||||
          b.push('OP_PUSHDATA2');
 | 
			
		||||
          i += 2;
 | 
			
		||||
        } else if (op === 0x4e) {
 | 
			
		||||
          push = buf.readUInt32LE(i);
 | 
			
		||||
          b.push('OP_PUSHDATA4');
 | 
			
		||||
          i += 4;
 | 
			
		||||
        } else {
 | 
			
		||||
          push = op;
 | 
			
		||||
          b.push('OP_PUSHBYTES_' + push);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const data = buf.slice(i, i + push);
 | 
			
		||||
        if (data.length !== push) {
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        b.push(data.toString('hex'));
 | 
			
		||||
        i += data.length;
 | 
			
		||||
      } else {
 | 
			
		||||
        if (op === 0x00) {
 | 
			
		||||
          b.push('OP_0');
 | 
			
		||||
        } else if (op === 0x4f) {
 | 
			
		||||
          b.push('OP_PUSHNUM_NEG1');
 | 
			
		||||
        } else if (op === 0xb1) {
 | 
			
		||||
          b.push('OP_CLTV');
 | 
			
		||||
        } else if (op === 0xb2) {
 | 
			
		||||
          b.push('OP_CSV');
 | 
			
		||||
        } else if (op === 0xba) {
 | 
			
		||||
          b.push('OP_CHECKSIGADD');
 | 
			
		||||
        } else {
 | 
			
		||||
          const opcode = bitcoinjs.script.toASM([ op ]);
 | 
			
		||||
          if (opcode && op < 0xfd) {
 | 
			
		||||
            if (/^OP_(\d+)$/.test(opcode)) {
 | 
			
		||||
              b.push(opcode.replace(/^OP_(\d+)$/, 'OP_PUSHNUM_$1'));
 | 
			
		||||
            } else {
 | 
			
		||||
              b.push(opcode);
 | 
			
		||||
            }
 | 
			
		||||
          } else {
 | 
			
		||||
            b.push('OP_RETURN_' + op);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        i += 1;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return b.join(' ');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private addInnerScriptsToVin(vin: IEsploraApi.Vin): void {
 | 
			
		||||
    if (!vin.prevout) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (vin.prevout.scriptpubkey_type === 'p2sh') {
 | 
			
		||||
      const redeemScript = vin.scriptsig_asm.split(' ').reverse()[0];
 | 
			
		||||
      vin.inner_redeemscript_asm = this.convertScriptSigAsm(redeemScript);
 | 
			
		||||
      if (vin.witness && vin.witness.length > 2) {
 | 
			
		||||
        const witnessScript = vin.witness[vin.witness.length - 1];
 | 
			
		||||
        vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (vin.prevout.scriptpubkey_type === 'v0_p2wsh' && vin.witness) {
 | 
			
		||||
      const witnessScript = vin.witness[vin.witness.length - 1];
 | 
			
		||||
      vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (vin.prevout.scriptpubkey_type === 'v1_p2tr' && vin.witness && vin.witness.length > 1) {
 | 
			
		||||
      const witnessScript = vin.witness[vin.witness.length - 2];
 | 
			
		||||
      vin.inner_witnessscript_asm = this.convertScriptSigAsm(witnessScript);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default BitcoinApi;
 | 
			
		||||
@ -1,12 +0,0 @@
 | 
			
		||||
import config from '../../config';
 | 
			
		||||
const bitcoin = require('./rpc-api/index');
 | 
			
		||||
 | 
			
		||||
const nodeRpcCredentials: any = {
 | 
			
		||||
  host: config.CORE_RPC.HOST,
 | 
			
		||||
  port: config.CORE_RPC.PORT,
 | 
			
		||||
  user: config.CORE_RPC.USERNAME,
 | 
			
		||||
  pass: config.CORE_RPC.PASSWORD,
 | 
			
		||||
  timeout: 60000,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default new bitcoin.Client(nodeRpcCredentials);
 | 
			
		||||
@ -1,172 +0,0 @@
 | 
			
		||||
export namespace IEsploraApi {
 | 
			
		||||
  export interface Transaction {
 | 
			
		||||
    txid: string;
 | 
			
		||||
    version: number;
 | 
			
		||||
    locktime: number;
 | 
			
		||||
    size: number;
 | 
			
		||||
    weight: number;
 | 
			
		||||
    fee: number;
 | 
			
		||||
    vin: Vin[];
 | 
			
		||||
    vout: Vout[];
 | 
			
		||||
    status: Status;
 | 
			
		||||
    hex?: string;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface Recent {
 | 
			
		||||
    txid: string;
 | 
			
		||||
    fee: number;
 | 
			
		||||
    vsize: number;
 | 
			
		||||
    value: number;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface Vin {
 | 
			
		||||
    txid: string;
 | 
			
		||||
    vout: number;
 | 
			
		||||
    is_coinbase: boolean;
 | 
			
		||||
    scriptsig: string;
 | 
			
		||||
    scriptsig_asm: string;
 | 
			
		||||
    inner_redeemscript_asm: string;
 | 
			
		||||
    inner_witnessscript_asm: string;
 | 
			
		||||
    sequence: any;
 | 
			
		||||
    witness: string[];
 | 
			
		||||
    prevout: Vout | null;
 | 
			
		||||
    // Elements
 | 
			
		||||
    is_pegin?: boolean;
 | 
			
		||||
    issuance?: Issuance;
 | 
			
		||||
    // Custom
 | 
			
		||||
    lazy?: boolean;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  interface Issuance {
 | 
			
		||||
    asset_id: string;
 | 
			
		||||
    is_reissuance: string;
 | 
			
		||||
    asset_blinding_nonce: string;
 | 
			
		||||
    asset_entropy: string;
 | 
			
		||||
    contract_hash: string;
 | 
			
		||||
    assetamount?: number;
 | 
			
		||||
    assetamountcommitment?: string;
 | 
			
		||||
    tokenamount?: number;
 | 
			
		||||
    tokenamountcommitment?: string;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface Vout {
 | 
			
		||||
    scriptpubkey: string;
 | 
			
		||||
    scriptpubkey_asm: string;
 | 
			
		||||
    scriptpubkey_type: string;
 | 
			
		||||
    scriptpubkey_address: string;
 | 
			
		||||
    value: number;
 | 
			
		||||
    // Elements
 | 
			
		||||
    valuecommitment?: number;
 | 
			
		||||
    asset?: string;
 | 
			
		||||
    pegout?: Pegout;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  interface Pegout {
 | 
			
		||||
    genesis_hash: string;
 | 
			
		||||
    scriptpubkey: string;
 | 
			
		||||
    scriptpubkey_asm: string;
 | 
			
		||||
    scriptpubkey_address: string;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface Status {
 | 
			
		||||
    confirmed: boolean;
 | 
			
		||||
    block_height?: number;
 | 
			
		||||
    block_hash?: string;
 | 
			
		||||
    block_time?: number;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface Block {
 | 
			
		||||
    id: string;
 | 
			
		||||
    height: number;
 | 
			
		||||
    version: number;
 | 
			
		||||
    timestamp: number;
 | 
			
		||||
    bits: number;
 | 
			
		||||
    nonce: number;
 | 
			
		||||
    difficulty: number;
 | 
			
		||||
    merkle_root: string;
 | 
			
		||||
    tx_count: number;
 | 
			
		||||
    size: number;
 | 
			
		||||
    weight: number;
 | 
			
		||||
    previousblockhash: string;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface Address {
 | 
			
		||||
    address: string;
 | 
			
		||||
    chain_stats: ChainStats;
 | 
			
		||||
    mempool_stats: MempoolStats;
 | 
			
		||||
    electrum?: boolean;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface ChainStats {
 | 
			
		||||
    funded_txo_count: number;
 | 
			
		||||
    funded_txo_sum: number;
 | 
			
		||||
    spent_txo_count: number;
 | 
			
		||||
    spent_txo_sum: number;
 | 
			
		||||
    tx_count: number;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface MempoolStats {
 | 
			
		||||
    funded_txo_count: number;
 | 
			
		||||
    funded_txo_sum: number;
 | 
			
		||||
    spent_txo_count: number;
 | 
			
		||||
    spent_txo_sum: number;
 | 
			
		||||
    tx_count: number;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface Outspend {
 | 
			
		||||
    spent: boolean;
 | 
			
		||||
    txid?: string;
 | 
			
		||||
    vin?: number;
 | 
			
		||||
    status?: Status;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface Asset {
 | 
			
		||||
    asset_id: string;
 | 
			
		||||
    issuance_txin: IssuanceTxin;
 | 
			
		||||
    issuance_prevout: IssuancePrevout;
 | 
			
		||||
    reissuance_token: string;
 | 
			
		||||
    contract_hash: string;
 | 
			
		||||
    status: Status;
 | 
			
		||||
    chain_stats: AssetStats;
 | 
			
		||||
    mempool_stats: AssetStats;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface AssetExtended extends Asset {
 | 
			
		||||
    name: string;
 | 
			
		||||
    ticker: string;
 | 
			
		||||
    precision: number;
 | 
			
		||||
    entity: Entity;
 | 
			
		||||
    version: number;
 | 
			
		||||
    issuer_pubkey: string;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface Entity {
 | 
			
		||||
    domain: string;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  interface IssuanceTxin {
 | 
			
		||||
    txid: string;
 | 
			
		||||
    vin: number;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  interface IssuancePrevout {
 | 
			
		||||
    txid: string;
 | 
			
		||||
    vout: number;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  interface AssetStats {
 | 
			
		||||
    tx_count: number;
 | 
			
		||||
    issuance_count: number;
 | 
			
		||||
    issued_amount: number;
 | 
			
		||||
    burned_amount: number;
 | 
			
		||||
    has_blinded_issuances: boolean;
 | 
			
		||||
    reissuance_tokens: number;
 | 
			
		||||
    burned_reissuance_tokens: number;
 | 
			
		||||
    peg_in_count: number;
 | 
			
		||||
    peg_in_amount: number;
 | 
			
		||||
    peg_out_count: number;
 | 
			
		||||
    peg_out_amount: number;
 | 
			
		||||
    burn_count: number;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -1,84 +0,0 @@
 | 
			
		||||
import config from '../../config';
 | 
			
		||||
import axios, { AxiosRequestConfig } from 'axios';
 | 
			
		||||
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
 | 
			
		||||
import { IEsploraApi } from './esplora-api.interface';
 | 
			
		||||
 | 
			
		||||
class ElectrsApi implements AbstractBitcoinApi {
 | 
			
		||||
  axiosConfig: AxiosRequestConfig = {
 | 
			
		||||
    timeout: 10000,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  constructor() { }
 | 
			
		||||
 | 
			
		||||
  $getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]> {
 | 
			
		||||
    return axios.get<IEsploraApi.Transaction['txid'][]>(config.ESPLORA.REST_API_URL + '/mempool/txids', this.axiosConfig)
 | 
			
		||||
      .then((response) => response.data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getRawTransaction(txId: string): Promise<IEsploraApi.Transaction> {
 | 
			
		||||
    return axios.get<IEsploraApi.Transaction>(config.ESPLORA.REST_API_URL + '/tx/' + txId, this.axiosConfig)
 | 
			
		||||
      .then((response) => response.data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getBlockHeightTip(): Promise<number> {
 | 
			
		||||
    return axios.get<number>(config.ESPLORA.REST_API_URL + '/blocks/tip/height', this.axiosConfig)
 | 
			
		||||
      .then((response) => response.data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getTxIdsForBlock(hash: string): Promise<string[]> {
 | 
			
		||||
    return axios.get<string[]>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/txids', this.axiosConfig)
 | 
			
		||||
      .then((response) => response.data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getBlockHash(height: number): Promise<string> {
 | 
			
		||||
    return axios.get<string>(config.ESPLORA.REST_API_URL + '/block-height/' + height, this.axiosConfig)
 | 
			
		||||
      .then((response) => response.data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getBlockHeader(hash: string): Promise<string> {
 | 
			
		||||
    return axios.get<string>(config.ESPLORA.REST_API_URL + '/block/' + hash + '/header', this.axiosConfig)
 | 
			
		||||
      .then((response) => response.data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getBlock(hash: string): Promise<IEsploraApi.Block> {
 | 
			
		||||
    return axios.get<IEsploraApi.Block>(config.ESPLORA.REST_API_URL + '/block/' + hash, this.axiosConfig)
 | 
			
		||||
      .then((response) => response.data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getAddress(address: string): Promise<IEsploraApi.Address> {
 | 
			
		||||
    throw new Error('Method getAddress not implemented.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getAddressTransactions(address: string, txId?: string): Promise<IEsploraApi.Transaction[]> {
 | 
			
		||||
    throw new Error('Method getAddressTransactions not implemented.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getAddressPrefix(prefix: string): string[] {
 | 
			
		||||
    throw new Error('Method not implemented.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $sendRawTransaction(rawTransaction: string): Promise<string> {
 | 
			
		||||
    throw new Error('Method not implemented.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getOutspend(txId: string, vout: number): Promise<IEsploraApi.Outspend> {
 | 
			
		||||
    return axios.get<IEsploraApi.Outspend>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspend/' + vout, this.axiosConfig)
 | 
			
		||||
      .then((response) => response.data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getOutspends(txId: string): Promise<IEsploraApi.Outspend[]> {
 | 
			
		||||
    return axios.get<IEsploraApi.Outspend[]>(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspends', this.axiosConfig)
 | 
			
		||||
      .then((response) => response.data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async $getBatchedOutspends(txId: string[]): Promise<IEsploraApi.Outspend[][]> {
 | 
			
		||||
    const outspends: IEsploraApi.Outspend[][] = [];
 | 
			
		||||
    for (const tx of txId) {
 | 
			
		||||
      const outspend = await this.$getOutspends(tx);
 | 
			
		||||
      outspends.push(outspend);
 | 
			
		||||
    }
 | 
			
		||||
    return outspends;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ElectrsApi;
 | 
			
		||||
@ -1,92 +0,0 @@
 | 
			
		||||
module.exports = {
 | 
			
		||||
  addMultiSigAddress: 'addmultisigaddress',
 | 
			
		||||
  addNode: 'addnode', // bitcoind v0.8.0+
 | 
			
		||||
  backupWallet: 'backupwallet',
 | 
			
		||||
  createMultiSig: 'createmultisig',
 | 
			
		||||
  createRawTransaction: 'createrawtransaction', // bitcoind v0.7.0+
 | 
			
		||||
  decodeRawTransaction: 'decoderawtransaction', // bitcoind v0.7.0+
 | 
			
		||||
  decodeScript: 'decodescript',
 | 
			
		||||
  dumpPrivKey: 'dumpprivkey',
 | 
			
		||||
  dumpWallet: 'dumpwallet', // bitcoind v0.9.0+
 | 
			
		||||
  encryptWallet: 'encryptwallet',
 | 
			
		||||
  estimateFee: 'estimatefee', // bitcoind v0.10.0x
 | 
			
		||||
  estimatePriority: 'estimatepriority', // bitcoind v0.10.0+
 | 
			
		||||
  generate: 'generate', // bitcoind v0.11.0+
 | 
			
		||||
  getAccount: 'getaccount',
 | 
			
		||||
  getAccountAddress: 'getaccountaddress',
 | 
			
		||||
  getAddedNodeInfo: 'getaddednodeinfo', // bitcoind v0.8.0+
 | 
			
		||||
  getAddressesByAccount: 'getaddressesbyaccount',
 | 
			
		||||
  getBalance: 'getbalance',
 | 
			
		||||
  getBestBlockHash: 'getbestblockhash', // bitcoind v0.9.0+
 | 
			
		||||
  getBlock: 'getblock',
 | 
			
		||||
  getBlockStats: 'getblockstats',
 | 
			
		||||
  getBlockFilter: 'getblockfilter',
 | 
			
		||||
  getBlockchainInfo: 'getblockchaininfo', // bitcoind v0.9.2+
 | 
			
		||||
  getBlockCount: 'getblockcount',
 | 
			
		||||
  getBlockHash: 'getblockhash',
 | 
			
		||||
  getBlockHeader: 'getblockheader',
 | 
			
		||||
  getBlockTemplate: 'getblocktemplate', // bitcoind v0.7.0+
 | 
			
		||||
  getChainTips: 'getchaintips', // bitcoind v0.10.0+
 | 
			
		||||
  getChainTxStats: 'getchaintxstats',
 | 
			
		||||
  getConnectionCount: 'getconnectioncount',
 | 
			
		||||
  getDifficulty: 'getdifficulty',
 | 
			
		||||
  getGenerate: 'getgenerate',
 | 
			
		||||
  getInfo: 'getinfo',
 | 
			
		||||
  getMempoolAncestors: 'getmempoolancestors',
 | 
			
		||||
  getMempoolDescendants: 'getmempooldescendants',
 | 
			
		||||
  getMempoolEntry: 'getmempoolentry',
 | 
			
		||||
  getMempoolInfo: 'getmempoolinfo', // bitcoind v0.10+
 | 
			
		||||
  getMiningInfo: 'getmininginfo',
 | 
			
		||||
  getNetTotals: 'getnettotals',
 | 
			
		||||
  getNetworkInfo: 'getnetworkinfo', // bitcoind v0.9.2+
 | 
			
		||||
  getNetworkHashPs: 'getnetworkhashps', // bitcoind v0.9.0+
 | 
			
		||||
  getNewAddress: 'getnewaddress',
 | 
			
		||||
  getPeerInfo: 'getpeerinfo', // bitcoind v0.7.0+
 | 
			
		||||
  getRawChangeAddress: 'getrawchangeaddress', // bitcoin v0.9+
 | 
			
		||||
  getRawMemPool: 'getrawmempool', // bitcoind v0.7.0+
 | 
			
		||||
  getRawTransaction: 'getrawtransaction', // bitcoind v0.7.0+
 | 
			
		||||
  getReceivedByAccount: 'getreceivedbyaccount',
 | 
			
		||||
  getReceivedByAddress: 'getreceivedbyaddress',
 | 
			
		||||
  getTransaction: 'gettransaction',
 | 
			
		||||
  getTxOut: 'gettxout', // bitcoind v0.7.0+
 | 
			
		||||
  getTxOutProof: 'gettxoutproof', // bitcoind v0.11.0+
 | 
			
		||||
  getTxOutSetInfo: 'gettxoutsetinfo', // bitcoind v0.7.0+
 | 
			
		||||
  getUnconfirmedBalance: 'getunconfirmedbalance', // bitcoind v0.9.0+
 | 
			
		||||
  getWalletInfo: 'getwalletinfo', // bitcoind v0.9.2+
 | 
			
		||||
  help: 'help',
 | 
			
		||||
  importAddress: 'importaddress', // bitcoind v0.10.0+
 | 
			
		||||
  importPrivKey: 'importprivkey',
 | 
			
		||||
  importWallet: 'importwallet', // bitcoind v0.9.0+
 | 
			
		||||
  keypoolRefill: 'keypoolrefill',
 | 
			
		||||
  keyPoolRefill: 'keypoolrefill',
 | 
			
		||||
  listAccounts: 'listaccounts',
 | 
			
		||||
  listAddressGroupings: 'listaddressgroupings', // bitcoind v0.7.0+
 | 
			
		||||
  listLockUnspent: 'listlockunspent', // bitcoind v0.8.0+
 | 
			
		||||
  listReceivedByAccount: 'listreceivedbyaccount',
 | 
			
		||||
  listReceivedByAddress: 'listreceivedbyaddress',
 | 
			
		||||
  listSinceBlock: 'listsinceblock',
 | 
			
		||||
  listTransactions: 'listtransactions',
 | 
			
		||||
  listUnspent: 'listunspent', // bitcoind v0.7.0+
 | 
			
		||||
  lockUnspent: 'lockunspent', // bitcoind v0.8.0+
 | 
			
		||||
  move: 'move',
 | 
			
		||||
  ping: 'ping', // bitcoind v0.9.0+
 | 
			
		||||
  prioritiseTransaction: 'prioritisetransaction', // bitcoind v0.10.0+
 | 
			
		||||
  sendFrom: 'sendfrom',
 | 
			
		||||
  sendMany: 'sendmany',
 | 
			
		||||
  sendRawTransaction: 'sendrawtransaction', // bitcoind v0.7.0+
 | 
			
		||||
  sendToAddress: 'sendtoaddress',
 | 
			
		||||
  setAccount: 'setaccount',
 | 
			
		||||
  setGenerate: 'setgenerate',
 | 
			
		||||
  setTxFee: 'settxfee',
 | 
			
		||||
  signMessage: 'signmessage',
 | 
			
		||||
  signRawTransaction: 'signrawtransaction', // bitcoind v0.7.0+
 | 
			
		||||
  stop: 'stop',
 | 
			
		||||
  submitBlock: 'submitblock', // bitcoind v0.7.0+
 | 
			
		||||
  validateAddress: 'validateaddress',
 | 
			
		||||
  verifyChain: 'verifychain', // bitcoind v0.9.0+
 | 
			
		||||
  verifyMessage: 'verifymessage',
 | 
			
		||||
  verifyTxOutProof: 'verifytxoutproof', // bitcoind v0.11.0+
 | 
			
		||||
  walletLock: 'walletlock',
 | 
			
		||||
  walletPassphrase: 'walletpassphrase',
 | 
			
		||||
  walletPassphraseChange: 'walletpassphrasechange'
 | 
			
		||||
}
 | 
			
		||||
@ -1,61 +0,0 @@
 | 
			
		||||
var commands = require('./commands')
 | 
			
		||||
var rpc = require('./jsonrpc')
 | 
			
		||||
 | 
			
		||||
// ===----------------------------------------------------------------------===//
 | 
			
		||||
// JsonRPC
 | 
			
		||||
// ===----------------------------------------------------------------------===//
 | 
			
		||||
function Client (opts) {
 | 
			
		||||
  // @ts-ignore
 | 
			
		||||
  this.rpc = new rpc.JsonRPC(opts)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ===----------------------------------------------------------------------===//
 | 
			
		||||
// cmd
 | 
			
		||||
// ===----------------------------------------------------------------------===//
 | 
			
		||||
Client.prototype.cmd = function () {
 | 
			
		||||
  var args = [].slice.call(arguments)
 | 
			
		||||
  var cmd = args.shift()
 | 
			
		||||
 | 
			
		||||
  callRpc(cmd, args, this.rpc)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ===----------------------------------------------------------------------===//
 | 
			
		||||
// callRpc
 | 
			
		||||
// ===----------------------------------------------------------------------===//
 | 
			
		||||
function callRpc (cmd, args, rpc) {
 | 
			
		||||
  var fn = args[args.length - 1]
 | 
			
		||||
 | 
			
		||||
  // If the last argument is a callback, pop it from the args list
 | 
			
		||||
  if (typeof fn === 'function') {
 | 
			
		||||
    args.pop()
 | 
			
		||||
  } else {
 | 
			
		||||
    fn = function () {}
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return rpc.call(cmd, args, function () {
 | 
			
		||||
    var args = [].slice.call(arguments)
 | 
			
		||||
      // @ts-ignore
 | 
			
		||||
    args.unshift(null)
 | 
			
		||||
      // @ts-ignore
 | 
			
		||||
    fn.apply(this, args)
 | 
			
		||||
  }, function (err) {
 | 
			
		||||
    fn(err)
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ===----------------------------------------------------------------------===//
 | 
			
		||||
// Initialize wrappers
 | 
			
		||||
// ===----------------------------------------------------------------------===//
 | 
			
		||||
(function () {
 | 
			
		||||
  for (var protoFn in commands) {
 | 
			
		||||
    (function (protoFn) {
 | 
			
		||||
      Client.prototype[protoFn] = function () {
 | 
			
		||||
        var args = [].slice.call(arguments)
 | 
			
		||||
        return callRpc(commands[protoFn], args, this.rpc)
 | 
			
		||||
      }
 | 
			
		||||
    })(protoFn)
 | 
			
		||||
  }
 | 
			
		||||
})()
 | 
			
		||||
 | 
			
		||||
// Export!
 | 
			
		||||
module.exports.Client = Client;
 | 
			
		||||
@ -1,162 +0,0 @@
 | 
			
		||||
var http = require('http')
 | 
			
		||||
var https = require('https')
 | 
			
		||||
 | 
			
		||||
var JsonRPC = function (opts) {
 | 
			
		||||
  // @ts-ignore
 | 
			
		||||
  this.opts = opts || {}
 | 
			
		||||
  // @ts-ignore
 | 
			
		||||
  this.http = this.opts.ssl ? https : http
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
JsonRPC.prototype.call = function (method, params) {
 | 
			
		||||
  return new Promise((resolve, reject) => {
 | 
			
		||||
    var time = Date.now()
 | 
			
		||||
    var requestJSON
 | 
			
		||||
 | 
			
		||||
    if (Array.isArray(method)) {
 | 
			
		||||
      // multiple rpc batch call
 | 
			
		||||
      requestJSON = []
 | 
			
		||||
      method.forEach(function (batchCall, i) {
 | 
			
		||||
        requestJSON.push({
 | 
			
		||||
          id: time + '-' + i,
 | 
			
		||||
          method: batchCall.method,
 | 
			
		||||
          params: batchCall.params
 | 
			
		||||
        })
 | 
			
		||||
      })
 | 
			
		||||
    } else {
 | 
			
		||||
      // single rpc call
 | 
			
		||||
      requestJSON = {
 | 
			
		||||
        id: time,
 | 
			
		||||
        method: method,
 | 
			
		||||
        params: params
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // First we encode the request into JSON
 | 
			
		||||
    requestJSON = JSON.stringify(requestJSON)
 | 
			
		||||
 | 
			
		||||
    // prepare request options
 | 
			
		||||
    var requestOptions = {
 | 
			
		||||
      host: this.opts.host || 'localhost',
 | 
			
		||||
      port: this.opts.port || 8332,
 | 
			
		||||
      method: 'POST',
 | 
			
		||||
      path: '/',
 | 
			
		||||
      headers: {
 | 
			
		||||
        'Host': this.opts.host || 'localhost',
 | 
			
		||||
        'Content-Length': requestJSON.length
 | 
			
		||||
      },
 | 
			
		||||
      agent: false,
 | 
			
		||||
      rejectUnauthorized: this.opts.ssl && this.opts.sslStrict !== false
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this.opts.ssl && this.opts.sslCa) {
 | 
			
		||||
    // @ts-ignore 
 | 
			
		||||
      requestOptions.ca = this.opts.sslCa
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // use HTTP auth if user and password set
 | 
			
		||||
    if (this.opts.user && this.opts.pass) {
 | 
			
		||||
      // @ts-ignore
 | 
			
		||||
      requestOptions.auth = this.opts.user + ':' + this.opts.pass
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Now we'll make a request to the server
 | 
			
		||||
    var cbCalled = false
 | 
			
		||||
    var request = this.http.request(requestOptions)
 | 
			
		||||
 | 
			
		||||
    // start request timeout timer
 | 
			
		||||
    var reqTimeout = setTimeout(function () {
 | 
			
		||||
      if (cbCalled) return
 | 
			
		||||
      cbCalled = true
 | 
			
		||||
      request.abort()
 | 
			
		||||
      var err = new Error('ETIMEDOUT')
 | 
			
		||||
      // @ts-ignore
 | 
			
		||||
      err.code = 'ETIMEDOUT'
 | 
			
		||||
      reject(err)
 | 
			
		||||
    }, this.opts.timeout || 30000)
 | 
			
		||||
 | 
			
		||||
    // set additional timeout on socket in case of remote freeze after sending headers
 | 
			
		||||
    request.setTimeout(this.opts.timeout || 30000, function () {
 | 
			
		||||
      if (cbCalled) return
 | 
			
		||||
      cbCalled = true
 | 
			
		||||
      request.abort()
 | 
			
		||||
      var err = new Error('ESOCKETTIMEDOUT')
 | 
			
		||||
      // @ts-ignore
 | 
			
		||||
      err.code = 'ESOCKETTIMEDOUT'
 | 
			
		||||
      reject(err)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    request.on('error', function (err) {
 | 
			
		||||
      if (cbCalled) return
 | 
			
		||||
      cbCalled = true
 | 
			
		||||
      clearTimeout(reqTimeout)
 | 
			
		||||
      reject(err)
 | 
			
		||||
    })
 | 
			
		||||
 | 
			
		||||
    request.on('response', function (response) {
 | 
			
		||||
      clearTimeout(reqTimeout)
 | 
			
		||||
 | 
			
		||||
      // We need to buffer the response chunks in a nonblocking way.
 | 
			
		||||
      var buffer = ''
 | 
			
		||||
      response.on('data', function (chunk) {
 | 
			
		||||
        buffer = buffer + chunk
 | 
			
		||||
      })
 | 
			
		||||
      // When all the responses are finished, we decode the JSON and
 | 
			
		||||
      // depending on whether it's got a result or an error, we call
 | 
			
		||||
      // emitSuccess or emitError on the promise.
 | 
			
		||||
      response.on('end', function () {
 | 
			
		||||
        var err
 | 
			
		||||
 | 
			
		||||
        if (cbCalled) return
 | 
			
		||||
        cbCalled = true
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
          var decoded = JSON.parse(buffer)
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
          if (response.statusCode !== 200) {
 | 
			
		||||
            err = new Error('Invalid params, response status code: ' + response.statusCode)
 | 
			
		||||
            err.code = -32602
 | 
			
		||||
            reject(err)
 | 
			
		||||
          } else {
 | 
			
		||||
            err = new Error('Problem parsing JSON response from server')
 | 
			
		||||
            err.code = -32603
 | 
			
		||||
            reject(err)
 | 
			
		||||
          }
 | 
			
		||||
          return
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!Array.isArray(decoded)) {
 | 
			
		||||
          decoded = [decoded]
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // iterate over each response, normally there will be just one
 | 
			
		||||
        // unless a batch rpc call response is being processed
 | 
			
		||||
        decoded.forEach(function (decodedResponse, i) {
 | 
			
		||||
          if (decodedResponse.hasOwnProperty('error') && decodedResponse.error != null) {
 | 
			
		||||
            if (reject) {
 | 
			
		||||
              err = new Error(decodedResponse.error.message || '')
 | 
			
		||||
              if (decodedResponse.error.code) {
 | 
			
		||||
                err.code = decodedResponse.error.code
 | 
			
		||||
              }
 | 
			
		||||
              reject(err)
 | 
			
		||||
            }
 | 
			
		||||
          } else if (decodedResponse.hasOwnProperty('result')) {
 | 
			
		||||
            // @ts-ignore
 | 
			
		||||
            resolve(decodedResponse.result, response.headers)
 | 
			
		||||
          } else {
 | 
			
		||||
            if (reject) {
 | 
			
		||||
              err = new Error(decodedResponse.error.message || '')
 | 
			
		||||
              if (decodedResponse.error.code) {
 | 
			
		||||
                err.code = decodedResponse.error.code
 | 
			
		||||
              }
 | 
			
		||||
              reject(err)
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
      })
 | 
			
		||||
    })
 | 
			
		||||
    request.end(requestJSON);
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
module.exports.JsonRPC = JsonRPC
 | 
			
		||||
@ -1,17 +0,0 @@
 | 
			
		||||
import logger from '../../logger';
 | 
			
		||||
import DB from '../../database';
 | 
			
		||||
 | 
			
		||||
class StatisticsApi {
 | 
			
		||||
  public async $getStatistics(): Promise<any> {
 | 
			
		||||
    try {
 | 
			
		||||
      const query = `SELECT UNIX_TIMESTAMP(added) AS added, channel_count, node_count, total_capacity FROM statistics ORDER BY id DESC`;
 | 
			
		||||
      const [rows]: any = await DB.query(query);
 | 
			
		||||
      return rows;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err('$getStatistics error: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new StatisticsApi();
 | 
			
		||||
@ -1,110 +0,0 @@
 | 
			
		||||
const configFile = require('../mempool-config.json');
 | 
			
		||||
 | 
			
		||||
interface IConfig {
 | 
			
		||||
  MEMPOOL: {
 | 
			
		||||
    NETWORK: 'mainnet' | 'testnet' | 'signet';
 | 
			
		||||
    BACKEND: 'lnd' | 'cln' | 'ldk';
 | 
			
		||||
    HTTP_PORT: number;
 | 
			
		||||
    API_URL_PREFIX: string;
 | 
			
		||||
    STDOUT_LOG_MIN_PRIORITY: 'emerg' | 'alert' | 'crit' | 'err' | 'warn' | 'notice' | 'info' | 'debug';
 | 
			
		||||
  };
 | 
			
		||||
  ESPLORA: {
 | 
			
		||||
    REST_API_URL: string;
 | 
			
		||||
  };
 | 
			
		||||
  SYSLOG: {
 | 
			
		||||
    ENABLED: boolean;
 | 
			
		||||
    HOST: string;
 | 
			
		||||
    PORT: number;
 | 
			
		||||
    MIN_PRIORITY: 'emerg' | 'alert' | 'crit' | 'err' | 'warn' | 'notice' | 'info' | 'debug';
 | 
			
		||||
    FACILITY: string;
 | 
			
		||||
  };
 | 
			
		||||
  LN_NODE_AUTH: {
 | 
			
		||||
    TLS_CERT_PATH: string;
 | 
			
		||||
    MACAROON_PATH: string;
 | 
			
		||||
    SOCKET: string;
 | 
			
		||||
  };
 | 
			
		||||
  CORE_RPC: {
 | 
			
		||||
    HOST: string;
 | 
			
		||||
    PORT: number;
 | 
			
		||||
    USERNAME: string;
 | 
			
		||||
    PASSWORD: string;
 | 
			
		||||
  };
 | 
			
		||||
  DATABASE: {
 | 
			
		||||
    HOST: string,
 | 
			
		||||
    SOCKET: string,
 | 
			
		||||
    PORT: number;
 | 
			
		||||
    DATABASE: string;
 | 
			
		||||
    USERNAME: string;
 | 
			
		||||
    PASSWORD: string;
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const defaults: IConfig = {
 | 
			
		||||
  'MEMPOOL': {
 | 
			
		||||
    'NETWORK': 'mainnet',
 | 
			
		||||
    'BACKEND': 'lnd',
 | 
			
		||||
    'HTTP_PORT': 8999,
 | 
			
		||||
    'API_URL_PREFIX': '/api/v1/',
 | 
			
		||||
    'STDOUT_LOG_MIN_PRIORITY': 'debug',
 | 
			
		||||
  },
 | 
			
		||||
  'ESPLORA': {
 | 
			
		||||
    'REST_API_URL': 'http://127.0.0.1:3000',
 | 
			
		||||
  },
 | 
			
		||||
  'SYSLOG': {
 | 
			
		||||
    'ENABLED': true,
 | 
			
		||||
    'HOST': '127.0.0.1',
 | 
			
		||||
    'PORT': 514,
 | 
			
		||||
    'MIN_PRIORITY': 'info',
 | 
			
		||||
    'FACILITY': 'local7'
 | 
			
		||||
  },
 | 
			
		||||
  'LN_NODE_AUTH': {
 | 
			
		||||
    'TLS_CERT_PATH': '',
 | 
			
		||||
    'MACAROON_PATH': '',
 | 
			
		||||
    'SOCKET': 'localhost:10009',
 | 
			
		||||
  },
 | 
			
		||||
  'CORE_RPC': {
 | 
			
		||||
    'HOST': '127.0.0.1',
 | 
			
		||||
    'PORT': 8332,
 | 
			
		||||
    'USERNAME': 'mempool',
 | 
			
		||||
    'PASSWORD': 'mempool'
 | 
			
		||||
  },
 | 
			
		||||
  'DATABASE': {
 | 
			
		||||
    'HOST': '127.0.0.1',
 | 
			
		||||
    'SOCKET': '',
 | 
			
		||||
    'PORT': 3306,
 | 
			
		||||
    'DATABASE': 'mempool',
 | 
			
		||||
    'USERNAME': 'mempool',
 | 
			
		||||
    'PASSWORD': 'mempool'
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class Config implements IConfig {
 | 
			
		||||
  MEMPOOL: IConfig['MEMPOOL'];
 | 
			
		||||
  ESPLORA: IConfig['ESPLORA'];
 | 
			
		||||
  SYSLOG: IConfig['SYSLOG'];
 | 
			
		||||
  LN_NODE_AUTH: IConfig['LN_NODE_AUTH'];
 | 
			
		||||
  CORE_RPC: IConfig['CORE_RPC'];
 | 
			
		||||
  DATABASE: IConfig['DATABASE'];
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
    const configs = this.merge(configFile, defaults);
 | 
			
		||||
    this.MEMPOOL = configs.MEMPOOL;
 | 
			
		||||
    this.ESPLORA = configs.ESPLORA;
 | 
			
		||||
    this.SYSLOG = configs.SYSLOG;
 | 
			
		||||
    this.LN_NODE_AUTH = configs.LN_NODE_AUTH;
 | 
			
		||||
    this.CORE_RPC = configs.CORE_RPC;
 | 
			
		||||
    this.DATABASE = configs.DATABASE;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  merge = (...objects: object[]): IConfig => {
 | 
			
		||||
    // @ts-ignore
 | 
			
		||||
    return objects.reduce((prev, next) => {
 | 
			
		||||
      Object.keys(prev).forEach(key => {
 | 
			
		||||
        next[key] = { ...next[key], ...prev[key] };
 | 
			
		||||
      });
 | 
			
		||||
      return next;
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new Config();
 | 
			
		||||
@ -1,260 +0,0 @@
 | 
			
		||||
import config from './config';
 | 
			
		||||
import DB from './database';
 | 
			
		||||
import logger from './logger';
 | 
			
		||||
 | 
			
		||||
const sleep = (ms: number) => new Promise(res => setTimeout(res, ms));
 | 
			
		||||
 | 
			
		||||
class DatabaseMigration {
 | 
			
		||||
  private static currentVersion = 1;
 | 
			
		||||
  private queryTimeout = 120000;
 | 
			
		||||
 | 
			
		||||
  constructor() { }
 | 
			
		||||
  /**
 | 
			
		||||
   * Entry point
 | 
			
		||||
   */
 | 
			
		||||
  public async $initializeOrMigrateDatabase(): Promise<void> {
 | 
			
		||||
    logger.debug('MIGRATIONS: Running migrations');
 | 
			
		||||
 | 
			
		||||
    await this.$printDatabaseVersion();
 | 
			
		||||
 | 
			
		||||
    // First of all, if the `state` database does not exist, create it so we can track migration version
 | 
			
		||||
    if (!await this.$checkIfTableExists('state')) {
 | 
			
		||||
      logger.debug('MIGRATIONS: `state` table does not exist. Creating it.');
 | 
			
		||||
      try {
 | 
			
		||||
        await this.$createMigrationStateTable();
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        logger.err('MIGRATIONS: Unable to create `state` table, aborting in 10 seconds. ' + e);
 | 
			
		||||
        await sleep(10000);
 | 
			
		||||
        process.exit(-1);
 | 
			
		||||
      }
 | 
			
		||||
      logger.debug('MIGRATIONS: `state` table initialized.');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let databaseSchemaVersion = 0;
 | 
			
		||||
    try {
 | 
			
		||||
      databaseSchemaVersion = await this.$getSchemaVersionFromDatabase();
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err('MIGRATIONS: Unable to get current database migration version, aborting in 10 seconds. ' + e);
 | 
			
		||||
      await sleep(10000);
 | 
			
		||||
      process.exit(-1);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    logger.debug('MIGRATIONS: Current state.schema_version ' + databaseSchemaVersion);
 | 
			
		||||
    logger.debug('MIGRATIONS: Latest DatabaseMigration.version is ' + DatabaseMigration.currentVersion);
 | 
			
		||||
    if (databaseSchemaVersion >= DatabaseMigration.currentVersion) {
 | 
			
		||||
      logger.debug('MIGRATIONS: Nothing to do.');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Now, create missing tables. Those queries cannot be wrapped into a transaction unfortunately
 | 
			
		||||
    try {
 | 
			
		||||
      await this.$createMissingTablesAndIndexes(databaseSchemaVersion);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err('MIGRATIONS: Unable to create required tables, aborting in 10 seconds. ' + e);
 | 
			
		||||
      await sleep(10000);
 | 
			
		||||
      process.exit(-1);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (DatabaseMigration.currentVersion > databaseSchemaVersion) {
 | 
			
		||||
      logger.notice('MIGRATIONS: Upgrading datababse schema');
 | 
			
		||||
      try {
 | 
			
		||||
        await this.$migrateTableSchemaFromVersion(databaseSchemaVersion);
 | 
			
		||||
        logger.notice(`MIGRATIONS: OK. Database schema have been migrated from version ${databaseSchemaVersion} to ${DatabaseMigration.currentVersion} (latest version)`);
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        logger.err('MIGRATIONS: Unable to migrate database, aborting. ' + e);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Create all missing tables
 | 
			
		||||
   */
 | 
			
		||||
  private async $createMissingTablesAndIndexes(databaseSchemaVersion: number) {
 | 
			
		||||
    try {
 | 
			
		||||
      await this.$executeQuery(this.getCreateStatisticsQuery(), await this.$checkIfTableExists('statistics'));
 | 
			
		||||
      await this.$executeQuery(this.getCreateNodesQuery(), await this.$checkIfTableExists('nodes'));
 | 
			
		||||
      await this.$executeQuery(this.getCreateChannelsQuery(), await this.$checkIfTableExists('channels'));
 | 
			
		||||
      await this.$executeQuery(this.getCreateNodesStatsQuery(), await this.$checkIfTableExists('node_stats'));
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Small query execution wrapper to log all executed queries
 | 
			
		||||
   */
 | 
			
		||||
  private async $executeQuery(query: string, silent: boolean = false): Promise<any> {
 | 
			
		||||
    if (!silent) {
 | 
			
		||||
      logger.debug('MIGRATIONS: Execute query:\n' + query);
 | 
			
		||||
    }
 | 
			
		||||
    return DB.query({ sql: query, timeout: this.queryTimeout });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Check if 'table' exists in the database
 | 
			
		||||
   */
 | 
			
		||||
  private async $checkIfTableExists(table: string): Promise<boolean> {
 | 
			
		||||
    const query = `SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = '${config.DATABASE.DATABASE}' AND TABLE_NAME = '${table}'`;
 | 
			
		||||
    const [rows] = await DB.query({ sql: query, timeout: this.queryTimeout });
 | 
			
		||||
    return rows[0]['COUNT(*)'] === 1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get current database version
 | 
			
		||||
   */
 | 
			
		||||
  private async $getSchemaVersionFromDatabase(): Promise<number> {
 | 
			
		||||
    const query = `SELECT number FROM state WHERE name = 'schema_version';`;
 | 
			
		||||
    const [rows] = await this.$executeQuery(query, true);
 | 
			
		||||
    return rows[0]['number'];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Create the `state` table
 | 
			
		||||
   */
 | 
			
		||||
  private async $createMigrationStateTable(): Promise<void> {
 | 
			
		||||
    try {
 | 
			
		||||
      const query = `CREATE TABLE IF NOT EXISTS state (
 | 
			
		||||
        name varchar(25) NOT NULL,
 | 
			
		||||
        number int(11) NULL,
 | 
			
		||||
        string varchar(100) NULL,
 | 
			
		||||
        CONSTRAINT name_unique UNIQUE (name)
 | 
			
		||||
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
 | 
			
		||||
      await this.$executeQuery(query);
 | 
			
		||||
 | 
			
		||||
      // Set initial values
 | 
			
		||||
      await this.$executeQuery(`INSERT INTO state VALUES('schema_version', 0, NULL);`);
 | 
			
		||||
      await this.$executeQuery(`INSERT INTO state VALUES('last_node_stats', 0, '1970-01-01');`);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * We actually execute the migrations queries here
 | 
			
		||||
   */
 | 
			
		||||
  private async $migrateTableSchemaFromVersion(version: number): Promise<void> {
 | 
			
		||||
    const transactionQueries: string[] = [];
 | 
			
		||||
    for (const query of this.getMigrationQueriesFromVersion(version)) {
 | 
			
		||||
      transactionQueries.push(query);
 | 
			
		||||
    }
 | 
			
		||||
    transactionQueries.push(this.getUpdateToLatestSchemaVersionQuery());
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      await this.$executeQuery('START TRANSACTION;');
 | 
			
		||||
      for (const query of transactionQueries) {
 | 
			
		||||
        await this.$executeQuery(query);
 | 
			
		||||
      }
 | 
			
		||||
      await this.$executeQuery('COMMIT;');
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      await this.$executeQuery('ROLLBACK;');
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Generate migration queries based on schema version
 | 
			
		||||
   */
 | 
			
		||||
  private getMigrationQueriesFromVersion(version: number): string[] {
 | 
			
		||||
    const queries: string[] = [];
 | 
			
		||||
    return queries;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Save the schema version in the database
 | 
			
		||||
   */
 | 
			
		||||
  private getUpdateToLatestSchemaVersionQuery(): string {
 | 
			
		||||
    return `UPDATE state SET number = ${DatabaseMigration.currentVersion} WHERE name = 'schema_version';`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Print current database version
 | 
			
		||||
   */
 | 
			
		||||
  private async $printDatabaseVersion() {
 | 
			
		||||
    try {
 | 
			
		||||
      const [rows] = await this.$executeQuery('SELECT VERSION() as version;', true);
 | 
			
		||||
      logger.debug(`MIGRATIONS: Database engine version '${rows[0].version}'`);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.debug(`MIGRATIONS: Could not fetch database engine version. ` + e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private getCreateStatisticsQuery(): string {
 | 
			
		||||
    return `CREATE TABLE IF NOT EXISTS statistics (
 | 
			
		||||
      id int(11) NOT NULL AUTO_INCREMENT,
 | 
			
		||||
      added datetime NOT NULL,
 | 
			
		||||
      channel_count int(11) NOT NULL,
 | 
			
		||||
      node_count int(11) NOT NULL,
 | 
			
		||||
      total_capacity double unsigned NOT NULL,
 | 
			
		||||
      PRIMARY KEY (id)
 | 
			
		||||
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private getCreateNodesQuery(): string {
 | 
			
		||||
    return `CREATE TABLE IF NOT EXISTS nodes (
 | 
			
		||||
      public_key varchar(66) NOT NULL,
 | 
			
		||||
      first_seen datetime NOT NULL,
 | 
			
		||||
      updated_at datetime NOT NULL,
 | 
			
		||||
      alias varchar(200) CHARACTER SET utf8mb4 NOT NULL,
 | 
			
		||||
      color varchar(200) NOT NULL,
 | 
			
		||||
      sockets text DEFAULT NULL,
 | 
			
		||||
      PRIMARY KEY (public_key),
 | 
			
		||||
      KEY alias (alias(10))
 | 
			
		||||
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private getCreateChannelsQuery(): string {
 | 
			
		||||
    return `CREATE TABLE IF NOT EXISTS channels (
 | 
			
		||||
      id bigint(11) unsigned NOT NULL,
 | 
			
		||||
      short_id varchar(15) NOT NULL DEFAULT '',
 | 
			
		||||
      capacity bigint(20) unsigned NOT NULL,
 | 
			
		||||
      transaction_id varchar(64) NOT NULL,
 | 
			
		||||
      transaction_vout int(11) NOT NULL,
 | 
			
		||||
      updated_at datetime DEFAULT NULL,
 | 
			
		||||
      created datetime DEFAULT NULL,
 | 
			
		||||
      status int(11) NOT NULL DEFAULT 0,
 | 
			
		||||
      closing_transaction_id varchar(64) DEFAULT NULL,
 | 
			
		||||
      closing_date datetime DEFAULT NULL,
 | 
			
		||||
      closing_reason int(11) DEFAULT NULL,
 | 
			
		||||
      node1_public_key varchar(66) NOT NULL,
 | 
			
		||||
      node1_base_fee_mtokens bigint(20) unsigned DEFAULT NULL,
 | 
			
		||||
      node1_cltv_delta int(11) DEFAULT NULL,
 | 
			
		||||
      node1_fee_rate bigint(11) DEFAULT NULL,
 | 
			
		||||
      node1_is_disabled tinyint(1) DEFAULT NULL,
 | 
			
		||||
      node1_max_htlc_mtokens bigint(20) unsigned DEFAULT NULL,
 | 
			
		||||
      node1_min_htlc_mtokens bigint(20) DEFAULT NULL,
 | 
			
		||||
      node1_updated_at datetime DEFAULT NULL,
 | 
			
		||||
      node2_public_key varchar(66) NOT NULL,
 | 
			
		||||
      node2_base_fee_mtokens bigint(20) unsigned DEFAULT NULL,
 | 
			
		||||
      node2_cltv_delta int(11) DEFAULT NULL,
 | 
			
		||||
      node2_fee_rate bigint(11) DEFAULT NULL,
 | 
			
		||||
      node2_is_disabled tinyint(1) DEFAULT NULL,
 | 
			
		||||
      node2_max_htlc_mtokens bigint(20) unsigned DEFAULT NULL,
 | 
			
		||||
      node2_min_htlc_mtokens bigint(20) unsigned DEFAULT NULL,
 | 
			
		||||
      node2_updated_at datetime DEFAULT NULL,
 | 
			
		||||
      PRIMARY KEY (id),
 | 
			
		||||
      KEY node1_public_key (node1_public_key),
 | 
			
		||||
      KEY node2_public_key (node2_public_key),
 | 
			
		||||
      KEY status (status),
 | 
			
		||||
      KEY short_id (short_id),
 | 
			
		||||
      KEY transaction_id (transaction_id),
 | 
			
		||||
      KEY closing_transaction_id (closing_transaction_id)
 | 
			
		||||
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private getCreateNodesStatsQuery(): string {
 | 
			
		||||
    return `CREATE TABLE IF NOT EXISTS node_stats (
 | 
			
		||||
      id int(11) unsigned NOT NULL AUTO_INCREMENT,
 | 
			
		||||
      public_key varchar(66) NOT NULL DEFAULT '',
 | 
			
		||||
      added date NOT NULL,
 | 
			
		||||
      capacity bigint(20) unsigned NOT NULL DEFAULT 0,
 | 
			
		||||
      channels int(11) unsigned NOT NULL DEFAULT 0,
 | 
			
		||||
      PRIMARY KEY (id),
 | 
			
		||||
      UNIQUE KEY added (added,public_key),
 | 
			
		||||
      KEY public_key (public_key)
 | 
			
		||||
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;`;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new DatabaseMigration();
 | 
			
		||||
@ -1,51 +0,0 @@
 | 
			
		||||
import config from './config';
 | 
			
		||||
import { createPool, Pool, PoolConnection } from 'mysql2/promise';
 | 
			
		||||
import logger from './logger';
 | 
			
		||||
import { PoolOptions } from 'mysql2/typings/mysql';
 | 
			
		||||
 | 
			
		||||
 class DB {
 | 
			
		||||
  constructor() {
 | 
			
		||||
    if (config.DATABASE.SOCKET !== '') {
 | 
			
		||||
      this.poolConfig.socketPath = config.DATABASE.SOCKET;
 | 
			
		||||
    } else {
 | 
			
		||||
      this.poolConfig.host = config.DATABASE.HOST;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  private pool: Pool | null = null;
 | 
			
		||||
  private poolConfig: PoolOptions = {
 | 
			
		||||
    port: config.DATABASE.PORT,
 | 
			
		||||
    database: config.DATABASE.DATABASE,
 | 
			
		||||
    user: config.DATABASE.USERNAME,
 | 
			
		||||
    password: config.DATABASE.PASSWORD,
 | 
			
		||||
    connectionLimit: 10,
 | 
			
		||||
    supportBigNumbers: true,
 | 
			
		||||
    timezone: '+00:00',
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  public async query(query, params?) {
 | 
			
		||||
    const pool = await this.getPool();
 | 
			
		||||
    return pool.query(query, params);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async checkDbConnection() {
 | 
			
		||||
    try {
 | 
			
		||||
      await this.query('SELECT ?', [1]);
 | 
			
		||||
      logger.info('Database connection established.');
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.err('Could not connect to database: ' + (e instanceof Error ? e.message : e));
 | 
			
		||||
      process.exit(1);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async getPool(): Promise<Pool> {
 | 
			
		||||
    if (this.pool === null) {
 | 
			
		||||
      this.pool = createPool(this.poolConfig);
 | 
			
		||||
      this.pool.on('connection', function (newConnection: PoolConnection) {
 | 
			
		||||
        newConnection.query(`SET time_zone='+00:00'`);
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    return this.pool;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new DB();
 | 
			
		||||
@ -1,23 +0,0 @@
 | 
			
		||||
import DB from './database';
 | 
			
		||||
import databaseMigration from './database-migration';
 | 
			
		||||
import statsUpdater from './tasks/stats-updater.service';
 | 
			
		||||
import nodeSyncService from './tasks/node-sync.service';
 | 
			
		||||
import server from './server';
 | 
			
		||||
 | 
			
		||||
class LightningServer {
 | 
			
		||||
  constructor() {
 | 
			
		||||
    this.init();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async init() {
 | 
			
		||||
    await DB.checkDbConnection();
 | 
			
		||||
    await databaseMigration.$initializeOrMigrateDatabase();
 | 
			
		||||
 | 
			
		||||
    nodeSyncService.$startService();
 | 
			
		||||
    statsUpdater.$startService();
 | 
			
		||||
 | 
			
		||||
    server.startServer();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const lightningServer = new LightningServer();
 | 
			
		||||
@ -1,145 +0,0 @@
 | 
			
		||||
import config from './config';
 | 
			
		||||
import * as dgram from 'dgram';
 | 
			
		||||
 | 
			
		||||
class Logger {
 | 
			
		||||
  static priorities = {
 | 
			
		||||
    emerg: 0,
 | 
			
		||||
    alert: 1,
 | 
			
		||||
    crit: 2,
 | 
			
		||||
    err: 3,
 | 
			
		||||
    warn: 4,
 | 
			
		||||
    notice: 5,
 | 
			
		||||
    info: 6,
 | 
			
		||||
    debug: 7
 | 
			
		||||
  };
 | 
			
		||||
  static facilities = {
 | 
			
		||||
    kern: 0,
 | 
			
		||||
    user: 1,
 | 
			
		||||
    mail: 2,
 | 
			
		||||
    daemon: 3,
 | 
			
		||||
    auth: 4,
 | 
			
		||||
    syslog: 5,
 | 
			
		||||
    lpr: 6,
 | 
			
		||||
    news: 7,
 | 
			
		||||
    uucp: 8,
 | 
			
		||||
    local0: 16,
 | 
			
		||||
    local1: 17,
 | 
			
		||||
    local2: 18,
 | 
			
		||||
    local3: 19,
 | 
			
		||||
    local4: 20,
 | 
			
		||||
    local5: 21,
 | 
			
		||||
    local6: 22,
 | 
			
		||||
    local7: 23
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // @ts-ignore
 | 
			
		||||
  public emerg: ((msg: string) => void);
 | 
			
		||||
  // @ts-ignore
 | 
			
		||||
  public alert: ((msg: string) => void);
 | 
			
		||||
  // @ts-ignore
 | 
			
		||||
  public crit: ((msg: string) => void);
 | 
			
		||||
  // @ts-ignore
 | 
			
		||||
  public err: ((msg: string) => void);
 | 
			
		||||
  // @ts-ignore
 | 
			
		||||
  public warn: ((msg: string) => void);
 | 
			
		||||
  // @ts-ignore
 | 
			
		||||
  public notice: ((msg: string) => void);
 | 
			
		||||
  // @ts-ignore
 | 
			
		||||
  public info: ((msg: string) => void);
 | 
			
		||||
  // @ts-ignore
 | 
			
		||||
  public debug: ((msg: string) => void);
 | 
			
		||||
 | 
			
		||||
  private name = 'mempool';
 | 
			
		||||
  private client: dgram.Socket;
 | 
			
		||||
  private network: string;
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
    let prio;
 | 
			
		||||
    for (prio in Logger.priorities) {
 | 
			
		||||
      if (true) {
 | 
			
		||||
        this.addprio(prio);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    this.client = dgram.createSocket('udp4');
 | 
			
		||||
    this.network = this.getNetwork();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private addprio(prio): void {
 | 
			
		||||
    this[prio] = (function(_this) {
 | 
			
		||||
      return function(msg) {
 | 
			
		||||
        return _this.msg(prio, msg);
 | 
			
		||||
      };
 | 
			
		||||
    })(this);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private getNetwork(): string {
 | 
			
		||||
    if (config.MEMPOOL.NETWORK && config.MEMPOOL.NETWORK !== 'mainnet') {
 | 
			
		||||
      return config.MEMPOOL.NETWORK;
 | 
			
		||||
    }
 | 
			
		||||
    return '';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private msg(priority, msg) {
 | 
			
		||||
    let consolemsg, prionum, syslogmsg;
 | 
			
		||||
    if (typeof msg === 'string' && msg.length > 0) {
 | 
			
		||||
      while (msg[msg.length - 1].charCodeAt(0) === 10) {
 | 
			
		||||
        msg = msg.slice(0, msg.length - 1);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    const network = this.network ? ' <' + this.network + '>' : '';
 | 
			
		||||
    prionum = Logger.priorities[priority] || Logger.priorities.info;
 | 
			
		||||
    consolemsg = `${this.ts()} [${process.pid}] ${priority.toUpperCase()}:${network} ${msg}`;
 | 
			
		||||
 | 
			
		||||
    if (config.SYSLOG.ENABLED && Logger.priorities[priority] <= Logger.priorities[config.SYSLOG.MIN_PRIORITY]) {
 | 
			
		||||
      syslogmsg = `<${(Logger.facilities[config.SYSLOG.FACILITY] * 8 + prionum)}> ${this.name}[${process.pid}]: ${priority.toUpperCase()}${network} ${msg}`;
 | 
			
		||||
      this.syslog(syslogmsg);
 | 
			
		||||
    }
 | 
			
		||||
    if (Logger.priorities[priority] > Logger.priorities[config.MEMPOOL.STDOUT_LOG_MIN_PRIORITY]) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (priority === 'warning') {
 | 
			
		||||
      priority = 'warn';
 | 
			
		||||
    }
 | 
			
		||||
    if (priority === 'debug') {
 | 
			
		||||
      priority = 'info';
 | 
			
		||||
    }
 | 
			
		||||
    if (priority === 'err') {
 | 
			
		||||
      priority = 'error';
 | 
			
		||||
    }
 | 
			
		||||
    return (console[priority] || console.error)(consolemsg);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private syslog(msg) {
 | 
			
		||||
    let msgbuf;
 | 
			
		||||
    msgbuf = Buffer.from(msg);
 | 
			
		||||
    this.client.send(msgbuf, 0, msgbuf.length, config.SYSLOG.PORT, config.SYSLOG.HOST, function(err, bytes) {
 | 
			
		||||
      if (err) {
 | 
			
		||||
        console.log(err);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private leadZero(n: number): number | string {
 | 
			
		||||
    if (n < 10) {
 | 
			
		||||
      return '0' + n;
 | 
			
		||||
    }
 | 
			
		||||
    return n;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private ts() {
 | 
			
		||||
    let day, dt, hours, minutes, month, months, seconds;
 | 
			
		||||
    dt = new Date();
 | 
			
		||||
    hours = this.leadZero(dt.getHours());
 | 
			
		||||
    minutes = this.leadZero(dt.getMinutes());
 | 
			
		||||
    seconds = this.leadZero(dt.getSeconds());
 | 
			
		||||
    month = dt.getMonth();
 | 
			
		||||
    day = dt.getDate();
 | 
			
		||||
    if (day < 10) {
 | 
			
		||||
      day = ' ' + day;
 | 
			
		||||
    }
 | 
			
		||||
    months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
 | 
			
		||||
    return months[month] + ' ' + day + ' ' + hours + ':' + minutes + ':' + seconds;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new Logger();
 | 
			
		||||
@ -1,40 +0,0 @@
 | 
			
		||||
import { Express, Request, Response, NextFunction } from 'express';
 | 
			
		||||
import * as express from 'express';
 | 
			
		||||
import * as http from 'http';
 | 
			
		||||
import logger from './logger';
 | 
			
		||||
import config from './config';
 | 
			
		||||
import generalRoutes from './api/explorer/general.routes';
 | 
			
		||||
import nodesRoutes from './api/explorer/nodes.routes';
 | 
			
		||||
import channelsRoutes from './api/explorer/channels.routes';
 | 
			
		||||
 | 
			
		||||
class Server {
 | 
			
		||||
  private server: http.Server | undefined;
 | 
			
		||||
  private app: Express = express();
 | 
			
		||||
 | 
			
		||||
  public startServer() {
 | 
			
		||||
    this.app
 | 
			
		||||
      .use((req: Request, res: Response, next: NextFunction) => {
 | 
			
		||||
        res.setHeader('Access-Control-Allow-Origin', '*');
 | 
			
		||||
        next();
 | 
			
		||||
      })
 | 
			
		||||
      .use(express.urlencoded({ extended: true }))
 | 
			
		||||
      .use(express.text())
 | 
			
		||||
    ;
 | 
			
		||||
 | 
			
		||||
    this.server = http.createServer(this.app);
 | 
			
		||||
 | 
			
		||||
    this.server.listen(config.MEMPOOL.HTTP_PORT, () => {
 | 
			
		||||
      logger.notice(`Mempool Lightning is running on port ${config.MEMPOOL.HTTP_PORT}`);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.initRoutes();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private initRoutes() {
 | 
			
		||||
    generalRoutes.initRoutes(this.app);
 | 
			
		||||
    nodesRoutes.initRoutes(this.app);
 | 
			
		||||
    channelsRoutes.initRoutes(this.app);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new Server();
 | 
			
		||||
@ -1,22 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "module": "commonjs",
 | 
			
		||||
    "target": "esnext",
 | 
			
		||||
    "lib": ["es2019", "dom"],
 | 
			
		||||
    "strict": true,
 | 
			
		||||
    "noImplicitAny": false,
 | 
			
		||||
    "sourceMap": false,
 | 
			
		||||
    "outDir": "dist",
 | 
			
		||||
    "moduleResolution": "node",
 | 
			
		||||
    "typeRoots": [
 | 
			
		||||
      "node_modules/@types"
 | 
			
		||||
    ],
 | 
			
		||||
    "allowSyntheticDefaultImports": true
 | 
			
		||||
  },
 | 
			
		||||
  "include": [
 | 
			
		||||
    "src/**/*.ts"
 | 
			
		||||
  ],
 | 
			
		||||
  "exclude": [
 | 
			
		||||
    "dist/**"
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
@ -1,137 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "rules": {
 | 
			
		||||
    "arrow-return-shorthand": true,
 | 
			
		||||
    "callable-types": true,
 | 
			
		||||
    "class-name": true,
 | 
			
		||||
    "comment-format": [
 | 
			
		||||
      true,
 | 
			
		||||
      "check-space"
 | 
			
		||||
    ],
 | 
			
		||||
    "curly": true,
 | 
			
		||||
    "deprecation": {
 | 
			
		||||
      "severity": "warn"
 | 
			
		||||
    },
 | 
			
		||||
    "eofline": true,
 | 
			
		||||
    "forin": false,
 | 
			
		||||
    "import-blacklist": [
 | 
			
		||||
      true,
 | 
			
		||||
      "rxjs",
 | 
			
		||||
      "rxjs/Rx"
 | 
			
		||||
    ],
 | 
			
		||||
    "import-spacing": true,
 | 
			
		||||
    "indent": [
 | 
			
		||||
      true,
 | 
			
		||||
      "spaces"
 | 
			
		||||
    ],
 | 
			
		||||
    "interface-over-type-literal": true,
 | 
			
		||||
    "label-position": true,
 | 
			
		||||
    "max-line-length": [
 | 
			
		||||
      true,
 | 
			
		||||
      140
 | 
			
		||||
    ],
 | 
			
		||||
    "member-access": false,
 | 
			
		||||
    "member-ordering": [
 | 
			
		||||
      true,
 | 
			
		||||
      {
 | 
			
		||||
        "order": [
 | 
			
		||||
          "static-field",
 | 
			
		||||
          "instance-field",
 | 
			
		||||
          "static-method",
 | 
			
		||||
          "instance-method"
 | 
			
		||||
        ]
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "no-arg": true,
 | 
			
		||||
    "no-bitwise": true,
 | 
			
		||||
    "no-console": [
 | 
			
		||||
      true,
 | 
			
		||||
      "debug",
 | 
			
		||||
      "info",
 | 
			
		||||
      "time",
 | 
			
		||||
      "timeEnd",
 | 
			
		||||
      "trace"
 | 
			
		||||
    ],
 | 
			
		||||
    "no-construct": true,
 | 
			
		||||
    "no-debugger": true,
 | 
			
		||||
    "no-duplicate-super": true,
 | 
			
		||||
    "no-empty": false,
 | 
			
		||||
    "no-empty-interface": true,
 | 
			
		||||
    "no-eval": true,
 | 
			
		||||
    "no-inferrable-types": false,
 | 
			
		||||
    "no-misused-new": true,
 | 
			
		||||
    "no-non-null-assertion": true,
 | 
			
		||||
    "no-shadowed-variable": true,
 | 
			
		||||
    "no-string-literal": false,
 | 
			
		||||
    "no-string-throw": true,
 | 
			
		||||
    "no-switch-case-fall-through": true,
 | 
			
		||||
    "no-trailing-whitespace": true,
 | 
			
		||||
    "no-unnecessary-initializer": true,
 | 
			
		||||
    "no-unused-expression": true,
 | 
			
		||||
    "no-use-before-declare": true,
 | 
			
		||||
    "no-var-keyword": true,
 | 
			
		||||
    "object-literal-sort-keys": false,
 | 
			
		||||
    "one-line": [
 | 
			
		||||
      true,
 | 
			
		||||
      "check-open-brace",
 | 
			
		||||
      "check-catch",
 | 
			
		||||
      "check-else",
 | 
			
		||||
      "check-whitespace"
 | 
			
		||||
    ],
 | 
			
		||||
    "prefer-const": true,
 | 
			
		||||
    "quotemark": [
 | 
			
		||||
      true,
 | 
			
		||||
      "single"
 | 
			
		||||
    ],
 | 
			
		||||
    "radix": true,
 | 
			
		||||
    "semicolon": [
 | 
			
		||||
      true,
 | 
			
		||||
      "always"
 | 
			
		||||
    ],
 | 
			
		||||
    "triple-equals": [
 | 
			
		||||
      true,
 | 
			
		||||
      "allow-null-check"
 | 
			
		||||
    ],
 | 
			
		||||
    "typedef-whitespace": [
 | 
			
		||||
      true,
 | 
			
		||||
      {
 | 
			
		||||
        "call-signature": "nospace",
 | 
			
		||||
        "index-signature": "nospace",
 | 
			
		||||
        "parameter": "nospace",
 | 
			
		||||
        "property-declaration": "nospace",
 | 
			
		||||
        "variable-declaration": "nospace"
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "unified-signatures": true,
 | 
			
		||||
    "variable-name": false,
 | 
			
		||||
    "whitespace": [
 | 
			
		||||
      true,
 | 
			
		||||
      "check-branch",
 | 
			
		||||
      "check-decl",
 | 
			
		||||
      "check-operator",
 | 
			
		||||
      "check-separator",
 | 
			
		||||
      "check-type"
 | 
			
		||||
    ],
 | 
			
		||||
    "directive-selector": [
 | 
			
		||||
      true,
 | 
			
		||||
      "attribute",
 | 
			
		||||
      "app",
 | 
			
		||||
      "camelCase"
 | 
			
		||||
    ],
 | 
			
		||||
    "component-selector": [
 | 
			
		||||
      true,
 | 
			
		||||
      "element",
 | 
			
		||||
      "app",
 | 
			
		||||
      "kebab-case"
 | 
			
		||||
    ],
 | 
			
		||||
    "no-output-on-prefix": true,
 | 
			
		||||
    "use-input-property-decorator": true,
 | 
			
		||||
    "use-output-property-decorator": true,
 | 
			
		||||
    "use-host-property-decorator": true,
 | 
			
		||||
    "no-input-rename": true,
 | 
			
		||||
    "no-output-rename": true,
 | 
			
		||||
    "use-life-cycle-interface": true,
 | 
			
		||||
    "use-pipe-transform-interface": true,
 | 
			
		||||
    "component-class-suffix": true,
 | 
			
		||||
    "directive-class-suffix": true
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user