Adding optional Blockstream esplora backend support.
This commit is contained in:
		
							parent
							
								
									143949bab4
								
							
						
					
					
						commit
						136b6eb76e
					
				@ -19,5 +19,7 @@
 | 
			
		||||
  "BITCOIN_NODE_PORT": 8332,
 | 
			
		||||
  "BITCOIN_NODE_USER": "",
 | 
			
		||||
  "BITCOIN_NODE_PASS": "",
 | 
			
		||||
  "BACKEND_API": "bitcoind",
 | 
			
		||||
  "ESPLORA_API_URL": "https://www.blockstream.info/api",
 | 
			
		||||
  "TX_PER_SECOND_SPAN_SECONDS": 150
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -9,6 +9,7 @@
 | 
			
		||||
  "author": "",
 | 
			
		||||
  "license": "ISC",
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "axios": "^0.19.0",
 | 
			
		||||
    "bitcoin": "^3.0.1",
 | 
			
		||||
    "compression": "^1.7.3",
 | 
			
		||||
    "express": "^4.16.3",
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										10
									
								
								backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,10 @@
 | 
			
		||||
import { IMempoolInfo, ITransaction, IBlock } from '../../interfaces';
 | 
			
		||||
 | 
			
		||||
export interface AbstractBitcoinApi {
 | 
			
		||||
  getMempoolInfo(): Promise<IMempoolInfo>;
 | 
			
		||||
  getRawMempool(): Promise<ITransaction['txid'][]>;
 | 
			
		||||
  getRawTransaction(txId: string): Promise<ITransaction>;
 | 
			
		||||
  getBlockCount(): Promise<number>;
 | 
			
		||||
  getBlock(hash: string): Promise<IBlock>;
 | 
			
		||||
  getBlockHash(height: number): Promise<string>;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										16
									
								
								backend/src/api/bitcoin/bitcoin-api-factory.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								backend/src/api/bitcoin/bitcoin-api-factory.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,16 @@
 | 
			
		||||
const config = require('../../../mempool-config.json');
 | 
			
		||||
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
 | 
			
		||||
import BitcoindApi from './bitcoind-api';
 | 
			
		||||
import EsploraApi from './esplora-api';
 | 
			
		||||
 | 
			
