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

186 lines
6.5 KiB
TypeScript

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