Some checks failed
CI / Frontend Lint (push) Failing after 6s
CI / Frontend Type Check (push) Failing after 6s
CI / Frontend Build (push) Failing after 6s
CI / Frontend E2E Tests (push) Failing after 8s
CI / Contracts Compile (push) Has been cancelled
CI / Contracts Test (push) Has been cancelled
CI / Orchestrator Build (push) Has been cancelled
Security Scan / OWASP ZAP Scan (push) Has been cancelled
Security Scan / Dependency Vulnerability Scan (push) Has been cancelled
172 lines
5.2 KiB
TypeScript
172 lines
5.2 KiB
TypeScript
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<keyof typeof inst> = [
|
|
"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;
|
|
}
|
|
}
|
|
|