@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
JavaScript
/**
* 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