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 .mnemonic import Mnemonic
from .lnutil import SENT, RECEIVED
from .lnutil import ln_dummy_address
from .lnpeer import channel_id_from_funding_tx
from .plugin import run_hook
from .version import ELECTRUM_VERSION
@ -922,8 +923,12 @@ class Commands:
return True
@command('wpn')
async def open_channel(self, connection_string, amount, channel_push=0, password=None, wallet: Abstract_Wallet = None):
chan = await wallet.lnworker._open_channel_coroutine(connection_string, satoshis(amount), satoshis(channel_push), password)
async def open_channel(self, connection_string, amount, push_amount=0, password=None, wallet: Abstract_Wallet = None):
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()
@command('wn')
@ -1037,7 +1042,7 @@ command_options = {
'timeout': (None, "Timeout in seconds"),
'force': (None, "Create new address beyond gap limit, if no more addresses are available."),
'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."),
'paid': (None, "Show only paid requests."),
'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.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.i18n import _
from electrum.lnchannel import Channel
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 .channel_details import ChannelDetailsDialog
@ -162,26 +162,38 @@ class ChannelsList(MyTreeView):
def new_channel_dialog(self):
lnworker = self.parent.wallet.lnworker
d = WindowModalDialog(self.parent, _('Open Channel'))
d.setMinimumWidth(700)
vbox = QVBoxLayout(d)
h = QGridLayout()
vbox.addWidget(QLabel(_('Enter Remote Node ID or connection string or invoice')))
local_nodeid = QLineEdit()
local_nodeid.setMinimumWidth(700)
local_nodeid.setText(bh2u(lnworker.node_keypair.pubkey))
local_nodeid.setReadOnly(True)
local_nodeid.setCursorPosition(0)
remote_nodeid = QLineEdit()
local_amt_inp = BTCAmountEdit(self.parent.get_decimal_point)
local_amt_inp.setAmount(200000)
push_amt_inp = BTCAmountEdit(self.parent.get_decimal_point)
push_amt_inp.setAmount(0)
remote_nodeid.setMinimumWidth(700)
amount_e = BTCAmountEdit(self.parent.get_decimal_point)
# max button
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(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(QLabel('Local amount'), 2, 0)
h.addWidget(local_amt_inp, 2, 1)
h.addWidget(QLabel('Push amount'), 3, 0)
h.addWidget(push_amt_inp, 3, 1)
h.addWidget(QLabel('Amount'), 2, 0)
hbox = QHBoxLayout()
hbox.addWidget(amount_e)
hbox.addWidget(max_button)
hbox.addStretch(1)
h.addLayout(hbox, 2, 1)
vbox.addLayout(h)
ok_button = OkButton(d)
ok_button.setDefault(True)
@ -191,7 +203,6 @@ class ChannelsList(MyTreeView):
remote_nodeid.setCursorPosition(0)
if not d.exec_():
return
local_amt = local_amt_inp.get_amount()
push_amt = push_amt_inp.get_amount()
connect_contents = str(remote_nodeid.text()).strip()
self.parent.open_channel(connect_contents, local_amt, push_amt)
funding_sat = '!' if max_button.isChecked() else amount_e.get_amount()
connect_str = str(remote_nodeid.text()).strip()
self.parent.open_channel(connect_str, funding_sat, 0)

30
electrum/gui/qt/confirm_tx_dialog.py

@ -51,18 +51,17 @@ if TYPE_CHECKING:
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.outputs = outputs
self.get_coins = inputs
self.make_tx = make_tx
self.output_value = output_value
self.tx = None # type: Optional[Transaction]
self.config = window.config
self.wallet = window.wallet
self.external_keypairs = external_keypairs
self.not_enough_funds = False
self.no_dynfee_estimates = 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)
def timer_actions(self):
@ -86,17 +85,8 @@ class TxEditor:
def update_tx(self):
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:
self.tx = make_tx(fee_estimator)
self.tx = self.make_tx(fee_estimator)
self.not_enough_funds = False
self.no_dynfee_estimates = False
except NotEnoughFunds:
@ -107,7 +97,7 @@ class TxEditor:
self.no_dynfee_estimates = True
self.tx = None
try:
self.tx = make_tx(0)
self.tx = self.make_tx(0)
except BaseException:
return
except InternalAddressCorruption as e:
@ -131,9 +121,9 @@ class TxEditor:
class ConfirmTxDialog(TxEditor, WindowModalDialog):
# 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"))
vbox = QVBoxLayout()
self.setLayout(vbox)
@ -218,9 +208,7 @@ class ConfirmTxDialog(TxEditor, WindowModalDialog):
def update(self):
tx = self.tx
output_values = [x.value for x in self.outputs]
is_max = '!' in output_values
amount = tx.output_value() if is_max else sum(output_values)
amount = tx.output_value() if self.output_value == '!' else self.output_value
self.amount_label.setText(self.main_window.format_amount_and_units(amount))
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.util import PR_PAID, PR_UNPAID, PR_INFLIGHT, PR_FAILED
from electrum.util import pr_expiration_values
from electrum.lnutil import ln_dummy_address
from .exception_window import Exception_Hook
from .amountedit import AmountEdit, BTCAmountEdit, MyLineEdit, FeerateEdit
@ -1282,8 +1283,18 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
def spend_max(self):
if run_hook('abort_send', self):
return
outputs = self.payto_e.get_outputs(True)
if not outputs:
return
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)
## substract extra fee
#__, 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 = []
for invoice in invoices:
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):
if invoice['type'] == PR_TYPE_LN:
self.pay_lightning_invoice(invoice['invoice'])
elif invoice['type'] == PR_TYPE_ONCHAIN:
outputs = invoice['outputs']
self.pay_onchain_dialog(self.get_coins, outputs, invoice=invoice)
self.pay_onchain_dialog(self.get_coins(), outputs, invoice=invoice)
else:
raise Exception('unknown invoice type')
def get_coins(self):
def get_coins(self, nonlocal_only=False):
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]:
return self.utxo_list.get_spend_list()
@ -1470,10 +1481,19 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
# trustedcoin requires this
if run_hook('abort_send', self):
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'):
self.preview_tx_dialog(inputs, outputs, invoice=invoice)
self.preview_tx_dialog(make_tx, outputs, is_sweep=is_sweep, invoice=invoice)
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()
if d.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.sign_tx_with_password(tx, sign_done, password, external_keypairs)
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):
d = PreviewTxDialog(inputs, outputs, external_keypairs, window=self, invoice=invoice)
def preview_tx_dialog(self, make_tx, outputs, is_sweep=False, invoice=None):
d = PreviewTxDialog(make_tx, outputs, is_sweep, window=self, invoice=invoice)
d.show()
def broadcast_or_show(self, tx, invoice=None):
@ -1572,21 +1592,26 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
WaitingDialog(self, _('Broadcasting transaction...'),
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
# we need to know the fee before we broadcast, because the txid is required
# however, the user must not be allowed to broadcast early
funding_sat = local_amt + push_amt
inputs = self.get_coins
outputs = [PartialTxOutput.from_address_and_value(self.wallet.dummy_address(), funding_sat)]
d = ConfirmTxDialog(self, inputs, outputs, None)
cancelled, is_send, password, tx = d.run()
make_tx = self.mktx_for_open_channel(funding_sat)
d = ConfirmTxDialog(self, make_tx, funding_sat, False)
cancelled, is_send, password, funding_tx = d.run()
if not is_send:
return
if cancelled:
return
# read funding_sat from tx; converts '!' to int value
funding_sat = funding_tx.output_value_for_address(ln_dummy_address())
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):
n = chan.constraints.funding_txn_minimum_depth
message = '\n'.join([
@ -2647,7 +2672,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, Logger):
scriptpubkey = bfh(bitcoin.address_to_script(addr))
outputs = [PartialTxOutput(scriptpubkey=scriptpubkey, value='!')]
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):
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):
def __init__(self, inputs, outputs, external_keypairs, *, window: 'ElectrumWindow', invoice):
TxEditor.__init__(self, window, inputs, outputs, external_keypairs)
def __init__(self, make_tx, outputs, is_sweep, *, window: 'ElectrumWindow', invoice):
TxEditor.__init__(self, window, make_tx, outputs, is_sweep)
BaseTxDialog.__init__(self, parent=window, invoice=invoice, desc='', prompt_if_unsaved=False, finalized=False)
self.update_tx()
self.update()

