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