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