From f2780e65cdae729e680675b3078b84a781490f43 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Wed, 22 Jun 2022 19:08:16 +0000 Subject: [PATCH 1/4] Disable mined block animations --- .../block-overview-graph.component.scss | 1 + .../block-overview-graph.component.ts | 15 ++++++++ .../block-overview-graph/block-scene.ts | 34 ++++++++++++++++--- .../app/components/block/block.component.ts | 32 +++++++---------- 4 files changed, 58 insertions(+), 24 deletions(-) diff --git a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.scss b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.scss index 05b9b340a..58b53aebf 100644 --- a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.scss +++ b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.scss @@ -26,6 +26,7 @@ .loader-wrapper { position: absolute; + background: #181b2d7f; left: 0; right: 0; top: 0; diff --git a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts index a458ebd5f..e2774ac03 100644 --- a/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts +++ b/frontend/src/app/components/block-overview-graph/block-overview-graph.component.ts @@ -68,6 +68,21 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy { this.start(); } + destroy(): void { + if (this.scene) { + this.scene.destroy(); + this.start(); + } + } + + // initialize the scene without any entry transition + setup(transactions: TransactionStripped[]): void { + if (this.scene) { + this.scene.setup(transactions); + this.start(); + } + } + enter(transactions: TransactionStripped[], direction: string): void { if (this.scene) { this.scene.enter(transactions, direction); diff --git a/frontend/src/app/components/block-overview-graph/block-scene.ts b/frontend/src/app/components/block-overview-graph/block-scene.ts index fc5bfff8e..af64c0f20 100644 --- a/frontend/src/app/components/block-overview-graph/block-scene.ts +++ b/frontend/src/app/components/block-overview-graph/block-scene.ts @@ -29,10 +29,6 @@ export default class BlockScene { this.init({ width, height, resolution, blockLimit, orientation, flip, vertexArray }); } - destroy(): void { - Object.values(this.txs).forEach(tx => tx.destroy()); - } - resize({ width = this.width, height = this.height }: { width?: number, height?: number}): void { this.width = width; this.height = height; @@ -46,6 +42,36 @@ export default class BlockScene { } } + // Destroy the current layout and clean up graphics sprites without any exit animation + destroy(): void { + Object.values(this.txs).forEach(tx => tx.destroy()); + this.txs = {}; + this.layout = null; + } + + // set up the scene with an initial set of transactions, without any transition animation + setup(txs: TransactionStripped[]) { + // clean up any old transactions + Object.values(this.txs).forEach(tx => { + tx.destroy(); + delete this.txs[tx.txid]; + }); + this.layout = new BlockLayout({ width: this.gridWidth, height: this.gridHeight }); + txs.forEach(tx => { + const txView = new TxView(tx, this.vertexArray); + this.txs[tx.txid] = txView; + this.place(txView); + this.saveGridToScreenPosition(txView); + this.applyTxUpdate(txView, { + display: { + position: txView.screenPosition, + color: txView.getColor() + }, + duration: 0 + }); + }); + } + // Animate new block entering scene enter(txs: TransactionStripped[], direction) { this.replace(txs, direction); diff --git a/frontend/src/app/components/block/block.component.ts b/frontend/src/app/components/block/block.component.ts index 39c4042fb..9d8f3e486 100644 --- a/frontend/src/app/components/block/block.component.ts +++ b/frontend/src/app/components/block/block.component.ts @@ -2,9 +2,9 @@ import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/co import { Location } from '@angular/common'; import { ActivatedRoute, ParamMap, Router } from '@angular/router'; import { ElectrsApiService } from '../../services/electrs-api.service'; -import { switchMap, tap, debounceTime, catchError, map, shareReplay, startWith, pairwise } from 'rxjs/operators'; +import { switchMap, tap, throttleTime, catchError, map, shareReplay, startWith, pairwise } from 'rxjs/operators'; import { Transaction, Vout } from '../../interfaces/electrs.interface'; -import { Observable, of, Subscription } from 'rxjs'; +import { Observable, of, Subscription, asyncScheduler } from 'rxjs'; import { StateService } from '../../services/state.service'; import { SeoService } from 'src/app/services/seo.service'; import { WebsocketService } from 'src/app/services/websocket.service'; @@ -33,7 +33,6 @@ export class BlockComponent implements OnInit, OnDestroy { strippedTransactions: TransactionStripped[]; overviewTransitionDirection: string; isLoadingOverview = true; - isAwaitingOverview = true; error: any; blockSubsidy: number; fees: number; @@ -124,6 +123,7 @@ export class BlockComponent implements OnInit, OnDestroy { return of(history.state.data.block); } else { this.isLoadingBlock = true; + this.isLoadingOverview = true; let blockInCache: BlockExtended; if (isBlockHeight) { @@ -170,13 +170,9 @@ export class BlockComponent implements OnInit, OnDestroy { this.transactions = null; this.transactionsError = null; this.isLoadingOverview = true; - this.isAwaitingOverview = true; - this.overviewError = true; - if (this.blockGraph) { - this.blockGraph.exit(direction); - } + this.overviewError = null; }), - debounceTime(300), + throttleTime(300, asyncScheduler, { leading: true, trailing: true }), shareReplay(1) ); this.transactionSubscription = block$.pipe( @@ -194,11 +190,6 @@ export class BlockComponent implements OnInit, OnDestroy { } this.transactions = transactions; this.isLoadingTransactions = false; - - if (!this.isAwaitingOverview && this.blockGraph && this.strippedTransactions && this.overviewTransitionDirection) { - this.isLoadingOverview = false; - this.blockGraph.replace(this.strippedTransactions, this.overviewTransitionDirection, false); - } }, (error) => { this.error = error; @@ -226,18 +217,19 @@ export class BlockComponent implements OnInit, OnDestroy { ), ) .subscribe(({transactions, direction}: {transactions: TransactionStripped[], direction: string}) => { - this.isAwaitingOverview = false; this.strippedTransactions = transactions; - this.overviewTransitionDirection = direction; - if (!this.isLoadingTransactions && this.blockGraph) { - this.isLoadingOverview = false; - this.blockGraph.replace(this.strippedTransactions, this.overviewTransitionDirection, false); + this.isLoadingOverview = false; + if (this.blockGraph) { + this.blockGraph.destroy(); + this.blockGraph.setup(this.strippedTransactions); } }, (error) => { this.error = error; this.isLoadingOverview = false; - this.isAwaitingOverview = false; + if (this.blockGraph) { + this.blockGraph.destroy(); + } }); this.networkChangedSubscription = this.stateService.networkChanged$ From da28e7b80e6a2bf6d81a0f112046f4ef4905ddde Mon Sep 17 00:00:00 2001 From: nymkappa Date: Wed, 22 Jun 2022 23:17:49 +0200 Subject: [PATCH 2/4] Preload the previous block - Disable 300 ms on block page --- .../app/components/block/block.component.ts | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/components/block/block.component.ts b/frontend/src/app/components/block/block.component.ts index 39c4042fb..3ff65c9df 100644 --- a/frontend/src/app/components/block/block.component.ts +++ b/frontend/src/app/components/block/block.component.ts @@ -54,6 +54,9 @@ export class BlockComponent implements OnInit, OnDestroy { blocksSubscription: Subscription; networkChangedSubscription: Subscription; queryParamsSubscription: Subscription; + nextBlockSubscription: Subscription = undefined; + nextBlockSummarySubscription: Subscription = undefined; + nextBlockTxListSubscription: Subscription = undefined; @ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent; @@ -152,6 +155,14 @@ export class BlockComponent implements OnInit, OnDestroy { } }), tap((block: BlockExtended) => { + // Preload previous block summary (execute the http query so the response will be cached) + this.unsubscribeNextBlockSubscriptions(); + setTimeout(() => { + this.nextBlockSubscription = this.apiService.getBlock$(block.previousblockhash).subscribe(); + this.nextBlockTxListSubscription = this.electrsApiService.getBlockTransactions$(block.previousblockhash).subscribe(); + this.nextBlockSummarySubscription = this.apiService.getStrippedBlockTransactions$(block.previousblockhash).subscribe(); + }, 100); + this.block = block; this.blockHeight = block.height; const direction = (this.lastBlockHeight < this.blockHeight) ? 'right' : 'left'; @@ -176,7 +187,6 @@ export class BlockComponent implements OnInit, OnDestroy { this.blockGraph.exit(direction); } }), - debounceTime(300), shareReplay(1) ); this.transactionSubscription = block$.pipe( @@ -273,6 +283,19 @@ export class BlockComponent implements OnInit, OnDestroy { this.blocksSubscription.unsubscribe(); this.networkChangedSubscription.unsubscribe(); this.queryParamsSubscription.unsubscribe(); + this.unsubscribeNextBlockSubscriptions(); + } + + unsubscribeNextBlockSubscriptions() { + if (this.nextBlockSubscription !== undefined) { + this.nextBlockSubscription.unsubscribe(); + } + if (this.nextBlockSummarySubscription !== undefined) { + this.nextBlockSummarySubscription.unsubscribe(); + } + if (this.nextBlockTxListSubscription !== undefined) { + this.nextBlockTxListSubscription.unsubscribe(); + } } // TODO - Refactor this.fees/this.reward for liquid because it is not From 1479039fb5be04ce2f8b2ceac1f147aa52566324 Mon Sep 17 00:00:00 2001 From: softsimon Date: Wed, 22 Jun 2022 23:34:44 +0200 Subject: [PATCH 3/4] Batch outspends requests fixes #1902 --- .../bitcoin/bitcoin-api-abstract-factory.ts | 1 + backend/src/api/bitcoin/bitcoin-api.ts | 9 +++++++ backend/src/api/bitcoin/esplora-api.ts | 14 ++++++++-- backend/src/index.ts | 1 + backend/src/routes.ts | 24 +++++++++++++++++ .../transactions-list.component.ts | 27 ++++++++----------- frontend/src/app/services/api.service.ts | 9 +++++++ 7 files changed, 67 insertions(+), 18 deletions(-) diff --git a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts index 71269da31..1956e5756 100644 --- a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts +++ b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts @@ -13,6 +13,7 @@ export interface AbstractBitcoinApi { $getAddressPrefix(prefix: string): string[]; $sendRawTransaction(rawTransaction: string): Promise; $getOutspends(txId: string): Promise; + $getBatchedOutspends(txId: string[]): Promise; } export interface BitcoinRpcCredentials { host: string; diff --git a/backend/src/api/bitcoin/bitcoin-api.ts b/backend/src/api/bitcoin/bitcoin-api.ts index 41671ede1..a30295c2f 100644 --- a/backend/src/api/bitcoin/bitcoin-api.ts +++ b/backend/src/api/bitcoin/bitcoin-api.ts @@ -141,6 +141,15 @@ class BitcoinApi implements AbstractBitcoinApi { return outSpends; } + async $getBatchedOutspends(txId: string[]): Promise { + const outspends: IEsploraApi.Outspend[][] = []; + for (const tx of txId) { + const outspend = await this.$getOutspends(tx); + outspends.push(outspend); + } + return outspends; + } + $getEstimatedHashrate(blockHeight: number): Promise { // 120 is the default block span in Core return this.bitcoindClient.getNetworkHashPs(120, blockHeight); diff --git a/backend/src/api/bitcoin/esplora-api.ts b/backend/src/api/bitcoin/esplora-api.ts index d92bba1bf..a2e9ba4f1 100644 --- a/backend/src/api/bitcoin/esplora-api.ts +++ b/backend/src/api/bitcoin/esplora-api.ts @@ -61,8 +61,18 @@ class ElectrsApi implements AbstractBitcoinApi { throw new Error('Method not implemented.'); } - $getOutspends(): Promise { - throw new Error('Method not implemented.'); + $getOutspends(txId: string): Promise { + return axios.get(config.ESPLORA.REST_API_URL + '/tx/' + txId, this.axiosConfig) + .then((response) => response.data); + } + + async $getBatchedOutspends(txId: string[]): Promise { + const outspends: IEsploraApi.Outspend[][] = []; + for (const tx of txId) { + const outspend = await this.$getOutspends(tx); + outspends.push(outspend); + } + return outspends; } } diff --git a/backend/src/index.ts b/backend/src/index.ts index 6bd8de841..2d1438842 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -195,6 +195,7 @@ class Server { setUpHttpApiRoutes() { this.app .get(config.MEMPOOL.API_URL_PREFIX + 'transaction-times', routes.getTransactionTimes) + .get(config.MEMPOOL.API_URL_PREFIX + 'outspends', routes.$getBatchedOutspends) .get(config.MEMPOOL.API_URL_PREFIX + 'cpfp/:txId', routes.getCpfpInfo) .get(config.MEMPOOL.API_URL_PREFIX + 'difficulty-adjustment', routes.getDifficultyChange) .get(config.MEMPOOL.API_URL_PREFIX + 'fees/recommended', routes.getRecommendedFees) diff --git a/backend/src/routes.ts b/backend/src/routes.ts index e63549d09..b86187e4c 100644 --- a/backend/src/routes.ts +++ b/backend/src/routes.ts @@ -120,6 +120,30 @@ class Routes { res.json(times); } + public async $getBatchedOutspends(req: Request, res: Response) { + if (!Array.isArray(req.query.txId)) { + res.status(500).send('Not an array'); + return; + } + if (req.query.txId.length > 50) { + res.status(400).send('Too many txids requested'); + return; + } + const txIds: string[] = []; + for (const _txId in req.query.txId) { + if (typeof req.query.txId[_txId] === 'string') { + txIds.push(req.query.txId[_txId].toString()); + } + } + + try { + const batchedOutspends = await bitcoinApi.$getBatchedOutspends(txIds); + res.json(batchedOutspends); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + public getCpfpInfo(req: Request, res: Response) { if (!/^[a-fA-F0-9]{64}$/.test(req.params.txId)) { res.status(501).send(`Invalid transaction ID.`); diff --git a/frontend/src/app/components/transactions-list/transactions-list.component.ts b/frontend/src/app/components/transactions-list/transactions-list.component.ts index ba8ba60ba..d5ec36151 100644 --- a/frontend/src/app/components/transactions-list/transactions-list.component.ts +++ b/frontend/src/app/components/transactions-list/transactions-list.component.ts @@ -5,8 +5,9 @@ import { Outspend, Transaction, Vin, Vout } from '../../interfaces/electrs.inter import { ElectrsApiService } from '../../services/electrs-api.service'; import { environment } from 'src/environments/environment'; import { AssetsService } from 'src/app/services/assets.service'; -import { map, switchMap } from 'rxjs/operators'; +import { map, tap, switchMap } from 'rxjs/operators'; import { BlockExtended } from 'src/app/interfaces/node-api.interface'; +import { ApiService } from 'src/app/services/api.service'; @Component({ selector: 'app-transactions-list', @@ -30,7 +31,7 @@ export class TransactionsListComponent implements OnInit, OnChanges { latestBlock$: Observable; outspendsSubscription: Subscription; - refreshOutspends$: ReplaySubject<{ [str: string]: Observable}> = new ReplaySubject(); + refreshOutspends$: ReplaySubject = new ReplaySubject(); showDetails$ = new BehaviorSubject(false); outspends: Outspend[][] = []; assetsMinimal: any; @@ -38,6 +39,7 @@ export class TransactionsListComponent implements OnInit, OnChanges { constructor( public stateService: StateService, private electrsApiService: ElectrsApiService, + private apiService: ApiService, private assetsService: AssetsService, private ref: ChangeDetectorRef, ) { } @@ -55,20 +57,14 @@ export class TransactionsListComponent implements OnInit, OnChanges { this.outspendsSubscription = merge( this.refreshOutspends$ .pipe( - switchMap((observableObject) => forkJoin(observableObject)), - map((outspends: any) => { - const newOutspends: Outspend[] = []; - for (const i in outspends) { - if (outspends.hasOwnProperty(i)) { - newOutspends.push(outspends[i]); - } - } - this.outspends = this.outspends.concat(newOutspends); + switchMap((txIds) => this.apiService.getOutspendsBatched$(txIds)), + tap((outspends: Outspend[][]) => { + this.outspends = this.outspends.concat(outspends); }), ), this.stateService.utxoSpent$ .pipe( - map((utxoSpent) => { + tap((utxoSpent) => { for (const i in utxoSpent) { this.outspends[0][i] = { spent: true, @@ -96,7 +92,7 @@ export class TransactionsListComponent implements OnInit, OnChanges { } }, 10); } - const observableObject = {}; + this.transactions.forEach((tx, i) => { tx['@voutLimit'] = true; tx['@vinLimit'] = true; @@ -117,10 +113,9 @@ export class TransactionsListComponent implements OnInit, OnChanges { tx['addressValue'] = addressIn - addressOut; } - - observableObject[i] = this.electrsApiService.getOutspends$(tx.txid); }); - this.refreshOutspends$.next(observableObject); + + this.refreshOutspends$.next(this.transactions.map((tx) => tx.txid)); } onScroll() { diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index 42c942dad..bd32685fd 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -5,6 +5,7 @@ import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITrans import { Observable } from 'rxjs'; import { StateService } from './state.service'; import { WebsocketResponse } from '../interfaces/websocket.interface'; +import { Outspend } from '../interfaces/electrs.interface'; @Injectable({ providedIn: 'root' @@ -74,6 +75,14 @@ export class ApiService { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/transaction-times', { params }); } + getOutspendsBatched$(txIds: string[]): Observable { + let params = new HttpParams(); + txIds.forEach((txId: string) => { + params = params.append('txId[]', txId); + }); + return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/v1/outspends', { params }); + } + requestDonation$(amount: number, orderId: string): Observable { const params = { amount: amount, From 960513c370b96d465118a9e35b1e89ae724b29ab Mon Sep 17 00:00:00 2001 From: softsimon Date: Thu, 23 Jun 2022 11:55:56 +0200 Subject: [PATCH 4/4] Fix for outspends when using esplora --- backend/src/api/bitcoin/esplora-api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/api/bitcoin/esplora-api.ts b/backend/src/api/bitcoin/esplora-api.ts index a2e9ba4f1..f882180b1 100644 --- a/backend/src/api/bitcoin/esplora-api.ts +++ b/backend/src/api/bitcoin/esplora-api.ts @@ -62,7 +62,7 @@ class ElectrsApi implements AbstractBitcoinApi { } $getOutspends(txId: string): Promise { - return axios.get(config.ESPLORA.REST_API_URL + '/tx/' + txId, this.axiosConfig) + return axios.get(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspends', this.axiosConfig) .then((response) => response.data); }