From 2211ed91740bd12bdeff7ac5e9f9bb5c70c686bd Mon Sep 17 00:00:00 2001 From: Alexandru Popenta Date: Wed, 10 Jan 2024 17:32:09 +0200 Subject: [PATCH 1/9] add sign and perform action for multisig operations --- multiversx_sdk_cli/cli.py | 2 + multiversx_sdk_cli/cli_multisig.py | 128 +++++++++++++++++++++++++++++ multiversx_sdk_cli/cli_shared.py | 12 +++ multiversx_sdk_cli/transactions.py | 2 +- 4 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 multiversx_sdk_cli/cli_multisig.py diff --git a/multiversx_sdk_cli/cli.py b/multiversx_sdk_cli/cli.py index fe2e996b..b044c957 100644 --- a/multiversx_sdk_cli/cli.py +++ b/multiversx_sdk_cli/cli.py @@ -15,6 +15,7 @@ import multiversx_sdk_cli.cli_dns import multiversx_sdk_cli.cli_ledger import multiversx_sdk_cli.cli_localnet +import multiversx_sdk_cli.cli_multisig import multiversx_sdk_cli.cli_transactions import multiversx_sdk_cli.cli_validators import multiversx_sdk_cli.cli_wallet @@ -94,6 +95,7 @@ def setup_parser(args: List[str]): commands.append(multiversx_sdk_cli.cli_data.setup_parser(subparsers)) commands.append(multiversx_sdk_cli.cli_delegation.setup_parser(args, subparsers)) commands.append(multiversx_sdk_cli.cli_dns.setup_parser(args, subparsers)) + commands.append(multiversx_sdk_cli.cli_multisig.setup_parser(args, subparsers)) parser.epilog = """ ---------------------- diff --git a/multiversx_sdk_cli/cli_multisig.py b/multiversx_sdk_cli/cli_multisig.py new file mode 100644 index 00000000..cc045c6a --- /dev/null +++ b/multiversx_sdk_cli/cli_multisig.py @@ -0,0 +1,128 @@ +import logging +from typing import Any, List + +from multiversx_sdk_core.transaction_factories.transactions_factory_config import \ + TransactionsFactoryConfig + +from multiversx_sdk_cli import cli_shared, utils +from multiversx_sdk_cli.contracts import SmartContract +from multiversx_sdk_cli.errors import BadUsage +from multiversx_sdk_cli.transactions import compute_relayed_v1_data + +logger = logging.getLogger("cli.multisig") + + +def setup_parser(args: List[str], subparsers: Any) -> Any: + parser = cli_shared.add_group_subparser(subparsers, "multisig", "Interact with a multisig smart contract") + subparsers = parser.add_subparsers() + + sub = cli_shared.add_command_subparser(subparsers, "multisig", "sign", f"Sign a proposed action.") + cli_shared.add_multisig_address_arg(sub) + cli_shared.add_multisig_view_address_arg(sub) + cli_shared.add_sign_multisig_action_arg(sub) + cli_shared.add_wallet_args(args, sub) + cli_shared.add_tx_args(args, sub, with_receiver=False, with_data=False, with_guardian=True) + + cli_shared.add_outfile_arg(sub, what="signed transaction, hash") + cli_shared.add_broadcast_args(sub, relay=True) + cli_shared.add_proxy_arg(sub) + cli_shared.add_guardian_wallet_args(args, sub) + sub.add_argument("--wait-result", action="store_true", default=False, + help="signal to wait for the transaction result - only valid if --send is set") + sub.add_argument("--timeout", default=100, help="max num of seconds to wait for result" + " - only valid if --wait-result is set") + sub.set_defaults(func=sign_action) + + sub = cli_shared.add_command_subparser(subparsers, "multisig", "perform-action", f"Perform an action that has reached quorum.") + cli_shared.add_multisig_address_arg(sub) + cli_shared.add_multisig_view_address_arg(sub) + cli_shared.add_sign_multisig_action_arg(sub) + cli_shared.add_wallet_args(args, sub) + cli_shared.add_tx_args(args, sub, with_receiver=False, with_data=False, with_guardian=True) + + cli_shared.add_outfile_arg(sub, what="signed transaction, hash") + cli_shared.add_broadcast_args(sub, relay=True) + cli_shared.add_proxy_arg(sub) + cli_shared.add_guardian_wallet_args(args, sub) + sub.add_argument("--wait-result", action="store_true", default=False, + help="signal to wait for the transaction result - only valid if --send is set") + sub.add_argument("--timeout", default=100, help="max num of seconds to wait for result" + " - only valid if --wait-result is set") + sub.set_defaults(func=perform_action) + + parser.epilog = cli_shared.build_group_epilog(subparsers) + return subparsers + + +def sign_action(args: Any): + args = utils.as_object(args) + + cli_shared.check_guardian_and_options_args(args) + cli_shared.check_broadcast_args(args) + cli_shared.prepare_chain_id_in_args(args) + cli_shared.prepare_nonce_in_args(args) + + sender = cli_shared.prepare_account(args) + + config = TransactionsFactoryConfig(args.chain) + contract = SmartContract(config) + + action_id = args.action_id + if action_id == "all": + raise BadUsage("`all` is not supported at the moment. Please use a specific action id") + + mapped_args = _map_args_to_contract_call_args(args, "sign") + tx = contract.prepare_execute_transaction(sender, mapped_args) + + if hasattr(args, "relay") and args.relay: + args.outfile.write(compute_relayed_v1_data(tx)) + return + + cli_shared.send_or_simulate(tx, args) + + +def perform_action(args: Any): + args = utils.as_object(args) + + cli_shared.check_guardian_and_options_args(args) + cli_shared.check_broadcast_args(args) + cli_shared.prepare_chain_id_in_args(args) + cli_shared.prepare_nonce_in_args(args) + + sender = cli_shared.prepare_account(args) + + config = TransactionsFactoryConfig(args.chain) + contract = SmartContract(config) + + action_id = args.action_id + if action_id == "all": + raise BadUsage("`all` is not supported at the moment. Please use a specific action id") + + mapped_args = _map_args_to_contract_call_args(args, "performAction") + tx = contract.prepare_execute_transaction(sender, mapped_args) + + if hasattr(args, "relay") and args.relay: + args.outfile.write(compute_relayed_v1_data(tx)) + return + + cli_shared.send_or_simulate(tx, args) + + +class PlainObject: + pass + + +def _map_args_to_contract_call_args(args: Any, function: str) -> Any: + obj = PlainObject() + obj.contract = args.multisig + obj.function = function + obj.arguments = args.action_id + obj.value = args.value + obj.token_transfers = None + obj.gas_limit = args.gas_limit + obj.nonce = args.nonce + obj.version = args.version + obj.options = args.options + obj.guardian = args.guardian + + return obj diff --git a/multiversx_sdk_cli/cli_shared.py b/multiversx_sdk_cli/cli_shared.py index 02005c06..2bf1b667 100644 --- a/multiversx_sdk_cli/cli_shared.py +++ b/multiversx_sdk_cli/cli_shared.py @@ -137,6 +137,18 @@ def add_omit_fields_arg(sub: Any): sub.add_argument("--omit-fields", default="[]", type=str, required=False, help="omit fields in the output payload (default: %(default)s)") +def add_multisig_address_arg(sub: Any): + sub.add_argument("--multisig", help="the address of the multisig contract") + + +def add_multisig_view_address_arg(sub: Any): + sub.add_argument("--multisig-view", help="the address of the multisig-view contract") + + +def add_sign_multisig_action_arg(sub: Any): + sub.add_argument("--action-id", help="an integer representing the ID of the action; cand also be `all`") + + def parse_omit_fields_arg(args: Any) -> List[str]: literal = args.omit_fields parsed = ast.literal_eval(literal) diff --git a/multiversx_sdk_cli/transactions.py b/multiversx_sdk_cli/transactions.py index 418a469d..d859a03c 100644 --- a/multiversx_sdk_cli/transactions.py +++ b/multiversx_sdk_cli/transactions.py @@ -30,7 +30,7 @@ class INetworkProvider(Protocol): def send_transaction(self, transaction: ITransaction) -> str: ... - def send_transactions(self, transactions: Sequence[ITransaction]) -> Tuple[int, str]: + def send_transactions(self, transactions: Sequence[ITransaction]) -> Tuple[int, Dict[str, str]]: ... def get_transaction(self, tx_hash: str, with_process_status: Optional[bool] = False) -> ITransactionOnNetwork: From 11ec107f6741ddf94a08ec4e105a5f35148e03e0 Mon Sep 17 00:00:00 2001 From: Alexandru Popenta Date: Thu, 11 Jan 2024 12:35:53 +0200 Subject: [PATCH 2/9] add unit tests for multisig --- multiversx_sdk_cli/tests/test_cli_multisig.py | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 multiversx_sdk_cli/tests/test_cli_multisig.py 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..696b5ecd --- /dev/null +++ b/multiversx_sdk_cli/tests/test_cli_multisig.py @@ -0,0 +1,69 @@ +import base64 +import json +from pathlib import Path +from typing import Any, Dict + +from multiversx_sdk_cli.cli import main + +parent = Path(__file__).parent +alice = parent / "testdata" / "alice.pem" + + +def test_sign_action(capsys: Any): + return_code = main([ + "multisig", "sign", + "--multisig", "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30", + "--action-id", "2", + "--pem", str(alice), + "--nonce", "1289", + "--gas-limit", "10000000", + "--proxy", "https://testnet-api.multiversx.com" + ]) + assert False if return_code else True + + transaction = get_transaction(capsys) + + data_field: str = transaction["data"] + data = base64.b64decode(data_field.encode()).decode() + assert data == "sign@02" + + receiver = transaction["receiver"] + assert receiver == "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + + chain_id = transaction["chainID"] + assert chain_id == "T" + + +def test_perform_action(capsys: Any): + return_code = main([ + "multisig", "perform-action", + "--multisig", "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30", + "--action-id", "2", + "--pem", str(alice), + "--nonce", "1290", + "--gas-limit", "10000000", + "--proxy", "https://testnet-api.multiversx.com" + ]) + assert False if return_code else True + + transaction = get_transaction(capsys) + + data_field: str = transaction["data"] + data = base64.b64decode(data_field.encode()).decode() + assert data == "performAction@02" + + receiver = transaction["receiver"] + assert receiver == "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + + chain_id = transaction["chainID"] + assert chain_id == "T" + + +def get_transaction(capsys: Any) -> Dict[str, Any]: + out = _read_stdout(capsys) + output = json.loads(out) + return output["emittedTransaction"] + + +def _read_stdout(capsys: Any) -> str: + return capsys.readouterr().out.strip() From e78b8a6b997af8932b2a618568e6052c41d5b658 Mon Sep 17 00:00:00 2001 From: Alexandru Popenta Date: Thu, 11 Jan 2024 12:57:36 +0200 Subject: [PATCH 3/9] refactoring --- multiversx_sdk_cli/cli_multisig.py | 62 ++++++++++++++++-------------- multiversx_sdk_cli/contracts.py | 2 +- 2 files changed, 35 insertions(+), 29 deletions(-) diff --git a/multiversx_sdk_cli/cli_multisig.py b/multiversx_sdk_cli/cli_multisig.py index cc045c6a..01397e9e 100644 --- a/multiversx_sdk_cli/cli_multisig.py +++ b/multiversx_sdk_cli/cli_multisig.py @@ -1,6 +1,7 @@ import logging from typing import Any, List +from multiversx_sdk_core import Address from multiversx_sdk_core.transaction_factories.transactions_factory_config import \ TransactionsFactoryConfig @@ -11,6 +12,9 @@ logger = logging.getLogger("cli.multisig") +MULTISIG_SIGN_ACTION_FUNCTION = "sign" +MULTISIG_PERFORM_ACTION_FUNCTION = "performAction" + def setup_parser(args: List[str], subparsers: Any) -> Any: parser = cli_shared.add_group_subparser(subparsers, "multisig", "Interact with a multisig smart contract") @@ -62,17 +66,28 @@ def sign_action(args: Any): cli_shared.prepare_chain_id_in_args(args) cli_shared.prepare_nonce_in_args(args) - sender = cli_shared.prepare_account(args) - config = TransactionsFactoryConfig(args.chain) contract = SmartContract(config) + sender = cli_shared.prepare_account(args) + contract_address = Address.new_from_bech32(args.multisig) + action_id = args.action_id if action_id == "all": raise BadUsage("`all` is not supported at the moment. Please use a specific action id") - mapped_args = _map_args_to_contract_call_args(args, "sign") - tx = contract.prepare_execute_transaction(sender, mapped_args) + tx = contract.prepare_execute_transaction( + caller=sender, + contract=contract_address, + function=MULTISIG_SIGN_ACTION_FUNCTION, + arguments=args.action_id, + gas_limit=int(args.gas_limit), + value=int(args.value), + transfers=None, + nonce=int(args.nonce), + version=int(args.version), + options=int(args.options), + guardian=args.guardian) if hasattr(args, "relay") and args.relay: args.outfile.write(compute_relayed_v1_data(tx)) @@ -89,40 +104,31 @@ def perform_action(args: Any): cli_shared.prepare_chain_id_in_args(args) cli_shared.prepare_nonce_in_args(args) - sender = cli_shared.prepare_account(args) - config = TransactionsFactoryConfig(args.chain) contract = SmartContract(config) + sender = cli_shared.prepare_account(args) + contract_address = Address.new_from_bech32(args.multisig) + action_id = args.action_id if action_id == "all": raise BadUsage("`all` is not supported at the moment. Please use a specific action id") - mapped_args = _map_args_to_contract_call_args(args, "performAction") - tx = contract.prepare_execute_transaction(sender, mapped_args) + tx = contract.prepare_execute_transaction( + caller=sender, + contract=contract_address, + function=MULTISIG_PERFORM_ACTION_FUNCTION, + arguments=args.action_id, + gas_limit=int(args.gas_limit), + value=int(args.value), + transfers=None, + nonce=int(args.nonce), + version=int(args.version), + options=int(args.options), + guardian=args.guardian) if hasattr(args, "relay") and args.relay: args.outfile.write(compute_relayed_v1_data(tx)) return cli_shared.send_or_simulate(tx, args) - - -class PlainObject: - pass - - -def _map_args_to_contract_call_args(args: Any, function: str) -> Any: - obj = PlainObject() - obj.contract = args.multisig - obj.function = function - obj.arguments = args.action_id - obj.value = args.value - obj.token_transfers = None - obj.gas_limit = args.gas_limit - obj.nonce = args.nonce - obj.version = args.version - obj.options = args.options - obj.guardian = args.guardian - - return obj diff --git a/multiversx_sdk_cli/contracts.py b/multiversx_sdk_cli/contracts.py index 251ffbae..1fb08205 100644 --- a/multiversx_sdk_cli/contracts.py +++ b/multiversx_sdk_cli/contracts.py @@ -113,7 +113,7 @@ def prepare_deploy_transaction(self, def prepare_execute_transaction(self, caller: Account, - contract: Address, + contract: IAddress, function: str, arguments: Union[List[str], None], gas_limit: int, From 5e2ca6329b4cfd0658372c5a243dee2607c1e2da Mon Sep 17 00:00:00 2001 From: Alexandru Popenta Date: Fri, 12 Jan 2024 15:18:00 +0200 Subject: [PATCH 4/9] add token transfers & unit tests for multisig --- multiversx_sdk_cli/cli_contracts.py | 8 +- multiversx_sdk_cli/cli_shared.py | 6 + multiversx_sdk_cli/cli_transactions.py | 54 ++++++++- multiversx_sdk_cli/contracts.py | 7 +- multiversx_sdk_cli/multisig.py | 103 ++++++++++++++++++ multiversx_sdk_cli/tests/test_cli_multisig.py | 96 ++++++++++++++++ 6 files changed, 259 insertions(+), 15 deletions(-) create mode 100644 multiversx_sdk_cli/multisig.py diff --git a/multiversx_sdk_cli/cli_contracts.py b/multiversx_sdk_cli/cli_contracts.py index 53cdf3db..43736790 100644 --- a/multiversx_sdk_cli/cli_contracts.py +++ b/multiversx_sdk_cli/cli_contracts.py @@ -97,7 +97,7 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: cli_shared.add_tx_args(args, sub, with_receiver=False, with_data=False, with_guardian=True) _add_function_arg(sub) _add_arguments_arg(sub) - _add_token_transfers_args(sub) + cli_shared.add_token_transfers_arg(sub) sub.add_argument("--wait-result", action="store_true", default=False, help="signal to wait for the transaction result - only valid if --send is set") sub.add_argument("--timeout", default=100, help="max num of seconds to wait for result" @@ -236,12 +236,6 @@ def _add_arguments_arg(sub: Any): "boolean] or hex-encoded. E.g. --arguments 42 0x64 1000 0xabba str:TOK-a1c2ef true erd1[..]") -def _add_token_transfers_args(sub: Any): - sub.add_argument("--token-transfers", nargs='+', - help="token transfers for transfer & execute, as [token, amount] " - "E.g. --token-transfers NFT-123456-0a 1 ESDT-987654 100000000") - - def _add_metadata_arg(sub: Any): sub.add_argument("--metadata-not-upgradeable", dest="metadata_upgradeable", action="store_false", help="‼ mark the contract as NOT upgradeable (default: upgradeable)") diff --git a/multiversx_sdk_cli/cli_shared.py b/multiversx_sdk_cli/cli_shared.py index 2bf1b667..ef3158e2 100644 --- a/multiversx_sdk_cli/cli_shared.py +++ b/multiversx_sdk_cli/cli_shared.py @@ -137,6 +137,12 @@ def add_omit_fields_arg(sub: Any): sub.add_argument("--omit-fields", default="[]", type=str, required=False, help="omit fields in the output payload (default: %(default)s)") +def add_token_transfers_arg(sub: Any): + sub.add_argument("--token-transfers", nargs='+', + help="token transfers for transfer & execute, as [token, amount] " + "E.g. --token-transfers NFT-123456-0a 1 ESDT-987654 100000000") + + def add_multisig_address_arg(sub: Any): sub.add_argument("--multisig", help="the address of the multisig contract") diff --git a/multiversx_sdk_cli/cli_transactions.py b/multiversx_sdk_cli/cli_transactions.py index 6850ac8e..63d4c37a 100644 --- a/multiversx_sdk_cli/cli_transactions.py +++ b/multiversx_sdk_cli/cli_transactions.py @@ -7,10 +7,14 @@ from multiversx_sdk_cli import cli_shared, utils from multiversx_sdk_cli.cli_output import CLIOutputBuilder from multiversx_sdk_cli.cosign_transaction import cosign_transaction -from multiversx_sdk_cli.errors import NoWalletProvided +from multiversx_sdk_cli.errors import BadUsage, NoWalletProvided +from multiversx_sdk_cli.multisig import ( + prepare_transaction_for_custom_token_transfer, + prepare_transaction_for_egld_transfer) from multiversx_sdk_cli.transactions import (compute_relayed_v1_data, do_prepare_transaction, - load_transaction_from_file) + load_transaction_from_file, + sign_tx_by_guardian) def setup_parser(args: List[str], subparsers: Any) -> Any: @@ -23,6 +27,8 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: cli_shared.add_broadcast_args(sub, relay=True) cli_shared.add_proxy_arg(sub) cli_shared.add_guardian_wallet_args(args, sub) + cli_shared.add_multisig_address_arg(sub) + cli_shared.add_token_transfers_arg(sub) sub.add_argument("--wait-result", action="store_true", default=False, help="signal to wait for the transaction result - only valid if --send is set") sub.add_argument("--timeout", default=100, help="max num of seconds to wait for result" @@ -71,10 +77,46 @@ def create_transaction(args: Any): cli_shared.prepare_chain_id_in_args(args) cli_shared.prepare_nonce_in_args(args) - if args.data_file: - args.data = Path(args.data_file).read_text() - - tx = do_prepare_transaction(args) + if args.multisig: + if args.data: + raise BadUsage("`--data` should not be provided when interacting with a multisig") + + sender = cli_shared.prepare_account(args) + + if int(args.value): + tx = prepare_transaction_for_egld_transfer( + sender=sender, + multisig=args.multisig, + receiver=args.receiver, + chain_id=args.chain, + value=int(args.value), + gas_limit=int(args.gas_limit), + nonce=int(args.nonce), + version=int(args.version), + options=int(args.options), + guardian=args.guardian + ) + else: + tx = prepare_transaction_for_custom_token_transfer( + sender=sender, + multisig=args.multisig, + receiver=args.receiver, + chain_id=args.chain, + transfers=args.token_transfers, + gas_limit=int(args.gas_limit), + nonce=int(args.nonce), + version=int(args.version), + options=int(args.options), + guardian=args.guardian) + + if tx.guardian: + tx = sign_tx_by_guardian(args, tx) + + else: + if args.data_file: + args.data = Path(args.data_file).read_text() + + tx = do_prepare_transaction(args) if hasattr(args, "relay") and args.relay: args.outfile.write(compute_relayed_v1_data(tx)) diff --git a/multiversx_sdk_cli/contracts.py b/multiversx_sdk_cli/contracts.py index 1fb08205..5db8d2c0 100644 --- a/multiversx_sdk_cli/contracts.py +++ b/multiversx_sdk_cli/contracts.py @@ -123,7 +123,10 @@ def prepare_execute_transaction(self, version: int, options: int, guardian: str) -> Transaction: - token_transfers = self._prepare_token_transfers(transfers) if transfers else [] + if value and transfers: + raise errors.BadUsage("Can't send both native and custom tokens") + + token_transfers = self.prepare_token_transfers(transfers) if transfers else [] args = prepare_args_for_factory(arguments) if arguments else [] tx = self._factory.create_transaction_for_execute( @@ -180,7 +183,7 @@ def prepare_upgrade_transaction(self, return tx - def _prepare_token_transfers(self, transfers: List[str]) -> List[TokenTransfer]: + def prepare_token_transfers(self, transfers: List[str]) -> List[TokenTransfer]: token_computer = TokenComputer() token_transfers: List[TokenTransfer] = [] diff --git a/multiversx_sdk_cli/multisig.py b/multiversx_sdk_cli/multisig.py new file mode 100644 index 00000000..cc69712c --- /dev/null +++ b/multiversx_sdk_cli/multisig.py @@ -0,0 +1,103 @@ +from typing import List + +from multiversx_sdk_core import (Address, TokenComputer, TokenTransfer, + Transaction) +from multiversx_sdk_core.transaction_factories.token_transfers_data_builder import \ + TokenTransfersDataBuilder +from multiversx_sdk_core.transaction_factories.transactions_factory_config import \ + TransactionsFactoryConfig + +from multiversx_sdk_cli.accounts import Account +from multiversx_sdk_cli.contracts import SmartContract + + +def prepare_transaction_for_egld_transfer(sender: Account, + multisig: str, + receiver: str, + chain_id: str, + value: int, + gas_limit: int, + nonce: int, + version: int, + options: int, + guardian: str) -> Transaction: + config = TransactionsFactoryConfig(chain_id) + contract = SmartContract(config) + + return contract.prepare_execute_transaction( + caller=sender, + contract=Address.from_bech32(multisig), + function="proposeTransferExecute", + arguments=[f"{receiver}", f"{value}"], + gas_limit=gas_limit, + value=0, + transfers=None, + nonce=nonce, + version=version, + options=options, + guardian=guardian) + + +def prepare_transaction_for_custom_token_transfer(sender: Account, + multisig: str, + receiver: str, + chain_id: str, + transfers: List[str], + gas_limit: int, + nonce: int, + version: int, + options: int, + guardian: str) -> Transaction: + config = TransactionsFactoryConfig(chain_id) + contract = SmartContract(config) + + token_transfers = contract.prepare_token_transfers(transfers) + transfer_receiver = Address.from_bech32(receiver) + transfer_data_parts = _prepare_data_parts_for_multisig_transfer(transfer_receiver, token_transfers) + multisig_contract = Address.from_bech32(multisig) + + arguments: List[str] = [transfer_receiver.to_hex(), "00"] + if transfer_data_parts[0] != "ESDTTransfer": + arguments[0] = multisig_contract.to_hex() + + transfer_data_parts[0] = transfer_data_parts[0].encode().hex() + arguments.extend(transfer_data_parts) + + tx = contract.prepare_execute_transaction( + caller=sender, + contract=multisig_contract, + function="proposeAsyncCall", + arguments=None, + gas_limit=gas_limit, + value=0, + transfers=None, + nonce=nonce, + version=version, + options=options, + guardian=guardian) + + data_field = tx.data.decode() + "@" + _build_data_payload(arguments) + tx.data = data_field.encode() + tx.signature = bytes.fromhex(sender.sign_transaction(tx)) + return tx + + +def _prepare_data_parts_for_multisig_transfer(receiver: Address, token_transfers: List[TokenTransfer]): + token_computer = TokenComputer() + data_builder = TokenTransfersDataBuilder(token_computer) + data_parts: List[str] = [] + + if len(token_transfers) == 1: + transfer = token_transfers[0] + if token_computer.is_fungible(transfer.token): + data_parts = data_builder.build_args_for_esdt_transfer(transfer=transfer) + else: + data_parts = data_builder.build_args_for_single_esdt_nft_transfer(transfer=transfer, receiver=receiver) + elif len(token_transfers) > 1: + data_parts = data_builder.build_args_for_multi_esdt_nft_transfer(receiver=receiver, transfers=token_transfers) + + return data_parts + + +def _build_data_payload(parts: List[str]) -> str: + return "@".join(parts) diff --git a/multiversx_sdk_cli/tests/test_cli_multisig.py b/multiversx_sdk_cli/tests/test_cli_multisig.py index 696b5ecd..0e1c2bf4 100644 --- a/multiversx_sdk_cli/tests/test_cli_multisig.py +++ b/multiversx_sdk_cli/tests/test_cli_multisig.py @@ -59,6 +59,102 @@ def test_perform_action(capsys: Any): assert chain_id == "T" +def test_propose_egld_transfer(capsys: Any): + return_code = main([ + "tx", "new", + "--pem", str(alice), + "--receiver", "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx", + "--nonce", "1429", + "--chain", "T", + "--gas-limit", "10000000", + "--multisig", "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30", + "--value", "1000000000000000" + ]) + assert False if return_code else True + + transaction = get_transaction(capsys) + + data_field: str = transaction["data"] + data = base64.b64decode(data_field.encode()).decode() + assert data == "proposeTransferExecute@8049d639e5a6980d1cd2392abcce41029cda74a1563523a202f09641cc2618f8@038d7ea4c68000" + + receiver = transaction["receiver"] + assert receiver == "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + + chain_id = transaction["chainID"] + assert chain_id == "T" + + value = int(transaction["value"]) + assert value == 0 + + signature = transaction["signature"] + assert signature == "285edffe65006f738ce6fff640ddd9cb69c7380a219ec3549cb35744cd1106ffd41005b8d471899eb1db55e556b042db7bab0830a5250860348fb101d644c805" + + +def test_propose_esdt_transfer(capsys: Any): + return_code = main([ + "tx", "new", + "--pem", str(alice), + "--receiver", "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx", + "--nonce", "1427", + "--chain", "T", + "--gas-limit", "10000000", + "--multisig", "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30", + "--token-transfers", "TST-267761", "10" + ]) + assert False if return_code else True + + transaction = get_transaction(capsys) + + data_field: str = transaction["data"] + data = base64.b64decode(data_field.encode()).decode() + assert data == "proposeAsyncCall@8049d639e5a6980d1cd2392abcce41029cda74a1563523a202f09641cc2618f8@00@455344545472616e73666572@5453542d323637373631@0a" + + receiver = transaction["receiver"] + assert receiver == "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + + chain_id = transaction["chainID"] + assert chain_id == "T" + + value = int(transaction["value"]) + assert value == 0 + + signature = transaction["signature"] + assert signature == "dc80e70409fce3a20bd5c80ef1f1039a0474729ddb27188afacbd2d8237294172d5bf8b4bc759e48a2e5c724983dbb6ba5f17b48bb7a8d2dfc4c076a113fa50f" + + +def test_propose_multi_esdt_nft_transfer(capsys: Any): + return_code = main([ + "tx", "new", + "--pem", str(alice), + "--receiver", "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx", + "--nonce", "1434", + "--chain", "T", + "--gas-limit", "10000000", + "--multisig", "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30", + "--token-transfers", "TST-267761", "10", "ZZZ-9ee87d", "10000" + ]) + assert False if return_code else True + + transaction = get_transaction(capsys) + + data_field: str = transaction["data"] + data = base64.b64decode(data_field.encode()).decode() + assert data == "proposeAsyncCall@000000000000000005000a2a0f13340978c2eea268a5a2dcf917012978f61f5c@00@4d756c7469455344544e46545472616e73666572@8049d639e5a6980d1cd2392abcce41029cda74a1563523a202f09641cc2618f8@02@5453542d323637373631@@0a@5a5a5a2d396565383764@@2710" + + receiver = transaction["receiver"] + assert receiver == "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + + chain_id = transaction["chainID"] + assert chain_id == "T" + + value = int(transaction["value"]) + assert value == 0 + + signature = transaction["signature"] + assert signature == "563fc6eefe9469cf90191462cfe21ab25ae7291c1c411cd4b3778717c827045eabc58b689308e8ee45676a8e49cf75c2856a83e41e93dad5c4acb5ccb65c5b04" + + def get_transaction(capsys: Any) -> Dict[str, Any]: out = _read_stdout(capsys) output = json.loads(out) From 4d2802772d8026260d56c9d3b93ba0c696c0d0d9 Mon Sep 17 00:00:00 2001 From: Alexandru Popenta Date: Fri, 12 Jan 2024 18:40:09 +0200 Subject: [PATCH 5/9] add multisig deposit & tests --- multiversx_sdk_cli/cli_multisig.py | 53 ++++++++++- multiversx_sdk_cli/multisig.py | 30 +++++- multiversx_sdk_cli/tests/test_cli_multisig.py | 93 +++++++++++++++++++ 3 files changed, 174 insertions(+), 2 deletions(-) diff --git a/multiversx_sdk_cli/cli_multisig.py b/multiversx_sdk_cli/cli_multisig.py index 01397e9e..f3979402 100644 --- a/multiversx_sdk_cli/cli_multisig.py +++ b/multiversx_sdk_cli/cli_multisig.py @@ -8,7 +8,10 @@ from multiversx_sdk_cli import cli_shared, utils from multiversx_sdk_cli.contracts import SmartContract from multiversx_sdk_cli.errors import BadUsage -from multiversx_sdk_cli.transactions import compute_relayed_v1_data +from multiversx_sdk_cli.multisig import \ + prepare_transaction_for_depositing_funds +from multiversx_sdk_cli.transactions import (compute_relayed_v1_data, + sign_tx_by_guardian) logger = logging.getLogger("cli.multisig") @@ -54,6 +57,23 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: " - only valid if --wait-result is set") sub.set_defaults(func=perform_action) + sub = cli_shared.add_command_subparser(subparsers, "multisig", "deposit", f"Deposit assets into the multisig contract.") + cli_shared.add_multisig_address_arg(sub) + cli_shared.add_multisig_view_address_arg(sub) + cli_shared.add_wallet_args(args, sub) + cli_shared.add_tx_args(args, sub, with_receiver=False, with_data=False, with_guardian=True) + cli_shared.add_token_transfers_arg(sub) + + cli_shared.add_outfile_arg(sub, what="signed transaction, hash") + cli_shared.add_broadcast_args(sub, relay=True) + cli_shared.add_proxy_arg(sub) + cli_shared.add_guardian_wallet_args(args, sub) + sub.add_argument("--wait-result", action="store_true", default=False, + help="signal to wait for the transaction result - only valid if --send is set") + sub.add_argument("--timeout", default=100, help="max num of seconds to wait for result" + " - only valid if --wait-result is set") + sub.set_defaults(func=deposit_funds) + parser.epilog = cli_shared.build_group_epilog(subparsers) return subparsers @@ -132,3 +152,34 @@ def perform_action(args: Any): return cli_shared.send_or_simulate(tx, args) + + +def deposit_funds(args: Any): + cli_shared.check_guardian_and_options_args(args) + cli_shared.check_broadcast_args(args) + cli_shared.prepare_chain_id_in_args(args) + cli_shared.prepare_nonce_in_args(args) + + sender = cli_shared.prepare_account(args) + + tx = prepare_transaction_for_depositing_funds( + sender=sender, + multisig=args.multisig, + chain_id=args.chain, + value=int(args.value), + transfers=args.token_transfers, + gas_limit=int(args.gas_limit), + nonce=int(args.nonce), + version=int(args.version), + options=int(args.options), + guardian=args.guardian + ) + + if tx.guardian: + tx = sign_tx_by_guardian(args, tx) + + if hasattr(args, "relay") and args.relay: + args.outfile.write(compute_relayed_v1_data(tx)) + return + + cli_shared.send_or_simulate(tx, args) diff --git a/multiversx_sdk_cli/multisig.py b/multiversx_sdk_cli/multisig.py index cc69712c..2d02ab34 100644 --- a/multiversx_sdk_cli/multisig.py +++ b/multiversx_sdk_cli/multisig.py @@ -1,4 +1,4 @@ -from typing import List +from typing import List, Union from multiversx_sdk_core import (Address, TokenComputer, TokenTransfer, Transaction) @@ -82,6 +82,34 @@ def prepare_transaction_for_custom_token_transfer(sender: Account, return tx +def prepare_transaction_for_depositing_funds(sender: Account, + multisig: str, + chain_id: str, + value: int, + transfers: Union[List[str], None], + gas_limit: int, + nonce: int, + version: int, + options: int, + guardian: str): + config = TransactionsFactoryConfig(chain_id) + contract = SmartContract(config) + + return contract.prepare_execute_transaction( + caller=sender, + contract=Address.from_bech32(multisig), + function="deposit", + arguments=None, + gas_limit=gas_limit, + value=value, + transfers=transfers, + nonce=nonce, + version=version, + options=options, + guardian=guardian + ) + + def _prepare_data_parts_for_multisig_transfer(receiver: Address, token_transfers: List[TokenTransfer]): token_computer = TokenComputer() data_builder = TokenTransfersDataBuilder(token_computer) diff --git a/multiversx_sdk_cli/tests/test_cli_multisig.py b/multiversx_sdk_cli/tests/test_cli_multisig.py index 0e1c2bf4..8d810b05 100644 --- a/multiversx_sdk_cli/tests/test_cli_multisig.py +++ b/multiversx_sdk_cli/tests/test_cli_multisig.py @@ -59,6 +59,99 @@ def test_perform_action(capsys: Any): assert chain_id == "T" +def test_deposit_egld(capsys: Any): + return_code = main([ + "multisig", "deposit", + "--multisig", "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30", + "--pem", str(alice), + "--nonce", "1449", + "--chain", "T", + "--gas-limit", "10000000", + "--value", "50000000000000000" + ]) + assert False if return_code else True + + transaction = get_transaction(capsys) + + data_field: str = transaction["data"] + data = base64.b64decode(data_field.encode()).decode() + assert data == "deposit" + + receiver = transaction["receiver"] + assert receiver == "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + + chain_id = transaction["chainID"] + assert chain_id == "T" + + value = int(transaction["value"]) + assert value == 50000000000000000 + + signature = transaction["signature"] + assert signature == "ba823e12c7eba1cb8e7c41f7f4042d54742a8162114875150e0f9d1e3535fdb74e7a89adfbda4360cd9f02de531facaf2a60a8f53ae48765ff2c7ae685f61704" + + +def test_deposit_esdt(capsys: Any): + return_code = main([ + "multisig", "deposit", + "--multisig", "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30", + "--pem", str(alice), + "--nonce", "1525", + "--chain", "T", + "--gas-limit", "10000000", + "--token-transfers", "TST-267761", "1000" + ]) + assert False if return_code else True + + transaction = get_transaction(capsys) + + data_field: str = transaction["data"] + data = base64.b64decode(data_field.encode()).decode() + assert data == "ESDTTransfer@5453542d323637373631@03e8@6465706f736974" + + receiver = transaction["receiver"] + assert receiver == "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + + chain_id = transaction["chainID"] + assert chain_id == "T" + + value = int(transaction["value"]) + assert value == 0 + + signature = transaction["signature"] + assert signature == "4bb4421783ba2b19060d6d7b84bcdda484f475a52eab6eb44679af2cac1dfae0c10552efd84ef7bdae028a40bc656f30e52984aa4f90581700377e44c4d4810b" + + +def test_deposit_multi_esdt(capsys: Any): + return_code = main([ + "multisig", "deposit", + "--multisig", "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30", + "--pem", str(alice), + "--nonce", "1531", + "--chain", "T", + "--gas-limit", "10000000", + "--token-transfers", "TST-267761", "1700", "ZZZ-9ee87d", "1200" + ]) + assert False if return_code else True + + transaction = get_transaction(capsys) + + data_field: str = transaction["data"] + data = base64.b64decode(data_field.encode()).decode() + assert data == "MultiESDTNFTTransfer@000000000000000005000a2a0f13340978c2eea268a5a2dcf917012978f61f5c@02@5453542d323637373631@@06a4@5a5a5a2d396565383764@@04b0@6465706f736974" + + receiver = transaction["receiver"] + assert receiver == "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th" + + chain_id = transaction["chainID"] + assert chain_id == "T" + + value = int(transaction["value"]) + assert value == 0 + + signature = transaction["signature"] + assert signature == "034e8644e4363640224c20556b9a3abb3aef36d59697754a44e9d0cbe26e31de8bd78d4529b5c5a3b74a7e51a14f0f62e10f01386318b005f384c69e885c960c" + + def test_propose_egld_transfer(capsys: Any): return_code = main([ "tx", "new", From f3a9d409be1d0763c815d20cedd5a93045c01043 Mon Sep 17 00:00:00 2001 From: Alexandru Popenta Date: Mon, 15 Jan 2024 17:42:34 +0200 Subject: [PATCH 6/9] add contract deploy using multisig --- multiversx_sdk_cli/cli_contracts.py | 60 +++++--- multiversx_sdk_cli/cli_multisig.py | 134 ++++++++++++++++-- multiversx_sdk_cli/cli_shared.py | 8 +- multiversx_sdk_cli/contracts.py | 2 +- multiversx_sdk_cli/dns.py | 4 +- multiversx_sdk_cli/multisig.py | 79 +++++++++-- multiversx_sdk_cli/sign_verify.py | 2 +- multiversx_sdk_cli/tests/test_cli_multisig.py | 58 +++++++- multiversx_sdk_cli/tests/test_contracts.py | 2 +- multiversx_sdk_cli/transactions.py | 2 +- 10 files changed, 305 insertions(+), 46 deletions(-) diff --git a/multiversx_sdk_cli/cli_contracts.py b/multiversx_sdk_cli/cli_contracts.py index 43736790..1467c248 100644 --- a/multiversx_sdk_cli/cli_contracts.py +++ b/multiversx_sdk_cli/cli_contracts.py @@ -17,8 +17,11 @@ from multiversx_sdk_cli.cosign_transaction import cosign_transaction from multiversx_sdk_cli.dependency_checker import check_if_rust_is_installed from multiversx_sdk_cli.docker import is_docker_installed, run_docker -from multiversx_sdk_cli.errors import DockerMissingError, NoWalletProvided +from multiversx_sdk_cli.errors import (BadUsage, DockerMissingError, + NoWalletProvided) from multiversx_sdk_cli.interfaces import IAddress +from multiversx_sdk_cli.multisig import \ + prepare_transaction_for_deploying_contract from multiversx_sdk_cli.projects.core import get_project_paths_recursively from multiversx_sdk_cli.projects.templates import Contract from multiversx_sdk_cli.ux import show_message @@ -85,6 +88,8 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: " - only valid if --wait-result is set") cli_shared.add_broadcast_args(sub) cli_shared.add_guardian_wallet_args(args, sub) + cli_shared.add_multisig_address_arg(sub) + cli_shared.add_contract_address_for_multisig_deploy(sub) sub.set_defaults(func=deploy) @@ -315,20 +320,43 @@ def deploy(args: Any): address_computer = AddressComputer(NUMBER_OF_SHARDS) contract_address = address_computer.compute_contract_address(deployer=sender.address, deployment_nonce=args.nonce) - tx = contract.prepare_deploy_transaction( - owner=sender, - bytecode=Path(args.bytecode), - arguments=args.arguments, - upgradeable=args.metadata_upgradeable, - readable=args.metadata_readable, - payable=args.metadata_payable, - payable_by_sc=args.metadata_payable_by_sc, - gas_limit=int(args.gas_limit), - value=int(args.value), - nonce=int(args.nonce), - version=int(args.version), - options=int(args.options), - guardian=args.guardian) + if args.multisig: + if not args.deployed_contract: + raise BadUsage("`--deployed-contract` needs to be provided when proposing a deploy action for the multisig contract") + + tx = prepare_transaction_for_deploying_contract( + sender=sender, + multisig=args.multisig, + deployed_contract=args.deployed_contract, + arguments=args.arguments, + upgradeable=args.metadata_upgradeable, + readable=args.metadata_readable, + payable=args.metadata_payable, + payable_by_sc=args.metadata_payable_by_sc, + chain_id=args.chain, + value=int(args.value), + gas_limit=int(args.gas_limit), + nonce=int(args.nonce), + version=int(args.version), + options=int(args.options), + guardian=args.guardian + ) + else: + tx = contract.prepare_deploy_transaction( + owner=sender, + bytecode=Path(args.bytecode), + arguments=args.arguments, + upgradeable=args.metadata_upgradeable, + readable=args.metadata_readable, + payable=args.metadata_payable, + payable_by_sc=args.metadata_payable_by_sc, + gas_limit=int(args.gas_limit), + value=int(args.value), + nonce=int(args.nonce), + version=int(args.version), + options=int(args.options), + guardian=args.guardian) + tx = _sign_guarded_tx(args, tx) logger.info("Contract address: %s", contract_address.to_bech32()) @@ -436,7 +464,7 @@ def _send_or_simulate(tx: Transaction, contract_address: IAddress, args: Any): def verify(args: Any) -> None: - contract = Address.from_bech32(args.contract) + contract = Address.new_from_bech32(args.contract) verifier_url = args.verifier_url packaged_src = Path(args.packaged_src).expanduser().resolve() diff --git a/multiversx_sdk_cli/cli_multisig.py b/multiversx_sdk_cli/cli_multisig.py index f3979402..5ffd3c75 100644 --- a/multiversx_sdk_cli/cli_multisig.py +++ b/multiversx_sdk_cli/cli_multisig.py @@ -5,7 +5,7 @@ from multiversx_sdk_core.transaction_factories.transactions_factory_config import \ TransactionsFactoryConfig -from multiversx_sdk_cli import cli_shared, utils +from multiversx_sdk_cli import cli_shared from multiversx_sdk_cli.contracts import SmartContract from multiversx_sdk_cli.errors import BadUsage from multiversx_sdk_cli.multisig import \ @@ -16,7 +16,9 @@ logger = logging.getLogger("cli.multisig") MULTISIG_SIGN_ACTION_FUNCTION = "sign" +MULTISIG_UNSIGN_ACTION_FUNCTION = "unsign" MULTISIG_PERFORM_ACTION_FUNCTION = "performAction" +MULTISIG_DISCARD_ACTION_FUNCTION = "discardAction" def setup_parser(args: List[str], subparsers: Any) -> Any: @@ -26,7 +28,7 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: sub = cli_shared.add_command_subparser(subparsers, "multisig", "sign", f"Sign a proposed action.") cli_shared.add_multisig_address_arg(sub) cli_shared.add_multisig_view_address_arg(sub) - cli_shared.add_sign_multisig_action_arg(sub) + cli_shared.add_multisig_action_arg(sub) cli_shared.add_wallet_args(args, sub) cli_shared.add_tx_args(args, sub, with_receiver=False, with_data=False, with_guardian=True) @@ -40,10 +42,27 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: " - only valid if --wait-result is set") sub.set_defaults(func=sign_action) + sub = cli_shared.add_command_subparser(subparsers, "multisig", "unsign", f"Unsign a previously signed proposed action.") + cli_shared.add_multisig_address_arg(sub) + cli_shared.add_multisig_view_address_arg(sub) + cli_shared.add_multisig_action_arg(sub) + cli_shared.add_wallet_args(args, sub) + cli_shared.add_tx_args(args, sub, with_receiver=False, with_data=False, with_guardian=True) + + cli_shared.add_outfile_arg(sub, what="signed transaction, hash") + cli_shared.add_broadcast_args(sub, relay=True) + cli_shared.add_proxy_arg(sub) + cli_shared.add_guardian_wallet_args(args, sub) + sub.add_argument("--wait-result", action="store_true", default=False, + help="signal to wait for the transaction result - only valid if --send is set") + sub.add_argument("--timeout", default=100, help="max num of seconds to wait for result" + " - only valid if --wait-result is set") + sub.set_defaults(func=unsign_action) + sub = cli_shared.add_command_subparser(subparsers, "multisig", "perform-action", f"Perform an action that has reached quorum.") cli_shared.add_multisig_address_arg(sub) cli_shared.add_multisig_view_address_arg(sub) - cli_shared.add_sign_multisig_action_arg(sub) + cli_shared.add_multisig_action_arg(sub) cli_shared.add_wallet_args(args, sub) cli_shared.add_tx_args(args, sub, with_receiver=False, with_data=False, with_guardian=True) @@ -57,6 +76,23 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: " - only valid if --wait-result is set") sub.set_defaults(func=perform_action) + sub = cli_shared.add_command_subparser(subparsers, "multisig", "discard-action", f"Discard a proposed action.") + cli_shared.add_multisig_address_arg(sub) + cli_shared.add_multisig_view_address_arg(sub) + cli_shared.add_multisig_action_arg(sub) + cli_shared.add_wallet_args(args, sub) + cli_shared.add_tx_args(args, sub, with_receiver=False, with_data=False, with_guardian=True) + + cli_shared.add_outfile_arg(sub, what="signed transaction, hash") + cli_shared.add_broadcast_args(sub, relay=True) + cli_shared.add_proxy_arg(sub) + cli_shared.add_guardian_wallet_args(args, sub) + sub.add_argument("--wait-result", action="store_true", default=False, + help="signal to wait for the transaction result - only valid if --send is set") + sub.add_argument("--timeout", default=100, help="max num of seconds to wait for result" + " - only valid if --wait-result is set") + sub.set_defaults(func=discard_action) + sub = cli_shared.add_command_subparser(subparsers, "multisig", "deposit", f"Deposit assets into the multisig contract.") cli_shared.add_multisig_address_arg(sub) cli_shared.add_multisig_view_address_arg(sub) @@ -79,8 +115,6 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: def sign_action(args: Any): - args = utils.as_object(args) - cli_shared.check_guardian_and_options_args(args) cli_shared.check_broadcast_args(args) cli_shared.prepare_chain_id_in_args(args) @@ -100,7 +134,7 @@ def sign_action(args: Any): caller=sender, contract=contract_address, function=MULTISIG_SIGN_ACTION_FUNCTION, - arguments=args.action_id, + arguments=[args.action_id], gas_limit=int(args.gas_limit), value=int(args.value), transfers=None, @@ -109,6 +143,9 @@ def sign_action(args: Any): options=int(args.options), guardian=args.guardian) + if tx.guardian: + tx = sign_tx_by_guardian(args, tx) + if hasattr(args, "relay") and args.relay: args.outfile.write(compute_relayed_v1_data(tx)) return @@ -116,9 +153,46 @@ def sign_action(args: Any): cli_shared.send_or_simulate(tx, args) -def perform_action(args: Any): - args = utils.as_object(args) +def unsign_action(args: Any): + cli_shared.check_guardian_and_options_args(args) + cli_shared.check_broadcast_args(args) + cli_shared.prepare_chain_id_in_args(args) + cli_shared.prepare_nonce_in_args(args) + + config = TransactionsFactoryConfig(args.chain) + contract = SmartContract(config) + + sender = cli_shared.prepare_account(args) + contract_address = Address.new_from_bech32(args.multisig) + + action_id = args.action_id + if action_id == "all": + raise BadUsage("`all` is not supported at the moment. Please use a specific action id") + + tx = contract.prepare_execute_transaction( + caller=sender, + contract=contract_address, + function=MULTISIG_UNSIGN_ACTION_FUNCTION, + arguments=[args.action_id], + gas_limit=int(args.gas_limit), + value=int(args.value), + transfers=None, + nonce=int(args.nonce), + version=int(args.version), + options=int(args.options), + guardian=args.guardian) + + if tx.guardian: + tx = sign_tx_by_guardian(args, tx) + + if hasattr(args, "relay") and args.relay: + args.outfile.write(compute_relayed_v1_data(tx)) + return + + cli_shared.send_or_simulate(tx, args) + +def perform_action(args: Any): cli_shared.check_guardian_and_options_args(args) cli_shared.check_broadcast_args(args) cli_shared.prepare_chain_id_in_args(args) @@ -138,7 +212,46 @@ def perform_action(args: Any): caller=sender, contract=contract_address, function=MULTISIG_PERFORM_ACTION_FUNCTION, - arguments=args.action_id, + arguments=[args.action_id], + gas_limit=int(args.gas_limit), + value=int(args.value), + transfers=None, + nonce=int(args.nonce), + version=int(args.version), + options=int(args.options), + guardian=args.guardian) + + if tx.guardian: + tx = sign_tx_by_guardian(args, tx) + + if hasattr(args, "relay") and args.relay: + args.outfile.write(compute_relayed_v1_data(tx)) + return + + cli_shared.send_or_simulate(tx, args) + + +def discard_action(args: Any): + cli_shared.check_guardian_and_options_args(args) + cli_shared.check_broadcast_args(args) + cli_shared.prepare_chain_id_in_args(args) + cli_shared.prepare_nonce_in_args(args) + + config = TransactionsFactoryConfig(args.chain) + contract = SmartContract(config) + + sender = cli_shared.prepare_account(args) + contract_address = Address.new_from_bech32(args.multisig) + + action_id = args.action_id + if action_id == "all": + raise BadUsage("`all` is not supported at the moment. Please use a specific action id") + + tx = contract.prepare_execute_transaction( + caller=sender, + contract=contract_address, + function=MULTISIG_DISCARD_ACTION_FUNCTION, + arguments=[args.action_id], gas_limit=int(args.gas_limit), value=int(args.value), transfers=None, @@ -147,6 +260,9 @@ def perform_action(args: Any): options=int(args.options), guardian=args.guardian) + if tx.guardian: + tx = sign_tx_by_guardian(args, tx) + if hasattr(args, "relay") and args.relay: args.outfile.write(compute_relayed_v1_data(tx)) return diff --git a/multiversx_sdk_cli/cli_shared.py b/multiversx_sdk_cli/cli_shared.py index ef3158e2..68cf976a 100644 --- a/multiversx_sdk_cli/cli_shared.py +++ b/multiversx_sdk_cli/cli_shared.py @@ -147,12 +147,16 @@ def add_multisig_address_arg(sub: Any): sub.add_argument("--multisig", help="the address of the multisig contract") +def add_contract_address_for_multisig_deploy(sub: Any): + sub.add_argument("--deployed-contract", help="the address of the already deployed contract to be deployed by the multisig") + + def add_multisig_view_address_arg(sub: Any): sub.add_argument("--multisig-view", help="the address of the multisig-view contract") -def add_sign_multisig_action_arg(sub: Any): - sub.add_argument("--action-id", help="an integer representing the ID of the action; cand also be `all`") +def add_multisig_action_arg(sub: Any): + sub.add_argument("--action-id", help="an integer representing the ID of the action; can also be `all`") def parse_omit_fields_arg(args: Any) -> List[str]: diff --git a/multiversx_sdk_cli/contracts.py b/multiversx_sdk_cli/contracts.py index 5db8d2c0..4e78f615 100644 --- a/multiversx_sdk_cli/contracts.py +++ b/multiversx_sdk_cli/contracts.py @@ -308,7 +308,7 @@ def _to_hex(arg: str): if arg.isnumeric(): return _prepare_decimal(arg) elif arg.startswith(DEFAULT_HRP): - addr = Address.from_bech32(arg) + addr = Address.new_from_bech32(arg) return _prepare_hexadecimal(f"{HEX_PREFIX}{addr.hex()}") elif arg.lower() == FALSE_STR_LOWER or arg.lower() == TRUE_STR_LOWER: as_str = f"{HEX_PREFIX}01" if arg.lower() == TRUE_STR_LOWER else f"{HEX_PREFIX}00" diff --git a/multiversx_sdk_cli/dns.py b/multiversx_sdk_cli/dns.py index 54485a48..901b739c 100644 --- a/multiversx_sdk_cli/dns.py +++ b/multiversx_sdk_cli/dns.py @@ -26,8 +26,8 @@ def resolve(name: str, proxy: INetworkProvider) -> Address: result = query_contract(dns_address, proxy, "resolve", [name_arg]) if len(result) == 0: - return Address.from_bech32(ADDRESS_ZERO_BECH32) - return Address.from_hex(result[0].hex, DEFAULT_HRP) + return Address.new_from_bech32(ADDRESS_ZERO_BECH32) + return Address.new_from_hex(result[0].hex, DEFAULT_HRP) def validate_name(name: str, shard_id: int, proxy: INetworkProvider): diff --git a/multiversx_sdk_cli/multisig.py b/multiversx_sdk_cli/multisig.py index 2d02ab34..2b2a17d9 100644 --- a/multiversx_sdk_cli/multisig.py +++ b/multiversx_sdk_cli/multisig.py @@ -1,14 +1,19 @@ -from typing import List, Union +from typing import Any, List, Union -from multiversx_sdk_core import (Address, TokenComputer, TokenTransfer, - Transaction) +from multiversx_sdk_core import (Address, CodeMetadata, TokenComputer, + TokenTransfer, Transaction) +from multiversx_sdk_core.serializer import arg_to_string, args_to_strings from multiversx_sdk_core.transaction_factories.token_transfers_data_builder import \ TokenTransfersDataBuilder from multiversx_sdk_core.transaction_factories.transactions_factory_config import \ TransactionsFactoryConfig from multiversx_sdk_cli.accounts import Account -from multiversx_sdk_cli.contracts import SmartContract +from multiversx_sdk_cli.contracts import (SmartContract, + prepare_args_for_factory) +from multiversx_sdk_cli.interfaces import IAddress + +MULTISIG_DEPLOY_FUNCTION = "proposeSCDeployFromSource" def prepare_transaction_for_egld_transfer(sender: Account, @@ -26,7 +31,7 @@ def prepare_transaction_for_egld_transfer(sender: Account, return contract.prepare_execute_transaction( caller=sender, - contract=Address.from_bech32(multisig), + contract=Address.new_from_bech32(multisig), function="proposeTransferExecute", arguments=[f"{receiver}", f"{value}"], gas_limit=gas_limit, @@ -52,9 +57,9 @@ def prepare_transaction_for_custom_token_transfer(sender: Account, contract = SmartContract(config) token_transfers = contract.prepare_token_transfers(transfers) - transfer_receiver = Address.from_bech32(receiver) + transfer_receiver = Address.new_from_bech32(receiver) transfer_data_parts = _prepare_data_parts_for_multisig_transfer(transfer_receiver, token_transfers) - multisig_contract = Address.from_bech32(multisig) + multisig_contract = Address.new_from_bech32(multisig) arguments: List[str] = [transfer_receiver.to_hex(), "00"] if transfer_data_parts[0] != "ESDTTransfer": @@ -91,13 +96,13 @@ def prepare_transaction_for_depositing_funds(sender: Account, nonce: int, version: int, options: int, - guardian: str): + guardian: str) -> Transaction: config = TransactionsFactoryConfig(chain_id) contract = SmartContract(config) return contract.prepare_execute_transaction( caller=sender, - contract=Address.from_bech32(multisig), + contract=Address.new_from_bech32(multisig), function="deposit", arguments=None, gas_limit=gas_limit, @@ -106,8 +111,64 @@ def prepare_transaction_for_depositing_funds(sender: Account, nonce=nonce, version=version, options=options, + guardian=guardian) + + +def prepare_transaction_for_deploying_contract(sender: Account, + multisig: str, + deployed_contract: str, + arguments: Union[List[str], None], + upgradeable: bool, + readable: bool, + payable: bool, + payable_by_sc: bool, + chain_id: str, + value: int, + gas_limit: int, + nonce: int, + version: int, + options: int, + guardian: str) -> Transaction: + # convert the args to proper type instead of strings + prepared_arguments = prepare_args_for_factory(arguments) if arguments else [] + metadata = CodeMetadata(upgradeable, readable, payable, payable_by_sc) + contract = Address.new_from_bech32(deployed_contract) + + data = _prepare_data_field_for_deploy_transaction(amount=value, + deployed_contract=contract, + metadata=metadata, + arguments=prepared_arguments) + tx = Transaction( + sender=sender.address.to_bech32(), + receiver=multisig, + gas_limit=gas_limit, + chain_id=chain_id, + nonce=nonce, + amount=0, + data=data, + version=version, + options=options, guardian=guardian ) + tx.signature = bytes.fromhex(sender.sign_transaction(tx)) + + return tx + + +def _prepare_data_field_for_deploy_transaction(amount: int, + deployed_contract: IAddress, + metadata: CodeMetadata, + arguments: List[Any]) -> bytes: + data_parts = [ + MULTISIG_DEPLOY_FUNCTION, + arg_to_string(amount), + deployed_contract.to_hex(), + str(metadata) + ] + data_parts.extend(args_to_strings(arguments)) + payload = _build_data_payload(data_parts) + + return payload.encode() def _prepare_data_parts_for_multisig_transfer(receiver: Address, token_transfers: List[TokenTransfer]): diff --git a/multiversx_sdk_cli/sign_verify.py b/multiversx_sdk_cli/sign_verify.py index 240a843e..f49f0971 100644 --- a/multiversx_sdk_cli/sign_verify.py +++ b/multiversx_sdk_cli/sign_verify.py @@ -24,7 +24,7 @@ def verify_signature(self) -> bool: verifiable_message.signature = bytes.fromhex(self.signature) message_computer = MessageComputer() - verifier = UserVerifier.from_address(Address.from_bech32(self.address)) + verifier = UserVerifier.from_address(Address.new_from_bech32(self.address)) is_signed = verifier.verify(message_computer.compute_bytes_for_signing(verifiable_message), verifiable_message.signature) return is_signed diff --git a/multiversx_sdk_cli/tests/test_cli_multisig.py b/multiversx_sdk_cli/tests/test_cli_multisig.py index 8d810b05..e1febc55 100644 --- a/multiversx_sdk_cli/tests/test_cli_multisig.py +++ b/multiversx_sdk_cli/tests/test_cli_multisig.py @@ -16,8 +16,8 @@ def test_sign_action(capsys: Any): "--action-id", "2", "--pem", str(alice), "--nonce", "1289", - "--gas-limit", "10000000", - "--proxy", "https://testnet-api.multiversx.com" + "--chain", "T", + "--gas-limit", "10000000" ]) assert False if return_code else True @@ -34,6 +34,31 @@ def test_sign_action(capsys: Any): assert chain_id == "T" +def test_unsign_action(capsys: Any): + return_code = main([ + "multisig", "unsign", + "--multisig", "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30", + "--action-id", "2", + "--pem", str(alice), + "--nonce", "1289", + "--chain", "T", + "--gas-limit", "10000000" + ]) + assert False if return_code else True + + transaction = get_transaction(capsys) + + data_field: str = transaction["data"] + data = base64.b64decode(data_field.encode()).decode() + assert data == "unsign@02" + + receiver = transaction["receiver"] + assert receiver == "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + + chain_id = transaction["chainID"] + assert chain_id == "T" + + def test_perform_action(capsys: Any): return_code = main([ "multisig", "perform-action", @@ -41,8 +66,8 @@ def test_perform_action(capsys: Any): "--action-id", "2", "--pem", str(alice), "--nonce", "1290", - "--gas-limit", "10000000", - "--proxy", "https://testnet-api.multiversx.com" + "--chain", "T", + "--gas-limit", "10000000" ]) assert False if return_code else True @@ -59,6 +84,31 @@ def test_perform_action(capsys: Any): assert chain_id == "T" +def test_discard_action(capsys: Any): + return_code = main([ + "multisig", "discard-action", + "--multisig", "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30", + "--action-id", "15", + "--pem", str(alice), + "--nonce", "55", + "--chain", "T", + "--gas-limit", "10000000" + ]) + assert False if return_code else True + + transaction = get_transaction(capsys) + + data_field: str = transaction["data"] + data = base64.b64decode(data_field.encode()).decode() + assert data == "discardAction@0f" + + receiver = transaction["receiver"] + assert receiver == "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + + chain_id = transaction["chainID"] + assert chain_id == "T" + + def test_deposit_egld(capsys: Any): return_code = main([ "multisig", "deposit", diff --git a/multiversx_sdk_cli/tests/test_contracts.py b/multiversx_sdk_cli/tests/test_contracts.py index 733969a3..2cc7e251 100644 --- a/multiversx_sdk_cli/tests/test_contracts.py +++ b/multiversx_sdk_cli/tests/test_contracts.py @@ -56,7 +56,7 @@ def test_prepare_argument(): def test_contract_verification_create_request_signature(): account = Account(pem_file=str(testdata_folder / "walletKey.pem")) - contract_address = Address.from_bech32("erd1qqqqqqqqqqqqqpgqeyj9g344pqguukajpcfqz9p0rfqgyg4l396qespdck") + contract_address = Address.new_from_bech32("erd1qqqqqqqqqqqqqpgqeyj9g344pqguukajpcfqz9p0rfqgyg4l396qespdck") request_payload = b"test" signature = _create_request_signature(account, contract_address, request_payload) diff --git a/multiversx_sdk_cli/transactions.py b/multiversx_sdk_cli/transactions.py index d859a03c..5d026236 100644 --- a/multiversx_sdk_cli/transactions.py +++ b/multiversx_sdk_cli/transactions.py @@ -114,7 +114,7 @@ def get_guardian_account_from_args(args: Any): account = Account(key_file=args.guardian_keyfile, password=password) elif args.guardian_ledger: address = do_get_ledger_address(account_index=args.guardian_ledger_account_index, address_index=args.guardian_ledger_address_index) - account = Account(address=Address.from_bech32(address)) + account = Account(address=Address.new_from_bech32(address)) else: raise errors.NoWalletProvided() From 263f60b1da21afeeca24fd767fadbc5578acd567 Mon Sep 17 00:00:00 2001 From: Alexandru Popenta Date: Tue, 16 Jan 2024 13:56:20 +0200 Subject: [PATCH 7/9] implement contract deploy from source and contract upgrade from source for the multisig --- multiversx_sdk_cli/cli_contracts.py | 110 +++++++++++++----- multiversx_sdk_cli/cli_shared.py | 4 - multiversx_sdk_cli/multisig.py | 70 ++++++++++- multiversx_sdk_cli/tests/test_cli_multisig.py | 59 ++++++++++ 4 files changed, 203 insertions(+), 40 deletions(-) diff --git a/multiversx_sdk_cli/cli_contracts.py b/multiversx_sdk_cli/cli_contracts.py index 1467c248..03d760f8 100644 --- a/multiversx_sdk_cli/cli_contracts.py +++ b/multiversx_sdk_cli/cli_contracts.py @@ -20,8 +20,9 @@ from multiversx_sdk_cli.errors import (BadUsage, DockerMissingError, NoWalletProvided) from multiversx_sdk_cli.interfaces import IAddress -from multiversx_sdk_cli.multisig import \ - prepare_transaction_for_deploying_contract +from multiversx_sdk_cli.multisig import ( + prepare_transaction_for_deploying_contract, + prepare_transaction_upgrading_contract) from multiversx_sdk_cli.projects.core import get_project_paths_recursively from multiversx_sdk_cli.projects.templates import Contract from multiversx_sdk_cli.ux import show_message @@ -89,7 +90,7 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: cli_shared.add_broadcast_args(sub) cli_shared.add_guardian_wallet_args(args, sub) cli_shared.add_multisig_address_arg(sub) - cli_shared.add_contract_address_for_multisig_deploy(sub) + add_contract_address_for_multisig_deploy(sub) sub.set_defaults(func=deploy) @@ -128,6 +129,8 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: " - only valid if --wait-result is set") cli_shared.add_broadcast_args(sub) cli_shared.add_guardian_wallet_args(args, sub) + cli_shared.add_multisig_address_arg(sub) + add_contract_address_for_multisig_upgrade(sub) sub.set_defaults(func=upgrade) @@ -223,8 +226,8 @@ def _add_recursive_arg(sub: Any): def _add_bytecode_arg(sub: Any): - sub.add_argument("--bytecode", type=str, required=True, - help="the file containing the WASM bytecode") + sub.add_argument("--bytecode", type=str, + help="the file containing the WASM bytecode; not needed when deploying using a multisig contract") def _add_contract_arg(sub: Any): @@ -253,6 +256,14 @@ def _add_metadata_arg(sub: Any): sub.set_defaults(metadata_upgradeable=True, metadata_payable=False) +def add_contract_address_for_multisig_deploy(sub: Any): + sub.add_argument("--deployed-contract", help="the address of the already deployed contract to be re-deployed by the multisig") + + +def add_contract_address_for_multisig_upgrade(sub: Any): + sub.add_argument("--upgraded-contract", help="the address of the already upgraded contract, that will be used to upgrade the contract owned by the multisig") + + def list_templates(args: Any): tag = args.tag contract = Contract(tag) @@ -314,20 +325,25 @@ def deploy(args: Any): cli_shared.prepare_nonce_in_args(args) sender = cli_shared.prepare_account(args) - config = TransactionsFactoryConfig(args.chain) - contract = SmartContract(config) - address_computer = AddressComputer(NUMBER_OF_SHARDS) - contract_address = address_computer.compute_contract_address(deployer=sender.address, deployment_nonce=args.nonce) if args.multisig: if not args.deployed_contract: raise BadUsage("`--deployed-contract` needs to be provided when proposing a deploy action for the multisig contract") + multisig_address = Address.new_from_bech32(args.multisig) + + if not args.proxy: + raise BadUsage("`--proxy` is required in order to compute the contract address") + + proxy = ProxyNetworkProvider(args.proxy) + multisig_nonce = proxy.get_account(multisig_address).nonce + contract_address = address_computer.compute_contract_address(deployer=multisig_address, deployment_nonce=multisig_nonce) + tx = prepare_transaction_for_deploying_contract( sender=sender, - multisig=args.multisig, - deployed_contract=args.deployed_contract, + multisig=Address.new_from_bech32(args.multisig), + deployed_contract=Address.new_from_bech32(args.deployed_contract), arguments=args.arguments, upgradeable=args.metadata_upgradeable, readable=args.metadata_readable, @@ -339,9 +355,15 @@ def deploy(args: Any): nonce=int(args.nonce), version=int(args.version), options=int(args.options), - guardian=args.guardian - ) + guardian=args.guardian) else: + if not args.bytecode: + raise BadUsage("`--bytecode` is required when deploying a contract") + + contract_address = address_computer.compute_contract_address(deployer=sender.address, deployment_nonce=args.nonce) + config = TransactionsFactoryConfig(args.chain) + contract = SmartContract(config) + tx = contract.prepare_deploy_transaction( owner=sender, bytecode=Path(args.bytecode), @@ -416,27 +438,53 @@ def upgrade(args: Any): cli_shared.prepare_nonce_in_args(args) sender = cli_shared.prepare_account(args) - config = TransactionsFactoryConfig(args.chain) - contract = SmartContract(config) contract_address = Address.new_from_bech32(args.contract) - tx = contract.prepare_upgrade_transaction( - owner=sender, - contract=contract_address, - bytecode=Path(args.bytecode), - arguments=args.arguments, - upgradeable=args.metadata_upgradeable, - readable=args.metadata_readable, - payable=args.metadata_payable, - payable_by_sc=args.metadata_payable_by_sc, - gas_limit=int(args.gas_limit), - value=int(args.value), - nonce=int(args.nonce), - version=int(args.version), - options=int(args.options), - guardian=args.guardian) - tx = _sign_guarded_tx(args, tx) + if args.multisig: + if not args.upgraded_contract: + raise BadUsage("`--upgraded-contract` needs to be provided when proposing an upgrade action for a contract owned by a multisig contract") + tx = prepare_transaction_upgrading_contract( + sender=sender, + contract_address=Address.new_from_bech32(args.contract), + multisig=Address.new_from_bech32(args.multisig), + upgraded_contract=Address.new_from_bech32(args.upgraded_contract), + arguments=args.arguments, + upgradeable=args.metadata_upgradeable, + readable=args.metadata_readable, + payable=args.metadata_payable, + payable_by_sc=args.metadata_payable_by_sc, + chain_id=args.chain, + value=int(args.value), + gas_limit=int(args.gas_limit), + nonce=int(args.nonce), + version=int(args.version), + options=int(args.options), + guardian=args.guardian) + else: + if not args.bytecode: + raise BadUsage("`--bytecode` is required when upgrading a contract") + + config = TransactionsFactoryConfig(args.chain) + contract = SmartContract(config) + + tx = contract.prepare_upgrade_transaction( + owner=sender, + contract=contract_address, + bytecode=Path(args.bytecode), + arguments=args.arguments, + upgradeable=args.metadata_upgradeable, + readable=args.metadata_readable, + payable=args.metadata_payable, + payable_by_sc=args.metadata_payable_by_sc, + gas_limit=int(args.gas_limit), + value=int(args.value), + nonce=int(args.nonce), + version=int(args.version), + options=int(args.options), + guardian=args.guardian) + + tx = _sign_guarded_tx(args, tx) _send_or_simulate(tx, contract_address, args) diff --git a/multiversx_sdk_cli/cli_shared.py b/multiversx_sdk_cli/cli_shared.py index 68cf976a..af91217b 100644 --- a/multiversx_sdk_cli/cli_shared.py +++ b/multiversx_sdk_cli/cli_shared.py @@ -147,10 +147,6 @@ def add_multisig_address_arg(sub: Any): sub.add_argument("--multisig", help="the address of the multisig contract") -def add_contract_address_for_multisig_deploy(sub: Any): - sub.add_argument("--deployed-contract", help="the address of the already deployed contract to be deployed by the multisig") - - def add_multisig_view_address_arg(sub: Any): sub.add_argument("--multisig-view", help="the address of the multisig-view contract") diff --git a/multiversx_sdk_cli/multisig.py b/multiversx_sdk_cli/multisig.py index 2b2a17d9..02af842f 100644 --- a/multiversx_sdk_cli/multisig.py +++ b/multiversx_sdk_cli/multisig.py @@ -14,6 +14,7 @@ from multiversx_sdk_cli.interfaces import IAddress MULTISIG_DEPLOY_FUNCTION = "proposeSCDeployFromSource" +MULTISIG_UPGRADE_FUNCTION = "proposeSCUpgradeFromSource" def prepare_transaction_for_egld_transfer(sender: Account, @@ -115,8 +116,8 @@ def prepare_transaction_for_depositing_funds(sender: Account, def prepare_transaction_for_deploying_contract(sender: Account, - multisig: str, - deployed_contract: str, + multisig: IAddress, + deployed_contract: IAddress, arguments: Union[List[str], None], upgradeable: bool, readable: bool, @@ -132,15 +133,14 @@ def prepare_transaction_for_deploying_contract(sender: Account, # convert the args to proper type instead of strings prepared_arguments = prepare_args_for_factory(arguments) if arguments else [] metadata = CodeMetadata(upgradeable, readable, payable, payable_by_sc) - contract = Address.new_from_bech32(deployed_contract) data = _prepare_data_field_for_deploy_transaction(amount=value, - deployed_contract=contract, + deployed_contract=deployed_contract, metadata=metadata, arguments=prepared_arguments) tx = Transaction( sender=sender.address.to_bech32(), - receiver=multisig, + receiver=multisig.to_bech32(), gas_limit=gas_limit, chain_id=chain_id, nonce=nonce, @@ -155,6 +155,66 @@ def prepare_transaction_for_deploying_contract(sender: Account, return tx +def prepare_transaction_upgrading_contract(sender: Account, + contract_address: IAddress, + multisig: IAddress, + upgraded_contract: IAddress, + arguments: Union[List[str], None], + upgradeable: bool, + readable: bool, + payable: bool, + payable_by_sc: bool, + chain_id: str, + value: int, + gas_limit: int, + nonce: int, + version: int, + options: int, + guardian: str) -> Transaction: + # convert the args to proper type instead of strings + prepared_arguments = prepare_args_for_factory(arguments) if arguments else [] + metadata = CodeMetadata(upgradeable, readable, payable, payable_by_sc) + + data = _prepare_data_field_for_upgrade_transaction(contract_address=contract_address, + amount=value, + upgraded_contract=upgraded_contract, + metadata=metadata, + arguments=prepared_arguments) + tx = Transaction( + sender=sender.address.to_bech32(), + receiver=multisig.to_bech32(), + gas_limit=gas_limit, + chain_id=chain_id, + nonce=nonce, + amount=0, + data=data, + version=version, + options=options, + guardian=guardian + ) + tx.signature = bytes.fromhex(sender.sign_transaction(tx)) + + return tx + + +def _prepare_data_field_for_upgrade_transaction(contract_address: IAddress, + amount: int, + upgraded_contract: IAddress, + metadata: CodeMetadata, + arguments: List[Any]) -> bytes: + data_parts = [ + MULTISIG_UPGRADE_FUNCTION, + contract_address.to_hex(), + arg_to_string(amount), + upgraded_contract.to_hex(), + str(metadata) + ] + data_parts.extend(args_to_strings(arguments)) + payload = _build_data_payload(data_parts) + + return payload.encode() + + def _prepare_data_field_for_deploy_transaction(amount: int, deployed_contract: IAddress, metadata: CodeMetadata, diff --git a/multiversx_sdk_cli/tests/test_cli_multisig.py b/multiversx_sdk_cli/tests/test_cli_multisig.py index e1febc55..3431afd1 100644 --- a/multiversx_sdk_cli/tests/test_cli_multisig.py +++ b/multiversx_sdk_cli/tests/test_cli_multisig.py @@ -298,6 +298,65 @@ def test_propose_multi_esdt_nft_transfer(capsys: Any): assert signature == "563fc6eefe9469cf90191462cfe21ab25ae7291c1c411cd4b3778717c827045eabc58b689308e8ee45676a8e49cf75c2856a83e41e93dad5c4acb5ccb65c5b04" +def test_propose_contract_deploy_from_source(capsys: Any): + return_code = main([ + "contract", "deploy", + "--pem", str(alice), + "--nonce", "60", + "--chain", "T", + "--proxy", "https://testnet-api.multiversx.com", + "--gas-limit", "100000000", + "--multisig", "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30", + "--deployed-contract", "erd1qqqqqqqqqqqqqpgq8z2zzyu30f4607hth0tfj5m3vpjvwrvvrawqw09jem", + "--arguments", "0" + ]) + assert False if return_code else True + + transaction = get_transaction(capsys) + + data_field: str = transaction["data"] + data = base64.b64decode(data_field.encode()).decode() + assert data == "proposeSCDeployFromSource@@0000000000000000050038942113917a6ba7faebbbd69953716064c70d8c1f5c@0500@" + + receiver = transaction["receiver"] + assert receiver == "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + + chain_id = transaction["chainID"] + assert chain_id == "T" + + value = int(transaction["value"]) + assert value == 0 + + +def test_propose_contract_upgrade_from_source(capsys: Any): + return_code = main([ + "contract", "upgrade", "erd1qqqqqqqqqqqqqpgqz0kha878srg82eznjhdyvgarwycwjgs6rawq02lh6j", + "--pem", str(alice), + "--nonce", "6241", + "--chain", "T", + "--gas-limit", "100000000", + "--multisig", "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30", + "--upgraded-contract", "erd1qqqqqqqqqqqqqpgq8z2zzyu30f4607hth0tfj5m3vpjvwrvvrawqw09jem", + "--arguments", "0" + ]) + assert False if return_code else True + + transaction = get_transaction(capsys) + + data_field: str = transaction["data"] + data = base64.b64decode(data_field.encode()).decode() + assert data == "proposeSCUpgradeFromSource@0000000000000000050013ed7e9fc780d075645395da4623a37130e9221a1f5c@@0000000000000000050038942113917a6ba7faebbbd69953716064c70d8c1f5c@0500@" + + receiver = transaction["receiver"] + assert receiver == "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + + chain_id = transaction["chainID"] + assert chain_id == "T" + + value = int(transaction["value"]) + assert value == 0 + + def get_transaction(capsys: Any) -> Dict[str, Any]: out = _read_stdout(capsys) output = json.loads(out) From 2a7d74d83351026141b014ebda4bdda2ee60cad8 Mon Sep 17 00:00:00 2001 From: Alexandru Popenta Date: Tue, 16 Jan 2024 17:16:28 +0200 Subject: [PATCH 8/9] add support fot contract calls for the multisig contract --- multiversx_sdk_cli/cli_contracts.py | 49 ++++-- multiversx_sdk_cli/multisig.py | 87 +++++++++- multiversx_sdk_cli/tests/test_cli_multisig.py | 149 ++++++++++++++++++ 3 files changed, 265 insertions(+), 20 deletions(-) diff --git a/multiversx_sdk_cli/cli_contracts.py b/multiversx_sdk_cli/cli_contracts.py index 03d760f8..27cd12f5 100644 --- a/multiversx_sdk_cli/cli_contracts.py +++ b/multiversx_sdk_cli/cli_contracts.py @@ -21,6 +21,7 @@ NoWalletProvided) from multiversx_sdk_cli.interfaces import IAddress from multiversx_sdk_cli.multisig import ( + prepare_transaction_for_contract_call, prepare_transaction_for_deploying_contract, prepare_transaction_upgrading_contract) from multiversx_sdk_cli.projects.core import get_project_paths_recursively @@ -110,6 +111,7 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: " - only valid if --wait-result is set") cli_shared.add_broadcast_args(sub, relay=True) cli_shared.add_guardian_wallet_args(args, sub) + cli_shared.add_multisig_address_arg(sub) sub.set_defaults(func=call) @@ -409,24 +411,41 @@ def call(args: Any): cli_shared.prepare_nonce_in_args(args) sender = cli_shared.prepare_account(args) - config = TransactionsFactoryConfig(args.chain) - contract = SmartContract(config) contract_address = Address.new_from_bech32(args.contract) - tx = contract.prepare_execute_transaction( - caller=sender, - contract=contract_address, - function=args.function, - arguments=args.arguments, - gas_limit=int(args.gas_limit), - value=int(args.value), - transfers=args.token_transfers, - nonce=int(args.nonce), - version=int(args.version), - options=int(args.options), - guardian=args.guardian) - tx = _sign_guarded_tx(args, tx) + if args.multisig: + tx = prepare_transaction_for_contract_call( + sender=sender, + contract_address=contract_address, + function=args.function, + arguments=args.arguments, + multisig=Address.new_from_bech32(args.multisig), + value=int(args.value), + transfers=args.token_transfers, + gas_limit=int(args.gas_limit), + chain_id=args.chain, + nonce=int(args.nonce), + version=int(args.version), + options=int(args.options), + guardian=args.guardian) + else: + config = TransactionsFactoryConfig(args.chain) + contract = SmartContract(config) + tx = contract.prepare_execute_transaction( + caller=sender, + contract=contract_address, + function=args.function, + arguments=args.arguments, + gas_limit=int(args.gas_limit), + value=int(args.value), + transfers=args.token_transfers, + nonce=int(args.nonce), + version=int(args.version), + options=int(args.options), + guardian=args.guardian) + + tx = _sign_guarded_tx(args, tx) _send_or_simulate(tx, contract_address, args) diff --git a/multiversx_sdk_cli/multisig.py b/multiversx_sdk_cli/multisig.py index 02af842f..be9837f8 100644 --- a/multiversx_sdk_cli/multisig.py +++ b/multiversx_sdk_cli/multisig.py @@ -11,8 +11,12 @@ from multiversx_sdk_cli.accounts import Account from multiversx_sdk_cli.contracts import (SmartContract, prepare_args_for_factory) +from multiversx_sdk_cli.errors import BadUsage from multiversx_sdk_cli.interfaces import IAddress +MULTISIG_DEPOSIT_FUNCTION = "deposit" +MULTISIG_TRANSFER_AND_EXECUTE = "proposeTransferExecute" +MULTISIG_ASYNC_CALL = "proposeAsyncCall" MULTISIG_DEPLOY_FUNCTION = "proposeSCDeployFromSource" MULTISIG_UPGRADE_FUNCTION = "proposeSCUpgradeFromSource" @@ -33,7 +37,7 @@ def prepare_transaction_for_egld_transfer(sender: Account, return contract.prepare_execute_transaction( caller=sender, contract=Address.new_from_bech32(multisig), - function="proposeTransferExecute", + function=MULTISIG_TRANSFER_AND_EXECUTE, arguments=[f"{receiver}", f"{value}"], gas_limit=gas_limit, value=0, @@ -66,13 +70,13 @@ def prepare_transaction_for_custom_token_transfer(sender: Account, if transfer_data_parts[0] != "ESDTTransfer": arguments[0] = multisig_contract.to_hex() - transfer_data_parts[0] = transfer_data_parts[0].encode().hex() + transfer_data_parts[0] = arg_to_string(transfer_data_parts[0]) arguments.extend(transfer_data_parts) tx = contract.prepare_execute_transaction( caller=sender, contract=multisig_contract, - function="proposeAsyncCall", + function=MULTISIG_ASYNC_CALL, arguments=None, gas_limit=gas_limit, value=0, @@ -104,7 +108,7 @@ def prepare_transaction_for_depositing_funds(sender: Account, return contract.prepare_execute_transaction( caller=sender, contract=Address.new_from_bech32(multisig), - function="deposit", + function=MULTISIG_DEPOSIT_FUNCTION, arguments=None, gas_limit=gas_limit, value=value, @@ -197,6 +201,79 @@ def prepare_transaction_upgrading_contract(sender: Account, return tx +def prepare_transaction_for_contract_call(sender: Account, + contract_address: IAddress, + function: str, + arguments: Union[List[str], None], + multisig: IAddress, + value: int, + transfers: Union[List[str], None], + gas_limit: int, + chain_id: str, + nonce: int, + version: int, + options: int, + guardian: str) -> Transaction: + if value and transfers: + raise BadUsage("Can't send both native and custom tokens") + + config = TransactionsFactoryConfig(chain_id) + contract = SmartContract(config) + + token_transfers = contract.prepare_token_transfers(transfers) if transfers else [] + prepared_args = prepare_args_for_factory(arguments) if arguments else [] + + data_field = _prepare_data_field_for_contract_call(contract_address=contract_address, + multisig=multisig, + function=function, + arguments=prepared_args, + value=value, + token_transfers=token_transfers) + tx = Transaction( + sender=sender.address.to_bech32(), + receiver=multisig.to_bech32(), + gas_limit=gas_limit, + chain_id=chain_id, + nonce=nonce, + amount=0, + data=data_field, + version=version, + options=options, + guardian=guardian + ) + tx.signature = bytes.fromhex(sender.sign_transaction(tx)) + + return tx + + +def _prepare_data_field_for_contract_call(contract_address: IAddress, + multisig: IAddress, + function: str, + arguments: List[Any], + value: int, + token_transfers: List[TokenTransfer]): + data_parts = [ + MULTISIG_ASYNC_CALL, + contract_address.to_hex(), + arg_to_string(value) + ] + + transfer_data_parts = _prepare_data_parts_for_multisig_transfer(receiver=contract_address, token_transfers=token_transfers) + + if transfer_data_parts: + if transfer_data_parts[0] != "ESDTTransfer": + data_parts[1] = multisig.to_hex() + + transfer_data_parts[0] = arg_to_string(transfer_data_parts[0]) + data_parts.extend(transfer_data_parts) + + data_parts.append(arg_to_string(function)) + data_parts.extend(args_to_strings(arguments)) + + data_field = _build_data_payload(data_parts) + return data_field.encode() + + def _prepare_data_field_for_upgrade_transaction(contract_address: IAddress, amount: int, upgraded_contract: IAddress, @@ -231,7 +308,7 @@ def _prepare_data_field_for_deploy_transaction(amount: int, return payload.encode() -def _prepare_data_parts_for_multisig_transfer(receiver: Address, token_transfers: List[TokenTransfer]): +def _prepare_data_parts_for_multisig_transfer(receiver: IAddress, token_transfers: List[TokenTransfer]) -> List[str]: token_computer = TokenComputer() data_builder = TokenTransfersDataBuilder(token_computer) data_parts: List[str] = [] diff --git a/multiversx_sdk_cli/tests/test_cli_multisig.py b/multiversx_sdk_cli/tests/test_cli_multisig.py index 3431afd1..1d04de8c 100644 --- a/multiversx_sdk_cli/tests/test_cli_multisig.py +++ b/multiversx_sdk_cli/tests/test_cli_multisig.py @@ -357,6 +357,155 @@ def test_propose_contract_upgrade_from_source(capsys: Any): assert value == 0 +def test_propose_contract_call_no_transfer(capsys: Any): + return_code = main([ + "contract", "call", "erd1qqqqqqqqqqqqqpgq8z2zzyu30f4607hth0tfj5m3vpjvwrvvrawqw09jem", + "--pem", str(alice), + "--nonce", "9550", + "--chain", "T", + "--gas-limit", "100000000", + "--function", "add", + "--arguments", "10", + "--multisig", "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + ]) + assert False if return_code else True + + transaction = get_transaction(capsys) + + data_field: str = transaction["data"] + data = base64.b64decode(data_field.encode()).decode() + assert data == "proposeAsyncCall@0000000000000000050038942113917a6ba7faebbbd69953716064c70d8c1f5c@@616464@0a" + + receiver = transaction["receiver"] + assert receiver == "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + + chain_id = transaction["chainID"] + assert chain_id == "T" + + value = int(transaction["value"]) + assert value == 0 + + +def test_propose_contract_call_with_egld_transfer(capsys: Any): + return_code = main([ + "contract", "call", "erd1qqqqqqqqqqqqqpgq8z2zzyu30f4607hth0tfj5m3vpjvwrvvrawqw09jem", + "--pem", str(alice), + "--nonce", "9552", + "--chain", "T", + "--value", "1000000000000000", + "--gas-limit", "100000000", + "--function", "add", + "--arguments", "10", + "--multisig", "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + ]) + assert False if return_code else True + + transaction = get_transaction(capsys) + + data_field: str = transaction["data"] + data = base64.b64decode(data_field.encode()).decode() + assert data == "proposeAsyncCall@0000000000000000050038942113917a6ba7faebbbd69953716064c70d8c1f5c@038d7ea4c68000@616464@0a" + + receiver = transaction["receiver"] + assert receiver == "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + + chain_id = transaction["chainID"] + assert chain_id == "T" + + value = int(transaction["value"]) + assert value == 0 + + +def test_propose_contract_call_with_esdt_transfer(capsys: Any): + return_code = main([ + "contract", "call", "erd1qqqqqqqqqqqqqpgq8z2zzyu30f4607hth0tfj5m3vpjvwrvvrawqw09jem", + "--pem", str(alice), + "--nonce", "9553", + "--chain", "T", + "--token-transfers", "ZZZ-9ee87d", "1000", + "--gas-limit", "100000000", + "--function", "add", + "--arguments", "10", + "--multisig", "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + ]) + assert False if return_code else True + + transaction = get_transaction(capsys) + + data_field: str = transaction["data"] + data = base64.b64decode(data_field.encode()).decode() + assert data == "proposeAsyncCall@0000000000000000050038942113917a6ba7faebbbd69953716064c70d8c1f5c@@455344545472616e73666572@5a5a5a2d396565383764@03e8@616464@0a" + + receiver = transaction["receiver"] + assert receiver == "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + + chain_id = transaction["chainID"] + assert chain_id == "T" + + value = int(transaction["value"]) + assert value == 0 + + +def test_propose_contract_call_with_multi_esdt_transfer(capsys: Any): + return_code = main([ + "contract", "call", "erd1qqqqqqqqqqqqqpgq8z2zzyu30f4607hth0tfj5m3vpjvwrvvrawqw09jem", + "--pem", str(alice), + "--nonce", "9554", + "--chain", "T", + "--token-transfers", "ZZZ-9ee87d", "1300", "TST-267761", "600", + "--gas-limit", "100000000", + "--function", "add", + "--arguments", "10", + "--multisig", "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + ]) + assert False if return_code else True + + transaction = get_transaction(capsys) + + data_field: str = transaction["data"] + data = base64.b64decode(data_field.encode()).decode() + assert data == "proposeAsyncCall@000000000000000005000a2a0f13340978c2eea268a5a2dcf917012978f61f5c@@4d756c7469455344544e46545472616e73666572@0000000000000000050038942113917a6ba7faebbbd69953716064c70d8c1f5c@02@5a5a5a2d396565383764@@0514@5453542d323637373631@@0258@616464@0a" + + receiver = transaction["receiver"] + assert receiver == "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + + chain_id = transaction["chainID"] + assert chain_id == "T" + + value = int(transaction["value"]) + assert value == 0 + + +def test_propose_contract_call_with_multi_esdt_nft_transfer(capsys: Any): + return_code = main([ + "contract", "call", "erd1qqqqqqqqqqqqqpgq8z2zzyu30f4607hth0tfj5m3vpjvwrvvrawqw09jem", + "--pem", str(alice), + "--nonce", "9555", + "--chain", "T", + "--token-transfers", "ZZZ-9ee87d", "700", "METATEST-e05d11-01", "1500", + "--gas-limit", "100000000", + "--function", "add", + "--arguments", "10", + "--multisig", "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + ]) + assert False if return_code else True + + transaction = get_transaction(capsys) + + data_field: str = transaction["data"] + data = base64.b64decode(data_field.encode()).decode() + assert data == "proposeAsyncCall@000000000000000005000a2a0f13340978c2eea268a5a2dcf917012978f61f5c@@4d756c7469455344544e46545472616e73666572@0000000000000000050038942113917a6ba7faebbbd69953716064c70d8c1f5c@02@5a5a5a2d396565383764@@02bc@4d455441544553542d653035643131@01@05dc@616464@0a" + + receiver = transaction["receiver"] + assert receiver == "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + + chain_id = transaction["chainID"] + assert chain_id == "T" + + value = int(transaction["value"]) + assert value == 0 + + def get_transaction(capsys: Any) -> Dict[str, Any]: out = _read_stdout(capsys) output = json.loads(out) From ece79926612ffe5b464ab8fc0d014cb5bf94bbf0 Mon Sep 17 00:00:00 2001 From: Alexandru Popenta Date: Wed, 17 Jan 2024 12:22:03 +0200 Subject: [PATCH 9/9] add support for more multisig operations --- multiversx_sdk_cli/cli_multisig.py | 219 +++++++++++++++++- multiversx_sdk_cli/tests/test_cli_multisig.py | 112 +++++++++ 2 files changed, 329 insertions(+), 2 deletions(-) diff --git a/multiversx_sdk_cli/cli_multisig.py b/multiversx_sdk_cli/cli_multisig.py index 5ffd3c75..bbdf8420 100644 --- a/multiversx_sdk_cli/cli_multisig.py +++ b/multiversx_sdk_cli/cli_multisig.py @@ -19,6 +19,10 @@ MULTISIG_UNSIGN_ACTION_FUNCTION = "unsign" MULTISIG_PERFORM_ACTION_FUNCTION = "performAction" MULTISIG_DISCARD_ACTION_FUNCTION = "discardAction" +MULTISIG_ADD_NEW_BOARD_MEMBER = "proposeAddBoardMember" +MULTISIG_ADD_PROPOSER = "proposeAddProposer" +MULTISIG_REMOVE_USER = "proposeRemoveUser" +MULTISIG_CHAGE_QUORUM = "proposeChangeQuorum" def setup_parser(args: List[str], subparsers: Any) -> Any: @@ -110,10 +114,86 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: " - only valid if --wait-result is set") sub.set_defaults(func=deposit_funds) + sub = cli_shared.add_command_subparser(subparsers, "multisig", "add-board-member", f"Propose an action to add a new board member.") + cli_shared.add_multisig_address_arg(sub) + _add_member_arg(sub) + cli_shared.add_multisig_view_address_arg(sub) + cli_shared.add_wallet_args(args, sub) + cli_shared.add_tx_args(args, sub, with_receiver=False, with_data=False, with_guardian=True) + + cli_shared.add_outfile_arg(sub, what="signed transaction, hash") + cli_shared.add_broadcast_args(sub, relay=True) + cli_shared.add_proxy_arg(sub) + cli_shared.add_guardian_wallet_args(args, sub) + sub.add_argument("--wait-result", action="store_true", default=False, + help="signal to wait for the transaction result - only valid if --send is set") + sub.add_argument("--timeout", default=100, help="max num of seconds to wait for result" + " - only valid if --wait-result is set") + sub.set_defaults(func=propose_add_board_member) + + sub = cli_shared.add_command_subparser(subparsers, "multisig", "add-proposer", f"Propose an action to add a new proposer.") + cli_shared.add_multisig_address_arg(sub) + _add_member_arg(sub) + cli_shared.add_multisig_view_address_arg(sub) + cli_shared.add_wallet_args(args, sub) + cli_shared.add_tx_args(args, sub, with_receiver=False, with_data=False, with_guardian=True) + + cli_shared.add_outfile_arg(sub, what="signed transaction, hash") + cli_shared.add_broadcast_args(sub, relay=True) + cli_shared.add_proxy_arg(sub) + cli_shared.add_guardian_wallet_args(args, sub) + sub.add_argument("--wait-result", action="store_true", default=False, + help="signal to wait for the transaction result - only valid if --send is set") + sub.add_argument("--timeout", default=100, help="max num of seconds to wait for result" + " - only valid if --wait-result is set") + sub.set_defaults(func=propose_add_proposer) + + sub = cli_shared.add_command_subparser(subparsers, "multisig", "remove-user", f"Propose an action to remove an user.") + cli_shared.add_multisig_address_arg(sub) + _add_member_arg(sub) + cli_shared.add_multisig_view_address_arg(sub) + cli_shared.add_wallet_args(args, sub) + cli_shared.add_tx_args(args, sub, with_receiver=False, with_data=False, with_guardian=True) + + cli_shared.add_outfile_arg(sub, what="signed transaction, hash") + cli_shared.add_broadcast_args(sub, relay=True) + cli_shared.add_proxy_arg(sub) + cli_shared.add_guardian_wallet_args(args, sub) + sub.add_argument("--wait-result", action="store_true", default=False, + help="signal to wait for the transaction result - only valid if --send is set") + sub.add_argument("--timeout", default=100, help="max num of seconds to wait for result" + " - only valid if --wait-result is set") + sub.set_defaults(func=propose_remove_user) + + sub = cli_shared.add_command_subparser(subparsers, "multisig", "quorum", f"Propose an action to change the quorum size.") + cli_shared.add_multisig_address_arg(sub) + _add_quorum_arg(sub) + cli_shared.add_multisig_view_address_arg(sub) + cli_shared.add_wallet_args(args, sub) + cli_shared.add_tx_args(args, sub, with_receiver=False, with_data=False, with_guardian=True) + + cli_shared.add_outfile_arg(sub, what="signed transaction, hash") + cli_shared.add_broadcast_args(sub, relay=True) + cli_shared.add_proxy_arg(sub) + cli_shared.add_guardian_wallet_args(args, sub) + sub.add_argument("--wait-result", action="store_true", default=False, + help="signal to wait for the transaction result - only valid if --send is set") + sub.add_argument("--timeout", default=100, help="max num of seconds to wait for result" + " - only valid if --wait-result is set") + sub.set_defaults(func=propose_change_quorum_size) + parser.epilog = cli_shared.build_group_epilog(subparsers) return subparsers +def _add_member_arg(sub: Any): + sub.add_argument("--proposed-member", required=True, help="the address of the proposed member") + + +def _add_quorum_arg(sub: Any): + sub.add_argument("--quorum-size", required=True, help="the proposed quorum size") + + def sign_action(args: Any): cli_shared.check_guardian_and_options_args(args) cli_shared.check_broadcast_args(args) @@ -288,8 +368,143 @@ def deposit_funds(args: Any): nonce=int(args.nonce), version=int(args.version), options=int(args.options), - guardian=args.guardian - ) + guardian=args.guardian) + + if tx.guardian: + tx = sign_tx_by_guardian(args, tx) + + if hasattr(args, "relay") and args.relay: + args.outfile.write(compute_relayed_v1_data(tx)) + return + + cli_shared.send_or_simulate(tx, args) + + +def propose_add_board_member(args: Any): + cli_shared.check_guardian_and_options_args(args) + cli_shared.check_broadcast_args(args) + cli_shared.prepare_chain_id_in_args(args) + cli_shared.prepare_nonce_in_args(args) + + sender = cli_shared.prepare_account(args) + + config = TransactionsFactoryConfig(args.chain) + contract = SmartContract(config) + + tx = contract.prepare_execute_transaction( + caller=sender, + contract=Address.new_from_bech32(args.multisig), + function=MULTISIG_ADD_NEW_BOARD_MEMBER, + arguments=[args.proposed_member], + gas_limit=int(args.gas_limit), + value=int(args.value), + transfers=None, + nonce=int(args.nonce), + version=int(args.version), + options=int(args.options), + guardian=args.guardian) + + if tx.guardian: + tx = sign_tx_by_guardian(args, tx) + + if hasattr(args, "relay") and args.relay: + args.outfile.write(compute_relayed_v1_data(tx)) + return + + cli_shared.send_or_simulate(tx, args) + + +def propose_add_proposer(args: Any): + cli_shared.check_guardian_and_options_args(args) + cli_shared.check_broadcast_args(args) + cli_shared.prepare_chain_id_in_args(args) + cli_shared.prepare_nonce_in_args(args) + + sender = cli_shared.prepare_account(args) + + config = TransactionsFactoryConfig(args.chain) + contract = SmartContract(config) + + tx = contract.prepare_execute_transaction( + caller=sender, + contract=Address.new_from_bech32(args.multisig), + function=MULTISIG_ADD_PROPOSER, + arguments=[args.proposed_member], + gas_limit=int(args.gas_limit), + value=int(args.value), + transfers=None, + nonce=int(args.nonce), + version=int(args.version), + options=int(args.options), + guardian=args.guardian) + + if tx.guardian: + tx = sign_tx_by_guardian(args, tx) + + if hasattr(args, "relay") and args.relay: + args.outfile.write(compute_relayed_v1_data(tx)) + return + + cli_shared.send_or_simulate(tx, args) + + +def propose_remove_user(args: Any): + cli_shared.check_guardian_and_options_args(args) + cli_shared.check_broadcast_args(args) + cli_shared.prepare_chain_id_in_args(args) + cli_shared.prepare_nonce_in_args(args) + + sender = cli_shared.prepare_account(args) + + config = TransactionsFactoryConfig(args.chain) + contract = SmartContract(config) + + tx = contract.prepare_execute_transaction( + caller=sender, + contract=Address.new_from_bech32(args.multisig), + function=MULTISIG_REMOVE_USER, + arguments=[args.proposed_member], + gas_limit=int(args.gas_limit), + value=int(args.value), + transfers=None, + nonce=int(args.nonce), + version=int(args.version), + options=int(args.options), + guardian=args.guardian) + + if tx.guardian: + tx = sign_tx_by_guardian(args, tx) + + if hasattr(args, "relay") and args.relay: + args.outfile.write(compute_relayed_v1_data(tx)) + return + + cli_shared.send_or_simulate(tx, args) + + +def propose_change_quorum_size(args: Any): + cli_shared.check_guardian_and_options_args(args) + cli_shared.check_broadcast_args(args) + cli_shared.prepare_chain_id_in_args(args) + cli_shared.prepare_nonce_in_args(args) + + sender = cli_shared.prepare_account(args) + + config = TransactionsFactoryConfig(args.chain) + contract = SmartContract(config) + + tx = contract.prepare_execute_transaction( + caller=sender, + contract=Address.new_from_bech32(args.multisig), + function=MULTISIG_CHAGE_QUORUM, + arguments=[args.quorum_size], + gas_limit=int(args.gas_limit), + value=int(args.value), + transfers=None, + nonce=int(args.nonce), + version=int(args.version), + options=int(args.options), + guardian=args.guardian) if tx.guardian: tx = sign_tx_by_guardian(args, tx) diff --git a/multiversx_sdk_cli/tests/test_cli_multisig.py b/multiversx_sdk_cli/tests/test_cli_multisig.py index 1d04de8c..3814b595 100644 --- a/multiversx_sdk_cli/tests/test_cli_multisig.py +++ b/multiversx_sdk_cli/tests/test_cli_multisig.py @@ -506,6 +506,118 @@ def test_propose_contract_call_with_multi_esdt_nft_transfer(capsys: Any): assert value == 0 +def test_propose_add_board_member(capsys: Any): + return_code = main([ + "multisig", "add-board-member", + "--multisig", "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30", + "--proposed-member", "erd1fggp5ru0jhcjrp5rjqyqrnvhr3sz3v2e0fm3ktknvlg7mcyan54qzccnan", + "--pem", str(alice), + "--nonce", "12243", + "--chain", "T", + "--gas-limit", "10000000" + ]) + assert False if return_code else True + + transaction = get_transaction(capsys) + + data_field: str = transaction["data"] + data = base64.b64decode(data_field.encode()).decode() + assert data == "proposeAddBoardMember@4a101a0f8f95f1218683900801cd971c6028b1597a771b2ed367d1ede09d9d2a" + + receiver = transaction["receiver"] + assert receiver == "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + + chain_id = transaction["chainID"] + assert chain_id == "T" + + value = int(transaction["value"]) + assert value == 0 + + +def test_propose_add_proposer(capsys: Any): + return_code = main([ + "multisig", "add-proposer", + "--multisig", "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30", + "--proposed-member", "erd1fggp5ru0jhcjrp5rjqyqrnvhr3sz3v2e0fm3ktknvlg7mcyan54qzccnan", + "--pem", str(alice), + "--nonce", "12244", + "--chain", "T", + "--gas-limit", "10000000" + ]) + assert False if return_code else True + + transaction = get_transaction(capsys) + + data_field: str = transaction["data"] + data = base64.b64decode(data_field.encode()).decode() + assert data == "proposeAddProposer@4a101a0f8f95f1218683900801cd971c6028b1597a771b2ed367d1ede09d9d2a" + + receiver = transaction["receiver"] + assert receiver == "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + + chain_id = transaction["chainID"] + assert chain_id == "T" + + value = int(transaction["value"]) + assert value == 0 + + +def test_propose_remove_user(capsys: Any): + return_code = main([ + "multisig", "remove-user", + "--multisig", "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30", + "--proposed-member", "erd1fggp5ru0jhcjrp5rjqyqrnvhr3sz3v2e0fm3ktknvlg7mcyan54qzccnan", + "--pem", str(alice), + "--nonce", "12245", + "--chain", "T", + "--gas-limit", "10000000" + ]) + assert False if return_code else True + + transaction = get_transaction(capsys) + + data_field: str = transaction["data"] + data = base64.b64decode(data_field.encode()).decode() + assert data == "proposeRemoveUser@4a101a0f8f95f1218683900801cd971c6028b1597a771b2ed367d1ede09d9d2a" + + receiver = transaction["receiver"] + assert receiver == "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + + chain_id = transaction["chainID"] + assert chain_id == "T" + + value = int(transaction["value"]) + assert value == 0 + + +def test_propose_change_quorum_size(capsys: Any): + return_code = main([ + "multisig", "quorum", + "--multisig", "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30", + "--quorum-size", "2", + "--pem", str(alice), + "--nonce", "12247", + "--chain", "T", + "--gas-limit", "10000000" + ]) + assert False if return_code else True + + transaction = get_transaction(capsys) + + data_field: str = transaction["data"] + data = base64.b64decode(data_field.encode()).decode() + assert data == "proposeChangeQuorum@02" + + receiver = transaction["receiver"] + assert receiver == "erd1qqqqqqqqqqqqqpgqpg4q7ye5p9uv9m4zdzj69h8ezuqjj78krawq9zqz30" + + chain_id = transaction["chainID"] + assert chain_id == "T" + + value = int(transaction["value"]) + assert value == 0 + + def get_transaction(capsys: Any) -> Dict[str, Any]: out = _read_stdout(capsys) output = json.loads(out)