feat: automatically verify feerate

This commit is contained in:
tecnovert
2026-05-28 16:48:13 +02:00
parent 5099b9ebaa
commit 48ea745cc2
16 changed files with 230 additions and 38 deletions
+11
View File
@@ -4126,6 +4126,9 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
msg_buf.fee_rate_to = ci_to.make_int(fee_rate)
if swap_type == SwapTypes.XMR_SWAP:
ci_from.validateFeeRate(msg_buf.fee_rate_from)
ci_to.validateFeeRate(msg_buf.fee_rate_to)
xmr_offer = XmrOffer()
chain_a_ci = ci_to if reverse_bid else ci_from
@@ -6028,6 +6031,9 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
if offer.swap_type != SwapTypes.XMR_SWAP:
raise ValueError(f"TODO: Unknown swap type {offer.swap_type.name}")
ci_from.validateFeeRate(xmr_offer.a_fee_rate)
ci_to.validateFeeRate(xmr_offer.b_fee_rate)
if not (self.debug and extra_options.get("debug_skip_validation", False)):
self.validateBidValidTime(
offer.swap_type, coin_from, coin_to, valid_for_seconds
@@ -10033,6 +10039,10 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
ensure(len(offer_data.proof_signature) == 0, "Unexpected data")
ensure(len(offer_data.pkhash_seller) == 0, "Unexpected data")
ensure(len(offer_data.secret_hash) == 0, "Unexpected data")
ci_from.validateFeeRate(offer_data.fee_rate_from)
ci_to.validateFeeRate(offer_data.fee_rate_to)
else:
raise ValueError("Unknown swap type {}.".format(offer_data.swap_type))
@@ -10058,6 +10068,7 @@ class BasicSwap(BaseApp, BSXNetwork, UIApp):
# Check for sent
existing_offer = self.getOffer(offer_id, cursor=cursor)
if existing_offer is None:
bid_reversed: bool = (
offer_data.swap_type == SwapTypes.XMR_SWAP
and self.is_reverse_ads_bid(
+4 -1
View File
@@ -50,7 +50,7 @@ class CoinInterface:
def compareFeeRates(a, b) -> bool:
return abs(a - b) < 20
def __init__(self, network):
def __init__(self, network, **kwargs):
self.setDefaults()
self._network = network
self._mx_wallet = threading.Lock()
@@ -195,6 +195,9 @@ class AdaptorSigInterface:
class Secp256k1Interface(CoinInterface, AdaptorSigInterface):
def __init__(self, **kwargs):
super().__init__(**kwargs)
@staticmethod
def curve_type():
return Curves.secp256k1
+7 -2
View File
@@ -71,8 +71,13 @@ class BCHInterface(BTCInterface):
# TODO: BCH Watchonly: Remove when BCH watchonly works.
return True
def __init__(self, coin_settings, network, swap_client=None):
super(BCHInterface, self).__init__(coin_settings, network, swap_client)
def __init__(self, coin_settings, network, swap_client=None, **kwargs):
super().__init__(
coin_settings=coin_settings,
network=network,
swap_client=swap_client,
**kwargs,
)
self.swap_client = swap_client
def has_segwit(self) -> bool:
+11 -5
View File
@@ -27,6 +27,7 @@ from basicswap.basicswap_util import (
getVoutByScriptPubKey,
)
from basicswap.interface.base import Secp256k1Interface
from basicswap.interface.utils import FeeValidator
from basicswap.util import (
b2i,
ensure,
@@ -184,7 +185,7 @@ def extractScriptLockRefundScriptValues(script_bytes: bytes):
return pk1, pk2, csv_val, pk3
class BTCInterface(Secp256k1Interface):
class BTCInterface(FeeValidator, Secp256k1Interface):
_scantxoutset_lock = threading.Lock()
_MAX_SCANTXOUTSET_RETRIES = 3
@@ -278,8 +279,15 @@ class BTCInterface(Secp256k1Interface):
def depth_spendable() -> int:
return 0
def __init__(self, coin_settings, network, swap_client=None):
super().__init__(network)
def __init__(self, coin_settings, network, swap_client=None, **kwargs):
self._sc = swap_client
self._log = self._sc.log if self._sc and self._sc.log else logging
super().__init__(
coin_settings=coin_settings,
network=network,
swap_client=swap_client,
**kwargs,
)
self._rpc_host = coin_settings.get("rpchost", "127.0.0.1")
self._rpcport = coin_settings["rpcport"]
self._rpcauth = coin_settings["rpcauth"]
@@ -304,8 +312,6 @@ class BTCInterface(Secp256k1Interface):
self.setConfTarget(coin_settings["conf_target"])
self._use_segwit = coin_settings["use_segwit"]
self._connection_type = coin_settings["connection_type"]
self._sc = swap_client
self._log = self._sc.log if self._sc and self._sc.log else logging
self._expect_seedid_hex = None
self._altruistic = coin_settings.get("altruistic", True)
self._use_descriptors = coin_settings.get("use_descriptors", False)
+7 -2
View File
@@ -24,8 +24,13 @@ class DASHInterface(BTCInterface):
def coin_type():
return Coins.DASH
def __init__(self, coin_settings, network, swap_client=None):
super().__init__(coin_settings, network, swap_client)
def __init__(self, coin_settings, network, swap_client=None, **kwargs):
super().__init__(
coin_settings=coin_settings,
network=network,
swap_client=swap_client,
**kwargs,
)
self._wallet_passphrase = ""
self._have_checked_seed = False
+8 -9
View File
@@ -2,7 +2,7 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2024 tecnovert
# Copyright (c) 2024-2025 The Basicswap developers
# Copyright (c) 2024-2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
@@ -20,9 +20,8 @@ from basicswap.chainparams import Coins
from basicswap.contrib.test_framework.script import (
CScriptNum,
)
from basicswap.interface.base import (
Secp256k1Interface,
)
from basicswap.interface.base import Secp256k1Interface
from basicswap.interface.utils import FeeValidator
from basicswap.interface.btc import (
extractScriptLockScriptValues,
extractScriptLockRefundScriptValues,
@@ -181,7 +180,7 @@ def extract_sig_and_pk(sig_script: bytes) -> (bytes, bytes):
return sig, pk
class DCRInterface(Secp256k1Interface):
class DCRInterface(FeeValidator, Secp256k1Interface):
@staticmethod
def coin_type():
@@ -258,13 +257,13 @@ class DCRInterface(Secp256k1Interface):
def depth_spendable() -> int:
return 0
def __init__(self, coin_settings, network, swap_client=None):
super().__init__(network)
def __init__(self, coin_settings, network, swap_client=None, **kwargs):
self._sc = swap_client
self._log = self._sc.log if self._sc and self._sc.log else logging
super().__init__(coin_settings=coin_settings, network=network, **kwargs)
self._rpc_host = coin_settings.get("rpchost", "127.0.0.1")
self._rpcport = coin_settings["rpcport"]
self._rpcauth = coin_settings["rpcauth"]
self._sc = swap_client
self._log = self._sc.log if self._sc and self._sc.log else logging
self.rpc = make_rpc_func(self._rpcport, self._rpcauth, host=self._rpc_host)
if "walletrpcport" in coin_settings:
self._walletrpcport = coin_settings["walletrpcport"]
+7 -2
View File
@@ -32,8 +32,13 @@ class DOGEInterface(BTCInterface):
def xmr_swap_b_lock_spend_tx_vsize() -> int:
return 192
def __init__(self, coin_settings, network, swap_client=None):
super(DOGEInterface, self).__init__(coin_settings, network, swap_client)
def __init__(self, coin_settings, network, swap_client=None, **kwargs):
super().__init__(
coin_settings=coin_settings,
network=network,
swap_client=swap_client,
**kwargs,
)
def getScriptDest(self, script: bytearray) -> bytearray:
# P2SH
+7 -2
View File
@@ -38,8 +38,13 @@ class FIROInterface(BTCInterface):
def coin_type():
return Coins.FIRO
def __init__(self, coin_settings, network, swap_client=None):
super(FIROInterface, self).__init__(coin_settings, network, swap_client)
def __init__(self, coin_settings, network, swap_client=None, **kwargs):
super().__init__(
coin_settings=coin_settings,
network=network,
swap_client=swap_client,
**kwargs,
)
# No multiwallet support
self.rpc_wallet = make_rpc_func(
self._rpcport, self._rpcauth, host=self._rpc_host
+14 -4
View File
@@ -16,8 +16,13 @@ class LTCInterface(BTCInterface):
def coin_type():
return Coins.LTC
def __init__(self, coin_settings, network, swap_client=None):
super(LTCInterface, self).__init__(coin_settings, network, swap_client)
def __init__(self, coin_settings, network, swap_client=None, **kwargs):
super().__init__(
coin_settings=coin_settings,
network=network,
swap_client=swap_client,
**kwargs,
)
self._rpc_wallet_mweb = coin_settings.get("mweb_wallet_name", "mweb")
self.rpc_wallet_mweb = make_rpc_func(
self._rpcport,
@@ -265,8 +270,13 @@ class LTCInterfaceMWEB(LTCInterface):
def interface_type(self) -> int:
return Coins.LTC_MWEB
def __init__(self, coin_settings, network, swap_client=None):
super(LTCInterfaceMWEB, self).__init__(coin_settings, network, swap_client)
def __init__(self, coin_settings, network, swap_client=None, **kwargs):
super().__init__(
coin_settings=coin_settings,
network=network,
swap_client=swap_client,
**kwargs,
)
self._rpc_wallet = coin_settings.get("mweb_wallet_name", "mweb")
self.rpc_wallet = make_rpc_func(
self._rpcport, self._rpcauth, host=self._rpc_host, wallet=self._rpc_wallet
+7 -2
View File
@@ -73,8 +73,13 @@ class NAVInterface(BTCInterface):
def txoType():
return CTxOut
def __init__(self, coin_settings, network, swap_client=None):
super(NAVInterface, self).__init__(coin_settings, network, swap_client)
def __init__(self, coin_settings, network, swap_client=None, **kwargs):
super().__init__(
coin_settings=coin_settings,
network=network,
swap_client=swap_client,
**kwargs,
)
# No multiwallet support
self.rpc_wallet = make_rpc_func(
self._rpcport, self._rpcauth, host=self._rpc_host
+11 -2
View File
@@ -81,8 +81,17 @@ class PARTInterface(BTCInterface):
def txoType():
return CTxOutPart
def __init__(self, coin_settings, network, swap_client=None):
super().__init__(coin_settings, network, swap_client)
@staticmethod
def defaultMaxFeeRate() -> int:
return PARTInterface.COIN() // 2
def __init__(self, coin_settings, network, swap_client=None, **kwargs):
super().__init__(
coin_settings=coin_settings,
network=network,
swap_client=swap_client,
**kwargs,
)
self.setAnonTxRingSize(int(coin_settings.get("anon_tx_ring_size", 12)))
def use_tx_vsize(self) -> bool:
+7 -2
View File
@@ -10,8 +10,13 @@ from basicswap.contrib.test_framework.messages import CTxOut
class PassthroughBTCInterface(BTCInterface):
def __init__(self, coin_settings, network):
super().__init__(coin_settings, network)
def __init__(self, coin_settings, network, swap_client=None, **kwargs):
super().__init__(
coin_settings=coin_settings,
network=network,
swap_client=swap_client,
**kwargs,
)
self.txoType = CTxOut
self._network = network
self.blocks_confirmed = coin_settings["blocks_confirmed"]
+7 -2
View File
@@ -27,8 +27,13 @@ class PIVXInterface(BTCInterface):
def coin_type():
return Coins.PIVX
def __init__(self, coin_settings, network, swap_client=None):
super(PIVXInterface, self).__init__(coin_settings, network, swap_client)
def __init__(self, coin_settings, network, swap_client=None, **kwargs):
super().__init__(
coin_settings=coin_settings,
network=network,
swap_client=swap_client,
**kwargs,
)
# No multiwallet support
self.rpc_wallet = make_rpc_func(
self._rpcport, self._rpcauth, host=self._rpc_host
+111
View File
@@ -0,0 +1,111 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2026 The Basicswap developers
# Distributed under the MIT software license, see the accompanying
# file LICENSE or http://www.opensource.org/licenses/mit-license.php.
from basicswap.contrib.test_framework.messages import COIN
class FeeValidator:
@staticmethod
def defaultMaxFeeRate() -> int:
return COIN // 10
def makeIntFromSetting(
self, settings: dict, setting_name: str, default: int
) -> int:
# Return make_int(setting), or already integer default
if setting_name in settings:
return self.make_int(settings[setting_name])
return default
def __init__(self, **kwargs):
default_low_fee_conf_target: int = 24
default_low_fee_rate: int = 0
default_high_estimated_feerate_multiplier: float = 2.0
default_high_fee_rate: int = self.defaultMaxFeeRate()
if self._sc:
chain_client_settings = self._sc.getChainClientSettings(
self.coin_type()
) # basicswap.json
settings = self._sc.settings
default_low_fee_conf_target = int(
settings.get("low_fee_conf_target", default_low_fee_conf_target)
)
default_low_fee_rate = self.makeIntFromSetting(
settings, "low_feerate", default_low_fee_rate
)
default_high_estimated_feerate_multiplier = float(
settings.get(
"high_estimated_feerate_multiplier",
default_high_estimated_feerate_multiplier,
)
)
default_high_fee_rate = self.makeIntFromSetting(
settings, "high_feerate", default_high_fee_rate
)
else:
if kwargs.get("network") != "regtest":
raise ValueError("swapclient unset")
chain_client_settings = {}
self._low_fee_conf_target = int(
chain_client_settings.get(
"low_fee_conf_target", default_low_fee_conf_target
)
)
self._low_feerate = self.makeIntFromSetting(
chain_client_settings, "low_feerate", default_low_fee_rate
)
# Set below 1.0 to disable estimating the max feerate and use max_feerate
self._high_estimated_feerate_multiplier = float(
chain_client_settings.get(
"high_estimated_feerate_multiplier",
default_high_estimated_feerate_multiplier,
)
)
self._high_feerate = self.makeIntFromSetting(
chain_client_settings, "high_feerate", default_high_fee_rate
)
super().__init__(**kwargs)
def validateFeeRate(self, feerate: int) -> None:
if self._low_feerate > 0:
min_feerate_src = "set_value"
min_feerate = self._low_feerate
else:
min_feerate, min_feerate_src = self.get_fee_rate(self._low_fee_conf_target)
min_feerate = self.make_int(min_feerate)
if self._high_estimated_feerate_multiplier >= 1.0:
max_feerate, max_feerate_src = self.get_fee_rate()
max_feerate = (
self.make_int(max_feerate) * self._high_estimated_feerate_multiplier
)
else:
max_feerate_src = "set_value"
max_feerate = self._high_feerate
if max_feerate_src in ("estimatesmartfee", "electrum"):
if max_feerate > self._high_feerate:
max_feerate_src = "clamped_to_set_value"
max_feerate = self._high_feerate
self._log.debug(
f"Verify {self.ticker()} fee rate {feerate}, min {min_feerate} {min_feerate_src}, max {max_feerate} {max_feerate_src}"
)
if feerate < min_feerate:
err_msg: str = (
f"Fee rate too low, {feerate} < {min_feerate}, {min_feerate_src}"
)
self._log.error(err_msg)
raise ValueError(err_msg)
if feerate > max_feerate:
err_msg: str = (
f"Fee rate too high, {feerate} > {max_feerate}, {max_feerate_src}"
)
self._log.error(err_msg)
raise ValueError(err_msg)
+10 -2
View File
@@ -101,8 +101,13 @@ class XMRInterface(CoinInterface):
return True
return super().is_transient_error(ex)
def __init__(self, coin_settings, network, swap_client=None):
super().__init__(network)
def __init__(self, coin_settings, network, swap_client=None, **kwargs):
super().__init__(
coin_settings=coin_settings,
network=network,
swap_client=swap_client,
**kwargs,
)
self._addr_prefix = self.chainparams_network()["address_prefix"]
@@ -857,3 +862,6 @@ class XMRInterface(CoinInterface):
except Exception as e:
self._log.error(f"listWalletTransactions failed: {e}")
return []
def validateFeeRate(self, fee_rate: int) -> bool:
pass # Fee rate isn't used
+1 -1
View File
@@ -741,7 +741,7 @@ class Test(BaseTest):
ci0 = cls.swap_clients[0].ci(cls.test_coin)
if not cls.restore_instance:
dcr_mining_addr = ci0.rpc_wallet("getnewaddress")
assert dcr_mining_addr in cls.dcr_mining_addrs
assert dcr_mining_addr == cls.dcr_mining_addr
cls.dcr_ticket_account = ci0.rpc_wallet(
"getaccount",
[