import type { ColumnType } from 'kysely' import type { Hex } from 'ox' import type * as VerifiedTokens from '../../internal/VerifiedTokens.js' import type * as Db from '../Db.js' import type * as db_Schema from '../Schema.js' /** A bigint chain id, read as a string from pg and written as a number. */ type ChainId = ColumnType /** Columns of the `verified_tokens` table. */ export type Table = Omit & { chainId: ChainId } /** Columns of the `verified_token_lists` table. */ export type ListTable = Omit & { chainId: ChainId } /** A stored verified-token row. */ export type Row = db_Schema.VerifiedToken /** A chain's list-level metadata. */ export type Meta = db_Schema.VerifiedTokenList /** * Reads a chain's list metadata — the "head" the soft-refresh cache compares * versions against. * * @param db - The database. * @param chainId - The chain id. * @returns The metadata, or `undefined` when the chain has no list yet. */ export async function head(db: Db.Db, chainId: number): Promise { const row = await db.kysely .selectFrom('verified_token_lists') .selectAll() .where('chainId', '=', String(chainId)) .executeTakeFirst() return row ? { ...row, chainId: Number(row.chainId) } : undefined } /** * Reads a chain's tokens in canonical list order. * * @param db - The database. * @param chainId - The chain id. * @returns The rows. */ export async function list(db: Db.Db, chainId: number): Promise { const rows = await db.kysely .selectFrom('verified_tokens') .selectAll() .where('chainId', '=', String(chainId)) .orderBy('position') .execute() return rows.map((row) => ({ ...row, chainId: Number(row.chainId) })) } /** * Publishes a chain's whole list: replaces its token rows (positions rewritten * wholesale) and bumps the list metadata. Callers run this inside the * per-chain publish lock/transaction, so the rewrite is atomic. * * @param db - The database (a transaction-scoped handle). * @param chainId - The chain id. * @param input - The validated list and its new version metadata. */ export async function publish(db: Db.Db, chainId: number, input: publish.Input): Promise { await db.kysely.deleteFrom('verified_tokens').where('chainId', '=', String(chainId)).execute() if (input.tokens.length > 0) await db.kysely .insertInto('verified_tokens') .values( input.tokens.map((token, position) => ({ address: token.address, chainId, currency: token.currency, decimals: token.decimals, logoUri: token.logoUri ?? null, name: token.name, position, symbol: token.symbol, })), ) .execute() await db.kysely .insertInto('verified_token_lists') .values({ chainId, updatedAt: input.updatedAt, version: input.version }) .onConflict((oc) => oc.column('chainId').doUpdateSet({ updatedAt: input.updatedAt, version: input.version }), ) .execute() } export declare namespace publish { /** A validated list to publish. */ type Input = { /** Tokens in canonical order (addresses lowercased by the domain layer). */ tokens: readonly VerifiedTokens.Token[] /** ISO timestamp of the publish. */ updatedAt: string /** New opaque list version. */ version: string } } /** Maps a stored row to the domain token (null `logoUri` → absent field). */ export function toToken(row: Row): VerifiedTokens.Token { return { address: row.address as Hex.Hex, currency: row.currency, decimals: row.decimals, id: row.address as Hex.Hex, name: row.name, symbol: row.symbol, ...(row.logoUri === null ? {} : { logoUri: row.logoUri }), } }