forked from petertodd/replace-by-fee-tools
-
Notifications
You must be signed in to change notification settings - Fork 2
/
bump-fee.py
executable file
·185 lines (144 loc) · 6.16 KB
/
bump-fee.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
#!/usr/bin/python3
# Copyright (C) 2014 Peter Todd <[email protected]>
#
# This file is subject to the license terms in the LICENSE file found in the
# top-level directory of this distribution.
import argparse
import binascii
import bitcoin
import bitcoin.rpc
import logging
import math
from bitcoin.core import b2x, b2lx, lx, str_money_value, COIN, CMutableTransaction, CMutableTxIn, CMutableTxOut
from bitcoin.wallet import CBitcoinAddress
DUST = int(0.0001 * COIN)
parser = argparse.ArgumentParser(description="Bump tx fee.")
parser.add_argument('-v', action='store_true',
dest='verbose',
help='Verbose')
parser.add_argument('-t', action='store_true',
dest='testnet',
help='Enable testnet')
parser.add_argument('-n', action='store_true',
dest='dryrun',
help="Dry-run; don't actually send the transaction")
parser.add_argument('-s', action='store_true',
dest='first_seen_safe',
help="First-seen-safe rules; do not decrease the value of any txouts")
parser.add_argument('-r', action='store', type=float,
dest='ratio',
metavar='RATIO',
default=10.0,
help='Ratio of new fee to old fee; default 10x higher')
parser.add_argument('txid', action='store', type=str,
help='Transaction id')
args = parser.parse_args()
if args.verbose:
logging.root.setLevel('DEBUG')
if args.testnet:
bitcoin.SelectParams('testnet')
rpc = bitcoin.rpc.Proxy()
try:
args.txid = lx(args.txid)
except ValueError as err:
parser.error('Invalid txid: %s' % str(err))
if len(args.txid) != 32:
parser.error('Invalid txid: Wrong length.')
try:
rpc.gettransaction(args.txid)
except IndexError as err:
parser.exit('Invalid txid: Not in wallet.')
txinfo = rpc.getrawtransaction(args.txid, True)
tx = CMutableTransaction.from_tx(txinfo['tx'])
if 'confirmations' in txinfo and txinfo['confirmations'] > 0:
parser.exit("Transaction already mined; %d confirmations." % txinfo['confirmations'])
# Find a txout that was being used for change
change_txout = None
for vout in tx.vout:
try:
addr = CBitcoinAddress.from_scriptPubKey(vout.scriptPubKey)
except ValueError:
continue
if rpc.validateaddress(addr)['ismine']:
change_txout = vout
break
if change_txout is None:
# No suitable change txout; no txout was an address in our wallet.
#
# Create a new txout for use as change.
addr = rpc.getrawchangeaddress()
change_txout = CMutableTxOut(0, addr.to_scriptPubKey())
tx.vout.append(change_txout)
min_change_txout_nValue = 0
if args.first_seen_safe:
min_change_txout_nValue = change_txout.nValue
logging.debug('First-seen-safe enabled: will not reduce change txout value below %s BTC' % \
str_money_value(min_change_txout_nValue))
# Find total value in
value_in = 0
for vin in tx.vin:
prevout_tx = rpc.getrawtransaction(vin.prevout.hash)
value_in += prevout_tx.vout[vin.prevout.n].nValue
value_out = sum([vout.nValue for vout in tx.vout])
# Units: satoshi's per byte
old_fees_per_byte = (value_in-value_out) / len(tx.serialize())
desired_fees_per_byte = old_fees_per_byte * args.ratio
# Old transaction might have had no fees at all, in which case use the minimum of 0.1mBTC/KB
desired_fees_per_byte = max(desired_fees_per_byte, 0.0001*COIN / 1000)
logging.debug('Old size: %.3f KB, Old fees: %s, %s BTC/KB, Desired fees: %s BTC/KB' % \
(len(tx.serialize()) / 1000,
str_money_value(value_in-value_out),
str_money_value(old_fees_per_byte * 1000),
str_money_value(desired_fees_per_byte * 1000)))
unspent = sorted(rpc.listunspent(1), key=lambda x: x['amount'])
# Modify the transaction by either reducing the amount of change out, or adding
# new inputs, until it meets the fees-per-byte that we want.
while (value_in-value_out) / len(tx.serialize()) < desired_fees_per_byte:
# What's the delta fee that we need to get to our desired fees per byte at
# the current tx size?
delta_fee = math.ceil((desired_fees_per_byte * len(tx.serialize())) - (value_in - value_out))
# Ensure termination; the loop converges so it can get stuck at no fee.
if delta_fee < 1:
break
logging.debug('Delta fee: %s' % str_money_value(delta_fee))
# Can we simply reduce the value of the change output?
new_change_txout_nValue = change_txout.nValue - delta_fee
if new_change_txout_nValue > DUST and new_change_txout_nValue >= min_change_txout_nValue:
value_out -= change_txout.nValue - new_change_txout_nValue
change_txout.nValue = new_change_txout_nValue
else:
# Looks like we need to add another input. We could be clever about
# this, but nah, just add the largest unspent input to the tx and try
# again.
try:
new_unspent = unspent[-1]
unspent = unspent[:-1]
except IndexError:
parser.exit('Not enough confirmed funds left unspent to bump fees')
new_outpoint = new_unspent['outpoint']
new_amount = new_unspent['amount']
logging.debug('Adding new input %s:%d with value %s BTC' % \
(b2lx(new_outpoint.hash), new_outpoint.n,
str_money_value(new_amount)))
new_txin = CMutableTxIn(new_outpoint)
value_in += new_amount
change_txout.nValue += new_amount
value_out += new_amount
tx.vin.append(new_txin)
# re-sign the tx so we can figure out how large the new input's scriptSig will be.
r = rpc.signrawtransaction(tx)
assert(r['complete'])
tx.vin[-1].scriptSig = r['tx'].vin[-1].scriptSig
logging.debug('New size: %.3f KB, New fees: %s, %s BTC/KB' % \
(len(tx.serialize()) / 1000,
str_money_value(value_in-value_out),
str_money_value((value_in-value_out) / len(tx.serialize()) * 1000)))
r = rpc.signrawtransaction(tx)
assert(r['complete'])
tx = r['tx']
if args.dryrun:
print(b2x(tx.serialize()))
else:
logging.debug('Sending tx %s' % b2x(tx.serialize()))
txid = rpc.sendrawtransaction(tx)
print(b2lx(txid))