Skip to content

Commit 44a9a1f

Browse files
committed
qml: let user finalize forward swap onchain tx before initiating swap
1 parent ce64dea commit 44a9a1f

File tree

3 files changed

+72
-44
lines changed

3 files changed

+72
-44
lines changed

electrum/gui/qml/components/SwapDialog.qml

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,19 @@ ElDialog {
293293
enabled: swaphelper.valid && (swaphelper.state == SwapHelper.ServiceReady || swaphelper.state == SwapHelper.Failed)
294294

295295
onClicked: {
296-
swaphelper.executeSwap()
296+
if (swaphelper.isReverse) {
297+
swaphelper.executeSwap()
298+
} else {
299+
swaphelper.prepNormalSwap()
300+
var dialog = forwardSwapTxDialog.createObject(app, {
301+
finalizer: swaphelper.finalizer,
302+
satoshis: swaphelper.finalizer.amount
303+
})
304+
dialog.accepted.connect(function() {
305+
swaphelper.executeSwap()
306+
})
307+
dialog.open()
308+
}
297309
}
298310
}
299311
FlatButton {
@@ -331,6 +343,15 @@ ElDialog {
331343
}
332344
}
333345

346+
Component {
347+
id: forwardSwapTxDialog
348+
ConfirmTxDialog {
349+
amountLabelText: qsTr('Amount to swap')
350+
sendButtonText: qsTr('Swap')
351+
finalizer: swaphelper.finalizer
352+
}
353+
}
354+
334355
Component.onCompleted: {
335356
swapslider.value = swaphelper.sliderPos
336357
}

electrum/gui/qml/qeswaphelper.py

Lines changed: 40 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from PyQt6.QtCore import (pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer, pyqtEnum, QAbstractListModel, Qt,
77
QModelIndex)
88

9+
from electrum.gui.qml.qetxfinalizer import QETxFinalizer
910
from electrum.i18n import _
1011
from electrum.bitcoin import DummyAddress
1112
from electrum.logging import get_logger
@@ -156,10 +157,12 @@ def __init__(self, parent=None):
156157
super().__init__(parent)
157158

158159
self._wallet = None # type: Optional[QEWallet]
160+
self._finalizer = None # type: Optional[QETxFinalizer]
159161
self._sliderPos = 0
160162
self._rangeMin = -1
161163
self._rangeMax = 1
162-
self._tx = None
164+
self._tx = None # updated on feeslider move and fee histogram updates, used for estimation
165+
self._finalized_tx = None # updated by finalizer, used for actual forward swap
163166
self._valid = False
164167
self._state = QESwapHelper.State.Initialized
165168
self._userinfo = QESwapHelper.MESSAGE_SWAP_HOWTO
@@ -213,6 +216,11 @@ def wallet(self, wallet: QEWallet):
213216
self.run_swap_manager()
214217
self.walletChanged.emit()
215218

219+
finalizerChanged = pyqtSignal()
220+
@pyqtProperty(QETxFinalizer, notify=finalizerChanged)
221+
def finalizer(self):
222+
return self._finalizer
223+
216224
sliderPosChanged = pyqtSignal()
217225
@pyqtProperty(float, notify=sliderPosChanged)
218226
def sliderPos(self):
@@ -545,24 +553,26 @@ def initSwapSliderRange(self):
545553
self.swap_slider_moved()
546554

547555
@profiler
548-
def update_tx(self, onchain_amount: Union[int, str]):
556+
def update_tx(self, onchain_amount: Union[int, str], fee_policy: Optional[FeePolicy] = None):
549557
"""Updates the transaction associated with a forward swap."""
550558
if onchain_amount is None:
551559
self._tx = None
552560
self.valid = False
553561
return
554-
outputs = [PartialTxOutput.from_address_and_value(DummyAddress.SWAP, onchain_amount)]
555-
coins = self._wallet.wallet.get_spendable_coins(None)
556-
fee_policy = FeePolicy('eta:2')
557562
try:
558-
self._tx = self._wallet.wallet.make_unsigned_transaction(
559-
coins=coins,
560-
outputs=outputs,
561-
fee_policy=fee_policy)
563+
self._tx = self._create_swap_tx(onchain_amount, fee_policy)
562564
except (NotEnoughFunds, NoDynamicFeeEstimates):
563565
self._tx = None
564566
self.valid = False
565567

568+
def _create_swap_tx(self, onchain_amount: int | str, fee_policy: Optional[FeePolicy] = None):
569+
outputs = [PartialTxOutput.from_address_and_value(DummyAddress.SWAP, onchain_amount)]
570+
coins = self._wallet.wallet.get_spendable_coins(None)
571+
fee_policy = fee_policy if fee_policy else FeePolicy('eta:2')
572+
return self._wallet.wallet.make_unsigned_transaction(
573+
coins=coins, outputs=outputs, fee_policy=fee_policy
574+
)
575+
566576
@qt_event_listener
567577
def on_event_fee_histogram(self, *args):
568578
self.swap_slider_moved()
@@ -621,13 +631,15 @@ def fwd_swap_updatetx(self):
621631

622632
def do_normal_swap(self, lightning_amount, onchain_amount):
623633
assert self._tx
634+
assert self._finalized_tx
624635
if lightning_amount is None or onchain_amount is None:
625636
return
626637

