diff --git a/external/monero-cpp b/external/monero-cpp index 6927f7b..6c7b4f2 160000 --- a/external/monero-cpp +++ b/external/monero-cpp @@ -1 +1 @@ -Subproject commit 6927f7bf52dabad2179dfe2a0a0e1a95e12290c1 +Subproject commit 6c7b4f2bbb2af79c236955ccc2ca56a86a656ae8 diff --git a/src/cpp/wallet/py_monero_wallet_bindings.cpp b/src/cpp/wallet/py_monero_wallet_bindings.cpp index 15b2394..2ed7da8 100644 --- a/src/cpp/wallet/py_monero_wallet_bindings.cpp +++ b/src/cpp/wallet/py_monero_wallet_bindings.cpp @@ -53,6 +53,11 @@ */ #include "py_monero_types.h" +// declared in monero-cpp's monero_wallet_full.cpp but not exposed by any header +namespace monero { + std::string get_default_ringdb_path(cryptonote::network_type nettype); +} + void py_monero_bind_wallet(py::module_& m, PyMoneroTypes& t) { // monero_wallet_config t.py_monero_wallet_config @@ -1100,6 +1105,9 @@ void py_monero_bind_wallet(py::module_& m, PyMoneroTypes& t) { .def_static("get_seed_languages", []() { MONERO_CATCH_AND_RETHROW(monero_wallet_full::get_seed_languages()); }, py::call_guard()) + .def_static("get_default_ringdb_path", [](monero_network_type nettype) { + MONERO_CATCH_AND_RETHROW(get_default_ringdb_path(static_cast(nettype))); + }, py::arg("nettype")) .def("get_keys_file_buffer", [](monero_wallet_full& self, std::string& password, bool view_only) -> py::bytes { MONERO_CATCH_AND_RETHROW(py::bytes(self.get_keys_file_buffer(password, view_only))); }, py::arg("password"), py::arg("view_only")) @@ -1126,6 +1134,12 @@ void py_monero_bind_wallet(py::module_& m, PyMoneroTypes& t) { .def("get_rpc_connection", [](monero_wallet_rpc& self) { MONERO_CATCH_AND_RETHROW(self.get_rpc_connection()); }, py::call_guard()) + .def("set_poll_period_in_ms", [](monero_wallet_rpc& self, uint64_t period_ms) { + MONERO_CATCH_AND_RETHROW(self.set_poll_period_in_ms(period_ms)); + }, py::arg("period_ms"), py::call_guard()) + .def("get_subaddress", [](monero_wallet_rpc& self, uint32_t account_idx, uint32_t subaddress_idx) { + MONERO_CATCH_AND_RETHROW(self.get_subaddress(account_idx, subaddress_idx)); + }, py::arg("account_idx"), py::arg("subaddress_idx"), py::call_guard()) // this because of function hiding .def("set_daemon_connection", [](PyMoneroWallet& self, const std::shared_ptr& connection, const boost::optional& is_trusted) { MONERO_CATCH_AND_RETHROW(self.set_daemon_connection(connection, is_trusted)); diff --git a/src/python/monero_wallet_full.pyi b/src/python/monero_wallet_full.pyi index 9c71f9d..c2a4b0b 100644 --- a/src/python/monero_wallet_full.pyi +++ b/src/python/monero_wallet_full.pyi @@ -17,6 +17,16 @@ class MoneroWalletFull(MoneroWallet): """ ... + @staticmethod + def get_default_ringdb_path(nettype: MoneroNetworkType) -> str: + """ + Get the default ring database path for a given network type. + + :param MoneroNetworkType nettype: the network type. + :returns str: the default ring database path. + """ + ... + @staticmethod def get_seed_languages() -> list[str]: """ diff --git a/src/python/monero_wallet_rpc.pyi b/src/python/monero_wallet_rpc.pyi index b9f13dc..7ec8a05 100644 --- a/src/python/monero_wallet_rpc.pyi +++ b/src/python/monero_wallet_rpc.pyi @@ -75,6 +75,14 @@ class MoneroWalletRpc(MoneroWallet): """Save and close the current wallet and stop the RPC server.""" ... + def set_poll_period_in_ms(self, period_ms: int) -> None: + """ + Set the period between polls of the wallet for notifications. + + :param int period_ms: the poll period in milliseconds. + """ + ... + def get_seed_languages(self) -> list[str]: """ Get all supported wallet seed languages. diff --git a/tests/test_monero_daemon_interface.py b/tests/test_monero_daemon_interface.py index 65f02e9..9802f80 100644 --- a/tests/test_monero_daemon_interface.py +++ b/tests/test_monero_daemon_interface.py @@ -1,7 +1,7 @@ import pytest import logging -from monero import MoneroDaemon, MoneroBan +from monero import MoneroDaemon, MoneroBan, MoneroDaemonListener from utils import BaseTestClass logger: logging.Logger = logging.getLogger("TestMoneroDaemonInterface") @@ -21,10 +21,22 @@ def daemon(self) -> MoneroDaemon: # Test interface calls + @pytest.mark.not_supported + def test_add_listener(self, daemon: MoneroDaemon) -> None: + daemon.add_listener(MoneroDaemonListener()) + + @pytest.mark.not_supported + def test_remove_listener(self, daemon: MoneroDaemon) -> None: + daemon.remove_listener(MoneroDaemonListener()) + @pytest.mark.not_supported def test_remove_listeners(self, daemon: MoneroDaemon) -> None: daemon.remove_listeners() + @pytest.mark.not_supported + def test_get_listeners(self, daemon: MoneroDaemon) -> None: + daemon.get_listeners() + @pytest.mark.not_supported def test_get_version(self, daemon: MoneroDaemon) -> None: daemon.get_version() @@ -261,6 +273,30 @@ def test_set_peer_ban(self, daemon: MoneroDaemon) -> None: def test_get_peer_ban(self, daemon: MoneroDaemon) -> None: daemon.get_peer_ban("") + @pytest.mark.not_supported + def test_get_download_limit(self, daemon: MoneroDaemon) -> None: + daemon.get_download_limit() + + @pytest.mark.not_supported + def test_set_download_limit(self, daemon: MoneroDaemon) -> None: + daemon.set_download_limit(100) + + @pytest.mark.not_supported + def test_reset_download_limit(self, daemon: MoneroDaemon) -> None: + daemon.reset_download_limit() + + @pytest.mark.not_supported + def test_get_upload_limit(self, daemon: MoneroDaemon) -> None: + daemon.get_upload_limit() + + @pytest.mark.not_supported + def test_set_upload_limit(self, daemon: MoneroDaemon) -> None: + daemon.set_upload_limit(100) + + @pytest.mark.not_supported + def test_reset_upload_limit(self, daemon: MoneroDaemon) -> None: + daemon.reset_upload_limit() + @pytest.mark.not_supported def test_start_mining(self, daemon: MoneroDaemon) -> None: daemon.start_mining("", 1, False, False) @@ -293,6 +329,10 @@ def test_pop_blocks(self, daemon: MoneroDaemon) -> None: def test_flush_cache(self, daemon: MoneroDaemon) -> None: daemon.flush_cache() + @pytest.mark.not_supported + def test_stop(self, daemon: MoneroDaemon) -> None: + daemon.stop() + @pytest.mark.not_supported def test_set_bootstrap_daemon(self, daemon: MoneroDaemon) -> None: daemon.set_bootstrap_daemon("") diff --git a/tests/test_monero_daemon_model.py b/tests/test_monero_daemon_model.py index fbdb872..02f7f7e 100644 --- a/tests/test_monero_daemon_model.py +++ b/tests/test_monero_daemon_model.py @@ -753,6 +753,55 @@ def test_block_merge(self) -> None: a.merge(b) assert a.hex == "deadbeef" + def test_block_header_merge_dispatches_to_block_merge(self) -> None: + a: MoneroBlock = MoneroBlock() + a.height = 100 + b: MoneroBlock = MoneroBlock() + b.height = 100 + b.hex = "deadbeef" # a.hex is unset -> merge fills the gap + + MoneroBlockHeader.merge(a, b) + assert a.hex == "deadbeef" + + def test_block_merge_adopts_other_miner_tx(self) -> None: + a: MoneroBlock = MoneroBlock() + a.height = 100 + # a.miner_tx is unset -> merge adopts other's miner tx and repoints its block at a + + b: MoneroBlock = MoneroBlock() + b.height = 100 + miner_tx: MoneroTx = MoneroTx() + miner_tx.hash = "a" * 64 + miner_tx.is_miner_tx = True + b.miner_tx = miner_tx + + a.merge(b) + assert a.miner_tx is not None + assert a.miner_tx is b.miner_tx + assert a.miner_tx.hash == "a" * 64 + assert a.miner_tx.block is a + + def test_block_merge_merges_distinct_miner_txs(self) -> None: + a: MoneroBlock = MoneroBlock() + a.height = 100 + miner_a: MoneroTx = MoneroTx() + miner_a.hash = "a" * 64 + miner_a.is_confirmed = True # required: tx merge() dereferences is_confirmed directly + a.miner_tx = miner_a + + b: MoneroBlock = MoneroBlock() + b.height = 100 + miner_b: MoneroTx = MoneroTx() + miner_b.hash = "a" * 64 + miner_b.is_confirmed = True + miner_b.num_confirmations = 5 # miner_a's num_confirmations is unset -> merge fills the gap + b.miner_tx = miner_b + + a.merge(b) + assert a.miner_tx is miner_a # kept in place, fields merged into it + assert a.miner_tx.num_confirmations == 5 + assert a.miner_tx.block is a + def test_block_merge_txs_with_unset_hash_are_kept_distinct(self) -> None: script: LiteralString = ( "import monero, sys\n" @@ -785,6 +834,25 @@ def test_tx_copy(self) -> None: assert copy is not tx assert copy.serialize() == tx.serialize() + def test_tx_copy_deep_copies_inputs(self) -> None: + tx: MoneroTx = MoneroTx() + tx.hash = "a" * 64 + + key_image: MoneroKeyImage = MoneroKeyImage() + key_image.hex = "1" * 64 + tx_input: MoneroOutput = MoneroOutput() + tx_input.tx = tx + tx_input.key_image = key_image + tx_input.amount = 1234 + tx.inputs = [tx_input] + + copy: MoneroTx = tx.copy() + assert len(copy.inputs) == 1 + assert copy.inputs[0] is not tx_input + assert copy.inputs[0].key_image.hex == "1" * 64 # type: ignore + assert copy.inputs[0].amount == 1234 + assert copy.inputs[0].tx is copy + def test_tx_merge(self) -> None: a: MoneroTx = MoneroTx() a.hash = "a" * 64 diff --git a/tests/test_monero_daemon_rpc.py b/tests/test_monero_daemon_rpc.py index 7535ae9..34bc53d 100644 --- a/tests/test_monero_daemon_rpc.py +++ b/tests/test_monero_daemon_rpc.py @@ -1303,6 +1303,7 @@ def test_set_log_hash_rate(self, daemon: MoneroDaemonRpc) -> None: # Can pop blocks from the top of the chain @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") @pytest.mark.skipif(Utils.REGTEST is False, reason="REGTEST disabled") + @pytest.mark.flaky(reruns=5, reruns_delay=5, only_rerun=[]) def test_pop_blocks(self, daemon: MoneroDaemonRpc) -> None: # stop mining so it doesn't race the pop, then restore the shared chain afterwards MiningUtils.try_stop_mining(daemon) diff --git a/tests/test_monero_utils.py b/tests/test_monero_utils.py index dd7fde8..5f59170 100644 --- a/tests/test_monero_utils.py +++ b/tests/test_monero_utils.py @@ -16,7 +16,7 @@ MoneroBlock, MoneroTxWallet, MoneroIncomingTransfer, MoneroOutputWallet, MoneroTx ) -from utils import AddressBook, KeysBook, WalletUtils, BaseTestClass, WalletErrorUtils +from utils import AddressBook, KeysBook, WalletUtils, BaseTestClass logger: logging.Logger = logging.getLogger("TestMoneroUtils") @@ -435,7 +435,9 @@ def test_payment_uri_invalid_network_type(self, config: TestMoneroUtils.Config) tx_config: MoneroTxConfig = WalletUtils.build_payment_uri_config(address) with pytest.raises(Exception) as exc_info: MoneroUtils.get_payment_uri(tx_config) - WalletErrorUtils.test_invalid_address_error(exc_info.value, address) + + # get_payment_uri() wraps make_uri()'s error with context, unlike e.g. validate_address() + assert str(exc_info.value) == f"Cannot make URI from supplied parameters: wrong address: {address}" # Test deprecated standalone payment id def test_payment_uri_deprecated_payment_uri(self, config: TestMoneroUtils.Config) -> None: @@ -444,7 +446,9 @@ def test_payment_uri_deprecated_payment_uri(self, config: TestMoneroUtils.Config tx_config.payment_id = "03284e41c342f03603284e41c342f03603284e41c342f03603284e41c342f036" with pytest.raises(Exception) as exc_info: MoneroUtils.get_payment_uri(tx_config, MoneroNetworkType.TESTNET) - WalletErrorUtils.test_deprecated_payment_id_error(exc_info.value) + + # get_payment_uri() wraps make_uri()'s error with context, unlike e.g. validate_address() + assert str(exc_info.value) == "Cannot make URI from supplied parameters: Standalone payment id deprecated, use integrated address instead" # Can get version def test_get_version(self) -> None: diff --git a/tests/test_monero_wallet_full.py b/tests/test_monero_wallet_full.py index 220342e..a0c61f5 100644 --- a/tests/test_monero_wallet_full.py +++ b/tests/test_monero_wallet_full.py @@ -1,5 +1,6 @@ import pytest import logging +import os from typing import Optional from typing_extensions import override @@ -16,7 +17,7 @@ TestUtils as Utils, StringUtils, BaseTestClass, AssertUtils, WalletUtils, WalletType, SyncSeedTester, SyncProgressTester, - WalletEqualityUtils, WalletErrorUtils + WalletEqualityUtils, WalletErrorUtils, MiningUtils ) from test_monero_wallet_common import BaseTestMoneroWallet @@ -656,6 +657,22 @@ def test_get_height_by_date_regtest(self, wallet: MoneroWallet) -> None: #endregion + #region Notification Tests + + # Can wait for the next block to be added and be notified of its height + @pytest.mark.skipif(Utils.TEST_NOTIFICATIONS is False, reason="TEST_NOTIFICATIONS disabled") + @pytest.mark.flaky(reruns=5, reruns_delay=10, only_rerun=[]) + def test_wait_for_next_block(self, wallet: MoneroWalletFull) -> None: + height_before: int = wallet.get_height() + MiningUtils.try_start_mining() + try: + height: int = wallet.wait_for_next_block() + assert height > height_before + finally: + MiningUtils.try_stop_mining() + + #endregion + #region Disabled Tests @pytest.mark.skip(reason="TODO disabled because importing key images deletes corresponding incoming transfers: #5812") @@ -695,6 +712,20 @@ def wallet(self) -> MoneroWalletFull: """Shared disconnected full wallet.""" return Utils.get_wallet_full_offline() + def test_get_default_ringdb_path_mainnet(self) -> None: + path: str = MoneroWalletFull.get_default_ringdb_path(MoneroNetworkType.MAINNET) + assert path.endswith(".shared-ringdb") + assert not path.endswith("testnet") + assert not path.endswith("stagenet") + + def test_get_default_ringdb_path_testnet(self) -> None: + path: str = MoneroWalletFull.get_default_ringdb_path(MoneroNetworkType.TESTNET) + assert path.endswith(os.path.join(".shared-ringdb", "testnet")) + + def test_get_default_ringdb_path_stagenet(self) -> None: + path: str = MoneroWalletFull.get_default_ringdb_path(MoneroNetworkType.STAGENET) + assert path.endswith(os.path.join(".shared-ringdb", "stagenet")) + # import_key_images guards unset key image fields def test_import_key_images_hex_not_defined(self, wallet: MoneroWalletFull) -> None: with pytest.raises(RuntimeError) as exc_info: diff --git a/tests/test_monero_wallet_interface.py b/tests/test_monero_wallet_interface.py index 1e27128..aabcd8d 100644 --- a/tests/test_monero_wallet_interface.py +++ b/tests/test_monero_wallet_interface.py @@ -345,6 +345,30 @@ def test_relay_tx(self, wallet: MoneroWallet) -> None: def test_relay_txs(self, wallet: MoneroWallet) -> None: wallet.relay_txs([]) + def test_relay_txs_none_tx_raises(self, wallet: MoneroWallet) -> None: + with pytest.raises(RuntimeError) as exc_info: + wallet.relay_txs([None]) # type: ignore + assert str(exc_info.value) == "Tx metadata is not initialized" + + def test_relay_txs_uninitialized_metadata_raises(self, wallet: MoneroWallet) -> None: + tx: MoneroTxWallet = MoneroTxWallet() + with pytest.raises(RuntimeError) as exc_info: + wallet.relay_txs([tx]) + assert str(exc_info.value) == "Tx metadata is not initialized" + + def test_relay_txs_empty_metadata_raises(self, wallet: MoneroWallet) -> None: + tx: MoneroTxWallet = MoneroTxWallet() + tx.metadata = "" + with pytest.raises(RuntimeError) as exc_info: + wallet.relay_txs([tx]) + assert str(exc_info.value) == "Tx metadata is not initialized" + + @pytest.mark.not_supported + def test_relay_txs_with_metadata(self, wallet: MoneroWallet) -> None: + tx: MoneroTxWallet = MoneroTxWallet() + tx.metadata = StringUtils.get_random_string() + wallet.relay_txs([tx]) + @pytest.mark.not_supported def test_describe_tx_set(self, wallet: MoneroWallet) -> None: wallet.describe_tx_set(MoneroTxSet()) @@ -397,6 +421,10 @@ def test_check_reserve_proof(self, wallet: MoneroWallet) -> None: def test_get_tx_note(self, wallet: MoneroWallet) -> None: wallet.get_tx_note("") + @pytest.mark.not_supported + def test_get_tx_notes(self, wallet: MoneroWallet) -> None: + wallet.get_tx_notes(["", ""]) + @pytest.mark.not_supported def test_set_tx_note(self, wallet: MoneroWallet) -> None: wallet.set_tx_note("", "") @@ -533,4 +561,8 @@ def test_save(self, wallet: MoneroWallet) -> None: def test_close(self, wallet: MoneroWallet) -> None: wallet.close() + @pytest.mark.not_supported + def test_is_closed(self, wallet: MoneroWallet) -> None: + wallet.is_closed() + #endregion diff --git a/tests/test_monero_wallet_keys.py b/tests/test_monero_wallet_keys.py index e422b1e..479c9a8 100644 --- a/tests/test_monero_wallet_keys.py +++ b/tests/test_monero_wallet_keys.py @@ -118,11 +118,6 @@ def test_send(self, wallet: MoneroWallet) -> None: def test_send_with_payment_id(self, wallet: MoneroWallet) -> None: return super().test_send_with_payment_id(wallet) - @pytest.mark.not_implemented - @override - def test_decode_integrated_address(self, wallet: MoneroWallet) -> None: - return super().test_decode_integrated_address(wallet) - @pytest.mark.not_supported @override def test_send_split(self, wallet: MoneroWallet) -> None: @@ -299,11 +294,6 @@ def test_import_key_images(self, wallet: MoneroWallet) -> None: def test_set_attributes(self, wallet: MoneroWallet) -> None: return super().test_set_attributes(wallet) - @pytest.mark.not_supported - @override - def test_get_payment_uri(self, wallet: MoneroWallet) -> None: - return super().test_get_payment_uri(wallet) - @pytest.mark.not_supported @override def test_mining(self, daemon: MoneroDaemonRpc, wallet: MoneroWallet) -> None: @@ -354,11 +344,6 @@ def test_scan_txs(self, wallet: MoneroWallet) -> None: def test_get_default_fee_priority(self, wallet: MoneroWallet) -> None: return super().test_get_default_fee_priority(wallet) - @pytest.mark.not_implemented - @override - def test_sign_and_verify_messages(self, wallet: MoneroWallet) -> None: - return super().test_sign_and_verify_messages(wallet) - @pytest.mark.not_supported @override def test_freeze_outputs(self, wallet: MoneroWallet) -> None: @@ -778,6 +763,11 @@ def test_get_account(self, wallet: MoneroWallet) -> None: retrieved = wallet.get_account(account.index) retrieved.subaddresses = wallet.get_subaddresses(account.index, self._subaddress_indices) + def test_get_account_with_subaddresses_not_supported(self, wallet: MoneroWallet) -> None: + with pytest.raises(RuntimeError) as exc_info: + wallet.get_account(0, True) + assert str(exc_info.value) == "monero_wallet_keys::get_account(account_idx, include_subaddresses) include_subaddresses must be false" + @override def test_get_accounts_without_subaddresses(self, wallet: MoneroWallet) -> None: accounts: list[MoneroAccount] = self._get_test_accounts(wallet) diff --git a/tests/test_monero_wallet_model.py b/tests/test_monero_wallet_model.py index 33841ba..b0deab6 100644 --- a/tests/test_monero_wallet_model.py +++ b/tests/test_monero_wallet_model.py @@ -1149,6 +1149,14 @@ def test_address_book_entry_deserialize(self) -> None: entry.payment_id = "e" * 16 AssertUtils.assert_serialization_integrity(entry) + def test_address_book_entry_constructor_with_payment_id_deserialize(self) -> None: + entry: MoneroAddressBookEntry = MoneroAddressBookEntry(0, TestUtils.ADDRESS, "friend", "e" * 16) + assert entry.index == 0 + assert entry.address == TestUtils.ADDRESS + assert entry.description == "friend" + assert entry.payment_id == "e" * 16 + AssertUtils.assert_serialization_integrity(entry) + def test_account_tag_deserialize(self) -> None: tag: MoneroAccountTag = MoneroAccountTag() tag.tag = "savings" @@ -1156,6 +1164,13 @@ def test_account_tag_deserialize(self) -> None: tag.account_indices = [0, 1, 2] AssertUtils.assert_serialization_integrity(tag) + def test_account_tag_constructor_deserialize(self) -> None: + tag: MoneroAccountTag = MoneroAccountTag("savings", "Savings accounts") + assert tag.tag == "savings" + assert tag.label == "Savings accounts" + assert len(tag.account_indices) == 0 + AssertUtils.assert_serialization_integrity(tag) + def test_tx_set_deserialize(self) -> None: tx_set: MoneroTxSet = MoneroTxSet() tx_set.unsigned_tx_hex = "deadbeef" diff --git a/tests/test_monero_wallet_rpc.py b/tests/test_monero_wallet_rpc.py index c5c6574..1be9793 100644 --- a/tests/test_monero_wallet_rpc.py +++ b/tests/test_monero_wallet_rpc.py @@ -4,13 +4,13 @@ from monero import ( MoneroWallet, MoneroWalletConfig, MoneroWalletRpc, MoneroAccount, MoneroError, MoneroDaemonRpc, - MoneroTxWallet, MoneroUtils + MoneroTxWallet, MoneroUtils, MoneroSubaddress ) from typing_extensions import override from utils import ( TestUtils as Utils, StringUtils, WalletType, - WalletNotificationCollector, WalletErrorUtils + WalletNotificationCollector, WalletErrorUtils, WalletUtils, AssertUtils ) from test_monero_wallet_common import BaseTestMoneroWallet @@ -105,6 +105,32 @@ def test_sync_progress(self, wallet: MoneroWalletRpc) -> None: wallet.sync(0, listener) assert str(exc_info.value) == ERR_MSG + # Can set the poll period for wallet notifications + @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") + def test_set_poll_period_in_ms(self, wallet: MoneroWalletRpc) -> None: + try: + wallet.set_poll_period_in_ms(1000) + finally: + # restore the configured period so later tests poll at the expected rate + wallet.set_poll_period_in_ms(Utils.SYNC_PERIOD_IN_MS) + + # Can get a subaddress via monero_wallet_rpc's + @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") + def test_get_subaddress_rpc(self, wallet: MoneroWalletRpc) -> None: + account_idx: int = 0 + subaddress_idx: int = 0 + + subaddress: MoneroSubaddress = wallet.get_subaddress(account_idx, subaddress_idx) + WalletUtils.test_subaddress(subaddress) + assert subaddress.account_index == account_idx + assert subaddress.index == subaddress_idx + + plural: list[MoneroSubaddress] = wallet.get_subaddresses(account_idx, [subaddress_idx]) + assert len(plural) == 1 + AssertUtils.assert_equals(subaddress, plural[0]) + + assert subaddress.address == wallet.get_address(account_idx, subaddress_idx) + @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") @override def test_get_subaddress_address_out_of_range(self, wallet: MoneroWallet) -> None: