diff --git a/make_post_sell/views/crypto.py b/make_post_sell/views/crypto.py index c630223..6b7e70e 100644 --- a/make_post_sell/views/crypto.py +++ b/make_post_sell/views/crypto.py @@ -16,6 +16,8 @@ from . import ( shop_is_ready_required, ) +from decimal import Decimal + import json import time import uuid as _uuid @@ -143,13 +145,33 @@ def crypto_xmr_start(request): if usd_per_xmr is None: raise RuntimeError(f"Failed to fetch USD/XMR rate: {last_err}") - # Compute piconero owed with transaction fee buffer + # Compute piconero owed with dynamic transaction fee estimation usd_total = float(invoice.total) xmr_amount = usd_total / usd_per_xmr + base_amount_piconero = int(xmr_amount * 1_000_000_000_000) - # Add small fee buffer (default 0.0001 XMR) to cover transaction costs - fee_buffer_xmr = float(settings.get("monero.fee_buffer", "0.0001")) - xmr_amount_with_fee = xmr_amount + fee_buffer_xmr + # Get dynamic fee estimate - we need the processor config first + from ..models.crypto_processor import CryptoProcessor + processor = ( + request.dbsession.query(CryptoProcessor) + .filter( + CryptoProcessor.shop_id == shop.id, + CryptoProcessor.coin_type == "XMR", + CryptoProcessor.enabled == True, + ) + .first() + ) + + if processor and processor.sweep_to_address: + # Use dynamic fee estimation with the actual sweep address + fee_buffer_xmr = estimate_monero_fee_for_quote( + settings, processor.sweep_to_address, base_amount_piconero + ) + else: + # Fallback to hardcoded fee if no processor configured yet + fee_buffer_xmr = Decimal(settings.get("monero.fee_buffer", "0.0001")) + + xmr_amount_with_fee = Decimal(str(xmr_amount)) + fee_buffer_xmr expected_piconero = int(xmr_amount_with_fee * 1_000_000_000_000) # Quote expiry - use shop-specific setting @@ -183,18 +205,7 @@ def crypto_xmr_start(request): else: confirmations_required = int(settings.get("monero.confirmations.high")) - # Get crypto processor configuration for this shop - from ..models.crypto_processor import CryptoProcessor - - processor = ( - request.dbsession.query(CryptoProcessor) - .filter( - CryptoProcessor.shop_id == shop.id, - CryptoProcessor.coin_type == "XMR", - CryptoProcessor.enabled == True, - ) - .first() - ) + # processor already queried above for fee estimation if not processor or processor.wallet_label is None: request.session.flash(("Shop has not configured Monero payments.", "error")) @@ -368,13 +379,13 @@ def crypto_doge_start(request): if usd_per_doge is None: raise RuntimeError(f"Failed to fetch USD/DOGE rate: {last_err}") - # Compute koinu (smallest unit) owed with transaction fee buffer + # Compute koinu (smallest unit) owed with dynamic transaction fee estimation usd_total = float(invoice.total) doge_amount = usd_total / usd_per_doge - # Add fee buffer (default 0.01 DOGE) to cover transaction costs - fee_buffer_doge = float(settings.get("dogecoin.fee_buffer", "0.01")) - doge_amount_with_fee = doge_amount + fee_buffer_doge + # Use dynamic fee estimation for more accurate quotes + fee_buffer_doge = estimate_dogecoin_fee_for_quote(settings) + doge_amount_with_fee = Decimal(str(doge_amount)) + fee_buffer_doge expected_koinu = int(doge_amount_with_fee * 100_000_000) # Convert to koinu # Quote expiry - use shop-specific setting @@ -846,6 +857,53 @@ def crypto_quotes_history(request): } +def estimate_monero_fee_for_quote(settings, shop_sweep_to_address, amount_piconero): + """Estimate Monero transaction fee for quote creation using dynamic RPC call.""" + try: + client = get_client_from_settings(settings) + + # We need a dummy subaddress for the test - use index 0 which should exist + # This is just for fee estimation, not actual transaction + test_transfer_result = client._call("transfer", { + "destinations": [{ + "address": shop_sweep_to_address, + "amount": amount_piconero + }], + "account_index": 0, # Use account 0 for estimation + "priority": 1, + "do_not_relay": True, # Don't broadcast, just estimate + "get_tx_metadata": True + }) + + estimated_fee_piconero = int(test_transfer_result.get("fee", 100000000000)) # Fallback to 0.0001 XMR + return Decimal(estimated_fee_piconero) / Decimal('1000000000000') # Convert to XMR + except Exception as e: + # Fallback to hardcoded fee if RPC call fails + fallback_fee = float(settings.get("monero.fee_buffer", "0.0001")) + return Decimal(str(fallback_fee)) + + +def estimate_dogecoin_fee_for_quote(settings): + """Estimate Dogecoin transaction fee for quote creation using estimatesmartfee.""" + try: + client = get_dogecoin_client_from_settings(settings) + + # Use estimatesmartfee to get current network fee estimate + fee_estimate_result = client._call("estimatesmartfee", [2]) # 2 block target + if fee_estimate_result and "feerate" in fee_estimate_result: + # feerate is in DOGE per KB, estimate transaction size as ~0.5KB + estimated_fee_doge = float(fee_estimate_result["feerate"]) * 0.5 + return Decimal(str(estimated_fee_doge)) + else: + # Fallback to hardcoded fee if estimatesmartfee fails + fallback_fee = float(settings.get("dogecoin.fee_buffer", "0.01")) + return Decimal(str(fallback_fee)) + except Exception as e: + # Fallback to hardcoded fee if RPC call fails + fallback_fee = float(settings.get("dogecoin.fee_buffer", "0.01")) + return Decimal(str(fallback_fee)) + + def get_payment_status_info(status): """Get display information for payment status.""" status_mapping = {