Add sync status checks for crypto payment methods
Crypto payment buttons now only appear when nodes are fully synced, preventing failed payments due to incomplete blockchain synchronization. - Add is_synced() and get_sync_status() methods to MoneroClient and DogecoinClient - Add request.monero_synced and request.dogecoin_synced request methods - Update payment visibility logic to require both enabled AND synced status - Add helpful sync status messages in checkout template when nodes are syncing - Prevent customer frustration from unsynchronized payment processing Payment method visibility now requires: 1. App globally enabled 2. Shop has cold wallet configured 3. RPC accessible and responsive 4. Node/wallet fully synchronized
This commit is contained in:
parent
171fc916b7
commit
fbc0b55dc3
4 changed files with 206 additions and 4 deletions
|
|
@ -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:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
<br/>
|
||||
<div style="background:#fff3cd; border:1px solid #ffeaa7; padding:10px; border-radius:5px; color:#856404;">
|
||||
<p style="margin:0; font-size:14px;">
|
||||
⏳ <strong>Monero (XMR) payments temporarily unavailable</strong><br/>
|
||||
The Monero wallet is still synchronizing with the blockchain. Please try again later.
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if dogecoin_enabled and not dogecoin_synced and cart.requires_payment and cart.shop_product_dict|length == 1 %}
|
||||
<br/>
|
||||
<div style="background:#fff3cd; border:1px solid #ffeaa7; padding:10px; border-radius:5px; color:#856404;">
|
||||
<p style="margin:0; font-size:14px;">
|
||||
⏳ <strong>Dogecoin (DOGE) payments temporarily unavailable</strong><br/>
|
||||
The Dogecoin node is still synchronizing with the blockchain. Please try again later.
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if not stripe_enabled and not (monero_enabled and xmr_processor_enabled) and not (dogecoin_enabled and doge_processor_enabled) %}
|
||||
<br/>
|
||||
<p>No payment methods are enabled. Please contact the shop owner.</p>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue