diff --git a/electrum/gui/qt/my_treeview.py b/electrum/gui/qt/my_treeview.py index 36fe9753f..5f4892873 100644 --- a/electrum/gui/qt/my_treeview.py +++ b/electrum/gui/qt/my_treeview.py @@ -71,9 +71,9 @@ if TYPE_CHECKING: from .main_window import ElectrumWindow -class MyMenu(QMenu): +class QMenuWithConfig(QMenu): - def __init__(self, config): + def __init__(self, config: 'SimpleConfig'): QMenu.__init__(self) self.setToolTipsVisible(True) self.config = config @@ -113,7 +113,7 @@ class MyMenu(QMenu): def create_toolbar_with_menu(config: 'SimpleConfig', title): - menu = MyMenu(config) + menu = QMenuWithConfig(config) toolbar_button = QToolButton() toolbar_button.setIcon(read_QIcon("preferences.png")) toolbar_button.setMenu(menu) diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index 304c94601..8ceeec730 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -63,9 +63,9 @@ from .util import (MessageBoxMixin, read_QIcon, Buttons, icon_path, TRANSACTION_FILE_EXTENSION_FILTER_ONLY_COMPLETE_TX, TRANSACTION_FILE_EXTENSION_FILTER_ONLY_PARTIAL_TX, BlockingWaitingDialog, getSaveFileName, ColorSchemeItem, - get_iconname_qrcode, VLine) + get_iconname_qrcode, VLine, WaitingDialog) from .rate_limiter import rate_limited -from .my_treeview import create_toolbar_with_menu +from .my_treeview import create_toolbar_with_menu, QMenuWithConfig if TYPE_CHECKING: from .main_window import ElectrumWindow @@ -456,7 +456,7 @@ class TxDialog(QDialog, MessageBoxMixin): self.desc = self.wallet.get_label_for_txid(txid) or None self.setMinimumWidth(640) - self.psbt_only_widgets = [] # type: List[QWidget] + self.psbt_only_widgets = [] # type: List[Union[QWidget, QAction]] vbox = QVBoxLayout() self.setLayout(vbox) @@ -502,15 +502,24 @@ class TxDialog(QDialog, MessageBoxMixin): b.clicked.connect(self.close) b.setDefault(True) - self.export_actions_menu = export_actions_menu = QMenu() + self.export_actions_menu = export_actions_menu = QMenuWithConfig(config=self.config) self.add_export_actions_to_menu(export_actions_menu) export_actions_menu.addSeparator() - export_submenu = export_actions_menu.addMenu(_("For CoinJoin; strip privates")) - self.add_export_actions_to_menu(export_submenu, gettx=self._gettx_for_coinjoin) - self.psbt_only_widgets.append(export_submenu) - export_submenu = export_actions_menu.addMenu(_("For hardware device; include xpubs")) - self.add_export_actions_to_menu(export_submenu, gettx=self._gettx_for_hardware_device) - self.psbt_only_widgets.append(export_submenu) + export_option = export_actions_menu.addConfig( + self.config.cv.GUI_QT_TX_DIALOG_EXPORT_STRIP_SENSITIVE_METADATA) + self.psbt_only_widgets.append(export_option) + export_option = export_actions_menu.addConfig( + self.config.cv.GUI_QT_TX_DIALOG_EXPORT_INCLUDE_GLOBAL_XPUBS) + self.psbt_only_widgets.append(export_option) + if self.wallet.has_support_for_slip_19_ownership_proofs(): + export_option = export_actions_menu.addAction( + _('Include SLIP-19 ownership proofs'), + self._add_slip_19_ownership_proofs_to_tx) + export_option.setToolTip(_("Some cosigners (e.g. Trezor) might require this for coinjoins.")) + self._export_option_slip19 = export_option + export_option.setCheckable(True) + export_option.setChecked(False) + self.psbt_only_widgets.append(export_option) self.export_actions_button = QToolButton() self.export_actions_button.setText(_("Share")) @@ -604,9 +613,17 @@ class TxDialog(QDialog, MessageBoxMixin): # Override escape-key to close normally (and invoke closeEvent) self.close() - def add_export_actions_to_menu(self, menu: QMenu, *, gettx: Callable[[], Transaction] = None) -> None: - if gettx is None: - gettx = lambda: None + def add_export_actions_to_menu(self, menu: QMenu) -> None: + def gettx() -> Transaction: + if not isinstance(self.tx, PartialTransaction): + return self.tx + tx = copy.deepcopy(self.tx) + if self.config.GUI_QT_TX_DIALOG_EXPORT_INCLUDE_GLOBAL_XPUBS: + Network.run_from_another_thread( + tx.prepare_for_export_for_hardware_device(self.wallet)) + if self.config.GUI_QT_TX_DIALOG_EXPORT_STRIP_SENSITIVE_METADATA: + tx.prepare_for_export_for_coinjoin() + return tx action = QAction(_("Copy to clipboard"), self) action.triggered.connect(lambda: self.copy_to_clipboard(tx=gettx())) @@ -620,20 +637,19 @@ class TxDialog(QDialog, MessageBoxMixin): action.triggered.connect(lambda: self.export_to_file(tx=gettx())) menu.addAction(action) - def _gettx_for_coinjoin(self) -> PartialTransaction: - if not isinstance(self.tx, PartialTransaction): - raise Exception("Can only export partial transactions for coinjoins.") - tx = copy.deepcopy(self.tx) - tx.prepare_for_export_for_coinjoin() - return tx - - def _gettx_for_hardware_device(self) -> PartialTransaction: - if not isinstance(self.tx, PartialTransaction): - raise Exception("Can only export partial transactions for hardware device.") - tx = copy.deepcopy(self.tx) - Network.run_from_another_thread( - tx.prepare_for_export_for_hardware_device(self.wallet)) - return tx + def _add_slip_19_ownership_proofs_to_tx(self): + assert isinstance(self.tx, PartialTransaction) + def on_success(result): + self._export_option_slip19.setEnabled(False) + self.main_window.pop_top_level_window(self) + def on_failure(exc_info): + self._export_option_slip19.setChecked(False) + self.main_window.on_error(exc_info) + self.main_window.pop_top_level_window(self) + task = partial(self.wallet.add_slip_19_ownership_proofs_to_tx, self.tx) + msg = _('Adding SLIP-19 ownership proofs to transaction...') + self.main_window.push_top_level_window(self) + WaitingDialog(self, msg, task, on_success, on_failure) def copy_to_clipboard(self, *, tx: Transaction = None): if tx is None: diff --git a/electrum/keystore.py b/electrum/keystore.py index 1f2512384..40b2946b9 100644 --- a/electrum/keystore.py +++ b/electrum/keystore.py @@ -202,6 +202,12 @@ class KeyStore(Logger, ABC): def can_have_deterministic_lightning_xprv(self) -> bool: return False + def has_support_for_slip_19_ownership_proofs(self) -> bool: + return False + + def add_slip_19_ownership_proofs_to_tx(self, tx: 'PartialTransaction', *, password) -> None: + raise NotImplementedError() + class Software_KeyStore(KeyStore): diff --git a/electrum/plugins/trezor/clientbase.py b/electrum/plugins/trezor/clientbase.py index ab6422b00..a7c92f026 100644 --- a/electrum/plugins/trezor/clientbase.py +++ b/electrum/plugins/trezor/clientbase.py @@ -260,6 +260,16 @@ class TrezorClientBase(HardwareClientBase, Logger): with self.run_flow(): return trezorlib.btc.sign_tx(self.client, *args, **kwargs) + @runs_in_hwd_thread + def get_ownership_id(self, *args, **kwargs): + with self.run_flow(): + return trezorlib.btc.get_ownership_id(self.client, *args, **kwargs) + + @runs_in_hwd_thread + def get_ownership_proof(self, *args, **kwargs): + with self.run_flow(): + return trezorlib.btc.get_ownership_proof(self.client, *args, **kwargs) + @runs_in_hwd_thread def reset_device(self, *args, **kwargs): with self.run_flow(): diff --git a/electrum/plugins/trezor/trezor.py b/electrum/plugins/trezor/trezor.py index cd9a884c4..c1bf02c96 100644 --- a/electrum/plugins/trezor/trezor.py +++ b/electrum/plugins/trezor/trezor.py @@ -34,6 +34,8 @@ try: TxInputType, TxOutputType, TxOutputBinType, TransactionType, AmountUnit) from trezorlib.client import PASSPHRASE_ON_DEVICE + import trezorlib.log + #trezorlib.log.enable_debug_output() TREZORLIB = True except Exception as e: @@ -95,6 +97,39 @@ class TrezorKeyStore(Hardware_KeyStore): self.plugin.sign_transaction(self, tx, prev_tx) + def has_support_for_slip_19_ownership_proofs(self) -> bool: + return True + + def add_slip_19_ownership_proofs_to_tx(self, tx: 'PartialTransaction', password) -> None: + assert isinstance(tx, PartialTransaction) + client = self.get_client() + assert isinstance(client, TrezorClientBase), client + for txin in tx.inputs(): + if txin.is_coinbase_input(): + continue + if txin.is_complete() or not txin.is_mine: + continue + assert txin.scriptpubkey + desc = txin.script_descriptor + assert desc + trezor_multisig = None + if multi := desc.get_simple_multisig(): + # trezor_multisig = self._make_multisig(multi) + raise Exception("multisig not supported for slip-19 ownership proof") + trezor_script_type = self.plugin.get_trezor_input_script_type(desc.to_legacy_electrum_script_type()) + my_pubkey, full_path = self.find_my_pubkey_in_txinout(txin) + if full_path: + trezor_address_n = full_path + else: + continue + proof, _proof_sig = client.get_ownership_proof( + coin_name=self.plugin.get_coin_name(), + n=trezor_address_n, + multisig=trezor_multisig, + script_type=trezor_script_type, + ) + txin.slip_19_ownership_proof = proof + class TrezorInitSettings(NamedTuple): word_count: int @@ -352,11 +387,13 @@ class TrezorPlugin(HW_PluginBase): assert isinstance(tx, PartialTransaction) assert isinstance(txin, PartialTxInput) assert keystore - if txin.is_complete() or not txin.is_mine: + if txin.is_complete() or not txin.is_mine: # we don't sign txinputtype.script_type = InputScriptType.EXTERNAL assert txin.scriptpubkey txinputtype.script_pubkey = txin.scriptpubkey - else: + if not txin.is_mine and txin.slip_19_ownership_proof: + txinputtype.ownership_proof = txin.slip_19_ownership_proof + else: # we sign desc = txin.script_descriptor assert desc if multi := desc.get_simple_multisig(): diff --git a/electrum/simple_config.py b/electrum/simple_config.py index e3b410e6a..36b472f77 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -1083,6 +1083,14 @@ This will result in longer routes; it might increase your fees and decrease the 'Download parent transactions from the network.\n' 'Allows filling in missing fee and input details.'), ) + GUI_QT_TX_DIALOG_EXPORT_STRIP_SENSITIVE_METADATA = ConfigVar( + 'gui_qt_tx_dialog_export_strip_sensitive_metadata', default=False, type_=bool, + short_desc=lambda: _('For CoinJoin; strip privates'), + ) + GUI_QT_TX_DIALOG_EXPORT_INCLUDE_GLOBAL_XPUBS = ConfigVar( + 'gui_qt_tx_dialog_export_include_global_xpubs', default=False, type_=bool, + short_desc=lambda: _('For hardware device; include xpubs'), + ) GUI_QT_RECEIVE_TABS_INDEX = ConfigVar('receive_tabs_index', default=0, type_=int) GUI_QT_RECEIVE_TAB_QR_VISIBLE = ConfigVar('receive_qr_visible', default=False, type_=bool) GUI_QT_TX_EDITOR_SHOW_IO = ConfigVar( diff --git a/electrum/transaction.py b/electrum/transaction.py index 3063586a8..62d1af0b8 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -1289,6 +1289,7 @@ class PSBTInputType(IntEnum): BIP32_DERIVATION = 6 FINAL_SCRIPTSIG = 7 FINAL_SCRIPTWITNESS = 8 + SLIP19_OWNERSHIP_PROOF = 0x19 class PSBTOutputType(IntEnum): @@ -1386,6 +1387,7 @@ class PartialTxInput(TxInput, PSBTSection): self.bip32_paths = {} # type: Dict[bytes, Tuple[bytes, Sequence[int]]] # pubkey -> (xpub_fingerprint, path) self.redeem_script = None # type: Optional[bytes] self.witness_script = None # type: Optional[bytes] + self.slip_19_ownership_proof = None # type: Optional[bytes] self._unknown = {} # type: Dict[bytes, bytes] self._script_descriptor = None # type: Optional[Descriptor] @@ -1439,6 +1441,7 @@ class PartialTxInput(TxInput, PSBTSection): 'part_sigs': {pubkey.hex(): sig.hex() for pubkey, sig in self.part_sigs.items()}, 'bip32_paths': {pubkey.hex(): (xfp.hex(), bip32.convert_bip32_intpath_to_strpath(path)) for pubkey, (xfp, path) in self.bip32_paths.items()}, + 'slip_19_ownership_proof': self.slip_19_ownership_proof.hex() if self.slip_19_ownership_proof else None, 'unknown_psbt_fields': {key.hex(): val.hex() for key, val in self._unknown.items()}, }) return d @@ -1553,6 +1556,11 @@ class PartialTxInput(TxInput, PSBTSection): raise SerializationError(f"duplicate key: {repr(kt)}") self.witness = val if key: raise SerializationError(f"key for {repr(kt)} must be empty") + elif kt == PSBTInputType.SLIP19_OWNERSHIP_PROOF: + if self.slip_19_ownership_proof is not None: + raise SerializationError(f"duplicate key: {repr(kt)}") + self.slip_19_ownership_proof = val + if key: raise SerializationError(f"key for {repr(kt)} must be empty") else: full_key = self.get_fullkey_from_keytype_and_key(kt, key) if full_key in self._unknown: @@ -1579,6 +1587,8 @@ class PartialTxInput(TxInput, PSBTSection): wr(PSBTInputType.FINAL_SCRIPTSIG, self.script_sig) if self.witness is not None: wr(PSBTInputType.FINAL_SCRIPTWITNESS, self.witness) + if self.slip_19_ownership_proof: + wr(PSBTInputType.SLIP19_OWNERSHIP_PROOF, self.slip_19_ownership_proof) for full_key, val in sorted(self._unknown.items()): key_type, key = self.get_keytype_and_key_from_fullkey(full_key) wr(key_type, val, key=key) diff --git a/electrum/wallet.py b/electrum/wallet.py index d5a5f29f8..5e2fb82bf 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -2447,6 +2447,12 @@ class Abstract_Wallet(ABC, Logger, EventListener): self._add_txinout_derivation_info(txin, address, only_der_suffix=only_der_suffix) txin.block_height = self.adb.get_tx_height(txin.prevout.txid.hex()).height + def has_support_for_slip_19_ownership_proofs(self) -> bool: + return False + + def add_slip_19_ownership_proofs_to_tx(self, tx: PartialTransaction) -> None: + raise NotImplementedError() + def get_script_descriptor_for_address(self, address: str) -> Optional[Descriptor]: if not self.is_mine(address): return None @@ -3765,6 +3771,13 @@ class Standard_Wallet(Simple_Wallet, Deterministic_Wallet): pubkey = pubkeys[0] return bitcoin.pubkey_to_address(self.txin_type, pubkey) + def has_support_for_slip_19_ownership_proofs(self) -> bool: + return self.keystore.has_support_for_slip_19_ownership_proofs() + + def add_slip_19_ownership_proofs_to_tx(self, tx: PartialTransaction) -> None: + tx.add_info_from_wallet(self) + self.keystore.add_slip_19_ownership_proofs_to_tx(tx=tx, password=None) + class Multisig_Wallet(Deterministic_Wallet): # generic m of n