From 70331251861badd17f0544bb97a77d75c39e9db7 Mon Sep 17 00:00:00 2001 From: SomberNight Date: Wed, 3 Jan 2018 18:08:10 +0100 Subject: [PATCH] fee ui - rounding: display info icon with tooltip. show pre-rounding values in ui. --- gui/qt/fee_slider.py | 6 +++ gui/qt/main_window.py | 86 ++++++++++++++++++++++++++++++++++-------- icons.qrc | 1 + icons/info.png | Bin 0 -> 1771 bytes lib/coinchooser.py | 2 + lib/simple_config.py | 18 ++++++++- lib/wallet.py | 1 + 7 files changed, 96 insertions(+), 18 deletions(-) create mode 100644 icons/info.png diff --git a/gui/qt/fee_slider.py b/gui/qt/fee_slider.py index c8b6637f..3d938492 100644 --- a/gui/qt/fee_slider.py +++ b/gui/qt/fee_slider.py @@ -18,6 +18,7 @@ class FeeSlider(QSlider): self.lock = threading.RLock() self.update() self.valueChanged.connect(self.moved) + self._active = True def moved(self, pos): with self.lock: @@ -56,9 +57,11 @@ class FeeSlider(QSlider): self.setToolTip(tooltip) def activate(self): + self._active = True self.setStyleSheet('') def deactivate(self): + self._active = False # TODO it would be nice to find a platform-independent solution # that makes the slider look as if it was disabled self.setStyleSheet( @@ -79,3 +82,6 @@ class FeeSlider(QSlider): } """ ) + + def is_active(self): + return self._active diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index ea2fe96d..b6c09855 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -619,7 +619,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): def format_amount_and_units(self, amount): text = self.format_amount(amount) + ' '+ self.base_unit() - x = self.fx.format_amount_and_units(amount) + x = self.fx.format_amount_and_units(amount) if self.fx else None if text and x: text += ' (%s)'%x return text @@ -1070,6 +1070,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): if fee_rate: self.feerate_e.setAmount(fee_rate // 1000) + else: + self.feerate_e.setAmount(None) self.fee_e.setModified(False) self.fee_slider.activate() @@ -1103,6 +1105,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.size_e.setStyleSheet(ColorScheme.DEFAULT.as_stylesheet()) self.feerate_e = FeerateEdit(lambda: 2 if self.fee_unit else 0) + self.feerate_e.setAmount(self.config.fee_per_byte()) self.feerate_e.textEdited.connect(partial(on_fee_or_feerate, self.feerate_e, False)) self.feerate_e.editingFinished.connect(partial(on_fee_or_feerate, self.feerate_e, True)) @@ -1110,6 +1113,18 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.fee_e.textEdited.connect(partial(on_fee_or_feerate, self.fee_e, False)) self.fee_e.editingFinished.connect(partial(on_fee_or_feerate, self.fee_e, True)) + def feerounding_onclick(): + text = (_('To somewhat protect your privacy, Electrum tries to create change with similar precision to other outputs.') + ' ' + + _('At most 100 satoshis might be lost due to this rounding.') + '\n' + + _('Also, dust is not kept as change, but added to the fee.')) + QMessageBox.information(self, 'Fee rounding', text) + + self.feerounding_icon = QPushButton(QIcon(':icons/info.png'), '') + self.feerounding_icon.setFixedWidth(20) + self.feerounding_icon.setFlat(True) + self.feerounding_icon.clicked.connect(feerounding_onclick) + self.feerounding_icon.setVisible(False) + self.connect_fields(self, self.amount_e, self.fiat_send_e, self.fee_e) vbox_feelabel = QVBoxLayout() @@ -1123,12 +1138,14 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): hbox.addWidget(self.feerate_e) hbox.addWidget(self.size_e) hbox.addWidget(self.fee_e) + hbox.addWidget(self.feerounding_icon, Qt.AlignLeft) + hbox.addStretch(1) vbox_feecontrol = QVBoxLayout() vbox_feecontrol.addWidget(self.fee_adv_controls) vbox_feecontrol.addWidget(self.fee_slider) - grid.addLayout(vbox_feecontrol, 5, 1, 1, 3) + grid.addLayout(vbox_feecontrol, 5, 1, 1, -1) if not self.config.get('show_fee', False): self.fee_adv_controls.setVisible(False) @@ -1252,15 +1269,22 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): try: tx = make_tx(fee_estimator) self.not_enough_funds = False - except NotEnoughFunds: - self.not_enough_funds = True + except (NotEnoughFunds, NoDynamicFeeEstimates) as e: if not freeze_fee: self.fee_e.setAmount(None) - return - except NoDynamicFeeEstimates: - tx = make_tx(0) - size = tx.estimated_size() - self.size_e.setAmount(size) + if not freeze_feerate: + self.feerate_e.setAmount(None) + self.feerounding_icon.setVisible(False) + + if isinstance(e, NotEnoughFunds): + self.not_enough_funds = True + elif isinstance(e, NoDynamicFeeEstimates): + try: + tx = make_tx(0) + size = tx.estimated_size() + self.size_e.setAmount(size) + except BaseException: + pass return except BaseException: traceback.print_exc(file=sys.stderr) @@ -1270,12 +1294,35 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.size_e.setAmount(size) fee = tx.get_fee() - if not freeze_fee: - fee = None if self.not_enough_funds else fee - self.fee_e.setAmount(fee) - if not freeze_feerate: - fee_rate = fee // size if fee is not None else None - self.feerate_e.setAmount(fee_rate) + fee = None if self.not_enough_funds else fee + + # Displayed fee/fee_rate values are set according to user input. + # Due to rounding or dropping dust in CoinChooser, + # actual fees often differ somewhat. + if freeze_feerate or self.fee_slider.is_active(): + displayed_feerate = self.feerate_e.get_amount() + displayed_feerate = displayed_feerate // 1000 if displayed_feerate else 0 + displayed_fee = displayed_feerate * size + self.fee_e.setAmount(displayed_fee) + else: + if freeze_fee: + displayed_fee = self.fee_e.get_amount() + else: + # fallback to actual fee if nothing is frozen + displayed_fee = fee + self.fee_e.setAmount(displayed_fee) + displayed_fee = displayed_fee if displayed_fee else 0 + displayed_feerate = displayed_fee // size if displayed_fee is not None else None + self.feerate_e.setAmount(displayed_feerate) + + # show/hide fee rounding icon + feerounding = (fee - displayed_fee) if fee else 0 + if feerounding: + self.feerounding_icon.setToolTip( + _('additional {} satoshis will be added').format(feerounding)) + self.feerounding_icon.setVisible(True) + else: + self.feerounding_icon.setVisible(False) if self.is_max: amount = tx.output_value() @@ -1354,7 +1401,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): fee_estimator = self.fee_e.get_amount() elif self.is_send_feerate_frozen(): amount = self.feerate_e.get_amount() - amount = 0 if amount is None else float(amount) + amount = 0 if amount is None else amount fee_estimator = partial( simple_config.SimpleConfig.estimate_fee_for_feerate, amount) else: @@ -1440,6 +1487,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.show_transaction(tx, tx_desc) return + if not self.network: + self.show_error(_("You can't broadcast a transaction without a live network connection.")) + return + # confirmation dialog msg = [ _("Amount to be sent") + ": " + self.format_amount_and_units(amount), @@ -1640,7 +1691,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): e.setText('') e.setFrozen(False) self.fee_slider.activate() + self.feerate_e.setAmount(self.config.fee_per_byte()) self.size_e.setAmount(0) + self.feerounding_icon.setVisible(False) self.set_pay_from([]) self.tx_external_keypairs = {} self.update_status() @@ -3009,6 +3062,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): grid.addWidget(QLabel(_('Output amount') + ':'), 2, 0) grid.addWidget(output_amount, 2, 1) fee_e = BTCAmountEdit(self.get_decimal_point) + # FIXME with dyn fees, without estimates, there are all kinds of crashes here def f(x): a = max_fee - fee_e.get_amount() output_amount.setText((self.format_amount(a) + ' ' + self.base_unit()) if a else '') diff --git a/icons.qrc b/icons.qrc index e1007e58..92e19a1a 100644 --- a/icons.qrc +++ b/icons.qrc @@ -14,6 +14,7 @@ icons/electrum_light_icon.png icons/electrum_dark_icon.png icons/file.png + icons/info.png icons/keepkey.png icons/keepkey_unpaired.png icons/key.png diff --git a/icons/info.png b/icons/info.png new file mode 100644 index 0000000000000000000000000000000000000000..f11f99694aae1c743899495533965d61c94a73f5 GIT binary patch literal 1771 zcmV;P)PuVoCRJ^rMeGA4;gSl{3nYP-%Mu8& zr`eMk%rF^RoC#M@|pZ|9piGv9yCnQy*xW`Jdau(Gk;Z=Py`G(#Y?NFmw) zu>yd1wm1&O5mp#h3YR2_6JP8P{@bd)%NFvhcX~H6@Pd%^9Gj|{NSIYFSJqqQbU199 z&@>4E$QmZ}tfAtGbS9BX*_f3iqRY(uJ|O>j_`tUBmtKfcVs}sflalB)X5Q-cxH8R+ z0Y_kk7d1W)#3Icqr7#(vMmQSBmFwerJejr=(P?J9et7@3&mn}m??Uz*>f1~Z|6s-= ztLkgHt+`Rw`aGpJGlys_fsrdW&Ff=PA}P)o%6Rdsmw$P-ls1YMva9=;=5RUR6iU3b z((g6f*F9*fayv`+dI1v26wVD@$&N?jk_CFIS6}_(2hTrm7ISY=LOKuj*E?+DQz5Wr z^Muv~cY_YqX zc5A#{gk(B{lY;1F;5+Zmg*8J~DP?z(O5Efo_103-yqx3_9ukE`Bp6CdyDKBg6{SIAg5 zWxOSb9tpO$+O;KqoeH5z!jAS9JCWGxbh_RsRA=5$KYr-MCdqPO+ruris;<1~WS79& z+L-$R6*%>p}{1@=8e zu*h%jO8#`%dp3#cE6Yx(96Kbvg4Se2zQUJ_oR-w*0yF@VaK3 zX{sH2dwM(b?Q?_>$?m3xfbzJ?U$G0t2P;BGmD`D?20s(zpYrXug*@?A|9X=-*xFoQ z<_j>x#R-L2=FW92Gc!-DpzXm1X(|rw?K!?KXS*HBOo`vrcwD-#%5BNgt((Ay=h@Q9 zp2Jt=hR@^DrxNL31Ly+so`_9|XPX)VmgXP237iiroDW+kB1coB->ymVZ0?!>;PIa0 zs|@DVb$*|IZR`_5tw{#xtuLov@kkUFNkx}h+SJt}{sPTG0Bv{(G1DKs7Lh$-3 zk0&c23NHVp5^53%xaP;Gp@_mz#F_~&gg}+cnT<`S)&uxV0{{hT^}1b-MeZ${ND>IR zfJX0J`)II`_BZ z=Y%FT5JU}dC;V0}PqzpH0L5z3Ff0!AOMqb-P)bFCSs@HWGP=Q)b*pqTS<_^t#(~_c z#cC*)NM;ti>wl1VEU7DqVIU_&hzrxHj7{PGM;fb8N+FR>+d+H*m=$8MIBqJIld+8!aIOW}!lWxXqb@MIh|A%_nCAv6KxWxs6jdslCUQ$;x$E0Plv3};5-C?Kk*thoB@jy_5lg0=On5hEyDj8MSLgx}^^IJMii(}!bfzk0O{@Z_B zktCp-h1aBb8O!eIttiGLF-hRx-Cb62cy{;nAF@mOeP?jvl5Ha_Mk<}b*L~mVS%d$4 zobk0$en zl{-{(5BB_}cZJZ=!Jv&AVS|DQAn%xyfC?Lo?+N^^vg35_qv-$S@gGKx0F)dbM&JMd N002ovPDHLkV1lvGV4na0 literal 0 HcmV?d00001 diff --git a/lib/coinchooser.py b/lib/coinchooser.py index c4004c87..a29aa732 100644 --- a/lib/coinchooser.py +++ b/lib/coinchooser.py @@ -273,6 +273,8 @@ class CoinChooserRandom(CoinChooserBase): candidates.add(tuple(sorted(permutation[:count + 1]))) break else: + # FIXME this assumes that the effective value of any bkt is >= 0 + # we should make sure not to choose buckets with <= 0 eff. val. raise NotEnoughFunds() candidates = [[buckets[n] for n in c] for c in candidates] diff --git a/lib/simple_config.py b/lib/simple_config.py index b3da52de..33f426bd 100644 --- a/lib/simple_config.py +++ b/lib/simple_config.py @@ -5,7 +5,8 @@ import os import stat from copy import deepcopy -from .util import user_dir, print_error, print_stderr, PrintError +from .util import (user_dir, print_error, print_stderr, PrintError, + NoDynamicFeeEstimates) from .bitcoin import MAX_FEE_RATE, FEE_TARGETS @@ -245,6 +246,9 @@ class SimpleConfig(PrintError): return self.get('dynamic_fees', True) def fee_per_kb(self): + """Returns sat/kvB fee to pay for a txn. + Note: might return None. + """ dyn = self.is_dynfee() if dyn: fee_rate = self.dynfee(self.get('fee_level', 2)) @@ -252,8 +256,18 @@ class SimpleConfig(PrintError): fee_rate = self.get('fee_per_kb', self.max_fee_rate()/2) return fee_rate + 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): - return self.estimate_fee_for_feerate(self.fee_per_kb(), size) + fee_per_kb = self.fee_per_kb() + if fee_per_kb is None: + raise NoDynamicFeeEstimates() + return self.estimate_fee_for_feerate(fee_per_kb, size) @classmethod def estimate_fee_for_feerate(cls, fee_per_kb, size): diff --git a/lib/wallet.py b/lib/wallet.py index 3a3ed8f8..a9dc646b 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -923,6 +923,7 @@ class Abstract_Wallet(PrintError): tx = coin_chooser.make_tx(inputs, outputs, change_addrs[:max_change], fee_estimator, self.dust_threshold()) else: + # FIXME?? this might spend inputs with negative effective value... sendable = sum(map(lambda x:x['value'], inputs)) _type, data, value = outputs[i_max] outputs[i_max] = (_type, data, 0)