tx.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593
  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 *
  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. 'mixed_inputs': """
  35. NOTE: This transaction uses a mixture of both mmgen and non-mmgen inputs,
  36. which makes the signing process more complicated. When signing the
  37. transaction, keys for the non-mmgen inputs must be supplied in a separate
  38. file using the '-k' option of mmgen-txsign.
  39. Alternatively, you may import the mmgen keys into the wallet.dat of your
  40. offline bitcoind, first generating the required keys with mmgen-keygen and
  41. then running mmgen-txsign with the '-f' option to force the use of
  42. wallet.dat as the key source.
  43. Selected mmgen inputs: %s"""
  44. }
  45. def connect_to_bitcoind():
  46. host,port,user,passwd = "localhost",8332,"rpcuser","rpcpassword"
  47. cfg = get_cfg_options((user,passwd))
  48. import mmgen.rpc.connection
  49. f = mmgen.rpc.connection.BitcoinConnection
  50. try:
  51. c = f(cfg[user],cfg[passwd],host,port)
  52. except:
  53. msg("Unable to establish RPC connection with bitcoind")
  54. sys.exit(2)
  55. return c
  56. def trim_exponent(n):
  57. '''Remove exponent and trailing zeros.
  58. '''
  59. d = Decimal(n)
  60. return d.quantize(Decimal(1)) if d == d.to_integral() else d.normalize()
  61. def check_address(rcpt_address):
  62. from mmgen.bitcoin import verify_addr
  63. if not verify_addr(rcpt_address):
  64. sys.exit(3)
  65. def check_btc_amt(send_amt):
  66. from decimal import Decimal
  67. try:
  68. retval = Decimal(send_amt)
  69. except:
  70. msg("%s: Invalid amount" % send_amt)
  71. sys.exit(3)
  72. if retval.as_tuple()[-1] < -8:
  73. msg("%s: Too many decimal places in amount" % send_amt)
  74. sys.exit(3)
  75. return trim_exponent(retval)
  76. def get_cfg_options(cfg_keys):
  77. if "HOME" in os.environ:
  78. cfg_file = "%s/%s" % (os.environ["HOME"], ".bitcoin/bitcoin.conf")
  79. elif "HOMEPATH" in os.environ:
  80. # Windows:
  81. cfg_file = "%s%s" % (os.environ["HOMEPATH"],
  82. r"\Application Data\Bitcoin\bitcoin.conf")
  83. else:
  84. msg("Unable to find bitcoin configuration file")
  85. sys.exit(3)
  86. try:
  87. f = open(cfg_file)
  88. except:
  89. msg("Unable to open file '%s' for reading" % cfg_file)
  90. sys.exit(2)
  91. cfg = {}
  92. for line in f.readlines():
  93. s = line.translate(None,"\n\t ").split("=")
  94. for k in cfg_keys:
  95. if s[0] == k: cfg[k] = s[1]
  96. f.close()
  97. for k in cfg_keys:
  98. if not k in cfg:
  99. msg("Configuration option '%s' must be set in %s" % (k,cfg_file))
  100. sys.exit(2)
  101. return cfg
  102. def print_tx_to_file(tx,sel_unspent,send_amt,opts):
  103. tx_id = make_chksum_6(unhexlify(tx)).upper()
  104. outfile = "tx_%s[%s].raw" % (tx_id,send_amt)
  105. if 'outdir' in opts:
  106. outfile = "%s/%s" % (opts['outdir'], outfile)
  107. metadata = "%s %s %s" % (tx_id, send_amt, make_timestamp())
  108. sig_data = [{"txid":i.txid,"vout":i.vout,"scriptPubKey":i.scriptPubKey}
  109. for i in sel_unspent]
  110. data = "%s\n%s\n%s\n%s\n" % (
  111. metadata, tx, repr(sig_data),
  112. repr([i.__dict__ for i in sel_unspent])
  113. )
  114. write_to_file(outfile,data,confirm=False)
  115. msg("Transaction data saved to file '%s'" % outfile)
  116. def print_signed_tx_to_file(tx,sig_tx,metadata,opts):
  117. tx_id = make_chksum_6(unhexlify(tx)).upper()
  118. outfile = "tx_{}[{}].sig".format(*metadata[:2])
  119. if 'outdir' in opts:
  120. outfile = "%s/%s" % (opts['outdir'], outfile)
  121. data = "%s\n%s\n" % (" ".join(metadata),sig_tx)
  122. write_to_file(outfile,data,confirm=False)
  123. msg("Signed transaction saved to file '%s'" % outfile)
  124. def print_sent_tx_to_file(tx,metadata,opts):
  125. outfile = "tx_{}[{}].out".format(*metadata[:2])
  126. if 'outdir' in opts:
  127. outfile = "%s/%s" % (opts['outdir'], outfile)
  128. write_to_file(outfile,tx+"\n",confirm=False)
  129. msg("Transaction ID saved to file '%s'" % outfile)
  130. def sort_and_view(unspent):
  131. def s_amt(a,b): return cmp(a.amount,b.amount)
  132. def s_txid(a,b):
  133. return cmp("%s %03s" % (a.txid,a.vout), "%s %03s" % (b.txid,b.vout))
  134. def s_addr(a,b): return cmp(a.address,b.address)
  135. def s_age(a,b): return cmp(b.confirmations,a.confirmations)
  136. def s_mmgen(a,b): return cmp(a.account,b.account)
  137. fs = " %-4s %-11s %-2s %-34s %-13s %-s"
  138. sort,group,show_mmaddr,reverse = "",False,False,False
  139. total = trim_exponent(sum([i.amount for i in unspent]))
  140. from copy import deepcopy
  141. msg("")
  142. while True:
  143. out = deepcopy(unspent)
  144. for i in out: i.skip = ""
  145. for n in range(len(out)):
  146. if group and n < len(out)-1:
  147. a,b = out[n],out[n+1]
  148. if sort == "address" and a.address == b.address:
  149. out[n+1].skip = "d"
  150. elif sort == "txid" and a.txid == b.txid:
  151. out[n+1].skip = "t"
  152. output = ["UNSPENT OUTPUTS (sort order: %s%s%s) Total BTC: %s" % (
  153. "reverse " if reverse else "",
  154. sort if sort else "None",
  155. " (grouped)" if group and (sort == "address" or sort == "txid") else "",
  156. total
  157. )]
  158. output.append(fs % ("Num","TX id Vout","","Address","Amount (BTC)",
  159. "Age(days)"))
  160. for i in out:
  161. amt = str(trim_exponent(i.amount))
  162. lfill = 3 - len(amt.split(".")[0]) if "." in amt else 3 - len(amt)
  163. i.amt = " "*lfill + amt
  164. i.days = int(i.confirmations * mins_per_block / (60*24))
  165. for n,i in enumerate(out):
  166. if i.skip == "d":
  167. addr = "|" + "." * 33
  168. else:
  169. if show_mmaddr:
  170. if verify_mmgen_label(i.account):
  171. addr = "%s.. %s" % (i.address[:4],i.account)
  172. else:
  173. addr = i.address
  174. else:
  175. addr = i.address
  176. txid = " |..." if i.skip == "t" else i.txid[:8]+"..."
  177. output.append(fs % (str(n+1)+")",txid,i.vout,addr,i.amt,i.days))
  178. skip_body = False
  179. while True:
  180. if skip_body:
  181. skip_body = False
  182. immed_chars = "qpP"
  183. else:
  184. msg("\n".join(output))
  185. msg("""
  186. Sort options: [t]xid, [a]mount, a[d]dress, [A]ge, [r]everse, [M]mgen addr
  187. View options: [g]roup, show [m]mgen addr""")
  188. immed_chars = "qpPtadArMgm"
  189. reply = get_char(
  190. "(Type 'q' to quit sorting, 'p' to print to file, 'v' to view in pager): ",
  191. immed_chars=immed_chars)
  192. if reply == 'a': unspent.sort(s_amt); sort = "amount"; break
  193. elif reply == 't': unspent.sort(s_txid); sort = "txid"; break
  194. elif reply == 'd': unspent.sort(s_addr); sort = "address"; break
  195. elif reply == 'A': unspent.sort(s_age); sort = "age"; break
  196. elif reply == 'M': unspent.sort(s_mmgen); show_mmaddr,sort=True,"mmgen"; break
  197. elif reply == 'r':
  198. reverse = False if reverse else True
  199. unspent.reverse()
  200. break
  201. elif reply == 'g': group = False if group else True; break
  202. elif reply == 'm': show_mmaddr = False if show_mmaddr else True; break
  203. elif reply == 'p':
  204. pfs = " %-4s %-67s %-34s %-12s %-13s %-10s %s"
  205. pout = [pfs % ("Num","TX id,Vout","Address","MMgen ID",
  206. "Amount (BTC)","Age (days)", "Comment")]
  207. for n,i in enumerate(out):
  208. if verify_mmgen_label(i.account):
  209. s = i.account.split(None,1)
  210. mmid,cmt = s[0],(s[1] if len(s) == 2 else "")
  211. else:
  212. mmid,cmt = "",i.account
  213. os = pfs % (str(n+1)+")", str(i.txid)+","+str(i.vout),
  214. i.address,mmid,i.amt,i.days,cmt)
  215. pout.append(os.rstrip())
  216. sort_info = (
  217. ("reverse," if reverse else "") +
  218. (sort if sort else "unsorted")
  219. )
  220. outdata = \
  221. "Unspent outputs ({} UTC)\nSort order: {}\n\n{}\n\nTotal BTC: {}\n".format(
  222. make_timestr(), sort_info, "\n".join(pout), total
  223. )
  224. outfile = "listunspent[%s].out" % sort_info
  225. write_to_file(outfile, outdata)
  226. skip_body = True
  227. msg("\nData written to '%s'" % outfile)
  228. elif reply == 'v': do_pager("\n".join(output))
  229. elif reply == 'q': break
  230. else: msg("Invalid input")
  231. msg("\n")
  232. if reply in 'q': break
  233. return tuple(unspent)
  234. def verify_mmgen_label(s,return_str=False,check_label_len=False):
  235. fail = "" if return_str else False
  236. success = s if return_str else True
  237. if not s: return fail
  238. try:
  239. mminfo,comment = s.split(None,1)
  240. except:
  241. mminfo,comment = s,None
  242. if mminfo[8] != ':': return fail
  243. for i in mminfo[:8]:
  244. if not i in "01234567890ABCDEF": return fail
  245. for i in mminfo[9:]:
  246. if not i in "0123456789": return fail
  247. if check_label_len and comment:
  248. check_addr_comment(comment)
  249. return success
  250. def view_tx_data(c,inputs_data,tx_hex,metadata=[],pager=False):
  251. td = c.decoderawtransaction(tx_hex)
  252. out = "TRANSACTION DATA\n\n"
  253. if metadata:
  254. out += "Header: [Tx ID: {}] [Amount: {} BTC] [Time: {}]\n\n".format(*metadata)
  255. out += "Inputs:\n\n"
  256. total_in = 0
  257. for n,i in enumerate(td['vin']):
  258. for j in inputs_data:
  259. if j['txid'] == i['txid'] and j['vout'] == i['vout']:
  260. days = int(j['confirmations'] * mins_per_block / (60*24))
  261. total_in += j['amount']
  262. out += (" " + """
  263. %-2s tx,vout: %s,%s
  264. address: %s
  265. ID/label: %s
  266. amount: %s BTC
  267. confirmations: %s (around %s days)
  268. """.strip() %
  269. (n+1,i['txid'],i['vout'],j['address'],verify_mmgen_label(j['account'],True),
  270. trim_exponent(j['amount']),j['confirmations'],days)+"\n\n")
  271. break
  272. out += "Total input: %s BTC\n\n" % trim_exponent(total_in)
  273. total_out = 0
  274. out += "Outputs:\n\n"
  275. for n,i in enumerate(td['vout']):
  276. total_out += i['value']
  277. out += (" " + """
  278. %-2s address: %s
  279. amount: %s BTC
  280. """.strip() % (
  281. n,
  282. i['scriptPubKey']['addresses'][0],
  283. trim_exponent(i['value']))
  284. + "\n\n")
  285. out += "Total output: %s BTC\n" % trim_exponent(total_out)
  286. out += "TX fee: %s BTC\n" % trim_exponent(total_in-total_out)
  287. if pager: do_pager(out+"\n")
  288. else: msg("\n"+out)
  289. def parse_tx_data(tx_data,infile):
  290. if len(tx_data) != 4:
  291. msg("'%s': not a transaction file" % infile)
  292. sys.exit(2)
  293. err_fmt = "Transaction %s is invalid"
  294. if len(tx_data[0].split()) != 3:
  295. msg(err_fmt % "metadata")
  296. sys.exit(2)
  297. try: unhexlify(tx_data[1])
  298. except:
  299. msg(err_fmt % "hex data")
  300. sys.exit(2)
  301. try:
  302. sig_data = eval(tx_data[2])
  303. except:
  304. msg(err_fmt % "signature data")
  305. sys.exit(2)
  306. try:
  307. inputs_data = eval(tx_data[3])
  308. except:
  309. msg(err_fmt % "inputs data")
  310. sys.exit(2)
  311. return tx_data[0].split(),tx_data[1],sig_data,inputs_data
  312. def select_outputs(unspent,prompt):
  313. while True:
  314. reply = my_raw_input(prompt).strip()
  315. if not reply: continue
  316. from mmgen.utils import parse_address_list
  317. selected = parse_address_list(reply,sep=None)
  318. if not selected: continue
  319. if selected[-1] > len(unspent):
  320. msg("Inputs must be less than %s" % len(unspent))
  321. continue
  322. return selected
  323. def make_tx_out(rcpt_arg):
  324. import decimal
  325. try:
  326. tx_out = dict([(i.split(":")[0],i.split(":")[1])
  327. for i in rcpt_arg.split(",")])
  328. except:
  329. msg("Invalid format: %s" % rcpt_arg)
  330. sys.exit(3)
  331. try:
  332. for i in tx_out.keys():
  333. tx_out[i] = trim_exponent(Decimal(tx_out[i]))
  334. except decimal.InvalidOperation:
  335. msg("Decimal conversion error in suboption '%s:%s'" % (i,tx_out[i]))
  336. sys.exit(3)
  337. return tx_out
  338. def check_addr_comment(label):
  339. if len(label) > max_addr_label_len:
  340. msg("'%s': overlong label (length must be <=%s)" %
  341. (label,max_addr_label_len))
  342. sys.exit(3)
  343. from string import ascii_letters, digits
  344. chrs = tuple(ascii_letters + digits) + addr_label_symbols
  345. for ch in list(label):
  346. if ch not in chrs:
  347. msg("'%s': illegal character in label '%s'" % (ch,label))
  348. msg("Permitted characters: A-Za-z0-9, plus '%s'" %
  349. "', '".join(addr_label_symbols))
  350. sys.exit(3)
  351. def parse_addrs_file(f):
  352. lines = get_lines_from_file(f,"address data")
  353. lines = remove_blanks_comments(lines)
  354. try:
  355. seed_id,obrace = lines[0].split()
  356. except:
  357. msg("Invalid first line: '%s'" % lines[0])
  358. sys.exit(3)
  359. cbrace = lines[-1]
  360. if obrace != '{':
  361. msg("'%s': invalid first line" % lines[0])
  362. elif cbrace != '}':
  363. msg("'%s': invalid last line" % cbrace)
  364. elif len(seed_id) != 8:
  365. msg("'%s': invalid Seed ID" % seed_id)
  366. else:
  367. try:
  368. unhexlify(seed_id)
  369. except:
  370. msg("'%s': invalid Seed ID" % seed_id)
  371. sys.exit(3)
  372. ret = []
  373. for i in lines[1:-1]:
  374. d = i.split(None,2)
  375. try: d[0] = int(d[0])
  376. except:
  377. msg("'%s': invalid address num. in line: %s" % (d[0],d))
  378. sys.exit(3)
  379. from mmgen.bitcoin import verify_addr
  380. if not verify_addr(d[1]):
  381. msg("'%s': invalid address" % d[1])
  382. sys.exit(3)
  383. if len(d) == 3: check_addr_comment(d[2])
  384. ret.append(tuple(d))
  385. return seed_id,ret
  386. sys.exit(3)
  387. def sign_transaction(c,tx_hex,sig_data,keys=None):
  388. if keys:
  389. msg("%s keys total" % len(keys))
  390. if debug: print "Keys:\n %s" % "\n ".join(keys)
  391. from mmgen.rpc import exceptions
  392. try:
  393. sig_tx = c.signrawtransaction(tx_hex,sig_data,keys)
  394. except exceptions.InvalidAddressOrKey:
  395. msg("Invalid address or key")
  396. sys.exit(3)
  397. # except:
  398. # msg("Failed to sign transaction")
  399. # sys.exit(3)
  400. return sig_tx
  401. def get_keys_for_mmgen_addrs(mmgen_addrs,infiles,opts):
  402. seed_ids = list(set([i['account'][:8] for i in mmgen_addrs]))
  403. seed_ids_save = seed_ids[0:]
  404. keys = []
  405. while seed_ids:
  406. infile = False
  407. if infiles:
  408. infile = infiles.pop()
  409. seed = get_seed(infile,opts)
  410. elif "from_brain" in opts or "from_mnemonic" in opts or "from_seed" in opts:
  411. msg("Need data for seed ID %s" % seed_ids[0])
  412. seed = get_seed_retry("",opts)
  413. else:
  414. b,p,v = ("A seed","","is") if len(seed_ids) == 1 else ("Seed","s","are")
  415. msg("ERROR: %s source%s %s required for the following seed ID%s: %s" %
  416. (b,p,v,p," ".join(seed_ids)))
  417. sys.exit(2)
  418. seed_id = make_chksum_8(seed)
  419. if seed_id in seed_ids:
  420. seed_ids.remove(seed_id)
  421. seed_id_addrs = [
  422. int(i['account'].split()[0][9:]) for i in mmgen_addrs
  423. if i['account'][:8] == seed_id]
  424. from mmgen.addr import generate_keys
  425. keys += [i['wif'] for i in generate_keys(seed, seed_id_addrs)]
  426. else:
  427. if seed_id in seed_ids_save:
  428. msg_r("Ignoring duplicate seed source")
  429. if infile: msg(" '%s'" % infile)
  430. else: msg(" for ID %s" % seed_id)
  431. else:
  432. msg("Seed source produced an invalid seed ID (%s)" % seed_id)
  433. if infile:
  434. msg("Invalid input file: %s" % infile)
  435. sys.exit(2)
  436. return keys
  437. def sign_tx_with_bitcoind_wallet(c,tx_hex,sig_data,keys,opts):
  438. try:
  439. sig_tx = sign_transaction(c,tx_hex,sig_data,keys)
  440. except:
  441. from mmgen.rpc import exceptions
  442. msg("Using keys in wallet.dat as per user request")
  443. prompt = "Enter passphrase for bitcoind wallet: "
  444. while True:
  445. passwd = get_bitcoind_passphrase(prompt,opts)
  446. try:
  447. c.walletpassphrase(passwd, 9999)
  448. except exceptions.WalletPassphraseIncorrect:
  449. msg("Passphrase incorrect")
  450. else:
  451. msg("Passphrase OK"); break
  452. sig_tx = sign_transaction(c,tx_hex,sig_data,keys)
  453. msg("Locking wallet")
  454. try:
  455. c.walletlock()
  456. except:
  457. msg("Failed to lock wallet")
  458. return sig_tx
  459. def missing_keys_errormsg(other_addrs):
  460. msg("""
  461. A key file (option '-f') or wallet.dat (option '-w') must be supplied
  462. for the following non-mmgen address%s: %s""" %
  463. ("" if len(other_addrs) == 1 else "es",
  464. " ".join([i['address'] for i in other_addrs])
  465. ))