Consolidate webapp structure by merging nested components into the main repository

This commit is contained in:
defiQUG
2025-11-05 16:12:53 -08:00
parent 09c5a1fd5e
commit 3b09c35c47
55 changed files with 10240 additions and 0 deletions

View File

@@ -0,0 +1,159 @@
import type { Request, Response } from "express";
import { v4 as uuidv4 } from "uuid";
import { createHash } from "crypto";
import { validatePlan, checkStepDependencies } from "../services/planValidation";
import { storePlan, getPlanById, updatePlanSignature } from "../db/plans";
import type { Plan, PlanStep } from "../types/plan";
/**
* POST /api/plans
* Create a new execution plan
*/
export async function createPlan(req: Request, res: Response) {
try {
const plan: Plan = req.body;
// Validate plan structure
const validation = validatePlan(plan);
if (!validation.valid) {
return res.status(400).json({
error: "Invalid plan",
errors: validation.errors,
});
}
// Check step dependencies
const dependencyCheck = checkStepDependencies(plan.steps);
if (!dependencyCheck.valid) {
return res.status(400).json({
error: "Invalid step dependencies",
errors: dependencyCheck.errors,
});
}
// Generate plan ID and hash
const planId = uuidv4();
const planHash = createHash("sha256")
.update(JSON.stringify(plan))
.digest("hex");
// Store plan
const storedPlan = {
...plan,
plan_id: planId,
plan_hash: planHash,
created_at: new Date().toISOString(),
status: "pending",
};
await storePlan(storedPlan);
res.status(201).json({
plan_id: planId,
plan_hash: planHash,
});
} catch (error: any) {
res.status(500).json({
error: "Failed to create plan",
message: error.message,
});
}
}
/**
* GET /api/plans/:planId
* Get plan details
*/
export async function getPlan(req: Request, res: Response) {
try {
const { planId } = req.params;
const plan = await getPlanById(planId);
if (!plan) {
return res.status(404).json({
error: "Plan not found",
});
}
res.json(plan);
} catch (error: any) {
res.status(500).json({
error: "Failed to get plan",
message: error.message,
});
}
}
/**
* POST /api/plans/:planId/signature
* Add user signature to plan
*/
export async function addSignature(req: Request, res: Response) {
try {
const { planId } = req.params;
const { signature, messageHash, signerAddress } = req.body;
if (!signature || !messageHash || !signerAddress) {
return res.status(400).json({
error: "Missing required fields: signature, messageHash, signerAddress",
});
}
const plan = await getPlanById(planId);
if (!plan) {
return res.status(404).json({
error: "Plan not found",
});
}
// Update plan with signature
await updatePlanSignature(planId, {
signature,
messageHash,
signerAddress,
signedAt: new Date().toISOString(),
});
res.json({
success: true,
planId,
});
} catch (error: any) {
res.status(500).json({
error: "Failed to add signature",
message: error.message,
});
}
}
/**
* POST /api/plans/:planId/validate
* Validate plan structure and dependencies
*/
export async function validatePlanEndpoint(req: Request, res: Response) {
try {
const { planId } = req.params;
const plan = await getPlanById(planId);
if (!plan) {
return res.status(404).json({
error: "Plan not found",
});
}
const validation = validatePlan(plan);
const dependencyCheck = checkStepDependencies(plan.steps);
res.json({
valid: validation.valid && dependencyCheck.valid,
validation: validation,
dependencies: dependencyCheck,
});
} catch (error: any) {
res.status(500).json({
error: "Failed to validate plan",
message: error.message,
});
}
}

View File

