diff --git a/gui/qt/history_widget.py b/gui/qt/history_widget.py index f1bf67966..e7c0cb545 100644 --- a/gui/qt/history_widget.py +++ b/gui/qt/history_widget.py @@ -117,11 +117,16 @@ class HistoryWidget(MyTreeWidget): if not tx_hash: return tx_URL = block_explorer_URL(self.config, 'tx', tx_hash) - if not tx_URL: - return + conf, timestamp = self.wallet.get_confirmations(tx_hash) + tx = self.wallet.transactions.get(tx_hash) + is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx) + rbf = is_mine and (conf == 0) and tx and not tx.is_final() menu = QMenu() menu.addAction(_("Copy ID to Clipboard"), lambda: self.parent.app.clipboard().setText(tx_hash)) - menu.addAction(_("Details"), lambda: self.parent.show_transaction(self.wallet.transactions.get(tx_hash))) + menu.addAction(_("Details"), lambda: self.parent.show_transaction(tx)) menu.addAction(_("Edit description"), lambda: self.editItem(item, self.editable_columns[0])) - menu.addAction(_("View on block explorer"), lambda: webbrowser.open(tx_URL)) + if rbf: + menu.addAction(_("Increase fee"), lambda: self.parent.bump_fee_dialog(tx)) + if tx_URL: + menu.addAction(_("View on block explorer"), lambda: webbrowser.open(tx_URL)) menu.exec_(self.viewport().mapToGlobal(position)) diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index b0c024a8f..17eed842c 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -2785,6 +2785,19 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): qr_combo.currentIndexChanged.connect(on_video_device) gui_widgets.append((qr_label, qr_combo)) + rbf_cb = QCheckBox(_('Enable Replace-By-Fee')) + rbf_cb.setChecked(self.wallet.use_rbf) + if not self.config.is_modifiable('use_rbf'): + rbf_cb.setEnabled(False) + def on_rbf(x): + rbf_result = x == Qt.Checked + if self.wallet.use_rbf != rbf_result: + self.wallet.use_rbf = rbf_result + self.wallet.storage.put('use_rbf', self.wallet.use_rbf) + rbf_cb.stateChanged.connect(on_rbf) + rbf_cb.setToolTip(_('Enable RBF')) + tx_widgets.append((rbf_cb, None)) + usechange_cb = QCheckBox(_('Use change addresses')) usechange_cb.setChecked(self.wallet.use_change) if not self.config.is_modifiable('use_change'): usechange_cb.setEnabled(False) @@ -2796,6 +2809,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): multiple_cb.setEnabled(self.wallet.use_change) usechange_cb.stateChanged.connect(on_usechange) usechange_cb.setToolTip(_('Using change addresses makes it more difficult for other people to track your transactions.')) + tx_widgets.append((usechange_cb, None)) def on_multiple(x): multiple = x == Qt.Checked @@ -2812,7 +2826,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): ])) multiple_cb.setChecked(multiple_change) multiple_cb.stateChanged.connect(on_multiple) - tx_widgets.append((usechange_cb, None)) tx_widgets.append((multiple_cb, None)) showtx_cb = QCheckBox(_('View transaction before signing')) @@ -2973,20 +2986,14 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): def show_account_details(self, k): account = self.wallet.accounts[k] - d = WindowModalDialog(self, _('Account Details')) - vbox = QVBoxLayout(d) name = self.wallet.get_account_name(k) label = QLabel('Name: ' + name) vbox.addWidget(label) - vbox.addWidget(QLabel(_('Address type') + ': ' + account.get_type())) - vbox.addWidget(QLabel(_('Derivation') + ': ' + k)) - vbox.addWidget(QLabel(_('Master Public Key:'))) - text = QTextEdit() text.setReadOnly(True) text.setMaximumHeight(170) @@ -2995,3 +3002,28 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): text.setText(mpk_text) vbox.addLayout(Buttons(CloseButton(d))) d.exec_() + + def bump_fee_dialog(self, tx): + is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx) + fee = -fee + d = WindowModalDialog(self, _('Bump Fee')) + vbox = QVBoxLayout(d) + vbox.addWidget(QLabel(_('Current fee') + ': %s'% self.format_amount(fee) + ' ' + self.base_unit())) + vbox.addWidget(QLabel(_('New Fee') + ': ')) + e = BTCAmountEdit(self.get_decimal_point) + e.setAmount(fee + self.wallet.relayfee()) + vbox.addWidget(e) + vbox.addLayout(Buttons(CancelButton(d), OkButton(d))) + if not d.exec_(): + return + new_fee = e.get_amount() + delta = new_fee - fee + if delta < 0: + self.show_error("fee too low") + return + try: + new_tx = self.wallet.bump_fee(tx, delta) + except BaseException as e: + self.show_error(e) + return + self.show_transaction(new_tx) diff --git a/lib/wallet.py b/lib/wallet.py index af1426fdc..3d0f870ef 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -172,15 +172,17 @@ class Abstract_Wallet(PrintError): self.gap_limit_for_change = 6 # constant # saved fields self.seed_version = storage.get('seed_version', NEW_SEED_VERSION) - self.use_change = storage.get('use_change',True) + self.use_change = storage.get('use_change', True) self.multiple_change = storage.get('multiple_change', False) self.use_encryption = storage.get('use_encryption', False) + self.use_rbf = storage.get('use_rbf', False) self.seed = storage.get('seed', '') # encrypted self.labels = storage.get('labels', {}) self.frozen_addresses = set(storage.get('frozen_addresses',[])) self.stored_height = storage.get('stored_height', 0) # last known height (for offline mode) self.history = storage.get('addr_history',{}) # address -> list(txid, height) + # imported_keys is deprecated. The GUI should call convert_imported_keys self.imported_keys = self.storage.get('imported_keys',{}) @@ -988,6 +990,7 @@ class Abstract_Wallet(PrintError): def add_input_info(self, txin): address = txin['address'] + txin['sequence'] = 0 if self.use_rbf else 0xffffffff account_id, sequence = self.get_address_index(address) account = self.accounts[account_id] redeemScript = account.redeem_script(*sequence) @@ -998,7 +1001,6 @@ class Abstract_Wallet(PrintError): txin['pubkeys'] = list(pubkeys) txin['x_pubkeys'] = list(x_pubkeys) txin['signatures'] = [None] * len(pubkeys) - if redeemScript: txin['redeemScript'] = redeemScript txin['num_sig'] = account.m @@ -1176,6 +1178,23 @@ class Abstract_Wallet(PrintError): age = tx_age return age > age_limit + def bump_fee(self, tx, delta): + if tx.is_final(): + raise BaseException("cannot bump fee: transaction is final") + inputs = copy.deepcopy(tx.inputs()) + outputs = copy.deepcopy(tx.outputs()) + for txin in inputs: + txin['signatures'] = [None] * len(txin['signatures']) + for i, o in enumerate(outputs): + otype, address, value = o + if self.is_mine(address) and value >= delta: + outputs[i] = otype, address, value - delta + break + else: + raise BaseException("cannot bump fee") + new_tx = Transaction.from_io(inputs, outputs) + return new_tx + def can_sign(self, tx): if self.is_watching_only(): return False