import type { ColumnType, JSONColumnType, Selectable } from 'kysely' import type { Address } from 'viem' import type * as Campaigns from '../../internal/rewards/Campaigns.js' import type * as Db from '../Db.js' import type * as db_Schema from '../Schema.js' /** Columns of the `reward_accounts` table. */ export type Table = Omit & { /** Bigint chain id, read as a string from pg and written as a number. */ chainId: ColumnType /** Surviving qualified deposit lots. */ lots: JSONColumnType } /** A stored current recipient projection. */ export type Record = db_Schema.RewardAccount /** Reads one recipient projection. */ export async function get(db: Db.Db, options: get.Options): Promise { const row = await db.kysely .selectFrom('reward_accounts') .selectAll() .where('chainId', '=', String(options.chainId)) .where('vaultAddress', '=', normalize(options.vaultAddress)) .where('recipient', '=', normalize(options.recipient)) .executeTakeFirst() return row ? toRecord(row) : undefined } export declare namespace get { /** Recipient projection identity. */ type Options = { /** Chain containing the vault. */ chainId: number /** Registered recipient. */ recipient: string /** EarnVault address. */ vaultAddress: string } } /** Lists one campaign's current recipient projections. */ export async function list(db: Db.Db, options: list.Options): Promise { const rows = await db.kysely .selectFrom('reward_accounts') .selectAll() .where('chainId', '=', String(options.chainId)) .where('vaultAddress', '=', normalize(options.vaultAddress)) .orderBy('recipient', 'asc') .execute() return rows.map(toRecord) } export declare namespace list { /** Campaign identity. */ type Options = { /** Chain containing the vault. */ chainId: number /** EarnVault address. */ vaultAddress: string } } /** Summarizes current unpaid cumulative entitlement for operations. */ export async function unpaid(db: Db.Db, options: unpaid.Options): Promise { const rows = await db.kysely .selectFrom('reward_accounts') .select(['cumulativeEntitlement', 'cumulativePaid']) .where('chainId', '=', String(options.chainId)) .where('vaultAddress', '=', normalize(options.vaultAddress)) .whereRef('cumulativeEntitlement', '>', 'cumulativePaid') .execute() return { earnShares: rows .reduce( (sum, row) => sum + BigInt(row.cumulativeEntitlement) - BigInt(row.cumulativePaid), 0n, ) .toString(), recipients: rows.length, } } export declare namespace unpaid { /** Campaign selector. */ type Options = { /** Chain containing the vault. */ chainId: number /** EarnVault address. */ vaultAddress: string } /** Current unpaid totals. */ type Result = { /** Unpaid EarnShare units. */ earnShares: string /** Recipients with a positive unpaid delta. */ recipients: number } } /** Atomically replaces delivered projections for one campaign. */ export async function replace(db: Db.Db, options: replace.Options): Promise { await db.kysely .deleteFrom('reward_accounts') .where('chainId', '=', String(options.chainId)) .where('vaultAddress', '=', normalize(options.vaultAddress)) .execute() if (options.accounts.length === 0) return for (let offset = 0; offset < options.accounts.length; offset += 500) await db.kysely .insertInto('reward_accounts') .values( options.accounts.slice(offset, offset + 500).map((account) => ({ ...account, chainId: options.chainId, lots: JSON.stringify(account.lots), recipient: normalize(account.recipient), vaultAddress: normalize(options.vaultAddress), })), ) .execute() } export declare namespace replace { /** Closing projections for one delivered run. */ type Options = { /** Recipient projections without campaign identity. */ accounts: readonly Omit[] /** Chain containing the vault. */ chainId: number /** EarnVault address. */ vaultAddress: Address } } /** * Clears deposit-derived state for the given recipients. A zero-state account * without a registration version is replayed from history by the next run. */ export async function resetPrincipal(db: Db.Db, options: resetPrincipal.Options): Promise { const recipients = [...new Set(options.recipients.map(normalize))] if (recipients.length === 0) return 0 const result = await db.kysely .updateTable('reward_accounts') .set({ allocatedPrincipalAssets: '0', eligibilityRegisteredAt: null, lots: JSON.stringify([]), qualifiedEarnShares: '0', }) .where('chainId', '=', String(options.chainId)) .where('vaultAddress', '=', normalize(options.vaultAddress)) .where('recipient', 'in', recipients) .executeTakeFirst() return Number(result.numUpdatedRows) } export declare namespace resetPrincipal { /** Recipients whose deposit-derived state is cleared. */ type Options = { /** Chain containing the vault. */ chainId: number /** Registered recipients to reset. */ recipients: readonly string[] /** EarnVault address. */ vaultAddress: string } } function normalize(address: string): Address { return address.toLowerCase() as Address } function toRecord(row: Selectable): Record { return { ...row, chainId: Number(row.chainId) } }