diff --git a/.gitignore b/.gitignore index a5648ce..6116492 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ _CPack_Packages *.deb *egg-info *dist-info -test_wallets \ No newline at end of file +test_wallets +.github/instructions/codacy.instructions.md \ No newline at end of file diff --git a/README.md b/README.md index df36c57..e74c2a9 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ > [!CAUTION] > > monero-python is without funding and needs tests to reach a stable release for production environments, expect bugs and breaking changes. -> There is a [Monero CSS proposal](https://repo.getmonero.org/monero-project/ccs-proposals/-/merge_requests/598) +> There is a [Monero CCS proposal](https://repo.getmonero.org/monero-project/ccs-proposals/-/merge_requests/598) > for maintenance of this library, check it out! A Python library for creating Monero applications using RPC and Python bindings to [monero v0.18.4.3 'Fluorine Fermi'](https://github.com/monero-project/monero/tree/v0.18.4.3). diff --git a/setup.py b/setup.py index 9608fb7..f9420ea 100644 --- a/setup.py +++ b/setup.py @@ -8,47 +8,47 @@ this_dir = Path(__file__).parent.resolve() ext_modules = [ - Pybind11Extension( - 'monero', - [ - 'src/cpp/py_monero.cpp' - ], - include_dirs=[ - pybind11.get_include(), - str(this_dir / 'external' / 'monero-cpp' / 'src'), - str(this_dir / 'external' / 'monero-cpp' / 'external' / 'monero-project' / 'src'), - str(this_dir / 'external' / 'monero-cpp' / 'external' / 'monero-project' / 'contrib' / 'epee' / 'include'), - str(this_dir / 'external' / 'monero-cpp' / 'external' / 'monero-project' / 'external'), - str(this_dir / 'external' / 'monero-cpp' / 'external' / 'monero-project' / 'external' / 'easylogging++'), - str(this_dir / 'external' / 'monero-cpp' / 'external' / 'monero-project' / 'external' / 'rapidjson' / 'include'), - str(this_dir / 'src' / 'cpp'), - str(this_dir / 'src' / 'cpp' / 'common'), - str(this_dir / 'src' / 'cpp' / 'daemon'), - str(this_dir / 'src' / 'cpp' / 'wallet') - ], - library_dirs=[ - str(this_dir / 'external' / 'monero-cpp' / 'build') - ], - libraries=['monero-cpp'], - language='c++', - extra_compile_args=['/std:c++17'] if sys.platform == "win32" else ['-std=c++17'], - ), + Pybind11Extension( + 'monero', + [ + 'src/cpp/py_monero.cpp' + ], + include_dirs=[ + pybind11.get_include(), + str(this_dir / 'external' / 'monero-cpp' / 'src'), + str(this_dir / 'external' / 'monero-cpp' / 'external' / 'monero-project' / 'src'), + str(this_dir / 'external' / 'monero-cpp' / 'external' / 'monero-project' / 'contrib' / 'epee' / 'include'), + str(this_dir / 'external' / 'monero-cpp' / 'external' / 'monero-project' / 'external'), + str(this_dir / 'external' / 'monero-cpp' / 'external' / 'monero-project' / 'external' / 'easylogging++'), + str(this_dir / 'external' / 'monero-cpp' / 'external' / 'monero-project' / 'external' / 'rapidjson' / 'include'), + str(this_dir / 'src' / 'cpp'), + str(this_dir / 'src' / 'cpp' / 'common'), + str(this_dir / 'src' / 'cpp' / 'daemon'), + str(this_dir / 'src' / 'cpp' / 'wallet') + ], + library_dirs=[ + str(this_dir / 'external' / 'monero-cpp' / 'build') + ], + libraries=['monero-cpp'], + language='c++', + extra_compile_args=['/std:c++17'] if sys.platform == "win32" else ['-std=c++17'], + ), ] setup( - name='monero', - version='0.0.1', - author='everoddandeven', - author_email="everoddandeven@protonmail.com", - maintainer='everoddandeven', - maintainer_email='everoddandeven@protonmail.com', - license="MIT", - url='https://github.com/everoddandeven/monero-python', - download_url="https://github.com/everoddandeven/monero-python/releases", - description='A Python library for using Monero.', - long_description='Python bindings for monero-cpp.', - keywords=["monero", "monero-python", "python", "bindings", "pybind11"], - ext_modules=ext_modules, - install_requires=['pybind11>=2.12.0'], - cmdclass={"build_ext": build_ext} + name='monero', + version='0.0.1', + author='everoddandeven', + author_email="everoddandeven@protonmail.com", + maintainer='everoddandeven', + maintainer_email='everoddandeven@protonmail.com', + license="MIT", + url='https://github.com/everoddandeven/monero-python', + download_url="https://github.com/everoddandeven/monero-python/releases", + description='A Python library for using Monero.', + long_description='Python bindings for monero-cpp.', + keywords=["monero", "monero-python", "python", "bindings", "pybind11"], + ext_modules=ext_modules, + install_requires=['pybind11>=2.12.0'], + cmdclass={"build_ext": build_ext} ) diff --git a/src/python/monero_connection_manager_listener.pyi b/src/python/monero_connection_manager_listener.pyi index e7c0092..397e3d1 100644 --- a/src/python/monero_connection_manager_listener.pyi +++ b/src/python/monero_connection_manager_listener.pyi @@ -1,3 +1,4 @@ +from typing import Optional from .monero_rpc_connection import MoneroRpcConnection @@ -8,7 +9,7 @@ class MoneroConnectionManagerListener: def __init__(self) -> None: """Initialize a connection manager listener.""" ... - def on_connection_changed(self, connection: MoneroRpcConnection) -> None: + def on_connection_changed(self, connection: Optional[MoneroRpcConnection]) -> None: """ Notified on connection change events. diff --git a/src/python/monero_rpc_error.pyi b/src/python/monero_rpc_error.pyi index 255aa0a..4ec4a3a 100644 --- a/src/python/monero_rpc_error.pyi +++ b/src/python/monero_rpc_error.pyi @@ -2,7 +2,7 @@ class MoneroRpcError(RuntimeError): """ Exception when interacting with the Monero daemon or wallet RPC API. """ - def __init__(self, code: int, aMessage: str): + def __init__(self, code: int, aMessage: str) -> None: ... def get_code(self) -> int: """ diff --git a/src/python/monero_utils.pyi b/src/python/monero_utils.pyi index 9570edb..0ae8d63 100644 --- a/src/python/monero_utils.pyi +++ b/src/python/monero_utils.pyi @@ -1,3 +1,4 @@ +from typing import Any from .monero_output_wallet import MoneroOutputWallet from .monero_block import MoneroBlock from .monero_transfer import MoneroTransfer @@ -21,7 +22,7 @@ class MoneroUtils: """ ... @staticmethod - def binary_to_dict(bin: bytes) -> dict: + def binary_to_dict(bin: bytes) -> dict[Any, Any]: """ Deserialize a dictionary from binary format. @@ -48,7 +49,7 @@ class MoneroUtils: """ ... @staticmethod - def dict_to_binary(dictionary: dict) -> bytes: + def dict_to_binary(dictionary: dict[Any, Any]) -> bytes: """ Converts a dictionary into binary format. diff --git a/tests/test_monero_connection_manager.py b/tests/test_monero_connection_manager.py index 9bf7694..50dad71 100644 --- a/tests/test_monero_connection_manager.py +++ b/tests/test_monero_connection_manager.py @@ -2,335 +2,334 @@ from typing import Optional from monero import ( - MoneroWalletRpc, MoneroConnectionManager, MoneroRpcConnection, MoneroConnectionPollType + MoneroWalletRpc, MoneroConnectionManager, MoneroRpcConnection, MoneroConnectionPollType ) from utils import ConnectionChangeCollector, MoneroTestUtils as Utils class TestMoneroConnectionManager: - def test_connection_manager(self): - walletRpcs: list[MoneroWalletRpc] = [] - connectionManager: Optional[MoneroConnectionManager] = None - try: - i: int = 0 - - while i < 5: - walletRpcs.append(Utils.start_wallet_rpc_process()) - i += 1 - # start monero-wallet-rpc instances as test server connections (can also use monerod servers) - - # create connection manager - connectionManager = MoneroConnectionManager() - - # listen for changes - listener = ConnectionChangeCollector() - connectionManager.add_listener(listener) - - # add prioritized connections - connection: Optional[MoneroRpcConnection] = walletRpcs[4].get_daemon_connection() - assert connection is not None - connection.priority = 1 - connectionManager.add_connection(connection) - connection = walletRpcs[2].get_daemon_connection() - assert connection is not None - connection.priority = 2 - connectionManager.add_connection(connection) - connection = walletRpcs[3].get_daemon_connection() - assert connection is not None - connection.priority = 2 - connectionManager.add_connection(connection) - connection = walletRpcs[0].get_daemon_connection() - assert connection is not None - connectionManager.add_connection(connection) # default priority is lowest - connection = walletRpcs[1].get_daemon_connection() - assert connection is not None - assert connection.uri is not None - connectionManager.add_connection(MoneroRpcConnection(connection.uri)) # test unauthenticated - - # test connections and order - orderedConnections: list[MoneroRpcConnection] = connectionManager.get_connections() - Utils.assert_true(orderedConnections[0] == walletRpcs[4].get_daemon_connection()) - Utils.assert_true(orderedConnections[1] == walletRpcs[2].get_daemon_connection()) - Utils.assert_true(orderedConnections[2] == walletRpcs[3].get_daemon_connection()) - Utils.assert_true(orderedConnections[3] == walletRpcs[0].get_daemon_connection()) - connection = walletRpcs[1].get_daemon_connection() - assert connection is not None - Utils.assert_equals(orderedConnections[4].uri, connection.uri) - - for connection in orderedConnections: - assert connection.is_online() is None + def test_connection_manager(self): + wallet_rpcs: list[MoneroWalletRpc] = [] + connection_manager: Optional[MoneroConnectionManager] = None + try: + i: int = 0 + + while i < 5: + wallet_rpcs.append(Utils.start_wallet_rpc_process()) + i += 1 + # start monero-wallet-rpc instances as test server connections (can also use monerod servers) + + # create connection manager + connection_manager = MoneroConnectionManager() + + # listen for changes + listener = ConnectionChangeCollector() + connection_manager.add_listener(listener) + + # add prioritized connections + connection: Optional[MoneroRpcConnection] = wallet_rpcs[4].get_daemon_connection() + assert connection is not None + connection.priority = 1 + connection_manager.add_connection(connection) + connection = wallet_rpcs[2].get_daemon_connection() + assert connection is not None + connection.priority = 2 + connection_manager.add_connection(connection) + connection = wallet_rpcs[3].get_daemon_connection() + assert connection is not None + connection.priority = 2 + connection_manager.add_connection(connection) + connection = wallet_rpcs[0].get_daemon_connection() + assert connection is not None + connection_manager.add_connection(connection) # default priority is lowest + connection = wallet_rpcs[1].get_daemon_connection() + assert connection is not None + assert connection.uri is not None + connection_manager.add_connection(MoneroRpcConnection(connection.uri)) # test unauthenticated + + # test connections and order + ordered_connections: list[MoneroRpcConnection] = connection_manager.get_connections() + Utils.assert_true(ordered_connections[0] == wallet_rpcs[4].get_daemon_connection()) + Utils.assert_true(ordered_connections[1] == wallet_rpcs[2].get_daemon_connection()) + Utils.assert_true(ordered_connections[2] == wallet_rpcs[3].get_daemon_connection()) + Utils.assert_true(ordered_connections[3] == wallet_rpcs[0].get_daemon_connection()) + connection = wallet_rpcs[1].get_daemon_connection() + assert connection is not None + Utils.assert_equals(ordered_connections[4].uri, connection.uri) + + for connection in ordered_connections: + assert connection.is_online() is None - # test getting connection by uri - connection = walletRpcs[0].get_daemon_connection() - assert connection is not None - assert connection.uri is not None - Utils.assert_true(connectionManager.has_connection(connection.uri)) - Utils.assert_true(connectionManager.get_connection_by_uri(connection.uri) == walletRpcs[0].get_daemon_connection()) - - # test unknown connection - numExpectedChanges: int = 0 - connectionManager.set_connection(orderedConnections[0]) - Utils.assert_equals(None, connectionManager.is_connected()) - numExpectedChanges += 1 - Utils.assert_equals(numExpectedChanges, listener.changedConnections.size()) + # test getting connection by uri + connection = wallet_rpcs[0].get_daemon_connection() + assert connection is not None + assert connection.uri is not None + Utils.assert_true(connection_manager.has_connection(connection.uri)) + Utils.assert_true(connection_manager.get_connection_by_uri(connection.uri) == wallet_rpcs[0].get_daemon_connection()) + + # test unknown connection + num_expected_changes: int = 0 + connection_manager.set_connection(ordered_connections[0]) + Utils.assert_equals(None, connection_manager.is_connected()) + num_expected_changes += 1 + Utils.assert_equals(num_expected_changes, listener.changed_connections.size()) - # auto connect to best available connection - connectionManager.start_polling(Utils.SYNC_PERIOD_IN_MS) - Utils.wait_for(Utils.AUTO_CONNECT_TIMEOUT_MS) - Utils.assert_true(connectionManager.is_connected()) - connection = connectionManager.get_connection() - assert connection is not None - Utils.assert_true(connection.is_online()) - Utils.assert_true(connection == walletRpcs[4].get_daemon_connection()) - numExpectedChanges += 1 - Utils.assert_equals(numExpectedChanges, listener.changedConnections.size()) - Utils.assert_true(listener.changedConnections.get(listener.changedConnections.size() - 1) == connection) - connectionManager.set_auto_switch(False) - connectionManager.stop_polling() - connectionManager.disconnect() - numExpectedChanges += 1 - Utils.assert_equals(numExpectedChanges, listener.changedConnections.size()) - Utils.assert_true(listener.changedConnections.get(listener.changedConnections.size() - 1) == None) - - # start periodically checking connection without auto switch - connectionManager.start_polling(Utils.SYNC_PERIOD_IN_MS, False) - - # connect to best available connection in order of priority and response time - connection = connectionManager.get_best_available_connection() - connectionManager.set_connection(connection) - Utils.assert_true(connection == walletRpcs[4].get_daemon_connection()) - Utils.assert_true(connection.is_online()) - Utils.assert_true(connection.is_authenticated()) - numExpectedChanges += 1 - Utils.assert_equals(numExpectedChanges, listener.changedConnections.size()) - Utils.assert_true(listener.changedConnections.get(listener.changedConnections.size() - 1) == connection) - - # test connections and order - orderedConnections = connectionManager.get_connections() - Utils.assert_true(orderedConnections[0] == walletRpcs[4].get_daemon_connection()) - Utils.assert_true(orderedConnections[1] == walletRpcs[2].get_daemon_connection()) - Utils.assert_true(orderedConnections[2] == walletRpcs[3].get_daemon_connection()) - Utils.assert_true(orderedConnections[3] == walletRpcs[0].get_daemon_connection()) - connection = walletRpcs[1].get_daemon_connection() - assert connection is not None - Utils.assert_equals(orderedConnections[4].uri, connection.uri) - for orderedConnection in orderedConnections: - Utils.assert_is_none(orderedConnection.is_online()) - - # shut down prioritized servers - Utils.stop_wallet_rpc_process(walletRpcs[2]) - Utils.stop_wallet_rpc_process(walletRpcs[3]) - Utils.stop_wallet_rpc_process(walletRpcs[4]) - Utils.wait_for(Utils.SYNC_PERIOD_IN_MS + 100) # allow time to poll - Utils.assert_false(connectionManager.is_connected()) - connection = connectionManager.get_connection() - assert connection is not None - Utils.assert_false(connection.is_online()) - Utils.assert_is_none(connection.is_authenticated()) - numExpectedChanges += 1 - Utils.assert_equals(numExpectedChanges, listener.changedConnections.size()) - Utils.assert_true(listener.changedConnections.get(listener.changedConnections.size() - 1) == connectionManager.get_connection()) - - # test connection order - orderedConnections = connectionManager.get_connections() - Utils.assert_true(orderedConnections[0] == walletRpcs[4].get_daemon_connection()) - Utils.assert_true(orderedConnections[1] == walletRpcs[0].get_daemon_connection()) - connection = walletRpcs[1].get_daemon_connection() - assert connection is not None - Utils.assert_equals(orderedConnections[2].uri, connection.uri) - Utils.assert_true(orderedConnections[3] == walletRpcs[2].get_daemon_connection()) - Utils.assert_true(orderedConnections[4] == walletRpcs[3].get_daemon_connection()) - - # check all connections - connectionManager.check_connections() - - # test connection order - orderedConnections = connectionManager.get_connections() - Utils.assert_true(orderedConnections[0] == walletRpcs[4].get_daemon_connection()) - Utils.assert_true(orderedConnections[1] == walletRpcs[0].get_daemon_connection()) - connection = walletRpcs[1].get_daemon_connection() - assert connection is not None - Utils.assert_equals(orderedConnections[2].uri, connection.uri) - Utils.assert_true(orderedConnections[3] == walletRpcs[2].get_daemon_connection()) - Utils.assert_true(orderedConnections[4] == walletRpcs[3].get_daemon_connection()) - - # test online and authentication status - for orderedConnection in orderedConnections: - is_online = orderedConnection.is_online() - is_authenticated = orderedConnection.is_authenticated() - if (i == 1 or i == 2): - Utils.assert_true(is_online) - else: - Utils.assert_false(is_online) - if (i == 1): - Utils.assert_true(is_authenticated) - elif (i == 2): - Utils.assert_false(is_authenticated) - else: - Utils.assert_is_none(is_authenticated) - - # test auto switch when disconnected - connectionManager.set_auto_switch(True) - Utils.wait_for(Utils.SYNC_PERIOD_IN_MS + 100) - Utils.assert_true(connectionManager.is_connected()) - connection = connectionManager.get_connection() - assert connection is not None - Utils.assert_true(connection.is_online()) - Utils.assert_true(connection == walletRpcs[0].get_daemon_connection()) - numExpectedChanges += 1 - Utils.assert_equals(numExpectedChanges, listener.changedConnections.size()) - Utils.assert_true(listener.changedConnections.get(listener.changedConnections.size() - 1) == connection) - - # test connection order - orderedConnections = connectionManager.get_connections() - Utils.assert_true(orderedConnections[0] == connection) - Utils.assert_true(orderedConnections[0] == walletRpcs[0].get_daemon_connection()) - connection = walletRpcs[1].get_daemon_connection() - assert connection is not None - Utils.assert_equals(orderedConnections[1].uri, connection.uri) - Utils.assert_true(orderedConnections[2] == walletRpcs[4].get_daemon_connection()) - Utils.assert_true(orderedConnections[3] == walletRpcs[2].get_daemon_connection()) - Utils.assert_true(orderedConnections[4] == walletRpcs[3].get_daemon_connection()) - - # connect to specific endpoint without authentication - connection = orderedConnections[1] - Utils.assert_false(connection.is_authenticated()) - connectionManager.set_connection(connection) - Utils.assert_false(connectionManager.is_connected()) - numExpectedChanges += 1 - Utils.assert_equals(numExpectedChanges, listener.changedConnections.size()) - - # connect to specific endpoint with authentication - orderedConnections[1].set_credentials("rpc_user", "abc123") - connectionManager.check_connection() - connection = connectionManager.get_connection() - assert connection is not None - connection = walletRpcs[1].get_daemon_connection() - assert connection is not None - Utils.assert_equals(connection.uri, connection.uri) - Utils.assert_true(connection.is_online()) - Utils.assert_true(connection.is_authenticated()) - numExpectedChanges += 1 - Utils.assert_equals(numExpectedChanges, listener.changedConnections.size()) - Utils.assert_true(listener.changedConnections.get(listener.changedConnections.size() - 1) == connection) - - # test connection order - orderedConnections = connectionManager.get_connections() - Utils.assert_true(orderedConnections[0] == connectionManager.get_connection()) - connection = walletRpcs[1].get_daemon_connection() - assert connection is not None - Utils.assert_equals(orderedConnections[0].uri, connection.uri) - Utils.assert_true(orderedConnections[1] == walletRpcs[0].get_daemon_connection()) - Utils.assert_true(orderedConnections[2] == walletRpcs[4].get_daemon_connection()) - Utils.assert_true(orderedConnections[3] == walletRpcs[2].get_daemon_connection()) - Utils.assert_true(orderedConnections[4] == walletRpcs[3].get_daemon_connection()) - - first: bool = True - for orderedConnection in orderedConnections: - if (i <= 1): - Utils.assert_true(orderedConnection.is_online() if first else not orderedConnection.is_online() ) + # auto connect to best available connection + connection_manager.start_polling(Utils.SYNC_PERIOD_IN_MS) + Utils.wait_for(Utils.AUTO_CONNECT_TIMEOUT_MS) + Utils.assert_true(connection_manager.is_connected()) + connection = connection_manager.get_connection() + assert connection is not None + Utils.assert_true(connection.is_online()) + Utils.assert_true(connection == wallet_rpcs[4].get_daemon_connection()) + num_expected_changes += 1 + Utils.assert_equals(num_expected_changes, listener.changed_connections.size()) + Utils.assert_true(listener.changed_connections.get(listener.changed_connections.size() - 1) == connection) + connection_manager.set_auto_switch(False) + connection_manager.stop_polling() + connection_manager.disconnect() + num_expected_changes += 1 + Utils.assert_equals(num_expected_changes, listener.changed_connections.size()) + Utils.assert_true(listener.changed_connections.get(listener.changed_connections.size() - 1) == None) + + # start periodically checking connection without auto switch + connection_manager.start_polling(Utils.SYNC_PERIOD_IN_MS, False) + + # connect to best available connection in order of priority and response time + connection = connection_manager.get_best_available_connection() + connection_manager.set_connection(connection) + Utils.assert_true(connection == wallet_rpcs[4].get_daemon_connection()) + Utils.assert_true(connection.is_online()) + Utils.assert_true(connection.is_authenticated()) + num_expected_changes += 1 + Utils.assert_equals(num_expected_changes, listener.changed_connections.size()) + Utils.assert_true(listener.changed_connections.get(listener.changed_connections.size() - 1) == connection) + + # test connections and order + ordered_connections = connection_manager.get_connections() + Utils.assert_true(ordered_connections[0] == wallet_rpcs[4].get_daemon_connection()) + Utils.assert_true(ordered_connections[1] == wallet_rpcs[2].get_daemon_connection()) + Utils.assert_true(ordered_connections[2] == wallet_rpcs[3].get_daemon_connection()) + Utils.assert_true(ordered_connections[3] == wallet_rpcs[0].get_daemon_connection()) + connection = wallet_rpcs[1].get_daemon_connection() + assert connection is not None + Utils.assert_equals(ordered_connections[4].uri, connection.uri) + for orderedConnection in ordered_connections: + Utils.assert_is_none(orderedConnection.is_online()) + + # shut down prioritized servers + Utils.stop_wallet_rpc_process(wallet_rpcs[2]) + Utils.stop_wallet_rpc_process(wallet_rpcs[3]) + Utils.stop_wallet_rpc_process(wallet_rpcs[4]) + Utils.wait_for(Utils.SYNC_PERIOD_IN_MS + 100) # allow time to poll + Utils.assert_false(connection_manager.is_connected()) + connection = connection_manager.get_connection() + assert connection is not None + Utils.assert_false(connection.is_online()) + Utils.assert_is_none(connection.is_authenticated()) + num_expected_changes += 1 + Utils.assert_equals(num_expected_changes, listener.changed_connections.size()) + Utils.assert_true(listener.changed_connections.get(listener.changed_connections.size() - 1) == connection_manager.get_connection()) + + # test connection order + ordered_connections = connection_manager.get_connections() + Utils.assert_true(ordered_connections[0] == wallet_rpcs[4].get_daemon_connection()) + Utils.assert_true(ordered_connections[1] == wallet_rpcs[0].get_daemon_connection()) + connection = wallet_rpcs[1].get_daemon_connection() + assert connection is not None + Utils.assert_equals(ordered_connections[2].uri, connection.uri) + Utils.assert_true(ordered_connections[3] == wallet_rpcs[2].get_daemon_connection()) + Utils.assert_true(ordered_connections[4] == wallet_rpcs[3].get_daemon_connection()) + + # check all connections + connection_manager.check_connections() + + # test connection order + ordered_connections = connection_manager.get_connections() + Utils.assert_true(ordered_connections[0] == wallet_rpcs[4].get_daemon_connection()) + Utils.assert_true(ordered_connections[1] == wallet_rpcs[0].get_daemon_connection()) + connection = wallet_rpcs[1].get_daemon_connection() + assert connection is not None + Utils.assert_equals(ordered_connections[2].uri, connection.uri) + Utils.assert_true(ordered_connections[3] == wallet_rpcs[2].get_daemon_connection()) + Utils.assert_true(ordered_connections[4] == wallet_rpcs[3].get_daemon_connection()) + + # test online and authentication status + for orderedConnection in ordered_connections: + is_online = orderedConnection.is_online() + is_authenticated = orderedConnection.is_authenticated() + if (i == 1 or i == 2): + Utils.assert_true(is_online) + else: + Utils.assert_false(is_online) + if (i == 1): + Utils.assert_true(is_authenticated) + elif (i == 2): + Utils.assert_false(is_authenticated) + else: + Utils.assert_is_none(is_authenticated) + + # test auto switch when disconnected + connection_manager.set_auto_switch(True) + Utils.wait_for(Utils.SYNC_PERIOD_IN_MS + 100) + Utils.assert_true(connection_manager.is_connected()) + connection = connection_manager.get_connection() + assert connection is not None + Utils.assert_true(connection.is_online()) + Utils.assert_true(connection == wallet_rpcs[0].get_daemon_connection()) + num_expected_changes += 1 + Utils.assert_equals(num_expected_changes, listener.changed_connections.size()) + Utils.assert_true(listener.changed_connections.get(listener.changed_connections.size() - 1) == connection) + + # test connection order + ordered_connections = connection_manager.get_connections() + Utils.assert_true(ordered_connections[0] == connection) + Utils.assert_true(ordered_connections[0] == wallet_rpcs[0].get_daemon_connection()) + connection = wallet_rpcs[1].get_daemon_connection() + assert connection is not None + Utils.assert_equals(ordered_connections[1].uri, connection.uri) + Utils.assert_true(ordered_connections[2] == wallet_rpcs[4].get_daemon_connection()) + Utils.assert_true(ordered_connections[3] == wallet_rpcs[2].get_daemon_connection()) + Utils.assert_true(ordered_connections[4] == wallet_rpcs[3].get_daemon_connection()) + + # connect to specific endpoint without authentication + connection = ordered_connections[1] + Utils.assert_false(connection.is_authenticated()) + connection_manager.set_connection(connection) + Utils.assert_false(connection_manager.is_connected()) + num_expected_changes += 1 + Utils.assert_equals(num_expected_changes, listener.changed_connections.size()) + + # connect to specific endpoint with authentication + ordered_connections[1].set_credentials("rpc_user", "abc123") + connection_manager.check_connection() + connection = connection_manager.get_connection() + assert connection is not None + connection = wallet_rpcs[1].get_daemon_connection() + assert connection is not None + Utils.assert_equals(connection.uri, connection.uri) + Utils.assert_true(connection.is_online()) + Utils.assert_true(connection.is_authenticated()) + num_expected_changes += 1 + Utils.assert_equals(num_expected_changes, listener.changed_connections.size()) + Utils.assert_true(listener.changed_connections.get(listener.changed_connections.size() - 1) == connection) + + # test connection order + ordered_connections = connection_manager.get_connections() + Utils.assert_true(ordered_connections[0] == connection_manager.get_connection()) + connection = wallet_rpcs[1].get_daemon_connection() + assert connection is not None + Utils.assert_equals(ordered_connections[0].uri, connection.uri) + Utils.assert_true(ordered_connections[1] == wallet_rpcs[0].get_daemon_connection()) + Utils.assert_true(ordered_connections[2] == wallet_rpcs[4].get_daemon_connection()) + Utils.assert_true(ordered_connections[3] == wallet_rpcs[2].get_daemon_connection()) + Utils.assert_true(ordered_connections[4] == wallet_rpcs[3].get_daemon_connection()) + + first: bool = True + for orderedConnection in ordered_connections: + if (i <= 1): + Utils.assert_true(orderedConnection.is_online() if first else not orderedConnection.is_online() ) - Utils.assert_false(orderedConnections[4].is_online()) - - # set connection to existing uri - connection = walletRpcs[0].get_daemon_connection() - assert connection is not None - connectionManager.set_connection(connection.uri) - Utils.assert_true(connectionManager.is_connected()) - Utils.assert_true(walletRpcs[0].get_daemon_connection() == connectionManager.get_connection()) - connection = connectionManager.get_connection() - assert connection is not None - Utils.assert_equals(Utils.WALLET_RPC_USERNAME, connection.username) - Utils.assert_equals(Utils.WALLET_RPC_PASSWORD, connection.password) - numExpectedChanges += 1 - Utils.assert_equals(numExpectedChanges, listener.changedConnections.size()) - Utils.assert_true(listener.changedConnections.get(listener.changedConnections.size() - 1) == walletRpcs[0].get_daemon_connection()) - - # set connection to new uri - connectionManager.stop_polling() - uri: str = "http:#localhost:49999" - connectionManager.set_connection(uri) - connection = connectionManager.get_connection() - assert connection is not None - Utils.assert_equals(uri, connection.uri) - numExpectedChanges += 1 - Utils.assert_equals(numExpectedChanges, listener.changedConnections.size()) - connection = listener.changedConnections.get(listener.changedConnections.size() -1) - assert connection is not None - Utils.assert_equals(uri, connection.uri) - - # set connection to empty string - connectionManager.set_connection("") - Utils.assert_equals(None, connectionManager.get_connection()) - numExpectedChanges += 1 - Utils.assert_equals(numExpectedChanges, listener.changedConnections.size()) - - # check all connections and test auto switch - connectionManager.check_connections() - numExpectedChanges += 1 - Utils.assert_equals(numExpectedChanges, listener.changedConnections.size()) - Utils.assert_true(connectionManager.is_connected()) + Utils.assert_false(ordered_connections[4].is_online()) + + # set connection to existing uri + connection = wallet_rpcs[0].get_daemon_connection() + assert connection is not None + connection_manager.set_connection(connection.uri) + Utils.assert_true(connection_manager.is_connected()) + Utils.assert_true(wallet_rpcs[0].get_daemon_connection() == connection_manager.get_connection()) + connection = connection_manager.get_connection() + assert connection is not None + Utils.assert_equals(Utils.WALLET_RPC_USERNAME, connection.username) + Utils.assert_equals(Utils.WALLET_RPC_PASSWORD, connection.password) + num_expected_changes += 1 + Utils.assert_equals(num_expected_changes, listener.changed_connections.size()) + Utils.assert_true(listener.changed_connections.get(listener.changed_connections.size() - 1) == wallet_rpcs[0].get_daemon_connection()) + + # set connection to new uri + connection_manager.stop_polling() + uri: str = "http:#localhost:49999" + connection_manager.set_connection(uri) + connection = connection_manager.get_connection() + assert connection is not None + Utils.assert_equals(uri, connection.uri) + num_expected_changes += 1 + Utils.assert_equals(num_expected_changes, listener.changed_connections.size()) + connection = listener.changed_connections.get(listener.changed_connections.size() -1) + assert connection is not None + Utils.assert_equals(uri, connection.uri) + + # set connection to empty string + connection_manager.set_connection("") + Utils.assert_equals(None, connection_manager.get_connection()) + num_expected_changes += 1 + Utils.assert_equals(num_expected_changes, listener.changed_connections.size()) + + # check all connections and test auto switch + connection_manager.check_connections() + num_expected_changes += 1 + Utils.assert_equals(num_expected_changes, listener.changed_connections.size()) + Utils.assert_true(connection_manager.is_connected()) - # remove current connection and test auto switch - connection = connectionManager.get_connection() - assert connection is not None - assert connection.uri is not None - connectionManager.remove_connection(connection.uri) - numExpectedChanges += 1 - Utils.assert_equals(numExpectedChanges, listener.changedConnections.size()) - Utils.assert_false(connectionManager.is_connected()) - connectionManager.check_connections() - numExpectedChanges += 1 - Utils.assert_equals(numExpectedChanges, listener.changedConnections.size()) - Utils.assert_true(connectionManager.is_connected()) + # remove current connection and test auto switch + connection = connection_manager.get_connection() + assert connection is not None + assert connection.uri is not None + connection_manager.remove_connection(connection.uri) + num_expected_changes += 1 + Utils.assert_equals(num_expected_changes, listener.changed_connections.size()) + Utils.assert_false(connection_manager.is_connected()) + connection_manager.check_connections() + num_expected_changes += 1 + Utils.assert_equals(num_expected_changes, listener.changed_connections.size()) + Utils.assert_true(connection_manager.is_connected()) - # test polling current connection - connectionManager.set_connection(None) - Utils.assert_false(connectionManager.is_connected()) - numExpectedChanges += 1 - Utils.assert_equals(numExpectedChanges, listener.changedConnections.size()) - connectionManager.start_polling(period_ms=Utils.SYNC_PERIOD_IN_MS, poll_type=MoneroConnectionPollType.CURRENT) - Utils.wait_for(Utils.AUTO_CONNECT_TIMEOUT_MS) - Utils.assert_true(connectionManager.is_connected()) - numExpectedChanges += 1 - Utils.assert_equals(numExpectedChanges, listener.changedConnections.size()) + # test polling current connection + connection_manager.set_connection(None) + Utils.assert_false(connection_manager.is_connected()) + num_expected_changes += 1 + Utils.assert_equals(num_expected_changes, listener.changed_connections.size()) + connection_manager.start_polling(period_ms=Utils.SYNC_PERIOD_IN_MS, poll_type=MoneroConnectionPollType.CURRENT) + Utils.wait_for(Utils.AUTO_CONNECT_TIMEOUT_MS) + Utils.assert_true(connection_manager.is_connected()) + num_expected_changes += 1 + Utils.assert_equals(num_expected_changes, listener.changed_connections.size()) - # test polling all connections - connectionManager.set_connection(None) - numExpectedChanges += 1 - Utils.assert_equals(numExpectedChanges, listener.changedConnections.size()) - connectionManager.start_polling(period_ms=Utils.SYNC_PERIOD_IN_MS, poll_type=MoneroConnectionPollType.ALL) - Utils.wait_for(Utils.AUTO_CONNECT_TIMEOUT_MS) - Utils.assert_true(connectionManager.is_connected()) - numExpectedChanges += 1 - Utils.assert_equals(numExpectedChanges, listener.changedConnections.size()) + # test polling all connections + connection_manager.set_connection(None) + num_expected_changes += 1 + Utils.assert_equals(num_expected_changes, listener.changed_connections.size()) + connection_manager.start_polling(period_ms=Utils.SYNC_PERIOD_IN_MS, poll_type=MoneroConnectionPollType.ALL) + Utils.wait_for(Utils.AUTO_CONNECT_TIMEOUT_MS) + Utils.assert_true(connection_manager.is_connected()) + num_expected_changes += 1 + Utils.assert_equals(num_expected_changes, listener.changed_connections.size()) - # shut down all connections - connection = connectionManager.get_connection() - assert connection is not None - for walletRpc in walletRpcs: - Utils.stop_wallet_rpc_process(walletRpc) + # shut down all connections + connection = connection_manager.get_connection() + assert connection is not None + for wallet_rpc in wallet_rpcs: + Utils.stop_wallet_rpc_process(wallet_rpc) - Utils.wait_for(Utils.SYNC_PERIOD_IN_MS + 100) - Utils.assert_false(connection.is_online()) - numExpectedChanges += 1 - Utils.assert_equals(numExpectedChanges, listener.changedConnections.size()) - Utils.assert_true(listener.changedConnections.get(listener.changedConnections.size() - 1) == connection) - - # reset - connectionManager.reset() - Utils.assert_equals(0, len(connectionManager.get_connections())) - Utils.assert_equals(None, connectionManager.get_connection()) - - finally: - # stop connection manager - if connectionManager is not None: - connectionManager.reset() - - # stop monero-wallet-rpc instances - for walletRpc in walletRpcs: - #try { Utils.stop_wallet_rpc_process(walletRpc) } - #catch (Exception e2) { } - pass - \ No newline at end of file + Utils.wait_for(Utils.SYNC_PERIOD_IN_MS + 100) + Utils.assert_false(connection.is_online()) + num_expected_changes += 1 + Utils.assert_equals(num_expected_changes, listener.changed_connections.size()) + Utils.assert_true(listener.changed_connections.get(listener.changed_connections.size() - 1) == connection) + + # reset + connection_manager.reset() + Utils.assert_equals(0, len(connection_manager.get_connections())) + Utils.assert_equals(None, connection_manager.get_connection()) + + finally: + # stop connection manager + if connection_manager is not None: + connection_manager.reset() + + # stop monero-wallet-rpc instances + for wallet_rpc in wallet_rpcs: + #try { Utils.stop_wallet_rpc_process(wallet_rpc) } + #catch (Exception e2) { } + pass diff --git a/tests/test_monero_daemon_rpc.py b/tests/test_monero_daemon_rpc.py index fba683c..496ff5c 100644 --- a/tests/test_monero_daemon_rpc.py +++ b/tests/test_monero_daemon_rpc.py @@ -4,519 +4,518 @@ from typing import Optional from monero import ( - MoneroDaemonRpc, MoneroVersion, MoneroBlockHeader, MoneroBlockTemplate, - MoneroBlock, MoneroWalletRpc, MoneroMiningStatus, MoneroPruneResult, - MoneroDaemonUpdateCheckResult, MoneroDaemonUpdateDownloadResult, - MoneroDaemonListener, MoneroPeer, MoneroDaemonInfo, MoneroDaemonSyncInfo, - MoneroHardForkInfo, MoneroAltChain, MoneroTx, MoneroSubmitTxResult, - MoneroTxPoolStats + MoneroDaemonRpc, MoneroVersion, MoneroBlockHeader, MoneroBlockTemplate, + MoneroBlock, MoneroWalletRpc, MoneroMiningStatus, MoneroPruneResult, + MoneroDaemonUpdateCheckResult, MoneroDaemonUpdateDownloadResult, + MoneroDaemonListener, MoneroPeer, MoneroDaemonInfo, MoneroDaemonSyncInfo, + MoneroHardForkInfo, MoneroAltChain, MoneroTx, MoneroSubmitTxResult, + MoneroTxPoolStats ) from utils import MoneroTestUtils as Utils, TestContext, BinaryBlockContext class TestMoneroDaemonRpc: - _daemon: MoneroDaemonRpc = Utils.get_daemon_rpc() - _wallet: MoneroWalletRpc = Utils.get_wallet_rpc() - BINARY_BLOCK_CTX: BinaryBlockContext = BinaryBlockContext() - LITE_MODE: bool = False - TEST_NOTIFICATIONS: bool = True - - # Can get the daemon's version - def test_get_version(self): - Utils.assert_true(Utils.TEST_NON_RELAYS) - version: MoneroVersion = self._daemon.get_version() - assert version.number is not None - Utils.assert_true(version.number > 0) - Utils.assert_not_none(version.is_release) - - # Can indicate if it's trusted - def test_is_trusted(self): - Utils.assert_true(Utils.TEST_NON_RELAYS) - self._daemon.is_trusted() - - # Can get the blockchain height - def test_get_geight(self): - Utils.assert_true(Utils.TEST_NON_RELAYS) - height = self._daemon.get_height() - Utils.assert_true(height > 0, "Height must be greater than 0") - - # Can get a block hash by height - def test_get_block_id_by_height(self): - Utils.assert_true(Utils.TEST_NON_RELAYS) - lastHeader: MoneroBlockHeader = self._daemon.get_last_block_header() - assert lastHeader.height is not None - hash: str = self._daemon.get_block_hash(lastHeader.height) - Utils.assert_not_none(hash) - Utils.assert_equals(64, len(hash)) - - # Can get a block template - def test_get_block_template(self): - Utils.assert_true(Utils.TEST_NON_RELAYS) - template: MoneroBlockTemplate = self._daemon.get_block_template(Utils.ADDRESS, 2) - Utils.test_block_template(template) - - # Can get the last block's header - def test_get_last_block_header(self): - Utils.assert_true(Utils.TEST_NON_RELAYS) - lastHeader: MoneroBlockHeader = self._daemon.get_last_block_header() - Utils.test_block_header(lastHeader, True) - - # Can get a block header by hash - def test_get_block_header_by_hash(self): - Utils.assert_true(Utils.TEST_NON_RELAYS) - - # retrieve by hash of last block - lastHeader: MoneroBlockHeader = self._daemon.get_last_block_header() - assert lastHeader.height is not None - hash: str = self._daemon.get_block_hash(lastHeader.height) - header: MoneroBlockHeader = self._daemon.get_block_header_by_hash(hash) - Utils.test_block_header(header, True) - Utils.assert_equals(lastHeader, header) - - # retrieve by hash of previous to last block - hash = self._daemon.get_block_hash(lastHeader.height - 1) - header = self._daemon.get_block_header_by_hash(hash) - Utils.test_block_header(header, True) - Utils.assert_equals(lastHeader.height - 1, header.height) - - # Can get a block header by height - def test_get_block_header_by_height(self): - Utils.assert_true(Utils.TEST_NON_RELAYS) - - # retrieve by height of last block - lastHeader: MoneroBlockHeader = self._daemon.get_last_block_header() - assert lastHeader.height is not None - header: MoneroBlockHeader = self._daemon.get_block_header_by_height(lastHeader.height) - Utils.test_block_header(header, True) - Utils.assert_equals(lastHeader, header) - - # retrieve by height of previous to last block - header = self._daemon.get_block_header_by_height(lastHeader.height - 1) - Utils.test_block_header(header, True) - Utils.assert_equals(lastHeader.height - 1, header.height) - - # Can get block headers by range - # TODO: test start with no end, vice versa, inclusivity - def test_get_block_headers_by_range(self): - Utils.assert_true(Utils.TEST_NON_RELAYS) - - # determine start and end height based on number of blocks and how many blocks ago - numBlocks = 100 - numBlocksAgo = 100 - currentHeight = self._daemon.get_height() - startHeight = currentHeight - numBlocksAgo - endHeight = currentHeight - (numBlocksAgo - numBlocks) - 1 - - # fetch headers - headers: list[MoneroBlockHeader] = self._daemon.get_block_headers_by_range(startHeight, endHeight) - - # test headers - Utils.assert_equals(numBlocks, len(headers)) - i: int = 0 - while i < numBlocks: - header: MoneroBlockHeader = headers[i] - Utils.assert_equals(startHeight + i, header.height) - Utils.test_block_header(header, True) - i += 1 - - # Can get a block by hash - def test_get_block_by_hash(self): - Utils.assert_true(Utils.TEST_NON_RELAYS) - - # test config - ctx = TestContext() - ctx.hasHex = True - ctx.hasTxs = False - ctx.headerIsFull = True - - # retrieve by hash of last block - lastHeader: MoneroBlockHeader = self._daemon.get_last_block_header() - assert lastHeader.height is not None - hash: str = self._daemon.get_block_hash(lastHeader.height) - block: MoneroBlock = self._daemon.get_block_by_hash(hash) - assert block.height is not None - Utils.test_block(block, ctx) - Utils.assert_equals(self._daemon.get_block_by_height(block.height), block) - Utils.assert_equals(None, block.txs) - - # retrieve by hash of previous to last block - hash = self._daemon.get_block_hash(lastHeader.height - 1) - block = self._daemon.get_block_by_hash(hash) - Utils.test_block(block, ctx) - Utils.assert_equals(self._daemon.get_block_by_height(lastHeader.height - 1), block) - Utils.assert_equals(None, block.txs) - - # Can get blocks by hash which includes transactions (binary) - def test_get_blocks_by_hash_binary(self): - Utils.assert_true(Utils.TEST_NON_RELAYS) - raise NotImplementedError("Not implemented") - - # Can get a block by height - def test_get_block_by_height(self): - Utils.assert_true(Utils.TEST_NON_RELAYS) - # config for testing blocks - ctx = TestContext() - ctx.hasHex = True - ctx.headerIsFull = True - ctx.hasTxs = False - - # retrieve by height of last block - lastHeader: MoneroBlockHeader = self._daemon.get_last_block_header() - assert lastHeader.height is not None - block: MoneroBlock = self._daemon.get_block_by_height(lastHeader.height) - assert block.height is not None - Utils.test_block(block, ctx) - Utils.assert_equals(self._daemon.get_block_by_height(block.height), block) - - # retrieve by height of previous to last block - block = self._daemon.get_block_by_height(lastHeader.height - 1) - Utils.test_block(block, ctx) - Utils.assert_equals(lastHeader.height - 1, block.height) - - # Can get blocks by height which includes transactions (binary) - def test_get_blocks_by_height_binary(self): - Utils.assert_true(Utils.TEST_NON_RELAYS) - - # set number of blocks to test - numBlocks = 100 - - # select random heights # TODO: this is horribly inefficient way of computing last 100 blocks if not shuffling - currentHeight: int = self._daemon.get_height() - allHeights: list[int] = [] - i: int = 0 - while i < currentHeight: - allHeights.append(i) - i += 1 - - heights: list[int] = [] - i = len(allHeights) - numBlocks - - while i < len(allHeights): - heights.append(allHeights[i]) - i += 1 - - # fetch blocks - blocks: list[MoneroBlock] = self._daemon.get_blocks_by_height(heights) - - # test blocks - txFound: bool = False - Utils.assert_equals(numBlocks, len(blocks)) - i = 0 - while i < len(heights): - block: MoneroBlock = blocks[i] - if len(block.txs) > 0: - txFound = True - - Utils.test_block(block, self.BINARY_BLOCK_CTX) - Utils.assert_equals(block.height, heights[i]) - i += 1 - - Utils.assert_true(txFound, "No transactions found to test") - - # Can get transaction pool statistics - def test_get_tx_pool_statistics(self): - Utils.assert_true(Utils.TEST_NON_RELAYS) - daemon = self._daemon - wallet = self._wallet - Utils.WALLET_TX_TRACKER.wait_for_wallet_txs_to_clear_pool(daemon, Utils.SYNC_PERIOD_IN_MS, [wallet]) - err: Optional[Exception] = None - txIds: list[str] = [] - try: - # submit txs to the pool but don't relay - i = 1 - while 1 < 3: - # submit tx hex - tx: MoneroTx = Utils.get_unrelayed_tx(wallet, i) - assert tx.full_hex is not None - result: MoneroSubmitTxResult = self._daemon.submit_tx_hex(tx.full_hex, True) - Utils.assert_true(result.is_good, json.dumps(result)) - assert tx.hash is not None - txIds.append(tx.hash) - - # get tx pool stats - stats: MoneroTxPoolStats = self._daemon.get_tx_pool_stats() - assert stats.num_txs is not None - Utils.assert_true(stats.num_txs > i - 1) - Utils.test_tx_pool_stats(stats) - i += 1 - - except Exception as e: - err = e - - # flush txs - self._daemon.flush_tx_pool(txIds) - if err is not None: - raise Exception(err) - - # Can get general information - def test_get_general_information(self): - Utils.assert_true(Utils.TEST_NON_RELAYS) - info: MoneroDaemonInfo = self._daemon.get_info() - Utils.test_info(info) - - # Can get sync information - def test_get_sync_information(self): - Utils.assert_true(Utils.TEST_NON_RELAYS) - syncInfo: MoneroDaemonSyncInfo = self._daemon.get_sync_info() - Utils.test_sync_info(syncInfo) - - # Can get hard fork information - def test_get_hard_fork_information(self): - Utils.assert_true(Utils.TEST_NON_RELAYS) - hardForkInfo: MoneroHardForkInfo = self._daemon.get_hard_fork_info() - Utils.test_hard_fork_info(hardForkInfo) - - # Can get alternative chains - def test_get_alternative_chains(self): - Utils.assert_true(Utils.TEST_NON_RELAYS) - altChains: list[MoneroAltChain] = self._daemon.get_alt_chains() - for altChain in altChains: - Utils.test_alt_chain(altChain) - - # Can get alternative block hashes - def test_get_alternative_block_ids(self): - Utils.assert_true(Utils.TEST_NON_RELAYS) - altBlockIds: list[str] = self._daemon.get_alt_block_hashes() - for altBlockId in altBlockIds: - Utils.assert_not_none(altBlockId) - Utils.assert_equals(64, len(altBlockId)) # TODO: common validation - - # Can get, set, and reset a download bandwidth limit - def test_set_download_bandwidth(self): - Utils.assert_true(Utils.TEST_NON_RELAYS) - initVal: int = self._daemon.get_download_limit() - Utils.assert_true(initVal > 0) - setVal: int = initVal * 2 - self._daemon.set_download_limit(setVal) - Utils.assert_equals(setVal, self._daemon.get_download_limit()) - resetVal: int = self._daemon.reset_download_limit() - Utils.assert_equals(initVal, resetVal) - - # test invalid limits - try: - self._daemon.set_download_limit(0) - raise Exception("Should have thrown error on invalid input") - except Exception as e: - Utils.assert_equals("Download limit must be an integer greater than 0", str(e)) - - Utils.assert_equals(self._daemon.get_download_limit(), initVal) - - # Can get, set, and reset an upload bandwidth limit - def test_set_upload_bandwidth(self): - Utils.assert_true(Utils.TEST_NON_RELAYS) - initVal: int = self._daemon.get_upload_limit() - Utils.assert_true(initVal > 0) - setVal: int = initVal * 2 - self._daemon.set_upload_limit(setVal) - Utils.assert_equals(setVal, self._daemon.get_upload_limit()) - resetVal: int = self._daemon.reset_upload_limit() - Utils.assert_equals(initVal, resetVal) - - # test invalid limits - try: - self._daemon.set_upload_limit(0) - raise Exception("Should have thrown error on invalid input") - except Exception as e: - Utils.assert_equals("Upload limit must be an integer greater than 0", str(e)) - - Utils.assert_equals(initVal, self._daemon.get_upload_limit()) - - # Can get peers with active incoming or outgoing connections - def test_get_peers(self): - Utils.assert_true(Utils.TEST_NON_RELAYS) - peers: list[MoneroPeer] = self._daemon.get_peers() - Utils.assert_false(len(peers) == 0, "Daemon has no incoming or outgoing peers to test") - for peer in peers: - Utils.test_peer(peer) - - # Can get all known peers which may be online or offline - def test_get_known_peers(self): - Utils.assert_true(Utils.TEST_NON_RELAYS) - peers: list[MoneroPeer] = self._daemon.get_known_peers() - Utils.assert_false(len(peers) == 0, "Daemon has no known peers to test") - for peer in peers: - Utils.test_known_peer(peer, False) - - # Can limit the number of outgoing peers - def test_set_outgoing_peer_limit(self): - Utils.assert_true(Utils.TEST_NON_RELAYS) - self._daemon.set_outgoing_peer_limit(0) - self._daemon.set_outgoing_peer_limit(8) - self._daemon.set_outgoing_peer_limit(10) - - # Can limit the number of incoming peers - def test_set_incoming_peer_limit(self): - Utils.assert_true(Utils.TEST_NON_RELAYS) - self._daemon.set_incoming_peer_limit(0) - self._daemon.set_incoming_peer_limit(8) - self._daemon.set_incoming_peer_limit(10) - - # Can notify listeners when a new block is added to the chain - def test_block_listener(self): - Utils.assert_true(not self.LITE_MODE and self.TEST_NOTIFICATIONS) - - try: - # start mining if possible to help push the network along - address: str = self._wallet.get_primary_address() - try: - self._daemon.start_mining(address, 8, False, True) - except: - pass - - # register a listener - listener: MoneroDaemonListener = MoneroDaemonListener() - self._daemon.add_listener(listener) - - # wait for next block notification - header: MoneroBlockHeader = self._daemon.wait_for_next_block_header() - self._daemon.remove_listener(listener) # unregister listener so daemon does not keep polling - Utils.test_block_header(header, True) - - # test that listener was called with equivalent header - Utils.assert_equals(header, listener.last_header) - except Exception as e: - raise e - finally: - # stop mining - try : - self._daemon.stop_mining() - except: - pass - - # Can start and stop mining - def test_mining(self): - Utils.assert_true(Utils.TEST_NON_RELAYS) - - # stop mining at beginning of test - try: - self._daemon.stop_mining() - except: - pass - - # generate address to mine to - address: str = self._wallet.get_primary_address() - - # start mining - self._daemon.start_mining(address, 2, False, True) - - # stop mining - self._daemon.stop_mining() - - # Can get mining status - def test_get_mining_status(self): - Utils.assert_true(Utils.TEST_NON_RELAYS) - - try: - # stop mining at beginning of test - try: - self._daemon.stop_mining() - except: - pass - - # test status without mining - status: MoneroMiningStatus = self._daemon.get_mining_status() - Utils.assert_equals(False, status.is_active) - Utils.assert_is_none(status.address) - Utils.assert_equals(0, status.speed) - Utils.assert_equals(0, status.num_threads) - Utils.assert_is_none(status.is_background) - - # test status with mining - address: str = self._wallet.get_primary_address() - threadCount: int = 3 - isBackground: bool = False - self._daemon.start_mining(address, threadCount, isBackground, True) - status = self._daemon.get_mining_status() - assert status.speed is not None - Utils.assert_equals(True, status.is_active) - Utils.assert_equals(address, status.address) - Utils.assert_true(status.speed >= 0) - Utils.assert_equals(threadCount, status.num_threads) - Utils.assert_equals(isBackground, status.is_background) - except Exception as e: - raise e - finally: - - # stop mining at end of test - try: + _daemon: MoneroDaemonRpc = Utils.get_daemon_rpc() + _wallet: MoneroWalletRpc = Utils.get_wallet_rpc() + BINARY_BLOCK_CTX: BinaryBlockContext = BinaryBlockContext() + LITE_MODE: bool = False + TEST_NOTIFICATIONS: bool = True + + # Can get the daemon's version + def test_get_version(self): + Utils.assert_true(Utils.TEST_NON_RELAYS) + version: MoneroVersion = self._daemon.get_version() + assert version.number is not None + Utils.assert_true(version.number > 0) + Utils.assert_not_none(version.is_release) + + # Can indicate if it's trusted + def test_is_trusted(self): + Utils.assert_true(Utils.TEST_NON_RELAYS) + self._daemon.is_trusted() + + # Can get the blockchain height + def test_get_geight(self): + Utils.assert_true(Utils.TEST_NON_RELAYS) + height = self._daemon.get_height() + Utils.assert_true(height > 0, "Height must be greater than 0") + + # Can get a block hash by height + def test_get_block_id_by_height(self): + Utils.assert_true(Utils.TEST_NON_RELAYS) + last_header: MoneroBlockHeader = self._daemon.get_last_block_header() + assert last_header.height is not None + hash_str: str = self._daemon.get_block_hash(last_header.height) + Utils.assert_not_none(hash_str) + Utils.assert_equals(64, len(hash_str)) + + # Can get a block template + def test_get_block_template(self): + Utils.assert_true(Utils.TEST_NON_RELAYS) + template: MoneroBlockTemplate = self._daemon.get_block_template(Utils.ADDRESS, 2) + Utils.test_block_template(template) + + # Can get the last block's header + def test_get_last_block_header(self): + Utils.assert_true(Utils.TEST_NON_RELAYS) + last_header: MoneroBlockHeader = self._daemon.get_last_block_header() + Utils.test_block_header(last_header, True) + + # Can get a block header by hash + def test_get_block_header_by_hash(self): + Utils.assert_true(Utils.TEST_NON_RELAYS) + + # retrieve by hash of last block + last_header: MoneroBlockHeader = self._daemon.get_last_block_header() + assert last_header.height is not None + hash_str: str = self._daemon.get_block_hash(last_header.height) + header: MoneroBlockHeader = self._daemon.get_block_header_by_hash(hash_str) + Utils.test_block_header(header, True) + Utils.assert_equals(last_header, header) + + # retrieve by hash of previous to last block + hash_str = self._daemon.get_block_hash(last_header.height - 1) + header = self._daemon.get_block_header_by_hash(hash_str) + Utils.test_block_header(header, True) + Utils.assert_equals(last_header.height - 1, header.height) + + # Can get a block header by height + def test_get_block_header_by_height(self): + Utils.assert_true(Utils.TEST_NON_RELAYS) + + # retrieve by height of last block + last_header: MoneroBlockHeader = self._daemon.get_last_block_header() + assert last_header.height is not None + header: MoneroBlockHeader = self._daemon.get_block_header_by_height(last_header.height) + Utils.test_block_header(header, True) + Utils.assert_equals(last_header, header) + + # retrieve by height of previous to last block + header = self._daemon.get_block_header_by_height(last_header.height - 1) + Utils.test_block_header(header, True) + Utils.assert_equals(last_header.height - 1, header.height) + + # Can get block headers by range + # TODO: test start with no end, vice versa, inclusivity + def test_get_block_headers_by_range(self): + Utils.assert_true(Utils.TEST_NON_RELAYS) + + # determine start and end height based on number of blocks and how many blocks ago + num_blocks = 100 + num_blocks_ago = 100 + current_height = self._daemon.get_height() + start_height = current_height - num_blocks_ago + end_height = current_height - (num_blocks_ago - num_blocks) - 1 + + # fetch headers + headers: list[MoneroBlockHeader] = self._daemon.get_block_headers_by_range(start_height, end_height) + + # test headers + Utils.assert_equals(num_blocks, len(headers)) + i: int = 0 + while i < num_blocks: + header: MoneroBlockHeader = headers[i] + Utils.assert_equals(start_height + i, header.height) + Utils.test_block_header(header, True) + i += 1 + + # Can get a block by hash + def test_get_block_by_hash(self): + Utils.assert_true(Utils.TEST_NON_RELAYS) + + # test config + ctx = TestContext() + ctx.has_hex = True + ctx.has_txs = False + ctx.header_is_full = True + + # retrieve by hash of last block + last_header: MoneroBlockHeader = self._daemon.get_last_block_header() + assert last_header.height is not None + hash_str: str = self._daemon.get_block_hash(last_header.height) + block: MoneroBlock = self._daemon.get_block_by_hash(hash_str) + assert block.height is not None + Utils.test_block(block, ctx) + Utils.assert_equals(self._daemon.get_block_by_height(block.height), block) + Utils.assert_equals(None, block.txs) + + # retrieve by hash of previous to last block + hash_str = self._daemon.get_block_hash(last_header.height - 1) + block = self._daemon.get_block_by_hash(hash_str) + Utils.test_block(block, ctx) + Utils.assert_equals(self._daemon.get_block_by_height(last_header.height - 1), block) + Utils.assert_equals(None, block.txs) + + # Can get blocks by hash which includes transactions (binary) + def test_get_blocks_by_hash_binary(self) -> None: + Utils.assert_true(Utils.TEST_NON_RELAYS) + raise NotImplementedError("Not implemented") + + # Can get a block by height + def test_get_block_by_height(self): + Utils.assert_true(Utils.TEST_NON_RELAYS) + # config for testing blocks + ctx = TestContext() + ctx.has_hex = True + ctx.header_is_full = True + ctx.has_txs = False + + # retrieve by height of last block + last_header: MoneroBlockHeader = self._daemon.get_last_block_header() + assert last_header.height is not None + block: MoneroBlock = self._daemon.get_block_by_height(last_header.height) + assert block.height is not None + Utils.test_block(block, ctx) + Utils.assert_equals(self._daemon.get_block_by_height(block.height), block) + + # retrieve by height of previous to last block + block = self._daemon.get_block_by_height(last_header.height - 1) + Utils.test_block(block, ctx) + Utils.assert_equals(last_header.height - 1, block.height) + + # Can get blocks by height which includes transactions (binary) + def test_get_blocks_by_height_binary(self): + Utils.assert_true(Utils.TEST_NON_RELAYS) + + # set number of blocks to test + num_blocks = 100 + + # select random heights # TODO: this is horribly inefficient way of computing last 100 blocks if not shuffling + current_height: int = self._daemon.get_height() + all_heights: list[int] = [] + i: int = 0 + while i < current_height: + all_heights.append(i) + i += 1 + + heights: list[int] = [] + i = len(all_heights) - num_blocks + + while i < len(all_heights): + heights.append(all_heights[i]) + i += 1 + + # fetch blocks + blocks: list[MoneroBlock] = self._daemon.get_blocks_by_height(heights) + + # test blocks + tx_found: bool = False + Utils.assert_equals(num_blocks, len(blocks)) + i = 0 + while i < len(heights): + block: MoneroBlock = blocks[i] + if len(block.txs) > 0: + tx_found = True + + Utils.test_block(block, self.BINARY_BLOCK_CTX) + Utils.assert_equals(block.height, heights[i]) + i += 1 + + Utils.assert_true(tx_found, "No transactions found to test") + + # Can get transaction pool statistics + def test_get_tx_pool_statistics(self): + Utils.assert_true(Utils.TEST_NON_RELAYS) + daemon = self._daemon + wallet = self._wallet + Utils.WALLET_TX_TRACKER.wait_for_wallet_txs_to_clear_pool(daemon, Utils.SYNC_PERIOD_IN_MS, [wallet]) + err: Optional[Exception] = None + tx_ids: list[str] = [] + try: + # submit txs to the pool but don't relay + i = 1 + while 1 < 3: + # submit tx hex + tx: MoneroTx = Utils.get_unrelayed_tx(wallet, i) + assert tx.full_hex is not None + result: MoneroSubmitTxResult = self._daemon.submit_tx_hex(tx.full_hex, True) + Utils.assert_true(result.is_good, json.dumps(result)) + assert tx.hash is not None + tx_ids.append(tx.hash) + + # get tx pool stats + stats: MoneroTxPoolStats = self._daemon.get_tx_pool_stats() + assert stats.num_txs is not None + Utils.assert_true(stats.num_txs > i - 1) + Utils.test_tx_pool_stats(stats) + i += 1 + + except Exception as e: + err = e + + # flush txs + self._daemon.flush_tx_pool(tx_ids) + if err is not None: + raise err + + # Can get general information + def test_get_general_information(self): + Utils.assert_true(Utils.TEST_NON_RELAYS) + info: MoneroDaemonInfo = self._daemon.get_info() + Utils.test_info(info) + + # Can get sync information + def test_get_sync_information(self): + Utils.assert_true(Utils.TEST_NON_RELAYS) + sync_info: MoneroDaemonSyncInfo = self._daemon.get_sync_info() + Utils.test_sync_info(sync_info) + + # Can get hard fork information + def test_get_hard_fork_information(self): + Utils.assert_true(Utils.TEST_NON_RELAYS) + hard_fork_info: MoneroHardForkInfo = self._daemon.get_hard_fork_info() + Utils.test_hard_fork_info(hard_fork_info) + + # Can get alternative chains + def test_get_alternative_chains(self): + Utils.assert_true(Utils.TEST_NON_RELAYS) + alt_chains: list[MoneroAltChain] = self._daemon.get_alt_chains() + for altChain in alt_chains: + Utils.test_alt_chain(altChain) + + # Can get alternative block hashes + def test_get_alternative_block_ids(self): + Utils.assert_true(Utils.TEST_NON_RELAYS) + alt_block_ids: list[str] = self._daemon.get_alt_block_hashes() + for altBlockId in alt_block_ids: + Utils.assert_not_none(altBlockId) + Utils.assert_equals(64, len(altBlockId)) # TODO: common validation + + # Can get, set, and reset a download bandwidth limit + def test_set_download_bandwidth(self): + Utils.assert_true(Utils.TEST_NON_RELAYS) + init_val: int = self._daemon.get_download_limit() + Utils.assert_true(init_val > 0) + set_val: int = init_val * 2 + self._daemon.set_download_limit(set_val) + Utils.assert_equals(set_val, self._daemon.get_download_limit()) + reset_val: int = self._daemon.reset_download_limit() + Utils.assert_equals(init_val, reset_val) + + # test invalid limits + try: + self._daemon.set_download_limit(0) + raise Exception("Should have thrown error on invalid input") + except Exception as e: + Utils.assert_equals("Download limit must be an integer greater than 0", str(e)) + + Utils.assert_equals(self._daemon.get_download_limit(), init_val) + + # Can get, set, and reset an upload bandwidth limit + def test_set_upload_bandwidth(self): + Utils.assert_true(Utils.TEST_NON_RELAYS) + init_val: int = self._daemon.get_upload_limit() + Utils.assert_true(init_val > 0) + set_val: int = init_val * 2 + self._daemon.set_upload_limit(set_val) + Utils.assert_equals(set_val, self._daemon.get_upload_limit()) + reset_val: int = self._daemon.reset_upload_limit() + Utils.assert_equals(init_val, reset_val) + + # test invalid limits + try: + self._daemon.set_upload_limit(0) + raise Exception("Should have thrown error on invalid input") + except Exception as e: + Utils.assert_equals("Upload limit must be an integer greater than 0", str(e)) + + Utils.assert_equals(init_val, self._daemon.get_upload_limit()) + + # Can get peers with active incoming or outgoing connections + def test_get_peers(self): + Utils.assert_true(Utils.TEST_NON_RELAYS) + peers: list[MoneroPeer] = self._daemon.get_peers() + Utils.assert_false(len(peers) == 0, "Daemon has no incoming or outgoing peers to test") + for peer in peers: + Utils.test_peer(peer) + + # Can get all known peers which may be online or offline + def test_get_known_peers(self): + Utils.assert_true(Utils.TEST_NON_RELAYS) + peers: list[MoneroPeer] = self._daemon.get_known_peers() + Utils.assert_false(len(peers) == 0, "Daemon has no known peers to test") + for peer in peers: + Utils.test_known_peer(peer, False) + + # Can limit the number of outgoing peers + def test_set_outgoing_peer_limit(self): + Utils.assert_true(Utils.TEST_NON_RELAYS) + self._daemon.set_outgoing_peer_limit(0) + self._daemon.set_outgoing_peer_limit(8) + self._daemon.set_outgoing_peer_limit(10) + + # Can limit the number of incoming peers + def test_set_incoming_peer_limit(self): + Utils.assert_true(Utils.TEST_NON_RELAYS) + self._daemon.set_incoming_peer_limit(0) + self._daemon.set_incoming_peer_limit(8) + self._daemon.set_incoming_peer_limit(10) + + # Can notify listeners when a new block is added to the chain + def test_block_listener(self): + Utils.assert_true(not self.LITE_MODE and self.TEST_NOTIFICATIONS) + + try: + # start mining if possible to help push the network along + address: str = self._wallet.get_primary_address() + try: + self._daemon.start_mining(address, 8, False, True) + except: + pass + + # register a listener + listener: MoneroDaemonListener = MoneroDaemonListener() + self._daemon.add_listener(listener) + + # wait for next block notification + header: MoneroBlockHeader = self._daemon.wait_for_next_block_header() + self._daemon.remove_listener(listener) # unregister listener so daemon does not keep polling + Utils.test_block_header(header, True) + + # test that listener was called with equivalent header + Utils.assert_equals(header, listener.last_header) + except Exception as e: + raise e + finally: + # stop mining + try : + self._daemon.stop_mining() + except: + pass + + # Can start and stop mining + def test_mining(self): + Utils.assert_true(Utils.TEST_NON_RELAYS) + + # stop mining at beginning of test + try: + self._daemon.stop_mining() + except: + pass + + # generate address to mine to + address: str = self._wallet.get_primary_address() + + # start mining + self._daemon.start_mining(address, 2, False, True) + + # stop mining self._daemon.stop_mining() - except: - pass - - # Can submit a mined block to the network - def test_submit_mined_block(self): - Utils.assert_true(Utils.TEST_NON_RELAYS) - - # get template to mine on - template: MoneroBlockTemplate = self._daemon.get_block_template(Utils.ADDRESS) - assert template.block_template_blob is not None - - # TODO monero rpc: way to get mining nonce when found in order to submit? - - # try to submit block hashing blob without nonce - try: - self._daemon.submit_block(template.block_template_blob) - raise Exception("Should have thrown error") - except Exception as e: - # Utils.assert_equals(-7, (int) e.getCode()) - Utils.assert_equals("Block not accepted", str(e)) - - # Can prune the blockchain - def test_prune_blockchain(self): - Utils.assert_true(Utils.TEST_NON_RELAYS) - - result: MoneroPruneResult = self._daemon.prune_blockchain(True) - - if (result.is_pruned): - assert result.pruning_seed is not None - Utils.assert_true(result.pruning_seed > 0) - else: - Utils.assert_equals(0, result.pruning_seed) - - # Can check for an update - def test_check_for_update(self): - Utils.assert_true(Utils.TEST_NON_RELAYS) - - result: MoneroDaemonUpdateCheckResult = self._daemon.check_for_update() - Utils.test_update_check_result(result) - - # Can download an update - @pytest.mark.skip(reason="") - def test_download_update(self): - Utils.assert_true(Utils.TEST_NON_RELAYS) - - # download to default path - result: MoneroDaemonUpdateDownloadResult = self._daemon.download_update() - Utils.test_update_download_result(result, None) - - # download to defined path - path: str = "test_download_" + str(time.time()) + ".tar.bz2" - result = self._daemon.download_update(path) - Utils.test_update_download_result(result, path) - - # test invalid path - if (result.is_update_available): - try: - result = self._daemon.download_update("./ohhai/there") - raise Exception("Should have thrown error") - except Exception as e: - Utils.assert_not_equals(str(e), "Should have thrown error") - # Utils.assert_equals(500, (int) e.getCode()) # TODO monerod: this causes a 500 in daemon rpc - - # Can be stopped - @pytest.mark.skip(reason="test is disabled to not interfere with other tests") - def test_stop(self): - Utils.assert_true(Utils.TEST_NON_RELAYS) - - # stop the daemon - self._daemon.stop() - - # give the daemon time to shut down - # TimeUnit.MILLISECONDS.sleep(Utils.SYNC_PERIOD_IN_MS) - - # try to interact with the daemon - try: - self._daemon.get_height() - raise Exception("Should have thrown error") - except Exception as e: - Utils.assert_not_equals("Should have thrown error", str(e)) + + # Can get mining status + def test_get_mining_status(self): + Utils.assert_true(Utils.TEST_NON_RELAYS) + + try: + # stop mining at beginning of test + try: + self._daemon.stop_mining() + except: + pass + + # test status without mining + status: MoneroMiningStatus = self._daemon.get_mining_status() + Utils.assert_equals(False, status.is_active) + Utils.assert_is_none(status.address) + Utils.assert_equals(0, status.speed) + Utils.assert_equals(0, status.num_threads) + Utils.assert_is_none(status.is_background) + + # test status with mining + address: str = self._wallet.get_primary_address() + thread_count: int = 3 + is_background: bool = False + self._daemon.start_mining(address, thread_count, is_background, True) + status = self._daemon.get_mining_status() + assert status.speed is not None + Utils.assert_equals(True, status.is_active) + Utils.assert_equals(address, status.address) + Utils.assert_true(status.speed >= 0) + Utils.assert_equals(thread_count, status.num_threads) + Utils.assert_equals(is_background, status.is_background) + except Exception as e: + raise e + finally: + # stop mining at end of test + try: + self._daemon.stop_mining() + except: + pass + + # Can submit a mined block to the network + def test_submit_mined_block(self): + Utils.assert_true(Utils.TEST_NON_RELAYS) + + # get template to mine on + template: MoneroBlockTemplate = self._daemon.get_block_template(Utils.ADDRESS) + assert template.block_template_blob is not None + + # TODO monero rpc: way to get mining nonce when found in order to submit? + + # try to submit block hashing blob without nonce + try: + self._daemon.submit_block(template.block_template_blob) + raise Exception("Should have thrown error") + except Exception as e: + # Utils.assert_equals(-7, (int) e.getCode()) + Utils.assert_equals("Block not accepted", str(e)) + + # Can prune the blockchain + def test_prune_blockchain(self): + Utils.assert_true(Utils.TEST_NON_RELAYS) + + result: MoneroPruneResult = self._daemon.prune_blockchain(True) + + if (result.is_pruned): + assert result.pruning_seed is not None + Utils.assert_true(result.pruning_seed > 0) + else: + Utils.assert_equals(0, result.pruning_seed) + + # Can check for an update + def test_check_for_update(self): + Utils.assert_true(Utils.TEST_NON_RELAYS) + + result: MoneroDaemonUpdateCheckResult = self._daemon.check_for_update() + Utils.test_update_check_result(result) + + # Can download an update + @pytest.mark.skip(reason="") + def test_download_update(self): + Utils.assert_true(Utils.TEST_NON_RELAYS) + + # download to default path + result: MoneroDaemonUpdateDownloadResult = self._daemon.download_update() + Utils.test_update_download_result(result, None) + + # download to defined path + path: str = "test_download_" + str(time.time()) + ".tar.bz2" + result = self._daemon.download_update(path) + Utils.test_update_download_result(result, path) + + # test invalid path + if result.is_update_available: + try: + result = self._daemon.download_update("./ohhai/there") + raise Exception("Should have thrown error") + except Exception as e: + Utils.assert_not_equals(str(e), "Should have thrown error") + # Utils.assert_equals(500, (int) e.getCode()) # TODO monerod: this causes a 500 in daemon rpc + + # Can be stopped + @pytest.mark.skip(reason="test is disabled to not interfere with other tests") + def test_stop(self): + Utils.assert_true(Utils.TEST_NON_RELAYS) + + # stop the daemon + self._daemon.stop() + + # give the daemon time to shut down + # TimeUnit.MILLISECONDS.sleep(Utils.SYNC_PERIOD_IN_MS) + + # try to interact with the daemon + try: + self._daemon.get_height() + raise Exception("Should have thrown error") + except Exception as e: + Utils.assert_not_equals("Should have thrown error", str(e)) diff --git a/tests/test_monero_utils.py b/tests/test_monero_utils.py index 8a43881..1161e10 100644 --- a/tests/test_monero_utils.py +++ b/tests/test_monero_utils.py @@ -1,229 +1,232 @@ import pytest -from monero import MoneroNetworkType, MoneroIntegratedAddress, MoneroUtils, MoneroTxConfig +from typing import Any +from monero import ( + MoneroNetworkType, MoneroIntegratedAddress, MoneroUtils, MoneroTxConfig +) from utils import MoneroTestUtils class TestMoneroUtils: - # Can get integrated addresses - def test_get_integrated_address(self): - primaryAddress: str = "58qRVVjZ4KxMX57TH6yWqGcH5AswvZZS494hWHcHPt6cDkP7V8AqxFhi3RKXZueVRgUnk8niQGHSpY5Bm9DjuWn16GDKXpF" - subaddress: str = "7B9w2xieXjhDumgPX39h1CAYELpsZ7Pe8Wqtr3pVL9jJ5gGDqgxjWt55gTYUCAuhahhM85ajEp6VbQfLDPETt4oT2ZRXa6n" - paymentId: str = "03284e41c342f036" - networkType: MoneroNetworkType = MoneroNetworkType.STAGENET - - # get integrated address with randomly generated payment id - integratedAddress: MoneroIntegratedAddress = MoneroUtils.get_integrated_address(networkType, primaryAddress, "") - assert primaryAddress == integratedAddress.standard_address - assert 16 == len(integratedAddress.payment_id) - assert 106 == len(integratedAddress.integrated_address) - - # get integrated address with specific payment id - integratedAddress = MoneroUtils.get_integrated_address(networkType, primaryAddress, paymentId) - assert primaryAddress == integratedAddress.standard_address - assert paymentId == integratedAddress.payment_id - assert 106 == len(integratedAddress.integrated_address) - - # get integrated address with subaddress - integratedAddress = MoneroUtils.get_integrated_address(networkType, subaddress, paymentId) - assert subaddress == integratedAddress.standard_address - assert paymentId == integratedAddress.payment_id - assert 106 == len(integratedAddress.integrated_address) - - # get integrated address with invalid payment id - try: - MoneroUtils.get_integrated_address(networkType, primaryAddress, "123") - raise Exception("Getting integrated address with invalid payment id should have failed") - except Exception as err: - assert "Invalid payment id" == str(err) - - # Can serialize heights with small numbers - def test_serialize_heights_small(self): - map: dict = { - "heights": [111, 222, 333] - } - - binary: bytes = MoneroUtils.dict_to_binary(map) - - MoneroTestUtils.assert_true(len(binary) > 0) - - map2: dict = MoneroUtils.binary_to_dict(binary) - - assert map == map2 - - # Can serialize heights with big numbers - def test_serialize_heights_big(self): - map: dict = { - "heights": [123456, 1234567, 870987] - } - - binary: bytes = MoneroUtils.dict_to_binary(map) - MoneroTestUtils.assert_true(len(binary) > 0) - map2: dict = MoneroUtils.binary_to_dict(binary) - - assert map == map2 - - # Can serialize map with text - def test_serialize_text_short(self): - map: dict = { - "msg": "Hello there my good man lets make a nice long text to test with lots of exclamation marks!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" - } - - binary: bytes = MoneroUtils.dict_to_binary(map) - MoneroTestUtils.assert_true(len(binary) > 0) - map2: dict = MoneroUtils.binary_to_dict(binary) - - assert map == map2 - - # Can serialize json with long text - def test_serialize_text_long(self): - map: dict = { - "msg": "Hello there my good man lets make a nice long text to test with lots of exclamation marks!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" + - "Hello there my good man lets make a nice long text to test with lots of exclamation marks!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" + - "Hello there my good man lets make a nice long text to test with lots of exclamation marks!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" + - "Hello there my good man lets make a nice long text to test with lots of exclamation marks!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" + - "Hello there my good man lets make a nice long text to test with lots of exclamation marks!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" + - "Hello there my good man lets make a nice long text to test with lots of exclamation marks!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" + - "Hello there my good man lets make a nice long text to test with lots of exclamation marks!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" + - "Hello there my good man lets make a nice long text to test with lots of exclamation marks!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" + - "Hello there my good man lets make a nice long text to test with lots of exclamation marks!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" + - "Hello there my good man lets make a nice long text to test with lots of exclamation marks!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" + - "Hello there my good man lets make a nice long text to test with lots of exclamation marks!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" + - "Hello there my good man lets make a nice long text to test with lots of exclamation marks!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" + - "Hello there my good man lets make a nice long text to test with lots of exclamation marks!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" + - "Hello there my good man lets make a nice long text to test with lots of exclamation marks!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" - } - - binary: bytes = MoneroUtils.dict_to_binary(map) - MoneroTestUtils.assert_true(len(binary) > 0) - map2: dict = MoneroUtils.binary_to_dict(binary) - - assert map == map2 - - # Can validate addresses - def test_address_validation(self): - - # test mainnet primary address validation - assert (MoneroUtils.is_valid_address("42U9v3qs5CjZEePHBZHwuSckQXebuZu299NSmVEmQ41YJZQhKcPyujyMSzpDH4VMMVSBo3U3b54JaNvQLwAjqDhKS3rvM3L", MoneroNetworkType.MAINNET)) == True - assert (MoneroUtils.is_valid_address("48ZxX3Y2y5s4nJ8fdz2w65TrTEp9PRsv5J8iHSShkHQcE2V31FhnWptioNst1K9oeDY4KpWZ7v8V2BZNVa4Wdky89iqmPz2", MoneroNetworkType.MAINNET)) == True - assert (MoneroUtils.is_valid_address("48W972Fx1SQMCHVKENnPpM7tRcL5oWMgpMCqQDbhH8UrjDFg2H9i5AQWXuU1qacJgUUCVLTsgDmZKXGz1vPLXY8QB5ypYqG", MoneroNetworkType.MAINNET)) == True - - # test mainnet integrated address validation - MoneroUtils.validate_address("4CApvrfMgUFZEePHBZHwuSckQXebuZu299NSmVEmQ41YJZQhKcPyujyMSzpDH4VMMVSBo3U3b54JaNvQLwAjqDhKeGLQ9vfRBRKFKnBtVH", MoneroNetworkType.MAINNET) - MoneroUtils.validate_address("4JGdXrMXaMP4nJ8fdz2w65TrTEp9PRsv5J8iHSShkHQcE2V31FhnWptioNst1K9oeDY4KpWZ7v8V2BZNVa4Wdky8DvDyXvDZXvE9jTQwom", MoneroNetworkType.MAINNET) - MoneroUtils.validate_address("4JCp7q5SchvMCHVKENnPpM7tRcL5oWMgpMCqQDbhH8UrjDFg2H9i5AQWXuU1qacJgUUCVLTsgDmZKXGz1vPLXY8QFySJXARQWju8AuRN2z", MoneroNetworkType.MAINNET) - - # test mainnet subaddress validation - MoneroUtils.validate_address("891TQPrWshJVpnBR4ZMhHiHpLx1PUnMqa3ccV5TJFBbqcJa3DWhjBh2QByCv3Su7WDPTGMHmCKkiVFN2fyGJKwbM1t6G7Ea", MoneroNetworkType.MAINNET) - MoneroUtils.validate_address("88fyq3t8Gxn1QWMG189EufHtMHXZXkfJtJKFJXqeA4GpSiuyfjVwVyp47PeQJnD7Tc8iK8TDvvhcmEmfh8nx7Va2ToP8wAo", MoneroNetworkType.MAINNET) - MoneroUtils.validate_address("88hnoBiX3TPjbFaQE8RxgyBcf3DtMKZWWQMoArBjQfn37JJwtm568mPX6ipcCuGKDnLCzgjmpLSqce4aBDyapJJAFtNxUMb", MoneroNetworkType.MAINNET) - - # test testnet primary address validation - MoneroUtils.validate_address("9tUBnNCkC3UKGygHCwYvAB1FscpjUuq5e9MYJd2rXuiiTjjfVeSVjnbSG5VTnJgBgy9Y7GTLfxpZNMUwNZjGfdFr1z79eV1", MoneroNetworkType.TESTNET) - MoneroUtils.validate_address("9xZmQa1kYakGoHcfXeBgcsLf622NCpChcACwXxfdgY9uAa9hXSPCV9cLvUsAShfDcFKDdPzCNJ1n5cFGKw5GVM722pjuGPd", MoneroNetworkType.TESTNET) - MoneroUtils.validate_address("A2TXS6QFQ4wEsp8U7C2Y4B7wBtiML8aDG7mdCbRvDQmRaRNj1YSSgJE46fSzUkwgpMUCXFqscvrQuN7oKpP6eDyQ7XuYsuf", MoneroNetworkType.TESTNET) - - # test testnet integrated address validation - MoneroUtils.validate_address("A4AroB2EoJzKGygHCwYvAB1FscpjUuq5e9MYJd2rXuiiTjjfVeSVjnbSG5VTnJgBgy9Y7GTLfxpZNMUwNZjGfdFr2QY5Ba2aHhTEdQa2ra", MoneroNetworkType.TESTNET) - MoneroUtils.validate_address("A8GSRNqF9rGGoHcfXeBgcsLf622NCpChcACwXxfdgY9uAa9hXSPCV9cLvUsAShfDcFKDdPzCNJ1n5cFGKw5GVM723iPoCEF1Fs9BcPYxTW", MoneroNetworkType.TESTNET) - MoneroUtils.validate_address("ACACSuDk1LTEsp8U7C2Y4B7wBtiML8aDG7mdCbRvDQmRaRNj1YSSgJE46fSzUkwgpMUCXFqscvrQuN7oKpP6eDyQAdgDoT3UnMYKQz7SHC", MoneroNetworkType.TESTNET) - - # test testnet subaddress validation - MoneroUtils.validate_address("BgnKzHPJQDcg7xiP7bMN9MfPv9Z8ciT71iEMYnCdgBRBFETWgu9nKTr8fnzyGfU9h9gyNA8SFzYYzHfTS9KhqytSU943Nu1", MoneroNetworkType.TESTNET) - MoneroUtils.validate_address("BZwiuKkoNP59zgPHTxpNw3PM4DW2xiAVQJWqfFRrGyeZ7afVdQqoiJg3E2dDL3Ja8BV4ov2LEoHx9UjzF3W4ihPBSZvWwTx", MoneroNetworkType.TESTNET) - MoneroUtils.validate_address("Bhf1DEYrentcehUvNreLK5gxosnC2VStMXNCCs163RTxQq4jxFYvpw7LrQFmrMwWW2KsXLhMRtyho6Lq11ci3Fb246bxYmi", MoneroNetworkType.TESTNET) - - # test stagenet primary address validation - MoneroUtils.validate_address("5B8s3obCY2ETeQB3GNAGPK2zRGen5UeW1WzegSizVsmf6z5NvM2GLoN6zzk1vHyzGAAfA8pGhuYAeCFZjHAp59jRVQkunGS", MoneroNetworkType.STAGENET) - MoneroUtils.validate_address("57VfotUbSZLG82UkKhWXDjS5ZEK9ZCDcmjdk4gpVq2fbKdEgwRCFrGTLZ2MMdSHphRWJDWVBi5qS8T7dz13JTCWtC228zyn", MoneroNetworkType.STAGENET) - MoneroUtils.validate_address("52FysgWJYmAG73QUQZRULJj2Dv2C2mceUMB5zHqNzMn8WBtfPWQrSUFSQUKTX9r7bUMmVSGbrau976xYLynR8jTWLdA7rfp", MoneroNetworkType.STAGENET) - - # test stagenet integrated address validation - MoneroUtils.validate_address("5LqY4cQh9HkTeQB3GNAGPK2zRGen5UeW1WzegSizVsmf6z5NvM2GLoN6zzk1vHyzGAAfA8pGhuYAeCFZjHAp59jRj6LZRFrjuGK8Whthg2", MoneroNetworkType.STAGENET) - MoneroUtils.validate_address("5HCLphJ63prG82UkKhWXDjS5ZEK9ZCDcmjdk4gpVq2fbKdEgwRCFrGTLZ2MMdSHphRWJDWVBi5qS8T7dz13JTCWtHETX8zcUhDjVKcynf6", MoneroNetworkType.STAGENET) - MoneroUtils.validate_address("5BxetVKoA2gG73QUQZRULJj2Dv2C2mceUMB5zHqNzMn8WBtfPWQrSUFSQUKTX9r7bUMmVSGbrau976xYLynR8jTWVwQwpHNg5fCLgtA2Dv", MoneroNetworkType.STAGENET) - - # test stagenet subaddress validation - MoneroUtils.validate_address("778B5D2JmMh5TJVWFbygJR15dvio5Z5B24hfSrWDzeroM8j8Lqc9sMoFE6324xg2ReaAZqHJkgfGFRugRmYHugHZ4f17Gxo", MoneroNetworkType.STAGENET) - MoneroUtils.validate_address("73U97wGEH9RCVUf6bopo45jSgoqjMzz4mTUsvWs5EusmYAmFcBYFm7wKMVmgtVKCBhMQqXrcMbHvwck2md63jMZSFJxUhQ2", MoneroNetworkType.STAGENET) - MoneroUtils.validate_address("747wPpaPKrjDPZrF48jAfz9pRRUHLMCWfYu2UanP4ZfTG8NrmYrSEWNW8gYoadU8hTiwBjV14e6DLaC5xfhyEpX5154aMm6", MoneroNetworkType.STAGENET) - - # test invalid addresses on mainnet - MoneroTestUtils.test_invalid_address(None, MoneroNetworkType.MAINNET) - MoneroTestUtils.test_invalid_address("", MoneroNetworkType.MAINNET) - MoneroTestUtils.test_invalid_address("42ZxX3Y2y5s4nJ8fdz2w65TrTEp9PRsv5J8iHSShkHQcE2V31FhnWptioNst1K9oeDY4KpWZ7v8V2BZNVa4Wdky89iqmPz2", MoneroNetworkType.MAINNET) - MoneroTestUtils.test_invalid_address("41ApvrfMgUFZEePHBZHwuSckQXebuZu299NSmVEmQ41YJZQhKcPyujyMSzpDH4VMMVSBo3U3b54JaNvQLwAjqDhKeGLQ9vfRBRKFKnBtVH", MoneroNetworkType.MAINNET) - MoneroTestUtils.test_invalid_address("81fyq3t8Gxn1QWMG189EufHtMHXZXkfJtJKFJXqeA4GpSiuyfjVwVyp47PeQJnD7Tc8iK8TDvvhcmEmfh8nx7Va2ToP8wAo", MoneroNetworkType.MAINNET) - - # test invalid addresses on testnet - MoneroTestUtils.test_invalid_address(None, MoneroNetworkType.TESTNET) - MoneroTestUtils.test_invalid_address("", MoneroNetworkType.TESTNET) - MoneroTestUtils.test_invalid_address("91UBnNCkC3UKGygHCwYvAB1FscpjUuq5e9MYJd2rXuiiTjjfVeSVjnbSG5VTnJgBgy9Y7GTLfxpZNMUwNZjGfdFr1z79eV1", MoneroNetworkType.TESTNET) - MoneroTestUtils.test_invalid_address("A1AroB2EoJzKGygHCwYvAB1FscpjUuq5e9MYJd2rXuiiTjjfVeSVjnbSG5VTnJgBgy9Y7GTLfxpZNMUwNZjGfdFr2QY5Ba2aHhTEdQa2ra", MoneroNetworkType.TESTNET) - MoneroTestUtils.test_invalid_address("B1nKzHPJQDcg7xiP7bMN9MfPv9Z8ciT71iEMYnCdgBRBFETWgu9nKTr8fnzyGfU9h9gyNA8SFzYYzHfTS9KhqytSU943Nu1", MoneroNetworkType.TESTNET) - - # test invalid addresses on stagenet - MoneroTestUtils.test_invalid_address(None, MoneroNetworkType.STAGENET) - MoneroTestUtils.test_invalid_address("", MoneroNetworkType.STAGENET) - MoneroTestUtils.test_invalid_address("518s3obCY2ETeQB3GNAGPK2zRGen5UeW1WzegSizVsmf6z5NvM2GLoN6zzk1vHyzGAAfA8pGhuYAeCFZjHAp59jRVQkunGS", MoneroNetworkType.STAGENET) - MoneroTestUtils.test_invalid_address("51qY4cQh9HkTeQB3GNAGPK2zRGen5UeW1WzegSizVsmf6z5NvM2GLoN6zzk1vHyzGAAfA8pGhuYAeCFZjHAp59jRj6LZRFrjuGK8Whthg2", MoneroNetworkType.STAGENET) - MoneroTestUtils.test_invalid_address("718B5D2JmMh5TJVWFbygJR15dvio5Z5B24hfSrWDzeroM8j8Lqc9sMoFE6324xg2ReaAZqHJkgfGFRugRmYHugHZ4f17Gxo", MoneroNetworkType.STAGENET) - - # Can validate keys - def test_key_validation(self): - - # test private view key validation - MoneroTestUtils.assert_true(MoneroUtils.is_valid_private_view_key("86cf351d10894769feba29b9e201e12fb100b85bb52fc5825c864eef55c5840d")) - MoneroTestUtils.test_invalid_public_view_key("") - MoneroTestUtils.test_invalid_private_view_key(None) - MoneroTestUtils.test_invalid_private_view_key("5B8s3obCY2ETeQB3GNAGPK2zRGen5UeW1WzegSizVsmf6z5NvM2GLoN6zzk1vHyzGAAfA8pGhuYAeCFZjHAp59jRVQkunGS") - - # test public view key validation - MoneroTestUtils.assert_true(MoneroUtils.is_valid_public_view_key("99873d76ca874ff1aad676b835dd303abcb21c9911ca8a3d9130abc4544d8a0a")) - MoneroTestUtils.test_invalid_public_view_key("") - MoneroTestUtils.test_invalid_public_view_key(None) - MoneroTestUtils.test_invalid_public_view_key("z86cf351d10894769feba29b9e201e12fb100b85bb52fc5825c864eef55c5840d") - - # test private spend key validation - MoneroTestUtils.assert_true(MoneroUtils.is_valid_private_spend_key("e9ba887e93620ef9fafdfe0c6d3022949f1c5713cbd9ef631f18a0fb00421dee")) - MoneroTestUtils.test_invalid_private_spend_key("") - MoneroTestUtils.test_invalid_private_spend_key(None) - MoneroTestUtils.test_invalid_private_spend_key("z86cf351d10894769feba29b9e201e12fb100b85bb52fc5825c864eef55c5840d") - - # test public spend key validation - MoneroTestUtils.assert_true(MoneroUtils.is_valid_public_spend_key("3e48df9e9d8038dbf6f5382fac2becd8686273cda5bd87187e45dca7ec5af37b")) - MoneroTestUtils.test_invalid_public_spend_key("") - MoneroTestUtils.test_invalid_public_spend_key(None) - MoneroTestUtils.test_invalid_public_spend_key("z86cf351d10894769feba29b9e201e12fb100b85bb52fc5825c864eef55c5840d") - - # Can convert between XMR and atomic units - def test_atomic_unit_conversion(self): - assert 1000000000000 == MoneroUtils.xmr_to_atomic_units(1) - assert 1 == MoneroUtils.atomic_units_to_xmr(1000000000000) - assert 1000000000 == MoneroUtils.xmr_to_atomic_units(0.001) - assert 0.001 == MoneroUtils.atomic_units_to_xmr(1000000000) - assert 250000000000 == MoneroUtils.xmr_to_atomic_units(0.25) - assert 0.25 == MoneroUtils.atomic_units_to_xmr(250000000000) - assert 1250000000000 == MoneroUtils.xmr_to_atomic_units(1.25) - assert 1.25 == MoneroUtils.atomic_units_to_xmr(1250000000000) - assert 2796726190000 == MoneroUtils.xmr_to_atomic_units(2.79672619) - assert 2.79672619 == MoneroUtils.atomic_units_to_xmr(2796726190000) - assert 2796726190001 == MoneroUtils.xmr_to_atomic_units(2.796726190001) - assert 2.796726190001 == MoneroUtils.atomic_units_to_xmr(2796726190001) - assert 2796726189999 == MoneroUtils.xmr_to_atomic_units(2.796726189999) - assert 2.796726189999 == MoneroUtils.atomic_units_to_xmr(2796726189999) - assert 2796726180000 == MoneroUtils.xmr_to_atomic_units(2.79672618) - assert 2.79672618 == MoneroUtils.atomic_units_to_xmr(2796726180000) - - # Can get payment uri - def test_get_payment_uri(self): - config = MoneroTxConfig() - config.address = "42U9v3qs5CjZEePHBZHwuSckQXebuZu299NSmVEmQ41YJZQhKcPyujyMSzpDH4VMMVSBo3U3b54JaNvQLwAjqDhKS3rvM3L" - config.amount = 250000000000 - config.recipient_name = "John Doe" - config.note = "My transfer to wallet" - - payment_uri = MoneroUtils.get_payment_uri(config) - - assert payment_uri == "monero:42U9v3qs5CjZEePHBZHwuSckQXebuZu299NSmVEmQ41YJZQhKcPyujyMSzpDH4VMMVSBo3U3b54JaNvQLwAjqDhKS3rvM3L?tx_amount=0.250000000000&recipient_name=John%20Doe&tx_description=My%20transfer%20to%20wallet" + # Can get integrated addresses + def test_get_integrated_address(self): + primary_address: str = "58qRVVjZ4KxMX57TH6yWqGcH5AswvZZS494hWHcHPt6cDkP7V8AqxFhi3RKXZueVRgUnk8niQGHSpY5Bm9DjuWn16GDKXpF" + subaddress: str = "7B9w2xieXjhDumgPX39h1CAYELpsZ7Pe8Wqtr3pVL9jJ5gGDqgxjWt55gTYUCAuhahhM85ajEp6VbQfLDPETt4oT2ZRXa6n" + payment_id: str = "03284e41c342f036" + network_type: MoneroNetworkType = MoneroNetworkType.STAGENET + + # get integrated address with randomly generated payment id + integrated_address: MoneroIntegratedAddress = MoneroUtils.get_integrated_address(network_type, primary_address, "") + assert primary_address == integrated_address.standard_address + assert 16 == len(integrated_address.payment_id) + assert 106 == len(integrated_address.integrated_address) + + # get integrated address with specific payment id + integrated_address = MoneroUtils.get_integrated_address(network_type, primary_address, payment_id) + assert primary_address == integrated_address.standard_address + assert payment_id == integrated_address.payment_id + assert 106 == len(integrated_address.integrated_address) + + # get integrated address with subaddress + integrated_address = MoneroUtils.get_integrated_address(network_type, subaddress, payment_id) + assert subaddress == integrated_address.standard_address + assert payment_id == integrated_address.payment_id + assert 106 == len(integrated_address.integrated_address) + + # get integrated address with invalid payment id + try: + MoneroUtils.get_integrated_address(network_type, primary_address, "123") + raise Exception("Getting integrated address with invalid payment id should have failed") + except Exception as err: + assert "Invalid payment id" == str(err) + + # Can serialize heights with small numbers + def test_serialize_heights_small(self): + jsonMap: dict[Any, Any] = { + "heights": [111, 222, 333] + } + + binary: bytes = MoneroUtils.dict_to_binary(jsonMap) + + MoneroTestUtils.assert_true(len(binary) > 0) + + jsonMap2: dict[Any, Any] = MoneroUtils.binary_to_dict(binary) + + assert jsonMap == jsonMap2 + + # Can serialize heights with big numbers + def test_serialize_heights_big(self): + jsonMap: dict[Any, Any] = { + "heights": [123456, 1234567, 870987] + } + + binary: bytes = MoneroUtils.dict_to_binary(jsonMap) + MoneroTestUtils.assert_true(len(binary) > 0) + jsonMap2: dict[Any, Any] = MoneroUtils.binary_to_dict(binary) + + assert jsonMap == jsonMap2 + + # Can serialize jsonMap with text + def test_serialize_text_short(self): + jsonMap: dict[Any, Any] = { + "msg": "Hello there my good man lets make a nice long text to test with lots of exclamation marks!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" + } + + binary: bytes = MoneroUtils.dict_to_binary(jsonMap) + MoneroTestUtils.assert_true(len(binary) > 0) + jsonMap2: dict[Any, Any] = MoneroUtils.binary_to_dict(binary) + + assert jsonMap == jsonMap2 + + # Can serialize json with long text + def test_serialize_text_long(self): + jsonMap: dict[str, str] = { + "msg": "Hello there my good man lets make a nice long text to test with lots of exclamation marks!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" + + "Hello there my good man lets make a nice long text to test with lots of exclamation marks!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" + + "Hello there my good man lets make a nice long text to test with lots of exclamation marks!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" + + "Hello there my good man lets make a nice long text to test with lots of exclamation marks!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" + + "Hello there my good man lets make a nice long text to test with lots of exclamation marks!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" + + "Hello there my good man lets make a nice long text to test with lots of exclamation marks!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" + + "Hello there my good man lets make a nice long text to test with lots of exclamation marks!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" + + "Hello there my good man lets make a nice long text to test with lots of exclamation marks!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" + + "Hello there my good man lets make a nice long text to test with lots of exclamation marks!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" + + "Hello there my good man lets make a nice long text to test with lots of exclamation marks!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" + + "Hello there my good man lets make a nice long text to test with lots of exclamation marks!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" + + "Hello there my good man lets make a nice long text to test with lots of exclamation marks!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" + + "Hello there my good man lets make a nice long text to test with lots of exclamation marks!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" + + "Hello there my good man lets make a nice long text to test with lots of exclamation marks!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" + } + + binary: bytes = MoneroUtils.dict_to_binary(jsonMap) + MoneroTestUtils.assert_true(len(binary) > 0) + jsonMap2: dict[Any, Any] = MoneroUtils.binary_to_dict(binary) + + assert jsonMap == jsonMap2 + + # Can validate addresses + def test_address_validation(self): + + # test mainnet primary address validation + assert (MoneroUtils.is_valid_address("42U9v3qs5CjZEePHBZHwuSckQXebuZu299NSmVEmQ41YJZQhKcPyujyMSzpDH4VMMVSBo3U3b54JaNvQLwAjqDhKS3rvM3L", MoneroNetworkType.MAINNET)) == True + assert (MoneroUtils.is_valid_address("48ZxX3Y2y5s4nJ8fdz2w65TrTEp9PRsv5J8iHSShkHQcE2V31FhnWptioNst1K9oeDY4KpWZ7v8V2BZNVa4Wdky89iqmPz2", MoneroNetworkType.MAINNET)) == True + assert (MoneroUtils.is_valid_address("48W972Fx1SQMCHVKENnPpM7tRcL5oWMgpMCqQDbhH8UrjDFg2H9i5AQWXuU1qacJgUUCVLTsgDmZKXGz1vPLXY8QB5ypYqG", MoneroNetworkType.MAINNET)) == True + + # test mainnet integrated address validation + MoneroUtils.validate_address("4CApvrfMgUFZEePHBZHwuSckQXebuZu299NSmVEmQ41YJZQhKcPyujyMSzpDH4VMMVSBo3U3b54JaNvQLwAjqDhKeGLQ9vfRBRKFKnBtVH", MoneroNetworkType.MAINNET) + MoneroUtils.validate_address("4JGdXrMXaMP4nJ8fdz2w65TrTEp9PRsv5J8iHSShkHQcE2V31FhnWptioNst1K9oeDY4KpWZ7v8V2BZNVa4Wdky8DvDyXvDZXvE9jTQwom", MoneroNetworkType.MAINNET) + MoneroUtils.validate_address("4JCp7q5SchvMCHVKENnPpM7tRcL5oWMgpMCqQDbhH8UrjDFg2H9i5AQWXuU1qacJgUUCVLTsgDmZKXGz1vPLXY8QFySJXARQWju8AuRN2z", MoneroNetworkType.MAINNET) + + # test mainnet subaddress validation + MoneroUtils.validate_address("891TQPrWshJVpnBR4ZMhHiHpLx1PUnMqa3ccV5TJFBbqcJa3DWhjBh2QByCv3Su7WDPTGMHmCKkiVFN2fyGJKwbM1t6G7Ea", MoneroNetworkType.MAINNET) + MoneroUtils.validate_address("88fyq3t8Gxn1QWMG189EufHtMHXZXkfJtJKFJXqeA4GpSiuyfjVwVyp47PeQJnD7Tc8iK8TDvvhcmEmfh8nx7Va2ToP8wAo", MoneroNetworkType.MAINNET) + MoneroUtils.validate_address("88hnoBiX3TPjbFaQE8RxgyBcf3DtMKZWWQMoArBjQfn37JJwtm568mPX6ipcCuGKDnLCzgjmpLSqce4aBDyapJJAFtNxUMb", MoneroNetworkType.MAINNET) + + # test testnet primary address validation + MoneroUtils.validate_address("9tUBnNCkC3UKGygHCwYvAB1FscpjUuq5e9MYJd2rXuiiTjjfVeSVjnbSG5VTnJgBgy9Y7GTLfxpZNMUwNZjGfdFr1z79eV1", MoneroNetworkType.TESTNET) + MoneroUtils.validate_address("9xZmQa1kYakGoHcfXeBgcsLf622NCpChcACwXxfdgY9uAa9hXSPCV9cLvUsAShfDcFKDdPzCNJ1n5cFGKw5GVM722pjuGPd", MoneroNetworkType.TESTNET) + MoneroUtils.validate_address("A2TXS6QFQ4wEsp8U7C2Y4B7wBtiML8aDG7mdCbRvDQmRaRNj1YSSgJE46fSzUkwgpMUCXFqscvrQuN7oKpP6eDyQ7XuYsuf", MoneroNetworkType.TESTNET) + + # test testnet integrated address validation + MoneroUtils.validate_address("A4AroB2EoJzKGygHCwYvAB1FscpjUuq5e9MYJd2rXuiiTjjfVeSVjnbSG5VTnJgBgy9Y7GTLfxpZNMUwNZjGfdFr2QY5Ba2aHhTEdQa2ra", MoneroNetworkType.TESTNET) + MoneroUtils.validate_address("A8GSRNqF9rGGoHcfXeBgcsLf622NCpChcACwXxfdgY9uAa9hXSPCV9cLvUsAShfDcFKDdPzCNJ1n5cFGKw5GVM723iPoCEF1Fs9BcPYxTW", MoneroNetworkType.TESTNET) + MoneroUtils.validate_address("ACACSuDk1LTEsp8U7C2Y4B7wBtiML8aDG7mdCbRvDQmRaRNj1YSSgJE46fSzUkwgpMUCXFqscvrQuN7oKpP6eDyQAdgDoT3UnMYKQz7SHC", MoneroNetworkType.TESTNET) + + # test testnet subaddress validation + MoneroUtils.validate_address("BgnKzHPJQDcg7xiP7bMN9MfPv9Z8ciT71iEMYnCdgBRBFETWgu9nKTr8fnzyGfU9h9gyNA8SFzYYzHfTS9KhqytSU943Nu1", MoneroNetworkType.TESTNET) + MoneroUtils.validate_address("BZwiuKkoNP59zgPHTxpNw3PM4DW2xiAVQJWqfFRrGyeZ7afVdQqoiJg3E2dDL3Ja8BV4ov2LEoHx9UjzF3W4ihPBSZvWwTx", MoneroNetworkType.TESTNET) + MoneroUtils.validate_address("Bhf1DEYrentcehUvNreLK5gxosnC2VStMXNCCs163RTxQq4jxFYvpw7LrQFmrMwWW2KsXLhMRtyho6Lq11ci3Fb246bxYmi", MoneroNetworkType.TESTNET) + + # test stagenet primary address validation + MoneroUtils.validate_address("5B8s3obCY2ETeQB3GNAGPK2zRGen5UeW1WzegSizVsmf6z5NvM2GLoN6zzk1vHyzGAAfA8pGhuYAeCFZjHAp59jRVQkunGS", MoneroNetworkType.STAGENET) + MoneroUtils.validate_address("57VfotUbSZLG82UkKhWXDjS5ZEK9ZCDcmjdk4gpVq2fbKdEgwRCFrGTLZ2MMdSHphRWJDWVBi5qS8T7dz13JTCWtC228zyn", MoneroNetworkType.STAGENET) + MoneroUtils.validate_address("52FysgWJYmAG73QUQZRULJj2Dv2C2mceUMB5zHqNzMn8WBtfPWQrSUFSQUKTX9r7bUMmVSGbrau976xYLynR8jTWLdA7rfp", MoneroNetworkType.STAGENET) + + # test stagenet integrated address validation + MoneroUtils.validate_address("5LqY4cQh9HkTeQB3GNAGPK2zRGen5UeW1WzegSizVsmf6z5NvM2GLoN6zzk1vHyzGAAfA8pGhuYAeCFZjHAp59jRj6LZRFrjuGK8Whthg2", MoneroNetworkType.STAGENET) + MoneroUtils.validate_address("5HCLphJ63prG82UkKhWXDjS5ZEK9ZCDcmjdk4gpVq2fbKdEgwRCFrGTLZ2MMdSHphRWJDWVBi5qS8T7dz13JTCWtHETX8zcUhDjVKcynf6", MoneroNetworkType.STAGENET) + MoneroUtils.validate_address("5BxetVKoA2gG73QUQZRULJj2Dv2C2mceUMB5zHqNzMn8WBtfPWQrSUFSQUKTX9r7bUMmVSGbrau976xYLynR8jTWVwQwpHNg5fCLgtA2Dv", MoneroNetworkType.STAGENET) + + # test stagenet subaddress validation + MoneroUtils.validate_address("778B5D2JmMh5TJVWFbygJR15dvio5Z5B24hfSrWDzeroM8j8Lqc9sMoFE6324xg2ReaAZqHJkgfGFRugRmYHugHZ4f17Gxo", MoneroNetworkType.STAGENET) + MoneroUtils.validate_address("73U97wGEH9RCVUf6bopo45jSgoqjMzz4mTUsvWs5EusmYAmFcBYFm7wKMVmgtVKCBhMQqXrcMbHvwck2md63jMZSFJxUhQ2", MoneroNetworkType.STAGENET) + MoneroUtils.validate_address("747wPpaPKrjDPZrF48jAfz9pRRUHLMCWfYu2UanP4ZfTG8NrmYrSEWNW8gYoadU8hTiwBjV14e6DLaC5xfhyEpX5154aMm6", MoneroNetworkType.STAGENET) + + # test invalid addresses on mainnet + MoneroTestUtils.test_invalid_address(None, MoneroNetworkType.MAINNET) + MoneroTestUtils.test_invalid_address("", MoneroNetworkType.MAINNET) + MoneroTestUtils.test_invalid_address("42ZxX3Y2y5s4nJ8fdz2w65TrTEp9PRsv5J8iHSShkHQcE2V31FhnWptioNst1K9oeDY4KpWZ7v8V2BZNVa4Wdky89iqmPz2", MoneroNetworkType.MAINNET) + MoneroTestUtils.test_invalid_address("41ApvrfMgUFZEePHBZHwuSckQXebuZu299NSmVEmQ41YJZQhKcPyujyMSzpDH4VMMVSBo3U3b54JaNvQLwAjqDhKeGLQ9vfRBRKFKnBtVH", MoneroNetworkType.MAINNET) + MoneroTestUtils.test_invalid_address("81fyq3t8Gxn1QWMG189EufHtMHXZXkfJtJKFJXqeA4GpSiuyfjVwVyp47PeQJnD7Tc8iK8TDvvhcmEmfh8nx7Va2ToP8wAo", MoneroNetworkType.MAINNET) + + # test invalid addresses on testnet + MoneroTestUtils.test_invalid_address(None, MoneroNetworkType.TESTNET) + MoneroTestUtils.test_invalid_address("", MoneroNetworkType.TESTNET) + MoneroTestUtils.test_invalid_address("91UBnNCkC3UKGygHCwYvAB1FscpjUuq5e9MYJd2rXuiiTjjfVeSVjnbSG5VTnJgBgy9Y7GTLfxpZNMUwNZjGfdFr1z79eV1", MoneroNetworkType.TESTNET) + MoneroTestUtils.test_invalid_address("A1AroB2EoJzKGygHCwYvAB1FscpjUuq5e9MYJd2rXuiiTjjfVeSVjnbSG5VTnJgBgy9Y7GTLfxpZNMUwNZjGfdFr2QY5Ba2aHhTEdQa2ra", MoneroNetworkType.TESTNET) + MoneroTestUtils.test_invalid_address("B1nKzHPJQDcg7xiP7bMN9MfPv9Z8ciT71iEMYnCdgBRBFETWgu9nKTr8fnzyGfU9h9gyNA8SFzYYzHfTS9KhqytSU943Nu1", MoneroNetworkType.TESTNET) + + # test invalid addresses on stagenet + MoneroTestUtils.test_invalid_address(None, MoneroNetworkType.STAGENET) + MoneroTestUtils.test_invalid_address("", MoneroNetworkType.STAGENET) + MoneroTestUtils.test_invalid_address("518s3obCY2ETeQB3GNAGPK2zRGen5UeW1WzegSizVsmf6z5NvM2GLoN6zzk1vHyzGAAfA8pGhuYAeCFZjHAp59jRVQkunGS", MoneroNetworkType.STAGENET) + MoneroTestUtils.test_invalid_address("51qY4cQh9HkTeQB3GNAGPK2zRGen5UeW1WzegSizVsmf6z5NvM2GLoN6zzk1vHyzGAAfA8pGhuYAeCFZjHAp59jRj6LZRFrjuGK8Whthg2", MoneroNetworkType.STAGENET) + MoneroTestUtils.test_invalid_address("718B5D2JmMh5TJVWFbygJR15dvio5Z5B24hfSrWDzeroM8j8Lqc9sMoFE6324xg2ReaAZqHJkgfGFRugRmYHugHZ4f17Gxo", MoneroNetworkType.STAGENET) + + # Can validate keys + def test_key_validation(self): + + # test private view key validation + MoneroTestUtils.assert_true(MoneroUtils.is_valid_private_view_key("86cf351d10894769feba29b9e201e12fb100b85bb52fc5825c864eef55c5840d")) + MoneroTestUtils.test_invalid_public_view_key("") + MoneroTestUtils.test_invalid_private_view_key(None) + MoneroTestUtils.test_invalid_private_view_key("5B8s3obCY2ETeQB3GNAGPK2zRGen5UeW1WzegSizVsmf6z5NvM2GLoN6zzk1vHyzGAAfA8pGhuYAeCFZjHAp59jRVQkunGS") + + # test public view key validation + MoneroTestUtils.assert_true(MoneroUtils.is_valid_public_view_key("99873d76ca874ff1aad676b835dd303abcb21c9911ca8a3d9130abc4544d8a0a")) + MoneroTestUtils.test_invalid_public_view_key("") + MoneroTestUtils.test_invalid_public_view_key(None) + MoneroTestUtils.test_invalid_public_view_key("z86cf351d10894769feba29b9e201e12fb100b85bb52fc5825c864eef55c5840d") + + # test private spend key validation + MoneroTestUtils.assert_true(MoneroUtils.is_valid_private_spend_key("e9ba887e93620ef9fafdfe0c6d3022949f1c5713cbd9ef631f18a0fb00421dee")) + MoneroTestUtils.test_invalid_private_spend_key("") + MoneroTestUtils.test_invalid_private_spend_key(None) + MoneroTestUtils.test_invalid_private_spend_key("z86cf351d10894769feba29b9e201e12fb100b85bb52fc5825c864eef55c5840d") + + # test public spend key validation + MoneroTestUtils.assert_true(MoneroUtils.is_valid_public_spend_key("3e48df9e9d8038dbf6f5382fac2becd8686273cda5bd87187e45dca7ec5af37b")) + MoneroTestUtils.test_invalid_public_spend_key("") + MoneroTestUtils.test_invalid_public_spend_key(None) + MoneroTestUtils.test_invalid_public_spend_key("z86cf351d10894769feba29b9e201e12fb100b85bb52fc5825c864eef55c5840d") + + # Can convert between XMR and atomic units + def test_atomic_unit_conversion(self): + assert 1000000000000 == MoneroUtils.xmr_to_atomic_units(1) + assert 1 == MoneroUtils.atomic_units_to_xmr(1000000000000) + assert 1000000000 == MoneroUtils.xmr_to_atomic_units(0.001) + assert 0.001 == MoneroUtils.atomic_units_to_xmr(1000000000) + assert 250000000000 == MoneroUtils.xmr_to_atomic_units(0.25) + assert 0.25 == MoneroUtils.atomic_units_to_xmr(250000000000) + assert 1250000000000 == MoneroUtils.xmr_to_atomic_units(1.25) + assert 1.25 == MoneroUtils.atomic_units_to_xmr(1250000000000) + assert 2796726190000 == MoneroUtils.xmr_to_atomic_units(2.79672619) + assert 2.79672619 == MoneroUtils.atomic_units_to_xmr(2796726190000) + assert 2796726190001 == MoneroUtils.xmr_to_atomic_units(2.796726190001) + assert 2.796726190001 == MoneroUtils.atomic_units_to_xmr(2796726190001) + assert 2796726189999 == MoneroUtils.xmr_to_atomic_units(2.796726189999) + assert 2.796726189999 == MoneroUtils.atomic_units_to_xmr(2796726189999) + assert 2796726180000 == MoneroUtils.xmr_to_atomic_units(2.79672618) + assert 2.79672618 == MoneroUtils.atomic_units_to_xmr(2796726180000) + + # Can get payment uri + def test_get_payment_uri(self): + config = MoneroTxConfig() + config.address = "42U9v3qs5CjZEePHBZHwuSckQXebuZu299NSmVEmQ41YJZQhKcPyujyMSzpDH4VMMVSBo3U3b54JaNvQLwAjqDhKS3rvM3L" + config.amount = 250000000000 + config.recipient_name = "John Doe" + config.note = "My transfer to wallet" + + payment_uri = MoneroUtils.get_payment_uri(config) + + assert payment_uri == "monero:42U9v3qs5CjZEePHBZHwuSckQXebuZu299NSmVEmQ41YJZQhKcPyujyMSzpDH4VMMVSBo3U3b54JaNvQLwAjqDhKS3rvM3L?tx_amount=0.250000000000&recipient_name=John%20Doe&tx_description=My%20transfer%20to%20wallet" diff --git a/tests/test_monero_wallet_common.py b/tests/test_monero_wallet_common.py index 816ec1c..b22bcd0 100644 --- a/tests/test_monero_wallet_common.py +++ b/tests/test_monero_wallet_common.py @@ -2,944 +2,939 @@ from abc import ABC, abstractmethod from typing import Optional -from time import time from datetime import datetime from monero import ( - MoneroWallet, MoneroWalletRpc, MoneroDaemonRpc, MoneroWalletConfig, MoneroUtils, - MoneroTxConfig, MoneroDestination, MoneroRpcConnection, MoneroError, - MoneroKeyImage, MoneroTxQuery + MoneroWallet, MoneroWalletRpc, MoneroDaemonRpc, MoneroWalletConfig, + MoneroTxConfig, MoneroDestination, MoneroRpcConnection, MoneroError, + MoneroKeyImage, MoneroTxQuery, MoneroUtils ) from utils import MoneroTestUtils as TestUtils, WalletEqualityUtils class BaseTestMoneroWallet(ABC): - _wallet: MoneroWallet - _daemon: MoneroDaemonRpc - - # region Private Methods - - def _get_test_daemon(self) -> MoneroDaemonRpc: - return TestUtils.get_daemon_rpc() - - @abstractmethod - def _open_wallet(self, config: Optional[MoneroWalletConfig]) -> MoneroWallet: - ... - - @abstractmethod - def _create_wallet(self, config: MoneroWalletConfig) -> MoneroWallet: - ... - - @abstractmethod - def _close_wallet(self, wallet: MoneroWallet, save: bool = False) -> None: - ... - - @abstractmethod - def _get_seed_languages(self) -> list[str]: - ... - - def _open_wallet_from_path(self, path: str, password: str | None) -> MoneroWallet: - config = MoneroWalletConfig() - config.path = path - config.password = password - - return self._open_wallet(config) - - @abstractmethod - def get_test_wallet(self) -> MoneroWallet: - ... - - # endregion - - # region Tests - - def test_create_wallet_random(self) -> None: - """ - Can create a random wallet. - """ - TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) - e1: Exception | None = None - wallet: MoneroWallet | None = None - try: - config = MoneroWalletConfig() - wallet = self._create_wallet(config) - path = wallet.get_path() - e2: Exception | None = None - - try: - MoneroUtils.validate_address(wallet.get_primary_address(), TestUtils.NETWORK_TYPE) - MoneroUtils.validate_private_view_key(wallet.get_private_view_key()) - MoneroUtils.validate_private_spend_key(wallet.get_private_spend_key()) - MoneroUtils.validate_mnemonic(wallet.get_seed()) - if not isinstance(wallet, MoneroWalletRpc): - TestUtils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, wallet.get_seed_language()) # TODO monero-wallet-rpc: get seed language - except Exception as e: - e2 = e - - self._close_wallet(wallet) - if e2 is not None: - raise e2 - - # attempt to create wallet at same path - try: + _wallet: MoneroWallet + _daemon: MoneroDaemonRpc + + # region Private Methods + + def _get_test_daemon(self) -> MoneroDaemonRpc: + return TestUtils.get_daemon_rpc() + + @abstractmethod + def _open_wallet(self, config: Optional[MoneroWalletConfig]) -> MoneroWallet: + ... + + @abstractmethod + def _create_wallet(self, config: MoneroWalletConfig) -> MoneroWallet: + ... + + @abstractmethod + def _close_wallet(self, wallet: MoneroWallet, save: bool = False) -> None: + ... + + @abstractmethod + def _get_seed_languages(self) -> list[str]: + ... + + def _open_wallet_from_path(self, path: str, password: str | None) -> MoneroWallet: config = MoneroWalletConfig() config.path = path - wallet = self._create_wallet(config) - except Exception as e: - TestUtils.assert_equals("Wallet already exists: " + path, str(e)) + config.password = password - # attempt to create wallet with unknown language - try: - config = MoneroWalletConfig() - config.language = "english" - raise Exception("Should have thrown error") - except Exception as e: - TestUtils.assert_equals("Unknown language: english", str(e)) + return self._open_wallet(config) + + @abstractmethod + def get_test_wallet(self) -> MoneroWallet: + ... + + # endregion + + # region Tests - except Exception as e: - e1 = e + def test_create_wallet_random(self) -> None: + """ + Can create a random wallet. + """ + TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) + e1: Exception | None = None + wallet: MoneroWallet | None = None + try: + config = MoneroWalletConfig() + wallet = self._create_wallet(config) + path = wallet.get_path() + e2: Exception | None = None + + try: + MoneroUtils.validate_address(wallet.get_primary_address(), TestUtils.NETWORK_TYPE) + MoneroUtils.validate_private_view_key(wallet.get_private_view_key()) + MoneroUtils.validate_private_spend_key(wallet.get_private_spend_key()) + MoneroUtils.validate_mnemonic(wallet.get_seed()) + if not isinstance(wallet, MoneroWalletRpc): + TestUtils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, wallet.get_seed_language()) # TODO monero-wallet-rpc: get seed language + except Exception as e: + e2 = e + + self._close_wallet(wallet) + if e2 is not None: + raise e2 + + # attempt to create wallet at same path + try: + config = MoneroWalletConfig() + config.path = path + wallet = self._create_wallet(config) + except Exception as e: + TestUtils.assert_equals("Wallet already exists: " + path, str(e)) + + # attempt to create wallet with unknown language + try: + config = MoneroWalletConfig() + config.language = "english" + raise Exception("Should have thrown error") + except Exception as e: + TestUtils.assert_equals("Unknown language: english", str(e)) + + except Exception as e: + e1 = e - if e1 is not None: - raise e1 + if e1 is not None: + raise e1 - def test_create_wallet_from_seed(self) -> None: - TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) - e1: Exception | None = None # emulating Python "finally" but compatible with other languages - try: - - # save for comparison - primaryAddress = self._wallet.get_primary_address() - privateViewKey = self._wallet.get_private_view_key() - privateSpendKey = self._wallet.get_private_spend_key() - - # recreate test wallet from seed - config = MoneroWalletConfig() - config.seed = TestUtils.SEED - config.restore_height = TestUtils.FIRST_RECEIVE_HEIGHT - - wallet: MoneroWallet = self._create_wallet(config) - path = wallet.get_path() - e2: Exception | None = None - try: - TestUtils.assert_equals(primaryAddress, wallet.get_primary_address()) - TestUtils.assert_equals(privateViewKey, wallet.get_private_view_key()) - TestUtils.assert_equals(privateSpendKey, wallet.get_private_spend_key()) - TestUtils.assert_equals(TestUtils.SEED, wallet.get_seed()) - if not isinstance(wallet, MoneroWalletRpc): - TestUtils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, wallet.get_seed_language()) - except Exception as e: - e2 = e - - self._close_wallet(wallet) - if e2 is not None: - raise e2 - - # attempt to create wallet with two missing words - try: + def test_create_wallet_from_seed(self) -> None: + TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) + e1: Exception | None = None # emulating Python "finally" but compatible with other languages + try: + + # save for comparison + primary_address = self._wallet.get_primary_address() + private_view_key = self._wallet.get_private_view_key() + private_spend_key = self._wallet.get_private_spend_key() + + # recreate test wallet from seed + config = MoneroWalletConfig() + config.seed = TestUtils.SEED + config.restore_height = TestUtils.FIRST_RECEIVE_HEIGHT + + wallet: MoneroWallet = self._create_wallet(config) + path = wallet.get_path() + e2: Exception | None = None + try: + TestUtils.assert_equals(primary_address, wallet.get_primary_address()) + TestUtils.assert_equals(private_view_key, wallet.get_private_view_key()) + TestUtils.assert_equals(private_spend_key, wallet.get_private_spend_key()) + TestUtils.assert_equals(TestUtils.SEED, wallet.get_seed()) + if not isinstance(wallet, MoneroWalletRpc): + TestUtils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, wallet.get_seed_language()) + except Exception as e: + e2 = e + + self._close_wallet(wallet) + if e2 is not None: + raise e2 + + # attempt to create wallet with two missing words + try: + config = MoneroWalletConfig() + config.seed = "memoir desk algebra inbound innocent unplugs fully okay five inflamed giant factual ritual toyed topic snake unhappy guarded tweezers haunted inundate giant" + config.restore_height = TestUtils.FIRST_RECEIVE_HEIGHT + wallet = self._create_wallet(config) + except Exception as e: + TestUtils.assert_equals("Invalid mnemonic", str(e)) + + # attempt to create wallet at same path + try: + config = MoneroWalletConfig() + config.path = path + self._create_wallet(config) + raise Exception("Should have thrown error") + except Exception as e: + TestUtils.assert_equals("Wallet already exists: " + path, str(e)) + + except Exception as e: + e1 = e + + if e1 is not None: + raise e1 + + def test_create_wallet_from_seed_with_offset(self) -> None: + TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) + e1: Exception | None = None # emulating Python "finally" but compatible with other languages + try: + + # create test wallet with offset + config = MoneroWalletConfig() + config.seed = TestUtils.SEED + config.restore_height = TestUtils.FIRST_RECEIVE_HEIGHT + config.seed_offset = "my secret offset!" + wallet: MoneroWallet = self._create_wallet(config) + e2: Exception | None = None + try: + MoneroUtils.validate_mnemonic(wallet.get_seed()) + TestUtils.assert_not_equals(TestUtils.SEED, wallet.get_seed()) + MoneroUtils.validate_address(wallet.get_primary_address(), TestUtils.NETWORK_TYPE) + TestUtils.assert_not_equals(TestUtils.ADDRESS, wallet.get_primary_address()) + if not isinstance(wallet, MoneroWalletRpc): + TestUtils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, wallet.get_seed_language()) # TODO monero-wallet-rpc: support + except Exception as e: + e2 = e + + self._close_wallet(wallet) + if e2 is not None: + raise e2 + except Exception as e: + e1 = e + + if e1 is not None: + raise e1 + + def test_create_wallet_from_keys(self) -> None: + TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) + e1: Exception | None = None # emulating Java "finally" but compatible with other languages + try: + # save for comparison + primary_address = self._wallet.get_primary_address() + private_view_key = self._wallet.get_private_view_key() + private_spend_key = self._wallet.get_private_spend_key() + + # recreate test wallet from keys + config = MoneroWalletConfig() + config.primary_address = primary_address + config.private_view_key = private_view_key + config.private_spend_key = private_spend_key + config.restore_height = self._daemon.get_height() + wallet: MoneroWallet = self._create_wallet(config) + path = wallet.get_path() + e2: Exception | None = None + try: + TestUtils.assert_equals(primary_address, wallet.get_primary_address()) + TestUtils.assert_equals(private_view_key, wallet.get_private_view_key()) + TestUtils.assert_equals(private_spend_key, wallet.get_private_spend_key()) + if not wallet.is_connected_to_daemon(): + print("WARNING: wallet created from keys is not connected to authenticated daemon") # TODO monero-project: keys wallets not connected + TestUtils.assert_true(wallet.is_connected_to_daemon(), "Wallet created from keys is not connected to authenticated daemon") + if not isinstance(wallet, MoneroWalletRpc): + MoneroUtils.validate_mnemonic(wallet.get_seed()) # TODO monero-wallet-rpc: cannot get seed from wallet created from keys? + TestUtils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, wallet.get_seed_language()) + + except Exception as e: + e2 = e + + self._close_wallet(wallet) + if e2 is not None: + raise e2 + + # recreate test wallet from spend key + if not isinstance(wallet, MoneroWalletRpc): # TODO monero-wallet-rpc: cannot create wallet from spend key? + config = MoneroWalletConfig() + config.private_spend_key = private_spend_key + config.restore_height = self._daemon.get_height() + wallet = self._create_wallet(config) + e2 = None + try: + TestUtils.assert_equals(primary_address, wallet.get_primary_address()) + TestUtils.assert_equals(private_view_key, wallet.get_private_view_key()) + TestUtils.assert_equals(private_spend_key, wallet.get_private_spend_key()) + if not wallet.is_connected_to_daemon(): + print("WARNING: wallet created from keys is not connected to authenticated daemon") # TODO monero-project: keys wallets not connected + TestUtils.assert_true(wallet.is_connected_to_daemon(), "Wallet created from keys is not connected to authenticated daemon") + if not isinstance(wallet, MoneroWalletRpc): + MoneroUtils.validate_mnemonic(wallet.get_seed()) # TODO monero-wallet-rpc: cannot get seed from wallet created from keys? + TestUtils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, wallet.get_seed_language()) + + except Exception as e: + e2 = e + + self._close_wallet(wallet) + if e2 is not None: + raise e2 + + # attempt to create wallet at same path + try: + config = MoneroWalletConfig() + config.path = path + self._create_wallet(config) + raise Exception("Should have thrown error") + except Exception as e: + TestUtils.assert_equals("Wallet already exists: " + path, str(e)) + + except Exception as e: + e1 = e + + + if e1 is not None: + raise e1 + + def test_subaddress_lookahead(self) -> None: + TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) + e1: Exception | None = None # emulating Java "finally" but compatible with other languages + receiver: MoneroWallet | None = None + try: + # create wallet with high subaddress lookahead + config = MoneroWalletConfig() + config.account_lookahead = 1 + config.subaddress_lookahead = 100000 + receiver = self._create_wallet(config) + + # transfer funds to subaddress with high index + tx_config = MoneroTxConfig() + tx_config.account_index = 0 + dest = MoneroDestination() + dest.address = receiver.get_subaddress(0, 85000).address + dest.amount = TestUtils.MAX_FEE + tx_config.destinations.append(dest) + tx_config.relay = True + + self._wallet.create_tx(tx_config) + + # observe unconfirmed funds + + TestUtils.wait_for(1000) + receiver.sync() + assert receiver.get_balance() > 0 + except Exception as e: + e1 = e + + if receiver is not None: + self._close_wallet(receiver) + if e1 is not None: + raise e1 + + def test_get_version(self) -> None: + TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) + version = self._wallet.get_version() + assert version.number is not None + assert version.number > 0 + assert version.is_release is not None + + def test_get_path(self) -> None: + TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) + + # create random wallet config = MoneroWalletConfig() - config.seed = "memoir desk algebra inbound innocent unplugs fully okay five inflamed giant factual ritual toyed topic snake unhappy guarded tweezers haunted inundate giant" - config.restore_height = TestUtils.FIRST_RECEIVE_HEIGHT wallet = self._create_wallet(config) - except Exception as e: - TestUtils.assert_equals("Invalid mnemonic", str(e)) - - # attempt to create wallet at same path - try: - config = MoneroWalletConfig() - config.path = path - self._create_wallet(config) - raise Exception("Should have thrown error") - except Exception as e: - TestUtils.assert_equals("Wallet already exists: " + path, str(e)) - - except Exception as e: - e1 = e - - if e1 is not None: - raise e1 - - def test_create_wallet_from_seed_with_offset(self) -> None: - TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) - e1: Exception | None = None # emulating Python "finally" but compatible with other languages - try: - - # create test wallet with offset - config = MoneroWalletConfig() - config.seed = TestUtils.SEED - config.restore_height = TestUtils.FIRST_RECEIVE_HEIGHT - config.seed_offset = "my secret offset!" - wallet: MoneroWallet = self._create_wallet(config) - e2: Exception | None = None - try: - MoneroUtils.validate_mnemonic(wallet.get_seed()) - TestUtils.assert_not_equals(TestUtils.SEED, wallet.get_seed()) - MoneroUtils.validate_address(wallet.get_primary_address(), TestUtils.NETWORK_TYPE) - TestUtils.assert_not_equals(TestUtils.ADDRESS, wallet.get_primary_address()) - if not isinstance(wallet, MoneroWalletRpc): - TestUtils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, wallet.get_seed_language()) # TODO monero-wallet-rpc: support - except Exception as e: - e2 = e - - self._close_wallet(wallet) - if e2 is not None: - raise e2 - except Exception as e: - e1 = e - - if e1 is not None: - raise e1 - - def test_create_wallet_from_keys(self) -> None: - TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) - e1: Exception | None = None # emulating Java "finally" but compatible with other languages - try: - # save for comparison - primaryAddress = self._wallet.get_primary_address() - privateViewKey = self._wallet.get_private_view_key() - privateSpendKey = self._wallet.get_private_spend_key() - - # recreate test wallet from keys - config = MoneroWalletConfig() - config.primary_address = primaryAddress - config.private_view_key = privateViewKey - config.private_spend_key = privateSpendKey - config.restore_height = self._daemon.get_height() - wallet: MoneroWallet = self._create_wallet(config) - path = wallet.get_path() - e2: Exception | None = None - try: - TestUtils.assert_equals(primaryAddress, wallet.get_primary_address()) - TestUtils.assert_equals(privateViewKey, wallet.get_private_view_key()) - TestUtils.assert_equals(privateSpendKey, wallet.get_private_spend_key()) - if not wallet.is_connected_to_daemon(): - print("WARNING: wallet created from keys is not connected to authenticated daemon") # TODO monero-project: keys wallets not connected - TestUtils.assert_true(wallet.is_connected_to_daemon(), "Wallet created from keys is not connected to authenticated daemon") - if not isinstance(wallet, MoneroWalletRpc): - MoneroUtils.validate_mnemonic(wallet.get_seed()) # TODO monero-wallet-rpc: cannot get seed from wallet created from keys? - TestUtils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, wallet.get_seed_language()) - - except Exception as e: - e2 = e - - self._close_wallet(wallet) - if e2 is not None: - raise e2 - - # recreate test wallet from spend key - if not isinstance(wallet, MoneroWalletRpc): # TODO monero-wallet-rpc: cannot create wallet from spend key? + + # set a random attribute + #String uuid = UUID.randomUUID().toString() + uuid = TestUtils.get_random_string() + wallet.set_attribute("uuid", uuid) + + # record the wallet's path then save and close + path = wallet.get_path() + self._close_wallet(wallet, True) + + # re-open the wallet using its path + wallet = self._open_wallet_from_path(path, None) + + # test the attribute + TestUtils.assert_equals(uuid, wallet.get_attribute("uuid")) + self._close_wallet(wallet) + + def test_set_daemon_connection(self): + + # create random wallet with default daemon connection config = MoneroWalletConfig() - config.private_spend_key = privateSpendKey - config.restore_height = self._daemon.get_height() wallet = self._create_wallet(config) - e2 = None + connection = MoneroRpcConnection(TestUtils.DAEMON_RPC_URI, TestUtils.DAEMON_RPC_USERNAME, TestUtils.DAEMON_RPC_PASSWORD) + TestUtils.assert_equals(connection, wallet.get_daemon_connection()) + TestUtils.assert_true(wallet.is_connected_to_daemon()) # uses default localhost connection + + # set empty server uri + wallet.set_daemon_connection("") + TestUtils.assert_equals(None, wallet.get_daemon_connection()) + TestUtils.assert_false(wallet.is_connected_to_daemon()) + + # set offline server uri + wallet.set_daemon_connection(TestUtils.OFFLINE_SERVER_URI) + connection = MoneroRpcConnection(TestUtils.OFFLINE_SERVER_URI, "", "") + TestUtils.assert_equals(connection, wallet.get_daemon_connection()) + TestUtils.assert_false(wallet.is_connected_to_daemon()) + + # set daemon with wrong credentials + wallet.set_daemon_connection(TestUtils.DAEMON_RPC_URI, "wronguser", "wrongpass") + connection = MoneroRpcConnection(TestUtils.DAEMON_RPC_URI, "wronguser", "wrongpass") + TestUtils.assert_equals(connection, wallet.get_daemon_connection()) + if "" == TestUtils.DAEMON_RPC_USERNAME: + TestUtils.assert_true(wallet.is_connected_to_daemon()) # TODO: monerod without authentication works with bad credentials? + else: + TestUtils.assert_false(wallet.is_connected_to_daemon()) + + # set daemon with authentication + wallet.set_daemon_connection(TestUtils.DAEMON_RPC_URI, TestUtils.DAEMON_RPC_USERNAME, TestUtils.DAEMON_RPC_PASSWORD) + connection = MoneroRpcConnection(TestUtils.DAEMON_RPC_URI, TestUtils.DAEMON_RPC_USERNAME, TestUtils.DAEMON_RPC_PASSWORD) + TestUtils.assert_equals(connection, wallet.get_daemon_connection()) + TestUtils.assert_true(wallet.is_connected_to_daemon()) + + # nullify daemon connection + wallet.set_daemon_connection(None) + TestUtils.assert_equals(None, wallet.get_daemon_connection()) + wallet.set_daemon_connection(TestUtils.DAEMON_RPC_URI) + connection = MoneroRpcConnection(TestUtils.DAEMON_RPC_URI) + TestUtils.assert_equals(connection, wallet.get_daemon_connection()) + wallet.set_daemon_connection(None) + TestUtils.assert_equals(None, wallet.get_daemon_connection()) + + # set daemon uri to non-daemon + wallet.set_daemon_connection("www.getmonero.org") + connection = MoneroRpcConnection("www.getmonero.org") + TestUtils.assert_equals(connection, wallet.get_daemon_connection()) + TestUtils.assert_false(wallet.is_connected_to_daemon()) + + # set daemon to invalid uri + wallet.set_daemon_connection("abc123") + TestUtils.assert_false(wallet.is_connected_to_daemon()) + + # attempt to sync try: - TestUtils.assert_equals(primaryAddress, wallet.get_primary_address()) - TestUtils.assert_equals(privateViewKey, wallet.get_private_view_key()) - TestUtils.assert_equals(privateSpendKey, wallet.get_private_spend_key()) - if not wallet.is_connected_to_daemon(): - print("WARNING: wallet created from keys is not connected to authenticated daemon") # TODO monero-project: keys wallets not connected - TestUtils.assert_true(wallet.is_connected_to_daemon(), "Wallet created from keys is not connected to authenticated daemon") - if not isinstance(wallet, MoneroWalletRpc): - MoneroUtils.validate_mnemonic(wallet.get_seed()) # TODO monero-wallet-rpc: cannot get seed from wallet created from keys? - TestUtils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, wallet.get_seed_language()) - + wallet.sync() + raise Exception("Exception expected") except Exception as e: - e2 = e + TestUtils.assert_equals("Wallet is not connected to daemon", str(e)) + finally: + self._close_wallet(wallet) + + def test_get_seed(self): + TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) + seed = self._wallet.get_seed() + MoneroUtils.validate_mnemonic(seed) + TestUtils.assert_equals(TestUtils.SEED, seed) + + def test_get_seed_language(self): + TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) + language = self._wallet.get_seed_language() + TestUtils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, language) + + def test_get_seed_languages(self): + TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) + languages = self._get_seed_languages() + assert len(languages) > 0 + for language in languages: + assert len(language) > 0 + + def test_get_private_view_key(self): + TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) + private_view_key = self._wallet.get_private_view_key() + MoneroUtils.validate_private_view_key(private_view_key) + + def test_get_private_spend_key(self): + TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) + private_spend_key = self._wallet.get_private_spend_key() + MoneroUtils.validate_private_spend_key(private_spend_key) + + def test_get_public_view_key(self): + TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) + public_view_key = self._wallet.get_public_view_key() + MoneroUtils.validate_private_spend_key(public_view_key) + + def test_get_public_spend_key(self): + TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) + public_spend_key = self._wallet.get_public_spend_key() + MoneroUtils.validate_private_spend_key(public_spend_key) + + def test_get_primary_address(self): + TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) + primary_address = self._wallet.get_primary_address() + MoneroUtils.validate_address(primary_address, TestUtils.NETWORK_TYPE) + TestUtils.assert_equals(self._wallet.get_address(0, 0), primary_address) + + def test_get_subaddress_address(self): + TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) + TestUtils.assert_equals(self._wallet.get_primary_address(), (self._wallet.get_address(0, 0))) + for account in self._wallet.get_accounts(True): + for subaddress in account.subaddresses: + assert account.index is not None + assert subaddress.index is not None + TestUtils.assert_equals(subaddress.address, self._wallet.get_address(account.index, subaddress.index)) + + def test_get_subaddress_address_out_of_range(self): + TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) + accounts = self._wallet.get_accounts(True) + account_idx = len(accounts) - 1 + subaddress_idx = len(accounts[account_idx].subaddresses) + address = self._wallet.get_address(account_idx, subaddress_idx) + TestUtils.assert_not_none(address) + TestUtils.assert_true(len(address) > 0) + + def test_get_address_indices(self): + TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) + wallet = self._wallet + # get last subaddress to test + accounts = wallet.get_accounts(True) + account_idx = len(accounts) - 1 + subaddress_idx = len(accounts[account_idx].subaddresses) - 1 + address = wallet.get_address(account_idx, subaddress_idx) + TestUtils.assert_not_none(address) - self._close_wallet(wallet) - if e2 is not None: - raise e2 - - # attempt to create wallet at same path - try: - config = MoneroWalletConfig() - config.path = path - self._create_wallet(config) - raise Exception("Should have thrown error") - except Exception as e: - TestUtils.assert_equals("Wallet already exists: " + path, str(e)) - - except Exception as e: - e1 = e - - - if e1 is not None: - raise e1 - - def test_subaddress_lookahead(self) -> None: - TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) - e1: Exception | None = None # emulating Java "finally" but compatible with other languages - receiver: MoneroWallet | None = None - try: - # create wallet with high subaddress lookahead - config = MoneroWalletConfig() - config.account_lookahead = 1 - config.subaddress_lookahead = 100000 - receiver = self._create_wallet(config) - - # transfer funds to subaddress with high index - tx_config = MoneroTxConfig() - tx_config.account_index = 0 - dest = MoneroDestination() - dest.address = receiver.get_subaddress(0, 85000).address - dest.amount = TestUtils.MAX_FEE - tx_config.destinations.append(dest) - tx_config.relay = True - - self._wallet.create_tx(tx_config) - - # observe unconfirmed funds - - TestUtils.wait_for(1000) - receiver.sync() - assert receiver.get_balance() > 0 - except Exception as e: - e1 = e - - if receiver is not None: - self._close_wallet(receiver) - if e1 is not None: - raise e1 - - def test_get_version(self) -> None: - TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) - version = self._wallet.get_version() - assert version.number is not None - assert version.number > 0 - assert version.is_release is not None - - def test_get_path(self) -> None: - TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) - - # create random wallet - config = MoneroWalletConfig() - wallet = self._create_wallet(config) - - # set a random attribute - #String uuid = UUID.randomUUID().toString() - uuid = TestUtils.get_random_string() - wallet.set_attribute("uuid", uuid) - - # record the wallet's path then save and close - path = wallet.get_path() - self._close_wallet(wallet, True) - - # re-open the wallet using its path - wallet = self._open_wallet_from_path(path, None) - - # test the attribute - TestUtils.assert_equals(uuid, wallet.get_attribute("uuid")) - self._close_wallet(wallet) + # get address index + subaddress = wallet.get_address_index(address) + TestUtils.assert_equals(account_idx, subaddress.account_index) + TestUtils.assert_equals(subaddress_idx, subaddress.index) - def test_set_daemon_connection(self): - - # create random wallet with default daemon connection - config = MoneroWalletConfig() - wallet = self._create_wallet(config) - connection = MoneroRpcConnection(TestUtils.DAEMON_RPC_URI, TestUtils.DAEMON_RPC_USERNAME, TestUtils.DAEMON_RPC_PASSWORD) - TestUtils.assert_equals(connection, wallet.get_daemon_connection()) - TestUtils.assert_true(wallet.is_connected_to_daemon()) # uses default localhost connection - - # set empty server uri - wallet.set_daemon_connection("") - TestUtils.assert_equals(None, wallet.get_daemon_connection()) - TestUtils.assert_false(wallet.is_connected_to_daemon()) - - # set offline server uri - wallet.set_daemon_connection(TestUtils.OFFLINE_SERVER_URI) - connection = MoneroRpcConnection(TestUtils.OFFLINE_SERVER_URI, "", "") - TestUtils.assert_equals(connection, wallet.get_daemon_connection()) - TestUtils.assert_false(wallet.is_connected_to_daemon()) - - # set daemon with wrong credentials - wallet.set_daemon_connection(TestUtils.DAEMON_RPC_URI, "wronguser", "wrongpass") - connection = MoneroRpcConnection(TestUtils.DAEMON_RPC_URI, "wronguser", "wrongpass") - TestUtils.assert_equals(connection, wallet.get_daemon_connection()) - if "" == TestUtils.DAEMON_RPC_USERNAME or TestUtils.DAEMON_RPC_USERNAME is None: - TestUtils.assert_true(wallet.is_connected_to_daemon()) # TODO: monerod without authentication works with bad credentials? - else: - TestUtils.assert_false(wallet.is_connected_to_daemon()) - - # set daemon with authentication - wallet.set_daemon_connection(TestUtils.DAEMON_RPC_URI, TestUtils.DAEMON_RPC_USERNAME, TestUtils.DAEMON_RPC_PASSWORD) - connection = MoneroRpcConnection(TestUtils.DAEMON_RPC_URI, TestUtils.DAEMON_RPC_USERNAME, TestUtils.DAEMON_RPC_PASSWORD) - TestUtils.assert_equals(connection, wallet.get_daemon_connection()) - TestUtils.assert_true(wallet.is_connected_to_daemon()) - - # nullify daemon connection - wallet.set_daemon_connection(None) - TestUtils.assert_equals(None, wallet.get_daemon_connection()) - wallet.set_daemon_connection(TestUtils.DAEMON_RPC_URI) - connection = MoneroRpcConnection(TestUtils.DAEMON_RPC_URI) - TestUtils.assert_equals(connection, wallet.get_daemon_connection()) - wallet.set_daemon_connection(None) - TestUtils.assert_equals(None, wallet.get_daemon_connection()) - - # set daemon uri to non-daemon - wallet.set_daemon_connection("www.getmonero.org") - connection = MoneroRpcConnection("www.getmonero.org") - TestUtils.assert_equals(connection, wallet.get_daemon_connection()) - TestUtils.assert_false(wallet.is_connected_to_daemon()) - - # set daemon to invalid uri - wallet.set_daemon_connection("abc123") - TestUtils.assert_false(wallet.is_connected_to_daemon()) - - # attempt to sync - try: - wallet.sync() - raise Exception("Exception expected") - except Exception as e: - TestUtils.assert_equals("Wallet is not connected to daemon", str(e)) - finally: - self._close_wallet(wallet) - - def test_get_seed(self): - TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) - seed = self._wallet.get_seed() - MoneroUtils.validate_mnemonic(seed) - TestUtils.assert_equals(TestUtils.SEED, seed) - - def test_get_seed_language(self): - TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) - language = self._wallet.get_seed_language() - TestUtils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, language) - - def test_get_seed_languages(self): - TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) - languages = self._get_seed_languages() - assert len(languages) > 0 - for language in languages: - assert len(language) > 0 - - def test_get_private_view_key(self): - TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) - privateViewKey = self._wallet.get_private_view_key() - MoneroUtils.validate_private_view_key(privateViewKey) - - def test_get_private_spend_key(self): - TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) - privateSpendKey = self._wallet.get_private_spend_key() - MoneroUtils.validate_private_spend_key(privateSpendKey) - - def test_get_public_view_key(self): - TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) - publicViewKey = self._wallet.get_public_view_key() - MoneroUtils.validate_private_spend_key(publicViewKey) - - def test_get_public_spend_key(self): - TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) - publicSpendKey = self._wallet.get_public_spend_key() - MoneroUtils.validate_private_spend_key(publicSpendKey) - - def test_get_primary_address(self): - TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) - primaryAddress = self._wallet.get_primary_address() - MoneroUtils.validate_address(primaryAddress, TestUtils.NETWORK_TYPE) - TestUtils.assert_equals(self._wallet.get_address(0, 0), primaryAddress) - - def test_get_subaddress_address(self): - TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) - TestUtils.assert_equals(self._wallet.get_primary_address(), (self._wallet.get_address(0, 0))) - for account in self._wallet.get_accounts(True): - for subaddress in account.subaddresses: - assert account.index is not None - assert subaddress.index is not None - TestUtils.assert_equals(subaddress.address, self._wallet.get_address(account.index, subaddress.index)) - - def test_get_subaddress_address_out_of_range(self): - TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) - accounts = self._wallet.get_accounts(True) - accountIdx = len(accounts) - 1 - subaddressIdx = len(accounts[accountIdx].subaddresses) - address = self._wallet.get_address(accountIdx, subaddressIdx) - TestUtils.assert_not_none(address) - TestUtils.assert_true(len(address) > 0) - - def test_get_address_indices(self): - TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) - wallet = self._wallet - # get last subaddress to test - accounts = wallet.get_accounts(True) - accountIdx = len(accounts) - 1 - subaddressIdx = len(accounts[accountIdx].subaddresses) - 1 - address = wallet.get_address(accountIdx, subaddressIdx) - TestUtils.assert_not_none(address) - - # get address index - subaddress = wallet.get_address_index(address) - TestUtils.assert_equals(accountIdx, subaddress.account_index) - TestUtils.assert_equals(subaddressIdx, subaddress.index) - - # test valid but unfound address - nonWalletAddress = TestUtils.get_external_wallet_address() - try: - subaddress = wallet.get_address_index(nonWalletAddress) - raise Exception("Should have thrown exception") - except Exception as e: - TestUtils.assert_equals("Address doesn't belong to the wallet", str(e)) - - # test invalid address - try: - subaddress = wallet.get_address_index("this is definitely not an address") - raise Exception("Should have thrown exception") - except Exception as e: - TestUtils.assert_equals("Invalid address", str(e)) - - def test_decode_integrated_address(self): - TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) - wallet = self._wallet - integratedAddress = wallet.get_integrated_address('', "03284e41c342f036") - decodedAddress = wallet.decode_integrated_address(integratedAddress.integrated_address) - TestUtils.assert_equals(integratedAddress, decodedAddress) - - # decode invalid address - try: - wallet.decode_integrated_address("bad address") - raise Exception("Should have failed decoding bad address") - except Exception as e: - TestUtils.assert_equals("Invalid address", str(e)) - - def test_sync_without_progress(self): - TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) - wallet = self._wallet - daemon = self._daemon - numBlocks = 100 - chainHeight = daemon.get_height() - TestUtils.assert_true(chainHeight >= numBlocks) - result = wallet.sync(chainHeight - numBlocks) # sync end of chain - TestUtils.assert_true(result.num_blocks_fetched >= 0) - TestUtils.assert_not_none(result.received_money) - - def test_wallet_equality_ground_truth(self): - TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) - wallet = self._wallet - daemon = self._daemon - TestUtils.WALLET_TX_TRACKER.wait_for_wallet_txs_to_clear_pool(daemon, TestUtils.SYNC_PERIOD_IN_MS, [wallet]) - walletGt = TestUtils.create_wallet_ground_truth(TestUtils.NETWORK_TYPE, TestUtils.SEED, None, TestUtils.FIRST_RECEIVE_HEIGHT) - try: - WalletEqualityUtils.testWalletEqualityOnChain(daemon, TestUtils.SYNC_PERIOD_IN_MS, TestUtils.WALLET_TX_TRACKER, walletGt, wallet) - finally: - walletGt.close() + # test valid but unfound address + non_wallet_address = TestUtils.get_external_wallet_address() + try: + subaddress = wallet.get_address_index(non_wallet_address) + raise Exception("Should have thrown exception") + except Exception as e: + TestUtils.assert_equals("Address doesn't belong to the wallet", str(e)) + + # test invalid address + try: + subaddress = wallet.get_address_index("this is definitely not an address") + raise Exception("Should have thrown exception") + except Exception as e: + TestUtils.assert_equals("Invalid address", str(e)) + + def test_decode_integrated_address(self): + TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) + wallet = self._wallet + integrated_address = wallet.get_integrated_address('', "03284e41c342f036") + decoded_address = wallet.decode_integrated_address(integrated_address.integrated_address) + TestUtils.assert_equals(integrated_address, decoded_address) + + # decode invalid address + try: + wallet.decode_integrated_address("bad address") + raise Exception("Should have failed decoding bad address") + except Exception as e: + TestUtils.assert_equals("Invalid address", str(e)) + + def test_sync_without_progress(self): + TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) + wallet = self._wallet + daemon = self._daemon + num_blocks = 100 + chain_height = daemon.get_height() + TestUtils.assert_true(chain_height >= num_blocks) + result = wallet.sync(chain_height - num_blocks) # sync end of chain + TestUtils.assert_true(result.num_blocks_fetched >= 0) + TestUtils.assert_not_none(result.received_money) + + def test_wallet_equality_ground_truth(self): + TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) + wallet = self._wallet + daemon = self._daemon + TestUtils.WALLET_TX_TRACKER.wait_for_wallet_txs_to_clear_pool(daemon, TestUtils.SYNC_PERIOD_IN_MS, [wallet]) + wallet_gt = TestUtils.create_wallet_ground_truth(TestUtils.NETWORK_TYPE, TestUtils.SEED, None, TestUtils.FIRST_RECEIVE_HEIGHT) + try: + WalletEqualityUtils.test_wallet_equality_on_chain(daemon, TestUtils.SYNC_PERIOD_IN_MS, TestUtils.WALLET_TX_TRACKER, wallet_gt, wallet) + finally: + wallet_gt.close() - def test_get_height(self): - TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) - height = self._wallet.get_height() - assert height >= 0 + def test_get_height(self): + TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) + height = self._wallet.get_height() + assert height >= 0 - def test_get_height_by_date(self): - TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) - - # collect dates to test starting 100 days ago - DAY_MS = 24 * 60 * 60 * 1000 - yesterday = TestUtils.current_timestamp() - DAY_MS # TODO monero-project: today's date can throw exception as "in future" so we test up to yesterday - dates: list[datetime] = [] - i = 99 - - while i >= 0: - dates.append(datetime.fromtimestamp((yesterday - DAY_MS * i) / 1000)) # subtract i days - i -= 1 - - # test heights by date - lastHeight: Optional[int] = None - for date in dates: - height = self._wallet.get_height_by_date(date.year + 1900, date.month + 1, date.day) - assert (height >= 0) - if (lastHeight is not None): - assert (height >= lastHeight) - lastHeight = height - - assert lastHeight is not None - assert (lastHeight >= 0) - height = self._wallet.get_height() - assert (height >= 0) - - # test future date - try: - tomorrow = datetime.fromtimestamp((yesterday + DAY_MS * 2) / 1000) - self._wallet.get_height_by_date(tomorrow.year + 1900, tomorrow.month + 1, tomorrow.day) - raise Exception("Expected exception on future date") - except MoneroError as err: - assert "specified date is in the future" == str(err) - - def test_get_all_balances(self): - TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) - - # fetch accounts with all info as reference - accounts = self._wallet.get_accounts(True) - wallet = self._wallet - # test that balances add up between accounts and wallet - accountsBalance = 0 - accountsUnlockedBalance = 0 - for account in accounts: - assert account.index is not None - assert account.balance is not None - assert account.unlocked_balance is not None - accountsBalance += account.balance - accountsUnlockedBalance += account.unlocked_balance - - # test that balances add up between subaddresses and accounts - subaddressesBalance = 0 - subaddressesUnlockedBalance = 0 - for subaddress in account.subaddresses: - assert subaddress.account_index is not None - assert subaddress.index is not None - assert subaddress.balance is not None - assert subaddress.unlocked_balance is not None - subaddressesBalance += subaddress.balance - subaddressesUnlockedBalance += subaddress.unlocked_balance - - # test that balances are consistent with get_accounts() call - assert wallet.get_balance(subaddress.account_index, subaddress.index) == subaddress.balance - assert wallet.get_unlocked_balance(subaddress.account_index, subaddress.index) == subaddress.unlocked_balance - - assert wallet.get_balance(account.index) == subaddressesBalance - assert wallet.get_unlocked_balance(account.index) == subaddressesUnlockedBalance - - TestUtils.test_unsigned_big_integer(accountsBalance) - TestUtils.test_unsigned_big_integer(accountsUnlockedBalance) - assert wallet.get_balance() == accountsBalance - assert wallet.get_unlocked_balance() == accountsUnlockedBalance - - def test_get_accounts_without_subaddresses(self): - TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) - accounts = self._wallet.get_accounts() - assert len(accounts) > 0 - for account in accounts: - TestUtils.test_account(account) - assert len(account.subaddresses) == 0 - - def test_get_accounts_with_subaddresses(self): - TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) - accounts = self._wallet.get_accounts(True) - assert len(accounts) > 0 - for account in accounts: - TestUtils.test_account(account) - assert len(account.subaddresses) > 0 - - def test_get_account(self): - TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) - accounts = self._wallet.get_accounts() - assert len(accounts) > 0 - for account in accounts: - TestUtils.test_account(account) - - # test without subaddresses - assert account.index is not None - retrieved = self._wallet.get_account(account.index) - assert len(retrieved.subaddresses) == 0 - - # test with subaddresses - retrieved = self._wallet.get_account(account.index, True) - assert len(retrieved.subaddresses) > 0 - - def test_create_account_without_label(self): - TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) - accountsBefore = self._wallet.get_accounts() - createdAccount = self._wallet.create_account() - TestUtils.test_account(createdAccount) - assert len(accountsBefore) == len(self._wallet.get_accounts()) - 1 - - def test_create_account_with_label(self): - TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) - wallet = self._wallet - # create account with label - accountsBefore = wallet.get_accounts() - label = TestUtils.get_random_string() - createdAccount = wallet.create_account(label) - TestUtils.test_account(createdAccount) - assert createdAccount.index is not None - assert len(accountsBefore) == len(wallet.get_accounts()) - 1 - assert label == wallet.get_subaddress(createdAccount.index, 0).label - - # fetch and test account - createdAccount = wallet.get_account(createdAccount.index) - TestUtils.test_account(createdAccount) - - # create account with same label - createdAccount = wallet.create_account(label) - TestUtils.test_account(createdAccount) - assert len(accountsBefore) == len(wallet.get_accounts()) - 2 - assert createdAccount.index is not None - assert label == wallet.get_subaddress(createdAccount.index, 0).label - - # fetch and test account - createdAccount = wallet.get_account(createdAccount.index) - TestUtils.test_account(createdAccount) - - def test_set_account_label(self): - # create account - wallet = self._wallet - if len(wallet.get_accounts()) < 2: - wallet.create_account() - - # set account label - label = TestUtils.get_random_string() - wallet.set_account_label(1, label) - assert label == wallet.get_subaddress(1, 0).label - - def test_get_subaddresses(self): - TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) - wallet = self._wallet - accounts = wallet.get_accounts() - assert len(accounts) > 0 - for account in accounts: - assert account.index is not None - subaddresses = wallet.get_subaddresses(account.index) - assert len(subaddresses) > 0 - for subaddress in subaddresses: - TestUtils.test_subaddress(subaddress) - assert account.index == subaddress.account_index - - def test_get_subaddresses_by_indices(self): - TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) - wallet = self._wallet - accounts = wallet.get_accounts() - assert len(accounts) > 0 - for account in accounts: - - # get subaddresses - assert account.index is not None - subaddresses = wallet.get_subaddresses(account.index) - assert len(subaddresses) > 0 - - # remove a subaddress for query if possible - if len(subaddresses) > 1: - subaddresses.remove(subaddresses[0]) - - # get subaddress indices - subaddressIndices: list[int] = [] - for subaddress in subaddresses: - assert subaddress.index is not None - subaddressIndices.append(subaddress.index) - assert len(subaddressIndices) > 0 - - # fetch subaddresses by indices - fetchedSubaddresses = wallet.get_subaddresses(account.index, subaddressIndices) - - # original subaddresses (minus one removed if applicable) is equal to fetched subaddresses - assert TestUtils.assert_subaddresses_equal(subaddresses, fetchedSubaddresses) - - def test_get_subaddress_by_index(self): - TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) - wallet = self._wallet - accounts = wallet.get_accounts() - assert len(accounts) > 0 - for account in accounts: - assert account.index is not None - subaddresses = wallet.get_subaddresses(account.index) - assert len(subaddresses) > 0 - for subaddress in subaddresses: - assert subaddress.index is not None - TestUtils.test_subaddress(subaddress) - TestUtils.assert_subaddress_equal(subaddress, wallet.get_subaddress(account.index, subaddress.index)) - TestUtils.assert_subaddress_equal(subaddress, wallet.get_subaddresses(account.index, [subaddress.index])[0]) # test plural call with single subaddr number - - def test_create_subaddress(self): - TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) - wallet = self._wallet - # create subaddresses across accounts - accounts = wallet.get_accounts() - if len(accounts) < 2: - wallet.create_account() - accounts = wallet.get_accounts() - assert len(accounts) > 1 - accountIdx = 0 - while accountIdx < 2: - - # create subaddress with no label - subaddresses = wallet.get_subaddresses(accountIdx) - subaddress = wallet.create_subaddress(accountIdx) - assert subaddress.label is None - TestUtils.test_subaddress(subaddress) - subaddressesNew = wallet.get_subaddresses(accountIdx) - assert len(subaddressesNew) - 1 == len(subaddresses) - TestUtils.assert_subaddress_equal(subaddress, subaddressesNew[len(subaddressesNew) - 1]) - - # create subaddress with label - subaddresses = wallet.get_subaddresses(accountIdx) - uuid = TestUtils.get_random_string() - subaddress = wallet.create_subaddress(accountIdx, uuid) - assert (uuid == subaddress.label) - TestUtils.test_subaddress(subaddress) - subaddressesNew = wallet.get_subaddresses(accountIdx) - assert len(subaddresses) == len(subaddressesNew) - 1 - TestUtils.assert_subaddress_equal(subaddress, subaddressesNew[len(subaddressesNew) - 1]) - - accountIdx += 1 - - def test_set_subaddress_label(self): - wallet = self._wallet - # create subaddresses - while len(wallet.get_subaddresses(0)) < 3: - wallet.create_subaddress(0) - - # set subaddress labels - subaddressIdx = 0 - while subaddressIdx < len(wallet.get_subaddresses(0)): - label = TestUtils.get_random_string() - wallet.set_subaddress_label(0, subaddressIdx, label) - assert (label == wallet.get_subaddress(0, subaddressIdx).label) - subaddressIdx += 1 - - # [...] - - def test_set_tx_note(self) -> None: - TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) - wallet = self._wallet - txs = TestUtils.get_random_transactions(wallet, None, 1, 5) - - # set notes - uuid = TestUtils.get_random_string() - i: int = 0 - - while i < len(txs): - tx_hash = txs[i].hash - assert tx_hash is not None - wallet.set_tx_note(tx_hash, f"{uuid}{i}") - i += 1 - - i = 0 - # get notes - while i < len(txs): - tx_hash = txs[i].hash - assert tx_hash is not None - assert wallet.get_tx_note(tx_hash) == f"{uuid}{i}" - i += 1 - - def test_set_tx_notes(self): - TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) - wallet = self._wallet - # set tx notes - uuid = TestUtils.get_random_string() - txs = wallet.get_txs() - assert len(txs) >= 3, "Test requires 3 or more wallet transactions run send tests" - txHashes: list[str] = [] - txNotes: list[str] = [] - i = 0 - while i < len(txHashes): - tx_hash = txs[i].hash - assert tx_hash is not None - txHashes.append(tx_hash) - txNotes.append(f"{uuid}{i}") - i += 1 - - wallet.set_tx_notes(txHashes, txNotes) - - # get tx notes - txNotes = wallet.get_tx_notes(txHashes) - for tx_note in txNotes: - assert f"{uuid}{i}" == tx_note - - # TODO: test that get transaction has note - - def test_export_key_images(self): - TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) - wallet = self._wallet - images = wallet.export_key_images(True) - assert len(images) > 0, "No signed key images in wallet" - - for image in images: - assert isinstance(image, MoneroKeyImage) - assert image.hex is not None and len(image.hex) > 0 - assert image.signature is not None and len(image.signature) > 0 - - # wallet exports key images since last export by default - images = wallet.export_key_images() - imagesAll: list[MoneroKeyImage] = wallet.export_key_images(True) - assert len(imagesAll) > len(images) - - def test_get_new_key_images_from_last_import(self): - TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) - wallet = self._wallet - # get outputs hex - outputsHex = wallet.export_outputs() - - # import outputs hex - if outputsHex is not None: - numImported = wallet.import_outputs(outputsHex) - assert numImported >= 0 - - # get and test new key images from last import - images = wallet.get_new_key_images_from_last_import() - if len(images) == 0: - raise Exception("No new key images in last import") # TODO: these are already known to the wallet, so no new key images will be imported - for image in images: - assert image.hex is not None and len(image.hex) > 0 - assert image.signature is not None and len(image.signature) > 0 - - def test_import_key_images(self): - TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) - wallet = self._wallet - images = wallet.export_key_images() - assert len(images) > 0, "Wallet does not have any key images run send tests" - result = wallet.import_key_images(images) - assert result.height is not None and result.height > 0 - - # determine if non-zero spent and unspent amounts are expected - query = MoneroTxQuery() - query.is_outgoing = True - query.is_confirmed = True - txs = wallet.get_txs(query) - balance = wallet.get_balance() - hasSpent = len(txs) > 0 - hasUnspent = balance > 0 - - # test amounts - TestUtils.test_unsigned_big_integer(result.spent_amount, hasSpent) - TestUtils.test_unsigned_big_integer(result.unspent_amount, hasUnspent) - - # [...] - - def test_get_payment_uri(self): - TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) - wallet = self._wallet - # test with address and amount - config1 = MoneroTxConfig() - config1.address = wallet.get_address(0, 0) - config1.amount = 0 - uri = wallet.get_payment_uri(config1) - config2 = wallet.parse_payment_uri(uri) - TestUtils.assert_equals(config1, config2) - - # test with subaddress and all fields - config1.destinations[0].address = wallet.get_subaddress(0, 1).address - config1.destinations[0].amount = 425000000000 - config1.recipient_name = "John Doe" - config1.note = "OMZG XMR FTW" - uri = wallet.get_payment_uri(config1) - config2 = wallet.parse_payment_uri(uri) - TestUtils.assert_equals(config1, config2) - - # test with undefined address - address = config1.destinations[0].address - config1.destinations[0].address = None - try: - wallet.get_payment_uri(config1) - raise Exception("Should have thrown RPC exception with invalid parameters") - except Exception as e: - assert str(e).index("Cannot make URI from supplied parameters") >= 0 - - config1.destinations[0].address = address - - # test with standalone payment id - config1.payment_id = "03284e41c342f03603284e41c342f03603284e41c342f03603284e41c342f036" - try: - wallet.get_payment_uri(config1) - raise Exception("Should have thrown RPC exception with invalid parameters") - except Exception as e: - assert str(e).index("Cannot make URI from supplied parameters") >= 0 - - def test_mining(self): - TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) - daemon = self._daemon - wallet = self._wallet - - status = daemon.get_mining_status() - if status.is_active: - wallet.stop_mining() - wallet.start_mining(2, False, True) - wallet.stop_mining() - - # endregion \ No newline at end of file + def test_get_height_by_date(self): + TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) + + # collect dates to test starting 100 days ago + DAY_MS = 24 * 60 * 60 * 1000 + yesterday = TestUtils.current_timestamp() - DAY_MS # TODO monero-project: today's date can throw exception as "in future" so we test up to yesterday + dates: list[datetime] = [] + i = 99 + + while i >= 0: + dates.append(datetime.fromtimestamp((yesterday - DAY_MS * i) / 1000)) # subtract i days + i -= 1 + + # test heights by date + last_height: Optional[int] = None + for date in dates: + height = self._wallet.get_height_by_date(date.year + 1900, date.month + 1, date.day) + assert (height >= 0) + if (last_height is not None): + assert (height >= last_height) + last_height = height + + assert last_height is not None + assert (last_height >= 0) + height = self._wallet.get_height() + assert (height >= 0) + + # test future date + try: + tomorrow = datetime.fromtimestamp((yesterday + DAY_MS * 2) / 1000) + self._wallet.get_height_by_date(tomorrow.year + 1900, tomorrow.month + 1, tomorrow.day) + raise Exception("Expected exception on future date") + except MoneroError as err: + assert "specified date is in the future" == str(err) + + def test_get_all_balances(self): + TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) + + # fetch accounts with all info as reference + accounts = self._wallet.get_accounts(True) + wallet = self._wallet + # test that balances add up between accounts and wallet + accounts_balance = 0 + accounts_unlocked_balance = 0 + for account in accounts: + assert account.index is not None + assert account.balance is not None + assert account.unlocked_balance is not None + accounts_balance += account.balance + accounts_unlocked_balance += account.unlocked_balance + + # test that balances add up between subaddresses and accounts + subaddresses_balance = 0 + subaddresses_unlocked_balance = 0 + for subaddress in account.subaddresses: + assert subaddress.account_index is not None + assert subaddress.index is not None + assert subaddress.balance is not None + assert subaddress.unlocked_balance is not None + subaddresses_balance += subaddress.balance + subaddresses_unlocked_balance += subaddress.unlocked_balance + + # test that balances are consistent with get_accounts() call + assert wallet.get_balance(subaddress.account_index, subaddress.index) == subaddress.balance + assert wallet.get_unlocked_balance(subaddress.account_index, subaddress.index) == subaddress.unlocked_balance + + assert wallet.get_balance(account.index) == subaddresses_balance + assert wallet.get_unlocked_balance(account.index) == subaddresses_unlocked_balance + + TestUtils.test_unsigned_big_integer(accounts_balance) + TestUtils.test_unsigned_big_integer(accounts_unlocked_balance) + assert wallet.get_balance() == accounts_balance + assert wallet.get_unlocked_balance() == accounts_unlocked_balance + + def test_get_accounts_without_subaddresses(self): + TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) + accounts = self._wallet.get_accounts() + assert len(accounts) > 0 + for account in accounts: + TestUtils.test_account(account) + assert len(account.subaddresses) == 0 + + def test_get_accounts_with_subaddresses(self): + TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) + accounts = self._wallet.get_accounts(True) + assert len(accounts) > 0 + for account in accounts: + TestUtils.test_account(account) + assert len(account.subaddresses) > 0 + + def test_get_account(self): + TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) + accounts = self._wallet.get_accounts() + assert len(accounts) > 0 + for account in accounts: + TestUtils.test_account(account) + + # test without subaddresses + assert account.index is not None + retrieved = self._wallet.get_account(account.index) + assert len(retrieved.subaddresses) == 0 + + # test with subaddresses + retrieved = self._wallet.get_account(account.index, True) + assert len(retrieved.subaddresses) > 0 + + def test_create_account_without_label(self): + TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) + accounts_before = self._wallet.get_accounts() + created_account = self._wallet.create_account() + TestUtils.test_account(created_account) + assert len(accounts_before) == len(self._wallet.get_accounts()) - 1 + + def test_create_account_with_label(self): + TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) + wallet = self._wallet + # create account with label + accounts_before = wallet.get_accounts() + label = TestUtils.get_random_string() + created_account = wallet.create_account(label) + TestUtils.test_account(created_account) + assert created_account.index is not None + assert len(accounts_before) == len(wallet.get_accounts()) - 1 + assert label == wallet.get_subaddress(created_account.index, 0).label + + # fetch and test account + created_account = wallet.get_account(created_account.index) + TestUtils.test_account(created_account) + + # create account with same label + created_account = wallet.create_account(label) + TestUtils.test_account(created_account) + assert len(accounts_before) == len(wallet.get_accounts()) - 2 + assert created_account.index is not None + assert label == wallet.get_subaddress(created_account.index, 0).label + + # fetch and test account + created_account = wallet.get_account(created_account.index) + TestUtils.test_account(created_account) + + def test_set_account_label(self): + # create account + wallet = self._wallet + if len(wallet.get_accounts()) < 2: + wallet.create_account() + + # set account label + label = TestUtils.get_random_string() + wallet.set_account_label(1, label) + assert label == wallet.get_subaddress(1, 0).label + + def test_get_subaddresses(self): + TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) + wallet = self._wallet + accounts = wallet.get_accounts() + assert len(accounts) > 0 + for account in accounts: + assert account.index is not None + subaddresses = wallet.get_subaddresses(account.index) + assert len(subaddresses) > 0 + for subaddress in subaddresses: + TestUtils.test_subaddress(subaddress) + assert account.index == subaddress.account_index + + def test_get_subaddresses_by_indices(self): + TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) + wallet = self._wallet + accounts = wallet.get_accounts() + assert len(accounts) > 0 + for account in accounts: + + # get subaddresses + assert account.index is not None + subaddresses = wallet.get_subaddresses(account.index) + assert len(subaddresses) > 0 + + # remove a subaddress for query if possible + if len(subaddresses) > 1: + subaddresses.remove(subaddresses[0]) + + # get subaddress indices + subaddress_indices: list[int] = [] + for subaddress in subaddresses: + assert subaddress.index is not None + subaddress_indices.append(subaddress.index) + assert len(subaddress_indices) > 0 + + # fetch subaddresses by indices + fetched_subaddresses = wallet.get_subaddresses(account.index, subaddress_indices) + + # original subaddresses (minus one removed if applicable) is equal to fetched subaddresses + assert TestUtils.assert_subaddresses_equal(subaddresses, fetched_subaddresses) + + def test_get_subaddress_by_index(self): + TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) + wallet = self._wallet + accounts = wallet.get_accounts() + assert len(accounts) > 0 + for account in accounts: + assert account.index is not None + subaddresses = wallet.get_subaddresses(account.index) + assert len(subaddresses) > 0 + for subaddress in subaddresses: + assert subaddress.index is not None + TestUtils.test_subaddress(subaddress) + TestUtils.assert_subaddress_equal(subaddress, wallet.get_subaddress(account.index, subaddress.index)) + TestUtils.assert_subaddress_equal(subaddress, wallet.get_subaddresses(account.index, [subaddress.index])[0]) # test plural call with single subaddr number + + def test_create_subaddress(self): + TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) + wallet = self._wallet + # create subaddresses across accounts + accounts = wallet.get_accounts() + if len(accounts) < 2: + wallet.create_account() + accounts = wallet.get_accounts() + assert len(accounts) > 1 + account_idx = 0 + while account_idx < 2: + + # create subaddress with no label + subaddresses = wallet.get_subaddresses(account_idx) + subaddress = wallet.create_subaddress(account_idx) + assert subaddress.label is None + TestUtils.test_subaddress(subaddress) + subaddresses_new = wallet.get_subaddresses(account_idx) + assert len(subaddresses_new) - 1 == len(subaddresses) + TestUtils.assert_subaddress_equal(subaddress, subaddresses_new[len(subaddresses_new) - 1]) + + # create subaddress with label + subaddresses = wallet.get_subaddresses(account_idx) + uuid = TestUtils.get_random_string() + subaddress = wallet.create_subaddress(account_idx, uuid) + assert (uuid == subaddress.label) + TestUtils.test_subaddress(subaddress) + subaddresses_new = wallet.get_subaddresses(account_idx) + assert len(subaddresses) == len(subaddresses_new) - 1 + TestUtils.assert_subaddress_equal(subaddress, subaddresses_new[len(subaddresses_new) - 1]) + + account_idx += 1 + + def test_set_subaddress_label(self): + wallet = self._wallet + # create subaddresses + while len(wallet.get_subaddresses(0)) < 3: + wallet.create_subaddress(0) + + # set subaddress labels + subaddress_idx = 0 + while subaddress_idx < len(wallet.get_subaddresses(0)): + label = TestUtils.get_random_string() + wallet.set_subaddress_label(0, subaddress_idx, label) + assert (label == wallet.get_subaddress(0, subaddress_idx).label) + subaddress_idx += 1 + + def test_set_tx_note(self) -> None: + TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) + wallet = self._wallet + txs = TestUtils.get_random_transactions(wallet, None, 1, 5) + + # set notes + uuid = TestUtils.get_random_string() + i: int = 0 + + while i < len(txs): + tx_hash = txs[i].hash + assert tx_hash is not None + wallet.set_tx_note(tx_hash, f"{uuid}{i}") + i += 1 + + i = 0 + # get notes + while i < len(txs): + tx_hash = txs[i].hash + assert tx_hash is not None + assert wallet.get_tx_note(tx_hash) == f"{uuid}{i}" + i += 1 + + def test_set_tx_notes(self): + TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) + wallet = self._wallet + # set tx notes + uuid = TestUtils.get_random_string() + txs = wallet.get_txs() + assert len(txs) >= 3, "Test requires 3 or more wallet transactions run send tests" + tx_hashes: list[str] = [] + tx_notes: list[str] = [] + i = 0 + while i < len(tx_hashes): + tx_hash = txs[i].hash + assert tx_hash is not None + tx_hashes.append(tx_hash) + tx_notes.append(f"{uuid}{i}") + i += 1 + + wallet.set_tx_notes(tx_hashes, tx_notes) + + # get tx notes + tx_notes = wallet.get_tx_notes(tx_hashes) + for tx_note in tx_notes: + assert f"{uuid}{i}" == tx_note + + # TODO: test that get transaction has note + + def test_export_key_images(self): + TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) + wallet = self._wallet + images = wallet.export_key_images(True) + assert len(images) > 0, "No signed key images in wallet" + + for image in images: + assert isinstance(image, MoneroKeyImage) + assert image.hex is not None and len(image.hex) > 0 + assert image.signature is not None and len(image.signature) > 0 + + # wallet exports key images since last export by default + images = wallet.export_key_images() + images_all: list[MoneroKeyImage] = wallet.export_key_images(True) + assert len(images_all) > len(images) + + def test_get_new_key_images_from_last_import(self): + TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) + wallet = self._wallet + # get outputs hex + outputs_hex = wallet.export_outputs() + + # import outputs hex + if outputs_hex != "": + num_imported = wallet.import_outputs(outputs_hex) + assert num_imported >= 0 + + # get and test new key images from last import + images = wallet.get_new_key_images_from_last_import() + if len(images) == 0: + raise Exception("No new key images in last import") # TODO: these are already known to the wallet, so no new key images will be imported + for image in images: + assert image.hex is not None and len(image.hex) > 0 + assert image.signature is not None and len(image.signature) > 0 + + def test_import_key_images(self): + TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) + wallet = self._wallet + images = wallet.export_key_images() + assert len(images) > 0, "Wallet does not have any key images run send tests" + result = wallet.import_key_images(images) + assert result.height is not None and result.height > 0 + + # determine if non-zero spent and unspent amounts are expected + query = MoneroTxQuery() + query.is_outgoing = True + query.is_confirmed = True + txs = wallet.get_txs(query) + balance = wallet.get_balance() + has_spent = len(txs) > 0 + has_unspent = balance > 0 + + # test amounts + TestUtils.test_unsigned_big_integer(result.spent_amount, has_spent) + TestUtils.test_unsigned_big_integer(result.unspent_amount, has_unspent) + + def test_get_payment_uri(self): + TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) + wallet = self._wallet + # test with address and amount + config1 = MoneroTxConfig() + config1.address = wallet.get_address(0, 0) + config1.amount = 0 + uri = wallet.get_payment_uri(config1) + config2 = wallet.parse_payment_uri(uri) + TestUtils.assert_equals(config1, config2) + + # test with subaddress and all fields + config1.destinations[0].address = wallet.get_subaddress(0, 1).address + config1.destinations[0].amount = 425000000000 + config1.recipient_name = "John Doe" + config1.note = "OMZG XMR FTW" + uri = wallet.get_payment_uri(config1) + config2 = wallet.parse_payment_uri(uri) + TestUtils.assert_equals(config1, config2) + + # test with undefined address + address = config1.destinations[0].address + config1.destinations[0].address = None + try: + wallet.get_payment_uri(config1) + raise Exception("Should have thrown RPC exception with invalid parameters") + except Exception as e: + assert str(e).index("Cannot make URI from supplied parameters") >= 0 + + config1.destinations[0].address = address + + # test with standalone payment id + config1.payment_id = "03284e41c342f03603284e41c342f03603284e41c342f03603284e41c342f036" + try: + wallet.get_payment_uri(config1) + raise Exception("Should have thrown RPC exception with invalid parameters") + except Exception as e: + assert str(e).index("Cannot make URI from supplied parameters") >= 0 + + def test_mining(self): + TestUtils.assert_true(TestUtils.TEST_NON_RELAYS) + daemon = self._daemon + wallet = self._wallet + + status = daemon.get_mining_status() + if status.is_active: + wallet.stop_mining() + wallet.start_mining(2, False, True) + wallet.stop_mining() + + # endregion diff --git a/tests/test_monero_wallet_full.py b/tests/test_monero_wallet_full.py index 1079b12..851548a 100644 --- a/tests/test_monero_wallet_full.py +++ b/tests/test_monero_wallet_full.py @@ -14,124 +14,122 @@ class TestMoneroWalletFull(BaseTestMoneroWallet): - _daemon: MoneroDaemonRpc = Utils.get_daemon_rpc() - _wallet: MoneroWalletFull = Utils.get_wallet_full() # type: ignore - - @override - def _create_wallet(self, config: Optional[MoneroWalletConfig], startSyncing: bool = True): - # assign defaults - if config is None: - config = MoneroWalletConfig() - random: bool = config.seed is None and config.primary_address is None - if config.path is None: - config.path = Utils.TEST_WALLETS_DIR + "/" + Utils.get_random_string() - if config.password is None: - config.password = Utils.WALLET_PASSWORD - if config.network_type is None: - config.network_type = Utils.NETWORK_TYPE - #if config.server is None and config.connection_manager is None: - if config.server is None: - config.server = self._daemon.get_rpc_connection() - if config.restore_height is None and not random: - config.restore_height = 0 + _daemon: MoneroDaemonRpc = Utils.get_daemon_rpc() + _wallet: MoneroWalletFull = Utils.get_wallet_full() # type: ignore + + @override + def _create_wallet(self, config: Optional[MoneroWalletConfig], startSyncing: bool = True): + # assign defaults + if config is None: + config = MoneroWalletConfig() + random: bool = config.seed is None and config.primary_address is None + if config.path is None: + config.path = Utils.TEST_WALLETS_DIR + "/" + Utils.get_random_string() + if config.password is None: + config.password = Utils.WALLET_PASSWORD + if config.network_type is None: + config.network_type = Utils.NETWORK_TYPE + #if config.server is None and config.connection_manager is None: + if config.server is None: + config.server = self._daemon.get_rpc_connection() + if config.restore_height is None and not random: + config.restore_height = 0 - # create wallet - wallet = MoneroWalletFull.create_wallet(config) - if (not random): - Utils.assert_equals(0 if config.restore_height is None else config.restore_height, wallet.get_restore_height()) - if (startSyncing is not False and wallet.is_connected_to_daemon()): - wallet.start_syncing(Utils.SYNC_PERIOD_IN_MS) - return wallet - - @override - def _open_wallet(self, config: Optional[MoneroWalletConfig], startSyncing: bool = True) -> MoneroWalletFull: - # assign defaults - if config is None: - config = MoneroWalletConfig(); - if config.password is None: - config.password = Utils.WALLET_PASSWORD - if config.network_type is not None: - config.network_type = Utils.NETWORK_TYPE - if config.server is None and config.connection_manager is None: - config.server = self._daemon.get_rpc_connection() + # create wallet + wallet = MoneroWalletFull.create_wallet(config) + if (not random): + Utils.assert_equals(0 if config.restore_height is None else config.restore_height, wallet.get_restore_height()) + if (startSyncing is not False and wallet.is_connected_to_daemon()): + wallet.start_syncing(Utils.SYNC_PERIOD_IN_MS) + return wallet + + @override + def _open_wallet(self, config: Optional[MoneroWalletConfig], startSyncing: bool = True) -> MoneroWalletFull: + # assign defaults + if config is None: + config = MoneroWalletConfig(); + if config.password is None: + config.password = Utils.WALLET_PASSWORD + if config.network_type is not None: + config.network_type = Utils.NETWORK_TYPE + if config.server is None and config.connection_manager is None: + config.server = self._daemon.get_rpc_connection() - # open wallet - assert config.network_type is not None - assert config.path is not None - - wallet = MoneroWalletFull.open_wallet(config.path, config.password, config.network_type) - if startSyncing != False and wallet.is_connected_to_daemon(): - wallet.start_syncing(Utils.SYNC_PERIOD_IN_MS) - return wallet - - @override - def _close_wallet(self, wallet: MoneroWallet, save: bool = False) -> None: - wallet.close(save) - - @override - def _get_seed_languages(self) -> list[str]: - return self._wallet.get_seed_languages() - - @override - def get_test_wallet(self) -> MoneroWallet: - return Utils.get_wallet_full() - - def test_wallet_creation_and_close(self): - - config_keys = MoneroWalletConfig() - config_keys.language = "English" - config_keys.network_type = MoneroNetworkType.TESTNET - keys_wallet = MoneroWalletFull.create_wallet(config_keys) - seed = keys_wallet.get_seed() - - config = MoneroWalletConfig() - config.path = "test_wallet_sync" - config.password = "password" - config.network_type = MoneroNetworkType.TESTNET - config.restore_height = 0 - config.seed = seed - config.language = "English" - - wallet = MoneroWalletFull.create_wallet(config) - assert wallet.is_view_only() is False - wallet.close(save=False) - - for ext in ["", ".keys", ".address.txt"]: - try: - os.remove(f"test_wallet_sync{ext}") - except FileNotFoundError: - pass - - # Can create a subaddress with and without a label - def test_create_subaddress(self): - Utils.assert_true(Utils.TEST_NON_RELAYS) - - # create subaddresses across accounts - accounts: list[MoneroAccount] = self._wallet.get_accounts() - if len(accounts) < 2: - self._wallet.create_account() - - accounts = self._wallet.get_accounts() - Utils.assert_true(len(accounts) > 1) - account_idx: int = 0 - while account_idx < 2: - # create subaddress with no label - subaddresses: list[MoneroSubaddress] = self._wallet.get_subaddresses(account_idx) - subaddress: MoneroSubaddress = self._wallet.create_subaddress(account_idx) - Utils.assert_is_none(subaddress.label) - Utils.test_subaddress(subaddress) - subaddressesNew: list[MoneroSubaddress] = self._wallet.get_subaddresses(account_idx) - Utils.assert_equals(len(subaddressesNew) - 1, len(subaddresses)) - Utils.assert_equals(subaddress, subaddressesNew[len(subaddressesNew) - 1]) - - # create subaddress with label - subaddresses = self._wallet.get_subaddresses(account_idx) - uuid: str = Utils.get_random_string() - subaddress = self._wallet.create_subaddress(account_idx, uuid) - Utils.assert_equals(uuid, subaddress.label) - Utils.test_subaddress(subaddress) - subaddressesNew = self._wallet.get_subaddresses(account_idx) - Utils.assert_equals(len(subaddresses), len(subaddressesNew) - 1) - Utils.assert_equals(subaddress, subaddressesNew[len(subaddressesNew) - 1]) - account_idx += 1 - \ No newline at end of file + # open wallet + assert config.network_type is not None + assert config.path is not None + + wallet = MoneroWalletFull.open_wallet(config.path, config.password, config.network_type) + if startSyncing != False and wallet.is_connected_to_daemon(): + wallet.start_syncing(Utils.SYNC_PERIOD_IN_MS) + return wallet + + @override + def _close_wallet(self, wallet: MoneroWallet, save: bool = False) -> None: + wallet.close(save) + + @override + def _get_seed_languages(self) -> list[str]: + return self._wallet.get_seed_languages() + + @override + def get_test_wallet(self) -> MoneroWallet: + return Utils.get_wallet_full() + + def test_wallet_creation_and_close(self): + config_keys = MoneroWalletConfig() + config_keys.language = "English" + config_keys.network_type = MoneroNetworkType.TESTNET + keys_wallet = MoneroWalletFull.create_wallet(config_keys) + seed = keys_wallet.get_seed() + + config = MoneroWalletConfig() + config.path = "test_wallet_sync" + config.password = "password" + config.network_type = MoneroNetworkType.TESTNET + config.restore_height = 0 + config.seed = seed + config.language = "English" + + wallet = MoneroWalletFull.create_wallet(config) + assert wallet.is_view_only() is False + wallet.close(save=False) + + for ext in ["", ".keys", ".address.txt"]: + try: + os.remove(f"test_wallet_sync{ext}") + except FileNotFoundError: + pass + + # Can create a subaddress with and without a label + def test_create_subaddress(self): + Utils.assert_true(Utils.TEST_NON_RELAYS) + + # create subaddresses across accounts + accounts: list[MoneroAccount] = self._wallet.get_accounts() + if len(accounts) < 2: + self._wallet.create_account() + + accounts = self._wallet.get_accounts() + Utils.assert_true(len(accounts) > 1) + account_idx: int = 0 + while account_idx < 2: + # create subaddress with no label + subaddresses: list[MoneroSubaddress] = self._wallet.get_subaddresses(account_idx) + subaddress: MoneroSubaddress = self._wallet.create_subaddress(account_idx) + Utils.assert_is_none(subaddress.label) + Utils.test_subaddress(subaddress) + subaddresses_new: list[MoneroSubaddress] = self._wallet.get_subaddresses(account_idx) + Utils.assert_equals(len(subaddresses_new) - 1, len(subaddresses)) + Utils.assert_equals(subaddress, subaddresses_new[len(subaddresses_new) - 1]) + + # create subaddress with label + subaddresses = self._wallet.get_subaddresses(account_idx) + uuid: str = Utils.get_random_string() + subaddress = self._wallet.create_subaddress(account_idx, uuid) + Utils.assert_equals(uuid, subaddress.label) + Utils.test_subaddress(subaddress) + subaddresses_new = self._wallet.get_subaddresses(account_idx) + Utils.assert_equals(len(subaddresses), len(subaddresses_new) - 1) + Utils.assert_equals(subaddress, subaddresses_new[len(subaddresses_new) - 1]) + account_idx += 1 diff --git a/tests/test_monero_wallet_keys.py b/tests/test_monero_wallet_keys.py index 1b2692a..a70e8aa 100644 --- a/tests/test_monero_wallet_keys.py +++ b/tests/test_monero_wallet_keys.py @@ -2,8 +2,8 @@ from typing import Optional from typing_extensions import override from monero import ( - MoneroWalletKeys, MoneroWalletConfig, MoneroWallet, - MoneroUtils, MoneroAccount, MoneroSubaddress + MoneroWalletKeys, MoneroWalletConfig, MoneroWallet, + MoneroUtils, MoneroAccount, MoneroSubaddress ) from utils import MoneroTestUtils as Utils @@ -12,460 +12,458 @@ class TestMoneroWalletKeys(BaseTestMoneroWallet): - _account_indices: list[int] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] - _subaddress_indices: list[int] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] - _wallet: MoneroWalletKeys = Utils.get_wallet_keys() # type: ignore - - def _test_account(self, account: Optional[MoneroAccount]): - assert account is not None - assert account.index is not None - assert account.index >= 0 - assert account.primary_address is not None - assert account.tag is None or len(account.tag) > 0 - - def _test_subaddress(self, subaddress: Optional[MoneroSubaddress]): - assert subaddress is not None - assert subaddress.index is not None - assert subaddress.account_index is not None - assert subaddress.label is None or len(subaddress.label) > 0 - - def _get_subaddress(self, account_idx: int, subaddress_idx) -> Optional[MoneroSubaddress]: - subaddress_indices: list[int] = [subaddress_idx] - subaddresses = self._wallet.get_subaddresses(account_idx, subaddress_indices) - - if len(subaddresses) == 0: - return None - - return subaddresses[0] - - def _get_test_accounts(self, include_subaddresses: bool = False) -> list[MoneroAccount]: - account_indices = self._account_indices - subaddress_indices = self._subaddress_indices - accounts: list[MoneroAccount] = [] - for account_idx in account_indices: - account = self._wallet.get_account(account_idx) - - if include_subaddresses: - account.subaddresses = self._wallet.get_subaddresses(account_idx, subaddress_indices) - - accounts.append(account) - - return accounts - - @override - def _create_wallet(self, config: MoneroWalletConfig): - print(f"create_wallet(): seed: {config.seed}, address: {config.primary_address}, view key: {config.private_view_key}, spend key {config.private_spend_key}") - # assign defaults - if (config is None): - config = MoneroWalletConfig() - print(f"create_wallet(self): created config") - - random: bool = config.seed is None and config.primary_address is None and config.private_spend_key is None - print(f"create_wallet(self): random = {random}") - if config.network_type is None: - config.network_type = Utils.NETWORK_TYPE - - # create wallet - if random: - wallet = MoneroWalletKeys.create_wallet_random(config) - elif config.seed is not None and config.seed != "": - wallet = MoneroWalletKeys.create_wallet_from_seed(config) - elif config.primary_address is not None and config.private_view_key is not None: - wallet = MoneroWalletKeys.create_wallet_from_keys(config) - elif config.primary_address is not None and config.private_spend_key is not None: - wallet = MoneroWalletKeys.create_wallet_from_keys(config) - else: - raise Exception("Invalid configuration") - - return wallet - - @override - def _open_wallet(self, config: Optional[MoneroWalletConfig]) -> MoneroWallet: - raise NotImplementedError("TestMoneroWalletKeys._open_wallet(): not supported") - - @override - def _close_wallet(self, wallet: MoneroWallet, save: bool = False) -> None: - # not supported by keys wallet - pass - - @override - def _get_seed_languages(self) -> list[str]: - return self._wallet.get_seed_languages() - - @override - def get_test_wallet(self) -> MoneroWallet: - return Utils.get_wallet_keys() - - @pytest.mark.skip(reason="Wallet path not supported") - @override - def test_get_path(self) -> None: - return super().test_get_path() - - @pytest.mark.skip(reason="Connection not supported") - @override - def test_set_daemon_connection(self): - return super().test_set_daemon_connection() - - @pytest.mark.skip(reason="Sync not supported") - @override - def test_sync_without_progress(self): - return super().test_sync_without_progress() - - @override - def test_create_wallet_random(self) -> None: - """ - Can create a random wallet. - """ - Utils.assert_true(Utils.TEST_NON_RELAYS) - e1: Exception | None = None - wallet: MoneroWallet | None = None - try: - config = MoneroWalletConfig() - wallet = self._create_wallet(config) - e2: Exception | None = None - - try: - MoneroUtils.validate_address(wallet.get_primary_address(), Utils.NETWORK_TYPE) - MoneroUtils.validate_private_view_key(wallet.get_private_view_key()) - MoneroUtils.validate_private_spend_key(wallet.get_private_spend_key()) - MoneroUtils.validate_mnemonic(wallet.get_seed()) - Utils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, wallet.get_seed_language()) # TODO monero-wallet-rpc: get seed language - except Exception as e: - e2 = e - - if e2 is not None: - raise e2 - - # attempt to create wallet with unknown language - try: - config = MoneroWalletConfig() - config.language = "english" - wallet = self._create_wallet(config) - raise Exception("Should have thrown error") - except Exception as e: - Utils.assert_equals("Unknown language: english", str(e)) - - except Exception as e: - e1 = e - - if e1 is not None: - raise e1 - - @override - def test_create_wallet_from_seed(self) -> None: - Utils.assert_true(Utils.TEST_NON_RELAYS) - e1: Exception | None = None # emulating Python "finally" but compatible with other languages - try: - - # save for comparison - primaryAddress = self._wallet.get_primary_address() - privateViewKey = self._wallet.get_private_view_key() - privateSpendKey = self._wallet.get_private_spend_key() - - # recreate test wallet from seed - config = MoneroWalletConfig() - config.seed = Utils.SEED - - wallet: MoneroWallet = self._create_wallet(config) - e2: Exception | None = None - try: - Utils.assert_equals(primaryAddress, wallet.get_primary_address()) - Utils.assert_equals(privateViewKey, wallet.get_private_view_key()) - Utils.assert_equals(privateSpendKey, wallet.get_private_spend_key()) - Utils.assert_equals(Utils.SEED, wallet.get_seed()) - Utils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, wallet.get_seed_language()) - except Exception as e: - e2 = e - - if e2 is not None: - raise e2 - - # attempt to create wallet with two missing words - try: - config = MoneroWalletConfig() - config.seed = "memoir desk algebra inbound innocent unplugs fully okay five inflamed giant factual ritual toyed topic snake unhappy guarded tweezers haunted inundate giant" - wallet = self._create_wallet(config) - except Exception as e: - Utils.assert_equals("Invalid mnemonic", str(e)) - - - except Exception as e: - e1 = e - - if e1 is not None: - raise e1 - - @override - def test_create_wallet_from_seed_with_offset(self) -> None: - Utils.assert_true(Utils.TEST_NON_RELAYS) - e1: Exception | None = None # emulating Python "finally" but compatible with other languages - try: - - # create test wallet with offset - config = MoneroWalletConfig() - config.seed = Utils.SEED - config.seed_offset = "my secret offset!" - wallet: MoneroWallet = self._create_wallet(config) - e2: Exception | None = None - try: - MoneroUtils.validate_mnemonic(wallet.get_seed()) - Utils.assert_not_equals(Utils.SEED, wallet.get_seed()) - MoneroUtils.validate_address(wallet.get_primary_address(), Utils.NETWORK_TYPE) - Utils.assert_not_equals(Utils.ADDRESS, wallet.get_primary_address()) - Utils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, wallet.get_seed_language()) # TODO monero-wallet-rpc: support - except Exception as e: - e2 = e - - if e2 is not None: - raise e2 - except Exception as e: - e1 = e - - if e1 is not None: - raise e1 - - @override - def test_create_wallet_from_keys(self) -> None: - Utils.assert_true(Utils.TEST_NON_RELAYS) - e1: Exception | None = None # emulating Java "finally" but compatible with other languages - try: - # save for comparison - primaryAddress = self._wallet.get_primary_address() - privateViewKey = self._wallet.get_private_view_key() - privateSpendKey = self._wallet.get_private_spend_key() - - # recreate test wallet from keys - config = MoneroWalletConfig() - config.primary_address = primaryAddress - config.private_view_key = privateViewKey - config.private_spend_key = privateSpendKey - wallet: MoneroWallet = self._create_wallet(config) - e2: Exception | None = None - try: - Utils.assert_equals(primaryAddress, wallet.get_primary_address()) - Utils.assert_equals(privateViewKey, wallet.get_private_view_key()) - Utils.assert_equals(privateSpendKey, wallet.get_private_spend_key()) - MoneroUtils.validate_mnemonic(wallet.get_seed()) # TODO monero-wallet-rpc: cannot get seed from wallet created from keys? - Utils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, wallet.get_seed_language()) - - except Exception as e: - e2 = e - - self._close_wallet(wallet) - if e2 is not None: - raise e2 - - # recreate test wallet from spend key - config = MoneroWalletConfig() - config.primary_address = primaryAddress - config.private_spend_key = privateSpendKey - wallet = self._create_wallet(config) - e2 = None - try: - Utils.assert_equals(primaryAddress, wallet.get_primary_address()) - Utils.assert_equals(privateViewKey, wallet.get_private_view_key()) - Utils.assert_equals(privateSpendKey, wallet.get_private_spend_key()) - MoneroUtils.validate_mnemonic(wallet.get_seed()) # TODO monero-wallet-rpc: cannot get seed from wallet created from keys? - Utils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, wallet.get_seed_language()) - - except Exception as e: - e2 = e - - self._close_wallet(wallet) - if e2 is not None: - raise e2 - - except Exception as e: - e1 = e - - if e1 is not None: - raise e1 - - @pytest.mark.skip(reason="Subaddress lookahead not supported") - @override - def test_subaddress_lookahead(self) -> None: - return super().test_subaddress_lookahead() - - @pytest.mark.skip(reason="Not implemented") - @override - def test_decode_integrated_address(self): - return super().test_decode_integrated_address() - - @override - def test_get_subaddress_address(self): - Utils.assert_true(Utils.TEST_NON_RELAYS) - Utils.assert_equals(self._wallet.get_primary_address(), (self._wallet.get_address(0, 0))) - accounts = self._get_test_accounts(True) - - for account in accounts: - assert account is not None - assert account.index is not None - assert account.primary_address is not None - MoneroUtils.validate_address(account.primary_address, Utils.NETWORK_TYPE) - - for subaddress in account.subaddresses: + _account_indices: list[int] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + _subaddress_indices: list[int] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + _wallet: MoneroWalletKeys = Utils.get_wallet_keys() # type: ignore + + def _test_account(self, account: Optional[MoneroAccount]): + assert account is not None + assert account.index is not None + assert account.index >= 0 + assert account.primary_address is not None + assert account.tag is None or len(account.tag) > 0 + + def _test_subaddress(self, subaddress: Optional[MoneroSubaddress]): assert subaddress is not None assert subaddress.index is not None - Utils.assert_equals(subaddress.address, self._wallet.get_address(account.index, subaddress.index)) - - @override - def test_get_subaddress_address_out_of_range(self): - Utils.assert_true(Utils.TEST_NON_RELAYS) - accounts = self._get_test_accounts(True) - accountIdx = len(accounts) - 1 - subaddressIdx = len(accounts[accountIdx].subaddresses) - address = self._wallet.get_address(accountIdx, subaddressIdx) - Utils.assert_not_none(address) - Utils.assert_true(len(address) > 0) - - @pytest.mark.skip(reason="Get address index not supported") - @override - def test_get_address_indices(self): - return super().test_get_address_indices() - - @pytest.mark.skip(reason="Not supported") - @override - def test_wallet_equality_ground_truth(self): - return super().test_wallet_equality_ground_truth() - - @pytest.mark.skip(reason="Not supported") - @override - def test_get_height(self): - return super().test_get_height() - - @pytest.mark.skip(reason="Not supported") - @override - def test_get_height_by_date(self): - return super().test_get_height_by_date() - - @pytest.mark.skip(reason="Not supported") - @override - def test_get_all_balances(self): - return super().test_get_all_balances() - - @override - def test_get_accounts_without_subaddresses(self): - accounts = self._get_test_accounts() - assert len(accounts) > 0 - for account in accounts: - self._test_account(account) - assert len(account.subaddresses) == 0 - - @override - def test_get_accounts_with_subaddresses(self): - accounts = self._get_test_accounts(True) - assert len(accounts) > 0 - for account in accounts: - self._test_account(account) - assert len(account.subaddresses) > 0 - - @override - def test_get_account(self): - Utils.assert_true(Utils.TEST_NON_RELAYS) - accounts = self._get_test_accounts() - assert len(accounts) > 0 - for account in accounts: - self._test_account(account) - - # test without subaddresses - assert account.index is not None - retrieved = self._wallet.get_account(account.index) - assert len(retrieved.subaddresses) == 0 - - # test with subaddresses - retrieved = self._wallet.get_account(account.index) - retrieved.subaddresses = self._wallet.get_subaddresses(account.index, self._subaddress_indices) - - @pytest.mark.skip(reason="Account creation not supported") - @override - def test_create_account_without_label(self): - return super().test_create_account_without_label() - - @pytest.mark.skip(reason="Account/label creation not supported") - @override - def test_create_account_with_label(self): - return super().test_create_account_with_label() - - @pytest.mark.skip(reason="Label creation not supported") - @override - def test_set_account_label(self): - return super().test_set_account_label() - - @override - def test_get_subaddresses(self): - wallet = self._wallet - accounts = self._get_test_accounts() - assert len(accounts) > 0 - for account in accounts: - assert account.index is not None - subaddresses = wallet.get_subaddresses(account.index, self._subaddress_indices) - assert len(subaddresses) > 0 - for subaddress in subaddresses: - self._test_subaddress(subaddress) - assert account.index == subaddress.account_index - - @pytest.mark.skip(reason="Not supported") - @override - def test_get_subaddresses_by_indices(self): - return super().test_get_subaddresses_by_indices() - - @override - def test_get_subaddress_by_index(self): - accounts = self._get_test_accounts() - assert len(accounts) > 0 - for account in accounts: - assert account.index is not None - subaddresses = self._wallet.get_subaddresses(account.index, self._subaddress_indices) - assert len(subaddresses) > 0 - - for subaddress in subaddresses: - assert subaddress.index is not None - self._test_subaddress(subaddress) - Utils.assert_subaddress_equal(subaddress, self._get_subaddress(account.index, subaddress.index)) - Utils.assert_subaddress_equal(subaddress, self._wallet.get_subaddresses(account.index, [subaddress.index])[0]) # test plural call with single subaddr number - - @pytest.mark.skip(reason="Subaddress creation not supported") - @override - def test_create_subaddress(self): - return super().test_create_subaddress() - - @pytest.mark.skip(reason="Labels not supported") - @override - def test_set_subaddress_label(self): - return super().test_set_subaddress_label() - - @pytest.mark.skip(reason="Tx note not supported") - @override - def test_set_tx_note(self) -> None: - return super().test_set_tx_note() - - @pytest.mark.skip(reason="Tx note not supported") - @override - def test_set_tx_notes(self) -> None: - return super().test_set_tx_notes() - - @pytest.mark.skip(reason="Export key images not supported") - @override - def test_export_key_images(self): - return super().test_export_key_images() - - @pytest.mark.skip(reason="Import key images not supported") - @override - def test_get_new_key_images_from_last_import(self): - return super().test_get_new_key_images_from_last_import() - - @pytest.mark.skip(reason="Import key images not supported") - @override - def test_import_key_images(self): - return super().test_import_key_images() - - @pytest.mark.skip(reason="Payment uri not supported") - @override - def test_get_payment_uri(self): - return super().test_get_payment_uri() - - @pytest.mark.skip(reason="Mining not supported") - @override - def test_mining(self): - return super().test_mining() - \ No newline at end of file + assert subaddress.account_index is not None + assert subaddress.label is None or len(subaddress.label) > 0 + + def _get_subaddress(self, account_idx: int, subaddress_idx: int) -> Optional[MoneroSubaddress]: + subaddress_indices: list[int] = [subaddress_idx] + subaddresses = self._wallet.get_subaddresses(account_idx, subaddress_indices) + + if len(subaddresses) == 0: + return None + + return subaddresses[0] + + def _get_test_accounts(self, include_subaddresses: bool = False) -> list[MoneroAccount]: + account_indices = self._account_indices + subaddress_indices = self._subaddress_indices + accounts: list[MoneroAccount] = [] + for account_idx in account_indices: + account = self._wallet.get_account(account_idx) + + if include_subaddresses: + account.subaddresses = self._wallet.get_subaddresses(account_idx, subaddress_indices) + + accounts.append(account) + + return accounts + + @override + def _create_wallet(self, config: Optional[MoneroWalletConfig]): + # assign defaults + if (config is None): + config = MoneroWalletConfig() + print("create_wallet(self): created config") + + print(f"create_wallet(): seed: {config.seed}, address: {config.primary_address}, view key: {config.private_view_key}, spend key {config.private_spend_key}") + + random: bool = config.seed is None and config.primary_address is None and config.private_spend_key is None + print(f"create_wallet(self): random = {random}") + if config.network_type is None: + config.network_type = Utils.NETWORK_TYPE + + # create wallet + if random: + wallet = MoneroWalletKeys.create_wallet_random(config) + elif config.seed is not None and config.seed != "": + wallet = MoneroWalletKeys.create_wallet_from_seed(config) + elif config.primary_address is not None and config.private_view_key is not None: + wallet = MoneroWalletKeys.create_wallet_from_keys(config) + elif config.primary_address is not None and config.private_spend_key is not None: + wallet = MoneroWalletKeys.create_wallet_from_keys(config) + else: + raise Exception("Invalid configuration") + + return wallet + + @override + def _open_wallet(self, config: Optional[MoneroWalletConfig]) -> MoneroWallet: + raise NotImplementedError("TestMoneroWalletKeys._open_wallet(): not supported") + + @override + def _close_wallet(self, wallet: MoneroWallet, save: bool = False) -> None: + # not supported by keys wallet + pass + + @override + def _get_seed_languages(self) -> list[str]: + return self._wallet.get_seed_languages() + + @override + def get_test_wallet(self) -> MoneroWallet: + return Utils.get_wallet_keys() + + @pytest.mark.skip(reason="Wallet path not supported") + @override + def test_get_path(self) -> None: + return super().test_get_path() + + @pytest.mark.skip(reason="Connection not supported") + @override + def test_set_daemon_connection(self): + return super().test_set_daemon_connection() + + @pytest.mark.skip(reason="Sync not supported") + @override + def test_sync_without_progress(self): + return super().test_sync_without_progress() + + @override + def test_create_wallet_random(self) -> None: + """ + Can create a random wallet. + """ + Utils.assert_true(Utils.TEST_NON_RELAYS) + e1: Exception | None = None + wallet: MoneroWallet | None = None + try: + config = MoneroWalletConfig() + wallet = self._create_wallet(config) + e2: Exception | None = None + + try: + MoneroUtils.validate_address(wallet.get_primary_address(), Utils.NETWORK_TYPE) + MoneroUtils.validate_private_view_key(wallet.get_private_view_key()) + MoneroUtils.validate_private_spend_key(wallet.get_private_spend_key()) + MoneroUtils.validate_mnemonic(wallet.get_seed()) + Utils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, wallet.get_seed_language()) # TODO monero-wallet-rpc: get seed language + except Exception as e: + e2 = e + + if e2 is not None: + raise e2 + + # attempt to create wallet with unknown language + try: + config = MoneroWalletConfig() + config.language = "english" + wallet = self._create_wallet(config) + raise Exception("Should have thrown error") + except Exception as e: + Utils.assert_equals("Unknown language: english", str(e)) + + except Exception as e: + e1 = e + + if e1 is not None: + raise e1 + + @override + def test_create_wallet_from_seed(self) -> None: + Utils.assert_true(Utils.TEST_NON_RELAYS) + e1: Exception | None = None # emulating Python "finally" but compatible with other languages + try: + + # save for comparison + primary_address = self._wallet.get_primary_address() + private_view_key = self._wallet.get_private_view_key() + private_spend_key = self._wallet.get_private_spend_key() + + # recreate test wallet from seed + config = MoneroWalletConfig() + config.seed = Utils.SEED + + wallet: MoneroWallet = self._create_wallet(config) + e2: Exception | None = None + try: + Utils.assert_equals(primary_address, wallet.get_primary_address()) + Utils.assert_equals(private_view_key, wallet.get_private_view_key()) + Utils.assert_equals(private_spend_key, wallet.get_private_spend_key()) + Utils.assert_equals(Utils.SEED, wallet.get_seed()) + Utils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, wallet.get_seed_language()) + except Exception as e: + e2 = e + + if e2 is not None: + raise e2 + + # attempt to create wallet with two missing words + try: + config = MoneroWalletConfig() + config.seed = "memoir desk algebra inbound innocent unplugs fully okay five inflamed giant factual ritual toyed topic snake unhappy guarded tweezers haunted inundate giant" + wallet = self._create_wallet(config) + except Exception as e: + Utils.assert_equals("Invalid mnemonic", str(e)) + + except Exception as e: + e1 = e + + if e1 is not None: + raise e1 + + @override + def test_create_wallet_from_seed_with_offset(self) -> None: + Utils.assert_true(Utils.TEST_NON_RELAYS) + e1: Exception | None = None # emulating Python "finally" but compatible with other languages + try: + # create test wallet with offset + config = MoneroWalletConfig() + config.seed = Utils.SEED + config.seed_offset = "my secret offset!" + wallet: MoneroWallet = self._create_wallet(config) + e2: Exception | None = None + try: + MoneroUtils.validate_mnemonic(wallet.get_seed()) + Utils.assert_not_equals(Utils.SEED, wallet.get_seed()) + MoneroUtils.validate_address(wallet.get_primary_address(), Utils.NETWORK_TYPE) + Utils.assert_not_equals(Utils.ADDRESS, wallet.get_primary_address()) + Utils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, wallet.get_seed_language()) # TODO monero-wallet-rpc: support + except Exception as e: + e2 = e + + if e2 is not None: + raise e2 + except Exception as e: + e1 = e + + if e1 is not None: + raise e1 + + @override + def test_create_wallet_from_keys(self) -> None: + Utils.assert_true(Utils.TEST_NON_RELAYS) + e1: Exception | None = None # emulating Java "finally" but compatible with other languages + try: + # save for comparison + primary_address = self._wallet.get_primary_address() + private_view_key = self._wallet.get_private_view_key() + private_spend_key = self._wallet.get_private_spend_key() + + # recreate test wallet from keys + config = MoneroWalletConfig() + config.primary_address = primary_address + config.private_view_key = private_view_key + config.private_spend_key = private_spend_key + wallet: MoneroWallet = self._create_wallet(config) + e2: Exception | None = None + try: + Utils.assert_equals(primary_address, wallet.get_primary_address()) + Utils.assert_equals(private_view_key, wallet.get_private_view_key()) + Utils.assert_equals(private_spend_key, wallet.get_private_spend_key()) + MoneroUtils.validate_mnemonic(wallet.get_seed()) # TODO monero-wallet-rpc: cannot get seed from wallet created from keys? + Utils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, wallet.get_seed_language()) + + except Exception as e: + e2 = e + + self._close_wallet(wallet) + if e2 is not None: + raise e2 + + # recreate test wallet from spend key + config = MoneroWalletConfig() + config.primary_address = primary_address + config.private_spend_key = private_spend_key + wallet = self._create_wallet(config) + e2 = None + try: + Utils.assert_equals(primary_address, wallet.get_primary_address()) + Utils.assert_equals(private_view_key, wallet.get_private_view_key()) + Utils.assert_equals(private_spend_key, wallet.get_private_spend_key()) + MoneroUtils.validate_mnemonic(wallet.get_seed()) # TODO monero-wallet-rpc: cannot get seed from wallet created from keys? + Utils.assert_equals(MoneroWallet.DEFAULT_LANGUAGE, wallet.get_seed_language()) + + except Exception as e: + e2 = e + + self._close_wallet(wallet) + if e2 is not None: + raise e2 + + except Exception as e: + e1 = e + + if e1 is not None: + raise e1 + + @pytest.mark.skip(reason="Subaddress lookahead not supported") + @override + def test_subaddress_lookahead(self) -> None: + return super().test_subaddress_lookahead() + + @pytest.mark.skip(reason="Not implemented") + @override + def test_decode_integrated_address(self): + return super().test_decode_integrated_address() + + @override + def test_get_subaddress_address(self): + Utils.assert_true(Utils.TEST_NON_RELAYS) + Utils.assert_equals(self._wallet.get_primary_address(), (self._wallet.get_address(0, 0))) + accounts = self._get_test_accounts(True) + + for account in accounts: + assert account is not None + assert account.index is not None + assert account.primary_address is not None + MoneroUtils.validate_address(account.primary_address, Utils.NETWORK_TYPE) + + for subaddress in account.subaddresses: + assert subaddress is not None + assert subaddress.index is not None + Utils.assert_equals(subaddress.address, self._wallet.get_address(account.index, subaddress.index)) + + @override + def test_get_subaddress_address_out_of_range(self): + Utils.assert_true(Utils.TEST_NON_RELAYS) + accounts = self._get_test_accounts(True) + account_idx = len(accounts) - 1 + subaddress_idx = len(accounts[account_idx].subaddresses) + address = self._wallet.get_address(account_idx, subaddress_idx) + Utils.assert_not_none(address) + Utils.assert_true(len(address) > 0) + + @pytest.mark.skip(reason="Get address index not supported") + @override + def test_get_address_indices(self): + return super().test_get_address_indices() + + @pytest.mark.skip(reason="Not supported") + @override + def test_wallet_equality_ground_truth(self): + return super().test_wallet_equality_ground_truth() + + @pytest.mark.skip(reason="Not supported") + @override + def test_get_height(self): + return super().test_get_height() + + @pytest.mark.skip(reason="Not supported") + @override + def test_get_height_by_date(self): + return super().test_get_height_by_date() + + @pytest.mark.skip(reason="Not supported") + @override + def test_get_all_balances(self): + return super().test_get_all_balances() + + @override + def test_get_accounts_without_subaddresses(self): + accounts = self._get_test_accounts() + assert len(accounts) > 0 + for account in accounts: + self._test_account(account) + assert len(account.subaddresses) == 0 + + @override + def test_get_accounts_with_subaddresses(self): + accounts = self._get_test_accounts(True) + assert len(accounts) > 0 + for account in accounts: + self._test_account(account) + assert len(account.subaddresses) > 0 + + @override + def test_get_account(self): + Utils.assert_true(Utils.TEST_NON_RELAYS) + accounts = self._get_test_accounts() + assert len(accounts) > 0 + for account in accounts: + self._test_account(account) + + # test without subaddresses + assert account.index is not None + retrieved = self._wallet.get_account(account.index) + assert len(retrieved.subaddresses) == 0 + + # test with subaddresses + retrieved = self._wallet.get_account(account.index) + retrieved.subaddresses = self._wallet.get_subaddresses(account.index, self._subaddress_indices) + + @pytest.mark.skip(reason="Account creation not supported") + @override + def test_create_account_without_label(self): + return super().test_create_account_without_label() + + @pytest.mark.skip(reason="Account/label creation not supported") + @override + def test_create_account_with_label(self): + return super().test_create_account_with_label() + + @pytest.mark.skip(reason="Label creation not supported") + @override + def test_set_account_label(self): + return super().test_set_account_label() + + @override + def test_get_subaddresses(self): + wallet = self._wallet + accounts = self._get_test_accounts() + assert len(accounts) > 0 + for account in accounts: + assert account.index is not None + subaddresses = wallet.get_subaddresses(account.index, self._subaddress_indices) + assert len(subaddresses) > 0 + for subaddress in subaddresses: + self._test_subaddress(subaddress) + assert account.index == subaddress.account_index + + @pytest.mark.skip(reason="Not supported") + @override + def test_get_subaddresses_by_indices(self): + return super().test_get_subaddresses_by_indices() + + @override + def test_get_subaddress_by_index(self): + accounts = self._get_test_accounts() + assert len(accounts) > 0 + for account in accounts: + assert account.index is not None + subaddresses = self._wallet.get_subaddresses(account.index, self._subaddress_indices) + assert len(subaddresses) > 0 + + for subaddress in subaddresses: + assert subaddress.index is not None + self._test_subaddress(subaddress) + Utils.assert_subaddress_equal(subaddress, self._get_subaddress(account.index, subaddress.index)) + Utils.assert_subaddress_equal(subaddress, self._wallet.get_subaddresses(account.index, [subaddress.index])[0]) # test plural call with single subaddr number + + @pytest.mark.skip(reason="Subaddress creation not supported") + @override + def test_create_subaddress(self): + return super().test_create_subaddress() + + @pytest.mark.skip(reason="Labels not supported") + @override + def test_set_subaddress_label(self): + return super().test_set_subaddress_label() + + @pytest.mark.skip(reason="Tx note not supported") + @override + def test_set_tx_note(self) -> None: + return super().test_set_tx_note() + + @pytest.mark.skip(reason="Tx note not supported") + @override + def test_set_tx_notes(self) -> None: + return super().test_set_tx_notes() + + @pytest.mark.skip(reason="Export key images not supported") + @override + def test_export_key_images(self): + return super().test_export_key_images() + + @pytest.mark.skip(reason="Import key images not supported") + @override + def test_get_new_key_images_from_last_import(self): + return super().test_get_new_key_images_from_last_import() + + @pytest.mark.skip(reason="Import key images not supported") + @override + def test_import_key_images(self): + return super().test_import_key_images() + + @pytest.mark.skip(reason="Payment uri not supported") + @override + def test_get_payment_uri(self): + return super().test_get_payment_uri() + + @pytest.mark.skip(reason="Mining not supported") + @override + def test_mining(self): + return super().test_mining() diff --git a/tests/test_sample_code.py b/tests/test_sample_code.py index 537a833..134565c 100644 --- a/tests/test_sample_code.py +++ b/tests/test_sample_code.py @@ -3,131 +3,133 @@ from typing_extensions import override from monero import ( - MoneroDaemon, MoneroDaemonRpc, MoneroOutputWallet, MoneroTx, MoneroWalletRpc, MoneroTxConfig, - MoneroWalletFull, MoneroWalletConfig, MoneroConnectionManager, MoneroRpcConnection, - MoneroNetworkType, MoneroWalletListener, MoneroTxWallet + MoneroDaemon, MoneroDaemonRpc, MoneroOutputWallet, MoneroTx, MoneroWalletRpc, MoneroTxConfig, + MoneroWalletFull, MoneroWalletConfig, MoneroConnectionManager, MoneroRpcConnection, + MoneroNetworkType, MoneroWalletListener, MoneroTxWallet ) from utils import MoneroTestUtils as Utils, SampleConnectionListener, WalletSyncPrinter class WalletFundsListener(MoneroWalletListener): - FUNDS_RECEIVED: bool = False + funds_received: bool = False - @override - def on_output_received(self, output: MoneroOutputWallet) -> None: - amount = output.amount - txHash = output.tx.hash - isConfirmed = output.tx.is_confirmed - # isLocked = output.tx.is_locked - self.FUNDS_RECEIVED = True + @override + def on_output_received(self, output: MoneroOutputWallet) -> None: + amount = output.amount + tx_hash = output.tx.hash + is_confirmed = output.tx.is_confirmed + # is_locked = output.tx.is_locked + self.funds_received = True class TestSampleCode: - # Sample code demonstration - def test_sample_code(self): - # connect to daemon - daemon: MoneroDaemon = MoneroDaemonRpc("http:#localhost:28081", "", "") - height: int = daemon.get_height() # 1523651 - txsInPool: list[MoneroTx] = daemon.get_tx_pool() # get transactions in the pool - - # create wallet from seed using python bindings to monero-project - config = MoneroWalletConfig() - config.path = "./test_wallets/" + Utils.get_random_string() - config.password = "supersecretpassword123" - config.network_type = MoneroNetworkType.TESTNET - config.server = MoneroRpcConnection("http:#localhost:28081", "superuser", "abctesting123") - config.seed = Utils.SEED - config.restore_height = Utils.FIRST_RECEIVE_HEIGHT + # Sample code demonstration + def test_sample_code(self): + # connect to daemon + daemon: MoneroDaemon = MoneroDaemonRpc("http:#localhost:28081", "", "") + height: int = daemon.get_height() # 1523651 + txs_in_pool: list[MoneroTx] = daemon.get_tx_pool() # get transactions in the pool + + # create wallet from seed using python bindings to monero-project + config = MoneroWalletConfig() + config.path = "./test_wallets/" + Utils.get_random_string() + config.password = "supersecretpassword123" + config.network_type = MoneroNetworkType.TESTNET + config.server = MoneroRpcConnection("http:#localhost:28081", "superuser", "abctesting123") + config.seed = Utils.SEED + config.restore_height = Utils.FIRST_RECEIVE_HEIGHT - walletFull = MoneroWalletFull.create_wallet(config) - listener: MoneroWalletListener = WalletSyncPrinter() - # synchronize the wallet and receive progress notifications - walletFull.sync(listener) - - # synchronize in the background every 5 seconds - walletFull.start_syncing(5000) - - # receive notifications when funds are received, confirmed, and unlocked - fundsListener = WalletFundsListener() - walletFull.add_listener(fundsListener) + wallet_full = MoneroWalletFull.create_wallet(config) + listener: MoneroWalletListener = WalletSyncPrinter() + # synchronize the wallet and receive progress notifications + wallet_full.sync(listener) + + # synchronize in the background every 5 seconds + wallet_full.start_syncing(5000) + + # receive notifications when funds are received, confirmed, and unlocked + funds_listener = WalletFundsListener() + wallet_full.add_listener(funds_listener) - # connect to wallet RPC and open wallet - walletRpc = MoneroWalletRpc(Utils.WALLET_RPC_URI, Utils.WALLET_RPC_USERNAME, Utils.WALLET_RPC_PASSWORD) # *** REPLACE WITH CONSTANTS IN README *** - walletRpc.open_wallet("test_wallet_1", "supersecretpassword123") # *** CHANGE README TO "sample_wallet_rpc" *** - primaryAddress: str = walletRpc.get_primary_address() # 555zgduFhmKd2o8rPUz... - balance: int = walletRpc.get_balance() # 533648366742 - txs: list[MoneroTxWallet] = walletRpc.get_txs() # get transactions containing transfers to/from the wallet - - # send funds from RPC wallet to full wallet - Utils.WALLET_TX_TRACKER.wait_for_wallet_txs_to_clear_pool(daemon, Utils.SYNC_PERIOD_IN_MS, [walletRpc]) # *** REMOVE FROM README SAMPLE *** - Utils.WALLET_TX_TRACKER.wait_for_unlocked_balance(daemon, Utils.SYNC_PERIOD_IN_MS, walletRpc, 0, None, 250000000000) # *** REMOVE FROM README SAMPLE *** - tx_config = MoneroTxConfig() - tx_config.account_index = 0 - tx_config.address = walletFull.get_address(1, 0) - tx_config.amount = 250000000000 # send 0.25 XMR (denominated in atomic units) - tx_config.relay = False # create transaction and relay to the network if true - createdTx: MoneroTxWallet = walletRpc.create_tx(tx_config) - fee: int | None = createdTx.fee # "Are you sure you want to send... ?" - walletRpc.relay_tx(createdTx) # relay the transaction - - # recipient receives unconfirmed funds within 5 seconds - Utils.wait_for(5000) - Utils.assert_true(fundsListener.FUNDS_RECEIVED) - - # save and close full wallet - walletFull.close(True) + # connect to wallet RPC and open wallet + wallet_rpc = MoneroWalletRpc(Utils.WALLET_RPC_URI, Utils.WALLET_RPC_USERNAME, Utils.WALLET_RPC_PASSWORD) # *** REPLACE WITH CONSTANTS IN README *** + wallet_rpc.open_wallet("test_wallet_1", "supersecretpassword123") # *** CHANGE README TO "sample_wallet_rpc" *** + primary_address: str = wallet_rpc.get_primary_address() # 555zgduFhmKd2o8rPUz... + balance: int = wallet_rpc.get_balance() # 533648366742 + txs: list[MoneroTxWallet] = wallet_rpc.get_txs() # get transactions containing transfers to/from the wallet + + # send funds from RPC wallet to full wallet + Utils.WALLET_TX_TRACKER.wait_for_wallet_txs_to_clear_pool(daemon, Utils.SYNC_PERIOD_IN_MS, [wallet_rpc]) # *** REMOVE FROM README SAMPLE *** + Utils.WALLET_TX_TRACKER.wait_for_unlocked_balance(daemon, Utils.SYNC_PERIOD_IN_MS, wallet_rpc, 0, None, 250000000000) # *** REMOVE FROM README SAMPLE *** + tx_config = MoneroTxConfig() + tx_config.account_index = 0 + tx_config.address = wallet_full.get_address(1, 0) + tx_config.amount = 250000000000 # send 0.25 XMR (denominated in atomic units) + tx_config.relay = False # create transaction and relay to the network if true + created_tx: MoneroTxWallet = wallet_rpc.create_tx(tx_config) + fee: int | None = created_tx.fee # "Are you sure you want to send... ?" + wallet_rpc.relay_tx(created_tx) # relay the transaction + + # recipient receives unconfirmed funds within 5 seconds + Utils.wait_for(5000) + Utils.assert_true(funds_listener.funds_received) + + # save and close full wallet + wallet_full.close(True) - # Connection manager demonstration - def test_connection_manager_demo(self): - - # create connection manager - connectionManager = MoneroConnectionManager() - - # add managed connections with priorities - con1 = MoneroRpcConnection("http:#localhost:28081") - con1.priority = 1 - connectionManager.add_connection(con1) # use localhost as first priority - con2 = MoneroRpcConnection("http:#example.com") - connectionManager.add_connection(con2) # default priority is prioritized last - - # set current connection - con3 = MoneroRpcConnection("http:#foo.bar", "admin", "password") - connectionManager.set_connection(con3) # connection is added if new + # Connection manager demonstration + def test_connection_manager_demo(self): + + # create connection manager + connection_manager = MoneroConnectionManager() + + # add managed connections with priorities + con1 = MoneroRpcConnection("http:#localhost:28081") + con1.priority = 1 + connection_manager.add_connection(con1) # use localhost as first priority + con2 = MoneroRpcConnection("http:#example.com") + connection_manager.add_connection(con2) # default priority is prioritized last + + # set current connection + con3 = MoneroRpcConnection("http:#foo.bar", "admin", "password") + connection_manager.set_connection(con3) # connection is added if new - # create or open wallet governed by connection manager - wallet_config = MoneroWalletConfig() - # *** CHANGE README TO "sample_wallet_full" *** - wallet_config.path = "./test_wallets/" + Utils.get_random_string() - wallet_config.password = "supersecretpassword123" - wallet_config.network_type = MoneroNetworkType.TESTNET - # wallet_config.connection_manager = connectionManager - wallet_config.seed = Utils.SEED - wallet_config.restore_height = Utils.FIRST_RECEIVE_HEIGHT - walletFull = MoneroWalletFull.create_wallet(wallet_config) # *** REPLACE WITH FIRST RECEIVE HEIGHT IN README *** - - # check connection status - connectionManager.check_connection() - print(f"Connection manager is connected: {connectionManager.is_connected()}") - print(f"Connection is online: {connectionManager.get_connection().is_online()}") - print(f"Connection is authenticated: {connectionManager.get_connection().is_authenticated()}") - - # receive notifications of any changes to current connection - listener = SampleConnectionListener() - connectionManager.add_listener(listener) - - # check connections every 10 seconds (in order of priority) and switch to the best - connectionManager.start_polling(10000) - - # get best available connection in order of priority then response time - bestConnection: MoneroRpcConnection = connectionManager.get_best_available_connection() - - # check status of all connections - connectionManager.check_connections() - - # get connections in order of current connection, online status from last check, priority, and name - connections: list[MoneroRpcConnection] = connectionManager.get_connections() - - # clear connection manager - connectionManager.clear() + # create or open wallet governed by connection manager + wallet_config = MoneroWalletConfig() + # *** CHANGE README TO "sample_wallet_full" *** + wallet_config.path = "./test_wallets/" + Utils.get_random_string() + wallet_config.password = "supersecretpassword123" + wallet_config.network_type = MoneroNetworkType.TESTNET + # wallet_config.connection_manager = connection_manager + wallet_config.seed = Utils.SEED + wallet_config.restore_height = Utils.FIRST_RECEIVE_HEIGHT + wallet_full = MoneroWalletFull.create_wallet(wallet_config) # *** REPLACE WITH FIRST RECEIVE HEIGHT IN README *** + + # check connection status + connection_manager.check_connection() + print(f"Connection manager is connected: {connection_manager.is_connected()}") + print(f"Connection is online: {connection_manager.get_connection().is_online()}") + print(f"Connection is authenticated: {connection_manager.get_connection().is_authenticated()}") + + # receive notifications of any changes to current connection + listener = SampleConnectionListener() + connection_manager.add_listener(listener) + + # check connections every 10 seconds (in order of priority) and switch to the best + connection_manager.start_polling(10000) + + # get best available connection in order of priority then response time + best_connection: MoneroRpcConnection = connection_manager.get_best_available_connection() + + assert best_connection is not None + + # check status of all connections + connection_manager.check_connections() + + # get connections in order of current connection, online status from last check, priority, and name + connections: list[MoneroRpcConnection] = connection_manager.get_connections() + + # clear connection manager + connection_manager.clear() diff --git a/tests/utils/binary_block_context.py b/tests/utils/binary_block_context.py index f631da2..1b6f1e9 100644 --- a/tests/utils/binary_block_context.py +++ b/tests/utils/binary_block_context.py @@ -3,14 +3,14 @@ class BinaryBlockContext(TestContext): - def __init__(self) -> None: - super().__init__() - self.hasHex = False - self.headerIsFull = False - self.hasTxs = True - self.txContext = TestContext() - self.txContext.isPruned = False - self.txContext.isConfirmed = True - self.txContext.fromGetTxPool = False - self.txContext.hasOutputIndices = False - self.txContext.fromBinaryBlock = True + def __init__(self) -> None: + super().__init__() + self.has_hex = False + self.header_is_full = False + self.has_txs = True + self.tx_context = TestContext() + self.tx_context.is_pruned = False + self.tx_context.is_confirmed = True + self.tx_context.from_get_tx_pool = False + self.tx_context.has_output_indices = False + self.tx_context.from_binary_block = True diff --git a/tests/utils/connection_change_collector.py b/tests/utils/connection_change_collector.py index d92fbe4..4ab7b3d 100644 --- a/tests/utils/connection_change_collector.py +++ b/tests/utils/connection_change_collector.py @@ -2,24 +2,25 @@ from typing_extensions import override from monero import MoneroConnectionManagerListener, MoneroRpcConnection + class ConnectionList(list[MoneroRpcConnection]): - def size(self) -> int: - return len(self) + def size(self) -> int: + return len(self) + + def get(self, index: int) -> Optional[MoneroRpcConnection]: + return self[index] - def get(self, index: int) -> Optional[MoneroRpcConnection]: - return self[index] class ConnectionChangeCollector(MoneroConnectionManagerListener): - changedConnections: ConnectionList + changed_connections: ConnectionList - def __init__(self) -> None: - super().__init__() - self.changedConnections = ConnectionList() + def __init__(self) -> None: + super().__init__() + self.changed_connections = ConnectionList() - @override - def on_connection_changed(self, connection: MoneroRpcConnection) -> None: - self.changedConnections.append(connection) - - + @override + def on_connection_changed(self, connection: Optional[MoneroRpcConnection]) -> None: + if connection is not None: + self.changed_connections.append(connection) diff --git a/tests/utils/monero_test_utils.py b/tests/utils/monero_test_utils.py index 46e33e5..6361f61 100644 --- a/tests/utils/monero_test_utils.py +++ b/tests/utils/monero_test_utils.py @@ -2,15 +2,15 @@ from abc import ABC from random import choices, shuffle from time import sleep, time -from os.path import exists as pathExists +from os.path import exists as path_exists from os import makedirs from monero import ( - MoneroNetworkType, MoneroTx, MoneroUtils, MoneroWalletFull, MoneroRpcConnection, - MoneroWalletConfig, MoneroDaemonRpc, MoneroWalletRpc, MoneroBlockHeader, MoneroBlockTemplate, - MoneroBlock, MoneroDaemonUpdateCheckResult, MoneroDaemonUpdateDownloadResult, MoneroWalletKeys, - MoneroSubaddress, MoneroPeer, MoneroDaemonInfo, MoneroDaemonSyncInfo, MoneroHardForkInfo, - MoneroAltChain, MoneroTxPoolStats, MoneroWallet, MoneroRpcError, MoneroTxConfig, - MoneroAccount, MoneroTxWallet, MoneroTxQuery + MoneroNetworkType, MoneroTx, MoneroUtils, MoneroWalletFull, MoneroRpcConnection, + MoneroWalletConfig, MoneroDaemonRpc, MoneroWalletRpc, MoneroBlockHeader, MoneroBlockTemplate, + MoneroBlock, MoneroDaemonUpdateCheckResult, MoneroDaemonUpdateDownloadResult, MoneroWalletKeys, + MoneroSubaddress, MoneroPeer, MoneroDaemonInfo, MoneroDaemonSyncInfo, MoneroHardForkInfo, + MoneroAltChain, MoneroTxPoolStats, MoneroWallet, MoneroRpcError, MoneroTxConfig, + MoneroAccount, MoneroTxWallet, MoneroTxQuery, MoneroConnectionSpan ) from .wallet_sync_printer import WalletSyncPrinter @@ -20,879 +20,887 @@ class MoneroTestUtils(ABC): - # directory with monero binaries to test (monerod and monero-wallet-rpc) - MONERO_BINS_DIR = "" - WALLET_PORT_OFFSETS: dict[MoneroWalletRpc, int] = {} - BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' - - _WALLET_FULL: Optional[MoneroWalletFull] = None - _WALLET_KEYS: Optional[MoneroWalletKeys] = None - _WALLET_RPC: Optional[MoneroWalletRpc] = None - _DAEMON_RPC: Optional[MoneroDaemonRpc] = None - # monero daemon rpc endpoint configuration (change per your configuration) - DAEMON_RPC_URI: str = "localhost:28081" - DAEMON_RPC_USERNAME: str = "" - DAEMON_RPC_PASSWORD: str = "" - DAEMON_LOCAL_PATH = MONERO_BINS_DIR + "/monerod" - TEST_NON_RELAYS: bool = True - - WALLET_TX_TRACKER = WalletTxTracker() - - # monero wallet rpc configuration (change per your configuration) - WALLET_RPC_PORT_START: int = 28084 # test wallet executables will bind to consecutive ports after these - WALLET_RPC_ZMQ_ENABLED: bool = False - WALLET_RPC_ZMQ_PORT_START: int = 58083 - WALLET_RPC_ZMQ_BIND_PORT_START: int = 48083 # TODO: zmq bind port necessary? - WALLET_RPC_USERNAME: str = "rpc_user" - WALLET_RPC_PASSWORD: str = "abc123" - WALLET_RPC_ZMQ_DOMAIN: str = "127.0.0.1" - WALLET_RPC_DOMAIN: str = "localhost" - WALLET_RPC_URI = WALLET_RPC_DOMAIN + ":" + str(WALLET_RPC_PORT_START) - WALLET_RPC_ZMQ_ENABLED: bool = False - WALLET_RPC_ZMQ_URI = "tcp:#" + WALLET_RPC_ZMQ_DOMAIN + ":" + str(WALLET_RPC_ZMQ_PORT_START) - WALLET_RPC_LOCAL_PATH = MONERO_BINS_DIR + "/monero-wallet-rpc" - WALLET_RPC_LOCAL_WALLET_DIR = MONERO_BINS_DIR - WALLET_RPC_ACCESS_CONTROL_ORIGINS = "http:#localhost:8080" # cors access from web browser - - # test wallet config - WALLET_NAME = "test_wallet_1" - WALLET_PASSWORD = "supersecretpassword123" - TEST_WALLETS_DIR = "./test_wallets" - WALLET_FULL_PATH = TEST_WALLETS_DIR + "/" + WALLET_NAME - - # test wallet constants - MAX_FEE = 7500000*10000 - NETWORK_TYPE: MoneroNetworkType = MoneroNetworkType.TESTNET - LANGUAGE: str = "English" - SEED: str = "silk mocked cucumber lettuce hope adrenalin aching lush roles fuel revamp baptism wrist long tender teardrop midst pastry pigment equip frying inbound pinched ravine frying" - ADDRESS: str = "A1y9sbVt8nqhZAVm3me1U18rUVXcjeNKuBd1oE2cTs8biA9cozPMeyYLhe77nPv12JA3ejJN3qprmREriit2fi6tJDi99RR" - FIRST_RECEIVE_HEIGHT: int = 171 # NOTE: this value must be the height of the wallet's first tx for tests - SYNC_PERIOD_IN_MS: int = 5000 # period between wallet syncs in milliseconds - OFFLINE_SERVER_URI: str = "offline_server_uri" # dummy server uri to remain offline because wallet2 connects to default if not given - AUTO_CONNECT_TIMEOUT_MS: int = 3000 - - @classmethod - def current_timestamp(cls) -> int: - return round(time() * 1000) - - @classmethod - def current_timestamp_str(cls) -> str: - return f"{cls.current_timestamp()}" - - @classmethod - def network_type_to_str(cls, nettype: MoneroNetworkType) -> str: - if nettype == MoneroNetworkType.MAINNET: - return "mainnet" - elif nettype == MoneroNetworkType.TESTNET: - return "testnet" - elif nettype == MoneroNetworkType.STAGENET: - return "stagenet" - - raise TypeError(f"Invalid network type provided: {str(nettype)}") - - @classmethod - def get_network_type(cls) -> str: - return cls.network_type_to_str(cls.NETWORK_TYPE) - - @classmethod - def createDirIfNotExists(cls, dirPath: str) -> None: - print(f"createDirIfNotExists(): {dirPath}") - if pathExists(dirPath): - return - - makedirs(dirPath) - - @classmethod - def initializeTestWalletDir(cls) -> None: - cls.createDirIfNotExists(cls.TEST_WALLETS_DIR) - - @classmethod - def assert_false(cls, expr: Any, message: str = "assertion failed"): - assert expr == False, message - - @classmethod - def assert_true(cls, expr: Any, message: str = "assertion failed"): - assert expr == True, message - - @classmethod - def assert_not_none(cls, expr: Any, message: str = "assertion failed"): - assert expr is not None, message - - @classmethod - def assert_is_none(cls, expr: Any, message: str = "assertion failed"): - assert expr is None, message - - @classmethod - def assert_equals(cls, expr1: Any, expr2: Any, message: str = "assertion failed"): - assert expr1 == expr2, f"{message}: {expr1} == {expr2}" - - @classmethod - def assert_not_equals(cls, expr1: Any, expr2: Any, message: str = "assertion failed"): - assert expr1 != expr2, f"{message}: {expr1} != {expr2}" - - @classmethod - def assert_is(cls, expr: Any, what: Any, message: str = "assertion failed"): - assert expr is what, f"{message}: {expr} is {what}" - - @classmethod - def get_random_string(cls, n: int = 25) -> str: - return ''.join(choices(cls.BASE58_ALPHABET, k=n)) - - @classmethod - def start_wallet_rpc_process(cls, offline: bool = False) -> MoneroWalletRpc: - # get next available offset of ports to bind to - portOffset: int = 1 - while (portOffset in cls.WALLET_PORT_OFFSETS.values()): - portOffset += 1 - - # create command to start client with internal monero-wallet-rpc process - cmd: list[str] = [ - cls.WALLET_RPC_LOCAL_PATH, - "--" + cls.get_network_type(), - "--rpc-bind-port", f"{cls.WALLET_RPC_PORT_START + portOffset}", - "--rpc-login", cls.WALLET_RPC_USERNAME + ":" + cls.WALLET_RPC_PASSWORD, - "--wallet-dir", cls.WALLET_RPC_LOCAL_WALLET_DIR, - "--rpc-access-control-origins", cls.WALLET_RPC_ACCESS_CONTROL_ORIGINS - ] - if (offline): - cmd.append("--offline") - else: - cmd.extend(["--daemon-address", cls.DAEMON_RPC_URI]) - if cls.DAEMON_RPC_USERNAME is not None and cls.DAEMON_RPC_USERNAME != "": - cmd.extend(["--daemon-login", cls.DAEMON_RPC_USERNAME + ":" + cls.DAEMON_RPC_PASSWORD]) - - # start with zmq if enabled - if (cls.WALLET_RPC_ZMQ_ENABLED): - cmd.extend(["--zmq-rpc-bind-port", f"{cls.WALLET_RPC_ZMQ_BIND_PORT_START + portOffset}"]) - cmd.extend(["--zmq-pub", "tcp://" + cls.WALLET_RPC_ZMQ_DOMAIN + ":" + f"{cls.WALLET_RPC_ZMQ_PORT_START + portOffset}"]) - else: - #cmd.add("--no-zmq") # TODO: enable this when zmq supported in monero-wallet-rpc - pass - - # register wallet with port offset - try: - wallet = MoneroWalletRpc(cmd) - cls.WALLET_PORT_OFFSETS[wallet] = portOffset - return wallet - except Exception as e: - raise Exception(e) - - @classmethod - def stop_wallet_rpc_process(cls, wallet: MoneroWalletRpc): - del cls.WALLET_PORT_OFFSETS[wallet] - wallet.stop_process() - - @classmethod - def wait_for(cls, time: int): - sleep(time / 1000) - - @classmethod - def check_test_wallets_dir_exists(cls) -> bool: - return pathExists(cls.TEST_WALLETS_DIR) - - @classmethod - def create_test_wallets_dir(cls) -> None: - makedirs(cls.TEST_WALLETS_DIR) - - @classmethod - def get_daemon_rpc(cls) -> MoneroDaemonRpc: - if cls._DAEMON_RPC is None: - cls._DAEMON_RPC = MoneroDaemonRpc(cls.DAEMON_RPC_URI, cls.DAEMON_RPC_USERNAME, cls.DAEMON_RPC_PASSWORD) - - return cls._DAEMON_RPC - - @classmethod - def get_wallet_keys_config(cls) -> MoneroWalletConfig: - config = MoneroWalletConfig() - config.network_type = cls.NETWORK_TYPE - config.seed = cls.SEED - return config - - @classmethod - def get_wallet_keys(cls) -> MoneroWalletKeys: - if cls._WALLET_KEYS is None: - config = cls.get_wallet_keys_config() - cls._WALLET_KEYS = MoneroWalletKeys.create_wallet_from_seed(config) - - return cls._WALLET_KEYS - - @classmethod - def get_wallet_full_config(cls, daemon_connection: MoneroRpcConnection) -> MoneroWalletConfig: - config = MoneroWalletConfig() - config.path = cls.WALLET_FULL_PATH - config.password = cls.WALLET_PASSWORD - config.network_type = cls.NETWORK_TYPE - config.seed = cls.SEED - config.server = daemon_connection - config.restore_height = cls.FIRST_RECEIVE_HEIGHT - - return config - - @classmethod - def get_wallet_full(cls) -> MoneroWalletFull: - if cls._WALLET_FULL is None: - # create wallet from seed if it doesn't exist - if not MoneroWalletFull.wallet_exists(cls.WALLET_FULL_PATH): - - # create directory for test wallets if it doesn't exist - cls.initializeTestWalletDir() - - # create wallet with connection - daemon_connection = MoneroRpcConnection(cls.DAEMON_RPC_URI, cls.DAEMON_RPC_USERNAME, cls.DAEMON_RPC_PASSWORD) - config = cls.get_wallet_full_config(daemon_connection) - cls._WALLET_FULL = MoneroWalletFull.create_wallet(config) - assert cls.FIRST_RECEIVE_HEIGHT, cls._WALLET_FULL.get_restore_height() - assert daemon_connection == cls._WALLET_FULL.get_daemon_connection() - - # otherwise open existing wallet and update daemon connection - else: - cls._WALLET_FULL = MoneroWalletFull.open_wallet(cls.WALLET_FULL_PATH, cls.WALLET_PASSWORD, cls.NETWORK_TYPE) - cls._WALLET_FULL.set_daemon_connection(cls.get_daemon_rpc().get_rpc_connection()) - - # sync and save wallet - listener = WalletSyncPrinter() - cls._WALLET_FULL.sync(listener) - cls._WALLET_FULL.save() - cls._WALLET_FULL.start_syncing(cls.SYNC_PERIOD_IN_MS) # start background synchronizing with sync period - - # ensure we're testing the right wallet - assert cls.SEED == cls._WALLET_FULL.get_seed() - assert cls.ADDRESS == cls._WALLET_FULL.get_primary_address() - return cls._WALLET_FULL - - @classmethod - def get_wallet_rpc(cls) -> MoneroWalletRpc: - if cls._WALLET_RPC is None: - - # construct wallet rpc instance with daemon connection - rpc = MoneroRpcConnection(cls.WALLET_RPC_URI, cls.WALLET_RPC_USERNAME, cls.WALLET_RPC_PASSWORD, cls.WALLET_RPC_ZMQ_URI if cls.WALLET_RPC_ZMQ_ENABLED else '') - cls._WALLET_RPC = MoneroWalletRpc(rpc) - - # attempt to open test wallet - try: - cls._WALLET_RPC.open_wallet(cls.WALLET_NAME, cls.WALLET_PASSWORD) - except MoneroRpcError as e: - - # -1 returned when wallet does not exist or fails to open e.g. it's already open by another application - if (e.get_code() == -1): - # create wallet + # directory with monero binaries to test (monerod and monero-wallet-rpc) + MONERO_BINS_DIR = "" + WALLET_PORT_OFFSETS: dict[MoneroWalletRpc, int] = {} + BASE58_ALPHABET = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' + + _WALLET_FULL: Optional[MoneroWalletFull] = None + _WALLET_KEYS: Optional[MoneroWalletKeys] = None + _WALLET_RPC: Optional[MoneroWalletRpc] = None + _DAEMON_RPC: Optional[MoneroDaemonRpc] = None + # monero daemon rpc endpoint configuration (change per your configuration) + DAEMON_RPC_URI: str = "localhost:28081" + DAEMON_RPC_USERNAME: str = "" + DAEMON_RPC_PASSWORD: str = "" + DAEMON_LOCAL_PATH = MONERO_BINS_DIR + "/monerod" + TEST_NON_RELAYS: bool = True + + WALLET_TX_TRACKER = WalletTxTracker() + + # monero wallet rpc configuration (change per your configuration) + WALLET_RPC_PORT_START: int = 28084 # test wallet executables will bind to consecutive ports after these + WALLET_RPC_ZMQ_ENABLED: bool = False + WALLET_RPC_ZMQ_PORT_START: int = 58083 + WALLET_RPC_ZMQ_BIND_PORT_START: int = 48083 # TODO: zmq bind port necessary? + WALLET_RPC_USERNAME: str = "rpc_user" + WALLET_RPC_PASSWORD: str = "abc123" + WALLET_RPC_ZMQ_DOMAIN: str = "127.0.0.1" + WALLET_RPC_DOMAIN: str = "localhost" + WALLET_RPC_URI = WALLET_RPC_DOMAIN + ":" + str(WALLET_RPC_PORT_START) + WALLET_RPC_ZMQ_URI = "tcp:#" + WALLET_RPC_ZMQ_DOMAIN + ":" + str(WALLET_RPC_ZMQ_PORT_START) + WALLET_RPC_LOCAL_PATH = MONERO_BINS_DIR + "/monero-wallet-rpc" + WALLET_RPC_LOCAL_WALLET_DIR = MONERO_BINS_DIR + WALLET_RPC_ACCESS_CONTROL_ORIGINS = "http:#localhost:8080" # cors access from web browser + + # test wallet config + WALLET_NAME = "test_wallet_1" + WALLET_PASSWORD = "supersecretpassword123" + TEST_WALLETS_DIR = "./test_wallets" + WALLET_FULL_PATH = TEST_WALLETS_DIR + "/" + WALLET_NAME + + # test wallet constants + MAX_FEE = 7500000*10000 + NETWORK_TYPE: MoneroNetworkType = MoneroNetworkType.TESTNET + LANGUAGE: str = "English" + SEED: str = "silk mocked cucumber lettuce hope adrenalin aching lush roles fuel revamp baptism wrist long tender teardrop midst pastry pigment equip frying inbound pinched ravine frying" + ADDRESS: str = "A1y9sbVt8nqhZAVm3me1U18rUVXcjeNKuBd1oE2cTs8biA9cozPMeyYLhe77nPv12JA3ejJN3qprmREriit2fi6tJDi99RR" + FIRST_RECEIVE_HEIGHT: int = 171 # NOTE: this value must be the height of the wallet's first tx for tests + SYNC_PERIOD_IN_MS: int = 5000 # period between wallet syncs in milliseconds + OFFLINE_SERVER_URI: str = "offline_server_uri" # dummy server uri to remain offline because wallet2 connects to default if not given + AUTO_CONNECT_TIMEOUT_MS: int = 3000 + + @classmethod + def current_timestamp(cls) -> int: + return round(time() * 1000) + + @classmethod + def current_timestamp_str(cls) -> str: + return f"{cls.current_timestamp()}" + + @classmethod + def network_type_to_str(cls, nettype: MoneroNetworkType) -> str: + if nettype == MoneroNetworkType.MAINNET: + return "mainnet" + elif nettype == MoneroNetworkType.TESTNET: + return "testnet" + elif nettype == MoneroNetworkType.STAGENET: + return "stagenet" + + raise TypeError(f"Invalid network type provided: {str(nettype)}") + + @classmethod + def get_network_type(cls) -> str: + return cls.network_type_to_str(cls.NETWORK_TYPE) + + @classmethod + def create_dir_if_not_exists(cls, dirPath: str) -> None: + print(f"create_dir_if_not_exists(): {dirPath}") + if path_exists(dirPath): + return + + makedirs(dirPath) + + @classmethod + def initialize_test_wallet_dir(cls) -> None: + cls.create_dir_if_not_exists(cls.TEST_WALLETS_DIR) + + @classmethod + def assert_false(cls, expr: Any, message: str = "assertion failed"): + assert expr == False, message + + @classmethod + def assert_true(cls, expr: Any, message: str = "assertion failed"): + assert expr == True, message + + @classmethod + def assert_not_none(cls, expr: Any, message: str = "assertion failed"): + assert expr is not None, message + + @classmethod + def assert_is_none(cls, expr: Any, message: str = "assertion failed"): + assert expr is None, message + + @classmethod + def assert_equals(cls, expr1: Any, expr2: Any, message: str = "assertion failed"): + assert expr1 == expr2, f"{message}: {expr1} == {expr2}" + + @classmethod + def assert_not_equals(cls, expr1: Any, expr2: Any, message: str = "assertion failed"): + assert expr1 != expr2, f"{message}: {expr1} != {expr2}" + + @classmethod + def assert_is(cls, expr: Any, what: Any, message: str = "assertion failed"): + assert expr is what, f"{message}: {expr} is {what}" + + @classmethod + def get_random_string(cls, n: int = 25) -> str: + return ''.join(choices(cls.BASE58_ALPHABET, k=n)) + + @classmethod + def start_wallet_rpc_process(cls, offline: bool = False) -> MoneroWalletRpc: + # get next available offset of ports to bind to + portOffset: int = 1 + while (portOffset in cls.WALLET_PORT_OFFSETS.values()): + portOffset += 1 + + # create command to start client with internal monero-wallet-rpc process + cmd: list[str] = [ + cls.WALLET_RPC_LOCAL_PATH, + "--" + cls.get_network_type(), + "--rpc-bind-port", f"{cls.WALLET_RPC_PORT_START + portOffset}", + "--rpc-login", cls.WALLET_RPC_USERNAME + ":" + cls.WALLET_RPC_PASSWORD, + "--wallet-dir", cls.WALLET_RPC_LOCAL_WALLET_DIR, + "--rpc-access-control-origins", cls.WALLET_RPC_ACCESS_CONTROL_ORIGINS + ] + if (offline): + cmd.append("--offline") + else: + cmd.extend(["--daemon-address", cls.DAEMON_RPC_URI]) + if cls.DAEMON_RPC_USERNAME != "": + cmd.extend(["--daemon-login", cls.DAEMON_RPC_USERNAME + ":" + cls.DAEMON_RPC_PASSWORD]) + + # start with zmq if enabled + if (cls.WALLET_RPC_ZMQ_ENABLED): + cmd.extend(["--zmq-rpc-bind-port", f"{cls.WALLET_RPC_ZMQ_BIND_PORT_START + portOffset}"]) + cmd.extend(["--zmq-pub", "tcp://" + cls.WALLET_RPC_ZMQ_DOMAIN + ":" + f"{cls.WALLET_RPC_ZMQ_PORT_START + portOffset}"]) + else: + #cmd.add("--no-zmq") # TODO: enable this when zmq supported in monero-wallet-rpc + pass + + # register wallet with port offset + try: + wallet = MoneroWalletRpc(cmd) + cls.WALLET_PORT_OFFSETS[wallet] = portOffset + return wallet + except Exception as e: + raise e + + @classmethod + def stop_wallet_rpc_process(cls, wallet: MoneroWalletRpc): + del cls.WALLET_PORT_OFFSETS[wallet] + wallet.stop_process() + + @classmethod + def wait_for(cls, time: int): + sleep(time / 1000) + + @classmethod + def check_test_wallets_dir_exists(cls) -> bool: + return path_exists(cls.TEST_WALLETS_DIR) + + @classmethod + def create_test_wallets_dir(cls) -> None: + makedirs(cls.TEST_WALLETS_DIR) + + @classmethod + def get_daemon_rpc(cls) -> MoneroDaemonRpc: + if cls._DAEMON_RPC is None: + cls._DAEMON_RPC = MoneroDaemonRpc(cls.DAEMON_RPC_URI, cls.DAEMON_RPC_USERNAME, cls.DAEMON_RPC_PASSWORD) + + return cls._DAEMON_RPC + + @classmethod + def get_wallet_keys_config(cls) -> MoneroWalletConfig: + config = MoneroWalletConfig() + config.network_type = cls.NETWORK_TYPE + config.seed = cls.SEED + return config + + @classmethod + def get_wallet_keys(cls) -> MoneroWalletKeys: + if cls._WALLET_KEYS is None: + config = cls.get_wallet_keys_config() + cls._WALLET_KEYS = MoneroWalletKeys.create_wallet_from_seed(config) + + return cls._WALLET_KEYS + + @classmethod + def get_wallet_full_config(cls, daemon_connection: MoneroRpcConnection) -> MoneroWalletConfig: config = MoneroWalletConfig() - config.path = cls.WALLET_NAME + config.path = cls.WALLET_FULL_PATH config.password = cls.WALLET_PASSWORD + config.network_type = cls.NETWORK_TYPE config.seed = cls.SEED + config.server = daemon_connection config.restore_height = cls.FIRST_RECEIVE_HEIGHT - cls._WALLET_RPC.create_wallet(config) - else: - raise e - - # ensure we're testing the right wallet - assert cls.SEED == cls._WALLET_RPC.get_seed() - assert cls.ADDRESS == cls._WALLET_RPC.get_primary_address() - - # sync and save wallet - cls._WALLET_RPC.sync() - cls._WALLET_RPC.save() - cls._WALLET_RPC.start_syncing(cls.SYNC_PERIOD_IN_MS) - - # return cached wallet rpc - return cls._WALLET_RPC - - @classmethod - def create_wallet_ground_truth(cls, networkType: MoneroNetworkType, seed: str, startHeight: int | None, restoreHeight: int | None) -> MoneroWalletFull: - # create directory for test wallets if it doesn't exist - if not cls.check_test_wallets_dir_exists(): - cls.create_test_wallets_dir() - - # create ground truth wallet - daemonConnection = MoneroRpcConnection(cls.DAEMON_RPC_URI, cls.DAEMON_RPC_USERNAME, cls.DAEMON_RPC_PASSWORD) - path = cls.TEST_WALLETS_DIR + "/gt_wallet_" + cls.current_timestamp_str() - config = MoneroWalletConfig() - config.path = path - config.password = cls.WALLET_PASSWORD - config.network_type = networkType - config.seed = seed - config.server = daemonConnection - config.restore_height = restoreHeight - - if startHeight is None: - startHeight = 0 - - gtWallet = MoneroWalletFull.create_wallet(config) - cls.assert_equals(restoreHeight, gtWallet.get_restore_height()) - gtWallet.sync(startHeight, WalletSyncPrinter()) - gtWallet.start_syncing(cls.SYNC_PERIOD_IN_MS) - - # close the full wallet when the runtime is shutting down to release resources - - return gtWallet - - @classmethod - def test_invalid_address(cls, address: Optional[str], networkType: MoneroNetworkType) -> None: - if address is None: - return - - cls.assert_false(MoneroUtils.is_valid_address(address, networkType)) - try: - MoneroUtils.validate_address(address, networkType) - raise Exception("Should have thrown exception") - except Exception as e: - cls.assert_false(len(str(e)) == 0) - - @classmethod - def test_invalid_private_view_key(cls, privateViewKey: Optional[str]): - if privateViewKey is None: - return - - cls.assert_false(MoneroUtils.is_valid_private_view_key(privateViewKey)) - try: - MoneroUtils.validate_private_view_key(privateViewKey) - raise Exception("Should have thrown exception") - except Exception as e: - cls.assert_false(len(str(e)) == 0) - - @classmethod - def test_invalid_public_view_key(cls, public_view_key: Optional[str]) -> None: - if public_view_key is None: - return - - cls.assert_false(MoneroUtils.is_valid_public_view_key(public_view_key)) - try: - MoneroUtils.validate_public_view_key(public_view_key) - raise Exception("Should have thrown exception") - except Exception as e: - cls.assert_false(len(str(e)) == 0) - - @classmethod - def test_invalid_private_spend_key(cls, privateSpendKey: Optional[str]): - if privateSpendKey is None: - return - - try: - cls.assert_false(MoneroUtils.is_valid_private_spend_key(privateSpendKey)) - MoneroUtils.validate_private_spend_key(privateSpendKey) - raise Exception("Should have thrown exception") - except Exception as e: - cls.assert_false(len(str(e)) == 0) - - @classmethod - def test_invalid_public_spend_key(cls, publicSpendKey: Optional[str]): - if publicSpendKey is None: - return - - cls.assert_false(MoneroUtils.is_valid_public_spend_key(publicSpendKey)) - try: - MoneroUtils.validate_public_spend_key(publicSpendKey) - raise Exception("Should have thrown exception") - except Exception as e: - cls.assert_false(len(str(e)) == 0) - - @classmethod - def test_block_template(cls, template: MoneroBlockTemplate): - cls.assert_not_none(template) - cls.assert_not_none(template.block_template_blob) - cls.assert_not_none(template.block_hashing_blob) - cls.assert_not_none(template.difficulty) - cls.assert_not_none(template.expected_reward) - cls.assert_not_none(template.height) - cls.assert_not_none(template.prev_hash) - cls.assert_not_none(template.reserved_offset) - cls.assert_not_none(template.seed_height) - assert template.seed_height is not None - cls.assert_true(template.seed_height > 0) - cls.assert_not_none(template.seed_hash) - cls.assert_false(template.seed_hash == "") - # next seed hash can be null or initialized TODO: test circumstances for each - - @classmethod - def test_block_header(cls, header: MoneroBlockHeader, is_full: bool): - cls.assert_not_none(header) - assert header.height is not None - cls.assert_true(header.height >= 0) - assert header.major_version is not None - cls.assert_true(header.major_version > 0) - assert header.minor_version is not None - cls.assert_true(header.minor_version >= 0) - assert header.timestamp is not None - if (header.height == 0): - cls.assert_true(header.timestamp == 0) - else: - cls.assert_true(header.timestamp > 0) - cls.assert_not_none(header.prev_hash) - cls.assert_not_none(header.nonce) - if (header.nonce == 0): - print(f"WARNING: header nonce is 0 at height {header.height}") # TODO (monero-project): why is header nonce 0? - else: - assert header.nonce is not None - cls.assert_true(header.nonce > 0) - cls.assert_is_none(header.pow_hash) # never seen defined - if (is_full): - assert header.size is not None - assert header.depth is not None - assert header.difficulty is not None - assert header.cumulative_difficulty is not None - assert header.hash is not None - assert header.miner_tx_hash is not None - assert header.num_txs is not None - assert header.weight is not None - cls.assert_true(header.size > 0) - cls.assert_true(header.depth >= 0) - cls.assert_true(header.difficulty > 0) - cls.assert_true(header.cumulative_difficulty > 0) - cls.assert_equals(64, len(header.hash)) - cls.assert_equals(64, len(header.miner_tx_hash)) - cls.assert_true(header.num_txs >= 0) - cls.assert_not_none(header.orphan_status) - cls.assert_not_none(header.reward) - cls.assert_not_none(header.weight) - cls.assert_true(header.weight > 0) - else: - cls.assert_is_none(header.size) - cls.assert_is_none(header.depth) - cls.assert_is_none(header.difficulty) - cls.assert_is_none(header.cumulative_difficulty) - cls.assert_is_none(header.hash) - cls.assert_is_none(header.miner_tx_hash) - cls.assert_is_none(header.num_txs) - cls.assert_is_none(header.orphan_status) - cls.assert_is_none(header.reward) - cls.assert_is_none(header.weight) - - @classmethod - def test_miner_tx(cls, miner_tx: MoneroTx): - assert miner_tx is not None - cls.assert_not_none(miner_tx.is_miner_tx) - assert miner_tx.version is not None - cls.assert_true(miner_tx.version >= 0) - cls.assert_not_none(miner_tx.extra) - cls.assert_true(len(miner_tx.extra) > 0) - assert miner_tx.unlock_time is not None - cls.assert_true(miner_tx.unlock_time >= 0) - - # TODO: miner tx does not have hashes in binary requests so this will fail, need to derive using prunable data - # TestContext ctx = new TestContext() - # ctx.hasJson = false - # ctx.isPruned = true - # ctx.is_full = false - # ctx.isConfirmed = true - # ctx.isMiner = true - # ctx.fromGetTxPool = true - # testTx(miner_tx, ctx) - - @classmethod - def test_tx(cls, tx: MoneroTx, ctx: TestContext): - raise NotImplementedError() - - # TODO: test block deep copy - @classmethod - def test_block(cls, block: MoneroBlock, ctx: TestContext): - # test required fields - cls.assert_not_none(block) - assert block.miner_tx is not None - cls.test_miner_tx(block.miner_tx) # TODO: miner tx doesn't have as much stuff, can't call testTx? - cls.test_block_header(block, ctx.headerIsFull) - - if (ctx.hasHex): - assert block.hex is not None - cls.assert_true(len(block.hex) > 1) - else: - cls.assert_is_none(block.hex) - - if (ctx.hasTxs): - cls.assert_not_none(ctx.txContext) - for tx in block.txs: - cls.assert_true(block == tx.block) - cls.test_tx(tx, ctx.txContext) - - else: - cls.assert_is_none(ctx.txContext) - cls.assert_is_none(block.txs) - - @classmethod - def is_empty(cls, value: Union[str, list, None]) -> bool: - return value == "" - - @classmethod - def test_update_check_result(cls, result: MoneroDaemonUpdateCheckResult): - cls.assert_true(isinstance(result,MoneroDaemonUpdateCheckResult)) - cls.assert_not_none(result.is_update_available) - if (result.is_update_available): - cls.assert_false(cls.is_empty(result.auto_uri), "No auto uri is daemon online?") - cls.assert_false(cls.is_empty(result.user_uri)) - cls.assert_false(cls.is_empty(result.version)) - cls.assert_false(cls.is_empty(result.hash)) - assert result.hash is not None - cls.assert_equals(64, len(result.hash)) - else: - cls.assert_is_none(result.auto_uri) - cls.assert_is_none(result.user_uri) - cls.assert_is_none(result.version) - cls.assert_is_none(result.hash) - - @classmethod - def test_update_download_result(cls, result: MoneroDaemonUpdateDownloadResult, path: Optional[str]): - cls.test_update_check_result(result) - if (result.is_update_available): - if path is not None: - cls.assert_equals(path, result.download_path) - else: - cls.assert_not_none(result.download_path) - else: - cls.assert_is_none(result.download_path) - - @classmethod - def test_unsigned_big_integer(cls, value: Any, boolVal: bool = False): - if not isinstance(value, int): - raise Exception("Value is not number") - - if value < 0: - raise Exception("Value cannot be negative") - - @classmethod - def test_account(cls, account: Optional[MoneroAccount]): - # test account - assert account is not None - assert account.index is not None - assert account.index >= 0 - assert account.primary_address is not None - - MoneroUtils.validate_address(account.primary_address, cls.NETWORK_TYPE) - cls.test_unsigned_big_integer(account.balance) - cls.test_unsigned_big_integer(account.unlocked_balance) - - # if given, test subaddresses and that their balances add up to account balances - if account.subaddresses is not None: - balance = 0 - unlockedBalance = 0 - i = 0 - j = len(account.subaddresses) - while i < j: - cls.test_subaddress(account.subaddresses[i]) - assert account.index == account.subaddresses[i].account_index - assert i == account.subaddresses[i].index - address_balance = account.subaddresses[i].balance - assert address_balance is not None - balance += address_balance - address_balance = account.subaddresses[i].unlocked_balance - assert address_balance is not None - unlockedBalance += address_balance - - assert account.balance == balance, "Subaddress balances " + str(balance) + " != account " + str(account.index) + " balance " + str(account.balance) - assert account.unlocked_balance == unlockedBalance, "Subaddress unlocked balances " + str(unlockedBalance) + " != account " + str(account.index) + " unlocked balance " + str(account.unlocked_balance) - - # tag must be undefined or non-empty - tag = account.tag - assert tag is None or len(tag) > 0 - - @classmethod - def test_subaddress(cls, subaddress: MoneroSubaddress): - assert subaddress.account_index is not None - assert subaddress.index is not None - assert subaddress.balance is not None - assert subaddress.num_unspent_outputs is not None - assert subaddress.num_blocks_to_unlock is not None - - cls.assert_true(subaddress.account_index >= 0) - cls.assert_true(subaddress.index >= 0) - cls.assert_not_none(subaddress.address) - cls.assert_true(subaddress.label is None or subaddress.label != "") - cls.test_unsigned_big_integer(subaddress.balance) - cls.test_unsigned_big_integer(subaddress.unlocked_balance) - cls.assert_true(subaddress.num_unspent_outputs >= 0) - cls.assert_not_none(subaddress.is_used) - if subaddress.balance > 0: - cls.assert_true(subaddress.is_used) - cls.assert_true(subaddress.num_blocks_to_unlock >= 0) - - @classmethod - def assert_subaddress_equal(cls, subaddress: Optional[MoneroSubaddress], other: Optional[MoneroSubaddress]): - if subaddress is None and other is None: - return - assert not (subaddress is None or other is None) - assert subaddress.address == other.address - assert subaddress.account_index == other.account_index - assert subaddress.balance == other.balance - assert subaddress.index == other.index - assert subaddress.is_used == other.is_used - assert subaddress.label == other.label - assert subaddress.num_blocks_to_unlock == other.num_blocks_to_unlock - assert subaddress.num_unspent_outputs == other.num_unspent_outputs - assert subaddress.unlocked_balance == other.unlocked_balance - - @classmethod - def assert_subaddresses_equal(cls, subaddresses1: list[MoneroSubaddress], subaddresses2: list[MoneroSubaddress]): - size1 = len(subaddresses1) - size2 = len(subaddresses2) - if size1 != size2: - raise Exception("Number of subaddresses doens't match") - - i = 0 - - while i < size1: - cls.assert_subaddress_equal(subaddresses1[i], subaddresses2[i]) - i += 1 - - @classmethod - def test_known_peer(cls, peer: Optional[MoneroPeer], from_connection: bool): - assert peer is not None, "Peer is null" - assert peer.id is not None - assert peer.host is not None - assert peer.port is not None - cls.assert_false(len(peer.id) == 0) - cls.assert_false(len(peer.host) == 0) - cls.assert_true(peer.port > 0) - cls.assert_true(peer.rpc_port is None or peer.rpc_port >= 0) - cls.assert_not_none(peer.is_online) - if peer.rpc_credits_per_hash is not None: - cls.test_unsigned_big_integer(peer.rpc_credits_per_hash) - if (from_connection): - cls.assert_is_none(peer.last_seen_timestamp) - else: - assert peer.last_seen_timestamp is not None - - if (peer.last_seen_timestamp < 0): - print(f"Last seen timestamp is invalid: {peer.last_seen_timestamp}") - cls.assert_true(peer.last_seen_timestamp >= 0) - - cls.assert_true(peer.pruning_seed is None or peer.pruning_seed >= 0) - - @classmethod - def test_peer(cls, peer: MoneroPeer): - cls.assert_true(isinstance(peer, MoneroPeer)) - cls.test_known_peer(peer, True) - assert peer.hash is not None - assert peer.avg_download is not None - assert peer.avg_upload is not None - assert peer.current_download is not None - assert peer.current_upload is not None - assert peer.height is not None - assert peer.live_time is not None - assert peer.num_receives is not None - assert peer.receive_idle_time is not None - assert peer.num_sends is not None - assert peer.send_idle_time is not None - assert peer.num_support_flags is not None - - cls.assert_false(len(peer.hash) == 0) - cls.assert_true(peer.avg_download >= 0) - cls.assert_true(peer.avg_upload >= 0) - cls.assert_true(peer.current_download >= 0) - cls.assert_true(peer.current_upload >= 0) - cls.assert_true(peer.height >= 0) - cls.assert_true(peer.live_time >= 0) - cls.assert_not_none(peer.is_local_ip) - cls.assert_not_none(peer.is_local_host) - cls.assert_true(peer.num_receives >= 0) - cls.assert_true(peer.receive_idle_time >= 0) - cls.assert_true(peer.num_sends >= 0) - cls.assert_true(peer.send_idle_time >= 0) - cls.assert_not_none(peer.state) - cls.assert_true(peer.num_support_flags >= 0) - cls.assert_not_none(peer.connection_type) - - @classmethod - def test_info(cls, info: MoneroDaemonInfo): - assert info.num_alt_blocks is not None - assert info.block_size_limit is not None - assert info.block_size_median is not None - assert info.num_offline_peers is not None - assert info.num_online_peers is not None - assert info.height is not None - assert info.height_without_bootstrap is not None - assert info.num_incoming_connections is not None - assert info.num_outgoing_connections is not None - assert info.num_rpc_connections is not None - assert info.start_timestamp is not None - assert info.adjusted_timestamp is not None - assert info.target is not None - assert info.target_height is not None - assert info.num_txs is not None - assert info.num_txs_pool is not None - assert info.block_weight_limit is not None - assert info.block_weight_median is not None - assert info.database_size is not None - - cls.assert_not_none(info.version) - cls.assert_true(info.num_alt_blocks >= 0) - cls.assert_true(info.block_size_limit > 0) - cls.assert_true(info.block_size_median > 0) - cls.assert_true(info.bootstrap_daemon_address is None or not cls.is_empty(info.bootstrap_daemon_address)) - cls.test_unsigned_big_integer(info.cumulative_difficulty) - cls.test_unsigned_big_integer(info.free_space) - cls.assert_true(info.num_offline_peers >= 0) - cls.assert_true(info.num_online_peers >= 0) - cls.assert_true(info.height >= 0) - cls.assert_true(info.height_without_bootstrap > 0) - cls.assert_true(info.num_incoming_connections >= 0) - cls.assert_not_none(info.network_type) - cls.assert_not_none(info.is_offline) - cls.assert_true(info.num_outgoing_connections >= 0) - cls.assert_true(info.num_rpc_connections >= 0) - cls.assert_true(info.start_timestamp > 0) - cls.assert_true(info.adjusted_timestamp > 0) - cls.assert_true(info.target > 0) - cls.assert_true(info.target_height >= 0) - cls.assert_true(info.num_txs >= 0) - cls.assert_true(info.num_txs_pool >= 0) - cls.assert_not_none(info.was_bootstrap_ever_used) - cls.assert_true(info.block_weight_limit > 0) - cls.assert_true(info.block_weight_median > 0) - cls.assert_true(info.database_size > 0) - cls.assert_not_none(info.update_available) - cls.test_unsigned_big_integer(info.credits, False) # 0 credits - cls.assert_false(cls.is_empty(info.top_block_hash)) - cls.assert_not_none(info.is_busy_syncing) - cls.assert_not_none(info.is_synchronized) - - @classmethod - def test_sync_info(cls, syncInfo: MoneroDaemonSyncInfo): - cls.assert_true(isinstance(syncInfo, MoneroDaemonSyncInfo)) - assert syncInfo.height is not None - cls.assert_true(syncInfo.height >= 0) - if syncInfo.peers is not None: - cls.assert_true(len(syncInfo.peers) > 0) - for connection in syncInfo.peers: - cls.test_peer(connection) - - # TODO: test that this is being hit, so far not used - if (syncInfo.spans is None): - cls.assert_true(len(syncInfo.spans) > 0) - for span in syncInfo.spans: - testConnectionSpan(span) - - assert syncInfo.next_needed_pruning_seed is not None - cls.assert_true(syncInfo.next_needed_pruning_seed >= 0) - cls.assert_is_none(syncInfo.overview) - cls.test_unsigned_big_integer(syncInfo.credits, False) # 0 credits - cls.assert_is_none(syncInfo.top_block_hash) - - @classmethod - def test_hard_fork_info(cls, hardForkInfo: MoneroHardForkInfo): - cls.assert_not_none(hardForkInfo.earliest_height) - cls.assert_not_none(hardForkInfo.is_enabled) - cls.assert_not_none(hardForkInfo.state) - cls.assert_not_none(hardForkInfo.threshold) - cls.assert_not_none(hardForkInfo.version) - cls.assert_not_none(hardForkInfo.num_votes) - cls.assert_not_none(hardForkInfo.voting) - cls.assert_not_none(hardForkInfo.window) - cls.test_unsigned_big_integer(hardForkInfo.credits, False) # 0 credits - cls.assert_is_none(hardForkInfo.top_block_hash) - - @classmethod - def test_alt_chain(cls, alt_chain: MoneroAltChain): - cls.assert_not_none(alt_chain) - cls.assert_false(len(alt_chain.block_hashes) == 0) - cls.test_unsigned_big_integer(alt_chain.difficulty, True) - assert alt_chain.height is not None - assert alt_chain.length is not None - assert alt_chain.main_chain_parent_block_hash is not None - cls.assert_true(alt_chain.height > 0) - cls.assert_true(alt_chain.length > 0) - cls.assert_equals(64, len(alt_chain.main_chain_parent_block_hash)) - - @classmethod - def get_unrelayed_tx(cls, wallet: MoneroWallet, accountIdx: int): - assert accountIdx > 0, "Txs sent from/to same account are not properly synced from the pool" # TODO monero-project - config = MoneroTxConfig() - config.account_index = accountIdx - config.address = wallet.get_primary_address() - config.amount = cls.MAX_FEE - - tx = wallet.create_tx(config) - assert (tx.full_hex is None or tx.full_hex == "") is False - assert tx.relay is False - return tx - - @classmethod - def test_tx_pool_stats(cls, stats: MoneroTxPoolStats): - cls.assert_not_none(stats) - assert stats.num_txs is not None - cls.assert_true(stats.num_txs >= 0) - if stats.num_txs > 0: - #if (stats.num_txs == 1): - # cls.assert_is_none(stats.histo) - #else: - # histo: dict[int, int] = stats.histo - # cls.assert_not_none(histo) - # cls.assert_true(len(histo) > 0) - #for (Long key : histo.keySet()) { - # cls.assert_true(histo.get(key) >= 0) - - assert stats.bytes_max is not None - assert stats.bytes_med is not None - assert stats.bytes_min is not None - assert stats.bytes_total is not None - assert stats.oldest_timestamp is not None - assert stats.num10m is not None - assert stats.num_double_spends is not None - assert stats.num_failing is not None - assert stats.num_not_relayed is not None - - cls.assert_true(stats.bytes_max > 0) - cls.assert_true(stats.bytes_med > 0) - cls.assert_true(stats.bytes_min > 0) - cls.assert_true(stats.bytes_total > 0) - cls.assert_true(stats.histo98pc is None or stats.histo98pc > 0) - cls.assert_true(stats.oldest_timestamp > 0) - cls.assert_true(stats.num10m >= 0) - cls.assert_true(stats.num_double_spends >= 0) - cls.assert_true(stats.num_failing >= 0) - cls.assert_true(stats.num_not_relayed >= 0) - - else: - cls.assert_is_none(stats.bytes_max) - cls.assert_is_none(stats.bytes_med) - cls.assert_is_none(stats.bytes_min) - cls.assert_equals(0, stats.bytes_total) - cls.assert_is_none(stats.histo98pc) - cls.assert_is_none(stats.oldest_timestamp) - cls.assert_equals(0, stats.num10m) - cls.assert_equals(0, stats.num_double_spends) - cls.assert_equals(0, stats.num_failing) - cls.assert_equals(0, stats.num_not_relayed) - #cls.assert_is_none(stats.histo) - - @classmethod - def get_external_wallet_address(cls) -> str: - networkType: MoneroNetworkType | None = cls.get_daemon_rpc().get_info().network_type - - if networkType == MoneroNetworkType.STAGENET: - return "78Zq71rS1qK4CnGt8utvMdWhVNMJexGVEDM2XsSkBaGV9bDSnRFFhWrQTbmCACqzevE8vth9qhWfQ9SUENXXbLnmMVnBwgW" # subaddress - if networkType == MoneroNetworkType.TESTNET: - return "BhsbVvqW4Wajf4a76QW3hA2B3easR5QdNE5L8NwkY7RWXCrfSuaUwj1DDUsk3XiRGHBqqsK3NPvsATwcmNNPUQQ4SRR2b3V" # subaddress - if networkType == MoneroNetworkType.MAINNET: - return "87a1Yf47UqyQFCrMqqtxfvhJN9se3PgbmU7KUFWqhSu5aih6YsZYoxfjgyxAM1DztNNSdoYTZYn9xa3vHeJjoZqdAybnLzN" # subaddress - else: - raise Exception("Invalid network type: " + str(networkType)) - - @classmethod - def get_and_test_txs(cls, wallet: MoneroWallet, a, b, c: bool) -> list[MoneroTxWallet]: - raise NotImplementedError() - - @classmethod - def get_random_transactions(cls, wallet: MoneroWallet, query: Optional[MoneroTxQuery] = None, min_txs: Optional[int] = None, max_txs: Optional[int] = None) -> list[MoneroTxWallet]: - txs = wallet.get_txs(query if query is not None else MoneroTxQuery()) - - if min_txs is not None: - assert len(txs) >= min_txs, f"{len(txs)}/{min_txs} transactions found with the query" - - shuffle(txs) - - if max_txs is None: - return txs - - result: list[MoneroTxWallet] = [] - i = 0 - - for tx in txs: - result.append(tx) - if i >= max_txs - 1: - break - i += 1 - - return result - - @classmethod - def test_tx_wallet(cls, tx: MoneroTxWallet, ctx: TxContext) -> None: - raise NotImplementedError() - \ No newline at end of file + + return config + + @classmethod + def get_wallet_full(cls) -> MoneroWalletFull: + if cls._WALLET_FULL is None: + # create wallet from seed if it doesn't exist + if not MoneroWalletFull.wallet_exists(cls.WALLET_FULL_PATH): + # create directory for test wallets if it doesn't exist + cls.initialize_test_wallet_dir() + + # create wallet with connection + daemon_connection = MoneroRpcConnection(cls.DAEMON_RPC_URI, cls.DAEMON_RPC_USERNAME, cls.DAEMON_RPC_PASSWORD) + config = cls.get_wallet_full_config(daemon_connection) + cls._WALLET_FULL = MoneroWalletFull.create_wallet(config) + assert cls.FIRST_RECEIVE_HEIGHT, cls._WALLET_FULL.get_restore_height() + assert daemon_connection == cls._WALLET_FULL.get_daemon_connection() + + # otherwise open existing wallet and update daemon connection + else: + cls._WALLET_FULL = MoneroWalletFull.open_wallet(cls.WALLET_FULL_PATH, cls.WALLET_PASSWORD, cls.NETWORK_TYPE) + cls._WALLET_FULL.set_daemon_connection(cls.get_daemon_rpc().get_rpc_connection()) + + # sync and save wallet + listener = WalletSyncPrinter() + cls._WALLET_FULL.sync(listener) + cls._WALLET_FULL.save() + cls._WALLET_FULL.start_syncing(cls.SYNC_PERIOD_IN_MS) # start background synchronizing with sync period + + # ensure we're testing the right wallet + assert cls.SEED == cls._WALLET_FULL.get_seed() + assert cls.ADDRESS == cls._WALLET_FULL.get_primary_address() + return cls._WALLET_FULL + + @classmethod + def get_wallet_rpc(cls) -> MoneroWalletRpc: + if cls._WALLET_RPC is None: + + # construct wallet rpc instance with daemon connection + rpc = MoneroRpcConnection(cls.WALLET_RPC_URI, cls.WALLET_RPC_USERNAME, cls.WALLET_RPC_PASSWORD, cls.WALLET_RPC_ZMQ_URI if cls.WALLET_RPC_ZMQ_ENABLED else '') + cls._WALLET_RPC = MoneroWalletRpc(rpc) + + # attempt to open test wallet + try: + cls._WALLET_RPC.open_wallet(cls.WALLET_NAME, cls.WALLET_PASSWORD) + except MoneroRpcError as e: + # -1 returned when wallet does not exist or fails to open e.g. it's already open by another application + if (e.get_code() == -1): + # create wallet + config = MoneroWalletConfig() + config.path = cls.WALLET_NAME + config.password = cls.WALLET_PASSWORD + config.seed = cls.SEED + config.restore_height = cls.FIRST_RECEIVE_HEIGHT + cls._WALLET_RPC.create_wallet(config) + else: + raise e + + # ensure we're testing the right wallet + assert cls.SEED == cls._WALLET_RPC.get_seed() + assert cls.ADDRESS == cls._WALLET_RPC.get_primary_address() + + # sync and save wallet + cls._WALLET_RPC.sync() + cls._WALLET_RPC.save() + cls._WALLET_RPC.start_syncing(cls.SYNC_PERIOD_IN_MS) + + # return cached wallet rpc + return cls._WALLET_RPC + + @classmethod + def create_wallet_ground_truth(cls, network_type: MoneroNetworkType, seed: str, start_height: int | None, restoreHeight: int | None) -> MoneroWalletFull: + # create directory for test wallets if it doesn't exist + if not cls.check_test_wallets_dir_exists(): + cls.create_test_wallets_dir() + + # create ground truth wallet + daemon_connection = MoneroRpcConnection(cls.DAEMON_RPC_URI, cls.DAEMON_RPC_USERNAME, cls.DAEMON_RPC_PASSWORD) + path = cls.TEST_WALLETS_DIR + "/gt_wallet_" + cls.current_timestamp_str() + config = MoneroWalletConfig() + config.path = path + config.password = cls.WALLET_PASSWORD + config.network_type = network_type + config.seed = seed + config.server = daemon_connection + config.restore_height = restoreHeight + + if start_height is None: + start_height = 0 + + gt_wallet = MoneroWalletFull.create_wallet(config) + cls.assert_equals(restoreHeight, gt_wallet.get_restore_height()) + gt_wallet.sync(start_height, WalletSyncPrinter()) + gt_wallet.start_syncing(cls.SYNC_PERIOD_IN_MS) + + # close the full wallet when the runtime is shutting down to release resources + + return gt_wallet + + @classmethod + def test_invalid_address(cls, address: Optional[str], network_type: MoneroNetworkType) -> None: + if address is None: + return + + cls.assert_false(MoneroUtils.is_valid_address(address, network_type)) + + try: + MoneroUtils.validate_address(address, network_type) + raise Exception("Should have thrown exception") + except Exception as e: + cls.assert_false(len(str(e)) == 0) + + @classmethod + def test_invalid_private_view_key(cls, private_view_key: Optional[str]): + if private_view_key is None: + return + + cls.assert_false(MoneroUtils.is_valid_private_view_key(private_view_key)) + + try: + MoneroUtils.validate_private_view_key(private_view_key) + raise Exception("Should have thrown exception") + except Exception as e: + cls.assert_false(len(str(e)) == 0) + + @classmethod + def test_invalid_public_view_key(cls, public_view_key: Optional[str]) -> None: + if public_view_key is None: + return + + cls.assert_false(MoneroUtils.is_valid_public_view_key(public_view_key)) + + try: + MoneroUtils.validate_public_view_key(public_view_key) + raise Exception("Should have thrown exception") + except Exception as e: + cls.assert_false(len(str(e)) == 0) + + @classmethod + def test_invalid_private_spend_key(cls, private_spend_key: Optional[str]): + if private_spend_key is None: + return + + cls.assert_false(MoneroUtils.is_valid_private_spend_key(private_spend_key)) + + try: + MoneroUtils.validate_private_spend_key(private_spend_key) + raise Exception("Should have thrown exception") + except Exception as e: + cls.assert_false(len(str(e)) == 0) + + @classmethod + def test_invalid_public_spend_key(cls, public_spend_key: Optional[str]): + if public_spend_key is None: + return + + cls.assert_false(MoneroUtils.is_valid_public_spend_key(public_spend_key)) + try: + MoneroUtils.validate_public_spend_key(public_spend_key) + raise Exception("Should have thrown exception") + except Exception as e: + cls.assert_false(len(str(e)) == 0) + + @classmethod + def test_block_template(cls, template: MoneroBlockTemplate): + cls.assert_not_none(template) + cls.assert_not_none(template.block_template_blob) + cls.assert_not_none(template.block_hashing_blob) + cls.assert_not_none(template.difficulty) + cls.assert_not_none(template.expected_reward) + cls.assert_not_none(template.height) + cls.assert_not_none(template.prev_hash) + cls.assert_not_none(template.reserved_offset) + cls.assert_not_none(template.seed_height) + assert template.seed_height is not None + cls.assert_true(template.seed_height > 0) + cls.assert_not_none(template.seed_hash) + cls.assert_false(template.seed_hash == "") + # next seed hash can be null or initialized TODO: test circumstances for each + + @classmethod + def test_block_header(cls, header: MoneroBlockHeader, is_full: bool): + cls.assert_not_none(header) + assert header.height is not None + cls.assert_true(header.height >= 0) + assert header.major_version is not None + cls.assert_true(header.major_version > 0) + assert header.minor_version is not None + cls.assert_true(header.minor_version >= 0) + assert header.timestamp is not None + if (header.height == 0): + cls.assert_true(header.timestamp == 0) + else: + cls.assert_true(header.timestamp > 0) + cls.assert_not_none(header.prev_hash) + cls.assert_not_none(header.nonce) + if (header.nonce == 0): + print(f"WARNING: header nonce is 0 at height {header.height}") # TODO (monero-project): why is header nonce 0? + else: + assert header.nonce is not None + cls.assert_true(header.nonce > 0) + + cls.assert_is_none(header.pow_hash) # never seen defined + + if (is_full): + assert header.size is not None + assert header.depth is not None + assert header.difficulty is not None + assert header.cumulative_difficulty is not None + assert header.hash is not None + assert header.miner_tx_hash is not None + assert header.num_txs is not None + assert header.weight is not None + cls.assert_true(header.size > 0) + cls.assert_true(header.depth >= 0) + cls.assert_true(header.difficulty > 0) + cls.assert_true(header.cumulative_difficulty > 0) + cls.assert_equals(64, len(header.hash)) + cls.assert_equals(64, len(header.miner_tx_hash)) + cls.assert_true(header.num_txs >= 0) + cls.assert_not_none(header.orphan_status) + cls.assert_not_none(header.reward) + cls.assert_not_none(header.weight) + cls.assert_true(header.weight > 0) + else: + cls.assert_is_none(header.size) + cls.assert_is_none(header.depth) + cls.assert_is_none(header.difficulty) + cls.assert_is_none(header.cumulative_difficulty) + cls.assert_is_none(header.hash) + cls.assert_is_none(header.miner_tx_hash) + cls.assert_is_none(header.num_txs) + cls.assert_is_none(header.orphan_status) + cls.assert_is_none(header.reward) + cls.assert_is_none(header.weight) + + @classmethod + def test_miner_tx(cls, miner_tx: MoneroTx): + assert miner_tx is not None + cls.assert_not_none(miner_tx.is_miner_tx) + assert miner_tx.version is not None + cls.assert_true(miner_tx.version >= 0) + cls.assert_not_none(miner_tx.extra) + cls.assert_true(len(miner_tx.extra) > 0) + assert miner_tx.unlock_time is not None + cls.assert_true(miner_tx.unlock_time >= 0) + + # TODO: miner tx does not have hashes in binary requests so this will fail, need to derive using prunable data + # ctx = new TestContext() + # ctx.has_json = false + # ctx.is_pruned = true + # ctx.is_full = false + # ctx.is_confirmed = true + # ctx.is_miner = true + # ctx.from_get_tx_pool = true + # cls.test_tx(miner_tx, ctx) + + @classmethod + def test_tx(cls, tx: MoneroTx, ctx: Optional[TestContext]) -> None: + raise NotImplementedError() + + # TODO: test block deep copy + + @classmethod + def test_block(cls, block: MoneroBlock, ctx: TestContext): + # test required fields + cls.assert_not_none(block) + assert block.miner_tx is not None + cls.test_miner_tx(block.miner_tx) # TODO: miner tx doesn't have as much stuff, can't call testTx? + cls.test_block_header(block, ctx.header_is_full) + + if (ctx.has_hex): + assert block.hex is not None + cls.assert_true(len(block.hex) > 1) + else: + cls.assert_is_none(block.hex) + + if (ctx.has_txs): + cls.assert_not_none(ctx.tx_context) + for tx in block.txs: + cls.assert_true(block == tx.block) + cls.test_tx(tx, ctx.tx_context) + + else: + cls.assert_is_none(ctx.tx_context) + cls.assert_is_none(block.txs) + + @classmethod + def is_empty(cls, value: Union[str, list[Any], None]) -> bool: + return value == "" + + @classmethod + def test_update_check_result(cls, result: Union[Any, MoneroDaemonUpdateCheckResult]): + assert result is not None + cls.assert_true(isinstance(result, MoneroDaemonUpdateCheckResult)) + cls.assert_not_none(result.is_update_available) + if (result.is_update_available): + cls.assert_false(cls.is_empty(result.auto_uri), "No auto uri is daemon online?") + cls.assert_false(cls.is_empty(result.user_uri)) + cls.assert_false(cls.is_empty(result.version)) + cls.assert_false(cls.is_empty(result.hash)) + assert result.hash is not None + cls.assert_equals(64, len(result.hash)) + else: + cls.assert_is_none(result.auto_uri) + cls.assert_is_none(result.user_uri) + cls.assert_is_none(result.version) + cls.assert_is_none(result.hash) + + @classmethod + def test_update_download_result(cls, result: MoneroDaemonUpdateDownloadResult, path: Optional[str]): + cls.test_update_check_result(result) + if (result.is_update_available): + if path is not None: + cls.assert_equals(path, result.download_path) + else: + cls.assert_not_none(result.download_path) + else: + cls.assert_is_none(result.download_path) + + @classmethod + def test_unsigned_big_integer(cls, value: Any, boolVal: bool = False): + if not isinstance(value, int): + raise Exception("Value is not number") + + if value < 0: + raise Exception("Value cannot be negative") + + @classmethod + def test_account(cls, account: Optional[MoneroAccount]): + # test account + assert account is not None + assert account.index is not None + assert account.index >= 0 + assert account.primary_address is not None + + MoneroUtils.validate_address(account.primary_address, cls.NETWORK_TYPE) + cls.test_unsigned_big_integer(account.balance) + cls.test_unsigned_big_integer(account.unlocked_balance) + + # if given, test subaddresses and that their balances add up to account balances + if account.subaddresses is not None: + balance = 0 + unlocked_balance = 0 + i = 0 + j = len(account.subaddresses) + while i < j: + cls.test_subaddress(account.subaddresses[i]) + assert account.index == account.subaddresses[i].account_index + assert i == account.subaddresses[i].index + address_balance = account.subaddresses[i].balance + assert address_balance is not None + balance += address_balance + address_balance = account.subaddresses[i].unlocked_balance + assert address_balance is not None + unlocked_balance += address_balance + + assert account.balance == balance, "Subaddress balances " + str(balance) + " != account " + str(account.index) + " balance " + str(account.balance) + assert account.unlocked_balance == unlocked_balance, "Subaddress unlocked balances " + str(unlocked_balance) + " != account " + str(account.index) + " unlocked balance " + str(account.unlocked_balance) + + # tag must be undefined or non-empty + tag = account.tag + assert tag is None or len(tag) > 0 + + @classmethod + def test_subaddress(cls, subaddress: MoneroSubaddress): + assert subaddress.account_index is not None + assert subaddress.index is not None + assert subaddress.balance is not None + assert subaddress.num_unspent_outputs is not None + assert subaddress.num_blocks_to_unlock is not None + + cls.assert_true(subaddress.account_index >= 0) + cls.assert_true(subaddress.index >= 0) + cls.assert_not_none(subaddress.address) + cls.assert_true(subaddress.label is None or subaddress.label != "") + cls.test_unsigned_big_integer(subaddress.balance) + cls.test_unsigned_big_integer(subaddress.unlocked_balance) + cls.assert_true(subaddress.num_unspent_outputs >= 0) + cls.assert_not_none(subaddress.is_used) + if subaddress.balance > 0: + cls.assert_true(subaddress.is_used) + cls.assert_true(subaddress.num_blocks_to_unlock >= 0) + + @classmethod + def assert_subaddress_equal(cls, subaddress: Optional[MoneroSubaddress], other: Optional[MoneroSubaddress]): + if subaddress is None and other is None: + return + assert not (subaddress is None or other is None) + assert subaddress.address == other.address + assert subaddress.account_index == other.account_index + assert subaddress.balance == other.balance + assert subaddress.index == other.index + assert subaddress.is_used == other.is_used + assert subaddress.label == other.label + assert subaddress.num_blocks_to_unlock == other.num_blocks_to_unlock + assert subaddress.num_unspent_outputs == other.num_unspent_outputs + assert subaddress.unlocked_balance == other.unlocked_balance + + @classmethod + def assert_subaddresses_equal(cls, subaddresses1: list[MoneroSubaddress], subaddresses2: list[MoneroSubaddress]): + size1 = len(subaddresses1) + size2 = len(subaddresses2) + if size1 != size2: + raise Exception("Number of subaddresses doens't match") + + i = 0 + + while i < size1: + cls.assert_subaddress_equal(subaddresses1[i], subaddresses2[i]) + i += 1 + + @classmethod + def test_known_peer(cls, peer: Optional[MoneroPeer], from_connection: bool): + assert peer is not None, "Peer is null" + assert peer.id is not None + assert peer.host is not None + assert peer.port is not None + cls.assert_false(len(peer.id) == 0) + cls.assert_false(len(peer.host) == 0) + cls.assert_true(peer.port > 0) + cls.assert_true(peer.rpc_port is None or peer.rpc_port >= 0) + cls.assert_not_none(peer.is_online) + if peer.rpc_credits_per_hash is not None: + cls.test_unsigned_big_integer(peer.rpc_credits_per_hash) + if (from_connection): + cls.assert_is_none(peer.last_seen_timestamp) + else: + assert peer.last_seen_timestamp is not None + + if (peer.last_seen_timestamp < 0): + print(f"Last seen timestamp is invalid: {peer.last_seen_timestamp}") + cls.assert_true(peer.last_seen_timestamp >= 0) + + cls.assert_true(peer.pruning_seed is None or peer.pruning_seed >= 0) + + @classmethod + def test_peer(cls, peer: Union[Any, MoneroPeer]): + cls.assert_true(isinstance(peer, MoneroPeer)) + cls.test_known_peer(peer, True) + assert peer.hash is not None + assert peer.avg_download is not None + assert peer.avg_upload is not None + assert peer.current_download is not None + assert peer.current_upload is not None + assert peer.height is not None + assert peer.live_time is not None + assert peer.num_receives is not None + assert peer.receive_idle_time is not None + assert peer.num_sends is not None + assert peer.send_idle_time is not None + assert peer.num_support_flags is not None + + cls.assert_false(len(peer.hash) == 0) + cls.assert_true(peer.avg_download >= 0) + cls.assert_true(peer.avg_upload >= 0) + cls.assert_true(peer.current_download >= 0) + cls.assert_true(peer.current_upload >= 0) + cls.assert_true(peer.height >= 0) + cls.assert_true(peer.live_time >= 0) + cls.assert_not_none(peer.is_local_ip) + cls.assert_not_none(peer.is_local_host) + cls.assert_true(peer.num_receives >= 0) + cls.assert_true(peer.receive_idle_time >= 0) + cls.assert_true(peer.num_sends >= 0) + cls.assert_true(peer.send_idle_time >= 0) + cls.assert_not_none(peer.state) + cls.assert_true(peer.num_support_flags >= 0) + cls.assert_not_none(peer.connection_type) + + @classmethod + def test_info(cls, info: MoneroDaemonInfo): + assert info.num_alt_blocks is not None + assert info.block_size_limit is not None + assert info.block_size_median is not None + assert info.num_offline_peers is not None + assert info.num_online_peers is not None + assert info.height is not None + assert info.height_without_bootstrap is not None + assert info.num_incoming_connections is not None + assert info.num_outgoing_connections is not None + assert info.num_rpc_connections is not None + assert info.start_timestamp is not None + assert info.adjusted_timestamp is not None + assert info.target is not None + assert info.target_height is not None + assert info.num_txs is not None + assert info.num_txs_pool is not None + assert info.block_weight_limit is not None + assert info.block_weight_median is not None + assert info.database_size is not None + + cls.assert_not_none(info.version) + cls.assert_true(info.num_alt_blocks >= 0) + cls.assert_true(info.block_size_limit > 0) + cls.assert_true(info.block_size_median > 0) + cls.assert_true(info.bootstrap_daemon_address is None or not cls.is_empty(info.bootstrap_daemon_address)) + cls.test_unsigned_big_integer(info.cumulative_difficulty) + cls.test_unsigned_big_integer(info.free_space) + cls.assert_true(info.num_offline_peers >= 0) + cls.assert_true(info.num_online_peers >= 0) + cls.assert_true(info.height >= 0) + cls.assert_true(info.height_without_bootstrap > 0) + cls.assert_true(info.num_incoming_connections >= 0) + cls.assert_not_none(info.network_type) + cls.assert_not_none(info.is_offline) + cls.assert_true(info.num_outgoing_connections >= 0) + cls.assert_true(info.num_rpc_connections >= 0) + cls.assert_true(info.start_timestamp > 0) + cls.assert_true(info.adjusted_timestamp > 0) + cls.assert_true(info.target > 0) + cls.assert_true(info.target_height >= 0) + cls.assert_true(info.num_txs >= 0) + cls.assert_true(info.num_txs_pool >= 0) + cls.assert_not_none(info.was_bootstrap_ever_used) + cls.assert_true(info.block_weight_limit > 0) + cls.assert_true(info.block_weight_median > 0) + cls.assert_true(info.database_size > 0) + cls.assert_not_none(info.update_available) + cls.test_unsigned_big_integer(info.credits, False) # 0 credits + cls.assert_false(cls.is_empty(info.top_block_hash)) + cls.assert_not_none(info.is_busy_syncing) + cls.assert_not_none(info.is_synchronized) + + @classmethod + def test_sync_info(cls, sync_info: Union[Any, MoneroDaemonSyncInfo]): + cls.assert_true(isinstance(sync_info, MoneroDaemonSyncInfo)) + assert sync_info.height is not None + cls.assert_true(sync_info.height >= 0) + if sync_info.peers is not None: + cls.assert_true(len(sync_info.peers) > 0) + for connection in sync_info.peers: + cls.test_peer(connection) + + # TODO: test that this is being hit, so far not used + if (sync_info.spans is None): + cls.assert_true(len(sync_info.spans) > 0) + for span in sync_info.spans: + cls.test_connection_span(span) + + assert sync_info.next_needed_pruning_seed is not None + cls.assert_true(sync_info.next_needed_pruning_seed >= 0) + cls.assert_is_none(sync_info.overview) + cls.test_unsigned_big_integer(sync_info.credits, False) # 0 credits + cls.assert_is_none(sync_info.top_block_hash) + + @classmethod + def test_connection_span(cls, span: Union[MoneroConnectionSpan, Any]) -> None: + raise NotImplementedError() + + @classmethod + def test_hard_fork_info(cls, hard_fork_info: MoneroHardForkInfo): + cls.assert_not_none(hard_fork_info.earliest_height) + cls.assert_not_none(hard_fork_info.is_enabled) + cls.assert_not_none(hard_fork_info.state) + cls.assert_not_none(hard_fork_info.threshold) + cls.assert_not_none(hard_fork_info.version) + cls.assert_not_none(hard_fork_info.num_votes) + cls.assert_not_none(hard_fork_info.voting) + cls.assert_not_none(hard_fork_info.window) + cls.test_unsigned_big_integer(hard_fork_info.credits, False) # 0 credits + cls.assert_is_none(hard_fork_info.top_block_hash) + + @classmethod + def test_alt_chain(cls, alt_chain: MoneroAltChain): + cls.assert_not_none(alt_chain) + cls.assert_false(len(alt_chain.block_hashes) == 0) + cls.test_unsigned_big_integer(alt_chain.difficulty, True) + assert alt_chain.height is not None + assert alt_chain.length is not None + assert alt_chain.main_chain_parent_block_hash is not None + cls.assert_true(alt_chain.height > 0) + cls.assert_true(alt_chain.length > 0) + cls.assert_equals(64, len(alt_chain.main_chain_parent_block_hash)) + + @classmethod + def get_unrelayed_tx(cls, wallet: MoneroWallet, account_idx: int): + assert account_idx > 0, "Txs sent from/to same account are not properly synced from the pool" # TODO monero-project + config = MoneroTxConfig() + config.account_index = account_idx + config.address = wallet.get_primary_address() + config.amount = cls.MAX_FEE + + tx = wallet.create_tx(config) + assert (tx.full_hex is None or tx.full_hex == "") is False + assert tx.relay is False + return tx + + @classmethod + def test_tx_pool_stats(cls, stats: MoneroTxPoolStats): + cls.assert_not_none(stats) + assert stats.num_txs is not None + cls.assert_true(stats.num_txs >= 0) + if stats.num_txs > 0: + #if (stats.num_txs == 1): + # cls.assert_is_none(stats.histo) + #else: + # histo: dict[int, int] = stats.histo + # cls.assert_not_none(histo) + # cls.assert_true(len(histo) > 0) + #for (Long key : histo.keySet()) { + # cls.assert_true(histo.get(key) >= 0) + + assert stats.bytes_max is not None + assert stats.bytes_med is not None + assert stats.bytes_min is not None + assert stats.bytes_total is not None + assert stats.oldest_timestamp is not None + assert stats.num10m is not None + assert stats.num_double_spends is not None + assert stats.num_failing is not None + assert stats.num_not_relayed is not None + + cls.assert_true(stats.bytes_max > 0) + cls.assert_true(stats.bytes_med > 0) + cls.assert_true(stats.bytes_min > 0) + cls.assert_true(stats.bytes_total > 0) + cls.assert_true(stats.histo98pc is None or stats.histo98pc > 0) + cls.assert_true(stats.oldest_timestamp > 0) + cls.assert_true(stats.num10m >= 0) + cls.assert_true(stats.num_double_spends >= 0) + cls.assert_true(stats.num_failing >= 0) + cls.assert_true(stats.num_not_relayed >= 0) + + else: + cls.assert_is_none(stats.bytes_max) + cls.assert_is_none(stats.bytes_med) + cls.assert_is_none(stats.bytes_min) + cls.assert_equals(0, stats.bytes_total) + cls.assert_is_none(stats.histo98pc) + cls.assert_is_none(stats.oldest_timestamp) + cls.assert_equals(0, stats.num10m) + cls.assert_equals(0, stats.num_double_spends) + cls.assert_equals(0, stats.num_failing) + cls.assert_equals(0, stats.num_not_relayed) + #cls.assert_is_none(stats.histo) + + @classmethod + def get_external_wallet_address(cls) -> str: + network_type: MoneroNetworkType | None = cls.get_daemon_rpc().get_info().network_type + + if network_type == MoneroNetworkType.STAGENET: + return "78Zq71rS1qK4CnGt8utvMdWhVNMJexGVEDM2XsSkBaGV9bDSnRFFhWrQTbmCACqzevE8vth9qhWfQ9SUENXXbLnmMVnBwgW" # subaddress + if network_type == MoneroNetworkType.TESTNET: + return "BhsbVvqW4Wajf4a76QW3hA2B3easR5QdNE5L8NwkY7RWXCrfSuaUwj1DDUsk3XiRGHBqqsK3NPvsATwcmNNPUQQ4SRR2b3V" # subaddress + if network_type == MoneroNetworkType.MAINNET: + return "87a1Yf47UqyQFCrMqqtxfvhJN9se3PgbmU7KUFWqhSu5aih6YsZYoxfjgyxAM1DztNNSdoYTZYn9xa3vHeJjoZqdAybnLzN" # subaddress + else: + raise Exception("Invalid network type: " + str(network_type)) + + @classmethod + def get_and_test_txs(cls, wallet: MoneroWallet, a, b, c: bool) -> list[MoneroTxWallet]: + raise NotImplementedError() + + @classmethod + def get_random_transactions(cls, wallet: MoneroWallet, query: Optional[MoneroTxQuery] = None, min_txs: Optional[int] = None, max_txs: Optional[int] = None) -> list[MoneroTxWallet]: + txs = wallet.get_txs(query if query is not None else MoneroTxQuery()) + + if min_txs is not None: + assert len(txs) >= min_txs, f"{len(txs)}/{min_txs} transactions found with the query" + + shuffle(txs) + + if max_txs is None: + return txs + + result: list[MoneroTxWallet] = [] + i = 0 + + for tx in txs: + result.append(tx) + if i >= max_txs - 1: + break + i += 1 + + return result + + @classmethod + def test_tx_wallet(cls, tx: MoneroTxWallet, ctx: TxContext) -> None: + raise NotImplementedError() diff --git a/tests/utils/print_height.py b/tests/utils/print_height.py index 6f36f9f..a2f3c41 100644 --- a/tests/utils/print_height.py +++ b/tests/utils/print_height.py @@ -5,8 +5,7 @@ class PrintHeight(ABC): - @classmethod - def print(cls) -> None: - daemon = MoneroTestUtils.get_daemon_rpc() - print(f"Height: {daemon.get_height()}") - + @classmethod + def print(cls) -> None: + daemon = MoneroTestUtils.get_daemon_rpc() + print(f"Height: {daemon.get_height()}") diff --git a/tests/utils/sample_connection_listener.py b/tests/utils/sample_connection_listener.py index 6180288..c4f18de 100644 --- a/tests/utils/sample_connection_listener.py +++ b/tests/utils/sample_connection_listener.py @@ -1,13 +1,13 @@ - +from typing import Optional from typing_extensions import override from monero import MoneroConnectionManagerListener, MoneroRpcConnection + class SampleConnectionListener(MoneroConnectionManagerListener): - def __init__(self) -> None: - MoneroConnectionManagerListener.__init__(self) + def __init__(self) -> None: + MoneroConnectionManagerListener.__init__(self) - @override - def on_connection_changed(self, connection: MoneroRpcConnection) -> None: - print(f"Connection changed to: {connection.uri if connection is not None else 'None'}") - \ No newline at end of file + @override + def on_connection_changed(self, connection: Optional[MoneroRpcConnection]) -> None: + print(f"Connection changed to: {connection.uri if connection is not None else 'None'}") diff --git a/tests/utils/test_context.py b/tests/utils/test_context.py index 8a7ab18..66106c7 100644 --- a/tests/utils/test_context.py +++ b/tests/utils/test_context.py @@ -1,14 +1,14 @@ from __future__ import annotations -from typing import Any +from typing import Optional + class TestContext: - hasHex: bool = True - hasTxs: bool = False - headerIsFull: bool = True - txContext: TestContext - isPruned: bool = False - isConfirmed: bool = False - fromGetTxPool: bool = False - hasOutputIndices: bool = False - fromBinaryBlock: bool = False - \ No newline at end of file + has_hex: bool = True + has_txs: bool = False + header_is_full: bool = True + tx_context: Optional[TestContext] = None + is_pruned: bool = False + is_confirmed: bool = False + from_get_tx_pool: bool = False + has_output_indices: bool = False + from_binary_block: bool = False diff --git a/tests/utils/tx_context.py b/tests/utils/tx_context.py index 1d40b02..4d4a4da 100644 --- a/tests/utils/tx_context.py +++ b/tests/utils/tx_context.py @@ -6,26 +6,26 @@ class TxContext: - wallet: Optional[MoneroWallet] - config: Optional[MoneroTxConfig] - hasOutgoingTransfer: Optional[bool] - hasIncomingTransfers: Optional[bool] - hasDestinations: Optional[bool] - isCopy: Optional[bool] # indicates if a copy is being tested which means back references won't be the same - includeOutputs: Optional[bool] - isSendResponse: Optional[bool] - isSweepResponse: Optional[bool] - isSweepOutputResponse: Optional[bool] # TODO monero-wallet-rpc: this only necessary because sweep_output does not return account index - - def __init__(self, ctx: Optional[TxContext] = None) -> None: - if ctx is not None: - self.wallet = ctx.wallet - self.config = ctx.config - self.hasOutgoingTransfer = ctx.hasOutgoingTransfer - self.hasIncomingTransfers = ctx.hasIncomingTransfers - self.hasDestinations = ctx.hasDestinations - self.isCopy = ctx.isCopy - self.includeOutputs = ctx.includeOutputs - self.isSendResponse = ctx.isSendResponse - self.isSweepResponse = ctx.isSweepResponse - self.isSweepOutputResponse = ctx.isSweepOutputResponse + wallet: Optional[MoneroWallet] + config: Optional[MoneroTxConfig] + has_outgoing_transfer: Optional[bool] + has_incoming_transfers: Optional[bool] + has_destinations: Optional[bool] + is_copy: Optional[bool] # indicates if a copy is being tested which means back references won't be the same + include_outputs: Optional[bool] + is_send_response: Optional[bool] + is_sweep_response: Optional[bool] + is_sweep_output_response: Optional[bool] # TODO monero-wallet-rpc: this only necessary because sweep_output does not return account index + + def __init__(self, ctx: Optional[TxContext] = None) -> None: + if ctx is not None: + self.wallet = ctx.wallet + self.config = ctx.config + self.has_outgoing_transfer = ctx.has_outgoing_transfer + self.has_incoming_transfers = ctx.has_incoming_transfers + self.has_destinations = ctx.has_destinations + self.is_copy = ctx.is_copy + self.include_outputs = ctx.include_outputs + self.is_send_response = ctx.is_send_response + self.is_sweep_response = ctx.is_sweep_response + self.is_sweep_output_response = ctx.is_sweep_output_response diff --git a/tests/utils/wallet_equality_utils.py b/tests/utils/wallet_equality_utils.py index de3e1d1..ef8de45 100644 --- a/tests/utils/wallet_equality_utils.py +++ b/tests/utils/wallet_equality_utils.py @@ -1,143 +1,146 @@ from abc import ABC -from monero import MoneroWallet, MoneroTxQuery, MoneroDaemon, MoneroTransferQuery, MoneroOutputQuery, MoneroAccount, MoneroSubaddress, MoneroTxWallet, MoneroTransfer, MoneroOutputWallet +from monero import ( + MoneroWallet, MoneroTxQuery, MoneroDaemon, MoneroTransferQuery, + MoneroOutputQuery, MoneroAccount, MoneroSubaddress, + MoneroTxWallet, MoneroTransfer, MoneroOutputWallet +) from .wallet_tx_tracker import WalletTxTracker class WalletEqualityUtils(ABC): - @classmethod - def testWalletEqualityOnChain(cls, daemon: MoneroDaemon, sync_period_ms: int, WALLET_TX_TRACKER: WalletTxTracker, w1: MoneroWallet, w2: MoneroWallet) -> None: - WALLET_TX_TRACKER.reset() # all wallets need to wait for txs to confirm to reliably sync - - # wait for relayed txs associated with wallets to clear pool - assert w1.is_connected_to_daemon() == w2.is_connected_to_daemon() - if (w1.is_connected_to_daemon()): - WALLET_TX_TRACKER.wait_for_wallet_txs_to_clear_pool(daemon, sync_period_ms, [w1, w2]) - - # sync the wallets until same height - while (w1.get_height() != w2.get_height()): - w1.sync() - w2.sync() - - # test that wallets are equal using only on-chain data - assert w1.get_height() == w2.get_height() - assert w1.get_seed() == w2.get_seed() - assert w1.get_primary_address() == w2.get_primary_address() - assert w1.get_private_view_key() == w2.get_private_view_key() - assert w1.get_private_spend_key() == w2.get_private_spend_key() - - txQuery = MoneroTxQuery() - txQuery.is_confirmed = True - cls.testTxWalletsEqualOnChain(w1.get_txs(txQuery), w2.get_txs(txQuery)) - txQuery.include_outputs = True - cls.testTxWalletsEqualOnChain(w1.get_txs(txQuery), w2.get_txs(txQuery)) # fetch and compare outputs - cls.testAccountsEqualOnChain(w1.get_accounts(True), w2.get_accounts(True)) - assert w1.get_balance() == w2.get_balance() - assert w1.get_unlocked_balance() == w2.get_unlocked_balance() - transferQuery = MoneroTransferQuery() - transferQuery.tx_query = MoneroTxQuery() - transferQuery.tx_query.is_confirmed = True - cls.testTransfersEqualOnChain(w1.get_transfers(transferQuery), w2.get_transfers(transferQuery)) - outputQuery = MoneroOutputQuery() - outputQuery.tx_query = MoneroTxQuery() - outputQuery.tx_query.is_confirmed = True - cls.testOutputWalletsEqualOnChain(w1.get_outputs(outputQuery), w2.get_outputs(outputQuery)) - - - @classmethod - def testAccountsEqualOnChain(cls, accounts1: list[MoneroAccount], accounts2: list[MoneroAccount]) -> None: - accounts1_size = len(accounts1) - accounts2_size = len(accounts2) - size = accounts1_size if accounts1_size > accounts2_size else accounts2_size - i = 0 - - while i < size: - if (i < accounts1_size and i < accounts2_size): - cls.testAccountEqualOnChain(accounts1[i], accounts2[i]) - elif (i >= accounts1_size): - j = i - - while j < accounts2_size: - assert 0 == accounts2[j].balance - assert len(accounts2[j].subaddresses) >= 1 - for subaddress in accounts2[j].subaddresses: - assert subaddress.is_used == False - j += 1 - - return - else: - j = i - while j < accounts1_size: - assert 0 == accounts1[j].balance - assert len(accounts1[j].subaddresses) >= 1 - for subaddress in accounts1[j].subaddresses: - assert subaddress.is_used == False - j += 1 - - return - - @classmethod - def testAccountEqualOnChain(cls, account1: MoneroAccount, account2: MoneroAccount) -> None: - # nullify off-chain data for comparison - subaddresses1 = account1.subaddresses - subaddresses2 = account2.subaddresses - account1.subaddresses.clear() - account2.subaddresses.clear() - account1.tag = None - account2.tag = None - - # test account equality - assert account1 == account2 - cls.testSubaddressesEqualOnChain(subaddresses1, subaddresses2) - - @classmethod - def testSubaddressesEqualOnChain(cls, subaddresses1: list[MoneroSubaddress], subaddresses2: list[MoneroSubaddress]) -> None: - subaddresses1_len = len(subaddresses1) - subaddresses2_len = len(subaddresses2) - size = subaddresses1_len if subaddresses1_len > subaddresses2_len else subaddresses2_len - i = 0 - - while i < size: - if i < subaddresses1_len and i < subaddresses2_len: - cls.testSubaddressEqualOnChain(subaddresses1[i], subaddresses2[i]); - elif i >= subaddresses1_len: - j = i - while j < subaddresses2_len: - assert 0 == subaddresses2[j].balance - assert False == subaddresses2[j].is_used - j += 1 - - return - else: - j = i - while j < subaddresses1_len: - assert 0 == subaddresses1[i].balance - assert False == subaddresses1[j].is_used - - return - i += 1 - - @classmethod - def testSubaddressEqualOnChain(cls, subaddress1: MoneroSubaddress, subaddress2: MoneroSubaddress) -> None: - subaddress1.label = None # nullify off-chain data for comparison - subaddress2.label = None - assert subaddress1 == subaddress2 - - @classmethod - def testTxWalletsEqualOnChain(cls, txs1: list[MoneroTxWallet], txs2: list[MoneroTxWallet]) -> None: - raise NotImplementedError("test_tx_wallets_equal_on_chain(): not implemented") - - @classmethod - def transferCachedInfo(cls, src: MoneroTxWallet, tgt: MoneroTxWallet) -> None: - raise NotImplementedError("transfer_cached_info(): not implemented") - - @classmethod - def testTransfersEqualOnChain(cls, transfers1: list[MoneroTransfer], transfers2: list[MoneroTransfer]) -> None: - raise NotImplementedError("test_transfers_equal_on_chain(): not implemented") - - @classmethod - def testOutputWalletsEqualOnChain(cls, outputs1: list[MoneroOutputWallet], outputs2: list[MoneroOutputWallet]) -> None: - raise NotImplementedError("test_output_wallet_equals_on_chain(): not implemented") - \ No newline at end of file + @classmethod + def test_wallet_equality_on_chain(cls, daemon: MoneroDaemon, sync_period_ms: int, tx_tracker: WalletTxTracker, w1: MoneroWallet, w2: MoneroWallet) -> None: + tx_tracker.reset() # all wallets need to wait for txs to confirm to reliably sync + + # wait for relayed txs associated with wallets to clear pool + assert w1.is_connected_to_daemon() == w2.is_connected_to_daemon() + if (w1.is_connected_to_daemon()): + tx_tracker.wait_for_wallet_txs_to_clear_pool(daemon, sync_period_ms, [w1, w2]) + + # sync the wallets until same height + while (w1.get_height() != w2.get_height()): + w1.sync() + w2.sync() + + # test that wallets are equal using only on-chain data + assert w1.get_height() == w2.get_height() + assert w1.get_seed() == w2.get_seed() + assert w1.get_primary_address() == w2.get_primary_address() + assert w1.get_private_view_key() == w2.get_private_view_key() + assert w1.get_private_spend_key() == w2.get_private_spend_key() + + tx_query = MoneroTxQuery() + tx_query.is_confirmed = True + cls.testTxWalletsEqualOnChain(w1.get_txs(tx_query), w2.get_txs(tx_query)) + tx_query.include_outputs = True + cls.testTxWalletsEqualOnChain(w1.get_txs(tx_query), w2.get_txs(tx_query)) # fetch and compare outputs + cls.test_accounts_equal_on_chain(w1.get_accounts(True), w2.get_accounts(True)) + assert w1.get_balance() == w2.get_balance() + assert w1.get_unlocked_balance() == w2.get_unlocked_balance() + transfer_query = MoneroTransferQuery() + transfer_query.tx_query = MoneroTxQuery() + transfer_query.tx_query.is_confirmed = True + cls.testTransfersEqualOnChain(w1.get_transfers(transfer_query), w2.get_transfers(transfer_query)) + output_query = MoneroOutputQuery() + output_query.tx_query = MoneroTxQuery() + output_query.tx_query.is_confirmed = True + cls.testOutputWalletsEqualOnChain(w1.get_outputs(output_query), w2.get_outputs(output_query)) + + @classmethod + def test_accounts_equal_on_chain(cls, accounts1: list[MoneroAccount], accounts2: list[MoneroAccount]) -> None: + accounts1_size = len(accounts1) + accounts2_size = len(accounts2) + size = accounts1_size if accounts1_size > accounts2_size else accounts2_size + i = 0 + + while i < size: + if (i < accounts1_size and i < accounts2_size): + cls.test_account_equal_on_chain(accounts1[i], accounts2[i]) + elif (i >= accounts1_size): + j = i + + while j < accounts2_size: + assert 0 == accounts2[j].balance + assert len(accounts2[j].subaddresses) >= 1 + for subaddress in accounts2[j].subaddresses: + assert subaddress.is_used == False + j += 1 + + return + else: + j = i + while j < accounts1_size: + assert 0 == accounts1[j].balance + assert len(accounts1[j].subaddresses) >= 1 + for subaddress in accounts1[j].subaddresses: + assert subaddress.is_used == False + j += 1 + + return + + @classmethod + def test_account_equal_on_chain(cls, account1: MoneroAccount, account2: MoneroAccount) -> None: + # nullify off-chain data for comparison + subaddresses1 = account1.subaddresses + subaddresses2 = account2.subaddresses + account1.subaddresses.clear() + account2.subaddresses.clear() + account1.tag = None + account2.tag = None + + # test account equality + assert account1 == account2 + cls.test_subaddresses_equal_on_chain(subaddresses1, subaddresses2) + + @classmethod + def test_subaddresses_equal_on_chain(cls, subaddresses1: list[MoneroSubaddress], subaddresses2: list[MoneroSubaddress]) -> None: + subaddresses1_len = len(subaddresses1) + subaddresses2_len = len(subaddresses2) + size = subaddresses1_len if subaddresses1_len > subaddresses2_len else subaddresses2_len + i = 0 + + while i < size: + if i < subaddresses1_len and i < subaddresses2_len: + cls.test_subaddress_equal_on_chain(subaddresses1[i], subaddresses2[i]); + elif i >= subaddresses1_len: + j = i + while j < subaddresses2_len: + assert 0 == subaddresses2[j].balance + assert False == subaddresses2[j].is_used + j += 1 + + return + else: + j = i + while j < subaddresses1_len: + assert 0 == subaddresses1[i].balance + assert False == subaddresses1[j].is_used + + return + + i += 1 + + @classmethod + def test_subaddress_equal_on_chain(cls, subaddress1: MoneroSubaddress, subaddress2: MoneroSubaddress) -> None: + subaddress1.label = None # nullify off-chain data for comparison + subaddress2.label = None + assert subaddress1 == subaddress2 + + @classmethod + def testTxWalletsEqualOnChain(cls, txs1: list[MoneroTxWallet], txs2: list[MoneroTxWallet]) -> None: + raise NotImplementedError("test_tx_wallets_equal_on_chain(): not implemented") + + @classmethod + def transferCachedInfo(cls, src: MoneroTxWallet, tgt: MoneroTxWallet) -> None: + raise NotImplementedError("transfer_cached_info(): not implemented") + + @classmethod + def testTransfersEqualOnChain(cls, transfers1: list[MoneroTransfer], transfers2: list[MoneroTransfer]) -> None: + raise NotImplementedError("test_transfers_equal_on_chain(): not implemented") + + @classmethod + def testOutputWalletsEqualOnChain(cls, outputs1: list[MoneroOutputWallet], outputs2: list[MoneroOutputWallet]) -> None: + raise NotImplementedError("test_output_wallet_equals_on_chain(): not implemented") diff --git a/tests/utils/wallet_sync_printer.py b/tests/utils/wallet_sync_printer.py index 14f5453..9723bbd 100644 --- a/tests/utils/wallet_sync_printer.py +++ b/tests/utils/wallet_sync_printer.py @@ -1,20 +1,20 @@ from typing_extensions import override from monero import MoneroWalletListener + class WalletSyncPrinter(MoneroWalletListener): - next_increment: float - sync_resolution: float + next_increment: float + sync_resolution: float + + def __init__(self, sync_resolution: float = 0.05) -> None: + super().__init__() + self.next_increment = 0 + self.sync_resolution = sync_resolution - def __init__(self, sync_resolution: float = 0.05) -> None: - super().__init__() - self.next_increment = 0 - self.sync_resolution = sync_resolution - - @override - def on_sync_progress(self, height: int, start_height: int, end_height: int, percent_done: float, message: str): - if (percent_done == 1.0 or percent_done >= self.next_increment): - msg = f"on_sync_progess({height}, {start_height}, {end_height}, {percent_done}, {message})" - print(msg) - self.next_increment += self.sync_resolution - \ No newline at end of file + @override + def on_sync_progress(self, height: int, start_height: int, end_height: int, percent_done: float, message: str): + if (percent_done == 1.0 or percent_done >= self.next_increment): + msg = f"on_sync_progess({height}, {start_height}, {end_height}, {percent_done}, {message})" + print(msg) + self.next_increment += self.sync_resolution diff --git a/tests/utils/wallet_tx_tracker.py b/tests/utils/wallet_tx_tracker.py index c0911f9..bce46c3 100644 --- a/tests/utils/wallet_tx_tracker.py +++ b/tests/utils/wallet_tx_tracker.py @@ -6,118 +6,116 @@ class WalletTxTracker: - _clearedWallets: set[MoneroWallet] + _cleared_wallets: set[MoneroWallet] - def __init__(self) -> None: - self._clearedWallets = set() + def __init__(self) -> None: + self._cleared_wallets = set() - def reset(self) -> None: - self._clearedWallets.clear() + def reset(self) -> None: + self._cleared_wallets.clear() - def wait_for_wallet_txs_to_clear_pool(self, daemon: MoneroDaemon, sync_period_ms: int, wallets: list[MoneroWallet]) -> None: - - # get wallet tx hashes - txHashesWallet: set[str] = set() - - for wallet in wallets: - if wallet not in self._clearedWallets: - wallet.sync() - for tx in wallet.get_txs(): - assert tx.hash is not None - txHashesWallet.add(tx.hash) - - # loop until all wallet txs clear from pool - isFirst: bool = True - miningStarted: bool = False - # daemon = TestUtils.getDaemonRpc() - while True: - - # get hashes of relayed, non-failed txs in the pool - txHashesPool: set[str] = set() - for tx in daemon.get_tx_pool(): - assert tx.hash is not None - if not tx.is_relayed: - continue - elif tx.is_failed: - daemon.flush_tx_pool(tx.hash) # flush tx if failed + def wait_for_wallet_txs_to_clear_pool(self, daemon: MoneroDaemon, sync_period_ms: int, wallets: list[MoneroWallet]) -> None: + # get wallet tx hashes + tx_hashes_wallet: set[str] = set() + + for wallet in wallets: + if wallet not in self._cleared_wallets: + wallet.sync() + for tx in wallet.get_txs(): + assert tx.hash is not None + tx_hashes_wallet.add(tx.hash) + + # loop until all wallet txs clear from pool + is_first: bool = True + mining_started: bool = False + # daemon = TestUtils.getDaemonRpc() + while True: + # get hashes of relayed, non-failed txs in the pool + tx_hashes_pool: set[str] = set() + for tx in daemon.get_tx_pool(): + assert tx.hash is not None + if not tx.is_relayed: + continue + elif tx.is_failed: + daemon.flush_tx_pool(tx.hash) # flush tx if failed + else: + tx_hashes_pool.add(tx.hash) + + # get hashes to wait for as intersection of wallet and pool txs + tx_hashes_pool = tx_hashes_pool.intersection(tx_hashes_wallet) + + # break if no txs to wait for + if len(tx_hashes_pool) == 0: + break + + # if first time waiting, log message and start mining + if is_first: + is_first = False + print("Waiting for wallet txs to clear from the pool in order to fully sync and avoid double spend attempts (known issue)") + mining_status = daemon.get_mining_status() + if (not mining_status.is_active): + try: + daemon.start_mining(MINING_ADDRESS, 1, False, False) + mining_started = True + except: # no problem + pass + + # sleep for a moment + sleep(sync_period_ms) + + # stop mining if started mining + if (mining_started): + daemon.stop_mining() + + # sync wallets with the pool + for wallet in wallets: + wallet.sync() + self._cleared_wallets.add(wallet) + + def wait_for_unlocked_balance(self, daemon: MoneroDaemon, sync_period_ms: int, wallet: MoneroWallet, account_index: int, subaddress_index: int | None, min_amount: int | None = None): + if min_amount is None: + min_amount = 0 + + # check if wallet has balance + if (subaddress_index is not None and wallet.get_balance(account_index, subaddress_index) < min_amount): + raise Exception("Wallet does not have enough balance to wait for") + elif subaddress_index is None and wallet.get_balance(account_index) < min_amount: + raise Exception("Wallet does not have enough balance to wait for") + + # check if wallet has unlocked balance + if subaddress_index is not None: + unlocked_balance = wallet.get_unlocked_balance(account_index, subaddress_index) else: - txHashesPool.add(tx.hash) - - # get hashes to wait for as intersection of wallet and pool txs - txHashesPool = txHashesPool.intersection(txHashesWallet) - - # break if no txs to wait for - if len(txHashesPool) == 0: - break - - # if first time waiting, log message and start mining - if isFirst: - isFirst = False - print("Waiting for wallet txs to clear from the pool in order to fully sync and avoid double spend attempts (known issue)") - miningStatus = daemon.get_mining_status() - if (not miningStatus.is_active): - try: - daemon.start_mining(MINING_ADDRESS, 1, False, False) - miningStarted = True - except: # no problem - pass - - # sleep for a moment - sleep(sync_period_ms) - - # stop mining if started mining - if (miningStarted): - daemon.stop_mining() - - # sync wallets with the pool - for wallet in wallets: - wallet.sync() - self._clearedWallets.add(wallet) - - def wait_for_unlocked_balance(self, daemon: MoneroDaemon, sync_period_ms: int, wallet: MoneroWallet, accountIndex: int, subaddressIndex: int | None, minAmount: int | None = None): - if minAmount is None: - minAmount = 0 - - # check if wallet has balance - if (subaddressIndex is not None and wallet.get_balance(accountIndex, subaddressIndex) < minAmount): - raise Exception("Wallet does not have enough balance to wait for") - elif subaddressIndex is None and wallet.get_balance(accountIndex) < minAmount: - raise Exception("Wallet does not have enough balance to wait for") - - # check if wallet has unlocked balance - if subaddressIndex is not None: - unlockedBalance = wallet.get_unlocked_balance(accountIndex, subaddressIndex) - else: - unlockedBalance = wallet.get_unlocked_balance(accountIndex) - - if (unlockedBalance > minAmount): - return unlockedBalance - - # start mining - # daemon = TestUtils.getDaemonRpc() - miningStarted: bool = False - if not daemon.get_mining_status().is_active: - try: - daemon.start_mining(MINING_ADDRESS, 1, False, False) - miningStarted = True - except: - pass # it's all ok my friend ... + unlocked_balance = wallet.get_unlocked_balance(account_index) + + if (unlocked_balance > min_amount): + return unlocked_balance - # wait for unlocked balance // TODO: promote to MoneroWallet interface? - print("Waiting for unlocked balance") - while (unlockedBalance < minAmount): - if subaddressIndex is not None: - unlockedBalance = wallet.get_unlocked_balance(accountIndex, subaddressIndex) - else: - unlockedBalance = wallet.get_unlocked_balance(accountIndex) + # start mining + # daemon = TestUtils.getDaemonRpc() + mining_started: bool = False + if not daemon.get_mining_status().is_active: + try: + daemon.start_mining(MINING_ADDRESS, 1, False, False) + mining_started = True + except: + pass # it's all ok my friend ... - try: - sleep(sync_period_ms) - except: - pass - - # stop mining if started - if (miningStarted): - daemon.stop_mining() - - return unlockedBalance + # wait for unlocked balance // TODO: promote to MoneroWallet interface? + print("Waiting for unlocked balance") + while (unlocked_balance < min_amount): + if subaddress_index is not None: + unlocked_balance = wallet.get_unlocked_balance(account_index, subaddress_index) + else: + unlocked_balance = wallet.get_unlocked_balance(account_index) + + try: + sleep(sync_period_ms) + except: + pass + + # stop mining if started + if (mining_started): + daemon.stop_mining() + + return unlocked_balance