From 4796d83eae3f2a98381ddeedd506fb03246c5196 Mon Sep 17 00:00:00 2001 From: Russell Ballestrini Date: Wed, 24 Sep 2025 13:09:38 -0400 Subject: [PATCH] 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) --- make_post_sell/lib/crypto_watcher.py | 12 +++---- make_post_sell/templates/crypto_checkout.j2 | 36 ++++++++++----------- make_post_sell/tests/test_crypto_watcher.py | 4 +-- 3 files changed, 24 insertions(+), 28 deletions(-) diff --git a/make_post_sell/lib/crypto_watcher.py b/make_post_sell/lib/crypto_watcher.py index b7a4b0f..9c597ea 100644 --- a/make_post_sell/lib/crypto_watcher.py +++ b/make_post_sell/lib/crypto_watcher.py @@ -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 diff --git a/make_post_sell/templates/crypto_checkout.j2 b/make_post_sell/templates/crypto_checkout.j2 index 4b2297e..577a608 100644 --- a/make_post_sell/templates/crypto_checkout.j2 +++ b/make_post_sell/templates/crypto_checkout.j2 @@ -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'; diff --git a/make_post_sell/tests/test_crypto_watcher.py b/make_post_sell/tests/test_crypto_watcher.py index 7c5a59a..b8682cc 100644 --- a/make_post_sell/tests/test_crypto_watcher.py +++ b/make_post_sell/tests/test_crypto_watcher.py @@ -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()