@@ -0,0 +1,45 @@
import type { Request, Response } from "express";
import { executionCoordinator } from "../services/execution";
/**
* GET /api/plans/:planId/status/stream
* Server-Sent Events stream for real-time execution status
*/
export function streamPlanStatus(req: Request, res: Response) {
const { planId } = req.params;
// Set SSE headers
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
res.setHeader("X-Accel-Buffering", "no"); // Disable nginx buffering
// Send initial connection message
res.write(`data: ${JSON.stringify({ type: "connected", planId })}\n\n`);
// Listen for status updates
const statusHandler = (executionId: string, event: any) => {
// Only send events for this plan
if (event.planId === planId || executionId.includes(planId)) {
res.write(`data: ${JSON.stringify(event)}\n\n`);
}
};
executionCoordinator.onStatus(statusHandler);
// Handle client disconnect
req.on("close", () => {
executionCoordinator.off("status", statusHandler);
res.end();
});
// Keep connection alive with ping
const pingInterval = setInterval(() => {
res.write(`: ping\n\n`);
}, 30000);
req.on("close", () => {
clearInterval(pingInterval);
});
}

View File

@@ -0,0 +1,29 @@
// In-memory database for plans (mock implementation)
// In production, replace with actual database (PostgreSQL, MongoDB, etc.)
const plans: Map<string, any> = new Map();
export async function storePlan(plan: any): Promise<void> {
plans.set(plan.plan_id, plan);
}
export async function getPlanById(planId: string): Promise<any | null> {
return plans.get(planId) || null;
}
export async function updatePlanSignature(planId: string, signature: any): Promise<void> {
const plan = plans.get(planId);
if (plan) {
plan.signature = signature;
plans.set(planId, plan);
}
}
export async function updatePlanStatus(planId: string, status: string): Promise<void> {
const plan = plans.get(planId);
if (plan) {
plan.status = status;
plans.set(planId, plan);
}
}

View File

@@ -0,0 +1,127 @@
/**
* Bank Connector Integration
* Supports multiple banking rails: SWIFT, SEPA, FedNow, ISO-20022
*/
export interface BankConnector {
name: string;
type: "SWIFT" | "SEPA" | "FEDNOW" | "ISO20022";
sendMessage(message: string): Promise<{ success: boolean; messageId?: string; error?: string }>;
getStatus(messageId: string): Promise<{ status: string; details?: any }>;
}
/**
* SWIFT Connector
*/
export class SwiftConnector implements BankConnector {
name = "SWIFT";
type: "SWIFT" = "SWIFT";
async sendMessage(message: string): Promise<{ success: boolean; messageId?: string; error?: string }> {
// Mock implementation
// In production, this would integrate with SWIFT API
console.log("[SWIFT] Sending message:", message);
return {
success: true,
messageId: `SWIFT-${Date.now()}`,
};
}
async getStatus(messageId: string): Promise<{ status: string; details?: any }> {
// Mock implementation
return {
status: "ACCEPTED",
};
}
}
/**
* SEPA Connector
*/
export class SepaConnector implements BankConnector {
name = "SEPA";
type: "SEPA" = "SEPA";
async sendMessage(message: string): Promise<{ success: boolean; messageId?: string; error?: string }> {
// Mock implementation
// In production, this would integrate with SEPA API
console.log("[SEPA] Sending message:", message);
return {
success: true,
messageId: `SEPA-${Date.now()}`,
};
}
async getStatus(messageId: string): Promise<{ status: string; details?: any }> {
return {
status: "ACCEPTED",
};
}
}
/**
* FedNow Connector
*/
export class FedNowConnector implements BankConnector {
name = "FedNow";
type: "FEDNOW" = "FEDNOW";
async sendMessage(message: string): Promise<{ success: boolean; messageId?: string; error?: string }> {
// Mock implementation
// In production, this would integrate with FedNow API
console.log("[FedNow] Sending message:", message);
return {
success: true,
messageId: `FEDNOW-${Date.now()}`,
};
}
async getStatus(messageId: string): Promise<{ status: string; details?: any }> {
return {
status: "ACCEPTED",
};
}
}
/**
* ISO-20022 Generic Connector
*/
export class Iso20022Connector implements BankConnector {
name = "ISO-20022";
type: "ISO20022" = "ISO20022";
async sendMessage(message: string): Promise<{ success: boolean; messageId?: string; error?: string }> {
// Mock implementation
// In production, this would parse ISO-20022 message and route to appropriate bank
console.log("[ISO-20022] Sending message:", message);
return {
success: true,
messageId: `ISO-${Date.now()}`,
};
}
async getStatus(messageId: string): Promise<{ status: string; details?: any }> {
return {
status: "ACCEPTED",
};
}
}
/**
* Get connector for a specific rail type
*/
export function getConnector(type: "SWIFT" | "SEPA" | "FEDNOW" | "ISO20022"): BankConnector {
switch (type) {
case "SWIFT":
return new SwiftConnector();
case "SEPA":
return new SepaConnector();
case "FEDNOW":
return new FedNowConnector();
case "ISO20022":
return new Iso20022Connector();
default:
return new Iso20022Connector();
}
}

