Address page mostly working.

This commit is contained in:
softsimon 2020-12-22 06:04:31 +07:00
parent ecc0f316cc
commit f84b9e6582
No known key found for this signature in database
GPG Key ID: 488D7DCFB5A430D7
9 changed files with 195 additions and 25 deletions

View File

@ -30,6 +30,7 @@
"@codewarriorr/electrum-client-js": "^0.1.1",
"@mempool/bitcoin": "^3.0.2",
"axios": "^0.21.0",
"crypto-js": "^4.0.0",
"express": "^4.17.1",
"locutus": "^2.0.12",
"mysql2": "^1.6.1",

View File

@ -1,4 +1,5 @@
import { MempoolInfo, Transaction, Block, MempoolEntries, MempoolEntry, Address } from '../../interfaces';
import { MempoolInfo, Transaction, Block, MempoolEntries, MempoolEntry, Address, AddressInformation,
ScriptHashBalance, ScriptHashHistory } from '../../interfaces';
export interface AbstractBitcoinApi {
$getMempoolInfo(): Promise<MempoolInfo>;
@ -10,6 +11,9 @@ export interface AbstractBitcoinApi {
$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>;

View File

@ -1,8 +1,10 @@
import config from '../../config';
import { Transaction, Block, MempoolInfo, RpcBlock, MempoolEntries, MempoolEntry, Address } from '../../interfaces';
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 {
class BitcoindApi implements AbstractBitcoinApi {
bitcoindClient: any;
constructor() {
@ -82,6 +84,18 @@ class BitcoindApi {
$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,11 +1,13 @@
import config from '../../config';
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
import { Transaction, Block, MempoolInfo, RpcBlock, MempoolEntries, MempoolEntry, Address } from '../../interfaces';
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;
@ -117,6 +119,23 @@ class BitcoindElectrsApi implements AbstractBitcoinApi {
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,6 +1,7 @@
import config from '../../config';
import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
import { Transaction, Block, MempoolInfo, MempoolEntry, MempoolEntries, Address } from '../../interfaces';
import { Transaction, Block, MempoolInfo, MempoolEntry, MempoolEntries, Address,
AddressInformation, ScriptHashBalance, ScriptHashHistory } from '../../interfaces';
import axios from 'axios';
class ElectrsApi implements AbstractBitcoinApi {
@ -63,6 +64,19 @@ class ElectrsApi implements AbstractBitcoinApi {
$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

@ -67,17 +67,22 @@ class Blocks {
let found = 0;
for (let i = 0; i < txIds.length; i++) {
// When using bitcoind, just fetch the coinbase tx for now
if ((config.MEMPOOL.BACKEND === 'bitcoind' ||
config.MEMPOOL.BACKEND === 'bitcoind-electrs') && i === 0) {
const tx = await transactionUtils.getTransactionExtended(txIds[i], true);
if (tx) {
transactions.push(tx);
}
}
if (mempool[txIds[i]]) {
transactions.push(mempool[txIds[i]]);
found++;
} else {
// When using bitcoind, just skip parsing past block tx's for now except for coinbase
if (config.MEMPOOL.BACKEND === 'electrs' || i === 0) { //
logger.debug(`Fetching block tx ${i} of ${txIds.length}`);
const tx = await transactionUtils.getTransactionExtended(txIds[i]);
if (tx) {
transactions.push(tx);
}
} else if (config.MEMPOOL.BACKEND === 'electrs') {
logger.debug(`Fetching block tx ${i} of ${txIds.length}`);
const tx = await transactionUtils.getTransactionExtended(txIds[i]);
if (tx) {
transactions.push(tx);
}
}
}

View File

@ -34,13 +34,14 @@ class TransactionUtils {
return this.extendTransaction(transaction);
}
public extendTransaction(transaction: Transaction | MempoolEntry): TransactionExtended {
public extendTransaction(transaction: Transaction): TransactionExtended {
transaction['vsize'] = Math.round(transaction.weight / 4);
transaction['feePerVsize'] = Math.max(1, (transaction.fee || 0) / (transaction.weight / 4));
if (!transaction.in_active_chain) {
transaction['firstSeen'] = Math.round((new Date().getTime() / 1000));
}
// @ts-ignore
return Object.assign({
vsize: Math.round(transaction.weight / 4),
feePerVsize: Math.max(1, (transaction.fee || 0) / (transaction.weight / 4)),
firstSeen: Math.round((new Date().getTime() / 1000)),
}, transaction);
return transaction;
}
public stripCoinbaseTransaction(tx: TransactionExtended): TransactionMinerInfo {
@ -88,7 +89,7 @@ class TransactionUtils {
scriptpubkey: vout.scriptPubKey.hex,
scriptpubkey_address: vout.scriptPubKey && vout.scriptPubKey.addresses ? vout.scriptPubKey.addresses[0] : null,
scriptpubkey_asm: vout.scriptPubKey.asm,
scriptpubkey_type: vout.scriptPubKey.type,
scriptpubkey_type: this.translateScriptPubKeyType(vout.scriptPubKey.type),
};
});
if (transaction.confirmations) {
@ -106,6 +107,25 @@ class TransactionUtils {
}
}
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) {

View File

@ -30,6 +30,9 @@ export interface Transaction {
vin: Vin[];
vout: Vout[];
status: Status;
// bitcoind (temp?)
in_active_chain?: boolean;
}
export interface TransactionMinerInfo {
@ -294,3 +297,24 @@ 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;
}

View File

@ -1,5 +1,5 @@
import config from './config';
import { Request, Response } from 'express';
import { json, Request, Response } from 'express';
import statistics from './api/statistics';
import feeApi from './api/fee-api';
import backendInfo from './api/backend-info';
@ -568,16 +568,85 @@ class Routes {
}
public async getAddress(req: Request, res: Response) {
if (config.MEMPOOL.BACKEND === 'bitcoind') {
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
return;
}
try {
const result = await bitcoinApi.$getAddress(req.params.hash);
res.json(result);
const addressInfo = await bitcoinApi.$validateAddress(req.params.address);
if (!addressInfo || !addressInfo.isvalid) {
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) {
res.status(500).send(e.message);
}
}
public async getAddressTransactions(req: Request, res: Response) {
res.status(404).send('Not implemented');
if (config.MEMPOOL.BACKEND === 'bitcoind') {
res.status(405).send('Address lookups cannot be used with bitcoind as backend.');
return;
}
try {
const addressInfo = await bitcoinApi.$validateAddress(req.params.address);
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);
} catch (e) {
res.status(500).send(e.message);
}
}
public async getAdressTxChain(req: Request, res: Response) {