remove RBF/CPFP support, trustedcoin plugin
This commit is contained in:
parent
a0f4f21382
commit
de2ef374c9
|
@ -61,7 +61,6 @@ issue #3374. Users should upgrade to 3.0.5.
|
||||||
|
|
||||||
# Release 2.9.3
|
# Release 2.9.3
|
||||||
* fix configuration file issue #2719
|
* fix configuration file issue #2719
|
||||||
* fix ledger signing of non-RBF transactions
|
|
||||||
* disable 'spend confirmed only' option by default
|
* disable 'spend confirmed only' option by default
|
||||||
|
|
||||||
# Release 2.9.2
|
# Release 2.9.2
|
||||||
|
@ -83,8 +82,7 @@ issue #3374. Users should upgrade to 3.0.5.
|
||||||
classical SPV model.
|
classical SPV model.
|
||||||
* The desired branch of a blockchain fork can be selected using the
|
* The desired branch of a blockchain fork can be selected using the
|
||||||
network dialog. Branches are identified by the hash and height of
|
network dialog. Branches are identified by the hash and height of
|
||||||
the diverging block. Coin splitting is possible using RBF
|
the diverging block.
|
||||||
transaction (a tutorial will be added).
|
|
||||||
* Multibit support: If the user enters a BIP39 seed (or uses a
|
* Multibit support: If the user enters a BIP39 seed (or uses a
|
||||||
hardware wallet), the full derivation path is configurable in the
|
hardware wallet), the full derivation path is configurable in the
|
||||||
install wizard.
|
install wizard.
|
||||||
|
@ -107,7 +105,6 @@ issue #3374. Users should upgrade to 3.0.5.
|
||||||
|
|
||||||
# Release 2.8.2
|
# Release 2.8.2
|
||||||
* show paid invoices in history tab
|
* show paid invoices in history tab
|
||||||
* improve CPFP dialog
|
|
||||||
* fixes for trezor, keepkey
|
* fixes for trezor, keepkey
|
||||||
* other minor bugfixes
|
* other minor bugfixes
|
||||||
|
|
||||||
|
@ -130,8 +127,6 @@ issue #3374. Users should upgrade to 3.0.5.
|
||||||
contacts files may be imported from the menu.
|
contacts files may be imported from the menu.
|
||||||
* Fees improvements:
|
* Fees improvements:
|
||||||
- Dynamic fees are enabled by default.
|
- 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.
|
* Support for Digital Bitbox hardware wallet.
|
||||||
* The GUI shows a blue icon when connected using a proxy.
|
* 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
|
# Release 2.7.15
|
||||||
* Use fee slider for both static and dynamic fees.
|
* Use fee slider for both static and dynamic fees.
|
||||||
* Add fee slider to RBF dialog (fix #2083).
|
|
||||||
* Simplify fee preferences.
|
* Simplify fee preferences.
|
||||||
* Critical: Fix password update issue (#2097). This bug prevents
|
* Critical: Fix password update issue (#2097). This bug prevents
|
||||||
password updates in multisig and 2FA wallets. It may also cause
|
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
|
# Release 2.7.10
|
||||||
* various fixes for hardware wallets
|
* various fixes for hardware wallets
|
||||||
* improve fee bumping
|
|
||||||
* separate sign and broadcast buttons in Qt tx dialog
|
* separate sign and broadcast buttons in Qt tx dialog
|
||||||
* allow spaces in private keys
|
* allow spaces in private keys
|
||||||
|
|
||||||
|
@ -192,7 +185,6 @@ issue #3374. Users should upgrade to 3.0.5.
|
||||||
* Fix hardware wallet issues #1975, #1976
|
* Fix hardware wallet issues #1975, #1976
|
||||||
|
|
||||||
# Release 2.7.8
|
# Release 2.7.8
|
||||||
* Fix a bug with fee bumping
|
|
||||||
* Fix crash when parsing request (issue #1969)
|
* Fix crash when parsing request (issue #1969)
|
||||||
|
|
||||||
# Release 2.7.7
|
# 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
|
- Multiple hardware cosigners can be used in the same multisig
|
||||||
wallet. One icon per keystore is displayed in the satus bar. Each
|
wallet. One icon per keystore is displayed in the satus bar. Each
|
||||||
connected device will co-sign the transaction.
|
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
|
* Dynamic fees: Dynamic fees are enabled by default. A slider allows
|
||||||
the user to select the expected confirmation time of their
|
the user to select the expected confirmation time of their
|
||||||
transaction. The expected confirmation times of incoming
|
transaction. The expected confirmation times of incoming
|
||||||
|
|
|
@ -115,10 +115,6 @@ class ElectrumWindow(App):
|
||||||
if len(names) >1:
|
if len(names) >1:
|
||||||
ChoiceDialog(_('Choose your chain'), names, '', cb).open()
|
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)
|
use_change = BooleanProperty(False)
|
||||||
def on_use_change(self, instance, x):
|
def on_use_change(self, instance, x):
|
||||||
self.electrum_config.set_key('use_change', self.use_change, True)
|
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.daemon = self.gui_object.daemon
|
||||||
self.fx = self.daemon.fx
|
self.fx = self.daemon.fx
|
||||||
|
|
||||||
self.use_rbf = config.get('use_rbf', False)
|
|
||||||
self.use_change = config.get('use_change', True)
|
self.use_change = config.get('use_change', True)
|
||||||
self.use_unconfirmed = not config.get('confirmed_only', False)
|
self.use_unconfirmed = not config.get('confirmed_only', False)
|
||||||
|
|
||||||
|
|
|
@ -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('''
|
|
||||||
<BumpFeeDialog@Popup>
|
|
||||||
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()
|
|
|
@ -67,16 +67,6 @@ Builder.load_string('''
|
||||||
description: _("Save and synchronize your labels.")
|
description: _("Save and synchronize your labels.")
|
||||||
action: partial(root.plugin_dialog, 'labels', self)
|
action: partial(root.plugin_dialog, 'labels', self)
|
||||||
CardSeparator
|
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:
|
SettingsItem:
|
||||||
status: _('Yes') if app.use_unconfirmed else _('No')
|
status: _('Yes') if app.use_unconfirmed else _('No')
|
||||||
title: _('Spend unconfirmed') + ': ' + self.status
|
title: _('Spend unconfirmed') + ': ' + self.status
|
||||||
|
|
|
@ -17,7 +17,6 @@ Builder.load_string('''
|
||||||
is_mine: True
|
is_mine: True
|
||||||
can_sign: False
|
can_sign: False
|
||||||
can_broadcast: False
|
can_broadcast: False
|
||||||
can_rbf: False
|
|
||||||
fee_str: ''
|
fee_str: ''
|
||||||
date_str: ''
|
date_str: ''
|
||||||
amount_str: ''
|
amount_str: ''
|
||||||
|
@ -74,13 +73,12 @@ Builder.load_string('''
|
||||||
Button:
|
Button:
|
||||||
size_hint: 0.5, None
|
size_hint: 0.5, None
|
||||||
height: '48dp'
|
height: '48dp'
|
||||||
text: _('Sign') if root.can_sign else _('Broadcast') if root.can_broadcast else _('Bump fee') if root.can_rbf else ''
|
text: _('Sign') if root.can_sign else _('Broadcast') if root.can_broadcast else ''
|
||||||
disabled: not(root.can_sign or root.can_broadcast or root.can_rbf)
|
disabled: not(root.can_sign or root.can_broadcast)
|
||||||
opacity: 0 if self.disabled else 1
|
opacity: 0 if self.disabled else 1
|
||||||
on_release:
|
on_release:
|
||||||
if root.can_sign: root.do_sign()
|
if root.can_sign: root.do_sign()
|
||||||
if root.can_broadcast: root.do_broadcast()
|
if root.can_broadcast: root.do_broadcast()
|
||||||
if root.can_rbf: root.do_rbf()
|
|
||||||
IconButton:
|
IconButton:
|
||||||
size_hint: 0.5, None
|
size_hint: 0.5, None
|
||||||
height: '48dp'
|
height: '48dp'
|
||||||
|
@ -107,7 +105,7 @@ class TxDialog(Factory.Popup):
|
||||||
|
|
||||||
def update(self):
|
def update(self):
|
||||||
format_amount = self.app.format_amount_and_units
|
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 ''
|
self.tx_hash = tx_hash or ''
|
||||||
if timestamp:
|
if timestamp:
|
||||||
self.date_str = datetime.fromtimestamp(timestamp).isoformat(' ')[:-3]
|
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.can_sign = self.wallet.can_sign(self.tx)
|
||||||
self.ids.output_list.update(self.tx.outputs())
|
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):
|
def do_sign(self):
|
||||||
self.app.protected(_("Enter your PIN code in order to sign this transaction"), self._do_sign, ())
|
self.app.protected(_("Enter your PIN code in order to sign this transaction"), self._do_sign, ())
|
||||||
|
|
||||||
|
|
|
@ -263,14 +263,9 @@ class SendScreen(CScreen):
|
||||||
outputs = [(bitcoin.TYPE_ADDRESS, address, amount)]
|
outputs = [(bitcoin.TYPE_ADDRESS, address, amount)]
|
||||||
message = self.screen.message
|
message = self.screen.message
|
||||||
amount = sum(map(lambda x:x[2], outputs))
|
amount = sum(map(lambda x:x[2], outputs))
|
||||||
if self.app.electrum_config.get('use_rbf'):
|
self._do_send(amount, message, outputs)
|
||||||
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)
|
|
||||||
|
|
||||||
def _do_send(self, amount, message, outputs, rbf):
|
def _do_send(self, amount, message, outputs):
|
||||||
# make unsigned transaction
|
# make unsigned transaction
|
||||||
config = self.app.electrum_config
|
config = self.app.electrum_config
|
||||||
coins = self.app.wallet.get_spendable_coins(None, config)
|
coins = self.app.wallet.get_spendable_coins(None, config)
|
||||||
|
@ -283,8 +278,6 @@ class SendScreen(CScreen):
|
||||||
traceback.print_exc(file=sys.stdout)
|
traceback.print_exc(file=sys.stdout)
|
||||||
self.app.show_error(str(e))
|
self.app.show_error(str(e))
|
||||||
return
|
return
|
||||||
if rbf:
|
|
||||||
tx.set_rbf(True)
|
|
||||||
fee = tx.get_fee()
|
fee = tx.get_fee()
|
||||||
msg = [
|
msg = [
|
||||||
_("Amount to be sent") + ": " + self.app.format_amount_and_units(amount),
|
_("Amount to be sent") + ": " + self.app.format_amount_and_units(amount),
|
||||||
|
|
|
@ -163,14 +163,6 @@ class HistoryList(MyTreeWidget):
|
||||||
menu.addAction(_("Edit %s")%column_title, lambda: self.editItem(item, column))
|
menu.addAction(_("Edit %s")%column_title, lambda: self.editItem(item, column))
|
||||||
|
|
||||||
menu.addAction(_("Details"), lambda: self.parent.show_transaction(tx))
|
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:
|
if pr_key:
|
||||||
menu.addAction(QIcon(":icons/seal"), _("View invoice"), lambda: self.parent.show_invoice(pr_key))
|
menu.addAction(QIcon(":icons/seal"), _("View invoice"), lambda: self.parent.show_invoice(pr_key))
|
||||||
if tx_URL:
|
if tx_URL:
|
||||||
|
|
|
@ -1081,17 +1081,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
self.fee_e.editingFinished.connect(self.update_fee)
|
self.fee_e.editingFinished.connect(self.update_fee)
|
||||||
self.connect_fields(self, self.amount_e, self.fiat_send_e, self.fee_e)
|
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('<p>' + ' '.join(msg) + '</p>')
|
|
||||||
self.rbf_checkbox.setVisible(False)
|
|
||||||
|
|
||||||
grid.addWidget(self.fee_e_label, 5, 0)
|
grid.addWidget(self.fee_e_label, 5, 0)
|
||||||
grid.addWidget(self.fee_slider, 5, 1)
|
grid.addWidget(self.fee_slider, 5, 1)
|
||||||
grid.addWidget(self.fee_e, 5, 2)
|
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 = EnterButton(_("Preview"), self.do_preview)
|
||||||
self.preview_button.setToolTip(_('Display the details of your transactions before signing it.'))
|
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:
|
if fee is None:
|
||||||
return
|
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):
|
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))
|
amount = tx.output_value() if self.is_max else sum(map(lambda x:x[2], outputs))
|
||||||
fee = tx.get_fee()
|
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:
|
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"))
|
self.show_error(_("This transaction requires a higher fee, or it will not be propagated by the network"))
|
||||||
return
|
return
|
||||||
|
@ -1567,7 +1541,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
e.setText('')
|
e.setText('')
|
||||||
e.setFrozen(False)
|
e.setFrozen(False)
|
||||||
self.set_pay_from([])
|
self.set_pay_from([])
|
||||||
self.rbf_checkbox.setChecked(False)
|
|
||||||
self.tx_external_keypairs = {}
|
self.tx_external_keypairs = {}
|
||||||
self.update_status()
|
self.update_status()
|
||||||
run_hook('do_clear', self)
|
run_hook('do_clear', self)
|
||||||
|
@ -2506,16 +2479,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
feebox_cb.stateChanged.connect(on_feebox)
|
feebox_cb.stateChanged.connect(on_feebox)
|
||||||
fee_widgets.append((feebox_cb, None))
|
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)
|
self.fee_unit = self.config.get('fee_unit', 0)
|
||||||
fee_unit_label = HelpLabel(_('Fee Unit') + ':', '')
|
fee_unit_label = HelpLabel(_('Fee Unit') + ':', '')
|
||||||
fee_unit_combo = QComboBox()
|
fee_unit_combo = QComboBox()
|
||||||
|
@ -2904,93 +2867,3 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||||
grid.setRowStretch(len(plugins.descriptions.values()), 1)
|
grid.setRowStretch(len(plugins.descriptions.values()), 1)
|
||||||
vbox.addLayout(Buttons(CloseButton(d)))
|
vbox.addLayout(Buttons(CloseButton(d)))
|
||||||
d.exec_()
|
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)
|
|
||||||
|
|
|
@ -175,7 +175,7 @@ class TxDialog(QDialog, MessageBoxMixin):
|
||||||
desc = self.desc
|
desc = self.desc
|
||||||
base_unit = self.main_window.base_unit()
|
base_unit = self.main_window.base_unit()
|
||||||
format_amount = self.main_window.format_amount
|
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()
|
size = self.tx.estimated_size()
|
||||||
self.broadcast_button.setEnabled(can_broadcast)
|
self.broadcast_button.setEnabled(can_broadcast)
|
||||||
can_sign = not self.tx.is_complete() and \
|
can_sign = not self.tx.is_complete() and \
|
||||||
|
|
|
@ -410,7 +410,7 @@ class Commands:
|
||||||
message = util.to_bytes(message)
|
message = util.to_bytes(message)
|
||||||
return bitcoin.verify_message(address, sig, 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
|
self.nocheck = nocheck
|
||||||
change_addr = self._resolver(change_addr)
|
change_addr = self._resolver(change_addr)
|
||||||
domain = None if domain is None else map(self._resolver, domain)
|
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)
|
tx = self.wallet.make_unsigned_transaction(coins, final_outputs, self.config, fee, change_addr)
|
||||||
if locktime != None:
|
if locktime != None:
|
||||||
tx.locktime = locktime
|
tx.locktime = locktime
|
||||||
if rbf:
|
|
||||||
tx.set_rbf(True)
|
|
||||||
if not unsigned:
|
if not unsigned:
|
||||||
run_hook('sign_tx', self.wallet, tx)
|
run_hook('sign_tx', self.wallet, tx)
|
||||||
self.wallet.sign_transaction(tx, password)
|
self.wallet.sign_transaction(tx, password)
|
||||||
return tx
|
return tx
|
||||||
|
|
||||||
@command('wp')
|
@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. """
|
"""Create a transaction. """
|
||||||
tx_fee = satoshis(fee)
|
tx_fee = satoshis(fee)
|
||||||
domain = from_addr.split(',') if from_addr else None
|
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()
|
return tx.as_dict()
|
||||||
|
|
||||||
@command('wp')
|
@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. """
|
"""Create a multi-output transaction. """
|
||||||
tx_fee = satoshis(fee)
|
tx_fee = satoshis(fee)
|
||||||
domain = from_addr.split(',') if from_addr else None
|
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()
|
return tx.as_dict()
|
||||||
|
|
||||||
@command('w')
|
@command('w')
|
||||||
|
@ -716,7 +714,6 @@ command_options = {
|
||||||
'language': ("-L", "Default language for wordlist"),
|
'language': ("-L", "Default language for wordlist"),
|
||||||
'privkey': (None, "Private key. Set to '?' to get a prompt."),
|
'privkey': (None, "Private key. Set to '?' to get a prompt."),
|
||||||
'unsigned': ("-u", "Do not sign transaction"),
|
'unsigned': ("-u", "Do not sign transaction"),
|
||||||
'rbf': (None, "Replace-by-fee transaction"),
|
|
||||||
'locktime': (None, "Set locktime block number"),
|
'locktime': (None, "Set locktime block number"),
|
||||||
'domain': ("-D", "List of addresses"),
|
'domain': ("-D", "List of addresses"),
|
||||||
'memo': ("-m", "Description of the request"),
|
'memo': ("-m", "Description of the request"),
|
||||||
|
|
|
@ -669,11 +669,6 @@ class Transaction:
|
||||||
s += int_to_hex(txin.get('sequence', 0xffffffff - 1), 4)
|
s += int_to_hex(txin.get('sequence', 0xffffffff - 1), 4)
|
||||||
return s
|
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):
|
def BIP_LI01_sort(self):
|
||||||
# See https://github.com/kristovatlas/rfc/blob/master/bips/bip-li01.mediawiki
|
# See https://github.com/kristovatlas/rfc/blob/master/bips/bip-li01.mediawiki
|
||||||
self._inputs.sort(key = lambda i: (i['prevout_hash'], i['prevout_n']))
|
self._inputs.sort(key = lambda i: (i['prevout_hash'], i['prevout_n']))
|
||||||
|
|
|
@ -142,7 +142,6 @@ def sweep(privkeys, network, config, recipient, fee=None, imax=100):
|
||||||
locktime = network.get_local_height()
|
locktime = network.get_local_height()
|
||||||
|
|
||||||
tx = Transaction.from_io(inputs, outputs, locktime=locktime)
|
tx = Transaction.from_io(inputs, outputs, locktime=locktime)
|
||||||
tx.set_rbf(True)
|
|
||||||
tx.sign(keypairs)
|
tx.sign(keypairs)
|
||||||
return tx
|
return tx
|
||||||
|
|
||||||
|
@ -501,7 +500,6 @@ class Abstract_Wallet(PrintError):
|
||||||
is_relevant, is_mine, v, fee = self.get_wallet_delta(tx)
|
is_relevant, is_mine, v, fee = self.get_wallet_delta(tx)
|
||||||
exp_n = None
|
exp_n = None
|
||||||
can_broadcast = False
|
can_broadcast = False
|
||||||
can_bump = False
|
|
||||||
label = ''
|
label = ''
|
||||||
height = conf = timestamp = None
|
height = conf = timestamp = None
|
||||||
tx_hash = tx.txid()
|
tx_hash = tx.txid()
|
||||||
|
@ -522,7 +520,6 @@ class Abstract_Wallet(PrintError):
|
||||||
size = tx.estimated_size()
|
size = tx.estimated_size()
|
||||||
fee_per_kb = fee * 1000 / size
|
fee_per_kb = fee * 1000 / size
|
||||||
exp_n = self.network.config.reverse_dynfee(fee_per_kb)
|
exp_n = self.network.config.reverse_dynfee(fee_per_kb)
|
||||||
can_bump = is_mine and not tx.is_final()
|
|
||||||
else:
|
else:
|
||||||
status = _("Signed")
|
status = _("Signed")
|
||||||
can_broadcast = self.network is not None
|
can_broadcast = self.network is not None
|
||||||
|
@ -541,7 +538,7 @@ class Abstract_Wallet(PrintError):
|
||||||
else:
|
else:
|
||||||
amount = None
|
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):
|
def get_addr_io(self, address):
|
||||||
h = self.history.get(address, [])
|
h = self.history.get(address, [])
|
||||||
|
@ -1037,61 +1034,6 @@ class Abstract_Wallet(PrintError):
|
||||||
age = tx_age
|
age = tx_age
|
||||||
return age > age_limit
|
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):
|
def add_input_info(self, txin):
|
||||||
address = txin['address']
|
address = txin['address']
|
||||||
if self.is_mine(address):
|
if self.is_mine(address):
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
from electrum.i18n import _
|
|
||||||
|
|
||||||
fullname = _('Two Factor Authentication')
|
|
||||||
description = ''.join([
|
|
||||||
_("This plugin adds two-factor authentication to your wallet."), '<br/>',
|
|
||||||
_("For more information, visit"),
|
|
||||||
" <a href=\"https://api.trustedcoin.com/#/electrum-help\">https://api.trustedcoin.com/#/electrum-help</a>"
|
|
||||||
])
|
|
||||||
requires_wallet_type = ['2fa']
|
|
||||||
registers_wallet_type = '2fa'
|
|
||||||
available_for = ['qt', 'cmdline']
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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.') + '<br/>'\
|
|
||||||
+ _("For more information, visit") + " <a href=\"https://api.trustedcoin.com/#/electrum-help\">https://api.trustedcoin.com/#/electrum-help</a>"
|
|
||||||
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.') + '<br/>'
|
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
|
@ -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'
|
|
Loading…
Reference in New Issue