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