Repository: spesmilo/electrum
Issue: 2FA OTP might expire while signing large transactions

Hello,

I try to send my BTC from my 2FA wallet and it's stay many time in signing transaction and after 1 min appears invalid OTP, it's like 2fa code is wrong but I think that is because it stay many time signing the transaction and the code changes during this time. 
Help me please.
Thanks.

Changed files:
plugins/trustedcoin/qt.py, plugins/trustedcoin/cmdline.py, plugins/trustedcoin/trustedcoin.py, gui/qt/main_window.py, lib/commands.py

diff --git a/gui/qt/main_window.py b/gui/qt/main_window.py
index a3a944875..82363233b 100644
--- a/gui/qt/main_window.py
+++ b/gui/qt/main_window.py
@@ -1584,8 +1584,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
             # can sign directly
             task = partial(Transaction.sign, tx, self.tx_external_keypairs)
         else:
-            # call hook to see if plugin needs gui interaction
-            run_hook('sign_tx', self, tx)
             task = partial(self.wallet.sign_transaction, tx, password)
         WaitingDialog(self, _('Signing transaction...'), task,
                       on_signed, on_failed)
diff --git a/lib/commands.py b/lib/commands.py
index e9c6cf277..7ae0bea64 100644
--- a/lib/commands.py
+++ b/lib/commands.py
@@ -426,7 +426,6 @@ class Commands:
         if rbf:
             tx.set_rbf(True)
         if not unsigned:
-            run_hook('sign_tx', self.wallet, tx)
             self.wallet.sign_transaction(tx, password)
         return tx
 
diff --git a/plugins/trustedcoin/cmdline.py b/plugins/trustedcoin/cmdline.py
index 51e34fda1..d70b96905 100644
--- a/plugins/trustedcoin/cmdline.py
+++ b/plugins/trustedcoin/cmdline.py
@@ -27,10 +27,10 @@ from electrum.i18n import _
 from electrum.plugins import hook
 from .trustedcoin import TrustedCoinPlugin
 
+
 class Plugin(TrustedCoinPlugin):
 
-    @hook
-    def sign_tx(self, wallet, tx):
+    def prompt_user_for_otp(self, wallet, tx):
         if not isinstance(wallet, self.wallet_class):
             return
         if not wallet.can_sign_without_server():
diff --git a/plugins/trustedcoin/qt.py b/plugins/trustedcoin/qt.py
index 693fe1575..2712b4d18 100644
--- a/plugins/trustedcoin/qt.py
+++ b/plugins/trustedcoin/qt.py
@@ -24,6 +24,7 @@
 # SOFTWARE.
 
 from functools import partial
+import threading
 from threading import Thread
 import re
 from decimal import Decimal
@@ -37,6 +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 .trustedcoin import TrustedCoinPlugin, server
 
 
@@ -45,6 +47,38 @@ class TOS(QTextEdit):
     error_signal = pyqtSignal(object)
 
 
+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):
+        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()
+
+
 class Plugin(TrustedCoinPlugin):
 
     def __init__(self, parent, config, name):
@@ -55,6 +89,7 @@ class Plugin(TrustedCoinPlugin):
         wallet = window.wallet
         if not isinstance(wallet, self.wallet_class):
             return
+        wallet.handler_2fa = HandlerTwoFactor(self, window)
         if wallet.can_sign_without_server():
             msg = ' '.join([
                 _('This wallet was restored from seed, and it contains two master private keys.'),
@@ -88,19 +123,8 @@ class Plugin(TrustedCoinPlugin):
             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 prompt_user_for_otp(self, wallet, tx):
+        wallet.handler_2fa.prompt_user_for_otp(wallet, tx)
 
     def waiting_dialog(self, window, on_finished=None):
         task = partial(self.request_billing_info, window.wallet)
diff --git a/plugins/trustedcoin/trustedcoin.py b/plugins/trustedcoin/trustedcoin.py
index 0dab526aa..4d49640be 100644
--- a/plugins/trustedcoin/trustedcoin.py
+++ b/plugins/trustedcoin/trustedcoin.py
@@ -215,6 +215,7 @@ 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()
@@ -272,6 +273,7 @@ class Wallet_2fa(Multisig_Wallet):
         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:
             self.print_error("sign_transaction: no auth code")
             return
@@ -285,6 +287,7 @@ class Wallet_2fa(Multisig_Wallet):
         self.print_error("twofactor: is complete", tx.is_complete())
         # reset billing_info
         self.billing_info = None
+        self.auth_code = None
 
 
 # Utility functions