From f9159f58771c3333aff1ee5f46cb38fb366a824c Mon Sep 17 00:00:00 2001 From: jwinterm Date: Sun, 19 Apr 2026 15:51:45 -0400 Subject: [PATCH] fix: Wownero atomic unit is 10^11, not 10^12 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The codebase was forked from xmrcheckout and kept Monero's 10^12 atomic-unit constant everywhere. Wownero uses 11 decimal places, not 12. Effect: _xmr_to_atomic(amount) produced a required_atomic value 10× larger than what wallet-rpc actually reports for the subaddress. Invoices flagged total_paid_atomic correctly (that comes from the daemon) but is_paid = total_paid >= required was always false, so the reconciler never flipped status pending → payment_detected, webhook never fired, Medusa never saw the payment. Fix all four places: reconciler._xmr_to_atomic, btcpay_routes._xmr_to_atomic, btcpay_routes._atomic_to_xmr, btcpay_routes._format_xmr_fixed, routes.py invoice-status response. Extracted a WOW_ATOMIC_UNITS constant so this doesn't get lost again when we touch these files. --- api/app/btcpay_routes.py | 17 +++++++++++++---- api/app/reconciler.py | 10 +++++++++- api/app/routes.py | 6 ++++-- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/api/app/btcpay_routes.py b/api/app/btcpay_routes.py index 985644b..eefeaeb 100644 --- a/api/app/btcpay_routes.py +++ b/api/app/btcpay_routes.py @@ -198,8 +198,17 @@ def _after_expiration(invoice: Invoice) -> bool: return detected_at > expires_at +# Wownero uses 11 decimal places (atomic unit = 10^-11 WOW), NOT 12 like +# Monero. Forked from xmrcheckout and MUST be corrected wherever the constant +# appears. Using 10^12 here broke payment detection — required_atomic came +# out 10× larger than what wallet-rpc actually reports on the subaddress, so +# is_paid never flipped and invoices stayed pending forever. +WOW_ATOMIC_UNITS = Decimal("100000000000") # 10^11 +WOW_ATOMIC_QUANTIZE = Decimal("0.00000000001") # 11 decimal places + + def _xmr_to_atomic(amount: Decimal) -> int: - quantized = (Decimal(amount) * Decimal("1000000000000")).to_integral_value( + quantized = (Decimal(amount) * WOW_ATOMIC_UNITS).to_integral_value( rounding=ROUND_DOWN ) return int(quantized) @@ -218,15 +227,15 @@ def _btcpay_amount_currency(invoice: Invoice) -> tuple[str, str, dict[str, Any]] def _format_xmr_fixed(value: Decimal) -> str: """Format WOW amount with trailing zeros stripped.""" - formatted = f"{value.quantize(Decimal('0.000000000001')):.12f}" + formatted = f"{value.quantize(WOW_ATOMIC_QUANTIZE):.11f}" if "." in formatted: formatted = formatted.rstrip("0").rstrip(".") return formatted def _atomic_to_xmr(value: int) -> Decimal: - return (Decimal(value) / Decimal("1000000000000")).quantize( - Decimal("0.000000000001"), rounding=ROUND_DOWN + return (Decimal(value) / WOW_ATOMIC_UNITS).quantize( + WOW_ATOMIC_QUANTIZE, rounding=ROUND_DOWN ) diff --git a/api/app/reconciler.py b/api/app/reconciler.py index 0fea2c8..fd66c0b 100644 --- a/api/app/reconciler.py +++ b/api/app/reconciler.py @@ -417,8 +417,16 @@ def _safe_update_reconciler_status( +# Wownero uses 11 decimal places (atomic unit = 10^-11 WOW), NOT 12 like +# Monero. Using 10^12 here makes required_atomic 10× larger than actual paid +# amount, so is_paid never becomes true and invoices stay pending forever +# even after detection + confirmations. This entire codebase was forked from +# xmrcheckout and the constant must be corrected wherever it appears. +WOW_ATOMIC_UNITS = Decimal("100000000000") # 10^11 + + def _xmr_to_atomic(amount: Decimal) -> int: - quantized = (Decimal(amount) * Decimal("1000000000000")).to_integral_value( + quantized = (Decimal(amount) * WOW_ATOMIC_UNITS).to_integral_value( rounding=ROUND_DOWN ) return int(quantized) diff --git a/api/app/routes.py b/api/app/routes.py index 25c6b4a..4ee6633 100644 --- a/api/app/routes.py +++ b/api/app/routes.py @@ -302,8 +302,10 @@ def _public_invoice_status_response( metadata = invoice.metadata_json or {} update: dict[str, Any] = {} if invoice.total_paid_atomic is not None: - total_paid = (Decimal(invoice.total_paid_atomic) / Decimal("1000000000000")).quantize( - Decimal("0.000000000001"), rounding=ROUND_DOWN + # Wownero uses 11 decimal places (10^11), NOT 12 like Monero. Using + # 10^12 here under-reports amount_paid by a factor of 10. + total_paid = (Decimal(invoice.total_paid_atomic) / Decimal("100000000000")).quantize( + Decimal("0.00000000001"), rounding=ROUND_DOWN ) update["amount_paid_xmr"] = format_xmr_amount(total_paid) btcpay_data = metadata.get("btcpay") if isinstance(metadata, dict) else None