Files
lp-terminal_github/indexer/store.ts
T
labrinyang 4e317eff6d LP TERMINAL — terminal-style LP frontend for Robinhood Chain
Static SPA over UP33 (ve(3,3)) + the official Uniswap v2/v3 deployments:
browse and add liquidity across all three, manage CL/v2 positions, one-token
ZAP adds, aggregator vs native swaps, and LP-based limit orders. Ships with a
zero-dependency pool indexer for uniswap discovery.

Browser-wallet signing only; no key material anywhere.
2026-07-17 19:10:30 +08:00

225 lines
8.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// SQLite store (node:sqlite — built into node ≥22.13, zero dependencies).
// bigints are stored as TEXT and travel as strings through the API; REAL
// columns are display/ranking data only, never used to build transactions.
import { mkdirSync } from 'node:fs'
import { dirname } from 'node:path'
import { DatabaseSync } from 'node:sqlite'
import { DB_PATH, now } from './config'
mkdirSync(dirname(DB_PATH), { recursive: true })
export const db = new DatabaseSync(DB_PATH)
db.exec(`
PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
CREATE TABLE IF NOT EXISTS pools (
address TEXT PRIMARY KEY, -- lowercase
proto TEXT NOT NULL, -- 'univ2' | 'univ3'
token0 TEXT NOT NULL, -- lowercase
token1 TEXT NOT NULL,
fee_ppm INTEGER NOT NULL, -- univ2 fixed 3000 (0.30%)
tick_spacing INTEGER, -- univ3 only
created_block INTEGER, -- univ3 only (from PoolCreated)
pair_index INTEGER, -- univ2 only (allPairs index)
added_ts INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_pools_t0 ON pools(token0);
CREATE INDEX IF NOT EXISTS idx_pools_t1 ON pools(token1);
CREATE TABLE IF NOT EXISTS tokens (
address TEXT PRIMARY KEY,
symbol TEXT NOT NULL DEFAULT '?',
decimals INTEGER NOT NULL DEFAULT 18,
meta_ok INTEGER NOT NULL DEFAULT 0, -- 0 = symbol/decimals defaulted (call reverted)
price_usd REAL,
price_depth_usd REAL NOT NULL DEFAULT 0, -- USD depth backing the price (bigger wins)
price_src TEXT, -- 'gt' | 'pool' | 'anchor'
price_updated INTEGER
);
CREATE TABLE IF NOT EXISTS pool_state (
address TEXT PRIMARY KEY,
sqrt_price TEXT, -- univ3
tick INTEGER, -- univ3
liquidity TEXT, -- univ3 in-range L
reserve0 TEXT NOT NULL DEFAULT '0', -- univ2: reserves; univ3: erc20 balances (TVL basis)
reserve1 TEXT NOT NULL DEFAULT '0',
total_supply TEXT, -- univ2 LP supply
tvl_usd REAL,
tvl_approx INTEGER NOT NULL DEFAULT 0, -- 1 = only one side priced (tvl = 2× that side)
updated INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_state_tvl ON pool_state(tvl_usd);
CREATE TABLE IF NOT EXISTS pool_stats (
address TEXT PRIMARY KEY,
vol24h_usd REAL,
txns24h INTEGER,
liq_usd REAL, -- GT's own reserve figure (cross-check; tvl_usd is chain-derived)
source TEXT NOT NULL,
updated INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS kv (k TEXT PRIMARY KEY, v TEXT NOT NULL);
`)
// ---- kv ----
const kvGetQ = db.prepare('SELECT v FROM kv WHERE k = ?')
const kvSetQ = db.prepare('INSERT INTO kv (k, v) VALUES (?, ?) ON CONFLICT(k) DO UPDATE SET v = excluded.v')
export const kvGet = (k: string): string | undefined => (kvGetQ.get(k) as { v: string } | undefined)?.v
export const kvSet = (k: string, v: string) => void kvSetQ.run(k, v)
// ---- pools ----
const insPoolQ = db.prepare(`
INSERT OR IGNORE INTO pools (address, proto, token0, token1, fee_ppm, tick_spacing, created_block, pair_index, added_ts)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
/** returns true when the pool is new */
export function insertPool(p: {
address: string
proto: 'univ2' | 'univ3'
token0: string
token1: string
feePpm: number
tickSpacing?: number
createdBlock?: number
pairIndex?: number
}): boolean {
const r = insPoolQ.run(
p.address.toLowerCase(),
p.proto,
p.token0.toLowerCase(),
p.token1.toLowerCase(),
p.feePpm,
p.tickSpacing ?? null,
p.createdBlock ?? null,
p.pairIndex ?? null,
now(),
)
return Number(r.changes) > 0
}
export type PoolRow = {
address: string
proto: 'univ2' | 'univ3'
token0: string
token1: string
fee_ppm: number
tick_spacing: number | null
}
const poolsByAddrQ = db.prepare('SELECT address, proto, token0, token1, fee_ppm, tick_spacing FROM pools WHERE address = ?')
export const poolRow = (addr: string) => poolsByAddrQ.get(addr.toLowerCase()) as PoolRow | undefined
export const allPoolAddrs = (): string[] =>
(db.prepare('SELECT address FROM pools').all() as { address: string }[]).map((r) => r.address)
export const poolCounts = () =>
db.prepare(`SELECT proto, COUNT(*) AS n FROM pools GROUP BY proto`).all() as { proto: string; n: number }[]
// ---- tokens ----
const insTokenQ = db.prepare(`
INSERT INTO tokens (address, symbol, decimals, meta_ok) VALUES (?, ?, ?, ?)
ON CONFLICT(address) DO UPDATE SET symbol = excluded.symbol, decimals = excluded.decimals, meta_ok = excluded.meta_ok`)
export const upsertTokenMeta = (addr: string, symbol: string, decimals: number, metaOk: boolean) =>
void insTokenQ.run(addr.toLowerCase(), symbol, decimals, metaOk ? 1 : 0)
const priceQ = db.prepare(`
INSERT INTO tokens (address, price_usd, price_depth_usd, price_src, price_updated) VALUES (?, ?, ?, ?, ?)
ON CONFLICT(address) DO UPDATE SET price_usd = excluded.price_usd, price_depth_usd = excluded.price_depth_usd,
price_src = excluded.price_src, price_updated = excluded.price_updated`)
export const setTokenPrice = (addr: string, usd: number, depthUsd: number, src: string) =>
void priceQ.run(addr.toLowerCase(), usd, depthUsd, src, now())
export type TokenRow = {
address: string
symbol: string
decimals: number
meta_ok: number
price_usd: number | null
price_depth_usd: number
price_src: string | null
price_updated: number | null
}
export const allTokens = () => db.prepare('SELECT * FROM tokens').all() as TokenRow[]
export const missingMetaTokens = (): string[] =>
(
db
.prepare(
`SELECT DISTINCT u.addr FROM (SELECT token0 AS addr FROM pools UNION SELECT token1 FROM pools) u
LEFT JOIN tokens t ON t.address = u.addr WHERE t.address IS NULL`,
)
.all() as { addr: string }[]
).map((r) => r.addr)
// ---- pool_state ----
const upStateQ = db.prepare(`
INSERT INTO pool_state (address, sqrt_price, tick, liquidity, reserve0, reserve1, total_supply, updated)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(address) DO UPDATE SET sqrt_price = excluded.sqrt_price, tick = excluded.tick,
liquidity = excluded.liquidity, reserve0 = excluded.reserve0, reserve1 = excluded.reserve1,
total_supply = excluded.total_supply, updated = excluded.updated`)
export const upsertState = (
addr: string,
s: { sqrtPrice?: bigint; tick?: number; liquidity?: bigint; reserve0: bigint; reserve1: bigint; totalSupply?: bigint },
) =>
void upStateQ.run(
addr.toLowerCase(),
s.sqrtPrice !== undefined ? String(s.sqrtPrice) : null,
s.tick ?? null,
s.liquidity !== undefined ? String(s.liquidity) : null,
String(s.reserve0),
String(s.reserve1),
s.totalSupply !== undefined ? String(s.totalSupply) : null,
now(),
)
const tvlQ = db.prepare('UPDATE pool_state SET tvl_usd = ?, tvl_approx = ? WHERE address = ?')
export const setTvl = (addr: string, tvl: number | null, approx: boolean) =>
void tvlQ.run(tvl, approx ? 1 : 0, addr.toLowerCase())
// ---- pool_stats ----
const upStatsQ = db.prepare(`
INSERT INTO pool_stats (address, vol24h_usd, txns24h, liq_usd, source, updated) VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(address) DO UPDATE SET vol24h_usd = excluded.vol24h_usd, txns24h = excluded.txns24h,
liq_usd = excluded.liq_usd, source = excluded.source, updated = excluded.updated`)
export const upsertStats = (addr: string, vol24h: number | null, txns24h: number | null, liqUsd: number | null, source: string) =>
void upStatsQ.run(addr.toLowerCase(), vol24h, txns24h, liqUsd, source, now())
/** hot set: real TVL, or GT-visible activity, or freshly created */
export const hotAddrs = (): string[] =>
(
db
.prepare(
`SELECT address FROM pool_state WHERE tvl_usd >= ?
UNION SELECT address FROM pool_stats WHERE vol24h_usd > 0
UNION SELECT address FROM pools WHERE added_ts > ?`,
)
.all(10_000, now() - 3_600) as { address: string }[]
).map((r) => r.address)
/**
* active set for the hourly sweep: anything that ever showed ≥$100 TVL plus
* everything younger than 48h. The launchpads mint ~20k dust pools/day — the
* 6-hourly census (allPoolAddrs) keeps their state honest, the hourly sweep
* stays bounded by real liquidity instead of catalog size.
*/
export const activeAddrs = (): string[] =>
(
db
.prepare(
`SELECT address FROM pool_state WHERE tvl_usd >= ?
UNION SELECT address FROM pools WHERE added_ts > ?`,
)
.all(100, now() - 172_800) as { address: string }[]
).map((r) => r.address)
export const tx = (fn: () => void) => {
db.exec('BEGIN')
try {
fn()
db.exec('COMMIT')
} catch (e) {
db.exec('ROLLBACK')
throw e
}
}