qt: expose swaps to address as Submarine Payments
Exposes reverse submarine swaps to an external/specific address in the TxEditor gui as "Submarine Payments". The user can enter a onchain address in the Send Tab and then pay it from the lightning balance in the send tab by enabling the Submarine Payments option in the TxEditor dialog menu and switching to the Submarine Payment tab in the Tab bar.
This commit is contained in:
@@ -23,50 +23,61 @@
|
|||||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
# SOFTWARE.
|
# SOFTWARE.
|
||||||
|
|
||||||
|
import asyncio
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from typing import TYPE_CHECKING, Optional, Union, Callable
|
from typing import TYPE_CHECKING, Optional, Union
|
||||||
|
|
||||||
|
from electrum_aionostr.util import from_nip19
|
||||||
from PyQt6.QtCore import Qt, QTimer
|
from PyQt6.QtCore import Qt, QTimer
|
||||||
from PyQt6.QtGui import QIcon
|
from PyQt6.QtGui import QIcon
|
||||||
from PyQt6.QtWidgets import QHBoxLayout, QVBoxLayout, QLabel, QGridLayout, QPushButton, QToolButton, QMenu, QComboBox
|
from PyQt6.QtWidgets import (QHBoxLayout, QVBoxLayout, QLabel, QGridLayout, QPushButton, QToolButton,
|
||||||
|
QComboBox, QTabWidget, QWidget, QStackedWidget)
|
||||||
|
|
||||||
from electrum.i18n import _
|
from electrum.i18n import _
|
||||||
from electrum.util import NotEnoughFunds, NoDynamicFeeEstimates
|
from electrum.util import (quantize_feerate, profiler, NotEnoughFunds, NoDynamicFeeEstimates,
|
||||||
from electrum.util import quantize_feerate
|
get_asyncio_loop, wait_for2, nostr_pow_worker)
|
||||||
from electrum.plugin import run_hook
|
from electrum.plugin import run_hook
|
||||||
from electrum.transaction import Transaction, PartialTransaction
|
from electrum.transaction import PartialTransaction, TxOutput
|
||||||
from electrum.wallet import InternalAddressCorruption
|
from electrum.wallet import InternalAddressCorruption
|
||||||
from electrum.bitcoin import DummyAddress
|
from electrum.bitcoin import DummyAddress
|
||||||
from electrum.fee_policy import FeePolicy, FixedFeePolicy, FeeMethod
|
from electrum.fee_policy import FeePolicy, FixedFeePolicy, FeeMethod
|
||||||
|
from electrum.logging import Logger
|
||||||
|
from electrum.submarine_swaps import NostrTransport, HttpTransport
|
||||||
|
from .seed_dialog import seed_warning_msg
|
||||||
|
|
||||||
from .util import (WindowModalDialog, ColorScheme, HelpLabel, Buttons, CancelButton,
|
from .util import (WindowModalDialog, ColorScheme, HelpLabel, Buttons, CancelButton, WWLabel,
|
||||||
WWLabel, read_QIcon)
|
read_QIcon, debug_widget_layouts, qt_event_listener, QtEventListener, IconLabel)
|
||||||
from .transaction_dialog import TxSizeLabel, TxFiatLabel, TxInOutWidget
|
from .transaction_dialog import TxSizeLabel, TxFiatLabel, TxInOutWidget
|
||||||
from .fee_slider import FeeSlider, FeeComboBox
|
from .fee_slider import FeeSlider, FeeComboBox
|
||||||
from .amountedit import FeerateEdit, BTCAmountEdit
|
from .amountedit import FeerateEdit, BTCAmountEdit
|
||||||
from .locktimeedit import LockTimeEdit
|
from .locktimeedit import LockTimeEdit
|
||||||
from .my_treeview import QMenuWithConfig
|
from .my_treeview import QMenuWithConfig
|
||||||
|
from .swap_dialog import SwapServerDialog
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .main_window import ElectrumWindow
|
from .main_window import ElectrumWindow
|
||||||
|
|
||||||
|
|
||||||
class TxEditor(WindowModalDialog):
|
class TxEditor(WindowModalDialog, QtEventListener, Logger):
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, *, title='',
|
self, *, title='',
|
||||||
window: 'ElectrumWindow',
|
window: 'ElectrumWindow',
|
||||||
make_tx,
|
make_tx,
|
||||||
output_value: Union[int, str],
|
output_value: Union[int, str],
|
||||||
|
payee_outputs: Optional[list[TxOutput]] = None,
|
||||||
allow_preview=True,
|
allow_preview=True,
|
||||||
batching_candidates=None,
|
batching_candidates=None,
|
||||||
):
|
):
|
||||||
|
|
||||||
WindowModalDialog.__init__(self, window, title=title)
|
WindowModalDialog.__init__(self, window, title=title)
|
||||||
|
Logger.__init__(self)
|
||||||
self.main_window = window
|
self.main_window = window
|
||||||
self.make_tx = make_tx
|
self.make_tx = make_tx
|
||||||
self.output_value = output_value
|
self.output_value = output_value
|
||||||
|
# used only for submarine payments as they construct tx independently of make_tx
|
||||||
|
self.payee_outputs = payee_outputs
|
||||||
self.tx = None # type: Optional[PartialTransaction]
|
self.tx = None # type: Optional[PartialTransaction]
|
||||||
self.messages = []
|
self.messages = []
|
||||||
self.error = '' # set by side effect
|
self.error = '' # set by side effect
|
||||||
@@ -85,40 +96,87 @@ class TxEditor(WindowModalDialog):
|
|||||||
self._base_tx = None # for batching
|
self._base_tx = None # for batching
|
||||||
self.batching_candidates = batching_candidates
|
self.batching_candidates = batching_candidates
|
||||||
|
|
||||||
|
self.swap_manager = self.wallet.lnworker.swap_manager if self.wallet.has_lightning() else None
|
||||||
|
self.swap_transport = None # type: Optional[Union[NostrTransport, HttpTransport]]
|
||||||
|
self.ongoing_swap_transport_connection_attempt = None # type: Optional[asyncio.Task]
|
||||||
|
self.did_swap = False # used to clear the PI on send tab
|
||||||
|
|
||||||
self.locktime_e = LockTimeEdit(self)
|
self.locktime_e = LockTimeEdit(self)
|
||||||
self.locktime_e.valueEdited.connect(self.trigger_update)
|
self.locktime_e.valueEdited.connect(self.trigger_update)
|
||||||
self.locktime_label = QLabel(_("LockTime") + ": ")
|
self.locktime_label = QLabel(_("LockTime") + ": ")
|
||||||
self.io_widget = TxInOutWidget(self.main_window, self.wallet)
|
self.io_widget = TxInOutWidget(self.main_window, self.wallet)
|
||||||
self.create_fee_controls()
|
self.create_fee_controls()
|
||||||
|
|
||||||
vbox = QVBoxLayout()
|
onchain_vbox = QVBoxLayout()
|
||||||
self.setLayout(vbox)
|
onchain_top = self.create_top_bar(self.help_text)
|
||||||
|
onchain_grid = self.create_grid()
|
||||||
top = self.create_top_bar(self.help_text)
|
onchain_vbox.addLayout(onchain_top)
|
||||||
grid = self.create_grid()
|
onchain_vbox.addLayout(onchain_grid)
|
||||||
|
onchain_vbox.addWidget(self.io_widget)
|
||||||
vbox.addLayout(top)
|
|
||||||
vbox.addLayout(grid)
|
|
||||||
vbox.addWidget(self.io_widget)
|
|
||||||
self.message_label = WWLabel('')
|
self.message_label = WWLabel('')
|
||||||
self.message_label.setMinimumHeight(70)
|
self.message_label.setMinimumHeight(70)
|
||||||
vbox.addWidget(self.message_label)
|
onchain_vbox.addWidget(self.message_label)
|
||||||
|
|
||||||
buttons = self.create_buttons_bar()
|
onchain_buttons = self.create_buttons_bar()
|
||||||
vbox.addStretch(1)
|
onchain_vbox.addStretch(1)
|
||||||
vbox.addLayout(buttons)
|
onchain_vbox.addLayout(onchain_buttons)
|
||||||
|
|
||||||
|
# onchain tab is the main tab and the content is also shown if tabs are disabled
|
||||||
|
self.onchain_tab = QWidget()
|
||||||
|
self.onchain_tab.setContentsMargins(0,0,0,0)
|
||||||
|
self.onchain_tab.setLayout(onchain_vbox)
|
||||||
|
|
||||||
|
# optional submarine payment tab, the tab is only shown if the option is enabled
|
||||||
|
self.submarine_payment_tab = self.create_submarine_payment_tab()
|
||||||
|
|
||||||
|
self.tab_widget = QTabWidget()
|
||||||
|
self.tab_widget.setTabBarAutoHide(True) # hides the tab bar if there is only one tab
|
||||||
|
self.tab_widget.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.tab_widget.tabBarClicked.connect(self.on_tab_clicked)
|
||||||
|
|
||||||
|
self.main_layout = QVBoxLayout()
|
||||||
|
self.main_layout.addWidget(self.tab_widget)
|
||||||
|
self.main_layout.setContentsMargins(6, 6, 6, 6) # reduce outermost margins a bit
|
||||||
|
self.setLayout(self.main_layout)
|
||||||
|
|
||||||
self.set_io_visible()
|
self.set_io_visible()
|
||||||
self.set_fee_edit_visible()
|
self.set_fee_edit_visible()
|
||||||
self.set_locktime_visible()
|
self.set_locktime_visible()
|
||||||
self.update_fee_target()
|
self.update_fee_target()
|
||||||
self.resize(self.layout().sizeHint())
|
self.update_tab_visibility()
|
||||||
|
self.resize_to_fit_content()
|
||||||
|
|
||||||
self.timer = QTimer(self)
|
self.timer = QTimer(self)
|
||||||
self.timer.setInterval(500)
|
self.timer.setInterval(500)
|
||||||
self.timer.setSingleShot(False)
|
self.timer.setSingleShot(False)
|
||||||
self.timer.timeout.connect(self.timer_actions)
|
self.timer.timeout.connect(self.timer_actions)
|
||||||
self.timer.start()
|
self.timer.start()
|
||||||
|
self.register_callbacks()
|
||||||
|
# debug_widget_layouts(self) # enable to show red lines around all elements
|
||||||
|
|
||||||
|
def accept(self):
|
||||||
|
self._cleanup()
|
||||||
|
super().accept()
|
||||||
|
|
||||||
|
def reject(self):
|
||||||
|
self._cleanup()
|
||||||
|
super().reject()
|
||||||
|
|
||||||
|
def closeEvent(self, event):
|
||||||
|
self._cleanup()
|
||||||
|
super().closeEvent(event)
|
||||||
|
|
||||||
|
def _cleanup(self):
|
||||||
|
self.unregister_callbacks()
|
||||||
|
if self.ongoing_swap_transport_connection_attempt:
|
||||||
|
self.ongoing_swap_transport_connection_attempt.cancel()
|
||||||
|
if isinstance(self.swap_transport, NostrTransport):
|
||||||
|
asyncio.run_coroutine_threadsafe(self.swap_transport.stop(), get_asyncio_loop())
|
||||||
|
self.swap_transport = None # HTTPTransport doesn't need to be closed
|
||||||
|
|
||||||
|
def on_tab_clicked(self, index):
|
||||||
|
if self.tab_widget.widget(index) == self.submarine_payment_tab:
|
||||||
|
self.prepare_swap_transport()
|
||||||
|
|
||||||
def is_batching(self) -> bool:
|
def is_batching(self) -> bool:
|
||||||
return self._base_tx is not None
|
return self._base_tx is not None
|
||||||
@@ -140,6 +198,13 @@ class TxEditor(WindowModalDialog):
|
|||||||
# expected to set self.tx, self.message and self.error
|
# expected to set self.tx, self.message and self.error
|
||||||
raise NotImplementedError()
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
def create_grid(self) -> QGridLayout:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def help_text(self) -> str:
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
def update_fee_target(self):
|
def update_fee_target(self):
|
||||||
if self.fee_slider.is_active():
|
if self.fee_slider.is_active():
|
||||||
text = self.fee_policy.get_target_text()
|
text = self.fee_policy.get_target_text()
|
||||||
@@ -230,6 +295,32 @@ class TxEditor(WindowModalDialog):
|
|||||||
self.fee_slider.setFixedWidth(200)
|
self.fee_slider.setFixedWidth(200)
|
||||||
self.fee_target.setFixedSize(self.feerate_e.sizeHint())
|
self.fee_target.setFixedSize(self.feerate_e.sizeHint())
|
||||||
|
|
||||||
|
def update_tab_visibility(self):
|
||||||
|
"""Update self.tab_widget to show all tabs that are enabled."""
|
||||||
|
# first remove all tabs
|
||||||
|
while self.tab_widget.count() > 0:
|
||||||
|
self.tab_widget.removeTab(0)
|
||||||
|
|
||||||
|
# always show onchain payment tab
|
||||||
|
self.tab_widget.addTab(self.onchain_tab, _('Onchain'))
|
||||||
|
|
||||||
|
allow_swaps = self.allow_preview and self.payee_outputs # allow_preview is false for ln channel opening txs
|
||||||
|
if self.config.WALLET_ENABLE_SUBMARINE_PAYMENTS and allow_swaps:
|
||||||
|
i = self.tab_widget.addTab(self.submarine_payment_tab, _('Submarine Payment'))
|
||||||
|
tooltip = self.config.cv.WALLET_ENABLE_SUBMARINE_PAYMENTS.get_long_desc()
|
||||||
|
if len(self.payee_outputs) > 1:
|
||||||
|
self.tab_widget.setTabEnabled(i, False)
|
||||||
|
tooltip = _("Submarine Payments don't support multiple outputs (Pay-to-many).")
|
||||||
|
elif self.payee_outputs[0].value == '!':
|
||||||
|
self.tab_widget.setTabEnabled(i, False)
|
||||||
|
self.submarine_payment_tab.setEnabled(False)
|
||||||
|
tooltip = _("Submarine Payments don't support 'Max' value spends.")
|
||||||
|
self.tab_widget.tabBar().setTabToolTip(i, tooltip)
|
||||||
|
|
||||||
|
# enable document mode if there is only one tab to hide the frame
|
||||||
|
self.tab_widget.setDocumentMode(self.tab_widget.count() < 2)
|
||||||
|
self.resize_to_fit_content()
|
||||||
|
|
||||||
def trigger_update(self):
|
def trigger_update(self):
|
||||||
# set tx to None so that the ok button is disabled while we compute the new tx
|
# set tx to None so that the ok button is disabled while we compute the new tx
|
||||||
self.tx = None
|
self.tx = None
|
||||||
@@ -311,7 +402,6 @@ class TxEditor(WindowModalDialog):
|
|||||||
feerate_color = ColorScheme.BLUE
|
feerate_color = ColorScheme.BLUE
|
||||||
self.fee_e.setStyleSheet(fee_color.as_stylesheet())
|
self.fee_e.setStyleSheet(fee_color.as_stylesheet())
|
||||||
self.feerate_e.setStyleSheet(feerate_color.as_stylesheet())
|
self.feerate_e.setStyleSheet(feerate_color.as_stylesheet())
|
||||||
#
|
|
||||||
self.needs_update = True
|
self.needs_update = True
|
||||||
|
|
||||||
def update_fee_fields(self):
|
def update_fee_fields(self):
|
||||||
@@ -411,6 +501,7 @@ class TxEditor(WindowModalDialog):
|
|||||||
self.pref_menu.addConfig(self.config.cv.GUI_QT_TX_EDITOR_SHOW_LOCKTIME, callback=cb)
|
self.pref_menu.addConfig(self.config.cv.GUI_QT_TX_EDITOR_SHOW_LOCKTIME, callback=cb)
|
||||||
self.pref_menu.addSeparator()
|
self.pref_menu.addSeparator()
|
||||||
self.pref_menu.addConfig(self.config.cv.WALLET_SEND_CHANGE_TO_LIGHTNING, callback=self.trigger_update)
|
self.pref_menu.addConfig(self.config.cv.WALLET_SEND_CHANGE_TO_LIGHTNING, callback=self.trigger_update)
|
||||||
|
self.pref_menu.addConfig(self.config.cv.WALLET_ENABLE_SUBMARINE_PAYMENTS, callback=self.update_tab_visibility)
|
||||||
self.pref_menu.addToggle(
|
self.pref_menu.addToggle(
|
||||||
_('Use change addresses'),
|
_('Use change addresses'),
|
||||||
self.toggle_use_change,
|
self.toggle_use_change,
|
||||||
@@ -442,11 +533,12 @@ class TxEditor(WindowModalDialog):
|
|||||||
hbox.addWidget(self.pref_button)
|
hbox.addWidget(self.pref_button)
|
||||||
return hbox
|
return hbox
|
||||||
|
|
||||||
|
@profiler(min_threshold=0.02)
|
||||||
def resize_to_fit_content(self):
|
def resize_to_fit_content(self):
|
||||||
# fixme: calling resize once is not enough...
|
# update all geometries so the updated size hints are used for size adjustment
|
||||||
size = self.layout().sizeHint()
|
for widget in self.findChildren(QWidget):
|
||||||
self.resize(size)
|
widget.updateGeometry()
|
||||||
self.resize(size)
|
self.adjustSize()
|
||||||
|
|
||||||
def toggle_use_change(self):
|
def toggle_use_change(self):
|
||||||
self.wallet.use_change = not self.wallet.use_change
|
self.wallet.use_change = not self.wallet.use_change
|
||||||
@@ -593,17 +685,343 @@ class TxEditor(WindowModalDialog):
|
|||||||
def can_pay_assuming_zero_fees(self, confirmed_only: bool) -> bool:
|
def can_pay_assuming_zero_fees(self, confirmed_only: bool) -> bool:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
### --- Functionality for reverse submarine swaps to external address ---
|
||||||
|
def create_submarine_payment_tab(self) -> QWidget:
|
||||||
|
"""Returns widget for submarine payment functionality to be added as tab"""
|
||||||
|
tab_widget = QWidget()
|
||||||
|
vbox = QVBoxLayout(tab_widget)
|
||||||
|
|
||||||
|
# stack two views, a warning view and the regular one. The warning view is shown if
|
||||||
|
# the swap cannot be performed, e.g. due to missing liquidity.
|
||||||
|
self.submarine_stacked_widget = QStackedWidget()
|
||||||
|
|
||||||
|
# Normal layout page
|
||||||
|
normal_page = QWidget()
|
||||||
|
h = QGridLayout(normal_page)
|
||||||
|
self.submarine_lightning_send_amount_label = QLabel()
|
||||||
|
self.submarine_onchain_send_amount_label = QLabel()
|
||||||
|
self.submarine_claim_mining_fee_label = QLabel()
|
||||||
|
self.submarine_server_fee_label = QLabel()
|
||||||
|
self.submarine_we_send_label = IconLabel(text=_('You send')+':')
|
||||||
|
self.submarine_we_send_label.setIcon(read_QIcon('lightning.png'))
|
||||||
|
self.submarine_they_receive_label = IconLabel(text=_('They receive')+':')
|
||||||
|
self.submarine_they_receive_label.setIcon(read_QIcon('bitcoin.png'))
|
||||||
|
h.addWidget(self.submarine_we_send_label, 0, 0)
|
||||||
|
h.addWidget(self.submarine_lightning_send_amount_label, 0, 1)
|
||||||
|
h.addWidget(self.submarine_they_receive_label, 1, 0)
|
||||||
|
h.addWidget(self.submarine_onchain_send_amount_label, 1, 1)
|
||||||
|
h.addWidget(QLabel(_('Swap fee')+':'), 2, 0)
|
||||||
|
h.addWidget(self.submarine_server_fee_label, 2, 1, 1, 2)
|
||||||
|
h.addWidget(QLabel(_('Mining fee')+':'), 3, 0)
|
||||||
|
h.addWidget(self.submarine_claim_mining_fee_label, 3, 1, 1, 2)
|
||||||
|
|
||||||
|
# Warning layout page
|
||||||
|
warning_page = QWidget()
|
||||||
|
warning_layout = QVBoxLayout(warning_page)
|
||||||
|
self.submarine_warning_label = QLabel('')
|
||||||
|
warning_layout.addWidget(self.submarine_warning_label)
|
||||||
|
|
||||||
|
self.submarine_stacked_widget.addWidget(normal_page)
|
||||||
|
self.submarine_stacked_widget.addWidget(warning_page)
|
||||||
|
|
||||||
|
vbox.addWidget(self.submarine_stacked_widget)
|
||||||
|
vbox.addStretch(1)
|
||||||
|
|
||||||
|
self.server_button = QPushButton()
|
||||||
|
self.server_button.clicked.connect(self.choose_swap_server)
|
||||||
|
self.server_button.setEnabled(False)
|
||||||
|
self.server_button.setVisible(False)
|
||||||
|
|
||||||
|
self.submarine_ok_button = QPushButton(_('OK'))
|
||||||
|
self.submarine_ok_button.setDefault(True)
|
||||||
|
self.submarine_ok_button.setEnabled(False)
|
||||||
|
# pay button must not self.accept() as this triggers closing the transport
|
||||||
|
self.submarine_ok_button.clicked.connect(self.start_submarine_swap)
|
||||||
|
|
||||||
|
buttons = Buttons(CancelButton(self), self.submarine_ok_button)
|
||||||
|
buttons.insertWidget(0, self.server_button)
|
||||||
|
vbox.addLayout(buttons)
|
||||||
|
|
||||||
|
return tab_widget
|
||||||
|
|
||||||
|
def show_swap_transport_connection_message(self):
|
||||||
|
self.submarine_stacked_widget.setCurrentIndex(1)
|
||||||
|
self.submarine_warning_label.setText(_("Connecting, please wait..."))
|
||||||
|
self.submarine_ok_button.setEnabled(False)
|
||||||
|
|
||||||
|
def prepare_swap_transport(self):
|
||||||
|
if self.swap_transport is not None and self.swap_transport.is_connected.is_set():
|
||||||
|
# we already have a connected transport, no need to create a new one
|
||||||
|
return
|
||||||
|
if self.ongoing_swap_transport_connection_attempt \
|
||||||
|
and not self.ongoing_swap_transport_connection_attempt.done():
|
||||||
|
# another task is currently trying to connect
|
||||||
|
return
|
||||||
|
|
||||||
|
# there should only be a connected transport.
|
||||||
|
# a useless transport should get cleaned up and not stored.
|
||||||
|
assert self.swap_transport is None, "swap transport wasn't cleaned up properly"
|
||||||
|
|
||||||
|
# give user feedback that we are connection now
|
||||||
|
self.show_swap_transport_connection_message()
|
||||||
|
new_swap_transport = self.main_window.create_sm_transport()
|
||||||
|
if not new_swap_transport:
|
||||||
|
# user declined to enable Nostr and has no http server configured
|
||||||
|
self.update_submarine_tab()
|
||||||
|
return
|
||||||
|
|
||||||
|
async def connect_and_update_tab(transport):
|
||||||
|
try:
|
||||||
|
await self.initialize_swap_transport(transport)
|
||||||
|
self.update_submarine_tab()
|
||||||
|
except Exception:
|
||||||
|
self.logger.exception("failed to create swap transport")
|
||||||
|
|
||||||
|
task = asyncio.run_coroutine_threadsafe(
|
||||||
|
connect_and_update_tab(new_swap_transport),
|
||||||
|
get_asyncio_loop(),
|
||||||
|
)
|
||||||
|
# this task will get cancelled if the TxEditor gets closed
|
||||||
|
self.ongoing_swap_transport_connection_attempt = task
|
||||||
|
|
||||||
|
async def initialize_swap_transport(self, new_swap_transport):
|
||||||
|
# start the transport
|
||||||
|
if isinstance(new_swap_transport, NostrTransport):
|
||||||
|
asyncio.create_task(new_swap_transport.main_loop())
|
||||||
|
else:
|
||||||
|
assert isinstance(new_swap_transport, HttpTransport)
|
||||||
|
asyncio.create_task(new_swap_transport.get_pairs_just_once())
|
||||||
|
# wait for the transport to be connected
|
||||||
|
if not await self.wait_for_swap_transport(new_swap_transport):
|
||||||
|
return
|
||||||
|
self.swap_transport = new_swap_transport
|
||||||
|
|
||||||
|
async def wait_for_swap_transport(self, new_swap_transport: Union[HttpTransport, NostrTransport]) -> bool:
|
||||||
|
"""
|
||||||
|
Wait until we found the announcement event of the configured swap server.
|
||||||
|
If it is not found but the relay connection is established return True anyway,
|
||||||
|
the user will then need to select a different swap server.
|
||||||
|
"""
|
||||||
|
timeout = new_swap_transport.connect_timeout + 1
|
||||||
|
try:
|
||||||
|
# swap_manager.is_initialized gets set once we got pairs of the configured swap server
|
||||||
|
await wait_for2(self.swap_manager.is_initialized.wait(), timeout)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
self.logger.debug(f"swap transport initialization timed out after {timeout} sec")
|
||||||
|
except Exception:
|
||||||
|
self.logger.exception("failed to initialize swap transport")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.swap_manager.is_initialized.is_set():
|
||||||
|
return True
|
||||||
|
|
||||||
|
# timed out above
|
||||||
|
if self.config.SWAPSERVER_URL:
|
||||||
|
# http swapserver didn't return pairs
|
||||||
|
self.logger.error(f"couldn't request pairs from {self.config.SWAPSERVER_URL=}")
|
||||||
|
return False
|
||||||
|
elif new_swap_transport.is_connected.is_set():
|
||||||
|
assert isinstance(new_swap_transport, NostrTransport)
|
||||||
|
# couldn't find announcement of configured swapserver, maybe it is gone.
|
||||||
|
# update_submarine_tab will tell the user to select a different swap server.
|
||||||
|
return True
|
||||||
|
|
||||||
|
# we couldn't even connect to the relays, this transport is useless. maybe network issues.
|
||||||
|
return False
|
||||||
|
|
||||||
|
def choose_swap_server(self) -> None:
|
||||||
|
assert isinstance(self.swap_transport, NostrTransport), self.swap_transport
|
||||||
|
self.main_window.choose_swapserver_dialog(self.swap_transport)
|
||||||
|
self.update_submarine_tab()
|
||||||
|
|
||||||
|
def start_submarine_swap(self):
|
||||||
|
assert self.payee_outputs and len(self.payee_outputs) == 1
|
||||||
|
payee_output = self.payee_outputs[0]
|
||||||
|
|
||||||
|
assert self.expected_onchain_amount_sat is not None
|
||||||
|
assert self.lightning_send_amount_sat is not None
|
||||||
|
assert self.last_server_mining_fee_sat is not None
|
||||||
|
assert self.swap_transport.is_connected.is_set()
|
||||||
|
assert self.swap_manager.is_initialized.is_set()
|
||||||
|
|
||||||
|
self.tx = None # prevent broadcasting
|
||||||
|
self.submarine_ok_button.setEnabled(False)
|
||||||
|
coro = self.swap_manager.reverse_swap(
|
||||||
|
transport=self.swap_transport,
|
||||||
|
lightning_amount_sat=self.lightning_send_amount_sat,
|
||||||
|
expected_onchain_amount_sat=self.expected_onchain_amount_sat,
|
||||||
|
prepayment_sat=2 * self.last_server_mining_fee_sat,
|
||||||
|
claim_to_output=payee_output,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
funding_txid = self.main_window.run_coroutine_dialog(coro, _('Initiating Submarine Payment...'))
|
||||||
|
except Exception as e:
|
||||||
|
self.close()
|
||||||
|
self.main_window.show_error(_("Submarine Payment failed:") + "\n" + str(e))
|
||||||
|
return
|
||||||
|
self.did_swap = True
|
||||||
|
# accepting closes the swap transport, so it needs to happen after the swap
|
||||||
|
self.accept()
|
||||||
|
self.main_window.on_swap_result(funding_txid, is_reverse=True)
|
||||||
|
|
||||||
|
def update_submarine_tab(self):
|
||||||
|
assert self.payee_outputs, "Opened submarine payment tab without outputs?"
|
||||||
|
assert len(self.payee_outputs) == \
|
||||||
|
len([o for o in self.payee_outputs if not o.is_change and not isinstance(o.value, str)])
|
||||||
|
f = self.main_window.format_amount_and_units
|
||||||
|
self.logger.debug(f"TxEditor updating submarine tab")
|
||||||
|
|
||||||
|
if not self.swap_manager:
|
||||||
|
self.set_swap_tab_warning(_("Enable Lightning in the 'Channels' tab to use Submarine Swaps."))
|
||||||
|
return
|
||||||
|
if not self.swap_transport:
|
||||||
|
# couldn't connect to nostr relays or http server didn't respond
|
||||||
|
self.set_swap_tab_warning(_("Submarine swap provider unavailable."))
|
||||||
|
return
|
||||||
|
|
||||||
|
# Update the swapserver selection button text
|
||||||
|
if isinstance(self.swap_transport, NostrTransport):
|
||||||
|
self.server_button.setVisible(True)
|
||||||
|
self.server_button.setEnabled(True)
|
||||||
|
if self.config.SWAPSERVER_NPUB:
|
||||||
|
pubkey = from_nip19(self.config.SWAPSERVER_NPUB)['object'].hex()
|
||||||
|
self.server_button.setIcon(SwapServerDialog.pubkey_to_q_icon(pubkey))
|
||||||
|
self.server_button.setText(
|
||||||
|
f' {len(self.swap_transport.get_recent_offers())} ' +
|
||||||
|
(_('providers') if len(self.swap_transport.get_recent_offers()) != 1 else _('provider'))
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# HTTPTransport or no Network, not showing server selection button
|
||||||
|
self.server_button.setEnabled(False)
|
||||||
|
self.server_button.setVisible(False)
|
||||||
|
|
||||||
|
if not self.swap_manager.is_initialized.is_set():
|
||||||
|
# connected to nostr relays but couldn't find swapserver announcement
|
||||||
|
assert isinstance(self.swap_transport, NostrTransport), "HTTPTransport shouldn't get set if it cannot fetch pairs"
|
||||||
|
assert self.swap_transport.is_connected.is_set(), "closed transport wasn't cleaned up"
|
||||||
|
if self.config.SWAPSERVER_NPUB:
|
||||||
|
msg = _("Couldn't connect to your swap provider. Please select a different provider.")
|
||||||
|
else:
|
||||||
|
msg = _('Please select a submarine swap provider.')
|
||||||
|
self.set_swap_tab_warning(msg)
|
||||||
|
return
|
||||||
|
|
||||||
|
# update values
|
||||||
|
self.lightning_send_amount_sat = self.swap_manager.get_send_amount(
|
||||||
|
self.payee_outputs[0].value, # claim tx fee reserve gets added in get_send_amount
|
||||||
|
is_reverse=True,
|
||||||
|
)
|
||||||
|
self.last_server_mining_fee_sat = self.swap_manager.mining_fee
|
||||||
|
self.expected_onchain_amount_sat = (
|
||||||
|
self.payee_outputs[0].value + self.swap_manager.get_fee_for_txbatcher()
|
||||||
|
)
|
||||||
|
|
||||||
|
# get warning
|
||||||
|
warning_text = self.get_swap_warning()
|
||||||
|
if warning_text:
|
||||||
|
self.set_swap_tab_warning(warning_text)
|
||||||
|
return
|
||||||
|
|
||||||
|
# There is no warning, show the normal view (amounts etc.)
|
||||||
|
self.submarine_stacked_widget.setCurrentIndex(0)
|
||||||
|
|
||||||
|
# label showing the payment amount (the amount the user entered in SendTab)
|
||||||
|
self.submarine_onchain_send_amount_label.setText(f(self.payee_outputs[0].value))
|
||||||
|
|
||||||
|
# the fee we pay to claim the funding output to the onchain address, shown as "Mining Fee"
|
||||||
|
claim_tx_mining_fee = self.swap_manager.get_fee_for_txbatcher()
|
||||||
|
self.submarine_claim_mining_fee_label.setText(f(claim_tx_mining_fee))
|
||||||
|
|
||||||
|
assert self.lightning_send_amount_sat is not None
|
||||||
|
self.submarine_lightning_send_amount_label.setText(f(self.lightning_send_amount_sat))
|
||||||
|
# complete fee we pay to the server
|
||||||
|
server_fee = self.lightning_send_amount_sat - self.expected_onchain_amount_sat
|
||||||
|
self.submarine_server_fee_label.setText(f(server_fee))
|
||||||
|
|
||||||
|
self.submarine_ok_button.setEnabled(True)
|
||||||
|
|
||||||
|
def get_swap_warning(self) -> Optional[str]:
|
||||||
|
f = self.main_window.format_amount_and_units
|
||||||
|
ln_can_send = int(self.wallet.lnworker.num_sats_can_send())
|
||||||
|
|
||||||
|
if self.expected_onchain_amount_sat < self.swap_manager.get_min_amount():
|
||||||
|
return '\n'.join([
|
||||||
|
_("Payment amount below the minimum possible swap amount."),
|
||||||
|
_("You need to send a higher amount to be able to do a Submarine Payment."),
|
||||||
|
_("Minimum amount: {}").format(f(self.swap_manager.get_min_amount())),
|
||||||
|
])
|
||||||
|
|
||||||
|
too_low_outbound_liquidity_msg = '\n'.join([
|
||||||
|
_("You don't have enough outgoing capacity in your lightning channels."),
|
||||||
|
_("To add outgoing capacity you can open a new lightning channel or do a submarine swap."),
|
||||||
|
_("Your lightning channels can send: {}").format(f(ln_can_send)),
|
||||||
|
])
|
||||||
|
|
||||||
|
# prioritize showing the swap provider liquidity warning before the channel liquidity warning
|
||||||
|
# as it could be annoying for the user to be told to open a new channel just to come back to
|
||||||
|
# notice there is no provider supporting their swap amount
|
||||||
|
if self.lightning_send_amount_sat is None:
|
||||||
|
provider_liquidity = self.swap_manager.get_provider_max_forward_amount()
|
||||||
|
if provider_liquidity < self.swap_manager.get_min_amount():
|
||||||
|
provider_liquidity = 0
|
||||||
|
msg = [
|
||||||
|
_("The selected swap provider is unable to offer a forward swap of this value."),
|
||||||
|
_("In order to continue select a different provider or try to send a smaller amount."),
|
||||||
|
_("Available liquidity") + f": {f(provider_liquidity)}",
|
||||||
|
]
|
||||||
|
# we don't know exactly how much we need to send on ln yet, so we can assume 0 provider fees
|
||||||
|
probably_too_low_outbound_liquidity = self.expected_onchain_amount_sat > ln_can_send
|
||||||
|
if probably_too_low_outbound_liquidity:
|
||||||
|
msg.extend([
|
||||||
|
"",
|
||||||
|
"Please also note:",
|
||||||
|
too_low_outbound_liquidity_msg,
|
||||||
|
])
|
||||||
|
return "\n".join(msg)
|
||||||
|
|
||||||
|
# if we have lightning_send_amount_sat our provider has enough liquidity, so we know the exact
|
||||||
|
# amount we need to send including the providers fees
|
||||||
|
too_low_outbound_liquidity = self.lightning_send_amount_sat > ln_can_send
|
||||||
|
if too_low_outbound_liquidity:
|
||||||
|
return too_low_outbound_liquidity_msg
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def set_swap_tab_warning(self, warning: str):
|
||||||
|
msg = _('Submarine Payment not possible:') + '\n' + warning
|
||||||
|
self.submarine_warning_label.setText(msg)
|
||||||
|
self.submarine_stacked_widget.setCurrentIndex(1)
|
||||||
|
self.submarine_ok_button.setEnabled(False)
|
||||||
|
|
||||||
|
@qt_event_listener
|
||||||
|
def on_event_swap_offers_changed(self, _):
|
||||||
|
if self.ongoing_swap_transport_connection_attempt \
|
||||||
|
and not self.ongoing_swap_transport_connection_attempt.done():
|
||||||
|
return
|
||||||
|
if not self.submarine_payment_tab.isVisible():
|
||||||
|
return
|
||||||
|
self.update_submarine_tab()
|
||||||
|
|
||||||
|
|
||||||
class ConfirmTxDialog(TxEditor):
|
class ConfirmTxDialog(TxEditor):
|
||||||
help_text = '' #_('Set the mining fee of your transaction')
|
help_text = '' #_('Set the mining fee of your transaction')
|
||||||
|
|
||||||
def __init__(self, *, window: 'ElectrumWindow', make_tx, output_value: Union[int, str], allow_preview=True, batching_candidates=None):
|
def __init__(
|
||||||
|
self, *,
|
||||||
|
window: 'ElectrumWindow',
|
||||||
|
make_tx,
|
||||||
|
output_value: Union[int, str],
|
||||||
|
payee_outputs: Optional[list[TxOutput]] = None,
|
||||||
|
allow_preview=True,
|
||||||
|
batching_candidates=None,
|
||||||
|
):
|
||||||
|
|
||||||
TxEditor.__init__(
|
TxEditor.__init__(
|
||||||
self,
|
self,
|
||||||
window=window,
|
window=window,
|
||||||
make_tx=make_tx,
|
make_tx=make_tx,
|
||||||
output_value=output_value,
|
output_value=output_value,
|
||||||
|
payee_outputs=payee_outputs,
|
||||||
title=_("New Transaction"), # todo: adapt title for channel funding tx, swaps
|
title=_("New Transaction"), # todo: adapt title for channel funding tx, swaps
|
||||||
allow_preview=allow_preview, # false for channel funding
|
allow_preview=allow_preview, # false for channel funding
|
||||||
batching_candidates=batching_candidates,
|
batching_candidates=batching_candidates,
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ from electrum.util import (format_time, UserCancelled, profiler, bfh, InvalidPas
|
|||||||
from electrum.bip21 import BITCOIN_BIP21_URI_SCHEME
|
from electrum.bip21 import BITCOIN_BIP21_URI_SCHEME
|
||||||
from electrum.payment_identifier import PaymentIdentifier
|
from electrum.payment_identifier import PaymentIdentifier
|
||||||
from electrum.invoices import PR_PAID, Invoice
|
from electrum.invoices import PR_PAID, Invoice
|
||||||
from electrum.transaction import (Transaction, PartialTxInput,
|
from electrum.transaction import (Transaction, PartialTxInput, TxOutput,
|
||||||
PartialTransaction, PartialTxOutput)
|
PartialTransaction, PartialTxOutput)
|
||||||
from electrum.wallet import (Multisig_Wallet, Abstract_Wallet,
|
from electrum.wallet import (Multisig_Wallet, Abstract_Wallet,
|
||||||
sweep_preparations, InternalAddressCorruption,
|
sweep_preparations, InternalAddressCorruption,
|
||||||
@@ -1504,14 +1504,28 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
|
|||||||
return
|
return
|
||||||
# we need to know the fee before we broadcast, because the txid is required
|
# we need to know the fee before we broadcast, because the txid is required
|
||||||
make_tx = self.mktx_for_open_channel(funding_sat=funding_sat, node_id=node_id)
|
make_tx = self.mktx_for_open_channel(funding_sat=funding_sat, node_id=node_id)
|
||||||
funding_tx, _ = self.confirm_tx_dialog(make_tx, funding_sat, allow_preview=False)
|
funding_tx, _, _ = self.confirm_tx_dialog(make_tx, funding_sat, allow_preview=False)
|
||||||
if not funding_tx:
|
if not funding_tx:
|
||||||
return
|
return
|
||||||
self._open_channel(connect_str, funding_sat, push_amt, funding_tx)
|
self._open_channel(connect_str, funding_sat, push_amt, funding_tx)
|
||||||
|
|
||||||
def confirm_tx_dialog(self, make_tx, output_value, *, allow_preview=True, batching_candidates=None) -> tuple[Optional[PartialTransaction], bool]:
|
def confirm_tx_dialog(
|
||||||
d = ConfirmTxDialog(window=self, make_tx=make_tx, output_value=output_value, allow_preview=allow_preview, batching_candidates=batching_candidates)
|
self,
|
||||||
return d.run(), d.is_preview
|
make_tx,
|
||||||
|
output_value, *,
|
||||||
|
payee_outputs: Optional[list[TxOutput]] = None,
|
||||||
|
allow_preview=True,
|
||||||
|
batching_candidates=None,
|
||||||
|
) -> tuple[Optional[PartialTransaction], bool, bool]:
|
||||||
|
d = ConfirmTxDialog(
|
||||||
|
window=self,
|
||||||
|
make_tx=make_tx,
|
||||||
|
output_value=output_value,
|
||||||
|
payee_outputs=payee_outputs,
|
||||||
|
allow_preview=allow_preview,
|
||||||
|
batching_candidates=batching_candidates,
|
||||||
|
)
|
||||||
|
return d.run(), d.is_preview, d.did_swap
|
||||||
|
|
||||||
@protected
|
@protected
|
||||||
def _open_channel(self, connect_str, funding_sat, push_amt, funding_tx, password):
|
def _open_channel(self, connect_str, funding_sat, push_amt, funding_tx, password):
|
||||||
|
|||||||
@@ -338,9 +338,16 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
|
|||||||
coins_conservative = get_coins(nonlocal_only=True, confirmed_only=True)
|
coins_conservative = get_coins(nonlocal_only=True, confirmed_only=True)
|
||||||
candidates = self.wallet.get_candidates_for_batching(outputs, coins=coins_conservative)
|
candidates = self.wallet.get_candidates_for_batching(outputs, coins=coins_conservative)
|
||||||
|
|
||||||
tx, is_preview = self.window.confirm_tx_dialog(make_tx, output_value, batching_candidates=candidates)
|
tx, is_preview, paid_with_swap = self.window.confirm_tx_dialog(
|
||||||
|
make_tx,
|
||||||
|
output_value,
|
||||||
|
payee_outputs=[o for o in outputs if not o.is_change],
|
||||||
|
batching_candidates=candidates,
|
||||||
|
)
|
||||||
if tx is None:
|
if tx is None:
|
||||||
# user cancelled
|
if paid_with_swap:
|
||||||
|
self.do_clear()
|
||||||
|
# user cancelled or paid with swap
|
||||||
return
|
return
|
||||||
|
|
||||||
if swap_dummy_output := tx.get_dummy_output(DummyAddress.SWAP):
|
if swap_dummy_output := tx.get_dummy_output(DummyAddress.SWAP):
|
||||||
|
|||||||
@@ -301,7 +301,7 @@ class SwapDialog(WindowModalDialog, QtEventListener):
|
|||||||
self.needs_tx_update = True
|
self.needs_tx_update = True
|
||||||
# update icon
|
# update icon
|
||||||
pubkey = from_nip19(self.config.SWAPSERVER_NPUB)['object'].hex() if self.config.SWAPSERVER_NPUB else ''
|
pubkey = from_nip19(self.config.SWAPSERVER_NPUB)['object'].hex() if self.config.SWAPSERVER_NPUB else ''
|
||||||
self.server_button.setIcon(SwapServerDialog._pubkey_to_q_icon(pubkey))
|
self.server_button.setIcon(SwapServerDialog.pubkey_to_q_icon(pubkey))
|
||||||
|
|
||||||
def get_client_swap_limits_sat(self) -> Tuple[int, int]:
|
def get_client_swap_limits_sat(self) -> Tuple[int, int]:
|
||||||
"""Returns the (min, max) client swap limits in sat."""
|
"""Returns the (min, max) client swap limits in sat."""
|
||||||
@@ -531,12 +531,12 @@ class SwapServerDialog(WindowModalDialog, QtEventListener):
|
|||||||
labels[self.Columns.LAST_SEEN] = age(x.timestamp)
|
labels[self.Columns.LAST_SEEN] = age(x.timestamp)
|
||||||
item = QTreeWidgetItem(labels)
|
item = QTreeWidgetItem(labels)
|
||||||
item.setData(self.Columns.PUBKEY, ROLE_NPUB, x.server_npub)
|
item.setData(self.Columns.PUBKEY, ROLE_NPUB, x.server_npub)
|
||||||
item.setIcon(self.Columns.PUBKEY, self._pubkey_to_q_icon(x.server_pubkey))
|
item.setIcon(self.Columns.PUBKEY, self.pubkey_to_q_icon(x.server_pubkey))
|
||||||
items.append(item)
|
items.append(item)
|
||||||
self.servers_list.insertTopLevelItems(0, items)
|
self.servers_list.insertTopLevelItems(0, items)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _pubkey_to_q_icon(server_pubkey: str) -> QIcon:
|
def pubkey_to_q_icon(server_pubkey: str) -> QIcon:
|
||||||
color = QColor(*pubkey_to_rgb_color(server_pubkey))
|
color = QColor(*pubkey_to_rgb_color(server_pubkey))
|
||||||
color_pixmap = QPixmap(100, 100)
|
color_pixmap = QPixmap(100, 100)
|
||||||
color_pixmap.fill(color)
|
color_pixmap.fill(color)
|
||||||
|
|||||||
@@ -1522,6 +1522,21 @@ def set_windows_os_screenshot_protection_drm_flag(window: QWidget) -> None:
|
|||||||
except Exception:
|
except Exception:
|
||||||
_logger.exception(f"failed to set windows screenshot protection flag")
|
_logger.exception(f"failed to set windows screenshot protection flag")
|
||||||
|
|
||||||
|
|
||||||
|
def debug_widget_layouts(gui_element: QObject):
|
||||||
|
"""Draw red borders around all widgets of given QObject for debugging.
|
||||||
|
E.g. add util.debug_widget_layouts(self) at the end of TxEditor.__init__
|
||||||
|
"""
|
||||||
|
assert isinstance(gui_element, QObject) and hasattr(gui_element, 'findChildren')
|
||||||
|
def set_border(widget):
|
||||||
|
if widget is not None:
|
||||||
|
widget.setStyleSheet(widget.styleSheet() + " * { border: 1px solid red; }")
|
||||||
|
|
||||||
|
# Apply to all child widgets recursively
|
||||||
|
for widget in gui_element.findChildren(QWidget):
|
||||||
|
set_border(widget)
|
||||||
|
|
||||||
|
|
||||||
class _ABCQObjectMeta(type(QObject), ABCMeta): pass
|
class _ABCQObjectMeta(type(QObject), ABCMeta): pass
|
||||||
class _ABCQWidgetMeta(type(QWidget), ABCMeta): pass
|
class _ABCQWidgetMeta(type(QWidget), ABCMeta): pass
|
||||||
class AbstractQObject(QObject, ABC, metaclass=_ABCQObjectMeta): pass
|
class AbstractQObject(QObject, ABC, metaclass=_ABCQObjectMeta): pass
|
||||||
|
|||||||
@@ -688,6 +688,13 @@ class SimpleConfig(Logger):
|
|||||||
short_desc=lambda: _('Send change to Lightning'),
|
short_desc=lambda: _('Send change to Lightning'),
|
||||||
long_desc=lambda: _('If possible, send the change of this transaction to your channels, with a submarine swap'),
|
long_desc=lambda: _('If possible, send the change of this transaction to your channels, with a submarine swap'),
|
||||||
)
|
)
|
||||||
|
WALLET_ENABLE_SUBMARINE_PAYMENTS = ConfigVar(
|
||||||
|
'enable_submarine_payments', default=False, type_=bool,
|
||||||
|
short_desc=lambda: _('Submarine Payments'),
|
||||||
|
long_desc=lambda: _('Send onchain payments directly from your Lightning balance with a '
|
||||||
|
'submarine swap. This allows you to do onchain transactions even if your entire '
|
||||||
|
'wallet balance is inside Lightning channels.')
|
||||||
|
)
|
||||||
WALLET_FREEZE_REUSED_ADDRESS_UTXOS = ConfigVar(
|
WALLET_FREEZE_REUSED_ADDRESS_UTXOS = ConfigVar(
|
||||||
'wallet_freeze_reused_address_utxos', default=False, type_=bool,
|
'wallet_freeze_reused_address_utxos', default=False, type_=bool,
|
||||||
short_desc=lambda: _('Avoid spending from used addresses'),
|
short_desc=lambda: _('Avoid spending from used addresses'),
|
||||||
|
|||||||
@@ -548,7 +548,7 @@ class SwapManager(Logger):
|
|||||||
"""
|
"""
|
||||||
Construct claim tx that spends exactly the funding utxo to the swap output, independent of the
|
Construct claim tx that spends exactly the funding utxo to the swap output, independent of the
|
||||||
current fee environment to guarantee the correct amount is being sent to the claim output which
|
current fee environment to guarantee the correct amount is being sent to the claim output which
|
||||||
might be an external address and to keep the claim transaction uncorrelated to the wallets utxos.
|
might be an external address.
|
||||||
"""
|
"""
|
||||||
assert swap.claim_to_output, swap
|
assert swap.claim_to_output, swap
|
||||||
txout = PartialTxOutput.from_address_and_value(swap.claim_to_output[0], swap.claim_to_output[1])
|
txout = PartialTxOutput.from_address_and_value(swap.claim_to_output[0], swap.claim_to_output[1])
|
||||||
@@ -1720,6 +1720,7 @@ class NostrTransport(SwapServerTransport):
|
|||||||
async def stop(self):
|
async def stop(self):
|
||||||
self.logger.info("shutting down nostr transport")
|
self.logger.info("shutting down nostr transport")
|
||||||
self.sm.is_initialized.clear()
|
self.sm.is_initialized.clear()
|
||||||
|
self.is_connected.clear()
|
||||||
await self.taskgroup.cancel_remaining()
|
await self.taskgroup.cancel_remaining()
|
||||||
await self.relay_manager.close()
|
await self.relay_manager.close()
|
||||||
self.logger.info("nostr transport shut down")
|
self.logger.info("nostr transport shut down")
|
||||||
|
|||||||
Reference in New Issue
Block a user