diff --git a/landing/src/components/LiveStats.vue b/landing/src/components/LiveStats.vue index 2fc2173..3d4b12e 100644 --- a/landing/src/components/LiveStats.vue +++ b/landing/src/components/LiveStats.vue @@ -4,7 +4,7 @@ Live -
+
ETH Reserve
{{ ethReserveDisplay }}
@@ -13,18 +13,13 @@ + Gathering data...
ETH / Token
{{ ethPerTokenDisplay }}
{{ ethPerTokenSecondary }}
-
-
Floor Price
-
{{ floorPriceDisplay }}
-
{{ floorPriceSecondary }}
-
{{ floorDistanceText }}
-
Supply (7d)
{{ totalSupply }}
@@ -32,6 +27,7 @@ + Gathering data...
Holders
@@ -40,6 +36,7 @@ + Gathering data...
Rebalances
@@ -76,10 +73,6 @@ interface Stats { burnedLastWeek: string; netSupplyChangeWeek: string; ethReserveGrowthBps: number | null; - feesEarned7dEth: string | null; - floorPriceWei: string | null; - floorDistanceBps: number | null; - currentPriceWei: string | null; ringBuffer: string[] | null; ringBufferPointer: number | null; } @@ -362,35 +355,6 @@ const totalSupply = computed(() => { return `${(supply / 1000).toFixed(1)}K KRK`; }); -// Floor price: only show when data is available -const showFloorPrice = computed(() => { - return !!(stats.value?.floorPriceWei && stats.value.floorPriceWei !== '0'); -}); - -const floorPriceAmount = computed(() => { - if (!showFloorPrice.value || !stats.value?.floorPriceWei) return null; - return weiToEth(stats.value.floorPriceWei); -}); - -const floorPriceDisplay = computed(() => { - if (floorPriceAmount.value == null) return '—'; - const eth = floorPriceAmount.value; - if (ethUsdPrice.value) return formatUsd(eth * ethUsdPrice.value); - return formatSmallEth(eth); -}); - -const floorPriceSecondary = computed((): string | null => { - if (floorPriceAmount.value == null || !ethUsdPrice.value) return null; - return `(${formatSmallEth(floorPriceAmount.value)})`; -}); - -const floorDistanceText = computed((): string | null => { - if (!stats.value || stats.value.floorDistanceBps == null) return null; - const distPct = Number(stats.value.floorDistanceBps) / 100; - const aboveBelow = distPct >= 0 ? 'above' : 'below'; - return `(${Math.abs(distPct).toFixed(0)}% ${aboveBelow})`; -}); - async function fetchStats() { try { const endpoint = `${window.location.origin}/api/graphql`; @@ -413,10 +377,6 @@ async function fetchStats() { burnedLastWeek netSupplyChangeWeek ethReserveGrowthBps - feesEarned7dEth - floorPriceWei - floorDistanceBps - currentPriceWei ringBuffer ringBufferPointer } @@ -434,47 +394,6 @@ async function fetchStats() { if (data.data?.statss?.items?.[0]) { const s = data.data.statss.items[0]; - // If ETH reserve is 0 from Ponder (EthScarcity/EthAbundance events never emitted), - // read WETH balance of the Uniswap V3 pool directly via RPC - if (s.lastEthReserve === '0' || !s.lastEthReserve) { - try { - const rpc = `${window.location.origin}/api/rpc`; - const deployResp = await fetch(`${window.location.origin}/app/deployments-local.json`); - if (deployResp.ok) { - const deployments = await deployResp.json(); - const krkAddr = deployments.contracts?.Kraiken; - if (krkAddr) { - const wethAddr = '0x4200000000000000000000000000000000000006'; - const factoryAddr = '0x4752ba5DBc23f44D87826276BF6Fd6b1C372aD24'; - const fee = 10000; // 1% fee tier - - // Step 1: factory.getPool(weth, kraiken, fee) → pool address - // selector: 0x1698ee82 - const wethPad = wethAddr.slice(2).padStart(64, '0'); - const krkPad = krkAddr.slice(2).padStart(64, '0'); - const feePad = fee.toString(16).padStart(64, '0'); - const poolRes = await fetch(rpc, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_call', - params: [{ to: factoryAddr, data: '0x1698ee82' + wethPad + krkPad + feePad }, 'latest'] }) }); - const poolJson = await poolRes.json(); - const poolAddr = '0x' + (poolJson.result || '').slice(26); - - if (poolAddr.length === 42 && poolAddr !== '0x' + '0'.repeat(40)) { - // Step 2: weth.balanceOf(pool) → ETH reserve in pool - const poolPad = poolAddr.slice(2).padStart(64, '0'); - const balRes = await fetch(rpc, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'eth_call', - params: [{ to: wethAddr, data: '0x70a08231' + poolPad }, 'latest'] }) }); - const balJson = await balRes.json(); - const wethBal = BigInt(balJson.result || '0x0'); - if (wethBal > 0n) { - s.lastEthReserve = wethBal.toString(); - } - } - } - } - } catch { /* ignore RPC fallback errors */ } - } // Detect changed fields for flash animation const prev = stats.value; if (prev) { @@ -487,7 +406,6 @@ async function fetchStats() { changed.add('supply'); changed.add('ethPerToken'); } - if (s.floorPriceWei !== prev.floorPriceWei) changed.add('floorPrice'); if (s.holderCount !== prev.holderCount) changed.add('holders'); if (s.recentersLastWeek !== prev.recentersLastWeek) changed.add('rebalances'); if (changed.size > 0) { @@ -539,9 +457,6 @@ onUnmounted(() => { margin: 0 auto padding: 0 32px - &.has-floor - max-width: 1020px - @media (min-width: 640px) grid-template-columns: repeat(2, 1fr) @@ -549,9 +464,6 @@ onUnmounted(() => { grid-template-columns: repeat(3, 1fr) gap: 32px - &.has-floor - grid-template-columns: repeat(3, 1fr) - .stat-item display: flex flex-direction: column @@ -608,10 +520,11 @@ onUnmounted(() => { color: rgba(240, 240, 240, 0.45) letter-spacing: 0.3px -.floor-distance +.spark-placeholder font-size: 11px - color: rgba(240, 240, 240, 0.5) + color: rgba(240, 240, 240, 0.3) letter-spacing: 0.3px + margin-top: 4px .pulse animation: pulse-glow 2s ease-in-out infinite diff --git a/services/ponder/src/helpers/stats.ts b/services/ponder/src/helpers/stats.ts index a641326..dd60132 100644 --- a/services/ponder/src/helpers/stats.ts +++ b/services/ponder/src/helpers/stats.ts @@ -341,6 +341,51 @@ export async function recordEthReserveSnapshot(context: StatsContext, timestamp: }); } +// WETH address is identical across Base mainnet, Base Sepolia, and local Anvil fork +const WETH_ADDRESS = (process.env.WETH_ADDRESS || '0x4200000000000000000000000000000000000006') as `0x${string}`; + +// Minimal ERC-20 ABI — only balanceOf is needed +const erc20BalanceOfAbi = [ + { + name: 'balanceOf', + type: 'function', + stateMutability: 'view', + inputs: [{ name: 'account', type: 'address' }], + outputs: [{ name: '', type: 'uint256' }], + }, +] as const; + +/** + * Read WETH balance of the Uniswap V3 pool via Ponder's cached client and + * persist it as `lastEthReserve` in the stats row. + * + * Call this from any event handler where a trade or stake changes the pool + * balance (Kraiken:Transfer buys/sells, Stake:PositionCreated/Removed). + * EthScarcity/EthAbundance handlers already receive the balance in event args + * and update `lastEthReserve` directly via `updateReserveStats()`. + */ +export async function updateEthReserve(context: StatsContext, poolAddress: `0x${string}`) { + let wethBalance: bigint; + try { + wethBalance = await context.client.readContract({ + abi: erc20BalanceOfAbi, + address: WETH_ADDRESS, + functionName: 'balanceOf', + args: [poolAddress], + }); + } catch (error) { + const logger = getLogger(context); + logger.warn('[stats.updateEthReserve] Failed to read WETH balance', error); + return; + } + + if (wethBalance === 0n) return; // Pool not yet seeded — don't overwrite a real value with 0 + + await context.db.update(stats, { id: STATS_ID }).set({ + lastEthReserve: wethBalance, + }); +} + export async function refreshMinStake(context: StatsContext, statsData?: Awaited>) { let currentStats = statsData; if (!currentStats) { diff --git a/services/ponder/src/kraiken.ts b/services/ponder/src/kraiken.ts index 1056920..dfedaa1 100644 --- a/services/ponder/src/kraiken.ts +++ b/services/ponder/src/kraiken.ts @@ -9,6 +9,7 @@ import { checkBlockHistorySufficient, RING_BUFFER_SEGMENTS, refreshMinStake, + updateEthReserve, } from './helpers/stats'; import { validateContractVersion } from './helpers/version'; @@ -138,6 +139,9 @@ ponder.on('Kraiken:Transfer', async ({ event, context }) => { blockNumber: Number(event.block.number), txHash: event.transaction.hash, }); + + // Update ETH reserve from pool WETH balance — buys/sells shift pool ETH + await updateEthReserve(context, POOL_ADDRESS); } } diff --git a/services/ponder/src/stake.ts b/services/ponder/src/stake.ts index 67cce80..6a26496 100644 --- a/services/ponder/src/stake.ts +++ b/services/ponder/src/stake.ts @@ -7,9 +7,13 @@ import { refreshOutstandingStake, updateHourlyData, checkBlockHistorySufficient, + updateEthReserve, } from './helpers/stats'; import type { StatsContext } from './helpers/stats'; +// Pool address — staking/unstaking events keep lastEthReserve fresh alongside buy/sell events +const POOL_ADDRESS = (process.env.POOL_ADDRESS || '0x1f69cbfc7d3529a4fb4eadf18ec5644b2603b5ab') as `0x${string}`; + const ZERO = 0n; async function getKraikenTotalSupply(context: StatsContext) { @@ -66,6 +70,8 @@ ponder.on('Stake:PositionCreated', async ({ event, context }) => { await refreshOutstandingStake(context); await markPositionsUpdated(context, event.block.timestamp); + // Keep ETH reserve fresh — stake events may coincide with pool activity + await updateEthReserve(context, POOL_ADDRESS); }); ponder.on('Stake:PositionRemoved', async ({ event, context }) => { @@ -100,6 +106,8 @@ ponder.on('Stake:PositionRemoved', async ({ event, context }) => { await refreshOutstandingStake(context); await markPositionsUpdated(context, event.block.timestamp); + // Keep ETH reserve fresh — unstake events may coincide with pool activity + await updateEthReserve(context, POOL_ADDRESS); if (checkBlockHistorySufficient(context, event)) { await updateHourlyData(context, event.block.timestamp);