diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 32c1c314..0fa4d9bb 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -19,7 +19,7 @@ jobs: strategy: matrix: - os: [windows-2019] + os: [windows-2022] python-version: [3.11] steps: diff --git a/CLI.md b/CLI.md index 7c572ddd..96424409 100644 --- a/CLI.md +++ b/CLI.md @@ -23,19 +23,19 @@ See: COMMAND GROUPS: - {address,contract,tx,validator,ledger,wallet,validator-wallet,deps,config,localnet,data,staking-provider,dns,faucet,multisig,governance,env,get} + {config-wallet,contract,tx,validator,ledger,wallet,validator-wallet,deps,config,localnet,data,staking-provider,dns,faucet,multisig,governance,env,get} TOP-LEVEL OPTIONS: -h, --help show this help message and exit -v, --version show program's version number and exit --verbose --log-level {debug,info,warning,error} - default: debug + default: info ---------------------- COMMAND GROUPS summary ---------------------- -address Configure MultiversX CLI to use a default wallet. +config-wallet Configure MultiversX CLI to use a default wallet. contract Deploy, upgrade and interact with Smart Contracts tx Create and broadcast Transactions validator Stake, UnStake, UnBond, Unjail and other actions useful for Validators @@ -5783,12 +5783,12 @@ options: -h, --help show this help message and exit ``` -## Group **Address** +## Group **ConfigWallet** ``` -$ mxpy address --help -usage: mxpy address COMMAND [-h] ... +$ mxpy config-wallet --help +usage: mxpy config-wallet COMMAND [-h] ... Configure MultiversX CLI to use a default wallet. @@ -5801,149 +5801,148 @@ OPTIONS: ---------------- COMMANDS summary ---------------- -new Creates a new address config and sets it as the active address. -list List available addresses -dump Dumps the active address. -get Gets a config value from the active address. -set Sets a config value for the active address. -delete Deletes a config value from the active address. -switch Switch to a different address. -remove Deletes an address using the alias. No default address will be set. Use `address switch` to set a new address. -reset Deletes the config file. No default address will be set. +new Creates a new wallet config and sets it as the active wallet. +list List configured wallets +dump Dumps the active wallet. +get Gets a config value from the specified wallet. +set Sets a config value for the specified wallet. +delete Deletes a config value from the specified wallet. +switch Switch to a different wallet. +remove Removes a wallet from the config using the alias. No default wallet will be set. Use `config-wallet switch` to set a new wallet. +reset Deletes the config file. No default wallet will be set. ``` -### Address.New +### ConfigWallet.New ``` -$ mxpy address new --help -usage: mxpy address new [-h] ... +$ mxpy config-wallet new --help +usage: mxpy config-wallet new [-h] ... -Creates a new address config and sets it as the active address. +Creates a new wallet config and sets it as the active wallet. positional arguments: - alias the alias of the wallet + alias the alias of the wallet options: - -h, --help show this help message and exit - --template TEMPLATE an address config from which to create the new address + -h, --help show this help message and exit + --path PATH the absolute path to the wallet file ``` -### Address.List +### ConfigWallet.List ``` -$ mxpy address list --help -usage: mxpy address list [-h] ... +$ mxpy config-wallet list --help +usage: mxpy config-wallet list [-h] ... -List available addresses +List configured wallets options: -h, --help show this help message and exit ``` -### Address.Dump +### ConfigWallet.Dump ``` -$ mxpy address dump --help -usage: mxpy address dump [-h] ... +$ mxpy config-wallet dump --help +usage: mxpy config-wallet dump [-h] ... -Dumps the active address. +Dumps the active wallet. options: -h, --help show this help message and exit ``` -### Address.Get +### ConfigWallet.Get ``` -$ mxpy address get --help -usage: mxpy address get [-h] ... +$ mxpy config-wallet get --help +usage: mxpy config-wallet get [-h] ... -Gets a config value from the active address. +Gets a config value from the specified wallet. positional arguments: - value the value to get from the active address (e.g. path) + value the value to get from the specified wallet (e.g. path) options: - -h, --help show this help message and exit + -h, --help show this help message and exit + --alias ALIAS the alias of the wallet ``` -### Address.Set +### ConfigWallet.Set ``` -$ mxpy address set --help -usage: mxpy address set [-h] ... +$ mxpy config-wallet set --help +usage: mxpy config-wallet set [-h] ... -Sets a config value for the active address. +Sets a config value for the specified wallet. positional arguments: - key the key to set for the active address (e.g. index) - value the value to set for the specified key + key the key to set for the specified wallet (e.g. index) + value the value to set for the specified key options: - -h, --help show this help message and exit + -h, --help show this help message and exit + --alias ALIAS the alias of the wallet ``` -### Address.Set +### ConfigWallet.Switch ``` -$ mxpy address delete --help -usage: mxpy address delete [-h] ... +$ mxpy config-wallet switch --help +usage: mxpy config-wallet switch [-h] ... -Deletes a config value from the active address. - -positional arguments: - value the value to delete for the active address +Switch to a different wallet. options: - -h, --help show this help message and exit + -h, --help show this help message and exit + --alias ALIAS the alias of the wallet ``` -### Address.Switch +### ConfigWallet.Delete ``` -$ mxpy address switch --help -usage: mxpy address switch [-h] ... +$ mxpy config-wallet delete --help +usage: mxpy config-wallet delete [-h] ... -Switch to a different address. +Deletes a config value from the specified wallet. positional arguments: - alias the alias of the wallet + value the value to delete for the specified address options: - -h, --help show this help message and exit + -h, --help show this help message and exit + --alias ALIAS the alias of the wallet ``` -### Address.Remove +### ConfigWallet.Remove ``` -$ mxpy address remove --help -usage: mxpy address remove [-h] ... +$ mxpy config-wallet remove --help +usage: mxpy config-wallet remove [-h] ... -Deletes an address using the alias. No default address will be set. Use `address switch` to set a new address. - -positional arguments: - alias the alias of the wallet +Removes a wallet from the config using the alias. No default wallet will be set. Use `config-wallet switch` to set a new wallet. options: - -h, --help show this help message and exit + -h, --help show this help message and exit + --alias ALIAS the alias of the wallet ``` -### Address.Reset +### ConfigWallet.Reset ``` -$ mxpy address reset --help -usage: mxpy address reset [-h] ... +$ mxpy config-wallet reset --help +usage: mxpy config-wallet reset [-h] ... -Deletes the config file. No default address will be set. +Deletes the config file. No default wallet will be set. options: -h, --help show this help message and exit diff --git a/CLI.md.sh b/CLI.md.sh index 2ad195f3..ceb6f205 100755 --- a/CLI.md.sh +++ b/CLI.md.sh @@ -195,16 +195,16 @@ generate() { command "Environment.Remove" "env remove" command "Environment.Reset" "env reset" - group "Address" "address" - command "Address.New" "address new" - command "Address.List" "address list" - command "Address.Dump" "address dump" - command "Address.Get" "address get" - command "Address.Set" "address set" - command "Address.Set" "address delete" - command "Address.Switch" "address switch" - command "Address.Remove" "address remove" - command "Address.Reset" "address reset" + group "ConfigWallet" "config-wallet" + command "ConfigWallet.New" "config-wallet new" + command "ConfigWallet.List" "config-wallet list" + command "ConfigWallet.Dump" "config-wallet dump" + command "ConfigWallet.Get" "config-wallet get" + command "ConfigWallet.Set" "config-wallet set" + command "ConfigWallet.Switch" "config-wallet switch" + command "ConfigWallet.Delete" "config-wallet delete" + command "ConfigWallet.Remove" "config-wallet remove" + command "ConfigWallet.Reset" "config-wallet reset" group "Get" "get" command "Get.Account" "get account" diff --git a/multiversx_sdk_cli/address_config.py b/multiversx_sdk_cli/address_config.py deleted file mode 100644 index f1bf731f..00000000 --- a/multiversx_sdk_cli/address_config.py +++ /dev/null @@ -1,163 +0,0 @@ -from functools import cache -from pathlib import Path -from typing import Any - -from multiversx_sdk_cli.constants import SDK_PATH -from multiversx_sdk_cli.errors import ( - AliasAlreadyExistsError, - AliasProtectedError, - InvalidAddressConfigValue, - UnknownAddressAliasError, -) -from multiversx_sdk_cli.utils import read_json_file, write_json_file - -LOCAL_ADDRESS_CONFIG_PATH = Path("addresses.mxpy.json").resolve() -GLOBAL_ADDRESS_CONFIG_PATH = SDK_PATH / "addresses.mxpy.json" - - -def get_defaults() -> dict[str, str]: - """ - Not all values are required for a config to be valid. - - Valid config for PEM wallets: - ``` - { - "kind": "pem", - "path": "/path/to/wallet.pem", - "index": "0" # optional, defaults to 0 - } - ``` - - Valid config for KEYSTORE wallets: - ``` - { - "kind": "keystore", - "path": "/path/to/wallet.json", - "index": "0" # optional, defaults to 0 - } - ``` - - For keystore wallets, you'll be prompted to enter the password when using the wallet. - """ - return { - "kind": "", - "path": "", - "index": "", - } - - -@cache -def get_value(name: str) -> str: - _guard_valid_name(name) - data = get_active_address() - 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 InvalidAddressConfigValue(f"Key is not present in address config: [{name}]") - - -def get_active_address() -> dict[str, str]: - """Returns the active address configuration.""" - data = read_address_config_file() - addresses: dict[str, Any] = data.get("addresses", {}) - active_address: str = data.get("active", "default") - result: dict[str, str] = addresses.get(active_address, {}) - - return result - - -@cache -def read_address_config_file() -> dict[str, Any]: - config_path = resolve_address_config_path() - if config_path.exists(): - data: dict[str, Any] = read_json_file(config_path) - return data - return dict() - - -def resolve_address_config_path() -> Path: - if LOCAL_ADDRESS_CONFIG_PATH.is_file(): - return LOCAL_ADDRESS_CONFIG_PATH - return GLOBAL_ADDRESS_CONFIG_PATH - - -def set_value(name: str, value: Any): - """Sets a key-value pair in the active address config.""" - _guard_valid_name(name) - data = read_address_config_file() - active_env = data.get("active", "default") - data.setdefault("addresses", {}) - data["addresses"].setdefault(active_env, {}) - data["addresses"][active_env][name] = value - _write_file(data) - - -def _write_file(data: dict[str, Any]): - env_path = resolve_address_config_path() - write_json_file(str(env_path), data) - - -def set_active(name: str): - """Switches to the address configuration with the given name.""" - data = read_address_config_file() - _guard_valid_address_name(data, name) - data["active"] = name - _write_file(data) - - -def _guard_valid_address_name(env: Any, name: str): - envs = env.get("addresses", {}) - if name not in envs: - raise UnknownAddressAliasError(name) - - -def create_new_address_config(name: str, template: str): - """Creates a new address config with the given name and optional template.""" - data = read_address_config_file() - _guard_alias_unique(data, name) - new_address = {} - if template: - _guard_valid_address_name(data, template) - new_address = data["addresses"][template] - - data["active"] = name - data.setdefault("addresses", {}) - data["addresses"][name] = new_address - _write_file(data) - - -def _guard_alias_unique(env: Any, name: str): - envs = env.get("addresses", {}) - if name in envs: - raise AliasAlreadyExistsError(name) - - -def delete_config_value(name: str): - """Deletes a key-value pair of the active address config.""" - _guard_valid_alias_deletion(name) - data = read_address_config_file() - active_env = data.get("active", "default") - data.setdefault("addresses", {}) - data["addresses"].setdefault(active_env, {}) - del data["addresses"][active_env][name] - _write_file(data) - - -def delete_alias(name: str): - """Deletes the address configuration with the given name.""" - _guard_valid_alias_deletion(name) - data = read_address_config_file() - data["addresses"].pop(name, None) - if data["active"] == name: - data["active"] = "default" - _write_file(data) - - -def _guard_valid_alias_deletion(name: str): - if name == "default": - raise AliasProtectedError(name) diff --git a/multiversx_sdk_cli/cli.py b/multiversx_sdk_cli/cli.py index 16acbb32..0e2a2639 100644 --- a/multiversx_sdk_cli/cli.py +++ b/multiversx_sdk_cli/cli.py @@ -9,8 +9,8 @@ from multiversx_sdk import LibraryConfig from rich.logging import RichHandler -import multiversx_sdk_cli.cli_address import multiversx_sdk_cli.cli_config +import multiversx_sdk_cli.cli_config_wallet import multiversx_sdk_cli.cli_contracts import multiversx_sdk_cli.cli_data import multiversx_sdk_cli.cli_delegation @@ -124,7 +124,7 @@ def setup_parser(args: list[str]): subparsers = parser.add_subparsers() commands: list[Any] = [] - commands.append(multiversx_sdk_cli.cli_address.setup_parser(subparsers)) + commands.append(multiversx_sdk_cli.cli_config_wallet.setup_parser(subparsers)) commands.append(multiversx_sdk_cli.cli_contracts.setup_parser(args, subparsers)) commands.append(multiversx_sdk_cli.cli_transactions.setup_parser(args, subparsers)) commands.append(multiversx_sdk_cli.cli_validators.setup_parser(args, subparsers)) diff --git a/multiversx_sdk_cli/cli_address.py b/multiversx_sdk_cli/cli_address.py deleted file mode 100644 index 771f3a85..00000000 --- a/multiversx_sdk_cli/cli_address.py +++ /dev/null @@ -1,150 +0,0 @@ -import logging -import os -from typing import Any - -from multiversx_sdk_cli import cli_shared -from multiversx_sdk_cli.address_config import ( - create_new_address_config, - delete_alias, - delete_config_value, - get_active_address, - get_value, - read_address_config_file, - resolve_address_config_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.address") - - -def setup_parser(subparsers: Any) -> Any: - parser = cli_shared.add_group_subparser(subparsers, "address", "Configure MultiversX CLI to use a default wallet.") - subparsers = parser.add_subparsers() - - sub = cli_shared.add_command_subparser( - subparsers, "address", "new", "Creates a new address config and sets it as the active address." - ) - _add_alias_arg(sub) - sub.add_argument( - "--template", - required=False, - help="an address config from which to create the new address", - ) - sub.set_defaults(func=new_address_config) - - sub = cli_shared.add_command_subparser(subparsers, "address", "list", "List available addresses") - sub.set_defaults(func=list_addresses) - - sub = cli_shared.add_command_subparser(subparsers, "address", "dump", "Dumps the active address.") - sub.set_defaults(func=dump) - - sub = cli_shared.add_command_subparser(subparsers, "address", "get", "Gets a config value from the active address.") - sub.add_argument("value", type=str, help="the value to get from the active address (e.g. path)") - sub.set_defaults(func=get_address_config_value) - - sub = cli_shared.add_command_subparser(subparsers, "address", "set", "Sets a config value for the active address.") - sub.add_argument("key", type=str, help="the key to set for the active address (e.g. index)") - sub.add_argument("value", type=str, help="the value to set for the specified key") - sub.set_defaults(func=set_address_config_value) - - sub = cli_shared.add_command_subparser( - subparsers, "address", "delete", "Deletes a config value from the active address." - ) - sub.add_argument("value", type=str, help="the value to delete for the active address") - sub.set_defaults(func=delete_address_config_value) - - sub = cli_shared.add_command_subparser(subparsers, "address", "switch", "Switch to a different address.") - _add_alias_arg(sub) - sub.set_defaults(func=switch_address) - - sub = cli_shared.add_command_subparser( - subparsers, - "address", - "remove", - "Deletes an address using the alias. No default address will be set. Use `address switch` to set a new address.", - ) - _add_alias_arg(sub) - sub.set_defaults(func=remove_address) - - sub = cli_shared.add_command_subparser( - subparsers, - "address", - "reset", - "Deletes the config file. No default address will be set.", - ) - sub.set_defaults(func=delete_address_config_file) - - parser.epilog = cli_shared.build_group_epilog(subparsers) - return subparsers - - -def _add_alias_arg(sub: Any): - sub.add_argument("alias", type=str, help="the alias of the wallet") - - -def new_address_config(args: Any): - create_new_address_config(name=args.alias, template=args.template) - dump_out_json(get_active_address()) - - -def list_addresses(args: Any): - _ensure_address_config_file_exists() - - data = read_address_config_file() - dump_out_json(data) - - -def dump(args: Any): - _ensure_address_config_file_exists() - dump_out_json(get_active_address()) - - -def get_address_config_value(args: Any): - _ensure_address_config_file_exists() - value = get_value(args.value) - print(value) - - -def set_address_config_value(args: Any): - _ensure_address_config_file_exists() - set_value(args.key, args.value) - - -def delete_address_config_value(args: Any): - _ensure_address_config_file_exists() - - delete_config_value(args.value) - dump_out_json(get_active_address()) - - -def switch_address(args: Any): - _ensure_address_config_file_exists() - - set_active(args.alias) - dump_out_json(get_active_address()) - - -def remove_address(args: Any): - _ensure_address_config_file_exists() - delete_alias(args.alias) - - -def delete_address_config_file(args: Any): - address_file = resolve_address_config_path() - if not address_file.is_file(): - logger.info("Address config file not found. Aborting...") - return - - confirm_continuation(f"The file `{str(address_file)}` will be deleted. Do you want to continue? (y/n)") - os.remove(address_file) - logger.info("Successfully deleted the address config file.") - - -def _ensure_address_config_file_exists(): - address_file = resolve_address_config_path() - if not address_file.is_file(): - logger.info("Address config file not found. Aborting...") - exit(1) diff --git a/multiversx_sdk_cli/cli_config_wallet.py b/multiversx_sdk_cli/cli_config_wallet.py new file mode 100644 index 00000000..08c90dc0 --- /dev/null +++ b/multiversx_sdk_cli/cli_config_wallet.py @@ -0,0 +1,155 @@ +import logging +import os +from typing import Any + +from multiversx_sdk_cli import cli_shared +from multiversx_sdk_cli.config_wallet import ( + create_new_wallet_config, + delete_alias, + delete_config_value, + get_active_wallet, + get_value, + read_wallet_config_file, + resolve_wallet_config_path, + set_value, + switch_wallet, +) +from multiversx_sdk_cli.utils import dump_out_json +from multiversx_sdk_cli.ux import confirm_continuation + +logger = logging.getLogger("cli.config_wallet") + + +def setup_parser(subparsers: Any) -> Any: + parser = cli_shared.add_group_subparser( + subparsers, "config-wallet", "Configure MultiversX CLI to use a default wallet." + ) + subparsers = parser.add_subparsers() + + sub = cli_shared.add_command_subparser( + subparsers, "config-wallet", "new", "Creates a new wallet config and sets it as the active wallet." + ) + sub.add_argument("alias", type=str, help="the alias of the wallet") + sub.add_argument("--path", type=str, required=False, help="the absolute path to the wallet file") + sub.set_defaults(func=new_wallet_config) + + sub = cli_shared.add_command_subparser(subparsers, "config-wallet", "list", "List configured wallets") + sub.set_defaults(func=list_wallets) + + sub = cli_shared.add_command_subparser(subparsers, "config-wallet", "dump", "Dumps the active wallet.") + sub.set_defaults(func=dump) + + sub = cli_shared.add_command_subparser( + subparsers, "config-wallet", "get", "Gets a config value from the specified wallet." + ) + sub.add_argument("value", type=str, help="the value to get from the specified wallet (e.g. path)") + _add_alias_arg(sub) + sub.set_defaults(func=get_wallet_config_value) + + sub = cli_shared.add_command_subparser( + subparsers, "config-wallet", "set", "Sets a config value for the specified wallet." + ) + sub.add_argument("key", type=str, help="the key to set for the specified wallet (e.g. index)") + sub.add_argument("value", type=str, help="the value to set for the specified key") + _add_alias_arg(sub) + sub.set_defaults(func=set_wallet_config_value) + + sub = cli_shared.add_command_subparser( + subparsers, "config-wallet", "delete", "Deletes a config value from the specified wallet." + ) + sub.add_argument("value", type=str, help="the value to delete for the specified address") + _add_alias_arg(sub) + sub.set_defaults(func=delete_wallet_config_value) + + sub = cli_shared.add_command_subparser(subparsers, "config-wallet", "switch", "Switch to a different wallet.") + _add_alias_arg(sub) + sub.set_defaults(func=switch_wallet_to_active) + + sub = cli_shared.add_command_subparser( + subparsers, + "config-wallet", + "remove", + "Removes a wallet from the config using the alias. No default wallet will be set. Use `config-wallet switch` to set a new wallet.", + ) + _add_alias_arg(sub) + sub.set_defaults(func=remove_wallet) + + sub = cli_shared.add_command_subparser( + subparsers, + "config-wallet", + "reset", + "Deletes the config file. No default wallet will be set.", + ) + sub.set_defaults(func=delete_wallet_config_file) + + parser.epilog = cli_shared.build_group_epilog(subparsers) + return subparsers + + +def _add_alias_arg(sub: Any): + sub.add_argument("--alias", type=str, required=True, help="the alias of the wallet") + + +def new_wallet_config(args: Any): + create_new_wallet_config(name=args.alias, path=args.path) + dump_out_json(get_active_wallet()) + + +def list_wallets(args: Any): + _ensure_wallet_config_file_exists() + + data = read_wallet_config_file() + dump_out_json(data) + + +def dump(args: Any): + _ensure_wallet_config_file_exists() + dump_out_json(get_active_wallet()) + + +def get_wallet_config_value(args: Any): + _ensure_wallet_config_file_exists() + value = get_value(args.value, args.alias) + print(value) + + +def set_wallet_config_value(args: Any): + _ensure_wallet_config_file_exists() + set_value(args.key, args.value, args.alias) + + +def delete_wallet_config_value(args: Any): + _ensure_wallet_config_file_exists() + + delete_config_value(args.value, args.alias) + dump_out_json(get_active_wallet()) + + +def switch_wallet_to_active(args: Any): + _ensure_wallet_config_file_exists() + + switch_wallet(args.alias) + dump_out_json(get_active_wallet()) + + +def remove_wallet(args: Any): + _ensure_wallet_config_file_exists() + delete_alias(args.alias) + + +def delete_wallet_config_file(args: Any): + address_file = resolve_wallet_config_path() + if not address_file.is_file(): + logger.info("Wallet config file not found. Aborting...") + return + + confirm_continuation(f"The file `{str(address_file)}` will be deleted. Do you want to continue? (y/n)") + os.remove(address_file) + logger.info("Successfully deleted the address config file.") + + +def _ensure_wallet_config_file_exists(): + address_file = resolve_wallet_config_path() + if not address_file.is_file(): + logger.info("Wallet config file not found. Aborting...") + exit(1) diff --git a/multiversx_sdk_cli/cli_shared.py b/multiversx_sdk_cli/cli_shared.py index c7046f22..7b4d3c5c 100644 --- a/multiversx_sdk_cli/cli_shared.py +++ b/multiversx_sdk_cli/cli_shared.py @@ -22,17 +22,17 @@ ) from multiversx_sdk_cli import config, utils -from multiversx_sdk_cli.address_config import ( - get_active_address, - read_address_config_file, - resolve_address_config_path, -) from multiversx_sdk_cli.cli_output import CLIOutputBuilder from multiversx_sdk_cli.cli_password import ( load_guardian_password, load_password, load_relayer_password, ) +from multiversx_sdk_cli.config_wallet import ( + get_active_wallet, + read_wallet_config_file, + resolve_wallet_config_path, +) from multiversx_sdk_cli.constants import ( DEFAULT_GAS_PRICE, DEFAULT_TX_VERSION, @@ -44,10 +44,9 @@ ArgumentsNotProvidedError, BadUsage, IncorrectWalletError, - InvalidAddressConfigValue, LedgerError, NoWalletProvided, - UnknownAddressAliasError, + UnknownWalletAliasError, WalletError, ) from multiversx_sdk_cli.guardian_relayer_data import GuardianRelayerData @@ -369,53 +368,46 @@ def prepare_account(args: Any): def load_wallet_by_alias(alias: str, hrp: str) -> Account: - file_path = resolve_address_config_path() + file_path = resolve_wallet_config_path() if not file_path.is_file(): - raise AddressConfigFileError("The address config file was not found.") + raise AddressConfigFileError("The wallet config file was not found.") - file = read_address_config_file() + file = read_wallet_config_file() if file == dict(): - raise AddressConfigFileError("Address config file is empty.") + raise AddressConfigFileError("Wallet config file is empty.") - addresses: dict[str, Any] = file["addresses"] - wallet = addresses.get(alias, None) + wallets: dict[str, Any] = file["wallets"] + wallet = wallets.get(alias, None) if not wallet: - raise UnknownAddressAliasError(alias) + raise UnknownWalletAliasError(alias) - logger.info(f"Using sender [{alias}] from address config.") - return _load_wallet_from_address_config(wallet=wallet, hrp=hrp) + logger.info(f"Using sender [{alias}] from wallet config.") + return _load_wallet_from_wallet_config(wallet=wallet, hrp=hrp) def load_default_wallet(hrp: str) -> Account: - active_address = get_active_address() - if active_address == dict(): - logger.info("No default wallet found in address config.") + active_wallet = get_active_wallet() + if active_wallet == dict(): + logger.info("No default wallet found in wallet config.") raise NoWalletProvided() - alias_of_default_wallet = read_address_config_file().get("active", "") - logger.info(f"Using sender [{alias_of_default_wallet}] from address config.") - - return _load_wallet_from_address_config(wallet=active_address, hrp=hrp) + alias_of_default_wallet = read_wallet_config_file().get("active", "") + logger.info(f"Using sender [{alias_of_default_wallet}] from wallet config.") + return _load_wallet_from_wallet_config(wallet=active_wallet, hrp=hrp) -def _load_wallet_from_address_config(wallet: dict[str, str], hrp: str) -> Account: - kind = wallet.get("kind", None) - if not kind: - raise AddressConfigFileError("'kind' field must be set in the address config.") - - if kind not in ["pem", "keystore"]: - raise InvalidAddressConfigValue("'kind' must be 'pem' or 'keystore'") +def _load_wallet_from_wallet_config(wallet: dict[str, str], hrp: str) -> Account: wallet_path = wallet.get("path", None) if not wallet_path: - raise AddressConfigFileError("'path' field must be set in the address config.") + raise AddressConfigFileError("'path' field must be set in the wallet config.") path = Path(wallet_path) index = int(wallet.get("index", 0)) - if kind == "pem": + if path.suffix == ".pem": return Account.new_from_pem(file_path=path, index=index, hrp=hrp) - else: + elif path.suffix == ".json": logger.info(f"Using keystore wallet at: [{path}].") password = getpass("Please enter the wallet password: ") @@ -423,6 +415,8 @@ def _load_wallet_from_address_config(wallet: dict[str, str], hrp: str) -> Accoun return Account.new_from_keystore(file_path=path, password=password, address_index=index, hrp=hrp) except Exception as e: raise WalletError(str(e)) + else: + raise WalletError(f"Unsupported wallet file type: [{path.suffix}]. Supported types are: `.pem` and `.json`.") def _get_address_hrp(args: Any) -> str: diff --git a/multiversx_sdk_cli/config_wallet.py b/multiversx_sdk_cli/config_wallet.py new file mode 100644 index 00000000..71eb5745 --- /dev/null +++ b/multiversx_sdk_cli/config_wallet.py @@ -0,0 +1,177 @@ +from functools import cache +from pathlib import Path +from typing import Any, Optional + +from multiversx_sdk_cli.constants import SDK_PATH +from multiversx_sdk_cli.errors import ( + AliasAlreadyExistsError, + AliasProtectedError, + InvalidAddressConfigValue, + UnknownWalletAliasError, +) +from multiversx_sdk_cli.utils import read_json_file, write_json_file + +LOCAL_WALLET_CONFIG_PATH = Path("wallets.mxpy.json").resolve() +GLOBAL_WALLET_CONFIG_PATH = SDK_PATH / "wallets.mxpy.json" + + +def get_defaults() -> dict[str, str]: + """ + Not all values are required for a config to be valid. + + Valid config for PEM wallets: + ``` + { + "path": "/path/to/wallet.pem", + "index": "0" # optional, defaults to 0 + } + ``` + + Valid config for KEYSTORE wallets: + ``` + { + "path": "/path/to/wallet.json", + "index": "0" # optional, defaults to 0 + } + ``` + + For keystore wallets, you'll be prompted to enter the password when using the wallet. + """ + return { + "path": "", + "index": "0", + } + + +@cache +def get_value(name: str, alias: str) -> str: + _guard_valid_name(name) + data = read_wallet_config_file() + available_wallets = data.get("wallets", {}) + + wallet = available_wallets.get(alias, None) + if wallet is None: + raise UnknownWalletAliasError(alias) + + default_value = get_defaults()[name] + value = wallet.get(name, default_value) + assert isinstance(value, str) + return value + + +def _guard_valid_name(name: str): + if name not in get_defaults().keys(): + raise InvalidAddressConfigValue(f"Key is not present in wallet config: [{name}]") + + +def get_active_wallet() -> dict[str, str]: + """Returns the active wallet configuration.""" + data = read_wallet_config_file() + addresses: dict[str, Any] = data.get("wallets", {}) + active_address: str = data.get("active", "default") + result: dict[str, str] = addresses.get(active_address, {}) + + return result + + +@cache +def read_wallet_config_file() -> dict[str, Any]: + config_path = resolve_wallet_config_path() + if config_path.exists(): + data: dict[str, Any] = read_json_file(config_path) + return data + return dict() + + +def resolve_wallet_config_path() -> Path: + if LOCAL_WALLET_CONFIG_PATH.is_file(): + return LOCAL_WALLET_CONFIG_PATH + return GLOBAL_WALLET_CONFIG_PATH + + +def set_value(name: str, value: str, alias: str): + """Sets a key-value pair in the specified wallet config.""" + _guard_valid_name(name) + data = read_wallet_config_file() + available_wallets = data.get("wallets", {}) + + wallet = available_wallets.get(alias, None) + if wallet is None: + raise UnknownWalletAliasError(alias) + + wallet[name] = value + available_wallets[alias] = wallet + data["wallets"] = available_wallets + _write_file(data) + + +def _write_file(data: dict[str, Any]): + env_path = resolve_wallet_config_path() + write_json_file(str(env_path), data) + + +def switch_wallet(name: str): + """Switches to the wallet configuration with the given name.""" + data = read_wallet_config_file() + _guard_valid_wallet_name(data, name) + data["active"] = name + _write_file(data) + + +def _guard_valid_wallet_name(env: Any, name: str): + envs = env.get("wallets", {}) + if name not in envs: + raise UnknownWalletAliasError(name) + + +def create_new_wallet_config(name: str, path: Optional[str] = None): + """Creates a new wallet config with the given name and sets it as the default wallet.""" + data = read_wallet_config_file() + _guard_alias_unique(data, name) + new_wallet = {} + + if path: + new_wallet["path"] = path + + data["active"] = name + data.setdefault("wallets", {}) + data["wallets"][name] = new_wallet + _write_file(data) + + +def _guard_alias_unique(env: Any, name: str): + wallets = env.get("wallets", {}) + if name in wallets: + raise AliasAlreadyExistsError(name) + + +def delete_config_value(key: str, alias: str): + """Deletes a key-value pair of the specified wallet config.""" + _guard_valid_name(key) + + data = read_wallet_config_file() + available_wallets = data.get("wallets", {}) + + wallet = available_wallets.get(alias, None) + if wallet is None: + raise UnknownWalletAliasError(alias) + + del wallet[key] + available_wallets[alias] = wallet + data["wallets"] = available_wallets + _write_file(data) + + +def delete_alias(name: str): + """Deletes the wallet configuration with the given name.""" + _guard_valid_alias_deletion(name) + data = read_wallet_config_file() + data["wallets"].pop(name, None) + if data["active"] == name: + data["active"] = "default" + _write_file(data) + + +def _guard_valid_alias_deletion(name: str): + if name == "default": + raise AliasProtectedError(name) diff --git a/multiversx_sdk_cli/errors.py b/multiversx_sdk_cli/errors.py index 72275099..d03c7983 100644 --- a/multiversx_sdk_cli/errors.py +++ b/multiversx_sdk_cli/errors.py @@ -188,7 +188,7 @@ def __init__(self, name: str): super().__init__(f"Environment entry already exists: {name}.") -class UnknownAddressAliasError(KnownError): +class UnknownWalletAliasError(KnownError): def __init__(self, name: str): super().__init__(f"Alias is not known: {name}.") diff --git a/multiversx_sdk_cli/tests/test_cli_default_wallet.py b/multiversx_sdk_cli/tests/test_cli_default_wallet.py index d9631123..ba806e3c 100644 --- a/multiversx_sdk_cli/tests/test_cli_default_wallet.py +++ b/multiversx_sdk_cli/tests/test_cli_default_wallet.py @@ -6,15 +6,15 @@ from multiversx_sdk_cli.cli import main -def test_empty_address_config(capsys: Any, monkeypatch: Any, tmp_path: Path): - test_file = tmp_path / "addresses.mxpy.json" +def test_empty_wallet_config(capsys: Any, monkeypatch: Any, tmp_path: Path): + test_file = tmp_path / "wallets.mxpy.json" test_file.write_text("{}") - import multiversx_sdk_cli.address_config + import multiversx_sdk_cli.config_wallet - monkeypatch.setattr(multiversx_sdk_cli.address_config, "LOCAL_ADDRESS_CONFIG_PATH", test_file) - monkeypatch.setattr(multiversx_sdk_cli.address_config, "GLOBAL_ADDRESS_CONFIG_PATH", test_file) - multiversx_sdk_cli.address_config.read_address_config_file.cache_clear() + monkeypatch.setattr(multiversx_sdk_cli.config_wallet, "LOCAL_WALLET_CONFIG_PATH", test_file) + monkeypatch.setattr(multiversx_sdk_cli.config_wallet, "GLOBAL_WALLET_CONFIG_PATH", test_file) + multiversx_sdk_cli.config_wallet.read_wallet_config_file.cache_clear() return_code = main( [ @@ -55,19 +55,19 @@ def test_empty_address_config(capsys: Any, monkeypatch: Any, tmp_path: Path): ) assert return_code out = _read_stdout(capsys) - assert "Address config file is empty." in out + assert "Wallet config file is empty." in out def test_without_address_config(capsys: Any, monkeypatch: Any, tmp_path: Path): # Ensure the address config file does not exist; if the actual name is used, when running the tests locally, it will fail with a different error message - test_file = tmp_path / "test-addresses.mxpy.json" + test_file = tmp_path / "test-wallets.mxpy.json" assert not test_file.exists() - import multiversx_sdk_cli.address_config + import multiversx_sdk_cli.config_wallet - monkeypatch.setattr(multiversx_sdk_cli.address_config, "LOCAL_ADDRESS_CONFIG_PATH", test_file) - monkeypatch.setattr(multiversx_sdk_cli.address_config, "GLOBAL_ADDRESS_CONFIG_PATH", test_file) - multiversx_sdk_cli.address_config.read_address_config_file.cache_clear() + monkeypatch.setattr(multiversx_sdk_cli.config_wallet, "LOCAL_WALLET_CONFIG_PATH", test_file) + monkeypatch.setattr(multiversx_sdk_cli.config_wallet, "GLOBAL_WALLET_CONFIG_PATH", test_file) + multiversx_sdk_cli.config_wallet.read_wallet_config_file.cache_clear() return_code = main( [ @@ -108,86 +108,26 @@ def test_without_address_config(capsys: Any, monkeypatch: Any, tmp_path: Path): ) assert return_code out = _read_stdout(capsys) - assert "The address config file was not found." in out + assert "The wallet config file was not found." in out def test_incomplete_address_config(capsys: Any, monkeypatch: Any, tmp_path: Path): - test_file = tmp_path / "addresses.mxpy.json" - json_file = { - "active": "alice", - "addresses": { - "alice": { - "path": "/example/to/wallet.pem", - "index": "0", - }, - }, - } - test_file.write_text(json.dumps(json_file)) - - import multiversx_sdk_cli.address_config - - monkeypatch.setattr(multiversx_sdk_cli.address_config, "LOCAL_ADDRESS_CONFIG_PATH", test_file) - monkeypatch.setattr(multiversx_sdk_cli.address_config, "GLOBAL_ADDRESS_CONFIG_PATH", test_file) - multiversx_sdk_cli.address_config.read_address_config_file.cache_clear() - - return_code = main( - [ - "tx", - "new", - "--receiver", - "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx", - "--gas-limit", - "50000", - "--nonce", - "0", - "--chain", - "D", - ] - ) - assert return_code - out = _read_stdout(capsys) - assert "'kind' field must be set in the address config." in out - - # Clear the captured content - capsys.readouterr() - - return_code = main( - [ - "tx", - "new", - "--receiver", - "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx", - "--gas-limit", - "50000", - "--sender", - "alice", - "--nonce", - "0", - "--chain", - "D", - ] - ) - assert return_code - out = _read_stdout(capsys) - assert "'kind' field must be set in the address config." in out - - # Clear the captured content - capsys.readouterr() + test_file = tmp_path / "wallets.mxpy.json" + import multiversx_sdk_cli.config_wallet json_file = { "active": "alice", - "addresses": { + "wallets": { "alice": { - "kind": "pem", "index": "0", }, }, } test_file.write_text(json.dumps(json_file)) - monkeypatch.setattr(multiversx_sdk_cli.address_config, "LOCAL_ADDRESS_CONFIG_PATH", test_file) - monkeypatch.setattr(multiversx_sdk_cli.address_config, "GLOBAL_ADDRESS_CONFIG_PATH", test_file) - multiversx_sdk_cli.address_config.read_address_config_file.cache_clear() + monkeypatch.setattr(multiversx_sdk_cli.config_wallet, "LOCAL_WALLET_CONFIG_PATH", test_file) + monkeypatch.setattr(multiversx_sdk_cli.config_wallet, "GLOBAL_WALLET_CONFIG_PATH", test_file) + multiversx_sdk_cli.config_wallet.read_wallet_config_file.cache_clear() return_code = main( [ @@ -205,7 +145,7 @@ def test_incomplete_address_config(capsys: Any, monkeypatch: Any, tmp_path: Path ) assert return_code out = _read_stdout(capsys) - assert "'path' field must be set in the address config." in out + assert "'path' field must be set in the wallet config." in out # Clear the captured content capsys.readouterr() @@ -228,14 +168,14 @@ def test_incomplete_address_config(capsys: Any, monkeypatch: Any, tmp_path: Path ) assert return_code out = _read_stdout(capsys) - assert "'path' field must be set in the address config." in out + assert "'path' field must be set in the wallet config." in out # Clear the captured content capsys.readouterr() json_file = { "active": "alice", - "addresses": { + "wallets": { "alice": { "kind": "keystore", "path": "/example/to/wallet.json", @@ -245,8 +185,8 @@ def test_incomplete_address_config(capsys: Any, monkeypatch: Any, tmp_path: Path } test_file.write_text(json.dumps(json_file)) - monkeypatch.setattr(multiversx_sdk_cli.address_config, "LOCAL_ADDRESS_CONFIG_PATH", test_file) - multiversx_sdk_cli.address_config.read_address_config_file.cache_clear() + monkeypatch.setattr(multiversx_sdk_cli.config_wallet, "LOCAL_WALLET_CONFIG_PATH", test_file) + multiversx_sdk_cli.config_wallet.read_wallet_config_file.cache_clear() monkeypatch.setattr(cli_shared, "getpass", lambda *args, **kwargs: "")