Track miner reward contribution

This commit is contained in:
Dorian
2026-05-09 16:59:06 +01:00
parent 641a086d62
commit c256726cfa
4 changed files with 112 additions and 6 deletions

View File

@@ -2,7 +2,7 @@ import { config } from "../config.js";
import { logger } from "../logger.js";
import { digestFetch } from "./digest.js";
import { parseClientsHtml, parseThreadsHtml } from "./parse.js";
import type { CurrentJob, DatumSnapshot, NetworkStat, PoolStat } from "./types.js";
import type { CurrentJob, DatumSnapshot, MinerStat, NetworkStat, PoolStat } from "./types.js";
const DEFAULT_TIMEOUT_MS = 10_000;
@@ -54,6 +54,19 @@ let lastSnapshot: DatumSnapshot = {
let timer: NodeJS.Timeout | null = null;
let pollInFlight = false;
type ContributionEntry = {
miner: MinerStat;
lastRawAcceptedWork: number;
lastRawAcceptedShares: number;
totalAcceptedWork: number;
totalAcceptedShares: number;
firstSeenAt: number;
lastSeenAt: number;
currentlyConnected: boolean;
};
const contributionLedger = new Map<string, ContributionEntry>();
function formatErr(err: unknown): string {
if (!(err instanceof Error)) return String(err);
const cause = (err as Error & { cause?: unknown }).cause;
@@ -98,6 +111,77 @@ type MempoolHashrate = {
currentDifficulty?: number;
};
function minerIdentity(m: MinerStat): string {
return m.nickname || m.authUsername || m.remoteHost;
}
function applyContributionLedger(miners: MinerStat[], fetchedAt: number): MinerStat[] {
for (const entry of contributionLedger.values()) {
entry.currentlyConnected = false;
}
const connectedKeys = new Set<string>();
for (const miner of miners) {
const key = minerIdentity(miner);
connectedKeys.add(key);
const existing = contributionLedger.get(key);
if (!existing) {
contributionLedger.set(key, {
miner,
lastRawAcceptedWork: miner.diffAcceptedSum,
lastRawAcceptedShares: miner.diffAcceptedCount,
totalAcceptedWork: miner.diffAcceptedSum,
totalAcceptedShares: miner.diffAcceptedCount,
firstSeenAt: fetchedAt,
lastSeenAt: fetchedAt,
currentlyConnected: true,
});
continue;
}
const workDelta = miner.diffAcceptedSum - existing.lastRawAcceptedWork;
const shareDelta = miner.diffAcceptedCount - existing.lastRawAcceptedShares;
existing.totalAcceptedWork += workDelta >= 0 ? workDelta : miner.diffAcceptedSum;
existing.totalAcceptedShares += shareDelta >= 0 ? shareDelta : miner.diffAcceptedCount;
existing.lastRawAcceptedWork = miner.diffAcceptedSum;
existing.lastRawAcceptedShares = miner.diffAcceptedCount;
existing.lastSeenAt = fetchedAt;
existing.currentlyConnected = true;
existing.miner = miner;
}
const totalWork = [...contributionLedger.values()].reduce((sum, entry) => sum + entry.totalAcceptedWork, 0);
const decorate = (miner: MinerStat, entry: ContributionEntry): MinerStat => ({
...miner,
contributionAcceptedWork: entry.totalAcceptedWork,
contributionAcceptedShares: entry.totalAcceptedShares,
contributionPct: totalWork > 0 ? (entry.totalAcceptedWork / totalWork) * 100 : 0,
contributionFirstSeenAt: entry.firstSeenAt,
contributionLastSeenAt: entry.lastSeenAt,
currentlyConnected: entry.currentlyConnected,
});
const live = miners.map((miner) => decorate(miner, contributionLedger.get(minerIdentity(miner))!));
const offline = [...contributionLedger.entries()]
.filter(([key]) => !connectedKeys.has(key))
.map(([, entry]) =>
decorate(
{
...entry.miner,
hashrateThs: 0,
hashrateAgeS: null,
subscribed: false,
status: "idle",
},
entry,
),
);
return [...live, ...offline].sort((a, b) => (b.contributionAcceptedWork ?? 0) - (a.contributionAcceptedWork ?? 0));
}
async function fetchNetworkStats(): Promise<NetworkStat> {
const timeout = abortableSignal(DEFAULT_TIMEOUT_MS);
try {
@@ -216,7 +300,7 @@ async function pollOnce(): Promise<DatumSnapshot> {
);
}
const miners = parseClientsHtml(clientsHtml);
const miners = applyContributionLedger(parseClientsHtml(clientsHtml), fetchedAt);
const threads = threadsHtml ? parseThreadsHtml(threadsHtml) : [];
if (miners.length === 0) {
logger.warn(

View File

@@ -29,6 +29,12 @@ export type MinerStat = {
userAgent: string;
subscribed: boolean;
status: 'hashing' | 'stale' | 'idle';
contributionAcceptedWork?: number;
contributionAcceptedShares?: number;
contributionPct?: number;
contributionFirstSeenAt?: number;
contributionLastSeenAt?: number;
currentlyConnected?: boolean;
};
export type PoolStat = {

View File

@@ -46,6 +46,12 @@ const lastShareLabel = computed(() => {
const lifetimeShares = computed(() => props.miner.diffAcceptedCount);
const acceptedWork = computed(() => isBoomerHeater.value ? "Zero. Obviously." : formatBig(props.miner.diffAcceptedSum));
const contributionWork = computed(() =>
isBoomerHeater.value ? "Zero. Obviously." : formatBig(props.miner.contributionAcceptedWork ?? props.miner.diffAcceptedSum),
);
const contributionShare = computed(() =>
isBoomerHeater.value ? "0.00%" : `${(props.miner.contributionPct ?? 0).toFixed(2)}%`,
);
const bestWork = computed(() => {
if (isBoomerHeater.value) return "Fuck all";
return props.bestWork && props.bestWork > 0 ? formatBig(props.bestWork) : "collecting";
@@ -102,8 +108,12 @@ function formatBig(n: number): string {
<div class="value glow-amber">{{ bestWork }}</div>
</div>
<div class="stat">
<div class="label">accepted work</div>
<div class="value">{{ acceptedWork }}</div>
<div class="label">reward share</div>
<div class="value glow-green">{{ contributionShare }}</div>
</div>
<div class="stat">
<div class="label">reward work</div>
<div class="value">{{ contributionWork }}</div>
</div>
<div class="stat">
<div class="label">reject %</div>
@@ -112,8 +122,8 @@ function formatBig(n: number): string {
</div>
</div>
<div class="stat">
<div class="label">vdiff</div>
<div class="value">{{ isBoomerHeater ? "Fiat" : miner.vdiff.toLocaleString() }}</div>
<div class="label">accepted work</div>
<div class="value">{{ acceptedWork }}</div>
</div>
</div>

View File

@@ -18,6 +18,12 @@ export type MinerStat = {
userAgent: string;
subscribed: boolean;
status: "hashing" | "stale" | "idle";
contributionAcceptedWork?: number;
contributionAcceptedShares?: number;
contributionPct?: number;
contributionFirstSeenAt?: number;
contributionLastSeenAt?: number;
currentlyConnected?: boolean;
};
export type PoolStat = {