From 1f23938555618fb44915117c393d93d8dd2df7bc Mon Sep 17 00:00:00 2001 From: npty Date: Fri, 12 Jul 2024 17:29:36 +0700 Subject: [PATCH 01/22] feat: add gas-service command --- sui/gas-service.js | 94 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 sui/gas-service.js diff --git a/sui/gas-service.js b/sui/gas-service.js new file mode 100644 index 000000000..9a50e5226 --- /dev/null +++ b/sui/gas-service.js @@ -0,0 +1,94 @@ +const { saveConfig, prompt, printInfo } = require('../evm/utils'); +const { Command, Option } = require('commander'); +const { TransactionBlock } = require('@mysten/sui.js/transactions'); +const { bcs } = require('@mysten/sui.js/bcs'); +const { loadSuiConfig } = require('./utils'); +const { ethers } = require('hardhat'); +const { + utils: { arrayify, keccak256, toUtf8Bytes }, + constants: { HashZero }, +} = ethers; + +const { addBaseOptions } = require('./cli-utils'); +const { getWallet, printWalletInfo, broadcast } = require('./sign-utils'); + +async function payGas(config, chain, args, options) { + const [keypair, client] = getWallet(chain, options); + const walletAddress = keypair.toSuiAddress(); + + const gasServiceConfig = chain.contracts.axelar_gas_service; + const gasServicePackageId = gasServiceConfig.address; + + const gatewayConfig = chain.contracts.axelar_gateway; + const gatewayPackageId = gatewayConfig.address; + + const tx = new TransactionBlock(); + + const [coin] = tx.splitCoins(tx.gas, [100]); + + const [destinationChain, destinationAddress, payload] = args; + + let channel = options.channel; + + if (!options.channel) { + [channel] = tx.moveCall({ + target: `${gatewayPackageId}::channel::new`, + arguments: [], + }); + } + + tx.moveCall({ + target: `${gasServicePackageId}::gas_service::pay_gas`, + arguments: [ + gasServicePackageId, // Gas service package ID + coin, // Coin + channel, // Channel address + tx.pure(bcs.string().serialize(destinationChain).toBytes()), // Destination chain + tx.pure(bcs.string().serialize(destinationAddress).toBytes()), // Destination address + tx.pure(bcs.vector(bcs.u8()).serialize(arrayify(payload)).toBytes()), // Payload + walletAddress, // Refund address + bcs.vector(), // Params + ], + }); +} + +async function processCommand(command, config, chain, args, options) { + const [keypair, client] = getWallet(chain, options); + + await printWalletInfo(keypair, client, chain, options); + + if (!chain.contracts.axelar_gas_service) { + throw new Error('Axelar gas service contract not found'); + } + + switch (command) { + case 'pay_gas': + await payGas(config, chain, args, options); + break; + } +} + +async function mainProcessor(command, options, args, processor) { + const config = loadSuiConfig(options.env); + + await processor(command, config, config.sui, args, options); + saveConfig(config, options.env); +} + +if (require.main === module) { + const program = new Command(); + + program.name('gas-service').description('Interact with the gas service contract.'); + + const payGasProgram = program.command('pay_gas '); + payGasProgram.description('Pay gas to the destination chain.'); + payGasProgram.action((destinationChain, destinationAddress, payload, options) => { + mainProcessor('pay_gas', options, [destinationChain, destinationAddress, payload], processCommand); + }); + + program.addCommand(payGasProgram); + + addBaseOptions(program); + + program.parse(); +} From e5c5ae3e134dfa93c3fe208bb5f7b55b5a5e5a2b Mon Sep 17 00:00:00 2001 From: npty Date: Fri, 12 Jul 2024 20:30:22 +0700 Subject: [PATCH 02/22] chore: add pay_gas command --- sui/gas-service.js | 45 +++++++++++++++++++-------------------------- 1 file changed, 19 insertions(+), 26 deletions(-) diff --git a/sui/gas-service.js b/sui/gas-service.js index 9a50e5226..42e31b59d 100644 --- a/sui/gas-service.js +++ b/sui/gas-service.js @@ -1,12 +1,11 @@ -const { saveConfig, prompt, printInfo } = require('../evm/utils'); -const { Command, Option } = require('commander'); +const { saveConfig, printInfo } = require('../evm/utils'); +const { Command } = require('commander'); const { TransactionBlock } = require('@mysten/sui.js/transactions'); const { bcs } = require('@mysten/sui.js/bcs'); const { loadSuiConfig } = require('./utils'); const { ethers } = require('hardhat'); const { - utils: { arrayify, keccak256, toUtf8Bytes }, - constants: { HashZero }, + utils: { arrayify }, } = ethers; const { addBaseOptions } = require('./cli-utils'); @@ -19,37 +18,31 @@ async function payGas(config, chain, args, options) { const gasServiceConfig = chain.contracts.axelar_gas_service; const gasServicePackageId = gasServiceConfig.address; - const gatewayConfig = chain.contracts.axelar_gateway; - const gatewayPackageId = gatewayConfig.address; - const tx = new TransactionBlock(); - const [coin] = tx.splitCoins(tx.gas, [100]); - - const [destinationChain, destinationAddress, payload] = args; + const [senderAddress, amount, destinationChain, destinationAddress, payload] = args; - let channel = options.channel; + const atomicAmount = ethers.utils.parseUnits(amount, 6).toString(); - if (!options.channel) { - [channel] = tx.moveCall({ - target: `${gatewayPackageId}::channel::new`, - arguments: [], - }); - } + const [coin] = tx.splitCoins(tx.gas, [atomicAmount]); tx.moveCall({ target: `${gasServicePackageId}::gas_service::pay_gas`, arguments: [ - gasServicePackageId, // Gas service package ID + tx.object(gasServiceConfig.objects.gas_service), coin, // Coin - channel, // Channel address + tx.pure.address(senderAddress), // Channel address tx.pure(bcs.string().serialize(destinationChain).toBytes()), // Destination chain tx.pure(bcs.string().serialize(destinationAddress).toBytes()), // Destination address tx.pure(bcs.vector(bcs.u8()).serialize(arrayify(payload)).toBytes()), // Payload - walletAddress, // Refund address - bcs.vector(), // Params + tx.pure.address(walletAddress), // Refund address + tx.pure(bcs.vector(bcs.u8()).serialize(arrayify('0x')).toBytes()), // Params ], }); + + const receipt = await broadcast(client, keypair, tx); + + printInfo('Gas paid', receipt.digest); } async function processCommand(command, config, chain, args, options) { @@ -70,7 +63,6 @@ async function processCommand(command, config, chain, args, options) { async function mainProcessor(command, options, args, processor) { const config = loadSuiConfig(options.env); - await processor(command, config, config.sui, args, options); saveConfig(config, options.env); } @@ -80,15 +72,16 @@ if (require.main === module) { program.name('gas-service').description('Interact with the gas service contract.'); - const payGasProgram = program.command('pay_gas '); + const payGasProgram = program.command('pay_gas '); payGasProgram.description('Pay gas to the destination chain.'); - payGasProgram.action((destinationChain, destinationAddress, payload, options) => { - mainProcessor('pay_gas', options, [destinationChain, destinationAddress, payload], processCommand); + payGasProgram.action((senderAddress, amount, destinationChain, destinationAddress, payload, options) => { + mainProcessor('pay_gas', options, [senderAddress, amount, destinationChain, destinationAddress, payload], processCommand); }); + program.addCommand(payGasProgram); - addBaseOptions(program); + addBaseOptions(payGasProgram); program.parse(); } From ba43cf7d997975a7b450ec05b8d1fe82f8751b04 Mon Sep 17 00:00:00 2001 From: npty Date: Fri, 12 Jul 2024 20:56:30 +0700 Subject: [PATCH 03/22] feat: implement 'add_gas' --- sui/gas-service.js | 80 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 69 insertions(+), 11 deletions(-) diff --git a/sui/gas-service.js b/sui/gas-service.js index 42e31b59d..84e02ca32 100644 --- a/sui/gas-service.js +++ b/sui/gas-service.js @@ -1,5 +1,5 @@ const { saveConfig, printInfo } = require('../evm/utils'); -const { Command } = require('commander'); +const { Command, Option } = require('commander'); const { TransactionBlock } = require('@mysten/sui.js/transactions'); const { bcs } = require('@mysten/sui.js/bcs'); const { loadSuiConfig } = require('./utils'); @@ -18,9 +18,14 @@ async function payGas(config, chain, args, options) { const gasServiceConfig = chain.contracts.axelar_gas_service; const gasServicePackageId = gasServiceConfig.address; + const channel = options.channel; + + const refundAddress = options.refund_address || walletAddress; + const params = options.params || '0x'; + const tx = new TransactionBlock(); - const [senderAddress, amount, destinationChain, destinationAddress, payload] = args; + const [amount, destinationChain, destinationAddress, payload] = args; const atomicAmount = ethers.utils.parseUnits(amount, 6).toString(); @@ -31,12 +36,12 @@ async function payGas(config, chain, args, options) { arguments: [ tx.object(gasServiceConfig.objects.gas_service), coin, // Coin - tx.pure.address(senderAddress), // Channel address + tx.pure.address(channel), // Channel address tx.pure(bcs.string().serialize(destinationChain).toBytes()), // Destination chain tx.pure(bcs.string().serialize(destinationAddress).toBytes()), // Destination address tx.pure(bcs.vector(bcs.u8()).serialize(arrayify(payload)).toBytes()), // Payload - tx.pure.address(walletAddress), // Refund address - tx.pure(bcs.vector(bcs.u8()).serialize(arrayify('0x')).toBytes()), // Params + tx.pure.address(refundAddress), // Refund address + tx.pure(bcs.vector(bcs.u8()).serialize(arrayify(params)).toBytes()), // Params ], }); @@ -45,6 +50,40 @@ async function payGas(config, chain, args, options) { printInfo('Gas paid', receipt.digest); } +async function addGas(config, chain, args, options) { + const [keypair, client] = getWallet(chain, options); + const walletAddress = keypair.toSuiAddress(); + + const gasServiceConfig = chain.contracts.axelar_gas_service; + const gasServicePackageId = gasServiceConfig.address; + + const refundAddress = options.refund_address || walletAddress; + const params = options.params || '0x'; + + const tx = new TransactionBlock(); + + const [messageId, amount] = args; + + const atomicAmount = ethers.utils.parseUnits(amount, 6).toString(); + + const [coin] = tx.splitCoins(tx.gas, [atomicAmount]); + + tx.moveCall({ + target: `${gasServicePackageId}::gas_service::add_gas`, + arguments: [ + tx.object(gasServiceConfig.objects.gas_service), + coin, // Coin + tx.pure(bcs.string().serialize(messageId).toBytes()), // Message ID for the contract call + tx.pure.address(refundAddress), // Refund address + tx.pure(bcs.vector(bcs.u8()).serialize(arrayify(params)).toBytes()), // Params + ], + }); + + const receipt = await broadcast(client, keypair, tx); + + printInfo('Gas added', receipt.digest); +} + async function processCommand(command, config, chain, args, options) { const [keypair, client] = getWallet(chain, options); @@ -56,8 +95,13 @@ async function processCommand(command, config, chain, args, options) { switch (command) { case 'pay_gas': + printInfo('Action', 'Pay gas'); await payGas(config, chain, args, options); break; + case 'add_gas': + printInfo('Action', 'Add gas'); + await addGas(config, chain, args, options); + break; } } @@ -72,16 +116,30 @@ if (require.main === module) { program.name('gas-service').description('Interact with the gas service contract.'); - const payGasProgram = program.command('pay_gas '); - payGasProgram.description('Pay gas to the destination chain.'); - payGasProgram.action((senderAddress, amount, destinationChain, destinationAddress, payload, options) => { - mainProcessor('pay_gas', options, [senderAddress, amount, destinationChain, destinationAddress, payload], processCommand); - }); - + const payGasProgram = program + .command('pay_gas ') + .description('Pay gas for the contract call.') + .requiredOption('--channel ', 'Existing channel ID to initiate a cross-chain message over') + .option('--refund_address ', 'Refund address. Default is the sender address.') + .option('--params ', 'Params. Default is empty.') + .action((amount, destinationChain, destinationAddress, payload, options) => { + mainProcessor('pay_gas', options, [amount, destinationChain, destinationAddress, payload], processCommand); + }); + + const addGasProgram = program + .command('add_gas ') + .description('Add gas for the contract call.') + .option('--refund_address ', 'Refund address.') + .option('--params ', 'Params. Default is empty.') + .action((messageId, amount, options) => { + mainProcessor('add_gas', options, [messageId, amount], processCommand); + }); program.addCommand(payGasProgram); + program.addCommand(addGasProgram); addBaseOptions(payGasProgram); + addBaseOptions(addGasProgram); program.parse(); } From f494f18c5ecdfcba5cb08829091fa6e61038a93a Mon Sep 17 00:00:00 2001 From: npty Date: Fri, 12 Jul 2024 20:59:10 +0700 Subject: [PATCH 04/22] chore: remove Option --- sui/gas-service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sui/gas-service.js b/sui/gas-service.js index 84e02ca32..02a4552bc 100644 --- a/sui/gas-service.js +++ b/sui/gas-service.js @@ -1,5 +1,5 @@ const { saveConfig, printInfo } = require('../evm/utils'); -const { Command, Option } = require('commander'); +const { Command } = require('commander'); const { TransactionBlock } = require('@mysten/sui.js/transactions'); const { bcs } = require('@mysten/sui.js/bcs'); const { loadSuiConfig } = require('./utils'); From 6ea0e8bcf84b581898cc98ed2951db7ee9fca231 Mon Sep 17 00:00:00 2001 From: npty Date: Sat, 13 Jul 2024 10:11:25 +0700 Subject: [PATCH 05/22] feat: add collectGas --- sui/gas-service.js | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/sui/gas-service.js b/sui/gas-service.js index 02a4552bc..4dd5fe0b2 100644 --- a/sui/gas-service.js +++ b/sui/gas-service.js @@ -84,6 +84,34 @@ async function addGas(config, chain, args, options) { printInfo('Gas added', receipt.digest); } +async function collectGas(config, chain, args, options) { + const [keypair, client] = getWallet(chain, options); + const walletAddress = keypair.toSuiAddress(); + + const gasServiceConfig = chain.contracts.axelar_gas_service; + const gasServicePackageId = gasServiceConfig.address; + + const tx = new TransactionBlock(); + + const [amount] = args; + const receiver = options.receiver || walletAddress; + + const atomicAmount = ethers.utils.parseUnits(amount, 6).toString(); + + tx.moveCall({ + target: `${gasServicePackageId}::gas_service::collect_gas`, + arguments: [ + tx.object(gasServiceConfig.objects.gas_service), + tx.pure.address(receiver), // Receiver address + atomicAmount, // Amount + ], + }); + + const receipt = await broadcast(client, keypair, tx); + + printInfo('Gas collected', receipt.digest); +} + async function processCommand(command, config, chain, args, options) { const [keypair, client] = getWallet(chain, options); @@ -102,6 +130,10 @@ async function processCommand(command, config, chain, args, options) { printInfo('Action', 'Add gas'); await addGas(config, chain, args, options); break; + case 'collect_gas': + printInfo('Action', 'Collect gas'); + await collectGas(config, chain, args, options); + break; } } @@ -135,11 +167,21 @@ if (require.main === module) { mainProcessor('add_gas', options, [messageId, amount], processCommand); }); + const collectGasProgram = program + .command('collect_gas ') + .description('Collect gas from the gas service contract.') + .option("--receiver ", "Receiver address. Default is the sender address.") + .action((amount, options) => { + mainProcessor('collect_gas', options, [amount], processCommand); + }); + program.addCommand(payGasProgram); program.addCommand(addGasProgram); + program.addCommand(collectGasProgram); addBaseOptions(payGasProgram); addBaseOptions(addGasProgram); + addBaseOptions(collectGasProgram); program.parse(); } From b287edceaef7db68e2ab48a8c929741f84583896 Mon Sep 17 00:00:00 2001 From: npty Date: Sat, 13 Jul 2024 17:01:24 +0700 Subject: [PATCH 06/22] chore: add amount utils --- sui/amount-utils.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 sui/amount-utils.js diff --git a/sui/amount-utils.js b/sui/amount-utils.js new file mode 100644 index 000000000..97068f3d2 --- /dev/null +++ b/sui/amount-utils.js @@ -0,0 +1,16 @@ +import { ethers } from 'ethers'; + +// Convert formatted amount to atomic units (e.g. 1000000000). Default decimals is 9 for SUI +function getAtomicAmount(amount, decimals = 9) { + return ethers.utils.parseUnits(amount, decimals).toBigInt(); +} + +// Convert atomic amount to formatted units (e.g. 1.0) with decimals. Default decimals is 9 for SUI +function getFormattedAmount(amount, decimals = 9) { + return ethers.utils.formatUnits(amount, decimals); +} + +module.exports = { + getAtomicAmount, + getFormattedAmount, +}; From 4c1da1b991fe777b2fb56c4de957037c71f44652 Mon Sep 17 00:00:00 2001 From: npty Date: Sat, 13 Jul 2024 17:03:11 +0700 Subject: [PATCH 07/22] chore: use getAtomicAmount --- sui/amount-utils.js | 2 +- sui/gas-service.js | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/sui/amount-utils.js b/sui/amount-utils.js index 97068f3d2..c0d93f762 100644 --- a/sui/amount-utils.js +++ b/sui/amount-utils.js @@ -1,4 +1,4 @@ -import { ethers } from 'ethers'; +const { ethers } = require('ethers'); // Convert formatted amount to atomic units (e.g. 1000000000). Default decimals is 9 for SUI function getAtomicAmount(amount, decimals = 9) { diff --git a/sui/gas-service.js b/sui/gas-service.js index 4dd5fe0b2..5a53f0cbe 100644 --- a/sui/gas-service.js +++ b/sui/gas-service.js @@ -4,6 +4,7 @@ const { TransactionBlock } = require('@mysten/sui.js/transactions'); const { bcs } = require('@mysten/sui.js/bcs'); const { loadSuiConfig } = require('./utils'); const { ethers } = require('hardhat'); +const { getAtomicAmount } = require('./amount-utils'); const { utils: { arrayify }, } = ethers; @@ -27,7 +28,7 @@ async function payGas(config, chain, args, options) { const [amount, destinationChain, destinationAddress, payload] = args; - const atomicAmount = ethers.utils.parseUnits(amount, 6).toString(); + const atomicAmount = getAtomicAmount(amount); const [coin] = tx.splitCoins(tx.gas, [atomicAmount]); @@ -64,7 +65,7 @@ async function addGas(config, chain, args, options) { const [messageId, amount] = args; - const atomicAmount = ethers.utils.parseUnits(amount, 6).toString(); + const atomicAmount = getAtomicAmount(amount); const [coin] = tx.splitCoins(tx.gas, [atomicAmount]); @@ -96,14 +97,15 @@ async function collectGas(config, chain, args, options) { const [amount] = args; const receiver = options.receiver || walletAddress; - const atomicAmount = ethers.utils.parseUnits(amount, 6).toString(); + const atomicAmount = getAtomicAmount(amount); tx.moveCall({ target: `${gasServicePackageId}::gas_service::collect_gas`, arguments: [ tx.object(gasServiceConfig.objects.gas_service), + tx.object(gasServiceConfig.objects.gas_collector_cap), tx.pure.address(receiver), // Receiver address - atomicAmount, // Amount + tx.pure.u64(atomicAmount), // Amount ], }); @@ -168,9 +170,9 @@ if (require.main === module) { }); const collectGasProgram = program - .command('collect_gas ') + .command('collect_gas ') .description('Collect gas from the gas service contract.') - .option("--receiver ", "Receiver address. Default is the sender address.") + .option('--receiver ', 'Receiver address. Default is the sender address.') .action((amount, options) => { mainProcessor('collect_gas', options, [amount], processCommand); }); From 76fa49d74f4312a46d23932ef0566c7334457a38 Mon Sep 17 00:00:00 2001 From: npty Date: Sun, 14 Jul 2024 14:57:59 +0700 Subject: [PATCH 08/22] feat: add refund command --- sui/gas-service.js | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/sui/gas-service.js b/sui/gas-service.js index 5a53f0cbe..e1d22352b 100644 --- a/sui/gas-service.js +++ b/sui/gas-service.js @@ -114,6 +114,35 @@ async function collectGas(config, chain, args, options) { printInfo('Gas collected', receipt.digest); } +async function refund(config, chain, args, options) { + const [keypair, client] = getWallet(chain, options); + const walletAddress = keypair.toSuiAddress(); + + const gasServiceConfig = chain.contracts.axelar_gas_service; + const gasServicePackageId = gasServiceConfig.address; + + const tx = new TransactionBlock(); + + const [messageId, amount] = args; + + const atomicAmount = getAtomicAmount(amount); + + tx.moveCall({ + target: `${gasServicePackageId}::gas_service::refund`, + arguments: [ + tx.object(gasServiceConfig.objects.gas_service), + tx.object(gasServiceConfig.objects.gas_collector_cap), + tx.pure(bcs.string().serialize(messageId).toBytes()), // Message ID for the contract call + tx.pure.address(walletAddress), // Refund address + tx.pure.u64(atomicAmount), // Amount + ], + }); + + const receipt = await broadcast(client, keypair, tx); + + printInfo('Gas refunded', receipt.digest); +} + async function processCommand(command, config, chain, args, options) { const [keypair, client] = getWallet(chain, options); @@ -136,6 +165,10 @@ async function processCommand(command, config, chain, args, options) { printInfo('Action', 'Collect gas'); await collectGas(config, chain, args, options); break; + case 'refund': + printInfo('Action', 'Refund gas'); + await refund(config, chain, args, options); + break; } } @@ -177,13 +210,23 @@ if (require.main === module) { mainProcessor('collect_gas', options, [amount], processCommand); }); + const refundProgram = program + .command('refund ') + .description('Refund gas from the gas service contract.') + .option('--receiver ', 'Receiver address. Default is the sender address.') + .action((messageId, amount, options) => { + mainProcessor('refund', options, [messageId, amount], processCommand); + }); + program.addCommand(payGasProgram); program.addCommand(addGasProgram); program.addCommand(collectGasProgram); + program.addCommand(refundProgram); addBaseOptions(payGasProgram); addBaseOptions(addGasProgram); addBaseOptions(collectGasProgram); + addBaseOptions(refundProgram); program.parse(); } From ed7e61c17edfdeadb303a9b58bd06b54d72a661d Mon Sep 17 00:00:00 2001 From: npty Date: Sun, 14 Jul 2024 15:08:07 +0700 Subject: [PATCH 09/22] chore: fix duplicated commands --- sui/gas-service.js | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/sui/gas-service.js b/sui/gas-service.js index e1d22352b..866a3fac4 100644 --- a/sui/gas-service.js +++ b/sui/gas-service.js @@ -181,11 +181,15 @@ async function mainProcessor(command, options, args, processor) { if (require.main === module) { const program = new Command(); - program.name('gas-service').description('Interact with the gas service contract.'); + program + .name('gas-service') + .description( + 'Interact with the gas service contract.', + ); - const payGasProgram = program + const payGasProgram = new Command() .command('pay_gas ') - .description('Pay gas for the contract call.') + .description('Pay gas for the new contract call.') .requiredOption('--channel ', 'Existing channel ID to initiate a cross-chain message over') .option('--refund_address ', 'Refund address. Default is the sender address.') .option('--params ', 'Params. Default is empty.') @@ -193,16 +197,16 @@ if (require.main === module) { mainProcessor('pay_gas', options, [amount, destinationChain, destinationAddress, payload], processCommand); }); - const addGasProgram = program + const addGasProgram = new Command() .command('add_gas ') - .description('Add gas for the contract call.') + .description('Add gas for the existing contract call.') .option('--refund_address ', 'Refund address.') .option('--params ', 'Params. Default is empty.') .action((messageId, amount, options) => { mainProcessor('add_gas', options, [messageId, amount], processCommand); }); - const collectGasProgram = program + const collectGasProgram = new Command() .command('collect_gas ') .description('Collect gas from the gas service contract.') .option('--receiver ', 'Receiver address. Default is the sender address.') @@ -210,7 +214,7 @@ if (require.main === module) { mainProcessor('collect_gas', options, [amount], processCommand); }); - const refundProgram = program + const refundProgram = new Command() .command('refund ') .description('Refund gas from the gas service contract.') .option('--receiver ', 'Receiver address. Default is the sender address.') From d916c1ce85025dfcc54aa22b58addadea9bc45ee Mon Sep 17 00:00:00 2001 From: npty Date: Mon, 15 Jul 2024 15:04:20 +0700 Subject: [PATCH 10/22] chore: check for gas service balance --- sui/gas-service.js | 27 +++++++++++++++++---------- sui/types-utils.js | 12 ++++++++++++ sui/utils.js | 14 ++++++++++++++ 3 files changed, 43 insertions(+), 10 deletions(-) diff --git a/sui/gas-service.js b/sui/gas-service.js index 866a3fac4..35cb42ebc 100644 --- a/sui/gas-service.js +++ b/sui/gas-service.js @@ -1,10 +1,11 @@ -const { saveConfig, printInfo } = require('../evm/utils'); +const { saveConfig, printInfo, printError } = require('../evm/utils'); const { Command } = require('commander'); const { TransactionBlock } = require('@mysten/sui.js/transactions'); const { bcs } = require('@mysten/sui.js/bcs'); -const { loadSuiConfig } = require('./utils'); +const { gasServiceStruct } = require('./types-utils'); +const { loadSuiConfig, getBcsBytesByObjectId } = require('./utils'); const { ethers } = require('hardhat'); -const { getAtomicAmount } = require('./amount-utils'); +const { getAtomicAmount, getFormattedAmount } = require('./amount-utils'); const { utils: { arrayify }, } = ethers; @@ -91,14 +92,24 @@ async function collectGas(config, chain, args, options) { const gasServiceConfig = chain.contracts.axelar_gas_service; const gasServicePackageId = gasServiceConfig.address; - - const tx = new TransactionBlock(); + const gasServiceObjectId = gasServiceConfig.objects.gas_service; const [amount] = args; const receiver = options.receiver || walletAddress; const atomicAmount = getAtomicAmount(amount); + const bytes = await getBcsBytesByObjectId(client, gasServiceObjectId); + const { balance: gasServiceBalance } = gasServiceStruct.parse(bytes); + + // Check if the gas service balance is sufficient + if (gasServiceBalance < atomicAmount) { + printError('Insufficient gas service balance', `${getFormattedAmount(gasServiceBalance)} < ${getFormattedAmount(atomicAmount)}`); + return; + } + + const tx = new TransactionBlock(); + tx.moveCall({ target: `${gasServicePackageId}::gas_service::collect_gas`, arguments: [ @@ -181,11 +192,7 @@ async function mainProcessor(command, options, args, processor) { if (require.main === module) { const program = new Command(); - program - .name('gas-service') - .description( - 'Interact with the gas service contract.', - ); + program.name('gas-service').description('Interact with the gas service contract.'); const payGasProgram = new Command() .command('pay_gas ') diff --git a/sui/types-utils.js b/sui/types-utils.js index 8a2d38bb8..0be8d14c3 100644 --- a/sui/types-utils.js +++ b/sui/types-utils.js @@ -1,6 +1,7 @@ 'use strict'; const { bcs } = require('@mysten/sui.js/bcs'); +const { fromHEX, toHEX } = require('@mysten/bcs'); const { ethers } = require('hardhat'); const { utils: { arrayify, hexlify }, @@ -46,6 +47,16 @@ const proofStruct = bcs.struct('Proof', { signatures: bcs.vector(bcs.vector(bcs.u8())), }); +const UID = bcs.fixedArray(32, bcs.u8()).transform({ + input: (id) => fromHEX(id), + output: (id) => toHEX(Uint8Array.from(id)), +}); + +const gasServiceStruct = bcs.struct("GasService", { + id: UID, + balance: bcs.u64(), +}); + module.exports = { addressStruct, signerStruct, @@ -54,4 +65,5 @@ module.exports = { messageToSignStruct, messageStruct, proofStruct, + gasServiceStruct, }; diff --git a/sui/utils.js b/sui/utils.js index 295aeb01f..03d2de1b2 100644 --- a/sui/utils.js +++ b/sui/utils.js @@ -6,6 +6,7 @@ const { BigNumber, utils: { arrayify, hexlify }, } = ethers; +const { fromB64 } = require('@mysten/bcs'); const { CosmWasmClient } = require('@cosmjs/cosmwasm-stargate'); const getAmplifierSigners = async (config, chain) => { @@ -31,6 +32,18 @@ const getAmplifierSigners = async (config, chain) => { }; }; +// Given sui client and object id, return the base64-decoded object bcs bytes +const getBcsBytesByObjectId = async (client, objectId) => { + const response = await client.getObject({ + id: objectId, + options: { + showBcs: true, + }, + }); + + return fromB64(response.data.bcs.bcsBytes); +}; + const loadSuiConfig = (env) => { const config = loadConfig(env); const suiEnv = env === 'local' ? 'localnet' : env; @@ -55,6 +68,7 @@ const findPublishedObject = (published, packageName, contractName) => { module.exports = { getAmplifierSigners, + getBcsBytesByObjectId, loadSuiConfig, findPublishedObject, }; From 1266f2103bdc6bbbd66a28ded47b6c528d4888ec Mon Sep 17 00:00:00 2001 From: npty Date: Mon, 15 Jul 2024 15:11:29 +0700 Subject: [PATCH 11/22] chore: check refund amount --- sui/gas-service.js | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/sui/gas-service.js b/sui/gas-service.js index 35cb42ebc..136c7fbd9 100644 --- a/sui/gas-service.js +++ b/sui/gas-service.js @@ -25,12 +25,11 @@ async function payGas(config, chain, args, options) { const refundAddress = options.refund_address || walletAddress; const params = options.params || '0x'; - const tx = new TransactionBlock(); - const [amount, destinationChain, destinationAddress, payload] = args; const atomicAmount = getAtomicAmount(amount); + const tx = new TransactionBlock(); const [coin] = tx.splitCoins(tx.gas, [atomicAmount]); tx.moveCall({ @@ -62,12 +61,11 @@ async function addGas(config, chain, args, options) { const refundAddress = options.refund_address || walletAddress; const params = options.params || '0x'; - const tx = new TransactionBlock(); - const [messageId, amount] = args; const atomicAmount = getAtomicAmount(amount); + const tx = new TransactionBlock(); const [coin] = tx.splitCoins(tx.gas, [atomicAmount]); tx.moveCall({ @@ -131,20 +129,30 @@ async function refund(config, chain, args, options) { const gasServiceConfig = chain.contracts.axelar_gas_service; const gasServicePackageId = gasServiceConfig.address; - - const tx = new TransactionBlock(); + const gasServiceObjectId = gasServiceConfig.objects.gas_service; const [messageId, amount] = args; + const receiver = options.receiver || walletAddress; const atomicAmount = getAtomicAmount(amount); + const bytes = await getBcsBytesByObjectId(client, gasServiceObjectId); + const { balance: gasServiceBalance } = gasServiceStruct.parse(bytes); + + // Check if the gas service balance is sufficient + if (gasServiceBalance < atomicAmount) { + printError('Insufficient gas service balance', `${getFormattedAmount(gasServiceBalance)} < ${getFormattedAmount(atomicAmount)}`); + return; + } + + const tx = new TransactionBlock(); tx.moveCall({ target: `${gasServicePackageId}::gas_service::refund`, arguments: [ tx.object(gasServiceConfig.objects.gas_service), tx.object(gasServiceConfig.objects.gas_collector_cap), tx.pure(bcs.string().serialize(messageId).toBytes()), // Message ID for the contract call - tx.pure.address(walletAddress), // Refund address + tx.pure.address(receiver), // Refund address tx.pure.u64(atomicAmount), // Amount ], }); From 2289bbf867b341cb850e3bb1783fcb40f6dfa34c Mon Sep 17 00:00:00 2001 From: npty Date: Mon, 15 Jul 2024 15:13:23 +0700 Subject: [PATCH 12/22] chore: fix lint --- sui/types-utils.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sui/types-utils.js b/sui/types-utils.js index 0be8d14c3..b5a7cb1a1 100644 --- a/sui/types-utils.js +++ b/sui/types-utils.js @@ -48,13 +48,13 @@ const proofStruct = bcs.struct('Proof', { }); const UID = bcs.fixedArray(32, bcs.u8()).transform({ - input: (id) => fromHEX(id), - output: (id) => toHEX(Uint8Array.from(id)), + input: (id) => fromHEX(id), + output: (id) => toHEX(Uint8Array.from(id)), }); -const gasServiceStruct = bcs.struct("GasService", { - id: UID, - balance: bcs.u64(), +const gasServiceStruct = bcs.struct('GasService', { + id: UID, + balance: bcs.u64(), }); module.exports = { From bd28456074eb376dba2b9f0d124d0c3c3edadbd1 Mon Sep 17 00:00:00 2001 From: npty Date: Mon, 15 Jul 2024 19:59:19 +0700 Subject: [PATCH 13/22] chore: move UID type --- sui/types-utils.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sui/types-utils.js b/sui/types-utils.js index b5a7cb1a1..5f2c0a4ba 100644 --- a/sui/types-utils.js +++ b/sui/types-utils.js @@ -22,6 +22,11 @@ const bytes32Struct = bcs.fixedArray(32, bcs.u8()).transform({ output: (id) => hexlify(id), }); +const UID = bcs.fixedArray(32, bcs.u8()).transform({ + input: (id) => fromHEX(id), + output: (id) => toHEX(Uint8Array.from(id)), +}); + const signersStruct = bcs.struct('WeightedSigners', { signers: bcs.vector(signerStruct), threshold: bcs.u128(), @@ -47,11 +52,6 @@ const proofStruct = bcs.struct('Proof', { signatures: bcs.vector(bcs.vector(bcs.u8())), }); -const UID = bcs.fixedArray(32, bcs.u8()).transform({ - input: (id) => fromHEX(id), - output: (id) => toHEX(Uint8Array.from(id)), -}); - const gasServiceStruct = bcs.struct('GasService', { id: UID, balance: bcs.u64(), From 37956342554820375f167e3f537edc0e17b401ce Mon Sep 17 00:00:00 2001 From: npty Date: Wed, 17 Jul 2024 10:04:51 +0700 Subject: [PATCH 14/22] chore: refactor cli --- sui/cli-utils.js | 9 +++++++++ sui/gas-service.js | 23 ++++++++++------------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/sui/cli-utils.js b/sui/cli-utils.js index 1ef5569b7..bb72d5f6f 100644 --- a/sui/cli-utils.js +++ b/sui/cli-utils.js @@ -51,7 +51,16 @@ const addExtendedOptions = (program, options = {}) => { return program; }; +const addBaseOptionsToCommands = (program, options = {}) => { + if (program.commands.length > 0) { + program.commands.forEach((command) => { + addBaseOptions(command, options); + }); + } +}; + module.exports = { addBaseOptions, addExtendedOptions, + addBaseOptionsToCommands, }; diff --git a/sui/gas-service.js b/sui/gas-service.js index 136c7fbd9..cbe980ac4 100644 --- a/sui/gas-service.js +++ b/sui/gas-service.js @@ -10,7 +10,7 @@ const { utils: { arrayify }, } = ethers; -const { addBaseOptions } = require('./cli-utils'); +const { addBaseOptionsToCommands } = require('./cli-utils'); const { getWallet, printWalletInfo, broadcast } = require('./sign-utils'); async function payGas(config, chain, args, options) { @@ -202,7 +202,7 @@ if (require.main === module) { program.name('gas-service').description('Interact with the gas service contract.'); - const payGasProgram = new Command() + const payGasCmd = new Command() .command('pay_gas ') .description('Pay gas for the new contract call.') .requiredOption('--channel ', 'Existing channel ID to initiate a cross-chain message over') @@ -212,7 +212,7 @@ if (require.main === module) { mainProcessor('pay_gas', options, [amount, destinationChain, destinationAddress, payload], processCommand); }); - const addGasProgram = new Command() + const addGasCmd = new Command() .command('add_gas ') .description('Add gas for the existing contract call.') .option('--refund_address ', 'Refund address.') @@ -221,7 +221,7 @@ if (require.main === module) { mainProcessor('add_gas', options, [messageId, amount], processCommand); }); - const collectGasProgram = new Command() + const collectGasCmd = new Command() .command('collect_gas ') .description('Collect gas from the gas service contract.') .option('--receiver ', 'Receiver address. Default is the sender address.') @@ -229,7 +229,7 @@ if (require.main === module) { mainProcessor('collect_gas', options, [amount], processCommand); }); - const refundProgram = new Command() + const refundCmd = new Command() .command('refund ') .description('Refund gas from the gas service contract.') .option('--receiver ', 'Receiver address. Default is the sender address.') @@ -237,15 +237,12 @@ if (require.main === module) { mainProcessor('refund', options, [messageId, amount], processCommand); }); - program.addCommand(payGasProgram); - program.addCommand(addGasProgram); - program.addCommand(collectGasProgram); - program.addCommand(refundProgram); + program.addCommand(payGasCmd); + program.addCommand(addGasCmd); + program.addCommand(collectGasCmd); + program.addCommand(refundCmd); - addBaseOptions(payGasProgram); - addBaseOptions(addGasProgram); - addBaseOptions(collectGasProgram); - addBaseOptions(refundProgram); + addBaseOptionsToCommands(program); program.parse(); } From 4d357e3a33eed0b81da9b13dcd779b803161e249 Mon Sep 17 00:00:00 2001 From: npty Date: Wed, 17 Jul 2024 10:18:41 +0700 Subject: [PATCH 15/22] chore: pass function into mainProcessor --- sui/gas-service.js | 57 ++++++++++++++++------------------------------ 1 file changed, 20 insertions(+), 37 deletions(-) diff --git a/sui/gas-service.js b/sui/gas-service.js index cbe980ac4..7f8b46669 100644 --- a/sui/gas-service.js +++ b/sui/gas-service.js @@ -35,7 +35,7 @@ async function payGas(config, chain, args, options) { tx.moveCall({ target: `${gasServicePackageId}::gas_service::pay_gas`, arguments: [ - tx.object(gasServiceConfig.objects.gas_service), + tx.object(gasServiceConfig.objects.GasService), coin, // Coin tx.pure.address(channel), // Channel address tx.pure(bcs.string().serialize(destinationChain).toBytes()), // Destination chain @@ -55,7 +55,7 @@ async function addGas(config, chain, args, options) { const [keypair, client] = getWallet(chain, options); const walletAddress = keypair.toSuiAddress(); - const gasServiceConfig = chain.contracts.axelar_gas_service; + const gasServiceConfig = chain.contracts.GasService; const gasServicePackageId = gasServiceConfig.address; const refundAddress = options.refund_address || walletAddress; @@ -71,7 +71,7 @@ async function addGas(config, chain, args, options) { tx.moveCall({ target: `${gasServicePackageId}::gas_service::add_gas`, arguments: [ - tx.object(gasServiceConfig.objects.gas_service), + tx.object(gasServiceConfig.objects.GasService), coin, // Coin tx.pure(bcs.string().serialize(messageId).toBytes()), // Message ID for the contract call tx.pure.address(refundAddress), // Refund address @@ -90,7 +90,7 @@ async function collectGas(config, chain, args, options) { const gasServiceConfig = chain.contracts.axelar_gas_service; const gasServicePackageId = gasServiceConfig.address; - const gasServiceObjectId = gasServiceConfig.objects.gas_service; + const gasServiceObjectId = gasServiceConfig.objects.GasService; const [amount] = args; const receiver = options.receiver || walletAddress; @@ -111,8 +111,8 @@ async function collectGas(config, chain, args, options) { tx.moveCall({ target: `${gasServicePackageId}::gas_service::collect_gas`, arguments: [ - tx.object(gasServiceConfig.objects.gas_service), - tx.object(gasServiceConfig.objects.gas_collector_cap), + tx.object(gasServiceConfig.objects.GasService), + tx.object(gasServiceConfig.objects.GasCollectorCap), tx.pure.address(receiver), // Receiver address tx.pure.u64(atomicAmount), // Amount ], @@ -129,7 +129,7 @@ async function refund(config, chain, args, options) { const gasServiceConfig = chain.contracts.axelar_gas_service; const gasServicePackageId = gasServiceConfig.address; - const gasServiceObjectId = gasServiceConfig.objects.gas_service; + const gasServiceObjectId = gasServiceConfig.objects.GasService; const [messageId, amount] = args; const receiver = options.receiver || walletAddress; @@ -149,8 +149,8 @@ async function refund(config, chain, args, options) { tx.moveCall({ target: `${gasServicePackageId}::gas_service::refund`, arguments: [ - tx.object(gasServiceConfig.objects.gas_service), - tx.object(gasServiceConfig.objects.gas_collector_cap), + tx.object(gasServiceConfig.objects.GasService), + tx.object(gasServiceConfig.objects.GasCollectorCap), tx.pure(bcs.string().serialize(messageId).toBytes()), // Message ID for the contract call tx.pure.address(receiver), // Refund address tx.pure.u64(atomicAmount), // Amount @@ -167,31 +167,14 @@ async function processCommand(command, config, chain, args, options) { await printWalletInfo(keypair, client, chain, options); - if (!chain.contracts.axelar_gas_service) { - throw new Error('Axelar gas service contract not found'); + if (!chain.contracts.GasService) { + throw new Error('GasService contract not found'); } - switch (command) { - case 'pay_gas': - printInfo('Action', 'Pay gas'); - await payGas(config, chain, args, options); - break; - case 'add_gas': - printInfo('Action', 'Add gas'); - await addGas(config, chain, args, options); - break; - case 'collect_gas': - printInfo('Action', 'Collect gas'); - await collectGas(config, chain, args, options); - break; - case 'refund': - printInfo('Action', 'Refund gas'); - await refund(config, chain, args, options); - break; - } + await command(config, chain, args, options); } -async function mainProcessor(command, options, args, processor) { +async function mainProcessor(options, args, processor, command) { const config = loadSuiConfig(options.env); await processor(command, config, config.sui, args, options); saveConfig(config, options.env); @@ -203,30 +186,30 @@ if (require.main === module) { program.name('gas-service').description('Interact with the gas service contract.'); const payGasCmd = new Command() - .command('pay_gas ') + .command('payGas ') .description('Pay gas for the new contract call.') .requiredOption('--channel ', 'Existing channel ID to initiate a cross-chain message over') .option('--refund_address ', 'Refund address. Default is the sender address.') .option('--params ', 'Params. Default is empty.') .action((amount, destinationChain, destinationAddress, payload, options) => { - mainProcessor('pay_gas', options, [amount, destinationChain, destinationAddress, payload], processCommand); + mainProcessor(options, [amount, destinationChain, destinationAddress, payload], processCommand, payGas); }); const addGasCmd = new Command() - .command('add_gas ') + .command('addGas ') .description('Add gas for the existing contract call.') .option('--refund_address ', 'Refund address.') .option('--params ', 'Params. Default is empty.') .action((messageId, amount, options) => { - mainProcessor('add_gas', options, [messageId, amount], processCommand); + mainProcessor(options, [messageId, amount], processCommand, addGas); }); const collectGasCmd = new Command() - .command('collect_gas ') + .command('collectGas ') .description('Collect gas from the gas service contract.') .option('--receiver ', 'Receiver address. Default is the sender address.') .action((amount, options) => { - mainProcessor('collect_gas', options, [amount], processCommand); + mainProcessor(options, [amount], processCommand, collectGas); }); const refundCmd = new Command() @@ -234,7 +217,7 @@ if (require.main === module) { .description('Refund gas from the gas service contract.') .option('--receiver ', 'Receiver address. Default is the sender address.') .action((messageId, amount, options) => { - mainProcessor('refund', options, [messageId, amount], processCommand); + mainProcessor(options, [messageId, amount], processCommand, refund); }); program.addCommand(payGasCmd); From 23ec11a70742a9c5e61e9d63a8dbe84a0195cc2d Mon Sep 17 00:00:00 2001 From: npty Date: Wed, 17 Jul 2024 10:33:24 +0700 Subject: [PATCH 16/22] chore: reduce duplication --- sui/gas-service.js | 43 ++++++++++++++++--------------------------- 1 file changed, 16 insertions(+), 27 deletions(-) diff --git a/sui/gas-service.js b/sui/gas-service.js index 7f8b46669..2ec8d3bfc 100644 --- a/sui/gas-service.js +++ b/sui/gas-service.js @@ -13,19 +13,15 @@ const { const { addBaseOptionsToCommands } = require('./cli-utils'); const { getWallet, printWalletInfo, broadcast } = require('./sign-utils'); -async function payGas(config, chain, args, options) { - const [keypair, client] = getWallet(chain, options); +async function payGas(keypair, client, gasServiceConfig, args, options) { const walletAddress = keypair.toSuiAddress(); - const gasServiceConfig = chain.contracts.axelar_gas_service; const gasServicePackageId = gasServiceConfig.address; - const channel = options.channel; - - const refundAddress = options.refund_address || walletAddress; + const refundAddress = options.refundAddress || walletAddress; const params = options.params || '0x'; - const [amount, destinationChain, destinationAddress, payload] = args; + const [amount, destinationChain, destinationAddress, channelId, payload] = args; const atomicAmount = getAtomicAmount(amount); @@ -37,7 +33,7 @@ async function payGas(config, chain, args, options) { arguments: [ tx.object(gasServiceConfig.objects.GasService), coin, // Coin - tx.pure.address(channel), // Channel address + tx.pure.address(channelId), // Channel address tx.pure(bcs.string().serialize(destinationChain).toBytes()), // Destination chain tx.pure(bcs.string().serialize(destinationAddress).toBytes()), // Destination address tx.pure(bcs.vector(bcs.u8()).serialize(arrayify(payload)).toBytes()), // Payload @@ -51,14 +47,12 @@ async function payGas(config, chain, args, options) { printInfo('Gas paid', receipt.digest); } -async function addGas(config, chain, args, options) { - const [keypair, client] = getWallet(chain, options); +async function addGas(keypair, client, gasServiceConfig, args, options) { const walletAddress = keypair.toSuiAddress(); - const gasServiceConfig = chain.contracts.GasService; const gasServicePackageId = gasServiceConfig.address; - const refundAddress = options.refund_address || walletAddress; + const refundAddress = options.refundAddress || walletAddress; const params = options.params || '0x'; const [messageId, amount] = args; @@ -84,11 +78,9 @@ async function addGas(config, chain, args, options) { printInfo('Gas added', receipt.digest); } -async function collectGas(config, chain, args, options) { - const [keypair, client] = getWallet(chain, options); +async function collectGas(keypair, client, gasServiceConfig, args, options) { const walletAddress = keypair.toSuiAddress(); - const gasServiceConfig = chain.contracts.axelar_gas_service; const gasServicePackageId = gasServiceConfig.address; const gasServiceObjectId = gasServiceConfig.objects.GasService; @@ -123,11 +115,9 @@ async function collectGas(config, chain, args, options) { printInfo('Gas collected', receipt.digest); } -async function refund(config, chain, args, options) { - const [keypair, client] = getWallet(chain, options); +async function refund(keypair, client, gasServiceConfig, args, options) { const walletAddress = keypair.toSuiAddress(); - const gasServiceConfig = chain.contracts.axelar_gas_service; const gasServicePackageId = gasServiceConfig.address; const gasServiceObjectId = gasServiceConfig.objects.GasService; @@ -162,7 +152,7 @@ async function refund(config, chain, args, options) { printInfo('Gas refunded', receipt.digest); } -async function processCommand(command, config, chain, args, options) { +async function processCommand(command, chain, args, options) { const [keypair, client] = getWallet(chain, options); await printWalletInfo(keypair, client, chain, options); @@ -171,12 +161,12 @@ async function processCommand(command, config, chain, args, options) { throw new Error('GasService contract not found'); } - await command(config, chain, args, options); + await command(keypair, client, chain.contracts.GasService, args, options); } async function mainProcessor(options, args, processor, command) { const config = loadSuiConfig(options.env); - await processor(command, config, config.sui, args, options); + await processor(command, config.sui, args, options); saveConfig(config, options.env); } @@ -186,19 +176,18 @@ if (require.main === module) { program.name('gas-service').description('Interact with the gas service contract.'); const payGasCmd = new Command() - .command('payGas ') + .command('payGas ') .description('Pay gas for the new contract call.') - .requiredOption('--channel ', 'Existing channel ID to initiate a cross-chain message over') - .option('--refund_address ', 'Refund address. Default is the sender address.') + .option('--refundAddress ', 'Refund address. Default is the sender address.') .option('--params ', 'Params. Default is empty.') - .action((amount, destinationChain, destinationAddress, payload, options) => { - mainProcessor(options, [amount, destinationChain, destinationAddress, payload], processCommand, payGas); + .action((amount, destinationChain, destinationAddress, channelId, payload, options) => { + mainProcessor(options, [amount, destinationChain, destinationAddress, channelId, payload], processCommand, payGas); }); const addGasCmd = new Command() .command('addGas ') .description('Add gas for the existing contract call.') - .option('--refund_address ', 'Refund address.') + .option('--refundAddress ', 'Refund address.') .option('--params ', 'Params. Default is empty.') .action((messageId, amount, options) => { mainProcessor(options, [messageId, amount], processCommand, addGas); From 5f9be84aee3f180a7f83c2bc854e8e1431bb36f0 Mon Sep 17 00:00:00 2001 From: npty Date: Wed, 17 Jul 2024 13:23:33 +0700 Subject: [PATCH 17/22] chore: add example gas payment script --- sui/README.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/sui/README.md b/sui/README.md index 87897d3c4..94bc22133 100644 --- a/sui/README.md +++ b/sui/README.md @@ -51,7 +51,7 @@ node sui/faucet.js Deploy the gateway package: -- By querying the signer set from the Amplifier contract (this only works if Amplifier contracts have been setup): +- By querying the signer set from the Amplifier contract (this only works if Amplifier contracts have been setup): ```bash node sui/deploy-gateway.js @@ -59,13 +59,13 @@ node sui/deploy-gateway.js 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. +- For testing convenience, you can use the secp256k1 wallet as the signer set for the gateway. ```bash node sui/deploy-gateway.js --signers wallet --nonce test ``` -- You can also provide a JSON object with a full signer set: +- You can also provide a JSON object with a full signer set: ```bash node sui/deploy-gateway.js -e testnet --signers '{"signers": [{"pubkey": "0x020194ead85b350d90472117e6122cf1764d93bf17d6de4b51b03d19afc4d6302b", "weight": 1}], "threshold": 1, "nonce": "0x0000000000000000000000000000000000000000000000000000000000000000"}' @@ -89,6 +89,14 @@ Call Contract: node sui/gateway.js call-contract ethereum 0xba76c6980428A0b10CFC5d8ccb61949677A61233 0x1234 ``` +Pay for gas: + +The syntax is `node sui/gas-service.js payGas ` + +```bash +node sui/gas-service.js payGas 0.1 ethereum 0x6f24A47Fc8AE5441Eb47EFfC3665e70e69Ac3F05 0xba76c6980428A0b10CFC5d8ccb61949677A61233 0x1234 +``` + Approve messages: If the gateway was deployed using the wallet, you can submit a message approval with it From 94a40388d51a47cdc3f51048da6ba5bf51eff014 Mon Sep 17 00:00:00 2001 From: npty Date: Wed, 17 Jul 2024 13:24:34 +0700 Subject: [PATCH 18/22] chore: fix lint --- sui/cli-utils.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sui/cli-utils.js b/sui/cli-utils.js index bb72d5f6f..d3e5a37a2 100644 --- a/sui/cli-utils.js +++ b/sui/cli-utils.js @@ -52,11 +52,11 @@ const addExtendedOptions = (program, options = {}) => { }; const addBaseOptionsToCommands = (program, options = {}) => { - if (program.commands.length > 0) { - program.commands.forEach((command) => { - addBaseOptions(command, options); - }); - } + if (program.commands.length > 0) { + program.commands.forEach((command) => { + addBaseOptions(command, options); + }); + } }; module.exports = { From b3fd2dd654a32551f726a77f2f0f792ef43fd7dd Mon Sep 17 00:00:00 2001 From: npty Date: Wed, 17 Jul 2024 15:18:08 +0700 Subject: [PATCH 19/22] chore: atomicAmount to unitAmount --- sui/amount-utils.js | 4 ++-- sui/gas-service.js | 30 +++++++++++++++--------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/sui/amount-utils.js b/sui/amount-utils.js index c0d93f762..8fa5bfee1 100644 --- a/sui/amount-utils.js +++ b/sui/amount-utils.js @@ -1,7 +1,7 @@ const { ethers } = require('ethers'); // Convert formatted amount to atomic units (e.g. 1000000000). Default decimals is 9 for SUI -function getAtomicAmount(amount, decimals = 9) { +function getUnitAmount(amount, decimals = 9) { return ethers.utils.parseUnits(amount, decimals).toBigInt(); } @@ -11,6 +11,6 @@ function getFormattedAmount(amount, decimals = 9) { } module.exports = { - getAtomicAmount, + getUnitAmount, getFormattedAmount, }; diff --git a/sui/gas-service.js b/sui/gas-service.js index 2ec8d3bfc..d6072578d 100644 --- a/sui/gas-service.js +++ b/sui/gas-service.js @@ -5,12 +5,12 @@ const { bcs } = require('@mysten/sui.js/bcs'); const { gasServiceStruct } = require('./types-utils'); const { loadSuiConfig, getBcsBytesByObjectId } = require('./utils'); const { ethers } = require('hardhat'); -const { getAtomicAmount, getFormattedAmount } = require('./amount-utils'); +const { getUnitAmount, getFormattedAmount } = require('./amount-utils'); const { utils: { arrayify }, } = ethers; -const { addBaseOptionsToCommands } = require('./cli-utils'); +const { addOptionsToCommands, addBaseOptions } = require('./cli-utils'); const { getWallet, printWalletInfo, broadcast } = require('./sign-utils'); async function payGas(keypair, client, gasServiceConfig, args, options) { @@ -23,10 +23,10 @@ async function payGas(keypair, client, gasServiceConfig, args, options) { const [amount, destinationChain, destinationAddress, channelId, payload] = args; - const atomicAmount = getAtomicAmount(amount); + const unitAmount = getUnitAmount(amount); const tx = new TransactionBlock(); - const [coin] = tx.splitCoins(tx.gas, [atomicAmount]); + const [coin] = tx.splitCoins(tx.gas, [unitAmount]); tx.moveCall({ target: `${gasServicePackageId}::gas_service::pay_gas`, @@ -57,10 +57,10 @@ async function addGas(keypair, client, gasServiceConfig, args, options) { const [messageId, amount] = args; - const atomicAmount = getAtomicAmount(amount); + const unitAmount = getUnitAmount(amount); const tx = new TransactionBlock(); - const [coin] = tx.splitCoins(tx.gas, [atomicAmount]); + const [coin] = tx.splitCoins(tx.gas, [unitAmount]); tx.moveCall({ target: `${gasServicePackageId}::gas_service::add_gas`, @@ -87,14 +87,14 @@ async function collectGas(keypair, client, gasServiceConfig, args, options) { const [amount] = args; const receiver = options.receiver || walletAddress; - const atomicAmount = getAtomicAmount(amount); + const unitAmount = getUnitAmount(amount); const bytes = await getBcsBytesByObjectId(client, gasServiceObjectId); const { balance: gasServiceBalance } = gasServiceStruct.parse(bytes); // Check if the gas service balance is sufficient - if (gasServiceBalance < atomicAmount) { - printError('Insufficient gas service balance', `${getFormattedAmount(gasServiceBalance)} < ${getFormattedAmount(atomicAmount)}`); + if (gasServiceBalance < unitAmount) { + printError('Insufficient gas service balance', `${getFormattedAmount(gasServiceBalance)} < ${getFormattedAmount(unitAmount)}`); return; } @@ -106,7 +106,7 @@ async function collectGas(keypair, client, gasServiceConfig, args, options) { tx.object(gasServiceConfig.objects.GasService), tx.object(gasServiceConfig.objects.GasCollectorCap), tx.pure.address(receiver), // Receiver address - tx.pure.u64(atomicAmount), // Amount + tx.pure.u64(unitAmount), // Amount ], }); @@ -124,14 +124,14 @@ async function refund(keypair, client, gasServiceConfig, args, options) { const [messageId, amount] = args; const receiver = options.receiver || walletAddress; - const atomicAmount = getAtomicAmount(amount); + const unitAmount = getUnitAmount(amount); const bytes = await getBcsBytesByObjectId(client, gasServiceObjectId); const { balance: gasServiceBalance } = gasServiceStruct.parse(bytes); // Check if the gas service balance is sufficient - if (gasServiceBalance < atomicAmount) { - printError('Insufficient gas service balance', `${getFormattedAmount(gasServiceBalance)} < ${getFormattedAmount(atomicAmount)}`); + if (gasServiceBalance < unitAmount) { + printError('Insufficient gas service balance', `${getFormattedAmount(gasServiceBalance)} < ${getFormattedAmount(unitAmount)}`); return; } @@ -143,7 +143,7 @@ async function refund(keypair, client, gasServiceConfig, args, options) { tx.object(gasServiceConfig.objects.GasCollectorCap), tx.pure(bcs.string().serialize(messageId).toBytes()), // Message ID for the contract call tx.pure.address(receiver), // Refund address - tx.pure.u64(atomicAmount), // Amount + tx.pure.u64(unitAmount), // Amount ], }); @@ -214,7 +214,7 @@ if (require.main === module) { program.addCommand(collectGasCmd); program.addCommand(refundCmd); - addBaseOptionsToCommands(program); + addOptionsToCommands(program, addBaseOptions); program.parse(); } From 5f5ad307c71af578c9f2489f5663ded5077bba33 Mon Sep 17 00:00:00 2001 From: npty Date: Wed, 17 Jul 2024 15:19:14 +0700 Subject: [PATCH 20/22] chore: make add options to multiple commands more generic --- sui/cli-utils.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/sui/cli-utils.js b/sui/cli-utils.js index d3e5a37a2..d7b2cbe10 100644 --- a/sui/cli-utils.js +++ b/sui/cli-utils.js @@ -51,16 +51,20 @@ const addExtendedOptions = (program, options = {}) => { return program; }; -const addBaseOptionsToCommands = (program, options = {}) => { +// `optionMethod` is a method such as `addBaseOptions` +// `options` is an option object for optionMethod +const addOptionsToCommands = (program, optionMethod, options) => { if (program.commands.length > 0) { program.commands.forEach((command) => { - addBaseOptions(command, options); + optionMethod(command, options); }); } + + optionMethod(program, options); }; module.exports = { addBaseOptions, addExtendedOptions, - addBaseOptionsToCommands, + addOptionsToCommands, }; From 46088f6b107f6bd419ba85f544e2335e6873fdf3 Mon Sep 17 00:00:00 2001 From: npty Date: Thu, 18 Jul 2024 16:10:20 +0700 Subject: [PATCH 21/22] chore: using custom processing --- sui/README.md | 6 +++--- sui/cli-utils.js | 9 ++++++++ sui/gas-service.js | 51 +++++++++++++++++++++++----------------------- 3 files changed, 37 insertions(+), 29 deletions(-) diff --git a/sui/README.md b/sui/README.md index 94bc22133..4fb3e275f 100644 --- a/sui/README.md +++ b/sui/README.md @@ -91,10 +91,10 @@ node sui/gateway.js call-contract ethereum 0xba76c6980428A0b10CFC5d8ccb61949677A Pay for gas: -The syntax is `node sui/gas-service.js payGas ` +The syntax is `node sui/gas-service.js payGas --amount ` ```bash -node sui/gas-service.js payGas 0.1 ethereum 0x6f24A47Fc8AE5441Eb47EFfC3665e70e69Ac3F05 0xba76c6980428A0b10CFC5d8ccb61949677A61233 0x1234 +node sui/gas-service.js payGas --amount 0.1 ethereum 0x6f24A47Fc8AE5441Eb47EFfC3665e70e69Ac3F05 0xba76c6980428A0b10CFC5d8ccb61949677A61233 0x1234 ``` Approve messages: @@ -197,7 +197,7 @@ example for adding multisig info to chains config: "publicKey": "AIqrCb324p6Qd4srkqCzn9NJHS7W17tA7r3t7Ur6aYN", "weight": 1, "schemeType": "ed25519" - }, + }, . . . diff --git a/sui/cli-utils.js b/sui/cli-utils.js index d7b2cbe10..90c6109f6 100644 --- a/sui/cli-utils.js +++ b/sui/cli-utils.js @@ -3,6 +3,7 @@ require('dotenv').config(); const { Option } = require('commander'); +const { getUnitAmount } = require('./amount-utils'); const addBaseOptions = (program, options = {}) => { program.addOption( @@ -63,8 +64,16 @@ const addOptionsToCommands = (program, optionMethod, options) => { optionMethod(program, options); }; +// Custom option processing for amount. https://github.com/tj/commander.js?tab=readme-ov-file#custom-option-processing +// The user is expected to pass a full amount (e.g. 1.0), and this option parser will convert it to smallest units (e.g. 1000000000). +// Note that this function will use decimals of 9 for SUI. So, other tokens with different decimals will not work. +const parseSuiUnitAmount = (value, previous) => { + return getUnitAmount(value); +}; + module.exports = { addBaseOptions, addExtendedOptions, addOptionsToCommands, + parseSuiUnitAmount, }; diff --git a/sui/gas-service.js b/sui/gas-service.js index d6072578d..fec7fb5f1 100644 --- a/sui/gas-service.js +++ b/sui/gas-service.js @@ -5,12 +5,12 @@ const { bcs } = require('@mysten/sui.js/bcs'); const { gasServiceStruct } = require('./types-utils'); const { loadSuiConfig, getBcsBytesByObjectId } = require('./utils'); const { ethers } = require('hardhat'); -const { getUnitAmount, getFormattedAmount } = require('./amount-utils'); +const { getFormattedAmount } = require('./amount-utils'); const { utils: { arrayify }, } = ethers; -const { addOptionsToCommands, addBaseOptions } = require('./cli-utils'); +const { addOptionsToCommands, addBaseOptions, parseSuiUnitAmount } = require('./cli-utils'); const { getWallet, printWalletInfo, broadcast } = require('./sign-utils'); async function payGas(keypair, client, gasServiceConfig, args, options) { @@ -21,9 +21,8 @@ async function payGas(keypair, client, gasServiceConfig, args, options) { const refundAddress = options.refundAddress || walletAddress; const params = options.params || '0x'; - const [amount, destinationChain, destinationAddress, channelId, payload] = args; - - const unitAmount = getUnitAmount(amount); + const [destinationChain, destinationAddress, channelId, payload] = args; + const unitAmount = options.amount; const tx = new TransactionBlock(); const [coin] = tx.splitCoins(tx.gas, [unitAmount]); @@ -55,9 +54,8 @@ async function addGas(keypair, client, gasServiceConfig, args, options) { const refundAddress = options.refundAddress || walletAddress; const params = options.params || '0x'; - const [messageId, amount] = args; - - const unitAmount = getUnitAmount(amount); + const [messageId] = args; + const unitAmount = options.amount; const tx = new TransactionBlock(); const [coin] = tx.splitCoins(tx.gas, [unitAmount]); @@ -84,11 +82,9 @@ async function collectGas(keypair, client, gasServiceConfig, args, options) { const gasServicePackageId = gasServiceConfig.address; const gasServiceObjectId = gasServiceConfig.objects.GasService; - const [amount] = args; + const unitAmount = options.amount; const receiver = options.receiver || walletAddress; - const unitAmount = getUnitAmount(amount); - const bytes = await getBcsBytesByObjectId(client, gasServiceObjectId); const { balance: gasServiceBalance } = gasServiceStruct.parse(bytes); @@ -121,11 +117,10 @@ async function refund(keypair, client, gasServiceConfig, args, options) { const gasServicePackageId = gasServiceConfig.address; const gasServiceObjectId = gasServiceConfig.objects.GasService; - const [messageId, amount] = args; + const [messageId] = args; + const unitAmount = options.amount; const receiver = options.receiver || walletAddress; - const unitAmount = getUnitAmount(amount); - const bytes = await getBcsBytesByObjectId(client, gasServiceObjectId); const { balance: gasServiceBalance } = gasServiceStruct.parse(bytes); @@ -176,37 +171,41 @@ if (require.main === module) { program.name('gas-service').description('Interact with the gas service contract.'); const payGasCmd = new Command() - .command('payGas ') + .command('payGas ') .description('Pay gas for the new contract call.') .option('--refundAddress ', 'Refund address. Default is the sender address.') + .requiredOption('--amount ', 'Amount to pay gas', parseSuiUnitAmount) .option('--params ', 'Params. Default is empty.') - .action((amount, destinationChain, destinationAddress, channelId, payload, options) => { - mainProcessor(options, [amount, destinationChain, destinationAddress, channelId, payload], processCommand, payGas); + .action((destinationChain, destinationAddress, channelId, payload, options) => { + mainProcessor(options, [destinationChain, destinationAddress, channelId, payload], processCommand, payGas); }); const addGasCmd = new Command() - .command('addGas ') + .command('addGas ') .description('Add gas for the existing contract call.') .option('--refundAddress ', 'Refund address.') + .requiredOption('--amount ', 'Amount to add gas', parseSuiUnitAmount) .option('--params ', 'Params. Default is empty.') - .action((messageId, amount, options) => { - mainProcessor(options, [messageId, amount], processCommand, addGas); + .action((messageId, options) => { + mainProcessor(options, [messageId], processCommand, addGas); }); const collectGasCmd = new Command() - .command('collectGas ') + .command('collectGas') .description('Collect gas from the gas service contract.') .option('--receiver ', 'Receiver address. Default is the sender address.') - .action((amount, options) => { - mainProcessor(options, [amount], processCommand, collectGas); + .requiredOption('--amount ', 'Amount to collect gas', parseSuiUnitAmount) + .action((options) => { + mainProcessor(options, [], processCommand, collectGas); }); const refundCmd = new Command() - .command('refund ') + .command('refund ') .description('Refund gas from the gas service contract.') .option('--receiver ', 'Receiver address. Default is the sender address.') - .action((messageId, amount, options) => { - mainProcessor(options, [messageId, amount], processCommand, refund); + .requiredOption('--amount ', 'Amount to refund gas', parseSuiUnitAmount) + .action((messageId, options) => { + mainProcessor(options, [messageId], processCommand, refund); }); program.addCommand(payGasCmd); From f6cd4fdd979c777a1ce4179982c04c4dc23eaa7b Mon Sep 17 00:00:00 2001 From: npty Date: Thu, 18 Jul 2024 16:20:38 +0700 Subject: [PATCH 22/22] chore: add validation --- sui/cli-utils.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/sui/cli-utils.js b/sui/cli-utils.js index 90c6109f6..d36d97120 100644 --- a/sui/cli-utils.js +++ b/sui/cli-utils.js @@ -2,7 +2,7 @@ require('dotenv').config(); -const { Option } = require('commander'); +const { Option, InvalidArgumentError } = require('commander'); const { getUnitAmount } = require('./amount-utils'); const addBaseOptions = (program, options = {}) => { @@ -68,7 +68,11 @@ const addOptionsToCommands = (program, optionMethod, options) => { // The user is expected to pass a full amount (e.g. 1.0), and this option parser will convert it to smallest units (e.g. 1000000000). // Note that this function will use decimals of 9 for SUI. So, other tokens with different decimals will not work. const parseSuiUnitAmount = (value, previous) => { - return getUnitAmount(value); + try { + return getUnitAmount(value); + } catch (error) { + throw new InvalidArgumentError('Please use the correct format (e.g. 1.0)'); + } }; module.exports = {