from functools import partial from typing import TYPE_CHECKING, Sequence from PyQt6.QtCore import Qt from PyQt6.QtWidgets import QPushButton, QLabel, QVBoxLayout, QWidget, QGridLayout from electrum.i18n import _ from electrum.plugin import hook from electrum.wallet import Multisig_Wallet from electrum.keystore import Hardware_KeyStore from electrum.util import ChoiceItem from electrum.hw_wallet.qt import QtHandlerBase, QtPluginBase from electrum.hw_wallet.plugin import only_hook_if_libraries_available from electrum.gui.qt.wizard.wallet import WCScriptAndDerivation, WCHWXPub, WCHWUninitialized, WCHWUnlock from electrum.gui.qt.util import WindowModalDialog, CloseButton, getOpenFileName, getSaveFileName, RichLabel from electrum.gui.qt.main_window import ElectrumWindow from .coldcard import ColdcardPlugin, xfp2str if TYPE_CHECKING: from electrum.gui.qt.wizard.wallet import QENewWalletWizard CC_DEBUG = False class Plugin(ColdcardPlugin, QtPluginBase): icon_unpaired = "coldcard_unpaired.png" icon_paired = "coldcard.png" def create_handler(self, window): return Coldcard_Handler(window) def trim_file_suffix(self, path): return path.rsplit('.', 1)[0] @only_hook_if_libraries_available @hook def receive_menu(self, menu, addrs, wallet): if len(addrs) != 1: return self._add_menu_action(menu, addrs[0], wallet) @only_hook_if_libraries_available @hook def transaction_dialog_address_menu(self, menu, addr, wallet): self._add_menu_action(menu, addr, wallet) @only_hook_if_libraries_available @hook def wallet_info_buttons(self, main_window: 'ElectrumWindow', dialog): # user is about to see the "Wallet Information" dialog # - add a button if multisig wallet, and a Coldcard is a cosigner. assert isinstance(main_window, ElectrumWindow), f"{type(main_window)}" buttons = [] wallet = main_window.wallet if type(wallet) is not Multisig_Wallet: return coldcard_keystores = [ ks for ks in wallet.get_keystores() if type(ks) == self.keystore_class ] if not coldcard_keystores: # doesn't involve a Coldcard wallet, hide feature return btn_export = QPushButton(_("Export multisig for Coldcard as file")) btn_export.clicked.connect(lambda unused: self.export_multisig_setup(main_window, wallet)) buttons.append(btn_export) btn_import_usb = QPushButton(_("Export multisig to Coldcard via USB")) btn_import_usb.clicked.connect(lambda unused: self.import_multisig_wallet_to_cc(main_window, coldcard_keystores)) buttons.append(btn_import_usb) return buttons def import_multisig_wallet_to_cc(self, main_window: 'ElectrumWindow', coldcard_keystores: Sequence[Hardware_KeyStore]): from io import StringIO from ckcc.protocol import CCProtocolPacker index = main_window.query_choice( _("Please select which {} device to use:").format(self.device), [ChoiceItem(key=i, label=ks.label) for i, ks in enumerate(coldcard_keystores)] ) if index is not None: selected_keystore = coldcard_keystores[index] client = self.get_client(selected_keystore, force_pair=True, allow_user_interaction=False) if client is None: main_window.show_error("{} not connected.").format(selected_keystore.label) return wallet = main_window.wallet sio = StringIO() basename = self.trim_file_suffix(wallet.basename()) ColdcardPlugin.export_ms_wallet(wallet, sio, basename) sio.seek(0) file_len, sha = client.dev.upload_file(sio.read().encode("utf-8"), verify=True) client.dev.send_recv(CCProtocolPacker.multisig_enroll(file_len, sha)) main_window.show_message('\n'.join([ _("Wallet setup file '{}' imported successfully.").format(basename), _("Confirm import on your {} device.").format(selected_keystore.label) ])) def export_multisig_setup(self, main_window, wallet): basename = self.trim_file_suffix(wallet.basename()) name = f'{basename}-cc-export.txt'.replace(' ', '-') fileName = getSaveFileName( parent=main_window, title=_("Select where to save the setup file"), filename=name, filter="*.txt", config=self.config, ) if fileName: with open(fileName, "wt") as f: ColdcardPlugin.export_ms_wallet(wallet, f, basename) main_window.show_message(_("Wallet setup file '{}' exported successfully").format(name)) def show_settings_dialog(self, window, keystore): # When they click on the icon for CC we come here. # - doesn't matter if device not connected, continue CKCCSettingsDialog(window, self, keystore).exec() @hook def init_wallet_wizard(self, wizard: 'QENewWalletWizard'): self.extend_wizard(wizard) # insert coldcard pages in new wallet wizard def extend_wizard(self, wizard: 'QENewWalletWizard'): super().extend_wizard(wizard) views = { 'coldcard_start': {'gui': WCScriptAndDerivation}, 'coldcard_xpub': {'gui': WCHWXPub}, 'coldcard_not_initialized': {'gui': WCHWUninitialized}, 'coldcard_unlock': {'gui': WCHWUnlock} } wizard.navmap_merge(views) class Coldcard_Handler(QtHandlerBase): MESSAGE_DIALOG_TITLE = _("Coldcard Status") def __init__(self, win): super(Coldcard_Handler, self).__init__(win, 'Coldcard') class CKCCSettingsDialog(WindowModalDialog): def __init__(self, window: ElectrumWindow, plugin, keystore): title = _("{} Settings").format(plugin.device) super(CKCCSettingsDialog, self).__init__(window, title) self.setMaximumWidth(540) # Note: Coldcard may **not** be connected at present time. Keep working! devmgr = plugin.device_manager() #config = devmgr.config #handler = keystore.handler self.thread = thread = keystore.thread self.keystore = keystore assert isinstance(window, ElectrumWindow), f"{type(window)}" self.window = window def connect_and_doit(): # Attempt connection to device, or raise. device_id = plugin.choose_device(window, keystore) if not device_id: raise RuntimeError("Device not connected") 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) title = RichLabel('''