parse & display hasher

This commit is contained in:
Mononaut 2025-06-18 04:04:58 +00:00
parent 721a647564
commit 5a08039867
No known key found for this signature in database
GPG key ID: A3F058E41374C04E
7 changed files with 87 additions and 15 deletions

View file

@ -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:`);

View file

@ -182,11 +182,11 @@
<td i18n="block.miner">Miner</td>
<td *ngIf="stateService.env.MINING_DASHBOARD">
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, block.extras.pool.slug]" class="badge" style="color: #FFF;padding:0;">
<span class="miner-name" *ngIf="block.extras.pool.minerNames?.length > 1 && block.extras.pool.minerNames[1] != ''">
@if (block.extras.pool.minerNames[1].length > 16) {
{{ block.extras.pool.minerNames[1].slice(0, 15) }}…
<span class="miner-name" *ngIf="block.minerName">
@if (block.minerName.length > 16) {
{{ block.minerName.slice(0, 15) }}…
} @else {
{{ block.extras.pool.minerNames[1] }}
{{ block.minerName }}
}
</span>
<img class="pool-logo" [src]="'/resources/mining-pools/' + block.extras.pool.slug + '.svg'" onError="this.src = '/resources/mining-pools/default.svg'" [alt]="'Logo of ' + block.extras.pool.name + ' mining pool'">

View file

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

View file

@ -291,11 +291,11 @@
@if (pool) {
<td class="wrap-cell">
<a placement="bottom" [routerLink]="['/mining/pool' | relativeUrl, pool.slug]" class="badge" style="color: #FFF;padding:0;">
<span class="miner-name" *ngIf="pool.minerNames?.length > 1 && pool.minerNames[1] != ''">
@if (pool.minerNames[1].length > 16) {
{{ pool.minerNames[1].slice(0, 15) }}…
<span class="miner-name" *ngIf="pool.minerName">
@if (pool.minerName.length > 16) {
{{ pool.minerName.slice(0, 15) }}…
} @else {
{{ pool.minerNames[1] }}
{{ pool.minerName }}
}
</span>
<img class="pool-logo" [src]="'/resources/mining-pools/' + pool.slug + '.svg'" onError="this.src = '/resources/mining-pools/default.svg'" [alt]="'Logo of ' + pool.name + ' mining pool'">

View file

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

View file

@ -213,6 +213,7 @@ export interface BlockExtension {
export interface BlockExtended extends Block {
extras?: BlockExtension;
minerName?: string;
}
export interface BlockAudit extends BlockExtended {

View file

@ -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 "/<miner-tag>/(<hasher-tag>/)?"
* 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;
}