harb/services/ponder/ponder.schema.ts
openhands 3fceb4145a feat: replace tax with holders in ring buffer, add sparkline charts (#170)
Ring buffer slot 3 now stores holderCount snapshots instead of tax deltas.
Tax tracking simplified to a totalTaxPaid counter on the stats record.
Removed unbounded ethReserveHistory and feeHistory tables; 7d ETH reserve
growth is now computed from the ring buffer. LiveStats renders inline SVG
sparklines for ETH reserve, supply, and holders with holder growth %.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 18:56:36 +00:00

292 lines
7 KiB
TypeScript

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; // ethReserve, minted, burned, holderCount
export const stackMeta = onchainTable('stackMeta', t => ({
id: t.text().primaryKey(),
contractVersion: t
.integer()
.notNull()
.$default(() => 0),
ponderVersion: t
.text()
.notNull()
.$default(() => 'unknown'),
kraikenLibVersion: t
.integer()
.notNull()
.$default(() => 0),
updatedAt: t
.bigint()
.notNull()
.$default(() => 0n),
}));
// Global protocol stats - singleton with id "0x01"
export const stats = onchainTable('stats', t => ({
id: t.text().primaryKey(), // Always "0x01"
kraikenTotalSupply: t
.bigint()
.notNull()
.$default(() => 0n),
stakeTotalSupply: t
.bigint()
.notNull()
.$default(() => 0n),
outstandingStake: t
.bigint()
.notNull()
.$default(() => 0n),
positionsUpdatedAt: t
.bigint()
.notNull()
.$default(() => 0n),
minStake: t
.bigint()
.notNull()
.$default(() => 0n),
// Totals
totalMinted: t
.bigint()
.notNull()
.$default(() => 0n),
totalBurned: t
.bigint()
.notNull()
.$default(() => 0n),
totalTaxPaid: t
.bigint()
.notNull()
.$default(() => 0n),
// Rolling windows - calculated from ring buffer
mintedLastWeek: t
.bigint()
.notNull()
.$default(() => 0n),
mintedLastDay: t
.bigint()
.notNull()
.$default(() => 0n),
mintNextHourProjected: t
.bigint()
.notNull()
.$default(() => 0n),
burnedLastWeek: t
.bigint()
.notNull()
.$default(() => 0n),
burnedLastDay: t
.bigint()
.notNull()
.$default(() => 0n),
burnNextHourProjected: t
.bigint()
.notNull()
.$default(() => 0n),
taxPaidLastWeek: t
.bigint()
.notNull()
.$default(() => 0n),
taxPaidLastDay: t
.bigint()
.notNull()
.$default(() => 0n),
taxPaidNextHourProjected: t
.bigint()
.notNull()
.$default(() => 0n),
// Hourly ETH reserve snapshots (from ring buffer slot 0)
ethReserveLastDay: t
.bigint()
.notNull()
.$default(() => 0n),
ethReserveLastWeek: t
.bigint()
.notNull()
.$default(() => 0n),
// Net supply change (minted - burned)
netSupplyChangeDay: t
.bigint()
.notNull()
.$default(() => 0n),
netSupplyChangeWeek: t
.bigint()
.notNull()
.$default(() => 0n),
// Ring buffer state (flattened array of length HOURS_IN_RING_BUFFER * 4)
ringBufferPointer: t
.integer()
.notNull()
.$default(() => 0),
lastHourlyUpdateTimestamp: t
.bigint()
.notNull()
.$default(() => 0n),
ringBuffer: t
.jsonb()
.$type<string[]>()
.notNull()
.$default(() => Array(HOURS_IN_RING_BUFFER * RING_BUFFER_SEGMENTS).fill('0')),
// LiquidityManager stats
holderCount: t
.integer()
.notNull()
.$default(() => 0),
lastRecenterTimestamp: t
.bigint()
.notNull()
.$default(() => 0n),
lastRecenterTick: t
.integer()
.notNull()
.$default(() => 0),
recentersLastDay: t
.integer()
.notNull()
.$default(() => 0),
recentersLastWeek: t
.integer()
.notNull()
.$default(() => 0),
lastEthReserve: t
.bigint()
.notNull()
.$default(() => 0n),
lastVwapTick: t
.integer()
.notNull()
.$default(() => 0),
// 7-day ETH reserve growth metrics
ethReserve7dAgo: t.bigint(),
ethReserveGrowthBps: t.integer(),
// 7-day trading fees earned
feesEarned7dEth: t
.bigint()
.notNull()
.$default(() => 0n),
feesEarned7dKrk: t
.bigint()
.notNull()
.$default(() => 0n),
feesLastUpdated: t.bigint(),
// Floor price metrics
floorTick: t.integer(),
floorPriceWei: t.bigint(),
currentPriceWei: t.bigint(),
floorDistanceBps: t.integer(),
}));
// Individual staking positions
export const positions = onchainTable(
'positions',
t => ({
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%) - for display
taxRateIndex: t.integer().notNull(), // Tax rate index from contract - source of truth
kraikenDeposit: t.bigint().notNull(),
stakeDeposit: t.bigint().notNull(),
taxPaid: t
.bigint()
.notNull()
.$default(() => 0n),
snatched: t
.integer()
.notNull()
.$default(() => 0),
creationTime: t.bigint().notNull(),
lastTaxTime: t.bigint().notNull(),
status: t
.text()
.notNull()
.$default(() => 'Active'), // "Active" or "Closed"
createdAt: t.bigint().notNull(),
closedAt: t.bigint(),
totalSupplyInit: t.bigint().notNull(),
totalSupplyEnd: t.bigint(),
payout: t
.bigint()
.notNull()
.$default(() => 0n),
}),
table => ({
ownerIdx: index().on(table.owner),
statusIdx: index().on(table.status),
taxRateIndexIdx: index().on(table.taxRateIndex),
})
);
// 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);
// Recenters - track LiquidityManager recenter events
export const recenters = onchainTable('recenters', t => ({
id: t.text().primaryKey(), // block_logIndex format
timestamp: t.bigint().notNull(),
currentTick: t.integer().notNull(),
isUp: t.boolean().notNull(),
ethBalance: t.bigint(), // nullable - only from Scarcity/Abundance events
outstandingSupply: t.bigint(), // nullable
vwapTick: t.integer(), // nullable
}));
// Holders - track Kraiken token holders with cost basis for P&L
export const holders = onchainTable(
'holders',
t => ({
address: t.hex().primaryKey(),
balance: t.bigint().notNull(),
// Cost basis tracking (updated on swaps only, not wallet-to-wallet transfers)
totalEthSpent: t
.bigint()
.notNull()
.$default(() => 0n), // cumulative ETH spent buying KRK
totalTokensAcquired: t
.bigint()
.notNull()
.$default(() => 0n), // cumulative KRK received from buys
}),
table => ({
addressIdx: index().on(table.address),
})
);
// Transaction history for wallet dashboard
export const transactions = onchainTable(
'transactions',
t => ({
id: t.text().primaryKey(), // txHash-logIndex
holder: t.hex().notNull(),
type: t.text().notNull(), // "buy" | "sell" | "stake" | "unstake" | "snatch_in" | "snatch_out"
tokenAmount: t.bigint().notNull(),
ethAmount: t
.bigint()
.notNull()
.$default(() => 0n),
timestamp: t.bigint().notNull(),
blockNumber: t.integer().notNull(),
txHash: t.hex().notNull(),
}),
table => ({
holderIdx: index().on(table.holder),
timestampIdx: index().on(table.timestamp),
})
);
// Helper constants
export const STATS_ID = '0x01';
export const SECONDS_IN_HOUR = 3600;