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.
This commit is contained in:
@@ -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.")
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user