Skip to content

Commit 3260ece

Browse files
committed
qml: let user finalize forward swap onchain tx before initiating swap
1 parent 9e752d2 commit 3260ece

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
@@ -7,6 +7,7 @@
77
QModelIndex)
88
from PyQt6.QtGui import QColor
99

10+
from electrum.gui.qml.qetxfinalizer import QETxFinalizer
1011
from electrum.i18n import _
1112
from electrum.bitcoin import DummyAddress
1213
from electrum.logging import get_logger
@@ -158,10 +159,12 @@ def __init__(self, parent=None):
158159
super().__init__(parent)
159160

160161
self._wallet = None # type: Optional[QEWallet]
162+
self._finalizer = None # type: Optional[QETxFinalizer]
161163
self._sliderPos = 0
162164
self._rangeMin = -1
163165
self._rangeMax = 1
164-
self._tx = None
166+
self._tx = None # updated on feeslider move and fee histogram updates, used for estimation
167+
self._finalized_tx = None # updated by finalizer, used for actual forward swap
165168
self._valid = False
166169
self._state = QESwapHelper.State.Initialized
167170
self._userinfo = QESwapHelper.MESSAGE_SWAP_HOWTO
@@ -215,6 +218,11 @@ def wallet(self, wallet: QEWallet):
215218
self.run_swap_manager()
216219
self.walletChanged.emit()
217220

221+
finalizerChanged = pyqtSignal()
222+
@pyqtProperty(QETxFinalizer, notify=finalizerChanged)
223+
def finalizer(self):
224+
return self._finalizer
225+
218226
sliderPosChanged = pyqtSignal()
219227
@pyqtProperty(float, notify=sliderPosChanged)
220228
def sliderPos(self):
@@ -547,24 +555,26 @@ def initSwapSliderRange(self):
547555
self.swap_slider_moved()
548556

549557
@profiler
550-
def update_tx(self, onchain_amount: Union[int, str]):
558+
def update_tx(self, onchain_amount: Union[int, str], fee_policy: Optional[FeePolicy] = None):
551559
"""Updates the transaction associated with a forward swap."""
552560
if onchain_amount is None:
553561
self._tx = None
554562
self.valid = False
555563
return
556-
outputs = [PartialTxOutput.from_address_and_value(DummyAddress.SWAP, onchain_amount)]
557-
coins = self._wallet.wallet.get_spendable_coins(None)
558-
fee_policy = FeePolicy('eta:2')
559564
try:
560-
self._tx = self._wallet.wallet.make_unsigned_transaction(
561-
coins=coins,
562-
outputs=outputs,
563-
fee_policy=fee_policy)
565+
self._tx = self._create_swap_tx(onchain_amount, fee_policy)
564566
except (NotEnoughFunds, NoDynamicFeeEstimates):
565567
self._tx = None
566568
self.valid = False
567569

570+
def _create_swap_tx(self, onchain_amount: int | str, fee_policy: Optional[FeePolicy] = None):
571+
outputs = [PartialTxOutput.from_address_and_value(DummyAddress.SWAP, onchain_amount)]
572+
coins = self._wallet.wallet.get_spendable_coins(None)
573+
fee_policy = fee_policy if fee_policy else FeePolicy('eta:2')
574+
return self._wallet.wallet.make_unsigned_transaction(
575+
coins=coins, outputs=outputs, fee_policy=fee_policy
576+
)
577+
568578
@qt_event_listener
569579
def on_event_fee_histogram(self, *args):
570580
self.swap_slider_moved()
@@ -623,13 +633,15 @@ def fwd_swap_updatetx(self):
623633

624634
def do_normal_swap(self, lightning_amount, onchain_amount):
625635
assert self._tx
636+
assert self._finalized_tx
626637
if lightning_amount is None or onchain_amount is None:
627638
return
628639

640+
assert self._finalized_tx.get_dummy_output(DummyAddress.SWAP).value == onchain_amount
641+
629642
async def swap_task():
630643
assert self.swap_transport is not None, "Swap transport not available"
631644
try:
632-
dummy_tx = self._create_tx(onchain_amount)
633645
self.userinfo = _('Performing swap...')
634646
self.state = QESwapHelper.State.Started
635647
self._swap, invoice = await self._wallet.wallet.lnworker.swap_manager.request_normal_swap(
@@ -638,10 +650,11 @@ async def swap_task():
638650
expected_onchain_amount_sat=onchain_amount,
639651
)
640652

641-
tx = self._wallet.wallet.lnworker.swap_manager.create_funding_tx(self._swap, dummy_tx, password=self._wallet.password)
642-
coro2 = self._wallet.wallet.lnworker.swap_manager.wait_for_htlcs_and_broadcast(
653+
tx = self._wallet.wallet.lnworker.swap_manager.create_funding_tx(
654+
self._swap, self._finalized_tx, password=self._wallet.password)
655+
coro = self._wallet.wallet.lnworker.swap_manager.wait_for_htlcs_and_broadcast(
643656
transport=self.swap_transport, swap=self._swap, invoice=invoice, tx=tx)
644-
self._fut_htlc_wait = fut = asyncio.create_task(coro2)
657+
self._fut_htlc_wait = fut = asyncio.create_task(coro)
645658

646659
self.canCancel = True
647660
txid = await fut
@@ -675,32 +688,20 @@ async def swap_task():
675688

676689
asyncio.run_coroutine_threadsafe(swap_task(), get_asyncio_loop())
677690

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

705706
def do_reverse_swap(self, lightning_amount, onchain_amount):
706707
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)