diff --git a/multiversx_sdk_cli/cli_contracts.py b/multiversx_sdk_cli/cli_contracts.py index b9a76122..1ff316fb 100644 --- a/multiversx_sdk_cli/cli_contracts.py +++ b/multiversx_sdk_cli/cli_contracts.py @@ -79,7 +79,7 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: cli_shared.add_outfile_arg(sub) cli_shared.add_wallet_args(args, sub) cli_shared.add_proxy_arg(sub) - cli_shared.add_tx_args(args, sub, with_receiver=False, with_data=False, with_guardian=True) + cli_shared.add_tx_args(args, sub, with_receiver=False, with_data=False) _add_arguments_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") @@ -97,7 +97,7 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: cli_shared.add_outfile_arg(sub) cli_shared.add_wallet_args(args, sub) cli_shared.add_proxy_arg(sub) - cli_shared.add_tx_args(args, sub, with_receiver=False, with_data=False, with_guardian=True) + cli_shared.add_tx_args(args, sub, with_receiver=False, with_data=False) _add_function_arg(sub) _add_arguments_arg(sub) _add_token_transfers_args(sub) @@ -119,7 +119,7 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: _add_metadata_arg(sub) cli_shared.add_wallet_args(args, sub) cli_shared.add_proxy_arg(sub) - cli_shared.add_tx_args(args, sub, with_receiver=False, with_data=False, with_guardian=True) + cli_shared.add_tx_args(args, sub, with_receiver=False, with_data=False) _add_arguments_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") diff --git a/multiversx_sdk_cli/cli_delegation.py b/multiversx_sdk_cli/cli_delegation.py index eef8a357..c341a850 100644 --- a/multiversx_sdk_cli/cli_delegation.py +++ b/multiversx_sdk_cli/cli_delegation.py @@ -182,7 +182,7 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: def _add_common_arguments(args: List[str], sub: Any): cli_shared.add_proxy_arg(sub) cli_shared.add_wallet_args(args, sub) - cli_shared.add_tx_args(args, sub, with_receiver=False, with_data=False, with_estimate_gas=True, with_guardian=True) + cli_shared.add_tx_args(args, sub, with_receiver=False, with_data=False, with_estimate_gas=True) cli_shared.add_broadcast_args(sub, relay=False) cli_shared.add_outfile_arg(sub, what="signed transaction, hash") cli_shared.add_guardian_wallet_args(args, sub) diff --git a/multiversx_sdk_cli/cli_dns.py b/multiversx_sdk_cli/cli_dns.py index 1e87358e..80e93747 100644 --- a/multiversx_sdk_cli/cli_dns.py +++ b/multiversx_sdk_cli/cli_dns.py @@ -21,7 +21,7 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: cli_shared.add_broadcast_args(sub, relay=True) cli_shared.add_wallet_args(args, sub) cli_shared.add_proxy_arg(sub) - cli_shared.add_tx_args(args, sub, with_receiver=False, with_data=False, with_guardian=True) + cli_shared.add_tx_args(args, sub, with_receiver=False, with_data=False) cli_shared.add_guardian_wallet_args(args, sub) sub.add_argument("--name", help="the name to register") sub.set_defaults(func=register) diff --git a/multiversx_sdk_cli/cli_shared.py b/multiversx_sdk_cli/cli_shared.py index 10ba00fe..867c2e1f 100644 --- a/multiversx_sdk_cli/cli_shared.py +++ b/multiversx_sdk_cli/cli_shared.py @@ -63,7 +63,13 @@ def add_command_subparser(subparsers: Any, group: str, command: str, description ) -def add_tx_args(args: List[str], sub: Any, with_nonce: bool = True, with_receiver: bool = True, with_data: bool = True, with_estimate_gas: bool = False, with_guardian: bool = False): +def add_tx_args( + args: List[str], + sub: Any, + with_nonce: bool = True, + with_receiver: bool = True, + with_data: bool = True, + with_estimate_gas: bool = False): if with_nonce: sub.add_argument("--nonce", type=int, required=not ("--recall-nonce" in args), help="# the nonce for the transaction") sub.add_argument("--recall-nonce", action="store_true", default=False, help="тно whether to recall the nonce when creating the transaction (default: %(default)s)") @@ -85,12 +91,18 @@ def add_tx_args(args: List[str], sub: Any, with_nonce: bool = True, with_receive sub.add_argument("--chain", help="the chain identifier") sub.add_argument("--version", type=int, default=DEFAULT_TX_VERSION, help="the transaction version (default: %(default)s)") - if with_guardian: - add_guardian_args(sub) + add_guardian_args(sub) + add_relayed_v3_args(sub) sub.add_argument("--options", type=int, default=0, help="the transaction options (default: 0)") +def add_relayed_v3_args(sub: Any): + sub.add_argument("--relayer", help="the address of the relayer") + sub.add_argument("--inner-transactions", help="a json file containing the inner transactions; should only be provided when creating the relayer's transaction") + sub.add_argument("--inner-transactions-outfile", type=str, help="where to save the transaction as an inner transaction (default: stdout)") + + def add_guardian_args(sub: Any): sub.add_argument("--guardian", type=str, help="the address of the guradian", default="") sub.add_argument("--guardian-service-url", type=str, help="the url of the guardian service", default="") diff --git a/multiversx_sdk_cli/cli_transactions.py b/multiversx_sdk_cli/cli_transactions.py index 0956f328..924e4c9d 100644 --- a/multiversx_sdk_cli/cli_transactions.py +++ b/multiversx_sdk_cli/cli_transactions.py @@ -1,15 +1,21 @@ +import logging from pathlib import Path -from typing import Any, List +from typing import Any, Dict, List + +from multiversx_sdk import Transaction, TransactionsConverter 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.custom_network_provider import CustomNetworkProvider -from multiversx_sdk_cli.errors import NoWalletProvided +from multiversx_sdk_cli.errors import BadUsage, NoWalletProvided from multiversx_sdk_cli.transactions import (compute_relayed_v1_data, do_prepare_transaction, + load_inner_transactions_from_file, load_transaction_from_file) +logger = logging.getLogger("cli.transactions") + def setup_parser(args: List[str], subparsers: Any) -> Any: parser = cli_shared.add_group_subparser(subparsers, "tx", "Create and broadcast Transactions") @@ -58,7 +64,7 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: def _add_common_arguments(args: List[str], sub: Any): cli_shared.add_wallet_args(args, sub) - cli_shared.add_tx_args(args, sub, with_guardian=True) + cli_shared.add_tx_args(args, sub) sub.add_argument("--data-file", type=str, default=None, help="a file containing transaction data") @@ -79,15 +85,46 @@ def create_transaction(args: Any): if args.data_file: args.data = Path(args.data_file).read_text() + check_relayer_transaction_with_data_field_for_relayed_v3(args) + tx = do_prepare_transaction(args) + if hasattr(args, "inner_transactions_outfile") and args.inner_transactions_outfile: + save_transaction_to_inner_transactions_file(tx, args) + return + if hasattr(args, "relay") and args.relay: + logger.warning("RelayedV1 transactions are deprecated. Please use RelayedV3 instead.") args.outfile.write(compute_relayed_v1_data(tx)) return cli_shared.send_or_simulate(tx, args) +def save_transaction_to_inner_transactions_file(transaction: Transaction, args: Any): + inner_txs_file = Path(args.inner_transactions_outfile).expanduser() + transactions = get_inner_transactions_if_any(inner_txs_file) + transactions.append(transaction) + + tx_converter = TransactionsConverter() + inner_transactions: Dict[str, Any] = {} + inner_transactions["innerTransactions"] = [tx_converter.transaction_to_dictionary(tx) for tx in transactions] + + with open(inner_txs_file, "w") as file: + utils.dump_out_json(inner_transactions, file) + + +def get_inner_transactions_if_any(file: Path) -> List[Transaction]: + if file.is_file(): + return load_inner_transactions_from_file(file) + return [] + + +def check_relayer_transaction_with_data_field_for_relayed_v3(args: Any): + if hasattr(args, "inner_transactions") and args.inner_transactions and args.data: + raise BadUsage("Can't set data field when creating a relayedV3 transaction") + + def send_transaction(args: Any): args = utils.as_object(args) diff --git a/multiversx_sdk_cli/cli_validators.py b/multiversx_sdk_cli/cli_validators.py index e2a73b54..106f403c 100644 --- a/multiversx_sdk_cli/cli_validators.py +++ b/multiversx_sdk_cli/cli_validators.py @@ -88,7 +88,7 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: def _add_common_arguments(args: List[str], sub: Any): cli_shared.add_proxy_arg(sub) cli_shared.add_wallet_args(args, sub) - cli_shared.add_tx_args(args, sub, with_receiver=False, with_data=False, with_estimate_gas=True, with_guardian=True) + cli_shared.add_tx_args(args, sub, with_receiver=False, with_data=False, with_estimate_gas=True) cli_shared.add_broadcast_args(sub, relay=False) cli_shared.add_outfile_arg(sub, what="signed transaction, hash") cli_shared.add_guardian_wallet_args(args, sub) diff --git a/multiversx_sdk_cli/interfaces.py b/multiversx_sdk_cli/interfaces.py index e59c3c8f..e0db9a2f 100644 --- a/multiversx_sdk_cli/interfaces.py +++ b/multiversx_sdk_cli/interfaces.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, Protocol +from typing import Any, Dict, Protocol, Sequence class IAddress(Protocol): @@ -25,6 +25,11 @@ class ITransaction(Protocol): guardian: str signature: bytes guardian_signature: bytes + relayer: str + + @property + def inner_transactions(self) -> Sequence["ITransaction"]: + ... class IAccount(Protocol): diff --git a/multiversx_sdk_cli/tests/test_cli_transactions.py b/multiversx_sdk_cli/tests/test_cli_transactions.py index 1d1b4220..c8148ae3 100644 --- a/multiversx_sdk_cli/tests/test_cli_transactions.py +++ b/multiversx_sdk_cli/tests/test_cli_transactions.py @@ -1,10 +1,12 @@ import json +import os from pathlib import Path -from typing import Any +from typing import Any, List from multiversx_sdk_cli.cli import main testdata_path = Path(__file__).parent / "testdata" +testdata_out = Path(__file__).parent / "testdata-out" def test_relayed_v1_transaction(capsys: Any): @@ -87,5 +89,93 @@ def test_create_multi_transfer_transaction(capsys: Any): assert signature == "575b029d52ff5ffbfb7bab2f04052de88a6f7d022a6ad368459b8af9acaed3717d3f95db09f460649a8f405800838bc2c432496bd03c9039ea166bd32b84660e" +def test_create_and_save_inner_transaction(): + return_code = main([ + "tx", "new", + "--pem", str(testdata_path / "alice.pem"), + "--receiver", "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx", + "--nonce", "77", + "--gas-limit", "500000", + "--relayer", "erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8", + "--inner-transactions-outfile", str(testdata_out / "inner_transactions.json"), + "--chain", "T", + ]) + assert False if return_code else True + assert Path(testdata_out / "inner_transactions.json").is_file() + + +def test_create_and_append_inner_transaction(): + return_code = main([ + "tx", "new", + "--pem", str(testdata_path / "alice.pem"), + "--receiver", "erd1fggp5ru0jhcjrp5rjqyqrnvhr3sz3v2e0fm3ktknvlg7mcyan54qzccnan", + "--nonce", "1234", + "--gas-limit", "50000", + "--relayer", "erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8", + "--inner-transactions-outfile", str(testdata_out / "inner_transactions.json"), + "--chain", "T", + ]) + assert False if return_code else True + + with open(testdata_out / "inner_transactions.json", "r") as file: + json_file = json.load(file) + + inner_txs: List[Any] = json_file["innerTransactions"] + assert len(inner_txs) == 2 + + +def test_create_invalid_relayed_transaction(): + return_code = main([ + "tx", "new", + "--pem", str(testdata_path / "testUser.pem"), + "--receiver", "erd1cqqxak4wun7508e0yj9ng843r6hv4mzd0hhpjpsejkpn9wa9yq8sj7u2u5", + "--nonce", "987", + "--gas-limit", "5000000", + "--inner-transactions", str(testdata_out / "inner_transactions.json"), + "--data", "test data", + "--chain", "T", + ]) + assert return_code + + +def test_create_relayer_transaction(capsys: Any): + return_code = main([ + "tx", "new", + "--pem", str(testdata_path / "testUser.pem"), + "--receiver", "erd1cqqxak4wun7508e0yj9ng843r6hv4mzd0hhpjpsejkpn9wa9yq8sj7u2u5", + "--nonce", "987", + "--gas-limit", "5000000", + "--inner-transactions", str(testdata_out / "inner_transactions.json"), + "--chain", "T", + ]) + # remove test file to ensure consistency when running test file locally + os.remove(testdata_out / "inner_transactions.json") + + assert False if return_code else True + + tx = _read_stdout(capsys) + tx_json = json.loads(tx)["emittedTransaction"] + + assert tx_json["sender"] == "erd1cqqxak4wun7508e0yj9ng843r6hv4mzd0hhpjpsejkpn9wa9yq8sj7u2u5" + assert tx_json["receiver"] == "erd1cqqxak4wun7508e0yj9ng843r6hv4mzd0hhpjpsejkpn9wa9yq8sj7u2u5" + assert tx_json["gasLimit"] == 5000000 + assert tx_json["nonce"] == 987 + assert tx_json["chainID"] == "T" + + # should be the two inner transactions created in the tests above + inner_transactions = tx_json["innerTransactions"] + assert len(inner_transactions) == 2 + + assert inner_transactions[0]["sender"] == "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th" + assert inner_transactions[0]["receiver"] == "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx" + assert inner_transactions[0]["nonce"] == 77 + assert inner_transactions[0]["relayer"] == "erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8" + + assert inner_transactions[1]["sender"] == "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th" + assert inner_transactions[1]["receiver"] == "erd1fggp5ru0jhcjrp5rjqyqrnvhr3sz3v2e0fm3ktknvlg7mcyan54qzccnan" + assert inner_transactions[1]["nonce"] == 1234 + assert inner_transactions[1]["relayer"] == "erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8" + + def _read_stdout(capsys: Any) -> str: return capsys.readouterr().out.strip() diff --git a/multiversx_sdk_cli/transactions.py b/multiversx_sdk_cli/transactions.py index 9c08e748..1e441a17 100644 --- a/multiversx_sdk_cli/transactions.py +++ b/multiversx_sdk_cli/transactions.py @@ -2,10 +2,11 @@ import json import logging import time +from pathlib import Path from typing import Any, Dict, List, Optional, Protocol, TextIO from multiversx_sdk import (Address, Token, TokenComputer, TokenTransfer, - Transaction, TransactionPayload, + Transaction, TransactionsConverter, TransactionsFactoryConfig, TransferTransactionsFactory) @@ -37,48 +38,33 @@ def get_transaction(self, tx_hash: str, with_process_status: Optional[bool] = Fa ... -class JSONTransaction: - def __init__(self) -> None: - self.hash = "" - self.nonce = 0 - self.value = "0" - self.receiver = "" - self.sender = "" - self.senderUsername = "" - self.receiverUsername = "" - self.gasPrice = 0 - self.gasLimit = 0 - self.data: str = "" - self.chainID = "" - self.version = 0 - self.options = 0 - self.signature = "" - self.guardian = "" - self.guardianSignature = "" - - def do_prepare_transaction(args: Any) -> Transaction: account = load_sender_account_from_args(args) + + native_amount = int(args.value) transfers = getattr(args, "token_transfers", []) - transfers = prepare_token_transfers(transfers) if transfers else [] + transfers = prepare_token_transfers(transfers) if transfers else None config = TransactionsFactoryConfig(args.chain) factory = TransferTransactionsFactory(config) receiver = Address.new_from_bech32(args.receiver) - # will be replaced with 'create_transaction_for_transfer' - if transfers: - tx = factory.create_transaction_for_esdt_token_transfer( + if native_amount or transfers: + tx = factory.create_transaction_for_transfer( sender=account.address, receiver=receiver, - token_transfers=transfers + native_amount=native_amount, + token_transfers=transfers, + data=str(args.data).encode() ) else: - tx = factory.create_transaction_for_native_token_transfer( - sender=account.address, - receiver=receiver, - native_amount=int(args.value), - data=str(args.data) + # this is for transactions with no token transfers(egld/esdt); useful for setting the data field + tx = Transaction( + sender=account.address.to_bech32(), + receiver=receiver.to_bech32(), + data=str(args.data).encode(), + gas_limit=int(args.gas_limit), + chain_id=args.chain ) tx.gas_limit = int(args.gas_limit) @@ -93,6 +79,12 @@ def do_prepare_transaction(args: Any) -> Transaction: if args.guardian: tx.guardian = args.guardian + if args.relayer: + tx.relayer = Address.new_from_bech32(args.relayer).to_bech32() + + if args.inner_transactions: + tx.inner_transactions = load_inner_transactions_from_file(Path(args.inner_transactions).expanduser()) + tx.signature = bytes.fromhex(account.sign_transaction(tx)) tx = sign_tx_by_guardian(args, tx) @@ -231,37 +223,15 @@ def compute_relayed_v1_data(tx: Transaction) -> str: def load_transaction_from_file(f: TextIO) -> Transaction: data_json: bytes = f.read().encode() - fields = json.loads(data_json).get("tx") or json.loads(data_json).get("emittedTransaction") - - instance = JSONTransaction() - instance.__dict__.update(fields) - - loaded_tx = Transaction( - chain_id=instance.chainID, - sender=instance.sender, - receiver=instance.receiver, - sender_username=decode_field_value(instance.senderUsername), - receiver_username=decode_field_value(instance.receiverUsername), - gas_limit=instance.gasLimit, - gas_price=instance.gasPrice, - value=int(instance.value), - data=TransactionPayload.from_encoded_str(instance.data).data, - version=instance.version, - options=instance.options, - nonce=instance.nonce - ) - - if instance.guardian: - loaded_tx.guardian = instance.guardian - - if instance.signature: - loaded_tx.signature = bytes.fromhex(instance.signature) + transaction_dictionary = json.loads(data_json).get("tx") or json.loads(data_json).get("emittedTransaction") - if instance.guardianSignature: - loaded_tx.guardian_signature = bytes.fromhex(instance.guardianSignature) + tx_converter = TransactionsConverter() + return tx_converter.dictionary_to_transaction(transaction_dictionary) - return loaded_tx +def load_inner_transactions_from_file(path: Path) -> List[Transaction]: + data_json = path.read_text() + transactions: List[Dict[str, Any]] = json.loads(data_json).get("innerTransactions") -def decode_field_value(value: str) -> str: - return base64.b64decode(value).decode() + tx_converter = TransactionsConverter() + return [tx_converter.dictionary_to_transaction(transaction) for transaction in transactions] diff --git a/pyproject.toml b/pyproject.toml index 33a016c4..0bc30b98 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,8 +27,8 @@ dependencies = [ "semver", "requests-cache", "rich==13.3.4", - "multiversx-sdk==0.13.0", - "argcomplete==3.2.2" + "argcomplete==3.2.2", + "multiversx-sdk==0.13.0" ] [project.scripts]