From c7cb7d1ac4e17c3902fdda350e08038595df7890 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sat, 23 Mar 2024 11:27:28 +0000 Subject: [PATCH 01/62] Add testmempoolaccept API endpoint --- .../bitcoin/bitcoin-api-abstract-factory.ts | 3 ++- .../src/api/bitcoin/bitcoin-api.interface.ts | 13 +++++++++++ backend/src/api/bitcoin/bitcoin-api.ts | 10 +++++++- backend/src/api/bitcoin/bitcoin.routes.ts | 14 +++++++++++ backend/src/api/bitcoin/esplora-api.ts | 5 ++++ backend/src/api/common.ts | 23 +++++++++++++++++++ .../src/app/interfaces/node-api.interface.ts | 13 +++++++++++ frontend/src/app/services/api.service.ts | 8 +++++-- 8 files changed, 85 insertions(+), 4 deletions(-) diff --git a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts index cc0c801b5..4ec50f4b3 100644 --- a/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts +++ b/backend/src/api/bitcoin/bitcoin-api-abstract-factory.ts @@ -1,4 +1,4 @@ -import { IBitcoinApi } from './bitcoin-api.interface'; +import { IBitcoinApi, TestMempoolAcceptResult } from './bitcoin-api.interface'; import { IEsploraApi } from './esplora-api.interface'; export interface AbstractBitcoinApi { @@ -22,6 +22,7 @@ export interface AbstractBitcoinApi { $getScriptHash(scripthash: string): Promise; $getScriptHashTransactions(address: string, lastSeenTxId: string): Promise; $sendRawTransaction(rawTransaction: string): Promise; + $testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise; $getOutspend(txId: string, vout: number): Promise; $getOutspends(txId: string): Promise; $getBatchedOutspends(txId: string[]): Promise; diff --git a/backend/src/api/bitcoin/bitcoin-api.interface.ts b/backend/src/api/bitcoin/bitcoin-api.interface.ts index e176566d7..6e8583f6f 100644 --- a/backend/src/api/bitcoin/bitcoin-api.interface.ts +++ b/backend/src/api/bitcoin/bitcoin-api.interface.ts @@ -205,3 +205,16 @@ export namespace IBitcoinApi { "utxo_size_inc": number; } } + +export interface TestMempoolAcceptResult { + txid: string, + wtxid: string, + allowed?: boolean, + vsize?: number, + fees?: { + base: number, + "effective-feerate": number, + "effective-includes": string[], + }, + ['reject-reason']?: string, +} diff --git a/backend/src/api/bitcoin/bitcoin-api.ts b/backend/src/api/bitcoin/bitcoin-api.ts index d19eb06ac..c3304b432 100644 --- a/backend/src/api/bitcoin/bitcoin-api.ts +++ b/backend/src/api/bitcoin/bitcoin-api.ts @@ -1,6 +1,6 @@ import * as bitcoinjs from 'bitcoinjs-lib'; import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-factory'; -import { IBitcoinApi } from './bitcoin-api.interface'; +import { IBitcoinApi, TestMempoolAcceptResult } from './bitcoin-api.interface'; import { IEsploraApi } from './esplora-api.interface'; import blocks from '../blocks'; import mempool from '../mempool'; @@ -174,6 +174,14 @@ class BitcoinApi implements AbstractBitcoinApi { return this.bitcoindClient.sendRawTransaction(rawTransaction); } + async $testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise { + if (rawTransactions.length) { + return this.bitcoindClient.testMempoolAccept(rawTransactions, maxfeerate ?? undefined); + } else { + return []; + } + } + async $getOutspend(txId: string, vout: number): Promise { const txOut = await this.bitcoindClient.getTxOut(txId, vout, false); return { diff --git a/backend/src/api/bitcoin/bitcoin.routes.ts b/backend/src/api/bitcoin/bitcoin.routes.ts index a82cda3f0..ab5b0a73a 100644 --- a/backend/src/api/bitcoin/bitcoin.routes.ts +++ b/backend/src/api/bitcoin/bitcoin.routes.ts @@ -55,6 +55,7 @@ class BitcoinRoutes { .get(config.MEMPOOL.API_URL_PREFIX + 'mempool/recent', this.getRecentMempoolTransactions) .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId', this.getTransaction) .post(config.MEMPOOL.API_URL_PREFIX + 'tx', this.$postTransaction) + .post(config.MEMPOOL.API_URL_PREFIX + 'txs/test', this.$testTransactions) .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/hex', this.getRawTransaction) .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/status', this.getTransactionStatus) .get(config.MEMPOOL.API_URL_PREFIX + 'tx/:txId/outspends', this.getTransactionOutspends) @@ -749,6 +750,19 @@ class BitcoinRoutes { } } + private async $testTransactions(req: Request, res: Response) { + res.setHeader('content-type', 'text/plain'); + try { + const rawTxs = Common.getTransactionsFromRequest(req); + const maxfeerate = parseFloat(req.query.maxfeerate as string); + const result = await bitcoinApi.$testMempoolAccept(rawTxs, maxfeerate); + res.send(result); + } catch (e: any) { + res.status(400).send(e.message && e.code ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code, message: e.message }) + : (e.message || 'Error')); + } + } + } export default new BitcoinRoutes(); diff --git a/backend/src/api/bitcoin/esplora-api.ts b/backend/src/api/bitcoin/esplora-api.ts index a9dadf4a0..90e50d4c2 100644 --- a/backend/src/api/bitcoin/esplora-api.ts +++ b/backend/src/api/bitcoin/esplora-api.ts @@ -5,6 +5,7 @@ import { AbstractBitcoinApi, HealthCheckHost } from './bitcoin-api-abstract-fact import { IEsploraApi } from './esplora-api.interface'; import logger from '../../logger'; import { Common } from '../common'; +import { TestMempoolAcceptResult } from './bitcoin-api.interface'; interface FailoverHost { host: string, @@ -327,6 +328,10 @@ class ElectrsApi implements AbstractBitcoinApi { throw new Error('Method not implemented.'); } + $testMempoolAccept(rawTransactions: string[], maxfeerate?: number): Promise { + throw new Error('Method not implemented.'); + } + $getOutspend(txId: string, vout: number): Promise { return this.failoverRouter.$get('/tx/' + txId + '/outspend/' + vout); } diff --git a/backend/src/api/common.ts b/backend/src/api/common.ts index 92dfceb52..ddbe98ec5 100644 --- a/backend/src/api/common.ts +++ b/backend/src/api/common.ts @@ -946,6 +946,29 @@ export class Common { return this.validateTransactionHex(matches[1].toLowerCase()); } + static getTransactionsFromRequest(req: Request): string[] { + if (typeof req.body !== 'string') { + throw Object.assign(new Error('Non-string request body'), { code: -1 }); + } + + const txs = req.body.split(','); + + return txs.map(rawTx => { + // Support both upper and lower case hex + // Support both txHash= Form and direct API POST + const reg = /^((?:[a-fA-F0-9]{2})+)$/; + const matches = reg.exec(rawTx); + if (!matches || !matches[1]) { + throw Object.assign(new Error('Invalid hex string'), { code: -2 }); + } + + // Guaranteed to be a hex string of multiple of 2 + // Guaranteed to be lower case + // Guaranteed to pass validation (see function below) + return this.validateTransactionHex(matches[1].toLowerCase()); + }); + } + private static validateTransactionHex(txhex: string): string { // Do not mutate txhex diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts index 343d43a2e..ed7f5d9d4 100644 --- a/frontend/src/app/interfaces/node-api.interface.ts +++ b/frontend/src/app/interfaces/node-api.interface.ts @@ -423,4 +423,17 @@ export interface AccelerationInfo { effective_fee: number, boost_rate: number, boost_cost: number, +} + +export interface TestMempoolAcceptResult { + txid: string, + wtxid: string, + allowed?: boolean, + vsize?: number, + fees?: { + base: number, + "effective-feerate": number, + "effective-includes": string[], + }, + ['reject-reason']?: string, } \ No newline at end of file diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index 145a8705e..f5e59d0c9 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; import { HttpClient, HttpParams, HttpResponse } from '@angular/common/http'; -import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators, - PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, BlockSizesAndWeights, RbfTree, BlockAudit, Acceleration, AccelerationHistoryParams, CurrentPegs, AuditStatus, FederationAddress, FederationUtxo, RecentPeg, PegsVolume, AccelerationInfo } from '../interfaces/node-api.interface'; +import { CpfpInfo, OptimizedMempoolStats, AddressInformation, LiquidPegs, ITranslators, PoolStat, BlockExtended, TransactionStripped, RewardStats, AuditScore, BlockSizesAndWeights, + RbfTree, BlockAudit, CurrentPegs, AuditStatus, FederationAddress, FederationUtxo, RecentPeg, PegsVolume, AccelerationInfo, TestMempoolAcceptResult } from '../interfaces/node-api.interface'; import { BehaviorSubject, Observable, catchError, filter, map, of, shareReplay, take, tap } from 'rxjs'; import { StateService } from './state.service'; import { Transaction } from '../interfaces/electrs.interface'; @@ -238,6 +238,10 @@ export class ApiService { return this.httpClient.post(this.apiBaseUrl + this.apiBasePath + '/api/tx', hexPayload, { responseType: 'text' as 'json'}); } + testTransactions$(hexPayload: string): Observable { + return this.httpClient.post(this.apiBaseUrl + this.apiBasePath + '/api/txs/test', hexPayload, { responseType: 'text' as 'json'}); + } + getTransactionStatus$(txid: string): Observable { return this.httpClient.get(this.apiBaseUrl + this.apiBasePath + '/api/tx/' + txid + '/status'); } From f3232b2d5c9d6b5ccf48fdd24295f85a6fe43d9f Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 24 Mar 2024 09:02:19 +0000 Subject: [PATCH 02/62] Add Test Transactions page --- .../test-transactions.component.html | 53 ++++++++++++++ .../test-transactions.component.scss | 33 +++++++++ .../test-transactions.component.ts | 71 +++++++++++++++++++ frontend/src/app/master-page.module.ts | 5 ++ frontend/src/app/services/api.service.ts | 4 +- frontend/src/app/shared/shared.module.ts | 3 + 6 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 frontend/src/app/components/test-transactions/test-transactions.component.html create mode 100644 frontend/src/app/components/test-transactions/test-transactions.component.scss create mode 100644 frontend/src/app/components/test-transactions/test-transactions.component.ts diff --git a/frontend/src/app/components/test-transactions/test-transactions.component.html b/frontend/src/app/components/test-transactions/test-transactions.component.html new file mode 100644 index 000000000..7f15da2f2 --- /dev/null +++ b/frontend/src/app/components/test-transactions/test-transactions.component.html @@ -0,0 +1,53 @@ +
+

Test Transactions

+ +
+ +
+ +
+ + +
+ +

{{ error }}

