Skip to content

Commit

Permalink
feat: execute contract script (#30)
Browse files Browse the repository at this point in the history
* add: script to run executeContract function

* use: log functions from util script

* run: lint and prettier

* chore: update grammar

Co-authored-by: Dean <deanamiel1@gmail.com>

* chore: update option description

Co-authored-by: Dean <deanamiel1@gmail.com>

* chore: improved lint warnnings

* refactor: use contractNames for artifact paths and address, remove env option for nativeValue and methodName

* rename: executeContract script to execute-contract

* remove: unused commented command option

* chore: remove lint error and run prettier

---------

Co-authored-by: Ayush Tiwari <ayush@axelar.network>
Co-authored-by: Dean <deanamiel1@gmail.com>
  • Loading branch information
3 people authored Sep 6, 2023
1 parent a2d15e8 commit 179dae3
Show file tree
Hide file tree
Showing 3 changed files with 258 additions and 2 deletions.
250 changes: 250 additions & 0 deletions evm/execute-contract.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
'use strict';

require('dotenv').config();

const {
Wallet,
Contract,
getDefaultProvider,
utils: { isAddress },
} = require('ethers');

const readlineSync = require('readline-sync');
const { Command, Option } = require('commander');
const { isNumber, isString, loadConfig, saveConfig, printObj, printLog, printError, printInfo } = require('./utils');

async function getCallData(methodName, targetContract, inputRecipient, inputAmount) {
var recipient, amount;

switch (methodName) {
case 'withdraw': {
if (inputRecipient) {
recipient = inputRecipient;
} else {
recipient = readlineSync.question('Enter the recipient address for withdrawal:');
}

if (inputAmount) {
amount = inputAmount;
} else {
amount = readlineSync.question('Enter the amount of tokens to withdraw:');
}

if (!isAddress(recipient)) {
throw new Error('Missing or incorrect recipient address for withdrawal from user input.');
}

if (!isNumber(amount)) {
throw new Error('Missing or incorrect withdrawal amount from user input.');
}

const callData = targetContract.interface.encodeFunctionData('withdraw', [recipient, amount]);
return callData;
}

case 'transfer': {
if (inputRecipient) {
recipient = inputRecipient;
} else {
recipient = readlineSync.question('Enter the recipient address for transfer:');
}

if (inputAmount) {
amount = inputAmount;
} else {
amount = readlineSync.question('Enter the amount of tokens to transfer:');
}

if (!isAddress(recipient)) {
throw new Error('Missing or incorrect recipient address for transfer from user input.');
}

if (!isNumber(amount)) {
throw new Error('Missing or incorrect transfer amount from user input.');
}

const callData = targetContract.interface.encodeFunctionData('transfer', [recipient, amount]);
return callData;
}

case 'approve': {
if (inputRecipient) {
recipient = inputRecipient;
} else {
recipient = readlineSync.question('Enter the recipient address for approval:');
}

if (inputAmount) {
amount = inputAmount;
} else {
amount = readlineSync.question('Enter the amount of tokens to approve:');
}

if (!isAddress(recipient)) {
throw new Error('Missing or incorrect recipient address for approval from user input.');
}

if (!isNumber(amount)) {
throw new Error('Missing or incorrect approval amount from user input.');
}

const callData = targetContract.interface.encodeFunctionData('approve', [recipient, amount]);
return callData;
}

default: {
throw new Error('The method name does not match any of the specified choices');
}
}
}

async function executeContract(options, chain, wallet) {
const {
callContractPath,
targetContractPath,
callContractName,
targetContractName,
targetContractAddress,
callData,
nativeValue,
methodName,
recipientAddress,
amount,
} = options;
const contracts = chain.contracts;

if (!contracts[callContractName]) {
throw new Error('Missing call contract address in the info file');
}

const callContractAddress = contracts[callContractName].address;

if (!isAddress(callContractAddress)) {
throw new Error('Missing call contract address in the address info.');
}

if (!isAddress(targetContractAddress)) {
throw new Error('Missing target address in the address info.');
}

if (!isString(methodName)) {
throw new Error('Missing method name from the user info.');
}

if (!isNumber(Number(nativeValue))) {
throw new Error('Missing native value from user info');
}

var contractPath =
callContractPath.charAt(0) === '@' ? callContractPath : callContractPath + callContractName + '.sol/' + callContractName + '.json';
printInfo('Call Contract path', contractPath);

const IContractExecutor = require(contractPath);
const contract = new Contract(callContractAddress, IContractExecutor.abi, wallet);
var finalCallData, finalNativeValue;

if (methodName === 'default') {
finalCallData = callData;
finalNativeValue = nativeValue;
} else {
contractPath =
targetContractPath.charAt(0) === '@'
? targetContractPath
: targetContractPath + targetContractName + '.sol/' + targetContractName + '.json';
printInfo('Target Contract path', contractPath);
const ITargetContract = require(contractPath);
const targetContract = new Contract(targetContractAddress, ITargetContract.abi, wallet);
finalCallData = await getCallData(methodName, targetContract, recipientAddress, Number(amount));
finalNativeValue = Number(0);
}

(async () => {
try {
const result = await contract.executeContract(targetContractAddress, finalCallData, finalNativeValue);
printLog('Function successfully called with return value as: ');
printObj(result);
} catch (error) {
printError('Calling executeContract method failed with Error: ', error);
}
})();
}

async function main(options) {
const config = loadConfig(options.env);
let chains = options.chainNames.split(',').map((str) => str.trim());

if (options.chainNames === 'all') {
chains = Object.keys(config.chains);
}

for (const chain of chains) {
if (config.chains[chain.toLowerCase()] === undefined) {
throw new Error(`Chain ${chain} is not defined in the info file`);
}
}

for (const chain of chains) {
const rpc = config.chains[chain.toLowerCase()].rpc;
const provider = getDefaultProvider(rpc);
const privateKey = options.privateKey;

if (!isString(privateKey)) {
throw new Error('Private Key value is not provided in the info file');
}

const wallet = new Wallet(privateKey, provider);
await executeContract(options, config.chains[chain.toLowerCase()], wallet);
saveConfig(config, options.env);
}
}

const program = new Command();

program.name('execute-contract').description('Executes a call to an external contract');

program.addOption(
new Option('-e, --env <env>', 'environment')
.choices(['local', 'devnet', 'stagenet', 'testnet', 'mainnet'])
.default('testnet')
.makeOptionMandatory(true)
.env('ENV'),
);
program.addOption(new Option('-n, --chainNames <chainNames>', 'chain names').makeOptionMandatory(true));
program.addOption(
new Option('-pc, --callContractPath <callContractPath>', 'artifact path for the called contract').makeOptionMandatory(true),
);
program.addOption(new Option('-cn, --callContractName <callContractName>', 'name of the called contract').makeOptionMandatory(true));
program.addOption(
new Option('-pt, --targetContractPath <targetContractPath>', 'artifact path for the target contract').makeOptionMandatory(true),
);
program.addOption(
new Option(
'-tn, --targetContractName <targetContractName>',
'name of the target contract that is called through executeContract',
).makeOptionMandatory(false),
);
program.addOption(
new Option('-ta, --targetContractAddress <targetContractAddress>', 'The address of the contract to be called')
.makeOptionMandatory(true)
.env('TARGET_ADDR'),
);
program.addOption(
new Option('-v, --nativeValue <nativeValue>', 'The amount of native token (e.g., Ether) to be sent along with the call').default(0),
);
program.addOption(
new Option('-m, --methodName <methodName>', 'method name to call in executeContract')
.choices(['withdraw', 'transfer', 'approve', 'default'])
.default('default'),
);
program.addOption(
new Option('-k, --privateKey <privateKey>', 'The private key of the caller').makeOptionMandatory(true).env('PRIVATE_KEY'),
);
program.addOption(new Option('-c, --callData <callData>', 'The calldata to be sent').env('CALL_DATA').default('0x'));
program.addOption(new Option('-ra, --recipientAddress <recipientAddress>', 'The recipient address for the tokens').env('RECIPIENT_ADDR'));
program.addOption(new Option('-am, --amount <amount>', 'The amount of tokens to transfer/withdraw/provide allowance etc.').env('AMOUNT'));

program.action((options) => {
main(options);
});

program.parse();
2 changes: 1 addition & 1 deletion evm/upgradable.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ const { Contract, ContractFactory } = require('ethers');
const { deployAndInitContractConstant, create3DeployAndInitContract } = require('@axelar-network/axelar-gmp-sdk-solidity');
const IUpgradable = require('@axelar-network/axelar-gmp-sdk-solidity/interfaces/IUpgradable.json');

const { verifyContract, deployCreate, getBytecodeHash, deployContract, printInfo, getDeployedAddress } = require('./utils');
const { verifyContract, deployCreate, getBytecodeHash, deployContract, printInfo } = require('./utils');

async function deployUpgradable(
wallet,
Expand Down
8 changes: 7 additions & 1 deletion evm/verify-contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,13 @@ async function verifyContracts(config, chain, options) {

console.log(`Verifying ${contractName} on ${chain.name} at address ${contract.address}...`);

await verifyContract(env, chain.name, contract.address, [chain.contracts.AxelarGateway.address, chain.contracts.AxelarGasService.address], verifyOptions);
await verifyContract(
env,
chain.name,
contract.address,
[chain.contracts.AxelarGateway.address, chain.contracts.AxelarGasService.address],
verifyOptions,
);
break;
}

Expand Down

0 comments on commit 179dae3

Please sign in to comment.