import type { Pool } from 'pg'; import { address, verifySignature } from '../testnet/consensus.ts'; import { moneroDepositAddressRequestSchema, moneroDepositAddressMessage } from '../../shared/monero-deposit.ts'; import { createOrRecoverMoneroDepositSubaddress, reserveMoneroDepositAllocation } from './monero-deposit-allocation-store.ts'; import type { MoneroDepositAddressRpc } from './monero-observer.ts'; const MAX_REQUEST_LIFETIME_MS = 5 * 60_000; export type MainnetEnrollmentContext = { custodianId: string; chainId: string; walletGenerationId: string; walletFingerprint: string; accountIndex: number; now?: () => number; }; export type MoneroDepositAddressResult = { network: 'mainnet'; allocationId: string; walletGenerationId: string; addressIndex: number; address: string; replayed: boolean; }; /** * Verify an expiring customer-wallet signature and durably allocate that customer's one mainnet * subaddress. This only enrolls an address; it never credits a balance, starts a sale, or moves XMR. * Do not expose a public route until the matching credit, withdrawal, and recovery paths are ready. */ export async function enrollMoneroDepositAddress(pool: Pool, rpc: MoneroDepositAddressRpc, context: MainnetEnrollmentContext, rawRequest: unknown): Promise { const request = moneroDepositAddressRequestSchema.parse(rawRequest); if (request.network !== 'wikideck-randomx-mainnet-1' || request.custodianId !== context.custodianId || request.chainId !== context.chainId || request.walletGenerationId !== context.walletGenerationId || request.walletFingerprint !== context.walletFingerprint || request.accountIndex !== context.accountIndex) { throw new Error('Deposit address request does not match this mainnet custodian and wallet generation'); } if (address(request.publicKey) !== request.actor) throw new Error('Deposit address signer does not own the requested beneficiary'); const { signature, ...unsigned } = request; const message = moneroDepositAddressMessage(unsigned); if (!verifySignature(request.publicKey, request.actor, message, signature)) { throw new Error('Deposit address request signature is invalid'); } const now = context.now?.() ?? Date.now(); if (!Number.isSafeInteger(now) || now < 0 || request.expiresAt < now || request.expiresAt > now + MAX_REQUEST_LIFETIME_MS) { throw new Error('Deposit address request is expired or outside the allowed window'); } const { allocation } = await reserveMoneroDepositAllocation(pool, { requestId: request.requestId, custodianId: context.custodianId, marketNetwork: 'wikideck-randomx-mainnet-1', marketChainId: context.chainId, beneficiary: request.actor, walletGenerationId: context.walletGenerationId, walletFingerprint: context.walletFingerprint, accountIndex: context.accountIndex, reservedAt: now, }); const registered = await createOrRecoverMoneroDepositSubaddress(pool, rpc, allocation.allocationId, context.walletGenerationId, now); if (registered.allocation.beneficiary !== request.actor || registered.allocation.custodianId !== context.custodianId || registered.allocation.marketChainId !== context.chainId || registered.allocation.moneroNetwork !== 'mainnet' || registered.allocation.status !== 'registered' || !registered.allocation.moneroAddress) { throw new Error('Registered Monero deposit address does not match the signed beneficiary allocation'); } return { network: 'mainnet', allocationId: registered.allocation.allocationId, walletGenerationId: registered.allocation.walletGenerationId, addressIndex: registered.allocation.addressIndex, address: registered.allocation.moneroAddress, replayed: registered.replayed }; }