ETA |
diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts
index 7e1ae525e..6e39cbacd 100644
--- a/frontend/src/app/components/transaction/transaction.component.ts
+++ b/frontend/src/app/components/transaction/transaction.component.ts
@@ -1,5 +1,6 @@
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,
@@ -43,6 +44,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
fetchRbfSubscription: Subscription;
fetchCachedTxSubscription: Subscription;
txReplacedSubscription: Subscription;
+ altBackendTxSubscription: Subscription;
blocksSubscription: Subscription;
queryParamsSubscription: Subscription;
urlFragmentSubscription: Subscription;
@@ -55,6 +57,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
fetchCpfp$ = new Subject();
fetchRbfHistory$ = new Subject();
fetchCachedTx$ = new Subject();
+ checkAltBackend$ = new Subject();
now = new Date().getTime();
timeAvg$: Observable;
liquidUnblinding = new LiquidUnblinding();
@@ -69,6 +72,10 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
hideFlow: boolean = this.stateService.hideFlow.value;
overrideFlowPreference: boolean = null;
flowEnabled: boolean;
+ notFound: boolean = false;
+ altTx: Transaction;
+ fullRBF: boolean = false;
+ altBackend: string;
tooltipPosition: { x: number, y: number };
@@ -80,13 +87,17 @@ 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,
private audioService: AudioService,
private apiService: ApiService,
private seoService: SeoService
- ) {}
+ ) {
+ this.fullRBF = stateService.env.FULL_RBF_ENABLED;
+ this.altBackend = stateService.env.ALT_BACKEND_URL;
+ }
ngOnInit() {
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
.pipe(
switchMap((params: ParamMap) => {
@@ -282,8 +325,13 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
)
.subscribe((tx: Transaction) => {
if (!tx) {
+ this.notFound = true;
+ if (this.stateService.env.ALT_BACKEND_URL) {
+ this.checkAltBackend$.next(this.txId);
+ }
return;
}
+ this.notFound = false;
this.tx = tx;
if (tx.fee === undefined) {
@@ -322,6 +370,9 @@ 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);
},
@@ -423,6 +474,8 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.isLoadingTx = true;
this.rbfTransaction = undefined;
this.replaced = false;
+ this.altTx = null;
+ this.notFound = false;
this.transactionTime = -1;
this.cpfpInfo = null;
this.rbfReplaces = [];
@@ -493,6 +546,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
this.fetchCpfpSubscription.unsubscribe();
this.fetchRbfSubscription.unsubscribe();
this.fetchCachedTxSubscription.unsubscribe();
+ this.altBackendTxSubscription?.unsubscribe();
this.txReplacedSubscription.unsubscribe();
this.blocksSubscription.unsubscribe();
this.queryParamsSubscription.unsubscribe();
diff --git a/frontend/src/app/services/alt-electrs-api.service.ts b/frontend/src/app/services/alt-electrs-api.service.ts
new file mode 100644
index 000000000..fdb5e046e
--- /dev/null
+++ b/frontend/src/app/services/alt-electrs-api.service.ts
@@ -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 {
+ return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/block/' + hash);
+ }
+
+ listBlocks$(height?: number): Observable {
+ return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/blocks/' + (height || ''));
+ }
+
+ getTransaction$(txId: string): Observable {
+ return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/tx/' + txId);
+ }
+
+ getRecentTransaction$(): Observable {
+ return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/mempool/recent');
+ }
+
+ getOutspend$(hash: string, vout: number): Observable {
+ return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/tx/' + hash + '/outspend/' + vout);
+ }
+
+ getOutspends$(hash: string): Observable {
+ return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/tx/' + hash + '/outspends');
+ }
+
+ getBlockTransactions$(hash: string, index: number = 0): Observable {
+ return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/block/' + hash + '/txs/' + index);
+ }
+
+ getBlockHashFromHeight$(height: number): Observable {
+ return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/block-height/' + height, {responseType: 'text'});
+ }
+
+ getAddress$(address: string): Observable {
+ return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address);
+ }
+
+ getAddressTransactions$(address: string): Observable {
+ return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address + '/txs');
+ }
+
+ getAddressTransactionsFromHash$(address: string, txid: string): Observable {
+ return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/address/' + address + '/txs/chain/' + txid);
+ }
+
+ getAsset$(assetId: string): Observable {
+ return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/asset/' + assetId);
+ }
+
+ getAssetTransactions$(assetId: string): Observable {
+ return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/asset/' + assetId + '/txs');
+ }
+
+ getAssetTransactionsFromHash$(assetId: string, txid: string): Observable {
+ return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/asset/' + assetId + '/txs/chain/' + txid);
+ }
+
+ getAddressesByPrefix$(prefix: string): Observable {
+ if (prefix.toLowerCase().indexOf('bc1') === 0) {
+ prefix = prefix.toLowerCase();
+ }
+ return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/address-prefix/' + prefix);
+ }
+}
diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts
index 86efa57f8..67fe98d6d 100644
--- a/frontend/src/app/services/state.service.ts
+++ b/frontend/src/app/services/state.service.ts
@@ -42,6 +42,8 @@ export interface Env {
MAINNET_BLOCK_AUDIT_START_HEIGHT: number;
TESTNET_BLOCK_AUDIT_START_HEIGHT: number;
SIGNET_BLOCK_AUDIT_START_HEIGHT: number;
+ FULL_RBF_ENABLED: boolean;
+ ALT_BACKEND_URL: string;
}
const defaultEnv: Env = {
@@ -70,6 +72,8 @@ const defaultEnv: Env = {
'MAINNET_BLOCK_AUDIT_START_HEIGHT': 0,
'TESTNET_BLOCK_AUDIT_START_HEIGHT': 0,
'SIGNET_BLOCK_AUDIT_START_HEIGHT': 0,
+ 'FULL_RBF_ENABLED': false,
+ 'ALT_BACKEND_URL': '',
};
@Injectable({
|