diff --git a/src/cpp/wallet/py_monero_wallet_model.cpp b/src/cpp/wallet/py_monero_wallet_model.cpp index 6065e3d..99a3f02 100644 --- a/src/cpp/wallet/py_monero_wallet_model.cpp +++ b/src/cpp/wallet/py_monero_wallet_model.cpp @@ -240,11 +240,13 @@ void PyMoneroTxWallet::init_sent(const monero::monero_tx_config &config, std::sh outgoing_transfer->m_tx = tx; if (config.m_subaddress_indices.size() == 1) { + // we know src subaddress indices iff request specifies 1 outgoing_transfer->m_subaddress_indices = config.m_subaddress_indices; } if (copy_destinations) { - for(const auto &conf_dest : config.m_destinations) { + auto conf_dests = config.get_normalized_destinations(); + for(const auto &conf_dest : conf_dests) { auto dest = std::make_shared(); conf_dest->copy(conf_dest, dest); outgoing_transfer->m_destinations.push_back(dest); @@ -462,15 +464,13 @@ void PyMoneroTxWallet::from_property_tree_with_transfer(const boost::property_tr } } - size_t num_destinations = config.m_destinations.size(); - if (num_destinations == 0 && config.m_address != boost::none){ - num_destinations++; - } + auto destinations = config.get_normalized_destinations(); + size_t num_destinations = destinations.size(); if (num_destinations != amounts_by_dest.size()) throw std::runtime_error("Expected destinations size equal to amounts by dest size"); - for(uint64_t i = 0; i < config.m_destinations.size(); i++) { + for(uint64_t i = 0; i < num_destinations; i++) { auto dest = std::make_shared(); - dest->m_address = config.m_destinations[i]->m_address; + dest->m_address = destinations[i]->m_address; dest->m_amount = amounts_by_dest[i]; outgoing_transfer->m_destinations.push_back(dest); } @@ -492,6 +492,10 @@ void PyMoneroTxWallet::from_property_tree_with_transfer(const boost::property_tr tx->m_is_outgoing = true; if (tx->m_outgoing_transfer != boost::none) { + // overwrite to avoid reconcile error TODO: remove after >18.3.1 when amounts_by_dest supported + if (tx->m_outgoing_transfer.get()->m_destinations.size() != 0) { + tx->m_outgoing_transfer.get()->m_destinations.clear(); + } tx->m_outgoing_transfer.get()->merge(tx->m_outgoing_transfer.get(), outgoing_transfer); } else tx->m_outgoing_transfer = outgoing_transfer; @@ -636,9 +640,9 @@ void PyMoneroTxWallet::merge_tx(const std::shared_ptr& tx, std void PyMoneroTxSet::from_property_tree(const boost::property_tree::ptree& node, const std::shared_ptr& set) { for (boost::property_tree::ptree::const_iterator it = node.begin(); it != node.end(); ++it) { std::string key = it->first; - if (key == std::string("multisig_txset")) set->m_multisig_tx_hex = it->second.data(); - else if (key == std::string("unsigned_txset")) set->m_unsigned_tx_hex = it->second.data(); - else if (key == std::string("signed_txset")) set->m_signed_tx_hex = it->second.data(); + if (key == std::string("multisig_txset") && !it->second.data().empty()) set->m_multisig_tx_hex = it->second.data(); + else if (key == std::string("unsigned_txset") && !it->second.data().empty()) set->m_unsigned_tx_hex = it->second.data(); + else if (key == std::string("signed_txset") && !it->second.data().empty()) set->m_signed_tx_hex = it->second.data(); } } diff --git a/src/cpp/wallet/py_monero_wallet_rpc.cpp b/src/cpp/wallet/py_monero_wallet_rpc.cpp index 1a84a25..37e4702 100644 --- a/src/cpp/wallet/py_monero_wallet_rpc.cpp +++ b/src/cpp/wallet/py_monero_wallet_rpc.cpp @@ -1851,9 +1851,10 @@ std::vector> PyMoneroWalletRpc::sweep_account( if (config.m_subaddress_indices.size() == 1) { transfer->m_subaddress_indices = config.m_subaddress_indices; } - auto destination = std::make_shared(); + auto destination = std::make_shared(); destination->m_address = destinations[0]->m_address; destination->m_amount = transfer->m_amount; + transfer->m_destinations.clear(); transfer->m_destinations.push_back(destination); tx->m_payment_id = config.m_payment_id; if (tx->m_unlock_time == boost::none) tx->m_unlock_time = 0; diff --git a/tests/test_monero_wallet_common.py b/tests/test_monero_wallet_common.py index 397adf3..f6889c5 100644 --- a/tests/test_monero_wallet_common.py +++ b/tests/test_monero_wallet_common.py @@ -423,6 +423,132 @@ def test_create_then_relay(self, wallet: MoneroWallet) -> None: def test_create_then_relay_split(self, wallet: MoneroWallet) -> None: WalletUtils.test_send_to_single(wallet, True, False) + # Can sweep individual outputs identified by their key images + @pytest.mark.skipif(TestUtils.TEST_RELAYS is False, reason="TEST_RELAYS disabled") + def test_sweep_outputs(self, wallet: MoneroWallet) -> None: + TestUtils.WALLET_TX_TRACKER.wait_for_txs_to_clear_pool(wallet) + + # test config + num_outputs: int = 3 + + # get outputs to sweep (not spent, unlocked, and amount >= fee) + query: MoneroOutputQuery = MoneroOutputQuery() + query.is_spent = False + tx_query: MoneroTxQuery = MoneroTxQuery() + tx_query.is_locked = False + query.set_tx_query(tx_query, True) + spendable_unlocked_outputs: list[MoneroOutputWallet] = wallet.get_outputs(query) + outputs_to_sweep: list[MoneroOutputWallet] = [] + for spendable_output in spendable_unlocked_outputs: + if len(outputs_to_sweep) >= num_outputs: + break + assert spendable_output.amount is not None + if spendable_output.amount > TxUtils.MAX_FEE: + outputs_to_sweep.append(spendable_output) + + assert len(outputs_to_sweep) >= num_outputs, "Wallet does not have enough sweepable outputs; run send tests" + + # sweep each output by key image + for output in outputs_to_sweep: + TxUtils.test_output_wallet(output) + assert output.is_spent is False + assert output.tx is not None + assert isinstance(output.tx, MoneroTxWallet) + assert output.tx.is_locked is False + assert output.amount is not None + + if output.amount <= TxUtils.MAX_FEE: + continue + + # sweep output to address + assert output.account_index is not None + assert output.subaddress_index is not None + assert output.key_image is not None + assert output.key_image.hex is not None + address: str = wallet.get_address(output.account_index, output.subaddress_index) + config: MoneroTxConfig = MoneroTxConfig() + config.address = address + config.key_image = output.key_image.hex + config.relay = True + tx: MoneroTxWallet = wallet.sweep_output(config) + + # test result tx + ctx: TxContext = TxContext() + ctx.wallet = wallet + ctx.config = config + ctx.config.can_split = False + ctx.is_send_response = True + ctx.is_sweep_response = True + ctx.is_sweep_output_response = True + TxUtils.test_tx_wallet(tx, ctx) + + # get outputs after sweeping + after_outputs: list[MoneroOutputWallet] = wallet.get_outputs() + + # swept outputs are now spent + for after_output in after_outputs: + assert after_output.key_image is not None + + for output in outputs_to_sweep: + assert output.key_image is not None + if output.key_image.hex == after_output.key_image.hex: + assert after_output.is_spent is True, "Output should be spent" + + # Can sweep individual outputs identified by their key images + @pytest.mark.skipif(TestUtils.TEST_RELAYS is False, reason="TEST_RELAYS disabled") + def test_sweep_dust_no_relay(self, wallet: MoneroWallet) -> None: + TestUtils.WALLET_TX_TRACKER.wait_for_txs_to_clear_pool(wallet) + + # sweep dust which returns empty list if no dust to sweep (dust does not exist after rct) + txs: list[MoneroTxWallet] = wallet.sweep_dust(False) + if len(txs) == 0: + return + + # test txs + ctx: TxContext = TxContext() + ctx.is_send_response = True + ctx.config = MoneroTxConfig() + ctx.config.relay = False + ctx.is_sweep_response = True + for tx in txs: + TxUtils.test_tx_wallet(tx, ctx) + + # relay txs + metadatas: list[str] = [] + for tx in txs: + assert tx.metadata is not None + metadatas.append(tx.metadata) + + tx_hashes: list[str] = wallet.relay_txs(metadatas) + assert len(tx_hashes) == len(txs) + for tx_hash in tx_hashes: + assert len(tx_hash) == 64 + + # fetch and test txs + tx_query: MoneroTxQuery = MoneroTxQuery() + tx_query.hashes = tx_hashes + txs = wallet.get_txs(tx_query) + ctx.config.relay = True + for tx in txs: + TxUtils.test_tx_wallet(tx, ctx) + + # Can sweep dust + @pytest.mark.skipif(TestUtils.TEST_RELAYS is False, reason="TEST_RELAYS disabled") + def test_sweep_dust(self, wallet: MoneroWallet) -> None: + TestUtils.WALLET_TX_TRACKER.wait_for_txs_to_clear_pool(wallet) + + # sweep dust which returns empty list if no dust to sweep (dust does not exist after rct) + txs: list[MoneroTxWallet] = wallet.sweep_dust(True) + + # test any txs + ctx: TxContext = TxContext() + ctx.wallet = wallet + ctx.config = None + ctx.is_send_response = True + ctx.is_sweep_response = True + for tx in txs: + TxUtils.test_tx_wallet(tx, ctx) + #endregion #region Non Relays Tests @@ -3202,6 +3328,181 @@ def test_get_default_fee_priority(self, wallet: MoneroWallet) -> None: #region Reset Tests + # Can sweep subaddresses + @pytest.mark.skipif(TestUtils.TEST_RESETS is False, reason="TEST_RESETS disabled") + def test_sweep_subaddresses(self, wallet: MoneroWallet) -> None: + TestUtils.WALLET_TX_TRACKER.wait_for_txs_to_clear_pool(wallet) + NUM_SUBADDRESSES_TO_SWEEP: int = 2 + + # collect subaddresses with balance and unlocked balance + subaddresses: list[MoneroSubaddress] = [] + subaddresses_balance: list[MoneroSubaddress] = [] + subaddresses_unlocked: list[MoneroSubaddress] = [] + for account in wallet.get_accounts(True): + if account.index == 0: + # skip default account + continue + for subaddress in account.subaddresses: + subaddresses.append(subaddress) + assert subaddress.balance is not None + if subaddress.balance > TxUtils.MAX_FEE: + subaddresses_balance.append(subaddress) + assert subaddress.unlocked_balance is not None + if subaddress.unlocked_balance > TxUtils.MAX_FEE: + subaddresses_unlocked.append(subaddress) + + # test requires at least one more subaddresses than the number being swept to verify it does not change + msg: str = f"Test requires balance in at least {NUM_SUBADDRESSES_TO_SWEEP + 1} subaddresses from non-default acccount; run send-to-multiple tests" + assert len(subaddresses_balance) >= NUM_SUBADDRESSES_TO_SWEEP + 1, msg + assert len(subaddresses_unlocked) >= NUM_SUBADDRESSES_TO_SWEEP + 1, "Wallet is waiting on unlocked funds" + + # sweep from first unlocked subaddresses + for i in range(NUM_SUBADDRESSES_TO_SWEEP): + # sweep unlocked account + unlocked_subaddress: MoneroSubaddress = subaddresses_unlocked[i] + config: MoneroTxConfig = MoneroTxConfig() + config.address = wallet.get_primary_address() + assert unlocked_subaddress.account_index is not None + assert unlocked_subaddress.index is not None + config.account_index = unlocked_subaddress.account_index + config.subaddress_indices.append(unlocked_subaddress.index) + config.relay = True + txs: list[MoneroTxWallet] = wallet.sweep_unlocked(config) + + # test transactions + assert len(txs) > 0 + for tx in txs: + assert tx.tx_set is not None + assert tx in tx.tx_set.txs + ctx: TxContext = TxContext() + ctx.wallet = wallet + ctx.config = config + ctx.is_send_response = True + ctx.is_sweep_response = True + TxUtils.test_tx_wallet(tx, ctx) + + # assert unlocked balance is less than max fee + subaddress: MoneroSubaddress = wallet.get_subaddress(unlocked_subaddress.account_index, unlocked_subaddress.index) + assert subaddress.unlocked_balance is not None + assert subaddress.unlocked_balance < TxUtils.MAX_FEE + + # test subaddresses after sweeping + subaddresses_after: list[MoneroSubaddress] = [] + for account in wallet.get_accounts(True): + if account.index == 0: + # skip default account + continue + for subaddress in account.subaddresses: + subaddresses_after.append(subaddress) + + assert len(subaddresses) == len(subaddresses_after) + for i, subaddress_before in enumerate(subaddresses): + subaddress_after: MoneroSubaddress = subaddresses_after[i] + assert subaddress_after.unlocked_balance is not None + + # determine if subaddress was swept + swept: bool = False + for j in range(NUM_SUBADDRESSES_TO_SWEEP): + subaddress_unlocked: MoneroSubaddress = subaddresses_unlocked[j] + account_eq: bool = subaddress_unlocked.account_index == subaddress_before.account_index + subaddr_eq: bool = subaddress_unlocked.index == subaddress_before.index + if account_eq and subaddr_eq: + swept = True + break + + # assert unlocked balance is less than max fee if swept, unchanged otherwise + if swept: + assert subaddress_after.unlocked_balance < TxUtils.MAX_FEE + else: + assert subaddress_before.unlocked_balance == subaddress_after.unlocked_balance + + # Can sweep accounts + @pytest.mark.skipif(TestUtils.TEST_RESETS is False, reason="TEST_RESETS disabled") + def test_sweep_accounts(self, wallet: MoneroWallet) -> None: + TestUtils.WALLET_TX_TRACKER.wait_for_txs_to_clear_pool(wallet) + NUM_ACCOUNTS_TO_SWEEP: int = 1 + + # collect accounts with sufficient balance and unlocked balance to cover the fee + accounts: list[MoneroAccount] = wallet.get_accounts(True) + accounts_balance: list[MoneroAccount] = [] + accounts_unlocked: list[MoneroAccount] = [] + for account in accounts: + if account.index == 0: + # skip default account + continue + assert account.balance is not None + assert account.unlocked_balance is not None + if account.balance > TxUtils.MAX_FEE: + accounts_balance.append(account) + if account.unlocked_balance > TxUtils.MAX_FEE: + accounts_unlocked.append(account) + + # test requires at least one more accounts than the number being swept to verify it does not change + + msg: str = f"Test requires balance greater than the fee in at least {NUM_ACCOUNTS_TO_SWEEP + 1} non-default accounts; run send-to-multiple tests" + assert len(accounts_balance) >= NUM_ACCOUNTS_TO_SWEEP + 1, msg + assert len(accounts_unlocked) >= NUM_ACCOUNTS_TO_SWEEP + 1, "Wallet is waiting on unlocked funds" + + # sweep from first unlocked accounts + for i in range(NUM_ACCOUNTS_TO_SWEEP): + # sweep unlocked account + unlocked_account: MoneroAccount = accounts_unlocked[i] + assert unlocked_account.index is not None + config: MoneroTxConfig = MoneroTxConfig() + config.address = wallet.get_primary_address() + config.account_index = unlocked_account.index + config.relay = True + txs: list[MoneroTxWallet] = wallet.sweep_unlocked(config) + + # test transactions + assert len(txs) > 0 + for tx in txs: + ctx: TxContext = TxContext() + ctx.wallet = wallet + ctx.config = config + ctx.is_send_response = True + ctx.is_sweep_response = True + TxUtils.test_tx_wallet(tx, ctx) + assert tx.tx_set is not None + assert tx in tx.tx_set.txs + + # assert unlocked account balance less than max fee + account: MoneroAccount = wallet.get_account(unlocked_account.index) + + # test accounts after sweeping + accounts_after: list[MoneroAccount] = wallet.get_accounts(True) + assert len(accounts) == len(accounts_after) + for i, account_before in enumerate(accounts): + account_after: MoneroAccount = accounts_after[i] + assert account_after.unlocked_balance is not None + + # determine if account was swept + swept: bool = False + for j in range(NUM_ACCOUNTS_TO_SWEEP): + account_unlocked = accounts_unlocked[j] + if account_unlocked.index == account_before.index: + swept = True + break + + # assert unlocked balance is less than max fee if swept, unchanged otherwise + if swept: + assert account_after.unlocked_balance < TxUtils.MAX_FEE + else: + assert account_after.unlocked_balance == account_after.unlocked_balance + + # Can sweep the whole wallet by accounts + @pytest.mark.skipif(TestUtils.TEST_RESETS is False, reason="TEST_RESETS disabled") + def test_sweep_wallet_by_accounts(self, wallet: MoneroWallet) -> None: + IntegrationTestUtils.fund_wallet_and_wait_for_unlocked(wallet) + WalletUtils.test_sweep_wallet(wallet, None) + + # Can sweep the whole wallet by subaddresses + #@pytest.mark.skipif(TestUtils.TEST_RESETS is False, reason="TEST_RESETS disabled") + @pytest.mark.xfail(reason="TODO wallet2 error: No unlocked balance in the specified subaddress(es)") + def test_sweep_wallet_by_subaddresses(self, wallet: MoneroWallet) -> None: + IntegrationTestUtils.fund_wallet_and_wait_for_unlocked(wallet) + WalletUtils.test_sweep_wallet(wallet, True) + # Can scan transactions by id @pytest.mark.skipif(TestUtils.TEST_RESETS is False, reason="TEST_RESETS disabled") def test_scan_txs(self, wallet: MoneroWallet) -> None: diff --git a/tests/test_monero_wallet_keys.py b/tests/test_monero_wallet_keys.py index d095b98..ee4a1b6 100644 --- a/tests/test_monero_wallet_keys.py +++ b/tests/test_monero_wallet_keys.py @@ -367,7 +367,7 @@ def test_freeze_outputs(self, wallet: MoneroWallet) -> None: def test_get_outputs_with_query(self, wallet: MoneroWallet) -> None: return super().test_get_outputs_with_query(wallet) - @pytest.mark.xfail(reason="Keys-only wallet does not have enumerable set of subaddresses") + @pytest.mark.xfail(raises=RuntimeError, reason="Keys-only wallet does not have enumerable set of subaddresses") @override def test_input_key_images(self, wallet: MoneroWallet) -> None: return super().test_input_key_images(wallet) @@ -462,6 +462,41 @@ def test_get_reserve_proof_account(self, wallet: MoneroWallet) -> None: def test_view_only_and_offline_wallets(self, wallet: MoneroWallet) -> None: return super().test_view_only_and_offline_wallets(wallet) + @pytest.mark.not_supported + @override + def test_sweep_outputs(self, wallet: MoneroWallet) -> None: + return super().test_sweep_outputs(wallet) + + @pytest.mark.not_supported + @override + def test_sweep_dust_no_relay(self, wallet: MoneroWallet) -> None: + return super().test_sweep_dust_no_relay(wallet) + + @pytest.mark.not_supported + @override + def test_sweep_dust(self, wallet: MoneroWallet) -> None: + return super().test_sweep_dust(wallet) + + @pytest.mark.not_supported + @override + def test_sweep_subaddresses(self, wallet: MoneroWallet) -> None: + return super().test_sweep_subaddresses(wallet) + + @pytest.mark.not_supported + @override + def test_sweep_accounts(self, wallet: MoneroWallet) -> None: + return super().test_sweep_accounts(wallet) + + @pytest.mark.not_supported + @override + def test_sweep_wallet_by_accounts(self, wallet: MoneroWallet) -> None: + return super().test_sweep_wallet_by_accounts(wallet) + + @pytest.mark.not_supported + @override + def test_sweep_wallet_by_subaddresses(self, wallet: MoneroWallet) -> None: + return super().test_sweep_wallet_by_subaddresses(wallet) + #endregion #region Tests diff --git a/tests/test_monero_wallet_rpc.py b/tests/test_monero_wallet_rpc.py index a2e1b62..9dc552e 100644 --- a/tests/test_monero_wallet_rpc.py +++ b/tests/test_monero_wallet_rpc.py @@ -349,11 +349,6 @@ def test_subaddress_lookahead(self, wallet: MoneroWallet) -> None: def test_get_txs_with_payment_ids(self, wallet: MoneroWallet) -> None: return super().test_get_txs_with_payment_ids(wallet) - @pytest.mark.skip(reason="TODO Destination vectors are different") - @override - def test_subtract_fee_from(self, wallet: MoneroWallet) -> None: - return super().test_subtract_fee_from(wallet) - @pytest.mark.skip(reason="TODO wallet rpc can't find destinations in outgoing transfers") def test_check_tx_key(self, wallet: MoneroWallet) -> None: return super().test_check_tx_key(wallet) diff --git a/tests/utils/tx_utils.py b/tests/utils/tx_utils.py index 5d81b2b..dd56783 100644 --- a/tests/utils/tx_utils.py +++ b/tests/utils/tx_utils.py @@ -308,7 +308,7 @@ def test_tx_wallet(cls, tx: Optional[MoneroTxWallet], context: Optional[TxContex assert tx.is_outgoing is True cls.test_transfer(tx.outgoing_transfer, ctx) if ctx.is_sweep_response is True: - assert len(tx.outgoing_transfer.destinations) == 1 + assert len(tx.outgoing_transfer.destinations) == 1, f"Expected 1 tx, got {len(tx.outgoing_transfer.destinations)}" # TODO handle special cases else: assert len(tx.incoming_transfers) > 0 @@ -360,7 +360,7 @@ def test_tx_wallet(cls, tx: Optional[MoneroTxWallet], context: Optional[TxContex # test common attributes assert ctx.config is not None - config = ctx.config + config: MoneroTxConfig = ctx.config assert tx.is_confirmed is False cls.test_transfer(tx.outgoing_transfer, ctx) assert tx.ring_size == MoneroUtils.get_ring_size() @@ -391,10 +391,11 @@ def test_tx_wallet(cls, tx: Optional[MoneroTxWallet], context: Optional[TxContex # TODO: remove this after >18.3.1 when amounts_by_dest_list official logger.warning("Destinations not returned from split transactions") else: - subtract_fee_from_dests = len(config.subtract_fee_from) > 0 + subtract_fee_from_dests: bool = len(config.subtract_fee_from) > 0 if ctx.is_sweep_response is True: - assert len(config.destinations) == 1 - assert config.destinations[0].amount is None + dests: list[MoneroDestination] = config.get_normalized_destinations() + assert len(dests) == 1 + assert dests[0].amount is None if not subtract_fee_from_dests: assert tx.outgoing_transfer.amount == tx.outgoing_transfer.destinations[0].amount diff --git a/tests/utils/wallet_sweeper.py b/tests/utils/wallet_sweeper.py new file mode 100644 index 0000000..9c7b63d --- /dev/null +++ b/tests/utils/wallet_sweeper.py @@ -0,0 +1,116 @@ +from typing import Optional + +from monero import ( + MoneroWallet, MoneroSubaddress, + MoneroTxConfig, MoneroTxWallet, + MoneroOutputQuery, MoneroOutputWallet, + MoneroTxQuery +) + +from .tx_context import TxContext +from .assert_utils import AssertUtils +from .tx_utils import TxUtils +from .test_utils import TestUtils + + +class WalletSweeper: + """Wallet funds sweeper""" + + _wallet: MoneroWallet + """Test wallet instance to sweep funds from""" + _sweep_each_subaddress: Optional[bool] + """Indicates if each subaddress must be sweeped out""" + + def __init__(self, wallet: MoneroWallet, sweep_each_subaddress: Optional[bool]) -> None: + """ + Initialize a new wallet sweeper. + + :param MoneroWallet wallet: wallet to sweep funds from. + :param bool sweep_each_subaddress: sweep each wallet subaddress. + """ + self._wallet = wallet + self._sweep_each_subaddress = sweep_each_subaddress + + def _check_for_balance(self) -> None: + """ + Checks for subaddresses with enough unlocked balance for sweep test. + """ + # verify 2 subaddresses with enough unlocked balance to cover the fee + subaddresses_balance: list[MoneroSubaddress] = [] + subaddresses_unlocked: list[MoneroSubaddress] = [] + + for account in self._wallet.get_accounts(True): + for subaddress in account.subaddresses: + assert subaddress.balance is not None + assert subaddress.unlocked_balance is not None + if subaddress.balance > TxUtils.MAX_FEE: + subaddresses_balance.append(subaddress) + if subaddress.unlocked_balance > TxUtils.MAX_FEE: + subaddresses_unlocked.append(subaddress) + + assert len(subaddresses_balance) >= 2, "Test requires multiple accounts with a balance greater than the fee; run send to multiple first" + assert len(subaddresses_unlocked) >= 2, "Wallet is waiting on unlocked funds" + + def _check_outputs(self) -> None: + """ + Check for outputs amount after sweep. + """ + # all unspent, unlocked outputs must be less than fee + query: MoneroOutputQuery = MoneroOutputQuery() + query.is_spent = False + tx_query: MoneroTxQuery = MoneroTxQuery() + tx_query.is_locked = False + query.set_tx_query(tx_query, True) + spendable_outputs: list[MoneroOutputWallet] = self._wallet.get_outputs(query) + for spendable_output in spendable_outputs: + assert spendable_output.amount is not None + assert spendable_output.amount < TxUtils.MAX_FEE, f"Unspent output should have been swept\n{spendable_output.serialize()}" + + # all subaddress unlocked balances must be less than fee + for account in self._wallet.get_accounts(True): + for subaddress in account.subaddresses: + assert subaddress.unlocked_balance is not None + assert subaddress.unlocked_balance < TxUtils.MAX_FEE, "No subaddress should have more unlocked than the fee" + + def sweep(self) -> None: + """ + Sweep outputs from wallet. + """ + # cleanup and check balance + TestUtils.WALLET_TX_TRACKER.wait_for_txs_to_clear_pool(self._wallet) + self._check_for_balance() + + # sweep funds + destination: str = self._wallet.get_primary_address() + config: MoneroTxConfig = MoneroTxConfig() + config.address = destination + config.sweep_each_subaddress = self._sweep_each_subaddress + config.relay = True + copy: MoneroTxConfig = config.copy() + txs: list[MoneroTxWallet] = self._wallet.sweep_unlocked(config) + # config is unchanged + AssertUtils.assert_equals(config, copy) + for tx in txs: + assert tx.tx_set is not None + assert tx in tx.tx_set.txs + assert tx.tx_set.multisig_tx_hex is None + assert tx.tx_set.signed_tx_hex is None + assert tx.tx_set.unsigned_tx_hex is None + + assert len(txs) > 0 + for tx in txs: + assert tx.outgoing_transfer is not None + config = MoneroTxConfig() + config.address = destination + config.account_index = tx.outgoing_transfer.account_index + config.sweep_each_subaddress = self._sweep_each_subaddress + config.relay = True + + ctx: TxContext = TxContext() + ctx.wallet = self._wallet + ctx.config = config + ctx.is_send_response = True + ctx.is_sweep_response = True + TxUtils.test_tx_wallet(tx, ctx) + + self._check_outputs() diff --git a/tests/utils/wallet_utils.py b/tests/utils/wallet_utils.py index a7e0602..6209252 100644 --- a/tests/utils/wallet_utils.py +++ b/tests/utils/wallet_utils.py @@ -17,6 +17,7 @@ from .single_tx_sender import SingleTxSender from .to_multiple_tx_sender import ToMultipleTxSender from .from_multiple_tx_sender import FromMultipleTxSender +from .wallet_sweeper import WalletSweeper logger: logging.Logger = logging.getLogger("WalletUtils") @@ -368,3 +369,8 @@ def fund_wallet(cls, wallet: MoneroWallet, xmr_amount_per_address: float = 10, n logger.debug(f"Funded test wallet {primary_addr} with {sent_amount_xmr_str} in {len(txs)} txs") return txs + + @classmethod + def test_sweep_wallet(cls, wallet: MoneroWallet, sweep_each_subaddress: Optional[bool]) -> None: + sweeper: WalletSweeper = WalletSweeper(wallet, sweep_each_subaddress) + sweeper.sweep()