UNPKG

@venly/wallet-mcp

Version:

Production-ready MCP server enabling AI agents to perform Web3 wallet operations through Venly's blockchain infrastructure. Supports 14+ networks including Ethereum, Polygon, Arbitrum, and stablecoin operations.

647 lines 24 kB
/** * Workflow Engine * * Central orchestration engine for multi-step financial workflows * with cross-MCP server coordination and robust error handling */ import winston from 'winston'; import { WorkflowStatus, WorkflowTriggerType } from '../types/venly.js'; import { VenlyClient } from '../venly/VenlyClient.js'; /** * Main workflow orchestration engine */ export class WorkflowEngine { logger; venlyClient; activeWorkflows = new Map(); templates = new Map(); mcpServers = new Map(); stats = { totalExecutions: 0, successfulExecutions: 0, failedExecutions: 0, averageExecutionTime: 0, activeExecutions: 0 }; constructor(venlyClient) { this.venlyClient = venlyClient; this.logger = this.createLogger(); this.logger.info('WorkflowEngine initialized'); } // ============================================================================ // Template Management // ============================================================================ /** * Register a workflow template */ registerTemplate(template) { this.templates.set(template.id, template); this.logger.info('Workflow template registered', { templateId: template.id, name: template.name, steps: template.steps.length, complexity: template.metadata.complexity }); } /** * Get workflow template by ID */ getTemplate(templateId) { return this.templates.get(templateId); } /** * List all available templates */ listTemplates() { return Array.from(this.templates.values()); } /** * Remove workflow template */ removeTemplate(templateId) { const removed = this.templates.delete(templateId); if (removed) { this.logger.info('Workflow template removed', { templateId }); } return removed; } // ============================================================================ // MCP Server Management // ============================================================================ /** * Register an MCP server configuration */ registerMCPServer(config) { this.mcpServers.set(config.name, config); this.logger.info('MCP server registered', { serverName: config.name, endpoint: config.endpoint, authType: config.authentication?.type }); } /** * Get MCP server configuration */ getMCPServer(serverName) { return this.mcpServers.get(serverName); } // ============================================================================ // Workflow Execution // ============================================================================ /** * Execute a workflow from template */ async executeWorkflow(templateId, inputs, triggeredBy = { type: WorkflowTriggerType.MANUAL }) { const template = this.templates.get(templateId); if (!template) { throw new Error(`Workflow template not found: ${templateId}`); } if (!template.isActive) { throw new Error(`Workflow template is inactive: ${templateId}`); } const executionId = this.generateExecutionId(); const startTime = new Date(); const execution = { id: executionId, templateId, status: WorkflowStatus.PENDING, currentStep: 0, totalSteps: template.steps.length, startTime, results: [], inputs, triggeredBy, progress: { percentage: 0, completedSteps: 0, failedSteps: 0, skippedSteps: 0 } }; this.activeWorkflows.set(executionId, execution); this.stats.totalExecutions++; this.stats.activeExecutions++; this.logger.info('Starting workflow execution', { executionId, templateId, templateName: template.name, totalSteps: template.steps.length, triggeredBy }); try { execution.status = WorkflowStatus.RUNNING; await this.executeWorkflowSteps(execution, template); execution.status = WorkflowStatus.COMPLETED; execution.endTime = new Date(); this.stats.successfulExecutions++; this.logger.info('Workflow execution completed successfully', { executionId, duration: execution.endTime.getTime() - execution.startTime.getTime(), completedSteps: execution.progress.completedSteps }); } catch (error) { execution.status = WorkflowStatus.FAILED; execution.endTime = new Date(); execution.error = error instanceof Error ? error.message : 'Unknown error'; this.stats.failedExecutions++; this.logger.error('Workflow execution failed', { executionId, error: execution.error, currentStep: execution.currentStep }); } finally { this.stats.activeExecutions--; this.updateAverageExecutionTime(execution); } return execution; } /** * Execute workflow steps sequentially with dependency management */ async executeWorkflowSteps(execution, template) { const context = { executionId: execution.id, templateId: execution.templateId, inputs: execution.inputs, variables: {}, stepResults: new Map(), currentStep: 0, metadata: {} }; // Build dependency graph (for future use in complex dependency resolution) // const _dependencyGraph = this.buildDependencyGraph(template.steps); const executedSteps = new Set(); const parallelSteps = []; for (let i = 0; i < template.steps.length; i++) { const step = template.steps[i]; if (!step) continue; // Check if dependencies are satisfied if (step.dependsOn && !step.dependsOn.every(dep => executedSteps.has(dep))) { this.logger.debug('Step dependencies not satisfied, skipping for now', { stepId: step.id, dependsOn: step.dependsOn, executed: Array.from(executedSteps) }); continue; } execution.currentStep = i; context.currentStep = i; if (step.parallel) { // Execute step in parallel parallelSteps.push(this.executeStepWithTracking(step, context, execution)); } else { // Wait for any parallel steps to complete before continuing if (parallelSteps.length > 0) { await Promise.all(parallelSteps); parallelSteps.length = 0; } // Execute step sequentially await this.executeStepWithTracking(step, context, execution); } executedSteps.add(step.id); } // Wait for any remaining parallel steps if (parallelSteps.length > 0) { await Promise.all(parallelSteps); } } /** * Execute a single step with result tracking */ async executeStepWithTracking(step, context, execution) { const stepResult = { stepId: step.id, status: 'PENDING', startTime: new Date(), retryCount: 0, logs: [] }; execution.results.push(stepResult); context.stepResults.set(step.id, stepResult); try { stepResult.status = 'RUNNING'; stepResult.logs.push(`Starting step: ${step.name}`); // Check step conditions if (step.conditions && !this.evaluateConditions(step.conditions, context)) { stepResult.status = 'SKIPPED'; stepResult.logs.push('Step conditions not met, skipping'); execution.progress.skippedSteps++; return; } // Execute the step const result = await this.executeStep(step, context); stepResult.status = 'COMPLETED'; stepResult.endTime = new Date(); stepResult.result = result; stepResult.logs.push('Step completed successfully'); execution.progress.completedSteps++; this.logger.debug('Step executed successfully', { stepId: step.id, stepName: step.name, duration: stepResult.endTime.getTime() - stepResult.startTime.getTime() }); } catch (error) { stepResult.status = 'FAILED'; stepResult.endTime = new Date(); stepResult.error = error instanceof Error ? error.message : 'Unknown error'; stepResult.logs.push(`Step failed: ${stepResult.error}`); execution.progress.failedSteps++; this.logger.error('Step execution failed', { stepId: step.id, stepName: step.name, error: stepResult.error }); // Check if we should retry if (step.retryPolicy && stepResult.retryCount < step.retryPolicy.maxRetries) { await this.retryStep(step, context, stepResult); } else { throw error; } } finally { // Update progress percentage execution.progress.percentage = Math.round(((execution.progress.completedSteps + execution.progress.skippedSteps) / execution.totalSteps) * 100); } } /** * Execute a single workflow step */ async executeStep(step, context) { this.logger.debug('Executing workflow step', { stepId: step.id, stepName: step.name, stepType: step.type, mcpServer: step.mcpServer }); switch (step.type) { case 'VENLY_TRANSACTION': return this.executeVenlyTransaction(step, context); case 'FIAT_CONVERSION': return this.executeFiatConversion(step, context); case 'EXTERNAL_MCP_CALL': return this.handleCrossMCPCall(step.mcpServer, step.action, step.parameters); case 'WEBHOOK_TRIGGER': return this.executeWebhookTrigger(step, context); case 'CONDITION_CHECK': return this.executeConditionCheck(step, context); case 'DATA_EXPORT': return this.executeDataExport(step, context); default: throw new Error(`Unsupported step type: ${step.type}`); } } // ============================================================================ // Step Type Implementations // ============================================================================ /** * Execute Venly transaction step */ async executeVenlyTransaction(step, _context) { const { action } = step; // Note: WorkflowEngine needs to be updated to handle user credentials // For now, we'll throw an error indicating this functionality needs user credentials throw new Error(`Venly ${action} requires user credentials. WorkflowEngine needs to be updated to support user-specific authentication.`); } /** * Execute fiat conversion step */ async executeFiatConversion(step, _context) { const { action } = step; // Note: WorkflowEngine needs to be updated to handle user credentials // For now, we'll throw an error indicating this functionality needs user credentials throw new Error(`Fiat ${action} requires user credentials. WorkflowEngine needs to be updated to support user-specific authentication.`); } /** * Execute webhook trigger step */ async executeWebhookTrigger(step, _context) { const { action, parameters } = step; switch (action) { case 'setup_transaction_webhooks': case 'setup_balance_webhooks': case 'setup_portfolio_webhooks': return this.venlyClient.setupWebhook(parameters); default: throw new Error(`Unsupported webhook action: ${action}`); } } /** * Execute condition check step */ async executeConditionCheck(step, context) { const { parameters } = step; const conditions = parameters['conditions']; if (!conditions || conditions.length === 0) { return true; } return this.evaluateConditions(conditions, context); } /** * Execute data export step */ async executeDataExport(step, _context) { // This would integrate with the FinancialDataExporter // For now, we'll simulate the export process const { parameters } = step; return { exportId: `export_${Date.now()}`, format: parameters['format'] || 'JSON', status: 'COMPLETED', downloadUrl: `https://api.venly.io/exports/export_${Date.now()}`, createdAt: new Date().toISOString() }; } // ============================================================================ // Cross-MCP Integration // ============================================================================ /** * Handle cross-MCP server calls */ async handleCrossMCPCall(serverName, action, parameters) { const startTime = Date.now(); const serverConfig = this.mcpServers.get(serverName); if (!serverConfig) { throw new Error(`MCP server not configured: ${serverName}`); } this.logger.info('Executing cross-MCP call', { server: serverName, action, endpoint: serverConfig.endpoint }); try { // For now, we'll simulate cross-MCP calls // In a real implementation, this would make actual HTTP calls to MCP servers const result = await this.simulateMCPCall(serverName, action, parameters); const duration = Date.now() - startTime; const callResult = { server: serverName, action, success: true, result, duration, timestamp: new Date().toISOString() }; this.logger.info('Cross-MCP call completed successfully', { server: serverName, action, duration }); return callResult; } catch (error) { const duration = Date.now() - startTime; const errorMessage = error instanceof Error ? error.message : 'Unknown error'; this.logger.error('Cross-MCP call failed', { server: serverName, action, error: errorMessage, duration }); throw error; } } /** * Simulate MCP server calls (for demonstration) */ async simulateMCPCall(serverName, action, parameters) { // Simulate network delay await new Promise(resolve => setTimeout(resolve, Math.random() * 1000 + 500)); switch (serverName) { case 'stripe': return this.simulateStripeCall(action, parameters); case 'paypal': return this.simulatePayPalCall(action, parameters); case 'modern_treasury': return this.simulateModernTreasuryCall(action, parameters); default: throw new Error(`Unsupported MCP server: ${serverName}`); } } /** * Simulate Stripe MCP calls */ simulateStripeCall(action, parameters) { switch (action) { case 'create_invoice': return { id: `inv_${Date.now()}`, amount: parameters['amount'] || 1000, currency: parameters['currency'] || 'usd', status: 'open', hosted_invoice_url: `https://invoice.stripe.com/i/acct_test/test_${Date.now()}` }; case 'create_payment_intent': return { id: `pi_${Date.now()}`, amount: parameters['amount'] || 1000, currency: parameters['currency'] || 'usd', status: 'requires_payment_method' }; default: throw new Error(`Unsupported Stripe action: ${action}`); } } /** * Simulate PayPal MCP calls */ simulatePayPalCall(action, parameters) { switch (action) { case 'create_order': return { id: `ORDER_${Date.now()}`, status: 'CREATED', amount: { currency_code: parameters['currency'] || 'USD', value: parameters['amount'] || '10.00' } }; default: throw new Error(`Unsupported PayPal action: ${action}`); } } /** * Simulate Modern Treasury MCP calls */ simulateModernTreasuryCall(action, parameters) { switch (action) { case 'record_payment': return { id: `payment_${Date.now()}`, amount: parameters['amount'] || 1000, currency: parameters['currency'] || 'USD', status: 'pending' }; case 'create_ledger_entry': return { id: `ledger_${Date.now()}`, amount: parameters['amount'] || 1000, currency: parameters['currency'] || 'USD', status: 'posted' }; default: throw new Error(`Unsupported Modern Treasury action: ${action}`); } } // ============================================================================ // Workflow Management // ============================================================================ /** * Get workflow execution by ID */ getWorkflowExecution(executionId) { return this.activeWorkflows.get(executionId); } /** * List all active workflow executions */ listActiveWorkflows() { return Array.from(this.activeWorkflows.values()); } /** * Cancel workflow execution */ async cancelWorkflow(executionId) { const execution = this.activeWorkflows.get(executionId); if (!execution) { return false; } execution.status = WorkflowStatus.CANCELLED; execution.endTime = new Date(); this.logger.info('Workflow execution cancelled', { executionId }); return true; } /** * Get workflow execution statistics */ getWorkflowStats() { return { ...this.stats }; } // ============================================================================ // Private Helper Methods // ============================================================================ /** * Generate unique execution ID */ generateExecutionId() { return `exec_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } /** * Evaluate workflow conditions */ evaluateConditions(conditions, context) { let result = true; let currentLogicalOperator = 'AND'; for (const condition of conditions) { const conditionResult = this.evaluateCondition(condition, context); if (currentLogicalOperator === 'AND') { result = result && conditionResult; } else { result = result || conditionResult; } currentLogicalOperator = condition.logicalOperator || 'AND'; } return result; } /** * Evaluate single condition */ evaluateCondition(condition, context) { const fieldValue = this.getFieldValue(condition.field, context); switch (condition.operator) { case 'equals': return fieldValue === condition.value; case 'greater_than': return Number(fieldValue) > Number(condition.value); case 'less_than': return Number(fieldValue) < Number(condition.value); case 'contains': return String(fieldValue).includes(String(condition.value)); case 'not_equals': return fieldValue !== condition.value; default: return false; } } /** * Get field value from context */ getFieldValue(field, context) { // Support dot notation for nested fields const parts = field.split('.'); let value = context; for (const part of parts) { if (value && typeof value === 'object' && part in value) { value = value[part]; } else { return undefined; } } return value; } /** * Retry failed step */ async retryStep(step, context, stepResult) { const retryPolicy = step.retryPolicy; stepResult.retryCount++; const delay = Math.min(retryPolicy.baseDelay * Math.pow(retryPolicy.backoffMultiplier, stepResult.retryCount - 1), retryPolicy.maxDelay); this.logger.info('Retrying failed step', { stepId: step.id, retryCount: stepResult.retryCount, delay }); await new Promise(resolve => setTimeout(resolve, delay)); try { const result = await this.executeStep(step, context); stepResult.status = 'COMPLETED'; stepResult.result = result; stepResult.error = undefined; stepResult.logs.push(`Step completed successfully on retry ${stepResult.retryCount}`); } catch (error) { stepResult.error = error instanceof Error ? error.message : 'Unknown error'; stepResult.logs.push(`Retry ${stepResult.retryCount} failed: ${stepResult.error}`); if (stepResult.retryCount < retryPolicy.maxRetries) { await this.retryStep(step, context, stepResult); } else { stepResult.status = 'FAILED'; throw error; } } } /** * Update average execution time */ updateAverageExecutionTime(execution) { if (!execution.endTime) return; const executionTime = execution.endTime.getTime() - execution.startTime.getTime(); const totalTime = this.stats.averageExecutionTime * (this.stats.totalExecutions - 1); this.stats.averageExecutionTime = (totalTime + executionTime) / this.stats.totalExecutions; } /** * Create Winston logger instance */ createLogger() { return winston.createLogger({ level: process.env['LOG_LEVEL'] || 'info', format: winston.format.combine(winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json()), defaultMeta: { service: 'workflow-engine' }, transports: [ new winston.transports.Console({ format: winston.format.combine(winston.format.colorize(), winston.format.simple()) }) ] }); } } //# sourceMappingURL=WorkflowEngine.js.map