diff --git a/RELEASE-NOTES b/RELEASE-NOTES index 4a9b99f6..de06fbe7 100644 --- a/RELEASE-NOTES +++ b/RELEASE-NOTES @@ -61,7 +61,6 @@ issue #3374. Users should upgrade to 3.0.5. # Release 2.9.3 * fix configuration file issue #2719 - * fix ledger signing of non-RBF transactions * disable 'spend confirmed only' option by default # Release 2.9.2 @@ -83,8 +82,7 @@ issue #3374. Users should upgrade to 3.0.5. classical SPV model. * The desired branch of a blockchain fork can be selected using the network dialog. Branches are identified by the hash and height of - the diverging block. Coin splitting is possible using RBF - transaction (a tutorial will be added). + the diverging block. * Multibit support: If the user enters a BIP39 seed (or uses a hardware wallet), the full derivation path is configurable in the install wizard. @@ -107,7 +105,6 @@ issue #3374. Users should upgrade to 3.0.5. # Release 2.8.2 * show paid invoices in history tab - * improve CPFP dialog * fixes for trezor, keepkey * other minor bugfixes @@ -130,8 +127,6 @@ issue #3374. Users should upgrade to 3.0.5. contacts files may be imported from the menu. * Fees improvements: - Dynamic fees are enabled by default. - - Child Pays For Parent (CPFP) dialog in the GUI. - - RBF is automatically proposed for low fee transactions. * Support for Digital Bitbox hardware wallet. * The GUI shows a blue icon when connected using a proxy. @@ -153,7 +148,6 @@ issue #3374. Users should upgrade to 3.0.5. # Release 2.7.15 * Use fee slider for both static and dynamic fees. - * Add fee slider to RBF dialog (fix #2083). * Simplify fee preferences. * Critical: Fix password update issue (#2097). This bug prevents password updates in multisig and 2FA wallets. It may also cause @@ -181,7 +175,6 @@ issue #3374. Users should upgrade to 3.0.5. # Release 2.7.10 * various fixes for hardware wallets - * improve fee bumping * separate sign and broadcast buttons in Qt tx dialog * allow spaces in private keys @@ -192,7 +185,6 @@ issue #3374. Users should upgrade to 3.0.5. * Fix hardware wallet issues #1975, #1976 # Release 2.7.8 - * Fix a bug with fee bumping * Fix crash when parsing request (issue #1969) # Release 2.7.7 @@ -256,10 +248,6 @@ issue #3374. Users should upgrade to 3.0.5. - Multiple hardware cosigners can be used in the same multisig wallet. One icon per keystore is displayed in the satus bar. Each connected device will co-sign the transaction. - * Replace-By-Fee: RBF transactions are supported in both Qt and - Android. A warning is displayed in the history for transactions - that are replaceable, have unconfirmed parents, or that have very - low fees. * Dynamic fees: Dynamic fees are enabled by default. A slider allows the user to select the expected confirmation time of their transaction. The expected confirmation times of incoming diff --git a/gui/kivy/main_window.py b/gui/kivy/main_window.py index 3da05ff3..2cc1386c 100644 --- a/gui/kivy/main_window.py +++ b/gui/kivy/main_window.py @@ -115,10 +115,6 @@ class ElectrumWindow(App): if len(names) >1: ChoiceDialog(_('Choose your chain'), names, '', cb).open() - use_rbf = BooleanProperty(False) - def on_use_rbf(self, instance, x): - self.electrum_config.set_key('use_rbf', self.use_rbf, True) - use_change = BooleanProperty(False) def on_use_change(self, instance, x): self.electrum_config.set_key('use_change', self.use_change, True) @@ -259,7 +255,6 @@ class ElectrumWindow(App): self.daemon = self.gui_object.daemon self.fx = self.daemon.fx - self.use_rbf = config.get('use_rbf', False) self.use_change = config.get('use_change', True) self.use_unconfirmed = not config.get('confirmed_only', False) diff --git a/gui/kivy/uix/dialogs/bump_fee_dialog.py b/gui/kivy/uix/dialogs/bump_fee_dialog.py deleted file mode 100644 index a5c74cee..00000000 --- a/gui/kivy/uix/dialogs/bump_fee_dialog.py +++ /dev/null @@ -1,119 +0,0 @@ -from kivy.app import App -from kivy.factory import Factory -from kivy.properties import ObjectProperty -from kivy.lang import Builder - -from electrum.util import fee_levels -from electrum_gui.kivy.i18n import _ - -Builder.load_string(''' - - title: _('Bump fee') - size_hint: 0.8, 0.8 - pos_hint: {'top':0.9} - BoxLayout: - orientation: 'vertical' - padding: '10dp' - - GridLayout: - height: self.minimum_height - size_hint_y: None - cols: 1 - spacing: '10dp' - BoxLabel: - id: old_fee - text: _('Current Fee') - value: '' - BoxLabel: - id: new_fee - text: _('New Fee') - value: '' - Label: - id: tooltip - text: '' - size_hint_y: None - Slider: - id: slider - range: 0, 4 - step: 1 - on_value: root.on_slider(self.value) - BoxLayout: - orientation: 'horizontal' - size_hint: 1, 0.2 - Label: - text: _('Final') - CheckBox: - id: final_cb - Widget: - size_hint: 1, 1 - BoxLayout: - orientation: 'horizontal' - size_hint: 1, 0.5 - Button: - text: 'Cancel' - size_hint: 0.5, None - height: '48dp' - on_release: root.dismiss() - Button: - text: 'OK' - size_hint: 0.5, None - height: '48dp' - on_release: - root.dismiss() - root.on_ok() -''') - -class BumpFeeDialog(Factory.Popup): - - def __init__(self, app, fee, size, callback): - Factory.Popup.__init__(self) - self.app = app - self.init_fee = fee - self.tx_size = size - self.callback = callback - self.config = app.electrum_config - self.fee_step = self.config.max_fee_rate() / 10 - self.dynfees = self.config.get('dynamic_fees', True) and self.app.network - self.ids.old_fee.value = self.app.format_amount_and_units(self.init_fee) - self.update_slider() - self.update_text() - - def update_text(self): - value = int(self.ids.slider.value) - self.ids.new_fee.value = self.app.format_amount_and_units(self.get_fee()) - if self.dynfees: - value = int(self.ids.slider.value) - self.ids.tooltip.text = fee_levels[value] - - def update_slider(self): - slider = self.ids.slider - if self.dynfees: - slider.range = (0, 4) - slider.step = 1 - slider.value = 3 - else: - slider.range = (1, 10) - slider.step = 1 - rate = self.init_fee*1000//self.tx_size - slider.value = min( rate * 2 // self.fee_step, 10) - - def get_fee(self): - value = int(self.ids.slider.value) - if self.dynfees: - if self.config.has_fee_estimates(): - dynfee = self.config.dynfee(value) - return int(dynfee * self.tx_size // 1000) - else: - return int(value*self.fee_step * self.tx_size // 1000) - - def on_ok(self): - new_fee = self.get_fee() - is_final = self.ids.final_cb.active - self.callback(self.init_fee, new_fee, is_final) - - def on_slider(self, value): - self.update_text() - - def on_checkbox(self, b): - self.dynfees = b - self.update_text() diff --git a/gui/kivy/uix/dialogs/settings.py b/gui/kivy/uix/dialogs/settings.py index 0d29d2ed..badd9182 100644 --- a/gui/kivy/uix/dialogs/settings.py +++ b/gui/kivy/uix/dialogs/settings.py @@ -67,16 +67,6 @@ Builder.load_string(''' description: _("Save and synchronize your labels.") action: partial(root.plugin_dialog, 'labels', self) CardSeparator - SettingsItem: - status: 'ON' if app.use_rbf else 'OFF' - title: _('Replace-by-fee') + ': ' + self.status - description: _("Create replaceable transactions.") - message: - _('If you check this box, your transactions will be marked as non-final,') \ - + ' ' + _('and you will have the possiblity, while they are unconfirmed, to replace them with transactions that pays higher fees.') \ - + ' ' + _('Note that some merchants do not accept non-final transactions until they are confirmed.') - action: partial(root.boolean_dialog, 'use_rbf', _('Replace by fee'), self.message) - CardSeparator SettingsItem: status: _('Yes') if app.use_unconfirmed else _('No') title: _('Spend unconfirmed') + ': ' + self.status diff --git a/gui/kivy/uix/dialogs/tx_dialog.py b/gui/kivy/uix/dialogs/tx_dialog.py index d5b87699..44d47b74 100644 --- a/gui/kivy/uix/dialogs/tx_dialog.py +++ b/gui/kivy/uix/dialogs/tx_dialog.py @@ -17,7 +17,6 @@ Builder.load_string(''' is_mine: True can_sign: False can_broadcast: False - can_rbf: False fee_str: '' date_str: '' amount_str: '' @@ -74,13 +73,12 @@ Builder.load_string(''' Button: size_hint: 0.5, None height: '48dp' - text: _('Sign') if root.can_sign else _('Broadcast') if root.can_broadcast else _('Bump fee') if root.can_rbf else '' - disabled: not(root.can_sign or root.can_broadcast or root.can_rbf) + text: _('Sign') if root.can_sign else _('Broadcast') if root.can_broadcast else '' + disabled: not(root.can_sign or root.can_broadcast) opacity: 0 if self.disabled else 1 on_release: if root.can_sign: root.do_sign() if root.can_broadcast: root.do_broadcast() - if root.can_rbf: root.do_rbf() IconButton: size_hint: 0.5, None height: '48dp' @@ -107,7 +105,7 @@ class TxDialog(Factory.Popup): def update(self): format_amount = self.app.format_amount_and_units - tx_hash, self.status_str, self.description, self.can_broadcast, self.can_rbf, amount, fee, height, conf, timestamp, exp_n = self.wallet.get_tx_info(self.tx) + tx_hash, self.status_str, self.description, self.can_broadcast, amount, fee, height, conf, timestamp, exp_n = self.wallet.get_tx_info(self.tx) self.tx_hash = tx_hash or '' if timestamp: self.date_str = datetime.fromtimestamp(timestamp).isoformat(' ')[:-3] @@ -128,31 +126,6 @@ class TxDialog(Factory.Popup): self.can_sign = self.wallet.can_sign(self.tx) self.ids.output_list.update(self.tx.outputs()) - def do_rbf(self): - from .bump_fee_dialog import BumpFeeDialog - is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(self.tx) - size = self.tx.estimated_size() - d = BumpFeeDialog(self.app, fee, size, self._do_rbf) - d.open() - - def _do_rbf(self, old_fee, new_fee, is_final): - if new_fee is None: - return - delta = new_fee - old_fee - if delta < 0: - self.app.show_error("fee too low") - return - try: - new_tx = self.wallet.bump_fee(self.tx, delta) - except BaseException as e: - self.app.show_error(str(e)) - return - if is_final: - new_tx.set_rbf(False) - self.tx = new_tx - self.update() - self.do_sign() - def do_sign(self): self.app.protected(_("Enter your PIN code in order to sign this transaction"), self._do_sign, ()) diff --git a/gui/kivy/uix/screens.py b/gui/kivy/uix/screens.py index b8ffe5dd..d0fbde4d 100644 --- a/gui/kivy/uix/screens.py +++ b/gui/kivy/uix/screens.py @@ -263,14 +263,9 @@ class SendScreen(CScreen): outputs = [(bitcoin.TYPE_ADDRESS, address, amount)] message = self.screen.message amount = sum(map(lambda x:x[2], outputs)) - if self.app.electrum_config.get('use_rbf'): - from .dialogs.question import Question - d = Question(_('Should this transaction be replaceable?'), lambda b: self._do_send(amount, message, outputs, b)) - d.open() - else: - self._do_send(amount, message, outputs, False) + self._do_send(amount, message, outputs) - def _do_send(self, amount, message, outputs, rbf): + def _do_send(self, amount, message, outputs): # make unsigned transaction config = self.app.electrum_config coins = self.app.wallet.get_spendable_coins(None, config) @@ -283,8 +278,6 @@ class SendScreen(CScreen): traceback.print_exc(file=sys.stdout) self.app.show_error(str(e)) return - if rbf: - tx.set_rbf(True) fee = tx.get_fee() msg = [ _("Amount to be sent") + ": " + self.app.format_amount_and_units(amount), diff --git a/gui/qt/history_list.py b/gui/qt/history_list.py index 6e17037b..970128b3 100644 --- a/gui/qt/history_list.py +++ b/gui/qt/history_list.py @@ -163,14 +163,6 @@ class HistoryList(MyTreeWidget): menu.addAction(_("Edit %s")%column_title, lambda: self.editItem(item, column)) menu.addAction(_("Details"), lambda: self.parent.show_transaction(tx)) - if is_unconfirmed and tx: - rbf = is_mine and not tx.is_final() - if rbf: - menu.addAction(_("Increase fee"), lambda: self.parent.bump_fee_dialog(tx)) - else: - child_tx = self.wallet.cpfp(tx, 0) - if child_tx: - menu.addAction(_("Child pays for parent"), lambda: self.parent.cpfp(tx, child_tx)) if pr_key: menu.addAction(QIcon(":icons/seal"), _("View invoice"), lambda: self.parent.show_invoice(pr_key)) if tx_URL: diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py index 288c2c6b..7dedfdc7 100644 --- a/gui/qt/main_window.py +++ b/gui/qt/main_window.py @@ -1081,17 +1081,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): self.fee_e.editingFinished.connect(self.update_fee) self.connect_fields(self, self.amount_e, self.fiat_send_e, self.fee_e) - self.rbf_checkbox = QCheckBox(_('Replaceable')) - msg = [_('If you check this box, your transaction will be marked as non-final,'), - _('and you will have the possiblity, while it is unconfirmed, to replace it with a transaction that pays a higher fee.'), - _('Note that some merchants do not accept non-final transactions until they are confirmed.')] - self.rbf_checkbox.setToolTip('

