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:
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):