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:
+1
-1
@@ -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
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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', [
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user