main_txcreate.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498
  1. #!/usr/bin/env python
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2015 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. mmgen-txcreate: Create a Bitcoin transaction from MMGen- or non-MMGen inputs
  20. to MMGen- or non-MMGen outputs
  21. """
  22. import sys
  23. from decimal import Decimal
  24. import mmgen.config as g
  25. from mmgen.Opts import *
  26. from mmgen.license import *
  27. from mmgen.tx import *
  28. help_data = {
  29. 'prog_name': g.prog_name,
  30. 'desc': "Create a BTC transaction with outputs to specified addresses",
  31. 'usage': "[opts] <addr,amt> ... [change addr] [addr file] ...",
  32. 'options': """
  33. -h, --help Print this help message
  34. -c, --comment-file= f Source the transaction's comment from file 'f'
  35. -d, --outdir= d Specify an alternate directory 'd' for output
  36. -e, --echo-passphrase Print passphrase to screen when typing it
  37. -f, --tx-fee= f Transaction fee (default: {g.tx_fee} BTC)
  38. -i, --info Display unspent outputs and exit
  39. -q, --quiet Suppress warnings; overwrite files without
  40. prompting
  41. -v, --verbose Produce more verbose output
  42. """.format(g=g),
  43. 'notes': """
  44. Transaction inputs are chosen from a list of the user's unpent outputs
  45. via an interactive menu.
  46. Ages of transactions are approximate based on an average block creation
  47. interval of {g.mins_per_block} minutes.
  48. Addresses on the command line can be Bitcoin addresses or {pnm} addresses
  49. of the form <seed ID>:<number>.
  50. To send all inputs (minus TX fee) to a single output, specify one address
  51. with no amount on the command line.
  52. """.format(g=g,pnm=g.proj_name)
  53. }
  54. wmsg = {
  55. 'too_many_acct_addresses': """
  56. ERROR: More than one address found for account: "%s".
  57. Your "wallet.dat" file appears to have been altered by a non-{pnm} program.
  58. Please restore your tracking wallet from a backup or create a new one and
  59. re-import your addresses.
  60. """.strip().format(pnm=g.proj_name),
  61. 'addr_in_addrfile_only': """
  62. Warning: output address {mmgenaddr} is not in the tracking wallet, which means
  63. its balance will not be tracked. You're strongly advised to import the address
  64. into your tracking wallet before broadcasting this transaction.
  65. """.strip(),
  66. 'addr_not_found': """
  67. No data for MMgen address {mmgenaddr} could be found in either the tracking
  68. wallet or the supplied address file. Please import this address into your
  69. tracking wallet, or supply an address file for it on the command line.
  70. """.strip(),
  71. 'addr_not_found_no_addrfile': """
  72. No data for MMgen address {mmgenaddr} could be found in the tracking wallet.
  73. Please import this address into your tracking wallet or supply an address file
  74. for it on the command line.
  75. """.strip(),
  76. 'no_spendable_outputs': """
  77. No spendable outputs found! Import addresses with balances into your
  78. watch-only wallet using '{pnm}-addrimport' and then re-run this program.
  79. """.strip().format(pnm=g.proj_name.lower()),
  80. 'mixed_inputs': """
  81. NOTE: This transaction uses a mixture of both mmgen and non-mmgen inputs, which
  82. makes the signing process more complicated. When signing the transaction, keys
  83. for the non-{pnm} inputs must be supplied to '{pnl}-txsign' in a file with the
  84. '--keys-from-file' option.
  85. Selected mmgen inputs: %s
  86. """.strip().format(pnm=g.proj_name,pnl=g.proj_name.lower()),
  87. 'not_enough_btc': """
  88. Not enough BTC in the inputs for this transaction (%s BTC)
  89. """.strip(),
  90. 'throwaway_change': """
  91. ERROR: This transaction produces change (%s BTC); however, no change address
  92. was specified.
  93. """.strip(),
  94. }
  95. def format_unspent_outputs_for_printing(out,sort_info,total):
  96. pfs = " %-4s %-67s %-34s %-12s %-13s %-8s %-10s %s"
  97. pout = [pfs % ("Num","TX id,Vout","Address","MMgen ID",
  98. "Amount (BTC)","Conf.","Age (days)", "Comment")]
  99. for n,i in enumerate(out):
  100. addr = "=" if i.skip == "addr" and "grouped" in sort_info else i.address
  101. tx = " " * 63 + "=" \
  102. if i.skip == "txid" and "grouped" in sort_info else str(i.txid)
  103. s = pfs % (str(n+1)+")", tx+","+str(i.vout),addr,
  104. i.mmid,i.amt,i.confirmations,i.days,i.comment)
  105. pout.append(s.rstrip())
  106. return \
  107. "Unspent outputs ({} UTC)\nSort order: {}\n\n{}\n\nTotal BTC: {}\n".format(
  108. make_timestr(), " ".join(sort_info), "\n".join(pout), total
  109. )
  110. def sort_and_view(unspent,opts):
  111. def s_amt(i): return i.amount
  112. def s_txid(i): return "%s %03s" % (i.txid,i.vout)
  113. def s_addr(i): return i.address
  114. def s_age(i): return i.confirmations
  115. def s_mmgen(i):
  116. if i.mmid:
  117. return "{}:{:>0{w}}".format(
  118. *i.mmid.split(":"), w=g.mmgen_idx_max_digits)
  119. else: return "G" + i.comment
  120. sort,group,show_days,show_mmaddr,reverse = "age",False,False,True,True
  121. unspent.sort(key=s_age,reverse=reverse) # Reverse age sort by default
  122. total = trim_exponent(sum([i.amount for i in unspent]))
  123. max_acct_len = max([len(i.mmid+" "+i.comment) for i in unspent])
  124. hdr_fmt = "UNSPENT OUTPUTS (sort order: %s) Total BTC: %s"
  125. options_msg = """
  126. Sort options: [t]xid, [a]mount, a[d]dress, [A]ge, [r]everse, [M]mgen addr
  127. Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen
  128. """.strip()
  129. prompt = \
  130. "('q' = quit sorting, 'p' = print to file, 'v' = pager view, 'w' = wide view): "
  131. mmid_w = max(len(i.mmid) for i in unspent)
  132. from copy import deepcopy
  133. from mmgen.term import get_terminal_size
  134. write_to_file_msg = ""
  135. msg("")
  136. while True:
  137. cols = get_terminal_size()[0]
  138. if cols < g.min_screen_width:
  139. msg("%s-txcreate requires a screen at least %s characters wide" %
  140. (g.proj_name.lower(),g.min_screen_width))
  141. sys.exit(2)
  142. addr_w = min(34+((1+max_acct_len) if show_mmaddr else 0),cols-46)
  143. acct_w = min(max_acct_len, max(24,int(addr_w-10)))
  144. btaddr_w = addr_w - acct_w - 1
  145. tx_w = max(11,min(64, cols-addr_w-32))
  146. txdots = "..." if tx_w < 64 else ""
  147. fs = " %-4s %-" + str(tx_w) + "s %-2s %-" + str(addr_w) + "s %-13s %-s"
  148. table_hdr = fs % ("Num","TX id Vout","","Address","Amount (BTC)",
  149. "Age(d)" if show_days else "Conf.")
  150. unsp = deepcopy(unspent)
  151. for i in unsp: i.skip = ""
  152. if group and (sort == "address" or sort == "txid"):
  153. for a,b in [(unsp[i],unsp[i+1]) for i in range(len(unsp)-1)]:
  154. if sort == "address" and a.address == b.address: b.skip = "addr"
  155. elif sort == "txid" and a.txid == b.txid: b.skip = "txid"
  156. for i in unsp:
  157. amt = str(trim_exponent(i.amount))
  158. lfill = 3 - len(amt.split(".")[0]) if "." in amt else 3 - len(amt)
  159. i.amt = " "*lfill + amt
  160. i.days = int(i.confirmations * g.mins_per_block / (60*24))
  161. i.age = i.days if show_days else i.confirmations
  162. if i.skip == "addr":
  163. i.addr = "|" + "." * 33
  164. else:
  165. if show_mmaddr:
  166. dots = ".." if btaddr_w < len(i.address) else ""
  167. i.addr = "%s%s %s" % (
  168. i.address[:btaddr_w-len(dots)],
  169. dots, (
  170. ("{:<{w}} ".format(i.mmid,w=mmid_w) if i.mmid else "")
  171. + i.comment)[:acct_w]
  172. )
  173. else:
  174. i.addr = i.address
  175. i.tx = " " * (tx_w-4) + "|..." if i.skip == "txid" \
  176. else i.txid[:tx_w-len(txdots)]+txdots
  177. sort_info = ["reverse"] if reverse else []
  178. sort_info.append(sort if sort else "unsorted")
  179. if group and (sort == "address" or sort == "txid"):
  180. sort_info.append("grouped")
  181. out = [hdr_fmt % (" ".join(sort_info), total), table_hdr]
  182. out += [fs % (str(n+1)+")",i.tx,i.vout,i.addr,i.amt,i.age)
  183. for n,i in enumerate(unsp)]
  184. msg("\n".join(out) +"\n\n" + write_to_file_msg + options_msg)
  185. write_to_file_msg = ""
  186. skip_prompt = False
  187. while True:
  188. reply = get_char(prompt, immed_chars="atDdAMrgmeqpvw")
  189. if reply == 'a': unspent.sort(key=s_amt); sort = "amount"
  190. elif reply == 't': unspent.sort(key=s_txid); sort = "txid"
  191. elif reply == 'D': show_days = not show_days
  192. elif reply == 'd': unspent.sort(key=s_addr); sort = "address"
  193. elif reply == 'A': unspent.sort(key=s_age); sort = "age"
  194. elif reply == 'M':
  195. unspent.sort(key=s_mmgen); sort = "mmgen"
  196. show_mmaddr = True
  197. elif reply == 'r':
  198. unspent.reverse()
  199. reverse = not reverse
  200. elif reply == 'g': group = not group
  201. elif reply == 'm': show_mmaddr = not show_mmaddr
  202. elif reply == 'e': pass
  203. elif reply == 'q': pass
  204. elif reply == 'p':
  205. d = format_unspent_outputs_for_printing(unsp,sort_info,total)
  206. of = "listunspent[%s].out" % ",".join(sort_info)
  207. write_to_file(of, d, opts,"",False,False)
  208. write_to_file_msg = "Data written to '%s'\n\n" % of
  209. elif reply == 'v':
  210. do_pager("\n".join(out))
  211. continue
  212. elif reply == 'w':
  213. data = format_unspent_outputs_for_printing(unsp,sort_info,total)
  214. do_pager(data)
  215. continue
  216. else:
  217. msg("\nInvalid input")
  218. continue
  219. break
  220. msg("\n")
  221. if reply == 'q': break
  222. return tuple(unspent)
  223. def select_outputs(unspent,prompt):
  224. while True:
  225. reply = my_raw_input(prompt).strip()
  226. if not reply: continue
  227. selected = parse_addr_idxs(reply,sep=None)
  228. if not selected: continue
  229. if selected[-1] > len(unspent):
  230. msg("Inputs must be less than %s" % len(unspent))
  231. continue
  232. return selected
  233. def get_acct_data_from_wallet(c,acct_data):
  234. # acct_data is global object initialized by caller
  235. vmsg_r("Getting account data from wallet...")
  236. accts,i = c.listaccounts(minconf=0,includeWatchonly=True),0
  237. for acct in accts:
  238. ma,comment = parse_mmgen_label(acct)
  239. if ma:
  240. i += 1
  241. addrlist = c.getaddressesbyaccount(acct)
  242. if len(addrlist) != 1:
  243. msg(wmsg['too_many_acct_addresses'] % acct)
  244. sys.exit(2)
  245. seed_id,idx = ma.split(":")
  246. if seed_id not in acct_data:
  247. acct_data[seed_id] = {}
  248. acct_data[seed_id][idx] = (addrlist[0],comment)
  249. vmsg("%s %s addresses found, %s accounts total" % (i,g.proj_name,len(accts)))
  250. def mmaddr2btcaddr_unspent(unspent,mmaddr):
  251. vmsg_r("Searching for {g.proj_name} address {m} in wallet...".format(g=g,m=mmaddr))
  252. m = [u for u in unspent if u.mmid == mmaddr]
  253. if len(m) == 0:
  254. vmsg("not found")
  255. return "",""
  256. elif len(m) > 1:
  257. msg(wmsg['too_many_acct_addresses'] % acct); sys.exit(2)
  258. else:
  259. vmsg("success (%s)" % m[0].address)
  260. return m[0].address, m[0].comment
  261. sys.exit()
  262. def mmaddr2btcaddr(c,mmaddr,acct_data,ail):
  263. # assume mmaddr has already been checked
  264. if not acct_data: get_acct_data_from_wallet(c,acct_data)
  265. btcaddr = mmaddr2btcaddr_addrdata(mmaddr,acct_data,"wallet")[0]
  266. # btcaddr,comment = mmaddr2btcaddr_unspent(us,mmaddr)
  267. if not btcaddr:
  268. if ail:
  269. sid,idx = mmaddr.split(":")
  270. btcaddr = ail.addrinfo(sid).btcaddr(int(idx))
  271. if btcaddr:
  272. msg(wmsg['addr_in_addrfile_only'].format(mmgenaddr=mmaddr))
  273. if not keypress_confirm("Continue anyway?"):
  274. sys.exit(1)
  275. else:
  276. msg(wmsg['addr_not_found'].format(mmgenaddr=mmaddr))
  277. sys.exit(2)
  278. else:
  279. msg(wmsg['addr_not_found_no_addrfile'].format(mmgenaddr=mmaddr))
  280. sys.exit(2)
  281. return btcaddr
  282. opts,cmd_args = parse_opts(sys.argv,help_data)
  283. if g.debug: show_opts_and_cmd_args(opts,cmd_args)
  284. if 'comment_file' in opts:
  285. comment = get_tx_comment_from_file(opts['comment_file'])
  286. c = connect_to_bitcoind()
  287. if not 'info' in opts:
  288. do_license_msg(immed=True)
  289. tx_out,acct_data,change_addr = {},{},""
  290. from mmgen.addr import AddrInfo,AddrInfoList
  291. ail = AddrInfoList()
  292. addrfiles = [a for a in cmd_args if get_extension(a) == g.addrfile_ext]
  293. cmd_args = set(cmd_args) - set(addrfiles)
  294. for a in addrfiles:
  295. check_infile(a)
  296. ail.add(AddrInfo(a))
  297. for a in cmd_args:
  298. if "," in a:
  299. a1,a2 = split2(a,",")
  300. if is_btc_addr(a1):
  301. btcaddr = a1
  302. elif is_mmgen_addr(a1):
  303. btcaddr = mmaddr2btcaddr(c,a1,acct_data,ail)
  304. else:
  305. msg("%s: unrecognized subargument in argument '%s'" % (a1,a))
  306. sys.exit(2)
  307. ret = normalize_btc_amt(a2)
  308. if ret:
  309. tx_out[btcaddr] = ret
  310. else:
  311. msg("%s: invalid amount in argument '%s'" % (a2,a))
  312. sys.exit(2)
  313. elif is_mmgen_addr(a) or is_btc_addr(a):
  314. if change_addr:
  315. msg("ERROR: More than one change address specified: %s, %s" %
  316. (change_addr, a))
  317. sys.exit(2)
  318. change_addr = a if is_btc_addr(a) else \
  319. mmaddr2btcaddr(c,a,acct_data,ail)
  320. tx_out[change_addr] = 0
  321. else:
  322. msg("%s: unrecognized argument" % a)
  323. sys.exit(2)
  324. if not tx_out:
  325. msg("At least one output must be specified on the command line")
  326. sys.exit(2)
  327. tx_fee = opts['tx_fee'] if 'tx_fee' in opts else g.tx_fee
  328. tx_fee = normalize_btc_amt(tx_fee)
  329. if tx_fee > g.max_tx_fee:
  330. msg("Transaction fee too large: %s > %s" % (tx_fee,g.max_tx_fee))
  331. sys.exit(2)
  332. if g.debug: show_opts_and_cmd_args(opts,cmd_args)
  333. if g.bogus_wallet_data: # for debugging purposes only
  334. us = eval(get_data_from_file(g.bogus_wallet_data))
  335. else:
  336. us = c.listunspent()
  337. # write_to_file("bogus_unspent.json", repr(us), opts); sys.exit()
  338. if not us: msg(wmsg['no_spendable_outputs']); sys.exit(2)
  339. for o in us:
  340. o.mmid,o.comment = parse_mmgen_label(o.account)
  341. del o.account
  342. unspent = sort_and_view(us,opts)
  343. total = trim_exponent(sum([i.amount for i in unspent]))
  344. msg("Total unspent: %s BTC (%s outputs)" % (total, len(unspent)))
  345. if 'info' in opts: sys.exit(0)
  346. send_amt = sum([tx_out[i] for i in tx_out.keys()])
  347. msg("Total amount to spend: %s%s" % (
  348. (send_amt or "Unknown")," BTC" if send_amt else ""))
  349. while True:
  350. sel_nums = select_outputs(unspent,
  351. "Enter a range or space-separated list of outputs to spend: ")
  352. msg("Selected output%s: %s" %
  353. (("" if len(sel_nums) == 1 else "s"), " ".join(str(i) for i in sel_nums))
  354. )
  355. sel_unspent = [unspent[i-1] for i in sel_nums]
  356. mmaddrs = set([i.mmid for i in sel_unspent])
  357. mmaddrs.discard("")
  358. if mmaddrs and len(mmaddrs) < len(sel_unspent):
  359. msg(wmsg['mixed_inputs'] % ", ".join(sorted(mmaddrs)))
  360. if not keypress_confirm("Accept?"):
  361. continue
  362. total_in = trim_exponent(sum([i.amount for i in sel_unspent]))
  363. change = trim_exponent(total_in - (send_amt + tx_fee))
  364. if change >= 0:
  365. prompt = "Transaction produces %s BTC in change. OK?" % change
  366. if keypress_confirm(prompt,default_yes=True):
  367. break
  368. else:
  369. msg(wmsg['not_enough_btc'] % change)
  370. if change > 0 and not change_addr:
  371. msg(wmsg['throwaway_change'] % change)
  372. sys.exit(2)
  373. if change_addr in tx_out and not change:
  374. msg("Warning: Change address will be unused as transaction produces no change")
  375. del tx_out[change_addr]
  376. for k,v in tx_out.items(): tx_out[k] = float(v)
  377. if change > 0: tx_out[change_addr] = float(change)
  378. tx_in = [{"txid":i.txid, "vout":i.vout} for i in sel_unspent]
  379. if g.debug:
  380. print "tx_in:", repr(tx_in)
  381. print "tx_out:", repr(tx_out)
  382. if 'comment_file' in opts:
  383. if keypress_confirm("Edit comment?",False):
  384. comment = get_tx_comment_from_user(comment)
  385. else:
  386. if keypress_confirm("Add a comment to transaction?",False):
  387. comment = get_tx_comment_from_user()
  388. else: comment = False
  389. tx_hex = c.createrawtransaction(tx_in,tx_out)
  390. qmsg("Transaction successfully created")
  391. amt = send_amt or change
  392. tx_id = make_chksum_6(unhexlify(tx_hex)).upper()
  393. metadata = tx_id, amt, make_timestamp()
  394. sel_unspent = [i.__dict__ for i in sel_unspent]
  395. def make_b2m_map(inputs_data,tx_out):
  396. m = [(d['address'],(d['mmid'],d['comment'])) for d in inputs_data if d['mmid']]
  397. d = ail.make_reverse_dict(tx_out.keys())
  398. d.update(m)
  399. return d
  400. b2m_map = make_b2m_map(sel_unspent,tx_out)
  401. prompt_and_view_tx_data(c,"View decoded transaction?",
  402. sel_unspent,tx_hex,b2m_map,comment,metadata)
  403. if keypress_confirm("Save transaction?",default_yes=False):
  404. outfile = "tx_%s[%s].%s" % (tx_id,amt,g.rawtx_ext)
  405. data = make_tx_data("{} {} {}".format(*metadata),
  406. tx_hex,sel_unspent,b2m_map,comment)
  407. write_to_file(outfile,data,opts,"transaction",False,True)
  408. else:
  409. msg("Transaction not saved")