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):