diff --git a/README.md b/README.md index 1c53f85..3a33298 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![Tests](https://github.com/everoddandeven/monero-python/actions/workflows/test.yml/badge.svg)](https://github.com/everoddandeven/monero-python/actions/workflows/test.yml) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/aeff91a5b1d543ddb400f88ffce150a8)](https://app.codacy.com/gh/everoddandeven/monero-python/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade) -> [!NOTE] +> [!WARNING] > > monero-python is currently under maintenance, expect bugs and breaking changes. > The maintenance of this project has been generously funded by the [Monero CCS](https://ccs.getmonero.org/proposals/everoddandeven-monero-python-maintenance.html). @@ -35,7 +35,7 @@ from monero import * # connect to daemon daemon: MoneroDaemon = MoneroDaemonRpc("http://localhost:38081", "superuser", "abctesting123") -height: int = daemon.get_height(); # 1523651 +height: int = daemon.get_height() # 1523651 txsInPool: list[MoneroTx] = daemon.get_tx_pool() # get transactions in the pool # create wallet from mnemonic phrase using Python bindings to monero-project @@ -170,7 +170,7 @@ For example: `export LD_PRELOAD=/path/to/libjemalloc.a` then run your app. ```bash # With PIP - pip3 install pytest --break-system-packages + pip3 install pytest pytest-rerunfailures --break-system-packages ``` ```bash # System-wide installation Ubuntu/Debian @@ -185,14 +185,22 @@ For example: `export LD_PRELOAD=/path/to/libjemalloc.a` then run your app. 2. Clone the project repository: ```bash git clone --recurse-submodules https://github.com/everoddandeven/monero-python.git + + cd monero-python ``` -3. `cd monero-python` -4. Start RPC servers: +3. Setup docker test environment + ```bash + docker compose -f tests/docker-compose.yml up -d node_1 node_2 xmr_wallet_1 xmr_wallet_2 + ``` +4. Or start RPC servers locally: 1. Download and install [Monero CLI](https://web.getmonero.org/downloads/). 2. Start monerod, e.g.: `./monerod --stagenet` (or use a remote daemon). 3. Start monero-wallet-rpc, e.g.: `./monero-wallet-rpc --daemon-address http://localhost:38081 --stagenet --rpc-bind-port 38083 --rpc-login rpc_user:abc123 --wallet-dir ./` -5. Configure the appropriate RPC endpoints, authentication, and other settings in [monero_test_utils.py](tests/utils/monero_test_utils.py). -6. Run all *.py files in tests folder with `pytest`. +5. Configure the appropriate RPC endpoints, authentication, and other settings in [config.ini](tests/config/config.ini). +6. Run all python tests with: + ```bash + pytest + ``` ## Related projects diff --git a/pytest.ini b/pytest.ini index 1cd06b9..e1b7e81 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,6 +1,9 @@ [pytest] minversion = 6.0 -addopts = -ra -q +addopts = -v +log_level = INFO +log_cli = True log_cli_level = INFO +console_output_style = progress testpaths = tests diff --git a/src/cpp/daemon/py_monero_daemon_model.cpp b/src/cpp/daemon/py_monero_daemon_model.cpp index a51c04f..68767ab 100644 --- a/src/cpp/daemon/py_monero_daemon_model.cpp +++ b/src/cpp/daemon/py_monero_daemon_model.cpp @@ -340,7 +340,6 @@ void PyMoneroBlock::from_property_tree(const boost::property_tree::ptree& node, void PyMoneroBlock::from_property_tree(const boost::property_tree::ptree& node, const std::vector& heights, std::vector>& blocks) { const auto& rpc_blocks = node.get_child("blocks"); const auto& rpc_txs = node.get_child("txs"); - if (rpc_blocks.size() != rpc_txs.size()) { throw std::runtime_error("blocks and txs size mismatch"); } @@ -358,18 +357,14 @@ void PyMoneroBlock::from_property_tree(const boost::property_tree::ptree& node, PyMoneroBlock::from_property_tree(block_n, block); block->m_height = heights.at(idx); blocks.push_back(block); - std::vector tx_hashes; if (auto hashes = it_block->second.get_child_optional("tx_hashes")) { - for (const auto& h : *hashes) { - tx_hashes.push_back(h.second.get_value()); - } + for (const auto& h : *hashes) tx_hashes.push_back(h.second.get_value()); } // build transactions std::vector> txs; size_t tx_idx = 0; - for (const auto& tx_node : it_txs->second) { auto tx = std::make_shared(); tx->m_hash = tx_hashes.at(tx_idx++); @@ -390,9 +385,7 @@ void PyMoneroBlock::from_property_tree(const boost::property_tree::ptree& node, // merge into one block block->m_txs.clear(); for (auto& tx : txs) { - if (tx->m_block != boost::none) { - block->merge(block, tx->m_block.get()); - } + if (tx->m_block != boost::none) block->merge(block, tx->m_block.get()); else { tx->m_block = block; block->m_txs.push_back(tx); @@ -401,7 +394,6 @@ void PyMoneroBlock::from_property_tree(const boost::property_tree::ptree& node, } } - void PyMoneroOutput::from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& output) { for (boost::property_tree::ptree::const_iterator it = node.begin(); it != node.end(); ++it) { std::string key = it->first; @@ -712,10 +704,15 @@ void PyMoneroMiningStatus::from_property_tree(const boost::property_tree::ptree& std::string key = it->first; if (key == std::string("active")) status->m_is_active = it->second.get_value(); else if (key == std::string("is_background_mining_enabled")) status->m_is_background = it->second.get_value(); - else if (key == std::string("address")) status->m_address = it->second.data(); + else if (key == std::string("address") && !it->second.data().empty()) status->m_address = it->second.data(); else if (key == std::string("speed")) status->m_speed = it->second.get_value(); else if (key == std::string("threads_count")) status->m_num_threads = it->second.get_value(); } + + if (status->m_is_active != boost::none && *status->m_is_active == false) { + status->m_is_background = boost::none; + status->m_address = boost::none; + } } std::vector PyMoneroTxHashes::from_property_tree(const boost::property_tree::ptree& node) { @@ -973,15 +970,9 @@ void PyMoneroDaemonInfo::from_property_tree(const boost::property_tree::ptree& n else if (key == std::string("height_without_bootstrap")) info->m_height_without_bootstrap = it->second.get_value(); else if (key == std::string("nettype")) { std::string nettype = it->second.data(); - if (nettype == std::string("mainnet") || nettype == std::string("fakechain")) { - info->m_network_type = monero::monero_network_type::MAINNET; - } - else if (nettype == std::string("testnet")) { - info->m_network_type = monero::monero_network_type::TESTNET; - } - else if (nettype == std::string("stagenet")) { - info->m_network_type = monero::monero_network_type::STAGENET; - } + if (nettype == std::string("mainnet") || nettype == std::string("fakechain")) info->m_network_type = monero::monero_network_type::MAINNET; + else if (nettype == std::string("testnet")) info->m_network_type = monero::monero_network_type::TESTNET; + else if (nettype == std::string("stagenet")) info->m_network_type = monero::monero_network_type::STAGENET; } else if (key == std::string("offline")) info->m_is_offline = it->second.get_value(); else if (key == std::string("incoming_connections_count")) info->m_num_incoming_connections = it->second.get_value(); diff --git a/src/cpp/wallet/py_monero_wallet_model.cpp b/src/cpp/wallet/py_monero_wallet_model.cpp index 57998a9..cdc9acb 100644 --- a/src/cpp/wallet/py_monero_wallet_model.cpp +++ b/src/cpp/wallet/py_monero_wallet_model.cpp @@ -1215,19 +1215,18 @@ rapidjson::Value PyMoneroWalletRelayTxParams::to_rapidjson_val(rapidjson::Docume return root; } -PyMoneroSweepParams::PyMoneroSweepParams(const monero_tx_config& config) { - m_address = config.m_address; - m_account_index = config.m_account_index; - m_subaddr_indices = config.m_subaddress_indices; - m_key_image = config.m_key_image; - m_relay = config.m_relay; - m_priority = config.m_priority; - m_payment_id = config.m_payment_id; - m_below_amount = config.m_below_amount; - m_get_tx_key = true; - m_get_tx_hex = true; - m_get_tx_metadata = true; -} +PyMoneroSweepParams::PyMoneroSweepParams(const monero_tx_config& config): + m_address(config.m_address), + m_account_index(config.m_account_index), + m_subaddr_indices(config.m_subaddress_indices), + m_key_image(config.m_key_image), + m_relay(config.m_relay), + m_priority(config.m_priority), + m_payment_id(config.m_payment_id), + m_below_amount(config.m_below_amount), + m_get_tx_key(true), + m_get_tx_hex(true), + m_get_tx_metadata(true) { } rapidjson::Value PyMoneroSweepParams::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const { rapidjson::Value root(rapidjson::kObjectType); @@ -1301,37 +1300,30 @@ rapidjson::Value PyMoneroImportExportKeyImagesParams::to_rapidjson_val(rapidjson return root; } -PyMoneroCreateOpenWalletParams::PyMoneroCreateOpenWalletParams(const boost::optional& filename, const boost::optional &password) { - m_filename = filename; - m_password = password; -} +PyMoneroCreateOpenWalletParams::PyMoneroCreateOpenWalletParams(const boost::optional& filename, const boost::optional &password): + m_filename(filename), m_password(password) { } -PyMoneroCreateOpenWalletParams::PyMoneroCreateOpenWalletParams(const boost::optional& filename, const boost::optional &password, const boost::optional &language) { - m_filename = filename; - m_password = password; - m_language = language; -} +PyMoneroCreateOpenWalletParams::PyMoneroCreateOpenWalletParams(const boost::optional& filename, const boost::optional &password, const boost::optional &language): + m_filename(filename), m_password(password), m_language(language) { } -PyMoneroCreateOpenWalletParams::PyMoneroCreateOpenWalletParams(const boost::optional& filename, const boost::optional &password, const boost::optional &seed, const boost::optional &seed_offset, const boost::optional &restore_height, const boost::optional &language, const boost::optional &autosave_current, const boost::optional &enable_multisig_experimental) { - m_filename = filename; - m_password = password; - m_seed = seed; - m_seed_offset = seed_offset; - m_restore_height = restore_height; - m_language = language; - m_autosave_current = autosave_current; - m_enable_multisig_experimental = enable_multisig_experimental; -} +PyMoneroCreateOpenWalletParams::PyMoneroCreateOpenWalletParams(const boost::optional& filename, const boost::optional &password, const boost::optional &seed, const boost::optional &seed_offset, const boost::optional &restore_height, const boost::optional &language, const boost::optional &autosave_current, const boost::optional &enable_multisig_experimental): + m_filename(filename), + m_password(password), + m_seed(seed), + m_seed_offset(seed_offset), + m_restore_height(restore_height), + m_language(language), + m_autosave_current(autosave_current), + m_enable_multisig_experimental(enable_multisig_experimental) { } -PyMoneroCreateOpenWalletParams::PyMoneroCreateOpenWalletParams(const boost::optional& filename, const boost::optional &password, const boost::optional &address, const boost::optional &view_key, const boost::optional &spend_key, const boost::optional &restore_height, const boost::optional &autosave_current) { - m_filename = filename; - m_password = password; - m_address = address; - m_view_key = view_key; - m_spend_key = spend_key; - m_restore_height = restore_height; - m_autosave_current = autosave_current; -} +PyMoneroCreateOpenWalletParams::PyMoneroCreateOpenWalletParams(const boost::optional& filename, const boost::optional &password, const boost::optional &address, const boost::optional &view_key, const boost::optional &spend_key, const boost::optional &restore_height, const boost::optional &autosave_current): + m_filename(filename), + m_password(password), + m_address(address), + m_view_key(view_key), + m_spend_key(spend_key), + m_restore_height(restore_height), + m_autosave_current(autosave_current) { } rapidjson::Value PyMoneroCreateOpenWalletParams::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const { rapidjson::Value root(rapidjson::kObjectType); @@ -1351,34 +1343,20 @@ rapidjson::Value PyMoneroCreateOpenWalletParams::to_rapidjson_val(rapidjson::Doc return root; } -PyMoneroReserveProofParams::PyMoneroReserveProofParams(const std::string &message, bool all) { - m_all = all; - m_message = message; -} +PyMoneroReserveProofParams::PyMoneroReserveProofParams(const std::string &message, bool all): + m_all(all), m_message(message) { } -PyMoneroReserveProofParams::PyMoneroReserveProofParams(const std::string &address, const std::string &message, const std::string &signature) { - m_address = address; - m_message = message; - m_signature = signature; -} +PyMoneroReserveProofParams::PyMoneroReserveProofParams(const std::string &address, const std::string &message, const std::string &signature): + m_address(address), m_message(message), m_signature(signature) { } -PyMoneroReserveProofParams::PyMoneroReserveProofParams(const std::string &tx_hash, const std::string &address, const std::string &message, const std::string &signature) { - m_tx_hash = tx_hash; - m_address = address; - m_message = message; - m_signature = signature; -} +PyMoneroReserveProofParams::PyMoneroReserveProofParams(const std::string &tx_hash, const std::string &address, const std::string &message, const std::string &signature): + m_tx_hash(tx_hash), m_address(address), m_message(message), m_signature(signature) { } -PyMoneroReserveProofParams::PyMoneroReserveProofParams(const std::string &tx_hash, const std::string &message) { - m_tx_hash = tx_hash; - m_message = message; -} +PyMoneroReserveProofParams::PyMoneroReserveProofParams(const std::string &tx_hash, const std::string &message): + m_tx_hash(tx_hash), m_message(message) { } -PyMoneroReserveProofParams::PyMoneroReserveProofParams(uint32_t account_index, uint64_t amount, const std::string &message) { - m_account_index = account_index; - m_amount = amount; - m_message = message; -} +PyMoneroReserveProofParams::PyMoneroReserveProofParams(uint32_t account_index, uint64_t amount, const std::string &message): + m_account_index(account_index), m_amount(amount), m_message(message) { } rapidjson::Value PyMoneroReserveProofParams::to_rapidjson_val(rapidjson::Document::AllocatorType& allocator) const { rapidjson::Value root(rapidjson::kObjectType); diff --git a/src/cpp/wallet/py_monero_wallet_rpc.cpp b/src/cpp/wallet/py_monero_wallet_rpc.cpp index e77eac1..dbadff8 100644 --- a/src/cpp/wallet/py_monero_wallet_rpc.cpp +++ b/src/cpp/wallet/py_monero_wallet_rpc.cpp @@ -602,8 +602,10 @@ monero_account PyMoneroWalletRpc::get_account(const uint32_t account_idx, bool i monero_account PyMoneroWalletRpc::get_account(const uint32_t account_idx, bool include_subaddresses, bool skip_balances) const { for(auto& account : monero::monero_wallet::get_accounts()) { if (account.m_index.get() == account_idx) { - std::vector empty_indices; - if (include_subaddresses) account.m_subaddresses = get_subaddresses(account_idx, empty_indices, skip_balances); + if (include_subaddresses) { + std::vector empty_indices; + account.m_subaddresses = get_subaddresses(account_idx, empty_indices, skip_balances); + } return account; } } @@ -701,13 +703,11 @@ std::vector PyMoneroWalletRpc::get_subaddresses(const uint32_ auto response = m_rpc->send_json_request(request); if (response->m_result == boost::none) throw std::runtime_error("Invalid Monero JSONRPC response"); auto node = response->m_result.get(); - std::vector subaddresses; // initialize subaddresses for (auto it = node.begin(); it != node.end(); ++it) { std::string key = it->first; - if (key == std::string("addresses")) { auto node2 = it->second; for (auto it2 = node2.begin(); it2 != node2.end(); ++it2) { @@ -718,12 +718,10 @@ std::vector PyMoneroWalletRpc::get_subaddresses(const uint32_ } break; } - } // fetch and initialize subaddress balances if (!skip_balances) { - // these fields are not initialized if subaddress is unused and therefore not returned from `get_balance` for (auto &subaddress : subaddresses) { subaddress.m_balance = 0; @@ -737,7 +735,6 @@ std::vector PyMoneroWalletRpc::get_subaddresses(const uint32_ auto response2 = m_rpc->send_json_request(request); if (response2->m_result == boost::none) throw std::runtime_error("Invalid Monero JSONRPC response"); auto node2 = response2->m_result.get(); - std::vector> subaddresses2; PyMoneroSubaddress::from_rpc_property_tree(node2, subaddresses2); diff --git a/tests/config/config.ini b/tests/config/config.ini new file mode 100644 index 0000000..6ad7542 --- /dev/null +++ b/tests/config/config.ini @@ -0,0 +1,34 @@ +[general] +test_non_relays=True +lite_mode=False +test_notifications=True +network_type=mainnet +auto_connect_timeout_ms=3000 + +[daemon] +rpc_uri=127.0.0.1:18081 +rpc_username= +rpc_password= + +[wallet] +name=test_wallet_1 +password=supersecretpassword123 +dir=./test_wallets +language=English +address=48W9YHwPzRz9aPTeXCA6kmSpW6HsvmWx578jj3of2gT3JwZzwTf33amESBoNDkL6SVK34Q2HTKqgYbGyE1hBws3wCrcBDR2 +private_view_key=e8c2288181bad9ec410d7322efd65f663c6da57bd1d1198636278a039743a600 +private_spend_key=be7a2f71097f146bdf0fb5bb8edfe2240a9767e15adee74d95af1b5a64f29a0c +public_view_key=42e465bdcd00de50516f1c7049bbe26bd3c11195e8dae5cceb38bad92d484269 +public_spend_key=b58d33a1dac23d334539cbed3657b69a5c967d6860357e24ab4d11899a312a6b +seed=vortex degrees outbreak teeming gimmick school rounded tonic observant injury leech ought problems ahead upcoming ledge textbook cigar atrium trash dunes eavesdrop dullness evolved vortex +first_receive_height=171 +rpc_port_start=18082 +rpc_username=rpc_user +rpc_password=abc123 +rpc_access_control_origins="http:#localhost:8080" +rpc_domain=localhost +rpc_zmq_enabled=False +rpc_zmq_port_start=48083 +rpc_zmq_bind_port_start=48083 +rpc_zmq_domain=127.0.0.1 +sync_period_in_ms=5000 diff --git a/tests/config/test_monero_utils.ini b/tests/config/test_monero_utils.ini index 3a667f7..3a66263 100644 --- a/tests/config/test_monero_utils.ini +++ b/tests/config/test_monero_utils.ini @@ -53,4 +53,5 @@ public_spend_key=3e48df9e9d8038dbf6f5382fac2becd8686273cda5bd87187e45dca7ec5af37 invalid_private_view_key=5B8s3obCY2ETeQB3GNAGPK2zRGen5UeW1WzegSizVsmf6z5NvM2GLoN6zzk1vHyzGAAfA8pGhuYAeCFZjHAp59jRVQkunGS invalid_public_view_key=z86cf351d10894769feba29b9e201e12fb100b85bb52fc5825c864eef55c5840d invalid_private_spend_key=z86cf351d10894769feba29b9e201e12fb100b85bb52fc5825c864eef55c5840d -invalid_public_spend_key=z86cf351d10894769feba29b9e201e12fb100b85bb52fc5825c864eef55c5840d \ No newline at end of file +invalid_public_spend_key=z86cf351d10894769feba29b9e201e12fb100b85bb52fc5825c864eef55c5840d +seed=vortex degrees outbreak teeming gimmick school rounded tonic observant injury leech ought problems ahead upcoming ledge textbook cigar atrium trash dunes eavesdrop dullness evolved vortex diff --git a/tests/test_monero_connection_manager.py b/tests/test_monero_connection_manager.py index 35b5204..6a199ec 100644 --- a/tests/test_monero_connection_manager.py +++ b/tests/test_monero_connection_manager.py @@ -4,7 +4,7 @@ from monero import ( MoneroWallet, MoneroConnectionManager, MoneroRpcConnection, MoneroConnectionPollType ) -from utils import ConnectionChangeCollector, MoneroTestUtils as Utils +from utils import ConnectionChangeCollector, TestUtils as Utils # TODO enable connection manager tests diff --git a/tests/test_monero_daemon_rpc.py b/tests/test_monero_daemon_rpc.py index 2ec936e..d86bcad 100644 --- a/tests/test_monero_daemon_rpc.py +++ b/tests/test_monero_daemon_rpc.py @@ -5,20 +5,21 @@ from monero import ( MoneroDaemonRpc, MoneroVersion, MoneroBlockHeader, MoneroBlockTemplate, - MoneroBlock, MoneroWalletRpc, MoneroMiningStatus, MoneroPruneResult, + MoneroBlock, MoneroWallet, MoneroMiningStatus, MoneroPruneResult, MoneroDaemonUpdateCheckResult, MoneroDaemonUpdateDownloadResult, MoneroDaemonListener, MoneroPeer, MoneroDaemonInfo, MoneroDaemonSyncInfo, MoneroHardForkInfo, MoneroAltChain, MoneroTx, MoneroSubmitTxResult, MoneroTxPoolStats, MoneroBan, MoneroTxConfig, MoneroDestination ) -from utils import MoneroTestUtils as Utils, TestContext, BinaryBlockContext, MiningUtils +from utils import TestUtils as Utils, TestContext, BinaryBlockContext, MiningUtils logger: logging.Logger = logging.getLogger(__name__) +Utils.load_config() class TestMoneroDaemonRpc: _daemon: MoneroDaemonRpc = Utils.get_daemon_rpc() - _wallet: MoneroWalletRpc #= Utils.get_wallet_rpc() + _wallet: MoneroWallet = Utils.get_wallet_rpc() BINARY_BLOCK_CTX: BinaryBlockContext = BinaryBlockContext() #region Fixtures @@ -189,7 +190,7 @@ def test_get_block_by_height(self): # Can get blocks by height which includes transactions (binary) #@pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") - @pytest.mark.skip(reason="Not implemented") + @pytest.mark.skip(reason="TODO fund wallet") def test_get_blocks_by_height_binary(self): # set number of blocks to test num_blocks = 100 @@ -308,7 +309,7 @@ def test_get_tx_by_hash(self) -> None: # Can get transactions by hashes with and without pruning #@pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") - @pytest.mark.skip(reason="Needs wallet rpc") + @pytest.mark.skip(reason="TODO fund wallet") def test_get_txs_by_hashes(self) -> None: # fetch tx hashses to test tx_hashes = Utils.get_confirmed_tx_hashes(self._daemon) @@ -357,7 +358,7 @@ def test_get_txs_by_hashes(self) -> None: # Can get transaction pool statistics #@pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") - @pytest.mark.skip("TODO") + @pytest.mark.skip("TODO implement monero_wallet_rpc.get_txs()") def test_get_tx_pool_statistics(self): daemon = self._daemon wallet = self._wallet @@ -511,8 +512,7 @@ def test_set_incoming_peer_limit(self): self._daemon.set_incoming_peer_limit(10) # Can notify listeners when a new block is added to the chain - #@pytest.mark.skipif(Utils.LITE_MODE is True or Utils.TEST_NOTIFICATIONS is False, reason="TEST_NOTIFICATIONS disabled") - @pytest.mark.skip("TODO") + @pytest.mark.skipif(Utils.LITE_MODE is True or Utils.TEST_NOTIFICATIONS is False, reason="TEST_NOTIFICATIONS disabled") def test_block_listener(self): try: # start mining if possible to help push the network along @@ -520,7 +520,7 @@ def test_block_listener(self): try: self._daemon.start_mining(address, 8, False, True) except Exception as e: - print(f"[!]: {str(e)}") + logger.warning(f"[!]: {str(e)}") # register a listener listener: MoneroDaemonListener = MoneroDaemonListener() @@ -540,7 +540,7 @@ def test_block_listener(self): try : self._daemon.stop_mining() except Exception as e: - print(f"[!]: {str(e)}") + logger.warning(f"[!]: {str(e)}") # Can ban a peer @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") @@ -597,14 +597,13 @@ def test_ban_peers(self): assert found2, f"Could not find peer ban2 {addr2}" # Can start and stop mining - #@pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") - @pytest.mark.skip("TODO") + @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_mining(self): # stop mining at beginning of test try: self._daemon.stop_mining() except Exception as e: - print(f"[!]: {str(e)}") + logger.warning(f"[!]: {str(e)}") # generate address to mine to address: str = self._wallet.get_primary_address() @@ -616,20 +615,19 @@ def test_mining(self): self._daemon.stop_mining() # Can get mining status - #@pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") - @pytest.mark.skip("TODO") + @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_get_mining_status(self): try: # stop mining at beginning of test try: self._daemon.stop_mining() except Exception as e: - print(f"[!]: {str(e)}") + logger.warning(f"[!]: {str(e)}") # 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_is_none(status.address, f"Mining address is not None: {status.address}") Utils.assert_equals(0, status.speed) Utils.assert_equals(0, status.num_threads) Utils.assert_is_none(status.is_background) @@ -653,7 +651,7 @@ def test_get_mining_status(self): try: self._daemon.stop_mining() except Exception as e: - print(f"[!]: {str(e)}") + logger.warning(f"[!]: {str(e)}") # Can submit a mined block to the network @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") diff --git a/tests/test_monero_rpc_connection.py b/tests/test_monero_rpc_connection.py index 507923d..0f6f521 100644 --- a/tests/test_monero_rpc_connection.py +++ b/tests/test_monero_rpc_connection.py @@ -1,7 +1,7 @@ -import pytest # type: ignore +import pytest from monero import MoneroRpcConnection -from utils import MoneroTestUtils as Utils +from utils import TestUtils as Utils class TestMoneroRpcConnection: @@ -10,15 +10,15 @@ class TestMoneroRpcConnection: def test_node_rpc_connection(self): connection = MoneroRpcConnection(Utils.DAEMON_RPC_URI, Utils.DAEMON_RPC_USERNAME, Utils.DAEMON_RPC_PASSWORD) Utils.test_rpc_connection(connection, Utils.DAEMON_RPC_URI) - assert connection.check_connection() - assert connection.is_connected() - assert connection.is_online() # Test wallet rpc connection - @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS is disabled") + @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") def test_wallet_rpc_connection(self): connection = MoneroRpcConnection(Utils.WALLET_RPC_URI, Utils.WALLET_RPC_USERNAME, Utils.WALLET_RPC_PASSWORD) Utils.test_rpc_connection(connection, Utils.WALLET_RPC_URI) - assert connection.check_connection() - assert connection.is_connected() - assert connection.is_online() + + # Test invalid connection + @pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled") + def test_invalid_connection(self): + connection = MoneroRpcConnection(Utils.OFFLINE_SERVER_URI) + Utils.test_rpc_connection(connection, Utils.OFFLINE_SERVER_URI, False) diff --git a/tests/test_monero_utils.py b/tests/test_monero_utils.py index 53299e4..6db00e7 100644 --- a/tests/test_monero_utils.py +++ b/tests/test_monero_utils.py @@ -5,7 +5,7 @@ from monero import ( MoneroNetworkType, MoneroIntegratedAddress, MoneroUtils, MoneroTxConfig ) -from utils import MoneroTestUtils, AddressBook, KeysBook +from utils import TestUtils, AddressBook, KeysBook class TestMoneroUtils: @@ -75,7 +75,7 @@ def test_serialize_heights_small(self): binary: bytes = MoneroUtils.dict_to_binary(json_map) - MoneroTestUtils.assert_true(len(binary) > 0) + TestUtils.assert_true(len(binary) > 0) json_map2: dict[Any, Any] = MoneroUtils.binary_to_dict(binary) @@ -88,7 +88,7 @@ def test_serialize_heights_big(self): } binary: bytes = MoneroUtils.dict_to_binary(json_map) - MoneroTestUtils.assert_true(len(binary) > 0) + TestUtils.assert_true(len(binary) > 0) json_map2: dict[Any, Any] = MoneroUtils.binary_to_dict(binary) assert json_map == json_map2 @@ -101,7 +101,7 @@ def test_serialize_text_short(self, config: TestMoneroUtils.Config): } binary: bytes = MoneroUtils.dict_to_binary(json_map) - MoneroTestUtils.assert_true(len(binary) > 0) + TestUtils.assert_true(len(binary) > 0) json_map2: dict[Any, Any] = MoneroUtils.binary_to_dict(binary) assert json_map == json_map2 @@ -127,7 +127,7 @@ def test_serialize_text_long(self, config: TestMoneroUtils.Config): } binary: bytes = MoneroUtils.dict_to_binary(json_map) - MoneroTestUtils.assert_true(len(binary) > 0) + TestUtils.assert_true(len(binary) > 0) json_map2: dict[Any, Any] = MoneroUtils.binary_to_dict(binary) assert json_map == json_map2 @@ -181,52 +181,64 @@ def test_address_validation(self, config: TestMoneroUtils.Config): assert (MoneroUtils.is_valid_address(config.stagenet.subaddress_3, MoneroNetworkType.STAGENET)) is True # test invalid addresses on mainnet - MoneroTestUtils.test_invalid_address(None, MoneroNetworkType.MAINNET) - MoneroTestUtils.test_invalid_address("", MoneroNetworkType.MAINNET) - MoneroTestUtils.test_invalid_address(config.mainnet.invalid_1, MoneroNetworkType.MAINNET) - MoneroTestUtils.test_invalid_address(config.mainnet.invalid_2, MoneroNetworkType.MAINNET) - MoneroTestUtils.test_invalid_address(config.mainnet.invalid_3, MoneroNetworkType.MAINNET) + TestUtils.test_invalid_address(None, MoneroNetworkType.MAINNET) + TestUtils.test_invalid_address("", MoneroNetworkType.MAINNET) + TestUtils.test_invalid_address(config.mainnet.invalid_1, MoneroNetworkType.MAINNET) + TestUtils.test_invalid_address(config.mainnet.invalid_2, MoneroNetworkType.MAINNET) + TestUtils.test_invalid_address(config.mainnet.invalid_3, MoneroNetworkType.MAINNET) # test invalid addresses on testnet - MoneroTestUtils.test_invalid_address(None, MoneroNetworkType.TESTNET) - MoneroTestUtils.test_invalid_address("", MoneroNetworkType.TESTNET) - MoneroTestUtils.test_invalid_address(config.testnet.invalid_1, MoneroNetworkType.TESTNET) - MoneroTestUtils.test_invalid_address(config.testnet.invalid_2, MoneroNetworkType.TESTNET) - MoneroTestUtils.test_invalid_address(config.testnet.invalid_3, MoneroNetworkType.TESTNET) + TestUtils.test_invalid_address(None, MoneroNetworkType.TESTNET) + TestUtils.test_invalid_address("", MoneroNetworkType.TESTNET) + TestUtils.test_invalid_address(config.testnet.invalid_1, MoneroNetworkType.TESTNET) + TestUtils.test_invalid_address(config.testnet.invalid_2, MoneroNetworkType.TESTNET) + TestUtils.test_invalid_address(config.testnet.invalid_3, MoneroNetworkType.TESTNET) # test invalid addresses on stagenet - MoneroTestUtils.test_invalid_address(None, MoneroNetworkType.STAGENET) - MoneroTestUtils.test_invalid_address("", MoneroNetworkType.STAGENET) - MoneroTestUtils.test_invalid_address(config.stagenet.invalid_1, MoneroNetworkType.STAGENET) - MoneroTestUtils.test_invalid_address(config.stagenet.invalid_2, MoneroNetworkType.STAGENET) - MoneroTestUtils.test_invalid_address(config.stagenet.invalid_3, MoneroNetworkType.STAGENET) + TestUtils.test_invalid_address(None, MoneroNetworkType.STAGENET) + TestUtils.test_invalid_address("", MoneroNetworkType.STAGENET) + TestUtils.test_invalid_address(config.stagenet.invalid_1, MoneroNetworkType.STAGENET) + TestUtils.test_invalid_address(config.stagenet.invalid_2, MoneroNetworkType.STAGENET) + TestUtils.test_invalid_address(config.stagenet.invalid_3, MoneroNetworkType.STAGENET) # Can validate keys def test_key_validation(self, config: TestMoneroUtils.Config): # test private view key validation - MoneroTestUtils.assert_true(MoneroUtils.is_valid_private_view_key(config.keys.private_view_key)) - MoneroTestUtils.test_invalid_private_view_key("") - MoneroTestUtils.test_invalid_private_view_key(None) - MoneroTestUtils.test_invalid_private_view_key(config.keys.invalid_private_view_key) + TestUtils.assert_true(MoneroUtils.is_valid_private_view_key(config.keys.private_view_key)) + TestUtils.test_invalid_private_view_key("") + TestUtils.test_invalid_private_view_key(None) + TestUtils.test_invalid_private_view_key(config.keys.invalid_private_view_key) # test public view key validation - MoneroTestUtils.assert_true(MoneroUtils.is_valid_public_view_key(config.keys.public_view_key)) - MoneroTestUtils.test_invalid_public_view_key("") - MoneroTestUtils.test_invalid_public_view_key(None) - MoneroTestUtils.test_invalid_public_view_key(config.keys.invalid_public_view_key) + TestUtils.assert_true(MoneroUtils.is_valid_public_view_key(config.keys.public_view_key)) + TestUtils.test_invalid_public_view_key("") + TestUtils.test_invalid_public_view_key(None) + TestUtils.test_invalid_public_view_key(config.keys.invalid_public_view_key) # test private spend key validation - MoneroTestUtils.assert_true(MoneroUtils.is_valid_private_spend_key(config.keys.private_spend_key)) - MoneroTestUtils.test_invalid_private_spend_key("") - MoneroTestUtils.test_invalid_private_spend_key(None) - MoneroTestUtils.test_invalid_private_spend_key(config.keys.invalid_private_spend_key) + TestUtils.assert_true(MoneroUtils.is_valid_private_spend_key(config.keys.private_spend_key)) + TestUtils.test_invalid_private_spend_key("") + TestUtils.test_invalid_private_spend_key(None) + TestUtils.test_invalid_private_spend_key(config.keys.invalid_private_spend_key) # test public spend key validation - MoneroTestUtils.assert_true(MoneroUtils.is_valid_public_spend_key(config.keys.public_spend_key)) - MoneroTestUtils.test_invalid_public_spend_key("") - MoneroTestUtils.test_invalid_public_spend_key(None) - MoneroTestUtils.test_invalid_public_spend_key(config.keys.invalid_public_spend_key) + TestUtils.assert_true(MoneroUtils.is_valid_public_spend_key(config.keys.public_spend_key)) + TestUtils.test_invalid_public_spend_key("") + TestUtils.test_invalid_public_spend_key(None) + TestUtils.test_invalid_public_spend_key(config.keys.invalid_public_spend_key) + + # Can validate seed + def test_mnemonic_validation(self, config: TestMoneroUtils.Config): + + # test valid seed + TestUtils.assert_true(MoneroUtils.is_valid_mnemonic(config.keys.seed), f"Invalid seed: {config.keys.seed}") + + # test invalid seed + TestUtils.assert_false(MoneroUtils.is_valid_mnemonic("invalid monero wallet seed")) + + # test empty seed + TestUtils.assert_false(MoneroUtils.is_valid_mnemonic("")) # Can convert between XMR and atomic units def test_atomic_unit_conversion(self): diff --git a/tests/test_monero_wallet_common.py b/tests/test_monero_wallet_common.py index 4548161..d266457 100644 --- a/tests/test_monero_wallet_common.py +++ b/tests/test_monero_wallet_common.py @@ -11,7 +11,7 @@ MoneroTxConfig, MoneroDestination, MoneroRpcConnection, MoneroError, MoneroKeyImage, MoneroTxQuery, MoneroUtils ) -from utils import MoneroTestUtils as TestUtils, WalletEqualityUtils +from utils import TestUtils, WalletEqualityUtils logger: logging.Logger = logging.getLogger(__name__) @@ -39,6 +39,10 @@ def parse(cls, parser: ConfigParser) -> BaseTestMoneroWallet.Config: def _get_test_daemon(cls) -> MoneroDaemonRpc: return TestUtils.get_daemon_rpc() + @abstractmethod + def get_test_wallet(self) -> MoneroWallet: + ... + @abstractmethod def _open_wallet(self, config: Optional[MoneroWalletConfig]) -> MoneroWallet: ... @@ -62,12 +66,9 @@ def _open_wallet_from_path(self, path: str, password: str | None) -> MoneroWalle return self._open_wallet(config) - @abstractmethod - def get_test_wallet(self) -> MoneroWallet: - ... - @classmethod - def is_random_wallet_config(cls, config: MoneroWalletConfig) -> bool: + def is_random_wallet_config(cls, config: Optional[MoneroWalletConfig]) -> bool: + assert config is not None return config.seed is None and config.primary_address is None #endregion @@ -251,7 +252,7 @@ def test_create_wallet_from_keys(self) -> None: TestUtils.assert_equals(private_spend_key, wallet.get_private_spend_key()) if not wallet.is_connected_to_daemon(): # TODO monero-project: keys wallets not connected - print(f"WARNING: {self.CREATED_WALLET_KEYS_ERROR}") + logger.warning(f"WARNING: {self.CREATED_WALLET_KEYS_ERROR}") TestUtils.assert_true(wallet.is_connected_to_daemon(), self.CREATED_WALLET_KEYS_ERROR) if not isinstance(wallet, MoneroWalletRpc): # TODO monero-wallet-rpc: cannot get seed from wallet created from keys? @@ -278,7 +279,7 @@ def test_create_wallet_from_keys(self) -> None: TestUtils.assert_equals(private_spend_key, wallet.get_private_spend_key()) if not wallet.is_connected_to_daemon(): # TODO monero-project: keys wallets not connected - print(f"WARNING: {self.CREATED_WALLET_KEYS_ERROR}") + logger.warning(f"{self.CREATED_WALLET_KEYS_ERROR}") TestUtils.assert_true(wallet.is_connected_to_daemon(), self.CREATED_WALLET_KEYS_ERROR) if not isinstance(wallet, MoneroWalletRpc): # TODO monero-wallet-rpc: cannot get seed from wallet created from keys? diff --git a/tests/test_monero_wallet_full.py b/tests/test_monero_wallet_full.py index 8d614cf..93274bb 100644 --- a/tests/test_monero_wallet_full.py +++ b/tests/test_monero_wallet_full.py @@ -8,9 +8,11 @@ MoneroSubaddress, MoneroDaemonRpc, MoneroWallet ) -from utils import MoneroTestUtils as Utils +from utils import TestUtils as Utils from test_monero_wallet_common import BaseTestMoneroWallet +Utils.load_config() + # TODO enable full wallet tests @pytest.mark.skipif(True, reason="TODO") diff --git a/tests/test_monero_wallet_keys.py b/tests/test_monero_wallet_keys.py index 6bb020e..81ffd8f 100644 --- a/tests/test_monero_wallet_keys.py +++ b/tests/test_monero_wallet_keys.py @@ -7,11 +7,12 @@ MoneroWalletKeys, MoneroWalletConfig, MoneroWallet, MoneroUtils, MoneroAccount, MoneroSubaddress ) -from utils import MoneroTestUtils as Utils +from utils import TestUtils as Utils from test_monero_wallet_common import BaseTestMoneroWallet logger: logging.Logger = logging.getLogger(__name__) +Utils.load_config() class TestMoneroWalletKeys(BaseTestMoneroWallet): @@ -30,7 +31,8 @@ def before_each(self, request: pytest.FixtureRequest): @classmethod @override - def is_random_wallet_config(cls, config: MoneroWalletConfig) -> bool: + def is_random_wallet_config(cls, config: Optional[MoneroWalletConfig]) -> bool: + assert config is not None return super().is_random_wallet_config(config) and config.private_spend_key is None @override @@ -38,17 +40,8 @@ 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 = self.is_random_wallet_config(config) - print(f"create_wallet(self): random = {random}") if config.network_type is None: config.network_type = Utils.NETWORK_TYPE diff --git a/tests/test_monero_wallet_rpc.py b/tests/test_monero_wallet_rpc.py index 38d4fae..ba43bcc 100644 --- a/tests/test_monero_wallet_rpc.py +++ b/tests/test_monero_wallet_rpc.py @@ -1,15 +1,17 @@ import pytest -from monero import MoneroWallet, MoneroWalletConfig, MoneroDaemonRpc +from monero import MoneroWallet, MoneroWalletConfig, MoneroDaemonRpc, MoneroWalletRpc from typing_extensions import override -from utils import MoneroTestUtils as Utils +from utils import TestUtils as Utils from test_monero_wallet_common import BaseTestMoneroWallet +Utils.load_config() + class TestMoneroWalletRpc(BaseTestMoneroWallet): - _wallet: MoneroWallet = Utils.get_wallet_rpc() _daemon: MoneroDaemonRpc = Utils.get_daemon_rpc() + _wallet: MoneroWalletRpc = Utils.get_wallet_rpc() # type: ignore #region Overrides diff --git a/tests/test_sample_code.py b/tests/test_sample_code.py deleted file mode 100644 index 36596bc..0000000 --- a/tests/test_sample_code.py +++ /dev/null @@ -1,147 +0,0 @@ -import pytest - -from typing_extensions import override - -from monero import ( - 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 - - @override - def on_output_received(self, output: MoneroOutputWallet) -> None: - amount = output.amount - tx_hash = output.tx.hash - is_confirmed = output.tx.is_confirmed - print(f"Received {amount}, confirmed {is_confirmed}, tx hash: {tx_hash}") - self.funds_received = True - - -@pytest.mark.skipif(True, reason="TODO") -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 - txs_in_pool: list[MoneroTx] = daemon.get_tx_pool() # get transactions in the pool - print(f"Found {len(txs_in_pool)} tx(s) in pool at height {height}") - - # 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 - - 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 - # *** REPLACE WITH CONSTANTS IN README *** - wallet_rpc = MoneroWalletRpc(Utils.WALLET_RPC_URI, Utils.WALLET_RPC_USERNAME, Utils.WALLET_RPC_PASSWORD) - 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 - print(f"Open wallet {primary_address}, balance: {balance}, tx(s): {len(txs)}") - # send funds from RPC wallet to full wallet - # *** REMOVE FROM README SAMPLE *** - 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 - ) - 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... ?" - assert fee is not None - 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 - 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 = connection_manager - wallet_config.seed = Utils.SEED - wallet_config.restore_height = Utils.FIRST_RECEIVE_HEIGHT - # *** REPLACE WITH FIRST RECEIVE HEIGHT IN README *** - wallet_full = MoneroWalletFull.create_wallet(wallet_config) - print(f"Created wallet {wallet_full.get_path()}") - - # 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 the 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() - - assert len(connections) > 0 - - # clear connection manager - connection_manager.clear() diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py index fcdffd6..2db1972 100644 --- a/tests/utils/__init__.py +++ b/tests/utils/__init__.py @@ -1,4 +1,4 @@ -from .monero_test_utils import MoneroTestUtils +from .test_utils import TestUtils from .mining_utils import MiningUtils from .wallet_sync_printer import WalletSyncPrinter from .connection_change_collector import ConnectionChangeCollector @@ -14,7 +14,7 @@ from .const import MINING_ADDRESS __all__ = [ - 'MoneroTestUtils', + 'TestUtils', 'MiningUtils', 'WalletSyncPrinter', 'ConnectionChangeCollector', diff --git a/tests/utils/address_book.py b/tests/utils/address_book.py index 169e2b0..d815617 100644 --- a/tests/utils/address_book.py +++ b/tests/utils/address_book.py @@ -1,7 +1,7 @@ from __future__ import annotations from configparser import ConfigParser from monero import MoneroNetworkType -from .monero_test_utils import MoneroTestUtils +from .test_utils import TestUtils class AddressBook: @@ -42,5 +42,5 @@ def parse(cls, parser: ConfigParser, section: str) -> AddressBook: entry.invalid_1 = parser.get(section, 'invalid_1') entry.invalid_2 = parser.get(section, 'invalid_2') entry.invalid_3 = parser.get(section, 'invalid_3') - entry.network_type = MoneroTestUtils.parse_network_type(section) + entry.network_type = TestUtils.parse_network_type(section) return entry diff --git a/tests/utils/keys_book.py b/tests/utils/keys_book.py index 020a013..e482101 100644 --- a/tests/utils/keys_book.py +++ b/tests/utils/keys_book.py @@ -11,6 +11,7 @@ class KeysBook: invalid_public_view_key: str = '' invalid_private_spend_key: str = '' invalid_public_spend_key: str = '' + seed: str = '' @classmethod def parse(cls, parser: ConfigParser) -> KeysBook: @@ -25,4 +26,5 @@ def parse(cls, parser: ConfigParser) -> KeysBook: book.invalid_public_view_key = parser.get('keys', 'invalid_public_view_key') book.invalid_private_spend_key = parser.get('keys', 'invalid_private_spend_key') book.invalid_public_spend_key = parser.get('keys', 'invalid_public_spend_key') + book.seed = parser.get('keys', 'seed') return book diff --git a/tests/utils/mining_utils.py b/tests/utils/mining_utils.py index 1ee65ef..03c1e7f 100644 --- a/tests/utils/mining_utils.py +++ b/tests/utils/mining_utils.py @@ -1,8 +1,12 @@ +import logging + from typing import Optional from time import sleep from monero import MoneroDaemonRpc from .const import MINING_ADDRESS +logger: logging.Logger = logging.getLogger(__name__) + class MiningUtils: """ @@ -22,12 +26,12 @@ def _get_daemon(cls) -> MoneroDaemonRpc: return cls._DAEMON @classmethod - def is_mining(cls) -> bool: + def is_mining(cls, d: Optional[MoneroDaemonRpc] = None) -> bool: """ Check if mining is enabled. """ # max tries 3 - daemon = cls._get_daemon() + daemon = cls._get_daemon() if d is None else d for i in range(3): try: status = daemon.get_mining_status() @@ -40,49 +44,49 @@ def is_mining(cls) -> bool: return False @classmethod - def start_mining(cls) -> None: + def start_mining(cls, d: Optional[MoneroDaemonRpc] = None) -> None: """ Start internal mining. """ if cls.is_mining(): raise Exception("Mining already started") - daemon = cls._get_daemon() + daemon = cls._get_daemon() if d is None else d daemon.start_mining(MINING_ADDRESS, 1, False, False) @classmethod - def stop_mining(cls) -> None: + def stop_mining(cls, d: Optional[MoneroDaemonRpc] = None) -> None: """ Stop internal mining. """ if not cls.is_mining(): raise Exception("Mining already stopped") - daemon = cls._get_daemon() + daemon = cls._get_daemon() if d is None else d daemon.stop_mining() @classmethod - def try_stop_mining(cls) -> bool: + def try_stop_mining(cls, d: Optional[MoneroDaemonRpc] = None) -> bool: """ Try stop internal mining. """ try: - cls.stop_mining() + cls.stop_mining(d) return True except Exception as e: - print(f"MiningUtils.stop_mining(): {e}") + logger.warning(f"MiningUtils.stop_mining(): {e}") return False @classmethod - def try_start_mining(cls) -> bool: + def try_start_mining(cls, d: Optional[MoneroDaemonRpc] = None) -> bool: """ Try start internal mining. """ try: - cls.start_mining() + cls.start_mining(d) return True except Exception as e: - print(f"MiningUtils.start_mining(): {e}") + logger.warning(f"MiningUtils.start_mining(): {e}") return False @classmethod @@ -101,14 +105,17 @@ def wait_for_height(cls, height: int) -> int: stop_mining = True while current_height < height: - print(f"[INFO] Waiting for height ({current_height}/{height})") + logger.info(f"Waiting for blockchain height ({current_height}/{height})") block = daemon.wait_for_next_block_header() assert block.height is not None current_height = block.height + sleep(3) if stop_mining: cls.stop_mining() sleep(3) current_height = daemon.get_height() + logger.info(f"Blockchain height: {current_height}") + return current_height diff --git a/tests/utils/print_height.py b/tests/utils/print_height.py index a2f3c41..8d37c7b 100644 --- a/tests/utils/print_height.py +++ b/tests/utils/print_height.py @@ -1,11 +1,14 @@ +import logging from abc import ABC -from .monero_test_utils import MoneroTestUtils +from .test_utils import TestUtils + +logger: logging.Logger = logging.getLogger(__name__) class PrintHeight(ABC): @classmethod def print(cls) -> None: - daemon = MoneroTestUtils.get_daemon_rpc() - print(f"Height: {daemon.get_height()}") + daemon = TestUtils.get_daemon_rpc() + logging.info(f"Height: {daemon.get_height()}") diff --git a/tests/utils/sample_connection_listener.py b/tests/utils/sample_connection_listener.py index c4f18de..56507a9 100644 --- a/tests/utils/sample_connection_listener.py +++ b/tests/utils/sample_connection_listener.py @@ -1,7 +1,11 @@ +import logging + from typing import Optional from typing_extensions import override from monero import MoneroConnectionManagerListener, MoneroRpcConnection +logger: logging.Logger = logging.getLogger(__name__) + class SampleConnectionListener(MoneroConnectionManagerListener): @@ -10,4 +14,4 @@ def __init__(self) -> None: @override def on_connection_changed(self, connection: Optional[MoneroRpcConnection]) -> None: - print(f"Connection changed to: {connection.uri if connection is not None else 'None'}") + logger.debug(f"Connection changed to: {connection.uri if connection is not None else 'None'}") diff --git a/tests/utils/monero_test_utils.py b/tests/utils/test_utils.py similarity index 88% rename from tests/utils/monero_test_utils.py rename to tests/utils/test_utils.py index 3c1f234..16d8c44 100644 --- a/tests/utils/monero_test_utils.py +++ b/tests/utils/test_utils.py @@ -1,9 +1,13 @@ +import logging + from typing import Any, Optional, Union from abc import ABC -from random import choices, shuffle +from random import shuffle from time import sleep, time from os.path import exists as path_exists from os import makedirs, getenv +from secrets import token_hex +from configparser import ConfigParser from monero import ( MoneroNetworkType, MoneroTx, MoneroUtils, MoneroWalletFull, MoneroRpcConnection, MoneroWalletConfig, MoneroDaemonRpc, MoneroWalletRpc, MoneroBlockHeader, MoneroBlockTemplate, @@ -20,24 +24,24 @@ from .tx_context import TxContext from .binary_block_context import BinaryBlockContext +logger: logging.Logger = logging.getLogger(__name__) + -class MoneroTestUtils(ABC): +class TestUtils(ABC): __test__ = False + _LOADED: bool = False - MONERO_BINS_DIR = "" """directory with monero binaries to test (monerod and monero-wallet-rpc)""" 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 - DAEMON_RPC_URI: str = "127.0.0.1:18081" + DAEMON_RPC_URI: str = "" """monero daemon rpc endpoint configuration (change per your configuration)""" DAEMON_RPC_USERNAME: str = "" DAEMON_RPC_PASSWORD: str = "" - DAEMON_LOCAL_PATH = MONERO_BINS_DIR + "/monerod" TEST_NON_RELAYS: bool = True LITE_MODE: bool = False TEST_NOTIFICATIONS: bool = True @@ -50,35 +54,32 @@ class MoneroTestUtils(ABC): 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" + WALLET_RPC_USERNAME: str = "" + WALLET_RPC_PASSWORD: str = "" + WALLET_RPC_ZMQ_DOMAIN: str = "" + WALLET_RPC_DOMAIN: str = "" + WALLET_RPC_URI: str = "" + WALLET_RPC_ZMQ_URI: str = "" + WALLET_RPC_ACCESS_CONTROL_ORIGINS: str = "" """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 - + WALLET_NAME: str = "" + WALLET_PASSWORD: str = "" + TEST_WALLETS_DIR: str = "" + WALLET_FULL_PATH: str = "" # test wallet constants MAX_FEE = 7500000*10000 NETWORK_TYPE: MoneroNetworkType = MoneroNetworkType.MAINNET REGTEST: bool = getenv("REGTEST") == "true" or True - LANGUAGE: str = "English" - SEED: str = "vortex degrees outbreak teeming gimmick school rounded tonic observant injury leech ought problems ahead upcoming ledge textbook cigar atrium trash dunes eavesdrop dullness evolved vortex" - ADDRESS: str = "48W9YHwPzRz9aPTeXCA6kmSpW6HsvmWx578jj3of2gT3JwZzwTf33amESBoNDkL6SVK34Q2HTKqgYbGyE1hBws3wCrcBDR2" - PRIVATE_VIEW_KEY: str = "e8c2288181bad9ec410d7322efd65f663c6da57bd1d1198636278a039743a600" - PRIVATE_SPEND_KEY: str = "be7a2f71097f146bdf0fb5bb8edfe2240a9767e15adee74d95af1b5a64f29a0c" - PUBLIC_SPEND_KEY: str = "b58d33a1dac23d334539cbed3657b69a5c967d6860357e24ab4d11899a312a6b" - PUBLIC_VIEW_KEY: str = "42e465bdcd00de50516f1c7049bbe26bd3c11195e8dae5cceb38bad92d484269" - FIRST_RECEIVE_HEIGHT: int = 171 + LANGUAGE: str = "" + SEED: str = "" + ADDRESS: str = "" + PRIVATE_VIEW_KEY: str = "" + PRIVATE_SPEND_KEY: str = "" + PUBLIC_SPEND_KEY: str = "" + PUBLIC_VIEW_KEY: str = "" + FIRST_RECEIVE_HEIGHT: int = 0 """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""" @@ -86,6 +87,61 @@ class MoneroTestUtils(ABC): """dummy server uri to remain offline because wallet2 connects to default if not given""" AUTO_CONNECT_TIMEOUT_MS: int = 3000 + @classmethod + def load_config(cls) -> None: + """ + Load utils configuration from tests/config/config.ini + """ + if cls._LOADED: + return + + parser = ConfigParser() + parser.read('tests/config/config.ini') + + # validate config + assert parser.has_section("general") + assert parser.has_section("daemon") + assert parser.has_section("wallet") + + # parse general config + cls.TEST_NON_RELAYS = parser.getboolean('general', 'test_non_relays') + cls.TEST_NOTIFICATIONS = parser.getboolean('general', 'test_notifications') + cls.LITE_MODE = parser.getboolean('general', 'lite_mode') + cls.AUTO_CONNECT_TIMEOUT_MS = parser.getint('general', 'auto_connect_timeout_ms') + cls.NETWORK_TYPE = cls.parse_network_type(parser.get('general', 'network_type')) + cls._LOADED = True + + # parse daemon config + cls.DAEMON_RPC_URI = parser.get('daemon', 'rpc_uri') + cls.DAEMON_RPC_USERNAME = parser.get('daemon', 'rpc_username') + cls.DAEMON_RPC_PASSWORD = parser.get('daemon', 'rpc_password') + + # parse wallet config + cls.WALLET_NAME = parser.get('wallet', 'name') + cls.WALLET_PASSWORD = parser.get('wallet', 'password') + cls.ADDRESS = parser.get('wallet', 'address') + cls.PRIVATE_VIEW_KEY = parser.get('wallet', 'private_view_key') + cls.PRIVATE_SPEND_KEY = parser.get('wallet', 'private_spend_key') + cls.PUBLIC_VIEW_KEY = parser.get('wallet', 'public_view_key') + cls.PUBLIC_SPEND_KEY = parser.get('wallet', 'public_spend_key') + cls.SEED = parser.get('wallet', 'seed') + cls.FIRST_RECEIVE_HEIGHT = parser.getint('wallet', 'first_receive_height') + cls.TEST_WALLETS_DIR = parser.get('wallet', 'dir') + cls.WALLET_FULL_PATH = cls.TEST_WALLETS_DIR + "/" + cls.WALLET_NAME + cls.LANGUAGE = parser.get('wallet', 'language') + cls.WALLET_RPC_DOMAIN = parser.get('wallet', 'rpc_domain') + cls.WALLET_RPC_PORT_START = parser.getint('wallet', 'rpc_port_start') + cls.WALLET_RPC_USERNAME = parser.get('wallet', 'rpc_username') + cls.WALLET_RPC_PASSWORD = parser.get('wallet', 'rpc_password') + cls.WALLET_RPC_ACCESS_CONTROL_ORIGINS = parser.get('wallet', 'rpc_access_control_origins') + cls.WALLET_RPC_ZMQ_ENABLED = parser.getboolean('wallet', 'rpc_zmq_enabled') + cls.WALLET_RPC_ZMQ_PORT_START = parser.getint('wallet', 'rpc_zmq_port_start') + cls.WALLET_RPC_ZMQ_BIND_PORT_START = parser.getint('wallet', 'rpc_zmq_bind_port_start') + cls.WALLET_RPC_ZMQ_DOMAIN = parser.get('wallet', 'rpc_zmq_domain') + cls.WALLET_RPC_URI = cls.WALLET_RPC_DOMAIN + ":" + str(cls.WALLET_RPC_PORT_START) + cls.WALLET_RPC_ZMQ_URI = "tcp:#" + cls.WALLET_RPC_ZMQ_DOMAIN + ":" + str(cls.WALLET_RPC_ZMQ_PORT_START) + cls.SYNC_PERIOD_IN_MS = parser.getint('wallet', 'sync_period_in_ms') + @classmethod def current_timestamp(cls) -> int: return round(time() * 1000) @@ -108,11 +164,11 @@ def network_type_to_str(cls, nettype: MoneroNetworkType) -> str: @classmethod def parse_network_type(cls, nettype: str) -> MoneroNetworkType: net = nettype.lower() - if net == "mainnet": + if net == "mainnet" or net == "main": return MoneroNetworkType.MAINNET - elif net == "testnet": + elif net == "testnet" or net == "test": return MoneroNetworkType.TESTNET - elif net == "stagenet": + elif net == "stagenet" or net == "stage": return MoneroNetworkType.STAGENET raise TypeError(f"Invalid network type provided: {str(nettype)}") @@ -167,7 +223,7 @@ def assert_is(cls, expr: Any, what: Any, message: str = "assertion failed"): @classmethod def get_random_string(cls, n: int = 25) -> str: - return ''.join(choices(cls.BASE58_ALPHABET, k=n)) + return token_hex(n) @classmethod def get_wallets(cls, wallet_type: str) -> list[MoneroWallet]: @@ -437,13 +493,11 @@ def test_block_header(cls, header: MoneroBlockHeader, is_full: bool): cls.assert_not_none(header.nonce) if header.nonce == 0: # TODO (monero-project): why is header nonce 0? - print(f"WARNING: header nonce is 0 at height {header.height}") + logger.warning(f"header nonce is 0 at height {header.height}") 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 @@ -675,7 +729,7 @@ def test_known_peer(cls, peer: Optional[MoneroPeer], from_connection: bool): 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}") + logger.warning(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) @@ -735,7 +789,6 @@ def test_info(cls, info: MoneroDaemonInfo): 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) @@ -949,14 +1002,25 @@ def get_confirmed_tx_hashes(cls, daemon: MoneroDaemon) -> list[str]: return hashes @classmethod - def test_rpc_connection(cls, connection: Optional[MoneroRpcConnection], uri: Optional[str]) -> None: + def test_rpc_connection(cls, connection: Optional[MoneroRpcConnection], uri: Optional[str], connected: bool = True) -> None: assert connection is not None assert uri is not None assert len(uri) > 0 assert connection.uri == uri - - @classmethod - def test_get_blocks_range(cls, daemon: MoneroDaemonRpc, start_height: Optional[int], end_height: Optional[int], chain_height: int, chunked: bool, block_ctx: BinaryBlockContext) -> None: + assert connection.check_connection() == connected + assert connection.is_connected() == connected + assert connection.is_online() == connected + + @classmethod + def test_get_blocks_range( + cls, + daemon: MoneroDaemonRpc, + start_height: Optional[int], + end_height: Optional[int], + chain_height: int, + chunked: bool, + block_ctx: BinaryBlockContext + ) -> None: # fetch blocks by range real_start_height = 0 if start_height is None else start_height real_end_height = chain_height - 1 if end_height is None else end_height @@ -964,6 +1028,6 @@ def test_get_blocks_range(cls, daemon: MoneroDaemonRpc, start_height: Optional[i cls.assert_equals(real_end_height - real_start_height + 1, len(blocks)) # test each block - for i in range(len(blocks)): - cls.assert_equals(real_start_height + i, blocks[i].height) - cls.test_block(blocks[i], block_ctx) + for i, block in enumerate(blocks): + cls.assert_equals(real_start_height + i, block.height) + cls.test_block(block, block_ctx) diff --git a/tests/utils/wallet_sync_printer.py b/tests/utils/wallet_sync_printer.py index 1589f19..24d74ae 100644 --- a/tests/utils/wallet_sync_printer.py +++ b/tests/utils/wallet_sync_printer.py @@ -1,6 +1,10 @@ +import logging + from typing_extensions import override from monero import MoneroWalletListener +logger: logging.Logger = logging.getLogger(__name__) + class WalletSyncPrinter(MoneroWalletListener): @@ -16,5 +20,5 @@ def __init__(self, sync_resolution: float = 0.05) -> None: 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_progress({height}, {start_height}, {end_height}, {percent_done}, {message})" - print(msg) + logger.info(msg) self.next_increment += self.sync_resolution diff --git a/tests/utils/wallet_tx_tracker.py b/tests/utils/wallet_tx_tracker.py index 0a01773..444544c 100644 --- a/tests/utils/wallet_tx_tracker.py +++ b/tests/utils/wallet_tx_tracker.py @@ -1,8 +1,10 @@ +import logging from time import sleep - from monero import MoneroDaemon, MoneroWallet from .const import MINING_ADDRESS +logger: logging.Logger = logging.getLogger(__name__) + class WalletTxTracker: @@ -30,7 +32,7 @@ def wait_for_wallet_txs_to_clear_pool( # 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() @@ -53,14 +55,14 @@ def wait_for_wallet_txs_to_clear_pool( # 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)") + logger.info("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 Exception as e: # no problem - print(f"[!]: {str(e)}") + logger.warning(f"Error: {str(e)}") # sleep for a moment sleep(sync_period_ms) @@ -98,17 +100,17 @@ def wait_for_unlocked_balance( return unlocked_balance # 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 Exception as e: - print(f"[!]: {str(e)}") + logger.warning(f"Error: {str(e)}") # wait for unlocked balance // TODO: promote to MoneroWallet interface? - print("Waiting for unlocked balance") + logger.info("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) @@ -118,7 +120,7 @@ def wait_for_unlocked_balance( try: sleep(sync_period_ms) except Exception as e: - print(f"[!]: {str(e)}") + logger.warning(f"Error: {str(e)}") # stop mining if started if mining_started: