Merge branch 'esplora'

* esplora:
  Adding optional Blockstream esplora backend support.
This commit is contained in:
Simon Lindh 2019-10-22 17:28:56 +08:00
commit f5e74c844b
12 changed files with 1361 additions and 33 deletions

View File

@ -19,5 +19,7 @@
"BITCOIN_NODE_PORT": 8332, "BITCOIN_NODE_PORT": 8332,
"BITCOIN_NODE_USER": "", "BITCOIN_NODE_USER": "",
"BITCOIN_NODE_PASS": "", "BITCOIN_NODE_PASS": "",
"BACKEND_API": "bitcoind",
"ESPLORA_API_URL": "https://www.blockstream.info/api",
"TX_PER_SECOND_SPAN_SECONDS": 150 "TX_PER_SECOND_SPAN_SECONDS": 150
} }

View File

@ -9,6 +9,7 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"axios": "^0.19.0",
"bitcoin": "^3.0.1", "bitcoin": "^3.0.1",
"compression": "^1.7.3", "compression": "^1.7.3",
"express": "^4.16.3", "express": "^4.16.3",

View File

@ -0,0 +1,10 @@
import { IMempoolInfo, ITransaction, IBlock } from '../../interfaces';
export interface AbstractBitcoinApi {
getMempoolInfo(): Promise<IMempoolInfo>;
getRawMempool(): Promise<ITransaction['txid'][]>;
getRawTransaction(txId: string): Promise<ITransaction>;
getBlockCount(): Promise<number>;
getBlock(hash: string): Promise<IBlock>;
getBlockHash(height: number): Promise<string>;
}

View File

@ -0,0 +1,16 @@
const config = require('../../../mempool-config.json');
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
import BitcoindApi from './bitcoind-api';
import EsploraApi from './esplora-api';
function factory(): AbstractBitcoinApi {
switch (config.BACKEND_API) {
case 'esplora':
return new EsploraApi();
case 'bitcoind':
default:
return new BitcoindApi();
}
}
export default factory();

View File

