You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
242 lines
8.3 KiB
242 lines
8.3 KiB
import time
|
|
|
|
from electrum.i18n import _
|
|
from electrum.plugin import hook
|
|
from electrum.wallet import Standard_Wallet
|
|
from electrum.gui.qt.util import *
|
|
|
|
from .coldcard import ColdcardPlugin
|
|
from ..hw_wallet.qt import QtHandlerBase, QtPluginBase
|
|
|
|
|
|
class Plugin(ColdcardPlugin, QtPluginBase):
|
|
icon_unpaired = ":icons/coldcard_unpaired.png"
|
|
icon_paired = ":icons/coldcard.png"
|
|
|
|
def create_handler(self, window):
|
|
return Coldcard_Handler(window)
|
|
|
|
@hook
|
|
def receive_menu(self, menu, addrs, wallet):
|
|
if type(wallet) is not Standard_Wallet:
|
|
return
|
|
keystore = wallet.get_keystore()
|
|
if type(keystore) == self.keystore_class and len(addrs) == 1:
|
|
def show_address():
|
|
keystore.thread.add(partial(self.show_address, wallet, addrs[0]))
|
|
menu.addAction(_("Show on Coldcard"), show_address)
|
|
|
|
@hook
|
|
def transaction_dialog(self, dia):
|
|
# see gui/qt/transaction_dialog.py
|
|
|
|
keystore = dia.wallet.get_keystore()
|
|
if type(keystore) != self.keystore_class:
|
|
# not a Coldcard wallet, hide feature
|
|
return
|
|
|
|
# - add a new button, near "export"
|
|
btn = QPushButton(_("Save PSBT"))
|
|
btn.clicked.connect(lambda unused: self.export_psbt(dia))
|
|
if dia.tx.is_complete():
|
|
# but disable it for signed transactions (nothing to do if already signed)
|
|
btn.setDisabled(True)
|
|
|
|
dia.sharing_buttons.append(btn)
|
|
|
|
def export_psbt(self, dia):
|
|
# Called from hook in transaction dialog
|
|
tx = dia.tx
|
|
|
|
if tx.is_complete():
|
|
# if they sign while dialog is open, it can transition from unsigned to signed,
|
|
# which we don't support here, so do nothing
|
|
return
|
|
|
|
# can only expect Coldcard wallets to work with these files (right now)
|
|
keystore = dia.wallet.get_keystore()
|
|
assert type(keystore) == self.keystore_class
|
|
|
|
# convert to PSBT
|
|
raw_psbt = keystore.build_psbt(tx, wallet=dia.wallet)
|
|
|
|
name = (dia.wallet.basename() + time.strftime('-%y%m%d-%H%M.psbt')).replace(' ', '-')
|
|
fileName = dia.main_window.getSaveFileName(_("Select where to save the PSBT file"),
|
|
name, "*.psbt")
|
|
if fileName:
|
|
with open(fileName, "wb+") as f:
|
|
f.write(raw_psbt)
|
|
dia.show_message(_("Transaction exported successfully"))
|
|
dia.saved = True
|
|
|
|
def show_settings_dialog(self, window, keystore):
|
|
# When they click on the icon for CC we come here.
|
|
device_id = self.choose_device(window, keystore)
|
|
if device_id:
|
|
CKCCSettingsDialog(window, self, keystore, device_id).exec_()
|
|
|
|
|
|
class Coldcard_Handler(QtHandlerBase):
|
|
setup_signal = pyqtSignal()
|
|
#auth_signal = pyqtSignal(object)
|
|
|
|
def __init__(self, win):
|
|
super(Coldcard_Handler, self).__init__(win, 'Coldcard')
|
|
self.setup_signal.connect(self.setup_dialog)
|
|
#self.auth_signal.connect(self.auth_dialog)
|
|
|
|
|
|
def message_dialog(self, msg):
|
|
self.clear_dialog()
|
|
self.dialog = dialog = WindowModalDialog(self.top_level_window(), _("Coldcard Status"))
|
|
l = QLabel(msg)
|
|
vbox = QVBoxLayout(dialog)
|
|
vbox.addWidget(l)
|
|
dialog.show()
|
|
|
|
def get_setup(self):
|
|
self.done.clear()
|
|
self.setup_signal.emit()
|
|
self.done.wait()
|
|
return
|
|
|
|
def setup_dialog(self):
|
|
self.show_error(_('Please initialization your Coldcard while disconnected.'))
|
|
return
|
|
|
|
class CKCCSettingsDialog(WindowModalDialog):
|
|
'''This dialog doesn't require a device be paired with a wallet.
|
|
We want users to be able to wipe a device even if they've forgotten
|
|
their PIN.'''
|
|
|
|
def __init__(self, window, plugin, keystore, device_id):
|
|
title = _("{} Settings").format(plugin.device)
|
|
super(CKCCSettingsDialog, self).__init__(window, title)
|
|
self.setMaximumWidth(540)
|
|
|
|
devmgr = plugin.device_manager()
|
|
config = devmgr.config
|
|
handler = keystore.handler
|
|
self.thread = thread = keystore.thread
|
|
|
|
def connect_and_doit():
|
|
client = devmgr.client_by_id(device_id)
|
|
if not client:
|
|
raise RuntimeError("Device not connected")
|
|
return client
|
|
|
|
body = QWidget()
|
|
body_layout = QVBoxLayout(body)
|
|
grid = QGridLayout()
|
|
grid.setColumnStretch(2, 1)
|
|
|
|
# see <http://doc.qt.io/archives/qt-4.8/richtext-html-subset.html>
|
|
title = QLabel('''<center>
|
|
<span style="font-size: x-large">Coldcard Wallet</span>
|
|
<br><span style="font-size: medium">from Coinkite Inc.</span>
|
|
<br><a href="https://coldcardwallet.com">coldcardwallet.com</a>''')
|
|
title.setTextInteractionFlags(Qt.LinksAccessibleByMouse)
|
|
|
|
grid.addWidget(title , 0,0, 1,2, Qt.AlignHCenter)
|
|
y = 3
|
|
|
|
rows = [
|
|
('fw_version', _("Firmware Version")),
|
|
('fw_built', _("Build Date")),
|
|
('bl_version', _("Bootloader")),
|
|
('xfp', _("Master Fingerprint")),
|
|
('serial', _("USB Serial")),
|
|
]
|
|
for row_num, (member_name, label) in enumerate(rows):
|
|
widget = QLabel('<tt>000000000000')
|
|
widget.setTextInteractionFlags(Qt.TextSelectableByMouse | Qt.TextSelectableByKeyboard)
|
|
|
|
grid.addWidget(QLabel(label), y, 0, 1,1, Qt.AlignRight)
|
|
grid.addWidget(widget, y, 1, 1, 1, Qt.AlignLeft)
|
|
setattr(self, member_name, widget)
|
|
y += 1
|
|
body_layout.addLayout(grid)
|
|
|
|
upg_btn = QPushButton('Upgrade')
|
|
#upg_btn.setDefault(False)
|
|
def _start_upgrade():
|
|
thread.add(connect_and_doit, on_success=self.start_upgrade)
|
|
upg_btn.clicked.connect(_start_upgrade)
|
|
|
|
y += 3
|
|
grid.addWidget(upg_btn, y, 0)
|
|
grid.addWidget(CloseButton(self), y, 1)
|
|
|
|
dialog_vbox = QVBoxLayout(self)
|
|
dialog_vbox.addWidget(body)
|
|
|
|
# Fetch values and show them
|
|
thread.add(connect_and_doit, on_success=self.show_values)
|
|
|
|
def show_values(self, client):
|
|
dev = client.dev
|
|
|
|
self.xfp.setText('<tt>0x%08x' % dev.master_fingerprint)
|
|
self.serial.setText('<tt>%s' % dev.serial)
|
|
|
|
# ask device for versions: allow extras for future
|
|
fw_date, fw_rel, bl_rel, *rfu = client.get_version()
|
|
|
|
self.fw_version.setText('<tt>%s' % fw_rel)
|
|
self.fw_built.setText('<tt>%s' % fw_date)
|
|
self.bl_version.setText('<tt>%s' % bl_rel)
|
|
|
|
def start_upgrade(self, client):
|
|
# ask for a filename (must have already downloaded it)
|
|
mw = get_parent_main_window(self)
|
|
dev = client.dev
|
|
|
|
fileName = mw.getOpenFileName("Select upgraded firmware file", "*.dfu")
|
|
if not fileName:
|
|
return
|
|
|
|
from ckcc.utils import dfu_parse
|
|
from ckcc.sigheader import FW_HEADER_SIZE, FW_HEADER_OFFSET, FW_HEADER_MAGIC
|
|
from ckcc.protocol import CCProtocolPacker
|
|
from hashlib import sha256
|
|
import struct
|
|
|
|
try:
|
|
with open(fileName, 'rb') as fd:
|
|
|
|
# unwrap firmware from the DFU
|
|
offset, size, *ignored = dfu_parse(fd)
|
|
|
|
fd.seek(offset)
|
|
firmware = fd.read(size)
|
|
|
|
hpos = FW_HEADER_OFFSET
|
|
hdr = bytes(firmware[hpos:hpos + FW_HEADER_SIZE]) # needed later too
|
|
magic = struct.unpack_from("<I", hdr)[0]
|
|
|
|
if magic != FW_HEADER_MAGIC:
|
|
raise ValueError("Bad magic")
|
|
except Exception as exc:
|
|
mw.show_error("Does not appear to be a Coldcard firmware file.\n\n%s" % exc)
|
|
return
|
|
|
|
# TODO:
|
|
# - detect if they are trying to downgrade; aint gonna work
|
|
# - warn them about the reboot?
|
|
# - length checks
|
|
# - add progress local bar
|
|
mw.show_message("Ready to Upgrade.\n\nBe patient. Unit will reboot itself when complete.")
|
|
|
|
def doit():
|
|
dlen, _ = dev.upload_file(firmware, verify=True)
|
|
assert dlen == len(firmware)
|
|
|
|
# append the firmware header a second time
|
|
result = dev.send_recv(CCProtocolPacker.upload(size, size+FW_HEADER_SIZE, hdr))
|
|
|
|
# make it reboot into bootlaoder which might install it
|
|
dev.send_recv(CCProtocolPacker.reboot())
|
|
|
|
self.thread.add(doit)
|
|
self.close()
|
|
# EOF
|
|
|