Browse Source

qml: port cancel normal swap feature from desktop client

master
Sander van Grieken 2 years ago
parent
commit
bbfe5225b6
  1. 30
      electrum/gui/qml/components/SwapDialog.qml
  2. 89
      electrum/gui/qml/qeswaphelper.py

30
electrum/gui/qml/components/SwapDialog.qml

@ -35,7 +35,7 @@ ElDialog {
text: swaphelper.userinfo
iconStyle: swaphelper.state == SwapHelper.Started
? InfoTextArea.IconStyle.Spinner
: swaphelper.state == SwapHelper.Failed
: swaphelper.state == SwapHelper.Failed || swaphelper.state == SwapHelper.Cancelled
? InfoTextArea.IconStyle.Error
: swaphelper.state == SwapHelper.Success
? InfoTextArea.IconStyle.Done
@ -251,15 +251,31 @@ ElDialog {
Item { Layout.fillHeight: true; Layout.preferredWidth: 1 }
FlatButton {
ButtonContainer {
Layout.columnSpan: 2
Layout.fillWidth: true
text: qsTr('Ok')
icon.source: Qt.resolvedUrl('../../icons/confirmed.png')
enabled: swaphelper.valid && (swaphelper.state == SwapHelper.ServiceReady || swaphelper.state == SwapHelper.Failed)
FlatButton {
Layout.fillWidth: true
Layout.preferredWidth: 1
text: qsTr('Ok')
icon.source: Qt.resolvedUrl('../../icons/confirmed.png')
visible: !swaphelper.canCancel
enabled: swaphelper.valid && (swaphelper.state == SwapHelper.ServiceReady || swaphelper.state == SwapHelper.Failed)
onClicked: {
swaphelper.executeSwap()
}
}
FlatButton {
Layout.fillWidth: true
Layout.preferredWidth: 1
text: qsTr('Cancel')
icon.source: Qt.resolvedUrl('../../icons/closebutton.png')
visible: swaphelper.canCancel
onClicked: {
swaphelper.executeSwap()
onClicked: {
swaphelper.cancelNormalSwap()
}
}
}
}

89
electrum/gui/qml/qeswaphelper.py

@ -1,6 +1,6 @@
import asyncio
import concurrent
import threading
import math
from typing import Union
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer, Q_ENUMS
@ -8,7 +8,7 @@ from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer, Q_
from electrum.i18n import _
from electrum.bitcoin import DummyAddress
from electrum.logging import get_logger
from electrum.transaction import PartialTxOutput
from electrum.transaction import PartialTxOutput, PartialTransaction
from electrum.util import NotEnoughFunds, NoDynamicFeeEstimates, profiler, get_asyncio_loop
from .auth import AuthMixin, auth_protect
@ -16,6 +16,10 @@ from .qetypes import QEAmount
from .qewallet import QEWallet
from .util import QtEventListener, qt_event_listener
class InvalidSwapParameters(Exception): pass
class QESwapHelper(AuthMixin, QObject, QtEventListener):
_logger = get_logger(__name__)
@ -25,6 +29,7 @@ class QESwapHelper(AuthMixin, QObject, QtEventListener):
Started = 2
Failed = 3
Success = 4
Cancelled = 5
Q_ENUMS(State)
@ -51,7 +56,10 @@ class QESwapHelper(AuthMixin, QObject, QtEventListener):
self._server_miningfee = QEAmount()
self._miningfee = QEAmount()
self._isReverse = False
self._canCancel = False
self._swap = None
self._fut_htlc_wait = None
self._service_available = False
self._send_amount = 0
self._receive_amount = 0
@ -225,6 +233,16 @@ class QESwapHelper(AuthMixin, QObject, QtEventListener):
self._isReverse = isReverse
self.isReverseChanged.emit()
canCancelChanged = pyqtSignal()
@pyqtProperty(bool, notify=canCancelChanged)
def canCancel(self):
return self._canCancel
@canCancel.setter
def canCancel(self, canCancel):
if self._canCancel != canCancel:
self._canCancel = canCancel
self.canCancelChanged.emit()
def init_swap_slider_range(self):
lnworker = self._wallet.wallet.lnworker
@ -346,20 +364,26 @@ class QESwapHelper(AuthMixin, QObject, QtEventListener):
if lightning_amount is None or onchain_amount is None:
return
loop = get_asyncio_loop()
coro = self._wallet.wallet.lnworker.swap_manager.normal_swap(
coro = self._wallet.wallet.lnworker.swap_manager.request_normal_swap(
lightning_amount_sat=lightning_amount,
expected_onchain_amount_sat=onchain_amount,
password=self._wallet.password,
tx=self._tx,
)
def swap_task():
try:
dummy_tx = self._create_tx(onchain_amount)
fut = asyncio.run_coroutine_threadsafe(coro, loop)
self.userinfo = _('Performing swap...')
self.state = QESwapHelper.State.Started
self._swap, invoice = fut.result()
tx = self._wallet.wallet.lnworker.swap_manager.create_funding_tx(self._swap, dummy_tx, self._wallet.password)
coro2 = self._wallet.wallet.lnworker.swap_manager.wait_for_htlcs_and_broadcast(self._swap, invoice, tx)
self._fut_htlc_wait = fut = asyncio.run_coroutine_threadsafe(coro2, loop)
self.canCancel = True
txid = fut.result()
try: # swaphelper might be destroyed at this point
try: # swaphelper might be destroyed at this point
self.userinfo = ' '.join([
_('Success!'),
_('Your funding transaction has been broadcast.'),
@ -369,16 +393,51 @@ class QESwapHelper(AuthMixin, QObject, QtEventListener):
self.state = QESwapHelper.State.Success
except RuntimeError:
pass
except concurrent.futures.CancelledError:
self._wallet.wallet.lnworker.swap_manager.cancel_normal_swap(self._swap)
self.userinfo = _('Swap cancelled')
self.state = QESwapHelper.State.Cancelled
except Exception as e:
try: # swaphelper might be destroyed at this point
try: # swaphelper might be destroyed at this point
self.state = QESwapHelper.State.Failed
self.userinfo = _('Error') + ': ' + str(e)
self._logger.error(str(e))
except RuntimeError:
pass
finally:
try: # swaphelper might be destroyed at this point
self.canCancel = False
self._swap = None
self._fut_htlc_wait = None
except RuntimeError:
pass
threading.Thread(target=swap_task, daemon=True).start()
def _create_tx(self, onchain_amount: Union[int, str, None]) -> PartialTransaction:
# TODO: func taken from qt GUI, this should be common code
assert not self.isReverse
if onchain_amount is None:
raise InvalidSwapParameters("onchain_amount is None")
# coins = self.window.get_coins()
coins = self._wallet.wallet.get_spendable_coins()
if onchain_amount == '!':
max_amount = sum(c.value_sats() for c in coins)
max_swap_amount = self._wallet.wallet.lnworker.swap_manager.max_amount_forward_swap()
if max_swap_amount is None:
raise InvalidSwapParameters("swap_manager.max_amount_forward_swap() is None")
if max_amount > max_swap_amount:
onchain_amount = max_swap_amount
self._wallet.wallet.config.WALLET_SEND_CHANGE_TO_LIGHTNING = False
outputs = [PartialTxOutput.from_address_and_value(DummyAddress.SWAP, onchain_amount)]
try:
tx = self._wallet.wallet.make_unsigned_transaction(
coins=coins,
outputs=outputs)
except (NotEnoughFunds, NoDynamicFeeEstimates) as e:
raise InvalidSwapParameters(str(e)) from e
return tx
def do_reverse_swap(self, lightning_amount, onchain_amount):
if lightning_amount is None or onchain_amount is None:
return
@ -394,9 +453,9 @@ class QESwapHelper(AuthMixin, QObject, QtEventListener):
fut = asyncio.run_coroutine_threadsafe(coro, loop)
self.userinfo = _('Performing swap...')
self.state = QESwapHelper.State.Started
success = fut.result()
try: # swaphelper might be destroyed at this point
if success:
txid = fut.result()
try: # swaphelper might be destroyed at this point
if txid:
self.userinfo = ' '.join([
_('Success!'),
_('The funding transaction has been detected.'),
@ -410,7 +469,7 @@ class QESwapHelper(AuthMixin, QObject, QtEventListener):
except RuntimeError:
pass
except Exception as e:
try: # swaphelper might be destroyed at this point
try: # swaphelper might be destroyed at this point
self.state = QESwapHelper.State.Failed
self.userinfo = _('Error') + ': ' + str(e)
self._logger.error(str(e))
@ -436,3 +495,9 @@ class QESwapHelper(AuthMixin, QObject, QtEventListener):
lightning_amount = self._receive_amount
onchain_amount = self._send_amount
self.do_normal_swap(lightning_amount, onchain_amount)
@pyqtSlot()
def cancelNormalSwap(self):
assert self._swap
self.canCancel = False
self._fut_htlc_wait.cancel()

Loading…
Cancel
Save