electrum

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

qt.py (9269B)


      1 import time, os
      2 from functools import partial
      3 import copy
      4 
      5 from PyQt5.QtCore import Qt, pyqtSignal
      6 from PyQt5.QtWidgets import QPushButton, QLabel, QVBoxLayout, QWidget, QGridLayout
      7 
      8 from electrum.gui.qt.util import (WindowModalDialog, CloseButton, Buttons, getOpenFileName,
      9                                   getSaveFileName)
     10 from electrum.gui.qt.transaction_dialog import TxDialog
     11 from electrum.gui.qt.main_window import ElectrumWindow
     12 
     13 from electrum.i18n import _
     14 from electrum.plugin import hook
     15 from electrum.wallet import Multisig_Wallet
     16 from electrum.transaction import PartialTransaction
     17 
     18 from .coldcard import ColdcardPlugin, xfp2str
     19 from ..hw_wallet.qt import QtHandlerBase, QtPluginBase
     20 from ..hw_wallet.plugin import only_hook_if_libraries_available
     21 
     22 
     23 CC_DEBUG = False
     24 
     25 class Plugin(ColdcardPlugin, QtPluginBase):
     26     icon_unpaired = "coldcard_unpaired.png"
     27     icon_paired = "coldcard.png"
     28 
     29     def create_handler(self, window):
     30         return Coldcard_Handler(window)
     31 
     32     @only_hook_if_libraries_available
     33     @hook
     34     def receive_menu(self, menu, addrs, wallet):
     35         # Context menu on each address in the Addresses Tab, right click...
     36         if len(addrs) != 1:
     37             return
     38         for keystore in wallet.get_keystores():
     39             if type(keystore) == self.keystore_class:
     40                 def show_address(keystore=keystore):
     41                     keystore.thread.add(partial(self.show_address, wallet, addrs[0], keystore=keystore))
     42                 device_name = "{} ({})".format(self.device, keystore.label)
     43                 menu.addAction(_("Show on {}").format(device_name), show_address)
     44 
     45     @only_hook_if_libraries_available
     46     @hook
     47     def wallet_info_buttons(self, main_window, dialog):
     48         # user is about to see the "Wallet Information" dialog
     49         # - add a button if multisig wallet, and a Coldcard is a cosigner.
     50         wallet = main_window.wallet
     51 
     52         if type(wallet) is not Multisig_Wallet:
     53             return
     54 
     55         if not any(type(ks) == self.keystore_class for ks in wallet.get_keystores()):
     56             # doesn't involve a Coldcard wallet, hide feature
     57             return
     58 
     59         btn = QPushButton(_("Export for Coldcard"))
     60         btn.clicked.connect(lambda unused: self.export_multisig_setup(main_window, wallet))
     61 
     62         return btn
     63 
     64     def export_multisig_setup(self, main_window, wallet):
     65 
     66         basename = wallet.basename().rsplit('.', 1)[0]        # trim .json
     67         name = f'{basename}-cc-export.txt'.replace(' ', '-')
     68         fileName = getSaveFileName(
     69             parent=main_window,
     70             title=_("Select where to save the setup file"),
     71             filename=name,
     72             filter="*.txt",
     73             config=self.config,
     74         )
     75         if fileName:
     76             with open(fileName, "wt") as f:
     77                 ColdcardPlugin.export_ms_wallet(wallet, f, basename)
     78             main_window.show_message(_("Wallet setup file exported successfully"))
     79 
     80     def show_settings_dialog(self, window, keystore):
     81         # When they click on the icon for CC we come here.
     82         # - doesn't matter if device not connected, continue
     83         CKCCSettingsDialog(window, self, keystore).exec_()
     84 
     85 
     86 class Coldcard_Handler(QtHandlerBase):
     87 
     88     def __init__(self, win):
     89         super(Coldcard_Handler, self).__init__(win, 'Coldcard')
     90 
     91     def message_dialog(self, msg):
     92         self.clear_dialog()
     93         self.dialog = dialog = WindowModalDialog(self.top_level_window(), _("Coldcard Status"))
     94         l = QLabel(msg)
     95         vbox = QVBoxLayout(dialog)
     96         vbox.addWidget(l)
     97         dialog.show()
     98 
     99 
    100 class CKCCSettingsDialog(WindowModalDialog):
    101 
    102     def __init__(self, window: ElectrumWindow, plugin, keystore):
    103         title = _("{} Settings").format(plugin.device)
    104         super(CKCCSettingsDialog, self).__init__(window, title)
    105         self.setMaximumWidth(540)
    106 
    107         # Note: Coldcard may **not** be connected at present time. Keep working!
    108 
    109         devmgr = plugin.device_manager()
    110         #config = devmgr.config
    111         #handler = keystore.handler
    112         self.thread = thread = keystore.thread
    113         self.keystore = keystore
    114         assert isinstance(window, ElectrumWindow), f"{type(window)}"
    115         self.window = window
    116 
    117         def connect_and_doit():
    118             # Attempt connection to device, or raise.
    119             device_id = plugin.choose_device(window, keystore)
    120             if not device_id:
    121                 raise RuntimeError("Device not connected")
    122             client = devmgr.client_by_id(device_id)
    123             if not client:
    124                 raise RuntimeError("Device not connected")
    125             return client
    126 
    127         body = QWidget()
    128         body_layout = QVBoxLayout(body)
    129         grid = QGridLayout()
    130         grid.setColumnStretch(2, 1)
    131 
    132         # see <http://doc.qt.io/archives/qt-4.8/richtext-html-subset.html>
    133         title = QLabel('''<center>
    134 <span style="font-size: x-large">Coldcard Wallet</span>
    135 <br><span style="font-size: medium">from Coinkite Inc.</span>
    136 <br><a href="https://coldcardwallet.com">coldcardwallet.com</a>''')
    137         title.setTextInteractionFlags(Qt.LinksAccessibleByMouse)
    138 
    139         grid.addWidget(title , 0,0, 1,2, Qt.AlignHCenter)
    140         y = 3
    141 
    142         rows = [
    143             ('xfp', _("Master Fingerprint")),
    144             ('serial', _("USB Serial")),
    145             ('fw_version', _("Firmware Version")),
    146             ('fw_built', _("Build Date")),
    147             ('bl_version', _("Bootloader")),
    148         ]
    149         for row_num, (member_name, label) in enumerate(rows):
    150             # XXX we know xfp already, even if not connected
    151             widget = QLabel('<tt>000000000000')
    152             widget.setTextInteractionFlags(Qt.TextSelectableByMouse | Qt.TextSelectableByKeyboard)
    153 
    154             grid.addWidget(QLabel(label), y, 0, 1,1, Qt.AlignRight)
    155             grid.addWidget(widget, y, 1, 1, 1, Qt.AlignLeft)
    156             setattr(self, member_name, widget)
    157             y += 1
    158         body_layout.addLayout(grid)
    159 
    160         upg_btn = QPushButton(_('Upgrade'))
    161         #upg_btn.setDefault(False)
    162         def _start_upgrade():
    163             thread.add(connect_and_doit, on_success=self.start_upgrade)
    164         upg_btn.clicked.connect(_start_upgrade)
    165 
    166         y += 3
    167         grid.addWidget(upg_btn, y, 0)
    168         grid.addWidget(CloseButton(self), y, 1)
    169 
    170         dialog_vbox = QVBoxLayout(self)
    171         dialog_vbox.addWidget(body)
    172 
    173         # Fetch firmware/versions values and show them.
    174         thread.add(connect_and_doit, on_success=self.show_values, on_error=self.show_placeholders)
    175 
    176     def show_placeholders(self, unclear_arg):
    177         # device missing, so hide lots of detail.
    178         self.xfp.setText('<tt>%s' % self.keystore.get_root_fingerprint())
    179         self.serial.setText('(not connected)')
    180         self.fw_version.setText('')
    181         self.fw_built.setText('')
    182         self.bl_version.setText('')
    183 
    184     def show_values(self, client):
    185 
    186         dev = client.dev
    187 
    188         self.xfp.setText('<tt>%s' % xfp2str(dev.master_fingerprint))
    189         self.serial.setText('<tt>%s' % dev.serial)
    190 
    191         # ask device for versions: allow extras for future
    192         fw_date, fw_rel, bl_rel, *rfu = client.get_version()
    193 
    194         self.fw_version.setText('<tt>%s' % fw_rel)
    195         self.fw_built.setText('<tt>%s' % fw_date)
    196         self.bl_version.setText('<tt>%s' % bl_rel)
    197 
    198     def start_upgrade(self, client):
    199         # ask for a filename (must have already downloaded it)
    200         dev = client.dev
    201 
    202         fileName = getOpenFileName(
    203             parent=self,
    204             title="Select upgraded firmware file",
    205             filter="*.dfu",
    206             config=self.window.config,
    207         )
    208         if not fileName:
    209             return
    210 
    211         from ckcc.utils import dfu_parse
    212         from ckcc.sigheader import FW_HEADER_SIZE, FW_HEADER_OFFSET, FW_HEADER_MAGIC
    213         from ckcc.protocol import CCProtocolPacker
    214         from hashlib import sha256
    215         import struct
    216 
    217         try:
    218             with open(fileName, 'rb') as fd:
    219 
    220                 # unwrap firmware from the DFU
    221                 offset, size, *ignored = dfu_parse(fd)
    222 
    223                 fd.seek(offset)
    224                 firmware = fd.read(size)
    225 
    226             hpos = FW_HEADER_OFFSET
    227             hdr = bytes(firmware[hpos:hpos + FW_HEADER_SIZE])        # needed later too
    228             magic = struct.unpack_from("<I", hdr)[0]
    229 
    230             if magic != FW_HEADER_MAGIC:
    231                 raise ValueError("Bad magic")
    232         except Exception as exc:
    233             self.window.show_error("Does not appear to be a Coldcard firmware file.\n\n%s" % exc)
    234             return
    235 
    236         # TODO: 
    237         # - detect if they are trying to downgrade; aint gonna work
    238         # - warn them about the reboot?
    239         # - length checks
    240         # - add progress local bar
    241         self.window.show_message("Ready to Upgrade.\n\nBe patient. Unit will reboot itself when complete.")
    242 
    243         def doit():
    244             dlen, _ = dev.upload_file(firmware, verify=True)
    245             assert dlen == len(firmware)
    246 
    247             # append the firmware header a second time
    248             result = dev.send_recv(CCProtocolPacker.upload(size, size+FW_HEADER_SIZE, hdr))
    249 
    250             # make it reboot into bootlaoder which might install it
    251             dev.send_recv(CCProtocolPacker.reboot())
    252 
    253         self.thread.add(doit)
    254         self.close()