View File

@@ -0,0 +1,80 @@
/**
* Compliance Provider Integration
* Supports KYC/AML providers: Onfido, Chainalysis, Entra Verified ID
*/
export interface KYCResult {
level: number;
verified: boolean;
expiresAt?: string;
}
export interface AMLResult {
passed: boolean;
lastCheck?: string;
riskLevel?: string;
}
export interface IdentityData {
lei?: string;
did?: string;
}
/**
* Check KYC status with Onfido
*/
export async function checkKYC(userId: string): Promise<KYCResult | null> {
// Mock implementation
// In production, this would:
// 1. Call Onfido API to check KYC status
// 2. Parse response and return structured data
console.log(`[Onfido] Checking KYC for user ${userId}`);
// Mock: return verified KYC
return {
level: 2,
verified: true,
expiresAt: new Date(Date.now() + 365 * 24 * 60 * 60 * 1000).toISOString(),
};
}
/**
* Check AML status with Chainalysis
*/
export async function checkAML(userId: string): Promise<AMLResult | null> {
// Mock implementation
// In production, this would:
// 1. Call Chainalysis API to check AML status
// 2. Perform sanctions screening
// 3. Return risk assessment
console.log(`[Chainalysis] Checking AML for user ${userId}`);
// Mock: return passed AML
return {
passed: true,
lastCheck: new Date().toISOString(),
riskLevel: "LOW",
};
}
/**
* Get identity data (LEI, DID) from Entra Verified ID
*/
export async function getIdentityData(userId: string): Promise<IdentityData | null> {
// Mock implementation
// In production, this would:
// 1. Call Entra Verified ID API
// 2. Retrieve LEI and DID credentials
// 3. Verify credentials
console.log(`[Entra] Getting identity for user ${userId}`);
// Mock: return identity data
return {
lei: "1234567890ABCDEF123456",
did: "did:web:example.com:user:" + userId,
};
}

View File

