mirror of
https://github.com/labrinyang/lp-terminal.git
synced 2026-07-27 21:27:43 +00:00
d73eaf873a
A price used to propagate from any pool with enough depth on the priced side, which let a thin pool with one inflated side mint a price for its other token and carry it across the graph. Depth is now measured on the side that is already credibly priced, and a pool-priced side may claim at most 100x that depth. Guard rails around it, all in one place: prices travel at most 3 hops from a GT/anchor seed, |tick| beyond 700k is a broken init rather than a market, stored prices must land in a plausibility band, and a TVL above $1B is treated as corrupt instead of as a whale. Adds a log tail so pool stats follow chain events instead of polling, a demand-gated frontpage, and a last-good fallback so a failed refresh serves the previous stat rather than a hole. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
50 lines
2.7 KiB
TypeScript
50 lines
2.7 KiB
TypeScript
// Event-driven state refresh. A pool's reserves change only when it emits an
|
|
// event, so we don't reread on a timer — we ask the chain which pools changed.
|
|
// One eth_getLogs (v2 Sync, v3 Swap/Mint/Burn, any address) since the last block
|
|
// we processed yields exactly the pools that traded; we reread only those. Cost
|
|
// tracks trading activity, not catalog size — this is what lets the 6-hourly
|
|
// census over 100k+ dust pools, and the timed hot/active sweeps, go away.
|
|
//
|
|
// Two deliberate choices:
|
|
// - reread state, don't apply the logs' own deltas: whatever the chain returns
|
|
// now is the truth, so a reorg needs no unwinding and a missed log self-heals
|
|
// on the pool's next event (the daily census is the longstop).
|
|
// - gated on the same POOLS-tab demand signal as the frontpage sweep — the only
|
|
// consumer of fresh state is a watching user, and a getLogs every 12s costs
|
|
// 75 CU. Idle → no polling. On resume we jump the cursor to head instead of
|
|
// replaying the idle backlog: freshness wants the current state, not history.
|
|
import { numberToHex, toEventSelector } from 'viem'
|
|
import { TUNE, log } from './config'
|
|
import { pc } from './rpc'
|
|
import { computeTvlFor, sweepState } from './state'
|
|
import { kvGet, kvSet, poolsHitAgeMs } from './store'
|
|
|
|
export const TOPICS = [
|
|
toEventSelector('Sync(uint112,uint112)'), // univ2 reserves (fires on every mint/burn/swap)
|
|
toEventSelector('Swap(address,address,int256,int256,uint160,uint128,int24)'), // univ3 price + liquidity
|
|
toEventSelector('Mint(address,address,int24,int24,uint128,uint256,uint256)'), // univ3 liquidity add
|
|
toEventSelector('Burn(address,int24,int24,uint128,uint256,uint256)'), // univ3 liquidity remove
|
|
]
|
|
|
|
/** reread every catalog pool that emitted a tracked event since the last run */
|
|
export async function logtail(): Promise<void> {
|
|
if (poolsHitAgeMs() > TUNE.frontpageGateMs) return // nobody watching — don't spend getLogs
|
|
const head = Number(await pc.getBlockNumber())
|
|
const cursor = Number(kvGet('logtail_block') ?? 0)
|
|
const from = cursor && head - cursor <= TUNE.logtailMaxBlocks ? cursor + 1 : head // skip idle backlog
|
|
if (from > head) return
|
|
|
|
const logs = (await pc.request({
|
|
method: 'eth_getLogs',
|
|
params: [{ fromBlock: numberToHex(from), toBlock: numberToHex(head), topics: [TOPICS] }],
|
|
})) as { address: string }[]
|
|
|
|
const dirty = [...new Set(logs.map((l) => l.address.toLowerCase()))]
|
|
if (dirty.length) {
|
|
const swept = await sweepState(dirty) // sweepState ignores addresses not in the catalog
|
|
computeTvlFor(dirty)
|
|
log(`[logtail] blocks ${from}-${head}: ${dirty.length} pools emitted, ${swept} in catalog`)
|
|
}
|
|
kvSet('logtail_block', String(head))
|
|
}
|