|
|
|
|
@@ -23,50 +23,61 @@
|
|
|
|
|
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
|
|
|
# SOFTWARE.
|
|
|
|
|
|
|
|
|
|
import asyncio
|
|
|
|
|
from decimal import Decimal
|
|
|
|
|
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.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.util import NotEnoughFunds, NoDynamicFeeEstimates
|
|
|
|
|
from electrum.util import quantize_feerate
|
|
|
|
|
from electrum.util import (quantize_feerate, profiler, NotEnoughFunds, NoDynamicFeeEstimates,
|
|
|
|
|
get_asyncio_loop, wait_for2, nostr_pow_worker)
|
|
|
|
|
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.bitcoin import DummyAddress
|
|
|
|
|
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,
|
|
|
|
|
WWLabel, read_QIcon)
|
|
|
|
|
from .util import (WindowModalDialog, ColorScheme, HelpLabel, Buttons, CancelButton, WWLabel,
|
|
|
|
|
read_QIcon, debug_widget_layouts, qt_event_listener, QtEventListener, IconLabel)
|
|
|
|
|
from .transaction_dialog import TxSizeLabel, TxFiatLabel, TxInOutWidget
|
|
|
|
|
from .fee_slider import FeeSlider, FeeComboBox
|
|
|
|
|
from .amountedit import FeerateEdit, BTCAmountEdit
|
|
|
|
|
from .locktimeedit import LockTimeEdit
|
|
|
|
|
from .my_treeview import QMenuWithConfig
|
|
|
|
|
from .swap_dialog import SwapServerDialog
|
|
|
|
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
|
|
|
from .main_window import ElectrumWindow
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TxEditor(WindowModalDialog):
|
|
|
|
|
class TxEditor(WindowModalDialog, QtEventListener, Logger):
|
|
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
|
self, *, title='',
|
|
|
|
|
window: 'ElectrumWindow',
|
|
|
|
|
make_tx,
|
|
|
|
|
output_value: Union[int, str],
|
|
|
|
|
payee_outputs: Optional[list[TxOutput]] = None,
|
|
|
|
|
allow_preview=True,
|
|
|
|
|
batching_candidates=None,
|
|
|
|
|
):
|
|
|
|
|
|
|
|
|
|
WindowModalDialog.__init__(self, window, title=title)
|
|
|
|
|
Logger.__init__(self)
|
|
|
|
|
self.main_window = window
|
|
|
|
|
self.make_tx = make_tx
|
|
|
|
|
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.messages = []
|
|
|
|
|
self.error = '' # set by side effect
|
|
|
|
|
@@ -85,40 +96,87 @@ class TxEditor(WindowModalDialog):
|
|
|
|
|
self._base_tx = None # for batching
|
|
|
|
|
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.valueEdited.connect(self.trigger_update)
|
|
|
|
|
self.locktime_label = QLabel(_("LockTime") + ": ")
|
|
|
|
|
self.io_widget = TxInOutWidget(self.main_window, self.wallet)
|
|
|
|
|
self.create_fee_controls()
|
|
|
|
|
|
|
|
|
|
vbox = QVBoxLayout()
|
|
|
|
|
self.setLayout(vbox)
|
|
|
|
|
|
|
|
|
|
top = self.create_top_bar(self.help_text)
|
|
|
|
|
grid = self.create_grid()
|
|
|
|
|
|
|
|
|
|
vbox.addLayout(top)
|
|
|
|
|
vbox.addLayout(grid)
|
|
|
|
|
vbox.addWidget(self.io_widget)
|
|
|
|
|
onchain_vbox = QVBoxLayout()
|
|
|
|
|
onchain_top = self.create_top_bar(self.help_text)
|
|
|
|
|
onchain_grid = self.create_grid()
|
|
|
|
|
onchain_vbox.addLayout(onchain_top)
|
|
|
|
|
onchain_vbox.addLayout(onchain_grid)
|
|
|
|
|
onchain_vbox.addWidget(self.io_widget)
|
|
|
|
|
self.message_label = WWLabel('')
|
|
|
|
|
self.message_label.setMinimumHeight(70)
|
|
|
|
|
vbox.addWidget(self.message_label)
|
|
|
|
|
onchain_vbox.addWidget(self.message_label)
|
|
|
|
|
|
|
|
|
|
buttons = self.create_buttons_bar()
|
|
|
|
|
vbox.addStretch(1)
|
|
|
|
|
vbox.addLayout(buttons)
|
|
|
|
|
onchain_buttons = self.create_buttons_bar()
|
|
|
|
|
onchain_vbox.addStretch(1)
|
|
|
|
|
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_fee_edit_visible()
|
|
|
|
|
self.set_locktime_visible()
|
|
|
|
|
self.update_fee_target()
|
|
|
|
|
self.resize(self.layout().sizeHint())
|
|
|
|
|
self.update_tab_visibility()
|
|
|
|
|
self.resize_to_fit_content()
|
|
|
|
|
|
|
|
|
|
self.timer = QTimer(self)
|
|
|
|
|
self.timer.setInterval(500)
|
|
|
|
|
self.timer.setSingleShot(False)
|
|
|
|
|
self.timer.timeout.connect(self.timer_actions)
|
|
|
|
|
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:
|
|
|
|
|
return self._base_tx is not None
|
|
|
|
|
@@ -140,6 +198,13 @@ class TxEditor(WindowModalDialog):
|
|
|
|
|
# expected to set self.tx, self.message and self.error
|
|
|
|
|
raise NotImplementedError()
|
|
|
|
|
|
|
|
|
|
def create_grid(self) -> QGridLayout:
|
|
|
|
|
raise NotImplementedError()
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def help_text(self) -> str:
|
|
|
|
|
raise NotImplementedError()
|
|
|
|
|
|
|
|
|
|
def update_fee_target(self):
|
|
|
|
|
if self.fee_slider.is_active():
|
|
|
|
|
text = self.fee_policy.get_target_text()
|
|
|
|
|
@@ -230,6 +295,32 @@ class TxEditor(WindowModalDialog):
|
|
|
|
|
self.fee_slider.setFixedWidth(200)
|
|
|
|
|
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):
|
|
|
|
|
# set tx to None so that the ok button is disabled while we compute the new tx
|
|
|
|
|
self.tx = None
|
|
|
|
|
@@ -311,7 +402,6 @@ class TxEditor(WindowModalDialog):
|
|
|
|
|
feerate_color = ColorScheme.BLUE
|
|
|
|
|
self.fee_e.setStyleSheet(fee_color.as_stylesheet())
|
|
|
|
|
self.feerate_e.setStyleSheet(feerate_color.as_stylesheet())
|
|
|
|
|
#
|
|
|
|
|
self.needs_update = True
|
|
|
|
|
|
|
|
|
|
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.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_ENABLE_SUBMARINE_PAYMENTS, callback=self.update_tab_visibility)
|
|
|
|
|
self.pref_menu.addToggle(
|
|
|
|
|
_('Use change addresses'),
|
|
|
|
|
self.toggle_use_change,
|
|
|
|
|
@@ -442,11 +533,12 @@ class TxEditor(WindowModalDialog):
|
|
|
|
|
hbox.addWidget(self.pref_button)
|
|
|
|
|
return hbox
|
|
|
|
|
|
|
|
|
|
@profiler(min_threshold=0.02)
|
|
|
|
|
def resize_to_fit_content(self):
|
|
|
|
|
# fixme: calling resize once is not enough...
|
|
|
|
|
size = self.layout().sizeHint()
|
|
|
|
|
self.resize(size)
|
|
|
|
|
self.resize(size)
|
|
|
|
|
# update all geometries so the updated size hints are used for size adjustment
|
|
|
|
|
for widget in self.findChildren(QWidget):
|
|
|
|
|
widget.updateGeometry()
|
|
|
|
|
self.adjustSize()
|
|
|
|
|
|
|
|
|
|
def toggle_use_change(self):
|
|
|
|
|
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:
|
|
|
|
|
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):
|
|
|
|
|
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__(
|
|
|
|
|
self,
|
|
|
|
|
window=window,
|
|
|
|
|
make_tx=make_tx,
|
|
|
|
|
output_value=output_value,
|
|
|
|
|
payee_outputs=payee_outputs,
|
|
|
|
|
title=_("New Transaction"), # todo: adapt title for channel funding tx, swaps
|
|
|
|
|
allow_preview=allow_preview, # false for channel funding
|
|
|
|
|
batching_candidates=batching_candidates,
|
|
|
|
|
|