Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/cpp/wallet/py_monero_wallet_bindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@
*/
#include "py_monero_types.h"

// declared in monero-cpp's monero_wallet_full.cpp but not exposed by any header
namespace monero {
std::string get_default_ringdb_path(cryptonote::network_type nettype);
}

void py_monero_bind_wallet(py::module_& m, PyMoneroTypes& t) {
// monero_wallet_config
t.py_monero_wallet_config
Expand Down Expand Up @@ -1100,6 +1105,9 @@ void py_monero_bind_wallet(py::module_& m, PyMoneroTypes& t) {
.def_static("get_seed_languages", []() {
MONERO_CATCH_AND_RETHROW(monero_wallet_full::get_seed_languages());
}, py::call_guard<py::gil_scoped_release>())
.def_static("get_default_ringdb_path", [](monero_network_type nettype) {
MONERO_CATCH_AND_RETHROW(get_default_ringdb_path(static_cast<cryptonote::network_type>(nettype)));
}, py::arg("nettype"))
.def("get_keys_file_buffer", [](monero_wallet_full& self, std::string& password, bool view_only) -> py::bytes {
MONERO_CATCH_AND_RETHROW(py::bytes(self.get_keys_file_buffer(password, view_only)));
}, py::arg("password"), py::arg("view_only"))
Expand All @@ -1126,6 +1134,12 @@ void py_monero_bind_wallet(py::module_& m, PyMoneroTypes& t) {
.def("get_rpc_connection", [](monero_wallet_rpc& self) {
MONERO_CATCH_AND_RETHROW(self.get_rpc_connection());
}, py::call_guard<py::gil_scoped_release>())
.def("set_poll_period_in_ms", [](monero_wallet_rpc& self, uint64_t period_ms) {
MONERO_CATCH_AND_RETHROW(self.set_poll_period_in_ms(period_ms));
}, py::arg("period_ms"), py::call_guard<py::gil_scoped_release>())
.def("get_subaddress", [](monero_wallet_rpc& self, uint32_t account_idx, uint32_t subaddress_idx) {
MONERO_CATCH_AND_RETHROW(self.get_subaddress(account_idx, subaddress_idx));
}, py::arg("account_idx"), py::arg("subaddress_idx"), py::call_guard<py::gil_scoped_release>())
// this because of function hiding
.def("set_daemon_connection", [](PyMoneroWallet& self, const std::shared_ptr<monero_rpc_connection>& connection, const boost::optional<bool>& is_trusted) {
MONERO_CATCH_AND_RETHROW(self.set_daemon_connection(connection, is_trusted));
Expand Down
10 changes: 10 additions & 0 deletions src/python/monero_wallet_full.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@ class MoneroWalletFull(MoneroWallet):
"""
...

@staticmethod
def get_default_ringdb_path(nettype: MoneroNetworkType) -> str:
"""
Get the default ring database path for a given network type.

:param MoneroNetworkType nettype: the network type.
:returns str: the default ring database path.
"""
...

@staticmethod
def get_seed_languages() -> list[str]:
"""
Expand Down
8 changes: 8 additions & 0 deletions src/python/monero_wallet_rpc.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,14 @@ class MoneroWalletRpc(MoneroWallet):
"""Save and close the current wallet and stop the RPC server."""
...

def set_poll_period_in_ms(self, period_ms: int) -> None:
"""
Set the period between polls of the wallet for notifications.

:param int period_ms: the poll period in milliseconds.
"""
...

def get_seed_languages(self) -> list[str]:
"""
Get all supported wallet seed languages.
Expand Down
42 changes: 41 additions & 1 deletion tests/test_monero_daemon_interface.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import pytest
import logging

from monero import MoneroDaemon, MoneroBan
from monero import MoneroDaemon, MoneroBan, MoneroDaemonListener
from utils import BaseTestClass

logger: logging.Logger = logging.getLogger("TestMoneroDaemonInterface")
Expand All @@ -21,10 +21,22 @@ def daemon(self) -> MoneroDaemon:

# Test interface calls

@pytest.mark.not_supported
def test_add_listener(self, daemon: MoneroDaemon) -> None:
daemon.add_listener(MoneroDaemonListener())

@pytest.mark.not_supported
def test_remove_listener(self, daemon: MoneroDaemon) -> None:
daemon.remove_listener(MoneroDaemonListener())

@pytest.mark.not_supported
def test_remove_listeners(self, daemon: MoneroDaemon) -> None:
daemon.remove_listeners()

@pytest.mark.not_supported
def test_get_listeners(self, daemon: MoneroDaemon) -> None:
daemon.get_listeners()

@pytest.mark.not_supported
def test_get_version(self, daemon: MoneroDaemon) -> None:
daemon.get_version()
Expand Down Expand Up @@ -261,6 +273,30 @@ def test_set_peer_ban(self, daemon: MoneroDaemon) -> None:
def test_get_peer_ban(self, daemon: MoneroDaemon) -> None:
daemon.get_peer_ban("")

@pytest.mark.not_supported
def test_get_download_limit(self, daemon: MoneroDaemon) -> None:
daemon.get_download_limit()

@pytest.mark.not_supported
def test_set_download_limit(self, daemon: MoneroDaemon) -> None:
daemon.set_download_limit(100)

@pytest.mark.not_supported
def test_reset_download_limit(self, daemon: MoneroDaemon) -> None:
daemon.reset_download_limit()

@pytest.mark.not_supported
def test_get_upload_limit(self, daemon: MoneroDaemon) -> None:
daemon.get_upload_limit()

@pytest.mark.not_supported
def test_set_upload_limit(self, daemon: MoneroDaemon) -> None:
daemon.set_upload_limit(100)

@pytest.mark.not_supported
def test_reset_upload_limit(self, daemon: MoneroDaemon) -> None:
daemon.reset_upload_limit()

@pytest.mark.not_supported
def test_start_mining(self, daemon: MoneroDaemon) -> None:
daemon.start_mining("", 1, False, False)
Expand Down Expand Up @@ -293,6 +329,10 @@ def test_pop_blocks(self, daemon: MoneroDaemon) -> None:
def test_flush_cache(self, daemon: MoneroDaemon) -> None:
daemon.flush_cache()

@pytest.mark.not_supported
def test_stop(self, daemon: MoneroDaemon) -> None:
daemon.stop()

@pytest.mark.not_supported
def test_set_bootstrap_daemon(self, daemon: MoneroDaemon) -> None:
daemon.set_bootstrap_daemon("")
Expand Down
68 changes: 68 additions & 0 deletions tests/test_monero_daemon_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,55 @@ def test_block_merge(self) -> None:
a.merge(b)
assert a.hex == "deadbeef"

def test_block_header_merge_dispatches_to_block_merge(self) -> None:
a: MoneroBlock = MoneroBlock()
a.height = 100
b: MoneroBlock = MoneroBlock()
b.height = 100
b.hex = "deadbeef" # a.hex is unset -> merge fills the gap

MoneroBlockHeader.merge(a, b)
assert a.hex == "deadbeef"

def test_block_merge_adopts_other_miner_tx(self) -> None:
a: MoneroBlock = MoneroBlock()
a.height = 100
# a.miner_tx is unset -> merge adopts other's miner tx and repoints its block at a

b: MoneroBlock = MoneroBlock()
b.height = 100
miner_tx: MoneroTx = MoneroTx()
miner_tx.hash = "a" * 64
miner_tx.is_miner_tx = True
b.miner_tx = miner_tx

a.merge(b)
assert a.miner_tx is not None
assert a.miner_tx is b.miner_tx
assert a.miner_tx.hash == "a" * 64
assert a.miner_tx.block is a

def test_block_merge_merges_distinct_miner_txs(self) -> None:
a: MoneroBlock = MoneroBlock()
a.height = 100
miner_a: MoneroTx = MoneroTx()
miner_a.hash = "a" * 64
miner_a.is_confirmed = True # required: tx merge() dereferences is_confirmed directly
a.miner_tx = miner_a

b: MoneroBlock = MoneroBlock()
b.height = 100
miner_b: MoneroTx = MoneroTx()
miner_b.hash = "a" * 64
miner_b.is_confirmed = True
miner_b.num_confirmations = 5 # miner_a's num_confirmations is unset -> merge fills the gap
b.miner_tx = miner_b

a.merge(b)
assert a.miner_tx is miner_a # kept in place, fields merged into it
assert a.miner_tx.num_confirmations == 5
assert a.miner_tx.block is a

def test_block_merge_txs_with_unset_hash_are_kept_distinct(self) -> None:
script: LiteralString = (
"import monero, sys\n"
Expand Down Expand Up @@ -785,6 +834,25 @@ def test_tx_copy(self) -> None:
assert copy is not tx
assert copy.serialize() == tx.serialize()

def test_tx_copy_deep_copies_inputs(self) -> None:
tx: MoneroTx = MoneroTx()
tx.hash = "a" * 64

key_image: MoneroKeyImage = MoneroKeyImage()
key_image.hex = "1" * 64
tx_input: MoneroOutput = MoneroOutput()
tx_input.tx = tx
tx_input.key_image = key_image
tx_input.amount = 1234
tx.inputs = [tx_input]

copy: MoneroTx = tx.copy()
assert len(copy.inputs) == 1
assert copy.inputs[0] is not tx_input
assert copy.inputs[0].key_image.hex == "1" * 64 # type: ignore
assert copy.inputs[0].amount == 1234
assert copy.inputs[0].tx is copy

def test_tx_merge(self) -> None:
a: MoneroTx = MoneroTx()
a.hash = "a" * 64
Expand Down
1 change: 1 addition & 0 deletions tests/test_monero_daemon_rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -1303,6 +1303,7 @@ def test_set_log_hash_rate(self, daemon: MoneroDaemonRpc) -> None:
# Can pop blocks from the top of the chain
@pytest.mark.skipif(Utils.TEST_NON_RELAYS is False, reason="TEST_NON_RELAYS disabled")
@pytest.mark.skipif(Utils.REGTEST is False, reason="REGTEST disabled")
@pytest.mark.flaky(reruns=5, reruns_delay=5, only_rerun=[])
def test_pop_blocks(self, daemon: MoneroDaemonRpc) -> None:
# stop mining so it doesn't race the pop, then restore the shared chain afterwards
MiningUtils.try_stop_mining(daemon)
Expand Down
10 changes: 7 additions & 3 deletions tests/test_monero_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
MoneroBlock, MoneroTxWallet, MoneroIncomingTransfer, MoneroOutputWallet,
MoneroTx
)
from utils import AddressBook, KeysBook, WalletUtils, BaseTestClass, WalletErrorUtils
from utils import AddressBook, KeysBook, WalletUtils, BaseTestClass

logger: logging.Logger = logging.getLogger("TestMoneroUtils")

Expand Down Expand Up @@ -435,7 +435,9 @@ def test_payment_uri_invalid_network_type(self, config: TestMoneroUtils.Config)
tx_config: MoneroTxConfig = WalletUtils.build_payment_uri_config(address)
with pytest.raises(Exception) as exc_info:
MoneroUtils.get_payment_uri(tx_config)
WalletErrorUtils.test_invalid_address_error(exc_info.value, address)

# get_payment_uri() wraps make_uri()'s error with context, unlike e.g. validate_address()
assert str(exc_info.value) == f"Cannot make URI from supplied parameters: wrong address: {address}"

# Test deprecated standalone payment id
def test_payment_uri_deprecated_payment_uri(self, config: TestMoneroUtils.Config) -> None:
Expand All @@ -444,7 +446,9 @@ def test_payment_uri_deprecated_payment_uri(self, config: TestMoneroUtils.Config
tx_config.payment_id = "03284e41c342f03603284e41c342f03603284e41c342f03603284e41c342f036"
with pytest.raises(Exception) as exc_info:
MoneroUtils.get_payment_uri(tx_config, MoneroNetworkType.TESTNET)
WalletErrorUtils.test_deprecated_payment_id_error(exc_info.value)

# get_payment_uri() wraps make_uri()'s error with context, unlike e.g. validate_address()
assert str(exc_info.value) == "Cannot make URI from supplied parameters: Standalone payment id deprecated, use integrated address instead"

# Can get version
def test_get_version(self) -> None:
Expand Down
33 changes: 32 additions & 1 deletion tests/test_monero_wallet_full.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import pytest
import logging
import os

from typing import Optional
from typing_extensions import override
Expand All @@ -16,7 +17,7 @@
TestUtils as Utils, StringUtils, BaseTestClass,
AssertUtils, WalletUtils, WalletType,
SyncSeedTester, SyncProgressTester,
WalletEqualityUtils, WalletErrorUtils
WalletEqualityUtils, WalletErrorUtils, MiningUtils
)
from test_monero_wallet_common import BaseTestMoneroWallet

Expand Down Expand Up @@ -656,6 +657,22 @@ def test_get_height_by_date_regtest(self, wallet: MoneroWallet) -> None:

#endregion

#region Notification Tests

# Can wait for the next block to be added and be notified of its height
@pytest.mark.skipif(Utils.TEST_NOTIFICATIONS is False, reason="TEST_NOTIFICATIONS disabled")
@pytest.mark.flaky(reruns=5, reruns_delay=10, only_rerun=[])
def test_wait_for_next_block(self, wallet: MoneroWalletFull) -> None:
height_before: int = wallet.get_height()
MiningUtils.try_start_mining()
try:
height: int = wallet.wait_for_next_block()
assert height > height_before
finally:
MiningUtils.try_stop_mining()

#endregion

#region Disabled Tests

@pytest.mark.skip(reason="TODO disabled because importing key images deletes corresponding incoming transfers: #5812")
Expand Down Expand Up @@ -695,6 +712,20 @@ def wallet(self) -> MoneroWalletFull:
"""Shared disconnected full wallet."""
return Utils.get_wallet_full_offline()

def test_get_default_ringdb_path_mainnet(self) -> None:
path: str = MoneroWalletFull.get_default_ringdb_path(MoneroNetworkType.MAINNET)
assert path.endswith(".shared-ringdb")
assert not path.endswith("testnet")
assert not path.endswith("stagenet")

def test_get_default_ringdb_path_testnet(self) -> None:
path: str = MoneroWalletFull.get_default_ringdb_path(MoneroNetworkType.TESTNET)
assert path.endswith(os.path.join(".shared-ringdb", "testnet"))

def test_get_default_ringdb_path_stagenet(self) -> None:
path: str = MoneroWalletFull.get_default_ringdb_path(MoneroNetworkType.STAGENET)
assert path.endswith(os.path.join(".shared-ringdb", "stagenet"))

# import_key_images guards unset key image fields
def test_import_key_images_hex_not_defined(self, wallet: MoneroWalletFull) -> None:
with pytest.raises(RuntimeError) as exc_info:
Expand Down
32 changes: 32 additions & 0 deletions tests/test_monero_wallet_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,30 @@ def test_relay_tx(self, wallet: MoneroWallet) -> None:
def test_relay_txs(self, wallet: MoneroWallet) -> None:
wallet.relay_txs([])

def test_relay_txs_none_tx_raises(self, wallet: MoneroWallet) -> None:
with pytest.raises(RuntimeError) as exc_info:
wallet.relay_txs([None]) # type: ignore
assert str(exc_info.value) == "Tx metadata is not initialized"

def test_relay_txs_uninitialized_metadata_raises(self, wallet: MoneroWallet) -> None:
tx: MoneroTxWallet = MoneroTxWallet()
with pytest.raises(RuntimeError) as exc_info:
wallet.relay_txs([tx])
assert str(exc_info.value) == "Tx metadata is not initialized"

def test_relay_txs_empty_metadata_raises(self, wallet: MoneroWallet) -> None:
tx: MoneroTxWallet = MoneroTxWallet()
tx.metadata = ""
with pytest.raises(RuntimeError) as exc_info:
wallet.relay_txs([tx])
assert str(exc_info.value) == "Tx metadata is not initialized"

@pytest.mark.not_supported
def test_relay_txs_with_metadata(self, wallet: MoneroWallet) -> None:
tx: MoneroTxWallet = MoneroTxWallet()
tx.metadata = StringUtils.get_random_string()
wallet.relay_txs([tx])

@pytest.mark.not_supported
def test_describe_tx_set(self, wallet: MoneroWallet) -> None:
wallet.describe_tx_set(MoneroTxSet())
Expand Down Expand Up @@ -397,6 +421,10 @@ def test_check_reserve_proof(self, wallet: MoneroWallet) -> None:
def test_get_tx_note(self, wallet: MoneroWallet) -> None:
wallet.get_tx_note("")

@pytest.mark.not_supported
def test_get_tx_notes(self, wallet: MoneroWallet) -> None:
wallet.get_tx_notes(["", ""])

@pytest.mark.not_supported
def test_set_tx_note(self, wallet: MoneroWallet) -> None:
wallet.set_tx_note("", "")
Expand Down Expand Up @@ -533,4 +561,8 @@ def test_save(self, wallet: MoneroWallet) -> None:
def test_close(self, wallet: MoneroWallet) -> None:
wallet.close()

@pytest.mark.not_supported
def test_is_closed(self, wallet: MoneroWallet) -> None:
wallet.is_closed()

#endregion
Loading
Loading