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

Added RelayedV3 support #457

Merged
merged 5 commits into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
357 changes: 223 additions & 134 deletions CLI.md

Large diffs are not rendered by default.

32 changes: 31 additions & 1 deletion multiversx_sdk_cli/cli_shared.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ def add_tx_args(
with_nonce: bool = True,
with_receiver: bool = True,
with_data: bool = True,
with_estimate_gas: bool = False):
with_estimate_gas: bool = False,
with_relayer_wallet_args: bool = True):
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 @@ -90,6 +91,10 @@ def add_tx_args(
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)")

sub.add_argument("--relayer", help="the bech32 address of the relayer")
if with_relayer_wallet_args:
add_relayed_v3_wallet_args(args, sub)

add_guardian_args(sub)

sub.add_argument("--options", type=int, default=0, help="the transaction options (default: 0)")
Expand Down Expand Up @@ -122,6 +127,17 @@ 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):
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

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", 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", 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")


def add_proxy_arg(sub: Any):
sub.add_argument("--proxy", help="🔗 the URL of the proxy")

Expand Down Expand Up @@ -166,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)
Expand Down
33 changes: 32 additions & 1 deletion multiversx_sdk_cli/cli_transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 relayer 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

Expand Down Expand Up @@ -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])
5 changes: 5 additions & 0 deletions multiversx_sdk_cli/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
2 changes: 2 additions & 0 deletions multiversx_sdk_cli/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ class ITransaction(Protocol):
guardian: str
signature: bytes
guardian_signature: bytes
relayer: str
relayer_signature: bytes


class IAccount(Protocol):
Expand Down
108 changes: 108 additions & 0 deletions multiversx_sdk_cli/tests/test_cli_transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,5 +105,113 @@ 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",
Copy link
Contributor

Choose a reason for hiding this comment

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

Not possible to take a user transaction, already signed, and sign it with a relayer? Maybe mxpy tx relay or mxpy wallet sign-as-relayer? 💭

I think we should allow users to decouple (in a way or another) the step of signing as user from the step of signing as relayer.

Maybe brainstorm this a bit?

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, sounds like a good idea. Added a new command, mxpy tx relay.

"--pem", str(testdata_path / "alice.pem"),
"--receiver", "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx",
"--nonce", "7",
"--gas-limit", "1300000",
"--value", "1000000000000000000",
"--chain", "T",
"--relayer", "erd1cqqxak4wun7508e0yj9ng843r6hv4mzd0hhpjpsejkpn9wa9yq8sj7u2u5",
"--relayer-pem", str(testdata_path / "testUser.pem")
])
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 tx_json["relayerSignature"]

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"),
"--receiver", "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx",
"--nonce", "7",
"--gas-limit", "1300000",
"--value", "1000000000000000000",
"--chain", "T",
"--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["signature"] == initial_sender_signature
assert tx_json["relayerSignature"] == initial_relayer_signature

# Clear the captured content
capsys.readouterr()


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:
return capsys.readouterr().out.strip()
30 changes: 29 additions & 1 deletion multiversx_sdk_cli/transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -78,6 +78,20 @@ def do_prepare_transaction(args: Any) -> Transaction:
if args.guardian:
tx.guardian = args.guardian

if args.relayer:
tx.relayer = args.relayer

try:
relayer_account = load_relayer_account_from_args(args)
if relayer_account.address.to_bech32() != tx.relayer:
raise IncorrectWalletError("")

tx.relayer_signature = bytes.fromhex(relayer_account.sign_transaction(tx))
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)

Expand All @@ -97,6 +111,20 @@ def load_sender_account_from_args(args: Any) -> Account:
return account


def load_relayer_account_from_args(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_token_transfers(transfers: List[Any]) -> List[TokenTransfer]:
token_computer = TokenComputer()
token_transfers: List[TokenTransfer] = []
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "multiversx-sdk-cli"
version = "9.9.1"
version = "9.10.0"
authors = [
{ name="MultiversX" },
]
Expand Down
Loading