separate fee policy from config

- Wallet.make_unsigned_transaction takes a FeePolicy parameter
 - fee sliders act on a FeePolicy instead of config
 - different fee policies may be used for different purposes
 - do not detect dust outputs in lnsweep, delegate that to lnwatcher
This commit is contained in:
ThomasV
2025-02-24 12:20:44 +01:00
parent 6234cbf97b
commit 840243e029
32 changed files with 851 additions and 851 deletions
+1 -1
View File
@@ -318,7 +318,7 @@ def construct_script(items: Sequence[Union[str, int, bytes, opcodes]], values=No
def relayfee(network: 'Network' = None) -> int:
"""Returns feerate in sat/kbyte."""
from .simple_config import FEERATE_DEFAULT_RELAY, FEERATE_MAX_RELAY
from .fee_policy import FEERATE_DEFAULT_RELAY, FEERATE_MAX_RELAY
if network and network.relay_fee is not None:
fee = network.relay_fee
else:
+32 -21
View File
@@ -70,6 +70,7 @@ from .plugin import run_hook, DeviceMgr, Plugins
from .version import ELECTRUM_VERSION
from .simple_config import SimpleConfig
from .invoices import Invoice
from .fee_policy import FeePolicy
from . import submarine_swaps
from . import GuiImportError
from . import crypto
@@ -237,7 +238,7 @@ class Commands(Logger):
'auto_connect': net_params.auto_connect,
'version': ELECTRUM_VERSION,
'default_wallet': self.config.get_wallet_path(),
'fee_per_kb': self.config.fee_per_kb(),
'fee_estimates': self.network.fee_estimates.get_data()
}
return response
@@ -728,21 +729,20 @@ class Commands(Logger):
return out['address']
@command('n')
async def sweep(self, privkey, destination, fee=None, nocheck=False, imax=100):
async def sweep(self, privkey, destination, fee=None, feerate=None, nocheck=False, imax=100):
"""Sweep private keys. Returns a transaction that spends UTXOs from
privkey to a destination address. The transaction is not
broadcasted."""
from .wallet import sweep
tx_fee = satoshis(fee)
fee_policy = self._get_fee_policy(fee, feerate)
privkeys = privkey.split()
self.nocheck = nocheck
#dest = self._resolver(destination)
tx = await sweep(
privkeys,
network=self.network,
config=self.config,
to_address=destination,
fee=tx_fee,
fee_policy=fee_policy,
imax=imax,
)
return tx.serialize() if tx else None
@@ -761,12 +761,25 @@ class Commands(Logger):
message = util.to_bytes(message)
return bitcoin.verify_usermessage_with_address(address, sig, message)
def _get_fee_policy(self, fee, feerate):
if fee is not None and feerate is not None:
raise Exception('Cannot set both fee and feerate')
if fee is not None:
fee_sats = satoshis(fee)
fee_policy = FeePolicy(f'fixed:{fee_sats}')
elif feerate is not None:
feerate_per_byte = 1000 * feerate
fee_policy = FeePolicy(f'feerate:{feerate_per_byte}')
else:
fee_policy = FeePolicy(self.config.FEE_POLICY)
return fee_policy
@command('wp')
async def payto(self, destination, amount, fee=None, feerate=None, from_addr=None, from_coins=None, change_addr=None,
nocheck=False, unsigned=False, rbf=True, password=None, locktime=None, addtransaction=False, wallet: Abstract_Wallet = None):
"""Create a transaction. """
self.nocheck = nocheck
tx_fee = satoshis(fee)
fee_policy = self._get_fee_policy(fee, feerate)
domain_addr = from_addr.split(',') if from_addr else None
domain_coins = from_coins.split(',') if from_coins else None
change_addr = self._resolver(change_addr, wallet)
@@ -775,8 +788,7 @@ class Commands(Logger):
outputs = [PartialTxOutput.from_address_and_value(destination, amount_sat)]
tx = wallet.create_transaction(
outputs,
fee=tx_fee,
feerate=feerate,
fee_policy=fee_policy,
change_addr=change_addr,
domain_addr=domain_addr,
domain_coins=domain_coins,
@@ -794,7 +806,7 @@ class Commands(Logger):
nocheck=False, unsigned=False, rbf=True, password=None, locktime=None, addtransaction=False, wallet: Abstract_Wallet = None):
"""Create a multi-output transaction. """
self.nocheck = nocheck
tx_fee = satoshis(fee)
fee_policy = self._get_fee_policy(fee, feerate)
domain_addr = from_addr.split(',') if from_addr else None
domain_coins = from_coins.split(',') if from_coins else None
change_addr = self._resolver(change_addr, wallet)
@@ -806,8 +818,7 @@ class Commands(Logger):
final_outputs.append(PartialTxOutput.from_address_and_value(address, amount_sat))
tx = wallet.create_transaction(
final_outputs,
fee=tx_fee,
feerate=feerate,
fee_policy=fee_policy,
change_addr=change_addr,
domain_addr=domain_addr,
domain_coins=domain_coins,
@@ -1142,23 +1153,23 @@ class Commands(Logger):
""" return wallet synchronization status """
return wallet.is_up_to_date()
@command('')
@command('n')
async def getfeerate(self):
"""Return current fee rate settings and current estimate (in sat/kvByte).
"""
method, value, feerate, tooltip = self.config.getfeerate()
Return current fee estimate given network conditions (in sat/kvByte).
To change the fee policy, use 'getconfig/setconfig fee_policy'
"""
fee_policy = FeePolicy(self.config.FEE_POLICY)
description = fee_policy.get_target_text()
feerate = fee_policy.fee_per_kb(self.network)
tooltip = fee_policy.get_estimate_text(self.network)
return {
'method': method,
'value': value,
'policy': fee_policy.get_descriptor(),
'description': description,
'sat/kvB': feerate,
'tooltip': tooltip,
}
@command('')
async def setfeerate(self, method, value):
"""Set fee rate estimation method and value"""
self.config.setfeerate(method, value)
@command('w')
async def removelocaltx(self, txid, wallet: Abstract_Wallet = None):
"""Remove a 'local' transaction from the wallet, and its dependent
+414
View File
@@ -0,0 +1,414 @@
from typing import Optional, Sequence, Tuple, Union, TYPE_CHECKING, Dict
from decimal import Decimal
from numbers import Real
from enum import IntEnum
from .i18n import _
from .util import NoDynamicFeeEstimates, quantize_feerate, format_fee_satoshis
from . import util
from . import constants
from .logging import Logger
if TYPE_CHECKING:
from .network import Network
FEE_ETA_TARGETS = [25, 10, 5, 2, 1]
FEE_DEPTH_TARGETS = [10_000_000, 5_000_000, 2_000_000, 1_000_000,
800_000, 600_000, 400_000, 250_000, 100_000]
FEERATE_STATIC_VALUES = [1000, 2000, 5000, 10000, 20000, 30000,
50000, 70000, 100000, 150000, 200000, 300000]
# satoshi per kbyte
FEERATE_MAX_DYNAMIC = 1500000
FEERATE_WARNING_HIGH_FEE = 600000
FEERATE_FALLBACK_STATIC_FEE = 150000
FEERATE_DEFAULT_RELAY = 1000
FEERATE_MAX_RELAY = 50000
# warn user if fee/amount for on-chain tx is higher than this
FEE_RATIO_HIGH_WARNING = 0.05
FEE_LN_ETA_TARGET = 2 # note: make sure the network is asking for estimates for this target
FEE_LN_LOW_ETA_TARGET = 25 # note: make sure the network is asking for estimates for this target
# The min feerate_per_kw that can be used in lightning so that
# the resulting onchain tx pays the min relay fee.
# This would be FEERATE_DEFAULT_RELAY / 4 if not for rounding errors,
# see https://github.com/ElementsProject/lightning/commit/2e687b9b352c9092b5e8bd4a688916ac50b44af0
FEERATE_PER_KW_MIN_RELAY_LIGHTNING = 253
def closest_index(value, array) -> int:
dist = list(map(lambda x: abs(x - value), array))
return min(range(len(dist)), key=dist.__getitem__)
class FeeMethod(IntEnum):
# note: careful changing these names! they appear in the config files.
FIXED = 0 # fixed absolute fee
FEERATE = 1 # fixed fee rate
ETA = 2 # dynamic, ETA based
MEMPOOL = 3 # dynamic, mempool based
@classmethod
def slider_values(cls):
return [FeeMethod.FEERATE, FeeMethod.ETA, FeeMethod.MEMPOOL]
def name_for_GUI(self):
names = {
FeeMethod.FEERATE: _('Feerate'),
FeeMethod.ETA:_('ETA'),
FeeMethod.MEMPOOL :_('Mempool')
}
return names[self]
@classmethod
def slider_index_of_method(cls, method):
i = FeeMethod.slider_values().index(method)
assert i is not None
return i
class FeePolicy(Logger):
# object associated to a fee slider
def __init__(self, descriptor: str):
Logger.__init__(self)
try:
name, value = descriptor.split(':')
self.method = FeeMethod[name.upper()]
self.value = int(value) # target (e.g. num blocks, nbytes from mempool tip, sat/kbyte)
except Exception:
self.logger.warning(f"Could not parse fee policy descriptor '{descriptor}'. Falling back to 'eta:2'")
self.method = FeeMethod.ETA
self.value = 2
def __repr__(self):
return self.get_descriptor()
def get_descriptor(self) -> str:
return self.method.name.lower() + ':' + str(self.value)
def set_method(self, method: FeeMethod):
assert isinstance(method, FeeMethod)
self.method = method
# default values
if self.method == FeeMethod.MEMPOOL:
self.value = 1000000 # 1 mb from tip
elif self.method == FeeMethod.ETA:
self.value = 2 # 2 blocks
elif self.method == FeeMethod.FEERATE:
self.value = 5000 # sats per vkb
else:
self.value = 10 # sats
def _get_array(self) -> Sequence[int]:
if self.method == FeeMethod.MEMPOOL:
return FEE_DEPTH_TARGETS
elif self.method == FeeMethod.ETA:
return FEE_ETA_TARGETS
elif self.method == FeeMethod.FEERATE:
return FEERATE_STATIC_VALUES
else:
raise Exception('')
def set_value_from_slider_pos(self, slider_pos: int):
array = self._get_array()
slider_pos = max(0, min(slider_pos, len(array)-1))
self.value = array[slider_pos]
def get_slider_pos(self) -> int:
array = self._get_array()
return closest_index(self.value, array)
def get_slider_max(self) -> int:
array = self._get_array()
maxp = len(array) - 1
return maxp
@property
def use_dynamic_estimates(self):
return self.method in [FeeMethod.ETA, FeeMethod.MEMPOOL]
@classmethod
def depth_target(self, slider_pos: int) -> int:
"""Returns mempool depth target in bytes for a fee slider position."""
slider_pos = max(slider_pos, 0)
slider_pos = min(slider_pos, len(FEE_DEPTH_TARGETS)-1)
return FEE_DEPTH_TARGETS[slider_pos]
def eta_target(self, slider_pos: int) -> int:
"""Returns 'num blocks' ETA target for a fee slider position."""
return FEE_ETA_TARGETS[slider_pos]
@classmethod
def eta_tooltip(self, x):
if x < 0:
return _('Low fee')
elif x == 1:
return _('In the next block')
else:
return _('Within {} blocks').format(x)
def get_target_text(self):
""" Description of what the target is: static fee / num blocks to confirm in / mempool depth """
if self.method == FeeMethod.ETA:
return self.eta_tooltip(self.value)
elif self.method == FeeMethod.MEMPOOL:
return self.depth_tooltip(self.value)
elif self.method == FeeMethod.FEERATE:
fee_per_byte = self.value/1000
return format_fee_satoshis(fee_per_byte) + f" {util.UI_UNIT_NAME_FEERATE_SAT_PER_VBYTE}"
def get_estimate_text(self, network: 'Network') -> str:
"""
Description of the current fee estimate corresponding to the target
"""
fee_per_kb = self.fee_per_kb(network)
fee_per_byte = fee_per_kb/1000 if fee_per_kb is not None else None
tooltip = ''
if self.use_dynamic_estimates:
if fee_per_byte is not None:
tooltip = format_fee_satoshis(fee_per_byte) + f" {util.UI_UNIT_NAME_FEERATE_SAT_PER_VBYTE}"
elif self.method == FeeMethod.FEERATE:
assert fee_per_kb is not None
assert fee_per_byte is not None
if network and network.mempool_fees.has_data():
depth = network.mempool_fees.fee_to_depth(fee_per_byte)
tooltip = self.depth_tooltip(depth)
if network and network.fee_estimates.has_data():
eta = network.fee_estimates.fee_to_eta(fee_per_kb)
tooltip += '\n' + self.eta_tooltip(eta)
return tooltip
def get_tooltip(self, network: 'Network'):
target = self.get_target_text()
estimate = self.get_estimate_text(network)
if self.use_dynamic_estimates:
return _('Target') + ': ' + target + '\n' + _('Current rate') + ': ' + estimate
else:
return _('Fixed rate') + ': ' + target + '\n' + _('Estimate') + ': ' + estimate
@classmethod
def depth_tooltip(self, depth: Optional[int]) -> str:
"""Returns text tooltip for given mempool depth (in vbytes)."""
if depth is None:
return "unknown from tip"
depth_mb = self.get_depth_mb_str(depth)
return _("{} from tip").format(depth_mb)
@classmethod
def get_depth_mb_str(self, depth: int) -> str:
# e.g. 500_000 -> "0.50 MB"
depth_mb = "{:.2f}".format(depth / 1_000_000) # maybe .rstrip("0") ?
return f"{depth_mb} {util.UI_UNIT_NAME_MEMPOOL_MB}"
def fee_per_kb(self, network: 'Network') -> Optional[int]:
"""Returns sat/kvB fee to pay for a txn.
Note: might return None.
"""
if self.use_dynamic_estimates and constants.net is constants.BitcoinRegtest:
return FEERATE_FALLBACK_STATIC_FEE
if self.method == FeeMethod.FEERATE:
fee_rate = self.value
elif self.method == FeeMethod.MEMPOOL:
if network and network.mempool_fees.has_data():
fee_rate = network.mempool_fees.depth_to_fee(self.get_slider_pos())
else:
fee_rate = None
elif self.method == FeeMethod.ETA:
if network and network.fee_estimates.has_data():
fee_rate = network.fee_estimates.eta_to_fee(self.get_slider_pos())
else:
fee_rate = None
else:
raise Exception(self.method)
if fee_rate is not None:
fee_rate = int(fee_rate)
return fee_rate
def fee_per_byte(self, network: 'Network') -> Optional[int]:
"""Returns sat/vB fee to pay for a txn.
Note: might return None.
"""
fee_per_kb = self.fee_per_kb(network)
return fee_per_kb / 1000 if fee_per_kb is not None else None
def estimate_fee(
self, size: Union[int, float, Decimal], *,
network: 'Network' = None,
allow_fallback_to_static_rates: bool = False,
) -> int:
if self.method == FeeMethod.FIXED:
return self.value
if network is None and self.use_dynamic_estimates:
if allow_fallback_to_static_rates:
fee_per_kb = FEERATE_FALLBACK_STATIC_FEE
else:
raise NoDynamicFeeEstimates()
else:
fee_per_kb = self.fee_per_kb(network)
return self.estimate_fee_for_feerate(fee_per_kb, size)
@classmethod
def estimate_fee_for_feerate(cls, fee_per_kb: Union[int, float, Decimal],
size: Union[int, float, Decimal]) -> int:
# note: 'size' is in vbytes
size = Decimal(size)
fee_per_kb = Decimal(fee_per_kb)
fee_per_byte = fee_per_kb / 1000
# to be consistent with what is displayed in the GUI,
# the calculation needs to use the same precision:
fee_per_byte = quantize_feerate(fee_per_byte)
return round(fee_per_byte * size)
class FixedFeePolicy(FeePolicy):
def __init__(self, fee):
FeePolicy.__init__(self, 'fixed:%d'%fee)
def impose_hard_limits_on_fee(func):
def get_fee_within_limits(self, *args, **kwargs):
fee = func(self, *args, **kwargs)
if fee is None:
return fee
fee = min(FEERATE_MAX_DYNAMIC, fee)
fee = max(FEERATE_DEFAULT_RELAY, fee)
return fee
return get_fee_within_limits
class FeeHistogram:
def __init__(self):
self._data = None # type: Optional[Sequence[Tuple[Union[float, int], int]]]
def has_data(self) -> bool:
return self._data is not None
def set_data(self, data):
self._data = data
def fee_to_depth(self, target_fee: Real) -> Optional[int]:
"""For a given sat/vbyte fee, returns an estimate of how deep
it would be in the current mempool in vbytes.
Pessimistic == overestimates the depth.
"""
if self._data is None:
return None
depth = 0
for fee, s in self._data:
depth += s
if fee <= target_fee:
break
return depth
@impose_hard_limits_on_fee
def depth_target_to_fee(self, target: int) -> Optional[int]:
"""Returns fee in sat/kbyte.
target: desired mempool depth in vbytes
"""
if self._data is None:
return None
depth = 0
for fee, s in self._data:
depth += s
if depth > target:
break
else:
return 0
# add one sat/byte as currently that is the max precision of the histogram
# note: precision depends on server.
# old ElectrumX <1.16 has 1 s/b prec, >=1.16 has 0.1 s/b prec.
# electrs seems to use untruncated double-precision floating points.
# # TODO decrease this to 0.1 s/b next time we bump the required protocol version
fee += 1
# convert to sat/kbyte
return int(fee * 1000)
def depth_to_fee(self, slider_pos) -> Optional[int]:
"""Returns fee in sat/kbyte."""
target = FeePolicy.depth_target(slider_pos)
return self.depth_target_to_fee(target)
def get_capped_data(self):
""" used by QML """
data = self._data or [[FEERATE_DEFAULT_RELAY/1000,1]]
# cap the histogram to a limited number of megabytes
bytes_limit = 10*1000*1000
bytes_current = 0
capped_histogram = []
for item in sorted(data, key=lambda x: x[0], reverse=True):
if bytes_current >= bytes_limit:
break
slot = min(item[1], bytes_limit-bytes_current)
bytes_current += slot
capped_histogram.append([
max(FEERATE_DEFAULT_RELAY/1000, item[0]), # clamped to [FEERATE_DEFAULT_RELAY/1000,inf[
slot, # width of bucket
bytes_current, # cumulative depth at far end of bucket
])
return data, bytes_current
class FeeTimeEstimates:
def __init__(self):
self.data = {} # type: Dict[int, int]
def get_data(self):
return self.data
def has_data(self):
# we do not request estimate for next block fee
return len(self.data) == len(FEE_ETA_TARGETS) - 1
def set_data(self, nblock_target: int, fee_per_kb: int):
assert isinstance(nblock_target, int), f"expected int, got {nblock_target!r}"
assert isinstance(fee_per_kb, int), f"expected int, got {fee_per_kb!r}"
self.data[nblock_target] = fee_per_kb
def fee_to_eta(self, fee_per_kb: Optional[int]) -> int:
"""Returns 'num blocks' ETA estimate for given fee rate,
or -1 for low fee.
"""
import operator
lst = list(self.data.items())
next_block_fee = self.eta_target_to_fee(1)
if next_block_fee is not None:
lst += [(1, next_block_fee)]
if not lst or fee_per_kb is None:
return -1
dist = map(lambda x: (x[0], abs(x[1] - fee_per_kb)), lst)
min_target, min_value = min(dist, key=operator.itemgetter(1))
if fee_per_kb < self.data.get(FEE_ETA_TARGETS[0])/2:
min_target = -1
return min_target
def eta_to_fee(self, slider_pos) -> Optional[int]:
"""Returns fee in sat/kbyte."""
slider_pos = max(slider_pos, 0)
slider_pos = min(slider_pos, len(FEE_ETA_TARGETS) - 1)
if slider_pos < len(FEE_ETA_TARGETS) - 1:
num_blocks = FEE_ETA_TARGETS[int(slider_pos)]
fee = self.eta_target_to_fee(num_blocks)
else:
fee = self.eta_target_to_fee(1)
return fee
@impose_hard_limits_on_fee
def eta_target_to_fee(self, num_blocks: int) -> Optional[int]:
"""Returns fee in sat/kbyte."""
if num_blocks == 1:
fee = self.data.get(2)
if fee is not None:
fee += fee / 2
fee = int(fee)
else:
fee = self.data.get(num_blocks)
if fee is not None:
fee = int(fee)
return fee
@@ -12,9 +12,9 @@ ElComboBox {
valueRole: 'value'
model: [
{ text: qsTr('ETA'), value: 1 },
{ text: qsTr('Mempool'), value: 2 },
{ text: qsTr('Static'), value: 0 }
{ text: qsTr('ETA'), value: FeeSlider.FSMethod.ETA },
{ text: qsTr('Mempool'), value: FeeSlider.FSMethod.MEMPOOL },
{ text: qsTr('Feerate'), value: FeeSlider.FSMethod.FEERATE }
]
onCurrentValueChanged: {
if (activeFocus)
+2 -2
View File
@@ -30,7 +30,7 @@ from .qeqr import QEQRParser, QEQRImageProvider, QEQRImageProviderHelper
from .qeqrscanner import QEQRScanner
from .qebitcoin import QEBitcoin
from .qefx import QEFX
from .qetxfinalizer import QETxFinalizer, QETxRbfFeeBumper, QETxCpfpFeeBumper, QETxCanceller, QETxSweepFinalizer
from .qetxfinalizer import QETxFinalizer, QETxRbfFeeBumper, QETxCpfpFeeBumper, QETxCanceller, QETxSweepFinalizer, FeeSlider
from .qeinvoice import QEInvoice, QEInvoiceParser
from .qerequestdetails import QERequestDetails
from .qetypes import QEAmount
@@ -408,7 +408,7 @@ class ElectrumQmlApplication(QGuiApplication):
qmlRegisterType(QETxCanceller, 'org.electrum', 1, 0, 'TxCanceller')
qmlRegisterType(QETxSweepFinalizer, 'org.electrum', 1, 0, 'SweepFinalizer')
qmlRegisterType(QEBip39RecoveryListModel, 'org.electrum', 1, 0, 'Bip39RecoveryListModel')
qmlRegisterType(FeeSlider, 'org.electrum', 1, 0, 'FeeSlider')
# TODO QT6: these were declared as uncreatable, but that doesn't seem to work for pyqt6
# qmlRegisterUncreatableType(QEAmount, 'org.electrum', 1, 0, 'Amount', 'Amount can only be used as property')
# qmlRegisterUncreatableType(QENewWalletWizard, 'org.electrum', 1, 0, 'QNewWalletWizard', 'QNewWalletWizard can only be used as property')
+2 -18
View File
@@ -5,7 +5,7 @@ from PyQt6.QtCore import pyqtProperty, pyqtSignal, QObject
from electrum.logging import get_logger
from electrum import constants
from electrum.interface import ServerAddr
from electrum.simple_config import FEERATE_DEFAULT_RELAY
from electrum.fee_policy import FEERATE_DEFAULT_RELAY
from .util import QtEventListener, event_listener
from .qeserverlistmodel import QEServerListModel
@@ -135,23 +135,7 @@ class QENetwork(QObject, QtEventListener):
self.update_histogram(histogram)
def update_histogram(self, histogram):
if not histogram:
histogram = [[FEERATE_DEFAULT_RELAY/1000,1]]
# cap the histogram to a limited number of megabytes
bytes_limit = 10*1000*1000
bytes_current = 0
capped_histogram = []
for item in sorted(histogram, key=lambda x: x[0], reverse=True):
if bytes_current >= bytes_limit:
break
slot = min(item[1], bytes_limit-bytes_current)
bytes_current += slot
capped_histogram.append([
max(FEERATE_DEFAULT_RELAY/1000, item[0]), # clamped to [FEERATE_DEFAULT_RELAY/1000,inf[
slot, # width of bucket
bytes_current, # cumulative depth at far end of bucket
])
capped_histogram, bytes_current = histogram.get_capped_data()
# add clamping attributes for the GUI
self._fee_histogram = {
'histogram': capped_histogram,
+2 -1
View File
@@ -10,6 +10,7 @@ from electrum.transaction import tx_from_any, Transaction, PartialTxInput, Sigha
from electrum.network import Network
from electrum.address_synchronizer import TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_FUTURE
from electrum.wallet import TxSighashDanger
from electrum.fee_policy import FeePolicy
from .qewallet import QEWallet
from .qetypes import QEAmount
@@ -327,7 +328,7 @@ class QETxDetails(QObject, QtEventListener):
self.update_mined_status(txinfo.tx_mined_status)
else:
if txinfo.tx_mined_status.height in [TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT]:
self._mempool_depth = self._wallet.wallet.config.depth_tooltip(txinfo.mempool_depth_bytes)
self._mempool_depth = FeePolicy.depth_tooltip(txinfo.mempool_depth_bytes)
self._in_mempool = True
elif txinfo.tx_mined_status.height == TX_HEIGHT_FUTURE:
self._lock_delay = txinfo.tx_mined_status.wanted_height - self._wallet.wallet.adb.get_local_height()
+48 -35
View File
@@ -1,10 +1,11 @@
import copy
from enum import IntEnum
import threading
from decimal import Decimal
from typing import Optional, TYPE_CHECKING
from functools import partial
from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, pyqtEnum
from electrum.logging import get_logger
from electrum.i18n import _
@@ -14,6 +15,7 @@ from electrum.util import NotEnoughFunds, profiler, quantize_feerate, UserFacing
from electrum.wallet import CannotBumpFee, CannotDoubleSpendTx, CannotCPFP, BumpFeeStrategy, sweep_preparations
from electrum import keystore
from electrum.plugin import run_hook
from electrum.fee_policy import FeePolicy, FeeMethod
from .qewallet import QEWallet
from .qetypes import QEAmount
@@ -24,13 +26,35 @@ if TYPE_CHECKING:
class FeeSlider(QObject):
@pyqtEnum
class FSMethod(IntEnum):
FEERATE = 0
ETA = 1
MEMPOOL = 2
def to_fee_method(self) -> 'FeeMethod':
return {
self.FEERATE: FeeMethod.FEERATE,
self.ETA: FeeMethod.ETA,
self.MEMPOOL: FeeMethod.MEMPOOL,
}[self]
@classmethod
def from_fee_method(cls, fm: FeeMethod) -> 'FeeSlider.FSMethod':
return {
FeeMethod.FEERATE: cls.FEERATE,
FeeMethod.ETA: cls.ETA,
FeeMethod.MEMPOOL: cls.MEMPOOL,
}[fm]
def __init__(self, parent=None):
super().__init__(parent)
self._wallet = None # type: Optional[QEWallet]
self._sliderSteps = 0
self._sliderPos = 0
self._method = -1
self._fee_policy = None
self._target = ''
self._config = None # type: Optional[SimpleConfig]
@@ -66,22 +90,20 @@ class FeeSlider(QObject):
methodChanged = pyqtSignal()
@pyqtProperty(int, notify=methodChanged)
def method(self):
return self._method
def method(self) -> int:
fsmethod = self.FSMethod.from_fee_method(self._fee_policy.method)
return int(fsmethod)
@method.setter
def method(self, method):
if self._method != method:
self._method = method
def method(self, method: int):
fsmethod = self.FSMethod(method)
method = fsmethod.to_fee_method()
if self._fee_policy.method != method:
self._fee_policy.set_method(method)
self.update_slider()
self.methodChanged.emit()
self.save_config()
def get_method(self):
dynfees = self._method > 0
mempool = self._method == 2
return dynfees, mempool
targetChanged = pyqtSignal()
@pyqtProperty(str, notify=targetChanged)
def target(self):
@@ -94,21 +116,16 @@ class FeeSlider(QObject):
self.targetChanged.emit()
def update_slider(self):
dynfees, mempool = self.get_method()
maxp, pos, fee_rate = self._config.get_fee_slider(dynfees, mempool)
self._sliderSteps = maxp
self._sliderPos = pos
self._sliderSteps = self._fee_policy.get_slider_max()
self._sliderPos = self._fee_policy.get_slider_pos()
self.sliderStepsChanged.emit()
self.sliderPosChanged.emit()
def update_target(self):
target, tooltip, dyn = self._config.get_fee_target()
self.target = target
self.target = self._fee_policy.get_target_text()
def read_config(self):
mempool = self._config.use_mempool_fees()
dynfees = self._config.is_dynfee()
self._method = (2 if mempool else 1) if dynfees else 0
self._fee_policy = FeePolicy(self._config.FEE_POLICY)
self.update_slider()
self.methodChanged.emit()
self.update_target()
@@ -116,16 +133,8 @@ class FeeSlider(QObject):
def save_config(self):
value = int(self._sliderPos)
dynfees, mempool = self.get_method()
self._config.FEE_EST_DYNAMIC = dynfees
self._config.FEE_EST_USE_MEMPOOL = mempool
if dynfees:
if mempool:
self._config.FEE_EST_DYNAMIC_MEMPOOL_SLIDERPOS = value
else:
self._config.FEE_EST_DYNAMIC_ETA_SLIDERPOS = value
else:
self._config.FEE_EST_STATIC_FEERATE = self._config.static_fee(value)
self._fee_policy.set_value_from_slider_pos(value)
self._config.FEE_POLICY = self._fee_policy.get_descriptor()
self.update_target()
self.update()
@@ -362,7 +371,11 @@ class QETxFinalizer(TxFeeSlider):
# default impl
coins = self._wallet.wallet.get_spendable_coins(None)
outputs = [PartialTxOutput.from_address_and_value(self.address, amount)]
tx = self._wallet.wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee=None, rbf=self._rbf)
tx = self._wallet.wallet.make_unsigned_transaction(
coins=coins,
outputs=outputs,
fee_policy=self._fee_policy,
rbf=self._rbf)
self._logger.debug('fee: %d, inputs: %d, outputs: %d' % (tx.get_fee(), len(tx.inputs()), len(tx.outputs())))
@@ -587,7 +600,7 @@ class QETxRbfFeeBumper(TxFeeSlider, TxMonMixin):
# not initialized yet
return
fee_per_kb = self._config.fee_per_kb()
fee_per_kb = self._fee_policy.fee_per_kb(self._wallet.wallet.network)
if fee_per_kb is None:
# dynamic method and no network
self._logger.debug('no fee_per_kb')
@@ -684,7 +697,7 @@ class QETxCanceller(TxFeeSlider, TxMonMixin):
# not initialized yet
return
fee_per_kb = self._config.fee_per_kb()
fee_per_kb = self._fee_policy.fee_per_kb()
if fee_per_kb is None:
# dynamic method and no network
self._logger.debug('no fee_per_kb')
@@ -826,7 +839,7 @@ class QETxCpfpFeeBumper(TxFeeSlider, TxMonMixin):
self.validChanged.emit()
self.warning = ''
fee_per_kb = self._config.fee_per_kb()
fee_per_kb = self._fee_policy.fee_per_kb()
if fee_per_kb is None:
# dynamic method and no network
self._logger.debug('no fee_per_kb')
+29 -34
View File
@@ -40,6 +40,7 @@ from electrum.transaction import Transaction, PartialTransaction
from electrum.wallet import InternalAddressCorruption
from electrum.simple_config import SimpleConfig
from electrum.bitcoin import DummyAddress
from electrum.fee_policy import FeePolicy, FixedFeePolicy
from .util import (WindowModalDialog, ColorScheme, HelpLabel, Buttons, CancelButton,
WWLabel, read_QIcon)
@@ -71,6 +72,8 @@ class TxEditor(WindowModalDialog):
self.error = '' # set by side effect
self.config = window.config
self.network = window.network
self.fee_policy = FeePolicy(self.config.FEE_POLICY)
self.wallet = window.wallet
self.feerounding_sats = 0
self.not_enough_funds = False
@@ -124,23 +127,14 @@ class TxEditor(WindowModalDialog):
def stop_editor_updates(self):
self.main_window.gui_object.timer.timeout.disconnect(self.timer_actions)
def set_fee_config(self, dyn, pos, fee_rate):
if dyn:
if self.config.use_mempool_fees():
self.config.cv.FEE_EST_DYNAMIC_MEMPOOL_SLIDERPOS.set(pos, save=False)
else:
self.config.cv.FEE_EST_DYNAMIC_ETA_SLIDERPOS.set(pos, save=False)
else:
self.config.cv.FEE_EST_STATIC_FEERATE.set(fee_rate, save=False)
def update_tx(self, *, fallback_to_zero_fee: bool = False):
# expected to set self.tx, self.message and self.error
raise NotImplementedError()
def update_fee_target(self):
text = self.fee_slider.get_dynfee_target()
text = self.fee_slider.fee_policy.get_target_text()
self.fee_target.setText(text)
self.fee_target.setVisible(bool(text)) # hide in static mode
# self.fee_target.setVisible(self.fee_slider.fee_policy.use_dynamic_estimates) # hide in static mode
def update_feerate_label(self):
self.feerate_label.setText(self.feerate_e.text() + ' ' + self.feerate_e.base_unit())
@@ -164,7 +158,7 @@ class TxEditor(WindowModalDialog):
self.fiat_fee_label.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet())
self.feerate_e = FeerateEdit(lambda: 0)
self.feerate_e.setAmount(self.config.fee_per_byte())
self.feerate_e.setAmount(self.fee_policy.fee_per_byte(self.network))
self.feerate_e.textEdited.connect(partial(self.on_fee_or_feerate, self.feerate_e, False))
self.feerate_e.editingFinished.connect(partial(self.on_fee_or_feerate, self.feerate_e, True))
self.update_feerate_label()
@@ -180,7 +174,7 @@ class TxEditor(WindowModalDialog):
self.feerate_e.textChanged.connect(self.entry_changed)
self.fee_target = QLabel('')
self.fee_slider = FeeSlider(self, self.config, self.fee_slider_callback)
self.fee_slider = FeeSlider(self, self.fee_policy, self.fee_slider_callback)
self.fee_combo = FeeComboBox(self.fee_slider)
self.fee_combo.setFocusPolicy(Qt.FocusPolicy.NoFocus)
@@ -229,8 +223,8 @@ class TxEditor(WindowModalDialog):
self._update_widgets()
self.needs_update = True
def fee_slider_callback(self, dyn, pos, fee_rate):
self.set_fee_config(dyn, pos, fee_rate)
def fee_slider_callback(self, fee_rate):
self.config.FEE_POLICY = self.fee_policy.get_descriptor()
self.fee_slider.activate()
if fee_rate:
fee_rate = Decimal(fee_rate)
@@ -258,13 +252,13 @@ class TxEditor(WindowModalDialog):
# because that event is emitted when we press OK
self.trigger_update()
def is_send_fee_frozen(self):
def is_send_fee_frozen(self) -> bool:
return self.fee_e.isVisible() and self.fee_e.isModified() \
and (self.fee_e.text() or self.fee_e.hasFocus())
and (bool(self.fee_e.text()) or self.fee_e.hasFocus())
def is_send_feerate_frozen(self):
def is_send_feerate_frozen(self) -> bool:
return self.feerate_e.isVisible() and self.feerate_e.isModified() \
and (self.feerate_e.text() or self.feerate_e.hasFocus())
and (bool(self.feerate_e.text()) or self.feerate_e.hasFocus())
def feerounding_text(self):
return (_('Additional {} satoshis are going to be added.').format(self.feerounding_sats))
@@ -274,17 +268,17 @@ class TxEditor(WindowModalDialog):
self.feerounding_icon.setIcon(read_QIcon('info.png') if b else QIcon())
self.feerounding_icon.setEnabled(b)
def get_fee_estimator(self):
if self.is_send_fee_frozen() and self.fee_e.get_amount() is not None:
fee_estimator = self.fee_e.get_amount()
elif self.is_send_feerate_frozen() and self.feerate_e.get_amount() is not None:
amount = self.feerate_e.get_amount() # sat/byte feerate
amount = 0 if amount is None else amount * 1000 # sat/kilobyte feerate
fee_estimator = partial(
SimpleConfig.estimate_fee_for_feerate, amount)
def get_fee_policy(self):
feerate = self.feerate_e.get_amount()
fee_amount = self.fee_e.get_amount()
if self.is_send_fee_frozen() and fee_amount is not None:
fee_policy = FixedFeePolicy(fee_amount)
elif self.is_send_feerate_frozen() and feerate is not None:
feerate_per_kb = int(feerate * 1000)
fee_policy = FeePolicy(f'static:{feerate_per_kb}')
else:
fee_estimator = None
return fee_estimator
fee_policy = self.fee_slider.get_policy()
return fee_policy
def entry_changed(self):
# blue color denotes auto-filled values
@@ -635,10 +629,10 @@ class ConfirmTxDialog(TxEditor):
self.amount_label.setText(amount_str)
def update_tx(self, *, fallback_to_zero_fee: bool = False):
fee_estimator = self.get_fee_estimator()
fee_policy = self.get_fee_policy()
confirmed_only = self.config.WALLET_SPEND_CONFIRMED_ONLY
try:
self.tx = self.make_tx(fee_estimator, confirmed_only=confirmed_only)
self.tx = self.make_tx(fee_policy, confirmed_only=confirmed_only)
self.not_enough_funds = False
self.no_dynfee_estimates = False
except NotEnoughFunds:
@@ -646,16 +640,17 @@ class ConfirmTxDialog(TxEditor):
self.tx = None
if fallback_to_zero_fee:
try:
self.tx = self.make_tx(0, confirmed_only=confirmed_only)
self.tx = self.make_tx(FixedFeePolicy(0), confirmed_only=confirmed_only)
except BaseException:
return
else:
return
except NoDynamicFeeEstimates:
# is this still needed?
self.no_dynfee_estimates = True
self.tx = None
try:
self.tx = self.make_tx(0, confirmed_only=confirmed_only)
self.tx = self.make_tx(FixedFeePolicy(0), confirmed_only=confirmed_only)
except NotEnoughFunds:
self.not_enough_funds = True
return
@@ -670,7 +665,7 @@ class ConfirmTxDialog(TxEditor):
def can_pay_assuming_zero_fees(self, confirmed_only) -> bool:
# called in send_tab.py
try:
tx = self.make_tx(0, confirmed_only=confirmed_only)
tx = self.make_tx(FixedFeePolicy(0), confirmed_only=confirmed_only)
except NotEnoughFunds:
return False
else:
+27 -43
View File
@@ -5,84 +5,68 @@ from PyQt6.QtCore import Qt
from PyQt6.QtWidgets import QSlider, QToolTip, QComboBox
from electrum.i18n import _
from electrum.fee_policy import FeeMethod
class FeeComboBox(QComboBox):
def __init__(self, fee_slider):
QComboBox.__init__(self)
self.config = fee_slider.config
self.fee_slider = fee_slider
self.addItems([_('Static'), _('ETA'), _('Mempool')])
self.setCurrentIndex((2 if self.config.use_mempool_fees() else 1) if self.config.is_dynfee() else 0)
self.addItems([x.name_for_GUI() for x in FeeMethod.slider_values()])
index = FeeMethod.slider_index_of_method(self.fee_slider.fee_policy.method)
self.setCurrentIndex(index)
self.currentIndexChanged.connect(self.on_fee_type)
self.help_msg = '\n'.join([
_('Static: the fee slider uses static values'),
_('Feerate: the fee slider uses static feerate values'),
_('ETA: fee rate is based on average confirmation time estimates'),
_('Mempool based: fee rate is targeting a depth in the memory pool')
]
)
def on_fee_type(self, x):
self.config.FEE_EST_USE_MEMPOOL = (x == 2)
self.config.FEE_EST_DYNAMIC = (x > 0)
self.fee_slider.update()
method = FeeMethod.slider_values()[x]
self.fee_slider.fee_policy.set_method(method)
self.fee_slider.update(is_initialized=True)
class FeeSlider(QSlider):
def __init__(self, window, config, callback):
def __init__(self, window, fee_policy, callback):
QSlider.__init__(self, Qt.Orientation.Horizontal)
self.config = config
self.window = window
self.network = window.network
self.callback = callback
self.dyn = False
self.fee_policy = fee_policy
self.lock = threading.RLock()
self.update()
self.update(is_initialized=False)
self.valueChanged.connect(self.moved)
self._active = True
def get_fee_rate(self, pos):
if self.dyn:
fee_rate = self.config.depth_to_fee(pos) if self.config.use_mempool_fees() else self.config.eta_to_fee(pos)
else:
fee_rate = self.config.static_fee(pos)
return fee_rate
@property
def dyn(self):
return self.fee_policy.use_dynamic_estimates
def get_policy(self):
return self.fee_policy
def moved(self, pos):
with self.lock:
fee_rate = self.get_fee_rate(pos)
tooltip = self.get_tooltip(pos, fee_rate)
self.fee_policy.set_value_from_slider_pos(pos)
fee_rate = self.fee_policy.fee_per_kb(self.network)
tooltip = self.fee_policy.get_tooltip(self.network)
QToolTip.showText(QCursor.pos(), tooltip, self)
self.setToolTip(tooltip)
self.callback(self.dyn, pos, fee_rate)
self.callback(fee_rate)
def get_tooltip(self, pos, fee_rate):
mempool = self.config.use_mempool_fees()
target, estimate = self.config.get_fee_text(pos, self.dyn, mempool, fee_rate)
if self.dyn:
return _('Target') + ': ' + target + '\n' + _('Current rate') + ': ' + estimate
else:
return _('Fixed rate') + ': ' + target + '\n' + _('Estimate') + ': ' + estimate
def get_dynfee_target(self):
if not self.dyn:
return ''
pos = self.value()
fee_rate = self.get_fee_rate(pos)
mempool = self.config.use_mempool_fees()
target, estimate = self.config.get_fee_text(pos, True, mempool, fee_rate)
return target
def update(self):
def update(self, *, is_initialized: bool):
with self.lock:
self.dyn = self.config.is_dynfee()
mempool = self.config.use_mempool_fees()
maxp, pos, fee_rate = self.config.get_fee_slider(self.dyn, mempool)
pos = self.fee_policy.get_slider_pos()
maxp = self.fee_policy.get_slider_max()
self.setRange(0, maxp)
self.setValue(pos)
tooltip = self.get_tooltip(pos, fee_rate)
self.setToolTip(tooltip)
if is_initialized:
self.moved(pos)
def activate(self):
self._active = True
+7 -5
View File
@@ -72,6 +72,7 @@ from electrum.logging import Logger
from electrum.lntransport import extract_nodeid, ConnStringFormatError
from electrum.lnaddr import lndecode
from electrum.submarine_swaps import SwapServerTransport, NostrTransport
from electrum.fee_policy import FeePolicy
from .rate_limiter import rate_limited
from .exception_window import Exception_Hook
@@ -1384,11 +1385,11 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
WaitingDialog(self, msg, task, on_success, on_failure)
def mktx_for_open_channel(self, *, funding_sat, node_id):
make_tx = lambda fee_est, *, confirmed_only=False: self.wallet.lnworker.mktx_for_open_channel(
make_tx = lambda fee_policy, *, confirmed_only=False: self.wallet.lnworker.mktx_for_open_channel(
coins = self.get_coins(nonlocal_only=True, confirmed_only=confirmed_only),
funding_sat=funding_sat,
node_id=node_id,
fee_est=fee_est)
fee_policy=fee_policy)
return make_tx
def open_channel(self, connect_str, funding_sat, push_amt):
@@ -2692,15 +2693,16 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
fee = min(max_fee, fee)
fee = max(total_size, fee) # pay at least 1 sat/byte for combined size
return fee
suggested_feerate = self.config.fee_per_kb()
fee_policy = FeePolicy(self.config.FEE_POLICY)
suggested_feerate = fee_policy.fee_per_kb(self.network)
fee = get_child_fee_from_total_feerate(suggested_feerate)
fee_e.setAmount(fee)
grid.addWidget(QLabel(_('Fee for child') + ':'), 3, 0)
grid.addWidget(fee_e, 3, 1)
def on_rate(dyn, pos, fee_rate):
def on_rate(fee_rate):
fee = get_child_fee_from_total_feerate(fee_rate)
fee_e.setAmount(fee)
fee_slider = FeeSlider(self, self.config, on_rate)
fee_slider = FeeSlider(self, fee_policy, on_rate)
fee_combo = FeeComboBox(fee_slider)
fee_slider.update()
grid.addWidget(fee_slider, 4, 1)
+4 -13
View File
@@ -266,10 +266,10 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
outputs = pi.get_onchain_outputs('!')
if not outputs:
return
make_tx = lambda fee_est, *, confirmed_only=False: self.wallet.make_unsigned_transaction(
make_tx = lambda fee_policy, *, confirmed_only=False: self.wallet.make_unsigned_transaction(
fee_policy=fee_policy,
coins=self.window.get_coins(),
outputs=outputs,
fee=fee_est,
is_sweep=False)
try:
try:
@@ -325,10 +325,10 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
# we call get_coins inside make_tx, so that inputs can be changed dynamically
if get_coins is None:
get_coins = self.window.get_coins
make_tx = lambda fee_est, *, confirmed_only=False: self.wallet.make_unsigned_transaction(
make_tx = lambda fee_policy, *, confirmed_only=False: self.wallet.make_unsigned_transaction(
coins=get_coins(nonlocal_only=nonlocal_only, confirmed_only=confirmed_only),
fee_policy=fee_policy,
outputs=outputs,
fee=fee_est,
is_sweep=is_sweep)
output_values = [x.value for x in outputs]
is_max = any(parse_max_spend(outval) for outval in output_values)
@@ -667,7 +667,6 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
lnworker = self.wallet.lnworker
if lnworker is None or not lnworker.can_pay_invoice(invoice):
coins = self.window.get_coins(nonlocal_only=True)
can_pay_onchain = invoice.can_be_paid_onchain() and self.wallet.can_pay_onchain(invoice.get_outputs(), coins=coins)
can_pay_with_new_channel = False
can_pay_with_swap = False
can_rebalance = False
@@ -695,19 +694,11 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
_('You will be able to pay once the swap is confirmed.')
])
choices.append(('swap', msg))
if can_pay_onchain:
msg = ''.join([
_('Pay onchain'), '\n',
_('Funds will be sent to the invoice fallback address.')
])
choices.append(('onchain', msg))
msg = _('You cannot pay that invoice using Lightning.')
if lnworker and lnworker.channels:
num_sats_can_send = int(lnworker.num_sats_can_send())
msg += '\n' + _('Your channels can send {}.').format(self.format_amount(num_sats_can_send) + ' ' + self.base_unit())
if not choices:
if not can_pay_onchain:
msg += '\n' + _('Also, you have insufficient funds to pay on-chain.')
self.window.show_error(msg)
return
r = self.window.query_choice(msg, choices)
+6 -9
View File
@@ -7,6 +7,7 @@ from electrum.i18n import _
from electrum.util import NotEnoughFunds, NoDynamicFeeEstimates, UserCancelled
from electrum.bitcoin import DummyAddress
from electrum.transaction import PartialTxOutput, PartialTransaction
from electrum.fee_policy import FeePolicy
from electrum.gui import messages
from . import util
@@ -76,7 +77,8 @@ class SwapDialog(WindowModalDialog, QtEventListener):
self.send_amount_e.setEnabled(recv_amount_sat is None)
self.recv_amount_e.setEnabled(recv_amount_sat is None)
self.max_button.setEnabled(recv_amount_sat is None)
fee_slider = FeeSlider(self.window, self.config, self.fee_slider_callback)
self.fee_policy = FeePolicy(self.config.FEE_POLICY)
fee_slider = FeeSlider(self.window, self.fee_policy, self.fee_slider_callback)
fee_combo = FeeComboBox(fee_slider)
fee_slider.update()
self.fee_label = QLabel()
@@ -147,14 +149,8 @@ class SwapDialog(WindowModalDialog, QtEventListener):
recv_amount_sat = max(recv_amount_sat, self.swap_manager.get_min_amount())
self.recv_amount_e.setAmount(recv_amount_sat)
def fee_slider_callback(self, dyn, pos, fee_rate):
if dyn:
if self.config.use_mempool_fees():
self.config.cv.FEE_EST_DYNAMIC_MEMPOOL_SLIDERPOS.set(pos, save=False)
else:
self.config.cv.FEE_EST_DYNAMIC_ETA_SLIDERPOS.set(pos, save=False)
else:
self.config.cv.FEE_EST_STATIC_FEERATE.set(fee_rate, save=False)
def fee_slider_callback(self, fee_rate):
self.config.FEE_POLICY = self.fee_policy.get_descriptor()
if self.send_follows:
self.on_recv_edited()
else:
@@ -319,6 +315,7 @@ class SwapDialog(WindowModalDialog, QtEventListener):
outputs = [PartialTxOutput.from_address_and_value(DummyAddress.SWAP, onchain_amount)]
try:
tx = self.window.wallet.make_unsigned_transaction(
fee_policy=self.fee_policy,
coins=coins,
outputs=outputs,
send_change_to_lightning=False,
+2 -1
View File
@@ -843,7 +843,8 @@ class TxDialog(QDialog, MessageBoxMixin):
self.date_label.setText(_("Date: {}").format(time_str))
self.date_label.show()
elif exp_n is not None:
self.date_label.setText(_('Position in mempool: {}').format(self.config.depth_tooltip(exp_n)))
from electrum.fee_policy import FeePolicy
self.date_label.setText(_('Position in mempool: {}').format(FeePolicy.depth_tooltip(exp_n)))
self.date_label.show()
else:
self.date_label.hide()
+6 -6
View File
@@ -764,14 +764,14 @@ class ElectrumGui(BaseElectrumGui, EventListener):
self.network.run_from_another_thread(self.network.set_parameters(net_params))
def settings_dialog(self):
fee = str(Decimal(self.config.fee_per_kb()) / COIN)
from electrum.fee_policy import FeePolicy
out = self.run_dialog('Settings', [
{'label':'Default fee', 'type':'satoshis', 'value': fee}
], buttons = 1)
{'label':'Fee policy', 'type':'str', 'value': self.config.FEE_POLICY}
], buttons = 1)
if out:
if out.get('Default fee'):
fee = int(Decimal(out['Default fee']) * COIN)
self.config.FEE_EST_STATIC_FEERATE = fee
if descr := out.get('Fee policy'):
fee_policy = FeePolicy(descr)
self.config.FEE_POLICY = fee_policy.get_descriptor()
def password_dialog(self):
out = self.run_dialog('Password', [
+2 -2
View File
@@ -58,6 +58,7 @@ from . import constants
from .i18n import _
from .logging import Logger
from .transaction import Transaction
from .fee_policy import FEE_ETA_TARGETS
if TYPE_CHECKING:
from .network import Network
@@ -736,11 +737,10 @@ class Interface(Logger):
await self.session.send_request('server.ping')
async def request_fee_estimates(self):
from .simple_config import FEE_ETA_TARGETS
while True:
async with OldTaskGroup() as group:
fee_tasks = []
for i in FEE_ETA_TARGETS:
for i in FEE_ETA_TARGETS[0:-1]:
fee_tasks.append((i, await group.spawn(self.get_estimatefee(i))))
for nblock_target, task in fee_tasks:
fee = task.result()
+4 -2
View File
@@ -65,7 +65,7 @@ from .address_synchronizer import TX_HEIGHT_LOCAL
from .lnutil import CHANNEL_OPENING_TIMEOUT
from .lnutil import ChannelBackupStorage, ImportedChannelBackupStorage, OnchainChannelBackupStorage
from .lnutil import format_short_channel_id
from .simple_config import FEERATE_PER_KW_MIN_RELAY_LIGHTNING
from .fee_policy import FEERATE_PER_KW_MIN_RELAY_LIGHTNING
if TYPE_CHECKING:
from .lnworker import LNWallet
@@ -1510,15 +1510,17 @@ class Channel(AbstractChannel):
def create_sweeptxs_for_watchtower(self, ctn: int) -> List[Transaction]:
from .lnsweep import sweep_their_ctx_watchtower
from .fee_policy import FeePolicy
from .transaction import PartialTxOutput, PartialTransaction
secret, ctx = self.get_secret_and_commitment(REMOTE, ctn=ctn)
txs = []
txins = sweep_their_ctx_watchtower(self, ctx, secret)
fee_policy = FeePolicy('eta:2')
for txin in txins:
output_idx = txin.prevout.out_idx
value = ctx.outputs()[output_idx].value
tx_size_bytes = 121
fee = self.lnworker.config.estimate_fee(tx_size_bytes, allow_fallback_to_static_rates=True)
fee = fee_policy.estimate_fee(tx_size_bytes, network=self.lnworker.network, allow_fallback_to_static_rates=True)
outvalue = value - fee
sweep_outputs = [PartialTxOutput.from_address_and_value(self.get_sweep_address(), outvalue)]
sweep_tx = PartialTransaction.from_io([txin], sweep_outputs, version=2)
+4 -3
View File
@@ -55,7 +55,7 @@ from .interface import GracefulDisconnect
from .lnrouter import fee_for_edge_msat
from .json_db import StoredDict
from .invoices import PR_PAID
from .simple_config import FEE_LN_ETA_TARGET, FEERATE_PER_KW_MIN_RELAY_LIGHTNING
from .fee_policy import FEE_LN_ETA_TARGET, FEERATE_PER_KW_MIN_RELAY_LIGHTNING
from .trampoline import decode_routing_info
if TYPE_CHECKING:
@@ -2705,9 +2705,10 @@ class Peer(Logger, EventListener):
if config.TEST_SHUTDOWN_FEE:
our_fee = config.TEST_SHUTDOWN_FEE
else:
fee_rate_per_kb = config.eta_target_to_fee(FEE_LN_ETA_TARGET)
fee_rate_per_kb = self.network.fee_estimates.eta_target_to_fee(FEE_LN_ETA_TARGET)
if fee_rate_per_kb is None: # fallback
fee_rate_per_kb = self.network.config.fee_per_kb()
from .fee_policy import FeePolicy
fee_rate_per_kb = FeePolicy(config.FEE_POLICY).fee_per_kb(self.network)
if fee_rate_per_kb is not None:
our_fee = fee_rate_per_kb * closing_tx.estimated_size() // 1000
# TODO: anchors: remove this, as commitment fee rate can be below chain head fee rate?
+6 -32
View File
@@ -8,7 +8,7 @@ import electrum_ecc as ecc
from .util import bfh, UneconomicFee
from .crypto import privkey_to_pubkey
from .bitcoin import redeem_script_to_address, dust_threshold, construct_witness
from .bitcoin import redeem_script_to_address, construct_witness
from . import descriptor
from . import bitcoin
@@ -23,7 +23,6 @@ from .lnutil import (make_commitment_output_to_remote_address, make_commitment_o
derive_multisig_funding_key_if_we_opened, derive_multisig_funding_key_if_they_opened)
from .transaction import (Transaction, TxInput, PartialTxInput,
PartialTxOutput, TxOutpoint, script_GetOp, match_script_against_template)
from .simple_config import SimpleConfig
from .logging import get_logger, Logger
if TYPE_CHECKING:
@@ -79,7 +78,7 @@ def sweep_their_ctx_watchtower(
witness_script=witness_script,
privkey=watcher_revocation_privkey,
is_revocation=True,
config=chan.lnworker.config)
)
if txin:
txins.append(txin)
@@ -107,7 +106,6 @@ def sweep_their_ctx_watchtower(
privkey=watcher_revocation_privkey,
is_revocation=True,
cltv_abs=cltv_abs,
config=chan.lnworker.config,
has_anchors=chan.has_anchors()
)
htlc_to_ctx_output_idx_map = map_htlcs_to_ctx_output_idxs(
@@ -150,7 +148,7 @@ def sweep_their_ctx_watchtower(
htlctx_witness_script=htlc_tx_witness_script,
privkey=watcher_revocation_privkey,
is_revocation=True,
config=chan.lnworker.config)
)
htlc_to_ctx_output_idx_map = map_htlcs_to_ctx_output_idxs(
chan=chan,
@@ -195,7 +193,7 @@ def sweep_their_ctx_justice(
witness_script=witness_script,
privkey=other_revocation_privkey,
is_revocation=True,
config=chan.lnworker.config)
)
return sweep_txin
return None
@@ -243,7 +241,6 @@ def sweep_their_htlctx_justice(
htlctx_witness_script=witness_script,
privkey=other_revocation_privkey,
is_revocation=True,
config=chan.lnworker.config
)
index_to_sweepinfo = {}
for output_idx in htlc_outputs_idxs:
@@ -345,7 +342,6 @@ def sweep_our_ctx(
privkey=our_localdelayed_privkey.get_secret_bytes(),
is_revocation=False,
to_self_delay=to_self_delay,
config=chan.lnworker.config,
):
prevout = ctx.txid() + ':%d'%output_idx
txs[prevout] = SweepInfo(
@@ -401,7 +397,6 @@ def sweep_our_ctx(
htlctx_witness_script=htlctx_witness_script,
privkey=our_localdelayed_privkey.get_secret_bytes(),
is_revocation=False,
config=chan.lnworker.config
):
txs[actual_htlc_tx.txid() + f':{output_idx}'] = SweepInfo(
name=f'second-stage-htlc:{output_idx}',
@@ -559,7 +554,6 @@ def sweep_their_ctx_to_remote_backup(
ctx=ctx,
output_idx=output_idx,
our_payment_privkey=our_payment_privkey,
config=chan.lnworker.config,
has_anchors=True
):
txs[prevout] = SweepInfo(
@@ -660,7 +654,6 @@ def sweep_their_ctx(
ctx=ctx,
output_idx=output_idx,
our_payment_privkey=our_payment_privkey,
config=chan.lnworker.config,
has_anchors=chan.has_anchors()
):
txs[prevout] = SweepInfo(
@@ -700,7 +693,6 @@ def sweep_their_ctx(
privkey=our_revocation_privkey if is_revocation else our_htlc_privkey.get_secret_bytes(),
is_revocation=is_revocation,
cltv_abs=cltv_abs,
config=chan.lnworker.config,
has_anchors=chan.has_anchors(),
):
txs[prevout] = SweepInfo(
@@ -778,7 +770,6 @@ def sweep_their_ctx_htlc(
preimage: Optional[bytes], output_idx: int,
privkey: bytes, is_revocation: bool,
cltv_abs: int,
config: SimpleConfig,
has_anchors: bool,
) -> Optional[PartialTxInput]:
"""Deals with normal (non-CSV timelocked) HTLC output sweeps."""
@@ -792,11 +783,6 @@ def sweep_their_ctx_htlc(
txin.witness_script = witness_script
txin.script_sig = b''
txin.nsequence = 1 if has_anchors else 0xffffffff - 2
tx_size_bytes = 200 # TODO (depends on offered/received and is_revocation)
fee = config.estimate_fee(tx_size_bytes, allow_fallback_to_static_rates=True)
outvalue = val - fee
if outvalue <= dust_threshold():
return None
txin.privkey = privkey
if not is_revocation:
txin.make_witness = lambda sig: construct_witness([sig, preimage, witness_script])
@@ -810,7 +796,6 @@ def sweep_their_ctx_htlc(
def sweep_their_ctx_to_remote(
ctx: Transaction, output_idx: int,
our_payment_privkey: ecc.ECPrivkey,
config: SimpleConfig,
has_anchors: bool,
) -> Optional[PartialTxInput]:
assert has_anchors is True
@@ -826,11 +811,6 @@ def sweep_their_ctx_to_remote(
txin.script_sig = b''
txin.witness_script = witness_script
txin.nsequence = 1
tx_size_bytes = 196 # approx size of p2wsh->p2wpkh
fee = config.estimate_fee(tx_size_bytes, allow_fallback_to_static_rates=True)
outvalue = val - fee
if outvalue <= dust_threshold():
return None
txin.privkey = our_payment_privkey.get_secret_bytes()
txin.make_witness = lambda sig: construct_witness([sig, witness_script])
return txin
@@ -859,7 +839,7 @@ def sweep_ctx_anchor(*, ctx: Transaction, multisig_key: Keypair) -> Optional[Par
def sweep_ctx_to_local(
*, ctx: Transaction, output_idx: int, witness_script: bytes,
privkey: bytes, is_revocation: bool, config: SimpleConfig,
privkey: bytes, is_revocation: bool,
to_self_delay: int = None) -> Optional[PartialTxInput]:
"""Create a txin that sweeps the 'to_local' output of a commitment
transaction into our wallet.
@@ -877,11 +857,6 @@ def sweep_ctx_to_local(
if not is_revocation:
assert isinstance(to_self_delay, int)
txin.nsequence = to_self_delay
tx_size_bytes = 121 # approx size of to_local -> p2wpkh
fee = config.estimate_fee(tx_size_bytes, allow_fallback_to_static_rates=True)
outvalue = val - fee
if outvalue <= dust_threshold():
return None
txin.privkey = privkey
assert txin.witness_script
txin.make_witness = lambda sig: construct_witness([sig, int(is_revocation), witness_script])
@@ -895,7 +870,7 @@ def sweep_htlctx_output(
privkey: bytes,
is_revocation: bool,
to_self_delay: int = None,
config: SimpleConfig) -> Optional[PartialTxInput]:
) -> Optional[PartialTxInput]:
"""Create a txn that sweeps the output of a first stage htlc tx
(i.e. sweeps from an HTLC-Timeout or an HTLC-Success tx).
"""
@@ -908,5 +883,4 @@ def sweep_htlctx_output(
privkey=privkey,
is_revocation=is_revocation,
to_self_delay=to_self_delay,
config=config,
)
+1 -1
View File
@@ -34,6 +34,7 @@ from .lnaddr import lndecode
from .bip32 import BIP32Node, BIP32_PRIME
from .transaction import BCDataStream, OPPushDataGeneric
from .logging import get_logger
from .fee_policy import FEERATE_PER_KW_MIN_RELAY_LIGHTNING
if TYPE_CHECKING:
@@ -206,7 +207,6 @@ class ChannelConfig(StoredObject):
raise Exception(
"both to_local and to_remote amounts for the initial commitment "
"transaction are less than or equal to channel_reserve_satoshis")
from .simple_config import FEERATE_PER_KW_MIN_RELAY_LIGHTNING
if initial_feerate_per_kw < FEERATE_PER_KW_MIN_RELAY_LIGHTNING:
raise Exception(f"feerate lower than min relay fee. {initial_feerate_per_kw} sat/kw.")
+20 -8
View File
@@ -3,18 +3,19 @@
# file LICENCE or http://www.opensource.org/licenses/mit-license.php
from typing import NamedTuple, Iterable, TYPE_CHECKING
import os
import copy
import asyncio
from enum import IntEnum, auto
from typing import NamedTuple, Dict
from . import util
from .wallet_db import WalletDB
from .util import bfh, log_exceptions, ignore_exceptions, TxMinedInfo, random_shuffled_copy
from .util import log_exceptions, ignore_exceptions, TxMinedInfo
from .util import EventListener, event_listener
from .address_synchronizer import AddressSynchronizer, TX_HEIGHT_LOCAL, TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_FUTURE
from .transaction import Transaction, TxOutpoint, PartialTransaction
from .logging import Logger
from .bitcoin import dust_threshold
from .fee_policy import FeePolicy
if TYPE_CHECKING:
@@ -36,10 +37,6 @@ class TxMinedDepth(IntEnum):
FREE = auto()
from .util import EventListener, event_listener
class LNWatcher(Logger, EventListener):
LOGGING_SHORTCUT = 'W'
@@ -54,6 +51,7 @@ class LNWatcher(Logger, EventListener):
self.register_callbacks()
# status gets populated when we run
self.channel_status = {}
self.fee_policy = FeePolicy('eta:2')
async def stop(self):
self.unregister_callbacks()
@@ -219,6 +217,18 @@ class LNWalletWatcher(LNWatcher):
keep_watching=keep_watching)
await self.lnworker.handle_onchain_state(chan)
def is_dust(self, sweep_info):
if sweep_info.name in ['local_anchor', 'remote_anchor']:
return False
if sweep_info.txout is not None:
return False
value = sweep_info.txin._trusted_value_sats
witness_size = len(sweep_info.txin.make_witness(71*b'\x00'))
tx_size_vbytes = 84 + witness_size//4 # assumes no batching, sweep to p2wpkh
self.logger.info(f'{sweep_info.name} size = {tx_size_vbytes}')
fee = self.fee_policy.estimate_fee(tx_size_vbytes, network=self.network, allow_fallback_to_static_rates=True)
return value - fee <= dust_threshold()
@log_exceptions
async def sweep_commitment_transaction(self, funding_outpoint, closing_tx) -> bool:
"""This function is called when a channel was closed. In this case
@@ -229,7 +239,6 @@ class LNWalletWatcher(LNWatcher):
chan = self.lnworker.channel_by_txo(funding_outpoint)
if not chan:
return False
chan_id_for_log = chan.get_id_for_log()
# detect who closed and get information about how to claim outputs
sweep_info_dict = chan.sweep_ctx(closing_tx)
self.logger.info(f"do_breach_remedy: {[x.name for x in sweep_info_dict.values()]}")
@@ -237,6 +246,8 @@ class LNWalletWatcher(LNWatcher):
# create and broadcast transactions
for prevout, sweep_info in sweep_info_dict.items():
if self.is_dust(sweep_info):
continue
prev_txid, prev_index = prevout.split(':')
name = sweep_info.name + ' ' + chan.get_id_for_log()
self.lnworker.wallet.set_default_label(prevout, name)
@@ -290,6 +301,7 @@ class LNWalletWatcher(LNWatcher):
# password is needed for 1st stage htlc tx with anchors because we add inputs
password = self.lnworker.wallet.get_unlocked_password()
new_tx = self.lnworker.wallet.create_transaction(
fee_policy = self.fee_policy,
inputs = inputs,
outputs = outputs,
password = password,
+14 -16
View File
@@ -38,6 +38,8 @@ from .util import (
make_aiohttp_session, timestamp_to_datetime, random_shuffled_copy, is_private_netaddress,
UnrelatedTransactionException, LightningHistoryItem
)
from .fee_policy import FeePolicy, FixedFeePolicy
from .fee_policy import FEERATE_FALLBACK_STATIC_FEE, FEE_LN_ETA_TARGET, FEE_LN_LOW_ETA_TARGET, FEERATE_PER_KW_MIN_RELAY_LIGHTNING
from .invoices import Invoice, PR_UNPAID, PR_PAID, PR_INFLIGHT, PR_FAILED, LN_EXPIRY_NEVER, BaseInvoice
from .bitcoin import COIN, opcodes, make_op_return, address_to_scripthash, DummyAddress
from .bip32 import BIP32Node
@@ -1300,11 +1302,12 @@ class LNWallet(LNWorker):
self.wallet.unlock(password)
coins = self.wallet.get_spendable_coins(None)
node_id = peer.pubkey
fee_policy = FeePolicy(self.config.FEE_POLICY)
funding_tx = self.mktx_for_open_channel(
coins=coins,
funding_sat=funding_sat,
node_id=node_id,
fee_est=None)
fee_policy=fee_policy)
chan, funding_tx = await self._open_channel_coroutine(
peer=peer,
funding_tx=funding_tx,
@@ -1387,7 +1390,8 @@ class LNWallet(LNWorker):
coins: Sequence[PartialTxInput],
funding_sat: int,
node_id: bytes,
fee_est=None) -> PartialTransaction:
fee_policy: FeePolicy,
) -> PartialTransaction:
from .wallet import get_locktime_for_new_transaction
outputs = [PartialTxOutput.from_address_and_value(DummyAddress.CHANNEL, funding_sat)]
@@ -1397,7 +1401,7 @@ class LNWallet(LNWorker):
tx = self.wallet.make_unsigned_transaction(
coins=coins,
outputs=outputs,
fee=fee_est)
fee_policy=fee_policy)
tx.set_rbf(False)
# rm randomness from locktime, as we use the locktime as entropy for deriving the funding_privkey
# (and it would be confusing to get a collision as a consequence of the randomness)
@@ -1413,16 +1417,16 @@ class LNWallet(LNWorker):
min_funding_sat = max(min_funding_sat, 100_000) # at least 1mBTC
if min_funding_sat > self.config.LIGHTNING_MAX_FUNDING_SAT:
return
fee_est = partial(self.config.estimate_fee, allow_fallback_to_static_rates=True) # to avoid NoDynamicFeeEstimates
fee_policy = FeePolicy(f'feerate:{FEERATE_FALLBACK_STATIC_FEE}')
try:
self.mktx_for_open_channel(coins=coins, funding_sat=min_funding_sat, node_id=bytes(32), fee_est=fee_est)
self.mktx_for_open_channel(coins=coins, funding_sat=min_funding_sat, node_id=bytes(32), fee_policy=fee_policy)
funding_sat = min_funding_sat
except NotEnoughFunds:
return
# if available, suggest twice that amount:
if 2 * min_funding_sat <= self.config.LIGHTNING_MAX_FUNDING_SAT:
try:
self.mktx_for_open_channel(coins=coins, funding_sat=2*min_funding_sat, node_id=bytes(32), fee_est=fee_est)
self.mktx_for_open_channel(coins=coins, funding_sat=2*min_funding_sat, node_id=bytes(32), fee_policy=fee_policy)
funding_sat = 2 * min_funding_sat
except NotEnoughFunds:
pass
@@ -2966,23 +2970,17 @@ class LNWallet(LNWorker):
await self.taskgroup.spawn(self.reestablish_peer_for_given_channel(chan))
def current_target_feerate_per_kw(self) -> int:
from .simple_config import FEE_LN_ETA_TARGET, FEERATE_FALLBACK_STATIC_FEE
from .simple_config import FEERATE_PER_KW_MIN_RELAY_LIGHTNING
if constants.net is constants.BitcoinRegtest:
feerate_per_kvbyte = self.network.config.FEE_EST_STATIC_FEERATE
if self.network.fee_estimates.has_data():
feerate_per_kvbyte = self.network.fee_estimates.eta_target_to_fee(FEE_LN_ETA_TARGET)
else:
feerate_per_kvbyte = self.network.config.eta_target_to_fee(FEE_LN_ETA_TARGET)
if feerate_per_kvbyte is None:
feerate_per_kvbyte = FEERATE_FALLBACK_STATIC_FEE
feerate_per_kvbyte = FEERATE_FALLBACK_STATIC_FEE
return max(FEERATE_PER_KW_MIN_RELAY_LIGHTNING, feerate_per_kvbyte // 4)
def current_low_feerate_per_kw(self) -> int:
from .simple_config import FEE_LN_LOW_ETA_TARGET
from .simple_config import FEERATE_PER_KW_MIN_RELAY_LIGHTNING
if constants.net is constants.BitcoinRegtest:
feerate_per_kvbyte = 0
else:
feerate_per_kvbyte = self.network.config.eta_target_to_fee(FEE_LN_LOW_ETA_TARGET) or 0
feerate_per_kvbyte = self.network.fee_estimates.eta_target_to_fee(FEE_LN_LOW_ETA_TARGET) or 0
low_feerate_per_kw = max(FEERATE_PER_KW_MIN_RELAY_LIGHTNING, feerate_per_kvbyte // 4)
# make sure this is never higher than the target feerate:
low_feerate_per_kw = min(low_feerate_per_kw, self.current_target_feerate_per_kw())
+30 -8
View File
@@ -62,6 +62,8 @@ from .interface import (Interface, PREFERRED_NETWORK_PROTOCOL,
from .version import PROTOCOL_VERSION
from .i18n import _
from .logging import get_logger, Logger
from .fee_policy import FeeHistogram, FeeTimeEstimates, FEE_ETA_TARGETS
if TYPE_CHECKING:
from collections.abc import Coroutine
@@ -364,6 +366,10 @@ class Network(Logger, NetworkRetryManager[ServerAddr]):
self._has_ever_managed_to_connect_to_server = False
self._was_started = False
self.mempool_fees = FeeHistogram()
self.fee_estimates = FeeTimeEstimates()
self.last_time_fee_estimates_requested = 0 # zero ensures immediate fees
def has_internet_connection(self) -> bool:
"""Our guess whether the device has Internet-connectivity."""
@@ -497,11 +503,27 @@ class Network(Logger, NetworkRetryManager[ServerAddr]):
await group.spawn(self._request_fee_estimates(interface))
async def _request_fee_estimates(self, interface):
self.config.requested_fee_estimates()
self.requested_fee_estimates()
histogram = await interface.get_fee_histogram()
self.config.mempool_fees = histogram
self.mempool_fees.set_data(histogram)
self.logger.info(f'fee_histogram {len(histogram)}')
util.trigger_callback('fee_histogram', self.config.mempool_fees)
util.trigger_callback('fee_histogram', self.mempool_fees)
def is_fee_estimates_update_required(self):
"""Checks time since last requested and updated fee estimates.
Returns True if an update should be requested.
"""
now = time.time()
return now - self.last_time_fee_estimates_requested > 60
def has_fee_etas(self):
return self.fee_estimates.has_data()
def has_fee_mempool(self) -> bool:
return self.mempool_fees.has_data()
def requested_fee_estimates(self):
self.last_time_fee_estimates_requested = time.time()
def get_parameters(self) -> NetworkParameters:
return NetworkParameters(server=self.default_server,
@@ -532,11 +554,10 @@ class Network(Logger, NetworkRetryManager[ServerAddr]):
def get_fee_estimates(self):
from statistics import median
from .simple_config import FEE_ETA_TARGETS
if self.auto_connect:
with self.interfaces_lock:
out = {}
for n in FEE_ETA_TARGETS:
for n in FEE_ETA_TARGETS[0:-1]:
try:
out[n] = int(median(filter(None, [i.fee_estimates_eta.get(n) for i in self.interfaces.values()])))
except Exception:
@@ -551,11 +572,12 @@ class Network(Logger, NetworkRetryManager[ServerAddr]):
if fee_est is None:
fee_est = self.get_fee_estimates()
for nblock_target, fee in fee_est.items():
self.config.update_fee_estimates(nblock_target, fee)
self.fee_estimates.set_data(nblock_target, fee)
if not hasattr(self, "_prev_fee_est") or self._prev_fee_est != fee_est:
self._prev_fee_est = copy.copy(fee_est)
self.logger.info(f'fee_estimates {fee_est}')
util.trigger_callback('fee', self.config.fee_estimates)
util.trigger_callback('fee', self.fee_estimates)
@with_recent_servers_lock
def get_servers(self):
@@ -1404,7 +1426,7 @@ class Network(Logger, NetworkRetryManager[ServerAddr]):
async def maintain_main_interface():
await self._ensure_there_is_a_main_interface()
if self.is_connected():
if self.config.is_fee_estimates_update_required():
if self.is_fee_estimates_update_required():
await self.interface.taskgroup.spawn(self._request_fee_estimates, self.interface)
while True:
+2 -390
View File
@@ -15,34 +15,13 @@ from . import constants
from . import invoices
from .util import base_units, base_unit_name_to_decimal_point, decimal_point_to_base_unit_name, UnknownBaseUnit, DECIMAL_POINT_DEFAULT
from .util import format_satoshis, format_fee_satoshis, os_chmod
from .util import user_dir, make_dir, NoDynamicFeeEstimates, quantize_feerate
from .util import user_dir, make_dir
from .lnutil import LN_MAX_FUNDING_SAT_LEGACY
from .i18n import _
from .logging import get_logger, Logger
FEE_ETA_TARGETS = [25, 10, 5, 2]
FEE_DEPTH_TARGETS = [10_000_000, 5_000_000, 2_000_000, 1_000_000,
800_000, 600_000, 400_000, 250_000, 100_000]
FEE_LN_ETA_TARGET = 2 # note: make sure the network is asking for estimates for this target
FEE_LN_LOW_ETA_TARGET = 25 # note: make sure the network is asking for estimates for this target
# satoshi per kbyte
FEERATE_MAX_DYNAMIC = 1500000
FEERATE_WARNING_HIGH_FEE = 600000
FEERATE_FALLBACK_STATIC_FEE = 150000
FEERATE_DEFAULT_RELAY = 1000
FEERATE_MAX_RELAY = 50000
FEERATE_STATIC_VALUES = [1000, 2000, 5000, 10000, 20000, 30000,
50000, 70000, 100000, 150000, 200000, 300000]
# The min feerate_per_kw that can be used in lightning so that
# the resulting onchain tx pays the min relay fee.
# This would be FEERATE_DEFAULT_RELAY / 4 if not for rounding errors,
# see https://github.com/ElementsProject/lightning/commit/2e687b9b352c9092b5e8bd4a688916ac50b44af0
FEERATE_PER_KW_MIN_RELAY_LIGHTNING = 253
FEE_RATIO_HIGH_WARNING = 0.05 # warn user if fee/amount for on-chain tx is higher than this
@@ -195,9 +174,6 @@ class SimpleConfig(Logger):
# a thread-safe way.
self.lock = threading.RLock()
self.mempool_fees = None # type: Optional[Sequence[Tuple[Union[float, int], int]]]
self.fee_estimates = {} # type: Dict[int, int]
self.last_time_fee_estimates_requested = 0 # zero ensures immediate fees
# The following two functions are there for dependency injection when
# testing.
@@ -492,366 +468,6 @@ class SimpleConfig(Logger):
path = wallet.storage.path
self.GUI_LAST_WALLET = path
def impose_hard_limits_on_fee(func):
def get_fee_within_limits(self, *args, **kwargs):
fee = func(self, *args, **kwargs)
if fee is None:
return fee
fee = min(FEERATE_MAX_DYNAMIC, fee)
fee = max(FEERATE_DEFAULT_RELAY, fee)
return fee
return get_fee_within_limits
def eta_to_fee(self, slider_pos) -> Optional[int]:
"""Returns fee in sat/kbyte."""
slider_pos = max(slider_pos, 0)
slider_pos = min(slider_pos, len(FEE_ETA_TARGETS))
if slider_pos < len(FEE_ETA_TARGETS):
num_blocks = FEE_ETA_TARGETS[int(slider_pos)]
fee = self.eta_target_to_fee(num_blocks)
else:
fee = self.eta_target_to_fee(1)
return fee
@impose_hard_limits_on_fee
def eta_target_to_fee(self, num_blocks: int) -> Optional[int]:
"""Returns fee in sat/kbyte."""
if num_blocks == 1:
fee = self.fee_estimates.get(2)
if fee is not None:
fee += fee / 2
fee = int(fee)
else:
fee = self.fee_estimates.get(num_blocks)
if fee is not None:
fee = int(fee)
return fee
def fee_to_depth(self, target_fee: Real) -> Optional[int]:
"""For a given sat/vbyte fee, returns an estimate of how deep
it would be in the current mempool in vbytes.
Pessimistic == overestimates the depth.
"""
if self.mempool_fees is None:
return None
depth = 0
for fee, s in self.mempool_fees:
depth += s
if fee <= target_fee:
break
return depth
def depth_to_fee(self, slider_pos) -> Optional[int]:
"""Returns fee in sat/kbyte."""
target = self.depth_target(slider_pos)
return self.depth_target_to_fee(target)
@impose_hard_limits_on_fee
def depth_target_to_fee(self, target: int) -> Optional[int]:
"""Returns fee in sat/kbyte.
target: desired mempool depth in vbytes
"""
if self.mempool_fees is None:
return None
depth = 0
for fee, s in self.mempool_fees:
depth += s
if depth > target:
break
else:
return 0
# add one sat/byte as currently that is the max precision of the histogram
# note: precision depends on server.
# old ElectrumX <1.16 has 1 s/b prec, >=1.16 has 0.1 s/b prec.
# electrs seems to use untruncated double-precision floating points.
# # TODO decrease this to 0.1 s/b next time we bump the required protocol version
fee += 1
# convert to sat/kbyte
return int(fee * 1000)
def depth_target(self, slider_pos: int) -> int:
"""Returns mempool depth target in bytes for a fee slider position."""
slider_pos = max(slider_pos, 0)
slider_pos = min(slider_pos, len(FEE_DEPTH_TARGETS)-1)
return FEE_DEPTH_TARGETS[slider_pos]
def eta_target(self, slider_pos: int) -> int:
"""Returns 'num blocks' ETA target for a fee slider position."""
if slider_pos == len(FEE_ETA_TARGETS):
return 1
return FEE_ETA_TARGETS[slider_pos]
def fee_to_eta(self, fee_per_kb: Optional[int]) -> int:
"""Returns 'num blocks' ETA estimate for given fee rate,
or -1 for low fee.
"""
import operator
lst = list(self.fee_estimates.items())
next_block_fee = self.eta_target_to_fee(1)
if next_block_fee is not None:
lst += [(1, next_block_fee)]
if not lst or fee_per_kb is None:
return -1
dist = map(lambda x: (x[0], abs(x[1] - fee_per_kb)), lst)
min_target, min_value = min(dist, key=operator.itemgetter(1))
if fee_per_kb < self.fee_estimates.get(FEE_ETA_TARGETS[0])/2:
min_target = -1
return min_target
def get_depth_mb_str(self, depth: int) -> str:
# e.g. 500_000 -> "0.50 MB"
depth_mb = "{:.2f}".format(depth / 1_000_000) # maybe .rstrip("0") ?
return f"{depth_mb} {util.UI_UNIT_NAME_MEMPOOL_MB}"
def depth_tooltip(self, depth: Optional[int]) -> str:
"""Returns text tooltip for given mempool depth (in vbytes)."""
if depth is None:
return "unknown from tip"
depth_mb = self.get_depth_mb_str(depth)
return _("{} from tip").format(depth_mb)
def eta_tooltip(self, x):
if x < 0:
return _('Low fee')
elif x == 1:
return _('In the next block')
else:
return _('Within {} blocks').format(x)
def get_fee_target(self):
dyn = self.is_dynfee()
mempool = self.use_mempool_fees()
pos = self.get_depth_level() if mempool else self.get_fee_level()
fee_rate = self.fee_per_kb()
target, tooltip = self.get_fee_text(pos, dyn, mempool, fee_rate)
return target, tooltip, dyn
def get_fee_status(self):
target, tooltip, dyn = self.get_fee_target()
return tooltip + ' [%s]'%target if dyn else target + ' [Static]'
def get_fee_text(
self,
slider_pos: int,
dyn: bool,
mempool: bool,
fee_per_kb: Optional[int],
):
"""Returns (text, tooltip) where
text is what we target: static fee / num blocks to confirm in / mempool depth
tooltip is the corresponding estimate (e.g. num blocks for a static fee)
fee_rate is in sat/kbyte
"""
if fee_per_kb is None:
rate_str = 'unknown'
fee_per_byte = None
else:
fee_per_byte = fee_per_kb/1000
rate_str = format_fee_satoshis(fee_per_byte) + f" {util.UI_UNIT_NAME_FEERATE_SAT_PER_VBYTE}"
if dyn:
if mempool:
depth = self.depth_target(slider_pos)
text = self.depth_tooltip(depth)
else:
eta = self.eta_target(slider_pos)
text = self.eta_tooltip(eta)
tooltip = rate_str
else: # using static fees
assert fee_per_kb is not None
assert fee_per_byte is not None
text = rate_str
if mempool and self.has_fee_mempool():
depth = self.fee_to_depth(fee_per_byte)
tooltip = self.depth_tooltip(depth)
elif not mempool and self.has_fee_etas():
eta = self.fee_to_eta(fee_per_kb)
tooltip = self.eta_tooltip(eta)
else:
tooltip = ''
return text, tooltip
def get_depth_level(self) -> int:
maxp = len(FEE_DEPTH_TARGETS) - 1
return min(maxp, self.FEE_EST_DYNAMIC_MEMPOOL_SLIDERPOS)
def get_fee_level(self) -> int:
maxp = len(FEE_ETA_TARGETS) # not (-1) to have "next block"
return min(maxp, self.FEE_EST_DYNAMIC_ETA_SLIDERPOS)
def get_fee_slider(self, dyn, mempool) -> Tuple[int, int, Optional[int]]:
if dyn:
if mempool:
pos = self.get_depth_level()
maxp = len(FEE_DEPTH_TARGETS) - 1
fee_rate = self.depth_to_fee(pos)
else:
pos = self.get_fee_level()
maxp = len(FEE_ETA_TARGETS) # not (-1) to have "next block"
fee_rate = self.eta_to_fee(pos)
else:
fee_rate = self.fee_per_kb(dyn=False)
pos = self.static_fee_index(fee_rate)
maxp = len(FEERATE_STATIC_VALUES) - 1
return maxp, pos, fee_rate
def static_fee(self, i):
return FEERATE_STATIC_VALUES[i]
def static_fee_index(self, fee_per_kb: Optional[int]) -> int:
if fee_per_kb is None:
raise TypeError('static fee cannot be None')
dist = list(map(lambda x: abs(x - fee_per_kb), FEERATE_STATIC_VALUES))
return min(range(len(dist)), key=dist.__getitem__)
def has_fee_etas(self):
return len(self.fee_estimates) == 4
def has_fee_mempool(self) -> bool:
return self.mempool_fees is not None
def has_dynamic_fees_ready(self):
if self.use_mempool_fees():
return self.has_fee_mempool()
else:
return self.has_fee_etas()
def is_dynfee(self) -> bool:
return self.FEE_EST_DYNAMIC
def use_mempool_fees(self) -> bool:
return self.FEE_EST_USE_MEMPOOL
def _feerate_from_fractional_slider_position(self, fee_level: float, dyn: bool,
mempool: bool) -> Union[int, None]:
fee_level = max(fee_level, 0)
fee_level = min(fee_level, 1)
if dyn:
max_pos = (len(FEE_DEPTH_TARGETS) - 1) if mempool else len(FEE_ETA_TARGETS)
slider_pos = round(fee_level * max_pos)
fee_rate = self.depth_to_fee(slider_pos) if mempool else self.eta_to_fee(slider_pos)
else:
max_pos = len(FEERATE_STATIC_VALUES) - 1
slider_pos = round(fee_level * max_pos)
fee_rate = FEERATE_STATIC_VALUES[slider_pos]
return fee_rate
def fee_per_kb(self, dyn: bool=None, mempool: bool=None, fee_level: float=None) -> Optional[int]:
"""Returns sat/kvB fee to pay for a txn.
Note: might return None.
fee_level: float between 0.0 and 1.0, representing fee slider position
"""
if constants.net is constants.BitcoinRegtest:
return self.FEE_EST_STATIC_FEERATE
if dyn is None:
dyn = self.is_dynfee()
if mempool is None:
mempool = self.use_mempool_fees()
if fee_level is not None:
return self._feerate_from_fractional_slider_position(fee_level, dyn, mempool)
# there is no fee_level specified; will use config.
# note: 'depth_level' and 'fee_level' in config are integer slider positions,
# unlike fee_level here, which (when given) is a float in [0.0, 1.0]
if dyn:
if mempool:
fee_rate = self.depth_to_fee(self.get_depth_level())
else:
fee_rate = self.eta_to_fee(self.get_fee_level())
else:
fee_rate = self.FEE_EST_STATIC_FEERATE
if fee_rate is not None:
fee_rate = int(fee_rate)
return fee_rate
def getfeerate(self) -> Tuple[str, int, Optional[int], str]:
dyn = self.is_dynfee()
mempool = self.use_mempool_fees()
if dyn:
if mempool:
method = 'mempool'
fee_level = self.get_depth_level()
value = self.depth_target(fee_level)
fee_rate = self.depth_to_fee(fee_level)
tooltip = self.depth_tooltip(value)
else:
method = 'ETA'
fee_level = self.get_fee_level()
value = self.eta_target(fee_level)
fee_rate = self.eta_to_fee(fee_level)
tooltip = self.eta_tooltip(value)
else:
method = 'static'
value = self.FEE_EST_STATIC_FEERATE
fee_rate = value
tooltip = 'static feerate'
return method, value, fee_rate, tooltip
def setfeerate(self, fee_method: str, value: int):
if fee_method == 'mempool':
if value not in FEE_DEPTH_TARGETS:
raise Exception(f"Error: fee_level must be in {FEE_DEPTH_TARGETS}")
self.FEE_EST_USE_MEMPOOL = True
self.FEE_EST_DYNAMIC = True
self.FEE_EST_DYNAMIC_MEMPOOL_SLIDERPOS = FEE_DEPTH_TARGETS.index(value)
elif fee_method == 'ETA':
if value not in FEE_ETA_TARGETS:
raise Exception(f"Error: fee_level must be in {FEE_ETA_TARGETS}")
self.FEE_EST_USE_MEMPOOL = False
self.FEE_EST_DYNAMIC = True
self.FEE_EST_DYNAMIC_ETA_SLIDERPOS = FEE_ETA_TARGETS.index(value)
elif fee_method == 'static':
self.FEE_EST_DYNAMIC = False
self.FEE_EST_STATIC_FEERATE = value
else:
raise Exception(f"Invalid parameter: {fee_method}. Valid methods are: ETA, mempool, static.")
def fee_per_byte(self):
"""Returns sat/vB fee to pay for a txn.
Note: might return None.
"""
fee_per_kb = self.fee_per_kb()
return fee_per_kb / 1000 if fee_per_kb is not None else None
def estimate_fee(self, size: Union[int, float, Decimal], *,
allow_fallback_to_static_rates: bool = False) -> int:
fee_per_kb = self.fee_per_kb()
if fee_per_kb is None:
if allow_fallback_to_static_rates:
fee_per_kb = FEERATE_FALLBACK_STATIC_FEE
else:
raise NoDynamicFeeEstimates()
return self.estimate_fee_for_feerate(fee_per_kb, size)
@classmethod
def estimate_fee_for_feerate(cls, fee_per_kb: Union[int, float, Decimal],
size: Union[int, float, Decimal]) -> int:
# note: 'size' is in vbytes
size = Decimal(size)
fee_per_kb = Decimal(fee_per_kb)
fee_per_byte = fee_per_kb / 1000
# to be consistent with what is displayed in the GUI,
# the calculation needs to use the same precision:
fee_per_byte = quantize_feerate(fee_per_byte)
return round(fee_per_byte * size)
def update_fee_estimates(self, nblock_target: int, fee_per_kb: int):
assert isinstance(nblock_target, int), f"expected int, got {nblock_target!r}"
assert isinstance(fee_per_kb, int), f"expected int, got {fee_per_kb!r}"
self.fee_estimates[nblock_target] = fee_per_kb
def is_fee_estimates_update_required(self):
"""Checks time since last requested and updated fee estimates.
Returns True if an update should be requested.
"""
now = time.time()
return now - self.last_time_fee_estimates_requested > 60
def requested_fee_estimates(self):
self.last_time_fee_estimates_requested = time.time()
def get_video_device(self):
device = self.VIDEO_DEVICE_PATH
if device == 'default':
@@ -1065,11 +681,7 @@ Warning: setting this to too low will result in lots of payment failures."""),
TEST_SHUTDOWN_FEE_RANGE = ConfigVar('test_shutdown_fee_range', default=None)
TEST_SHUTDOWN_LEGACY = ConfigVar('test_shutdown_legacy', default=False, type_=bool)
FEE_EST_DYNAMIC = ConfigVar('dynamic_fees', default=True, type_=bool)
FEE_EST_USE_MEMPOOL = ConfigVar('mempool_fees', default=False, type_=bool)
FEE_EST_STATIC_FEERATE = ConfigVar('fee_per_kb', default=FEERATE_FALLBACK_STATIC_FEE, type_=int)
FEE_EST_DYNAMIC_ETA_SLIDERPOS = ConfigVar('fee_level', default=2, type_=int)
FEE_EST_DYNAMIC_MEMPOOL_SLIDERPOS = ConfigVar('depth_level', default=2, type_=int)
FEE_POLICY = ConfigVar('fee_policy', default='eta:2', type_=str)
RPC_USERNAME = ConfigVar('rpcuser', default=None, type_=str)
RPC_PASSWORD = ConfigVar('rpcpassword', default=None, type_=str)
+15 -12
View File
@@ -37,6 +37,7 @@ from .json_db import StoredObject, stored_in
from . import constants
from .address_synchronizer import TX_HEIGHT_LOCAL
from .i18n import _
from .fee_policy import FeePolicy
from .bitcoin import construct_script
from .crypto import ripemd
@@ -166,17 +167,18 @@ def create_claim_tx(
*,
txin: PartialTxInput,
swap: SwapData,
config: 'SimpleConfig',
network: 'Network',
fee_policy: FeePolicy,
) -> PartialTransaction:
"""Create tx to either claim successful reverse-swap,
or to get refunded for timed-out forward-swap.
"""
# FIXME the mining fee should depend on swap.is_reverse.
# the txs are not the same size...
amount_sat = txin.value_sats() - SwapManager._get_fee(size=SWAP_TX_SIZE, config=config)
amount_sat = txin.value_sats() - SwapManager._get_fee(size=SWAP_TX_SIZE, fee_policy=fee_policy, network=network)
if amount_sat < dust_threshold():
raise BelowDustLimit()
txin, locktime = SwapManager.create_claim_txin(txin=txin, swap=swap, config=config)
txin, locktime = SwapManager.create_claim_txin(txin=txin, swap=swap)
txout = PartialTxOutput.from_address_and_value(swap.receive_address, amount_sat)
tx = PartialTransaction.from_io([txin], [txout], version=2, locktime=locktime)
sig = tx.sign_txin(0, txin.privkey)
@@ -200,6 +202,7 @@ class SwapManager(Logger):
self.wallet = wallet
self.config = wallet.config
self.fee_policy = FeePolicy(wallet.config.FEE_POLICY)
self.lnworker = lnworker
self.config = wallet.config
self.taskgroup = OldTaskGroup()
@@ -447,7 +450,7 @@ class SwapManager(Logger):
if spent_height is not None and not should_bump_fee:
return
try:
tx = create_claim_tx(txin=txin, swap=swap, config=self.wallet.config)
tx = create_claim_tx(txin=txin, swap=swap, fee_policy=self.fee_policy, network=self.network)
except BelowDustLimit:
self.logger.info('utxo value below dust threshold')
return
@@ -465,11 +468,11 @@ class SwapManager(Logger):
def get_fee(self, size):
# note: 'size' is in vbytes
return self._get_fee(size=size, config=self.wallet.config)
return self._get_fee(size=size, fee_policy=self.fee_policy, network=self.network)
@classmethod
def _get_fee(cls, *, size, config: 'SimpleConfig'):
return config.estimate_fee(size, allow_fallback_to_static_rates=True)
def _get_fee(cls, *, size, fee_policy: FeePolicy, network: 'Network'):
return fee_policy.estimate_fee(size, network=network, allow_fallback_to_static_rates=True)
def get_swap(self, payment_hash: bytes) -> Optional[SwapData]:
# for history
@@ -818,6 +821,7 @@ class SwapManager(Logger):
outputs=[funding_output],
rbf=True,
password=password,
fee_policy=self.fee_policy,
)
else:
tx.replace_output_address(DummyAddress.SWAP, swap.lockup_address)
@@ -1101,11 +1105,10 @@ class SwapManager(Logger):
@classmethod
def create_claim_txin(
cls,
*,
txin: PartialTxInput,
swap: SwapData,
config: 'SimpleConfig',
cls,
*,
txin: PartialTxInput,
swap: SwapData,
) -> PartialTransaction:
if swap.is_reverse: # successful reverse swap
locktime = 0
+34 -66
View File
@@ -59,7 +59,8 @@ from .util import (NotEnoughFunds, UserCancelled, profiler, OldTaskGroup, ignore
WalletFileException, BitcoinException,
InvalidPassword, format_time, timestamp_to_datetime, Satoshis,
Fiat, bfh, TxMinedInfo, quantize_feerate, OrderedDictWithIndex)
from .simple_config import SimpleConfig, FEE_RATIO_HIGH_WARNING, FEERATE_WARNING_HIGH_FEE
from .simple_config import SimpleConfig
from .fee_policy import FeePolicy, FeeMethod, FEE_RATIO_HIGH_WARNING, FEERATE_WARNING_HIGH_FEE
from .bitcoin import COIN, TYPE_ADDRESS
from .bitcoin import is_address, address_to_script, is_minikey, relayfee, dust_threshold
from .bitcoin import DummyAddress, DummyAddressUsedInTxException
@@ -177,30 +178,24 @@ async def sweep(
privkeys: Iterable[str],
*,
network: 'Network',
config: 'SimpleConfig',
to_address: str,
fee: int = None,
fee_policy: FeePolicy,
imax=100,
locktime=None,
tx_version=None) -> PartialTransaction:
inputs, keypairs = await sweep_preparations(privkeys, network, imax)
total = sum(txin.value_sats() for txin in inputs)
if fee is None:
outputs = [PartialTxOutput(scriptpubkey=bitcoin.address_to_script(to_address),
value=total)]
tx = PartialTransaction.from_io(inputs, outputs)
fee = config.estimate_fee(tx.estimated_size())
outputs = [PartialTxOutput(scriptpubkey=bitcoin.address_to_script(to_address), value=total)]
tx = PartialTransaction.from_io(inputs, outputs)
fee = fee_policy.estimate_fee(tx.estimated_size(), network=network)
if total - fee < 0:
raise Exception(_('Not enough funds on address.') + '\nTotal: %d satoshis\nFee: %d'%(total, fee))
if total - fee < dust_threshold(network):
raise Exception(_('Not enough funds on address.') + '\nTotal: %d satoshis\nFee: %d\nDust Threshold: %d'%(total, fee, dust_threshold(network)))
outputs = [PartialTxOutput(scriptpubkey=bitcoin.address_to_script(to_address),
value=total - fee)]
outputs = [PartialTxOutput(scriptpubkey=bitcoin.address_to_script(to_address), value=total - fee)]
if locktime is None:
locktime = get_locktime_for_new_transaction(network)
tx = PartialTransaction.from_io(inputs, outputs, locktime=locktime, version=tx_version)
tx.set_rbf(True)
tx.sign(keypairs)
@@ -941,10 +936,10 @@ class Abstract_Wallet(ABC, Logger, EventListener):
status = _('Unconfirmed')
if fee is None:
fee = self.adb.get_tx_fee(tx_hash)
if fee and self.network and self.config.has_fee_mempool():
if fee and self.network and self.network.has_fee_mempool():
size = tx.estimated_size()
fee_per_byte = fee / size
exp_n = self.config.fee_to_depth(fee_per_byte)
exp_n = self.network.mempool_fees.fee_to_depth(fee_per_byte)
can_bump = (is_any_input_ismine or is_swap) and self.can_rbf_tx(tx)
can_dscancel = (is_any_input_ismine and self.can_rbf_tx(tx, is_dscancel=True)
and not all([self.is_mine(txout.address) for txout in tx.outputs()]))
@@ -1697,10 +1692,10 @@ class Abstract_Wallet(ABC, Logger, EventListener):
fee_per_byte = fee / size
extra.append(format_fee_satoshis(fee_per_byte) + f" {util.UI_UNIT_NAME_FEERATE_SAT_PER_VB}")
if fee is not None and height in (TX_HEIGHT_UNCONF_PARENT, TX_HEIGHT_UNCONFIRMED) \
and self.config.has_fee_mempool():
exp_n = self.config.fee_to_depth(fee_per_byte)
and self.network and self.network.has_fee_mempool():
exp_n = self.network.mempool_fees.fee_to_depth(fee_per_byte)
if exp_n is not None:
extra.append(self.config.get_depth_mb_str(exp_n))
extra.append(FeePolicy.get_depth_mb_str(exp_n))
if height == TX_HEIGHT_LOCAL:
status = 3
elif height == TX_HEIGHT_UNCONF_PARENT:
@@ -1823,24 +1818,13 @@ class Abstract_Wallet(ABC, Logger, EventListener):
assert is_address(selected_addr), f"not valid bitcoin address: {selected_addr}"
return selected_addr
def can_pay_onchain(self, outputs, coins=None):
fee = partial(self.config.estimate_fee, allow_fallback_to_static_rates=True) # to avoid NoDynamicFeeEstimates
try:
self.make_unsigned_transaction(
coins=coins,
outputs=outputs,
fee=fee)
except NotEnoughFunds:
return False
return True
@profiler(min_threshold=0.1)
def make_unsigned_transaction(
self, *,
coins: Sequence[PartialTxInput],
outputs: List[PartialTxOutput],
inputs: Optional[List[PartialTxInput]] = None,
fee=None,
fee_policy: FeePolicy = None,
change_addr: str = None,
is_sweep: bool = False, # used by Wallet_2fa subclass
rbf: Optional[bool] = True,
@@ -1876,7 +1860,7 @@ class Abstract_Wallet(ABC, Logger, EventListener):
i_max_sum += weight
i_max.append((weight, i))
if fee is None and self.config.fee_per_kb() is None:
if fee_policy.method is not FeeMethod.FIXED and fee_policy.fee_per_kb(self.network) is None:
raise NoDynamicFeeEstimates()
for txin in coins:
@@ -1884,15 +1868,7 @@ class Abstract_Wallet(ABC, Logger, EventListener):
nSequence = 0xffffffff - (2 if rbf else 1)
txin.nsequence = nSequence
# Fee estimator
if fee is None:
fee_estimator = self.config.estimate_fee
elif isinstance(fee, Number):
fee_estimator = lambda size: fee
elif callable(fee):
fee_estimator = fee
else:
raise Exception(f'Invalid argument fee: {fee}')
fee_estimator = partial(fee_policy.estimate_fee, network=self.network)
# set if we merge with another transaction
rbf_merge_txid = None
@@ -2217,7 +2193,7 @@ class Abstract_Wallet(ABC, Logger, EventListener):
for item in coins:
self.add_input_info(item)
def fee_estimator(size):
return self.config.estimate_fee_for_feerate(fee_per_kb=new_fee_rate*1000, size=size)
return FeePolicy.estimate_fee_for_feerate(fee_per_kb=new_fee_rate*1000, size=size)
coin_chooser = coinchooser.get_coin_chooser(self.config)
try:
return coin_chooser.make_tx(
@@ -3100,41 +3076,33 @@ class Abstract_Wallet(ABC, Logger, EventListener):
pass
def create_transaction(
self,
outputs,
*,
fee=None,
feerate=None,
change_addr=None,
domain_addr=None,
domain_coins=None,
sign=True,
rbf=True,
password=None,
locktime=None,
tx_version: Optional[int] = None,
base_tx: Optional[PartialTransaction] = None,
inputs: Optional[List[PartialTxInput]] = None,
send_change_to_lightning: Optional[bool] = None,
nonlocal_only: bool = False,
BIP69_sort: bool = True,
self,
outputs,
*,
fee_policy: FeePolicy=None,
change_addr=None,
domain_addr=None,
domain_coins=None,
sign=True,
rbf=True,
password=None,
locktime=None,
tx_version: Optional[int] = None,
base_tx: Optional[PartialTransaction] = None,
inputs: Optional[List[PartialTxInput]] = None,
send_change_to_lightning: Optional[bool] = None,
nonlocal_only: bool = False,
BIP69_sort: bool = True,
) -> PartialTransaction:
"""Helper function for make_unsigned_transaction."""
if fee is not None and feerate is not None:
raise UserFacingException("Cannot specify both 'fee' and 'feerate' at the same time!")
coins = self.get_spendable_coins(domain_addr, nonlocal_only=nonlocal_only)
if domain_coins is not None:
coins = [coin for coin in coins if (coin.prevout.to_str() in domain_coins)]
if feerate is not None:
fee_per_kb = 1000 * Decimal(feerate)
fee_estimator = partial(SimpleConfig.estimate_fee_for_feerate, fee_per_kb)
else:
fee_estimator = fee
tx = self.make_unsigned_transaction(
coins=coins,
inputs=inputs,
outputs=outputs,
fee=fee_estimator,
fee_policy=fee_policy,
change_addr=change_addr,
base_tx=base_tx,
send_change_to_lightning=send_change_to_lightning,
+55
View File
@@ -0,0 +1,55 @@
from electrum.fee_policy import FeeHistogram
from . import ElectrumTestCase
class Test_FeeHistogram(ElectrumTestCase):
def setUp(self):
super(Test_FeeHistogram, self).setUp()
def tearDown(self):
super(Test_FeeHistogram, self).tearDown()
def test_depth_target_to_fee(self):
mempool_fees = FeeHistogram()
mempool_fees.set_data([[49, 100110], [10, 121301], [6, 153731], [5, 125872], [1, 36488810]])
self.assertEqual( 2 * 1000, mempool_fees.depth_target_to_fee(1000000))
self.assertEqual( 6 * 1000, mempool_fees.depth_target_to_fee( 500000))
self.assertEqual( 7 * 1000, mempool_fees.depth_target_to_fee( 250000))
self.assertEqual(11 * 1000, mempool_fees.depth_target_to_fee( 200000))
self.assertEqual(50 * 1000, mempool_fees.depth_target_to_fee( 100000))
mempool_fees.set_data([])
self.assertEqual( 1 * 1000, mempool_fees.depth_target_to_fee(10 ** 5))
self.assertEqual( 1 * 1000, mempool_fees.depth_target_to_fee(10 ** 6))
self.assertEqual( 1 * 1000, mempool_fees.depth_target_to_fee(10 ** 7))
mempool_fees.set_data([[1, 36488810]])
self.assertEqual( 2 * 1000, mempool_fees.depth_target_to_fee(10 ** 5))
self.assertEqual( 2 * 1000, mempool_fees.depth_target_to_fee(10 ** 6))
self.assertEqual( 2 * 1000, mempool_fees.depth_target_to_fee(10 ** 7))
self.assertEqual( 1 * 1000, mempool_fees.depth_target_to_fee(10 ** 8))
mempool_fees.set_data([[5, 125872], [1, 36488810]])
self.assertEqual( 6 * 1000, mempool_fees.depth_target_to_fee(10 ** 5))
self.assertEqual( 2 * 1000, mempool_fees.depth_target_to_fee(10 ** 6))
self.assertEqual( 2 * 1000, mempool_fees.depth_target_to_fee(10 ** 7))
self.assertEqual( 1 * 1000, mempool_fees.depth_target_to_fee(10 ** 8))
mempool_fees.set_data([])
self.assertEqual(1 * 1000, mempool_fees.depth_target_to_fee(10 ** 5))
mempool_fees.set_data(None)
self.assertEqual(None, mempool_fees.depth_target_to_fee(10 ** 5))
def test_fee_to_depth(self):
mempool_fees = FeeHistogram()
mempool_fees.set_data([[49, 100000], [10, 120000], [6, 150000], [5, 125000], [1, 36000000]])
self.assertEqual(100000, mempool_fees.fee_to_depth(500))
self.assertEqual(100000, mempool_fees.fee_to_depth(50))
self.assertEqual(100000, mempool_fees.fee_to_depth(49))
self.assertEqual(220000, mempool_fees.fee_to_depth(48))
self.assertEqual(220000, mempool_fees.fee_to_depth(10))
self.assertEqual(370000, mempool_fees.fee_to_depth(9))
self.assertEqual(370000, mempool_fees.fee_to_depth(6.5))
self.assertEqual(370000, mempool_fees.fee_to_depth(6))
self.assertEqual(495000, mempool_fees.fee_to_depth(5.5))
self.assertEqual(36495000, mempool_fees.fee_to_depth(0.5))
+5 -5
View File
@@ -9,7 +9,7 @@ from electrum.invoices import PR_UNPAID, PR_PAID, PR_UNCONFIRMED, BaseInvoice, I
from electrum.address_synchronizer import TX_HEIGHT_UNCONFIRMED
from electrum.transaction import Transaction, PartialTxOutput
from electrum.util import TxMinedInfo, InvoiceError
from electrum.fee_policy import FixedFeePolicy
class TestWalletPaymentRequests(ElectrumTestCase):
"""test 'incoming' invoices"""
@@ -72,7 +72,7 @@ class TestWalletPaymentRequests(ElectrumTestCase):
# get paid onchain
wallet2 = self.create_wallet2() # type: Standard_Wallet
outputs = [PartialTxOutput.from_address_and_value(pr.get_address(), pr.get_amount_sat())]
tx = wallet2.create_transaction(outputs=outputs, fee=5000)
tx = wallet2.create_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000))
wallet1.adb.receive_tx_callback(tx, TX_HEIGHT_UNCONFIRMED)
self.assertEqual(PR_UNCONFIRMED, wallet1.get_invoice_status(pr))
# tx gets mined
@@ -102,7 +102,7 @@ class TestWalletPaymentRequests(ElectrumTestCase):
# get paid onchain
wallet2 = self.create_wallet2() # type: Standard_Wallet
outputs = [PartialTxOutput.from_address_and_value(pr.get_address(), pr.get_amount_sat())]
tx = wallet2.create_transaction(outputs=outputs, fee=5000)
tx = wallet2.create_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000))
wallet1.adb.receive_tx_callback(tx, TX_HEIGHT_UNCONFIRMED)
self.assertEqual(PR_UNCONFIRMED, wallet1.get_invoice_status(pr))
# tx gets mined
@@ -132,7 +132,7 @@ class TestWalletPaymentRequests(ElectrumTestCase):
# get paid onchain
wallet2 = self.create_wallet2() # type: Standard_Wallet
outputs = [PartialTxOutput.from_address_and_value(pr.get_address(), pr.get_amount_sat())]
tx = wallet2.create_transaction(outputs=outputs, fee=5000)
tx = wallet2.create_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000))
wallet1.adb.receive_tx_callback(tx, TX_HEIGHT_UNCONFIRMED)
self.assertEqual(PR_UNCONFIRMED, wallet1.get_invoice_status(pr))
# tx mined in the past (before invoice creation)
@@ -201,7 +201,7 @@ class TestWalletPaymentRequests(ElectrumTestCase):
# pr2 gets paid onchain
wallet2 = self.create_wallet2() # type: Standard_Wallet
outputs = [PartialTxOutput.from_address_and_value(pr2.get_address(), pr2.get_amount_sat())]
tx = wallet2.create_transaction(outputs=outputs, fee=5000)
tx = wallet2.create_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000))
wallet1.adb.receive_tx_callback(tx, TX_HEIGHT_UNCONFIRMED)
self.assertEqual(PR_UNCONFIRMED, wallet1.get_invoice_status(pr2))
self.assertEqual(pr2, wallet1.get_request_by_addr(addr1))
+6 -9
View File
@@ -44,7 +44,7 @@ from electrum.lnutil import LOCAL, REMOTE
from electrum.invoices import PR_PAID, PR_UNPAID
from electrum.interface import GracefulDisconnect
from electrum.simple_config import SimpleConfig
from electrum.fee_policy import FeeTimeEstimates
from .test_lnchannel import create_test_channels
@@ -68,6 +68,7 @@ class MockNetwork:
self.callbacks = defaultdict(list)
self.lnwatcher = None
self.interface = None
self.fee_estimates = FeeTimeEstimates()
self.config = config
self.asyncio_loop = util.get_asyncio_loop()
self.channel_db = ChannelDB(self)
@@ -1282,10 +1283,8 @@ class TestPeerDirect(TestPeer):
bob_channel.config[HTLCOwner.LOCAL].upfront_shutdown_script = b''
p1, p2, w1, w2, q1, q2 = self.prepare_peers(alice_channel, bob_channel)
w1.network.config.FEE_EST_DYNAMIC = False
w2.network.config.FEE_EST_DYNAMIC = False
w1.network.config.FEE_EST_STATIC_FEERATE = 5000
w2.network.config.FEE_EST_STATIC_FEERATE = 1000
w1.network.config.FEE_POLICY = 'feerate:5000'
w2.network.config.FEE_POLICY = 'feerate:1000'
async def test():
async def close():
@@ -1316,10 +1315,8 @@ class TestPeerDirect(TestPeer):
bob_channel.config[HTLCOwner.LOCAL].upfront_shutdown_script = bob_uss
p1, p2, w1, w2, q1, q2 = self.prepare_peers(alice_channel, bob_channel)
w1.network.config.FEE_EST_DYNAMIC = False
w2.network.config.FEE_EST_DYNAMIC = False
w1.network.config.FEE_EST_STATIC_FEERATE = 5000
w2.network.config.FEE_EST_STATIC_FEERATE = 1000
w1.network.config.FEE_POLICY = 'feerate:5000'
w2.network.config.FEE_POLICY = 'feerate:1000'
async def test():
async def close():
-40
View File
@@ -203,46 +203,6 @@ class Test_SimpleConfig(ElectrumTestCase):
with self.assertRaises(KeyError):
config.cv.from_key("server333")
def test_depth_target_to_fee(self):
config = SimpleConfig(self.options)
config.mempool_fees = [[49, 100110], [10, 121301], [6, 153731], [5, 125872], [1, 36488810]]
self.assertEqual( 2 * 1000, config.depth_target_to_fee(1000000))
self.assertEqual( 6 * 1000, config.depth_target_to_fee( 500000))
self.assertEqual( 7 * 1000, config.depth_target_to_fee( 250000))
self.assertEqual(11 * 1000, config.depth_target_to_fee( 200000))
self.assertEqual(50 * 1000, config.depth_target_to_fee( 100000))
config.mempool_fees = []
self.assertEqual( 1 * 1000, config.depth_target_to_fee(10 ** 5))
self.assertEqual( 1 * 1000, config.depth_target_to_fee(10 ** 6))
self.assertEqual( 1 * 1000, config.depth_target_to_fee(10 ** 7))
config.mempool_fees = [[1, 36488810]]
self.assertEqual( 2 * 1000, config.depth_target_to_fee(10 ** 5))
self.assertEqual( 2 * 1000, config.depth_target_to_fee(10 ** 6))
self.assertEqual( 2 * 1000, config.depth_target_to_fee(10 ** 7))
self.assertEqual( 1 * 1000, config.depth_target_to_fee(10 ** 8))
config.mempool_fees = [[5, 125872], [1, 36488810]]
self.assertEqual( 6 * 1000, config.depth_target_to_fee(10 ** 5))
self.assertEqual( 2 * 1000, config.depth_target_to_fee(10 ** 6))
self.assertEqual( 2 * 1000, config.depth_target_to_fee(10 ** 7))
self.assertEqual( 1 * 1000, config.depth_target_to_fee(10 ** 8))
config.mempool_fees = []
self.assertEqual(1 * 1000, config.depth_target_to_fee(10 ** 5))
config.mempool_fees = None
self.assertEqual(None, config.depth_target_to_fee(10 ** 5))
def test_fee_to_depth(self):
config = SimpleConfig(self.options)
config.mempool_fees = [[49, 100000], [10, 120000], [6, 150000], [5, 125000], [1, 36000000]]
self.assertEqual(100000, config.fee_to_depth(500))
self.assertEqual(100000, config.fee_to_depth(50))
self.assertEqual(100000, config.fee_to_depth(49))
self.assertEqual(220000, config.fee_to_depth(48))
self.assertEqual(220000, config.fee_to_depth(10))
self.assertEqual(370000, config.fee_to_depth(9))
self.assertEqual(370000, config.fee_to_depth(6.5))
self.assertEqual(370000, config.fee_to_depth(6))
self.assertEqual(495000, config.fee_to_depth(5.5))
self.assertEqual(36495000, config.fee_to_depth(0.5))
class TestUserConfig(ElectrumTestCase):
+6 -4
View File
@@ -2,6 +2,7 @@ from electrum import SimpleConfig
from electrum.util import bfh
from electrum.transaction import PartialTxInput, TxOutpoint
from electrum.submarine_swaps import SwapData, create_claim_tx
from electrum.fee_policy import FeePolicy
from . import ElectrumTestCase
@@ -13,8 +14,7 @@ class TestSwapTxs(ElectrumTestCase):
super().setUp()
self.maxDiff = None
self.config = SimpleConfig({'electrum_path': self.electrum_path})
self.config.FEE_EST_DYNAMIC = False
self.config.FEE_EST_STATIC_FEERATE = 1000
self.fee_policy = FeePolicy('feerate:1000')
def test_claim_tx_for_successful_reverse_swap(self):
swap_data = SwapData(
@@ -39,7 +39,8 @@ class TestSwapTxs(ElectrumTestCase):
tx = create_claim_tx(
txin=txin,
swap=swap_data,
config=self.config,
fee_policy=self.fee_policy,
network=None,
)
self.assertEqual(
"02000000000101f9db8580febd5c0f85b6f1576c83f7739109e3a2d772743e3217e9537fea7e890000000000fdffffff019007030000000000160014fbfad1ca8741ce640a3ea130bd4478fdd8a2dd8f03473044022025506044aba4939f4f2faa94710673ca65530a621f1fa538a3d046dc98bb685e02205f8d463dc6f81e1083f26fa963e581dabc80ea42f8cd59c9e31f3bf531168a9c0120f1939b5723155713855d7ebea6e174f77d41d669269e7f138856c3de190e7a366a8201208763a914d7a62ef0270960fe23f0f351b28caadab62c21838821030bfd61153816df786036ea293edce851d3a4b9f4a1c66bdc1a17f00ffef3d6b167750334ef24b1752102fc8128f17f9e666ea281c702171ab16c1dd2a4337b71f08970f5aa10c608a93268ac00000000",
@@ -69,7 +70,8 @@ class TestSwapTxs(ElectrumTestCase):
tx = create_claim_tx(
txin=txin,
swap=swap_data,
config=self.config,
fee_policy=self.fee_policy,
network=None,
)
self.assertEqual(
"0200000000010106871505e5f6dc76f406f38e34e29b54908c6b54da978c28c18fb39ab1dcec080000000000fdffffff013afb01000000000016001497b4b718e7d06c9c43cd3bcf37905041b718b81f0347304402200ae708af1393f785c541bbc4d7351791b76a53077a292b71cb2a25ad13a15f9902206b7b91c414ec0d6e5098a1acc26de4b47f3aac414b7a49741e8f27cc6a967a19010065a914b12bd886ef4fd9ef1c03e899123f2c4b96cec0878763210267ca676c2ed05bb6c380880f1e50b6ef91025dfa963dc49d6c5cb9848f2acf7d670339ef24b1752103d8190cdfcc7dd929a583b7ea8fa8eb1d8463195d336be2f2df94f950ce8b659968ac39ef2400",
+62 -61
View File
@@ -16,6 +16,7 @@ from electrum.wallet import (sweep, Multisig_Wallet, Standard_Wallet, Imported_W
TransactionPotentiallyDangerousException, TransactionDangerousException,
TxSighashRiskLevel)
from electrum.util import bfh, NotEnoughFunds, UnrelatedTransactionException, UserFacingException, TxMinedInfo
from electrum.fee_policy import FixedFeePolicy
from electrum.transaction import Transaction, PartialTxOutput, tx_from_any, Sighash
from electrum.mnemonic import calc_seed_type
from electrum.network import Network
@@ -872,7 +873,7 @@ class TestWalletSending(ElectrumTestCase):
# wallet1 -> wallet2
outputs = [PartialTxOutput.from_address_and_value(wallet2.get_receiving_address(), 250000)]
tx = wallet1.create_transaction(outputs=outputs, password=None, fee=5000, tx_version=1, rbf=False)
tx = wallet1.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), tx_version=1, rbf=False)
self.assertTrue(tx.is_complete())
self.assertTrue(tx.is_segwit())
@@ -891,7 +892,7 @@ class TestWalletSending(ElectrumTestCase):
# wallet2 -> wallet1
outputs = [PartialTxOutput.from_address_and_value(wallet1.get_receiving_address(), 100000)]
tx = wallet2.create_transaction(outputs=outputs, password=None, fee=5000, tx_version=1, rbf=False)
tx = wallet2.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), tx_version=1, rbf=False)
self.assertTrue(tx.is_complete())
self.assertFalse(tx.is_segwit())
@@ -944,7 +945,7 @@ class TestWalletSending(ElectrumTestCase):
# wallet1 -> wallet2
outputs = [PartialTxOutput.from_address_and_value(wallet2.get_receiving_address(), 370000)]
tx = wallet1a.create_transaction(outputs=outputs, password=None, fee=5000, tx_version=1, rbf=False)
tx = wallet1a.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), tx_version=1, rbf=False)
partial_tx = tx.serialize_as_bytes().hex()
self.assertEqual("70736274ff01007501000000017120d4e1f2cdfe7df000d632cff74167fb354f0546d5cfc228e5c98756d55cb20100000000feffffff0250a50500000000001976a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac2862b1000000000017a9142e517854aa54668128c0e9a3fdd4dec13ad571368700000000000100e0010000000001014121f99dc02f0364d2dab3d08905ff4c36fc76c55437fd90b769c35cc18618280100000000fdffffff02d4c22d00000000001600143fd1bc5d32245850c8cb5be5b09c73ccbb9a0f75001bb7000000000017a91480c2353f6a7bc3c71e99e062655b19adb3dd2e4887024830450221008781c78df0c9d4b5ea057333195d5d76bc29494d773f14fa80e27d2f288b2c360220762531614799b6f0fb8d539b18cb5232ab4253dd4385435157b28a44ff63810d0121033de77d21926e09efd04047ae2d39dbd3fb9db446e8b7ed53e0f70f9c9478f735dac11300220202afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f284730440220751ee3599e59debb8b2aeef61bb5f574f26379cd961caf382d711a507bc632390220598d53e62557c4a5ab8cfb2f8948f37cca06a861714b55c781baf2c3d7a580b501010469522102afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f2821030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf2103e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce53ae220602afb4af9a91264e1c6dce3ebe5312801723270ac0ba8134b7b49129328fcb0f280c0036e9ac00000000000000002206030b482838721a38d94847699fed8818b5c5f56500ef72f13489e365b65e5749cf0c48adc7a00000000000000000220603e5db7969ae2f2576e6a061bf3bb2db16571e77ffb41e0b27170734359235cbce0cdb692427000000000000000000000100695221022ec6f62b0f3b7c2446f44346bff0a6f06b5fdbc27368be8a36478e0287fe47be21024238f21f90527dc87e945f389f3d1711943b06f0a738d5baab573fc0ab6c98582102b7139e93747d7c77f62af5a38b8a2b009f3456aa94dea9bf21f73a6298c867a253ae2202022ec6f62b0f3b7c2446f44346bff0a6f06b5fdbc27368be8a36478e0287fe47be0cdb69242701000000000000002202024238f21f90527dc87e945f389f3d1711943b06f0a738d5baab573fc0ab6c98580c0036e9ac0100000000000000220202b7139e93747d7c77f62af5a38b8a2b009f3456aa94dea9bf21f73a6298c867a20c48adc7a0010000000000000000",
partial_tx)
@@ -969,7 +970,7 @@ class TestWalletSending(ElectrumTestCase):
# wallet2 -> wallet1
outputs = [PartialTxOutput.from_address_and_value(wallet1a.get_receiving_address(), 100000)]
tx = wallet2.create_transaction(outputs=outputs, password=None, fee=5000, tx_version=1, rbf=False, sign=False)
tx = wallet2.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), tx_version=1, rbf=False, sign=False)
self.assertEqual(
"pkh(045f7ba332df2a7b4f5d13f246e307c9174cfa9b8b05f3b83410a3c23ef8958d610be285963d67c7bc1feb082f168fa9877c25999963ff8b56b242a852b23e25ed)",
tx.inputs()[0].script_descriptor.to_string_no_checksum())
@@ -1043,7 +1044,7 @@ class TestWalletSending(ElectrumTestCase):
# wallet1 -> wallet2
outputs = [PartialTxOutput.from_address_and_value(wallet2a.get_receiving_address(), 165000)]
tx = wallet1a.create_transaction(outputs=outputs, password=None, fee=5000, tx_version=1, rbf=False, sign=False)
tx = wallet1a.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), tx_version=1, rbf=False, sign=False)
self.assertEqual((0, 2), tx.signature_count())
self.assertEqual(
"wsh(sortedmulti(2,[b2e35a7d/1h]tpubD9aPYLPPYw8MxU3cD57LwpV5v7GomHxdv62MSbPcRkp47zwXx69ACUFsKrj8xzuzRrij9FWVhfvkvNqtqsr8ZtefkDsGZ9GLuHzoS6bXyk1/0/0,[53b77ddb/1h]tpubD8spLJysN7v7V1KHvkZ7AwjnXShKafopi7Vu3Ahs2S46FxBPTode8DgGxDo55k4pJvETGScZFwnM5f2Y31EUjteJdhxR73sjr9ieydgah2U/0/0,[43067d63/1h]tpubD8khd1g1tzFeKeaU59QV811hyvhwn9KDfy5sqFJ5m2wJLw6rUt4AZviqutRPXTUAK4SpU2we3y2WBP916Ma8Em4qFGcbYkFvXVfpGYV3oZR/0/0))",
@@ -1078,7 +1079,7 @@ class TestWalletSending(ElectrumTestCase):
# wallet2 -> wallet1
outputs = [PartialTxOutput.from_address_and_value(wallet1a.get_receiving_address(), 100000)]
tx = wallet2a.create_transaction(outputs=outputs, password=None, fee=5000, tx_version=1, rbf=False)
tx = wallet2a.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), tx_version=1, rbf=False)
self.assertEqual((1, 2), tx.signature_count())
self.assertEqual(
"sh(wsh(sortedmulti(2,[d1dbcc21]tpubDDsv4RpsGViZeEVwivuj3aaKhFQSv1kYsz64mwRoHkqBfw8qBSYEmc8TtyVGotJb44V3pviGzefP9m9hidRg9dPPaDWL2yoRpMW3hdje3Rk/0/0,[17cea914]tpubDCZU2kACPGACYDvAXvZUXQ7cE7msFfCtpah5QCuaz8iarKMLTgR4c2u8RGKdFhbb3YJxzmktDd1rCtF58ksyVgFw28pchY55uwkDiXjY9hU/0/0)))",
@@ -1140,7 +1141,7 @@ class TestWalletSending(ElectrumTestCase):
# wallet1 -> wallet2
outputs = [PartialTxOutput.from_address_and_value(wallet2.get_receiving_address(), 1000000)]
tx = wallet1a.create_transaction(outputs=outputs, password=None, fee=5000, tx_version=1, rbf=False)
tx = wallet1a.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), tx_version=1, rbf=False)
self.assertTrue(tx.is_complete())
self.assertFalse(tx.is_segwit())
@@ -1159,7 +1160,7 @@ class TestWalletSending(ElectrumTestCase):
# wallet2 -> wallet1
outputs = [PartialTxOutput.from_address_and_value(wallet1a.get_receiving_address(), 300000)]
tx = wallet2.create_transaction(outputs=outputs, password=None, fee=5000, tx_version=1, rbf=False)
tx = wallet2.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), tx_version=1, rbf=False)
self.assertTrue(tx.is_complete())
self.assertTrue(tx.is_segwit())
@@ -1266,7 +1267,7 @@ class TestWalletSending(ElectrumTestCase):
# create tx
outputs = [PartialTxOutput.from_address_and_value('2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', 2500000)]
coins = wallet.get_spendable_coins(domain=None)
tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee=5000)
tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(5000))
tx.set_rbf(True)
tx.locktime = 1325501
tx.version = 1
@@ -1526,7 +1527,7 @@ class TestWalletSending(ElectrumTestCase):
# create tx
outputs = [PartialTxOutput.from_address_and_value('2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', 2500000)]
coins = wallet.get_spendable_coins(domain=None)
tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee=5000)
tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(5000))
tx.set_rbf(True)
tx.locktime = 1325499
tx.version = 1
@@ -1706,7 +1707,7 @@ class TestWalletSending(ElectrumTestCase):
# create tx
outputs = [PartialTxOutput.from_address_and_value('tb1q7rl9cxr85962ztnsze089zs8ycv52hk43f3m9n', '!')]
coins = wallet.get_spendable_coins(domain=None)
tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee=5000)
tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(5000))
tx.set_rbf(True)
tx.locktime = 1325499
if simulate_moving_txs:
@@ -1767,7 +1768,7 @@ class TestWalletSending(ElectrumTestCase):
# create tx
outputs = [PartialTxOutput.from_address_and_value('2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', '!')]
coins = wallet.get_spendable_coins(domain=None)
tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee=5000)
tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(5000))
tx.set_rbf(True)
tx.locktime = 1325499
tx.version = 1
@@ -1831,7 +1832,7 @@ class TestWalletSending(ElectrumTestCase):
# create tx
outputs = [PartialTxOutput.from_address_and_value('2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', '!')]
coins = wallet.get_spendable_coins(domain=None)
tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee=5000)
tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(5000))
tx.set_rbf(True)
tx.locktime = 1325499
tx.version = 1
@@ -1904,7 +1905,7 @@ class TestWalletSending(ElectrumTestCase):
# create tx
outputs = [PartialTxOutput.from_address_and_value('2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', 2_500_000)]
coins = wallet.get_spendable_coins(domain=None)
tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee=5000)
tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(5000))
tx.set_rbf(True)
tx.locktime = 1325499
tx.version = 1
@@ -1943,7 +1944,7 @@ class TestWalletSending(ElectrumTestCase):
# no new input will be needed. just a new output, and change decreased.
outputs = [PartialTxOutput.from_address_and_value('tb1qy6xmdj96v5dzt3j08hgc05yk3kltqsnmw4r6ry', 2_500_000)]
coins = wallet.get_spendable_coins(domain=None)
tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee=20000)
tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(20000))
tx.set_rbf(True)
tx.locktime = 1325499
tx.version = 1
@@ -1974,7 +1975,7 @@ class TestWalletSending(ElectrumTestCase):
# new input will be needed!
outputs = [PartialTxOutput.from_address_and_value('2NCVwbmEpvaXKHpXUGJfJr9iB5vtRN3vcut', 6_000_000)]
coins = wallet.get_spendable_coins(domain=None)
tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee=100_000)
tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(100000))
tx.set_rbf(True)
tx.locktime = 1325499
tx.version = 1
@@ -2028,7 +2029,7 @@ class TestWalletSending(ElectrumTestCase):
self.assertEqual(2, len(coins))
wallet.config.WALLET_BATCH_RBF = batch_rbf
tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee=1000)
tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(1000))
tx.set_rbf(True)
tx.locktime = 2423302
tx.version = 2
@@ -2051,7 +2052,7 @@ class TestWalletSending(ElectrumTestCase):
# first payment to dest_addr
outputs1 = [PartialTxOutput.from_address_and_value(dest_addr, 200_000)]
coins = wallet.get_spendable_coins(domain=None)
tx1 = wallet.make_unsigned_transaction(coins=coins, outputs=outputs1, fee=2000)
tx1 = wallet.make_unsigned_transaction(coins=coins, outputs=outputs1, fee_policy=FixedFeePolicy(2000))
tx1.set_rbf(True)
tx1.locktime = 2534850
tx1.version = 2
@@ -2066,7 +2067,7 @@ class TestWalletSending(ElectrumTestCase):
# second payment to dest_addr (merged)
outputs2 = [PartialTxOutput.from_address_and_value(dest_addr, 100_000)]
coins = wallet.get_spendable_coins(domain=None)
tx2 = wallet.make_unsigned_transaction(coins=coins, outputs=outputs2, fee=3000)
tx2 = wallet.make_unsigned_transaction(coins=coins, outputs=outputs2, fee_policy=FixedFeePolicy(3000))
tx2.set_rbf(True)
tx2.locktime = 2534850
tx2.version = 2
@@ -2085,7 +2086,7 @@ class TestWalletSending(ElectrumTestCase):
# second payment to dest_addr (not merged, just duplicate outputs)
outputs2 = [PartialTxOutput.from_address_and_value(dest_addr, 100_000)]
coins = wallet.get_spendable_coins(domain=None)
tx3 = wallet.make_unsigned_transaction(coins=coins, outputs=outputs2, fee=3000)
tx3 = wallet.make_unsigned_transaction(coins=coins, outputs=outputs2, fee_policy=FixedFeePolicy(3000))
tx3.set_rbf(True)
tx3.locktime = 2534850
tx3.version = 2
@@ -2165,7 +2166,7 @@ class TestWalletSending(ElectrumTestCase):
privkeys = ['93NQ7CFbwTPyKDJLXe97jczw33fiLijam2SCZL3Uinz1NSbHrTu',]
network = NetworkMock()
dest_addr = 'tb1q3ws2p0qjk5vrravv065xqlnkckvzcpclk79eu2'
tx = await sweep(privkeys, network=network, config=self.config, to_address=dest_addr, fee=5000, locktime=1325785, tx_version=1)
tx = await sweep(privkeys, network=network, to_address=dest_addr, fee_policy=FixedFeePolicy(5000), locktime=1325785, tx_version=1)
tx_copy = tx_from_any(tx.serialize())
self.assertEqual('010000000129349e5641d79915e9d0282fdbaee8c3df0b6731bab9d70bf626e8588bde24ac010000004847304402206bf0d0a93abae0d5873a62ebf277a5dd2f33837821e8b93e74d04e19d71b578002201a6d729bc159941ef5c4c9e5fe13ece9fc544351ba531b00f68ba549c8b38a9a01fdffffff01b82e0f00000000001600148ba0a0bc12b51831f58c7ea8607e76c5982c071fd93a1400',
@@ -2190,7 +2191,7 @@ class TestWalletSending(ElectrumTestCase):
privkeys = ['cUygTZe4jZLVwE4G44NznCPTeGvgsgassqucUHkAJxGC71Rst2kH',]
network = NetworkMock()
dest_addr = 'tb1q5uy5xjcn55gwdkmghht8yp3vwz3088f6e3e0em'
tx = await sweep(privkeys, network=network, config=self.config, to_address=dest_addr, fee=5000, locktime=2420006, tx_version=2)
tx = await sweep(privkeys, network=network, to_address=dest_addr, fee_policy=FixedFeePolicy(5000), locktime=2420006, tx_version=2)
tx_copy = tx_from_any(tx.serialize())
self.assertEqual('02000000015eb359ccfcd67c3e6b10bb937a796807007708c1f413840d0e627a3f94a1a48401000000484730440220043fc85a43e918ac41e494e309fdf204ca245d260cb5ea09108b196ca65d8a09022056f852f0f521e79ab2124d7e9f779c7290329ce5628ef8e92601980b065d3eb501fdffffff017f9e010000000000160014a709434b13a510e6db68bdd672062c70a2f39d3a26ed2400',
@@ -2215,7 +2216,7 @@ class TestWalletSending(ElectrumTestCase):
privkeys = ['p2pkh:91gxDahzHiJ63HXmLP7pvZrkF8i5gKBXk4VqWfhbhJjtf6Ni5NU',]
network = NetworkMock()
dest_addr = 'tb1q3ws2p0qjk5vrravv065xqlnkckvzcpclk79eu2'
tx = await sweep(privkeys, network=network, config=self.config, to_address=dest_addr, fee=5000, locktime=2420010, tx_version=2)
tx = await sweep(privkeys, network=network, to_address=dest_addr, fee_policy=FixedFeePolicy(5000), locktime=2420010, tx_version=2)
tx_copy = tx_from_any(tx.serialize())
self.assertEqual('02000000010615142d6c296f276c7da9fb2b09f655d594f73b76740404f1424c66c78ca715000000008a47304402206d2dae571ca2f51e0d4a8ce6a6335fa25ac09f4bbed26439124d93f035bdbb130220249dc2039f1da338a40679f0e79c25a2dc2983688e6c04753348f2aa8435e375014104b875ab889006d4a9be8467c9256cf54e1073f7f9a037604f571cc025bbf47b2987b4c862d5b687bb5328adccc69e67a17b109b6328228695a1c384573acd6199fdffffff0186500300000000001600148ba0a0bc12b51831f58c7ea8607e76c5982c071f2aed2400',
@@ -2240,7 +2241,7 @@ class TestWalletSending(ElectrumTestCase):
privkeys = ['p2pkh:cN3LiXmurmGRF5xngYd8XS2ZsP2KeXFUh4SH7wpC8uJJzw52JPq1',]
network = NetworkMock()
dest_addr = 'tb1q782f750ekkxysp2rrscr6yknmn634e2pv8lktu'
tx = await sweep(privkeys, network=network, config=self.config, to_address=dest_addr, fee=1000, locktime=2420010, tx_version=2)
tx = await sweep(privkeys, network=network, to_address=dest_addr, fee_policy=FixedFeePolicy(1000), locktime=2420010, tx_version=2)
tx_copy = tx_from_any(tx.serialize())
self.assertEqual('02000000016717835a2e1e152a69e7528a0f1346c1d37ee6e76c5e23b5d1c5a5b40241768a000000006a473044022038ad38003943bfd3ed39ba4340d545753fcad632a8fe882d01e4f0140ddb3cfb022019498260e29f5fbbcde9176bfb3553b7acec5fe284a9a3a33547a2d082b60355012103b875ab889006d4a9be8467c9256cf54e1073f7f9a037604f571cc025bbf47b29fdffffff0158de010000000000160014f1d49f51f9b58c4805431c303d12d3dcf51ae5412aed2400',
@@ -2265,7 +2266,7 @@ class TestWalletSending(ElectrumTestCase):
privkeys = ['p2wpkh-p2sh:cQMRGsiEsFX5YoxVZaMEzBruAkCWnoFf1SG7SRm2tLHDEN165TrA',]
network = NetworkMock()
dest_addr = 'tb1qu7n2tzm90a3f29kvxlhzsc7t40ddk075ut5w44'
tx = await sweep(privkeys, network=network, config=self.config, to_address=dest_addr, fee=500, locktime=2420010, tx_version=2)
tx = await sweep(privkeys, network=network, to_address=dest_addr, fee_policy=FixedFeePolicy(500), locktime=2420010, tx_version=2)
tx_copy = tx_from_any(tx.serialize())
self.assertEqual('020000000001011d1725072a6e60687a59b878ecaf940ea0385880613d9d5502102bd78ef97b9a0000000017160014e7a6a58b657f629516cc37ee2863cbabdadb3fd4fdffffff01fc47020000000000160014e7a6a58b657f629516cc37ee2863cbabdadb3fd402473044022048ea4c558fd374f5d5066440a7f4933393cb377802cb949e3039fedf0378a29402204b4a58c591117cc1e37f07b03cc03cc6198dbf547e2bff813e2e2102bd2057e00121029f46ba81b3c6ad84e52841364dc54ca1097d0c30a68fb529766504c4b1c599352aed2400',
@@ -2290,7 +2291,7 @@ class TestWalletSending(ElectrumTestCase):
privkeys = ['p2wpkh:cV2BvgtpLNX328m4QrhqycBGA6EkZUFfHM9kKjVXjfyD53uNfC4q',]
network = NetworkMock()
dest_addr = 'tb1qhuy2e45lrdcp9s4ezeptx5kwxcnahzgpar9scc'
tx = await sweep(privkeys, network=network, config=self.config, to_address=dest_addr, fee=500, locktime=2420010, tx_version=2)
tx = await sweep(privkeys, network=network, to_address=dest_addr, fee_policy=FixedFeePolicy(500), locktime=2420010, tx_version=2)
tx_copy = tx_from_any(tx.serialize())
self.assertEqual('02000000000101e328aeb4f9dc1b85a2709ce59b0478a15ed9fb5e7f84fb62422f99b8cd6ad7010000000000fdffffff01087e010000000000160014bf08acd69f1b7012c2b91642b352ce3627db89010247304402204993099c4663d92ef4c9a28b3f45a40a6585754fe22ecfdc0a76c43fda7c9d04022006a75e0fd3ad1862d8e81015a71d2a1489ec7a9264e6e63b8fe6bb90c27e799b0121038ca94e7c715152fd89803c2a40a934c7c4035fb87b3cba981cd1e407369cfe312aed2400',
@@ -2325,7 +2326,7 @@ class TestWalletSending(ElectrumTestCase):
# wallet1 creates tx1, with output back to himself
outputs = [PartialTxOutput.from_address_and_value("tb1qhye4wfp26kn0l7ynpn5a4hvt539xc3zf0n76t3", 10_000_000)]
tx1 = wallet1.create_transaction(outputs=outputs, fee=5000, tx_version=2, rbf=True, sign=False)
tx1 = wallet1.create_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), tx_version=2, rbf=True, sign=False)
tx1.locktime = 1607022
partial_tx1 = tx1.serialize_as_bytes().hex()
self.assertEqual("70736274ff0100710200000001d5bd4f8ebe63f0521f94e2d174b95d4327757a7e74fda3c9ff5c08796318f8d80000000000fdffffff02b82e0f0000000000160014250dbabd5761d7e0773d6147699938dd08ec2eb88096980000000000160014b93357242ad5a6fff8930ce9dadd8ba44a6c44496e8518000001011fc0d8a70000000000160014aba1c9faecc3f8882e641583e8734a3f9d01b15a0100df0200000000010162ecbac2f0c8662f53505d9410fdc56c84c5642ddbd3358d9a27d564e26731130200000000fdffffff02c0d8a70000000000160014aba1c9faecc3f8882e641583e8734a3f9d01b15ab89ed5000000000016001470afbd97b2dc351bd167f714e294b2fd3b60aedf02483045022100c93449989510e279eb14a0193d5c262ae93034b81376a1f6be259c6080d3ba5d0220536ab394f7c20f301d7ec2ef11be6e7b6d492053dce56458931c1b54218ec0fd012103b8f5a11df8e68cf335848e83a41fdad3c7413dc42148248a3799b58c93919ca01085180022060205e8db1b1906219782fadb18e763c0874a3118a17ce931e01707cbde194e041510775087560000008000000000000000000022020240ef5d2efee3b04b313a254df1b13a0b155451581e73943b21f3346bf6e1ba351077508756000000800100000000000000002202024a410b1212e88573561887b2bc38c90c074e4be425b9f3d971a9207825d9d3c8107750875600000080000000000100000000",
@@ -2337,7 +2338,7 @@ class TestWalletSending(ElectrumTestCase):
# wallet2 creates tx2, with output back to himself
outputs = [PartialTxOutput.from_address_and_value("tb1qufnj5k2rrsnpjq7fg6d2pq3q9um6skdyyehw5m", 10_000_000)]
tx2 = wallet2.create_transaction(outputs=outputs, fee=5000, tx_version=2, rbf=True, sign=False)
tx2 = wallet2.create_transaction(outputs=outputs, fee_policy=FixedFeePolicy(5000), tx_version=2, rbf=True, sign=False)
tx2.locktime = 1607023
partial_tx2 = tx2.serialize_as_bytes().hex()
self.assertEqual("70736274ff0100710200000001e546bc0a7c9736e82a07df5c24fe6d05df58a310dc376cf09302842ca7264f930100000000fdffffff02988d07000000000016001453675a59be834aa6d139c3ebea56646a9b160c4c8096980000000000160014e2672a59431c261903c9469aa082202f37a859a46f8518000001011fa037a000000000001600140719d12228c61cab793ecd659c09cfe565a845c30100df02000000000101d5bd4f8ebe63f0521f94e2d174b95d4327757a7e74fda3c9ff5c08796318f8d80100000000fdffffff025066350000000000160014e3aa82aa2e754507d5585c0b6db06cc0cb4927b7a037a000000000001600140719d12228c61cab793ecd659c09cfe565a845c302483045022100f42e27519bd2379c22951c16b038fa6d49164fe6802854f2fdc7ee87fe31a8bc02204ea71e9324781b44bf7fea2f318caf3bedc5b497cbd1b4313fa71f833500bcb7012103a7853e1ee02a1629c8e870ec694a1420aeb98e6f5d071815257028f62d6f784169851800220602275b4fba18bb34e5198a9cfb3e940306658839079b3bda50d504a9cf2bae36f41067f36697000000800000000001000000002202036e4d0a5fb845b2f1c3c868c2ce7212b155b73e91c05be1b7a77c48830831ba4f1067f366970000008001000000000000000022020200062fdea2b0a056b17fa6b91dd87f5b5d838fe1ee84d636a5022f9a340eebcc1067f3669700000080000000000000000000",
@@ -2411,7 +2412,7 @@ class TestWalletSending(ElectrumTestCase):
# create tx
outputs = [PartialTxOutput.from_address_and_value('tb1qfrlx5pza9vmez6vpx7swt8yp0nmgz3qa7jjkuf', 100_000)]
coins = wallet_2of2.get_spendable_coins(domain=None)
tx = wallet_2of2.make_unsigned_transaction(coins=coins, outputs=outputs, fee=5000)
tx = wallet_2of2.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(5000))
tx.set_rbf(True)
tx.locktime = 1665628
@@ -2472,7 +2473,7 @@ class TestWalletSending(ElectrumTestCase):
# create tx
outputs = [PartialTxOutput.from_address_and_value('miFLSDZBXUo4on8PGhTRTAufUn4mP61uoH', '!')]
coins = wallet.get_spendable_coins(domain=None)
tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee=5000)
tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(5000))
tx.set_rbf(True)
tx.locktime = 1859362
tx.version = 2
@@ -2517,7 +2518,7 @@ class TestWalletSending(ElectrumTestCase):
# create tx
outputs = [PartialTxOutput.from_address_and_value('2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', 2500000)]
coins = wallet.get_spendable_coins(domain=None)
tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee=5000)
tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(5000))
tx.set_rbf(True)
tx.locktime = 1325499
tx.version = 1
@@ -2582,7 +2583,7 @@ class TestWalletSending(ElectrumTestCase):
# create tx
outputs = [PartialTxOutput.from_address_and_value('2N1VTMMFb91SH9SNRAkT7z8otP5eZEct4KL', '!')]
coins = wallet.get_spendable_coins(domain=None)
tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee=5000)
tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(5000))
tx.set_rbf(True)
tx.locktime = 1325499
tx.version = 1
@@ -2704,7 +2705,7 @@ class TestWalletSending(ElectrumTestCase):
# create tx1
outputs = [PartialTxOutput.from_address_and_value('tb1qsfcddwf7yytl62e3catwv8hpl2hs9e36g2cqxl', 100000)]
coins = wallet.get_spendable_coins(domain=None)
tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee=190)
tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(190))
tx.set_rbf(True)
tx.locktime = 1938861
tx.version = 2
@@ -2717,7 +2718,7 @@ class TestWalletSending(ElectrumTestCase):
# create tx2, which spends from unsigned tx1
outputs = [PartialTxOutput.from_address_and_value('tb1qq0lm9esmq6pfjc3jls7v6twy93lnqcs85wlth3', '!')]
coins = wallet.get_spendable_coins(domain=None)
tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee=5000)
tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(5000))
tx.set_rbf(True)
tx.locktime = 1938863
tx.version = 2
@@ -2741,7 +2742,7 @@ class TestWalletSending(ElectrumTestCase):
outputs = [PartialTxOutput.from_address_and_value('tb1qsfcddwf7yytl62e3catwv8hpl2hs9e36g2cqxl', '!')]
coins = wallet.get_spendable_coins(domain=None)
with self.assertRaises(NotEnoughFunds):
tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee=0)
tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(0))
# bootstrap wallet
funding_tx = Transaction('0200000000010132515e6aade1b79ec7dd3bac0896d8b32c56195d23d07d48e21659cef24301560100000000fdffffff0112841e000000000016001477fe6d2a27e8860c278d4d2cd90bad716bb9521a02473044022041ed68ef7ef122813ac6a5e996b8284f645c53fbe6823b8e430604a8915a867802203233f5f4d347a687eb19b2aa570829ab12aeeb29a24cc6d6d20b8b3d79e971ae012102bee0ee043817e50ac1bb31132770f7c41e35946ccdcb771750fb9696bdd1b307ad951d00')
@@ -2752,7 +2753,7 @@ class TestWalletSending(ElectrumTestCase):
with self.subTest(msg="funded wallet, zero output value, zero fee"):
outputs = [PartialTxOutput.from_address_and_value('tb1qsfcddwf7yytl62e3catwv8hpl2hs9e36g2cqxl', 0)]
coins = wallet.get_spendable_coins(domain=None)
tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee=0)
tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(0))
self.assertEqual(1, len(tx.inputs()))
self.assertEqual(2, len(tx.outputs()))
@@ -2775,7 +2776,7 @@ class TestWalletSending(ElectrumTestCase):
outputs = [PartialTxOutput.from_address_and_value('tb1qq4pypzwxf5uanfyckmsu3ejxxf6rrvjqchza3v', 49646)]
coins = wallet.get_spendable_coins(domain=None)
tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee=1000)
tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(1000))
tx.set_rbf(True)
tx.locktime = 2004420
tx.version = 2
@@ -2810,7 +2811,7 @@ class TestWalletSending(ElectrumTestCase):
outputs = [PartialTxOutput.from_address_and_value('tb1qq4pypzwxf5uanfyckmsu3ejxxf6rrvjqchza3v', 49646)]
coins = wallet.get_spendable_coins(domain=None)
tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee=1000)
tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(1000))
tx.set_rbf(True)
tx.locktime = 2004420
tx.version = 2
@@ -2855,7 +2856,7 @@ class TestWalletSending(ElectrumTestCase):
outputs = [PartialTxOutput.from_address_and_value('tb1qq4pypzwxf5uanfyckmsu3ejxxf6rrvjqchza3v', 49646)]
coins = wallet.get_spendable_coins(domain=None)
tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee=1000)
tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(1000))
tx.set_rbf(True)
tx.locktime = 2004420
tx.version = 2
@@ -2936,7 +2937,7 @@ class TestWalletSending(ElectrumTestCase):
coins = wallet.get_spendable_coins(domain=None)
# create spending tx
tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee=5000, rbf=True)
tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(5000), rbf=True)
tx.version = 2
tx.locktime = 2378363
self.assertEqual("04cf670cc809560ab6b1a362c119dbd59ea6a7621d00a4a05c0ef1839e65c035", tx.txid())
@@ -2966,7 +2967,7 @@ class TestWalletSending(ElectrumTestCase):
# create spending tx again, but now we have full key origin info
wallet.get_keystores()[0].add_key_origin(derivation_prefix="m/48'/1'/0'/2'", root_fingerprint="30cf1be5")
tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee=5000, rbf=True)
tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(5000), rbf=True)
tx.version = 2
tx.locktime = 2378363
self.assertEqual("04cf670cc809560ab6b1a362c119dbd59ea6a7621d00a4a05c0ef1839e65c035", tx.txid())
@@ -3007,7 +3008,7 @@ class TestWalletSending(ElectrumTestCase):
coins = wallet.get_spendable_coins(domain=None)
# create spending tx
tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee=5000, rbf=True)
tx = wallet.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(5000), rbf=True)
tx.version = 2
tx.locktime = 2378367
self.assertEqual("5c0d5eea8c2c12a383406bb37e6158167e44bfe6cd1ad590b7d97002cdfc9fff", tx.txid())
@@ -3073,7 +3074,7 @@ class TestWalletSending(ElectrumTestCase):
# cosignerA creates and signs the tx
outputs = [PartialTxOutput.from_address_and_value("tb1qgacvp0zvgtk3etggjayuezrc2mkql8veshv4xw", 200_000)]
coins = wallet1a.get_spendable_coins(domain=None)
tx = wallet1a.make_unsigned_transaction(coins=coins, outputs=outputs, fee=5000)
tx = wallet1a.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(5000))
tx.set_rbf(True)
tx.locktime = 2429212
tx.version = 2
@@ -3146,10 +3147,10 @@ class TestWalletSending(ElectrumTestCase):
outputs = [PartialTxOutput.from_address_and_value(bitcoin.DummyAddress.CHANNEL, 250000)]
with self.assertRaises(bitcoin.DummyAddressUsedInTxException):
tx = wallet1.create_transaction(outputs=outputs, password=None, fee=5000, tx_version=1, rbf=False)
tx = wallet1.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), tx_version=1, rbf=False)
coins = wallet1.get_spendable_coins(domain=None)
tx = wallet1.make_unsigned_transaction(coins=coins, outputs=outputs, fee=5000)
tx = wallet1.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(5000))
with self.assertRaises(bitcoin.DummyAddressUsedInTxException):
wallet1.sign_transaction(tx, password=None)
@@ -3167,7 +3168,7 @@ class TestWalletSending(ElectrumTestCase):
outputs = [PartialTxOutput.from_address_and_value('tb1qgacvp0zvgtk3etggjayuezrc2mkql8veshv4xw', '!')]
coins = wallet1.get_spendable_coins(domain=None)
tx = wallet1.make_unsigned_transaction(coins=coins, outputs=outputs, fee=1000)
tx = wallet1.make_unsigned_transaction(coins=coins, outputs=outputs, fee_policy=FixedFeePolicy(1000))
self.assertEqual(2, len(tx.inputs()))
tx.inputs()[0].sighash = Sighash.NONE
@@ -3226,7 +3227,7 @@ class TestWalletOfflineSigning(ElectrumTestCase):
# create unsigned tx
outputs = [PartialTxOutput.from_address_and_value('tb1qyw3c0rvn6kk2c688y3dygvckn57525y8qnxt3a', 2500000)]
tx = wallet_online.create_transaction(outputs=outputs, password=None, fee=5000, rbf=True)
tx = wallet_online.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), rbf=True)
tx.locktime = 1446655
tx.version = 1
@@ -3274,7 +3275,7 @@ class TestWalletOfflineSigning(ElectrumTestCase):
# create unsigned tx
outputs = [PartialTxOutput.from_address_and_value('tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)]
tx = wallet_online.create_transaction(outputs=outputs, password=None, fee=5000, rbf=True)
tx = wallet_online.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), rbf=True)
tx.locktime = 1325340
tx.version = 1
@@ -3329,7 +3330,7 @@ class TestWalletOfflineSigning(ElectrumTestCase):
# create unsigned tx
outputs = [PartialTxOutput.from_address_and_value('tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)]
tx = wallet_online.create_transaction(outputs=outputs, password=None, fee=5000, rbf=True)
tx = wallet_online.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), rbf=True)
tx.locktime = 1325341
tx.version = 1
@@ -3374,7 +3375,7 @@ class TestWalletOfflineSigning(ElectrumTestCase):
# create unsigned tx
outputs = [PartialTxOutput.from_address_and_value('tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)]
tx = wallet_online.create_transaction(outputs=outputs, password=None, fee=5000, rbf=True)
tx = wallet_online.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), rbf=True)
tx.locktime = 1325341
tx.version = 1
@@ -3432,7 +3433,7 @@ class TestWalletOfflineSigning(ElectrumTestCase):
# create unsigned tx
outputs = [PartialTxOutput.from_address_and_value('tb1qp0mv2sxsyxxfj5gl0332f9uyez93su9cf26757', 2500000)]
tx = wallet_online.create_transaction(outputs=outputs, password=None, fee=5000, rbf=True)
tx = wallet_online.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), rbf=True)
tx.locktime = 1325341
tx.version = 1
@@ -3491,7 +3492,7 @@ class TestWalletOfflineSigning(ElectrumTestCase):
# create unsigned tx
outputs = [PartialTxOutput.from_address_and_value('tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)]
tx = wallet_online.create_transaction(outputs=outputs, password=None, fee=5000, rbf=True)
tx = wallet_online.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), rbf=True)
tx.locktime = 1325340
tx.version = 1
@@ -3528,7 +3529,7 @@ class TestWalletOfflineSigning(ElectrumTestCase):
# create unsigned tx
outputs = [PartialTxOutput.from_address_and_value('tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)]
tx = wallet_online.create_transaction(outputs=outputs, password=None, fee=5000, rbf=True)
tx = wallet_online.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), rbf=True)
tx.locktime = 1325340
tx.version = 1
@@ -3568,7 +3569,7 @@ class TestWalletOfflineSigning(ElectrumTestCase):
# create unsigned tx
outputs = [PartialTxOutput.from_address_and_value('tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)]
tx = wallet_online.create_transaction(outputs=outputs, password=None, fee=5000, rbf=True)
tx = wallet_online.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), rbf=True)
tx.locktime = 1325340
tx.version = 1
@@ -3609,7 +3610,7 @@ class TestWalletOfflineSigning(ElectrumTestCase):
# create unsigned tx
outputs = [PartialTxOutput.from_address_and_value('tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)]
tx = wallet_online.create_transaction(outputs=outputs, password=None, fee=5000, rbf=True)
tx = wallet_online.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), rbf=True)
tx.locktime = 1325340
tx.version = 1
@@ -3653,7 +3654,7 @@ class TestWalletOfflineSigning(ElectrumTestCase):
# create unsigned tx
outputs = [PartialTxOutput.from_address_and_value('tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)]
tx = wallet_online.create_transaction(outputs=outputs, password=None, fee=5000, rbf=True)
tx = wallet_online.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), rbf=True)
tx.locktime = 1325340
tx.version = 1
@@ -3694,7 +3695,7 @@ class TestWalletOfflineSigning(ElectrumTestCase):
# create unsigned tx
outputs = [PartialTxOutput.from_address_and_value('tb1quk7ahlhr3qmjndy0uvu9y9hxfesrtahtta9ghm', 2500000)]
tx = wallet_online.create_transaction(outputs=outputs, password=None, fee=5000, rbf=True)
tx = wallet_online.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), rbf=True)
tx.locktime = 1325340
tx.version = 1
@@ -3748,7 +3749,7 @@ class TestWalletOfflineSigning(ElectrumTestCase):
# create unsigned tx
outputs = [PartialTxOutput.from_address_and_value('2MuCQQHJNnrXzQzuqfUCfAwAjPqpyEHbgue', 2500000)]
tx = wallet_online.create_transaction(outputs=outputs, password=None, fee=5000, rbf=True)
tx = wallet_online.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), rbf=True)
tx.locktime = 1325503
tx.version = 1
@@ -3815,7 +3816,7 @@ class TestWalletOfflineSigning(ElectrumTestCase):
# create unsigned tx
outputs = [PartialTxOutput.from_address_and_value('2N8CtJRwxb2GCaiWWdSHLZHHLoZy53CCyxf', 2500000)]
tx = wallet_online.create_transaction(outputs=outputs, password=None, fee=5000, rbf=True)
tx = wallet_online.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), rbf=True)
tx.locktime = 1325504
tx.version = 1
@@ -3885,7 +3886,7 @@ class TestWalletOfflineSigning(ElectrumTestCase):
# create unsigned tx
outputs = [PartialTxOutput.from_address_and_value('2MyoZVy8T1t94yLmyKu8DP1SmbWvnxbkwRA', 2500000)]
tx = wallet_online.create_transaction(outputs=outputs, password=None, fee=5000, rbf=True)
tx = wallet_online.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), rbf=True)
tx.locktime = 1325505
tx.version = 1
@@ -4208,7 +4209,7 @@ class TestWalletHistory_HelperFns(ElectrumTestCase):
# wallet1 -> wallet2
outputs = [PartialTxOutput.from_address_and_value("2MuUcGmQ2mLN3vjTuqDSgZpk4LPKDsuPmhN", 165000)]
tx = wallet1.create_transaction(outputs=outputs, password=None, fee=5000, tx_version=1, rbf=False, sign=False)
tx = wallet1.create_transaction(outputs=outputs, password=None, fee_policy=FixedFeePolicy(5000), tx_version=1, rbf=False, sign=False)
self.assertEqual(
"wsh(sortedmulti(2,[b2e35a7d/1h]tpubD9aPYLPPYw8MxU3cD57LwpV5v7GomHxdv62MSbPcRkp47zwXx69ACUFsKrj8xzuzRrij9FWVhfvkvNqtqsr8ZtefkDsGZ9GLuHzoS6bXyk1/0/0,[53b77ddb/1h]tpubD8spLJysN7v7V1KHvkZ7AwjnXShKafopi7Vu3Ahs2S46FxBPTode8DgGxDo55k4pJvETGScZFwnM5f2Y31EUjteJdhxR73sjr9ieydgah2U/0/0,[43067d63/1h]tpubD8khd1g1tzFeKeaU59QV811hyvhwn9KDfy5sqFJ5m2wJLw6rUt4AZviqutRPXTUAK4SpU2we3y2WBP916Ma8Em4qFGcbYkFvXVfpGYV3oZR/0/0))",
tx.inputs()[0].script_descriptor.to_string_no_checksum())