Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(stellar): refactor operators script and add missing commands #540

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
20 changes: 10 additions & 10 deletions stellar/deploy-contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,21 +113,21 @@ async function getInitializeArgs(config, chain, contractName, wallet, options) {
}

case 'interchain_token_service': {
const gatewayAddress = nativeToScVal(Address.fromString(chain?.contracts?.axelar_gateway?.address), { type: 'address' });
const gasServiceAddress = nativeToScVal(Address.fromString(chain?.contracts?.axelar_gas_service?.address), { type: 'address' });
const gatewayAddress = nativeToScVal(Address.fromString(chain.contracts?.axelar_gateway?.address), { type: 'address' });
const gasServiceAddress = nativeToScVal(Address.fromString(chain.contracts?.axelar_gas_service?.address), { type: 'address' });
const itsHubAddress = nativeToScVal(config.axelar?.contracts?.InterchainTokenService?.address, { type: 'string' });
const chainName = nativeToScVal('stellar', { type: 'string' });
const nativeTokenAddress = nativeToScVal(Address.fromString(chain?.tokenAddress), { type: 'address' });
const nativeTokenAddress = nativeToScVal(Address.fromString(chain.tokenAddress), { type: 'address' });

if (!chain?.contracts?.interchain_token?.wasmHash) {
if (!chain.contracts?.interchain_token?.wasmHash) {
throw new Error(`interchain_token contract's wasm hash does not exist.`);
}

const interchainTokenWasmHash = nativeToScVal(Buffer.from(chain?.contracts?.interchain_token?.wasmHash, 'hex'), {
const interchainTokenWasmHash = nativeToScVal(Buffer.from(chain.contracts?.interchain_token?.wasmHash, 'hex'), {
type: 'bytes',
});

const tokenManagerWasmHash = nativeToScVal(Buffer.from(chain?.contracts?.token_manager?.wasmHash, 'hex'), {
const tokenManagerWasmHash = nativeToScVal(Buffer.from(chain.contracts?.token_manager?.wasmHash, 'hex'), {
type: 'bytes',
});

Expand All @@ -148,7 +148,7 @@ async function getInitializeArgs(config, chain, contractName, wallet, options) {
return { owner };

case 'axelar_gas_service': {
const operatorsAddress = chain?.contracts?.axelar_operators?.address;
const operatorsAddress = chain.contracts?.axelar_operators?.address;
const operator = operatorsAddress ? nativeToScVal(Address.fromString(operatorsAddress), { type: 'address' }) : owner;

return { owner, operator };
Expand All @@ -159,11 +159,11 @@ async function getInitializeArgs(config, chain, contractName, wallet, options) {
}

case 'example': {
const gatewayAddress = nativeToScVal(Address.fromString(chain?.contracts?.axelar_gateway?.address), { type: 'address' });
const gasServiceAddress = nativeToScVal(Address.fromString(chain?.contracts?.axelar_gas_service?.address), { type: 'address' });
const gatewayAddress = nativeToScVal(Address.fromString(chain.contracts?.axelar_gateway?.address), { type: 'address' });
const gasServiceAddress = nativeToScVal(Address.fromString(chain.contracts?.axelar_gas_service?.address), { type: 'address' });
const itsAddress = options.useDummyItsAddress
? gatewayAddress
: nativeToScVal(chain?.contracts?.interchain_token_service?.address, { type: 'address' });
: nativeToScVal(chain.contracts?.interchain_token_service?.address, { type: 'address' });

return { gatewayAddress, gasServiceAddress, itsAddress };
}
Expand Down
2 changes: 1 addition & 1 deletion stellar/gateway.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ async function execute(wallet, _, chain, contractConfig, args, options) {

const messageApproved = await broadcast(isMessageApprovedOperation, wallet, chain, 'is_message_approved called', options);

if (!messageApproved._value) {
if (!messageApproved.value()) {
printWarn('Contract call not approved at the gateway');
return;
}
Expand Down
6 changes: 3 additions & 3 deletions stellar/its.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ async function deployRemoteInterchainToken(wallet, _, chain, contract, args, opt
const caller = addressToScVal(wallet.publicKey());
const [salt, destinationChain] = args;
const saltBytes32 = saltToBytes32(salt);
const gasTokenAddress = options.gasTokenAddress || chain?.tokenAddress;
const gasTokenAddress = options.gasTokenAddress || chain.tokenAddress;
const gasFeeAmount = options.gasFeeAmount;

const operation = contract.call(
Expand All @@ -81,7 +81,7 @@ async function deployRemoteCanonicalToken(wallet, _, chain, contract, args, opti
const spenderScVal = addressToScVal(wallet.publicKey());

const [tokenAddress, destinationChain] = args;
const gasTokenAddress = options.gasTokenAddress || chain?.tokenAddress;
const gasTokenAddress = options.gasTokenAddress || chain.tokenAddress;
const gasFeeAmount = options.gasFeeAmount;

const operation = contract.call(
Expand All @@ -99,7 +99,7 @@ async function interchainTransfer(wallet, _, chain, contract, args, options) {
const caller = addressToScVal(wallet.publicKey());
const [tokenId, destinationChain, destinationAddress, amount] = args;
const data = options.data === '' ? nativeToScVal(null, { type: 'null' }) : hexToScVal(options.data);
const gasTokenAddress = options.gasTokenAddress || chain?.tokenAddress;
const gasTokenAddress = options.gasTokenAddress || chain.tokenAddress;
const gasFeeAmount = options.gasFeeAmount;

const operation = contract.call(
Expand Down
227 changes: 137 additions & 90 deletions stellar/operators.js
Original file line number Diff line number Diff line change
@@ -1,127 +1,174 @@
const { Contract, Address, nativeToScVal } = require('@stellar/stellar-sdk');
const { Command, Option } = require('commander');
const { getWallet, broadcast, addBaseOptions } = require('./utils');
const { loadConfig, printInfo, parseArgs, validateParameters } = require('../evm/utils');
const { getChainConfig } = require('../common');
require('./cli-utils');

async function processCommand(options, _, chain) {
const wallet = await getWallet(chain, options);
'use strict';

const contract = new Contract(options.address || chain.contracts?.axelar_operators?.address);
const { Contract, nativeToScVal } = require('@stellar/stellar-sdk');
const { Command, Option } = require('commander');
const { getWallet, broadcast, addBaseOptions, addressToScVal, tokenToScVal, isValidAddress } = require('./utils');
const {
loadConfig,
printInfo,
printWarn,
parseArgs,
validateParameters,
saveConfig,
addOptionsToCommands,
getChainConfig,
} = require('../common');
const { prompt } = require('../common/utils');

async function isOperator(wallet, _, chain, contract, args, options) {
const [address] = args;
const operation = contract.call('is_operator', addressToScVal(address));
const result = await broadcast(operation, wallet, chain, 'is_operator called', options);

if (result.value()) {
printInfo(address + ' is an operator');
} else {
printWarn(address + ' is not an operator');
}
}

let operation;
async function addOperator(wallet, _, chain, contract, args, options) {
const [address] = args;
const operation = contract.call('add_operator', addressToScVal(address));
await broadcast(operation, wallet, chain, 'add_operator called', options);
}

switch (options.action) {
case 'is_operator': {
if (!options.args) {
throw new Error(`Missing --args operatorAddress the params.`);
}
async function removeOperator(wallet, _, chain, contract, args, options) {
const [address] = args;
const operation = contract.call('remove_operator', addressToScVal(address));
await broadcast(operation, wallet, chain, 'remove_operator called', options);
}

const operator = Address.fromString(options.args).toScVal();
operation = contract.call('is_operator', operator);
break;
}
async function collectFees(wallet, _, chain, contract, args, options) {
const operator = addressToScVal(wallet.publicKey());
const [receiver] = args;
const gasServiceAddress = chain.contracts?.axelar_gas_service?.address;
const gasTokenAddress = options.gasTokenAddress || chain.tokenAddress;
const gasFeeAmount = options.gasFeeAmount;

case 'add_operator': {
if (!options.args) {
throw new Error(`Missing --args operatorAddress the params.`);
}
validateParameters({
isNonEmptyString: { receiver, gasServiceAddress, gasTokenAddress },
ahramy marked this conversation as resolved.
Show resolved Hide resolved
isValidNumber: { gasFeeAmount },
});

const operator = Address.fromString(options.args).toScVal();
operation = contract.call('add_operator', operator);
break;
}
const target = addressToScVal(gasServiceAddress);
const method = nativeToScVal('collect_fees', { type: 'symbol' });
const params = nativeToScVal([addressToScVal(receiver), tokenToScVal(gasTokenAddress, gasFeeAmount)]);

case 'remove_operator': {
if (!options.args) {
throw new Error(`Missing --args operatorAddress the params.`);
}
const operation = contract.call('execute', operator, target, method, params);

const operator = Address.fromString(options.args).toScVal();
operation = contract.call('remove_operator', operator);
break;
}
await broadcast(operation, wallet, chain, 'collect_fees called', options);
}

case 'refund': {
const operator = Address.fromString(wallet.publicKey()).toScVal();
const gasService = options.target || chain.contracts?.axelar_gas_service?.address;
async function refund(wallet, _, chain, contract, args, options) {
const operator = addressToScVal(wallet.publicKey());
const [messageId, receiver] = args;
const gasServiceAddress = chain.contracts?.axelar_gas_service?.address;
const gasTokenAddress = options.gasTokenAddress || chain.tokenAddress;
const gasFeeAmount = options.gasFeeAmount;

if (!gasService) {
throw new Error(`Missing AxelarGasService address in the chain info.`);
}
validateParameters({
isNonEmptyString: { messageId, receiver, gasServiceAddress, gasTokenAddress },
isValidNumber: { gasFeeAmount },
});

const target = Address.fromString(gasService).toScVal();
const method = nativeToScVal('refund', { type: 'symbol' });
const [messageId, receiver, tokenAddress, tokenAmount] = parseArgs(options.args || '');
const target = addressToScVal(gasServiceAddress);
const method = nativeToScVal('refund', { type: 'symbol' });
const params = nativeToScVal([
nativeToScVal(messageId, { type: 'string' }),
addressToScVal(receiver),
tokenToScVal(gasTokenAddress, gasFeeAmount),
]);

validateParameters({
isNonEmptyString: { messageId, receiver, tokenAddress },
isValidNumber: { tokenAmount },
});
const operation = contract.call('execute', operator, target, method, params);

const args = nativeToScVal([
messageId,
Address.fromString(receiver),
{ address: Address.fromString(tokenAddress), amount: tokenAmount },
]);
await broadcast(operation, wallet, chain, 'refund called', options);
}

operation = contract.call('execute', operator, target, method, args);
break;
}
async function execute(wallet, _, chain, contract, args, options) {
const operator = addressToScVal(wallet.publicKey());
const [target, method, params] = args;

case 'execute': {
const operator = Address.fromString(wallet.publicKey()).toScVal();
validateParameters({
isNonEmptyString: { target, method, params },
});

if (!options.target) {
throw new Error(`Missing target address param.`);
}
const operation = contract.call(
'execute',
operator,
addressToScVal(target),
nativeToScVal(method, { type: 'symbol' }),
nativeToScVal(parseArgs(params || '')),
);

const target = Address.fromString(options.target).toScVal();
await broadcast(operation, wallet, chain, 'Executed', options);
}

if (!options.method) {
throw new Error(`Missing method name param.`);
}
async function mainProcessor(processor, args, options) {
const { yes } = options;
const config = loadConfig(options.env);
const chain = getChainConfig(config, options.chainName);
const wallet = await getWallet(chain, options);

const method = nativeToScVal(options.method, { type: 'symbol' });
if (prompt(`Proceed with action ${processor.name}`, yes)) {
return;
}

const args = nativeToScVal(parseArgs(options.args || ''));
const contractAddress = chain.contracts?.axelar_operators?.address;

operation = contract.call('execute', operator, target, method, args);
break;
}
validateParameters({
isNonEmptyString: { contractAddress },
});

default: {
throw new Error(`Unknown action: ${options.action}`);
}
if (!isValidAddress(contractAddress)) {
throw new Error('Invalid operators contract');
}
Comment on lines +123 to 125
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This defeats the point of validateParameters. Take a look at it's implementation. We want to add support for stellar address validation to it

validateParameters({
    isValidStellarAddress: { contractAddress },
});

Copy link
Member

@milapsheth milapsheth Feb 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could specialize into two functions as well
isStellarContract and isStellarAccount, since these addresses start with C and G respectively for stricter validation

Copy link
Contributor Author

@ahramy ahramy Feb 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can work on this on a separate PR since this task may be out of scope and could impact other scripts. I've created a ticket to track it: https://axelarnetwork.atlassian.net/browse/AXE-7492


const returnValue = await broadcast(operation, wallet, chain, `${options.action} performed`, options);
const contract = new Contract(contractAddress);

if (returnValue.value()) {
printInfo('Return value', returnValue.value());
}
await processor(wallet, config, chain, contract, args, options);

saveConfig(config, options.env);
}

if (require.main === module) {
const program = new Command();

program.name('operators').description('Operators contract management');

addBaseOptions(program, { address: true });
program.addOption(
new Option('--action <action>', 'operator contract action')
.choices(['is_operator', 'add_operator', 'remove_operator', 'refund', 'execute'])
.makeOptionMandatory(true),
);
program.addOption(new Option('--args <args>', 'arguments for the contract call'));
program.addOption(new Option('--target <target>', 'target contract for the execute call'));
program.addOption(new Option('--method <method>', 'target method for the execute call'));
program.command('is-operator <address>').action((address, options) => {
mainProcessor(isOperator, [address], options);
});

program.command('add-operator <address>').action((address, options) => {
mainProcessor(addOperator, [address], options);
});

program.action((options) => {
const config = loadConfig(options.env);
processCommand(options, config, getChainConfig(config, options.chainName));
program.command('remove-operator <addrsess>').action((address, options) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
program.command('remove-operator <addrsess>').action((address, options) => {
program.command('remove-operator <address>').action((address, options) => {

mainProcessor(removeOperator, [address], options);
});

program
.command('collect-fees <receiver>')
.addOption(new Option('--gas-token-address <gasTokenAddress>', 'gas token address (default: XLM)'))
.addOption(new Option('--gas-fee-amount <gasFeeAmount>', 'gas fee amount').default(0))
.action((receiver, options) => {
mainProcessor(collectFees, [receiver], options);
});

program
.command('refund <messageId> <receiver>')
.addOption(new Option('--gas-token-address <gasTokenAddress>', 'gas token address (default: XLM)'))
.addOption(new Option('--gas-fee-amount <gasFeeAmount>', 'gas fee amount').default(0))
.action((messageId, receiver, options) => {
mainProcessor(refund, [messageId, receiver], options);
});

program.command('execute <target> <method> <params>').action((target, method, params, options) => {
mainProcessor(execute, [target, method, params], options);
});

addOptionsToCommands(program, addBaseOptions);

program.parse();
}
11 changes: 11 additions & 0 deletions stellar/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,16 @@ function stellarAddressToBytes(address) {
return hexlify(Buffer.from(address, 'ascii'));
}

function isValidAddress(address) {
try {
// try conversion
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: implied with try block

Address.fromString(address);
return true;
} catch {
return false;
}
}

module.exports = {
stellarCmd,
ASSET_TYPE_NATIVE,
Expand All @@ -361,4 +371,5 @@ module.exports = {
tokenMetadataToScVal,
saltToBytes32,
stellarAddressToBytes,
isValidAddress,
};