check for txs in alternative backend mempool
This commit is contained in:
parent
7da308c1e1
commit
be27e3df52
@ -20,5 +20,7 @@
|
|||||||
"MAINNET_BLOCK_AUDIT_START_HEIGHT": 0,
|
"MAINNET_BLOCK_AUDIT_START_HEIGHT": 0,
|
||||||
"TESTNET_BLOCK_AUDIT_START_HEIGHT": 0,
|
"TESTNET_BLOCK_AUDIT_START_HEIGHT": 0,
|
||||||
"SIGNET_BLOCK_AUDIT_START_HEIGHT": 0,
|
"SIGNET_BLOCK_AUDIT_START_HEIGHT": 0,
|
||||||
"LIGHTNING": false
|
"LIGHTNING": false,
|
||||||
|
"FULL_RBF_ENABLED": false,
|
||||||
|
"ALT_BACKEND_URL": "https://rbf.mempool.space"
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,13 @@
|
|||||||
<div class="container-xl">
|
<div class="container-xl">
|
||||||
|
|
||||||
<div class="title-block">
|
<div class="title-block">
|
||||||
|
<div *ngIf="fullRBF && notFound && altTx && !replaced" class="alert alert-mempool" role="alert">
|
||||||
|
<span i18n="transaction.rbf.only-default-mempool">This transaction cannot be found in the Full RBF mempool. It may depend on an ancestor which has been replaced by fee.</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="!fullRBF && notFound && altTx && !replaced" class="alert alert-mempool" role="alert">
|
||||||
|
<span i18n="transaction.rbf.only-full-rbf-mempool">This transaction cannot be found in the default mempool. It may replace a transaction which does not opt-in to replace-by-fee.</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div *ngIf="rbfTransaction" class="alert alert-mempool" role="alert">
|
<div *ngIf="rbfTransaction" class="alert alert-mempool" role="alert">
|
||||||
<span i18n="transaction.rbf.replacement|RBF replacement">This transaction has been replaced by:</span>
|
<span i18n="transaction.rbf.replacement|RBF replacement">This transaction has been replaced by:</span>
|
||||||
<app-truncate [text]="rbfTransaction.txid" [lastChars]="12" [link]="['/tx/' | relativeUrl, rbfTransaction.txid]"></app-truncate>
|
<app-truncate [text]="rbfTransaction.txid" [lastChars]="12" [link]="['/tx/' | relativeUrl, rbfTransaction.txid]"></app-truncate>
|
||||||
@ -101,7 +108,7 @@
|
|||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
<table class="table table-borderless table-striped">
|
<table class="table table-borderless table-striped">
|
||||||
<tbody>
|
<tbody>
|
||||||
<ng-template [ngIf]="transactionTime !== 0 && !replaced">
|
<ng-template [ngIf]="transactionTime !== 0 && !notFound">
|
||||||
<tr *ngIf="transactionTime === -1; else firstSeenTmpl">
|
<tr *ngIf="transactionTime === -1; else firstSeenTmpl">
|
||||||
<td><span class="skeleton-loader"></span></td>
|
<td><span class="skeleton-loader"></span></td>
|
||||||
<td><span class="skeleton-loader"></span></td>
|
<td><span class="skeleton-loader"></span></td>
|
||||||
@ -113,7 +120,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<tr *ngIf="!replaced">
|
<tr *ngIf="!notFound">
|
||||||
<td class="td-width" i18n="transaction.eta|Transaction ETA">ETA</td>
|
<td class="td-width" i18n="transaction.eta|Transaction ETA">ETA</td>
|
||||||
<td>
|
<td>
|
||||||
<ng-template [ngIf]="txInBlockIndex === undefined" [ngIfElse]="estimationTmpl">
|
<ng-template [ngIf]="txInBlockIndex === undefined" [ngIfElse]="estimationTmpl">
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Component, OnInit, AfterViewInit, OnDestroy, HostListener, ViewChild, ElementRef } from '@angular/core';
|
import { Component, OnInit, AfterViewInit, OnDestroy, HostListener, ViewChild, ElementRef } from '@angular/core';
|
||||||
import { ElectrsApiService } from '../../services/electrs-api.service';
|
import { ElectrsApiService } from '../../services/electrs-api.service';
|
||||||
|
import { AltElectrsApiService } from '../../services/alt-electrs-api.service';
|
||||||
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
import { ActivatedRoute, ParamMap, Router } from '@angular/router';
|
||||||
import {
|
import {
|
||||||
switchMap,
|
switchMap,
|
||||||
@ -43,6 +44,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
fetchRbfSubscription: Subscription;
|
fetchRbfSubscription: Subscription;
|
||||||
fetchCachedTxSubscription: Subscription;
|
fetchCachedTxSubscription: Subscription;
|
||||||
txReplacedSubscription: Subscription;
|
txReplacedSubscription: Subscription;
|
||||||
|
altBackendTxSubscription: Subscription;
|
||||||
blocksSubscription: Subscription;
|
blocksSubscription: Subscription;
|
||||||
queryParamsSubscription: Subscription;
|
queryParamsSubscription: Subscription;
|
||||||
urlFragmentSubscription: Subscription;
|
urlFragmentSubscription: Subscription;
|
||||||
@ -55,6 +57,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
fetchCpfp$ = new Subject<string>();
|
fetchCpfp$ = new Subject<string>();
|
||||||
fetchRbfHistory$ = new Subject<string>();
|
fetchRbfHistory$ = new Subject<string>();
|
||||||
fetchCachedTx$ = new Subject<string>();
|
fetchCachedTx$ = new Subject<string>();
|
||||||
|
checkAltBackend$ = new Subject<string>();
|
||||||
now = new Date().getTime();
|
now = new Date().getTime();
|
||||||
timeAvg$: Observable<number>;
|
timeAvg$: Observable<number>;
|
||||||
liquidUnblinding = new LiquidUnblinding();
|
liquidUnblinding = new LiquidUnblinding();
|
||||||
@ -69,6 +72,10 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
hideFlow: boolean = this.stateService.hideFlow.value;
|
hideFlow: boolean = this.stateService.hideFlow.value;
|
||||||
overrideFlowPreference: boolean = null;
|
overrideFlowPreference: boolean = null;
|
||||||
flowEnabled: boolean;
|
flowEnabled: boolean;
|
||||||
|
notFound: boolean = false;
|
||||||
|
altTx: Transaction;
|
||||||
|
fullRBF: boolean = false;
|
||||||
|
altBackend: string;
|
||||||
|
|
||||||
tooltipPosition: { x: number, y: number };
|
tooltipPosition: { x: number, y: number };
|
||||||
|
|
||||||
@ -80,13 +87,17 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
private router: Router,
|
private router: Router,
|
||||||
private relativeUrlPipe: RelativeUrlPipe,
|
private relativeUrlPipe: RelativeUrlPipe,
|
||||||
private electrsApiService: ElectrsApiService,
|
private electrsApiService: ElectrsApiService,
|
||||||
|
private altElectrsApiService: AltElectrsApiService,
|
||||||
private stateService: StateService,
|
private stateService: StateService,
|
||||||
private cacheService: CacheService,
|
private cacheService: CacheService,
|
||||||
private websocketService: WebsocketService,
|
private websocketService: WebsocketService,
|
||||||
private audioService: AudioService,
|
private audioService: AudioService,
|
||||||
private apiService: ApiService,
|
private apiService: ApiService,
|
||||||
private seoService: SeoService
|
private seoService: SeoService
|
||||||
) {}
|
) {
|
||||||
|
this.fullRBF = stateService.env.FULL_RBF_ENABLED;
|
||||||
|
this.altBackend = stateService.env.ALT_BACKEND_URL;
|
||||||
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.websocketService.want(['blocks', 'mempool-blocks']);
|
this.websocketService.want(['blocks', 'mempool-blocks']);
|
||||||
@ -208,6 +219,38 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.altBackendTxSubscription = this.checkAltBackend$
|
||||||
|
.pipe(
|
||||||
|
switchMap((txId) =>
|
||||||
|
this.altElectrsApiService
|
||||||
|
.getTransaction$(txId)
|
||||||
|
.pipe(
|
||||||
|
catchError((e) => {
|
||||||
|
return of(null);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).subscribe((tx) => {
|
||||||
|
if (!tx) {
|
||||||
|
this.altTx = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.altTx = tx;
|
||||||
|
if (tx.fee === undefined) {
|
||||||
|
this.altTx.fee = 0;
|
||||||
|
}
|
||||||
|
this.altTx.feePerVsize = tx.fee / (tx.weight / 4);
|
||||||
|
if (!this.tx) {
|
||||||
|
this.tx = tx;
|
||||||
|
this.isLoadingTx = false;
|
||||||
|
this.error = undefined;
|
||||||
|
this.waitingForTransaction = false;
|
||||||
|
this.graphExpanded = false;
|
||||||
|
this.setupGraph();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
this.subscription = this.route.paramMap
|
this.subscription = this.route.paramMap
|
||||||
.pipe(
|
.pipe(
|
||||||
switchMap((params: ParamMap) => {
|
switchMap((params: ParamMap) => {
|
||||||
@ -282,8 +325,13 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
)
|
)
|
||||||
.subscribe((tx: Transaction) => {
|
.subscribe((tx: Transaction) => {
|
||||||
if (!tx) {
|
if (!tx) {
|
||||||
|
this.notFound = true;
|
||||||
|
if (this.stateService.env.ALT_BACKEND_URL) {
|
||||||
|
this.checkAltBackend$.next(this.txId);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.notFound = false;
|
||||||
|
|
||||||
this.tx = tx;
|
this.tx = tx;
|
||||||
if (tx.fee === undefined) {
|
if (tx.fee === undefined) {
|
||||||
@ -322,6 +370,9 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.fetchCpfp$.next(this.tx.txid);
|
this.fetchCpfp$.next(this.tx.txid);
|
||||||
}
|
}
|
||||||
this.fetchRbfHistory$.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);
|
setTimeout(() => { this.applyFragment(); }, 0);
|
||||||
},
|
},
|
||||||
@ -423,6 +474,8 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.isLoadingTx = true;
|
this.isLoadingTx = true;
|
||||||
this.rbfTransaction = undefined;
|
this.rbfTransaction = undefined;
|
||||||
this.replaced = false;
|
this.replaced = false;
|
||||||
|
this.altTx = null;
|
||||||
|
this.notFound = false;
|
||||||
this.transactionTime = -1;
|
this.transactionTime = -1;
|
||||||
this.cpfpInfo = null;
|
this.cpfpInfo = null;
|
||||||
this.rbfReplaces = [];
|
this.rbfReplaces = [];
|
||||||
@ -493,6 +546,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
|
|||||||
this.fetchCpfpSubscription.unsubscribe();
|
this.fetchCpfpSubscription.unsubscribe();
|
||||||
this.fetchRbfSubscription.unsubscribe();
|
this.fetchRbfSubscription.unsubscribe();
|
||||||
this.fetchCachedTxSubscription.unsubscribe();
|
this.fetchCachedTxSubscription.unsubscribe();
|
||||||
|
this.altBackendTxSubscription?.unsubscribe();
|
||||||
this.txReplacedSubscription.unsubscribe();
|
this.txReplacedSubscription.unsubscribe();
|
||||||
this.blocksSubscription.unsubscribe();
|
this.blocksSubscription.unsubscribe();
|
||||||
this.queryParamsSubscription.unsubscribe();
|
this.queryParamsSubscription.unsubscribe();
|
||||||
|
91
frontend/src/app/services/alt-electrs-api.service.ts
Normal file
91
frontend/src/app/services/alt-electrs-api.service.ts
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
@ -42,6 +42,8 @@ export interface Env {
|
|||||||
MAINNET_BLOCK_AUDIT_START_HEIGHT: number;
|
MAINNET_BLOCK_AUDIT_START_HEIGHT: number;
|
||||||
TESTNET_BLOCK_AUDIT_START_HEIGHT: number;
|
TESTNET_BLOCK_AUDIT_START_HEIGHT: number;
|
||||||
SIGNET_BLOCK_AUDIT_START_HEIGHT: number;
|
SIGNET_BLOCK_AUDIT_START_HEIGHT: number;
|
||||||
|
FULL_RBF_ENABLED: boolean;
|
||||||
|
ALT_BACKEND_URL: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultEnv: Env = {
|
const defaultEnv: Env = {
|
||||||
@ -70,6 +72,8 @@ const defaultEnv: Env = {
|
|||||||
'MAINNET_BLOCK_AUDIT_START_HEIGHT': 0,
|
'MAINNET_BLOCK_AUDIT_START_HEIGHT': 0,
|
||||||
'TESTNET_BLOCK_AUDIT_START_HEIGHT': 0,
|
'TESTNET_BLOCK_AUDIT_START_HEIGHT': 0,
|
||||||
'SIGNET_BLOCK_AUDIT_START_HEIGHT': 0,
|
'SIGNET_BLOCK_AUDIT_START_HEIGHT': 0,
|
||||||
|
'FULL_RBF_ENABLED': false,
|
||||||
|
'ALT_BACKEND_URL': '',
|
||||||
};
|
};
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
|
Loading…
x
Reference in New Issue
Block a user