URI: 
       tsupport TrustedCoin plugin in the kivy GUI - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
   DIR commit 8d5e666d30aef45dc9992e1e01d9335083cb54e8
   DIR parent 5a75ce74d775ec5ea789c546f879c7212027ba06
  HTML Author: ThomasV <thomasv@electrum.org>
       Date:   Fri, 18 May 2018 18:07:52 +0200
       
       support TrustedCoin plugin in the kivy GUI
       
       Diffstat:
         M gui/kivy/main_window.py             |       3 ++-
         M gui/kivy/uix/dialogs/installwizard… |     189 +++++++++++++++++++++++++++++++
         M gui/qt/installwizard.py             |       3 +--
         M gui/qt/main_window.py               |      11 +++++------
         M lib/base_wizard.py                  |      14 +++++++-------
         M lib/util.py                         |       4 ++++
         M lib/wallet.py                       |       1 +
         M plugins/trustedcoin/__init__.py     |       2 +-
         M plugins/trustedcoin/qt.py           |      82 ++++++++++++++-----------------
         M plugins/trustedcoin/trustedcoin.py  |      74 ++++++++++++++++++-------------
       
       10 files changed, 291 insertions(+), 92 deletions(-)
       ---
   DIR diff --git a/gui/kivy/main_window.py b/gui/kivy/main_window.py
       t@@ -505,7 +505,7 @@ class ElectrumWindow(App):
                else:
                    Logger.debug('Electrum: Wallet not found. Launching install wizard')
                    storage = WalletStorage(path, manual_upgrades=True)
       -            wizard = Factory.InstallWizard(self.electrum_config, storage)
       +            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)
       t@@ -823,6 +823,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):
   DIR diff --git a/gui/kivy/uix/dialogs/installwizard.py b/gui/kivy/uix/dialogs/installwizard.py
       t@@ -15,6 +15,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
       t@@ -24,6 +25,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('''
       t@@ -171,6 +173,95 @@ 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 : ''
       +    ScrollView:
       +        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
       +
       +<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)
       +
       +<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)
       +    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
       +    BoxLayout:
       +        orientation: 'horizontal'
       +        size_hint: 1, 0.2
       +        Widget
       +        CheckBox:
       +            id:cb
       +            on_state: Clock.schedule_once(root.on_cb)
       +
       +<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)
       +
        <MButton@Button>:
            size_hint: 1, None
            height: '33dp'
       t@@ -485,6 +576,87 @@ class WizardMultisigDialog(WizardDialog):
                n = self.ids.n.value
                return m, n
        
       +class WizardKnownOTPDialog(WizardDialog):
       +
       +    def __init__(self, wizard, **kwargs):
       +        WizardDialog.__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, check the box below to request a new secret. You will need to retype your seed.")
       +
       +    def get_otp(self):
       +        otp = self.ids.otp.text
       +        if len(otp) != 6:
       +            return
       +        try:
       +            return int(otp)
       +        except:
       +            return
       +
       +    def get_params(self, button):
       +        return (self.get_otp(), self.ids.cb.active)
       +
       +    def on_cb(self, dt):
       +        self.ids.otp.text = ''
       +        self.ids.next.disabled = not self.ids.cb.active
       +
       +    def on_text(self, dt):
       +        self.ids.next.disabled = self.get_otp() is None
       +
       +class WizardNewOTPDialog(WizardDialog):
       +
       +    def __init__(self, wizard, **kwargs):
       +        WizardDialog.__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_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 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)
       +
       +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):
       t@@ -789,6 +961,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()
       t@@ -800,6 +987,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))
   DIR diff --git a/gui/qt/installwizard.py b/gui/qt/installwizard.py
       t@@ -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)
   DIR diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py
       t@@ -1577,20 +1577,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):
        
   DIR diff --git a/lib/base_wizard.py b/lib/base_wizard.py
       t@@ -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 = []
       t@@ -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:]
       t@@ -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)
        
   DIR diff --git a/lib/util.py b/lib/util.py
       t@@ -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
   DIR diff --git a/lib/wallet.py b/lib/wallet.py
       t@@ -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
   DIR diff --git a/plugins/trustedcoin/__init__.py b/plugins/trustedcoin/__init__.py
       t@@ -8,4 +8,4 @@ description = ''.join([
        ])
        requires_wallet_type = ['2fa']
        registers_wallet_type = '2fa'
       -available_for = ['qt', 'cmdline']
       +available_for = ['qt', 'cmdline', 'kivy']
   DIR diff --git a/plugins/trustedcoin/qt.py b/plugins/trustedcoin/qt.py
       t@@ -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
        
        
       t@@ -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):
        
       t@@ -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)
       t@@ -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))
        
       t@@ -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")))
       t@@ -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)
       t@@ -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)
       t@@ -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())
   DIR diff --git a/plugins/trustedcoin/trustedcoin.py b/plugins/trustedcoin/trustedcoin.py
       t@@ -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):
       t@@ -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()
       t@@ -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
       t@@ -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)
       t@@ -336,6 +345,21 @@ class TrustedCoinPlugin(BasePlugin):
                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:
                    return
       t@@ -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')
       t@@ -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
       t@@ -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.
       t@@ -550,10 +563,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:
       t@@ -569,8 +581,8 @@ class TrustedCoinPlugin(BasePlugin):
            def do_auth(self, wizard, short_id, otp, xpub3):
                try:
                    server.auth(short_id, otp)
       -        except:
       -            wizard.show_message(_('Incorrect password'))
       +        except Exception as e:
       +            wizard.show_message(str(e))
                    return
                k3 = keystore.from_xpub(xpub3)
                wizard.storage.put('x3/', k3.dump())
       t@@ -603,7 +615,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):
       t@@ -614,4 +626,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'