main_txcreate.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522
  1. #!/usr/bin/env python
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2016 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 to and from MMGen- or non-MMGen
  20. inputs and outputs
  21. """
  22. from decimal import Decimal
  23. from mmgen.common import *
  24. from mmgen.tx import *
  25. from mmgen.term import get_char
  26. pnm = g.proj_name
  27. opts_data = {
  28. 'desc': 'Create a BTC transaction with outputs to specified addresses',
  29. 'usage': '[opts] <addr,amt> ... [change addr] [addr file] ...',
  30. 'options': """
  31. -h, --help Print this help message
  32. -a, --tx-fee-adj= f Adjust transaction fee by factor 'f' (see below)
  33. -c, --comment-file= f Source the transaction's comment from file 'f'
  34. -C, --tx-confs= c Desired number of confirmations (default: {g.tx_confs})
  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 (but see below))
  38. -i, --info Display unspent outputs and exit
  39. -q, --quiet Suppress warnings; overwrite files without prompting
  40. -v, --verbose Produce more verbose output
  41. """.format(g=g),
  42. 'notes': """
  43. Transaction inputs are chosen from a list of the user's unpent outputs
  44. via an interactive menu.
  45. If the transaction fee is not specified by the user, it will be calculated
  46. using bitcoind's "estimatefee" function for the default (or user-specified)
  47. number of confirmations. If "estimatefee" fails, the global default fee of
  48. {g.tx_fee} BTC will be used.
  49. Dynamic fees will be multiplied by the value of '--tx-fee-adj', if specified.
  50. Ages of transactions are approximate based on an average block discovery
  51. interval of {g.mins_per_block} minutes.
  52. All addresses on the command line can be either Bitcoin addresses or {pnm}
  53. addresses of the form <seed ID>:<index>.
  54. To send the value of all inputs (minus TX fee) to a single output, specify
  55. one address with no amount on the command line.
  56. """.format(g=g,pnm=pnm)
  57. }
  58. wmsg = {
  59. 'too_many_acct_addresses': """
  60. ERROR: More than one address found for account: '%s'.
  61. Your 'wallet.dat' file appears to have been altered by a non-{pnm} program.
  62. Please restore your tracking wallet from a backup or create a new one and
  63. re-import your addresses.
  64. """.strip().format(pnm=pnm),
  65. 'addr_in_addrfile_only': """
  66. Warning: output address {mmgenaddr} is not in the tracking wallet, which means
  67. its balance will not be tracked. You're strongly advised to import the address
  68. into your tracking wallet before broadcasting this transaction.
  69. """.strip(),
  70. 'addr_not_found': """
  71. No data for {pnm} address {mmgenaddr} could be found in either the tracking
  72. wallet or the supplied address file. Please import this address into your
  73. tracking wallet, or supply an address file for it on the command line.
  74. """.strip(),
  75. 'addr_not_found_no_addrfile': """
  76. No data for {pnm} address {mmgenaddr} could be found in the tracking wallet.
  77. Please import this address into your tracking wallet or supply an address file
  78. for it on the command line.
  79. """.strip(),
  80. 'no_spendable_outputs': """
  81. No spendable outputs found! Import addresses with balances into your
  82. watch-only wallet using '{pnm}-addrimport' and then re-run this program.
  83. """.strip(),
  84. 'mixed_inputs': """
  85. NOTE: This transaction uses a mixture of both {pnm} and non-{pnm} inputs, which
  86. makes the signing process more complicated. When signing the transaction, keys
  87. for the non-{pnm} inputs must be supplied to '{pnl}-txsign' in a file with the
  88. '--keys-from-file' option.
  89. Selected mmgen inputs: %s
  90. """.strip().format(pnm=pnm,pnl=pnm.lower()),
  91. 'not_enough_btc': """
  92. Not enough BTC in the inputs for this transaction (%s BTC)
  93. """.strip(),
  94. 'throwaway_change': """
  95. ERROR: This transaction produces change (%s BTC); however, no change address
  96. was specified.
  97. """.strip(),
  98. }
  99. def format_unspent_outputs_for_printing(out,sort_info,total):
  100. pfs = ' %-4s %-67s %-34s %-14s %-12s %-8s %-6s %s'
  101. pout = [pfs % ('Num','Tx ID,Vout','Address','{pnm} ID'.format(pnm=pnm),
  102. 'Amount(BTC)','Conf.','Age(d)', 'Comment')]
  103. for n,i in enumerate(out):
  104. addr = '=' if i['skip'] == 'addr' and 'grouped' in sort_info else i['address']
  105. tx = ' ' * 63 + '=' \
  106. if i['skip'] == 'txid' and 'grouped' in sort_info else str(i['txid'])
  107. s = pfs % (str(n+1)+')', tx+','+str(i['vout']),addr,
  108. i['mmid'],i['amt'].strip(),i['confirmations'],i['days'],i['comment'])
  109. pout.append(s.rstrip())
  110. return \
  111. 'Unspent outputs ({} UTC)\nSort order: {}\n\n{}\n\nTotal BTC: {}\n'.format(
  112. make_timestr(), ' '.join(sort_info), '\n'.join(pout), normalize_btc_amt(total)
  113. )
  114. def sort_and_view(unspent):
  115. def s_amt(i): return i['amount']
  116. def s_txid(i): return '%s %03s' % (i['txid'],i['vout'])
  117. def s_addr(i): return i['address']
  118. def s_age(i): return i['confirmations']
  119. def s_mmgen(i):
  120. if i['mmid']:
  121. return '{}:{:>0{w}}'.format(
  122. *i['mmid'].split(':'), w=g.mmgen_idx_max_digits)
  123. else: return 'G' + i['comment']
  124. sort,group,show_days,show_mmaddr,reverse = 'age',False,False,True,True
  125. unspent.sort(key=s_age,reverse=reverse) # Reverse age sort by default
  126. total = sum([i['amount'] for i in unspent])
  127. max_acct_len = max([len(i['mmid']+' '+i['comment']) for i in unspent])
  128. hdr_fmt = 'UNSPENT OUTPUTS (sort order: %s) Total BTC: %s'
  129. options_msg = """
  130. Sort options: [t]xid, [a]mount, a[d]dress, [A]ge, [r]everse, [M]mgen addr
  131. Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen
  132. """.strip()
  133. prompt = \
  134. "('q' = quit sorting, 'p' = print to file, 'v' = pager view, 'w' = wide view): "
  135. mmid_w = max(len(i['mmid']) for i in unspent)
  136. from copy import deepcopy
  137. from mmgen.term import get_terminal_size
  138. written_to_file_msg = ''
  139. msg('')
  140. while True:
  141. cols = get_terminal_size()[0]
  142. if cols < g.min_screen_width:
  143. die(2,
  144. '{pnl}-txcreate requires a screen at least {w} characters wide'.format(
  145. pnl=pnm.lower(),w=g.min_screen_width))
  146. addr_w = min(34+((1+max_acct_len) if show_mmaddr else 0),cols-46)
  147. acct_w = min(max_acct_len, max(24,int(addr_w-10)))
  148. btaddr_w = addr_w - acct_w - 1
  149. tx_w = max(11,min(64, cols-addr_w-32))
  150. txdots = ('','...')[tx_w < 64]
  151. fs = ' %-4s %-' + str(tx_w) + 's %-2s %-' + str(addr_w) + 's %-13s %-s'
  152. table_hdr = fs % ('Num','TX id Vout','','Address','Amount (BTC)',
  153. ('Conf.','Age(d)')[show_days])
  154. unsp = deepcopy(unspent)
  155. for i in unsp: i['skip'] = ''
  156. if group and (sort == 'address' or sort == 'txid'):
  157. for a,b in [(unsp[i],unsp[i+1]) for i in range(len(unsp)-1)]:
  158. if sort == 'address' and a['address'] == b['address']: b['skip'] = 'addr'
  159. elif sort == 'txid' and a['txid'] == b['txid']: b['skip'] = 'txid'
  160. for i in unsp:
  161. amt = str(normalize_btc_amt(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'] * g.mins_per_block / (60*24))
  165. i['age'] = i['days'] if show_days else i['confirmations']
  166. addr_disp = (i['address'],'|' + '.'*33)[i['skip']=='addr']
  167. mmid_disp = (i['mmid'],'.'*len(i['mmid']))[i['skip']=='addr']
  168. if show_mmaddr:
  169. dots = ('','..')[btaddr_w < len(i['address'])]
  170. i['addr'] = '%s%s %s' % (
  171. addr_disp[:btaddr_w-len(dots)],
  172. dots, (
  173. ('{:<{w}} '.format(mmid_disp,w=mmid_w) if i['mmid'] else '')
  174. + i['comment'])[:acct_w]
  175. )
  176. else:
  177. i['addr'] = addr_disp
  178. i['tx'] = ' ' * (tx_w-4) + '|...' if i['skip'] == 'txid' \
  179. else i['txid'][:tx_w-len(txdots)]+txdots
  180. sort_info = ([],['reverse'])[reverse]
  181. sort_info.append(sort if sort else 'unsorted')
  182. if group and (sort == 'address' or sort == 'txid'):
  183. sort_info.append('grouped')
  184. out = [hdr_fmt % (' '.join(sort_info), normalize_btc_amt(total)), table_hdr]
  185. out += [fs % (str(n+1)+')',i['tx'],i['vout'],i['addr'],i['amt'],i['age'])
  186. for n,i in enumerate(unsp)]
  187. msg('\n'.join(out) +'\n\n' + written_to_file_msg + options_msg)
  188. written_to_file_msg = ''
  189. skip_prompt = False
  190. while True:
  191. reply = get_char(prompt, immed_chars='atDdAMrgmeqpvw')
  192. if reply == 'a': unspent.sort(key=s_amt); sort = 'amount'
  193. elif reply == 't': unspent.sort(key=s_txid); sort = 'txid'
  194. elif reply == 'D': show_days = not show_days
  195. elif reply == 'd': unspent.sort(key=s_addr); sort = 'address'
  196. elif reply == 'A': unspent.sort(key=s_age); sort = 'age'
  197. elif reply == 'M':
  198. unspent.sort(key=s_mmgen); sort = 'mmgen'
  199. show_mmaddr = True
  200. elif reply == 'r':
  201. unspent.reverse()
  202. reverse = not reverse
  203. elif reply == 'g': group = not group
  204. elif reply == 'm': show_mmaddr = not show_mmaddr
  205. elif reply == 'e': pass
  206. elif reply == 'q': pass
  207. elif reply == 'p':
  208. d = format_unspent_outputs_for_printing(unsp,sort_info,total)
  209. of = 'listunspent[%s].out' % ','.join(sort_info)
  210. write_data_to_file(of,d,'unspent outputs listing')
  211. written_to_file_msg = "Data written to '%s'\n\n" % of
  212. elif reply == 'v':
  213. do_pager('\n'.join(out))
  214. continue
  215. elif reply == 'w':
  216. data = format_unspent_outputs_for_printing(unsp,sort_info,total)
  217. do_pager(data)
  218. continue
  219. else:
  220. msg('\nInvalid input')
  221. continue
  222. break
  223. msg('\n')
  224. if reply == 'q': break
  225. return tuple(unspent)
  226. def select_outputs(unspent,prompt):
  227. while True:
  228. reply = my_raw_input(prompt).strip()
  229. if not reply: continue
  230. selected = parse_addr_idxs(reply,sep=None)
  231. if not selected: continue
  232. if selected[-1] > len(unspent):
  233. msg('Inputs must be less than %s' % len(unspent))
  234. continue
  235. return selected
  236. def mmaddr2btcaddr_unspent(unspent,mmaddr):
  237. vmsg_r('Searching for {pnm} address {m} in wallet...'.format(pnm=pnm,m=mmaddr))
  238. m = [u for u in unspent if u['mmid'] == mmaddr]
  239. if len(m) == 0:
  240. vmsg('not found')
  241. return '',''
  242. elif len(m) > 1:
  243. die(2,wmsg['too_many_acct_addresses'] % acct)
  244. else:
  245. vmsg('success (%s)' % m[0].address)
  246. return m[0].address, m[0].comment
  247. def mmaddr2btcaddr(c,mmaddr,ail_w,ail_f):
  248. # assume mmaddr has already been checked
  249. btc_addr = ail_w.mmaddr2btcaddr(mmaddr)
  250. if not btc_addr:
  251. if ail_f:
  252. btc_addr = ail_f.mmaddr2btcaddr(mmaddr)
  253. if btc_addr:
  254. msg(wmsg['addr_in_addrfile_only'].format(mmgenaddr=mmaddr))
  255. if not keypress_confirm('Continue anyway?'):
  256. sys.exit(1)
  257. else:
  258. die(2,wmsg['addr_not_found'].format(pnm=pnm,mmgenaddr=mmaddr))
  259. else:
  260. die(2,wmsg['addr_not_found_no_addrfile'].format(pnm=pnm,mmgenaddr=mmaddr))
  261. return btc_addr
  262. def get_fee_estimate():
  263. if 'tx_fee' in opt.set_by_user:
  264. return None
  265. else:
  266. ret = c.estimatefee(opt.tx_confs)
  267. if ret != -1:
  268. return ret
  269. else:
  270. m = """
  271. Fee estimation failed!
  272. Your possible courses of action (from best to worst):
  273. 1) Re-run script with a different '--tx-confs' parameter (now '{c}')
  274. 2) Re-run script with the '--tx-fee' option (specify fee manually)
  275. 3) Accept the global default fee of {f} BTC
  276. Accept the global default fee of {f} BTC?
  277. """.format(c=opt.tx_confs,f=opt.tx_fee).strip()
  278. if keypress_confirm(m):
  279. return None
  280. else:
  281. die(1,'Exiting at user request')
  282. # main(): execution begins here
  283. def get_fee_estimate():
  284. if 'tx_fee' in opt.set_by_user:
  285. return None
  286. else:
  287. ret = c.estimatefee(opt.tx_confs)
  288. if ret != -1:
  289. return ret
  290. else:
  291. m = """
  292. Fee estimation failed!
  293. Your possible courses of action (from best to worst):
  294. 1) Re-run script with a different '--tx-confs' parameter (now '{c}')
  295. 2) Re-run script with the '--tx-fee' option (specify fee manually)
  296. 3) Accept the global default fee of {f} BTC
  297. Accept the global default fee of {f} BTC?
  298. """.format(c=opt.tx_confs,f=opt.tx_fee).strip()
  299. if keypress_confirm(m):
  300. return None
  301. else:
  302. die(1,'Exiting at user request')
  303. # see: https://bitcoin.stackexchange.com/questions/1195/how-to-calculate-transaction-size-before-sending
  304. def get_tx_size_and_fee(inputs,outputs):
  305. tx_size = len(inputs)*180 + len(outputs)*34 + 10
  306. if fee_estimate:
  307. ftype,fee = 'Calculated','{:.8f}'.format(fee_estimate*opt.tx_fee_adj*tx_size / 1024)
  308. else:
  309. ftype,fee = 'User-selected',opt.tx_fee
  310. if not keypress_confirm('{} TX fee: {} BTC. OK?'.format(ftype,fee),default_yes=True):
  311. while True:
  312. ufee = my_raw_input('Enter transaction fee: ')
  313. if normalize_btc_amt(ufee):
  314. if Decimal(ufee) > g.max_tx_fee:
  315. msg('{} BTC: fee too large (maximum fee: {} BTC)'.format(ufee,g.max_tx_fee))
  316. else:
  317. fee = ufee
  318. break
  319. vmsg('Inputs:{} Outputs:{} TX size:{}'.format(len(sel_unspent),len(tx_out),tx_size))
  320. vmsg('Fee estimate: {} (1024 bytes, {} confs)'.format(fee_estimate,opt.tx_confs))
  321. m = ('',' (after %sx adjustment)' % opt.tx_fee_adj)[opt.tx_fee_adj != 1]
  322. vmsg('TX fee: {}{}'.format(fee,m))
  323. return tx_size,normalize_btc_amt(fee)
  324. # main(): execution begins here
  325. cmd_args = opts.init(opts_data)
  326. tx = MMGenTX()
  327. if opt.comment_file: tx.add_comment(opt.comment_file)
  328. c = bitcoin_connection()
  329. if not opt.info:
  330. do_license_msg(immed=True)
  331. addrfiles = [a for a in cmd_args if get_extension(a) == g.addrfile_ext]
  332. cmd_args = set(cmd_args) - set(addrfiles)
  333. from mmgen.addr import AddrInfo,AddrInfoList
  334. ail_f = AddrInfoList()
  335. for a in addrfiles:
  336. check_infile(a)
  337. ail_f.add(AddrInfo(a))
  338. ail_w = AddrInfoList(bitcoind_connection=c)
  339. for a in cmd_args:
  340. if ',' in a:
  341. a1,a2 = split2(a,',')
  342. if is_btc_addr(a1):
  343. btc_addr = a1
  344. elif is_mmgen_addr(a1):
  345. btc_addr = mmaddr2btcaddr(c,a1,ail_w,ail_f)
  346. else:
  347. die(2,"%s: unrecognized subargument in argument '%s'" % (a1,a))
  348. btc_amt = convert_to_btc_amt(a2)
  349. if btc_amt:
  350. tx.add_output(btc_addr,btc_amt)
  351. else:
  352. die(2,"%s: invalid amount in argument '%s'" % (a2,a))
  353. elif is_mmgen_addr(a) or is_btc_addr(a):
  354. if tx.change_addr:
  355. die(2,'ERROR: More than one change address specified: %s, %s' %
  356. (change_addr, a))
  357. tx.change_addr = a if is_btc_addr(a) else mmaddr2btcaddr(c,a,ail_w,ail_f)
  358. tx.add_output(tx.change_addr,Decimal('0'))
  359. else:
  360. die(2,'%s: unrecognized argument' % a)
  361. if not tx.outputs:
  362. die(2,'At least one output must be specified on the command line')
  363. if opt.tx_fee > g.max_tx_fee:
  364. die(2,'Transaction fee too large: %s > %s' % (opt.tx_fee,g.max_tx_fee))
  365. fee_estimate = get_fee_estimate()
  366. if g.bogus_wallet_data: # for debugging purposes only
  367. us = eval(get_data_from_file(g.bogus_wallet_data))
  368. else:
  369. us = c.listunspent()
  370. # write_data_to_file('bogus_unspent.json', repr(us), 'bogus unspent data')
  371. # sys.exit()
  372. if not us:
  373. die(2,wmsg['no_spendable_outputs'])
  374. for o in us:
  375. o['mmid'],o['comment'] = parse_mmgen_label(o['account'])
  376. del o['account']
  377. unspent = sort_and_view(us)
  378. total = sum([i['amount'] for i in unspent])
  379. msg('Total unspent: %s BTC (%s outputs)' % (normalize_btc_amt(total), len(unspent)))
  380. if opt.info: sys.exit()
  381. tx.send_amt = tx.sum_outputs()
  382. msg('Total amount to spend: %s' % ('Unknown','%s BTC'%tx.send_amt,)[bool(tx.send_amt)])
  383. while True:
  384. sel_nums = select_outputs(unspent,
  385. 'Enter a range or space-separated list of outputs to spend: ')
  386. msg('Selected output%s: %s' % (
  387. ('s','')[len(sel_nums)==1],
  388. ' '.join(str(i) for i in sel_nums)
  389. ))
  390. sel_unspent = [unspent[i-1] for i in sel_nums]
  391. mmaddrs = set([i['mmid'] for i in sel_unspent])
  392. if '' in mmaddrs and len(mmaddrs) > 1:
  393. mmaddrs.discard('')
  394. msg(wmsg['mixed_inputs'] % ', '.join(sorted(mmaddrs)))
  395. if not keypress_confirm('Accept?'):
  396. continue
  397. tx.copy_inputs(sel_unspent) # makes tx.inputs
  398. tx.calculate_size_and_fee(fee_estimate) # sets tx.size, tx.fee
  399. change_amt = tx.sum_inputs() - tx.send_amt - tx.fee
  400. if change_amt >= 0:
  401. prompt = 'Transaction produces %s BTC in change. OK?' % change_amt
  402. if keypress_confirm(prompt,default_yes=True):
  403. break
  404. else:
  405. msg(wmsg['not_enough_btc'] % change_amt)
  406. if change_amt > 0:
  407. if not tx.change_addr:
  408. die(2,wmsg['throwaway_change'] % change_amt)
  409. tx.add_output(tx.change_addr,change_amt)
  410. elif tx.change_addr:
  411. msg('Warning: Change address will be unused as transaction produces no change')
  412. tx.del_output(tx.change_addr)
  413. if not tx.send_amt:
  414. tx.send_amt = change_amt
  415. dmsg('tx: %s' % tx)
  416. tx.add_comment() # edits an existing comment
  417. tx.create_raw(c) # creates tx.hex, tx.txid
  418. tx.add_mmaddrs_to_outputs(ail_w,ail_f)
  419. tx.add_timestamp()
  420. tx.add_blockcount(c)
  421. qmsg('Transaction successfully created')
  422. dmsg('TX (final): %s' % tx)
  423. tx.view_with_prompt('View decoded transaction?')
  424. tx.write_to_file(ask_write_default_yes=False)