diff --git a/electrum/gui/qml/components/ExportTxDialog.qml b/electrum/gui/qml/components/ExportTxDialog.qml index 62b4291bf5c..249f07c18a9 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 5f8747c2780..e17b4efaedb 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 5ddad940c4d..9f0a2ac6105 100644 --- a/electrum/gui/qml/qetxdetails.py +++ b/electrum/gui/qml/qetxdetails.py @@ -383,9 +383,11 @@ def update(self, from_txid: bool = False): 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 @@ def save(self): @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 2223dea801b..3908c7e31d6 100644 --- a/electrum/gui/qml/qetxfinalizer.py +++ b/electrum/gui/qml/qetxfinalizer.py @@ -494,7 +494,8 @@ def on_sign_failed(self, msg: str = None): @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/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 31518139882..ef62956c436 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -1165,7 +1165,7 @@ def show_transaction( 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, ): @@ -1173,7 +1173,7 @@ def show_transaction( 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, ) @@ -1409,18 +1409,18 @@ def get_manually_selected_coins(self) -> Optional[Sequence[PartialTxInput]]: """ 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 81c9ca3fdd2..ecefdea9e44 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -304,10 +304,6 @@ def pay_onchain_dialog( 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: @@ -353,12 +349,12 @@ def make_tx(fee_policy, *, confirmed_only=False, base_tx=None): 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, @@ -711,7 +707,7 @@ def pay_lightning_invoice(self, invoice: Invoice): 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 @@ -724,9 +720,7 @@ def pay_lightning_invoice(self, invoice: Invoice): 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) @@ -741,7 +735,7 @@ def broadcast_transaction(self, tx: Transaction, *, payment_identifier: PaymentI 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)) @@ -750,19 +744,22 @@ def broadcast_thread(): 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 47d86d60cce..e73952eafd9 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -64,7 +64,7 @@ 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__) @@ -409,7 +409,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, @@ -420,7 +420,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: @@ -445,7 +445,7 @@ def __init__( 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. @@ -458,13 +458,15 @@ def __init__( 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]] @@ -483,13 +485,8 @@ def __init__( 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) @@ -570,6 +567,13 @@ def on_edited(): 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 @@ -598,7 +602,7 @@ def do_broadcast(self): 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 @@ -713,6 +717,7 @@ def sign_done(success): 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) @@ -842,7 +847,8 @@ def update(self): # 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: diff --git a/electrum/plugins/psbt_nostr/psbt_nostr.py b/electrum/plugins/psbt_nostr/psbt_nostr.py index 0be6ab995a9..5fcbd7937f0 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 @@ 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.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 @@ async def nostr_manager(self): 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 @@ async def check_direct_messages(self): 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 @@ def mark_pending_event_rcvd(self, event_id): 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 @@ def add_transaction_to_wallet(self, tx, *, on_failure=None, on_success=None): 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 caf48328ea4..b3862a19aaf 100644 --- a/electrum/plugins/psbt_nostr/qml.py +++ b/electrum/plugins/psbt_nostr/qml.py @@ -36,21 +36,19 @@ from electrum.gui.qml.qewallet import QEWallet -from .psbt_nostr import PsbtNostrPlugin, CosignerWallet, now +from .psbt_nostr import PsbtNostrPlugin, CosignerWallet if TYPE_CHECKING: from electrum.wallet import Abstract_Wallet from electrum.gui.qml import ElectrumQmlApplication -USER_PROMPT_COOLDOWN = 10 - class QReceiveSignalObject(QObject): def __init__(self, plugin: 'Plugin'): 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,18 +64,30 @@ def canSendPsbt(self, wallet: 'QEWallet', tx: str) -> bool: 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), label) + + @pyqtSlot(QEWallet, str, str) + def saveTxLabel(self, wallet: 'QEWallet', tx: str, label: str): 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.save_tx_label(tx_from_any(tx, deserialize=True), label) @pyqtSlot(QEWallet, str) - def acceptPsbt(self, wallet: 'QEWallet', event_id: str): + @pyqtSlot(QEWallet, str, bool) + def acceptPsbt(self, wallet: 'QEWallet', event_id: str, save_to_wallet: bool = False): cosigner_wallet = self._plugin.cosigner_wallets.get(wallet.wallet) if not cosigner_wallet: return - cosigner_wallet.accept_psbt(event_id) + cosigner_wallet.accept_psbt(event_id, save_to_wallet) + if save_to_wallet: + # let GUI update view through wallet_updated callback + util.trigger_callback('wallet_updated', wallet.wallet) @pyqtSlot(QEWallet, str) def rejectPsbt(self, wallet: 'QEWallet', event_id: str): @@ -123,23 +133,18 @@ def __init__(self, wallet: 'Multisig_Wallet', plugin: 'Plugin'): self.register_callbacks() self.tx = None - 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()) - else: - self.mark_pending_event_rcvd(event_id) - self.add_transaction_to_wallet(self.tx, on_failure=self.on_add_fail) + self.plugin.so.cosignerReceivedPsbt.emit(pubkey, event_id, tx.serialize(), label) 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) @@ -157,13 +162,16 @@ def do_send(self, messages: List[Tuple[str, str]], txid: Optional[str] = None): except Exception as e: self.plugin.so.sendPsbtFailed.emit(str(e)) - def accept_psbt(self, event_id): + def save_tx_label(self, tx, label): + self.wallet.set_label(tx.txid(), label) + + def accept_psbt(self, event_id, save: bool = False): + if save: + self.add_transaction_to_wallet(self.tx, on_failure=self.on_add_fail) self.mark_pending_event_rcvd(event_id) def reject_psbt(self, event_id): - self.user_prompt_cooldown = now() + USER_PROMPT_COOLDOWN self.mark_pending_event_rcvd(event_id) - self.add_transaction_to_wallet(self.tx, on_failure=self.on_add_fail) def on_add_fail(self): self.logger.error('failed to add tx to wallet') diff --git a/electrum/plugins/psbt_nostr/qml/PsbtReceiveDialog.qml b/electrum/plugins/psbt_nostr/qml/PsbtReceiveDialog.qml new file mode 100644 index 00000000000..19d1352e4b5 --- /dev/null +++ b/electrum/plugins/psbt_nostr/qml/PsbtReceiveDialog.qml @@ -0,0 +1,91 @@ +import QtQuick +import QtQuick.Layouts +import QtQuick.Controls +import QtQuick.Controls.Material + +import "../../../gui/qml/components/controls" + +ElDialog { + id: dialog + title: qsTr("PSBT received") + iconSource: Qt.resolvedUrl('../../../gui/icons/question.png') + + enum Choice { + None, + Open, + Save + } + + property string tx_label + property int choice: PsbtReceiveDialog.Choice.None + + // TODO: it might be better to defer popup until no dialogs are shown + z: 1 // raise z so it also covers dialogs using overlay as parent + + anchors.centerIn: parent + + padding: 0 + + width: rootLayout.width + + ColumnLayout { + id: rootLayout + width: dialog.parent.width * 2/3 + + ColumnLayout { + Layout.margins: constants.paddingMedium + Layout.fillWidth: true + + TextArea { + id: message + Layout.fillWidth: true + readOnly: true + wrapMode: TextInput.WordWrap + textFormat: TextEdit.RichText + background: Rectangle { + color: 'transparent' + } + + text: [ + tx_label + ? qsTr('A transaction was received from your cosigner with label:
%1
').arg(tx_label) + : qsTr('A transaction was received from your cosigner.'), + qsTr('Do you want to open it now?') + ].join('
') + } + } + + ButtonContainer { + Layout.fillWidth: true + + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 + text: qsTr('Open') + icon.source: Qt.resolvedUrl('../../../gui/icons/confirmed.png') + onClicked: { + choice = PsbtReceiveDialog.Choice.Open + doAccept() + } + } + + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 + text: qsTr('Discard') + icon.source: Qt.resolvedUrl('../../../gui/icons/closebutton.png') + onClicked: doReject() + } + FlatButton { + Layout.fillWidth: true + Layout.preferredWidth: 1 + text: qsTr('Save to Wallet') + icon.source: Qt.resolvedUrl('../../../gui/icons/wallet.png') + onClicked: { + choice = PsbtReceiveDialog.Choice.Save + doAccept() + } + } + } + } +} diff --git a/electrum/plugins/psbt_nostr/qml/main.qml b/electrum/plugins/psbt_nostr/qml/main.qml index 7df6d6aed52..2c2d5f9d6a6 100644 --- a/electrum/plugins/psbt_nostr/qml/main.qml +++ b/electrum/plugins/psbt_nostr/qml/main.qml @@ -7,21 +7,26 @@ import "../../../gui/qml/components/controls" Item { Connections { target: AppController ? AppController.plugin('psbt_nostr') : null - function onCosignerReceivedPsbt(pubkey, event, tx) { - var dialog = app.messageDialog.createObject(app, { - text: [ - qsTr('A transaction was received from your cosigner.'), - qsTr('Do you want to open it now?') - ].join('\n'), - yesno: true + function onCosignerReceivedPsbt(pubkey, event, tx, label) { + var dialog = psbtReceiveDialog.createObject(app, { + tx_label: label }) dialog.accepted.connect(function () { - var page = app.stack.push(Qt.resolvedUrl('../../../gui/qml/components/TxDetails.qml'), { - rawtx: tx - }) - page.closed.connect(function () { - target.acceptPsbt(Daemon.currentWallet, event) - }) + if (dialog.choice == PsbtReceiveDialog.Choice.Open) { + console.log('label:' + label) + console.log('tx:' + tx) + target.saveTxLabel(Daemon.currentWallet, tx, label) + var page = app.stack.push(Qt.resolvedUrl('../../../gui/qml/components/TxDetails.qml'), { + rawtx: tx + }) + page.closed.connect(function () { + target.acceptPsbt(Daemon.currentWallet, event) + }) + } else if (dialog.choice == PsbtReceiveDialog.Choice.Save) { + target.acceptPsbt(Daemon.currentWallet, event, true) + } else { + console.log('choice not set') + } }) dialog.rejected.connect(function () { target.rejectPsbt(Daemon.currentWallet, event) @@ -30,6 +35,13 @@ Item { } } + Component { + id: psbtReceiveDialog + PsbtReceiveDialog { + onClosed: destroy() + } + } + property variant export_tx_button: Component { FlatButton { id: psbt_nostr_send_button @@ -40,16 +52,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 e18f31af5f4..b86c13e64f5 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 .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 @@ def transaction_dialog(self, d: 'TxDialog'): 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 @@ def on_event_psbt_nostr_received(self, wallet, *args): 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 @@ def do_send(self, messages: List[Tuple[str, str]], txid: Optional[str] = None): 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):