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.
+
+ ⏳ Dogecoin (DOGE) payments temporarily unavailable
+ The Dogecoin node is still synchronizing with the blockchain. Please try again later.
+
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, }