diff --git a/.github/workflows/test-sui.yaml b/.github/workflows/test-sui.yaml index 1364c12c..c38bdb01 100644 --- a/.github/workflows/test-sui.yaml +++ b/.github/workflows/test-sui.yaml @@ -33,9 +33,13 @@ jobs: - name: Spin up Sui Network run: nohup sh -c "sui-test-validator" > nohup.out 2> nohup.err < /dev/null & - - name: Sleep for 30 seconds - run: sleep 30s - shell: bash + - name: Wait for Sui network + uses: nev7n/wait_for_response@v1 + with: + url: 'http://localhost:9123' + responseCode: 200 + timeout: 60000 + interval: 1000 - name: Setup Sui Local Network run: | @@ -139,7 +143,61 @@ jobs: ###### Command: Multisig ###### - # TODO: Add multisig tests + - name: Init Multisig + run: | + # Create new addresses + sui client new-address secp256k1 multisig1 + sui client new-address secp256k1 multisig2 + + # Export keys and addresses + KEY_1=$(sui keytool export --key-identity multisig1 --json | jq -r .key.publicBase64Key) + KEY_2=$(sui keytool export --key-identity multisig2 --json | jq -r .key.publicBase64Key) + + # Get multisig address + MULTISIG_ADDRESS=$(sui keytool multi-sig-address --pks $KEY_1 $KEY_2 --weights 1 1 --threshold 1 --json | jq -r .multisigAddress) + + # Initialize multisig + node sui/multisig.js --action init --threshold 1 --base64PublicKeys $KEY_1 $KEY_2 --schemeTypes secp256k1 secp256k1 + + # Faucet operations + node sui/faucet.js --recipient $MULTISIG_ADDRESS + + # Set environment variables + echo "MULTISIG_ADDRESS=$MULTISIG_ADDRESS" >> $GITHUB_ENV + + - name: Transfer Upgrade Cap to Multisig Address + run: | + upgrade_cap=$(cat axelar-chains-config/info/local.json | jq -r '.sui.contracts.AxelarGateway.objects.UpgradeCap') + node sui/transfer-object.js --objectId $upgrade_cap --recipient $MULTISIG_ADDRESS + + - name: Generate Unsigned Tx File + run: | + node sui/deploy-contract.js upgrade AxelarGateway any_upgrade --offline --txFilePath ./tx-upgrade.json --sender $MULTISIG_ADDRESS + + - name: Sign Tx File with Multisig Signer + run: | + pk_1=$(sui keytool export --key-identity multisig1 --json | jq .exportedPrivateKey | sed 's/"//g') + pk_2=$(sui keytool export --key-identity multisig2 --json | jq .exportedPrivateKey | sed 's/"//g') + node sui/multisig.js --action sign --txBlockPath ./tx-upgrade.json --signatureFilePath signature-1.json --offline --privateKey $pk_1 + node sui/multisig.js --action sign --txBlockPath ./tx-upgrade.json --signatureFilePath signature-2.json --offline --privateKey $pk_2 + + - name: Submit Signed Tx File + run: | + # Define output file for the executed transaction + output_file="./output.json" + + # Execute the upgrade transaction + node sui/multisig.js --txBlockPath ./tx-upgrade.json --signatureFilePath ./combined.json --action combine --signatures signature-1.json signature-2.json --executeResultPath ${output_file} + + # Store the new package id in a variable + new_package_id=$(jq '.objectChanges[] | select(.type == "published") | .packageId' $output_file | sed 's/"//g') + + # Update the local.json file with the new package id + jq --arg pkg "$new_package_id" '.sui.contracts.AxelarGateway.address = $pkg' axelar-chains-config/info/local.json > temp.json \ + && mv temp.json axelar-chains-config/info/local.json + + - name: Post Upgrade Gateway Approval With New Package ID + run: node sui/gateway.js approve --proof wallet ethereum 0x32034b47cb29d162d9d803cc405356f4ac0ec07fe847ace431385fe8acf3e6e5-10 0x4F4495243837681061C4743b74B3eEdf548D56A5 0x6ce0d81b412abca2770eddb1549c9fcff721889c3aab1203dc93866db22ecc4b 0x56570de287d73cd1cb6092bb8fdee6173974955fdef345ae579ee9f475ea7432 ###### Command: Transfer Object ###### - name: Transfer Object diff --git a/sui/faucet.js b/sui/faucet.js index a52acdb1..2ffb69b0 100644 --- a/sui/faucet.js +++ b/sui/faucet.js @@ -3,19 +3,20 @@ const { requestSuiFromFaucetV0, getFaucetHost } = require('@mysten/sui/faucet'); const { saveConfig, loadConfig, printInfo } = require('../common/utils'); const { getWallet, printWalletInfo, addBaseOptions } = require('./utils'); -const { Command } = require('commander'); +const { Command, Option } = require('commander'); async function processCommand(config, chain, options) { const [keypair, client] = getWallet(chain, options); + const recipient = options.recipient || keypair.toSuiAddress(); await printWalletInfo(keypair, client, chain, options); await requestSuiFromFaucetV0({ host: getFaucetHost(chain.networkType), - recipient: keypair.toSuiAddress(), + recipient, }); - printInfo('Funds requested'); + printInfo('Funds requested', recipient); } async function mainProcessor(options, processor) { @@ -27,13 +28,15 @@ async function mainProcessor(options, processor) { if (require.main === module) { const program = new Command(); - program.name('faucet').description('Query the faucet for funds.'); + program + .name('faucet') + .addOption(new Option('--recipient ', 'recipient to request funds for')) + .description('Query the faucet for funds.') + .action((options) => { + mainProcessor(options, processCommand); + }); addBaseOptions(program); - program.action((options) => { - mainProcessor(options, processCommand); - }); - program.parse(); } diff --git a/sui/multisig.js b/sui/multisig.js index c09fef3a..a1cfbd9e 100644 --- a/sui/multisig.js +++ b/sui/multisig.js @@ -1,9 +1,63 @@ const { Command, Option } = require('commander'); const { fromB64 } = require('@mysten/bcs'); -const { loadConfig, printInfo, validateParameters } = require('../common/utils'); +const { saveConfig } = require('../common/utils'); +const { loadConfig, printInfo, validateParameters, printWarn } = require('../common/utils'); const { getSignedTx, storeSignedTx } = require('../evm/sign-utils'); const { addBaseOptions, getWallet, getMultisig, signTransactionBlockBytes, broadcastSignature } = require('./utils'); +async function initMultisigConfig(chain, options) { + const { base64PublicKeys, threshold } = options; + + if (!base64PublicKeys) { + throw new Error('Please provide public keys with --base64PublicKeys option'); + } + + if (!threshold) { + throw new Error('Please provide threshold with --threshold option'); + } + + const uniqueKeys = new Set(base64PublicKeys); + + if (uniqueKeys.size !== base64PublicKeys.length) { + throw new Error('Duplicate public keys found'); + } + + const schemeTypes = options.schemeTypes || Array(base64PublicKeys.length).fill('secp256k1'); + const weights = options.weights || Array(base64PublicKeys.length).fill(1); + + if (!options.schemeTypes) { + printWarn('Scheme types not provided, defaulting to secp256k1'); + } + + if (!options.weights) { + printWarn('Weights not provided, defaulting to 1'); + } + + if (base64PublicKeys.length !== weights.length) { + throw new Error('Public keys and weights length mismatch'); + } + + if (base64PublicKeys.length !== schemeTypes.length) { + throw new Error('Public keys and scheme types length mismatch'); + } + + const signers = base64PublicKeys.map((key, i) => ({ + publicKey: key, + weight: parseInt(weights[i]), + schemeType: options.schemeTypes[i], + })); + + chain.multisig = { + signers, + threshold: parseInt(threshold), + }; + + printInfo('Saved multisig config'); + + // To print in the separate lines with proper indentation + printInfo(JSON.stringify(chain.multisig, null, 2)); +} + async function signTx(keypair, client, options) { const txFileData = getSignedTx(options.txBlockPath); const txData = txFileData?.unsignedTx; @@ -85,7 +139,12 @@ async function combineSignature(client, chain, options) { if (!options.offline) { const txResult = await broadcastSignature(client, txBlockBytes, combinedSignature); - printInfo('Transaction result', JSON.stringify(txResult)); + + if (options.executeResultPath) { + storeSignedTx(options.executeResultPath, txResult); + } + + printInfo('Executed', txResult.digest); } else { const data = { message: firstSignData.message, @@ -104,6 +163,11 @@ async function processCommand(chain, options) { let fileData; switch (options.action) { + case 'init': { + fileData = await initMultisigConfig(chain, options); + break; + } + case 'sign': { fileData = await signTx(keypair, client, options); break; @@ -143,6 +207,7 @@ async function processCommand(chain, options) { async function mainProcessor(options, processor) { const config = loadConfig(options.env); await processor(config.sui, options); + saveConfig(config, options.env); } if (require.main === module) { @@ -153,12 +218,28 @@ if (require.main === module) { addBaseOptions(program); program.addOption(new Option('--txBlockPath ', 'path to unsigned tx block')); - program.addOption(new Option('--action ', 'action').choices(['sign', 'combine', 'execute']).makeOptionMandatory(true)); + program.addOption(new Option('--action ', 'action').choices(['sign', 'combine', 'execute', 'init']).makeOptionMandatory(true)); program.addOption(new Option('--multisigKey ', 'multisig key').env('MULTISIG_KEY')); program.addOption(new Option('--signatures [files...]', 'array of signed transaction files')); program.addOption(new Option('--offline', 'run in offline mode')); program.addOption(new Option('--combinedSignPath ', 'combined signature file path')); program.addOption(new Option('--signatureFilePath ', 'signed signature will be stored')); + program.addOption(new Option('--executeResultPath ', 'execute result will be stored')); + + // The following options are only used with the init action + program.addOption( + new Option('--base64PublicKeys [base64PublicKeys...]', 'An array of public keys to use for init the multisig address'), + ); + program.addOption( + new Option('--weights [weights...]', 'An array of weight for each base64 public key. The default value is 1 for each'), + ); + program.addOption( + new Option( + '--schemeTypes [schemeTypes...]', + 'An array of scheme types for each base64 public key. The default value is secp256k1 for each', + ), + ); + program.addOption(new Option('--threshold ', 'threshold for multisig')); program.action((options) => { mainProcessor(options, processCommand);