import { ponder } from 'ponder:registry'; import { positions, transactions, stats, STATS_ID, TAX_RATES } from 'ponder:schema'; import { ensureStatsExists, getStakeTotalSupply, markPositionsUpdated, 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) { return context.client.readContract({ abi: context.contracts.Kraiken.abi, address: context.contracts.Kraiken.address, functionName: 'totalSupply', }); } function toShareRatio(share: bigint, stakeTotalSupply: bigint): number { if (stakeTotalSupply === 0n) return 0; return Number(share) / Number(stakeTotalSupply); } ponder.on('Stake:PositionCreated', async ({ event, context }) => { await ensureStatsExists(context, event.block.timestamp); const stakeTotalSupply = await getStakeTotalSupply(context); const shareRatio = toShareRatio(event.args.share, stakeTotalSupply); const totalSupplyInit = await getKraikenTotalSupply(context); const taxRateIndex = Number(event.args.taxRate); await context.db.insert(positions).values({ id: event.args.positionId.toString(), owner: event.args.owner as `0x${string}`, share: shareRatio, taxRate: TAX_RATES[taxRateIndex] || 0, taxRateIndex, kraikenDeposit: event.args.kraikenDeposit, stakeDeposit: event.args.kraikenDeposit, taxPaid: ZERO, snatched: 0, creationTime: event.block.timestamp, lastTaxTime: event.block.timestamp, status: 'Active', createdAt: event.block.timestamp, totalSupplyInit, totalSupplyEnd: null, payout: ZERO, }); // Record stake transaction await context.db.insert(transactions).values({ id: `${event.transaction.hash}-${event.log.logIndex}`, holder: event.args.owner as `0x${string}`, type: 'stake', tokenAmount: event.args.kraikenDeposit, ethAmount: ZERO, timestamp: event.block.timestamp, blockNumber: Number(event.block.number), txHash: event.transaction.hash, }); 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 }) => { await ensureStatsExists(context, event.block.timestamp); const positionId = event.args.positionId.toString(); const position = await context.db.find(positions, { id: positionId }); if (!position) return; const totalSupplyEnd = await getKraikenTotalSupply(context); await context.db.update(positions, { id: positionId }).set({ status: 'Closed', closedAt: event.block.timestamp, totalSupplyEnd, payout: (position.payout ?? ZERO) + event.args.kraikenPayout, kraikenDeposit: ZERO, stakeDeposit: ZERO, }); // Record unstake transaction (could be voluntary unstake or snatch payout) await context.db.insert(transactions).values({ id: `${event.transaction.hash}-${event.log.logIndex}`, holder: position.owner, type: 'unstake', tokenAmount: event.args.kraikenPayout, ethAmount: ZERO, timestamp: event.block.timestamp, blockNumber: Number(event.block.number), txHash: event.transaction.hash, }); 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); } }); ponder.on('Stake:PositionShrunk', async ({ event, context }) => { await ensureStatsExists(context, event.block.timestamp); const positionId = event.args.positionId.toString(); const position = await context.db.find(positions, { id: positionId }); if (!position) return; const stakeTotalSupply = await getStakeTotalSupply(context); const shareRatio = toShareRatio(event.args.newShares, stakeTotalSupply); await context.db.update(positions, { id: positionId }).set({ share: shareRatio, kraikenDeposit: BigInt(position.kraikenDeposit ?? ZERO) - event.args.kraikenPayout, stakeDeposit: BigInt(position.stakeDeposit ?? ZERO) - event.args.kraikenPayout, snatched: position.snatched + 1, payout: BigInt(position.payout ?? ZERO) + event.args.kraikenPayout, }); await refreshOutstandingStake(context); await markPositionsUpdated(context, event.block.timestamp); if (checkBlockHistorySufficient(context, event)) { await updateHourlyData(context, event.block.timestamp); } }); ponder.on('Stake:PositionTaxPaid', async ({ event, context }) => { await ensureStatsExists(context, event.block.timestamp); const positionId = event.args.positionId.toString(); const position = await context.db.find(positions, { id: positionId }); if (!position) return; const stakeTotalSupply = await getStakeTotalSupply(context); const shareRatio = toShareRatio(event.args.newShares, stakeTotalSupply); const taxRateIndex = Number(event.args.taxRate); await context.db.update(positions, { id: positionId }).set({ taxPaid: BigInt(position.taxPaid ?? ZERO) + event.args.taxPaid, share: shareRatio, taxRate: TAX_RATES[taxRateIndex] || position.taxRate, taxRateIndex, lastTaxTime: event.block.timestamp, }); // Update totalTaxPaid counter (no longer ring-buffered) const statsData = await context.db.find(stats, { id: STATS_ID }); if (statsData) { await context.db.update(stats, { id: STATS_ID }).set({ totalTaxPaid: statsData.totalTaxPaid + event.args.taxPaid, }); } if (checkBlockHistorySufficient(context, event)) { await updateHourlyData(context, event.block.timestamp); } await refreshOutstandingStake(context); await markPositionsUpdated(context, event.block.timestamp); }); ponder.on('Stake:PositionRateHiked', async ({ event, context }) => { const positionId = event.args.positionId.toString(); const taxRateIndex = Number(event.args.newTaxRate); await context.db.update(positions, { id: positionId }).set({ taxRate: TAX_RATES[taxRateIndex] || 0, taxRateIndex, }); await markPositionsUpdated(context, event.block.timestamp); });