Browse Source

send tx change to lightning

master
ThomasV 3 years ago
parent
commit
649ce979ab
  1. 4
      electrum/bitcoin.py
  2. 2
      electrum/coinchooser.py
  3. 5
      electrum/gui/qml/qechannelopener.py
  4. 6
      electrum/gui/qml/qeswaphelper.py
  5. 16
      electrum/gui/qt/confirm_tx_dialog.py
  6. 20
      electrum/gui/qt/main_window.py
  7. 15
      electrum/gui/qt/send_tab.py
  8. 19
      electrum/gui/qt/swap_dialog.py
  9. 14
      electrum/gui/qt/transaction_dialog.py
  10. 9
      electrum/lnpeer.py
  11. 3
      electrum/lnutil.py
  12. 8
      electrum/lnworker.py
  13. 1
      electrum/simple_config.py
  14. 39
      electrum/submarine_swaps.py
  15. 22
      electrum/transaction.py
  16. 10
      electrum/wallet.py

4
electrum/bitcoin.py

@ -753,3 +753,7 @@ def is_minikey(text: str) -> bool:
def minikey_to_private_key(text: str) -> bytes: def minikey_to_private_key(text: str) -> bytes:
return sha256(text) 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())

2
electrum/coinchooser.py

@ -221,6 +221,8 @@ class CoinChooserBase(Logger):
amounts = [amount for amount in amounts if amount >= dust_threshold] amounts = [amount for amount in amounts if amount >= dust_threshold]
change = [PartialTxOutput.from_address_and_value(addr, amount) change = [PartialTxOutput.from_address_and_value(addr, amount)
for addr, amount in zip(change_addrs, amounts)] for addr, amount in zip(change_addrs, amounts)]
for c in change:
c.is_change = True
return change return change
def _construct_tx_from_selected_buckets(self, *, buckets: Sequence[Bucket], def _construct_tx_from_selected_buckets(self, *, buckets: Sequence[Bucket],

5
electrum/gui/qml/qechannelopener.py

@ -8,7 +8,8 @@ from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
from electrum.i18n import _ from electrum.i18n import _
from electrum.gui import messages from electrum.gui import messages
from electrum.util import bfh 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.lnworker import hardcoded_trampoline_nodes
from electrum.logging import get_logger from electrum.logging import get_logger
@ -181,7 +182,7 @@ class QEChannelOpener(QObject, AuthMixin):
""" """
self._logger.debug('opening channel') self._logger.debug('opening channel')
# read funding_sat from tx; converts '!' to int value # 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 lnworker = self._wallet.wallet.lnworker
def open_thread(): def open_thread():

6
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 PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QTimer, Q_ENUMS
from electrum.i18n import _ 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.logging import get_logger
from electrum.transaction import PartialTxOutput from electrum.transaction import PartialTxOutput
from electrum.util import NotEnoughFunds, NoDynamicFeeEstimates, profiler, get_asyncio_loop 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 # this is just to estimate the maximal spendable onchain amount for HTLC
self.update_tx('!') self.update_tx('!')
try: 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 except AttributeError: # happens if there are no utxos
max_onchain_spend = 0 max_onchain_spend = 0
reverse = int(min(lnworker.num_sats_can_send(), reverse = int(min(lnworker.num_sats_can_send(),
@ -283,7 +283,7 @@ class QESwapHelper(AuthMixin, QObject, QtEventListener):
self._tx = None self._tx = None
self.valid = False self.valid = False
return 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) coins = self._wallet.wallet.get_spendable_coins(None)
try: try:
self._tx = self._wallet.wallet.make_unsigned_transaction( self._tx = self._wallet.wallet.make_unsigned_transaction(

16
electrum/gui/qt/confirm_tx_dialog.py

@ -96,6 +96,7 @@ class TxEditor(WindowModalDialog):
vbox.addLayout(grid) vbox.addLayout(grid)
vbox.addWidget(self.io_widget) vbox.addWidget(self.io_widget)
self.message_label = WWLabel('') self.message_label = WWLabel('')
self.message_label.setMinimumHeight(70)
vbox.addWidget(self.message_label) vbox.addWidget(self.message_label)
buttons = self.create_buttons_bar() buttons = self.create_buttons_bar()
@ -395,6 +396,11 @@ class TxEditor(WindowModalDialog):
self.toggle_locktime, self.toggle_locktime,
_('Edit Locktime'), '') _('Edit Locktime'), '')
self.pref_menu.addSeparator() 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( add_pref_action(
self.wallet.use_change, self.wallet.use_change,
self.toggle_use_change, self.toggle_use_change,
@ -465,6 +471,11 @@ class TxEditor(WindowModalDialog):
self.config.WALLET_BATCH_RBF = b self.config.WALLET_BATCH_RBF = b
self.trigger_update() 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): def toggle_confirmed_only(self):
b = not self.config.WALLET_SPEND_CONFIRMED_ONLY b = not self.config.WALLET_SPEND_CONFIRMED_ONLY
self.config.WALLET_SPEND_CONFIRMED_ONLY = b self.config.WALLET_SPEND_CONFIRMED_ONLY = b
@ -563,6 +574,8 @@ class TxEditor(WindowModalDialog):
self.error = long_warning self.error = long_warning
else: else:
messages.append(long_warning) 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 # warn if spending unconf
if any((txin.block_height is not None and txin.block_height<=0) for txin in self.tx.inputs()): 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.')) 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()) num_change = sum(int(o.is_change) for o in self.tx.outputs())
if num_change > 1: if num_change > 1:
messages.append(_('This transaction has {} change outputs.'.format(num_change))) 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 # TODO: warn if we send change back to input address
return messages return messages

20
electrum/gui/qt/main_window.py

@ -51,7 +51,7 @@ import electrum
from electrum.gui import messages from electrum.gui import messages
from electrum import (keystore, ecc, constants, util, bitcoin, commands, from electrum import (keystore, ecc, constants, util, bitcoin, commands,
paymentrequest, lnutil) 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.plugin import run_hook, BasePlugin
from electrum.i18n import _ from electrum.i18n import _
from electrum.util import (format_time, UserCancelled, profiler, bfh, InvalidPassword, 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.exchange_rate import FxThread
from electrum.simple_config import SimpleConfig from electrum.simple_config import SimpleConfig
from electrum.logging import Logger 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.lnaddr import lndecode
from electrum.submarine_swaps import SwapServerError from electrum.submarine_swaps import SwapServerError
@ -163,6 +163,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
computing_privkeys_signal = pyqtSignal() computing_privkeys_signal = pyqtSignal()
show_privkeys_signal = pyqtSignal() show_privkeys_signal = pyqtSignal()
show_error_signal = pyqtSignal(str) show_error_signal = pyqtSignal(str)
show_message_signal = pyqtSignal(str)
labels_changed_signal = pyqtSignal() labels_changed_signal = pyqtSignal()
def __init__(self, gui_object: 'ElectrumGui', wallet: Abstract_Wallet): 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.app.update_fiat_signal.connect(self.update_fiat)
self.show_error_signal.connect(self.show_error) self.show_error_signal.connect(self.show_error)
self.show_message_signal.connect(self.show_message)
self.history_list.setFocus() self.history_list.setFocus()
# network callbacks # network callbacks
@ -1236,6 +1238,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
'''Sign the transaction in a separate thread. When done, calls '''Sign the transaction in a separate thread. When done, calls
the callback with a success code of True or False. the callback with a success code of True or False.
''' '''
def on_success(result): def on_success(result):
callback(True) callback(True)
def on_failure(exc_info): def on_failure(exc_info):
@ -1279,7 +1282,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
@protected @protected
def _open_channel(self, connect_str, funding_sat, push_amt, funding_tx, password): def _open_channel(self, connect_str, funding_sat, push_amt, funding_tx, password):
# read funding_sat from tx; converts '!' to int value # 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(): def task():
return self.wallet.lnworker.open_channel( return self.wallet.lnworker.open_channel(
connect_str=connect_str, connect_str=connect_str,
@ -2822,3 +2825,14 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger, QtEventListener):
return return
d = RebalanceDialog(self, chan1, chan2, amount_sat) d = RebalanceDialog(self, chan1, chan2, amount_sat)
d.run() 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)

15
electrum/gui/qt/send_tab.py

@ -325,6 +325,15 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
# user cancelled # user cancelled
return return
is_preview = conf_dlg.is_preview 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: if is_preview:
self.window.show_transaction(tx, external_keypairs=external_keypairs, payment_identifier=payment_identifier) self.window.show_transaction(tx, external_keypairs=external_keypairs, payment_identifier=payment_identifier)
return return
@ -711,6 +720,12 @@ class SendTab(QWidget, MessageBoxMixin, Logger):
def broadcast_transaction(self, tx: Transaction, *, payment_identifier: PaymentIdentifier = None): def broadcast_transaction(self, tx: Transaction, *, payment_identifier: PaymentIdentifier = None):
# note: payment_identifier is explicitly passed as self.payto_e.payment_identifier might # note: payment_identifier is explicitly passed as self.payto_e.payment_identifier might
# already be cleared or otherwise have changed. # 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(): def broadcast_thread():
# non-GUI thread # non-GUI thread

19
electrum/gui/qt/swap_dialog.py

@ -5,7 +5,7 @@ from PyQt5.QtWidgets import QLabel, QVBoxLayout, QGridLayout, QPushButton
from electrum.i18n import _ from electrum.i18n import _
from electrum.util import NotEnoughFunds, NoDynamicFeeEstimates 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.transaction import PartialTxOutput, PartialTransaction
from electrum.gui import messages from electrum.gui import messages
@ -173,7 +173,7 @@ class SwapDialog(WindowModalDialog, QtEventListener):
def _spend_max_forward_swap(self, tx: Optional[PartialTransaction]) -> None: def _spend_max_forward_swap(self, tx: Optional[PartialTransaction]) -> None:
if tx: 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) self.send_amount_e.setAmount(amount)
else: else:
self.send_amount_e.setAmount(None) self.send_amount_e.setAmount(None)
@ -256,7 +256,7 @@ class SwapDialog(WindowModalDialog, QtEventListener):
lightning_amount_sat=lightning_amount, lightning_amount_sat=lightning_amount,
expected_onchain_amount_sat=onchain_amount + self.swap_manager.get_claim_fee(), 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 return True
else: else:
lightning_amount = self.recv_amount_e.get_amount() 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") raise InvalidSwapParameters("swap_manager.max_amount_forward_swap() is None")
if max_amount > max_swap_amount: if max_amount > max_swap_amount:
onchain_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: try:
tx = self.window.wallet.make_unsigned_transaction( tx = self.window.wallet.make_unsigned_transaction(
coins=coins, coins=coins,
@ -325,16 +325,7 @@ class SwapDialog(WindowModalDialog, QtEventListener):
tx=tx, tx=tx,
channels=self.channels, channels=self.channels,
) )
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)
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)
def get_description(self): def get_description(self):
onchain_funds = "onchain funds" onchain_funds = "onchain funds"

14
electrum/gui/qt/transaction_dialog.py

@ -46,7 +46,7 @@ from electrum.simple_config import SimpleConfig
from electrum.util import quantize_feerate from electrum.util import quantize_feerate
from electrum import bitcoin 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.i18n import _
from electrum.plugin import run_hook from electrum.plugin import run_hook
from electrum import simple_config from electrum import simple_config
@ -123,6 +123,8 @@ class TxInOutWidget(QWidget):
legend=_("Change Address"), color=ColorScheme.YELLOW, tooltip=_("Wallet change address")) legend=_("Change Address"), color=ColorScheme.YELLOW, tooltip=_("Wallet change address"))
self.txo_color_2fa = TxOutputColoring( self.txo_color_2fa = TxOutputColoring(
legend=_("TrustedCoin (2FA) batch fee"), color=ColorScheme.BLUE, tooltip=_("TrustedCoin (2FA) fee for the next batch of transactions")) 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_header = QLabel()
self.outputs_textedit = QTextBrowserWithDefaultSize(750, 100) self.outputs_textedit = QTextBrowserWithDefaultSize(750, 100)
self.outputs_textedit.setOpenLinks(False) # disable automatic link opening 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_recv.legend_label)
outheader_hbox.addWidget(self.txo_color_change.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_2fa.legend_label)
outheader_hbox.addWidget(self.txo_color_swap.legend_label)
vbox = QVBoxLayout() vbox = QVBoxLayout()
vbox.addLayout(self.inheader_hbox) vbox.addLayout(self.inheader_hbox)
@ -164,9 +167,10 @@ class TxInOutWidget(QWidget):
lnk.setToolTip(_('Click to open, right-click for menu')) lnk.setToolTip(_('Click to open, right-click for menu'))
lnk.setAnchor(True) lnk.setAnchor(True)
lnk.setUnderlineStyle(QTextCharFormat.SingleUnderline) 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: 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_mine(addr):
if self.wallet.is_change(addr): if self.wallet.is_change(addr):
tf_used_change = True tf_used_change = True
@ -179,6 +183,9 @@ class TxInOutWidget(QWidget):
fmt.setAnchor(True) fmt.setAnchor(True)
fmt.setUnderlineStyle(QTextCharFormat.SingleUnderline) fmt.setUnderlineStyle(QTextCharFormat.SingleUnderline)
return fmt 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): elif self.wallet.is_billing_address(addr):
tf_used_2fa = True tf_used_2fa = True
return self.txo_color_2fa.text_char_format 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_recv.legend_label.setVisible(tf_used_recv)
self.txo_color_change.legend_label.setVisible(tf_used_change) self.txo_color_change.legend_label.setVisible(tf_used_change)
self.txo_color_2fa.legend_label.setVisible(tf_used_2fa) 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): def _open_internal_link(self, target):
"""Accepts either a str txid, str address, or a QUrl which should be """Accepts either a str txid, str address, or a QUrl which should be

9
electrum/lnpeer.py

@ -24,7 +24,7 @@ from . import constants
from .util import (bfh, log_exceptions, ignore_exceptions, chunks, OldTaskGroup, from .util import (bfh, log_exceptions, ignore_exceptions, chunks, OldTaskGroup,
UnrelatedTransactionException, error_text_bytes_to_safe_str) UnrelatedTransactionException, error_text_bytes_to_safe_str)
from . import transaction 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 .transaction import PartialTxOutput, match_script_against_template, Sighash
from .logging import Logger from .logging import Logger
from .lnonion import (new_onion_packet, OnionFailureCode, calc_hops_data_for_payment, 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 .lnmsg import encode_msg, decode_msg, UnknownOptionalMsgType, FailedToParseMsg
from .interface import GracefulDisconnect from .interface import GracefulDisconnect
from .lnrouter import fee_for_edge_msat from .lnrouter import fee_for_edge_msat
from .lnutil import ln_dummy_address
from .json_db import StoredDict from .json_db import StoredDict
from .invoices import PR_PAID from .invoices import PR_PAID
from .simple_config import FEE_LN_ETA_TARGET from .simple_config import FEE_LN_ETA_TARGET
@ -813,11 +812,7 @@ class Peer(Logger):
redeem_script = funding_output_script(local_config, remote_config) redeem_script = funding_output_script(local_config, remote_config)
funding_address = bitcoin.redeem_script_to_address('p2wsh', redeem_script) funding_address = bitcoin.redeem_script_to_address('p2wsh', redeem_script)
funding_output = PartialTxOutput.from_address_and_value(funding_address, funding_sat) funding_output = PartialTxOutput.from_address_and_value(funding_address, funding_sat)
dummy_output = PartialTxOutput.from_address_and_value(ln_dummy_address(), funding_sat) funding_tx.replace_dummy_output('channel', funding_address)
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])
# find and encrypt op_return data associated to funding_address # find and encrypt op_return data associated to funding_address
has_onchain_backup = self.lnworker and self.lnworker.has_recoverable_channels() has_onchain_backup = self.lnworker and self.lnworker.has_recoverable_channels()
if has_onchain_backup: if has_onchain_backup:

3
electrum/lnutil.py

@ -53,9 +53,6 @@ HTLC_OUTPUT_WEIGHT = 172
LN_MAX_FUNDING_SAT_LEGACY = pow(2, 24) - 1 LN_MAX_FUNDING_SAT_LEGACY = pow(2, 24) - 1
DUST_LIMIT_MAX = 1000 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 from .json_db import StoredObject, stored_in, stored_as

8
electrum/lnworker.py

@ -56,7 +56,7 @@ from .lnchannel import ChannelState, PeerState, HTLCWithStatus
from .lnrater import LNRater from .lnrater import LNRater
from . import lnutil from . import lnutil
from .lnutil import funding_output_script 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, from .lnutil import (Outpoint, LNPeerAddr,
get_compressed_pubkey_from_bech32, extract_nodeid, get_compressed_pubkey_from_bech32, extract_nodeid,
PaymentFailure, split_host_port, ConnStringFormatError, PaymentFailure, split_host_port, ConnStringFormatError,
@ -66,7 +66,7 @@ from .lnutil import (Outpoint, LNPeerAddr,
UpdateAddHtlc, Direction, LnFeatures, ShortChannelID, UpdateAddHtlc, Direction, LnFeatures, ShortChannelID,
HtlcLog, derive_payment_secret_from_payment_preimage, HtlcLog, derive_payment_secret_from_payment_preimage,
NoPathFound, InvalidGossipMsg) 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 .transaction import PartialTxOutput, PartialTransaction, PartialTxInput
from .lnonion import OnionFailureCode, OnionRoutingFailure, OnionPacket from .lnonion import OnionFailureCode, OnionRoutingFailure, OnionPacket
from .lnmsg import decode_msg from .lnmsg import decode_msg
@ -1274,7 +1274,7 @@ class LNWallet(LNWorker):
funding_sat: int, funding_sat: int,
node_id: bytes, node_id: bytes,
fee_est=None) -> PartialTransaction: 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(): if self.has_recoverable_channels():
dummy_scriptpubkey = make_op_return(self.cb_data(node_id)) dummy_scriptpubkey = make_op_return(self.cb_data(node_id))
outputs.append(PartialTxOutput(scriptpubkey=dummy_scriptpubkey, value=0)) outputs.append(PartialTxOutput(scriptpubkey=dummy_scriptpubkey, value=0))
@ -2549,7 +2549,7 @@ class LNWallet(LNWorker):
# check that we can send onchain # check that we can send onchain
swap_server_mining_fee = 10000 # guessing, because we have not called get_pairs yet 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_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): if not self.wallet.can_pay_onchain([swap_output], coins=coins):
continue continue
return (chan, swap_recv_amount) return (chan, swap_recv_amount)

1
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_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) WALLET_USE_SINGLE_PASSWORD = ConfigVar('single_password', default=False, type_=bool)
# note: 'use_change' and 'multiple_change' are per-wallet settings # 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_USE_EXCHANGE_RATE = ConfigVar('use_exchange_rate', default=False, type_=bool)
FX_CURRENCY = ConfigVar('currency', default='EUR', type_=str) FX_CURRENCY = ConfigVar('currency', default='EUR', type_=str)

