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 35808cb14..df4cdf330 100644
--- a/frontend/src/app/components/address-graph/address-graph.component.html
+++ b/frontend/src/app/components/address-graph/address-graph.component.html
@@ -1,14 +1,14 @@
-
+
-
-
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 6ae3dd8e8..9538f8750 100644
--- a/frontend/src/app/components/address-graph/address-graph.component.ts
+++ b/frontend/src/app/components/address-graph/address-graph.component.ts
@@ -7,6 +7,7 @@ import { ElectrsApiService } from '../../services/electrs-api.service';
import { AmountShortenerPipe } from '../../shared/pipes/amount-shortener.pipe';
import { Router } from '@angular/router';
import { RelativeUrlPipe } from '../../shared/pipes/relative-url/relative-url.pipe';
+import { StateService } from '../../services/state.service';
@Component({
selector: 'app-address-graph',
@@ -26,8 +27,10 @@ export class AddressGraphComponent implements OnChanges {
@Input() address: string;
@Input() isPubkey: boolean = false;
@Input() stats: ChainStats;
+ @Input() height: number = 200;
@Input() right: number | string = 10;
@Input() left: number | string = 70;
+ @Input() widget: boolean = false;
data: any[] = [];
hoverData: any[] = [];
@@ -43,6 +46,7 @@ export class AddressGraphComponent implements OnChanges {
constructor(
@Inject(LOCALE_ID) public locale: string,
+ public stateService: StateService,
private electrsApiService: ElectrsApiService,
private router: Router,
private amountShortenerPipe: AmountShortenerPipe,
@@ -52,6 +56,9 @@ export class AddressGraphComponent implements OnChanges {
ngOnChanges(changes: SimpleChanges): void {
this.isLoading = true;
+ if (!this.address || !this.stats) {
+ return;
+ }
(this.isPubkey
? this.electrsApiService.getScriptHashSummary$((this.address.length === 66 ? '21' : '41') + this.address + 'ac')
: this.electrsApiService.getAddressSummary$(this.address)).pipe(
diff --git a/frontend/src/app/components/balance-widget/balance-widget.component.html b/frontend/src/app/components/balance-widget/balance-widget.component.html
new file mode 100644
index 000000000..4923a2c06
--- /dev/null
+++ b/frontend/src/app/components/balance-widget/balance-widget.component.html
@@ -0,0 +1,59 @@
+
+
+
+
+
BTC Holdings
+
+ {{ ((addressInfo.chain_stats.funded_txo_sum - addressInfo.chain_stats.spent_txo_sum) / 100_000_000) | number: '1.2-2' }} BTC
+
+
+
+
+
Change (7d)
+
+ {{ delta7d > 0 ? '+' : ''}}{{ ((delta7d) / 100_000_000) | number: '1.2-2' }} BTC
+
+
+
+
+
Change (30d)
+
+ {{ delta30d > 0 ? '+' : ''}}{{ ((delta30d) / 100_000_000) | number: '1.2-2' }} BTC
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/app/components/balance-widget/balance-widget.component.scss b/frontend/src/app/components/balance-widget/balance-widget.component.scss
new file mode 100644
index 000000000..a2f803c79
--- /dev/null
+++ b/frontend/src/app/components/balance-widget/balance-widget.component.scss
@@ -0,0 +1,160 @@
+.balance-container {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-around;
+ height: 76px;
+ .shared-block {
+ color: var(--transparent-fg);
+ font-size: 12px;
+ }
+ .item {
+ padding: 0 5px;
+ width: 100%;
+ max-width: 150px;
+ &:last-child {
+ display: none;
+ @media (min-width: 485px) {
+ display: table-cell;
+ }
+ @media (min-width: 768px) {
+ display: none;
+ }
+ @media (min-width: 992px) {
+ display: table-cell;
+ }
+ }
+ }
+ .card-text {
+ font-size: 22px;
+ margin-top: -9px;
+ position: relative;
+ }
+}
+
+
+.balance-skeleton {
+ display: flex;
+ justify-content: space-between;
+ @media (min-width: 376px) {
+ flex-direction: row;
+ }
+ .item {
+ min-width: 120px;
+ max-width: 150px;
+ margin: 0;
+ width: -webkit-fill-available;
+ @media (min-width: 376px) {
+ margin: 0 auto 0px;
+ }
+ &:last-child{
+ display: none;
+ @media (min-width: 485px) {
+ display: block;
+ }
+ @media (min-width: 768px) {
+ display: none;
+ }
+ @media (min-width: 992px) {
+ display: block;
+ }
+ }
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+ .card-text {
+ .skeleton-loader {
+ width: 100%;
+ display: block;
+ &:first-child {
+ margin: 14px auto 0;
+ max-width: 80px;
+ }
+ &:last-child {
+ margin: 10px auto 0;
+ max-width: 120px;
+ }
+ }
+ }
+}
+
+.card {
+ background-color: var(--bg);
+ height: 126px;
+}
+
+.card-title {
+ color: var(--title-fg);
+ font-size: 1rem;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.progress {
+ display: inline-flex;
+ width: 100%;
+ background-color: var(--secondary);
+ height: 1.1rem;
+ max-width: 180px;
+}
+
+.skeleton-loader {
+ max-width: 100%;
+}
+
+.more-padding {
+ padding: 24px 20px;
+}
+
+.small-bar {
+ height: 8px;
+ top: -4px;
+ max-width: 120px;
+}
+
+.loading-container {
+ min-height: 76px;
+}
+
+.main-title {
+ position: relative;
+ color: #ffffff91;
+ margin-top: -13px;
+ font-size: 10px;
+ text-transform: uppercase;
+ font-weight: 500;
+ text-align: center;
+ padding-bottom: 3px;
+}
+
+.card-wrapper {
+ .card {
+ height: auto !important;
+ }
+ .card-body {
+ display: flex;
+ flex: inherit;
+ text-align: center;
+ flex-direction: column;
+ justify-content: space-around;
+ padding: 24px 20px;
+ }
+}
+
+.retarget-sign {
+ margin-right: -3px;
+ font-size: 14px;
+ top: -2px;
+ position: relative;
+}
+
+.previous-retarget-sign {
+ margin-right: -2px;
+ font-size: 10px;
+}
+
+.symbol {
+ font-size: 13px;
+ white-space: nowrap;
+}
\ No newline at end of file
diff --git a/frontend/src/app/components/balance-widget/balance-widget.component.ts b/frontend/src/app/components/balance-widget/balance-widget.component.ts
new file mode 100644
index 000000000..91f9b5ecc
--- /dev/null
+++ b/frontend/src/app/components/balance-widget/balance-widget.component.ts
@@ -0,0 +1,72 @@
+import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
+import { StateService } from '../../services/state.service';
+import { Address, AddressTxSummary } from '../../interfaces/electrs.interface';
+import { ElectrsApiService } from '../../services/electrs-api.service';
+import { catchError, of } from 'rxjs';
+
+@Component({
+ selector: 'app-balance-widget',
+ templateUrl: './balance-widget.component.html',
+ styleUrls: ['./balance-widget.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class BalanceWidgetComponent implements OnInit, OnChanges {
+ @Input() address: string;
+ @Input() addressInfo: Address;
+ @Input() isPubkey: boolean = false;
+
+ isLoading: boolean = true;
+ error: any;
+
+ delta7d: number = 0;
+ delta30d: number = 0;
+
+ constructor(
+ public stateService: StateService,
+ private electrsApiService: ElectrsApiService,
+ private cd: ChangeDetectorRef,
+ ) { }
+
+ ngOnInit(): void {
+
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ this.isLoading = true;
+ if (!this.address || !this.addressInfo) {
+ return;
+ }
+ (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) {
+ console.log('got address summary!');
+ this.error = null;
+ this.calculateStats(addressSummary);
+ }
+ this.isLoading = false;
+ this.cd.markForCheck();
+ });
+ }
+
+ calculateStats(summary: AddressTxSummary[]): void {
+ let weekTotal = 0;
+ let monthTotal = 0;
+ const weekAgo = (Date.now() / 1000) - (60 * 60 * 24 * 7);
+ const monthAgo = (Date.now() / 1000) - (60 * 60 * 24 * 30);
+ for (let i = 0; i < summary.length && summary[i].time >= monthAgo; i++) {
+ monthTotal += summary[i].value;
+ if (summary[i].time >= weekAgo) {
+ weekTotal += summary[i].value;
+ }
+ }
+ this.delta7d = weekTotal;
+ this.delta30d = monthTotal;
+ console.log('calculated address stats: ', weekTotal, monthTotal);
+ }
+}
diff --git a/frontend/src/app/components/custom-dashboard/custom-dashboard.component.html b/frontend/src/app/components/custom-dashboard/custom-dashboard.component.html
new file mode 100644
index 000000000..6e931d0a7
--- /dev/null
+++ b/frontend/src/app/components/custom-dashboard/custom-dashboard.component.html
@@ -0,0 +1,252 @@
+
+
+
+ @for (widget of widgets; track widget.component) {
+ @switch (widget.component) {
+ @case ('fees') {
+
+ }
+ @case ('difficulty') {
+
+ }
+ @case ('goggles') {
+
+ }
+ @case ('incoming') {
+
+
+
+
+
Incoming Transactions
+
+
+
+
+
+
+
+
Minimum fee
+
Purging
+
+ <
+
+
+
+
Unconfirmed
+
+ {{ mempoolInfoData.value.memPoolInfo.size | number }} TXs
+
+
+
+
+
+ }
+ @case ('replacements') {
+
+
+
+
+ |
+ |
+ |
+ |
+
+
+
+ }
+ @case ('blocks') {
+
+
+
+
+ |
+ |
+ |
+ |
+
+
+
+ }
+ @case ('transactions') {
+
+
+
+
Recent Transactions
+
+
+ TXID |
+ Amount |
+ {{ currency }} |
+ Fee |
+
+
+
+
+
+
+
+ |
+ Confidential |
+ |
+ |
+
+
+
+
+
+
+
+
+
+
+ |
+ |
+ |
+ |
+
+
+
+ }
+ @case ('balance') {
+
+ }
+ @case ('address') {
+
+ }
+ }
+ }
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/app/components/custom-dashboard/custom-dashboard.component.scss b/frontend/src/app/components/custom-dashboard/custom-dashboard.component.scss
new file mode 100644
index 000000000..4a9ffe94a
--- /dev/null
+++ b/frontend/src/app/components/custom-dashboard/custom-dashboard.component.scss
@@ -0,0 +1,490 @@
+.dashboard-container {
+ text-align: center;
+ margin-top: 0.5rem;
+ .col {
+ margin-bottom: 1.5rem;
+ }
+}
+
+.card {
+ background-color: var(--bg);
+ height: 100%;
+}
+
+.card-title {
+ color: var(--title-fg);
+ font-size: 1rem;
+}
+
+.info-block {
+ float: left;
+ width: 350px;
+ line-height: 25px;
+}
+
+.progress {
+ display: inline-flex;
+ width: 100%;
+ background-color: var(--secondary);
+ height: 1.1rem;
+ max-width: 180px;
+}
+
+.bg-warning {
+ background-color: #b58800 !important;
+}
+
+.skeleton-loader {
+ max-width: 100%;
+}
+
+.more-padding {
+ padding: 18px;
+}
+
+.graph-card {
+ height: 100%;
+ @media (min-width: 768px) {
+ height: 415px;
+ }
+ @media (min-width: 992px) {
+ height: 510px;
+ }
+}
+
+.mempool-info-data {
+ min-height: 56px;
+ display: block;
+ @media (min-width: 485px) {
+ display: flex;
+ flex-direction: row;
+ }
+ &.lbtc-pegs-stats {
+ display: flex;
+ flex-direction: row;
+ }
+ h5 {
+ margin-bottom: 10px;
+ }
+ .item {
+ width: 50%;
+ margin: 0px auto 20px;
+ display: inline-block;
+ @media (min-width: 485px) {
+ margin: 0px auto 10px;
+ }
+ @media (min-width: 768px) {
+ margin: 0px auto 0px;
+ }
+ &:last-child {
+ margin: 0px auto 0px;
+ }
+ &:nth-child(2) {
+ order: 2;
+ @media (min-width: 485px) {
+ order: 3;
+ }
+ }
+ &:nth-child(3) {
+ order: 3;
+ @media (min-width: 485px) {
+ order: 2;
+ display: block;
+ }
+ @media (min-width: 768px) {
+ display: none;
+ }
+ @media (min-width: 992px) {
+ display: block;
+ }
+ }
+ .card-text {
+ font-size: 18px;
+ span {
+ color: var(--transparent-fg);
+ font-size: 12px;
+ }
+ .bitcoin-color {
+ color: var(--orange);
+ }
+ }
+ .progress {
+ width: 90%;
+ @media (min-width: 768px) {
+ width: 100%;
+ }
+ }
+ }
+ .bar {
+ width: 93%;
+ margin: 0px 5px 20px;
+ @media (min-width: 485px) {
+ max-width: 200px;
+ margin: 0px auto 0px;
+ }
+ }
+ .skeleton-loader {
+ width: 100%;
+ max-width: 100px;
+ display: block;
+ margin: 18px auto 0;
+ }
+ .skeleton-loader-big {
+ max-width: 180px;
+ }
+}
+
+.latest-transactions {
+ width: 100%;
+ text-align: left;
+ table-layout:fixed;
+ tr, td, th {
+ border: 0px;
+ padding-top: 0.71rem !important;
+ padding-bottom: 0.75rem !important;
+ }
+ td {
+ overflow:hidden;
+ width: 25%;
+ }
+ .table-cell-satoshis {
+ display: none;
+ text-align: right;
+ @media (min-width: 576px) {
+ display: table-cell;
+ }
+ @media (min-width: 768px) {
+ display: none;
+ }
+ @media (min-width: 1100px) {
+ display: table-cell;
+ }
+ }
+ .table-cell-fiat {
+ display: none;
+ text-align: right;
+ @media (min-width: 485px) {
+ display: table-cell;
+ }
+ @media (min-width: 768px) {
+ display: none;
+ }
+ @media (min-width: 992px) {
+ display: table-cell;
+ }
+ }
+ .table-cell-fees {
+ text-align: right;
+ }
+}
+.skeleton-loader-transactions {
+ max-width: 250px;
+ position: relative;
+ top: 2px;
+ margin-bottom: -3px;
+ height: 18px;
+}
+
+.lastest-blocks-table {
+ width: 100%;
+ text-align: left;
+ tr, td, th {
+ border: 0px;
+ padding-top: 0.65rem !important;
+ padding-bottom: 0.7rem !important;
+ }
+ .table-cell-height {
+ width: 15%;
+ }
+ .table-cell-mined {
+ width: 35%;
+ text-align: left;
+ }
+ .table-cell-transaction-count {
+ display: none;
+ text-align: right;
+ width: 20%;
+ display: table-cell;
+ }
+ .table-cell-size {
+ display: none;
+ text-align: center;
+ width: 30%;
+ @media (min-width: 485px) {
+ display: table-cell;
+ }
+ @media (min-width: 768px) {
+ display: none;
+ }
+ @media (min-width: 992px) {
+ display: table-cell;
+ }
+ }
+}
+
+.lastest-replacements-table {
+ width: 100%;
+ text-align: left;
+ table-layout:fixed;
+ tr, td, th {
+ border: 0px;
+ padding-top: 0.71rem !important;
+ padding-bottom: 0.75rem !important;
+ }
+ td {
+ overflow:hidden;
+ width: 25%;
+ }
+ .table-cell-txid {
+ width: 25%;
+ text-align: start;
+ }
+ .table-cell-old-fee {
+ width: 25%;
+ text-align: end;
+
+ @media(max-width: 1080px) {
+ display: none;
+ }
+ }
+ .table-cell-new-fee {
+ width: 20%;
+ text-align: end;
+ }
+ .table-cell-badges {
+ width: 23%;
+ padding-right: 0;
+ padding-left: 5px;
+ text-align: end;
+
+ .badge {
+ margin-left: 5px;
+ }
+ }
+}
+
+.mempool-graph {
+ height: 255px;
+ @media (min-width: 768px) {
+ height: 285px;
+ }
+ @media (min-width: 992px) {
+ height: 370px;
+ }
+}
+.loadingGraphs{
+ height: 250px;
+ display: grid;
+ place-items: center;
+}
+
+.inc-tx-progress-bar {
+ max-width: 250px;
+ .progress-bar {
+ padding: 4px;
+ }
+}
+
+.terms-of-service {
+ margin-top: 1rem;
+}
+
+.small-bar {
+ height: 8px;
+ top: -4px;
+ max-width: 120px;
+}
+
+.loading-container {
+ min-height: 76px;
+}
+
+.main-title {
+ position: relative;
+ color: #ffffff91;
+ margin-top: -13px;
+ font-size: 10px;
+ text-transform: uppercase;
+ font-weight: 500;
+ text-align: center;
+ padding-bottom: 3px;
+}
+
+.card-wrapper {
+ .card {
+ height: auto !important;
+ }
+ .card-body {
+ display: flex;
+ flex: inherit;
+ text-align: center;
+ flex-direction: column;
+ justify-content: space-around;
+ padding: 22px 20px;
+ &.liquid {
+ height: 124.5px;
+ }
+ }
+ .less-padding {
+ padding: 20px 20px;
+ }
+}
+
+.retarget-sign {
+ margin-right: -3px;
+ font-size: 14px;
+ top: -2px;
+ position: relative;
+}
+
+.previous-retarget-sign {
+ margin-right: -2px;
+ font-size: 10px;
+}
+
+.assetIcon {
+ width: 40px;
+ height: 40px;
+}
+
+.asset-title {
+ text-align: left;
+ vertical-align: middle;
+}
+
+.asset-icon {
+ width: 65px;
+ height: 65px;
+ vertical-align: middle;
+}
+
+.circulating-amount {
+ text-align: right;
+ width: 100%;
+ vertical-align: middle;
+}
+
+.clear-link {
+ color: white;
+}
+
+.pool-name {
+ display: inline-block;
+ vertical-align: text-top;
+ padding-left: 10px;
+}
+
+.title-link, .title-link:hover, .title-link:focus, .title-link:active {
+ display: block;
+ margin-bottom: 10px;
+ text-decoration: none;
+ color: inherit;
+}
+
+.mempool-block-wrapper {
+ max-height: 410px;
+ max-width: 410px;
+ margin: auto;
+
+ @media (min-width: 768px) {
+ max-height: 344px;
+ max-width: 344px;
+ }
+ @media (min-width: 992px) {
+ max-height: 410px;
+ max-width: 410px;
+ }
+}
+
+.goggle-badge {
+ margin: 6px 5px 8px;
+ background: none;
+ border: solid 2px var(--primary);
+ cursor: pointer;
+
+ &.active {
+ background: var(--primary);
+ }
+}
+
+.btn-xs {
+ padding: 0.35rem 0.5rem;
+ font-size: 12px;
+}
+
+.quick-filter {
+ margin-top: 5px;
+ margin-bottom: 6px;
+}
+
+.card-liquid {
+ background-color: var(--bg);
+ height: 418px;
+ @media (min-width: 992px) {
+ height: 512px;
+ }
+ &.smaller {
+ height: 408px;
+ }
+}
+
+.card-title-liquid {
+ padding-top: 20px;
+ margin-left: 10px;
+}
+
+.in-progress-message {
+ position: relative;
+ color: #ffffff91;
+ margin-top: 20px;
+ text-align: center;
+ padding-bottom: 3px;
+ font-weight: 500;
+}
+
+.stats-card {
+ min-height: 56px;
+ display: block;
+ @media (min-width: 485px) {
+ display: flex;
+ flex-direction: row;
+ }
+ h5 {
+ margin-bottom: 10px;
+ }
+ .item {
+ width: 50%;
+ display: inline-block;
+ margin: 0px auto 20px;
+ &:nth-child(2) {
+ order: 2;
+ @media (min-width: 485px) {
+ order: 3;
+ }
+ }
+ &:nth-child(3) {
+ order: 3;
+ @media (min-width: 485px) {
+ order: 2;
+ display: block;
+ }
+ @media (min-width: 768px) {
+ display: none;
+ }
+ @media (min-width: 992px) {
+ display: block;
+ }
+ }
+ .card-title {
+ font-size: 1rem;
+ color: var(--title-fg);
+ }
+ .card-text {
+ font-size: 18px;
+ span {
+ color: var(--transparent-fg);
+ font-size: 12px;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/app/components/custom-dashboard/custom-dashboard.component.ts b/frontend/src/app/components/custom-dashboard/custom-dashboard.component.ts
new file mode 100644
index 000000000..4cfffe8b6
--- /dev/null
+++ b/frontend/src/app/components/custom-dashboard/custom-dashboard.component.ts
@@ -0,0 +1,323 @@
+import { AfterViewInit, ChangeDetectionStrategy, Component, HostListener, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
+import { combineLatest, merge, Observable, of, Subject, Subscription } from 'rxjs';
+import { catchError, filter, map, scan, shareReplay, switchMap, tap } from 'rxjs/operators';
+import { BlockExtended, OptimizedMempoolStats, TransactionStripped } from '../../interfaces/node-api.interface';
+import { MempoolInfo, ReplacementInfo } from '../../interfaces/websocket.interface';
+import { ApiService } from '../../services/api.service';
+import { StateService } from '../../services/state.service';
+import { WebsocketService } from '../../services/websocket.service';
+import { SeoService } from '../../services/seo.service';
+import { ActiveFilter, FilterMode, GradientMode, toFlags } from '../../shared/filters.utils';
+import { detectWebGL } from '../../shared/graphs.utils';
+import { Address } from '../../interfaces/electrs.interface';
+import { ElectrsApiService } from '../../services/electrs-api.service';
+
+interface MempoolBlocksData {
+ blocks: number;
+ size: number;
+}
+
+interface MempoolInfoData {
+ memPoolInfo: MempoolInfo;
+ vBytesPerSecond: number;
+ progressWidth: string;
+ progressColor: string;
+}
+
+interface MempoolStatsData {
+ mempool: OptimizedMempoolStats[];
+ weightPerSecond: any;
+}
+
+@Component({
+ selector: 'app-custom-dashboard',
+ templateUrl: './custom-dashboard.component.html',
+ styleUrls: ['./custom-dashboard.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush
+})
+export class CustomDashboardComponent implements OnInit, OnDestroy, AfterViewInit {
+ network$: Observable
;
+ mempoolBlocksData$: Observable;
+ mempoolInfoData$: Observable;
+ mempoolLoadingStatus$: Observable;
+ vBytesPerSecondLimit = 1667;
+ transactions$: Observable;
+ blocks$: Observable;
+ replacements$: Observable;
+ latestBlockHeight: number;
+ mempoolTransactionsWeightPerSecondData: any;
+ mempoolStats$: Observable;
+ transactionsWeightPerSecondOptions: any;
+ isLoadingWebSocket$: Observable;
+ isLoad: boolean = true;
+ filterSubscription: Subscription;
+ mempoolInfoSubscription: Subscription;
+ currencySubscription: Subscription;
+ currency: string;
+ incomingGraphHeight: number = 300;
+ graphHeight: number = 300;
+ webGlEnabled = true;
+
+ widgets;
+
+ addressSubscription: Subscription;
+ address: Address;
+
+ goggleResolution = 82;
+ goggleCycle: { index: number, name: string, mode: FilterMode, filters: string[], gradient: GradientMode }[] = [
+ { index: 0, name: $localize`:@@dfc3c34e182ea73c5d784ff7c8135f087992dac1:All`, mode: 'and', filters: [], gradient: 'age' },
+ { index: 1, name: $localize`Consolidation`, mode: 'and', filters: ['consolidation'], gradient: 'fee' },
+ { index: 2, name: $localize`Coinjoin`, mode: 'and', filters: ['coinjoin'], gradient: 'fee' },
+ { index: 3, name: $localize`Data`, mode: 'or', filters: ['inscription', 'fake_pubkey', 'op_return'], gradient: 'fee' },
+ ];
+ goggleFlags = 0n;
+ goggleMode: FilterMode = 'and';
+ gradientMode: GradientMode = 'age';
+ goggleIndex = 0;
+
+ private destroy$ = new Subject();
+
+ constructor(
+ public stateService: StateService,
+ private apiService: ApiService,
+ private electrsApiService: ElectrsApiService,
+ private websocketService: WebsocketService,
+ private seoService: SeoService,
+ @Inject(PLATFORM_ID) private platformId: Object,
+ ) {
+ this.webGlEnabled = this.stateService.isBrowser && detectWebGL();
+ this.widgets = this.stateService.env.customize?.dashboard.widgets || [];
+ }
+
+ ngAfterViewInit(): void {
+ this.stateService.focusSearchInputDesktop();
+ }
+
+ ngOnDestroy(): void {
+ this.filterSubscription.unsubscribe();
+ this.mempoolInfoSubscription.unsubscribe();
+ this.currencySubscription.unsubscribe();
+ this.websocketService.stopTrackRbfSummary();
+ if (this.addressSubscription) {
+ this.addressSubscription.unsubscribe();
+ this.websocketService.stopTrackingAddress();
+ this.address = null;
+ }
+ this.destroy$.next(1);
+ this.destroy$.complete();
+ }
+
+ ngOnInit(): void {
+ this.onResize();
+ this.isLoadingWebSocket$ = this.stateService.isLoadingWebSocket$;
+ this.seoService.resetTitle();
+ this.seoService.resetDescription();
+ this.websocketService.want(['blocks', 'stats', 'mempool-blocks', 'live-2h-chart']);
+ this.websocketService.startTrackRbfSummary();
+ this.network$ = merge(of(''), this.stateService.networkChanged$);
+ this.mempoolLoadingStatus$ = this.stateService.loadingIndicators$
+ .pipe(
+ map((indicators) => indicators.mempool !== undefined ? indicators.mempool : 100)
+ );
+
+ this.filterSubscription = this.stateService.activeGoggles$.subscribe((active: ActiveFilter) => {
+ const activeFilters = active.filters.sort().join(',');
+ for (const goggle of this.goggleCycle) {
+ if (goggle.mode === active.mode) {
+ const goggleFilters = goggle.filters.sort().join(',');
+ if (goggleFilters === activeFilters) {
+ this.goggleIndex = goggle.index;
+ this.goggleFlags = toFlags(goggle.filters);
+ this.goggleMode = goggle.mode;
+ this.gradientMode = active.gradient;
+ return;
+ }
+ }
+ }
+ this.goggleCycle.push({
+ index: this.goggleCycle.length,
+ name: 'Custom',
+ mode: active.mode,
+ filters: active.filters,
+ gradient: active.gradient,
+ });
+ this.goggleIndex = this.goggleCycle.length - 1;
+ this.goggleFlags = toFlags(active.filters);
+ this.goggleMode = active.mode;
+ });
+
+ this.mempoolInfoData$ = combineLatest([
+ this.stateService.mempoolInfo$,
+ this.stateService.vbytesPerSecond$
+ ]).pipe(
+ map(([mempoolInfo, vbytesPerSecond]) => {
+ const percent = Math.round((Math.min(vbytesPerSecond, this.vBytesPerSecondLimit) / this.vBytesPerSecondLimit) * 100);
+
+ let progressColor = 'bg-success';
+ if (vbytesPerSecond > 1667) {
+ progressColor = 'bg-warning';
+ }
+ if (vbytesPerSecond > 3000) {
+ progressColor = 'bg-danger';
+ }
+
+ const mempoolSizePercentage = (mempoolInfo.usage / mempoolInfo.maxmempool * 100);
+ let mempoolSizeProgress = 'bg-danger';
+ if (mempoolSizePercentage <= 50) {
+ mempoolSizeProgress = 'bg-success';
+ } else if (mempoolSizePercentage <= 75) {
+ mempoolSizeProgress = 'bg-warning';
+ }
+
+ return {
+ memPoolInfo: mempoolInfo,
+ vBytesPerSecond: vbytesPerSecond,
+ progressWidth: percent + '%',
+ progressColor: progressColor,
+ mempoolSizeProgress: mempoolSizeProgress,
+ };
+ })
+ );
+
+ this.mempoolInfoSubscription = this.mempoolInfoData$.subscribe();
+
+ this.mempoolBlocksData$ = this.stateService.mempoolBlocks$
+ .pipe(
+ map((mempoolBlocks) => {
+ const size = mempoolBlocks.map((m) => m.blockSize).reduce((a, b) => a + b, 0);
+ const vsize = mempoolBlocks.map((m) => m.blockVSize).reduce((a, b) => a + b, 0);
+
+ return {
+ size: size,
+ blocks: Math.ceil(vsize / this.stateService.blockVSize)
+ };
+ })
+ );
+
+ this.transactions$ = this.stateService.transactions$;
+
+ this.blocks$ = this.stateService.blocks$
+ .pipe(
+ tap((blocks) => {
+ this.latestBlockHeight = blocks[0].height;
+ }),
+ switchMap((blocks) => {
+ if (this.stateService.env.MINING_DASHBOARD === true) {
+ for (const block of blocks) {
+ // @ts-ignore: Need to add an extra field for the template
+ block.extras.pool.logo = `/resources/mining-pools/` +
+ block.extras.pool.slug + '.svg';
+ }
+ }
+ return of(blocks.slice(0, 6));
+ })
+ );
+
+ this.replacements$ = this.stateService.rbfLatestSummary$;
+
+ this.mempoolStats$ = this.stateService.connectionState$
+ .pipe(
+ filter((state) => state === 2),
+ switchMap(() => this.apiService.list2HStatistics$().pipe(
+ catchError((e) => {
+ return of(null);
+ })
+ )),
+ switchMap((mempoolStats) => {
+ return merge(
+ this.stateService.live2Chart$
+ .pipe(
+ scan((acc, stats) => {
+ acc.unshift(stats);
+ acc = acc.slice(0, 120);
+ return acc;
+ }, (mempoolStats || []))
+ ),
+ of(mempoolStats)
+ );
+ }),
+ map((mempoolStats) => {
+ if (mempoolStats) {
+ return {
+ mempool: mempoolStats,
+ weightPerSecond: this.handleNewMempoolData(mempoolStats.concat([])),
+ };
+ } else {
+ return null;
+ }
+ }),
+ shareReplay(1),
+ );
+
+ this.currencySubscription = this.stateService.fiatCurrency$.subscribe((fiat) => {
+ this.currency = fiat;
+ });
+
+ this.startAddressSubscription();
+ }
+
+ handleNewMempoolData(mempoolStats: OptimizedMempoolStats[]) {
+ mempoolStats.reverse();
+ const labels = mempoolStats.map(stats => stats.added);
+
+ return {
+ labels: labels,
+ series: [mempoolStats.map((stats) => [stats.added * 1000, stats.vbytes_per_second])],
+ };
+ }
+
+ trackByBlock(index: number, block: BlockExtended) {
+ return block.height;
+ }
+
+ getArrayFromNumber(num: number): number[] {
+ return Array.from({ length: num }, (_, i) => i + 1);
+ }
+
+ setFilter(index): void {
+ const selected = this.goggleCycle[index];
+ this.stateService.activeGoggles$.next(selected);
+ }
+
+ startAddressSubscription(): void {
+ if (this.stateService.env.customize && this.stateService.env.customize.dashboard.widgets.some(w => w.props?.address)) {
+ const address = this.stateService.env.customize.dashboard.widgets.find(w => w.props?.address).props.address;
+ const addressString = (/^[A-Z]{2,5}1[AC-HJ-NP-Z02-9]{8,100}|04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}$/.test(address)) ? address.toLowerCase() : address;
+
+ this.addressSubscription = (
+ addressString.match(/04[a-fA-F0-9]{128}|(02|03)[a-fA-F0-9]{64}/)
+ ? this.electrsApiService.getPubKeyAddress$(addressString)
+ : this.electrsApiService.getAddress$(addressString)
+ ).pipe(
+ catchError((err) => {
+ // this.isLoadingAddress = false;
+ // this.error = err;
+ // this.seoService.logSoft404();
+ console.log(err);
+ return of(null);
+ }),
+ filter((address) => !!address)
+ ).subscribe((address: Address) => {
+ this.websocketService.startTrackAddress(address.address);
+ this.address = address;
+ });
+ }
+ }
+
+ @HostListener('window:resize', ['$event'])
+ onResize(): void {
+ if (window.innerWidth >= 992) {
+ this.incomingGraphHeight = 300;
+ this.goggleResolution = 82;
+ this.graphHeight = 400;
+ } else if (window.innerWidth >= 768) {
+ this.incomingGraphHeight = 215;
+ this.goggleResolution = 80;
+ this.graphHeight = 310;
+ } else {
+ this.incomingGraphHeight = 180;
+ this.goggleResolution = 86;
+ this.graphHeight = 310;
+ }
+ }
+}
diff --git a/frontend/src/app/graphs/graphs.module.ts b/frontend/src/app/graphs/graphs.module.ts
index 761bd8e1f..83aebed73 100644
--- a/frontend/src/app/graphs/graphs.module.ts
+++ b/frontend/src/app/graphs/graphs.module.ts
@@ -27,6 +27,7 @@ import { PoolRankingComponent } from '../components/pool-ranking/pool-ranking.co
import { PoolComponent } from '../components/pool/pool.component';
import { TelevisionComponent } from '../components/television/television.component';
import { DashboardComponent } from '../dashboard/dashboard.component';
+import { CustomDashboardComponent } from '../components/custom-dashboard/custom-dashboard.component';
import { MiningDashboardComponent } from '../components/mining-dashboard/mining-dashboard.component';
import { AcceleratorDashboardComponent } from '../components/acceleration/accelerator-dashboard/accelerator-dashboard.component';
import { HashrateChartComponent } from '../components/hashrate-chart/hashrate-chart.component';
@@ -39,6 +40,7 @@ import { CommonModule } from '@angular/common';
@NgModule({
declarations: [
DashboardComponent,
+ CustomDashboardComponent,
MempoolBlockComponent,
AddressComponent,
diff --git a/frontend/src/app/graphs/graphs.routing.module.ts b/frontend/src/app/graphs/graphs.routing.module.ts
index e069022cd..9c7d55930 100644
--- a/frontend/src/app/graphs/graphs.routing.module.ts
+++ b/frontend/src/app/graphs/graphs.routing.module.ts
@@ -17,10 +17,16 @@ import { StartComponent } from '../components/start/start.component';
import { StatisticsComponent } from '../components/statistics/statistics.component';
import { TelevisionComponent } from '../components/television/television.component';
import { DashboardComponent } from '../dashboard/dashboard.component';
+import { CustomDashboardComponent } from '../components/custom-dashboard/custom-dashboard.component';
import { AccelerationFeesGraphComponent } from '../components/acceleration/acceleration-fees-graph/acceleration-fees-graph.component';
import { AccelerationsListComponent } from '../components/acceleration/accelerations-list/accelerations-list.component';
import { AddressComponent } from '../components/address/address.component';
+const browserWindow = window || {};
+// @ts-ignore
+const browserWindowEnv = browserWindow.__env || {};
+const isCustomized = browserWindowEnv?.customize;
+
const routes: Routes = [
{
path: '',
@@ -149,7 +155,7 @@ const routes: Routes = [
component: StartComponent,
children: [{
path: '',
- component: DashboardComponent,
+ component: isCustomized ? CustomDashboardComponent : DashboardComponent,
}]
},
]
diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts
index 50268029b..c900090a3 100644
--- a/frontend/src/app/shared/shared.module.ts
+++ b/frontend/src/app/shared/shared.module.ts
@@ -65,6 +65,7 @@ import { FeesBoxComponent } from '../components/fees-box/fees-box.component';
import { DifficultyComponent } from '../components/difficulty/difficulty.component';
import { DifficultyTooltipComponent } from '../components/difficulty/difficulty-tooltip.component';
import { DifficultyMiningComponent } from '../components/difficulty-mining/difficulty-mining.component';
+import { BalanceWidgetComponent } from '../components/balance-widget/balance-widget.component';
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';
@@ -173,6 +174,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
DifficultyComponent,
DifficultyMiningComponent,
DifficultyTooltipComponent,
+ BalanceWidgetComponent,
RbfTimelineComponent,
RbfTimelineTooltipComponent,
PushTransactionComponent,
@@ -309,6 +311,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
DifficultyComponent,
DifficultyMiningComponent,
DifficultyTooltipComponent,
+ BalanceWidgetComponent,
RbfTimelineComponent,
RbfTimelineTooltipComponent,
PushTransactionComponent,