URI: 
       tqt.py - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
       tqt.py (13329B)
       ---
            1 #!/usr/bin/env python
            2 #
            3 # Electrum - Lightweight Bitcoin Client
            4 # Copyright (C) 2015 Thomas Voegtlin
            5 #
            6 # Permission is hereby granted, free of charge, to any person
            7 # obtaining a copy of this software and associated documentation files
            8 # (the "Software"), to deal in the Software without restriction,
            9 # including without limitation the rights to use, copy, modify, merge,
           10 # publish, distribute, sublicense, and/or sell copies of the Software,
           11 # and to permit persons to whom the Software is furnished to do so,
           12 # subject to the following conditions:
           13 #
           14 # The above copyright notice and this permission notice shall be
           15 # included in all copies or substantial portions of the Software.
           16 #
           17 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
           18 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
           19 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
           20 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
           21 # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
           22 # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
           23 # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
           24 # SOFTWARE.
           25 
           26 from functools import partial
           27 import threading
           28 import sys
           29 import os
           30 from typing import TYPE_CHECKING
           31 
           32 from PyQt5.QtGui import QPixmap
           33 from PyQt5.QtCore import QObject, pyqtSignal
           34 from PyQt5.QtWidgets import (QTextEdit, QVBoxLayout, QLabel, QGridLayout, QHBoxLayout,
           35                              QRadioButton, QCheckBox, QLineEdit)
           36 
           37 from electrum.gui.qt.util import (read_QIcon, WindowModalDialog, WaitingDialog, OkButton,
           38                                   CancelButton, Buttons, icon_path, WWLabel, CloseButton)
           39 from electrum.gui.qt.qrcodewidget import QRCodeWidget
           40 from electrum.gui.qt.amountedit import AmountEdit
           41 from electrum.gui.qt.main_window import StatusBarButton
           42 from electrum.gui.qt.installwizard import InstallWizard
           43 from electrum.i18n import _
           44 from electrum.plugin import hook
           45 from electrum.util import is_valid_email
           46 from electrum.logging import Logger
           47 from electrum.base_wizard import GoBack, UserCancelled
           48 
           49 from .trustedcoin import TrustedCoinPlugin, server
           50 
           51 if TYPE_CHECKING:
           52     from electrum.gui.qt.main_window import ElectrumWindow
           53     from electrum.wallet import Abstract_Wallet
           54 
           55 
           56 class TOS(QTextEdit):
           57     tos_signal = pyqtSignal()
           58     error_signal = pyqtSignal(object)
           59 
           60 
           61 class HandlerTwoFactor(QObject, Logger):
           62 
           63     def __init__(self, plugin, window):
           64         QObject.__init__(self)
           65         self.plugin = plugin
           66         self.window = window
           67         Logger.__init__(self)
           68 
           69     def prompt_user_for_otp(self, wallet, tx, on_success, on_failure):
           70         if not isinstance(wallet, self.plugin.wallet_class):
           71             return
           72         if wallet.can_sign_without_server():
           73             return
           74         if not wallet.keystores['x3/'].can_sign(tx, ignore_watching_only=True):
           75             self.logger.info("twofactor: xpub3 not needed")
           76             return
           77         window = self.window.top_level_window()
           78         auth_code = self.plugin.auth_dialog(window)
           79         WaitingDialog(parent=window,
           80                       message=_('Waiting for TrustedCoin server to sign transaction...'),
           81                       task=lambda: wallet.on_otp(tx, auth_code),
           82                       on_success=lambda *args: on_success(tx),
           83                       on_error=on_failure)
           84 
           85 
           86 class Plugin(TrustedCoinPlugin):
           87 
           88     def __init__(self, parent, config, name):
           89         super().__init__(parent, config, name)
           90 
           91     @hook
           92     def load_wallet(self, wallet: 'Abstract_Wallet', window: 'ElectrumWindow'):
           93         if not isinstance(wallet, self.wallet_class):
           94             return
           95         wallet.handler_2fa = HandlerTwoFactor(self, window)
           96         if wallet.can_sign_without_server():
           97             msg = ' '.join([
           98                 _('This wallet was restored from seed, and it contains two master private keys.'),
           99                 _('Therefore, two-factor authentication is disabled.')
          100             ])
          101             action = lambda: window.show_message(msg)
          102         else:
          103             action = partial(self.settings_dialog, window)
          104         button = StatusBarButton(read_QIcon("trustedcoin-status.png"),
          105                                  _("TrustedCoin"), action)
          106         window.statusBar().addPermanentWidget(button)
          107         self.start_request_thread(window.wallet)
          108 
          109     def auth_dialog(self, window):
          110         d = WindowModalDialog(window, _("Authorization"))
          111         vbox = QVBoxLayout(d)
          112         pw = AmountEdit(None, is_int = True)
          113         msg = _('Please enter your Google Authenticator code')
          114         vbox.addWidget(QLabel(msg))
          115         grid = QGridLayout()
          116         grid.setSpacing(8)
          117         grid.addWidget(QLabel(_('Code')), 1, 0)
          118         grid.addWidget(pw, 1, 1)
          119         vbox.addLayout(grid)
          120         msg = _('If you have lost your second factor, you need to restore your wallet from seed in order to request a new code.')
          121         label = QLabel(msg)
          122         label.setWordWrap(1)
          123         vbox.addWidget(label)
          124         vbox.addLayout(Buttons(CancelButton(d), OkButton(d)))
          125         if not d.exec_():
          126             return
          127         return pw.get_amount()
          128 
          129     def prompt_user_for_otp(self, wallet, tx, on_success, on_failure):
          130         wallet.handler_2fa.prompt_user_for_otp(wallet, tx, on_success, on_failure)
          131 
          132     def waiting_dialog_for_billing_info(self, window, *, on_finished=None):
          133         def task():
          134             return self.request_billing_info(window.wallet, suppress_connection_error=False)
          135         def on_error(exc_info):
          136             e = exc_info[1]
          137             window.show_error("{header}\n{exc}\n\n{tor}"
          138                               .format(header=_('Error getting TrustedCoin account info.'),
          139                                       exc=repr(e),
          140                                       tor=_('If you keep experiencing network problems, try using a Tor proxy.')))
          141         return WaitingDialog(parent=window,
          142                              message=_('Requesting account info from TrustedCoin server...'),
          143                              task=task,
          144                              on_success=on_finished,
          145                              on_error=on_error)
          146 
          147     @hook
          148     def abort_send(self, window):
          149         wallet = window.wallet
          150         if not isinstance(wallet, self.wallet_class):
          151             return
          152         if wallet.can_sign_without_server():
          153             return
          154         if wallet.billing_info is None:
          155             self.waiting_dialog_for_billing_info(window)
          156             return True
          157         return False
          158 
          159     def settings_dialog(self, window):
          160         self.waiting_dialog_for_billing_info(window,
          161                                              on_finished=partial(self.show_settings_dialog, window))
          162 
          163     def show_settings_dialog(self, window, success):
          164         if not success:
          165             window.show_message(_('Server not reachable.'))
          166             return
          167 
          168         wallet = window.wallet
          169         d = WindowModalDialog(window, _("TrustedCoin Information"))
          170         d.setMinimumSize(500, 200)
          171         vbox = QVBoxLayout(d)
          172         hbox = QHBoxLayout()
          173 
          174         logo = QLabel()
          175         logo.setPixmap(QPixmap(icon_path("trustedcoin-status.png")))
          176         msg = _('This wallet is protected by TrustedCoin\'s two-factor authentication.') + '<br/>'\
          177               + _("For more information, visit") + " <a href=\"https://api.trustedcoin.com/#/electrum-help\">https://api.trustedcoin.com/#/electrum-help</a>"
          178         label = QLabel(msg)
          179         label.setOpenExternalLinks(1)
          180 
          181         hbox.addStretch(10)
          182         hbox.addWidget(logo)
          183         hbox.addStretch(10)
          184         hbox.addWidget(label)
          185         hbox.addStretch(10)
          186 
          187         vbox.addLayout(hbox)
          188         vbox.addStretch(10)
          189 
          190         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 every time you run out of prepaid transactions.') + '<br/>'
          191         label = QLabel(msg)
          192         label.setWordWrap(1)
          193         vbox.addWidget(label)
          194 
          195         vbox.addStretch(10)
          196         grid = QGridLayout()
          197         vbox.addLayout(grid)
          198 
          199         price_per_tx = wallet.price_per_tx
          200         n_prepay = wallet.num_prepay()
          201         i = 0
          202         for k, v in sorted(price_per_tx.items()):
          203             if k == 1:
          204                 continue
          205             grid.addWidget(QLabel("Pay every %d transactions:"%k), i, 0)
          206             grid.addWidget(QLabel(window.format_amount(v/k) + ' ' + window.base_unit() + "/tx"), i, 1)
          207             b = QRadioButton()
          208             b.setChecked(k == n_prepay)
          209             b.clicked.connect(lambda b, k=k: self.config.set_key('trustedcoin_prepay', k, True))
          210             grid.addWidget(b, i, 2)
          211             i += 1
          212 
          213         n = wallet.billing_info.get('tx_remaining', 0)
          214         grid.addWidget(QLabel(_("Your wallet has {} prepaid transactions.").format(n)), i, 0)
          215         vbox.addLayout(Buttons(CloseButton(d)))
          216         d.exec_()
          217 
          218     def go_online_dialog(self, wizard: InstallWizard):
          219         msg = [
          220             _("Your wallet file is: {}.").format(os.path.abspath(wizard.path)),
          221             _("You need to be online in order to complete the creation of "
          222               "your wallet.  If you generated your seed on an offline "
          223               'computer, click on "{}" to close this window, move your '
          224               "wallet file to an online computer, and reopen it with "
          225               "Electrum.").format(_('Cancel')),
          226             _('If you are online, click on "{}" to continue.').format(_('Next'))
          227         ]
          228         msg = '\n\n'.join(msg)
          229         wizard.reset_stack()
          230         try:
          231             wizard.confirm_dialog(title='', message=msg, run_next = lambda x: wizard.run('accept_terms_of_use'))
          232         except (GoBack, UserCancelled):
          233             # user clicked 'Cancel' and decided to move wallet file manually
          234             storage, db = wizard.create_storage(wizard.path)
          235             raise
          236 
          237     def accept_terms_of_use(self, window):
          238         vbox = QVBoxLayout()
          239         vbox.addWidget(QLabel(_("Terms of Service")))
          240 
          241         tos_e = TOS()
          242         tos_e.setReadOnly(True)
          243         vbox.addWidget(tos_e)
          244         tos_received = False
          245 
          246         vbox.addWidget(QLabel(_("Please enter your e-mail address")))
          247         email_e = QLineEdit()
          248         vbox.addWidget(email_e)
          249 
          250         next_button = window.next_button
          251         prior_button_text = next_button.text()
          252         next_button.setText(_('Accept'))
          253 
          254         def request_TOS():
          255             try:
          256                 tos = server.get_terms_of_service()
          257             except Exception as e:
          258                 self.logger.exception('Could not retrieve Terms of Service')
          259                 tos_e.error_signal.emit(_('Could not retrieve Terms of Service:')
          260                                         + '\n' + repr(e))
          261                 return
          262             self.TOS = tos
          263             tos_e.tos_signal.emit()
          264 
          265         def on_result():
          266             tos_e.setText(self.TOS)
          267             nonlocal tos_received
          268             tos_received = True
          269             set_enabled()
          270 
          271         def on_error(msg):
          272             window.show_error(str(msg))
          273             window.terminate()
          274 
          275         def set_enabled():
          276             next_button.setEnabled(tos_received and is_valid_email(email_e.text()))
          277 
          278         tos_e.tos_signal.connect(on_result)
          279         tos_e.error_signal.connect(on_error)
          280         t = threading.Thread(target=request_TOS)
          281         t.setDaemon(True)
          282         t.start()
          283         email_e.textChanged.connect(set_enabled)
          284         email_e.setFocus(True)
          285         window.exec_layout(vbox, next_enabled=False)
          286         next_button.setText(prior_button_text)
          287         email = str(email_e.text())
          288         self.create_remote_key(email, window)
          289 
          290     def request_otp_dialog(self, window, short_id, otp_secret, xpub3):
          291         vbox = QVBoxLayout()
          292         if otp_secret is not None:
          293             uri = "otpauth://totp/%s?secret=%s"%('trustedcoin.com', otp_secret)
          294             l = QLabel("Please scan the following QR code in Google Authenticator. You may as well use the following key: %s"%otp_secret)
          295             l.setWordWrap(True)
          296             vbox.addWidget(l)
          297             qrw = QRCodeWidget(uri)
          298             vbox.addWidget(qrw, 1)
          299             msg = _('Then, enter your Google Authenticator code:')
          300         else:
          301             label = QLabel(
          302                 "This wallet is already registered with TrustedCoin. "
          303                 "To finalize wallet creation, please enter your Google Authenticator Code. "
          304             )
          305             label.setWordWrap(1)
          306             vbox.addWidget(label)
          307             msg = _('Google Authenticator code:')
          308         hbox = QHBoxLayout()
          309         hbox.addWidget(WWLabel(msg))
          310         pw = AmountEdit(None, is_int = True)
          311         pw.setFocus(True)
          312         pw.setMaximumWidth(50)
          313         hbox.addWidget(pw)
          314         vbox.addLayout(hbox)
          315         cb_lost = QCheckBox(_("I have lost my Google Authenticator account"))
          316         cb_lost.setToolTip(_("Check this box to request a new secret. You will need to retype your seed."))
          317         vbox.addWidget(cb_lost)
          318         cb_lost.setVisible(otp_secret is None)
          319         def set_enabled():
          320             b = True if cb_lost.isChecked() else len(pw.text()) == 6
          321             window.next_button.setEnabled(b)
          322         pw.textChanged.connect(set_enabled)
          323         cb_lost.toggled.connect(set_enabled)
          324         window.exec_layout(vbox, next_enabled=False, raise_on_cancel=False)
          325         self.check_otp(window, short_id, otp_secret, xpub3, pw.get_amount(), cb_lost.isChecked())