17
electrum/lnpeer.py

@ -46,10 +46,12 @@ from .lntransport import LNTransport, LNTransportBase
from .lnmsg import encode_msg, decode_msg
from .interface import GracefulDisconnect, NetworkException
from .lnrouter import fee_for_edge_msat
from .lnutil import ln_dummy_address
if TYPE_CHECKING:
from .lnworker import LNWorker, LNGossip, LNWallet
from .lnrouter import RouteEdge
from .transaction import PartialTransaction
LN_P2P_NETWORK_TIMEOUT = 20
@ -479,12 +481,8 @@ class Peer(Logger):
return local_config
@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:
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)
feerate = self.lnworker.current_feerate_per_kw()
local_config = self.make_local_config(funding_sat, push_msat, LOCAL)
@ -555,16 +553,19 @@ class Peer(Logger):
initial_msat=push_msat,
reserve_sat = remote_reserve_sat,
htlc_minimum_msat = htlc_min,
next_per_commitment_point=remote_per_commitment_point,
current_per_commitment_point=None,
revocation_store=their_revocation_store,
)
# create funding tx
# replace dummy output in funding tx
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)
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_index = funding_tx.outputs().index(funding_output)
# remote commitment transaction

5
electrum/lnutil.py

@ -27,6 +27,11 @@ if TYPE_CHECKING:
HTLC_TIMEOUT_WEIGHT = 663
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):
pubkey: bytes

23
electrum/lnworker.py

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

4
electrum/tests/regtest/regtest.sh

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

8
electrum/transaction.py

@ -879,6 +879,14 @@ class Transaction:
script = bitcoin.address_to_script(addr)
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:
"""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],
outputs: List[PartialTxOutput], fee=None,
change_addr: str = None, is_sweep=False) -> PartialTransaction:
# prevent side-effect with '!'
outputs = copy.deepcopy(outputs)
# check outputs
i_max = None
for i, o in enumerate(outputs):

Loading…
Cancel
Save