import type { ColumnType, JSONColumnType, Selectable } from 'kysely' import { customAlphabet } from 'nanoid' import type { Address } from 'viem' import type * as Db from '../Db.js' import type * as db_Schema from '../Schema.js' const slugSuffix = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', 8) /** Columns of the `earn_vaults` table. */ export type Table = Omit< db_Schema.EarnVault, 'chainId' | 'privateInputTokens' | 'privateOutputTokens' | 'zones' > & { /** Bigint chain id, read as a string from pg and written as a number. */ chainId: ColumnType /** Tokens accepted for private deposits. */ privateInputTokens: JSONColumnType /** Tokens supported for private redemptions. */ privateOutputTokens: JSONColumnType /** Curated private Zone routes. */ zones: JSONColumnType } /** A stored curated Earn vault. */ export type Record = db_Schema.EarnVault /** Reads one curated vault by chain and vault address. */ export async function get(db: Db.Db, options: get.Options): Promise { const row = await db.kysely .selectFrom('earn_vaults') .selectAll() .where('chainId', '=', String(options.chainId)) .where('vaultAddress', '=', normalize(options.vaultAddress)) .executeTakeFirst() return row ? toRecord(row) : undefined } export declare namespace get { /** Identity of one curated vault. */ type Options = { /** Chain the vault belongs to. */ chainId: number /** Earn vault address. */ vaultAddress: string } } /** Lists curated vaults in stable chain and address order. */ export async function list(db: Db.Db, options: list.Options = {}): Promise { let query = db.kysely.selectFrom('earn_vaults').selectAll() if (options.chainId !== undefined) query = query.where('chainId', '=', String(options.chainId)) const rows = await query.orderBy('chainId', 'asc').orderBy('vaultAddress', 'asc').execute() return rows.map(toRecord) } export declare namespace list { /** Optional curated-vault filters. */ type Options = { /** Restrict results to one chain. */ chainId?: number | undefined } } /** Inserts or updates a curated vault while preserving its stable identity fields. */ export async function upsert(db: Db.Db, options: upsert.Options): Promise { const now = new Date().toISOString() const slug = options.slug ?? `${kebab(options.label)}-${slugSuffix()}` const row = await db.kysely .insertInto('earn_vaults') .values({ chainId: options.chainId, createdAt: now, description: options.description, label: options.label, privateInputTokens: JSON.stringify(options.privateInputTokens), privateOutputTokens: JSON.stringify(options.privateOutputTokens), slug, updatedAt: now, vaultAddress: normalize(options.vaultAddress), zones: JSON.stringify(normalizeZones(options.zones)), }) .onConflict((oc) => oc.columns(['chainId', 'vaultAddress']).doUpdateSet({ description: options.description, label: options.label, privateInputTokens: JSON.stringify(options.privateInputTokens), privateOutputTokens: JSON.stringify(options.privateOutputTokens), ...(options.slug !== undefined ? { slug } : {}), updatedAt: now, zones: JSON.stringify(normalizeZones(options.zones)), }), ) .returningAll() .executeTakeFirstOrThrow() return toRecord(row) } export declare namespace upsert { /** Curated fields stored for one verified vault. */ type Options = { /** Chain the vault belongs to. */ chainId: number /** Curated vault description. */ description: string | null /** Curated vault label. */ label: string /** Tokens accepted for private deposits. */ privateInputTokens: readonly Address[] /** Tokens supported for private redemptions. */ privateOutputTokens: readonly Address[] /** Stable slug, generated from the label when omitted. */ slug?: string | undefined /** Earn vault address. */ vaultAddress: Address /** Curated private Zone routes. */ zones: db_Schema.EarnVault['zones'] } } /** Removes one curated vault. */ export async function remove(db: Db.Db, options: remove.Options): Promise { const row = await db.kysely .deleteFrom('earn_vaults') .where('chainId', '=', String(options.chainId)) .where('vaultAddress', '=', normalize(options.vaultAddress)) .returningAll() .executeTakeFirst() return row ? toRecord(row) : undefined } export declare namespace remove { /** Identity of one curated vault. */ type Options = { /** Chain the vault belongs to. */ chainId: number /** Earn vault address. */ vaultAddress: string } } function kebab(label: string): string { return ( label .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, '') || 'vault' ) } function toRecord(row: Selectable): Record { return { ...row, chainId: Number(row.chainId) } } function normalize(address: string): Address { return address.toLowerCase() as Address } function normalizeZones(zones: db_Schema.EarnVault['zones']): db_Schema.EarnVault['zones'] { return zones.map((zone) => ({ ...zone, earnRouter: normalize(zone.earnRouter) })) }