electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

qt.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())