diff --git a/common/cli-utils.js b/common/cli-utils.js new file mode 100644 index 000000000..88fbc4a90 --- /dev/null +++ b/common/cli-utils.js @@ -0,0 +1,87 @@ +'use strict'; + +require('dotenv').config(); + +const { Option } = require('commander'); + +const addBaseOptions = (program, options = {}) => { + program.addOption( + new Option('-e, --env ', 'environment') + .choices(['local', 'devnet', 'devnet-amplifier', 'devnet-verifiers', 'stagenet', 'testnet', 'mainnet']) + .default('testnet') + .makeOptionMandatory(true) + .env('ENV'), + ); + program.addOption(new Option('-y, --yes', 'skip deployment prompt confirmation').env('YES')); + program.addOption(new Option('--parallel', 'run script parallely wrt chains')); + program.addOption(new Option('--saveChainSeparately', 'save chain info separately')); + program.addOption(new Option('--gasOptions ', 'gas options cli override')); + + if (!options.ignoreChainNames) { + program.addOption( + new Option('-n, --chainNames ', 'chains to run the script over').makeOptionMandatory(true).env('CHAINS'), + ); + program.addOption(new Option('--skipChains ', 'chains to skip over')); + program.addOption( + new Option( + '--startFromChain ', + 'start from a specific chain onwards in the config, useful when a cmd fails for an intermediate chain', + ), + ); + } + + if (!options.ignorePrivateKey) { + program.addOption(new Option('-p, --privateKey ', 'private key').makeOptionMandatory(true).env('PRIVATE_KEY')); + } + + if (options.address) { + program.addOption(new Option('-a, --address
', 'override address')); + } + + return program; +}; + +const addExtendedOptions = (program, options = {}) => { + addBaseOptions(program, options); + + program.addOption(new Option('-v, --verify', 'verify the deployed contract on the explorer').env('VERIFY')); + + if (options.artifactPath) { + program.addOption(new Option('--artifactPath ', 'artifact path')); + } + + if (options.contractName) { + program.addOption(new Option('-c, --contractName ', 'contract name').makeOptionMandatory(true)); + } + + if (options.deployMethod) { + program.addOption( + new Option('-m, --deployMethod ', 'deployment method') + .choices(['create', 'create2', 'create3']) + .default(options.deployMethod), + ); + } + + if (options.salt) { + program.addOption(new Option('-s, --salt ', 'salt to use for create2 deployment').env('SALT')); + } + + if (options.skipExisting) { + program.addOption(new Option('-x, --skipExisting', 'skip existing if contract was already deployed on chain').env('SKIP_EXISTING')); + } + + if (options.upgrade) { + program.addOption(new Option('-u, --upgrade', 'upgrade a deployed contract').env('UPGRADE')); + } + + if (options.predictOnly) { + program.addOption(new Option('--predictOnly', 'output the predicted changes only').env('PREDICT_ONLY')); + } + + return program; +}; + +module.exports = { + addBaseOptions, + addExtendedOptions, +}; diff --git a/common/index.js b/common/index.js new file mode 100644 index 000000000..ceab4806c --- /dev/null +++ b/common/index.js @@ -0,0 +1,4 @@ +module.exports = { + ...require('./cli-utils'), + ...require('./utils'), +}; diff --git a/common/utils.js b/common/utils.js new file mode 100644 index 000000000..2c2a85c2f --- /dev/null +++ b/common/utils.js @@ -0,0 +1,314 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const { outputJsonSync } = require('fs-extra'); +const chalk = require('chalk'); +const https = require('https'); +const http = require('http'); +const readlineSync = require('readline-sync'); + +function loadConfig(env) { + return require(`${__dirname}/../axelar-chains-config/info/${env}.json`); +} + +function saveConfig(config, env) { + writeJSON(config, `${__dirname}/../axelar-chains-config/info/${env}.json`); +} + +const writeJSON = (data, name) => { + outputJsonSync(name, data, { + spaces: 2, + EOL: '\n', + }); +}; + +const printInfo = (msg, info = '', colour = chalk.green) => { + if (info) { + console.log(`${msg}: ${colour(info)}\n`); + } else { + console.log(`${msg}\n`); + } +}; + +const printWarn = (msg, info = '') => { + if (info) { + msg = `${msg}: ${info}`; + } + + console.log(`${chalk.italic.yellow(msg)}\n`); +}; + +const printError = (msg, info = '') => { + if (info) { + msg = `${msg}: ${info}`; + } + + console.log(`${chalk.bold.red(msg)}\n`); +}; + +function printLog(log) { + console.log(JSON.stringify({ log }, null, 2)); +} + +const isNonEmptyString = (arg) => { + return typeof arg === 'string' && arg !== ''; +}; + +const isString = (arg) => { + return typeof arg === 'string'; +}; + +const isStringArray = (arr) => Array.isArray(arr) && arr.every(isString); + +const isNumber = (arg) => { + return Number.isInteger(arg); +}; + +const isValidNumber = (arg) => { + return !isNaN(parseInt(arg)) && isFinite(arg); +}; + +const isValidDecimal = (arg) => { + return !isNaN(parseFloat(arg)) && isFinite(arg); +}; + +const isNumberArray = (arr) => { + if (!Array.isArray(arr)) { + return false; + } + + for (const item of arr) { + if (!isNumber(item)) { + return false; + } + } + + return true; +}; + +const isNonEmptyStringArray = (arr) => { + if (!Array.isArray(arr)) { + return false; + } + + for (const item of arr) { + if (typeof item !== 'string') { + return false; + } + } + + return true; +}; + +function copyObject(obj) { + return JSON.parse(JSON.stringify(obj)); +} + +const httpGet = (url) => { + return new Promise((resolve, reject) => { + (url.startsWith('https://') ? https : http).get(url, (res) => { + const { statusCode } = res; + const contentType = res.headers['content-type']; + let error; + + if (statusCode !== 200 && statusCode !== 301) { + error = new Error('Request Failed.\n' + `Request: ${url}\nStatus Code: ${statusCode}`); + } else if (!/^application\/json/.test(contentType)) { + error = new Error('Invalid content-type.\n' + `Expected application/json but received ${contentType}`); + } + + if (error) { + res.resume(); + reject(error); + return; + } + + res.setEncoding('utf8'); + let rawData = ''; + res.on('data', (chunk) => { + rawData += chunk; + }); + res.on('end', () => { + try { + const parsedData = JSON.parse(rawData); + resolve(parsedData); + } catch (e) { + reject(e); + } + }); + }); + }); +}; + +const httpPost = async (url, data) => { + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }); + return response.json(); +}; + +/** + * Parses the input string into an array of arguments, recognizing and converting + * to the following types: boolean, number, array, and string. + * + * @param {string} args - The string of arguments to parse. + * + * @returns {Array} - An array containing parsed arguments. + * + * @example + * const input = "hello true 123 [1,2,3]"; + * const output = parseArgs(input); + * console.log(output); // Outputs: [ 'hello', true, 123, [ 1, 2, 3] ] + */ +const parseArgs = (args) => { + return args + .split(/\s+/) + .filter((item) => item !== '') + .map((arg) => { + if (arg.startsWith('[') && arg.endsWith(']')) { + return JSON.parse(arg); + } else if (arg === 'true') { + return true; + } else if (arg === 'false') { + return false; + } else if (!isNaN(arg) && !arg.startsWith('0x')) { + return Number(arg); + } + + return arg; + }); +}; + +function sleep(ms) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +function timeout(prom, time, exception) { + let timer; + + // Racing the promise with a timer + // If the timer resolves first, the promise is rejected with the exception + return Promise.race([prom, new Promise((resolve, reject) => (timer = setTimeout(reject, time, exception)))]).finally(() => + clearTimeout(timer), + ); +} + +/** + * Validate if the input string matches the time format YYYY-MM-DDTHH:mm:ss + * + * @param {string} timeString - The input time string. + * @return {boolean} - Returns true if the format matches, false otherwise. + */ +function isValidTimeFormat(timeString) { + const regex = /^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|1\d|2\d|3[01])T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d$/; + + if (timeString === '0') { + return true; + } + + return regex.test(timeString); +} + +const dateToEta = (utcTimeString) => { + if (utcTimeString === '0') { + return 0; + } + + const date = new Date(utcTimeString + 'Z'); + + if (isNaN(date.getTime())) { + throw new Error(`Invalid date format provided: ${utcTimeString}`); + } + + return Math.floor(date.getTime() / 1000); +}; + +const etaToDate = (timestamp) => { + const date = new Date(timestamp * 1000); + + if (isNaN(date.getTime())) { + throw new Error(`Invalid timestamp provided: ${timestamp}`); + } + + return date.toISOString().slice(0, 19); +}; + +const getCurrentTimeInSeconds = () => { + const now = new Date(); + const currentTimeInSecs = Math.floor(now.getTime() / 1000); + return currentTimeInSecs; +}; + +/** + * Prompt the user for confirmation + * @param {string} question Prompt question + * @param {boolean} yes If true, skip the prompt + * @returns {boolean} Returns true if the prompt was skipped, false otherwise + */ +const prompt = (question, yes = false) => { + // skip the prompt if yes was passed + if (yes) { + return false; + } + + const answer = readlineSync.question(`${question} ${chalk.green('(y/n)')} `); + console.log(); + + return answer !== 'y'; +}; + +function findProjectRoot(startDir) { + let currentDir = startDir; + + while (currentDir !== path.parse(currentDir).root) { + const potentialPackageJson = path.join(currentDir, 'package.json'); + + if (fs.existsSync(potentialPackageJson)) { + return currentDir; + } + + currentDir = path.resolve(currentDir, '..'); + } + + throw new Error('Unable to find project root'); +} + +function toBigNumberString(number) { + return Math.ceil(number).toLocaleString('en', { useGrouping: false }); +} + +module.exports = { + loadConfig, + saveConfig, + writeJSON, + printInfo, + printWarn, + printError, + printLog, + isNonEmptyString, + isString, + isStringArray, + isNumber, + isValidNumber, + isValidDecimal, + isNumberArray, + isNonEmptyStringArray, + isValidTimeFormat, + copyObject, + httpGet, + httpPost, + parseArgs, + sleep, + dateToEta, + etaToDate, + getCurrentTimeInSeconds, + prompt, + findProjectRoot, + toBigNumberString, + timeout, +}; diff --git a/evm/cli-utils.js b/evm/cli-utils.js index 88fbc4a90..070c799d6 100644 --- a/evm/cli-utils.js +++ b/evm/cli-utils.js @@ -1,87 +1,3 @@ -'use strict'; - -require('dotenv').config(); - -const { Option } = require('commander'); - -const addBaseOptions = (program, options = {}) => { - program.addOption( - new Option('-e, --env ', 'environment') - .choices(['local', 'devnet', 'devnet-amplifier', 'devnet-verifiers', 'stagenet', 'testnet', 'mainnet']) - .default('testnet') - .makeOptionMandatory(true) - .env('ENV'), - ); - program.addOption(new Option('-y, --yes', 'skip deployment prompt confirmation').env('YES')); - program.addOption(new Option('--parallel', 'run script parallely wrt chains')); - program.addOption(new Option('--saveChainSeparately', 'save chain info separately')); - program.addOption(new Option('--gasOptions ', 'gas options cli override')); - - if (!options.ignoreChainNames) { - program.addOption( - new Option('-n, --chainNames ', 'chains to run the script over').makeOptionMandatory(true).env('CHAINS'), - ); - program.addOption(new Option('--skipChains ', 'chains to skip over')); - program.addOption( - new Option( - '--startFromChain ', - 'start from a specific chain onwards in the config, useful when a cmd fails for an intermediate chain', - ), - ); - } - - if (!options.ignorePrivateKey) { - program.addOption(new Option('-p, --privateKey ', 'private key').makeOptionMandatory(true).env('PRIVATE_KEY')); - } - - if (options.address) { - program.addOption(new Option('-a, --address
', 'override address')); - } - - return program; -}; - -const addExtendedOptions = (program, options = {}) => { - addBaseOptions(program, options); - - program.addOption(new Option('-v, --verify', 'verify the deployed contract on the explorer').env('VERIFY')); - - if (options.artifactPath) { - program.addOption(new Option('--artifactPath ', 'artifact path')); - } - - if (options.contractName) { - program.addOption(new Option('-c, --contractName ', 'contract name').makeOptionMandatory(true)); - } - - if (options.deployMethod) { - program.addOption( - new Option('-m, --deployMethod ', 'deployment method') - .choices(['create', 'create2', 'create3']) - .default(options.deployMethod), - ); - } - - if (options.salt) { - program.addOption(new Option('-s, --salt ', 'salt to use for create2 deployment').env('SALT')); - } - - if (options.skipExisting) { - program.addOption(new Option('-x, --skipExisting', 'skip existing if contract was already deployed on chain').env('SKIP_EXISTING')); - } - - if (options.upgrade) { - program.addOption(new Option('-u, --upgrade', 'upgrade a deployed contract').env('UPGRADE')); - } - - if (options.predictOnly) { - program.addOption(new Option('--predictOnly', 'output the predicted changes only').env('PREDICT_ONLY')); - } - - return program; -}; - module.exports = { - addBaseOptions, - addExtendedOptions, + ...require('../common/cli-utils'), }; diff --git a/evm/utils.js b/evm/utils.js index e28bcaddb..d9e8c058e 100644 --- a/evm/utils.js +++ b/evm/utils.js @@ -9,13 +9,31 @@ const { getDefaultProvider, BigNumber, } = ethers; -const https = require('https'); -const http = require('http'); const fs = require('fs'); const path = require('path'); -const { outputJsonSync } = require('fs-extra'); -const readlineSync = require('readline-sync'); const chalk = require('chalk'); +const { + loadConfig, + saveConfig, + isNonEmptyString, + isNonEmptyStringArray, + isNumber, + isNumberArray, + isString, + isValidNumber, + isValidTimeFormat, + printInfo, + isValidDecimal, + copyObject, + printError, + printWarn, + writeJSON, + httpGet, + httpPost, + sleep, + findProjectRoot, + timeout, +} = require('../common'); const { create3DeployContract, deployContractConstant, @@ -122,138 +140,6 @@ const deployCreate3 = async ( return contract; }; -const printInfo = (msg, info = '', colour = chalk.green) => { - if (info) { - console.log(`${msg}: ${colour(info)}\n`); - } else { - console.log(`${msg}\n`); - } -}; - -const printWarn = (msg, info = '') => { - if (info) { - msg = `${msg}: ${info}`; - } - - console.log(`${chalk.italic.yellow(msg)}\n`); -}; - -const printError = (msg, info = '') => { - if (info) { - msg = `${msg}: ${info}`; - } - - console.log(`${chalk.bold.red(msg)}\n`); -}; - -function printLog(log) { - console.log(JSON.stringify({ log }, null, 2)); -} - -const writeJSON = (data, name) => { - outputJsonSync(name, data, { - spaces: 2, - EOL: '\n', - }); -}; - -const httpGet = (url) => { - return new Promise((resolve, reject) => { - (url.startsWith('https://') ? https : http).get(url, (res) => { - const { statusCode } = res; - const contentType = res.headers['content-type']; - let error; - - if (statusCode !== 200 && statusCode !== 301) { - error = new Error('Request Failed.\n' + `Request: ${url}\nStatus Code: ${statusCode}`); - } else if (!/^application\/json/.test(contentType)) { - error = new Error('Invalid content-type.\n' + `Expected application/json but received ${contentType}`); - } - - if (error) { - res.resume(); - reject(error); - return; - } - - res.setEncoding('utf8'); - let rawData = ''; - res.on('data', (chunk) => { - rawData += chunk; - }); - res.on('end', () => { - try { - const parsedData = JSON.parse(rawData); - resolve(parsedData); - } catch (e) { - reject(e); - } - }); - }); - }); -}; - -const httpPost = async (url, data) => { - const response = await fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(data), - }); - return response.json(); -}; - -const isNonEmptyString = (arg) => { - return typeof arg === 'string' && arg !== ''; -}; - -const isString = (arg) => { - return typeof arg === 'string'; -}; - -const isStringArray = (arr) => Array.isArray(arr) && arr.every(isString); - -const isNumber = (arg) => { - return Number.isInteger(arg); -}; - -const isValidNumber = (arg) => { - return !isNaN(parseInt(arg)) && isFinite(arg); -}; - -const isValidDecimal = (arg) => { - return !isNaN(parseFloat(arg)) && isFinite(arg); -}; - -const isNumberArray = (arr) => { - if (!Array.isArray(arr)) { - return false; - } - - for (const item of arr) { - if (!isNumber(item)) { - return false; - } - } - - return true; -}; - -const isNonEmptyStringArray = (arr) => { - if (!Array.isArray(arr)) { - return false; - } - - for (const item of arr) { - if (typeof item !== 'string') { - return false; - } - } - - return true; -}; - const isAddressArray = (arr) => { if (!Array.isArray(arr)) return false; @@ -280,12 +166,6 @@ const isBytes32Array = (arr) => { return true; }; -const getCurrentTimeInSeconds = () => { - const now = new Date(); - const currentTimeInSecs = Math.floor(now.getTime() / 1000); - return currentTimeInSecs; -}; - /** * Determines if a given input is a valid keccak256 hash. * @@ -344,22 +224,6 @@ function isValidAddress(address, allowZeroAddress) { return isAddress(address); } -/** - * Validate if the input string matches the time format YYYY-MM-DDTHH:mm:ss - * - * @param {string} timeString - The input time string. - * @return {boolean} - Returns true if the format matches, false otherwise. - */ -function isValidTimeFormat(timeString) { - const regex = /^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|1\d|2\d|3[01])T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d$/; - - if (timeString === '0') { - return true; - } - - return regex.test(timeString); -} - // Validate if the input privateKey is correct function isValidPrivateKey(privateKey) { // Check if it's a valid hexadecimal string @@ -431,38 +295,6 @@ function validateParameters(parameters) { } } -/** - * Parses the input string into an array of arguments, recognizing and converting - * to the following types: boolean, number, array, and string. - * - * @param {string} args - The string of arguments to parse. - * - * @returns {Array} - An array containing parsed arguments. - * - * @example - * const input = "hello true 123 [1,2,3]"; - * const output = parseArgs(input); - * console.log(output); // Outputs: [ 'hello', true, 123, [ 1, 2, 3] ] - */ -const parseArgs = (args) => { - return args - .split(/\s+/) - .filter((item) => item !== '') - .map((arg) => { - if (arg.startsWith('[') && arg.endsWith(']')) { - return JSON.parse(arg); - } else if (arg === 'true') { - return true; - } else if (arg === 'false') { - return false; - } else if (!isNaN(arg) && !arg.startsWith('0x')) { - return Number(arg); - } - - return arg; - }); -}; - /** * Compute bytecode hash for a deployed contract or contract factory as it would appear on-chain. * Some chains don't use keccak256 for their state representation, which is taken into account by this function. @@ -685,22 +517,10 @@ const getAmplifierKeyAddresses = async (config, chain) => { return { addresses: weightedAddresses, threshold: verifierSet.threshold, created_at: verifierSet.created_at, verifierSetId }; }; -function sleep(ms) { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -function loadConfig(env) { - return require(`${__dirname}/../axelar-chains-config/info/${env}.json`); -} - function loadParallelExecutionConfig(env, chain) { return require(`${__dirname}/../chains-info/${env}-${chain}.json`); } -function saveConfig(config, env) { - writeJSON(config, `${__dirname}/../axelar-chains-config/info/${env}.json`); -} - function saveParallelExecutionConfig(config, env, chain) { writeJSON(config, `${__dirname}/../chains-info/${env}-${chain}.json`); } @@ -806,30 +626,6 @@ const deployContract = async ( } }; -const dateToEta = (utcTimeString) => { - if (utcTimeString === '0') { - return 0; - } - - const date = new Date(utcTimeString + 'Z'); - - if (isNaN(date.getTime())) { - throw new Error(`Invalid date format provided: ${utcTimeString}`); - } - - return Math.floor(date.getTime() / 1000); -}; - -const etaToDate = (timestamp) => { - const date = new Date(timestamp * 1000); - - if (isNaN(date.getTime())) { - throw new Error(`Invalid timestamp provided: ${timestamp}`); - } - - return date.toISOString().slice(0, 19); -}; - /** * Check if a specific event was emitted in a transaction receipt. * @@ -844,10 +640,6 @@ function wasEventEmitted(receipt, contract, eventName) { return receipt.logs.some((log) => log.topics[0] === event.topics[0]); } -function copyObject(obj) { - return JSON.parse(JSON.stringify(obj)); -} - const mainProcessor = async (options, processCommand, save = true, catchErr = false) => { if (!options.env) { throw new Error('Environment was not provided'); @@ -987,24 +779,6 @@ const mainProcessor = async (options, processCommand, save = true, catchErr = fa } }; -/** - * Prompt the user for confirmation - * @param {string} question Prompt question - * @param {boolean} yes If true, skip the prompt - * @returns {boolean} Returns true if the prompt was skipped, false otherwise - */ -const prompt = (question, yes = false) => { - // skip the prompt if yes was passed - if (yes) { - return false; - } - - const answer = readlineSync.question(`${question} ${chalk.green('(y/n)')} `); - console.log(); - - return answer !== 'y'; -}; - function getConfigByChainId(chainId, config) { for (const chain of Object.values(config.chains)) { if (chain.chainId === chainId) { @@ -1015,22 +789,6 @@ function getConfigByChainId(chainId, config) { throw new Error(`Chain with chainId ${chainId} not found in the config`); } -function findProjectRoot(startDir) { - let currentDir = startDir; - - while (currentDir !== path.parse(currentDir).root) { - const potentialPackageJson = path.join(currentDir, 'package.json'); - - if (fs.existsSync(potentialPackageJson)) { - return currentDir; - } - - currentDir = path.resolve(currentDir, '..'); - } - - throw new Error('Unable to find project root'); -} - function findContractPath(dir, contractName) { const files = fs.readdirSync(dir); @@ -1184,20 +942,6 @@ function isValidChain(config, chainName) { } } -function toBigNumberString(number) { - return Math.ceil(number).toLocaleString('en', { useGrouping: false }); -} - -function timeout(prom, time, exception) { - let timer; - - // Racing the promise with a timer - // If the timer resolves first, the promise is rejected with the exception - return Promise.race([prom, new Promise((resolve, reject) => (timer = setTimeout(reject, time, exception)))]).finally(() => - clearTimeout(timer), - ); -} - async function relayTransaction(options, chain, contract, method, params, nativeValue = 0, gasOptions = {}, expectedEvent = null) { if (options.relayerAPI) { const result = await httpPost(options.relayerAPI, { @@ -1270,55 +1014,31 @@ async function getWeightedSigners(config, chain, options) { } module.exports = { + ...require('../common/utils'), deployCreate, deployCreate2, deployCreate3, deployContract, - writeJSON, - copyObject, - httpGet, - httpPost, printObj, - printLog, - printInfo, - printWarn, - printError, getBytecodeHash, predictAddressCreate, getDeployedAddress, - isString, - isNonEmptyString, - isStringArray, - isNumber, - isValidNumber, - isValidDecimal, - isNumberArray, - isNonEmptyStringArray, isAddressArray, isKeccak256Hash, isValidCalldata, isValidBytesAddress, validateParameters, - parseArgs, getProxy, getEVMBatch, getEVMAddresses, getConfigByChainId, - sleep, - loadConfig, - saveConfig, printWalletInfo, - isValidTimeFormat, - dateToEta, - etaToDate, - getCurrentTimeInSeconds, wasEventEmitted, isContract, isValidAddress, isValidPrivateKey, isValidTokenId, verifyContract, - prompt, mainProcessor, getContractPath, getContractJSON, @@ -1327,8 +1047,6 @@ module.exports = { getSaltFromKey, getDeployOptions, isValidChain, - toBigNumberString, - timeout, getAmplifierKeyAddresses, getContractConfig, relayTransaction,