import type { Plan, PlanStep } from "../types/plan"; export interface ValidationResult { valid: boolean; errors: string[]; } const MAX_RECURSION_DEPTH = 3; const MAX_LTV = 0.6; /** * Validate plan structure */ export function validatePlan(plan: Plan): ValidationResult { const errors: string[] = []; // Check required fields if (!plan.steps || plan.steps.length === 0) { errors.push("Plan must contain at least one step"); } // Check recursion depth const borrowSteps = plan.steps.filter((s) => s.type === "borrow"); const recursionDepth = borrowSteps.length - 1; if (recursionDepth > MAX_RECURSION_DEPTH) { errors.push(`Recursion depth ${recursionDepth} exceeds maximum ${MAX_RECURSION_DEPTH}`); } // Check LTV if (plan.maxLTV && plan.maxLTV > MAX_LTV) { errors.push(`Max LTV ${plan.maxLTV} exceeds maximum ${MAX_LTV}`); } // Validate each step plan.steps.forEach((step, index) => { const stepErrors = validateStep(step, index); errors.push(...stepErrors); }); return { valid: errors.length === 0, errors, }; } /** * Validate individual step */ function validateStep(step: PlanStep, index: number): string[] { const errors: string[] = []; switch (step.type) { case "borrow": if (!step.asset || step.amount <= 0) { errors.push(`Step ${index + 1}: Invalid borrow step (asset or amount missing)`); } break; case "swap": if (!step.from || !step.to || step.amount <= 0) { errors.push(`Step ${index + 1}: Invalid swap step (from/to/amount missing)`); } break; case "repay": if (!step.asset || step.amount <= 0) { errors.push(`Step ${index + 1}: Invalid repay step (asset or amount missing)`); } break; case "pay": if (!step.asset || step.amount <= 0 || !step.beneficiary?.IBAN) { errors.push(`Step ${index + 1}: Invalid pay step (asset/amount/IBAN missing)`); } break; case "issueInstrument": { const inst = step.instrument; if (!inst) { errors.push(`Step ${index + 1}: issueInstrument step missing instrument terms`); break; } const required: Array = [ "applicant", "issuingBankBIC", "beneficiaryBankBIC", "beneficiaryName", "currency", "tenor", "expiryDate", "placeOfPresentation", "governingLaw", "templateRef", "templateHash", ]; for (const key of required) { if (!inst[key] || String(inst[key]).trim() === "") { errors.push(`Step ${index + 1}: instrument.${String(key)} is required`); } } if (!(inst.amount > 0)) { errors.push(`Step ${index + 1}: instrument.amount must be > 0`); } if (inst.currency && !/^[A-Z]{3}$/.test(inst.currency)) { errors.push(`Step ${index + 1}: instrument.currency must be ISO 4217 (e.g. USD)`); } // BIC is 8 or 11 chars: 4 bank + 2 country + 2 location [+ 3 branch] const bicRe = /^[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}([A-Z0-9]{3})?$/; if (inst.issuingBankBIC && !bicRe.test(inst.issuingBankBIC)) { errors.push(`Step ${index + 1}: instrument.issuingBankBIC is not a valid BIC`); } if (inst.beneficiaryBankBIC && !bicRe.test(inst.beneficiaryBankBIC)) { errors.push(`Step ${index + 1}: instrument.beneficiaryBankBIC is not a valid BIC`); } if (inst.expiryDate && !/^\d{4}-\d{2}-\d{2}$/.test(inst.expiryDate)) { errors.push(`Step ${index + 1}: instrument.expiryDate must be YYYY-MM-DD`); } if (inst.templateHash && !/^[0-9a-fA-F]{64}$/.test(inst.templateHash)) { errors.push(`Step ${index + 1}: instrument.templateHash must be 64 hex chars (sha256)`); } break; } } return errors; } /** * Check step dependencies */ export function checkStepDependencies(steps: PlanStep[]): ValidationResult { const errors: string[] = []; for (let i = 1; i < steps.length; i++) { const prevStep = steps[i - 1]; const currentStep = steps[i]; // Check if current step depends on previous step output if (currentStep.type === "swap") { // Swap should receive from previous step const prevOutput = getStepOutput(prevStep); if (prevOutput && currentStep.from !== prevOutput.asset) { errors.push(`Step ${i + 1}: Swap expects ${currentStep.from} but previous step outputs ${prevOutput.asset}`); } } if (currentStep.type === "repay") { // Repay should use same asset as previous step const prevOutput = getStepOutput(prevStep); if (prevOutput && currentStep.asset !== prevOutput.asset) { errors.push(`Step ${i + 1}: Repay expects ${currentStep.asset} but previous step outputs ${prevOutput.asset}`); } } } return { valid: errors.length === 0, errors, }; } /** * Get step output (what asset/amount this step produces) */ function getStepOutput(step: PlanStep): { asset: string; amount: number } | null { switch (step.type) { case "borrow": return step.asset ? { asset: step.asset, amount: step.amount } : null; case "swap": return step.to ? { asset: step.to, amount: step.amount } : null; default: return null; } }