Browse Source

Give users an option to cancel a submarine swap while awaiting HTLCs.

Note that HTLCs must not be cancelled after the funding transaction
has been broadcast. If one want to cancel a swap once the funding
transaction is in mempool, one should double spend the transaction.
master
ThomasV 2 years ago
parent
commit
9df8bb61a5
  1. 21
      electrum/gui/qt/main_window.py
  2. 5
      electrum/gui/qt/send_tab.py
  3. 21
      electrum/gui/qt/swap_dialog.py
  4. 6
      electrum/gui/qt/util.py
  5. 16
      electrum/submarine_swaps.py

21
electrum/gui/qt/main_window.py

@ -299,6 +299,27 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
self._update_check_thread.checked.connect(on_version_received)
self._update_check_thread.start()
def run_coroutine_dialog(self, coro, text, on_result, on_cancelled):
""" run coroutine in a waiting dialog, with a Cancel button that cancels the coroutine """
from electrum import util
loop = util.get_asyncio_loop()
assert util.get_running_loop() != loop, 'must not be called from asyncio thread'
future = asyncio.run_coroutine_threadsafe(coro, loop)
def task():
try:
return future.result()
except concurrent.futures.CancelledError:
on_cancelled()
try:
WaitingDialog(
self, text, task,
on_success=on_result,
on_error=self.on_error,
on_cancel=future.cancel)
except Exception as e:
self.show_error(str(e))
raise
def run_coroutine_from_thread(self, coro, name, on_result=None):
if self._cleaned_up:
self.logger.warning(f"stopping or already stopped but run_coroutine_from_thread was called.")

5
electrum/gui/qt/send_tab.py

@ -724,7 +724,10 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
sm = self.wallet.lnworker.swap_manager
swap = sm.get_swap(tx.swap_payment_hash)
coro = sm.wait_for_htlcs_and_broadcast(swap, tx.swap_invoice, tx)
self.window.run_coroutine_from_thread(coro, _('Awaiting lightning payment..'), on_result=self.window.on_swap_result)
self.window.run_coroutine_dialog(
coro, _('Awaiting swap payment...'),
on_result=self.window.on_swap_result,
on_cancelled=lambda: sm.cancel_normal_swap(swap))
return
def broadcast_thread():

21
electrum/gui/qt/swap_dialog.py

@ -317,16 +317,25 @@ class SwapDialog(WindowModalDialog, QtEventListener):
self.ok_button.setEnabled(bool(send_amount) and bool(recv_amount))
def do_normal_swap(self, lightning_amount, onchain_amount, password):
tx = self._create_tx(onchain_amount)
assert tx
coro = self.swap_manager.normal_swap(
dummy_tx = self._create_tx(onchain_amount)
assert dummy_tx
sm = self.swap_manager
coro = sm.request_normal_swap(
lightning_amount_sat=lightning_amount,
expected_onchain_amount_sat=onchain_amount,
password=password,
tx=tx,
channels=self.channels,
)
self.window.run_coroutine_from_thread(coro, _('Swapping funds'), on_result=self.window.on_swap_result)
try:
swap, invoice = self.network.run_from_another_thread(coro)
except Exception as e:
self.window.show_error(str(e))
return
tx = sm.create_funding_tx(swap, dummy_tx, password)
coro2 = sm.wait_for_htlcs_and_broadcast(swap, invoice, tx)
self.window.run_coroutine_dialog(
coro2, _('Awaiting swap payment...'),
on_result=self.window.on_swap_result,
on_cancelled=lambda: sm.cancel_normal_swap(swap))
def get_description(self):
onchain_funds = "onchain funds"

6
electrum/gui/qt/util.py

@ -320,7 +320,7 @@ class WindowModalDialog(QDialog, MessageBoxMixin):
class WaitingDialog(WindowModalDialog):
'''Shows a please wait dialog whilst running a task. It is not
necessary to maintain a reference to this dialog.'''
def __init__(self, parent: QWidget, message: str, task, on_success=None, on_error=None):
def __init__(self, parent: QWidget, message: str, task, on_success=None, on_error=None, on_cancel=None):
assert parent
if isinstance(parent, MessageBoxMixin):
parent = parent.top_level_window()
@ -328,6 +328,10 @@ class WaitingDialog(WindowModalDialog):
self.message_label = QLabel(message)
vbox = QVBoxLayout(self)
vbox.addWidget(self.message_label)
if on_cancel:
self.cancel_button = CancelButton(self)
self.cancel_button.clicked.connect(on_cancel)
vbox.addLayout(Buttons(self.cancel_button))
self.accepted.connect(self.on_accepted)
self.show()
self.thread = TaskThread(self)

16
electrum/submarine_swaps.py

@ -252,7 +252,14 @@ class SwapManager(Logger):
continue
await self.taskgroup.spawn(self.pay_invoice(key))
def fail_normal_swap(self, swap):
def cancel_normal_swap(self, swap):
""" we must not have broadcast the funding tx """
if swap.funding_txid is not None:
self.logger.info(f'cannot fail swap {swap.payment_hash.hex()}: already funded')
return
self._fail_normal_swap(swap)
def _fail_normal_swap(self, swap):
if swap.payment_hash in self.lnworker.hold_invoice_callbacks:
self.logger.info(f'failing normal swap {swap.payment_hash.hex()}')
self.lnworker.unregister_hold_invoice(swap.payment_hash)
@ -282,7 +289,7 @@ class SwapManager(Logger):
if not swap.is_reverse:
# we might have received HTLCs and double spent the funding tx
# in that case we need to fail the HTLCs
self.fail_normal_swap(swap)
self._fail_normal_swap(swap)
txin = None
if txin:
@ -321,7 +328,7 @@ class SwapManager(Logger):
else:
# refund tx
if spent_height > 0:
self.fail_normal_swap(swap)
self._fail_normal_swap(swap)
return
if delta < 0:
# too early for refund
@ -688,7 +695,6 @@ class SwapManager(Logger):
else:
# broadcast funding tx right away
await self.broadcast_funding_tx(swap, tx)
# fixme: if broadcast fails, we need to fail htlcs and cancel the swap
return swap.funding_txid
def create_funding_tx(self, swap, tx, password):
@ -723,8 +729,8 @@ class SwapManager(Logger):
@log_exceptions
async def broadcast_funding_tx(self, swap, tx):
await self.network.broadcast_transaction(tx)
swap.funding_txid = tx.txid()
await self.network.broadcast_transaction(tx)
async def reverse_swap(
self,

Loading…
Cancel
Save