diff --git a/multiversx_sdk_cli/cli_shared.py b/multiversx_sdk_cli/cli_shared.py index ca4e8e3c..5ce01dde 100644 --- a/multiversx_sdk_cli/cli_shared.py +++ b/multiversx_sdk_cli/cli_shared.py @@ -127,12 +127,13 @@ def add_guardian_wallet_args(args: List[str], sub: Any): sub.add_argument("--guardian-ledger-address-index", type=int, default=0, help="🔐 the index of the address when using Ledger") +# Required check not properly working, same for guardian. Will be refactored in the future. def add_relayed_v3_wallet_args(args: List[str], sub: Any): - sub.add_argument("--relayer-pem", required=check_if_sign_method_required(args, "--relayer-pem"), help="🔑 the PEM file, if keyfile not provided") + sub.add_argument("--relayer-pem", help="🔑 the PEM file, if keyfile not provided") sub.add_argument("--relayer-pem-index", type=int, default=0, help="🔑 the index in the PEM file (default: %(default)s)") - sub.add_argument("--relayer-keyfile", required=check_if_sign_method_required(args, "--relayer-keyfile"), help="🔑 a JSON keyfile, if PEM not provided") + sub.add_argument("--relayer-keyfile", help="🔑 a JSON keyfile, if PEM not provided") sub.add_argument("--relayer-passfile", help="🔑 a file containing keyfile's password, if keyfile provided") - sub.add_argument("--relayer-ledger", action="store_true", required=check_if_sign_method_required(args, "--relayer-ledger"), default=False, help="🔐 bool flag for signing transaction using ledger") + sub.add_argument("--relayer-ledger", action="store_true", default=False, help="🔐 bool flag for signing transaction using ledger") sub.add_argument("--relayer-ledger-account-index", type=int, default=0, help="🔐 the index of the account when using Ledger") sub.add_argument("--relayer-ledger-address-index", type=int, default=0, help="🔐 the index of the address when using Ledger") @@ -181,6 +182,20 @@ def prepare_account(args: Any): return account +def prepare_relayer_account(args: Any) -> Account: + if args.relayer_ledger: + account = LedgerAccount(account_index=args.relayer_ledger_account_index, address_index=args.relayer_ledger_address_index) + if args.relayer_pem: + account = Account(pem_file=args.relayer_pem, pem_index=args.relayer_pem_index) + elif args.relayer_keyfile: + password = load_password(args) + account = Account(key_file=args.relayer_keyfile, password=password) + else: + raise errors.NoWalletProvided() + + return account + + def prepare_guardian_account(args: Any): if args.guardian_pem: account = Account(pem_file=args.guardian_pem, pem_index=args.guardian_pem_index) diff --git a/multiversx_sdk_cli/cli_transactions.py b/multiversx_sdk_cli/cli_transactions.py index d8a8ae1d..b9dc2bd6 100644 --- a/multiversx_sdk_cli/cli_transactions.py +++ b/multiversx_sdk_cli/cli_transactions.py @@ -8,7 +8,7 @@ from multiversx_sdk_cli.cli_output import CLIOutputBuilder from multiversx_sdk_cli.config import get_config_for_network_providers from multiversx_sdk_cli.cosign_transaction import cosign_transaction -from multiversx_sdk_cli.errors import NoWalletProvided +from multiversx_sdk_cli.errors import IncorrectWalletError, NoWalletProvided from multiversx_sdk_cli.transactions import (compute_relayed_v1_data, do_prepare_transaction, load_transaction_from_file) @@ -57,6 +57,14 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: cli_shared.add_guardian_wallet_args(args, sub) sub.set_defaults(func=sign_transaction) + sub = cli_shared.add_command_subparser(subparsers, "tx", "relay", f"Relay a previously saved transaction.{CLIOutputBuilder.describe()}") + cli_shared.add_relayed_v3_wallet_args(args, sub) + cli_shared.add_infile_arg(sub, what="a previously saved transaction") + cli_shared.add_outfile_arg(sub, what="the signed transaction") + cli_shared.add_broadcast_args(sub) + cli_shared.add_proxy_arg(sub) + sub.set_defaults(func=relay_transaction) + parser.epilog = cli_shared.build_group_epilog(subparsers) return subparsers @@ -141,3 +149,26 @@ def sign_transaction(args: Any): tx = cosign_transaction(tx, args.guardian_service_url, args.guardian_2fa_code) cli_shared.send_or_simulate(tx, args) + + +def relay_transaction(args: Any): + args = utils.as_object(args) + + if not _is_relayer_wallet_provided(args): + raise NoWalletProvided() + + cli_shared.check_broadcast_args(args) + + tx = load_transaction_from_file(args.infile) + relayer = cli_shared.prepare_relayer_account(args) + + if tx.relayer != relayer.address.to_bech32(): + raise IncorrectWalletError("Relayer wallet does not match the relayer's address set in the transaction.") + + tx.relayer_signature = bytes.fromhex(relayer.sign_transaction(tx)) + + cli_shared.send_or_simulate(tx, args) + + +def _is_relayer_wallet_provided(args: Any): + return any([args.relayer_pem, args.relayer_keyfile, args.relayer_ledger]) diff --git a/multiversx_sdk_cli/errors.py b/multiversx_sdk_cli/errors.py index b5cb32b4..2164ad6d 100644 --- a/multiversx_sdk_cli/errors.py +++ b/multiversx_sdk_cli/errors.py @@ -182,3 +182,8 @@ def __init__(self, message: str, inner: Any = None): class NativeAuthClientError(KnownError): def __init__(self, message: str): super().__init__(message) + + +class IncorrectWalletError(KnownError): + def __init__(self, message: str): + super().__init__(message) diff --git a/multiversx_sdk_cli/tests/test_cli_transactions.py b/multiversx_sdk_cli/tests/test_cli_transactions.py index 6509b7db..b7bcd4a3 100644 --- a/multiversx_sdk_cli/tests/test_cli_transactions.py +++ b/multiversx_sdk_cli/tests/test_cli_transactions.py @@ -2,8 +2,6 @@ from pathlib import Path from typing import Any -import pytest - from multiversx_sdk_cli.cli import main testdata_path = Path(__file__).parent / "testdata" @@ -107,7 +105,46 @@ def test_create_multi_transfer_transaction_with_single_egld_transfer(capsys: Any assert data == "MultiESDTNFTTransfer@8049d639e5a6980d1cd2392abcce41029cda74a1563523a202f09641cc2618f8@01@45474c442d303030303030@@0de0b6b3a7640000" +def test_relayed_v3_without_relayer_wallet(capsys: Any): + return_code = main([ + "tx", "new", + "--pem", str(testdata_path / "alice.pem"), + "--receiver", "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx", + "--nonce", "7", + "--gas-limit", "1300000", + "--value", "1000000000000000000", + "--chain", "T", + "--relayer", "erd1cqqxak4wun7508e0yj9ng843r6hv4mzd0hhpjpsejkpn9wa9yq8sj7u2u5" + ]) + assert return_code == 0 + tx = _read_stdout(capsys) + tx_json = json.loads(tx)["emittedTransaction"] + assert tx_json["sender"] == "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th" + assert tx_json["receiver"] == "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx" + assert tx_json["relayer"] == "erd1cqqxak4wun7508e0yj9ng843r6hv4mzd0hhpjpsejkpn9wa9yq8sj7u2u5" + assert tx_json["signature"] + assert not tx_json["relayerSignature"] + + +def test_relayed_v3_incorrect_relayer(): + return_code = main([ + "tx", "new", + "--pem", str(testdata_path / "alice.pem"), + "--receiver", "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx", + "--nonce", "7", + "--gas-limit", "1300000", + "--value", "1000000000000000000", + "--chain", "T", + "--relayer", "erd1cqqxak4wun7508e0yj9ng843r6hv4mzd0hhpjpsejkpn9wa9yq8sj7u2u5", + "--relayer-pem", str(testdata_path / "alice.pem") + ]) + assert return_code + + def test_create_relayed_v3_transaction(capsys: Any): + # create relayed v3 tx and save signature and relayer signature + # create the same tx, save to file + # sign from file with relayer wallet and make sure signatures match return_code = main([ "tx", "new", "--pem", str(testdata_path / "alice.pem"), @@ -129,7 +166,13 @@ def test_create_relayed_v3_transaction(capsys: Any): assert tx_json["signature"] assert tx_json["relayerSignature"] - # no relayer wallet provided + initial_sender_signature = tx_json["signature"] + initial_relayer_signature = tx_json["relayerSignature"] + + # Clear the captured content + capsys.readouterr() + + # save tx to file then load and sign tx by relayer return_code = main([ "tx", "new", "--pem", str(testdata_path / "alice.pem"), @@ -138,30 +181,36 @@ def test_create_relayed_v3_transaction(capsys: Any): "--gas-limit", "1300000", "--value", "1000000000000000000", "--chain", "T", - "--relayer", "erd1cqqxak4wun7508e0yj9ng843r6hv4mzd0hhpjpsejkpn9wa9yq8sj7u2u5" + "--relayer", "erd1cqqxak4wun7508e0yj9ng843r6hv4mzd0hhpjpsejkpn9wa9yq8sj7u2u5", + "--outfile", str(testdata_out / "relayed.json") + ]) + assert return_code == 0 + + # Clear the captured content + capsys.readouterr() + + return_code = main([ + "tx", "relay", + "--relayer-pem", str(testdata_path / "testUser.pem"), + "--infile", str(testdata_out / "relayed.json") ]) assert return_code == 0 + tx = _read_stdout(capsys) tx_json = json.loads(tx)["emittedTransaction"] - assert tx_json["sender"] == "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th" - assert tx_json["receiver"] == "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx" - assert tx_json["relayer"] == "erd1cqqxak4wun7508e0yj9ng843r6hv4mzd0hhpjpsejkpn9wa9yq8sj7u2u5" - assert tx_json["signature"] - assert not tx_json["relayerSignature"] + assert tx_json["signature"] == initial_sender_signature + assert tx_json["relayerSignature"] == initial_relayer_signature + + # Clear the captured content + capsys.readouterr() - # incorrect relayer wallet - with pytest.raises(Exception, match="Relayer address does not match the provided relayer wallet."): - main([ - "tx", "new", - "--pem", str(testdata_path / "alice.pem"), - "--receiver", "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx", - "--nonce", "7", - "--gas-limit", "1300000", - "--value", "1000000000000000000", - "--chain", "T", - "--relayer", "erd1cqqxak4wun7508e0yj9ng843r6hv4mzd0hhpjpsejkpn9wa9yq8sj7u2u5", - "--relayer-pem", str(testdata_path / "alice.pem") - ]) + +def test_check_relayer_wallet_is_provided(): + return_code = main([ + "tx", "relay", + "--infile", str(testdata_out / "relayed.json") + ]) + assert return_code def _read_stdout(capsys: Any) -> str: diff --git a/multiversx_sdk_cli/transactions.py b/multiversx_sdk_cli/transactions.py index b1ae6a42..96536228 100644 --- a/multiversx_sdk_cli/transactions.py +++ b/multiversx_sdk_cli/transactions.py @@ -14,7 +14,7 @@ from multiversx_sdk_cli.cli_password import (load_guardian_password, load_password) from multiversx_sdk_cli.cosign_transaction import cosign_transaction -from multiversx_sdk_cli.errors import NoWalletProvided +from multiversx_sdk_cli.errors import IncorrectWalletError, NoWalletProvided from multiversx_sdk_cli.interfaces import ITransaction from multiversx_sdk_cli.ledger.ledger_functions import do_get_ledger_address @@ -84,11 +84,13 @@ def do_prepare_transaction(args: Any) -> Transaction: try: relayer_account = load_relayer_account_from_args(args) if relayer_account.address.to_bech32() != tx.relayer: - raise Exception("Relayer address does not match the provided relayer wallet.") + raise IncorrectWalletError("") tx.relayer_signature = bytes.fromhex(relayer_account.sign_transaction(tx)) - except errors.NoWalletProvided: + except NoWalletProvided: logger.warning("Relayer wallet not provided. Transaction will not be signed by relayer.") + except IncorrectWalletError: + raise IncorrectWalletError("Relayer wallet does not match the relayer's address set in the transaction.") tx.signature = bytes.fromhex(account.sign_transaction(tx)) tx = sign_tx_by_guardian(args, tx)