@@ -0,0 +1,72 @@
import type { Plan } from "../types/plan";
import { generatePacs008 } from "./iso20022";
/**
* Prepare bank instruction (2PC prepare phase)
* Sends provisional ISO-20022 message
*/
export async function prepareBankInstruction(plan: Plan): Promise<boolean> {
console.log(`[Bank] Preparing instruction for plan ${plan.plan_id}`);
// Mock: In real implementation, this would:
// 1. Generate provisional ISO-20022 message (pacs.008 with conditional settlement)
// 2. Send to bank connector
// 3. Receive provisional acceptance
await new Promise((resolve) => setTimeout(resolve, 100));
return true;
}
/**
* Commit bank instruction (2PC commit phase)
* Confirms final settlement
*/
export async function commitBankInstruction(plan: Plan): Promise<{
success: boolean;
isoMessageId?: string;
error?: string;
}> {
console.log(`[Bank] Committing instruction for plan ${plan.plan_id}`);
try {
// Generate final ISO-20022 message
const isoMessage = await generatePacs008(plan);
// Mock: In real implementation, this would:
// 1. Send ISO message to bank connector
// 2. Receive confirmation and message ID
// 3. Store message ID for audit trail
const isoMessageId = `MSG-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
// Simulate processing delay
await new Promise((resolve) => setTimeout(resolve, 300));
return {
success: true,
isoMessageId,
};
} catch (error: any) {
return {
success: false,
error: error.message,
};
}
}
/**
* Abort bank instruction (2PC abort phase)
* Cancels provisional instruction
*/
export async function abortBankInstruction(planId: string): Promise<void> {
console.log(`[Bank] Aborting instruction for plan ${planId}`);
// Mock: In real implementation, this would:
// 1. Generate cancellation message (camt.056)
// 2. Send to bank connector
// 3. Confirm cancellation
await new Promise((resolve) => setTimeout(resolve, 100));
}

View File

@@ -0,0 +1,102 @@
import type { Plan } from "../types/plan";
import { checkKYC, checkAML, getIdentityData } from "../integrations/compliance";
export interface ComplianceStatus {
lei?: string;
did?: string;
kyc?: {
level: number;
verified: boolean;
expiresAt?: string;
};
aml?: {
passed: boolean;
lastCheck?: string;
riskLevel?: string;
};
valid: boolean;
}
/**
* Get compliance status for a user/creator
*/
export async function getComplianceStatus(creator: string): Promise<ComplianceStatus> {
try {
const identity = await getIdentityData(creator);
const kyc = await checkKYC(creator);
const aml = await checkAML(creator);
return {
lei: identity?.lei,
did: identity?.did,
kyc: {
level: kyc?.level || 0,
verified: kyc?.verified || false,
expiresAt: kyc?.expiresAt,
},
aml: {
passed: aml?.passed || false,
lastCheck: aml?.lastCheck,
riskLevel: aml?.riskLevel || "LOW",
},
valid: !!(identity?.lei && identity?.did && kyc?.verified && aml?.passed),
};
} catch (error) {
console.error("Compliance check failed:", error);
return {
valid: false,
};
}
}
/**
* Get compliance data for ISO message injection
*/
export async function getComplianceData(creator: string): Promise<ComplianceStatus> {
return await getComplianceStatus(creator);
}
/**
* Validate workflow compliance requirements
*/
export async function validateWorkflowCompliance(plan: Plan): Promise<{
valid: boolean;
required: string[];
missing: string[];
warnings: string[];
}> {
const status = await getComplianceStatus(plan.creator);
const hasFiatSteps = plan.steps.some((s) => s.type === "pay" || s.type === "repay");
const required: string[] = [];
const missing: string[] = [];
const warnings: string[] = [];
if (hasFiatSteps) {
required.push("LEI", "DID", "KYC", "AML");
if (!status.lei) missing.push("LEI");
if (!status.did) missing.push("DID");
if (!status.kyc?.verified) missing.push("KYC");
if (!status.aml?.passed) missing.push("AML");
// Check KYC expiration
if (status.kyc?.expiresAt) {
const expiresAt = new Date(status.kyc.expiresAt);
if (expiresAt < new Date()) {
warnings.push("KYC has expired");
} else if (expiresAt < new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)) {
warnings.push("KYC expires within 30 days");
}
}
}
return {
valid: missing.length === 0,
required,
missing,
warnings,
};
}

View File

@@ -0,0 +1,77 @@
import type { Plan } from "../types/plan";
/**
* Prepare DLT execution (2PC prepare phase)
* Reserves collateral and locks amounts
*/
export async function prepareDLTExecution(plan: Plan): Promise<boolean> {
// Mock: In real implementation, this would call the handler contract's prepare() function
// For now, simulate preparation
console.log(`[DLT] Preparing execution for plan ${plan.plan_id}`);
// Simulate async preparation
await new Promise((resolve) => setTimeout(resolve, 100));
return true;
}
/**
* Commit DLT execution (2PC commit phase)
* Executes all DLT steps atomically
*/
export async function commitDLTExecution(plan: Plan): Promise<{
success: boolean;
txHash?: string;
error?: string;
}> {
console.log(`[DLT] Committing execution for plan ${plan.plan_id}`);
try {
// Mock: In real implementation, this would:
// 1. Call handler contract's executeCombo() function
// 2. Wait for transaction confirmation
// 3. Return transaction hash
const txHash = `0x${Math.random().toString(16).substr(2, 64)}`;
// Simulate execution delay
await new Promise((resolve) => setTimeout(resolve, 500));
return {
success: true,
txHash,
};
} catch (error: any) {
return {
success: false,
error: error.message,
};
}
}
/**
* Abort DLT execution (2PC abort phase)
* Releases reserved collateral and unlocks amounts
*/
export async function abortDLTExecution(planId: string): Promise<void> {
console.log(`[DLT] Aborting execution for plan ${planId}`);
// Mock: In real implementation, this would call handler contract's abort() function
// to release any reserved resources
await new Promise((resolve) => setTimeout(resolve, 100));
}
/**
* Get DLT execution status
*/
export async function getDLTStatus(planId: string): Promise<{
status: string;
txHash?: string;
blockNumber?: number;
}> {
// Mock implementation
return {
status: "pending",
};
}

View File

@@ -0,0 +1,202 @@
import { EventEmitter } from "events";
import { getPlanById, updatePlanStatus } from "../db/plans";
import { prepareDLTExecution, commitDLTExecution, abortDLTExecution } from "./dlt";
import { prepareBankInstruction, commitBankInstruction, abortBankInstruction } from "./bank";
import { registerPlan, finalizePlan } from "./notary";
import type { PlanStatusEvent } from "../types/execution";
export class ExecutionCoordinator extends EventEmitter {
private executions: Map<string, {
planId: string;
status: string;
phase: string;
startedAt: Date;
error?: string;
}> = new Map();
/**
* Execute a plan using 2PC (two-phase commit) pattern
*/
async executePlan(planId: string): Promise<{ executionId: string }> {
const executionId = `exec-${Date.now()}`;
this.executions.set(executionId, {
planId,
status: "pending",
phase: "prepare",
startedAt: new Date(),
});
this.emitStatus(executionId, {
phase: "prepare",
status: "in_progress",
timestamp: new Date().toISOString(),
});
try {
// Get plan
const plan = await getPlanById(planId);
if (!plan) {
throw new Error("Plan not found");
}
// PHASE 1: PREPARE
await this.preparePhase(executionId, plan);
// PHASE 2: EXECUTE DLT
await this.executeDLTPhase(executionId, plan);
// PHASE 3: BANK INSTRUCTION
await this.bankInstructionPhase(executionId, plan);
// PHASE 4: COMMIT
await this.commitPhase(executionId, plan);
this.emitStatus(executionId, {
phase: "complete",
status: "complete",
timestamp: new Date().toISOString(),
});
await updatePlanStatus(planId, "complete");
return { executionId };
} catch (error: any) {
// Rollback on error
await this.abortExecution(executionId, planId, error.message);
throw error;
}
}
private async preparePhase(executionId: string, plan: any) {
this.emitStatus(executionId, {
phase: "prepare",
status: "in_progress",
timestamp: new Date().toISOString(),
});
// Prepare DLT execution
const dltPrepared = await prepareDLTExecution(plan);
if (!dltPrepared) {
throw new Error("DLT preparation failed");
}
// Prepare bank instruction (provisional)
const bankPrepared = await prepareBankInstruction(plan);
if (!bankPrepared) {
await abortDLTExecution(plan.plan_id);
throw new Error("Bank preparation failed");
}
// Register plan with notary
await registerPlan(plan);
this.emitStatus(executionId, {
phase: "prepare",
status: "complete",
timestamp: new Date().toISOString(),
});
}
private async executeDLTPhase(executionId: string, plan: any) {
this.emitStatus(executionId, {
phase: "execute_dlt",
status: "in_progress",
timestamp: new Date().toISOString(),
});
const result = await commitDLTExecution(plan);
if (!result.success) {
await abortDLTExecution(plan.plan_id);
await abortBankInstruction(plan.plan_id);
throw new Error("DLT execution failed: " + result.error);
}
this.emitStatus(executionId, {
phase: "execute_dlt",
status: "complete",
dltTxHash: result.txHash,
timestamp: new Date().toISOString(),
});
}
private async bankInstructionPhase(executionId: string, plan: any) {
this.emitStatus(executionId, {
phase: "bank_instruction",
status: "in_progress",
timestamp: new Date().toISOString(),
});
const result = await commitBankInstruction(plan);
if (!result.success) {
// DLT already committed, need to handle rollback
throw new Error("Bank instruction failed: " + result.error);
}
this.emitStatus(executionId, {
phase: "bank_instruction",
status: "complete",
isoMessageId: result.isoMessageId,
timestamp: new Date().toISOString(),
});
}
private async commitPhase(executionId: string, plan: any) {
this.emitStatus(executionId, {
phase: "commit",
status: "in_progress",
timestamp: new Date().toISOString(),
});
// Finalize with notary
await finalizePlan(plan.plan_id, {
dltTxHash: "mock-tx-hash",
isoMessageId: "mock-iso-id",
});
this.emitStatus(executionId, {
phase: "commit",
status: "complete",
timestamp: new Date().toISOString(),
});
}
async abortExecution(executionId: string, planId: string, error: string) {
const execution = this.executions.get(executionId);
if (!execution) return;
try {
// Abort DLT
await abortDLTExecution(planId);
// Abort bank
await abortBankInstruction(planId);
await updatePlanStatus(planId, "aborted");
this.emitStatus(executionId, {
phase: "aborted",
status: "failed",
error,
timestamp: new Date().toISOString(),
});
} catch (abortError: any) {
console.error("Abort failed:", abortError);
}
}
async getExecutionStatus(executionId: string) {
return this.executions.get(executionId);
}
private emitStatus(executionId: string, event: PlanStatusEvent) {
this.emit("status", executionId, event);
}
onStatus(callback: (executionId: string, event: PlanStatusEvent) => void) {
this.on("status", callback);
}
}
export const executionCoordinator = new ExecutionCoordinator();

View File

@@ -0,0 +1,179 @@
import type { Plan } from "../types/plan";
import { getComplianceData } from "./compliance";
/**
* Generate ISO-20022 pacs.008 (Customer Credit Transfer) message
*/
export async function generatePacs008(plan: Plan): Promise<string> {
const complianceData = await getComplianceData(plan.creator);
// Find pay step
const payStep = plan.steps.find((s) => s.type === "pay");
if (!payStep || payStep.type !== "pay") {
throw new Error("Plan must contain a pay step");
}
const isoMessage = {
Document: {
"@xmlns": "urn:iso:std:iso:20022:tech:xsd:pacs.008.001.10",
"@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
CstmrCdtTrfInitn: {
GrpHdr: {
MsgId: `MSG-${plan.plan_id}`,
CreDtTm: new Date().toISOString(),
NbOfTxs: "1",
CtrlSum: payStep.amount.toString(),
InitgPty: {
Nm: complianceData?.lei || "Unknown",
Id: {
OrgId: {
Othr: {
Id: complianceData?.lei || "",
SchmeNm: {
Cd: "LEI",
},
},
},
},
},
},
PmtInf: {
PmtInfId: `PMT-${plan.plan_id}`,
PmtMtd: "TRF",
NbOfTxs: "1",
CtrlSum: payStep.amount.toString(),
PmtTpInf: {
SvcLvl: {
Cd: "SEPA",
},
},
ReqdExctnDt: new Date().toISOString().split("T")[0],
Dbtr: {
Nm: complianceData?.lei || "Unknown",
Id: {
OrgId: {
Othr: {
Id: complianceData?.lei || "",
},
},
},
},
DbtrAcct: {
Id: {
IBAN: "DE89370400440532013000", // Mock
},
},
DbtrAgt: {
FinInstnId: {
BICFI: "DEUTDEFF", // Mock
},
},
CdtTrfTxInf: {
PmtId: {
InstrId: `INSTR-${plan.plan_id}`,
EndToEndId: plan.plan_id,
},
Amt: {
InstdAmt: {
"@Ccy": payStep.asset,
"#text": payStep.amount.toString(),
},
},
CdtrAgt: {
FinInstnId: {
BICFI: payStep.beneficiary.BIC || "UNKNOWN",
},
},
Cdtr: {
Nm: payStep.beneficiary.name || "Unknown",
},
CdtrAcct: {
Id: {
IBAN: payStep.beneficiary.IBAN || "",
},
},
RmtInf: {
Ustrd: `Plan ID: ${plan.plan_id}, Plan Hash: ${plan.plan_hash}`,
},
SplmtryData: {
PlcAndNm: "ComplianceData",
Envlp: {
Compl: {
LEI: complianceData?.lei || "",
DID: complianceData?.did || "",
KYC: {
Level: complianceData?.kyc?.level || 0,
Verified: complianceData?.kyc?.verified || false,
},
AML: {
Passed: complianceData?.aml?.passed || false,
RiskLevel: complianceData?.aml?.riskLevel || "UNKNOWN",
},
},
},
},
},
},
},
},
};
// Convert to XML string (simplified - in production use proper XML builder)
return JSON.stringify(isoMessage, null, 2);
}
/**
* Generate ISO-20022 camt.052 (Bank Statement) message
*/
export async function generateCamt052(planId: string, accountId: string): Promise<string> {
// Mock implementation
return JSON.stringify({
Document: {
"@xmlns": "urn:iso:std:iso:20022:tech:xsd:camt.052.001.10",
BkToCstmrAcctRpt: {
GrpHdr: {
MsgId: `MSG-${planId}`,
CreDtTm: new Date().toISOString(),
},
Rpt: {
Id: `RPT-${planId}`,
Acct: {
Id: {
IBAN: accountId,
},
},
},
},
},
});
}
/**
* Generate ISO-20022 camt.056 (Cancellation Request) message
*/
export async function generateCamt056(planId: string, originalMessageId: string): Promise<string> {
// Mock implementation
return JSON.stringify({
Document: {
"@xmlns": "urn:iso:std:iso:20022:tech:xsd:camt.056.001.10",
CstmrPmtCxlReq: {
Assgnmt: {
Id: `ASSGN-${planId}`,
},
Case: {
Id: `CASE-${planId}`,
Cretr: {
Nm: "Orchestrator",
},
},
Undrlyg: {
TxInf: {
OrgnlInstrId: originalMessageId,
OrgnlEndToEndId: planId,
},
},
},
},
});
}

View File

@@ -0,0 +1,78 @@
import { createHash } from "crypto";
import type { Plan } from "../types/plan";
/**
* Register plan with notary service
* Stores plan hash and metadata for audit trail
*/
export async function registerPlan(plan: Plan): Promise<{
notaryProof: string;
registeredAt: string;
}> {
console.log(`[Notary] Registering plan ${plan.plan_id}`);
// Compute plan hash
const planHash = createHash("sha256")
.update(JSON.stringify(plan))
.digest("hex");
// Mock: In real implementation, this would:
// 1. Call NotaryRegistry contract's registerPlan() function
// 2. Store plan hash, metadata, timestamp
// 3. Get notary signature/proof
const notaryProof = `0x${createHash("sha256")
.update(planHash + "notary-secret")
.digest("hex")}`;
return {
notaryProof,
registeredAt: new Date().toISOString(),
};
}
/**
* Finalize plan with execution results
* Records final execution state and receipts
*/
export async function finalizePlan(
planId: string,
results: {
dltTxHash?: string;
isoMessageId?: string;
}
): Promise<{
receiptId: string;
finalizedAt: string;
}> {
console.log(`[Notary] Finalizing plan ${planId}`);
// Mock: In real implementation, this would:
// 1. Call NotaryRegistry contract's finalizePlan() function
// 2. Store execution results, receipts
// 3. Get final notary proof
const receiptId = `receipt-${planId}-${Date.now()}`;
return {
receiptId,
finalizedAt: new Date().toISOString(),
};
}
/**
* Get notary proof for a plan
*/
export async function getNotaryProof(planId: string): Promise<{
planHash: string;
notaryProof: string;
registeredAt: string;
} | null> {
// Mock implementation
return {
planHash: `0x${Math.random().toString(16).substr(2, 64)}`,
notaryProof: `0x${Math.random().toString(16).substr(2, 64)}`,
registeredAt: new Date().toISOString(),
};
}

View File

@@ -0,0 +1,125 @@
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;
}
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 { asset: step.asset, amount: step.amount };
case "swap":
return { asset: step.to, amount: step.amount };
default:
return null;
}
}

View File

@@ -0,0 +1,79 @@
import type { Plan } from "../types/plan";
import { getNotaryProof } from "./notary";
import { getDLTStatus } from "./dlt";
export interface Receipt {
receiptId: string;
planId: string;
planHash: string;
dltTransaction?: {
txHash: string;
blockNumber: number;
timestamp: string;
};
isoMessage?: {
messageId: string;
messageType: string;
timestamp: string;
};
notaryProof?: {
proof: string;
registeredAt: string;
finalizedAt?: string;
};
status: string;
createdAt: string;
}
/**
* Generate receipt for a plan execution
*/
export async function generateReceipt(plan: Plan): Promise<Receipt> {
const notaryProof = await getNotaryProof(plan.plan_id);
const dltStatus = await getDLTStatus(plan.plan_id);
const receipt: Receipt = {
receiptId: `receipt-${plan.plan_id}-${Date.now()}`,
planId: plan.plan_id,
planHash: plan.plan_hash || "",
status: "complete",
createdAt: new Date().toISOString(),
};
if (dltStatus.txHash) {
receipt.dltTransaction = {
txHash: dltStatus.txHash,
blockNumber: dltStatus.blockNumber || 0,
timestamp: new Date().toISOString(),
};
}
// Find pay step to get ISO message ID
const payStep = plan.steps.find((s) => s.type === "pay");
if (payStep) {
receipt.isoMessage = {
messageId: `MSG-${plan.plan_id}`,
messageType: "pacs.008",
timestamp: new Date().toISOString(),
};
}
if (notaryProof) {
receipt.notaryProof = {
proof: notaryProof.notaryProof,
registeredAt: notaryProof.registeredAt,
};
}
return receipt;
}
/**
* Get all receipts for a plan
*/
export async function getPlanReceipts(planId: string): Promise<Receipt[]> {
// Mock: In real implementation, this would query database
// For now, return empty array or mock data
return [];
}

View File

@@ -0,0 +1,10 @@
export interface PlanStatusEvent {
phase: string;
status: "pending" | "in_progress" | "complete" | "failed";
planId?: string;
dltTxHash?: string;
isoMessageId?: string;
error?: string;
timestamp: string;
}

View File

@@ -0,0 +1,26 @@
export interface Plan {
plan_id?: string;
creator: string;
steps: PlanStep[];
maxRecursion?: number;
maxLTV?: number;
signature?: string;
plan_hash?: string;
created_at?: string;
status?: string;
}
export interface PlanStep {
type: "borrow" | "swap" | "repay" | "pay";
asset?: string;
amount: number;
from?: string;
to?: string;
collateralRef?: string;
beneficiary?: {
IBAN?: string;
BIC?: string;
name?: string;
};
}