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.checked.connect(on_version_received)
|
||||||
self._update_check_thread.start()
|
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):
|
def run_coroutine_from_thread(self, coro, name, on_result=None):
|
||||||
if self._cleaned_up:
|
if self._cleaned_up:
|
||||||
self.logger.warning(f"stopping or already stopped but run_coroutine_from_thread was called.")
|
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
|
sm = self.wallet.lnworker.swap_manager
|
||||||
swap = sm.get_swap(tx.swap_payment_hash)
|
swap = sm.get_swap(tx.swap_payment_hash)
|
||||||
coro = sm.wait_for_htlcs_and_broadcast(swap, tx.swap_invoice, tx)
|
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
|
return
|
||||||
|
|
||||||
def broadcast_thread():
|
def broadcast_thread():
|
||||||
|
|||||||
@@ -317,16 +317,25 @@ class SwapDialog(WindowModalDialog, QtEventListener):
|
|||||||
self.ok_button.setEnabled(bool(send_amount) and bool(recv_amount))
|
self.ok_button.setEnabled(bool(send_amount) and bool(recv_amount))
|
||||||
|
|
||||||
def do_normal_swap(self, lightning_amount, onchain_amount, password):
|
def do_normal_swap(self, lightning_amount, onchain_amount, password):
|
||||||
tx = self._create_tx(onchain_amount)
|
dummy_tx = self._create_tx(onchain_amount)
|
||||||
assert tx
|
assert dummy_tx
|
||||||
coro = self.swap_manager.normal_swap(
|
sm = self.swap_manager
|
||||||
|
coro = sm.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=password,
|
|
||||||
tx=tx,
|
|
||||||
channels=self.channels,
|
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):
|
def get_description(self):
|
||||||
onchain_funds = "onchain funds"
|
onchain_funds = "onchain funds"
|
||||||
|
|||||||
@@ -320,7 +320,7 @@ class WindowModalDialog(QDialog, MessageBoxMixin):
|
|||||||
class WaitingDialog(WindowModalDialog):
|
class WaitingDialog(WindowModalDialog):
|
||||||
'''Shows a please wait dialog whilst running a task. It is not
|
'''Shows a please wait dialog whilst running a task. It is not
|
||||||
necessary to maintain a reference to this dialog.'''
|
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
|
assert parent
|
||||||
if isinstance(parent, MessageBoxMixin):
|
if isinstance(parent, MessageBoxMixin):
|
||||||
parent = parent.top_level_window()
|
parent = parent.top_level_window()
|
||||||
@@ -328,6 +328,10 @@ class WaitingDialog(WindowModalDialog):
|
|||||||
self.message_label = QLabel(message)
|
self.message_label = QLabel(message)
|
||||||
vbox = QVBoxLayout(self)
|
vbox = QVBoxLayout(self)
|
||||||
vbox.addWidget(self.message_label)
|
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.accepted.connect(self.on_accepted)
|
||||||
self.show()
|
self.show()
|
||||||
self.thread = TaskThread(self)
|
self.thread = TaskThread(self)
|
||||||
|
|||||||
@@ -252,7 +252,14 @@ class SwapManager(Logger):
|
|||||||
continue
|
continue
|
||||||
await self.taskgroup.spawn(self.pay_invoice(key))
|
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:
|
if swap.payment_hash in self.lnworker.hold_invoice_callbacks:
|
||||||
self.logger.info(f'failing normal swap {swap.payment_hash.hex()}')
|
self.logger.info(f'failing normal swap {swap.payment_hash.hex()}')
|
||||||
self.lnworker.unregister_hold_invoice(swap.payment_hash)
|
self.lnworker.unregister_hold_invoice(swap.payment_hash)
|
||||||
@@ -282,7 +289,7 @@ class SwapManager(Logger):
|
|||||||
if not swap.is_reverse:
|
if not swap.is_reverse:
|
||||||
# we might have received HTLCs and double spent the funding tx
|
# we might have received HTLCs and double spent the funding tx
|
||||||
# in that case we need to fail the HTLCs
|
# in that case we need to fail the HTLCs
|
||||||
self.fail_normal_swap(swap)
|
self._fail_normal_swap(swap)
|
||||||
txin = None
|
txin = None
|
||||||
|
|
||||||
if txin:
|
if txin:
|
||||||
@@ -321,7 +328,7 @@ class SwapManager(Logger):
|
|||||||
else:
|
else:
|
||||||
# refund tx
|
# refund tx
|
||||||
if spent_height > 0:
|
if spent_height > 0:
|
||||||
self.fail_normal_swap(swap)
|
self._fail_normal_swap(swap)
|
||||||
return
|
return
|
||||||
if delta < 0:
|
if delta < 0:
|
||||||
# too early for refund
|
# too early for refund
|
||||||
@@ -688,7 +695,6 @@ class SwapManager(Logger):
|
|||||||
else:
|
else:
|
||||||
# broadcast funding tx right away
|
# broadcast funding tx right away
|
||||||
await self.broadcast_funding_tx(swap, tx)
|
await self.broadcast_funding_tx(swap, tx)
|
||||||
# fixme: if broadcast fails, we need to fail htlcs and cancel the swap
|
|
||||||
return swap.funding_txid
|
return swap.funding_txid
|
||||||
|
|
||||||
def create_funding_tx(self, swap, tx, password):
|
def create_funding_tx(self, swap, tx, password):
|
||||||
@@ -723,8 +729,8 @@ class SwapManager(Logger):
|
|||||||
|
|
||||||
@log_exceptions
|
@log_exceptions
|
||||||
async def broadcast_funding_tx(self, swap, tx):
|
async def broadcast_funding_tx(self, swap, tx):
|
||||||
await self.network.broadcast_transaction(tx)
|
|
||||||
swap.funding_txid = tx.txid()
|
swap.funding_txid = tx.txid()
|
||||||
|
await self.network.broadcast_transaction(tx)
|
||||||
|
|
||||||
async def reverse_swap(
|
async def reverse_swap(
|
||||||
self,
|
self,
|
||||||
|
|||||||
Reference in New Issue
Block a user