import * as z from 'zod/mini' import type * as Db from '../../db/Db.js' import * as FundingDepositAddresses from '../../db/tables/fundingDepositAddresses.js' import * as Id from '../Id.js' import * as OpenApi from '../OpenApi.js' import * as Provider from './Provider.js' import * as Transfer from './Transfer.js' /** Public funding deposit address statuses. */ export const statuses = ['action-required', 'active'] as const /** Lifecycle status of a funding deposit address. */ export type Status = (typeof statuses)[number] /** Internal responsibility for delivering the requested destination token. */ export const deliveryStrategies = ['provider', 'tempo'] as const /** Whether the provider or Tempo delivers the requested destination token. */ export type DeliveryStrategy = (typeof deliveryStrategies)[number] /** Bounded provider state retained privately for reconciliation. */ export type ProviderState = Record const transitions: Record = { 'action-required': ['active'], active: ['action-required'], } /** Returns whether the lifecycle allows moving between address statuses. */ export function canTransition(from: Status, to: Status): boolean { return transitions[from].includes(to) } /** Generates a funding deposit address id (`fda_…`, lexically time-ordered). */ export function generateId(now: Date = new Date()): string { return Id.generateSortable('fda', now) } /** Zod schemas owned by the funding deposit address resource. */ export namespace schema { /** Funding deposit address lifecycle status. */ export const Status = z .enum(statuses) .check( z.describe('Whether the address can safely process new deposits.'), z.meta({ examples: ['active'] }), ) /** Public fields persisted as the address snapshot. */ export const Snapshot = z .object({ address: z .string() .check( z.describe('Reusable source-chain deposit address.'), z.meta({ examples: ['TJRabPrwbZy45sbavfcjinPJC18kjpRTv8'] }), ), destinationChain: Provider.schema.ChainRef, destinationToken: Provider.schema.TokenRef, provider: Provider.schema.ProviderRef, recipient: z .string() .check( z.regex(/^0x[0-9a-fA-F]{40}$/), z.describe('Tempo account that receives completed deposits.'), z.meta({ examples: [`0x${'11'.repeat(20)}`] }), ), refundAddress: z .string() .check( z.describe('Source-chain address that receives refunds.'), z.meta({ examples: ['TJRabPrwbZy45sbavfcjinPJC18kjpRTv8'] }), ), sourceChain: Provider.schema.ChainRef, sourceToken: Provider.schema.TokenRef, subsidize: z .boolean() .check( z.describe('Whether Tempo guarantees normalized 1:1 delivery.'), z.meta({ examples: [false] }), ), }) .check(z.describe('Public route and ownership-independent fields of a deposit address.')) /** One reusable funding deposit address. */ export const FundingDepositAddress = OpenApi.component( z .object({ address: Snapshot.shape.address, createdAt: z.iso .datetime() .check( z.describe('When the deposit address was created (ISO 8601).'), z.meta({ examples: ['2026-08-04T00:00:00.000Z'] }), ), destinationChain: Snapshot.shape.destinationChain, destinationToken: Snapshot.shape.destinationToken, id: z .string() .check( z.regex(/^fda_[A-Za-z0-9_-]+$/), z.describe('Funding deposit address id (`fda_…`).'), z.meta({ examples: ['fda_001785792000000_2ZPE2gvateYEQ0dQslgvkhjx'] }), ), provider: Snapshot.shape.provider, recipient: Snapshot.shape.recipient, refundAddress: Snapshot.shape.refundAddress, sourceChain: Snapshot.shape.sourceChain, sourceToken: Snapshot.shape.sourceToken, status: Status, subsidize: Snapshot.shape.subsidize, updatedAt: z.iso .datetime() .check( z.describe('When the deposit address last materially changed (ISO 8601).'), z.meta({ examples: ['2026-08-04T00:00:00.000Z'] }), ), }) .check(z.describe('One reusable address for funding a Tempo account.')), 'FundingDepositAddress', ) /** A reusable funding deposit address with terms sampled during creation. */ export const CreatedFundingDepositAddress = OpenApi.component( z .object({ ...FundingDepositAddress.shape, destinationAmount: Provider.schema.FundingQuote.shape.destinationAmount, destinationAmountMin: Provider.schema.FundingQuote.shape.destinationAmountMin, fees: z.array(Transfer.schema.Fee).check(z.describe('Fees applied to the quoted deposit.')), quote: Provider.schema.QuoteDetails, sourceAmount: Provider.schema.FundingQuote.shape.sourceAmount, }) .check(z.describe('A reusable funding deposit address with current quoted delivery terms.')), 'CreatedFundingDepositAddress', ) } /** Stored public snapshot of a funding deposit address. */ export type Snapshot = z.output /** A funding deposit address as returned by reads. */ export type Public = z.output /** A funding deposit address returned by creation with current quoted terms. */ export type Created = z.output /** Creates a durable funding deposit address for an already-provisioned provider address. */ export async function create( db: Db.Db, input: create.Input, ): Promise { return FundingDepositAddresses.insert(db, record(input)) } /** Creates or returns the reusable address for the same owner and route. */ export async function createOrGet( db: Db.Db, input: createOrGet.Input, ): Promise { const value = record(input) const result = await FundingDepositAddresses.insertOrGet(db, { match: { destinationTokenKey: value.destinationTokenKey, environment: value.environment, orgId: value.orgId, ...(value.projectId === null ? {} : { projectId: value.projectId }), recipient: value.recipient, refundAddress: value.refundAddress, sourceChainId: value.sourceChainId, sourceTokenKey: value.sourceTokenKey, subsidize: value.subsidize, }, record: value, }).catch((cause) => { if (cause instanceof FundingDepositAddresses.ConflictError) throw new ConflictError() throw cause }) if (result.providerId !== value.providerId) throw new ConflictError() return { record: result, type: result.id === value.id ? 'created' : 'existing' } } export declare namespace createOrGet { /** Fields required to persist or find a provisioned deposit address. */ type Input = create.Input /** Whether provisioning created or matched the stored reusable address. */ type ReturnType = { /** Stored reusable deposit address. */ record: FundingDepositAddresses.Record /** Whether the provider address was newly stored or matched an existing identity. */ type: 'created' | 'existing' } } function record(input: create.Input): FundingDepositAddresses.Record { const now = input.now ?? new Date() const parsed = schema.Snapshot.parse(input.snapshot) const snapshot = { ...parsed, address: parsed.sourceChain.kind === 'evm' ? parsed.address.toLowerCase() : parsed.address, } return { address: snapshot.address, apiKeyId: input.apiKeyId, createdAt: now.toISOString(), deliveryStrategy: input.deliveryStrategy, destinationTokenKey: snapshot.destinationToken.tokenKey, environment: input.environment, id: input.id ?? generateId(now), lastPolledAt: null, nextPollAt: now.toISOString(), orgId: input.orgId, pollFailureCount: 0, pollLeaseUntil: null, pollLeaseVersion: 0, projectId: input.projectId ?? null, providerOutputToken: input.providerOutputToken, providerId: snapshot.provider.id, providerRequestIds: input.providerRequestIds ?? [], providerState: input.providerState ?? null, recipient: snapshot.recipient, refundAddress: snapshot.refundAddress, snapshot, sourceChainId: snapshot.sourceChain.id, sourceTokenKey: snapshot.sourceToken.tokenKey, status: 'active', subsidize: snapshot.subsidize, updatedAt: now.toISOString(), version: 1, } } export declare namespace create { /** Fields required to persist a provisioned deposit address. */ type Input = { /** API key that provisioned the address. */ apiKeyId: string /** Responsibility for delivering the requested destination token. */ deliveryStrategy: DeliveryStrategy /** Key environment the address belongs to. */ environment: 'production' | 'sandbox' /** Address id override for deterministic tests. */ id?: string | undefined /** Creation time override for deterministic tests. */ now?: Date | undefined /** Owning organization id (`org_…`). */ orgId: string /** Attributed project id for a project-scoped API key. */ projectId?: string | undefined /** Token that the provider outputs on Tempo. */ providerOutputToken: Provider.TokenRef /** Private provider request identifiers. */ providerRequestIds?: readonly string[] | undefined /** Private bounded provider state. */ providerState?: ProviderState | undefined /** Public address snapshot. */ snapshot: Snapshot } } /** Applies a version-guarded funding deposit address status transition. */ export async function transition( db: Db.Db, options: transition.Options, ): Promise { const current = await FundingDepositAddresses.get(db, options.id) if (!current) return { type: 'not_found' } if (current.version !== options.expectedVersion) return { current, type: 'stale' } if (!canTransition(current.status, options.status)) return { current, type: 'invalid' } const updated = await FundingDepositAddresses.update(db, { expectedVersion: options.expectedVersion, id: options.id, status: options.status, updatedAt: new Date().toISOString(), version: current.version + 1, }) if (updated) return { record: updated, type: 'applied' } const latest = await FundingDepositAddresses.get(db, options.id) return latest ? { current: latest, type: 'stale' } : { type: 'not_found' } } export declare namespace transition { /** Fields required for a guarded status transition. */ type Options = { /** Version that the transition was computed against. */ expectedVersion: number /** Funding deposit address id (`fda_…`). */ id: string /** New lifecycle status. */ status: Status } /** Outcome of a guarded address transition. */ type Result = | { record: FundingDepositAddresses.Record; type: 'applied' } | { current: FundingDepositAddresses.Record; type: 'invalid' } | { current: FundingDepositAddresses.Record; type: 'stale' } | { type: 'not_found' } } /** Serializes a stored address to the public read shape. */ export function toPublic(record: FundingDepositAddresses.Record): Public { return schema.FundingDepositAddress.parse({ ...record.snapshot, createdAt: record.createdAt, id: record.id, status: record.status, subsidize: record.subsidize, updatedAt: record.updatedAt, }) } /** Error thrown when a provider conflicts with a stored reusable address. */ export class ConflictError extends Error { override name = 'FundingDepositAddress.ConflictError' }