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>
This commit is contained in:
parent
4206f3bc63
commit
3fceb4145a
5 changed files with 179 additions and 113 deletions
|
|
@ -5,6 +5,9 @@
|
|||
<div class="stat-label">ETH Reserve</div>
|
||||
<div class="stat-value">{{ ethReserveAmount }}</div>
|
||||
<div v-if="growthIndicator !== null" class="growth-badge" :class="growthClass">{{ growthIndicator }}</div>
|
||||
<svg v-if="ethReserveSpark.length > 1" class="sparkline" viewBox="0 0 80 24" preserveAspectRatio="none">
|
||||
<polyline :points="toSvgPoints(ethReserveSpark)" fill="none" stroke="rgba(96,165,250,0.5)" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">ETH / Token</div>
|
||||
|
|
@ -19,10 +22,17 @@
|
|||
<div class="stat-label">Supply (7d)</div>
|
||||
<div class="stat-value">{{ totalSupply }}</div>
|
||||
<div v-if="netSupplyIndicator !== null" class="growth-badge" :class="netSupplyClass">{{ netSupplyIndicator }}</div>
|
||||
<svg v-if="supplySpark.length > 1" class="sparkline" viewBox="0 0 80 24" preserveAspectRatio="none">
|
||||
<polyline :points="toSvgPoints(supplySpark)" fill="none" stroke="rgba(74,222,128,0.5)" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<div class="stat-label">Holders</div>
|
||||
<div class="stat-value">{{ holders }}</div>
|
||||
<div v-if="holderGrowthIndicator !== null" class="growth-badge" :class="holderGrowthClass">{{ holderGrowthIndicator }}</div>
|
||||
<svg v-if="holdersSpark.length > 1" class="sparkline" viewBox="0 0 80 24" preserveAspectRatio="none">
|
||||
<polyline :points="toSvgPoints(holdersSpark)" fill="none" stroke="rgba(251,191,36,0.5)" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="round" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-item" :class="{ 'pulse': isRecentRebalance }">
|
||||
<div class="stat-label">Rebalances</div>
|
||||
|
|
@ -44,22 +54,25 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
||||
|
||||
const RING_SEGMENTS = 4; // ethReserve, minted, burned, holderCount
|
||||
const RING_HOURS = 168; // 7 days * 24 hours
|
||||
|
||||
interface Stats {
|
||||
kraikenTotalSupply: string;
|
||||
holderCount: number;
|
||||
lastRecenterTimestamp: number;
|
||||
recentersLastWeek: number;
|
||||
lastEthReserve: string;
|
||||
taxPaidLastWeek: string;
|
||||
mintedLastWeek: string;
|
||||
burnedLastWeek: string;
|
||||
netSupplyChangeWeek: string;
|
||||
// New fields (batch1) — all nullable until indexer has sufficient history
|
||||
ethReserveGrowthBps: number | null;
|
||||
feesEarned7dEth: string | null;
|
||||
floorPriceWei: string | null;
|
||||
floorDistanceBps: number | null;
|
||||
currentPriceWei: string | null;
|
||||
ringBuffer: string[] | null;
|
||||
ringBufferPointer: number | null;
|
||||
}
|
||||
|
||||
const stats = ref<Stats | null>(null);
|
||||
|
|
@ -76,6 +89,101 @@ function weiToEth(wei: string | null | undefined): number {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a time-ordered series from the ring buffer for a given slot offset.
|
||||
* Skips leading zeros (pre-launch padding).
|
||||
*/
|
||||
function extractSeries(ringBuffer: string[], pointer: number, slotOffset: number): number[] {
|
||||
const raw: number[] = [];
|
||||
for (let i = 0; i < RING_HOURS; i++) {
|
||||
// Walk from oldest to newest
|
||||
const idx = ((pointer + 1 + i) % RING_HOURS) * RING_SEGMENTS + slotOffset;
|
||||
raw.push(Number(ringBuffer[idx] || '0'));
|
||||
}
|
||||
// Skip leading zeros
|
||||
const firstNonZero = raw.findIndex(v => v > 0);
|
||||
return firstNonZero === -1 ? [] : raw.slice(firstNonZero);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build cumulative net supply series from minted (slot 1) and burned (slot 2).
|
||||
*/
|
||||
function extractSupplySeries(ringBuffer: string[], pointer: number): number[] {
|
||||
const minted: number[] = [];
|
||||
const burned: number[] = [];
|
||||
for (let i = 0; i < RING_HOURS; i++) {
|
||||
const idx = ((pointer + 1 + i) % RING_HOURS) * RING_SEGMENTS;
|
||||
minted.push(Number(ringBuffer[idx + 1] || '0'));
|
||||
burned.push(Number(ringBuffer[idx + 2] || '0'));
|
||||
}
|
||||
// Build cumulative net supply change
|
||||
const cumulative: number[] = [];
|
||||
let sum = 0;
|
||||
let hasData = false;
|
||||
for (let i = 0; i < RING_HOURS; i++) {
|
||||
const net = minted[i] - burned[i];
|
||||
if (net !== 0) hasData = true;
|
||||
sum += net;
|
||||
if (hasData) cumulative.push(sum);
|
||||
}
|
||||
return cumulative;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a number[] series to SVG polyline points string, scaled to 80x24.
|
||||
*/
|
||||
function toSvgPoints(series: number[]): string {
|
||||
if (series.length < 2) return '';
|
||||
const min = Math.min(...series);
|
||||
const max = Math.max(...series);
|
||||
const range = max - min || 1;
|
||||
return series
|
||||
.map((v, i) => {
|
||||
const x = (i / (series.length - 1)) * 80;
|
||||
const y = 24 - ((v - min) / range) * 22 - 1; // 1px padding top/bottom
|
||||
return `${x.toFixed(1)},${y.toFixed(1)}`;
|
||||
})
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
// Sparkline series extracted from ring buffer
|
||||
const ethReserveSpark = computed(() => {
|
||||
if (!stats.value?.ringBuffer || stats.value.ringBufferPointer == null) return [];
|
||||
return extractSeries(stats.value.ringBuffer, stats.value.ringBufferPointer, 0);
|
||||
});
|
||||
|
||||
const supplySpark = computed(() => {
|
||||
if (!stats.value?.ringBuffer || stats.value.ringBufferPointer == null) return [];
|
||||
return extractSupplySeries(stats.value.ringBuffer, stats.value.ringBufferPointer);
|
||||
});
|
||||
|
||||
const holdersSpark = computed(() => {
|
||||
if (!stats.value?.ringBuffer || stats.value.ringBufferPointer == null) return [];
|
||||
return extractSeries(stats.value.ringBuffer, stats.value.ringBufferPointer, 3);
|
||||
});
|
||||
|
||||
// Holder growth indicator from ring buffer
|
||||
const holderGrowthIndicator = computed((): string | null => {
|
||||
const series = holdersSpark.value;
|
||||
if (series.length < 2) return null;
|
||||
const oldest = series[0];
|
||||
const newest = series[series.length - 1];
|
||||
if (oldest === 0) return null;
|
||||
const pct = ((newest - oldest) / oldest) * 100;
|
||||
if (Math.abs(pct) < 0.1) return '~ flat';
|
||||
return pct > 0 ? `↑ ${pct.toFixed(1)}% this week` : `↓ ${Math.abs(pct).toFixed(1)}% this week`;
|
||||
});
|
||||
|
||||
const holderGrowthClass = computed(() => {
|
||||
const series = holdersSpark.value;
|
||||
if (series.length < 2) return '';
|
||||
const oldest = series[0];
|
||||
const newest = series[series.length - 1];
|
||||
if (newest > oldest) return 'growth-up';
|
||||
if (newest < oldest) return 'growth-down';
|
||||
return 'growth-flat';
|
||||
});
|
||||
|
||||
const ethReserveAmount = computed(() => {
|
||||
if (!stats.value) return '0.00 ETH';
|
||||
const eth = weiToEth(stats.value.lastEthReserve);
|
||||
|
|
@ -107,7 +215,6 @@ const ethPerToken = computed(() => {
|
|||
const supply = Number(stats.value.kraikenTotalSupply) / 1e18;
|
||||
if (supply === 0) return '—';
|
||||
const ratio = reserve / supply;
|
||||
// Format with appropriate precision
|
||||
if (ratio >= 0.01) return `${ratio.toFixed(4)} ETH`;
|
||||
if (ratio >= 0.0001) return `${ratio.toFixed(6)} ETH`;
|
||||
return `${ratio.toExponential(2)} ETH`;
|
||||
|
|
@ -207,7 +314,6 @@ async function fetchStats() {
|
|||
lastRecenterTimestamp
|
||||
recentersLastWeek
|
||||
lastEthReserve
|
||||
taxPaidLastWeek
|
||||
mintedLastWeek
|
||||
burnedLastWeek
|
||||
netSupplyChangeWeek
|
||||
|
|
@ -216,6 +322,8 @@ async function fetchStats() {
|
|||
floorPriceWei
|
||||
floorDistanceBps
|
||||
currentPriceWei
|
||||
ringBuffer
|
||||
ringBufferPointer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -338,6 +446,12 @@ onUnmounted(() => {
|
|||
background: rgba(255, 255, 255, 0.05)
|
||||
border-color: rgba(255, 255, 255, 0.12)
|
||||
|
||||
.sparkline
|
||||
width: 80px
|
||||
height: 24px
|
||||
margin-top: 4px
|
||||
opacity: 0.8
|
||||
|
||||
.stat-label
|
||||
font-size: 12px
|
||||
color: rgba(240, 240, 240, 0.6)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ 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, tax
|
||||
const RING_BUFFER_SEGMENTS = 4; // ethReserve, minted, burned, holderCount
|
||||
|
||||
export const stackMeta = onchainTable('stackMeta', t => ({
|
||||
id: t.text().primaryKey(),
|
||||
|
|
@ -188,21 +188,6 @@ export const stats = onchainTable('stats', t => ({
|
|||
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
|
||||
export const positions = onchainTable(
|
||||
'positions',
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ type HandlerArgs = Handler extends (...args: infer Args) => unknown ? Args[0] :
|
|||
export type StatsContext = HandlerArgs extends { context: infer C } ? C : never;
|
||||
type StatsEvent = HandlerArgs extends { event: infer E } ? E : never;
|
||||
|
||||
export const RING_BUFFER_SEGMENTS = 4; // ethReserve, minted, burned, tax
|
||||
export const RING_BUFFER_SEGMENTS = 4; // ethReserve, minted, burned, holderCount
|
||||
export const MINIMUM_BLOCKS_FOR_RINGBUFFER = 100;
|
||||
|
||||
// Get deploy block from environment (set by bootstrap)
|
||||
|
|
@ -34,46 +34,52 @@ function computeMetrics(ringBuffer: bigint[], pointer: number) {
|
|||
let mintedWeek = 0n;
|
||||
let burnedDay = 0n;
|
||||
let burnedWeek = 0n;
|
||||
let taxDay = 0n;
|
||||
let taxWeek = 0n;
|
||||
// Slot 0 now stores ETH reserve snapshots per hour (latest value, not cumulative)
|
||||
let ethReserveLatest = 0n; // Most recent non-zero snapshot
|
||||
let ethReserve24hAgo = 0n; // Snapshot from ~24h ago
|
||||
let ethReserve7dAgo = 0n; // Oldest snapshot in buffer
|
||||
// Slot 0: ETH reserve snapshots per hour (latest value, not cumulative)
|
||||
let ethReserveLatest = 0n;
|
||||
let ethReserve24hAgo = 0n;
|
||||
let ethReserve7dAgo = 0n;
|
||||
// Slot 3: holderCount snapshots per hour
|
||||
let holderCountLatest = 0n;
|
||||
let holderCount24hAgo = 0n;
|
||||
let holderCount7dAgo = 0n;
|
||||
|
||||
for (let i = 0; i < HOURS_IN_RING_BUFFER; i++) {
|
||||
const baseIndex = ((pointer - i + HOURS_IN_RING_BUFFER) % HOURS_IN_RING_BUFFER) * RING_BUFFER_SEGMENTS;
|
||||
const ethReserve = ringBuffer[baseIndex + 0];
|
||||
const minted = ringBuffer[baseIndex + 1];
|
||||
const burned = ringBuffer[baseIndex + 2];
|
||||
const tax = ringBuffer[baseIndex + 3];
|
||||
const holderCount = ringBuffer[baseIndex + 3];
|
||||
|
||||
// Track ETH reserve at key points
|
||||
if (i === 0 && ethReserve > 0n) ethReserveLatest = ethReserve;
|
||||
if (i === 23 && ethReserve > 0n) ethReserve24hAgo = ethReserve;
|
||||
if (ethReserve > 0n) ethReserve7dAgo = ethReserve; // Last non-zero = oldest
|
||||
|
||||
// Track holder count at key points
|
||||
if (i === 0 && holderCount > 0n) holderCountLatest = holderCount;
|
||||
if (i === 23 && holderCount > 0n) holderCount24hAgo = holderCount;
|
||||
if (holderCount > 0n) holderCount7dAgo = holderCount; // Last non-zero = oldest
|
||||
|
||||
if (i < 24) {
|
||||
mintedDay += minted;
|
||||
burnedDay += burned;
|
||||
taxDay += tax;
|
||||
}
|
||||
|
||||
mintedWeek += minted;
|
||||
burnedWeek += burned;
|
||||
taxWeek += tax;
|
||||
}
|
||||
|
||||
return {
|
||||
ethReserveLatest,
|
||||
ethReserve24hAgo,
|
||||
ethReserve7dAgo,
|
||||
holderCountLatest,
|
||||
holderCount24hAgo,
|
||||
holderCount7dAgo,
|
||||
mintedDay,
|
||||
mintedWeek,
|
||||
burnedDay,
|
||||
burnedWeek,
|
||||
taxDay,
|
||||
taxWeek,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -95,12 +101,10 @@ function computeProjections(ringBuffer: bigint[], pointer: number, timestamp: bi
|
|||
|
||||
const mintProjection = project(ringBuffer[currentBase + 1], ringBuffer[previousBase + 1], metrics.mintedWeek);
|
||||
const burnProjection = project(ringBuffer[currentBase + 2], ringBuffer[previousBase + 2], metrics.burnedWeek);
|
||||
const taxProjection = project(ringBuffer[currentBase + 3], ringBuffer[previousBase + 3], metrics.taxWeek);
|
||||
|
||||
return {
|
||||
mintProjection,
|
||||
burnProjection,
|
||||
taxProjection,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -211,6 +215,11 @@ export async function updateHourlyData(context: StatsContext, timestamp: bigint)
|
|||
let pointer = statsData.ringBufferPointer ?? 0;
|
||||
const lastUpdate = statsData.lastHourlyUpdateTimestamp ?? 0n;
|
||||
|
||||
// Snapshot current holderCount into ring buffer slot 3
|
||||
const currentHolderCount = BigInt(statsData.holderCount ?? 0);
|
||||
const base = pointer * RING_BUFFER_SEGMENTS;
|
||||
ringBuffer[base + 3] = currentHolderCount;
|
||||
|
||||
if (lastUpdate === 0n) {
|
||||
await context.db.update(stats, { id: STATS_ID }).set({
|
||||
lastHourlyUpdateTimestamp: currentHour,
|
||||
|
|
@ -225,11 +234,11 @@ export async function updateHourlyData(context: StatsContext, timestamp: bigint)
|
|||
|
||||
for (let h = 0; h < hoursElapsed; h++) {
|
||||
pointer = (pointer + 1) % HOURS_IN_RING_BUFFER;
|
||||
const base = pointer * RING_BUFFER_SEGMENTS;
|
||||
ringBuffer[base + 0] = 0n;
|
||||
ringBuffer[base + 1] = 0n;
|
||||
ringBuffer[base + 2] = 0n;
|
||||
ringBuffer[base + 3] = 0n;
|
||||
const newBase = pointer * RING_BUFFER_SEGMENTS;
|
||||
ringBuffer[newBase + 0] = 0n;
|
||||
ringBuffer[newBase + 1] = 0n;
|
||||
ringBuffer[newBase + 2] = 0n;
|
||||
ringBuffer[newBase + 3] = currentHolderCount; // Carry forward current holderCount
|
||||
}
|
||||
|
||||
const metrics = computeMetrics(ringBuffer, pointer);
|
||||
|
|
@ -242,15 +251,12 @@ export async function updateHourlyData(context: StatsContext, timestamp: bigint)
|
|||
mintedLastWeek: metrics.mintedWeek,
|
||||
burnedLastDay: metrics.burnedDay,
|
||||
burnedLastWeek: metrics.burnedWeek,
|
||||
taxPaidLastDay: metrics.taxDay,
|
||||
taxPaidLastWeek: metrics.taxWeek,
|
||||
ethReserveLastDay: metrics.ethReserveLatest > 0n ? metrics.ethReserveLatest - metrics.ethReserve24hAgo : 0n,
|
||||
ethReserveLastWeek: metrics.ethReserveLatest > 0n ? metrics.ethReserveLatest - metrics.ethReserve7dAgo : 0n,
|
||||
netSupplyChangeDay: metrics.mintedDay - metrics.burnedDay,
|
||||
netSupplyChangeWeek: metrics.mintedWeek - metrics.burnedWeek,
|
||||
mintNextHourProjected: metrics.mintedWeek / 7n,
|
||||
burnNextHourProjected: metrics.burnedWeek / 7n,
|
||||
taxPaidNextHourProjected: metrics.taxWeek / 7n,
|
||||
});
|
||||
} else {
|
||||
const metrics = computeMetrics(ringBuffer, pointer);
|
||||
|
|
@ -262,15 +268,12 @@ export async function updateHourlyData(context: StatsContext, timestamp: bigint)
|
|||
mintedLastWeek: metrics.mintedWeek,
|
||||
burnedLastDay: metrics.burnedDay,
|
||||
burnedLastWeek: metrics.burnedWeek,
|
||||
taxPaidLastDay: metrics.taxDay,
|
||||
taxPaidLastWeek: metrics.taxWeek,
|
||||
ethReserveLastDay: metrics.ethReserveLatest > 0n ? metrics.ethReserveLatest - metrics.ethReserve24hAgo : 0n,
|
||||
ethReserveLastWeek: metrics.ethReserveLatest > 0n ? metrics.ethReserveLatest - metrics.ethReserve7dAgo : 0n,
|
||||
netSupplyChangeDay: metrics.mintedDay - metrics.burnedDay,
|
||||
netSupplyChangeWeek: metrics.mintedWeek - metrics.burnedWeek,
|
||||
mintNextHourProjected: projections.mintProjection,
|
||||
burnNextHourProjected: projections.burnProjection,
|
||||
taxPaidNextHourProjected: projections.taxProjection,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
import { ponder } from 'ponder:registry';
|
||||
import { getLogger } from './helpers/logger';
|
||||
import { recenters, stats, STATS_ID, ethReserveHistory } from 'ponder:schema';
|
||||
import { ensureStatsExists, recordEthReserveSnapshot } from './helpers/stats';
|
||||
import { gte, asc } from 'drizzle-orm';
|
||||
|
||||
const SECONDS_IN_7_DAYS = 7n * 24n * 60n * 60n;
|
||||
import { recenters, stats, STATS_ID, HOURS_IN_RING_BUFFER } from 'ponder:schema';
|
||||
import { ensureStatsExists, recordEthReserveSnapshot, parseRingBuffer, RING_BUFFER_SEGMENTS } from './helpers/stats';
|
||||
|
||||
/**
|
||||
* Fee tracking approach:
|
||||
|
|
@ -17,12 +14,6 @@ const SECONDS_IN_7_DAYS = 7n * 24n * 60n * 60n;
|
|||
* - 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.
|
||||
*/
|
||||
|
||||
|
|
@ -134,14 +125,6 @@ ponder.on('LiquidityManager:EthScarcity', async ({ event, context }) => {
|
|||
);
|
||||
}
|
||||
|
||||
// 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);
|
||||
});
|
||||
|
|
@ -195,7 +178,7 @@ ponder.on('LiquidityManager:EthAbundance', async ({ event, context }) => {
|
|||
|
||||
/**
|
||||
* Shared logic for EthScarcity and EthAbundance handlers:
|
||||
* Records ETH reserve history, calculates 7d growth, floor price, and updates stats.
|
||||
* Records ETH reserve in ring buffer, calculates 7d growth from ring buffer, floor price, and updates stats.
|
||||
*/
|
||||
async function updateReserveStats(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
|
|
@ -205,30 +188,32 @@ async function updateReserveStats(
|
|||
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);
|
||||
// Record ETH reserve in ring buffer for hourly time-series
|
||||
await recordEthReserveSnapshot(context, event.block.timestamp, ethBalance);
|
||||
|
||||
// Compute 7d growth from ring buffer (slot 0 = ethReserve snapshots)
|
||||
const statsData = await context.db.find(stats, { id: STATS_ID });
|
||||
let ethReserve7dAgo: bigint | null = null;
|
||||
let ethReserveGrowthBps: number | null = null;
|
||||
|
||||
if (oldReserves.length > 0 && oldReserves[0]) {
|
||||
ethReserve7dAgo = oldReserves[0].ethBalance;
|
||||
if (statsData) {
|
||||
const ringBuffer = parseRingBuffer(statsData.ringBuffer as string[]);
|
||||
const pointer = statsData.ringBufferPointer ?? 0;
|
||||
|
||||
// Walk backwards through ring buffer to find oldest non-zero ETH reserve
|
||||
for (let i = HOURS_IN_RING_BUFFER - 1; i >= 0; i--) {
|
||||
const baseIndex = ((pointer - i + HOURS_IN_RING_BUFFER) % HOURS_IN_RING_BUFFER) * RING_BUFFER_SEGMENTS;
|
||||
const reserve = ringBuffer[baseIndex + 0];
|
||||
if (reserve > 0n) {
|
||||
ethReserve7dAgo = reserve;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (ethReserve7dAgo && ethReserve7dAgo > 0n) {
|
||||
ethReserveGrowthBps = calculateBps(ethBalance, ethReserve7dAgo);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate floor price (from vwapTick) and current price (from currentTick)
|
||||
const floorTick = Number(vwapTick);
|
||||
|
|
@ -249,7 +234,4 @@ async function updateReserveStats(
|
|||
currentPriceWei,
|
||||
floorDistanceBps,
|
||||
});
|
||||
|
||||
// Record ETH reserve in ring buffer for hourly time-series
|
||||
await recordEthReserveSnapshot(context, event.block.timestamp, ethBalance);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,12 +4,9 @@ import {
|
|||
ensureStatsExists,
|
||||
getStakeTotalSupply,
|
||||
markPositionsUpdated,
|
||||
parseRingBuffer,
|
||||
refreshOutstandingStake,
|
||||
serializeRingBuffer,
|
||||
updateHourlyData,
|
||||
checkBlockHistorySufficient,
|
||||
RING_BUFFER_SEGMENTS,
|
||||
} from './helpers/stats';
|
||||
import type { StatsContext } from './helpers/stats';
|
||||
|
||||
|
|
@ -154,31 +151,16 @@ ponder.on('Stake:PositionTaxPaid', async ({ event, context }) => {
|
|||
lastTaxTime: event.block.timestamp,
|
||||
});
|
||||
|
||||
// Only update ringbuffer if we have sufficient block history
|
||||
// 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)) {
|
||||
const statsData = await context.db.find(stats, { id: STATS_ID });
|
||||
if (statsData) {
|
||||
const ringBuffer = parseRingBuffer(statsData.ringBuffer as string[]);
|
||||
const pointer = statsData.ringBufferPointer ?? 0;
|
||||
const baseIndex = pointer * RING_BUFFER_SEGMENTS;
|
||||
|
||||
ringBuffer[baseIndex + 3] = ringBuffer[baseIndex + 3] + event.args.taxPaid;
|
||||
|
||||
await context.db.update(stats, { id: STATS_ID }).set({
|
||||
ringBuffer: serializeRingBuffer(ringBuffer),
|
||||
totalTaxPaid: statsData.totalTaxPaid + event.args.taxPaid,
|
||||
});
|
||||
}
|
||||
|
||||
await updateHourlyData(context, event.block.timestamp);
|
||||
} else {
|
||||
// Insufficient history - update only totalTaxPaid without ringbuffer
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await refreshOutstandingStake(context);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue