Skip to content

Commit

Permalink
Add bumpfee method to wallet
Browse files Browse the repository at this point in the history
  • Loading branch information
Cryp Toon committed May 7, 2024
1 parent 3e49628 commit 19ca898
Show file tree
Hide file tree
Showing 5 changed files with 175 additions and 12 deletions.
1 change: 1 addition & 0 deletions bitcoinlib/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,7 @@ class DbTransactionOutput(Base):
spent = Column(Boolean, default=False, doc="Indicated if output is already spent in another transaction")
spending_txid = Column(LargeBinary(33), doc="Transaction hash of input which spends this output")
spending_index_n = Column(Integer, doc="Index number of transaction input which spends this output")
is_change = Column(Boolean, default=False, doc="Is this a change output / output to own wallet?")

__table_args__ = (UniqueConstraint('transaction_id', 'output_n', name='constraint_transaction_output_unique'),)

Expand Down
28 changes: 23 additions & 5 deletions bitcoinlib/transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ def __init__(self, prev_txid, output_n, keys=None, signatures=None, public_hash=
:param output_n: Output number in previous transaction.
:type output_n: bytes, int
:param keys: A list of Key objects or public / private key string in various formats. If no list is provided but a bytes or string variable, a list with one item will be created. Optional
:type keys: list (bytes, str, Key)
:type keys: list (bytes, str, Key, HDKey)
:param signatures: Specify optional signatures
:type signatures: list (bytes, str, Signature)
:param public_hash: Public key hash or script hash. Specify if key is not available
Expand Down Expand Up @@ -1822,15 +1822,15 @@ def add_input(self, prev_txid, output_n, keys=None, signatures=None, public_hash
:param output_n: Output number in previous transaction.
:type output_n: bytes, int
:param keys: Public keys can be provided to construct an Unlocking script. Optional
:type keys: bytes, str
:type keys: list (bytes, str, Key, HDKey)
:param signatures: Add signatures to input if already known
:type signatures: bytes, str
:param public_hash: Specify public hash from key or redeemscript if key is not available
:type public_hash: bytes
:param unlocking_script: Unlocking script (scriptSig) to prove ownership. Optional
:type unlocking_script: bytes, hexstring
:param locking_script: TODO: find better name...
:type locking_script: bytes, str
:param locking_script: Locking script (scriptPubKey) of previous output if known
:type locking_script: bytes, hexstring
:param script_type: Type of unlocking script used, i.e. p2pkh or p2sh_multisig. Default is p2pkh
:type script_type: str
:param address: Specify address of input if known, default is to derive from key or scripts
Expand Down Expand Up @@ -2147,6 +2147,23 @@ def shuffle(self):
self.shuffle_outputs()

def bumpfee(self, fee=0, extra_fee=0):
"""
Increase fee for this transaction. If replace-by-fee is signaled in this transaction the fee can be
increased to speed up inclusion on the blockchain.
If not fee or extra_fee is provided the extra fee will be increased by the formule you can find in the code
below using the BUMPFEE_DEFAULT_MULTIPLIER from the config settings.
The extra fee will be deducted from change output. This method fails if there are not enough change outputs
to cover fees.
:param fee: New fee for this transaction
:type fee: int
:param extra_fee: Extra fee to add to current transaction fee
:type extra_fee: int
:return None:
"""
if not self.fee:
raise TransactionError("Current transaction fee is zero, cannot increase fee")
if not self.vsize:
Expand All @@ -2162,7 +2179,8 @@ def bumpfee(self, fee=0, extra_fee=0):
raise TransactionError("Extra fee cannot be less than minimal required fee")
fee = self.fee + extra_fee
else:
fee = self.fee + (minimal_required_fee * BUMPFEE_DEFAULT_MULTIPLIER)
fee = int(self.fee * (1.03 ** BUMPFEE_DEFAULT_MULTIPLIER) +
(minimal_required_fee * BUMPFEE_DEFAULT_MULTIPLIER))
extra_fee = fee - self.fee

remaining_fee = extra_fee
Expand Down
91 changes: 87 additions & 4 deletions bitcoinlib/wallets.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
from bitcoinlib.networks import Network
from bitcoinlib.values import Value, value_to_satoshi
from bitcoinlib.services.services import Service
from bitcoinlib.transactions import Input, Output, Transaction, get_unlocking_script_type
from bitcoinlib.transactions import Input, Output, Transaction, get_unlocking_script_type, TransactionError
from bitcoinlib.scripts import Script
from sqlalchemy import func, or_

Expand Down Expand Up @@ -753,7 +753,7 @@ def from_txid(cls, hdwallet, txid):
public_key = key.key().public_hex
outputs.append(Output(value=out.value, address=address, public_key=public_key,
lock_script=out.script, spent=out.spent, output_n=out.output_n,
script_type=out.script_type, network=network))
script_type=out.script_type, network=network, change=out.is_change))

