Use minfee second node to supply alt rbf policy mempool txs
This commit is contained in:
parent
be27e3df52
commit
cdfee53cf5
@ -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",
|
||||
|
@ -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__",
|
||||
|
@ -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 });
|
||||
|
175
backend/src/api/alt-mempool.ts
Normal file
175
backend/src/api/alt-mempool.ts
Normal 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();
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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 };
|
||||
|
@ -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,
|
||||
|
@ -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(),
|
||||
|
@ -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',
|
||||
|
@ -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);
|
||||
|
@ -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__",
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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` +
|
||||
|
@ -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({
|
||||
|
Loading…
x
Reference in New Issue
Block a user