1
0

psbt_nostr: send label along with PSBT

This commit is contained in:
Sander van Grieken
2025-04-28 12:29:53 +02:00
parent 6566f2f0a4
commit a9213c4d66
8 changed files with 88 additions and 48 deletions

View File

@@ -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')

View File

@@ -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()
}

View File

@@ -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]

View File

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

View File

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

View File

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

View File

@@ -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: <br/><br/><b>%1</b>').arg(label)
: qsTr('A transaction was received from your cosigner.'),
qsTr('Do you want to open it now?')
].join('\n'),
yesno: true
].join('<br/><br/>'),
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()
}
}

View File

@@ -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 = '<br/>'.join([
_("A transaction was received from your cosigner.") if not label else
_("A transaction was received from your cosigner with label: <br/><big>{}</big><br/>").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):