import { createHash } from 'node:crypto'; import { IS_PILOT, NETWORK } from '../../shared/network.ts'; import { pilotMoneroDepositRequestMessage, pilotMoneroDepositRequestSchema } from '../../shared/pilot-monero-deposit.ts'; import type { PilotCustodyIdentity } from './pilot-custody-identity.ts'; import { address, CHAIN_ID, verifySignature } from '../testnet/consensus.ts'; const MAX_REQUEST_LIFETIME_MS = 5 * 60_000; export type PilotDepositRequestContext = { identity: PilotCustodyIdentity; walletGenerationId: string; walletFingerprint: string; accountIndex: number; now?: () => number; }; /** * Validate a signed pilot deposit intent without allocating an address or touching PostgreSQL/Wallet-RPC. * This boundary is intentionally identity-only until credit, withdrawal, and recovery are connected. */ export function validatePilotMoneroDepositRequest(context: PilotDepositRequestContext, rawRequest: unknown) { if (!IS_PILOT || NETWORK !== 'wikideck-randomx-pilot-1') { throw new Error('Pilot Monero deposit requests require the pilot market profile'); } const request = pilotMoneroDepositRequestSchema.parse(rawRequest); const { identity } = context; if (identity.version !== 1 || identity.mode !== 'pilot-custody' || identity.policy !== 'pilot-custody-v1' || identity.marketNetwork !== NETWORK || identity.marketChainId !== CHAIN_ID || identity.moneroNetwork !== 'mainnet' || identity.activationStatus !== 'identity-only' || request.marketNetwork !== identity.marketNetwork || request.marketChainId !== identity.marketChainId || request.moneroNetwork !== identity.moneroNetwork || request.custodianId !== identity.custodianId || request.walletGenerationId !== context.walletGenerationId || request.walletFingerprint !== context.walletFingerprint || request.accountIndex !== context.accountIndex) { throw new Error('Pilot deposit request does not match the identity, Monero network, or wallet generation'); } if (!/^[a-f0-9]{64}$/.test(context.walletFingerprint) || !Number.isSafeInteger(context.accountIndex) || context.accountIndex < 0 || context.accountIndex > 0xffff_ffff) { throw new Error('Invalid configured pilot watch-only wallet identity'); } if (address(request.publicKey) !== request.beneficiary) { throw new Error('Pilot deposit request signer does not own the requested beneficiary'); } const { signature, ...unsigned } = request; const message = pilotMoneroDepositRequestMessage(unsigned); if (!verifySignature(request.publicKey, request.beneficiary, message, signature)) { throw new Error('Pilot deposit 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('Pilot deposit request is expired or outside the allowed window'); } const requestDigest = createHash('sha256').update(message).digest('hex'); return { requestId: request.requestId, custodianId: identity.custodianId, marketNetwork: identity.marketNetwork, marketChainId: identity.marketChainId, moneroNetwork: identity.moneroNetwork, beneficiary: request.beneficiary, walletGenerationId: request.walletGenerationId, walletFingerprint: request.walletFingerprint, accountIndex: request.accountIndex, expiresAt: request.expiresAt, requestDigest, } as const; }