Browse Source

Pass make_tx function to ConfirmTxDialog

- allow 'spend max' when opening a channel (fixes #5698)
 - display amount minus fee when 'max' buttons are pressed
 - estimate fee of channel funding using a template with dummy address
ln-negative-red
ThomasV 5 years ago
parent
commit
78813dcb7d
  1. 11
      electrum/commands.py
  2. 47
      electrum/gui/qt/channels_list.py
  3. 30
      electrum/gui/qt/confirm_tx_dialog.py
  4. 61
      electrum/gui/qt/main_window.py
  5. 4
      electrum/gui/qt/transaction_dialog.py
  6. 17
      electrum/lnpeer.py
  7. 5
      electrum/lnutil.py
  8. 23
      electrum/lnworker.py
  9. 4
      electrum/tests/regtest/regtest.sh
  10. 8
      electrum/transaction.py
  11. 4
      electrum/wallet.py

11
electrum/commands.py

@ -52,6 +52,7 @@ from .wallet import Abstract_Wallet, create_new_wallet, restore_wallet_from_text
from .address_synchronizer import TX_HEIGHT_LOCAL from .address_synchronizer import TX_HEIGHT_LOCAL
from .mnemonic import Mnemonic from .mnemonic import Mnemonic
from .lnutil import SENT, RECEIVED from .lnutil import SENT, RECEIVED
from .lnutil import ln_dummy_address
from .lnpeer import channel_id_from_funding_tx from .lnpeer import channel_id_from_funding_tx
from .plugin import run_hook from .plugin import run_hook
from .version import ELECTRUM_VERSION from .version import ELECTRUM_VERSION
@ -922,8 +923,12 @@ class Commands:
return True return True
@command('wpn') @command('wpn')
async def open_channel(self, connection_string, amount, channel_push=0, password=None, wallet: Abstract_Wallet = None): async def open_channel(self, connection_string, amount, push_amount=0, password=None, wallet: Abstract_Wallet = None):
chan = await wallet.lnworker._open_channel_coroutine(connection_string, satoshis(amount), satoshis(channel_push), password) funding_sat = satoshis(amount)
push_sat = satoshis(push_amount)
dummy_output = PartialTxOutput.from_address_and_value(ln_dummy_address(), funding_sat)
funding_tx = wallet.mktx(outputs = [dummy_output], rbf=False, sign=False, nonlocal_only=True)
chan = await wallet.lnworker._open_channel_coroutine(connection_string, funding_tx, funding_sat, push_sat, password)
return chan.funding_outpoint.to_str() return chan.funding_outpoint.to_str()
@command('wn') @command('wn')
@ -1037,7 +1042,7 @@ command_options = {
'timeout': (None, "Timeout in seconds"), 'timeout': (None, "Timeout in seconds"),
'force': (None, "Create new address beyond gap limit, if no more addresses are available."), 'force': (None, "Create new address beyond gap limit, if no more addresses are available."),
'pending': (None, "Show only pending requests."), 'pending': (None, "Show only pending requests."),
'channel_push':(None, 'Push initial amount (in BTC)'), 'push_amount': (None, 'Push initial amount (in BTC)'),
'expired': (None, "Show only expired requests."), 'expired': (None, "Show only expired requests."),
'paid': (None, "Show only paid requests."), 'paid': (None, "Show only paid requests."),
'show_addresses': (None, "Show input and output addresses"), 'show_addresses': (None, "Show input and output addresses"),

47
electrum/gui/qt/channels_list.py

@ -5,15 +5,15 @@ from enum import IntEnum
from PyQt5 import QtCore, QtWidgets, QtGui from PyQt5 import QtCore, QtWidgets, QtGui
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QMenu, QHBoxLayout, QLabel, QVBoxLayout, QGridLayout, QLineEdit from PyQt5.QtWidgets import QMenu, QHBoxLayout, QLabel, QVBoxLayout, QGridLayout, QLineEdit, QPushButton
from electrum.util import inv_dict, bh2u, bfh from electrum.util import inv_dict, bh2u, bfh
from electrum.i18n import _ from electrum.i18n import _
from electrum.lnchannel import Channel from electrum.lnchannel import Channel
from electrum.wallet import Abstract_Wallet from electrum.wallet import Abstract_Wallet
from electrum.lnutil import LOCAL, REMOTE, ConnStringFormatError, format_short_channel_id from electrum.lnutil import LOCAL, REMOTE, ConnStringFormatError, format_short_channel_id, LN_MAX_FUNDING_SAT
from .util import MyTreeView, WindowModalDialog, Buttons, OkButton, CancelButton, EnterButton, WWLabel, WaitingDialog from .util import MyTreeView, WindowModalDialog, Buttons, OkButton, CancelButton, EnterButton, WWLabel, WaitingDialog, HelpLabel
from .amountedit import BTCAmountEdit from .amountedit import BTCAmountEdit
from .channel_details import ChannelDetailsDialog from .channel_details import ChannelDetailsDialog
@ -162,26 +162,38 @@ class ChannelsList(MyTreeView):
def new_channel_dialog(self): def new_channel_dialog(self):
lnworker = self.parent.wallet.lnworker lnworker = self.parent.wallet.lnworker
d = WindowModalDialog(self.parent, _('Open Channel')) d = WindowModalDialog(self.parent, _('Open Channel'))
d.setMinimumWidth(700)
vbox = QVBoxLayout(d) vbox = QVBoxLayout(d)
h = QGridLayout() vbox.addWidget(QLabel(_('Enter Remote Node ID or connection string or invoice')))
local_nodeid = QLineEdit() local_nodeid = QLineEdit()
local_nodeid.setMinimumWidth(700)
local_nodeid.setText(bh2u(lnworker.node_keypair.pubkey)) local_nodeid.setText(bh2u(lnworker.node_keypair.pubkey))
local_nodeid.setReadOnly(True) local_nodeid.setReadOnly(True)
local_nodeid.setCursorPosition(0) local_nodeid.setCursorPosition(0)
remote_nodeid = QLineEdit() remote_nodeid = QLineEdit()
local_amt_inp = BTCAmountEdit(self.parent.get_decimal_point) remote_nodeid.setMinimumWidth(700)
local_amt_inp.setAmount(200000) amount_e = BTCAmountEdit(self.parent.get_decimal_point)
push_amt_inp = BTCAmountEdit(self.parent.get_decimal_point) # max button
push_amt_inp.setAmount(0) def spend_max():
make_tx = self.parent.mktx_for_open_channel('!')
tx = make_tx(None)
amount = tx.output_value()
amount = min(amount, LN_MAX_FUNDING_SAT)
amount_e.setAmount(amount)
amount_e.setFrozen(True)
max_button = EnterButton(_("Max"), spend_max)
max_button.setFixedWidth(100)
max_button.setCheckable(True)
h = QGridLayout()
h.addWidget(QLabel(_('Your Node ID')), 0, 0) h.addWidget(QLabel(_('Your Node ID')), 0, 0)
h.addWidget(local_nodeid, 0, 1) h.addWidget(local_nodeid, 0, 1)
h.addWidget(QLabel(_('Remote Node ID or connection string or invoice')), 1, 0) h.addWidget(QLabel(_('Remote Node ID')), 1, 0)
h.addWidget(remote_nodeid, 1, 1) h.addWidget(remote_nodeid, 1, 1)
h.addWidget(QLabel('Local amount'), 2, 0) h.addWidget(QLabel('Amount'), 2, 0)
h.addWidget(local_amt_inp, 2, 1) hbox = QHBoxLayout()
h.addWidget(QLabel('Push amount'), 3, 0) hbox.addWidget(amount_e)
h.addWidget(push_amt_inp, 3, 1) hbox.addWidget(max_button)
hbox.addStretch(1)
h.addLayout(hbox, 2, 1)
vbox.addLayout(h) vbox.addLayout(h)
ok_button = OkButton(d) ok_button = OkButton(d)
ok_button.setDefault(True) ok_button.setDefault(True)
@ -191,7 +203,6 @@ class ChannelsList(MyTreeView):
remote_nodeid.setCursorPosition(0) remote_nodeid.setCursorPosition(0)
if not d.exec_(): if not d.exec_():
return return
local_amt = local_amt_inp.get_amount() funding_sat = '!' if max_button.isChecked() else amount_e.get_amount()
push_amt = push_amt_inp.get_amount() connect_str = str(remote_nodeid.text()).strip()
connect_contents = str(remote_nodeid.text()).strip() self.parent.open_channel(connect_str, funding_sat, 0)
self.parent.open_channel(connect_contents, local_amt, push_amt)

30
electrum/gui/qt/confirm_tx_dialog.py

@ -51,18 +51,17 @@ if TYPE_CHECKING:
class TxEditor: class TxEditor:
def __init__(self, window: 'ElectrumWindow', inputs, outputs, external_keypairs): def __init__(self, window: 'ElectrumWindow', make_tx, output_value, is_sweep):
self.main_window = window self.main_window = window
self.outputs = outputs self.make_tx = make_tx
self.get_coins = inputs self.output_value = output_value
self.tx = None # type: Optional[Transaction] self.tx = None # type: Optional[Transaction]
self.config = window.config self.config = window.config
self.wallet = window.wallet self.wallet = window.wallet
self.external_keypairs = external_keypairs
self.not_enough_funds = False self.not_enough_funds = False
self.no_dynfee_estimates = False self.no_dynfee_estimates = False
self.needs_update = False self.needs_update = False
self.password_required = self.wallet.has_keystore_encryption() and not external_keypairs self.password_required = self.wallet.has_keystore_encryption() and not is_sweep
self.main_window.gui_object.timer.timeout.connect(self.timer_actions) self.main_window.gui_object.timer.timeout.connect(self.timer_actions)
def timer_actions(self): def timer_actions(self):
@ -86,17 +85,8 @@ class TxEditor:
def update_tx(self): def update_tx(self):
fee_estimator = self.get_fee_estimator() fee_estimator = self.get_fee_estimator()
is_sweep = bool(self.external_keypairs)
coins = self.get_coins()
# deepcopy outputs because '!' is converted to number
outputs = copy.deepcopy(self.outputs)
make_tx = lambda fee_est: self.wallet.make_unsigned_transaction(
coins=coins,
outputs=outputs,
fee=fee_est,
is_sweep=is_sweep)
try: try:
self.tx = make_tx(fee_estimator) self.tx = self.make_tx(fee_estimator)
self.not_enough_funds = False self.not_enough_funds = False
self.no_dynfee_estimates = False self.no_dynfee_estimates = False
except NotEnoughFunds: except NotEnoughFunds:
@ -107,7 +97,7 @@ class TxEditor:
self.no_dynfee_estimates = True self.no_dynfee_estimates = True
self.tx = None self.tx = None
try: try:
self.tx = make_tx(0) self.tx = self.make_tx(0)
except BaseException: except BaseException:
return return
except InternalAddressCorruption as e: except InternalAddressCorruption as e:
@ -131,9 +121,9 @@ class TxEditor:
class ConfirmTxDialog(TxEditor, WindowModalDialog): class ConfirmTxDialog(TxEditor, WindowModalDialog):
# set fee and return password (after pw check) # set fee and return password (after pw check)
def __init__(self, window: 'ElectrumWindow', inputs, outputs, external_keypairs): def __init__(self, window: 'ElectrumWindow', make_tx, output_value, is_sweep):
TxEditor.__init__(self, window, inputs, outputs, external_keypairs) TxEditor.__init__(self, window, make_tx, output_value, is_sweep)
WindowModalDialog.__init__(self, window, _("Confirm Transaction")) WindowModalDialog.__init__(self, window, _("Confirm Transaction"))
vbox = QVBoxLayout() vbox = QVBoxLayout()
self.setLayout(vbox) self.setLayout(vbox)
@ -218,9 +208,7 @@ class ConfirmTxDialog(TxEditor, WindowModalDialog):
def update(self): def update(self):
tx = self.tx tx = self.tx
output_values = [x.value for x in self.outputs] amount = tx.output_value() if self.output_value == '!' else self.output_value
is_max = '!' in output_values
amount = tx.output_value() if is_max else sum(output_values)
self.amount_label.setText(self.main_window.format_amount_and_units(amount)) self.amount_label.setText(self.main_window.format_amount_and_units(amount))
if self.not_enough_funds: if self.not_enough_funds:

61
electrum/gui/qt/main_window.py

@ -76,6 +76,7 @@ from electrum.simple_config import SimpleConfig
from electrum.logging import Logger from electrum.logging import Logger
from electrum.util import PR_PAID, PR_UNPAID, PR_INFLIGHT, PR_FAILED from electrum.util import PR_PAID, PR_UNPAID, PR_INFLIGHT, PR_FAILED
from electrum.util import pr_expiration_values from electrum.util import pr_expiration_values
from electrum.lnutil import ln_dummy_address
from .exception_window import Exception_Hook from .exception_window import Exception_Hook
from .amountedit import AmountEdit, BTCAmountEdit, MyLineEdit, FeerateEdit from .amountedit import AmountEdit, BTCAmountEdit, MyLineEdit, FeerateEdit
@ -1282,8 +1283,18 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
def spend_max(self): def spend_max(self):
if run_hook('abort_send', self): if run_hook('abort_send', self):
return return
outputs = self.payto_e.get_outputs(True)
if not outputs:
return
self.max_button.setChecked(True) self.max_button.setChecked(True)
amount = sum(x.value_sats() for x in self.get_coins()) make_tx = lambda fee_est: self.wallet.make_unsigned_transaction(
coins=self.get_coins(),
outputs=outputs,
fee=fee_est,
is_sweep=False)
tx = make_tx(None)
amount = tx.output_value()#sum(x.value_sats() for x in self.get_coins())
self.amount_e.setAmount(amount) self.amount_e.setAmount(amount)
## substract extra fee ## substract extra fee
#__, x_fee_amount = run_hook('get_tx_extra_fee', self.wallet, tx) or (None, 0) #__, x_fee_amount = run_hook('get_tx_extra_fee', self.wallet, tx) or (None, 0)
@ -1448,20 +1459,20 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
outputs = [] outputs = []
for invoice in invoices: for invoice in invoices:
outputs += invoice['outputs'] outputs += invoice['outputs']
self.pay_onchain_dialog(self.get_coins, outputs) self.pay_onchain_dialog(self.get_coins(), outputs)
def do_pay_invoice(self, invoice): def do_pay_invoice(self, invoice):
if invoice['type'] == PR_TYPE_LN: if invoice['type'] == PR_TYPE_LN:
self.pay_lightning_invoice(invoice['invoice']) self.pay_lightning_invoice(invoice['invoice'])
elif invoice['type'] == PR_TYPE_ONCHAIN: elif invoice['type'] == PR_TYPE_ONCHAIN:
outputs = invoice['outputs'] outputs = invoice['outputs']
self.pay_onchain_dialog(self.get_coins, outputs, invoice=invoice) self.pay_onchain_dialog(self.get_coins(), outputs, invoice=invoice)
else: else:
raise Exception('unknown invoice type') raise Exception('unknown invoice type')
def get_coins(self): def get_coins(self, nonlocal_only=False):
coins = self.get_manually_selected_coins() coins = self.get_manually_selected_coins()
return coins or self.wallet.get_spendable_coins(None) return coins or self.wallet.get_spendable_coins(None, nonlocal_only=nonlocal_only)
def get_manually_selected_coins(self) -> Sequence[PartialTxInput]: def get_manually_selected_coins(self) -> Sequence[PartialTxInput]:
return self.utxo_list.get_spend_list() return self.utxo_list.get_spend_list()
@ -1470,10 +1481,19 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
# trustedcoin requires this # trustedcoin requires this
if run_hook('abort_send', self): if run_hook('abort_send', self):
return return
is_sweep = bool(external_keypairs)
make_tx = lambda fee_est: self.wallet.make_unsigned_transaction(
coins=inputs,
outputs=outputs,
fee=fee_est,
is_sweep=is_sweep)
if self.config.get('advanced_preview'): if self.config.get('advanced_preview'):
self.preview_tx_dialog(inputs, outputs, invoice=invoice) self.preview_tx_dialog(make_tx, outputs, is_sweep=is_sweep, invoice=invoice)
return return
d = ConfirmTxDialog(self, inputs, outputs, external_keypairs)
output_values = [x.value for x in outputs]
output_value = '!' if '!' in output_values else sum(output_values)
d = ConfirmTxDialog(self, make_tx, output_value, is_sweep)
d.update_tx() d.update_tx()
if d.not_enough_funds: if d.not_enough_funds:
self.show_message(_('Not Enough Funds')) self.show_message(_('Not Enough Funds'))
@ -1487,10 +1507,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
self.broadcast_or_show(tx, invoice=invoice) self.broadcast_or_show(tx, invoice=invoice)
self.sign_tx_with_password(tx, sign_done, password, external_keypairs) self.sign_tx_with_password(tx, sign_done, password, external_keypairs)
else: else:
self.preview_tx_dialog(inputs, outputs, external_keypairs=external_keypairs, invoice=invoice) self.preview_tx_dialog(make_tx, outputs, is_sweep=is_sweep, invoice=invoice)
def preview_tx_dialog(self, inputs, outputs, external_keypairs=None, invoice=None): def preview_tx_dialog(self, make_tx, outputs, is_sweep=False, invoice=None):
d = PreviewTxDialog(inputs, outputs, external_keypairs, window=self, invoice=invoice) d = PreviewTxDialog(make_tx, outputs, is_sweep, window=self, invoice=invoice)
d.show() d.show()
def broadcast_or_show(self, tx, invoice=None): def broadcast_or_show(self, tx, invoice=None):
@ -1572,21 +1592,26 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
WaitingDialog(self, _('Broadcasting transaction...'), WaitingDialog(self, _('Broadcasting transaction...'),
broadcast_thread, broadcast_done, self.on_error) broadcast_thread, broadcast_done, self.on_error)
def open_channel(self, connect_str, local_amt, push_amt): def mktx_for_open_channel(self, funding_sat):
coins = self.get_coins(nonlocal_only=True)
make_tx = partial(self.wallet.lnworker.mktx_for_open_channel, coins, funding_sat)
return make_tx
def open_channel(self, connect_str, funding_sat, push_amt):
# use ConfirmTxDialog # use ConfirmTxDialog
# we need to know the fee before we broadcast, because the txid is required # we need to know the fee before we broadcast, because the txid is required
# however, the user must not be allowed to broadcast early # however, the user must not be allowed to broadcast early
funding_sat = local_amt + push_amt make_tx = self.mktx_for_open_channel(funding_sat)
inputs = self.get_coins d = ConfirmTxDialog(self, make_tx, funding_sat, False)
outputs = [PartialTxOutput.from_address_and_value(self.wallet.dummy_address(), funding_sat)] cancelled, is_send, password, funding_tx = d.run()
d = ConfirmTxDialog(self, inputs, outputs, None)
cancelled, is_send, password, tx = d.run()
if not is_send: if not is_send:
return return
if cancelled: if cancelled:
return return
# read funding_sat from tx; converts '!' to int value
funding_sat = funding_tx.output_value_for_address(ln_dummy_address())
def task(): def task():
return self.wallet.lnworker.open_channel(connect_str, local_amt, push_amt, password) return self.wallet.lnworker.open_channel(connect_str, funding_tx, funding_sat, push_amt, password)
def on_success(chan): def on_success(chan):
n = chan.constraints.funding_txn_minimum_depth n = chan.constraints.funding_txn_minimum_depth
message = '\n'.join([ message = '\n'.join([
@ -2647,7 +2672,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
scriptpubkey = bfh(bitcoin.address_to_script(addr)) scriptpubkey = bfh(bitcoin.address_to_script(addr))
outputs = [PartialTxOutput(scriptpubkey=scriptpubkey, value='!')] outputs = [PartialTxOutput(scriptpubkey=scriptpubkey, value='!')]
self.warn_if_watching_only() self.warn_if_watching_only()
self.pay_onchain_dialog(lambda: coins, outputs, invoice=None, external_keypairs=keypairs) self.pay_onchain_dialog(coins, outputs, invoice=None, external_keypairs=keypairs)
def _do_import(self, title, header_layout, func): def _do_import(self, title, header_layout, func):
text = text_dialog(self, title, header_layout, _('Import'), allow_multi=True) text = text_dialog(self, title, header_layout, _('Import'), allow_multi=True)

4
electrum/gui/qt/transaction_dialog.py

@ -602,8 +602,8 @@ class TxDialog(BaseTxDialog):
class PreviewTxDialog(BaseTxDialog, TxEditor): class PreviewTxDialog(BaseTxDialog, TxEditor):
def __init__(self, inputs, outputs, external_keypairs, *, window: 'ElectrumWindow', invoice): def __init__(self, make_tx, outputs, is_sweep, *, window: 'ElectrumWindow', invoice):
TxEditor.__init__(self, window, inputs, outputs, external_keypairs) TxEditor.__init__(self, window, make_tx, outputs, is_sweep)
BaseTxDialog.__init__(self, parent=window, invoice=invoice, desc='', prompt_if_unsaved=False, finalized=False) BaseTxDialog.__init__(self, parent=window, invoice=invoice, desc='', prompt_if_unsaved=False, finalized=False)
self.update_tx() self.update_tx()
self.update() self.update()

17
electrum/lnpeer.py

@ -46,10 +46,12 @@ from .lntransport import LNTransport, LNTransportBase
from .lnmsg import encode_msg, decode_msg from .lnmsg import encode_msg, decode_msg
from .interface import GracefulDisconnect, NetworkException from .interface import GracefulDisconnect, NetworkException
from .lnrouter import fee_for_edge_msat from .lnrouter import fee_for_edge_msat
from .lnutil import ln_dummy_address
if TYPE_CHECKING: if TYPE_CHECKING:
from .lnworker import LNWorker, LNGossip, LNWallet from .lnworker import LNWorker, LNGossip, LNWallet
from .lnrouter import RouteEdge from .lnrouter import RouteEdge
from .transaction import PartialTransaction
LN_P2P_NETWORK_TIMEOUT = 20 LN_P2P_NETWORK_TIMEOUT = 20
@ -479,12 +481,8 @@ class Peer(Logger):
return local_config return local_config
@log_exceptions @log_exceptions
async def channel_establishment_flow(self, password: Optional[str], funding_sat: int, async def channel_establishment_flow(self, password: Optional[str], funding_tx: 'PartialTransaction', funding_sat: int,
push_msat: int, temp_channel_id: bytes) -> Channel: push_msat: int, temp_channel_id: bytes) -> Channel:
wallet = self.lnworker.wallet
# dry run creating funding tx to see if we even have enough funds
funding_tx_test = wallet.mktx(outputs=[PartialTxOutput.from_address_and_value(wallet.dummy_address(), funding_sat)],
password=password, nonlocal_only=True)
await asyncio.wait_for(self.initialized.wait(), LN_P2P_NETWORK_TIMEOUT) await asyncio.wait_for(self.initialized.wait(), LN_P2P_NETWORK_TIMEOUT)
feerate = self.lnworker.current_feerate_per_kw() feerate = self.lnworker.current_feerate_per_kw()
local_config = self.make_local_config(funding_sat, push_msat, LOCAL) local_config = self.make_local_config(funding_sat, push_msat, LOCAL)
@ -555,16 +553,19 @@ class Peer(Logger):
initial_msat=push_msat, initial_msat=push_msat,
reserve_sat = remote_reserve_sat, reserve_sat = remote_reserve_sat,
htlc_minimum_msat = htlc_min, htlc_minimum_msat = htlc_min,
next_per_commitment_point=remote_per_commitment_point, next_per_commitment_point=remote_per_commitment_point,
current_per_commitment_point=None, current_per_commitment_point=None,
revocation_store=their_revocation_store, revocation_store=their_revocation_store,
) )
# create funding tx # replace dummy output in funding tx
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)
funding_tx = wallet.mktx(outputs=[funding_output], password=password, nonlocal_only=True) dummy_output = PartialTxOutput.from_address_and_value(ln_dummy_address(), funding_sat)
funding_tx.outputs().remove(dummy_output)
funding_tx.add_outputs([funding_output])
funding_tx.set_rbf(False)
self.lnworker.wallet.sign_transaction(funding_tx, password)
funding_txid = funding_tx.txid() funding_txid = funding_tx.txid()
funding_index = funding_tx.outputs().index(funding_output) funding_index = funding_tx.outputs().index(funding_output)
# remote commitment transaction # remote commitment transaction

5
electrum/lnutil.py

@ -27,6 +27,11 @@ if TYPE_CHECKING:
HTLC_TIMEOUT_WEIGHT = 663 HTLC_TIMEOUT_WEIGHT = 663
HTLC_SUCCESS_WEIGHT = 703 HTLC_SUCCESS_WEIGHT = 703
LN_MAX_FUNDING_SAT = pow(2, 24)
# dummy address for fee estimation of funding tx
def ln_dummy_address():
return redeem_script_to_address('p2wsh', '')
class Keypair(NamedTuple): class Keypair(NamedTuple):
pubkey: bytes pubkey: bytes

23
electrum/lnworker.py

@ -24,6 +24,7 @@ from . import keystore
from .util import profiler from .util import profiler
from .util import PR_UNPAID, PR_EXPIRED, PR_PAID, PR_INFLIGHT, PR_FAILED from .util import PR_UNPAID, PR_EXPIRED, PR_PAID, PR_INFLIGHT, PR_FAILED
from .util import PR_TYPE_LN from .util import PR_TYPE_LN
from .lnutil import LN_MAX_FUNDING_SAT
from .keystore import BIP32_KeyStore from .keystore import BIP32_KeyStore
from .bitcoin import COIN from .bitcoin import COIN
from .transaction import Transaction from .transaction import Transaction
@ -48,6 +49,8 @@ from .lnutil import (Outpoint, LNPeerAddr,
NUM_MAX_EDGES_IN_PAYMENT_PATH, SENT, RECEIVED, HTLCOwner, NUM_MAX_EDGES_IN_PAYMENT_PATH, SENT, RECEIVED, HTLCOwner,
UpdateAddHtlc, Direction, LnLocalFeatures, format_short_channel_id, UpdateAddHtlc, Direction, LnLocalFeatures, format_short_channel_id,
ShortChannelID) ShortChannelID)
from .lnutil import ln_dummy_address
from .transaction import PartialTxOutput
from .lnonion import OnionFailureCode from .lnonion import OnionFailureCode
from .lnmsg import decode_msg from .lnmsg import decode_msg
from .i18n import _ from .i18n import _
@ -768,13 +771,14 @@ class LNWallet(LNWorker):
await self.force_close_channel(chan.channel_id) await self.force_close_channel(chan.channel_id)
@log_exceptions @log_exceptions
async def _open_channel_coroutine(self, connect_str, local_amount_sat, push_sat, password): async def _open_channel_coroutine(self, connect_str, funding_tx, funding_sat, push_sat, password):
peer = await self.add_peer(connect_str) peer = await self.add_peer(connect_str)
# peer might just have been connected to # peer might just have been connected to
await asyncio.wait_for(peer.initialized.wait(), LN_P2P_NETWORK_TIMEOUT) await asyncio.wait_for(peer.initialized.wait(), LN_P2P_NETWORK_TIMEOUT)
chan = await peer.channel_establishment_flow( chan = await peer.channel_establishment_flow(
password, password,
funding_sat=local_amount_sat + push_sat, funding_tx=funding_tx,
funding_sat=funding_sat,
push_msat=push_sat * 1000, push_msat=push_sat * 1000,
temp_channel_id=os.urandom(32)) temp_channel_id=os.urandom(32))
self.save_channel(chan) self.save_channel(chan)
@ -805,8 +809,19 @@ class LNWallet(LNWorker):
peer = await self._add_peer(host, port, node_id) peer = await self._add_peer(host, port, node_id)
return peer return peer
def open_channel(self, connect_str, local_amt_sat, push_amt_sat, password=None, timeout=20): def mktx_for_open_channel(self, coins, funding_sat, fee_est):
coro = self._open_channel_coroutine(connect_str, local_amt_sat, push_amt_sat, password) dummy_address = ln_dummy_address()
outputs = [PartialTxOutput.from_address_and_value(dummy_address, funding_sat)]
tx = self.wallet.make_unsigned_transaction(
coins=coins,
outputs=outputs,
fee=fee_est)
tx.set_rbf(False)
return tx
def open_channel(self, connect_str, funding_tx, funding_sat, push_amt_sat, password=None, timeout=20):
assert funding_sat <= LN_MAX_FUNDING_SAT
coro = self._open_channel_coroutine(connect_str, funding_tx, funding_sat, push_amt_sat, password)
fut = asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop) fut = asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop)
try: try:
chan = fut.result(timeout=timeout) chan = fut.result(timeout=timeout)

