tx.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. #!/usr/bin/env python
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C) 2013 by philemon <mmgen-py@yandex.com>
  5. #
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. """
  19. tx.py: Bitcoin transaction routines
  20. """
  21. from binascii import unhexlify
  22. from mmgen.utils import msg,msg_r,write_to_file,my_raw_input,get_char,make_chksum_8,make_timestamp
  23. import sys, os
  24. from decimal import Decimal
  25. from mmgen.config import *
  26. txmsg = {
  27. 'not_enough_btc': "Not enough BTC in the inputs for this transaction (%s BTC)",
  28. 'throwaway_change': """
  29. ERROR: This transaction produces change (%s BTC); however, no change
  30. address was specified. Total inputs - transaction fee = %s BTC.
  31. To create a valid transaction with no change address, send this sum to the
  32. specified recipient address.
  33. """.strip()
  34. }
  35. def connect_to_bitcoind(http_timeout=30):
  36. host,port,user,passwd = "localhost",8332,"rpcuser","rpcpassword"
  37. cfg = get_cfg_options((user,passwd))
  38. import mmgen.rpc.connection
  39. f = mmgen.rpc.connection.BitcoinConnection
  40. try:
  41. c = f(cfg[user],cfg[passwd],host,port,http_timeout=http_timeout)
  42. except:
  43. msg("Unable to establish RPC connection with bitcoind")
  44. sys.exit(2)
  45. return c
  46. def trim_exponent(d):
  47. '''Remove exponent and trailing zeros.
  48. '''
  49. return d.quantize(Decimal(1)) if d == d.to_integral() else d.normalize()
  50. def check_address(rcpt_address):
  51. from mmgen.bitcoin import verify_addr
  52. if not verify_addr(rcpt_address):
  53. sys.exit(3)
  54. def check_btc_amt(send_amt):
  55. from decimal import Decimal
  56. try:
  57. retval = Decimal(send_amt)
  58. except:
  59. msg("%s: Invalid amount" % send_amt)
  60. sys.exit(3)
  61. if retval.as_tuple()[-1] < -8:
  62. msg("%s: Too many decimal places in amount" % send_amt)
  63. sys.exit(3)
  64. return trim_exponent(retval)
  65. def get_cfg_options(cfg_keys):
  66. cfg_file = "%s/%s" % (os.environ["HOME"], ".bitcoin/bitcoin.conf")
  67. try:
  68. f = open(cfg_file)
  69. except:
  70. msg("Unable to open file '%s' for reading" % cfg_file)
  71. sys.exit(2)
  72. cfg = {}
  73. for line in f.readlines():
  74. s = line.translate(None,"\n\t ").split("=")
  75. for k in cfg_keys:
  76. if s[0] == k: cfg[k] = s[1]
  77. f.close()
  78. for k in cfg_keys:
  79. if not k in cfg:
  80. msg("Configuration option '%s' must be set in %s" % (k,cfg_file))
  81. sys.exit(2)
  82. return cfg
  83. def print_tx_to_file(tx,sel_unspent,send_amt,opts):
  84. sig_data = [{"txid":i.txid,"vout":i.vout,"scriptPubKey":i.scriptPubKey}
  85. for i in sel_unspent]
  86. tx_id = make_chksum_8(unhexlify(tx))
  87. outfile = "%s[%s].tx" % (tx_id,send_amt)
  88. if 'outdir' in opts:
  89. outfile = "%s/%s" % (opts['outdir'], outfile)
  90. metadata = "%s %s %s" % (tx_id, send_amt, make_timestamp())
  91. data = "%s\n%s\n%s\n%s\n" % (
  92. metadata, tx, repr(sig_data),
  93. repr([i.__dict__ for i in sel_unspent])
  94. )
  95. write_to_file(outfile,data,confirm=False)
  96. msg("Transaction data saved to file '%s'" % outfile)
  97. def print_signed_tx_to_file(tx,sig_tx,metadata,opts):
  98. tx_id = make_chksum_8(unhexlify(tx))
  99. outfile = "{}[{}].txsig".format(*metadata[:2])
  100. if 'outdir' in opts:
  101. outfile = "%s/%s" % (opts['outdir'], outfile)
  102. data = "%s\n%s\n" % (" ".join(metadata),sig_tx)
  103. write_to_file(outfile,data,confirm=False)
  104. msg("Signed transaction saved to file '%s'" % outfile)
  105. def print_sent_tx_to_file(tx,metadata,opts):
  106. outfile = "{}[{}].txout".format(*metadata[:2])
  107. if 'outdir' in opts:
  108. outfile = "%s/%s" % (opts['outdir'], outfile)
  109. write_to_file(outfile,tx+"\n",confirm=False)
  110. msg("Transaction ID saved to file '%s'" % outfile)
  111. def sort_and_view(unspent):
  112. def s_amt(a,b): return cmp(a.amount,b.amount)
  113. def s_txid(a,b):
  114. return cmp("%s %03s" % (a.txid,a.vout), "%s %03s" % (b.txid,b.vout))
  115. def s_addr(a,b): return cmp(a.address,b.address)
  116. def s_age(a,b): return cmp(b.confirmations,a.confirmations)
  117. fs = " %-4s %-11s %-2s %-34s %13s %-s"
  118. fs_hdr = " %-4s %-11s %-4s %-35s %-9s %-s"
  119. sort,group,reverse = "",False,False
  120. from copy import deepcopy
  121. msg("")
  122. while True:
  123. out = deepcopy(unspent)
  124. for i in out: i.skip = ""
  125. for n in range(len(out)):
  126. if group and n < len(out)-1:
  127. a,b = out[n],out[n+1]
  128. if sort == "address" and a.address == b.address:
  129. out[n+1].skip = "d"
  130. elif sort == "txid" and a.txid == b.txid:
  131. out[n+1].skip = "t"
  132. output = []
  133. output.append("UNSPENT OUTPUTS (sort order: %s%s%s)" % (
  134. "reverse " if reverse else "",
  135. sort if sort else "None",
  136. " (grouped)" if group and (sort == "address" or sort == "txid") else ""
  137. ))
  138. output.append(fs_hdr % ("Num","TX id","Vout","Address","Amount",
  139. "Age (days)"))
  140. for n,i in enumerate(out):
  141. amt = str(trim_exponent(i.amount))
  142. fill = 8 - len(amt.split(".")[-1]) if "." in amt else 9
  143. addr = " |" + "-"*32 if i.skip == "d" else i.address
  144. txid = " |---" if i.skip == "t" else i.txid[:8]+"..."
  145. days = int(i.confirmations * mins_per_block / (60*24))
  146. output.append(fs % (str(n+1)+")", txid,i.vout,addr,amt+(" "*fill),days))
  147. while True:
  148. reply = get_char("\n".join(output) +
  149. """\n
  150. Sort options: [t]xid, [a]mount, a[d]dress, [A]ge, [r]everse, [g]roup
  151. (Type 'q' to quit sorting): """).strip()
  152. if reply == 'a': unspent.sort(s_amt); sort = "amount"; break
  153. elif reply == 't': unspent.sort(s_txid); sort = "txid"; break
  154. elif reply == 'd': unspent.sort(s_addr); sort = "address"; break
  155. elif reply == 'A': unspent.sort(s_age); sort = "age"; break
  156. elif reply == 'r':
  157. reverse = False if reverse else True
  158. unspent.reverse()
  159. break
  160. elif reply == 'g': group = False if group else True; break
  161. elif reply == 'q': break
  162. else: msg("Invalid input")
  163. msg("\n")
  164. if reply == 'q': break
  165. return unspent
  166. def view_tx_data(c,inputs_data,tx_hex,metadata=[]):
  167. td = c.decoderawtransaction(tx_hex)
  168. msg("TRANSACTION DATA:\n")
  169. if metadata: msg(
  170. "Header: [ID: {}] [Amount: {} BTC] [Time: {}]\n".format(*metadata))
  171. msg("Inputs:")
  172. total_in = 0
  173. for n,i in enumerate(td['vin']):
  174. for j in inputs_data:
  175. if j['txid'] == i['txid'] and j['vout'] == i['vout']:
  176. days = int(j['confirmations'] * mins_per_block / (60*24))
  177. total_in += j['amount']
  178. msg(" " + """
  179. %-2s tx,vout: %s,%s
  180. address: %s
  181. amount: %s BTC
  182. confirmations: %s (around %s days)
  183. """.strip() %
  184. (n+1,i['txid'],i['vout'],j['address'],
  185. trim_exponent(j['amount']),j['confirmations'],days)+"\n")
  186. break
  187. msg("Total input: %s BTC\n" % trim_exponent(total_in))
  188. total_out = 0
  189. msg("Outputs:")
  190. for n,i in enumerate(td['vout']):
  191. total_out += i['value']
  192. msg(" " + """
  193. %-2s address: %s
  194. amount: %s BTC
  195. """.strip() % (
  196. n,
  197. i['scriptPubKey']['addresses'][0],
  198. trim_exponent(i['value']))
  199. + "\n")
  200. msg("Total output: %s BTC" % trim_exponent(total_out))
  201. msg("TX fee: %s BTC\n" % trim_exponent(total_in-total_out))
  202. def parse_tx_data(tx_data):
  203. if len(tx_data) != 4:
  204. msg("'%s': not a transaction file" % infile)
  205. sys.exit(2)
  206. err_fmt = "Transaction %s is invalid"
  207. if len(tx_data[0].split()) != 3:
  208. msg(err_fmt % "metadata")
  209. sys.exit(2)
  210. try: unhexlify(tx_data[1])
  211. except:
  212. msg(err_fmt % "hex data")
  213. sys.exit(2)
  214. try:
  215. sig_data = eval(tx_data[2])
  216. except:
  217. msg(err_fmt % "signature data")
  218. sys.exit(2)
  219. try:
  220. inputs_data = eval(tx_data[3])
  221. except:
  222. msg(err_fmt % "inputs data")
  223. sys.exit(2)
  224. return tx_data[0].split(),tx_data[1],sig_data,inputs_data
  225. def select_outputs(unspent,prompt):
  226. while True:
  227. reply = my_raw_input(prompt).strip()
  228. if reply:
  229. selected = ()
  230. try:
  231. selected = [int(i) - 1 for i in reply.split()]
  232. except: pass
  233. else:
  234. for i in selected:
  235. if i < 0 or i >= len(unspent):
  236. msg(
  237. "Input must be a number or numbers between 1 and %s" % len(unspent))
  238. break
  239. else: break
  240. msg("'%s': Invalid input" % reply)
  241. return [unspent[i] for i in selected]
  242. def make_tx_out(rcpt_arg):
  243. import decimal
  244. try:
  245. tx_out = dict([(i.split(":")[0],i.split(":")[1])
  246. for i in rcpt_arg.split(",")])
  247. except:
  248. msg("Invalid format: %s" % rcpt_arg)
  249. sys.exit(3)
  250. try:
  251. for i in tx_out.keys():
  252. tx_out[i] = trim_exponent(Decimal(tx_out[i]))
  253. except decimal.InvalidOperation:
  254. msg("Decimal conversion error in suboption '%s:%s'" % (i,tx_out[i]))
  255. sys.exit(3)
  256. return tx_out