		||||
function factory(): AbstractBitcoinApi {
 | 
			
		||||
  switch (config.BACKEND_API) {
 | 
			
		||||
    case 'esplora':
 | 
			
		||||
      return new EsploraApi();
 | 
			
		||||
    case 'bitcoind':
 | 
			
		||||
    default:
 | 
			
		||||
      return new BitcoindApi();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default factory();
 | 
			
		||||
@ -1,8 +1,9 @@
 | 
			
		||||
const config = require('../../mempool-config.json');
 | 
			
		||||
const config = require('../../../mempool-config.json');
 | 
			
		||||
import * as bitcoin from 'bitcoin';
 | 
			
		||||
import { ITransaction, IMempoolInfo, IBlock } from '../interfaces';
 | 
			
		||||
import { ITransaction, IMempoolInfo, IBlock } from '../../interfaces';
 | 
			
		||||
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
 | 
			
		||||
 | 
			
		||||
class BitcoinApi {
 | 
			
		||||
class BitcoindApi implements AbstractBitcoinApi {
 | 
			
		||||
  client: any;
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
@ -81,4 +82,4 @@ class BitcoinApi {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new BitcoinApi();
 | 
			
		||||
export default BitcoindApi;
 | 
			
		||||
							
								
								
									
										99
									
								
								backend/src/api/bitcoin/esplora-api.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								backend/src/api/bitcoin/esplora-api.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,99 @@
 | 
			
		||||
const config = require('../../../mempool-config.json');
 | 
			
		||||
import { ITransaction, IMempoolInfo, IBlock } from '../../interfaces';
 | 
			
		||||
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
 | 
			
		||||
import axios, { AxiosResponse } from 'axios';
 | 
			
		||||
 | 
			
		||||
class EsploraApi implements AbstractBitcoinApi {
 | 
			
		||||
  client: any;
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
    this.client = axios.create({
 | 
			
		||||
      baseURL: config.ESPLORA_API_URL,
 | 
			
		||||
      timeout: 15000,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getMempoolInfo(): Promise<IMempoolInfo> {
 | 
			
		||||
    return new Promise(async (resolve, reject) => {
 | 
			
		||||
      try {
 | 
			
		||||
        const response: AxiosResponse = await this.client.get('/mempool');
 | 
			
		||||
        resolve({
 | 
			
		||||
          size: response.data.count,
 | 
			
		||||
          bytes: response.data.vsize,
 | 
			
		||||
        });
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        reject(error);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getRawMempool(): Promise<ITransaction['txid'][]> {
 | 
			
		||||
    return new Promise(async (resolve, reject) => {
 | 
			
		||||
      try {
 | 
			
		||||
        const response: AxiosResponse = await this.client.get('/mempool/txids');
 | 
			
		||||
        resolve(response.data);
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        reject(error);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getRawTransaction(txId: string): Promise<ITransaction> {
 | 
			
		||||
    return new Promise(async (resolve, reject) => {
 | 
			
		||||
      try {
 | 
			
		||||
        const response: AxiosResponse = await this.client.get('/tx/' + txId);
 | 
			
		||||
 | 
			
		||||
        response.data.vsize = response.data.size;
 | 
			
		||||
        response.data.size = response.data.weight;
 | 
			
		||||
        response.data.fee = response.data.fee / 100000000;
 | 
			
		||||
 | 
			
		||||
        resolve(response.data);
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        reject(error);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getBlockCount(): Promise<number> {
 | 
			
		||||
    return new Promise(async (resolve, reject) => {
 | 
			
		||||
      try {
 | 
			
		||||
        const response: AxiosResponse = await this.client.get('/blocks/tip/height');
 | 
			
		||||
        resolve(response.data);
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        reject(error);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getBlock(hash: string): Promise<IBlock> {
 | 
			
		||||
    return new Promise(async (resolve, reject) => {
 | 
			
		||||
      try {
 | 
			
		||||
        const blockInfo: AxiosResponse = await this.client.get('/block/' + hash);
 | 
			
		||||
        const blockTxs: AxiosResponse = await this.client.get('/block/' + hash + '/txids');
 | 
			
		||||
 | 
			
		||||
        const block = blockInfo.data;
 | 
			
		||||
        block.hash = hash;
 | 
			
		||||
        block.nTx = block.tx_count;
 | 
			
		||||
        block.time = block.timestamp;
 | 
			
		||||
        block.tx = blockTxs.data;
 | 
			
		||||
 | 
			
		||||
        resolve(block);
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        reject(error);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getBlockHash(height: number): Promise<string> {
 | 
			
		||||
    return new Promise(async (resolve, reject) => {
 | 
			
		||||
      try {
 | 
			
		||||
        const response: AxiosResponse = await this.client.get('/block-height/' + height);
 | 
			
		||||
        resolve(response.data);
 | 
			
		||||
      } catch (error) {
 | 
			
		||||
        reject(error);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default EsploraApi;
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
const config = require('../../mempool-config.json');
 | 
			
		||||
import bitcoinApi from './bitcoin-api-wrapper';
 | 
			
		||||
import bitcoinApi from './bitcoin/bitcoin-api-factory';
 | 
			
		||||
import { DB } from '../database';
 | 
			
		||||
import { IBlock, ITransaction } from '../interfaces';
 | 
			
		||||
import memPool from './mempool';
 | 
			
		||||
@ -56,7 +56,7 @@ class Blocks {
 | 
			
		||||
          block = storedBlock;
 | 
			
		||||
        } else {
 | 
			
		||||
          const blockHash = await bitcoinApi.getBlockHash(this.currentBlockHeight);
 | 
			
		||||
          block = await bitcoinApi.getBlock(blockHash, 1);
 | 
			
		||||
          block = await bitcoinApi.getBlock(blockHash);
 | 
			
		||||
 | 
			
		||||
          const coinbase = await memPool.getRawTransaction(block.tx[0], true);
 | 
			
		||||
          if (coinbase && coinbase.totalOut) {
 | 
			
		||||
@ -74,6 +74,7 @@ class Blocks {
 | 
			
		||||
              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);
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
const config = require('../../mempool-config.json');
 | 
			
		||||
import bitcoinApi from './bitcoin-api-wrapper';
 | 
			
		||||
import bitcoinApi from './bitcoin/bitcoin-api-factory';
 | 
			
		||||
import { ITransaction, IMempoolInfo, IMempool } from '../interfaces';
 | 
			
		||||
 | 
			
		||||
class Mempool {
 | 
			
		||||
@ -52,32 +52,40 @@ class Mempool {
 | 
			
		||||
  public async getRawTransaction(txId: string, isCoinbase = false): Promise<ITransaction | false> {
 | 
			
		||||
    try {
 | 
			
		||||
      const transaction = await bitcoinApi.getRawTransaction(txId);
 | 
			
		||||
      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');
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      let totalOut = 0;
 | 
			
		||||
      transaction.vout.forEach((output) => totalOut += output.value);
 | 
			
		||||
 | 
			
		||||
      if (totalIn > totalOut) {
 | 
			
		||||
        transaction.fee = parseFloat((totalIn - totalOut).toFixed(8));
 | 
			
		||||
      if (config.BACKEND_API === 'esplora') {
 | 
			
		||||
        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 / 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;
 | 
			
		||||
      }
 | 
			
		||||
      transaction.totalOut = totalOut;
 | 
			
		||||
      return transaction;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      console.log(txId + ' not found');
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,7 @@ import * as http from 'http';
 | 
			
		||||
import * as https from 'https';
 | 
			
		||||
import * as WebSocket from 'ws';
 | 
			
		||||
 | 
			
		||||
import bitcoinApi from './api/bitcoin-api-wrapper';
 | 
			
		||||
import bitcoinApi from './api/bitcoin/bitcoin-api-factory';
 | 
			
		||||
import diskCache from './api/disk-cache';
 | 
			
		||||
import memPool from './api/mempool';
 | 
			
		||||
import blocks from './api/blocks';
 | 
			
		||||
@ -55,7 +55,7 @@ class MempoolSpace {
 | 
			
		||||
        port: 8999
 | 
			
		||||
    };
 | 
			
		||||
    this.server.listen(opts, () => {
 | 
			
		||||
      console.log(`Server started on ${opts.host}:${opts.port})`);
 | 
			
		||||
      console.log(`Server started on ${opts.host}:${opts.port}`);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,10 @@
 | 
			
		||||
export interface IMempoolInfo {
 | 
			
		||||
  size: number;
 | 
			
		||||
  bytes: number;
 | 
			
		||||
  usage: number;
 | 
			
		||||
  maxmempool: number;
 | 
			
		||||
  mempoolminfee: number;
 | 
			
		||||
  minrelaytxfee: number;
 | 
			
		||||
  usage?: number;
 | 
			
		||||
  maxmempool?: number;
 | 
			
		||||
  mempoolminfee?: number;
 | 
			
		||||
  minrelaytxfee?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ITransaction {
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										1187
									
								
								backend/yarn.lock
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1187
									
								
								backend/yarn.lock
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -22,6 +22,9 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
 | 
			
		||||
  ngOnInit() {
 | 
			
		||||
    this.blocksSubscription = this.memPoolService.blocks$
 | 
			
		||||
      .subscribe((block) => {
 | 
			
		||||
        if (this.blocks.some((b) => b.height === block.height)) {
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
        this.blocks.unshift(block);
 | 
			
		||||
        this.blocks = this.blocks.slice(0, 8);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user