Use minfee second node to supply alt rbf policy mempool txs

This commit is contained in:
Mononaut 2023-01-17 17:50:15 -06:00
parent be27e3df52
commit cdfee53cf5
No known key found for this signature in database
GPG Key ID: A3F058E41374C04E
19 changed files with 294 additions and 157 deletions

View File

@ -27,7 +27,8 @@
"POOLS_JSON_TREE_URL": "https://api.github.com/repos/mempool/mining-pools/git/trees/master",
"ADVANCED_GBT_AUDIT": false,
"ADVANCED_GBT_MEMPOOL": false,
"CPFP_INDEXING": false
"CPFP_INDEXING": false,
"RBF_DUAL_NODE": false
},
"CORE_RPC": {
"HOST": "127.0.0.1",

View File

@ -26,9 +26,10 @@
"INDEXING_BLOCKS_AMOUNT": 14,
"POOLS_JSON_TREE_URL": "__POOLS_JSON_TREE_URL__",
"POOLS_JSON_URL": "__POOLS_JSON_URL__",
"ADVANCED_GBT_AUDIT": "__MEMPOOL_ADVANCED_GBT_AUDIT__",
"ADVANCED_GBT_MEMPOOL": "__MEMPOOL_ADVANCED_GBT_MEMPOOL__",
"CPFP_INDEXING": "__MEMPOOL_CPFP_INDEXING__"
"ADVANCED_GBT_AUDIT": "__ADVANCED_GBT_AUDIT__",
"ADVANCED_GBT_MEMPOOL": "__ADVANCED_GBT_MEMPOOL__",
"CPFP_INDEXING": "__CPFP_INDEXING__",
"RBF_DUAL_NODE": "__RBF_DUAL_NODE__"
},
"CORE_RPC": {
"HOST": "__CORE_RPC_HOST__",

View File

@ -41,6 +41,7 @@ describe('Mempool Backend Config', () => {
ADVANCED_GBT_AUDIT: false,
ADVANCED_GBT_MEMPOOL: false,
CPFP_INDEXING: false,
RBF_DUAL_NODE: false,
});
expect(config.ELECTRUM).toStrictEqual({ HOST: '127.0.0.1', PORT: 3306, TLS_ENABLED: true });

View File

@ -0,0 +1,175 @@
import config from '../config';
import { TransactionExtended } from '../mempool.interfaces';
import logger from '../logger';
import { Common } from './common';
import { IBitcoinApi } from './bitcoin/bitcoin-api.interface';
import BitcoinApi from './bitcoin/bitcoin-api';
import bitcoinApi from './bitcoin/bitcoin-api-factory';
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
import { IEsploraApi } from './bitcoin/esplora-api.interface';
import { Mempool } from './mempool';
class AltMempool extends Mempool {
private bitcoinSecondApi: BitcoinApi;
constructor() {
super();
this.bitcoinSecondApi = new BitcoinApi(bitcoinSecondClient, this, bitcoinApi);
}
protected init(): void {
// override
}
public setOutOfSync(): void {
this.inSync = false;
}
public setMempool(mempoolData: { [txId: string]: TransactionExtended }): void {
this.mempoolCache = mempoolData;
}
public getFirstSeenForTransactions(txIds: string[]): number[] {
const txTimes: number[] = [];
txIds.forEach((txId: string) => {
const tx = this.mempoolCache[txId];
if (tx && tx.firstSeen) {
txTimes.push(tx.firstSeen);
} else {
txTimes.push(0);
}
});
return txTimes;
}
public async $updateMempool(): Promise<void> {
logger.debug(`Updating alternative mempool...`);
const start = new Date().getTime();
const currentMempoolSize = Object.keys(this.mempoolCache).length;
const transactions = await this.bitcoinSecondApi.$getRawMempool();
const diff = transactions.length - currentMempoolSize;
this.mempoolCacheDelta = Math.abs(diff);
const loadingMempool = this.mempoolCacheDelta > 100;
for (const txid of transactions) {
if (!this.mempoolCache[txid]) {
try {
const transaction = await this.$fetchTransaction(txid);
this.mempoolCache[txid] = transaction;
if (loadingMempool && Object.keys(this.mempoolCache).length % 50 === 0) {
logger.info(`loaded ${Object.keys(this.mempoolCache).length}/${transactions.length} alternative mempool transactions`);
}
} catch (e) {
logger.debug(`Error finding transaction '${txid}' in the alternative mempool: ` + (e instanceof Error ? e.message : e));
}
}
if ((new Date().getTime()) - start > Mempool.WEBSOCKET_REFRESH_RATE_MS) {
break;
}
}
if (loadingMempool) {
logger.info(`loaded ${Object.keys(this.mempoolCache).length}/${transactions.length} alternative mempool transactions`);
}
// Prevent mempool from clear on bitcoind restart by delaying the deletion
if (this.mempoolProtection === 0
&& currentMempoolSize > 20000
&& transactions.length / currentMempoolSize <= 0.80
) {
this.mempoolProtection = 1;
setTimeout(() => {
this.mempoolProtection = 2;
}, 1000 * 60 * config.MEMPOOL.CLEAR_PROTECTION_MINUTES);
}
const deletedTransactions: string[] = [];
if (this.mempoolProtection !== 1) {
this.mempoolProtection = 0;
// Index object for faster search
const transactionsObject = {};
transactions.forEach((txId) => transactionsObject[txId] = true);
// Flag transactions for lazy deletion
for (const tx in this.mempoolCache) {
if (!transactionsObject[tx] && !this.mempoolCache[tx].deleteAfter) {
deletedTransactions.push(this.mempoolCache[tx].txid);
}
}
for (const txid of deletedTransactions) {
delete this.mempoolCache[txid];
}
}
this.mempoolCacheDelta = Math.abs(transactions.length - Object.keys(this.mempoolCache).length);
const end = new Date().getTime();
const time = end - start;
logger.debug(`Alt mempool updated in ${time / 1000} seconds. New size: ${Object.keys(this.mempoolCache).length} (${diff > 0 ? '+' + diff : diff})`);
}
public getTransaction(txid: string): TransactionExtended {
return this.mempoolCache[txid] || null;
}
protected async $fetchTransaction(txid: string): Promise<TransactionExtended> {
const rawTx = await this.bitcoinSecondApi.$getRawTransaction(txid, false, true, false);
return this.extendTransaction(rawTx);
}
protected extendTransaction(transaction: IEsploraApi.Transaction): TransactionExtended {
// @ts-ignore
if (transaction.vsize) {
// @ts-ignore
return transaction;
}
const feePerVbytes = Math.max(Common.isLiquid() ? 0.1 : 1,
(transaction.fee || 0) / (transaction.weight / 4));
const transactionExtended: TransactionExtended = Object.assign({
vsize: Math.round(transaction.weight / 4),
feePerVsize: feePerVbytes,
effectiveFeePerVsize: feePerVbytes,
}, transaction);
if (!transaction.status.confirmed) {
transactionExtended.firstSeen = Math.round((new Date().getTime() / 1000));
}
return transactionExtended;
}
public async $updateMemPoolInfo(): Promise<void> {
this.mempoolInfo = await this.$getMempoolInfo();
}
public getMempoolInfo(): IBitcoinApi.MempoolInfo {
return this.mempoolInfo;
}
public getTxPerSecond(): number {
return this.txPerSecond;
}
public getVBytesPerSecond(): number {
return this.vBytesPerSecond;
}
public handleRbfTransactions(rbfTransactions: { [txid: string]: TransactionExtended; }): void {
for (const rbfTransaction in rbfTransactions) {
if (this.mempoolCache[rbfTransaction]) {
// Erase the replaced transactions from the local mempool
delete this.mempoolCache[rbfTransaction];
}
}
}
protected updateTxPerSecond(): void {}
protected deleteExpiredTransactions(): void {}
protected $getMempoolInfo(): any {
return bitcoinSecondClient.getMempoolInfo();
}
}
export default new AltMempool();

View File

@ -4,19 +4,20 @@ import EsploraApi from './esplora-api';
import BitcoinApi from './bitcoin-api';
import ElectrumApi from './electrum-api';
import bitcoinClient from './bitcoin-client';
import mempool from '../mempool';
function bitcoinApiFactory(): AbstractBitcoinApi {
switch (config.MEMPOOL.BACKEND) {
case 'esplora':
return new EsploraApi();
case 'electrum':
return new ElectrumApi(bitcoinClient);
return new ElectrumApi(bitcoinClient, mempool);
case 'none':
default:
return new BitcoinApi(bitcoinClient);
return new BitcoinApi(bitcoinClient, mempool);
}
}
export const bitcoinCoreApi = new BitcoinApi(bitcoinClient);
export const bitcoinCoreApi = new BitcoinApi(bitcoinClient, mempool);
export default bitcoinApiFactory();

View File

@ -3,15 +3,18 @@ import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory';
import { IBitcoinApi } from './bitcoin-api.interface';
import { IEsploraApi } from './esplora-api.interface';
import blocks from '../blocks';
import mempool from '../mempool';
import { TransactionExtended } from '../../mempool.interfaces';
import { MempoolBlock, TransactionExtended } from '../../mempool.interfaces';
class BitcoinApi implements AbstractBitcoinApi {
private mempool: any = null;
private rawMempoolCache: IBitcoinApi.RawMempool | null = null;
protected bitcoindClient: any;
protected backupBitcoinApi: BitcoinApi;
constructor(bitcoinClient: any) {
constructor(bitcoinClient: any, mempool: any, backupBitcoinApi: any = null) {
this.bitcoindClient = bitcoinClient;
this.mempool = mempool;
this.backupBitcoinApi = backupBitcoinApi;
}
static convertBlock(block: IBitcoinApi.Block): IEsploraApi.Block {
@ -34,7 +37,7 @@ class BitcoinApi implements AbstractBitcoinApi {
$getRawTransaction(txId: string, skipConversion = false, addPrevout = false, lazyPrevouts = false): Promise<IEsploraApi.Transaction> {
// If the transaction is in the mempool we already converted and fetched the fee. Only prevouts are missing
const txInMempool = mempool.getMempool()[txId];
const txInMempool = this.mempool.getMempool()[txId];
if (txInMempool && addPrevout) {
return this.$addPrevouts(txInMempool);
}
@ -118,7 +121,7 @@ class BitcoinApi implements AbstractBitcoinApi {
$getAddressPrefix(prefix: string): string[] {
const found: { [address: string]: string } = {};
const mp = mempool.getMempool();
const mp = this.mempool.getMempool();
for (const tx in mp) {
for (const vout of mp[tx].vout) {
if (vout.scriptpubkey_address.indexOf(prefix) === 0) {
@ -260,7 +263,7 @@ class BitcoinApi implements AbstractBitcoinApi {
return transaction;
}
let mempoolEntry: IBitcoinApi.MempoolEntry;
if (!mempool.isInSync() && !this.rawMempoolCache) {
if (!this.mempool.isInSync() && !this.rawMempoolCache) {
this.rawMempoolCache = await this.$getRawMempoolVerbose();
}
if (this.rawMempoolCache && this.rawMempoolCache[transaction.txid]) {
@ -316,10 +319,23 @@ class BitcoinApi implements AbstractBitcoinApi {
transaction.vin[i].lazy = true;
continue;
}
const innerTx = await this.$getRawTransaction(transaction.vin[i].txid, false, false);
transaction.vin[i].prevout = innerTx.vout[transaction.vin[i].vout];
this.addInnerScriptsToVin(transaction.vin[i]);
totalIn += innerTx.vout[transaction.vin[i].vout].value;
let innerTx;
try {
innerTx = await this.$getRawTransaction(transaction.vin[i].txid, false, false);
} catch (e) {
if (this.backupBitcoinApi) {
// input tx is confirmed, but the preferred client has txindex=0, so fetch from the backup client instead.
const backupTx = await this.backupBitcoinApi.$getRawTransaction(transaction.vin[i].txid);
innerTx = JSON.parse(JSON.stringify(backupTx));
} else {
throw e;
}
}
if (innerTx) {
transaction.vin[i].prevout = innerTx.vout[transaction.vin[i].vout];
this.addInnerScriptsToVin(transaction.vin[i]);
totalIn += innerTx.vout[transaction.vin[i].vout].value;
}
}
if (lazyPrevouts && transaction.vin.length > 12) {
transaction.fee = -1;

View File

@ -19,6 +19,7 @@ import bitcoinClient from './bitcoin-client';
import difficultyAdjustment from '../difficulty-adjustment';
import transactionRepository from '../../repositories/TransactionRepository';
import rbfCache from '../rbf-cache';
import altMempool from '../alt-mempool';
class BitcoinRoutes {
public initRoutes(app: Application) {
@ -32,6 +33,7 @@ class BitcoinRoutes {
.get(config.MEMPOOL.API_URL_PREFIX + 'backend-info', this.getBackendInfo)
.get(config.MEMPOOL.API_URL_PREFIX + 'init-data', this.getInitData)
.get(config.MEMPOOL.API_URL_PREFIX + 'validate-address/:address', this.validateAddress)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/alt/:txId', this.getAltTransaction)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/replaces', this.getRbfHistory)
.get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/cached', this.getCachedTx)
.post(config.MEMPOOL.API_URL_PREFIX + 'tx/push', this.$postTransactionForm)
@ -241,6 +243,23 @@ class BitcoinRoutes {
}
}
private async getAltTransaction(req: Request, res: Response) {
try {
const transaction = altMempool.getTransaction(req.params.txId);
if (transaction) {
res.json(transaction);
} else {
res.status(404).send('No such transaction in the alternate mempool');
}
} catch (e) {
let statusCode = 500;
if (e instanceof Error && e instanceof Error && e.message && e.message.indexOf('No such mempool or blockchain transaction') > -1) {
statusCode = 404;
}
res.status(statusCode).send(e instanceof Error ? e.message : e);
}
}
private async getRawTransaction(req: Request, res: Response) {
try {
const transaction: IEsploraApi.Transaction = await bitcoinApi.$getRawTransaction(req.params.txId, true);

View File

@ -12,8 +12,8 @@ import memoryCache from '../memory-cache';
class BitcoindElectrsApi extends BitcoinApi implements AbstractBitcoinApi {
private electrumClient: any;
constructor(bitcoinClient: any) {
super(bitcoinClient);
constructor(bitcoinClient: any, mempool: any) {
super(bitcoinClient, mempool);
const electrumConfig = { client: 'mempool-v2', version: '1.4' };
const electrumPersistencePolicy = { retryPeriod: 10000, maxRetry: 1000, callback: null };

View File

@ -249,7 +249,7 @@ class MempoolBlocks {
cluster.forEach(txid => {
if (txid === tx.txid) {
matched = true;
} else {
} else if (mempool[txid]) {
const relative = {
txid: txid,
fee: mempool[txid].fee,

View File

@ -10,28 +10,32 @@ import bitcoinClient from './bitcoin/bitcoin-client';
import bitcoinSecondClient from './bitcoin/bitcoin-second-client';
import rbfCache from './rbf-cache';
class Mempool {
private static WEBSOCKET_REFRESH_RATE_MS = 10000;
private static LAZY_DELETE_AFTER_SECONDS = 30;
private inSync: boolean = false;
private mempoolCacheDelta: number = -1;
private mempoolCache: { [txId: string]: TransactionExtended } = {};
private mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0,
export class Mempool {
protected static WEBSOCKET_REFRESH_RATE_MS = 10000;
protected static LAZY_DELETE_AFTER_SECONDS = 30;
protected inSync: boolean = false;
protected mempoolCacheDelta: number = -1;
protected mempoolCache: { [txId: string]: TransactionExtended } = {};
protected mempoolInfo: IBitcoinApi.MempoolInfo = { loaded: false, size: 0, bytes: 0, usage: 0, total_fee: 0,
maxmempool: 300000000, mempoolminfee: 0.00001000, minrelaytxfee: 0.00001000 };
private mempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[],
protected mempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[],
deletedTransactions: TransactionExtended[]) => void) | undefined;
private asyncMempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[],
protected asyncMempoolChangedCallback: ((newMempool: {[txId: string]: TransactionExtended; }, newTransactions: TransactionExtended[],
deletedTransactions: TransactionExtended[]) => Promise<void>) | undefined;
private txPerSecondArray: number[] = [];
private txPerSecond: number = 0;
protected txPerSecondArray: number[] = [];
protected txPerSecond: number = 0;
private vBytesPerSecondArray: VbytesPerSecond[] = [];
private vBytesPerSecond: number = 0;
private mempoolProtection = 0;
private latestTransactions: any[] = [];
protected vBytesPerSecondArray: VbytesPerSecond[] = [];
protected vBytesPerSecond: number = 0;
protected mempoolProtection = 0;
protected latestTransactions: any[] = [];
constructor() {
this.init();
}
protected init(): void {
setInterval(this.updateTxPerSecond.bind(this), 1000);
setInterval(this.deleteExpiredTransactions.bind(this), 20000);
}
@ -217,7 +221,7 @@ class Mempool {
}
}
private updateTxPerSecond() {
protected updateTxPerSecond() {
const nowMinusTimeSpan = new Date().getTime() - (1000 * config.STATISTICS.TX_PER_SECOND_SAMPLE_PERIOD);
this.txPerSecondArray = this.txPerSecondArray.filter((unixTime) => unixTime > nowMinusTimeSpan);
this.txPerSecond = this.txPerSecondArray.length / config.STATISTICS.TX_PER_SECOND_SAMPLE_PERIOD || 0;
@ -230,7 +234,7 @@ class Mempool {
}
}
private deleteExpiredTransactions() {
protected deleteExpiredTransactions() {
const now = new Date().getTime();
for (const tx in this.mempoolCache) {
const lazyDeleteAt = this.mempoolCache[tx].deleteAfter;
@ -241,7 +245,7 @@ class Mempool {
}
}
private $getMempoolInfo() {
protected $getMempoolInfo() {
if (config.MEMPOOL.USE_SECOND_NODE_FOR_MINFEE) {
return Promise.all([
bitcoinClient.getMempoolInfo(),

View File

@ -32,6 +32,7 @@ interface IConfig {
ADVANCED_GBT_AUDIT: boolean;
ADVANCED_GBT_MEMPOOL: boolean;
CPFP_INDEXING: boolean;
RBF_DUAL_NODE: boolean;
};
ESPLORA: {
REST_API_URL: string;
@ -153,6 +154,7 @@ const defaults: IConfig = {
'ADVANCED_GBT_AUDIT': false,
'ADVANCED_GBT_MEMPOOL': false,
'CPFP_INDEXING': false,
'RBF_DUAL_NODE': false,
},
'ESPLORA': {
'REST_API_URL': 'http://127.0.0.1:3000',

View File

@ -17,6 +17,7 @@ import logger from './logger';
import backendInfo from './api/backend-info';
import loadingIndicators from './api/loading-indicators';
import mempool from './api/mempool';
import altMempool from './api/alt-mempool';
import elementsParser from './api/liquid/elements-parser';
import databaseMigration from './api/database-migration';
import syncAssets from './sync-assets';
@ -170,6 +171,9 @@ class Server {
await poolsUpdater.updatePoolsJson();
await blocks.$updateBlocks();
await memPool.$updateMempool();
if (config.MEMPOOL.RBF_DUAL_NODE) {
await altMempool.$updateMempool();
}
indexer.$run();
setTimeout(this.runMainUpdateLoop.bind(this), config.MEMPOOL.POLL_RATE_MS);

View File

@ -25,7 +25,8 @@
"AUTOMATIC_BLOCK_REINDEXING": __MEMPOOL_AUTOMATIC_BLOCK_REINDEXING__,
"ADVANCED_GBT_AUDIT": __MEMPOOL_ADVANCED_GBT_AUDIT__,
"ADVANCED_GBT_MEMPOOL": __MEMPOOL_ADVANCED_GBT_MEMPOOL__,
"CPFP_INDEXING": __MEMPOOL_CPFP_INDEXING__
"CPFP_INDEXING": __MEMPOOL_CPFP_INDEXING__,
"RBF_DUAL_NODE": __RBF_DUAL_NODE__
},
"CORE_RPC": {
"HOST": "__CORE_RPC_HOST__",

View File

@ -30,6 +30,8 @@ __MEMPOOL_POOLS_JSON_TREE_URL__=${MEMPOOL_POOLS_JSON_TREE_URL:=https://api.githu
__MEMPOOL_ADVANCED_GBT_AUDIT__=${MEMPOOL_ADVANCED_GBT_AUDIT:=false}
__MEMPOOL_ADVANCED_GBT_MEMPOOL__=${MEMPOOL_ADVANCED_GBT_MEMPOOL:=false}
__MEMPOOL_CPFP_INDEXING__=${MEMPOOL_CPFP_INDEXING:=false}
__MEMPOOL_RBF_DUAL_NODE__=${MEMPOOL_RBF_DUAL_NODE:=false}
# CORE_RPC
__CORE_RPC_HOST__=${CORE_RPC_HOST:=127.0.0.1}
@ -142,6 +144,7 @@ sed -i "s!__MEMPOOL_POOLS_JSON_TREE_URL__!${__MEMPOOL_POOLS_JSON_TREE_URL__}!g"
sed -i "s!__MEMPOOL_ADVANCED_GBT_MEMPOOL__!${__MEMPOOL_ADVANCED_GBT_MEMPOOL__}!g" mempool-config.json
sed -i "s!__MEMPOOL_ADVANCED_GBT_AUDIT__!${__MEMPOOL_ADVANCED_GBT_AUDIT__}!g" mempool-config.json
sed -i "s!__MEMPOOL_CPFP_INDEXING__!${__MEMPOOL_CPFP_INDEXING__}!g" mempool-config.json
sed -i "s/__MEMPOOL_RBF_DUAL_NODE__/${__MEMPOOL_RBF_DUAL_NODE__}/g" mempool-config.json
sed -i "s/__CORE_RPC_HOST__/${__CORE_RPC_HOST__}/g" mempool-config.json
sed -i "s/__CORE_RPC_PORT__/${__CORE_RPC_PORT__}/g" mempool-config.json

View File

@ -22,5 +22,5 @@
"SIGNET_BLOCK_AUDIT_START_HEIGHT": 0,
"LIGHTNING": false,
"FULL_RBF_ENABLED": false,
"ALT_BACKEND_URL": "https://rbf.mempool.space"
"ALT_BACKEND_ENABLED": false
}

View File

@ -1,6 +1,5 @@
import { Component, OnInit, AfterViewInit, OnDestroy, HostListener, ViewChild, ElementRef } from '@angular/core';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { AltElectrsApiService } from '../../services/alt-electrs-api.service';
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
import {
switchMap,
@ -44,7 +43,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
fetchRbfSubscription: Subscription;
fetchCachedTxSubscription: Subscription;
txReplacedSubscription: Subscription;
altBackendTxSubscription: Subscription;
altTxSubscription: Subscription;
blocksSubscription: Subscription;
queryParamsSubscription: Subscription;
urlFragmentSubscription: Subscription;
@ -87,7 +86,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
private router: Router,
private relativeUrlPipe: RelativeUrlPipe,
private electrsApiService: ElectrsApiService,
private altElectrsApiService: AltElectrsApiService,
private stateService: StateService,
private cacheService: CacheService,
private websocketService: WebsocketService,
@ -96,7 +94,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
private seoService: SeoService
) {
this.fullRBF = stateService.env.FULL_RBF_ENABLED;
this.altBackend = stateService.env.ALT_BACKEND_URL;
}
ngOnInit() {
@ -203,27 +200,29 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
return;
}
this.tx = tx;
if (tx.fee === undefined) {
this.tx.fee = 0;
}
this.tx.feePerVsize = tx.fee / (tx.weight / 4);
this.isLoadingTx = false;
this.error = undefined;
this.waitingForTransaction = false;
this.graphExpanded = false;
this.setupGraph();
if (!this.altTx && !this.tx) {
this.tx = tx;
if (tx.fee === undefined) {
this.tx.fee = 0;
}
this.tx.feePerVsize = tx.fee / (tx.weight / 4);
this.isLoadingTx = false;
this.error = undefined;
this.waitingForTransaction = false;
this.graphExpanded = false;
this.setupGraph();
if (!this.tx?.status?.confirmed) {
this.fetchRbfHistory$.next(this.tx.txid);
if (!this.tx?.status?.confirmed) {
this.fetchRbfHistory$.next(this.tx.txid);
}
}
});
this.altBackendTxSubscription = this.checkAltBackend$
this.altTxSubscription = this.checkAltBackend$
.pipe(
switchMap((txId) =>
this.altElectrsApiService
.getTransaction$(txId)
this.apiService
.getAltTransaction$(txId)
.pipe(
catchError((e) => {
return of(null);
@ -326,7 +325,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
.subscribe((tx: Transaction) => {
if (!tx) {
this.notFound = true;
if (this.stateService.env.ALT_BACKEND_URL) {
if (this.stateService.env.ALT_BACKEND_ENABLED) {
this.checkAltBackend$.next(this.txId);
}
return;
@ -370,9 +369,6 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.fetchCpfp$.next(this.tx.txid);
}
this.fetchRbfHistory$.next(this.tx.txid);
if (this.stateService.env.ALT_BACKEND_URL) {
this.checkAltBackend$.next(this.txId);
}
}
setTimeout(() => { this.applyFragment(); }, 0);
},
@ -546,7 +542,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.fetchCpfpSubscription.unsubscribe();
this.fetchRbfSubscription.unsubscribe();
this.fetchCachedTxSubscription.unsubscribe();
this.altBackendTxSubscription?.unsubscribe();
this.altTxSubscription?.unsubscribe();
this.txReplacedSubscription.unsubscribe();
this.blocksSubscription.unsubscribe();
this.queryParamsSubscription.unsubscribe();

View File

@ -1,91 +0,0 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Transaction, Address, Outspend, Recent, Asset } from '../interfaces/electrs.interface';
import { StateService } from './state.service';
import { BlockExtended } from '../interfaces/node-api.interface';
@Injectable({
providedIn: 'root'
})
export class AltElectrsApiService {
private apiBaseUrl: string; // base URL is protocol, hostname, and port
private apiBasePath: string; // network path is /testnet, etc. or '' for mainnet
constructor(
private httpClient: HttpClient,
private stateService: StateService,
) {
this.apiBaseUrl = stateService.env.ALT_BACKEND_URL || '';
this.apiBasePath = ''; // assume mainnet by default
this.stateService.networkChanged$.subscribe((network) => {
if (network === 'bisq') {
network = '';
}
this.apiBasePath = network ? '/' + network : '';
});
}
getBlock$(hash: string): Observable<BlockExtended> {
return this.httpClient.get<BlockExtended>(this.apiBaseUrl + this.apiBasePath + '/api/block/' + hash);
}
listBlocks$(height?: number): Observable<BlockExtended[]> {
return this.httpClient.get<BlockExtended[]>(this.apiBaseUrl + this.apiBasePath + '/api/blocks/' + (height || ''));
}
getTransaction$(txId: string): Observable<Transaction> {
return this.httpClient.get<Transaction>(this.apiBaseUrl + this.apiBasePath + '/api/tx/' + txId);
}
getRecentTransaction$(): Observable<Recent[]> {
return this.httpClient.get<Recent[]>(this.apiBaseUrl + this.apiBasePath + '/api/mempool/recent');
}
getOutspend$(hash: string, vout: number): Observable<Outspend> {
return this.httpClient.get<Outspend>(this.apiBaseUrl + this.apiBasePath + '/api/tx/' + hash + '/outspend/' + vout);
}
getOutspends$(hash: string): Observable<Outspend[]> {
return this.httpClient.get<Outspend[]>(this.apiBaseUrl + this.apiBasePath + '/api/tx/' + hash + '/outspends');
}
getBlockTransactions$(hash: string, index: number = 0): Observable<Transaction[]> {
return this.httpClient.get<Transaction[]>(this.apiBaseUrl + this.apiBasePath + '/api/block/' + hash + '/txs/' + index);
}
getBlockHashFromHeight$(height: number): Observable<string> {
return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/block-height/' + height, {responseType: 'text'});
}
getAddress$(address: string): Observable<Address> {
return this.httpClient.get<Address>(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address);
}
getAddressTransactions$(address: string): Observable<Transaction[]> {
return this.httpClient.get<Transaction[]>(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address + '/txs');
}
getAddressTransactionsFromHash$(address: string, txid: string): Observable<Transaction[]> {
return this.httpClient.get<Transaction[]>(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address + '/txs/chain/' + txid);
}
getAsset$(assetId: string): Observable<Asset> {
return this.httpClient.get<Asset>(this.apiBaseUrl + this.apiBasePath + '/api/asset/' + assetId);
}
getAssetTransactions$(assetId: string): Observable<Transaction[]> {
return this.httpClient.get<Transaction[]>(this.apiBaseUrl + this.apiBasePath + '/api/asset/' + assetId + '/txs');
}
getAssetTransactionsFromHash$(assetId: string, txid: string): Observable<Transaction[]> {
return this.httpClient.get<Transaction[]>(this.apiBaseUrl + this.apiBasePath + '/api/asset/' + assetId + '/txs/chain/' + txid);
}
getAddressesByPrefix$(prefix: string): Observable<string[]> {
if (prefix.toLowerCase().indexOf('bc1') === 0) {
prefix = prefix.toLowerCase();
}
return this.httpClient.get<string[]>(this.apiBaseUrl + this.apiBasePath + '/api/address-prefix/' + prefix);
}
}

View File

@ -143,6 +143,10 @@ export class ApiService {
return this.httpClient.post<any>(this.apiBaseUrl + this.apiBasePath + '/api/tx', hexPayload, { responseType: 'text' as 'json'});
}
getAltTransaction$(txId: string): Observable<Transaction> {
return this.httpClient.get<Transaction>(this.apiBaseUrl + this.apiBasePath + '/api/v1/tx/alt/' + txId);
}
listPools$(interval: string | undefined) : Observable<any> {
return this.httpClient.get<any>(
this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/pools` +

View File

@ -43,7 +43,7 @@ export interface Env {
TESTNET_BLOCK_AUDIT_START_HEIGHT: number;
SIGNET_BLOCK_AUDIT_START_HEIGHT: number;
FULL_RBF_ENABLED: boolean;
ALT_BACKEND_URL: string;
ALT_BACKEND_ENABLED: boolean;
}
const defaultEnv: Env = {
@ -73,7 +73,7 @@ const defaultEnv: Env = {
'TESTNET_BLOCK_AUDIT_START_HEIGHT': 0,
'SIGNET_BLOCK_AUDIT_START_HEIGHT': 0,
'FULL_RBF_ENABLED': false,
'ALT_BACKEND_URL': '',
'ALT_BACKEND_ENABLED': false,
};
@Injectable({