update recommended selection algo

This commit is contained in:
serinko
2025-09-05 22:51:59 +02:00
parent 7d1250b193
commit c4410bd187
+66 -32
View File
@@ -1,5 +1,12 @@
import { cache } from "react";
type DeclaredRole = {
entry?: boolean;
exit_ipr?: boolean;
exit_nr?: boolean;
mixnode?: boolean;
};
type ApiNode = {
node_id?: number;
total_stake?: number | string;
@@ -7,11 +14,10 @@ type ApiNode = {
description?: {
mixnet_websockets?: { ws_port?: number | null };
wireguard?: unknown | null;
declared_role?: {
entry?: boolean;
exit_ipr?: boolean;
exit_nr?: boolean;
};
declared_role?: DeclaredRole;
};
self_description?: {
declared_role?: DeclaredRole;
};
rewarding_details?: {
cost_params?: {
@@ -26,47 +32,75 @@ function toNumber(x: unknown, fallback = 0): number {
}
const MIN_STAKE = 50_000_000_000; // 50B
const MAX_PM = 0.2;
function hasRequiredRoles(n: ApiNode): boolean {
const r = n.self_description?.declared_role ?? n.description?.declared_role ?? {};
return !!(r.entry && r.exit_ipr && r.exit_nr);
}
function hasWs9000(n: ApiNode): boolean {
return n.description?.mixnet_websockets?.ws_port === 9000;
}
function hasGoodPM(n: ApiNode): boolean {
const pm = toNumber(n.rewarding_details?.cost_params?.profit_margin_percent, NaN);
return !Number.isNaN(pm) && pm <= MAX_PM;
}
function stakeOk(n: ApiNode): boolean {
return toNumber(n.total_stake, 0) > MIN_STAKE;
}
function wireguardOn(n: ApiNode): boolean {
return n.description?.wireguard != null;
}
function sortByUptimeDescStakeAsc(a: ApiNode, b: ApiNode): number {
const ua = toNumber(a.uptime, 0);
const ub = toNumber(b.uptime, 0);
if (ub !== ua) return ub - ua;
const sa = toNumber(a.total_stake, 0);
const sb = toNumber(b.total_stake, 0);
return sa - sb;
}
async function fetchRecommendedNodes(): Promise<number[]> {
const url = "https://api.nym.spectredao.net/api/v1/nodes?size=3000";
const res = await fetch(url, { cache: "no-store" });
if (!res.ok) throw new Error(`Failed to fetch nodes: ${res.status}`);
const data = (await res.json()) as unknown;
if (!Array.isArray(data)) {
const json = (await res.json()) as unknown;
if (!Array.isArray(json)) {
throw new Error("Unexpected API shape: expected an array");
}
const nodes = json as ApiNode[];
const nodes = data as ApiNode[];
const baseFilter = (n: ApiNode) =>
hasWs9000(n) && hasRequiredRoles(n) && hasGoodPM(n) && stakeOk(n);
const filtered = nodes.filter((n) => {
const ws9000 = n.description?.mixnet_websockets?.ws_port === 9000;
const wgOn = n.description?.wireguard != null;
const wgCandidates = nodes
.filter((n) => baseFilter(n) && wireguardOn(n))
.sort(sortByUptimeDescStakeAsc);
const pm = toNumber(n.rewarding_details?.cost_params?.profit_margin_percent, NaN);
const pmOk = !Number.isNaN(pm) && pm <= 0.2;
let picked = wgCandidates.slice(0, 10);
const r = n.description?.declared_role;
const rolesOk = !!(r?.entry && r?.exit_ipr && r?.exit_nr);
if (picked.length < 10) {
const relaxed = nodes
.filter((n) => baseFilter(n)) // WG relaxed
.sort(sortByUptimeDescStakeAsc);
const stakeOk = toNumber(n.total_stake, 0) > MIN_STAKE;
const set = new Set(picked.map((n) => n.node_id));
for (const n of relaxed) {
if (set.size >= 10) break;
if (!set.has(n.node_id)) {
picked.push(n);
set.add(n.node_id);
}
}
}
return ws9000 && wgOn && pmOk && rolesOk && stakeOk;
});
filtered.sort((a, b) => {
const ua = toNumber(a.uptime, 0);
const ub = toNumber(b.uptime, 0);
if (ub !== ua) return ub - ua; // uptime DESC
const sa = toNumber(a.total_stake, 0);
const sb = toNumber(b.total_stake, 0);
return sa - sb; // stake ASC
});
return filtered
.slice(0, 10)
return picked
.map((n) => (typeof n.node_id === "number" ? n.node_id : toNumber(n.node_id, 0)))
.filter((id) => Number.isFinite(id) && id > 0);
}