mirror of
https://github.com/mempool/mempool.git
synced 2024-11-19 09:52:14 +01:00
Big refactor of multiple backends handling.
This commit is contained in:
parent
8d0db12abe
commit
bb28a56622
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"MEMPOOL": {
|
"MEMPOOL": {
|
||||||
"NETWORK": "mainnet",
|
"NETWORK": "mainnet",
|
||||||
"BACKEND": "electrs",
|
"BACKEND": "electrum",
|
||||||
"HTTP_PORT": 8999,
|
"HTTP_PORT": 8999,
|
||||||
"SPAWN_CLUSTER_PROCS": 0,
|
"SPAWN_CLUSTER_PROCS": 0,
|
||||||
"API_URL_PREFIX": "/api/v1/",
|
"API_URL_PREFIX": "/api/v1/",
|
||||||
|
@ -3,7 +3,7 @@ import * as fs from 'fs';
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { BisqBlocks, BisqBlock, BisqTransaction, BisqStats, BisqTrade } from './interfaces';
|
import { BisqBlocks, BisqBlock, BisqTransaction, BisqStats, BisqTrade } from './interfaces';
|
||||||
import { Common } from '../common';
|
import { Common } from '../common';
|
||||||
import { Block } from '../../interfaces';
|
import { BlockExtended } from '../../mempool.interfaces';
|
||||||
import { StaticPool } from 'node-worker-threads-pool';
|
import { StaticPool } from 'node-worker-threads-pool';
|
||||||
import logger from '../../logger';
|
import logger from '../../logger';
|
||||||
|
|
||||||
@ -42,7 +42,7 @@ class Bisq {
|
|||||||
this.startSubDirectoryWatcher();
|
this.startSubDirectoryWatcher();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleNewBitcoinBlock(block: Block): void {
|
handleNewBitcoinBlock(block: BlockExtended): void {
|
||||||
if (block.height - 2 > this.latestBlockHeight && this.latestBlockHeight !== 0) {
|
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...`);
|
logger.warn(`Bitcoin block height (#${block.height}) has diverged from the latest Bisq block height (#${this.latestBlockHeight}). Restarting watchers...`);
|
||||||
this.startTopDirectoryWatcher();
|
this.startTopDirectoryWatcher();
|
||||||
|
@ -1,21 +1,13 @@
|
|||||||
import { MempoolInfo, Transaction, Block, MempoolEntries, MempoolEntry, Address, AddressInformation,
|
import { IEsploraApi } from './esplora-api.interface';
|
||||||
ScriptHashBalance, ScriptHashHistory } from '../../interfaces';
|
|
||||||
|
|
||||||
export interface AbstractBitcoinApi {
|
export interface AbstractBitcoinApi {
|
||||||
$getMempoolInfo(): Promise<MempoolInfo>;
|
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]>;
|
||||||
$getRawMempool(): Promise<Transaction['txid'][]>;
|
$getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean): Promise<IEsploraApi.Transaction>;
|
||||||
$getRawTransaction(txId: string): Promise<Transaction>;
|
$getRawTransactionBitcoind(txId: string, skipConversion?: boolean, addPrevout?: boolean): Promise<IEsploraApi.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<IEsploraApi.Block>;
|
||||||
$getMempoolEntry(txid: string): Promise<MempoolEntry>;
|
$getAddress(address: string): Promise<IEsploraApi.Address>;
|
||||||
$getAddress(address: string): Promise<Address>;
|
$getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
|
||||||
$validateAddress(address: string): Promise<AddressInformation>;
|
|
||||||
$getScriptHashBalance(scriptHash: string): Promise<ScriptHashBalance>;
|
|
||||||
$getScriptHashHistory(scriptHash: string): Promise<ScriptHashHistory[]>;
|
|
||||||
|
|
||||||
// Custom
|
|
||||||
$getRawMempoolVerbose(): Promise<MempoolEntries>;
|
|
||||||
$getRawTransactionBitcond(txId: string): Promise<Transaction>;
|
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
import config from '../../config';
|
import config from '../../config';
|
||||||
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
|
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
|
||||||
import BitcoindElectrsApi from './bitcoind-electrs-api';
|
import EsploraApi from './esplora-api';
|
||||||
import BitcoindApi from './bitcoind-api';
|
import BitcoinApi from './bitcoin-api';
|
||||||
import ElectrsApi from './electrs-api';
|
import ElectrumApi from './electrum-api';
|
||||||
|
|
||||||
function bitcoinApiFactory(): AbstractBitcoinApi {
|
function bitcoinApiFactory(): AbstractBitcoinApi {
|
||||||
switch (config.MEMPOOL.BACKEND) {
|
switch (config.MEMPOOL.BACKEND) {
|
||||||
case 'electrs':
|
case 'esplora':
|
||||||
return new ElectrsApi();
|
return new EsploraApi();
|
||||||
case 'bitcoind-electrs':
|
case 'electrum':
|
||||||
return new BitcoindElectrsApi();
|
return new ElectrumApi();
|
||||||
case 'bitcoind':
|
case 'none':
|
||||||
default:
|
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 bitcoinApi from './bitcoin/bitcoin-api-factory';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
import memPool from './mempool';
|
import memPool from './mempool';
|
||||||
import { Block, TransactionExtended, TransactionMinerInfo } from '../interfaces';
|
import { BlockExtended, TransactionExtended, TransactionMinerInfo } from '../mempool.interfaces';
|
||||||
import { Common } from './common';
|
import { Common } from './common';
|
||||||
import diskCache from './disk-cache';
|
import diskCache from './disk-cache';
|
||||||
import transactionUtils from './transaction-utils';
|
import transactionUtils from './transaction-utils';
|
||||||
|
|
||||||
class Blocks {
|
class Blocks {
|
||||||
private static KEEP_BLOCK_AMOUNT = 8;
|
private static KEEP_BLOCK_AMOUNT = 8;
|
||||||
private blocks: Block[] = [];
|
private blocks: BlockExtended[] = [];
|
||||||
private currentBlockHeight = 0;
|
private currentBlockHeight = 0;
|
||||||
private lastDifficultyAdjustmentTime = 0;
|
private lastDifficultyAdjustmentTime = 0;
|
||||||
private newBlockCallbacks: ((block: Block, txIds: string[], transactions: TransactionExtended[]) => void)[] = [];
|
private newBlockCallbacks: ((block: BlockExtended, txIds: string[], transactions: TransactionExtended[]) => void)[] = [];
|
||||||
|
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
public getBlocks(): Block[] {
|
public getBlocks(): BlockExtended[] {
|
||||||
return this.blocks;
|
return this.blocks;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setBlocks(blocks: Block[]) {
|
public setBlocks(blocks: BlockExtended[]) {
|
||||||
this.blocks = blocks;
|
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);
|
this.newBlockCallbacks.push(fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,9 +68,8 @@ class Blocks {
|
|||||||
|
|
||||||
for (let i = 0; i < txIds.length; i++) {
|
for (let i = 0; i < txIds.length; i++) {
|
||||||
// When using bitcoind, just fetch the coinbase tx for now
|
// When using bitcoind, just fetch the coinbase tx for now
|
||||||
if ((config.MEMPOOL.BACKEND === 'bitcoind' ||
|
if (config.MEMPOOL.BACKEND !== 'none' && i === 0) {
|
||||||
config.MEMPOOL.BACKEND === 'bitcoind-electrs') && i === 0) {
|
const tx = await transactionUtils.$getTransactionExtended(txIds[i]);
|
||||||
const tx = await transactionUtils.getTransactionExtended(txIds[i], true);
|
|
||||||
if (tx) {
|
if (tx) {
|
||||||
transactions.push(tx);
|
transactions.push(tx);
|
||||||
}
|
}
|
||||||
@ -78,9 +77,9 @@ class Blocks {
|
|||||||
if (mempool[txIds[i]]) {
|
if (mempool[txIds[i]]) {
|
||||||
transactions.push(mempool[txIds[i]]);
|
transactions.push(mempool[txIds[i]]);
|
||||||
found++;
|
found++;
|
||||||
} else if (config.MEMPOOL.BACKEND === 'electrs') {
|
} else if (config.MEMPOOL.BACKEND === 'esplora') {
|
||||||
logger.debug(`Fetching block tx ${i} of ${txIds.length}`);
|
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) {
|
if (tx) {
|
||||||
transactions.push(tx);
|
transactions.push(tx);
|
||||||
}
|
}
|
||||||
@ -89,23 +88,24 @@ class Blocks {
|
|||||||
|
|
||||||
logger.debug(`${found} of ${txIds.length} found in mempool. ${txIds.length - found} not found.`);
|
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);
|
const blockExtended: BlockExtended = Object.assign({}, block);
|
||||||
block.coinbaseTx = this.stripCoinbaseTransaction(transactions[0]);
|
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);
|
transactions.sort((a, b) => b.feePerVsize - a.feePerVsize);
|
||||||
block.medianFee = transactions.length > 1 ? Common.median(transactions.map((tx) => tx.feePerVsize)) : 0;
|
blockExtended.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.feeRange = transactions.length > 1 ? Common.getFeesInRange(transactions.slice(0, transactions.length - 1), 8) : [0, 0];
|
||||||
|
|
||||||
if (block.height % 2016 === 0) {
|
if (block.height % 2016 === 0) {
|
||||||
this.lastDifficultyAdjustmentTime = block.timestamp;
|
this.lastDifficultyAdjustmentTime = block.timestamp;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.blocks.push(block);
|
this.blocks.push(blockExtended);
|
||||||
if (this.blocks.length > Blocks.KEEP_BLOCK_AMOUNT) {
|
if (this.blocks.length > Blocks.KEEP_BLOCK_AMOUNT) {
|
||||||
this.blocks = this.blocks.slice(-Blocks.KEEP_BLOCK_AMOUNT);
|
this.blocks = this.blocks.slice(-Blocks.KEEP_BLOCK_AMOUNT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.newBlockCallbacks.length) {
|
if (this.newBlockCallbacks.length) {
|
||||||
this.newBlockCallbacks.forEach((cb) => cb(block, txIds, transactions));
|
this.newBlockCallbacks.forEach((cb) => cb(blockExtended, txIds, transactions));
|
||||||
}
|
}
|
||||||
diskCache.$saveCacheToDisk();
|
diskCache.$saveCacheToDisk();
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { TransactionExtended, TransactionStripped } from '../interfaces';
|
import { TransactionExtended, TransactionStripped } from '../mempool.interfaces';
|
||||||
|
|
||||||
export class Common {
|
export class Common {
|
||||||
static median(numbers: number[]) {
|
static median(numbers: number[]) {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import config from '../config';
|
import config from '../config';
|
||||||
import { MempoolBlock } from '../interfaces';
|
import { MempoolBlock } from '../mempool.interfaces';
|
||||||
import projectedBlocks from './mempool-blocks';
|
import projectedBlocks from './mempool-blocks';
|
||||||
|
|
||||||
class FeeApi {
|
class FeeApi {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { MempoolBlock, TransactionExtended, MempoolBlockWithTransactions } from '../interfaces';
|
import { MempoolBlock, TransactionExtended, MempoolBlockWithTransactions } from '../mempool.interfaces';
|
||||||
import { Common } from './common';
|
import { Common } from './common';
|
||||||
|
|
||||||
class MempoolBlocks {
|
class MempoolBlocks {
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
import config from '../config';
|
import config from '../config';
|
||||||
import bitcoinApi from './bitcoin/bitcoin-api-factory';
|
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 logger from '../logger';
|
||||||
import { Common } from './common';
|
import { Common } from './common';
|
||||||
import transactionUtils from './transaction-utils';
|
import transactionUtils from './transaction-utils';
|
||||||
|
import { IBitcoinApi } from './bitcoin/bitcoin-api.interface';
|
||||||
|
import bitcoinBaseApi from './bitcoin/bitcoin-base.api';
|
||||||
|
|
||||||
class Mempool {
|
class Mempool {
|
||||||
private inSync: boolean = false;
|
private inSync: boolean = false;
|
||||||
private mempoolCache: { [txId: string]: TransactionExtended } = {};
|
private mempoolCache: { [txId: string]: TransactionExtended } = {};
|
||||||
private mempoolInfo: MempoolInfo = { size: 0, bytes: 0 };
|
private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0,
|
||||||
private mempoolChangedCallback: ((newMempool: { [txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[],
|
maxmempool: 0, mempoolminfee: 0, minrelaytxfee: 0 };
|
||||||
|
private mempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[],
|
||||||
deletedTransactions: TransactionExtended[]) => void) | undefined;
|
deletedTransactions: TransactionExtended[]) => void) | undefined;
|
||||||
|
|
||||||
private txPerSecondArray: number[] = [];
|
private txPerSecondArray: number[] = [];
|
||||||
@ -49,10 +52,10 @@ class Mempool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async $updateMemPoolInfo() {
|
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;
|
return this.mempoolInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,8 +70,9 @@ class Mempool {
|
|||||||
public getFirstSeenForTransactions(txIds: string[]): number[] {
|
public getFirstSeenForTransactions(txIds: string[]): number[] {
|
||||||
const txTimes: number[] = [];
|
const txTimes: number[] = [];
|
||||||
txIds.forEach((txId: string) => {
|
txIds.forEach((txId: string) => {
|
||||||
if (this.mempoolCache[txId]) {
|
const tx = this.mempoolCache[txId];
|
||||||
txTimes.push(this.mempoolCache[txId].firstSeen);
|
if (tx && tx.firstSeen) {
|
||||||
|
txTimes.push(tx.firstSeen);
|
||||||
} else {
|
} else {
|
||||||
txTimes.push(0);
|
txTimes.push(0);
|
||||||
}
|
}
|
||||||
@ -88,7 +92,7 @@ class Mempool {
|
|||||||
|
|
||||||
for (const txid of transactions) {
|
for (const txid of transactions) {
|
||||||
if (!this.mempoolCache[txid]) {
|
if (!this.mempoolCache[txid]) {
|
||||||
const transaction = await transactionUtils.getTransactionExtended(txid, false, true);
|
const transaction = await transactionUtils.$getTransactionExtended(txid, true);
|
||||||
if (transaction) {
|
if (transaction) {
|
||||||
this.mempoolCache[txid] = transaction;
|
this.mempoolCache[txid] = transaction;
|
||||||
txCount++;
|
txCount++;
|
||||||
@ -118,6 +122,7 @@ class Mempool {
|
|||||||
|
|
||||||
// Prevent mempool from clear on bitcoind restart by delaying the deletion
|
// Prevent mempool from clear on bitcoind restart by delaying the deletion
|
||||||
if (this.mempoolProtection === 0
|
if (this.mempoolProtection === 0
|
||||||
|
&& config.MEMPOOL.BACKEND === 'esplora'
|
||||||
&& currentMempoolSize > 20000
|
&& currentMempoolSize > 20000
|
||||||
&& transactions.length / currentMempoolSize <= 0.80
|
&& transactions.length / currentMempoolSize <= 0.80
|
||||||
) {
|
) {
|
||||||
|
@ -2,7 +2,7 @@ import memPool from './mempool';
|
|||||||
import { DB } from '../database';
|
import { DB } from '../database';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
|
|
||||||
import { Statistic, TransactionExtended, OptimizedStatistic } from '../interfaces';
|
import { Statistic, TransactionExtended, OptimizedStatistic } from '../mempool.interfaces';
|
||||||
|
|
||||||
class Statistics {
|
class Statistics {
|
||||||
protected intervalTimer: NodeJS.Timer | undefined;
|
protected intervalTimer: NodeJS.Timer | undefined;
|
||||||
|
@ -1,16 +1,15 @@
|
|||||||
import bitcoinApi from './bitcoin/bitcoin-api-factory';
|
import bitcoinApi from './bitcoin/bitcoin-api-factory';
|
||||||
import { MempoolEntries, MempoolEntry, Transaction, TransactionExtended, TransactionMinerInfo } from '../interfaces';
|
|
||||||
import config from '../config';
|
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
import mempool from './mempool';
|
import { TransactionExtended, TransactionMinerInfo } from '../mempool.interfaces';
|
||||||
import blocks from './blocks';
|
import { IEsploraApi } from './bitcoin/esplora-api.interface';
|
||||||
|
|
||||||
class TransactionUtils {
|
class TransactionUtils {
|
||||||
private mempoolEntriesCache: MempoolEntries | null = null;
|
|
||||||
|
|
||||||
constructor() { }
|
constructor() { }
|
||||||
|
|
||||||
public async $addPrevoutsToTransaction(transaction: TransactionExtended): Promise<TransactionExtended> {
|
public async $addPrevoutsToTransaction(transaction: TransactionExtended): Promise<TransactionExtended> {
|
||||||
|
if (transaction.vin[0].is_coinbase) {
|
||||||
|
return transaction;
|
||||||
|
}
|
||||||
for (const vin of transaction.vin) {
|
for (const vin of transaction.vin) {
|
||||||
const innerTx = await bitcoinApi.$getRawTransaction(vin.txid);
|
const innerTx = await bitcoinApi.$getRawTransaction(vin.txid);
|
||||||
vin.prevout = innerTx.vout[vin.vout];
|
vin.prevout = innerTx.vout[vin.vout];
|
||||||
@ -18,26 +17,10 @@ class TransactionUtils {
|
|||||||
return transaction;
|
return transaction;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async $calculateFeeFromInputs(transaction: Transaction): Promise<TransactionExtended> {
|
public extendTransaction(transaction: IEsploraApi.Transaction): 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 {
|
|
||||||
transaction['vsize'] = Math.round(transaction.weight / 4);
|
transaction['vsize'] = Math.round(transaction.weight / 4);
|
||||||
transaction['feePerVsize'] = Math.max(1, (transaction.fee || 0) / (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));
|
transaction['firstSeen'] = Math.round((new Date().getTime() / 1000));
|
||||||
}
|
}
|
||||||
// @ts-ignore
|
// @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 {
|
try {
|
||||||
let transaction: Transaction;
|
let transaction: IEsploraApi.Transaction;
|
||||||
if (inMempool) {
|
if (inMempool) {
|
||||||
transaction = await bitcoinApi.$getRawTransactionBitcond(txId);
|
transaction = await bitcoinApi.$getRawTransactionBitcoind(txId, false, addPrevouts);
|
||||||
} else {
|
} else {
|
||||||
transaction = await bitcoinApi.$getRawTransaction(txId);
|
transaction = await bitcoinApi.$getRawTransaction(txId, false, addPrevouts);
|
||||||
}
|
|
||||||
if (config.MEMPOOL.BACKEND !== 'electrs' && !isCoinbase) {
|
|
||||||
if (inMempool) {
|
|
||||||
transaction = await this.$appendFeeData(transaction);
|
|
||||||
} else {
|
|
||||||
transaction = await this.$calculateFeeFromInputs(transaction);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return this.extendTransaction(transaction);
|
return this.extendTransaction(transaction);
|
||||||
} catch (e) {
|
} 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();
|
export default new TransactionUtils();
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
import * as WebSocket from 'ws';
|
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 blocks from './blocks';
|
||||||
import memPool from './mempool';
|
import memPool from './mempool';
|
||||||
import backendInfo from './backend-info';
|
import backendInfo from './backend-info';
|
||||||
@ -117,7 +117,7 @@ class WebsocketHandler {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getInitData(_blocks?: Block[]) {
|
getInitData(_blocks?: BlockExtended[]) {
|
||||||
if (!_blocks) {
|
if (!_blocks) {
|
||||||
_blocks = blocks.getBlocks();
|
_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) {
|
if (!this.wss) {
|
||||||
throw new Error('WebSocket.Server is not set');
|
throw new Error('WebSocket.Server is not set');
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ const configFile = require('../mempool-config.json');
|
|||||||
interface IConfig {
|
interface IConfig {
|
||||||
MEMPOOL: {
|
MEMPOOL: {
|
||||||
NETWORK: 'mainnet' | 'testnet' | 'liquid';
|
NETWORK: 'mainnet' | 'testnet' | 'liquid';
|
||||||
BACKEND: 'electrs' | 'bitcoind' | 'bitcoind-electrs';
|
BACKEND: 'esplora' | 'electrum' | 'none';
|
||||||
HTTP_PORT: number;
|
HTTP_PORT: number;
|
||||||
SPAWN_CLUSTER_PROCS: number;
|
SPAWN_CLUSTER_PROCS: number;
|
||||||
API_URL_PREFIX: string;
|
API_URL_PREFIX: string;
|
||||||
@ -20,7 +20,7 @@ interface IConfig {
|
|||||||
PORT: number;
|
PORT: number;
|
||||||
USERNAME: string;
|
USERNAME: string;
|
||||||
PASSWORD: string;
|
PASSWORD: string;
|
||||||
},
|
};
|
||||||
DATABASE: {
|
DATABASE: {
|
||||||
ENABLED: boolean;
|
ENABLED: boolean;
|
||||||
HOST: string,
|
HOST: string,
|
||||||
@ -53,7 +53,7 @@ interface IConfig {
|
|||||||
const defaults: IConfig = {
|
const defaults: IConfig = {
|
||||||
'MEMPOOL': {
|
'MEMPOOL': {
|
||||||
'NETWORK': 'mainnet',
|
'NETWORK': 'mainnet',
|
||||||
'BACKEND': 'electrs',
|
'BACKEND': 'none',
|
||||||
'HTTP_PORT': 8999,
|
'HTTP_PORT': 8999,
|
||||||
'SPAWN_CLUSTER_PROCS': 0,
|
'SPAWN_CLUSTER_PROCS': 0,
|
||||||
'API_URL_PREFIX': '/api/v1/',
|
'API_URL_PREFIX': '/api/v1/',
|
||||||
@ -66,10 +66,10 @@ const defaults: IConfig = {
|
|||||||
'PORT': 3306
|
'PORT': 3306
|
||||||
},
|
},
|
||||||
'BITCOIND': {
|
'BITCOIND': {
|
||||||
'HOST': "127.0.0.1",
|
'HOST': '127.0.0.1',
|
||||||
'PORT': 8332,
|
'PORT': 8332,
|
||||||
'USERNAME': "mempoo",
|
'USERNAME': 'mempool',
|
||||||
'PASSWORD': "mempool"
|
'PASSWORD': 'mempool'
|
||||||
},
|
},
|
||||||
'DATABASE': {
|
'DATABASE': {
|
||||||
'ENABLED': true,
|
'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
|
this.app
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId', routes.getTransaction)
|
.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 + '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 + 'block-height/:height', routes.getBlockHeight)
|
||||||
.get(config.MEMPOOL.API_URL_PREFIX + 'address/:address', routes.getAddress)
|
.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', 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)
|
.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 config from './config';
|
||||||
import { json, Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import statistics from './api/statistics';
|
import statistics from './api/statistics';
|
||||||
import feeApi from './api/fee-api';
|
import feeApi from './api/fee-api';
|
||||||
import backendInfo from './api/backend-info';
|
import backendInfo from './api/backend-info';
|
||||||
@ -8,7 +8,7 @@ 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, Transaction, TransactionExtended } from './interfaces';
|
import { OptimizedStatistic, RequiredSpec, TransactionExtended } from './mempool.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';
|
||||||
@ -533,10 +533,10 @@ class Routes {
|
|||||||
if (txInMempool) {
|
if (txInMempool) {
|
||||||
transaction = txInMempool;
|
transaction = txInMempool;
|
||||||
} else {
|
} else {
|
||||||
transaction = await transactionUtils.getTransactionExtended(req.params.txId);
|
transaction = await transactionUtils.$getTransactionExtended(req.params.txId, false, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (transaction) {
|
if (transaction) {
|
||||||
transaction = await transactionUtils.$addPrevoutsToTransaction(transaction);
|
|
||||||
res.json(transaction);
|
res.json(transaction);
|
||||||
} else {
|
} else {
|
||||||
res.status(500).send('Error fetching transaction.');
|
res.status(500).send('Error fetching transaction.');
|
||||||
@ -560,7 +560,20 @@ class Routes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getBlockTransactions(req: Request, res: Response) {
|
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) {
|
public async getBlockHeight(req: Request, res: Response) {
|
||||||
@ -568,81 +581,27 @@ class Routes {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getAddress(req: Request, res: Response) {
|
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.');
|
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const addressInfo = await bitcoinApi.$validateAddress(req.params.address);
|
const addressData = await bitcoinApi.$getAddress(req.params.address);
|
||||||
if (!addressInfo || !addressInfo.isvalid) {
|
res.json(addressData);
|
||||||
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,
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e.message);
|
res.status(500).send(e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getAddressTransactions(req: Request, res: Response) {
|
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.');
|
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const addressInfo = await bitcoinApi.$validateAddress(req.params.address);
|
const transactions = await bitcoinApi.$getAddressTransactions(req.params.address, req.params.txId);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
res.json(transactions);
|
res.json(transactions);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).send(e.message);
|
res.status(500).send(e.message);
|
||||||
|
Loading…
Reference in New Issue
Block a user