Merge pull request #10303 from f321x/submarine_payment_func
swaps/qt: expose swaps to external address
This commit is contained in:
@@ -23,50 +23,62 @@
|
||||
# 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,
|
||||
pubkey_to_q_icon)
|
||||
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 +97,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 +199,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 +296,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 +403,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 +502,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 +534,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 +686,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(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,
|
||||
|
||||
@@ -62,7 +62,7 @@ from electrum.util import (format_time, UserCancelled, profiler, bfh, InvalidPas
|
||||
from electrum.bip21 import BITCOIN_BIP21_URI_SCHEME
|
||||
from electrum.payment_identifier import PaymentIdentifier
|
||||
from electrum.invoices import PR_PAID, Invoice
|
||||
from electrum.transaction import (Transaction, PartialTxInput,
|
||||
from electrum.transaction import (Transaction, PartialTxInput, TxOutput,
|
||||
PartialTransaction, PartialTxOutput)
|
||||
from electrum.wallet import (Multisig_Wallet, Abstract_Wallet,
|
||||
sweep_preparations, InternalAddressCorruption,
|
||||
@@ -1504,14 +1504,28 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
|
||||
return
|
||||
# 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)
|
||||
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:
|
||||
return
|
||||
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]:
|
||||
d = ConfirmTxDialog(window=self, make_tx=make_tx, output_value=output_value, allow_preview=allow_preview, batching_candidates=batching_candidates)
|
||||
return d.run(), d.is_preview
|
||||
def confirm_tx_dialog(
|
||||
self,
|
||||
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
|
||||
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)
|
||||
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:
|
||||
# user cancelled
|
||||
if paid_with_swap:
|
||||
self.do_clear()
|
||||
# user cancelled or paid with swap
|
||||
return
|
||||
|
||||
if swap_dummy_output := tx.get_dummy_output(DummyAddress.SWAP):
|
||||
|
||||
@@ -13,12 +13,13 @@ from electrum.util import NotEnoughFunds, NoDynamicFeeEstimates, UserCancelled
|
||||
from electrum.bitcoin import DummyAddress
|
||||
from electrum.transaction import PartialTxOutput, PartialTransaction
|
||||
from electrum.fee_policy import FeePolicy
|
||||
from electrum.submarine_swaps import NostrTransport, pubkey_to_rgb_color
|
||||
from electrum.submarine_swaps import NostrTransport
|
||||
|
||||
from electrum.gui import messages
|
||||
from . import util
|
||||
from .util import (WindowModalDialog, Buttons, OkButton, CancelButton,
|
||||
EnterButton, ColorScheme, WWLabel, read_QIcon, IconLabel, char_width_in_lineedit)
|
||||
EnterButton, ColorScheme, WWLabel, read_QIcon, IconLabel, char_width_in_lineedit,
|
||||
pubkey_to_q_icon)
|
||||
from .util import qt_event_listener, QtEventListener
|
||||
from .amountedit import BTCAmountEdit
|
||||
from .fee_slider import FeeSlider, FeeComboBox
|
||||
@@ -301,7 +302,7 @@ class SwapDialog(WindowModalDialog, QtEventListener):
|
||||
self.needs_tx_update = True
|
||||
# update icon
|
||||
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(pubkey_to_q_icon(pubkey))
|
||||
|
||||
def get_client_swap_limits_sat(self) -> Tuple[int, int]:
|
||||
"""Returns the (min, max) client swap limits in sat."""
|
||||
@@ -531,13 +532,7 @@ class SwapServerDialog(WindowModalDialog, QtEventListener):
|
||||
labels[self.Columns.LAST_SEEN] = age(x.timestamp)
|
||||
item = QTreeWidgetItem(labels)
|
||||
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, pubkey_to_q_icon(x.server_pubkey))
|
||||
items.append(item)
|
||||
self.servers_list.insertTopLevelItems(0, items)
|
||||
|
||||
@staticmethod
|
||||
def _pubkey_to_q_icon(server_pubkey: str) -> QIcon:
|
||||
color = QColor(*pubkey_to_rgb_color(server_pubkey))
|
||||
color_pixmap = QPixmap(100, 100)
|
||||
color_pixmap.fill(color)
|
||||
return QIcon(color_pixmap)
|
||||
|
||||
@@ -26,6 +26,7 @@ from electrum.util import (FileImportFailed, FileExportFailed, resource_path, Ev
|
||||
from electrum.invoices import (PR_UNPAID, PR_PAID, PR_EXPIRED, PR_INFLIGHT, PR_UNKNOWN, PR_FAILED, PR_ROUTING,
|
||||
PR_UNCONFIRMED, PR_BROADCASTING, PR_BROADCAST)
|
||||
from electrum.qrreader import MissingQrDetectionLib, QrCodeResult
|
||||
from electrum.submarine_swaps import pubkey_to_rgb_color
|
||||
|
||||
from electrum.gui.common_qt.util import TaskThread
|
||||
|
||||
@@ -726,6 +727,13 @@ def get_icon_camera() -> QIcon:
|
||||
return read_QIcon(name)
|
||||
|
||||
|
||||
def pubkey_to_q_icon(server_pubkey: str) -> QIcon:
|
||||
color = QColor(*pubkey_to_rgb_color(server_pubkey))
|
||||
color_pixmap = QPixmap(100, 100)
|
||||
color_pixmap.fill(color)
|
||||
return QIcon(color_pixmap)
|
||||
|
||||
|
||||
def add_input_actions_to_context_menu(gih: 'GenericInputHandler', m: QMenu) -> None:
|
||||
if gih.on_qr_from_camera_input_btn:
|
||||
m.addAction(get_icon_camera(), _("Read QR code with camera"), gih.on_qr_from_camera_input_btn)
|
||||
@@ -1522,6 +1530,21 @@ def set_windows_os_screenshot_protection_drm_flag(window: QWidget) -> None:
|
||||
except Exception:
|
||||
_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 _ABCQWidgetMeta(type(QWidget), ABCMeta): pass
|
||||
class AbstractQObject(QObject, ABC, metaclass=_ABCQObjectMeta): pass
|
||||
|
||||
@@ -688,6 +688,13 @@ class SimpleConfig(Logger):
|
||||
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'),
|
||||
)
|
||||
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', default=False, type_=bool,
|
||||
short_desc=lambda: _('Avoid spending from used addresses'),
|
||||
|
||||
@@ -24,11 +24,12 @@ from collections import defaultdict
|
||||
from .i18n import _
|
||||
from .logging import Logger
|
||||
from .crypto import sha256, ripemd
|
||||
from .bitcoin import script_to_p2wsh, opcodes, dust_threshold, DummyAddress, construct_witness, construct_script
|
||||
from .bitcoin import (script_to_p2wsh, opcodes, dust_threshold, DummyAddress, construct_witness,
|
||||
construct_script, address_to_script)
|
||||
from . import bitcoin
|
||||
from .transaction import (
|
||||
PartialTxInput, PartialTxOutput, PartialTransaction, Transaction, TxInput, TxOutpoint, script_GetOp,
|
||||
match_script_against_template, OPPushDataGeneric, OPPushDataPubkey
|
||||
match_script_against_template, OPPushDataGeneric, OPPushDataPubkey, TxOutput,
|
||||
)
|
||||
from .util import (
|
||||
log_exceptions, ignore_exceptions, BelowDustLimit, OldTaskGroup, ca_path, gen_nostr_ann_pow,
|
||||
@@ -199,7 +200,7 @@ class SwapData(StoredObject):
|
||||
prepay_hash = attr.ib(type=Optional[bytes], converter=hex_to_bytes)
|
||||
privkey = attr.ib(type=bytes, converter=hex_to_bytes)
|
||||
lockup_address = attr.ib(type=str)
|
||||
receive_address = attr.ib(type=str)
|
||||
claim_to_output = attr.ib(type=Optional[Tuple[str, int]]) # address, amount to claim the funding utxo to
|
||||
funding_txid = attr.ib(type=Optional[str])
|
||||
spending_txid = attr.ib(type=Optional[str])
|
||||
is_redeemed = attr.ib(type=bool)
|
||||
@@ -520,6 +521,9 @@ class SwapManager(Logger):
|
||||
if spent_height is not None and spent_height > 0:
|
||||
return
|
||||
txin, locktime = self.create_claim_txin(txin=txin, swap=swap)
|
||||
if swap.is_reverse and swap.claim_to_output:
|
||||
asyncio.create_task(self._claim_to_output(swap, txin))
|
||||
return
|
||||
# note: there is no csv in the script, we just set this so that txbatcher waits for one confirmation
|
||||
name = 'swap claim' if swap.is_reverse else 'swap refund'
|
||||
can_be_batched = True
|
||||
@@ -540,6 +544,42 @@ class SwapManager(Logger):
|
||||
self.logger.info('got NoDynamicFeeEstimates')
|
||||
return
|
||||
|
||||
async def _claim_to_output(self, swap: SwapData, claim_txin: PartialTxInput):
|
||||
"""
|
||||
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
|
||||
might be an external address.
|
||||
"""
|
||||
assert swap.claim_to_output, swap
|
||||
txout = PartialTxOutput.from_address_and_value(swap.claim_to_output[0], swap.claim_to_output[1])
|
||||
tx = PartialTransaction.from_io([claim_txin], [txout])
|
||||
can_be_broadcast = self.wallet.adb.get_tx_height(swap.funding_txid).height() > 0
|
||||
already_broadcast = self.wallet.adb.get_tx_height(tx.txid()).height() >= 0
|
||||
self.logger.debug(f"_claim_to_output: {can_be_broadcast=} {already_broadcast=}")
|
||||
|
||||
# add tx to db so it can be shown as future tx
|
||||
if not self.wallet.adb.get_transaction(tx.txid()):
|
||||
try:
|
||||
self.wallet.adb.add_transaction(tx)
|
||||
except Exception:
|
||||
self.logger.exception("")
|
||||
return
|
||||
trigger_callback('wallet_updated', self)
|
||||
|
||||
# set or update future tx wanted height if it has not been broadcast yet
|
||||
local_height = self.network.get_local_height()
|
||||
wanted_height = local_height + claim_txin.get_block_based_relative_locktime()
|
||||
if not already_broadcast and self.wallet.adb.future_tx.get(tx.txid(), 0) < wanted_height:
|
||||
self.wallet.adb.set_future_tx(tx.txid(), wanted_height=wanted_height)
|
||||
|
||||
if can_be_broadcast and not already_broadcast:
|
||||
tx = self.wallet.sign_transaction(tx, password=None, ignore_warnings=True)
|
||||
assert tx and tx.is_complete(), tx
|
||||
try:
|
||||
await self.wallet.network.broadcast_transaction(tx)
|
||||
except Exception:
|
||||
self.logger.exception(f"cannot broadcast swap to output claim tx")
|
||||
|
||||
def get_fee_for_txbatcher(self):
|
||||
return self._get_tx_fee(self.config.FEE_POLICY_SWAPS)
|
||||
|
||||
@@ -687,7 +727,6 @@ class SwapManager(Logger):
|
||||
prepay_hash = None
|
||||
|
||||
lockup_address = script_to_p2wsh(redeem_script)
|
||||
receive_address = self.wallet.get_receiving_address()
|
||||
swap = SwapData(
|
||||
redeem_script=redeem_script,
|
||||
locktime=locktime,
|
||||
@@ -696,7 +735,7 @@ class SwapManager(Logger):
|
||||
prepay_hash=prepay_hash,
|
||||
lockup_address=lockup_address,
|
||||
onchain_amount=onchain_amount_sat,
|
||||
receive_address=receive_address,
|
||||
claim_to_output=None,
|
||||
lightning_amount=lightning_amount_sat,
|
||||
is_reverse=False,
|
||||
is_redeemed=False,
|
||||
@@ -749,12 +788,17 @@ class SwapManager(Logger):
|
||||
preimage: bytes,
|
||||
payment_hash: bytes,
|
||||
prepay_hash: Optional[bytes] = None,
|
||||
claim_to_output: Optional[TxOutput] = None,
|
||||
) -> SwapData:
|
||||
if payment_hash.hex() in self._swaps:
|
||||
raise Exception("payment_hash already in use")
|
||||
assert sha256(preimage) == payment_hash
|
||||
lockup_address = script_to_p2wsh(redeem_script)
|
||||
receive_address = self.wallet.get_receiving_address()
|
||||
if claim_to_output is not None:
|
||||
# the claim_to_output value needs to be lower than the funding utxo value, otherwise
|
||||
# there are no funds left for the fee of the claim tx
|
||||
assert claim_to_output.value < onchain_amount_sat, f"{claim_to_output=} >= {onchain_amount_sat=}"
|
||||
claim_to_output = (claim_to_output.address, claim_to_output.value)
|
||||
swap = SwapData(
|
||||
redeem_script=redeem_script,
|
||||
locktime=locktime,
|
||||
@@ -763,7 +807,7 @@ class SwapManager(Logger):
|
||||
prepay_hash=prepay_hash,
|
||||
lockup_address=lockup_address,
|
||||
onchain_amount=onchain_amount_sat,
|
||||
receive_address=receive_address,
|
||||
claim_to_output=claim_to_output,
|
||||
lightning_amount=lightning_amount_sat,
|
||||
is_reverse=True,
|
||||
is_redeemed=False,
|
||||
@@ -1014,6 +1058,7 @@ class SwapManager(Logger):
|
||||
expected_onchain_amount_sat: int,
|
||||
prepayment_sat: int,
|
||||
channels: Optional[Sequence['Channel']] = None,
|
||||
claim_to_output: Optional[TxOutput] = None,
|
||||
) -> Optional[str]:
|
||||
"""send on Lightning, receive on-chain
|
||||
|
||||
@@ -1116,7 +1161,9 @@ class SwapManager(Logger):
|
||||
payment_hash=payment_hash,
|
||||
prepay_hash=prepay_hash,
|
||||
onchain_amount_sat=onchain_amount,
|
||||
lightning_amount_sat=lightning_amount_sat)
|
||||
lightning_amount_sat=lightning_amount_sat,
|
||||
claim_to_output=claim_to_output,
|
||||
)
|
||||
# initiate fee payment.
|
||||
if fee_invoice:
|
||||
fee_invoice_obj = Invoice.from_bech32(fee_invoice)
|
||||
@@ -1124,7 +1171,7 @@ class SwapManager(Logger):
|
||||
# we return if we detect funding
|
||||
async def wait_for_funding(swap):
|
||||
while swap.funding_txid is None:
|
||||
await asyncio.sleep(1)
|
||||
await asyncio.sleep(0.1)
|
||||
# initiate main payment
|
||||
invoice_obj = Invoice.from_bech32(invoice)
|
||||
tasks = [asyncio.create_task(self.lnworker.pay_invoice(invoice_obj, channels=channels)), asyncio.create_task(wait_for_funding(swap))]
|
||||
@@ -1421,23 +1468,16 @@ class SwapManager(Logger):
|
||||
def get_groups_for_onchain_history(self):
|
||||
current_height = self.wallet.adb.get_local_height()
|
||||
d = {}
|
||||
# add info about submarine swaps
|
||||
settled_payments = self.lnworker.get_payments(status='settled')
|
||||
with self.swaps_lock:
|
||||
swaps_items = list(self._swaps.items())
|
||||
for payment_hash_hex, swap in swaps_items:
|
||||
txid = swap.spending_txid if swap.is_reverse else swap.funding_txid
|
||||
if txid is None:
|
||||
continue
|
||||
payment_hash = bytes.fromhex(payment_hash_hex)
|
||||
if payment_hash in settled_payments:
|
||||
plist = settled_payments[payment_hash]
|
||||
info = self.lnworker.get_payment_info(payment_hash)
|
||||
direction, amount_msat, fee_msat, timestamp = self.lnworker.get_payment_value(info, plist)
|
||||
else:
|
||||
amount_msat = 0
|
||||
|
||||
if swap.is_reverse:
|
||||
if swap.is_reverse and swap.claim_to_output:
|
||||
group_label = 'Submarine Payment' + ' ' + self.config.format_amount_and_units(swap.claim_to_output[1])
|
||||
elif swap.is_reverse:
|
||||
group_label = 'Reverse swap' + ' ' + self.config.format_amount_and_units(swap.lightning_amount)
|
||||
else:
|
||||
group_label = 'Forward swap' + ' ' + self.config.format_amount_and_units(swap.onchain_amount)
|
||||
@@ -1466,6 +1506,27 @@ class SwapManager(Logger):
|
||||
'label': _('Refund transaction'),
|
||||
}
|
||||
self.wallet._accounting_addresses.add(swap.lockup_address)
|
||||
elif swap.is_reverse and swap.claim_to_output: # submarine payment
|
||||
claim_tx = self.lnwatcher.adb.get_transaction(swap.spending_txid)
|
||||
payee_spk = address_to_script(swap.claim_to_output[0])
|
||||
if claim_tx and payee_spk not in (o.scriptpubkey for o in claim_tx.outputs()):
|
||||
# the swapserver must have refunded itself as the claim_tx did not spend
|
||||
# to the address we intended it to spend to, remove the funding
|
||||
# address again from accounting addresses so the refund tx is not incorrectly
|
||||
# shown in the wallet history as tx spending from this wallet
|
||||
self.wallet._accounting_addresses.discard(swap.lockup_address)
|
||||
# add the funding tx to the group as the total amount of the group would
|
||||
# otherwise be ~2x the actual payment as the claim tx gets counted as negative
|
||||
# value (as it sends from the wallet/accounting address balance)
|
||||
d[swap.funding_txid] = {
|
||||
'group_id': txid,
|
||||
'label': _('Funding transaction'),
|
||||
'group_label': group_label,
|
||||
}
|
||||
# add the lockup_address as the claim tx would otherwise not touch the wallet and
|
||||
# wouldn't be shown in the history.
|
||||
self.wallet._accounting_addresses.add(swap.lockup_address)
|
||||
|
||||
return d
|
||||
|
||||
def get_group_id_for_payment_hash(self, payment_hash: bytes) -> Optional[str]:
|
||||
@@ -1659,6 +1720,7 @@ class NostrTransport(SwapServerTransport):
|
||||
async def stop(self):
|
||||
self.logger.info("shutting down nostr transport")
|
||||
self.sm.is_initialized.clear()
|
||||
self.is_connected.clear()
|
||||
await self.taskgroup.cancel_remaining()
|
||||
await self.relay_manager.close()
|
||||
self.logger.info("nostr transport shut down")
|
||||
|
||||
@@ -73,7 +73,7 @@ class WalletUnfinished(WalletFileException):
|
||||
# seed_version is now used for the version of the wallet file
|
||||
OLD_SEED_VERSION = 4 # electrum versions < 2.0
|
||||
NEW_SEED_VERSION = 11 # electrum versions >= 2.0
|
||||
FINAL_SEED_VERSION = 61 # electrum >= 2.7 will set this to prevent
|
||||
FINAL_SEED_VERSION = 62 # electrum >= 2.7 will set this to prevent
|
||||
# old versions from overwriting new format
|
||||
|
||||
|
||||
@@ -237,6 +237,7 @@ class WalletDBUpgrader(Logger):
|
||||
self._convert_version_59()
|
||||
self._convert_version_60()
|
||||
self._convert_version_61()
|
||||
self._convert_version_62()
|
||||
self.put('seed_version', FINAL_SEED_VERSION) # just to be sure
|
||||
|
||||
def _convert_wallet_type(self):
|
||||
@@ -1170,6 +1171,17 @@ class WalletDBUpgrader(Logger):
|
||||
lightning_payments[rhash] = new
|
||||
self.data['seed_version'] = 61
|
||||
|
||||
def _convert_version_62(self):
|
||||
if not self._is_upgrade_method_needed(61, 61):
|
||||
return
|
||||
swaps = self.data.get('submarine_swaps', {})
|
||||
# remove unused receive_address field which is getting replaced by a claim_to_output field
|
||||
# which also allows specifying an amount
|
||||
for swap in swaps.values():
|
||||
del swap['receive_address']
|
||||
swap['claim_to_output'] = None
|
||||
self.data['seed_version'] = 62
|
||||
|
||||
def _convert_imported(self):
|
||||
if not self._is_upgrade_method_needed(0, 13):
|
||||
return
|
||||
|
||||
@@ -69,7 +69,7 @@ SWAPDATA = SwapData(
|
||||
prepay_hash=None,
|
||||
privkey=bytes.fromhex('58fd0018a9a2737d1d6b81d380df96bf0c858473a9592015508a270a7c9b1d8d'),
|
||||
lockup_address='tb1q2pvugjl4w56rqw4c7zg0q6mmmev0t5jjy3qzg7sl766phh9fxjxsrtl77t',
|
||||
receive_address='tb1ql0adrj58g88xgz375yct63rclhv29hv03u0mel',
|
||||
claim_to_output=None,
|
||||
funding_txid='897eea7f53e917323e7472d7a2e3099173f7836c57f1b6850f5cbdfe8085dbf9',
|
||||
spending_txid=None,
|
||||
is_redeemed=False,
|
||||
|
||||
Reference in New Issue
Block a user