electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit 78813dcb7de97358f395c3a937f8312223ebc91c
parent 970bd4e95f4a9a6c27850d35d93af699eed5e06c
Author: ThomasV <thomasv@electrum.org>
Date:   Wed, 13 Nov 2019 09:20:19 +0100

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

Diffstat:
Melectrum/commands.py | 11++++++++---
Melectrum/gui/qt/channels_list.py | 47+++++++++++++++++++++++++++++------------------
Melectrum/gui/qt/confirm_tx_dialog.py | 30+++++++++---------------------
Melectrum/gui/qt/main_window.py | 61+++++++++++++++++++++++++++++++++++++++++++------------------
Melectrum/gui/qt/transaction_dialog.py | 4++--
Melectrum/lnpeer.py | 17+++++++++--------
Melectrum/lnutil.py | 5+++++
Melectrum/lnworker.py | 23+++++++++++++++++++----
Melectrum/tests/regtest/regtest.sh | 4++--
Melectrum/transaction.py | 8++++++++
Melectrum/wallet.py | 4++++
11 files changed, 138 insertions(+), 76 deletions(-)

diff --git a/electrum/commands.py b/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"), diff --git a/electrum/gui/qt/channels_list.py b/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) diff --git a/electrum/gui/qt/confirm_tx_dialog.py b/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: diff --git a/electrum/gui/qt/main_window.py b/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) diff --git a/electrum/gui/qt/transaction_dialog.py b/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() diff --git a/electrum/lnpeer.py b/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 diff --git a/electrum/lnutil.py b/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 diff --git a/electrum/lnworker.py b/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) diff --git a/electrum/tests/regtest/regtest.sh b/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 diff --git a/electrum/transaction.py b/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 diff --git a/electrum/wallet.py b/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):