Files
CurrenciCombo/orchestrator/src/services/planValidation.ts
nsatoshi 3e1fb9ef7e
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
PR C: wire real NotaryRegistry on Chain 138 (arch step 4) (#7)
2026-04-22 17:11:50 +00:00

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;
}
}