Fix auto-sweep bug for locked Monero funds

- Fix critical bug where payments were incorrectly marked as 'swept' when funds were still locked
- Auto-sweep now properly retries until funds unlock (after Monero's 10-block lock time)
- Fix test expectation for expired-refunded status (hyphen not underscore)
This commit is contained in:
Russell Ballestrini 2025-09-24 13:09:38 -04:00
parent 13eb2abbb2
commit 4796d83eae
3 changed files with 24 additions and 28 deletions

View file

@ -94,17 +94,13 @@ def auto_sweep_payment_xmr(client, crypto_payment: CryptoPayment, dbsession=None
# Calculate sweep amount for THIS SPECIFIC payment only
payment_amount_xmr = Decimal(crypto_payment.received_amount) / atomic_units
# If no balance, mark as already swept (another payment swept it)
# If no unlocked balance, funds are still locked (10-block lock time)
if unlocked_balance == 0:
logger.info(
f"No balance to sweep for payment {crypto_payment.id} - marking as swept"
f"No unlocked balance for payment {crypto_payment.id} - funds still locked, will retry later"
)
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
# Don't mark as swept - funds are just locked!
return False
# Sweep all available balance minus a small buffer for fees
# Use "sweep_all" instead of exact amount to handle fees automatically

View file

@ -157,28 +157,16 @@
cryptoAddress.style.display = 'none';
}
// Disable payment buttons but keep them visible for context
// Remove all payment buttons completely
if (buttonGrid) {
const copyAddrBtn = document.getElementById('copy-addr');
const copyAmtBtn = document.getElementById('copy-amt');
const walletLink = buttonGrid.querySelector('a[href^="monero:"], a[href^="dogecoin:"]');
[copyAddrBtn, copyAmtBtn].forEach(btn => {
if (btn) {
btn.disabled = true;
btn.style.opacity = '0.5';
btn.style.cursor = 'not-allowed';
btn.style.background = '#ccc';
btn.textContent = btn.textContent.replace('Copy', 'Payment Received');
}
});
if (walletLink) {
walletLink.style.opacity = '0.5';
walletLink.style.pointerEvents = 'none';
walletLink.style.background = '#ccc';
walletLink.textContent = 'Payment Received';
}
// Remove the buttons
if (copyAddrBtn) copyAddrBtn.remove();
if (copyAmtBtn) copyAmtBtn.remove();
if (walletLink) walletLink.remove();
}
}
@ -243,6 +231,12 @@
// Payment received - remove payment elements to prevent double-sending
removePaymentElements();
// Hide cancel button - can't cancel after receiving funds
const cancelBtn = document.getElementById('cancel-btn');
if (cancelBtn) {
cancelBtn.style.display = 'none';
}
// Hide countdown since payment is received
if (expiresEl) {
expiresEl.parentNode.style.display = 'none';
@ -364,9 +358,15 @@
// Handle different status transitions
if (currentStatus === 'received') {
// Payment received - remove payment elements to prevent double-sending
// but keep polling for confirmation updates
// and hide cancel button since funds were received
removePaymentElements();
// Hide cancel button - can't cancel after receiving funds
const cancelBtn = document.getElementById('cancel-btn');
if (cancelBtn) {
cancelBtn.style.display = 'none';
}
// Hide countdown since payment is received
if (expiresEl && expiresEl.parentNode) {
expiresEl.parentNode.style.display = 'none';

View file

@ -603,8 +603,8 @@ class CryptoWatcherIntegrationTests(unittest.TestCase):
process_payment(mock_request, payment, transfers, client=mock_client)
# Payment should be expired_refunded (refund succeeded)
self.assertEqual(payment.status, "expired_refunded")
# Payment should be expired-refunded (refund succeeded)
self.assertEqual(payment.status, "expired-refunded")
# Refund should be attempted
mock_rescue.handle_expired_payment.assert_called_once()