mirror of
https://github.com/mempool/mempool.git
synced 2025-03-03 17:47:01 +01:00
Refactored transaction handling.
This commit is contained in:
parent
5dbf6789a7
commit
ecc0f316cc
12 changed files with 331 additions and 145 deletions
|
@ -1,16 +1,17 @@
|
||||||
import { MempoolInfo, Transaction, Block, MempoolEntries, MempoolEntry } from '../../interfaces';
|
import { MempoolInfo, Transaction, Block, MempoolEntries, MempoolEntry, Address } from '../../interfaces';
|
||||||
|
|
||||||
export interface AbstractBitcoinApi {
|
export interface AbstractBitcoinApi {
|
||||||
getMempoolInfo(): Promise<MempoolInfo>;
|
$getMempoolInfo(): Promise<MempoolInfo>;
|
||||||
getRawMempool(): Promise<Transaction['txid'][]>;
|
$getRawMempool(): Promise<Transaction['txid'][]>;
|
||||||
getRawTransaction(txId: string): Promise<Transaction>;
|
$getRawTransaction(txId: string): Promise<Transaction>;
|
||||||
getBlockHeightTip(): Promise<number>;
|
$getBlockHeightTip(): Promise<number>;
|
||||||
getTxIdsForBlock(hash: string): Promise<string[]>;
|
$getTxIdsForBlock(hash: string): Promise<string[]>;
|
||||||
getBlockHash(height: number): Promise<string>;
|
$getBlockHash(height: number): Promise<string>;
|
||||||
getBlock(hash: string): Promise<Block>;
|
$getBlock(hash: string): Promise<Block>;
|
||||||
getMempoolEntry(txid: string): Promise<MempoolEntry>;
|
$getMempoolEntry(txid: string): Promise<MempoolEntry>;
|
||||||
|
$getAddress(address: string): Promise<Address>;
|
||||||
|
|
||||||
// Custom
|
// Custom
|
||||||
getRawMempoolVerbose(): Promise<MempoolEntries>;
|
$getRawMempoolVerbose(): Promise<MempoolEntries>;
|
||||||
getRawTransactionBitcond(txId: string): Promise<Transaction>;
|
$getRawTransactionBitcond(txId: string): Promise<Transaction>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import config from '../../config';
|
import config from '../../config';
|
||||||
import { Transaction, Block, MempoolInfo, RpcBlock, MempoolEntries, MempoolEntry } from '../../interfaces';
|
import { Transaction, Block, MempoolInfo, RpcBlock, MempoolEntries, MempoolEntry, Address } from '../../interfaces';
|
||||||
import * as bitcoin from '@mempool/bitcoin';
|
import * as bitcoin from '@mempool/bitcoin';
|
||||||
|
|
||||||
class BitcoindApi {
|
class BitcoindApi {
|
||||||
|
@ -15,23 +15,23 @@ class BitcoindApi {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getMempoolInfo(): Promise<MempoolInfo> {
|
$getMempoolInfo(): Promise<MempoolInfo> {
|
||||||
return this.bitcoindClient.getMempoolInfo();
|
return this.bitcoindClient.getMempoolInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
getRawMempool(): Promise<Transaction['txid'][]> {
|
$getRawMempool(): Promise<Transaction['txid'][]> {
|
||||||
return this.bitcoindClient.getRawMemPool();
|
return this.bitcoindClient.getRawMemPool();
|
||||||
}
|
}
|
||||||
|
|
||||||
getRawMempoolVerbose(): Promise<MempoolEntries> {
|
$getRawMempoolVerbose(): Promise<MempoolEntries> {
|
||||||
return this.bitcoindClient.getRawMemPool(true);
|
return this.bitcoindClient.getRawMemPool(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
getMempoolEntry(txid: string): Promise<MempoolEntry> {
|
$getMempoolEntry(txid: string): Promise<MempoolEntry> {
|
||||||
return this.bitcoindClient.getMempoolEntry(txid,);
|
return this.bitcoindClient.getMempoolEntry(txid);
|
||||||
}
|
}
|
||||||
|
|
||||||
getRawTransaction(txId: string): Promise<Transaction> {
|
$getRawTransaction(txId: string): Promise<Transaction> {
|
||||||
return this.bitcoindClient.getRawTransaction(txId, true)
|
return this.bitcoindClient.getRawTransaction(txId, true)
|
||||||
.then((transaction: Transaction) => {
|
.then((transaction: Transaction) => {
|
||||||
transaction.vout.forEach((vout) => vout.value = vout.value * 100000000);
|
transaction.vout.forEach((vout) => vout.value = vout.value * 100000000);
|
||||||
|
@ -39,23 +39,23 @@ class BitcoindApi {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getBlockHeightTip(): Promise<number> {
|
$getBlockHeightTip(): Promise<number> {
|
||||||
return this.bitcoindClient.getChainTips()
|
return this.bitcoindClient.getChainTips()
|
||||||
.then((result) => result[0].height);
|
.then((result) => result[0].height);
|
||||||
}
|
}
|
||||||
|
|
||||||
getTxIdsForBlock(hash: string): Promise<string[]> {
|
$getTxIdsForBlock(hash: string): Promise<string[]> {
|
||||||
return this.bitcoindClient.getBlock(hash, 1)
|
return this.bitcoindClient.getBlock(hash, 1)
|
||||||
.then((rpcBlock: RpcBlock) => {
|
.then((rpcBlock: RpcBlock) => {
|
||||||
return rpcBlock.tx;
|
return rpcBlock.tx;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getBlockHash(height: number): Promise<string> {
|
$getBlockHash(height: number): Promise<string> {
|
||||||
return this.bitcoindClient.getBlockHash(height)
|
return this.bitcoindClient.getBlockHash(height);
|
||||||
}
|
}
|
||||||
|
|
||||||
getBlock(hash: string): Promise<Block> {
|
$getBlock(hash: string): Promise<Block> {
|
||||||
return this.bitcoindClient.getBlock(hash)
|
return this.bitcoindClient.getBlock(hash)
|
||||||
.then((rpcBlock: RpcBlock) => {
|
.then((rpcBlock: RpcBlock) => {
|
||||||
return {
|
return {
|
||||||
|
@ -75,7 +75,11 @@ class BitcoindApi {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getRawTransactionBitcond(txId: string): Promise<Transaction> {
|
$getRawTransactionBitcond(txId: string): Promise<Transaction> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$getAddress(address: string): Promise<Address> {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import config from '../../config';
|
import config from '../../config';
|
||||||
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
|
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
|
||||||
import { Transaction, Block, MempoolInfo, RpcBlock, MempoolEntries, MempoolEntry } from '../../interfaces';
|
import { Transaction, Block, MempoolInfo, RpcBlock, MempoolEntries, MempoolEntry, Address } from '../../interfaces';
|
||||||
import * as bitcoin from '@mempool/bitcoin';
|
import * as bitcoin from '@mempool/bitcoin';
|
||||||
import * as ElectrumClient from '@codewarriorr/electrum-client-js';
|
import * as ElectrumClient from '@codewarriorr/electrum-client-js';
|
||||||
import logger from '../../logger';
|
import logger from '../../logger';
|
||||||
|
import transactionUtils from '../transaction-utils';
|
||||||
|
|
||||||
class BitcoindElectrsApi implements AbstractBitcoinApi {
|
class BitcoindElectrsApi implements AbstractBitcoinApi {
|
||||||
bitcoindClient: any;
|
bitcoindClient: any;
|
||||||
|
@ -27,64 +28,64 @@ class BitcoindElectrsApi implements AbstractBitcoinApi {
|
||||||
this.electrumClient.connect(
|
this.electrumClient.connect(
|
||||||
'electrum-client-js',
|
'electrum-client-js',
|
||||||
'1.4'
|
'1.4'
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getMempoolInfo(): Promise<MempoolInfo> {
|
$getMempoolInfo(): Promise<MempoolInfo> {
|
||||||
return this.bitcoindClient.getMempoolInfo();
|
return this.bitcoindClient.getMempoolInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
getRawMempool(): Promise<Transaction['txid'][]> {
|
$getRawMempool(): Promise<Transaction['txid'][]> {
|
||||||
return this.bitcoindClient.getRawMemPool();
|
return this.bitcoindClient.getRawMemPool();
|
||||||
}
|
}
|
||||||
|
|
||||||
getRawMempoolVerbose(): Promise<MempoolEntries> {
|
$getRawMempoolVerbose(): Promise<MempoolEntries> {
|
||||||
return this.bitcoindClient.getRawMemPool(true);
|
return this.bitcoindClient.getRawMemPool(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
getMempoolEntry(txid: string): Promise<MempoolEntry> {
|
$getMempoolEntry(txid: string): Promise<MempoolEntry> {
|
||||||
return this.bitcoindClient.getMempoolEntry(txid,);
|
return this.bitcoindClient.getMempoolEntry(txid);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRawTransaction(txId: string): Promise<Transaction> {
|
async $getRawTransaction(txId: string): Promise<Transaction> {
|
||||||
try {
|
try {
|
||||||
const transaction: Transaction = await this.electrumClient.blockchain_transaction_get(txId, true);
|
const transaction: Transaction = await this.electrumClient.blockchain_transaction_get(txId, true);
|
||||||
if (!transaction) {
|
if (!transaction) {
|
||||||
throw new Error('not found');
|
throw new Error(txId + ' not found!');
|
||||||
}
|
}
|
||||||
transaction.vout.forEach((vout) => vout.value = vout.value * 100000000);
|
transactionUtils.bitcoindToElectrsTransaction(transaction);
|
||||||
return transaction;
|
return transaction;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.debug('getRawTransaction error: ' + (e.message || e));
|
logger.debug('getRawTransaction error: ' + (e.message || e));
|
||||||
throw new Error(e);
|
throw new Error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getRawTransactionBitcond(txId: string): Promise<Transaction> {
|
$getRawTransactionBitcond(txId: string): Promise<Transaction> {
|
||||||
return this.bitcoindClient.getRawTransaction(txId, true)
|
return this.bitcoindClient.getRawTransaction(txId, true)
|
||||||
.then((transaction: Transaction) => {
|
.then((transaction: Transaction) => {
|
||||||
transaction.vout.forEach((vout) => vout.value = vout.value * 100000000);
|
transactionUtils.bitcoindToElectrsTransaction(transaction);
|
||||||
return transaction;
|
return transaction;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getBlockHeightTip(): Promise<number> {
|
$getBlockHeightTip(): Promise<number> {
|
||||||
return this.bitcoindClient.getChainTips()
|
return this.bitcoindClient.getChainTips()
|
||||||
.then((result) => result[0].height);
|
.then((result) => result[0].height);
|
||||||
}
|
}
|
||||||
|
|
||||||
getTxIdsForBlock(hash: string): Promise<string[]> {
|
$getTxIdsForBlock(hash: string): Promise<string[]> {
|
||||||
return this.bitcoindClient.getBlock(hash, 1)
|
return this.bitcoindClient.getBlock(hash, 1)
|
||||||
.then((rpcBlock: RpcBlock) => {
|
.then((rpcBlock: RpcBlock) => {
|
||||||
return rpcBlock.tx;
|
return rpcBlock.tx;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getBlockHash(height: number): Promise<string> {
|
$getBlockHash(height: number): Promise<string> {
|
||||||
return this.bitcoindClient.getBlockHash(height)
|
return this.bitcoindClient.getBlockHash(height);
|
||||||
}
|
}
|
||||||
|
|
||||||
getBlock(hash: string): Promise<Block> {
|
$getBlock(hash: string): Promise<Block> {
|
||||||
return this.bitcoindClient.getBlock(hash)
|
return this.bitcoindClient.getBlock(hash)
|
||||||
.then((rpcBlock: RpcBlock) => {
|
.then((rpcBlock: RpcBlock) => {
|
||||||
return {
|
return {
|
||||||
|
@ -103,6 +104,19 @@ class BitcoindElectrsApi implements AbstractBitcoinApi {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default BitcoindElectrsApi;
|
export default BitcoindElectrsApi;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import config from '../../config';
|
import config from '../../config';
|
||||||
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
|
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
|
||||||
import { Transaction, Block, MempoolInfo, MempoolEntry, MempoolEntries } from '../../interfaces';
|
import { Transaction, Block, MempoolInfo, MempoolEntry, MempoolEntries, Address } from '../../interfaces';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
class ElectrsApi implements AbstractBitcoinApi {
|
class ElectrsApi implements AbstractBitcoinApi {
|
||||||
|
@ -8,7 +8,7 @@ class ElectrsApi implements AbstractBitcoinApi {
|
||||||
constructor() {
|
constructor() {
|
||||||
}
|
}
|
||||||
|
|
||||||
getMempoolInfo(): Promise<MempoolInfo> {
|
$getMempoolInfo(): Promise<MempoolInfo> {
|
||||||
return axios.get<any>(config.ELECTRS.REST_API_URL + '/mempool', { timeout: 10000 })
|
return axios.get<any>(config.ELECTRS.REST_API_URL + '/mempool', { timeout: 10000 })
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
return {
|
return {
|
||||||
|
@ -18,45 +18,49 @@ class ElectrsApi implements AbstractBitcoinApi {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getRawMempool(): Promise<Transaction['txid'][]> {
|
$getRawMempool(): Promise<Transaction['txid'][]> {
|
||||||
return axios.get<Transaction['txid'][]>(config.ELECTRS.REST_API_URL + '/mempool/txids')
|
return axios.get<Transaction['txid'][]>(config.ELECTRS.REST_API_URL + '/mempool/txids')
|
||||||
.then((response) => response.data);
|
.then((response) => response.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
getRawTransaction(txId: string): Promise<Transaction> {
|
$getRawTransaction(txId: string): Promise<Transaction> {
|
||||||
return axios.get<Transaction>(config.ELECTRS.REST_API_URL + '/tx/' + txId)
|
return axios.get<Transaction>(config.ELECTRS.REST_API_URL + '/tx/' + txId)
|
||||||
.then((response) => response.data);
|
.then((response) => response.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
getBlockHeightTip(): Promise<number> {
|
$getBlockHeightTip(): Promise<number> {
|
||||||
return axios.get<number>(config.ELECTRS.REST_API_URL + '/blocks/tip/height')
|
return axios.get<number>(config.ELECTRS.REST_API_URL + '/blocks/tip/height')
|
||||||
.then((response) => response.data);
|
.then((response) => response.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
getTxIdsForBlock(hash: string): Promise<string[]> {
|
$getTxIdsForBlock(hash: string): Promise<string[]> {
|
||||||
return axios.get<string[]>(config.ELECTRS.REST_API_URL + '/block/' + hash + '/txids')
|
return axios.get<string[]>(config.ELECTRS.REST_API_URL + '/block/' + hash + '/txids')
|
||||||
.then((response) => response.data);
|
.then((response) => response.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
getBlockHash(height: number): Promise<string> {
|
$getBlockHash(height: number): Promise<string> {
|
||||||
return axios.get<string>(config.ELECTRS.REST_API_URL + '/block-height/' + height)
|
return axios.get<string>(config.ELECTRS.REST_API_URL + '/block-height/' + height)
|
||||||
.then((response) => response.data);
|
.then((response) => response.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
getBlock(hash: string): Promise<Block> {
|
$getBlock(hash: string): Promise<Block> {
|
||||||
return axios.get<Block>(config.ELECTRS.REST_API_URL + '/block/' + hash)
|
return axios.get<Block>(config.ELECTRS.REST_API_URL + '/block/' + hash)
|
||||||
.then((response) => response.data);
|
.then((response) => response.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
getRawMempoolVerbose(): Promise<MempoolEntries> {
|
$getRawMempoolVerbose(): Promise<MempoolEntries> {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
|
|
||||||
getMempoolEntry(): Promise<MempoolEntry> {
|
$getMempoolEntry(): Promise<MempoolEntry> {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
|
|
||||||
getRawTransactionBitcond(txId: string): Promise<Transaction> {
|
$getRawTransactionBitcond(txId: string): Promise<Transaction> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$getAddress(address: string): Promise<Address> {
|
||||||
throw new Error('Method not implemented.');
|
throw new Error('Method not implemented.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,10 @@ import config from '../config';
|
||||||
import bitcoinApi from './bitcoin/bitcoin-api-factory';
|
import bitcoinApi from './bitcoin/bitcoin-api-factory';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
import memPool from './mempool';
|
import memPool from './mempool';
|
||||||
import { Block, Transaction, TransactionExtended, TransactionMinerInfo } from '../interfaces';
|
import { Block, TransactionExtended, TransactionMinerInfo } from '../interfaces';
|
||||||
import { Common } from './common';
|
import { Common } from './common';
|
||||||
import diskCache from './disk-cache';
|
import diskCache from './disk-cache';
|
||||||
|
import transactionUtils from './transaction-utils';
|
||||||
|
|
||||||
class Blocks {
|
class Blocks {
|
||||||
private static KEEP_BLOCK_AMOUNT = 8;
|
private static KEEP_BLOCK_AMOUNT = 8;
|
||||||
|
@ -28,7 +29,7 @@ class Blocks {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $updateBlocks() {
|
public async $updateBlocks() {
|
||||||
const blockHeightTip = await bitcoinApi.getBlockHeightTip();
|
const blockHeightTip = await bitcoinApi.$getBlockHeightTip();
|
||||||
|
|
||||||
if (this.blocks.length === 0) {
|
if (this.blocks.length === 0) {
|
||||||
this.currentBlockHeight = blockHeightTip - Blocks.KEEP_BLOCK_AMOUNT;
|
this.currentBlockHeight = blockHeightTip - Blocks.KEEP_BLOCK_AMOUNT;
|
||||||
|
@ -43,8 +44,8 @@ class Blocks {
|
||||||
|
|
||||||
if (!this.lastDifficultyAdjustmentTime) {
|
if (!this.lastDifficultyAdjustmentTime) {
|
||||||
const heightDiff = blockHeightTip % 2016;
|
const heightDiff = blockHeightTip % 2016;
|
||||||
const blockHash = await bitcoinApi.getBlockHash(blockHeightTip - heightDiff);
|
const blockHash = await bitcoinApi.$getBlockHash(blockHeightTip - heightDiff);
|
||||||
const block = await bitcoinApi.getBlock(blockHash);
|
const block = await bitcoinApi.$getBlock(blockHash);
|
||||||
this.lastDifficultyAdjustmentTime = block.timestamp;
|
this.lastDifficultyAdjustmentTime = block.timestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,11 +57,11 @@ class Blocks {
|
||||||
logger.debug(`New block found (#${this.currentBlockHeight})!`);
|
logger.debug(`New block found (#${this.currentBlockHeight})!`);
|
||||||
}
|
}
|
||||||
|
|
||||||
let transactions: TransactionExtended[] = [];
|
const transactions: TransactionExtended[] = [];
|
||||||
|
|
||||||
const blockHash = await bitcoinApi.getBlockHash(this.currentBlockHeight);
|
const blockHash = await bitcoinApi.$getBlockHash(this.currentBlockHeight);
|
||||||
const block = await bitcoinApi.getBlock(blockHash);
|
const block = await bitcoinApi.$getBlock(blockHash);
|
||||||
let txIds: string[] = await bitcoinApi.getTxIdsForBlock(blockHash);
|
const txIds: string[] = await bitcoinApi.$getTxIdsForBlock(blockHash);
|
||||||
|
|
||||||
const mempool = memPool.getMempool();
|
const mempool = memPool.getMempool();
|
||||||
let found = 0;
|
let found = 0;
|
||||||
|
@ -70,19 +71,13 @@ class Blocks {
|
||||||
transactions.push(mempool[txIds[i]]);
|
transactions.push(mempool[txIds[i]]);
|
||||||
found++;
|
found++;
|
||||||
} else {
|
} else {
|
||||||
if (config.MEMPOOL.BACKEND === 'electrs') {
|
// When using bitcoind, just skip parsing past block tx's for now except for coinbase
|
||||||
|
if (config.MEMPOOL.BACKEND === 'electrs' || i === 0) { //
|
||||||
logger.debug(`Fetching block tx ${i} of ${txIds.length}`);
|
logger.debug(`Fetching block tx ${i} of ${txIds.length}`);
|
||||||
const tx = await memPool.getTransactionExtended(txIds[i]);
|
const tx = await transactionUtils.getTransactionExtended(txIds[i]);
|
||||||
if (tx) {
|
if (tx) {
|
||||||
transactions.push(tx);
|
transactions.push(tx);
|
||||||
}
|
}
|
||||||
} else { // When using bitcoind, just skip parsing past block tx's for now
|
|
||||||
if (i === 0) {
|
|
||||||
const tx = await memPool.getTransactionExtended(txIds[i], true);
|
|
||||||
if (tx) {
|
|
||||||
transactions.push(tx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -115,6 +110,10 @@ class Blocks {
|
||||||
return this.lastDifficultyAdjustmentTime;
|
return this.lastDifficultyAdjustmentTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getCurrentBlockHeight(): number {
|
||||||
|
return this.currentBlockHeight;
|
||||||
|
}
|
||||||
|
|
||||||
private stripCoinbaseTransaction(tx: TransactionExtended): TransactionMinerInfo {
|
private stripCoinbaseTransaction(tx: TransactionExtended): TransactionMinerInfo {
|
||||||
return {
|
return {
|
||||||
vin: [{
|
vin: [{
|
||||||
|
@ -122,7 +121,7 @@ class Blocks {
|
||||||
}],
|
}],
|
||||||
vout: tx.vout
|
vout: tx.vout
|
||||||
.map((vout) => ({
|
.map((vout) => ({
|
||||||
scriptpubkey_address: vout.scriptpubkey_address || (vout['scriptPubKey']['addresses'] && vout['scriptPubKey']['addresses'][0]) || null,
|
scriptpubkey_address: vout.scriptpubkey_address,
|
||||||
value: vout.value
|
value: vout.value
|
||||||
}))
|
}))
|
||||||
.filter((vout) => vout.value)
|
.filter((vout) => vout.value)
|
||||||
|
|
|
@ -53,7 +53,7 @@ export class Common {
|
||||||
txid: tx.txid,
|
txid: tx.txid,
|
||||||
fee: tx.fee,
|
fee: tx.fee,
|
||||||
weight: tx.weight,
|
weight: tx.weight,
|
||||||
value: tx.vout ? tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0) : 0,
|
value: tx.vout.reduce((acc, vout) => acc + (vout.value ? vout.value : 0), 0),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ import bitcoinApi from './bitcoin/bitcoin-api-factory';
|
||||||
import { MempoolInfo, TransactionExtended, Transaction, VbytesPerSecond, MempoolEntry, MempoolEntries } from '../interfaces';
|
import { MempoolInfo, TransactionExtended, Transaction, VbytesPerSecond, MempoolEntry, MempoolEntries } from '../interfaces';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
import { Common } from './common';
|
import { Common } from './common';
|
||||||
|
import transactionUtils from './transaction-utils';
|
||||||
|
|
||||||
class Mempool {
|
class Mempool {
|
||||||
private inSync: boolean = false;
|
private inSync: boolean = false;
|
||||||
|
@ -18,7 +19,6 @@ class Mempool {
|
||||||
private vBytesPerSecond: number = 0;
|
private vBytesPerSecond: number = 0;
|
||||||
private mempoolProtection = 0;
|
private mempoolProtection = 0;
|
||||||
private latestTransactions: any[] = [];
|
private latestTransactions: any[] = [];
|
||||||
private mempoolEntriesCache: MempoolEntries | null = null;
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
setInterval(this.updateTxPerSecond.bind(this), 1000);
|
setInterval(this.updateTxPerSecond.bind(this), 1000);
|
||||||
|
@ -49,7 +49,7 @@ class Mempool {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $updateMemPoolInfo() {
|
public async $updateMemPoolInfo() {
|
||||||
this.mempoolInfo = await bitcoinApi.getMempoolInfo();
|
this.mempoolInfo = await bitcoinApi.$getMempoolInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
public getMempoolInfo(): MempoolInfo | undefined {
|
public getMempoolInfo(): MempoolInfo | undefined {
|
||||||
|
@ -76,60 +76,19 @@ class Mempool {
|
||||||
return txTimes;
|
return txTimes;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getTransactionExtended(txId: string, isCoinbase = false): Promise<TransactionExtended | false> {
|
|
||||||
try {
|
|
||||||
let transaction: Transaction;
|
|
||||||
if (!isCoinbase && config.MEMPOOL.BACKEND === 'bitcoind-electrs') {
|
|
||||||
transaction = await bitcoinApi.getRawTransactionBitcond(txId);
|
|
||||||
} else {
|
|
||||||
transaction = await bitcoinApi.getRawTransaction(txId);
|
|
||||||
}
|
|
||||||
if (config.MEMPOOL.BACKEND !== 'electrs' && !isCoinbase) {
|
|
||||||
transaction = await this.$appendFeeData(transaction);
|
|
||||||
}
|
|
||||||
return this.extendTransaction(transaction);
|
|
||||||
} catch (e) {
|
|
||||||
logger.debug(txId + ' not found');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async $appendFeeData(transaction: Transaction): Promise<Transaction> {
|
|
||||||
let mempoolEntry: MempoolEntry;
|
|
||||||
if (!this.inSync && !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;
|
|
||||||
}
|
|
||||||
|
|
||||||
private extendTransaction(transaction: Transaction | MempoolEntry): TransactionExtended {
|
|
||||||
// @ts-ignore
|
|
||||||
return Object.assign({
|
|
||||||
vsize: Math.round(transaction.weight / 4),
|
|
||||||
feePerVsize: Math.max(1, (transaction.fee || 0) / (transaction.weight / 4)),
|
|
||||||
firstSeen: Math.round((new Date().getTime() / 1000)),
|
|
||||||
}, transaction);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async $updateMempool() {
|
public async $updateMempool() {
|
||||||
logger.debug('Updating mempool');
|
logger.debug('Updating mempool');
|
||||||
const start = new Date().getTime();
|
const start = new Date().getTime();
|
||||||
let hasChange: boolean = false;
|
let hasChange: boolean = false;
|
||||||
const currentMempoolSize = Object.keys(this.mempoolCache).length;
|
const currentMempoolSize = Object.keys(this.mempoolCache).length;
|
||||||
let txCount = 0;
|
let txCount = 0;
|
||||||
const transactions = await bitcoinApi.getRawMempool();
|
const transactions = await bitcoinApi.$getRawMempool();
|
||||||
const diff = transactions.length - currentMempoolSize;
|
const diff = transactions.length - currentMempoolSize;
|
||||||
const newTransactions: TransactionExtended[] = [];
|
const newTransactions: TransactionExtended[] = [];
|
||||||
|
|
||||||
for (const txid of transactions) {
|
for (const txid of transactions) {
|
||||||
if (!this.mempoolCache[txid]) {
|
if (!this.mempoolCache[txid]) {
|
||||||
const transaction = await this.getTransactionExtended(txid);
|
const transaction = await transactionUtils.getTransactionExtended(txid, false, true);
|
||||||
if (transaction) {
|
if (transaction) {
|
||||||
this.mempoolCache[txid] = transaction;
|
this.mempoolCache[txid] = transaction;
|
||||||
txCount++;
|
txCount++;
|
||||||
|
@ -197,7 +156,6 @@ class Mempool {
|
||||||
|
|
||||||
if (!this.inSync && transactions.length === Object.keys(newMempool).length) {
|
if (!this.inSync && transactions.length === Object.keys(newMempool).length) {
|
||||||
this.inSync = true;
|
this.inSync = true;
|
||||||
this.mempoolEntriesCache = null;
|
|
||||||
logger.info('The mempool is now in sync!');
|
logger.info('The mempool is now in sync!');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
124
backend/src/api/transaction-utils.ts
Normal file
124
backend/src/api/transaction-utils.ts
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
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';
|
||||||
|
|
||||||
|
class TransactionUtils {
|
||||||
|
private mempoolEntriesCache: MempoolEntries | null = null;
|
||||||
|
|
||||||
|
constructor() { }
|
||||||
|
|
||||||
|
public async $addPrevoutsToTransaction(transaction: TransactionExtended): Promise<TransactionExtended> {
|
||||||
|
for (const vin of transaction.vin) {
|
||||||
|
const innerTx = await bitcoinApi.$getRawTransaction(vin.txid);
|
||||||
|
vin.prevout = innerTx.vout[vin.vout];
|
||||||
|
}
|
||||||
|
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 | MempoolEntry): TransactionExtended {
|
||||||
|
// @ts-ignore
|
||||||
|
return Object.assign({
|
||||||
|
vsize: Math.round(transaction.weight / 4),
|
||||||
|
feePerVsize: Math.max(1, (transaction.fee || 0) / (transaction.weight / 4)),
|
||||||
|
firstSeen: Math.round((new Date().getTime() / 1000)),
|
||||||
|
}, transaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
public stripCoinbaseTransaction(tx: TransactionExtended): TransactionMinerInfo {
|
||||||
|
return {
|
||||||
|
vin: [{
|
||||||
|
scriptsig: tx.vin[0].scriptsig || tx.vin[0]['coinbase']
|
||||||
|
}],
|
||||||
|
vout: tx.vout
|
||||||
|
.map((vout) => ({
|
||||||
|
scriptpubkey_address: vout.scriptpubkey_address,
|
||||||
|
value: vout.value
|
||||||
|
}))
|
||||||
|
.filter((vout) => vout.value)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getTransactionExtended(txId: string, isCoinbase = false, inMempool = false): Promise<TransactionExtended | null> {
|
||||||
|
try {
|
||||||
|
let transaction: Transaction;
|
||||||
|
if (inMempool) {
|
||||||
|
transaction = await bitcoinApi.$getRawTransactionBitcond(txId);
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.extendTransaction(transaction);
|
||||||
|
} catch (e) {
|
||||||
|
logger.debug('getTransactionExtended error: ' + (e.message || e));
|
||||||
|
console.log(e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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: 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 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();
|
|
@ -208,6 +208,21 @@ class Server {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config.MEMPOOL.BACKEND === 'bitcoind' || config.MEMPOOL.BACKEND === 'bitcoind-electrs') {
|
||||||
|
this.app
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId', routes.getTransaction)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/outspends', routes.getTransactionOutspends)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash', routes.getBlock)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'blocks', routes.getBlocks)
|
||||||
|
.get(config.MEMPOOL.API_URL_PREFIX + 'block/:hash/txs/:index', routes.getBlockTransactions)
|
||||||
|
.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-prefix/:prefix', routes.getAddressPrefix)
|
||||||
|
;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -142,7 +142,7 @@ export interface RpcBlock {
|
||||||
strippedsize: number;
|
strippedsize: number;
|
||||||
weight: number;
|
weight: number;
|
||||||
height: number;
|
height: number;
|
||||||
version: number,
|
version: number;
|
||||||
versionHex: string;
|
versionHex: string;
|
||||||
merkleroot: string;
|
merkleroot: string;
|
||||||
tx: Transaction[];
|
tx: Transaction[];
|
||||||
|
@ -152,38 +152,38 @@ export interface RpcBlock {
|
||||||
bits: number;
|
bits: number;
|
||||||
difficulty: number;
|
difficulty: number;
|
||||||
chainwork: string;
|
chainwork: string;
|
||||||
nTx: number,
|
nTx: number;
|
||||||
previousblockhash: string;
|
previousblockhash: string;
|
||||||
nextblockhash: string;
|
nextblockhash: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MempoolEntries { [txId: string]: MempoolEntry };
|
export interface MempoolEntries { [txId: string]: MempoolEntry; }
|
||||||
|
|
||||||
export interface MempoolEntry {
|
export interface MempoolEntry {
|
||||||
fees: Fees
|
fees: Fees;
|
||||||
vsize: number
|
vsize: number;
|
||||||
weight: number
|
weight: number;
|
||||||
fee: number
|
fee: number;
|
||||||
modifiedfee: number
|
modifiedfee: number;
|
||||||
time: number
|
time: number;
|
||||||
height: number
|
height: number;
|
||||||
descendantcount: number
|
descendantcount: number;
|
||||||
descendantsize: number
|
descendantsize: number;
|
||||||
descendantfees: number
|
descendantfees: number;
|
||||||
ancestorcount: number
|
ancestorcount: number;
|
||||||
ancestorsize: number
|
ancestorsize: number;
|
||||||
ancestorfees: number
|
ancestorfees: number;
|
||||||
wtxid: string
|
wtxid: string;
|
||||||
depends: any[]
|
depends: any[];
|
||||||
spentby: any[]
|
spentby: any[];
|
||||||
'bip125-replaceable': boolean
|
'bip125-replaceable': boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Fees {
|
export interface Fees {
|
||||||
base: number
|
base: number;
|
||||||
modified: number
|
modified: number;
|
||||||
ancestor: number
|
ancestor: number;
|
||||||
descendant: number
|
descendant: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Address {
|
export interface Address {
|
||||||
|
|
|
@ -8,10 +8,12 @@ import mempool from './api/mempool';
|
||||||
import bisq from './api/bisq/bisq';
|
import bisq from './api/bisq/bisq';
|
||||||
import websocketHandler from './api/websocket-handler';
|
import websocketHandler from './api/websocket-handler';
|
||||||
import bisqMarket from './api/bisq/markets-api';
|
import bisqMarket from './api/bisq/markets-api';
|
||||||
import { OptimizedStatistic, RequiredSpec } from './interfaces';
|
import { OptimizedStatistic, RequiredSpec, Transaction, TransactionExtended } from './interfaces';
|
||||||
import { MarketsApiError } from './api/bisq/interfaces';
|
import { MarketsApiError } from './api/bisq/interfaces';
|
||||||
import donations from './api/donations';
|
import donations from './api/donations';
|
||||||
import logger from './logger';
|
import logger from './logger';
|
||||||
|
import bitcoinApi from './api/bitcoin/bitcoin-api-factory';
|
||||||
|
import transactionUtils from './api/transaction-utils';
|
||||||
|
|
||||||
class Routes {
|
class Routes {
|
||||||
private cache: { [date: string]: OptimizedStatistic[] } = {
|
private cache: { [date: string]: OptimizedStatistic[] } = {
|
||||||
|
@ -524,6 +526,71 @@ class Routes {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getTransaction(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
let transaction: TransactionExtended | null;
|
||||||
|
const txInMempool = mempool.getMempool()[req.params.txId];
|
||||||
|
if (txInMempool) {
|
||||||
|
transaction = txInMempool;
|
||||||
|
} else {
|
||||||
|
transaction = await transactionUtils.getTransactionExtended(req.params.txId);
|
||||||
|
}
|
||||||
|
if (transaction) {
|
||||||
|
transaction = await transactionUtils.$addPrevoutsToTransaction(transaction);
|
||||||
|
res.json(transaction);
|
||||||
|
} else {
|
||||||
|
res.status(500).send('Error fetching transaction.');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).send(e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getBlock(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const result = await bitcoinApi.$getBlock(req.params.hash);
|
||||||
|
res.json(result);
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).send(e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getBlocks(req: Request, res: Response) {
|
||||||
|
res.status(404).send('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getBlockTransactions(req: Request, res: Response) {
|
||||||
|
res.status(404).send('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getBlockHeight(req: Request, res: Response) {
|
||||||
|
res.status(404).send('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getAddress(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const result = await bitcoinApi.$getAddress(req.params.hash);
|
||||||
|
res.json(result);
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).send(e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getAddressTransactions(req: Request, res: Response) {
|
||||||
|
res.status(404).send('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getAdressTxChain(req: Request, res: Response) {
|
||||||
|
res.status(404).send('Not implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getAddressPrefix(req: Request, res: Response) {
|
||||||
|
res.json([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getTransactionOutspends(req: Request, res: Response) {
|
||||||
|
res.json([]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new Routes();
|
export default new Routes();
|
||||||
|
|
|
@ -9,10 +9,10 @@
|
||||||
"ws": true
|
"ws": true
|
||||||
},
|
},
|
||||||
"/api/": {
|
"/api/": {
|
||||||
"target": "http://localhost:50001/",
|
"target": "http://localhost:8999/",
|
||||||
"secure": false,
|
"secure": false,
|
||||||
"pathRewrite": {
|
"pathRewrite": {
|
||||||
"^/api/": ""
|
"^/api/": "/api/v1/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/testnet/api/v1": {
|
"/testnet/api/v1": {
|
||||||
|
|
Loading…
Add table
Reference in a new issue