return cls(hdwallet=hdwallet, inputs=inputs, outputs=outputs, locktime=db_tx.locktime,
version=db_tx.version, network=network, fee=db_tx.fee, fee_per_kb=fee_per_kb,
Expand Down Expand Up @@ -949,7 +949,7 @@ def store(self):
if not tx_output:
new_tx_item = DbTransactionOutput(
transaction_id=txidn, output_n=to.output_n, key_id=key_id, address=to.address, value=to.value,
spent=spent, script=to.lock_script, script_type=to.script_type)
spent=spent, script=to.lock_script, script_type=to.script_type, is_change=to.change)
sess.add(new_tx_item)
elif key_id:
tx_output.key_id = key_id
Expand Down Expand Up @@ -1040,7 +1040,10 @@ def delete(self):
filter(DbTransaction.txid == inp.prev_txid, DbTransactionOutput.output_n == inp.output_n,
DbTransactionOutput.spent.is_(True), DbTransaction.wallet_id == self.hdwallet.wallet_id).all()
for u in prev_utxos:
u.spent = False
# Check if output is spent in another transaction
if session.query(DbTransactionInput).filter(DbTransactionInput.transaction_id ==
inp.transaction_id).first():
u.spent = False
session.query(DbTransactionInput).filter_by(transaction_id=tx.id).delete()
qr = session.query(DbKey).filter_by(latest_txid=txid)
qr.update({DbKey.latest_txid: None, DbKey.used: False})
Expand All @@ -1051,6 +1054,80 @@ def delete(self):
self.hdwallet._commit()
return res

def bumpfee(self, fee=0, extra_fee=0, broadcast=False):
"""
Increase fee for this transaction. If replace-by-fee is signaled in this transaction the fee can be
increased to speed up inclusion on the blockchain.
If not fee or extra_fee is provided the extra fee will be increased by the formule you can find in the code
below using the BUMPFEE_DEFAULT_MULTIPLIER from the config settings.
The extra fee will be deducted from change output. This method fails if there are not enough change outputs
to cover fees.
If this transaction does not have enough inputs to cover extra fee, an extra wallet utxo will be aaded to
inputs if available.
Previous broadcasted transaction will be removed from wallet with this replace-by-fee transaction and wallet
information updated.
:param fee: New fee for this transaction
:type fee: int
:param extra_fee: Extra fee to add to current transaction fee
:type extra_fee: int
:param broadcast: Increase fee and directly broadcast transaction to the network
:type broadcast: bool
:return None:
"""
fees_not_provided = not (fee or extra_fee)
old_txid = self.txid
try:
super(WalletTransaction, self).bumpfee(fee, extra_fee)
except TransactionError as e:
if str(e) != "Not enough unspent outputs to bump transaction fee":
raise TransactionError(str(e))
else:
# Add extra input to cover fee
if fees_not_provided:
extra_fee = int(self.fee * (0.03 ** BUMPFEE_DEFAULT_MULTIPLIER) +
(self.vsize * BUMPFEE_DEFAULT_MULTIPLIER))
new_inp = self.add_input_from_wallet(amount_min=extra_fee)
# Add value of extra input to change output
change_outputs = [o for o in self.outputs if o.change]
if change_outputs:
change_outputs[0].value += self.inputs[new_inp].value
else:
self.add_output(self.inputs[new_inp].value, self.hdwallet.get_key().address, change=True)
if fees_not_provided:
extra_fee += 25 * BUMPFEE_DEFAULT_MULTIPLIER
super(WalletTransaction, self).bumpfee(fee, extra_fee)
# remove previous transaction and update wallet
if self.pushed:
self.hdwallet.transaction_delete(old_txid)
if broadcast:
self.send()

