67 lines
3.5 KiB
TypeScript
Executable File
67 lines
3.5 KiB
TypeScript
Executable File
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;
|
|
}
|