experimental stratum job visualization
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
<div class="container-xl" style="min-height: 335px">
|
||||
<h1 class="float-left" i18n="master-page.blocks">Stratum Jobs</h1>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div style="min-height: 295px">
|
||||
<table *ngIf="poolsReady && (rows$ | async) as rows;" class="stratum-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<td class="height">Height</td>
|
||||
<td class="reward">Reward</td>
|
||||
<td class="merkle" [attr.colspan]="rows[0]?.merkleCells?.length || 4">
|
||||
Merkle Branches
|
||||
</td>
|
||||
<td class="pool">Pool</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (row of rows; track row.job.pool) {
|
||||
<tr>
|
||||
<td class="height">
|
||||
{{ row.job.height }}
|
||||
</td>
|
||||
<td class="reward">
|
||||
<app-amount [satoshis]="row.job.reward"></app-amount>
|
||||
</td>
|
||||
@for (cell of row.merkleCells; track $index) {
|
||||
<td class="merkle" [style.background-color]="cell.hash ? '#' + cell.hash.slice(0, 6) : ''">
|
||||
<div class="pipe-segment" [class]="pipeToClass(cell.type)"></div>
|
||||
</td>
|
||||
}
|
||||
<td class="pool">
|
||||
@if (pools[row.job.pool]) {
|
||||
<a class="badge" [routerLink]="[('/mining/pool/' + pools[row.job.pool].slug) | relativeUrl]">
|
||||
<img class="pool-logo" [src]="'/resources/mining-pools/' + pools[row.job.pool].slug + '.svg'" onError="this.src = '/resources/mining-pools/default.svg'" [alt]="'Logo of ' + pools[row.job.pool].name + ' mining pool'">
|
||||
{{ pools[row.job.pool].name}}
|
||||
</a>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,101 @@
|
||||
.stratum-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
td {
|
||||
position: relative;
|
||||
height: 2em;
|
||||
|
||||
&.height, &.reward {
|
||||
padding: 0 5px;
|
||||
}
|
||||
|
||||
&.pool {
|
||||
padding-left: 5px;
|
||||
padding-right: 20px;
|
||||
}
|
||||
|
||||
&.merkle {
|
||||
width: 100px;
|
||||
.pipe-segment {
|
||||
position: absolute;
|
||||
border-color: white;
|
||||
box-sizing: content-box;
|
||||
|
||||
&.vertical {
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
border-left: solid 4px;
|
||||
}
|
||||
&.horizontal {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 50%;
|
||||
border-top: solid 4px;
|
||||
}
|
||||
&.branch-top {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 100%;
|
||||
height: 50%;
|
||||
border-top: solid 4px;
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
box-sizing: content-box;
|
||||
top: -4px;
|
||||
right: 0px;
|
||||
bottom: 0;
|
||||
width: 50%;
|
||||
border-top: solid 4px;
|
||||
border-left: solid 4px;
|
||||
border-top-left-radius: 5px;
|
||||
}
|
||||
}
|
||||
&.branch-mid {
|
||||
bottom: 0;
|
||||
right: 0px;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
border-left: solid 4px;
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
box-sizing: content-box;
|
||||
top: -4px;
|
||||
left: -4px;
|
||||
width: 100%;
|
||||
height: 50%;
|
||||
border-bottom: solid 4px;
|
||||
border-left: solid 4px;
|
||||
border-bottom-left-radius: 5px;
|
||||
}
|
||||
}
|
||||
&.branch-end {
|
||||
top: -4px;
|
||||
right: 0;
|
||||
width: 50%;
|
||||
height: 50%;
|
||||
border-bottom-left-radius: 5px;
|
||||
border-bottom: solid 4px;
|
||||
border-left: solid 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: relative;
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
.pool-logo {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
position: relative;
|
||||
top: -1px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
import { Component, OnInit, ChangeDetectionStrategy, OnDestroy, ChangeDetectorRef } from '@angular/core';
|
||||
import { StateService } from '../../../services/state.service';
|
||||
import { WebsocketService } from '../../../services/websocket.service';
|
||||
import { map, Observable } from 'rxjs';
|
||||
import { StratumJob } from '../../../interfaces/websocket.interface';
|
||||
import { MiningService } from '../../../services/mining.service';
|
||||
import { SinglePoolStats } from '../../../interfaces/node-api.interface';
|
||||
|
||||
type MerkleCellType = ' ' | '┬' | '├' | '└' | '│' | '─' | 'leaf';
|
||||
|
||||
interface MerkleCell {
|
||||
hash: string;
|
||||
type: MerkleCellType;
|
||||
job?: StratumJob;
|
||||
}
|
||||
|
||||
interface MerkleTree {
|
||||
hash?: string;
|
||||
job: string;
|
||||
size: number;
|
||||
children?: MerkleTree[];
|
||||
}
|
||||
|
||||
interface PoolRow {
|
||||
job: StratumJob;
|
||||
merkleCells: MerkleCell[];
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-stratum-list',
|
||||
templateUrl: './stratum-list.component.html',
|
||||
styleUrls: ['./stratum-list.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class StratumList implements OnInit, OnDestroy {
|
||||
rows$: Observable<PoolRow[]>;
|
||||
pools: { [id: number]: SinglePoolStats } = {};
|
||||
poolsReady: boolean = false;
|
||||
|
||||
constructor(
|
||||
private stateService: StateService,
|
||||
private websocketService: WebsocketService,
|
||||
private miningService: MiningService,
|
||||
private cd: ChangeDetectorRef,
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.miningService.getPools().subscribe(pools => {
|
||||
this.pools = {};
|
||||
for (const pool of pools) {
|
||||
this.pools[pool.unique_id] = pool;
|
||||
}
|
||||
this.poolsReady = true;
|
||||
this.cd.markForCheck();
|
||||
});
|
||||
this.rows$ = this.stateService.stratumJobs$.pipe(
|
||||
map((jobs) => this.processJobs(jobs)),
|
||||
);
|
||||
this.websocketService.startTrackStratum('all');
|
||||
}
|
||||
|
||||
processJobs(jobs: Record<string, StratumJob>): PoolRow[] {
|
||||
if (Object.keys(jobs).length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const numBranches = Math.max(...Object.values(jobs).map(job => job.merkleBranches.length));
|
||||
|
||||
let trees: MerkleTree[] = Object.keys(jobs).map(job => ({
|
||||
job,
|
||||
size: 1,
|
||||
}));
|
||||
|
||||
// build tree from bottom up
|
||||
for (let col = numBranches - 1; col >= 0; col--) {
|
||||
const groups: Record<string, MerkleTree[]> = {};
|
||||
for (const tree of trees) {
|
||||
const hash = jobs[tree.job].merkleBranches[col];
|
||||
if (!groups[hash]) {
|
||||
groups[hash] = [];
|
||||
}
|
||||
groups[hash].push(tree);
|
||||
}
|
||||
trees = Object.values(groups).map(group => ({
|
||||
hash: jobs[group[0].job].merkleBranches[col],
|
||||
job: group[0].job,
|
||||
children: group,
|
||||
size: group.reduce((acc, tree) => acc + tree.size, 0),
|
||||
}));
|
||||
}
|
||||
|
||||
// initialize grid of cells
|
||||
const rows: (MerkleCell | null)[][] = [];
|
||||
for (let i = 0; i < Object.keys(jobs).length; i++) {
|
||||
const row: (MerkleCell | null)[] = [];
|
||||
for (let j = 0; j <= numBranches; j++) {
|
||||
row.push(null);
|
||||
}
|
||||
rows.push(row);
|
||||
}
|
||||
|
||||
// fill in the cells
|
||||
let colTrees = [trees.sort((a, b) => {
|
||||
if (a.size !== b.size) {
|
||||
return b.size - a.size;
|
||||
}
|
||||
return a.job.localeCompare(b.job);
|
||||
})];
|
||||
for (let col = 0; col <= numBranches; col++) {
|
||||
let row = 0;
|
||||
const nextTrees: MerkleTree[][] = [];
|
||||
for (let g = 0; g < colTrees.length; g++) {
|
||||
for (let t = 0; t < colTrees[g].length; t++) {
|
||||
const tree = colTrees[g][t];
|
||||
const isFirstTree = (t === 0);
|
||||
const isLastTree = (t === colTrees[g].length - 1);
|
||||
for (let i = 0; i < tree.size; i++) {
|
||||
const isFirstCell = (i === 0);
|
||||
const isLeaf = (col === numBranches);
|
||||
rows[row][col] = {
|
||||
hash: tree.hash,
|
||||
job: isLeaf ? jobs[tree.job] : undefined,
|
||||
type: 'leaf',
|
||||
};
|
||||
if (col > 0) {
|
||||
rows[row][col - 1].type = getCellType(isFirstCell, isFirstTree, isLastTree);
|
||||
}
|
||||
row++;
|
||||
}
|
||||
if (tree.children) {
|
||||
nextTrees.push(tree.children.sort((a, b) => {
|
||||
if (a.size !== b.size) {
|
||||
return b.size - a.size;
|
||||
}
|
||||
return a.job.localeCompare(b.job);
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
colTrees = nextTrees;
|
||||
}
|
||||
return rows.map(row => ({
|
||||
job: row[row.length - 1].job,
|
||||
merkleCells: row.slice(0, -1),
|
||||
}));
|
||||
}
|
||||
|
||||
pipeToClass(type: MerkleCellType): string {
|
||||
return {
|
||||
' ': 'empty',
|
||||
'┬': 'branch-top',
|
||||
'├': 'branch-mid',
|
||||
'└': 'branch-end',
|
||||
'│': 'vertical',
|
||||
'─': 'horizontal',
|
||||
'leaf': 'leaf'
|
||||
}[type];
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.websocketService.stopTrackStratum();
|
||||
}
|
||||
}
|
||||
|
||||
function getCellType(isFirstCell, isFirstTree, isLastTree): MerkleCellType {
|
||||
if (isFirstCell) {
|
||||
if (isFirstTree) {
|
||||
if (isLastTree) {
|
||||
return '─';
|
||||
} else {
|
||||
return '┬';
|
||||
}
|
||||
} else if (isLastTree) {
|
||||
return '└';
|
||||
} else {
|
||||
return '├';
|
||||
}
|
||||
} else {
|
||||
if (isLastTree) {
|
||||
return ' ';
|
||||
} else {
|
||||
return '│';
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user