harb/services/ponder/src/kraiken.ts
2026-02-23 21:57:13 +00:00

218 lines
7.5 KiB
TypeScript

import { ponder } from 'ponder:registry';
import { getLogger } from './helpers/logger';
import { stats, holders, transactions, STATS_ID } from 'ponder:schema';
import {
ensureStatsExists,
parseRingBuffer,
serializeRingBuffer,
updateHourlyData,
checkBlockHistorySufficient,
RING_BUFFER_SEGMENTS,
refreshMinStake,
updateEthReserve,
} from './helpers/stats';
import { validateContractVersion } from './helpers/version';
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' as const;
// Pool address for detecting swaps (buys/sells)
// Computed deterministically from Uniswap V3 factory + WETH + Kraiken + 1% fee
const POOL_ADDRESS = (process.env.POOL_ADDRESS || '0x1f69cbfc7d3529a4fb4eadf18ec5644b2603b5ab').toLowerCase() as `0x${string}`;
// Track if version has been validated
let versionValidated = false;
ponder.on('Kraiken:Transfer', async ({ event, context }) => {
// Validate version once at first event
if (!versionValidated) {
await validateContractVersion(context);
versionValidated = true;
}
const { from, to, value } = event.args;
await ensureStatsExists(context, event.block.timestamp);
// Track holder balances for ALL transfers (not just mint/burn)
const statsData = await context.db.find(stats, { id: STATS_ID });
if (!statsData) return;
let holderCountDelta = 0;
// CRITICAL FIX: Skip holder tracking for self-transfers (from === to)
// Self-transfers don't change balances or holder counts
const isSelfTransfer = from !== ZERO_ADDRESS && to !== ZERO_ADDRESS && from === to;
if (!isSelfTransfer) {
// Update 'from' holder (if not mint)
if (from !== ZERO_ADDRESS) {
const fromHolder = await context.db.find(holders, { address: from });
// CRITICAL FIX: Validate that holder exists before processing transfer
if (!fromHolder) {
getLogger(context).error(`Transfer from non-existent holder ${from} in block ${event.block.number}. This should not happen.`);
// Don't process this transfer's holder tracking
return;
}
const newBalance = fromHolder.balance - value;
// CRITICAL FIX: Prevent negative balances
if (newBalance < 0n) {
getLogger(context).error(`Transfer would create negative balance for ${from}: ${fromHolder.balance} - ${value} = ${newBalance}`);
return;
}
if (newBalance === 0n) {
// Holder balance went to zero - remove from holder count
await context.db.update(holders, { address: from }).set({
balance: 0n,
});
holderCountDelta -= 1;
} else {
await context.db.update(holders, { address: from }).set({
balance: newBalance,
});
}
}
// Update 'to' holder (if not burn)
if (to !== ZERO_ADDRESS) {
const toHolder = await context.db.find(holders, { address: to });
const oldBalance = toHolder?.balance ?? 0n;
const newBalance = oldBalance + value;
// Detect buy: tokens coming FROM the pool = user bought KRK
const isBuy = from.toLowerCase() === POOL_ADDRESS;
let ethSpentDelta = 0n;
if (isBuy && value > 0n) {
// Approximate ETH cost using current price from stats
const currentPrice = statsData.currentPriceWei ?? 0n;
if (currentPrice > 0n) {
ethSpentDelta = (value * currentPrice) / 10n ** 18n;
}
}
if (toHolder) {
await context.db.update(holders, { address: to }).set({
balance: newBalance,
...(isBuy && {
totalEthSpent: (toHolder.totalEthSpent ?? 0n) + ethSpentDelta,
totalTokensAcquired: (toHolder.totalTokensAcquired ?? 0n) + value,
}),
});
// If this was a new holder (balance was 0), increment count
if (oldBalance === 0n) {
holderCountDelta += 1;
}
} else {
// New holder
await context.db.insert(holders).values({
address: to,
balance: newBalance,
totalEthSpent: ethSpentDelta,
totalTokensAcquired: isBuy ? value : 0n,
});
holderCountDelta += 1;
}
}
}
// Record buy/sell transactions
if (!isSelfTransfer && from !== ZERO_ADDRESS && to !== ZERO_ADDRESS) {
const isBuy = from.toLowerCase() === POOL_ADDRESS;
const isSell = to.toLowerCase() === POOL_ADDRESS;
if (isBuy || isSell) {
const currentPrice = statsData.currentPriceWei ?? 0n;
const ethEstimate = currentPrice > 0n ? (value * currentPrice) / 10n ** 18n : 0n;
const txId = `${event.transaction.hash}-${event.log.logIndex}`;
await context.db.insert(transactions).values({
id: txId,
holder: isBuy ? to : from,
type: isBuy ? 'buy' : 'sell',
tokenAmount: value,
ethAmount: ethEstimate,
timestamp: event.block.timestamp,
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);
}
}
// Update holder count if changed (with underflow protection)
if (holderCountDelta !== 0) {
const newHolderCount = statsData.holderCount + holderCountDelta;
// IMPORTANT FIX: Prevent holder count underflow
if (newHolderCount < 0) {
getLogger(context).error(
`Holder count would go negative: ${statsData.holderCount} + ${holderCountDelta} = ${newHolderCount}. Skipping update.`
);
return;
}
await context.db.update(stats, { id: STATS_ID }).set({
holderCount: newHolderCount,
});
}
// Check if we have sufficient block history for reliable ringbuffer operations
if (!checkBlockHistorySufficient(context, event)) {
// Insufficient history - skip ringbuffer updates but continue with basic stats
if (from === ZERO_ADDRESS) {
await context.db.update(stats, { id: STATS_ID }).set({
kraikenTotalSupply: statsData.kraikenTotalSupply + value,
totalMinted: statsData.totalMinted + value,
});
} else if (to === ZERO_ADDRESS) {
await context.db.update(stats, { id: STATS_ID }).set({
kraikenTotalSupply: statsData.kraikenTotalSupply - value,
totalBurned: statsData.totalBurned + value,
});
}
return;
}
await updateHourlyData(context, event.block.timestamp);
const ringBuffer = parseRingBuffer(statsData.ringBuffer as string[]);
const pointer = statsData.ringBufferPointer ?? 0;
const baseIndex = pointer * RING_BUFFER_SEGMENTS;
if (from === ZERO_ADDRESS) {
ringBuffer[baseIndex + 1] = ringBuffer[baseIndex + 1] + value;
await context.db.update(stats, { id: STATS_ID }).set({
ringBuffer: serializeRingBuffer(ringBuffer),
kraikenTotalSupply: statsData.kraikenTotalSupply + value,
totalMinted: statsData.totalMinted + value,
});
} else if (to === ZERO_ADDRESS) {
ringBuffer[baseIndex + 2] = ringBuffer[baseIndex + 2] + value;
await context.db.update(stats, { id: STATS_ID }).set({
ringBuffer: serializeRingBuffer(ringBuffer),
kraikenTotalSupply: statsData.kraikenTotalSupply - value,
totalBurned: statsData.totalBurned + value,
});
}
await updateHourlyData(context, event.block.timestamp);
});
ponder.on('StatsBlock:block', async ({ event, context }) => {
const statsData = await ensureStatsExists(context, event.block.timestamp);
await refreshMinStake(context, statsData);
// Only update hourly data if we have sufficient block history
if (checkBlockHistorySufficient(context, event)) {
await updateHourlyData(context, event.block.timestamp);
}
});