electrum

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

clientbase.py (11974B)


      1 import time
      2 from struct import pack
      3 
      4 from electrum import ecc
      5 from electrum.i18n import _
      6 from electrum.util import UserCancelled, UserFacingException
      7 from electrum.keystore import bip39_normalize_passphrase
      8 from electrum.bip32 import BIP32Node, convert_bip32_path_to_list_of_uint32 as parse_path
      9 from electrum.logging import Logger
     10 from electrum.plugin import runs_in_hwd_thread
     11 from electrum.plugins.hw_wallet.plugin import OutdatedHwFirmwareException, HardwareClientBase
     12 
     13 from trezorlib.client import TrezorClient, PASSPHRASE_ON_DEVICE
     14 from trezorlib.exceptions import TrezorFailure, Cancelled, OutdatedFirmwareError
     15 from trezorlib.messages import WordRequestType, FailureType, RecoveryDeviceType, ButtonRequestType
     16 import trezorlib.btc
     17 import trezorlib.device
     18 
     19 MESSAGES = {
     20     ButtonRequestType.ConfirmOutput:
     21         _("Confirm the transaction output on your {} device"),
     22     ButtonRequestType.ResetDevice:
     23         _("Complete the initialization process on your {} device"),
     24     ButtonRequestType.ConfirmWord:
     25         _("Write down the seed word shown on your {}"),
     26     ButtonRequestType.WipeDevice:
     27         _("Confirm on your {} that you want to wipe it clean"),
     28     ButtonRequestType.ProtectCall:
     29         _("Confirm on your {} device the message to sign"),
     30     ButtonRequestType.SignTx:
     31         _("Confirm the total amount spent and the transaction fee on your {} device"),
     32     ButtonRequestType.Address:
     33         _("Confirm wallet address on your {} device"),
     34     ButtonRequestType._Deprecated_ButtonRequest_PassphraseType:
     35         _("Choose on your {} device where to enter your passphrase"),
     36     ButtonRequestType.PassphraseEntry:
     37         _("Please enter your passphrase on the {} device"),
     38     'default': _("Check your {} device to continue"),
     39 }
     40 
     41 
     42 class TrezorClientBase(HardwareClientBase, Logger):
     43     def __init__(self, transport, handler, plugin):
     44         HardwareClientBase.__init__(self, plugin=plugin)
     45         if plugin.is_outdated_fw_ignored():
     46             TrezorClient.is_outdated = lambda *args, **kwargs: False
     47         self.client = TrezorClient(transport, ui=self)
     48         self.device = plugin.device
     49         self.handler = handler
     50         Logger.__init__(self)
     51 
     52         self.msg = None
     53         self.creating_wallet = False
     54 
     55         self.in_flow = False
     56 
     57         self.used()
     58 
     59     def run_flow(self, message=None, creating_wallet=False):
     60         if self.in_flow:
     61             raise RuntimeError("Overlapping call to run_flow")
     62 
     63         self.in_flow = True
     64         self.msg = message
     65         self.creating_wallet = creating_wallet
     66         self.prevent_timeouts()
     67         return self
     68 
     69     def end_flow(self):
     70         self.in_flow = False
     71         self.msg = None
     72         self.creating_wallet = False
     73         self.handler.finished()
     74         self.used()
     75 
     76     def __enter__(self):
     77         return self
     78 
     79     def __exit__(self, exc_type, e, traceback):
     80         self.end_flow()
     81         if e is not None:
     82             if isinstance(e, Cancelled):
     83                 raise UserCancelled() from e
     84             elif isinstance(e, TrezorFailure):
     85                 raise RuntimeError(str(e)) from e
     86             elif isinstance(e, OutdatedFirmwareError):
     87                 raise OutdatedHwFirmwareException(e) from e
     88             else:
     89                 return False
     90         return True
     91 
     92     @property
     93     def features(self):
     94         return self.client.features
     95 
     96     def __str__(self):
     97         return "%s/%s" % (self.label(), self.features.device_id)
     98 
     99     def label(self):
    100         return self.features.label
    101 
    102     def get_soft_device_id(self):
    103         return self.features.device_id
    104 
    105     def is_initialized(self):
    106         return self.features.initialized
    107 
    108     def is_pairable(self):
    109         return not self.features.bootloader_mode
    110 
    111     @runs_in_hwd_thread
    112     def has_usable_connection_with_device(self):
    113         if self.in_flow:
    114             return True
    115 
    116         try:
    117             self.client.init_device()
    118         except BaseException:
    119             return False
    120         return True
    121 
    122     def used(self):
    123         self.last_operation = time.time()
    124 
    125     def prevent_timeouts(self):
    126         self.last_operation = float('inf')
    127 
    128     @runs_in_hwd_thread
    129     def timeout(self, cutoff):
    130         '''Time out the client if the last operation was before cutoff.'''
    131         if self.last_operation < cutoff:
    132             self.logger.info("timed out")
    133             self.clear_session()
    134 
    135     def i4b(self, x):
    136         return pack('>I', x)
    137 
    138     @runs_in_hwd_thread
    139     def get_xpub(self, bip32_path, xtype, creating=False):
    140         address_n = parse_path(bip32_path)
    141         with self.run_flow(creating_wallet=creating):
    142             node = trezorlib.btc.get_public_node(self.client, address_n).node
    143         return BIP32Node(xtype=xtype,
    144                          eckey=ecc.ECPubkey(node.public_key),
    145                          chaincode=node.chain_code,
    146                          depth=node.depth,
    147                          fingerprint=self.i4b(node.fingerprint),
    148                          child_number=self.i4b(node.child_num)).to_xpub()
    149 
    150     @runs_in_hwd_thread
    151     def toggle_passphrase(self):
    152         if self.features.passphrase_protection:
    153             msg = _("Confirm on your {} device to disable passphrases")
    154         else:
    155             msg = _("Confirm on your {} device to enable passphrases")
    156         enabled = not self.features.passphrase_protection
    157         with self.run_flow(msg):
    158             trezorlib.device.apply_settings(self.client, use_passphrase=enabled)
    159 
    160     @runs_in_hwd_thread
    161     def change_label(self, label):
    162         with self.run_flow(_("Confirm the new label on your {} device")):
    163             trezorlib.device.apply_settings(self.client, label=label)
    164 
    165     @runs_in_hwd_thread
    166     def change_homescreen(self, homescreen):
    167         with self.run_flow(_("Confirm on your {} device to change your home screen")):
    168             trezorlib.device.apply_settings(self.client, homescreen=homescreen)
    169 
    170     @runs_in_hwd_thread
    171     def set_pin(self, remove):
    172         if remove:
    173             msg = _("Confirm on your {} device to disable PIN protection")
    174         elif self.features.pin_protection:
    175             msg = _("Confirm on your {} device to change your PIN")
    176         else:
    177             msg = _("Confirm on your {} device to set a PIN")
    178         with self.run_flow(msg):
    179             trezorlib.device.change_pin(self.client, remove)
    180 
    181     @runs_in_hwd_thread
    182     def clear_session(self):
    183         '''Clear the session to force pin (and passphrase if enabled)
    184         re-entry.  Does not leak exceptions.'''
    185         self.logger.info(f"clear session: {self}")
    186         self.prevent_timeouts()
    187         try:
    188             self.client.clear_session()
    189         except BaseException as e:
    190             # If the device was removed it has the same effect...
    191             self.logger.info(f"clear_session: ignoring error {e}")
    192 
    193     @runs_in_hwd_thread
    194     def close(self):
    195         '''Called when Our wallet was closed or the device removed.'''
    196         self.logger.info("closing client")
    197         self.clear_session()
    198 
    199     @runs_in_hwd_thread
    200     def is_uptodate(self):
    201         if self.client.is_outdated():
    202             return False
    203         return self.client.version >= self.plugin.minimum_firmware
    204 
    205     def get_trezor_model(self):
    206         """Returns '1' for Trezor One, 'T' for Trezor T."""
    207         return self.features.model
    208 
    209     def device_model_name(self):
    210         model = self.get_trezor_model()
    211         if model == '1':
    212             return "Trezor One"
    213         elif model == 'T':
    214             return "Trezor T"
    215         return None
    216 
    217     @runs_in_hwd_thread
    218     def show_address(self, address_str, script_type, multisig=None):
    219         coin_name = self.plugin.get_coin_name()
    220         address_n = parse_path(address_str)
    221         with self.run_flow():
    222             return trezorlib.btc.get_address(
    223                 self.client,
    224                 coin_name,
    225                 address_n,
    226                 show_display=True,
    227                 script_type=script_type,
    228                 multisig=multisig)
    229 
    230     @runs_in_hwd_thread
    231     def sign_message(self, address_str, message):
    232         coin_name = self.plugin.get_coin_name()
    233         address_n = parse_path(address_str)
    234         with self.run_flow():
    235             return trezorlib.btc.sign_message(
    236                 self.client,
    237                 coin_name,
    238                 address_n,
    239                 message)
    240 
    241     @runs_in_hwd_thread
    242     def recover_device(self, recovery_type, *args, **kwargs):
    243         input_callback = self.mnemonic_callback(recovery_type)
    244         with self.run_flow():
    245             return trezorlib.device.recover(
    246                 self.client,
    247                 *args,
    248                 input_callback=input_callback,
    249                 type=recovery_type,
    250                 **kwargs)
    251 
    252     # ========= Unmodified trezorlib methods =========
    253 
    254     @runs_in_hwd_thread
    255     def sign_tx(self, *args, **kwargs):
    256         with self.run_flow():
    257             return trezorlib.btc.sign_tx(self.client, *args, **kwargs)
    258 
    259     @runs_in_hwd_thread
    260     def reset_device(self, *args, **kwargs):
    261         with self.run_flow():
    262             return trezorlib.device.reset(self.client, *args, **kwargs)
    263 
    264     @runs_in_hwd_thread
    265     def wipe_device(self, *args, **kwargs):
    266         with self.run_flow():
    267             return trezorlib.device.wipe(self.client, *args, **kwargs)
    268 
    269     # ========= UI methods ==========
    270 
    271     def button_request(self, code):
    272         message = self.msg or MESSAGES.get(code) or MESSAGES['default']
    273         self.handler.show_message(message.format(self.device), self.client.cancel)
    274 
    275     def get_pin(self, code=None):
    276         show_strength = True
    277         if code == 2:
    278             msg = _("Enter a new PIN for your {}:")
    279         elif code == 3:
    280             msg = (_("Re-enter the new PIN for your {}.\n\n"
    281                      "NOTE: the positions of the numbers have changed!"))
    282         else:
    283             msg = _("Enter your current {} PIN:")
    284             show_strength = False
    285         pin = self.handler.get_pin(msg.format(self.device), show_strength=show_strength)
    286         if not pin:
    287             raise Cancelled
    288         if len(pin) > 9:
    289             self.handler.show_error(_('The PIN cannot be longer than 9 characters.'))
    290             raise Cancelled
    291         return pin
    292 
    293     def get_passphrase(self, available_on_device):
    294         if self.creating_wallet:
    295             msg = _("Enter a passphrase to generate this wallet.  Each time "
    296                     "you use this wallet your {} will prompt you for the "
    297                     "passphrase.  If you forget the passphrase you cannot "
    298                     "access the bitcoins in the wallet.").format(self.device)
    299         else:
    300             msg = _("Enter the passphrase to unlock this wallet:")
    301 
    302         self.handler.passphrase_on_device = available_on_device
    303         passphrase = self.handler.get_passphrase(msg, self.creating_wallet)
    304         if passphrase is PASSPHRASE_ON_DEVICE:
    305             return passphrase
    306         if passphrase is None:
    307             raise Cancelled
    308         passphrase = bip39_normalize_passphrase(passphrase)
    309         length = len(passphrase)
    310         if length > 50:
    311             self.handler.show_error(_("Too long passphrase ({} > 50 chars).").format(length))
    312             raise Cancelled
    313         return passphrase
    314 
    315     def _matrix_char(self, matrix_type):
    316         num = 9 if matrix_type == WordRequestType.Matrix9 else 6
    317         char = self.handler.get_matrix(num)
    318         if char == 'x':
    319             raise Cancelled
    320         return char
    321 
    322     def mnemonic_callback(self, recovery_type):
    323         if recovery_type is None:
    324             return None
    325 
    326         if recovery_type == RecoveryDeviceType.Matrix:
    327             return self._matrix_char
    328 
    329         step = 0
    330         def word_callback(_ignored):
    331             nonlocal step
    332             step += 1
    333             msg = _("Step {}/24.  Enter seed word as explained on your {}:").format(step, self.device)
    334             word = self.handler.get_word(msg)
    335             if not word:
    336                 raise Cancelled
    337             return word
    338         return word_callback