Browse Source

if possible, batch new transaction with existing rbf transaction

3.3.3.1
ThomasV 6 years ago
parent
commit
2b8d801b36
  1. 6
      electrum/address_synchronizer.py
  2. 8
      electrum/coinchooser.py
  3. 2
      electrum/gui/qt/transaction_dialog.py
  4. 13
      electrum/transaction.py
  5. 55
      electrum/wallet.py

6
electrum/address_synchronizer.py

@ -484,6 +484,12 @@ class AddressSynchronizer(PrintError):
self.threadlocal_cache.local_height = orig_val self.threadlocal_cache.local_height = orig_val
return f return f
def get_unconfirmed_tx(self):
for tx_hash, tx_mined_status, delta, balance in self.get_history():
if tx_mined_status.conf <= 0 and delta < 0:
tx = self.transactions.get(tx_hash)
return tx
@with_local_height_cached @with_local_height_cached
def get_history(self, domain=None): def get_history(self, domain=None):
# get domain # get domain

8
electrum/coinchooser.py

@ -187,7 +187,7 @@ class CoinChooserBase(PrintError):
self.print_error('not keeping dust', dust) self.print_error('not keeping dust', dust)
return change return change
def make_tx(self, coins, outputs, change_addrs, fee_estimator, def make_tx(self, coins, inputs, outputs, change_addrs, fee_estimator,
dust_threshold): dust_threshold):
"""Select unspent coins to spend to pay outputs. If the change is """Select unspent coins to spend to pay outputs. If the change is
greater than dust_threshold (after adding the change output to greater than dust_threshold (after adding the change output to
@ -202,7 +202,9 @@ class CoinChooserBase(PrintError):
self.p = PRNG(''.join(sorted(utxos))) self.p = PRNG(''.join(sorted(utxos)))
# Copy the outputs so when adding change we don't modify "outputs" # Copy the outputs so when adding change we don't modify "outputs"
tx = Transaction.from_io([], outputs[:]) tx = Transaction.from_io(inputs[:], outputs[:])
v = tx.input_value()
# Weight of the transaction with no inputs and no change # Weight of the transaction with no inputs and no change
# Note: this will use legacy tx serialization as the need for "segwit" # Note: this will use legacy tx serialization as the need for "segwit"
# would be detected from inputs. The only side effect should be that the # would be detected from inputs. The only side effect should be that the
@ -230,7 +232,7 @@ class CoinChooserBase(PrintError):
def sufficient_funds(buckets): def sufficient_funds(buckets):
'''Given a list of buckets, return True if it has enough '''Given a list of buckets, return True if it has enough
value to pay for the transaction''' value to pay for the transaction'''
total_input = sum(bucket.value for bucket in buckets) total_input = v + sum(bucket.value for bucket in buckets)
total_weight = get_tx_weight(buckets) total_weight = get_tx_weight(buckets)
return total_input >= spent_amount + fee_estimator_w(total_weight) return total_input >= spent_amount + fee_estimator_w(total_weight)

2
electrum/gui/qt/transaction_dialog.py

@ -87,7 +87,7 @@ class TxDialog(QDialog, MessageBoxMixin):
# if the wallet can populate the inputs with more info, do it now. # if the wallet can populate the inputs with more info, do it now.
# as a result, e.g. we might learn an imported address tx is segwit, # as a result, e.g. we might learn an imported address tx is segwit,
# in which case it's ok to display txid # in which case it's ok to display txid
self.wallet.add_input_info_to_all_inputs(tx) tx.add_inputs_info(self.wallet)
self.setMinimumWidth(950) self.setMinimumWidth(950)
self.setWindowTitle(_("Transaction")) self.setWindowTitle(_("Transaction"))

13
electrum/transaction.py

@ -763,6 +763,17 @@ class Transaction:
txin['witness'] = None # force re-serialization txin['witness'] = None # force re-serialization
self.raw = None self.raw = None
def add_inputs_info(self, wallet):
if self.is_complete():
return
for txin in self.inputs():
wallet.add_input_info(txin)
def remove_signatures(self):
for txin in self.inputs():
txin['signatures'] = [None] * len(txin['signatures'])
assert not self.is_complete()
def deserialize(self, force_full_parse=False): def deserialize(self, force_full_parse=False):
if self.raw is None: if self.raw is None:
return return
@ -1199,8 +1210,6 @@ class Transaction:
return s, r return s, r
def is_complete(self): def is_complete(self):
if not self.is_partial_originally:
return True
s, r = self.signature_count() s, r = self.signature_count()
return r == s return r == s

55
electrum/wallet.py

@ -536,7 +536,7 @@ class Abstract_Wallet(AddressSynchronizer):
def dust_threshold(self): def dust_threshold(self):
return dust_threshold(self.network) return dust_threshold(self.network)
def make_unsigned_transaction(self, inputs, outputs, config, fixed_fee=None, def make_unsigned_transaction(self, coins, outputs, config, fixed_fee=None,
change_addr=None, is_sweep=False): change_addr=None, is_sweep=False):
# check outputs # check outputs
i_max = None i_max = None
@ -550,13 +550,13 @@ class Abstract_Wallet(AddressSynchronizer):
i_max = i i_max = i
# Avoid index-out-of-range with inputs[0] below # Avoid index-out-of-range with inputs[0] below
if not inputs: if not coins:
raise NotEnoughFunds() raise NotEnoughFunds()
if fixed_fee is None and config.fee_per_kb() is None: if fixed_fee is None and config.fee_per_kb() is None:
raise NoDynamicFeeEstimates() raise NoDynamicFeeEstimates()
for item in inputs: for item in coins:
self.add_input_info(item) self.add_input_info(item)
# change address # change address
@ -591,19 +591,34 @@ class Abstract_Wallet(AddressSynchronizer):
# Let the coin chooser select the coins to spend # Let the coin chooser select the coins to spend
max_change = self.max_change_outputs if self.multiple_change else 1 max_change = self.max_change_outputs if self.multiple_change else 1
coin_chooser = coinchooser.get_coin_chooser(config) coin_chooser = coinchooser.get_coin_chooser(config)
tx = coin_chooser.make_tx(inputs, outputs, change_addrs[:max_change], # If there is an unconfirmed RBF tx, merge with it
base_tx = self.get_unconfirmed_tx()
if base_tx and not base_tx.is_final():
base_tx = Transaction(base_tx.serialize())
base_tx.deserialize(force_full_parse=True)
base_tx.remove_signatures()
base_tx.add_inputs_info(self)
base_fee = base_tx.get_fee()
fee_per_byte = Decimal(base_fee) / base_tx.estimated_size()
fee_estimator = lambda size: base_fee + round(fee_per_byte * size)
txi = base_tx.inputs()
txo = list(filter(lambda x: not self.is_change(x[1]), base_tx.outputs()))
else:
txi = []
txo = []
tx = coin_chooser.make_tx(coins, txi, outputs[:] + txo, change_addrs[:max_change],
fee_estimator, self.dust_threshold()) fee_estimator, self.dust_threshold())
else: else:
# FIXME?? this might spend inputs with negative effective value... # FIXME?? this might spend inputs with negative effective value...
sendable = sum(map(lambda x:x['value'], inputs)) sendable = sum(map(lambda x:x['value'], coins))
outputs[i_max] = outputs[i_max]._replace(value=0) outputs[i_max] = outputs[i_max]._replace(value=0)
tx = Transaction.from_io(inputs, outputs[:]) tx = Transaction.from_io(coins, outputs[:])
fee = fee_estimator(tx.estimated_size()) fee = fee_estimator(tx.estimated_size())
amount = sendable - tx.output_value() - fee amount = sendable - tx.output_value() - fee
if amount < 0: if amount < 0:
raise NotEnoughFunds() raise NotEnoughFunds()
outputs[i_max] = outputs[i_max]._replace(value=amount) outputs[i_max] = outputs[i_max]._replace(value=amount)
tx = Transaction.from_io(inputs, outputs[:]) tx = Transaction.from_io(coins, outputs[:])
# Timelock tx to current height. # Timelock tx to current height.
tx.locktime = self.get_local_height() tx.locktime = self.get_local_height()
@ -679,11 +694,10 @@ class Abstract_Wallet(AddressSynchronizer):
raise CannotBumpFee(_('Cannot bump fee') + ': ' + _('transaction is final')) raise CannotBumpFee(_('Cannot bump fee') + ': ' + _('transaction is final'))
tx = Transaction(tx.serialize()) tx = Transaction(tx.serialize())
tx.deserialize(force_full_parse=True) # need to parse inputs tx.deserialize(force_full_parse=True) # need to parse inputs
inputs = copy.deepcopy(tx.inputs()) tx.remove_signatures()
outputs = copy.deepcopy(tx.outputs()) tx.add_inputs_info(self)
for txin in inputs: inputs = tx.inputs()
txin['signatures'] = [None] * len(txin['signatures']) outputs = tx.outputs()
self.add_input_info(txin)
# use own outputs # use own outputs
s = list(filter(lambda x: self.is_mine(x[1]), outputs)) s = list(filter(lambda x: self.is_mine(x[1]), outputs))
# ... unless there is none # ... unless there is none
@ -738,26 +752,21 @@ class Abstract_Wallet(AddressSynchronizer):
def add_input_info(self, txin): def add_input_info(self, txin):
address = self.get_txin_address(txin) address = self.get_txin_address(txin)
if self.is_mine(address): if self.is_mine(address):
txin['address'] = address
txin['type'] = self.get_txin_type(address) txin['type'] = self.get_txin_type(address)
# segwit needs value to sign # segwit needs value to sign
if txin.get('value') is None and Transaction.is_input_value_needed(txin): if txin.get('value') is None:
received, spent = self.get_addr_io(address) received, spent = self.get_addr_io(address)
item = received.get(txin['prevout_hash']+':%d'%txin['prevout_n']) item = received.get(txin['prevout_hash']+':%d'%txin['prevout_n'])
tx_height, value, is_cb = item if item:
txin['value'] = value txin['value'] = item[1]
self.add_input_sig_info(txin, address) self.add_input_sig_info(txin, address)
def add_input_info_to_all_inputs(self, tx):
if tx.is_complete():
return
for txin in tx.inputs():
self.add_input_info(txin)
def can_sign(self, tx): def can_sign(self, tx):
if tx.is_complete(): if tx.is_complete():
return False return False
# add info to inputs if we can; otherwise we might return a false negative: # add info to inputs if we can; otherwise we might return a false negative:
self.add_input_info_to_all_inputs(tx) # though note that this is a side-effect tx.add_inputs_info(self)
for k in self.get_keystores(): for k in self.get_keystores():
if k.can_sign(tx): if k.can_sign(tx):
return True return True
@ -804,7 +813,7 @@ class Abstract_Wallet(AddressSynchronizer):
def sign_transaction(self, tx, password): def sign_transaction(self, tx, password):
if self.is_watching_only(): if self.is_watching_only():
return return
self.add_input_info_to_all_inputs(tx) tx.add_inputs_info(self)
# hardware wallets require extra info # hardware wallets require extra info
if any([(isinstance(k, Hardware_KeyStore) and k.can_sign(tx)) for k in self.get_keystores()]): if any([(isinstance(k, Hardware_KeyStore) and k.can_sign(tx)) for k in self.get_keystores()]):
self.add_hw_info(tx) self.add_hw_info(tx)

Loading…
Cancel
Save