New base code for mempool blockchain explorerer
This commit is contained in:
		
							parent
							
								
									d296239f54
								
							
						
					
					
						commit
						43f41b8aab
					
				
							
								
								
									
										57
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										57
									
								
								Dockerfile
									
									
									
									
									
								
							@ -1,57 +0,0 @@
 | 
			
		||||
FROM alpine:latest
 | 
			
		||||
 | 
			
		||||
RUN mkdir /mempool.space/
 | 
			
		||||
COPY ./backend /mempool.space/backend/
 | 
			
		||||
COPY ./frontend /mempool.space/frontend/
 | 
			
		||||
COPY ./mariadb-structure.sql /mempool.space/mariadb-structure.sql
 | 
			
		||||
 | 
			
		||||
RUN apk add mariadb mariadb-client git nginx npm rsync bash
 | 
			
		||||
 | 
			
		||||
RUN mysql_install_db --user=mysql --datadir=/var/lib/mysql/
 | 
			
		||||
RUN /usr/bin/mysqld_safe --datadir='/var/lib/mysql/'& \
 | 
			
		||||
    sleep 60 && \
 | 
			
		||||
    mysql -e "create database mempool" && \
 | 
			
		||||
    mysql -e "grant all privileges on mempool.* to 'mempool'@'localhost' identified by 'mempool'" && \
 | 
			
		||||
    mysql mempool < /mempool.space/mariadb-structure.sql
 | 
			
		||||
RUN sed -i "/^skip-networking/ c#skip-networking" /etc/my.cnf.d/mariadb-server.cnf
 | 
			
		||||
 | 
			
		||||
RUN export NG_CLI_ANALYTICS=ci && \
 | 
			
		||||
    npm install -g typescript && \
 | 
			
		||||
    cd /mempool.space/frontend && \
 | 
			
		||||
    npm install && \
 | 
			
		||||
    cd /mempool.space/backend && \
 | 
			
		||||
    npm install && \
 | 
			
		||||
    tsc
 | 
			
		||||
 | 
			
		||||
COPY ./nginx-nossl-docker.conf /etc/nginx/nginx.conf
 | 
			
		||||
    
 | 
			
		||||
ENV ENV dev
 | 
			
		||||
ENV DB_HOST localhost
 | 
			
		||||
ENV DB_PORT 3306
 | 
			
		||||
ENV DB_USER mempool
 | 
			
		||||
ENV DB_PASSWORD mempool
 | 
			
		||||
ENV DB_DATABASE mempool
 | 
			
		||||
ENV API_ENDPOINT /api/v1/
 | 
			
		||||
ENV CHAT_SSL_ENABLED false
 | 
			
		||||
ENV MEMPOOL_REFRESH_RATE_MS 500
 | 
			
		||||
ENV INITIAL_BLOCK_AMOUNT 8
 | 
			
		||||
ENV DEFAULT_PROJECTED_BLOCKS_AMOUNT 3
 | 
			
		||||
ENV KEEP_BLOCK_AMOUNT 24
 | 
			
		||||
ENV BITCOIN_NODE_HOST bitcoinhost
 | 
			
		||||
ENV BITCOIN_NODE_PORT 8332
 | 
			
		||||
ENV BITCOIN_NODE_USER bitcoinuser
 | 
			
		||||
ENV BITCOIN_NODE_PASS bitcoinpass
 | 
			
		||||
ENV TX_PER_SECOND_SPAN_SECONDS 150
 | 
			
		||||
ENV BACKEND_API bitcoind
 | 
			
		||||
ENV ELECTRS_API_URL https://www.blockstream.info/api
 | 
			
		||||
 | 
			
		||||
RUN cd /mempool.space/frontend/ && \
 | 
			
		||||
    npm run build && \
 | 
			
		||||
    rsync -av --delete dist/mempool/ /var/www/html/
 | 
			
		||||
 | 
			
		||||
EXPOSE 80
 | 
			
		||||
 | 
			
		||||
COPY ./entrypoint.sh /mempool.space/entrypoint.sh
 | 
			
		||||
RUN chmod +x /mempool.space/entrypoint.sh
 | 
			
		||||
WORKDIR /mempool.space
 | 
			
		||||
CMD ["/mempool.space/entrypoint.sh"]
 | 
			
		||||
							
								
								
									
										10
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								README.md
									
									
									
									
									
								
							@ -1,4 +1,5 @@
 | 
			
		||||
# mempool.space
 | 
			
		||||
🚨This is beta software, and may have issues!🚨
 | 
			
		||||
Please help us test and report bugs to our GitHub issue tracker.
 | 
			
		||||
 | 
			
		||||
Mempool visualizer for the Bitcoin blockchain. Live demo: https://mempool.space/
 | 
			
		||||
@ -12,13 +13,6 @@ Mempool visualizer for the Bitcoin blockchain. Live demo: https://mempool.space/
 | 
			
		||||
* MySQL or MariaDB (default config)
 | 
			
		||||
* Nginx (use supplied nginx.conf)
 | 
			
		||||
 | 
			
		||||
## Checking out release tag
 | 
			
		||||
```bash
 | 
			
		||||
  git clone https://github.com/mempool-space/mempool.space
 | 
			
		||||
  cd mempool.space
 | 
			
		||||
  git checkout v1.0.0 # put latest release tag here
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
## Bitcoin Core (bitcoind)
 | 
			
		||||
 | 
			
		||||
Enable RPC and txindex in bitcoin.conf
 | 
			
		||||
@ -34,6 +28,8 @@ Enable RPC and txindex in bitcoin.conf
 | 
			
		||||
Install dependencies and build code:
 | 
			
		||||
 | 
			
		||||
```bash
 | 
			
		||||
  cd mempool.space
 | 
			
		||||
 | 
			
		||||
  # Install TypeScript Globally
 | 
			
		||||
  npm install -g typescript
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,25 +1,19 @@
 | 
			
		||||
{
 | 
			
		||||
  "ENV": "dev",
 | 
			
		||||
  "HTTP_PORT": 8999,
 | 
			
		||||
  "DB_HOST": "localhost",
 | 
			
		||||
  "DB_PORT": 3306,
 | 
			
		||||
  "DB_USER": "mempool",
 | 
			
		||||
  "DB_PASSWORD": "mempool",
 | 
			
		||||
  "DB_DATABASE": "mempool",
 | 
			
		||||
  "HTTP_PORT": 3000,
 | 
			
		||||
  "API_ENDPOINT": "/api/v1/",
 | 
			
		||||
  "CHAT_SSL_ENABLED": false,
 | 
			
		||||
  "CHAT_SSL_PRIVKEY": "",
 | 
			
		||||
  "CHAT_SSL_CERT": "",
 | 
			
		||||
  "CHAT_SSL_CHAIN": "",
 | 
			
		||||
  "MEMPOOL_REFRESH_RATE_MS": 500,
 | 
			
		||||
  "INITIAL_BLOCK_AMOUNT": 8,
 | 
			
		||||
  "ELECTRS_POLL_RATE_MS": 2000,
 | 
			
		||||
  "MEMPOOL_REFRESH_RATE_MS": 10000,
 | 
			
		||||
  "DEFAULT_PROJECTED_BLOCKS_AMOUNT": 3,
 | 
			
		||||
  "KEEP_BLOCK_AMOUNT": 24,
 | 
			
		||||
  "BITCOIN_NODE_HOST": "localhost",
 | 
			
		||||
  "BITCOIN_NODE_PORT": 8332,
 | 
			
		||||
  "BITCOIN_NODE_USER": "",
 | 
			
		||||
  "BITCOIN_NODE_PASS": "",
 | 
			
		||||
  "BACKEND_API": "bitcoind",
 | 
			
		||||
  "ELECTRS_API_URL": "https://www.blockstream.info/api",
 | 
			
		||||
  "TX_PER_SECOND_SPAN_SECONDS": 150
 | 
			
		||||
  "INITIAL_BLOCK_AMOUNT": 8,
 | 
			
		||||
  "TX_PER_SECOND_SPAN_SECONDS": 150,
 | 
			
		||||
  "ELECTRS_API_URL": "https://www.blockstream.info/testnet/api",
 | 
			
		||||
  "SSL": false,
 | 
			
		||||
  "SSL_CERT_FILE_PATH": "/etc/letsencrypt/live/mysite/fullchain.pem",
 | 
			
		||||
  "SSL_KEY_FILE_PATH": "/etc/letsencrypt/live/mysite/privkey.pem"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,31 +1,26 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "mempool-backend",
 | 
			
		||||
  "name": "mempool-space-explorer-backend",
 | 
			
		||||
  "version": "1.0.0",
 | 
			
		||||
  "description": "Bitcoin Mempool Visualizer",
 | 
			
		||||
  "description": "Mempool space backend",
 | 
			
		||||
  "main": "index.ts",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "build": "tsc",
 | 
			
		||||
    "start": "npm run build && node dist/index.js"
 | 
			
		||||
  },
 | 
			
		||||
  "author": {
 | 
			
		||||
    "name": "Simon Lindh",
 | 
			
		||||
    "url": "https://github.com/mempool-space/mempool.space"
 | 
			
		||||
  },
 | 
			
		||||
  "license": "MIT",
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "bitcoin": "^3.0.1",
 | 
			
		||||
    "compression": "^1.7.3",
 | 
			
		||||
    "express": "^4.16.3",
 | 
			
		||||
    "compression": "^1.7.4",
 | 
			
		||||
    "express": "^4.17.1",
 | 
			
		||||
    "mysql2": "^1.6.1",
 | 
			
		||||
    "request": "^2.88.0",
 | 
			
		||||
    "ws": "^6.0.0"
 | 
			
		||||
    "ws": "^7.2.0"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@types/express": "^4.16.0",
 | 
			
		||||
    "@types/mysql2": "github:types/mysql2",
 | 
			
		||||
    "@types/compression": "^1.0.1",
 | 
			
		||||
    "@types/express": "^4.17.2",
 | 
			
		||||
    "@types/request": "^2.48.2",
 | 
			
		||||
    "@types/ws": "^6.0.1",
 | 
			
		||||
    "@types/ws": "^6.0.4",
 | 
			
		||||
    "tslint": "^5.11.0",
 | 
			
		||||
    "typescript": "^3.1.1"
 | 
			
		||||
    "typescript": "~3.6.4"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,19 +0,0 @@
 | 
			
		||||
import { IMempoolInfo, ITransaction, IBlock } from '../../interfaces';
 | 
			
		||||
 | 
			
		||||
export interface AbstractBitcoinApi {
 | 
			
		||||
  getMempoolInfo(): Promise<IMempoolInfo>;
 | 
			
		||||
  getRawMempool(): Promise<ITransaction['txid'][]>;
 | 
			
		||||
  getRawTransaction(txId: string): Promise<ITransaction>;
 | 
			
		||||
  getBlockCount(): Promise<number>;
 | 
			
		||||
  getBlockAndTransactions(hash: string): Promise<IBlock>;
 | 
			
		||||
  getBlockHash(height: number): Promise<string>;
 | 
			
		||||
 | 
			
		||||
  getBlock(hash: string): Promise<IBlock>;
 | 
			
		||||
  getBlockTransactions(hash: string): Promise<IBlock>;
 | 
			
		||||
  getBlockTransactionsFromIndex(hash: string, index: number): Promise<IBlock>;
 | 
			
		||||
  getBlocks(): Promise<string>;
 | 
			
		||||
  getBlocksFromHeight(height: number): Promise<string>;
 | 
			
		||||
  getAddress(address: string): Promise<IBlock>;
 | 
			
		||||
  getAddressTransactions(address: string): Promise<IBlock>;
 | 
			
		||||
  getAddressTransactionsFromLastSeenTxid(address: string, lastSeenTxid: string): Promise<IBlock>;
 | 
			
		||||
}
 | 
			
		||||
@ -1,16 +0,0 @@
 | 
			
		||||
const config = require('../../../mempool-config.json');
 | 
			
		||||
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
 | 
			
		||||
import BitcoindApi from './bitcoind-api';
 | 
			
		||||
import ElectrsApi from './electrs-api';
 | 
			
		||||
 | 
			
		||||
