mirror of
https://github.com/LBRYFoundation/LBRY-Vault.git
synced 2025-08-23 17:47:31 +00:00
if possible, batch new transaction with existing rbf transaction
This commit is contained in:
parent
bd32b88f62
commit
2b8d801b36
5 changed files with 55 additions and 29 deletions
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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"))
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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…
Add table
Reference in a new issue