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);