dop-stick
Version:
Source control tooling for versionable-upgradeable smart contracts
430 lines (402 loc) • 19.2 kB
JavaScript
;
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}\`


.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 '';
if (numCoverage >= 70)
return '';
return '';
}
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': '',
'pure': '',
'payable': '',
'nonpayable': ''
};
return badges[mutability] || mutability;
}
getDiscrepancyBadge(type) {
const badges = {
'missing': '',
'extra': '',
'mismatch': ''
};
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 '';
if (Number(coverage) > 70)
return '';
if (Number(coverage) > 50)
return '';
return '';
}
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 '';
}
if (Number(coverage) > 70) {
return '';
}
if (Number(coverage) > 50) {
return '';
}
return '';
}
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 '';
}
if (healthScore > 70) {
return '';
}
if (healthScore > 50) {
return '';
}
return '';
}
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