diff --git a/README.md b/README.md index 48948fb..d009789 100644 --- a/README.md +++ b/README.md @@ -57,8 +57,8 @@ Options: -f, --outputFormat output file format (choices: "png", "svg", "eps", "puml", default: "svg") -o, --outputFileName output file name. Defaults to shortened tx hashes joined together with a 'v' prefix for value transfer diagrams. -u, --url URL of the archive node with trace transaction support (default: "http://localhost:8545", env: ARCHIVE_NODE_URL) - -c, --chain blockchain explorer network to get source code from (choices: "mainnet", "goerli", "sepolia", "arbitrum", "optimisim", "polygon", "avalanche", "bsc", "crono", "fantom", "gnosis", - "moonbeam", default: "mainnet", env: ETH_NETWORK) + -c, --chain blockchain explorer to get contract source code from. `none` will not get any source code. `custom` will use the `explorerUrl` option. (choices: "mainnet", "custom", "none", "goerli", "sepolia", "arbitrum", "optimisim", "polygon", "avalanche", "bsc", "crono", "fantom", "gnosis", "moonbeam", "celo", "base", default: "mainnet", env: ETH_NETWORK) + -e, --explorerUrl required if a `custom` chain is used. eg a testnet like Polygon Mumbai https://api-testnet.polygonscan.com/api (env: EXPLORER_URL) -cf, --configFile name of the json configuration file that can override contract details like name and ABI (default: "tx.config.json") -af, --abiFile name of the json abi file that can override contract details like ABI (default: "tx.abi.json") -m, --memory max Java memory of PlantUML process in gigabytes. Java default is 1/4 of physical memory. Large txs in png format will need up to 12g. svg format is much better for large transactions. diff --git a/lib/callDiagram.js b/lib/callDiagram.js index 97086ad..beb6502 100644 --- a/lib/callDiagram.js +++ b/lib/callDiagram.js @@ -25,7 +25,7 @@ const generateCallDiagram = async (hashes, options) => { return new GethClient_1.default(options.url, options.chain); } })(); - const etherscanClient = new EtherscanClient_1.default(options.etherscanKey, options.chain); + const etherscanClient = new EtherscanClient_1.default(options.etherscanKey, options.chain, options.explorerUrl); const txManager = new transaction_1.TransactionManager(ethereumNodeClient, etherscanClient); let transactions = await txManager.getTransactions(hashes, options.chain); const transactionTracesUnfiltered = await txManager.getTraces(transactions); diff --git a/lib/clients/EthereumNodeClient.js b/lib/clients/EthereumNodeClient.js index 4d712df..f8d8070 100644 --- a/lib/clients/EthereumNodeClient.js +++ b/lib/clients/EthereumNodeClient.js @@ -40,6 +40,10 @@ const tokenInfoAddresses = { address: "0x04a05bE01C94d576B3eA3e824aF52668BAC606c0", ens: false, }, + base: { + address: "0x04a05bE01C94d576B3eA3e824aF52668BAC606c0", + ens: false, + }, }; const ProxySlot = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"; class EthereumNodeClient { @@ -61,8 +65,6 @@ class EthereumNodeClient { } }; this.ethersProvider = new ethers_1.providers.JsonRpcProvider(url); - if (!tokenInfoAddresses[network]) - throw Error(`Can not get token info from ${network} as TokenInfo contract has not been deployed`); this.tokenInfoAddress = tokenInfoAddresses[network]; } async getTransactionDetails(txHash) { @@ -108,6 +110,12 @@ class EthereumNodeClient { } } async getTokenDetails(contractAddresses) { + if (!this.tokenInfoAddress) { + return contractAddresses.map(address => ({ + address, + noContract: false, + })); + } const tokenInfo = new ethers_1.Contract(this.tokenInfoAddress.address, this.tokenInfoAddress.ens ? ABIs_1.TokenInfoEnsABI : ABIs_1.TokenInfoABI, this.ethersProvider); try { // Break up the calls into 10 contracts at a time diff --git a/lib/clients/EtherscanClient.d.ts b/lib/clients/EtherscanClient.d.ts index f8112ad..c7b36a9 100644 --- a/lib/clients/EtherscanClient.d.ts +++ b/lib/clients/EtherscanClient.d.ts @@ -1,9 +1,7 @@ -import { Contract, Network, Token } from "../types/tx2umlTypes"; +import { Contract, Network } from "../types/tx2umlTypes"; export default class EtherscanClient { - readonly apiKey: string; - readonly network: Network; + readonly apiKey?: string; readonly url: string; - constructor(apiKey?: string, network?: Network); + constructor(apiKey?: string, network?: Network, url?: string); getContract(contractAddress: string): Promise; - getToken(contractAddress: string): Promise; } diff --git a/lib/clients/EtherscanClient.js b/lib/clients/EtherscanClient.js index 8eabe25..acad2c5 100644 --- a/lib/clients/EtherscanClient.js +++ b/lib/clients/EtherscanClient.js @@ -5,63 +5,86 @@ var __importDefault = (this && this.__importDefault) || function (mod) { Object.defineProperty(exports, "__esModule", { value: true }); const axios_1 = __importDefault(require("axios")); const ethers_1 = require("ethers"); -const tx2umlTypes_1 = require("../types/tx2umlTypes"); -const regEx_1 = require("../utils/regEx"); const time_1 = require("../utils/time"); const debug = require("debug")("tx2uml"); class EtherscanClient { - constructor( - // Register your API key at https://etherscan.io/myapiKey - apiKey = "Q35WDQ2354617I8E2Z1E4WU3MIEP89DW9H", network = "mainnet") { - this.apiKey = apiKey; - this.network = network; - if (!tx2umlTypes_1.networks.includes(network)) { - throw new Error(`Invalid network "${network}". Must be one of ${tx2umlTypes_1.networks}`); + constructor(apiKey, network, url) { + if (network === "none") { + return; + } + if (network === "custom") { + if (!url || !apiKey) { + throw new Error("explorerUrl and etherscanKey options must be set for a custom network"); + } + this.url = url; + this.apiKey = apiKey; + return; } else if (network === "mainnet") { this.url = "https://api.etherscan.io/api"; + // Register your API key at https://etherscan.io/myapiKey + this.apiKey = apiKey || "Q35WDQ2354617I8E2Z1E4WU3MIEP89DW9H"; } else if (network === "polygon") { this.url = "https://api.polygonscan.com/api"; - this.apiKey = "AMHGNTV5A7XYGX2M781JB3RC1DZFVRWQEB"; + this.apiKey = apiKey || "AMHGNTV5A7XYGX2M781JB3RC1DZFVRWQEB"; } else if (network === "arbitrum") { this.url = "https://api.arbiscan.io/api"; - this.apiKey = "ZGTK2TAGWMAB6IAC12BMK8YYPNCPIM8VDQ"; + this.apiKey = apiKey || "ZGTK2TAGWMAB6IAC12BMK8YYPNCPIM8VDQ"; } else if (network === "avalanche") { this.url = "https://api.snowtrace.io/api"; - this.apiKey = "U5FAN98S5XNH5VI83TI4H35R9I4TDCKEJY"; + this.apiKey = apiKey || "U5FAN98S5XNH5VI83TI4H35R9I4TDCKEJY"; } else if (network === "bsc") { this.url = "https://api.bscscan.com/api"; - this.apiKey = "APYH49FXVY9UA3KTDI6F4WP3KPIC86NITN"; + this.apiKey = apiKey || "APYH49FXVY9UA3KTDI6F4WP3KPIC86NITN"; } else if (network === "crono") { this.url = "https://api.cronoscan.com/api"; - this.apiKey = "76A3RG5WHTPMMR66E9SFI2EIDT6MP976W2"; + this.apiKey = apiKey || "76A3RG5WHTPMMR66E9SFI2EIDT6MP976W2"; } else if (network === "fantom") { this.url = "https://api.ftmscan.com/api"; - this.apiKey = "71KRX13XPZMGR3D1Q85W78G2DSZ4JPMAEX"; + this.apiKey = apiKey || "71KRX13XPZMGR3D1Q85W78G2DSZ4JPMAEX"; } else if (network === "optimisim") { this.url = "https://api-optimistic.etherscan.io/api"; - this.apiKey = "FEXS1HXVA4Y2RNTMEA8V1UTK21S4JWHH9U"; + this.apiKey = apiKey || "FEXS1HXVA4Y2RNTMEA8V1UTK21S4JWHH9U"; } else if (network === "moonbeam") { this.url = "https://api-moonbeam.moonscan.io/api"; - this.apiKey = "5EUFXW6TDC16VERF3D9SCWRRU6AEMTBHNJ"; + this.apiKey = apiKey || "5EUFXW6TDC16VERF3D9SCWRRU6AEMTBHNJ"; } else if (network === "gnosis") { this.url = "https://api.gnosisscan.io/api"; - this.apiKey = "2RWGXIWK538EJ8XSP9DE2JUINSCG7UCSJB"; + this.apiKey = apiKey || "2RWGXIWK538EJ8XSP9DE2JUINSCG7UCSJB"; + } + else if (network === "celo") { + this.url = "https://api.celoscan.io/api"; + this.apiKey = apiKey || "JBV78T5KP15W7WKKKD6KC4J8RX2F4PK8AF"; + } + else if (network === "base") { + this.url = "https://api.basescan.org/api"; + this.apiKey = apiKey || "9I5HUJHPD4ZNXJ4M8TZJ1HD2QBVP1U3M3J"; } else { + if (!apiKey) { + throw new Error(`The etherscanKey option must be set for a "${network}" network`); + } this.url = `https://api-${network}.etherscan.io/api`; + this.apiKey = apiKey; } } async getContract(contractAddress) { + if (!this.url) { + return { + address: contractAddress, + noContract: false, + contractName: null, + }; + } try { const response = await axios_1.default.get(this.url, { params: { @@ -109,45 +132,6 @@ class EtherscanClient { throw new Error(`Failed to get contract details for contract ${contractAddress} from Etherscan using url ${this.url}`, { cause: err }); } } - // This only works with an Etherscan Pro account - async getToken(contractAddress) { - if (!contractAddress?.match(regEx_1.ethereumAddress)) { - throw new TypeError(`Contract address "${contractAddress}" must be 20 bytes in hexadecimal format with a 0x prefix`); - } - try { - const response = await axios_1.default.get(this.url, { - params: { - module: "token", - action: "tokeninfo", - contractaddress: contractAddress, - apiKey: this.apiKey, - }, - }); - if (response?.data?.status === "0") { - throw new Error(response?.data?.result); - } - if (!response?.data?.result) { - throw new Error(`no token attributes in Etherscan response: ${response?.data}`); - } - const attributes = response.data.result[0]; - const token = { - address: contractAddress, - name: attributes.name, - symbol: attributes.symbol, - decimals: attributes.decimals, - totalSupply: ethers_1.BigNumber.from(attributes.totalSupply), - }; - debug(`Got token from Etherscan for address ${contractAddress}:\n${JSON.stringify(token)}`); - return token; - } - catch (err) { - if (err?.response?.status === 404) { - debug(`Could not find token details for contract ${contractAddress} from Etherscan`); - return null; - } - throw new Error(`Failed to get token for address ${contractAddress} from Etherscan using url ${this.url}`, { cause: err }); - } - } } exports.default = EtherscanClient; //# sourceMappingURL=EtherscanClient.js.map \ No newline at end of file diff --git a/lib/tx2uml.js b/lib/tx2uml.js index eea3c52..b3e8639 100755 --- a/lib/tx2uml.js +++ b/lib/tx2uml.js @@ -21,10 +21,11 @@ program .addOption(new commander_1.Option("-u, --url ", "URL of the archive node with trace transaction support") .env("ARCHIVE_NODE_URL") .default("http://localhost:8545")) - .addOption(new commander_1.Option("-c, --chain ", "blockchain explorer network to get source code from") + .addOption(new commander_1.Option("-c, --chain ", "blockchain explorer to get contract source code from. `none` will not get any source code. `custom` will use the `explorerUrl` option.") .choices(tx2umlTypes_1.networks) .default("mainnet") .env("ETH_NETWORK")) + .addOption(new commander_1.Option("-e, --explorerUrl ", "required if a `custom` chain is used. eg a testnet like Polygon Mumbai https://api-testnet.polygonscan.com/api").env("EXPLORER_URL")) .option("-cf, --configFile ", "name of the json configuration file that can override contract details like name and ABI", "tx.config.json") .option("-af, --abiFile ", "name of the json abi file that can override contract details like ABI", "tx.abi.json") .option("-m, --memory ", "max Java memory of PlantUML process in gigabytes. Java default is 1/4 of physical memory. Large txs in png format will need up to 12g. svg format is much better for large transactions.") diff --git a/lib/types/tx2umlTypes.d.ts b/lib/types/tx2umlTypes.d.ts index b9c12a0..61c782a 100644 --- a/lib/types/tx2umlTypes.d.ts +++ b/lib/types/tx2umlTypes.d.ts @@ -150,7 +150,7 @@ export type ParamTypeInternal = { components?: ParamTypeInternal[]; }; export declare const nodeTypes: readonly ["geth", "erigon", "nether", "openeth", "tgeth", "besu", "anvil"]; -export declare const networks: readonly ["mainnet", "goerli", "sepolia", "arbitrum", "optimisim", "polygon", "avalanche", "bsc", "crono", "fantom", "gnosis", "moonbeam"]; +export declare const networks: readonly ["mainnet", "custom", "none", "goerli", "sepolia", "arbitrum", "optimisim", "polygon", "avalanche", "bsc", "crono", "fantom", "gnosis", "moonbeam", "celo", "base"]; export type Network = (typeof networks)[number]; export declare const setNetworkCurrency: (network: Network) => "AVAX" | "MATIC" | "BNB" | "CRO" | "FTM" | "xDAI" | "GLMR" | "ETH"; export declare const outputFormats: readonly ["png", "svg", "eps", "puml"]; @@ -182,6 +182,7 @@ export interface SourceMap { } export interface CallDiagramOptions extends TracePumlGenerationOptions { chain?: Network; + explorerUrl?: string; url?: string; nodeType: string; noAddresses?: string[]; @@ -192,6 +193,7 @@ export interface CallDiagramOptions extends TracePumlGenerationOptions { } export interface TransferPumlGenerationOptions extends OutputOptions { chain?: Network; + explorerUrl?: string; url?: string; etherscanKey?: string; configFile?: string; diff --git a/lib/types/tx2umlTypes.js b/lib/types/tx2umlTypes.js index 1ea3f85..00bbea8 100644 --- a/lib/types/tx2umlTypes.js +++ b/lib/types/tx2umlTypes.js @@ -27,6 +27,8 @@ exports.nodeTypes = [ ]; exports.networks = [ "mainnet", + "custom", + "none", "goerli", "sepolia", "arbitrum", @@ -38,6 +40,8 @@ exports.networks = [ "fantom", "gnosis", "moonbeam", + "celo", + "base", ]; const setNetworkCurrency = (network) => network === "avalanche" ? "AVAX" diff --git a/lib/valueDiagram.js b/lib/valueDiagram.js index 3372d77..45d1977 100644 --- a/lib/valueDiagram.js +++ b/lib/valueDiagram.js @@ -15,7 +15,7 @@ const utils_1 = require("ethers/lib/utils"); const generateValueDiagram = async (hashes, options) => { const gethClient = new GethClient_1.default(options.url, options.chain); // Initiate Etherscan client - const etherscanClient = new EtherscanClient_1.default(options.etherscanKey, options.chain); + const etherscanClient = new EtherscanClient_1.default(options.etherscanKey, options.chain, options.explorerUrl); const txManager = new transaction_1.TransactionManager(gethClient, etherscanClient); let transactions = await txManager.getTransactions(hashes, options.chain); const transactionTransfers = options.onlyToken diff --git a/src/contracts/README.md b/src/contracts/README.md index 7055591..5507df1 100644 --- a/src/contracts/README.md +++ b/src/contracts/README.md @@ -79,7 +79,7 @@ See ENS's [Reverse records](https://github.com/ensdomains/reverse-records/#deplo [TokenDetails](./TokenInfo.sol) takes a constructor parameter `_reverseRecords` which is the address of Ethereum Name Service's `ReverseRecords` contract. This is only used on Goerli and Mainnet. | Chain | Address | \_reverseRecords | -| --------- | ------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | +|-----------| ------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | Mainnet | [0xEf6B7d3885f4Af1bDfcB66FE0370D6012B38a8Db](https://etherscan.io/address/0xEf6B7d3885f4Af1bDfcB66FE0370D6012B38a8Db#code) | [0x3671aE578E63FdF66ad4F3E12CC0c0d71Ac7510C](https://etherscan.io/address/0x3671aE578E63FdF66ad4F3E12CC0c0d71Ac7510C) | | Goerli | [0x0395f995f2cecc40e2bf45d4905004313fcece6e](https://goerli.etherscan.io/address/0x0395f995f2cecc40e2bf45d4905004313fcece6e#code) | [0x333Fc8f550043f239a2CF79aEd5e9cF4A20Eb41e](https://goerli.etherscan.io/address/0x333Fc8f550043f239a2CF79aEd5e9cF4A20Eb41e) | | Sepolia | [0xe147cb7d90b9253844130e2c4a7ef0ffb641c3ea](https://sepolia.etherscan.io/address/0xe147cb7d90b9253844130e2c4a7ef0ffb641c3ea#code) | 0x0000000000000000000000000000000000000000 | @@ -88,7 +88,11 @@ See ENS's [Reverse records](https://github.com/ensdomains/reverse-records/#deplo | Optimism | [0x8E2587265C68CD9EE3EcBf22DC229980b47CB960](https://optimistic.etherscan.io/address/0x8E2587265C68CD9EE3EcBf22DC229980b47CB960#code) | 0x0000000000000000000000000000000000000000 | | Avalanche | [0x4e557a2936D3a4Ec2cA4981e6cCCfE330C1634DF](https://snowtrace.io/address/0x4e557a2936D3a4Ec2cA4981e6cCCfE330C1634DF#code) | 0x0000000000000000000000000000000000000000 | | Gnosis | [0x04a05bE01C94d576B3eA3e824aF52668BAC606c0](https://gnosisscan.io/address/0x04a05be01c94d576b3ea3e824af52668bac606c0#code) | 0x0000000000000000000000000000000000000000 | +| Base | [0x04a05bE01C94d576B3eA3e824aF52668BAC606c0](https://basescan.org/address/0x04a05bE01C94d576B3eA3e824aF52668BAC606c0) | 0x0000000000000000000000000000000000000000 | | BSC | | 0x0000000000000000000000000000000000000000 | | Crono | | 0x0000000000000000000000000000000000000000 | | Fantom | | 0x0000000000000000000000000000000000000000 | | Moonbeam | | 0x0000000000000000000000000000000000000000 | +| Celo | | 0x0000000000000000000000000000000000000000 | + +Deployments can be done using [Remix](https://remix.ethereum.org/) diff --git a/src/ts/callDiagram.ts b/src/ts/callDiagram.ts index 6cb0ab0..9a0a6c1 100644 --- a/src/ts/callDiagram.ts +++ b/src/ts/callDiagram.ts @@ -29,7 +29,8 @@ export const generateCallDiagram = async ( const etherscanClient = new EtherscanClient( options.etherscanKey, - options.chain + options.chain, + options.explorerUrl ) const txManager = new TransactionManager( ethereumNodeClient, diff --git a/src/ts/clients/EthereumNodeClient.ts b/src/ts/clients/EthereumNodeClient.ts index 500d309..e8f1657 100644 --- a/src/ts/clients/EthereumNodeClient.ts +++ b/src/ts/clients/EthereumNodeClient.ts @@ -64,6 +64,10 @@ const tokenInfoAddresses: { address: "0x04a05bE01C94d576B3eA3e824aF52668BAC606c0", ens: false, }, + base: { + address: "0x04a05bE01C94d576B3eA3e824aF52668BAC606c0", + ens: false, + }, } const ProxySlot = @@ -78,10 +82,6 @@ export default abstract class EthereumNodeClient { public readonly network: Network ) { this.ethersProvider = new providers.JsonRpcProvider(url) - if (!tokenInfoAddresses[network]) - throw Error( - `Can not get token info from ${network} as TokenInfo contract has not been deployed` - ) this.tokenInfoAddress = tokenInfoAddresses[network] } @@ -150,6 +150,12 @@ export default abstract class EthereumNodeClient { async getTokenDetails( contractAddresses: string[] ): Promise { + if (!this.tokenInfoAddress) { + return contractAddresses.map(address => ({ + address, + noContract: false, + })) + } const tokenInfo = new Contract( this.tokenInfoAddress.address, this.tokenInfoAddress.ens ? TokenInfoEnsABI : TokenInfoABI, diff --git a/src/ts/clients/EtherscanClient.ts b/src/ts/clients/EtherscanClient.ts index 541149e..d583a88 100644 --- a/src/ts/clients/EtherscanClient.ts +++ b/src/ts/clients/EtherscanClient.ts @@ -1,59 +1,84 @@ import axios from "axios" -import { BigNumber, Contract as EthersContract } from "ethers" +import { Contract as EthersContract } from "ethers" -import { Contract, Network, networks, Token } from "../types/tx2umlTypes" -import { ethereumAddress } from "../utils/regEx" +import { Contract, Network } from "../types/tx2umlTypes" import { sleep } from "../utils/time" const debug = require("debug")("tx2uml") export default class EtherscanClient { + public readonly apiKey?: string public readonly url: string - constructor( - // Register your API key at https://etherscan.io/myapiKey - public readonly apiKey: string = "Q35WDQ2354617I8E2Z1E4WU3MIEP89DW9H", - public readonly network: Network = "mainnet" - ) { - if (!networks.includes(network)) { - throw new Error( - `Invalid network "${network}". Must be one of ${networks}` - ) + constructor(apiKey?: string, network?: Network, url?: string) { + if (network === "none") { + return + } + if (network === "custom") { + if (!url || !apiKey) { + throw new Error( + "explorerUrl and etherscanKey options must be set for a custom network" + ) + } + this.url = url + this.apiKey = apiKey + return } else if (network === "mainnet") { this.url = "https://api.etherscan.io/api" + // Register your API key at https://etherscan.io/myapiKey + this.apiKey = apiKey || "Q35WDQ2354617I8E2Z1E4WU3MIEP89DW9H" } else if (network === "polygon") { this.url = "https://api.polygonscan.com/api" - this.apiKey = "AMHGNTV5A7XYGX2M781JB3RC1DZFVRWQEB" + this.apiKey = apiKey || "AMHGNTV5A7XYGX2M781JB3RC1DZFVRWQEB" } else if (network === "arbitrum") { this.url = "https://api.arbiscan.io/api" - this.apiKey = "ZGTK2TAGWMAB6IAC12BMK8YYPNCPIM8VDQ" + this.apiKey = apiKey || "ZGTK2TAGWMAB6IAC12BMK8YYPNCPIM8VDQ" } else if (network === "avalanche") { this.url = "https://api.snowtrace.io/api" - this.apiKey = "U5FAN98S5XNH5VI83TI4H35R9I4TDCKEJY" + this.apiKey = apiKey || "U5FAN98S5XNH5VI83TI4H35R9I4TDCKEJY" } else if (network === "bsc") { this.url = "https://api.bscscan.com/api" - this.apiKey = "APYH49FXVY9UA3KTDI6F4WP3KPIC86NITN" + this.apiKey = apiKey || "APYH49FXVY9UA3KTDI6F4WP3KPIC86NITN" } else if (network === "crono") { this.url = "https://api.cronoscan.com/api" - this.apiKey = "76A3RG5WHTPMMR66E9SFI2EIDT6MP976W2" + this.apiKey = apiKey || "76A3RG5WHTPMMR66E9SFI2EIDT6MP976W2" } else if (network === "fantom") { this.url = "https://api.ftmscan.com/api" - this.apiKey = "71KRX13XPZMGR3D1Q85W78G2DSZ4JPMAEX" + this.apiKey = apiKey || "71KRX13XPZMGR3D1Q85W78G2DSZ4JPMAEX" } else if (network === "optimisim") { this.url = "https://api-optimistic.etherscan.io/api" - this.apiKey = "FEXS1HXVA4Y2RNTMEA8V1UTK21S4JWHH9U" + this.apiKey = apiKey || "FEXS1HXVA4Y2RNTMEA8V1UTK21S4JWHH9U" } else if (network === "moonbeam") { this.url = "https://api-moonbeam.moonscan.io/api" - this.apiKey = "5EUFXW6TDC16VERF3D9SCWRRU6AEMTBHNJ" + this.apiKey = apiKey || "5EUFXW6TDC16VERF3D9SCWRRU6AEMTBHNJ" } else if (network === "gnosis") { this.url = "https://api.gnosisscan.io/api" - this.apiKey = "2RWGXIWK538EJ8XSP9DE2JUINSCG7UCSJB" + this.apiKey = apiKey || "2RWGXIWK538EJ8XSP9DE2JUINSCG7UCSJB" + } else if (network === "celo") { + this.url = "https://api.celoscan.io/api" + this.apiKey = apiKey || "JBV78T5KP15W7WKKKD6KC4J8RX2F4PK8AF" + } else if (network === "base") { + this.url = "https://api.basescan.org/api" + this.apiKey = apiKey || "9I5HUJHPD4ZNXJ4M8TZJ1HD2QBVP1U3M3J" } else { + if (!apiKey) { + throw new Error( + `The etherscanKey option must be set for a "${network}" network` + ) + } this.url = `https://api-${network}.etherscan.io/api` + this.apiKey = apiKey } } async getContract(contractAddress: string): Promise { + if (!this.url) { + return { + address: contractAddress, + noContract: false, + contractName: null, + } + } try { const response = await axios.get(this.url, { params: { @@ -123,61 +148,4 @@ export default class EtherscanClient { ) } } - - // This only works with an Etherscan Pro account - async getToken(contractAddress: string): Promise { - if (!contractAddress?.match(ethereumAddress)) { - throw new TypeError( - `Contract address "${contractAddress}" must be 20 bytes in hexadecimal format with a 0x prefix` - ) - } - - try { - const response = await axios.get(this.url, { - params: { - module: "token", - action: "tokeninfo", - contractaddress: contractAddress, - apiKey: this.apiKey, - }, - }) - if (response?.data?.status === "0") { - throw new Error(response?.data?.result) - } - if (!response?.data?.result) { - throw new Error( - `no token attributes in Etherscan response: ${response?.data}` - ) - } - - const attributes = response.data.result[0] - - const token: Token = { - address: contractAddress, - name: attributes.name, - symbol: attributes.symbol, - decimals: attributes.decimals, - totalSupply: BigNumber.from(attributes.totalSupply), - } - - debug( - `Got token from Etherscan for address ${contractAddress}:\n${JSON.stringify( - token - )}` - ) - - return token - } catch (err) { - if (err?.response?.status === 404) { - debug( - `Could not find token details for contract ${contractAddress} from Etherscan` - ) - return null - } - throw new Error( - `Failed to get token for address ${contractAddress} from Etherscan using url ${this.url}`, - { cause: err } - ) - } - } } diff --git a/src/ts/tx2uml.ts b/src/ts/tx2uml.ts index fbf4634..a98427a 100644 --- a/src/ts/tx2uml.ts +++ b/src/ts/tx2uml.ts @@ -44,12 +44,18 @@ program .addOption( new Option( "-c, --chain ", - "blockchain explorer network to get source code from" + "blockchain explorer to get contract source code from. `none` will not get any source code. `custom` will use the `explorerUrl` option." ) .choices(networks) .default("mainnet") .env("ETH_NETWORK") ) + .addOption( + new Option( + "-e, --explorerUrl ", + "required if a `custom` chain is used. eg a testnet like Polygon Mumbai https://api-testnet.polygonscan.com/api" + ).env("EXPLORER_URL") + ) .option( "-cf, --configFile ", "name of the json configuration file that can override contract details like name and ABI", diff --git a/src/ts/types/tx2umlTypes.ts b/src/ts/types/tx2umlTypes.ts index e6fc6af..c39a9aa 100644 --- a/src/ts/types/tx2umlTypes.ts +++ b/src/ts/types/tx2umlTypes.ts @@ -172,6 +172,8 @@ export const nodeTypes = [ export const networks = [ "mainnet", + "custom", + "none", "goerli", "sepolia", "arbitrum", @@ -183,6 +185,8 @@ export const networks = [ "fantom", "gnosis", "moonbeam", + "celo", + "base", ] export type Network = (typeof networks)[number] @@ -201,6 +205,8 @@ export const setNetworkCurrency = (network: Network) => ? "xDAI" : network === "moonbeam" ? "GLMR" + : network === "celo" + ? "CELO" : "ETH" export const outputFormats = ["png", "svg", "eps", "puml"] @@ -237,6 +243,7 @@ export interface SourceMap { export interface CallDiagramOptions extends TracePumlGenerationOptions { chain?: Network + explorerUrl?: string url?: string nodeType: string noAddresses?: string[] @@ -248,6 +255,7 @@ export interface CallDiagramOptions extends TracePumlGenerationOptions { export interface TransferPumlGenerationOptions extends OutputOptions { chain?: Network + explorerUrl?: string url?: string etherscanKey?: string configFile?: string diff --git a/src/ts/valueDiagram.ts b/src/ts/valueDiagram.ts index caa1a0f..8fca9bf 100644 --- a/src/ts/valueDiagram.ts +++ b/src/ts/valueDiagram.ts @@ -21,7 +21,8 @@ export const generateValueDiagram = async ( // Initiate Etherscan client const etherscanClient = new EtherscanClient( options.etherscanKey, - options.chain + options.chain, + options.explorerUrl ) const txManager = new TransactionManager(gethClient, etherscanClient) diff --git a/tests/tests.sh b/tests/tests.sh index f5f6f8b..d0e40bf 100644 --- a/tests/tests.sh +++ b/tests/tests.sh @@ -94,6 +94,8 @@ tx2uml 0xfb153c572e304093023b4f9694ef39135b6ed5b2515453173e81ec02df2e2104 -v --c ##### Avalanche export ARCHIVE_NODE_URL=https://api.avax.network/ext/bc/C/rpc +# Using an archive node +tx2uml call 0xb145f0f6838cd75b1d9086bc52577eefb14f18ac25f443fe60c55bcaff198dde -v --chain avalanche ## Unwrapping WETH.e tx2uml value 0xb145f0f6838cd75b1d9086bc52577eefb14f18ac25f443fe60c55bcaff198dde -v --onlyToken --chain avalanche # ParaSwap 393.94480757 BTC.b for 5,355.278278395507586356 WETH.e @@ -115,3 +117,11 @@ tx2uml value 0x5db5c2400ab56db697b3cc9aa02a05deab658e1438ce2f8692ca009cc45171dd tx2uml value 0xb252ebbce1aead091c767463a242feb9e470d8d920a67f89c85449e676662584 -v --onlyToken --chain arbitrum # 1Inch swap tx2uml value 0x28650d09908542f6d1e08abeb476cb576d7daf72b0cef81aa24c142206b35f7c -v --onlyToken --chain arbitrum +# using QuickNode which supports custom tracers +tx2uml value 0x28650d09908542f6d1e08abeb476cb576d7daf72b0cef81aa24c142206b35f7c -v --chain arbitrum + +### Base +tx2uml value -c base -v 0x83fa7d62d5790e62010ad93c91eeab146171a1f4fa54897a8e540ef29ab98f17 +tx2uml value -c base -v 0x83fa7d62d5790e62010ad93c91eeab146171a1f4fa54897a8e540ef29ab98f17 -f png +tx2uml call -c base -v 0x83fa7d62d5790e62010ad93c91eeab146171a1f4fa54897a8e540ef29ab98f17 +tx2uml call -c base -v -g -pv -l 0x83fa7d62d5790e62010ad93c91eeab146171a1f4fa54897a8e540ef29ab98f17