638+
assert self._finalized_tx.get_dummy_output(DummyAddress.SWAP).value == onchain_amount
639+
627640
async def swap_task():
628641
assert self.swap_transport is not None, "Swap transport not available"
629642
try:
630-
dummy_tx = self._create_tx(onchain_amount)
631643
self.userinfo = _('Performing swap...')
632644
self.state = QESwapHelper.State.Started
633645
self._swap, invoice = await self._wallet.wallet.lnworker.swap_manager.request_normal_swap(
@@ -636,10 +648,11 @@ async def swap_task():
636648
expected_onchain_amount_sat=onchain_amount,
637649
)
638650

639-
tx = self._wallet.wallet.lnworker.swap_manager.create_funding_tx(self._swap, dummy_tx, password=self._wallet.password)
640-
coro2 = self._wallet.wallet.lnworker.swap_manager.wait_for_htlcs_and_broadcast(
651+
tx = self._wallet.wallet.lnworker.swap_manager.create_funding_tx(
652+
self._swap, self._finalized_tx, password=self._wallet.password)
653+
coro = self._wallet.wallet.lnworker.swap_manager.wait_for_htlcs_and_broadcast(
641654
transport=self.swap_transport, swap=self._swap, invoice=invoice, tx=tx)
642-
self._fut_htlc_wait = fut = asyncio.create_task(coro2)
655+
self._fut_htlc_wait = fut = asyncio.create_task(coro)
643656

644657
self.canCancel = True
645658
txid = await fut
@@ -673,32 +686,20 @@ async def swap_task():
673686

674687
asyncio.run_coroutine_threadsafe(swap_task(), get_asyncio_loop())
675688

676-
def _create_tx(self, onchain_amount: Union[int, str, None]) -> PartialTransaction:
677-
# TODO: func taken from qt GUI, this should be common code
678-
assert not self.isReverse
679-
if onchain_amount is None:
680-
raise InvalidSwapParameters("onchain_amount is None")
681-
# coins = self.window.get_coins()
682-
coins = self._wallet.wallet.get_spendable_coins()
683-
if onchain_amount == '!':
684-
max_amount = sum(c.value_sats() for c in coins)
685-
max_swap_amount = self._wallet.wallet.lnworker.swap_manager.client_max_amount_forward_swap()
686-
if max_swap_amount is None:
687-
raise InvalidSwapParameters("swap_manager.client_max_amount_forward_swap() is None")
688-
if max_amount > max_swap_amount:
689-
onchain_amount = max_swap_amount
690-
outputs = [PartialTxOutput.from_address_and_value(DummyAddress.SWAP, onchain_amount)]
691-
fee_policy = FeePolicy('eta:2')
692-
try:
693-
tx = self._wallet.wallet.make_unsigned_transaction(
694-
coins=coins,
695-
outputs=outputs,
696-
send_change_to_lightning=False,
697-
fee_policy=fee_policy
698-
)
699-
except (NotEnoughFunds, NoDynamicFeeEstimates) as e:
700-
raise InvalidSwapParameters(str(e)) from e
701-
return tx
689+
@pyqtSlot()
690+
def prepNormalSwap(self):
691+
def mktx(amt, fee_policy: FeePolicy):
692+
try:
693+
self._finalized_tx = self._create_swap_tx(amt, fee_policy)
694+
except (NotEnoughFunds, NoDynamicFeeEstimates):
695+
self._finalized_tx = None
696+
return self._finalized_tx
697+
698+
self._finalizer = QETxFinalizer(self, make_tx=mktx)
699+
self._finalizer.canRbf = False
700+
self._finalizer.amount = QEAmount(amount_sat=self._send_amount)
701+
self._finalizer.wallet = self._wallet
702+
self.finalizerChanged.emit()
702703

703704
def do_reverse_swap(self, lightning_amount, onchain_amount):
704705
if lightning_amount is None or onchain_amount is None:

electrum/gui/qml/qetxfinalizer.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from enum import IntEnum
33
import threading
44
from decimal import Decimal
5-
from typing import Optional, TYPE_CHECKING
5+
from typing import Optional, TYPE_CHECKING, Callable
66
from functools import partial
77

88
from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, pyqtEnum
@@ -54,7 +54,7 @@ def __init__(self, parent=None):
5454
self._wallet = None # type: Optional[QEWallet]
5555
self._sliderSteps = 0
5656
self._sliderPos = 0
57-
self._fee_policy = None
57+
self._fee_policy: Optional[FeePolicy] = None
5858
self._target = ''
5959
self._config = None # type: Optional[SimpleConfig]
6060

@@ -314,7 +314,13 @@ class QETxFinalizer(TxFeeSlider):
314314
finished = pyqtSignal([bool, bool, bool], arguments=['signed', 'saved', 'complete'])
315315
signError = pyqtSignal([str], arguments=['message'])
316316

317-
def __init__(self, parent=None, *, make_tx=None, accept=None):
317+
def __init__(
318+
self,
319+
parent=None,
320+
*,
321+
make_tx: Optional[Callable[[int | str, Optional[FeePolicy]], PartialTransaction]] = None,
322+
accept: Optional[Callable[[PartialTransaction], None]] = None
323+
):
318324
super().__init__(parent)
319325
self.f_make_tx = make_tx
320326
self.f_accept = accept
@@ -377,7 +383,7 @@ def canRbf(self, canRbf):
377383
self.rbf = self._canRbf # if we can RbF, we do RbF
378384

379385
@profiler
380-
def make_tx(self, amount):
386+
def make_tx(self, amount: int | str) -> PartialTransaction:
381387
self._logger.debug(f'make_tx amount={amount}')
382388

383389
if self.f_make_tx:

0 commit comments

Comments
 (0)