Validation-only pilot contract
This commit is contained in:
+66
@@ -0,0 +1,66 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user