allow fractional feerates (#4324)
This commit is contained in:
@@ -1,11 +1,12 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
from PyQt5.QtCore import *
|
from PyQt5.QtCore import *
|
||||||
from PyQt5.QtGui import *
|
from PyQt5.QtGui import *
|
||||||
from PyQt5.QtWidgets import (QLineEdit, QStyle, QStyleOptionFrame)
|
from PyQt5.QtWidgets import (QLineEdit, QStyle, QStyleOptionFrame)
|
||||||
|
|
||||||
from decimal import Decimal
|
from electrum.util import format_satoshis_plain, decimal_point_to_base_unit_name, FEERATE_PRECISION
|
||||||
from electrum.util import format_satoshis_plain, decimal_point_to_base_unit_name
|
|
||||||
|
|
||||||
|
|
||||||
class MyLineEdit(QLineEdit):
|
class MyLineEdit(QLineEdit):
|
||||||
@@ -19,7 +20,7 @@ class MyLineEdit(QLineEdit):
|
|||||||
class AmountEdit(MyLineEdit):
|
class AmountEdit(MyLineEdit):
|
||||||
shortcut = pyqtSignal()
|
shortcut = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, base_unit, is_int = False, parent=None):
|
def __init__(self, base_unit, is_int=False, parent=None):
|
||||||
QLineEdit.__init__(self, parent)
|
QLineEdit.__init__(self, parent)
|
||||||
# This seems sufficient for hundred-BTC amounts with 8 decimals
|
# This seems sufficient for hundred-BTC amounts with 8 decimals
|
||||||
self.setFixedWidth(140)
|
self.setFixedWidth(140)
|
||||||
@@ -28,10 +29,14 @@ class AmountEdit(MyLineEdit):
|
|||||||
self.is_int = is_int
|
self.is_int = is_int
|
||||||
self.is_shortcut = False
|
self.is_shortcut = False
|
||||||
self.help_palette = QPalette()
|
self.help_palette = QPalette()
|
||||||
|
self.extra_precision = 0
|
||||||
|
|
||||||
def decimal_point(self):
|
def decimal_point(self):
|
||||||
return 8
|
return 8
|
||||||
|
|
||||||
|
def max_precision(self):
|
||||||
|
return self.decimal_point() + self.extra_precision
|
||||||
|
|
||||||
def numbify(self):
|
def numbify(self):
|
||||||
text = self.text().strip()
|
text = self.text().strip()
|
||||||
if text == '!':
|
if text == '!':
|
||||||
@@ -45,7 +50,7 @@ class AmountEdit(MyLineEdit):
|
|||||||
if '.' in s:
|
if '.' in s:
|
||||||
p = s.find('.')
|
p = s.find('.')
|
||||||
s = s.replace('.','')
|
s = s.replace('.','')
|
||||||
s = s[:p] + '.' + s[p:p+self.decimal_point()]
|
s = s[:p] + '.' + s[p:p+self.max_precision()]
|
||||||
self.setText(s)
|
self.setText(s)
|
||||||
# setText sets Modified to False. Instead we want to remember
|
# setText sets Modified to False. Instead we want to remember
|
||||||
# if updates were because of user modification.
|
# if updates were because of user modification.
|
||||||
@@ -75,7 +80,7 @@ class AmountEdit(MyLineEdit):
|
|||||||
|
|
||||||
class BTCAmountEdit(AmountEdit):
|
class BTCAmountEdit(AmountEdit):
|
||||||
|
|
||||||
def __init__(self, decimal_point, is_int = False, parent=None):
|
def __init__(self, decimal_point, is_int=False, parent=None):
|
||||||
AmountEdit.__init__(self, self._base_unit, is_int, parent)
|
AmountEdit.__init__(self, self._base_unit, is_int, parent)
|
||||||
self.decimal_point = decimal_point
|
self.decimal_point = decimal_point
|
||||||
|
|
||||||
@@ -87,8 +92,15 @@ class BTCAmountEdit(AmountEdit):
|
|||||||
x = Decimal(str(self.text()))
|
x = Decimal(str(self.text()))
|
||||||
except:
|
except:
|
||||||
return None
|
return None
|
||||||
p = pow(10, self.decimal_point())
|
# scale it to max allowed precision, make it an int
|
||||||
return int( p * x )
|
power = pow(10, self.max_precision())
|
||||||
|
max_prec_amount = int(power * x)
|
||||||
|
# if the max precision is simply what unit conversion allows, just return
|
||||||
|
if self.max_precision() == self.decimal_point():
|
||||||
|
return max_prec_amount
|
||||||
|
# otherwise, scale it back to the expected unit
|
||||||
|
amount = Decimal(max_prec_amount) / pow(10, self.max_precision()-self.decimal_point())
|
||||||
|
return Decimal(amount) if not self.is_int else int(amount)
|
||||||
|
|
||||||
def setAmount(self, amount):
|
def setAmount(self, amount):
|
||||||
if amount is None:
|
if amount is None:
|
||||||
@@ -98,6 +110,11 @@ class BTCAmountEdit(AmountEdit):
|
|||||||
|
|
||||||
|
|
||||||
class FeerateEdit(BTCAmountEdit):
|
class FeerateEdit(BTCAmountEdit):
|
||||||
|
|
||||||
|
def __init__(self, decimal_point, is_int=False, parent=None):
|
||||||
|
super().__init__(decimal_point, is_int, parent)
|
||||||
|
self.extra_precision = FEERATE_PRECISION
|
||||||
|
|
||||||
def _base_unit(self):
|
def _base_unit(self):
|
||||||
return 'sat/byte'
|
return 'sat/byte'
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ from electrum.util import (format_time, format_satoshis, format_fee_satoshis,
|
|||||||
UserCancelled, NoDynamicFeeEstimates, profiler,
|
UserCancelled, NoDynamicFeeEstimates, profiler,
|
||||||
export_meta, import_meta, bh2u, bfh, InvalidPassword,
|
export_meta, import_meta, bh2u, bfh, InvalidPassword,
|
||||||
base_units, base_units_list, base_unit_name_to_decimal_point,
|
base_units, base_units_list, base_unit_name_to_decimal_point,
|
||||||
decimal_point_to_base_unit_name)
|
decimal_point_to_base_unit_name, quantize_feerate)
|
||||||
from electrum import Transaction
|
from electrum import Transaction
|
||||||
from electrum import util, bitcoin, commands, coinchooser
|
from electrum import util, bitcoin, commands, coinchooser
|
||||||
from electrum import paymentrequest
|
from electrum import paymentrequest
|
||||||
@@ -1102,7 +1102,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
|||||||
self.config.set_key('fee_per_kb', fee_rate, False)
|
self.config.set_key('fee_per_kb', fee_rate, False)
|
||||||
|
|
||||||
if fee_rate:
|
if fee_rate:
|
||||||
self.feerate_e.setAmount(fee_rate // 1000)
|
fee_rate = Decimal(fee_rate)
|
||||||
|
self.feerate_e.setAmount(quantize_feerate(fee_rate / 1000))
|
||||||
else:
|
else:
|
||||||
self.feerate_e.setAmount(None)
|
self.feerate_e.setAmount(None)
|
||||||
self.fee_e.setModified(False)
|
self.fee_e.setModified(False)
|
||||||
@@ -1334,12 +1335,12 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
|||||||
if freeze_feerate or self.fee_slider.is_active():
|
if freeze_feerate or self.fee_slider.is_active():
|
||||||
displayed_feerate = self.feerate_e.get_amount()
|
displayed_feerate = self.feerate_e.get_amount()
|
||||||
if displayed_feerate:
|
if displayed_feerate:
|
||||||
displayed_feerate = displayed_feerate // 1000
|
displayed_feerate = quantize_feerate(displayed_feerate / 1000)
|
||||||
else:
|
else:
|
||||||
# fallback to actual fee
|
# fallback to actual fee
|
||||||
displayed_feerate = fee // size if fee is not None else None
|
displayed_feerate = quantize_feerate(fee / size) if fee is not None else None
|
||||||
self.feerate_e.setAmount(displayed_feerate)
|
self.feerate_e.setAmount(displayed_feerate)
|
||||||
displayed_fee = displayed_feerate * size if displayed_feerate is not None else None
|
displayed_fee = round(displayed_feerate * size) if displayed_feerate is not None else None
|
||||||
self.fee_e.setAmount(displayed_fee)
|
self.fee_e.setAmount(displayed_fee)
|
||||||
else:
|
else:
|
||||||
if freeze_fee:
|
if freeze_fee:
|
||||||
@@ -1349,14 +1350,14 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
|||||||
displayed_fee = fee
|
displayed_fee = fee
|
||||||
self.fee_e.setAmount(displayed_fee)
|
self.fee_e.setAmount(displayed_fee)
|
||||||
displayed_fee = displayed_fee if displayed_fee else 0
|
displayed_fee = displayed_fee if displayed_fee else 0
|
||||||
displayed_feerate = displayed_fee // size if displayed_fee is not None else None
|
displayed_feerate = quantize_feerate(displayed_fee / size) if displayed_fee is not None else None
|
||||||
self.feerate_e.setAmount(displayed_feerate)
|
self.feerate_e.setAmount(displayed_feerate)
|
||||||
|
|
||||||
# show/hide fee rounding icon
|
# show/hide fee rounding icon
|
||||||
feerounding = (fee - displayed_fee) if fee else 0
|
feerounding = (fee - displayed_fee) if fee else 0
|
||||||
self.set_feerounding_text(feerounding)
|
self.set_feerounding_text(int(feerounding))
|
||||||
self.feerounding_icon.setToolTip(self.feerounding_text)
|
self.feerounding_icon.setToolTip(self.feerounding_text)
|
||||||
self.feerounding_icon.setVisible(bool(feerounding))
|
self.feerounding_icon.setVisible(abs(feerounding) >= 1)
|
||||||
|
|
||||||
if self.is_max:
|
if self.is_max:
|
||||||
amount = tx.output_value()
|
amount = tx.output_value()
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import threading
|
|||||||
import time
|
import time
|
||||||
import os
|
import os
|
||||||
import stat
|
import stat
|
||||||
|
from decimal import Decimal
|
||||||
|
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
|
||||||
@@ -473,12 +474,9 @@ class SimpleConfig(PrintError):
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def estimate_fee_for_feerate(cls, fee_per_kb, size):
|
def estimate_fee_for_feerate(cls, fee_per_kb, size):
|
||||||
# note: We only allow integer sat/byte values atm.
|
fee_per_kb = Decimal(fee_per_kb)
|
||||||
# The GUI for simplicity reasons only displays integer sat/byte,
|
fee_per_byte = fee_per_kb / 1000
|
||||||
# and for the sake of consistency, we thus only use integer sat/byte in
|
return round(fee_per_byte * size)
|
||||||
# the backend too.
|
|
||||||
fee_per_byte = int(fee_per_kb / 1000)
|
|
||||||
return int(fee_per_byte * size)
|
|
||||||
|
|
||||||
def update_fee_estimates(self, key, value):
|
def update_fee_estimates(self, key, value):
|
||||||
self.fee_estimates[key] = value
|
self.fee_estimates[key] = value
|
||||||
|
|||||||
16
lib/util.py
16
lib/util.py
@@ -24,6 +24,7 @@ import binascii
|
|||||||
import os, sys, re, json
|
import os, sys, re, json
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
import decimal
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
import traceback
|
import traceback
|
||||||
import urllib
|
import urllib
|
||||||
@@ -472,8 +473,21 @@ def format_satoshis(x, num_zeros=0, decimal_point=8, precision=None, is_diff=Fal
|
|||||||
result = " " * (15 - len(result)) + result
|
result = " " * (15 - len(result)) + result
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
FEERATE_PRECISION = 1 # num fractional decimal places for sat/byte fee rates
|
||||||
|
_feerate_quanta = Decimal(10) ** (-FEERATE_PRECISION)
|
||||||
|
|
||||||
|
|
||||||
def format_fee_satoshis(fee, num_zeros=0):
|
def format_fee_satoshis(fee, num_zeros=0):
|
||||||
return format_satoshis(fee, num_zeros, 0, precision=1)
|
return format_satoshis(fee, num_zeros, 0, precision=FEERATE_PRECISION)
|
||||||
|
|
||||||
|
|
||||||
|
def quantize_feerate(fee):
|
||||||
|
"""Strip sat/byte fee rate of excess precision."""
|
||||||
|
if fee is None:
|
||||||
|
return None
|
||||||
|
return Decimal(fee).quantize(_feerate_quanta, rounding=decimal.ROUND_HALF_DOWN)
|
||||||
|
|
||||||
|
|
||||||
def timestamp_to_datetime(timestamp):
|
def timestamp_to_datetime(timestamp):
|
||||||
if timestamp is None:
|
if timestamp is None:
|
||||||
|
|||||||
Reference in New Issue
Block a user