+
+ +
+ +
+ + + + + + + + + + + + + + + + + +
Allowed?TXIDEffective fee rateRejection reason
+ + + + - + + + + + + - + + {{ result['reject-reason'] || '-' }} +
+
+ +
\ No newline at end of file diff --git a/frontend/src/app/components/test-transactions/test-transactions.component.scss b/frontend/src/app/components/test-transactions/test-transactions.component.scss new file mode 100644 index 000000000..399575b8e --- /dev/null +++ b/frontend/src/app/components/test-transactions/test-transactions.component.scss @@ -0,0 +1,33 @@ +.accept-results { + td, th { + &.allowed { + width: 10%; + } + &.txid { + width: 50%; + } + &.rate { + width: 20%; + text-align: right; + white-space: wrap; + } + &.reason { + width: 20%; + text-align: right; + white-space: wrap; + } + } + + @media (max-width: 950px) { + table-layout: auto; + + td, th { + &.allowed { + width: 100px; + } + &.txid { + max-width: 200px; + } + } + } +} \ No newline at end of file diff --git a/frontend/src/app/components/test-transactions/test-transactions.component.ts b/frontend/src/app/components/test-transactions/test-transactions.component.ts new file mode 100644 index 000000000..c959cb2b0 --- /dev/null +++ b/frontend/src/app/components/test-transactions/test-transactions.component.ts @@ -0,0 +1,71 @@ +import { Component, OnInit } from '@angular/core'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { ApiService } from '../../services/api.service'; +import { StateService } from '../../services/state.service'; +import { SeoService } from '../../services/seo.service'; +import { OpenGraphService } from '../../services/opengraph.service'; +import { TestMempoolAcceptResult } from '../../interfaces/node-api.interface'; + +@Component({ + selector: 'app-test-transactions', + templateUrl: './test-transactions.component.html', + styleUrls: ['./test-transactions.component.scss'] +}) +export class TestTransactionsComponent implements OnInit { + testTxsForm: UntypedFormGroup; + error: string = ''; + results: TestMempoolAcceptResult[] = []; + isLoading = false; + invalidMaxfeerate = false; + + constructor( + private formBuilder: UntypedFormBuilder, + private apiService: ApiService, + public stateService: StateService, + private seoService: SeoService, + private ogService: OpenGraphService, + ) { } + + ngOnInit(): void { + this.testTxsForm = this.formBuilder.group({ + txs: ['', Validators.required], + maxfeerate: ['', Validators.min(0)] + }); + + this.seoService.setTitle($localize`:@@meta.title.test-txs:Test Transactions`); + this.ogService.setManualOgImage('tx-push.jpg'); + } + + testTxs() { + let maxfeerate; + this.invalidMaxfeerate = false; + try { + const maxfeerateVal = this.testTxsForm.get('maxfeerate')?.value; + if (maxfeerateVal != null && maxfeerateVal !== '') { + maxfeerate = parseFloat(maxfeerateVal) / 100_000; + } + } catch (e) { + this.invalidMaxfeerate = true; + } + + this.isLoading = true; + this.error = ''; + this.results = []; + this.apiService.testTransactions$((this.testTxsForm.get('txs')?.value as string).split(',').map(hex => hex.trim()).join(','), maxfeerate === 0.1 ? null : maxfeerate) + .subscribe((result) => { + this.isLoading = false; + this.results = result || []; + this.testTxsForm.reset(); + }, + (error) => { + if (typeof error.error === 'string') { + const matchText = error.error.match('"message":"(.*?)"'); + this.error = matchText && matchText[1] || error.error; + } else if (error.message) { + this.error = error.message; + } + this.isLoading = false; + }); + } + +} diff --git a/frontend/src/app/master-page.module.ts b/frontend/src/app/master-page.module.ts index 018809d59..2d3c34a56 100644 --- a/frontend/src/app/master-page.module.ts +++ b/frontend/src/app/master-page.module.ts @@ -6,6 +6,7 @@ import { SharedModule } from './shared/shared.module'; import { StartComponent } from './components/start/start.component'; import { PushTransactionComponent } from './components/push-transaction/push-transaction.component'; +import { TestTransactionsComponent } from './components/test-transactions/test-transactions.component'; import { CalculatorComponent } from './components/calculator/calculator.component'; import { BlocksList } from './components/blocks-list/blocks-list.component'; import { RbfList } from './components/rbf-list/rbf-list.component'; @@ -30,6 +31,10 @@ const routes: Routes = [ path: 'tx/push', component: PushTransactionComponent, }, + { + path: 'tx/test', + component: TestTransactionsComponent, + }, { path: 'about', loadChildren: () => import('./components/about/about.module').then(m => m.AboutModule), diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index f5e59d0c9..e41f8eb9e 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -238,8 +238,8 @@ export class ApiService { return this.httpClient.post(this.apiBaseUrl + this.apiBasePath + '/api/tx', hexPayload, { responseType: 'text' as 'json'}); } - testTransactions$(hexPayload: string): Observable { - return this.httpClient.post(this.apiBaseUrl + this.apiBasePath + '/api/txs/test', hexPayload, { responseType: 'text' as 'json'}); + testTransactions$(hexPayload: string, maxfeerate?: number): Observable { + return this.httpClient.post(this.apiBaseUrl + this.apiBasePath + `/api/txs/test${maxfeerate != null ? '?maxfeerate=' + maxfeerate.toFixed(8) : ''}`, hexPayload); } getTransactionStatus$(txid: string): Observable { diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index 50268029b..d018dd1e8 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -68,6 +68,7 @@ import { DifficultyMiningComponent } from '../components/difficulty-mining/diffi import { RbfTimelineComponent } from '../components/rbf-timeline/rbf-timeline.component'; import { RbfTimelineTooltipComponent } from '../components/rbf-timeline/rbf-timeline-tooltip.component'; import { PushTransactionComponent } from '../components/push-transaction/push-transaction.component'; +import { TestTransactionsComponent } from '../components/test-transactions/test-transactions.component'; import { AssetsFeaturedComponent } from '../components/assets/assets-featured/assets-featured.component'; import { AssetGroupComponent } from '../components/assets/asset-group/asset-group.component'; import { AssetCirculationComponent } from '../components/asset-circulation/asset-circulation.component'; @@ -176,6 +177,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir RbfTimelineComponent, RbfTimelineTooltipComponent, PushTransactionComponent, + TestTransactionsComponent, AssetsNavComponent, AssetsFeaturedComponent, AssetGroupComponent, @@ -312,6 +314,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir RbfTimelineComponent, RbfTimelineTooltipComponent, PushTransactionComponent, + TestTransactionsComponent, AssetsNavComponent, AssetsFeaturedComponent, AssetGroupComponent, From 2a4325580248cc252652b074baf085b89dc5ea41 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Mon, 25 Mar 2024 05:52:03 +0000 Subject: [PATCH 03/62] testmempool accept more validation & switch to JSON array format --- backend/src/api/bitcoin/bitcoin.routes.ts | 2 +- backend/src/api/common.ts | 12 ++++++++---- backend/src/index.ts | 1 + .../test-transactions.component.scss | 1 + .../test-transactions.component.ts | 17 ++++++++++++++++- frontend/src/app/services/api.service.ts | 4 ++-- 6 files changed, 29 insertions(+), 8 deletions(-) diff --git a/backend/src/api/bitcoin/bitcoin.routes.ts b/backend/src/api/bitcoin/bitcoin.routes.ts index ab5b0a73a..ec63f4ff8 100644 --- a/backend/src/api/bitcoin/bitcoin.routes.ts +++ b/backend/src/api/bitcoin/bitcoin.routes.ts @@ -751,13 +751,13 @@ class BitcoinRoutes { } private async $testTransactions(req: Request, res: Response) { - res.setHeader('content-type', 'text/plain'); try { const rawTxs = Common.getTransactionsFromRequest(req); const maxfeerate = parseFloat(req.query.maxfeerate as string); const result = await bitcoinApi.$testMempoolAccept(rawTxs, maxfeerate); res.send(result); } catch (e: any) { + res.setHeader('content-type', 'text/plain'); res.status(400).send(e.message && e.code ? 'testmempoolaccept RPC error: ' + JSON.stringify({ code: e.code, message: e.message }) : (e.message || 'Error')); } diff --git a/backend/src/api/common.ts b/backend/src/api/common.ts index ddbe98ec5..2c9338814 100644 --- a/backend/src/api/common.ts +++ b/backend/src/api/common.ts @@ -946,12 +946,16 @@ export class Common { return this.validateTransactionHex(matches[1].toLowerCase()); } - static getTransactionsFromRequest(req: Request): string[] { - if (typeof req.body !== 'string') { - throw Object.assign(new Error('Non-string request body'), { code: -1 }); + static getTransactionsFromRequest(req: Request, limit: number = 25): string[] { + if (!Array.isArray(req.body) || req.body.some(hex => typeof hex !== 'string')) { + throw Object.assign(new Error('Invalid request body (should be an array of hexadecimal strings)'), { code: -1 }); } - const txs = req.body.split(','); + if (limit && req.body.length > limit) { + throw Object.assign(new Error('Exceeded maximum of 25 transactions'), { code: -1 }); + } + + const txs = req.body; return txs.map(rawTx => { // Support both upper and lower case hex diff --git a/backend/src/index.ts b/backend/src/index.ts index b088155ea..df9f7dc65 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -131,6 +131,7 @@ class Server { }) .use(express.urlencoded({ extended: true })) .use(express.text({ type: ['text/plain', 'application/base64'] })) + .use(express.json()) ; if (config.DATABASE.ENABLED && config.FIAT_PRICE.ENABLED) { diff --git a/frontend/src/app/components/test-transactions/test-transactions.component.scss b/frontend/src/app/components/test-transactions/test-transactions.component.scss index 399575b8e..ffdd5811b 100644 --- a/frontend/src/app/components/test-transactions/test-transactions.component.scss +++ b/frontend/src/app/components/test-transactions/test-transactions.component.scss @@ -2,6 +2,7 @@ td, th { &.allowed { width: 10%; + text-align: center; } &.txid { width: 50%; diff --git a/frontend/src/app/components/test-transactions/test-transactions.component.ts b/frontend/src/app/components/test-transactions/test-transactions.component.ts index c959cb2b0..7291dae20 100644 --- a/frontend/src/app/components/test-transactions/test-transactions.component.ts +++ b/frontend/src/app/components/test-transactions/test-transactions.component.ts @@ -37,6 +37,21 @@ export class TestTransactionsComponent implements OnInit { } testTxs() { + let txs: string[] = []; + try { + txs = (this.testTxsForm.get('txs')?.value as string).split(',').map(hex => hex.trim()); + if (!txs?.length) { + this.error = 'At least one transaction is required'; + return; + } else if (txs.length > 25) { + this.error = 'Exceeded maximum of 25 transactions'; + return; + } + } catch (e) { + this.error = e?.message; + return; + } + let maxfeerate; this.invalidMaxfeerate = false; try { @@ -51,7 +66,7 @@ export class TestTransactionsComponent implements OnInit { this.isLoading = true; this.error = ''; this.results = []; - this.apiService.testTransactions$((this.testTxsForm.get('txs')?.value as string).split(',').map(hex => hex.trim()).join(','), maxfeerate === 0.1 ? null : maxfeerate) + this.apiService.testTransactions$(txs, maxfeerate === 0.1 ? null : maxfeerate) .subscribe((result) => { this.isLoading = false; this.results = result || []; diff --git a/frontend/src/app/services/api.service.ts b/frontend/src/app/services/api.service.ts index e41f8eb9e..0531916a0 100644 --- a/frontend/src/app/services/api.service.ts +++ b/frontend/src/app/services/api.service.ts @@ -238,8 +238,8 @@ export class ApiService { return this.httpClient.post(this.apiBaseUrl + this.apiBasePath + '/api/tx', hexPayload, { responseType: 'text' as 'json'}); } - testTransactions$(hexPayload: string, maxfeerate?: number): Observable { - return this.httpClient.post(this.apiBaseUrl + this.apiBasePath + `/api/txs/test${maxfeerate != null ? '?maxfeerate=' + maxfeerate.toFixed(8) : ''}`, hexPayload); + testTransactions$(rawTxs: string[], maxfeerate?: number): Observable { + return this.httpClient.post(this.apiBaseUrl + this.apiBasePath + `/api/txs/test${maxfeerate != null ? '?maxfeerate=' + maxfeerate.toFixed(8) : ''}`, rawTxs); } getTransactionStatus$(txid: string): Observable { From f9498cf5a2e8589b23499cc195f872fe4441801e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 29 Apr 2024 02:06:21 +0000 Subject: [PATCH 04/62] Bump nginx from 1.25.4-alpine to 1.26.0-alpine in /docker/frontend Bumps nginx from 1.25.4-alpine to 1.26.0-alpine. --- updated-dependencies: - dependency-name: nginx dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- docker/frontend/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/frontend/Dockerfile b/docker/frontend/Dockerfile index 3a63107bf..bee617595 100644 --- a/docker/frontend/Dockerfile +++ b/docker/frontend/Dockerfile @@ -13,7 +13,7 @@ RUN npm install --omit=dev --omit=optional RUN npm run build -FROM nginx:1.25.4-alpine +FROM nginx:1.26.0-alpine WORKDIR /patch From 57b466d80ff42725ca84b8751d16e487a1af9443 Mon Sep 17 00:00:00 2001 From: natsoni Date: Mon, 29 Apr 2024 11:54:28 +0200 Subject: [PATCH 05/62] Update block display default mode to fees --- frontend/src/app/components/blockchain/blockchain.component.ts | 2 +- frontend/src/app/services/state.service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/app/components/blockchain/blockchain.component.ts b/frontend/src/app/components/blockchain/blockchain.component.ts index 60dc22e12..d70e788a2 100644 --- a/frontend/src/app/components/blockchain/blockchain.component.ts +++ b/frontend/src/app/components/blockchain/blockchain.component.ts @@ -55,7 +55,7 @@ export class BlockchainComponent implements OnInit, OnDestroy, OnChanges { firstValueFrom(this.stateService.chainTip$).then(() => { this.loadingTip = false; }); - this.blockDisplayMode = this.StorageService.getValue('block-display-mode-preference') as 'size' | 'fees' || 'size'; + this.blockDisplayMode = this.StorageService.getValue('block-display-mode-preference') as 'size' | 'fees' || 'fees'; } ngOnDestroy(): void { diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts index 286ae5e48..81fe41bd1 100644 --- a/frontend/src/app/services/state.service.ts +++ b/frontend/src/app/services/state.service.ts @@ -279,7 +279,7 @@ export class StateService { this.rateUnits$ = new BehaviorSubject(rateUnitPreference || 'vb'); const blockDisplayModePreference = this.storageService.getValue('block-display-mode-preference'); - this.blockDisplayMode$ = new BehaviorSubject(blockDisplayModePreference || 'size'); + this.blockDisplayMode$ = new BehaviorSubject(blockDisplayModePreference || 'fees'); const viewAmountModePreference = this.storageService.getValue('view-amount-mode') as 'btc' | 'sats' | 'fiat'; this.viewAmountMode$ = new BehaviorSubject<'btc' | 'sats' | 'fiat'>(viewAmountModePreference || 'btc'); From a582a3b0ed213354bd22aca232fdc69217d7bc76 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Mon, 29 Apr 2024 20:54:56 +0000 Subject: [PATCH 06/62] Polish address balance graph, add period toggle --- .../address-graph.component.html | 6 -- .../address-graph.component.scss | 17 +----- .../address-graph/address-graph.component.ts | 59 +++++++++++++------ .../components/address/address.component.html | 12 +++- .../components/address/address.component.scss | 16 +++++ .../components/address/address.component.ts | 18 ++++++ 6 files changed, 86 insertions(+), 42 deletions(-) diff --git a/frontend/src/app/components/address-graph/address-graph.component.html b/frontend/src/app/components/address-graph/address-graph.component.html index df4cdf330..32e16913a 100644 --- a/frontend/src/app/components/address-graph/address-graph.component.html +++ b/frontend/src/app/components/address-graph/address-graph.component.html @@ -1,12 +1,6 @@
-
-
- Balance History -
-
-
diff --git a/frontend/src/app/components/address-graph/address-graph.component.scss b/frontend/src/app/components/address-graph/address-graph.component.scss index a118549fb..3752203c1 100644 --- a/frontend/src/app/components/address-graph/address-graph.component.scss +++ b/frontend/src/app/components/address-graph/address-graph.component.scss @@ -45,23 +45,8 @@ display: flex; flex: 1; width: 100%; - padding-bottom: 20px; + padding-bottom: 10px; padding-right: 10px; - @media (max-width: 992px) { - padding-bottom: 25px; - } - @media (max-width: 829px) { - padding-bottom: 50px; - } - @media (max-width: 767px) { - padding-bottom: 25px; - } - @media (max-width: 629px) { - padding-bottom: 55px; - } - @media (max-width: 567px) { - padding-bottom: 55px; - } } .chart-widget { width: 100%; diff --git a/frontend/src/app/components/address-graph/address-graph.component.ts b/frontend/src/app/components/address-graph/address-graph.component.ts index 26a1bd408..a5db9602d 100644 --- a/frontend/src/app/components/address-graph/address-graph.component.ts +++ b/frontend/src/app/components/address-graph/address-graph.component.ts @@ -1,6 +1,6 @@ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnChanges, SimpleChanges } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, Input, LOCALE_ID, OnChanges, OnDestroy, SimpleChanges } from '@angular/core'; import { echarts, EChartsOption } from '../../graphs/echarts'; -import { Observable, of } from 'rxjs'; +import { BehaviorSubject, Observable, Subscription, combineLatest, of } from 'rxjs'; import { catchError } from 'rxjs/operators'; import { AddressTxSummary, ChainStats } from '../../interfaces/electrs.interface'; import { ElectrsApiService } from '../../services/electrs-api.service'; @@ -32,7 +32,7 @@ const periodSeconds = { `], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class AddressGraphComponent implements OnChanges { +export class AddressGraphComponent implements OnChanges, OnDestroy { @Input() address: string; @Input() isPubkey: boolean = false; @Input() stats: ChainStats; @@ -46,6 +46,9 @@ export class AddressGraphComponent implements OnChanges { data: any[] = []; hoverData: any[] = []; + subscription: Subscription; + redraw$: BehaviorSubject = new BehaviorSubject(false); + chartOptions: EChartsOption = {}; chartInitOptions = { renderer: 'svg', @@ -70,24 +73,38 @@ export class AddressGraphComponent implements OnChanges { if (!this.address || !this.stats) { return; } - (this.addressSummary$ || (this.isPubkey - ? this.electrsApiService.getScriptHashSummary$((this.address.length === 66 ? '21' : '41') + this.address + 'ac') - : this.electrsApiService.getAddressSummary$(this.address)).pipe( - catchError(e => { - this.error = `Failed to fetch address balance history: ${e?.status || ''} ${e?.statusText || 'unknown error'}`; - return of(null); - }), - )).subscribe(addressSummary => { - if (addressSummary) { - this.error = null; - this.prepareChartOptions(addressSummary); + if (changes.address || changes.isPubkey || changes.addressSummary$) { + if (this.subscription) { + this.subscription.unsubscribe(); } - this.isLoading = false; - this.cd.markForCheck(); - }); + this.subscription = combineLatest([ + this.redraw$, + (this.addressSummary$ || (this.isPubkey + ? this.electrsApiService.getScriptHashSummary$((this.address.length === 66 ? '21' : '41') + this.address + 'ac') + : this.electrsApiService.getAddressSummary$(this.address)).pipe( + catchError(e => { + this.error = `Failed to fetch address balance history: ${e?.status || ''} ${e?.statusText || 'unknown error'}`; + return of(null); + }), + )) + ]).subscribe(([redraw, addressSummary]) => { + if (addressSummary) { + this.error = null; + this.prepareChartOptions(addressSummary); + } + this.isLoading = false; + this.cd.markForCheck(); + }); + } else { + // re-trigger subscription + this.redraw$.next(true); + } } prepareChartOptions(summary): void { + if (!summary || !this.stats) { + return; + } let total = (this.stats.funded_txo_sum - this.stats.spent_txo_sum); this.data = summary.map(d => { const balance = total; @@ -104,8 +121,8 @@ export class AddressGraphComponent implements OnChanges { ); } - const maxValue = this.data.reduce((acc, d) => Math.max(acc, Math.abs(d[1] || d.value[1])), 0); - const minValue = this.data.reduce((acc, d) => Math.min(acc, Math.abs(d[1] || d.value[1])), maxValue); + const maxValue = this.data.reduce((acc, d) => Math.max(acc, Math.abs(d[1] ?? d.value[1])), 0); + const minValue = this.data.reduce((acc, d) => Math.min(acc, Math.abs(d[1] ?? d.value[1])), maxValue); this.chartOptions = { color: [ @@ -230,6 +247,10 @@ export class AddressGraphComponent implements OnChanges { this.chartInstance.on('click', 'series', this.onChartClick.bind(this)); } + ngOnDestroy(): void { + this.subscription.unsubscribe(); + } + isMobile() { return (window.innerWidth <= 767.98); } diff --git a/frontend/src/app/components/address/address.component.html b/frontend/src/app/components/address/address.component.html index 531b97464..661e84869 100644 --- a/frontend/src/app/components/address/address.component.html +++ b/frontend/src/app/components/address/address.component.html @@ -53,10 +53,20 @@
+
+

Balance History

+
+
+ all + | + recent +
- +
diff --git a/frontend/src/app/components/address/address.component.scss b/frontend/src/app/components/address/address.component.scss index 7107c73f2..78ca0e80d 100644 --- a/frontend/src/app/components/address/address.component.scss +++ b/frontend/src/app/components/address/address.component.scss @@ -109,3 +109,19 @@ h1 { flex-grow: 0.5; } } + +.widget-toggler { + font-size: 12px; + position: absolute; + top: -20px; + right: 3px; + text-align: right; +} + +.toggler-option { + text-decoration: none; +} + +.inactive { + color: var(--transparent-fg); +} \ No newline at end of file diff --git a/frontend/src/app/components/address/address.component.ts b/frontend/src/app/components/address/address.component.ts index 95abe4ac1..19712a702 100644 --- a/frontend/src/app/components/address/address.component.ts +++ b/frontend/src/app/components/address/address.component.ts @@ -38,6 +38,8 @@ export class AddressComponent implements OnInit, OnDestroy { txCount = 0; received = 0; sent = 0; + now = Date.now() / 1000; + balancePeriod: 'all' | '1m' = 'all'; private tempTransactions: Transaction[]; private timeTxIndexes: number[]; @@ -174,6 +176,10 @@ export class AddressComponent implements OnInit, OnDestroy { this.transactions = this.tempTransactions; this.isLoadingTransactions = false; + + if (!this.showBalancePeriod()) { + this.setBalancePeriod('all'); + } }, (error) => { console.log(error); @@ -296,6 +302,18 @@ export class AddressComponent implements OnInit, OnDestroy { this.txCount = this.address.chain_stats.tx_count + this.address.mempool_stats.tx_count; } + setBalancePeriod(period: 'all' | '1m'): boolean { + this.balancePeriod = period; + return false; + } + + showBalancePeriod(): boolean { + return this.transactions?.length && ( + !this.transactions[0].status?.confirmed + || this.transactions[0].status.block_time > (this.now - (60 * 60 * 24 * 30)) + ); + } + ngOnDestroy() { this.mainSubscription.unsubscribe(); this.mempoolTxSubscription.unsubscribe(); From bf541f08984cd2b72839229acf18960f2763c56f Mon Sep 17 00:00:00 2001 From: hunicus <93150691+hunicus@users.noreply.github.com> Date: Mon, 29 Apr 2024 08:07:09 -0700 Subject: [PATCH 07/62] Replace bisq with onbtc for about alliances --- .../app/components/about/about.component.html | 4 +- .../app/components/about/about.component.scss | 5 +- frontend/src/resources/profile/onbtc-full.svg | 334 ++++++++++++++++++ 3 files changed, 339 insertions(+), 4 deletions(-) create mode 100644 frontend/src/resources/profile/onbtc-full.svg diff --git a/frontend/src/app/components/about/about.component.html b/frontend/src/app/components/about/about.component.html index 5185c9d01..59c3bfb4a 100644 --- a/frontend/src/app/components/about/about.component.html +++ b/frontend/src/app/components/about/about.component.html @@ -343,8 +343,8 @@ - - + +
diff --git a/frontend/src/app/components/about/about.component.scss b/frontend/src/app/components/about/about.component.scss index 81fcfbbd8..d1c15f838 100644 --- a/frontend/src/app/components/about/about.component.scss +++ b/frontend/src/app/components/about/about.component.scss @@ -129,8 +129,9 @@ position: relative; width: 300px; } - .bisq { - top: 3px; + .sv { + height: 85px; + width: auto; position: relative; } } diff --git a/frontend/src/resources/profile/onbtc-full.svg b/frontend/src/resources/profile/onbtc-full.svg new file mode 100644 index 000000000..1c189ea6d --- /dev/null +++ b/frontend/src/resources/profile/onbtc-full.svg @@ -0,0 +1,334 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From f83404421aeadc076006ff1a976190194a2ccba6 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Wed, 1 May 2024 23:40:39 +0000 Subject: [PATCH 08/62] Add x widget --- frontend/custom-sv-config.json | 12 +++- .../custom-dashboard.component.html | 36 +++++++--- .../custom-dashboard.component.ts | 2 + .../twitter-widget.component.html | 16 +++++ .../twitter-widget.component.scss | 10 +++ .../twitter-widget.component.ts | 65 +++++++++++++++++++ frontend/src/app/services/state.service.ts | 1 + frontend/src/app/shared/shared.module.ts | 3 + 8 files changed, 133 insertions(+), 12 deletions(-) create mode 100644 frontend/src/app/components/twitter-widget/twitter-widget.component.html create mode 100644 frontend/src/app/components/twitter-widget/twitter-widget.component.scss create mode 100644 frontend/src/app/components/twitter-widget/twitter-widget.component.ts diff --git a/frontend/custom-sv-config.json b/frontend/custom-sv-config.json index f64f41be8..0f82a9da2 100644 --- a/frontend/custom-sv-config.json +++ b/frontend/custom-sv-config.json @@ -12,19 +12,26 @@ "dashboard": { "widgets": [ { - "component": "fees" + "component": "fees", + "mobileOrder": 4 }, { "component": "balance", + "mobileOrder": 1, "props": { "address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo" } }, { - "component": "goggles" + "component": "twitter", + "mobileOrder": 5, + "props": { + "handle": "bitcoinofficesv" + } }, { "component": "address", + "mobileOrder": 2, "props": { "address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo", "period": "1m" @@ -35,6 +42,7 @@ }, { "component": "addressTransactions", + "mobileOrder": 3, "props": { "address": "32ixEdVJWo3kmvJGMTZq5jAQVZZeuwnqzo" } diff --git a/frontend/src/app/components/custom-dashboard/custom-dashboard.component.html b/frontend/src/app/components/custom-dashboard/custom-dashboard.component.html index 9180571a0..36a5e956c 100644 --- a/frontend/src/app/components/custom-dashboard/custom-dashboard.component.html +++ b/frontend/src/app/components/custom-dashboard/custom-dashboard.component.html @@ -4,7 +4,7 @@ @for (widget of widgets; track widget.component) { @switch (widget.component) { @case ('fees') { -
+
Transaction Fees
@@ -14,12 +14,12 @@
} @case ('difficulty') { -
+
} @case ('goggles') { -
+
} @case ('incoming') { -
+
@@ -93,7 +93,7 @@ } @case ('replacements') { -
+
@@ -140,7 +140,7 @@ } @case ('blocks') { -
+
@@ -184,7 +184,7 @@ } @case ('transactions') { -
+
Recent Transactions
@@ -224,13 +224,13 @@ } @case ('balance') { -
+
Treasury
} @case ('address') { -
+
} @case ('addressTransactions') { -
+ diff --git a/frontend/src/app/components/custom-dashboard/custom-dashboard.component.ts b/frontend/src/app/components/custom-dashboard/custom-dashboard.component.ts index 2847b6586..2ed6ed48d 100644 --- a/frontend/src/app/components/custom-dashboard/custom-dashboard.component.ts +++ b/frontend/src/app/components/custom-dashboard/custom-dashboard.component.ts @@ -57,6 +57,7 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni incomingGraphHeight: number = 300; graphHeight: number = 300; webGlEnabled = true; + isMobile: boolean = window.innerWidth <= 767.98; widgets; @@ -368,5 +369,6 @@ export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewIni this.goggleResolution = 86; this.graphHeight = 310; } + this.isMobile = window.innerWidth <= 767.98; } } diff --git a/frontend/src/app/components/twitter-widget/twitter-widget.component.html b/frontend/src/app/components/twitter-widget/twitter-widget.component.html new file mode 100644 index 000000000..d1e042d60 --- /dev/null +++ b/frontend/src/app/components/twitter-widget/twitter-widget.component.html @@ -0,0 +1,16 @@ +@if (loading) { +
+
+
+} @else if (error) { +
+ failed to load X timeline +
+} + + diff --git a/frontend/src/app/components/twitter-widget/twitter-widget.component.scss b/frontend/src/app/components/twitter-widget/twitter-widget.component.scss new file mode 100644 index 000000000..38a39c014 --- /dev/null +++ b/frontend/src/app/components/twitter-widget/twitter-widget.component.scss @@ -0,0 +1,10 @@ +.spinner-wrapper, .error-wrapper { + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + display: flex; + justify-content: center; + align-items: center; +} \ No newline at end of file diff --git a/frontend/src/app/components/twitter-widget/twitter-widget.component.ts b/frontend/src/app/components/twitter-widget/twitter-widget.component.ts new file mode 100644 index 000000000..7ec865de7 --- /dev/null +++ b/frontend/src/app/components/twitter-widget/twitter-widget.component.ts @@ -0,0 +1,65 @@ +import { Component, Input, ChangeDetectionStrategy, SecurityContext } from '@angular/core'; +import { LanguageService } from '../../services/language.service'; +import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser'; + +@Component({ + selector: 'app-twitter-widget', + templateUrl: './twitter-widget.component.html', + styleUrls: ['./twitter-widget.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class TwitterWidgetComponent { + @Input() handle: string; + @Input() width = 300; + @Input() height = 400; + + loading: boolean = true; + error: boolean = false; + lang: string = 'en'; + + iframeSrc: SafeResourceUrl; + + constructor( + private languageService: LanguageService, + public sanitizer: DomSanitizer, + ) { + this.lang = this.languageService.getLanguage(); + this.setIframeSrc(); + } + + setIframeSrc(): void { + this.iframeSrc = this.sanitizer.bypassSecurityTrustResourceUrl(this.sanitizer.sanitize(SecurityContext.URL, + 'https://syndication.twitter.com/srv/timeline-profile/screen-name/bitcoinofficesv?creatorScreenName=mempool' + + '&dnt=true' + + '&embedId=twitter-widget-0' + + '&features=eyJ0ZndfdGltZWxpbmVfbGlzdCI6eyJidWNrZXQiOltdLCJ2ZXJzaW9uIjpudWxsfSwidGZ3X2ZvbGxvd2VyX2NvdW50X3N1bnNldCI6eyJidWNrZXQiOnRydWUsInZlcnNpb24iOm51bGx9LCJ0ZndfdHdlZXRfZWRpdF9iYWNrZW5kIjp7ImJ1Y2tldCI6Im9uIiwidmVyc2lvbiI6bnVsbH0sInRmd19yZWZzcmNfc2Vzc2lvbiI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9LCJ0ZndfZm9zbnJfc29mdF9pbnRlcnZlbnRpb25zX2VuYWJsZWQiOnsiYnVja2V0Ijoib24iLCJ2ZXJzaW9uIjpudWxsfSwidGZ3X21peGVkX21lZGlhXzE1ODk3Ijp7ImJ1Y2tldCI6InRyZWF0bWVudCIsInZlcnNpb24iOm51bGx9LCJ0ZndfZXhwZXJpbWVudHNfY29va2llX2V4cGlyYXRpb24iOnsiYnVja2V0IjoxMjA5NjAwLCJ2ZXJzaW9uIjpudWxsfSwidGZ3X3Nob3dfYmlyZHdhdGNoX3Bpdm90c19lbmFibGVkIjp7ImJ1Y2tldCI6Im9uIiwidmVyc2lvbiI6bnVsbH0sInRmd19kdXBsaWNhdGVfc2NyaWJlc190b19zZXR0aW5ncyI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9LCJ0ZndfdXNlX3Byb2ZpbGVfaW1hZ2Vfc2hhcGVfZW5hYmxlZCI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9LCJ0ZndfdmlkZW9faGxzX2R5bmFtaWNfbWFuaWZlc3RzXzE1MDgyIjp7ImJ1Y2tldCI6InRydWVfYml0cmF0ZSIsInZlcnNpb24iOm51bGx9LCJ0ZndfbGVnYWN5X3RpbWVsaW5lX3N1bnNldCI6eyJidWNrZXQiOnRydWUsInZlcnNpb24iOm51bGx9LCJ0ZndfdHdlZXRfZWRpdF9mcm9udGVuZCI6eyJidWNrZXQiOiJvbiIsInZlcnNpb24iOm51bGx9fQ%3D%3D' + + '&frame=false' + + '&hideBorder=true' + + '&hideFooter=false' + + '&hideHeader=true' + + '&hideScrollBar=false' + + `&lang=${this.lang}` + + '&maxHeight=500px' + + '&origin=https%3A%2F%2Fmempool.space%2F' + // + '&sessionId=88f6d661d0dcca99c43c0a590f6a3e61c89226a9' + + '&showHeader=false' + + '&showReplies=false' + + '&siteScreenName=mempool' + + '&theme=dark' + + '&transparent=true' + + '&widgetsVersion=2615f7e52b7e0%3A1702314776716' + )); + } + + onReady(): void { + console.log('ready!'); + this.loading = false; + this.error = false; + } + + onFailed(): void { + console.log('failed!') + this.loading = false; + this.error = true; + } +} \ No newline at end of file diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts index 286ae5e48..529e53ff0 100644 --- a/frontend/src/app/services/state.service.ts +++ b/frontend/src/app/services/state.service.ts @@ -33,6 +33,7 @@ export interface Customization { dashboard: { widgets: { component: string; + mobileOrder?: number; props: { [key: string]: any }; }[]; }; diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts index 80d6ca3cd..7f52a1b60 100644 --- a/frontend/src/app/shared/shared.module.ts +++ b/frontend/src/app/shared/shared.module.ts @@ -112,6 +112,7 @@ import { ClockComponent } from '../components/clock/clock.component'; import { CalculatorComponent } from '../components/calculator/calculator.component'; import { BitcoinsatoshisPipe } from '../shared/pipes/bitcoinsatoshis.pipe'; import { HttpErrorComponent } from '../shared/components/http-error/http-error.component'; +import { TwitterWidgetComponent } from '../components/twitter-widget/twitter-widget.component'; import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-directives/weight-directives'; @@ -224,6 +225,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir AccelerateCheckout, PendingStatsComponent, HttpErrorComponent, + TwitterWidgetComponent, ], imports: [ CommonModule, @@ -351,6 +353,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir AccelerateCheckout, PendingStatsComponent, HttpErrorComponent, + TwitterWidgetComponent, MempoolBlockOverviewComponent, ClockchainComponent, From a676d23a54dce903c1a7244899206240b72b2641 Mon Sep 17 00:00:00 2001 From: Matt Corallo Date: Thu, 2 May 2024 21:00:49 +0000 Subject: [PATCH 09/62] Restore MSRV to 1.63 In trying to upgrade my mempool instance, I discovered I couldn't build the latest mempool Rust code with my available Rust toolchain. It appears in #4612 the Rust MSRV was bumped without justification, which is reverted here. Note that `Cargo.lock` updates here should ensure the versions of dependent crates use the versions supported by our MSRV. Its possible that the dependency downgrades here break something, but things appear to be running fine for me locally, so figured I'd suggest this upstream. --- rust/gbt/Cargo.lock | 120 +++++++++++++--------------------------- rust/gbt/Cargo.toml | 6 +- rust/gbt/rust-toolchain | 2 +- 3 files changed, 41 insertions(+), 87 deletions(-) diff --git a/rust/gbt/Cargo.lock b/rust/gbt/Cargo.lock index d7ac48ec7..ed0b1c783 100644 --- a/rust/gbt/Cargo.lock +++ b/rust/gbt/Cargo.lock @@ -93,7 +93,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad291aa74992b9b7a7e88c38acbbf6ad7e107f1d90ee8775b7bc1fc3394f485c" dependencies = [ "quote", - "syn", + "syn 2.0.52", ] [[package]] @@ -159,12 +159,12 @@ checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libloading" -version = "0.8.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" dependencies = [ "cfg-if", - "windows-targets", + "winapi", ] [[package]] @@ -199,9 +199,9 @@ dependencies = [ [[package]] name = "napi" -version = "2.16.0" +version = "2.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a63d0570e4c3e0daf7a8d380563610e159f538e20448d6c911337246f40e84" +checksum = "fd063c93b900149304e3ba96ce5bf210cd4f81ef5eb80ded0d100df3e85a3ac0" dependencies = [ "bitflags", "ctor", @@ -213,29 +213,29 @@ dependencies = [ [[package]] name = "napi-build" -version = "2.1.2" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f9130fccc5f763cf2069b34a089a18f0d0883c66aceb81f2fad541a3d823c43" +checksum = "882a73d9ef23e8dc2ebbffb6a6ae2ef467c0f18ac10711e4cc59c5485d41df0e" [[package]] name = "napi-derive" -version = "2.16.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05bb7c37e3c1dda9312fdbe4a9fc7507fca72288ba154ec093e2d49114e727ce" +checksum = "da1c6a8fa84d549aa8708fcd062372bf8ec6e849de39016ab921067d21bde367" dependencies = [ "cfg-if", "convert_case", "napi-derive-backend", "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] name = "napi-derive-backend" -version = "1.0.62" +version = "1.0.52" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f785a8b8d7b83e925f5aa6d2ae3c159d17fe137ac368dc185bef410e7acdaeb4" +checksum = "20bbc7c69168d06a848f925ec5f0e0997f98e8c8d4f2cc30157f0da51c009e17" dependencies = [ "convert_case", "once_cell", @@ -243,14 +243,14 @@ dependencies = [ "quote", "regex", "semver", - "syn", + "syn 1.0.109", ] [[package]] name = "napi-sys" -version = "2.3.0" +version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2503fa6af34dc83fb74888df8b22afe933b58d37daf7d80424b1c60c68196b8b" +checksum = "166b5ef52a3ab5575047a9fe8d4a030cdd0f63c96f071cd6907674453b07bae3" dependencies = [ "libloading", ] @@ -333,14 +333,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.3" +version = "1.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +checksum = "ebee201405406dbf528b8b672104ae6d6d63e6d118cb10e4d51abbc7b58044ff" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.6", - "regex-syntax 0.8.2", + "regex-automata 0.3.9", + "regex-syntax 0.7.5", ] [[package]] @@ -354,13 +354,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.6" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.2", + "regex-syntax 0.7.5", ] [[package]] @@ -371,9 +371,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] name = "rustc-demangle" @@ -402,6 +402,17 @@ version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.52" @@ -453,7 +464,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.52", ] [[package]] @@ -534,60 +545,3 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-targets" -version = "0.52.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" diff --git a/rust/gbt/Cargo.toml b/rust/gbt/Cargo.toml index c6e736762..898912881 100644 --- a/rust/gbt/Cargo.toml +++ b/rust/gbt/Cargo.toml @@ -14,15 +14,15 @@ crate-type = ["cdylib"] [dependencies] priority-queue = "2.0.2" bytes = "1.4.0" -napi = { version = "2.16.0", features = ["napi8", "tokio_rt"] } -napi-derive = "2.16.0" +napi = { version = "2.0", features = ["napi8", "tokio_rt"] } +napi-derive = "2.0" bytemuck = "1.13.1" tracing = "0.1.36" tracing-log = "0.2.0" tracing-subscriber = { version = "0.3.15", features = ["env-filter"]} [build-dependencies] -napi-build = "2.1.2" +napi-build = "2.0" [profile.release] lto = true diff --git a/rust/gbt/rust-toolchain b/rust/gbt/rust-toolchain index 5b6cd6b3c..58e4eb6b2 100644 --- a/rust/gbt/rust-toolchain +++ b/rust/gbt/rust-toolchain @@ -1 +1 @@ -1.65 +1.63 From f8d30bf528100e7e11b19b852482a357ae10b44f Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sun, 21 Jan 2024 22:47:41 +0000 Subject: [PATCH 10/62] Add 'mempool delta' websocket subscriptions --- backend/src/api/websocket-handler.ts | 69 ++++++++++++++++++++++++++++ backend/src/mempool.interfaces.ts | 14 ++++++ 2 files changed, 83 insertions(+) diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index d4ff7efe3..9e8a4653a 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -3,6 +3,7 @@ import * as WebSocket from 'ws'; import { BlockExtended, TransactionExtended, MempoolTransactionExtended, WebsocketResponse, OptimizedStatistic, ILoadingIndicators, GbtCandidates, TxTrackingInfo, + MempoolBlockDelta, MempoolDelta, MempoolDeltaTxids } from '../mempool.interfaces'; import blocks from './blocks'; import memPool from './mempool'; @@ -364,6 +365,18 @@ class WebsocketHandler { client['track-donation'] = parsedMessage['track-donation']; } + if (parsedMessage['track-mempool-txids'] === true) { + client['track-mempool-txids'] = true; + } else if (parsedMessage['track-mempool-txids'] === false) { + delete client['track-mempool-txids']; + } + + if (parsedMessage['track-mempool'] === true) { + client['track-mempool'] = true; + } else if (parsedMessage['track-mempool'] === false) { + delete client['track-mempool']; + } + if (Object.keys(response).length) { client.send(this.serializeResponse(response)); } @@ -545,6 +558,27 @@ class WebsocketHandler { const latestTransactions = memPool.getLatestTransactions(); + const replacedTransactions: { replaced: string, by: TransactionExtended }[] = []; + for (const tx of newTransactions) { + if (rbfTransactions[tx.txid]) { + for (const replaced of rbfTransactions[tx.txid]) { + replacedTransactions.push({ replaced: replaced.txid, by: tx }); + } + } + } + const mempoolDeltaTxids: MempoolDeltaTxids = { + added: newTransactions.map(tx => tx.txid), + removed: deletedTransactions.map(tx => tx.txid), + mined: [], + replaced: replacedTransactions.map(replacement => ({ replaced: replacement.replaced, by: replacement.by.txid })), + }; + const mempoolDelta: MempoolDelta = { + added: newTransactions, + removed: deletedTransactions.map(tx => tx.txid), + mined: [], + replaced: replacedTransactions, + }; + // update init data const socketDataFields = { 'mempoolInfo': mempoolInfo, @@ -847,6 +881,14 @@ class WebsocketHandler { response['rbfLatestSummary'] = getCachedResponse('rbfLatestSummary', rbfSummary); } + if (client['track-mempool-txids']) { + response['mempool-txids'] = getCachedResponse('mempool-txids', mempoolDeltaTxids); + } + + if (client['track-mempool']) { + response['mempool-transactions'] = getCachedResponse('mempool-transactions', mempoolDelta); + } + if (Object.keys(response).length) { client.send(this.serializeResponse(response)); } @@ -992,6 +1034,25 @@ class WebsocketHandler { const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions(); + const replacedTransactions: { replaced: string, by: TransactionExtended }[] = []; + for (const txid of Object.keys(rbfTransactions)) { + for (const replaced of rbfTransactions[txid].replaced) { + replacedTransactions.push({ replaced: replaced.txid, by: rbfTransactions[txid].replacedBy }); + } + } + const mempoolDeltaTxids: MempoolDeltaTxids = { + added: [], + removed: [], + mined: transactions.map(tx => tx.txid), + replaced: replacedTransactions.map(replacement => ({ replaced: replacement.replaced, by: replacement.by.txid })), + }; + const mempoolDelta: MempoolDelta = { + added: [], + removed: [], + mined: transactions.map(tx => tx.txid), + replaced: replacedTransactions, + }; + const responseCache = { ...this.socketData }; function getCachedResponse(key, data): string { if (!responseCache[key]) { @@ -1185,6 +1246,14 @@ class WebsocketHandler { } } + if (client['track-mempool-txids']) { + response['mempool-txids'] = getCachedResponse('mempool-txids', mempoolDeltaTxids); + } + + if (client['track-mempool']) { + response['mempool-transactions'] = getCachedResponse('mempool-transactions', mempoolDelta); + } + if (Object.keys(response).length) { client.send(this.serializeResponse(response)); } diff --git a/backend/src/mempool.interfaces.ts b/backend/src/mempool.interfaces.ts index 0b4b20e02..516748e9c 100644 --- a/backend/src/mempool.interfaces.ts +++ b/backend/src/mempool.interfaces.ts @@ -71,6 +71,20 @@ export interface MempoolBlockDelta { changed: MempoolDeltaChange[]; } +export interface MempoolDeltaTxids { + added: string[]; + removed: string[]; + mined: string[]; + replaced: { replaced: string, by: string }[]; +} + +export interface MempoolDelta { + added: MempoolTransactionExtended[]; + removed: string[]; + mined: string[]; + replaced: { replaced: string, by: TransactionExtended }[]; +} + interface VinStrippedToScriptsig { scriptsig: string; } From 5172f032e709e4e6cf7afa7f6e50131dbae2d809 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Fri, 3 May 2024 16:31:56 +0000 Subject: [PATCH 11/62] Add sequence number to track-mempool subscription messages --- backend/src/api/websocket-handler.ts | 20 ++++++++++++-------- backend/src/mempool.interfaces.ts | 2 ++ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index 9e8a4653a..fdda3df88 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -558,6 +558,10 @@ class WebsocketHandler { const latestTransactions = memPool.getLatestTransactions(); + if (memPool.isInSync()) { + this.mempoolSequence++; + } + const replacedTransactions: { replaced: string, by: TransactionExtended }[] = []; for (const tx of newTransactions) { if (rbfTransactions[tx.txid]) { @@ -567,12 +571,14 @@ class WebsocketHandler { } } const mempoolDeltaTxids: MempoolDeltaTxids = { + sequence: this.mempoolSequence, added: newTransactions.map(tx => tx.txid), removed: deletedTransactions.map(tx => tx.txid), mined: [], replaced: replacedTransactions.map(replacement => ({ replaced: replacement.replaced, by: replacement.by.txid })), }; const mempoolDelta: MempoolDelta = { + sequence: this.mempoolSequence, added: newTransactions, removed: deletedTransactions.map(tx => tx.txid), mined: [], @@ -638,10 +644,6 @@ class WebsocketHandler { const addressCache = this.makeAddressCache(newTransactions); const removedAddressCache = this.makeAddressCache(deletedTransactions); - if (memPool.isInSync()) { - this.mempoolSequence++; - } - // TODO - Fix indentation after PR is merged for (const server of this.webSocketServers) { server.clients.forEach(async (client) => { @@ -1034,6 +1036,10 @@ class WebsocketHandler { const mBlocksWithTransactions = mempoolBlocks.getMempoolBlocksWithTransactions(); + if (memPool.isInSync()) { + this.mempoolSequence++; + } + const replacedTransactions: { replaced: string, by: TransactionExtended }[] = []; for (const txid of Object.keys(rbfTransactions)) { for (const replaced of rbfTransactions[txid].replaced) { @@ -1041,12 +1047,14 @@ class WebsocketHandler { } } const mempoolDeltaTxids: MempoolDeltaTxids = { + sequence: this.mempoolSequence, added: [], removed: [], mined: transactions.map(tx => tx.txid), replaced: replacedTransactions.map(replacement => ({ replaced: replacement.replaced, by: replacement.by.txid })), }; const mempoolDelta: MempoolDelta = { + sequence: this.mempoolSequence, added: [], removed: [], mined: transactions.map(tx => tx.txid), @@ -1061,10 +1069,6 @@ class WebsocketHandler { return responseCache[key]; } - if (memPool.isInSync()) { - this.mempoolSequence++; - } - // TODO - Fix indentation after PR is merged for (const server of this.webSocketServers) { server.clients.forEach((client) => { diff --git a/backend/src/mempool.interfaces.ts b/backend/src/mempool.interfaces.ts index 516748e9c..0fcddc45a 100644 --- a/backend/src/mempool.interfaces.ts +++ b/backend/src/mempool.interfaces.ts @@ -72,6 +72,7 @@ export interface MempoolBlockDelta { } export interface MempoolDeltaTxids { + sequence: number, added: string[]; removed: string[]; mined: string[]; @@ -79,6 +80,7 @@ export interface MempoolDeltaTxids { } export interface MempoolDelta { + sequence: number, added: MempoolTransactionExtended[]; removed: string[]; mined: string[]; From a8868b5f0f05ab4864ae6000df5692f9a679e27c Mon Sep 17 00:00:00 2001 From: Mononaut Date: Thu, 2 May 2024 23:48:10 +0000 Subject: [PATCH 12/62] Simplify websocket mempool delta handling --- .../mempool-block-overview.component.ts | 46 +++---------------- .../src/app/interfaces/websocket.interface.ts | 10 ++++ frontend/src/app/services/state.service.ts | 31 ++++++------- .../src/app/services/websocket.service.ts | 6 ++- 4 files changed, 36 insertions(+), 57 deletions(-) diff --git a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts index 4d01bd9b9..9971e92ab 100644 --- a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts +++ b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts @@ -1,11 +1,10 @@ -import { Component, ComponentRef, ViewChild, HostListener, Input, Output, EventEmitter, +import { Component, ViewChild, Input, Output, EventEmitter, OnInit, OnDestroy, OnChanges, ChangeDetectionStrategy, ChangeDetectorRef, AfterViewInit } from '@angular/core'; import { StateService } from '../../services/state.service'; -import { MempoolBlockDelta } from '../../interfaces/websocket.interface'; +import { MempoolBlockDelta, isMempoolDelta } from '../../interfaces/websocket.interface'; import { TransactionStripped } from '../../interfaces/node-api.interface'; import { BlockOverviewGraphComponent } from '../../components/block-overview-graph/block-overview-graph.component'; -import { Subscription, BehaviorSubject, merge, of, timer } from 'rxjs'; -import { switchMap, filter, concatMap, map } from 'rxjs/operators'; +import { Subscription, BehaviorSubject } from 'rxjs'; import { WebsocketService } from '../../services/websocket.service'; import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe'; import { Router } from '@angular/router'; @@ -39,10 +38,6 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang poolDirection: string = 'left'; blockSub: Subscription; - rateLimit = 1000; - private lastEventTime = Date.now() - this.rateLimit; - private subId = 0; - firstLoad: boolean = true; constructor( @@ -62,39 +57,13 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang } ngAfterViewInit(): void { - this.blockSub = merge( - this.stateService.mempoolBlockTransactions$, - this.stateService.mempoolBlockDelta$, - ).pipe( - concatMap(update => { - const now = Date.now(); - const timeSinceLastEvent = now - this.lastEventTime; - this.lastEventTime = Math.max(now, this.lastEventTime + this.rateLimit); - - const subId = this.subId; - - // If time since last event is less than X seconds, delay this event - if (timeSinceLastEvent < this.rateLimit) { - return timer(this.rateLimit - timeSinceLastEvent).pipe( - // Emit the event after the timer - map(() => ({ update, subId })) - ); - } else { - // If enough time has passed, emit the event immediately - return of({ update, subId }); - } - }) - ).subscribe(({ update, subId }) => { - // discard stale updates after a block transition - if (subId !== this.subId) { - return; - } + this.blockSub = this.stateService.mempoolBlockUpdate$.subscribe((update) => { // process update - if (update['added']) { + if (isMempoolDelta(update)) { // delta - this.updateBlock(update as MempoolBlockDelta); + this.updateBlock(update); } else { - const transactionsStripped = update as TransactionStripped[]; + const transactionsStripped = update.transactions; // new transactions if (this.firstLoad) { this.replaceBlock(transactionsStripped); @@ -137,7 +106,6 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang ngOnChanges(changes): void { if (changes.index) { - this.subId++; this.firstLoad = true; if (this.blockGraph) { this.blockGraph.clear(changes.index.currentValue > changes.index.previousValue ? this.chainDirection : this.poolDirection); diff --git a/frontend/src/app/interfaces/websocket.interface.ts b/frontend/src/app/interfaces/websocket.interface.ts index daf06603f..8c24979e7 100644 --- a/frontend/src/app/interfaces/websocket.interface.ts +++ b/frontend/src/app/interfaces/websocket.interface.ts @@ -75,6 +75,16 @@ export interface MempoolBlockDelta { removed: string[]; changed: { txid: string, rate: number, flags: number, acc: boolean }[]; } +export interface MempoolBlockState { + transactions: TransactionStripped[]; +} +export type MempoolBlockUpdate = MempoolBlockDelta | MempoolBlockState; +export function isMempoolState(update: MempoolBlockUpdate): update is MempoolBlockState { + return update['transactions'] !== undefined; +} +export function isMempoolDelta(update: MempoolBlockUpdate): update is MempoolBlockDelta { + return update['transactions'] === undefined; +} export interface MempoolBlockDeltaCompressed { added: TransactionCompressed[]; diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts index 286ae5e48..3554e465e 100644 --- a/frontend/src/app/services/state.service.ts +++ b/frontend/src/app/services/state.service.ts @@ -1,7 +1,7 @@ import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core'; import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable, merge } from 'rxjs'; import { Transaction } from '../interfaces/electrs.interface'; -import { HealthCheckHost, IBackendInfo, MempoolBlock, MempoolBlockDelta, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo } from '../interfaces/websocket.interface'; +import { HealthCheckHost, IBackendInfo, MempoolBlock, MempoolBlockDelta, MempoolBlockUpdate, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo, isMempoolState } from '../interfaces/websocket.interface'; import { BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree, TransactionStripped } from '../interfaces/node-api.interface'; import { Router, NavigationStart } from '@angular/router'; import { isPlatformBrowser } from '@angular/common'; @@ -127,8 +127,7 @@ export class StateService { bsqPrice$ = new ReplaySubject(1); mempoolInfo$ = new ReplaySubject(1); mempoolBlocks$ = new ReplaySubject(1); - mempoolBlockTransactions$ = new Subject(); - mempoolBlockDelta$ = new Subject(); + mempoolBlockUpdate$ = new Subject(); liveMempoolBlockTransactions$: Observable<{ [txid: string]: TransactionStripped}>; txConfirmed$ = new Subject<[string, BlockExtended]>(); txReplaced$ = new Subject(); @@ -215,25 +214,25 @@ export class StateService { this.router.navigate(['/tracker/' + window.location.pathname.slice(4)]); } - this.liveMempoolBlockTransactions$ = merge( - this.mempoolBlockTransactions$.pipe(map(transactions => { return { transactions }; })), - this.mempoolBlockDelta$.pipe(map(delta => { return { delta }; })), - ).pipe(scan((transactions: { [txid: string]: TransactionStripped }, change: any): { [txid: string]: TransactionStripped } => { - if (change.transactions) { - const txMap = {} + this.liveMempoolBlockTransactions$ = this.mempoolBlockUpdate$.pipe(scan((transactions: { [txid: string]: TransactionStripped }, change: MempoolBlockUpdate): { [txid: string]: TransactionStripped } => { + if (isMempoolState(change)) { + const txMap = {}; change.transactions.forEach(tx => { txMap[tx.txid] = tx; - }) + }); return txMap; } else { - change.delta.changed.forEach(tx => { - transactions[tx.txid].rate = tx.rate; - }) - change.delta.removed.forEach(txid => { + change.added.forEach(tx => { + transactions[tx.txid] = tx; + }); + change.removed.forEach(txid => { delete transactions[txid]; }); - change.delta.added.forEach(tx => { - transactions[tx.txid] = tx; + change.changed.forEach(tx => { + if (transactions[tx.txid]) { + transactions[tx.txid].rate = tx.rate; + transactions[tx.txid].acc = tx.acc; + } }); return transactions; } diff --git a/frontend/src/app/services/websocket.service.ts b/frontend/src/app/services/websocket.service.ts index 414f60bc5..e4df12aa6 100644 --- a/frontend/src/app/services/websocket.service.ts +++ b/frontend/src/app/services/websocket.service.ts @@ -401,14 +401,16 @@ export class WebsocketService { if (response['projected-block-transactions'].index == this.trackingMempoolBlock) { if (response['projected-block-transactions'].blockTransactions) { this.stateService.mempoolSequence = response['projected-block-transactions'].sequence; - this.stateService.mempoolBlockTransactions$.next(response['projected-block-transactions'].blockTransactions.map(uncompressTx)); + this.stateService.mempoolBlockUpdate$.next({ + transactions: response['projected-block-transactions'].blockTransactions.map(uncompressTx), + }); } else if (response['projected-block-transactions'].delta) { if (this.stateService.mempoolSequence && response['projected-block-transactions'].sequence !== this.stateService.mempoolSequence + 1) { this.stateService.mempoolSequence = 0; this.startTrackMempoolBlock(this.trackingMempoolBlock, true); } else { this.stateService.mempoolSequence = response['projected-block-transactions'].sequence; - this.stateService.mempoolBlockDelta$.next(uncompressDeltaChange(response['projected-block-transactions'].delta)); + this.stateService.mempoolBlockUpdate$.next(uncompressDeltaChange(response['projected-block-transactions'].delta)); } } } From 44116424b046a0f5bd45a7625dc7651b73c68bbf Mon Sep 17 00:00:00 2001 From: Mononaut Date: Thu, 2 May 2024 23:48:56 +0000 Subject: [PATCH 13/62] Reimplement mempool animation smoothing within viz component --- .../block-overview-graph.component.ts | 68 +++++++++++++++++++ .../block-overview-graph/block-scene.ts | 6 +- .../mempool-block-overview.component.ts | 6 +- 3 files changed, 76 insertions(+), 4 deletions(-) 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 3fee3f901..57db9bfca 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 @@ -81,6 +81,20 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On tooltipPosition: Position; readyNextFrame = false; + lastUpdate: number = 0; + pendingUpdate: { + count: number, + add: { [txid: string]: TransactionStripped }, + remove: { [txid: string]: string }, + change: { [txid: string]: { txid: string, rate: number | undefined, acc: boolean | undefined } }, + direction?: string, + } = { + count: 0, + add: {}, + remove: {}, + change: {}, + direction: 'left', + }; searchText: string; searchSubscription: Subscription; @@ -176,6 +190,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On destroy(): void { if (this.scene) { this.scene.destroy(); + this.clearUpdateQueue(); this.start(); } } @@ -188,6 +203,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On } this.filtersAvailable = filtersAvailable; if (this.scene) { + this.clearUpdateQueue(); this.scene.setup(transactions); this.readyNextFrame = true; this.start(); @@ -197,6 +213,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On enter(transactions: TransactionStripped[], direction: string): void { if (this.scene) { + this.clearUpdateQueue(); this.scene.enter(transactions, direction); this.start(); this.updateSearchHighlight(); @@ -205,6 +222,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On exit(direction: string): void { if (this.scene) { + this.clearUpdateQueue(); this.scene.exit(direction); this.start(); this.updateSearchHighlight(); @@ -213,13 +231,61 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On replace(transactions: TransactionStripped[], direction: string, sort: boolean = true, startTime?: number): void { if (this.scene) { + this.clearUpdateQueue(); this.scene.replace(transactions || [], direction, sort, startTime); this.start(); this.updateSearchHighlight(); } } + // collates non-urgent updates into a set of consistent pending changes + queueUpdate(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left'): void { + for (const tx of add) { + this.pendingUpdate.add[tx.txid] = tx; + delete this.pendingUpdate.remove[tx.txid]; + delete this.pendingUpdate.change[tx.txid]; + } + for (const txid of remove) { + delete this.pendingUpdate.add[txid]; + this.pendingUpdate.remove[txid] = txid; + delete this.pendingUpdate.change[txid]; + } + for (const tx of change) { + if (this.pendingUpdate.add[tx.txid]) { + this.pendingUpdate.add[tx.txid].rate = tx.rate; + this.pendingUpdate.add[tx.txid].acc = tx.acc; + } else { + this.pendingUpdate.change[tx.txid] = tx; + } + } + this.pendingUpdate.direction = direction; + this.pendingUpdate.count++; + } + + applyQueuedUpdates(): void { + if (this.pendingUpdate.count && performance.now() > (this.lastUpdate + this.animationDuration)) { + this.update([], [], [], this.pendingUpdate?.direction); + } + } + + clearUpdateQueue(): void { + this.pendingUpdate = { + count: 0, + add: {}, + remove: {}, + change: {}, + }; + this.lastUpdate = performance.now(); + } + update(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left', resetLayout: boolean = false): void { + // merge any pending changes into this update + this.queueUpdate(add, remove, change); + this.applyUpdate(Object.values(this.pendingUpdate.add), Object.values(this.pendingUpdate.remove), Object.values(this.pendingUpdate.change), direction, resetLayout); + this.clearUpdateQueue(); + } + + applyUpdate(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left', resetLayout: boolean = false): void { if (this.scene) { add = add.filter(tx => !this.scene.txs[tx.txid]); remove = remove.filter(txid => this.scene.txs[txid]); @@ -230,6 +296,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On } this.scene.update(add, remove, change, direction, resetLayout); this.start(); + this.lastUpdate = performance.now(); this.updateSearchHighlight(); } } @@ -370,6 +437,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On if (!now) { now = performance.now(); } + this.applyQueuedUpdates(); // skip re-render if there's no change to the scene if (this.scene && this.gl) { /* SET UP SHADER UNIFORMS */ 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 bef907a7a..9dd76dec9 100644 --- a/frontend/src/app/components/block-overview-graph/block-scene.ts +++ b/frontend/src/app/components/block-overview-graph/block-scene.ts @@ -13,7 +13,7 @@ export default class BlockScene { theme: ThemeService; orientation: string; flip: boolean; - animationDuration: number = 900; + animationDuration: number = 1000; configAnimationOffset: number | null; animationOffset: number; highlightingEnabled: boolean; @@ -179,7 +179,7 @@ export default class BlockScene { removed.forEach(tx => { tx.destroy(); }); - }, 1000); + }, (startTime - performance.now()) + this.animationDuration + 1000); if (resetLayout) { add.forEach(tx => { @@ -239,7 +239,7 @@ export default class BlockScene { { width: number, height: number, resolution: number, blockLimit: number, animationDuration: number, animationOffset: number, orientation: string, flip: boolean, vertexArray: FastVertexArray, theme: ThemeService, highlighting: boolean, colorFunction: ((tx: TxView) => Color) | null } ): void { - this.animationDuration = animationDuration || 1000; + this.animationDuration = animationDuration || this.animationDuration || 1000; this.configAnimationOffset = animationOffset; this.animationOffset = this.configAnimationOffset == null ? (this.width * 1.4) : this.configAnimationOffset; this.orientation = orientation; diff --git a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts index 9971e92ab..3cea7e123 100644 --- a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts +++ b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts @@ -141,7 +141,11 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang const direction = (this.blockIndex == null || this.index < this.blockIndex) ? this.poolDirection : this.chainDirection; this.blockGraph.replace(delta.added, direction); } else { - this.blockGraph.update(delta.added, delta.removed, delta.changed || [], blockMined ? this.chainDirection : this.poolDirection, blockMined); + if (blockMined) { + this.blockGraph.update(delta.added, delta.removed, delta.changed || [], blockMined ? this.chainDirection : this.poolDirection, blockMined); + } else { + this.blockGraph.queueUpdate(delta.added, delta.removed, delta.changed || [], this.poolDirection); + } } this.lastBlockHeight = this.stateService.latestBlockHeight; From e45a03729b6cbb6d01b898d273f7c59a53f0a390 Mon Sep 17 00:00:00 2001 From: softsimon Date: Sat, 4 May 2024 14:19:26 +0700 Subject: [PATCH 14/62] Adding footer link --- .../test-transactions/test-transactions.component.html | 2 +- .../components/global-footer/global-footer.component.html | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/frontend/src/app/components/test-transactions/test-transactions.component.html b/frontend/src/app/components/test-transactions/test-transactions.component.html index 7f15da2f2..20ba5c4bd 100644 --- a/frontend/src/app/components/test-transactions/test-transactions.component.html +++ b/frontend/src/app/components/test-transactions/test-transactions.component.html @@ -6,7 +6,7 @@
- +
diff --git a/frontend/src/app/shared/components/global-footer/global-footer.component.html b/frontend/src/app/shared/components/global-footer/global-footer.component.html index 7e3d44b06..b1ce52e5e 100644 --- a/frontend/src/app/shared/components/global-footer/global-footer.component.html +++ b/frontend/src/app/shared/components/global-footer/global-footer.component.html @@ -49,6 +49,7 @@

Lightning Explorer

Recent Blocks

Broadcast Transaction

+

Test Transaction

Connect to our Nodes

API Documentation

From 2e49ef38c923227dfea1387f61d92a694bc669a1 Mon Sep 17 00:00:00 2001 From: nymkappa <1612910616@pm.me> Date: Sat, 4 May 2024 10:01:21 +0200 Subject: [PATCH 15/62] [lightning] apply 4991 at node creation as well --- backend/src/api/explorer/nodes.api.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/backend/src/api/explorer/nodes.api.ts b/backend/src/api/explorer/nodes.api.ts index 3c65ee1f8..22c854fcc 100644 --- a/backend/src/api/explorer/nodes.api.ts +++ b/backend/src/api/explorer/nodes.api.ts @@ -666,7 +666,9 @@ class NodesApi { node.last_update = null; } - const sockets = (node.addresses?.map(a => a.addr).join(',')) ?? ''; + const uniqueAddr = [...new Set(node.addresses?.map(a => a.addr))]; + const formattedSockets = (uniqueAddr.join(',')) ?? ''; + const query = `INSERT INTO nodes( public_key, first_seen, @@ -695,13 +697,13 @@ class NodesApi { node.alias, this.aliasToSearchText(node.alias), node.color, - sockets, + formattedSockets, JSON.stringify(node.features), node.last_update, node.alias, this.aliasToSearchText(node.alias), node.color, - sockets, + formattedSockets, JSON.stringify(node.features), ]); } catch (e) { From 56a0d89b888c861e49b91060362fd682abbfbcd5 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sat, 4 May 2024 17:08:38 +0000 Subject: [PATCH 16/62] Improve deferred mempool visualization updates --- .../block-overview-graph.component.ts | 10 ++++++++-- .../mempool-block-overview.component.ts | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) 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 57db9bfca..ceb12738d 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 @@ -238,7 +238,7 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On } } - // collates non-urgent updates into a set of consistent pending changes + // collates deferred updates into a set of consistent pending changes queueUpdate(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left'): void { for (const tx of add) { this.pendingUpdate.add[tx.txid] = tx; @@ -262,9 +262,15 @@ export class BlockOverviewGraphComponent implements AfterViewInit, OnDestroy, On this.pendingUpdate.count++; } + deferredUpdate(add: TransactionStripped[], remove: string[], change: { txid: string, rate: number | undefined, acc: boolean | undefined }[], direction: string = 'left'): void { + this.queueUpdate(add, remove, change, direction); + this.applyQueuedUpdates(); + } + applyQueuedUpdates(): void { if (this.pendingUpdate.count && performance.now() > (this.lastUpdate + this.animationDuration)) { - this.update([], [], [], this.pendingUpdate?.direction); + this.applyUpdate(Object.values(this.pendingUpdate.add), Object.values(this.pendingUpdate.remove), Object.values(this.pendingUpdate.change), this.pendingUpdate.direction); + this.clearUpdateQueue(); } } diff --git a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts index 3cea7e123..2c564882e 100644 --- a/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts +++ b/frontend/src/app/components/mempool-block-overview/mempool-block-overview.component.ts @@ -144,7 +144,7 @@ export class MempoolBlockOverviewComponent implements OnInit, OnDestroy, OnChang if (blockMined) { this.blockGraph.update(delta.added, delta.removed, delta.changed || [], blockMined ? this.chainDirection : this.poolDirection, blockMined); } else { - this.blockGraph.queueUpdate(delta.added, delta.removed, delta.changed || [], this.poolDirection); + this.blockGraph.deferredUpdate(delta.added, delta.removed, delta.changed || [], this.poolDirection); } } From a29b29300e772e9e1f974156b9300c06e39db7c8 Mon Sep 17 00:00:00 2001 From: Mononaut Date: Sat, 4 May 2024 00:18:33 +0000 Subject: [PATCH 17/62] Replace acceleration API polling with websocket --- backend/src/api/mempool.ts | 4 ++ backend/src/api/websocket-handler.ts | 22 ++++++++ .../accelerations-list.component.ts | 15 ++++-- .../accelerator-dashboard.component.ts | 50 +++++++++++-------- .../pending-stats/pending-stats.component.ts | 8 +-- .../src/app/interfaces/websocket.interface.ts | 9 +++- frontend/src/app/services/state.service.ts | 26 ++++++++-- .../src/app/services/websocket.service.ts | 34 +++++++++++++ 8 files changed, 136 insertions(+), 32 deletions(-) diff --git a/backend/src/api/mempool.ts b/backend/src/api/mempool.ts index 176bedddb..c93d51005 100644 --- a/backend/src/api/mempool.ts +++ b/backend/src/api/mempool.ts @@ -404,6 +404,10 @@ class Mempool { const newAccelerationMap: { [txid: string]: Acceleration } = {}; for (const acceleration of newAccelerations) { + // skip transactions we don't know about + if (!this.mempoolCache[acceleration.txid]) { + continue; + } newAccelerationMap[acceleration.txid] = acceleration; if (this.accelerations[acceleration.txid] == null) { // new acceleration diff --git a/backend/src/api/websocket-handler.ts b/backend/src/api/websocket-handler.ts index fdda3df88..f92e6cdfe 100644 --- a/backend/src/api/websocket-handler.ts +++ b/backend/src/api/websocket-handler.ts @@ -347,6 +347,17 @@ class WebsocketHandler { } } + if (parsedMessage && parsedMessage['track-accelerations'] != null) { + if (parsedMessage['track-accelerations']) { + client['track-accelerations'] = true; + response['accelerations'] = JSON.stringify({ + accelerations: Object.values(memPool.getAccelerations()), + }); + } else { + client['track-accelerations'] = false; + } + } + if (parsedMessage.action === 'init') { if (!this.socketData['blocks']?.length || !this.socketData['da'] || !this.socketData['backendInfo'] || !this.socketData['conversions']) { this.updateSocketData(); @@ -537,6 +548,7 @@ class WebsocketHandler { const vBytesPerSecond = memPool.getVBytesPerSecond(); const rbfTransactions = Common.findRbfTransactions(newTransactions, deletedTransactions); const da = difficultyAdjustment.getDifficultyAdjustment(); + const accelerations = memPool.getAccelerations(); memPool.handleRbfTransactions(rbfTransactions); const rbfChanges = rbfCache.getRbfChanges(); let rbfReplacements; @@ -644,6 +656,12 @@ class WebsocketHandler { const addressCache = this.makeAddressCache(newTransactions); const removedAddressCache = this.makeAddressCache(deletedTransactions); + // pre-compute acceleration delta + const accelerationUpdate = { + added: accelerationDelta.map(txid => accelerations[txid]).filter(acc => acc != null), + removed: accelerationDelta.filter(txid => !accelerations[txid]), + }; + // TODO - Fix indentation after PR is merged for (const server of this.webSocketServers) { server.clients.forEach(async (client) => { @@ -891,6 +909,10 @@ class WebsocketHandler { response['mempool-transactions'] = getCachedResponse('mempool-transactions', mempoolDelta); } + if (client['track-accelerations'] && (accelerationUpdate.added.length || accelerationUpdate.removed.length)) { + response['accelerations'] = getCachedResponse('accelerations', accelerationUpdate); + } + if (Object.keys(response).length) { client.send(this.serializeResponse(response)); } diff --git a/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.ts b/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.ts index f2c082fc8..237b14317 100644 --- a/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.ts +++ b/frontend/src/app/components/acceleration/accelerations-list/accelerations-list.component.ts @@ -1,5 +1,5 @@ -import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef } from '@angular/core'; -import { combineLatest, BehaviorSubject, Observable, catchError, of, switchMap, tap } from 'rxjs'; +import { Component, OnInit, ChangeDetectionStrategy, Input, ChangeDetectorRef, OnDestroy } from '@angular/core'; +import { BehaviorSubject, Observable, catchError, of, switchMap, tap } from 'rxjs'; import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface'; import { StateService } from '../../../services/state.service'; import { WebsocketService } from '../../../services/websocket.service'; @@ -11,7 +11,7 @@ import { ServicesApiServices } from '../../../services/services-api.service'; styleUrls: ['./accelerations-list.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class AccelerationsListComponent implements OnInit { +export class AccelerationsListComponent implements OnInit, OnDestroy { @Input() widget: boolean = false; @Input() pending: boolean = false; @Input() accelerations$: Observable; @@ -44,7 +44,10 @@ export class AccelerationsListComponent implements OnInit { this.accelerationList$ = this.pageSubject.pipe( switchMap((page) => { - const accelerationObservable$ = this.accelerations$ || (this.pending ? this.servicesApiService.getAccelerations$() : this.servicesApiService.getAccelerationHistoryObserveResponse$({ page: page })); + const accelerationObservable$ = this.accelerations$ || (this.pending ? this.stateService.liveAccelerations$ : this.servicesApiService.getAccelerationHistoryObserveResponse$({ page: page })); + if (!this.accelerations$ && this.pending) { + this.websocketService.ensureTrackAccelerations(); + } return accelerationObservable$.pipe( switchMap(response => { let accelerations = response; @@ -85,4 +88,8 @@ export class AccelerationsListComponent implements OnInit { trackByBlock(index: number, block: BlockExtended): number { return block.height; } + + ngOnDestroy(): void { + this.websocketService.stopTrackAccelerations(); + } } \ No newline at end of file diff --git a/frontend/src/app/components/acceleration/accelerator-dashboard/accelerator-dashboard.component.ts b/frontend/src/app/components/acceleration/accelerator-dashboard/accelerator-dashboard.component.ts index dc53d8f95..282927b4a 100644 --- a/frontend/src/app/components/acceleration/accelerator-dashboard/accelerator-dashboard.component.ts +++ b/frontend/src/app/components/acceleration/accelerator-dashboard/accelerator-dashboard.component.ts @@ -1,10 +1,10 @@ -import { ChangeDetectionStrategy, Component, HostListener, Inject, OnInit, PLATFORM_ID } from '@angular/core'; +import { ChangeDetectionStrategy, Component, HostListener, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core'; import { SeoService } from '../../../services/seo.service'; import { OpenGraphService } from '../../../services/opengraph.service'; import { WebsocketService } from '../../../services/websocket.service'; import { Acceleration, BlockExtended } from '../../../interfaces/node-api.interface'; import { StateService } from '../../../services/state.service'; -import { Observable, catchError, combineLatest, distinctUntilChanged, interval, map, of, share, startWith, switchMap, tap } from 'rxjs'; +import { Observable, Subscription, catchError, combineLatest, distinctUntilChanged, map, of, share, switchMap, tap } from 'rxjs'; import { Color } from '../../block-overview-graph/sprite-types'; import { hexToColor } from '../../block-overview-graph/utils'; import TxView from '../../block-overview-graph/tx-view'; @@ -28,7 +28,7 @@ interface AccelerationBlock extends BlockExtended { styleUrls: ['./accelerator-dashboard.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) -export class AcceleratorDashboardComponent implements OnInit { +export class AcceleratorDashboardComponent implements OnInit, OnDestroy { blocks$: Observable; accelerations$: Observable; pendingAccelerations$: Observable; @@ -39,6 +39,8 @@ export class AcceleratorDashboardComponent implements OnInit { firstLoad = true; timespan: '3d' | '1w' | '1m' = '1w'; + accelerationDeltaSubscription: Subscription; + graphHeight: number = 300; theme: ThemeService; @@ -59,27 +61,28 @@ export class AcceleratorDashboardComponent implements OnInit { ngOnInit(): void { this.onResize(); this.websocketService.want(['blocks', 'mempool-blocks', 'stats']); + this.websocketService.startTrackAccelerations(); - this.pendingAccelerations$ = (this.stateService.isBrowser ? interval(30000) : of(null)).pipe( - startWith(true), - switchMap(() => { - return this.serviceApiServices.getAccelerations$().pipe( - catchError(() => { - return of([]); - }), - ); - }), - tap(accelerations => { - if (!this.firstLoad && accelerations.some(acc => !this.seen.has(acc.txid))) { - this.audioService.playSound('bright-harmony'); - } - for(const acc of accelerations) { - this.seen.add(acc.txid); - } - this.firstLoad = false; - }), + this.pendingAccelerations$ = this.stateService.liveAccelerations$.pipe( share(), ); + this.accelerationDeltaSubscription = this.stateService.accelerations$.subscribe((delta) => { + if (!delta.reset) { + let hasNewAcceleration = false; + for (const acc of delta.added) { + if (!this.seen.has(acc.txid)) { + hasNewAcceleration = true; + } + this.seen.add(acc.txid); + } + for (const txid of delta.removed) { + this.seen.delete(txid); + } + if (hasNewAcceleration) { + this.audioService.playSound('bright-harmony'); + } + } + }); this.accelerations$ = this.stateService.chainTip$.pipe( distinctUntilChanged(), @@ -154,6 +157,11 @@ export class AcceleratorDashboardComponent implements OnInit { return false; } + ngOnDestroy(): void { + this.accelerationDeltaSubscription.unsubscribe(); + this.websocketService.stopTrackAccelerations(); + } + @HostListener('window:resize', ['$event']) onResize(): void { if (window.innerWidth >= 992) { diff --git a/frontend/src/app/components/acceleration/pending-stats/pending-stats.component.ts b/frontend/src/app/components/acceleration/pending-stats/pending-stats.component.ts index ed7061156..568e60d7e 100644 --- a/frontend/src/app/components/acceleration/pending-stats/pending-stats.component.ts +++ b/frontend/src/app/components/acceleration/pending-stats/pending-stats.component.ts @@ -2,7 +2,8 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core import { Observable, of } from 'rxjs'; import { switchMap } from 'rxjs/operators'; import { Acceleration } from '../../../interfaces/node-api.interface'; -import { ServicesApiServices } from '../../../services/services-api.service'; +import { StateService } from '../../../services/state.service'; +import { WebsocketService } from '../../../services/websocket.service'; @Component({ selector: 'app-pending-stats', @@ -15,11 +16,12 @@ export class PendingStatsComponent implements OnInit { public accelerationStats$: Observable; constructor( - private servicesApiService: ServicesApiServices, + private stateService: StateService, + private websocketService: WebsocketService, ) { } ngOnInit(): void { - this.accelerationStats$ = (this.accelerations$ || this.servicesApiService.getAccelerations$()).pipe( + this.accelerationStats$ = (this.accelerations$ || this.stateService.liveAccelerations$).pipe( switchMap(accelerations => { let totalAccelerations = 0; let totalFeeDelta = 0; diff --git a/frontend/src/app/interfaces/websocket.interface.ts b/frontend/src/app/interfaces/websocket.interface.ts index 8c24979e7..22789986e 100644 --- a/frontend/src/app/interfaces/websocket.interface.ts +++ b/frontend/src/app/interfaces/websocket.interface.ts @@ -1,7 +1,7 @@ import { SafeResourceUrl } from '@angular/platform-browser'; import { ILoadingIndicators } from '../services/state.service'; import { Transaction } from './electrs.interface'; -import { BlockExtended, DifficultyAdjustment, RbfTree, TransactionStripped } from './node-api.interface'; +import { Acceleration, BlockExtended, DifficultyAdjustment, RbfTree, TransactionStripped } from './node-api.interface'; export interface WebsocketResponse { backend?: 'esplora' | 'electrum' | 'none'; @@ -35,6 +35,7 @@ export interface WebsocketResponse { 'track-mempool-block'?: number; 'track-rbf'?: string; 'track-rbf-summary'?: boolean; + 'track-accelerations'?: boolean; 'watch-mempool'?: boolean; 'refresh-blocks'?: boolean; } @@ -92,6 +93,12 @@ export interface MempoolBlockDeltaCompressed { changed: MempoolDeltaChange[]; } +export interface AccelerationDelta { + added: Acceleration[]; + removed: string[]; + reset?: boolean; +} + export interface MempoolInfo { loaded: boolean; // (boolean) True if the mempool is fully loaded size: number; // (numeric) Current tx count diff --git a/frontend/src/app/services/state.service.ts b/frontend/src/app/services/state.service.ts index b939574b5..34197a3d1 100644 --- a/frontend/src/app/services/state.service.ts +++ b/frontend/src/app/services/state.service.ts @@ -1,8 +1,8 @@ import { Inject, Injectable, PLATFORM_ID, LOCALE_ID } from '@angular/core'; -import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable, merge } from 'rxjs'; +import { ReplaySubject, BehaviorSubject, Subject, fromEvent, Observable } from 'rxjs'; import { Transaction } from '../interfaces/electrs.interface'; -import { HealthCheckHost, IBackendInfo, MempoolBlock, MempoolBlockDelta, MempoolBlockUpdate, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo, isMempoolState } from '../interfaces/websocket.interface'; -import { BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree, TransactionStripped } from '../interfaces/node-api.interface'; +import { AccelerationDelta, HealthCheckHost, IBackendInfo, MempoolBlock, MempoolBlockUpdate, MempoolInfo, Recommendedfees, ReplacedTransaction, ReplacementInfo, isMempoolState } from '../interfaces/websocket.interface'; +import { Acceleration, BlockExtended, CpfpInfo, DifficultyAdjustment, MempoolPosition, OptimizedMempoolStats, RbfTree, TransactionStripped } from '../interfaces/node-api.interface'; import { Router, NavigationStart } from '@angular/router'; import { isPlatformBrowser } from '@angular/common'; import { filter, map, scan, shareReplay } from 'rxjs/operators'; @@ -129,6 +129,8 @@ export class StateService { mempoolBlocks$ = new ReplaySubject(1); mempoolBlockUpdate$ = new Subject(); liveMempoolBlockTransactions$: Observable<{ [txid: string]: TransactionStripped}>; + accelerations$ = new Subject(); + liveAccelerations$: Observable; txConfirmed$ = new Subject<[string, BlockExtended]>(); txReplaced$ = new Subject(); txRbfInfo$ = new Subject(); @@ -238,6 +240,24 @@ export class StateService { } }, {})); + // Emits the full list of pending accelerations each time it changes + this.liveAccelerations$ = this.accelerations$.pipe( + scan((accelerations: { [txid: string]: Acceleration }, delta: AccelerationDelta) => { + if (delta.reset) { + accelerations = {}; + } else { + for (const txid of delta.removed) { + delete accelerations[txid]; + } + } + for (const acc of delta.added) { + accelerations[acc.txid] = acc; + } + return accelerations; + }, {}), + map((accMap) => Object.values(accMap).sort((a,b) => b.added - a.added)) + ); + this.networkChanged$.subscribe((network) => { this.transactions$ = new BehaviorSubject(null); this.blocksSubject$.next([]); diff --git a/frontend/src/app/services/websocket.service.ts b/frontend/src/app/services/websocket.service.ts index e4df12aa6..fbadf0de3 100644 --- a/frontend/src/app/services/websocket.service.ts +++ b/frontend/src/app/services/websocket.service.ts @@ -33,6 +33,7 @@ export class WebsocketService { private isTrackingRbfSummary = false; private isTrackingAddress: string | false = false; private isTrackingAddresses: string[] | false = false; + private isTrackingAccelerations: boolean = false; private trackingMempoolBlock: number; private latestGitCommit = ''; private onlineCheckTimeout: number; @@ -132,6 +133,9 @@ export class WebsocketService { if (this.isTrackingAddresses) { this.startTrackAddresses(this.isTrackingAddresses); } + if (this.isTrackingAccelerations) { + this.startTrackAccelerations(); + } this.stateService.connectionState$.next(2); } @@ -235,6 +239,24 @@ export class WebsocketService { this.isTrackingRbfSummary = false; } + startTrackAccelerations() { + this.websocketSubject.next({ 'track-accelerations': true }); + this.isTrackingAccelerations = true; + } + + stopTrackAccelerations() { + if (this.isTrackingAccelerations) { + this.websocketSubject.next({ 'track-accelerations': false }); + this.isTrackingAccelerations = false; + } + } + + ensureTrackAccelerations() { + if (!this.isTrackingAccelerations) { + this.startTrackAccelerations(); + } + } + fetchStatistics(historicalDate: string) { this.websocketSubject.next({ historicalDate }); } @@ -416,6 +438,18 @@ export class WebsocketService { } } + if (response['accelerations']) { + if (response['accelerations'].accelerations) { + this.stateService.accelerations$.next({ + added: response['accelerations'].accelerations, + removed: [], + reset: true, + }); + } else { + this.stateService.accelerations$.next(response['accelerations']); + } + } + if (response['live-2h-chart']) { this.stateService.live2Chart$.next(response['live-2h-chart']); } From 69faa1d493b2341f81cef5615cf6f6cfbbf23ccd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 May 2024 02:26:59 +0000 Subject: [PATCH 18/62] Bump dtolnay/rust-toolchain Bumps [dtolnay/rust-toolchain](https://github.com/dtolnay/rust-toolchain) from bb45937a053e097f8591208d8e74c90db1873d07 to d8352f6b1d2e870bc5716e7a6d9b65c4cc244a1a. - [Release notes](https://github.com/dtolnay/rust-toolchain/releases) - [Commits](https://github.com/dtolnay/rust-toolchain/compare/bb45937a053e097f8591208d8e74c90db1873d07...d8352f6b1d2e870bc5716e7a6d9b65c4cc244a1a) --- updated-dependencies: - dependency-name: dtolnay/rust-toolchain dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b392f2dc2..c8e9ae33b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,7 @@ jobs: - name: Install ${{ steps.gettoolchain.outputs.toolchain }} Rust toolchain # Latest version available on this commit is 1.71.1 # Commit date is Aug 3, 2023 - uses: dtolnay/rust-toolchain@bb45937a053e097f8591208d8e74c90db1873d07 + uses: dtolnay/rust-toolchain@d8352f6b1d2e870bc5716e7a6d9b65c4cc244a1a with: toolchain: ${{ steps.gettoolchain.outputs.toolchain }} From 1e5a55917c31350c17f747c3b73ce7b369a0a84e Mon Sep 17 00:00:00 2001 From: Mononaut Date: Mon, 6 May 2024 15:40:32 +0000 Subject: [PATCH 19/62] Add testnet4 frontend support --- frontend/src/app/app-routing.module.ts | 38 +++++++++++++++++++ frontend/src/app/app.constants.ts | 32 ++++++++-------- frontend/src/app/bitcoin.utils.ts | 5 +++ .../components/amount/amount.component.html | 1 + .../blockchain-blocks.component.ts | 1 + .../app/components/clock/clock.component.ts | 1 + .../liquid-master-page.component.html | 3 +- .../master-page-preview.component.html | 3 +- .../master-page/master-page.component.html | 7 ++-- .../search-form/search-form.component.ts | 2 +- .../svg-images/svg-images.component.html | 3 ++ .../components/tracker/tracker.component.html | 3 +- .../tx-bowtie-graph.component.ts | 1 + .../src/app/services/enterprise.service.ts | 1 + frontend/src/app/services/mining.service.ts | 2 +- .../src/app/services/navigation.service.ts | 3 +- frontend/src/app/services/seo.service.ts | 6 ++- frontend/src/app/services/state.service.ts | 12 +++++- frontend/src/app/shared/common.utils.ts | 2 +- .../shared/components/btc/btc.component.html | 1 + .../global-footer.component.html | 5 ++- .../global-footer/global-footer.component.ts | 2 +- .../components/sats/sats.component.html | 1 + frontend/src/app/shared/regex.utils.ts | 34 +++++++++++++++-- 24 files changed, 133 insertions(+), 36 deletions(-) diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts index 7a23e7556..5262a431a 100644 --- a/frontend/src/app/app-routing.module.ts +++ b/frontend/src/app/app-routing.module.ts @@ -53,6 +53,44 @@ let routes: Routes = [ }, ] }, + { + path: 'testnet4', + children: [ + { + path: '', + pathMatch: 'full', + loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), + data: { preload: true }, + }, + { + path: '', + loadChildren: () => import('./master-page.module').then(m => m.MasterPageModule), + data: { preload: true }, + }, + { + path: 'wallet', + children: [], + component: AddressGroupComponent, + data: { + networkSpecific: true, + } + }, + { + path: 'status', + data: { networks: ['bitcoin', 'liquid'] }, + component: StatusViewComponent + }, + { + path: '', + loadChildren: () => import('./bitcoin-graphs.module').then(m => m.BitcoinGraphsModule), + data: { preload: true }, + }, + { + path: '**', + redirectTo: '/testnet4' + }, + ] + }, { path: 'signet', children: [ diff --git a/frontend/src/app/app.constants.ts b/frontend/src/app/app.constants.ts index bd81d02c0..aaa53b8ba 100644 --- a/frontend/src/app/app.constants.ts +++ b/frontend/src/app/app.constants.ts @@ -189,22 +189,22 @@ export const specialBlocks = { '0': { labelEvent: 'Genesis', labelEventCompleted: 'The Genesis of Bitcoin', - networks: ['mainnet', 'testnet'], + networks: ['mainnet', 'testnet', 'testnet4'], }, '210000': { labelEvent: 'Bitcoin\'s 1st Halving', labelEventCompleted: 'Block Subsidy has halved to 25 BTC per block', - networks: ['mainnet', 'testnet'], + networks: ['mainnet', 'testnet', 'testnet4'], }, '420000': { labelEvent: 'Bitcoin\'s 2nd Halving', labelEventCompleted: 'Block Subsidy has halved to 12.5 BTC per block', - networks: ['mainnet', 'testnet'], + networks: ['mainnet', 'testnet', 'testnet4'], }, '630000': { labelEvent: 'Bitcoin\'s 3rd Halving', labelEventCompleted: 'Block Subsidy has halved to 6.25 BTC per block', - networks: ['mainnet', 'testnet'], + networks: ['mainnet', 'testnet', 'testnet4'], }, '709632': { labelEvent: 'Taproot 🌱 activation', @@ -214,62 +214,62 @@ export const specialBlocks = { '840000': { labelEvent: 'Bitcoin\'s 4th Halving', labelEventCompleted: 'Block Subsidy has halved to 3.125 BTC per block', - networks: ['mainnet', 'testnet'], + networks: ['mainnet', 'testnet', 'testnet4'], }, '1050000': { labelEvent: 'Bitcoin\'s 5th Halving', labelEventCompleted: 'Block Subsidy has halved to 1.5625 BTC per block', - networks: ['mainnet', 'testnet'], + networks: ['mainnet', 'testnet', 'testnet4'], }, '1260000': { labelEvent: 'Bitcoin\'s 6th Halving', labelEventCompleted: 'Block Subsidy has halved to 0.78125 BTC per block', - networks: ['mainnet', 'testnet'], + networks: ['mainnet', 'testnet', 'testnet4'], }, '1470000': { labelEvent: 'Bitcoin\'s 7th Halving', labelEventCompleted: 'Block Subsidy has halved to 0.390625 BTC per block', - networks: ['mainnet', 'testnet'], + networks: ['mainnet', 'testnet', 'testnet4'], }, '1680000': { labelEvent: 'Bitcoin\'s 8th Halving', labelEventCompleted: 'Block Subsidy has halved to 0.1953125 BTC per block', - networks: ['mainnet', 'testnet'], + networks: ['mainnet', 'testnet', 'testnet4'], }, '1890000': { labelEvent: 'Bitcoin\'s 9th Halving', labelEventCompleted: 'Block Subsidy has halved to 0.09765625 BTC per block', - networks: ['mainnet', 'testnet'], + networks: ['mainnet', 'testnet', 'testnet4'], }, '2100000': { labelEvent: 'Bitcoin\'s 10th Halving', labelEventCompleted: 'Block Subsidy has halved to 0.04882812 BTC per block', - networks: ['mainnet', 'testnet'], + networks: ['mainnet', 'testnet', 'testnet4'], }, '2310000': { labelEvent: 'Bitcoin\'s 11th Halving', labelEventCompleted: 'Block Subsidy has halved to 0.02441406 BTC per block', - networks: ['mainnet', 'testnet'], + networks: ['mainnet', 'testnet', 'testnet4'], }, '2520000': { labelEvent: 'Bitcoin\'s 12th Halving', labelEventCompleted: 'Block Subsidy has halved to 0.01220703 BTC per block', - networks: ['mainnet', 'testnet'], + networks: ['mainnet', 'testnet', 'testnet4'], }, '2730000': { labelEvent: 'Bitcoin\'s 13th Halving', labelEventCompleted: 'Block Subsidy has halved to 0.00610351 BTC per block', - networks: ['mainnet', 'testnet'], + networks: ['mainnet', 'testnet', 'testnet4'], }, '2940000': { labelEvent: 'Bitcoin\'s 14th Halving', labelEventCompleted: 'Block Subsidy has halved to 0.00305175 BTC per block', - networks: ['mainnet', 'testnet'], + networks: ['mainnet', 'testnet', 'testnet4'], }, '3150000': { labelEvent: 'Bitcoin\'s 15th Halving', labelEventCompleted: 'Block Subsidy has halved to 0.00152587 BTC per block', - networks: ['mainnet', 'testnet'], + networks: ['mainnet', 'testnet', 'testnet4'], } }; diff --git a/frontend/src/app/bitcoin.utils.ts b/frontend/src/app/bitcoin.utils.ts index d5b5122fa..b9f4f39e1 100644 --- a/frontend/src/app/bitcoin.utils.ts +++ b/frontend/src/app/bitcoin.utils.ts @@ -266,6 +266,11 @@ const featureActivation = { segwit: 872730, taproot: 2032291, }, + testnet4: { + rbf: 0, + segwit: 0, + taproot: 0, + }, signet: { rbf: 0, segwit: 0, diff --git a/frontend/src/app/components/amount/amount.component.html b/frontend/src/app/components/amount/amount.component.html index f157d17d6..b38cf4c41 100644 --- a/frontend/src/app/components/amount/amount.component.html +++ b/frontend/src/app/components/amount/amount.component.html @@ -43,5 +43,6 @@ L- tL- t + t s diff --git a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts index 35499f162..1d0e284f8 100644 --- a/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts +++ b/frontend/src/app/components/blockchain-blocks/blockchain-blocks.component.ts @@ -70,6 +70,7 @@ export class BlockchainBlocksComponent implements OnInit, OnChanges, OnDestroy { liquid: ['var(--liquid)', 'var(--testnet-alt)'], 'liquidtestnet': ['var(--liquidtestnet)', 'var(--liquidtestnet-alt)'], testnet: ['var(--testnet)', 'var(--testnet-alt)'], + testnet4: ['var(--testnet)', 'var(--testnet-alt)'], signet: ['var(--signet)', 'var(--signet-alt)'], }; diff --git a/frontend/src/app/components/clock/clock.component.ts b/frontend/src/app/components/clock/clock.component.ts index 94ff3e810..90f24a753 100644 --- a/frontend/src/app/components/clock/clock.component.ts +++ b/frontend/src/app/components/clock/clock.component.ts @@ -36,6 +36,7 @@ export class ClockComponent implements OnInit { liquid: ['#116761', '#183550'], 'liquidtestnet': ['#494a4a', '#272e46'], testnet: ['#1d486f', '#183550'], + testnet4: ['#1d486f', '#183550'], signet: ['#6f1d5d', '#471850'], }; diff --git a/frontend/src/app/components/liquid-master-page/liquid-master-page.component.html b/frontend/src/app/components/liquid-master-page/liquid-master-page.component.html index 7d826ca87..7e39d9341 100644 --- a/frontend/src/app/components/liquid-master-page/liquid-master-page.component.html +++ b/frontend/src/app/components/liquid-master-page/liquid-master-page.component.html @@ -51,7 +51,8 @@
Mainnet Signet - Testnet + Testnet3 + Testnet4 Liquid Liquid Testnet diff --git a/frontend/src/app/components/master-page-preview/master-page-preview.component.html b/frontend/src/app/components/master-page-preview/master-page-preview.component.html index 36e8eed10..49efce400 100644 --- a/frontend/src/app/components/master-page-preview/master-page-preview.component.html +++ b/frontend/src/app/components/master-page-preview/master-page-preview.component.html @@ -15,7 +15,8 @@
Signet - Testnet + Testnet3 + Testnet4 Mainnet Testnet Mainnet diff --git a/frontend/src/app/components/master-page/master-page.component.html b/frontend/src/app/components/master-page/master-page.component.html index 5da892f4a..6383e999b 100644 --- a/frontend/src/app/components/master-page/master-page.component.html +++ b/frontend/src/app/components/master-page/master-page.component.html @@ -58,14 +58,15 @@ -