Compare commits
11 Commits
master
...
trustedcoi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5028cb4d49 | ||
|
|
1713e37e09 | ||
|
|
cad107d235 | ||
|
|
441379e885 | ||
|
|
6c51fc9360 | ||
|
|
0f7fd0b4a8 | ||
|
|
9dd49d6734 | ||
|
|
bba862f795 | ||
|
|
58844f6c00 | ||
|
|
a1efdc2730 | ||
|
|
477539206d |
@ -484,13 +484,17 @@ class ElectrumWindow(App):
|
||||
else:
|
||||
return ''
|
||||
|
||||
def on_wizard_complete(self, instance, wallet):
|
||||
if wallet:
|
||||
def on_wizard_complete(self, wizard, wallet):
|
||||
if wallet: # wizard returned a wallet
|
||||
wallet.start_threads(self.daemon.network)
|
||||
self.daemon.add_wallet(wallet)
|
||||
self.load_wallet(wallet)
|
||||
elif not self.wallet:
|
||||
# wizard did not return a wallet; and there is no wallet open atm
|
||||
# try to open last saved wallet (potentially start wizard again)
|
||||
self.load_wallet_by_name(self.electrum_config.get_wallet_path(), ask_if_wizard=True)
|
||||
|
||||
def load_wallet_by_name(self, path):
|
||||
def load_wallet_by_name(self, path, ask_if_wizard=False):
|
||||
if not path:
|
||||
return
|
||||
if self.wallet and self.wallet.storage.path == path:
|
||||
@ -502,12 +506,28 @@ class ElectrumWindow(App):
|
||||
else:
|
||||
self.load_wallet(wallet)
|
||||
else:
|
||||
Logger.debug('Electrum: Wallet not found. Launching install wizard')
|
||||
storage = WalletStorage(path, manual_upgrades=True)
|
||||
wizard = Factory.InstallWizard(self.electrum_config, storage)
|
||||
wizard.bind(on_wizard_complete=self.on_wizard_complete)
|
||||
action = wizard.storage.get_action()
|
||||
wizard.run(action)
|
||||
Logger.debug('Electrum: Wallet not found or action needed. Launching install wizard')
|
||||
|
||||
def launch_wizard():
|
||||
storage = WalletStorage(path, manual_upgrades=True)
|
||||
wizard = Factory.InstallWizard(self.electrum_config, self.plugins, storage)
|
||||
wizard.bind(on_wizard_complete=self.on_wizard_complete)
|
||||
action = wizard.storage.get_action()
|
||||
wizard.run(action)
|
||||
if not ask_if_wizard:
|
||||
launch_wizard()
|
||||
else:
|
||||
from .uix.dialogs.question import Question
|
||||
|
||||
def handle_answer(b: bool):
|
||||
if b:
|
||||
launch_wizard()
|
||||
else:
|
||||
try: os.unlink(path)
|
||||
except FileNotFoundError: pass
|
||||
self.stop()
|
||||
d = Question(_('Do you want to launch the wizard again?'), handle_answer)
|
||||
d.open()
|
||||
|
||||
def on_stop(self):
|
||||
Logger.info('on_stop')
|
||||
@ -661,6 +681,8 @@ class ElectrumWindow(App):
|
||||
self.fiat_balance = self.fx.format_amount(c+u+x) + ' [size=22dp]%s[/size]'% self.fx.ccy
|
||||
|
||||
def get_max_amount(self):
|
||||
if run_hook('abort_send', self):
|
||||
return ''
|
||||
inputs = self.wallet.get_spendable_coins(None, self.electrum_config)
|
||||
if not inputs:
|
||||
return ''
|
||||
@ -668,7 +690,9 @@ class ElectrumWindow(App):
|
||||
outputs = [(TYPE_ADDRESS, addr, '!')]
|
||||
tx = self.wallet.make_unsigned_transaction(inputs, outputs, self.electrum_config)
|
||||
amount = tx.output_value()
|
||||
return format_satoshis_plain(amount, self.decimal_point())
|
||||
__, x_fee_amount = run_hook('get_tx_extra_fee', self.wallet, tx) or (None, 0)
|
||||
amount_after_all_fees = amount - x_fee_amount
|
||||
return format_satoshis_plain(amount_after_all_fees, self.decimal_point())
|
||||
|
||||
def format_amount(self, x, is_diff=False, whitespaces=False):
|
||||
return format_satoshis(x, 0, self.decimal_point(), is_diff=is_diff, whitespaces=whitespaces)
|
||||
@ -806,6 +830,7 @@ class ElectrumWindow(App):
|
||||
except InvalidPassword:
|
||||
Clock.schedule_once(lambda dt: on_failure(_("Invalid PIN")))
|
||||
return
|
||||
on_success = run_hook('tc_sign_wrapper', self.wallet, tx, on_success, on_failure) or on_success
|
||||
Clock.schedule_once(lambda dt: on_success(tx))
|
||||
|
||||
def _broadcast_thread(self, tx, on_complete):
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
|
||||
from functools import partial
|
||||
import threading
|
||||
import os
|
||||
|
||||
from kivy.app import App
|
||||
from kivy.clock import Clock
|
||||
@ -15,6 +16,7 @@ from kivy.clock import Clock
|
||||
from kivy.utils import platform
|
||||
|
||||
from electrum.base_wizard import BaseWizard
|
||||
from electrum.util import is_valid_email
|
||||
|
||||
|
||||
from . import EventsDialog
|
||||
@ -24,6 +26,7 @@ from .password_dialog import PasswordDialog
|
||||
# global Variables
|
||||
is_test = (platform == "linux")
|
||||
test_seed = "time taxi field recycle tiny license olive virus report rare steel portion achieve"
|
||||
test_seed = "grape impose jazz bind spatial mind jelly tourist tank today holiday stomach"
|
||||
test_xpub = "xpub661MyMwAqRbcEbvVtRRSjqxVnaWVUMewVzMiURAKyYratih4TtBpMypzzefmv8zUNebmNVzB3PojdC5sV2P9bDgMoo9B3SARw1MXUUfU1GL"
|
||||
|
||||
Builder.load_string('''
|
||||
@ -63,10 +66,6 @@ Builder.load_string('''
|
||||
#auto_dismiss: False
|
||||
size_hint: None, None
|
||||
canvas.before:
|
||||
Color:
|
||||
rgba: 0, 0, 0, .9
|
||||
Rectangle:
|
||||
size: Window.size
|
||||
Color:
|
||||
rgba: .239, .588, .882, 1
|
||||
Rectangle:
|
||||
@ -171,6 +170,112 @@ Builder.load_string('''
|
||||
spacing: '14dp'
|
||||
size_hint: 1, None
|
||||
|
||||
<WizardConfirmDialog>
|
||||
message : ''
|
||||
Widget:
|
||||
size_hint: 1, 1
|
||||
Label:
|
||||
color: root.text_color
|
||||
size_hint: 1, None
|
||||
text_size: self.width, None
|
||||
height: self.texture_size[1]
|
||||
text: root.message
|
||||
Widget
|
||||
size_hint: 1, 1
|
||||
|
||||
<WizardTOSDialog>
|
||||
message : ''
|
||||
size_hint: 1, 1
|
||||
ScrollView:
|
||||
size_hint: 1, 1
|
||||
TextInput:
|
||||
color: root.text_color
|
||||
size_hint: 1, None
|
||||
text_size: self.width, None
|
||||
height: self.minimum_height
|
||||
text: root.message
|
||||
disabled: True
|
||||
|
||||
<WizardEmailDialog>
|
||||
Label:
|
||||
color: root.text_color
|
||||
size_hint: 1, None
|
||||
text_size: self.width, None
|
||||
height: self.texture_size[1]
|
||||
text: 'Please enter your email address'
|
||||
WizardTextInput:
|
||||
id: email
|
||||
on_text: Clock.schedule_once(root.on_text)
|
||||
multiline: False
|
||||
on_text_validate: Clock.schedule_once(root.on_enter)
|
||||
|
||||
<WizardKnownOTPDialog>
|
||||
message : ''
|
||||
message2: ''
|
||||
Widget:
|
||||
size_hint: 1, 1
|
||||
Label:
|
||||
color: root.text_color
|
||||
size_hint: 1, None
|
||||
text_size: self.width, None
|
||||
height: self.texture_size[1]
|
||||
text: root.message
|
||||
Widget
|
||||
size_hint: 1, 1
|
||||
WizardTextInput:
|
||||
id: otp
|
||||
on_text: Clock.schedule_once(root.on_text)
|
||||
multiline: False
|
||||
on_text_validate: Clock.schedule_once(root.on_enter)
|
||||
Widget
|
||||
size_hint: 1, 1
|
||||
Label:
|
||||
color: root.text_color
|
||||
size_hint: 1, None
|
||||
text_size: self.width, None
|
||||
height: self.texture_size[1]
|
||||
text: root.message2
|
||||
Widget
|
||||
size_hint: 1, 1
|
||||
height: '48sp'
|
||||
BoxLayout:
|
||||
orientation: 'horizontal'
|
||||
WizardButton:
|
||||
id: cb
|
||||
text: _('Request new secret')
|
||||
on_release: root.request_new_secret()
|
||||
size_hint: 1, None
|
||||
WizardButton:
|
||||
id: abort
|
||||
text: _('Abort creation')
|
||||
on_release: root.abort_wallet_creation()
|
||||
size_hint: 1, None
|
||||
|
||||
|
||||
<WizardNewOTPDialog>
|
||||
message : ''
|
||||
message2 : ''
|
||||
Label:
|
||||
color: root.text_color
|
||||
size_hint: 1, None
|
||||
text_size: self.width, None
|
||||
height: self.texture_size[1]
|
||||
text: root.message
|
||||
QRCodeWidget:
|
||||
id: qr
|
||||
size_hint: 1, 1
|
||||
Label:
|
||||
color: root.text_color
|
||||
size_hint: 1, None
|
||||
text_size: self.width, None
|
||||
height: self.texture_size[1]
|
||||
text: root.message2
|
||||
WizardTextInput:
|
||||
id: otp
|
||||
on_text: Clock.schedule_once(root.on_text)
|
||||
multiline: False
|
||||
on_text_validate: Clock.schedule_once(root.on_enter)
|
||||
|
||||
<MButton@Button>:
|
||||
size_hint: 1, None
|
||||
height: '33dp'
|
||||
@ -485,6 +590,101 @@ class WizardMultisigDialog(WizardDialog):
|
||||
n = self.ids.n.value
|
||||
return m, n
|
||||
|
||||
|
||||
class WizardOTPDialogBase(WizardDialog):
|
||||
|
||||
def get_otp(self):
|
||||
otp = self.ids.otp.text
|
||||
if len(otp) != 6:
|
||||
return
|
||||
try:
|
||||
return int(otp)
|
||||
except:
|
||||
return
|
||||
|
||||
def on_text(self, dt):
|
||||
self.ids.next.disabled = self.get_otp() is None
|
||||
|
||||
def on_enter(self, dt):
|
||||
# press next
|
||||
next = self.ids.next
|
||||
if not next.disabled:
|
||||
next.dispatch('on_release')
|
||||
|
||||
|
||||
class WizardKnownOTPDialog(WizardOTPDialogBase):
|
||||
|
||||
def __init__(self, wizard, **kwargs):
|
||||
WizardOTPDialogBase.__init__(self, wizard, **kwargs)
|
||||
self.message = _("This wallet is already registered with TrustedCoin. To finalize wallet creation, please enter your Google Authenticator Code.")
|
||||
self.message2 =_("If you have lost your Google Authenticator account, you can request a new secret. You will need to retype your seed.")
|
||||
self.request_new = False
|
||||
|
||||
def get_params(self, button):
|
||||
return (self.get_otp(), self.request_new)
|
||||
|
||||
def request_new_secret(self):
|
||||
self.request_new = True
|
||||
self.on_release(True)
|
||||
|
||||
def abort_wallet_creation(self):
|
||||
self._on_release = True
|
||||
os.unlink(self.wizard.storage.path)
|
||||
self.wizard.terminate()
|
||||
self.dismiss()
|
||||
|
||||
|
||||
class WizardNewOTPDialog(WizardOTPDialogBase):
|
||||
|
||||
def __init__(self, wizard, **kwargs):
|
||||
WizardOTPDialogBase.__init__(self, wizard, **kwargs)
|
||||
otp_secret = kwargs['otp_secret']
|
||||
uri = "otpauth://totp/%s?secret=%s"%('trustedcoin.com', otp_secret)
|
||||
self.message = "Please scan the following QR code in Google Authenticator. You may also use the secret key: %s"%otp_secret
|
||||
self.message2 = _('Then, enter your Google Authenticator code:')
|
||||
self.ids.qr.set_data(uri)
|
||||
|
||||
def get_params(self, button):
|
||||
return (self.get_otp(), False)
|
||||
|
||||
class WizardTOSDialog(WizardDialog):
|
||||
|
||||
def __init__(self, wizard, **kwargs):
|
||||
WizardDialog.__init__(self, wizard, **kwargs)
|
||||
self.ids.next.text = 'Accept'
|
||||
self.ids.next.disabled = False
|
||||
self.message = kwargs['tos']
|
||||
self.message2 = _('Enter your email address:')
|
||||
|
||||
class WizardEmailDialog(WizardDialog):
|
||||
|
||||
def get_params(self, button):
|
||||
return (self.ids.email.text,)
|
||||
|
||||
def on_text(self, dt):
|
||||
self.ids.next.disabled = not is_valid_email(self.ids.email.text)
|
||||
|
||||
def on_enter(self, dt):
|
||||
# press next
|
||||
next = self.ids.next
|
||||
if not next.disabled:
|
||||
next.dispatch('on_release')
|
||||
|
||||
class WizardConfirmDialog(WizardDialog):
|
||||
|
||||
def __init__(self, wizard, **kwargs):
|
||||
super(WizardConfirmDialog, self).__init__(wizard, **kwargs)
|
||||
self.message = kwargs.get('message', '')
|
||||
self.value = 'ok'
|
||||
|
||||
def on_parent(self, instance, value):
|
||||
if value:
|
||||
app = App.get_running_app()
|
||||
self._back = _back = partial(app.dispatch, 'on_back')
|
||||
|
||||
def get_params(self, button):
|
||||
return (True,)
|
||||
|
||||
class WizardChoiceDialog(WizardDialog):
|
||||
|
||||
def __init__(self, wizard, **kwargs):
|
||||
@ -789,6 +989,21 @@ class InstallWizard(BaseWizard, Widget):
|
||||
def restore_seed_dialog(self, **kwargs):
|
||||
RestoreSeedDialog(self, **kwargs).open()
|
||||
|
||||
def confirm_dialog(self, **kwargs):
|
||||
WizardConfirmDialog(self, **kwargs).open()
|
||||
|
||||
def tos_dialog(self, **kwargs):
|
||||
WizardTOSDialog(self, **kwargs).open()
|
||||
|
||||
def email_dialog(self, **kwargs):
|
||||
WizardEmailDialog(self, **kwargs).open()
|
||||
|
||||
def otp_dialog(self, **kwargs):
|
||||
if kwargs['otp_secret']:
|
||||
WizardNewOTPDialog(self, **kwargs).open()
|
||||
else:
|
||||
WizardKnownOTPDialog(self, **kwargs).open()
|
||||
|
||||
def add_xpub_dialog(self, **kwargs):
|
||||
kwargs['message'] += ' ' + _('Use the camera button to scan a QR code.')
|
||||
AddXpubDialog(self, **kwargs).open()
|
||||
@ -800,6 +1015,8 @@ class InstallWizard(BaseWizard, Widget):
|
||||
|
||||
def show_xpub_dialog(self, **kwargs): ShowXpubDialog(self, **kwargs).open()
|
||||
|
||||
def show_message(self, msg): self.show_error(msg)
|
||||
|
||||
def show_error(self, msg):
|
||||
app = App.get_running_app()
|
||||
Clock.schedule_once(lambda dt: app.show_error(msg))
|
||||
|
||||
@ -21,6 +21,7 @@ from electrum.util import profiler, parse_URI, format_time, InvalidPassword, Not
|
||||
from electrum import bitcoin
|
||||
from electrum.util import timestamp_to_datetime
|
||||
from electrum.paymentrequest import PR_UNPAID, PR_PAID, PR_UNKNOWN, PR_EXPIRED
|
||||
from electrum.plugins import run_hook
|
||||
|
||||
from .context_menu import ContextMenu
|
||||
|
||||
@ -283,6 +284,11 @@ class SendScreen(CScreen):
|
||||
_("Amount to be sent") + ": " + self.app.format_amount_and_units(amount),
|
||||
_("Mining fee") + ": " + self.app.format_amount_and_units(fee),
|
||||
]
|
||||
x_fee = run_hook('get_tx_extra_fee', self.app.wallet, tx)
|
||||
if x_fee:
|
||||
x_fee_address, x_fee_amount = x_fee
|
||||
msg.append(_("Additional fees") + ": " + self.app.format_amount_and_units(x_fee_amount))
|
||||
|
||||
if fee >= config.get('confirm_fee', 100000):
|
||||
msg.append(_('Warning')+ ': ' + _("The fee for this transaction seems unusually high."))
|
||||
msg.append(_("Enter your PIN code to proceed"))
|
||||
|
||||
@ -4,6 +4,7 @@ Popup:
|
||||
unconfirmed: 0
|
||||
unmatured: 0
|
||||
watching_only: app.wallet.is_watching_only()
|
||||
has_seed: app.wallet.has_seed()
|
||||
on_parent:
|
||||
self.confirmed, self.unconfirmed, self.unmatured = app.wallet.get_balance()
|
||||
BoxLayout:
|
||||
@ -61,8 +62,8 @@ Popup:
|
||||
Button:
|
||||
size_hint: 0.5, None
|
||||
height: '48dp'
|
||||
text: '' if root.watching_only else (_('Hide seed') if seed_label.text else _('Show seed'))
|
||||
disabled: root.watching_only
|
||||
text: '' if not root.has_seed else (_('Hide seed') if seed_label.text else _('Show seed'))
|
||||
disabled: not root.has_seed
|
||||
on_release:
|
||||
setattr(seed_label, 'text', '') if seed_label.text else app.show_seed(seed_label)
|
||||
Button:
|
||||
|
||||
@ -92,13 +92,12 @@ class InstallWizard(QDialog, MessageBoxMixin, BaseWizard):
|
||||
synchronized_signal = pyqtSignal(str)
|
||||
|
||||
def __init__(self, config, app, plugins, storage):
|
||||
BaseWizard.__init__(self, config, storage)
|
||||
BaseWizard.__init__(self, config, plugins, storage)
|
||||
QDialog.__init__(self, None)
|
||||
self.setWindowTitle('Electrum - ' + _('Install Wizard'))
|
||||
self.app = app
|
||||
self.config = config
|
||||
# Set for base base class
|
||||
self.plugins = plugins
|
||||
self.language_for_seed = config.get('language')
|
||||
self.setMinimumSize(600, 400)
|
||||
self.accept_signal.connect(self.accept)
|
||||
|
||||
@ -1267,6 +1267,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||
return w
|
||||
|
||||
def spend_max(self):
|
||||
if run_hook('abort_send', self):
|
||||
return
|
||||
self.is_max = True
|
||||
self.do_update_fee()
|
||||
|
||||
@ -1364,7 +1366,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||
|
||||
if self.is_max:
|
||||
amount = tx.output_value()
|
||||
self.amount_e.setAmount(amount)
|
||||
__, x_fee_amount = run_hook('get_tx_extra_fee', self.wallet, tx) or (None, 0)
|
||||
amount_after_all_fees = amount - x_fee_amount
|
||||
self.amount_e.setAmount(amount_after_all_fees)
|
||||
|
||||
def from_list_delete(self, item):
|
||||
i = self.from_list.indexOfTopLevelItem(item)
|
||||
@ -1577,20 +1581,19 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
|
||||
'''Sign the transaction in a separate thread. When done, calls
|
||||
the callback with a success code of True or False.
|
||||
'''
|
||||
|
||||
def on_signed(result):
|
||||
def on_success(result):
|
||||
callback(True)
|
||||
def on_failed(exc_info):
|
||||
def on_failure(exc_info):
|
||||
self.on_error(exc_info)
|
||||
callback(False)
|
||||
|
||||
on_success = run_hook('tc_sign_wrapper', self.wallet, tx, on_success, on_failure) or on_success
|
||||
if self.tx_external_keypairs:
|
||||
# can sign directly
|
||||
task = partial(Transaction.sign, tx, self.tx_external_keypairs)
|
||||
else:
|
||||
task = partial(self.wallet.sign_transaction, tx, password)
|
||||
WaitingDialog(self, _('Signing transaction...'), task,
|
||||
on_signed, on_failed)
|
||||
msg = _('Signing transaction...')
|
||||
WaitingDialog(self, msg, task, on_success, on_failure)
|
||||
|
||||
def broadcast_transaction(self, tx, tx_desc):
|
||||
|
||||
|
||||
@ -48,9 +48,10 @@ class GoBack(Exception): pass
|
||||
|
||||
class BaseWizard(object):
|
||||
|
||||
def __init__(self, config, storage):
|
||||
def __init__(self, config, plugins, storage):
|
||||
super(BaseWizard, self).__init__()
|
||||
self.config = config
|
||||
self.plugins = plugins
|
||||
self.storage = storage
|
||||
self.wallet = None
|
||||
self.stack = []
|
||||
@ -59,6 +60,9 @@ class BaseWizard(object):
|
||||
self.is_kivy = config.get('gui') == 'kivy'
|
||||
self.seed_type = None
|
||||
|
||||
def set_icon(self, icon):
|
||||
pass
|
||||
|
||||
def run(self, *args):
|
||||
action = args[0]
|
||||
args = args[1:]
|
||||
@ -369,12 +373,8 @@ class BaseWizard(object):
|
||||
elif self.seed_type == 'old':
|
||||
self.run('create_keystore', seed, '')
|
||||
elif self.seed_type == '2fa':
|
||||
if self.is_kivy:
|
||||
self.show_error(_('2FA seeds are not supported in this version'))
|
||||
self.run('restore_from_seed')
|
||||
else:
|
||||
self.load_2fa()
|
||||
self.run('on_restore_seed', seed, is_ext)
|
||||
self.load_2fa()
|
||||
self.run('on_restore_seed', seed, is_ext)
|
||||
else:
|
||||
raise Exception('Unknown seed type', self.seed_type)
|
||||
|
||||
|
||||
@ -446,6 +446,10 @@ def user_dir():
|
||||
#raise Exception("No home directory found in environment variables.")
|
||||
return
|
||||
|
||||
def is_valid_email(s):
|
||||
regexp = r"[^@]+@[^@]+\.[^@]+"
|
||||
return re.match(regexp, s) is not None
|
||||
|
||||
|
||||
def format_satoshis_plain(x, decimal_point = 8):
|
||||
"""Display a satoshi amount scaled. Always uses a '.' as a decimal
|
||||
|
||||
@ -1513,6 +1513,7 @@ class Abstract_Wallet(PrintError):
|
||||
k.sign_transaction(tx, password)
|
||||
except UserCancelled:
|
||||
continue
|
||||
return tx
|
||||
|
||||
def get_unused_addresses(self):
|
||||
# fixme: use slots from expired requests
|
||||
|
||||
@ -8,4 +8,4 @@ description = ''.join([
|
||||
])
|
||||
requires_wallet_type = ['2fa']
|
||||
registers_wallet_type = '2fa'
|
||||
available_for = ['qt', 'cmdline']
|
||||
available_for = ['qt', 'cmdline', 'kivy']
|
||||
|
||||
110
plugins/trustedcoin/kivy.py
Normal file
110
plugins/trustedcoin/kivy.py
Normal file
@ -0,0 +1,110 @@
|
||||
#!/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 kivy.clock import Clock
|
||||
|
||||
from electrum.i18n import _
|
||||
from electrum.plugins import hook
|
||||
from .trustedcoin import TrustedCoinPlugin, server, KIVY_DISCLAIMER, TrustedCoinException, ErrorConnectingServer
|
||||
|
||||
|
||||
|
||||
class Plugin(TrustedCoinPlugin):
|
||||
|
||||
disclaimer_msg = KIVY_DISCLAIMER
|
||||
|
||||
def __init__(self, parent, config, name):
|
||||
super().__init__(parent, config, name)
|
||||
|
||||
@hook
|
||||
def load_wallet(self, wallet, window):
|
||||
if not isinstance(wallet, self.wallet_class):
|
||||
return
|
||||
self.start_request_thread(wallet)
|
||||
|
||||
def go_online_dialog(self, wizard):
|
||||
# we skip this step on android
|
||||
wizard.run('accept_terms_of_use')
|
||||
|
||||
def prompt_user_for_otp(self, wallet, tx, on_success, on_failure):
|
||||
from electrum_gui.kivy.uix.dialogs.label_dialog import LabelDialog
|
||||
msg = _('Please enter your Google Authenticator code')
|
||||
d = LabelDialog(msg, '', lambda otp: self.on_otp(wallet, tx, otp, on_success, on_failure))
|
||||
d.open()
|
||||
|
||||
def on_otp(self, wallet, tx, otp, on_success, on_failure):
|
||||
try:
|
||||
wallet.on_otp(tx, otp)
|
||||
except TrustedCoinException as e:
|
||||
if e.status_code == 400: # invalid OTP
|
||||
Clock.schedule_once(lambda dt: on_failure(_('Invalid one-time password.')))
|
||||
else:
|
||||
Clock.schedule_once(lambda dt, bound_e=e: on_failure(_('Error') + ':\n' + str(bound_e)))
|
||||
except Exception as e:
|
||||
Clock.schedule_once(lambda dt, bound_e=e: on_failure(_('Error') + ':\n' + str(bound_e)))
|
||||
else:
|
||||
on_success(tx)
|
||||
|
||||
def accept_terms_of_use(self, wizard):
|
||||
def handle_error(msg, e):
|
||||
wizard.show_error(msg + ':\n' + str(e))
|
||||
wizard.terminate()
|
||||
try:
|
||||
tos = server.get_terms_of_service()
|
||||
except ErrorConnectingServer as e:
|
||||
Clock.schedule_once(lambda dt, bound_e=e: handle_error(_('Error connecting to server'), bound_e))
|
||||
except Exception as e:
|
||||
Clock.schedule_once(lambda dt, bound_e=e: handle_error(_('Error'), bound_e))
|
||||
else:
|
||||
f = lambda x: self.read_email(wizard)
|
||||
wizard.tos_dialog(tos=tos, run_next=f)
|
||||
|
||||
def read_email(self, wizard):
|
||||
f = lambda x: self.create_remote_key(x, wizard)
|
||||
wizard.email_dialog(run_next=f)
|
||||
|
||||
def request_otp_dialog(self, wizard, short_id, otp_secret, xpub3):
|
||||
f = lambda otp, reset: self.check_otp(wizard, short_id, otp_secret, xpub3, otp, reset)
|
||||
wizard.otp_dialog(otp_secret=otp_secret, run_next=f)
|
||||
|
||||
@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:
|
||||
self.start_request_thread(wallet)
|
||||
Clock.schedule_once(
|
||||
lambda dt: window.show_error(_('Requesting account info from TrustedCoin server...') + '\n' +
|
||||
_('Please try again.')))
|
||||
return True
|
||||
return False
|
||||
@ -38,7 +38,7 @@ 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 electrum.util import PrintError
|
||||
from electrum.util import PrintError, is_valid_email
|
||||
from .trustedcoin import TrustedCoinPlugin, server
|
||||
|
||||
|
||||
@ -48,36 +48,28 @@ class TOS(QTextEdit):
|
||||
|
||||
|
||||
class HandlerTwoFactor(QObject, PrintError):
|
||||
otp_start_signal = pyqtSignal(object, object)
|
||||
|
||||
def __init__(self, plugin, window):
|
||||
super().__init__()
|
||||
self.plugin = plugin
|
||||
self.window = window
|
||||
self.otp_start_signal.connect(self._prompt_user_for_otp)
|
||||
self.otp_done = threading.Event()
|
||||
|
||||
def prompt_user_for_otp(self, wallet, tx):
|
||||
self.otp_done.clear()
|
||||
self.otp_start_signal.emit(wallet, tx)
|
||||
self.otp_done.wait()
|
||||
|
||||
def _prompt_user_for_otp(self, wallet, tx):
|
||||
def prompt_user_for_otp(self, wallet, tx, on_success, on_failure):
|
||||
if not isinstance(wallet, self.plugin.wallet_class):
|
||||
return
|
||||
if wallet.can_sign_without_server():
|
||||
return
|
||||
if not wallet.keystores['x3/'].get_tx_derivations(tx):
|
||||
self.print_error("twofactor: xpub3 not needed")
|
||||
return
|
||||
window = self.window.top_level_window()
|
||||
auth_code = self.plugin.auth_dialog(window)
|
||||
try:
|
||||
window = self.window.top_level_window()
|
||||
if not isinstance(wallet, self.plugin.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.plugin.auth_dialog(window)
|
||||
else:
|
||||
self.print_error("twofactor: xpub3 not needed")
|
||||
wallet.auth_code = auth_code
|
||||
finally:
|
||||
self.otp_done.set()
|
||||
|
||||
wallet.on_otp(tx, auth_code)
|
||||
except:
|
||||
on_failure(sys.exc_info())
|
||||
return
|
||||
on_success(tx)
|
||||
|
||||
class Plugin(TrustedCoinPlugin):
|
||||
|
||||
@ -123,8 +115,8 @@ class Plugin(TrustedCoinPlugin):
|
||||
return
|
||||
return pw.get_amount()
|
||||
|
||||
def prompt_user_for_otp(self, wallet, tx):
|
||||
wallet.handler_2fa.prompt_user_for_otp(wallet, tx)
|
||||
def prompt_user_for_otp(self, wallet, tx, on_success, on_failure):
|
||||
wallet.handler_2fa.prompt_user_for_otp(wallet, tx, on_success, on_failure)
|
||||
|
||||
def waiting_dialog(self, window, on_finished=None):
|
||||
task = partial(self.request_billing_info, window.wallet)
|
||||
@ -145,7 +137,6 @@ class Plugin(TrustedCoinPlugin):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def settings_dialog(self, window):
|
||||
self.waiting_dialog(window, partial(self.show_settings_dialog, window))
|
||||
|
||||
@ -216,6 +207,20 @@ class Plugin(TrustedCoinPlugin):
|
||||
window.message_e.setFrozen(True)
|
||||
window.amount_e.setFrozen(True)
|
||||
|
||||
def go_online_dialog(self, wizard):
|
||||
msg = [
|
||||
_("Your wallet file is: {}.").format(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 "{}" to close this window, move your '
|
||||
"wallet file to an online computer, and reopen it with "
|
||||
"Electrum.").format(_('Cancel')),
|
||||
_('If you are online, click on "{}" to continue.').format(_('Next'))
|
||||
]
|
||||
msg = '\n\n'.join(msg)
|
||||
wizard.stack = []
|
||||
wizard.confirm_dialog(title='', message=msg, run_next = lambda x: wizard.run('accept_terms_of_use'))
|
||||
|
||||
def accept_terms_of_use(self, window):
|
||||
vbox = QVBoxLayout()
|
||||
vbox.addWidget(QLabel(_("Terms of Service")))
|
||||
@ -256,24 +261,21 @@ class Plugin(TrustedCoinPlugin):
|
||||
window.terminate()
|
||||
|
||||
def set_enabled():
|
||||
valid_email = re.match(regexp, email_e.text()) is not None
|
||||
next_button.setEnabled(tos_received and valid_email)
|
||||
next_button.setEnabled(tos_received and is_valid_email(email_e.text()))
|
||||
|
||||
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())
|
||||
email = str(email_e.text())
|
||||
self.create_remote_key(email, window)
|
||||
|
||||
def request_otp_dialog(self, window, _id, otp_secret):
|
||||
def request_otp_dialog(self, window, short_id, otp_secret, xpub3):
|
||||
vbox = QVBoxLayout()
|
||||
if otp_secret is not None:
|
||||
uri = "otpauth://totp/%s?secret=%s"%('trustedcoin.com', otp_secret)
|
||||
@ -291,7 +293,6 @@ class Plugin(TrustedCoinPlugin):
|
||||
label.setWordWrap(1)
|
||||
vbox.addWidget(label)
|
||||
msg = _('Google Authenticator code:')
|
||||
|
||||
hbox = QHBoxLayout()
|
||||
hbox.addWidget(WWLabel(msg))
|
||||
pw = AmountEdit(None, is_int = True)
|
||||
@ -299,21 +300,14 @@ class Plugin(TrustedCoinPlugin):
|
||||
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()
|
||||
|
||||
|
||||
window.exec_layout(vbox, next_enabled=False, raise_on_cancel=False)
|
||||
self.check_otp(window, short_id, otp_secret, xpub3, pw.get_amount(), cb_lost.isChecked())
|
||||
|
||||
@ -75,6 +75,18 @@ DISCLAIMER = [
|
||||
"To be safe from malware, you may want to do this on an offline "
|
||||
"computer, and move your wallet later to an online computer."),
|
||||
]
|
||||
|
||||
KIVY_DISCLAIMER = [
|
||||
_("Two-factor authentication is a service provided by TrustedCoin. "
|
||||
"To use it, you must have a separate device with Google Authenticator."),
|
||||
_("This service 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. A small fee will be charged on each transaction that uses the "
|
||||
"remote server."),
|
||||
_("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."),
|
||||
]
|
||||
RESTORE_MSG = _("Enter the seed for your 2-factor wallet:")
|
||||
|
||||
class TrustedCoinException(Exception):
|
||||
@ -215,7 +227,6 @@ class Wallet_2fa(Multisig_Wallet):
|
||||
Deterministic_Wallet.__init__(self, storage)
|
||||
self.is_billing = False
|
||||
self.billing_info = None
|
||||
self.auth_code = None
|
||||
|
||||
def can_sign_without_server(self):
|
||||
return not self.keystores['x2/'].is_watching_only()
|
||||
@ -269,25 +280,22 @@ class Wallet_2fa(Multisig_Wallet):
|
||||
tx = mk_tx(outputs)
|
||||
return tx
|
||||
|
||||
def sign_transaction(self, tx, password):
|
||||
Multisig_Wallet.sign_transaction(self, tx, password)
|
||||
if tx.is_complete():
|
||||
return
|
||||
self.plugin.prompt_user_for_otp(self, tx)
|
||||
if not self.auth_code:
|
||||
def on_otp(self, tx, otp):
|
||||
if not otp:
|
||||
self.print_error("sign_transaction: no auth code")
|
||||
return
|
||||
otp = int(otp)
|
||||
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)
|
||||
r = server.sign(short_id, raw_tx, otp)
|
||||
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
|
||||
self.auth_code = None
|
||||
|
||||
|
||||
|
||||
# Utility functions
|
||||
@ -316,6 +324,7 @@ def make_billing_address(wallet, num):
|
||||
|
||||
class TrustedCoinPlugin(BasePlugin):
|
||||
wallet_class = Wallet_2fa
|
||||
disclaimer_msg = DISCLAIMER
|
||||
|
||||
def __init__(self, parent, config, name):
|
||||
BasePlugin.__init__(self, parent, config, name)
|
||||
@ -335,6 +344,21 @@ class TrustedCoinPlugin(BasePlugin):
|
||||
def can_user_disable(self):
|
||||
return False
|
||||
|
||||
@hook
|
||||
def tc_sign_wrapper(self, wallet, tx, on_success, on_failure):
|
||||
if not isinstance(wallet, self.wallet_class):
|
||||
return
|
||||
if tx.is_complete():
|
||||
return
|
||||
if wallet.can_sign_without_server():
|
||||
return
|
||||
if not wallet.keystores['x3/'].get_tx_derivations(tx):
|
||||
self.print_error("twofactor: xpub3 not needed")
|
||||
return
|
||||
def wrapper(tx):
|
||||
self.prompt_user_for_otp(wallet, tx, on_success, on_failure)
|
||||
return wrapper
|
||||
|
||||
@hook
|
||||
def get_tx_extra_fee(self, wallet, tx):
|
||||
if type(wallet) != Wallet_2fa:
|
||||
@ -391,7 +415,7 @@ class TrustedCoinPlugin(BasePlugin):
|
||||
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'))
|
||||
wizard.confirm_dialog(title='Disclaimer', message='\n\n'.join(self.disclaimer_msg), run_next = lambda x: wizard.run('choose_seed'))
|
||||
|
||||
def choose_seed(self, wizard):
|
||||
title = _('Create or restore')
|
||||
@ -450,18 +474,7 @@ class TrustedCoinPlugin(BasePlugin):
|
||||
wizard.storage.put('x1/', k1.dump())
|
||||
wizard.storage.put('x2/', k2.dump())
|
||||
wizard.storage.write()
|
||||
msg = [
|
||||
_("Your wallet file is: {}.").format(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 "{}" to close this window, move your '
|
||||
"wallet file to an online computer, and reopen it with "
|
||||
"Electrum.").format(_('Cancel')),
|
||||
_('If you are online, click on "{}" to continue.').format(_('Next'))
|
||||
]
|
||||
msg = '\n\n'.join(msg)
|
||||
wizard.stack = []
|
||||
wizard.confirm_dialog(title='', message=msg, run_next = lambda x: wizard.run('create_remote_key'))
|
||||
self.go_online_dialog(wizard)
|
||||
|
||||
def restore_wallet(self, wizard):
|
||||
wizard.opt_bip39 = False
|
||||
@ -516,8 +529,8 @@ class TrustedCoinPlugin(BasePlugin):
|
||||
wizard.wallet = Wallet_2fa(storage)
|
||||
wizard.create_addresses()
|
||||
|
||||
def create_remote_key(self, wizard):
|
||||
email = self.accept_terms_of_use(wizard)
|
||||
|
||||
def create_remote_key(self, email, wizard):
|
||||
xpub1 = wizard.storage.get('x1/')['xpub']
|
||||
xpub2 = wizard.storage.get('x2/')['xpub']
|
||||
# Generate third key deterministically.
|
||||
@ -526,8 +539,9 @@ class TrustedCoinPlugin(BasePlugin):
|
||||
# secret must be sent by the server
|
||||
try:
|
||||
r = server.create(xpub1, xpub2, email)
|
||||
except socket.error:
|
||||
except (socket.error, ErrorConnectingServer):
|
||||
wizard.show_message('Server not reachable, aborting')
|
||||
wizard.terminate()
|
||||
return
|
||||
except TrustedCoinException as e:
|
||||
if e.status_code == 409:
|
||||
@ -550,10 +564,9 @@ class TrustedCoinPlugin(BasePlugin):
|
||||
except Exception as e:
|
||||
wizard.show_message(str(e))
|
||||
return
|
||||
self.check_otp(wizard, short_id, otp_secret, xpub3)
|
||||
self.request_otp_dialog(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)
|
||||
def check_otp(self, wizard, short_id, otp_secret, xpub3, otp, reset):
|
||||
if otp:
|
||||
self.do_auth(wizard, short_id, otp, xpub3)
|
||||
elif reset:
|
||||
@ -569,15 +582,24 @@ class TrustedCoinPlugin(BasePlugin):
|
||||
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')
|
||||
except TrustedCoinException as e:
|
||||
if e.status_code == 400: # invalid OTP
|
||||
wizard.show_message(_('Invalid one-time password.'))
|
||||
# ask again for otp
|
||||
self.request_otp_dialog(wizard, short_id, None, xpub3)
|
||||
else:
|
||||
wizard.show_message(str(e))
|
||||
wizard.terminate()
|
||||
except Exception as e:
|
||||
wizard.show_message(str(e))
|
||||
wizard.terminate()
|
||||
else:
|
||||
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)
|
||||
@ -603,7 +625,7 @@ class TrustedCoinPlugin(BasePlugin):
|
||||
if not new_secret:
|
||||
wizard.show_message(_('Request rejected by server'))
|
||||
return
|
||||
self.check_otp(wizard, short_id, new_secret, xpub3)
|
||||
self.request_otp_dialog(wizard, short_id, new_secret, xpub3)
|
||||
|
||||
@hook
|
||||
def get_action(self, storage):
|
||||
@ -614,4 +636,4 @@ class TrustedCoinPlugin(BasePlugin):
|
||||
if not storage.get('x2/'):
|
||||
return self, 'show_disclaimer'
|
||||
if not storage.get('x3/'):
|
||||
return self, 'create_remote_key'
|
||||
return self, 'accept_terms_of_use'
|
||||
|
||||
Loading…
Reference in New Issue
Block a user