39
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 PartialTxInput, PartialTxOutput, PartialTransaction, Transaction, TxInput, TxOutpoint
from .transaction import script_GetOp, match_script_against_template, OPPushDataGeneric, OPPushDataPubkey from .transaction import script_GetOp, match_script_against_template, OPPushDataGeneric, OPPushDataPubkey
from .util import log_exceptions, BelowDustLimit, OldTaskGroup from .util import log_exceptions, BelowDustLimit, OldTaskGroup
from .lnutil import REDEEM_AFTER_DOUBLE_SPENT_DELAY, ln_dummy_address from .lnutil import REDEEM_AFTER_DOUBLE_SPENT_DELAY
from .bitcoin import dust_threshold from .bitcoin import dust_threshold, get_dummy_address
from .logging import Logger from .logging import Logger
from .lnutil import hex_to_bytes from .lnutil import hex_to_bytes
from .lnaddr import lndecode from .lnaddr import lndecode
@ -190,6 +190,7 @@ class SwapManager(Logger):
self.wallet = wallet self.wallet = wallet
self.lnworker = lnworker self.lnworker = lnworker
self.taskgroup = None 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 = self.wallet.db.get_dict('submarine_swaps') # type: Dict[str, SwapData]
self._swaps_by_funding_outpoint = {} # type: Dict[TxOutpoint, SwapData] self._swaps_by_funding_outpoint = {} # type: Dict[TxOutpoint, SwapData]
@ -578,6 +579,11 @@ class SwapManager(Logger):
""" """
assert self.network assert self.network
assert self.lnwatcher 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 amount_msat = lightning_amount_sat * 1000
refund_privkey = os.urandom(32) refund_privkey = os.urandom(32)
refund_pubkey = ECPrivkey(refund_privkey).get_public_key_bytes(compressed=True) refund_pubkey = ECPrivkey(refund_privkey).get_public_key_bytes(compressed=True)
@ -654,8 +660,11 @@ class SwapManager(Logger):
their_pubkey=claim_pubkey, their_pubkey=claim_pubkey,
invoice=invoice, invoice=invoice,
prepay=False) 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: if self.server_supports_htlc_first:
# send invoice to server and wait for htlcs # send invoice to server and wait for htlcs
request_data = { request_data = {
@ -679,23 +688,39 @@ class SwapManager(Logger):
else: else:
# broadcast funding tx right away # broadcast funding tx right away
await self.broadcast_funding_tx(swap, tx) await self.broadcast_funding_tx(swap, tx)
# fixme: if broadcast fails, we need to fail htlcs and cancel the swap
return swap.funding_txid return swap.funding_txid
def create_funding_tx(self, swap, tx, password): def create_funding_tx(self, swap, tx, password):
# create funding tx # create funding tx
# note: rbf must not decrease payment # note: rbf must not decrease payment
# this is taken care of in wallet._is_rbf_allowed_to_touch_tx_output # 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: 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) tx = self.wallet.create_transaction(outputs=[funding_output], rbf=True, password=password)
else: else:
dummy_output = PartialTxOutput.from_address_and_value(ln_dummy_address(), swap.onchain_amount) tx.replace_dummy_output('swap', swap.lockup_address)
tx.outputs().remove(dummy_output)
tx.add_outputs([funding_output])
tx.set_rbf(True) tx.set_rbf(True)
self.wallet.sign_transaction(tx, password) self.wallet.sign_transaction(tx, password)
return tx 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 @log_exceptions
async def broadcast_funding_tx(self, swap, tx): async def broadcast_funding_tx(self, swap, tx):
await self.network.broadcast_transaction(tx) await self.network.broadcast_transaction(tx)

22
electrum/transaction.py

@ -52,6 +52,7 @@ from .bitcoin import (TYPE_ADDRESS, TYPE_SCRIPT, hash_160,
from .crypto import sha256d from .crypto import sha256d
from .logging import get_logger from .logging import get_logger
from .util import ShortID, OldTaskGroup from .util import ShortID, OldTaskGroup
from .bitcoin import get_dummy_address
from .descriptor import Descriptor, MissingSolutionPiece, create_dummy_descriptor_from_address from .descriptor import Descriptor, MissingSolutionPiece, create_dummy_descriptor_from_address
from .json_db import stored_in from .json_db import stored_in
@ -1155,6 +1156,27 @@ class Transaction:
script = bitcoin.address_to_script(addr) script = bitcoin.address_to_script(addr)
return self.get_output_idxs_from_scriptpubkey(script) 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): def output_value_for_address(self, addr):
# assumes exactly one output has that address # assumes exactly one output has that address
for o in self.outputs(): for o in self.outputs():

10
electrum/wallet.py

@ -60,7 +60,7 @@ from .util import (NotEnoughFunds, UserCancelled, profiler, OldTaskGroup, ignore
Fiat, bfh, TxMinedInfo, quantize_feerate, OrderedDictWithIndex) Fiat, bfh, TxMinedInfo, quantize_feerate, OrderedDictWithIndex)
from .simple_config import SimpleConfig, FEE_RATIO_HIGH_WARNING, FEERATE_WARNING_HIGH_FEE from .simple_config import SimpleConfig, FEE_RATIO_HIGH_WARNING, FEERATE_WARNING_HIGH_FEE
from .bitcoin import COIN, TYPE_ADDRESS 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 .crypto import sha256d
from . import keystore from . import keystore
from .keystore import (load_keystore, Hardware_KeyStore, KeyStore, KeyStoreWithMPK, from .keystore import (load_keystore, Hardware_KeyStore, KeyStore, KeyStoreWithMPK,
@ -1762,6 +1762,14 @@ class Abstract_Wallet(ABC, Logger, EventListener):
change_addrs=change_addrs, change_addrs=change_addrs,
fee_estimator_vb=fee_estimator, fee_estimator_vb=fee_estimator,
dust_threshold=self.dust_threshold()) 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: else:
# "spend max" branch # "spend max" branch
# note: This *will* spend inputs with negative effective value (if there are any). # note: This *will* spend inputs with negative effective value (if there are any).

Loading…
Cancel
Save