Refactored transaction handling.

This commit is contained in:
softsimon 2020-12-21 23:08:34 +07:00
parent 5dbf6789a7
commit ecc0f316cc
No known key found for this signature in database
GPG key ID: 488D7DCFB5A430D7
12 changed files with 331 additions and 145 deletions

View file

@ -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>;
} }

View file

@ -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.');
} }
} }

View file

@ -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;

View file

@ -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.');
} }
} }

View file

@ -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)

View file

@ -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),
}; };
} }
} }

View file

@ -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!');
} }

View 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();

View file

@ -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)
;
}
} }
} }

View file

@ -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 {

View file

@ -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();

View file

@ -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": {