def add_input_from_wallet(self, amount_min=0, key_id=None, min_confirms=0):
if not amount_min:
amount_min = self.network.dust_amount
utxos = self.hdwallet.utxos(self.account_id, network=self.network.name, min_confirms=min_confirms,
key_id=key_id)
current_inputs = [(i.prev_txid.hex(), i.output_n_int) for i in self.inputs]
unused_inputs = [u for u in utxos
if (u['txid'], u['output_n']) not in current_inputs and u['value'] >= amount_min]
if not unused_inputs:
raise TransactionError("Not enough unspent inputs found for transaction %s" %
self.txid)
# take first input
utxo = unused_inputs[0]
inp_keys, key = self.hdwallet._objects_by_key_id(utxo['key_id'])
unlock_script_type = get_unlocking_script_type(utxo['script_type'], self.witness_type,
multisig=self.hdwallet.multisig)
return self.add_input(utxo['txid'], utxo['output_n'], keys=inp_keys, script_type=unlock_script_type,
sigs_required=self.hdwallet.multisig_n_required, sort=self.hdwallet.sort_keys,
compressed=key.compressed, value=utxo['value'], address=utxo['address'],
locking_script=utxo['script'], witness_type=key.witness_type)

class Wallet(object):
"""
Expand Down Expand Up @@ -3559,6 +3636,12 @@ def transaction_spent(self, txid, output_n):
def update_transactions_from_block(block, network=None):
pass

def transaction_delete(self, txid):
wt = self.transaction(txid)
if wt:
wt.delete()
else:
raise WalletError("Transaction %s not found in this wallet" % txid)

def _objects_by_key_id(self, key_id):
key = self.session.query(DbKey).filter_by(id=key_id).scalar()
Expand Down
2 changes: 1 addition & 1 deletion tests/test_transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1351,7 +1351,7 @@ def test_transaction_bumpfee(self):
self.assertEqual(t.fee, 3333)
txid_before = t.txid
t.bumpfee()
self.assertEqual(t.fee, 4068)
self.assertEqual(t.fee, 4598)
self.assertNotEqual(t.txid, txid_before)
self.assertEqual(sum([i.value for i in t.inputs]) - sum([o.value for o in t.outputs]), t.fee)

Expand Down
65 changes: 63 additions & 2 deletions tests/test_wallets.py
Original file line number Diff line number Diff line change
Expand Up @@ -2365,8 +2365,8 @@ def test_wallet_anti_fee_sniping(self):
w = wallet_create_or_open('antifeesnipingtestwallet', network='testnet', db_uri=self.database_uri)
w.utxo_add(w.get_key().address, 1234567, os.urandom(32).hex(), 1)
t = w.send_to('tb1qrjtz22q59e76mhumy0p586cqukatw5vcd0xvvz', 123456)
# FIXME: Bitaps and Bitgo return incorrect blockcount for testnet
block_height = Service(network='testnet', exclude_providers=['bitgo', 'bitaps'], cache_uri='').blockcount()
# Bitaps and Bitgo return incorrect blockcount for testnet, so set delta
self.assertAlmostEqual(t.locktime, block_height+1, delta=3)

w2 = wallet_create_or_open('antifeesnipingtestwallet2', network='testnet', anti_fee_sniping=True)
Expand Down Expand Up @@ -2834,4 +2834,65 @@ def test_wallet_mixed_witness_types_passphrase(self):
expected_addresslist = \
['39h96ozh8F8W2sVrc2EhEbFwwdRoLHJAfB', '3LdJC6MSmFqKrn2WrxRfhd8DYkYYr8FNDr',
'MTSW4eC7xJiyp4YjwGZqpGmubsdm28Cdvc', 'bc1qgw8rg0057q9fmupx7ru6vtkxzy03gexc9ljycagj8z3hpzdfg7usvu56dp']
self.assertListEqual(sorted(w.addresslist()), expected_addresslist)
self.assertListEqual(sorted(w.addresslist()), expected_addresslist)

def test_wallet_transactions_add_input_from_wallet(self):
w = wallet_create_or_open('add_input_from_wallet_test', network='bitcoinlib_test',
db_uri=self.database_uri)
w.utxos_update()
t = WalletTransaction(w, network='bitcoinlib_test')
t.add_input_from_wallet()
t.add_output(100000, 'blt1q68x6ghc7anelyzm4v7hwl2g245e07agee8yfag')
t.add_output(99000000, 'blt1qy0dlpnmfd8ldt5ns5kp0m4ery79wjaw5fz30t3', change=True)
t.sign_and_update()
self.assertTrue(t.verified)
self.assertEqual(t.fee, 900000)
self.assertEqual(len(t.outputs), 2)
self.assertEqual(t.inputs[0].witness_type, 'segwit')

def test_wallet_transactions_bumpfee(self):
pkm = 'elephant dust deer company win final'
wallet_delete_if_exists('bumpfeetest01', force=True)
w = wallet_create_or_open('bumpfeetest01', keys=pkm, network='bitcoinlib_test', db_uri=self.database_uri)
w.utxos_update()
t = w.send_to('blt1qm89pcm4392vj93q9s2ft8saqzm4paruzj95a83', 99900000, fee=100000,
broadcast=True)
self.assertEqual(w.balance(), 100000000)
self.assertEqual(len(t.inputs), 1)
self.assertEqual(len(t.outputs), 1)
self.assertEqual(w.utxos()[0]['txid'], 'ea7bd8fe970ca6430cebbbf914ce2feeb369c3ae95edc117725dbe21519ccdab')
t.bumpfee(broadcast=True)
self.assertEqual(len(t.inputs), 2)
self.assertEqual(len(t.outputs), 2)
self.assertEqual(w.balance(), 99999325)

w = wallet_create_or_open('bumpfeetest02', keys=pkm, network='bitcoinlib_test', db_uri=self.database_uri)
w.utxos_update()
t = w.send_to('blt1qm89pcm4392vj93q9s2ft8saqzm4paruzj95a83', 50000000, fee=100000,
broadcast=True)
self.assertEqual(w.balance(), 149900000)
self.assertEqual(len(t.inputs), 1)
self.assertEqual(len(t.outputs), 2)
t.bumpfee(fee=200000, broadcast=True)
self.assertEqual(len(t.inputs), 1)
self.assertEqual(len(t.outputs), 2)
self.assertEqual(w.balance(), 149800000)

w = wallet_create_or_open('bumpfeetest03', keys=pkm, network='bitcoinlib_test', db_uri=self.database_uri)
w.utxos_update()
t = w.send_to('blt1qm89pcm4392vj93q9s2ft8saqzm4paruzj95a83', 99900000, fee=50000,
broadcast=True)
self.assertEqual(w.balance(), 100050000)
self.assertEqual(len(t.inputs), 1)
self.assertEqual(len(t.outputs), 2)
t.bumpfee(extra_fee=50000, broadcast=True)
self.assertEqual(len(t.inputs), 1)
self.assertEqual(len(t.outputs), 1)
self.assertEqual(w.balance(), 100000000)
self.assertEqual(len(w.utxos()), 1)

w = wallet_create_or_open('bumpfeetest04', keys=pkm, network='bitcoinlib_test', db_uri=self.database_uri)
w.utxos_update()
t = w.send_to('blt1qm89pcm4392vj93q9s2ft8saqzm4paruzj95a83', 199900000, fee=100000,
broadcast=True)
self.assertRaisesRegex(TransactionError, "Not enough unspent inputs found for transaction", t.bumpfee)

0 comments on commit 19ca898

Please sign in to comment.