@@ -285,6 +285,40 @@ async function etherscanClaimReceiver(
285285 return null ;
286286}
287287
288+ // A staker's currently-claimable MOR on a pool (still in the Morpheus contract,
289+ // pre-claim). Gnars' 25% of this is revenue ACCRUING to the treasury — it shows
290+ // up here so the treasury figure ticks up like the (also-unrealized) vault fee,
291+ // instead of sitting at 0 until the first claim→split→distribute. Etherscan
292+ // proxy eth_call (datacenter-safe); returns wei, 0 on any failure.
293+ const LATEST_REWARD_SIG = keccak256 ( toHex ( "getLatestUserReward(uint256,address)" ) ) . slice ( 0 , 10 ) ;
294+ async function etherscanLatestReward ( pool : Address , user : Address , key : string ) : Promise < bigint > {
295+ const data = `${ LATEST_REWARD_SIG } ${ "0" . repeat ( 64 ) } ${ pad32 ( user ) . slice ( 2 ) } ` ;
296+ const url =
297+ `https://api.etherscan.io/v2/api?chainid=1&module=proxy&action=eth_call` +
298+ `&to=${ pool } &data=${ data } &tag=latest&apikey=${ key } ` ;
299+ for ( let i = 0 ; i < 4 ; i ++ ) {
300+ try {
301+ const res = await fetch ( url , { cache : "no-store" } ) ;
302+ const j = ( await res . json ( ) ) as { result ?: unknown ; message ?: string } ;
303+ if ( typeof j . result === "string" && / ^ 0 x [ 0 - 9 a - f A - F ] + $ / . test ( j . result ) ) {
304+ try {
305+ return BigInt ( j . result ) ;
306+ } catch {
307+ return BigInt ( 0 ) ;
308+ }
309+ }
310+ if ( / r a t e l i m i t / i. test ( String ( j . result ) ) || / r a t e l i m i t / i. test ( String ( j . message ) ) ) {
311+ await sleep ( 600 ) ;
312+ continue ;
313+ }
314+ return BigInt ( 0 ) ;
315+ } catch {
316+ await sleep ( 300 ) ;
317+ }
318+ }
319+ return BigInt ( 0 ) ;
320+ }
321+
288322async function morBackersByRider ( ethUsd : UsdPrice ) : Promise < Record < string , OrbitBacker [ ] > > {
289323 const walletToId = new Map < string , RiderId > ( ) ;
290324 for ( const r of RIDER_LIST ) if ( r . wallet ) walletToId . set ( r . wallet . toLowerCase ( ) , r . id ) ;
@@ -426,9 +460,14 @@ async function morBackersByRider(ethUsd: UsdPrice): Promise<Record<string, Orbit
426460}
427461
428462/**
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.
463+ * MOR earned/accruing for the Gnars treasury, in three tiers (Gnars = 25% of the
464+ * staker's rewards throughout):
465+ * 1. directRaw — already distributed to the Gnars Arbitrum multisig.
466+ * 2. in-split — claimed to a staker's split, awaiting distribution (Arbitrum).
467+ * 3. accruing — still unclaimed in the Morpheus pools (mainnet, pending).
468+ * Tier 3 keeps the figure alive: it ticks up as MOR accrues, mirroring the
469+ * (also-unrealized) vault fee, instead of reading 0 until the first claim.
470+ * Best-effort — priced in USD via CoinGecko.
432471 */
433472async function gnarsMorEarned (
434473 morByRider : Record < string , OrbitBacker [ ] > ,
@@ -454,14 +493,22 @@ async function readGnarsMor(
454493
455494 // Unique (staker, athlete) pairs → the per-staker splits holding MOR.
456495 const pairs = new Map < string , [ Address , Address ] > ( ) ;
496+ // (pool, staker) targets for the still-unclaimed MOR accruing in Morpheus.
497+ const pendTargets : Array < [ Address , Address ] > = [ ] ;
457498 for ( const [ id , backers ] of Object . entries ( morByRider ) ) {
458499 const ref = walletById . get ( id ) ;
459500 if ( ! ref ) continue ;
460- for ( const b of backers ) pairs . set ( `${ b . address } -${ ref } ` . toLowerCase ( ) , [ b . address , ref ] ) ;
501+ for ( const b of backers ) {
502+ pairs . set ( `${ b . address } -${ ref } ` . toLowerCase ( ) , [ b . address , ref ] ) ;
503+ if ( b . kind === "mor" && b . asset && ETHERSCAN_KEY ) {
504+ const pool = b . asset === "steth" ? MORPHEUS_POOLS . stEth . pool : MORPHEUS_POOLS . usdc . pool ;
505+ pendTargets . push ( [ pool , b . address ] ) ;
506+ }
507+ }
461508 }
462509
463510 try {
464- const [ splitBals , directRaw , morUsd ] = await Promise . all ( [
511+ const [ splitBals , directRaw , morUsd , pendRaw ] = await Promise . all ( [
465512 Promise . all ( [ ...pairs . values ( ) ] . map ( ( [ s , a ] ) => splitMorBalance ( s , a ) . catch ( ( ) => 0 ) ) ) ,
466513 arbitrumClient
467514 . readContract ( {
@@ -473,9 +520,16 @@ async function readGnarsMor(
473520 . then ( ( b ) => Number ( formatUnits ( b , MOR_DECIMALS ) ) )
474521 . catch ( ( ) => 0 ) ,
475522 getTokenPriceUsd ( MOR_TOKEN , "arbitrum-one" ) ,
523+ Promise . all (
524+ pendTargets . map ( ( [ p , u ] ) =>
525+ etherscanLatestReward ( p , u , ETHERSCAN_KEY as string ) . catch ( ( ) => BigInt ( 0 ) ) ,
526+ ) ,
527+ ) ,
476528 ] ) ;
477- const pendingGnars = splitBals . reduce ( ( s , v ) => s + v , 0 ) * 0.25 ; // Gnars = 25% of each split
478- const mor = pendingGnars + directRaw ;
529+ const inSplitGnars = splitBals . reduce ( ( s , v ) => s + v , 0 ) * 0.25 ; // claimed, awaiting distribute
530+ const accruingGnars =
531+ pendRaw . reduce ( ( s , v ) => s + Number ( formatUnits ( v , MOR_DECIMALS ) ) , 0 ) * 0.25 ; // still in Morpheus
532+ const mor = inSplitGnars + directRaw + accruingGnars ;
479533 return { mor, morUsd } ;
480534 } catch {
481535 return { mor : 0 , morUsd : null as UsdPrice } ;
0 commit comments