diff --git a/.gitignore b/.gitignore index 50374163..ca36e556 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,5 @@ venv.bak/ typings multiversx_sdk_cli/tests/testdata-out + +.DS_store diff --git a/CLI.md b/CLI.md index dc4baf7a..1102b35f 100644 --- a/CLI.md +++ b/CLI.md @@ -23,7 +23,7 @@ See: COMMAND GROUPS: - {contract,tx,validator,ledger,wallet,validator-wallet,deps,config,localnet,data,staking-provider,dns,faucet,multisig,governance} + {contract,tx,validator,ledger,wallet,validator-wallet,deps,config,localnet,data,staking-provider,dns,faucet,multisig,governance,env} TOP-LEVEL OPTIONS: -h, --help show this help message and exit @@ -40,7 +40,7 @@ ledger Get Ledger App addresses and version wallet Create wallet, derive secret key from mnemonic, bech32 address helpers etc. validator-wallet Create a validator wallet, sign and verify messages and convert a validator wallet to a hex secret key. deps Manage dependencies or multiversx-sdk modules -config Configure multiversx-sdk (default values etc.) +config Configure MultiversX CLI (default values etc.) localnet Set up, start and control localnets data Data manipulation omnitool staking-provider Staking provider omnitool @@ -48,6 +48,7 @@ dns Operations related to the Domain Name Service faucet Get xEGLD on Devnet or Testnet multisig Deploy and interact with the Multisig Smart Contract governance Propose, vote and interact with the governance contract. +env Configure MultiversX CLI to use specific environment values. ``` ## Group **Contract** @@ -423,6 +424,7 @@ options: or Ledger devices (default: 0) --sender-username SENDER_USERNAME 🖄 the username of the sender --hrp HRP The hrp used to convert the address to its bech32 representation + --skip-confirmation, -y can be used to skip the confirmation prompt ``` ### Contract.ReproducibleBuild @@ -2832,7 +2834,7 @@ options: $ mxpy config --help usage: mxpy config COMMAND [-h] ... -Configure multiversx-sdk (default values etc.) +Configure MultiversX CLI (default values etc.) COMMANDS: {dump,get,set,delete,new,switch,list,reset} @@ -2843,12 +2845,12 @@ OPTIONS: ---------------- COMMANDS summary ---------------- -dump Dumps configuration. -get Gets a configuration value. -set Sets a configuration value. -delete Deletes a configuration value. -new Creates a new configuration. -switch Switch to a different config +dump Dumps the active configuration. +get Gets a configuration value from the active configuration. +set Sets a configuration value for the active configuration. +delete Deletes a configuration value from the active configuration. +new Creates a new configuration and sets it as the active configuration. +switch Switch to a different config. list List available configs reset Deletes the config file. Default config will be used. @@ -2860,7 +2862,7 @@ reset Deletes the config file. Default config will be u $ mxpy config dump --help usage: mxpy config dump [-h] ... -Dumps configuration. +Dumps the active configuration. options: -h, --help show this help message and exit @@ -2874,7 +2876,7 @@ options: $ mxpy config get --help usage: mxpy config get [-h] ... -Gets a configuration value. +Gets a configuration value from the active configuration. positional arguments: name the name of the configuration entry @@ -2890,7 +2892,7 @@ options: $ mxpy config set --help usage: mxpy config set [-h] ... -Sets a configuration value. +Sets a configuration value for the active configuration. positional arguments: name the name of the configuration entry @@ -2907,7 +2909,7 @@ options: $ mxpy config new --help usage: mxpy config new [-h] ... -Creates a new configuration. +Creates a new configuration and sets it as the active configuration. positional arguments: name the name of the configuration entry @@ -2924,7 +2926,7 @@ options: $ mxpy config switch --help usage: mxpy config switch [-h] ... -Switch to a different config +Switch to a different config. positional arguments: name the name of the configuration entry @@ -5893,3 +5895,154 @@ options: --proxy PROXY 🔗 the URL of the proxy ``` +## Group **Environment** + + +``` +$ mxpy env --help +usage: mxpy env COMMAND [-h] ... + +Configure MultiversX CLI to use specific environment values. + +COMMANDS: + {new,get,set,dump,delete,switch,list,remove,reset} + +OPTIONS: + -h, --help show this help message and exit + +---------------- +COMMANDS summary +---------------- +new Creates a new environment and sets it as the active environment. +get Gets an env value from the active environment. +set Sets an env value for the active environment. +dump Dumps the active environment. +delete Deletes an env value from the active environment. +switch Switch to a different environment. +list List available environments +remove Deletes an environment from the env file. Will switch to default env. +reset Deletes the environment file. Default env will be used. + +``` +### Environment.New + + +``` +$ mxpy env new --help +usage: mxpy env new [-h] ... + +Creates a new environment and sets it as the active environment. + +positional arguments: + name the name of the configuration entry + +options: + -h, --help show this help message and exit + --template TEMPLATE an environment from which to create the new environment + +``` +### Environment.Set + + +``` +$ mxpy env set --help +usage: mxpy env set [-h] ... + +Sets an env value for the active environment. + +positional arguments: + name the name of the configuration entry + value the new value + +options: + -h, --help show this help message and exit + +``` +### Environment.Get + + +``` +$ mxpy env get --help +usage: mxpy env get [-h] ... + +Gets an env value from the active environment. + +positional arguments: + name the name of the configuration entry + +options: + -h, --help show this help message and exit + +``` +### Environment.Dump + + +``` +$ mxpy env dump --help +usage: mxpy env dump [-h] ... + +Dumps the active environment. + +options: + -h, --help show this help message and exit + --default dumps the default environment instead of the active one. + +``` +### Environment.Switch + + +``` +$ mxpy env switch --help +usage: mxpy env switch [-h] ... + +Switch to a different environment. + +positional arguments: + name the name of the configuration entry + +options: + -h, --help show this help message and exit + +``` +### Environment.List + + +``` +$ mxpy env list --help +usage: mxpy env list [-h] ... + +List available environments + +options: + -h, --help show this help message and exit + +``` +### Environment.Remove + + +``` +$ mxpy env remove --help +usage: mxpy env remove [-h] ... + +Deletes an environment from the env file. Will switch to default env. + +positional arguments: + environment The environment to remove from env file. + +options: + -h, --help show this help message and exit + +``` +### Environment.Reset + + +``` +$ mxpy env reset --help +usage: mxpy env reset [-h] ... + +Deletes the environment file. Default env will be used. + +options: + -h, --help show this help message and exit + +``` diff --git a/CLI.md.sh b/CLI.md.sh index 24df2640..7e5b9ec5 100755 --- a/CLI.md.sh +++ b/CLI.md.sh @@ -183,6 +183,16 @@ generate() { command "Governance.GetVotingPower" "governance get-voting-power" command "Governance.GetConfig" "governance get-config" command "Governance.GetDelegatedVoteInfo" "governance get-delegated-vote-info" + + group "Environment" "env" + command "Environment.New" "env new" + command "Environment.Set" "env set" + command "Environment.Get" "env get" + command "Environment.Dump" "env dump" + command "Environment.Switch" "env switch" + command "Environment.List" "env list" + command "Environment.Remove" "env remove" + command "Environment.Reset" "env reset" } generate diff --git a/multiversx_sdk_cli/base_transactions_controller.py b/multiversx_sdk_cli/base_transactions_controller.py index 1a90c7f2..7c8203f8 100644 --- a/multiversx_sdk_cli/base_transactions_controller.py +++ b/multiversx_sdk_cli/base_transactions_controller.py @@ -10,7 +10,6 @@ StringValue, ) -from multiversx_sdk_cli.config import get_address_hrp from multiversx_sdk_cli.constants import ( ADDRESS_PREFIX, EXTRA_GAS_LIMIT_FOR_GUARDED_TRANSACTIONS, @@ -22,6 +21,7 @@ TRUE_STR_LOWER, ) from multiversx_sdk_cli.cosign_transaction import cosign_transaction +from multiversx_sdk_cli.env import get_address_hrp from multiversx_sdk_cli.errors import BadUserInput, TransactionSigningError from multiversx_sdk_cli.guardian_relayer_data import GuardianRelayerData from multiversx_sdk_cli.interfaces import IAccount diff --git a/multiversx_sdk_cli/cli.py b/multiversx_sdk_cli/cli.py index fabb067c..b41797bc 100644 --- a/multiversx_sdk_cli/cli.py +++ b/multiversx_sdk_cli/cli.py @@ -15,6 +15,7 @@ import multiversx_sdk_cli.cli_delegation import multiversx_sdk_cli.cli_deps import multiversx_sdk_cli.cli_dns +import multiversx_sdk_cli.cli_env import multiversx_sdk_cli.cli_faucet import multiversx_sdk_cli.cli_governance import multiversx_sdk_cli.cli_ledger @@ -26,6 +27,8 @@ import multiversx_sdk_cli.cli_wallet import multiversx_sdk_cli.version from multiversx_sdk_cli import config, errors, utils, ux +from multiversx_sdk_cli.cli_shared import set_proxy_from_config_if_not_provided +from multiversx_sdk_cli.env import get_address_hrp logger = logging.getLogger("cli") @@ -45,10 +48,11 @@ def main(cli_args: list[str] = sys.argv[1:]): def _do_main(cli_args: list[str]): utils.ensure_folder(config.SDK_PATH) - argv_with_config_args = config.add_config_args(cli_args) - parser = setup_parser(argv_with_config_args) + parser = setup_parser(cli_args) argcomplete.autocomplete(parser) - args = parser.parse_args(argv_with_config_args) + + _handle_verbose_argument(cli_args) + args = parser.parse_args(cli_args) if args.verbose: logging.basicConfig( @@ -65,7 +69,7 @@ def _do_main(cli_args: list[str]): ) verify_deprecated_entries_in_config_file() - default_hrp = config.get_address_hrp() + default_hrp = get_address_hrp() LibraryConfig.default_address_hrp = default_hrp if hasattr(args, "recall_nonce") and args.recall_nonce: @@ -74,6 +78,7 @@ def _do_main(cli_args: list[str]): if not hasattr(args, "func"): parser.print_help() else: + set_proxy_from_config_if_not_provided(args) args.func(args) @@ -126,6 +131,7 @@ def setup_parser(args: list[str]): commands.append(multiversx_sdk_cli.cli_faucet.setup_parser(args, subparsers)) commands.append(multiversx_sdk_cli.cli_multisig.setup_parser(args, subparsers)) commands.append(multiversx_sdk_cli.cli_governance.setup_parser(args, subparsers)) + commands.append(multiversx_sdk_cli.cli_env.setup_parser(subparsers)) parser.epilog = """ ---------------------- @@ -151,6 +157,13 @@ def verify_deprecated_entries_in_config_file(): ux.show_warning(message.rstrip("\n")) +def _handle_verbose_argument(args: list[str]): + verbose_arg = "--verbose" + if verbose_arg in args: + args.remove(verbose_arg) + args.insert(0, verbose_arg) + + if __name__ == "__main__": ret = main(sys.argv[1:]) sys.exit(ret) diff --git a/multiversx_sdk_cli/cli_config.py b/multiversx_sdk_cli/cli_config.py index 3d536af0..98981feb 100644 --- a/multiversx_sdk_cli/cli_config.py +++ b/multiversx_sdk_cli/cli_config.py @@ -10,10 +10,10 @@ def setup_parser(subparsers: Any) -> Any: - parser = cli_shared.add_group_subparser(subparsers, "config", "Configure multiversx-sdk (default values etc.)") + parser = cli_shared.add_group_subparser(subparsers, "config", "Configure MultiversX CLI (default values etc.)") subparsers = parser.add_subparsers() - sub = cli_shared.add_command_subparser(subparsers, "config", "dump", "Dumps configuration.") + sub = cli_shared.add_command_subparser(subparsers, "config", "dump", "Dumps the active configuration.") sub.add_argument( "--defaults", required=False, @@ -22,20 +22,28 @@ def setup_parser(subparsers: Any) -> Any: ) sub.set_defaults(func=dump) - sub = cli_shared.add_command_subparser(subparsers, "config", "get", "Gets a configuration value.") + sub = cli_shared.add_command_subparser( + subparsers, "config", "get", "Gets a configuration value from the active configuration." + ) _add_name_arg(sub) sub.set_defaults(func=get_value) - sub = cli_shared.add_command_subparser(subparsers, "config", "set", "Sets a configuration value.") + sub = cli_shared.add_command_subparser( + subparsers, "config", "set", "Sets a configuration value for the active configuration." + ) _add_name_arg(sub) sub.add_argument("value", help="the new value") sub.set_defaults(func=set_value) - sub = cli_shared.add_command_subparser(subparsers, "config", "delete", "Deletes a configuration value.") + sub = cli_shared.add_command_subparser( + subparsers, "config", "delete", "Deletes a configuration value from the active configuration." + ) _add_name_arg(sub) sub.set_defaults(func=delete_value) - sub = cli_shared.add_command_subparser(subparsers, "config", "new", "Creates a new configuration.") + sub = cli_shared.add_command_subparser( + subparsers, "config", "new", "Creates a new configuration and sets it as the active configuration." + ) _add_name_arg(sub) sub.add_argument( "--template", @@ -44,7 +52,7 @@ def setup_parser(subparsers: Any) -> Any: ) sub.set_defaults(func=new_config) - sub = cli_shared.add_command_subparser(subparsers, "config", "switch", "Switch to a different config") + sub = cli_shared.add_command_subparser(subparsers, "config", "switch", "Switch to a different config.") _add_name_arg(sub) sub.set_defaults(func=switch_config) diff --git a/multiversx_sdk_cli/cli_contracts.py b/multiversx_sdk_cli/cli_contracts.py index 9410de43..2f40d60c 100644 --- a/multiversx_sdk_cli/cli_contracts.py +++ b/multiversx_sdk_cli/cli_contracts.py @@ -30,6 +30,7 @@ from multiversx_sdk_cli.contract_verification import trigger_contract_verification from multiversx_sdk_cli.contracts import SmartContract from multiversx_sdk_cli.docker import is_docker_installed, run_docker +from multiversx_sdk_cli.env import MxpyEnv from multiversx_sdk_cli.errors import DockerMissingError from multiversx_sdk_cli.ux import show_warning @@ -311,7 +312,7 @@ def build(args: Any): The primary tool for building smart contracts is `sc-meta`. To install `sc-meta` check out the documentation: https://docs.multiversx.com/sdk-and-tools/troubleshooting/rust-setup. -After installing, use the `sc-meta all build` command. To lear more about `sc-meta`, check out this page: https://docs.multiversx.com/developers/meta/sc-meta-cli/#calling-build.""" +After installing, use the `sc-meta all build` command. To learn more about `sc-meta`, check out this page: https://docs.multiversx.com/developers/meta/sc-meta-cli/#calling-build.""" show_warning(message) @@ -329,7 +330,7 @@ def deploy(args: Any): args=args, ) - chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + chain_id = cli_shared.get_chain_id(args.proxy, args.chain) config = TransactionsFactoryConfig(chain_id) abi = Abi.load(Path(args.abi)) if args.abi else None @@ -359,7 +360,9 @@ def deploy(args: Any): contract_address = address_computer.compute_contract_address(deployer=sender.address, deployment_nonce=tx.nonce) logger.info("Contract address: %s", contract_address.to_bech32()) - utils.log_explorer_contract_address(args.chain, contract_address.to_bech32()) + + cli_config = MxpyEnv.from_active_env() + utils.log_explorer_contract_address(args.chain, contract_address.to_bech32(), cli_config.explorer_url) _send_or_simulate(tx, contract_address, args) @@ -378,7 +381,7 @@ def call(args: Any): args=args, ) - chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + chain_id = cli_shared.get_chain_id(args.proxy, args.chain) config = TransactionsFactoryConfig(chain_id) abi = Abi.load(Path(args.abi)) if args.abi else None @@ -424,7 +427,7 @@ def upgrade(args: Any): args=args, ) - chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + chain_id = cli_shared.get_chain_id(args.proxy, args.chain) config = TransactionsFactoryConfig(chain_id) abi = Abi.load(Path(args.abi)) if args.abi else None diff --git a/multiversx_sdk_cli/cli_delegation.py b/multiversx_sdk_cli/cli_delegation.py index ab7e27bb..994a40d3 100644 --- a/multiversx_sdk_cli/cli_delegation.py +++ b/multiversx_sdk_cli/cli_delegation.py @@ -395,7 +395,7 @@ def validate_arguments(args: Any): def _get_delegation_controller(args: Any): - chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + chain_id = cli_shared.get_chain_id(args.proxy, args.chain) config = TransactionsFactoryConfig(chain_id) delegation = DelegationOperations(config) return delegation diff --git a/multiversx_sdk_cli/cli_env.py b/multiversx_sdk_cli/cli_env.py new file mode 100644 index 00000000..4e3614a9 --- /dev/null +++ b/multiversx_sdk_cli/cli_env.py @@ -0,0 +1,149 @@ +import logging +import os +from typing import Any + +from multiversx_sdk_cli import cli_shared +from multiversx_sdk_cli.env import ( + create_new_env, + delete_env, + delete_value, + get_active_env, + get_defaults, + get_value, + read_env_file, + resolve_env_path, + set_active, + set_value, +) +from multiversx_sdk_cli.utils import dump_out_json +from multiversx_sdk_cli.ux import confirm_continuation + +logger = logging.getLogger("cli.env") + + +def setup_parser(subparsers: Any) -> Any: + parser = cli_shared.add_group_subparser( + subparsers, "env", "Configure MultiversX CLI to use specific environment values." + ) + subparsers = parser.add_subparsers() + + sub = cli_shared.add_command_subparser( + subparsers, "env", "new", "Creates a new environment and sets it as the active environment." + ) + _add_name_arg(sub) + sub.add_argument( + "--template", + required=False, + help="an environment from which to create the new environment", + ) + sub.set_defaults(func=new_env) + + sub = cli_shared.add_command_subparser(subparsers, "env", "get", "Gets an env value from the active environment.") + _add_name_arg(sub) + sub.set_defaults(func=get_env_value) + + sub = cli_shared.add_command_subparser(subparsers, "env", "set", "Sets an env value for the active environment.") + _add_name_arg(sub) + sub.add_argument("value", type=str, help="the new value") + sub.set_defaults(func=set_env_value) + + sub = cli_shared.add_command_subparser(subparsers, "env", "dump", "Dumps the active environment.") + sub.add_argument( + "--default", + required=False, + help="dumps the default environment instead of the active one.", + action="store_true", + ) + sub.set_defaults(func=dump) + + sub = cli_shared.add_command_subparser( + subparsers, "env", "delete", "Deletes an env value from the active environment." + ) + _add_name_arg(sub) + sub.set_defaults(func=delete_env_value) + + sub = cli_shared.add_command_subparser(subparsers, "env", "switch", "Switch to a different environment.") + _add_name_arg(sub) + sub.set_defaults(func=switch_env) + + sub = cli_shared.add_command_subparser(subparsers, "env", "list", "List available environments") + sub.set_defaults(func=list_envs) + + sub = cli_shared.add_command_subparser( + subparsers, + "env", + "remove", + "Deletes an environment from the env file. Will switch to default env.", + ) + sub.add_argument("environment", type=str, help="The environment to remove from env file.") + sub.set_defaults(func=remove_env_entry) + + sub = cli_shared.add_command_subparser( + subparsers, + "env", + "reset", + "Deletes the environment file. Default env will be used.", + ) + sub.set_defaults(func=delete_env_file) + + parser.epilog = cli_shared.build_group_epilog(subparsers) + return subparsers + + +def _add_name_arg(sub: Any): + sub.add_argument("name", type=str, help="the name of the configuration entry") + + +def dump(args: Any): + if args.default: + dump_out_json(get_defaults()) + else: + dump_out_json(get_active_env()) + + +def get_env_value(args: Any): + value = get_value(args.name) + print(value) + + +def set_env_value(args: Any): + set_value(args.name, args.value) + + +def delete_env_value(args: Any): + delete_value(args.name) + + +def new_env(args: Any): + create_new_env(name=args.name, template=args.template) + dump_out_json(get_active_env()) + + +def switch_env(args: Any): + set_active(args.name) + dump_out_json(get_active_env()) + + +def list_envs(args: Any): + data = read_env_file() + dump_out_json(data) + + +def remove_env_entry(args: Any): + envs_file = resolve_env_path() + if not envs_file.is_file(): + logger.info("Environment file not found. Aborting...") + return + + delete_env(args.environment) + + +def delete_env_file(args: Any): + envs_file = resolve_env_path() + if not envs_file.is_file(): + logger.info("Environment file not found. Aborting...") + return + + confirm_continuation(f"The file `{str(envs_file)}` will be deleted. Do you want to continue? (y/n)") + os.remove(envs_file) + logger.info("Successfully deleted the environment file.") diff --git a/multiversx_sdk_cli/cli_governance.py b/multiversx_sdk_cli/cli_governance.py index 3d04a7db..88798f3a 100644 --- a/multiversx_sdk_cli/cli_governance.py +++ b/multiversx_sdk_cli/cli_governance.py @@ -192,17 +192,21 @@ def _add_common_args(args: Any, sub: Any): cli_shared.add_wait_result_and_timeout_args(sub) -def create_proposal(args: Any): +def _ensure_args(args: Any): ensure_wallet_args_are_provided(args) validate_broadcast_args(args) validate_chain_id_args(args) + +def create_proposal(args: Any): + _ensure_args(args) + sender = cli_shared.prepare_sender(args) guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( sender=sender.address.to_bech32(), args=args, ) - chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + chain_id = cli_shared.get_chain_id(args.proxy, args.chain) gas_limit = args.gas_limit if args.gas_limit else 0 controller = GovernanceWrapper(TransactionsFactoryConfig(chain_id)) @@ -224,16 +228,14 @@ def create_proposal(args: Any): def vote(args: Any): - ensure_wallet_args_are_provided(args) - validate_broadcast_args(args) - validate_chain_id_args(args) + _ensure_args(args) sender = cli_shared.prepare_sender(args) guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( sender=sender.address.to_bech32(), args=args, ) - chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + chain_id = cli_shared.get_chain_id(args.proxy, args.chain) gas_limit = args.gas_limit if args.gas_limit else 0 controller = GovernanceWrapper(TransactionsFactoryConfig(chain_id)) @@ -253,16 +255,14 @@ def vote(args: Any): def close_proposal(args: Any): - ensure_wallet_args_are_provided(args) - validate_broadcast_args(args) - validate_chain_id_args(args) + _ensure_args(args) sender = cli_shared.prepare_sender(args) guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( sender=sender.address.to_bech32(), args=args, ) - chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + chain_id = cli_shared.get_chain_id(args.proxy, args.chain) gas_limit = args.gas_limit if args.gas_limit else 0 controller = GovernanceWrapper(TransactionsFactoryConfig(chain_id)) @@ -281,16 +281,14 @@ def close_proposal(args: Any): def clear_ended_proposals(args: Any): - ensure_wallet_args_are_provided(args) - validate_broadcast_args(args) - validate_chain_id_args(args) + _ensure_args(args) sender = cli_shared.prepare_sender(args) guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( sender=sender.address.to_bech32(), args=args, ) - chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + chain_id = cli_shared.get_chain_id(args.proxy, args.chain) gas_limit = args.gas_limit if args.gas_limit else 0 controller = GovernanceWrapper(TransactionsFactoryConfig(chain_id)) @@ -310,16 +308,14 @@ def clear_ended_proposals(args: Any): def claim_accumulated_fees(args: Any): - ensure_wallet_args_are_provided(args) - validate_broadcast_args(args) - validate_chain_id_args(args) + _ensure_args(args) sender = cli_shared.prepare_sender(args) guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( sender=sender.address.to_bech32(), args=args, ) - chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + chain_id = cli_shared.get_chain_id(args.proxy, args.chain) gas_limit = args.gas_limit if args.gas_limit else 0 controller = GovernanceWrapper(TransactionsFactoryConfig(chain_id)) @@ -337,16 +333,14 @@ def claim_accumulated_fees(args: Any): def change_config(args: Any): - ensure_wallet_args_are_provided(args) - validate_broadcast_args(args) - validate_chain_id_args(args) + _ensure_args(args) sender = cli_shared.prepare_sender(args) guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( sender=sender.address.to_bech32(), args=args, ) - chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + chain_id = cli_shared.get_chain_id(args.proxy, args.chain) gas_limit = args.gas_limit if args.gas_limit else 0 controller = GovernanceWrapper(TransactionsFactoryConfig(chain_id)) diff --git a/multiversx_sdk_cli/cli_multisig.py b/multiversx_sdk_cli/cli_multisig.py index 915ceaaf..4bf03fc3 100644 --- a/multiversx_sdk_cli/cli_multisig.py +++ b/multiversx_sdk_cli/cli_multisig.py @@ -654,14 +654,17 @@ def _add_common_args(args: Any, sub: Any, with_contract_arg: bool = True, with_r cli_shared.add_wait_result_and_timeout_args(sub) -def deploy(args: Any): - logger.debug("multisig.deploy") - +def _ensure_args(args: Any): validate_transaction_args(args) ensure_wallet_args_are_provided(args) validate_broadcast_args(args) validate_chain_id_args(args) + +def deploy(args: Any): + logger.debug("multisig.deploy") + _ensure_args(args) + sender = cli_shared.prepare_sender(args) guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( sender=sender.address.to_bech32(), @@ -672,7 +675,7 @@ def deploy(args: Any): quorum = args.quorum board_members = [Address.new_from_bech32(addr) for addr in args.board_members] - chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + chain_id = cli_shared.get_chain_id(args.proxy, args.chain) multisig = MultisigWrapper(TransactionsFactoryConfig(chain_id), abi) tx = multisig.prepare_deploy_transaction( @@ -702,10 +705,7 @@ def deploy(args: Any): def deposit(args: Any): - validate_transaction_args(args) - ensure_wallet_args_are_provided(args) - validate_broadcast_args(args) - validate_chain_id_args(args) + _ensure_args(args) sender = cli_shared.prepare_sender(args) guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( @@ -714,7 +714,7 @@ def deposit(args: Any): ) abi = Abi.load(Path(args.abi)) - chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + chain_id = cli_shared.get_chain_id(args.proxy, args.chain) multisig = MultisigWrapper(TransactionsFactoryConfig(chain_id), abi) contract = Address.new_from_bech32(args.contract) @@ -741,10 +741,7 @@ def deposit(args: Any): def discard_action(args: Any): - validate_transaction_args(args) - ensure_wallet_args_are_provided(args) - validate_broadcast_args(args) - validate_chain_id_args(args) + _ensure_args(args) sender = cli_shared.prepare_sender(args) guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( @@ -753,7 +750,7 @@ def discard_action(args: Any): ) abi = Abi.load(Path(args.abi)) - chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + chain_id = cli_shared.get_chain_id(args.proxy, args.chain) multisig = MultisigWrapper(TransactionsFactoryConfig(chain_id), abi) contract = Address.new_from_bech32(args.contract) @@ -775,10 +772,7 @@ def discard_action(args: Any): def discard_batch(args: Any): - validate_transaction_args(args) - ensure_wallet_args_are_provided(args) - validate_broadcast_args(args) - validate_chain_id_args(args) + _ensure_args(args) sender = cli_shared.prepare_sender(args) guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( @@ -787,7 +781,7 @@ def discard_batch(args: Any): ) abi = Abi.load(Path(args.abi)) - chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + chain_id = cli_shared.get_chain_id(args.proxy, args.chain) multisig = MultisigWrapper(TransactionsFactoryConfig(chain_id), abi) contract = Address.new_from_bech32(args.contract) @@ -809,10 +803,7 @@ def discard_batch(args: Any): def add_board_member(args: Any): - validate_transaction_args(args) - ensure_wallet_args_are_provided(args) - validate_broadcast_args(args) - validate_chain_id_args(args) + _ensure_args(args) sender = cli_shared.prepare_sender(args) guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( @@ -821,7 +812,7 @@ def add_board_member(args: Any): ) abi = Abi.load(Path(args.abi)) - chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + chain_id = cli_shared.get_chain_id(args.proxy, args.chain) multisig = MultisigWrapper(TransactionsFactoryConfig(chain_id), abi) contract = Address.new_from_bech32(args.contract) @@ -843,10 +834,7 @@ def add_board_member(args: Any): def add_proposer(args: Any): - validate_transaction_args(args) - ensure_wallet_args_are_provided(args) - validate_broadcast_args(args) - validate_chain_id_args(args) + _ensure_args(args) sender = cli_shared.prepare_sender(args) guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( @@ -855,7 +843,7 @@ def add_proposer(args: Any): ) abi = Abi.load(Path(args.abi)) - chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + chain_id = cli_shared.get_chain_id(args.proxy, args.chain) multisig = MultisigWrapper(TransactionsFactoryConfig(chain_id), abi) contract = Address.new_from_bech32(args.contract) @@ -877,10 +865,7 @@ def add_proposer(args: Any): def remove_user(args: Any): - validate_transaction_args(args) - ensure_wallet_args_are_provided(args) - validate_broadcast_args(args) - validate_chain_id_args(args) + _ensure_args(args) sender = cli_shared.prepare_sender(args) guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( @@ -889,7 +874,7 @@ def remove_user(args: Any): ) abi = Abi.load(Path(args.abi)) - chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + chain_id = cli_shared.get_chain_id(args.proxy, args.chain) multisig = MultisigWrapper(TransactionsFactoryConfig(chain_id), abi) contract = Address.new_from_bech32(args.contract) @@ -911,10 +896,7 @@ def remove_user(args: Any): def change_quorum(args: Any): - validate_transaction_args(args) - ensure_wallet_args_are_provided(args) - validate_broadcast_args(args) - validate_chain_id_args(args) + _ensure_args(args) sender = cli_shared.prepare_sender(args) guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( @@ -923,7 +905,7 @@ def change_quorum(args: Any): ) abi = Abi.load(Path(args.abi)) - chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + chain_id = cli_shared.get_chain_id(args.proxy, args.chain) multisig = MultisigWrapper(TransactionsFactoryConfig(chain_id), abi) contract = Address.new_from_bech32(args.contract) @@ -945,10 +927,7 @@ def change_quorum(args: Any): def transfer_and_execute(args: Any): - validate_transaction_args(args) - ensure_wallet_args_are_provided(args) - validate_broadcast_args(args) - validate_chain_id_args(args) + _ensure_args(args) sender = cli_shared.prepare_sender(args) guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( @@ -957,7 +936,7 @@ def transfer_and_execute(args: Any): ) abi = Abi.load(Path(args.abi)) - chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + chain_id = cli_shared.get_chain_id(args.proxy, args.chain) multisig = MultisigWrapper(TransactionsFactoryConfig(chain_id), abi) contract = Address.new_from_bech32(args.contract) @@ -990,10 +969,7 @@ def transfer_and_execute(args: Any): def transfer_and_execute_esdt(args: Any): - validate_transaction_args(args) - ensure_wallet_args_are_provided(args) - validate_broadcast_args(args) - validate_chain_id_args(args) + _ensure_args(args) if int(args.value) != 0: raise Exception("Native token transfer is not allowed for this command.") @@ -1008,7 +984,7 @@ def transfer_and_execute_esdt(args: Any): ) abi = Abi.load(Path(args.abi)) - chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + chain_id = cli_shared.get_chain_id(args.proxy, args.chain) multisig = MultisigWrapper(TransactionsFactoryConfig(chain_id), abi) contract = Address.new_from_bech32(args.contract) @@ -1042,10 +1018,7 @@ def transfer_and_execute_esdt(args: Any): def async_call(args: Any): - validate_transaction_args(args) - ensure_wallet_args_are_provided(args) - validate_broadcast_args(args) - validate_chain_id_args(args) + _ensure_args(args) sender = cli_shared.prepare_sender(args) guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( @@ -1054,7 +1027,7 @@ def async_call(args: Any): ) abi = Abi.load(Path(args.abi)) - chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + chain_id = cli_shared.get_chain_id(args.proxy, args.chain) multisig = MultisigWrapper(TransactionsFactoryConfig(chain_id), abi) contract = Address.new_from_bech32(args.contract) @@ -1092,10 +1065,7 @@ def async_call(args: Any): def deploy_from_source(args: Any): - validate_transaction_args(args) - ensure_wallet_args_are_provided(args) - validate_broadcast_args(args) - validate_chain_id_args(args) + _ensure_args(args) sender = cli_shared.prepare_sender(args) guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( @@ -1104,7 +1074,7 @@ def deploy_from_source(args: Any): ) abi = Abi.load(Path(args.abi)) - chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + chain_id = cli_shared.get_chain_id(args.proxy, args.chain) multisig = MultisigWrapper(TransactionsFactoryConfig(chain_id), abi) contract = Address.new_from_bech32(args.contract) @@ -1137,10 +1107,7 @@ def deploy_from_source(args: Any): def upgrade_from_source(args: Any): - validate_transaction_args(args) - ensure_wallet_args_are_provided(args) - validate_broadcast_args(args) - validate_chain_id_args(args) + _ensure_args(args) sender = cli_shared.prepare_sender(args) guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( @@ -1149,7 +1116,7 @@ def upgrade_from_source(args: Any): ) abi = Abi.load(Path(args.abi)) - chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + chain_id = cli_shared.get_chain_id(args.proxy, args.chain) multisig = MultisigWrapper(TransactionsFactoryConfig(chain_id), abi) contract = Address.new_from_bech32(args.contract) @@ -1184,10 +1151,7 @@ def upgrade_from_source(args: Any): def sign_action(args: Any): - validate_transaction_args(args) - ensure_wallet_args_are_provided(args) - validate_broadcast_args(args) - validate_chain_id_args(args) + _ensure_args(args) sender = cli_shared.prepare_sender(args) guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( @@ -1196,7 +1160,7 @@ def sign_action(args: Any): ) abi = Abi.load(Path(args.abi)) - chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + chain_id = cli_shared.get_chain_id(args.proxy, args.chain) multisig = MultisigWrapper(TransactionsFactoryConfig(chain_id), abi) contract = Address.new_from_bech32(args.contract) @@ -1218,10 +1182,7 @@ def sign_action(args: Any): def sign_batch(args: Any): - validate_transaction_args(args) - ensure_wallet_args_are_provided(args) - validate_broadcast_args(args) - validate_chain_id_args(args) + _ensure_args(args) sender = cli_shared.prepare_sender(args) guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( @@ -1230,7 +1191,7 @@ def sign_batch(args: Any): ) abi = Abi.load(Path(args.abi)) - chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + chain_id = cli_shared.get_chain_id(args.proxy, args.chain) multisig = MultisigWrapper(TransactionsFactoryConfig(chain_id), abi) contract = Address.new_from_bech32(args.contract) @@ -1252,10 +1213,7 @@ def sign_batch(args: Any): def sign_and_perform(args: Any): - validate_transaction_args(args) - ensure_wallet_args_are_provided(args) - validate_broadcast_args(args) - validate_chain_id_args(args) + _ensure_args(args) sender = cli_shared.prepare_sender(args) guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( @@ -1264,7 +1222,7 @@ def sign_and_perform(args: Any): ) abi = Abi.load(Path(args.abi)) - chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + chain_id = cli_shared.get_chain_id(args.proxy, args.chain) multisig = MultisigWrapper(TransactionsFactoryConfig(chain_id), abi) contract = Address.new_from_bech32(args.contract) @@ -1286,10 +1244,7 @@ def sign_and_perform(args: Any): def sign_batch_and_perform(args: Any): - validate_transaction_args(args) - ensure_wallet_args_are_provided(args) - validate_broadcast_args(args) - validate_chain_id_args(args) + _ensure_args(args) sender = cli_shared.prepare_sender(args) guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( @@ -1298,7 +1253,7 @@ def sign_batch_and_perform(args: Any): ) abi = Abi.load(Path(args.abi)) - chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + chain_id = cli_shared.get_chain_id(args.proxy, args.chain) multisig = MultisigWrapper(TransactionsFactoryConfig(chain_id), abi) contract = Address.new_from_bech32(args.contract) @@ -1320,10 +1275,7 @@ def sign_batch_and_perform(args: Any): def unsign_action(args: Any): - validate_transaction_args(args) - ensure_wallet_args_are_provided(args) - validate_broadcast_args(args) - validate_chain_id_args(args) + _ensure_args(args) sender = cli_shared.prepare_sender(args) guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( @@ -1332,7 +1284,7 @@ def unsign_action(args: Any): ) abi = Abi.load(Path(args.abi)) - chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + chain_id = cli_shared.get_chain_id(args.proxy, args.chain) multisig = MultisigWrapper(TransactionsFactoryConfig(chain_id), abi) contract = Address.new_from_bech32(args.contract) @@ -1354,10 +1306,7 @@ def unsign_action(args: Any): def unsign_batch(args: Any): - validate_transaction_args(args) - ensure_wallet_args_are_provided(args) - validate_broadcast_args(args) - validate_chain_id_args(args) + _ensure_args(args) sender = cli_shared.prepare_sender(args) guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( @@ -1366,7 +1315,7 @@ def unsign_batch(args: Any): ) abi = Abi.load(Path(args.abi)) - chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + chain_id = cli_shared.get_chain_id(args.proxy, args.chain) multisig = MultisigWrapper(TransactionsFactoryConfig(chain_id), abi) contract = Address.new_from_bech32(args.contract) @@ -1388,10 +1337,7 @@ def unsign_batch(args: Any): def unsign_for_outdated_board_members(args: Any): - validate_transaction_args(args) - ensure_wallet_args_are_provided(args) - validate_broadcast_args(args) - validate_chain_id_args(args) + _ensure_args(args) sender = cli_shared.prepare_sender(args) guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( @@ -1400,7 +1346,7 @@ def unsign_for_outdated_board_members(args: Any): ) abi = Abi.load(Path(args.abi)) - chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + chain_id = cli_shared.get_chain_id(args.proxy, args.chain) multisig = MultisigWrapper(TransactionsFactoryConfig(chain_id), abi) contract = Address.new_from_bech32(args.contract) @@ -1423,10 +1369,7 @@ def unsign_for_outdated_board_members(args: Any): def perform_action(args: Any): - validate_transaction_args(args) - ensure_wallet_args_are_provided(args) - validate_broadcast_args(args) - validate_chain_id_args(args) + _ensure_args(args) sender = cli_shared.prepare_sender(args) guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( @@ -1435,7 +1378,7 @@ def perform_action(args: Any): ) abi = Abi.load(Path(args.abi)) - chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + chain_id = cli_shared.get_chain_id(args.proxy, args.chain) multisig = MultisigWrapper(TransactionsFactoryConfig(chain_id), abi) contract = Address.new_from_bech32(args.contract) @@ -1457,10 +1400,7 @@ def perform_action(args: Any): def perform_batch(args: Any): - validate_transaction_args(args) - ensure_wallet_args_are_provided(args) - validate_broadcast_args(args) - validate_chain_id_args(args) + _ensure_args(args) sender = cli_shared.prepare_sender(args) guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( @@ -1469,7 +1409,7 @@ def perform_batch(args: Any): ) abi = Abi.load(Path(args.abi)) - chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + chain_id = cli_shared.get_chain_id(args.proxy, args.chain) multisig = MultisigWrapper(TransactionsFactoryConfig(chain_id), abi) contract = Address.new_from_bech32(args.contract) diff --git a/multiversx_sdk_cli/cli_shared.py b/multiversx_sdk_cli/cli_shared.py index 283df25e..e21ec67c 100644 --- a/multiversx_sdk_cli/cli_shared.py +++ b/multiversx_sdk_cli/cli_shared.py @@ -1,9 +1,12 @@ import argparse import ast +import base64 +import logging import sys from argparse import FileType +from functools import cache from pathlib import Path -from typing import Any, Text, Union, cast +from typing import Any, Optional, Text, Union, cast from multiversx_sdk import ( Account, @@ -29,6 +32,7 @@ DEFAULT_TX_VERSION, TCS_SERVICE_ID, ) +from multiversx_sdk_cli.env import MxpyEnv, get_address_hrp from multiversx_sdk_cli.errors import ( ArgumentsNotProvidedError, BadUsage, @@ -40,7 +44,10 @@ from multiversx_sdk_cli.simulation import Simulator from multiversx_sdk_cli.transactions import send_and_wait_for_result from multiversx_sdk_cli.utils import log_explorer_transaction -from multiversx_sdk_cli.ux import show_warning +from multiversx_sdk_cli.ux import confirm_continuation + +logger = logging.getLogger("cli_shared") + trusted_cosigner_service_url_by_chain_id = { "1": "https://tools.multiversx.com/guardian", @@ -371,7 +378,7 @@ def _get_address_hrp(args: Any) -> str: if hrp: return hrp - return config.get_address_hrp() + return get_address_hrp() def _get_hrp_from_proxy(args: Any) -> str: @@ -557,22 +564,16 @@ def get_current_nonce_for_address(address: Address, proxy_url: Union[str, None]) return proxy.get_account(address).nonce -def get_chain_id(chain_id: str, proxy_url: str) -> str: - if chain_id and proxy_url: - fetched_chain_id = _fetch_chain_id(proxy_url) - - if chain_id != fetched_chain_id: - show_warning( - f"The chain ID you have provided does not match the chain ID you got from the proxy. Will use the proxy's value: '{fetched_chain_id}'" - ) - return fetched_chain_id - +@cache +def get_chain_id(proxy_url: str, chain_id: Optional[str] = None) -> str: + """We know and have already validated that if chainID is not provided, proxy is provided.""" if chain_id: return chain_id return _fetch_chain_id(proxy_url) +@cache def _fetch_chain_id(proxy_url: str) -> str: network_provider_config = config.get_config_for_network_providers() proxy = ProxyNetworkProvider(url=proxy_url, config=network_provider_config) @@ -612,11 +613,16 @@ def send_or_simulate(tx: Transaction, args: Any, dump_output: bool = True) -> CL output_builder.set_emitted_transaction(tx) outfile = args.outfile if hasattr(args, "outfile") else None + hash = b"" try: if send_wait_result: + _confirm_continuation_if_required(tx) + transaction_on_network = send_and_wait_for_result(tx, proxy, args.timeout) output_builder.set_awaited_transaction(transaction_on_network) elif send_only: + _confirm_continuation_if_required(tx) + hash = proxy.send_transaction(tx) output_builder.set_emitted_transaction_hash(hash.hex()) elif simulate: @@ -628,15 +634,31 @@ def send_or_simulate(tx: Transaction, args: Any, dump_output: bool = True) -> CL if dump_output: utils.dump_out_json(output_transaction, outfile=outfile) - if send_only: + if send_only and hash: + cli_config = MxpyEnv.from_active_env() log_explorer_transaction( chain=output_transaction["emittedTransaction"]["chainID"], transaction_hash=output_transaction["emittedTransactionHash"], + explorer_url=cli_config.explorer_url, ) return output_builder +def _confirm_continuation_if_required(tx: Transaction) -> None: + env = MxpyEnv.from_active_env() + + if env.ask_confirmation: + transaction = tx.to_dictionary() + + # decode the data field from base64 if it exists + data = base64.b64decode(transaction.get("data", "")).decode() + transaction["data"] = data if data else "" + + utils.dump_out_json(transaction) + confirm_continuation("You are about to send the above transaction. Do you want to continue?") + + def prepare_sender(args: Any): """Returns the sender's account. If no account was provided, will raise an exception.""" @@ -690,3 +712,18 @@ def prepare_token_transfers(transfers: list[str]) -> list[TokenTransfer]: token_transfers.append(transfer) return token_transfers + + +def set_proxy_from_config_if_not_provided(args: Any) -> None: + """This function modifies the `args` object by setting the proxy from the config if not already set. If proxy is not needed (chainID and nonce are provided), the proxy will not be set.""" + if not hasattr(args, "proxy"): + return + + if not args.proxy: + if hasattr(args, "chain") and args.chain and hasattr(args, "nonce") and args.nonce is not None: + return + + env = MxpyEnv.from_active_env() + if env.proxy_url: + logger.info(f"Using proxy URL from config: {env.proxy_url}") + args.proxy = env.proxy_url diff --git a/multiversx_sdk_cli/cli_transactions.py b/multiversx_sdk_cli/cli_transactions.py index d086a99f..ccd65068 100644 --- a/multiversx_sdk_cli/cli_transactions.py +++ b/multiversx_sdk_cli/cli_transactions.py @@ -118,7 +118,7 @@ def create_transaction(args: Any): transfers = getattr(args, "token_transfers", None) transfers = cli_shared.prepare_token_transfers(transfers) if transfers else None - chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + chain_id = cli_shared.get_chain_id(args.proxy, args.chain) tx_controller = TransactionsController(chain_id) tx = tx_controller.create_transaction( @@ -148,6 +148,8 @@ def send_transaction(args: Any): proxy = ProxyNetworkProvider(url=args.proxy, config=config) try: + cli_shared._confirm_continuation_if_required(tx) + tx_hash = proxy.send_transaction(tx) output.set_emitted_transaction_hash(tx_hash.hex()) finally: diff --git a/multiversx_sdk_cli/cli_validators.py b/multiversx_sdk_cli/cli_validators.py index 7d06feb9..32a8532e 100644 --- a/multiversx_sdk_cli/cli_validators.py +++ b/multiversx_sdk_cli/cli_validators.py @@ -197,7 +197,7 @@ def do_stake(args: Any): def _get_validators_controller(args: Any): - chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + chain_id = cli_shared.get_chain_id(args.proxy, args.chain) validators = ValidatorsController(chain_id) return validators @@ -219,6 +219,7 @@ def _parse_public_bls_keys(public_bls_keys: str) -> list[ValidatorPublicKey]: def do_unstake(args: Any): validate_args(args) + sender = cli_shared.prepare_sender(args) guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( sender=sender.address.to_bech32(), @@ -247,6 +248,7 @@ def do_unstake(args: Any): def do_unjail(args: Any): validate_args(args) + sender = cli_shared.prepare_sender(args) guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( sender=sender.address.to_bech32(), @@ -275,6 +277,7 @@ def do_unjail(args: Any): def do_unbond(args: Any): validate_args(args) + sender = cli_shared.prepare_sender(args) guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( sender=sender.address.to_bech32(), @@ -303,6 +306,7 @@ def do_unbond(args: Any): def change_reward_address(args: Any): validate_args(args) + sender = cli_shared.prepare_sender(args) guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( sender=sender.address.to_bech32(), @@ -331,6 +335,7 @@ def change_reward_address(args: Any): def do_claim(args: Any): validate_args(args) + sender = cli_shared.prepare_sender(args) guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( sender=sender.address.to_bech32(), @@ -357,6 +362,7 @@ def do_claim(args: Any): def do_unstake_nodes(args: Any): validate_args(args) + sender = cli_shared.prepare_sender(args) guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( sender=sender.address.to_bech32(), @@ -386,6 +392,7 @@ def do_unstake_nodes(args: Any): def do_unstake_tokens(args: Any): validate_args(args) + sender = cli_shared.prepare_sender(args) guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( sender=sender.address.to_bech32(), @@ -414,6 +421,7 @@ def do_unstake_tokens(args: Any): def do_unbond_nodes(args: Any): validate_args(args) + sender = cli_shared.prepare_sender(args) guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( sender=sender.address.to_bech32(), @@ -443,6 +451,7 @@ def do_unbond_nodes(args: Any): def do_unbond_tokens(args: Any): validate_args(args) + sender = cli_shared.prepare_sender(args) guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( sender=sender.address.to_bech32(), @@ -471,6 +480,7 @@ def do_unbond_tokens(args: Any): def do_clean_registered_data(args: Any): validate_args(args) + sender = cli_shared.prepare_sender(args) guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( sender=sender.address.to_bech32(), @@ -497,6 +507,7 @@ def do_clean_registered_data(args: Any): def do_restake_unstaked_nodes(args: Any): validate_args(args) + sender = cli_shared.prepare_sender(args) guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( sender=sender.address.to_bech32(), diff --git a/multiversx_sdk_cli/cli_wallet.py b/multiversx_sdk_cli/cli_wallet.py index 558e720a..48d99048 100644 --- a/multiversx_sdk_cli/cli_wallet.py +++ b/multiversx_sdk_cli/cli_wallet.py @@ -9,8 +9,8 @@ from multiversx_sdk.core.address import get_shard_of_pubkey from multiversx_sdk_cli import cli_shared, utils -from multiversx_sdk_cli.config import get_address_hrp from multiversx_sdk_cli.constants import NUMBER_OF_SHARDS +from multiversx_sdk_cli.env import get_address_hrp from multiversx_sdk_cli.errors import ( BadUsage, BadUserInput, diff --git a/multiversx_sdk_cli/config.py b/multiversx_sdk_cli/config.py index ee83f3c5..219c7770 100644 --- a/multiversx_sdk_cli/config.py +++ b/multiversx_sdk_cli/config.py @@ -1,4 +1,5 @@ import os +from functools import cache from pathlib import Path from typing import Any @@ -11,23 +12,6 @@ GLOBAL_CONFIG_PATH = SDK_PATH / "mxpy.json" -class MetaChainSystemSCsCost: - STAKE = 5000000 - UNSTAKE = 5000000 - UNBOND = 5000000 - CLAIM = 5000000 - GET = 5000000 - CHANGE_REWARD_ADDRESS = 5000000 - CHANGE_VALIDATOR_KEYS = 5000000 - UNJAIL = 5000000 - DELEGATION_MANAGER_OPS = 50000000 - DELEGATION_OPS = 1000000 - UNSTAKE_TOKENS = 5000000 - UNBOND_TOKENS = 5000000 - CLEAN_REGISTERED_DATA = 5000000 - RESTAKE_UNSTAKED_NODES = 5000000 - - def get_dependency_resolution(key: str) -> str: try: return get_value(f"dependencies.{key}.resolution") @@ -48,6 +32,7 @@ def get_dependency_url(key: str, tag: str, platform: str) -> str: return url_template.replace("{TAG}", tag) +@cache def get_value(name: str) -> str: _guard_valid_name(name) data = get_active() @@ -57,10 +42,6 @@ def get_value(name: str) -> str: return value -def get_address_hrp() -> str: - return get_value("default_address_hrp") - - def set_value(name: str, value: Any): _guard_valid_name(name) data = read_file() @@ -145,25 +126,16 @@ def _guard_valid_config_deletion(name: str): def get_defaults() -> dict[str, Any]: return { - "dependencies.vmtools.tag": "v1.5.24", - "dependencies.vmtools.urlTemplate.linux": "https://github.com/multiversx/mx-chain-vm-go/archive/{TAG}.tar.gz", - "dependencies.vmtools.urlTemplate.osx": "https://github.com/multiversx/mx-chain-vm-go/archive/{TAG}.tar.gz", - "dependencies.vmtools.urlTemplate.windows": "https://github.com/multiversx/mx-chain-vm-go/archive/{TAG}.tar.gz", - "dependencies.rust.tag": "stable", "dependencies.golang.resolution": "SDK", "dependencies.golang.tag": "go1.20.7", "dependencies.golang.urlTemplate.linux": "https://golang.org/dl/{TAG}.linux-amd64.tar.gz", "dependencies.golang.urlTemplate.osx": "https://golang.org/dl/{TAG}.darwin-amd64.tar.gz", "dependencies.golang.urlTemplate.windows": "https://golang.org/dl/{TAG}.windows-amd64.zip", - "dependencies.twiggy.tag": "", - "dependencies.sc-meta.tag": "", "dependencies.testwallets.tag": "v1.0.0", "dependencies.testwallets.urlTemplate.linux": "https://github.com/multiversx/mx-sdk-testwallets/archive/{TAG}.tar.gz", "dependencies.testwallets.urlTemplate.osx": "https://github.com/multiversx/mx-sdk-testwallets/archive/{TAG}.tar.gz", "dependencies.testwallets.urlTemplate.windows": "https://github.com/multiversx/mx-sdk-testwallets/archive/{TAG}.tar.gz", - "dependencies.wasm-opt.tag": "0.112.0", "github_api_token": "", - "default_address_hrp": "erd", } @@ -179,6 +151,7 @@ def resolve_config_path() -> Path: return GLOBAL_CONFIG_PATH +@cache def read_file() -> dict[str, Any]: config_path = resolve_config_path() if config_path.exists(): @@ -192,53 +165,6 @@ def write_file(data: dict[str, Any]): utils.write_json_file(str(config_path), data) -def add_config_args(argv: list[str]) -> list[str]: - try: - command, subcommand, *_ = argv - except ValueError: - return argv - - config = read_file() - - try: - config_args = config[command][subcommand] - except KeyError: - return argv - - final_args = determine_final_args(argv, config_args) - print(f"Found extra arguments in mxpy.json. Final arguments: {final_args}") - return final_args - - -def determine_final_args(argv: list[str], config_args: dict[str, Any]) -> list[str]: - extra_args: list[str] = [] - for key, value in config_args.items(): - key_arg = f"--{key}" - # arguments from the command line override the config - if key_arg in argv: - continue - if any(arg.startswith(f"{key_arg}=") for arg in argv): - continue - extra_args.append(key_arg) - if value is True: - continue - if isinstance(value, list): - for item in value: # type: ignore - extra_args.append(str(item)) # type: ignore - else: - extra_args.append(str(value)) - - # the verbose flag is an exception since it has to go before the command and subcommand - # eg. mxpy --verbose contract deploy - verbose_flag = "--verbose" - pre_args = [] - if verbose_flag in extra_args: - extra_args.remove(verbose_flag) - pre_args = [verbose_flag] - - return pre_args + argv + extra_args - - def get_dependency_directory(key: str, tag: str) -> Path: parent_directory = get_dependency_parent_directory(key) return parent_directory / tag diff --git a/multiversx_sdk_cli/delegation.py b/multiversx_sdk_cli/delegation.py index 17a8c377..e87e2862 100644 --- a/multiversx_sdk_cli/delegation.py +++ b/multiversx_sdk_cli/delegation.py @@ -8,7 +8,7 @@ from multiversx_sdk.abi import BigUIntValue, Serializer from multiversx_sdk_cli.base_transactions_controller import BaseTransactionsController -from multiversx_sdk_cli.config import get_address_hrp +from multiversx_sdk_cli.env import get_address_hrp from multiversx_sdk_cli.errors import BadUsage from multiversx_sdk_cli.guardian_relayer_data import GuardianRelayerData from multiversx_sdk_cli.interfaces import IAccount diff --git a/multiversx_sdk_cli/dns.py b/multiversx_sdk_cli/dns.py index 95ab0a85..2714ca5a 100644 --- a/multiversx_sdk_cli/dns.py +++ b/multiversx_sdk_cli/dns.py @@ -15,8 +15,8 @@ validate_chain_id_args, validate_transaction_args, ) -from multiversx_sdk_cli.config import get_address_hrp from multiversx_sdk_cli.constants import ADDRESS_ZERO_HEX +from multiversx_sdk_cli.env import get_address_hrp from multiversx_sdk_cli.transactions import TransactionsController MaxNumShards = 256 @@ -77,7 +77,7 @@ def register(args: Any): receiver = dns_address_for_name(args.name) data = dns_register_data(args.name) - chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + chain_id = cli_shared.get_chain_id(args.proxy, args.chain) controller = TransactionsController(chain_id) tx = controller.create_transaction( diff --git a/multiversx_sdk_cli/env.py b/multiversx_sdk_cli/env.py new file mode 100644 index 00000000..8a878268 --- /dev/null +++ b/multiversx_sdk_cli/env.py @@ -0,0 +1,182 @@ +from dataclasses import dataclass +from functools import cache +from pathlib import Path +from typing import Any + +from multiversx_sdk_cli.errors import ( + EnvironmentAlreadyExistsError, + EnvironmentProtectedError, + InvalidConfirmationSettingError, + InvalidEnvironmentValue, + UnknownEnvironmentError, +) +from multiversx_sdk_cli.utils import read_json_file, write_json_file + +SDK_PATH = Path("~/multiversx-sdk").expanduser().resolve() +LOCAL_ENV_PATH = Path("env.mxpy.json").resolve() +GLOBAL_ENV_PATH = SDK_PATH / "env.mxpy.json" + + +@dataclass +class MxpyEnv: + address_hrp: str + proxy_url: str + explorer_url: str + ask_confirmation: bool + + @classmethod + @cache + def from_active_env(cls) -> "MxpyEnv": + return cls( + address_hrp=get_address_hrp(), + proxy_url=get_proxy_url(), + explorer_url=get_explorer_url(), + ask_confirmation=get_confirmation_setting(), + ) + + +def get_defaults() -> dict[str, str]: + return { + "default_address_hrp": "erd", + "proxy_url": "", + "explorer_url": "", + "ask_confirmation": "false", + } + + +@cache +def get_address_hrp() -> str: + return get_value("default_address_hrp") + + +@cache +def get_proxy_url() -> str: + return get_value("proxy_url") + + +@cache +def get_explorer_url() -> str: + return get_value("explorer_url") + + +@cache +def get_confirmation_setting() -> bool: + confirmation_value = get_value("ask_confirmation") + if confirmation_value.lower() in ["true", "yes", "1"]: + return True + elif confirmation_value.lower() in ["false", "no", "0"]: + return False + else: + raise InvalidConfirmationSettingError(confirmation_value) + + +@cache +def get_value(name: str) -> str: + _guard_valid_name(name) + data = get_active_env() + default_value = get_defaults()[name] + value = data.get(name, default_value) + assert isinstance(value, str) + return value + + +def _guard_valid_name(name: str): + if name not in get_defaults().keys(): + raise InvalidEnvironmentValue(f"Key is not present in environment config: [{name}]") + + +def get_active_env() -> dict[str, str]: + data = read_env_file() + envs: dict[str, Any] = data.get("environments", {}) + active_env_name: str = data.get("active", "default") + result: dict[str, str] = envs.get(active_env_name, {}) + + return result + + +@cache +def read_env_file() -> dict[str, Any]: + env_path = resolve_env_path() + if env_path.exists(): + data: dict[str, Any] = read_json_file(env_path) + return data + return dict() + + +def resolve_env_path() -> Path: + if LOCAL_ENV_PATH.is_file(): + return LOCAL_ENV_PATH + return GLOBAL_ENV_PATH + + +def set_value(name: str, value: Any): + _guard_valid_name(name) + data = read_env_file() + active_env = data.get("active", "default") + data.setdefault("environments", {}) + data["environments"].setdefault(active_env, {}) + data["environments"][active_env][name] = value + write_file(data) + + +def write_file(data: dict[str, Any]): + env_path = resolve_env_path() + write_json_file(str(env_path), data) + + +def delete_value(name: str): + """Deletes a key-value pair of the active env.""" + _guard_valid_env_deletion(name) + data = read_env_file() + active_env = data.get("active", "default") + data.setdefault("environments", {}) + data["environments"].setdefault(active_env, {}) + del data["environments"][active_env][name] + write_file(data) + + +def _guard_valid_env_deletion(name: str): + if name == "default": + raise EnvironmentProtectedError(name) + + +def set_active(name: str): + data = read_env_file() + _guard_valid_env_name(data, name) + data["active"] = name + write_file(data) + + +def _guard_valid_env_name(env: Any, name: str): + envs = env.get("environments", {}) + if name not in envs: + raise UnknownEnvironmentError(name) + + +def create_new_env(name: str, template: str): + data = read_env_file() + _guard_env_unique(data, name) + new_env = {} + if template: + _guard_valid_env_name(data, template) + new_env = data["environments"][template] + + data["active"] = name + data.setdefault("environments", {}) + data["environments"][name] = new_env + write_file(data) + + +def _guard_env_unique(env: Any, name: str): + envs = env.get("environments", {}) + if name in envs: + raise EnvironmentAlreadyExistsError(name) + + +def delete_env(name: str): + _guard_valid_env_deletion(name) + data = read_env_file() + data["environments"].pop(name, None) + if data["active"] == name: + data["active"] = "default" + write_file(data) diff --git a/multiversx_sdk_cli/errors.py b/multiversx_sdk_cli/errors.py index 1ec56977..009dfff5 100644 --- a/multiversx_sdk_cli/errors.py +++ b/multiversx_sdk_cli/errors.py @@ -159,3 +159,30 @@ def __init__(self, message: str): class TransactionSigningError(KnownError): def __init__(self, message: str): super().__init__(message) + + +class InvalidConfirmationSettingError(KnownError): + def __init__(self, value: str): + super().__init__( + f"Invalid confirmation setting: {value}. Valid values are: ['true', 'false', 'yes', 'no', '1', '0']." + ) + + +class InvalidEnvironmentValue(KnownError): + def __init__(self, message: str): + super().__init__(message) + + +class EnvironmentProtectedError(KnownError): + def __init__(self, name: str): + super().__init__(f"This environment name is protected: {name}.") + + +class UnknownEnvironmentError(KnownError): + def __init__(self, name: str): + super().__init__(f"Environment entry is not known: {name}.") + + +class EnvironmentAlreadyExistsError(KnownError): + def __init__(self, name: str): + super().__init__(f"Environment entry already exists: {name}.") diff --git a/multiversx_sdk_cli/tests/test_cli_contracts.py b/multiversx_sdk_cli/tests/test_cli_contracts.py index 4d579cfe..fa9d5bf6 100644 --- a/multiversx_sdk_cli/tests/test_cli_contracts.py +++ b/multiversx_sdk_cli/tests/test_cli_contracts.py @@ -270,6 +270,7 @@ def test_contract_flow(capsys: Any): def test_contract_deploy_without_required_arguments(): + """This test passes with an unaltered config. If proxy is set in the config, the test will fail due to mxpy fetching the nonce and the chain ID.""" alice = f"{parent}/testdata/alice.pem" adder = f"{parent}/testdata/adder.wasm" diff --git a/multiversx_sdk_cli/tests/test_cli_governance.py b/multiversx_sdk_cli/tests/test_cli_governance.py index c67922d2..65887c12 100644 --- a/multiversx_sdk_cli/tests/test_cli_governance.py +++ b/multiversx_sdk_cli/tests/test_cli_governance.py @@ -209,4 +209,5 @@ def _read_stdout(capsys: Any) -> str: def get_transaction(capsys: Any) -> dict[str, Any]: out = _read_stdout(capsys) output: dict[str, Any] = json.loads(out) - return output["emittedTransaction"] + tx: dict[str, Any] = output["emittedTransaction"] + return tx diff --git a/multiversx_sdk_cli/tests/test_cli_multisig.py b/multiversx_sdk_cli/tests/test_cli_multisig.py index e280e867..f2788f43 100644 --- a/multiversx_sdk_cli/tests/test_cli_multisig.py +++ b/multiversx_sdk_cli/tests/test_cli_multisig.py @@ -955,4 +955,5 @@ def _read_stdout(capsys: Any) -> str: def get_transaction(capsys: Any) -> dict[str, Any]: out = _read_stdout(capsys) output: dict[str, Any] = json.loads(out) - return output["emittedTransaction"] + tx: dict[str, Any] = output["emittedTransaction"] + return tx diff --git a/multiversx_sdk_cli/utils.py b/multiversx_sdk_cli/utils.py index f15518a4..9aaedd2d 100644 --- a/multiversx_sdk_cli/utils.py +++ b/multiversx_sdk_cli/utils.py @@ -150,22 +150,36 @@ def is_arg_present(args: list[str], key: str) -> bool: return False -def log_explorer(chain: str, name: str, path: str, details: str): - networks = { - "1": ("MultiversX Mainnet Explorer", "https://explorer.multiversx.com"), - "T": ("MultiversX Testnet Explorer", "https://testnet-explorer.multiversx.com"), - "D": ("MultiversX Devnet Explorer", "https://devnet-explorer.multiversx.com"), - } - try: - explorer_name, explorer_url = networks[chain] - logger.info(f"View this {name} in the {explorer_name}: {explorer_url}/{path}/{details}") - except KeyError: - return +def log_explorer(name: str, explorer: str, path: str, details: str): + logger.info(f"View this {name} in the MultiversX Explorer: {explorer}/{path}/{details}") + + +def log_explorer_contract_address(chain: str, address: str, explorer_url: Optional[str] = ""): + if explorer_url: + log_explorer("contract address", explorer_url, "accounts", address) + else: + explorer = get_explorer_by_chain_id(chain_id=chain) + if explorer: + log_explorer("contract address", explorer, "accounts", address) -def log_explorer_contract_address(chain: str, address: str): - log_explorer(chain, "contract address", "accounts", address) +def log_explorer_transaction(chain: str, transaction_hash: str, explorer_url: Optional[str] = ""): + if explorer_url: + log_explorer("transaction", explorer_url, "transactions", transaction_hash) + else: + explorer = get_explorer_by_chain_id(chain_id=chain) + if explorer: + log_explorer("transaction", explorer, "transactions", transaction_hash) -def log_explorer_transaction(chain: str, transaction_hash: str): - log_explorer(chain, "transaction", "transactions", transaction_hash) +def get_explorer_by_chain_id(chain_id: str) -> str: + explorers_by_chain_id = { + "1": "https://explorer.multiversx.com", + "T": "https://testnet-explorer.multiversx.com", + "D": "https://devnet-explorer.multiversx.com", + } + + try: + return explorers_by_chain_id[chain_id] + except KeyError: + return "" diff --git a/pyproject.toml b/pyproject.toml index a7b38afc..a6f06f85 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ authors = [ license = "MIT" description = "MultiversX Smart Contracts Tools" readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.10" classifiers = [ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License",