' + ' '.join(msg) + '

') - self.rbf_checkbox.setVisible(False) - grid.addWidget(self.fee_e_label, 5, 0) grid.addWidget(self.fee_slider, 5, 1) grid.addWidget(self.fee_e, 5, 2) - grid.addWidget(self.rbf_checkbox, 5, 3) self.preview_button = EnterButton(_("Preview"), self.do_preview) self.preview_button.setToolTip(_('Display the details of your transactions before signing it.')) @@ -1213,20 +1205,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): if fee is None: return - rbf_policy = self.config.get('rbf_policy', 1) - if rbf_policy == 0: - b = True - elif rbf_policy == 1: - fee_rate = fee * 1000 / tx.estimated_size() - try: - c = self.config.reverse_dynfee(fee_rate) - b = c in [-1, 25] - except: - b = False - elif rbf_policy == 2: - b = False - self.rbf_checkbox.setVisible(b) - self.rbf_checkbox.setChecked(b) def from_list_delete(self, item): @@ -1356,10 +1334,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): amount = tx.output_value() if self.is_max else sum(map(lambda x:x[2], outputs)) fee = tx.get_fee() - use_rbf = self.rbf_checkbox.isChecked() - if use_rbf: - tx.set_rbf(True) - if fee < self.wallet.relayfee() * tx.estimated_size() / 1000: self.show_error(_("This transaction requires a higher fee, or it will not be propagated by the network")) return @@ -1567,7 +1541,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): e.setText('') e.setFrozen(False) self.set_pay_from([]) - self.rbf_checkbox.setChecked(False) self.tx_external_keypairs = {} self.update_status() run_hook('do_clear', self) @@ -2506,16 +2479,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): feebox_cb.stateChanged.connect(on_feebox) fee_widgets.append((feebox_cb, None)) - rbf_policy = self.config.get('rbf_policy', 1) - rbf_label = HelpLabel(_('Propose Replace-By-Fee') + ':', '') - rbf_combo = QComboBox() - rbf_combo.addItems([_('Always'), _('If the fee is low'), _('Never')]) - rbf_combo.setCurrentIndex(rbf_policy) - def on_rbf(x): - self.config.set_key('rbf_policy', x) - rbf_combo.currentIndexChanged.connect(on_rbf) - fee_widgets.append((rbf_label, rbf_combo)) - self.fee_unit = self.config.get('fee_unit', 0) fee_unit_label = HelpLabel(_('Fee Unit') + ':', '') fee_unit_combo = QComboBox() @@ -2904,93 +2867,3 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError): grid.setRowStretch(len(plugins.descriptions.values()), 1) vbox.addLayout(Buttons(CloseButton(d))) d.exec_() - - def cpfp(self, parent_tx, new_tx): - total_size = parent_tx.estimated_size() + new_tx.estimated_size() - d = WindowModalDialog(self, _('Child Pays for Parent')) - vbox = QVBoxLayout(d) - msg = ( - "A CPFP is a transaction that sends an unconfirmed output back to " - "yourself, with a high fee. The goal is to have miners confirm " - "the parent transaction in order to get the fee attached to the " - "child transaction.") - vbox.addWidget(WWLabel(_(msg))) - msg2 = ("The proposed fee is computed using your " - "fee/kB settings, applied to the total size of both child and " - "parent transactions. After you broadcast a CPFP transaction, " - "it is normal to see a new unconfirmed transaction in your history.") - vbox.addWidget(WWLabel(_(msg2))) - grid = QGridLayout() - grid.addWidget(QLabel(_('Total size') + ':'), 0, 0) - grid.addWidget(QLabel('%d bytes'% total_size), 0, 1) - max_fee = new_tx.output_value() - grid.addWidget(QLabel(_('Input amount') + ':'), 1, 0) - grid.addWidget(QLabel(self.format_amount(max_fee) + ' ' + self.base_unit()), 1, 1) - output_amount = QLabel('') - grid.addWidget(QLabel(_('Output amount') + ':'), 2, 0) - grid.addWidget(output_amount, 2, 1) - fee_e = BTCAmountEdit(self.get_decimal_point) - def f(x): - a = max_fee - fee_e.get_amount() - output_amount.setText((self.format_amount(a) + ' ' + self.base_unit()) if a else '') - fee_e.textChanged.connect(f) - fee = self.config.fee_per_kb() * total_size / 1000 - fee_e.setAmount(fee) - grid.addWidget(QLabel(_('Fee' + ':')), 3, 0) - grid.addWidget(fee_e, 3, 1) - def on_rate(dyn, pos, fee_rate): - fee = fee_rate * total_size / 1000 - fee = min(max_fee, fee) - fee_e.setAmount(fee) - fee_slider = FeeSlider(self, self.config, on_rate) - fee_slider.update() - grid.addWidget(fee_slider, 4, 1) - vbox.addLayout(grid) - vbox.addLayout(Buttons(CancelButton(d), OkButton(d))) - if not d.exec_(): - return - fee = fee_e.get_amount() - if fee > max_fee: - self.show_error(_('Max fee exceeded')) - return - new_tx = self.wallet.cpfp(parent_tx, fee) - new_tx.set_rbf(True) - self.show_transaction(new_tx) - - def bump_fee_dialog(self, tx): - is_relevant, is_mine, v, fee = self.wallet.get_wallet_delta(tx) - tx_label = self.wallet.get_label(tx.txid()) - tx_size = tx.estimated_size() - 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' + ':'))) - - fee_e = BTCAmountEdit(self.get_decimal_point) - fee_e.setAmount(fee * 1.5) - vbox.addWidget(fee_e) - - def on_rate(dyn, pos, fee_rate): - fee = fee_rate * tx_size / 1000 - fee_e.setAmount(fee) - fee_slider = FeeSlider(self, self.config, on_rate) - vbox.addWidget(fee_slider) - cb = QCheckBox(_('Final')) - vbox.addWidget(cb) - vbox.addLayout(Buttons(CancelButton(d), OkButton(d))) - if not d.exec_(): - return - is_final = cb.isChecked() - new_fee = 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(str(e)) - return - if is_final: - new_tx.set_rbf(False) - self.show_transaction(new_tx, tx_label) diff --git a/gui/qt/transaction_dialog.py b/gui/qt/transaction_dialog.py index 71dbf8eb..a3683074 100644 --- a/gui/qt/transaction_dialog.py +++ b/gui/qt/transaction_dialog.py @@ -175,7 +175,7 @@ class TxDialog(QDialog, MessageBoxMixin): desc = self.desc base_unit = self.main_window.base_unit() format_amount = self.main_window.format_amount - tx_hash, status, label, can_broadcast, can_rbf, amount, fee, height, conf, timestamp, exp_n = self.wallet.get_tx_info(self.tx) + tx_hash, status, label, can_broadcast, amount, fee, height, conf, timestamp, exp_n = self.wallet.get_tx_info(self.tx) size = self.tx.estimated_size() self.broadcast_button.setEnabled(can_broadcast) can_sign = not self.tx.is_complete() and \ diff --git a/lib/commands.py b/lib/commands.py index 28aebaa1..91d994ea 100644 --- a/lib/commands.py +++ b/lib/commands.py @@ -410,7 +410,7 @@ class Commands: message = util.to_bytes(message) return bitcoin.verify_message(address, sig, message) - def _mktx(self, outputs, fee, change_addr, domain, nocheck, unsigned, rbf, password, locktime=None): + def _mktx(self, outputs, fee, change_addr, domain, nocheck, unsigned, password, locktime=None): self.nocheck = nocheck change_addr = self._resolver(change_addr) domain = None if domain is None else map(self._resolver, domain) @@ -424,27 +424,25 @@ class Commands: tx = self.wallet.make_unsigned_transaction(coins, final_outputs, self.config, fee, change_addr) if locktime != None: tx.locktime = locktime - if rbf: - tx.set_rbf(True) if not unsigned: run_hook('sign_tx', self.wallet, tx) self.wallet.sign_transaction(tx, password) return tx @command('wp') - def payto(self, destination, amount, fee=None, from_addr=None, change_addr=None, nocheck=False, unsigned=False, rbf=False, password=None, locktime=None): + def payto(self, destination, amount, fee=None, from_addr=None, change_addr=None, nocheck=False, unsigned=False, password=None, locktime=None): """Create a transaction. """ tx_fee = satoshis(fee) domain = from_addr.split(',') if from_addr else None - tx = self._mktx([(destination, amount)], tx_fee, change_addr, domain, nocheck, unsigned, rbf, password, locktime) + tx = self._mktx([(destination, amount)], tx_fee, change_addr, domain, nocheck, unsigned, password, locktime) return tx.as_dict() @command('wp') - def paytomany(self, outputs, fee=None, from_addr=None, change_addr=None, nocheck=False, unsigned=False, rbf=False, password=None, locktime=None): + def paytomany(self, outputs, fee=None, from_addr=None, change_addr=None, nocheck=False, unsigned=False, password=None, locktime=None): """Create a multi-output transaction. """ tx_fee = satoshis(fee) domain = from_addr.split(',') if from_addr else None - tx = self._mktx(outputs, tx_fee, change_addr, domain, nocheck, unsigned, rbf, password, locktime) + tx = self._mktx(outputs, tx_fee, change_addr, domain, nocheck, unsigned, password, locktime) return tx.as_dict() @command('w') @@ -716,7 +714,6 @@ command_options = { 'language': ("-L", "Default language for wordlist"), 'privkey': (None, "Private key. Set to '?' to get a prompt."), 'unsigned': ("-u", "Do not sign transaction"), - 'rbf': (None, "Replace-by-fee transaction"), 'locktime': (None, "Set locktime block number"), 'domain': ("-D", "List of addresses"), 'memo': ("-m", "Description of the request"), diff --git a/lib/transaction.py b/lib/transaction.py index 59fd19ed..4720f735 100644 --- a/lib/transaction.py +++ b/lib/transaction.py @@ -669,11 +669,6 @@ class Transaction: s += int_to_hex(txin.get('sequence', 0xffffffff - 1), 4) return s - def set_rbf(self, rbf): - nSequence = 0xffffffff - (2 if rbf else 1) - for txin in self.inputs(): - txin['sequence'] = nSequence - def BIP_LI01_sort(self): # See https://github.com/kristovatlas/rfc/blob/master/bips/bip-li01.mediawiki self._inputs.sort(key = lambda i: (i['prevout_hash'], i['prevout_n'])) diff --git a/lib/wallet.py b/lib/wallet.py index 6e893958..1a58d9ca 100644 --- a/lib/wallet.py +++ b/lib/wallet.py @@ -142,7 +142,6 @@ def sweep(privkeys, network, config, recipient, fee=None, imax=100): locktime = network.get_local_height() tx = Transaction.from_io(inputs, outputs, locktime=locktime) - tx.set_rbf(True) tx.sign(keypairs) return tx @@ -501,7 +500,6 @@ class Abstract_Wallet(PrintError): is_relevant, is_mine, v, fee = self.get_wallet_delta(tx) exp_n = None can_broadcast = False - can_bump = False label = '' height = conf = timestamp = None tx_hash = tx.txid() @@ -522,7 +520,6 @@ class Abstract_Wallet(PrintError): size = tx.estimated_size() fee_per_kb = fee * 1000 / size exp_n = self.network.config.reverse_dynfee(fee_per_kb) - can_bump = is_mine and not tx.is_final() else: status = _("Signed") can_broadcast = self.network is not None @@ -541,7 +538,7 @@ class Abstract_Wallet(PrintError): else: amount = None - return tx_hash, status, label, can_broadcast, can_bump, amount, fee, height, conf, timestamp, exp_n + return tx_hash, status, label, can_broadcast, amount, fee, height, conf, timestamp, exp_n def get_addr_io(self, address): h = self.history.get(address, []) @@ -1037,61 +1034,6 @@ 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']) - self.add_input_info(txin) - # use own outputs - s = list(filter(lambda x: self.is_mine(x[1]), outputs)) - # ... unless there is none - if not s: - s = outputs - x_fee = run_hook('get_tx_extra_fee', self, tx) - if x_fee: - x_fee_address, x_fee_amount = x_fee - s = filter(lambda x: x[1]!=x_fee_address, s) - - # prioritize low value outputs, to get rid of dust - s = sorted(s, key=lambda x: x[2]) - for o in s: - i = outputs.index(o) - otype, address, value = o - if value - delta >= self.dust_threshold(): - outputs[i] = otype, address, value - delta - delta = 0 - break - else: - del outputs[i] - delta -= value - if delta > 0: - continue - if delta > 0: - raise BaseException(_('Cannot bump fee: could not find suitable outputs')) - locktime = self.get_local_height() - return Transaction.from_io(inputs, outputs, locktime=locktime) - - def cpfp(self, tx, fee): - txid = tx.txid() - for i, o in enumerate(tx.outputs()): - otype, address, value = o - if otype == TYPE_ADDRESS and self.is_mine(address): - break - else: - return - coins = self.get_addr_utxo(address) - item = coins.get(txid+':%d'%i) - if not item: - return - self.add_input_info(item) - inputs = [item] - outputs = [(TYPE_ADDRESS, address, value - fee)] - locktime = self.get_local_height() - return Transaction.from_io(inputs, outputs, locktime=locktime) - def add_input_info(self, txin): address = txin['address'] if self.is_mine(address): diff --git a/plugins/trustedcoin/__init__.py b/plugins/trustedcoin/__init__.py deleted file mode 100644 index 76b39255..00000000 --- a/plugins/trustedcoin/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from electrum.i18n import _ - -fullname = _('Two Factor Authentication') -description = ''.join([ - _("This plugin adds two-factor authentication to your wallet."), '
', - _("For more information, visit"), - " https://api.trustedcoin.com/#/electrum-help" -]) -requires_wallet_type = ['2fa'] -registers_wallet_type = '2fa' -available_for = ['qt', 'cmdline'] diff --git a/plugins/trustedcoin/cmdline.py b/plugins/trustedcoin/cmdline.py deleted file mode 100644 index 51e34fda..00000000 --- a/plugins/trustedcoin/cmdline.py +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env python -# -# Electrum - Lightweight Bitcoin Client -# Copyright (C) 2015 Thomas Voegtlin -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation files -# (the "Software"), to deal in the Software without restriction, -# including without limitation the rights to use, copy, modify, merge, -# publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS -# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -from electrum.i18n import _ -from electrum.plugins import hook -from .trustedcoin import TrustedCoinPlugin - -class Plugin(TrustedCoinPlugin): - - @hook - def sign_tx(self, wallet, tx): - if not isinstance(wallet, self.wallet_class): - return - if not wallet.can_sign_without_server(): - self.print_error("twofactor:sign_tx") - auth_code = None - if wallet.keystores['x3/'].get_tx_derivations(tx): - msg = _('Please enter your Google Authenticator code:') - auth_code = int(input(msg)) - else: - self.print_error("twofactor: xpub3 not needed") - wallet.auth_code = auth_code - diff --git a/plugins/trustedcoin/qt.py b/plugins/trustedcoin/qt.py deleted file mode 100644 index cc0280cf..00000000 --- a/plugins/trustedcoin/qt.py +++ /dev/null @@ -1,292 +0,0 @@ -#!/usr/bin/env python -# -# Electrum - Lightweight Bitcoin Client -# Copyright (C) 2015 Thomas Voegtlin -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation files -# (the "Software"), to deal in the Software without restriction, -# including without limitation the rights to use, copy, modify, merge, -# publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS -# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -from functools import partial -from threading import Thread -import re -from decimal import Decimal - -from PyQt5.QtGui import * -from PyQt5.QtCore import * - -from electrum_gui.qt.util import * -from electrum_gui.qt.qrcodewidget import QRCodeWidget -from electrum_gui.qt.amountedit import AmountEdit -from electrum_gui.qt.main_window import StatusBarButton -from electrum.i18n import _ -from electrum.plugins import hook -from .trustedcoin import TrustedCoinPlugin, server - - -class TOS(QTextEdit): - tos_signal = pyqtSignal() - error_signal = pyqtSignal(object) - - -class Plugin(TrustedCoinPlugin): - - def __init__(self, parent, config, name): - super().__init__(parent, config, name) - - @hook - def on_new_window(self, window): - wallet = window.wallet - if not isinstance(wallet, self.wallet_class): - return - if wallet.can_sign_without_server(): - msg = ' '.join([ - _('This wallet was restored from seed, and it contains two master private keys.'), - _('Therefore, two-factor authentication is disabled.') - ]) - action = lambda: window.show_message(msg) - else: - action = partial(self.settings_dialog, window) - button = StatusBarButton(QIcon(":icons/trustedcoin-status.png"), - _("TrustedCoin"), action) - window.statusBar().addPermanentWidget(button) - self.start_request_thread(window.wallet) - - def auth_dialog(self, window): - d = WindowModalDialog(window, _("Authorization")) - vbox = QVBoxLayout(d) - pw = AmountEdit(None, is_int = True) - msg = _('Please enter your Google Authenticator code') - vbox.addWidget(QLabel(msg)) - grid = QGridLayout() - grid.setSpacing(8) - grid.addWidget(QLabel(_('Code')), 1, 0) - grid.addWidget(pw, 1, 1) - vbox.addLayout(grid) - msg = _('If you have lost your second factor, you need to restore your wallet from seed in order to request a new code.') - label = QLabel(msg) - label.setWordWrap(1) - vbox.addWidget(label) - vbox.addLayout(Buttons(CancelButton(d), OkButton(d))) - if not d.exec_(): - return - return pw.get_amount() - - @hook - def sign_tx(self, window, tx): - wallet = window.wallet - if not isinstance(wallet, self.wallet_class): - return - if not wallet.can_sign_without_server(): - self.print_error("twofactor:sign_tx") - auth_code = None - if wallet.keystores['x3/'].get_tx_derivations(tx): - auth_code = self.auth_dialog(window) - else: - self.print_error("twofactor: xpub3 not needed") - window.wallet.auth_code = auth_code - - def waiting_dialog(self, window, on_finished=None): - task = partial(self.request_billing_info, window.wallet) - return WaitingDialog(window, 'Getting billing information...', task, - on_finished) - - @hook - def abort_send(self, window): - wallet = window.wallet - if not isinstance(wallet, self.wallet_class): - return - if wallet.can_sign_without_server(): - return - if wallet.billing_info is None: - return True - return False - - - def settings_dialog(self, window): - self.waiting_dialog(window, partial(self.show_settings_dialog, window)) - - def show_settings_dialog(self, window, success): - if not success: - window.show_message(_('Server not reachable.')) - return - - wallet = window.wallet - d = WindowModalDialog(window, _("TrustedCoin Information")) - d.setMinimumSize(500, 200) - vbox = QVBoxLayout(d) - hbox = QHBoxLayout() - - logo = QLabel() - logo.setPixmap(QPixmap(":icons/trustedcoin-status.png")) - msg = _('This wallet is protected by TrustedCoin\'s two-factor authentication.') + '
'\ - + _("For more information, visit") + " https://api.trustedcoin.com/#/electrum-help" - label = QLabel(msg) - label.setOpenExternalLinks(1) - - hbox.addStretch(10) - hbox.addWidget(logo) - hbox.addStretch(10) - hbox.addWidget(label) - hbox.addStretch(10) - - vbox.addLayout(hbox) - vbox.addStretch(10) - - msg = _('TrustedCoin charges a small fee to co-sign transactions. The fee depends on how many prepaid transactions you buy. An extra output is added to your transaction everytime you run out of prepaid transactions.') + '
' - label = QLabel(msg) - label.setWordWrap(1) - vbox.addWidget(label) - - vbox.addStretch(10) - grid = QGridLayout() - vbox.addLayout(grid) - - price_per_tx = wallet.price_per_tx - n_prepay = wallet.num_prepay(self.config) - i = 0 - for k, v in sorted(price_per_tx.items()): - if k == 1: - continue - grid.addWidget(QLabel("Pay every %d transactions:"%k), i, 0) - grid.addWidget(QLabel(window.format_amount(v/k) + ' ' + window.base_unit() + "/tx"), i, 1) - b = QRadioButton() - b.setChecked(k == n_prepay) - b.clicked.connect(lambda b, k=k: self.config.set_key('trustedcoin_prepay', k, True)) - grid.addWidget(b, i, 2) - i += 1 - - n = wallet.billing_info.get('tx_remaining', 0) - grid.addWidget(QLabel(_("Your wallet has %d prepaid transactions.")%n), i, 0) - vbox.addLayout(Buttons(CloseButton(d))) - d.exec_() - - def on_buy(self, window, k, v, d): - d.close() - if window.pluginsdialog: - window.pluginsdialog.close() - wallet = window.wallet - uri = "bitcoin:" + wallet.billing_info['billing_address'] + "?message=TrustedCoin %d Prepaid Transactions&amount="%k + str(Decimal(v)/100000000) - wallet.is_billing = True - window.pay_to_URI(uri) - window.payto_e.setFrozen(True) - window.message_e.setFrozen(True) - window.amount_e.setFrozen(True) - - def accept_terms_of_use(self, window): - vbox = QVBoxLayout() - vbox.addWidget(QLabel(_("Terms of Service"))) - - tos_e = TOS() - tos_e.setReadOnly(True) - vbox.addWidget(tos_e) - tos_received = False - - vbox.addWidget(QLabel(_("Please enter your e-mail address"))) - email_e = QLineEdit() - vbox.addWidget(email_e) - - next_button = window.next_button - prior_button_text = next_button.text() - next_button.setText(_('Accept')) - - def request_TOS(): - try: - tos = server.get_terms_of_service() - except Exception as e: - import traceback - traceback.print_exc(file=sys.stderr) - tos_e.error_signal.emit(_('Could not retrieve Terms of Service:') - + '\n' + str(e)) - return - self.TOS = tos - tos_e.tos_signal.emit() - - def on_result(): - tos_e.setText(self.TOS) - nonlocal tos_received - tos_received = True - set_enabled() - - def on_error(msg): - window.show_error(str(msg)) - window.terminate() - - def set_enabled(): - valid_email = re.match(regexp, email_e.text()) is not None - next_button.setEnabled(tos_received and valid_email) - - tos_e.tos_signal.connect(on_result) - tos_e.error_signal.connect(on_error) - t = Thread(target=request_TOS) - t.setDaemon(True) - t.start() - - regexp = r"[^@]+@[^@]+\.[^@]+" - email_e.textChanged.connect(set_enabled) - email_e.setFocus(True) - - window.exec_layout(vbox, next_enabled=False) - next_button.setText(prior_button_text) - return str(email_e.text()) - - def request_otp_dialog(self, window, _id, otp_secret): - vbox = QVBoxLayout() - if otp_secret is not None: - uri = "otpauth://totp/%s?secret=%s"%('trustedcoin.com', otp_secret) - l = QLabel("Please scan the following QR code in Google Authenticator. You may as well use the following key: %s"%otp_secret) - l.setWordWrap(True) - vbox.addWidget(l) - qrw = QRCodeWidget(uri) - vbox.addWidget(qrw, 1) - msg = _('Then, enter your Google Authenticator code:') - else: - label = QLabel( - "This wallet is already registered with Trustedcoin. " - "To finalize wallet creation, please enter your Google Authenticator Code. " - ) - label.setWordWrap(1) - vbox.addWidget(label) - msg = _('Google Authenticator code:') - - hbox = QHBoxLayout() - hbox.addWidget(WWLabel(msg)) - pw = AmountEdit(None, is_int = True) - pw.setFocus(True) - pw.setMaximumWidth(50) - hbox.addWidget(pw) - vbox.addLayout(hbox) - - cb_lost = QCheckBox(_("I have lost my Google Authenticator account")) - cb_lost.setToolTip(_("Check this box to request a new secret. You will need to retype your seed.")) - vbox.addWidget(cb_lost) - cb_lost.setVisible(otp_secret is None) - - def set_enabled(): - b = True if cb_lost.isChecked() else len(pw.text()) == 6 - window.next_button.setEnabled(b) - - pw.textChanged.connect(set_enabled) - cb_lost.toggled.connect(set_enabled) - - window.exec_layout(vbox, next_enabled=False, - raise_on_cancel=False) - return pw.get_amount(), cb_lost.isChecked() - - diff --git a/plugins/trustedcoin/trustedcoin.py b/plugins/trustedcoin/trustedcoin.py deleted file mode 100644 index 20c6d1d3..00000000 --- a/plugins/trustedcoin/trustedcoin.py +++ /dev/null @@ -1,587 +0,0 @@ -#!/usr/bin/env python -# -# Electrum - Lightweight Bitcoin Client -# Copyright (C) 2015 Thomas Voegtlin -# -# Permission is hereby granted, free of charge, to any person -# obtaining a copy of this software and associated documentation files -# (the "Software"), to deal in the Software without restriction, -# including without limitation the rights to use, copy, modify, merge, -# publish, distribute, sublicense, and/or sell copies of the Software, -# and to permit persons to whom the Software is furnished to do so, -# subject to the following conditions: -# -# The above copyright notice and this permission notice shall be -# included in all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS -# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN -# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN -# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -# SOFTWARE. - -import socket -import os -import requests -import json -from urllib.parse import urljoin -from urllib.parse import quote - -import electrum -from electrum import bitcoin -from electrum import keystore -from electrum.bitcoin import * -from electrum.mnemonic import Mnemonic -from electrum import version -from electrum.wallet import Multisig_Wallet, Deterministic_Wallet -from electrum.i18n import _ -from electrum.plugins import BasePlugin, hook -from electrum.util import NotEnoughFunds - -# signing_xpub is hardcoded so that the wallet can be restored from seed, without TrustedCoin's server -signing_xpub = "xpub661MyMwAqRbcGnMkaTx2594P9EDuiEqMq25PM2aeG6UmwzaohgA6uDmNsvSUV8ubqwA3Wpste1hg69XHgjUuCD5HLcEp2QPzyV1HMrPppsL" -billing_xpub = "xpub6DTBdtBB8qUmH5c77v8qVGVoYk7WjJNpGvutqjLasNG1mbux6KsojaLrYf2sRhXAVU4NaFuHhbD9SvVPRt1MB1MaMooRuhHcAZH1yhQ1qDU" - -SEED_PREFIX = version.SEED_PREFIX_2FA - -DISCLAIMER = [ - _("Two-factor authentication is a service provided by TrustedCoin. " - "It uses a multi-signature wallet, where you own 2 of 3 keys. " - "The third key is stored on a remote server that signs transactions on " - "your behalf. To use this service, you will need a smartphone with " - "Google Authenticator installed."), - _("A small fee will be charged on each transaction that uses the " - "remote server. You may check and modify your billing preferences " - "once the installation is complete."), - _("Note that your coins are not locked in this service. You may withdraw " - "your funds at any time and at no cost, without the remote server, by " - "using the 'restore wallet' option with your wallet seed."), - _("The next step will generate the seed of your wallet. This seed will " - "NOT be saved in your computer, and it must be stored on paper. " - "To be safe from malware, you may want to do this on an offline " - "computer, and move your wallet later to an online computer."), -] -RESTORE_MSG = _("Enter the seed for your 2-factor wallet:") - -class TrustedCoinException(Exception): - def __init__(self, message, status_code=0): - Exception.__init__(self, message) - self.status_code = status_code - -class TrustedCoinCosignerClient(object): - def __init__(self, user_agent=None, base_url='https://api.trustedcoin.com/2/'): - self.base_url = base_url - self.debug = False - self.user_agent = user_agent - - def send_request(self, method, relative_url, data=None): - kwargs = {'headers': {}} - if self.user_agent: - kwargs['headers']['user-agent'] = self.user_agent - if method == 'get' and data: - kwargs['params'] = data - elif method == 'post' and data: - kwargs['data'] = json.dumps(data) - kwargs['headers']['content-type'] = 'application/json' - url = urljoin(self.base_url, relative_url) - if self.debug: - print('%s %s %s' % (method, url, data)) - response = requests.request(method, url, **kwargs) - if self.debug: - print(response.text) - if response.status_code != 200: - message = str(response.text) - if response.headers.get('content-type') == 'application/json': - r = response.json() - if 'message' in r: - message = r['message'] - raise TrustedCoinException(message, response.status_code) - if response.headers.get('content-type') == 'application/json': - return response.json() - else: - return response.text - - def get_terms_of_service(self, billing_plan='electrum-per-tx-otp'): - """ - Returns the TOS for the given billing plan as a plain/text unicode string. - :param billing_plan: the plan to return the terms for - """ - payload = {'billing_plan': billing_plan} - return self.send_request('get', 'tos', payload) - - def create(self, xpubkey1, xpubkey2, email, billing_plan='electrum-per-tx-otp'): - """ - Creates a new cosigner resource. - :param xpubkey1: a bip32 extended public key (customarily the hot key) - :param xpubkey2: a bip32 extended public key (customarily the cold key) - :param email: a contact email - :param billing_plan: the billing plan for the cosigner - """ - payload = { - 'email': email, - 'xpubkey1': xpubkey1, - 'xpubkey2': xpubkey2, - 'billing_plan': billing_plan, - } - return self.send_request('post', 'cosigner', payload) - - def auth(self, id, otp): - """ - Attempt to authenticate for a particular cosigner. - :param id: the id of the cosigner - :param otp: the one time password - """ - payload = {'otp': otp} - return self.send_request('post', 'cosigner/%s/auth' % quote(id), payload) - - def get(self, id): - """ Get billing info """ - return self.send_request('get', 'cosigner/%s' % quote(id)) - - def get_challenge(self, id): - """ Get challenge to reset Google Auth secret """ - return self.send_request('get', 'cosigner/%s/otp_secret' % quote(id)) - - def reset_auth(self, id, challenge, signatures): - """ Reset Google Auth secret """ - payload = {'challenge':challenge, 'signatures':signatures} - return self.send_request('post', 'cosigner/%s/otp_secret' % quote(id), payload) - - def sign(self, id, transaction, otp): - """ - Attempt to authenticate for a particular cosigner. - :param id: the id of the cosigner - :param transaction: the hex encoded [partially signed] compact transaction to sign - :param otp: the one time password - """ - payload = { - 'otp': otp, - 'transaction': transaction - } - return self.send_request('post', 'cosigner/%s/sign' % quote(id), payload) - - def transfer_credit(self, id, recipient, otp, signature_callback): - """ - Tranfer a cosigner's credits to another cosigner. - :param id: the id of the sending cosigner - :param recipient: the id of the recipient cosigner - :param otp: the one time password (of the sender) - :param signature_callback: a callback that signs a text message using xpubkey1/0/0 returning a compact sig - """ - payload = { - 'otp': otp, - 'recipient': recipient, - 'timestamp': int(time.time()), - - } - relative_url = 'cosigner/%s/transfer' % quote(id) - full_url = urljoin(self.base_url, relative_url) - headers = { - 'x-signature': signature_callback(full_url + '\n' + json.dumps(payload)) - } - return self.send_request('post', relative_url, payload, headers) - - -server = TrustedCoinCosignerClient(user_agent="Electrum/" + version.ELECTRUM_VERSION) - -class Wallet_2fa(Multisig_Wallet): - - wallet_type = '2fa' - - def __init__(self, storage): - self.m, self.n = 2, 3 - Deterministic_Wallet.__init__(self, storage) - self.is_billing = False - self.billing_info = None - - def can_sign_without_server(self): - return not self.keystores['x2/'].is_watching_only() - - def get_user_id(self): - return get_user_id(self.storage) - - def get_max_amount(self, config, inputs, recipient, fee): - from electrum.transaction import Transaction - sendable = sum(map(lambda x:x['value'], inputs)) - for i in inputs: - self.add_input_info(i) - xf = self.extra_fee(config) - _type, addr = recipient - if xf and sendable >= xf: - billing_address = self.billing_info['billing_address'] - sendable -= xf - outputs = [(_type, addr, sendable), - (TYPE_ADDRESS, billing_address, xf)] - else: - outputs = [(_type, addr, sendable)] - dummy_tx = Transaction.from_io(inputs, outputs) - if fee is None: - fee = self.estimate_fee(config, dummy_tx.estimated_size()) - amount = max(0, sendable - fee) - return amount, fee - - def min_prepay(self): - return min(self.price_per_tx.keys()) - - def num_prepay(self, config): - default = self.min_prepay() - n = config.get('trustedcoin_prepay', default) - if n not in self.price_per_tx: - n = default - return n - - def extra_fee(self, config): - if self.can_sign_without_server(): - return 0 - if self.billing_info is None: - self.plugin.start_request_thread(self) - return 0 - if self.billing_info.get('tx_remaining'): - return 0 - if self.is_billing: - return 0 - n = self.num_prepay(config) - price = int(self.price_per_tx[n]) - assert price <= 100000 * n - return price - - def make_unsigned_transaction(self, coins, outputs, config, fixed_fee=None, - change_addr=None, is_sweep=False): - mk_tx = lambda o: Multisig_Wallet.make_unsigned_transaction( - self, coins, o, config, fixed_fee, change_addr) - fee = self.extra_fee(config) if not is_sweep else 0 - if fee: - address = self.billing_info['billing_address'] - fee_output = (TYPE_ADDRESS, address, fee) - try: - tx = mk_tx(outputs + [fee_output]) - except NotEnoughFunds: - # trustedcoin won't charge if the total inputs is - # lower than their fee - tx = mk_tx(outputs) - if tx.input_value() >= fee: - raise - self.print_error("not charging for this tx") - else: - tx = mk_tx(outputs) - return tx - - def sign_transaction(self, tx, password): - Multisig_Wallet.sign_transaction(self, tx, password) - if tx.is_complete(): - return - if not self.auth_code: - self.print_error("sign_transaction: no auth code") - return - long_user_id, short_id = self.get_user_id() - tx_dict = tx.as_dict() - raw_tx = tx_dict["hex"] - r = server.sign(short_id, raw_tx, self.auth_code) - if r: - raw_tx = r.get('transaction') - tx.update(raw_tx) - self.print_error("twofactor: is complete", tx.is_complete()) - # reset billing_info - self.billing_info = None - - -# Utility functions - -def get_user_id(storage): - def make_long_id(xpub_hot, xpub_cold): - return bitcoin.sha256(''.join(sorted([xpub_hot, xpub_cold]))) - xpub1 = storage.get('x1/')['xpub'] - xpub2 = storage.get('x2/')['xpub'] - long_id = make_long_id(xpub1, xpub2) - short_id = hashlib.sha256(long_id).hexdigest() - return long_id, short_id - -def make_xpub(xpub, s): - version, _, _, _, c, cK = deserialize_xpub(xpub) - cK2, c2 = bitcoin._CKD_pub(cK, c, s) - return bitcoin.serialize_xpub(version, c2, cK2) - -def make_billing_address(wallet, num): - long_id, short_id = wallet.get_user_id() - xpub = make_xpub(billing_xpub, long_id) - version, _, _, _, c, cK = deserialize_xpub(xpub) - cK, c = bitcoin.CKD_pub(cK, c, num) - return bitcoin.public_key_to_p2pkh(cK) - - -class TrustedCoinPlugin(BasePlugin): - wallet_class = Wallet_2fa - - def __init__(self, parent, config, name): - BasePlugin.__init__(self, parent, config, name) - self.wallet_class.plugin = self - self.requesting = False - - @staticmethod - def is_valid_seed(seed): - return bitcoin.is_new_seed(seed, SEED_PREFIX) - - def is_available(self): - return True - - def is_enabled(self): - return True - - def can_user_disable(self): - return False - - @hook - def get_tx_extra_fee(self, wallet, tx): - if type(wallet) != Wallet_2fa: - return - address = wallet.billing_info['billing_address'] - for _type, addr, amount in tx.outputs(): - if _type == TYPE_ADDRESS and addr == address: - return address, amount - - def request_billing_info(self, wallet): - self.print_error("request billing info") - billing_info = server.get(wallet.get_user_id()[1]) - billing_address = make_billing_address(wallet, billing_info['billing_index']) - assert billing_address == billing_info['billing_address'] - wallet.billing_info = billing_info - wallet.price_per_tx = dict(billing_info['price_per_tx']) - wallet.price_per_tx.pop(1) - self.requesting = False - return True - - def start_request_thread(self, wallet): - from threading import Thread - if self.requesting is False: - self.requesting = True - t = Thread(target=self.request_billing_info, args=(wallet,)) - t.setDaemon(True) - t.start() - return t - - def make_seed(self): - return Mnemonic('english').make_seed(seed_type='2fa', num_bits=128) - - @hook - def do_clear(self, window): - window.wallet.is_billing = False - - def show_disclaimer(self, wizard): - wizard.set_icon(':icons/trustedcoin-wizard.png') - wizard.stack = [] - wizard.confirm_dialog(title='Disclaimer', message='\n\n'.join(DISCLAIMER), run_next = lambda x: wizard.run('choose_seed')) - - def choose_seed(self, wizard): - title = _('Create or restore') - message = _('Do you want to create a new seed, or to restore a wallet using an existing seed?') - choices = [ - ('create_seed', _('Create a new seed')), - ('restore_wallet', _('I already have a seed')), - ] - wizard.choice_dialog(title=title, message=message, choices=choices, run_next=wizard.run) - - def create_seed(self, wizard): - seed = self.make_seed() - f = lambda x: wizard.request_passphrase(seed, x) - wizard.show_seed_dialog(run_next=f, seed_text=seed) - - def get_xkeys(self, seed, passphrase, derivation): - from electrum.mnemonic import Mnemonic - from electrum.keystore import bip32_root, bip32_private_derivation - bip32_seed = Mnemonic.mnemonic_to_seed(seed, passphrase) - xprv, xpub = bip32_root(bip32_seed, 'standard') - xprv, xpub = bip32_private_derivation(xprv, "m/", derivation) - return xprv, xpub - - def xkeys_from_seed(self, seed, passphrase): - words = seed.split() - n = len(words) - # old version use long seed phrases - if n >= 24: - assert passphrase == '' - xprv1, xpub1 = self.get_xkeys(' '.join(words[0:12]), '', "m/") - xprv2, xpub2 = self.get_xkeys(' '.join(words[12:]), '', "m/") - elif n==12: - xprv1, xpub1 = self.get_xkeys(seed, passphrase, "m/0'/") - xprv2, xpub2 = self.get_xkeys(seed, passphrase, "m/1'/") - else: - raise BaseException('unrecognized seed length') - return xprv1, xpub1, xprv2, xpub2 - - def create_keystore(self, wizard, seed, passphrase): - # this overloads the wizard's method - xprv1, xpub1, xprv2, xpub2 = self.xkeys_from_seed(seed, passphrase) - k1 = keystore.from_xprv(xprv1) - k2 = keystore.from_xpub(xpub2) - wizard.request_password(run_next=lambda pw, encrypt: self.on_password(wizard, pw, encrypt, k1, k2)) - - def on_password(self, wizard, password, encrypt, k1, k2): - k1.update_password(None, password) - wizard.storage.set_password(password, encrypt) - wizard.storage.put('x1/', k1.dump()) - wizard.storage.put('x2/', k2.dump()) - wizard.storage.write() - msg = [ - _("Your wallet file is: %s.")%os.path.abspath(wizard.storage.path), - _("You need to be online in order to complete the creation of " - "your wallet. If you generated your seed on an offline " - 'computer, click on "%s" to close this window, move your ' - "wallet file to an online computer, and reopen it with " - "Electrum.") % _('Cancel'), - _('If you are online, click on "%s" to continue.') % _('Next') - ] - msg = '\n\n'.join(msg) - wizard.stack = [] - wizard.confirm_dialog(title='', message=msg, run_next = lambda x: wizard.run('create_remote_key')) - - def restore_wallet(self, wizard): - wizard.opt_bip39 = False - wizard.opt_ext = True - title = _("Restore two-factor Wallet") - f = lambda seed, is_bip39, is_ext: wizard.run('on_restore_seed', seed, is_ext) - wizard.restore_seed_dialog(run_next=f, test=self.is_valid_seed) - - def on_restore_seed(self, wizard, seed, is_ext): - f = lambda x: self.restore_choice(wizard, seed, x) - wizard.passphrase_dialog(run_next=f) if is_ext else f('') - - def restore_choice(self, wizard, seed, passphrase): - wizard.set_icon(':icons/trustedcoin-wizard.png') - wizard.stack = [] - title = _('Restore 2FA wallet') - msg = ' '.join([ - 'You are going to restore a wallet protected with two-factor authentication.', - 'Do you want to keep using two-factor authentication with this wallet,', - 'or do you want to disable it, and have two master private keys in your wallet?' - ]) - choices = [('keep', 'Keep'), ('disable', 'Disable')] - f = lambda x: self.on_choice(wizard, seed, passphrase, x) - wizard.choice_dialog(choices=choices, message=msg, title=title, run_next=f) - - def on_choice(self, wizard, seed, passphrase, x): - if x == 'disable': - f = lambda pw, encrypt: wizard.run('on_restore_pw', seed, passphrase, pw, encrypt) - wizard.request_password(run_next=f) - else: - self.create_keystore(wizard, seed, passphrase) - - def on_restore_pw(self, wizard, seed, passphrase, password, encrypt): - storage = wizard.storage - xprv1, xpub1, xprv2, xpub2 = self.xkeys_from_seed(seed, passphrase) - k1 = keystore.from_xprv(xprv1) - k2 = keystore.from_xprv(xprv2) - k1.add_seed(seed) - k1.update_password(None, password) - k2.update_password(None, password) - storage.put('x1/', k1.dump()) - storage.put('x2/', k2.dump()) - long_user_id, short_id = get_user_id(storage) - xpub3 = make_xpub(signing_xpub, long_user_id) - k3 = keystore.from_xpub(xpub3) - storage.put('x3/', k3.dump()) - storage.set_password(password, encrypt) - wizard.wallet = Wallet_2fa(storage) - wizard.create_addresses() - - def create_remote_key(self, wizard): - email = self.accept_terms_of_use(wizard) - xpub1 = wizard.storage.get('x1/')['xpub'] - xpub2 = wizard.storage.get('x2/')['xpub'] - # Generate third key deterministically. - long_user_id, short_id = get_user_id(wizard.storage) - xpub3 = make_xpub(signing_xpub, long_user_id) - # secret must be sent by the server - try: - r = server.create(xpub1, xpub2, email) - except socket.error: - wizard.show_message('Server not reachable, aborting') - return - except TrustedCoinException as e: - if e.status_code == 409: - r = None - else: - wizard.show_message(str(e)) - return - if r is None: - otp_secret = None - else: - otp_secret = r.get('otp_secret') - if not otp_secret: - wizard.show_message(_('Error')) - return - _xpub3 = r['xpubkey_cosigner'] - _id = r['id'] - try: - assert _id == short_id, ("user id error", _id, short_id) - assert xpub3 == _xpub3, ("xpub3 error", xpub3, _xpub3) - except Exception as e: - wizard.show_message(str(e)) - return - self.check_otp(wizard, short_id, otp_secret, xpub3) - - def check_otp(self, wizard, short_id, otp_secret, xpub3): - otp, reset = self.request_otp_dialog(wizard, short_id, otp_secret) - if otp: - self.do_auth(wizard, short_id, otp, xpub3) - elif reset: - wizard.opt_bip39 = False - wizard.opt_ext = True - f = lambda seed, is_bip39, is_ext: wizard.run('on_reset_seed', short_id, seed, is_ext, xpub3) - wizard.restore_seed_dialog(run_next=f, test=self.is_valid_seed) - - def on_reset_seed(self, wizard, short_id, seed, is_ext, xpub3): - f = lambda passphrase: wizard.run('on_reset_auth', short_id, seed, passphrase, xpub3) - wizard.passphrase_dialog(run_next=f) if is_ext else f('') - - def do_auth(self, wizard, short_id, otp, xpub3): - try: - server.auth(short_id, otp) - except: - wizard.show_message(_('Incorrect password')) - return - k3 = keystore.from_xpub(xpub3) - wizard.storage.put('x3/', k3.dump()) - wizard.storage.put('use_trustedcoin', True) - wizard.storage.write() - wizard.wallet = Wallet_2fa(wizard.storage) - wizard.run('create_addresses') - - def on_reset_auth(self, wizard, short_id, seed, passphrase, xpub3): - xprv1, xpub1, xprv2, xpub2 = self.xkeys_from_seed(seed, passphrase) - try: - assert xpub1 == wizard.storage.get('x1/')['xpub'] - assert xpub2 == wizard.storage.get('x2/')['xpub'] - except: - wizard.show_message(_('Incorrect seed')) - return - r = server.get_challenge(short_id) - challenge = r.get('challenge') - message = 'TRUSTEDCOIN CHALLENGE: ' + challenge - def f(xprv): - _, _, _, _, c, k = deserialize_xprv(xprv) - pk = bip32_private_key([0, 0], k, c) - key = regenerate_key(pk) - sig = key.sign_message(message, True) - return base64.b64encode(sig).decode() - - signatures = [f(x) for x in [xprv1, xprv2]] - r = server.reset_auth(short_id, challenge, signatures) - new_secret = r.get('otp_secret') - if not new_secret: - wizard.show_message(_('Request rejected by server')) - return - self.check_otp(wizard, short_id, new_secret, xpub3) - - @hook - def get_action(self, storage): - if storage.get('wallet_type') != '2fa': - return - if not storage.get('x1/'): - return self, 'show_disclaimer' - if not storage.get('x2/'): - return self, 'show_disclaimer' - if not storage.get('x3/'): - return self, 'create_remote_key'