Skip to content

Commit b080e02

Browse files
authored
Merge pull request #457 from multiversx/new-relayed-v3
Added RelayedV3 support
2 parents 7aa66a1 + 6f0ae29 commit b080e02

8 files changed

Lines changed: 431 additions & 138 deletions

File tree

CLI.md

Lines changed: 223 additions & 134 deletions
Large diffs are not rendered by default.

multiversx_sdk_cli/cli_shared.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@ def add_tx_args(
6868
with_nonce: bool = True,
6969
with_receiver: bool = True,
7070
with_data: bool = True,
71-
with_estimate_gas: bool = False):
71+
with_estimate_gas: bool = False,
72+
with_relayer_wallet_args: bool = True):
7273
if with_nonce:
7374
sub.add_argument("--nonce", type=int, required=not ("--recall-nonce" in args), help="# the nonce for the transaction")
7475
sub.add_argument("--recall-nonce", action="store_true", default=False, help="⭮ whether to recall the nonce when creating the transaction (default: %(default)s)")
@@ -90,6 +91,10 @@ def add_tx_args(
9091
sub.add_argument("--chain", help="the chain identifier")
9192
sub.add_argument("--version", type=int, default=DEFAULT_TX_VERSION, help="the transaction version (default: %(default)s)")
9293

94+
sub.add_argument("--relayer", help="the bech32 address of the relayer")
95+
if with_relayer_wallet_args:
96+
add_relayed_v3_wallet_args(args, sub)
97+
9398
add_guardian_args(sub)
9499

95100
sub.add_argument("--options", type=int, default=0, help="the transaction options (default: 0)")
@@ -122,6 +127,17 @@ def add_guardian_wallet_args(args: List[str], sub: Any):
122127
sub.add_argument("--guardian-ledger-address-index", type=int, default=0, help="🔐 the index of the address when using Ledger")
123128

124129

130+
# Required check not properly working, same for guardian. Will be refactored in the future.
131+
def add_relayed_v3_wallet_args(args: List[str], sub: Any):
132+
sub.add_argument("--relayer-pem", help="🔑 the PEM file, if keyfile not provided")
133+
sub.add_argument("--relayer-pem-index", type=int, default=0, help="🔑 the index in the PEM file (default: %(default)s)")
134+
sub.add_argument("--relayer-keyfile", help="🔑 a JSON keyfile, if PEM not provided")
135+
sub.add_argument("--relayer-passfile", help="🔑 a file containing keyfile's password, if keyfile provided")
136+
sub.add_argument("--relayer-ledger", action="store_true", default=False, help="🔐 bool flag for signing transaction using ledger")
137+
sub.add_argument("--relayer-ledger-account-index", type=int, default=0, help="🔐 the index of the account when using Ledger")
138+
sub.add_argument("--relayer-ledger-address-index", type=int, default=0, help="🔐 the index of the address when using Ledger")
139+
140+
125141
def add_proxy_arg(sub: Any):
126142
sub.add_argument("--proxy", help="🔗 the URL of the proxy")
127143

@@ -166,6 +182,20 @@ def prepare_account(args: Any):
166182
return account
167183

168184

185+
def prepare_relayer_account(args: Any) -> Account:
186+
if args.relayer_ledger:
187+
account = LedgerAccount(account_index=args.relayer_ledger_account_index, address_index=args.relayer_ledger_address_index)
188+
if args.relayer_pem:
189+
account = Account(pem_file=args.relayer_pem, pem_index=args.relayer_pem_index)
190+
elif args.relayer_keyfile:
191+
password = load_password(args)
192+
account = Account(key_file=args.relayer_keyfile, password=password)
193+
else:
194+
raise errors.NoWalletProvided()
195+
196+
return account
197+
198+
169199
def prepare_guardian_account(args: Any):
170200
if args.guardian_pem:
171201
account = Account(pem_file=args.guardian_pem, pem_index=args.guardian_pem_index)

multiversx_sdk_cli/cli_transactions.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from multiversx_sdk_cli.cli_output import CLIOutputBuilder
99
from multiversx_sdk_cli.config import get_config_for_network_providers
1010
from multiversx_sdk_cli.cosign_transaction import cosign_transaction
11-
from multiversx_sdk_cli.errors import NoWalletProvided
11+
from multiversx_sdk_cli.errors import IncorrectWalletError, NoWalletProvided
1212
from multiversx_sdk_cli.transactions import (compute_relayed_v1_data,
1313
do_prepare_transaction,
1414
load_transaction_from_file)
@@ -57,6 +57,14 @@ def setup_parser(args: List[str], subparsers: Any) -> Any:
5757
cli_shared.add_guardian_wallet_args(args, sub)
5858
sub.set_defaults(func=sign_transaction)
5959

60+
sub = cli_shared.add_command_subparser(subparsers, "tx", "relay", f"Relay a previously saved transaction.{CLIOutputBuilder.describe()}")
61+
cli_shared.add_relayed_v3_wallet_args(args, sub)
62+
cli_shared.add_infile_arg(sub, what="a previously saved transaction")
63+
cli_shared.add_outfile_arg(sub, what="the relayer signed transaction")
64+
cli_shared.add_broadcast_args(sub)
65+
cli_shared.add_proxy_arg(sub)
66+
sub.set_defaults(func=relay_transaction)
67+
6068
parser.epilog = cli_shared.build_group_epilog(subparsers)
6169
return subparsers
6270

@@ -141,3 +149,26 @@ def sign_transaction(args: Any):
141149
tx = cosign_transaction(tx, args.guardian_service_url, args.guardian_2fa_code)
142150

143151
cli_shared.send_or_simulate(tx, args)
152+
153+
154+
def relay_transaction(args: Any):
155+
args = utils.as_object(args)
156+
157+
if not _is_relayer_wallet_provided(args):
158+
raise NoWalletProvided()
159+
160+
cli_shared.check_broadcast_args(args)
161+
162+
tx = load_transaction_from_file(args.infile)
163+
relayer = cli_shared.prepare_relayer_account(args)
164+
165+
if tx.relayer != relayer.address.to_bech32():
166+
raise IncorrectWalletError("Relayer wallet does not match the relayer's address set in the transaction.")
167+
168+
tx.relayer_signature = bytes.fromhex(relayer.sign_transaction(tx))
169+
170+
cli_shared.send_or_simulate(tx, args)
171+
172+
173+
def _is_relayer_wallet_provided(args: Any):
174+
return any([args.relayer_pem, args.relayer_keyfile, args.relayer_ledger])

multiversx_sdk_cli/errors.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,3 +182,8 @@ def __init__(self, message: str, inner: Any = None):
182182
class NativeAuthClientError(KnownError):
183183
def __init__(self, message: str):
184184
super().__init__(message)
185+
186+
187+
class IncorrectWalletError(KnownError):
188+
def __init__(self, message: str):
189+
super().__init__(message)

multiversx_sdk_cli/interfaces.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ class ITransaction(Protocol):
2525
guardian: str
2626
signature: bytes
2727
guardian_signature: bytes
28+
relayer: str
29+
relayer_signature: bytes
2830

2931

3032
class IAccount(Protocol):

multiversx_sdk_cli/tests/test_cli_transactions.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,5 +105,113 @@ def test_create_multi_transfer_transaction_with_single_egld_transfer(capsys: Any
105105
assert data == "MultiESDTNFTTransfer@8049d639e5a6980d1cd2392abcce41029cda74a1563523a202f09641cc2618f8@01@45474c442d303030303030@@0de0b6b3a7640000"
106106

107107

108+
def test_relayed_v3_without_relayer_wallet(capsys: Any):
109+
return_code = main([
110+
"tx", "new",
111+
"--pem", str(testdata_path / "alice.pem"),
112+
"--receiver", "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx",
113+
"--nonce", "7",
114+
"--gas-limit", "1300000",
115+
"--value", "1000000000000000000",
116+
"--chain", "T",
117+
"--relayer", "erd1cqqxak4wun7508e0yj9ng843r6hv4mzd0hhpjpsejkpn9wa9yq8sj7u2u5"
118+
])
119+
assert return_code == 0
120+
tx = _read_stdout(capsys)
121+
tx_json = json.loads(tx)["emittedTransaction"]
122+
assert tx_json["sender"] == "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"
123+
assert tx_json["receiver"] == "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"
124+
assert tx_json["relayer"] == "erd1cqqxak4wun7508e0yj9ng843r6hv4mzd0hhpjpsejkpn9wa9yq8sj7u2u5"
125+
assert tx_json["signature"]
126+
assert not tx_json["relayerSignature"]
127+
128+
129+
def test_relayed_v3_incorrect_relayer():
130+
return_code = main([
131+
"tx", "new",
132+
"--pem", str(testdata_path / "alice.pem"),
133+
"--receiver", "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx",
134+
"--nonce", "7",
135+
"--gas-limit", "1300000",
136+
"--value", "1000000000000000000",
137+
"--chain", "T",
138+
"--relayer", "erd1cqqxak4wun7508e0yj9ng843r6hv4mzd0hhpjpsejkpn9wa9yq8sj7u2u5",
139+
"--relayer-pem", str(testdata_path / "alice.pem")
140+
])
141+
assert return_code
142+
143+
144+
def test_create_relayed_v3_transaction(capsys: Any):
145+
# create relayed v3 tx and save signature and relayer signature
146+
# create the same tx, save to file
147+
# sign from file with relayer wallet and make sure signatures match
148+
return_code = main([
149+
"tx", "new",
150+
"--pem", str(testdata_path / "alice.pem"),
151+
"--receiver", "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx",
152+
"--nonce", "7",
153+
"--gas-limit", "1300000",
154+
"--value", "1000000000000000000",
155+
"--chain", "T",
156+
"--relayer", "erd1cqqxak4wun7508e0yj9ng843r6hv4mzd0hhpjpsejkpn9wa9yq8sj7u2u5",
157+
"--relayer-pem", str(testdata_path / "testUser.pem")
158+
])
159+
assert return_code == 0
160+
161+
tx = _read_stdout(capsys)
162+
tx_json = json.loads(tx)["emittedTransaction"]
163+
assert tx_json["sender"] == "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"
164+
assert tx_json["receiver"] == "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx"
165+
assert tx_json["relayer"] == "erd1cqqxak4wun7508e0yj9ng843r6hv4mzd0hhpjpsejkpn9wa9yq8sj7u2u5"
166+
assert tx_json["signature"]
167+
assert tx_json["relayerSignature"]
168+
169+
initial_sender_signature = tx_json["signature"]
170+
initial_relayer_signature = tx_json["relayerSignature"]
171+
172+
# Clear the captured content
173+
capsys.readouterr()
174+
175+
# save tx to file then load and sign tx by relayer
176+
return_code = main([
177+
"tx", "new",
178+
"--pem", str(testdata_path / "alice.pem"),
179+
"--receiver", "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx",
180+
"--nonce", "7",
181+
"--gas-limit", "1300000",
182+
"--value", "1000000000000000000",
183+
"--chain", "T",
184+
"--relayer", "erd1cqqxak4wun7508e0yj9ng843r6hv4mzd0hhpjpsejkpn9wa9yq8sj7u2u5",
185+
"--outfile", str(testdata_out / "relayed.json")
186+
])
187+
assert return_code == 0
188+
189+
# Clear the captured content
190+
capsys.readouterr()
191+
192+
return_code = main([
193+
"tx", "relay",
194+
"--relayer-pem", str(testdata_path / "testUser.pem"),
195+
"--infile", str(testdata_out / "relayed.json")
196+
])
197+
assert return_code == 0
198+
199+
tx = _read_stdout(capsys)
200+
tx_json = json.loads(tx)["emittedTransaction"]
201+
assert tx_json["signature"] == initial_sender_signature
202+
assert tx_json["relayerSignature"] == initial_relayer_signature
203+
204+
# Clear the captured content
205+
capsys.readouterr()
206+
207+
208+
def test_check_relayer_wallet_is_provided():
209+
return_code = main([
210+
"tx", "relay",
211+
"--infile", str(testdata_out / "relayed.json")
212+
])
213+
assert return_code
214+
215+
108216
def _read_stdout(capsys: Any) -> str:
109217
return capsys.readouterr().out.strip()

multiversx_sdk_cli/transactions.py

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from multiversx_sdk_cli.cli_password import (load_guardian_password,
1515
load_password)
1616
from multiversx_sdk_cli.cosign_transaction import cosign_transaction
17-
from multiversx_sdk_cli.errors import NoWalletProvided
17+
from multiversx_sdk_cli.errors import IncorrectWalletError, NoWalletProvided
1818
from multiversx_sdk_cli.interfaces import ITransaction
1919
from multiversx_sdk_cli.ledger.ledger_functions import do_get_ledger_address
2020

@@ -78,6 +78,20 @@ def do_prepare_transaction(args: Any) -> Transaction:
7878
if args.guardian:
7979
tx.guardian = args.guardian
8080

81+
if args.relayer:
82+
tx.relayer = args.relayer
83+
84+
try:
85+
relayer_account = load_relayer_account_from_args(args)
86+
if relayer_account.address.to_bech32() != tx.relayer:
87+
raise IncorrectWalletError("")
88+
89+
tx.relayer_signature = bytes.fromhex(relayer_account.sign_transaction(tx))
90+
except NoWalletProvided:
91+
logger.warning("Relayer wallet not provided. Transaction will not be signed by relayer.")
92+
except IncorrectWalletError:
93+
raise IncorrectWalletError("Relayer wallet does not match the relayer's address set in the transaction.")
94+
8195
tx.signature = bytes.fromhex(account.sign_transaction(tx))
8296
tx = sign_tx_by_guardian(args, tx)
8397

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

99113

114+
def load_relayer_account_from_args(args: Any) -> Account:
115+
if args.relayer_ledger:
116+
account = LedgerAccount(account_index=args.relayer_ledger_account_index, address_index=args.relayer_ledger_address_index)
117+
if args.relayer_pem:
118+
account = Account(pem_file=args.relayer_pem, pem_index=args.relayer_pem_index)
119+
elif args.relayer_keyfile:
120+
password = load_password(args)
121+
account = Account(key_file=args.relayer_keyfile, password=password)
122+
else:
123+
raise errors.NoWalletProvided()
124+
125+
return account
126+
127+
100128
def prepare_token_transfers(transfers: List[Any]) -> List[TokenTransfer]:
101129
token_computer = TokenComputer()
102130
token_transfers: List[TokenTransfer] = []

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "multiversx-sdk-cli"
7-
version = "9.9.1"
7+
version = "9.10.0"
88
authors = [
99
{ name="MultiversX" },
1010
]

0 commit comments

Comments
 (0)