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.
This commit is contained in:
Russell Ballestrini 2025-09-23 13:46:45 -04:00
parent b2a47f6789
commit 171fc916b7
2 changed files with 82 additions and 10 deletions

View file

@ -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:

View file

@ -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()