diff --git a/multiversx_sdk_cli/base_transactions_controller.py b/multiversx_sdk_cli/base_transactions_controller.py index 858d81ca..49a6fb62 100644 --- a/multiversx_sdk_cli/base_transactions_controller.py +++ b/multiversx_sdk_cli/base_transactions_controller.py @@ -1,14 +1,32 @@ -from typing import Optional, Union +import logging +from typing import Any, Optional, Union -from multiversx_sdk import LedgerAccount, Transaction, TransactionComputer +from multiversx_sdk import Address, LedgerAccount, Transaction, TransactionComputer +from multiversx_sdk.abi import ( + AddressValue, + BigUIntValue, + BoolValue, + BytesValue, + StringValue, +) +from multiversx_sdk_cli.config import get_address_hrp from multiversx_sdk_cli.constants import ( + ADDRESS_PREFIX, EXTRA_GAS_LIMIT_FOR_GUARDED_TRANSACTIONS, EXTRA_GAS_LIMIT_FOR_RELAYED_TRANSACTIONS, + FALSE_STR_LOWER, + HEX_PREFIX, + MAINCHAIN_ADDRESS_HRP, + STR_PREFIX, + TRUE_STR_LOWER, ) from multiversx_sdk_cli.cosign_transaction import cosign_transaction +from multiversx_sdk_cli.errors import BadUserInput from multiversx_sdk_cli.interfaces import IAccount +logger = logging.getLogger("base_controller") + class BaseTransactionsController: def __init__(self) -> None: @@ -84,3 +102,45 @@ def _sign_guarded_transaction_if_guardian( def _sign_relayed_transaction_if_relayer(self, transaction: Transaction, relayer: Union[IAccount, None]): if relayer and transaction.relayer: transaction.relayer_signature = relayer.sign_transaction(transaction) + + def _convert_args_to_typed_values(self, arguments: list[str]) -> list[Any]: + args: list[Any] = [] + + for arg in arguments: + if arg.startswith(HEX_PREFIX): + args.append(BytesValue(self._hex_to_bytes(arg))) + elif arg.isnumeric(): + args.append(BigUIntValue(int(arg))) + elif arg.startswith(ADDRESS_PREFIX): + args.append(AddressValue.new_from_address(Address.new_from_bech32(arg[len(ADDRESS_PREFIX) :]))) + elif arg.startswith(MAINCHAIN_ADDRESS_HRP): + # this flow will be removed in the future + logger.warning( + "Address argument has no prefix. This flow will be removed in the future. Please provide each address using the `addr:` prefix. (e.g. --arguments addr:erd1...)" + ) + args.append(AddressValue.new_from_address(Address.new_from_bech32(arg))) + elif arg.startswith(get_address_hrp()): + args.append(AddressValue.new_from_address(Address.new_from_bech32(arg))) + elif arg.lower() == FALSE_STR_LOWER: + args.append(BoolValue(False)) + elif arg.lower() == TRUE_STR_LOWER: + args.append(BoolValue(True)) + elif arg.startswith(STR_PREFIX): + args.append(StringValue(arg[len(STR_PREFIX) :])) + else: + raise BadUserInput( + f"Unknown argument type for argument: `{arg}`. Use `mxpy contract --help` to check all supported arguments" + ) + + return args + + def _hex_to_bytes(self, arg: str): + argument = arg[len(HEX_PREFIX) :] + argument = argument.upper() + argument = self.ensure_even_length(argument) + return bytes.fromhex(argument) + + def ensure_even_length(self, string: str) -> str: + if len(string) % 2 == 1: + return "0" + string + return string diff --git a/multiversx_sdk_cli/cli.py b/multiversx_sdk_cli/cli.py index 00519941..41d3641f 100644 --- a/multiversx_sdk_cli/cli.py +++ b/multiversx_sdk_cli/cli.py @@ -18,6 +18,7 @@ import multiversx_sdk_cli.cli_faucet 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_validator_wallet import multiversx_sdk_cli.cli_validators @@ -122,6 +123,7 @@ def setup_parser(args: list[str]): 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_faucet.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 7714d374..eaaf2ad9 100644 --- a/multiversx_sdk_cli/cli_contracts.py +++ b/multiversx_sdk_cli/cli_contracts.py @@ -53,7 +53,7 @@ def setup_parser(args: list[str], subparsers: Any) -> Any: ) _add_bytecode_arg(sub) _add_contract_abi_arg(sub) - _add_metadata_arg(sub) + cli_shared.add_metadata_arg(sub) cli_shared.add_outfile_arg(sub) cli_shared.add_wallet_args(args, sub) cli_shared.add_proxy_arg(sub) @@ -118,7 +118,7 @@ def setup_parser(args: list[str], subparsers: Any) -> Any: _add_contract_abi_arg(sub) cli_shared.add_outfile_arg(sub) _add_bytecode_arg(sub) - _add_metadata_arg(sub) + cli_shared.add_metadata_arg(sub) cli_shared.add_wallet_args(args, sub) cli_shared.add_proxy_arg(sub) cli_shared.add_tx_args(args, sub, with_receiver=False, with_data=False) @@ -304,34 +304,6 @@ def _add_arguments_arg(sub: Any): ) -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)", - ) - sub.add_argument( - "--metadata-not-readable", - dest="metadata_readable", - action="store_false", - help="‼ mark the contract as NOT readable (default: readable)", - ) - sub.add_argument( - "--metadata-payable", - dest="metadata_payable", - action="store_true", - help="‼ mark the contract as payable (default: not payable)", - ) - sub.add_argument( - "--metadata-payable-by-sc", - dest="metadata_payable_by_sc", - action="store_true", - help="‼ mark the contract as payable by SC (default: not payable by SC)", - ) - sub.set_defaults(metadata_upgradeable=True, metadata_payable=False) - - def build(args: Any): message = """This command cannot build smart contracts anymore. @@ -413,6 +385,10 @@ def call(args: Any): arguments, should_prepare_args = _get_contract_arguments(args) contract_address = Address.new_from_bech32(args.contract) + token_transfers = None + if args.token_transfers: + token_transfers = cli_shared.prepare_token_transfers(args.token_transfers) + tx = contract.prepare_execute_transaction( caller=sender, contract=contract_address, @@ -422,7 +398,7 @@ def call(args: Any): gas_limit=int(args.gas_limit), gas_price=int(args.gas_price), value=int(args.value), - transfers=args.token_transfers, + token_transfers=token_transfers, nonce=sender.nonce, version=int(args.version), options=int(args.options), diff --git a/multiversx_sdk_cli/cli_multisig.py b/multiversx_sdk_cli/cli_multisig.py new file mode 100644 index 00000000..377c1551 --- /dev/null +++ b/multiversx_sdk_cli/cli_multisig.py @@ -0,0 +1,1940 @@ +import json +import logging +from pathlib import Path +from typing import Any + +from multiversx_sdk import ( + Action, + ActionFullInfo, + AddBoardMember, + AddProposer, + Address, + AddressComputer, + CallActionData, + ChangeQuorum, + EsdtTokenPayment, + EsdtTransferExecuteData, + MultisigController, + ProxyNetworkProvider, + RemoveUser, + SCDeployFromSource, + SCUpgradeFromSource, + SendAsyncCall, + SendTransferExecuteEgld, + SendTransferExecuteEsdt, + Transaction, + TransactionsFactoryConfig, +) +from multiversx_sdk.abi import Abi + +from multiversx_sdk_cli import cli_shared, utils +from multiversx_sdk_cli.args_validation import ( + ensure_wallet_args_are_provided, + validate_broadcast_args, + validate_chain_id_args, + validate_proxy_argument, + validate_transaction_args, +) +from multiversx_sdk_cli.cli_output import CLIOutputBuilder +from multiversx_sdk_cli.config import get_config_for_network_providers +from multiversx_sdk_cli.constants import NUMBER_OF_SHARDS +from multiversx_sdk_cli.multisig import MultisigWrapper + +logger = logging.getLogger("cli.multisig") + + +def setup_parser(args: list[str], subparsers: Any) -> Any: + parser = cli_shared.add_group_subparser( + subparsers, + "multisig", + "Deploy and interact with the Multisig Smart Contract", + ) + subparsers = parser.add_subparsers() + + output_description = CLIOutputBuilder.describe( + with_contract=True, with_transaction_on_network=True, with_simulation=True + ) + + sub = cli_shared.add_command_subparser( + subparsers, + "multisig", + "deploy", + f"Deploy a Multisig Smart Contract.{output_description}", + ) + _add_bytecode_arg(sub) + sub.add_argument( + "--quorum", + type=int, + required=True, + help="the number of signatures required to approve a proposal", + ) + sub.add_argument( + "--board-members", + required=True, + nargs="+", + type=str, + help="the bech32 addresses of the board members", + ) + cli_shared.add_metadata_arg(sub) + _add_common_args(args=args, sub=sub, with_contract_arg=False, with_receiver_arg=False) + + sub.set_defaults(func=deploy) + + sub = cli_shared.add_command_subparser( + subparsers, + "multisig", + "deposit", + f"Deposit native tokens (EGLD) or ESDT tokens into a Multisig Smart Contract.{output_description}", + ) + cli_shared.add_token_transfers_args(sub) + _add_common_args(args=args, sub=sub, with_contract_arg=True, with_receiver_arg=False) + + sub.set_defaults(func=deposit) + + sub = cli_shared.add_command_subparser( + subparsers, + "multisig", + "discard-action", + f"Discard a proposed action. Signatures must be removed first via `unsign`.{output_description}", + ) + _add_action_id_arg(sub) + _add_common_args(args=args, sub=sub, with_contract_arg=True, with_receiver_arg=False) + + sub.set_defaults(func=discard_action) + + sub = cli_shared.add_command_subparser( + subparsers, + "multisig", + "discard-batch", + f"Discard all the actions for the specified IDs.{output_description}", + ) + sub.add_argument( + "--action-ids", + required=True, + nargs="+", + type=int, + help="the IDs of the actions to discard", + ) + _add_common_args(args=args, sub=sub, with_contract_arg=True, with_receiver_arg=False) + + sub.set_defaults(func=discard_batch) + + sub = cli_shared.add_command_subparser( + subparsers, + "multisig", + "add-board-member", + f"Propose adding a new board member.{output_description}", + ) + sub.add_argument( + "--board-member", + required=True, + type=str, + help="the bech32 address of the proposed board member", + ) + _add_common_args(args=args, sub=sub, with_contract_arg=True, with_receiver_arg=False) + + sub.set_defaults(func=add_board_member) + + sub = cli_shared.add_command_subparser( + subparsers, + "multisig", + "add-proposer", + f"Propose adding a new proposer.{output_description}", + ) + sub.add_argument( + "--proposer", + required=True, + type=str, + help="the bech32 address of the proposed proposer", + ) + _add_common_args(args=args, sub=sub, with_contract_arg=True, with_receiver_arg=False) + + sub.set_defaults(func=add_proposer) + + sub = cli_shared.add_command_subparser( + subparsers, + "multisig", + "remove-user", + f"Propose removing a user from the Multisig Smart Contract.{output_description}", + ) + sub.add_argument( + "--user", + required=True, + type=str, + help="the bech32 address of the proposed user to be removed", + ) + _add_common_args(args=args, sub=sub, with_contract_arg=True, with_receiver_arg=False) + + sub.set_defaults(func=remove_user) + + sub = cli_shared.add_command_subparser( + subparsers, + "multisig", + "change-quorum", + f"Propose changing the quorum of the Multisig Smart Contract.{output_description}", + ) + sub.add_argument( + "--quorum", + required=True, + type=int, + help="the size of the new quorum (number of signatures required to approve a proposal)", + ) + _add_common_args(args=args, sub=sub, with_contract_arg=True, with_receiver_arg=False) + + sub.set_defaults(func=change_quorum) + + sub = cli_shared.add_command_subparser( + subparsers, + "multisig", + "transfer-and-execute", + f"Propose transferring EGLD and optionally calling a smart contract.{output_description}", + ) + sub.add_argument( + "--opt-gas-limit", + type=int, + help="the size of the new quorum (number of signatures required to approve a proposal)", + ) + sub.add_argument("--contract-abi", type=str, help="the ABI file of the contract to call") + sub.add_argument("--function", type=str, help="the function to call") + _add_arguments_arg(sub) + _add_common_args(args=args, sub=sub, with_contract_arg=True, with_receiver_arg=True) + + sub.set_defaults(func=transfer_and_execute) + + sub = cli_shared.add_command_subparser( + subparsers, + "multisig", + "transfer-and-execute-esdt", + f"Propose transferring ESDTs and optionally calling a smart contract.{output_description}", + ) + cli_shared.add_token_transfers_args(sub) + sub.add_argument( + "--opt-gas-limit", + type=int, + help="the size of the new quorum (number of signatures required to approve a proposal)", + ) + sub.add_argument("--contract-abi", type=str, help="the ABI file of the contract to call") + sub.add_argument("--function", type=str, help="the function to call") + _add_arguments_arg(sub) + _add_common_args(args=args, sub=sub, with_contract_arg=True, with_receiver_arg=True) + + sub.set_defaults(func=transfer_and_execute_esdt) + + sub = cli_shared.add_command_subparser( + subparsers, + "multisig", + "async-call", + f"Propose a transaction in which the contract will perform an async call.{output_description}", + ) + cli_shared.add_token_transfers_args(sub) + sub.add_argument( + "--opt-gas-limit", + type=int, + help="the size of the new quorum (number of signatures required to approve a proposal)", + ) + sub.add_argument("--contract-abi", type=str, help="the ABI file of the contract to call") + sub.add_argument("--function", type=str, help="the function to call") + _add_arguments_arg(sub) + _add_common_args(args=args, sub=sub, with_contract_arg=True, with_receiver_arg=True) + + sub.set_defaults(func=async_call) + + sub = cli_shared.add_command_subparser( + subparsers, + "multisig", + "deploy-from-source", + f"Propose a smart contract deploy from a previously deployed smart contract.{output_description}", + ) + + sub.add_argument("--contract-to-copy", required=True, type=str, help="the bech32 address of the contract to copy") + sub.add_argument("--contract-abi", type=str, help="the ABI file of the contract to copy") + cli_shared.add_metadata_arg(sub) + _add_arguments_arg(sub) + _add_common_args(args=args, sub=sub, with_contract_arg=True, with_receiver_arg=False) + + sub.set_defaults(func=deploy_from_source) + + sub = cli_shared.add_command_subparser( + subparsers, + "multisig", + "upgrade-from-source", + f"Propose a smart contract upgrade from a previously deployed smart contract.{output_description}", + ) + sub.add_argument( + "--contract-to-upgrade", required=True, type=str, help="the bech32 address of the contract to upgrade" + ) + sub.add_argument("--contract-to-copy", required=True, type=str, help="the bech32 address of the contract to copy") + sub.add_argument("--contract-abi", type=str, help="the ABI file of the contract to copy") + cli_shared.add_metadata_arg(sub) + _add_arguments_arg(sub) + _add_common_args(args=args, sub=sub, with_contract_arg=True, with_receiver_arg=False) + + sub.set_defaults(func=upgrade_from_source) + + sub = cli_shared.add_command_subparser( + subparsers, + "multisig", + "sign-action", + f"Sign a proposed action.{output_description}", + ) + _add_action_id_arg(sub) + _add_common_args(args=args, sub=sub, with_contract_arg=True, with_receiver_arg=False) + + sub.set_defaults(func=sign_action) + + sub = cli_shared.add_command_subparser( + subparsers, + "multisig", + "sign-batch", + f"Sign a batch of actions.{output_description}", + ) + sub.add_argument("--batch", required=True, type=int, help="the id of the batch to sign") + _add_common_args(args=args, sub=sub, with_contract_arg=True, with_receiver_arg=False) + + sub.set_defaults(func=sign_batch) + + sub = cli_shared.add_command_subparser( + subparsers, + "multisig", + "sign-and-perform", + f"Sign a proposed action and perform it. Works only if quorum is reached.{output_description}", + ) + _add_action_id_arg(sub) + _add_common_args(args=args, sub=sub, with_contract_arg=True, with_receiver_arg=False) + + sub.set_defaults(func=sign_and_perform) + + sub = cli_shared.add_command_subparser( + subparsers, + "multisig", + "sign-batch-and-perform", + f"Sign a batch of actions and perform them. Works only if quorum is reached.{output_description}", + ) + sub.add_argument("--batch", required=True, type=int, help="the id of the batch to sign") + _add_common_args(args=args, sub=sub, with_contract_arg=True, with_receiver_arg=False) + + sub.set_defaults(func=sign_batch_and_perform) + + sub = cli_shared.add_command_subparser( + subparsers, + "multisig", + "unsign-action", + f"Unsign a proposed action.{output_description}", + ) + _add_action_id_arg(sub) + _add_common_args(args=args, sub=sub, with_contract_arg=True, with_receiver_arg=False) + + sub.set_defaults(func=unsign_action) + + sub = cli_shared.add_command_subparser( + subparsers, + "multisig", + "unsign-batch", + f"Unsign a batch of actions.{output_description}", + ) + sub.add_argument("--batch", required=True, type=int, help="the id of the batch to unsign") + _add_common_args(args=args, sub=sub, with_contract_arg=True, with_receiver_arg=False) + + sub.set_defaults(func=unsign_batch) + + sub = cli_shared.add_command_subparser( + subparsers, + "multisig", + "unsign-for-outdated-members", + f"Unsign an action for outdated board members.{output_description}", + ) + _add_action_id_arg(sub) + sub.add_argument( + "--outdated-members", + nargs="+", + type=int, + help="IDs of the outdated board members", + ) + _add_common_args(args=args, sub=sub, with_contract_arg=True, with_receiver_arg=False) + + sub.set_defaults(func=unsign_for_outdated_board_members) + + sub = cli_shared.add_command_subparser( + subparsers, + "multisig", + "perform-action", + f"Perform an action that has reached quorum.{output_description}", + ) + _add_action_id_arg(sub) + _add_common_args(args=args, sub=sub, with_contract_arg=True, with_receiver_arg=False) + + sub.set_defaults(func=perform_action) + + sub = cli_shared.add_command_subparser( + subparsers, + "multisig", + "perform-batch", + f"Perform a batch of actions that has reached quorum.{output_description}", + ) + sub.add_argument("--batch", required=True, type=int, help="the id of the batch to perform") + _add_common_args(args=args, sub=sub, with_contract_arg=True, with_receiver_arg=False) + + sub.set_defaults(func=perform_batch) + + sub = cli_shared.add_command_subparser( + subparsers, + "multisig", + "get-quorum", + f"Perform a smart contract query to get the quorum.{output_description}", + ) + _add_contract_arg(sub) + _add_abi_arg(sub) + cli_shared.add_proxy_arg(sub) + + sub.set_defaults(func=get_quorum) + + sub = cli_shared.add_command_subparser( + subparsers, + "multisig", + "get-num-board-members", + f"Perform a smart contract query to get the number of board members.{output_description}", + ) + _add_contract_arg(sub) + _add_abi_arg(sub) + cli_shared.add_proxy_arg(sub) + + sub.set_defaults(func=get_num_board_members) + + sub = cli_shared.add_command_subparser( + subparsers, + "multisig", + "get-num-groups", + f"Perform a smart contract query to get the number of groups.{output_description}", + ) + _add_contract_arg(sub) + _add_abi_arg(sub) + cli_shared.add_proxy_arg(sub) + + sub.set_defaults(func=get_num_groups) + + sub = cli_shared.add_command_subparser( + subparsers, + "multisig", + "get-num-proposers", + f"Perform a smart contract query to get the number of proposers.{output_description}", + ) + _add_contract_arg(sub) + _add_abi_arg(sub) + cli_shared.add_proxy_arg(sub) + + sub.set_defaults(func=get_num_proposers) + + sub = cli_shared.add_command_subparser( + subparsers, + "multisig", + "get-action-group", + f"Perform a smart contract query to get the actions in a group.{output_description}", + ) + _add_contract_arg(sub) + _add_abi_arg(sub) + sub.add_argument("--group", required=True, type=int, help="the group id") + cli_shared.add_proxy_arg(sub) + + sub.set_defaults(func=get_action_group) + + sub = cli_shared.add_command_subparser( + subparsers, + "multisig", + "get-last-action-group-id", + f"Perform a smart contract query to get the id of the last action in a group.{output_description}", + ) + _add_contract_arg(sub) + _add_abi_arg(sub) + cli_shared.add_proxy_arg(sub) + + sub.set_defaults(func=get_last_group_action_id) + + sub = cli_shared.add_command_subparser( + subparsers, + "multisig", + "get-action-last-index", + f"Perform a smart contract query to get the index of the last action.{output_description}", + ) + _add_contract_arg(sub) + _add_abi_arg(sub) + cli_shared.add_proxy_arg(sub) + + sub.set_defaults(func=get_action_last_index) + + sub = cli_shared.add_command_subparser( + subparsers, + "multisig", + "is-signed-by", + f"Perform a smart contract query to check if an action is signed by a user.{output_description}", + ) + _add_contract_arg(sub) + _add_abi_arg(sub) + _add_action_id_arg(sub) + sub.add_argument("--user", required=True, type=str, help="the bech32 address of the user") + cli_shared.add_proxy_arg(sub) + + sub.set_defaults(func=is_signed_by) + + sub = cli_shared.add_command_subparser( + subparsers, + "multisig", + "is-quorum-reached", + f"Perform a smart contract query to check if an action has reached quorum.{output_description}", + ) + _add_contract_arg(sub) + _add_abi_arg(sub) + _add_action_id_arg(sub) + cli_shared.add_proxy_arg(sub) + + sub.set_defaults(func=is_quorum_reached) + + sub = cli_shared.add_command_subparser( + subparsers, + "multisig", + "get-pending-actions", + f"Perform a smart contract query to get the pending actions full info.{output_description}", + ) + _add_contract_arg(sub) + _add_abi_arg(sub) + cli_shared.add_proxy_arg(sub) + + sub.set_defaults(func=get_pending_actions_full_info) + + sub = cli_shared.add_command_subparser( + subparsers, + "multisig", + "get-user-role", + f"Perform a smart contract query to get the role of a user.{output_description}", + ) + _add_contract_arg(sub) + _add_abi_arg(sub) + sub.add_argument("--user", required=True, type=str, help="the bech32 address of the user") + cli_shared.add_proxy_arg(sub) + + sub.set_defaults(func=get_user_role) + + sub = cli_shared.add_command_subparser( + subparsers, + "multisig", + "get-board-members", + f"Perform a smart contract query to get all the board members.{output_description}", + ) + _add_contract_arg(sub) + _add_abi_arg(sub) + cli_shared.add_proxy_arg(sub) + + sub.set_defaults(func=get_all_board_members) + + sub = cli_shared.add_command_subparser( + subparsers, + "multisig", + "get-proposers", + f"Perform a smart contract query to get all the proposers.{output_description}", + ) + _add_contract_arg(sub) + _add_abi_arg(sub) + cli_shared.add_proxy_arg(sub) + + sub.set_defaults(func=get_all_proposers) + + sub = cli_shared.add_command_subparser( + subparsers, + "multisig", + "get-action-data", + f"Perform a smart contract query to get the data of an action.{output_description}", + ) + _add_contract_arg(sub) + _add_abi_arg(sub) + _add_action_id_arg(sub) + cli_shared.add_proxy_arg(sub) + + sub.set_defaults(func=get_action_data) + + sub = cli_shared.add_command_subparser( + subparsers, + "multisig", + "get-action-signers", + f"Perform a smart contract query to get the signers of an action.{output_description}", + ) + _add_contract_arg(sub) + _add_abi_arg(sub) + _add_action_id_arg(sub) + cli_shared.add_proxy_arg(sub) + + sub.set_defaults(func=get_action_signers) + + sub = cli_shared.add_command_subparser( + subparsers, + "multisig", + "get-action-signers-count", + f"Perform a smart contract query to get the number of signers of an action.{output_description}", + ) + _add_contract_arg(sub) + _add_abi_arg(sub) + _add_action_id_arg(sub) + cli_shared.add_proxy_arg(sub) + + sub.set_defaults(func=get_action_signer_count) + + sub = cli_shared.add_command_subparser( + subparsers, + "multisig", + "get-action-valid-signers-count", + f"Perform a smart contract query to get the number of valid signers of an action.{output_description}", + ) + _add_contract_arg(sub) + _add_abi_arg(sub) + _add_action_id_arg(sub) + cli_shared.add_proxy_arg(sub) + + sub.set_defaults(func=get_action_valid_signer_count) + + sub = cli_shared.add_command_subparser( + subparsers, + "multisig", + "parse-propose-action", + f"Parses the propose action transaction to extract proposal ID.{output_description}", + ) + _add_abi_arg(sub) + sub.add_argument("--hash", required=True, type=str, help="the transaction hash of the propose action") + cli_shared.add_proxy_arg(sub) + + sub.set_defaults(func=parse_proposal) + + parser.epilog = cli_shared.build_group_epilog(subparsers) + return subparsers + + +def _add_bytecode_arg(sub: Any): + sub.add_argument( + "--bytecode", + type=str, + required=True, + help="the file containing the WASM bytecode", + ) + + +def _add_contract_arg(sub: Any): + sub.add_argument("--contract", required=True, type=str, help="🖄 the bech32 address of the Multisig Smart Contract") + + +def _add_abi_arg(sub: Any): + sub.add_argument("--abi", required=True, type=str, help="the ABI file of the Multisig Smart Contract") + + +def _add_action_id_arg(sub: Any): + sub.add_argument("--action", required=True, type=int, help="the id of the action") + + +def _add_arguments_arg(sub: Any): + sub.add_argument( + "--arguments", + nargs="+", + help="arguments for the contract transaction, as [number, bech32-address, ascii string, " + "boolean] or hex-encoded. E.g. --arguments 42 0x64 1000 0xabba str:TOK-a1c2ef true addr:erd1[..]", + ) + sub.add_argument( + "--arguments-file", + type=str, + help="a json file containing the arguments. ONLY if abi file is provided. " + "E.g. [{ 'to': 'erd1...', 'amount': 10000000000 }]", + ) + + +def _add_common_args(args: Any, sub: Any, with_contract_arg: bool = True, with_receiver_arg: bool = False): + if with_contract_arg: + _add_contract_arg(sub) + + _add_abi_arg(sub) + cli_shared.add_outfile_arg(sub) + cli_shared.add_wallet_args(args, sub) + cli_shared.add_proxy_arg(sub) + cli_shared.add_tx_args(args, sub, with_receiver=with_receiver_arg, with_data=False) + cli_shared.add_broadcast_args(sub) + cli_shared.add_guardian_wallet_args(args, sub) + cli_shared.add_relayed_v3_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", + ) + + +def deploy(args: Any): + logger.debug("multisig.deploy") + + validate_transaction_args(args) + ensure_wallet_args_are_provided(args) + validate_broadcast_args(args) + validate_chain_id_args(args) + + sender = cli_shared.prepare_sender(args) + guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( + sender=sender.address.to_bech32(), + args=args, + ) + + abi = Abi.load(Path(args.abi)) + quorum = args.quorum + board_members = [Address.new_from_bech32(addr) for addr in args.board_members] + + chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + multisig = MultisigWrapper(TransactionsFactoryConfig(chain_id), abi) + + tx = multisig.prepare_deploy_transaction( + owner=sender, + nonce=sender.nonce, + bytecode=Path(args.bytecode), + quorum=quorum, + board_members=board_members, + 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), + gas_price=int(args.gas_price), + version=int(args.version), + options=int(args.options), + guardian_and_relayer_data=guardian_and_relayer_data, + ) + + address_computer = AddressComputer(NUMBER_OF_SHARDS) + contract_address = address_computer.compute_contract_address(deployer=sender.address, deployment_nonce=tx.nonce) + + logger.info("Contract address: %s", contract_address.to_bech32()) + utils.log_explorer_contract_address(args.chain, contract_address.to_bech32()) + + _send_or_simulate(tx, contract_address, args) + + +def deposit(args: Any): + validate_transaction_args(args) + ensure_wallet_args_are_provided(args) + validate_broadcast_args(args) + validate_chain_id_args(args) + + sender = cli_shared.prepare_sender(args) + guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( + sender=sender.address.to_bech32(), + args=args, + ) + + abi = Abi.load(Path(args.abi)) + chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + multisig = MultisigWrapper(TransactionsFactoryConfig(chain_id), abi) + + contract = Address.new_from_bech32(args.contract) + native_amount = int(args.value) + + token_transfers = args.token_transfers or None + if token_transfers: + token_transfers = cli_shared.prepare_token_transfers(token_transfers) + + tx = multisig.prepare_deposit_transaction( + owner=sender, + nonce=sender.nonce, + contract=contract, + native_amount=native_amount, + token_transfers=token_transfers, + gas_limit=int(args.gas_limit), + gas_price=int(args.gas_price), + version=int(args.version), + options=int(args.options), + guardian_and_relayer_data=guardian_and_relayer_data, + ) + + _send_or_simulate(tx, contract, args) + + +def discard_action(args: Any): + validate_transaction_args(args) + ensure_wallet_args_are_provided(args) + validate_broadcast_args(args) + validate_chain_id_args(args) + + sender = cli_shared.prepare_sender(args) + guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( + sender=sender.address.to_bech32(), + args=args, + ) + + abi = Abi.load(Path(args.abi)) + chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + multisig = MultisigWrapper(TransactionsFactoryConfig(chain_id), abi) + + contract = Address.new_from_bech32(args.contract) + action_id = int(args.action) + + tx = multisig.prepare_discard_action_transaction( + owner=sender, + nonce=sender.nonce, + contract=contract, + action_id=action_id, + gas_limit=int(args.gas_limit), + gas_price=int(args.gas_price), + version=int(args.version), + options=int(args.options), + guardian_and_relayer_data=guardian_and_relayer_data, + ) + + _send_or_simulate(tx, contract, args) + + +def discard_batch(args: Any): + validate_transaction_args(args) + ensure_wallet_args_are_provided(args) + validate_broadcast_args(args) + validate_chain_id_args(args) + + sender = cli_shared.prepare_sender(args) + guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( + sender=sender.address.to_bech32(), + args=args, + ) + + abi = Abi.load(Path(args.abi)) + chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + multisig = MultisigWrapper(TransactionsFactoryConfig(chain_id), abi) + + contract = Address.new_from_bech32(args.contract) + actions = args.action_ids + + tx = multisig.prepare_discard_batch_transaction( + owner=sender, + nonce=sender.nonce, + contract=contract, + action_ids=actions, + gas_limit=int(args.gas_limit), + gas_price=int(args.gas_price), + version=int(args.version), + options=int(args.options), + guardian_and_relayer_data=guardian_and_relayer_data, + ) + + _send_or_simulate(tx, contract, args) + + +def add_board_member(args: Any): + validate_transaction_args(args) + ensure_wallet_args_are_provided(args) + validate_broadcast_args(args) + validate_chain_id_args(args) + + sender = cli_shared.prepare_sender(args) + guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( + sender=sender.address.to_bech32(), + args=args, + ) + + abi = Abi.load(Path(args.abi)) + chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + multisig = MultisigWrapper(TransactionsFactoryConfig(chain_id), abi) + + contract = Address.new_from_bech32(args.contract) + board_member = Address.new_from_bech32(args.board_member) + + tx = multisig.prepare_add_board_member_transaction( + owner=sender, + nonce=sender.nonce, + contract=contract, + board_member=board_member, + gas_limit=int(args.gas_limit), + gas_price=int(args.gas_price), + version=int(args.version), + options=int(args.options), + guardian_and_relayer_data=guardian_and_relayer_data, + ) + + _send_or_simulate(tx, contract, args) + + +def add_proposer(args: Any): + validate_transaction_args(args) + ensure_wallet_args_are_provided(args) + validate_broadcast_args(args) + validate_chain_id_args(args) + + sender = cli_shared.prepare_sender(args) + guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( + sender=sender.address.to_bech32(), + args=args, + ) + + abi = Abi.load(Path(args.abi)) + chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + multisig = MultisigWrapper(TransactionsFactoryConfig(chain_id), abi) + + contract = Address.new_from_bech32(args.contract) + proposer = Address.new_from_bech32(args.proposer) + + tx = multisig.prepare_add_proposer_transaction( + owner=sender, + nonce=sender.nonce, + contract=contract, + proposer=proposer, + gas_limit=int(args.gas_limit), + gas_price=int(args.gas_price), + version=int(args.version), + options=int(args.options), + guardian_and_relayer_data=guardian_and_relayer_data, + ) + + _send_or_simulate(tx, contract, args) + + +def remove_user(args: Any): + validate_transaction_args(args) + ensure_wallet_args_are_provided(args) + validate_broadcast_args(args) + validate_chain_id_args(args) + + sender = cli_shared.prepare_sender(args) + guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( + sender=sender.address.to_bech32(), + args=args, + ) + + abi = Abi.load(Path(args.abi)) + chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + multisig = MultisigWrapper(TransactionsFactoryConfig(chain_id), abi) + + contract = Address.new_from_bech32(args.contract) + user = Address.new_from_bech32(args.user) + + tx = multisig.prepare_remove_user_transaction( + owner=sender, + nonce=sender.nonce, + contract=contract, + user=user, + gas_limit=int(args.gas_limit), + gas_price=int(args.gas_price), + version=int(args.version), + options=int(args.options), + guardian_and_relayer_data=guardian_and_relayer_data, + ) + + _send_or_simulate(tx, contract, args) + + +def change_quorum(args: Any): + validate_transaction_args(args) + ensure_wallet_args_are_provided(args) + validate_broadcast_args(args) + validate_chain_id_args(args) + + sender = cli_shared.prepare_sender(args) + guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( + sender=sender.address.to_bech32(), + args=args, + ) + + abi = Abi.load(Path(args.abi)) + chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + multisig = MultisigWrapper(TransactionsFactoryConfig(chain_id), abi) + + contract = Address.new_from_bech32(args.contract) + quorum = int(args.quorum) + + tx = multisig.prepare_change_quorum_transaction( + owner=sender, + nonce=sender.nonce, + contract=contract, + quorum=quorum, + gas_limit=int(args.gas_limit), + gas_price=int(args.gas_price), + version=int(args.version), + options=int(args.options), + guardian_and_relayer_data=guardian_and_relayer_data, + ) + + _send_or_simulate(tx, contract, args) + + +def transfer_and_execute(args: Any): + validate_transaction_args(args) + ensure_wallet_args_are_provided(args) + validate_broadcast_args(args) + validate_chain_id_args(args) + + sender = cli_shared.prepare_sender(args) + guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( + sender=sender.address.to_bech32(), + args=args, + ) + + abi = Abi.load(Path(args.abi)) + chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + multisig = MultisigWrapper(TransactionsFactoryConfig(chain_id), abi) + + contract = Address.new_from_bech32(args.contract) + + receiver = Address.new_from_bech32(args.receiver) + opt_gas_limit = int(args.opt_gas_limit) if args.opt_gas_limit else None + function = args.function if args.function else None + contract_abi = Abi.load(Path(args.contract_abi)) if args.contract_abi else None + arguments, should_prepare_args = _get_contract_arguments(args) + + tx = multisig.prepare_transfer_execute_transaction( + owner=sender, + nonce=sender.nonce, + contract=contract, + receiver=receiver, + gas_limit=int(args.gas_limit), + gas_price=int(args.gas_price), + version=int(args.version), + options=int(args.options), + should_prepare_args_for_factory=should_prepare_args, + guardian_and_relayer_data=guardian_and_relayer_data, + opt_gas_limit=opt_gas_limit, + function=function, + abi=contract_abi, + arguments=arguments, + native_token_amount=int(args.value), + ) + + _send_or_simulate(tx, contract, args) + + +def transfer_and_execute_esdt(args: Any): + validate_transaction_args(args) + ensure_wallet_args_are_provided(args) + validate_broadcast_args(args) + validate_chain_id_args(args) + + if int(args.value) != 0: + raise Exception("Native token transfer is not allowed for this command.") + + if not args.token_transfers: + raise Exception("Token transfers not provided.") + + sender = cli_shared.prepare_sender(args) + guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( + sender=sender.address.to_bech32(), + args=args, + ) + + abi = Abi.load(Path(args.abi)) + chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + multisig = MultisigWrapper(TransactionsFactoryConfig(chain_id), abi) + + contract = Address.new_from_bech32(args.contract) + + receiver = Address.new_from_bech32(args.receiver) + opt_gas_limit = int(args.opt_gas_limit) if args.opt_gas_limit else None + function = args.function if args.function else None + contract_abi = Abi.load(Path(args.contract_abi)) if args.contract_abi else None + arguments, should_prepare_args = _get_contract_arguments(args) + token_transfers = cli_shared.prepare_token_transfers(args.token_transfers) + + tx = multisig.prepare_transfer_execute_esdt_transaction( + owner=sender, + nonce=sender.nonce, + contract=contract, + receiver=receiver, + gas_limit=int(args.gas_limit), + gas_price=int(args.gas_price), + version=int(args.version), + options=int(args.options), + should_prepare_args_for_factory=should_prepare_args, + guardian_and_relayer_data=guardian_and_relayer_data, + token_transfers=token_transfers, + opt_gas_limit=opt_gas_limit, + function=function, + abi=contract_abi, + arguments=arguments, + ) + + _send_or_simulate(tx, contract, args) + + +def async_call(args: Any): + validate_transaction_args(args) + ensure_wallet_args_are_provided(args) + validate_broadcast_args(args) + validate_chain_id_args(args) + + sender = cli_shared.prepare_sender(args) + guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( + sender=sender.address.to_bech32(), + args=args, + ) + + abi = Abi.load(Path(args.abi)) + chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + multisig = MultisigWrapper(TransactionsFactoryConfig(chain_id), abi) + + contract = Address.new_from_bech32(args.contract) + + receiver = Address.new_from_bech32(args.receiver) + opt_gas_limit = int(args.opt_gas_limit) if args.opt_gas_limit else None + function = args.function if args.function else None + contract_abi = Abi.load(Path(args.contract_abi)) if args.contract_abi else None + arguments, should_prepare_args = _get_contract_arguments(args) + + token_transfers = args.token_transfers or None + if token_transfers: + token_transfers = cli_shared.prepare_token_transfers(args.token_transfers) + + tx = multisig.prepare_async_call_transaction( + owner=sender, + nonce=sender.nonce, + contract=contract, + receiver=receiver, + gas_limit=int(args.gas_limit), + gas_price=int(args.gas_price), + version=int(args.version), + options=int(args.options), + should_prepare_args_for_factory=should_prepare_args, + guardian_and_relayer_data=guardian_and_relayer_data, + native_token_amount=int(args.value), + token_transfers=token_transfers, + opt_gas_limit=opt_gas_limit, + function=function, + abi=contract_abi, + arguments=arguments, + ) + + _send_or_simulate(tx, contract, args) + + +def deploy_from_source(args: Any): + validate_transaction_args(args) + ensure_wallet_args_are_provided(args) + validate_broadcast_args(args) + validate_chain_id_args(args) + + sender = cli_shared.prepare_sender(args) + guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( + sender=sender.address.to_bech32(), + args=args, + ) + + abi = Abi.load(Path(args.abi)) + chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + multisig = MultisigWrapper(TransactionsFactoryConfig(chain_id), abi) + + contract = Address.new_from_bech32(args.contract) + contract_to_copy = Address.new_from_bech32(args.contract_to_copy) + + contract_abi = Abi.load(Path(args.contract_abi)) if args.contract_abi else None + arguments, should_prepare_args = _get_contract_arguments(args) + + tx = multisig.prepare_contract_deploy_from_source_transaction( + owner=sender, + nonce=sender.nonce, + contract=contract, + contract_to_copy=contract_to_copy, + gas_limit=int(args.gas_limit), + gas_price=int(args.gas_price), + version=int(args.version), + options=int(args.options), + upgradeable=args.metadata_upgradeable, + readable=args.metadata_readable, + payable=args.metadata_payable, + payable_by_sc=args.metadata_payable_by_sc, + should_prepare_args_for_factory=should_prepare_args, + guardian_and_relayer_data=guardian_and_relayer_data, + native_token_amount=int(args.value), + abi=contract_abi, + arguments=arguments, + ) + + _send_or_simulate(tx, contract, args) + + +def upgrade_from_source(args: Any): + validate_transaction_args(args) + ensure_wallet_args_are_provided(args) + validate_broadcast_args(args) + validate_chain_id_args(args) + + sender = cli_shared.prepare_sender(args) + guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( + sender=sender.address.to_bech32(), + args=args, + ) + + abi = Abi.load(Path(args.abi)) + chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + multisig = MultisigWrapper(TransactionsFactoryConfig(chain_id), abi) + + contract = Address.new_from_bech32(args.contract) + contract_to_upgrade = Address.new_from_bech32(args.contract_to_upgrade) + contract_to_copy = Address.new_from_bech32(args.contract_to_copy) + + contract_abi = Abi.load(Path(args.contract_abi)) if args.contract_abi else None + arguments, should_prepare_args = _get_contract_arguments(args) + + tx = multisig.prepare_contract_upgrade_from_source_transaction( + owner=sender, + nonce=sender.nonce, + contract=contract, + contract_to_upgrade=contract_to_upgrade, + contract_to_copy=contract_to_copy, + gas_limit=int(args.gas_limit), + gas_price=int(args.gas_price), + version=int(args.version), + options=int(args.options), + upgradeable=args.metadata_upgradeable, + readable=args.metadata_readable, + payable=args.metadata_payable, + payable_by_sc=args.metadata_payable_by_sc, + should_prepare_args_for_factory=should_prepare_args, + guardian_and_relayer_data=guardian_and_relayer_data, + native_token_amount=int(args.value), + abi=contract_abi, + arguments=arguments, + ) + + _send_or_simulate(tx, contract, args) + + +def sign_action(args: Any): + validate_transaction_args(args) + ensure_wallet_args_are_provided(args) + validate_broadcast_args(args) + validate_chain_id_args(args) + + sender = cli_shared.prepare_sender(args) + guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( + sender=sender.address.to_bech32(), + args=args, + ) + + abi = Abi.load(Path(args.abi)) + chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + multisig = MultisigWrapper(TransactionsFactoryConfig(chain_id), abi) + + contract = Address.new_from_bech32(args.contract) + action_id = int(args.action) + + tx = multisig.prepare_sign_action_transaction( + owner=sender, + nonce=sender.nonce, + contract=contract, + action_id=action_id, + gas_limit=int(args.gas_limit), + gas_price=int(args.gas_price), + version=int(args.version), + options=int(args.options), + guardian_and_relayer_data=guardian_and_relayer_data, + ) + + _send_or_simulate(tx, contract, args) + + +def sign_batch(args: Any): + validate_transaction_args(args) + ensure_wallet_args_are_provided(args) + validate_broadcast_args(args) + validate_chain_id_args(args) + + sender = cli_shared.prepare_sender(args) + guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( + sender=sender.address.to_bech32(), + args=args, + ) + + abi = Abi.load(Path(args.abi)) + chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + multisig = MultisigWrapper(TransactionsFactoryConfig(chain_id), abi) + + contract = Address.new_from_bech32(args.contract) + batch_id = int(args.batch) + + tx = multisig.prepare_sign_batch_transaction( + owner=sender, + nonce=sender.nonce, + contract=contract, + batch_id=batch_id, + gas_limit=int(args.gas_limit), + gas_price=int(args.gas_price), + version=int(args.version), + options=int(args.options), + guardian_and_relayer_data=guardian_and_relayer_data, + ) + + _send_or_simulate(tx, contract, args) + + +def sign_and_perform(args: Any): + validate_transaction_args(args) + ensure_wallet_args_are_provided(args) + validate_broadcast_args(args) + validate_chain_id_args(args) + + sender = cli_shared.prepare_sender(args) + guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( + sender=sender.address.to_bech32(), + args=args, + ) + + abi = Abi.load(Path(args.abi)) + chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + multisig = MultisigWrapper(TransactionsFactoryConfig(chain_id), abi) + + contract = Address.new_from_bech32(args.contract) + action_id = int(args.action) + + tx = multisig.prepare_sign_and_perform_transaction( + owner=sender, + nonce=sender.nonce, + contract=contract, + action_id=action_id, + gas_limit=int(args.gas_limit), + gas_price=int(args.gas_price), + version=int(args.version), + options=int(args.options), + guardian_and_relayer_data=guardian_and_relayer_data, + ) + + _send_or_simulate(tx, contract, args) + + +def sign_batch_and_perform(args: Any): + validate_transaction_args(args) + ensure_wallet_args_are_provided(args) + validate_broadcast_args(args) + validate_chain_id_args(args) + + sender = cli_shared.prepare_sender(args) + guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( + sender=sender.address.to_bech32(), + args=args, + ) + + abi = Abi.load(Path(args.abi)) + chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + multisig = MultisigWrapper(TransactionsFactoryConfig(chain_id), abi) + + contract = Address.new_from_bech32(args.contract) + batch_id = int(args.batch) + + tx = multisig.prepare_sign_batch_and_perform_transaction( + owner=sender, + nonce=sender.nonce, + contract=contract, + batch_id=batch_id, + gas_limit=int(args.gas_limit), + gas_price=int(args.gas_price), + version=int(args.version), + options=int(args.options), + guardian_and_relayer_data=guardian_and_relayer_data, + ) + + _send_or_simulate(tx, contract, args) + + +def unsign_action(args: Any): + validate_transaction_args(args) + ensure_wallet_args_are_provided(args) + validate_broadcast_args(args) + validate_chain_id_args(args) + + sender = cli_shared.prepare_sender(args) + guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( + sender=sender.address.to_bech32(), + args=args, + ) + + abi = Abi.load(Path(args.abi)) + chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + multisig = MultisigWrapper(TransactionsFactoryConfig(chain_id), abi) + + contract = Address.new_from_bech32(args.contract) + action_id = int(args.action) + + tx = multisig.prepare_unsign_action_transaction( + owner=sender, + nonce=sender.nonce, + contract=contract, + action_id=action_id, + gas_limit=int(args.gas_limit), + gas_price=int(args.gas_price), + version=int(args.version), + options=int(args.options), + guardian_and_relayer_data=guardian_and_relayer_data, + ) + + _send_or_simulate(tx, contract, args) + + +def unsign_batch(args: Any): + validate_transaction_args(args) + ensure_wallet_args_are_provided(args) + validate_broadcast_args(args) + validate_chain_id_args(args) + + sender = cli_shared.prepare_sender(args) + guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( + sender=sender.address.to_bech32(), + args=args, + ) + + abi = Abi.load(Path(args.abi)) + chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + multisig = MultisigWrapper(TransactionsFactoryConfig(chain_id), abi) + + contract = Address.new_from_bech32(args.contract) + batch_id = int(args.batch) + + tx = multisig.prepare_unsign_batch_transaction( + owner=sender, + nonce=sender.nonce, + contract=contract, + batch_id=batch_id, + gas_limit=int(args.gas_limit), + gas_price=int(args.gas_price), + version=int(args.version), + options=int(args.options), + guardian_and_relayer_data=guardian_and_relayer_data, + ) + + _send_or_simulate(tx, contract, args) + + +def unsign_for_outdated_board_members(args: Any): + validate_transaction_args(args) + ensure_wallet_args_are_provided(args) + validate_broadcast_args(args) + validate_chain_id_args(args) + + sender = cli_shared.prepare_sender(args) + guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( + sender=sender.address.to_bech32(), + args=args, + ) + + abi = Abi.load(Path(args.abi)) + chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + multisig = MultisigWrapper(TransactionsFactoryConfig(chain_id), abi) + + contract = Address.new_from_bech32(args.contract) + action_id = int(args.action) + + tx = multisig.prepare_unsign_for_outdated_board_members_transaction( + owner=sender, + nonce=sender.nonce, + contract=contract, + action_id=action_id, + outdated_board_members=args.outdated_members, + gas_limit=int(args.gas_limit), + gas_price=int(args.gas_price), + version=int(args.version), + options=int(args.options), + guardian_and_relayer_data=guardian_and_relayer_data, + ) + + _send_or_simulate(tx, contract, args) + + +def perform_action(args: Any): + validate_transaction_args(args) + ensure_wallet_args_are_provided(args) + validate_broadcast_args(args) + validate_chain_id_args(args) + + sender = cli_shared.prepare_sender(args) + guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( + sender=sender.address.to_bech32(), + args=args, + ) + + abi = Abi.load(Path(args.abi)) + chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + multisig = MultisigWrapper(TransactionsFactoryConfig(chain_id), abi) + + contract = Address.new_from_bech32(args.contract) + action_id = int(args.action) + + tx = multisig.prepare_perform_action_transaction( + owner=sender, + nonce=sender.nonce, + contract=contract, + action_id=action_id, + gas_limit=int(args.gas_limit), + gas_price=int(args.gas_price), + version=int(args.version), + options=int(args.options), + guardian_and_relayer_data=guardian_and_relayer_data, + ) + + _send_or_simulate(tx, contract, args) + + +def perform_batch(args: Any): + validate_transaction_args(args) + ensure_wallet_args_are_provided(args) + validate_broadcast_args(args) + validate_chain_id_args(args) + + sender = cli_shared.prepare_sender(args) + guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( + sender=sender.address.to_bech32(), + args=args, + ) + + abi = Abi.load(Path(args.abi)) + chain_id = cli_shared.get_chain_id(args.chain, args.proxy) + multisig = MultisigWrapper(TransactionsFactoryConfig(chain_id), abi) + + contract = Address.new_from_bech32(args.contract) + batch_id = int(args.batch) + + tx = multisig.prepare_perform_batch_transaction( + owner=sender, + nonce=sender.nonce, + contract=contract, + batch_id=batch_id, + gas_limit=int(args.gas_limit), + gas_price=int(args.gas_price), + version=int(args.version), + options=int(args.options), + guardian_and_relayer_data=guardian_and_relayer_data, + ) + + _send_or_simulate(tx, contract, args) + + +def get_quorum(args: Any): + validate_proxy_argument(args) + + abi = Abi.load(Path(args.abi)) + config = get_config_for_network_providers() + proxy = ProxyNetworkProvider(url=args.proxy, config=config) + chain_id = proxy.get_network_config().chain_id + multisig = MultisigController(chain_id, proxy, abi) + + quorum = multisig.get_quorum(Address.new_from_bech32(args.contract)) + print(f"Quorum: {quorum}") + + +def get_num_board_members(args: Any): + validate_proxy_argument(args) + + abi = Abi.load(Path(args.abi)) + config = get_config_for_network_providers() + proxy = ProxyNetworkProvider(url=args.proxy, config=config) + chain_id = proxy.get_network_config().chain_id + multisig = MultisigController(chain_id, proxy, abi) + + num_board_members = multisig.get_num_board_members(Address.new_from_bech32(args.contract)) + print(f"Number of board members: {num_board_members}") + + +def get_num_groups(args: Any): + validate_proxy_argument(args) + + abi = Abi.load(Path(args.abi)) + config = get_config_for_network_providers() + proxy = ProxyNetworkProvider(url=args.proxy, config=config) + chain_id = proxy.get_network_config().chain_id + multisig = MultisigController(chain_id, proxy, abi) + + num_groups = multisig.get_num_groups(Address.new_from_bech32(args.contract)) + print(f"Number of groups: {num_groups}") + + +def get_num_proposers(args: Any): + validate_proxy_argument(args) + + abi = Abi.load(Path(args.abi)) + config = get_config_for_network_providers() + proxy = ProxyNetworkProvider(url=args.proxy, config=config) + chain_id = proxy.get_network_config().chain_id + multisig = MultisigController(chain_id, proxy, abi) + + num_proposers = multisig.get_num_proposers(Address.new_from_bech32(args.contract)) + print(f"Number of proposers: {num_proposers}") + + +def get_action_group(args: Any): + validate_proxy_argument(args) + + abi = Abi.load(Path(args.abi)) + config = get_config_for_network_providers() + proxy = ProxyNetworkProvider(url=args.proxy, config=config) + chain_id = proxy.get_network_config().chain_id + multisig = MultisigController(chain_id, proxy, abi) + + actions = multisig.get_action_group(Address.new_from_bech32(args.contract), args.group) + print(f"Actions: [{actions}]") + + +def get_last_group_action_id(args: Any): + validate_proxy_argument(args) + + abi = Abi.load(Path(args.abi)) + config = get_config_for_network_providers() + proxy = ProxyNetworkProvider(url=args.proxy, config=config) + chain_id = proxy.get_network_config().chain_id + multisig = MultisigController(chain_id, proxy, abi) + + id = multisig.get_last_group_action_id(Address.new_from_bech32(args.contract)) + print(f"Last group action id: {id}") + + +def get_action_last_index(args: Any): + validate_proxy_argument(args) + + abi = Abi.load(Path(args.abi)) + config = get_config_for_network_providers() + proxy = ProxyNetworkProvider(url=args.proxy, config=config) + chain_id = proxy.get_network_config().chain_id + multisig = MultisigController(chain_id, proxy, abi) + + id = multisig.get_action_last_index(Address.new_from_bech32(args.contract)) + print(f"Action last index: {id}") + + +def is_signed_by(args: Any): + validate_proxy_argument(args) + + abi = Abi.load(Path(args.abi)) + config = get_config_for_network_providers() + proxy = ProxyNetworkProvider(url=args.proxy, config=config) + chain_id = proxy.get_network_config().chain_id + multisig = MultisigController(chain_id, proxy, abi) + + contract = Address.new_from_bech32(args.contract) + action_id = int(args.action) + user = Address.new_from_bech32(args.user) + + is_signed = multisig.is_signed_by(contract, user, action_id) + print(f"Action {action_id} is signed by {user.to_bech32()}: {is_signed}") + + +def is_quorum_reached(args: Any): + validate_proxy_argument(args) + + abi = Abi.load(Path(args.abi)) + config = get_config_for_network_providers() + proxy = ProxyNetworkProvider(url=args.proxy, config=config) + chain_id = proxy.get_network_config().chain_id + multisig = MultisigController(chain_id, proxy, abi) + + contract = Address.new_from_bech32(args.contract) + action_id = int(args.action) + + is_quorum_reached = multisig.is_quorum_reached(contract, action_id) + print(f"Quorum reached for action {action_id}: {is_quorum_reached}") + + +def get_pending_actions_full_info(args: Any): + validate_proxy_argument(args) + + abi = Abi.load(Path(args.abi)) + config = get_config_for_network_providers() + proxy = ProxyNetworkProvider(url=args.proxy, config=config) + chain_id = proxy.get_network_config().chain_id + contract = Address.new_from_bech32(args.contract) + + controller = MultisigController(chain_id, proxy, abi) + actions = controller.get_pending_actions_full_info(contract) + + output = [_convert_action_full_info_to_dict(action) for action in actions] + utils.dump_out_json(output) + + +def get_user_role(args: Any): + validate_proxy_argument(args) + + abi = Abi.load(Path(args.abi)) + config = get_config_for_network_providers() + proxy = ProxyNetworkProvider(url=args.proxy, config=config) + chain_id = proxy.get_network_config().chain_id + multisig = MultisigController(chain_id, proxy, abi) + + contract = Address.new_from_bech32(args.contract) + user = Address.new_from_bech32(args.user) + + role = multisig.get_user_role(contract, user) + print(f"User {user.to_bech32()} has role: {role.name}") + + +def get_all_board_members(args: Any): + validate_proxy_argument(args) + + abi = Abi.load(Path(args.abi)) + config = get_config_for_network_providers() + proxy = ProxyNetworkProvider(url=args.proxy, config=config) + chain_id = proxy.get_network_config().chain_id + multisig = MultisigController(chain_id, proxy, abi) + + contract = Address.new_from_bech32(args.contract) + + board_members = multisig.get_all_board_members(contract) + if not board_members: + print(None) + else: + print("Board members:") + for member in board_members: + print(f" - {member.to_bech32()}") + + +def get_all_proposers(args: Any): + validate_proxy_argument(args) + + abi = Abi.load(Path(args.abi)) + config = get_config_for_network_providers() + proxy = ProxyNetworkProvider(url=args.proxy, config=config) + chain_id = proxy.get_network_config().chain_id + multisig = MultisigController(chain_id, proxy, abi) + + contract = Address.new_from_bech32(args.contract) + + proposers = multisig.get_all_proposers(contract) + if not proposers: + print(None) + else: + print("Proposers:") + for proposer in proposers: + print(f" - {proposer.to_bech32()}") + + +def get_action_data(args: Any): + validate_proxy_argument(args) + + abi = Abi.load(Path(args.abi)) + config = get_config_for_network_providers() + proxy = ProxyNetworkProvider(url=args.proxy, config=config) + chain_id = proxy.get_network_config().chain_id + controller = MultisigController(chain_id, proxy, abi) + + contract = Address.new_from_bech32(args.contract) + action = args.action + + action_data = controller.get_action_data(contract=contract, action_id=action) + utils.dump_out_json(_convert_action_to_dict(action_data)) + + +def get_action_signers(args: Any): + validate_proxy_argument(args) + + abi = Abi.load(Path(args.abi)) + config = get_config_for_network_providers() + proxy = ProxyNetworkProvider(url=args.proxy, config=config) + chain_id = proxy.get_network_config().chain_id + multisig = MultisigController(chain_id, proxy, abi) + + contract = Address.new_from_bech32(args.contract) + action = args.action + + signers = multisig.get_action_signers(contract, action) + print(f"Signers for action {action}:") + for signer in signers: + print(f" - {signer.to_bech32()}") + + +def get_action_signer_count(args: Any): + validate_proxy_argument(args) + + abi = Abi.load(Path(args.abi)) + config = get_config_for_network_providers() + proxy = ProxyNetworkProvider(url=args.proxy, config=config) + chain_id = proxy.get_network_config().chain_id + multisig = MultisigController(chain_id, proxy, abi) + + contract = Address.new_from_bech32(args.contract) + action = args.action + + signers = multisig.get_action_signer_count(contract, action) + print(f"{signers} signers for action {action}:") + + +def get_action_valid_signer_count(args: Any): + validate_proxy_argument(args) + + abi = Abi.load(Path(args.abi)) + config = get_config_for_network_providers() + proxy = ProxyNetworkProvider(url=args.proxy, config=config) + chain_id = proxy.get_network_config().chain_id + multisig = MultisigController(chain_id, proxy, abi) + + contract = Address.new_from_bech32(args.contract) + action = args.action + + signers = multisig.get_action_valid_signer_count(contract, action) + print(f"{signers} valid signers for action {action}:") + + +def parse_proposal(args: Any): + validate_proxy_argument(args) + + abi = Abi.load(Path(args.abi)) + config = get_config_for_network_providers() + proxy = ProxyNetworkProvider(url=args.proxy, config=config) + chain_id = proxy.get_network_config().chain_id + multisig = MultisigController(chain_id, proxy, abi) + + id = multisig.await_completed_execute_propose_any(args.hash) + print(f"Proposal ID: {id}") + + +def _get_contract_arguments(args: Any) -> tuple[list[Any], bool]: + json_args = json.loads(Path(args.arguments_file).expanduser().read_text()) if args.arguments_file else None + + if json_args and args.arguments: + raise Exception("Provide either '--arguments' or '--arguments-file'.") + + if json_args: + if not args.abi: + raise Exception("Can't use '--arguments-file' without providing the Abi file.") + + return json_args, False + else: + return args.arguments, True + + +def _send_or_simulate(tx: Transaction, contract_address: Address, args: Any): + output_builder = cli_shared.send_or_simulate(tx, args, dump_output=False) + output_builder.set_contract_address(contract_address) + utils.dump_out_json(output_builder.build(), outfile=args.outfile) + + +def _convert_action_to_dict(action: Action) -> dict[str, Any]: + if isinstance(action, AddBoardMember): + return _convert_add_board_member_to_dict(action) + elif isinstance(action, AddProposer): + return _convert_add_proposer_to_dict(action) + elif isinstance(action, RemoveUser): + return _convert_remove_user_to_dict(action) + elif isinstance(action, ChangeQuorum): + return _convert_change_quorum_to_dict(action) + elif isinstance(action, SendTransferExecuteEgld): + return _convert_send_transfer_execute_egld_to_dict(action) + elif isinstance(action, SendTransferExecuteEsdt): + return _convert_send_transfer_execute_esdt_to_dict(action) + elif isinstance(action, SendAsyncCall): + return _convert_send_async_call_to_dict(action) + elif isinstance(action, SCDeployFromSource): + return _convert_sc_deploy_from_source_to_dict(action) + elif isinstance(action, SCUpgradeFromSource): + return _convert_sc_upgrade_from_source_to_dict(action) + else: + raise Exception(f"Unknown action type: {type(action)}") + + +def _convert_add_board_member_to_dict(action: AddBoardMember) -> dict[str, Any]: + return { + "type": "AddBoardMember", + "discriminant": action.discriminant, + "address": action.address.to_bech32(), + } + + +def _convert_add_proposer_to_dict(action: AddProposer) -> dict[str, Any]: + return { + "type": "AddProposer", + "discriminant": action.discriminant, + "address": action.address.to_bech32(), + } + + +def _convert_remove_user_to_dict(action: RemoveUser) -> dict[str, Any]: + return { + "type": "RemoveUser", + "discriminant": action.discriminant, + "address": action.address.to_bech32(), + } + + +def _convert_change_quorum_to_dict(action: ChangeQuorum) -> dict[str, Any]: + return { + "type": "ChangeQuorum", + "discriminant": action.discriminant, + "quorum": action.quorum, + } + + +def _convert_send_transfer_execute_egld_to_dict(action: SendTransferExecuteEgld) -> dict[str, Any]: + return { + "type": "SendTransferExecuteEgld", + "discriminant": action.discriminant, + "callActionData": _convert_call_action_data_to_dict(action.data), + } + + +def _convert_call_action_data_to_dict(call_action_data: CallActionData) -> dict[str, Any]: + return { + "to": call_action_data.to.to_bech32(), + "egldAmount": call_action_data.egld_amount, + "optGasLimit": call_action_data.opt_gas_limit, + "endpointName": call_action_data.endpoint_name, + "arguments": [arg.hex() for arg in call_action_data.arguments], + } + + +def _convert_send_transfer_execute_esdt_to_dict(action: SendTransferExecuteEsdt) -> dict[str, Any]: + return { + "type": "SendTransferExecuteEsdt", + "discriminant": action.discriminant, + "esdtTransferExecuteData": _convert_esdt_transfer_execute_data_to_dict(action.data), + } + + +def _convert_esdt_transfer_execute_data_to_dict(call_action_data: EsdtTransferExecuteData) -> dict[str, Any]: + return { + "to": call_action_data.to.to_bech32(), + "tokens": _convert_tokens_to_dict(call_action_data.tokens), + "optGasLimit": call_action_data.opt_gas_limit, + "endpointName": call_action_data.endpoint_name, + "arguments": [arg.hex() for arg in call_action_data.arguments], + } + + +def _convert_tokens_to_dict(tokens: list[EsdtTokenPayment]) -> list[dict[str, Any]]: + return [ + { + "tokenIdentifier": token.fields[0].get_payload(), + "tokenNonce": token.fields[1].get_payload(), + "amount": token.fields[2].get_payload(), + } + for token in tokens + ] + + +def _convert_send_async_call_to_dict(action: SendAsyncCall) -> dict[str, Any]: + return { + "type": "SendAsyncCall", + "discriminant": action.discriminant, + "callActionData": _convert_call_action_data_to_dict(action.data), + } + + +def _convert_sc_deploy_from_source_to_dict(action: SCDeployFromSource) -> dict[str, Any]: + return { + "type": "SCDeployFromSource", + "discriminant": action.discriminant, + "amount": action.amount, + "source": action.source.to_bech32(), + "codeMetadata": action.code_metadata.hex(), + "arguments": [arg.hex() for arg in action.arguments], + } + + +def _convert_sc_upgrade_from_source_to_dict(action: SCUpgradeFromSource) -> dict[str, Any]: + return { + "type": "SCDeployFromSource", + "discriminant": action.discriminant, + "scAddress": action.sc_address.to_bech32(), + "amount": action.amount, + "source": action.source.to_bech32(), + "codeMetadata": action.code_metadata.hex(), + "arguments": [arg.hex() for arg in action.arguments], + } + + +def _convert_action_full_info_to_dict(action: ActionFullInfo) -> dict[str, Any]: + return { + "actionId": action.action_id, + "groupId": action.group_id, + "actionData": _convert_action_to_dict(action.action_data), + "signers": [signer.to_bech32() for signer in action.signers], + } diff --git a/multiversx_sdk_cli/cli_shared.py b/multiversx_sdk_cli/cli_shared.py index 316b8629..13c88b54 100644 --- a/multiversx_sdk_cli/cli_shared.py +++ b/multiversx_sdk_cli/cli_shared.py @@ -11,6 +11,9 @@ ApiNetworkProvider, LedgerAccount, ProxyNetworkProvider, + Token, + TokenComputer, + TokenTransfer, Transaction, ) @@ -279,6 +282,34 @@ def add_token_transfers_args(sub: Any): ) +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)", + ) + sub.add_argument( + "--metadata-not-readable", + dest="metadata_readable", + action="store_false", + help="‼ mark the contract as NOT readable (default: readable)", + ) + sub.add_argument( + "--metadata-payable", + dest="metadata_payable", + action="store_true", + help="‼ mark the contract as payable (default: not payable)", + ) + sub.add_argument( + "--metadata-payable-by-sc", + dest="metadata_payable_by_sc", + action="store_true", + help="‼ mark the contract as payable by SC (default: not payable by SC)", + ) + sub.set_defaults(metadata_upgradeable=True, metadata_payable=False) + + def parse_omit_fields_arg(args: Any) -> list[str]: literal = args.omit_fields parsed = ast.literal_eval(literal) @@ -617,3 +648,21 @@ def prepare_guardian_relayer_data(args: Any) -> GuardianRelayerData: relayer=relayer, relayer_address=relayer_address, ) + + +def prepare_token_transfers(transfers: list[str]) -> list[TokenTransfer]: + """Converts a list of token transfers as received from the CLI to a list of TokenTransfer objects.""" + token_computer = TokenComputer() + token_transfers: list[TokenTransfer] = [] + + for i in range(0, len(transfers) - 1, 2): + extended_identifier = transfers[i] + amount = int(transfers[i + 1]) + nonce = token_computer.extract_nonce_from_extended_identifier(extended_identifier) + identifier = token_computer.extract_identifier_from_extended_identifier(extended_identifier) + + token = Token(identifier, nonce) + transfer = TokenTransfer(token, amount) + token_transfers.append(transfer) + + return token_transfers diff --git a/multiversx_sdk_cli/cli_transactions.py b/multiversx_sdk_cli/cli_transactions.py index 27785f43..9a5dec4a 100644 --- a/multiversx_sdk_cli/cli_transactions.py +++ b/multiversx_sdk_cli/cli_transactions.py @@ -2,14 +2,7 @@ from pathlib import Path from typing import Any -from multiversx_sdk import ( - Address, - ProxyNetworkProvider, - Token, - TokenComputer, - TokenTransfer, - TransactionComputer, -) +from multiversx_sdk import Address, ProxyNetworkProvider, TransactionComputer from multiversx_sdk_cli import cli_shared, utils from multiversx_sdk_cli.args_validation import ( @@ -133,7 +126,7 @@ def create_transaction(args: Any): gas_limit = int(args.gas_limit) if args.gas_limit else 0 transfers = getattr(args, "token_transfers", None) - transfers = prepare_token_transfers(transfers) if transfers else None + transfers = cli_shared.prepare_token_transfers(transfers) if transfers else None chain_id = cli_shared.get_chain_id(args.chain, args.proxy) tx_controller = TransactionsController(chain_id) @@ -155,22 +148,6 @@ def create_transaction(args: Any): cli_shared.send_or_simulate(tx, args) -def prepare_token_transfers(transfers: list[Any]) -> list[TokenTransfer]: - token_computer = TokenComputer() - token_transfers: list[TokenTransfer] = [] - - for i in range(0, len(transfers) - 1, 2): - identifier = transfers[i] - amount = int(transfers[i + 1]) - nonce = token_computer.extract_nonce_from_extended_identifier(identifier) - - token = Token(identifier, nonce) - transfer = TokenTransfer(token, amount) - token_transfers.append(transfer) - - return token_transfers - - def send_transaction(args: Any): validate_proxy_argument(args) diff --git a/multiversx_sdk_cli/constants.py b/multiversx_sdk_cli/constants.py index 9a997542..d851f7e7 100644 --- a/multiversx_sdk_cli/constants.py +++ b/multiversx_sdk_cli/constants.py @@ -19,3 +19,10 @@ TCS_SERVICE_ID = "MultiversXTCSService" EXTRA_GAS_LIMIT_FOR_GUARDED_TRANSACTIONS = 50_000 EXTRA_GAS_LIMIT_FOR_RELAYED_TRANSACTIONS = 50_000 + +HEX_PREFIX = "0x" +FALSE_STR_LOWER = "false" +TRUE_STR_LOWER = "true" +STR_PREFIX = "str:" +ADDRESS_PREFIX = "addr:" +MAINCHAIN_ADDRESS_HRP = "erd" diff --git a/multiversx_sdk_cli/contracts.py b/multiversx_sdk_cli/contracts.py index f4b19f2c..c3a47597 100644 --- a/multiversx_sdk_cli/contracts.py +++ b/multiversx_sdk_cli/contracts.py @@ -9,37 +9,20 @@ SmartContractQuery, SmartContractQueryResponse, SmartContractTransactionsFactory, - Token, - TokenComputer, TokenTransfer, Transaction, TransactionOnNetwork, TransactionsFactoryConfig, ) -from multiversx_sdk.abi import ( - Abi, - AddressValue, - BigUIntValue, - BoolValue, - BytesValue, - StringValue, -) +from multiversx_sdk.abi import Abi from multiversx_sdk_cli import errors from multiversx_sdk_cli.base_transactions_controller import BaseTransactionsController -from multiversx_sdk_cli.config import get_address_hrp from multiversx_sdk_cli.guardian_relayer_data import GuardianRelayerData from multiversx_sdk_cli.interfaces import IAccount logger = logging.getLogger("contracts") -HEX_PREFIX = "0x" -FALSE_STR_LOWER = "false" -TRUE_STR_LOWER = "true" -STR_PREFIX = "str:" -ADDRESS_PREFIX = "addr:" -MAINCHAIN_ADDRESS_HRP = "erd" - # fmt: off class INetworkProvider(Protocol): @@ -79,7 +62,7 @@ def prepare_deploy_transaction( ) -> Transaction: args = arguments if arguments else [] if should_prepare_args: - args = self._prepare_args_for_factory(args) + args = self._convert_args_to_typed_values(args) tx = self._factory.create_transaction_for_deploy( sender=owner.address, @@ -120,17 +103,15 @@ def prepare_execute_transaction( gas_limit: int, gas_price: int, value: int, - transfers: Union[list[str], None], + token_transfers: Union[list[TokenTransfer], None], nonce: int, version: int, options: int, guardian_and_relayer_data: GuardianRelayerData, ) -> Transaction: - token_transfers = self._prepare_token_transfers(transfers) if transfers else [] - args = arguments if arguments else [] if should_prepare_args: - args = self._prepare_args_for_factory(args) + args = self._convert_args_to_typed_values(args) tx = self._factory.create_transaction_for_execute( sender=caller.address, @@ -139,7 +120,7 @@ def prepare_execute_transaction( gas_limit=gas_limit, arguments=args, native_transfer_amount=value, - token_transfers=token_transfers, + token_transfers=token_transfers or [], ) tx.nonce = nonce tx.version = version @@ -180,7 +161,7 @@ def prepare_upgrade_transaction( ) -> Transaction: args = arguments if arguments else [] if should_prepare_args: - args = self._prepare_args_for_factory(args) + args = self._convert_args_to_typed_values(args) tx = self._factory.create_transaction_for_upgrade( sender=owner.address, @@ -222,7 +203,7 @@ def query_contract( ) -> list[Any]: args = arguments if arguments else [] if should_prepare_args: - args = self._prepare_args_for_factory(args) + args = self._convert_args_to_typed_values(args) sc_query_controller = SmartContractController(self._config.chain_id, proxy, self._abi) @@ -232,60 +213,3 @@ def query_contract( raise errors.QueryContractError("Couldn't query contract: ", e) return response - - def _prepare_token_transfers(self, transfers: list[str]) -> list[TokenTransfer]: - token_computer = TokenComputer() - token_transfers: list[TokenTransfer] = [] - - for i in range(0, len(transfers) - 1, 2): - identifier = transfers[i] - amount = int(transfers[i + 1]) - nonce = token_computer.extract_nonce_from_extended_identifier(identifier) - - token = Token(identifier, nonce) - transfer = TokenTransfer(token, amount) - token_transfers.append(transfer) - - return token_transfers - - def _prepare_args_for_factory(self, arguments: list[str]) -> list[Any]: - args: list[Any] = [] - - for arg in arguments: - if arg.startswith(HEX_PREFIX): - args.append(BytesValue(self._hex_to_bytes(arg))) - elif arg.isnumeric(): - args.append(BigUIntValue(int(arg))) - elif arg.startswith(ADDRESS_PREFIX): - args.append(AddressValue.new_from_address(Address.new_from_bech32(arg[len(ADDRESS_PREFIX) :]))) - elif arg.startswith(MAINCHAIN_ADDRESS_HRP): - # this flow will be removed in the future - logger.warning( - "Address argument has no prefix. This flow will be removed in the future. Please provide each address using the `addr:` prefix. (e.g. --arguments addr:erd1...)" - ) - args.append(AddressValue.new_from_address(Address.new_from_bech32(arg))) - elif arg.startswith(get_address_hrp()): - args.append(AddressValue.new_from_address(Address.new_from_bech32(arg))) - elif arg.lower() == FALSE_STR_LOWER: - args.append(BoolValue(False)) - elif arg.lower() == TRUE_STR_LOWER: - args.append(BoolValue(True)) - elif arg.startswith(STR_PREFIX): - args.append(StringValue(arg[len(STR_PREFIX) :])) - else: - raise errors.BadUserInput( - f"Unknown argument type for argument: `{arg}`. Use `mxpy contract --help` to check all supported arguments" - ) - - return args - - def _hex_to_bytes(self, arg: str): - argument = arg[len(HEX_PREFIX) :] - argument = argument.upper() - argument = self.ensure_even_length(argument) - return bytes.fromhex(argument) - - def ensure_even_length(self, string: str) -> str: - if len(string) % 2 == 1: - return "0" + string - return string diff --git a/multiversx_sdk_cli/multisig.py b/multiversx_sdk_cli/multisig.py new file mode 100644 index 00000000..be28cfa8 --- /dev/null +++ b/multiversx_sdk_cli/multisig.py @@ -0,0 +1,916 @@ +import logging +from pathlib import Path +from typing import Any, Optional + +from multiversx_sdk import ( + Address, + MultisigTransactionsFactory, + TokenTransfer, + Transaction, + TransactionsFactoryConfig, +) +from multiversx_sdk.abi import Abi + +from multiversx_sdk_cli.base_transactions_controller import BaseTransactionsController +from multiversx_sdk_cli.guardian_relayer_data import GuardianRelayerData +from multiversx_sdk_cli.interfaces import IAccount + +logger = logging.getLogger("multisig") + + +class MultisigWrapper(BaseTransactionsController): + def __init__(self, config: TransactionsFactoryConfig, abi: Abi): + self._factory = MultisigTransactionsFactory(config, abi) + + def prepare_deploy_transaction( + self, + owner: IAccount, + nonce: int, + bytecode: Path, + quorum: int, + board_members: list[Address], + upgradeable: bool, + readable: bool, + payable: bool, + payable_by_sc: bool, + gas_limit: int, + gas_price: int, + version: int, + options: int, + guardian_and_relayer_data: GuardianRelayerData, + ) -> Transaction: + tx = self._factory.create_transaction_for_deploy( + sender=owner.address, + bytecode=bytecode, + quorum=quorum, + board=board_members, + gas_limit=gas_limit, + is_upgradeable=upgradeable, + is_readable=readable, + is_payable=payable, + is_payable_by_sc=payable_by_sc, + ) + tx.nonce = nonce + tx.version = version + tx.options = options + tx.gas_price = gas_price + tx.guardian = guardian_and_relayer_data.guardian_address + tx.relayer = guardian_and_relayer_data.relayer_address + + self.sign_transaction( + transaction=tx, + sender=owner, + guardian=guardian_and_relayer_data.guardian, + relayer=guardian_and_relayer_data.relayer, + guardian_service_url=guardian_and_relayer_data.guardian_service_url, + guardian_2fa_code=guardian_and_relayer_data.guardian_2fa_code, + ) + + return tx + + def prepare_deposit_transaction( + self, + owner: IAccount, + nonce: int, + contract: Address, + gas_limit: int, + gas_price: int, + version: int, + options: int, + guardian_and_relayer_data: GuardianRelayerData, + native_amount: Optional[int] = None, + token_transfers: Optional[list[TokenTransfer]] = None, + ) -> Transaction: + tx = self._factory.create_transaction_for_deposit( + sender=owner.address, + contract=contract, + gas_limit=gas_limit, + native_token_amount=native_amount, + token_transfers=token_transfers, + ) + tx.nonce = nonce + tx.version = version + tx.options = options + tx.gas_price = gas_price + tx.guardian = guardian_and_relayer_data.guardian_address + tx.relayer = guardian_and_relayer_data.relayer_address + + self.sign_transaction( + transaction=tx, + sender=owner, + guardian=guardian_and_relayer_data.guardian, + relayer=guardian_and_relayer_data.relayer, + guardian_service_url=guardian_and_relayer_data.guardian_service_url, + guardian_2fa_code=guardian_and_relayer_data.guardian_2fa_code, + ) + + return tx + + def prepare_discard_action_transaction( + self, + owner: IAccount, + nonce: int, + contract: Address, + action_id: int, + gas_limit: int, + gas_price: int, + version: int, + options: int, + guardian_and_relayer_data: GuardianRelayerData, + ) -> Transaction: + tx = self._factory.create_transaction_for_discard_action( + sender=owner.address, + contract=contract, + action_id=action_id, + gas_limit=gas_limit, + ) + tx.nonce = nonce + tx.version = version + tx.options = options + tx.gas_price = gas_price + tx.guardian = guardian_and_relayer_data.guardian_address + tx.relayer = guardian_and_relayer_data.relayer_address + + self.sign_transaction( + transaction=tx, + sender=owner, + guardian=guardian_and_relayer_data.guardian, + relayer=guardian_and_relayer_data.relayer, + guardian_service_url=guardian_and_relayer_data.guardian_service_url, + guardian_2fa_code=guardian_and_relayer_data.guardian_2fa_code, + ) + + return tx + + def prepare_discard_batch_transaction( + self, + owner: IAccount, + nonce: int, + contract: Address, + action_ids: list[int], + gas_limit: int, + gas_price: int, + version: int, + options: int, + guardian_and_relayer_data: GuardianRelayerData, + ) -> Transaction: + tx = self._factory.create_transaction_for_discard_batch( + sender=owner.address, + contract=contract, + action_ids=action_ids, + gas_limit=gas_limit, + ) + tx.nonce = nonce + tx.version = version + tx.options = options + tx.gas_price = gas_price + tx.guardian = guardian_and_relayer_data.guardian_address + tx.relayer = guardian_and_relayer_data.relayer_address + + self.sign_transaction( + transaction=tx, + sender=owner, + guardian=guardian_and_relayer_data.guardian, + relayer=guardian_and_relayer_data.relayer, + guardian_service_url=guardian_and_relayer_data.guardian_service_url, + guardian_2fa_code=guardian_and_relayer_data.guardian_2fa_code, + ) + + return tx + + def prepare_add_board_member_transaction( + self, + owner: IAccount, + nonce: int, + contract: Address, + board_member: Address, + gas_limit: int, + gas_price: int, + version: int, + options: int, + guardian_and_relayer_data: GuardianRelayerData, + ) -> Transaction: + tx = self._factory.create_transaction_for_propose_add_board_member( + sender=owner.address, + contract=contract, + board_member=board_member, + gas_limit=gas_limit, + ) + tx.nonce = nonce + tx.version = version + tx.options = options + tx.gas_price = gas_price + tx.guardian = guardian_and_relayer_data.guardian_address + tx.relayer = guardian_and_relayer_data.relayer_address + + self.sign_transaction( + transaction=tx, + sender=owner, + guardian=guardian_and_relayer_data.guardian, + relayer=guardian_and_relayer_data.relayer, + guardian_service_url=guardian_and_relayer_data.guardian_service_url, + guardian_2fa_code=guardian_and_relayer_data.guardian_2fa_code, + ) + + return tx + + def prepare_add_proposer_transaction( + self, + owner: IAccount, + nonce: int, + contract: Address, + proposer: Address, + gas_limit: int, + gas_price: int, + version: int, + options: int, + guardian_and_relayer_data: GuardianRelayerData, + ) -> Transaction: + tx = self._factory.create_transaction_for_propose_add_proposer( + sender=owner.address, + contract=contract, + proposer=proposer, + gas_limit=gas_limit, + ) + tx.nonce = nonce + tx.version = version + tx.options = options + tx.gas_price = gas_price + tx.guardian = guardian_and_relayer_data.guardian_address + tx.relayer = guardian_and_relayer_data.relayer_address + + self.sign_transaction( + transaction=tx, + sender=owner, + guardian=guardian_and_relayer_data.guardian, + relayer=guardian_and_relayer_data.relayer, + guardian_service_url=guardian_and_relayer_data.guardian_service_url, + guardian_2fa_code=guardian_and_relayer_data.guardian_2fa_code, + ) + + return tx + + def prepare_remove_user_transaction( + self, + owner: IAccount, + nonce: int, + contract: Address, + user: Address, + gas_limit: int, + gas_price: int, + version: int, + options: int, + guardian_and_relayer_data: GuardianRelayerData, + ) -> Transaction: + tx = self._factory.create_transaction_for_propose_remove_user( + sender=owner.address, + contract=contract, + user=user, + gas_limit=gas_limit, + ) + tx.nonce = nonce + tx.version = version + tx.options = options + tx.gas_price = gas_price + tx.guardian = guardian_and_relayer_data.guardian_address + tx.relayer = guardian_and_relayer_data.relayer_address + + self.sign_transaction( + transaction=tx, + sender=owner, + guardian=guardian_and_relayer_data.guardian, + relayer=guardian_and_relayer_data.relayer, + guardian_service_url=guardian_and_relayer_data.guardian_service_url, + guardian_2fa_code=guardian_and_relayer_data.guardian_2fa_code, + ) + + return tx + + def prepare_change_quorum_transaction( + self, + owner: IAccount, + nonce: int, + contract: Address, + quorum: int, + gas_limit: int, + gas_price: int, + version: int, + options: int, + guardian_and_relayer_data: GuardianRelayerData, + ) -> Transaction: + tx = self._factory.create_transaction_for_propose_change_quorum( + sender=owner.address, + contract=contract, + quorum=quorum, + gas_limit=gas_limit, + ) + tx.nonce = nonce + tx.version = version + tx.options = options + tx.gas_price = gas_price + tx.guardian = guardian_and_relayer_data.guardian_address + tx.relayer = guardian_and_relayer_data.relayer_address + + self.sign_transaction( + transaction=tx, + sender=owner, + guardian=guardian_and_relayer_data.guardian, + relayer=guardian_and_relayer_data.relayer, + guardian_service_url=guardian_and_relayer_data.guardian_service_url, + guardian_2fa_code=guardian_and_relayer_data.guardian_2fa_code, + ) + + return tx + + def prepare_transfer_execute_transaction( + self, + owner: IAccount, + nonce: int, + contract: Address, + receiver: Address, + native_token_amount: int, + gas_limit: int, + gas_price: int, + version: int, + options: int, + should_prepare_args_for_factory: bool, + guardian_and_relayer_data: GuardianRelayerData, + opt_gas_limit: Optional[int] = None, + abi: Optional[Abi] = None, + function: Optional[str] = None, + arguments: Optional[list[Any]] = None, + ) -> Transaction: + args = arguments if arguments else [] + if should_prepare_args_for_factory: + args = self._convert_args_to_typed_values(args) + + tx = self._factory.create_transaction_for_propose_transfer_execute( + sender=owner.address, + contract=contract, + receiver=receiver, + native_token_amount=native_token_amount, + gas_limit=gas_limit, + opt_gas_limit=opt_gas_limit, + abi=abi, + function=function, + arguments=args, + ) + tx.nonce = nonce + tx.version = version + tx.options = options + tx.gas_price = gas_price + tx.guardian = guardian_and_relayer_data.guardian_address + tx.relayer = guardian_and_relayer_data.relayer_address + + self.sign_transaction( + transaction=tx, + sender=owner, + guardian=guardian_and_relayer_data.guardian, + relayer=guardian_and_relayer_data.relayer, + guardian_service_url=guardian_and_relayer_data.guardian_service_url, + guardian_2fa_code=guardian_and_relayer_data.guardian_2fa_code, + ) + + return tx + + def prepare_transfer_execute_esdt_transaction( + self, + owner: IAccount, + nonce: int, + contract: Address, + receiver: Address, + gas_limit: int, + gas_price: int, + version: int, + options: int, + should_prepare_args_for_factory: bool, + guardian_and_relayer_data: GuardianRelayerData, + token_transfers: list[TokenTransfer], + opt_gas_limit: Optional[int] = None, + abi: Optional[Abi] = None, + function: Optional[str] = None, + arguments: Optional[list[Any]] = None, + ) -> Transaction: + args = arguments if arguments else [] + if should_prepare_args_for_factory: + args = self._convert_args_to_typed_values(args) + + tx = self._factory.create_transaction_for_propose_transfer_esdt_execute( + sender=owner.address, + contract=contract, + receiver=receiver, + token_transfers=token_transfers, + gas_limit=gas_limit, + opt_gas_limit=opt_gas_limit, + abi=abi, + function=function, + arguments=args, + ) + tx.nonce = nonce + tx.version = version + tx.options = options + tx.gas_price = gas_price + tx.guardian = guardian_and_relayer_data.guardian_address + tx.relayer = guardian_and_relayer_data.relayer_address + + self.sign_transaction( + transaction=tx, + sender=owner, + guardian=guardian_and_relayer_data.guardian, + relayer=guardian_and_relayer_data.relayer, + guardian_service_url=guardian_and_relayer_data.guardian_service_url, + guardian_2fa_code=guardian_and_relayer_data.guardian_2fa_code, + ) + + return tx + + def prepare_async_call_transaction( + self, + owner: IAccount, + nonce: int, + contract: Address, + receiver: Address, + gas_limit: int, + gas_price: int, + version: int, + options: int, + should_prepare_args_for_factory: bool, + guardian_and_relayer_data: GuardianRelayerData, + native_token_amount: int = 0, + token_transfers: Optional[list[TokenTransfer]] = None, + opt_gas_limit: Optional[int] = None, + abi: Optional[Abi] = None, + function: Optional[str] = None, + arguments: Optional[list[Any]] = None, + ) -> Transaction: + args = arguments if arguments else [] + if should_prepare_args_for_factory: + args = self._convert_args_to_typed_values(args) + + tx = self._factory.create_transaction_for_propose_async_call( + sender=owner.address, + contract=contract, + receiver=receiver, + gas_limit=gas_limit, + native_token_amount=native_token_amount, + token_transfers=token_transfers, + opt_gas_limit=opt_gas_limit, + abi=abi, + function=function, + arguments=args, + ) + tx.nonce = nonce + tx.version = version + tx.options = options + tx.gas_price = gas_price + tx.guardian = guardian_and_relayer_data.guardian_address + tx.relayer = guardian_and_relayer_data.relayer_address + + self.sign_transaction( + transaction=tx, + sender=owner, + guardian=guardian_and_relayer_data.guardian, + relayer=guardian_and_relayer_data.relayer, + guardian_service_url=guardian_and_relayer_data.guardian_service_url, + guardian_2fa_code=guardian_and_relayer_data.guardian_2fa_code, + ) + + return tx + + def prepare_contract_deploy_from_source_transaction( + self, + owner: IAccount, + nonce: int, + contract: Address, + contract_to_copy: Address, + gas_limit: int, + gas_price: int, + version: int, + options: int, + upgradeable: bool, + readable: bool, + payable: bool, + payable_by_sc: bool, + should_prepare_args_for_factory: bool, + guardian_and_relayer_data: GuardianRelayerData, + native_token_amount: int = 0, + abi: Optional[Abi] = None, + arguments: Optional[list[Any]] = None, + ) -> Transaction: + args = arguments if arguments else [] + if should_prepare_args_for_factory: + args = self._convert_args_to_typed_values(args) + + tx = self._factory.create_transaction_for_propose_contract_deploy_from_source( + sender=owner.address, + contract=contract, + gas_limit=gas_limit, + contract_to_copy=contract_to_copy, + is_upgradeable=upgradeable, + is_readable=readable, + is_payable=payable, + is_payable_by_sc=payable_by_sc, + native_token_amount=native_token_amount, + abi=abi, + arguments=args, + ) + tx.nonce = nonce + tx.version = version + tx.options = options + tx.gas_price = gas_price + tx.guardian = guardian_and_relayer_data.guardian_address + tx.relayer = guardian_and_relayer_data.relayer_address + + self.sign_transaction( + transaction=tx, + sender=owner, + guardian=guardian_and_relayer_data.guardian, + relayer=guardian_and_relayer_data.relayer, + guardian_service_url=guardian_and_relayer_data.guardian_service_url, + guardian_2fa_code=guardian_and_relayer_data.guardian_2fa_code, + ) + + return tx + + def prepare_contract_upgrade_from_source_transaction( + self, + owner: IAccount, + nonce: int, + contract: Address, + contract_to_upgrade: Address, + contract_to_copy: Address, + gas_limit: int, + gas_price: int, + version: int, + options: int, + upgradeable: bool, + readable: bool, + payable: bool, + payable_by_sc: bool, + should_prepare_args_for_factory: bool, + guardian_and_relayer_data: GuardianRelayerData, + native_token_amount: int = 0, + abi: Optional[Abi] = None, + arguments: Optional[list[Any]] = None, + ) -> Transaction: + args = arguments if arguments else [] + if should_prepare_args_for_factory: + args = self._convert_args_to_typed_values(args) + + tx = self._factory.create_transaction_for_propose_contract_upgrade_from_source( + sender=owner.address, + contract=contract, + contract_to_upgrade=contract_to_upgrade, + contract_to_copy=contract_to_copy, + gas_limit=gas_limit, + is_upgradeable=upgradeable, + is_readable=readable, + is_payable=payable, + is_payable_by_sc=payable_by_sc, + native_token_amount=native_token_amount, + abi=abi, + arguments=args, + ) + tx.nonce = nonce + tx.version = version + tx.options = options + tx.gas_price = gas_price + tx.guardian = guardian_and_relayer_data.guardian_address + tx.relayer = guardian_and_relayer_data.relayer_address + + self.sign_transaction( + transaction=tx, + sender=owner, + guardian=guardian_and_relayer_data.guardian, + relayer=guardian_and_relayer_data.relayer, + guardian_service_url=guardian_and_relayer_data.guardian_service_url, + guardian_2fa_code=guardian_and_relayer_data.guardian_2fa_code, + ) + + return tx + + def prepare_sign_action_transaction( + self, + owner: IAccount, + nonce: int, + contract: Address, + action_id: int, + gas_limit: int, + gas_price: int, + version: int, + options: int, + guardian_and_relayer_data: GuardianRelayerData, + ) -> Transaction: + tx = self._factory.create_transaction_for_sign_action( + sender=owner.address, + contract=contract, + action_id=action_id, + gas_limit=gas_limit, + ) + tx.nonce = nonce + tx.version = version + tx.options = options + tx.gas_price = gas_price + tx.guardian = guardian_and_relayer_data.guardian_address + tx.relayer = guardian_and_relayer_data.relayer_address + + self.sign_transaction( + transaction=tx, + sender=owner, + guardian=guardian_and_relayer_data.guardian, + relayer=guardian_and_relayer_data.relayer, + guardian_service_url=guardian_and_relayer_data.guardian_service_url, + guardian_2fa_code=guardian_and_relayer_data.guardian_2fa_code, + ) + + return tx + + def prepare_sign_batch_transaction( + self, + owner: IAccount, + nonce: int, + contract: Address, + batch_id: int, + gas_limit: int, + gas_price: int, + version: int, + options: int, + guardian_and_relayer_data: GuardianRelayerData, + ) -> Transaction: + tx = self._factory.create_transaction_for_sign_batch( + sender=owner.address, + contract=contract, + batch_id=batch_id, + gas_limit=gas_limit, + ) + tx.nonce = nonce + tx.version = version + tx.options = options + tx.gas_price = gas_price + tx.guardian = guardian_and_relayer_data.guardian_address + tx.relayer = guardian_and_relayer_data.relayer_address + + self.sign_transaction( + transaction=tx, + sender=owner, + guardian=guardian_and_relayer_data.guardian, + relayer=guardian_and_relayer_data.relayer, + guardian_service_url=guardian_and_relayer_data.guardian_service_url, + guardian_2fa_code=guardian_and_relayer_data.guardian_2fa_code, + ) + + return tx + + def prepare_sign_and_perform_transaction( + self, + owner: IAccount, + nonce: int, + contract: Address, + action_id: int, + gas_limit: int, + gas_price: int, + version: int, + options: int, + guardian_and_relayer_data: GuardianRelayerData, + ) -> Transaction: + tx = self._factory.create_transaction_for_sign_and_perform( + sender=owner.address, + contract=contract, + action_id=action_id, + gas_limit=gas_limit, + ) + tx.nonce = nonce + tx.version = version + tx.options = options + tx.gas_price = gas_price + tx.guardian = guardian_and_relayer_data.guardian_address + tx.relayer = guardian_and_relayer_data.relayer_address + + self.sign_transaction( + transaction=tx, + sender=owner, + guardian=guardian_and_relayer_data.guardian, + relayer=guardian_and_relayer_data.relayer, + guardian_service_url=guardian_and_relayer_data.guardian_service_url, + guardian_2fa_code=guardian_and_relayer_data.guardian_2fa_code, + ) + + return tx + + def prepare_sign_batch_and_perform_transaction( + self, + owner: IAccount, + nonce: int, + contract: Address, + batch_id: int, + gas_limit: int, + gas_price: int, + version: int, + options: int, + guardian_and_relayer_data: GuardianRelayerData, + ) -> Transaction: + tx = self._factory.create_transaction_for_sign_batch_and_perform( + sender=owner.address, + contract=contract, + batch_id=batch_id, + gas_limit=gas_limit, + ) + tx.nonce = nonce + tx.version = version + tx.options = options + tx.gas_price = gas_price + tx.guardian = guardian_and_relayer_data.guardian_address + tx.relayer = guardian_and_relayer_data.relayer_address + + self.sign_transaction( + transaction=tx, + sender=owner, + guardian=guardian_and_relayer_data.guardian, + relayer=guardian_and_relayer_data.relayer, + guardian_service_url=guardian_and_relayer_data.guardian_service_url, + guardian_2fa_code=guardian_and_relayer_data.guardian_2fa_code, + ) + + return tx + + def prepare_unsign_action_transaction( + self, + owner: IAccount, + nonce: int, + contract: Address, + action_id: int, + gas_limit: int, + gas_price: int, + version: int, + options: int, + guardian_and_relayer_data: GuardianRelayerData, + ) -> Transaction: + tx = self._factory.create_transaction_for_unsign_action( + sender=owner.address, + contract=contract, + action_id=action_id, + gas_limit=gas_limit, + ) + tx.nonce = nonce + tx.version = version + tx.options = options + tx.gas_price = gas_price + tx.guardian = guardian_and_relayer_data.guardian_address + tx.relayer = guardian_and_relayer_data.relayer_address + + self.sign_transaction( + transaction=tx, + sender=owner, + guardian=guardian_and_relayer_data.guardian, + relayer=guardian_and_relayer_data.relayer, + guardian_service_url=guardian_and_relayer_data.guardian_service_url, + guardian_2fa_code=guardian_and_relayer_data.guardian_2fa_code, + ) + + return tx + + def prepare_unsign_batch_transaction( + self, + owner: IAccount, + nonce: int, + contract: Address, + batch_id: int, + gas_limit: int, + gas_price: int, + version: int, + options: int, + guardian_and_relayer_data: GuardianRelayerData, + ) -> Transaction: + tx = self._factory.create_transaction_for_unsign_batch( + sender=owner.address, + contract=contract, + batch_id=batch_id, + gas_limit=gas_limit, + ) + tx.nonce = nonce + tx.version = version + tx.options = options + tx.gas_price = gas_price + tx.guardian = guardian_and_relayer_data.guardian_address + tx.relayer = guardian_and_relayer_data.relayer_address + + self.sign_transaction( + transaction=tx, + sender=owner, + guardian=guardian_and_relayer_data.guardian, + relayer=guardian_and_relayer_data.relayer, + guardian_service_url=guardian_and_relayer_data.guardian_service_url, + guardian_2fa_code=guardian_and_relayer_data.guardian_2fa_code, + ) + + return tx + + def prepare_unsign_for_outdated_board_members_transaction( + self, + owner: IAccount, + nonce: int, + contract: Address, + action_id: int, + outdated_board_members: list[int], + gas_limit: int, + gas_price: int, + version: int, + options: int, + guardian_and_relayer_data: GuardianRelayerData, + ) -> Transaction: + tx = self._factory.create_transaction_for_unsign_for_outdated_board_members( + sender=owner.address, + contract=contract, + action_id=action_id, + outdated_board_members=outdated_board_members, + gas_limit=gas_limit, + ) + tx.nonce = nonce + tx.version = version + tx.options = options + tx.gas_price = gas_price + tx.guardian = guardian_and_relayer_data.guardian_address + tx.relayer = guardian_and_relayer_data.relayer_address + + self.sign_transaction( + transaction=tx, + sender=owner, + guardian=guardian_and_relayer_data.guardian, + relayer=guardian_and_relayer_data.relayer, + guardian_service_url=guardian_and_relayer_data.guardian_service_url, + guardian_2fa_code=guardian_and_relayer_data.guardian_2fa_code, + ) + + return tx + + def prepare_perform_action_transaction( + self, + owner: IAccount, + nonce: int, + contract: Address, + action_id: int, + gas_limit: int, + gas_price: int, + version: int, + options: int, + guardian_and_relayer_data: GuardianRelayerData, + ) -> Transaction: + tx = self._factory.create_transaction_for_perform_action( + sender=owner.address, + contract=contract, + action_id=action_id, + gas_limit=gas_limit, + ) + tx.nonce = nonce + tx.version = version + tx.options = options + tx.gas_price = gas_price + tx.guardian = guardian_and_relayer_data.guardian_address + tx.relayer = guardian_and_relayer_data.relayer_address + + self.sign_transaction( + transaction=tx, + sender=owner, + guardian=guardian_and_relayer_data.guardian, + relayer=guardian_and_relayer_data.relayer, + guardian_service_url=guardian_and_relayer_data.guardian_service_url, + guardian_2fa_code=guardian_and_relayer_data.guardian_2fa_code, + ) + + return tx + + def prepare_perform_batch_transaction( + self, + owner: IAccount, + nonce: int, + contract: Address, + batch_id: int, + gas_limit: int, + gas_price: int, + version: int, + options: int, + guardian_and_relayer_data: GuardianRelayerData, + ) -> Transaction: + tx = self._factory.create_transaction_for_perform_batch( + sender=owner.address, + contract=contract, + batch_id=batch_id, + gas_limit=gas_limit, + ) + tx.nonce = nonce + tx.version = version + tx.options = options + tx.gas_price = gas_price + tx.guardian = guardian_and_relayer_data.guardian_address + tx.relayer = guardian_and_relayer_data.relayer_address + + self.sign_transaction( + transaction=tx, + sender=owner, + guardian=guardian_and_relayer_data.guardian, + relayer=guardian_and_relayer_data.relayer, + guardian_service_url=guardian_and_relayer_data.guardian_service_url, + guardian_2fa_code=guardian_and_relayer_data.guardian_2fa_code, + ) + + return tx 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..e280e867 --- /dev/null +++ b/multiversx_sdk_cli/tests/test_cli_multisig.py @@ -0,0 +1,958 @@ +import base64 +import json +from pathlib import Path +from typing import Any + +from multiversx_sdk_cli.cli import main + +testdata = Path(__file__).parent / "testdata" +user = testdata / "testUser.pem" +user_address = "erd1cqqxak4wun7508e0yj9ng843r6hv4mzd0hhpjpsejkpn9wa9yq8sj7u2u5" +multisig_abi = testdata / "multisig.abi.json" +contract_address = "erd1qqqqqqqqqqqqqpgqe832k3l6d02ww7l9cvqum25539nmmdxa9ncsdutjuf" +contract_address_hex = "00000000000000000500c9e2ab47fa6bd4e77be5c301cdaa948967bdb4dd2cf1" +bob = "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx" +adder_abi = testdata / "adder.abi.json" + + +def test_deploy_multisig(capsys: Any): + multisig_bytecode = (testdata / "multisig.wasm").read_bytes() + + return_code = main( + [ + "multisig", + "deploy", + "--bytecode", + str(testdata / "multisig.wasm"), + "--abi", + str(multisig_abi), + "--quorum", + "1", + "--board-members", + user_address, + "--pem", + str(user), + "--nonce", + "0", + "--gas-limit", + "100000000", + "--chain", + "D", + ] + ) + assert not return_code + tx = get_transaction(capsys) + + assert tx["sender"] == user_address + assert tx["receiver"] == "erd1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq6gq4hu" + assert tx["value"] == "0" + assert tx["gasLimit"] == 100_000_000 + assert tx["chainID"] == "D" + data = tx["data"] + data = base64.b64decode(data).decode() + assert f"{multisig_bytecode.hex()}@0500@0504@02@c0006edaaee4fd479f2f248b341eb11eaecaec4d7dee190619958332bba5200f" + + +def test_deposit_native_token(capsys: Any): + return_code = main( + [ + "multisig", + "deposit", + "--contract", + str(contract_address), + "--abi", + str(multisig_abi), + "--pem", + str(user), + "--nonce", + "0", + "--gas-limit", + "60000000", + "--chain", + "D", + "--value", + "1000000000000000000", + ] + ) + assert not return_code + tx = get_transaction(capsys) + + assert tx["sender"] == user_address + assert tx["receiver"] == contract_address + assert tx["value"] == "1000000000000000000" + assert tx["gasLimit"] == 60_000_000 + assert tx["chainID"] == "D" + data = tx["data"] + data = base64.b64decode(data).decode() + assert data == "deposit" + + +def test_deposit_esdt(capsys: Any): + return_code = main( + [ + "multisig", + "deposit", + "--contract", + str(contract_address), + "--abi", + str(multisig_abi), + "--pem", + str(user), + "--nonce", + "0", + "--gas-limit", + "60000000", + "--chain", + "D", + "--token-transfers", + "MYTKN-a584f9", + "100000", + "SFT-1bc261-01", + "1", + ] + ) + assert not return_code + tx = get_transaction(capsys) + + assert tx["sender"] == user_address + assert tx["receiver"] == user_address + assert tx["value"] == "0" + assert tx["gasLimit"] == 60_000_000 + assert tx["chainID"] == "D" + data = tx["data"] + data = base64.b64decode(data).decode() + assert ( + data + == f"MultiESDTNFTTransfer@{contract_address_hex}@02@4d59544b4e2d613538346639@@0186a0@5346542d316263323631@01@01@6465706f736974" + ) + + +def test_discard_action(capsys: Any): + return_code = main( + [ + "multisig", + "discard-action", + "--contract", + str(contract_address), + "--abi", + str(multisig_abi), + "--action", + "7", + "--pem", + str(user), + "--nonce", + "0", + "--gas-limit", + "60000000", + "--chain", + "D", + ] + ) + assert not return_code + tx = get_transaction(capsys) + + assert tx["sender"] == user_address + assert tx["receiver"] == contract_address + assert tx["value"] == "0" + assert tx["gasLimit"] == 60_000_000 + assert tx["chainID"] == "D" + data = tx["data"] + data = base64.b64decode(data).decode() + assert data == "discardAction@07" + + +def test_discard_batch(capsys: Any): + return_code = main( + [ + "multisig", + "discard-batch", + "--contract", + str(contract_address), + "--abi", + str(multisig_abi), + "--action-ids", + "7", + "8", + "--pem", + str(user), + "--nonce", + "0", + "--gas-limit", + "60000000", + "--chain", + "D", + ] + ) + assert not return_code + tx = get_transaction(capsys) + + assert tx["sender"] == user_address + assert tx["receiver"] == contract_address + assert tx["value"] == "0" + assert tx["gasLimit"] == 60_000_000 + assert tx["chainID"] == "D" + data = tx["data"] + data = base64.b64decode(data).decode() + assert data == "discardBatch@07@08" + + +def test_add_board_member(capsys: Any): + return_code = main( + [ + "multisig", + "add-board-member", + "--contract", + str(contract_address), + "--abi", + str(multisig_abi), + "--board-member", + bob, + "--pem", + str(user), + "--nonce", + "0", + "--gas-limit", + "60000000", + "--chain", + "D", + ] + ) + assert not return_code + tx = get_transaction(capsys) + + assert tx["sender"] == user_address + assert tx["receiver"] == contract_address + assert tx["value"] == "0" + assert tx["gasLimit"] == 60_000_000 + assert tx["chainID"] == "D" + data = tx["data"] + data = base64.b64decode(data).decode() + assert data == "proposeAddBoardMember@8049d639e5a6980d1cd2392abcce41029cda74a1563523a202f09641cc2618f8" + + +def test_add_proposer(capsys: Any): + return_code = main( + [ + "multisig", + "add-proposer", + "--contract", + str(contract_address), + "--abi", + str(multisig_abi), + "--proposer", + bob, + "--pem", + str(user), + "--nonce", + "0", + "--gas-limit", + "60000000", + "--chain", + "D", + ] + ) + assert not return_code + tx = get_transaction(capsys) + + assert tx["sender"] == user_address + assert tx["receiver"] == contract_address + assert tx["value"] == "0" + assert tx["gasLimit"] == 60_000_000 + assert tx["chainID"] == "D" + data = tx["data"] + data = base64.b64decode(data).decode() + assert data == "proposeAddProposer@8049d639e5a6980d1cd2392abcce41029cda74a1563523a202f09641cc2618f8" + + +def test_remove_user(capsys: Any): + return_code = main( + [ + "multisig", + "remove-user", + "--contract", + str(contract_address), + "--abi", + str(multisig_abi), + "--user", + bob, + "--pem", + str(user), + "--nonce", + "0", + "--gas-limit", + "60000000", + "--chain", + "D", + ] + ) + assert not return_code + tx = get_transaction(capsys) + + assert tx["sender"] == user_address + assert tx["receiver"] == contract_address + assert tx["value"] == "0" + assert tx["gasLimit"] == 60_000_000 + assert tx["chainID"] == "D" + data = tx["data"] + data = base64.b64decode(data).decode() + assert data == "proposeRemoveUser@8049d639e5a6980d1cd2392abcce41029cda74a1563523a202f09641cc2618f8" + + +def test_change_quorum(capsys: Any): + return_code = main( + [ + "multisig", + "change-quorum", + "--contract", + str(contract_address), + "--abi", + str(multisig_abi), + "--quorum", + "10", + "--pem", + str(user), + "--nonce", + "0", + "--gas-limit", + "60000000", + "--chain", + "D", + ] + ) + assert not return_code + tx = get_transaction(capsys) + + assert tx["sender"] == user_address + assert tx["receiver"] == contract_address + assert tx["value"] == "0" + assert tx["gasLimit"] == 60_000_000 + assert tx["chainID"] == "D" + data = tx["data"] + data = base64.b64decode(data).decode() + assert data == "proposeChangeQuorum@0a" + + +def test_transfer_and_execute_with_abi(capsys: Any): + return_code = main( + [ + "multisig", + "transfer-and-execute", + "--contract", + str(contract_address), + "--abi", + str(multisig_abi), + "--opt-gas-limit", + "1000000", + "--contract-abi", + str(adder_abi), + "--function", + "add", + "--arguments", + "7", + "--receiver", + "erd1qqqqqqqqqqqqqpgq0rffvv4vk9vesqplv9ws55fxzdfaspqa8cfszy2hms", + "--value", + "1000000000000000000", + "--pem", + str(user), + "--nonce", + "0", + "--gas-limit", + "60000000", + "--chain", + "D", + ] + ) + assert not return_code + tx = get_transaction(capsys) + + assert tx["sender"] == user_address + assert tx["receiver"] == contract_address + assert tx["value"] == "0" + assert tx["gasLimit"] == 60_000_000 + assert tx["chainID"] == "D" + data = tx["data"] + data = base64.b64decode(data).decode() + assert ( + data + == "proposeTransferExecute@0000000000000000050078d29632acb15998003f615d0a51261353d8041d3e13@0de0b6b3a7640000@0100000000000f4240@616464@07" + ) + + +def test_transfer_and_execute_without_abi(capsys: Any): + return_code = main( + [ + "multisig", + "transfer-and-execute", + "--contract", + str(contract_address), + "--abi", + str(multisig_abi), + "--opt-gas-limit", + "1000000", + "--function", + "add", + "--arguments", + "0x07", + "--receiver", + "erd1qqqqqqqqqqqqqpgq0rffvv4vk9vesqplv9ws55fxzdfaspqa8cfszy2hms", + "--value", + "1000000000000000000", + "--pem", + str(user), + "--nonce", + "0", + "--gas-limit", + "60000000", + "--chain", + "D", + ] + ) + assert not return_code + tx = get_transaction(capsys) + + assert tx["sender"] == user_address + assert tx["receiver"] == contract_address + assert tx["value"] == "0" + assert tx["gasLimit"] == 60_000_000 + assert tx["chainID"] == "D" + data = tx["data"] + data = base64.b64decode(data).decode() + assert ( + data + == "proposeTransferExecute@0000000000000000050078d29632acb15998003f615d0a51261353d8041d3e13@0de0b6b3a7640000@0100000000000f4240@616464@07" + ) + + +def test_transfer_and_execute_without_execute(capsys: Any): + return_code = main( + [ + "multisig", + "transfer-and-execute", + "--contract", + str(contract_address), + "--abi", + str(multisig_abi), + "--receiver", + "erd1qqqqqqqqqqqqqpgq0rffvv4vk9vesqplv9ws55fxzdfaspqa8cfszy2hms", + "--value", + "1000000000000000000", + "--pem", + str(user), + "--nonce", + "0", + "--gas-limit", + "60000000", + "--chain", + "D", + ] + ) + assert not return_code + tx = get_transaction(capsys) + + assert tx["sender"] == user_address + assert tx["receiver"] == contract_address + assert tx["value"] == "0" + assert tx["gasLimit"] == 60_000_000 + assert tx["chainID"] == "D" + data = tx["data"] + data = base64.b64decode(data).decode() + assert ( + data + == "proposeTransferExecute@0000000000000000050078d29632acb15998003f615d0a51261353d8041d3e13@0de0b6b3a7640000@" + ) + + +def test_transfer_and_execute_esdt(capsys: Any): + return_code = main( + [ + "multisig", + "transfer-and-execute-esdt", + "--contract", + str(contract_address), + "--abi", + str(multisig_abi), + "--token-transfers", + "ALICE-5627f1", + "10", + "--opt-gas-limit", + "5000000", + "--function", + "distribute", + "--receiver", + "erd1qqqqqqqqqqqqqpgqfxlljcaalgl2qfcnxcsftheju0ts36kvl3ts3qkewe", + "--pem", + str(user), + "--nonce", + "0", + "--gas-limit", + "60000000", + "--chain", + "D", + ] + ) + assert not return_code + tx = get_transaction(capsys) + + assert tx["sender"] == user_address + assert tx["receiver"] == contract_address + assert tx["value"] == "0" + assert tx["gasLimit"] == 60_000_000 + assert tx["chainID"] == "D" + data = tx["data"] + data = base64.b64decode(data).decode() + assert ( + data + == "proposeTransferExecuteEsdt@0000000000000000050049bff963bdfa3ea02713362095df32e3d708eaccfc57@0000000c414c4943452d3536323766310000000000000000000000010a@0100000000004c4b40@3634363937333734373236393632373537343635" + ) + + +def test_async_call(capsys: Any): + return_code = main( + [ + "multisig", + "async-call", + "--contract", + str(contract_address), + "--abi", + str(multisig_abi), + "--opt-gas-limit", + "5000000", + "--contract-abi", + str(adder_abi), + "--function", + "add", + "--arguments", + "7", + "--receiver", + "erd1qqqqqqqqqqqqqpgqdvmhpxxmwv2vfz3sfpggzfyl5qznuz5x05vq5y37ql", + "--pem", + str(user), + "--nonce", + "0", + "--gas-limit", + "60000000", + "--chain", + "D", + ] + ) + assert not return_code + tx = get_transaction(capsys) + + assert tx["sender"] == user_address + assert tx["receiver"] == contract_address + assert tx["value"] == "0" + assert tx["gasLimit"] == 60_000_000 + assert tx["chainID"] == "D" + data = tx["data"] + data = base64.b64decode(data).decode() + assert ( + data + == "proposeAsyncCall@000000000000000005006b377098db7314c48a30485081249fa0053e0a867d18@@0100000000004c4b40@616464@07" + ) + + +def test_sc_deploy_from_source(capsys: Any): + return_code = main( + [ + "multisig", + "deploy-from-source", + "--contract", + str(contract_address), + "--abi", + str(multisig_abi), + "--contract-to-copy", + "erd1qqqqqqqqqqqqqpgqsuxsgykwm6r3s5apct2g5a2rcpe7kw0ed8ssf6h9f6", + "--contract-abi", + str(adder_abi), + "--arguments", + "0", + "--pem", + str(user), + "--nonce", + "0", + "--gas-limit", + "60000000", + "--chain", + "D", + "--value", + "50000000000000000", + ] + ) + assert not return_code + tx = get_transaction(capsys) + + assert tx["sender"] == user_address + assert tx["receiver"] == contract_address + assert tx["value"] == "0" + assert tx["gasLimit"] == 60_000_000 + assert tx["chainID"] == "D" + data = tx["data"] + data = base64.b64decode(data).decode() + assert ( + data + == "proposeSCDeployFromSource@b1a2bc2ec50000@00000000000000000500870d0412cede871853a1c2d48a7543c073eb39f969e1@0500@" + ) + + +def test_sc_upgrade_from_source(capsys: Any): + return_code = main( + [ + "multisig", + "upgrade-from-source", + "--contract", + str(contract_address), + "--abi", + str(multisig_abi), + "--contract-to-upgrade", + "erd1qqqqqqqqqqqqqpgqsuxsgykwm6r3s5apct2g5a2rcpe7kw0ed8ssf6h9f6", + "--contract-to-copy", + "erd1qqqqqqqqqqqqqpgqsuxsgykwm6r3s5apct2g5a2rcpe7kw0ed8ssf6h9f6", + "--arguments", + "0x00", + "--pem", + str(user), + "--nonce", + "0", + "--gas-limit", + "60000000", + "--chain", + "D", + "--value", + "50000000000000000", + ] + ) + assert not return_code + tx = get_transaction(capsys) + + assert tx["sender"] == user_address + assert tx["receiver"] == contract_address + assert tx["value"] == "0" + assert tx["gasLimit"] == 60_000_000 + assert tx["chainID"] == "D" + data = tx["data"] + data = base64.b64decode(data).decode() + assert ( + data + == "proposeSCUpgradeFromSource@00000000000000000500870d0412cede871853a1c2d48a7543c073eb39f969e1@b1a2bc2ec50000@00000000000000000500870d0412cede871853a1c2d48a7543c073eb39f969e1@0500@00" + ) + + +def test_sign_action(capsys: Any): + return_code = main( + [ + "multisig", + "sign-action", + "--contract", + str(contract_address), + "--abi", + str(multisig_abi), + "--action", + "7", + "--pem", + str(user), + "--nonce", + "0", + "--gas-limit", + "1000000", + "--chain", + "D", + ] + ) + assert not return_code + tx = get_transaction(capsys) + + assert tx["sender"] == user_address + assert tx["receiver"] == contract_address + assert tx["value"] == "0" + assert tx["gasLimit"] == 1_000_000 + assert tx["chainID"] == "D" + data = tx["data"] + data = base64.b64decode(data).decode() + assert data == "sign@07" + + +def test_sign_batch(capsys: Any): + return_code = main( + [ + "multisig", + "sign-batch", + "--contract", + str(contract_address), + "--abi", + str(multisig_abi), + "--batch", + "7", + "--pem", + str(user), + "--nonce", + "0", + "--gas-limit", + "1000000", + "--chain", + "D", + ] + ) + assert not return_code + tx = get_transaction(capsys) + + assert tx["sender"] == user_address + assert tx["receiver"] == contract_address + assert tx["value"] == "0" + assert tx["gasLimit"] == 1_000_000 + assert tx["chainID"] == "D" + data = tx["data"] + data = base64.b64decode(data).decode() + assert data == "signBatch@07" + + +def test_sign_and_perform(capsys: Any): + return_code = main( + [ + "multisig", + "sign-and-perform", + "--contract", + str(contract_address), + "--abi", + str(multisig_abi), + "--action", + "7", + "--pem", + str(user), + "--nonce", + "0", + "--gas-limit", + "1000000", + "--chain", + "D", + ] + ) + assert not return_code + tx = get_transaction(capsys) + + assert tx["sender"] == user_address + assert tx["receiver"] == contract_address + assert tx["value"] == "0" + assert tx["gasLimit"] == 1_000_000 + assert tx["chainID"] == "D" + data = tx["data"] + data = base64.b64decode(data).decode() + assert data == "signAndPerform@07" + + +def test_sign_batch_and_perform(capsys: Any): + return_code = main( + [ + "multisig", + "sign-batch-and-perform", + "--contract", + str(contract_address), + "--abi", + str(multisig_abi), + "--batch", + "7", + "--pem", + str(user), + "--nonce", + "0", + "--gas-limit", + "1000000", + "--chain", + "D", + ] + ) + assert not return_code + tx = get_transaction(capsys) + + assert tx["sender"] == user_address + assert tx["receiver"] == contract_address + assert tx["value"] == "0" + assert tx["gasLimit"] == 1_000_000 + assert tx["chainID"] == "D" + data = tx["data"] + data = base64.b64decode(data).decode() + assert data == "signBatchAndPerform@07" + + +def test_unsign_action(capsys: Any): + return_code = main( + [ + "multisig", + "unsign-action", + "--contract", + str(contract_address), + "--abi", + str(multisig_abi), + "--action", + "7", + "--pem", + str(user), + "--nonce", + "0", + "--gas-limit", + "1000000", + "--chain", + "D", + ] + ) + assert not return_code + tx = get_transaction(capsys) + + assert tx["sender"] == user_address + assert tx["receiver"] == contract_address + assert tx["value"] == "0" + assert tx["gasLimit"] == 1_000_000 + assert tx["chainID"] == "D" + data = tx["data"] + data = base64.b64decode(data).decode() + assert data == "unsign@07" + + +def test_unsign_batch(capsys: Any): + return_code = main( + [ + "multisig", + "unsign-batch", + "--contract", + str(contract_address), + "--abi", + str(multisig_abi), + "--batch", + "7", + "--pem", + str(user), + "--nonce", + "0", + "--gas-limit", + "1000000", + "--chain", + "D", + ] + ) + assert not return_code + tx = get_transaction(capsys) + + assert tx["sender"] == user_address + assert tx["receiver"] == contract_address + assert tx["value"] == "0" + assert tx["gasLimit"] == 1_000_000 + assert tx["chainID"] == "D" + data = tx["data"] + data = base64.b64decode(data).decode() + assert data == "unsignBatch@07" + + +def test_unsign_for_outdated_board_members(capsys: Any): + return_code = main( + [ + "multisig", + "unsign-for-outdated-members", + "--contract", + str(contract_address), + "--abi", + str(multisig_abi), + "--action", + "7", + "--outdated-members", + "1", + "2", + "--pem", + str(user), + "--nonce", + "0", + "--gas-limit", + "1000000", + "--chain", + "D", + ] + ) + assert not return_code + tx = get_transaction(capsys) + + assert tx["sender"] == user_address + assert tx["receiver"] == contract_address + assert tx["value"] == "0" + assert tx["gasLimit"] == 1_000_000 + assert tx["chainID"] == "D" + data = tx["data"] + data = base64.b64decode(data).decode() + assert data == "unsignForOutdatedBoardMembers@07@01@02" + + +def test_perform_action(capsys: Any): + return_code = main( + [ + "multisig", + "perform-action", + "--contract", + str(contract_address), + "--abi", + str(multisig_abi), + "--action", + "7", + "--pem", + str(user), + "--nonce", + "0", + "--gas-limit", + "1000000", + "--chain", + "D", + ] + ) + assert not return_code + tx = get_transaction(capsys) + + assert tx["sender"] == user_address + assert tx["receiver"] == contract_address + assert tx["value"] == "0" + assert tx["gasLimit"] == 1_000_000 + assert tx["chainID"] == "D" + data = tx["data"] + data = base64.b64decode(data).decode() + assert data == "performAction@07" + + +def test_perform_batch(capsys: Any): + return_code = main( + [ + "multisig", + "perform-batch", + "--contract", + str(contract_address), + "--abi", + str(multisig_abi), + "--batch", + "7", + "--pem", + str(user), + "--nonce", + "0", + "--gas-limit", + "1000000", + "--chain", + "D", + ] + ) + assert not return_code + tx = get_transaction(capsys) + + assert tx["sender"] == user_address + assert tx["receiver"] == contract_address + assert tx["value"] == "0" + assert tx["gasLimit"] == 1_000_000 + assert tx["chainID"] == "D" + data = tx["data"] + data = base64.b64decode(data).decode() + assert data == "performBatch@07" + + +def _read_stdout(capsys: Any) -> str: + stdout: str = capsys.readouterr().out.strip() + return stdout + + +def get_transaction(capsys: Any) -> dict[str, Any]: + out = _read_stdout(capsys) + output: dict[str, Any] = json.loads(out) + return output["emittedTransaction"] diff --git a/multiversx_sdk_cli/tests/test_contracts.py b/multiversx_sdk_cli/tests/test_contracts.py index d81cad3f..0ad046d6 100644 --- a/multiversx_sdk_cli/tests/test_contracts.py +++ b/multiversx_sdk_cli/tests/test_contracts.py @@ -40,7 +40,7 @@ def test_prepare_args_for_factories(): "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", ] - arguments = sc._prepare_args_for_factory(args) + arguments = sc._convert_args_to_typed_values(args) assert arguments[0].get_payload() == b"\x05" assert arguments[1].get_payload() == 123 assert arguments[2].get_payload() is False diff --git a/multiversx_sdk_cli/tests/test_shared.py b/multiversx_sdk_cli/tests/test_shared.py new file mode 100644 index 00000000..53c8b21d --- /dev/null +++ b/multiversx_sdk_cli/tests/test_shared.py @@ -0,0 +1,33 @@ +from multiversx_sdk_cli.cli_shared import prepare_token_transfers + + +def test_prepare_token_tranfers(): + # list of token transfers as interpreted by the CLI + token_transfers = [ + "FNG-123456", + "10000", + "SFT-123123-0a", + "3", + "NFT-987654-07", + "1", + "META-777777-10", + "123456789", + ] + transfers = prepare_token_transfers(token_transfers) + + assert len(transfers) == 4 + assert transfers[0].token.identifier == "FNG-123456" + assert transfers[0].token.nonce == 0 + assert transfers[0].amount == 10000 + + assert transfers[1].token.identifier == "SFT-123123" + assert transfers[1].token.nonce == 10 + assert transfers[1].amount == 3 + + assert transfers[2].token.identifier == "NFT-987654" + assert transfers[2].token.nonce == 7 + assert transfers[2].amount == 1 + + assert transfers[3].token.identifier == "META-777777" + assert transfers[3].token.nonce == 16 + assert transfers[3].amount == 123456789 diff --git a/requirements.txt b/requirements.txt index 7f295a29..4694973a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,4 +6,5 @@ ledgercomm[hid] rich==13.3.4 argcomplete==3.2.2 -multiversx-sdk[ledger]==1.2.0 +# multiversx-sdk[ledger]==1.2.0 +git+https://github.com/multiversx/mx-sdk-py.git@feat/next#egg=multiversx-sdk