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 01/84] 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 02/84] 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 1b25a71d9f5a0fd56df1e8e2012ae65115c9d7cc Mon Sep 17 00:00:00 2001 From: natsoni Date: Fri, 3 May 2024 11:53:33 +0200 Subject: [PATCH 03/84] Add accelerations category to graphs page --- .../src/app/components/graphs/graphs.component.html | 11 ++++++++++- .../src/app/components/graphs/graphs.component.scss | 5 +++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/frontend/src/app/components/graphs/graphs.component.html b/frontend/src/app/components/graphs/graphs.component.html index 94241b825..073aa02dc 100644 --- a/frontend/src/app/components/graphs/graphs.component.html +++ b/frontend/src/app/components/graphs/graphs.component.html @@ -1,4 +1,4 @@ -
+ +
diff --git a/frontend/src/app/components/graphs/graphs.component.scss b/frontend/src/app/components/graphs/graphs.component.scss index 0a066266d..715ec1b42 100644 --- a/frontend/src/app/components/graphs/graphs.component.scss +++ b/frontend/src/app/components/graphs/graphs.component.scss @@ -1,8 +1,8 @@ .menu { - flex-grow: 1; + flex-wrap: wrap; padding: 0 35px; @media (min-width: 576px) { - max-width: 400px; + max-width: 600px; } & > * { @@ -11,5 +11,6 @@ &.last-child { margin-inline-end: 0; } + margin-bottom: 5px; } } \ No newline at end of file From 453a2224cd754adf311ba773c3bd347144496736 Mon Sep 17 00:00:00 2001 From: natsoni Date: Fri, 3 May 2024 12:33:05 +0200 Subject: [PATCH 04/84] Add block fees vs subsidy bar chart --- backend/src/api/mining/mining-routes.ts | 21 + backend/src/api/mining/mining.ts | 7 + backend/src/repositories/BlocksRepository.ts | 46 ++ .../block-fees-subsidy-graph.component.html | 31 ++ .../block-fees-subsidy-graph.component.scss | 76 +++ .../block-fees-subsidy-graph.component.ts | 432 ++++++++++++++++++ .../components/graphs/graphs.component.html | 2 + frontend/src/app/graphs/graphs.module.ts | 2 + .../src/app/graphs/graphs.routing.module.ts | 6 + frontend/src/app/services/api.service.ts | 7 + 10 files changed, 630 insertions(+) create mode 100644 frontend/src/app/components/block-fees-subsidy-graph/block-fees-subsidy-graph.component.html create mode 100644 frontend/src/app/components/block-fees-subsidy-graph/block-fees-subsidy-graph.component.scss create mode 100644 frontend/src/app/components/block-fees-subsidy-graph/block-fees-subsidy-graph.component.ts diff --git a/backend/src/api/mining/mining-routes.ts b/backend/src/api/mining/mining-routes.ts index 3492114b5..7fa59e219 100644 --- a/backend/src/api/mining/mining-routes.ts +++ b/backend/src/api/mining/mining-routes.ts @@ -24,6 +24,8 @@ class MiningRoutes { .get(config.MEMPOOL.API_URL_PREFIX + 'mining/difficulty-adjustments', this.$getDifficultyAdjustments) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/reward-stats/:blockCount', this.$getRewardStats) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fees/:interval', this.$getHistoricalBlockFees) + .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/exact-fees/:height', this.$getHistoricalExactBlockFees) + .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/exact-fees', this.$getHistoricalExactBlockFees) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/rewards/:interval', this.$getHistoricalBlockRewards) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/fee-rates/:interval', this.$getHistoricalBlockFeeRates) .get(config.MEMPOOL.API_URL_PREFIX + 'mining/blocks/sizes-weights/:interval', this.$getHistoricalBlockSizeAndWeight) @@ -217,6 +219,25 @@ class MiningRoutes { } } + private async $getHistoricalExactBlockFees(req: Request, res: Response) { + try { + const heightRegex = /^(0|[1-9]\d{0,9})$/; + if (req.params.height !== undefined && !heightRegex.test(req.params.height)) { + throw new Error('Invalid height parameter'); + } + + const blockFees = await mining.$getHistoricalExactBlockFees(req.params.height); + const blockCount = await BlocksRepository.$blockCount(null, null); + res.header('Pragma', 'public'); + res.header('Cache-control', 'public'); + res.header('X-total-count', blockCount.toString()); + res.setHeader('Expires', new Date(Date.now() + 1000 * 60).toUTCString()); + res.json(blockFees); + } catch (e) { + res.status(500).send(e instanceof Error ? e.message : e); + } + } + private async $getHistoricalBlockRewards(req: Request, res: Response) { try { const blockRewards = await mining.$getHistoricalBlockRewards(req.params.interval); diff --git a/backend/src/api/mining/mining.ts b/backend/src/api/mining/mining.ts index 85554db2d..845e571f6 100644 --- a/backend/src/api/mining/mining.ts +++ b/backend/src/api/mining/mining.ts @@ -50,6 +50,13 @@ class Mining { ); } + /** + * Get historical (not averaged) block total fee + */ + public async $getHistoricalExactBlockFees(height: string | null = null): Promise { + return await BlocksRepository.$getHistoricalExactBlockFees(height); + } + /** * Get historical block rewards */ diff --git a/backend/src/repositories/BlocksRepository.ts b/backend/src/repositories/BlocksRepository.ts index e6e92d60f..ca2377e92 100644 --- a/backend/src/repositories/BlocksRepository.ts +++ b/backend/src/repositories/BlocksRepository.ts @@ -689,6 +689,52 @@ class BlocksRepository { } } + /** + * Get the historical block fees + */ + public async $getHistoricalExactBlockFees(height: string | null): Promise { + try { + let query = `SELECT + blocks.height, + fees, + prices.USD + FROM blocks + JOIN blocks_prices on blocks_prices.height = blocks.height + JOIN prices on prices.id = blocks_prices.price_id + `; + + if (height !== null) { + query += ` WHERE blocks.height <= ${height}`; + } + query += ` ORDER BY blocks.height DESC LIMIT 20000`; + + const [rows]: any = await DB.query(query); + + // Add accelerations data if available + if (config.MEMPOOL_SERVICES.ACCELERATIONS && rows.length > 0) { + + let query = ` + SELECT height, boost_cost FROM accelerations + WHERE height >= ${rows[rows.length - 1].height} AND height <= ${rows[0].height} + `; + const [accelerations]: any = await DB.query(query); + + for (let i = 0; i < accelerations.length; ++i) { + const idx = rows.findIndex((block) => block.height === accelerations[i].height); + if (idx !== -1) { + if (rows[idx].accelerations === undefined) rows[idx].accelerations = 0; + rows[idx].accelerations += accelerations[i].boost_cost; + } + } + } + + return rows; + } catch (e) { + logger.err('Cannot generate exact block fees history. Reason: ' + (e instanceof Error ? e.message : e)); + throw e; + } + } + /** * Get the historical averaged block rewards */ diff --git a/frontend/src/app/components/block-fees-subsidy-graph/block-fees-subsidy-graph.component.html b/frontend/src/app/components/block-fees-subsidy-graph/block-fees-subsidy-graph.component.html new file mode 100644 index 000000000..77eea19e1 --- /dev/null +++ b/frontend/src/app/components/block-fees-subsidy-graph/block-fees-subsidy-graph.component.html @@ -0,0 +1,31 @@ + + +
+
+
+ Block Fees Vs Subsidy + +
+ +
+ +
+ +
+
+
+ +
+
+
+
+
+ +
diff --git a/frontend/src/app/components/block-fees-subsidy-graph/block-fees-subsidy-graph.component.scss b/frontend/src/app/components/block-fees-subsidy-graph/block-fees-subsidy-graph.component.scss new file mode 100644 index 000000000..d29c62447 --- /dev/null +++ b/frontend/src/app/components/block-fees-subsidy-graph/block-fees-subsidy-graph.component.scss @@ -0,0 +1,76 @@ +.card-header { + border-bottom: 0; + font-size: 18px; + @media (min-width: 465px) { + font-size: 20px; + } + @media (min-width: 992px) { + height: 40px; + } +} + +.main-title { + position: relative; + color: #ffffff91; + margin-top: -13px; + font-size: 10px; + text-transform: uppercase; + font-weight: 500; + text-align: center; + padding-bottom: 3px; +} + +.full-container { + display: flex; + flex-direction: column; + padding: 0px 15px; + width: 100%; + height: calc(100vh - 225px); + min-height: 400px; + @media (min-width: 992px) { + height: calc(100vh - 150px); + } +} + +.chart { + display: flex; + flex: 1; + width: 100%; + padding-bottom: 20px; + 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%; + height: 100%; + max-height: 270px; +} + +.disabled { + pointer-events: none; + opacity: 0.5; +} + +.scrollable-dropdown { + max-height: 500px; + overflow-y: auto; +} + +.selected { + background-color: var(--active-bg); + color: #fff; +} \ No newline at end of file diff --git a/frontend/src/app/components/block-fees-subsidy-graph/block-fees-subsidy-graph.component.ts b/frontend/src/app/components/block-fees-subsidy-graph/block-fees-subsidy-graph.component.ts new file mode 100644 index 000000000..54d4f70a1 --- /dev/null +++ b/frontend/src/app/components/block-fees-subsidy-graph/block-fees-subsidy-graph.component.ts @@ -0,0 +1,432 @@ +import { ChangeDetectionStrategy, Component, Inject, Input, LOCALE_ID, OnInit } from '@angular/core'; +import { EChartsOption } from '../../graphs/echarts'; +import { Observable } from 'rxjs'; +import { map, share, startWith, switchMap, tap } from 'rxjs/operators'; +import { ApiService } from '../../services/api.service'; +import { SeoService } from '../../services/seo.service'; +import { formatNumber } from '@angular/common'; +import { UntypedFormBuilder, UntypedFormGroup } from '@angular/forms'; +import { download, formatterXAxis } from '../../shared/graphs.utils'; +import { ActivatedRoute, Router } from '@angular/router'; +import { FiatShortenerPipe } from '../../shared/pipes/fiat-shortener.pipe'; +import { FiatCurrencyPipe } from '../../shared/pipes/fiat-currency.pipe'; +import { StateService } from '../../services/state.service'; + +@Component({ + selector: 'app-block-fees-subsidy-graph', + templateUrl: './block-fees-subsidy-graph.component.html', + styleUrls: ['./block-fees-subsidy-graph.component.scss'], + styles: [` + .loadingGraphs { + position: absolute; + top: 50%; + left: calc(50% - 15px); + z-index: 100; + } + `], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class BlockFeesSubsidyGraphComponent implements OnInit { + @Input() right: number | string = 45; + @Input() left: number | string = 75; + + radioGroupForm: UntypedFormGroup; + + chartOptions: EChartsOption = {}; + chartInitOptions = { + renderer: 'svg', + }; + + statsObservable$: Observable; + isLoading = true; + formatNumber = formatNumber; + endBlock = ''; + blockCount = 0; + chartInstance: any = undefined; + showFiat = false; + dropdownOptions = []; + step = 20000; + includeAccelerations = false; + + constructor( + @Inject(LOCALE_ID) public locale: string, + private seoService: SeoService, + private apiService: ApiService, + private formBuilder: UntypedFormBuilder, + public stateService: StateService, + private route: ActivatedRoute, + private router: Router, + private fiatShortenerPipe: FiatShortenerPipe, + private fiatCurrencyPipe: FiatCurrencyPipe, + ) { + this.radioGroupForm = this.formBuilder.group({ endBlock: '' }); + this.radioGroupForm.controls.endBlock.setValue(''); + } + + ngOnInit(): void { + this.seoService.setTitle($localize`:@@mining.block-fees-subsidy:Block Fees Vs Subsidy`); + this.seoService.setDescription($localize`:@@meta.description.bitcoin.graphs.block-fees-subsidy:See the mining fees earned per Bitcoin block compared to the Bitcoin block subsidy, visualized in BTC and USD over time.`); + + this.route.queryParams.subscribe((params) => { + if (/^(0|[1-9]\d{0,9})$/.test(params['height'])) { + this.radioGroupForm.controls.endBlock.setValue(params['height'], { emitEvent: false }); + } + }); + + this.includeAccelerations = this.stateService.env.ACCELERATOR; + + this.statsObservable$ = this.radioGroupForm.get('endBlock').valueChanges + .pipe( + startWith(this.radioGroupForm.controls.endBlock.value), + switchMap((endBlock) => { + this.isLoading = true; + this.endBlock = endBlock; + return this.apiService.getHistoricalExactBlockFees$(endBlock === '' ? undefined : endBlock) + .pipe( + tap((response) => { + let blockReward = 50 * 100_000_000; + const subsidies = {}; + for (let i = 0; i <= 33; i++) { + subsidies[i] = blockReward; + blockReward = Math.floor(blockReward / 2); + } + + const existingHeights = new Set(response.body.map(val => val.height)); + + for (let i = response.body[0].height; i <= response.body[response.body.length - 1].height; i++) { + if (!existingHeights.has(i)) { + response.body.push({ height: i, fees: 0, missing: true }); + } + } + + response.body.sort((a, b) => a.height - b.height); + + const data = { + blockHeight: response.body.map(val => val.height), + blockSubsidy: response.body.map(val => val?.missing ? 0 : subsidies[Math.floor(Math.min(val.height / 210000, 33))] / 100_000_000), + blockSubsidyFiat: response.body.map(val => val?.missing ? 0 : subsidies[Math.floor(Math.min(val.height / 210000, 33))] / 100_000_000 * val.USD), + blockFees: response.body.map(val => val.fees / 100_000_000), + blockFeesFiat: response.body.map(val => val.fees / 100_000_000 * val.USD), + } + + let accelerationData = {}; + if (this.includeAccelerations) { + accelerationData = { + blockAccelerations: response.body.map(val => val?.accelerations ? val.accelerations / 100_000_000 : 0), + blockAccelerationsFiat: response.body.map(val => val?.accelerations ? val.accelerations / 100_000_000 * val.USD : 0), + } + } + + this.prepareChartOptions(data, accelerationData); + this.isLoading = false; + }), + map((response) => { + this.blockCount = parseInt(response.headers.get('x-total-count'), 10); + if (this.radioGroupForm.controls.endBlock.value === '') this.radioGroupForm.controls.endBlock.setValue((this.blockCount - 1).toString(), { emitEvent: false }); + this.dropdownOptions = [(this.blockCount - 1).toString()]; + if (this.blockCount) { + let i = this.blockCount - 1 - this.step; + while (i >= 0) { + this.dropdownOptions.push(i.toString()); + i -= this.step; + } + } + return { + blockCount: this.blockCount, + }; + }), + ); + }), + share() + ); + } + + prepareChartOptions(data, accelerationData) { + let title: object; + if (data.blockFees.length === 0) { + title = { + textStyle: { + color: 'grey', + fontSize: 15 + }, + text: $localize`:@@23555386d8af1ff73f297e89dd4af3f4689fb9dd:Indexing blocks`, + left: 'center', + top: 'center' + }; + } + + this.chartOptions = { + title: title, + color: [ + 'var(--orange)', + 'var(--success)', + 'var(--tertiary)' + ], + animation: false, + grid: { + top: 80, + bottom: 80, + right: this.right, + left: this.left, + }, + tooltip: { + show: !this.isMobile(), + trigger: 'axis', + axisPointer: { + type: 'line' + }, + backgroundColor: 'color-mix(in srgb, var(--active-bg) 95%, transparent)', + borderRadius: 4, + shadowColor: 'color-mix(in srgb, var(--active-bg) 95%, transparent)', + textStyle: { + color: 'var(--tooltip-grey)', + align: 'left', + }, + borderColor: 'var(--active-bg)', + formatter: function (data) { + if (data.length <= 0) { + return ''; + } + + let tooltip = `Block ${data[0].axisValue}
`; + for (let i = data.length - 1; i >= 0; i--) { + const tick = data[i]; + if (tick.seriesName.includes('Accelerations') && tick.data === 0) continue; + if (!this.showFiat) tooltip += `${tick.marker} ${tick.seriesName}: ${formatNumber(tick.data, this.locale, '1.0-3')} BTC
`; + else tooltip += `${tick.marker} ${tick.seriesName}: ${this.fiatCurrencyPipe.transform(tick.data, null, 'USD') }
`; + } + if (!this.showFiat) tooltip += `
${formatNumber(data.reduce((acc, val) => acc + val.data, 0), this.locale, '1.0-3')} BTC
`; + else tooltip += `
${this.fiatCurrencyPipe.transform(data.reduce((acc, val) => acc + val.data, 0), null, 'USD')}
`; + return tooltip; + }.bind(this) + }, + xAxis: data.blockFees.length === 0 ? undefined : + { + type: 'category', + data: data.blockHeight, + axisLabel: { + hideOverlap: true, + color: 'var(--grey)', + } + }, + legend: data.blockFees.length === 0 ? undefined : { + data: [ + { + name: 'Subsidy', + inactiveColor: 'var(--grey)', + textStyle: { + color: 'white', + }, + icon: 'roundRect', + }, + { + name: 'Fees', + inactiveColor: 'var(--grey)', + textStyle: { + color: 'white', + }, + icon: 'roundRect', + }, + this.includeAccelerations ? { + name: 'Accelerations', + inactiveColor: 'var(--grey)', + textStyle: { + color: 'white', + }, + icon: 'roundRect', + } : null, + { + name: 'Subsidy (USD)', + inactiveColor: 'var(--grey)', + textStyle: { + color: 'white', + }, + icon: 'roundRect', + }, + { + name: 'Fees (USD)', + inactiveColor: 'var(--grey)', + textStyle: { + color: 'white', + }, + icon: 'roundRect', + }, + this.includeAccelerations ? { + name: 'Accelerations (USD)', + inactiveColor: 'var(--grey)', + textStyle: { + color: 'white', + }, + icon: 'roundRect', + } : null + ].filter(legend => legend !== null), + selected: { + 'Subsidy (USD)': this.showFiat, + 'Fees (USD)': this.showFiat, + 'Accelerations (USD)': this.showFiat, + 'Subsidy': !this.showFiat, + 'Fees': !this.showFiat, + 'Accelerations': !this.showFiat, + }, + }, + yAxis: data.blockFees.length === 0 ? undefined : [ + { + type: 'value', + axisLabel: { + color: 'var(--grey)', + formatter: (val) => { + return `${val} BTC`; + } + }, + min: 0, + splitLine: { + lineStyle: { + type: 'dotted', + color: 'var(--transparent-fg)', + opacity: 0.25, + } + }, + }, + { + type: 'value', + position: 'right', + axisLabel: { + color: 'var(--grey)', + formatter: function(val) { + return this.fiatShortenerPipe.transform(val, null, 'USD'); + }.bind(this) + }, + splitLine: { + show: false, + }, + }, + ], + series: data.blockFees.length === 0 ? undefined : [ + { + name: 'Subsidy', + yAxisIndex: 0, + type: 'bar', + stack: 'total', + data: data.blockSubsidy, + }, + { + name: 'Fees', + yAxisIndex: 0, + type: 'bar', + stack: 'total', + data: data.blockFees, + }, + { + name: 'Accelerations', + yAxisIndex: 0, + type: 'bar', + stack: 'total', + data: accelerationData.blockAccelerations, + }, + { + name: 'Subsidy (USD)', + yAxisIndex: 1, + type: 'bar', + stack: 'total', + data: data.blockSubsidyFiat, + }, + { + name: 'Fees (USD)', + yAxisIndex: 1, + type: 'bar', + stack: 'total', + data: data.blockFeesFiat, + }, + { + name: 'Accelerations (USD)', + yAxisIndex: 1, + type: 'bar', + stack: 'total', + data: accelerationData.blockAccelerationsFiat, + }, + ], + dataZoom: data.blockFees.length === 0 ? undefined : [{ + type: 'inside', + realtime: true, + zoomLock: true, + maxSpan: 100, + minSpan: 1, + moveOnMouseMove: false, + }, { + showDetail: false, + show: true, + type: 'slider', + brushSelect: false, + realtime: true, + left: 20, + right: 15, + selectedDataBackground: { + lineStyle: { + color: '#fff', + opacity: 0.45, + }, + }, + }], + }; + } + + onChartInit(ec) { + this.chartInstance = ec; + + this.chartInstance.on('legendselectchanged', (params) => { + const isFiat = params.name.includes('USD'); + if (isFiat === this.showFiat) return; + + const isActivation = params.selected[params.name]; + if (isFiat === isActivation) { + this.showFiat = true; + this.chartInstance.dispatchAction({ type: 'legendUnSelect', name: 'Subsidy' }); + this.chartInstance.dispatchAction({ type: 'legendUnSelect', name: 'Fees' }); + this.chartInstance.dispatchAction({ type: 'legendUnSelect', name: 'Accelerations' }); + this.chartInstance.dispatchAction({ type: 'legendSelect', name: 'Subsidy (USD)' }); + this.chartInstance.dispatchAction({ type: 'legendSelect', name: 'Fees (USD)' }); + this.chartInstance.dispatchAction({ type: 'legendSelect', name: 'Accelerations (USD)' }); + } else { + this.showFiat = false; + this.chartInstance.dispatchAction({ type: 'legendSelect', name: 'Subsidy' }); + this.chartInstance.dispatchAction({ type: 'legendSelect', name: 'Fees' }); + this.chartInstance.dispatchAction({ type: 'legendSelect', name: 'Accelerations' }); + this.chartInstance.dispatchAction({ type: 'legendUnSelect', name: 'Subsidy (USD)' }); + this.chartInstance.dispatchAction({ type: 'legendUnSelect', name: 'Fees (USD)' }); + this.chartInstance.dispatchAction({ type: 'legendUnSelect', name: 'Accelerations (USD)' }); + } + }); + } + + isMobile() { + return (window.innerWidth <= 767.98); + } + + onSaveChart() { + // @ts-ignore + const prevBottom = this.chartOptions.grid.bottom; + const now = new Date(); + // @ts-ignore + this.chartOptions.grid.bottom = 40; + this.chartOptions.backgroundColor = 'var(--active-bg)'; + this.chartInstance.setOption(this.chartOptions); + download(this.chartInstance.getDataURL({ + pixelRatio: 2, + excludeComponents: ['dataZoom'], + }), `block-fees-subsidy-${this.endBlock}-${Math.round(now.getTime() / 1000)}.svg`); + // @ts-ignore + this.chartOptions.grid.bottom = prevBottom; + this.chartOptions.backgroundColor = 'none'; + this.chartInstance.setOption(this.chartOptions); + } + + selectBlockSpan(value: string) { + this.radioGroupForm.controls.endBlock.setValue(value); + this.router.navigate([], { queryParams: { height: value }, queryParamsHandling: 'merge' }); + } + + endBlockToSelector(value: string): string { + if (parseInt(value, 10) > this.blockCount) value = (this.blockCount).toString(); + return `Blocks ${Math.max(0, parseInt(value, 10) - this.step)} - ` + value; + } +} diff --git a/frontend/src/app/components/graphs/graphs.component.html b/frontend/src/app/components/graphs/graphs.component.html index 073aa02dc..c72aa8eb8 100644 --- a/frontend/src/app/components/graphs/graphs.component.html +++ b/frontend/src/app/components/graphs/graphs.component.html @@ -17,6 +17,8 @@ i18n="mining.block-fee-rates">Block Fee Rates Block Fees + Block Fees Vs Subsidy Block Rewards { + return this.httpClient.get( + this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/blocks/exact-fees` + + (height !== undefined ? `/${height}` : ''), { observe: 'response' } + ); + } + getHistoricalBlockRewards$(interval: string | undefined) : Observable { return this.httpClient.get( this.apiBaseUrl + this.apiBasePath + `/api/v1/mining/blocks/rewards` + From 1c3e0bdd6a03ead2749840fb462a201f5c244bed Mon Sep 17 00:00:00 2001 From: natsoni Date: Fri, 3 May 2024 14:45:09 +0200 Subject: [PATCH 05/84] Prevent address page to load more txs unnecessarily --- frontend/src/app/components/address/address.component.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/app/components/address/address.component.ts b/frontend/src/app/components/address/address.component.ts index 95abe4ac1..0a65a70b3 100644 --- a/frontend/src/app/components/address/address.component.ts +++ b/frontend/src/app/components/address/address.component.ts @@ -173,6 +173,7 @@ export class AddressComponent implements OnInit, OnDestroy { }); this.transactions = this.tempTransactions; + if (this.transactions.length === this.txCount) this.fullyLoaded = true; this.isLoadingTransactions = false; }, (error) => { From 6d595dcdb63cbddb0fd01622ed07e44640fda577 Mon Sep 17 00:00:00 2001 From: natsoni Date: Sat, 4 May 2024 16:56:02 +0200 Subject: [PATCH 06/84] Fix graph menu styling --- .../src/app/components/graphs/graphs.component.html | 11 +++++------ .../src/app/components/graphs/graphs.component.scss | 4 ++-- .../src/app/components/graphs/graphs.component.ts | 6 +++--- .../components/statistics/statistics.component.scss | 2 +- 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/frontend/src/app/components/graphs/graphs.component.html b/frontend/src/app/components/graphs/graphs.component.html index c72aa8eb8..53d18bbc2 100644 --- a/frontend/src/app/components/graphs/graphs.component.html +++ b/frontend/src/app/components/graphs/graphs.component.html @@ -1,9 +1,9 @@ -