diff --git a/make_post_sell/lib/crypto_clients.py b/make_post_sell/lib/crypto_clients.py index ebacb05..422b429 100644 --- a/make_post_sell/lib/crypto_clients.py +++ b/make_post_sell/lib/crypto_clients.py @@ -96,6 +96,55 @@ class MoneroClient: res = self._call("get_height") return int(res.get("height", 0)) + def is_synced(self) -> bool: + """ + Check if the Monero wallet is fully synced. + Returns True if wallet height matches daemon height. + """ + try: + # Get wallet height + wallet_height = self.get_height() + + # Get daemon height via wallet RPC + daemon_info = self._call("get_info") + daemon_height = int(daemon_info.get("height", 0)) + + # Consider synced if within 2 blocks (to handle timing issues) + return abs(wallet_height - daemon_height) <= 2 + except Exception: + # If any RPC call fails, consider not synced + return False + + def get_sync_status(self) -> dict: + """ + Get detailed sync status information. + Returns dict with wallet_height, daemon_height, synced status. + """ + try: + wallet_height = self.get_height() + daemon_info = self._call("get_info") + daemon_height = int(daemon_info.get("height", 0)) + + synced = abs(wallet_height - daemon_height) <= 2 + sync_percentage = min(100.0, (wallet_height / max(daemon_height, 1)) * 100) + + return { + "wallet_height": wallet_height, + "daemon_height": daemon_height, + "synced": synced, + "sync_percentage": sync_percentage, + "blocks_behind": max(0, daemon_height - wallet_height) + } + except Exception as e: + return { + "wallet_height": 0, + "daemon_height": 0, + "synced": False, + "sync_percentage": 0.0, + "blocks_behind": 0, + "error": str(e) + } + class MockMoneroClient: """ @@ -147,6 +196,21 @@ class MockMoneroClient: def get_height(self) -> int: return int(self._data.get("height", 0)) + def is_synced(self) -> bool: + """Mock client is always considered synced for testing.""" + return True + + def get_sync_status(self) -> dict: + """Mock client returns synced status for testing.""" + height = self.get_height() + return { + "wallet_height": height, + "daemon_height": height, + "synced": True, + "sync_percentage": 100.0, + "blocks_behind": 0 + } + class DogecoinClient: """ @@ -259,6 +323,63 @@ class DogecoinClient: """Get network status information.""" return self._call("getnetworkinfo") + def getblockchaininfo(self) -> Dict[str, Any]: + """Get blockchain synchronization status.""" + return self._call("getblockchaininfo") + + def is_synced(self) -> bool: + """ + Check if the Dogecoin node is fully synced. + Returns True if verification progress is near 100%. + """ + try: + info = self.getblockchaininfo() + progress = float(info.get("verificationprogress", 0)) + # Consider synced if >99.9% to handle small timing issues + return progress >= 0.999 + except Exception: + # If RPC call fails, consider not synced + return False + + def get_sync_status(self) -> dict: + """ + Get detailed sync status information. + Returns dict with blocks, headers, sync progress, etc. + """ + try: + blockchain_info = self.getblockchaininfo() + + blocks = int(blockchain_info.get("blocks", 0)) + headers = int(blockchain_info.get("headers", 0)) + progress = float(blockchain_info.get("verificationprogress", 0)) + + synced = progress >= 0.999 + sync_percentage = progress * 100 + blocks_behind = max(0, headers - blocks) + + return { + "blocks": blocks, + "headers": headers, + "synced": synced, + "sync_percentage": sync_percentage, + "blocks_behind": blocks_behind, + "verification_progress": progress, + "pruned": blockchain_info.get("pruned", False), + "size_on_disk": blockchain_info.get("size_on_disk", 0) + } + except Exception as e: + return { + "blocks": 0, + "headers": 0, + "synced": False, + "sync_percentage": 0.0, + "blocks_behind": 0, + "verification_progress": 0.0, + "pruned": False, + "size_on_disk": 0, + "error": str(e) + } + class MockDogecoinClient: """Mock client for testing without a real Dogecoin node.""" @@ -331,6 +452,34 @@ class MockDogecoinClient: "connections": 8, } + def getblockchaininfo(self) -> Dict[str, Any]: + """Mock blockchain info - always synced for testing.""" + return { + "chain": "main", + "blocks": 5500000, + "headers": 5500000, + "verificationprogress": 1.0, + "pruned": False, + "size_on_disk": 2000000000 # 2GB + } + + def is_synced(self) -> bool: + """Mock client is always considered synced for testing.""" + return True + + def get_sync_status(self) -> dict: + """Mock client returns synced status for testing.""" + return { + "blocks": 5500000, + "headers": 5500000, + "synced": True, + "sync_percentage": 100.0, + "blocks_behind": 0, + "verification_progress": 1.0, + "pruned": False, + "size_on_disk": 2000000000 + } + def get_client_from_settings(settings) -> MoneroClient: """ diff --git a/make_post_sell/request_methods.py b/make_post_sell/request_methods.py index 685c6b6..09ac8d0 100644 --- a/make_post_sell/request_methods.py +++ b/make_post_sell/request_methods.py @@ -207,6 +207,20 @@ def includeme(config): except Exception: return False + def add_monero_synced(request): + """Check if Monero wallet is available, responding, AND fully synced.""" + if not request.monero_enabled: + return False + + try: + from ..lib.crypto_clients import get_client_from_settings + + client = get_client_from_settings(request.registry.settings) + # Check if wallet is synced (ready for payment processing) + return client.is_synced() + except Exception: + return False + def add_dogecoin_enabled(request): """Check if Dogecoin payments are enabled globally.""" try: @@ -234,6 +248,20 @@ def includeme(config): except Exception: return False + def add_dogecoin_synced(request): + """Check if Dogecoin node is available, responding, AND fully synced.""" + if not request.dogecoin_enabled: + return False + + try: + from ..lib.crypto_clients import get_dogecoin_client_from_settings + + client = get_dogecoin_client_from_settings(request.registry.settings) + # Check if node is synced (ready for payment processing) + return client.is_synced() + except Exception: + return False + # Register functions to app config as request methods. # To prevent multiple DB lookups, cache result with `reify=True`. config.add_request_method(add_debug_mode, "debug_mode", reify=True) @@ -269,10 +297,12 @@ def includeme(config): config.add_request_method( add_monero_rpc_available, "monero_rpc_available", reify=True ) + config.add_request_method(add_monero_synced, "monero_synced", reify=True) config.add_request_method(add_dogecoin_enabled, "dogecoin_enabled", reify=True) config.add_request_method( add_dogecoin_rpc_available, "dogecoin_rpc_available", reify=True ) + config.add_request_method(add_dogecoin_synced, "dogecoin_synced", reify=True) def add_has_xmr_refund_address(request): """Check if the current user has an XMR refund address configured.""" diff --git a/make_post_sell/templates/cart_checkout.j2 b/make_post_sell/templates/cart_checkout.j2 index 49be883..ebeccfa 100644 --- a/make_post_sell/templates/cart_checkout.j2 +++ b/make_post_sell/templates/cart_checkout.j2 @@ -123,6 +123,27 @@ {% endif %} {% endif %} + {# Show sync status messages for enabled but not synced crypto methods #} + {% if monero_enabled and not monero_synced and cart.requires_payment and cart.shop_product_dict|length == 1 %} +
+
+

