Merge pull request #6553
[bitcoinplatinum.git] / contrib / spendfrom / spendfrom.py
blob72ee0425eb2363ab39f85ba9d239891aa0279dcd
1 #!/usr/bin/env python
3 # Use the raw transactions API to spend bitcoins received on particular addresses,
4 # and send any change back to that same address.
6 # Example usage:
7 # spendfrom.py # Lists available funds
8 # spendfrom.py --from=ADDRESS --to=ADDRESS --amount=11.00
10 # Assumes it will talk to a bitcoind or Bitcoin-Qt running
11 # on localhost.
13 # Depends on jsonrpc
16 from decimal import *
17 import getpass
18 import math
19 import os
20 import os.path
21 import platform
22 import sys
23 import time
24 from jsonrpc import ServiceProxy, json
26 BASE_FEE=Decimal("0.001")
28 def check_json_precision():
29 """Make sure json library being used does not lose precision converting BTC values"""
30 n = Decimal("20000000.00000003")
31 satoshis = int(json.loads(json.dumps(float(n)))*1.0e8)
32 if satoshis != 2000000000000003:
33 raise RuntimeError("JSON encode/decode loses precision")
35 def determine_db_dir():
36 """Return the default location of the bitcoin data directory"""
37 if platform.system() == "Darwin":
38 return os.path.expanduser("~/Library/Application Support/Bitcoin/")
39 elif platform.system() == "Windows":
40 return os.path.join(os.environ['APPDATA'], "Bitcoin")
41 return os.path.expanduser("~/.bitcoin")
43 def read_bitcoin_config(dbdir):
44 """Read the bitcoin.conf file from dbdir, returns dictionary of settings"""
45 from ConfigParser import SafeConfigParser
47 class FakeSecHead(object):
48 def __init__(self, fp):
49 self.fp = fp
50 self.sechead = '[all]\n'
51 def readline(self):
52 if self.sechead:
53 try: return self.sechead
54 finally: self.sechead = None
55 else:
56 s = self.fp.readline()
57 if s.find('#') != -1:
58 s = s[0:s.find('#')].strip() +"\n"
59 return s
61 config_parser = SafeConfigParser()
62 config_parser.readfp(FakeSecHead(open(os.path.join(dbdir, "bitcoin.conf"))))
63 return dict(config_parser.items("all"))
65 def connect_JSON(config):
66 """Connect to a bitcoin JSON-RPC server"""
67 testnet = config.get('testnet', '0')
68 testnet = (int(testnet) > 0) # 0/1 in config file, convert to True/False
69 if not 'rpcport' in config:
70 config['rpcport'] = 18332 if testnet else 8332
71 connect = "http://%s:%s@127.0.0.1:%s"%(config['rpcuser'], config['rpcpassword'], config['rpcport'])
72 try:
73 result = ServiceProxy(connect)
74 # ServiceProxy is lazy-connect, so send an RPC command mostly to catch connection errors,
75 # but also make sure the bitcoind we're talking to is/isn't testnet:
76 if result.getmininginfo()['testnet'] != testnet:
77 sys.stderr.write("RPC server at "+connect+" testnet setting mismatch\n")
78 sys.exit(1)
79 return result
80 except:
81 sys.stderr.write("Error connecting to RPC server at "+connect+"\n")
82 sys.exit(1)
84 def unlock_wallet(bitcoind):
85 info = bitcoind.getinfo()
86 if 'unlocked_until' not in info:
87 return True # wallet is not encrypted
88 t = int(info['unlocked_until'])
89 if t <= time.time():
90 try:
91 passphrase = getpass.getpass("Wallet is locked; enter passphrase: ")
92 bitcoind.walletpassphrase(passphrase, 5)
93 except:
94 sys.stderr.write("Wrong passphrase\n")
96 info = bitcoind.getinfo()
97 return int(info['unlocked_until']) > time.time()
99 def list_available(bitcoind):
100 address_summary = dict()
102 address_to_account = dict()
103 for info in bitcoind.listreceivedbyaddress(0):
104 address_to_account[info["address"]] = info["account"]
106 unspent = bitcoind.listunspent(0)
107 for output in unspent:
108 # listunspent doesn't give addresses, so:
109 rawtx = bitcoind.getrawtransaction(output['txid'], 1)
110 vout = rawtx["vout"][output['vout']]
111 pk = vout["scriptPubKey"]
113 # This code only deals with ordinary pay-to-bitcoin-address
114 # or pay-to-script-hash outputs right now; anything exotic is ignored.
115 if pk["type"] != "pubkeyhash" and pk["type"] != "scripthash":
116 continue
118 address = pk["addresses"][0]
119 if address in address_summary:
120 address_summary[address]["total"] += vout["value"]
121 address_summary[address]["outputs"].append(output)
122 else:
123 address_summary[address] = {
124 "total" : vout["value"],
125 "outputs" : [output],
126 "account" : address_to_account.get(address, "")
129 return address_summary
131 def select_coins(needed, inputs):
132 # Feel free to improve this, this is good enough for my simple needs:
133 outputs = []
134 have = Decimal("0.0")
135 n = 0
136 while have < needed and n < len(inputs):
137 outputs.append({ "txid":inputs[n]["txid"], "vout":inputs[n]["vout"]})
138 have += inputs[n]["amount"]
139 n += 1
140 return (outputs, have-needed)
142 def create_tx(bitcoind, fromaddresses, toaddress, amount, fee):
143 all_coins = list_available(bitcoind)
145 total_available = Decimal("0.0")
146 needed = amount+fee
147 potential_inputs = []
148 for addr in fromaddresses:
149 if addr not in all_coins:
150 continue
151 potential_inputs.extend(all_coins[addr]["outputs"])
152 total_available += all_coins[addr]["total"]
154 if total_available < needed:
155 sys.stderr.write("Error, only %f BTC available, need %f\n"%(total_available, needed));
156 sys.exit(1)
159 # Note:
160 # Python's json/jsonrpc modules have inconsistent support for Decimal numbers.
161 # Instead of wrestling with getting json.dumps() (used by jsonrpc) to encode
162 # Decimals, I'm casting amounts to float before sending them to bitcoind.
164 outputs = { toaddress : float(amount) }
165 (inputs, change_amount) = select_coins(needed, potential_inputs)
166 if change_amount > BASE_FEE: # don't bother with zero or tiny change
167 change_address = fromaddresses[-1]
168 if change_address in outputs:
169 outputs[change_address] += float(change_amount)
170 else:
171 outputs[change_address] = float(change_amount)
173 rawtx = bitcoind.createrawtransaction(inputs, outputs)
174 signed_rawtx = bitcoind.signrawtransaction(rawtx)
175 if not signed_rawtx["complete"]:
176 sys.stderr.write("signrawtransaction failed\n")
177 sys.exit(1)
178 txdata = signed_rawtx["hex"]
180 return txdata
182 def compute_amount_in(bitcoind, txinfo):
183 result = Decimal("0.0")
184 for vin in txinfo['vin']:
185 in_info = bitcoind.getrawtransaction(vin['txid'], 1)
186 vout = in_info['vout'][vin['vout']]
187 result = result + vout['value']
188 return result
190 def compute_amount_out(txinfo):
191 result = Decimal("0.0")
192 for vout in txinfo['vout']:
193 result = result + vout['value']
194 return result
196 def sanity_test_fee(bitcoind, txdata_hex, max_fee):
197 class FeeError(RuntimeError):
198 pass
199 try:
200 txinfo = bitcoind.decoderawtransaction(txdata_hex)
201 total_in = compute_amount_in(bitcoind, txinfo)
202 total_out = compute_amount_out(txinfo)
203 if total_in-total_out > max_fee:
204 raise FeeError("Rejecting transaction, unreasonable fee of "+str(total_in-total_out))
206 tx_size = len(txdata_hex)/2
207 kb = tx_size/1000 # integer division rounds down
208 if kb > 1 and fee < BASE_FEE:
209 raise FeeError("Rejecting no-fee transaction, larger than 1000 bytes")
210 if total_in < 0.01 and fee < BASE_FEE:
211 raise FeeError("Rejecting no-fee, tiny-amount transaction")
212 # Exercise for the reader: compute transaction priority, and
213 # warn if this is a very-low-priority transaction
215 except FeeError as err:
216 sys.stderr.write((str(err)+"\n"))
217 sys.exit(1)
219 def main():
220 import optparse
222 parser = optparse.OptionParser(usage="%prog [options]")
223 parser.add_option("--from", dest="fromaddresses", default=None,
224 help="addresses to get bitcoins from")
225 parser.add_option("--to", dest="to", default=None,
226 help="address to get send bitcoins to")
227 parser.add_option("--amount", dest="amount", default=None,
228 help="amount to send")
229 parser.add_option("--fee", dest="fee", default="0.0",
230 help="fee to include")
231 parser.add_option("--datadir", dest="datadir", default=determine_db_dir(),
232 help="location of bitcoin.conf file with RPC username/password (default: %default)")
233 parser.add_option("--testnet", dest="testnet", default=False, action="store_true",
234 help="Use the test network")
235 parser.add_option("--dry_run", dest="dry_run", default=False, action="store_true",
236 help="Don't broadcast the transaction, just create and print the transaction data")
238 (options, args) = parser.parse_args()
240 check_json_precision()
241 config = read_bitcoin_config(options.datadir)
242 if options.testnet: config['testnet'] = True
243 bitcoind = connect_JSON(config)
245 if options.amount is None:
246 address_summary = list_available(bitcoind)
247 for address,info in address_summary.iteritems():
248 n_transactions = len(info['outputs'])
249 if n_transactions > 1:
250 print("%s %.8f %s (%d transactions)"%(address, info['total'], info['account'], n_transactions))
251 else:
252 print("%s %.8f %s"%(address, info['total'], info['account']))
253 else:
254 fee = Decimal(options.fee)
255 amount = Decimal(options.amount)
256 while unlock_wallet(bitcoind) == False:
257 pass # Keep asking for passphrase until they get it right
258 txdata = create_tx(bitcoind, options.fromaddresses.split(","), options.to, amount, fee)
259 sanity_test_fee(bitcoind, txdata, amount*Decimal("0.01"))
260 if options.dry_run:
261 print(txdata)
262 else:
263 txid = bitcoind.sendrawtransaction(txdata)
264 print(txid)
266 if __name__ == '__main__':
267 main()