diff --git a/packages/ethernaut-interact-ui/src/prompts/abi.js b/packages/ethernaut-interact-ui/src/prompts/abi.js index bccf4939..b022821a 100644 --- a/packages/ethernaut-interact-ui/src/prompts/abi.js +++ b/packages/ethernaut-interact-ui/src/prompts/abi.js @@ -1,5 +1,8 @@ const storage = require('ethernaut-interact/src/internal/storage') -const EtherscanApi = require('ethernaut-interact/src/internal/etherscan') +const { + EtherscanApi, + getEtherscanUrl, +} = require('ethernaut-interact/src/internal/etherscan') const prompt = require('ethernaut-common/src/prompt') const spinner = require('ethernaut-common/src/spinner') const debug = require('ethernaut-common/src/debug') @@ -44,25 +47,6 @@ module.exports = async function promptAbi({ abi, hre, address }) { } } -// TODO: Does etherscan support other networks? -// Where can I find a complete list of endpoints? -function getEtherscanUrl(chainId) { - switch (chainId) { - case 1: // Mainnet - return 'https://api.etherscan.io/api' - case 5: // Goerli - return 'https://api-goerli.etherscan.io/api' - case 10: // Optimism mainnet - return 'https://api-optimistic.etherscan.io/api' - case 420: // Optimism goerli - return 'https://api-goerli-optimistic.etherscan.io/api' - case 11155111: // Sepolia - return 'https://api-sepolia.etherscan.io/api' - default: - return undefined - } -} - async function selectStrategy(address, chainId) { // Collect available choices since // not all strategies might be available diff --git a/packages/ethernaut-interact/src/internal/etherscan.js b/packages/ethernaut-interact/src/internal/etherscan.js index fa138028..b87cb17f 100644 --- a/packages/ethernaut-interact/src/internal/etherscan.js +++ b/packages/ethernaut-interact/src/internal/etherscan.js @@ -72,4 +72,23 @@ class EtherscanApi { } } -module.exports = EtherscanApi +// TODO: Does etherscan support other networks? +// Where can I find a complete list of endpoints? +function getEtherscanUrl(chainId) { + switch (chainId) { + case 1: // Mainnet + return 'https://api.etherscan.io/api' + case 5: // Goerli + return 'https://api-goerli.etherscan.io/api' + case 10: // Optimism mainnet + return 'https://api-optimistic.etherscan.io/api' + case 420: // Optimism goerli + return 'https://api-goerli-optimistic.etherscan.io/api' + case 11155111: // Sepolia + return 'https://api-sepolia.etherscan.io/api' + default: + return undefined + } +} + +module.exports = { EtherscanApi, getEtherscanUrl } diff --git a/packages/ethernaut-interact/src/tasks/info.js b/packages/ethernaut-interact/src/tasks/info.js new file mode 100644 index 00000000..e1ef0e73 --- /dev/null +++ b/packages/ethernaut-interact/src/tasks/info.js @@ -0,0 +1,60 @@ +const output = require('ethernaut-common/src/output') +const { getChainId } = require('ethernaut-common/src/network') +const { + EtherscanApi, + getEtherscanUrl, +} = require('ethernaut-interact/src/internal/etherscan') +const { checkEnvVar } = require('ethernaut-common/src/check-env') + +require('../scopes/interact') + .task('info', 'Find information about a contract address using Etherscan') + .addPositionalParam( + 'address', + 'The address of the contract to get information about', + ) + .addFlag('abi', 'Show the ABI of the contract') + .addFlag('source', 'Show the source of the contract') + .setAction(async ({ address, abi, source }, hre) => { + try { + await checkEnvVar( + 'ETHERSCAN_API_KEY', + 'This key is required to fetch contract information from Etherscan', + ) + + const chainId = await getChainId(hre) + const etherscanUrl = getEtherscanUrl(chainId) + if (!etherscanUrl) { + return output.errorBox( + `Etherscan not supported on chain with id ${chainId}`, + ) + } + + const etherscan = new EtherscanApi( + process.env.ETHERSCAN_API_KEY, + etherscanUrl, + ) + + const info = await etherscan.getContractCode(address) + + if (!info) { + throw new Error('Contract not found') + } + + let strs = [] + strs.push(`Contract: ${info.ContractName}`) + strs.push(`Address: ${address}`) + strs.push(`Implementation: ${info.Implementation}`) + + if (abi) { + strs.push(`ABI: ${JSON.stringify(info.ABI, null, 2)}`) + } + + if (source) { + strs.push(`Source: ${info.SourceCode}`) + } + + return output.resultBox(strs.join('\n')) + } catch (err) { + return output.errorBox(err) + } + })