function factory(): AbstractBitcoinApi {
 | 
			
		||||
  switch (config.BACKEND_API) {
 | 
			
		||||
    case 'electrs':
 | 
			
		||||
      return new ElectrsApi();
 | 
			
		||||
    case 'bitcoind':
 | 
			
		||||
    default:
 | 
			
		||||
      return new BitcoindApi();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default factory();
 | 
			
		||||
@ -1,110 +0,0 @@
 | 
			
		||||
const config = require('../../../mempool-config.json');
 | 
			
		||||
import * as bitcoin from 'bitcoin';
 | 
			
		||||
import { ITransaction, IMempoolInfo, IBlock } from '../../interfaces';
 | 
			
		||||
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
 | 
			
		||||
 | 
			
		||||
class BitcoindApi implements AbstractBitcoinApi {
 | 
			
		||||
  client: any;
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
    this.client = new bitcoin.Client({
 | 
			
		||||
      host: config.BITCOIN_NODE_HOST,
 | 
			
		||||
      port: config.BITCOIN_NODE_PORT,
 | 
			
		||||
      user: config.BITCOIN_NODE_USER,
 | 
			
		||||
      pass: config.BITCOIN_NODE_PASS,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getMempoolInfo(): Promise<IMempoolInfo> {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      this.client.getMempoolInfo((err: Error, mempoolInfo: any) => {
 | 
			
		||||
        if (err) {
 | 
			
		||||
          return reject(err);
 | 
			
		||||
        }
 | 
			
		||||
        resolve(mempoolInfo);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getRawMempool(): Promise<ITransaction['txid'][]> {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      this.client.getRawMemPool((err: Error, transactions: ITransaction['txid'][]) => {
 | 
			
		||||
        if (err) {
 | 
			
		||||
          return reject(err);
 | 
			
		||||
        }
 | 
			
		||||
        resolve(transactions);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getRawTransaction(txId: string): Promise<ITransaction> {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      this.client.getRawTransaction(txId, true, (err: Error, txData: ITransaction) => {
 | 
			
		||||
        if (err) {
 | 
			
		||||
          return reject(err);
 | 
			
		||||
        }
 | 
			
		||||
        resolve(txData);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getBlockCount(): Promise<number> {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      this.client.getBlockCount((err: Error, response: number) => {
 | 
			
		||||
        if (err) {
 | 
			
		||||
          return reject(err);
 | 
			
		||||
        }
 | 
			
		||||
        resolve(response);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getBlockAndTransactions(hash: string, verbosity:  1 | 2 = 1): Promise<IBlock> {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      this.client.getBlock(hash, verbosity, (err: Error, block: IBlock) => {
 | 
			
		||||
        if (err) {
 | 
			
		||||
          return reject(err);
 | 
			
		||||
        }
 | 
			
		||||
        resolve(block);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getBlockHash(height: number): Promise<string> {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      this.client.getBlockHash(height, (err: Error, response: string) => {
 | 
			
		||||
        if (err) {
 | 
			
		||||
          return reject(err);
 | 
			
		||||
        }
 | 
			
		||||
        resolve(response);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getBlock(hash: string): Promise<IBlock> {
 | 
			
		||||
    throw new Error('Method not implemented.');
 | 
			
		||||
  }
 | 
			
		||||
  getBlocks(): Promise<string> {
 | 
			
		||||
    throw new Error('Method not implemented.');
 | 
			
		||||
  }
 | 
			
		||||
  getBlocksFromHeight(height: number): Promise<string> {
 | 
			
		||||
    throw new Error('Method not implemented.');
 | 
			
		||||
  }
 | 
			
		||||
  getBlockTransactions(hash: string): Promise<IBlock> {
 | 
			
		||||
    throw new Error('Method not implemented.');
 | 
			
		||||
  }
 | 
			
		||||
  getBlockTransactionsFromIndex(hash: string, index: number): Promise<IBlock> {
 | 
			
		||||
    throw new Error('Method not implemented.');
 | 
			
		||||
  }
 | 
			
		||||
  getAddress(address: string): Promise<IBlock> {
 | 
			
		||||
    throw new Error('Method not implemented.');
 | 
			
		||||
  }
 | 
			
		||||
  getAddressTransactions(address: string): Promise<IBlock> {
 | 
			
		||||
    throw new Error('Method not implemented.');
 | 
			
		||||
  }
 | 
			
		||||
  getAddressTransactionsFromLastSeenTxid(address: string, lastSeenTxid: string): Promise<IBlock> {
 | 
			
		||||
    throw new Error('Method not implemented.');
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default BitcoindApi;
 | 
			
		||||
@ -1,33 +1,15 @@
 | 
			
		||||
const config = require('../../../mempool-config.json');
 | 
			
		||||
import { ITransaction, IMempoolInfo, IBlock } from '../../interfaces';
 | 
			
		||||
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
 | 
			
		||||
import { Transaction, Block } from '../../interfaces';
 | 
			
		||||
import * as request from 'request';
 | 
			
		||||
 | 
			
		||||
class ElectrsApi implements AbstractBitcoinApi {
 | 
			
		||||
class ElectrsApi {
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getMempoolInfo(): Promise<IMempoolInfo> {
 | 
			
		||||
  getRawMempool(): Promise<Transaction['txid'][]> {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      request(config.ELECTRS_API_URL + '/mempool', { json: true, timeout: 10000 }, (err, res, response) => {
 | 
			
		||||
        if (err) {
 | 
			
		||||
          reject(err);
 | 
			
		||||
        } else if (res.statusCode !== 200) {
 | 
			
		||||
          reject(response);
 | 
			
		||||
        } else {
 | 
			
		||||
          resolve({
 | 
			
		||||
            size: response.count,
 | 
			
		||||
            bytes: response.vsize,
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getRawMempool(): Promise<ITransaction['txid'][]> {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      request(config.ELECTRS_API_URL + '/mempool/txids', { json: true, timeout: 10000 }, (err, res, response) => {
 | 
			
		||||
      request(config.ELECTRS_API_URL + '/mempool/txids', { json: true, timeout: 10000, forever: true }, (err, res, response) => {
 | 
			
		||||
        if (err) {
 | 
			
		||||
          reject(err);
 | 
			
		||||
        } else if (res.statusCode !== 200) {
 | 
			
		||||
@ -39,24 +21,21 @@ class ElectrsApi implements AbstractBitcoinApi {
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getRawTransaction(txId: string): Promise<ITransaction> {
 | 
			
		||||
  getRawTransaction(txId: string): Promise<Transaction> {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      request(config.ELECTRS_API_URL + '/tx/' + txId, { json: true, timeout: 10000 }, (err, res, response) => {
 | 
			
		||||
      request(config.ELECTRS_API_URL + '/tx/' + txId, { json: true, timeout: 10000, forever: true }, (err, res, response) => {
 | 
			
		||||
        if (err) {
 | 
			
		||||
          reject(err);
 | 
			
		||||
        } else if (res.statusCode !== 200) {
 | 
			
		||||
          reject(response);
 | 
			
		||||
        } else {
 | 
			
		||||
          response.vsize = Math.round(response.weight / 4);
 | 
			
		||||
          response.fee = response.fee / 100000000;
 | 
			
		||||
          response.blockhash = response.status.block_hash;
 | 
			
		||||
          resolve(response);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getBlockCount(): Promise<number> {
 | 
			
		||||
  getBlockHeightTip(): Promise<number> {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      request(config.ELECTRS_API_URL + '/blocks/tip/height', { json: true, timeout: 10000 }, (err, res, response) => {
 | 
			
		||||
        if (err) {
 | 
			
		||||
@ -70,29 +49,15 @@ class ElectrsApi implements AbstractBitcoinApi {
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getBlockAndTransactions(hash: string): Promise<IBlock> {
 | 
			
		||||
  getTxIdsForBlock(hash: string): Promise<Block> {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      request(config.ELECTRS_API_URL + '/block/' + hash, { json: true, timeout: 10000 }, (err, res, response) => {
 | 
			
		||||
      request(config.ELECTRS_API_URL + '/block/' + hash + '/txids', { json: true, timeout: 10000 }, (err, res, response) => {
 | 
			
		||||
        if (err) {
 | 
			
		||||
          reject(err);
 | 
			
		||||
        } else if (res.statusCode !== 200) {
 | 
			
		||||
          reject(response);
 | 
			
		||||
        } else {
 | 
			
		||||
          request(config.ELECTRS_API_URL + '/block/' + hash + '/txids', { json: true, timeout: 10000 }, (err2, res2, response2) => {
 | 
			
		||||
            if (err2) {
 | 
			
		||||
              reject(err2);
 | 
			
		||||
            } else if (res.statusCode !== 200) {
 | 
			
		||||
              reject(response);
 | 
			
		||||
            } else {
 | 
			
		||||
              const block = response;
 | 
			
		||||
              block.hash = hash;
 | 
			
		||||
              block.nTx = block.tx_count;
 | 
			
		||||
              block.time = block.timestamp;
 | 
			
		||||
              block.tx = response2;
 | 
			
		||||
 | 
			
		||||
              resolve(block);
 | 
			
		||||
            }
 | 
			
		||||
          });
 | 
			
		||||
          resolve(response);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
@ -112,20 +77,6 @@ class ElectrsApi implements AbstractBitcoinApi {
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getBlocks(): Promise<string> {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      request(config.ELECTRS_API_URL + '/blocks', { json: true, timeout: 10000 }, (err, res, response) => {
 | 
			
		||||
        if (err) {
 | 
			
		||||
          reject(err);
 | 
			
		||||
        } else if (res.statusCode !== 200) {
 | 
			
		||||
          reject(response);
 | 
			
		||||
        } else  {
 | 
			
		||||
          resolve(response);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getBlocksFromHeight(height: number): Promise<string> {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      request(config.ELECTRS_API_URL + '/blocks/' + height, { json: true, timeout: 10000 }, (err, res, response) => {
 | 
			
		||||
@ -140,7 +91,7 @@ class ElectrsApi implements AbstractBitcoinApi {
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getBlock(hash: string): Promise<IBlock> {
 | 
			
		||||
  getBlock(hash: string): Promise<Block> {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      request(config.ELECTRS_API_URL + '/block/' + hash, { json: true, timeout: 10000 }, (err, res, response) => {
 | 
			
		||||
        if (err) {
 | 
			
		||||
@ -153,77 +104,6 @@ class ElectrsApi implements AbstractBitcoinApi {
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getBlockTransactions(hash: string): Promise<IBlock> {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      request(config.ELECTRS_API_URL + '/block/' + hash + '/txs', { json: true, timeout: 10000 }, (err, res, response) => {
 | 
			
		||||
        if (err) {
 | 
			
		||||
          reject(err);
 | 
			
		||||
        } else if (res.statusCode !== 200) {
 | 
			
		||||
          reject(response);
 | 
			
		||||
        } else  {
 | 
			
		||||
          resolve(response);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getBlockTransactionsFromIndex(hash: string, index: number): Promise<IBlock> {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      request(config.ELECTRS_API_URL + '/block/' + hash + '/txs/' + index, { json: true, timeout: 10000 }, (err, res, response) => {
 | 
			
		||||
        if (err) {
 | 
			
		||||
          reject(err);
 | 
			
		||||
        } else if (res.statusCode !== 200) {
 | 
			
		||||
          reject(response);
 | 
			
		||||
        } else  {
 | 
			
		||||
          resolve(response);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getAddress(address: string): Promise<IBlock> {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      request(config.ELECTRS_API_URL + '/address/' + address, { json: true, timeout: 10000 }, (err, res, response) => {
 | 
			
		||||
        if (err) {
 | 
			
		||||
          reject(err);
 | 
			
		||||
        } else if (res.statusCode !== 200) {
 | 
			
		||||
          reject(response);
 | 
			
		||||
        } else  {
 | 
			
		||||
          resolve(response);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getAddressTransactions(address: string): Promise<IBlock> {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      request(config.ELECTRS_API_URL + '/address/' + address + '/txs', { json: true, timeout: 10000 }, (err, res, response) => {
 | 
			
		||||
        if (err) {
 | 
			
		||||
          reject(err);
 | 
			
		||||
        } else if (res.statusCode !== 200) {
 | 
			
		||||
          reject(response);
 | 
			
		||||
        } else  {
 | 
			
		||||
          resolve(response);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getAddressTransactionsFromLastSeenTxid(address: string, lastSeenTxid: string): Promise<IBlock> {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      request(config.ELECTRS_API_URL + '/address/' + address + '/txs/chain/' + lastSeenTxid,
 | 
			
		||||
        { json: true, timeout: 10000 }, (err, res, response) => {
 | 
			
		||||
        if (err) {
 | 
			
		||||
          reject(err);
 | 
			
		||||
        } else if (res.statusCode !== 200) {
 | 
			
		||||
          reject(response);
 | 
			
		||||
        } else  {
 | 
			
		||||
          resolve(response);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ElectrsApi;
 | 
			
		||||
export default new ElectrsApi();
 | 
			
		||||
 | 
			
		||||
@ -1,206 +1,61 @@
 | 
			
		||||
const config = require('../../mempool-config.json');
 | 
			
		||||
import bitcoinApi from './bitcoin/bitcoin-api-factory';
 | 
			
		||||
import { DB } from '../database';
 | 
			
		||||
import { IBlock, ITransaction } from '../interfaces';
 | 
			
		||||
import memPool from './mempool';
 | 
			
		||||
import bitcoinApi from './bitcoin/electrs-api';
 | 
			
		||||
import { Block } from '../interfaces';
 | 
			
		||||
 | 
			
		||||
class Blocks {
 | 
			
		||||
  private blocks: IBlock[] = [];
 | 
			
		||||
  private newBlockCallback: Function | undefined;
 | 
			
		||||
  private blocks: Block[] = [];
 | 
			
		||||
  private currentBlockHeight = 0;
 | 
			
		||||
  private newBlockCallback: Function = () => {};
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
    setInterval(this.$clearOldTransactionsAndBlocksFromDatabase.bind(this), 86400000);
 | 
			
		||||
  constructor() { }
 | 
			
		||||
 | 
			
		||||
  public getBlocks(): Block[] {
 | 
			
		||||
    return this.blocks;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public setNewBlockCallback(fn: Function) {
 | 
			
		||||
    this.newBlockCallback = fn;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public getBlocks(): IBlock[] {
 | 
			
		||||
    return this.blocks;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public formatBlock(block: IBlock) {
 | 
			
		||||
    return {
 | 
			
		||||
      hash: block.hash,
 | 
			
		||||
      height: block.height,
 | 
			
		||||
      nTx: block.nTx - 1,
 | 
			
		||||
      size: block.size,
 | 
			
		||||
      time: block.time,
 | 
			
		||||
      weight: block.weight,
 | 
			
		||||
      fees: block.fees,
 | 
			
		||||
      minFee: block.minFee,
 | 
			
		||||
      maxFee: block.maxFee,
 | 
			
		||||
      medianFee: block.medianFee,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async updateBlocks() {
 | 
			
		||||
    try {
 | 
			
		||||
      const blockCount = await bitcoinApi.getBlockCount();
 | 
			
		||||
      const blockHeightTip = await bitcoinApi.getBlockHeightTip();
 | 
			
		||||
 | 
			
		||||
      if (this.blocks.length === 0) {
 | 
			
		||||
        this.currentBlockHeight = blockCount - config.INITIAL_BLOCK_AMOUNT;
 | 
			
		||||
        this.currentBlockHeight = blockHeightTip - config.INITIAL_BLOCK_AMOUNT;
 | 
			
		||||
      } else {
 | 
			
		||||
        this.currentBlockHeight = this.blocks[this.blocks.length - 1].height;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      while (this.currentBlockHeight < blockCount) {
 | 
			
		||||
        this.currentBlockHeight++;
 | 
			
		||||
 | 
			
		||||
        let block: IBlock | undefined;
 | 
			
		||||
 | 
			
		||||
        const storedBlock = await this.$getBlockFromDatabase(this.currentBlockHeight);
 | 
			
		||||
        if (storedBlock) {
 | 
			
		||||
          block = storedBlock;
 | 
			
		||||
      while (this.currentBlockHeight < blockHeightTip) {
 | 
			
		||||
        if (this.currentBlockHeight === 0) {
 | 
			
		||||
          this.currentBlockHeight = blockHeightTip;
 | 
			
		||||
        } else {
 | 
			
		||||
          const blockHash = await bitcoinApi.getBlockHash(this.currentBlockHeight);
 | 
			
		||||
          block = await bitcoinApi.getBlockAndTransactions(blockHash);
 | 
			
		||||
 | 
			
		||||
          const coinbase = await memPool.getRawTransaction(block.tx[0], true);
 | 
			
		||||
          if (coinbase && coinbase.totalOut) {
 | 
			
		||||
            block.fees = coinbase.totalOut;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          const mempool = memPool.getMempool();
 | 
			
		||||
          let found = 0;
 | 
			
		||||
          let notFound = 0;
 | 
			
		||||
 | 
			
		||||
          let transactions: ITransaction[] = [];
 | 
			
		||||
 | 
			
		||||
          for (let i = 1; i < block.tx.length; i++) {
 | 
			
		||||
            if (mempool[block.tx[i]]) {
 | 
			
		||||
              transactions.push(mempool[block.tx[i]]);
 | 
			
		||||
              found++;
 | 
			
		||||
            } else {
 | 
			
		||||
              console.log(`Fetching block tx ${i} of ${block.tx.length}`);
 | 
			
		||||
              const tx = await memPool.getRawTransaction(block.tx[i]);
 | 
			
		||||
              if (tx) {
 | 
			
		||||
                transactions.push(tx);
 | 
			
		||||
              }
 | 
			
		||||
              notFound++;
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          transactions.sort((a, b) => b.feePerVsize - a.feePerVsize);
 | 
			
		||||
          transactions = transactions.filter((tx: ITransaction) => tx.feePerVsize);
 | 
			
		||||
 | 
			
		||||
          block.minFee = transactions[transactions.length - 1] ? transactions[transactions.length - 1].feePerVsize : 0;
 | 
			
		||||
          block.maxFee = transactions[0] ? transactions[0].feePerVsize : 0;
 | 
			
		||||
          block.medianFee = this.median(transactions.map((tx) => tx.feePerVsize));
 | 
			
		||||
 | 
			
		||||
          console.log(`New block found (#${this.currentBlockHeight})! `
 | 
			
		||||
           + `${found} of ${block.tx.length} found in mempool. ${notFound} not found.`);
 | 
			
		||||
 | 
			
		||||
           if (this.newBlockCallback) {
 | 
			
		||||
            this.newBlockCallback(block);
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          this.$saveBlockToDatabase(block);
 | 
			
		||||
          this.$saveTransactionsToDatabase(block.height, transactions);
 | 
			
		||||
          this.currentBlockHeight++;
 | 
			
		||||
          console.log(`New block found (#${this.currentBlockHeight})!`);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const blockHash = await bitcoinApi.getBlockHash(this.currentBlockHeight);
 | 
			
		||||
        const block = await bitcoinApi.getBlock(blockHash);
 | 
			
		||||
        const txIds = await bitcoinApi.getTxIdsForBlock(blockHash);
 | 
			
		||||
 | 
			
		||||
        block.medianFee = 2;
 | 
			
		||||
        block.feeRange = [1, 3];
 | 
			
		||||
 | 
			
		||||
        this.blocks.push(block);
 | 
			
		||||
        if (this.blocks.length > config.KEEP_BLOCK_AMOUNT) {
 | 
			
		||||
          this.blocks.shift();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.newBlockCallback(block, txIds);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      console.log('Error getBlockCount', err);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $getBlockFromDatabase(height: number): Promise<IBlock | undefined> {
 | 
			
		||||
    try {
 | 
			
		||||
      const connection = await DB.pool.getConnection();
 | 
			
		||||
      const query = `
 | 
			
		||||
        SELECT * FROM blocks WHERE height = ?
 | 
			
		||||
      `;
 | 
			
		||||
 | 
			
		||||
      const [rows] = await connection.query<any>(query, [height]);
 | 
			
		||||
      connection.release();
 | 
			
		||||
 | 
			
		||||
      if (rows[0]) {
 | 
			
		||||
        return rows[0];
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      console.log('$get() block error', e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $saveBlockToDatabase(block: IBlock) {
 | 
			
		||||
    try {
 | 
			
		||||
      const connection = await DB.pool.getConnection();
 | 
			
		||||
      const query = `
 | 
			
		||||
        INSERT IGNORE INTO blocks
 | 
			
		||||
        (height, hash, size, weight, minFee, maxFee, time, fees, nTx, medianFee)
 | 
			
		||||
        VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
 | 
			
		||||
      `;
 | 
			
		||||
 | 
			
		||||
      const params: (any)[] = [
 | 
			
		||||
        block.height,
 | 
			
		||||
        block.hash,
 | 
			
		||||
        block.size,
 | 
			
		||||
        block.weight,
 | 
			
		||||
        block.minFee,
 | 
			
		||||
        block.maxFee,
 | 
			
		||||
        block.time,
 | 
			
		||||
        block.fees,
 | 
			
		||||
        block.nTx - 1,
 | 
			
		||||
        block.medianFee,
 | 
			
		||||
      ];
 | 
			
		||||
 | 
			
		||||
      await connection.query(query, params);
 | 
			
		||||
      connection.release();
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      console.log('$create() block error', e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $saveTransactionsToDatabase(blockheight: number, transactions: ITransaction[]) {
 | 
			
		||||
    try {
 | 
			
		||||
      const connection = await DB.pool.getConnection();
 | 
			
		||||
 | 
			
		||||
      for (let i = 0; i < transactions.length; i++) {
 | 
			
		||||
        const query = `
 | 
			
		||||
          INSERT IGNORE INTO transactions
 | 
			
		||||
          (blockheight, txid, fee, feePerVsize)
 | 
			
		||||
          VALUES(?, ?, ?, ?)
 | 
			
		||||
        `;
 | 
			
		||||
 | 
			
		||||
        const params: (any)[] = [
 | 
			
		||||
          blockheight,
 | 
			
		||||
          transactions[i].txid,
 | 
			
		||||
          transactions[i].fee,
 | 
			
		||||
          transactions[i].feePerVsize,
 | 
			
		||||
        ];
 | 
			
		||||
 | 
			
		||||
        await connection.query(query, params);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      connection.release();
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      console.log('$create() transaction error', e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $clearOldTransactionsAndBlocksFromDatabase() {
 | 
			
		||||
    try {
 | 
			
		||||
      const connection = await DB.pool.getConnection();
 | 
			
		||||
      let query = `DELETE FROM blocks WHERE height < ?`;
 | 
			
		||||
      await connection.query<any>(query, [this.currentBlockHeight - config.KEEP_BLOCK_AMOUNT]);
 | 
			
		||||
      query = `DELETE FROM transactions WHERE blockheight < ?`;
 | 
			
		||||
      await connection.query<any>(query, [this.currentBlockHeight - config.KEEP_BLOCK_AMOUNT]);
 | 
			
		||||
      connection.release();
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      console.log('$clearOldTransactionsFromDatabase() error', e);
 | 
			
		||||
      console.log('updateBlocks error', err);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private median(numbers: number[]) {
 | 
			
		||||
    if (!numbers.length) { return 0; }
 | 
			
		||||
    let medianNr = 0;
 | 
			
		||||
    const numsLen = numbers.length;
 | 
			
		||||
    numbers.sort();
 | 
			
		||||
@ -211,6 +66,20 @@ class Blocks {
 | 
			
		||||
    }
 | 
			
		||||
    return medianNr;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private getFeesInRange(transactions: any[], rangeLength: number) {
 | 
			
		||||
    const arr = [transactions[transactions.length - 1].feePerVsize];
 | 
			
		||||
    const chunk = 1 / (rangeLength - 1);
 | 
			
		||||
    let itemsToAdd = rangeLength - 2;
 | 
			
		||||
 | 
			
		||||
    while (itemsToAdd > 0) {
 | 
			
		||||
      arr.push(transactions[Math.floor(transactions.length * chunk * itemsToAdd)].feePerVsize);
 | 
			
		||||
      itemsToAdd--;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    arr.push(transactions[0].feePerVsize);
 | 
			
		||||
    return arr;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new Blocks();
 | 
			
		||||
 | 
			
		||||
@ -1,11 +1,11 @@
 | 
			
		||||
import projectedBlocks from './projected-blocks';
 | 
			
		||||
import projectedBlocks from './mempool-blocks';
 | 
			
		||||
import { DB } from '../database';
 | 
			
		||||
 | 
			
		||||
class FeeApi {
 | 
			
		||||
  constructor() { }
 | 
			
		||||
 | 
			
		||||
  public getRecommendedFee() {
 | 
			
		||||
    const pBlocks = projectedBlocks.getProjectedBlocks();
 | 
			
		||||
    const pBlocks = projectedBlocks.getMempoolBlocks();
 | 
			
		||||
    if (!pBlocks.length) {
 | 
			
		||||
      return {
 | 
			
		||||
        'fastestFee': 0,
 | 
			
		||||
@ -15,7 +15,7 @@ class FeeApi {
 | 
			
		||||
    }
 | 
			
		||||
    let firstMedianFee = Math.ceil(pBlocks[0].medianFee);
 | 
			
		||||
 | 
			
		||||
    if (pBlocks.length === 1 && pBlocks[0].blockWeight <= 2000000) {
 | 
			
		||||
    if (pBlocks.length === 1 && pBlocks[0].blockVSize <= 500000) {
 | 
			
		||||
      firstMedianFee = 1;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										97
									
								
								backend/src/api/mempool-blocks.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								backend/src/api/mempool-blocks.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,97 @@
 | 
			
		||||
const config = require('../../mempool-config.json');
 | 
			
		||||
import { MempoolBlock, SimpleTransaction } from '../interfaces';
 | 
			
		||||
 | 
			
		||||
class MempoolBlocks {
 | 
			
		||||
  private mempoolBlocks: MempoolBlock[] = [];
 | 
			
		||||
 | 
			
		||||
  constructor() {}
 | 
			
		||||
 | 
			
		||||
  public getMempoolBlocks(): MempoolBlock[] {
 | 
			
		||||
    return this.mempoolBlocks;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public updateMempoolBlocks(memPool: { [txid: string]: SimpleTransaction }): void {
 | 
			
		||||
    const latestMempool = memPool;
 | 
			
		||||
    const memPoolArray: SimpleTransaction[] = [];
 | 
			
		||||
    for (const i in latestMempool) {
 | 
			
		||||
      if (latestMempool.hasOwnProperty(i)) {
 | 
			
		||||
        memPoolArray.push(latestMempool[i]);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    memPoolArray.sort((a, b) => b.feePerVsize - a.feePerVsize);
 | 
			
		||||
    const transactionsSorted = memPoolArray.filter((tx) => tx.feePerVsize);
 | 
			
		||||
    this.mempoolBlocks = this.calculateMempoolBlocks(transactionsSorted);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private calculateMempoolBlocks(transactionsSorted: SimpleTransaction[]): MempoolBlock[] {
 | 
			
		||||
    const mempoolBlocks: MempoolBlock[] = [];
 | 
			
		||||
    let blockWeight = 0;
 | 
			
		||||
    let blockSize = 0;
 | 
			
		||||
    let transactions: SimpleTransaction[] = [];
 | 
			
		||||
    transactionsSorted.forEach((tx) => {
 | 
			
		||||
      if (blockWeight + tx.vsize < 1000000 || mempoolBlocks.length === config.DEFAULT_PROJECTED_BLOCKS_AMOUNT) {
 | 
			
		||||
        blockWeight += tx.vsize;
 | 
			
		||||
        blockSize += tx.size;
 | 
			
		||||
        transactions.push(tx);
 | 
			
		||||
      } else {
 | 
			
		||||
        mempoolBlocks.push(this.dataToMempoolBlocks(transactions, blockSize, blockWeight, mempoolBlocks.length));
 | 
			
		||||
        blockWeight = 0;
 | 
			
		||||
        blockSize = 0;
 | 
			
		||||
        transactions = [];
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    if (transactions.length) {
 | 
			
		||||
      mempoolBlocks.push(this.dataToMempoolBlocks(transactions, blockSize, blockWeight, mempoolBlocks.length));
 | 
			
		||||
    }
 | 
			
		||||
    return mempoolBlocks;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private dataToMempoolBlocks(transactions: SimpleTransaction[], blockSize: number, blockVSize: number, blocksIndex: number): MempoolBlock {
 | 
			
		||||
    let rangeLength = 3;
 | 
			
		||||
    if (blocksIndex === 0) {
 | 
			
		||||
      rangeLength = 8;
 | 
			
		||||
    }
 | 
			
		||||
    if (transactions.length > 4000) {
 | 
			
		||||
      rangeLength = 5;
 | 
			
		||||
    } else if (transactions.length > 10000) {
 | 
			
		||||
      rangeLength = 8;
 | 
			
		||||
    } else if (transactions.length > 25000) {
 | 
			
		||||
      rangeLength = 10;
 | 
			
		||||
    }
 | 
			
		||||
    return {
 | 
			
		||||
      blockSize: blockSize,
 | 
			
		||||
      blockVSize: blockVSize,
 | 
			
		||||
      nTx: transactions.length,
 | 
			
		||||
      medianFee: this.median(transactions.map((tx) => tx.feePerVsize)),
 | 
			
		||||
      feeRange: this.getFeesInRange(transactions, rangeLength),
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private median(numbers: number[]) {
 | 
			
		||||
    let medianNr = 0;
 | 
			
		||||
    const numsLen = numbers.length;
 | 
			
		||||
    numbers.sort();
 | 
			
		||||
    if (numsLen % 2 === 0) {
 | 
			
		||||
        medianNr = (numbers[numsLen / 2 - 1] + numbers[numsLen / 2]) / 2;
 | 
			
		||||
    } else {
 | 
			
		||||
        medianNr = numbers[(numsLen - 1) / 2];
 | 
			
		||||
    }
 | 
			
		||||
    return medianNr;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private getFeesInRange(transactions: SimpleTransaction[], rangeLength: number) {
 | 
			
		||||
    const arr = [transactions[transactions.length - 1].feePerVsize];
 | 
			
		||||
    const chunk = 1 / (rangeLength - 1);
 | 
			
		||||
    let itemsToAdd = rangeLength - 2;
 | 
			
		||||
 | 
			
		||||
    while (itemsToAdd > 0) {
 | 
			
		||||
      arr.push(transactions[Math.floor(transactions.length * chunk * itemsToAdd)].feePerVsize);
 | 
			
		||||
      itemsToAdd--;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    arr.push(transactions[0].feePerVsize);
 | 
			
		||||
    return arr;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new MempoolBlocks();
 | 
			
		||||
@ -1,10 +1,10 @@
 | 
			
		||||
const config = require('../../mempool-config.json');
 | 
			
		||||
import bitcoinApi from './bitcoin/bitcoin-api-factory';
 | 
			
		||||
import { ITransaction, IMempoolInfo, IMempool } from '../interfaces';
 | 
			
		||||
import bitcoinApi from './bitcoin/electrs-api';
 | 
			
		||||
import { MempoolInfo, SimpleTransaction, Transaction } from '../interfaces';
 | 
			
		||||
 | 
			
		||||
class Mempool {
 | 
			
		||||
  private mempool: IMempool = {};
 | 
			
		||||
  private mempoolInfo: IMempoolInfo | undefined;
 | 
			
		||||
  private mempoolCache: any = {};
 | 
			
		||||
  private mempoolInfo: MempoolInfo | undefined;
 | 
			
		||||
  private mempoolChangedCallback: Function | undefined;
 | 
			
		||||
 | 
			
		||||
  private txPerSecondArray: number[] = [];
 | 
			
		||||
@ -21,15 +21,18 @@ class Mempool {
 | 
			
		||||
    this.mempoolChangedCallback = fn;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public getMempool(): { [txid: string]: ITransaction } {
 | 
			
		||||
    return this.mempool;
 | 
			
		||||
  public getMempool(): { [txid: string]: SimpleTransaction } {
 | 
			
		||||
    return this.mempoolCache;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public setMempool(mempoolData: any) {
 | 
			
		||||
    this.mempool = mempoolData;
 | 
			
		||||
    this.mempoolCache = mempoolData;
 | 
			
		||||
    if (this.mempoolChangedCallback && mempoolData) {
 | 
			
		||||
      this.mempoolChangedCallback(mempoolData);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public getMempoolInfo(): IMempoolInfo | undefined {
 | 
			
		||||
  public getMempoolInfo(): MempoolInfo | undefined {
 | 
			
		||||
    return this.mempoolInfo;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -41,52 +44,16 @@ class Mempool {
 | 
			
		||||
    return this.vBytesPerSecond;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async updateMemPoolInfo() {
 | 
			
		||||
  public async getRawTransaction(txId: string): Promise<SimpleTransaction | false> {
 | 
			
		||||
    try {
 | 
			
		||||
      this.mempoolInfo = await bitcoinApi.getMempoolInfo();
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      console.log('Error getMempoolInfo', err);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async getRawTransaction(txId: string, isCoinbase = false): Promise<ITransaction | false> {
 | 
			
		||||
    try {
 | 
			
		||||
      const transaction = await bitcoinApi.getRawTransaction(txId);
 | 
			
		||||
 | 
			
		||||
      let totalOut = 0;
 | 
			
		||||
      transaction.vout.forEach((output) => totalOut += output.value);
 | 
			
		||||
 | 
			
		||||
      if (config.BACKEND_API === 'electrs') {
 | 
			
		||||
        transaction.feePerWeightUnit = (transaction.fee * 100000000) / transaction.weight || 0;
 | 
			
		||||
        transaction.feePerVsize = (transaction.fee * 100000000) / (transaction.vsize) || 0;
 | 
			
		||||
        transaction.totalOut = totalOut / 100000000;
 | 
			
		||||
      } else {
 | 
			
		||||
        let totalIn = 0;
 | 
			
		||||
        if (!isCoinbase) {
 | 
			
		||||
          for (let i = 0; i < transaction.vin.length; i++) {
 | 
			
		||||
            try {
 | 
			
		||||
              const result = await bitcoinApi.getRawTransaction(transaction.vin[i].txid);
 | 
			
		||||
              transaction.vin[i]['value'] = result.vout[transaction.vin[i].vout].value;
 | 
			
		||||
              totalIn += result.vout[transaction.vin[i].vout].value;
 | 
			
		||||
            } catch (err) {
 | 
			
		||||
              console.log('Locating historical tx error');
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (totalIn > totalOut) {
 | 
			
		||||
          transaction.fee = parseFloat((totalIn - totalOut).toFixed(8));
 | 
			
		||||
          transaction.feePerWeightUnit = (transaction.fee * 100000000) / (transaction.vsize * 4) || 0;
 | 
			
		||||
          transaction.feePerVsize = (transaction.fee * 100000000) / (transaction.vsize) || 0;
 | 
			
		||||
        } else if (!isCoinbase) {
 | 
			
		||||
          transaction.fee = 0;
 | 
			
		||||
          transaction.feePerVsize = 0;
 | 
			
		||||
          transaction.feePerWeightUnit = 0;
 | 
			
		||||
          console.log('Minus fee error!');
 | 
			
		||||
        }
 | 
			
		||||
        transaction.totalOut = totalOut;
 | 
			
		||||
      }
 | 
			
		||||
      return transaction;
 | 
			
		||||
      const transaction: Transaction = await bitcoinApi.getRawTransaction(txId);
 | 
			
		||||
      return {
 | 
			
		||||
        txid: transaction.txid,
 | 
			
		||||
        fee: transaction.fee,
 | 
			
		||||
        size: transaction.size,
 | 
			
		||||
        vsize: transaction.weight / 4,
 | 
			
		||||
        feePerVsize: transaction.fee / (transaction.weight / 4)
 | 
			
		||||
      };
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      console.log(txId + ' not found');
 | 
			
		||||
      return false;
 | 
			
		||||
@ -100,12 +67,13 @@ class Mempool {
 | 
			
		||||
    let txCount = 0;
 | 
			
		||||
    try {
 | 
			
		||||
      const transactions = await bitcoinApi.getRawMempool();
 | 
			
		||||
      const diff = transactions.length - Object.keys(this.mempool).length;
 | 
			
		||||
      for (const tx of transactions) {
 | 
			
		||||
        if (!this.mempool[tx]) {
 | 
			
		||||
          const transaction = await this.getRawTransaction(tx);
 | 
			
		||||
      const diff = transactions.length - Object.keys(this.mempoolCache).length;
 | 
			
		||||
 | 
			
		||||
      for (const txid of transactions) {
 | 
			
		||||
        if (!this.mempoolCache[txid]) {
 | 
			
		||||
          const transaction = await this.getRawTransaction(txid);
 | 
			
		||||
          if (transaction) {
 | 
			
		||||
            this.mempool[tx] = transaction;
 | 
			
		||||
            this.mempoolCache[txid] = transaction;
 | 
			
		||||
            txCount++;
 | 
			
		||||
            this.txPerSecondArray.push(new Date().getTime());
 | 
			
		||||
            this.vBytesPerSecondArray.push({
 | 
			
		||||
@ -114,33 +82,34 @@ class Mempool {
 | 
			
		||||
            });
 | 
			
		||||
            hasChange = true;
 | 
			
		||||
            if (diff > 0) {
 | 
			
		||||
              console.log('Calculated fee for transaction ' + txCount + ' / ' + diff);
 | 
			
		||||
              console.log('Fetched transaction ' + txCount + ' / ' + diff);
 | 
			
		||||
            } else {
 | 
			
		||||
              console.log('Calculated fee for transaction ' + txCount);
 | 
			
		||||
              console.log('Fetched transaction ' + txCount);
 | 
			
		||||
            }
 | 
			
		||||
          } else {
 | 
			
		||||
            console.log('Error finding transaction in mempool.');
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if ((new Date().getTime()) - start > config.MEMPOOL_REFRESH_RATE_MS * 10) {
 | 
			
		||||
        if ((new Date().getTime()) - start > config.MEMPOOL_REFRESH_RATE_MS) {
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const newMempool: IMempool = {};
 | 
			
		||||
      // Replace mempool to clear already confirmed transactions
 | 
			
		||||
      const newMempool: any = {};
 | 
			
		||||
      transactions.forEach((tx) => {
 | 
			
		||||
        if (this.mempool[tx]) {
 | 
			
		||||
          newMempool[tx] = this.mempool[tx];
 | 
			
		||||
        if (this.mempoolCache[tx]) {
 | 
			
		||||
          newMempool[tx] = this.mempoolCache[tx];
 | 
			
		||||
        } else {
 | 
			
		||||
          hasChange = true;
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      this.mempool = newMempool;
 | 
			
		||||
      this.mempoolCache = newMempool;
 | 
			
		||||
 | 
			
		||||
      if (hasChange && this.mempoolChangedCallback) {
 | 
			
		||||
        this.mempoolChangedCallback(this.mempool);
 | 
			
		||||
        this.mempoolChangedCallback(this.mempoolCache);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const end = new Date().getTime();
 | 
			
		||||
 | 
			
		||||
@ -1,102 +0,0 @@
 | 
			
		||||
const config = require('../../mempool-config.json');
 | 
			
		||||
import { ITransaction, IProjectedBlock, IMempool, IProjectedBlockInternal } from '../interfaces';
 | 
			
		||||
 | 
			
		||||
class ProjectedBlocks {
 | 
			
		||||
  private transactionsSorted: ITransaction[] = [];
 | 
			
		||||
 | 
			
		||||
  constructor() {}
 | 
			
		||||
 | 
			
		||||
  public getProjectedBlockFeesForBlock(index: number) {
 | 
			
		||||
    const projectedBlock = this.getProjectedBlocksInternal()[index];
 | 
			
		||||
 | 
			
		||||
    if (!projectedBlock) {
 | 
			
		||||
      throw new Error('No projected block for that index');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return projectedBlock.txFeePerVsizes.map((fpv) => {
 | 
			
		||||
      return {'fpv': fpv};
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public updateProjectedBlocks(memPool: IMempool): void {
 | 
			
		||||
    const latestMempool = memPool;
 | 
			
		||||
    const memPoolArray: ITransaction[] = [];
 | 
			
		||||
    for (const i in latestMempool) {
 | 
			
		||||
      if (latestMempool.hasOwnProperty(i)) {
 | 
			
		||||
        memPoolArray.push(latestMempool[i]);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    memPoolArray.sort((a, b) => b.feePerWeightUnit - a.feePerWeightUnit);
 | 
			
		||||
    this.transactionsSorted = memPoolArray.filter((tx) => tx.feePerWeightUnit);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public getProjectedBlocks(txId?: string, numberOfBlocks: number = config.DEFAULT_PROJECTED_BLOCKS_AMOUNT): IProjectedBlock[] {
 | 
			
		||||
    return this.getProjectedBlocksInternal(numberOfBlocks).map((projectedBlock) => {
 | 
			
		||||
      return {
 | 
			
		||||
        blockSize: projectedBlock.blockSize,
 | 
			
		||||
        blockWeight: projectedBlock.blockWeight,
 | 
			
		||||
        nTx: projectedBlock.nTx,
 | 
			
		||||
        minFee: projectedBlock.minFee,
 | 
			
		||||
        maxFee: projectedBlock.maxFee,
 | 
			
		||||
        minWeightFee: projectedBlock.minWeightFee,
 | 
			
		||||
        maxWeightFee: projectedBlock.maxWeightFee,
 | 
			
		||||
        medianFee: projectedBlock.medianFee,
 | 
			
		||||
        fees: projectedBlock.fees,
 | 
			
		||||
        hasMytx: txId ? projectedBlock.txIds.some((tx) => tx === txId) : false
 | 
			
		||||
      };
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private getProjectedBlocksInternal(numberOfBlocks: number = config.DEFAULT_PROJECTED_BLOCKS_AMOUNT): IProjectedBlockInternal[] {
 | 
			
		||||
    const projectedBlocks: IProjectedBlockInternal[] = [];
 | 
			
		||||
    let blockWeight = 0;
 | 
			
		||||
    let blockSize = 0;
 | 
			
		||||
    let transactions: ITransaction[] = [];
 | 
			
		||||
    this.transactionsSorted.forEach((tx) => {
 | 
			
		||||
      if (blockWeight + tx.vsize * 4 < 4000000 || projectedBlocks.length === numberOfBlocks) {
 | 
			
		||||
        blockWeight += tx.weight || tx.vsize * 4;
 | 
			
		||||
        blockSize += tx.size;
 | 
			
		||||
        transactions.push(tx);
 | 
			
		||||
      } else {
 | 
			
		||||
        projectedBlocks.push(this.dataToProjectedBlock(transactions, blockSize, blockWeight));
 | 
			
		||||
        blockWeight = 0;
 | 
			
		||||
        blockSize = 0;
 | 
			
		||||
        transactions = [];
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    if (transactions.length) {
 | 
			
		||||
      projectedBlocks.push(this.dataToProjectedBlock(transactions, blockSize, blockWeight));
 | 
			
		||||
    }
 | 
			
		||||
    return projectedBlocks;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private dataToProjectedBlock(transactions: ITransaction[], blockSize: number, blockWeight: number): IProjectedBlockInternal {
 | 
			
		||||
    return {
 | 
			
		||||
      blockSize: blockSize,
 | 
			
		||||
      blockWeight: blockWeight,
 | 
			
		||||
      nTx: transactions.length,
 | 
			
		||||
      minFee: transactions[transactions.length - 1].feePerVsize,
 | 
			
		||||
      maxFee: transactions[0].feePerVsize,
 | 
			
		||||
      minWeightFee: transactions[transactions.length - 1].feePerWeightUnit,
 | 
			
		||||
      maxWeightFee: transactions[0].feePerWeightUnit,
 | 
			
		||||
      medianFee: this.median(transactions.map((tx) => tx.feePerVsize)),
 | 
			
		||||
      txIds: transactions.map((tx) => tx.txid),
 | 
			
		||||
      txFeePerVsizes: transactions.map((tx) => tx.feePerVsize).reverse(),
 | 
			
		||||
      fees: transactions.map((tx) => tx.fee).reduce((acc, currValue) => acc + currValue),
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private median(numbers: number[]) {
 | 
			
		||||
    let medianNr = 0;
 | 
			
		||||
    const numsLen = numbers.length;
 | 
			
		||||
    numbers.sort();
 | 
			
		||||
    if (numsLen % 2 === 0) {
 | 
			
		||||
        medianNr = (numbers[numsLen / 2 - 1] + numbers[numsLen / 2]) / 2;
 | 
			
		||||
    } else {
 | 
			
		||||
        medianNr = numbers[(numsLen - 1) / 2];
 | 
			
		||||
    }
 | 
			
		||||
    return medianNr;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new ProjectedBlocks();
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import memPool from './mempool';
 | 
			
		||||
import { DB } from '../database';
 | 
			
		||||
 | 
			
		||||
import { ITransaction, IMempoolStats } from '../interfaces';
 | 
			
		||||
import { Statistic, SimpleTransaction } from '../interfaces';
 | 
			
		||||
 | 
			
		||||
class Statistics {
 | 
			
		||||
  protected intervalTimer: NodeJS.Timer | undefined;
 | 
			
		||||
@ -37,42 +37,28 @@ class Statistics {
 | 
			
		||||
 | 
			
		||||
    console.log('Running statistics');
 | 
			
		||||
 | 
			
		||||
    let memPoolArray: ITransaction[] = [];
 | 
			
		||||
    let memPoolArray: SimpleTransaction[] = [];
 | 
			
		||||
    for (const i in currentMempool) {
 | 
			
		||||
      if (currentMempool.hasOwnProperty(i)) {
 | 
			
		||||
        memPoolArray.push(currentMempool[i]);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    // Remove 0 and undefined
 | 
			
		||||
    memPoolArray = memPoolArray.filter((tx) => tx.feePerWeightUnit);
 | 
			
		||||
    memPoolArray = memPoolArray.filter((tx) => tx.feePerVsize);
 | 
			
		||||
 | 
			
		||||
    if (!memPoolArray.length) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    memPoolArray.sort((a, b) => a.feePerWeightUnit - b.feePerWeightUnit);
 | 
			
		||||
    memPoolArray.sort((a, b) => a.feePerVsize - b.feePerVsize);
 | 
			
		||||
    const totalWeight = memPoolArray.map((tx) => tx.vsize).reduce((acc, curr) => acc + curr) * 4;
 | 
			
		||||
    const totalFee = memPoolArray.map((tx) => tx.fee).reduce((acc, curr) => acc + curr);
 | 
			
		||||
 | 
			
		||||
    const logFees = [1, 2, 3, 4, 5, 6, 8, 10, 12, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100, 125, 150, 175, 200,
 | 
			
		||||
      250, 300, 350, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000];
 | 
			
		||||
 | 
			
		||||
    const weightUnitFees: { [feePerWU: number]: number } = {};
 | 
			
		||||
    const weightVsizeFees: { [feePerWU: number]: number } = {};
 | 
			
		||||
 | 
			
		||||
    memPoolArray.forEach((transaction) => {
 | 
			
		||||
      for (let i = 0; i < logFees.length; i++) {
 | 
			
		||||
        if ((logFees[i] === 2000 && transaction.feePerWeightUnit >= 2000) || transaction.feePerWeightUnit <= logFees[i]) {
 | 
			
		||||
          if (weightUnitFees[logFees[i]]) {
 | 
			
		||||
            weightUnitFees[logFees[i]] += transaction.vsize * 4;
 | 
			
		||||
          } else {
 | 
			
		||||
            weightUnitFees[logFees[i]] = transaction.vsize * 4;
 | 
			
		||||
          }
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    memPoolArray.forEach((transaction) => {
 | 
			
		||||
      for (let i = 0; i < logFees.length; i++) {
 | 
			
		||||
        if ((logFees[i] === 2000 && transaction.feePerVsize >= 2000) || transaction.feePerVsize <= logFees[i]) {
 | 
			
		||||
@ -93,10 +79,7 @@ class Statistics {
 | 
			
		||||
      vbytes_per_second: Math.round(vBytesPerSecond),
 | 
			
		||||
      mempool_byte_weight: totalWeight,
 | 
			
		||||
      total_fee: totalFee,
 | 
			
		||||
      fee_data: JSON.stringify({
 | 
			
		||||
        'wu': weightUnitFees,
 | 
			
		||||
        'vsize': weightVsizeFees
 | 
			
		||||
      }),
 | 
			
		||||
      fee_data: '',
 | 
			
		||||
      vsize_1: weightVsizeFees['1'] || 0,
 | 
			
		||||
      vsize_2: weightVsizeFees['2'] || 0,
 | 
			
		||||
      vsize_3: weightVsizeFees['3'] || 0,
 | 
			
		||||
@ -143,7 +126,7 @@ class Statistics {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $create(statistics: IMempoolStats): Promise<number | undefined> {
 | 
			
		||||
  private async $create(statistics: Statistic): Promise<number | undefined> {
 | 
			
		||||
    try {
 | 
			
		||||
      const connection = await DB.pool.getConnection();
 | 
			
		||||
      const query = `INSERT INTO statistics(
 | 
			
		||||
@ -295,7 +278,7 @@ class Statistics {
 | 
			
		||||
            AVG(vsize_2000) AS vsize_2000 FROM statistics GROUP BY UNIX_TIMESTAMP(added) DIV ${groupBy} ORDER BY id DESC LIMIT ${days}`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $get(id: number): Promise<IMempoolStats | undefined> {
 | 
			
		||||
  public async $get(id: number): Promise<Statistic | undefined> {
 | 
			
		||||
    try {
 | 
			
		||||
      const connection = await DB.pool.getConnection();
 | 
			
		||||
      const query = `SELECT * FROM statistics WHERE id = ?`;
 | 
			
		||||
@ -307,7 +290,7 @@ class Statistics {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $list2H(): Promise<IMempoolStats[]> {
 | 
			
		||||
  public async $list2H(): Promise<Statistic[]> {
 | 
			
		||||
    try {
 | 
			
		||||
      const connection = await DB.pool.getConnection();
 | 
			
		||||
      const query = `SELECT * FROM statistics ORDER BY id DESC LIMIT 120`;
 | 
			
		||||
@ -320,7 +303,7 @@ class Statistics {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $list24H(): Promise<IMempoolStats[]> {
 | 
			
		||||
  public async $list24H(): Promise<Statistic[]> {
 | 
			
		||||
    try {
 | 
			
		||||
      const connection = await DB.pool.getConnection();
 | 
			
		||||
      const query = this.getQueryForDays(120, 720);
 | 
			
		||||
@ -332,7 +315,7 @@ class Statistics {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $list1W(): Promise<IMempoolStats[]> {
 | 
			
		||||
  public async $list1W(): Promise<Statistic[]> {
 | 
			
		||||
    try {
 | 
			
		||||
      const connection = await DB.pool.getConnection();
 | 
			
		||||
      const query = this.getQueryForDays(120, 5040);
 | 
			
		||||
@ -345,7 +328,7 @@ class Statistics {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $list1M(): Promise<IMempoolStats[]> {
 | 
			
		||||
  public async $list1M(): Promise<Statistic[]> {
 | 
			
		||||
    try {
 | 
			
		||||
      const connection = await DB.pool.getConnection();
 | 
			
		||||
      const query = this.getQueryForDays(120, 20160);
 | 
			
		||||
@ -358,7 +341,7 @@ class Statistics {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $list3M(): Promise<IMempoolStats[]> {
 | 
			
		||||
  public async $list3M(): Promise<Statistic[]> {
 | 
			
		||||
    try {
 | 
			
		||||
      const connection = await DB.pool.getConnection();
 | 
			
		||||
      const query = this.getQueryForDays(120, 60480);
 | 
			
		||||
@ -371,7 +354,7 @@ class Statistics {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $list6M(): Promise<IMempoolStats[]> {
 | 
			
		||||
  public async $list6M(): Promise<Statistic[]> {
 | 
			
		||||
    try {
 | 
			
		||||
      const connection = await DB.pool.getConnection();
 | 
			
		||||
      const query = this.getQueryForDays(120, 120960);
 | 
			
		||||
 | 
			
		||||
@ -6,40 +6,42 @@ import * as http from 'http';
 | 
			
		||||
import * as https from 'https';
 | 
			
		||||
import * as WebSocket from 'ws';
 | 
			
		||||
 | 
			
		||||
import bitcoinApi from './api/bitcoin/bitcoin-api-factory';
 | 
			
		||||
import diskCache from './api/disk-cache';
 | 
			
		||||
import memPool from './api/mempool';
 | 
			
		||||
import blocks from './api/blocks';
 | 
			
		||||
import projectedBlocks from './api/projected-blocks';
 | 
			
		||||
import statistics from './api/statistics';
 | 
			
		||||
import { IBlock, IMempool, ITransaction, IMempoolStats } from './interfaces';
 | 
			
		||||
 | 
			
		||||
import routes from './routes';
 | 
			
		||||
import blocks from './api/blocks';
 | 
			
		||||
import memPool from './api/mempool';
 | 
			
		||||
import mempoolBlocks from './api/mempool-blocks';
 | 
			
		||||
import diskCache from './api/disk-cache';
 | 
			
		||||
import statistics from './api/statistics';
 | 
			
		||||
 | 
			
		||||
import { Block, SimpleTransaction, Statistic } from './interfaces';
 | 
			
		||||
 | 
			
		||||
import fiatConversion from './api/fiat-conversion';
 | 
			
		||||
 | 
			
		||||
class MempoolSpace {
 | 
			
		||||
class Server {
 | 
			
		||||
  private wss: WebSocket.Server;
 | 
			
		||||
  private server: https.Server | http.Server;
 | 
			
		||||
  private app: any;
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
    this.app = express();
 | 
			
		||||
 | 
			
		||||
    this.app
 | 
			
		||||
      .use((req, res, next)  => {
 | 
			
		||||
      .use((req, res, next) => {
 | 
			
		||||
        res.setHeader('Access-Control-Allow-Origin', '*');
 | 
			
		||||
        next();
 | 
			
		||||
      })
 | 
			
		||||
      .use(compression());
 | 
			
		||||
    if (config.ENV === 'dev') {
 | 
			
		||||
      this.server = http.createServer(this.app);
 | 
			
		||||
      this.wss = new WebSocket.Server({ server: this.server });
 | 
			
		||||
    } else {
 | 
			
		||||
 | 
			
		||||
    if (config.SSL === true) {
 | 
			
		||||
      const credentials = {
 | 
			
		||||
        cert: fs.readFileSync('/etc/letsencrypt/live/mempool.space/fullchain.pem'),
 | 
			
		||||
        key: fs.readFileSync('/etc/letsencrypt/live/mempool.space/privkey.pem'),
 | 
			
		||||
        cert: fs.readFileSync(config.SSL_CERT_FILE_PATH),
 | 
			
		||||
        key: fs.readFileSync(config.SSL_KEY_FILE_PATH),
 | 
			
		||||
      };
 | 
			
		||||
      this.server = https.createServer(credentials, this.app);
 | 
			
		||||
      this.wss = new WebSocket.Server({ server: this.server });
 | 
			
		||||
    } else {
 | 
			
		||||
      this.server = http.createServer(this.app);
 | 
			
		||||
      this.wss = new WebSocket.Server({ server: this.server });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this.setUpRoutes();
 | 
			
		||||
@ -50,20 +52,15 @@ class MempoolSpace {
 | 
			
		||||
    statistics.startStatistics();
 | 
			
		||||
    fiatConversion.startService();
 | 
			
		||||
 | 
			
		||||
    const opts = {
 | 
			
		||||
        host: '127.0.0.1',
 | 
			
		||||
        port: 8999
 | 
			
		||||
    };
 | 
			
		||||
    this.server.listen(opts, () => {
 | 
			
		||||
      console.log(`Server started on ${opts.host}:${opts.port}`);
 | 
			
		||||
    this.server.listen(config.HTTP_PORT, () => {
 | 
			
		||||
      console.log(`Server started on port ${config.HTTP_PORT}`);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async runMempoolIntervalFunctions() {
 | 
			
		||||
    await blocks.updateBlocks();
 | 
			
		||||
    await memPool.updateMemPoolInfo();
 | 
			
		||||
    await memPool.updateMempool();
 | 
			
		||||
    setTimeout(this.runMempoolIntervalFunctions.bind(this), config.MEMPOOL_REFRESH_RATE_MS);
 | 
			
		||||
    setTimeout(this.runMempoolIntervalFunctions.bind(this), config.ELECTRS_POLL_RATE_MS);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private setUpMempoolCache() {
 | 
			
		||||
@ -81,161 +78,36 @@ class MempoolSpace {
 | 
			
		||||
 | 
			
		||||
  private setUpWebsocketHandling() {
 | 
			
		||||
    this.wss.on('connection', (client: WebSocket) => {
 | 
			
		||||
      let theBlocks = blocks.getBlocks();
 | 
			
		||||
      theBlocks = theBlocks.concat([]).splice(theBlocks.length - config.INITIAL_BLOCK_AMOUNT);
 | 
			
		||||
      const formatedBlocks = theBlocks.map((b) => blocks.formatBlock(b));
 | 
			
		||||
 | 
			
		||||
      client.send(JSON.stringify({
 | 
			
		||||
        'mempoolInfo': memPool.getMempoolInfo(),
 | 
			
		||||
        'blocks': formatedBlocks,
 | 
			
		||||
        'projectedBlocks': projectedBlocks.getProjectedBlocks(),
 | 
			
		||||
        'txPerSecond': memPool.getTxPerSecond(),
 | 
			
		||||
        'vBytesPerSecond': memPool.getVBytesPerSecond(),
 | 
			
		||||
        'conversions': fiatConversion.getTickers()['BTCUSD'],
 | 
			
		||||
      }));
 | 
			
		||||
 | 
			
		||||
      client.on('message', async (message: any) => {
 | 
			
		||||
      client.on('message', (message: any) => {
 | 
			
		||||
        try {
 | 
			
		||||
          const parsedMessage = JSON.parse(message);
 | 
			
		||||
 | 
			
		||||
          if (parsedMessage.action === 'want') {
 | 
			
		||||
            client['want-stats'] = parsedMessage.data.indexOf('stats') > -1;
 | 
			
		||||
            client['want-blocks'] = parsedMessage.data.indexOf('blocks') > -1;
 | 
			
		||||
            client['want-projected-blocks'] = parsedMessage.data.indexOf('projected-blocks') > -1;
 | 
			
		||||
            client['want-live-2h-chart'] = parsedMessage.data.indexOf('live-2h-chart') > -1;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          if (parsedMessage.action === 'track-tx' && parsedMessage.txId && /^[a-fA-F0-9]{64}$/.test(parsedMessage.txId)) {
 | 
			
		||||
            const tx = await memPool.getRawTransaction(parsedMessage.txId);
 | 
			
		||||
            if (tx) {
 | 
			
		||||
              console.log('Now tracking: ' + parsedMessage.txId);
 | 
			
		||||
              client['trackingTx'] = true;
 | 
			
		||||
              client['txId'] = parsedMessage.txId;
 | 
			
		||||
              client['tx'] = tx;
 | 
			
		||||
 | 
			
		||||
              if (tx.blockhash) {
 | 
			
		||||
                const currentBlocks = blocks.getBlocks();
 | 
			
		||||
                const foundBlock = currentBlocks.find((block) => block.tx && block.tx.some((i: string) => i === parsedMessage.txId));
 | 
			
		||||
                if (foundBlock) {
 | 
			
		||||
                  console.log('Found block by looking in local cache');
 | 
			
		||||
                  client['blockHeight'] = foundBlock.height;
 | 
			
		||||
                } else {
 | 
			
		||||
                  const theBlock = await bitcoinApi.getBlockAndTransactions(tx.blockhash);
 | 
			
		||||
                  if (theBlock) {
 | 
			
		||||
                    client['blockHeight'] = theBlock.height;
 | 
			
		||||
                  }
 | 
			
		||||
                }
 | 
			
		||||
              } else {
 | 
			
		||||
                client['blockHeight'] = 0;
 | 
			
		||||
              }
 | 
			
		||||
              client.send(JSON.stringify({
 | 
			
		||||
                'projectedBlocks': projectedBlocks.getProjectedBlocks(client['txId']),
 | 
			
		||||
                'track-tx': {
 | 
			
		||||
                  tracking: true,
 | 
			
		||||
                  blockHeight: client['blockHeight'],
 | 
			
		||||
                  tx: client['tx'],
 | 
			
		||||
                }
 | 
			
		||||
              }));
 | 
			
		||||
            } else {
 | 
			
		||||
              console.log('TX NOT FOUND, NOT TRACKING');
 | 
			
		||||
              client['trackingTx'] = false;
 | 
			
		||||
              client['blockHeight'] = 0;
 | 
			
		||||
              client['tx'] = null;
 | 
			
		||||
              client.send(JSON.stringify({
 | 
			
		||||
                'track-tx': {
 | 
			
		||||
                  tracking: false,
 | 
			
		||||
                  blockHeight: 0,
 | 
			
		||||
                  message: 'not-found',
 | 
			
		||||
                }
 | 
			
		||||
              }));
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          if (parsedMessage.action === 'stop-tracking-tx') {
 | 
			
		||||
            console.log('STOP TRACKING');
 | 
			
		||||
            client['trackingTx'] = false;
 | 
			
		||||
            client.send(JSON.stringify({
 | 
			
		||||
              'track-tx': {
 | 
			
		||||
                tracking: false,
 | 
			
		||||
                blockHeight: 0,
 | 
			
		||||
                message: 'not-found',
 | 
			
		||||
              }
 | 
			
		||||
            }));
 | 
			
		||||
          if (parsedMessage && parsedMessage.txId && /^[a-fA-F0-9]{64}$/.test(parsedMessage.txId)) {
 | 
			
		||||
            client['txId'] = parsedMessage.txId;
 | 
			
		||||
          }
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
          console.log(e);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      client.on('close', () => {
 | 
			
		||||
        client['trackingTx'] = false;
 | 
			
		||||
      });
 | 
			
		||||
      const _blocks = blocks.getBlocks();
 | 
			
		||||
      if (!_blocks) {
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      client.send(JSON.stringify({
 | 
			
		||||
        'blocks': _blocks,
 | 
			
		||||
        'conversions': fiatConversion.getTickers()['BTCUSD'],
 | 
			
		||||
        'mempool-blocks': mempoolBlocks.getMempoolBlocks(),
 | 
			
		||||
      }));
 | 
			
		||||
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    blocks.setNewBlockCallback((block: IBlock) => {
 | 
			
		||||
      const formattedBlocks = blocks.formatBlock(block);
 | 
			
		||||
 | 
			
		||||
      this.wss.clients.forEach((client) => {
 | 
			
		||||
        if (client.readyState !== WebSocket.OPEN) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const response = {};
 | 
			
		||||
 | 
			
		||||
        if (client['trackingTx'] === true && client['blockHeight'] === 0) {
 | 
			
		||||
          if (block.tx.some((tx: ITransaction) => tx === client['txId'])) {
 | 
			
		||||
            client['blockHeight'] = block.height;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        response['track-tx'] = {
 | 
			
		||||
          tracking: client['trackingTx'] || false,
 | 
			
		||||
          blockHeight: client['blockHeight'],
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        response['block'] = formattedBlocks;
 | 
			
		||||
 | 
			
		||||
        client.send(JSON.stringify(response));
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    memPool.setMempoolChangedCallback((newMempool: IMempool) => {
 | 
			
		||||
      projectedBlocks.updateProjectedBlocks(newMempool);
 | 
			
		||||
 | 
			
		||||
      const pBlocks = projectedBlocks.getProjectedBlocks();
 | 
			
		||||
      const mempoolInfo = memPool.getMempoolInfo();
 | 
			
		||||
      const txPerSecond = memPool.getTxPerSecond();
 | 
			
		||||
      const vBytesPerSecond = memPool.getVBytesPerSecond();
 | 
			
		||||
 | 
			
		||||
      this.wss.clients.forEach((client: WebSocket) => {
 | 
			
		||||
        if (client.readyState !== WebSocket.OPEN) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const response = {};
 | 
			
		||||
 | 
			
		||||
        if (client['want-stats']) {
 | 
			
		||||
          response['mempoolInfo'] = mempoolInfo;
 | 
			
		||||
          response['txPerSecond'] = txPerSecond;
 | 
			
		||||
          response['vBytesPerSecond'] = vBytesPerSecond;
 | 
			
		||||
          response['track-tx'] = {
 | 
			
		||||
            tracking: client['trackingTx'] || false,
 | 
			
		||||
            blockHeight: client['blockHeight'],
 | 
			
		||||
          };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (client['want-projected-blocks'] && client['trackingTx'] && client['blockHeight'] === 0) {
 | 
			
		||||
          response['projectedBlocks'] = projectedBlocks.getProjectedBlocks(client['txId']);
 | 
			
		||||
        } else if (client['want-projected-blocks']) {
 | 
			
		||||
          response['projectedBlocks'] = pBlocks;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (Object.keys(response).length) {
 | 
			
		||||
          client.send(JSON.stringify(response));
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    statistics.setNewStatisticsEntryCallback((stats: IMempoolStats) => {
 | 
			
		||||
    statistics.setNewStatisticsEntryCallback((stats: Statistic) => {
 | 
			
		||||
      this.wss.clients.forEach((client: WebSocket) => {
 | 
			
		||||
        if (client.readyState !== WebSocket.OPEN) {
 | 
			
		||||
          return;
 | 
			
		||||
@ -248,14 +120,47 @@ class MempoolSpace {
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    blocks.setNewBlockCallback((block: Block, txIds: string[]) => {
 | 
			
		||||
      this.wss.clients.forEach((client) => {
 | 
			
		||||
        if (client.readyState !== WebSocket.OPEN) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (client['txId'] && txIds.indexOf(client['txId']) > -1) {
 | 
			
		||||
          client['txId'] = null;
 | 
			
		||||
          client.send(JSON.stringify({
 | 
			
		||||
            'block': block,
 | 
			
		||||
            'txConfirmed': true,
 | 
			
		||||
          }));
 | 
			
		||||
        } else {
 | 
			
		||||
          client.send(JSON.stringify({
 | 
			
		||||
            'block': block,
 | 
			
		||||
          }));
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    memPool.setMempoolChangedCallback((newMempool: { [txid: string]: SimpleTransaction }) => {
 | 
			
		||||
      mempoolBlocks.updateMempoolBlocks(newMempool);
 | 
			
		||||
      const pBlocks = mempoolBlocks.getMempoolBlocks();
 | 
			
		||||
 | 
			
		||||
      this.wss.clients.forEach((client: WebSocket) => {
 | 
			
		||||
        if (client.readyState !== WebSocket.OPEN) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        client.send(JSON.stringify({
 | 
			
		||||
          'mempool-blocks': pBlocks
 | 
			
		||||
        }));
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private setUpRoutes() {
 | 
			
		||||
    this.app
 | 
			
		||||
      .get(config.API_ENDPOINT + 'transactions/height/:id', routes.$getgetTransactionsForBlock)
 | 
			
		||||
      .get(config.API_ENDPOINT + 'transactions/projected/:id', routes.getgetTransactionsForProjectedBlock)
 | 
			
		||||
      .get(config.API_ENDPOINT + 'fees/recommended', routes.getRecommendedFees)
 | 
			
		||||
      .get(config.API_ENDPOINT + 'fees/projected-blocks', routes.getProjectedBlocks)
 | 
			
		||||
      .get(config.API_ENDPOINT + 'fees/mempool-blocks', routes.getMempoolBlocks)
 | 
			
		||||
      .get(config.API_ENDPOINT + 'statistics/2h', routes.get2HStatistics)
 | 
			
		||||
      .get(config.API_ENDPOINT + 'statistics/24h', routes.get24HStatistics.bind(routes))
 | 
			
		||||
      .get(config.API_ENDPOINT + 'statistics/1w', routes.get1WHStatistics.bind(routes))
 | 
			
		||||
@ -263,22 +168,7 @@ class MempoolSpace {
 | 
			
		||||
      .get(config.API_ENDPOINT + 'statistics/3m', routes.get3MStatistics.bind(routes))
 | 
			
		||||
      .get(config.API_ENDPOINT + 'statistics/6m', routes.get6MStatistics.bind(routes))
 | 
			
		||||
      ;
 | 
			
		||||
 | 
			
		||||
    if (config.BACKEND_API === 'electrs') {
 | 
			
		||||
      this.app
 | 
			
		||||
        .get(config.API_ENDPOINT + 'explorer/blocks', routes.getBlocks)
 | 
			
		||||
        .get(config.API_ENDPOINT + 'explorer/blocks/:height', routes.getBlocks)
 | 
			
		||||
        .get(config.API_ENDPOINT + 'explorer/tx/:id', routes.getRawTransaction)
 | 
			
		||||
        .get(config.API_ENDPOINT + 'explorer/block/:hash', routes.getBlock)
 | 
			
		||||
        .get(config.API_ENDPOINT + 'explorer/block/:hash/tx', routes.getBlockTransactions)
 | 
			
		||||
        .get(config.API_ENDPOINT + 'explorer/block/:hash/tx/:index', routes.getBlockTransactionsFromIndex)
 | 
			
		||||
        .get(config.API_ENDPOINT + 'explorer/address/:address', routes.getAddress)
 | 
			
		||||
        .get(config.API_ENDPOINT + 'explorer/address/:address/tx', routes.getAddressTransactions)
 | 
			
		||||
        .get(config.API_ENDPOINT + 'explorer/address/:address/tx/chain/:txid', routes.getAddressTransactionsFromTxid)
 | 
			
		||||
        ;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
const mempoolSpace = new MempoolSpace();
 | 
			
		||||
const server = new Server();
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
export interface IMempoolInfo {
 | 
			
		||||
export interface MempoolInfo {
 | 
			
		||||
  size: number;
 | 
			
		||||
  bytes: number;
 | 
			
		||||
  usage?: number;
 | 
			
		||||
@ -7,80 +7,110 @@ export interface IMempoolInfo {
 | 
			
		||||
  minrelaytxfee?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ITransaction {
 | 
			
		||||
export interface MempoolBlock {
 | 
			
		||||
  blockSize: number;
 | 
			
		||||
  blockVSize: number;
 | 
			
		||||
  nTx: number;
 | 
			
		||||
  medianFee: number;
 | 
			
		||||
  feeRange: number[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Transaction {
 | 
			
		||||
  txid: string;
 | 
			
		||||
  hash: string;
 | 
			
		||||
  version: number;
 | 
			
		||||
  size: number;
 | 
			
		||||
  vsize: number;
 | 
			
		||||
  weight: number;
 | 
			
		||||
  locktime: number;
 | 
			
		||||
  fee: number;
 | 
			
		||||
  size: number;
 | 
			
		||||
  weight: number;
 | 
			
		||||
  vin: Vin[];
 | 
			
		||||
  vout: Vout[];
 | 
			
		||||
  hex: string;
 | 
			
		||||
  status: Status;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface SimpleTransaction {
 | 
			
		||||
  txid: string;
 | 
			
		||||
  fee: number;
 | 
			
		||||
  feePerWeightUnit: number;
 | 
			
		||||
  feePerVsize: number;
 | 
			
		||||
  blockhash?: string;
 | 
			
		||||
  confirmations?: number;
 | 
			
		||||
  time?: number;
 | 
			
		||||
  blocktime?: number;
 | 
			
		||||
  totalOut?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IBlock {
 | 
			
		||||
  hash: string;
 | 
			
		||||
  confirmations: number;
 | 
			
		||||
  strippedsize: number;
 | 
			
		||||
  size: number;
 | 
			
		||||
  weight: number;
 | 
			
		||||
  height: number;
 | 
			
		||||
  version: number;
 | 
			
		||||
  versionHex: string;
 | 
			
		||||
  merkleroot: string;
 | 
			
		||||
  tx: any;
 | 
			
		||||
  time: number;
 | 
			
		||||
  mediantime: number;
 | 
			
		||||
  nonce: number;
 | 
			
		||||
  bits: string;
 | 
			
		||||
  difficulty: number;
 | 
			
		||||
  chainwork: string;
 | 
			
		||||
  nTx: number;
 | 
			
		||||
  previousblockhash: string;
 | 
			
		||||
  fees: number;
 | 
			
		||||
 | 
			
		||||
  minFee?: number;
 | 
			
		||||
  maxFee?: number;
 | 
			
		||||
  medianFee?: number;
 | 
			
		||||
  vsize: number;
 | 
			
		||||
  feePerVsize: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface ScriptSig {
 | 
			
		||||
  asm: string;
 | 
			
		||||
  hex: string;
 | 
			
		||||
export interface Prevout {
 | 
			
		||||
  scriptpubkey: string;
 | 
			
		||||
  scriptpubkey_asm: string;
 | 
			
		||||
  scriptpubkey_type: string;
 | 
			
		||||
  scriptpubkey_address: string;
 | 
			
		||||
  value: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface Vin {
 | 
			
		||||
export interface Vin {
 | 
			
		||||
  txid: string;
 | 
			
		||||
  vout: number;
 | 
			
		||||
  scriptSig: ScriptSig;
 | 
			
		||||
  sequence: number;
 | 
			
		||||
  prevout: Prevout;
 | 
			
		||||
  scriptsig: string;
 | 
			
		||||
  scriptsig_asm: string;
 | 
			
		||||
  inner_redeemscript_asm?: string;
 | 
			
		||||
  is_coinbase: boolean;
 | 
			
		||||
  sequence: any;
 | 
			
		||||
  witness?: string[];
 | 
			
		||||
  inner_witnessscript_asm?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface ScriptPubKey {
 | 
			
		||||
  asm: string;
 | 
			
		||||
  hex: string;
 | 
			
		||||
  reqSigs: number;
 | 
			
		||||
  type: string;
 | 
			
		||||
  addresses: string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface Vout {
 | 
			
		||||
export interface Vout {
 | 
			
		||||
  scriptpubkey: string;
 | 
			
		||||
  scriptpubkey_asm: string;
 | 
			
		||||
  scriptpubkey_type: string;
 | 
			
		||||
  scriptpubkey_address: string;
 | 
			
		||||
  value: number;
 | 
			
		||||
  n: number;
 | 
			
		||||
  scriptPubKey: ScriptPubKey;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IMempoolStats {
 | 
			
		||||
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;
 | 
			
		||||
  tx_count: number;
 | 
			
		||||
  size: number;
 | 
			
		||||
  weight: number;
 | 
			
		||||
  merkle_root: string;
 | 
			
		||||
  previousblockhash: string;
 | 
			
		||||
  nonce: any;
 | 
			
		||||
  bits: number;
 | 
			
		||||
 | 
			
		||||
  medianFee?: number;
 | 
			
		||||
  feeRange?: number[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Address {
 | 
			
		||||
  address: string;
 | 
			
		||||
  chain_stats: ChainStats;
 | 
			
		||||
  mempool_stats: MempoolStats;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 Statistic {
 | 
			
		||||
  id?: number;
 | 
			
		||||
  added: string;
 | 
			
		||||
  unconfirmed_transactions: number;
 | 
			
		||||
@ -130,23 +160,10 @@ export interface IMempoolStats {
 | 
			
		||||
  vsize_2000: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IProjectedBlockInternal extends IProjectedBlock {
 | 
			
		||||
  txIds: string[];
 | 
			
		||||
  txFeePerVsizes: number[];
 | 
			
		||||
export interface Outspend {
 | 
			
		||||
  spent: boolean;
 | 
			
		||||
  txid: string;
 | 
			
		||||
  vin: number;
 | 
			
		||||
  status: Status;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IProjectedBlock {
 | 
			
		||||
  blockSize: number;
 | 
			
		||||
  blockWeight: number;
 | 
			
		||||
  maxFee: number;
 | 
			
		||||
  maxWeightFee: number;
 | 
			
		||||
  medianFee: number;
 | 
			
		||||
  minFee: number;
 | 
			
		||||
  minWeightFee: number;
 | 
			
		||||
  nTx: number;
 | 
			
		||||
  fees: number;
 | 
			
		||||
  hasMyTxId?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IMempool { [txid: string]: ITransaction; }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,6 @@
 | 
			
		||||
import statistics from './api/statistics';
 | 
			
		||||
import feeApi from './api/fee-api';
 | 
			
		||||
import projectedBlocks from './api/projected-blocks';
 | 
			
		||||
import bitcoinApi from './api/bitcoin/bitcoin-api-factory';
 | 
			
		||||
import mempoolBlocks from './api/mempool-blocks';
 | 
			
		||||
 | 
			
		||||
class Routes {
 | 
			
		||||
  private cache = {};
 | 
			
		||||
@ -50,149 +49,14 @@ class Routes {
 | 
			
		||||
    res.send(result);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $getgetTransactionsForBlock(req, res) {
 | 
			
		||||
    const result = await feeApi.$getTransactionsForBlock(req.params.id);
 | 
			
		||||
    res.send(result);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async getgetTransactionsForProjectedBlock(req, res) {
 | 
			
		||||
  public async getMempoolBlocks(req, res) {
 | 
			
		||||
    try {
 | 
			
		||||
      const result = await projectedBlocks.getProjectedBlockFeesForBlock(req.params.id);
 | 
			
		||||
      const result = await mempoolBlocks.getMempoolBlocks();
 | 
			
		||||
      res.send(result);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      res.status(500).send(e.message);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async getProjectedBlocks(req, res) {
 | 
			
		||||
    try {
 | 
			
		||||
      let txId: string | undefined;
 | 
			
		||||
      if (req.query.txId && /^[a-fA-F0-9]{64}$/.test(req.query.txId)) {
 | 
			
		||||
        txId = req.query.txId;
 | 
			
		||||
      }
 | 
			
		||||
      const result = await projectedBlocks.getProjectedBlocks(txId, 6);
 | 
			
		||||
      res.send(result);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      res.status(500).send(e.message);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async getBlocks(req, res) {
 | 
			
		||||
    try {
 | 
			
		||||
      let result: string;
 | 
			
		||||
      if (req.params.height) {
 | 
			
		||||
        result = await bitcoinApi.getBlocksFromHeight(req.params.height);
 | 
			
		||||
      } else {
 | 
			
		||||
        result = await bitcoinApi.getBlocks();
 | 
			
		||||
      }
 | 
			
		||||
      res.send(result);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      res.status(500).send(e.message);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async getRawTransaction(req, res) {
 | 
			
		||||
    try {
 | 
			
		||||
      const result = await bitcoinApi.getRawTransaction(req.params.id);
 | 
			
		||||
      res.send(result);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      if (e.response) {
 | 
			
		||||
        res.status(e.response.status).send(e.response.data);
 | 
			
		||||
      } else {
 | 
			
		||||
        res.status(500, e.message);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async getBlock(req, res) {
 | 
			
		||||
    try {
 | 
			
		||||
      const result = await bitcoinApi.getBlock(req.params.hash);
 | 
			
		||||
      res.send(result);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      if (e.response) {
 | 
			
		||||
        res.status(e.response.status).send(e.response.data);
 | 
			
		||||
      } else {
 | 
			
		||||
        res.status(500, e.message);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async getBlockTransactions(req, res) {
 | 
			
		||||
    try {
 | 
			
		||||
      const result = await bitcoinApi.getBlockTransactions(req.params.hash);
 | 
			
		||||
      res.send(result);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      if (e.response) {
 | 
			
		||||
        res.status(e.response.status).send(e.response.data);
 | 
			
		||||
      } else {
 | 
			
		||||
        if (e.response) {
 | 
			
		||||
          res.status(e.response.status).send(e.response.data);
 | 
			
		||||
        } else {
 | 
			
		||||
          res.status(500, e.message);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async getBlockTransactionsFromIndex(req, res) {
 | 
			
		||||
    try {
 | 
			
		||||
      const result = await bitcoinApi.getBlockTransactionsFromIndex(req.params.hash, req.params.index);
 | 
			
		||||
      res.send(result);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      if (e.response) {
 | 
			
		||||
        res.status(e.response.status).send(e.response.data);
 | 
			
		||||
      } else {
 | 
			
		||||
        if (e.response) {
 | 
			
		||||
          res.status(e.response.status).send(e.response.data);
 | 
			
		||||
        } else {
 | 
			
		||||
          res.status(500, e.message);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async getAddress(req, res) {
 | 
			
		||||
    try {
 | 
			
		||||
      const result = await bitcoinApi.getAddress(req.params.address);
 | 
			
		||||
      res.send(result);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      if (e.response) {
 | 
			
		||||
        res.status(e.response.status).send(e.response.data);
 | 
			
		||||
      } else {
 | 
			
		||||
        if (e.response) {
 | 
			
		||||
          res.status(e.response.status).send(e.response.data);
 | 
			
		||||
        } else {
 | 
			
		||||
          res.status(500, e.message);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async getAddressTransactions(req, res) {
 | 
			
		||||
    try {
 | 
			
		||||
      const result = await bitcoinApi.getAddressTransactions(req.params.address);
 | 
			
		||||
      res.send(result);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      if (e.response) {
 | 
			
		||||
        res.status(e.response.status).send(e.response.data);
 | 
			
		||||
      } else {
 | 
			
		||||
        res.status(500, e.message);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async getAddressTransactionsFromTxid(req, res) {
 | 
			
		||||
    try {
 | 
			
		||||
      const result = await bitcoinApi.getAddressTransactionsFromLastSeenTxid(req.params.address, req.params.txid);
 | 
			
		||||
      res.send(result);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      if (e.response) {
 | 
			
		||||
        res.status(e.response.status).send(e.response.data);
 | 
			
		||||
      } else {
 | 
			
		||||
        res.status(500, e.message);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new Routes();
 | 
			
		||||
 | 
			
		||||
@ -31,6 +31,13 @@
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/caseless/-/caseless-0.12.2.tgz#f65d3d6389e01eeb458bd54dc8f52b95a9463bc8"
 | 
			
		||||
  integrity sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==
 | 
			
		||||
 | 
			
		||||
"@types/compression@^1.0.1":
 | 
			
		||||
  version "1.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/compression/-/compression-1.0.1.tgz#f3682a6b3ce2dbd4aece48547153ebc592281fa7"
 | 
			
		||||
  integrity sha512-GuoIYzD70h+4JUqUabsm31FGqvpCYHGKcLtor7nQ/YvUyNX0o9SJZ9boFI5HjFfbOda5Oe/XOvNK6FES8Y/79w==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@types/express" "*"
 | 
			
		||||
 | 
			
		||||
"@types/connect@*":
 | 
			
		||||
  version "3.4.32"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.32.tgz#aa0e9616b9435ccad02bc52b5b454ffc2c70ba28"
 | 
			
		||||
@ -39,17 +46,17 @@
 | 
			
		||||
    "@types/node" "*"
 | 
			
		||||
 | 
			
		||||
"@types/express-serve-static-core@*":
 | 
			
		||||
  version "4.16.10"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.16.10.tgz#3c1313c6e6b75594561b473a286f016a9abf2132"
 | 
			
		||||
  integrity sha512-gM6evDj0OvTILTRKilh9T5dTaGpv1oYiFcJAfgSejuMJgGJUsD9hKEU2lB4aiTNy4WwChxRnjfYFuBQsULzsJw==
 | 
			
		||||
  version "4.17.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.0.tgz#e80c25903df5800e926402b7e8267a675c54a281"
 | 
			
		||||
  integrity sha512-Xnub7w57uvcBqFdIGoRg1KhNOeEj0vB6ykUM7uFWyxvbdE89GFyqgmUcanAriMr4YOxNFZBAWkfcWIb4WBPt3g==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@types/node" "*"
 | 
			
		||||
    "@types/range-parser" "*"
 | 
			
		||||
 | 
			
		||||
"@types/express@^4.16.0":
 | 
			
		||||
  version "4.17.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.1.tgz#4cf7849ae3b47125a567dfee18bfca4254b88c5c"
 | 
			
		||||
  integrity sha512-VfH/XCP0QbQk5B5puLqTLEeFgR8lfCJHZJKkInZ9mkYd+u8byX0kztXEQxEk4wZXJs8HI+7km2ALXjn4YKcX9w==
 | 
			
		||||
"@types/express@*", "@types/express@^4.17.2":
 | 
			
		||||
  version "4.17.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.2.tgz#a0fb7a23d8855bac31bc01d5a58cadd9b2173e6c"
 | 
			
		||||
  integrity sha512-5mHFNyavtLoJmnusB8OKJ5bshSzw+qkMIBAobLrIM48HJvunFva9mOa6aBwh64lBFyNwBbs0xiEFuj4eU/NjCA==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@types/body-parser" "*"
 | 
			
		||||
    "@types/express-serve-static-core" "*"
 | 
			
		||||
@ -60,20 +67,10 @@
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.1.tgz#dc488842312a7f075149312905b5e3c0b054c79d"
 | 
			
		||||
  integrity sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==
 | 
			
		||||
 | 
			
		||||
"@types/mysql2@github:types/mysql2":
 | 
			
		||||
  version "1.0.0"
 | 
			
		||||
  resolved "https://codeload.github.com/types/mysql2/tar.gz/217efd4ccf9eccc0797522aa745d8a9e264f6a75"
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@types/mysql" types/mysql#v2.0.0
 | 
			
		||||
 | 
			
		||||
"@types/mysql@types/mysql#v2.0.0":
 | 
			
		||||
  version "2.0.0"
 | 
			
		||||
  resolved "https://codeload.github.com/types/mysql/tar.gz/da645a82afd66419ed439dddf174648aa68ba1f9"
 | 
			
		||||
 | 
			
		||||
"@types/node@*":
 | 
			
		||||
  version "12.11.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/node/-/node-12.11.1.tgz#1fd7b821f798b7fa29f667a1be8f3442bb8922a3"
 | 
			
		||||
  integrity sha512-TJtwsqZ39pqcljJpajeoofYRfeZ7/I/OMUQ5pR4q5wOKf2ocrUvBAZUMhWsOvKx3dVc/aaV5GluBivt0sWqA5A==
 | 
			
		||||
  version "12.12.17"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.17.tgz#191b71e7f4c325ee0fb23bc4a996477d92b8c39b"
 | 
			
		||||
  integrity sha512-Is+l3mcHvs47sKy+afn2O1rV4ldZFU7W8101cNlOd+MRbjM4Onida8jSZnJdTe/0Pcf25g9BNIUsuugmE6puHA==
 | 
			
		||||
 | 
			
		||||
"@types/range-parser@*":
 | 
			
		||||
  version "1.2.3"
 | 
			
		||||
@ -99,14 +96,14 @@
 | 
			
		||||
    "@types/mime" "*"
 | 
			
		||||
 | 
			
		||||
"@types/tough-cookie@*":
 | 
			
		||||
  version "2.3.5"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-2.3.5.tgz#9da44ed75571999b65c37b60c9b2b88db54c585d"
 | 
			
		||||
  integrity sha512-SCcK7mvGi3+ZNz833RRjFIxrn4gI1PPR3NtuIS+6vMkvmsGjosqTJwRt5bAEFLRz+wtJMWv8+uOnZf2hi2QXTg==
 | 
			
		||||
  version "2.3.6"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-2.3.6.tgz#c880579e087d7a0db13777ff8af689f4ffc7b0d5"
 | 
			
		||||
  integrity sha512-wHNBMnkoEBiRAd3s8KTKwIuO9biFtTf0LehITzBhSco+HQI0xkXZbLOD55SW3Aqw3oUkHstkm5SPv58yaAdFPQ==
 | 
			
		||||
 | 
			
		||||
"@types/ws@^6.0.1":
 | 
			
		||||
  version "6.0.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/ws/-/ws-6.0.3.tgz#b772375ba59d79066561c8d87500144d674ba6b3"
 | 
			
		||||
  integrity sha512-yBTM0P05Tx9iXGq00BbJPo37ox68R5vaGTXivs6RGh/BQ6QP5zqZDGWdAO6JbRE/iR1l80xeGAwCQS2nMV9S/w==
 | 
			
		||||
"@types/ws@^6.0.4":
 | 
			
		||||
  version "6.0.4"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/ws/-/ws-6.0.4.tgz#7797707c8acce8f76d8c34b370d4645b70421ff1"
 | 
			
		||||
  integrity sha512-PpPrX7SZW9re6+Ha8ojZG4Se8AZXgf0GK6zmfqEuCsY49LFDNXO3SByp44X3dFEqtB73lkCDAdUazhAjVPiNwg==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@types/node" "*"
 | 
			
		||||
 | 
			
		||||
@ -159,7 +156,7 @@ assert-plus@1.0.0, assert-plus@^1.0.0:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
 | 
			
		||||
  integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=
 | 
			
		||||
 | 
			
		||||
async-limiter@~1.0.0:
 | 
			
		||||
async-limiter@^1.0.0:
 | 
			
		||||
  version "1.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.1.tgz#dd379e94f0db8310b08291f9d64c3209766617fd"
 | 
			
		||||
  integrity sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==
 | 
			
		||||
@ -175,17 +172,9 @@ aws-sign2@~0.7.0:
 | 
			
		||||
  integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=
 | 
			
		||||
 | 
			
		||||
aws4@^1.8.0:
 | 
			
		||||
  version "1.8.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
 | 
			
		||||
  integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==
 | 
			
		||||
 | 
			
		||||
axios@^0.19.0:
 | 
			
		||||
  version "0.19.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.0.tgz#8e09bff3d9122e133f7b8101c8fbdd00ed3d2ab8"
 | 
			
		||||
  integrity sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    follow-redirects "1.5.10"
 | 
			
		||||
    is-buffer "^2.0.2"
 | 
			
		||||
  version "1.9.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.0.tgz#24390e6ad61386b0a747265754d2a17219de862c"
 | 
			
		||||
  integrity sha512-Uvq6hVe90D0B2WEnUqtdgY1bATGz3mw33nH9Y+dmA+w5DHvUmBgkr5rM/KCHpCsiFNRUfokW/szpPPgMK2hm4A==
 | 
			
		||||
 | 
			
		||||
balanced-match@^1.0.0:
 | 
			
		||||
  version "1.0.0"
 | 
			
		||||
@ -199,11 +188,6 @@ bcrypt-pbkdf@^1.0.0:
 | 
			
		||||
  dependencies:
 | 
			
		||||
    tweetnacl "^0.14.3"
 | 
			
		||||
 | 
			
		||||
bitcoin@^3.0.1:
 | 
			
		||||
  version "3.0.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/bitcoin/-/bitcoin-3.0.1.tgz#ff9e0b62a71bbb8adddb34ee2e427dac21c1096f"
 | 
			
		||||
  integrity sha1-/54LYqcbu4rd2zTuLkJ9rCHBCW8=
 | 
			
		||||
 | 
			
		||||
body-parser@1.19.0:
 | 
			
		||||
  version "1.19.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a"
 | 
			
		||||
@ -288,7 +272,7 @@ compressible@~2.0.16:
 | 
			
		||||
  dependencies:
 | 
			
		||||
    mime-db ">= 1.40.0 < 2"
 | 
			
		||||
 | 
			
		||||
compression@^1.7.3:
 | 
			
		||||
compression@^1.7.4:
 | 
			
		||||
  version "1.7.4"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f"
 | 
			
		||||
  integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==
 | 
			
		||||
@ -347,13 +331,6 @@ debug@2.6.9:
 | 
			
		||||
  dependencies:
 | 
			
		||||
    ms "2.0.0"
 | 
			
		||||
 | 
			
		||||
debug@=3.1.0:
 | 
			
		||||
  version "3.1.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261"
 | 
			
		||||
  integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    ms "2.0.0"
 | 
			
		||||
 | 
			
		||||
delayed-stream@~1.0.0:
 | 
			
		||||
  version "1.0.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
 | 
			
		||||
@ -422,7 +399,7 @@ etag@~1.8.1:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
 | 
			
		||||
  integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
 | 
			
		||||
 | 
			
		||||
express@^4.16.3:
 | 
			
		||||
express@^4.17.1:
 | 
			
		||||
  version "4.17.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/express/-/express-4.17.1.tgz#4491fc38605cf51f8629d39c2b5d026f98a4c134"
 | 
			
		||||
  integrity sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==
 | 
			
		||||
@ -496,13 +473,6 @@ finalhandler@~1.1.2:
 | 
			
		||||
    statuses "~1.5.0"
 | 
			
		||||
    unpipe "~1.0.0"
 | 
			
		||||
 | 
			
		||||
follow-redirects@1.5.10:
 | 
			
		||||
  version "1.5.10"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a"
 | 
			
		||||
  integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    debug "=3.1.0"
 | 
			
		||||
 | 
			
		||||
forever-agent@~0.6.1:
 | 
			
		||||
  version "0.6.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91"
 | 
			
		||||
@ -556,9 +526,9 @@ getpass@^0.1.1:
 | 
			
		||||
    assert-plus "^1.0.0"
 | 
			
		||||
 | 
			
		||||
glob@^7.1.1:
 | 
			
		||||
  version "7.1.4"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255"
 | 
			
		||||
  integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==
 | 
			
		||||
  version "7.1.6"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6"
 | 
			
		||||
  integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    fs.realpath "^1.0.0"
 | 
			
		||||
    inflight "^1.0.4"
 | 
			
		||||
@ -624,9 +594,9 @@ iconv-lite@0.4.24:
 | 
			
		||||
    safer-buffer ">= 2.1.2 < 3"
 | 
			
		||||
 | 
			
		||||
iconv-lite@^0.5.0:
 | 
			
		||||
  version "0.5.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.5.0.tgz#59cdde0a2a297cc2aeb0c6445a195ee89f127550"
 | 
			
		||||
  integrity sha512-NnEhI9hIEKHOzJ4f697DMz9IQEXr/MMJ5w64vN2/4Ai+wRnvV7SBrL0KLoRlwaKVghOc7LQ5YkPLuX146b6Ydw==
 | 
			
		||||
  version "0.5.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.5.1.tgz#b2425d3c7b18f7219f2ca663d103bddb91718d64"
 | 
			
		||||
  integrity sha512-ONHr16SQvKZNSqjQT9gy5z24Jw+uqfO02/ngBSBoqChZ+W8qXX7GPRa1RoUnzGADw8K63R1BXUMzarCVQBpY8Q==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    safer-buffer ">= 2.1.2 < 3"
 | 
			
		||||
 | 
			
		||||
@ -653,11 +623,6 @@ ipaddr.js@1.9.0:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.0.tgz#37df74e430a0e47550fe54a2defe30d8acd95f65"
 | 
			
		||||
  integrity sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==
 | 
			
		||||
 | 
			
		||||
is-buffer@^2.0.2:
 | 
			
		||||
  version "2.0.4"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.4.tgz#3e572f23c8411a5cfd9557c849e3665e0b290623"
 | 
			
		||||
  integrity sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A==
 | 
			
		||||
 | 
			
		||||
is-property@^1.0.2:
 | 
			
		||||
  version "1.0.2"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84"
 | 
			
		||||
@ -751,22 +716,17 @@ methods@~1.1.2:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
 | 
			
		||||
  integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
 | 
			
		||||
 | 
			
		||||
mime-db@1.40.0:
 | 
			
		||||
  version "1.40.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.40.0.tgz#a65057e998db090f732a68f6c276d387d4126c32"
 | 
			
		||||
  integrity sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA==
 | 
			
		||||
 | 
			
		||||
"mime-db@>= 1.40.0 < 2":
 | 
			
		||||
mime-db@1.42.0, "mime-db@>= 1.40.0 < 2":
 | 
			
		||||
  version "1.42.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.42.0.tgz#3e252907b4c7adb906597b4b65636272cf9e7bac"
 | 
			
		||||
  integrity sha512-UbfJCR4UAVRNgMpfImz05smAXK7+c+ZntjaA26ANtkXLlOe947Aag5zdIcKQULAiF9Cq4WxBi9jUs5zkA84bYQ==
 | 
			
		||||
 | 
			
		||||
mime-types@^2.1.12, mime-types@~2.1.19, mime-types@~2.1.24:
 | 
			
		||||
  version "2.1.24"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.24.tgz#b6f8d0b3e951efb77dedeca194cff6d16f676f81"
 | 
			
		||||
  integrity sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==
 | 
			
		||||
  version "2.1.25"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.25.tgz#39772d46621f93e2a80a856c53b86a62156a6437"
 | 
			
		||||
  integrity sha512-5KhStqB5xpTAeGqKBAMgwaYMnQik7teQN4IAzC7npDv6kzeU6prfkR67bc87J1kWMPGkoaZSq1npmexMgkmEVg==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    mime-db "1.40.0"
 | 
			
		||||
    mime-db "1.42.0"
 | 
			
		||||
 | 
			
		||||
mime@1.6.0:
 | 
			
		||||
  version "1.6.0"
 | 
			
		||||
@ -891,9 +851,9 @@ pseudomap@^1.0.2:
 | 
			
		||||
  integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM=
 | 
			
		||||
 | 
			
		||||
psl@^1.1.24:
 | 
			
		||||
  version "1.4.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/psl/-/psl-1.4.0.tgz#5dd26156cdb69fa1fdb8ab1991667d3f80ced7c2"
 | 
			
		||||
  integrity sha512-HZzqCGPecFLyoRj5HLfuDSKYTJkAfB5thKBIkRHtGjWwY7p1dAyveIbXIq4tO0KYfDF2tHqPUgY9SDnGm00uFw==
 | 
			
		||||
  version "1.6.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/psl/-/psl-1.6.0.tgz#60557582ee23b6c43719d9890fb4170ecd91e110"
 | 
			
		||||
  integrity sha512-SYKKmVel98NCOYXpkwUqZqh0ahZeeKfmisiLIcEZdsb+WbLv02g/dI5BUmZnIyOe7RzZtLax81nnb2HbvC2tzA==
 | 
			
		||||
 | 
			
		||||
punycode@^1.4.1:
 | 
			
		||||
  version "1.4.1"
 | 
			
		||||
@ -957,9 +917,9 @@ request@^2.88.0:
 | 
			
		||||
    uuid "^3.3.2"
 | 
			
		||||
 | 
			
		||||
resolve@^1.3.2:
 | 
			
		||||
  version "1.12.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.12.0.tgz#3fc644a35c84a48554609ff26ec52b66fa577df6"
 | 
			
		||||
  integrity sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==
 | 
			
		||||
  version "1.13.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.13.1.tgz#be0aa4c06acd53083505abb35f4d66932ab35d16"
 | 
			
		||||
  integrity sha512-CxqObCX8K8YtAhOBRg+lrcdn+LK+WYOS8tSjqSFbjtrI5PnS63QPhZl4+yKfrU9tdsbMu9Anr/amegT87M9Z6w==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    path-parse "^1.0.6"
 | 
			
		||||
 | 
			
		||||
@ -1078,9 +1038,9 @@ tslib@^1.8.0, tslib@^1.8.1:
 | 
			
		||||
  integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==
 | 
			
		||||
 | 
			
		||||
tslint@^5.11.0:
 | 
			
		||||
  version "5.20.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.20.0.tgz#fac93bfa79568a5a24e7be9cdde5e02b02d00ec1"
 | 
			
		||||
  integrity sha512-2vqIvkMHbnx8acMogAERQ/IuINOq6DFqgF8/VDvhEkBqQh/x6SP0Y+OHnKth9/ZcHQSroOZwUQSN18v8KKF0/g==
 | 
			
		||||
  version "5.20.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.20.1.tgz#e401e8aeda0152bc44dd07e614034f3f80c67b7d"
 | 
			
		||||
  integrity sha512-EcMxhzCFt8k+/UP5r8waCf/lzmeSyVlqxqMEDQE7rWYiQky8KpIBz1JAoYXfROHrPZ1XXd43q8yQnULOLiBRQg==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@babel/code-frame" "^7.0.0"
 | 
			
		||||
    builtin-modules "^1.1.1"
 | 
			
		||||
@ -1123,7 +1083,7 @@ type-is@~1.6.17, type-is@~1.6.18:
 | 
			
		||||
    media-typer "0.3.0"
 | 
			
		||||
    mime-types "~2.1.24"
 | 
			
		||||
 | 
			
		||||
typescript@^3.1.1:
 | 
			
		||||
typescript@~3.6.4:
 | 
			
		||||
  version "3.6.4"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.6.4.tgz#b18752bb3792bc1a0281335f7f6ebf1bbfc5b91d"
 | 
			
		||||
  integrity sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg==
 | 
			
		||||
@ -1169,12 +1129,12 @@ wrappy@1:
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
 | 
			
		||||
  integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
 | 
			
		||||
 | 
			
		||||
ws@^6.0.0:
 | 
			
		||||
  version "6.2.1"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb"
 | 
			
		||||
  integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==
 | 
			
		||||
ws@^7.2.0:
 | 
			
		||||
  version "7.2.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/ws/-/ws-7.2.0.tgz#422eda8c02a4b5dba7744ba66eebbd84bcef0ec7"
 | 
			
		||||
  integrity sha512-+SqNqFbwTm/0DC18KYzIsMTnEWpLwJsiasW/O17la4iDRRIO9uaHbvKiAS3AHgTiuuWerK/brj4O6MYZkei9xg==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    async-limiter "~1.0.0"
 | 
			
		||||
    async-limiter "^1.0.0"
 | 
			
		||||
 | 
			
		||||
yallist@^2.1.2:
 | 
			
		||||
  version "2.1.2"
 | 
			
		||||
 | 
			
		||||
@ -1,51 +0,0 @@
 | 
			
		||||
#!/bin/bash
 | 
			
		||||
## Start SQL
 | 
			
		||||
mysqld_safe&
 | 
			
		||||
sleep 5
 | 
			
		||||
## http server:
 | 
			
		||||
nginx
 | 
			
		||||
 | 
			
		||||
## Set up some files:
 | 
			
		||||
cd /mempool.space/backend 
 | 
			
		||||
rm -f cache.json
 | 
			
		||||
touch cache.json
 | 
			
		||||
 | 
			
		||||
## Build mempool-config.json file ourseleves.
 | 
			
		||||
## We used to use jq for this but that produced output which caused bugs,
 | 
			
		||||
## specifically numbers were surrounded by quotes, which breaks things.
 | 
			
		||||
## Old command was jq -n env > mempool-config.json
 | 
			
		||||
## This way is more complex, but more compatible with the backend functions.
 | 
			
		||||
 | 
			
		||||
## Define a function to allow us to easily get indexes of the = string in from the env output:
 | 
			
		||||
strindex() { 
 | 
			
		||||
  x="${1%%$2*}"
 | 
			
		||||
  [[ "$x" = "$1" ]] && echo -1 || echo "${#x}"
 | 
			
		||||
}
 | 
			
		||||
## Regex to check if we have a number or not:
 | 
			
		||||
NumberRegEx='^[0-9]+$'
 | 
			
		||||
## Delete the old file, and start a new one:
 | 
			
		||||
rm -f mempool-config.json
 | 
			
		||||
echo "{" >> mempool-config.json
 | 
			
		||||
## For each env we add into the mempool-config.json file in one of two ways.
 | 
			
		||||
## Either:
 | 
			
		||||
## "Variable": "Value",
 | 
			
		||||
## if a string, or
 | 
			
		||||
## "Variable": Value,
 | 
			
		||||
## if a integer
 | 
			
		||||
for e in `env`; do
 | 
			
		||||
    if [[ ${e:`strindex "$e" "="`+1} =~ $NumberRegEx ]] ; then
 | 
			
		||||
        ## Integer add:
 | 
			
		||||
        echo "\""${e:0:`strindex "$e" "="`}"\": "${e:`strindex "$e" "="`+1}","  >> mempool-config.json
 | 
			
		||||
    else
 | 
			
		||||
        ## String add:
 | 
			
		||||
        echo "\""${e:0:`strindex "$e" "="`}"\": \""${e:`strindex "$e" "="`+1}$"\","  >> mempool-config.json
 | 
			
		||||
    fi
 | 
			
		||||
done
 | 
			
		||||
## Take out the trailing , from the last entry.
 | 
			
		||||
## This means replacing the file with one that is missing the last character
 | 
			
		||||
echo `sed '$ s/.$//' mempool-config.json` > mempool-config.json
 | 
			
		||||
## And finally finish off:
 | 
			
		||||
echo "}" >> mempool-config.json
 | 
			
		||||
 | 
			
		||||
## Start mempoolspace:
 | 
			
		||||
node dist/index.js
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
# Editor configuration, see http://editorconfig.org
 | 
			
		||||
# Editor configuration, see https://editorconfig.org
 | 
			
		||||
root = true
 | 
			
		||||
 | 
			
		||||
[*]
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										7
									
								
								frontend/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								frontend/.gitignore
									
									
									
									
										vendored
									
									
								
							@ -4,10 +4,16 @@
 | 
			
		||||
/dist
 | 
			
		||||
/tmp
 | 
			
		||||
/out-tsc
 | 
			
		||||
# Only exists if Bazel was run
 | 
			
		||||
/bazel-out
 | 
			
		||||
 | 
			
		||||
# dependencies
 | 
			
		||||
/node_modules
 | 
			
		||||
 | 
			
		||||
# profiling files
 | 
			
		||||
chrome-profiler-events.json
 | 
			
		||||
speed-measure-plugin.json
 | 
			
		||||
 | 
			
		||||
# IDEs and editors
 | 
			
		||||
/.idea
 | 
			
		||||
.project
 | 
			
		||||
@ -23,6 +29,7 @@
 | 
			
		||||
!.vscode/tasks.json
 | 
			
		||||
!.vscode/launch.json
 | 
			
		||||
!.vscode/extensions.json
 | 
			
		||||
.history/*
 | 
			
		||||
 | 
			
		||||
# misc
 | 
			
		||||
/.sass-cache
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										27
									
								
								frontend/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								frontend/README.md
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,27 @@
 | 
			
		||||
# Mempool Space
 | 
			
		||||
 | 
			
		||||
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 8.1.2.
 | 
			
		||||
 | 
			
		||||
## Development server
 | 
			
		||||
 | 
			
		||||
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
 | 
			
		||||
 | 
			
		||||
## Code scaffolding
 | 
			
		||||
 | 
			
		||||
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`.
 | 
			
		||||
 | 
			
		||||
## Build
 | 
			
		||||
 | 
			
		||||
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build.
 | 
			
		||||
 | 
			
		||||
## Running unit tests
 | 
			
		||||
 | 
			
		||||
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
 | 
			
		||||
 | 
			
		||||
## Running end-to-end tests
 | 
			
		||||
 | 
			
		||||
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
 | 
			
		||||
 | 
			
		||||
## Further help
 | 
			
		||||
 | 
			
		||||
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
 | 
			
		||||
@ -3,25 +3,26 @@
 | 
			
		||||
  "version": 1,
 | 
			
		||||
  "newProjectRoot": "projects",
 | 
			
		||||
  "projects": {
 | 
			
		||||
    "mempool": {
 | 
			
		||||
      "root": "",
 | 
			
		||||
      "sourceRoot": "src",
 | 
			
		||||
    "mempoolspace": {
 | 
			
		||||
      "projectType": "application",
 | 
			
		||||
      "prefix": "app",
 | 
			
		||||
      "schematics": {
 | 
			
		||||
        "@schematics/angular:component": {
 | 
			
		||||
          "styleext": "scss"
 | 
			
		||||
          "style": "scss"
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      "root": "",
 | 
			
		||||
      "sourceRoot": "src",
 | 
			
		||||
      "prefix": "app",
 | 
			
		||||
      "architect": {
 | 
			
		||||
        "build": {
 | 
			
		||||
          "builder": "@angular-devkit/build-angular:browser",
 | 
			
		||||
          "options": {
 | 
			
		||||
            "outputPath": "dist/mempool",
 | 
			
		||||
            "outputPath": "dist/mempoolspace",
 | 
			
		||||
            "index": "src/index.html",
 | 
			
		||||
            "main": "src/main.ts",
 | 
			
		||||
            "polyfills": "src/polyfills.ts",
 | 
			
		||||
            "tsConfig": "src/tsconfig.app.json",
 | 
			
		||||
            "tsConfig": "tsconfig.app.json",
 | 
			
		||||
            "aot": true,
 | 
			
		||||
            "assets": [
 | 
			
		||||
              "src/favicon.ico",
 | 
			
		||||
              "src/assets"
 | 
			
		||||
@ -44,45 +45,38 @@
 | 
			
		||||
              "sourceMap": false,
 | 
			
		||||
              "extractCss": true,
 | 
			
		||||
              "namedChunks": false,
 | 
			
		||||
              "aot": true,
 | 
			
		||||
              "extractLicenses": true,
 | 
			
		||||
              "vendorChunk": false,
 | 
			
		||||
              "buildOptimizer": true
 | 
			
		||||
            },
 | 
			
		||||
            "electrs": {
 | 
			
		||||
              "fileReplacements": [
 | 
			
		||||
              "buildOptimizer": true,
 | 
			
		||||
              "budgets": [
 | 
			
		||||
                {
 | 
			
		||||
                  "replace": "src/environments/environment.ts",
 | 
			
		||||
                  "with": "src/environments/environment-electrs.prod.ts"
 | 
			
		||||
                  "type": "initial",
 | 
			
		||||
                  "maximumWarning": "2mb",
 | 
			
		||||
                  "maximumError": "5mb"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                  "type": "anyComponentStyle",
 | 
			
		||||
                  "maximumWarning": "6kb"
 | 
			
		||||
                }
 | 
			
		||||
              ],
 | 
			
		||||
              "optimization": true,
 | 
			
		||||
              "outputHashing": "all",
 | 
			
		||||
              "sourceMap": false,
 | 
			
		||||
              "extractCss": true,
 | 
			
		||||
              "namedChunks": false,
 | 
			
		||||
              "aot": true,
 | 
			
		||||
              "extractLicenses": true,
 | 
			
		||||
              "vendorChunk": false,
 | 
			
		||||
              "buildOptimizer": true
 | 
			
		||||
              ]
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "serve": {
 | 
			
		||||
          "builder": "@angular-devkit/build-angular:dev-server",
 | 
			
		||||
          "options": {
 | 
			
		||||
            "browserTarget": "mempool:build"
 | 
			
		||||
            "browserTarget": "mempoolspace:build"
 | 
			
		||||
          },
 | 
			
		||||
          "configurations": {
 | 
			
		||||
            "production": {
 | 
			
		||||
              "browserTarget": "mempool:build:production"
 | 
			
		||||
              "browserTarget": "mempoolspace:build:production"
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "extract-i18n": {
 | 
			
		||||
          "builder": "@angular-devkit/build-angular:extract-i18n",
 | 
			
		||||
          "options": {
 | 
			
		||||
            "browserTarget": "mempool:build"
 | 
			
		||||
            "browserTarget": "mempoolspace:build"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "test": {
 | 
			
		||||
@ -90,54 +84,44 @@
 | 
			
		||||
          "options": {
 | 
			
		||||
            "main": "src/test.ts",
 | 
			
		||||
            "polyfills": "src/polyfills.ts",
 | 
			
		||||
            "tsConfig": "src/tsconfig.spec.json",
 | 
			
		||||
            "karmaConfig": "src/karma.conf.js",
 | 
			
		||||
            "styles": [
 | 
			
		||||
              "src/styles.scss"
 | 
			
		||||
            ],
 | 
			
		||||
            "scripts": [],
 | 
			
		||||
            "tsConfig": "tsconfig.spec.json",
 | 
			
		||||
            "karmaConfig": "karma.conf.js",
 | 
			
		||||
            "assets": [
 | 
			
		||||
              "src/favicon.ico",
 | 
			
		||||
              "src/assets"
 | 
			
		||||
            ]
 | 
			
		||||
            ],
 | 
			
		||||
            "styles": [
 | 
			
		||||
              "src/styles.scss"
 | 
			
		||||
            ],
 | 
			
		||||
            "scripts": []
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "lint": {
 | 
			
		||||
          "builder": "@angular-devkit/build-angular:tslint",
 | 
			
		||||
          "options": {
 | 
			
		||||
            "tsConfig": [
 | 
			
		||||
              "src/tsconfig.app.json",
 | 
			
		||||
              "src/tsconfig.spec.json"
 | 
			
		||||
              "tsconfig.app.json",
 | 
			
		||||
              "tsconfig.spec.json",
 | 
			
		||||
              "e2e/tsconfig.json"
 | 
			
		||||
            ],
 | 
			
		||||
            "exclude": [
 | 
			
		||||
              "**/node_modules/**"
 | 
			
		||||
            ]
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "mempool-e2e": {
 | 
			
		||||
      "root": "e2e/",
 | 
			
		||||
      "projectType": "application",
 | 
			
		||||
      "architect": {
 | 
			
		||||
        },
 | 
			
		||||
        "e2e": {
 | 
			
		||||
          "builder": "@angular-devkit/build-angular:protractor",
 | 
			
		||||
          "options": {
 | 
			
		||||
            "protractorConfig": "e2e/protractor.conf.js",
 | 
			
		||||
            "devServerTarget": "mempool:serve"
 | 
			
		||||
          }
 | 
			
		||||
        },
 | 
			
		||||
        "lint": {
 | 
			
		||||
          "builder": "@angular-devkit/build-angular:tslint",
 | 
			
		||||
          "options": {
 | 
			
		||||
            "tsConfig": "e2e/tsconfig.e2e.json",
 | 
			
		||||
            "exclude": [
 | 
			
		||||
              "**/node_modules/**"
 | 
			
		||||
            ]
 | 
			
		||||
            "devServerTarget": "mempoolspace:serve"
 | 
			
		||||
          },
 | 
			
		||||
          "configurations": {
 | 
			
		||||
            "production": {
 | 
			
		||||
              "devServerTarget": "mempoolspace:serve:production"
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "defaultProject": "mempool"
 | 
			
		||||
}
 | 
			
		||||
    }},
 | 
			
		||||
  "defaultProject": "mempoolspace"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										12
									
								
								frontend/browserslist
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								frontend/browserslist
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
			
		||||
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
 | 
			
		||||
# For additional information regarding the format and rule options, please see:
 | 
			
		||||
# https://github.com/browserslist/browserslist#queries
 | 
			
		||||
 | 
			
		||||
# You can see what browsers were selected by your queries by running:
 | 
			
		||||
#   npx browserslist
 | 
			
		||||
 | 
			
		||||
> 0.5%
 | 
			
		||||
last 2 versions
 | 
			
		||||
Firefox ESR
 | 
			
		||||
not dead
 | 
			
		||||
not IE 9-11 # For IE 9-11 support, remove 'not'.
 | 
			
		||||
@ -1,8 +1,12 @@
 | 
			
		||||
// @ts-check
 | 
			
		||||
// Protractor configuration file, see link for more information
 | 
			
		||||
// https://github.com/angular/protractor/blob/master/lib/config.ts
 | 
			
		||||
 | 
			
		||||
const { SpecReporter } = require('jasmine-spec-reporter');
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @type { import("protractor").Config }
 | 
			
		||||
 */
 | 
			
		||||
exports.config = {
 | 
			
		||||
  allScriptsTimeout: 11000,
 | 
			
		||||
  specs: [
 | 
			
		||||
@ -21,7 +25,7 @@ exports.config = {
 | 
			
		||||
  },
 | 
			
		||||
  onPrepare() {
 | 
			
		||||
    require('ts-node').register({
 | 
			
		||||
      project: require('path').join(__dirname, './tsconfig.e2e.json')
 | 
			
		||||
      project: require('path').join(__dirname, './tsconfig.json')
 | 
			
		||||
    });
 | 
			
		||||
    jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
import { AppPage } from './app.po';
 | 
			
		||||
import { browser, logging } from 'protractor';
 | 
			
		||||
 | 
			
		||||
describe('workspace-project App', () => {
 | 
			
		||||
  let page: AppPage;
 | 
			
		||||
@ -9,6 +10,14 @@ describe('workspace-project App', () => {
 | 
			
		||||
 | 
			
		||||
  it('should display welcome message', () => {
 | 
			
		||||
    page.navigateTo();
 | 
			
		||||
    expect(page.getParagraphText()).toEqual('Welcome to app!');
 | 
			
		||||
    expect(page.getTitleText()).toEqual('Welcome to mempoolspace!');
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  afterEach(async () => {
 | 
			
		||||
    // Assert that there are no errors emitted from the browser
 | 
			
		||||
    const logs = await browser.manage().logs().get(logging.Type.BROWSER);
 | 
			
		||||
    expect(logs).not.toContain(jasmine.objectContaining({
 | 
			
		||||
      level: logging.Level.SEVERE,
 | 
			
		||||
    } as logging.Entry));
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@ -2,10 +2,10 @@ import { browser, by, element } from 'protractor';
 | 
			
		||||
 | 
			
		||||
export class AppPage {
 | 
			
		||||
  navigateTo() {
 | 
			
		||||
    return browser.get('/');
 | 
			
		||||
    return browser.get(browser.baseUrl) as Promise<any>;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getParagraphText() {
 | 
			
		||||
    return element(by.css('app-root h1')).getText();
 | 
			
		||||
  getTitleText() {
 | 
			
		||||
    return element(by.css('app-root h1')).getText() as Promise<string>;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
{
 | 
			
		||||
  "extends": "../tsconfig.json",
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "outDir": "../out-tsc/app",
 | 
			
		||||
    "outDir": "../out-tsc/e2e",
 | 
			
		||||
    "module": "commonjs",
 | 
			
		||||
    "target": "es5",
 | 
			
		||||
    "types": [
 | 
			
		||||
@ -10,4 +10,4 @@
 | 
			
		||||
      "node"
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
@ -16,8 +16,8 @@ module.exports = function (config) {
 | 
			
		||||
      clearContext: false // leave Jasmine Spec Runner output visible in browser
 | 
			
		||||
    },
 | 
			
		||||
    coverageIstanbulReporter: {
 | 
			
		||||
      dir: require('path').join(__dirname, '../coverage'),
 | 
			
		||||
      reports: ['html', 'lcovonly'],
 | 
			
		||||
      dir: require('path').join(__dirname, './coverage/mempoolspace'),
 | 
			
		||||
      reports: ['html', 'lcovonly', 'text-summary'],
 | 
			
		||||
      fixWebpackSourcePaths: true
 | 
			
		||||
    },
 | 
			
		||||
    reporters: ['progress', 'kjhtml'],
 | 
			
		||||
@ -26,6 +26,7 @@ module.exports = function (config) {
 | 
			
		||||
    logLevel: config.LOG_INFO,
 | 
			
		||||
    autoWatch: true,
 | 
			
		||||
    browsers: ['Chrome'],
 | 
			
		||||
    singleRun: false
 | 
			
		||||
    singleRun: false,
 | 
			
		||||
    restartOnFileChange: true
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										9036
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										9036
									
								
								frontend/package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -1,50 +1,55 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "mempool-frontend",
 | 
			
		||||
  "version": "1.0.0",
 | 
			
		||||
  "description": "Bitcoin Mempool Visualizer",
 | 
			
		||||
  "name": "mempoolspace",
 | 
			
		||||
  "version": "0.0.0",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "ng": "ng",
 | 
			
		||||
    "start": "ng serve --aot --proxy-config proxy.conf.json",
 | 
			
		||||
    "start": "ng serve --proxy-config proxy.conf.json",
 | 
			
		||||
    "build": "ng build --prod",
 | 
			
		||||
    "build-electrs": "ng build --prod --configuration=electrs",
 | 
			
		||||
    "test": "ng test",
 | 
			
		||||
    "lint": "ng lint",
 | 
			
		||||
    "e2e": "ng e2e"
 | 
			
		||||
  },
 | 
			
		||||
  "author": {
 | 
			
		||||
    "name": "Simon Lindh",
 | 
			
		||||
    "url": "https://github.com/mempool-space/mempool.space"
 | 
			
		||||
  },
 | 
			
		||||
  "license": "MIT",
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@angular/animations": "^8.2.11",
 | 
			
		||||
    "@angular/common": "^8.2.11",
 | 
			
		||||
    "@angular/compiler": "^8.2.11",
 | 
			
		||||
    "@angular/core": "^8.2.11",
 | 
			
		||||
    "@angular/forms": "^8.2.11",
 | 
			
		||||
    "@angular/platform-browser": "^8.2.11",
 | 
			
		||||
    "@angular/platform-browser-dynamic": "^8.2.11",
 | 
			
		||||
    "@angular/router": "^8.2.11",
 | 
			
		||||
    "@ng-bootstrap/ng-bootstrap": "^5.1.1",
 | 
			
		||||
    "angularx-qrcode": "^1.7.0-beta.5",
 | 
			
		||||
    "bootstrap": "^4.3.1",
 | 
			
		||||
    "chartist": "^0.11.2",
 | 
			
		||||
    "core-js": "^3.4.1",
 | 
			
		||||
    "ng-chartist": "^2.0.0-beta.1",
 | 
			
		||||
    "rxjs": "^6.5.3",
 | 
			
		||||
    "tslib": "^1.9.0",
 | 
			
		||||
    "@angular/animations": "~9.0.0",
 | 
			
		||||
    "@angular/common": "~9.0.0",
 | 
			
		||||
    "@angular/compiler": "~9.0.0",
 | 
			
		||||
    "@angular/core": "~9.0.0",
 | 
			
		||||
    "@angular/forms": "~9.0.0",
 | 
			
		||||
    "@angular/localize": "^9.0.1",
 | 
			
		||||
    "@angular/platform-browser": "~9.0.0",
 | 
			
		||||
    "@angular/platform-browser-dynamic": "~9.0.0",
 | 
			
		||||
    "@angular/router": "~9.0.0",
 | 
			
		||||
    "@ng-bootstrap/ng-bootstrap": "^5.3.0",
 | 
			
		||||
    "@types/qrcode": "^1.3.4",
 | 
			
		||||
    "bootstrap": "^4.4.1",
 | 
			
		||||
    "chartist": "^0.11.4",
 | 
			
		||||
    "clipboard": "^2.0.4",
 | 
			
		||||
    "qrcode": "^1.4.4",
 | 
			
		||||
    "rxjs": "~6.5.3",
 | 
			
		||||
    "tlite": "^0.1.9",
 | 
			
		||||
    "tslib": "^1.10.0",
 | 
			
		||||
    "zone.js": "~0.10.2"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@angular-devkit/build-angular": "~0.800.0",
 | 
			
		||||
    "@angular/cli": "~8.3.12",
 | 
			
		||||
    "@angular/compiler-cli": "^8.2.11",
 | 
			
		||||
    "@angular/language-service": "^8.2.11",
 | 
			
		||||
    "@types/chartist": "^0.9.46",
 | 
			
		||||
    "@types/node": "~8.9.4",
 | 
			
		||||
    "codelyzer": "~5.1.0",
 | 
			
		||||
    "@angular-devkit/build-angular": "~0.900.1",
 | 
			
		||||
    "@angular/cli": "~9.0.1",
 | 
			
		||||
    "@angular/compiler-cli": "~9.0.0",
 | 
			
		||||
    "@angular/language-service": "~9.0.0",
 | 
			
		||||
    "@types/jasmine": "~3.3.8",
 | 
			
		||||
    "@types/jasminewd2": "~2.0.3",
 | 
			
		||||
    "@types/node": "^12.11.1",
 | 
			
		||||
    "codelyzer": "^5.1.2",
 | 
			
		||||
    "jasmine-core": "~3.4.0",
 | 
			
		||||
    "jasmine-spec-reporter": "~4.2.1",
 | 
			
		||||
    "karma": "~4.1.0",
 | 
			
		||||
    "karma-chrome-launcher": "~2.2.0",
 | 
			
		||||
    "karma-coverage-istanbul-reporter": "~2.0.1",
 | 
			
		||||
    "karma-jasmine": "~2.0.1",
 | 
			
		||||
    "karma-jasmine-html-reporter": "^1.4.0",
 | 
			
		||||
    "protractor": "~5.4.0",
 | 
			
		||||
    "ts-node": "~7.0.0",
 | 
			
		||||
    "tslint": "~5.15.0",
 | 
			
		||||
    "typescript": "~3.4.3"
 | 
			
		||||
    "typescript": "~3.6.4"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -2,10 +2,5 @@
 | 
			
		||||
  "/api": {
 | 
			
		||||
    "target": "http://localhost:8999/",
 | 
			
		||||
    "secure": false
 | 
			
		||||
  },
 | 
			
		||||
  "/ws": {
 | 
			
		||||
    "target": "http://localhost:8999/",
 | 
			
		||||
    "secure": false,
 | 
			
		||||
    "ws": true
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,10 +1,13 @@
 | 
			
		||||
import { NgModule } from '@angular/core';
 | 
			
		||||
import { Routes, RouterModule } from '@angular/router';
 | 
			
		||||
import { BlockchainComponent } from './blockchain/blockchain.component';
 | 
			
		||||
import { AboutComponent } from './about/about.component';
 | 
			
		||||
import { StatisticsComponent } from './statistics/statistics.component';
 | 
			
		||||
import { TelevisionComponent } from './television/television.component';
 | 
			
		||||
import { MasterPageComponent } from './master-page/master-page.component';
 | 
			
		||||
import { StartComponent } from './components/start/start.component';
 | 
			
		||||
import { TransactionComponent } from './components/transaction/transaction.component';
 | 
			
		||||
import { BlockComponent } from './components/block/block.component';
 | 
			
		||||
import { AddressComponent } from './components/address/address.component';
 | 
			
		||||
import { MasterPageComponent } from './components/master-page/master-page.component';
 | 
			
		||||
import { AboutComponent } from './components/about/about.component';
 | 
			
		||||
import { TelevisionComponent } from './components/television/television.component';
 | 
			
		||||
import { StatisticsComponent } from './components/statistics/statistics.component';
 | 
			
		||||
 | 
			
		||||
const routes: Routes = [
 | 
			
		||||
  {
 | 
			
		||||
@ -13,30 +16,30 @@ const routes: Routes = [
 | 
			
		||||
    children: [
 | 
			
		||||
      {
 | 
			
		||||
        path: '',
 | 
			
		||||
        children: [],
 | 
			
		||||
        component: BlockchainComponent
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'tx/:id',
 | 
			
		||||
        children: [],
 | 
			
		||||
        component: BlockchainComponent
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'about',
 | 
			
		||||
        children: [],
 | 
			
		||||
        component: AboutComponent
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'statistics',
 | 
			
		||||
        component: StatisticsComponent,
 | 
			
		||||
        component: StartComponent,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'graphs',
 | 
			
		||||
        component: StatisticsComponent,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'explorer',
 | 
			
		||||
        loadChildren: './explorer/explorer.module#ExplorerModule',
 | 
			
		||||
        path: 'about',
 | 
			
		||||
        component: AboutComponent,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'tx/:id',
 | 
			
		||||
        children: [],
 | 
			
		||||
        component: TransactionComponent
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'block/:id',
 | 
			
		||||
        children: [],
 | 
			
		||||
        component: BlockComponent
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        path: 'address/:id',
 | 
			
		||||
        children: [],
 | 
			
		||||
        component: AddressComponent
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
@ -49,6 +52,7 @@ const routes: Routes = [
 | 
			
		||||
    redirectTo: ''
 | 
			
		||||
  }
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
  imports: [RouterModule.forRoot(routes)],
 | 
			
		||||
  exports: [RouterModule]
 | 
			
		||||
 | 
			
		||||
@ -1 +0,0 @@
 | 
			
		||||
<router-outlet></router-outlet>
 | 
			
		||||
@ -1,10 +0,0 @@
 | 
			
		||||
import { Component } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-root',
 | 
			
		||||
  templateUrl: './app.component.html',
 | 
			
		||||
  styleUrls: ['./app.component.scss']
 | 
			
		||||
})
 | 
			
		||||
export class AppComponent {
 | 
			
		||||
  constructor() { }
 | 
			
		||||
}
 | 
			
		||||
@ -1,56 +1,88 @@
 | 
			
		||||
import { BrowserModule } from '@angular/platform-browser';
 | 
			
		||||
import { NgModule } from '@angular/core';
 | 
			
		||||
 | 
			
		||||
import { AppComponent } from './app.component';
 | 
			
		||||
import { BlockchainComponent } from './blockchain/blockchain.component';
 | 
			
		||||
import { AppRoutingModule } from './app-routing.module';
 | 
			
		||||
import { SharedModule } from './shared/shared.module';
 | 
			
		||||
import { MemPoolService } from './services/mem-pool.service';
 | 
			
		||||
import { HttpClientModule } from '@angular/common/http';
 | 
			
		||||
import { FooterComponent } from './footer/footer.component';
 | 
			
		||||
import { AboutComponent } from './about/about.component';
 | 
			
		||||
import { TxBubbleComponent } from './tx-bubble/tx-bubble.component';
 | 
			
		||||
import { ReactiveFormsModule } from '@angular/forms';
 | 
			
		||||
import { BlockModalComponent } from './blockchain-blocks/block-modal/block-modal.component';
 | 
			
		||||
import { StatisticsComponent } from './statistics/statistics.component';
 | 
			
		||||
import { ProjectedBlockModalComponent } from './blockchain-projected-blocks/projected-block-modal/projected-block-modal.component';
 | 
			
		||||
import { TelevisionComponent } from './television/television.component';
 | 
			
		||||
import { BlockchainBlocksComponent } from './blockchain-blocks/blockchain-blocks.component';
 | 
			
		||||
import { BlockchainProjectedBlocksComponent } from './blockchain-projected-blocks/blockchain-projected-blocks.component';
 | 
			
		||||
import { ApiService } from './services/api.service';
 | 
			
		||||
import { MasterPageComponent } from './master-page/master-page.component';
 | 
			
		||||
import { FeeDistributionGraphComponent } from './fee-distribution-graph/fee-distribution-graph.component';
 | 
			
		||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
 | 
			
		||||
import { NgbButtonsModule } from '@ng-bootstrap/ng-bootstrap';
 | 
			
		||||
 | 
			
		||||
import { AppRoutingModule } from './app-routing.module';
 | 
			
		||||
import { AppComponent } from './components/app/app.component';
 | 
			
		||||
 | 
			
		||||
import { StartComponent } from './components/start/start.component';
 | 
			
		||||
import { ElectrsApiService } from './services/electrs-api.service';
 | 
			
		||||
import { TimeSincePipe } from './pipes/time-since/time-since.pipe';
 | 
			
		||||
import { BytesPipe } from './pipes/bytes-pipe/bytes.pipe';
 | 
			
		||||
import { VbytesPipe } from './pipes/bytes-pipe/vbytes.pipe';
 | 
			
		||||
import { WuBytesPipe } from './pipes/bytes-pipe/wubytes.pipe';
 | 
			
		||||
import { TransactionComponent } from './components/transaction/transaction.component';
 | 
			
		||||
import { TransactionsListComponent } from './components/transactions-list/transactions-list.component';
 | 
			
		||||
import { AmountComponent } from './components/amount/amount.component';
 | 
			
		||||
import { StateService } from './services/state.service';
 | 
			
		||||
import { BlockComponent } from './components/block/block.component';
 | 
			
		||||
import { ShortenStringPipe } from './pipes/shorten-string-pipe/shorten-string.pipe';
 | 
			
		||||
import { AddressComponent } from './components/address/address.component';
 | 
			
		||||
import { SearchFormComponent } from './components/search-form/search-form.component';
 | 
			
		||||
import { LatestBlocksComponent } from './components/latest-blocks/latest-blocks.component';
 | 
			
		||||
import { WebsocketService } from './services/websocket.service';
 | 
			
		||||
import { TimeSinceComponent } from './components/time-since/time-since.component';
 | 
			
		||||
import { AddressLabelsComponent } from './components/address-labels/address-labels.component';
 | 
			
		||||
import { MempoolBlocksComponent } from './components/mempool-blocks/mempool-blocks.component';
 | 
			
		||||
import { CeilPipe } from './pipes/math-ceil/math-ceil.pipe';
 | 
			
		||||
import { LatestTransactionsComponent } from './components/latest-transactions/latest-transactions.component';
 | 
			
		||||
import { QrcodeComponent } from './components/qrcode/qrcode.component';
 | 
			
		||||
import { ClipboardComponent } from './components/clipboard/clipboard.component';
 | 
			
		||||
import { MasterPageComponent } from './components/master-page/master-page.component';
 | 
			
		||||
import { AboutComponent } from './components/about/about.component';
 | 
			
		||||
import { TelevisionComponent } from './components/television/television.component';
 | 
			
		||||
import { StatisticsComponent } from './components/statistics/statistics.component';
 | 
			
		||||
import { ChartistComponent } from './components/statistics/chartist.component';
 | 
			
		||||
import { BlockchainBlocksComponent } from './components/blockchain-blocks/blockchain-blocks.component';
 | 
			
		||||
import { BlockchainComponent } from './components/blockchain/blockchain.component';
 | 
			
		||||
 | 
			
		||||
@NgModule({
 | 
			
		||||
  declarations: [
 | 
			
		||||
    AppComponent,
 | 
			
		||||
    BlockchainComponent,
 | 
			
		||||
    FooterComponent,
 | 
			
		||||
    StatisticsComponent,
 | 
			
		||||
    AboutComponent,
 | 
			
		||||
    TxBubbleComponent,
 | 
			
		||||
    BlockModalComponent,
 | 
			
		||||
    ProjectedBlockModalComponent,
 | 
			
		||||
    TelevisionComponent,
 | 
			
		||||
    BlockchainBlocksComponent,
 | 
			
		||||
    BlockchainProjectedBlocksComponent,
 | 
			
		||||
    MasterPageComponent,
 | 
			
		||||
    FeeDistributionGraphComponent,
 | 
			
		||||
    TelevisionComponent,
 | 
			
		||||
    BlockchainComponent,
 | 
			
		||||
    StartComponent,
 | 
			
		||||
    BlockchainBlocksComponent,
 | 
			
		||||
    StatisticsComponent,
 | 
			
		||||
    TransactionComponent,
 | 
			
		||||
    BlockComponent,
 | 
			
		||||
    TransactionsListComponent,
 | 
			
		||||
    TimeSincePipe,
 | 
			
		||||
    BytesPipe,
 | 
			
		||||
    VbytesPipe,
 | 
			
		||||
    WuBytesPipe,
 | 
			
		||||
    CeilPipe,
 | 
			
		||||
    ShortenStringPipe,
 | 
			
		||||
    AddressComponent,
 | 
			
		||||
    AmountComponent,
 | 
			
		||||
    SearchFormComponent,
 | 
			
		||||
    LatestBlocksComponent,
 | 
			
		||||
    TimeSinceComponent,
 | 
			
		||||
    AddressLabelsComponent,
 | 
			
		||||
    MempoolBlocksComponent,
 | 
			
		||||
    LatestTransactionsComponent,
 | 
			
		||||
    QrcodeComponent,
 | 
			
		||||
    ClipboardComponent,
 | 
			
		||||
    ChartistComponent,
 | 
			
		||||
  ],
 | 
			
		||||
  imports: [
 | 
			
		||||
    ReactiveFormsModule,
 | 
			
		||||
    BrowserModule,
 | 
			
		||||
    HttpClientModule,
 | 
			
		||||
    AppRoutingModule,
 | 
			
		||||
    SharedModule,
 | 
			
		||||
    HttpClientModule,
 | 
			
		||||
    ReactiveFormsModule,
 | 
			
		||||
    BrowserAnimationsModule,
 | 
			
		||||
    NgbButtonsModule,
 | 
			
		||||
  ],
 | 
			
		||||
  providers: [
 | 
			
		||||
    ApiService,
 | 
			
		||||
    MemPoolService,
 | 
			
		||||
  ],
 | 
			
		||||
  entryComponents: [
 | 
			
		||||
    BlockModalComponent,
 | 
			
		||||
    ProjectedBlockModalComponent,
 | 
			
		||||
    ElectrsApiService,
 | 
			
		||||
    StateService,
 | 
			
		||||
    WebsocketService,
 | 
			
		||||
    VbytesPipe,
 | 
			
		||||
  ],
 | 
			
		||||
  bootstrap: [AppComponent]
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
@ -1,37 +0,0 @@
 | 
			
		||||
<div class="modal-header">
 | 
			
		||||
  <h4 class="modal-title">Fee distribution for block 
 | 
			
		||||
    <a *ngIf="!isElectrsEnabled" href="https://www.blockstream.info/block-height/{{ block.height }}" target="_blank">#{{ block.height }}</a>
 | 
			
		||||
    <a *ngIf="isElectrsEnabled" (click)="activeModal.dismiss()" [routerLink]="['/explorer/block/', block.hash]">#{{ block.height }}</a>
 | 
			
		||||
  </h4>
 | 
			
		||||
  <button type="button" class="close" aria-label="Close" (click)="activeModal.dismiss('Cross click')">
 | 
			
		||||
    <span aria-hidden="true">×</span>
 | 
			
		||||
  </button>
 | 
			
		||||
</div>
 | 
			
		||||
<div class="modal-body">
 | 
			
		||||
  <div>
 | 
			
		||||
    <table class="table table-borderless table-sm">
 | 
			
		||||
      <tr>
 | 
			
		||||
        <th>Median fee:</th>
 | 
			
		||||
        <td>~{{ block.medianFee | ceil }} sat/vB <span *ngIf="conversions">(~<span class="green-color">{{ conversions.USD * (block.medianFee/100000000)*250 | currency:'USD':'symbol':'1.2-2' }}</span>/tx)</span></td>
 | 
			
		||||
        <th>Block size:</th>
 | 
			
		||||
        <td>{{ block.size | bytes: 2 }}</td>
 | 
			
		||||
      </tr>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <th>Fee span:</th>
 | 
			
		||||
        <td class="yellow-color">{{ block.minFee | ceil }} - {{ block.maxFee | ceil }} sat/vB</td>
 | 
			
		||||
        <th>Tx count:</th>
 | 
			
		||||
        <td>{{ block.nTx }} transactions</td>
 | 
			
		||||
      </tr>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <th>Total fees:</th>
 | 
			
		||||
        <td>{{ (block.fees - blockSubsidy) | number: '1.2-2' }} BTC <span *ngIf="conversions">(<span class="green-color">{{ conversions.USD * (block.fees - blockSubsidy) | currency:'USD':'symbol':'1.0-0' }}</span>)</span></td>
 | 
			
		||||
        <th>Block reward + fees:</th>
 | 
			
		||||
        <td>{{ block.fees | number: '1.2-2' }} BTC <span *ngIf="conversions">(<span class="green-color">{{ conversions.USD * block.fees | currency:'USD':'symbol':'1.0-0' }}</span>)</span></td>
 | 
			
		||||
      </tr>
 | 
			
		||||
    </table>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <hr>
 | 
			
		||||
 | 
			
		||||
  <app-fee-distribution-graph [blockHeight]="block.height"></app-fee-distribution-graph>
 | 
			
		||||
</div>
 | 
			
		||||
@ -1,35 +0,0 @@
 | 
			
		||||
import { Component, OnInit, Input } from '@angular/core';
 | 
			
		||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
 | 
			
		||||
import { IBlock } from '../../blockchain/interfaces';
 | 
			
		||||
import { MemPoolService } from '../../services/mem-pool.service';
 | 
			
		||||
import { environment } from '../../../environments/environment';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-block-modal',
 | 
			
		||||
  templateUrl: './block-modal.component.html',
 | 
			
		||||
  styleUrls: ['./block-modal.component.scss']
 | 
			
		||||
})
 | 
			
		||||
export class BlockModalComponent implements OnInit {
 | 
			
		||||
  @Input() block: IBlock;
 | 
			
		||||
  blockSubsidy = 50;
 | 
			
		||||
  isElectrsEnabled = !!environment.electrs;
 | 
			
		||||
  conversions: any;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    public activeModal: NgbActiveModal,
 | 
			
		||||
    private memPoolService: MemPoolService,
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
    this.memPoolService.conversions$
 | 
			
		||||
      .subscribe((conversions) => {
 | 
			
		||||
        this.conversions = conversions;
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
    let halvenings = Math.floor(this.block.height / 210000);
 | 
			
		||||
    while (halvenings > 0) {
 | 
			
		||||
      this.blockSubsidy = this.blockSubsidy / 2;
 | 
			
		||||
      halvenings--;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,21 +0,0 @@
 | 
			
		||||
<div class="blocks-container" *ngIf="blocks.length">
 | 
			
		||||
  <div *ngFor="let block of blocks; let i = index; trackBy: trackByBlocksFn" >
 | 
			
		||||
    <div (click)="openBlockModal(block);" class="text-center bitcoin-block mined-block" id="bitcoin-block-{{ block.height }}" [ngStyle]="getStyleForBlock(block)">
 | 
			
		||||
      <div class="block-height">
 | 
			
		||||
        <a *ngIf="!isElectrsEnabled" href="https://www.blockstream.info/block-height/{{ block.height }}" target="_blank">#{{ block.height }}</a>
 | 
			
		||||
        <a *ngIf="isElectrsEnabled" [routerLink]="['/explorer/block/', block.hash]">#{{ block.height }}</a>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="block-body">
 | 
			
		||||
        <div class="fees">
 | 
			
		||||
          ~{{ block.medianFee | ceil }} sat/vB
 | 
			
		||||
          <br/>
 | 
			
		||||
          <span class="yellow-color">{{ block.minFee | ceil }} - {{ block.maxFee | ceil }} sat/vB</span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="block-size">{{ block.size | bytes: 2 }}</div>
 | 
			
		||||
        <div class="transaction-count">{{ block.nTx }} transactions</div>
 | 
			
		||||
        <br /><br />
 | 
			
		||||
        <div class="time-difference">{{ block.time | timeSince : trigger }} ago</div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
@ -1,20 +0,0 @@
 | 
			
		||||
<div class="projected-blocks-container">
 | 
			
		||||
  <div *ngFor="let projectedBlock of projectedBlocks; let i = index; trackBy: trackByProjectedFn">
 | 
			
		||||
    <div (click)="openProjectedBlockModal(projectedBlock, i);" class="bitcoin-block text-center projected-block" id="projected-block-{{ i }}" [ngStyle]="getStyleForProjectedBlockAtIndex(i)">
 | 
			
		||||
      <div class="block-body" *ngIf="projectedBlocks?.length">
 | 
			
		||||
        <div class="fees">
 | 
			
		||||
          ~{{ projectedBlock.medianFee | ceil }} sat/vB
 | 
			
		||||
          <br/>
 | 
			
		||||
          <span class="yellow-color">{{ projectedBlock.minFee | ceil }} - {{ projectedBlock.maxFee | ceil }} sat/vB</span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="block-size">{{ projectedBlock.blockSize | bytes: 2 }}</div>
 | 
			
		||||
        <div class="transaction-count">{{ projectedBlock.nTx }} transactions</div>
 | 
			
		||||
        <div class="time-difference" *ngIf="i !== 3">In ~{{ 10 * i + 10 }} minutes</div>
 | 
			
		||||
        <ng-template [ngIf]="i === 3 && projectedBlocks?.length >= 4 && (projectedBlock.blockWeight / 4000000 | ceil) > 1">
 | 
			
		||||
          <div class="time-difference">+{{ projectedBlock.blockWeight / 4000000 | ceil }} blocks</div>
 | 
			
		||||
        </ng-template>
 | 
			
		||||
      </div>
 | 
			
		||||
      <span class="animated-border"></span>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
@ -1,58 +0,0 @@
 | 
			
		||||
import { Component, OnInit, OnDestroy } from '@angular/core';
 | 
			
		||||
import { IProjectedBlock, IBlock } from '../blockchain/interfaces';
 | 
			
		||||
import { ProjectedBlockModalComponent } from './projected-block-modal/projected-block-modal.component';
 | 
			
		||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
 | 
			
		||||
import { MemPoolService } from '../services/mem-pool.service';
 | 
			
		||||
import { Subscription } from 'rxjs';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-blockchain-projected-blocks',
 | 
			
		||||
  templateUrl: './blockchain-projected-blocks.component.html',
 | 
			
		||||
  styleUrls: ['./blockchain-projected-blocks.component.scss']
 | 
			
		||||
})
 | 
			
		||||
export class BlockchainProjectedBlocksComponent implements OnInit, OnDestroy {
 | 
			
		||||
  projectedBlocks: IProjectedBlock[];
 | 
			
		||||
  subscription: Subscription;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private modalService: NgbModal,
 | 
			
		||||
    private memPoolService: MemPoolService,
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
    this.subscription = this.memPoolService.projectedBlocks$
 | 
			
		||||
      .subscribe((projectedblocks) => this.projectedBlocks = projectedblocks);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnDestroy() {
 | 
			
		||||
    this.subscription.unsubscribe();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  trackByProjectedFn(index: number) {
 | 
			
		||||
    return index;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  openProjectedBlockModal(block: IBlock, index: number) {
 | 
			
		||||
    const modalRef = this.modalService.open(ProjectedBlockModalComponent, { size: 'lg' });
 | 
			
		||||
    modalRef.componentInstance.block = block;
 | 
			
		||||
    modalRef.componentInstance.index = index;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getStyleForProjectedBlockAtIndex(index: number) {
 | 
			
		||||
    const greenBackgroundHeight = 100 - (this.projectedBlocks[index].blockWeight / 4000000) * 100;
 | 
			
		||||
    if (window.innerWidth <= 768) {
 | 
			
		||||
      return {
 | 
			
		||||
        'top': 40 + index * 155 + 'px',
 | 
			
		||||
        'background': `repeating-linear-gradient(#554b45, #554b45 ${greenBackgroundHeight}%,
 | 
			
		||||
          #bd7c13 ${Math.max(greenBackgroundHeight, 0)}%, #c5345a 100%)`,
 | 
			
		||||
      };
 | 
			
		||||
    } else {
 | 
			
		||||
      return {
 | 
			
		||||
        'right': 40 + index * 155 + 'px',
 | 
			
		||||
        'background': `repeating-linear-gradient(#554b45, #554b45 ${greenBackgroundHeight}%,
 | 
			
		||||
          #bd7c13 ${Math.max(greenBackgroundHeight, 0)}%, #c5345a 100%)`,
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -1,30 +0,0 @@
 | 
			
		||||
<div class="modal-header">
 | 
			
		||||
  <h4 class="modal-title">Fee distribution for projected block</h4>
 | 
			
		||||
  <button type="button" class="close" aria-label="Close" (click)="activeModal.dismiss('Cross click')">
 | 
			
		||||
    <span aria-hidden="true">×</span>
 | 
			
		||||
  </button>
 | 
			
		||||
</div>
 | 
			
		||||
<div class="modal-body">
 | 
			
		||||
  <div>
 | 
			
		||||
    <table class="table table-borderless table-sm">
 | 
			
		||||
      <tr>
 | 
			
		||||
        <th>Median fee:</th>
 | 
			
		||||
        <td>~{{ block.medianFee | ceil }} sat/vB <span *ngIf="conversions">(~<span class="green-color">{{ conversions.USD * (block.medianFee/100000000)*250 | currency:'USD':'symbol':'1.2-2' }}</span>/tx)</span></td>
 | 
			
		||||
        <th>Tx count:</th>
 | 
			
		||||
        <td>{{ block.nTx }} transactions</td>
 | 
			
		||||
      </tr>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <th>Fee span:</th>
 | 
			
		||||
        <td class="yellow-color">{{ block.minFee | ceil }} - {{ block.maxFee | ceil }} sat/vB</td>
 | 
			
		||||
      </tr>
 | 
			
		||||
      <tr>
 | 
			
		||||
        <th>Total fees:</th>
 | 
			
		||||
        <td>{{ block.fees | number: '1.2-2' }} BTC <span *ngIf="conversions">(<span class="green-color">{{ conversions.USD * block.fees| currency:'USD':'symbol':'1.0-0' }}</span>)</span></td>
 | 
			
		||||
      </tr>
 | 
			
		||||
    </table>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <hr>
 | 
			
		||||
 | 
			
		||||
  <app-fee-distribution-graph [projectedBlockIndex]="index"></app-fee-distribution-graph>
 | 
			
		||||
</div>
 | 
			
		||||
@ -1,29 +0,0 @@
 | 
			
		||||
import { Component, OnInit, Input } from '@angular/core';
 | 
			
		||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
 | 
			
		||||
import { MemPoolService } from '../../services/mem-pool.service';
 | 
			
		||||
import { IBlock } from 'src/app/blockchain/interfaces';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-projected-block-modal',
 | 
			
		||||
  templateUrl: './projected-block-modal.component.html',
 | 
			
		||||
  styleUrls: ['./projected-block-modal.component.scss']
 | 
			
		||||
})
 | 
			
		||||
export class ProjectedBlockModalComponent implements OnInit {
 | 
			
		||||
  @Input() block: IBlock;
 | 
			
		||||
  @Input() index: number;
 | 
			
		||||
 | 
			
		||||
  conversions: any;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    public activeModal: NgbActiveModal,
 | 
			
		||||
    private memPoolService: MemPoolService,
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
    this.memPoolService.conversions$
 | 
			
		||||
      .subscribe((conversions) => {
 | 
			
		||||
        this.conversions = conversions;
 | 
			
		||||
      });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -1,177 +0,0 @@
 | 
			
		||||
export interface IMempoolInfo {
 | 
			
		||||
  size: number;
 | 
			
		||||
  bytes: number;
 | 
			
		||||
  usage: number;
 | 
			
		||||
  maxmempool: number;
 | 
			
		||||
  mempoolminfee: number;
 | 
			
		||||
  minrelaytxfee: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IMempoolDefaultResponse {
 | 
			
		||||
  mempoolInfo?: IMempoolInfo;
 | 
			
		||||
  blocks?: IBlock[];
 | 
			
		||||
  block?: IBlock;
 | 
			
		||||
  projectedBlocks?: IProjectedBlock[];
 | 
			
		||||
  'live-2h-chart'?: IMempoolStats;
 | 
			
		||||
  txPerSecond?: number;
 | 
			
		||||
  vBytesPerSecond: number;
 | 
			
		||||
  'track-tx'?: ITrackTx;
 | 
			
		||||
  conversions?: any;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ITrackTx {
 | 
			
		||||
  tx?: ITransaction;
 | 
			
		||||
  blockHeight: number;
 | 
			
		||||
  tracking: boolean;
 | 
			
		||||
  message?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IProjectedBlock {
 | 
			
		||||
  blockSize: number;
 | 
			
		||||
  blockWeight: number;
 | 
			
		||||
  maxFee: number;
 | 
			
		||||
  maxWeightFee: number;
 | 
			
		||||
  medianFee: number;
 | 
			
		||||
  minFee: number;
 | 
			
		||||
  minWeightFee: number;
 | 
			
		||||
  nTx: number;
 | 
			
		||||
  hasMytx: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IStrippedBlock {
 | 
			
		||||
  bits: number;
 | 
			
		||||
  difficulty: number;
 | 
			
		||||
  hash: string;
 | 
			
		||||
  height: number;
 | 
			
		||||
  nTx: number;
 | 
			
		||||
  size: number;
 | 
			
		||||
  strippedsize: number;
 | 
			
		||||
  time: number;
 | 
			
		||||
  weight: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ITransaction {
 | 
			
		||||
  txid: string;
 | 
			
		||||
  hash: string;
 | 
			
		||||
  version: number;
 | 
			
		||||
  size: number;
 | 
			
		||||
  vsize: number;
 | 
			
		||||
  locktime: number;
 | 
			
		||||
  vin: Vin[];
 | 
			
		||||
  vout: Vout[];
 | 
			
		||||
  hex: string;
 | 
			
		||||
 | 
			
		||||
  fee: number;
 | 
			
		||||
  feePerVsize: number;
 | 
			
		||||
  feePerWeightUnit: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IBlock {
 | 
			
		||||
  hash: string;
 | 
			
		||||
  confirmations: number;
 | 
			
		||||
  strippedsize: number;
 | 
			
		||||
  size: number;
 | 
			
		||||
  weight: number;
 | 
			
		||||
  height: number;
 | 
			
		||||
  version: number;
 | 
			
		||||
  versionHex: string;
 | 
			
		||||
  merkleroot: string;
 | 
			
		||||
  tx: ITransaction[];
 | 
			
		||||
  time: number;
 | 
			
		||||
  mediantime: number;
 | 
			
		||||
  nonce: number;
 | 
			
		||||
  bits: string;
 | 
			
		||||
  difficulty: number;
 | 
			
		||||
  chainwork: string;
 | 
			
		||||
  nTx: number;
 | 
			
		||||
  previousblockhash: string;
 | 
			
		||||
 | 
			
		||||
  minFee: number;
 | 
			
		||||
  maxFee: number;
 | 
			
		||||
  medianFee: number;
 | 
			
		||||
  fees: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface ScriptSig {
 | 
			
		||||
  asm: string;
 | 
			
		||||
  hex: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface Vin {
 | 
			
		||||
  txid: string;
 | 
			
		||||
  vout: number;
 | 
			
		||||
  scriptSig: ScriptSig;
 | 
			
		||||
  sequence: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface ScriptPubKey {
 | 
			
		||||
  asm: string;
 | 
			
		||||
  hex: string;
 | 
			
		||||
  reqSigs: number;
 | 
			
		||||
  type: string;
 | 
			
		||||
  addresses: string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface Vout {
 | 
			
		||||
  value: number;
 | 
			
		||||
  n: number;
 | 
			
		||||
  scriptPubKey: ScriptPubKey;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IMempoolStats {
 | 
			
		||||
  id: number;
 | 
			
		||||
  added: string;
 | 
			
		||||
  unconfirmed_transactions: number;
 | 
			
		||||
  tx_per_second: number;
 | 
			
		||||
  vbytes_per_second: number;
 | 
			
		||||
  mempool_byte_weight: number;
 | 
			
		||||
  fee_data: IFeeData;
 | 
			
		||||
  vsize_1: number;
 | 
			
		||||
  vsize_2: number;
 | 
			
		||||
  vsize_3: number;
 | 
			
		||||
  vsize_4: number;
 | 
			
		||||
  vsize_5: number;
 | 
			
		||||
  vsize_6: number;
 | 
			
		||||
  vsize_8: number;
 | 
			
		||||
  vsize_10: number;
 | 
			
		||||
  vsize_12: number;
 | 
			
		||||
  vsize_15: number;
 | 
			
		||||
  vsize_20: number;
 | 
			
		||||
  vsize_30: number;
 | 
			
		||||
  vsize_40: number;
 | 
			
		||||
  vsize_50: number;
 | 
			
		||||
  vsize_60: number;
 | 
			
		||||
  vsize_70: number;
 | 
			
		||||
  vsize_80: number;
 | 
			
		||||
  vsize_90: number;
 | 
			
		||||
  vsize_100: number;
 | 
			
		||||
  vsize_125: number;
 | 
			
		||||
  vsize_150: number;
 | 
			
		||||
  vsize_175: number;
 | 
			
		||||
  vsize_200: number;
 | 
			
		||||
  vsize_250: number;
 | 
			
		||||
  vsize_300: number;
 | 
			
		||||
  vsize_350: number;
 | 
			
		||||
  vsize_400: number;
 | 
			
		||||
  vsize_500: number;
 | 
			
		||||
  vsize_600: number;
 | 
			
		||||
  vsize_700: number;
 | 
			
		||||
  vsize_800: number;
 | 
			
		||||
  vsize_900: number;
 | 
			
		||||
  vsize_1000: number;
 | 
			
		||||
  vsize_1200: number;
 | 
			
		||||
  vsize_1400: number;
 | 
			
		||||
  vsize_1600: number;
 | 
			
		||||
  vsize_1800: number;
 | 
			
		||||
  vsize_2000: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IBlockTransaction {
 | 
			
		||||
  f: number;
 | 
			
		||||
  fpv: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface IFeeData {
 | 
			
		||||
  wu: { [ fee: string ]: number };
 | 
			
		||||
  vsize: { [ fee: string ]: number };
 | 
			
		||||
}
 | 
			
		||||
@ -4,10 +4,10 @@
 | 
			
		||||
 | 
			
		||||
  <h2>About</h2>
 | 
			
		||||
 | 
			
		||||
  <p>Mempool.Space is a realtime Bitcoin blockchain visualizer and statistics website focused on SegWit.</p>
 | 
			
		||||
  <p>Created by <a href="http://t.me/softcrypto">@softcrypto</a> (Telegram). <a href="https://twitter.com/softcrypt0">@softcrypt0</a> (Twitter).
 | 
			
		||||
  <br />Designed by <a href="https://emeraldo.io">emeraldo.io</a>.
 | 
			
		||||
  <br />Hosted by <a href="https://twitter.com/wiz">@wiz</a></p>
 | 
			
		||||
  <p>Mempool.Space is a realtime Bitcoin blockchain explorer and mempool visualizer.</p>
 | 
			
		||||
  <p>Created by <a href="https://twitter.com/softbtc">@softbtc</a>
 | 
			
		||||
  <br />Hosted by <a href="https://twitter.com/wiz">@wiz</a>
 | 
			
		||||
  <br />Designed by <a href="https://instagram.com/markjborg">@markjborg</a>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  <h2>Fee API</h2>
 | 
			
		||||
@ -19,18 +19,10 @@
 | 
			
		||||
  <br />
 | 
			
		||||
 | 
			
		||||
  <h1>Donate</h1>
 | 
			
		||||
  <h3>Segwit native</h3>
 | 
			
		||||
  <img src="./assets/btc-qr-code-segwit.png" width="200" height="200" />
 | 
			
		||||
  <br />
 | 
			
		||||
  bc1qqrmgr60uetlmrpylhtllawyha9z5gw6hwdmk2t
 | 
			
		||||
 | 
			
		||||
  <br /><br />
 | 
			
		||||
  <h3>Segwit compatibility</h3>
 | 
			
		||||
  <img src="./assets/btc-qr-code.png" width="200" height="200" />
 | 
			
		||||
  <br />
 | 
			
		||||
  3Ccig4G4u8hbExnxBJHeE5ZmxxWxvEQ65f
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  <br /><br />
 | 
			
		||||
 | 
			
		||||
  <h3>PayNym</h3>
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
import { Component, OnInit } from '@angular/core';
 | 
			
		||||
import { ApiService } from '../services/api.service';
 | 
			
		||||
import { WebsocketService } from '../../services/websocket.service';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-about',
 | 
			
		||||
@ -9,11 +9,11 @@ import { ApiService } from '../services/api.service';
 | 
			
		||||
export class AboutComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private apiService: ApiService,
 | 
			
		||||
    private websocketService: WebsocketService,
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
    this.apiService.webSocketWant([]);
 | 
			
		||||
    this.websocketService.want([]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1 @@
 | 
			
		||||
<span *ngIf="multisig" class="badge badge-pill badge-warning">multisig {{ multisigM }} of {{ multisigN }}</span>
 | 
			
		||||
@ -0,0 +1,3 @@
 | 
			
		||||
.badge {
 | 
			
		||||
  margin-right: 2px;
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,25 @@
 | 
			
		||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
 | 
			
		||||
 | 
			
		||||
import { AddressLabelsComponent } from './address-labels.component';
 | 
			
		||||
 | 
			
		||||
describe('AddressLabelsComponent', () => {
 | 
			
		||||
  let component: AddressLabelsComponent;
 | 
			
		||||
  let fixture: ComponentFixture<AddressLabelsComponent>;
 | 
			
		||||
 | 
			
		||||
  beforeEach(async(() => {
 | 
			
		||||
    TestBed.configureTestingModule({
 | 
			
		||||
      declarations: [ AddressLabelsComponent ]
 | 
			
		||||
    })
 | 
			
		||||
    .compileComponents();
 | 
			
		||||
  }));
 | 
			
		||||
 | 
			
		||||
  beforeEach(() => {
 | 
			
		||||
    fixture = TestBed.createComponent(AddressLabelsComponent);
 | 
			
		||||
    component = fixture.componentInstance;
 | 
			
		||||
    fixture.detectChanges();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should create', () => {
 | 
			
		||||
    expect(component).toBeTruthy();
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@ -0,0 +1,60 @@
 | 
			
		||||
import { Component, OnInit, ChangeDetectionStrategy, Input } from '@angular/core';
 | 
			
		||||
import { Vin, Vout } from '../../interfaces/electrs.interface';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-address-labels',
 | 
			
		||||
  templateUrl: './address-labels.component.html',
 | 
			
		||||
  styleUrls: ['./address-labels.component.scss'],
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class AddressLabelsComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
  @Input() vin: Vin;
 | 
			
		||||
  @Input() vout: Vout;
 | 
			
		||||
 | 
			
		||||
  multisig = false;
 | 
			
		||||
  multisigM: number;
 | 
			
		||||
  multisigN: number;
 | 
			
		||||
 | 
			
		||||
  constructor() { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
    if (this.vin) {
 | 
			
		||||
      this.handleVin();
 | 
			
		||||
    } else if (this.vout) {
 | 
			
		||||
      this.handleVout();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleVin() {
 | 
			
		||||
    if (this.vin.inner_witnessscript_asm && this.vin.inner_witnessscript_asm.indexOf('OP_CHECKMULTISIG') > -1) {
 | 
			
		||||
      const matches = this.getMatches(this.vin.inner_witnessscript_asm, /OP_PUSHNUM_([0-9])/g, 1);
 | 
			
		||||
      this.multisig = true;
 | 
			
		||||
      this.multisigM = matches[0];
 | 
			
		||||
      this.multisigN = matches[1];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this.vin.inner_redeemscript_asm && this.vin.inner_redeemscript_asm.indexOf('OP_CHECKMULTISIG') > -1) {
 | 
			
		||||
      const matches = this.getMatches(this.vin.inner_redeemscript_asm, /OP_PUSHNUM_([0-9])/g, 1);
 | 
			
		||||
      this.multisig = true;
 | 
			
		||||
      this.multisigM = matches[0];
 | 
			
		||||
      this.multisigN = matches[1];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleVout() {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getMatches(str: string, regex: RegExp, index: number) {
 | 
			
		||||
    if (!index) {
 | 
			
		||||
      index = 1;
 | 
			
		||||
    }
 | 
			
		||||
    const matches = [];
 | 
			
		||||
    let match;
 | 
			
		||||
    while (match = regex.exec(str)) {
 | 
			
		||||
      matches.push(match[index]);
 | 
			
		||||
    }
 | 
			
		||||
    return matches;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										100
									
								
								frontend/src/app/components/address/address.component.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								frontend/src/app/components/address/address.component.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,100 @@
 | 
			
		||||
<div class="container">
 | 
			
		||||
  <h1 style="float: left;">Address</h1>
 | 
			
		||||
  <a [routerLink]="['/address/', addressString]" style="line-height: 55px; margin-left: 10px;">{{ addressString }}</a>
 | 
			
		||||
  <app-clipboard [text]="addressString"></app-clipboard>
 | 
			
		||||
  <br>
 | 
			
		||||
 | 
			
		||||
  <div class="clearfix"></div>
 | 
			
		||||
 | 
			
		||||
  <ng-template [ngIf]="!isLoadingAddress && !error">
 | 
			
		||||
    <div class="box">
 | 
			
		||||
 | 
			
		||||
      <div class="row">
 | 
			
		||||
        <div class="col">
 | 
			
		||||
          <table class="table table-borderless table-striped">
 | 
			
		||||
            <tbody>
 | 
			
		||||
              <tr>
 | 
			
		||||
                <td>Number of transactions</td>
 | 
			
		||||
                <td>{{ address.chain_stats.tx_count + address.mempool_stats.tx_count }}</td>
 | 
			
		||||
              </tr>
 | 
			
		||||
              <tr>
 | 
			
		||||
                <td>Total received</td>
 | 
			
		||||
                <td>{{ (address.chain_stats.funded_txo_sum + address.mempool_stats.funded_txo_sum) / 100000000 | number: '1.2-2' }} BTC</td>
 | 
			
		||||
              </tr>
 | 
			
		||||
              <tr>
 | 
			
		||||
                <td>Total sent</td>
 | 
			
		||||
                <td>{{ (address.chain_stats.spent_txo_sum + address.mempool_stats.spent_txo_sum) / 100000000 | number: '1.2-2' }} BTC</td>
 | 
			
		||||
              </tr>
 | 
			
		||||
            </tbody>
 | 
			
		||||
          </table>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="col text-right">
 | 
			
		||||
          <div class="qr-wrapper">
 | 
			
		||||
            <app-qrcode [data]="address.address"></app-qrcode>
 | 
			
		||||
            <!--qrcode id="qrCode" [qrdata]="address.address" [size]="128" [level]="'M'"></qrcode>-->
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <br>
 | 
			
		||||
 | 
			
		||||
    <h2><ng-template [ngIf]="transactions?.length">{{ transactions?.length || '?' }} of </ng-template>{{ address.chain_stats.tx_count + address.mempool_stats.tx_count }} transactions</h2>
 | 
			
		||||
 | 
			
		||||
    <app-transactions-list [transactions]="transactions" [showConfirmations]="true"></app-transactions-list>
 | 
			
		||||
 | 
			
		||||
    <div class="text-center">
 | 
			
		||||
      <ng-template [ngIf]="isLoadingTransactions">
 | 
			
		||||
        <div class="spinner-border"></div>
 | 
			
		||||
        <br><br>
 | 
			
		||||
      </ng-template>
 | 
			
		||||
      <button *ngIf="transactions?.length && transactions?.length !== (address.chain_stats.tx_count + address.mempool_stats.tx_count)" type="button" class="btn btn-primary" (click)="loadMore()">Load more</button>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
  </ng-template>
 | 
			
		||||
 | 
			
		||||
  <ng-template [ngIf]="isLoadingAddress && !error">
 | 
			
		||||
 | 
			
		||||
    <div class="box">
 | 
			
		||||
      <div class="row">
 | 
			
		||||
        <div class="col">
 | 
			
		||||
          <table class="table table-borderless table-striped">
 | 
			
		||||
            <tbody>
 | 
			
		||||
              <tr>
 | 
			
		||||
                <td colspan="2"><span class="skeleton-loader"></span></td>
 | 
			
		||||
              </tr>
 | 
			
		||||
              <tr>
 | 
			
		||||
                <td colspan="2"><span class="skeleton-loader"></span></td>
 | 
			
		||||
              </tr>
 | 
			
		||||
              <tr>
 | 
			
		||||
                <td colspan="2"><span class="skeleton-loader"></span></td>
 | 
			
		||||
              </tr>
 | 
			
		||||
            </tbody>
 | 
			
		||||
          </table>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="col">
 | 
			
		||||
          
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <br>
 | 
			
		||||
 | 
			
		||||
    <div class="text-center">
 | 
			
		||||
      <div class="spinner-border"></div>
 | 
			
		||||
      <br><br>
 | 
			
		||||
    </div>
 | 
			
		||||
  </ng-template>
 | 
			
		||||
 | 
			
		||||
  <ng-template [ngIf]="error">
 | 
			
		||||
    <div class="text-center">
 | 
			
		||||
      Error loading address data.
 | 
			
		||||
      <br>
 | 
			
		||||
      <i>{{ error.error }}</i>
 | 
			
		||||
    </div>
 | 
			
		||||
  </ng-template>
 | 
			
		||||
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<br>
 | 
			
		||||
@ -1,11 +1,6 @@
 | 
			
		||||
.header-bg {
 | 
			
		||||
  background-color:#653b9c;
 | 
			
		||||
  font-size: 14px;
 | 
			
		||||
}
 | 
			
		||||
.header-bg a {
 | 
			
		||||
  color: #FFF;
 | 
			
		||||
  text-decoration: underline;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.qr-wrapper {
 | 
			
		||||
  background-color: #FFF;
 | 
			
		||||
@ -1,7 +1,8 @@
 | 
			
		||||
import { Component, OnInit, ChangeDetectorRef } from '@angular/core';
 | 
			
		||||
import { Component, OnInit } from '@angular/core';
 | 
			
		||||
import { ActivatedRoute, ParamMap } from '@angular/router';
 | 
			
		||||
import { ApiService } from 'src/app/services/api.service';
 | 
			
		||||
import { ElectrsApiService } from '../../services/electrs-api.service';
 | 
			
		||||
import { switchMap } from 'rxjs/operators';
 | 
			
		||||
import { Address, Transaction } from '../../interfaces/electrs.interface';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-address',
 | 
			
		||||
@ -9,17 +10,16 @@ import { switchMap } from 'rxjs/operators';
 | 
			
		||||
  styleUrls: ['./address.component.scss']
 | 
			
		||||
})
 | 
			
		||||
export class AddressComponent implements OnInit {
 | 
			
		||||
  address: any;
 | 
			
		||||
  address: Address;
 | 
			
		||||
  addressString: string;
 | 
			
		||||
  isLoadingAddress = true;
 | 
			
		||||
  latestBlockHeight: number;
 | 
			
		||||
  transactions: any[];
 | 
			
		||||
  transactions: Transaction[];
 | 
			
		||||
  isLoadingTransactions = true;
 | 
			
		||||
  error: any;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private route: ActivatedRoute,
 | 
			
		||||
    private apiService: ApiService,
 | 
			
		||||
    private ref: ChangeDetectorRef,
 | 
			
		||||
    private electrsApiService: ElectrsApiService,
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
@ -27,15 +27,17 @@ export class AddressComponent implements OnInit {
 | 
			
		||||
      switchMap((params: ParamMap) => {
 | 
			
		||||
        this.error = undefined;
 | 
			
		||||
        this.isLoadingAddress = true;
 | 
			
		||||
        const address: string = params.get('id') || '';
 | 
			
		||||
        return this.apiService.getAddress$(address);
 | 
			
		||||
        this.isLoadingTransactions = true;
 | 
			
		||||
        this.transactions = null;
 | 
			
		||||
        this.addressString = params.get('id') || '';
 | 
			
		||||
        return this.electrsApiService.getAddress$(this.addressString);
 | 
			
		||||
      })
 | 
			
		||||
    )
 | 
			
		||||
    .subscribe((address) => {
 | 
			
		||||
      this.address = address;
 | 
			
		||||
      this.isLoadingAddress = false;
 | 
			
		||||
      window.scrollTo(0, 0);
 | 
			
		||||
      this.getAddressTransactions(address.address);
 | 
			
		||||
      this.ref.markForCheck();
 | 
			
		||||
    },
 | 
			
		||||
    (error) => {
 | 
			
		||||
      console.log(error);
 | 
			
		||||
@ -45,7 +47,7 @@ export class AddressComponent implements OnInit {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getAddressTransactions(address: string) {
 | 
			
		||||
    this.apiService.getAddressTransactions$(address)
 | 
			
		||||
    this.electrsApiService.getAddressTransactions$(address)
 | 
			
		||||
      .subscribe((transactions: any) => {
 | 
			
		||||
        this.transactions = transactions;
 | 
			
		||||
        this.isLoadingTransactions = false;
 | 
			
		||||
@ -54,7 +56,7 @@ export class AddressComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
  loadMore() {
 | 
			
		||||
    this.isLoadingTransactions = true;
 | 
			
		||||
    this.apiService.getAddressTransactionsFromHash$(this.address.id, this.transactions[this.transactions.length - 1].txid)
 | 
			
		||||
    this.electrsApiService.getAddressTransactionsFromHash$(this.address.address, this.transactions[this.transactions.length - 1].txid)
 | 
			
		||||
      .subscribe((transactions) => {
 | 
			
		||||
        this.transactions = this.transactions.concat(transactions);
 | 
			
		||||
        this.isLoadingTransactions = false;
 | 
			
		||||
							
								
								
									
										6
									
								
								frontend/src/app/components/amount/amount.component.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								frontend/src/app/components/amount/amount.component.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,6 @@
 | 
			
		||||
<ng-container *ngIf="(viewFiat$ | async) && (conversions$ | async) as conversions; else viewFiatVin">
 | 
			
		||||
  <span>{{ conversions.USD * (satoshis / 100000000) | currency:'USD':'symbol':'1.2-2' }}</span>
 | 
			
		||||
</ng-container>
 | 
			
		||||
<ng-template #viewFiatVin>
 | 
			
		||||
  {{ satoshis / 100000000 }} BTC
 | 
			
		||||
</ng-template>
 | 
			
		||||
							
								
								
									
										3
									
								
								frontend/src/app/components/amount/amount.component.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								frontend/src/app/components/amount/amount.component.scss
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
.green-color {
 | 
			
		||||
  color: #3bcc49;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										25
									
								
								frontend/src/app/components/amount/amount.component.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								frontend/src/app/components/amount/amount.component.spec.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,25 @@
 | 
			
		||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
 | 
			
		||||
 | 
			
		||||
import { AmountComponent } from './amount.component';
 | 
			
		||||
 | 
			
		||||
describe('AmountComponent', () => {
 | 
			
		||||
  let component: AmountComponent;
 | 
			
		||||
  let fixture: ComponentFixture<AmountComponent>;
 | 
			
		||||
 | 
			
		||||
  beforeEach(async(() => {
 | 
			
		||||
    TestBed.configureTestingModule({
 | 
			
		||||
      declarations: [ AmountComponent ]
 | 
			
		||||
    })
 | 
			
		||||
    .compileComponents();
 | 
			
		||||
  }));
 | 
			
		||||
 | 
			
		||||
  beforeEach(() => {
 | 
			
		||||
    fixture = TestBed.createComponent(AmountComponent);
 | 
			
		||||
    component = fixture.componentInstance;
 | 
			
		||||
    fixture.detectChanges();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should create', () => {
 | 
			
		||||
    expect(component).toBeTruthy();
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										26
									
								
								frontend/src/app/components/amount/amount.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								frontend/src/app/components/amount/amount.component.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,26 @@
 | 
			
		||||
import { Component, OnInit, Input, ChangeDetectionStrategy } from '@angular/core';
 | 
			
		||||
import { StateService } from '../../services/state.service';
 | 
			
		||||
import { Observable } from 'rxjs';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-amount',
 | 
			
		||||
  templateUrl: './amount.component.html',
 | 
			
		||||
  styleUrls: ['./amount.component.scss'],
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush,
 | 
			
		||||
})
 | 
			
		||||
export class AmountComponent implements OnInit {
 | 
			
		||||
  conversions$: Observable<any>;
 | 
			
		||||
  viewFiat$: Observable<boolean>;
 | 
			
		||||
 | 
			
		||||
  @Input() satoshis: number;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private stateService: StateService,
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
    this.viewFiat$ = this.stateService.viewFiat$.asObservable();
 | 
			
		||||
    this.conversions$ = this.stateService.conversions$.asObservable();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								frontend/src/app/components/app/app.component.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								frontend/src/app/components/app/app.component.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
			
		||||
<router-outlet></router-outlet>
 | 
			
		||||
							
								
								
									
										7
									
								
								frontend/src/app/components/app/app.component.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								frontend/src/app/components/app/app.component.scss
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,7 @@
 | 
			
		||||
footer {
 | 
			
		||||
  max-width: 960px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.logo {
 | 
			
		||||
  height: 40px;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										35
									
								
								frontend/src/app/components/app/app.component.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								frontend/src/app/components/app/app.component.spec.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,35 @@
 | 
			
		||||
import { TestBed, async } from '@angular/core/testing';
 | 
			
		||||
import { RouterTestingModule } from '@angular/router/testing';
 | 
			
		||||
import { AppComponent } from './app.component';
 | 
			
		||||
 | 
			
		||||
describe('AppComponent', () => {
 | 
			
		||||
  beforeEach(async(() => {
 | 
			
		||||
    TestBed.configureTestingModule({
 | 
			
		||||
      imports: [
 | 
			
		||||
        RouterTestingModule
 | 
			
		||||
      ],
 | 
			
		||||
      declarations: [
 | 
			
		||||
        AppComponent
 | 
			
		||||
      ],
 | 
			
		||||
    }).compileComponents();
 | 
			
		||||
  }));
 | 
			
		||||
 | 
			
		||||
  it('should create the app', () => {
 | 
			
		||||
    const fixture = TestBed.createComponent(AppComponent);
 | 
			
		||||
    const app = fixture.debugElement.componentInstance;
 | 
			
		||||
    expect(app).toBeTruthy();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it(`should have as title 'mempoolspace'`, () => {
 | 
			
		||||
    const fixture = TestBed.createComponent(AppComponent);
 | 
			
		||||
    const app = fixture.debugElement.componentInstance;
 | 
			
		||||
    expect(app.title).toEqual('mempoolspace');
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should render title in a h1 tag', () => {
 | 
			
		||||
    const fixture = TestBed.createComponent(AppComponent);
 | 
			
		||||
    fixture.detectChanges();
 | 
			
		||||
    const compiled = fixture.debugElement.nativeElement;
 | 
			
		||||
    expect(compiled.querySelector('h1').textContent).toContain('Welcome to mempoolspace!');
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										15
									
								
								frontend/src/app/components/app/app.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								frontend/src/app/components/app/app.component.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,15 @@
 | 
			
		||||
import { Component } from '@angular/core';
 | 
			
		||||
import { Router } from '@angular/router';
 | 
			
		||||
import { WebsocketService } from '../../services/websocket.service';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-root',
 | 
			
		||||
  templateUrl: './app.component.html',
 | 
			
		||||
  styleUrls: ['./app.component.scss']
 | 
			
		||||
})
 | 
			
		||||
export class AppComponent {
 | 
			
		||||
  constructor(
 | 
			
		||||
    public router: Router,
 | 
			
		||||
    private websocketService: WebsocketService,
 | 
			
		||||
  ) { }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										134
									
								
								frontend/src/app/components/block/block.component.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								frontend/src/app/components/block/block.component.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,134 @@
 | 
			
		||||
<div class="container">
 | 
			
		||||
 | 
			
		||||
  <app-blockchain></app-blockchain>
 | 
			
		||||
 | 
			
		||||
  <h1>Block <ng-template [ngIf]="blockHeight"><a [routerLink]="['/block/', blockHash]">#{{ blockHeight }}</a></ng-template></h1>
 | 
			
		||||
  
 | 
			
		||||
    <ng-template [ngIf]="!isLoadingBlock && !error">
 | 
			
		||||
 | 
			
		||||
      <br>
 | 
			
		||||
  
 | 
			
		||||
      <div class="box">
 | 
			
		||||
        <div class="row">
 | 
			
		||||
          <div class="col">
 | 
			
		||||
            <table class="table table-borderless table-striped">
 | 
			
		||||
              <tbody>
 | 
			
		||||
                <tr>
 | 
			
		||||
                  <td>Timestamp</td>
 | 
			
		||||
                  <td>{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }} <i>(<app-time-since [time]="block.timestamp"></app-time-since> ago)</i></td>
 | 
			
		||||
                </tr>
 | 
			
		||||
                <tr>
 | 
			
		||||
                  <td>Number of transactions</td>
 | 
			
		||||
                  <td>{{ block.tx_count }}</td>
 | 
			
		||||
                </tr>
 | 
			
		||||
                <tr>
 | 
			
		||||
                  <td>Size</td>
 | 
			
		||||
                  <td>{{ block.size | bytes: 2 }}</td>
 | 
			
		||||
                </tr>
 | 
			
		||||
                <tr>
 | 
			
		||||
                  <td>Weight</td>
 | 
			
		||||
                  <td>{{ block.weight | wuBytes: 2 }}</td>
 | 
			
		||||
                </tr>
 | 
			
		||||
                <tr>
 | 
			
		||||
                  <td>Status</td>
 | 
			
		||||
                  <td><button *ngIf="latestBlock" class="btn btn-sm btn-success">{{ (latestBlock.height - block.height + 1) }} confirmation{{ (latestBlock.height - block.height + 1) === 1 ? '' : 's' }}</button></td>
 | 
			
		||||
                </tr>
 | 
			
		||||
              </tbody>
 | 
			
		||||
            </table>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="col">
 | 
			
		||||
            <table class="table table-borderless table-striped">
 | 
			
		||||
              <tbody>
 | 
			
		||||
                <tr>
 | 
			
		||||
                  <td>Hash</td>
 | 
			
		||||
                  <td><a [routerLink]="['/block/', block.id]" title="{{ block.id }}" >{{ block.id | shortenString : 32 }}</a></td>
 | 
			
		||||
                </tr>
 | 
			
		||||
                <tr>
 | 
			
		||||
                  <td>Previous Block</td>
 | 
			
		||||
                  <td><a [routerLink]="['/block/', block.previousblockhash]" [state]="{ data: { blockHeight: blockHeight - 1 } }" title="{{ block.previousblockhash }}">{{ block.previousblockhash | shortenString : 32 }}</a></td>
 | 
			
		||||
                </tr>
 | 
			
		||||
              </tbody>
 | 
			
		||||
            </table>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <br>
 | 
			
		||||
 | 
			
		||||
      <h2><ng-template [ngIf]="transactions?.length">{{ transactions?.length || '?' }} of </ng-template>{{ block.tx_count }} transactions</h2>
 | 
			
		||||
 | 
			
		||||
      <br>
 | 
			
		||||
 | 
			
		||||
      <app-transactions-list [transactions]="transactions"></app-transactions-list>
 | 
			
		||||
 | 
			
		||||
      <div class="text-center">
 | 
			
		||||
        <ng-template [ngIf]="isLoadingTransactions">
 | 
			
		||||
          <div class="spinner-border"></div>
 | 
			
		||||
          <br><br>
 | 
			
		||||
        </ng-template>
 | 
			
		||||
        <button *ngIf="transactions?.length && transactions?.length !== block.tx_count" type="button" class="btn btn-primary" (click)="loadMore()">Load more</button>
 | 
			
		||||
      </div>
 | 
			
		||||
  
 | 
			
		||||
    </ng-template>
 | 
			
		||||
 | 
			
		||||
    <ng-template [ngIf]="isLoadingBlock && !error">
 | 
			
		||||
      
 | 
			
		||||
      <br>
 | 
			
		||||
 | 
			
		||||
      <div class="box">
 | 
			
		||||
        <div class="row">
 | 
			
		||||
          <div class="col">
 | 
			
		||||
            <table class="table table-borderless table-striped">
 | 
			
		||||
              <tbody>
 | 
			
		||||
                <tr>
 | 
			
		||||
                  <td colspan="2"><span class="skeleton-loader"></span></td>
 | 
			
		||||
                </tr>
 | 
			
		||||
                <tr>
 | 
			
		||||
                  <td colspan="2"><span class="skeleton-loader"></span></td>
 | 
			
		||||
                </tr>
 | 
			
		||||
                <tr>
 | 
			
		||||
                  <td colspan="2"><span class="skeleton-loader"></span></td>
 | 
			
		||||
                </tr>
 | 
			
		||||
                <tr>
 | 
			
		||||
                  <td colspan="2"><span class="skeleton-loader"></span></td>
 | 
			
		||||
                </tr>
 | 
			
		||||
                <tr>
 | 
			
		||||
                  <td colspan="2"><span class="skeleton-loader"></span></td>
 | 
			
		||||
                </tr>
 | 
			
		||||
              </tbody>
 | 
			
		||||
            </table>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div class="col">
 | 
			
		||||
            <table class="table table-borderless table-striped">
 | 
			
		||||
              <tbody>
 | 
			
		||||
                <tr>
 | 
			
		||||
                  <td colspan="2"><span class="skeleton-loader"></span></td>
 | 
			
		||||
                </tr>
 | 
			
		||||
                <tr>
 | 
			
		||||
                  <td colspan="2"><span class="skeleton-loader"></span></td>
 | 
			
		||||
                </tr>
 | 
			
		||||
              </tbody>
 | 
			
		||||
            </table>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <br>
 | 
			
		||||
      
 | 
			
		||||
      <div class="text-center">
 | 
			
		||||
        <div class="spinner-border"></div>
 | 
			
		||||
        <br><br>
 | 
			
		||||
      </div>
 | 
			
		||||
    </ng-template>
 | 
			
		||||
 | 
			
		||||
    <ng-template [ngIf]="error">
 | 
			
		||||
      <div class="text-center">
 | 
			
		||||
        Error loading block data.
 | 
			
		||||
        <br>
 | 
			
		||||
        <i>{{ error.error }}</i>
 | 
			
		||||
      </div>
 | 
			
		||||
    </ng-template>
 | 
			
		||||
 | 
			
		||||
  </div>
 | 
			
		||||
  
 | 
			
		||||
  <br>
 | 
			
		||||
@ -1,9 +1,10 @@
 | 
			
		||||
import { Component, OnInit } from '@angular/core';
 | 
			
		||||
import { ActivatedRoute, ParamMap } from '@angular/router';
 | 
			
		||||
import { ApiService } from 'src/app/services/api.service';
 | 
			
		||||
import { MemPoolService } from 'src/app/services/mem-pool.service';
 | 
			
		||||
import { ElectrsApiService } from '../../services/electrs-api.service';
 | 
			
		||||
import { switchMap } from 'rxjs/operators';
 | 
			
		||||
import { ChangeDetectorRef } from '@angular/core';
 | 
			
		||||
import { Block, Transaction } from '../../interfaces/electrs.interface';
 | 
			
		||||
import { of } from 'rxjs';
 | 
			
		||||
import { StateService } from '../../services/state.service';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-block',
 | 
			
		||||
@ -11,48 +12,59 @@ import { ChangeDetectorRef } from '@angular/core';
 | 
			
		||||
  styleUrls: ['./block.component.scss']
 | 
			
		||||
})
 | 
			
		||||
export class BlockComponent implements OnInit {
 | 
			
		||||
  block: any;
 | 
			
		||||
  block: Block;
 | 
			
		||||
  blockHeight: number;
 | 
			
		||||
  blockHash: string;
 | 
			
		||||
  isLoadingBlock = true;
 | 
			
		||||
  latestBlockHeight: number;
 | 
			
		||||
  transactions: any[];
 | 
			
		||||
  latestBlock: Block;
 | 
			
		||||
  transactions: Transaction[];
 | 
			
		||||
  isLoadingTransactions = true;
 | 
			
		||||
  error: any;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private route: ActivatedRoute,
 | 
			
		||||
    private apiService: ApiService,
 | 
			
		||||
    private memPoolService: MemPoolService,
 | 
			
		||||
    private ref: ChangeDetectorRef,
 | 
			
		||||
    private electrsApiService: ElectrsApiService,
 | 
			
		||||
    private stateService: StateService,
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
    this.route.paramMap.pipe(
 | 
			
		||||
      switchMap((params: ParamMap) => {
 | 
			
		||||
        this.error = undefined;
 | 
			
		||||
        this.isLoadingBlock = true;
 | 
			
		||||
        const blockHash: string = params.get('id') || '';
 | 
			
		||||
        return this.apiService.getBlock$(blockHash);
 | 
			
		||||
        this.error = undefined;
 | 
			
		||||
 | 
			
		||||
        if (history.state.data && history.state.data.blockHeight) {
 | 
			
		||||
          this.blockHeight = history.state.data.blockHeight;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.blockHash = blockHash;
 | 
			
		||||
 | 
			
		||||
        if (history.state.data && history.state.data.block) {
 | 
			
		||||
          return of(history.state.data.block);
 | 
			
		||||
        } else {
 | 
			
		||||
          this.isLoadingBlock = true;
 | 
			
		||||
          return this.electrsApiService.getBlock$(blockHash);
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
    )
 | 
			
		||||
    .subscribe((block) => {
 | 
			
		||||
    .subscribe((block: Block) => {
 | 
			
		||||
      this.block = block;
 | 
			
		||||
      this.blockHeight = block.height;
 | 
			
		||||
      this.isLoadingBlock = false;
 | 
			
		||||
      this.getBlockTransactions(block.id);
 | 
			
		||||
      this.ref.markForCheck();
 | 
			
		||||
      window.scrollTo(0, 0);
 | 
			
		||||
    },
 | 
			
		||||
    (error) => {
 | 
			
		||||
      this.error = error;
 | 
			
		||||
      this.isLoadingBlock = false;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.memPoolService.blocks$
 | 
			
		||||
      .subscribe((block) => {
 | 
			
		||||
        this.latestBlockHeight = block.height;
 | 
			
		||||
      });
 | 
			
		||||
    this.stateService.blocks$
 | 
			
		||||
      .subscribe((block) => this.latestBlock = block);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getBlockTransactions(hash: string) {
 | 
			
		||||
    this.apiService.getBlockTransactions$(hash)
 | 
			
		||||
    this.electrsApiService.getBlockTransactions$(hash)
 | 
			
		||||
      .subscribe((transactions: any) => {
 | 
			
		||||
        this.transactions = transactions;
 | 
			
		||||
        this.isLoadingTransactions = false;
 | 
			
		||||
@ -61,7 +73,7 @@ export class BlockComponent implements OnInit {
 | 
			
		||||
 | 
			
		||||
  loadMore() {
 | 
			
		||||
    this.isLoadingTransactions = true;
 | 
			
		||||
    this.apiService.getBlockTransactions$(this.block.id, this.transactions.length)
 | 
			
		||||
    this.electrsApiService.getBlockTransactions$(this.block.id, this.transactions.length)
 | 
			
		||||
      .subscribe((transactions) => {
 | 
			
		||||
        this.transactions = this.transactions.concat(transactions);
 | 
			
		||||
        this.isLoadingTransactions = false;
 | 
			
		||||
@ -0,0 +1,20 @@
 | 
			
		||||
<div class="blocks-container" *ngIf="blocks.length">
 | 
			
		||||
  <div *ngFor="let block of blocks; let i = index; trackBy: trackByBlocksFn" >
 | 
			
		||||
    <div [routerLink]="['/block/', block.id]" [state]="{ data: { block: block } }" class="text-center bitcoin-block mined-block" id="bitcoin-block-{{ block.height }}" [ngStyle]="getStyleForBlock(block)">
 | 
			
		||||
      <div class="block-height">
 | 
			
		||||
        <a [routerLink]="['/block/', block.id]" [state]="{ data: { block: block } }">#{{ block.height }}</a>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="block-body">
 | 
			
		||||
        <div class="fees">
 | 
			
		||||
          ~{{ block.medianFee | ceil }} sat/vB
 | 
			
		||||
          <br/>
 | 
			
		||||
          <span class="yellow-color">{{ block.feeRange[0] | ceil }} - {{ block.feeRange[block.feeRange.length - 1] | ceil }} sat/vB</span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="block-size">{{ block.size | bytes: 2 }}</div>
 | 
			
		||||
        <div class="transaction-count">{{ block.tx_count }} transactions</div>
 | 
			
		||||
        <br /><br />
 | 
			
		||||
        <div class="time-difference">{{ block.timestamp | timeSince : trigger }} ago</div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
@ -1,10 +1,7 @@
 | 
			
		||||
import { Component, OnInit, OnDestroy } from '@angular/core';
 | 
			
		||||
import { IBlock } from '../blockchain/interfaces';
 | 
			
		||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
 | 
			
		||||
import { BlockModalComponent } from './block-modal/block-modal.component';
 | 
			
		||||
import { MemPoolService } from '../services/mem-pool.service';
 | 
			
		||||
import { Subscription } from 'rxjs';
 | 
			
		||||
import { environment } from '../../environments/environment';
 | 
			
		||||
import { Block } from 'src/app/interfaces/electrs.interface';
 | 
			
		||||
import { StateService } from 'src/app/services/state.service';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-blockchain-blocks',
 | 
			
		||||
@ -12,19 +9,17 @@ import { environment } from '../../environments/environment';
 | 
			
		||||
  styleUrls: ['./blockchain-blocks.component.scss']
 | 
			
		||||
})
 | 
			
		||||
export class BlockchainBlocksComponent implements OnInit, OnDestroy {
 | 
			
		||||
  blocks: IBlock[] = [];
 | 
			
		||||
  blocks: Block[] = [];
 | 
			
		||||
  blocksSubscription: Subscription;
 | 
			
		||||
  interval: any;
 | 
			
		||||
  trigger = 0;
 | 
			
		||||
  isElectrsEnabled = !!environment.electrs;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private modalService: NgbModal,
 | 
			
		||||
    private memPoolService: MemPoolService,
 | 
			
		||||
    private stateService: StateService,
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
    this.blocksSubscription = this.memPoolService.blocks$
 | 
			
		||||
    this.blocksSubscription = this.stateService.blocks$
 | 
			
		||||
      .subscribe((block) => {
 | 
			
		||||
        if (this.blocks.some((b) => b.height === block.height)) {
 | 
			
		||||
          return;
 | 
			
		||||
@ -41,27 +36,22 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
 | 
			
		||||
    clearInterval(this.interval);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  trackByBlocksFn(index: number, item: IBlock) {
 | 
			
		||||
  trackByBlocksFn(index: number, item: Block) {
 | 
			
		||||
    return item.height;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  openBlockModal(block: IBlock) {
 | 
			
		||||
    const modalRef = this.modalService.open(BlockModalComponent, { size: 'lg' });
 | 
			
		||||
    modalRef.componentInstance.block = block;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getStyleForBlock(block: IBlock) {
 | 
			
		||||
  getStyleForBlock(block: Block) {
 | 
			
		||||
    const greenBackgroundHeight = 100 - (block.weight / 4000000) * 100;
 | 
			
		||||
    if (window.innerWidth <= 768) {
 | 
			
		||||
      return {
 | 
			
		||||
        'top': 155 * this.blocks.indexOf(block) + 'px',
 | 
			
		||||
        'background': `repeating-linear-gradient(#2d3348, #2d3348 ${greenBackgroundHeight}%,
 | 
			
		||||
        top: 155 * this.blocks.indexOf(block) + 'px',
 | 
			
		||||
        background: `repeating-linear-gradient(#2d3348, #2d3348 ${greenBackgroundHeight}%,
 | 
			
		||||
          #9339f4 ${Math.max(greenBackgroundHeight, 0)}%, #105fb0 100%)`,
 | 
			
		||||
      };
 | 
			
		||||
    } else {
 | 
			
		||||
      return {
 | 
			
		||||
        'left': 155 * this.blocks.indexOf(block) + 'px',
 | 
			
		||||
        'background': `repeating-linear-gradient(#2d3348, #2d3348 ${greenBackgroundHeight}%,
 | 
			
		||||
        left: 155 * this.blocks.indexOf(block) + 'px',
 | 
			
		||||
        background: `repeating-linear-gradient(#2d3348, #2d3348 ${greenBackgroundHeight}%,
 | 
			
		||||
          #9339f4 ${Math.max(greenBackgroundHeight, 0)}%, #105fb0 100%)`,
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
@ -11,14 +11,10 @@
 | 
			
		||||
</div>
 | 
			
		||||
<div class="text-center" class="blockchain-wrapper">
 | 
			
		||||
  <div class="position-container">
 | 
			
		||||
    <app-blockchain-projected-blocks></app-blockchain-projected-blocks>
 | 
			
		||||
    <app-mempool-blocks></app-mempool-blocks>
 | 
			
		||||
    <app-blockchain-blocks></app-blockchain-blocks>
 | 
			
		||||
 | 
			
		||||
    <div id="divider" *ngIf="!isLoading"></div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<app-tx-bubble></app-tx-bubble>
 | 
			
		||||
 | 
			
		||||
<app-footer></app-footer>
 | 
			
		||||
@ -1,8 +1,8 @@
 | 
			
		||||
#divider {
 | 
			
		||||
  width: 3px;
 | 
			
		||||
  height: 3000px;
 | 
			
		||||
  height: 200px;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  top: -1000px;
 | 
			
		||||
  top: -50px;
 | 
			
		||||
  background-image: url('/assets/divider-new.png');
 | 
			
		||||
  background-repeat: repeat-y;
 | 
			
		||||
  position: absolute;
 | 
			
		||||
@ -17,12 +17,14 @@
 | 
			
		||||
 | 
			
		||||
.blockchain-wrapper {
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
  height: 250px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.position-container {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  left: 50%;
 | 
			
		||||
  top: calc(50% - 60px);
 | 
			
		||||
  /* top: calc(50% - 60px); */
 | 
			
		||||
  top: 180px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (max-width: 767.98px) {
 | 
			
		||||
@ -1,9 +1,9 @@
 | 
			
		||||
import { Component, OnInit, OnDestroy, Renderer2 } from '@angular/core';
 | 
			
		||||
import { MemPoolService, ITxTracking } from '../services/mem-pool.service';
 | 
			
		||||
import { ApiService } from '../services/api.service';
 | 
			
		||||
import { ActivatedRoute, ParamMap } from '@angular/router';
 | 
			
		||||
import { Subscription } from 'rxjs';
 | 
			
		||||
import { take } from 'rxjs/operators';
 | 
			
		||||
import { WebsocketService } from 'src/app/services/websocket.service';
 | 
			
		||||
import { StateService } from 'src/app/services/state.service';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-blockchain',
 | 
			
		||||
@ -19,14 +19,14 @@ export class BlockchainComponent implements OnInit, OnDestroy {
 | 
			
		||||
  isLoading = true;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private memPoolService: MemPoolService,
 | 
			
		||||
    private apiService: ApiService,
 | 
			
		||||
    private renderer: Renderer2,
 | 
			
		||||
    private route: ActivatedRoute,
 | 
			
		||||
    private websocketService: WebsocketService,
 | 
			
		||||
    private stateService: StateService,
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
    this.apiService.webSocketWant(['stats', 'blocks', 'projected-blocks']);
 | 
			
		||||
    /*
 | 
			
		||||
    this.apiService.webSocketWant(['stats', 'blocks', 'mempool-blocks']);
 | 
			
		||||
 | 
			
		||||
    this.txTrackingSubscription = this.memPoolService.txTracking$
 | 
			
		||||
      .subscribe((response: ITxTracking) => {
 | 
			
		||||
@ -36,9 +36,9 @@ export class BlockchainComponent implements OnInit, OnDestroy {
 | 
			
		||||
          setTimeout(() => { this.txShowTxNotFound = false; }, 2000);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    */
 | 
			
		||||
 | 
			
		||||
    this.renderer.addClass(document.body, 'disable-scroll');
 | 
			
		||||
 | 
			
		||||
    /*
 | 
			
		||||
    this.route.paramMap
 | 
			
		||||
      .subscribe((params: ParamMap) => {
 | 
			
		||||
        if (this.memPoolService.txTracking$.value.enabled) {
 | 
			
		||||
@ -53,6 +53,9 @@ export class BlockchainComponent implements OnInit, OnDestroy {
 | 
			
		||||
        this.apiService.webSocketStartTrackTx(txId);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      */
 | 
			
		||||
 | 
			
		||||
    /*
 | 
			
		||||
    this.memPoolService.txIdSearch$
 | 
			
		||||
      .subscribe((txId) => {
 | 
			
		||||
        if (txId) {
 | 
			
		||||
@ -64,11 +67,12 @@ export class BlockchainComponent implements OnInit, OnDestroy {
 | 
			
		||||
          }
 | 
			
		||||
          console.log('enabling tracking loading from idSearch!');
 | 
			
		||||
          this.txTrackingLoading = true;
 | 
			
		||||
          this.apiService.webSocketStartTrackTx(txId);
 | 
			
		||||
          this.websocketService.startTrackTx(txId);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
      */
 | 
			
		||||
 | 
			
		||||
    this.blocksSubscription = this.memPoolService.blocks$
 | 
			
		||||
    this.blocksSubscription = this.stateService.blocks$
 | 
			
		||||
      .pipe(
 | 
			
		||||
        take(1)
 | 
			
		||||
      )
 | 
			
		||||
@ -77,7 +81,6 @@ export class BlockchainComponent implements OnInit, OnDestroy {
 | 
			
		||||
 | 
			
		||||
  ngOnDestroy() {
 | 
			
		||||
    this.blocksSubscription.unsubscribe();
 | 
			
		||||
    this.txTrackingSubscription.unsubscribe();
 | 
			
		||||
    this.renderer.removeClass(document.body, 'disable-scroll');
 | 
			
		||||
    // this.txTrackingSubscription.unsubscribe();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,5 @@
 | 
			
		||||
<span #buttonWrapper [attr.data-tlite]="'Copied!'">
 | 
			
		||||
  <button #btn class="btn btn-sm btn-link pt-0" style="line-height: 1;" [attr.data-clipboard-text]="text"> 
 | 
			
		||||
    <img src="./assets/clippy.svg" width="13">
 | 
			
		||||
  </button>
 | 
			
		||||
</span>
 | 
			
		||||
@ -0,0 +1,25 @@
 | 
			
		||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
 | 
			
		||||
 | 
			
		||||
import { ClipboardComponent } from './clipboard.component';
 | 
			
		||||
 | 
			
		||||
describe('ClipboardComponent', () => {
 | 
			
		||||
  let component: ClipboardComponent;
 | 
			
		||||
  let fixture: ComponentFixture<ClipboardComponent>;
 | 
			
		||||
 | 
			
		||||
  beforeEach(async(() => {
 | 
			
		||||
    TestBed.configureTestingModule({
 | 
			
		||||
      declarations: [ ClipboardComponent ]
 | 
			
		||||
    })
 | 
			
		||||
    .compileComponents();
 | 
			
		||||
  }));
 | 
			
		||||
 | 
			
		||||
  beforeEach(() => {
 | 
			
		||||
    fixture = TestBed.createComponent(ClipboardComponent);
 | 
			
		||||
    component = fixture.componentInstance;
 | 
			
		||||
    fixture.detectChanges();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should create', () => {
 | 
			
		||||
    expect(component).toBeTruthy();
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										33
									
								
								frontend/src/app/components/clipboard/clipboard.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								frontend/src/app/components/clipboard/clipboard.component.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,33 @@
 | 
			
		||||
import { Component, OnInit, ViewChild, ElementRef, AfterViewInit, Input } from '@angular/core';
 | 
			
		||||
import * as ClipboardJS from 'clipboard';
 | 
			
		||||
import * as tlite from 'tlite';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-clipboard',
 | 
			
		||||
  templateUrl: './clipboard.component.html',
 | 
			
		||||
  styleUrls: ['./clipboard.component.scss']
 | 
			
		||||
})
 | 
			
		||||
export class ClipboardComponent implements AfterViewInit {
 | 
			
		||||
  @ViewChild('btn') btn: ElementRef;
 | 
			
		||||
  @ViewChild('buttonWrapper') buttonWrapper: ElementRef;
 | 
			
		||||
  @Input() text: string;
 | 
			
		||||
 | 
			
		||||
  clipboard: any;
 | 
			
		||||
 | 
			
		||||
  constructor() { }
 | 
			
		||||
 | 
			
		||||
  ngAfterViewInit() {
 | 
			
		||||
    this.clipboard = new ClipboardJS(this.btn.nativeElement);
 | 
			
		||||
    this.clipboard.on('success', (e) => {
 | 
			
		||||
      tlite.show(this.buttonWrapper.nativeElement);
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        tlite.hide(this.buttonWrapper.nativeElement);
 | 
			
		||||
      }, 1000);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onDestroy() {
 | 
			
		||||
    this.clipboard.destroy();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,43 @@
 | 
			
		||||
<table class="table table-borderless">
 | 
			
		||||
  <thead>
 | 
			
		||||
    <th style="width: 120px;">Height</th>
 | 
			
		||||
    <th class="d-none d-md-block" style="width: 300px;">Timestamp</th>
 | 
			
		||||
    <th style="width: 200px;">Mined</th>
 | 
			
		||||
    <th style="width: 150px;">Transactions</th>
 | 
			
		||||
    <th style="width: 175px;">Size</th>
 | 
			
		||||
    <th class="d-none d-md-block">Filled</th>
 | 
			
		||||
  </thead>
 | 
			
		||||
  <tbody>
 | 
			
		||||
    <tr *ngFor="let block of blocks; let i= index; trackBy: trackByBlock">
 | 
			
		||||
      <td><a [routerLink]="['./block', block.id]" [state]="{ data: { block: block } }">#{{ block.height }}</a></td>
 | 
			
		||||
      <td class="d-none d-md-block">{{ block.timestamp * 1000 | date:'yyyy-MM-dd HH:mm' }}</td>
 | 
			
		||||
      <td>{{ block.timestamp | timeSince : trigger }} ago</td>
 | 
			
		||||
      <td>{{ block.tx_count }}</td>
 | 
			
		||||
      <td>{{ block.size | bytes: 2 }}</td>
 | 
			
		||||
      <td class="d-none d-md-block">
 | 
			
		||||
        <div class="progress position-relative">
 | 
			
		||||
          <div class="progress-bar bg-success" role="progressbar" [ngStyle]="{'width': (block.weight / 4000000)*100 + '%' }"></div>
 | 
			
		||||
        </div>
 | 
			
		||||
      </td>
 | 
			
		||||
    </tr>
 | 
			
		||||
    <ng-template [ngIf]="isLoading">
 | 
			
		||||
      <tr *ngFor="let item of [1,2,3,4,5,6,7,8,9,10]">
 | 
			
		||||
        <td><span class="skeleton-loader"></span></td>
 | 
			
		||||
        <td><span class="skeleton-loader"></span></td>
 | 
			
		||||
        <td><span class="skeleton-loader"></span></td>
 | 
			
		||||
        <td><span class="skeleton-loader"></span></td>
 | 
			
		||||
        <td><span class="skeleton-loader"></span></td>
 | 
			
		||||
        <td><span class="skeleton-loader"></span></td>
 | 
			
		||||
      </tr>
 | 
			
		||||
    </ng-template>
 | 
			
		||||
  </tbody>
 | 
			
		||||
</table>
 | 
			
		||||
 | 
			
		||||
<div class="text-center">
 | 
			
		||||
  <ng-template [ngIf]="isLoading">
 | 
			
		||||
    <div class="spinner-border"></div>
 | 
			
		||||
    <br><br>
 | 
			
		||||
  </ng-template>
 | 
			
		||||
  <br>
 | 
			
		||||
  <button *ngIf="blocks.length" [disabled]="isLoading" type="button" class="btn btn-primary" (click)="loadMore()">Load more</button>
 | 
			
		||||
</div>
 | 
			
		||||
@ -0,0 +1,25 @@
 | 
			
		||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
 | 
			
		||||
 | 
			
		||||
import { LatestBlocksComponent } from './latest-blocks.component';
 | 
			
		||||
 | 
			
		||||
describe('LatestBlocksComponent', () => {
 | 
			
		||||
  let component: LatestBlocksComponent;
 | 
			
		||||
  let fixture: ComponentFixture<LatestBlocksComponent>;
 | 
			
		||||
 | 
			
		||||
  beforeEach(async(() => {
 | 
			
		||||
    TestBed.configureTestingModule({
 | 
			
		||||
      declarations: [ LatestBlocksComponent ]
 | 
			
		||||
    })
 | 
			
		||||
    .compileComponents();
 | 
			
		||||
  }));
 | 
			
		||||
 | 
			
		||||
  beforeEach(() => {
 | 
			
		||||
    fixture = TestBed.createComponent(LatestBlocksComponent);
 | 
			
		||||
    component = fixture.componentInstance;
 | 
			
		||||
    fixture.detectChanges();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should create', () => {
 | 
			
		||||
    expect(component).toBeTruthy();
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@ -0,0 +1,78 @@
 | 
			
		||||
import { Component, OnInit, OnDestroy } from '@angular/core';
 | 
			
		||||
import { ElectrsApiService } from '../../services/electrs-api.service';
 | 
			
		||||
import { StateService } from '../../services/state.service';
 | 
			
		||||
import { Block } from '../../interfaces/electrs.interface';
 | 
			
		||||
import { Subscription } from 'rxjs';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-latest-blocks',
 | 
			
		||||
  templateUrl: './latest-blocks.component.html',
 | 
			
		||||
  styleUrls: ['./latest-blocks.component.scss'],
 | 
			
		||||
})
 | 
			
		||||
export class LatestBlocksComponent implements OnInit, OnDestroy {
 | 
			
		||||
  blocks: any[] = [];
 | 
			
		||||
  blockSubscription: Subscription;
 | 
			
		||||
  isLoading = true;
 | 
			
		||||
  interval: any;
 | 
			
		||||
  trigger = 0;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private electrsApiService: ElectrsApiService,
 | 
			
		||||
    private stateService: StateService,
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
    this.blockSubscription = this.stateService.blocks$
 | 
			
		||||
      .subscribe((block) => {
 | 
			
		||||
        if (block === null || !this.blocks.length) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (block.height === this.blocks[0].height) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // If we are out of sync, reload the blocks instead
 | 
			
		||||
        if (block.height > this.blocks[0].height + 1) {
 | 
			
		||||
          this.loadInitialBlocks();
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (block.height === this.blocks[0].height) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.blocks.pop();
 | 
			
		||||
        this.blocks.unshift(block);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
    this.loadInitialBlocks();
 | 
			
		||||
    this.interval = window.setInterval(() => this.trigger++, 1000 * 60);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnDestroy() {
 | 
			
		||||
    clearInterval(this.interval);
 | 
			
		||||
    this.blockSubscription.unsubscribe();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  loadInitialBlocks() {
 | 
			
		||||
    this.electrsApiService.listBlocks$()
 | 
			
		||||
      .subscribe((blocks) => {
 | 
			
		||||
        this.blocks = blocks;
 | 
			
		||||
        this.isLoading = false;
 | 
			
		||||
      });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  loadMore() {
 | 
			
		||||
    this.isLoading = true;
 | 
			
		||||
    this.electrsApiService.listBlocks$(this.blocks[this.blocks.length - 1].height - 1)
 | 
			
		||||
      .subscribe((blocks) => {
 | 
			
		||||
        this.blocks = this.blocks.concat(blocks);
 | 
			
		||||
        this.isLoading = false;
 | 
			
		||||
      });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  trackByBlock(index: number, block: Block) {
 | 
			
		||||
    return block.height;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,28 @@
 | 
			
		||||
<table class="table table-borderless">
 | 
			
		||||
  <thead>
 | 
			
		||||
    <th>Transaction ID</th>
 | 
			
		||||
    <th style="width: 200px;">Value</th>
 | 
			
		||||
    <th style="width: 125px;">Size</th>
 | 
			
		||||
    <th style="width: 150px;">Fee</th>
 | 
			
		||||
  </thead>
 | 
			
		||||
  <tbody>
 | 
			
		||||
    <ng-container *ngIf="(transactions$ | async) as transactions">
 | 
			
		||||
      <ng-template [ngIf]="!isLoading">
 | 
			
		||||
        <tr *ngFor="let transaction of transactions">
 | 
			
		||||
          <td><a [routerLink]="['./tx/', transaction.txid]">{{ transaction.txid }}</a></td>
 | 
			
		||||
          <td>{{ transaction.value / 100000000 }} BTC</td>
 | 
			
		||||
          <td>{{ transaction.vsize | vbytes: 2 }}</td>
 | 
			
		||||
          <td>{{ transaction.fee / transaction.vsize | number : '1.2-2'}} sats/vB</td>
 | 
			
		||||
        </tr>
 | 
			
		||||
      </ng-template>
 | 
			
		||||
    </ng-container>
 | 
			
		||||
    <ng-template [ngIf]="isLoading">
 | 
			
		||||
      <tr *ngFor="let item of [1,2,3,4,5,6,7,8,9,10]">
 | 
			
		||||
        <td><span class="skeleton-loader"></span></td>
 | 
			
		||||
        <td><span class="skeleton-loader"></span></td>
 | 
			
		||||
        <td><span class="skeleton-loader"></span></td>
 | 
			
		||||
        <td><span class="skeleton-loader"></span></td>
 | 
			
		||||
      </tr>
 | 
			
		||||
    </ng-template>
 | 
			
		||||
  </tbody>
 | 
			
		||||
</table>
 | 
			
		||||
@ -0,0 +1,25 @@
 | 
			
		||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
 | 
			
		||||
 | 
			
		||||
import { LatestTransactionsComponent } from './latest-transactions.component';
 | 
			
		||||
 | 
			
		||||
describe('LatestTransactionsComponent', () => {
 | 
			
		||||
  let component: LatestTransactionsComponent;
 | 
			
		||||
  let fixture: ComponentFixture<LatestTransactionsComponent>;
 | 
			
		||||
 | 
			
		||||
  beforeEach(async(() => {
 | 
			
		||||
    TestBed.configureTestingModule({
 | 
			
		||||
      declarations: [ LatestTransactionsComponent ]
 | 
			
		||||
    })
 | 
			
		||||
    .compileComponents();
 | 
			
		||||
  }));
 | 
			
		||||
 | 
			
		||||
  beforeEach(() => {
 | 
			
		||||
    fixture = TestBed.createComponent(LatestTransactionsComponent);
 | 
			
		||||
    component = fixture.componentInstance;
 | 
			
		||||
    fixture.detectChanges();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should create', () => {
 | 
			
		||||
    expect(component).toBeTruthy();
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@ -0,0 +1,31 @@
 | 
			
		||||
import { Component, OnInit } from '@angular/core';
 | 
			
		||||
import { ElectrsApiService } from '../../services/electrs-api.service';
 | 
			
		||||
import { Observable, timer } from 'rxjs';
 | 
			
		||||
import { Recent } from '../../interfaces/electrs.interface';
 | 
			
		||||
import { flatMap, tap } from 'rxjs/operators';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-latest-transactions',
 | 
			
		||||
  templateUrl: './latest-transactions.component.html',
 | 
			
		||||
  styleUrls: ['./latest-transactions.component.scss']
 | 
			
		||||
})
 | 
			
		||||
export class LatestTransactionsComponent implements OnInit {
 | 
			
		||||
  transactions$: Observable<Recent[]>;
 | 
			
		||||
  isLoading = true;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private electrsApiService: ElectrsApiService,
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
    this.transactions$ = timer(0, 10000)
 | 
			
		||||
      .pipe(
 | 
			
		||||
        flatMap(() => {
 | 
			
		||||
          return this.electrsApiService.getRecentTransaction$()
 | 
			
		||||
            .pipe(
 | 
			
		||||
              tap(() => this.isLoading = false)
 | 
			
		||||
            );
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,31 @@
 | 
			
		||||
<header>
 | 
			
		||||
  <nav class="navbar navbar-expand-md navbar-dark bg-dark">
 | 
			
		||||
  <a class="navbar-brand" routerLink="/"><img src="./assets/mempool-space-logo.png" width="180" class="logo"> <span class="badge badge-warning" style="margin-left: 10px;" *ngIf="isOffline">Offline</span></a>
 | 
			
		||||
  
 | 
			
		||||
  <button class="navbar-toggler" type="button" (click)="collapse()" aria-controls="navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation">
 | 
			
		||||
    <span class="navbar-toggler-icon"></span>
 | 
			
		||||
  </button>
 | 
			
		||||
  
 | 
			
		||||
  <div class="navbar-collapse collapse" id="navbarCollapse" [ngClass]="{'show': navCollapsed}">
 | 
			
		||||
    <ul class="navbar-nav mr-auto">
 | 
			
		||||
      <li class="nav-item" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}">
 | 
			
		||||
        <a class="nav-link" routerLink="/" (click)="collapse()">Explorer</a>
 | 
			
		||||
      </li>
 | 
			
		||||
      <li class="nav-item" routerLinkActive="active">
 | 
			
		||||
        <a class="nav-link" routerLink="/graphs" (click)="collapse()">Graphs</a>
 | 
			
		||||
      </li>
 | 
			
		||||
      <li class="nav-item" routerLinkActive="active">
 | 
			
		||||
        <a class="nav-link" routerLink="/tv" (click)="collapse()">TV view  <img src="./assets/expand.png" width="15"/></a>
 | 
			
		||||
      </li>
 | 
			
		||||
      <li class="nav-item" routerLinkActive="active">
 | 
			
		||||
        <a class="nav-link" routerLink="/about" (click)="collapse()">About</a>
 | 
			
		||||
      </li>
 | 
			
		||||
    </ul>
 | 
			
		||||
    <app-search-form location="top"></app-search-form>
 | 
			
		||||
  </div>
 | 
			
		||||
</nav>
 | 
			
		||||
</header>
 | 
			
		||||
 | 
			
		||||
<br />
 | 
			
		||||
 | 
			
		||||
<router-outlet></router-outlet>
 | 
			
		||||
@ -0,0 +1,27 @@
 | 
			
		||||
import { Component, OnInit } from '@angular/core';
 | 
			
		||||
import { StateService } from '../../services/state.service';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-master-page',
 | 
			
		||||
  templateUrl: './master-page.component.html',
 | 
			
		||||
  styleUrls: ['./master-page.component.scss']
 | 
			
		||||
})
 | 
			
		||||
export class MasterPageComponent implements OnInit {
 | 
			
		||||
  navCollapsed = false;
 | 
			
		||||
  isOffline = false;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private stateService: StateService,
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
    this.stateService.isOffline$
 | 
			
		||||
      .subscribe((state) => {
 | 
			
		||||
        this.isOffline = state;
 | 
			
		||||
      });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  collapse(): void {
 | 
			
		||||
    this.navCollapsed = !this.navCollapsed;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,20 @@
 | 
			
		||||
<div class="mempool-blocks-container">
 | 
			
		||||
  <div *ngFor="let projectedBlock of mempoolBlocks; let i = index; trackBy: trackByFn">
 | 
			
		||||
    <div class="bitcoin-block text-center mempool-block" id="mempool-block-{{ i }}" [ngStyle]="getStyleForMempoolBlockAtIndex(i)">
 | 
			
		||||
      <div class="block-body" *ngIf="mempoolBlocks?.length">
 | 
			
		||||
        <div class="fees">
 | 
			
		||||
          ~{{ projectedBlock.medianFee | ceil }} sat/vB
 | 
			
		||||
          <br/>
 | 
			
		||||
          <span class="yellow-color">{{ projectedBlock.feeRange[0] | ceil }} - {{ projectedBlock.feeRange[projectedBlock.feeRange.length - 1] | ceil }} sat/vB</span>
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="block-size">{{ projectedBlock.blockSize | bytes: 2 }}</div>
 | 
			
		||||
        <div class="transaction-count">{{ projectedBlock.nTx }} transactions</div>
 | 
			
		||||
        <div class="time-difference" *ngIf="i !== 3">In ~{{ 10 * i + 10 }} minutes</div>
 | 
			
		||||
        <ng-template [ngIf]="i === 3 && mempoolBlocks?.length >= 4 && (projectedBlock.blockVSize / 1000000 | ceil) > 1">
 | 
			
		||||
          <div class="time-difference">+{{ projectedBlock.blockVSize / 1000000 | ceil }} blocks</div>
 | 
			
		||||
        </ng-template>
 | 
			
		||||
      </div>
 | 
			
		||||
      <span class="animated-border"></span>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
@ -1,7 +1,6 @@
 | 
			
		||||
.bitcoin-block {
 | 
			
		||||
  width: 125px;
 | 
			
		||||
  height: 125px;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.block-size {
 | 
			
		||||
@ -9,7 +8,7 @@
 | 
			
		||||
  font-weight: bold;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.projected-blocks-container {
 | 
			
		||||
.mempool-blocks-container {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  top: 0px;
 | 
			
		||||
  right: 0px;
 | 
			
		||||
@ -20,7 +19,7 @@
 | 
			
		||||
  opacity: 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.projected-block {
 | 
			
		||||
.mempool-block {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  top: 0;
 | 
			
		||||
}
 | 
			
		||||
@ -54,7 +53,7 @@
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (max-width: 767.98px) {
 | 
			
		||||
  .projected-blocks-container {
 | 
			
		||||
  .mempool-blocks-container {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    left: -165px;
 | 
			
		||||
    top: -40px;
 | 
			
		||||
@ -87,11 +86,11 @@
 | 
			
		||||
    transform-origin: top;     
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .projected-block.bitcoin-block::after {
 | 
			
		||||
  .mempool-block.bitcoin-block::after {
 | 
			
		||||
    background-color: #403834; 
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .projected-block.bitcoin-block::before {
 | 
			
		||||
  .mempool-block.bitcoin-block::before {
 | 
			
		||||
    background-color: #2d2825; 
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,83 @@
 | 
			
		||||
import { Component, OnInit, OnDestroy, Input, EventEmitter, Output } from '@angular/core';
 | 
			
		||||
import { Subscription } from 'rxjs';
 | 
			
		||||
import { MempoolBlock } from 'src/app/interfaces/websocket.interface';
 | 
			
		||||
import { StateService } from 'src/app/services/state.service';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-mempool-blocks',
 | 
			
		||||
  templateUrl: './mempool-blocks.component.html',
 | 
			
		||||
  styleUrls: ['./mempool-blocks.component.scss']
 | 
			
		||||
})
 | 
			
		||||
export class MempoolBlocksComponent implements OnInit, OnDestroy {
 | 
			
		||||
  mempoolBlocks: MempoolBlock[];
 | 
			
		||||
  mempoolBlocksSubscription: Subscription;
 | 
			
		||||
 | 
			
		||||
  blockWidth = 125;
 | 
			
		||||
  blockMarginLeft = 20;
 | 
			
		||||
 | 
			
		||||
  @Input() txFeePerVSize: number;
 | 
			
		||||
  @Output() rightPosition: EventEmitter<number> = new EventEmitter<number>(true);
 | 
			
		||||
  @Output() blockDepth: EventEmitter<number> = new EventEmitter<number>(true);
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private stateService: StateService,
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
    this.mempoolBlocksSubscription = this.stateService.mempoolBlocks$
 | 
			
		||||
      .subscribe((blocks) => {
 | 
			
		||||
        this.mempoolBlocks = blocks;
 | 
			
		||||
        this.calculateTransactionPosition();
 | 
			
		||||
      });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnDestroy() {
 | 
			
		||||
    this.mempoolBlocksSubscription.unsubscribe();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  trackByFn(index: number) {
 | 
			
		||||
    return index;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getStyleForMempoolBlockAtIndex(index: number) {
 | 
			
		||||
    const greenBackgroundHeight = 100 - this.mempoolBlocks[index].blockVSize / 1000000 * 100;
 | 
			
		||||
    return {
 | 
			
		||||
      'right': 40 + index * 155 + 'px',
 | 
			
		||||
      'background': `repeating-linear-gradient(to right,  #554b45, #554b45 ${greenBackgroundHeight}%,
 | 
			
		||||
        #bd7c13 ${Math.max(greenBackgroundHeight, 0)}%, #c5345a 100%)`,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  calculateTransactionPosition() {
 | 
			
		||||
    if (!this.txFeePerVSize) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for (const block of this.mempoolBlocks) {
 | 
			
		||||
      for (let i = 0; i < block.feeRange.length - 1; i++) {
 | 
			
		||||
        if (this.txFeePerVSize < block.feeRange[i + 1] && this.txFeePerVSize >= block.feeRange[i]) {
 | 
			
		||||
          const txInBlockIndex = this.mempoolBlocks.indexOf(block);
 | 
			
		||||
          const feeRangeIndex = block.feeRange.findIndex((val, index) => this.txFeePerVSize < block.feeRange[index + 1]);
 | 
			
		||||
          const feeRangeChunkSize = 1 / (block.feeRange.length - 1);
 | 
			
		||||
 | 
			
		||||
          const txFee = this.txFeePerVSize - block.feeRange[i];
 | 
			
		||||
          const max = block.feeRange[i + 1] - block.feeRange[i];
 | 
			
		||||
          const blockLocation = txFee / max;
 | 
			
		||||
 | 
			
		||||
          const chunkPositionOffset = blockLocation * feeRangeChunkSize;
 | 
			
		||||
          const feePosition = feeRangeChunkSize * feeRangeIndex + chunkPositionOffset;
 | 
			
		||||
 | 
			
		||||
          const blockedFilledPercentage = (block.blockVSize > 1000000 ? 1000000 : block.blockVSize) / 1000000;
 | 
			
		||||
 | 
			
		||||
          const arrowRightPosition = txInBlockIndex * (this.blockMarginLeft + this.blockWidth)
 | 
			
		||||
            + ((1 - feePosition) * blockedFilledPercentage * this.blockWidth);
 | 
			
		||||
 | 
			
		||||
          this.rightPosition.next(arrowRightPosition);
 | 
			
		||||
          this.blockDepth.next(txInBlockIndex);
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1
									
								
								frontend/src/app/components/qrcode/qrcode.component.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								frontend/src/app/components/qrcode/qrcode.component.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
			
		||||
<canvas #canvas></canvas>
 | 
			
		||||
							
								
								
									
										25
									
								
								frontend/src/app/components/qrcode/qrcode.component.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								frontend/src/app/components/qrcode/qrcode.component.spec.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,25 @@
 | 
			
		||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
 | 
			
		||||
 | 
			
		||||
import { QrcodeComponent } from './qrcode.component';
 | 
			
		||||
 | 
			
		||||
describe('QrcodeComponent', () => {
 | 
			
		||||
  let component: QrcodeComponent;
 | 
			
		||||
  let fixture: ComponentFixture<QrcodeComponent>;
 | 
			
		||||
 | 
			
		||||
  beforeEach(async(() => {
 | 
			
		||||
    TestBed.configureTestingModule({
 | 
			
		||||
      declarations: [ QrcodeComponent ]
 | 
			
		||||
    })
 | 
			
		||||
    .compileComponents();
 | 
			
		||||
  }));
 | 
			
		||||
 | 
			
		||||
  beforeEach(() => {
 | 
			
		||||
    fixture = TestBed.createComponent(QrcodeComponent);
 | 
			
		||||
    component = fixture.componentInstance;
 | 
			
		||||
    fixture.detectChanges();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should create', () => {
 | 
			
		||||
    expect(component).toBeTruthy();
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										43
									
								
								frontend/src/app/components/qrcode/qrcode.component.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								frontend/src/app/components/qrcode/qrcode.component.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,43 @@
 | 
			
		||||
import { Component, Input, AfterViewInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';
 | 
			
		||||
import * as QRCode from 'qrcode/build/qrcode.js';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-qrcode',
 | 
			
		||||
  templateUrl: './qrcode.component.html',
 | 
			
		||||
  styleUrls: ['./qrcode.component.scss']
 | 
			
		||||
})
 | 
			
		||||
export class QrcodeComponent implements AfterViewInit, OnDestroy {
 | 
			
		||||
  @Input() data: string;
 | 
			
		||||
  @ViewChild('canvas') canvas: ElementRef;
 | 
			
		||||
 | 
			
		||||
  qrcodeObject: any;
 | 
			
		||||
 | 
			
		||||
  constructor() { }
 | 
			
		||||
 | 
			
		||||
  ngAfterViewInit() {
 | 
			
		||||
    const opts = {
 | 
			
		||||
      errorCorrectionLevel: 'H',
 | 
			
		||||
      margin: 0,
 | 
			
		||||
      color: {
 | 
			
		||||
        dark: '#000',
 | 
			
		||||
        light: '#fff'
 | 
			
		||||
      },
 | 
			
		||||
      width: 125,
 | 
			
		||||
      height: 125,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    if (!this.data) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    QRCode.toCanvas(this.canvas.nativeElement, this.data.toUpperCase(), opts, (error: any) => {
 | 
			
		||||
      if (error) {
 | 
			
		||||
         console.error(error);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ngOnDestroy() {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,29 @@
 | 
			
		||||
<ng-template [ngIf]="location === 'start'" [ngIfElse]="top">
 | 
			
		||||
    
 | 
			
		||||
  <form [formGroup]="searchForm" (submit)="searchForm.valid && search()" novalidate>
 | 
			
		||||
    <div class="form-row">
 | 
			
		||||
      <div class="col-12 col-md-10 mb-2 mb-md-0">
 | 
			
		||||
        <input formControlName="searchText" type="text" class="form-control form-control-lg" [placeholder]="searchBoxPlaceholderText">
 | 
			
		||||
      </div>
 | 
			
		||||
      <div class="col-12 col-md-2">
 | 
			
		||||
        <button type="submit" class="btn btn-block btn-lg btn-primary">{{ searchButtonText }}</button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </form>
 | 
			
		||||
 | 
			
		||||
</ng-template>
 | 
			
		||||
 | 
			
		||||
<ng-template #top>
 | 
			
		||||
 | 
			
		||||
  <form [formGroup]="searchForm" (submit)="searchForm.valid && search()" novalidate>
 | 
			
		||||
    <div class="form-row">
 | 
			
		||||
      <div style="width: 350px;" class="mr-2">
 | 
			
		||||
        <input formControlName="searchText" type="text" class="form-control" [placeholder]="searchBoxPlaceholderText">
 | 
			
		||||
      </div>
 | 
			
		||||
      <div>
 | 
			
		||||
        <button type="submit" class="btn btn-block btn-primary">{{ searchButtonText }}</button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </form>
 | 
			
		||||
 | 
			
		||||
</ng-template>
 | 
			
		||||
@ -0,0 +1,25 @@
 | 
			
		||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
 | 
			
		||||
 | 
			
		||||
import { SearchFormComponent } from './search-form.component';
 | 
			
		||||
 | 
			
		||||
describe('SearchFormComponent', () => {
 | 
			
		||||
  let component: SearchFormComponent;
 | 
			
		||||
  let fixture: ComponentFixture<SearchFormComponent>;
 | 
			
		||||
 | 
			
		||||
  beforeEach(async(() => {
 | 
			
		||||
    TestBed.configureTestingModule({
 | 
			
		||||
      declarations: [ SearchFormComponent ]
 | 
			
		||||
    })
 | 
			
		||||
    .compileComponents();
 | 
			
		||||
  }));
 | 
			
		||||
 | 
			
		||||
  beforeEach(() => {
 | 
			
		||||
    fixture = TestBed.createComponent(SearchFormComponent);
 | 
			
		||||
    component = fixture.componentInstance;
 | 
			
		||||
    fixture.detectChanges();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should create', () => {
 | 
			
		||||
    expect(component).toBeTruthy();
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@ -0,0 +1,44 @@
 | 
			
		||||
import { Component, OnInit, Input, ChangeDetectionStrategy } from '@angular/core';
 | 
			
		||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
 | 
			
		||||
import { Router } from '@angular/router';
 | 
			
		||||
 | 
			
		||||
@Component({
 | 
			
		||||
  selector: 'app-search-form',
 | 
			
		||||
  templateUrl: './search-form.component.html',
 | 
			
		||||
  styleUrls: ['./search-form.component.scss'],
 | 
			
		||||
  changeDetection: ChangeDetectionStrategy.OnPush
 | 
			
		||||
})
 | 
			
		||||
export class SearchFormComponent implements OnInit {
 | 
			
		||||
  @Input() location: string;
 | 
			
		||||
  searchForm: FormGroup;
 | 
			
		||||
 | 
			
		||||
  searchButtonText = 'Search';
 | 
			
		||||
  searchBoxPlaceholderText = 'Transaction, address, block hash...';
 | 
			
		||||
 | 
			
		||||
  regexAddress = /^([a-km-zA-HJ-NP-Z1-9]{26,35}|[a-km-zA-HJ-NP-Z1-9]{80}|[a-z]{2,5}1[ac-hj-np-z02-9]{8,87})$/;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private formBuilder: FormBuilder,
 | 
			
		||||
    private router: Router,
 | 
			
		||||
  ) { }
 | 
			
		||||
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
    this.searchForm = this.formBuilder.group({
 | 
			
		||||
      searchText: ['', Validators.required],
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  search() {
 | 
			
		||||
    const searchText = this.searchForm.value.searchText.trim();
 | 
			
		||||
    if (searchText) {
 | 
			
		||||
      if (this.regexAddress.test(searchText)) {
 | 
			
		||||
        this.router.navigate(['/address/', searchText]);
 | 
			
		||||
      } else {
 | 
			
		||||
        this.router.navigate(['/tx/', searchText]);
 | 
			
		||||
      }
 | 
			
		||||
      this.searchForm.setValue({
 | 
			
		||||
        searchText: '',
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										20
									
								
								frontend/src/app/components/start/start.component.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								frontend/src/app/components/start/start.component.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,20 @@
 | 
			
		||||
<app-blockchain></app-blockchain>
 | 
			
		||||
 | 
			
		||||
<div class="box">
 | 
			
		||||
  <div class="container">
 | 
			
		||||
 | 
			
		||||
    <ul class="nav nav-tabs mb-2">
 | 
			
		||||
      <li class="nav-item">
 | 
			
		||||
        <a class="nav-link" [class.active]="view === 'blocks'" href="#" (click)="view = 'blocks'">Blocks</a>
 | 
			
		||||
      </li>
 | 
			
		||||
      <li class="nav-item">
 | 
			
		||||
        <a class="nav-link" [class.active]="view === 'transactions'" href="#" (click)="view = 'transactions'">Transactions</a>
 | 
			
		||||
      </li>
 | 
			
		||||
    </ul>
 | 
			
		||||
 | 
			
		||||
    <app-latest-blocks *ngIf="view === 'blocks'; else latestTransactions"></app-latest-blocks>
 | 
			
		||||
    <ng-template #latestTransactions>
 | 
			
		||||
      <app-latest-transactions></app-latest-transactions>
 | 
			
		||||
    </ng-template>
 | 
			
		||||
  </div>
 | 
			
		||||
</div>
 | 
			
		||||
							
								
								
									
										3
									
								
								frontend/src/app/components/start/start.component.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								frontend/src/app/components/start/start.component.scss
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,3 @@
 | 
			
		||||
.search-container {
 | 
			
		||||
  padding-top: 50px;
 | 
			
		||||
}
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user