Use dynamic fee estimation for crypto quote creation

- Add estimate_monero_fee_for_quote() using test transfer with do_not_relay
- Add estimate_dogecoin_fee_for_quote() using estimatesmartfee RPC
- Update Monero quote creation to use dynamic fees instead of hardcoded 0.0001 XMR
- Update Dogecoin quote creation to use dynamic fees instead of hardcoded 0.01 DOGE
- Maintains fallback to hardcoded fees if RPC calls fail
- Should provide more accurate quotes and reduce dust further
This commit is contained in:
Russell Ballestrini 2025-09-24 15:32:42 -04:00
parent 87471f2290
commit fa8ca79988

View file

@ -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 = {