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