diff --git a/frontend/src/app/components/block/block-preview.component.ts b/frontend/src/app/components/block/block-preview.component.ts
index f5b31e846..869043edd 100644
--- a/frontend/src/app/components/block/block-preview.component.ts
+++ b/frontend/src/app/components/block/block-preview.component.ts
@@ -8,7 +8,7 @@ import { SeoService } from '@app/services/seo.service';
import { OpenGraphService } from '@app/services/opengraph.service';
import { BlockExtended, TransactionStripped } from '@interfaces/node-api.interface';
import { ApiService } from '@app/services/api.service';
-import { seoDescriptionNetwork } from '@app/shared/common.utils';
+import { seoDescriptionNetwork, parseMinerName } from '@app/shared/common.utils';
import { BlockOverviewGraphComponent } from '@components/block-overview-graph/block-overview-graph.component';
import { ServicesApiServices } from '@app/services/services-api.service';
@@ -99,6 +99,7 @@ export class BlockPreviewComponent implements OnInit, OnDestroy {
filter((block: BlockExtended | void) => block != null),
tap((block: BlockExtended) => {
this.block = block;
+ this.block.minerName = parseMinerName(block);
this.blockHeight = block.height;
this.seoService.setTitle($localize`:@@block.component.browser-title:Block ${block.height}:BLOCK_HEIGHT:: ${block.id}:BLOCK_ID:`);
diff --git a/frontend/src/app/components/block/block.component.html b/frontend/src/app/components/block/block.component.html
index 105cdf31a..c87d51ebe 100644
--- a/frontend/src/app/components/block/block.component.html
+++ b/frontend/src/app/components/block/block.component.html
@@ -182,11 +182,11 @@
Miner |
- 1 && block.extras.pool.minerNames[1] != ''">
- @if (block.extras.pool.minerNames[1].length > 16) {
- {{ block.extras.pool.minerNames[1].slice(0, 15) }}…
+
+ @if (block.minerName.length > 16) {
+ {{ block.minerName.slice(0, 15) }}…
} @else {
- {{ block.extras.pool.minerNames[1] }}
+ {{ block.minerName }}
}
diff --git a/frontend/src/app/components/block/block.component.ts b/frontend/src/app/components/block/block.component.ts
index ddcf023ed..6ce5cbeee 100644
--- a/frontend/src/app/components/block/block.component.ts
+++ b/frontend/src/app/components/block/block.component.ts
@@ -2,7 +2,7 @@ import { Component, OnInit, OnDestroy, ViewChildren, QueryList, ChangeDetectorRe
import { Location } from '@angular/common';
import { ActivatedRoute, ParamMap, Params, Router } from '@angular/router';
import { ElectrsApiService } from '@app/services/electrs-api.service';
-import { switchMap, tap, throttleTime, catchError, map, shareReplay, startWith, filter, take } from 'rxjs/operators';
+import { switchMap, tap, throttleTime, catchError, map, shareReplay, startWith, filter } from 'rxjs/operators';
import { Observable, of, Subscription, asyncScheduler, EMPTY, combineLatest, forkJoin } from 'rxjs';
import { StateService } from '@app/services/state.service';
import { SeoService } from '@app/services/seo.service';
@@ -12,7 +12,7 @@ import { Acceleration, BlockAudit, BlockExtended, TransactionStripped } from '@i
import { ApiService } from '@app/services/api.service';
import { BlockOverviewGraphComponent } from '@components/block-overview-graph/block-overview-graph.component';
import { detectWebGL } from '@app/shared/graphs.utils';
-import { seoDescriptionNetwork } from '@app/shared/common.utils';
+import { seoDescriptionNetwork, parseMinerName } from '@app/shared/common.utils';
import { PriceService, Price } from '@app/services/price.service';
import { CacheService } from '@app/services/cache.service';
import { ServicesApiServices } from '@app/services/services-api.service';
@@ -144,6 +144,7 @@ export class BlockComponent implements OnInit, OnDestroy {
for (const block of blocks) {
if (block.id === this.blockHash) {
this.block = block;
+ this.block.minerName = parseMinerName(block);
if (block.extras) {
block.extras.minFee = this.getMinBlockFee(block);
block.extras.maxFee = this.getMaxBlockFee(block);
@@ -250,6 +251,7 @@ export class BlockComponent implements OnInit, OnDestroy {
}
this.updateAuditAvailableFromBlockHeight(block.height);
this.block = block;
+ this.block.minerName = parseMinerName(block);
if (block.extras) {
block.extras.minFee = this.getMinBlockFee(block);
block.extras.maxFee = this.getMaxBlockFee(block);
diff --git a/frontend/src/app/components/transaction/transaction-details/transaction-details.component.html b/frontend/src/app/components/transaction/transaction-details/transaction-details.component.html
index 819e27d89..fa7fc1fbc 100644
--- a/frontend/src/app/components/transaction/transaction-details/transaction-details.component.html
+++ b/frontend/src/app/components/transaction/transaction-details/transaction-details.component.html
@@ -291,11 +291,11 @@
@if (pool) {
|
- 1 && pool.minerNames[1] != ''">
- @if (pool.minerNames[1].length > 16) {
- {{ pool.minerNames[1].slice(0, 15) }}…
+
+ @if (pool.minerName.length > 16) {
+ {{ pool.minerName.slice(0, 15) }}…
} @else {
- {{ pool.minerNames[1] }}
+ {{ pool.minerName }}
}
diff --git a/frontend/src/app/components/transaction/transaction.component.ts b/frontend/src/app/components/transaction/transaction.component.ts
index 423c86530..c18670074 100644
--- a/frontend/src/app/components/transaction/transaction.component.ts
+++ b/frontend/src/app/components/transaction/transaction.component.ts
@@ -24,7 +24,7 @@ import { AudioService } from '@app/services/audio.service';
import { ApiService } from '@app/services/api.service';
import { SeoService } from '@app/services/seo.service';
import { StorageService } from '@app/services/storage.service';
-import { seoDescriptionNetwork } from '@app/shared/common.utils';
+import { parseMinerName, seoDescriptionNetwork } from '@app/shared/common.utils';
import { getTransactionFlags, getUnacceleratedFeeRate } from '@app/shared/transaction.utils';
import { Filter, TransactionFlags, toFilters } from '@app/shared/filters.utils';
import { BlockExtended, CpfpInfo, RbfTree, MempoolPosition, DifficultyAdjustment, Acceleration, AccelerationPosition } from '@interfaces/node-api.interface';
@@ -43,6 +43,7 @@ export interface Pool {
name: string;
slug: string;
minerNames: string[] | null;
+ minerName?: string;
}
export interface TxAuditStatus {
@@ -384,8 +385,8 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
}),
switchMap(({ hash, height }) => {
const foundBlock = this.cacheService.getCachedBlock(height) || null;
- return foundBlock ? of(foundBlock.extras.pool) : this.apiService.getBlock$(hash).pipe(
- map(block => block.extras.pool),
+ return foundBlock ? of({ ...foundBlock.extras.pool, minerName: parseMinerName(foundBlock) }) : this.apiService.getBlock$(hash).pipe(
+ map(block => ({ ...block.extras.pool, minerName: parseMinerName(block) })),
retry({ count: 3, delay: 2000 }),
catchError(() => of(null))
);
@@ -719,6 +720,7 @@ export class TransactionComponent implements OnInit, AfterViewInit, OnDestroy {
block_time: block.timestamp,
};
this.pool = block.extras.pool;
+ this.pool.minerName = parseMinerName(block);
this.txChanged$.next(true);
this.stateService.markBlock$.next({ blockHeight: block.height });
if (this.tx.acceleration || (this.accelerationInfo && ['accelerating', 'completed_provisional', 'completed'].includes(this.accelerationInfo.status))) {
diff --git a/frontend/src/app/interfaces/node-api.interface.ts b/frontend/src/app/interfaces/node-api.interface.ts
index 29a021dfa..2dcf600ac 100644
--- a/frontend/src/app/interfaces/node-api.interface.ts
+++ b/frontend/src/app/interfaces/node-api.interface.ts
@@ -213,6 +213,7 @@ export interface BlockExtension {
export interface BlockExtended extends Block {
extras?: BlockExtension;
+ minerName?: string;
}
export interface BlockAudit extends BlockExtended {
diff --git a/frontend/src/app/shared/common.utils.ts b/frontend/src/app/shared/common.utils.ts
index 9b53600c1..0aa1d932f 100644
--- a/frontend/src/app/shared/common.utils.ts
+++ b/frontend/src/app/shared/common.utils.ts
@@ -1,5 +1,5 @@
import { MempoolBlockDelta, MempoolBlockDeltaCompressed, MempoolDeltaChange, TransactionCompressed } from "../interfaces/websocket.interface";
-import { TransactionStripped } from "@interfaces/node-api.interface";
+import { BlockExtended, TransactionStripped } from "@interfaces/node-api.interface";
import { AmountShortenerPipe } from "@app/shared/pipes/amount-shortener.pipe";
import { Router, ActivatedRoute } from '@angular/router';
const amountShortenerPipe = new AmountShortenerPipe();
@@ -280,3 +280,69 @@ export function md5(inputString): string {
}
return rh(a)+rh(b)+rh(c)+rh(d);
}
+
+export function parseMinerName(block: BlockExtended): string | null {
+ if (block.extras?.pool?.minerNames?.[1]) {
+ return block.extras.pool.minerNames[1];
+ }
+
+ if (!block.extras?.coinbaseRaw) {
+ return null;
+ }
+
+ const tag = extractMinerTag(block.extras.coinbaseRaw);
+ return tag ? parseMinerTag(tag) : null;
+}
+
+function extractMinerTag(hex: string): string | null {
+ const bytes = hexToBytes(hex);
+ let pos = 0;
+
+ // Skip block height
+ const heightPushSize = bytes[pos];
+ pos += heightPushSize + 1;
+ if (pos >= bytes.length) {
+ return null;
+ }
+
+ // Get miner tag bytes
+ const tagPushSize = bytes[pos];
+ pos++;
+
+ // Extract and decode tag bytes
+ const tagBytes = bytes.slice(pos, pos + tagPushSize);
+ return byteToUtf8(tagBytes);
+}
+
+function hexToBytes(hex: string): number[] {
+ const bytes: number[] = [];
+ for (let i = 0; i < hex.length; i += 2) {
+ bytes.push(parseInt(hex.slice(i, i + 2), 16));
+ }
+ return bytes;
+}
+
+function byteToUtf8(bytes: number[]): string {
+ try {
+ return new TextDecoder('utf-8', { fatal: true }).decode(new Uint8Array(bytes));
+ } catch {
+ return bytes
+ .filter(byte => byte >= 32 && byte <= 126)
+ .map(byte => String.fromCharCode(byte))
+ .join('');
+ }
+}
+
+/**
+ * Parses a miner tag string in the format "//(/)?"
+ * Returns the hasher tag if present, removing any "Mined by " prefix
+ */
+function parseMinerTag(tag: string): string | null {
+ const match = tag.match(/\/[^/]+\/([^/]+\/)/);
+ if (!match) {
+ return null;
+ }
+
+ const hasherTag = match[1].slice(0, -1).replace(/^Mined by /, '');
+ return hasherTag;
+}
\ No newline at end of file
|