// On-chain state sweeps + USD pricing. // // State per pool (multicall): // univ3: slot0 + liquidity + erc20 balanceOf(token0/1) — balances (not L) // are the TVL basis, matching how GT/dexscreener report "reserve". // univ2: getReserves + totalSupply. // // Pricing is a waterfall: GeckoTerminal token prices are ground truth while // fresh (stats.ts seeds them, depth = pool reserve/2); everything else comes // from anchor propagation — a token gets priced through the deepest pool that // pairs it against an already-priced token, requiring ≥ TUNE.minDepthUsd of // priced-side depth so dust pools can't set prices. TVL then = sum of priced // sides (single-priced-side pools: 2× that side, flagged approximate). import { erc20Abi, formatUnits } from 'viem' import { uniV2PairAbi, uniV3PoolAbi } from '../src/abi' import { ADDR, TUNE, log, now } from './config' import { mc, ok, type Call } from './rpc' import { allTokens, db, missingMetaTokens, setTokenPrice, setTvl, tx, upsertState, upsertTokenMeta, type PoolRow, } from './store' const printable = (s: unknown): string | null => { if (typeof s !== 'string') return null const t = s.replace(/[^\x20-\x7e]/g, '').trim() return t ? t.slice(0, 24) : null } /** fetch symbol/decimals for catalog tokens we haven't met yet (10k/slice) */ export async function ensureTokenMeta(): Promise { const all = missingMetaTokens() for (let i = 0; i < all.length; i += 10_000) { const missing = all.slice(i, i + 10_000) const res = await mc( missing.flatMap((t) => [ { abi: erc20Abi, address: t as `0x${string}`, functionName: 'symbol' }, { abi: erc20Abi, address: t as `0x${string}`, functionName: 'decimals' }, ]), ) tx(() => { missing.forEach((t, j) => { const sym = printable(ok(res[j * 2])) const dec = ok(res[j * 2 + 1]) upsertTokenMeta(t, sym ?? t.slice(0, 6) + '…', dec ?? 18, sym !== null && dec !== undefined) }) }) } return all.length } const poolRowsQ = (addrs: string[]): PoolRow[] => { const out: PoolRow[] = [] const q = db.prepare('SELECT address, proto, token0, token1, fee_ppm, tick_spacing FROM pools WHERE address = ?') for (const a of addrs) { const r = q.get(a.toLowerCase()) as PoolRow | undefined if (r) out.push(r) } return out } /** refresh raw on-chain state for the given pools (memory-bounded slices) */ export async function sweepState(addrs: string[]): Promise { let done = 0 for (let i = 0; i < addrs.length; i += 5_000) { done += await sweepSlice(addrs.slice(i, i + 5_000)) } return done } async function sweepSlice(addrs: string[]): Promise { if (!addrs.length) return 0 const rows = poolRowsQ(addrs) const calls: Call[] = [] for (const p of rows) { const a = p.address as `0x${string}` if (p.proto === 'univ3') calls.push( { abi: uniV3PoolAbi, address: a, functionName: 'slot0' }, { abi: uniV3PoolAbi, address: a, functionName: 'liquidity' }, { abi: erc20Abi, address: p.token0 as `0x${string}`, functionName: 'balanceOf', args: [a] }, { abi: erc20Abi, address: p.token1 as `0x${string}`, functionName: 'balanceOf', args: [a] }, ) else calls.push( { abi: uniV2PairAbi, address: a, functionName: 'getReserves' }, { abi: uniV2PairAbi, address: a, functionName: 'totalSupply' }, ) } const res = await mc(calls) let i = 0 tx(() => { for (const p of rows) { if (p.proto === 'univ3') { const s0 = ok(res[i++]) const liq = ok(res[i++]) const b0 = ok(res[i++]) const b1 = ok(res[i++]) if (!s0) continue upsertState(p.address, { sqrtPrice: s0[0], tick: s0[1], liquidity: liq ?? 0n, reserve0: b0 ?? 0n, reserve1: b1 ?? 0n, }) } else { const rs = ok(res[i++]) const ts = ok(res[i++]) if (!rs) continue upsertState(p.address, { reserve0: rs[0], reserve1: rs[1], totalSupply: ts ?? 0n }) } } }) return rows.length } type PriceEntry = { usd: number; depth: number; src: string; updated: number } const loadPrices = (): Map => { const m = new Map() for (const t of allTokens()) if (t.price_usd != null && t.price_usd > 0) m.set(t.address, { usd: t.price_usd, depth: t.price_depth_usd, src: t.price_src ?? '?', updated: t.price_updated ?? 0 }) return m } type StateRow = { address: string proto: string token0: string token1: string reserve0: string reserve1: string } const statesQ = () => db .prepare( `SELECT p.address, p.proto, p.token0, p.token1, s.reserve0, s.reserve1 FROM pools p JOIN pool_state s ON s.address = p.address`, ) .all() as StateRow[] /** * Full pricing pass: propagate USD prices from GT/anchor seeds through pools, * then recompute every pool's TVL. Pure JS over in-memory rows (~35k pools), * runs after full sweeps and after each GT cycle. */ export function reprice(): { priced: number; tvlPools: number } { const decs = new Map(allTokens().map((t) => [t.address, t.decimals])) const prices = loadPrices() // bootstrap anchor before the first GT cycle: USDG ≈ $1 (GT overwrites it) if (!prices.has(ADDR.USDG.toLowerCase())) prices.set(ADDR.USDG.toLowerCase(), { usd: 1, depth: 1, src: 'anchor', updated: now() }) const states = statesQ() const human = (raw: string, addr: string) => Number(formatUnits(BigInt(raw), decs.get(addr) ?? 18)) const gtFresh = (e: PriceEntry) => e.src === 'gt' && now() - e.updated < TUNE.gtFreshSecs const dirty = new Map() for (let round = 0; round < 3; round++) { let changed = 0 for (const s of states) { const b0 = human(s.reserve0, s.token0) const b1 = human(s.reserve1, s.token1) for (const [known, other, kb, ob] of [ [s.token0, s.token1, b0, b1], [s.token1, s.token0, b1, b0], ] as const) { const kp = prices.get(known) if (!kp || ob <= 0) continue const depth = kb * kp.usd if (depth < TUNE.minDepthUsd) continue const existing = prices.get(other) if (existing && (gtFresh(existing) || existing.depth >= depth)) continue const e: PriceEntry = { usd: depth / ob, depth, src: 'pool', updated: now() } prices.set(other, e) dirty.set(other, e) changed++ } } if (!changed) break } let tvlPools = 0 tx(() => { for (const [addr, e] of dirty) setTokenPrice(addr, e.usd, e.depth, e.src) for (const s of states) { const p0 = prices.get(s.token0) const p1 = prices.get(s.token1) const u0 = p0 ? human(s.reserve0, s.token0) * p0.usd : null const u1 = p1 ? human(s.reserve1, s.token1) * p1.usd : null const tvl = u0 != null && u1 != null ? u0 + u1 : u0 != null ? u0 * 2 : u1 != null ? u1 * 2 : null setTvl(s.address, tvl, tvl != null && (u0 == null || u1 == null)) if (tvl != null) tvlPools++ } }) return { priced: prices.size, tvlPools } } /** cheap TVL refresh for a few pools using already-stored prices (no propagation) */ export function computeTvlFor(addrs: string[]): void { if (!addrs.length) return const decs = new Map(allTokens().map((t) => [t.address, t.decimals])) const prices = loadPrices() const q = db.prepare( `SELECT p.address, p.proto, p.token0, p.token1, s.reserve0, s.reserve1 FROM pools p JOIN pool_state s ON s.address = p.address WHERE p.address = ?`, ) tx(() => { for (const a of addrs) { const s = q.get(a.toLowerCase()) as StateRow | undefined if (!s) continue const human = (raw: string, addr: string) => Number(formatUnits(BigInt(raw), decs.get(addr) ?? 18)) const p0 = prices.get(s.token0) const p1 = prices.get(s.token1) const u0 = p0 ? human(s.reserve0, s.token0) * p0.usd : null const u1 = p1 ? human(s.reserve1, s.token1) * p1.usd : null const tvl = u0 != null && u1 != null ? u0 + u1 : u0 != null ? u0 * 2 : u1 != null ? u1 * 2 : null setTvl(s.address, tvl, tvl != null && (u0 == null || u1 == null)) } }) } export const sweepLog = (label: string, n: number, ms: number) => log(`[sweep] ${label} ${n} pools in ${(ms / 1000).toFixed(1)}s`)