Big refactor of multiple backends handling.
This commit is contained in:
		
							parent
							
								
									8d0db12abe
								
							
						
					
					
						commit
						bb28a56622
					
				@ -1,7 +1,7 @@
 | 
			
		||||
{
 | 
			
		||||
  "MEMPOOL": {
 | 
			
		||||
    "NETWORK": "mainnet",
 | 
			
		||||
    "BACKEND": "electrs",
 | 
			
		||||
    "BACKEND": "electrum",
 | 
			
		||||
    "HTTP_PORT": 8999,
 | 
			
		||||
    "SPAWN_CLUSTER_PROCS": 0,
 | 
			
		||||
    "API_URL_PREFIX": "/api/v1/",
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,7 @@ import * as fs from 'fs';
 | 
			
		||||
import axios from 'axios';
 | 
			
		||||
import { BisqBlocks, BisqBlock, BisqTransaction, BisqStats, BisqTrade } from './interfaces';
 | 
			
		||||
import { Common } from '../common';
 | 
			
		||||
import { Block } from '../../interfaces';
 | 
			
		||||
import { BlockExtended } from '../../mempool.interfaces';
 | 
			
		||||
import { StaticPool } from 'node-worker-threads-pool';
 | 
			
		||||
import logger from '../../logger';
 | 
			
		||||
 | 
			
		||||
@ -42,7 +42,7 @@ class Bisq {
 | 
			
		||||
    this.startSubDirectoryWatcher();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleNewBitcoinBlock(block: Block): void {
 | 
			
		||||
  handleNewBitcoinBlock(block: BlockExtended): void {
 | 
			
		||||
    if (block.height - 2 > this.latestBlockHeight && this.latestBlockHeight !== 0) {
 | 
			
		||||
      logger.warn(`Bitcoin block height (#${block.height}) has diverged from the latest Bisq block height (#${this.latestBlockHeight}). Restarting watchers...`);
 | 
			
		||||
      this.startTopDirectoryWatcher();
 | 
			
		||||
 | 
			
		||||
@ -1,21 +1,13 @@
 | 
			
		||||
import { MempoolInfo, Transaction, Block, MempoolEntries, MempoolEntry, Address, AddressInformation,
 | 
			
		||||
  ScriptHashBalance, ScriptHashHistory } from '../../interfaces';
 | 
			
		||||
import { IEsploraApi } from './esplora-api.interface';
 | 
			
		||||
 | 
			
		||||
export interface AbstractBitcoinApi {
 | 
			
		||||
  $getMempoolInfo(): Promise<MempoolInfo>;
 | 
			
		||||
  $getRawMempool(): Promise<Transaction['txid'][]>;
 | 
			
		||||
  $getRawTransaction(txId: string): Promise<Transaction>;
 | 
			
		||||
  $getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]>;
 | 
			
		||||
  $getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean): Promise<IEsploraApi.Transaction>;
 | 
			
		||||
  $getRawTransactionBitcoind(txId: string, skipConversion?: boolean, addPrevout?: boolean): Promise<IEsploraApi.Transaction>;
 | 
			
		||||
  $getBlockHeightTip(): Promise<number>;
 | 
			
		||||
  $getTxIdsForBlock(hash: string): Promise<string[]>;
 | 
			
		||||
  $getBlockHash(height: number): Promise<string>;
 | 
			
		||||
  $getBlock(hash: string): Promise<Block>;
 | 
			
		||||
  $getMempoolEntry(txid: string): Promise<MempoolEntry>;
 | 
			
		||||
  $getAddress(address: string): Promise<Address>;
 | 
			
		||||
  $validateAddress(address: string): Promise<AddressInformation>;
 | 
			
		||||
  $getScriptHashBalance(scriptHash: string): Promise<ScriptHashBalance>;
 | 
			
		||||
  $getScriptHashHistory(scriptHash: string): Promise<ScriptHashHistory[]>;
 | 
			
		||||
 | 
			
		||||
  // Custom
 | 
			
		||||
  $getRawMempoolVerbose(): Promise<MempoolEntries>;
 | 
			
		||||
  $getRawTransactionBitcond(txId: string): Promise<Transaction>;
 | 
			
		||||
  $getBlock(hash: string): Promise<IEsploraApi.Block>;
 | 
			
		||||
  $getAddress(address: string): Promise<IEsploraApi.Address>;
 | 
			
		||||
  $getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,18 +1,18 @@
 | 
			
		||||
import config from '../../config';
 | 
			
		||||
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
 | 
			
		||||
import BitcoindElectrsApi from './bitcoind-electrs-api';
 | 
			
		||||
import BitcoindApi from './bitcoind-api';
 | 
			
		||||
import ElectrsApi from './electrs-api';
 | 
			
		||||
import EsploraApi from './esplora-api';
 | 
			
		||||
import BitcoinApi from './bitcoin-api';
 | 
			
		||||
import ElectrumApi from './electrum-api';
 | 
			
		||||
 | 
			
		||||
function bitcoinApiFactory(): AbstractBitcoinApi {
 | 
			
		||||
  switch (config.MEMPOOL.BACKEND) {
 | 
			
		||||
    case 'electrs':
 | 
			
		||||
      return new ElectrsApi();
 | 
			
		||||
    case 'bitcoind-electrs':
 | 
			
		||||
      return new BitcoindElectrsApi();
 | 
			
		||||
    case 'bitcoind':
 | 
			
		||||
    case 'esplora':
 | 
			
		||||
      return new EsploraApi();
 | 
			
		||||
    case 'electrum':
 | 
			
		||||
      return new ElectrumApi();
 | 
			
		||||
    case 'none':
 | 
			
		||||
    default:
 | 
			
		||||
      return new BitcoindApi();
 | 
			
		||||
      return new BitcoinApi();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										116
									
								
								backend/src/api/bitcoin/bitcoin-api.interface.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								backend/src/api/bitcoin/bitcoin-api.interface.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,116 @@
 | 
			
		||||
export namespace IBitcoinApi {
 | 
			
		||||
  export interface MempoolInfo {
 | 
			
		||||
    loaded: boolean;                 //  (boolean) True if the mempool is fully loaded
 | 
			
		||||
    size: number;                    //  (numeric) Current tx count
 | 
			
		||||
    bytes: number;                   //  (numeric) Sum of all virtual transaction sizes as defined in BIP 141.
 | 
			
		||||
    usage: number;                   //  (numeric) Total memory usage for the mempool
 | 
			
		||||
    maxmempool: number;              //  (numeric) Maximum memory usage for the mempool
 | 
			
		||||
    mempoolminfee: number;           //  (numeric) Minimum fee rate in BTC/kB for tx to be accepted.
 | 
			
		||||
    minrelaytxfee: number;           //  (numeric) Current minimum relay fee for transactions
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface RawMempool { [txId: string]: MempoolEntry; }
 | 
			
		||||
 | 
			
		||||
  export interface MempoolEntry {
 | 
			
		||||
    vsize: number;                   //  (numeric) virtual transaction size as defined in BIP 141.
 | 
			
		||||
    weight: number;                  //  (numeric) transaction weight as defined in BIP 141.
 | 
			
		||||
    time: number;                    //  (numeric) local time transaction entered pool in seconds since 1 Jan 1970 GMT
 | 
			
		||||
    height: number;                  //  (numeric) block height when transaction entered pool
 | 
			
		||||
    descendantcount: number;         //  (numeric) number of in-mempool descendant transactions (including this one)
 | 
			
		||||
    descendantsize: number;          //  (numeric) virtual transaction size of in-mempool descendants (including this one)
 | 
			
		||||
    ancestorcount: number;           //  (numeric) number of in-mempool ancestor transactions (including this one)
 | 
			
		||||
    ancestorsize: number;            //  (numeric) virtual transaction size of in-mempool ancestors (including this one)
 | 
			
		||||
    wtxid: string;                   //  (string) hash of serialized transactionumber; including witness data
 | 
			
		||||
    fees: {
 | 
			
		||||
      base: number;                  //  (numeric) transaction fee in BTC
 | 
			
		||||
      modified: number;              //  (numeric) transaction fee with fee deltas used for mining priority in BTC
 | 
			
		||||
      ancestor: number;              //  (numeric) modified fees (see above) of in-mempool ancestors (including this one) in BTC
 | 
			
		||||
      descendant: number;            //  (numeric) modified fees (see above) of in-mempool descendants (including this one) in BTC
 | 
			
		||||
    };
 | 
			
		||||
    depends: string[];               //  (string) parent transaction id
 | 
			
		||||
    spentby: string[];               //  (array) unconfirmed transactions spending outputs from this transaction
 | 
			
		||||
    'bip125-replaceable': boolean;   //  (boolean) Whether this transaction could be replaced due to BIP125 (replace-by-fee)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface Block {
 | 
			
		||||
    hash: string;                    //  (string) the block hash (same as provided)
 | 
			
		||||
    confirmations: number;           //  (numeric) The number of confirmations, or -1 if the block is not on the main chain
 | 
			
		||||
    size: number;                    //  (numeric) The block size
 | 
			
		||||
    strippedsize: number;            //  (numeric) The block size excluding witness data
 | 
			
		||||
    weight: number;                  //  (numeric) The block weight as defined in BIP 141
 | 
			
		||||
    height: number;                  //  (numeric) The block height or index
 | 
			
		||||
    version: number;                 //  (numeric) The block version
 | 
			
		||||
    versionHex: string;              //  (string) The block version formatted in hexadecimal
 | 
			
		||||
    merkleroot: string;              //  (string) The merkle root
 | 
			
		||||
    tx: Transaction[];
 | 
			
		||||
    time: number;                    //  (numeric) The block time expressed in UNIX epoch time
 | 
			
		||||
    mediantime: number;              //  (numeric) The median block time expressed in UNIX epoch time
 | 
			
		||||
    nonce: number;                   //  (numeric) The nonce
 | 
			
		||||
    bits: string;                    //  (string) The bits
 | 
			
		||||
    difficulty: number;              //  (numeric) The difficulty
 | 
			
		||||
    chainwork: string;               //  (string) Expected number of hashes required to produce the chain up to this block (in hex)
 | 
			
		||||
    nTx: number;                     //  (numeric) The number of transactions in the block
 | 
			
		||||
    previousblockhash: string;       //  (string) The hash of the previous block
 | 
			
		||||
    nextblockhash: string;           //  (string) The hash of the next block
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface Transaction {
 | 
			
		||||
    in_active_chain: boolean;        //  (boolean) Whether specified block is in the active chain or not
 | 
			
		||||
    hex: string;                     //  (string) The serialized, hex-encoded data for 'txid'
 | 
			
		||||
    txid: string;                    //  (string) The transaction id (same as provided)
 | 
			
		||||
    hash: string;                    //  (string) The transaction hash (differs from txid for witness transactions)
 | 
			
		||||
    size: number;                    //  (numeric) The serialized transaction size
 | 
			
		||||
    vsize: number;                   //  (numeric) The virtual transaction size (differs from size for witness transactions)
 | 
			
		||||
    weight: number;                  //  (numeric) The transaction's weight (between vsize*4-3 and vsize*4)
 | 
			
		||||
    version: number;                 //  (numeric) The version
 | 
			
		||||
    locktime: number;                //  (numeric) The lock time
 | 
			
		||||
    vin: Vin[];
 | 
			
		||||
    vout: Vout[];
 | 
			
		||||
    blockhash: string;               //  (string) the block hash
 | 
			
		||||
    confirmations: number;           //  (numeric) The confirmations
 | 
			
		||||
    blocktime: number;               //  (numeric) The block time expressed in UNIX epoch time
 | 
			
		||||
    time: number;                    //  (numeric) Same as blocktime
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  interface Vin {
 | 
			
		||||
    txid?: string;                   //  (string) The transaction id
 | 
			
		||||
    vout?: number;                   //  (string)
 | 
			
		||||
    scriptSig?: {                    //  (json object) The script
 | 
			
		||||
      asm: string;                   //  (string) asm
 | 
			
		||||
      hex: string;                   //  (string) hex
 | 
			
		||||
    };
 | 
			
		||||
    sequence: number;                //  (numeric) The script sequence number
 | 
			
		||||
    txinwitness?: string[];          //  (string) hex-encoded witness data
 | 
			
		||||
    coinbase?: string;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  interface Vout {
 | 
			
		||||
    value: number;                   //  (numeric) The value in BTC
 | 
			
		||||
    n: number;                       //  (numeric) index
 | 
			
		||||
    scriptPubKey: {                  //  (json object)
 | 
			
		||||
      asm: string;                   //  (string) the asm
 | 
			
		||||
      hex: string;                   //  (string) the hex
 | 
			
		||||
      reqSigs: number;               //  (numeric) The required sigs
 | 
			
		||||
      type: string;                  //  (string) The type, eg 'pubkeyhash'
 | 
			
		||||
      addresses: string[]            //  (string) bitcoin address
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface AddressInformation {
 | 
			
		||||
    isvalid: boolean;                //  (boolean) If the address is valid or not. If not, this is the only property returned.
 | 
			
		||||
    address: string;                 //  (string) The bitcoin address validated
 | 
			
		||||
    scriptPubKey: string;            //  (string) The hex-encoded scriptPubKey generated by the address
 | 
			
		||||
    isscript: boolean;               //  (boolean) If the key is a script
 | 
			
		||||
    iswitness: boolean;              //  (boolean) If the address is a witness
 | 
			
		||||
    witness_version?: boolean;       //  (numeric, optional) The version number of the witness program
 | 
			
		||||
    witness_program: string;         //  (string, optional) The hex value of the witness program
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface ChainTips {
 | 
			
		||||
    height: number;                  //  (numeric) height of the chain tip
 | 
			
		||||
    hash: string;                    //  (string) block hash of the tip
 | 
			
		||||
    branchlen: number;               //  (numeric) zero for main chain, otherwise length of branch connecting the tip to the main chain
 | 
			
		||||
    status: 'invalid' | 'headers-only' | 'valid-headers' | 'valid-fork' | 'active';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										199
									
								
								backend/src/api/bitcoin/bitcoin-api.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										199
									
								
								backend/src/api/bitcoin/bitcoin-api.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,199 @@
 | 
			
		||||
import config from '../../config';
 | 
			
		||||
import * as bitcoin from '@mempool/bitcoin';
 | 
			
		||||
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
 | 
			
		||||
import { IBitcoinApi } from './bitcoin-api.interface';
 | 
			
		||||
import { IEsploraApi } from './esplora-api.interface';
 | 
			
		||||
import blocks from '../blocks';
 | 
			
		||||
import bitcoinBaseApi from './bitcoin-base.api';
 | 
			
		||||
import mempool from '../mempool';
 | 
			
		||||
 | 
			
		||||
class BitcoinApi implements AbstractBitcoinApi {
 | 
			
		||||
  private rawMempoolCache: IBitcoinApi.RawMempool | null = null;
 | 
			
		||||
  private bitcoindClient: any;
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
    this.bitcoindClient = new bitcoin.Client({
 | 
			
		||||
      host: config.BITCOIND.HOST,
 | 
			
		||||
      port: config.BITCOIND.PORT,
 | 
			
		||||
      user: config.BITCOIND.USERNAME,
 | 
			
		||||
      pass: config.BITCOIND.PASSWORD,
 | 
			
		||||
      timeout: 60000,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getRawTransactionBitcoind(txId: string, skipConversion = false, addPrevout = false): Promise<IEsploraApi.Transaction> {
 | 
			
		||||
    return this.bitcoindClient.getRawTransaction(txId, true)
 | 
			
		||||
      .then((transaction: IBitcoinApi.Transaction) => {
 | 
			
		||||
        if (skipConversion) {
 | 
			
		||||
          return transaction;
 | 
			
		||||
        }
 | 
			
		||||
        return this.$convertTransaction(transaction, addPrevout);
 | 
			
		||||
      });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getRawTransaction(txId: string, skipConversion = false, addPrevout = false): Promise<IEsploraApi.Transaction> {
 | 
			
		||||
    return this.bitcoindClient.getRawTransaction(txId, true)
 | 
			
		||||
      .then((transaction: IBitcoinApi.Transaction) => {
 | 
			
		||||
        if (skipConversion) {
 | 
			
		||||
          return transaction;
 | 
			
		||||
        }
 | 
			
		||||
        return this.$convertTransaction(transaction, addPrevout);
 | 
			
		||||
      });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getBlockHeightTip(): Promise<number> {
 | 
			
		||||
    return this.bitcoindClient.getChainTips()
 | 
			
		||||
      .then((result: IBitcoinApi.ChainTips[]) => result[0].height);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getTxIdsForBlock(hash: string): Promise<string[]> {
 | 
			
		||||
    return this.bitcoindClient.getBlock(hash, 1)
 | 
			
		||||
      .then((rpcBlock: IBitcoinApi.Block) => rpcBlock.tx);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getBlockHash(height: number): Promise<string> {
 | 
			
		||||
    return this.bitcoindClient.getBlockHash(height);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getBlock(hash: string): Promise<IEsploraApi.Block> {
 | 
			
		||||
    return this.bitcoindClient.getBlock(hash)
 | 
			
		||||
      .then((block: IBitcoinApi.Block) => this.convertBlock(block));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getAddress(address: string): Promise<IEsploraApi.Address> {
 | 
			
		||||
    throw new Error('Method getAddress not supported by the Bitcoin RPC API.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]> {
 | 
			
		||||
    throw new Error('Method getAddressTransactions not supported by the Bitcoin RPC API.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]> {
 | 
			
		||||
    return this.bitcoindClient.getRawMemPool();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected async $convertTransaction(transaction: IBitcoinApi.Transaction, addPrevout: boolean): Promise<IEsploraApi.Transaction> {
 | 
			
		||||
    let esploraTransaction: IEsploraApi.Transaction = {
 | 
			
		||||
      txid: transaction.txid,
 | 
			
		||||
      version: transaction.version,
 | 
			
		||||
      locktime: transaction.locktime,
 | 
			
		||||
      size: transaction.size,
 | 
			
		||||
      weight: transaction.weight,
 | 
			
		||||
      fee: 0,
 | 
			
		||||
      vin: [],
 | 
			
		||||
      vout: [],
 | 
			
		||||
      status: { confirmed: false },
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    esploraTransaction.vout = transaction.vout.map((vout) => {
 | 
			
		||||
      return {
 | 
			
		||||
        value: vout.value * 100000000,
 | 
			
		||||
        scriptpubkey: vout.scriptPubKey.hex,
 | 
			
		||||
        scriptpubkey_address: vout.scriptPubKey && vout.scriptPubKey.addresses ? vout.scriptPubKey.addresses[0] : '',
 | 
			
		||||
        scriptpubkey_asm: vout.scriptPubKey.asm,
 | 
			
		||||
        scriptpubkey_type: this.translateScriptPubKeyType(vout.scriptPubKey.type),
 | 
			
		||||
      };
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    esploraTransaction.vin = transaction.vin.map((vin) => {
 | 
			
		||||
      return {
 | 
			
		||||
        is_coinbase: !!vin.coinbase,
 | 
			
		||||
        prevout: null,
 | 
			
		||||
        scriptsig: vin.scriptSig && vin.scriptSig.hex || vin.coinbase || '',
 | 
			
		||||
        scriptsig_asm: vin.scriptSig && vin.scriptSig.asm || '',
 | 
			
		||||
        sequence: vin.sequence,
 | 
			
		||||
        txid: vin.txid || '',
 | 
			
		||||
        vout: vin.vout || 0,
 | 
			
		||||
        witness: vin.txinwitness,
 | 
			
		||||
      };
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    if (transaction.confirmations) {
 | 
			
		||||
      esploraTransaction.status = {
 | 
			
		||||
        confirmed: true,
 | 
			
		||||
        block_height: blocks.getCurrentBlockHeight() - transaction.confirmations + 1,
 | 
			
		||||
        block_hash: transaction.blockhash,
 | 
			
		||||
        block_time: transaction.blocktime,
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (transaction.confirmations) {
 | 
			
		||||
      esploraTransaction = await this.$calculateFeeFromInputs(esploraTransaction, addPrevout);
 | 
			
		||||
    } else {
 | 
			
		||||
      esploraTransaction = await this.$appendMempoolFeeData(esploraTransaction);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return esploraTransaction;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private convertBlock(block: IBitcoinApi.Block): IEsploraApi.Block {
 | 
			
		||||
    return {
 | 
			
		||||
      id: block.hash,
 | 
			
		||||
      height: block.height,
 | 
			
		||||
      version: block.version,
 | 
			
		||||
      timestamp: block.time,
 | 
			
		||||
      bits: parseInt(block.bits, 16),
 | 
			
		||||
      nonce: block.nonce,
 | 
			
		||||
      difficulty: block.difficulty,
 | 
			
		||||
      merkle_root: block.merkleroot,
 | 
			
		||||
      tx_count: block.nTx,
 | 
			
		||||
      size: block.size,
 | 
			
		||||
      weight: block.weight,
 | 
			
		||||
      previousblockhash: block.previousblockhash,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private translateScriptPubKeyType(outputType: string): string {
 | 
			
		||||
    const map = {
 | 
			
		||||
      'pubkey': 'p2pk',
 | 
			
		||||
      'pubkeyhash': 'p2pkh',
 | 
			
		||||
      'scripthash': 'p2sh',
 | 
			
		||||
      'witness_v0_keyhash': 'v0_p2wpkh',
 | 
			
		||||
      'witness_v0_scripthash': 'v0_p2wsh',
 | 
			
		||||
      'witness_v1_taproot': 'v1_p2tr',
 | 
			
		||||
      'nonstandard': 'nonstandard',
 | 
			
		||||
      'nulldata': 'op_return'
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    if (map[outputType]) {
 | 
			
		||||
      return map[outputType];
 | 
			
		||||
    } else {
 | 
			
		||||
      return '';
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $appendMempoolFeeData(transaction: IEsploraApi.Transaction): Promise<IEsploraApi.Transaction> {
 | 
			
		||||
    let mempoolEntry: IBitcoinApi.MempoolEntry;
 | 
			
		||||
    if (!mempool.isInSync() && !this.rawMempoolCache) {
 | 
			
		||||
      this.rawMempoolCache = await bitcoinBaseApi.$getRawMempoolVerbose();
 | 
			
		||||
    }
 | 
			
		||||
    if (this.rawMempoolCache && this.rawMempoolCache[transaction.txid]) {
 | 
			
		||||
      mempoolEntry = this.rawMempoolCache[transaction.txid];
 | 
			
		||||
    } else {
 | 
			
		||||
      mempoolEntry = await bitcoinBaseApi.$getMempoolEntry(transaction.txid);
 | 
			
		||||
    }
 | 
			
		||||
    transaction.fee = mempoolEntry.fees.base * 100000000;
 | 
			
		||||
    return transaction;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $calculateFeeFromInputs(transaction: IEsploraApi.Transaction, addPrevout: boolean): Promise<IEsploraApi.Transaction> {
 | 
			
		||||
    if (transaction.vin[0].is_coinbase) {
 | 
			
		||||
      transaction.fee = 0;
 | 
			
		||||
      return transaction;
 | 
			
		||||
    }
 | 
			
		||||
    let totalIn = 0;
 | 
			
		||||
    for (const vin of transaction.vin) {
 | 
			
		||||
      const innerTx = await this.$getRawTransaction(vin.txid, !addPrevout);
 | 
			
		||||
      if (addPrevout) {
 | 
			
		||||
        vin.prevout = innerTx.vout[vin.vout];
 | 
			
		||||
      }
 | 
			
		||||
      totalIn += innerTx.vout[vin.vout].value;
 | 
			
		||||
    }
 | 
			
		||||
    const totalOut = transaction.vout.reduce((prev, output) => prev + output.value, 0);
 | 
			
		||||
    transaction.fee = parseFloat((totalIn - totalOut).toFixed(8));
 | 
			
		||||
    return transaction;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default BitcoinApi;
 | 
			
		||||
							
								
								
									
										40
									
								
								backend/src/api/bitcoin/bitcoin-base.api.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								backend/src/api/bitcoin/bitcoin-base.api.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,40 @@
 | 
			
		||||
import config from '../../config';
 | 
			
		||||
import * as bitcoin from '@mempool/bitcoin';
 | 
			
		||||
import { IBitcoinApi } from './bitcoin-api.interface';
 | 
			
		||||
 | 
			
		||||
class BitcoinBaseApi {
 | 
			
		||||
  bitcoindClient: any;
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
    this.bitcoindClient = new bitcoin.Client({
 | 
			
		||||
      host: config.BITCOIND.HOST,
 | 
			
		||||
      port: config.BITCOIND.PORT,
 | 
			
		||||
      user: config.BITCOIND.USERNAME,
 | 
			
		||||
      pass: config.BITCOIND.PASSWORD,
 | 
			
		||||
      timeout: 60000,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getMempoolInfo(): Promise<IBitcoinApi.MempoolInfo> {
 | 
			
		||||
    return this.bitcoindClient.getMempoolInfo();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getRawTransaction(txId: string): Promise<IBitcoinApi.Transaction> {
 | 
			
		||||
    return this.bitcoindClient.getRawTransaction(txId, true);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getMempoolEntry(txid: string): Promise<IBitcoinApi.MempoolEntry> {
 | 
			
		||||
    return this.bitcoindClient.getMempoolEntry(txid);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getRawMempoolVerbose(): Promise<IBitcoinApi.RawMempool> {
 | 
			
		||||
    return this.bitcoindClient.getRawMemPool(true);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $validateAddress(address: string): Promise<IBitcoinApi.AddressInformation> {
 | 
			
		||||
    return this.bitcoindClient.validateAddress(address);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new BitcoinBaseApi();
 | 
			
		||||
@ -1,101 +0,0 @@
 | 
			
		||||
import config from '../../config';
 | 
			
		||||
import { Transaction, Block, MempoolInfo, RpcBlock, MempoolEntries, MempoolEntry, Address,
 | 
			
		||||
    AddressInformation, ScriptHashBalance, ScriptHashHistory } from '../../interfaces';
 | 
			
		||||
import * as bitcoin from '@mempool/bitcoin';
 | 
			
		||||
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
 | 
			
		||||
 | 
			
		||||
class BitcoindApi implements AbstractBitcoinApi {
 | 
			
		||||
  bitcoindClient: any;
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
    this.bitcoindClient = new bitcoin.Client({
 | 
			
		||||
      host: config.BITCOIND.HOST,
 | 
			
		||||
      port: config.BITCOIND.PORT,
 | 
			
		||||
      user: config.BITCOIND.USERNAME,
 | 
			
		||||
      pass: config.BITCOIND.PASSWORD,
 | 
			
		||||
      timeout: 60000,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getMempoolInfo(): Promise<MempoolInfo> {
 | 
			
		||||
    return this.bitcoindClient.getMempoolInfo();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getRawMempool(): Promise<Transaction['txid'][]> {
 | 
			
		||||
    return this.bitcoindClient.getRawMemPool();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getRawMempoolVerbose(): Promise<MempoolEntries> {
 | 
			
		||||
    return this.bitcoindClient.getRawMemPool(true);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getMempoolEntry(txid: string): Promise<MempoolEntry> {
 | 
			
		||||
    return this.bitcoindClient.getMempoolEntry(txid);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getRawTransaction(txId: string): Promise<Transaction> {
 | 
			
		||||
    return this.bitcoindClient.getRawTransaction(txId, true)
 | 
			
		||||
      .then((transaction: Transaction) => {
 | 
			
		||||
        transaction.vout.forEach((vout) => vout.value = vout.value * 100000000);
 | 
			
		||||
        return transaction;
 | 
			
		||||
      });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getBlockHeightTip(): Promise<number> {
 | 
			
		||||
    return this.bitcoindClient.getChainTips()
 | 
			
		||||
      .then((result) => result[0].height);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getTxIdsForBlock(hash: string): Promise<string[]> {
 | 
			
		||||
    return this.bitcoindClient.getBlock(hash, 1)
 | 
			
		||||
      .then((rpcBlock: RpcBlock) => {
 | 
			
		||||
        return rpcBlock.tx;
 | 
			
		||||
      });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getBlockHash(height: number): Promise<string> {
 | 
			
		||||
    return this.bitcoindClient.getBlockHash(height);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getBlock(hash: string): Promise<Block> {
 | 
			
		||||
    return this.bitcoindClient.getBlock(hash)
 | 
			
		||||
      .then((rpcBlock: RpcBlock) => {
 | 
			
		||||
        return {
 | 
			
		||||
          id: rpcBlock.hash,
 | 
			
		||||
          height: rpcBlock.height,
 | 
			
		||||
          version: rpcBlock.version,
 | 
			
		||||
          timestamp: rpcBlock.time,
 | 
			
		||||
          bits: rpcBlock.bits,
 | 
			
		||||
          nonce: rpcBlock.nonce,
 | 
			
		||||
          difficulty: rpcBlock.difficulty,
 | 
			
		||||
          merkle_root: rpcBlock.merkleroot,
 | 
			
		||||
          tx_count: rpcBlock.nTx,
 | 
			
		||||
          size: rpcBlock.size,
 | 
			
		||||
          weight: rpcBlock.weight,
 | 
			
		||||
          previousblockhash: rpcBlock.previousblockhash,
 | 
			
		||||
        };
 | 
			
		||||
      });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getRawTransactionBitcond(txId: string): Promise<Transaction> {
 | 
			
		||||
    throw new Error('Method not implemented.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getAddress(address: string): Promise<Address> {
 | 
			
		||||
    throw new Error('Method not implemented.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $validateAddress(address: string): Promise<AddressInformation> {
 | 
			
		||||
    return this.bitcoindClient.validateAddress(address);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getScriptHashBalance(scriptHash: string): Promise<ScriptHashBalance> {
 | 
			
		||||
    throw new Error('Method not implemented.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getScriptHashHistory(scriptHash: string): Promise<ScriptHashHistory[]> {
 | 
			
		||||
    throw new Error('Method not implemented.');
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default BitcoindApi;
 | 
			
		||||
@ -1,141 +0,0 @@
 | 
			
		||||
import config from '../../config';
 | 
			
		||||
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
 | 
			
		||||
import { Transaction, Block, MempoolInfo, RpcBlock, MempoolEntries, MempoolEntry, Address,
 | 
			
		||||
  AddressInformation, ScriptHashBalance, ScriptHashHistory } from '../../interfaces';
 | 
			
		||||
import * as bitcoin from '@mempool/bitcoin';
 | 
			
		||||
import * as ElectrumClient from '@codewarriorr/electrum-client-js';
 | 
			
		||||
import logger from '../../logger';
 | 
			
		||||
import transactionUtils from '../transaction-utils';
 | 
			
		||||
import * as sha256 from 'crypto-js/sha256';
 | 
			
		||||
import * as hexEnc from 'crypto-js/enc-hex';
 | 
			
		||||
class BitcoindElectrsApi implements AbstractBitcoinApi {
 | 
			
		||||
  bitcoindClient: any;
 | 
			
		||||
  electrumClient: any;
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
    this.bitcoindClient = new bitcoin.Client({
 | 
			
		||||
      host: config.BITCOIND.HOST,
 | 
			
		||||
      port: config.BITCOIND.PORT,
 | 
			
		||||
      user: config.BITCOIND.USERNAME,
 | 
			
		||||
      pass: config.BITCOIND.PASSWORD,
 | 
			
		||||
      timeout: 60000,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.electrumClient = new ElectrumClient(
 | 
			
		||||
      config.ELECTRS.HOST,
 | 
			
		||||
      config.ELECTRS.PORT,
 | 
			
		||||
      'ssl'
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    this.electrumClient.connect(
 | 
			
		||||
      'electrum-client-js',
 | 
			
		||||
      '1.4'
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getMempoolInfo(): Promise<MempoolInfo> {
 | 
			
		||||
    return this.bitcoindClient.getMempoolInfo();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getRawMempool(): Promise<Transaction['txid'][]> {
 | 
			
		||||
    return this.bitcoindClient.getRawMemPool();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getRawMempoolVerbose(): Promise<MempoolEntries> {
 | 
			
		||||
    return this.bitcoindClient.getRawMemPool(true);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getMempoolEntry(txid: string): Promise<MempoolEntry> {
 | 
			
		||||
    return this.bitcoindClient.getMempoolEntry(txid);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async $getRawTransaction(txId: string): Promise<Transaction> {
 | 
			
		||||
    try {
 | 
			
		||||
      const transaction: Transaction = await this.electrumClient.blockchain_transaction_get(txId, true);
 | 
			
		||||
      if (!transaction) {
 | 
			
		||||
        throw new Error(txId + ' not found!');
 | 
			
		||||
      }
 | 
			
		||||
      transactionUtils.bitcoindToElectrsTransaction(transaction);
 | 
			
		||||
      return transaction;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.debug('getRawTransaction error: ' + (e.message || e));
 | 
			
		||||
      throw new Error(e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getRawTransactionBitcond(txId: string): Promise<Transaction> {
 | 
			
		||||
    return this.bitcoindClient.getRawTransaction(txId, true)
 | 
			
		||||
      .then((transaction: Transaction) => {
 | 
			
		||||
        transactionUtils.bitcoindToElectrsTransaction(transaction);
 | 
			
		||||
        return transaction;
 | 
			
		||||
      });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getBlockHeightTip(): Promise<number> {
 | 
			
		||||
    return this.bitcoindClient.getChainTips()
 | 
			
		||||
      .then((result) => result[0].height);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getTxIdsForBlock(hash: string): Promise<string[]> {
 | 
			
		||||
    return this.bitcoindClient.getBlock(hash, 1)
 | 
			
		||||
      .then((rpcBlock: RpcBlock) => {
 | 
			
		||||
        return rpcBlock.tx;
 | 
			
		||||
      });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getBlockHash(height: number): Promise<string> {
 | 
			
		||||
    return this.bitcoindClient.getBlockHash(height);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getBlock(hash: string): Promise<Block> {
 | 
			
		||||
    return this.bitcoindClient.getBlock(hash)
 | 
			
		||||
      .then((rpcBlock: RpcBlock) => {
 | 
			
		||||
        return {
 | 
			
		||||
          id: rpcBlock.hash,
 | 
			
		||||
          height: rpcBlock.height,
 | 
			
		||||
          version: rpcBlock.version,
 | 
			
		||||
          timestamp: rpcBlock.time,
 | 
			
		||||
          bits: rpcBlock.bits,
 | 
			
		||||
          nonce: rpcBlock.nonce,
 | 
			
		||||
          difficulty: rpcBlock.difficulty,
 | 
			
		||||
          merkle_root: rpcBlock.merkleroot,
 | 
			
		||||
          tx_count: rpcBlock.nTx,
 | 
			
		||||
          size: rpcBlock.size,
 | 
			
		||||
          weight: rpcBlock.weight,
 | 
			
		||||
          previousblockhash: rpcBlock.previousblockhash,
 | 
			
		||||
        };
 | 
			
		||||
      });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async $getAddress(address: string): Promise<Address> {
 | 
			
		||||
    try {
 | 
			
		||||
      const addressInfo: Address = await this.electrumClient.blockchain_scripthash_getBalance(address);
 | 
			
		||||
      if (!address) {
 | 
			
		||||
        throw new Error('not found');
 | 
			
		||||
      }
 | 
			
		||||
      return addressInfo;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      logger.debug('getRawTransaction error: ' + (e.message || e));
 | 
			
		||||
      throw new Error(e);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $validateAddress(address: string): Promise<AddressInformation> {
 | 
			
		||||
    return this.bitcoindClient.validateAddress(address);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getScriptHashBalance(scriptHash: string): Promise<ScriptHashBalance> {
 | 
			
		||||
    return this.electrumClient.blockchain_scripthash_getBalance(this.encodeScriptHash(scriptHash));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getScriptHashHistory(scriptHash: string): Promise<ScriptHashHistory[]> {
 | 
			
		||||
    return this.electrumClient.blockchain_scripthash_getHistory(this.encodeScriptHash(scriptHash));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private encodeScriptHash(scriptPubKey: string): string {
 | 
			
		||||
    const addrScripthash = hexEnc.stringify(sha256(hexEnc.parse(scriptPubKey)));
 | 
			
		||||
    return addrScripthash.match(/.{2}/g).reverse().join('');
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default BitcoindElectrsApi;
 | 
			
		||||
@ -1,82 +0,0 @@
 | 
			
		||||
import config from '../../config';
 | 
			
		||||
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
 | 
			
		||||
import { Transaction, Block, MempoolInfo, MempoolEntry, MempoolEntries, Address,
 | 
			
		||||
  AddressInformation, ScriptHashBalance, ScriptHashHistory } from '../../interfaces';
 | 
			
		||||
import axios from 'axios';
 | 
			
		||||
 | 
			
		||||
class ElectrsApi implements AbstractBitcoinApi {
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getMempoolInfo(): Promise<MempoolInfo> {
 | 
			
		||||
    return axios.get<any>(config.ELECTRS.REST_API_URL + '/mempool', { timeout: 10000 })
 | 
			
		||||
      .then((response) => {
 | 
			
		||||
        return {
 | 
			
		||||
          size: response.data.count,
 | 
			
		||||
          bytes: response.data.vsize,
 | 
			
		||||
        };
 | 
			
		||||
      });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getRawMempool(): Promise<Transaction['txid'][]> {
 | 
			
		||||
    return axios.get<Transaction['txid'][]>(config.ELECTRS.REST_API_URL + '/mempool/txids')
 | 
			
		||||
      .then((response) => response.data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getRawTransaction(txId: string): Promise<Transaction> {
 | 
			
		||||
    return axios.get<Transaction>(config.ELECTRS.REST_API_URL + '/tx/' + txId)
 | 
			
		||||
      .then((response) => response.data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getBlockHeightTip(): Promise<number> {
 | 
			
		||||
    return axios.get<number>(config.ELECTRS.REST_API_URL + '/blocks/tip/height')
 | 
			
		||||
      .then((response) => response.data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getTxIdsForBlock(hash: string): Promise<string[]> {
 | 
			
		||||
    return axios.get<string[]>(config.ELECTRS.REST_API_URL + '/block/' + hash + '/txids')
 | 
			
		||||
      .then((response) => response.data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getBlockHash(height: number): Promise<string> {
 | 
			
		||||
    return axios.get<string>(config.ELECTRS.REST_API_URL + '/block-height/' + height)
 | 
			
		||||
      .then((response) => response.data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getBlock(hash: string): Promise<Block> {
 | 
			
		||||
    return axios.get<Block>(config.ELECTRS.REST_API_URL + '/block/' + hash)
 | 
			
		||||
      .then((response) => response.data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getRawMempoolVerbose(): Promise<MempoolEntries> {
 | 
			
		||||
    throw new Error('Method not implemented.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getMempoolEntry(): Promise<MempoolEntry> {
 | 
			
		||||
    throw new Error('Method not implemented.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getRawTransactionBitcond(txId: string): Promise<Transaction> {
 | 
			
		||||
    throw new Error('Method not implemented.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getAddress(address: string): Promise<Address> {
 | 
			
		||||
    throw new Error('Method not implemented.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getScriptHashBalance(scriptHash: string): Promise<ScriptHashBalance> {
 | 
			
		||||
    throw new Error('Method not implemented.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getScriptHashHistory(scriptHash: string): Promise<ScriptHashHistory[]> {
 | 
			
		||||
    throw new Error('Method not implemented.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $validateAddress(address: string): Promise<AddressInformation> {
 | 
			
		||||
    throw new Error('Method not implemented.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ElectrsApi;
 | 
			
		||||
							
								
								
									
										12
									
								
								backend/src/api/bitcoin/electrum-api.interface.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								backend/src/api/bitcoin/electrum-api.interface.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
			
		||||
export namespace IElectrumApi {
 | 
			
		||||
  export interface ScriptHashBalance {
 | 
			
		||||
    confirmed: number;
 | 
			
		||||
    unconfirmed: number;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface ScriptHashHistory {
 | 
			
		||||
    height: number;
 | 
			
		||||
    tx_hash: string;
 | 
			
		||||
    fee?: number;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										119
									
								
								backend/src/api/bitcoin/electrum-api.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								backend/src/api/bitcoin/electrum-api.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,119 @@
 | 
			
		||||
import config from '../../config';
 | 
			
		||||
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
 | 
			
		||||
import * as ElectrumClient from '@codewarriorr/electrum-client-js';
 | 
			
		||||
import { IBitcoinApi } from './bitcoin-api.interface';
 | 
			
		||||
import { IEsploraApi } from './esplora-api.interface';
 | 
			
		||||
import { IElectrumApi } from './electrum-api.interface';
 | 
			
		||||
import * as sha256 from 'crypto-js/sha256';
 | 
			
		||||
import * as hexEnc from 'crypto-js/enc-hex';
 | 
			
		||||
import BitcoinApi from './bitcoin-api';
 | 
			
		||||
import bitcoinBaseApi from './bitcoin-base.api';
 | 
			
		||||
class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi {
 | 
			
		||||
  private electrumClient: any;
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
    super();
 | 
			
		||||
 | 
			
		||||
    this.electrumClient = new ElectrumClient(
 | 
			
		||||
      config.ELECTRS.HOST,
 | 
			
		||||
      config.ELECTRS.PORT,
 | 
			
		||||
      'ssl'
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    this.electrumClient.connect(
 | 
			
		||||
      'electrum-client-js',
 | 
			
		||||
      '1.4'
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async $getRawTransaction(txId: string, skipConversion = false, addPrevout = false): Promise<IEsploraApi.Transaction> {
 | 
			
		||||
    const transaction: IBitcoinApi.Transaction = await this.electrumClient.blockchain_transaction_get(txId, true);
 | 
			
		||||
    if (!transaction) {
 | 
			
		||||
      throw new Error('Unable to get transaction: ' + txId);
 | 
			
		||||
    }
 | 
			
		||||
    if (skipConversion) {
 | 
			
		||||
      // @ts-ignore
 | 
			
		||||
      return transaction;
 | 
			
		||||
    }
 | 
			
		||||
    return this.$convertTransaction(transaction, addPrevout);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async $getAddress(address: string): Promise<IEsploraApi.Address> {
 | 
			
		||||
    const addressInfo = await bitcoinBaseApi.$validateAddress(address);
 | 
			
		||||
    if (!addressInfo || !addressInfo.isvalid) {
 | 
			
		||||
      return ({
 | 
			
		||||
        'address': address,
 | 
			
		||||
        'chain_stats': {
 | 
			
		||||
          'funded_txo_count': 0,
 | 
			
		||||
          'funded_txo_sum': 0,
 | 
			
		||||
          'spent_txo_count': 0,
 | 
			
		||||
          'spent_txo_sum': 0,
 | 
			
		||||
          'tx_count': 0
 | 
			
		||||
        },
 | 
			
		||||
        'mempool_stats': {
 | 
			
		||||
          'funded_txo_count': 0,
 | 
			
		||||
          'funded_txo_sum': 0,
 | 
			
		||||
          'spent_txo_count': 0,
 | 
			
		||||
          'spent_txo_sum': 0,
 | 
			
		||||
          'tx_count': 0
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const balance = await this.$getScriptHashBalance(addressInfo.scriptPubKey);
 | 
			
		||||
    const history = await this.$getScriptHashHistory(addressInfo.scriptPubKey);
 | 
			
		||||
 | 
			
		||||
    const unconfirmed = history.filter((h) => h.fee).length;
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      'address': addressInfo.address,
 | 
			
		||||
      'chain_stats': {
 | 
			
		||||
        'funded_txo_count': 0,
 | 
			
		||||
        'funded_txo_sum': balance.confirmed ? balance.confirmed : 0,
 | 
			
		||||
        'spent_txo_count': 0,
 | 
			
		||||
        'spent_txo_sum': balance.confirmed < 0 ? balance.confirmed : 0,
 | 
			
		||||
        'tx_count': history.length - unconfirmed,
 | 
			
		||||
      },
 | 
			
		||||
      'mempool_stats': {
 | 
			
		||||
        'funded_txo_count': 0,
 | 
			
		||||
        'funded_txo_sum': balance.unconfirmed > 0 ? balance.unconfirmed : 0,
 | 
			
		||||
        'spent_txo_count': 0,
 | 
			
		||||
        'spent_txo_sum': balance.unconfirmed < 0 ? -balance.unconfirmed : 0,
 | 
			
		||||
        'tx_count': unconfirmed,
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async $getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]> {
 | 
			
		||||
    const addressInfo = await bitcoinBaseApi.$validateAddress(address);
 | 
			
		||||
    if (!addressInfo || !addressInfo.isvalid) {
 | 
			
		||||
     return [];
 | 
			
		||||
    }
 | 
			
		||||
    const history = await this.$getScriptHashHistory(addressInfo.scriptPubKey);
 | 
			
		||||
    const transactions: IEsploraApi.Transaction[] = [];
 | 
			
		||||
    for (const h of history) {
 | 
			
		||||
      const tx = await this.$getRawTransaction(h.tx_hash);
 | 
			
		||||
      if (tx) {
 | 
			
		||||
        transactions.push(tx);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return transactions;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private $getScriptHashBalance(scriptHash: string): Promise<IElectrumApi.ScriptHashBalance> {
 | 
			
		||||
    return this.electrumClient.blockchain_scripthash_getBalance(this.encodeScriptHash(scriptHash));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private $getScriptHashHistory(scriptHash: string): Promise<IElectrumApi.ScriptHashHistory[]> {
 | 
			
		||||
    return this.electrumClient.blockchain_scripthash_getHistory(this.encodeScriptHash(scriptHash));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private encodeScriptHash(scriptPubKey: string): string {
 | 
			
		||||
    const addrScripthash = hexEnc.stringify(sha256(hexEnc.parse(scriptPubKey)));
 | 
			
		||||
    return addrScripthash.match(/.{2}/g).reverse().join('');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default BitcoindElectrsApi;
 | 
			
		||||
							
								
								
									
										168
									
								
								backend/src/api/bitcoin/esplora-api.interface.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										168
									
								
								backend/src/api/bitcoin/esplora-api.interface.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,168 @@
 | 
			
		||||
export namespace IEsploraApi {
 | 
			
		||||
  export interface Transaction {
 | 
			
		||||
    txid: string;
 | 
			
		||||
    version: number;
 | 
			
		||||
    locktime: number;
 | 
			
		||||
    size: number;
 | 
			
		||||
    weight: number;
 | 
			
		||||
    fee: number;
 | 
			
		||||
    vin: Vin[];
 | 
			
		||||
    vout: Vout[];
 | 
			
		||||
    status: Status;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface Recent {
 | 
			
		||||
    txid: string;
 | 
			
		||||
    fee: number;
 | 
			
		||||
    vsize: number;
 | 
			
		||||
    value: number;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface Vin {
 | 
			
		||||
    txid: string;
 | 
			
		||||
    vout: number;
 | 
			
		||||
    is_coinbase: boolean;
 | 
			
		||||
    scriptsig: string;
 | 
			
		||||
    scriptsig_asm: string;
 | 
			
		||||
    inner_redeemscript_asm?: string;
 | 
			
		||||
    inner_witnessscript_asm?: string;
 | 
			
		||||
    sequence: any;
 | 
			
		||||
    witness?: string[];
 | 
			
		||||
    prevout: Vout | null;
 | 
			
		||||
    // Elements
 | 
			
		||||
    is_pegin?: boolean;
 | 
			
		||||
    issuance?: Issuance;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  interface Issuance {
 | 
			
		||||
    asset_id: string;
 | 
			
		||||
    is_reissuance: string;
 | 
			
		||||
    asset_blinding_nonce: string;
 | 
			
		||||
    asset_entropy: string;
 | 
			
		||||
    contract_hash: string;
 | 
			
		||||
    assetamount?: number;
 | 
			
		||||
    assetamountcommitment?: string;
 | 
			
		||||
    tokenamount?: number;
 | 
			
		||||
    tokenamountcommitment?: string;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface Vout {
 | 
			
		||||
    scriptpubkey: string;
 | 
			
		||||
    scriptpubkey_asm: string;
 | 
			
		||||
    scriptpubkey_type: string;
 | 
			
		||||
    scriptpubkey_address: string;
 | 
			
		||||
    value: number;
 | 
			
		||||
    // Elements
 | 
			
		||||
    valuecommitment?: number;
 | 
			
		||||
    asset?: string;
 | 
			
		||||
    pegout?: Pegout;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  interface Pegout {
 | 
			
		||||
    genesis_hash: string;
 | 
			
		||||
    scriptpubkey: string;
 | 
			
		||||
    scriptpubkey_asm: string;
 | 
			
		||||
    scriptpubkey_address: string;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface Status {
 | 
			
		||||
    confirmed: boolean;
 | 
			
		||||
    block_height?: number;
 | 
			
		||||
    block_hash?: string;
 | 
			
		||||
    block_time?: number;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface Block {
 | 
			
		||||
    id: string;
 | 
			
		||||
    height: number;
 | 
			
		||||
    version: number;
 | 
			
		||||
    timestamp: number;
 | 
			
		||||
    bits: number;
 | 
			
		||||
    nonce: number;
 | 
			
		||||
    difficulty: number;
 | 
			
		||||
    merkle_root: string;
 | 
			
		||||
    tx_count: number;
 | 
			
		||||
    size: number;
 | 
			
		||||
    weight: number;
 | 
			
		||||
    previousblockhash: string;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface Address {
 | 
			
		||||
    address: string;
 | 
			
		||||
    chain_stats: ChainStats;
 | 
			
		||||
    mempool_stats: MempoolStats;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface ChainStats {
 | 
			
		||||
    funded_txo_count: number;
 | 
			
		||||
    funded_txo_sum: number;
 | 
			
		||||
    spent_txo_count: number;
 | 
			
		||||
    spent_txo_sum: number;
 | 
			
		||||
    tx_count: number;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface MempoolStats {
 | 
			
		||||
    funded_txo_count: number;
 | 
			
		||||
    funded_txo_sum: number;
 | 
			
		||||
    spent_txo_count: number;
 | 
			
		||||
    spent_txo_sum: number;
 | 
			
		||||
    tx_count: number;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface Outspend {
 | 
			
		||||
    spent: boolean;
 | 
			
		||||
    txid: string;
 | 
			
		||||
    vin: number;
 | 
			
		||||
    status: Status;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface Asset {
 | 
			
		||||
    asset_id: string;
 | 
			
		||||
    issuance_txin: IssuanceTxin;
 | 
			
		||||
    issuance_prevout: IssuancePrevout;
 | 
			
		||||
    reissuance_token: string;
 | 
			
		||||
    contract_hash: string;
 | 
			
		||||
    status: Status;
 | 
			
		||||
    chain_stats: AssetStats;
 | 
			
		||||
    mempool_stats: AssetStats;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface AssetExtended extends Asset {
 | 
			
		||||
    name: string;
 | 
			
		||||
    ticker: string;
 | 
			
		||||
    precision: number;
 | 
			
		||||
    entity: Entity;
 | 
			
		||||
    version: number;
 | 
			
		||||
    issuer_pubkey: string;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  export interface Entity {
 | 
			
		||||
    domain: string;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  interface IssuanceTxin {
 | 
			
		||||
    txid: string;
 | 
			
		||||
    vin: number;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  interface IssuancePrevout {
 | 
			
		||||
    txid: string;
 | 
			
		||||
    vout: number;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  interface AssetStats {
 | 
			
		||||
    tx_count: number;
 | 
			
		||||
    issuance_count: number;
 | 
			
		||||
    issued_amount: number;
 | 
			
		||||
    burned_amount: number;
 | 
			
		||||
    has_blinded_issuances: boolean;
 | 
			
		||||
    reissuance_tokens: number;
 | 
			
		||||
    burned_reissuance_tokens: number;
 | 
			
		||||
    peg_in_count: number;
 | 
			
		||||
    peg_in_amount: number;
 | 
			
		||||
    peg_out_count: number;
 | 
			
		||||
    peg_out_amount: number;
 | 
			
		||||
    burn_count: number;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										55
									
								
								backend/src/api/bitcoin/esplora-api.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								backend/src/api/bitcoin/esplora-api.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,55 @@
 | 
			
		||||
import config from '../../config';
 | 
			
		||||
import axios from 'axios';
 | 
			
		||||
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
 | 
			
		||||
import { IEsploraApi } from './esplora-api.interface';
 | 
			
		||||
 | 
			
		||||
class ElectrsApi implements AbstractBitcoinApi {
 | 
			
		||||
 | 
			
		||||
  constructor() { }
 | 
			
		||||
 | 
			
		||||
  $getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]> {
 | 
			
		||||
    return axios.get<IEsploraApi.Transaction['txid'][]>(config.ELECTRS.REST_API_URL + '/mempool/txids')
 | 
			
		||||
      .then((response) => response.data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getRawTransaction(txId: string): Promise<IEsploraApi.Transaction> {
 | 
			
		||||
    return axios.get<IEsploraApi.Transaction>(config.ELECTRS.REST_API_URL + '/tx/' + txId)
 | 
			
		||||
      .then((response) => response.data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getBlockHeightTip(): Promise<number> {
 | 
			
		||||
    return axios.get<number>(config.ELECTRS.REST_API_URL + '/blocks/tip/height')
 | 
			
		||||
      .then((response) => response.data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getTxIdsForBlock(hash: string): Promise<string[]> {
 | 
			
		||||
    return axios.get<string[]>(config.ELECTRS.REST_API_URL + '/block/' + hash + '/txids')
 | 
			
		||||
      .then((response) => response.data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getBlockHash(height: number): Promise<string> {
 | 
			
		||||
    return axios.get<string>(config.ELECTRS.REST_API_URL + '/block-height/' + height)
 | 
			
		||||
      .then((response) => response.data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getBlock(hash: string): Promise<IEsploraApi.Block> {
 | 
			
		||||
    return axios.get<IEsploraApi.Block>(config.ELECTRS.REST_API_URL + '/block/' + hash)
 | 
			
		||||
      .then((response) => response.data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getAddress(address: string): Promise<IEsploraApi.Address> {
 | 
			
		||||
    throw new Error('Method getAddress not implemented.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getAddressTransactions(address: string, txId?: string): Promise<IEsploraApi.Transaction[]> {
 | 
			
		||||
    throw new Error('Method getAddressTransactions not implemented.');
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  $getRawTransactionBitcoind(txId: string): Promise<IEsploraApi.Transaction> {
 | 
			
		||||
    return axios.get<IEsploraApi.Transaction>(config.ELECTRS.REST_API_URL + '/tx/' + txId)
 | 
			
		||||
      .then((response) => response.data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default ElectrsApi;
 | 
			
		||||
@ -2,29 +2,29 @@ import config from '../config';
 | 
			
		||||
import bitcoinApi from './bitcoin/bitcoin-api-factory';
 | 
			
		||||
import logger from '../logger';
 | 
			
		||||
import memPool from './mempool';
 | 
			
		||||
import { Block, TransactionExtended, TransactionMinerInfo } from '../interfaces';
 | 
			
		||||
import { BlockExtended, TransactionExtended, TransactionMinerInfo } from '../mempool.interfaces';
 | 
			
		||||
import { Common } from './common';
 | 
			
		||||
import diskCache from './disk-cache';
 | 
			
		||||
import transactionUtils from './transaction-utils';
 | 
			
		||||
 | 
			
		||||
class Blocks {
 | 
			
		||||
  private static KEEP_BLOCK_AMOUNT = 8;
 | 
			
		||||
  private blocks: Block[] = [];
 | 
			
		||||
  private blocks: BlockExtended[] = [];
 | 
			
		||||
  private currentBlockHeight = 0;
 | 
			
		||||
  private lastDifficultyAdjustmentTime = 0;
 | 
			
		||||
  private newBlockCallbacks: ((block: Block, txIds: string[], transactions: TransactionExtended[]) => void)[] = [];
 | 
			
		||||
  private newBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void)[] = [];
 | 
			
		||||
 | 
			
		||||
  constructor() { }
 | 
			
		||||
 | 
			
		||||
  public getBlocks(): Block[] {
 | 
			
		||||
  public getBlocks(): BlockExtended[] {
 | 
			
		||||
    return this.blocks;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public setBlocks(blocks: Block[]) {
 | 
			
		||||
  public setBlocks(blocks: BlockExtended[]) {
 | 
			
		||||
    this.blocks = blocks;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public setNewBlockCallback(fn: (block: Block, txIds: string[], transactions: TransactionExtended[]) => void) {
 | 
			
		||||
  public setNewBlockCallback(fn: (block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void) {
 | 
			
		||||
    this.newBlockCallbacks.push(fn);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -68,9 +68,8 @@ class Blocks {
 | 
			
		||||
 | 
			
		||||
      for (let i = 0; i < txIds.length; i++) {
 | 
			
		||||
        // When using bitcoind, just fetch the coinbase tx for now
 | 
			
		||||
        if ((config.MEMPOOL.BACKEND === 'bitcoind' ||
 | 
			
		||||
            config.MEMPOOL.BACKEND === 'bitcoind-electrs') && i === 0) {
 | 
			
		||||
          const tx = await transactionUtils.getTransactionExtended(txIds[i], true);
 | 
			
		||||
        if (config.MEMPOOL.BACKEND !== 'none' && i === 0) {
 | 
			
		||||
          const tx = await transactionUtils.$getTransactionExtended(txIds[i]);
 | 
			
		||||
          if (tx) {
 | 
			
		||||
            transactions.push(tx);
 | 
			
		||||
          }
 | 
			
		||||
@ -78,9 +77,9 @@ class Blocks {
 | 
			
		||||
        if (mempool[txIds[i]]) {
 | 
			
		||||
          transactions.push(mempool[txIds[i]]);
 | 
			
		||||
          found++;
 | 
			
		||||
        } else if (config.MEMPOOL.BACKEND === 'electrs') {
 | 
			
		||||
        } else if (config.MEMPOOL.BACKEND === 'esplora') {
 | 
			
		||||
          logger.debug(`Fetching block tx ${i} of ${txIds.length}`);
 | 
			
		||||
          const tx = await transactionUtils.getTransactionExtended(txIds[i]);
 | 
			
		||||
          const tx = await transactionUtils.$getTransactionExtended(txIds[i]);
 | 
			
		||||
          if (tx) {
 | 
			
		||||
            transactions.push(tx);
 | 
			
		||||
          }
 | 
			
		||||
@ -89,23 +88,24 @@ class Blocks {
 | 
			
		||||
 | 
			
		||||
      logger.debug(`${found} of ${txIds.length} found in mempool. ${txIds.length - found} not found.`);
 | 
			
		||||
 | 
			
		||||
      block.reward = transactions[0].vout.reduce((acc, curr) => acc + curr.value, 0);
 | 
			
		||||
      block.coinbaseTx = this.stripCoinbaseTransaction(transactions[0]);
 | 
			
		||||
      const blockExtended: BlockExtended = Object.assign({}, block);
 | 
			
		||||
      blockExtended.reward = transactions[0].vout.reduce((acc, curr) => acc + curr.value, 0);
 | 
			
		||||
      blockExtended.coinbaseTx = this.stripCoinbaseTransaction(transactions[0]);
 | 
			
		||||
      transactions.sort((a, b) => b.feePerVsize - a.feePerVsize);
 | 
			
		||||
      block.medianFee = transactions.length > 1 ? Common.median(transactions.map((tx) => tx.feePerVsize)) : 0;
 | 
			
		||||
      block.feeRange = transactions.length > 1 ? Common.getFeesInRange(transactions.slice(0, transactions.length - 1), 8) : [0, 0];
 | 
			
		||||
      blockExtended.medianFee = transactions.length > 1 ? Common.median(transactions.map((tx) => tx.feePerVsize)) : 0;
 | 
			
		||||
      blockExtended.feeRange = transactions.length > 1 ? Common.getFeesInRange(transactions.slice(0, transactions.length - 1), 8) : [0, 0];
 | 
			
		||||
 | 
			
		||||
      if (block.height % 2016 === 0) {
 | 
			
		||||
        this.lastDifficultyAdjustmentTime = block.timestamp;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this.blocks.push(block);
 | 
			
		||||
      this.blocks.push(blockExtended);
 | 
			
		||||
      if (this.blocks.length > Blocks.KEEP_BLOCK_AMOUNT) {
 | 
			
		||||
        this.blocks = this.blocks.slice(-Blocks.KEEP_BLOCK_AMOUNT);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (this.newBlockCallbacks.length) {
 | 
			
		||||
        this.newBlockCallbacks.forEach((cb) => cb(block, txIds, transactions));
 | 
			
		||||
        this.newBlockCallbacks.forEach((cb) => cb(blockExtended, txIds, transactions));
 | 
			
		||||
      }
 | 
			
		||||
      diskCache.$saveCacheToDisk();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { TransactionExtended, TransactionStripped } from '../interfaces';
 | 
			
		||||
import { TransactionExtended, TransactionStripped } from '../mempool.interfaces';
 | 
			
		||||
 | 
			
		||||
export class Common {
 | 
			
		||||
  static median(numbers: number[]) {
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
import config from '../config';
 | 
			
		||||
import { MempoolBlock } from '../interfaces';
 | 
			
		||||
import { MempoolBlock } from '../mempool.interfaces';
 | 
			
		||||
import projectedBlocks from './mempool-blocks';
 | 
			
		||||
 | 
			
		||||
class FeeApi {
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { MempoolBlock, TransactionExtended, MempoolBlockWithTransactions } from '../interfaces';
 | 
			
		||||
import { MempoolBlock, TransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces';
 | 
			
		||||
import { Common } from './common';
 | 
			
		||||
 | 
			
		||||
class MempoolBlocks {
 | 
			
		||||
 | 
			
		||||
@ -1,15 +1,18 @@
 | 
			
		||||
import config from '../config';
 | 
			
		||||
import bitcoinApi from './bitcoin/bitcoin-api-factory';
 | 
			
		||||
import { MempoolInfo, TransactionExtended, Transaction, VbytesPerSecond, MempoolEntry, MempoolEntries } from '../interfaces';
 | 
			
		||||
import { TransactionExtended, VbytesPerSecond } from '../mempool.interfaces';
 | 
			
		||||
import logger from '../logger';
 | 
			
		||||
import { Common } from './common';
 | 
			
		||||
import transactionUtils from './transaction-utils';
 | 
			
		||||
import { IBitcoinApi } from './bitcoin/bitcoin-api.interface';
 | 
			
		||||
import bitcoinBaseApi from './bitcoin/bitcoin-base.api';
 | 
			
		||||
 | 
			
		||||
class Mempool {
 | 
			
		||||
  private inSync: boolean = false;
 | 
			
		||||
  private mempoolCache: { [txId: string]: TransactionExtended } = {};
 | 
			
		||||
  private mempoolInfo: MempoolInfo = { size: 0, bytes: 0 };
 | 
			
		||||
  private mempoolChangedCallback: ((newMempool: { [txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[],
 | 
			
		||||
  private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0,
 | 
			
		||||
                                                    maxmempool: 0, mempoolminfee: 0, minrelaytxfee: 0 };
 | 
			
		||||
  private mempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[],
 | 
			
		||||
    deletedTransactions: TransactionExtended[]) => void) | undefined;
 | 
			
		||||
 | 
			
		||||
  private txPerSecondArray: number[] = [];
 | 
			
		||||
@ -49,10 +52,10 @@ class Mempool {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $updateMemPoolInfo() {
 | 
			
		||||
    this.mempoolInfo = await bitcoinApi.$getMempoolInfo();
 | 
			
		||||
    this.mempoolInfo = await bitcoinBaseApi.$getMempoolInfo();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public getMempoolInfo(): MempoolInfo | undefined {
 | 
			
		||||
  public getMempoolInfo(): IBitcoinApi.MempoolInfo | undefined {
 | 
			
		||||
    return this.mempoolInfo;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -67,8 +70,9 @@ class Mempool {
 | 
			
		||||
  public getFirstSeenForTransactions(txIds: string[]): number[] {
 | 
			
		||||
    const txTimes: number[] = [];
 | 
			
		||||
    txIds.forEach((txId: string) => {
 | 
			
		||||
      if (this.mempoolCache[txId]) {
 | 
			
		||||
        txTimes.push(this.mempoolCache[txId].firstSeen);
 | 
			
		||||
      const tx = this.mempoolCache[txId];
 | 
			
		||||
      if (tx && tx.firstSeen) {
 | 
			
		||||
        txTimes.push(tx.firstSeen);
 | 
			
		||||
      } else {
 | 
			
		||||
        txTimes.push(0);
 | 
			
		||||
      }
 | 
			
		||||
@ -88,7 +92,7 @@ class Mempool {
 | 
			
		||||
 | 
			
		||||
    for (const txid of transactions) {
 | 
			
		||||
      if (!this.mempoolCache[txid]) {
 | 
			
		||||
        const transaction = await transactionUtils.getTransactionExtended(txid, false, true);
 | 
			
		||||
        const transaction = await transactionUtils.$getTransactionExtended(txid, true);
 | 
			
		||||
        if (transaction) {
 | 
			
		||||
          this.mempoolCache[txid] = transaction;
 | 
			
		||||
          txCount++;
 | 
			
		||||
@ -118,6 +122,7 @@ class Mempool {
 | 
			
		||||
 | 
			
		||||
    // Prevent mempool from clear on bitcoind restart by delaying the deletion
 | 
			
		||||
    if (this.mempoolProtection === 0
 | 
			
		||||
      && config.MEMPOOL.BACKEND === 'esplora'
 | 
			
		||||
      && currentMempoolSize > 20000
 | 
			
		||||
      && transactions.length / currentMempoolSize <= 0.80
 | 
			
		||||
    ) {
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@ import memPool from './mempool';
 | 
			
		||||
import { DB } from '../database';
 | 
			
		||||
import logger from '../logger';
 | 
			
		||||
 | 
			
		||||
import { Statistic, TransactionExtended, OptimizedStatistic } from '../interfaces';
 | 
			
		||||
import { Statistic, TransactionExtended, OptimizedStatistic } from '../mempool.interfaces';
 | 
			
		||||
 | 
			
		||||
class Statistics {
 | 
			
		||||
  protected intervalTimer: NodeJS.Timer | undefined;
 | 
			
		||||
 | 
			
		||||
@ -1,16 +1,15 @@
 | 
			
		||||
import bitcoinApi from './bitcoin/bitcoin-api-factory';
 | 
			
		||||
import { MempoolEntries, MempoolEntry, Transaction, TransactionExtended, TransactionMinerInfo } from '../interfaces';
 | 
			
		||||
import config from '../config';
 | 
			
		||||
import logger from '../logger';
 | 
			
		||||
import mempool from './mempool';
 | 
			
		||||
import blocks from './blocks';
 | 
			
		||||
import { TransactionExtended, TransactionMinerInfo } from '../mempool.interfaces';
 | 
			
		||||
import { IEsploraApi } from './bitcoin/esplora-api.interface';
 | 
			
		||||
 | 
			
		||||
class TransactionUtils {
 | 
			
		||||
  private mempoolEntriesCache: MempoolEntries | null = null;
 | 
			
		||||
 | 
			
		||||
  constructor() { }
 | 
			
		||||
 | 
			
		||||
  public async $addPrevoutsToTransaction(transaction: TransactionExtended): Promise<TransactionExtended> {
 | 
			
		||||
    if (transaction.vin[0].is_coinbase) {
 | 
			
		||||
      return transaction;
 | 
			
		||||
    }
 | 
			
		||||
    for (const vin of transaction.vin) {
 | 
			
		||||
      const innerTx = await bitcoinApi.$getRawTransaction(vin.txid);
 | 
			
		||||
      vin.prevout = innerTx.vout[vin.vout];
 | 
			
		||||
@ -18,26 +17,10 @@ class TransactionUtils {
 | 
			
		||||
    return transaction;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async $calculateFeeFromInputs(transaction: Transaction): Promise<TransactionExtended> {
 | 
			
		||||
    if (transaction.vin[0]['coinbase']) {
 | 
			
		||||
      transaction.fee = 0;
 | 
			
		||||
      // @ts-ignore
 | 
			
		||||
      return transaction;
 | 
			
		||||
    }
 | 
			
		||||
    let totalIn = 0;
 | 
			
		||||
    for (const vin of transaction.vin) {
 | 
			
		||||
      const innerTx = await bitcoinApi.$getRawTransaction(vin.txid);
 | 
			
		||||
      totalIn += innerTx.vout[vin.vout].value;
 | 
			
		||||
    }
 | 
			
		||||
    const totalOut = transaction.vout.reduce((prev, output) => prev + output.value, 0);
 | 
			
		||||
    transaction.fee = parseFloat((totalIn - totalOut).toFixed(8));
 | 
			
		||||
    return this.extendTransaction(transaction);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public extendTransaction(transaction: Transaction): TransactionExtended {
 | 
			
		||||
  public extendTransaction(transaction: IEsploraApi.Transaction): TransactionExtended {
 | 
			
		||||
    transaction['vsize'] = Math.round(transaction.weight / 4);
 | 
			
		||||
    transaction['feePerVsize'] = Math.max(1, (transaction.fee || 0) / (transaction.weight / 4));
 | 
			
		||||
    if (!transaction.in_active_chain) {
 | 
			
		||||
    if (!transaction.status.confirmed) {
 | 
			
		||||
      transaction['firstSeen'] = Math.round((new Date().getTime() / 1000));
 | 
			
		||||
    }
 | 
			
		||||
    // @ts-ignore
 | 
			
		||||
@ -58,20 +41,13 @@ class TransactionUtils {
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async getTransactionExtended(txId: string, isCoinbase = false, inMempool = false): Promise<TransactionExtended | null> {
 | 
			
		||||
  public async $getTransactionExtended(txId: string, inMempool = false, addPrevouts = false): Promise<TransactionExtended | null> {
 | 
			
		||||
    try {
 | 
			
		||||
      let transaction: Transaction;
 | 
			
		||||
      let transaction: IEsploraApi.Transaction;
 | 
			
		||||
      if (inMempool) {
 | 
			
		||||
        transaction = await bitcoinApi.$getRawTransactionBitcond(txId);
 | 
			
		||||
        transaction = await bitcoinApi.$getRawTransactionBitcoind(txId, false, addPrevouts);
 | 
			
		||||
      } else {
 | 
			
		||||
        transaction = await bitcoinApi.$getRawTransaction(txId);
 | 
			
		||||
      }
 | 
			
		||||
      if (config.MEMPOOL.BACKEND !== 'electrs' && !isCoinbase) {
 | 
			
		||||
        if (inMempool) {
 | 
			
		||||
          transaction = await this.$appendFeeData(transaction);
 | 
			
		||||
        } else {
 | 
			
		||||
          transaction = await this.$calculateFeeFromInputs(transaction);
 | 
			
		||||
        }
 | 
			
		||||
        transaction = await bitcoinApi.$getRawTransaction(txId, false, addPrevouts);
 | 
			
		||||
      }
 | 
			
		||||
      return this.extendTransaction(transaction);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
@ -81,64 +57,6 @@ class TransactionUtils {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public bitcoindToElectrsTransaction(transaction: any): void {
 | 
			
		||||
    try {
 | 
			
		||||
      transaction.vout = transaction.vout.map((vout) => {
 | 
			
		||||
        return {
 | 
			
		||||
          value: vout.value * 100000000,
 | 
			
		||||
          scriptpubkey: vout.scriptPubKey.hex,
 | 
			
		||||
          scriptpubkey_address: vout.scriptPubKey && vout.scriptPubKey.addresses ? vout.scriptPubKey.addresses[0] : null,
 | 
			
		||||
          scriptpubkey_asm: vout.scriptPubKey.asm,
 | 
			
		||||
          scriptpubkey_type: this.translateScriptPubKeyType(vout.scriptPubKey.type),
 | 
			
		||||
        };
 | 
			
		||||
      });
 | 
			
		||||
      if (transaction.confirmations) {
 | 
			
		||||
        transaction['status'] = {
 | 
			
		||||
          confirmed: true,
 | 
			
		||||
          block_height: blocks.getCurrentBlockHeight() - transaction.confirmations,
 | 
			
		||||
          block_hash: transaction.blockhash,
 | 
			
		||||
          block_time: transaction.blocktime,
 | 
			
		||||
        };
 | 
			
		||||
      } else {
 | 
			
		||||
        transaction['status'] = { confirmed: false };
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      console.log('augment failed: ' + (e.message || e));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private translateScriptPubKeyType(outputType: string): string {
 | 
			
		||||
    const map = {
 | 
			
		||||
      'pubkey': 'p2pk',
 | 
			
		||||
      'pubkeyhash': 'p2pkh',
 | 
			
		||||
      'scripthash': 'p2sh',
 | 
			
		||||
      'witness_v0_keyhash': 'v0_p2wpkh',
 | 
			
		||||
      'witness_v0_scripthash': 'v0_p2wsh',
 | 
			
		||||
      'witness_v1_taproot': 'v1_p2tr',
 | 
			
		||||
      'nonstandard': 'nonstandard',
 | 
			
		||||
      'nulldata': 'nulldata'
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    if (map[outputType]) {
 | 
			
		||||
      return map[outputType];
 | 
			
		||||
    } else {
 | 
			
		||||
      return '';
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async $appendFeeData(transaction: Transaction): Promise<Transaction> {
 | 
			
		||||
    let mempoolEntry: MempoolEntry;
 | 
			
		||||
    if (!mempool.isInSync() && !this.mempoolEntriesCache) {
 | 
			
		||||
      this.mempoolEntriesCache = await bitcoinApi.$getRawMempoolVerbose();
 | 
			
		||||
    }
 | 
			
		||||
    if (this.mempoolEntriesCache && this.mempoolEntriesCache[transaction.txid]) {
 | 
			
		||||
      mempoolEntry = this.mempoolEntriesCache[transaction.txid];
 | 
			
		||||
    } else {
 | 
			
		||||
      mempoolEntry = await bitcoinApi.$getMempoolEntry(transaction.txid);
 | 
			
		||||
    }
 | 
			
		||||
    transaction.fee = mempoolEntry.fees.base * 100000000;
 | 
			
		||||
    return transaction;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default new TransactionUtils();
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import logger from '../logger';
 | 
			
		||||
import * as WebSocket from 'ws';
 | 
			
		||||
import { Block, TransactionExtended, WebsocketResponse, MempoolBlock, OptimizedStatistic } from '../interfaces';
 | 
			
		||||
import { BlockExtended, TransactionExtended, WebsocketResponse, MempoolBlock, OptimizedStatistic } from '../mempool.interfaces';
 | 
			
		||||
import blocks from './blocks';
 | 
			
		||||
import memPool from './mempool';
 | 
			
		||||
import backendInfo from './backend-info';
 | 
			
		||||
@ -117,7 +117,7 @@ class WebsocketHandler {
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getInitData(_blocks?: Block[]) {
 | 
			
		||||
  getInitData(_blocks?: BlockExtended[]) {
 | 
			
		||||
    if (!_blocks) {
 | 
			
		||||
      _blocks = blocks.getBlocks();
 | 
			
		||||
    }
 | 
			
		||||
@ -256,7 +256,7 @@ class WebsocketHandler {
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  handleNewBlock(block: Block, txIds: string[], transactions: TransactionExtended[]) {
 | 
			
		||||
  handleNewBlock(block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) {
 | 
			
		||||
    if (!this.wss) {
 | 
			
		||||
      throw new Error('WebSocket.Server is not set');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,7 @@ const configFile = require('../mempool-config.json');
 | 
			
		||||
interface IConfig {
 | 
			
		||||
  MEMPOOL: {
 | 
			
		||||
    NETWORK: 'mainnet' | 'testnet' | 'liquid';
 | 
			
		||||
    BACKEND: 'electrs' | 'bitcoind' | 'bitcoind-electrs';
 | 
			
		||||
    BACKEND: 'esplora' | 'electrum' | 'none';
 | 
			
		||||
    HTTP_PORT: number;
 | 
			
		||||
    SPAWN_CLUSTER_PROCS: number;
 | 
			
		||||
    API_URL_PREFIX: string;
 | 
			
		||||
@ -20,7 +20,7 @@ interface IConfig {
 | 
			
		||||
    PORT: number;
 | 
			
		||||
    USERNAME: string;
 | 
			
		||||
    PASSWORD: string;
 | 
			
		||||
  },
 | 
			
		||||
  };
 | 
			
		||||
  DATABASE: {
 | 
			
		||||
    ENABLED: boolean;
 | 
			
		||||
    HOST: string,
 | 
			
		||||
@ -53,7 +53,7 @@ interface IConfig {
 | 
			
		||||
const defaults: IConfig = {
 | 
			
		||||
  'MEMPOOL': {
 | 
			
		||||
    'NETWORK': 'mainnet',
 | 
			
		||||
    'BACKEND': 'electrs',
 | 
			
		||||
    'BACKEND': 'none',
 | 
			
		||||
    'HTTP_PORT': 8999,
 | 
			
		||||
    'SPAWN_CLUSTER_PROCS': 0,
 | 
			
		||||
    'API_URL_PREFIX': '/api/v1/',
 | 
			
		||||
@ -66,10 +66,10 @@ const defaults: IConfig = {
 | 
			
		||||
    'PORT': 3306
 | 
			
		||||
  },
 | 
			
		||||
  'BITCOIND': {
 | 
			
		||||
    'HOST': "127.0.0.1",
 | 
			
		||||
    'HOST': '127.0.0.1',
 | 
			
		||||
    'PORT': 8332,
 | 
			
		||||
    'USERNAME': "mempoo",
 | 
			
		||||
    'PASSWORD': "mempool"
 | 
			
		||||
    'USERNAME': 'mempool',
 | 
			
		||||
    'PASSWORD': 'mempool'
 | 
			
		||||
  },
 | 
			
		||||
  'DATABASE': {
 | 
			
		||||
    'ENABLED': true,
 | 
			
		||||
 | 
			
		||||
@ -209,7 +209,7 @@ class Server {
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (config.MEMPOOL.BACKEND === 'bitcoind' || config.MEMPOOL.BACKEND === 'bitcoind-electrs') {
 | 
			
		||||
    if (config.MEMPOOL.BACKEND !== 'esplora') {
 | 
			
		||||
      this.app
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId', routes.getTransaction)
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/outspends', routes.getTransactionOutspends)
 | 
			
		||||
@ -219,7 +219,7 @@ class Server {
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'block-height/:height', routes.getBlockHeight)
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'address/:address', routes.getAddress)
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'address/:address/txs', routes.getAddressTransactions)
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'address/:address/txs/chain/:txId', routes.getAdressTxChain)
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'address/:address/txs/chain/:txId', routes.getAddressTransactions)
 | 
			
		||||
        .get(config.MEMPOOL.API_URL_PREFIX + 'address-prefix/:prefix', routes.getAddressPrefix)
 | 
			
		||||
      ;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@ -1,320 +0,0 @@
 | 
			
		||||
export interface MempoolInfo {
 | 
			
		||||
  size: number;
 | 
			
		||||
  bytes: number;
 | 
			
		||||
  usage?: number;
 | 
			
		||||
  maxmempool?: number;
 | 
			
		||||
  mempoolminfee?: number;
 | 
			
		||||
  minrelaytxfee?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface MempoolBlock {
 | 
			
		||||
  blockSize: number;
 | 
			
		||||
  blockVSize: number;
 | 
			
		||||
  nTx: number;
 | 
			
		||||
  medianFee: number;
 | 
			
		||||
  totalFees: number;
 | 
			
		||||
  feeRange: number[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface MempoolBlockWithTransactions extends MempoolBlock {
 | 
			
		||||
  transactionIds: string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Transaction {
 | 
			
		||||
  txid: string;
 | 
			
		||||
  version: number;
 | 
			
		||||
  locktime: number;
 | 
			
		||||
  fee: number;
 | 
			
		||||
  size: number;
 | 
			
		||||
  weight: number;
 | 
			
		||||
  vin: Vin[];
 | 
			
		||||
  vout: Vout[];
 | 
			
		||||
  status: Status;
 | 
			
		||||
 | 
			
		||||
  // bitcoind (temp?)
 | 
			
		||||
  in_active_chain?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface TransactionMinerInfo {
 | 
			
		||||
  vin: VinStrippedToScriptsig[];
 | 
			
		||||
  vout: VoutStrippedToScriptPubkey[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface VinStrippedToScriptsig {
 | 
			
		||||
  scriptsig: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface VoutStrippedToScriptPubkey {
 | 
			
		||||
  scriptpubkey_address: string | undefined;
 | 
			
		||||
  value: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface TransactionExtended extends Transaction {
 | 
			
		||||
  vsize: number;
 | 
			
		||||
  feePerVsize: number;
 | 
			
		||||
  firstSeen: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface TransactionStripped {
 | 
			
		||||
  txid: string;
 | 
			
		||||
  fee: number;
 | 
			
		||||
  weight: number;
 | 
			
		||||
  value: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Vin {
 | 
			
		||||
  txid: string;
 | 
			
		||||
  vout: number;
 | 
			
		||||
  is_coinbase: boolean;
 | 
			
		||||
  scriptsig: string;
 | 
			
		||||
  scriptsig_asm: string;
 | 
			
		||||
  inner_redeemscript_asm?: string;
 | 
			
		||||
  inner_witnessscript_asm?: string;
 | 
			
		||||
  sequence: any;
 | 
			
		||||
  witness?: string[];
 | 
			
		||||
  prevout: Vout;
 | 
			
		||||
  // Elements
 | 
			
		||||
  is_pegin?: boolean;
 | 
			
		||||
  issuance?: Issuance;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface Issuance {
 | 
			
		||||
  asset_id: string;
 | 
			
		||||
  is_reissuance: string;
 | 
			
		||||
  asset_blinding_nonce: string;
 | 
			
		||||
  asset_entropy: string;
 | 
			
		||||
  contract_hash: string;
 | 
			
		||||
  assetamount?: number;
 | 
			
		||||
  assetamountcommitment?: string;
 | 
			
		||||
  tokenamount?: number;
 | 
			
		||||
  tokenamountcommitment?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Vout {
 | 
			
		||||
  scriptpubkey: string;
 | 
			
		||||
  scriptpubkey_asm: string;
 | 
			
		||||
  scriptpubkey_type: string;
 | 
			
		||||
  scriptpubkey_address: string;
 | 
			
		||||
  value: number;
 | 
			
		||||
  // Elements
 | 
			
		||||
  valuecommitment?: number;
 | 
			
		||||
  asset?: string;
 | 
			
		||||
  pegout?: Pegout;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface Pegout {
 | 
			
		||||
  genesis_hash: string;
 | 
			
		||||
  scriptpubkey: string;
 | 
			
		||||
  scriptpubkey_asm: string;
 | 
			
		||||
  scriptpubkey_address: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Status {
 | 
			
		||||
  confirmed: boolean;
 | 
			
		||||
  block_height?: number;
 | 
			
		||||
  block_hash?: string;
 | 
			
		||||
  block_time?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Block {
 | 
			
		||||
  id: string;
 | 
			
		||||
  height: number;
 | 
			
		||||
  version: number;
 | 
			
		||||
  timestamp: number;
 | 
			
		||||
  bits: number;
 | 
			
		||||
  nonce: number;
 | 
			
		||||
  difficulty: number;
 | 
			
		||||
  merkle_root: string;
 | 
			
		||||
  tx_count: number;
 | 
			
		||||
  size: number;
 | 
			
		||||
  weight: number;
 | 
			
		||||
  previousblockhash: string;
 | 
			
		||||
 | 
			
		||||
  // Custom properties
 | 
			
		||||
  medianFee?: number;
 | 
			
		||||
  feeRange?: number[];
 | 
			
		||||
  reward?: number;
 | 
			
		||||
  coinbaseTx?: TransactionMinerInfo;
 | 
			
		||||
  matchRate?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface RpcBlock {
 | 
			
		||||
  hash: string;
 | 
			
		||||
  confirmations: number;
 | 
			
		||||
  size: number;
 | 
			
		||||
  strippedsize: number;
 | 
			
		||||
  weight: number;
 | 
			
		||||
  height: number;
 | 
			
		||||
  version: number;
 | 
			
		||||
  versionHex: string;
 | 
			
		||||
  merkleroot: string;
 | 
			
		||||
  tx: Transaction[];
 | 
			
		||||
  time: number;
 | 
			
		||||
  mediantime: number;
 | 
			
		||||
  nonce: number;
 | 
			
		||||
  bits: number;
 | 
			
		||||
  difficulty: number;
 | 
			
		||||
  chainwork: string;
 | 
			
		||||
  nTx: number;
 | 
			
		||||
  previousblockhash: string;
 | 
			
		||||
  nextblockhash: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface MempoolEntries { [txId: string]: MempoolEntry; }
 | 
			
		||||
 | 
			
		||||
export interface MempoolEntry {
 | 
			
		||||
  fees: Fees;
 | 
			
		||||
  vsize: number;
 | 
			
		||||
  weight: number;
 | 
			
		||||
  fee: number;
 | 
			
		||||
  modifiedfee: number;
 | 
			
		||||
  time: number;
 | 
			
		||||
  height: number;
 | 
			
		||||
  descendantcount: number;
 | 
			
		||||
  descendantsize: number;
 | 
			
		||||
  descendantfees: number;
 | 
			
		||||
  ancestorcount: number;
 | 
			
		||||
  ancestorsize: number;
 | 
			
		||||
  ancestorfees: number;
 | 
			
		||||
  wtxid: string;
 | 
			
		||||
  depends: any[];
 | 
			
		||||
  spentby: any[];
 | 
			
		||||
  'bip125-replaceable': boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Fees {
 | 
			
		||||
  base: number;
 | 
			
		||||
  modified: number;
 | 
			
		||||
  ancestor: number;
 | 
			
		||||
  descendant: 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;
 | 
			
		||||
  tx_per_second: number;
 | 
			
		||||
  vbytes_per_second: number;
 | 
			
		||||
  total_fee: number;
 | 
			
		||||
  mempool_byte_weight: number;
 | 
			
		||||
  fee_data: string;
 | 
			
		||||
 | 
			
		||||
  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 OptimizedStatistic {
 | 
			
		||||
  id: number;
 | 
			
		||||
  added: string;
 | 
			
		||||
  unconfirmed_transactions: number;
 | 
			
		||||
  tx_per_second: number;
 | 
			
		||||
  vbytes_per_second: number;
 | 
			
		||||
  total_fee: number;
 | 
			
		||||
  mempool_byte_weight: number;
 | 
			
		||||
  vsizes: number[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface Outspend {
 | 
			
		||||
  spent: boolean;
 | 
			
		||||
  txid: string;
 | 
			
		||||
  vin: number;
 | 
			
		||||
  status: Status;
 | 
			
		||||
}
 | 
			
		||||
export interface WebsocketResponse {
 | 
			
		||||
  action: string;
 | 
			
		||||
  data: string[];
 | 
			
		||||
  'track-tx': string;
 | 
			
		||||
  'track-address': string;
 | 
			
		||||
  'watch-mempool': boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface VbytesPerSecond {
 | 
			
		||||
  unixTime: number;
 | 
			
		||||
  vSize: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface RequiredSpec { [name: string]: RequiredParams; }
 | 
			
		||||
 | 
			
		||||
interface RequiredParams {
 | 
			
		||||
  required: boolean;
 | 
			
		||||
  types: ('@string' | '@number' | '@boolean' | string)[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface AddressInformation {
 | 
			
		||||
  isvalid: boolean;
 | 
			
		||||
  address: string;
 | 
			
		||||
  scriptPubKey: string;
 | 
			
		||||
  isscript: boolean;
 | 
			
		||||
  iswitness: boolean;
 | 
			
		||||
  witness_version?: boolean;
 | 
			
		||||
  witness_program: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ScriptHashBalance {
 | 
			
		||||
  confirmed: number;
 | 
			
		||||
  unconfirmed: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ScriptHashHistory {
 | 
			
		||||
  height: number;
 | 
			
		||||
  tx_hash: string;
 | 
			
		||||
  fee?: number;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										138
									
								
								backend/src/mempool.interfaces.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								backend/src/mempool.interfaces.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,138 @@
 | 
			
		||||
import { IEsploraApi } from './api/bitcoin/esplora-api.interface';
 | 
			
		||||
 | 
			
		||||
export interface MempoolBlock {
 | 
			
		||||
  blockSize: number;
 | 
			
		||||
  blockVSize: number;
 | 
			
		||||
  nTx: number;
 | 
			
		||||
  medianFee: number;
 | 
			
		||||
  totalFees: number;
 | 
			
		||||
  feeRange: number[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface MempoolBlockWithTransactions extends MempoolBlock {
 | 
			
		||||
  transactionIds: string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface VinStrippedToScriptsig {
 | 
			
		||||
  scriptsig: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface VoutStrippedToScriptPubkey {
 | 
			
		||||
  scriptpubkey_address: string | undefined;
 | 
			
		||||
  value: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface TransactionExtended extends IEsploraApi.Transaction {
 | 
			
		||||
  vsize: number;
 | 
			
		||||
  feePerVsize: number;
 | 
			
		||||
  firstSeen?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface TransactionStripped {
 | 
			
		||||
  txid: string;
 | 
			
		||||
  fee: number;
 | 
			
		||||
  weight: number;
 | 
			
		||||
  value: number;
 | 
			
		||||
}
 | 
			
		||||
export interface BlockExtended extends IEsploraApi.Block {
 | 
			
		||||
  medianFee?: number;
 | 
			
		||||
  feeRange?: number[];
 | 
			
		||||
  reward?: number;
 | 
			
		||||
  coinbaseTx?: TransactionMinerInfo;
 | 
			
		||||
  matchRate?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface TransactionMinerInfo {
 | 
			
		||||
  vin: VinStrippedToScriptsig[];
 | 
			
		||||
  vout: VoutStrippedToScriptPubkey[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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;
 | 
			
		||||
  tx_per_second: number;
 | 
			
		||||
  vbytes_per_second: number;
 | 
			
		||||
  total_fee: number;
 | 
			
		||||
  mempool_byte_weight: number;
 | 
			
		||||
  fee_data: string;
 | 
			
		||||
 | 
			
		||||
  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 OptimizedStatistic {
 | 
			
		||||
  id: number;
 | 
			
		||||
  added: string;
 | 
			
		||||
  unconfirmed_transactions: number;
 | 
			
		||||
  tx_per_second: number;
 | 
			
		||||
  vbytes_per_second: number;
 | 
			
		||||
  total_fee: number;
 | 
			
		||||
  mempool_byte_weight: number;
 | 
			
		||||
  vsizes: number[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface WebsocketResponse {
 | 
			
		||||
  action: string;
 | 
			
		||||
  data: string[];
 | 
			
		||||
  'track-tx': string;
 | 
			
		||||
  'track-address': string;
 | 
			
		||||
  'watch-mempool': boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface VbytesPerSecond {
 | 
			
		||||
  unixTime: number;
 | 
			
		||||
  vSize: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface RequiredSpec { [name: string]: RequiredParams; }
 | 
			
		||||
 | 
			
		||||
interface RequiredParams {
 | 
			
		||||
  required: boolean;
 | 
			
		||||
  types: ('@string' | '@number' | '@boolean' | string)[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
import config from './config';
 | 
			
		||||
import { json, Request, Response } from 'express';
 | 
			
		||||
import { Request, Response } from 'express';
 | 
			
		||||
import statistics from './api/statistics';
 | 
			
		||||
import feeApi from './api/fee-api';
 | 
			
		||||
import backendInfo from './api/backend-info';
 | 
			
		||||
@ -8,7 +8,7 @@ import mempool from './api/mempool';
 | 
			
		||||
import bisq from './api/bisq/bisq';
 | 
			
		||||
import websocketHandler from './api/websocket-handler';
 | 
			
		||||
import bisqMarket from './api/bisq/markets-api';
 | 
			
		||||
import { OptimizedStatistic, RequiredSpec, Transaction, TransactionExtended } from './interfaces';
 | 
			
		||||
import { OptimizedStatistic, RequiredSpec, TransactionExtended } from './mempool.interfaces';
 | 
			
		||||
import { MarketsApiError } from './api/bisq/interfaces';
 | 
			
		||||
import donations from './api/donations';
 | 
			
		||||
import logger from './logger';
 | 
			
		||||
@ -533,10 +533,10 @@ class Routes {
 | 
			
		||||
      if (txInMempool) {
 | 
			
		||||
        transaction = txInMempool;
 | 
			
		||||
      } else {
 | 
			
		||||
        transaction = await transactionUtils.getTransactionExtended(req.params.txId);
 | 
			
		||||
        transaction = await transactionUtils.$getTransactionExtended(req.params.txId, false, true);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (transaction) {
 | 
			
		||||
        transaction = await transactionUtils.$addPrevoutsToTransaction(transaction);
 | 
			
		||||
        res.json(transaction);
 | 
			
		||||
      } else {
 | 
			
		||||
        res.status(500).send('Error fetching transaction.');
 | 
			
		||||
@ -560,7 +560,20 @@ class Routes {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async getBlockTransactions(req: Request, res: Response) {
 | 
			
		||||
    res.status(404).send('Not implemented');
 | 
			
		||||
    try {
 | 
			
		||||
      const txIds = await bitcoinApi.$getTxIdsForBlock(req.params.hash);
 | 
			
		||||
      const transactions: TransactionExtended[] = [];
 | 
			
		||||
 | 
			
		||||
      for (let i = 0; i < Math.min(15, txIds.length); i++) {
 | 
			
		||||
        const transaction = await transactionUtils.$getTransactionExtended(txIds[i], false, true);
 | 
			
		||||
        if (transaction) {
 | 
			
		||||
          transactions.push(transaction);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      res.json(transactions);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      res.status(500).send(e.message);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async getBlockHeight(req: Request, res: Response) {
 | 
			
		||||
@ -568,81 +581,27 @@ class Routes {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async getAddress(req: Request, res: Response) {
 | 
			
		||||
    if (config.MEMPOOL.BACKEND === 'bitcoind') {
 | 
			
		||||
    if (config.MEMPOOL.BACKEND === 'none') {
 | 
			
		||||
      res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      const addressInfo = await bitcoinApi.$validateAddress(req.params.address);
 | 
			
		||||
      if (!addressInfo || !addressInfo.isvalid) {
 | 
			
		||||
        res.json({
 | 
			
		||||
          'address': req.params.address,
 | 
			
		||||
          'chain_stats': {
 | 
			
		||||
            'funded_txo_count': 0,
 | 
			
		||||
            'funded_txo_sum': 0,
 | 
			
		||||
            'spent_txo_count': 0,
 | 
			
		||||
            'spent_txo_sum': 0,
 | 
			
		||||
            'tx_count': 0
 | 
			
		||||
          },
 | 
			
		||||
          'mempool_stats': {
 | 
			
		||||
            'funded_txo_count': 0,
 | 
			
		||||
            'funded_txo_sum': 0,
 | 
			
		||||
            'spent_txo_count': 0,
 | 
			
		||||
            'spent_txo_sum': 0,
 | 
			
		||||
            'tx_count': 0
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const balance = await bitcoinApi.$getScriptHashBalance(addressInfo.scriptPubKey);
 | 
			
		||||
      const history = await bitcoinApi.$getScriptHashHistory(addressInfo.scriptPubKey);
 | 
			
		||||
 | 
			
		||||
      const unconfirmed = history.filter((h) => h.fee).length;
 | 
			
		||||
 | 
			
		||||
      res.json({
 | 
			
		||||
        'address': addressInfo.address,
 | 
			
		||||
        'chain_stats': {
 | 
			
		||||
          'funded_txo_count': 0,
 | 
			
		||||
          'funded_txo_sum': balance.confirmed ? balance.confirmed : 0,
 | 
			
		||||
          'spent_txo_count': 0,
 | 
			
		||||
          'spent_txo_sum': balance.confirmed < 0 ? balance.confirmed : 0,
 | 
			
		||||
          'tx_count': history.length - unconfirmed,
 | 
			
		||||
        },
 | 
			
		||||
        'mempool_stats': {
 | 
			
		||||
          'funded_txo_count': 0,
 | 
			
		||||
          'funded_txo_sum': balance.unconfirmed > 0 ? balance.unconfirmed : 0,
 | 
			
		||||
          'spent_txo_count': 0,
 | 
			
		||||
          'spent_txo_sum': balance.unconfirmed < 0 ? -balance.unconfirmed : 0,
 | 
			
		||||
          'tx_count': unconfirmed,
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      const addressData = await bitcoinApi.$getAddress(req.params.address);
 | 
			
		||||
      res.json(addressData);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      res.status(500).send(e.message);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async getAddressTransactions(req: Request, res: Response) {
 | 
			
		||||
    if (config.MEMPOOL.BACKEND === 'bitcoind') {
 | 
			
		||||
    if (config.MEMPOOL.BACKEND === 'none') {
 | 
			
		||||
      res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      const addressInfo = await bitcoinApi.$validateAddress(req.params.address);
 | 
			
		||||
      if (!addressInfo || !addressInfo.isvalid) {
 | 
			
		||||
        res.json([]);
 | 
			
		||||
      }
 | 
			
		||||
      const history = await bitcoinApi.$getScriptHashHistory(addressInfo.scriptPubKey);
 | 
			
		||||
      const transactions: TransactionExtended[] = [];
 | 
			
		||||
      for (const h of history) {
 | 
			
		||||
        let tx = await transactionUtils.getTransactionExtended(h.tx_hash);
 | 
			
		||||
        if (tx) {
 | 
			
		||||
          tx = await transactionUtils.$addPrevoutsToTransaction(tx);
 | 
			
		||||
          transactions.push(tx);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      const transactions = await bitcoinApi.$getAddressTransactions(req.params.address, req.params.txId);
 | 
			
		||||
      res.json(transactions);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      res.status(500).send(e.message);
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user