Big refactor of multiple backends handling.

This commit is contained in:
softsimon
2020-12-28 04:47:22 +07:00
parent 8d0db12abe
commit bb28a56622
27 changed files with 946 additions and 869 deletions

View File

@@ -1,21 +1,13 @@
import { MempoolInfo, Transaction, Block, MempoolEntries, MempoolEntry, Address, AddressInformation,
ScriptHashBalance, ScriptHashHistory } from '../../interfaces';
import { IEsploraApi } from './esplora-api.interface';
export interface AbstractBitcoinApi {
$getMempoolInfo(): Promise<MempoolInfo>;
$getRawMempool(): Promise<Transaction['txid'][]>;
$getRawTransaction(txId: string): Promise<Transaction>;
$getRawMempool(): Promise<IEsploraApi.Transaction['txid'][]>;
$getRawTransaction(txId: string, skipConversion?: boolean, addPrevout?: boolean): Promise<IEsploraApi.Transaction>;
$getRawTransactionBitcoind(txId: string, skipConversion?: boolean, addPrevout?: boolean): Promise<IEsploraApi.Transaction>;
$getBlockHeightTip(): Promise<number>;
$getTxIdsForBlock(hash: string): Promise<string[]>;
$getBlockHash(height: number): Promise<string>;
$getBlock(hash: string): Promise<Block>;
$getMempoolEntry(txid: string): Promise<MempoolEntry>;
$getAddress(address: string): Promise<Address>;
$validateAddress(address: string): Promise<AddressInformation>;
$getScriptHashBalance(scriptHash: string): Promise<ScriptHashBalance>;
$getScriptHashHistory(scriptHash: string): Promise<ScriptHashHistory[]>;
// Custom
$getRawMempoolVerbose(): Promise<MempoolEntries>;
$getRawTransactionBitcond(txId: string): Promise<Transaction>;
$getBlock(hash: string): Promise<IEsploraApi.Block>;
$getAddress(address: string): Promise<IEsploraApi.Address>;
$getAddressTransactions(address: string, lastSeenTxId: string): Promise<IEsploraApi.Transaction[]>;
}

View File

@@ -1,18 +1,18 @@
import config from '../../config';
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
import BitcoindElectrsApi from './bitcoind-electrs-api';
import BitcoindApi from './bitcoind-api';
import ElectrsApi from './electrs-api';
import EsploraApi from './esplora-api';
import BitcoinApi from './bitcoin-api';
import ElectrumApi from './electrum-api';
function bitcoinApiFactory(): AbstractBitcoinApi {
switch (config.MEMPOOL.BACKEND) {
case 'electrs':
return new ElectrsApi();
case 'bitcoind-electrs':
return new BitcoindElectrsApi();
case 'bitcoind':
case 'esplora':
return new EsploraApi();
case 'electrum':
return new ElectrumApi();
case 'none':
default:
return new BitcoindApi();
return new BitcoinApi();
}
}

View 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';
}
}

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

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

View File

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

View File

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

View File

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

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

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

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

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