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)