Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Relayed V3 #434

Merged
merged 18 commits into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion multiversx_sdk_cli/cli_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,15 @@ 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,
with_guardian: bool = False,
with_relayed_v3: bool = True):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can simplify the arguments of add_tx_args, with respect to its usage. E.g. we can drop with_relayed_v3 and with_guardian, since the defaults are not overridden (at least, not now, maybe never).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Removed with_guardian and with_relayed_v3. Arguments are now always present.

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)")
Expand All @@ -88,6 +96,11 @@ def add_tx_args(args: List[str], sub: Any, with_nonce: bool = True, with_receive
if with_guardian:
add_guardian_args(sub)

if with_relayed_v3:
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)")

sub.add_argument("--options", type=int, default=0, help="the transaction options (default: 0)")


Expand Down
41 changes: 39 additions & 2 deletions multiversx_sdk_cli/cli_transactions.py
Original file line number Diff line number Diff line change
@@ -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")
Expand Down Expand Up @@ -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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a double check - is this an invalid case (wrt. to the Protocol)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, we can't set data field for the relayer's transaction.

raise BadUsage("Can't set data field when creating a relayedV3 transaction")


def send_transaction(args: Any):
args = utils.as_object(args)

Expand Down
7 changes: 6 additions & 1 deletion multiversx_sdk_cli/interfaces.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, Dict, Protocol
from typing import Any, Dict, Protocol, Sequence


class IAddress(Protocol):
Expand All @@ -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):
Expand Down
92 changes: 91 additions & 1 deletion multiversx_sdk_cli/tests/test_cli_transactions.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -87,5 +89,93 @@
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"]

Check warning on line 123 in multiversx_sdk_cli/tests/test_cli_transactions.py

View workflow job for this annotation

GitHub Actions / runner / mypy

[mypy] reported by reviewdog 🐶 By default the bodies of untyped functions are not checked, consider using --check-untyped-defs [annotation-unchecked] Raw Output: /home/runner/work/mx-sdk-py-cli/mx-sdk-py-cli/multiversx_sdk_cli/tests/test_cli_transactions.py:123:5: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs [annotation-unchecked]
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()
93 changes: 32 additions & 61 deletions multiversx_sdk_cli/transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -37,48 +38,34 @@ 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)

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(
# temporary workaround until proper fix in sdk-py
if native_amount or transfers:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, we can drop the if-else after the fix in sdk-py. Or keep this as it is, but then drop the comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll keep is as it is, at least for now. Removed the comment.

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)
Expand All @@ -93,6 +80,12 @@ def do_prepare_transaction(args: Any) -> Transaction:
if args.guardian:
tx.guardian = args.guardian

if args.relayer:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

tx.relayer = Address.new_from_bech32(args.relayer).to_bech32()

if args.inner_transactions:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

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)

Expand Down Expand Up @@ -231,37 +224,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_bytes()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

read_text(), instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

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]
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ requests-cache
rich==13.3.4
argcomplete==3.2.2

multiversx-sdk>=0.9.2,<1.0.0
multiversx-sdk @ git+https://github.com/multiversx/mx-sdk-py@feat/next
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 (maybe even a fixed commit hash or tag)

Loading