From 649ce979abfeb18a285dec792d455ecf137c6382 Mon Sep 17 00:00:00 2001 From: ThomasV Date: Fri, 16 Jun 2023 10:05:02 +0200 Subject: [PATCH] send tx change to lightning --- electrum/bitcoin.py | 4 +++ electrum/coinchooser.py | 2 ++ electrum/gui/qml/qechannelopener.py | 5 ++-- electrum/gui/qml/qeswaphelper.py | 6 ++--- electrum/gui/qt/confirm_tx_dialog.py | 16 +++++++++++ electrum/gui/qt/main_window.py | 20 +++++++++++--- electrum/gui/qt/send_tab.py | 15 +++++++++++ electrum/gui/qt/swap_dialog.py | 19 ++++--------- electrum/gui/qt/transaction_dialog.py | 14 +++++++--- electrum/lnpeer.py | 9 ++----- electrum/lnutil.py | 3 --- electrum/lnworker.py | 8 +++--- electrum/simple_config.py | 1 + electrum/submarine_swaps.py | 39 ++++++++++++++++++++++----- electrum/transaction.py | 22 +++++++++++++++ electrum/wallet.py | 10 ++++++- 16 files changed, 146 insertions(+), 47 deletions(-) diff --git a/electrum/bitcoin.py b/electrum/bitcoin.py index cb2431df5..94fcd95bb 100644 --- a/electrum/bitcoin.py +++ b/electrum/bitcoin.py @@ -753,3 +753,7 @@ def is_minikey(text: str) -> bool: def minikey_to_private_key(text: str) -> bytes: return sha256(text) + +# dummy address for fee estimation of funding tx +def get_dummy_address(purpose): + return redeem_script_to_address('p2wsh', sha256(bytes(purpose, "utf8")).hex()) diff --git a/electrum/coinchooser.py b/electrum/coinchooser.py index aefd39603..f2bfdb501 100644 --- a/electrum/coinchooser.py +++ b/electrum/coinchooser.py @@ -221,6 +221,8 @@ class CoinChooserBase(Logger): amounts = [amount for amount in amounts if amount >= dust_threshold] change = [PartialTxOutput.from_address_and_value(addr, amount) for addr, amount in zip(change_addrs, amounts)] + for c in change: + c.is_change = True return change def _construct_tx_from_selected_buckets(self, *, buckets: Sequence[Bucket], diff --git a/electrum/gui/qml/qechannelopener.py b/electrum/gui/qml/qechannelopener.py index 862dba431..bc89bfced 100644 --- a/electrum/gui/qml/qechannelopener.py +++ b/electrum/gui/qml/qechannelopener.py @@ -8,7 +8,8 @@ from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject from electrum.i18n import _ from electrum.gui import messages from electrum.util import bfh -from electrum.lnutil import extract_nodeid, ln_dummy_address, ConnStringFormatError +from electrum.lnutil import extract_nodeid, ConnStringFormatError +from electrum.bitcoin import get_dummy_address from electrum.lnworker import hardcoded_trampoline_nodes from electrum.logging import get_logger @@ -181,7 +182,7 @@ class QEChannelOpener(QObject, AuthMixin): """ self._logger.debug('opening channel') # read funding_sat from tx; converts '!' to int value - funding_sat = funding_tx.output_value_for_address(ln_dummy_address()) + funding_sat = funding_tx.output_value_for_address(get_dummy_address('channel')) lnworker = self._wallet.wallet.lnworker def open_thread(): diff --git a/electrum/gui/qml/qeswaphelper.py b/electrum/gui/qml/qeswaphelper.py index d75f5355d..07cd12267 100644 --- a/electrum/gui/qml/qeswaphelper.py +++ b/electrum/gui/qml/qeswaphelper.py @@ -6,7 +6,7 @@ from typing import Union from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer, Q_ENUMS from electrum.i18n import _ -from electrum.lnutil import ln_dummy_address +from electrum.bitcoin import get_dummy_address from electrum.logging import get_logger from electrum.transaction import PartialTxOutput from electrum.util import NotEnoughFunds, NoDynamicFeeEstimates, profiler, get_asyncio_loop @@ -245,7 +245,7 @@ class QESwapHelper(AuthMixin, QObject, QtEventListener): # this is just to estimate the maximal spendable onchain amount for HTLC self.update_tx('!') try: - max_onchain_spend = self._tx.output_value_for_address(ln_dummy_address()) + max_onchain_spend = self._tx.output_value_for_address(get_dummy_address('swap')) except AttributeError: # happens if there are no utxos max_onchain_spend = 0 reverse = int(min(lnworker.num_sats_can_send(), @@ -283,7 +283,7 @@ class QESwapHelper(AuthMixin, QObject, QtEventListener): self._tx = None self.valid = False return - outputs = [PartialTxOutput.from_address_and_value(ln_dummy_address(), onchain_amount)] + outputs = [PartialTxOutput.from_address_and_value(get_dummy_address('swap'), onchain_amount)] coins = self._wallet.wallet.get_spendable_coins(None) try: self._tx = self._wallet.wallet.make_unsigned_transaction( diff --git a/electrum/gui/qt/confirm_tx_dialog.py b/electrum/gui/qt/confirm_tx_dialog.py index de5dc296d..a21c1606a 100644 --- a/electrum/gui/qt/confirm_tx_dialog.py +++ b/electrum/gui/qt/confirm_tx_dialog.py @@ -96,6 +96,7 @@ class TxEditor(WindowModalDialog): vbox.addLayout(grid) vbox.addWidget(self.io_widget) self.message_label = WWLabel('') + self.message_label.setMinimumHeight(70) vbox.addWidget(self.message_label) buttons = self.create_buttons_bar() @@ -395,6 +396,11 @@ class TxEditor(WindowModalDialog): self.toggle_locktime, _('Edit Locktime'), '') self.pref_menu.addSeparator() + add_pref_action( + self.config.WALLET_SEND_CHANGE_TO_LIGHTNING, + self.toggle_send_change_to_lightning, + _('Send change to Lightning'), + _('If possible, send the change of this transaction to your channels, with a submarine swap')) add_pref_action( self.wallet.use_change, self.toggle_use_change, @@ -465,6 +471,11 @@ class TxEditor(WindowModalDialog): self.config.WALLET_BATCH_RBF = b self.trigger_update() + def toggle_send_change_to_lightning(self): + b = not self.config.WALLET_SEND_CHANGE_TO_LIGHTNING + self.config.WALLET_SEND_CHANGE_TO_LIGHTNING = b + self.trigger_update() + def toggle_confirmed_only(self): b = not self.config.WALLET_SPEND_CONFIRMED_ONLY self.config.WALLET_SPEND_CONFIRMED_ONLY = b @@ -563,6 +574,8 @@ class TxEditor(WindowModalDialog): self.error = long_warning else: messages.append(long_warning) + if self.tx.has_dummy_output('swap'): + messages.append(_('This transaction will send funds to a submarine swap.')) # warn if spending unconf if any((txin.block_height is not None and txin.block_height<=0) for txin in self.tx.inputs()): messages.append(_('This transaction will spend unconfirmed coins.')) @@ -573,6 +586,9 @@ class TxEditor(WindowModalDialog): num_change = sum(int(o.is_change) for o in self.tx.outputs()) if num_change > 1: messages.append(_('This transaction has {} change outputs.'.format(num_change))) + if num_change == 0: + messages.append(_('Make sure you pay enough mining fees; you will not be able to bump the fee later.')) + # TODO: warn if we send change back to input address return messages diff --git a/electrum/gui/qt/main_window.py b/electrum/gui/qt/main_window.py index 34e44e963..b1fa6ac48 100644 --- a/electrum/gui/qt/main_window.py +++ b/electrum/gui/qt/main_window.py @@ -51,7 +51,7 @@ import electrum from electrum.gui import messages from electrum import (keystore, ecc, constants, util, bitcoin, commands, paymentrequest, lnutil) -from electrum.bitcoin import COIN, is_address +from electrum.bitcoin import COIN, is_address, get_dummy_address from electrum.plugin import run_hook, BasePlugin from electrum.i18n import _ from electrum.util import (format_time, UserCancelled, profiler, bfh, InvalidPassword, @@ -70,7 +70,7 @@ from electrum.network import Network, UntrustedServerReturnedError, NetworkExcep from electrum.exchange_rate import FxThread from electrum.simple_config import SimpleConfig from electrum.logging import Logger -from electrum.lnutil import ln_dummy_address, extract_nodeid, ConnStringFormatError +from electrum.lnutil import extract_nodeid, ConnStringFormatError from electrum.lnaddr import lndecode from electrum.submarine_swaps import SwapServerError @@ -163,6 +163,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): computing_privkeys_signal = pyqtSignal() show_privkeys_signal = pyqtSignal() show_error_signal = pyqtSignal(str) + show_message_signal = pyqtSignal(str) labels_changed_signal = pyqtSignal() def __init__(self, gui_object: 'ElectrumGui', wallet: Abstract_Wallet): @@ -263,6 +264,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): self.app.update_fiat_signal.connect(self.update_fiat) self.show_error_signal.connect(self.show_error) + self.show_message_signal.connect(self.show_message) self.history_list.setFocus() # network callbacks @@ -1236,6 +1238,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): '''Sign the transaction in a separate thread. When done, calls the callback with a success code of True or False. ''' + def on_success(result): callback(True) def on_failure(exc_info): @@ -1279,7 +1282,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): @protected def _open_channel(self, connect_str, funding_sat, push_amt, funding_tx, password): # read funding_sat from tx; converts '!' to int value - funding_sat = funding_tx.output_value_for_address(ln_dummy_address()) + funding_sat = funding_tx.output_value_for_address(get_dummy_address('channel')) def task(): return self.wallet.lnworker.open_channel( connect_str=connect_str, @@ -2822,3 +2825,14 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener): return d = RebalanceDialog(self, chan1, chan2, amount_sat) d.run() + + def on_swap_result(self, txid): + msg = _("Submarine swap") + ': ' + (_("Success") if txid else _("Expired")) + '\n\n' + if txid: + msg += _("Funding transaction") + ': ' + txid + '\n' + msg += _("Please remain online until the funding transaction is confirmed.") + self.show_message_signal.emit(msg) + else: + msg += _("Lightning funds were not received.") + self.show_error_signal.emit(msg) + diff --git a/electrum/gui/qt/send_tab.py b/electrum/gui/qt/send_tab.py index 91169f46a..337bce707 100644 --- a/electrum/gui/qt/send_tab.py +++ b/electrum/gui/qt/send_tab.py @@ -325,6 +325,15 @@ class SendTab(QWidget, MessageBoxMixin, Logger): # user cancelled return is_preview = conf_dlg.is_preview + + if tx.has_dummy_output('swap'): + sm = self.wallet.lnworker.swap_manager + coro = sm.request_swap_for_tx(tx) + swap, invoice, tx = self.network.run_from_another_thread(coro) + assert not tx.has_dummy_output('swap') + tx.swap_invoice = invoice + tx.swap_payment_hash = swap.payment_hash + if is_preview: self.window.show_transaction(tx, external_keypairs=external_keypairs, payment_identifier=payment_identifier) return @@ -711,6 +720,12 @@ class SendTab(QWidget, MessageBoxMixin, Logger): def broadcast_transaction(self, tx: Transaction, *, payment_identifier: PaymentIdentifier = None): # note: payment_identifier is explicitly passed as self.payto_e.payment_identifier might # already be cleared or otherwise have changed. + if hasattr(tx, 'swap_payment_hash'): + sm = self.wallet.lnworker.swap_manager + swap = sm.get_swap(tx.swap_payment_hash) + coro = sm.wait_for_htlcs_and_broadcast(swap, tx.swap_invoice, tx) + self.window.run_coroutine_from_thread(coro, _('Awaiting lightning payment..'), on_result=self.window.on_swap_result) + return def broadcast_thread(): # non-GUI thread diff --git a/electrum/gui/qt/swap_dialog.py b/electrum/gui/qt/swap_dialog.py index 0b5e46e73..b29c9fff0 100644 --- a/electrum/gui/qt/swap_dialog.py +++ b/electrum/gui/qt/swap_dialog.py @@ -5,7 +5,7 @@ from PyQt5.QtWidgets import QLabel, QVBoxLayout, QGridLayout, QPushButton from electrum.i18n import _ from electrum.util import NotEnoughFunds, NoDynamicFeeEstimates -from electrum.lnutil import ln_dummy_address +from electrum.bitcoin import get_dummy_address from electrum.transaction import PartialTxOutput, PartialTransaction from electrum.gui import messages @@ -173,7 +173,7 @@ class SwapDialog(WindowModalDialog, QtEventListener): def _spend_max_forward_swap(self, tx: Optional[PartialTransaction]) -> None: if tx: - amount = tx.output_value_for_address(ln_dummy_address()) + amount = tx.output_value_for_address(get_dummy_address('swap')) self.send_amount_e.setAmount(amount) else: self.send_amount_e.setAmount(None) @@ -256,7 +256,7 @@ class SwapDialog(WindowModalDialog, QtEventListener): lightning_amount_sat=lightning_amount, expected_onchain_amount_sat=onchain_amount + self.swap_manager.get_claim_fee(), ) - self.window.run_coroutine_from_thread(coro, _('Swapping funds'), on_result=self.on_result) + self.window.run_coroutine_from_thread(coro, _('Swapping funds'), on_result=self.window.on_swap_result) return True else: lightning_amount = self.recv_amount_e.get_amount() @@ -294,7 +294,7 @@ class SwapDialog(WindowModalDialog, QtEventListener): raise InvalidSwapParameters("swap_manager.max_amount_forward_swap() is None") if max_amount > max_swap_amount: onchain_amount = max_swap_amount - outputs = [PartialTxOutput.from_address_and_value(ln_dummy_address(), onchain_amount)] + outputs = [PartialTxOutput.from_address_and_value(get_dummy_address('swap'), onchain_amount)] try: tx = self.window.wallet.make_unsigned_transaction( coins=coins, @@ -325,16 +325,7 @@ class SwapDialog(WindowModalDialog, QtEventListener): tx=tx, channels=self.channels, ) - self.window.run_coroutine_from_thread(coro, _('Swapping funds'), on_result=self.on_result) - - def on_result(self, txid): - msg = _("Submarine swap") + ': ' + (_("Success") if txid else _("Expired")) + '\n\n' - if txid: - msg += _("Funding transaction") + ': ' + txid + '\n' - msg += _("Please remain online until the funding transaction is confirmed.") - else: - msg += _("Lightning funds were not received.") - self.window.show_error_signal.emit(msg) + self.window.run_coroutine_from_thread(coro, _('Swapping funds'), on_result=self.window.on_swap_result) def get_description(self): onchain_funds = "onchain funds" diff --git a/electrum/gui/qt/transaction_dialog.py b/electrum/gui/qt/transaction_dialog.py index b6a483d24..d693c7323 100644 --- a/electrum/gui/qt/transaction_dialog.py +++ b/electrum/gui/qt/transaction_dialog.py @@ -46,7 +46,7 @@ from electrum.simple_config import SimpleConfig from electrum.util import quantize_feerate from electrum import bitcoin -from electrum.bitcoin import base_encode, NLOCKTIME_BLOCKHEIGHT_MAX +from electrum.bitcoin import base_encode, NLOCKTIME_BLOCKHEIGHT_MAX, get_dummy_address from electrum.i18n import _ from electrum.plugin import run_hook from electrum import simple_config @@ -123,6 +123,8 @@ class TxInOutWidget(QWidget): legend=_("Change Address"), color=ColorScheme.YELLOW, tooltip=_("Wallet change address")) self.txo_color_2fa = TxOutputColoring( legend=_("TrustedCoin (2FA) batch fee"), color=ColorScheme.BLUE, tooltip=_("TrustedCoin (2FA) fee for the next batch of transactions")) + self.txo_color_swap = TxOutputColoring( + legend=_("Submarine swap address"), color=ColorScheme.BLUE, tooltip=_("Submarine swap address")) self.outputs_header = QLabel() self.outputs_textedit = QTextBrowserWithDefaultSize(750, 100) self.outputs_textedit.setOpenLinks(False) # disable automatic link opening @@ -139,6 +141,7 @@ class TxInOutWidget(QWidget): outheader_hbox.addWidget(self.txo_color_recv.legend_label) outheader_hbox.addWidget(self.txo_color_change.legend_label) outheader_hbox.addWidget(self.txo_color_2fa.legend_label) + outheader_hbox.addWidget(self.txo_color_swap.legend_label) vbox = QVBoxLayout() vbox.addLayout(self.inheader_hbox) @@ -164,9 +167,10 @@ class TxInOutWidget(QWidget): lnk.setToolTip(_('Click to open, right-click for menu')) lnk.setAnchor(True) lnk.setUnderlineStyle(QTextCharFormat.SingleUnderline) - tf_used_recv, tf_used_change, tf_used_2fa = False, False, False + tf_used_recv, tf_used_change, tf_used_2fa, tf_used_swap = False, False, False, False def addr_text_format(addr: str) -> QTextCharFormat: - nonlocal tf_used_recv, tf_used_change, tf_used_2fa + nonlocal tf_used_recv, tf_used_change, tf_used_2fa, tf_used_swap + sm = self.wallet.lnworker.swap_manager if self.wallet.is_mine(addr): if self.wallet.is_change(addr): tf_used_change = True @@ -179,6 +183,9 @@ class TxInOutWidget(QWidget): fmt.setAnchor(True) fmt.setUnderlineStyle(QTextCharFormat.SingleUnderline) return fmt + elif sm and sm.is_lockup_address_for_a_swap(addr) or addr==get_dummy_address('swap'): + tf_used_swap = True + return self.txo_color_swap.text_char_format elif self.wallet.is_billing_address(addr): tf_used_2fa = True return self.txo_color_2fa.text_char_format @@ -267,6 +274,7 @@ class TxInOutWidget(QWidget): self.txo_color_recv.legend_label.setVisible(tf_used_recv) self.txo_color_change.legend_label.setVisible(tf_used_change) self.txo_color_2fa.legend_label.setVisible(tf_used_2fa) + self.txo_color_swap.legend_label.setVisible(tf_used_swap) def _open_internal_link(self, target): """Accepts either a str txid, str address, or a QUrl which should be diff --git a/electrum/lnpeer.py b/electrum/lnpeer.py index a9714bc7f..70d0ddcfe 100644 --- a/electrum/lnpeer.py +++ b/electrum/lnpeer.py @@ -24,7 +24,7 @@ from . import constants from .util import (bfh, log_exceptions, ignore_exceptions, chunks, OldTaskGroup, UnrelatedTransactionException, error_text_bytes_to_safe_str) from . import transaction -from .bitcoin import make_op_return +from .bitcoin import make_op_return, get_dummy_address from .transaction import PartialTxOutput, match_script_against_template, Sighash from .logging import Logger from .lnonion import (new_onion_packet, OnionFailureCode, calc_hops_data_for_payment, @@ -48,7 +48,6 @@ from .lntransport import LNTransport, LNTransportBase from .lnmsg import encode_msg, decode_msg, UnknownOptionalMsgType, FailedToParseMsg from .interface import GracefulDisconnect from .lnrouter import fee_for_edge_msat -from .lnutil import ln_dummy_address from .json_db import StoredDict from .invoices import PR_PAID from .simple_config import FEE_LN_ETA_TARGET @@ -813,11 +812,7 @@ class Peer(Logger): redeem_script = funding_output_script(local_config, remote_config) funding_address = bitcoin.redeem_script_to_address('p2wsh', redeem_script) funding_output = PartialTxOutput.from_address_and_value(funding_address, funding_sat) - dummy_output = PartialTxOutput.from_address_and_value(ln_dummy_address(), funding_sat) - if dummy_output not in funding_tx.outputs(): raise Exception("LN dummy output (err 1)") - funding_tx._outputs.remove(dummy_output) - if dummy_output in funding_tx.outputs(): raise Exception("LN dummy output (err 2)") - funding_tx.add_outputs([funding_output]) + funding_tx.replace_dummy_output('channel', funding_address) # find and encrypt op_return data associated to funding_address has_onchain_backup = self.lnworker and self.lnworker.has_recoverable_channels() if has_onchain_backup: diff --git a/electrum/lnutil.py b/electrum/lnutil.py index c7fb8a7a5..5bc6e1f0e 100644 --- a/electrum/lnutil.py +++ b/electrum/lnutil.py @@ -53,9 +53,6 @@ HTLC_OUTPUT_WEIGHT = 172 LN_MAX_FUNDING_SAT_LEGACY = pow(2, 24) - 1 DUST_LIMIT_MAX = 1000 -# dummy address for fee estimation of funding tx -def ln_dummy_address(): - return redeem_script_to_address('p2wsh', '') from .json_db import StoredObject, stored_in, stored_as diff --git a/electrum/lnworker.py b/electrum/lnworker.py index e67b9725c..9e6442ec1 100644 --- a/electrum/lnworker.py +++ b/electrum/lnworker.py @@ -56,7 +56,7 @@ from .lnchannel import ChannelState, PeerState, HTLCWithStatus from .lnrater import LNRater from . import lnutil from .lnutil import funding_output_script -from .bitcoin import redeem_script_to_address +from .bitcoin import redeem_script_to_address, get_dummy_address from .lnutil import (Outpoint, LNPeerAddr, get_compressed_pubkey_from_bech32, extract_nodeid, PaymentFailure, split_host_port, ConnStringFormatError, @@ -66,7 +66,7 @@ from .lnutil import (Outpoint, LNPeerAddr, UpdateAddHtlc, Direction, LnFeatures, ShortChannelID, HtlcLog, derive_payment_secret_from_payment_preimage, NoPathFound, InvalidGossipMsg) -from .lnutil import ln_dummy_address, ln_compare_features, IncompatibleLightningFeatures +from .lnutil import ln_compare_features, IncompatibleLightningFeatures from .transaction import PartialTxOutput, PartialTransaction, PartialTxInput from .lnonion import OnionFailureCode, OnionRoutingFailure, OnionPacket from .lnmsg import decode_msg @@ -1274,7 +1274,7 @@ class LNWallet(LNWorker): funding_sat: int, node_id: bytes, fee_est=None) -> PartialTransaction: - outputs = [PartialTxOutput.from_address_and_value(ln_dummy_address(), funding_sat)] + outputs = [PartialTxOutput.from_address_and_value(get_dummy_address('channel'), funding_sat)] if self.has_recoverable_channels(): dummy_scriptpubkey = make_op_return(self.cb_data(node_id)) outputs.append(PartialTxOutput(scriptpubkey=dummy_scriptpubkey, value=0)) @@ -2549,7 +2549,7 @@ class LNWallet(LNWorker): # check that we can send onchain swap_server_mining_fee = 10000 # guessing, because we have not called get_pairs yet swap_funding_sat = swap_recv_amount + swap_server_mining_fee - swap_output = PartialTxOutput.from_address_and_value(ln_dummy_address(), int(swap_funding_sat)) + swap_output = PartialTxOutput.from_address_and_value(get_dummy_address('swap'), int(swap_funding_sat)) if not self.wallet.can_pay_onchain([swap_output], coins=coins): continue return (chan, swap_recv_amount) diff --git a/electrum/simple_config.py b/electrum/simple_config.py index 961748641..88fa144ac 100644 --- a/electrum/simple_config.py +++ b/electrum/simple_config.py @@ -877,6 +877,7 @@ class SimpleConfig(Logger): WALLET_PAYREQ_EXPIRY_SECONDS = ConfigVar('request_expiry', default=invoices.PR_DEFAULT_EXPIRATION_WHEN_CREATING, type_=int) WALLET_USE_SINGLE_PASSWORD = ConfigVar('single_password', default=False, type_=bool) # note: 'use_change' and 'multiple_change' are per-wallet settings + WALLET_SEND_CHANGE_TO_LIGHTNING = ConfigVar('send_change_to_lightning', default=False, type_=bool) FX_USE_EXCHANGE_RATE = ConfigVar('use_exchange_rate', default=False, type_=bool) FX_CURRENCY = ConfigVar('currency', default='EUR', type_=str) diff --git a/electrum/submarine_swaps.py b/electrum/submarine_swaps.py index a20ad441f..9555655c1 100644 --- a/electrum/submarine_swaps.py +++ b/electrum/submarine_swaps.py @@ -16,8 +16,8 @@ from .bitcoin import (script_to_p2wsh, opcodes, p2wsh_nested_script, push_script from .transaction import PartialTxInput, PartialTxOutput, PartialTransaction, Transaction, TxInput, TxOutpoint from .transaction import script_GetOp, match_script_against_template, OPPushDataGeneric, OPPushDataPubkey from .util import log_exceptions, BelowDustLimit, OldTaskGroup -from .lnutil import REDEEM_AFTER_DOUBLE_SPENT_DELAY, ln_dummy_address -from .bitcoin import dust_threshold +from .lnutil import REDEEM_AFTER_DOUBLE_SPENT_DELAY +from .bitcoin import dust_threshold, get_dummy_address from .logging import Logger from .lnutil import hex_to_bytes from .lnaddr import lndecode @@ -190,6 +190,7 @@ class SwapManager(Logger): self.wallet = wallet self.lnworker = lnworker self.taskgroup = None + self.dummy_address = get_dummy_address('swap') self.swaps = self.wallet.db.get_dict('submarine_swaps') # type: Dict[str, SwapData] self._swaps_by_funding_outpoint = {} # type: Dict[TxOutpoint, SwapData] @@ -578,6 +579,11 @@ class SwapManager(Logger): """ assert self.network assert self.lnwatcher + swap, invoice = await self.request_normal_swap(lightning_amount_sat, expected_onchain_amount_sat, channels=channels) + tx = self.create_funding_tx(swap, tx, password) + return await self.wait_for_htlcs_and_broadcast(swap, invoice, tx, channels=channels) + + async def request_normal_swap(self, lightning_amount_sat, expected_onchain_amount_sat, channels=None): amount_msat = lightning_amount_sat * 1000 refund_privkey = os.urandom(32) refund_pubkey = ECPrivkey(refund_privkey).get_public_key_bytes(compressed=True) @@ -654,8 +660,11 @@ class SwapManager(Logger): their_pubkey=claim_pubkey, invoice=invoice, prepay=False) + return swap, invoice - tx = self.create_funding_tx(swap, tx, password) + async def wait_for_htlcs_and_broadcast(self, swap, invoice, tx, channels=None): + payment_hash = swap.payment_hash + refund_pubkey = ECPrivkey(swap.privkey).get_public_key_bytes(compressed=True) if self.server_supports_htlc_first: # send invoice to server and wait for htlcs request_data = { @@ -679,23 +688,39 @@ class SwapManager(Logger): else: # broadcast funding tx right away await self.broadcast_funding_tx(swap, tx) + # fixme: if broadcast fails, we need to fail htlcs and cancel the swap return swap.funding_txid def create_funding_tx(self, swap, tx, password): # create funding tx # note: rbf must not decrease payment # this is taken care of in wallet._is_rbf_allowed_to_touch_tx_output - funding_output = PartialTxOutput.from_address_and_value(swap.lockup_address, swap.onchain_amount) if tx is None: + funding_output = PartialTxOutput.from_address_and_value(swap.lockup_address, swap.onchain_amount) tx = self.wallet.create_transaction(outputs=[funding_output], rbf=True, password=password) else: - dummy_output = PartialTxOutput.from_address_and_value(ln_dummy_address(), swap.onchain_amount) - tx.outputs().remove(dummy_output) - tx.add_outputs([funding_output]) + tx.replace_dummy_output('swap', swap.lockup_address) tx.set_rbf(True) self.wallet.sign_transaction(tx, password) return tx + @log_exceptions + async def request_swap_for_tx(self, tx): + for o in tx.outputs(): + if o.address == self.dummy_address: + change_amount = o.value + break + else: + return + await self.get_pairs() + assert self.server_supports_htlc_first + lightning_amount_sat = self.get_recv_amount(change_amount, is_reverse=False) + swap, invoice = await self.request_normal_swap( + lightning_amount_sat = lightning_amount_sat, + expected_onchain_amount_sat=change_amount) + tx.replace_dummy_output('swap', swap.lockup_address) + return swap, invoice, tx + @log_exceptions async def broadcast_funding_tx(self, swap, tx): await self.network.broadcast_transaction(tx) diff --git a/electrum/transaction.py b/electrum/transaction.py index b11a2ee64..eccc87a3e 100644 --- a/electrum/transaction.py +++ b/electrum/transaction.py @@ -52,6 +52,7 @@ from .bitcoin import (TYPE_ADDRESS, TYPE_SCRIPT, hash_160, from .crypto import sha256d from .logging import get_logger from .util import ShortID, OldTaskGroup +from .bitcoin import get_dummy_address from .descriptor import Descriptor, MissingSolutionPiece, create_dummy_descriptor_from_address from .json_db import stored_in @@ -1155,6 +1156,27 @@ class Transaction: script = bitcoin.address_to_script(addr) return self.get_output_idxs_from_scriptpubkey(script) + def replace_output_address(self, old_address, new_address): + idx = list(self.get_output_idxs_from_address(old_address)) + assert len(idx) == 1 + amount = self._outputs[idx[0]].value + funding_output = PartialTxOutput.from_address_and_value(new_address, amount) + old_output = PartialTxOutput.from_address_and_value(old_address, amount) + self._outputs.remove(old_output) + self.add_outputs([funding_output]) + delattr(self, '_script_to_output_idx') + + def get_change_outputs(self): + return [o for o in self._outputs if o.is_change] + + def replace_dummy_output(self, purpose, new_address): + dummy_addr = get_dummy_address(purpose) + self.replace_output_address(dummy_addr, new_address) + + def has_dummy_output(self, purpose): + addr = get_dummy_address(purpose) + return len(self.get_output_idxs_from_address(addr)) == 1 + def output_value_for_address(self, addr): # assumes exactly one output has that address for o in self.outputs(): diff --git a/electrum/wallet.py b/electrum/wallet.py index 4ea693ba4..f75dab978 100644 --- a/electrum/wallet.py +++ b/electrum/wallet.py @@ -60,7 +60,7 @@ from .util import (NotEnoughFunds, UserCancelled, profiler, OldTaskGroup, ignore Fiat, bfh, TxMinedInfo, quantize_feerate, OrderedDictWithIndex) from .simple_config import SimpleConfig, FEE_RATIO_HIGH_WARNING, FEERATE_WARNING_HIGH_FEE from .bitcoin import COIN, TYPE_ADDRESS -from .bitcoin import is_address, address_to_script, is_minikey, relayfee, dust_threshold +from .bitcoin import is_address, address_to_script, is_minikey, relayfee, dust_threshold, get_dummy_address from .crypto import sha256d from . import keystore from .keystore import (load_keystore, Hardware_KeyStore, KeyStore, KeyStoreWithMPK, @@ -1762,6 +1762,14 @@ class Abstract_Wallet(ABC, Logger, EventListener): change_addrs=change_addrs, fee_estimator_vb=fee_estimator, dust_threshold=self.dust_threshold()) + if self.lnworker and self.config.WALLET_SEND_CHANGE_TO_LIGHTNING: + change = tx.get_change_outputs() + # do not use multiple change addresses + if len(change) == 1: + amount = change[0].value + ln_amount = self.lnworker.swap_manager.get_recv_amount(amount, is_reverse=False) + if ln_amount and ln_amount <= self.lnworker.num_sats_can_receive(): + tx.replace_output_address(change[0].address, get_dummy_address('swap')) else: # "spend max" branch # note: This *will* spend inputs with negative effective value (if there are any).