diff --git a/README.md b/README.md index 10eef970..5c76b7d1 100644 --- a/README.md +++ b/README.md @@ -51,42 +51,41 @@ Generate UML class or storage diagrams from local Solidity code or verified Soli Can also flatten or compare verified source files on Etherscan-like explorers. Options: - -sf, --subfolders number of subfolders that will be recursively searched for Solidity files. (default: all) - -f, --outputFormat output file format. (choices: "svg", "png", "dot", "all", default: "svg") - -o, --outputFileName output file name - -i, --ignoreFilesOrFolders comma separated list of files or folders to ignore - -n, --network Ethereum network which maps to a blockchain explorer (choices: "mainnet", "goerli", "sepolia", "polygon", "arbitrum", "avalanche", "bsc", "crono", "fantom", "moonbeam", - "optimism", "gnosis", "celo", default: "mainnet", env: ETH_NETWORK) - -e, --explorerUrl Override network with custom blockchain explorer API URL. eg Polygon Mumbai testnet https://api-testnet.polygonscan.com/api (env: EXPLORER_URL) - -k, --apiKey Blockchain explorer API key. eg Etherscan, Arbiscan, Optimism, BscScan, CronoScan, FTMScan, PolygonScan or SnowTrace API key (env: SCAN_API_KEY) - -bc, --backColor Canvas background color. "none" will use a transparent canvas. (default: "white") - -sc, --shapeColor Basic drawing color for graphics, not text (default: "black") - -fc, --fillColor Color used to fill the background of a node (default: "gray95") - -tc, --textColor Color used for text (default: "black") - -v, --verbose run with debugging statements (default: false) - -V, --version output the version number - -h, --help display help for command + -sf, --subfolders number of subfolders that will be recursively searched for Solidity files. (default: all) + -f, --outputFormat output file format. (choices: "svg", "png", "dot", "all", default: "svg") + -o, --outputFileName output file name + -i, --ignoreFilesOrFolders comma-separated list of files or folders to ignore + -n, --network Ethereum network which maps to a blockchain explorer (choices: "mainnet", "goerli", "sepolia", "polygon", "arbitrum", "avalanche", "bsc", "crono", "fantom", "moonbeam", "optimism", "gnosis", "celo", default: "mainnet", env: ETH_NETWORK) + -e, --explorerUrl Override the `network` option with a custom blockchain explorer API URL. eg Polygon Mumbai testnet https://api-testnet.polygonscan.com/api (env: EXPLORER_URL) + -k, --apiKey Blockchain explorer API key. eg Etherscan, Arbiscan, Optimism, BscScan, CronoScan, FTMScan, PolygonScan or SnowTrace API key (env: SCAN_API_KEY) + -bc, --backColor Canvas background color. "none" will use a transparent canvas. (default: "white") + -sc, --shapeColor Basic drawing color for graphics, not text (default: "black") + -fc, --fillColor Color used to fill the background of a node (default: "gray95") + -tc, --textColor Color used for text (default: "black") + -v, --verbose run with debugging statements (default: false) + -V, --version output the version number + -h, --help display help for command Commands: - class [options] Generates a UML class diagram from Solidity source code. - storage [options] Visually display a contract's storage slots. + class [options] Generates a UML class diagram from Solidity source code. + storage [options] Visually display a contract's storage slots. - WARNING: sol2uml does not use the Solidity compiler so may differ with solc. A known example is fixed-sized arrays declared with an expression will fail to be sized. - flatten Merges verified source files for a contract from a Blockchain explorer into one local Solidity file. + WARNING: sol2uml does not use the Solidity compiler so may differ with solc. A known example is fixed-sized arrays declared with an expression will fail to be sized. + flatten Merges verified source files for a contract from a Blockchain explorer into one local Solidity file. - In order for the merged code to compile, the following is done: - 1. pragma solidity is set using the compiler of the verified contract. - 2. All pragma solidity lines in the source files are commented out. - 3. File imports are commented out. - 4. "SPDX-License-Identifier" is renamed to "SPDX--License-Identifier". - 5. Contract dependencies are analysed so the files are merged in an order that will compile. - diff [options] Compare verified Solidity code to another verified contract or local source files. + In order for the merged code to compile, the following is done: + 1. pragma solidity is set using the compiler of the verified contract. + 2. All pragma solidity lines in the source files are commented out. + 3. File imports are commented out. + 4. "SPDX-License-Identifier" is renamed to "SPDX--License-Identifier". + 5. Contract dependencies are analysed so the files are merged in an order that will compile. + diff [options] Compare verified Solidity code to another verified contract, a local file or local source files. - The results show the comparison of contract A to B. - The green sections are additions to contract B that are not in contract A. - The red sections are removals from contract A that are not in contract B. - The line numbers are from contract B. There are no line numbers for the red sections as they are not in contract B. - help [command] display help for command + The results show the comparison of contract A to B. + The green sections are additions to contract B that are not in contract A. + The red sections are removals from contract A that are not in contract B. + The line numbers are from contract B. There are no line numbers for the red sections as they are not in contract B. + help [command] display help for command ``` ### Class usage @@ -182,7 +181,7 @@ Options: ``` Usage: sol2uml diff [options] -Compare verified Solidity code to another verified contract or local source files. +Compare verified Solidity code to another verified contract, a local file or local source files. The results show the comparison of contract A to B. The green sections are additions to contract B that are not in contract A. @@ -191,9 +190,10 @@ The line numbers are from contract B. There are no line numbers for the red sect Arguments: addressA Contract address in hexadecimal format with a 0x prefix of the first contract - addressB_folders Location of the contract source code to compare against. Can be a contract address or comma-separated list of local folders. - For example, 0x1091588Cc431275F99DC5Df311fd8E1Ab81c89F3 will get the verified source code from Etherscan - or ".,node_modules" will compare against local files in the current folder and the node_modules folder. + fileFoldersAddress Location of the contract source code to compare against. Can be a filename, comma-separated list of local folders or a contract address. Examples: + "flat.sol" will compare against a local file called "flat.sol". This must be used when address A's verified source code is a single, flat file. + ".,node_modules" will compare against local files under the current working folder and the node_modules folder. This is used when address A's verified source code is multiple files. + 0x1091588Cc431275F99DC5Df311fd8E1Ab81c89F3 will compare against the verified source code from Etherscan. Options: -s, --summary Only show a summary of the file differences (default: false) diff --git a/lib/diffContracts.d.ts b/lib/diffContracts.d.ts index f7a8a9ef..a1463db2 100644 --- a/lib/diffContracts.d.ts +++ b/lib/diffContracts.d.ts @@ -20,14 +20,15 @@ interface CompareContracts { files: DiffFiles[]; contractNameA: string; contractNameB?: string; + local?: 'file' | 'folders'; } export declare const compareVerifiedContracts: (addressA: string, aEtherscanParser: EtherscanParser, addressB: string, bEtherscanParser: EtherscanParser, options: DiffOptions) => Promise; -export declare const compareVerified2Local: (addressA: string, aEtherscanParser: EtherscanParser, localFolders: string[], options: DiffOptions) => Promise; +export declare const compareVerified2Local: (addressA: string, aEtherscanParser: EtherscanParser, fileOrBaseFolders: string[], options: DiffOptions) => Promise; export declare const compareFlattenContracts: (addressA: string, addressB: string, aEtherscanParser: EtherscanParser, bEtherscanParser: EtherscanParser, options: FlattenAndDiffOptions) => Promise<{ contractNameA: string; contractNameB: string; }>; -export declare const diffVerified2Local: (addressA: string, etherscanParserA: EtherscanParser, baseFolders: string[], ignoreFilesOrFolders?: string[]) => Promise; +export declare const diffVerified2Local: (addressA: string, etherscanParserA: EtherscanParser, fileOrBaseFolders: string[], ignoreFilesOrFolders?: string[]) => Promise; export declare const diffVerifiedContracts: (addressA: string, addressB: string, etherscanParserA: EtherscanParser, etherscanParserB: EtherscanParser, options: DiffOptions) => Promise; export declare const displayFileDiffSummary: (fileDiffs: DiffFiles[]) => void; export declare const displayFileDiffs: (fileDiffs: DiffFiles[], options?: { diff --git a/lib/diffContracts.js b/lib/diffContracts.js index 4400f4c6..d64a6d31 100644 --- a/lib/diffContracts.js +++ b/lib/diffContracts.js @@ -2,7 +2,6 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.displayFileDiffs = exports.displayFileDiffSummary = exports.diffVerifiedContracts = exports.diffVerified2Local = exports.compareFlattenContracts = exports.compareVerified2Local = exports.compareVerifiedContracts = void 0; const clc = require('cli-color'); -const fs_1 = require("fs"); const path_1 = require("path"); const parserFiles_1 = require("./parserFiles"); const writerFiles_1 = require("./writerFiles"); @@ -19,14 +18,19 @@ const compareVerifiedContracts = async (addressA, aEtherscanParser, addressB, bE (0, exports.displayFileDiffSummary)(files); }; exports.compareVerifiedContracts = compareVerifiedContracts; -const compareVerified2Local = async (addressA, aEtherscanParser, localFolders, options) => { +const compareVerified2Local = async (addressA, aEtherscanParser, fileOrBaseFolders, options) => { // compare verified contract to local files - const { contractNameA, files } = await (0, exports.diffVerified2Local)(addressA, aEtherscanParser, localFolders); + const { contractNameA, files, local } = await (0, exports.diffVerified2Local)(addressA, aEtherscanParser, fileOrBaseFolders); if (!options.summary) { (0, exports.displayFileDiffs)(files, options); } console.log(`Compared the "${contractNameA}" contract with address ${addressA} on ${options.network}`); - console.log(`to local files under folders "${localFolders}"\n`); + if (local) { + console.log(`to local file "${fileOrBaseFolders}"\n`); + } + else { + console.log(`to local files under folders "${fileOrBaseFolders}"\n`); + } (0, exports.displayFileDiffSummary)(files); }; exports.compareVerified2Local = compareVerified2Local; @@ -36,25 +40,52 @@ const compareFlattenContracts = async (addressA, addressB, aEtherscanParser, bEt const { solidityCode: codeB, contractName: contractNameB } = await bEtherscanParser.getSolidityCode(addressB, options.bFile || options.aFile); (0, diff_1.diffCode)(codeA, codeB, options.lineBuffer); if (options.saveFiles) { - await (0, writerFiles_1.writeSolidity)(codeA, addressA); - await (0, writerFiles_1.writeSolidity)(codeB, addressB); + await (0, writerFiles_1.writeSourceCode)(codeA, addressA); + await (0, writerFiles_1.writeSourceCode)(codeB, addressB); + } + if (options.bFile || options.aFile) { + console.log(`Compared the "${options.aFile}" file for the "${contractNameA}" contract with address ${addressA} on ${options.network}`); + console.log(`to the "${options.bFile || options.aFile}" file for the "${contractNameB}" contract with address ${addressB} on ${options.bNetwork || options.network}\n`); + } + else { + console.log(`Compared the flattened "${contractNameA}" contract with address ${addressA} on ${options.network}`); + console.log(`to the flattened "${contractNameB}" contract with address ${addressB} on ${options.bNetwork || options.network}\n`); } - console.log(`Compared the flattened "${contractNameA}" contract with address ${addressA} on ${options.network}`); - console.log(`to the flattened "${contractNameB}" contract with address ${addressB} on ${options.bNetwork || options.network}\n`); return { contractNameA, contractNameB }; }; exports.compareFlattenContracts = compareFlattenContracts; -const diffVerified2Local = async (addressA, etherscanParserA, baseFolders, ignoreFilesOrFolders = []) => { +const diffVerified2Local = async (addressA, etherscanParserA, fileOrBaseFolders, ignoreFilesOrFolders = []) => { const files = []; // Get all the source files for the verified contract from Etherscan const { files: aFiles, contractName: contractNameA } = await etherscanParserA.getSourceCode(addressA); - const bFiles = await (0, parserFiles_1.getSolidityFilesFromFolderOrFiles)(baseFolders, ignoreFilesOrFolders); + if (aFiles.length === 1 && (0, regEx_1.isAddress)(aFiles[0].filename)) { + // The verified contract is a single, flat file + const aFile = aFiles[0]; + const bFile = fileOrBaseFolders[0]; + if ((0, parserFiles_1.isFolder)(bFile)) { + throw Error(`Contract with address ${addressA} is a single, flat file so cannot be compared to a local files under folder(s) "${fileOrBaseFolders.toString()}".`); + } + // Try and read the bFile + const bCode = (0, parserFiles_1.readFile)(bFile, 'sol'); + files.push({ + filename: aFile.filename, + aCode: aFile.code, + bCode, + result: aFile.code === bCode ? 'match' : 'changed', + }); + return { + files, + contractNameA, + local: 'file', + }; + } + const bFiles = await (0, parserFiles_1.getSolidityFilesFromFolderOrFiles)(fileOrBaseFolders, ignoreFilesOrFolders); // For each file in the A contract for (const aFile of aFiles) { // Look for A contract filename in local filesystem let bFile; // for each of the base folders - for (const baseFolder of baseFolders) { + for (const baseFolder of fileOrBaseFolders) { bFile = bFiles.find((bFile) => { const resolvedPath = (0, path_1.resolve)(process.cwd(), baseFolder, aFile.filename); return bFile === resolvedPath; @@ -64,31 +95,26 @@ const diffVerified2Local = async (addressA, etherscanParserA, baseFolders, ignor } } if (bFile) { - try { - debug(`Matched verified file ${aFile.filename} to local file ${bFile}`); - // Try and read code from bFile - const bCode = (0, fs_1.readFileSync)(bFile, 'utf8'); - // The A contract filename exists in the B contract - if (aFile.code !== bCode) { - // console.log(`${aFile.filename} ${clc.red('different')}:`) - files.push({ - filename: aFile.filename, - aCode: aFile.code, - bCode, - result: 'changed', - }); - } - else { - files.push({ - filename: aFile.filename, - aCode: aFile.code, - bCode, - result: 'match', - }); - } + debug(`Matched verified file ${aFile.filename} to local file ${bFile}`); + // Try and read code from bFile + const bCode = (0, parserFiles_1.readFile)(bFile); + // The A contract filename exists in the B contract + if (aFile.code !== bCode) { + // console.log(`${aFile.filename} ${clc.red('different')}:`) + files.push({ + filename: aFile.filename, + aCode: aFile.code, + bCode, + result: 'changed', + }); } - catch (err) { - throw Error(`Failed to read local file ${bFile}`); + else { + files.push({ + filename: aFile.filename, + aCode: aFile.code, + bCode, + result: 'match', + }); } } else { diff --git a/lib/parserFiles.d.ts b/lib/parserFiles.d.ts index 3fbcb19f..9251bbc8 100644 --- a/lib/parserFiles.d.ts +++ b/lib/parserFiles.d.ts @@ -4,3 +4,6 @@ export declare const parseUmlClassesFromFiles: (filesOrFolders: readonly string[ export declare function getSolidityFilesFromFolderOrFiles(folderOrFilePaths: readonly string[], ignoreFilesOrFolders: readonly string[], subfolders?: number): Promise; export declare function getSolidityFilesFromFolderOrFile(folderOrFilePath: string, ignoreFilesOrFolders?: readonly string[], depthLimit?: number): Promise; export declare function parseSolidityFile(fileName: string): ASTNode; +export declare const readFile: (fileName: string, extension?: string) => string; +export declare const isFile: (fileName: string) => boolean; +export declare const isFolder: (fileName: string) => boolean; diff --git a/lib/parserFiles.js b/lib/parserFiles.js index 313e4e82..4fef56d6 100644 --- a/lib/parserFiles.js +++ b/lib/parserFiles.js @@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.parseSolidityFile = exports.getSolidityFilesFromFolderOrFile = exports.getSolidityFilesFromFolderOrFiles = exports.parseUmlClassesFromFiles = void 0; +exports.isFolder = exports.isFile = exports.readFile = exports.parseSolidityFile = exports.getSolidityFilesFromFolderOrFile = exports.getSolidityFilesFromFolderOrFiles = exports.parseUmlClassesFromFiles = void 0; const fs_1 = require("fs"); const path_1 = require("path"); const klaw_1 = __importDefault(require("klaw")); @@ -89,15 +89,7 @@ function getSolidityFilesFromFolderOrFile(folderOrFilePath, ignoreFilesOrFolders } exports.getSolidityFilesFromFolderOrFile = getSolidityFilesFromFolderOrFile; function parseSolidityFile(fileName) { - let solidityCode; - try { - solidityCode = (0, fs_1.readFileSync)(fileName, 'utf8'); - } - catch (err) { - throw new Error(`Failed to read solidity file ${fileName}.`, { - cause: err, - }); - } + const solidityCode = (0, exports.readFile)(fileName); try { return (0, parser_1.parse)(solidityCode, {}); } @@ -108,4 +100,47 @@ function parseSolidityFile(fileName) { } } exports.parseSolidityFile = parseSolidityFile; +const readFile = (fileName, extension) => { + try { + // try to read file with no extension + return (0, fs_1.readFileSync)(fileName, 'utf8'); + } + catch (err) { + if (!extension) { + throw new Error(`Failed to read file "${fileName}".`, { + cause: err, + }); + } + try { + // try to read file with extension + return (0, fs_1.readFileSync)(`${fileName}.${extension}`, 'utf8'); + } + catch (err) { + throw new Error(`Failed to read file "${fileName}" or "${fileName}.${extension}".`, { + cause: err, + }); + } + } +}; +exports.readFile = readFile; +const isFile = (fileName) => { + try { + const file = (0, fs_1.lstatSync)(fileName); + return file.isFile(); + } + catch (err) { + return false; + } +}; +exports.isFile = isFile; +const isFolder = (fileName) => { + try { + const file = (0, fs_1.lstatSync)(fileName); + return file.isDirectory(); + } + catch (err) { + return false; + } +}; +exports.isFolder = isFolder; //# sourceMappingURL=parserFiles.js.map \ No newline at end of file diff --git a/lib/sol2uml.js b/lib/sol2uml.js index 6b9da95f..1a3dcabd 100755 --- a/lib/sol2uml.js +++ b/lib/sol2uml.js @@ -209,7 +209,7 @@ In order for the merged code to compile, the following is done: const { solidityCode, contractName } = await etherscanParser.getSolidityCode(contractAddress); // Write Solidity to the contract address const outputFilename = combinedOptions.outputFileName || contractName; - await (0, writerFiles_1.writeSolidity)(solidityCode, outputFilename); + await (0, writerFiles_1.writeSourceCode)(solidityCode, outputFilename); } catch (err) { console.error(err); @@ -219,16 +219,17 @@ In order for the merged code to compile, the following is done: program .command('diff') .usage('[options] ') - .description(`Compare verified Solidity code to another verified contract or local source files. + .description(`Compare verified Solidity code to another verified contract, a local file or local source files. The results show the comparison of contract A to B. The ${clc.green('green')} sections are additions to contract B that are not in contract A. The ${clc.red('red')} sections are removals from contract A that are not in contract B. The line numbers are from contract B. There are no line numbers for the red sections as they are not in contract B.\n`) .argument('', 'Contract address in hexadecimal format with a 0x prefix of the first contract', validators_1.validateAddress) - .argument('', `Location of the contract source code to compare against. Can be a contract address or comma-separated list of local folders. -For example, 0x1091588Cc431275F99DC5Df311fd8E1Ab81c89F3 will get the verified source code from Etherscan -or ".,node_modules" will compare against local files in the current folder and the node_modules folder.`) + .argument('', `Location of the contract source code to compare against. Can be a filename, comma-separated list of local folders or a contract address. Examples: + "flat.sol" will compare against a local file called "flat.sol". This must be used when address A's verified source code is a single, flat file. + ".,node_modules" will compare against local files under the current working folder and the node_modules folder. This is used when address A's verified source code is multiple files. + 0x1091588Cc431275F99DC5Df311fd8E1Ab81c89F3 will compare against the verified source code from Etherscan.`) .option('-s, --summary', 'Only show a summary of the file differences', false) .option('-af --aFile ', 'Contract A source code filename without the .sol extension (default: compares all source files)') .option('-bf --bFile ', 'Contract B source code filename without the .sol extension (default: aFile if specified)') @@ -238,16 +239,16 @@ or ".,node_modules" will compare against local files in the current folder and t .option('--flatten', 'Flatten into a single file before comparing. Only works when comparing two verified contracts, not to local files', false) .option('--saveFiles', 'Save the flattened contract code to the filesystem when using the `flatten` option. The file names will be the contract address with a .sol extension', false) .option('-l, --lineBuffer ', 'Minimum number of lines before and after changes (default: 4)', validators_1.validateLineBuffer) - .action(async (addressA, addressB_folders, options, command) => { + .action(async (addressA, fileFoldersAddress, options, command) => { try { - debug(`About to compare ${addressA} to ${addressB_folders}`); + debug(`About to compare ${addressA} to ${fileFoldersAddress}`); const combinedOptions = { ...command.parent._optionValues, ...options, }; const aEtherscanParser = new parserEtherscan_1.EtherscanParser(combinedOptions.apiKey, combinedOptions.network, combinedOptions.explorerUrl); - if ((0, regEx_1.isAddress)(addressB_folders)) { - const addressB = addressB_folders; + if ((0, regEx_1.isAddress)(fileFoldersAddress)) { + const addressB = fileFoldersAddress; const bEtherscanParser = new parserEtherscan_1.EtherscanParser(combinedOptions.bApiKey || combinedOptions.apiKey, combinedOptions.bNetwork || combinedOptions.network, combinedOptions.bExplorerUrl || combinedOptions.explorerUrl); // If flattening or just comparing a single file if (options.flatten || options.aFile) { @@ -258,7 +259,7 @@ or ".,node_modules" will compare against local files in the current folder and t } } else { - const localFolders = addressB_folders.split(','); + const localFolders = fileFoldersAddress.split(','); await (0, diffContracts_1.compareVerified2Local)(addressA, aEtherscanParser, localFolders, combinedOptions); } } diff --git a/lib/writerFiles.d.ts b/lib/writerFiles.d.ts index fd790048..02677f71 100644 --- a/lib/writerFiles.d.ts +++ b/lib/writerFiles.d.ts @@ -8,7 +8,7 @@ export type OutputFormats = 'svg' | 'png' | 'dot' | 'all'; */ export declare const writeOutputFiles: (dot: string, contractName: string, outputFormat?: OutputFormats, outputFilename?: string) => Promise; export declare function convertDot2Svg(dot: string): any; -export declare function writeSolidity(code: string, filename?: string): void; +export declare function writeSourceCode(code: string, filename?: string, extension?: string): void; export declare function writeDot(dot: string, filename: string): void; /** * Writes an SVG file to the file system. diff --git a/lib/writerFiles.js b/lib/writerFiles.js index 29f52421..4034fd59 100644 --- a/lib/writerFiles.js +++ b/lib/writerFiles.js @@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.writePng = exports.writeSVG = exports.writeDot = exports.writeSolidity = exports.convertDot2Svg = exports.writeOutputFiles = void 0; +exports.writePng = exports.writeSVG = exports.writeDot = exports.writeSourceCode = exports.convertDot2Svg = exports.writeOutputFiles = void 0; const fs_1 = require("fs"); const path_1 = __importDefault(require("path")); const sync_1 = __importDefault(require("@aduh95/viz.js/sync")); @@ -64,22 +64,22 @@ function convertDot2Svg(dot) { } } exports.convertDot2Svg = convertDot2Svg; -function writeSolidity(code, filename = 'solidity') { - const extension = path_1.default.extname(filename); - const outputFile = extension === '.sol' ? filename : filename + '.sol'; - debug(`About to write Solidity to file ${outputFile}`); +function writeSourceCode(code, filename = 'source', extension = '.sol') { + const fileExtension = path_1.default.extname(filename); + const outputFile = fileExtension === extension ? filename : filename + extension; + debug(`About to write source code to file ${outputFile}`); (0, fs_1.writeFile)(outputFile, code, (err) => { if (err) { - throw new Error(`Failed to write Solidity to file ${outputFile}`, { + throw new Error(`Failed to write source code to file ${outputFile}`, { cause: err, }); } else { - console.log(`Solidity written to ${outputFile}`); + console.log(`Source code written to ${outputFile}`); } }); } -exports.writeSolidity = writeSolidity; +exports.writeSourceCode = writeSourceCode; function writeDot(dot, filename) { const dotFilename = changeFileExtension(filename, 'dot'); debug(`About to write Dot file to ${dotFilename}`); diff --git a/package-lock.json b/package-lock.json index 3660d4d0..0165d697 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sol2uml", - "version": "2.5.11", + "version": "2.5.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sol2uml", - "version": "2.5.11", + "version": "2.5.12", "license": "MIT", "dependencies": { "@aduh95/viz.js": "^3.7.0", diff --git a/package.json b/package.json index 46abd497..a6228eda 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sol2uml", - "version": "2.5.11", + "version": "2.5.12", "description": "Solidity contract visualisation tool.", "main": "./lib/index.js", "types": "./lib/index.d.ts", diff --git a/src/ts/diffContracts.ts b/src/ts/diffContracts.ts index eb208e83..10973193 100644 --- a/src/ts/diffContracts.ts +++ b/src/ts/diffContracts.ts @@ -1,10 +1,13 @@ const clc = require('cli-color') -import { readFileSync } from 'fs' import { resolve } from 'path' import { EtherscanParser } from './parserEtherscan' -import { getSolidityFilesFromFolderOrFiles } from './parserFiles' -import { writeSolidity } from './writerFiles' +import { + getSolidityFilesFromFolderOrFiles, + isFolder, + readFile, +} from './parserFiles' +import { writeSourceCode } from './writerFiles' import { isAddress } from './utils/regEx' import { diffCode } from './utils/diff' @@ -33,6 +36,7 @@ interface CompareContracts { files: DiffFiles[] contractNameA: string contractNameB?: string + local?: 'file' | 'folders' } export const compareVerifiedContracts = async ( @@ -69,14 +73,14 @@ export const compareVerifiedContracts = async ( export const compareVerified2Local = async ( addressA: string, aEtherscanParser: EtherscanParser, - localFolders: string[], + fileOrBaseFolders: string[], options: DiffOptions, ) => { // compare verified contract to local files - const { contractNameA, files } = await diffVerified2Local( + const { contractNameA, files, local } = await diffVerified2Local( addressA, aEtherscanParser, - localFolders, + fileOrBaseFolders, ) if (!options.summary) { @@ -86,7 +90,11 @@ export const compareVerified2Local = async ( console.log( `Compared the "${contractNameA}" contract with address ${addressA} on ${options.network}`, ) - console.log(`to local files under folders "${localFolders}"\n`) + if (local) { + console.log(`to local file "${fileOrBaseFolders}"\n`) + } else { + console.log(`to local files under folders "${fileOrBaseFolders}"\n`) + } displayFileDiffSummary(files) } @@ -109,18 +117,31 @@ export const compareFlattenContracts = async ( diffCode(codeA, codeB, options.lineBuffer) if (options.saveFiles) { - await writeSolidity(codeA, addressA) - await writeSolidity(codeB, addressB) + await writeSourceCode(codeA, addressA) + await writeSourceCode(codeB, addressB) } - console.log( - `Compared the flattened "${contractNameA}" contract with address ${addressA} on ${options.network}`, - ) - console.log( - `to the flattened "${contractNameB}" contract with address ${addressB} on ${ - options.bNetwork || options.network - }\n`, - ) + if (options.bFile || options.aFile) { + console.log( + `Compared the "${options.aFile}" file for the "${contractNameA}" contract with address ${addressA} on ${options.network}`, + ) + console.log( + `to the "${ + options.bFile || options.aFile + }" file for the "${contractNameB}" contract with address ${addressB} on ${ + options.bNetwork || options.network + }\n`, + ) + } else { + console.log( + `Compared the flattened "${contractNameA}" contract with address ${addressA} on ${options.network}`, + ) + console.log( + `to the flattened "${contractNameB}" contract with address ${addressB} on ${ + options.bNetwork || options.network + }\n`, + ) + } return { contractNameA, contractNameB } } @@ -128,7 +149,7 @@ export const compareFlattenContracts = async ( export const diffVerified2Local = async ( addressA: string, etherscanParserA: EtherscanParser, - baseFolders: string[], + fileOrBaseFolders: string[], ignoreFilesOrFolders: string[] = [], ): Promise => { const files: DiffFiles[] = [] @@ -136,8 +157,34 @@ export const diffVerified2Local = async ( const { files: aFiles, contractName: contractNameA } = await etherscanParserA.getSourceCode(addressA) + if (aFiles.length === 1 && isAddress(aFiles[0].filename)) { + // The verified contract is a single, flat file + const aFile = aFiles[0] + + const bFile = fileOrBaseFolders[0] + if (isFolder(bFile)) { + throw Error( + `Contract with address ${addressA} is a single, flat file so cannot be compared to a local files under folder(s) "${fileOrBaseFolders.toString()}".`, + ) + } + + // Try and read the bFile + const bCode = readFile(bFile, 'sol') + files.push({ + filename: aFile.filename, + aCode: aFile.code, + bCode, + result: aFile.code === bCode ? 'match' : 'changed', + }) + return { + files, + contractNameA, + local: 'file', + } + } + const bFiles = await getSolidityFilesFromFolderOrFiles( - baseFolders, + fileOrBaseFolders, ignoreFilesOrFolders, ) @@ -146,7 +193,7 @@ export const diffVerified2Local = async ( // Look for A contract filename in local filesystem let bFile: string // for each of the base folders - for (const baseFolder of baseFolders) { + for (const baseFolder of fileOrBaseFolders) { bFile = bFiles.find((bFile) => { const resolvedPath = resolve( process.cwd(), @@ -161,32 +208,28 @@ export const diffVerified2Local = async ( } if (bFile) { - try { - debug( - `Matched verified file ${aFile.filename} to local file ${bFile}`, - ) - // Try and read code from bFile - const bCode = readFileSync(bFile, 'utf8') - - // The A contract filename exists in the B contract - if (aFile.code !== bCode) { - // console.log(`${aFile.filename} ${clc.red('different')}:`) - files.push({ - filename: aFile.filename, - aCode: aFile.code, - bCode, - result: 'changed', - }) - } else { - files.push({ - filename: aFile.filename, - aCode: aFile.code, - bCode, - result: 'match', - }) - } - } catch (err) { - throw Error(`Failed to read local file ${bFile}`) + debug( + `Matched verified file ${aFile.filename} to local file ${bFile}`, + ) + // Try and read code from bFile + const bCode = readFile(bFile) + + // The A contract filename exists in the B contract + if (aFile.code !== bCode) { + // console.log(`${aFile.filename} ${clc.red('different')}:`) + files.push({ + filename: aFile.filename, + aCode: aFile.code, + bCode, + result: 'changed', + }) + } else { + files.push({ + filename: aFile.filename, + aCode: aFile.code, + bCode, + result: 'match', + }) } } else { debug( diff --git a/src/ts/parserFiles.ts b/src/ts/parserFiles.ts index e1cef6f6..44ff5c48 100644 --- a/src/ts/parserFiles.ts +++ b/src/ts/parserFiles.ts @@ -129,19 +129,54 @@ export function getSolidityFilesFromFolderOrFile( } export function parseSolidityFile(fileName: string): ASTNode { - let solidityCode: string + const solidityCode = readFile(fileName) try { - solidityCode = readFileSync(fileName, 'utf8') + return parse(solidityCode, {}) } catch (err) { - throw new Error(`Failed to read solidity file ${fileName}.`, { + throw new Error(`Failed to parse solidity code in file ${fileName}.`, { cause: err, }) } +} + +export const readFile = (fileName: string, extension?: string): string => { try { - return parse(solidityCode, {}) + // try to read file with no extension + return readFileSync(fileName, 'utf8') } catch (err) { - throw new Error(`Failed to parse solidity code in file ${fileName}.`, { - cause: err, - }) + if (!extension) { + throw new Error(`Failed to read file "${fileName}".`, { + cause: err, + }) + } + + try { + // try to read file with extension + return readFileSync(`${fileName}.${extension}`, 'utf8') + } catch (err) { + throw new Error( + `Failed to read file "${fileName}" or "${fileName}.${extension}".`, + { + cause: err, + }, + ) + } + } +} + +export const isFile = (fileName: string): boolean => { + try { + const file = lstatSync(fileName) + return file.isFile() + } catch (err) { + return false + } +} +export const isFolder = (fileName: string): boolean => { + try { + const file = lstatSync(fileName) + return file.isDirectory() + } catch (err) { + return false } } diff --git a/src/ts/sol2uml.ts b/src/ts/sol2uml.ts index 5fd6a4c3..626970d3 100644 --- a/src/ts/sol2uml.ts +++ b/src/ts/sol2uml.ts @@ -29,7 +29,7 @@ import { validateLineBuffer, validateVariables, } from './utils/validators' -import { writeOutputFiles, writeSolidity } from './writerFiles' +import { writeOutputFiles, writeSourceCode } from './writerFiles' const clc = require('cli-color') const program = new Command() @@ -437,7 +437,7 @@ In order for the merged code to compile, the following is done: // Write Solidity to the contract address const outputFilename = combinedOptions.outputFileName || contractName - await writeSolidity(solidityCode, outputFilename) + await writeSourceCode(solidityCode, outputFilename) } catch (err) { console.error(err) process.exit(2) @@ -448,7 +448,7 @@ program .command('diff') .usage('[options] ') .description( - `Compare verified Solidity code to another verified contract or local source files. + `Compare verified Solidity code to another verified contract, a local file or local source files. The results show the comparison of contract A to B. The ${clc.green( @@ -465,10 +465,11 @@ The line numbers are from contract B. There are no line numbers for the red sect validateAddress, ) .argument( - '', - `Location of the contract source code to compare against. Can be a contract address or comma-separated list of local folders. -For example, 0x1091588Cc431275F99DC5Df311fd8E1Ab81c89F3 will get the verified source code from Etherscan -or ".,node_modules" will compare against local files in the current folder and the node_modules folder.`, + '', + `Location of the contract source code to compare against. Can be a filename, comma-separated list of local folders or a contract address. Examples: + "flat.sol" will compare against a local file called "flat.sol". This must be used when address A's verified source code is a single, flat file. + ".,node_modules" will compare against local files under the current working folder and the node_modules folder. This is used when address A's verified source code is multiple files. + 0x1091588Cc431275F99DC5Df311fd8E1Ab81c89F3 will compare against the verified source code from Etherscan.`, ) .option( '-s, --summary', @@ -512,9 +513,9 @@ or ".,node_modules" will compare against local files in the current folder and t 'Minimum number of lines before and after changes (default: 4)', validateLineBuffer, ) - .action(async (addressA, addressB_folders, options, command) => { + .action(async (addressA, fileFoldersAddress, options, command) => { try { - debug(`About to compare ${addressA} to ${addressB_folders}`) + debug(`About to compare ${addressA} to ${fileFoldersAddress}`) const combinedOptions = { ...command.parent._optionValues, @@ -527,8 +528,8 @@ or ".,node_modules" will compare against local files in the current folder and t combinedOptions.explorerUrl, ) - if (isAddress(addressB_folders)) { - const addressB = addressB_folders + if (isAddress(fileFoldersAddress)) { + const addressB = fileFoldersAddress const bEtherscanParser = new EtherscanParser( combinedOptions.bApiKey || combinedOptions.apiKey, combinedOptions.bNetwork || combinedOptions.network, @@ -553,7 +554,7 @@ or ".,node_modules" will compare against local files in the current folder and t ) } } else { - const localFolders: string[] = addressB_folders.split(',') + const localFolders: string[] = fileFoldersAddress.split(',') await compareVerified2Local( addressA, aEtherscanParser, diff --git a/src/ts/writerFiles.ts b/src/ts/writerFiles.ts index 73d85d25..dd8d5fdd 100644 --- a/src/ts/writerFiles.ts +++ b/src/ts/writerFiles.ts @@ -71,18 +71,26 @@ export function convertDot2Svg(dot: string): any { } } -export function writeSolidity(code: string, filename = 'solidity') { - const extension = path.extname(filename) - const outputFile = extension === '.sol' ? filename : filename + '.sol' - debug(`About to write Solidity to file ${outputFile}`) +export function writeSourceCode( + code: string, + filename = 'source', + extension = '.sol', +) { + const fileExtension = path.extname(filename) + const outputFile = + fileExtension === extension ? filename : filename + extension + debug(`About to write source code to file ${outputFile}`) writeFile(outputFile, code, (err) => { if (err) { - throw new Error(`Failed to write Solidity to file ${outputFile}`, { - cause: err, - }) + throw new Error( + `Failed to write source code to file ${outputFile}`, + { + cause: err, + }, + ) } else { - console.log(`Solidity written to ${outputFile}`) + console.log(`Source code written to ${outputFile}`) } }) } diff --git a/tests/tests.sh b/tests/tests.sh index e55f139c..d126ff6d 100644 --- a/tests/tests.sh +++ b/tests/tests.sh @@ -18,6 +18,14 @@ sol2uml diff 0x1091588Cc431275F99DC5Df311fd8E1Ab81c89F3 0xEA24e9Bac006DE9635Ac7f sol2uml diff 0x1091588Cc431275F99DC5Df311fd8E1Ab81c89F3 0xEA24e9Bac006DE9635Ac7fA4D767fFb64FB5645c --aFile VaultStorage sol2uml diff 0xEA24e9Bac006DE9635Ac7fA4D767fFb64FB5645c .,node_modules +### OETH VaultAdmin +sol2uml diff 0x31a91336414d3B955E494E7d485a6B06b55FC8fB .,node_modules + +## Old Vault contract with a single, flattened file +sol2uml flatten 0x6bd6CC9605Ae43B424cB06363255b061A84DfFD3 +sol2uml diff 0x6bd6CC9605Ae43B424cB06363255b061A84DfFD3 Vault +sol2uml diff 0x6bd6CC9605Ae43B424cB06363255b061A84DfFD3 Vault.sol + ### OETH Frax Strategy ### Has added and changed contracts sol2uml diff 0x167747bf5b3b6bf2f7f7c4cce32c463e9598d425 0x5061cde874f75d119de3b07e191644097343ab9e -v @@ -76,6 +84,7 @@ sol2uml diff 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45 0x5615CDAb10dc425a742d64 sol2uml diff 0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45 0xB971eF87ede563556b2ED4b1C0b0019111Dd85d2 --bNetwork bsc # Storage + ## TestStorage sol2uml storage ../src/contracts -c TestStorage -v -o ../examples/storage @@ -122,3 +131,13 @@ sol2uml storage 0x8a3b6D3739461137d20825c36ED6016803d3104F \ ## Example from issue https://github.com/naddison36/sol2uml/issues/161 sol2uml storage 0xa90dAF1975BA13c26F63976e0Fd73A21F966EE0D --hideExpand __gap --network polygon -v + +# Class + +## Maker DSR Strategy Implementation +sol2uml 0x8a3b6D3739461137d20825c36ED6016803d3104F -v +## Contract on Polygon +sol2uml 0xa90dAF1975BA13c26F63976e0Fd73A21F966EE0D --network polygon -v + +## Local contracts +sol2uml ../src/contracts -b TestStorage -v