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 - - @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) { - - @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