Track miner reward contribution
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user