diff --git a/multiversx_sdk_cli/cli.py b/multiversx_sdk_cli/cli.py index fe2e996b..b044c957 100644 --- a/multiversx_sdk_cli/cli.py +++ b/multiversx_sdk_cli/cli.py @@ -15,6 +15,7 @@ import multiversx_sdk_cli.cli_dns import multiversx_sdk_cli.cli_ledger import multiversx_sdk_cli.cli_localnet +import multiversx_sdk_cli.cli_multisig import multiversx_sdk_cli.cli_transactions import multiversx_sdk_cli.cli_validators import multiversx_sdk_cli.cli_wallet @@ -94,6 +95,7 @@ def setup_parser(args: List[str]): commands.append(multiversx_sdk_cli.cli_data.setup_parser(subparsers)) commands.append(multiversx_sdk_cli.cli_delegation.setup_parser(args, subparsers)) commands.append(multiversx_sdk_cli.cli_dns.setup_parser(args, subparsers)) + commands.append(multiversx_sdk_cli.cli_multisig.setup_parser(args, subparsers)) parser.epilog = """ ---------------------- diff --git a/multiversx_sdk_cli/cli_contracts.py b/multiversx_sdk_cli/cli_contracts.py index 53cdf3db..27cd12f5 100644 --- a/multiversx_sdk_cli/cli_contracts.py +++ b/multiversx_sdk_cli/cli_contracts.py @@ -17,8 +17,13 @@ from multiversx_sdk_cli.cosign_transaction import cosign_transaction from multiversx_sdk_cli.dependency_checker import check_if_rust_is_installed from multiversx_sdk_cli.docker import is_docker_installed, run_docker -from multiversx_sdk_cli.errors import DockerMissingError, NoWalletProvided +from multiversx_sdk_cli.errors import (BadUsage, DockerMissingError, + NoWalletProvided) from multiversx_sdk_cli.interfaces import IAddress +from multiversx_sdk_cli.multisig import ( + prepare_transaction_for_contract_call, + prepare_transaction_for_deploying_contract, + prepare_transaction_upgrading_contract) from multiversx_sdk_cli.projects.core import get_project_paths_recursively from multiversx_sdk_cli.projects.templates import Contract from multiversx_sdk_cli.ux import show_message @@ -85,6 +90,8 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: " - only valid if --wait-result is set") cli_shared.add_broadcast_args(sub) cli_shared.add_guardian_wallet_args(args, sub) + cli_shared.add_multisig_address_arg(sub) + add_contract_address_for_multisig_deploy(sub) sub.set_defaults(func=deploy) @@ -97,13 +104,14 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: cli_shared.add_tx_args(args, sub, with_receiver=False, with_data=False, with_guardian=True) _add_function_arg(sub) _add_arguments_arg(sub) - _add_token_transfers_args(sub) + cli_shared.add_token_transfers_arg(sub) sub.add_argument("--wait-result", action="store_true", default=False, help="signal to wait for the transaction result - only valid if --send is set") sub.add_argument("--timeout", default=100, help="max num of seconds to wait for result" " - only valid if --wait-result is set") cli_shared.add_broadcast_args(sub, relay=True) cli_shared.add_guardian_wallet_args(args, sub) + cli_shared.add_multisig_address_arg(sub) sub.set_defaults(func=call) @@ -123,6 +131,8 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: " - only valid if --wait-result is set") cli_shared.add_broadcast_args(sub) cli_shared.add_guardian_wallet_args(args, sub) + cli_shared.add_multisig_address_arg(sub) + add_contract_address_for_multisig_upgrade(sub) sub.set_defaults(func=upgrade) @@ -218,8 +228,8 @@ def _add_recursive_arg(sub: Any): def _add_bytecode_arg(sub: Any): - sub.add_argument("--bytecode", type=str, required=True, - help="the file containing the WASM bytecode") + sub.add_argument("--bytecode", type=str, + help="the file containing the WASM bytecode; not needed when deploying using a multisig contract") def _add_contract_arg(sub: Any): @@ -236,12 +246,6 @@ def _add_arguments_arg(sub: Any): "boolean] or hex-encoded. E.g. --arguments 42 0x64 1000 0xabba str:TOK-a1c2ef true erd1[..]") -def _add_token_transfers_args(sub: Any): - sub.add_argument("--token-transfers", nargs='+', - help="token transfers for transfer & execute, as [token, amount] " - "E.g. --token-transfers NFT-123456-0a 1 ESDT-987654 100000000") - - def _add_metadata_arg(sub: Any): sub.add_argument("--metadata-not-upgradeable", dest="metadata_upgradeable", action="store_false", help="‼ mark the contract as NOT upgradeable (default: upgradeable)") @@ -254,6 +258,14 @@ def _add_metadata_arg(sub: Any): sub.set_defaults(metadata_upgradeable=True, metadata_payable=False) +def add_contract_address_for_multisig_deploy(sub: Any): + sub.add_argument("--deployed-contract", help="the address of the already deployed contract to be re-deployed by the multisig") + + +def add_contract_address_for_multisig_upgrade(sub: Any): + sub.add_argument("--upgraded-contract", help="the address of the already upgraded contract, that will be used to upgrade the contract owned by the multisig") + + def list_templates(args: Any): tag = args.tag contract = Contract(tag) @@ -315,26 +327,60 @@ def deploy(args: Any): cli_shared.prepare_nonce_in_args(args) sender = cli_shared.prepare_account(args) - config = TransactionsFactoryConfig(args.chain) - contract = SmartContract(config) - address_computer = AddressComputer(NUMBER_OF_SHARDS) - contract_address = address_computer.compute_contract_address(deployer=sender.address, deployment_nonce=args.nonce) - - tx = contract.prepare_deploy_transaction( - owner=sender, - bytecode=Path(args.bytecode), - arguments=args.arguments, - upgradeable=args.metadata_upgradeable, - readable=args.metadata_readable, - payable=args.metadata_payable, - payable_by_sc=args.metadata_payable_by_sc, - gas_limit=int(args.gas_limit), - value=int(args.value), - nonce=int(args.nonce), - version=int(args.version), - options=int(args.options), - guardian=args.guardian) + + if args.multisig: + if not args.deployed_contract: + raise BadUsage("`--deployed-contract` needs to be provided when proposing a deploy action for the multisig contract") + + multisig_address = Address.new_from_bech32(args.multisig) + + if not args.proxy: + raise BadUsage("`--proxy` is required in order to compute the contract address") + + proxy = ProxyNetworkProvider(args.proxy) + multisig_nonce = proxy.get_account(multisig_address).nonce + contract_address = address_computer.compute_contract_address(deployer=multisig_address, deployment_nonce=multisig_nonce) + + tx = prepare_transaction_for_deploying_contract( + sender=sender, + multisig=Address.new_from_bech32(args.multisig), + deployed_contract=Address.new_from_bech32(args.deployed_contract), + arguments=args.arguments, + upgradeable=args.metadata_upgradeable, + readable=args.metadata_readable, + payable=args.metadata_payable, + payable_by_sc=args.metadata_payable_by_sc, + chain_id=args.chain, + value=int(args.value), + gas_limit=int(args.gas_limit), + nonce=int(args.nonce), + version=int(args.version), + options=int(args.options), + guardian=args.guardian) + else: + if not args.bytecode: + raise BadUsage("`--bytecode` is required when deploying a contract") + + contract_address = address_computer.compute_contract_address(deployer=sender.address, deployment_nonce=args.nonce) + config = TransactionsFactoryConfig(args.chain) + contract = SmartContract(config) + + tx = contract.prepare_deploy_transaction( + owner=sender, + bytecode=Path(args.bytecode), + arguments=args.arguments, + upgradeable=args.metadata_upgradeable, + readable=args.metadata_readable, + payable=args.metadata_payable, + payable_by_sc=args.metadata_payable_by_sc, + gas_limit=int(args.gas_limit), + value=int(args.value), + nonce=int(args.nonce), + version=int(args.version), + options=int(args.options), + guardian=args.guardian) + tx = _sign_guarded_tx(args, tx) logger.info("Contract address: %s", contract_address.to_bech32()) @@ -365,24 +411,41 @@ def call(args: Any): cli_shared.prepare_nonce_in_args(args) sender = cli_shared.prepare_account(args) - config = TransactionsFactoryConfig(args.chain) - contract = SmartContract(config) contract_address = Address.new_from_bech32(args.contract) - tx = contract.prepare_execute_transaction( - caller=sender, - contract=contract_address, - function=args.function, - arguments=args.arguments, - gas_limit=int(args.gas_limit), - value=int(args.value), - transfers=args.token_transfers, - nonce=int(args.nonce), - version=int(args.version), - options=int(args.options), - guardian=args.guardian) - tx = _sign_guarded_tx(args, tx) + if args.multisig: + tx = prepare_transaction_for_contract_call( + sender=sender, + contract_address=contract_address, + function=args.function, + arguments=args.arguments, + multisig=Address.new_from_bech32(args.multisig), + value=int(args.value), + transfers=args.token_transfers, + gas_limit=int(args.gas_limit), + chain_id=args.chain, + nonce=int(args.nonce), + version=int(args.version), + options=int(args.options), + guardian=args.guardian) + else: + config = TransactionsFactoryConfig(args.chain) + contract = SmartContract(config) + + tx = contract.prepare_execute_transaction( + caller=sender, + contract=contract_address, + function=args.function, + arguments=args.arguments, + gas_limit=int(args.gas_limit), + value=int(args.value), + transfers=args.token_transfers, + nonce=int(args.nonce), + version=int(args.version), + options=int(args.options), + guardian=args.guardian) + tx = _sign_guarded_tx(args, tx) _send_or_simulate(tx, contract_address, args) @@ -394,27 +457,53 @@ def upgrade(args: Any): cli_shared.prepare_nonce_in_args(args) sender = cli_shared.prepare_account(args) - config = TransactionsFactoryConfig(args.chain) - contract = SmartContract(config) contract_address = Address.new_from_bech32(args.contract) - tx = contract.prepare_upgrade_transaction( - owner=sender, - contract=contract_address, - bytecode=Path(args.bytecode), - arguments=args.arguments, - upgradeable=args.metadata_upgradeable, - readable=args.metadata_readable, - payable=args.metadata_payable, - payable_by_sc=args.metadata_payable_by_sc, - gas_limit=int(args.gas_limit), - value=int(args.value), - nonce=int(args.nonce), - version=int(args.version), - options=int(args.options), - guardian=args.guardian) - tx = _sign_guarded_tx(args, tx) + if args.multisig: + if not args.upgraded_contract: + raise BadUsage("`--upgraded-contract` needs to be provided when proposing an upgrade action for a contract owned by a multisig contract") + + tx = prepare_transaction_upgrading_contract( + sender=sender, + contract_address=Address.new_from_bech32(args.contract), + multisig=Address.new_from_bech32(args.multisig), + upgraded_contract=Address.new_from_bech32(args.upgraded_contract), + arguments=args.arguments, + upgradeable=args.metadata_upgradeable, + readable=args.metadata_readable, + payable=args.metadata_payable, + payable_by_sc=args.metadata_payable_by_sc, + chain_id=args.chain, + value=int(args.value), + gas_limit=int(args.gas_limit), + nonce=int(args.nonce), + version=int(args.version), + options=int(args.options), + guardian=args.guardian) + else: + if not args.bytecode: + raise BadUsage("`--bytecode` is required when upgrading a contract") + + config = TransactionsFactoryConfig(args.chain) + contract = SmartContract(config) + + tx = contract.prepare_upgrade_transaction( + owner=sender, + contract=contract_address, + bytecode=Path(args.bytecode), + arguments=args.arguments, + upgradeable=args.metadata_upgradeable, + readable=args.metadata_readable, + payable=args.metadata_payable, + payable_by_sc=args.metadata_payable_by_sc, + gas_limit=int(args.gas_limit), + value=int(args.value), + nonce=int(args.nonce), + version=int(args.version), + options=int(args.options), + guardian=args.guardian) + tx = _sign_guarded_tx(args, tx) _send_or_simulate(tx, contract_address, args) @@ -442,7 +531,7 @@ def _send_or_simulate(tx: Transaction, contract_address: IAddress, args: Any): def verify(args: Any) -> None: - contract = Address.from_bech32(args.contract) + contract = Address.new_from_bech32(args.contract) verifier_url = args.verifier_url packaged_src = Path(args.packaged_src).expanduser().resolve() diff --git a/multiversx_sdk_cli/cli_multisig.py b/multiversx_sdk_cli/cli_multisig.py new file mode 100644 index 00000000..bbdf8420 --- /dev/null +++ b/multiversx_sdk_cli/cli_multisig.py @@ -0,0 +1,516 @@ +import logging +from typing import Any, List + +from multiversx_sdk_core import Address +from multiversx_sdk_core.transaction_factories.transactions_factory_config import \ + TransactionsFactoryConfig + +from multiversx_sdk_cli import cli_shared +from multiversx_sdk_cli.contracts import SmartContract +from multiversx_sdk_cli.errors import BadUsage +from multiversx_sdk_cli.multisig import \ + prepare_transaction_for_depositing_funds +from multiversx_sdk_cli.transactions import (compute_relayed_v1_data, + sign_tx_by_guardian) + +logger = logging.getLogger("cli.multisig") + +MULTISIG_SIGN_ACTION_FUNCTION = "sign" +MULTISIG_UNSIGN_ACTION_FUNCTION = "unsign" +MULTISIG_PERFORM_ACTION_FUNCTION = "performAction" +MULTISIG_DISCARD_ACTION_FUNCTION = "discardAction" +MULTISIG_ADD_NEW_BOARD_MEMBER = "proposeAddBoardMember" +MULTISIG_ADD_PROPOSER = "proposeAddProposer" +MULTISIG_REMOVE_USER = "proposeRemoveUser" +MULTISIG_CHAGE_QUORUM = "proposeChangeQuorum" + + +def setup_parser(args: List[str], subparsers: Any) -> Any: + parser = cli_shared.add_group_subparser(subparsers, "multisig", "Interact with a multisig smart contract") + subparsers = parser.add_subparsers() + + sub = cli_shared.add_command_subparser(subparsers, "multisig", "sign", f"Sign a proposed action.") + cli_shared.add_multisig_address_arg(sub) + cli_shared.add_multisig_view_address_arg(sub) + cli_shared.add_multisig_action_arg(sub) + cli_shared.add_wallet_args(args, sub) + cli_shared.add_tx_args(args, sub, with_receiver=False, with_data=False, with_guardian=True) + + cli_shared.add_outfile_arg(sub, what="signed transaction, hash") + cli_shared.add_broadcast_args(sub, relay=True) + cli_shared.add_proxy_arg(sub) + cli_shared.add_guardian_wallet_args(args, sub) + sub.add_argument("--wait-result", action="store_true", default=False, + help="signal to wait for the transaction result - only valid if --send is set") + sub.add_argument("--timeout", default=100, help="max num of seconds to wait for result" + " - only valid if --wait-result is set") + sub.set_defaults(func=sign_action) + + sub = cli_shared.add_command_subparser(subparsers, "multisig", "unsign", f"Unsign a previously signed proposed action.") + cli_shared.add_multisig_address_arg(sub) + cli_shared.add_multisig_view_address_arg(sub) + cli_shared.add_multisig_action_arg(sub) + cli_shared.add_wallet_args(args, sub) + cli_shared.add_tx_args(args, sub, with_receiver=False, with_data=False, with_guardian=True) + + cli_shared.add_outfile_arg(sub, what="signed transaction, hash") + cli_shared.add_broadcast_args(sub, relay=True) + cli_shared.add_proxy_arg(sub) + cli_shared.add_guardian_wallet_args(args, sub) + sub.add_argument("--wait-result", action="store_true", default=False, + help="signal to wait for the transaction result - only valid if --send is set") + sub.add_argument("--timeout", default=100, help="max num of seconds to wait for result" + " - only valid if --wait-result is set") + sub.set_defaults(func=unsign_action) + + sub = cli_shared.add_command_subparser(subparsers, "multisig", "perform-action", f"Perform an action that has reached quorum.") + cli_shared.add_multisig_address_arg(sub) + cli_shared.add_multisig_view_address_arg(sub) + cli_shared.add_multisig_action_arg(sub) + cli_shared.add_wallet_args(args, sub) + cli_shared.add_tx_args(args, sub, with_receiver=False, with_data=False, with_guardian=True) + + cli_shared.add_outfile_arg(sub, what="signed transaction, hash") + cli_shared.add_broadcast_args(sub, relay=True) + cli_shared.add_proxy_arg(sub) + cli_shared.add_guardian_wallet_args(args, sub) + sub.add_argument("--wait-result", action="store_true", default=False, + help="signal to wait for the transaction result - only valid if --send is set") + sub.add_argument("--timeout", default=100, help="max num of seconds to wait for result" + " - only valid if --wait-result is set") + sub.set_defaults(func=perform_action) + + sub = cli_shared.add_command_subparser(subparsers, "multisig", "discard-action", f"Discard a proposed action.") + cli_shared.add_multisig_address_arg(sub) + cli_shared.add_multisig_view_address_arg(sub) + cli_shared.add_multisig_action_arg(sub) + cli_shared.add_wallet_args(args, sub) + cli_shared.add_tx_args(args, sub, with_receiver=False, with_data=False, with_guardian=True) + + cli_shared.add_outfile_arg(sub, what="signed transaction, hash") + cli_shared.add_broadcast_args(sub, relay=True) + cli_shared.add_proxy_arg(sub) + cli_shared.add_guardian_wallet_args(args, sub) + sub.add_argument("--wait-result", action="store_true", default=False, + help="signal to wait for the transaction result - only valid if --send is set") + sub.add_argument("--timeout", default=100, help="max num of seconds to wait for result" + " - only valid if --wait-result is set") + sub.set_defaults(func=discard_action) + + sub = cli_shared.add_command_subparser(subparsers, "multisig", "deposit", f"Deposit assets into the multisig contract.") + cli_shared.add_multisig_address_arg(sub) + cli_shared.add_multisig_view_address_arg(sub) + cli_shared.add_wallet_args(args, sub) + cli_shared.add_tx_args(args, sub, with_receiver=False, with_data=False, with_guardian=True) + cli_shared.add_token_transfers_arg(sub) + + cli_shared.add_outfile_arg(sub, what="signed transaction, hash") + cli_shared.add_broadcast_args(sub, relay=True) + cli_shared.add_proxy_arg(sub) + cli_shared.add_guardian_wallet_args(args, sub) + sub.add_argument("--wait-result", action="store_true", default=False, + help="signal to wait for the transaction result - only valid if --send is set") + sub.add_argument("--timeout", default=100, help="max num of seconds to wait for result" + " - only valid if --wait-result is set") + sub.set_defaults(func=deposit_funds) + + sub = cli_shared.add_command_subparser(subparsers, "multisig", "add-board-member", f"Propose an action to add a new board member.") + cli_shared.add_multisig_address_arg(sub) + _add_member_arg(sub) + cli_shared.add_multisig_view_address_arg(sub) + cli_shared.add_wallet_args(args, sub) + cli_shared.add_tx_args(args, sub, with_receiver=False, with_data=False, with_guardian=True) + + cli_shared.add_outfile_arg(sub, what="signed transaction, hash") + cli_shared.add_broadcast_args(sub, relay=True) + cli_shared.add_proxy_arg(sub) + cli_shared.add_guardian_wallet_args(args, sub) + sub.add_argument("--wait-result", action="store_true", default=False, + help="signal to wait for the transaction result - only valid if --send is set") + sub.add_argument("--timeout", default=100, help="max num of seconds to wait for result" + " - only valid if --wait-result is set") + sub.set_defaults(func=propose_add_board_member) + + sub = cli_shared.add_command_subparser(subparsers, "multisig", "add-proposer", f"Propose an action to add a new proposer.") + cli_shared.add_multisig_address_arg(sub) + _add_member_arg(sub) + cli_shared.add_multisig_view_address_arg(sub) + cli_shared.add_wallet_args(args, sub) + cli_shared.add_tx_args(args, sub, with_receiver=False, with_data=False, with_guardian=True) + + cli_shared.add_outfile_arg(sub, what="signed transaction, hash") + cli_shared.add_broadcast_args(sub, relay=True) + cli_shared.add_proxy_arg(sub) + cli_shared.add_guardian_wallet_args(args, sub) + sub.add_argument("--wait-result", action="store_true", default=False, + help="signal to wait for the transaction result - only valid if --send is set") + sub.add_argument("--timeout", default=100, help="max num of seconds to wait for result" + " - only valid if --wait-result is set") + sub.set_defaults(func=propose_add_proposer) + + sub = cli_shared.add_command_subparser(subparsers, "multisig", "remove-user", f"Propose an action to remove an user.") + cli_shared.add_multisig_address_arg(sub) + _add_member_arg(sub) + cli_shared.add_multisig_view_address_arg(sub) + cli_shared.add_wallet_args(args, sub) + cli_shared.add_tx_args(args, sub, with_receiver=False, with_data=False, with_guardian=True) + + cli_shared.add_outfile_arg(sub, what="signed transaction, hash") + cli_shared.add_broadcast_args(sub, relay=True) + cli_shared.add_proxy_arg(sub) + cli_shared.add_guardian_wallet_args(args, sub) + sub.add_argument("--wait-result", action="store_true", default=False, + help="signal to wait for the transaction result - only valid if --send is set") + sub.add_argument("--timeout", default=100, help="max num of seconds to wait for result" + " - only valid if --wait-result is set") + sub.set_defaults(func=propose_remove_user) + + sub = cli_shared.add_command_subparser(subparsers, "multisig", "quorum", f"Propose an action to change the quorum size.") + cli_shared.add_multisig_address_arg(sub) + _add_quorum_arg(sub) + cli_shared.add_multisig_view_address_arg(sub) + cli_shared.add_wallet_args(args, sub) + cli_shared.add_tx_args(args, sub, with_receiver=False, with_data=False, with_guardian=True) + + cli_shared.add_outfile_arg(sub, what="signed transaction, hash") + cli_shared.add_broadcast_args(sub, relay=True) + cli_shared.add_proxy_arg(sub) + cli_shared.add_guardian_wallet_args(args, sub) + sub.add_argument("--wait-result", action="store_true", default=False, + help="signal to wait for the transaction result - only valid if --send is set") + sub.add_argument("--timeout", default=100, help="max num of seconds to wait for result" + " - only valid if --wait-result is set") + sub.set_defaults(func=propose_change_quorum_size) + + parser.epilog = cli_shared.build_group_epilog(subparsers) + return subparsers + + +def _add_member_arg(sub: Any): + sub.add_argument("--proposed-member", required=True, help="the address of the proposed member") + + +def _add_quorum_arg(sub: Any): + sub.add_argument("--quorum-size", required=True, help="the proposed quorum size") + + +def sign_action(args: Any): + cli_shared.check_guardian_and_options_args(args) + cli_shared.check_broadcast_args(args) + cli_shared.prepare_chain_id_in_args(args) + cli_shared.prepare_nonce_in_args(args) + + config = TransactionsFactoryConfig(args.chain) + contract = SmartContract(config) + + sender = cli_shared.prepare_account(args) + contract_address = Address.new_from_bech32(args.multisig) + + action_id = args.action_id + if action_id == "all": + raise BadUsage("`all` is not supported at the moment. Please use a specific action id") + + tx = contract.prepare_execute_transaction( + caller=sender, + contract=contract_address, + function=MULTISIG_SIGN_ACTION_FUNCTION, + arguments=[args.action_id], + gas_limit=int(args.gas_limit), + value=int(args.value), + transfers=None, + nonce=int(args.nonce), + version=int(args.version), + options=int(args.options), + guardian=args.guardian) + + if tx.guardian: + tx = sign_tx_by_guardian(args, tx) + + if hasattr(args, "relay") and args.relay: + args.outfile.write(compute_relayed_v1_data(tx)) + return + + cli_shared.send_or_simulate(tx, args) + + +def unsign_action(args: Any): + cli_shared.check_guardian_and_options_args(args) + cli_shared.check_broadcast_args(args) + cli_shared.prepare_chain_id_in_args(args) + cli_shared.prepare_nonce_in_args(args) + + config = TransactionsFactoryConfig(args.chain) + contract = SmartContract(config) + + sender = cli_shared.prepare_account(args) + contract_address = Address.new_from_bech32(args.multisig) + + action_id = args.action_id + if action_id == "all": + raise BadUsage("`all` is not supported at the moment. Please use a specific action id") + + tx = contract.prepare_execute_transaction( + caller=sender, + contract=contract_address, + function=MULTISIG_UNSIGN_ACTION_FUNCTION, + arguments=[args.action_id], + gas_limit=int(args.gas_limit), + value=int(args.value), + transfers=None, + nonce=int(args.nonce), + version=int(args.version), + options=int(args.options), + guardian=args.guardian) + + if tx.guardian: + tx = sign_tx_by_guardian(args, tx) + + if hasattr(args, "relay") and args.relay: + args.outfile.write(compute_relayed_v1_data(tx)) + return + + cli_shared.send_or_simulate(tx, args) + + +def perform_action(args: Any): + cli_shared.check_guardian_and_options_args(args) + cli_shared.check_broadcast_args(args) + cli_shared.prepare_chain_id_in_args(args) + cli_shared.prepare_nonce_in_args(args) + + config = TransactionsFactoryConfig(args.chain) + contract = SmartContract(config) + + sender = cli_shared.prepare_account(args) + contract_address = Address.new_from_bech32(args.multisig) + + action_id = args.action_id + if action_id == "all": + raise BadUsage("`all` is not supported at the moment. Please use a specific action id") + + tx = contract.prepare_execute_transaction( + caller=sender, + contract=contract_address, + function=MULTISIG_PERFORM_ACTION_FUNCTION, + arguments=[args.action_id], + gas_limit=int(args.gas_limit), + value=int(args.value), + transfers=None, + nonce=int(args.nonce), + version=int(args.version), + options=int(args.options), + guardian=args.guardian) + + if tx.guardian: + tx = sign_tx_by_guardian(args, tx) + + if hasattr(args, "relay") and args.relay: + args.outfile.write(compute_relayed_v1_data(tx)) + return + + cli_shared.send_or_simulate(tx, args) + + +def discard_action(args: Any): + cli_shared.check_guardian_and_options_args(args) + cli_shared.check_broadcast_args(args) + cli_shared.prepare_chain_id_in_args(args) + cli_shared.prepare_nonce_in_args(args) + + config = TransactionsFactoryConfig(args.chain) + contract = SmartContract(config) + + sender = cli_shared.prepare_account(args) + contract_address = Address.new_from_bech32(args.multisig) + + action_id = args.action_id + if action_id == "all": + raise BadUsage("`all` is not supported at the moment. Please use a specific action id") + + tx = contract.prepare_execute_transaction( + caller=sender, + contract=contract_address, + function=MULTISIG_DISCARD_ACTION_FUNCTION, + arguments=[args.action_id], + gas_limit=int(args.gas_limit), + value=int(args.value), + transfers=None, + nonce=int(args.nonce), + version=int(args.version), + options=int(args.options), + guardian=args.guardian) + + if tx.guardian: + tx = sign_tx_by_guardian(args, tx) + + if hasattr(args, "relay") and args.relay: + args.outfile.write(compute_relayed_v1_data(tx)) + return + + cli_shared.send_or_simulate(tx, args) + + +def deposit_funds(args: Any): + cli_shared.check_guardian_and_options_args(args) + cli_shared.check_broadcast_args(args) + cli_shared.prepare_chain_id_in_args(args) + cli_shared.prepare_nonce_in_args(args) + + sender = cli_shared.prepare_account(args) + + tx = prepare_transaction_for_depositing_funds( + sender=sender, + multisig=args.multisig, + chain_id=args.chain, + value=int(args.value), + transfers=args.token_transfers, + gas_limit=int(args.gas_limit), + nonce=int(args.nonce), + version=int(args.version), + options=int(args.options), + guardian=args.guardian) + + if tx.guardian: + tx = sign_tx_by_guardian(args, tx) + + if hasattr(args, "relay") and args.relay: + args.outfile.write(compute_relayed_v1_data(tx)) + return + + cli_shared.send_or_simulate(tx, args) + + +def propose_add_board_member(args: Any): + cli_shared.check_guardian_and_options_args(args) + cli_shared.check_broadcast_args(args) + cli_shared.prepare_chain_id_in_args(args) + cli_shared.prepare_nonce_in_args(args) + + sender = cli_shared.prepare_account(args) + + config = TransactionsFactoryConfig(args.chain) + contract = SmartContract(config) + + tx = contract.prepare_execute_transaction( + caller=sender, + contract=Address.new_from_bech32(args.multisig), + function=MULTISIG_ADD_NEW_BOARD_MEMBER, + arguments=[args.proposed_member], + gas_limit=int(args.gas_limit), + value=int(args.value), + transfers=None, + nonce=int(args.nonce), + version=int(args.version), + options=int(args.options), + guardian=args.guardian) + + if tx.guardian: + tx = sign_tx_by_guardian(args, tx) + + if hasattr(args, "relay") and args.relay: + args.outfile.write(compute_relayed_v1_data(tx)) + return + + cli_shared.send_or_simulate(tx, args) + + +def propose_add_proposer(args: Any): + cli_shared.check_guardian_and_options_args(args) + cli_shared.check_broadcast_args(args) + cli_shared.prepare_chain_id_in_args(args) + cli_shared.prepare_nonce_in_args(args) + + sender = cli_shared.prepare_account(args) + + config = TransactionsFactoryConfig(args.chain) + contract = SmartContract(config) + + tx = contract.prepare_execute_transaction( + caller=sender, + contract=Address.new_from_bech32(args.multisig), + function=MULTISIG_ADD_PROPOSER, + arguments=[args.proposed_member], + gas_limit=int(args.gas_limit), + value=int(args.value), + transfers=None, + nonce=int(args.nonce), + version=int(args.version), + options=int(args.options), + guardian=args.guardian) + + if tx.guardian: + tx = sign_tx_by_guardian(args, tx) + + if hasattr(args, "relay") and args.relay: + args.outfile.write(compute_relayed_v1_data(tx)) + return + + cli_shared.send_or_simulate(tx, args) + + +def propose_remove_user(args: Any): + cli_shared.check_guardian_and_options_args(args) + cli_shared.check_broadcast_args(args) + cli_shared.prepare_chain_id_in_args(args) + cli_shared.prepare_nonce_in_args(args) + + sender = cli_shared.prepare_account(args) + + config = TransactionsFactoryConfig(args.chain) + contract = SmartContract(config) + + tx = contract.prepare_execute_transaction( + caller=sender, + contract=Address.new_from_bech32(args.multisig), + function=MULTISIG_REMOVE_USER, + arguments=[args.proposed_member], + gas_limit=int(args.gas_limit), + value=int(args.value), + transfers=None, + nonce=int(args.nonce), + version=int(args.version), + options=int(args.options), + guardian=args.guardian) + + if tx.guardian: + tx = sign_tx_by_guardian(args, tx) + + if hasattr(args, "relay") and args.relay: + args.outfile.write(compute_relayed_v1_data(tx)) + return + + cli_shared.send_or_simulate(tx, args) + + +def propose_change_quorum_size(args: Any): + cli_shared.check_guardian_and_options_args(args) + cli_shared.check_broadcast_args(args) + cli_shared.prepare_chain_id_in_args(args) + cli_shared.prepare_nonce_in_args(args) + + sender = cli_shared.prepare_account(args) + + config = TransactionsFactoryConfig(args.chain) + contract = SmartContract(config) + + tx = contract.prepare_execute_transaction( + caller=sender, + contract=Address.new_from_bech32(args.multisig), + function=MULTISIG_CHAGE_QUORUM, + arguments=[args.quorum_size], + gas_limit=int(args.gas_limit), + value=int(args.value), + transfers=None, + nonce=int(args.nonce), + version=int(args.version), + options=int(args.options), + guardian=args.guardian) + + if tx.guardian: + tx = sign_tx_by_guardian(args, tx) + + if hasattr(args, "relay") and args.relay: + args.outfile.write(compute_relayed_v1_data(tx)) + return + + cli_shared.send_or_simulate(tx, args) diff --git a/multiversx_sdk_cli/cli_shared.py b/multiversx_sdk_cli/cli_shared.py index 02005c06..af91217b 100644 --- a/multiversx_sdk_cli/cli_shared.py +++ b/multiversx_sdk_cli/cli_shared.py @@ -137,6 +137,24 @@ def add_omit_fields_arg(sub: Any): sub.add_argument("--omit-fields", default="[]", type=str, required=False, help="omit fields in the output payload (default: %(default)s)") +def add_token_transfers_arg(sub: Any): + sub.add_argument("--token-transfers", nargs='+', + help="token transfers for transfer & execute, as [token, amount] " + "E.g. --token-transfers NFT-123456-0a 1 ESDT-987654 100000000") + + +def add_multisig_address_arg(sub: Any): + sub.add_argument("--multisig", help="the address of the multisig contract") + + +def add_multisig_view_address_arg(sub: Any): + sub.add_argument("--multisig-view", help="the address of the multisig-view contract") + + +def add_multisig_action_arg(sub: Any): + sub.add_argument("--action-id", help="an integer representing the ID of the action; can also be `all`") + + def parse_omit_fields_arg(args: Any) -> List[str]: literal = args.omit_fields parsed = ast.literal_eval(literal) diff --git a/multiversx_sdk_cli/cli_transactions.py b/multiversx_sdk_cli/cli_transactions.py index 6850ac8e..63d4c37a 100644 --- a/multiversx_sdk_cli/cli_transactions.py +++ b/multiversx_sdk_cli/cli_transactions.py @@ -7,10 +7,14 @@ from multiversx_sdk_cli import cli_shared, utils from multiversx_sdk_cli.cli_output import CLIOutputBuilder from multiversx_sdk_cli.cosign_transaction import cosign_transaction -from multiversx_sdk_cli.errors import NoWalletProvided +from multiversx_sdk_cli.errors import BadUsage, NoWalletProvided +from multiversx_sdk_cli.multisig import ( + prepare_transaction_for_custom_token_transfer, + prepare_transaction_for_egld_transfer) from multiversx_sdk_cli.transactions import (compute_relayed_v1_data, do_prepare_transaction, - load_transaction_from_file) + load_transaction_from_file, + sign_tx_by_guardian) def setup_parser(args: List[str], subparsers: Any) -> Any: @@ -23,6 +27,8 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: cli_shared.add_broadcast_args(sub, relay=True) cli_shared.add_proxy_arg(sub) cli_shared.add_guardian_wallet_args(args, sub) + cli_shared.add_multisig_address_arg(sub) + cli_shared.add_token_transfers_arg(sub) sub.add_argument("--wait-result", action="store_true", default=False, help="signal to wait for the transaction result - only valid if --send is set") sub.add_argument("--timeout", default=100, help="max num of seconds to wait for result" @@ -71,10 +77,46 @@ def create_transaction(args: Any): cli_shared.prepare_chain_id_in_args(args) cli_shared.prepare_nonce_in_args(args) - if args.data_file: - args.data = Path(args.data_file).read_text() - - tx = do_prepare_transaction(args) + if args.multisig: + if args.data: + raise BadUsage("`--data` should not be provided when interacting with a multisig") + + sender = cli_shared.prepare_account(args) + + if int(args.value): + tx = prepare_transaction_for_egld_transfer( + sender=sender, + multisig=args.multisig, + receiver=args.receiver, + chain_id=args.chain, + value=int(args.value), + gas_limit=int(args.gas_limit), + nonce=int(args.nonce), + version=int(args.version), + options=int(args.options), + guardian=args.guardian + ) + else: + tx = prepare_transaction_for_custom_token_transfer( + sender=sender, + multisig=args.multisig, + receiver=args.receiver, + chain_id=args.chain, + transfers=args.token_transfers, + gas_limit=int(args.gas_limit), + nonce=int(args.nonce), + version=int(args.version), + options=int(args.options), + guardian=args.guardian) + + if tx.guardian: + tx = sign_tx_by_guardian(args, tx) + + else: + if args.data_file: + args.data = Path(args.data_file).read_text() + + tx = do_prepare_transaction(args) if hasattr(args, "relay") and args.relay: args.outfile.write(compute_relayed_v1_data(tx)) diff --git a/multiversx_sdk_cli/contracts.py b/multiversx_sdk_cli/contracts.py index 251ffbae..4e78f615 100644 --- a/multiversx_sdk_cli/contracts.py +++ b/multiversx_sdk_cli/contracts.py @@ -113,7 +113,7 @@ def prepare_deploy_transaction(self, def prepare_execute_transaction(self, caller: Account, - contract: Address, + contract: IAddress, function: str, arguments: Union[List[str], None], gas_limit: int, @@ -123,7 +123,10 @@ def prepare_execute_transaction(self, version: int, options: int, guardian: str) -> Transaction: - token_transfers = self._prepare_token_transfers(transfers) if transfers else [] + if value and transfers: + raise errors.BadUsage("Can't send both native and custom tokens") + + token_transfers = self.prepare_token_transfers(transfers) if transfers else [] args = prepare_args_for_factory(arguments) if arguments else [] tx = self._factory.create_transaction_for_execute( @@ -180,7 +183,7 @@ def prepare_upgrade_transaction(self, return tx - def _prepare_token_transfers(self, transfers: List[str]) -> List[TokenTransfer]: + def prepare_token_transfers(self, transfers: List[str]) -> List[TokenTransfer]: token_computer = TokenComputer() token_transfers: List[TokenTransfer] = [] @@ -305,7 +308,7 @@ def _to_hex(arg: str): if arg.isnumeric(): return _prepare_decimal(arg) elif arg.startswith(DEFAULT_HRP): - addr = Address.from_bech32(arg) + addr = Address.new_from_bech32(arg) return _prepare_hexadecimal(f"{HEX_PREFIX}{addr.hex()}") elif arg.lower() == FALSE_STR_LOWER or arg.lower() == TRUE_STR_LOWER: as_str = f"{HEX_PREFIX}01" if arg.lower() == TRUE_STR_LOWER else f"{HEX_PREFIX}00" diff --git a/multiversx_sdk_cli/dns.py b/multiversx_sdk_cli/dns.py index 54485a48..901b739c 100644 --- a/multiversx_sdk_cli/dns.py +++ b/multiversx_sdk_cli/dns.py @@ -26,8 +26,8 @@ def resolve(name: str, proxy: INetworkProvider) -> Address: result = query_contract(dns_address, proxy, "resolve", [name_arg]) if len(result) == 0: - return Address.from_bech32(ADDRESS_ZERO_BECH32) - return Address.from_hex(result[0].hex, DEFAULT_HRP) + return Address.new_from_bech32(ADDRESS_ZERO_BECH32) + return Address.new_from_hex(result[0].hex, DEFAULT_HRP) def validate_name(name: str, shard_id: int, proxy: INetworkProvider): diff --git a/multiversx_sdk_cli/multisig.py b/multiversx_sdk_cli/multisig.py new file mode 100644 index 00000000..be9837f8 --- /dev/null +++ b/multiversx_sdk_cli/multisig.py @@ -0,0 +1,329 @@ +from typing import Any, List, Union + +from multiversx_sdk_core import (Address, CodeMetadata, TokenComputer, + TokenTransfer, Transaction) +from multiversx_sdk_core.serializer import arg_to_string, args_to_strings +from multiversx_sdk_core.transaction_factories.token_transfers_data_builder import \ + TokenTransfersDataBuilder +from multiversx_sdk_core.transaction_factories.transactions_factory_config import \ + TransactionsFactoryConfig + +from multiversx_sdk_cli.accounts import Account +from multiversx_sdk_cli.contracts import (SmartContract, + prepare_args_for_factory) +from multiversx_sdk_cli.errors import BadUsage +from multiversx_sdk_cli.interfaces import IAddress + +MULTISIG_DEPOSIT_FUNCTION = "deposit" +MULTISIG_TRANSFER_AND_EXECUTE = "proposeTransferExecute" +MULTISIG_ASYNC_CALL = "proposeAsyncCall" +MULTISIG_DEPLOY_FUNCTION = "proposeSCDeployFromSource" +MULTISIG_UPGRADE_FUNCTION = "proposeSCUpgradeFromSource" + + +def prepare_transaction_for_egld_transfer(sender: Account, + multisig: str, + receiver: str, + chain_id: str, + value: int, + gas_limit: int, + nonce: int, + version: int, + options: int, + guardian: str) -> Transaction: + config = TransactionsFactoryConfig(chain_id) + contract = SmartContract(config) + + return contract.prepare_execute_transaction( + caller=sender, + contract=Address.new_from_bech32(multisig), + function=MULTISIG_TRANSFER_AND_EXECUTE, + arguments=[f"{receiver}", f"{value}"], + gas_limit=gas_limit, + value=0, + transfers=None, + nonce=nonce, + version=version, + options=options, + guardian=guardian) + + +def prepare_transaction_for_custom_token_transfer(sender: Account, + multisig: str, + receiver: str, + chain_id: str, + transfers: List[str], + gas_limit: int, + nonce: int, + version: int, + options: int, + guardian: str) -> Transaction: + config = TransactionsFactoryConfig(chain_id) + contract = SmartContract(config) + + token_transfers = contract.prepare_token_transfers(transfers) + transfer_receiver = Address.new_from_bech32(receiver) + transfer_data_parts = _prepare_data_parts_for_multisig_transfer(transfer_receiver, token_transfers) + multisig_contract = Address.new_from_bech32(multisig) + + arguments: List[str] = [transfer_receiver.to_hex(), "00"] + if transfer_data_parts[0] != "ESDTTransfer": + arguments[0] = multisig_contract.to_hex() + + transfer_data_parts[0] = arg_to_string(transfer_data_parts[0]) + arguments.extend(transfer_data_parts) + + tx = contract.prepare_execute_transaction( + caller=sender, + contract=multisig_contract, + function=MULTISIG_ASYNC_CALL, + arguments=None, + gas_limit=gas_limit, + value=0, + transfers=None, + nonce=nonce, + version=version, + options=options, + guardian=guardian) + + data_field = tx.data.decode() + "@" + _build_data_payload(arguments) + tx.data = data_field.encode() + tx.signature = bytes.fromhex(sender.sign_transaction(tx)) + return tx + + +def prepare_transaction_for_depositing_funds(sender: Account, + multisig: str, + chain_id: str, + value: int, + transfers: Union[List[str], None], + gas_limit: int, + nonce: int, + version: int, + options: int, + guardian: str) -> Transaction: + config = TransactionsFactoryConfig(chain_id) + contract = SmartContract(config) + + return contract.prepare_execute_transaction( + caller=sender, + contract=Address.new_from_bech32(multisig), + function=MULTISIG_DEPOSIT_FUNCTION, + arguments=None, + gas_limit=gas_limit, + value=value, + transfers=transfers, + nonce=nonce, + version=version, + options=options, + guardian=guardian) + + +def prepare_transaction_for_deploying_contract(sender: Account, + multisig: IAddress, + deployed_contract: IAddress, + arguments: Union[List[str], None], + upgradeable: bool, + readable: bool, + payable: bool, + payable_by_sc: bool, + chain_id: str, + value: int, + gas_limit: int, + nonce: int, + version: int, + options: int, + guardian: str) -> Transaction: + # convert the args to proper type instead of strings + prepared_arguments = prepare_args_for_factory(arguments) if arguments else [] + metadata = CodeMetadata(upgradeable, readable, payable, payable_by_sc) + + data = _prepare_data_field_for_deploy_transaction(amount=value, + deployed_contract=deployed_contract, + metadata=metadata, + arguments=prepared_arguments) + tx = Transaction( + sender=sender.address.to_bech32(), + receiver=multisig.to_bech32(), + gas_limit=gas_limit, + chain_id=chain_id, + nonce=nonce, + amount=0, + data=data, + version=version, + options=options, + guardian=guardian + ) + tx.signature = bytes.fromhex(sender.sign_transaction(tx)) + + return tx + + +def prepare_transaction_upgrading_contract(sender: Account, + contract_address: IAddress, + multisig: IAddress, + upgraded_contract: IAddress, + arguments: Union[List[str], None], + upgradeable: bool, + readable: bool, + payable: bool, + payable_by_sc: bool, + chain_id: str, + value: int, + gas_limit: int, + nonce: int, + version: int, + options: int, + guardian: str) -> Transaction: + # convert the args to proper type instead of strings + prepared_arguments = prepare_args_for_factory(arguments) if arguments else [] + metadata = CodeMetadata(upgradeable, readable, payable, payable_by_sc) + + data = _prepare_data_field_for_upgrade_transaction(contract_address=contract_address, + amount=value, + upgraded_contract=upgraded_contract, + metadata=metadata, + arguments=prepared_arguments) + tx = Transaction( + sender=sender.address.to_bech32(), + receiver=multisig.to_bech32(), + gas_limit=gas_limit, + chain_id=chain_id, + nonce=nonce, + amount=0, + data=data, + version=version, + options=options, + guardian=guardian + ) + tx.signature = bytes.fromhex(sender.sign_transaction(tx)) + + return tx + + +def prepare_transaction_for_contract_call(sender: Account, + contract_address: IAddress, + function: str, + arguments: Union[List[str], None], + multisig: IAddress, + value: int, + transfers: Union[List[str], None], + gas_limit: int, + chain_id: str, + nonce: int, + version: int, + options: int, + guardian: str) -> Transaction: + if value and transfers: + raise BadUsage("Can't send both native and custom tokens") + + config = TransactionsFactoryConfig(chain_id) + contract = SmartContract(config) + + token_transfers = contract.prepare_token_transfers(transfers) if transfers else [] + prepared_args = prepare_args_for_factory(arguments) if arguments else [] + + data_field = _prepare_data_field_for_contract_call(contract_address=contract_address, + multisig=multisig, + function=function, + arguments=prepared_args, + value=value, + token_transfers=token_transfers) + tx = Transaction( + sender=sender.address.to_bech32(), + receiver=multisig.to_bech32(), + gas_limit=gas_limit, + chain_id=chain_id, + nonce=nonce, + amount=0, + data=data_field, + version=version, + options=options, + guardian=guardian + ) + tx.signature = bytes.fromhex(sender.sign_transaction(tx)) + + return tx + + +def _prepare_data_field_for_contract_call(contract_address: IAddress, + multisig: IAddress, + function: str, + arguments: List[Any], + value: int, + token_transfers: List[TokenTransfer]): + data_parts = [ + MULTISIG_ASYNC_CALL, + contract_address.to_hex(), + arg_to_string(value) + ] + + transfer_data_parts = _prepare_data_parts_for_multisig_transfer(receiver=contract_address, token_transfers=token_transfers) + + if transfer_data_parts: + if transfer_data_parts[0] != "ESDTTransfer": + data_parts[1] = multisig.to_hex() + + transfer_data_parts[0] = arg_to_string(transfer_data_parts[0]) + data_parts.extend(transfer_data_parts) + + data_parts.append(arg_to_string(function)) + data_parts.extend(args_to_strings(arguments)) + + data_field = _build_data_payload(data_parts) + return data_field.encode() + + +def _prepare_data_field_for_upgrade_transaction(contract_address: IAddress, + amount: int, + upgraded_contract: IAddress, + metadata: CodeMetadata, + arguments: List[Any]) -> bytes: + data_parts = [ + MULTISIG_UPGRADE_FUNCTION, + contract_address.to_hex(), + arg_to_string(amount), + upgraded_contract.to_hex(), + str(metadata) + ] + data_parts.extend(args_to_strings(arguments)) + payload = _build_data_payload(data_parts) + + return payload.encode() + + +def _prepare_data_field_for_deploy_transaction(amount: int, + deployed_contract: IAddress, + metadata: CodeMetadata, + arguments: List[Any]) -> bytes: + data_parts = [ + MULTISIG_DEPLOY_FUNCTION, + arg_to_string(amount), + deployed_contract.to_hex(), + str(metadata) + ] + data_parts.extend(args_to_strings(arguments)) + payload = _build_data_payload(data_parts) + + return payload.encode() + + +def _prepare_data_parts_for_multisig_transfer(receiver: IAddress, token_transfers: List[TokenTransfer]) -> List[str]: + token_computer = TokenComputer() + data_builder = TokenTransfersDataBuilder(token_computer) + data_parts: List[str] = [] + + if len(token_transfers) == 1: + transfer = token_transfers[0] + if token_computer.is_fungible(transfer.token): + data_parts = data_builder.build_args_for_esdt_transfer(transfer=transfer) + else: + data_parts = data_builder.build_args_for_single_esdt_nft_transfer(transfer=transfer, receiver=receiver) + elif len(token_transfers) > 1: + data_parts = data_builder.build_args_for_multi_esdt_nft_transfer(receiver=receiver, transfers=token_transfers) + + return data_parts + + +def _build_data_payload(parts: List[str]) -> str: + return "@".join(parts) diff --git a/multiversx_sdk_cli/sign_verify.py b/multiversx_sdk_cli/sign_verify.py index 240a843e..f49f0971 100644 --- a/multiversx_sdk_cli/sign_verify.py +++ b/multiversx_sdk_cli/sign_verify.py @@ -24,7 +24,7 @@ def verify_signature(self) -> bool: verifiable_message.signature = bytes.fromhex(self.signature) message_computer = MessageComputer() - verifier = UserVerifier.from_address(Address.from_bech32(self.address)) + verifier = UserVerifier.from_address(Address.new_from_bech32(self.address)) is_signed = verifier.verify(message_computer.compute_bytes_for_signing(verifiable_message), verifiable_message.signature) return is_signed diff --git a/multiversx_sdk_cli/tests/test_cli_multisig.py b/multiversx_sdk_cli/tests/test_cli_multisig.py new file mode 100644 index 00000000..3814b595 --- /dev/null +++ b/multiversx_sdk_cli/tests/test_cli_multisig.py @@ -0,0 +1,628 @@ +import base64 +import json +from pathlib import Path +from typing import Any, Dict + +from multiversx_sdk_cli.cli import main + +parent = Path(__file__).parent +alice = parent / "testdata" / "alice.pem" + + +def test_sign_action(capsys: Any): + return_code = main([ + "multisig", "sign", + "--multisig", "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30", + "--action-id", "2", + "--pem", str(alice), + "--nonce", "1289", + "--chain", "T", + "--gas-limit", "10000000" + ]) + assert False if return_code else True + + transaction = get_transaction(capsys) + + data_field: str = transaction["data"] + data = base64.b64decode(data_field.encode()).decode() + assert data == "sign@02" + + receiver = transaction["receiver"] + assert receiver == "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + + chain_id = transaction["chainID"] + assert chain_id == "T" + + +def test_unsign_action(capsys: Any): + return_code = main([ + "multisig", "unsign", + "--multisig", "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30", + "--action-id", "2", + "--pem", str(alice), + "--nonce", "1289", + "--chain", "T", + "--gas-limit", "10000000" + ]) + assert False if return_code else True + + transaction = get_transaction(capsys) + + data_field: str = transaction["data"] + data = base64.b64decode(data_field.encode()).decode() + assert data == "unsign@02" + + receiver = transaction["receiver"] + assert receiver == "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + + chain_id = transaction["chainID"] + assert chain_id == "T" + + +def test_perform_action(capsys: Any): + return_code = main([ + "multisig", "perform-action", + "--multisig", "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30", + "--action-id", "2", + "--pem", str(alice), + "--nonce", "1290", + "--chain", "T", + "--gas-limit", "10000000" + ]) + assert False if return_code else True + + transaction = get_transaction(capsys) + + data_field: str = transaction["data"] + data = base64.b64decode(data_field.encode()).decode() + assert data == "performAction@02" + + receiver = transaction["receiver"] + assert receiver == "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + + chain_id = transaction["chainID"] + assert chain_id == "T" + + +def test_discard_action(capsys: Any): + return_code = main([ + "multisig", "discard-action", + "--multisig", "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30", + "--action-id", "15", + "--pem", str(alice), + "--nonce", "55", + "--chain", "T", + "--gas-limit", "10000000" + ]) + assert False if return_code else True + + transaction = get_transaction(capsys) + + data_field: str = transaction["data"] + data = base64.b64decode(data_field.encode()).decode() + assert data == "discardAction@0f" + + receiver = transaction["receiver"] + assert receiver == "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + + chain_id = transaction["chainID"] + assert chain_id == "T" + + +def test_deposit_egld(capsys: Any): + return_code = main([ + "multisig", "deposit", + "--multisig", "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30", + "--pem", str(alice), + "--nonce", "1449", + "--chain", "T", + "--gas-limit", "10000000", + "--value", "50000000000000000" + ]) + assert False if return_code else True + + transaction = get_transaction(capsys) + + data_field: str = transaction["data"] + data = base64.b64decode(data_field.encode()).decode() + assert data == "deposit" + + receiver = transaction["receiver"] + assert receiver == "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + + chain_id = transaction["chainID"] + assert chain_id == "T" + + value = int(transaction["value"]) + assert value == 50000000000000000 + + signature = transaction["signature"] + assert signature == "ba823e12c7eba1cb8e7c41f7f4042d54742a8162114875150e0f9d1e3535fdb74e7a89adfbda4360cd9f02de531facaf2a60a8f53ae48765ff2c7ae685f61704" + + +def test_deposit_esdt(capsys: Any): + return_code = main([ + "multisig", "deposit", + "--multisig", "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30", + "--pem", str(alice), + "--nonce", "1525", + "--chain", "T", + "--gas-limit", "10000000", + "--token-transfers", "TST-267761", "1000" + ]) + assert False if return_code else True + + transaction = get_transaction(capsys) + + data_field: str = transaction["data"] + data = base64.b64decode(data_field.encode()).decode() + assert data == "ESDTTransfer@5453542d323637373631@03e8@6465706f736974" + + receiver = transaction["receiver"] + assert receiver == "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + + chain_id = transaction["chainID"] + assert chain_id == "T" + + value = int(transaction["value"]) + assert value == 0 + + signature = transaction["signature"] + assert signature == "4bb4421783ba2b19060d6d7b84bcdda484f475a52eab6eb44679af2cac1dfae0c10552efd84ef7bdae028a40bc656f30e52984aa4f90581700377e44c4d4810b" + + +def test_deposit_multi_esdt(capsys: Any): + return_code = main([ + "multisig", "deposit", + "--multisig", "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30", + "--pem", str(alice), + "--nonce", "1531", + "--chain", "T", + "--gas-limit", "10000000", + "--token-transfers", "TST-267761", "1700", "ZZZ-9ee87d", "1200" + ]) + assert False if return_code else True + + transaction = get_transaction(capsys) + + data_field: str = transaction["data"] + data = base64.b64decode(data_field.encode()).decode() + assert data == "MultiESDTNFTTransfer@000000000000000005000a2a0f13340978c2eea268a5a2dcf917012978f61f5c@02@5453542d323637373631@@06a4@5a5a5a2d396565383764@@04b0@6465706f736974" + + receiver = transaction["receiver"] + assert receiver == "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th" + + chain_id = transaction["chainID"] + assert chain_id == "T" + + value = int(transaction["value"]) + assert value == 0 + + signature = transaction["signature"] + assert signature == "034e8644e4363640224c20556b9a3abb3aef36d59697754a44e9d0cbe26e31de8bd78d4529b5c5a3b74a7e51a14f0f62e10f01386318b005f384c69e885c960c" + + +def test_propose_egld_transfer(capsys: Any): + return_code = main([ + "tx", "new", + "--pem", str(alice), + "--receiver", "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx", + "--nonce", "1429", + "--chain", "T", + "--gas-limit", "10000000", + "--multisig", "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30", + "--value", "1000000000000000" + ]) + assert False if return_code else True + + transaction = get_transaction(capsys) + + data_field: str = transaction["data"] + data = base64.b64decode(data_field.encode()).decode() + assert data == "proposeTransferExecute@8049d639e5a6980d1cd2392abcce41029cda74a1563523a202f09641cc2618f8@038d7ea4c68000" + + receiver = transaction["receiver"] + assert receiver == "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + + chain_id = transaction["chainID"] + assert chain_id == "T" + + value = int(transaction["value"]) + assert value == 0 + + signature = transaction["signature"] + assert signature == "285edffe65006f738ce6fff640ddd9cb69c7380a219ec3549cb35744cd1106ffd41005b8d471899eb1db55e556b042db7bab0830a5250860348fb101d644c805" + + +def test_propose_esdt_transfer(capsys: Any): + return_code = main([ + "tx", "new", + "--pem", str(alice), + "--receiver", "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx", + "--nonce", "1427", + "--chain", "T", + "--gas-limit", "10000000", + "--multisig", "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30", + "--token-transfers", "TST-267761", "10" + ]) + assert False if return_code else True + + transaction = get_transaction(capsys) + + data_field: str = transaction["data"] + data = base64.b64decode(data_field.encode()).decode() + assert data == "proposeAsyncCall@8049d639e5a6980d1cd2392abcce41029cda74a1563523a202f09641cc2618f8@00@455344545472616e73666572@5453542d323637373631@0a" + + receiver = transaction["receiver"] + assert receiver == "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + + chain_id = transaction["chainID"] + assert chain_id == "T" + + value = int(transaction["value"]) + assert value == 0 + + signature = transaction["signature"] + assert signature == "dc80e70409fce3a20bd5c80ef1f1039a0474729ddb27188afacbd2d8237294172d5bf8b4bc759e48a2e5c724983dbb6ba5f17b48bb7a8d2dfc4c076a113fa50f" + + +def test_propose_multi_esdt_nft_transfer(capsys: Any): + return_code = main([ + "tx", "new", + "--pem", str(alice), + "--receiver", "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx", + "--nonce", "1434", + "--chain", "T", + "--gas-limit", "10000000", + "--multisig", "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30", + "--token-transfers", "TST-267761", "10", "ZZZ-9ee87d", "10000" + ]) + assert False if return_code else True + + transaction = get_transaction(capsys) + + data_field: str = transaction["data"] + data = base64.b64decode(data_field.encode()).decode() + assert data == "proposeAsyncCall@000000000000000005000a2a0f13340978c2eea268a5a2dcf917012978f61f5c@00@4d756c7469455344544e46545472616e73666572@8049d639e5a6980d1cd2392abcce41029cda74a1563523a202f09641cc2618f8@02@5453542d323637373631@@0a@5a5a5a2d396565383764@@2710" + + receiver = transaction["receiver"] + assert receiver == "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + + chain_id = transaction["chainID"] + assert chain_id == "T" + + value = int(transaction["value"]) + assert value == 0 + + signature = transaction["signature"] + assert signature == "563fc6eefe9469cf90191462cfe21ab25ae7291c1c411cd4b3778717c827045eabc58b689308e8ee45676a8e49cf75c2856a83e41e93dad5c4acb5ccb65c5b04" + + +def test_propose_contract_deploy_from_source(capsys: Any): + return_code = main([ + "contract", "deploy", + "--pem", str(alice), + "--nonce", "60", + "--chain", "T", + "--proxy", "https://testnet-api.multiversx.com", + "--gas-limit", "100000000", + "--multisig", "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30", + "--deployed-contract", "erd1qqqqqqqqqqqqqpgq8z2zzyu30f4607hth0tfj5m3vpjvwrvvrawqw09jem", + "--arguments", "0" + ]) + assert False if return_code else True + + transaction = get_transaction(capsys) + + data_field: str = transaction["data"] + data = base64.b64decode(data_field.encode()).decode() + assert data == "proposeSCDeployFromSource@@0000000000000000050038942113917a6ba7faebbbd69953716064c70d8c1f5c@0500@" + + receiver = transaction["receiver"] + assert receiver == "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + + chain_id = transaction["chainID"] + assert chain_id == "T" + + value = int(transaction["value"]) + assert value == 0 + + +def test_propose_contract_upgrade_from_source(capsys: Any): + return_code = main([ + "contract", "upgrade", "erd1qqqqqqqqqqqqqpgqz0kha878srg82eznjhdyvgarwycwjgs6rawq02lh6j", + "--pem", str(alice), + "--nonce", "6241", + "--chain", "T", + "--gas-limit", "100000000", + "--multisig", "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30", + "--upgraded-contract", "erd1qqqqqqqqqqqqqpgq8z2zzyu30f4607hth0tfj5m3vpjvwrvvrawqw09jem", + "--arguments", "0" + ]) + assert False if return_code else True + + transaction = get_transaction(capsys) + + data_field: str = transaction["data"] + data = base64.b64decode(data_field.encode()).decode() + assert data == "proposeSCUpgradeFromSource@0000000000000000050013ed7e9fc780d075645395da4623a37130e9221a1f5c@@0000000000000000050038942113917a6ba7faebbbd69953716064c70d8c1f5c@0500@" + + receiver = transaction["receiver"] + assert receiver == "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + + chain_id = transaction["chainID"] + assert chain_id == "T" + + value = int(transaction["value"]) + assert value == 0 + + +def test_propose_contract_call_no_transfer(capsys: Any): + return_code = main([ + "contract", "call", "erd1qqqqqqqqqqqqqpgq8z2zzyu30f4607hth0tfj5m3vpjvwrvvrawqw09jem", + "--pem", str(alice), + "--nonce", "9550", + "--chain", "T", + "--gas-limit", "100000000", + "--function", "add", + "--arguments", "10", + "--multisig", "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + ]) + assert False if return_code else True + + transaction = get_transaction(capsys) + + data_field: str = transaction["data"] + data = base64.b64decode(data_field.encode()).decode() + assert data == "proposeAsyncCall@0000000000000000050038942113917a6ba7faebbbd69953716064c70d8c1f5c@@616464@0a" + + receiver = transaction["receiver"] + assert receiver == "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + + chain_id = transaction["chainID"] + assert chain_id == "T" + + value = int(transaction["value"]) + assert value == 0 + + +def test_propose_contract_call_with_egld_transfer(capsys: Any): + return_code = main([ + "contract", "call", "erd1qqqqqqqqqqqqqpgq8z2zzyu30f4607hth0tfj5m3vpjvwrvvrawqw09jem", + "--pem", str(alice), + "--nonce", "9552", + "--chain", "T", + "--value", "1000000000000000", + "--gas-limit", "100000000", + "--function", "add", + "--arguments", "10", + "--multisig", "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + ]) + assert False if return_code else True + + transaction = get_transaction(capsys) + + data_field: str = transaction["data"] + data = base64.b64decode(data_field.encode()).decode() + assert data == "proposeAsyncCall@0000000000000000050038942113917a6ba7faebbbd69953716064c70d8c1f5c@038d7ea4c68000@616464@0a" + + receiver = transaction["receiver"] + assert receiver == "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + + chain_id = transaction["chainID"] + assert chain_id == "T" + + value = int(transaction["value"]) + assert value == 0 + + +def test_propose_contract_call_with_esdt_transfer(capsys: Any): + return_code = main([ + "contract", "call", "erd1qqqqqqqqqqqqqpgq8z2zzyu30f4607hth0tfj5m3vpjvwrvvrawqw09jem", + "--pem", str(alice), + "--nonce", "9553", + "--chain", "T", + "--token-transfers", "ZZZ-9ee87d", "1000", + "--gas-limit", "100000000", + "--function", "add", + "--arguments", "10", + "--multisig", "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + ]) + assert False if return_code else True + + transaction = get_transaction(capsys) + + data_field: str = transaction["data"] + data = base64.b64decode(data_field.encode()).decode() + assert data == "proposeAsyncCall@0000000000000000050038942113917a6ba7faebbbd69953716064c70d8c1f5c@@455344545472616e73666572@5a5a5a2d396565383764@03e8@616464@0a" + + receiver = transaction["receiver"] + assert receiver == "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + + chain_id = transaction["chainID"] + assert chain_id == "T" + + value = int(transaction["value"]) + assert value == 0 + + +def test_propose_contract_call_with_multi_esdt_transfer(capsys: Any): + return_code = main([ + "contract", "call", "erd1qqqqqqqqqqqqqpgq8z2zzyu30f4607hth0tfj5m3vpjvwrvvrawqw09jem", + "--pem", str(alice), + "--nonce", "9554", + "--chain", "T", + "--token-transfers", "ZZZ-9ee87d", "1300", "TST-267761", "600", + "--gas-limit", "100000000", + "--function", "add", + "--arguments", "10", + "--multisig", "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + ]) + assert False if return_code else True + + transaction = get_transaction(capsys) + + data_field: str = transaction["data"] + data = base64.b64decode(data_field.encode()).decode() + assert data == "proposeAsyncCall@000000000000000005000a2a0f13340978c2eea268a5a2dcf917012978f61f5c@@4d756c7469455344544e46545472616e73666572@0000000000000000050038942113917a6ba7faebbbd69953716064c70d8c1f5c@02@5a5a5a2d396565383764@@0514@5453542d323637373631@@0258@616464@0a" + + receiver = transaction["receiver"] + assert receiver == "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + + chain_id = transaction["chainID"] + assert chain_id == "T" + + value = int(transaction["value"]) + assert value == 0 + + +def test_propose_contract_call_with_multi_esdt_nft_transfer(capsys: Any): + return_code = main([ + "contract", "call", "erd1qqqqqqqqqqqqqpgq8z2zzyu30f4607hth0tfj5m3vpjvwrvvrawqw09jem", + "--pem", str(alice), + "--nonce", "9555", + "--chain", "T", + "--token-transfers", "ZZZ-9ee87d", "700", "METATEST-e05d11-01", "1500", + "--gas-limit", "100000000", + "--function", "add", + "--arguments", "10", + "--multisig", "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + ]) + assert False if return_code else True + + transaction = get_transaction(capsys) + + data_field: str = transaction["data"] + data = base64.b64decode(data_field.encode()).decode() + assert data == "proposeAsyncCall@000000000000000005000a2a0f13340978c2eea268a5a2dcf917012978f61f5c@@4d756c7469455344544e46545472616e73666572@0000000000000000050038942113917a6ba7faebbbd69953716064c70d8c1f5c@02@5a5a5a2d396565383764@@02bc@4d455441544553542d653035643131@01@05dc@616464@0a" + + receiver = transaction["receiver"] + assert receiver == "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + + chain_id = transaction["chainID"] + assert chain_id == "T" + + value = int(transaction["value"]) + assert value == 0 + + +def test_propose_add_board_member(capsys: Any): + return_code = main([ + "multisig", "add-board-member", + "--multisig", "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30", + "--proposed-member", "erd1fggp5ru0jhcjrp5rjqyqrnvhr3sz3v2e0fm3ktknvlg7mcyan54qzccnan", + "--pem", str(alice), + "--nonce", "12243", + "--chain", "T", + "--gas-limit", "10000000" + ]) + assert False if return_code else True + + transaction = get_transaction(capsys) + + data_field: str = transaction["data"] + data = base64.b64decode(data_field.encode()).decode() + assert data == "proposeAddBoardMember@4a101a0f8f95f1218683900801cd971c6028b1597a771b2ed367d1ede09d9d2a" + + receiver = transaction["receiver"] + assert receiver == "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + + chain_id = transaction["chainID"] + assert chain_id == "T" + + value = int(transaction["value"]) + assert value == 0 + + +def test_propose_add_proposer(capsys: Any): + return_code = main([ + "multisig", "add-proposer", + "--multisig", "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30", + "--proposed-member", "erd1fggp5ru0jhcjrp5rjqyqrnvhr3sz3v2e0fm3ktknvlg7mcyan54qzccnan", + "--pem", str(alice), + "--nonce", "12244", + "--chain", "T", + "--gas-limit", "10000000" + ]) + assert False if return_code else True + + transaction = get_transaction(capsys) + + data_field: str = transaction["data"] + data = base64.b64decode(data_field.encode()).decode() + assert data == "proposeAddProposer@4a101a0f8f95f1218683900801cd971c6028b1597a771b2ed367d1ede09d9d2a" + + receiver = transaction["receiver"] + assert receiver == "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + + chain_id = transaction["chainID"] + assert chain_id == "T" + + value = int(transaction["value"]) + assert value == 0 + + +def test_propose_remove_user(capsys: Any): + return_code = main([ + "multisig", "remove-user", + "--multisig", "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30", + "--proposed-member", "erd1fggp5ru0jhcjrp5rjqyqrnvhr3sz3v2e0fm3ktknvlg7mcyan54qzccnan", + "--pem", str(alice), + "--nonce", "12245", + "--chain", "T", + "--gas-limit", "10000000" + ]) + assert False if return_code else True + + transaction = get_transaction(capsys) + + data_field: str = transaction["data"] + data = base64.b64decode(data_field.encode()).decode() + assert data == "proposeRemoveUser@4a101a0f8f95f1218683900801cd971c6028b1597a771b2ed367d1ede09d9d2a" + + receiver = transaction["receiver"] + assert receiver == "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + + chain_id = transaction["chainID"] + assert chain_id == "T" + + value = int(transaction["value"]) + assert value == 0 + + +def test_propose_change_quorum_size(capsys: Any): + return_code = main([ + "multisig", "quorum", + "--multisig", "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30", + "--quorum-size", "2", + "--pem", str(alice), + "--nonce", "12247", + "--chain", "T", + "--gas-limit", "10000000" + ]) + assert False if return_code else True + + transaction = get_transaction(capsys) + + data_field: str = transaction["data"] + data = base64.b64decode(data_field.encode()).decode() + assert data == "proposeChangeQuorum@02" + + receiver = transaction["receiver"] + assert receiver == "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + + chain_id = transaction["chainID"] + assert chain_id == "T" + + value = int(transaction["value"]) + assert value == 0 + + +def get_transaction(capsys: Any) -> Dict[str, Any]: + out = _read_stdout(capsys) + output = json.loads(out) + return output["emittedTransaction"] + + +def _read_stdout(capsys: Any) -> str: + return capsys.readouterr().out.strip() diff --git a/multiversx_sdk_cli/tests/test_contracts.py b/multiversx_sdk_cli/tests/test_contracts.py index 733969a3..2cc7e251 100644 --- a/multiversx_sdk_cli/tests/test_contracts.py +++ b/multiversx_sdk_cli/tests/test_contracts.py @@ -56,7 +56,7 @@ def test_prepare_argument(): def test_contract_verification_create_request_signature(): account = Account(pem_file=str(testdata_folder / "walletKey.pem")) - contract_address = Address.from_bech32("erd1qqqqqqqqqqqqqpgqeyj9g344pqguukajpcfqz9p0rfqgyg4l396qespdck") + contract_address = Address.new_from_bech32("erd1qqqqqqqqqqqqqpgqeyj9g344pqguukajpcfqz9p0rfqgyg4l396qespdck") request_payload = b"test" signature = _create_request_signature(account, contract_address, request_payload) diff --git a/multiversx_sdk_cli/transactions.py b/multiversx_sdk_cli/transactions.py index 418a469d..5d026236 100644 --- a/multiversx_sdk_cli/transactions.py +++ b/multiversx_sdk_cli/transactions.py @@ -30,7 +30,7 @@ class INetworkProvider(Protocol): def send_transaction(self, transaction: ITransaction) -> str: ... - def send_transactions(self, transactions: Sequence[ITransaction]) -> Tuple[int, str]: + def send_transactions(self, transactions: Sequence[ITransaction]) -> Tuple[int, Dict[str, str]]: ... def get_transaction(self, tx_hash: str, with_process_status: Optional[bool] = False) -> ITransactionOnNetwork: @@ -114,7 +114,7 @@ def get_guardian_account_from_args(args: Any): account = Account(key_file=args.guardian_keyfile, password=password) elif args.guardian_ledger: address = do_get_ledger_address(account_index=args.guardian_ledger_account_index, address_index=args.guardian_ledger_address_index) - account = Account(address=Address.from_bech32(address)) + account = Account(address=Address.new_from_bech32(address)) else: raise errors.NoWalletProvided()