From 6566f2f0a4c27bf535542ae5799aae0731799821 Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 28 Apr 2025 16:28:32 +0200 Subject: [PATCH 1/2] qt: pass Invoice object to transaction dialog when appropriate. This is purely informational and optional, with the main immediate use to provide the invoice description/message/label to the transaction dialog, so it can be stored when saving the tx in history, or passed along with PSBTs sent to cosigners. Before, the tx description was not saved in history when an invoice was not saved before signing and saving the tx for sending later. --- electrum/gui/qt/main_window.py | 16 ++++++------ electrum/gui/qt/send_tab.py | 37 ++++++++++++--------------- electrum/gui/qt/transaction_dialog.py | 34 ++++++++++++++---------- 3 files changed, 45 insertions(+), 42 deletions(-) diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index edf9cba3d..e70b0da20 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1169,7 +1169,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): tx: Transaction, *, external_keypairs: Mapping[bytes, bytes] = None, - payment_identifier: PaymentIdentifier = None, + invoice: Invoice = None, show_sign_button: bool = True, show_broadcast_button: bool = True, ): @@ -1177,7 +1177,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): tx, parent=self, external_keypairs=external_keypairs, - payment_identifier=payment_identifier, + invoice=invoice, show_sign_button=show_sign_button, show_broadcast_button=show_broadcast_button, ) @@ -1413,18 +1413,18 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): """ return self.utxo_list.get_spend_list() - def broadcast_or_show(self, tx: Transaction, *, payment_identifier: PaymentIdentifier = None): + def broadcast_or_show(self, tx: Transaction, *, invoice: 'Invoice' = None): if not tx.is_complete(): - self.show_transaction(tx, payment_identifier=payment_identifier) + self.show_transaction(tx, invoice=invoice) return if not self.network: self.show_error(_("You can't broadcast a transaction without a live network connection.")) - self.show_transaction(tx, payment_identifier=payment_identifier) + self.show_transaction(tx, invoice=invoice) return - self.broadcast_transaction(tx, payment_identifier=payment_identifier) + self.broadcast_transaction(tx, invoice=invoice) - def broadcast_transaction(self, tx: Transaction, *, payment_identifier: PaymentIdentifier = None): - self.send_tab.broadcast_transaction(tx, payment_identifier=payment_identifier) + def broadcast_transaction(self, tx: Transaction, *, invoice: Invoice = None): + self.send_tab.broadcast_transaction(tx, invoice=invoice) @protected def sign_tx( diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index 583749525..2817784bf 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -305,10 +305,6 @@ class SendTab(QWidget, MessageBoxMixin, Logger): if run_hook('abort_send', self): return - payment_identifier = None - if invoice and invoice.bip70: - payment_identifier = payment_identifier_from_invoice(self.wallet, invoice) - is_sweep = bool(external_keypairs) # we call get_coins inside make_tx, so that inputs can be changed dynamically if get_coins is None: @@ -354,12 +350,12 @@ class SendTab(QWidget, MessageBoxMixin, Logger): tx.swap_payment_hash = swap.payment_hash if is_preview: - self.window.show_transaction(tx, external_keypairs=external_keypairs, payment_identifier=payment_identifier) + self.window.show_transaction(tx, external_keypairs=external_keypairs, invoice=invoice) return self.save_pending_invoice() def sign_done(success): if success: - self.window.broadcast_or_show(tx, payment_identifier=payment_identifier) + self.window.broadcast_or_show(tx, invoice=invoice) self.window.sign_tx( tx, callback=sign_done, @@ -712,7 +708,7 @@ class SendTab(QWidget, MessageBoxMixin, Logger): chan, swap_recv_amount_sat = can_pay_with_swap self.window.run_swap_dialog(is_reverse=False, recv_amount_sat=swap_recv_amount_sat, channels=[chan]) elif r == 'onchain': - self.pay_onchain_dialog(invoice.get_outputs(), nonlocal_only=True) + self.pay_onchain_dialog(invoice.get_outputs(), nonlocal_only=True, invoice=invoice) return assert lnworker is not None @@ -725,9 +721,7 @@ class SendTab(QWidget, MessageBoxMixin, Logger): coro = lnworker.pay_invoice(invoice, amount_msat=amount_msat) self.window.run_coroutine_from_thread(coro, _('Sending payment')) - def broadcast_transaction(self, tx: Transaction, *, payment_identifier: PaymentIdentifier = None): - # note: payment_identifier is explicitly passed as self.payto_e.payment_identifier might - # already be cleared or otherwise have changed. + def broadcast_transaction(self, tx: Transaction, *, invoice: Invoice = None): if hasattr(tx, 'swap_payment_hash'): sm = self.wallet.lnworker.swap_manager swap = sm.get_swap(tx.swap_payment_hash) @@ -742,7 +736,7 @@ class SendTab(QWidget, MessageBoxMixin, Logger): def broadcast_thread(): # non-GUI thread - if payment_identifier and payment_identifier.has_expired(): + if invoice and invoice.has_expired(): return False, _("Invoice has expired") try: self.network.run_from_another_thread(self.network.broadcast_transaction(tx)) @@ -751,19 +745,22 @@ class SendTab(QWidget, MessageBoxMixin, Logger): except BestEffortRequestFailed as e: return False, repr(e) # success - txid = tx.txid() - if payment_identifier and payment_identifier.need_merchant_notify(): - refund_address = self.wallet.get_receiving_address() - payment_identifier.notify_merchant( - tx=tx, - refund_address=refund_address, - on_finished=self.notify_merchant_done_signal.emit - ) - return True, txid + if invoice and invoice.bip70: + payment_identifier = payment_identifier_from_invoice(invoice) + # FIXME: this should move to backend + if payment_identifier and payment_identifier.need_merchant_notify(): + refund_address = self.wallet.get_receiving_address() + payment_identifier.notify_merchant( + tx=tx, + refund_address=refund_address, + on_finished=self.notify_merchant_done_signal.emit + ) + return True, tx.txid() # Capture current TL window; override might be removed on return parent = self.window.top_level_window(lambda win: isinstance(win, MessageBoxMixin)) + # FIXME: move to backend and let Abstract_Wallet set broadcasting state, not gui self.wallet.set_broadcasting(tx, broadcasting_status=PR_BROADCASTING) def broadcast_done(result): diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index dfbfc6684..79312087d 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -64,7 +64,7 @@ from .my_treeview import create_toolbar_with_menu, QMenuWithConfig if TYPE_CHECKING: from .main_window import ElectrumWindow from electrum.wallet import Abstract_Wallet - from electrum.payment_identifier import PaymentIdentifier + from electrum.invoices import Invoice _logger = get_logger(__name__) @@ -418,7 +418,7 @@ def show_transaction( parent: 'ElectrumWindow', prompt_if_unsaved: bool = False, external_keypairs: Mapping[bytes, bytes] = None, - payment_identifier: 'PaymentIdentifier' = None, + invoice: 'Invoice' = None, on_closed: Callable[[], None] = None, show_sign_button: bool = True, show_broadcast_button: bool = True, @@ -429,7 +429,7 @@ def show_transaction( parent=parent, prompt_if_unsaved=prompt_if_unsaved, external_keypairs=external_keypairs, - payment_identifier=payment_identifier, + invoice=invoice, on_closed=on_closed, ) if not show_sign_button: @@ -454,7 +454,7 @@ class TxDialog(QDialog, MessageBoxMixin): parent: 'ElectrumWindow', prompt_if_unsaved: bool, external_keypairs: Mapping[bytes, bytes] = None, - payment_identifier: 'PaymentIdentifier' = None, + invoice: 'Invoice' = None, on_closed: Callable[[], None] = None, ): '''Transactions in the wallet will show their description. @@ -467,13 +467,15 @@ class TxDialog(QDialog, MessageBoxMixin): self.main_window = parent self.config = parent.config self.wallet = parent.wallet - self.payment_identifier = payment_identifier + self.invoice = invoice self.prompt_if_unsaved = prompt_if_unsaved self.on_closed = on_closed self.saved = False self.desc = None if txid := tx.txid(): self.desc = self.wallet.get_label_for_txid(txid) or None + if not self.desc and self.invoice: + self.desc = self.invoice.get_message() self.setMinimumWidth(640) self.psbt_only_widgets = [] # type: List[Union[QWidget, QAction]] @@ -492,13 +494,8 @@ class TxDialog(QDialog, MessageBoxMixin): self.tx_desc_label = QLabel(_("Description:")) vbox.addWidget(self.tx_desc_label) self.tx_desc = ButtonsLineEdit('') - def on_edited(): - text = self.tx_desc.text() - if self.wallet.set_label(txid, text): - self.main_window.history_list.update() - self.main_window.utxo_list.update() - self.main_window.labels_changed_signal.emit() - self.tx_desc.editingFinished.connect(on_edited) + + self.tx_desc.editingFinished.connect(self.store_tx_label) self.tx_desc.addCopyButton() vbox.addWidget(self.tx_desc) @@ -579,6 +576,13 @@ class TxDialog(QDialog, MessageBoxMixin): self.update() self.set_title() + def store_tx_label(self): + text = self.tx_desc.text() + if self.wallet.set_label(self.tx.txid(), text): + self.main_window.history_list.update() + self.main_window.utxo_list.update() + self.main_window.labels_changed_signal.emit() + def set_tx(self, tx: 'Transaction'): # Take a copy; it might get updated in the main window by # e.g. the FX plugin. If this happens during or after a long @@ -607,7 +611,7 @@ class TxDialog(QDialog, MessageBoxMixin): self.main_window.push_top_level_window(self) self.main_window.send_tab.save_pending_invoice() try: - self.main_window.broadcast_transaction(self.tx, payment_identifier=self.payment_identifier) + self.main_window.broadcast_transaction(self.tx, invoice=self.invoice) finally: self.main_window.pop_top_level_window(self) self.saved = True @@ -722,6 +726,7 @@ class TxDialog(QDialog, MessageBoxMixin): def save(self): self.main_window.push_top_level_window(self) if self.main_window.save_transaction_into_wallet(self.tx): + self.store_tx_label() self.save_button.setDisabled(True) self.saved = True self.main_window.pop_top_level_window(self) @@ -851,7 +856,8 @@ class TxDialog(QDialog, MessageBoxMixin): # note: when not finalized, RBF and locktime changes do not trigger # a make_tx, so the txid is unreliable, hence: self.tx_hash_e.setText(_('Unknown')) - if not self.wallet.adb.get_transaction(txid): + tx_in_db = bool(self.wallet.adb.get_transaction(txid)) + if not desc and not tx_in_db: self.tx_desc.hide() self.tx_desc_label.hide() else: From a9213c4d66657c305a61153e02be1a6ce7f10c4a Mon Sep 17 00:00:00 2001 From: Sander van Grieken Date: Mon, 28 Apr 2025 12:29:53 +0200 Subject: [PATCH 2/2] psbt_nostr: send label along with PSBT --- .../gui/qml/components/ExportTxDialog.qml | 1 + .../gui/qml/components/WalletMainView.qml | 3 +- electrum/gui/qml/qetxdetails.py | 11 +++-- electrum/gui/qml/qetxfinalizer.py | 3 +- electrum/plugins/psbt_nostr/psbt_nostr.py | 46 +++++++++++++------ electrum/plugins/psbt_nostr/qml.py | 15 +++--- electrum/plugins/psbt_nostr/qml/main.qml | 27 +++++++---- electrum/plugins/psbt_nostr/qt.py | 30 +++++++----- 8 files changed, 88 insertions(+), 48 deletions(-) diff --git a/electrum/gui/qml/components/ExportTxDialog.qml b/electrum/gui/qml/components/ExportTxDialog.qml index 62b4291bf..249f07c18 100644 --- a/electrum/gui/qml/components/ExportTxDialog.qml +++ b/electrum/gui/qml/components/ExportTxDialog.qml @@ -13,6 +13,7 @@ ElDialog { // if text_qr is undefined text will be used property string text_help property string text_warn + property string tx_label title: qsTr('Share Transaction') diff --git a/electrum/gui/qml/components/WalletMainView.qml b/electrum/gui/qml/components/WalletMainView.qml index 5f8747c27..e17b4efae 100644 --- a/electrum/gui/qml/components/WalletMainView.qml +++ b/electrum/gui/qml/components/WalletMainView.qml @@ -98,7 +98,8 @@ Item { ? '' : [qsTr('Warning: Some data (prev txs / "full utxos") was left out of the QR code as it would not fit.'), qsTr('This might cause issues if signing offline.'), - qsTr('As a workaround, copy to clipboard or use the Share option instead.')].join(' ') + qsTr('As a workaround, copy to clipboard or use the Share option instead.')].join(' '), + tx_label: data[3] }) dialog.open() } diff --git a/electrum/gui/qml/qetxdetails.py b/electrum/gui/qml/qetxdetails.py index 5ddad940c..9f0a2ac61 100644 --- a/electrum/gui/qml/qetxdetails.py +++ b/electrum/gui/qml/qetxdetails.py @@ -383,9 +383,11 @@ class QETxDetails(QObject, QtEventListener): self.detailsChanged.emit() - if self._label != txinfo.label: - self._label = txinfo.label - self.labelChanged.emit() + if self._txid: + label = self._wallet.wallet.get_label_for_txid(self._txid) + if self._label != label: + self._label = label + self.labelChanged.emit() def update_mined_status(self, tx_mined_info: TxMinedInfo): self._mempool_depth = '' @@ -505,4 +507,5 @@ class QETxDetails(QObject, QtEventListener): @pyqtSlot(result='QVariantList') def getSerializedTx(self): txqr = self._tx.to_qr_data() - return [str(self._tx), txqr[0], txqr[1]] + label = self._wallet.wallet.get_label_for_txid(self._tx.txid()) + return [str(self._tx), txqr[0], txqr[1], label] diff --git a/electrum/gui/qml/qetxfinalizer.py b/electrum/gui/qml/qetxfinalizer.py index 2223dea80..3908c7e31 100644 --- a/electrum/gui/qml/qetxfinalizer.py +++ b/electrum/gui/qml/qetxfinalizer.py @@ -494,7 +494,8 @@ class QETxFinalizer(TxFeeSlider): @pyqtSlot(result='QVariantList') def getSerializedTx(self): txqr = self._tx.to_qr_data() - return [str(self._tx), txqr[0], txqr[1]] + label = self._wallet.wallet.get_label_for_txid(self._tx.txid()) + return [str(self._tx), txqr[0], txqr[1], label] class TxMonMixin(QtEventListener): diff --git a/electrum/plugins/psbt_nostr/psbt_nostr.py b/electrum/plugins/psbt_nostr/psbt_nostr.py index 0be6ab995..5fcbd7937 100644 --- a/electrum/plugins/psbt_nostr/psbt_nostr.py +++ b/electrum/plugins/psbt_nostr/psbt_nostr.py @@ -23,6 +23,7 @@ # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. import asyncio +import json import ssl import time from contextlib import asynccontextmanager @@ -30,7 +31,7 @@ from contextlib import asynccontextmanager import electrum_ecc as ecc import electrum_aionostr as aionostr from electrum_aionostr.key import PrivateKey -from typing import Dict, TYPE_CHECKING, Union, List, Tuple, Optional +from typing import Dict, TYPE_CHECKING, Union, List, Tuple, Optional, Callable from electrum import util, Transaction from electrum.crypto import sha256 @@ -38,8 +39,9 @@ from electrum.i18n import _ from electrum.logging import Logger from electrum.plugin import BasePlugin from electrum.transaction import PartialTransaction, tx_from_any -from electrum.util import (log_exceptions, OldTaskGroup, ca_path, trigger_callback, event_listener, - make_aiohttp_proxy_connector) +from electrum.util import ( + log_exceptions, OldTaskGroup, ca_path, trigger_callback, event_listener, json_decode, make_aiohttp_proxy_connector +) from electrum.wallet import Multisig_Wallet if TYPE_CHECKING: @@ -165,11 +167,11 @@ class CosignerWallet(Logger): yield manager @log_exceptions - async def send_direct_messages(self, messages: List[Tuple[str, str]]): + async def send_direct_messages(self, messages: List[Tuple[str, dict]]): our_private_key: PrivateKey = aionostr.key.PrivateKey(bytes.fromhex(self.nostr_privkey)) async with self.nostr_manager() as manager: for pubkey, msg in messages: - encrypted_msg: str = our_private_key.encrypt_message(msg, pubkey) + encrypted_msg: str = our_private_key.encrypt_message(json.dumps(msg), pubkey) eid = await aionostr._add_event( manager, kind=NOSTR_EVENT_KIND, @@ -206,13 +208,16 @@ class CosignerWallet(Logger): self.known_events[event.id] = now() continue try: - tx = tx_from_any(message) + message = json_decode(message) + tx_hex = message.get('tx') + label = message.get('label', '') + tx = tx_from_any(tx_hex) except Exception as e: self.logger.info(_("Unable to deserialize the transaction:") + "\n" + str(e)) self.known_events[event.id] = now() continue self.logger.info(f"received PSBT from {event.pubkey}") - trigger_callback('psbt_nostr_received', self.wallet, event.pubkey, event.id, tx) + trigger_callback('psbt_nostr_received', self.wallet, event.pubkey, event.id, tx, label) await self.pending.wait() self.pending.clear() @@ -242,25 +247,34 @@ class CosignerWallet(Logger): self.known_events[event_id] = now() self.pending.set() - def prepare_messages(self, tx: Union[Transaction, PartialTransaction]) -> List[Tuple[str, str]]: + def prepare_messages(self, tx: Union[Transaction, PartialTransaction], label: str = None) -> List[Tuple[str, dict]]: messages = [] for xpub, pubkey in self.cosigner_list: if not self.cosigner_can_sign(tx, xpub): continue - raw_tx_bytes = tx.serialize_as_bytes() - messages.append((pubkey, raw_tx_bytes.hex())) + payload = {'tx': tx.serialize_as_bytes().hex()} + if label: + payload['label'] = label + messages.append((pubkey, payload)) return messages - def send_psbt(self, tx: Union[Transaction, PartialTransaction]): - self.do_send(self.prepare_messages(tx), tx.txid()) + def send_psbt(self, tx: Union[Transaction, PartialTransaction], label: str): + self.do_send(self.prepare_messages(tx, label), tx.txid()) - def do_send(self, messages: List[Tuple[str, str]], txid: Optional[str] = None): + def do_send(self, messages: List[Tuple[str, dict]], txid: Optional[str] = None): raise NotImplementedError() - def on_receive(self, pubkey, event_id, tx): + def on_receive(self, pubkey, event_id, tx, label: str): raise NotImplementedError() - def add_transaction_to_wallet(self, tx, *, on_failure=None, on_success=None): + def add_transaction_to_wallet( + self, + tx: Union['Transaction', 'PartialTransaction'], + *, + label: str = None, + on_failure: Callable = None, + on_success: Callable = None + ) -> None: try: # TODO: adding tx should be handled more gracefully here: # 1) don't replace tx with same tx with less signatures @@ -269,6 +283,8 @@ class CosignerWallet(Logger): if not self.wallet.adb.add_transaction(tx): # TODO: instead of bool return value, we could use specific fail reason exceptions here raise Exception('transaction was not added') + if label: + self.wallet.set_label(tx.txid(), label) except Exception as e: if on_failure: on_failure(str(e)) diff --git a/electrum/plugins/psbt_nostr/qml.py b/electrum/plugins/psbt_nostr/qml.py index 5922eae28..46d4ba2fd 100644 --- a/electrum/plugins/psbt_nostr/qml.py +++ b/electrum/plugins/psbt_nostr/qml.py @@ -50,7 +50,7 @@ class QReceiveSignalObject(QObject): QObject.__init__(self) self._plugin = plugin - cosignerReceivedPsbt = pyqtSignal(str, str, str) + cosignerReceivedPsbt = pyqtSignal(str, str, str, str) sendPsbtFailed = pyqtSignal(str, arguments=['reason']) sendPsbtSuccess = pyqtSignal() @@ -66,11 +66,12 @@ class QReceiveSignalObject(QObject): return cosigner_wallet.can_send_psbt(tx_from_any(tx, deserialize=True)) @pyqtSlot(QEWallet, str) - def sendPsbt(self, wallet: 'QEWallet', tx: str): + @pyqtSlot(QEWallet, str, str) + def sendPsbt(self, wallet: 'QEWallet', tx: str, label: str = None): cosigner_wallet = self._plugin.cosigner_wallets.get(wallet.wallet) if not cosigner_wallet: return - cosigner_wallet.send_psbt(tx_from_any(tx, deserialize=True)) + cosigner_wallet.send_psbt(tx_from_any(tx, deserialize=True), label) @pyqtSlot(QEWallet, str) def acceptPsbt(self, wallet: 'QEWallet', event_id: str): @@ -126,20 +127,20 @@ class QmlCosignerWallet(EventListener, CosignerWallet): self.user_prompt_cooldown = None @event_listener - def on_event_psbt_nostr_received(self, wallet, pubkey, event_id, tx: 'PartialTransaction'): + def on_event_psbt_nostr_received(self, wallet, pubkey, event_id, tx: 'PartialTransaction', label: str): if self.wallet == wallet: self.tx = tx if not (self.user_prompt_cooldown and self.user_prompt_cooldown > now()): - self.plugin.so.cosignerReceivedPsbt.emit(pubkey, event_id, tx.serialize()) + self.plugin.so.cosignerReceivedPsbt.emit(pubkey, event_id, tx.serialize(), label) else: self.mark_pending_event_rcvd(event_id) - self.add_transaction_to_wallet(self.tx, on_failure=self.on_add_fail) + self.add_transaction_to_wallet(self.tx, label=label, on_failure=self.on_add_fail) def close(self): super().close() self.unregister_callbacks() - def do_send(self, messages: List[Tuple[str, str]], txid: Optional[str] = None): + def do_send(self, messages: List[Tuple[str, dict]], txid: Optional[str] = None): if not messages: return coro = self.send_direct_messages(messages) diff --git a/electrum/plugins/psbt_nostr/qml/main.qml b/electrum/plugins/psbt_nostr/qml/main.qml index 7df6d6aed..8067d105d 100644 --- a/electrum/plugins/psbt_nostr/qml/main.qml +++ b/electrum/plugins/psbt_nostr/qml/main.qml @@ -7,13 +7,16 @@ import "../../../gui/qml/components/controls" Item { Connections { target: AppController ? AppController.plugin('psbt_nostr') : null - function onCosignerReceivedPsbt(pubkey, event, tx) { + function onCosignerReceivedPsbt(pubkey, event, tx, label) { var dialog = app.messageDialog.createObject(app, { text: [ - qsTr('A transaction was received from your cosigner.'), + label + ? qsTr('A transaction was received from your cosigner with label:

%1').arg(label) + : qsTr('A transaction was received from your cosigner.'), qsTr('Do you want to open it now?') - ].join('\n'), - yesno: true + ].join('

'), + yesno: true, + richText: true }) dialog.accepted.connect(function () { var page = app.stack.push(Qt.resolvedUrl('../../../gui/qml/components/TxDetails.qml'), { @@ -40,16 +43,24 @@ Item { onClicked: { console.log('about to psbt nostr send') psbt_nostr_send_button.enabled = false - AppController.plugin('psbt_nostr').sendPsbt(Daemon.currentWallet, dialog.text) + AppController.plugin('psbt_nostr').sendPsbt(Daemon.currentWallet, dialog.text, dialog.tx_label) } Connections { target: AppController ? AppController.plugin('psbt_nostr') : null + function onSendPsbtSuccess() { + dialog.close() + var msgdialog = app.messageDialog.createObject(app, { + text: qsTr('PSBT sent successfully') + }) + msgdialog.open() + } function onSendPsbtFailed(message) { psbt_nostr_send_button.enabled = true - var dialog = app.messageDialog.createObject(app, { - text: qsTr('Sending PSBT to co-signer failed:\n%1').arg(message) + var msgdialog = app.messageDialog.createObject(app, { + text: qsTr('Sending PSBT to co-signer failed:\n%1').arg(message), + iconSource: Qt.resolvedUrl('../../../gui/icons/warning.png') }) - dialog.open() + msgdialog.open() } } diff --git a/electrum/plugins/psbt_nostr/qt.py b/electrum/plugins/psbt_nostr/qt.py index e18f31af5..b86c13e64 100644 --- a/electrum/plugins/psbt_nostr/qt.py +++ b/electrum/plugins/psbt_nostr/qt.py @@ -24,7 +24,7 @@ # SOFTWARE. import asyncio from functools import partial -from typing import TYPE_CHECKING, List, Tuple, Optional +from typing import TYPE_CHECKING, List, Tuple, Optional, Union from PyQt6.QtCore import QObject, pyqtSignal from PyQt6.QtWidgets import QPushButton, QMessageBox @@ -39,11 +39,12 @@ from electrum.gui.qt.util import read_QIcon_from_bytes from .psbt_nostr import PsbtNostrPlugin, CosignerWallet if TYPE_CHECKING: + from electrum.transaction import Transaction, PartialTransaction from electrum.gui.qt.main_window import ElectrumWindow class QReceiveSignalObject(QObject): - cosignerReceivedPsbt = pyqtSignal(str, str, object) + cosignerReceivedPsbt = pyqtSignal(str, str, object, str) class Plugin(PsbtNostrPlugin): @@ -71,7 +72,7 @@ class Plugin(PsbtNostrPlugin): d.cosigner_send_button = b = QPushButton(_("Send to cosigner")) icon = read_QIcon_from_bytes(self.read_file("nostr_multisig.png")) b.setIcon(icon) - b.clicked.connect(lambda: cw.send_to_cosigners(d.tx)) + b.clicked.connect(lambda: cw.send_to_cosigners(d.tx, d.desc)) d.buttons.insert(0, b) b.setVisible(False) @@ -100,11 +101,11 @@ class QtCosignerWallet(EventListener, CosignerWallet): if self.wallet == wallet: self.obj.cosignerReceivedPsbt.emit(*args) # put on UI thread via signal - def send_to_cosigners(self, tx): - self.add_transaction_to_wallet(tx, on_failure=self.on_add_fail) - self.send_psbt(tx) + def send_to_cosigners(self, tx: Union['Transaction', 'PartialTransaction'], label: str): + self.add_transaction_to_wallet(tx, label=label, on_failure=self.on_add_fail) + self.send_psbt(tx, label) - def do_send(self, messages: List[Tuple[str, str]], txid: Optional[str] = None): + def do_send(self, messages: List[Tuple[str, dict]], txid: Optional[str] = None): if not messages: return coro = self.send_direct_messages(messages) @@ -122,21 +123,26 @@ class QtCosignerWallet(EventListener, CosignerWallet): self.window.show_message( _("Your transaction was sent to your cosigners via Nostr.") + '\n\n' + txid) - def on_receive(self, pubkey, event_id, tx): - msg = _("A transaction was received from your cosigner ({}).").format(str(event_id)[0:8]) + '\n' + \ - _("Do you want to open it now?") - result = self.window.show_message(msg, icon=QMessageBox.Icon.Question, buttons=[ + def on_receive(self, pubkey, event_id, tx, label): + msg = '
'.join([ + _("A transaction was received from your cosigner.") if not label else + _("A transaction was received from your cosigner with label:
{}
").format(label), + _("Do you want to open it now?") + ]) + result = self.window.show_message(msg, rich_text=True, icon=QMessageBox.Icon.Question, buttons=[ QMessageBox.StandardButton.Open, (QPushButton('Discard'), QMessageBox.ButtonRole.DestructiveRole, 100), (QPushButton('Save to wallet'), QMessageBox.ButtonRole.AcceptRole, 101)] ) if result == QMessageBox.StandardButton.Open: + if label: + self.wallet.set_label(tx.txid(), label) show_transaction(tx, parent=self.window, prompt_if_unsaved=True, on_closed=partial(self.on_tx_dialog_closed, event_id)) else: self.mark_pending_event_rcvd(event_id) if result == 100: # Discard return - self.add_transaction_to_wallet(tx, on_failure=self.on_add_fail) + self.add_transaction_to_wallet(tx, label=label, on_failure=self.on_add_fail) self.window.update_tabs() def on_tx_dialog_closed(self, event_id):