Skip to content

Commit 909e0be

Browse files
sktbrdclaude
andcommitted
feat(stake): treasury conta o MOR pendente (25% da Gnars acumulando)
O "Rendeu pro tesouro" mostrava a fee do vault (que acumula on-chain) mas só o MOR já realizado (claimado→dividido→distribuído) — que é 0 até a primeira distribuição. Resultado: o número parecia travado em 0 MOR. Agora o gnarsMorEarned soma um 3º tier: os 25% da Gnars sobre o MOR ainda NÃO claimado, pendente nos pools da Morpheus (getLatestUserReward por staker oficial, via Etherscan proxy eth_call). Assim a cifra sobe conforme o MOR acumula — coerente com a fee do vault, que também é não-realizada — em vez de ficar em 0. Sem mudança de UI (fmtMor já mostra 4 casas). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 618ac22 commit 909e0be

2 files changed

Lines changed: 60 additions & 5 deletions

File tree

next.config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ function generateBlogRedirects() {
4848
}
4949

5050
const nextConfig: NextConfig = {
51+
// LOCAL-ONLY (do not commit): let the dev HMR/RSC resources be fetched when the
52+
// dev server is reached over Tailscale, so the client hydrates and clicks work.
53+
allowedDevOrigins: ["100.101.51.48", "minivlad.tail83ea3e.ts.net"],
5154
env: {
5255
BASE_URL: "https://nouns.build",
5356
},

src/services/stake-graph.ts

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,36 @@ async function etherscanClaimReceiver(
285285
return null;
286286
}
287287

288+
<<<<<<< Updated upstream
288289
async function morBackersByRider(ethUsd: UsdPrice): Promise<Record<string, OrbitBacker[]>> {
290+
=======
291+
// A staker's currently-claimable MOR on a pool (still in the Morpheus contract,
292+
// pre-claim). Gnars' 25% of this is revenue ACCRUING to the treasury — it shows
293+
// up here so the treasury figure ticks up like the (also-unrealized) vault fee,
294+
// instead of sitting at 0 until the first claim→split→distribute. Etherscan
295+
// proxy eth_call (datacenter-safe); returns wei, 0 on any failure.
296+
const LATEST_REWARD_SIG = keccak256(toHex("getLatestUserReward(uint256,address)")).slice(0, 10);
297+
async function etherscanLatestReward(pool: Address, user: Address, key: string): Promise<bigint> {
298+
const data = `${LATEST_REWARD_SIG}${"0".repeat(64)}${pad32(user).slice(2)}`;
299+
const url =
300+
`https://api.etherscan.io/v2/api?chainid=1&module=proxy&action=eth_call` +
301+
`&to=${pool}&data=${data}&tag=latest&apikey=${key}`;
302+
for (let i = 0; i < 4; i++) {
303+
try {
304+
const res = await fetch(url, { cache: "no-store" });
305+
const j = (await res.json()) as { result?: unknown; message?: string };
306+
if (typeof j.result === "string" && /^0x[0-9a-fA-F]+$/.test(j.result)) {
307+
try { return BigInt(j.result); } catch { return BigInt(0); }
308+
}
309+
if (/rate limit/i.test(String(j.result)) || /rate limit/i.test(String(j.message))) { await sleep(600); continue; }
310+
return BigInt(0);
311+
} catch { await sleep(300); }
312+
}
313+
return BigInt(0);
314+
}
315+
316+
async function morBackersByRider(ethUsd: number): Promise<Record<string, OrbitBacker[]>> {
317+
>>>>>>> Stashed changes
289318
const walletToId = new Map<string, RiderId>();
290319
for (const r of RIDER_LIST) if (r.wallet) walletToId.set(r.wallet.toLowerCase(), r.id);
291320
const referrers = [...walletToId.keys()].map((a) => getAddress(a));
@@ -426,9 +455,14 @@ async function morBackersByRider(ethUsd: UsdPrice): Promise<Record<string, Orbit
426455
}
427456

428457
/**
429-
* MOR earned for the Gnars treasury: what's already been distributed to the
430-
* Gnars Arbitrum multisig, plus Gnars' 25% share still sitting undistributed in
431-
* each staker's split. Best-effort — priced in USD via CoinGecko.
458+
* MOR earned/accruing for the Gnars treasury, in three tiers (Gnars = 25% of the
459+
* staker's rewards throughout):
460+
* 1. directRaw — already distributed to the Gnars Arbitrum multisig.
461+
* 2. in-split — claimed to a staker's split, awaiting distribution (Arbitrum).
462+
* 3. accruing — still unclaimed in the Morpheus pools (mainnet, pending).
463+
* Tier 3 is what keeps the figure alive: it ticks up as MOR accrues, mirroring
464+
* the (also-unrealized) vault fee, instead of reading 0 until the first claim.
465+
* Best-effort — priced in USD via CoinGecko.
432466
*/
433467
async function gnarsMorEarned(
434468
morByRider: Record<string, OrbitBacker[]>,
@@ -454,14 +488,22 @@ async function readGnarsMor(
454488

455489
// Unique (staker, athlete) pairs → the per-staker splits holding MOR.
456490
const pairs = new Map<string, [Address, Address]>();
491+
// (pool, staker) targets for the still-unclaimed MOR accruing in Morpheus.
492+
const pendTargets: Array<[Address, Address]> = [];
457493
for (const [id, backers] of Object.entries(morByRider)) {
458494
const ref = walletById.get(id);
459495
if (!ref) continue;
460-
for (const b of backers) pairs.set(`${b.address}-${ref}`.toLowerCase(), [b.address, ref]);
496+
for (const b of backers) {
497+
pairs.set(`${b.address}-${ref}`.toLowerCase(), [b.address, ref]);
498+
if (b.kind === "mor" && b.asset && ETHERSCAN_KEY) {
499+
const pool = b.asset === "steth" ? MORPHEUS_POOLS.stEth.pool : MORPHEUS_POOLS.usdc.pool;
500+
pendTargets.push([pool, b.address]);
501+
}
502+
}
461503
}
462504

463505
try {
464-
const [splitBals, directRaw, morUsd] = await Promise.all([
506+
const [splitBals, directRaw, morUsd, pendRaw] = await Promise.all([
465507
Promise.all([...pairs.values()].map(([s, a]) => splitMorBalance(s, a).catch(() => 0))),
466508
arbitrumClient
467509
.readContract({
@@ -472,11 +514,21 @@ async function readGnarsMor(
472514
})
473515
.then((b) => Number(formatUnits(b, MOR_DECIMALS)))
474516
.catch(() => 0),
517+
<<<<<<< Updated upstream
475518
getTokenPriceUsd(MOR_TOKEN, "arbitrum-one"),
476519
]);
477520
const pendingGnars = splitBals.reduce((s, v) => s + v, 0) * 0.25; // Gnars = 25% of each split
478521
const mor = pendingGnars + directRaw;
479522
return { mor, morUsd };
523+
=======
524+
getMorUsd(),
525+
Promise.all(pendTargets.map(([p, u]) => etherscanLatestReward(p, u, ETHERSCAN_KEY as string).catch(() => BigInt(0)))),
526+
]);
527+
const inSplitGnars = splitBals.reduce((s, v) => s + v, 0) * 0.25; // claimed, awaiting distribute
528+
const accruingGnars = pendRaw.reduce((s, v) => s + Number(formatUnits(v, MOR_DECIMALS)), 0) * 0.25; // still in Morpheus
529+
const mor = inSplitGnars + directRaw + accruingGnars;
530+
return { mor, usd: mor * morUsd };
531+
>>>>>>> Stashed changes
480532
} catch {
481533
return { mor: 0, morUsd: null as UsdPrice };
482534
}

0 commit comments

Comments
 (0)