diff --git a/src/cpp/daemon/py_monero_daemon_model.cpp b/src/cpp/daemon/py_monero_daemon_model.cpp index 6c7e539..5f3fe40 100644 --- a/src/cpp/daemon/py_monero_daemon_model.cpp +++ b/src/cpp/daemon/py_monero_daemon_model.cpp @@ -631,10 +631,7 @@ void PyMoneroSubmitTxResult::from_property_tree(const boost::property_tree::ptre else if (key == std::string("too_big")) result->m_is_too_big = it->second.get_value(); else if (key == std::string("sanity_check_failed")) result->m_sanity_check_failed = it->second.get_value(); else if (key == std::string("credits")) result->m_credits = it->second.get_value(); - else if (key == std::string("top_hash")) { - std::string top_hash = it->second.data(); - if (!top_hash.empty()) result->m_top_block_hash = top_hash; - } + else if (key == std::string("top_hash") && !it->second.data().empty()) result->m_top_block_hash = it->second.data(); else if (key == std::string("tx_extra_too_big")) result->m_is_tx_extra_too_big = it->second.get_value(); else if (key == std::string("nonzero_unlock_time")) result->m_is_nonzero_unlock_time = 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 c407e61..df08c23 100644 --- a/src/cpp/wallet/py_monero_wallet_model.cpp +++ b/src/cpp/wallet/py_monero_wallet_model.cpp @@ -432,7 +432,7 @@ bool PyMoneroTxWallet::decode_rpc_type(const std::string &rpc_type, const std::s is_outgoing = true; tx->m_is_confirmed = false; tx->m_in_tx_pool = false; - tx->m_is_relayed = true; + tx->m_is_relayed = false; tx->m_relay = true; tx->m_is_failed = true; tx->m_is_miner_tx = false; diff --git a/tests/test_monero_wallet_common.py b/tests/test_monero_wallet_common.py index 514e169..f0c69e6 100644 --- a/tests/test_monero_wallet_common.py +++ b/tests/test_monero_wallet_common.py @@ -27,7 +27,8 @@ WalletType, IntegrationTestUtils, ViewOnlyAndOfflineWalletTester, WalletNotificationCollector, - MiningUtils, SendAndUpdateTxsTester + MiningUtils, SendAndUpdateTxsTester, + SyncWithPoolSubmitTester ) logger: logging.Logger = logging.getLogger("TestMoneroWalletCommon") @@ -264,6 +265,136 @@ def test_validate_inputs_sending_funds(self, wallet: MoneroWallet) -> None: if str(e) != "Invalid destination address": raise + # Can sync with txs in the pool sent from/to the same account + # TODO this test fails because wallet does not recognize pool tx sent from/to same account + @pytest.mark.skipif(TestUtils.TEST_RELAYS is False, reason="TEST_RELAYS disabled") + def test_sync_with_pool_same_accounts(self, daemon: MoneroDaemonRpc, wallet: MoneroWallet) -> None: + config: MoneroTxConfig = MoneroTxConfig() + config.account_index = 0 + config.address = wallet.get_primary_address() + config.amount = TxUtils.MAX_FEE * 5 + config.relay = True + self._test_sync_with_pool_submit(daemon, wallet, config) + + # Can sync with txs submitted and flushed from the pool + # This test takes at least 500 seconds to catchup failed txs + # (see wallet2::process_unconfirmed_transfer) + @pytest.mark.skipif(TestUtils.TEST_RELAYS is False, reason="TEST_RELAYS disabled") + @pytest.mark.skipif(TestUtils.LITE_MODE, reason="LITE_MODE enabled") + def test_sync_with_pool_submit_and_flush(self, daemon: MoneroDaemonRpc, wallet: MoneroWallet) -> None: + config: MoneroTxConfig = MoneroTxConfig() + config.account_index = 2 + config.address = wallet.get_primary_address() + config.amount = TxUtils.MAX_FEE * 5 + config.relay = False + self._test_sync_with_pool_submit(daemon, wallet, config) + + # Can sync with txs submitted and relayed from the pool + @pytest.mark.skipif(TestUtils.TEST_RELAYS is False, reason="TEST_RELAYS disabled") + @pytest.mark.skipif(TestUtils.LITE_MODE, reason="LITE_MODE enabled") + def test_sync_with_pool_submit_and_relay(self, daemon: MoneroDaemonRpc, wallet: MoneroWallet) -> None: + config: MoneroTxConfig = MoneroTxConfig() + config.account_index = 2 + config.address = wallet.get_primary_address() + config.amount = TxUtils.MAX_FEE * 5 + config.relay = True + self._test_sync_with_pool_submit(daemon, wallet, config) + + # can sync with txs relayed to the pool + @pytest.mark.skipif(TestUtils.TEST_RELAYS is False, reason="TEST_RELAYS disabled") + @pytest.mark.skipif(TestUtils.LITE_MODE, reason="LITE_MODE enabled") + def test_sync_with_pool_relay(self, daemon: MoneroDaemonRpc, wallet: MoneroWallet) -> None: + # wait one time for wallet txs in the pool to clear + TestUtils.WALLET_TX_TRACKER.wait_for_txs_to_clear_pool(wallet) + + # record wallet balances before submitting tx to pool + balance_before: int = wallet.get_balance() + unlocked_balance_before: int = wallet.get_unlocked_balance() + + # create config + config: MoneroTxConfig = MoneroTxConfig() + config.account_index = 2 + config.address = wallet.get_primary_address() + config.amount = TxUtils.MAX_FEE * 5 + + # create tx to relay + tx: MoneroTxWallet = wallet.create_tx(config) + assert tx.hash is not None + assert tx.full_hex is not None + assert tx.fee is not None + + # create another tx using same config which would be double spend + tx_double_spend: MoneroTxWallet = wallet.create_tx(config) + assert tx_double_spend.hash is not None + assert tx_double_spend.full_hex is not None + + # relay first tx directly to daemon + result: MoneroSubmitTxResult = daemon.submit_tx_hex(tx.full_hex) + assert result.is_good, "Transaction could not be submitted to the pool" + + # sync wallet which updates from pool + wallet.sync() + + # collect issues to report at end of test + issues: list[str] = [] + + # wallet should be aware of tx + fetched: MoneroTxWallet | None = wallet.get_tx(tx.hash) + assert fetched is not None, "Wallet should be aware of its tx in pool after syncing" + + # test wallet balances + if unlocked_balance_before == wallet.get_unlocked_balance(): + issues.append("unlocked balance should have decreased after relaying tx directly to the daemon") + + if balance_before == wallet.get_balance(): + issues.append("balance should have decreased after relaying tx directly to the daemon") + + expected_balance: int = balance_before - tx.fee + if expected_balance != wallet.get_balance(): + issues.append(f"expected balance after relaying tx directly to the daemon to be {expected_balance} but was {wallet.get_balance()}") + + # submit double spend tx + result_double_spend: MoneroSubmitTxResult = daemon.submit_tx_hex(tx_double_spend.full_hex) + if result_double_spend.is_good: + daemon.flush_tx_pool(tx_double_spend.hash) + issues.append("Tx submit result should have been double spend") + + # sync wallet which updates from pool + wallet.sync() + + # create tx using same config which is no longer double spend + tx2: MoneroTxWallet | None = None + try: + config_copy: MoneroTxConfig = config.copy() + config_copy.relay = True + tx2 = wallet.create_tx(config_copy) + except Exception as e: + logger.warning(e) + # TODO monero-project: this fails meaning wallet did not recognize tx relayed directly to pool + issues.append("creating and sending tx through wallet should succeed after syncing wallet with pool but creates a double spend") + + # submit the transaction to the pool and test + if tx2 is not None: + assert tx2.hash is not None + assert tx2.full_hex is not None + result2: MoneroSubmitTxResult = daemon.submit_tx_hex(tx2.full_hex, True) + if result2.is_double_spend: + issues.append("creating and submitting tx to daemon should succeed after syncing wallet with pool but was a double spend") + else: + assert result2.is_good + + wallet.sync() + if wallet.get_tx(tx2.hash) is None: + issues.append("wallet should be aware of tx created and relayed through it after syncing with pool") + + daemon.flush_tx_pool(tx2.hash) + + # should be no issues + for issue in issues: + logger.warning(issue) + + assert len(issues) == 0, f"test_sync_with_pool_relay has {len(issues)} issues" + # Can send to self @pytest.mark.skipif(TestUtils.TEST_RELAYS is False, reason="TEST_RELAYS disabled") def test_send_to_self(self, wallet: MoneroWallet) -> None: @@ -3814,6 +3945,11 @@ def _test_send_and_update_txs(cls, daemon: MoneroDaemonRpc, wallet: MoneroWallet tester: SendAndUpdateTxsTester = SendAndUpdateTxsTester(daemon, wallet, config) tester.test() + @classmethod + def _test_sync_with_pool_submit(cls, daemon: MoneroDaemonRpc, wallet: MoneroWallet, config: MoneroTxConfig) -> None: + tester: SyncWithPoolSubmitTester = SyncWithPoolSubmitTester(daemon, wallet, config) + tester.test() + #endregion #endregion diff --git a/tests/test_monero_wallet_keys.py b/tests/test_monero_wallet_keys.py index 5b2638c..2bdfb66 100644 --- a/tests/test_monero_wallet_keys.py +++ b/tests/test_monero_wallet_keys.py @@ -548,6 +548,26 @@ def test_update_locked_different_accounts(self, daemon: MoneroDaemonRpc, wallet: def test_update_locked_different_accounts_split(self, daemon: MoneroDaemonRpc, wallet: MoneroWallet) -> None: return super().test_update_locked_different_accounts_split(daemon, wallet) + @pytest.mark.not_supported + @override + def test_sync_with_pool_same_accounts(self, daemon: MoneroDaemonRpc, wallet: MoneroWallet) -> None: + return super().test_sync_with_pool_same_accounts(daemon, wallet) + + @pytest.mark.not_supported + @override + def test_sync_with_pool_submit_and_flush(self, daemon: MoneroDaemonRpc, wallet: MoneroWallet) -> None: + return super().test_sync_with_pool_submit_and_flush(daemon, wallet) + + @pytest.mark.not_supported + @override + def test_sync_with_pool_submit_and_relay(self, daemon: MoneroDaemonRpc, wallet: MoneroWallet) -> None: + return super().test_sync_with_pool_submit_and_relay(daemon, wallet) + + @pytest.mark.not_supported + @override + def test_sync_with_pool_relay(self, daemon: MoneroDaemonRpc, wallet: MoneroWallet) -> None: + return super().test_sync_with_pool_relay(daemon, wallet) + #endregion #region Tests diff --git a/tests/utils/__init__.py b/tests/utils/__init__.py index 7f2e963..bc60f03 100644 --- a/tests/utils/__init__.py +++ b/tests/utils/__init__.py @@ -32,6 +32,7 @@ from .sync_progress_tester import SyncProgressTester from .sync_seed_tester import SyncSeedTester from .send_and_update_txs_tester import SendAndUpdateTxsTester +from .sync_with_pool_submit_tester import SyncWithPoolSubmitTester __all__ = [ 'WalletUtils', @@ -67,5 +68,6 @@ 'WalletSyncTester', 'SyncProgressTester', 'SyncSeedTester', - 'SendAndUpdateTxsTester' + 'SendAndUpdateTxsTester', + 'SyncWithPoolSubmitTester' ] diff --git a/tests/utils/sync_with_pool_submit_tester.py b/tests/utils/sync_with_pool_submit_tester.py new file mode 100644 index 0000000..3c4e883 --- /dev/null +++ b/tests/utils/sync_with_pool_submit_tester.py @@ -0,0 +1,199 @@ +import logging + +from monero import ( + MoneroWallet, MoneroTxConfig, MoneroTxWallet, + MoneroSubmitTxResult, MoneroDaemon +) + +from .assert_utils import AssertUtils +from .test_utils import TestUtils + +logger: logging.Logger = logging.getLogger("SyncWithPoolSubmitTester") + + +class SyncWithPoolSubmitTester: + """Test wallet capability to sync transactions in the pool.""" + + daemon: MoneroDaemon + """Daemon instance.""" + wallet: MoneroWallet + """Test wallet instance.""" + config: MoneroTxConfig + """Transaction test configuration.""" + balance_before: int + """Wallet balance before test.""" + unlocked_balance_before: int + """Wallet unlocked balance before test.""" + txs_before: list[MoneroTxWallet] + """Wallet transactions before running test.""" + run_failing_code: bool + """Run failing monero core code.""" + + def __init__(self, daemon: MoneroDaemon, wallet: MoneroWallet, config: MoneroTxConfig, run_failing_core_code: bool = False) -> None: + """ + Initialize a new sync with poll submit tester. + + :param MoneroDaemon daemon: daemon test instance. + :param MoneroWallet wallet: wallet test instance. + :param MoneroTxConfig config: transaction config. + """ + self.daemon = daemon + self.wallet = wallet + self.config = config + self.balance_before = 0 + self.unlocked_balance_before = 0 + self.txs_before = [] + self.run_failing_code = run_failing_core_code + + def run_failing_core_code(self, config_no_relay: MoneroTxConfig) -> None: + """ + TODO monero-project this code fails wich indicates issues. + TODO monero-project sync txs from pool. + + :param MoneroTxConfig config_no_relay: Non-relay tx config. + """ + if not self.run_failing_code: + return + + # wallet balances should change + assert self.balance_before != self.wallet.get_balance(), "Wallet balance should revert to original after flushing tx from pool without relaying" + # TODO: test exact amounts, maybe in ux test + assert self.unlocked_balance_before != self.wallet.get_unlocked_balance(), "Wallet unlocked balance should revert to original after flushing tx from pool without relaying" + + # create tx using same config which is no longer double spend + tx2: MoneroTxWallet = self.wallet.create_tx(config_no_relay) + assert tx2.hash is not None + assert tx2.full_hex is not None + result2: MoneroSubmitTxResult = self.daemon.submit_tx_hex(tx2.full_hex) + + # test result and flush on finally + try: + if result2.is_double_spend: + raise Exception(f"Wallet created double spend transaction after syncing with the pool: {result2}") + + assert result2.is_good + self.wallet.sync() + # wallet is aware of tx2 + fetched = self.wallet.get_tx(tx2.hash) + assert fetched is not None and fetched.is_failed is False, "Submitted tx should not be null or failed" + finally: + self.daemon.flush_tx_pool(tx2.hash) + + def setup(self) -> None: + """Setup test.""" + # wait for txs to confirm and for sufficient unlocked balance + # TODO monero-project: update from pool does not prevent creating double spend tx + TestUtils.WALLET_TX_TRACKER.wait_for_txs_to_clear_wallets([self.wallet]) + assert len(self.config.subaddress_indices) == 0 + assert self.config.account_index is not None + TestUtils.WALLET_TX_TRACKER.wait_for_unlocked_balance(self.wallet, self.config.account_index, None, self.config.amount) + + # rescan spent outputs for reliable before/after state + self.wallet.rescan_spent() + + # record wallet balances before submitting tx to pool + self.balance_before = self.wallet.get_balance() + self.unlocked_balance_before = self.wallet.get_unlocked_balance() + self.txs_before = self.wallet.get_txs() + logger.debug(f"balance {self.balance_before}, unlocked balance {self.unlocked_balance_before}") + + def flush_tx(self, tx_hash: str) -> None: + """ + Flush created test tx. + + :param str tx_hash: hash of the transaction to flush. + """ + if self.config.relay is True: + return + + # flush the tx from the pool + self.daemon.flush_tx_pool(tx_hash) + logger.debug(f"flushed tx from pool: {tx_hash}") + + # clear tx from wallet + TestUtils.WALLET_TX_TRACKER.wait_for_txs_to_clear_wallets([self.wallet]) + + # wallet should see failed state + fetched = self.wallet.get_tx(tx_hash) + if fetched is not None: + assert fetched.is_failed, "Flushed tx should be failed" + assert not fetched.in_tx_pool, "Flushed tx should not be in pool" + assert not fetched.is_relayed, "Flushed tx should not be relayed" + + # wallet txs are restored + txs_after: list[MoneroTxWallet] = self.wallet.get_txs() + for tx_before in self.txs_before: + if tx_before.hash != tx_hash: + continue + found: bool = False + for tx_after in txs_after: + if tx_before.hash != tx_after.hash: + continue + # transfer fields which can update with confirmations + # TODO: merge instead? + tx_before.num_confirmations = tx_after.num_confirmations + tx_before.is_locked = tx_after.is_locked + + AssertUtils.assert_equals(tx_before, tx_after) + found = True + + assert found, f"Tx {tx_before.hash} not found after flushing tx from pool without relaying" + + # wallet balance should be restored + wallet_balance: int = self.wallet.get_balance() + logger.debug(f"New balance {wallet_balance}") + assert self.balance_before == wallet_balance, "Wallet balance should be same as original since tx was flushed and not relayed" + + def test(self) -> None: + """Run test.""" + self.setup() + # create tx but do not relay + config_no_relay: MoneroTxConfig = self.config.copy() + config_no_relay.relay = False + config_no_relay_copy: MoneroTxConfig = config_no_relay.copy() + tx: MoneroTxWallet = self.wallet.create_tx(config_no_relay) + assert tx.hash is not None + + # create tx using same config which is double spend + tx_double_spend = self.wallet.create_tx(config_no_relay) + assert tx_double_spend.hash is not None + assert tx_double_spend.full_hex is not None + + # test that config is unchanged + assert config_no_relay_copy != config_no_relay + AssertUtils.assert_equals(config_no_relay_copy, config_no_relay) + + # submit tx directly to the pool but do not relay + assert tx.full_hex is not None + result: MoneroSubmitTxResult = self.daemon.submit_tx_hex(tx.full_hex, True) + assert result.is_good, f"Transaction could not be submitted to the pool: {result}" + + # sync wallet wich checks pool + self.wallet.sync() + + # test result and flush on finally + try: + # test tx state + fetched: MoneroTxWallet | None = self.wallet.get_tx(tx.hash) + assert fetched is not None + assert fetched.is_failed is False, "Submitted tx should not be failed" + # TODO: relay flag is not correctly set, insufficient info comes from monero-wallet-rpc + #assert fetched.is_relayed is False, "Submitted tx should be same relayed as configuration" + assert fetched.in_tx_pool is True, "Submitted tx should be in the pool" + + # submit double spend tx + result_double_spend: MoneroSubmitTxResult = self.daemon.submit_tx_hex(tx_double_spend.full_hex, True) + if result_double_spend.is_good is True: + self.daemon.flush_tx_pool(tx_double_spend.hash) + raise Exception("Tx submit result should have been double spend") + + # relay if configured + if self.config.relay is True: + self.daemon.relay_tx_by_hash(tx.hash) + + # sync wallet which updates from pool + self.wallet.sync() + + self.run_failing_core_code(config_no_relay) + finally: + self.flush_tx(tx.hash) diff --git a/tests/utils/wallet_tx_tracker.py b/tests/utils/wallet_tx_tracker.py index 8b99c93..6dfd081 100644 --- a/tests/utils/wallet_tx_tracker.py +++ b/tests/utils/wallet_tx_tracker.py @@ -129,7 +129,7 @@ def _wait_for_txs_to_clear(self, clear_from_wallet: bool, wallets: list[MoneroWa logger.debug("Mining already active") # sleep for sync period - logger.debug(f"Waiting for {num_txs_in_pool} to confirm (it={num_it})...") + logger.debug(f"Waiting for {num_txs_in_pool} tx(s) to confirm (it={num_it})...") self._sleep() # stop mining if started mining