import * as z from 'zod/mini' import type * as Db from '../../db/Db.js' import * as RoutesDepositAddresses from '../../db/tables/routesDepositAddresses.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' /** Internal route deposit address statuses. */ export const statuses = ['action-required', 'active', 'deactivated'] as const /** Route deposit address statuses exposed by the existing public API. */ export const publicStatuses = ['action-required', 'active'] as const /** Lifecycle status of a route 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 /** Matches the webhook owner cap; four 25-address poll batches complete below the five-minute overdue alert. */ export const maximumPerCreator = 100 const transitions: Record = { 'action-required': ['active', 'deactivated'], active: ['action-required', 'deactivated'], deactivated: [], } /** Returns whether an address no longer needs reconciliation. */ export function isTerminal(status: Status): boolean { return status === 'deactivated' } /** Returns whether the lifecycle allows moving between address statuses. */ export function canTransition(from: Status, to: Status): boolean { return transitions[from].includes(to) } /** Generates a route deposit address id (`rda_…`, lexically time-ordered). */ export function generateId(now: Date = new Date()): string { return Id.generateSortable('rda', now) } /** Zod schemas owned by the route deposit address resource. */ export namespace schema { /** Route 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 route deposit address lifecycle status. */ export const PublicStatus = z .enum(publicStatuses) .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 route deposit address. */ export const RoutesDepositAddress = 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|rda)_[A-Za-z0-9_-]+$/), z.describe('Route deposit address id (`rda_…`, or legacy `fda_…`).'), z.meta({ examples: ['rda_001785792000000_2ZPE2gvateYEQ0dQslgvkhjx'] }), ), provider: Snapshot.shape.provider, recipient: Snapshot.shape.recipient, refundAddress: Snapshot.shape.refundAddress, sourceChain: Snapshot.shape.sourceChain, sourceToken: Snapshot.shape.sourceToken, status: PublicStatus, 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 routes a Tempo account.')), 'RoutesDepositAddress', ) /** A reusable route deposit address with terms sampled during creation. */ export const CreatedRoutesDepositAddress = OpenApi.component( z .object({ ...RoutesDepositAddress.shape, destinationAmount: Provider.schema.RoutesQuote.shape.destinationAmount, destinationAmountMin: Provider.schema.RoutesQuote.shape.destinationAmountMin, fees: z.array(Transfer.schema.Fee).check(z.describe('Fees applied to the quoted deposit.')), quote: Provider.schema.QuoteDetails, sourceAmount: Provider.schema.RoutesQuote.shape.sourceAmount, }) .check(z.describe('A reusable route deposit address with current quoted delivery terms.')), 'CreatedRoutesDepositAddress', ) } /** Stored public snapshot of a route deposit address. */ export type Snapshot = z.output /** A route deposit address as returned by reads. */ export type Public = z.output /** A route deposit address returned by creation with current quoted terms. */ export type Created = z.output /** Creates a durable route deposit address for an already-provisioned provider address. */ export async function create( db: Db.Db, input: create.Input, ): Promise { return RoutesDepositAddresses.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 RoutesDepositAddresses.insertOrGetWithinLimit(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, }, maxPerCreator: maximumPerCreator, record: value, }).catch((cause) => { if (cause instanceof RoutesDepositAddresses.ConflictError) throw new ConflictError() if (cause instanceof RoutesDepositAddresses.LimitExceededError) throw new LimitExceededError(cause.limit) if (cause instanceof RoutesDepositAddresses.OwnerNotFoundError) throw new OwnerNotFoundError() 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: RoutesDepositAddresses.Record /** Whether the provider address was newly stored or matched an existing identity. */ type: 'created' | 'existing' } } function record(input: create.Input): RoutesDepositAddresses.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, createdAt: now.toISOString(), creatorUserId: null, 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', statusUpdatedAt: now.toISOString(), subsidize: snapshot.subsidize, updatedAt: now.toISOString(), version: 1, } } export declare namespace create { /** Fields required to persist a provisioned deposit address. */ type Input = { /** 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 } } /** Persists or returns a reusable address already provisioned before the current capacity check. */ export async function recoverOrGet( db: Db.Db, input: recoverOrGet.Input, ): Promise { const value = record(input) const result = await RoutesDepositAddresses.insertProvisionedOrGet(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 RoutesDepositAddresses.ConflictError) throw new ConflictError() if (cause instanceof RoutesDepositAddresses.OwnerNotFoundError) throw new OwnerNotFoundError() throw cause }) if (result.providerId !== value.providerId) throw new ConflictError() return { record: result, type: result.id === value.id ? 'created' : 'existing' } } export declare namespace recoverOrGet { /** Already-provisioned deposit address fields. */ type Input = create.Input /** Whether recovery created or matched the stored reusable address. */ type ReturnType = createOrGet.ReturnType } /** Applies a version-guarded route deposit address status transition. */ export async function transition( db: Db.Db, options: transition.Options, ): Promise { const current = await RoutesDepositAddresses.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 now = new Date().toISOString() const updated = await RoutesDepositAddresses.update(db, { expectedVersion: options.expectedVersion, id: options.id, status: options.status, statusUpdatedAt: now, updatedAt: now, version: current.version + 1, }) if (updated) return { record: updated, type: 'applied' } const latest = await RoutesDepositAddresses.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 /** Route deposit address id (`rda_…`). */ id: string /** New lifecycle status. */ status: Status } /** Outcome of a guarded address transition. */ type Result = | { record: RoutesDepositAddresses.Record; type: 'applied' } | { current: RoutesDepositAddresses.Record; type: 'invalid' } | { current: RoutesDepositAddresses.Record; type: 'stale' } | { type: 'not_found' } } /** Serializes a stored address to the public read shape. */ export function toPublic(record: RoutesDepositAddresses.Record): Public { return schema.RoutesDepositAddress.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 = 'RoutesDepositAddress.ConflictError' } /** Organization creator's deposit-address cap reached. */ export class LimitExceededError extends Error { override name = 'RoutesDepositAddress.LimitExceededError' /** Maximum durable deposit addresses the canonical creator may own. */ limit: number constructor(limit: number) { super(`Route deposit address limit reached (${limit}).`) this.limit = limit } } /** Owning organization disappeared before address persistence. */ export class OwnerNotFoundError extends Error { override name = 'RoutesDepositAddress.OwnerNotFoundError' }