feat/ponder-lm-indexing (#142)
This commit is contained in:
parent
de3c8eef94
commit
31063379a8
107 changed files with 12517 additions and 367 deletions
|
|
@ -21,8 +21,16 @@ type Query {
|
|||
stackMetas(where: stackMetaFilter, orderBy: String, orderDirection: String, before: String, after: String, limit: Int): stackMetaPage!
|
||||
stats(id: String!): stats
|
||||
statss(where: statsFilter, orderBy: String, orderDirection: String, before: String, after: String, limit: Int): statsPage!
|
||||
ethReserveHistory(id: String!): ethReserveHistory
|
||||
ethReserveHistorys(where: ethReserveHistoryFilter, orderBy: String, orderDirection: String, before: String, after: String, limit: Int): ethReserveHistoryPage!
|
||||
feeHistory(id: String!): feeHistory
|
||||
feeHistorys(where: feeHistoryFilter, orderBy: String, orderDirection: String, before: String, after: String, limit: Int): feeHistoryPage!
|
||||
positions(id: String!): positions
|
||||
positionss(where: positionsFilter, orderBy: String, orderDirection: String, before: String, after: String, limit: Int): positionsPage!
|
||||
recenters(id: String!): recenters
|
||||
recenterss(where: recentersFilter, orderBy: String, orderDirection: String, before: String, after: String, limit: Int): recentersPage!
|
||||
holders(address: String!): holders
|
||||
holderss(where: holdersFilter, orderBy: String, orderDirection: String, before: String, after: String, limit: Int): holdersPage!
|
||||
_meta: Meta
|
||||
}
|
||||
|
||||
|
|
@ -115,6 +123,22 @@ type stats {
|
|||
ringBufferPointer: Int!
|
||||
lastHourlyUpdateTimestamp: BigInt!
|
||||
ringBuffer: JSON!
|
||||
holderCount: Int!
|
||||
lastRecenterTimestamp: BigInt!
|
||||
lastRecenterTick: Int!
|
||||
recentersLastDay: Int!
|
||||
recentersLastWeek: Int!
|
||||
lastEthReserve: BigInt!
|
||||
lastVwapTick: Int!
|
||||
ethReserve7dAgo: BigInt
|
||||
ethReserveGrowthBps: Int
|
||||
feesEarned7dEth: BigInt!
|
||||
feesEarned7dKrk: BigInt!
|
||||
feesLastUpdated: BigInt
|
||||
floorTick: Int
|
||||
floorPriceWei: BigInt
|
||||
currentPriceWei: BigInt
|
||||
floorDistanceBps: Int
|
||||
}
|
||||
|
||||
type statsPage {
|
||||
|
|
@ -320,6 +344,229 @@ input statsFilter {
|
|||
lastHourlyUpdateTimestamp_lt: BigInt
|
||||
lastHourlyUpdateTimestamp_gte: BigInt
|
||||
lastHourlyUpdateTimestamp_lte: BigInt
|
||||
holderCount: Int
|
||||
holderCount_not: Int
|
||||
holderCount_in: [Int]
|
||||
holderCount_not_in: [Int]
|
||||
holderCount_gt: Int
|
||||
holderCount_lt: Int
|
||||
holderCount_gte: Int
|
||||
holderCount_lte: Int
|
||||
lastRecenterTimestamp: BigInt
|
||||
lastRecenterTimestamp_not: BigInt
|
||||
lastRecenterTimestamp_in: [BigInt]
|
||||
lastRecenterTimestamp_not_in: [BigInt]
|
||||
lastRecenterTimestamp_gt: BigInt
|
||||
lastRecenterTimestamp_lt: BigInt
|
||||
lastRecenterTimestamp_gte: BigInt
|
||||
lastRecenterTimestamp_lte: BigInt
|
||||
lastRecenterTick: Int
|
||||
lastRecenterTick_not: Int
|
||||
lastRecenterTick_in: [Int]
|
||||
lastRecenterTick_not_in: [Int]
|
||||
lastRecenterTick_gt: Int
|
||||
lastRecenterTick_lt: Int
|
||||
lastRecenterTick_gte: Int
|
||||
lastRecenterTick_lte: Int
|
||||
recentersLastDay: Int
|
||||
recentersLastDay_not: Int
|
||||
recentersLastDay_in: [Int]
|
||||
recentersLastDay_not_in: [Int]
|
||||
recentersLastDay_gt: Int
|
||||
recentersLastDay_lt: Int
|
||||
recentersLastDay_gte: Int
|
||||
recentersLastDay_lte: Int
|
||||
recentersLastWeek: Int
|
||||
recentersLastWeek_not: Int
|
||||
recentersLastWeek_in: [Int]
|
||||
recentersLastWeek_not_in: [Int]
|
||||
recentersLastWeek_gt: Int
|
||||
recentersLastWeek_lt: Int
|
||||
recentersLastWeek_gte: Int
|
||||
recentersLastWeek_lte: Int
|
||||
lastEthReserve: BigInt
|
||||
lastEthReserve_not: BigInt
|
||||
lastEthReserve_in: [BigInt]
|
||||
lastEthReserve_not_in: [BigInt]
|
||||
lastEthReserve_gt: BigInt
|
||||
lastEthReserve_lt: BigInt
|
||||
lastEthReserve_gte: BigInt
|
||||
lastEthReserve_lte: BigInt
|
||||
lastVwapTick: Int
|
||||
lastVwapTick_not: Int
|
||||
lastVwapTick_in: [Int]
|
||||
lastVwapTick_not_in: [Int]
|
||||
lastVwapTick_gt: Int
|
||||
lastVwapTick_lt: Int
|
||||
lastVwapTick_gte: Int
|
||||
lastVwapTick_lte: Int
|
||||
ethReserve7dAgo: BigInt
|
||||
ethReserve7dAgo_not: BigInt
|
||||
ethReserve7dAgo_in: [BigInt]
|
||||
ethReserve7dAgo_not_in: [BigInt]
|
||||
ethReserve7dAgo_gt: BigInt
|
||||
ethReserve7dAgo_lt: BigInt
|
||||
ethReserve7dAgo_gte: BigInt
|
||||
ethReserve7dAgo_lte: BigInt
|
||||
ethReserveGrowthBps: Int
|
||||
ethReserveGrowthBps_not: Int
|
||||
ethReserveGrowthBps_in: [Int]
|
||||
ethReserveGrowthBps_not_in: [Int]
|
||||
ethReserveGrowthBps_gt: Int
|
||||
ethReserveGrowthBps_lt: Int
|
||||
ethReserveGrowthBps_gte: Int
|
||||
ethReserveGrowthBps_lte: Int
|
||||
feesEarned7dEth: BigInt
|
||||
feesEarned7dEth_not: BigInt
|
||||
feesEarned7dEth_in: [BigInt]
|
||||
feesEarned7dEth_not_in: [BigInt]
|
||||
feesEarned7dEth_gt: BigInt
|
||||
feesEarned7dEth_lt: BigInt
|
||||
feesEarned7dEth_gte: BigInt
|
||||
feesEarned7dEth_lte: BigInt
|
||||
feesEarned7dKrk: BigInt
|
||||
feesEarned7dKrk_not: BigInt
|
||||
feesEarned7dKrk_in: [BigInt]
|
||||
feesEarned7dKrk_not_in: [BigInt]
|
||||
feesEarned7dKrk_gt: BigInt
|
||||
feesEarned7dKrk_lt: BigInt
|
||||
feesEarned7dKrk_gte: BigInt
|
||||
feesEarned7dKrk_lte: BigInt
|
||||
feesLastUpdated: BigInt
|
||||
feesLastUpdated_not: BigInt
|
||||
feesLastUpdated_in: [BigInt]
|
||||
feesLastUpdated_not_in: [BigInt]
|
||||
feesLastUpdated_gt: BigInt
|
||||
feesLastUpdated_lt: BigInt
|
||||
feesLastUpdated_gte: BigInt
|
||||
feesLastUpdated_lte: BigInt
|
||||
floorTick: Int
|
||||
floorTick_not: Int
|
||||
floorTick_in: [Int]
|
||||
floorTick_not_in: [Int]
|
||||
floorTick_gt: Int
|
||||
floorTick_lt: Int
|
||||
floorTick_gte: Int
|
||||
floorTick_lte: Int
|
||||
floorPriceWei: BigInt
|
||||
floorPriceWei_not: BigInt
|
||||
floorPriceWei_in: [BigInt]
|
||||
floorPriceWei_not_in: [BigInt]
|
||||
floorPriceWei_gt: BigInt
|
||||
floorPriceWei_lt: BigInt
|
||||
floorPriceWei_gte: BigInt
|
||||
floorPriceWei_lte: BigInt
|
||||
currentPriceWei: BigInt
|
||||
currentPriceWei_not: BigInt
|
||||
currentPriceWei_in: [BigInt]
|
||||
currentPriceWei_not_in: [BigInt]
|
||||
currentPriceWei_gt: BigInt
|
||||
currentPriceWei_lt: BigInt
|
||||
currentPriceWei_gte: BigInt
|
||||
currentPriceWei_lte: BigInt
|
||||
floorDistanceBps: Int
|
||||
floorDistanceBps_not: Int
|
||||
floorDistanceBps_in: [Int]
|
||||
floorDistanceBps_not_in: [Int]
|
||||
floorDistanceBps_gt: Int
|
||||
floorDistanceBps_lt: Int
|
||||
floorDistanceBps_gte: Int
|
||||
floorDistanceBps_lte: Int
|
||||
}
|
||||
|
||||
type ethReserveHistory {
|
||||
id: String!
|
||||
timestamp: BigInt!
|
||||
ethBalance: BigInt!
|
||||
}
|
||||
|
||||
type ethReserveHistoryPage {
|
||||
items: [ethReserveHistory!]!
|
||||
pageInfo: PageInfo!
|
||||
totalCount: Int!
|
||||
}
|
||||
|
||||
input ethReserveHistoryFilter {
|
||||
AND: [ethReserveHistoryFilter]
|
||||
OR: [ethReserveHistoryFilter]
|
||||
id: String
|
||||
id_not: String
|
||||
id_in: [String]
|
||||
id_not_in: [String]
|
||||
id_contains: String
|
||||
id_not_contains: String
|
||||
id_starts_with: String
|
||||
id_ends_with: String
|
||||
id_not_starts_with: String
|
||||
id_not_ends_with: String
|
||||
timestamp: BigInt
|
||||
timestamp_not: BigInt
|
||||
timestamp_in: [BigInt]
|
||||
timestamp_not_in: [BigInt]
|
||||
timestamp_gt: BigInt
|
||||
timestamp_lt: BigInt
|
||||
timestamp_gte: BigInt
|
||||
timestamp_lte: BigInt
|
||||
ethBalance: BigInt
|
||||
ethBalance_not: BigInt
|
||||
ethBalance_in: [BigInt]
|
||||
ethBalance_not_in: [BigInt]
|
||||
ethBalance_gt: BigInt
|
||||
ethBalance_lt: BigInt
|
||||
ethBalance_gte: BigInt
|
||||
ethBalance_lte: BigInt
|
||||
}
|
||||
|
||||
type feeHistory {
|
||||
id: String!
|
||||
timestamp: BigInt!
|
||||
ethFees: BigInt!
|
||||
krkFees: BigInt!
|
||||
}
|
||||
|
||||
type feeHistoryPage {
|
||||
items: [feeHistory!]!
|
||||
pageInfo: PageInfo!
|
||||
totalCount: Int!
|
||||
}
|
||||
|
||||
input feeHistoryFilter {
|
||||
AND: [feeHistoryFilter]
|
||||
OR: [feeHistoryFilter]
|
||||
id: String
|
||||
id_not: String
|
||||
id_in: [String]
|
||||
id_not_in: [String]
|
||||
id_contains: String
|
||||
id_not_contains: String
|
||||
id_starts_with: String
|
||||
id_ends_with: String
|
||||
id_not_starts_with: String
|
||||
id_not_ends_with: String
|
||||
timestamp: BigInt
|
||||
timestamp_not: BigInt
|
||||
timestamp_in: [BigInt]
|
||||
timestamp_not_in: [BigInt]
|
||||
timestamp_gt: BigInt
|
||||
timestamp_lt: BigInt
|
||||
timestamp_gte: BigInt
|
||||
timestamp_lte: BigInt
|
||||
ethFees: BigInt
|
||||
ethFees_not: BigInt
|
||||
ethFees_in: [BigInt]
|
||||
ethFees_not_in: [BigInt]
|
||||
ethFees_gt: BigInt
|
||||
ethFees_lt: BigInt
|
||||
ethFees_gte: BigInt
|
||||
ethFees_lte: BigInt
|
||||
krkFees: BigInt
|
||||
krkFees_not: BigInt
|
||||
krkFees_in: [BigInt]
|
||||
krkFees_not_in: [BigInt]
|
||||
krkFees_gt: BigInt
|
||||
krkFees_lt: BigInt
|
||||
krkFees_gte: BigInt
|
||||
krkFees_lte: BigInt
|
||||
}
|
||||
|
||||
type positions {
|
||||
|
|
@ -493,4 +740,113 @@ input positionsFilter {
|
|||
payout_lt: BigInt
|
||||
payout_gte: BigInt
|
||||
payout_lte: BigInt
|
||||
}
|
||||
|
||||
type recenters {
|
||||
id: String!
|
||||
timestamp: BigInt!
|
||||
currentTick: Int!
|
||||
isUp: Boolean!
|
||||
ethBalance: BigInt
|
||||
outstandingSupply: BigInt
|
||||
vwapTick: Int
|
||||
}
|
||||
|
||||
type recentersPage {
|
||||
items: [recenters!]!
|
||||
pageInfo: PageInfo!
|
||||
totalCount: Int!
|
||||
}
|
||||
|
||||
input recentersFilter {
|
||||
AND: [recentersFilter]
|
||||
OR: [recentersFilter]
|
||||
id: String
|
||||
id_not: String
|
||||
id_in: [String]
|
||||
id_not_in: [String]
|
||||
id_contains: String
|
||||
id_not_contains: String
|
||||
id_starts_with: String
|
||||
id_ends_with: String
|
||||
id_not_starts_with: String
|
||||
id_not_ends_with: String
|
||||
timestamp: BigInt
|
||||
timestamp_not: BigInt
|
||||
timestamp_in: [BigInt]
|
||||
timestamp_not_in: [BigInt]
|
||||
timestamp_gt: BigInt
|
||||
timestamp_lt: BigInt
|
||||
timestamp_gte: BigInt
|
||||
timestamp_lte: BigInt
|
||||
currentTick: Int
|
||||
currentTick_not: Int
|
||||
currentTick_in: [Int]
|
||||
currentTick_not_in: [Int]
|
||||
currentTick_gt: Int
|
||||
currentTick_lt: Int
|
||||
currentTick_gte: Int
|
||||
currentTick_lte: Int
|
||||
isUp: Boolean
|
||||
isUp_not: Boolean
|
||||
isUp_in: [Boolean]
|
||||
isUp_not_in: [Boolean]
|
||||
ethBalance: BigInt
|
||||
ethBalance_not: BigInt
|
||||
ethBalance_in: [BigInt]
|
||||
ethBalance_not_in: [BigInt]
|
||||
ethBalance_gt: BigInt
|
||||
ethBalance_lt: BigInt
|
||||
ethBalance_gte: BigInt
|
||||
ethBalance_lte: BigInt
|
||||
outstandingSupply: BigInt
|
||||
outstandingSupply_not: BigInt
|
||||
outstandingSupply_in: [BigInt]
|
||||
outstandingSupply_not_in: [BigInt]
|
||||
outstandingSupply_gt: BigInt
|
||||
outstandingSupply_lt: BigInt
|
||||
outstandingSupply_gte: BigInt
|
||||
outstandingSupply_lte: BigInt
|
||||
vwapTick: Int
|
||||
vwapTick_not: Int
|
||||
vwapTick_in: [Int]
|
||||
vwapTick_not_in: [Int]
|
||||
vwapTick_gt: Int
|
||||
vwapTick_lt: Int
|
||||
vwapTick_gte: Int
|
||||
vwapTick_lte: Int
|
||||
}
|
||||
|
||||
type holders {
|
||||
address: String!
|
||||
balance: BigInt!
|
||||
}
|
||||
|
||||
type holdersPage {
|
||||
items: [holders!]!
|
||||
pageInfo: PageInfo!
|
||||
totalCount: Int!
|
||||
}
|
||||
|
||||
input holdersFilter {
|
||||
AND: [holdersFilter]
|
||||
OR: [holdersFilter]
|
||||
address: String
|
||||
address_not: String
|
||||
address_in: [String]
|
||||
address_not_in: [String]
|
||||
address_contains: String
|
||||
address_not_contains: String
|
||||
address_starts_with: String
|
||||
address_ends_with: String
|
||||
address_not_starts_with: String
|
||||
address_not_ends_with: String
|
||||
balance: BigInt
|
||||
balance_not: BigInt
|
||||
balance_in: [BigInt]
|
||||
balance_not_in: [BigInt]
|
||||
balance_gt: BigInt
|
||||
balance_lt: BigInt
|
||||
balance_gte: BigInt
|
||||
balance_lte: BigInt
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { createConfig } from 'ponder';
|
||||
import type { Abi } from 'viem';
|
||||
import { KraikenAbi, StakeAbi } from 'kraiken-lib/abis';
|
||||
import { KraikenAbi, StakeAbi, LiquidityManagerAbi } from 'kraiken-lib/abis';
|
||||
|
||||
// Network configurations keyed by canonical environment name
|
||||
type NetworkConfig = {
|
||||
|
|
@ -10,6 +10,7 @@ type NetworkConfig = {
|
|||
contracts: {
|
||||
kraiken: string;
|
||||
stake: string;
|
||||
liquidityManager: string;
|
||||
startBlock: number;
|
||||
};
|
||||
};
|
||||
|
|
@ -22,6 +23,7 @@ const networks: Record<string, NetworkConfig> = {
|
|||
contracts: {
|
||||
kraiken: process.env.KRAIKEN_ADDRESS || '0x56186c1E64cA8043dEF78d06AfF222212eA5df71',
|
||||
stake: process.env.STAKE_ADDRESS || '0x056E4a859558A3975761ABd7385506BC4D8A8E60',
|
||||
liquidityManager: process.env.LM_ADDRESS || '0x33d10f2449ffede92b43d4fba562f132ba6a766a',
|
||||
startBlock: parseInt(process.env.START_BLOCK || '31425917'),
|
||||
},
|
||||
},
|
||||
|
|
@ -31,6 +33,7 @@ const networks: Record<string, NetworkConfig> = {
|
|||
contracts: {
|
||||
kraiken: '0x22c264Ecf8D4E49D1E3CabD8DD39b7C4Ab51C1B8',
|
||||
stake: '0xe28020BCdEeAf2779dd47c670A8eFC2973316EE2',
|
||||
liquidityManager: process.env.LM_ADDRESS || '0x0000000000000000000000000000000000000000',
|
||||
startBlock: 20940337,
|
||||
},
|
||||
},
|
||||
|
|
@ -40,6 +43,7 @@ const networks: Record<string, NetworkConfig> = {
|
|||
contracts: {
|
||||
kraiken: '0x45caa5929f6ee038039984205bdecf968b954820',
|
||||
stake: '0xed70707fab05d973ad41eae8d17e2bcd36192cfc',
|
||||
liquidityManager: process.env.LM_ADDRESS || '0x0000000000000000000000000000000000000000',
|
||||
startBlock: 26038614,
|
||||
},
|
||||
},
|
||||
|
|
@ -85,6 +89,12 @@ export default createConfig({
|
|||
address: selectedNetwork.contracts.stake as `0x${string}`,
|
||||
startBlock: selectedNetwork.contracts.startBlock,
|
||||
},
|
||||
LiquidityManager: {
|
||||
abi: LiquidityManagerAbi satisfies Abi,
|
||||
chain: NETWORK,
|
||||
address: selectedNetwork.contracts.liquidityManager as `0x${string}`,
|
||||
startBlock: selectedNetwork.contracts.startBlock,
|
||||
},
|
||||
},
|
||||
blocks: {
|
||||
StatsBlock: {
|
||||
|
|
|
|||
|
|
@ -133,6 +133,72 @@ export const stats = onchainTable('stats', t => ({
|
|||
.$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(),
|
||||
}));
|
||||
|
||||
// ETH reserve history - tracks ethBalance over time for 7d growth calculation
|
||||
export const ethReserveHistory = onchainTable('ethReserveHistory', t => ({
|
||||
id: t.text().primaryKey(), // block_logIndex format
|
||||
timestamp: t.bigint().notNull(),
|
||||
ethBalance: t.bigint().notNull(),
|
||||
}));
|
||||
|
||||
// Fee history - tracks fees earned over time for 7d totals
|
||||
export const feeHistory = onchainTable('feeHistory', t => ({
|
||||
id: t.text().primaryKey(), // block_logIndex format
|
||||
timestamp: t.bigint().notNull(),
|
||||
ethFees: t.bigint().notNull(),
|
||||
krkFees: t.bigint().notNull(),
|
||||
}));
|
||||
|
||||
// Individual staking positions
|
||||
|
|
@ -180,6 +246,29 @@ export const positions = onchainTable(
|
|||
// 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
|
||||
export const holders = onchainTable(
|
||||
'holders',
|
||||
t => ({
|
||||
address: t.hex().primaryKey(),
|
||||
balance: t.bigint().notNull(),
|
||||
}),
|
||||
table => ({
|
||||
addressIdx: index().on(table.address),
|
||||
})
|
||||
);
|
||||
|
||||
// Helper constants
|
||||
export const STATS_ID = '0x01';
|
||||
export const SECONDS_IN_HOUR = 3600;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Import all event handlers
|
||||
import './kraiken';
|
||||
import './stake';
|
||||
import './lm';
|
||||
|
||||
// This file serves as the entry point for all indexing functions
|
||||
// Ponder will automatically register all event handlers from imported files
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { ponder } from 'ponder:registry';
|
||||
import { stats, STATS_ID } from 'ponder:schema';
|
||||
import { stats, holders, STATS_ID } from 'ponder:schema';
|
||||
import {
|
||||
ensureStatsExists,
|
||||
parseRingBuffer,
|
||||
|
|
@ -27,34 +27,114 @@ ponder.on('Kraiken:Transfer', async ({ event, context }) => {
|
|||
|
||||
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) {
|
||||
context.log.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) {
|
||||
context.log.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;
|
||||
|
||||
if (toHolder) {
|
||||
await context.db.update(holders, { address: to }).set({
|
||||
balance: newBalance,
|
||||
});
|
||||
// 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,
|
||||
});
|
||||
holderCountDelta += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
context.log.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) {
|
||||
const statsData = await context.db.find(stats, { id: STATS_ID });
|
||||
if (statsData) {
|
||||
await context.db.update(stats, { id: STATS_ID }).set({
|
||||
kraikenTotalSupply: statsData.kraikenTotalSupply + value,
|
||||
totalMinted: statsData.totalMinted + value,
|
||||
});
|
||||
}
|
||||
await context.db.update(stats, { id: STATS_ID }).set({
|
||||
kraikenTotalSupply: statsData.kraikenTotalSupply + value,
|
||||
totalMinted: statsData.totalMinted + value,
|
||||
});
|
||||
} else if (to === ZERO_ADDRESS) {
|
||||
const statsData = await context.db.find(stats, { id: STATS_ID });
|
||||
if (statsData) {
|
||||
await context.db.update(stats, { id: STATS_ID }).set({
|
||||
kraikenTotalSupply: statsData.kraikenTotalSupply - value,
|
||||
totalBurned: statsData.totalBurned + value,
|
||||
});
|
||||
}
|
||||
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 statsData = await context.db.find(stats, { id: STATS_ID });
|
||||
if (!statsData) return;
|
||||
|
||||
const ringBuffer = parseRingBuffer(statsData.ringBuffer as string[]);
|
||||
const pointer = statsData.ringBufferPointer ?? 0;
|
||||
const baseIndex = pointer * RING_BUFFER_SEGMENTS;
|
||||
|
|
|
|||
251
services/ponder/src/lm.ts
Normal file
251
services/ponder/src/lm.ts
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
import { ponder } from 'ponder:registry';
|
||||
import { recenters, stats, STATS_ID, ethReserveHistory } from 'ponder:schema';
|
||||
import { ensureStatsExists } from './helpers/stats';
|
||||
import { gte, asc } from 'drizzle-orm';
|
||||
|
||||
const SECONDS_IN_7_DAYS = 7n * 24n * 60n * 60n;
|
||||
|
||||
/**
|
||||
* Fee tracking approach:
|
||||
*
|
||||
* Option 1 (not implemented): Index Uniswap V3 Pool Collect events
|
||||
* - Pros: Accurate fee data directly from the pool
|
||||
* - Cons: Requires adding pool contract to ponder.config.ts, forcing a full resync
|
||||
*
|
||||
* Option 2 (not implemented): Derive from ETH balance changes
|
||||
* - Pros: No config changes needed
|
||||
* - Cons: Less accurate, hard to isolate fees from other balance changes
|
||||
*
|
||||
* Current: Fee tracking infrastructure (feeHistory table, stats fields) is in place
|
||||
* but not populated. To implement:
|
||||
* 1. Add UniswapV3Pool contract to ponder.config.ts with Collect event
|
||||
* 2. Handle Collect events to populate feeHistory table
|
||||
* 3. Calculate 7-day rolling totals from feeHistory
|
||||
*
|
||||
* The feesEarned7dEth and feesEarned7dKrk fields default to 0n until implemented.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Calculate price in wei per KRK token from a Uniswap V3 tick
|
||||
* For WETH/KRK pool where WETH is token0:
|
||||
* - price = amount1/amount0 = 1.0001^tick
|
||||
* - This gives KRK per WETH
|
||||
* - We want wei per KRK, so we invert and scale
|
||||
*/
|
||||
function priceFromTick(tick: number): bigint {
|
||||
// Calculate 1.0001^tick using floating point
|
||||
const price = Math.pow(1.0001, tick);
|
||||
|
||||
// Price is KRK/WETH, we want WEI per KRK
|
||||
// Since both tokens have 18 decimals, we need to invert
|
||||
// priceWei = (10^18) / price
|
||||
const priceWei = 10 ** 18 / price;
|
||||
|
||||
return BigInt(Math.floor(priceWei));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate basis points difference between two values
|
||||
* bps = (new - old) / old * 10000
|
||||
*/
|
||||
function calculateBps(newValue: bigint, oldValue: bigint): number {
|
||||
if (oldValue === 0n) return 0;
|
||||
const diff = newValue - oldValue;
|
||||
const bps = (Number(diff) * 10000) / Number(oldValue);
|
||||
return Math.floor(bps);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle LiquidityManager Recentered events
|
||||
* Creates a new recenter record and updates stats.
|
||||
* NOTE: Recenter day/week counts are simple incrementing counters.
|
||||
* For accurate rolling windows, the API layer can query the recenters table directly.
|
||||
*/
|
||||
ponder.on('LiquidityManager:Recentered', async ({ event, context }) => {
|
||||
await ensureStatsExists(context, event.block.timestamp);
|
||||
|
||||
const { currentTick, isUp } = event.args;
|
||||
const recenterId = `${event.block.number}_${event.log.logIndex}`;
|
||||
|
||||
// Insert recenter record (ethBalance populated below after read)
|
||||
await context.db.insert(recenters).values({
|
||||
id: recenterId,
|
||||
timestamp: event.block.timestamp,
|
||||
currentTick: Number(currentTick),
|
||||
isUp,
|
||||
ethBalance: null,
|
||||
outstandingSupply: null,
|
||||
vwapTick: null,
|
||||
});
|
||||
|
||||
// Update stats — increment counters (simple approach; API can do accurate rolling queries)
|
||||
const statsData = await context.db.find(stats, { id: STATS_ID });
|
||||
if (!statsData) return;
|
||||
|
||||
await context.db.update(stats, { id: STATS_ID }).set({
|
||||
lastRecenterTimestamp: event.block.timestamp,
|
||||
lastRecenterTick: Number(currentTick),
|
||||
recentersLastDay: (statsData.recentersLastDay ?? 0) + 1,
|
||||
recentersLastWeek: (statsData.recentersLastWeek ?? 0) + 1,
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Handle LiquidityManager EthScarcity events
|
||||
* Updates the most recent recenter record with ETH reserve and VWAP data
|
||||
* FIXED: Search for matching recenter by block + tick instead of assuming logIndex - 1
|
||||
*/
|
||||
ponder.on('LiquidityManager:EthScarcity', async ({ event, context }) => {
|
||||
const { currentTick, ethBalance, outstandingSupply, vwapTick } = event.args;
|
||||
|
||||
// Strategy: Try logIndex-1 first (common case), then search by block+tick (fallback)
|
||||
// This handles both the common case efficiently and edge cases correctly
|
||||
let recenterId = `${event.block.number}_${event.log.logIndex - 1}`;
|
||||
let recenter = await context.db.find(recenters, { id: recenterId });
|
||||
|
||||
// If logIndex-1 didn't work, search for matching recenter in same block by tick
|
||||
if (!recenter) {
|
||||
context.log.warn(`EthScarcity: logIndex-1 failed for block ${event.block.number}. Searching by tick ${currentTick}...`);
|
||||
|
||||
// Fallback: scan recent recenters from this block with matching tick
|
||||
// Build candidate IDs to check (scan backwards from current logIndex)
|
||||
for (let offset = 2; offset <= 10 && offset <= event.log.logIndex; offset++) {
|
||||
const candidateId = `${event.block.number}_${event.log.logIndex - offset}`;
|
||||
const candidate = await context.db.find(recenters, { id: candidateId });
|
||||
if (candidate && candidate.currentTick === Number(currentTick)) {
|
||||
recenter = candidate;
|
||||
recenterId = candidateId;
|
||||
context.log.info(`EthScarcity: Found matching recenter at offset -${offset}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (recenter) {
|
||||
await context.db.update(recenters, { id: recenterId }).set({
|
||||
ethBalance,
|
||||
outstandingSupply,
|
||||
vwapTick: Number(vwapTick),
|
||||
});
|
||||
} else {
|
||||
context.log.error(
|
||||
`EthScarcity: No matching Recentered event found for block ${event.block.number}, tick ${currentTick}, logIndex ${event.log.logIndex}`
|
||||
);
|
||||
}
|
||||
|
||||
// Record ETH reserve to history for 7d growth tracking
|
||||
const historyId = `${event.block.number}_${event.log.logIndex}`;
|
||||
await context.db.insert(ethReserveHistory).values({
|
||||
id: historyId,
|
||||
timestamp: event.block.timestamp,
|
||||
ethBalance,
|
||||
});
|
||||
|
||||
// Update stats with reserve data, floor price, and 7d growth
|
||||
await updateReserveStats(context, event, ethBalance, currentTick, vwapTick);
|
||||
});
|
||||
|
||||
/**
|
||||
* Handle LiquidityManager EthAbundance events
|
||||
* Updates the most recent recenter record with ETH reserve and VWAP data
|
||||
* FIXED: Search for matching recenter by block + tick instead of assuming logIndex - 1
|
||||
*/
|
||||
ponder.on('LiquidityManager:EthAbundance', async ({ event, context }) => {
|
||||
const { currentTick, ethBalance, outstandingSupply, vwapTick } = event.args;
|
||||
|
||||
// Strategy: Try logIndex-1 first (common case), then search by block+tick (fallback)
|
||||
// This handles both the common case efficiently and edge cases correctly
|
||||
let recenterId = `${event.block.number}_${event.log.logIndex - 1}`;
|
||||
let recenter = await context.db.find(recenters, { id: recenterId });
|
||||
|
||||
// If logIndex-1 didn't work, search for matching recenter in same block by tick
|
||||
if (!recenter) {
|
||||
context.log.warn(`EthAbundance: logIndex-1 failed for block ${event.block.number}. Searching by tick ${currentTick}...`);
|
||||
|
||||
// Fallback: scan recent recenters from this block with matching tick
|
||||
// Build candidate IDs to check (scan backwards from current logIndex)
|
||||
for (let offset = 2; offset <= 10 && offset <= event.log.logIndex; offset++) {
|
||||
const candidateId = `${event.block.number}_${event.log.logIndex - offset}`;
|
||||
const candidate = await context.db.find(recenters, { id: candidateId });
|
||||
if (candidate && candidate.currentTick === Number(currentTick)) {
|
||||
recenter = candidate;
|
||||
recenterId = candidateId;
|
||||
context.log.info(`EthAbundance: Found matching recenter at offset -${offset}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (recenter) {
|
||||
await context.db.update(recenters, { id: recenterId }).set({
|
||||
ethBalance,
|
||||
outstandingSupply,
|
||||
vwapTick: Number(vwapTick),
|
||||
});
|
||||
} else {
|
||||
context.log.error(
|
||||
`EthAbundance: No matching Recentered event found for block ${event.block.number}, tick ${currentTick}, logIndex ${event.log.logIndex}`
|
||||
);
|
||||
}
|
||||
|
||||
// Update stats with reserve data, floor price, and 7d growth
|
||||
await updateReserveStats(context, event, ethBalance, currentTick, vwapTick);
|
||||
});
|
||||
|
||||
/**
|
||||
* Shared logic for EthScarcity and EthAbundance handlers:
|
||||
* Records ETH reserve history, calculates 7d growth, floor price, and updates stats.
|
||||
*/
|
||||
async function updateReserveStats(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
context: { db: any; log: any },
|
||||
event: { block: { number: bigint; timestamp: bigint }; log: { logIndex: number } },
|
||||
ethBalance: bigint,
|
||||
currentTick: number | bigint,
|
||||
vwapTick: number | bigint
|
||||
) {
|
||||
// Record ETH reserve to history for 7d growth tracking
|
||||
const historyId = `${event.block.number}_${event.log.logIndex}`;
|
||||
await context.db.insert(ethReserveHistory).values({
|
||||
id: historyId,
|
||||
timestamp: event.block.timestamp,
|
||||
ethBalance,
|
||||
});
|
||||
|
||||
// Look back 7 days for growth calculation using raw Drizzle query
|
||||
const sevenDaysAgo = event.block.timestamp - SECONDS_IN_7_DAYS;
|
||||
const oldReserves = await context.db.sql
|
||||
.select()
|
||||
.from(ethReserveHistory)
|
||||
.where(gte(ethReserveHistory.timestamp, sevenDaysAgo))
|
||||
.orderBy(asc(ethReserveHistory.timestamp))
|
||||
.limit(1);
|
||||
|
||||
let ethReserve7dAgo: bigint | null = null;
|
||||
let ethReserveGrowthBps: number | null = null;
|
||||
|
||||
if (oldReserves.length > 0 && oldReserves[0]) {
|
||||
ethReserve7dAgo = oldReserves[0].ethBalance;
|
||||
ethReserveGrowthBps = calculateBps(ethBalance, ethReserve7dAgo);
|
||||
}
|
||||
|
||||
// Calculate floor price (from vwapTick) and current price (from currentTick)
|
||||
const floorTick = Number(vwapTick);
|
||||
const floorPriceWei = priceFromTick(floorTick);
|
||||
const currentPriceWei = priceFromTick(Number(currentTick));
|
||||
|
||||
// Calculate distance from floor in basis points
|
||||
const floorDistanceBps = calculateBps(currentPriceWei, floorPriceWei);
|
||||
|
||||
// Update stats with all metrics
|
||||
await context.db.update(stats, { id: STATS_ID }).set({
|
||||
lastEthReserve: ethBalance,
|
||||
lastVwapTick: Number(vwapTick),
|
||||
ethReserve7dAgo,
|
||||
ethReserveGrowthBps,
|
||||
floorTick,
|
||||
floorPriceWei,
|
||||
currentPriceWei,
|
||||
floorDistanceBps,
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue