plugins: psbt_nostr: let GUI handle a received PSBTs one by one by pausing receiving additional PSBTs until PSBT dialog is closed.
Accepting a PSBT opens the Tx dialog and pauses receiving additional PSBTs until the Tx dialog is closed. Rejecting a PSBT will start a cooldown and accept all pending PSBTs into the history for later inspection.
This commit is contained in:
@@ -18,11 +18,16 @@ Pane {
|
||||
property alias label: txdetails.label
|
||||
|
||||
signal detailsChanged
|
||||
signal closed
|
||||
|
||||
function close() {
|
||||
app.stack.pop()
|
||||
}
|
||||
|
||||
StackView.onRemoved: {
|
||||
closed()
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
@@ -28,7 +28,7 @@ import concurrent.futures
|
||||
import copy
|
||||
import datetime
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Optional, List, Union, Mapping
|
||||
from typing import TYPE_CHECKING, Optional, List, Union, Mapping, Callable
|
||||
from functools import partial
|
||||
from decimal import Decimal
|
||||
|
||||
@@ -410,6 +410,7 @@ def show_transaction(
|
||||
prompt_if_unsaved: bool = False,
|
||||
external_keypairs: Mapping[bytes, bytes] = None,
|
||||
payment_identifier: 'PaymentIdentifier' = None,
|
||||
on_closed: Callable[[], None] = None,
|
||||
):
|
||||
try:
|
||||
d = TxDialog(
|
||||
@@ -418,6 +419,7 @@ def show_transaction(
|
||||
prompt_if_unsaved=prompt_if_unsaved,
|
||||
external_keypairs=external_keypairs,
|
||||
payment_identifier=payment_identifier,
|
||||
on_closed=on_closed,
|
||||
)
|
||||
except SerializationError as e:
|
||||
_logger.exception('unable to deserialize the transaction')
|
||||
@@ -438,6 +440,7 @@ class TxDialog(QDialog, MessageBoxMixin):
|
||||
prompt_if_unsaved: bool,
|
||||
external_keypairs: Mapping[bytes, bytes] = None,
|
||||
payment_identifier: 'PaymentIdentifier' = None,
|
||||
on_closed: Callable[[], None] = None,
|
||||
):
|
||||
'''Transactions in the wallet will show their description.
|
||||
Pass desc to give a description for txs not yet in the wallet.
|
||||
@@ -451,6 +454,7 @@ class TxDialog(QDialog, MessageBoxMixin):
|
||||
self.wallet = parent.wallet
|
||||
self.payment_identifier = payment_identifier
|
||||
self.prompt_if_unsaved = prompt_if_unsaved
|
||||
self.on_closed = on_closed
|
||||
self.saved = False
|
||||
self.desc = None
|
||||
if txid := tx.txid():
|
||||
@@ -608,6 +612,9 @@ class TxDialog(QDialog, MessageBoxMixin):
|
||||
self._fetch_txin_data_fut.cancel()
|
||||
self._fetch_txin_data_fut = None
|
||||
|
||||
if self.on_closed:
|
||||
self.on_closed()
|
||||
|
||||
def reject(self):
|
||||
# Override escape-key to close normally (and invoke closeEvent)
|
||||
self.close()
|
||||
|
||||
@@ -79,11 +79,14 @@ class CosignerWallet(Logger):
|
||||
def __init__(self, wallet: 'Multisig_Wallet'):
|
||||
assert isinstance(wallet, Multisig_Wallet)
|
||||
self.wallet = wallet
|
||||
self.network = wallet.network
|
||||
self.config = self.wallet.config
|
||||
|
||||
Logger.__init__(self)
|
||||
|
||||
self.network = wallet.network
|
||||
self.config = self.wallet.config
|
||||
self.pending = asyncio.Event()
|
||||
self.known_events = wallet.db.get_dict('cosigner_events')
|
||||
|
||||
for k, v in list(self.known_events.items()):
|
||||
if v < now() - self.KEEP_DELAY:
|
||||
self.logger.info(f'deleting old event {k}')
|
||||
@@ -189,6 +192,8 @@ class CosignerWallet(Logger):
|
||||
return
|
||||
self.logger.info(f"received PSBT from {event.pubkey}")
|
||||
trigger_callback('psbt_nostr_received', self.wallet, event.pubkey, event.id, tx)
|
||||
await self.pending.wait()
|
||||
self.pending.clear()
|
||||
|
||||
def diagnostic_name(self):
|
||||
return self.wallet.diagnostic_name()
|
||||
@@ -203,8 +208,10 @@ class CosignerWallet(Logger):
|
||||
# note that tx could also be unrelated from wallet?... (not ismine inputs)
|
||||
return True
|
||||
|
||||
def mark_event_rcvd(self, event_id):
|
||||
def mark_pending_event_rcvd(self, event_id):
|
||||
self.logger.debug('marking event rcvd')
|
||||
self.known_events[event_id] = now()
|
||||
self.pending.set()
|
||||
|
||||
def prepare_messages(self, tx: Union[Transaction, PartialTransaction]) -> List[Tuple[str, str]]:
|
||||
messages = []
|
||||
@@ -223,3 +230,19 @@ class CosignerWallet(Logger):
|
||||
|
||||
def on_receive(self, pubkey, event_id, tx):
|
||||
raise NotImplementedError()
|
||||
|
||||
def add_transaction_to_wallet(self, tx, *, on_failure=None, on_success=None):
|
||||
try:
|
||||
# TODO: adding tx should be handled more gracefully here:
|
||||
# 1) don't replace tx with same tx with less signatures
|
||||
# 2) we could combine signatures if tx will become more complete
|
||||
# 3) ... more heuristics?
|
||||
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')
|
||||
except Exception as e:
|
||||
if on_failure:
|
||||
on_failure(str(e))
|
||||
else:
|
||||
if on_success:
|
||||
on_success()
|
||||
|
||||
@@ -36,12 +36,14 @@ from electrum.util import EventListener, event_listener
|
||||
|
||||
from electrum.gui.qml.qewallet import QEWallet
|
||||
|
||||
from .psbt_nostr import PsbtNostrPlugin, CosignerWallet
|
||||
from .psbt_nostr import PsbtNostrPlugin, CosignerWallet, now
|
||||
|
||||
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'):
|
||||
@@ -70,6 +72,13 @@ class QReceiveSignalObject(QObject):
|
||||
return
|
||||
cosigner_wallet.accept_psbt(event_id)
|
||||
|
||||
@pyqtSlot(QEWallet, str)
|
||||
def rejectPsbt(self, wallet: 'QEWallet', event_id: str):
|
||||
cosigner_wallet = self._plugin.cosigner_wallets[wallet.wallet]
|
||||
if not cosigner_wallet:
|
||||
return
|
||||
cosigner_wallet.reject_psbt(event_id)
|
||||
|
||||
|
||||
class Plugin(PsbtNostrPlugin):
|
||||
def __init__(self, parent, config, name):
|
||||
@@ -103,13 +112,18 @@ class QmlCosignerWallet(EventListener, CosignerWallet):
|
||||
self.plugin = plugin
|
||||
self.register_callbacks()
|
||||
|
||||
self.pending = None
|
||||
self.tx = None
|
||||
self.user_prompt_cooldown = None
|
||||
|
||||
@event_listener
|
||||
def on_event_psbt_nostr_received(self, wallet, pubkey, event, tx: 'PartialTransaction'):
|
||||
def on_event_psbt_nostr_received(self, wallet, pubkey, event_id, tx: 'PartialTransaction'):
|
||||
if self.wallet == wallet:
|
||||
self.plugin.so.cosignerReceivedPsbt.emit(pubkey, event, tx.serialize())
|
||||
self.on_receive(pubkey, event, tx)
|
||||
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)
|
||||
|
||||
def close(self):
|
||||
super().close()
|
||||
@@ -133,11 +147,13 @@ class QmlCosignerWallet(EventListener, CosignerWallet):
|
||||
except Exception as e:
|
||||
self.plugin.so.sendPsbtFailed.emit(str(e))
|
||||
|
||||
def on_receive(self, pubkey, event_id, tx):
|
||||
self.pending = (pubkey, event_id, tx)
|
||||
def accept_psbt(self, event_id):
|
||||
self.mark_pending_event_rcvd(event_id)
|
||||
|
||||
def accept_psbt(self, my_event_id):
|
||||
pubkey, event_id, tx = self.pending
|
||||
if event_id == my_event_id:
|
||||
self.mark_event_rcvd(event_id)
|
||||
self.pending = None
|
||||
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')
|
||||
|
||||
@@ -16,10 +16,15 @@ Item {
|
||||
yesno: true
|
||||
})
|
||||
dialog.accepted.connect(function () {
|
||||
app.stack.push(Qt.resolvedUrl('../../../gui/qml/components/TxDetails.qml'), {
|
||||
var page = app.stack.push(Qt.resolvedUrl('../../../gui/qml/components/TxDetails.qml'), {
|
||||
rawtx: tx
|
||||
})
|
||||
target.acceptPsbt(Daemon.currentWallet, event)
|
||||
page.closed.connect(function () {
|
||||
target.acceptPsbt(Daemon.currentWallet, event)
|
||||
})
|
||||
})
|
||||
dialog.rejected.connect(function () {
|
||||
target.rejectPsbt(Daemon.currentWallet, event)
|
||||
})
|
||||
dialog.open()
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
import asyncio
|
||||
from functools import partial
|
||||
from typing import TYPE_CHECKING, List, Tuple, Optional
|
||||
|
||||
from PyQt6.QtCore import QObject, pyqtSignal
|
||||
@@ -34,11 +35,13 @@ from electrum.wallet import Multisig_Wallet, Abstract_Wallet
|
||||
from electrum.util import UserCancelled, event_listener, EventListener
|
||||
from electrum.gui.qt.transaction_dialog import show_transaction, TxDialog
|
||||
|
||||
from .psbt_nostr import PsbtNostrPlugin, CosignerWallet
|
||||
from .psbt_nostr import PsbtNostrPlugin, CosignerWallet, now
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from electrum.gui.qt.main_window import ElectrumWindow
|
||||
|
||||
USER_PROMPT_COOLDOWN = 10
|
||||
|
||||
|
||||
class QReceiveSignalObject(QObject):
|
||||
cosignerReceivedPsbt = pyqtSignal(str, str, object)
|
||||
@@ -80,6 +83,7 @@ class QtCosignerWallet(EventListener, CosignerWallet):
|
||||
self.obj = QReceiveSignalObject()
|
||||
self.obj.cosignerReceivedPsbt.connect(self.on_receive)
|
||||
self.register_callbacks()
|
||||
self.user_prompt_cooldown = None
|
||||
|
||||
def close(self):
|
||||
super().close()
|
||||
@@ -92,7 +96,7 @@ class QtCosignerWallet(EventListener, CosignerWallet):
|
||||
|
||||
def hook_transaction_dialog(self, d: 'TxDialog'):
|
||||
d.cosigner_send_button = b = QPushButton(_("Send to cosigner"))
|
||||
b.clicked.connect(lambda: self.send_psbt(d.tx))
|
||||
b.clicked.connect(lambda: self.send_to_cosigners(d.tx))
|
||||
d.buttons.insert(0, b)
|
||||
b.setVisible(False)
|
||||
|
||||
@@ -108,6 +112,14 @@ class QtCosignerWallet(EventListener, CosignerWallet):
|
||||
else:
|
||||
d.cosigner_send_button.setVisible(False)
|
||||
|
||||
def send_to_cosigners(self, tx):
|
||||
def ok():
|
||||
self.logger.debug('ADDED')
|
||||
def nok(msg: str):
|
||||
self.logger.debug(f'NOT ADDED: {msg}')
|
||||
self.add_transaction_to_wallet(tx, on_success=ok, on_failure=nok)
|
||||
self.send_psbt(tx)
|
||||
|
||||
def do_send(self, messages: List[Tuple[str, str]], txid: Optional[str] = None):
|
||||
if not messages:
|
||||
return
|
||||
@@ -127,10 +139,21 @@ class QtCosignerWallet(EventListener, CosignerWallet):
|
||||
_("Your transaction was sent to your cosigners via Nostr.") + '\n\n' + txid)
|
||||
|
||||
def on_receive(self, pubkey, event_id, tx):
|
||||
window = self.window
|
||||
if not window.question(
|
||||
_("An transaction was received from your cosigner.") + '\n' +
|
||||
_("Do you want to open it now?")):
|
||||
return
|
||||
self.mark_event_rcvd(event_id)
|
||||
show_transaction(tx, parent=window, prompt_if_unsaved=True)
|
||||
open_now = False
|
||||
if not (self.user_prompt_cooldown and self.user_prompt_cooldown > now()):
|
||||
open_now = self.window.question(
|
||||
_("A transaction was received from your cosigner ({}).").format(str(event_id)[0:8]) + '\n' +
|
||||
_("Do you want to open it now?"))
|
||||
if not open_now:
|
||||
self.user_prompt_cooldown = now() + USER_PROMPT_COOLDOWN
|
||||
if open_now:
|
||||
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)
|
||||
self.add_transaction_to_wallet(tx, on_failure=self.on_add_fail)
|
||||
|
||||
def on_tx_dialog_closed(self, event_id):
|
||||
self.mark_pending_event_rcvd(event_id)
|
||||
|
||||
def on_add_fail(self, msg: str):
|
||||
self.window.show_error(msg)
|
||||
|
||||
Reference in New Issue
Block a user