import type { Selectable } from 'kysely' import type * as Db from '../Db.js' import type * as db_Schema from '../Schema.js' /** Columns of the `funding_transfer_transactions` table. */ export type Table = db_Schema.FundingTransferTransaction /** One verified transaction reference on a transfer. */ export type Record = Selectable /** * Inserts a transaction reference, lowercasing EVM hashes while preserving * other references. Repeats return `undefined`; conflicting source ownership throws. */ export function insert(db: Db.Db, record: Record): Promise { return db.kysely .insertInto('funding_transfer_transactions') .values({ ...record, transactionRef: normalize(record.chainId, record.transactionRef), }) .onConflict((oc) => oc.columns(['transferId', 'role', 'transactionRef']).doNothing()) .returningAll() .executeTakeFirst() } /** Claims a source transaction for one transfer without racing another transfer. */ export async function claimSource(db: Db.Db, record: Record): Promise { const transactionRef = normalize(record.chainId, record.transactionRef) const inserted = await db.kysely .insertInto('funding_transfer_transactions') .values({ ...record, transactionRef }) .onConflict((oc) => oc.doNothing()) .returningAll() .executeTakeFirst() if (inserted) return { record: inserted, type: 'claimed' } const existing = await getSource(db, { chainId: record.chainId, transactionRef, }) if (existing?.transferId === record.transferId) return { record: existing, type: 'replayed' } return { type: 'conflict' } } export declare namespace claimSource { /** Source transaction ownership outcome. */ type Result = | { record: Record; type: 'claimed' } | { record: Record; type: 'replayed' } | { type: 'conflict' } } /** Gets the transfer that owns one source transaction reference. */ export function getSource(db: Db.Db, options: getSource.Options): Promise { return db.kysely .selectFrom('funding_transfer_transactions') .selectAll() .where('chainId', '=', options.chainId) .where('role', '=', 'source') .where('transactionRef', '=', normalize(options.chainId, options.transactionRef)) .executeTakeFirst() } export declare namespace getSource { /** Source transaction identity. */ type Options = { /** CAIP-2 chain containing the transaction. */ chainId: string /** EVM transaction hash or case-sensitive chain reference. */ transactionRef: string } } /** Removes one source transaction only when the transfer still owns it. */ export function removeSource( db: Db.Db, options: removeSource.Options, ): Promise { return db.kysely .deleteFrom('funding_transfer_transactions') .where('chainId', '=', options.chainId) .where('role', '=', 'source') .where('transactionRef', '=', normalize(options.chainId, options.transactionHash)) .where('transferId', '=', options.transferId) .returningAll() .executeTakeFirst() } export declare namespace removeSource { /** Exact source evidence identity and owner. */ type Options = { /** CAIP-2 chain containing the transaction. */ chainId: string /** Source transaction reference. */ transactionHash: string /** Owning transfer id (`ftr_…`). */ transferId: string } } /** Lists a transfer's transaction references, oldest first. */ export function listByTransfer(db: Db.Db, transferId: string): Promise { return db.kysely .selectFrom('funding_transfer_transactions') .selectAll() .where('transferId', '=', transferId) .orderBy('createdAt', 'asc') .execute() } function normalize(chainId: string, transactionRef: string): string { return chainId.startsWith('eip155:') ? transactionRef.toLowerCase() : transactionRef }