import { sql, type Selectable } from 'kysely' import type * as Db from '../Db.js' import type * as db_Schema from '../Schema.js' /** Columns of the `routes_transfer_subsidy_nonces` table. */ export type Table = db_Schema.RoutesTransferSubsidyNonce /** One persisted destination signer nonce cursor. */ export type Record = Selectable /** Serializes inventory and nonce operations for one signer and chain. */ async function lock(db: Db.Db, options: Lane): Promise { const key = `${options.chainId}:${options.account.toLowerCase()}` await sql`SELECT pg_advisory_xact_lock(hashtextextended(${key}, 0))`.execute(db.kysely) } type Lane = { /** Destination subsidy signer address. */ account: string /** Destination EVM chain CAIP-2 id. */ chainId: string } /** Locks a signer lane and rejects new work when its nonce cannot be recovered. */ export async function lockAvailableIn(db: Db.Db, options: Lane): Promise { await lock(db, options) const current = await db.kysely .selectFrom('routes_transfer_subsidy_nonces') .selectAll() .where('account', '=', options.account.toLowerCase()) .where('chainId', '=', options.chainId) .forUpdate() .executeTakeFirst() if (current?.blockedAt) throw new BlockedError() } /** Blocks a signer lane after a reserved nonce is permanently rejected. */ export async function block(db: Db.Db, options: block.Options): Promise { return db.transaction((tx) => blockIn(tx, options)) } /** Blocks a signer lane inside a caller-owned transaction. */ export async function blockIn(db: Db.Db, options: block.Options): Promise { await lock(db, options) const now = new Date().toISOString() const updated = await db.kysely .updateTable('routes_transfer_subsidy_nonces') .set({ blockedAt: now, blockedReason: options.reason, blockedTransferId: options.transferId, updatedAt: now, }) .where('account', '=', options.account.toLowerCase()) .where('chainId', '=', options.chainId) .executeTakeFirst() if (updated.numUpdatedRows !== 1n) throw new MissingCursorError() } export declare namespace block { /** Signer lane and permanent rejection evidence. */ type Options = Lane & { /** Bounded reason for blocking the signer lane. */ reason: 'broadcast_rejected' /** Transfer holding the rejected nonce. */ transferId: string } } /** Reserves one nonce under a transaction-scoped signer and chain lock. */ export async function reserve(db: Db.Db, options: reserve.Options): Promise { const account = options.account.toLowerCase() await lock(db, options) const current = await db.kysely .selectFrom('routes_transfer_subsidy_nonces') .selectAll() .where('account', '=', account) .where('chainId', '=', options.chainId) .forUpdate() .executeTakeFirst() if (current?.blockedAt) throw new BlockedError() const nonce = Math.max(current ? Number(current.nextNonce) : 0, options.pendingNonce) await db.kysely .insertInto('routes_transfer_subsidy_nonces') .values({ account, blockedAt: null, blockedReason: null, blockedTransferId: null, chainId: options.chainId, nextNonce: String(nonce + 1), updatedAt: new Date().toISOString(), }) .onConflict((conflict) => conflict.columns(['chainId', 'account']).doUpdateSet({ nextNonce: String(nonce + 1), updatedAt: new Date().toISOString(), }), ) .execute() return nonce } /** The signer lane is blocked by an unrecoverable reserved nonce. */ export class BlockedError extends Error { override name = 'Routes.TransferSubsidyNonce.BlockedError' } /** A rejected subsidy transaction had no persisted nonce cursor. */ export class MissingCursorError extends Error { override name = 'Routes.TransferSubsidyNonce.MissingCursorError' } export declare namespace reserve { /** Signer and chain cursor used to reserve one nonce. */ type Options = { /** Destination subsidy signer address. */ account: string /** Destination EVM chain CAIP-2 id. */ chainId: string /** Next nonce observed from the pending RPC state. */ pendingNonce: number } }