From 171fc916b71f1424c87e3e00fc2129ec10d9a99b Mon Sep 17 00:00:00 2001 From: Russell Ballestrini Date: Tue, 23 Sep 2025 13:46:45 -0400 Subject: [PATCH] Fix auto-sweep database persistence bug Auto-sweep functions were not persisting swept_tx_hash to database because the modified crypto_payment objects weren't being added to the session. - Add dbsession parameter to auto_sweep_payment functions - Add explicit dbsession.add(crypto_payment) calls when sweep succeeds - Update tests to verify swept_tx_hash is set and payment added to session - Fix existing tests to work with new function signatures This resolves the issue where swept payments had NULL swept_tx_hash despite successful fund transfers, which caused infinite processing loops. --- make_post_sell/lib/crypto_watcher.py | 20 ++++-- make_post_sell/tests/test_crypto_watcher.py | 72 +++++++++++++++++++-- 2 files changed, 82 insertions(+), 10 deletions(-) diff --git a/make_post_sell/lib/crypto_watcher.py b/make_post_sell/lib/crypto_watcher.py index 53dab26..8fca0e0 100644 --- a/make_post_sell/lib/crypto_watcher.py +++ b/make_post_sell/lib/crypto_watcher.py @@ -47,21 +47,21 @@ def get_coin_config(coin_type): return COIN_CONFIGS.get(coin_type, COIN_CONFIGS["XMR"]) -def auto_sweep_payment(client, crypto_payment: CryptoPayment): +def auto_sweep_payment(client, crypto_payment: CryptoPayment, dbsession=None): """Auto-sweep funds from a confirmed payment to the shop's cold wallet.""" logger.info(f"Starting auto-sweep check for payment {crypto_payment.id}") # Dispatch to coin-specific sweep function if crypto_payment.coin_type == "XMR": - return auto_sweep_payment_xmr(client, crypto_payment) + return auto_sweep_payment_xmr(client, crypto_payment, dbsession) elif crypto_payment.coin_type == "DOGE": - return auto_sweep_payment_doge(client, crypto_payment) + return auto_sweep_payment_doge(client, crypto_payment, dbsession) else: logger.error(f"Unsupported coin type for sweep: {crypto_payment.coin_type}") return False -def auto_sweep_payment_xmr(client, crypto_payment: CryptoPayment): +def auto_sweep_payment_xmr(client, crypto_payment: CryptoPayment, dbsession=None): """Auto-sweep XMR funds from a confirmed payment to the shop's cold wallet.""" logger.info(f"Starting XMR auto-sweep check for payment {crypto_payment.id}") @@ -102,6 +102,8 @@ def auto_sweep_payment_xmr(client, crypto_payment: CryptoPayment): crypto_payment.swept_amount = crypto_payment.received_amount crypto_payment.swept_tx_hash = "pooled_sweep" crypto_payment.swept_timestamp = now_timestamp() + if dbsession: + dbsession.add(crypto_payment) return True # Sweep all available balance minus a small buffer for fees @@ -135,6 +137,8 @@ def auto_sweep_payment_xmr(client, crypto_payment: CryptoPayment): crypto_payment.swept_tx_hash = tx_hash crypto_payment.swept_timestamp = now_timestamp() crypto_payment.swept_network_fee = fee + if dbsession: + dbsession.add(crypto_payment) logger.info( f"Auto-sweep successful for payment {crypto_payment.id}! TX: {tx_hash}, Swept: {total_swept} atomic units, Fee: {fee} atomic units" ) @@ -148,7 +152,7 @@ def auto_sweep_payment_xmr(client, crypto_payment: CryptoPayment): return False -def auto_sweep_payment_doge(client, crypto_payment: CryptoPayment): +def auto_sweep_payment_doge(client, crypto_payment: CryptoPayment, dbsession=None): """Auto-sweep DOGE funds from a confirmed payment to the shop's cold wallet.""" logger.info(f"Starting DOGE auto-sweep check for payment {crypto_payment.id}") @@ -185,6 +189,8 @@ def auto_sweep_payment_doge(client, crypto_payment: CryptoPayment): crypto_payment.swept_amount = crypto_payment.received_amount crypto_payment.swept_tx_hash = "pooled_sweep" crypto_payment.swept_timestamp = now_timestamp() + if dbsession: + dbsession.add(crypto_payment) return True # Sweep all balance minus a small buffer for fees @@ -215,6 +221,8 @@ def auto_sweep_payment_doge(client, crypto_payment: CryptoPayment): crypto_payment.swept_timestamp = now_timestamp() # Note: DOGE RPC doesn't return fee info easily, so we leave it None crypto_payment.swept_network_fee = None + if dbsession: + dbsession.add(crypto_payment) logger.info( f"DOGE auto-sweep successful for payment {crypto_payment.id}! TX: {tx_hash}, Swept: {sweep_amount} DOGE" @@ -644,7 +652,7 @@ def process_payment( logger.info( f"Funds unlocked ({unlocked_balance} atomic units) - attempting sweep for payment {crypto_payment.id}" ) - sweep_success = auto_sweep_payment(client, crypto_payment) + sweep_success = auto_sweep_payment(client, crypto_payment, env_request.dbsession) if sweep_success and crypto_payment.is_swept: logger.info(f"Payment {crypto_payment.id} swept successfully") else: diff --git a/make_post_sell/tests/test_crypto_watcher.py b/make_post_sell/tests/test_crypto_watcher.py index e4b1288..ed77736 100644 --- a/make_post_sell/tests/test_crypto_watcher.py +++ b/make_post_sell/tests/test_crypto_watcher.py @@ -659,8 +659,8 @@ class CryptoWatcherIntegrationTests(unittest.TestCase): process_payment(mock_request, payment, transfers, client=mock_client) - # Auto-sweep should be called after balance check - mock_sweep.assert_called_with(mock_client, payment) + # Auto-sweep should be called after balance check with dbsession + mock_sweep.assert_called_with(mock_client, payment, mock_request.dbsession) self.assertEqual(mock_sweep.call_count, 1) @@ -766,6 +766,41 @@ class AutoSweepTests(unittest.TestCase): result = auto_sweep_payment(mock_client, payment) self.assertFalse(result) + def test_auto_sweep_database_session_update(self): + """Test that auto-sweep properly updates payment object in database session.""" + mock_client = MagicMock() + mock_client._call.side_effect = [ + # First call - get_balance + {"unlocked_balance": 1000000000000}, # 1 XMR + # Second call - sweep_all + {"tx_hash": "sweep_tx_123", "amount_list": [1000000000000], "fee": 5000000000}, + ] + + # Create a mock dbsession + mock_dbsession = MagicMock() + + # Create a mock payment + payment = MagicMock() + payment.id = "test_payment_id" + payment.shop_sweep_to_address = "cold_wallet_address" + payment.account_index = 0 + payment.received_amount = 500000000000 # 0.5 XMR + payment.is_swept = False + payment.coin_type = "XMR" + + # Test the sweep with dbsession + result = auto_sweep_payment(mock_client, payment, mock_dbsession) + self.assertTrue(result) + + # Verify that swept_tx_hash was set + self.assertEqual(payment.swept_tx_hash, "sweep_tx_123") + self.assertEqual(payment.swept_amount, 1000000000000) + self.assertEqual(payment.swept_network_fee, 5000000000) + self.assertIsNotNone(payment.swept_timestamp) + + # Verify that the payment was added to the database session + mock_dbsession.add.assert_called_once_with(payment) + class DogecoinWatcherUnitTests(unittest.TestCase): """Unit tests for Dogecoin crypto_watcher functions.""" @@ -935,7 +970,7 @@ class DogecoinWatcherUnitTests(unittest.TestCase): result = auto_sweep_payment(mock_client, doge_payment) self.assertTrue(result) - mock_doge_sweep.assert_called_once_with(mock_client, doge_payment) + mock_doge_sweep.assert_called_once_with(mock_client, doge_payment, None) # Test XMR dispatch xmr_payment = MagicMock() @@ -948,7 +983,7 @@ class DogecoinWatcherUnitTests(unittest.TestCase): result = auto_sweep_payment(mock_client, xmr_payment) self.assertTrue(result) - mock_xmr_sweep.assert_called_once_with(mock_client, xmr_payment) + mock_xmr_sweep.assert_called_once_with(mock_client, xmr_payment, None) # Test unsupported coin type unknown_payment = MagicMock() @@ -957,6 +992,35 @@ class DogecoinWatcherUnitTests(unittest.TestCase): result = auto_sweep_payment(mock_client, unknown_payment) self.assertFalse(result) + def test_auto_sweep_payment_doge_database_session_update(self): + """Test that DOGE auto-sweep properly updates payment object in database session.""" + mock_client = MagicMock() + mock_client.getbalance.return_value = 10.0 # 10 DOGE + mock_client.sendtoaddress.return_value = "doge_sweep_tx_456" + + # Create a mock dbsession + mock_dbsession = MagicMock() + + # Create a mock payment + payment = MagicMock() + payment.id = "doge_payment_id" + payment.shop_sweep_to_address = "DColdWalletAddress123" + payment.received_amount = 500000000 # 5 DOGE in koinu + payment.is_swept = False + payment.coin_type = "DOGE" + + # Test the sweep with dbsession + result = auto_sweep_payment_doge(mock_client, payment, mock_dbsession) + self.assertTrue(result) + + # Verify that swept_tx_hash was set + self.assertEqual(payment.swept_tx_hash, "doge_sweep_tx_456") + self.assertEqual(payment.swept_amount, 990000000) # 9.9 DOGE in koinu (10 - 0.1 fee buffer) + self.assertIsNotNone(payment.swept_timestamp) + + # Verify that the payment was added to the database session + mock_dbsession.add.assert_called_once_with(payment) + if __name__ == "__main__": unittest.main()