update recommended selection algo
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user