@ -1,8 +1,9 @@
const config = require('../../mempool-config.json'); const config = require('../../../mempool-config.json');
import * as bitcoin from 'bitcoin'; import * as bitcoin from 'bitcoin';
import { ITransaction, IMempoolInfo, IBlock } from '../interfaces'; import { ITransaction, IMempoolInfo, IBlock } from '../../interfaces';
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
class BitcoinApi { class BitcoindApi implements AbstractBitcoinApi {
client: any; client: any;
constructor() { constructor() {
@ -81,4 +82,4 @@ class BitcoinApi {
} }
} }
export default new BitcoinApi(); export default BitcoindApi;

View File

@ -0,0 +1,99 @@
const config = require('../../../mempool-config.json');
import { ITransaction, IMempoolInfo, IBlock } from '../../interfaces';
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
import axios, { AxiosResponse } from 'axios';
class EsploraApi implements AbstractBitcoinApi {
client: any;
constructor() {
this.client = axios.create({
baseURL: config.ESPLORA_API_URL,
timeout: 15000,
});
}
getMempoolInfo(): Promise<IMempoolInfo> {
return new Promise(async (resolve, reject) => {
try {
const response: AxiosResponse = await this.client.get('/mempool');
resolve({
size: response.data.count,
bytes: response.data.vsize,
});
} catch (error) {
reject(error);
}
});
}
getRawMempool(): Promise<ITransaction['txid'][]> {
return new Promise(async (resolve, reject) => {
try {
const response: AxiosResponse = await this.client.get('/mempool/txids');
resolve(response.data);
} catch (error) {
reject(error);
}
});
}
getRawTransaction(txId: string): Promise<ITransaction> {
return new Promise(async (resolve, reject) => {
try {
const response: AxiosResponse = await this.client.get('/tx/' + txId);
response.data.vsize = response.data.size;
response.data.size = response.data.weight;
response.data.fee = response.data.fee / 100000000;
resolve(response.data);
} catch (error) {
reject(error);
}
});
}
getBlockCount(): Promise<number> {
return new Promise(async (resolve, reject) => {
try {
const response: AxiosResponse = await this.client.get('/blocks/tip/height');
resolve(response.data);
} catch (error) {
reject(error);
}
});
}
getBlock(hash: string): Promise<IBlock> {
return new Promise(async (resolve, reject) => {
try {
const blockInfo: AxiosResponse = await this.client.get('/block/' + hash);
const blockTxs: AxiosResponse = await this.client.get('/block/' + hash + '/txids');
const block = blockInfo.data;
block.hash = hash;
block.nTx = block.tx_count;
block.time = block.timestamp;
block.tx = blockTxs.data;
resolve(block);
} catch (error) {
reject(error);
}
});
}
getBlockHash(height: number): Promise<string> {
return new Promise(async (resolve, reject) => {
try {
const response: AxiosResponse = await this.client.get('/block-height/' + height);
resolve(response.data);
} catch (error) {
reject(error);
}
});
}
}
export default EsploraApi;

View File

@ -1,5 +1,5 @@
const config = require('../../mempool-config.json'); const config = require('../../mempool-config.json');
import bitcoinApi from './bitcoin-api-wrapper'; import bitcoinApi from './bitcoin/bitcoin-api-factory';
import { DB } from '../database'; import { DB } from '../database';
import { IBlock, ITransaction } from '../interfaces'; import { IBlock, ITransaction } from '../interfaces';
import memPool from './mempool'; import memPool from './mempool';
@ -56,7 +56,7 @@ class Blocks {
block = storedBlock; block = storedBlock;
} else { } else {
const blockHash = await bitcoinApi.getBlockHash(this.currentBlockHeight); const blockHash = await bitcoinApi.getBlockHash(this.currentBlockHeight);
block = await bitcoinApi.getBlock(blockHash, 1); block = await bitcoinApi.getBlock(blockHash);
const coinbase = await memPool.getRawTransaction(block.tx[0], true); const coinbase = await memPool.getRawTransaction(block.tx[0], true);
if (coinbase && coinbase.totalOut) { if (coinbase && coinbase.totalOut) {
@ -74,6 +74,7 @@ class Blocks {
transactions.push(mempool[block.tx[i]]); transactions.push(mempool[block.tx[i]]);
found++; found++;
} else { } else {
console.log(`Fetching block tx ${i} of ${block.tx.length}`);
const tx = await memPool.getRawTransaction(block.tx[i]); const tx = await memPool.getRawTransaction(block.tx[i]);
if (tx) { if (tx) {
transactions.push(tx); transactions.push(tx);

View File

@ -1,5 +1,5 @@
const config = require('../../mempool-config.json'); const config = require('../../mempool-config.json');
import bitcoinApi from './bitcoin-api-wrapper'; import bitcoinApi from './bitcoin/bitcoin-api-factory';
import { ITransaction, IMempoolInfo, IMempool } from '../interfaces'; import { ITransaction, IMempoolInfo, IMempool } from '../interfaces';
class Mempool { class Mempool {
@ -52,32 +52,40 @@ class Mempool {
public async getRawTransaction(txId: string, isCoinbase = false): Promise<ITransaction | false> { public async getRawTransaction(txId: string, isCoinbase = false): Promise<ITransaction | false> {
try { try {
const transaction = await bitcoinApi.getRawTransaction(txId); const transaction = await bitcoinApi.getRawTransaction(txId);
let totalIn = 0;
if (!isCoinbase) {
for (let i = 0; i < transaction.vin.length; i++) {
try {
const result = await bitcoinApi.getRawTransaction(transaction.vin[i].txid);
transaction.vin[i]['value'] = result.vout[transaction.vin[i].vout].value;
totalIn += result.vout[transaction.vin[i].vout].value;
} catch (err) {
console.log('Locating historical tx error');
}
}
}
let totalOut = 0; let totalOut = 0;
transaction.vout.forEach((output) => totalOut += output.value); transaction.vout.forEach((output) => totalOut += output.value);
if (totalIn > totalOut) { if (config.BACKEND_API === 'esplora') {
transaction.fee = parseFloat((totalIn - totalOut).toFixed(8));
transaction.feePerWeightUnit = (transaction.fee * 100000000) / (transaction.vsize * 4) || 0; transaction.feePerWeightUnit = (transaction.fee * 100000000) / (transaction.vsize * 4) || 0;
transaction.feePerVsize = (transaction.fee * 100000000) / (transaction.vsize) || 0; transaction.feePerVsize = (transaction.fee * 100000000) / (transaction.vsize) || 0;
} else if (!isCoinbase) { transaction.totalOut = totalOut / 100000000;
transaction.fee = 0; } else {
transaction.feePerVsize = 0; let totalIn = 0;
transaction.feePerWeightUnit = 0; if (!isCoinbase) {
console.log('Minus fee error!'); for (let i = 0; i < transaction.vin.length; i++) {
try {
const result = await bitcoinApi.getRawTransaction(transaction.vin[i].txid);
transaction.vin[i]['value'] = result.vout[transaction.vin[i].vout].value;
totalIn += result.vout[transaction.vin[i].vout].value;
} catch (err) {
console.log('Locating historical tx error');
}
}
}
if (totalIn > totalOut) {
transaction.fee = parseFloat((totalIn - totalOut).toFixed(8));
transaction.feePerWeightUnit = (transaction.fee * 100000000) / (transaction.vsize * 4) || 0;
transaction.feePerVsize = (transaction.fee * 100000000) / (transaction.vsize) || 0;
} else if (!isCoinbase) {
transaction.fee = 0;
transaction.feePerVsize = 0;
transaction.feePerWeightUnit = 0;
console.log('Minus fee error!');
}
transaction.totalOut = totalOut;
} }
transaction.totalOut = totalOut;
return transaction; return transaction;
} catch (e) { } catch (e) {
console.log(txId + ' not found'); console.log(txId + ' not found');

View File

@ -6,7 +6,7 @@ import * as http from 'http';
import * as https from 'https'; import * as https from 'https';
import * as WebSocket from 'ws'; import * as WebSocket from 'ws';
import bitcoinApi from './api/bitcoin-api-wrapper'; import bitcoinApi from './api/bitcoin/bitcoin-api-factory';
import diskCache from './api/disk-cache'; import diskCache from './api/disk-cache';
import memPool from './api/mempool'; import memPool from './api/mempool';
import blocks from './api/blocks'; import blocks from './api/blocks';
@ -55,7 +55,7 @@ class MempoolSpace {
port: 8999 port: 8999
}; };
this.server.listen(opts, () => { this.server.listen(opts, () => {
console.log(`Server started on ${opts.host}:${opts.port})`); console.log(`Server started on ${opts.host}:${opts.port}`);
}); });
} }

View File

@ -1,10 +1,10 @@
export interface IMempoolInfo { export interface IMempoolInfo {
size: number; size: number;
bytes: number; bytes: number;
usage: number; usage?: number;
maxmempool: number; maxmempool?: number;
mempoolminfee: number; mempoolminfee?: number;
minrelaytxfee: number; minrelaytxfee?: number;
} }
export interface ITransaction { export interface ITransaction {

1187
backend/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@ -22,6 +22,9 @@ export class BlockchainBlocksComponent implements OnInit, OnDestroy {
ngOnInit() { ngOnInit() {
this.blocksSubscription = this.memPoolService.blocks$ this.blocksSubscription = this.memPoolService.blocks$
.subscribe((block) => { .subscribe((block) => {
if (this.blocks.some((b) => b.height === block.height)) {
return;
}
this.blocks.unshift(block); this.blocks.unshift(block);
this.blocks = this.blocks.slice(0, 8); this.blocks = this.blocks.slice(0, 8);
}); });