4
electrum/tests/regtest/regtest.sh

@ -109,8 +109,8 @@ fi
if [[ $1 == "open" ]]; then if [[ $1 == "open" ]]; then
bob_node=$($bob nodeid) bob_node=$($bob nodeid)
channel_id1=$($alice open_channel $bob_node 0.001 --channel_push 0.001) channel_id1=$($alice open_channel $bob_node 0.002 --push_amount 0.001)
channel_id2=$($carol open_channel $bob_node 0.001 --channel_push 0.001) channel_id2=$($carol open_channel $bob_node 0.002 --push_amount 0.001)
echo "mining 3 blocks" echo "mining 3 blocks"
new_blocks 3 new_blocks 3
sleep 10 # time for channelDB sleep 10 # time for channelDB

8
electrum/transaction.py

@ -879,6 +879,14 @@ 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 output_value_for_address(self, addr):
# assumes exactly one output has that address
for o in self.outputs():
if o.address == addr:
return o.value
else:
raise Exception('output not found', addr)
def convert_raw_tx_to_hex(raw: Union[str, bytes]) -> str: def convert_raw_tx_to_hex(raw: Union[str, bytes]) -> str:
"""Sanitizes tx-describing input (hex/base43/base64) into """Sanitizes tx-describing input (hex/base43/base64) into

4
electrum/wallet.py

@ -917,6 +917,10 @@ class Abstract_Wallet(AddressSynchronizer):
def make_unsigned_transaction(self, *, coins: Sequence[PartialTxInput], def make_unsigned_transaction(self, *, coins: Sequence[PartialTxInput],
outputs: List[PartialTxOutput], fee=None, outputs: List[PartialTxOutput], fee=None,
change_addr: str = None, is_sweep=False) -> PartialTransaction: change_addr: str = None, is_sweep=False) -> PartialTransaction:
# prevent side-effect with '!'
outputs = copy.deepcopy(outputs)
# check outputs # check outputs
i_max = None i_max = None
for i, o in enumerate(outputs): for i, o in enumerate(outputs):

Loading…
Cancel
Save