dop-stick
Version:
Source control tooling for versionable-upgradeable smart contracts
498 lines • 24.6 kB
JavaScript
;
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