UNPKG

dop-stick

Version:

Source control tooling for versionable-upgradeable smart contracts

498 lines 24.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.UpgradeProcessor = void 0; const diamond_1 = require("../types/diamond"); const logger_1 = require("./logsAndMetrics/core/logger"); const batchProcessor_1 = require("./batchProcessor"); const ethers_1 = require("ethers"); const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const readline_1 = __importDefault(require("readline")); const terminal_1 = require("./logsAndMetrics/core/terminal"); const parallelBatchLogAdapter_1 = require("./logsAndMetrics/adapters/parallelBatchLogAdapter"); const batchLogAdapter_1 = require("./logsAndMetrics/adapters/batchLogAdapter"); const validationTimelineAdapter_1 = require("./logsAndMetrics/adapters/validationTimelineAdapter"); const executionSummaryAdapter_1 = require("./logsAndMetrics/adapters/executionSummaryAdapter"); /** * UpgradeProcessor handles the processing and validation of diamond contract upgrades. * It manages the upgrade workflow including file loading, validation, and preparation of diamond cuts. */ class UpgradeProcessor { /** * Creates a new UpgradeProcessor instance * @param config - Configuration object containing upgrade settings and modes */ constructor(config) { var _a; this.config = config; this.lastValidatedModules = []; this.batchProcessor = new batchProcessor_1.BatchProcessor(config); this.mode = (_a = config.mode) !== null && _a !== void 0 ? _a : 'copilot-beta'; this.parallelBatchLogger = new parallelBatchLogAdapter_1.ParallelBatchLogAdapter(); this.validationTimeline = new validationTimelineAdapter_1.ValidationTimelineAdapter(this.mode); this.executionSummary = new executionSummaryAdapter_1.ExecutionSummaryAdapter(this.mode); } /** * Loads and validates the upgrade configuration file * @returns Promise resolving to parsed and validated upgrade configuration * @throws Error if file is not found or has invalid format */ async loadUpgradeFile() { var _a; try { // Get file path from config or use default const upgradeFilePath = ((_a = this.config.paths) === null || _a === void 0 ? void 0 : _a.upgrades) ? path_1.default.resolve(process.cwd(), this.config.paths.upgrades) : path_1.default.join(process.cwd(), 'upgrade.json'); if (!fs_1.default.existsSync(upgradeFilePath)) { throw new Error(`Upgrade file not found at: ${upgradeFilePath}`); } const fileContent = fs_1.default.readFileSync(upgradeFilePath, 'utf8'); const parsedConfig = JSON.parse(fileContent); // Create upgrade config, checking for modules first const upgradeConfig = { // If modules exists, use it; if not, check for upgrades; if neither exists, use empty array modules: 'modules' in parsedConfig ? parsedConfig.modules : (parsedConfig.upgrades || []), fail_all_if_one_fails: parsedConfig.fail_all_if_one_fails || false, add_or_replace: parsedConfig.add_or_replace || false }; // Log a warning if using legacy format if (!('modules' in parsedConfig) && 'upgrades' in parsedConfig) { logger_1.Logger.warning('Using legacy "upgrades" field. Please update your config to use "modules" instead.'); } // Validate the structure if (!upgradeConfig.modules || !Array.isArray(upgradeConfig.modules)) { throw new Error('Invalid upgrade file format: missing or invalid modules array'); } // Validate each entry upgradeConfig.modules.forEach((entry, index) => { var _a, _b, _c; if (typeof entry === 'string') { // String format is valid as is if (this.config.mode === 'strict' || this.config.mode === 'basic') { throw new Error(`Simple module format (${entry}) is only supported in copilot-beta and auto-pilot-beta modes`); } return; } // Object format validation if (!entry.module) { throw new Error(`Invalid module at index ${index}: missing module name`); } // Ensure at least one action is defined for object format if (!((_a = entry.add) === null || _a === void 0 ? void 0 : _a.length) && !((_b = entry.replace) === null || _b === void 0 ? void 0 : _b.length) && !((_c = entry.remove) === null || _c === void 0 ? void 0 : _c.length)) { throw new Error(`Module ${entry.module} has no functions defined`); } // Validate arrays if they exist if (entry.add && !Array.isArray(entry.add)) { throw new Error(`Invalid module ${entry.module}: 'add' must be an array`); } if (entry.replace && !Array.isArray(entry.replace)) { throw new Error(`Invalid module ${entry.module}: 'replace' must be an array`); } if (entry.remove && !Array.isArray(entry.remove)) { throw new Error(`Invalid module ${entry.module}: 'remove' must be an array`); } }); return upgradeConfig; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger_1.Logger.error(`Failed to load upgrade file: ${errorMessage}`); throw error; } } /** * Processes upgrades using the batch processing system * @param upgradeConfig - Configuration containing upgrade specifications * @returns Object containing processed modules, actions, selectors, and final cuts * @throws Error if validation fails in strict mode */ async processUpgrades(upgradeConfig) { try { const batches = await this.createBatches(upgradeConfig); const validatedModules = await this.validateBatches(batches); this.lastValidatedModules = validatedModules; const shouldProceed = await this.validationTimeline.processValidationResults(validatedModules); if (!shouldProceed) { throw new Error('Upgrade cancelled by user'); } const { individualCuts, groupedCuts } = this.prepareFinalCuts(validatedModules); // Display execution summary and wait for confirmation this.executionSummary.displayExecutionSummary(individualCuts); // Handle user interaction for module details and confirmation const proceed = await this.handleExecutionSummaryInteraction(); if (!proceed) { throw new Error('Upgrade cancelled by user'); } return { modules: groupedCuts.map(cut => cut.moduleName), actions: groupedCuts.map(cut => cut.action), selectors: groupedCuts.map(cut => cut.functionSelectors), finalCuts: groupedCuts, validatedModules: validatedModules }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); logger_1.Logger.error(`Failed to process upgrades: ${errorMessage}`); throw error; } } async createBatches(upgradeConfig, isParallel = false) { if (isParallel) { this.parallelBatchLogger.startBatchCreation(); } else { const batchLogger = batchLogAdapter_1.BatchLogAdapter.getInstance(); batchLogger.startBatchCreation(); } const batches = []; let success = true; // Original batch creation logic for (const entry of upgradeConfig.modules) { try { const moduleName = typeof entry === 'string' ? entry : entry.module; const batch = await this.batchProcessor.processUpgradeEntry(entry, this.mode); if (!isParallel) { const batchLogger = batchLogAdapter_1.BatchLogAdapter.getInstance(); batchLogger.logBatchSuccess(moduleName, batch); } batches.push(batch); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); const moduleName = typeof entry === 'string' ? entry : entry.module; if (!isParallel) { const batchLogger = batchLogAdapter_1.BatchLogAdapter.getInstance(); batchLogger.logBatchError(moduleName, errorMessage); } success = false; if (this.mode === 'strict') { throw error; } } } // Consolidate batches with same module name const consolidatedBatches = this.consolidateBatches(batches); if (!isParallel) { const batchLogger = batchLogAdapter_1.BatchLogAdapter.getInstance(); batchLogger.completeBatchCreation(success); } return consolidatedBatches; } /** * Consolidates multiple batches of the same module into a single batch * @param batches Original array of batches * @returns Consolidated array of batches with unique module names * @private */ consolidateBatches(batches) { const moduleMap = new Map(); for (const batch of batches) { if (!moduleMap.has(batch.moduleName)) { // First occurrence of this module moduleMap.set(batch.moduleName, { moduleName: batch.moduleName, functionSignatures: [...batch.functionSignatures], originalActions: new Map(batch.originalActions), suggestedActions: batch.suggestedActions ? new Map(batch.suggestedActions) : undefined }); } else { // Merge with existing module const existing = moduleMap.get(batch.moduleName); // Merge function signatures (unique) batch.functionSignatures.forEach(signature => { if (!existing.functionSignatures.includes(signature)) { existing.functionSignatures.push(signature); } }); // Merge original actions (later actions take precedence) batch.originalActions.forEach((action, signature) => { existing.originalActions.set(signature, action); }); // Merge suggested actions if they exist if (batch.suggestedActions) { if (!existing.suggestedActions) { existing.suggestedActions = new Map(); } batch.suggestedActions.forEach((action, signature) => { existing.suggestedActions.set(signature, action); }); } // Update in map moduleMap.set(batch.moduleName, existing); } } return Array.from(moduleMap.values()); } async validateBatches(batches, isParallel = false) { this.validationTimeline.startValidation(batches.length, isParallel); const results = []; for (const batch of batches) { try { const validationResult = await this.batchProcessor.validateBatch(batch, this.mode); results.push(validationResult); } catch (error) { // Handle validation errors... } } return results; } /** * Processes upgrades in parallel for improved performance * @param upgradeConfig - Configuration containing upgrade specifications * @returns Object containing processed modules and final cuts * @throws Error if parallel processing fails */ async processUpgradesParallel(upgradeConfig) { try { const batches = await this.createBatches(upgradeConfig, true); // Prepare batch summaries from the original batches const batchSummaries = batches.map(batch => ({ moduleName: batch.moduleName, totalFunctions: batch.functionSignatures.length, actions: { add: batch.functionSignatures.filter(sig => batch.originalActions.get(sig) === diamond_1.DiamondCutAction.Add).length, replace: batch.functionSignatures.filter(sig => batch.originalActions.get(sig) === diamond_1.DiamondCutAction.Replace).length, remove: batch.functionSignatures.filter(sig => batch.originalActions.get(sig) === diamond_1.DiamondCutAction.Remove).length } })); // Log the batch creation results this.parallelBatchLogger.logBatchResults(batchSummaries); // Start validation timeline and process modules this.validationTimeline.startValidation(batches.length, true); const validatedModules = await Promise.all(batches.map(batch => { const originalEntry = upgradeConfig.modules.find(entry => typeof entry === 'string' ? entry === batch.moduleName : entry.module === batch.moduleName); const isAutoDiscovered = typeof originalEntry === 'string'; return this.batchProcessor.validateBatch(batch, this.mode, isAutoDiscovered); })); // Process validation results const shouldProceed = await this.validationTimeline.processValidationResults(validatedModules); if (!shouldProceed) { throw new Error('Upgrade cancelled by user'); } const { individualCuts, groupedCuts } = this.prepareFinalCuts(validatedModules); // Display execution summary and wait for confirmation this.executionSummary.displayExecutionSummary(individualCuts); // Handle user interaction for module details and confirmation const proceed = await this.handleExecutionSummaryInteraction(); if (!proceed) { throw new Error('Upgrade cancelled by user'); } return { modules: groupedCuts.map(cut => cut.moduleName), actions: groupedCuts.map(cut => cut.action), selectors: groupedCuts.map(cut => cut.functionSelectors), finalCuts: groupedCuts, validatedModules: validatedModules }; } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); throw new Error(`Failed to process upgrades in parallel: ${errorMessage}`); } } /** * Prepares final diamond cuts from validated modules * @param validatedModules - Array of validation results for each module * @returns Object containing individual and grouped cuts * @private */ prepareFinalCuts(validatedModules) { const individualCuts = []; const groupedCutsMap = new Map(); for (const moduleResult of validatedModules) { // Track removed functions with their reasons const removedFunctionsWithReasons = [ ...(moduleResult.pendingMissingFunctions || []).map(fn => ({ signature: fn, reason: 'missing' })), ...moduleResult.pendingRemovals.map(removal => ({ signature: removal.signature, reason: 'invalid' })), ...(moduleResult.pendingDiscardedFunctions || []).map(discarded => ({ signature: discarded.signature, reason: 'discarded' })) ]; const removedFunctions = new Set(removedFunctionsWithReasons.map(f => f.signature)); // Add a special cut for removed functions if any exist if (removedFunctionsWithReasons.length > 0) { individualCuts.push({ facetAddress: ethers_1.ethers.constants.AddressZero, action: diamond_1.DiamondCutAction.Remove, functionSelectors: [], functionSignatures: [], moduleName: moduleResult.moduleName, removedFunctions: removedFunctionsWithReasons }); } // Process all original signatures except removed ones moduleResult.originalSignatures.forEach((originalAction, signature) => { if (removedFunctions.has(signature)) { return; // Skip removed functions } // Check if this signature has a suggested change from the suggestedChanges Map const suggestedAction = moduleResult.suggestedChanges.get(signature); const finalAction = suggestedAction !== null && suggestedAction !== void 0 ? suggestedAction : originalAction; const selector = ethers_1.ethers.utils.id(signature).slice(0, 10); // Add to individual cuts individualCuts.push({ facetAddress: ethers_1.ethers.constants.AddressZero, action: finalAction, functionSelectors: [selector], functionSignatures: [signature], changed: suggestedAction !== undefined && suggestedAction !== originalAction, originalAction: originalAction, moduleName: moduleResult.moduleName }); // Group by module and action for grouped cuts if (!groupedCutsMap.has(moduleResult.moduleName)) { groupedCutsMap.set(moduleResult.moduleName, new Map()); } const moduleGroupedCuts = groupedCutsMap.get(moduleResult.moduleName); if (!moduleGroupedCuts.has(finalAction)) { moduleGroupedCuts.set(finalAction, { facetAddress: ethers_1.ethers.constants.AddressZero, action: finalAction, functionSelectors: [], functionSignatures: [], moduleName: moduleResult.moduleName }); } const groupedCut = moduleGroupedCuts.get(finalAction); groupedCut.functionSelectors.push(selector); groupedCut.functionSignatures.push(signature); }); } // Convert grouped cuts map to array const groupedCuts = []; groupedCutsMap.forEach(moduleMap => { moduleMap.forEach(cut => { groupedCuts.push(cut); }); }); // console.log('\nFinal individual cuts:', individualCuts.map(cut => ({ // moduleName: cut.moduleName, // signature: cut.functionSignatures?.[0], // action: cut.action, // changed: cut.changed, // originalAction: cut.originalAction // }))); return { individualCuts, groupedCuts }; } async handleExecutionSummaryInteraction() { // Auto-proceed immediately in auto-pilot mode if (this.mode === 'auto-pilot-beta') { return true; } const rl = readline_1.default.createInterface({ input: process.stdin, output: process.stdout }); let autoPromptTimeout; try { while (true) { // Clear any existing timeout if (autoPromptTimeout) { clearTimeout(autoPromptTimeout); } // Set up auto-proceed timeout const timeoutPromise = new Promise(resolve => { autoPromptTimeout = setTimeout(() => { terminal_1.Terminal.writeLine('\n⏱ Auto-proceeding with upgrade after 5s of inactivity...'); resolve(true); }, 5000); }); // Get user input with timeout const inputPromise = new Promise(resolve => { rl.question('', resolve); }); // Race between timeout and user input const result = await Promise.race([timeoutPromise, inputPromise]); // If we got a boolean, it means the timeout won if (typeof result === 'boolean') { return true; } const answer = result.toLowerCase().trim(); // Handle different commands if (answer === 'y' || answer === 'proceed') { return true; } if (answer === 'n' || answer === 'exit') { return false; } if (answer === 'back') { this.executionSummary.displayExecutionSummary(this.prepareFinalCuts(await this.getValidatedModules()).individualCuts); continue; } if (answer.startsWith('show ')) { const moduleNumber = parseInt(answer.split(' ')[1]); const maxModules = this.executionSummary.getModuleCount(); if (isNaN(moduleNumber) || moduleNumber < 1 || moduleNumber > maxModules) { terminal_1.Terminal.writeLine('\n❌ Invalid module number'); terminal_1.Terminal.writeLine('Available modules:'); for (let i = 1; i <= maxModules; i++) { terminal_1.Terminal.writeLine(` ${i}. ${this.executionSummary.getModuleName(i)}`); } terminal_1.Terminal.writeLine(''); continue; } this.executionSummary.displayModuleDetails(moduleNumber); continue; } // Invalid command terminal_1.Terminal.writeLine('\n❌ Invalid command. Available commands:'); terminal_1.Terminal.writeLine(' • show <number> - Show details for a specific module'); terminal_1.Terminal.writeLine(' • back - Return to summary view'); terminal_1.Terminal.writeLine(' • proceed - Continue with upgrade'); terminal_1.Terminal.writeLine(' • exit - Cancel upgrade'); terminal_1.Terminal.writeLine('\nAuto-proceeding in 10 seconds if no command is entered...\n'); } } finally { if (autoPromptTimeout) { clearTimeout(autoPromptTimeout); } rl.close(); } } // Helper method to get validated modules async getValidatedModules() { return this.lastValidatedModules; } async extractModuleCuts(upgradeConfig) { const batches = await this.createBatches(upgradeConfig); const moduleCuts = []; for (const batch of batches) { // Create a cut for each module const cut = { facetAddress: ethers_1.ethers.constants.AddressZero, action: 0, functionSelectors: [], functionSignatures: batch.functionSignatures, moduleName: batch.moduleName, originalFacetName: batch.moduleName }; // Generate selectors from signatures cut.functionSelectors = batch.functionSignatures.map(sig => ethers_1.ethers.utils.id(sig).slice(0, 10)); moduleCuts.push(cut); } return moduleCuts; } } exports.UpgradeProcessor = UpgradeProcessor; //# sourceMappingURL=upgradeProcessor.js.map