diff --git a/common/utils.js b/common/utils.js index c5eb3231..e6819154 100644 --- a/common/utils.js +++ b/common/utils.js @@ -7,9 +7,27 @@ const chalk = require('chalk'); const https = require('https'); const http = require('http'); const readlineSync = require('readline-sync'); +const { CosmWasmClient } = require('@cosmjs/cosmwasm-stargate'); +const { ethers } = require('hardhat'); +const { + utils: { keccak256, hexlify }, +} = ethers; +const { normalizeBech32 } = require('@cosmjs/encoding'); function loadConfig(env) { - return require(`${__dirname}/../axelar-chains-config/info/${env}.json`); + const config = require(`${__dirname}/../axelar-chains-config/info/${env}.json`); + + if (!config.sui) { + config.sui = { + networkType: env === 'local' ? 'localnet' : env, + name: 'Sui', + contracts: { + AxelarGateway: {}, + }, + }; + } + + return config; } function saveConfig(config, env) { @@ -331,6 +349,66 @@ function toBigNumberString(number) { return Math.ceil(number).toLocaleString('en', { useGrouping: false }); } +const isValidCosmosAddress = (str) => { + try { + normalizeBech32(str); + + return true; + } catch (error) { + return false; + } +}; + +async function getDomainSeparator(config, chain, options) { + // Allow any domain separator for local deployments or `0x` if not provided + if (options.env === 'local') { + return options.domainSeparator || ethers.constants.HashZero; + } + + if (isKeccak256Hash(options.domainSeparator)) { + // return the domainSeparator for debug deployments + return options.domainSeparator; + } + + const { + axelar: { contracts, chainId }, + } = config; + const { + Router: { address: routerAddress }, + } = contracts; + + if (!isString(chain.axelarId)) { + throw new Error(`missing or invalid axelar ID for chain ${chain.name}`); + } + + if (!isString(routerAddress) || !isValidCosmosAddress(routerAddress)) { + throw new Error(`missing or invalid router address`); + } + + if (!isString(chainId)) { + throw new Error(`missing or invalid chain ID`); + } + + printInfo(`Retrieving domain separator for ${chain.name} from Axelar network`); + const domainSeparator = hexlify((await getContractConfig(config, chain.axelarId)).domain_separator); + const expectedDomainSeparator = calculateDomainSeparator(chain.axelarId, routerAddress, chainId); + + if (domainSeparator !== expectedDomainSeparator) { + throw new Error(`unexpected domain separator (want ${expectedDomainSeparator}, got ${domainSeparator})`); + } + + return domainSeparator; +} + +const getContractConfig = async (config, chain) => { + const key = Buffer.from('config'); + const client = await CosmWasmClient.connect(config.axelar.rpc); + const value = await client.queryContractRaw(config.axelar.contracts.MultisigProver[chain].address, key); + return JSON.parse(Buffer.from(value).toString('ascii')); +}; + +const calculateDomainSeparator = (chain, router, network) => keccak256(Buffer.from(`${chain}${router}${network}`)); + module.exports = { loadConfig, saveConfig, @@ -362,4 +440,5 @@ module.exports = { toBigNumberString, timeout, validateParameters, + getDomainSeparator, }; diff --git a/evm/deploy-amplifier-gateway.js b/evm/deploy-amplifier-gateway.js index a0b0bb10..419dbfff 100644 --- a/evm/deploy-amplifier-gateway.js +++ b/evm/deploy-amplifier-gateway.js @@ -7,7 +7,7 @@ const { ContractFactory, Contract, Wallet, - utils: { defaultAbiCoder, keccak256, hexlify }, + utils: { defaultAbiCoder, keccak256 }, getDefaultProvider, } = ethers; @@ -22,15 +22,12 @@ const { mainProcessor, deployContract, getGasOptions, - isKeccak256Hash, - getContractConfig, - isString, getWeightedSigners, getContractJSON, getDeployedAddress, getDeployOptions, + getDomainSeparator, } = require('./utils'); -const { calculateDomainSeparator, isValidCosmosAddress } = require('../cosmwasm/utils'); const { addExtendedOptions } = require('./cli-utils'); const { storeSignedTx, signTransaction, getWallet } = require('./sign-utils.js'); @@ -38,43 +35,6 @@ const { WEIGHTED_SIGNERS_TYPE, encodeWeightedSigners } = require('@axelar-networ const AxelarAmplifierGatewayProxy = require('@axelar-network/axelar-gmp-sdk-solidity/artifacts/contracts/gateway/AxelarAmplifierGatewayProxy.sol/AxelarAmplifierGatewayProxy.json'); const AxelarAmplifierGateway = require('@axelar-network/axelar-gmp-sdk-solidity/artifacts/contracts/gateway/AxelarAmplifierGateway.sol/AxelarAmplifierGateway.json'); -async function getDomainSeparator(config, chain, options) { - printInfo(`Retrieving domain separator for ${chain.name} from Axelar network`); - - if (isKeccak256Hash(options.domainSeparator)) { - // return the domainSeparator for debug deployments - return options.domainSeparator; - } - - const { - axelar: { contracts, chainId }, - } = config; - const { - Router: { address: routerAddress }, - } = contracts; - - if (!isString(chain.axelarId)) { - throw new Error(`missing or invalid axelar ID for chain ${chain.name}`); - } - - if (!isString(routerAddress) || !isValidCosmosAddress(routerAddress)) { - throw new Error(`missing or invalid router address`); - } - - if (!isString(chainId)) { - throw new Error(`missing or invalid chain ID`); - } - - const domainSeparator = hexlify((await getContractConfig(config, chain.axelarId)).domain_separator); - const expectedDomainSeparator = calculateDomainSeparator(chain.axelarId, routerAddress, chainId); - - if (domainSeparator !== expectedDomainSeparator) { - throw new Error(`unexpected domain separator (want ${expectedDomainSeparator}, got ${domainSeparator})`); - } - - return domainSeparator; -} - async function getSetupParams(config, chain, operator, options) { const { signers: signerSets, verifierSetId } = await getWeightedSigners(config, chain, options); printInfo('Setup params', JSON.stringify([operator, signerSets], null, 2)); diff --git a/package-lock.json b/package-lock.json index 55b67d22..296d788b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,8 @@ "@mysten/sui": "^1.3.0", "@stellar/stellar-sdk": "^12.0.0-rc3", "axios": "^1.6.2", - "path": "^0.12.7" + "path": "^0.12.7", + "toml": "^3.0.0" }, "devDependencies": { "@ledgerhq/hw-transport-node-hid": "^6.27.21", diff --git a/package.json b/package.json index 01379e8c..bcdb007e 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ "@mysten/sui": "^1.3.0", "@stellar/stellar-sdk": "^12.0.0-rc3", "axios": "^1.6.2", - "path": "^0.12.7" + "path": "^0.12.7", + "toml": "^3.0.0" }, "devDependencies": { "@ledgerhq/hw-transport-node-hid": "^6.27.21", diff --git a/sui/README.md b/sui/README.md index 0ea2275c..2cc6c789 100644 --- a/sui/README.md +++ b/sui/README.md @@ -54,7 +54,7 @@ Deploy the gateway package: - By querying the signer set from the Amplifier contract (this only works if Amplifier contracts have been setup): ```bash -node sui/deploy-contract.js deploy axelar_gateway +node sui/deploy-contract.js deploy AxelarGateway ``` Note: the `minimumRotationDelay` is in `seconds` unit. The default value is `24 * 60 * 60` (1 day). @@ -64,13 +64,13 @@ Use `--help` flag to see other setup params that can be overridden. - For testing convenience, you can use the secp256k1 wallet as the signer set for the gateway. ```bash -node sui/deploy-contract.js deploy axelar_gateway --signers wallet --nonce test +node sui/deploy-contract.js deploy AxelarGateway --signers wallet --nonce test ``` - You can also provide a JSON object with a full signer set: ```bash -node sui/deploy-contract.js deploy axelar_gateway -e testnet --signers '{"signers": [{"pub_key": "0x020194ead85b350d90472117e6122cf1764d93bf17d6de4b51b03d19afc4d6302b", "weight": 1}], "threshold": 1, "nonce": "0x0000000000000000000000000000000000000000000000000000000000000000"}' +node sui/deploy-contract.js deploy AxelarGateway -e testnet --signers '{"signers": [{"pub_key": "0x020194ead85b350d90472117e6122cf1764d93bf17d6de4b51b03d19afc4d6302b", "weight": 1}], "threshold": 1, "nonce": "0x0000000000000000000000000000000000000000000000000000000000000000"}' ``` Upgrading Gateway: @@ -78,7 +78,7 @@ Upgrading Gateway: To update the gateway run the following command: ```bash -node sui/deploy-contract.js upgrade axelar_gateway +node sui/deploy-contract.js upgrade AxelarGateway ``` policy should be one of the following: @@ -92,13 +92,19 @@ Provide `--txFilePath` with `--offline` to generate tx data file for offline sig Deploy the Gas Service package: ```bash -node sui/deploy-contract.js deploy gas_service +node sui/deploy-contract.js deploy GasService ``` Deploy the test GMP package: ```bash -node sui/deploy-test.js +node sui/deploy-contract.js deploy Test +``` + +Deploy the Operators package: + +```bash +node sui/deploy-contract.js deploy Operators ``` Call Contract: diff --git a/sui/deploy-contract.js b/sui/deploy-contract.js index eb562f56..af002165 100644 --- a/sui/deploy-contract.js +++ b/sui/deploy-contract.js @@ -5,156 +5,236 @@ const { toB64 } = require('@mysten/sui/utils'); const { bcs } = require('@mysten/sui/bcs'); const { Transaction } = require('@mysten/sui/transactions'); const { - utils: { arrayify, hexlify, toUtf8Bytes, keccak256 }, - constants: { HashZero }, + utils: { arrayify }, } = ethers; -const { saveConfig, printInfo, validateParameters, writeJSON } = require('../evm/utils'); -const { addBaseOptions } = require('./cli-utils'); +const { saveConfig, printInfo, validateParameters, writeJSON, getDomainSeparator, loadConfig } = require('../common'); +const { addBaseOptions, addOptionsToCommands } = require('./cli-utils'); const { getWallet, printWalletInfo, broadcast } = require('./sign-utils'); -const { loadSuiConfig, getAmplifierSigners, deployPackage, getObjectIdsByObjectTypes } = require('./utils'); const { bytes32Struct, signersStruct } = require('./types-utils'); -const { upgradePackage } = require('./deploy-utils'); -const { suiPackageAddress, suiClockAddress } = require('./utils'); - -async function getSigners(keypair, config, chain, options) { - if (options.signers === 'wallet') { - const pubKey = keypair.getPublicKey().toRawBytes(); - printInfo('Using wallet pubkey as the signer for the gateway', hexlify(pubKey)); - - if (keypair.getKeyScheme() !== 'Secp256k1') { - throw new Error('Only Secp256k1 pubkeys are supported by the gateway'); - } - - return { - signers: [{ pub_key: pubKey, weight: 1 }], - threshold: 1, - nonce: options.nonce ? keccak256(toUtf8Bytes(options.nonce)) : HashZero, - }; - } else if (options.signers) { - printInfo('Using provided signers', options.signers); - - const signers = JSON.parse(options.signers); - return { - signers: signers.signers.map(({ pub_key: pubKey, weight }) => { - return { pub_key: arrayify(pubKey), weight }; - }), - threshold: signers.threshold, - nonce: arrayify(signers.nonce) || HashZero, - }; - } - - return getAmplifierSigners(config, chain); +const { upgradePackage, UPGRADE_POLICIES } = require('./deploy-utils'); +const { + getSigners, + deployPackage, + getObjectIdsByObjectTypes, + suiPackageAddress, + suiClockAddress, + readMovePackageName, + getChannelId, +} = require('./utils'); + +/** + * Move Package Directories + * + * This array contains the names of Move package directories located in: + * `node_modules/@axelar-network/axelar-cgp-sui/move` + * + * Each string in this array corresponds to a folder name within that path. + * + * To deploy a new package: + * 1. Add the new package's folder name to this array + * 2. Ensure the corresponding folder exists in the specified path + * + */ +const PACKAGE_DIRS = ['gas_service', 'test', 'axelar_gateway', 'operators']; + +/** + * Post-Deployment Functions Mapping + * + * This object maps each package name to a post-deployment function. + */ +const POST_DEPLOY_FUNCTIONS = { + GasService: postDeployGasService, + Test: postDeployTest, + Operators: postDeployOperators, + AxelarGateway: postDeployAxelarGateway, +}; + +/** + * Command Options Mapping + * + * This object maps each package name to a function that returns an array of command options. + */ +const CMD_OPTIONS = { + AxelarGateway: () => [...DEPLOY_CMD_OPTIONS, ...GATEWAY_CMD_OPTIONS], + GasService: () => DEPLOY_CMD_OPTIONS, + Test: () => DEPLOY_CMD_OPTIONS, + Operators: () => DEPLOY_CMD_OPTIONS, +}; + +/** + * Supported Move Packages + * + * Maps each directory in PACKAGE_DIRS to an object containing: + * - packageName: Read from 'Move.toml' in the directory + * - packageDir: The directory name + * + */ +const supportedPackages = PACKAGE_DIRS.map((dir) => ({ + packageName: readMovePackageName(dir), + packageDir: dir, +})); + +/** + * Post-Deployment Functions + * + * This section defines functions to be executed after package deployment. + * These functions serve purposes such as: + * 1. Updating chain configuration with newly deployed object IDs + * 2. Submitting additional transactions for contract setup + * + * Define post-deployment functions for each supported package below. + */ + +async function postDeployGasService(published, args) { + const { chain } = args; + const [gasCollectorCapObjectId, gasServiceObjectId] = getObjectIdsByObjectTypes(published.publishTxn, [ + `${published.packageId}::gas_service::GasCollectorCap`, + `${published.packageId}::gas_service::GasService`, + ]); + chain.contracts.GasService.objects = { + GasCollectorCap: gasCollectorCapObjectId, + GasService: gasServiceObjectId, + }; } -async function deploy(keypair, client, contractName, config, chain, options) { - if (!chain.contracts[contractName]) { - chain.contracts[contractName] = {}; - } +async function postDeployTest(published, args) { + const { chain, config, options } = args; + const [keypair, client] = getWallet(chain, options); + const relayerDiscovery = config.sui.contracts.AxelarGateway?.objects?.RelayerDiscovery; - const { packageId, publishTxn } = await deployPackage(contractName, client, keypair, options); + const [singletonObjectId] = getObjectIdsByObjectTypes(published.publishTxn, [`${published.packageId}::test::Singleton`]); + const channelId = await getChannelId(client, singletonObjectId); + chain.contracts.Test.objects = { Singleton: singletonObjectId, ChannelId: channelId }; - printInfo('Publish transaction digest: ', publishTxn.digest); + const tx = new Transaction(); + tx.moveCall({ + target: `${published.packageId}::test::register_transaction`, + arguments: [tx.object(relayerDiscovery), tx.object(singletonObjectId)], + }); - const contractConfig = chain.contracts[contractName]; - contractConfig.address = packageId; - contractConfig.objects = {}; + const registerTx = await broadcast(client, keypair, tx); - switch (contractName) { - case 'gas_service': { - const [GasService, GasCollectorCap] = getObjectIdsByObjectTypes(publishTxn, [ - `${packageId}::gas_service::GasService`, - `${packageId}::gas_service::GasCollectorCap`, - ]); - contractConfig.objects = { GasService, GasCollectorCap }; - break; - } - - case 'axelar_gateway': { - const { minimumRotationDelay, domainSeparator, policy, previousSigners } = options; - const operator = options.operator || keypair.toSuiAddress(); - const signers = await getSigners(keypair, config, chain, options); - - validateParameters({ isNonEmptyString: { previousSigners, minimumRotationDelay }, isKeccak256Hash: { domainSeparator } }); - - const [creatorCap, relayerDiscovery, upgradeCap] = getObjectIdsByObjectTypes(publishTxn, [ - `${packageId}::gateway::CreatorCap`, - `${packageId}::discovery::RelayerDiscovery`, - `${suiPackageAddress}::package::UpgradeCap`, - ]); + printInfo('Register transaction', registerTx.digest); +} - const encodedSigners = signersStruct - .serialize({ - ...signers, - nonce: bytes32Struct.serialize(signers.nonce).toBytes(), - }) - .toBytes(); +async function postDeployOperators(published, args) { + const { chain } = args; + const [operatorsObjectId, ownerCapObjectId] = getObjectIdsByObjectTypes(published.publishTxn, [ + `${published.packageId}::operators::Operators`, + `${published.packageId}::operators::OwnerCap`, + ]); + chain.contracts.Operators.objects = { + Operators: operatorsObjectId, + OwnerCap: ownerCapObjectId, + }; +} - const tx = new Transaction(); +async function postDeployAxelarGateway(published, args) { + const { keypair, client, config, chain, options } = args; + const { packageId, publishTxn } = published; + const { minimumRotationDelay, policy, previousSigners } = options; + const operator = options.operator || keypair.toSuiAddress(); + const signers = await getSigners(keypair, config, chain, options); + const domainSeparator = await getDomainSeparator(config, chain, options); + + validateParameters({ + isNonEmptyString: { previousSigners }, + isValidNumber: { minimumRotationDelay }, + }); + + const [creatorCap, relayerDiscovery, upgradeCap] = getObjectIdsByObjectTypes(publishTxn, [ + `${packageId}::gateway::CreatorCap`, + `${packageId}::discovery::RelayerDiscovery`, + `${suiPackageAddress}::package::UpgradeCap`, + ]); + + const encodedSigners = signersStruct + .serialize({ + ...signers, + nonce: bytes32Struct.serialize(signers.nonce).toBytes(), + }) + .toBytes(); + + const tx = new Transaction(); + + const separator = tx.moveCall({ + target: `${packageId}::bytes32::new`, + arguments: [tx.pure(arrayify(domainSeparator))], + }); + + tx.moveCall({ + target: `${packageId}::gateway::setup`, + arguments: [ + tx.object(creatorCap), + tx.pure.address(operator), + separator, + tx.pure.u64(minimumRotationDelay), + tx.pure.u64(options.previousSigners), + tx.pure(bcs.vector(bcs.u8()).serialize(encodedSigners).toBytes()), + tx.object(suiClockAddress), + ], + }); + + if (policy !== 'any_upgrade') { + const upgradeType = UPGRADE_POLICIES[policy]; + tx.moveCall({ + target: `${suiPackageAddress}::package::${upgradeType}`, + arguments: [tx.object(upgradeCap)], + }); + } - const separator = tx.moveCall({ - target: `${packageId}::bytes32::new`, - arguments: [tx.pure(arrayify(domainSeparator))], - }); + const result = await broadcast(client, keypair, tx); - tx.moveCall({ - target: `${packageId}::gateway::setup`, - arguments: [ - tx.object(creatorCap), - tx.pure.address(operator), - separator, - tx.pure.u64(minimumRotationDelay), - tx.pure.u64(options.previousSigners), - tx.pure(bcs.vector(bcs.u8()).serialize(encodedSigners).toBytes()), - tx.object(suiClockAddress), - ], - }); + printInfo('Setup Gateway', result.digest); - if (policy !== 'any_upgrade') { - const upgradeType = policy === 'code_upgrade' ? 'only_additive_upgrades' : 'only_dep_upgrades'; + const [gateway] = getObjectIdsByObjectTypes(result, [`${packageId}::gateway::Gateway`]); - tx.moveCall({ - target: `${suiPackageAddress}::package::${upgradeType}`, - arguments: [tx.object(upgradeCap)], - }); - } + // Update chain configuration + chain.contracts.AxelarGateway = { + objects: { + Gateway: gateway, + RelayerDiscovery: relayerDiscovery, + UpgradeCap: upgradeCap, + }, + domainSeparator, + operator, + minimumRotationDelay, + }; +} - const result = await broadcast(client, keypair, tx); +async function deploy(keypair, client, supportedContract, config, chain, options) { + const { packageDir, packageName } = supportedContract; - printInfo('Setup transaction digest', result.digest); + // Deploy package + const published = await deployPackage(packageDir, client, keypair, options); - const [gateway] = getObjectIdsByObjectTypes(result, [`${packageId}::gateway::Gateway`]); + printInfo(`Deployed ${packageName}`, published.publishTxn.digest); - contractConfig.objects = { - gateway, - relayerDiscovery, - upgradeCap, - }; - contractConfig.domainSeparator = domainSeparator; - contractConfig.operator = operator; - contractConfig.minimumRotationDelay = minimumRotationDelay; - break; - } + // Update chain configuration with deployed contract address + chain.contracts[packageName] = { + address: published.packageId, + }; - default: { - throw new Error(`${contractName} is not supported.`); - } - } + // Execute post-deployment function + const executePostDeploymentFn = POST_DEPLOY_FUNCTIONS[packageName]; + executePostDeploymentFn(published, { keypair, client, config, chain, options }); - printInfo(`${contractName} deployed`, JSON.stringify(contractConfig, null, 2)); + printInfo(`${packageName} Configuration Updated`, JSON.stringify(chain.contracts[packageName], null, 2)); } -async function upgrade(keypair, client, contractName, policy, config, chain, options) { +async function upgrade(keypair, client, supportedPackage, policy, config, chain, options) { const { packageDependencies } = options; + const { packageName } = supportedPackage; options.policy = policy; - if (!chain.contracts[contractName]) { - throw new Error(`Cannot find specified contract: ${contractName}`); + if (!chain.contracts[packageName]) { + throw new Error(`Cannot find specified contract: ${packageName}`); } const contractsConfig = chain.contracts; - const packageConfig = contractsConfig?.[contractName]; + const contractConfig = contractsConfig?.[packageName]; - validateParameters({ isNonEmptyString: { contractName } }); + validateParameters({ isNonEmptyString: { packageName } }); if (packageDependencies) { for (const dependencies of packageDependencies) { @@ -164,11 +244,11 @@ async function upgrade(keypair, client, contractName, policy, config, chain, opt } const builder = new TxBuilder(client); - await upgradePackage(client, keypair, contractName, packageConfig, builder, options); + await upgradePackage(client, keypair, supportedPackage, contractConfig, builder, options); } async function mainProcessor(args, options, processor) { - const config = loadSuiConfig(options.env); + const config = loadConfig(options.env); const [keypair, client] = getWallet(config.sui, options); await printWalletInfo(keypair, client, config.sui, options); await processor(keypair, client, ...args, config, config.sui, options); @@ -185,48 +265,90 @@ async function mainProcessor(args, options, processor) { } } +/** + * Command Options + * + * This section defines options for the command that are specific to each package. + */ + +// Common deploy command options for all packages +const DEPLOY_CMD_OPTIONS = [ + new Option('--policy ', 'upgrade policy for upgrade cap: For example, use "any_upgrade" to allow all types of upgrades') + .choices(['any_upgrade', 'code_upgrade', 'dep_upgrade']) + .default('any_upgrade'), +]; + +// Gateway deploy command options +const GATEWAY_CMD_OPTIONS = [ + new Option('--signers ', 'JSON with the initial signer set').env('SIGNERS'), + new Option('--operator ', 'operator for the gateway (defaults to the deployer address)').env('OPERATOR'), + new Option('--minimumRotationDelay ', 'minium delay for signer rotations (in second)') + .argParser((val) => parseInt(val) * 1000) + .default(24 * 60 * 60), + new Option('--domainSeparator ', 'domain separator'), + new Option('--nonce ', 'nonce for the signer (defaults to HashZero)'), + new Option('--previousSigners ', 'number of previous signers to retain').default('15'), +]; + +const addDeployOptions = (program) => { + // Get the package name from the program name + const packageName = program.name(); + + // Find the corresponding options for the package + const options = CMD_OPTIONS[packageName](); + + // Add the options to the program + options.forEach((option) => program.addOption(option)); + + return program; +}; + if (require.main === module) { - const program = new Command(); - - program.name('deploy-contract').description('Deploy/Upgrade packages'); - - const deployCmd = program - .name('deploy') - .description('Deploy a Sui package') - .command('deploy ') - .addOption(new Option('--signers ', 'JSON with the initial signer set').env('SIGNERS')) - .addOption(new Option('--operator ', 'operator for the gateway (defaults to the deployer address)').env('OPERATOR')) - .addOption( - new Option('--minimumRotationDelay ', 'minium delay for signer rotations (in second)') - .default(24 * 60 * 60) - .parseArg((val) => parseInt(val) * 1000), - ) - .addOption(new Option('--domainSeparator ', 'domain separator')) - .addOption(new Option('--nonce ', 'nonce for the signer (defaults to HashZero)')) - .addOption(new Option('--previousSigners ', 'number of previous signers to retain').default('15')) - .addOption( - new Option('--policy ', 'upgrade policy for upgrade cap: For example, use "any_upgrade" to allow all types of upgrades') - .choices(['any_upgrade', 'code_upgrade', 'dep_upgrade']) - .default('any_upgrade'), - ) - .action((contractName, options) => { - mainProcessor([contractName], options, deploy); - }); + // 1st level command + const program = new Command('deploy-contract').description('Deploy/Upgrade packages'); + + // 2nd level commands + const deployCmd = new Command('deploy').description('Deploy a Sui package'); + const upgradeCmd = new Command('upgrade').description('Upgrade a Sui package'); + + // 3rd level commands for `deploy` + const deployContractCmds = supportedPackages.map((supportedPackage) => { + const { packageName } = supportedPackage; + const command = new Command(packageName).description(`Deploy ${packageName} contract`); - const upgradeCmd = program - .name('upgrade') - .description('Upgrade a Sui package') - .command('upgrade ') - .addOption(new Option('--sender ', 'transaction sender')) - .addOption(new Option('--digest ', 'digest hash for upgrade')) - .addOption(new Option('--offline', 'store tx block for sign')) - .addOption(new Option('--txFilePath ', 'unsigned transaction will be stored')) - .action((contractName, policy, options) => { - mainProcessor([contractName, policy], options, upgrade); + return addDeployOptions(command).action((options) => { + mainProcessor([supportedPackage], options, deploy); }); + }); + + // Add 3rd level commands to 2nd level command `deploy` + deployContractCmds.forEach((cmd) => deployCmd.addCommand(cmd)); + + // 3rd level commands for `upgrade` + const upgradeContractCmds = supportedPackages.map((supportedPackage) => { + const { packageName } = supportedPackage; + return new Command(packageName) + .description(`Upgrade ${packageName} contract`) + .command(`${packageName} `) + .addOption(new Option('--sender ', 'transaction sender')) + .addOption(new Option('--digest ', 'digest hash for upgrade')) + .addOption(new Option('--offline', 'store tx block for sign')) + .addOption(new Option('--txFilePath ', 'unsigned transaction will be stored')) + .action((policy, options) => { + mainProcessor([supportedPackage, policy], options, upgrade); + }); + }); + + // Add 3rd level commands to 2nd level command `upgrade` + upgradeContractCmds.forEach((cmd) => upgradeCmd.addCommand(cmd)); + + // Add base options to all 2nd and 3rd level commands + addOptionsToCommands(deployCmd, addBaseOptions); + addOptionsToCommands(upgradeCmd, addBaseOptions); - addBaseOptions(deployCmd); - addBaseOptions(upgradeCmd); + // Add 2nd level commands to 1st level command + program.addCommand(deployCmd); + program.addCommand(upgradeCmd); program.parse(); } diff --git a/sui/deploy-test.js b/sui/deploy-test.js index 8b0db9e9..9ab7bc77 100644 --- a/sui/deploy-test.js +++ b/sui/deploy-test.js @@ -1,6 +1,6 @@ -const { saveConfig, prompt, printInfo } = require('../common/utils'); +const { loadConfig, saveConfig, prompt, printInfo } = require('../common/utils'); const { Command } = require('commander'); -const { loadSuiConfig, deployPackage, getBcsBytesByObjectId } = require('./utils'); +const { deployPackage, getBcsBytesByObjectId } = require('./utils'); const { singletonStruct } = require('./types-utils'); const { Transaction } = require('@mysten/sui/transactions'); const { addBaseOptions } = require('./cli-utils'); @@ -54,7 +54,7 @@ async function processCommand(config, chain, options) { } async function mainProcessor(options, processor) { - const config = loadSuiConfig(options.env); + const config = loadConfig(options.env); await processor(config, config.sui, options); saveConfig(config, options.env); diff --git a/sui/deploy-utils.js b/sui/deploy-utils.js index 3c594116..0dd21568 100644 --- a/sui/deploy-utils.js +++ b/sui/deploy-utils.js @@ -1,25 +1,35 @@ -const { Command, Option } = require('commander'); -const { TxBuilder, updateMoveToml } = require('@axelar-network/axelar-cgp-sui'); const { bcs } = require('@mysten/bcs'); -const { fromB64, toB64 } = require('@mysten/bcs'); -const { saveConfig, printInfo, validateParameters, prompt, writeJSON } = require('../common/utils'); -const { addBaseOptions } = require('./cli-utils'); -const { getWallet } = require('./sign-utils'); -const { loadSuiConfig, getObjectIdsByObjectTypes, suiPackageAddress } = require('./utils'); - -async function upgradePackage(client, keypair, packageName, packageConfig, builder, options) { - const { modules, dependencies, digest } = await builder.getContractBuild(packageName); - const { policy, offline } = options; - const sender = options.sender || keypair.toSuiAddress(); +const { fromB64 } = require('@mysten/bcs'); +const { printInfo, validateParameters } = require('../common/utils'); +const { getObjectIdsByObjectTypes, suiPackageAddress } = require('./utils'); +const UPGRADE_POLICIES = { + code_upgrade: 'only_additive_upgrades', + dependency_upgrade: 'only_dep_upgrades', +}; - if (!['any_upgrade', 'code_upgrade', 'dep_upgrade'].includes(policy)) { - throw new Error(`Unknown upgrade policy: ${policy}. Supported policies: any_upgrade, code_upgrade, dep_upgrade`); +function getUpgradePolicyId(policy) { + switch (policy) { + case 'any_upgrade': + return 0; + case 'code_upgrade': + return 128; + case 'dep_upgrade': + return 192; + default: + throw new Error(`Unknown upgrade policy: ${policy}. Supported policies: any_upgrade, code_upgrade, dep_upgrade`); } +} - const upgradeCap = packageConfig.objects?.upgradeCap; +async function upgradePackage(client, keypair, packageToUpgrade, contractConfig, builder, options) { + const { packageDir, packageName } = packageToUpgrade; + const { modules, dependencies, digest } = await builder.getContractBuild(packageDir); + const { offline } = options; + const sender = options.sender || keypair.toSuiAddress(); + const upgradeCap = contractConfig.objects?.UpgradeCap; const digestHash = options.digest ? fromB64(options.digest) : digest; + const policy = getUpgradePolicyId(options.policy); - validateParameters({ isNonEmptyString: { upgradeCap, policy }, isNonEmptyStringArray: { modules, dependencies } }); + validateParameters({ isNonEmptyString: { upgradeCap }, isNonEmptyStringArray: { modules, dependencies } }); const tx = builder.tx; const cap = tx.object(upgradeCap); @@ -31,7 +41,7 @@ async function upgradePackage(client, keypair, packageName, packageConfig, build const receipt = tx.upgrade({ modules, dependencies, - package: packageConfig.address, + package: contractConfig.address, ticket, }); @@ -45,7 +55,7 @@ async function upgradePackage(client, keypair, packageName, packageConfig, build if (offline) { options.txBytes = txBytes; - options.offlineMessage = `Transaction to upgrade ${packageName}`; + options.offlineMessage = `Transaction to upgrade ${packageDir}`; } else { const signature = (await keypair.signTransaction(txBytes)).signature; const result = await client.executeTransactionBlock({ @@ -59,130 +69,16 @@ async function upgradePackage(client, keypair, packageName, packageConfig, build }); const packageId = (result.objectChanges?.filter((a) => a.type === 'published') ?? [])[0].packageId; - packageConfig.address = packageId; - const [upgradeCap] = getObjectIdsByObjectTypes(result, ['0x2::package::UpgradeCap']); - packageConfig.objects.upgradeCap = upgradeCap; - - printInfo('Transaction digest', JSON.stringify(result.digest, null, 2)); - printInfo(`${packageName} upgraded`, packageId); - } -} - -async function deployPackage(chain, client, keypair, packageName, packageConfig, builder, options) { - const { offline, sender } = options; - - const address = sender || keypair.toSuiAddress(); - await builder.publishPackageAndTransferCap(packageName, address); - const tx = builder.tx; - tx.setSender(address); - const txBytes = await tx.build({ client }); - - if (offline) { - options.txBytes = txBytes; - } else { - if (prompt(`Proceed with deployment on ${chain.name}?`, options.yes)) { - return; - } - - const signature = (await keypair.signTransaction(txBytes)).signature; - const publishTxn = await client.executeTransactionBlock({ - transactionBlock: txBytes, - signature, - options: { - showEffects: true, - showObjectChanges: true, - showEvents: true, - }, - }); - - packageConfig.address = (publishTxn.objectChanges?.find((a) => a.type === 'published') ?? []).packageId; - const objectChanges = publishTxn.objectChanges.filter((object) => object.type === 'created'); - packageConfig.objects = {}; - - for (const object of objectChanges) { - const array = object.objectType.split('::'); - const objectName = array[array.length - 1]; - - if (objectName) { - packageConfig.objects[objectName] = object.objectId; - } - } + contractConfig.address = packageId; + const [upgradeCap] = getObjectIdsByObjectTypes(result, [`${suiPackageAddress}::package::UpgradeCap`]); + contractConfig.objects.UpgradeCap = upgradeCap; - printInfo(`${packageName} deployed`, JSON.stringify(packageConfig, null, 2)); + printInfo('Transaction Digest', JSON.stringify(result.digest, null, 2)); + printInfo(`${packageName} Upgraded Address`, packageId); } } -async function processCommand(chain, options) { - const [keypair, client] = getWallet(chain, options); - const { upgrade, packageName, packageDependencies, offline, txFilePath } = options; - - printInfo('Wallet address', keypair.toSuiAddress()); - - if (!chain.contracts[packageName]) { - chain.contracts[packageName] = {}; - } - - const contractsConfig = chain.contracts; - const packageConfig = contractsConfig?.[packageName]; - - validateParameters({ isNonEmptyString: { packageName } }); - - if (packageDependencies) { - for (const dependencies of packageDependencies) { - const packageId = contractsConfig[dependencies]?.address; - updateMoveToml(dependencies, packageId); - } - } - - const builder = new TxBuilder(client); - - if (upgrade) { - await upgradePackage(client, keypair, packageName, packageConfig, builder, options); - } else { - await deployPackage(chain, client, keypair, packageName, packageConfig, builder, options); - } - - if (offline) { - validateParameters({ isNonEmptyString: { txFilePath } }); - - const txB64Bytes = toB64(options.txBytes); - - writeJSON({ status: 'PENDING', bytes: txB64Bytes }, txFilePath); - printInfo(`The unsigned transaction is`, txB64Bytes); - } -} - -async function mainProcessor(options, processor) { - const config = loadSuiConfig(options.env); - - await processor(config.sui, options); - saveConfig(config, options.env); -} - -if (require.main === module) { - const program = new Command(); - - program.name('deploy-upgrade').description('Deploy/Upgrade the Sui package'); - - addBaseOptions(program); - - program.addOption(new Option('--packageName ', 'package name to deploy/upgrade')); - program.addOption(new Option('--packageDependencies [packageDependencies...]', 'array of package dependencies')); - program.addOption(new Option('--upgrade', 'deploy or upgrade')); - program.addOption(new Option('--policy ', 'new policy to upgrade')); - program.addOption(new Option('--sender ', 'transaction sender')); - program.addOption(new Option('--digest ', 'digest hash for upgrade')); - program.addOption(new Option('--offline', 'store tx block for sign')); - program.addOption(new Option('--txFilePath ', 'unsigned transaction will be stored')); - - program.action((options) => { - mainProcessor(options, processCommand); - }); - - program.parse(); -} - module.exports = { + UPGRADE_POLICIES, upgradePackage, - deployPackage, }; diff --git a/sui/gas-service.js b/sui/gas-service.js index 1a6ba290..4b81be71 100644 --- a/sui/gas-service.js +++ b/sui/gas-service.js @@ -2,8 +2,9 @@ const { saveConfig, printInfo, printError } = require('../common/utils'); const { Command } = require('commander'); const { Transaction } = require('@mysten/sui/transactions'); const { bcs } = require('@mysten/sui/bcs'); +const { loadConfig } = require('../common/utils'); const { gasServiceStruct } = require('./types-utils'); -const { loadSuiConfig, getBcsBytesByObjectId } = require('./utils'); +const { getBcsBytesByObjectId } = require('./utils'); const { ethers } = require('hardhat'); const { getFormattedAmount } = require('./amount-utils'); const { @@ -160,7 +161,7 @@ async function processCommand(command, chain, args, options) { } async function mainProcessor(options, args, processor, command) { - const config = loadSuiConfig(options.env); + const config = loadConfig(options.env); await processor(command, config.sui, args, options); saveConfig(config, options.env); } diff --git a/sui/gateway.js b/sui/gateway.js index a26ef9ec..d46721de 100644 --- a/sui/gateway.js +++ b/sui/gateway.js @@ -8,10 +8,10 @@ const { constants: { HashZero }, } = ethers; +const { loadConfig } = require('../common/utils'); const { addBaseOptions } = require('./cli-utils'); const { getWallet, printWalletInfo, getRawPrivateKey, broadcast } = require('./sign-utils'); const { bytes32Struct, signersStruct, messageToSignStruct, messageStruct, proofStruct } = require('./types-utils'); -const { loadSuiConfig } = require('./utils'); const { getSigners } = require('./deploy-gateway'); const secp256k1 = require('secp256k1'); @@ -218,7 +218,7 @@ async function rotateSigners(keypair, client, config, chain, args, options) { } async function mainProcessor(processor, args, options) { - const config = loadSuiConfig(options.env); + const config = loadConfig(options.env); const [keypair, client] = getWallet(config.sui, options); await printWalletInfo(keypair, client, config.sui, options); diff --git a/sui/gmp.js b/sui/gmp.js index a1bf4a96..5c3ab2a5 100644 --- a/sui/gmp.js +++ b/sui/gmp.js @@ -2,7 +2,8 @@ const { saveConfig, printInfo } = require('../common/utils'); const { Command } = require('commander'); const { Transaction } = require('@mysten/sui/transactions'); const { bcs } = require('@mysten/sui/bcs'); -const { loadSuiConfig, getBcsBytesByObjectId } = require('./utils'); +const { getBcsBytesByObjectId } = require('./utils'); +const { loadConfig } = require('../common/utils'); const { ethers } = require('hardhat'); const { utils: { arrayify }, @@ -65,8 +66,8 @@ async function execute(keypair, client, contracts, args, options) { const [sourceChain, messageId, sourceAddress, payload] = args; - const gatewayObjectId = axelarGatewayConfig.objects.gateway; - const discoveryObjectId = axelarGatewayConfig.objects.relayerDiscovery; + const gatewayObjectId = axelarGatewayConfig.objects.Gateway; + const discoveryObjectId = axelarGatewayConfig.objects.RelayerDiscovery; // Get the channel id from the options or use the channel id from the deployed test contract object. const channelId = options.channelId || testConfig.objects.channelId; @@ -165,7 +166,7 @@ async function processCommand(command, chain, args, options) { } async function mainProcessor(command, options, args, processor) { - const config = loadSuiConfig(options.env); + const config = loadConfig(options.env); await processor(command, config.sui, args, options); saveConfig(config, options.env); } diff --git a/sui/multisig.js b/sui/multisig.js index 9d69414d..2571ac8d 100644 --- a/sui/multisig.js +++ b/sui/multisig.js @@ -3,7 +3,7 @@ const { fromB64 } = require('@mysten/bcs'); const { addBaseOptions } = require('./cli-utils'); const { getWallet, getMultisig, signTransactionBlockBytes, broadcastSignature } = require('./sign-utils'); const { getSignedTx, storeSignedTx } = require('../evm/sign-utils'); -const { loadSuiConfig } = require('./utils'); +const { loadConfig } = require('../common/utils'); const { printInfo, validateParameters } = require('../common/utils'); async function signTx(keypair, client, options) { @@ -143,7 +143,7 @@ async function processCommand(chain, options) { } async function mainProcessor(options, processor) { - const config = loadSuiConfig(options.env); + const config = loadConfig(options.env); await processor(config.sui, options); } diff --git a/sui/transfer-object.js b/sui/transfer-object.js index 1d58ee6f..10919e00 100644 --- a/sui/transfer-object.js +++ b/sui/transfer-object.js @@ -3,7 +3,7 @@ const { Command, Option } = require('commander'); const { printInfo, validateParameters } = require('../common/utils'); const { addExtendedOptions } = require('./cli-utils'); const { getWallet, printWalletInfo } = require('./sign-utils'); -const { loadSuiConfig } = require('./utils'); +const { loadConfig } = require('../common/utils'); async function processCommand(chain, options) { const [keypair, client] = getWallet(chain, options); @@ -54,7 +54,7 @@ async function processCommand(chain, options) { } async function mainProcessor(options, processor) { - const config = loadSuiConfig(options.env); + const config = loadConfig(options.env); await processor(config.sui, options); } diff --git a/sui/utils.js b/sui/utils.js index e019d1fe..eac07a8a 100644 --- a/sui/utils.js +++ b/sui/utils.js @@ -1,14 +1,18 @@ 'use strict'; const { ethers } = require('hardhat'); -const { loadConfig } = require('../common/utils'); +const toml = require('toml'); +const { printInfo, printError } = require('../common/utils'); const { BigNumber, - utils: { arrayify, hexlify }, + utils: { arrayify, hexlify, toUtf8Bytes, keccak256 }, + constants: { HashZero }, } = ethers; +const fs = require('fs'); const { fromB64 } = require('@mysten/bcs'); const { CosmWasmClient } = require('@cosmjs/cosmwasm-stargate'); const { updateMoveToml, copyMovePackage, TxBuilder } = require('@axelar-network/axelar-cgp-sui'); +const { singletonStruct } = require('./types-utils'); const suiPackageAddress = '0x2'; const suiClockAddress = '0x6'; @@ -48,23 +52,6 @@ const getBcsBytesByObjectId = async (client, objectId) => { return fromB64(response.data.bcs.bcsBytes); }; -const loadSuiConfig = (env) => { - const config = loadConfig(env); - const suiEnv = env === 'local' ? 'localnet' : env; - - if (!config.sui) { - config.sui = { - networkType: suiEnv, - name: 'Sui', - contracts: { - axelar_gateway: {}, - }, - }; - } - - return config; -}; - const deployPackage = async (packageName, client, keypair, options = {}) => { const compileDir = `${__dirname}/move`; @@ -80,6 +67,24 @@ const deployPackage = async (packageName, client, keypair, options = {}) => { return { packageId, publishTxn }; }; +const findPublishedObject = (published, packageDir, contractName) => { + const packageId = published.packageId; + return published.publishTxn.objectChanges.find((change) => change.objectType === `${packageId}::${packageDir}::${contractName}`); +}; + +const readMovePackageName = (moveDir) => { + try { + const moveToml = fs.readFileSync(`${__dirname}/../node_modules/@axelar-network/axelar-cgp-sui/move/${moveDir}/Move.toml`, 'utf8'); + + const { package: movePackage } = toml.parse(moveToml); + + return movePackage.name; + } catch (err) { + printError('Error reading TOML file'); + throw err; + } +}; + const getObjectIdsByObjectTypes = (txn, objectTypes) => objectTypes.map((objectType) => { const objectId = txn.objectChanges.find((change) => change.objectType === objectType)?.objectId; @@ -91,12 +96,52 @@ const getObjectIdsByObjectTypes = (txn, objectTypes) => return objectId; }); +// Parse bcs bytes from singleton object which is created when the Test contract is deployed +const getChannelId = async (client, singletonObjectId) => { + const bcsBytes = await getBcsBytesByObjectId(client, singletonObjectId); + const data = singletonStruct.parse(bcsBytes); + return '0x' + data.channel.id; +}; + +const getSigners = async (keypair, config, chain, options) => { + if (options.signers === 'wallet') { + const pubKey = keypair.getPublicKey().toRawBytes(); + printInfo('Using wallet pubkey as the signer for the gateway', hexlify(pubKey)); + + if (keypair.getKeyScheme() !== 'Secp256k1') { + throw new Error('Only Secp256k1 pubkeys are supported by the gateway'); + } + + return { + signers: [{ pub_key: pubKey, weight: 1 }], + threshold: 1, + nonce: options.nonce ? keccak256(toUtf8Bytes(options.nonce)) : HashZero, + }; + } else if (options.signers) { + printInfo('Using provided signers', options.signers); + + const signers = JSON.parse(options.signers); + return { + signers: signers.signers.map(({ pub_key: pubKey, weight }) => { + return { pub_key: arrayify(pubKey), weight }; + }), + threshold: signers.threshold, + nonce: arrayify(signers.nonce) || HashZero, + }; + } + + return getAmplifierSigners(config, chain); +}; + module.exports = { suiPackageAddress, suiClockAddress, getAmplifierSigners, getBcsBytesByObjectId, - loadSuiConfig, deployPackage, + findPublishedObject, + readMovePackageName, getObjectIdsByObjectTypes, + getChannelId, + getSigners, };