+ ⏳ Monero (XMR) payments temporarily unavailable
+ The Monero wallet is still synchronizing with the blockchain. Please try again later. +

+
+ {% endif %} + + {% if dogecoin_enabled and not dogecoin_synced and cart.requires_payment and cart.shop_product_dict|length == 1 %} +
+
+

+ ⏳ Dogecoin (DOGE) payments temporarily unavailable
+ The Dogecoin node is still synchronizing with the blockchain. Please try again later. +

+
+ {% endif %} + {% if not stripe_enabled and not (monero_enabled and xmr_processor_enabled) and not (dogecoin_enabled and doge_processor_enabled) %}

No payment methods are enabled. Please contact the shop owner.

diff --git a/make_post_sell/views/cart.py b/make_post_sell/views/cart.py index 3426cbe..f3067a1 100644 --- a/make_post_sell/views/cart.py +++ b/make_post_sell/views/cart.py @@ -472,9 +472,9 @@ def cart_checkout(request): request.session.flash(msg) return HTTPFound("/billing") - # Check if shop has enabled XMR crypto processor + # Check if shop has enabled XMR crypto processor AND wallet is synced xmr_processor_enabled = False - if request.monero_enabled: + if request.monero_enabled and request.monero_synced: from ..models.crypto_processor import CryptoProcessor xmr_processor = ( @@ -488,9 +488,9 @@ def cart_checkout(request): ) xmr_processor_enabled = xmr_processor is not None - # Check if shop has enabled DOGE crypto processor + # Check if shop has enabled DOGE crypto processor AND node is synced doge_processor_enabled = False - if request.dogecoin_enabled: + if request.dogecoin_enabled and request.dogecoin_synced: from ..models.crypto_processor import CryptoProcessor doge_processor = ( @@ -530,8 +530,10 @@ def cart_checkout(request): "active_card": stripe_user_shop.active_card if stripe_user_shop else None, "stripe_enabled": request.stripe_enabled, "monero_enabled": request.monero_enabled, + "monero_synced": request.monero_synced, "xmr_processor_enabled": xmr_processor_enabled, "dogecoin_enabled": request.dogecoin_enabled, + "dogecoin_synced": request.dogecoin_synced, "doge_processor_enabled": doge_processor_enabled, "pending_crypto_quotes": pending_quotes, }