tax rate, version and compose (#70)

resolves #67

Co-authored-by: johba <johba@harb.eth>
Reviewed-on: https://codeberg.org/johba/harb/pulls/70
This commit is contained in:
johba 2025-10-07 19:26:08 +02:00
parent d8ca557eb6
commit 6cbb1781ce
40 changed files with 1243 additions and 213 deletions

View file

@ -6,6 +6,7 @@ Ponder-based indexer that records Kraiken protocol activity and exposes the Grap
- Track Kraiken token transfers and staking events to maintain protocol stats, hourly ring buffers, and position state.
- Serve a GraphQL endpoint at `http://localhost:42069/graphql` for downstream consumers.
- Support `BASE_SEPOLIA_LOCAL_FORK`, `BASE_SEPOLIA`, and `BASE` environments through a single TypeScript codebase.
- **Tax Rate Handling**: Import `TAX_RATE_OPTIONS` from `kraiken-lib` (synced with Stake.sol via `scripts/sync-tax-rates.mjs`). The schema stores both `taxRateIndex` (source of truth) and `taxRate` decimal (for display). Event handlers extract the index from contract events and look up the decimal value.
## Key Files
- `ponder.config.ts` - Network selection and contract bindings (update addresses after deployments).

0
services/ponder/ponder-env.d.ts vendored Executable file → Normal file
View file

View file

@ -1,4 +1,5 @@
import { onchainTable, index } from 'ponder';
import { TAX_RATE_OPTIONS } from 'kraiken-lib/taxRates';
export const HOURS_IN_RING_BUFFER = 168; // 7 days * 24 hours
const RING_BUFFER_SEGMENTS = 4; // ubi, minted, burned, tax
@ -113,7 +114,8 @@ export const positions = onchainTable(
id: t.text().primaryKey(), // Position ID from contract
owner: t.hex().notNull(),
share: t.real().notNull(), // Share as decimal (0-1)
taxRate: t.real().notNull(), // Tax rate as decimal (e.g., 0.01 for 1%)
taxRate: t.real().notNull(), // Tax rate as decimal (e.g., 0.01 for 1%) - for display
taxRateIndex: t.integer().notNull(), // Tax rate index from contract - source of truth
kraikenDeposit: t.bigint().notNull(),
stakeDeposit: t.bigint().notNull(),
taxPaid: t
@ -142,14 +144,13 @@ export const positions = onchainTable(
table => ({
ownerIdx: index().on(table.owner),
statusIdx: index().on(table.status),
taxRateIndexIdx: index().on(table.taxRateIndex),
})
);
// Constants for tax rates (matches subgraph)
export const TAX_RATES = [
0.01, 0.03, 0.05, 0.07, 0.09, 0.11, 0.13, 0.15, 0.17, 0.19, 0.21, 0.25, 0.29, 0.33, 0.37, 0.41, 0.45, 0.49, 0.53, 0.57, 0.61, 0.65, 0.69,
0.73, 0.77, 0.81, 0.85, 0.89, 0.93, 0.97,
];
// Export decimal values for backward compatibility in event handlers
// Maps index → decimal (e.g., TAX_RATES[0] = 0.01 for 1% yearly)
export const TAX_RATES = TAX_RATE_OPTIONS.map(opt => opt.decimal);
// Helper constants
export const STATS_ID = '0x01';

View file

@ -0,0 +1,32 @@
import { isCompatibleVersion, getVersionMismatchError, KRAIKEN_LIB_VERSION } from 'kraiken-lib/version';
import type { Context } from 'ponder:registry';
/**
* Validates that the deployed Kraiken contract version is compatible
* with this indexer's kraiken-lib version.
*
* MUST be called at Ponder startup before processing any events.
* Fails hard (process.exit) on mismatch to prevent indexing wrong data.
*/
export async function validateContractVersion(context: Context): Promise<void> {
try {
const contractVersion = await context.client.readContract({
address: context.contracts.Kraiken.address,
abi: context.contracts.Kraiken.abi,
functionName: 'VERSION',
});
const versionNumber = Number(contractVersion);
if (!isCompatibleVersion(versionNumber)) {
console.error(getVersionMismatchError(versionNumber, 'ponder'));
process.exit(1);
}
console.log(`✓ Contract version validated: v${versionNumber} (kraiken-lib v${KRAIKEN_LIB_VERSION})`);
} catch (error) {
console.error('Failed to read contract VERSION:', error);
console.error('Ensure Kraiken contract has VERSION constant and is deployed correctly');
process.exit(1);
}
}

View file

@ -8,10 +8,20 @@ import {
checkBlockHistorySufficient,
RING_BUFFER_SEGMENTS,
} from './helpers/stats';
import { validateContractVersion } from './helpers/version';
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000' as const;
// 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);

View file

@ -34,11 +34,13 @@ ponder.on('Stake:PositionCreated', async ({ event, 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[Number(event.args.taxRate)] || 0,
taxRate: TAX_RATES[taxRateIndex] || 0,
taxRateIndex,
kraikenDeposit: event.args.kraikenDeposit,
stakeDeposit: event.args.kraikenDeposit,
taxPaid: ZERO,
@ -115,10 +117,12 @@ ponder.on('Stake:PositionTaxPaid', async ({ event, context }) => {
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[Number(event.args.taxRate)] || position.taxRate,
taxRate: TAX_RATES[taxRateIndex] || position.taxRate,
taxRateIndex,
lastTaxTime: event.block.timestamp,
});
@ -154,7 +158,9 @@ ponder.on('Stake:PositionTaxPaid', async ({ event, context }) => {
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[Number(event.args.newTaxRate)] || 0,
taxRate: TAX_RATES[taxRateIndex] || 0,
taxRateIndex,
});
});