qml: port cancel normal swap feature from desktop client
This commit is contained in:
@@ -35,7 +35,7 @@ ElDialog {
|
|||||||
text: swaphelper.userinfo
|
text: swaphelper.userinfo
|
||||||
iconStyle: swaphelper.state == SwapHelper.Started
|
iconStyle: swaphelper.state == SwapHelper.Started
|
||||||
? InfoTextArea.IconStyle.Spinner
|
? InfoTextArea.IconStyle.Spinner
|
||||||
: swaphelper.state == SwapHelper.Failed
|
: swaphelper.state == SwapHelper.Failed || swaphelper.state == SwapHelper.Cancelled
|
||||||
? InfoTextArea.IconStyle.Error
|
? InfoTextArea.IconStyle.Error
|
||||||
: swaphelper.state == SwapHelper.Success
|
: swaphelper.state == SwapHelper.Success
|
||||||
? InfoTextArea.IconStyle.Done
|
? InfoTextArea.IconStyle.Done
|
||||||
@@ -251,15 +251,31 @@ ElDialog {
|
|||||||
|
|
||||||
Item { Layout.fillHeight: true; Layout.preferredWidth: 1 }
|
Item { Layout.fillHeight: true; Layout.preferredWidth: 1 }
|
||||||
|
|
||||||
FlatButton {
|
ButtonContainer {
|
||||||
Layout.columnSpan: 2
|
Layout.columnSpan: 2
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
text: qsTr('Ok')
|
FlatButton {
|
||||||
icon.source: Qt.resolvedUrl('../../icons/confirmed.png')
|
Layout.fillWidth: true
|
||||||
enabled: swaphelper.valid && (swaphelper.state == SwapHelper.ServiceReady || swaphelper.state == SwapHelper.Failed)
|
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: {
|
onClicked: {
|
||||||
swaphelper.executeSwap()
|
swaphelper.executeSwap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FlatButton {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.preferredWidth: 1
|
||||||
|
text: qsTr('Cancel')
|
||||||
|
icon.source: Qt.resolvedUrl('../../icons/closebutton.png')
|
||||||
|
visible: swaphelper.canCancel
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
swaphelper.cancelNormalSwap()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
import concurrent
|
||||||
import threading
|
import threading
|
||||||
import math
|
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer, Q_ENUMS
|
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.i18n import _
|
||||||
from electrum.bitcoin import DummyAddress
|
from electrum.bitcoin import DummyAddress
|
||||||
from electrum.logging import get_logger
|
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 electrum.util import NotEnoughFunds, NoDynamicFeeEstimates, profiler, get_asyncio_loop
|
||||||
|
|
||||||
from .auth import AuthMixin, auth_protect
|
from .auth import AuthMixin, auth_protect
|
||||||
@@ -16,6 +16,10 @@ from .qetypes import QEAmount
|
|||||||
from .qewallet import QEWallet
|
from .qewallet import QEWallet
|
||||||
from .util import QtEventListener, qt_event_listener
|
from .util import QtEventListener, qt_event_listener
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidSwapParameters(Exception): pass
|
||||||
|
|
||||||
|
|
||||||
class QESwapHelper(AuthMixin, QObject, QtEventListener):
|
class QESwapHelper(AuthMixin, QObject, QtEventListener):
|
||||||
_logger = get_logger(__name__)
|
_logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -25,6 +29,7 @@ class QESwapHelper(AuthMixin, QObject, QtEventListener):
|
|||||||
Started = 2
|
Started = 2
|
||||||
Failed = 3
|
Failed = 3
|
||||||
Success = 4
|
Success = 4
|
||||||
|
Cancelled = 5
|
||||||
|
|
||||||
Q_ENUMS(State)
|
Q_ENUMS(State)
|
||||||
|
|
||||||
@@ -51,6 +56,9 @@ class QESwapHelper(AuthMixin, QObject, QtEventListener):
|
|||||||
self._server_miningfee = QEAmount()
|
self._server_miningfee = QEAmount()
|
||||||
self._miningfee = QEAmount()
|
self._miningfee = QEAmount()
|
||||||
self._isReverse = False
|
self._isReverse = False
|
||||||
|
self._canCancel = False
|
||||||
|
self._swap = None
|
||||||
|
self._fut_htlc_wait = None
|
||||||
|
|
||||||
self._service_available = False
|
self._service_available = False
|
||||||
self._send_amount = 0
|
self._send_amount = 0
|
||||||
@@ -225,6 +233,16 @@ class QESwapHelper(AuthMixin, QObject, QtEventListener):
|
|||||||
self._isReverse = isReverse
|
self._isReverse = isReverse
|
||||||
self.isReverseChanged.emit()
|
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):
|
def init_swap_slider_range(self):
|
||||||
lnworker = self._wallet.wallet.lnworker
|
lnworker = self._wallet.wallet.lnworker
|
||||||
@@ -346,20 +364,26 @@ class QESwapHelper(AuthMixin, QObject, QtEventListener):
|
|||||||
if lightning_amount is None or onchain_amount is None:
|
if lightning_amount is None or onchain_amount is None:
|
||||||
return
|
return
|
||||||
loop = get_asyncio_loop()
|
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,
|
lightning_amount_sat=lightning_amount,
|
||||||
expected_onchain_amount_sat=onchain_amount,
|
expected_onchain_amount_sat=onchain_amount,
|
||||||
password=self._wallet.password,
|
|
||||||
tx=self._tx,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def swap_task():
|
def swap_task():
|
||||||
try:
|
try:
|
||||||
|
dummy_tx = self._create_tx(onchain_amount)
|
||||||
fut = asyncio.run_coroutine_threadsafe(coro, loop)
|
fut = asyncio.run_coroutine_threadsafe(coro, loop)
|
||||||
self.userinfo = _('Performing swap...')
|
self.userinfo = _('Performing swap...')
|
||||||
self.state = QESwapHelper.State.Started
|
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()
|
txid = fut.result()
|
||||||
try: # swaphelper might be destroyed at this point
|
try: # swaphelper might be destroyed at this point
|
||||||
self.userinfo = ' '.join([
|
self.userinfo = ' '.join([
|
||||||
_('Success!'),
|
_('Success!'),
|
||||||
_('Your funding transaction has been broadcast.'),
|
_('Your funding transaction has been broadcast.'),
|
||||||
@@ -369,16 +393,51 @@ class QESwapHelper(AuthMixin, QObject, QtEventListener):
|
|||||||
self.state = QESwapHelper.State.Success
|
self.state = QESwapHelper.State.Success
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
pass
|
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:
|
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.state = QESwapHelper.State.Failed
|
||||||
self.userinfo = _('Error') + ': ' + str(e)
|
self.userinfo = _('Error') + ': ' + str(e)
|
||||||
self._logger.error(str(e))
|
self._logger.error(str(e))
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
pass
|
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()
|
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):
|
def do_reverse_swap(self, lightning_amount, onchain_amount):
|
||||||
if lightning_amount is None or onchain_amount is None:
|
if lightning_amount is None or onchain_amount is None:
|
||||||
return
|
return
|
||||||
@@ -394,9 +453,9 @@ class QESwapHelper(AuthMixin, QObject, QtEventListener):
|
|||||||
fut = asyncio.run_coroutine_threadsafe(coro, loop)
|
fut = asyncio.run_coroutine_threadsafe(coro, loop)
|
||||||
self.userinfo = _('Performing swap...')
|
self.userinfo = _('Performing swap...')
|
||||||
self.state = QESwapHelper.State.Started
|
self.state = QESwapHelper.State.Started
|
||||||
success = fut.result()
|
txid = fut.result()
|
||||||
try: # swaphelper might be destroyed at this point
|
try: # swaphelper might be destroyed at this point
|
||||||
if success:
|
if txid:
|
||||||
self.userinfo = ' '.join([
|
self.userinfo = ' '.join([
|
||||||
_('Success!'),
|
_('Success!'),
|
||||||
_('The funding transaction has been detected.'),
|
_('The funding transaction has been detected.'),
|
||||||
@@ -410,7 +469,7 @@ class QESwapHelper(AuthMixin, QObject, QtEventListener):
|
|||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
pass
|
pass
|
||||||
except Exception as e:
|
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.state = QESwapHelper.State.Failed
|
||||||
self.userinfo = _('Error') + ': ' + str(e)
|
self.userinfo = _('Error') + ': ' + str(e)
|
||||||
self._logger.error(str(e))
|
self._logger.error(str(e))
|
||||||
@@ -436,3 +495,9 @@ class QESwapHelper(AuthMixin, QObject, QtEventListener):
|
|||||||
lightning_amount = self._receive_amount
|
lightning_amount = self._receive_amount
|
||||||
onchain_amount = self._send_amount
|
onchain_amount = self._send_amount
|
||||||
self.do_normal_swap(lightning_amount, onchain_amount)
|
self.do_normal_swap(lightning_amount, onchain_amount)
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def cancelNormalSwap(self):
|
||||||
|
assert self._swap
|
||||||
|
self.canCancel = False
|
||||||
|
self._fut_htlc_wait.cancel()
|
||||||
|
|||||||
Reference in New Issue
Block a user