diff --git a/README.md b/README.md index d585407e..6a44ebc1 100644 --- a/README.md +++ b/README.md @@ -55,8 +55,9 @@ Options: -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 (choices: "mainnet", "goerli", "sepolia", "polygon", "arbitrum", "avalanche", "bsc", "crono", "fantom", "moonbeam", + -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") diff --git a/lib/parserEtherscan.d.ts b/lib/parserEtherscan.d.ts index 7e9333a4..1e0796ad 100644 --- a/lib/parserEtherscan.d.ts +++ b/lib/parserEtherscan.d.ts @@ -10,7 +10,7 @@ export declare class EtherscanParser { protected apikey: string; network: Network; readonly url: string; - constructor(apikey?: string, network?: Network); + constructor(apikey?: string, network?: Network, url?: string); /** * Parses the verified source code files from Etherscan * @param contractAddress Ethereum contract address with a 0x prefix diff --git a/lib/parserEtherscan.js b/lib/parserEtherscan.js index 21d11889..7ad51067 100644 --- a/lib/parserEtherscan.js +++ b/lib/parserEtherscan.js @@ -1,17 +1,23 @@ -"use strict"; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.parseRemapping = exports.parseRemappings = exports.EtherscanParser = exports.networks = void 0; -const axios_1 = __importDefault(require("axios")); -const parser_1 = require("@solidity-parser/parser"); -const converterAST2Classes_1 = require("./converterAST2Classes"); -const filterClasses_1 = require("./filterClasses"); -const regEx_1 = require("./utils/regEx"); -const path_1 = __importDefault(require("path")); -require('axios-debug-log'); -const debug = require('debug')('sol2uml'); +'use strict' +var __importDefault = + (this && this.__importDefault) || + function (mod) { + return mod && mod.__esModule ? mod : { default: mod } + } +Object.defineProperty(exports, '__esModule', { value: true }) +exports.parseRemapping = + exports.parseRemappings = + exports.EtherscanParser = + exports.networks = + void 0 +const axios_1 = __importDefault(require('axios')) +const parser_1 = require('@solidity-parser/parser') +const converterAST2Classes_1 = require('./converterAST2Classes') +const filterClasses_1 = require('./filterClasses') +const regEx_1 = require('./utils/regEx') +const path_1 = __importDefault(require('path')) +require('axios-debug-log') +const debug = require('debug')('sol2uml') exports.networks = [ 'mainnet', 'goerli', @@ -26,59 +32,57 @@ exports.networks = [ 'optimism', 'gnosis', 'celo', -]; +] class EtherscanParser { - constructor(apikey = 'ZAD4UI2RCXCQTP38EXS3UY2MPHFU5H9KB1', network = 'mainnet') { - this.apikey = apikey; - this.network = network; - if (!exports.networks.includes(network)) { - throw new Error(`Invalid network "${network}". Must be one of ${exports.networks}`); - } - else if (network === 'mainnet') { - this.url = 'https://api.etherscan.io/api'; - } - else if (network === 'polygon') { - this.url = 'https://api.polygonscan.com/api'; - this.apikey = 'AMHGNTV5A7XYGX2M781JB3RC1DZFVRWQEB'; - } - else if (network === 'arbitrum') { - this.url = 'https://api.arbiscan.io/api'; - this.apikey = 'ZGTK2TAGWMAB6IAC12BMK8YYPNCPIM8VDQ'; - } - else if (network === 'avalanche') { - this.url = 'https://api.snowtrace.io/api'; - this.apikey = 'U5FAN98S5XNH5VI83TI4H35R9I4TDCKEJY'; - } - else if (network === 'bsc') { - this.url = 'https://api.bscscan.com/api'; - this.apikey = 'APYH49FXVY9UA3KTDI6F4WP3KPIC86NITN'; - } - else if (network === 'crono') { - this.url = 'https://api.cronoscan.com/api'; - this.apikey = '76A3RG5WHTPMMR66E9SFI2EIDT6MP976W2'; - } - else if (network === 'fantom') { - this.url = 'https://api.ftmscan.com/api'; - this.apikey = '71KRX13XPZMGR3D1Q85W78G2DSZ4JPMAEX'; - } - else if (network === 'optimism') { - this.url = `https://api-optimistic.etherscan.io/api`; - this.apikey = 'FEXS1HXVA4Y2RNTMEA8V1UTK21S4JWHH9U'; - } - else if (network === 'moonbeam') { - this.url = 'https://api-moonbeam.moonscan.io/api'; - this.apikey = '5EUFXW6TDC16VERF3D9SCWRRU6AEMTBHNJ'; - } - else if (network === 'gnosis') { - this.url = 'https://api.gnosisscan.io/api'; - this.apikey = '2RWGXIWK538EJ8XSP9DE2JUINSCG7UCSJB'; + constructor( + apikey = 'ZAD4UI2RCXCQTP38EXS3UY2MPHFU5H9KB1', + network = 'mainnet', + url + ) { + this.apikey = apikey + this.network = network + if (url) { + this.url = url + return } - else if (network === 'celo') { - this.url = 'https://api.celoscan.io/api'; - this.apikey = 'JBV78T5KP15W7WKKKD6KC4J8RX2F4PK8AF'; - } - else { - this.url = `https://api-${network}.etherscan.io/api`; + if (!exports.networks.includes(network)) { + throw new Error( + `Invalid network "${network}". Must be one of ${exports.networks}` + ) + } else if (network === 'mainnet') { + this.url = 'https://api.etherscan.io/api' + } else if (network === 'polygon') { + this.url = 'https://api.polygonscan.com/api' + this.apikey = 'AMHGNTV5A7XYGX2M781JB3RC1DZFVRWQEB' + } else if (network === 'arbitrum') { + this.url = 'https://api.arbiscan.io/api' + this.apikey = 'ZGTK2TAGWMAB6IAC12BMK8YYPNCPIM8VDQ' + } else if (network === 'avalanche') { + this.url = 'https://api.snowtrace.io/api' + this.apikey = 'U5FAN98S5XNH5VI83TI4H35R9I4TDCKEJY' + } else if (network === 'bsc') { + this.url = 'https://api.bscscan.com/api' + this.apikey = 'APYH49FXVY9UA3KTDI6F4WP3KPIC86NITN' + } else if (network === 'crono') { + this.url = 'https://api.cronoscan.com/api' + this.apikey = '76A3RG5WHTPMMR66E9SFI2EIDT6MP976W2' + } else if (network === 'fantom') { + this.url = 'https://api.ftmscan.com/api' + this.apikey = '71KRX13XPZMGR3D1Q85W78G2DSZ4JPMAEX' + } else if (network === 'optimism') { + this.url = `https://api-optimistic.etherscan.io/api` + this.apikey = 'FEXS1HXVA4Y2RNTMEA8V1UTK21S4JWHH9U' + } else if (network === 'moonbeam') { + this.url = 'https://api-moonbeam.moonscan.io/api' + this.apikey = '5EUFXW6TDC16VERF3D9SCWRRU6AEMTBHNJ' + } else if (network === 'gnosis') { + this.url = 'https://api.gnosisscan.io/api' + this.apikey = '2RWGXIWK538EJ8XSP9DE2JUINSCG7UCSJB' + } else if (network === 'celo') { + this.url = 'https://api.celoscan.io/api' + this.apikey = 'JBV78T5KP15W7WKKKD6KC4J8RX2F4PK8AF' + } else { + this.url = `https://api-${network}.etherscan.io/api` } } /** @@ -87,18 +91,24 @@ class EtherscanParser { * @return Promise with an array of UmlClass objects */ async getUmlClasses(contractAddress) { - const { files, contractName, remappings } = await this.getSourceCode(contractAddress); - let umlClasses = []; + const { files, contractName, remappings } = await this.getSourceCode( + contractAddress + ) + let umlClasses = [] for (const file of files) { - debug(`Parsing source file ${file.filename}`); - const node = await this.parseSourceCode(file.code); - const umlClass = (0, converterAST2Classes_1.convertAST2UmlClasses)(node, file.filename, remappings); - umlClasses = umlClasses.concat(umlClass); + debug(`Parsing source file ${file.filename}`) + const node = await this.parseSourceCode(file.code) + const umlClass = (0, converterAST2Classes_1.convertAST2UmlClasses)( + node, + file.filename, + remappings + ) + umlClasses = umlClasses.concat(umlClass) } return { umlClasses, contractName, - }; + } } /** * Get Solidity code from Etherscan for a contract and merges all files @@ -107,51 +117,71 @@ class EtherscanParser { * @return Promise string of Solidity code */ async getSolidityCode(contractAddress, filename) { - const { files, contractName, compilerVersion, remappings } = await this.getSourceCode(contractAddress, filename); + const { files, contractName, compilerVersion, remappings } = + await this.getSourceCode(contractAddress, filename) // Parse the UmlClasses from the Solidity code in each file - let umlClasses = []; + let umlClasses = [] for (const file of files) { - const node = await this.parseSourceCode(file.code); - const umlClass = (0, converterAST2Classes_1.convertAST2UmlClasses)(node, file.filename, remappings); - umlClasses = umlClasses.concat(umlClass); + const node = await this.parseSourceCode(file.code) + const umlClass = (0, converterAST2Classes_1.convertAST2UmlClasses)( + node, + file.filename, + remappings + ) + umlClasses = umlClasses.concat(umlClass) } // Sort the classes so dependent code is first - const topologicalSortedClasses = (0, filterClasses_1.topologicalSortClasses)(umlClasses); + const topologicalSortedClasses = (0, + filterClasses_1.topologicalSortClasses)(umlClasses) // Get a list of filenames the classes are in - const sortedFilenames = topologicalSortedClasses.map((umlClass) => umlClass.relativePath); + const sortedFilenames = topologicalSortedClasses.map( + (umlClass) => umlClass.relativePath + ) // Remove duplicate filenames from the list - const dependentFilenames = [...new Set(sortedFilenames)]; + const dependentFilenames = [...new Set(sortedFilenames)] // find any files that didn't have dependencies found - const nonDependentFiles = files.filter((f) => !dependentFilenames.includes(f.filename)); - const nonDependentFilenames = nonDependentFiles.map((f) => f.filename); + const nonDependentFiles = files.filter( + (f) => !dependentFilenames.includes(f.filename) + ) + const nonDependentFilenames = nonDependentFiles.map((f) => f.filename) if (nonDependentFilenames.length) { - debug(`Failed to find dependencies to files: ${nonDependentFilenames}`); + debug( + `Failed to find dependencies to files: ${nonDependentFilenames}` + ) } - const solidityVersion = (0, regEx_1.parseSolidityVersion)(compilerVersion); - let solidityCode = `pragma solidity =${solidityVersion};\n`; + const solidityVersion = (0, regEx_1.parseSolidityVersion)( + compilerVersion + ) + let solidityCode = `pragma solidity =${solidityVersion};\n` // output non dependent code before the dependent files just in case sol2uml missed some dependencies - const filenames = [...nonDependentFilenames, ...dependentFilenames]; + const filenames = [...nonDependentFilenames, ...dependentFilenames] // For each filename filenames.forEach((filename) => { // Lookup the file that contains the Solidity code - const file = files.find((f) => f.filename === filename); + const file = files.find((f) => f.filename === filename) if (!file) - throw Error(`Failed to find file with filename "${filename}"`); + throw Error(`Failed to find file with filename "${filename}"`) // comment out any pragma solidity lines as its set from the compiler version - const removedPragmaSolidity = file.code.replace(/(\s)(pragma\s+solidity.*;)/gm, '$1/* $2 */'); + const removedPragmaSolidity = file.code.replace( + /(\s)(pragma\s+solidity.*;)/gm, + '$1/* $2 */' + ) // comment out any import statements // match whitespace before import // and characters after import up to ; // replace all in file and match across multiple lines - const removedImports = removedPragmaSolidity.replace(/^\s*?(import.*?;)/gms, '/* $1 */'); + const removedImports = removedPragmaSolidity.replace( + /^\s*?(import.*?;)/gms, + '/* $1 */' + ) // Rename SPDX-License-Identifier to SPDX--License-Identifier so the merged file will compile - const removedSPDX = removedImports.replace(/SPDX-/, 'SPDX--'); - solidityCode += removedSPDX; - }); + const removedSPDX = removedImports.replace(/SPDX-/, 'SPDX--') + solidityCode += removedSPDX + }) return { solidityCode, contractName, - }; + } } /** * Parses Solidity source code into an ASTNode object @@ -160,11 +190,13 @@ class EtherscanParser { */ async parseSourceCode(sourceCode) { try { - const node = (0, parser_1.parse)(sourceCode, {}); - return node; - } - catch (err) { - throw new Error(`Failed to parse solidity code from source code:\n${sourceCode}`, { cause: err }); + const node = (0, parser_1.parse)(sourceCode, {}) + return node + } catch (err) { + throw new Error( + `Failed to parse solidity code from source code:\n${sourceCode}`, + { cause: err } + ) } } /** @@ -173,9 +205,11 @@ class EtherscanParser { * @oaram filename optional, case-sensitive name of the source file without the .sol */ async getSourceCode(contractAddress, filename) { - const description = `get verified source code for address ${contractAddress} from Etherscan API.`; + const description = `get verified source code for address ${contractAddress} from Etherscan API` try { - debug(`About to get Solidity source code for ${contractAddress} from ${this.url}`); + debug( + `About to get Solidity source code for ${contractAddress} from ${this.url}` + ) const response = await axios_1.default.get(this.url, { params: { module: 'contract', @@ -183,62 +217,79 @@ class EtherscanParser { address: contractAddress, apikey: this.apikey, }, - }); + }) if (!Array.isArray(response?.data?.result)) { - throw new Error(`Failed to ${description}. No result array in HTTP data: ${JSON.stringify(response?.data)}`); + throw new Error( + `Failed to ${description}. No result array in HTTP data: ${JSON.stringify( + response?.data + )}` + ) } - let remappings; + let remappings const results = response.data.result.map((result) => { if (!result.SourceCode) { - throw new Error(`Failed to ${description}. Most likely the contract has not been verified on Etherscan.`); + throw new Error( + `Failed to ${description}. Most likely the contract has not been verified on Etherscan.` + ) } // if multiple Solidity source files if (result.SourceCode[0] === '{') { try { - let parableResultString = result.SourceCode; + let parableResultString = result.SourceCode // This looks like an Etherscan bug but we'll handle it here if (result.SourceCode[1] === '{') { // remove first { and last } from the SourceCode string so it can be JSON parsed - parableResultString = result.SourceCode.slice(1, -1); + parableResultString = result.SourceCode.slice(1, -1) } - const sourceCodeObject = JSON.parse(parableResultString); + const sourceCodeObject = JSON.parse(parableResultString) // Get any remapping of filenames from the settings - remappings = (0, exports.parseRemappings)(sourceCodeObject.settings?.remappings); + remappings = (0, exports.parseRemappings)( + sourceCodeObject.settings?.remappings + ) // The getsource response from Etherscan is inconsistent so we need to handle both shapes const sourceFiles = sourceCodeObject.sources ? Object.entries(sourceCodeObject.sources) - : Object.entries(sourceCodeObject); + : Object.entries(sourceCodeObject) return sourceFiles.map(([filename, code]) => ({ code: code.content, filename, - })); - } - catch (err) { - throw new Error(`Failed to parse Solidity source code from Etherscan's SourceCode. ${result.SourceCode}`, { cause: err }); + })) + } catch (err) { + throw new Error( + `Failed to parse Solidity source code from Etherscan's SourceCode. ${result.SourceCode}`, + { cause: err } + ) } } // if multiple Solidity source files with no Etherscan bug in the SourceCode field if (result?.SourceCode?.sources) { - const sourceFiles = Object.values(result.SourceCode.sources); + const sourceFiles = Object.values(result.SourceCode.sources) // Get any remapping of filenames from the settings - remappings = (0, exports.parseRemappings)(result.SourceCode.settings?.remappings); + remappings = (0, exports.parseRemappings)( + result.SourceCode.settings?.remappings + ) return sourceFiles.map(([filename, code]) => ({ code: code.content, filename, - })); + })) } // Solidity source code was not uploaded into multiple files so is just in the SourceCode field return { code: result.SourceCode, filename: contractAddress, - }; - }); - let files = results.flat(1); - const filenameWithExt = filename + '.sol'; + } + }) + let files = results.flat(1) + const filenameWithExt = filename + '.sol' if (filename) { - files = files.filter((r) => path_1.default.parse(r.filename).base == filenameWithExt); + files = files.filter( + (r) => + path_1.default.parse(r.filename).base == filenameWithExt + ) if (!files?.length) { - throw new Error(`Failed to find source file "${filename}" for contract ${contractAddress}`); + throw new Error( + `Failed to find source file "${filename}" for contract ${contractAddress}` + ) } } return { @@ -246,30 +297,31 @@ class EtherscanParser { contractName: response.data.result[0].ContractName, compilerVersion: response.data.result[0].CompilerVersion, remappings, - }; - } - catch (err) { + } + } catch (err) { if (err.message) { - throw err; + throw err } if (!err.response) { - throw new Error(`Failed to ${description}. No HTTP response.`); + throw new Error(`Failed to ${description}. No HTTP response.`) } - throw new Error(`Failed to ${description}. HTTP status code ${err.response?.status}, status text: ${err.response?.statusText}`, { cause: err }); + throw new Error( + `Failed to ${description}. HTTP status code ${err.response?.status}, status text: ${err.response?.statusText}`, + { cause: err } + ) } } } -exports.EtherscanParser = EtherscanParser; +exports.EtherscanParser = EtherscanParser /** * Parses Ethersan's remappings config in its API response * @param rawMappings */ const parseRemappings = (rawMappings) => { - if (!rawMappings) - return []; - return rawMappings.map((mapping) => (0, exports.parseRemapping)(mapping)); -}; -exports.parseRemappings = parseRemappings; + if (!rawMappings) return [] + return rawMappings.map((mapping) => (0, exports.parseRemapping)(mapping)) +} +exports.parseRemappings = parseRemappings /** * Parses a single mapping. For example * "@openzeppelin/=lib/openzeppelin-contracts/" @@ -278,13 +330,13 @@ exports.parseRemappings = parseRemappings; * @param mapping */ const parseRemapping = (mapping) => { - const equalIndex = mapping.indexOf('='); - const from = mapping.slice(0, equalIndex); - const to = mapping.slice(equalIndex + 1); + const equalIndex = mapping.indexOf('=') + const from = mapping.slice(0, equalIndex) + const to = mapping.slice(equalIndex + 1) return { from: new RegExp('^' + from), to, - }; -}; -exports.parseRemapping = parseRemapping; -//# sourceMappingURL=parserEtherscan.js.map \ No newline at end of file + } +} +exports.parseRemapping = parseRemapping +//# sourceMappingURL=parserEtherscan.js.map diff --git a/lib/parserGeneral.d.ts b/lib/parserGeneral.d.ts index 44111030..b721bae1 100644 --- a/lib/parserGeneral.d.ts +++ b/lib/parserGeneral.d.ts @@ -3,6 +3,7 @@ import { UmlClass } from './umlClass'; export interface ParserOptions { apiKey?: string; network?: Network; + explorerUrl?: string; subfolders?: string; ignoreFilesOrFolders?: string; } diff --git a/lib/parserGeneral.js b/lib/parserGeneral.js index d86087df..b92e9129 100644 --- a/lib/parserGeneral.js +++ b/lib/parserGeneral.js @@ -17,7 +17,7 @@ const parserUmlClasses = async (fileFolderAddress, options) => { if ((0, regEx_1.isAddress)(fileFolderAddress)) { debug(`argument ${fileFolderAddress} is an Ethereum address so checking Etherscan for the verified source code`); const etherscanApiKey = options.apiKey || 'ZAD4UI2RCXCQTP38EXS3UY2MPHFU5H9KB1'; - const etherscanParser = new parserEtherscan_1.EtherscanParser(etherscanApiKey, options.network); + const etherscanParser = new parserEtherscan_1.EtherscanParser(etherscanApiKey, options.network, options.explorerUrl); result = await etherscanParser.getUmlClasses(fileFolderAddress); } else { diff --git a/lib/sol2uml.js b/lib/sol2uml.js index 6f776434..754bf910 100755 --- a/lib/sol2uml.js +++ b/lib/sol2uml.js @@ -29,10 +29,11 @@ Can also flatten or compare verified source files on Etherscan-like explorers.`) .default('svg')) .option('-o, --outputFileName ', 'output file name') .option('-i, --ignoreFilesOrFolders ', 'comma separated list of files or folders to ignore') - .addOption(new commander_1.Option('-n, --network ', 'Ethereum network') + .addOption(new commander_1.Option('-n, --network ', 'Ethereum network which maps to a blockchain explorer') .choices(parserEtherscan_1.networks) .default('mainnet') .env('ETH_NETWORK')) + .addOption(new commander_1.Option('-e, --explorerUrl ', 'Override network with custom blockchain explorer API URL. eg Polygon Mumbai testnet https://api-testnet.polygonscan.com/api').env('EXPLORER_URL')) .addOption(new commander_1.Option('-k, --apiKey ', 'Blockchain explorer API key. eg Etherscan, Arbiscan, Optimism, BscScan, CronoScan, FTMScan, PolygonScan or SnowTrace API key').env('SCAN_API_KEY')) .option('-bc, --backColor ', 'Canvas background color. "none" will use a transparent canvas.', 'white') .option('-sc, --shapeColor ', 'Basic drawing color for graphics, not text', 'black') @@ -202,7 +203,7 @@ In order for the merged code to compile, the following is done: ...command.parent._optionValues, ...options, }; - const etherscanParser = new parserEtherscan_1.EtherscanParser(combinedOptions.apiKey, combinedOptions.network); + const etherscanParser = new parserEtherscan_1.EtherscanParser(combinedOptions.apiKey, combinedOptions.network, combinedOptions.explorerUrl); const { solidityCode, contractName } = await etherscanParser.getSolidityCode(contractAddress); // Write Solidity to the contract address const outputFilename = combinedOptions.outputFileName || contractName; @@ -239,7 +240,7 @@ The line numbers are from contract B. There are no line numbers for the red sect const lineBuffer = parseInt(options.lineBuffer); if (isNaN(lineBuffer)) throw Error(`Invalid line buffer "${options.lineBuffer}". Must be a number`); - const etherscanParser = new parserEtherscan_1.EtherscanParser(combinedOptions.apiKey, combinedOptions.network); + const etherscanParser = new parserEtherscan_1.EtherscanParser(combinedOptions.apiKey, combinedOptions.network, combinedOptions.explorerUrl); // Get verified Solidity code from Etherscan and flatten const { solidityCode: codeA, contractName: contractNameA } = await etherscanParser.getSolidityCode(addressA, combinedOptions.aFile); const { solidityCode: codeB, contractName: contractNameB } = await etherscanParser.getSolidityCode(addressB, combinedOptions.bFile || combinedOptions.aFile); diff --git a/package-lock.json b/package-lock.json index 4c5105a3..c30a2847 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sol2uml", - "version": "2.5.5", + "version": "2.5.6", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "sol2uml", - "version": "2.5.5", + "version": "2.5.6", "license": "MIT", "dependencies": { "@aduh95/viz.js": "^3.7.0", diff --git a/package.json b/package.json index 2d05ff17..c3b5a7ee 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sol2uml", - "version": "2.5.5", + "version": "2.5.6", "description": "Solidity contract visualisation tool.", "main": "./lib/index.js", "types": "./lib/index.d.ts", diff --git a/src/ts/parserEtherscan.ts b/src/ts/parserEtherscan.ts index 550e116e..84879d12 100644 --- a/src/ts/parserEtherscan.ts +++ b/src/ts/parserEtherscan.ts @@ -38,8 +38,13 @@ export class EtherscanParser { constructor( protected apikey: string = 'ZAD4UI2RCXCQTP38EXS3UY2MPHFU5H9KB1', - public network: Network = 'mainnet' + public network: Network = 'mainnet', + url?: string ) { + if (url) { + this.url = url + return + } if (!networks.includes(network)) { throw new Error( `Invalid network "${network}". Must be one of ${networks}` diff --git a/src/ts/parserGeneral.ts b/src/ts/parserGeneral.ts index d679b83a..bc47f374 100644 --- a/src/ts/parserGeneral.ts +++ b/src/ts/parserGeneral.ts @@ -8,6 +8,7 @@ const debug = require('debug')('sol2uml') export interface ParserOptions { apiKey?: string network?: Network + explorerUrl?: string subfolders?: string ignoreFilesOrFolders?: string } @@ -39,7 +40,8 @@ export const parserUmlClasses = async ( options.apiKey || 'ZAD4UI2RCXCQTP38EXS3UY2MPHFU5H9KB1' const etherscanParser = new EtherscanParser( etherscanApiKey, - options.network + options.network, + options.explorerUrl ) result = await etherscanParser.getUmlClasses(fileFolderAddress) diff --git a/src/ts/sol2uml.ts b/src/ts/sol2uml.ts index 79ff56bc..e8b5700b 100644 --- a/src/ts/sol2uml.ts +++ b/src/ts/sol2uml.ts @@ -50,11 +50,20 @@ Can also flatten or compare verified source files on Etherscan-like explorers.` 'comma separated list of files or folders to ignore' ) .addOption( - new Option('-n, --network ', 'Ethereum network') + new Option( + '-n, --network ', + 'Ethereum network which maps to a blockchain explorer' + ) .choices(networks) .default('mainnet') .env('ETH_NETWORK') ) + .addOption( + new Option( + '-e, --explorerUrl ', + 'Override network with custom blockchain explorer API URL. eg Polygon Mumbai testnet https://api-testnet.polygonscan.com/api' + ).env('EXPLORER_URL') + ) .addOption( new Option( '-k, --apiKey ', @@ -400,7 +409,8 @@ In order for the merged code to compile, the following is done: const etherscanParser = new EtherscanParser( combinedOptions.apiKey, - combinedOptions.network + combinedOptions.network, + combinedOptions.explorerUrl ) const { solidityCode, contractName } = @@ -480,7 +490,8 @@ The line numbers are from contract B. There are no line numbers for the red sect const etherscanParser = new EtherscanParser( combinedOptions.apiKey, - combinedOptions.network + combinedOptions.network, + combinedOptions.explorerUrl ) // Get verified Solidity code from Etherscan and flatten