UNPKG

dop-stick

Version:

Source control tooling for versionable-upgradeable smart contracts

430 lines (402 loc) 19.2 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ReadmeGenerator = void 0; const logger_1 = require("../logsAndMetrics/core/logger"); const path_1 = __importDefault(require("path")); const fs_1 = __importDefault(require("fs")); const fs_2 = require("fs"); class ReadmeGenerator { constructor(networkName, diamondAddress, config) { var _a; this.chartColors = { primary: '#4e73df', success: '#1cc88a', warning: '#f6c23e', danger: '#e74a3b', secondary: '#858796' }; this.networkName = networkName; this.diamondAddress = diamondAddress; this.reportsPath = ((_a = config === null || config === void 0 ? void 0 : config.paths) === null || _a === void 0 ? void 0 : _a.reports) ? path_1.default.resolve(config.paths.reports) : process.cwd(); } generateHeader(analysisData, infoHash) { return `# Diamond Contract Analysis 💎 **Info Hash:** \`${infoHash}\` ![Diamond Standard](https://img.shields.io/badge/Diamond_Standard-EIP--2535-blue) ![Network](https://img.shields.io/badge/Network-${this.networkName}-green) ![Last Updated](https://img.shields.io/badge/Last_Updated-${new Date().toISOString().split('T')[0]}-lightgrey) > **Diamond Address:** \`${this.diamondAddress}\` > **Implementation Version:** ${analysisData.metadata.version} ## Quick Statistics 📊 | Metric | Blockchain | Processed | Coverage | |--------|------------|-----------|----------| | Facets | ${analysisData.blockchainData.totalFacets} | ${analysisData.statistics.totalFacets} | ${this.calculateCoverage(analysisData.blockchainData.totalFacets, analysisData.statistics.totalFacets)}% | | Selectors | ${analysisData.blockchainData.totalSelectors} | ${analysisData.statistics.totalSelectors} | ${this.calculateCoverage(analysisData.blockchainData.totalSelectors, analysisData.statistics.totalSelectors)}% | \`\`\`mermaid pie title Selector Analysis "Found" : ${analysisData.statistics.totalSelectors - analysisData.statistics.totalUnknownSelectors} "Unknown" : ${analysisData.statistics.totalUnknownSelectors} \`\`\` `; } generateBlockchainSection(analysisData) { // First, create a mapping of found selectors per facet const foundSelectorsMap = new Map(); analysisData.facets.forEach(facet => { foundSelectorsMap.set(facet.address.toLowerCase(), new Set(facet.selectors.found.map(s => s.selector.toLowerCase()))); }); // Generate the summary table const summaryTable = `### Blockchain Facets Summary | Facet Address | Total Selectors | Found Selectors | Unknown Selectors | Coverage | |---------------|----------------|-----------------|-------------------|----------| ${analysisData.blockchainData.facetsBreakdown.map(facet => { var _a, _b; const address = facet.address; const totalSelectors = facet.selectors.length; const foundSelectors = (_b = (_a = foundSelectorsMap.get(address.toLowerCase())) === null || _a === void 0 ? void 0 : _a.size) !== null && _b !== void 0 ? _b : 0; const unknownSelectors = totalSelectors - foundSelectors; const coverage = this.calculateCoverage(totalSelectors, foundSelectors); return `| \`${address}\` | ${totalSelectors} | ${foundSelectors} ${this.getChangeIndicator(foundSelectors, totalSelectors)} | ${unknownSelectors} | ${coverage}% ${this.getCoverageBadge(coverage)} |`; }).join('\n')} | **TOTAL** | **${analysisData.blockchainData.totalSelectors}** | **${analysisData.comparisonAnalysis.matchedSelectors}** | **${analysisData.comparisonAnalysis.unmatchedSelectors}** | **${this.calculateCoverage(analysisData.blockchainData.totalSelectors, analysisData.comparisonAnalysis.matchedSelectors)}%** |`; // Generate detailed breakdown const detailedBreakdown = analysisData.blockchainData.facetsBreakdown.map(facet => { var _a; const foundSelectors = (_a = foundSelectorsMap.get(facet.address.toLowerCase())) !== null && _a !== void 0 ? _a : new Set(); const unknownSelectors = facet.selectors.filter(s => !foundSelectors.has(s.toLowerCase())); return ` <details> <summary><strong>Facet: \`${facet.address}\`</strong> (${facet.selectorCount} total selectors)</summary> #### Found Selectors (${foundSelectors.size}) ${foundSelectors.size > 0 ? ` \`\`\` ${Array.from(foundSelectors).join('\n')} \`\`\` ` : '*No selectors found*'} #### Unknown Selectors (${unknownSelectors.length}) ${unknownSelectors.length > 0 ? ` \`\`\` ${unknownSelectors.join('\n')} \`\`\` ` : '*No unknown selectors*'} </details>`; }).join('\n'); return `## Blockchain Data Analysis 🔍 ${summaryTable} ### Detailed Selector Breakdown ${detailedBreakdown} ### Selector Distribution \`\`\`mermaid pie title Overall Selector Status "Found Selectors" : ${analysisData.comparisonAnalysis.matchedSelectors} "Unknown Selectors" : ${analysisData.comparisonAnalysis.unmatchedSelectors} \`\`\` \`\`\`mermaid bar title Selectors per Facet ${analysisData.blockchainData.facetsBreakdown.map((f, i) => ` ${f.address.slice(0, 8)}...: ${f.selectorCount}`).join('\n ')} \`\`\` `; } getChangeIndicator(found, total) { if (found === total) return '✅'; if (found === 0) return '❌'; return '⚠️'; } getCoverageBadge(coverage) { const numCoverage = Number(coverage); if (numCoverage >= 90) return '![High](https://img.shields.io/badge/coverage-high-success)'; if (numCoverage >= 70) return '![Medium](https://img.shields.io/badge/coverage-medium-yellow)'; return '![Low](https://img.shields.io/badge/coverage-low-red)'; } generateProcessedFacetsSection(analysisData) { return `## Processed Facets Analysis 🔮 ### Overview - Total Facets: **${analysisData.statistics.totalFacets}** - Total Functions: **${analysisData.statistics.totalSelectors}** - Unknown Selectors: **${analysisData.statistics.totalUnknownSelectors}** - Coverage: **${this.calculateCoverage(analysisData.statistics.totalSelectors, analysisData.statistics.totalSelectors - analysisData.statistics.totalUnknownSelectors)}%** ${analysisData.facets.map(facet => this.generateFacetDetails(facet)).join('\n')} `; } generateFacetDetails(facet) { return ` <details> <summary><strong>${facet.name}</strong> (${facet.statistics.totalSelectors} functions)</summary> ### ${facet.name} **Address:** \`${facet.address}\` **Statistics:** - Total Selectors: ${facet.statistics.totalSelectors} - Found Selectors: ${facet.statistics.foundSelectors} - Unknown Selectors: ${facet.statistics.unknownSelectors} #### Known Functions ${facet.selectors.found.length > 0 ? ` | Selector | Name | Signature | Type | |----------|------|-----------|------| ${facet.selectors.found.map(f => `| \`${f.selector}\` | ${f.name} | \`${f.signature}\` | ${this.getMutabilityBadge(f.mutability)} |`).join('\n')} ` : '*No known functions*'} ${facet.selectors.unknown.length > 0 ? ` #### Unknown Selectors | Selector | Reason | |----------|--------| ${facet.selectors.unknown.map(u => `| \`${u.selector}\` | ${u.reason || 'Unknown'} |`).join('\n')} ` : ''} ${facet.events.length > 0 ? ` #### Events ${facet.events.map(event => ` ##### \`${event.name}\` - **Signature:** \`${event.signature}\` `).join('\n')} ` : ''} </details>`; } generateComparisonSection(analysisData) { return `## Comparison Analysis 📊 ### Overview - Matched Selectors: **${analysisData.comparisonAnalysis.matchedSelectors}** - Unmatched Selectors: **${analysisData.comparisonAnalysis.unmatchedSelectors}** ### Discrepancies ${analysisData.comparisonAnalysis.discrepancies.length > 0 ? ` | Type | Selector | Facet | Details | |------|----------|-------|----------| ${analysisData.comparisonAnalysis.discrepancies.map(d => `| ${this.getDiscrepancyBadge(d.type)} | \`${d.selector || 'N/A'}\` | \`${d.facetAddress}\` | ${d.details} |`).join('\n')} ` : '*No discrepancies found*'} \`\`\`mermaid pie title Selector Matching "Matched" : ${analysisData.comparisonAnalysis.matchedSelectors} "Unmatched" : ${analysisData.comparisonAnalysis.unmatchedSelectors} \`\`\` `; } getMutabilityBadge(mutability) { const badges = { 'view': '![View](https://img.shields.io/badge/view-blue)', 'pure': '![Pure](https://img.shields.io/badge/pure-purple)', 'payable': '![Payable](https://img.shields.io/badge/payable-gold)', 'nonpayable': '![NonPayable](https://img.shields.io/badge/nonpayable-green)' }; return badges[mutability] || mutability; } getDiscrepancyBadge(type) { const badges = { 'missing': '![Missing](https://img.shields.io/badge/Missing-red)', 'extra': '![Extra](https://img.shields.io/badge/Extra-yellow)', 'mismatch': '![Mismatch](https://img.shields.io/badge/Mismatch-orange)' }; return badges[type] || type; } calculateCoverage(total, found) { return ((found / total) * 100).toFixed(2); } generateVisualizationsSection(analysisData) { return `## Detailed Visualizations 📈 ### Function Distribution by Facet \`\`\`mermaid pie title Function Distribution ${analysisData.facets.map(facet => ` "${facet.name}" : ${facet.statistics.totalSelectors}`).join('\n')} \`\`\` ### Selector Status by Facet \`\`\`mermaid graph LR ${analysisData.facets.map((facet, i) => ` subgraph ${facet.name} F${i}K[Known: ${facet.statistics.foundSelectors}] F${i}U[Unknown: ${facet.statistics.unknownSelectors}] end`).join('\n')} \`\`\` ### Function Types Distribution \`\`\`mermaid pie title Function Types ${this.aggregateFunctionTypes(analysisData).map(([type, count]) => ` "${type}" : ${count}`).join('\n')} \`\`\` `; } generateTimelineSection(analysisData) { return `## Analysis Timeline 📅 \`\`\`mermaid timeline title Diamond Analysis Timeline section Analysis ${new Date(analysisData.metadata.timestamp).toLocaleDateString()} : Analysis Performed : ${analysisData.statistics.totalFacets} Facets Analyzed : ${analysisData.statistics.totalSelectors} Selectors Processed section Results Coverage : ${this.calculateCoverage(analysisData.statistics.totalSelectors, analysisData.statistics.totalSelectors - analysisData.statistics.totalUnknownSelectors)}% Functions Identified : ${analysisData.comparisonAnalysis.matchedSelectors} Selectors Matched : ${analysisData.comparisonAnalysis.unmatchedSelectors} Discrepancies Found \`\`\` `; } generateSummarySection(analysisData) { return `## Executive Summary 📋 ### Key Findings - **Implementation Quality**: ${this.getImplementationQualityBadge(analysisData)} - **Documentation Coverage**: ${this.getDocumentationCoverageBadge(analysisData)} - **Contract Health**: ${this.getContractHealthBadge(analysisData)} ### Recommendations ${this.generateRecommendations(analysisData)} ### Security Considerations ${this.generateSecurityConsiderations(analysisData)} `; } aggregateFunctionTypes(analysisData) { const types = new Map(); analysisData.facets.forEach(facet => { facet.selectors.found.forEach(func => { const count = types.get(func.mutability) || 0; types.set(func.mutability, count + 1); }); }); return Array.from(types.entries()); } getImplementationQualityBadge(analysisData) { const coverage = this.calculateCoverage(analysisData.statistics.totalSelectors, analysisData.statistics.totalSelectors - analysisData.statistics.totalUnknownSelectors); if (Number(coverage) > 90) return '![Excellent](https://img.shields.io/badge/Quality-Excellent-success)'; if (Number(coverage) > 70) return '![Good](https://img.shields.io/badge/Quality-Good-green)'; if (Number(coverage) > 50) return '![Fair](https://img.shields.io/badge/Quality-Fair-yellow)'; return '![Needs Improvement](https://img.shields.io/badge/Quality-Needs%20Improvement-red)'; } generateRecommendations(analysisData) { const recommendations = []; if (analysisData.statistics.totalUnknownSelectors > 0) { recommendations.push('- Consider documenting unknown selectors'); } if (analysisData.comparisonAnalysis.unmatchedSelectors > 0) { recommendations.push('- Investigate selector discrepancies'); } if (analysisData.facets.some(f => f.events.length === 0)) { recommendations.push('- Consider adding events for better transparency'); } return recommendations.length > 0 ? recommendations.join('\n') : '- No immediate recommendations'; } generateSecurityConsiderations(analysisData) { const considerations = []; // Add security considerations based on analysis if (analysisData.comparisonAnalysis.unmatchedSelectors > 0) { considerations.push('⚠️ Unmatched selectors might indicate potential security risks'); } considerations.push('🔒 Ensure proper access control for state-modifying functions'); considerations.push('📜 Regular audits recommended for complex diamond implementations'); return considerations.join('\n'); } getDocumentationCoverageBadge(analysisData) { const coverage = this.calculateCoverage(analysisData.statistics.totalSelectors, analysisData.statistics.totalSelectors - analysisData.statistics.totalUnknownSelectors); if (Number(coverage) > 90) { return '![Excellent](https://img.shields.io/badge/Documentation-Excellent-success)'; } if (Number(coverage) > 70) { return '![Good](https://img.shields.io/badge/Documentation-Good-green)'; } if (Number(coverage) > 50) { return '![Fair](https://img.shields.io/badge/Documentation-Fair-yellow)'; } return '![Needs Improvement](https://img.shields.io/badge/Documentation-Needs%20Improvement-red)'; } getContractHealthBadge(analysisData) { // Calculate health score based on multiple factors let healthScore = 0; // Factor 1: Selector matching const selectorMatchRate = analysisData.comparisonAnalysis.matchedSelectors / (analysisData.comparisonAnalysis.matchedSelectors + analysisData.comparisonAnalysis.unmatchedSelectors); healthScore += selectorMatchRate * 40; // 40% weight // Factor 2: Documentation coverage const docCoverage = (analysisData.statistics.totalSelectors - analysisData.statistics.totalUnknownSelectors) / analysisData.statistics.totalSelectors; healthScore += docCoverage * 30; // 30% weight // Factor 3: Implementation completeness const implementationScore = analysisData.blockchainData.totalSelectors > 0 ? (analysisData.statistics.totalSelectors / analysisData.blockchainData.totalSelectors) : 0; healthScore += implementationScore * 30; // 30% weight // Return appropriate badge based on health score if (healthScore > 90) { return '![Excellent](https://img.shields.io/badge/Health-Excellent-success)'; } if (healthScore > 70) { return '![Good](https://img.shields.io/badge/Health-Good-green)'; } if (healthScore > 50) { return '![Fair](https://img.shields.io/badge/Health-Fair-yellow)'; } return '![Needs Improvement](https://img.shields.io/badge/Health-Needs%20Improvement-red)'; } async generateOutputPaths() { const baseDir = path_1.default.join(this.reportsPath, 'dop-stick-info'); await fs_1.default.promises.mkdir(baseDir, { recursive: true }); const timestamp = new Date(); const timeString = timestamp.toISOString().replace(/[:.]/g, '-'); const hash = this.generateTimeHash(timestamp); const timestampDir = path_1.default.join(baseDir, timeString); await fs_1.default.promises.mkdir(timestampDir, { recursive: true }); // Fix symlink error by handling existing symlink const latestDir = path_1.default.join(baseDir, 'latest'); try { // Remove existing symlink or directory if ((0, fs_2.existsSync)(latestDir)) { const stats = await fs_1.default.promises.lstat(latestDir); if (stats.isSymbolicLink()) { await fs_1.default.promises.unlink(latestDir); } else { await fs_1.default.promises.rm(latestDir, { recursive: true }); } } // Create new symlink await fs_1.default.promises.symlink(timestampDir, latestDir, 'dir'); } catch (error) { logger_1.Logger.warn('Failed to create latest symlink:', error); // Continue even if symlink fails } return { baseDir, timestampDir, markdownPath: path_1.default.join(timestampDir, `diamond-analysis-${hash}.md`), jsonPath: path_1.default.join(timestampDir, `diamond-analysis-${hash}.json`), hash }; } generateTimeHash(timestamp) { const timeNum = timestamp.getTime(); return Buffer.from(timeNum.toString()).toString('base64').substring(0, 8); } async generateDocumentation(facets, unknownSelectors, analysisData) { const { markdownPath, jsonPath, hash } = await this.generateOutputPaths(); // Add hash to analysis data const analysisWithHash = { infoHash: hash, ...analysisData }; const content = [ this.generateHeader(analysisData, hash), this.generateSummarySection(analysisData), this.generateBlockchainSection(analysisData), this.generateProcessedFacetsSection(analysisData), this.generateComparisonSection(analysisData), this.generateVisualizationsSection(analysisData), this.generateTimelineSection(analysisData), '\n---\n', `Generated by DopStick v${analysisData.metadata.version} | ${new Date().toISOString()} | Info Hash: ${hash}` ].join('\n\n'); await fs_1.default.promises.writeFile(markdownPath, content); await fs_1.default.promises.writeFile(jsonPath, JSON.stringify(analysisWithHash, null, 2)); // logger.info(`Documentation generated successfully: // - Markdown: ${markdownPath} // - JSON: ${jsonPath} // - Info Hash: ${hash}`); return { markdownPath, jsonPath, hash }; } } exports.ReadmeGenerator = ReadmeGenerator; //# sourceMappingURL=readme-generator.js.map