diff --git a/multiversx_sdk/entrypoints/entrypoints_test.py b/multiversx_sdk/entrypoints/entrypoints_test.py index 3fc06472..d6a99de9 100644 --- a/multiversx_sdk/entrypoints/entrypoints_test.py +++ b/multiversx_sdk/entrypoints/entrypoints_test.py @@ -80,7 +80,7 @@ def test_contract_flow(self): tx_hash = self.entrypoint.send_transaction(transaction) outcome = controller.await_completed_deploy( - tx_hash, AwaitingOptions(polling_interval_in_milliseconds=6000, timeout_in_milliseconds=60000) + tx_hash, AwaitingOptions(polling_interval_in_milliseconds=600, timeout_in_milliseconds=30000) ) assert len(outcome.contracts) == 1 diff --git a/multiversx_sdk/network_providers/account_awaiter_test.py b/multiversx_sdk/network_providers/account_awaiter_test.py index 0cc65136..f79433e3 100644 --- a/multiversx_sdk/network_providers/account_awaiter_test.py +++ b/multiversx_sdk/network_providers/account_awaiter_test.py @@ -82,29 +82,29 @@ def condition(account: AccountOnNetwork): @pytest.mark.networkInteraction def test_ensure_error_if_timeout(self): - alice = load_wallets()["alice"] - alice_address = Address.new_from_bech32(alice.label) + grace = load_wallets()["grace"] + grace_address = Address.new_from_bech32(grace.label) bob = Address.new_from_bech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx") api = ApiNetworkProvider("https://devnet-api.multiversx.com") watcher = AccountAwaiter( fetcher=api, - polling_interval_in_milliseconds=1000, - timeout_interval_in_milliseconds=10000, + polling_interval_in_milliseconds=600, + timeout_interval_in_milliseconds=1000, ) value = 100_000 transaction = Transaction( - sender=alice_address, + sender=grace_address, receiver=bob, gas_limit=50000, chain_id="D", value=value, ) - transaction.nonce = api.get_account(alice_address).nonce + transaction.nonce = api.get_account(grace_address).nonce tx_computer = TransactionComputer() - transaction.signature = alice.secret_key.sign(tx_computer.compute_bytes_for_signing(transaction)) + transaction.signature = grace.secret_key.sign(tx_computer.compute_bytes_for_signing(transaction)) initial_balance = api.get_account(bob).balance diff --git a/multiversx_sdk/network_providers/api_network_provider_test.py b/multiversx_sdk/network_providers/api_network_provider_test.py index 44474fd1..23903bbd 100644 --- a/multiversx_sdk/network_providers/api_network_provider_test.py +++ b/multiversx_sdk/network_providers/api_network_provider_test.py @@ -31,7 +31,7 @@ def test_get_network_config(self): assert result.chain_id == "D" assert result.gas_per_data_byte == 1500 - assert result.round_duration == 6000 + assert result.round_duration == 600 assert result.min_gas_limit == 50000 assert result.min_gas_price == 1_000_000_000 assert result.raw @@ -175,7 +175,6 @@ def test_send_transactions(self): assert num_txs == 2 assert hashes == expected_hashes - @pytest.mark.skip def test_simulate_transaction(self): bob = load_wallets()["bob"] tx_computer = TransactionComputer() @@ -201,7 +200,7 @@ def test_simulate_transaction(self): transaction = Transaction( sender=Address.new_from_bech32(bob.label), receiver=Address.new_from_bech32("erd1qqqqqqqqqqqqqpgq076flgeualrdu5jyyj60snvrh7zu4qrg05vqez5jen"), - gas_limit=10000000, + gas_limit=1_500_000, chain_id="D", data=b"add@07", nonce=nonce, @@ -309,7 +308,7 @@ def test_send_and_await_for_completed_transaction(self): tx_on_network = self.api.await_transaction_completed( transaction_hash=hash, - options=AwaitingOptions(polling_interval_in_milliseconds=6000, timeout_in_milliseconds=30000), + options=AwaitingOptions(polling_interval_in_milliseconds=600, timeout_in_milliseconds=20000), ) assert tx_on_network.status.is_completed @@ -327,7 +326,7 @@ def test_send_and_await_for_completed_transaction(self): tx_on_network = self.api.await_transaction_completed( transaction_hash=hash, - options=AwaitingOptions(polling_interval_in_milliseconds=6000, timeout_in_milliseconds=30000), + options=AwaitingOptions(polling_interval_in_milliseconds=600, timeout_in_milliseconds=20000), ) assert not tx_on_network.status.is_successful @@ -351,7 +350,7 @@ def test_send_and_await_transaction_on_condition(self): tx_on_network = self.api.await_transaction_completed( transaction_hash=hash, - options=AwaitingOptions(polling_interval_in_milliseconds=6000, timeout_in_milliseconds=30000), + options=AwaitingOptions(polling_interval_in_milliseconds=600, timeout_in_milliseconds=20000), ) assert tx_on_network.status.is_completed @@ -373,7 +372,7 @@ def condition(tx: TransactionOnNetwork) -> bool: tx_on_network = self.api.await_transaction_on_condition( transaction_hash=hash, condition=condition, - options=AwaitingOptions(polling_interval_in_milliseconds=6000, timeout_in_milliseconds=30000), + options=AwaitingOptions(polling_interval_in_milliseconds=600, timeout_in_milliseconds=20000), ) assert tx_on_network.status.status == "fail" diff --git a/multiversx_sdk/network_providers/proxy_network_provider_test.py b/multiversx_sdk/network_providers/proxy_network_provider_test.py index 946894f1..91b264e1 100644 --- a/multiversx_sdk/network_providers/proxy_network_provider_test.py +++ b/multiversx_sdk/network_providers/proxy_network_provider_test.py @@ -31,7 +31,7 @@ def test_get_network_config(self): assert result.chain_id == "D" assert result.gas_per_data_byte == 1500 - assert result.round_duration == 6000 + assert result.round_duration == 600 assert result.min_gas_limit == 50000 assert result.min_gas_price == 1_000_000_000 assert result.raw @@ -281,7 +281,6 @@ def test_send_transactions(self): assert num_txs == 2 assert hashes == expected_hashes - @pytest.mark.skip def test_simulate_transaction(self): bob = load_wallets()["bob"] tx_computer = TransactionComputer() @@ -307,7 +306,7 @@ def test_simulate_transaction(self): transaction = Transaction( sender=Address.new_from_bech32(bob.label), receiver=Address.new_from_bech32("erd1qqqqqqqqqqqqqpgq076flgeualrdu5jyyj60snvrh7zu4qrg05vqez5jen"), - gas_limit=10000000, + gas_limit=1_500_000, chain_id="D", data=b"add@07", nonce=nonce, @@ -375,7 +374,7 @@ def test_send_and_await_for_completed_transaction(self): tx_on_network = self.proxy.await_transaction_completed( hash, options=AwaitingOptions( - polling_interval_in_milliseconds=6000, + polling_interval_in_milliseconds=600, timeout_in_milliseconds=30000, ), ) @@ -399,7 +398,7 @@ def condition(tx: TransactionOnNetwork) -> bool: tx_on_network = self.proxy.await_transaction_on_condition( transaction_hash=hash, condition=condition, - options=AwaitingOptions(polling_interval_in_milliseconds=6000, timeout_in_milliseconds=30000), + options=AwaitingOptions(polling_interval_in_milliseconds=600, timeout_in_milliseconds=30000), ) assert not tx_on_network.status.is_successful diff --git a/multiversx_sdk/network_providers/transaction_awaiter.py b/multiversx_sdk/network_providers/transaction_awaiter.py index 6df4ed8a..030e6f4a 100644 --- a/multiversx_sdk/network_providers/transaction_awaiter.py +++ b/multiversx_sdk/network_providers/transaction_awaiter.py @@ -92,31 +92,57 @@ def _await_conditionally( do_fetch: Callable[[], TransactionOnNetwork], error: Exception, ) -> TransactionOnNetwork: - is_condition_satisfied = False - fetched_data: Union[TransactionOnNetwork, None] = None - max_number_of_retries = self.timeout_interval_in_milliseconds // self.polling_interval_in_milliseconds + max_number_of_attempts = self.timeout_interval_in_milliseconds // self.polling_interval_in_milliseconds + fetched_data, attempts_made = self._fetch_until_condition( + is_satisfied=is_satisfied, + do_fetch=do_fetch, + number_of_attempts=max_number_of_attempts, + ) + + if fetched_data is None: + raise error + + if self.patience_time_in_milliseconds: + time.sleep(self.patience_time_in_milliseconds / ONE_SECOND_IN_MILLISECONDS) + remaining_attempts = max_number_of_attempts - attempts_made + + # Keep the existing post-patience refresh in addition to the unused attempts. + refreshed_data, _ = self._fetch_until_condition( + is_satisfied=is_satisfied, + do_fetch=do_fetch, + number_of_attempts=remaining_attempts + 1, + ) + + if refreshed_data is not None: + return refreshed_data + + logger.warning( + "Transaction status regressed after reaching the expected condition. " + "Returning the last satisfying response." + ) + + return fetched_data + + def _fetch_until_condition( + self, + is_satisfied: Callable[[TransactionOnNetwork], bool], + do_fetch: Callable[[], TransactionOnNetwork], + number_of_attempts: int, + ) -> tuple[Optional[TransactionOnNetwork], int]: + attempts_made = 0 + + while attempts_made < number_of_attempts: + attempts_made += 1 - number_of_retries = 0 - while number_of_retries < max_number_of_retries: try: fetched_data = do_fetch() - is_condition_satisfied = is_satisfied(fetched_data) - if is_condition_satisfied: - break + if is_satisfied(fetched_data): + return fetched_data, attempts_made except TransactionFetchingError: logger.warning("Couldn't fetch transaction. Retrying...") - except Exception as ex: - raise ex - number_of_retries += 1 - time.sleep(self.polling_interval_in_milliseconds / ONE_SECOND_IN_MILLISECONDS) + if attempts_made < number_of_attempts: + time.sleep(self.polling_interval_in_milliseconds / ONE_SECOND_IN_MILLISECONDS) - if fetched_data is None or not is_condition_satisfied: - raise error - - if self.patience_time_in_milliseconds: - time.sleep(self.patience_time_in_milliseconds / ONE_SECOND_IN_MILLISECONDS) - return do_fetch() - - return fetched_data + return None, attempts_made diff --git a/multiversx_sdk/network_providers/transaction_awaiter_test.py b/multiversx_sdk/network_providers/transaction_awaiter_test.py index 083cece6..a271cfb1 100644 --- a/multiversx_sdk/network_providers/transaction_awaiter_test.py +++ b/multiversx_sdk/network_providers/transaction_awaiter_test.py @@ -1,3 +1,5 @@ +from unittest.mock import patch + import pytest from multiversx_sdk.core.address import Address @@ -5,7 +7,6 @@ from multiversx_sdk.core.transaction_computer import TransactionComputer from multiversx_sdk.core.transaction_on_network import TransactionOnNetwork from multiversx_sdk.core.transaction_status import TransactionStatus -from multiversx_sdk.network_providers.api_network_provider import ApiNetworkProvider from multiversx_sdk.network_providers.errors import ( ExpectedTransactionStatusNotReachedError, ) @@ -22,6 +23,17 @@ from multiversx_sdk.testutils.wallets import load_wallets +class TransactionSequenceFetcher: + def __init__(self, transactions: list[TransactionOnNetwork]) -> None: + self.transactions = transactions + self.number_of_fetches = 0 + + def get_transaction(self, transaction_hash: bytes | str) -> TransactionOnNetwork: + transaction = self.transactions[self.number_of_fetches] + self.number_of_fetches += 1 + return transaction + + class TestTransactionAwaiter: provider = MockNetworkProvider() watcher = TransactionAwaiter( @@ -73,9 +85,9 @@ def test_await_status_not_executable(self): @pytest.mark.networkInteraction def test_on_network(self): alice = load_wallets()["alice"] - proxy = ProxyNetworkProvider("https://devnet-api.multiversx.com") + proxy = ProxyNetworkProvider("https://devnet-gateway.multiversx.com") watcher = TransactionAwaiter( - proxy, polling_interval_in_milliseconds=6000, timeout_interval_in_milliseconds=30000 + proxy, polling_interval_in_milliseconds=1000, timeout_interval_in_milliseconds=20000 ) tx_computer = TransactionComputer() @@ -116,17 +128,72 @@ def condition(tx: TransactionOnNetwork) -> bool: tx_from_network = self.watcher.await_on_condition(tx_hash, condition) assert tx_from_network.status.status == "failed" + def test_retries_after_status_regresses_during_patience(self): + pending = get_empty_transaction_on_network() + pending.status = TransactionStatus("pending") + + first_completed = get_empty_transaction_on_network() + first_completed.status = TransactionStatus("success") + + regressed = get_empty_transaction_on_network() + regressed.status = TransactionStatus("pending") + + refreshed_completed = get_empty_transaction_on_network() + refreshed_completed.status = TransactionStatus("success") + refreshed_completed.raw["refreshed"] = True + + fetcher = TransactionSequenceFetcher([pending, first_completed, regressed, refreshed_completed]) + watcher = TransactionAwaiter( + fetcher=fetcher, + polling_interval_in_milliseconds=10, + timeout_interval_in_milliseconds=40, + patience_time_in_milliseconds=10, + ) + + with patch("multiversx_sdk.network_providers.transaction_awaiter.time.sleep"): + tx_from_network = watcher.await_completed("transaction-hash") + + assert tx_from_network is refreshed_completed + assert tx_from_network.raw["refreshed"] + assert fetcher.number_of_fetches == 4 + + def test_returns_last_satisfying_response_after_remaining_attempts_are_exhausted(self): + pending = get_empty_transaction_on_network() + pending.status = TransactionStatus("pending") + + completed = get_empty_transaction_on_network() + completed.status = TransactionStatus("success") + + regressed_responses = [get_empty_transaction_on_network() for _ in range(3)] + for response in regressed_responses: + response.status = TransactionStatus("pending") + + fetcher = TransactionSequenceFetcher([pending, completed, *regressed_responses]) + watcher = TransactionAwaiter( + fetcher=fetcher, + polling_interval_in_milliseconds=10, + timeout_interval_in_milliseconds=40, + patience_time_in_milliseconds=10, + ) + + with patch("multiversx_sdk.network_providers.transaction_awaiter.time.sleep"): + tx_from_network = watcher.await_completed("transaction-hash") + + assert tx_from_network is completed + assert tx_from_network.status.is_completed + assert fetcher.number_of_fetches == 5 + @pytest.mark.networkInteraction def test_ensure_error_if_timeout(self): alice = load_wallets()["alice"] alice_address = Address.new_from_bech32(alice.label) bob = Address.new_from_bech32("erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx") - api = ApiNetworkProvider("https://devnet-api.multiversx.com") + api = ProxyNetworkProvider("https://devnet-gateway.multiversx.com") watcher = TransactionAwaiter( fetcher=api, - polling_interval_in_milliseconds=1000, - timeout_interval_in_milliseconds=10000, + polling_interval_in_milliseconds=600, + timeout_interval_in_milliseconds=1000, ) transaction = Transaction( diff --git a/pyproject.toml b/pyproject.toml index 6f8cc7ee..2531613d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ allow-direct-references = true [project] name = "multiversx-sdk" -version = "3.0.0" +version = "3.0.1" authors = [ { name="MultiversX" }, ]