electrum

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

clientbase.py (10111B)


      1 import time
      2 from struct import pack
      3 from typing import Optional
      4 
      5 from electrum import ecc
      6 from electrum.i18n import _
      7 from electrum.util import UserCancelled
      8 from electrum.keystore import bip39_normalize_passphrase
      9 from electrum.bip32 import BIP32Node, convert_bip32_path_to_list_of_uint32
     10 from electrum.logging import Logger
     11 from electrum.plugin import runs_in_hwd_thread
     12 from electrum.plugins.hw_wallet.plugin import HardwareClientBase, HardwareHandlerBase
     13 
     14 
     15 class GuiMixin(object):
     16     # Requires: self.proto, self.device
     17     handler: Optional[HardwareHandlerBase]
     18 
     19     # ref: https://github.com/trezor/trezor-common/blob/44dfb07cfaafffada4b2ce0d15ba1d90d17cf35e/protob/types.proto#L89
     20     messages = {
     21         3: _("Confirm the transaction output on your {} device"),
     22         4: _("Confirm internal entropy on your {} device to begin"),
     23         5: _("Write down the seed word shown on your {}"),
     24         6: _("Confirm on your {} that you want to wipe it clean"),
     25         7: _("Confirm on your {} device the message to sign"),
     26         8: _("Confirm the total amount spent and the transaction fee on your "
     27              "{} device"),
     28         10: _("Confirm wallet address on your {} device"),
     29         14: _("Choose on your {} device where to enter your passphrase"),
     30         'default': _("Check your {} device to continue"),
     31     }
     32 
     33     def callback_Failure(self, msg):
     34         # BaseClient's unfortunate call() implementation forces us to
     35         # raise exceptions on failure in order to unwind the stack.
     36         # However, making the user acknowledge they cancelled
     37         # gets old very quickly, so we suppress those.  The NotInitialized
     38         # one is misnamed and indicates a passphrase request was cancelled.
     39         if msg.code in (self.types.FailureType.PinCancelled,
     40                         self.types.FailureType.ActionCancelled,
     41                         self.types.FailureType.NotInitialized):
     42             raise UserCancelled()
     43         raise RuntimeError(msg.message)
     44 
     45     def callback_ButtonRequest(self, msg):
     46         message = self.msg
     47         if not message:
     48             message = self.messages.get(msg.code, self.messages['default'])
     49         self.handler.show_message(message.format(self.device), self.cancel)
     50         return self.proto.ButtonAck()
     51 
     52     def callback_PinMatrixRequest(self, msg):
     53         show_strength = True
     54         if msg.type == 2:
     55             msg = _("Enter a new PIN for your {}:")
     56         elif msg.type == 3:
     57             msg = (_("Re-enter the new PIN for your {}.\n\n"
     58                      "NOTE: the positions of the numbers have changed!"))
     59         else:
     60             msg = _("Enter your current {} PIN:")
     61             show_strength = False
     62         pin = self.handler.get_pin(msg.format(self.device), show_strength=show_strength)
     63         if len(pin) > 9:
     64             self.handler.show_error(_('The PIN cannot be longer than 9 characters.'))
     65             pin = ''  # to cancel below
     66         if not pin:
     67             return self.proto.Cancel()
     68         return self.proto.PinMatrixAck(pin=pin)
     69 
     70     def callback_PassphraseRequest(self, req):
     71         if req and hasattr(req, 'on_device') and req.on_device is True:
     72             return self.proto.PassphraseAck()
     73 
     74         if self.creating_wallet:
     75             msg = _("Enter a passphrase to generate this wallet.  Each time "
     76                     "you use this wallet your {} will prompt you for the "
     77                     "passphrase.  If you forget the passphrase you cannot "
     78                     "access the bitcoins in the wallet.").format(self.device)
     79         else:
     80             msg = _("Enter the passphrase to unlock this wallet:")
     81         passphrase = self.handler.get_passphrase(msg, self.creating_wallet)
     82         if passphrase is None:
     83             return self.proto.Cancel()
     84         passphrase = bip39_normalize_passphrase(passphrase)
     85 
     86         ack = self.proto.PassphraseAck(passphrase=passphrase)
     87         length = len(ack.passphrase)
     88         if length > 50:
     89             self.handler.show_error(_("Too long passphrase ({} > 50 chars).").format(length))
     90             return self.proto.Cancel()
     91         return ack
     92 
     93     def callback_PassphraseStateRequest(self, msg):
     94         return self.proto.PassphraseStateAck()
     95 
     96     def callback_WordRequest(self, msg):
     97         self.step += 1
     98         msg = _("Step {}/24.  Enter seed word as explained on "
     99                 "your {}:").format(self.step, self.device)
    100         word = self.handler.get_word(msg)
    101         # Unfortunately the device can't handle self.proto.Cancel()
    102         return self.proto.WordAck(word=word)
    103 
    104 
    105 class SafeTClientBase(HardwareClientBase, GuiMixin, Logger):
    106 
    107     def __init__(self, handler, plugin, proto):
    108         assert hasattr(self, 'tx_api')  # ProtocolMixin already constructed?
    109         HardwareClientBase.__init__(self, plugin=plugin)
    110         self.proto = proto
    111         self.device = plugin.device
    112         self.handler = handler
    113         self.tx_api = plugin
    114         self.types = plugin.types
    115         self.msg = None
    116         self.creating_wallet = False
    117         Logger.__init__(self)
    118         self.used()
    119 
    120     def __str__(self):
    121         return "%s/%s" % (self.label(), self.features.device_id)
    122 
    123     def label(self):
    124         return self.features.label
    125 
    126     def get_soft_device_id(self):
    127         return self.features.device_id
    128 
    129     def is_initialized(self):
    130         return self.features.initialized
    131 
    132     def is_pairable(self):
    133         return not self.features.bootloader_mode
    134 
    135     @runs_in_hwd_thread
    136     def has_usable_connection_with_device(self):
    137         try:
    138             res = self.ping("electrum pinging device")
    139             assert res == "electrum pinging device"
    140         except BaseException:
    141             return False
    142         return True
    143 
    144     def used(self):
    145         self.last_operation = time.time()
    146 
    147     def prevent_timeouts(self):
    148         self.last_operation = float('inf')
    149 
    150     @runs_in_hwd_thread
    151     def timeout(self, cutoff):
    152         '''Time out the client if the last operation was before cutoff.'''
    153         if self.last_operation < cutoff:
    154             self.logger.info("timed out")
    155             self.clear_session()
    156 
    157     @staticmethod
    158     def expand_path(n):
    159         return convert_bip32_path_to_list_of_uint32(n)
    160 
    161     @runs_in_hwd_thread
    162     def cancel(self):
    163         '''Provided here as in keepkeylib but not safetlib.'''
    164         self.transport.write(self.proto.Cancel())
    165 
    166     def i4b(self, x):
    167         return pack('>I', x)
    168 
    169     @runs_in_hwd_thread
    170     def get_xpub(self, bip32_path, xtype):
    171         address_n = self.expand_path(bip32_path)
    172         creating = False
    173         node = self.get_public_node(address_n, creating).node
    174         return BIP32Node(xtype=xtype,
    175                          eckey=ecc.ECPubkey(node.public_key),
    176                          chaincode=node.chain_code,
    177                          depth=node.depth,
    178                          fingerprint=self.i4b(node.fingerprint),
    179                          child_number=self.i4b(node.child_num)).to_xpub()
    180 
    181     @runs_in_hwd_thread
    182     def toggle_passphrase(self):
    183         if self.features.passphrase_protection:
    184             self.msg = _("Confirm on your {} device to disable passphrases")
    185         else:
    186             self.msg = _("Confirm on your {} device to enable passphrases")
    187         enabled = not self.features.passphrase_protection
    188         self.apply_settings(use_passphrase=enabled)
    189 
    190     @runs_in_hwd_thread
    191     def change_label(self, label):
    192         self.msg = _("Confirm the new label on your {} device")
    193         self.apply_settings(label=label)
    194 
    195     @runs_in_hwd_thread
    196     def change_homescreen(self, homescreen):
    197         self.msg = _("Confirm on your {} device to change your home screen")
    198         self.apply_settings(homescreen=homescreen)
    199 
    200     @runs_in_hwd_thread
    201     def set_pin(self, remove):
    202         if remove:
    203             self.msg = _("Confirm on your {} device to disable PIN protection")
    204         elif self.features.pin_protection:
    205             self.msg = _("Confirm on your {} device to change your PIN")
    206         else:
    207             self.msg = _("Confirm on your {} device to set a PIN")
    208         self.change_pin(remove)
    209 
    210     @runs_in_hwd_thread
    211     def clear_session(self):
    212         '''Clear the session to force pin (and passphrase if enabled)
    213         re-entry.  Does not leak exceptions.'''
    214         self.logger.info(f"clear session: {self}")
    215         self.prevent_timeouts()
    216         try:
    217             super(SafeTClientBase, self).clear_session()
    218         except BaseException as e:
    219             # If the device was removed it has the same effect...
    220             self.logger.info(f"clear_session: ignoring error {e}")
    221 
    222     @runs_in_hwd_thread
    223     def get_public_node(self, address_n, creating):
    224         self.creating_wallet = creating
    225         return super(SafeTClientBase, self).get_public_node(address_n)
    226 
    227     @runs_in_hwd_thread
    228     def close(self):
    229         '''Called when Our wallet was closed or the device removed.'''
    230         self.logger.info("closing client")
    231         self.clear_session()
    232         # Release the device
    233         self.transport.close()
    234 
    235     def firmware_version(self):
    236         f = self.features
    237         return (f.major_version, f.minor_version, f.patch_version)
    238 
    239     def atleast_version(self, major, minor=0, patch=0):
    240         return self.firmware_version() >= (major, minor, patch)
    241 
    242     @staticmethod
    243     def wrapper(func):
    244         '''Wrap methods to clear any message box they opened.'''
    245 
    246         def wrapped(self, *args, **kwargs):
    247             try:
    248                 self.prevent_timeouts()
    249                 return func(self, *args, **kwargs)
    250             finally:
    251                 self.used()
    252                 self.handler.finished()
    253                 self.creating_wallet = False
    254                 self.msg = None
    255 
    256         return wrapped
    257 
    258     @staticmethod
    259     def wrap_methods(cls):
    260         for method in ['apply_settings', 'change_pin',
    261                        'get_address', 'get_public_node',
    262                        'load_device_by_mnemonic', 'load_device_by_xprv',
    263                        'recovery_device', 'reset_device', 'sign_message',
    264                        'sign_tx', 'wipe_device']:
    265             setattr(cls, method, cls.wrapper(getattr(cls, method)))