Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

spendfrom.py 9.8KB

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