diff --git a/frontend/src/app/app-routing.module.ts b/frontend/src/app/app-routing.module.ts
index 79a8e1c02..7ca9e107b 100644
--- a/frontend/src/app/app-routing.module.ts
+++ b/frontend/src/app/app-routing.module.ts
@@ -4,6 +4,7 @@ import { AppPreloadingStrategy } from './app.preloading-strategy'
import { StartComponent } from './components/start/start.component';
import { TransactionComponent } from './components/transaction/transaction.component';
import { BlockComponent } from './components/block/block.component';
+import { BlockViewComponent } from './components/block-view/block-view.component';
import { ClockComponent } from './components/clock/clock.component';
import { AddressComponent } from './components/address/address.component';
import { MasterPageComponent } from './components/master-page/master-page.component';
@@ -373,6 +374,10 @@ let routes: Routes = [
path: 'clock/:mode/:index',
component: ClockComponent,
},
+ {
+ path: 'view/block/:id',
+ component: BlockViewComponent,
+ },
{
path: 'status',
data: { networks: ['bitcoin', 'liquid'] },
diff --git a/frontend/src/app/components/block-view/block-view.component.html b/frontend/src/app/components/block-view/block-view.component.html
new file mode 100644
index 000000000..905c69198
--- /dev/null
+++ b/frontend/src/app/components/block-view/block-view.component.html
@@ -0,0 +1,13 @@
+
diff --git a/frontend/src/app/components/block-view/block-view.component.scss b/frontend/src/app/components/block-view/block-view.component.scss
new file mode 100644
index 000000000..782d416d8
--- /dev/null
+++ b/frontend/src/app/components/block-view/block-view.component.scss
@@ -0,0 +1,22 @@
+.block-wrapper {
+ width: 100vw;
+ height: 100vh;
+ background: #181b2d;
+}
+
+.block-container {
+ flex-grow: 0;
+ flex-shrink: 0;
+ width: 100vw;
+ max-width: 100vh;
+ height: 100vh;
+ padding: 0;
+ margin: auto;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ * {
+ flex-grow: 1;
+ }
+}
\ No newline at end of file
diff --git a/frontend/src/app/components/block-view/block-view.component.ts b/frontend/src/app/components/block-view/block-view.component.ts
new file mode 100644
index 000000000..ef1a7247b
--- /dev/null
+++ b/frontend/src/app/components/block-view/block-view.component.ts
@@ -0,0 +1,176 @@
+import { Component, OnInit, OnDestroy, ViewChild, HostListener } from '@angular/core';
+import { ActivatedRoute, ParamMap } from '@angular/router';
+import { ElectrsApiService } from '../../services/electrs-api.service';
+import { switchMap, tap, throttleTime, catchError, shareReplay, startWith, pairwise, filter } from 'rxjs/operators';
+import { of, Subscription, asyncScheduler } from 'rxjs';
+import { StateService } from '../../services/state.service';
+import { SeoService } from '../../services/seo.service';
+import { OpenGraphService } from '../../services/opengraph.service';
+import { BlockExtended, TransactionStripped } from '../../interfaces/node-api.interface';
+import { ApiService } from '../../services/api.service';
+import { seoDescriptionNetwork } from '../../shared/common.utils';
+import { BlockOverviewGraphComponent } from '../block-overview-graph/block-overview-graph.component';
+
+function bestFitResolution(min, max, n) {
+ const target = (min + max) / 2;
+ let bestScore = Infinity;
+ let best = null;
+ for (let i = min; i <= max; i++) {
+ const remainder = (n % i);
+ if (remainder < bestScore || (remainder === bestScore && (Math.abs(i - target) < Math.abs(best - target)))) {
+ bestScore = remainder;
+ best = i;
+ }
+ }
+ return best;
+}
+
+@Component({
+ selector: 'app-block-view',
+ templateUrl: './block-view.component.html',
+ styleUrls: ['./block-view.component.scss']
+})
+export class BlockViewComponent implements OnInit, OnDestroy {
+ network = '';
+ block: BlockExtended;
+ blockHeight: number;
+ blockHash: string;
+ rawId: string;
+ isLoadingBlock = true;
+ strippedTransactions: TransactionStripped[];
+ isLoadingOverview = true;
+ autofit: boolean = false;
+ resolution: number = 80;
+
+ overviewSubscription: Subscription;
+ networkChangedSubscription: Subscription;
+ queryParamsSubscription: Subscription;
+
+ @ViewChild('blockGraph') blockGraph: BlockOverviewGraphComponent;
+
+ constructor(
+ private route: ActivatedRoute,
+ private electrsApiService: ElectrsApiService,
+ public stateService: StateService,
+ private seoService: SeoService,
+ private openGraphService: OpenGraphService,
+ private apiService: ApiService
+ ) { }
+
+ ngOnInit() {
+ this.network = this.stateService.network;
+
+ this.queryParamsSubscription = this.route.queryParams.subscribe((params) => {
+ this.autofit = params.autofit === 'true';
+ if (this.autofit) {
+ this.onResize();
+ }
+ });
+
+ const block$ = this.route.paramMap.pipe(
+ switchMap((params: ParamMap) => {
+ this.rawId = params.get('id') || '';
+
+ const blockHash: string = params.get('id') || '';
+ this.block = undefined;
+
+ let isBlockHeight = false;
+ if (/^[0-9]+$/.test(blockHash)) {
+ isBlockHeight = true;
+ } else {
+ this.blockHash = blockHash;
+ }
+
+ this.isLoadingBlock = true;
+ this.isLoadingOverview = true;
+
+ if (isBlockHeight) {
+ return this.electrsApiService.getBlockHashFromHeight$(parseInt(blockHash, 10))
+ .pipe(
+ switchMap((hash) => {
+ if (hash) {
+ this.blockHash = hash;
+ return this.apiService.getBlock$(hash);
+ } else {
+ return null;
+ }
+ }),
+ catchError(() => {
+ return of(null);
+ }),
+ );
+ }
+ return this.apiService.getBlock$(blockHash);
+ }),
+ filter((block: BlockExtended | void) => block != null),
+ tap((block: BlockExtended) => {
+ this.block = block;
+ this.blockHeight = block.height;
+
+ this.seoService.setTitle($localize`:@@block.component.browser-title:Block ${block.height}:BLOCK_HEIGHT:: ${block.id}:BLOCK_ID:`);
+ if( this.stateService.network === 'liquid' || this.stateService.network === 'liquidtestnet' ) {
+ this.seoService.setDescription($localize`:@@meta.description.liquid.block:See size, weight, fee range, included transactions, and more for Liquid${seoDescriptionNetwork(this.stateService.network)} block ${block.height}:BLOCK_HEIGHT: (${block.id}:BLOCK_ID:).`);
+ } else {
+ this.seoService.setDescription($localize`:@@meta.description.bitcoin.block:See size, weight, fee range, included transactions, audit (expected v actual), and more for Bitcoin${seoDescriptionNetwork(this.stateService.network)} block ${block.height}:BLOCK_HEIGHT: (${block.id}:BLOCK_ID:).`);
+ }
+ this.isLoadingBlock = false;
+ this.isLoadingOverview = true;
+ }),
+ shareReplay(1)
+ );
+
+ this.overviewSubscription = block$.pipe(
+ switchMap((block) => this.apiService.getStrippedBlockTransactions$(block.id)
+ .pipe(
+ catchError(() => {
+ return of([]);
+ }),
+ switchMap((transactions) => {
+ return of(transactions);
+ })
+ )
+ ),
+ )
+ .subscribe((transactions: TransactionStripped[]) => {
+ this.strippedTransactions = transactions;
+ this.isLoadingOverview = false;
+ if (this.blockGraph) {
+ this.blockGraph.destroy();
+ this.blockGraph.setup(this.strippedTransactions);
+ }
+ },
+ () => {
+ this.isLoadingOverview = false;
+ if (this.blockGraph) {
+ this.blockGraph.destroy();
+ }
+ });
+
+ this.networkChangedSubscription = this.stateService.networkChanged$
+ .subscribe((network) => this.network = network);
+ }
+
+ @HostListener('window:resize', ['$event'])
+ onResize(): void {
+ if (this.autofit) {
+ this.resolution = bestFitResolution(64, 96, Math.min(window.innerWidth, window.innerHeight));
+ console.log('resized, new resolution ', this.resolution, window.innerWidth, window.innerHeight);
+ // if (this.blockGraph && this.strippedTransactions) {
+ // this.blockGraph.destroy();
+ // this.blockGraph.setup(this.strippedTransactions);
+ // }
+ }
+ }
+
+ ngOnDestroy() {
+ if (this.overviewSubscription) {
+ this.overviewSubscription.unsubscribe();
+ }
+ if (this.networkChangedSubscription) {
+ this.networkChangedSubscription.unsubscribe();
+ }
+ if (this.queryParamsSubscription) {
+ this.queryParamsSubscription.unsubscribe();
+ }
+ }
+}
diff --git a/frontend/src/app/shared/shared.module.ts b/frontend/src/app/shared/shared.module.ts
index f7c253a96..bba70a2ce 100644
--- a/frontend/src/app/shared/shared.module.ts
+++ b/frontend/src/app/shared/shared.module.ts
@@ -97,6 +97,7 @@ import { AcceleratePreviewComponent } from '../components/accelerate-preview/acc
import { AccelerateFeeGraphComponent } from '../components/accelerate-preview/accelerate-fee-graph.component';
import { MempoolErrorComponent } from './components/mempool-error/mempool-error.component';
+import { BlockViewComponent } from '../components/block-view/block-view.component';
import { MempoolBlockOverviewComponent } from '../components/mempool-block-overview/mempool-block-overview.component';
import { ClockchainComponent } from '../components/clockchain/clockchain.component';
import { ClockFaceComponent } from '../components/clock-face/clock-face.component';
@@ -134,6 +135,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
FiatCurrencyPipe,
ColoredPriceDirective,
BlockchainComponent,
+ BlockViewComponent,
MempoolBlocksComponent,
BlockchainBlocksComponent,
AmountComponent,
@@ -196,6 +198,7 @@ import { OnlyVsizeDirective, OnlyWeightDirective } from './components/weight-dir
AccelerateFeeGraphComponent,
CalculatorComponent,
BitcoinsatoshisPipe,
+ BlockViewComponent,
MempoolBlockOverviewComponent,
ClockchainComponent,
ClockComponent,