mirror of
https://github.com/labrinyang/lp-terminal.git
synced 2026-08-18 15:18:05 +00:00
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.
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
// 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user