import type * as Db from '../../db/Db.js' import * as FundingDeposits from '../../db/tables/fundingDeposits.js' import * as WebhookSubscriptions from '../../db/tables/webhookSubscriptions.js' import * as Webhooks from '../Webhooks.js' import * as Deposit from './Deposit.js' /** Applies a deposit transition and stages every matching webhook obligation atomically. */ export async function transition( db: Db.Db, options: Deposit.transition.Options, ): Promise { return db.transaction(async (tx) => { const result = await Deposit.transition(tx, options) if (result.type !== 'applied') return { references: [], result } const staged = await stage(tx, { records: [result.record] }) return { references: staged.references, result } }) } export declare namespace transition { /** Transition result plus compact references for atomically staged obligations. */ type Result = { /** Queue references staged with the applied transition. */ references: Webhooks.QueueReference[] /** Version-guarded deposit transition outcome. */ result: Deposit.transition.Result } } /** Stages durable webhook obligations for committed funding deposit versions. */ export async function stage( db: Db.Db, options: stage.Options, ): Promise { const dispatchables: Webhooks.ensureQueueEvents.Dispatchable[] = [] const now = new Date().toISOString() for (const record of unique(options.records)) { const chainId = destinationChainId(record) if (chainId === undefined) continue const subscriptions = await WebhookSubscriptions.listActiveForFundingEvent(db, { chainId, environment: record.environment, eventType: 'funding:deposit.updated', now, orgId: record.orgId, ...(record.projectId === null ? {} : { projectId: record.projectId }), }) const data = Deposit.toPublic(record) for (const subscription of subscriptions) { if (!matches(subscription.filters, data)) continue dispatchables.push({ envelope: Webhooks.buildKeyedEnvelope({ createdAt: new Date(record.updatedAt), data, key: `${record.id}:${record.version}`, subscription, }), subscription, }) } } return Webhooks.ensureQueueEvents(db, dispatchables) } export declare namespace stage { /** Committed deposit changes eligible for customer webhook delivery. */ type Options = { /** Latest committed funding deposit records. */ records: readonly FundingDeposits.Record[] } } function destinationChainId(record: FundingDeposits.Record): number | undefined { const match = /^eip155:(\d+)$/.exec(record.snapshot.destinationChain.id) if (!match) return undefined const chainId = Number(match[1]) return Number.isSafeInteger(chainId) && chainId > 0 ? chainId : undefined } function matches(filters: Record, deposit: Deposit.Public): boolean { if ( typeof filters['depositAddressId'] === 'string' && filters['depositAddressId'] !== deposit.depositAddressId ) return false if ( typeof filters['recipient'] === 'string' && filters['recipient'].toLowerCase() !== deposit.recipient.toLowerCase() ) return false if (typeof filters['status'] === 'string' && filters['status'] !== deposit.status) return false return true } function unique(records: readonly FundingDeposits.Record[]): FundingDeposits.Record[] { const byId = new Map() for (const record of records) { const current = byId.get(record.id) if (!current || current.version < record.version) byId.set(record.id, record) } return [...byId.values()] }