Browse Source

object-oriented tracking wallet code
updates for oo tx code
updates for test/test.py

philemon 9 years ago
parent
commit
5ca6412b23
13 changed files with 393 additions and 332 deletions
  1. 10 4
      mmgen/addr.py
  2. 1 2
      mmgen/main_tool.py
  3. 9 203
      mmgen/main_txcreate.py
  4. 1 3
      mmgen/main_txsend.py
  5. 30 31
      mmgen/main_txsign.py
  6. 10 1
      mmgen/rpc.py
  7. 16 4
      mmgen/tool.py
  8. 220 0
      mmgen/tw.py
  9. 22 21
      mmgen/tx.py
  10. 12 12
      mmgen/util.py
  11. 6 7
      scripts/tx-old2new.py
  12. 1 0
      setup.py
  13. 55 44
      test/test.py

+ 10 - 4
mmgen/addr.py

@@ -31,6 +31,12 @@ from mmgen.obj import *
 pnm = g.proj_name
 pnm = g.proj_name
 
 
 addrmsgs = {
 addrmsgs = {
+	'too_many_acct_addresses': """
+ERROR: More than one address found for account: '%s'.
+Your 'wallet.dat' file appears to have been altered by a non-{pnm} program.
+Please restore your tracking wallet from a backup or create a new one and
+re-import your addresses.
+""".strip().format(pnm=pnm),
 	'addrfile_header': """
 	'addrfile_header': """
 # {pnm} address file
 # {pnm} address file
 #
 #
@@ -228,13 +234,14 @@ class AddrInfoList(MMGenObject):
 		vmsg_r('Getting account data from wallet...')
 		vmsg_r('Getting account data from wallet...')
 		accts = c.listaccounts(0,True)
 		accts = c.listaccounts(0,True)
 		data,i = {},0
 		data,i = {},0
-		for acct in accts:
+		alists = c.getaddressesbyaccount([[k] for k in accts],batch=True)
+		for acct,addrlist in zip(accts,alists):
 			ma,comment = parse_mmgen_label(acct)
 			ma,comment = parse_mmgen_label(acct)
 			if ma:
 			if ma:
 				i += 1
 				i += 1
-				addrlist = c.getaddressesbyaccount(acct)
+#				addrlist = c.getaddressesbyaccount(acct)
 				if len(addrlist) != 1:
 				if len(addrlist) != 1:
-					die(2,wmsg['too_many_acct_addresses'] % acct)
+					die(2,addrmsgs['too_many_acct_addresses'] % acct)
 				seed_id,idx = ma.split(':')
 				seed_id,idx = ma.split(':')
 				if seed_id not in data:
 				if seed_id not in data:
 					data[seed_id] = []
 					data[seed_id] = []
@@ -380,7 +387,6 @@ class AddrInfo(MMGenObject):
 
 
 		# Header
 		# Header
 		out = []
 		out = []
-		from mmgen.addr import addrmsgs
 		k = ('addrfile_header','keyfile_header')[status[0]==0]
 		k = ('addrfile_header','keyfile_header')[status[0]==0]
 		out.append(addrmsgs[k]+'\n')
 		out.append(addrmsgs[k]+'\n')
 		if self.checksum:
 		if self.checksum:

+ 1 - 2
mmgen/main_tool.py

@@ -39,8 +39,7 @@ opts_data = {
 	'notes': """
 	'notes': """
 
 
 COMMANDS:{}
 COMMANDS:{}
-Type '{} usage <command> for usage information on a particular
-command
+Type '{} help <command> for help on a particular command
 """.format(tool.cmd_help,g.prog_name)
 """.format(tool.cmd_help,g.prog_name)
 }
 }
 
 

+ 9 - 203
mmgen/main_txcreate.py

@@ -25,7 +25,7 @@ from decimal import Decimal
 
 
 from mmgen.common import *
 from mmgen.common import *
 from mmgen.tx import *
 from mmgen.tx import *
-from mmgen.term import get_char
+from mmgen.tw import *
 
 
 pnm = g.proj_name
 pnm = g.proj_name
 
 
@@ -68,12 +68,6 @@ one address with no amount on the command line.
 }
 }
 
 
 wmsg = {
 wmsg = {
-	'too_many_acct_addresses': """
-ERROR: More than one address found for account: '%s'.
-Your 'wallet.dat' file appears to have been altered by a non-{pnm} program.
-Please restore your tracking wallet from a backup or create a new one and
-re-import your addresses.
-""".strip().format(pnm=pnm),
 	'addr_in_addrfile_only': """
 	'addr_in_addrfile_only': """
 Warning: output address {mmgenaddr} is not in the tracking wallet, which means
 Warning: output address {mmgenaddr} is not in the tracking wallet, which means
 its balance will not be tracked.  You're strongly advised to import the address
 its balance will not be tracked.  You're strongly advised to import the address
@@ -88,10 +82,6 @@ tracking wallet, or supply an address file for it on the command line.
 No data for {pnm} address {mmgenaddr} could be found in the tracking wallet.
 No data for {pnm} address {mmgenaddr} could be found in the tracking wallet.
 Please import this address into your tracking wallet or supply an address file
 Please import this address into your tracking wallet or supply an address file
 for it on the command line.
 for it on the command line.
-""".strip(),
-	'no_spendable_outputs': """
-No spendable outputs found!  Import addresses with balances into your
-watch-only wallet using '{pnm}-addrimport' and then re-run this program.
 """.strip(),
 """.strip(),
 	'mixed_inputs': """
 	'mixed_inputs': """
 NOTE: This transaction uses a mixture of both {pnm} and non-{pnm} inputs, which
 NOTE: This transaction uses a mixture of both {pnm} and non-{pnm} inputs, which
@@ -110,163 +100,6 @@ was specified.
 """.strip(),
 """.strip(),
 }
 }
 
 
-def format_unspent_outputs_for_printing(out,sort_info,total):
-
-	pfs  = ' %-4s %-67s %-34s %-14s %-12s %-8s %-6s %s'
-	pout = [pfs % ('Num','Tx ID,Vout','Address','{pnm} ID'.format(pnm=pnm),
-		'Amount(BTC)','Conf.','Age(d)', 'Comment')]
-
-	for n,i in enumerate(out):
-		addr = '=' if i['skip'] == 'addr' and 'grouped' in sort_info else i['address']
-		tx = ' ' * 63 + '=' \
-			if i['skip'] == 'txid' and 'grouped' in sort_info else str(i['txid'])
-
-		s = pfs % (str(n+1)+')', tx+','+str(i['vout']),addr,
-				i['mmid'],i['amt'].strip(),i['confirmations'],i['days'],i['comment'])
-		pout.append(s.rstrip())
-
-	return \
-'Unspent outputs ({} UTC)\nSort order: {}\n\n{}\n\nTotal BTC: {}\n'.format(
-		make_timestr(), ' '.join(sort_info), '\n'.join(pout), normalize_btc_amt(total)
-	)
-
-
-def sort_and_view(unspent):
-
-	def s_amt(i):   return i['amount']
-	def s_txid(i):  return '%s %03s' % (i['txid'],i['vout'])
-	def s_addr(i):  return i['address']
-	def s_age(i):   return i['confirmations']
-	def s_mmgen(i):
-		if i['mmid']:
-			return '{}:{:>0{w}}'.format(
-				*i['mmid'].split(':'), w=g.mmgen_idx_max_digits)
-		else: return 'G' + i['comment']
-
-	sort,group,show_days,show_mmaddr,reverse = 'age',False,False,True,True
-	unspent.sort(key=s_age,reverse=reverse) # Reverse age sort by default
-
-	total = sum([i['amount'] for i in unspent])
-	max_acct_len = max([len(i['mmid']+' '+i['comment']) for i in unspent])
-
-	hdr_fmt   = 'UNSPENT OUTPUTS (sort order: %s)  Total BTC: %s'
-	options_msg = """
-Sort options: [t]xid, [a]mount, a[d]dress, [A]ge, [r]everse, [M]mgen addr
-Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen
-""".strip()
-	prompt = \
-"('q' = quit sorting, 'p' = print to file, 'v' = pager view, 'w' = wide view): "
-
-	mmid_w = max(len(i['mmid']) for i in unspent)
-	from copy import deepcopy
-	from mmgen.term import get_terminal_size
-
-	written_to_file_msg = ''
-	msg('')
-
-	while True:
-		cols = get_terminal_size()[0]
-		if cols < g.min_screen_width:
-			die(2,
-	'{pnl}-txcreate requires a screen at least {w} characters wide'.format(
-					pnl=pnm.lower(),w=g.min_screen_width))
-
-		addr_w = min(34+((1+max_acct_len) if show_mmaddr else 0),cols-46)
-		acct_w   = min(max_acct_len, max(24,int(addr_w-10)))
-		btaddr_w = addr_w - acct_w - 1
-		tx_w = max(11,min(64, cols-addr_w-32))
-		txdots = ('','...')[tx_w < 64]
-		fs = ' %-4s %-' + str(tx_w) + 's %-2s %-' + str(addr_w) + 's %-13s %-s'
-		table_hdr = fs % ('Num','TX id  Vout','','Address','Amount (BTC)',
-							('Conf.','Age(d)')[show_days])
-
-		unsp = deepcopy(unspent)
-		for i in unsp: i['skip'] = ''
-		if group and (sort == 'address' or sort == 'txid'):
-			for a,b in [(unsp[i],unsp[i+1]) for i in range(len(unsp)-1)]:
-				if sort == 'address' and a['address'] == b['address']: b['skip'] = 'addr'
-				elif sort == 'txid' and a['txid'] == b['txid']:        b['skip'] = 'txid'
-
-		for i in unsp:
-			amt = str(normalize_btc_amt(i['amount']))
-			lfill = 3 - len(amt.split('.')[0]) if '.' in amt else 3 - len(amt)
-			i['amt'] = ' '*lfill + amt
-			i['days'] = int(i['confirmations'] * g.mins_per_block / (60*24))
-			i['age'] = i['days'] if show_days else i['confirmations']
-
-			addr_disp = (i['address'],'|' + '.'*33)[i['skip']=='addr']
-			mmid_disp = (i['mmid'],'.'*len(i['mmid']))[i['skip']=='addr']
-
-			if show_mmaddr:
-				dots = ('','..')[btaddr_w < len(i['address'])]
-				i['addr'] = '%s%s %s' % (
-					addr_disp[:btaddr_w-len(dots)],
-					dots, (
-					('{:<{w}} '.format(mmid_disp,w=mmid_w) if i['mmid'] else '')
-						+ i['comment'])[:acct_w]
-					)
-			else:
-				i['addr'] = addr_disp
-
-			i['tx'] = ' ' * (tx_w-4) + '|...' if i['skip'] == 'txid' \
-					else i['txid'][:tx_w-len(txdots)]+txdots
-
-		sort_info = ([],['reverse'])[reverse]
-		sort_info.append(sort if sort else 'unsorted')
-		if group and (sort == 'address' or sort == 'txid'):
-			sort_info.append('grouped')
-
-		out  = [hdr_fmt % (' '.join(sort_info), normalize_btc_amt(total)), table_hdr]
-		out += [fs % (str(n+1)+')',i['tx'],i['vout'],i['addr'],i['amt'],i['age'])
-					for n,i in enumerate(unsp)]
-
-		msg('\n'.join(out) +'\n\n' + written_to_file_msg + options_msg)
-		written_to_file_msg = ''
-
-		skip_prompt = False
-
-		while True:
-			reply = get_char(prompt, immed_chars='atDdAMrgmeqpvw')
-
-			if   reply == 'a': unspent.sort(key=s_amt);  sort = 'amount'
-			elif reply == 't': unspent.sort(key=s_txid); sort = 'txid'
-			elif reply == 'D': show_days = not show_days
-			elif reply == 'd': unspent.sort(key=s_addr); sort = 'address'
-			elif reply == 'A': unspent.sort(key=s_age);  sort = 'age'
-			elif reply == 'M':
-				unspent.sort(key=s_mmgen); sort = 'mmgen'
-				show_mmaddr = True
-			elif reply == 'r':
-				unspent.reverse()
-				reverse = not reverse
-			elif reply == 'g': group = not group
-			elif reply == 'm': show_mmaddr = not show_mmaddr
-			elif reply == 'e': pass
-			elif reply == 'q': pass
-			elif reply == 'p':
-				d = format_unspent_outputs_for_printing(unsp,sort_info,total)
-				of = 'listunspent[%s].out' % ','.join(sort_info)
-				write_data_to_file(of,d,'unspent outputs listing')
-				written_to_file_msg = "Data written to '%s'\n\n" % of
-			elif reply == 'v':
-				do_pager('\n'.join(out))
-				continue
-			elif reply == 'w':
-				data = format_unspent_outputs_for_printing(unsp,sort_info,total)
-				do_pager(data)
-				continue
-			else:
-				msg('\nInvalid input')
-				continue
-
-			break
-
-		msg('\n')
-		if reply == 'q': break
-
-	return tuple(unspent)
-
-
 def select_outputs(unspent,prompt):
 def select_outputs(unspent,prompt):
 
 
 	while True:
 	while True:
@@ -285,20 +118,7 @@ def select_outputs(unspent,prompt):
 		return selected
 		return selected
 
 
 
 
-def mmaddr2btcaddr_unspent(unspent,mmaddr):
-	vmsg_r('Searching for {pnm} address {m} in wallet...'.format(pnm=pnm,m=mmaddr))
-	m = [u for u in unspent if u['mmid'] == mmaddr]
-	if len(m) == 0:
-		vmsg('not found')
-		return '',''
-	elif len(m) > 1:
-		die(2,wmsg['too_many_acct_addresses'] % acct)
-	else:
-		vmsg('success (%s)' % m[0].address)
-		return m[0].address, m[0].comment
-
-
-def mmaddr2btcaddr(c,mmaddr,ail_w,ail_f):
+def mmaddr2baddr(c,mmaddr,ail_w,ail_f):
 
 
 	# assume mmaddr has already been checked
 	# assume mmaddr has already been checked
 	btc_addr = ail_w.mmaddr2btcaddr(mmaddr)
 	btc_addr = ail_w.mmaddr2btcaddr(mmaddr)
@@ -369,7 +189,7 @@ if not opt.info:
 			if is_btc_addr(a1):
 			if is_btc_addr(a1):
 				btc_addr = a1
 				btc_addr = a1
 			elif is_mmgen_addr(a1):
 			elif is_mmgen_addr(a1):
-				btc_addr = mmaddr2btcaddr(c,a1,ail_w,ail_f)
+				btc_addr = mmaddr2baddr(c,a1,ail_w,ail_f)
 			else:
 			else:
 				die(2,"%s: unrecognized subargument in argument '%s'" % (a1,a))
 				die(2,"%s: unrecognized subargument in argument '%s'" % (a1,a))
 
 
@@ -382,7 +202,7 @@ if not opt.info:
 			if tx.change_addr:
 			if tx.change_addr:
 				die(2,'ERROR: More than one change address specified: %s, %s' %
 				die(2,'ERROR: More than one change address specified: %s, %s' %
 						(change_addr, a))
 						(change_addr, a))
-			tx.change_addr = a if is_btc_addr(a) else mmaddr2btcaddr(c,a,ail_w,ail_f)
+			tx.change_addr = a if is_btc_addr(a) else mmaddr2baddr(c,a,ail_w,ail_f)
 			tx.add_output(tx.change_addr,Decimal('0'))
 			tx.add_output(tx.change_addr,Decimal('0'))
 		else:
 		else:
 			die(2,'%s: unrecognized argument' % a)
 			die(2,'%s: unrecognized argument' % a)
@@ -395,24 +215,10 @@ if not opt.info:
 
 
 	fee_estimate = get_fee_estimate()
 	fee_estimate = get_fee_estimate()
 
 
+tw = MMGenTrackingWallet()
+tw.view_and_sort()
+tw.display_total()
 
 
-if g.bogus_wallet_data:  # for debugging purposes only
-	us = eval(get_data_from_file(g.bogus_wallet_data))
-else:
-	us = c.listunspent()
-#	write_data_to_file('bogus_unspent.json', repr(us), 'bogus unspent data')
-#	sys.exit()
-
-if not us:
-	die(2,wmsg['no_spendable_outputs'])
-for o in us:
-	o['mmid'],o['comment'] = parse_mmgen_label(o['account'])
-	del o['account']
-unspent = sort_and_view(us)
-
-total = sum([i['amount'] for i in unspent])
-
-msg('Total unspent: %s BTC (%s outputs)' % (normalize_btc_amt(total), len(unspent)))
 if opt.info: sys.exit()
 if opt.info: sys.exit()
 
 
 tx.send_amt = tx.sum_outputs()
 tx.send_amt = tx.sum_outputs()
@@ -420,13 +226,13 @@ tx.send_amt = tx.sum_outputs()
 msg('Total amount to spend: %s' % ('Unknown','%s BTC'%tx.send_amt,)[bool(tx.send_amt)])
 msg('Total amount to spend: %s' % ('Unknown','%s BTC'%tx.send_amt,)[bool(tx.send_amt)])
 
 
 while True:
 while True:
-	sel_nums = select_outputs(unspent,
+	sel_nums = select_outputs(tw.unspent,
 			'Enter a range or space-separated list of outputs to spend: ')
 			'Enter a range or space-separated list of outputs to spend: ')
 	msg('Selected output%s: %s' % (
 	msg('Selected output%s: %s' % (
 			('s','')[len(sel_nums)==1],
 			('s','')[len(sel_nums)==1],
 			' '.join(str(i) for i in sel_nums)
 			' '.join(str(i) for i in sel_nums)
 		))
 		))
-	sel_unspent = [unspent[i-1] for i in sel_nums]
+	sel_unspent = [tw.unspent[i-1] for i in sel_nums]
 
 
 	mmaddrs = set([i['mmid'] for i in sel_unspent])
 	mmaddrs = set([i['mmid'] for i in sel_unspent])
 
 

+ 1 - 3
mmgen/main_txsend.py

@@ -44,9 +44,7 @@ else: opts.usage()
 
 
 do_license_msg()
 do_license_msg()
 
 
-tx = MMGenTX()
-
-tx.parse_tx_file(infile,'signed transaction data')
+tx = MMGenTX(infile)
 
 
 c = bitcoin_connection()
 c = bitcoin_connection()
 
 

+ 30 - 31
mmgen/main_txsign.py

@@ -26,7 +26,7 @@ from mmgen.seed import SeedSource
 
 
 pnm = g.proj_name
 pnm = g.proj_name
 
 
-# Unneeded, as MMGen transactions cannot contain non-MMGen inputs
+# -w is unneeded - use bitcoin-cli walletdump instead
 # -w, --use-wallet-dat  Get keys from a running bitcoind
 # -w, --use-wallet-dat  Get keys from a running bitcoind
 opts_data = {
 opts_data = {
 	'desc':    'Sign Bitcoin transactions generated by {pnl}-txcreate'.format(pnl=pnm.lower()),
 	'desc':    'Sign Bitcoin transactions generated by {pnl}-txcreate'.format(pnl=pnm.lower()),
@@ -147,32 +147,32 @@ def generate_keys_for_mmgen_addrs(mmgen_addrs,infiles,saved_seeds):
 		d += [('{}:{}'.format(seed_id,e.idx),e.addr,e.wif) for e in ai.addrdata]
 		d += [('{}:{}'.format(seed_id,e.idx),e.addr,e.wif) for e in ai.addrdata]
 	return d
 	return d
 
 
-# function unneeded, as MMGen transactions cannot contain non-MMGen inputs
-def sign_tx_with_bitcoind_wallet(c,tx,tx_num_str,keys):
-	ok = tx.sign(c,tx_num_str,keys) # returns false on failure
-	if ok:
-		return ok
-	else:
-		msg('Using keys in wallet.dat as per user request')
-		prompt = 'Enter passphrase for bitcoind wallet: '
-		while True:
-			passwd = get_bitcoind_passphrase(prompt)
-			ret = c.walletpassphrase(passwd, 9999,ret_on_error=True)
-			if rpc_error(ret):
-				if rpc_errmsg(ret,'unencrypted wallet, but walletpassphrase was called'):
-					msg('Wallet is unencrypted'); break
-			else:
-				msg('Passphrase OK'); break
-
-		ok = tx.sign(c,tx_num_str,keys)
-
-		msg('Locking wallet')
-		ret = c.walletlock(ret_on_error=True)
-		if rpc_error(ret):
-			msg('Failed to lock wallet')
-
-		return ok
-
+# # function unneeded - use bitcoin-cli walletdump instead
+# def sign_tx_with_bitcoind_wallet(c,tx,tx_num_str,keys):
+# 	ok = tx.sign(c,tx_num_str,keys) # returns false on failure
+# 	if ok:
+# 		return ok
+# 	else:
+# 		msg('Using keys in wallet.dat as per user request')
+# 		prompt = 'Enter passphrase for bitcoind wallet: '
+# 		while True:
+# 			passwd = get_bitcoind_passphrase(prompt)
+# 			ret = c.walletpassphrase(passwd, 9999,ret_on_error=True)
+# 			if rpc_error(ret):
+# 				if rpc_errmsg(ret,'unencrypted wallet, but walletpassphrase was called'):
+# 					msg('Wallet is unencrypted'); break
+# 			else:
+# 				msg('Passphrase OK'); break
+#
+# 		ok = tx.sign(c,tx_num_str,keys)
+#
+# 		msg('Locking wallet')
+# 		ret = c.walletlock(ret_on_error=True)
+# 		if rpc_error(ret):
+# 			msg('Failed to lock wallet')
+#
+# 		return ok
+#
 def missing_keys_errormsg(addrs):
 def missing_keys_errormsg(addrs):
 	Msg("""
 	Msg("""
 A key file must be supplied (or use the '--use-wallet-dat' option)
 A key file must be supplied (or use the '--use-wallet-dat' option)
@@ -241,7 +241,7 @@ def check_maps_from_seeds(io_map,desc,infiles,saved_seeds,return_keys=False):
 			al,bl = 'generated seed:','tx file:'
 			al,bl = 'generated seed:','tx file:'
 			die(3,wmsg['mm2btc_mapping_error'] % (al,a,m[a],bl,b,io_map[b]))
 			die(3,wmsg['mm2btc_mapping_error'] % (al,a,m[a],bl,b,io_map[b]))
 	if return_keys:
 	if return_keys:
-		vmsg('Added %s wif key%s from seeds' % (len(d),suf(ret,'k')))
+		vmsg('Added %s wif key%s from seeds' % (len(d),suf(d,'k')))
 		return [e[2] for e in d]
 		return [e[2] for e in d]
 
 
 def get_keys_from_keylist(kldata,addrs):
 def get_keys_from_keylist(kldata,addrs):
@@ -287,8 +287,7 @@ for tx_num,tx_file in enumerate(tx_files,1):
 		msg('\nTransaction #%s of %s:' % (tx_num,len(tx_files)))
 		msg('\nTransaction #%s of %s:' % (tx_num,len(tx_files)))
 		tx_num_str = ' #%s' % tx_num
 		tx_num_str = ' #%s' % tx_num
 
 
-	tx = MMGenTX()
-	tx.parse_tx_file(tx_file,('transaction data','')[bool(opt.tx_id)])
+	tx = MMGenTX(tx_file)
 
 
 	if tx.check_signed(c):
 	if tx.check_signed(c):
 		die(1,'Transaction is already signed!')
 		die(1,'Transaction is already signed!')
@@ -335,6 +334,6 @@ for tx_num,tx_file in enumerate(tx_files,1):
 
 
 	if ok:
 	if ok:
 		tx.add_comment()   # edits an existing comment
 		tx.add_comment()   # edits an existing comment
-		tx.write_to_file(ask_write_default_yes=True)
+		tx.write_to_file(ask_write_default_yes=True,add_desc=tx_num_str)
 	else:
 	else:
 		die(3,'failed\nSome keys were missing.  Transaction %scould not be signed.' % tx_num_str)
 		die(3,'failed\nSome keys were missing.  Transaction %scould not be signed.' % tx_num_str)

+ 10 - 1
mmgen/rpc.py

@@ -78,8 +78,17 @@ class BitcoinRPCConnection(object):
 		dmsg('=== rpc.py debug ===')
 		dmsg('=== rpc.py debug ===')
 		dmsg('    RPC POST data ==> %s\n' % p)
 		dmsg('    RPC POST data ==> %s\n' % p)
 
 
+		from decimal import Decimal
+		class JSONDecEncoder(json.JSONEncoder):
+			def default(self, obj):
+				if isinstance(obj, Decimal):
+					return str(obj)
+				return json.JSONEncoder.default(self, obj)
+
+#		pp_msg(json.dumps(p,cls=JSONDecEncoder))
+
 		try:
 		try:
-			c.request('POST', '/', json.dumps(p), {
+			c.request('POST', '/', json.dumps(p,cls=JSONDecEncoder), {
 				'Host': self.host,
 				'Host': self.host,
 				'Authorization': 'Basic ' + base64.b64encode(self.auth_str)
 				'Authorization': 'Basic ' + base64.b64encode(self.auth_str)
 			})
 			})

+ 16 - 4
mmgen/tool.py

@@ -75,6 +75,7 @@ cmd_data = OrderedDict([
 	('listaddresses',["addrs [str='']",'minconf [int=1]','showempty [bool=False]','pager [bool=False]','showbtcaddrs [bool=False]']),
 	('listaddresses',["addrs [str='']",'minconf [int=1]','showempty [bool=False]','pager [bool=False]','showbtcaddrs [bool=False]']),
 	('getbalance',   ['minconf [int=1]']),
 	('getbalance',   ['minconf [int=1]']),
 	('txview',       ['<{} TX file> [str]'.format(pnm),'pager [bool=False]','terse [bool=False]']),
 	('txview',       ['<{} TX file> [str]'.format(pnm),'pager [bool=False]','terse [bool=False]']),
+	('twview',       ["sort [str='age']",'reverse [bool=False]','wide [bool=False]','pager [bool=False]']),
 
 
 	('add_label',       ['<{} address> [str]'.format(pnm),'<label> [str]']),
 	('add_label',       ['<{} address> [str]'.format(pnm),'<label> [str]']),
 	('remove_label',    ['<{} address> [str]'.format(pnm)]),
 	('remove_label',    ['<{} address> [str]'.format(pnm)]),
@@ -105,6 +106,7 @@ cmd_help = """
                   spendable/unspendable balances for individual {pnm} wallets
                   spendable/unspendable balances for individual {pnm} wallets
   listaddresses - list {pnm} addresses and their balances
   listaddresses - list {pnm} addresses and their balances
   txview        - show raw/signed {pnm} transaction in human-readable form
   txview        - show raw/signed {pnm} transaction in human-readable form
+  twview        - view tracking wallet
 
 
   General utilities:
   General utilities:
   hexdump      - encode data into formatted hexadecimal form (file or stdin)
   hexdump      - encode data into formatted hexadecimal form (file or stdin)
@@ -157,6 +159,10 @@ cmd_help = """
 
 
 def tool_usage(prog_name, command):
 def tool_usage(prog_name, command):
 	if command in cmd_data:
 	if command in cmd_data:
+		for line in cmd_help.split('\n'):
+			if '  ' + command in line:
+				c,h = line.split('-',1)
+				Msg('{}: {}'.format(c.strip(),h.strip()))
 		Msg('USAGE: %s %s %s' % (prog_name, command, ' '.join(cmd_data[command])))
 		Msg('USAGE: %s %s %s' % (prog_name, command, ' '.join(cmd_data[command])))
 	else:
 	else:
 		Msg("'%s': no such tool command" % command)
 		Msg("'%s': no such tool command" % command)
@@ -393,7 +399,7 @@ def listaddresses(addrs='',minconf=1,showempty=False,pager=False,showbtcaddrs=Fa
 					die(2,'duplicate BTC address ({}) for this MMGen address! ({})'.format(
 					die(2,'duplicate BTC address ({}) for this MMGen address! ({})'.format(
 							(d['address'], addrs[key][2])))
 							(d['address'], addrs[key][2])))
 			else:
 			else:
-				addrs[key] = [0,comment,d['address']]
+				addrs[key] = [Decimal('0'),comment,d['address']]
 			addrs[key][0] += d['amount']
 			addrs[key][0] += d['amount']
 			total += d['amount']
 			total += d['amount']
 
 
@@ -408,7 +414,7 @@ def listaddresses(addrs='',minconf=1,showempty=False,pager=False,showbtcaddrs=Fa
 				key = mmaddr.replace(':','_')
 				key = mmaddr.replace(':','_')
 				if key not in addrs:
 				if key not in addrs:
 					if showbtcaddrs: save_a.append([acct])
 					if showbtcaddrs: save_a.append([acct])
-					addrs[key] = [0,comment,'']
+					addrs[key] = [Decimal('0'),comment,'']
 
 
 		for acct,addr in zip(save_a,c.getaddressesbyaccount(save_a,batch=True)):
 		for acct,addr in zip(save_a,c.getaddressesbyaccount(save_a,batch=True)):
 			if len(addr) != 1:
 			if len(addr) != 1:
@@ -468,10 +474,16 @@ def getbalance(minconf=1):
 
 
 def txview(infile,pager=False,terse=False):
 def txview(infile,pager=False,terse=False):
 	c = bitcoin_connection()
 	c = bitcoin_connection()
-	tx = MMGenTX()
-	tx.parse_tx_file(infile,'transaction data')
+	tx = MMGenTX(infile)
 	tx.view(pager,pause=False,terse=terse)
 	tx.view(pager,pause=False,terse=terse)
 
 
+def twview(pager=False,reverse=False,wide=False,sort='age'):
+	from mmgen.tw import MMGenTrackingWallet
+	tw = MMGenTrackingWallet()
+	tw.do_sort(sort,reverse=reverse)
+	out = tw.format(wide=wide)
+	do_pager(out) if pager else sys.stdout.write(out)
+
 def add_label(mmaddr,label,remove=False):
 def add_label(mmaddr,label,remove=False):
 	if not is_mmgen_addr(mmaddr):
 	if not is_mmgen_addr(mmaddr):
 		die(1,'{a}: not a valid {pnm} address'.format(pnm=pnm,a=mmaddr))
 		die(1,'{a}: not a valid {pnm} address'.format(pnm=pnm,a=mmaddr))

+ 220 - 0
mmgen/tw.py

@@ -0,0 +1,220 @@
+#!/usr/bin/env python
+#
+# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
+# Copyright (C)2013-2016 Philemon <mmgen-py@yandex.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+"""
+tw: Tracking wallet methods for the MMGen suite
+"""
+
+from mmgen.common import *
+from mmgen.obj import *
+from mmgen.tx import parse_mmgen_label,normalize_btc_amt
+from mmgen.term import get_char
+
+class MMGenTrackingWallet(MMGenObject):
+
+	wmsg = {
+	'no_spendable_outputs': """
+No spendable outputs found!  Import addresses with balances into your
+watch-only wallet using '{}-addrimport' and then re-run this program.
+""".strip().format(g.proj_name)
+	}
+
+	sort_keys = 'address','age','amount','txid','mmaddr'
+	def s_address(self,i):  return i['address']
+	def s_age(self,i):      return 0 - i['confirmations']
+	def s_amount(self,i):   return i['amount']
+	def s_txid(self,i):     return '%s %03s' % (i['txid'],i['vout'])
+	def s_mmaddr(self,i):
+		if i['mmid']:
+			return '{}:{:>0{w}}'.format(
+				*i['mmid'].split(':'), w=g.mmgen_idx_max_digits)
+		else: return 'G' + i['comment']
+
+	def do_sort(self,key,reverse=None):
+		if key not in self.sort_keys:
+			fs = "'{}': invalid sort key.  Valid keys: [{}]"
+			die(2,fs.format(key,' '.join(self.sort_keys)))
+		if reverse == None: reverse = self.reverse
+		self.sort = key
+		self.unspent.sort(key=getattr(self,'s_'+key),reverse=reverse)
+
+	def sort_info(self,include_group=True):
+		ret = ([],['reverse'])[self.reverse]
+		ret.append(self.sort)
+		if include_group and self.group and (self.sort in ('address','txid')):
+			ret.append('grouped')
+		return ret
+
+	def __init__(self):
+		if g.bogus_wallet_data: # for debugging purposes only
+			us = eval(get_data_from_file(g.bogus_wallet_data))
+		else:
+			us = bitcoin_connection().listunspent()
+#		write_data_to_file('bogus_unspent.json', repr(us), 'bogus unspent data')
+#		sys.exit()
+
+		if not us: die(2,self.wmsg['no_spendable_outputs'])
+		for o in us:
+			o['mmid'],o['comment'] = parse_mmgen_label(o['account'])
+			del o['account']
+			o['skip'] = ''
+			amt = str(normalize_btc_amt(o['amount']))
+			lfill = 3 - len(amt.split('.')[0]) if '.' in amt else 3 - len(amt)
+			o['amt_fmt'] = ' '*lfill + amt
+			o['days'] = int(o['confirmations'] * g.mins_per_block / (60*24))
+
+		self.unspent  = us
+		self.fmt_display  = ''
+		self.fmt_print    = ''
+		self.cols         = None
+		self.reverse      = False
+		self.group        = False
+		self.show_days    = True
+		self.show_mmaddr  = True
+		self.do_sort('age')
+		self.total = sum([i['amount'] for i in self.unspent])
+
+	def set_cols(self):
+		from mmgen.term import get_terminal_size
+		self.cols = get_terminal_size()[0]
+		if self.cols < g.min_screen_width:
+			m = 'A screen at least {} characters wide is required to display the tracking wallet'
+			die(2,m.format(g.min_screen_width))
+
+	def display(self):
+		msg(self.format_for_display())
+
+	def format(self,wide=False):
+		return self.format_for_printing() if wide else self.format_for_display()
+
+	def format_for_display(self):
+		unspent = self.unspent
+		total = sum([i['amount'] for i in unspent])
+		mmid_w = max(len(i['mmid']) for i in unspent)
+		self.set_cols()
+
+		max_acct_len = max([len(i['mmid']+i['comment'])+1 for i in self.unspent])
+		addr_w = min(34+((1+max_acct_len) if self.show_mmaddr else 0),self.cols-46)
+		acct_w   = min(max_acct_len, max(24,int(addr_w-10)))
+		btaddr_w = addr_w - acct_w - 1
+		tx_w = max(11,min(64, self.cols-addr_w-32))
+		txdots = ('','...')[tx_w < 64]
+		fs = ' %-4s %-' + str(tx_w) + 's %-2s %-' + str(addr_w) + 's %-13s %-s'
+		table_hdr = fs % ('Num','TX id  Vout','','Address','Amount (BTC)',
+							('Conf.','Age(d)')[self.show_days])
+
+		from copy import deepcopy
+		unsp = deepcopy(unspent)
+		for i in unsp: i['skip'] = ''
+		if self.group and (self.sort in ('address','txid')):
+			for a,b in [(unsp[i],unsp[i+1]) for i in range(len(unsp)-1)]:
+				if self.sort == 'address' and a['address'] == b['address']: b['skip'] = 'addr'
+				elif self.sort == 'txid' and a['txid'] == b['txid']:        b['skip'] = 'txid'
+
+		for i in unsp:
+			addr_disp = (i['address'],'|' + '.'*33)[i['skip']=='addr']
+			mmid_disp = (i['mmid'],'.'*len(i['mmid']))[i['skip']=='addr']
+			if self.show_mmaddr:
+				dots = ('','..')[btaddr_w < len(i['address'])]
+				i['addr'] = '%s%s %s' % (
+					addr_disp[:btaddr_w-len(dots)],
+					dots, (
+					('{:<{w}} '.format(mmid_disp,w=mmid_w) if i['mmid'] else '')
+						+ i['comment'])[:acct_w]
+					)
+			else:
+				i['addr'] = addr_disp
+
+			i['tx'] = ' ' * (tx_w-4) + '|...' if i['skip'] == 'txid' \
+					else i['txid'][:tx_w-len(txdots)]+txdots
+
+		hdr_fmt   = 'UNSPENT OUTPUTS (sort order: %s)  Total BTC: %s'
+		out  = [hdr_fmt % (' '.join(self.sort_info()), normalize_btc_amt(total)), table_hdr]
+		out += [fs % (str(n+1)+')',i['tx'],i['vout'],i['addr'],i['amt_fmt'],
+					i['days'] if self.show_days else i['confirmations'])
+						for n,i in enumerate(unsp)]
+		self.fmt_display = '\n'.join(out)
+		return self.fmt_display
+
+	def format_for_printing(self):
+
+		total = sum([i['amount'] for i in self.unspent])
+		fs  = ' %-4s %-67s %-34s %-14s %-12s %-8s %-6s %s'
+		out = [fs % ('Num','Tx ID,Vout','Address','{} ID'.format(g.proj_name),
+			'Amount(BTC)','Conf.','Age(d)', 'Comment')]
+
+		for n,i in enumerate(self.unspent):
+			addr = '=' if i['skip'] == 'addr' and self.group else i['address']
+			tx = ' ' * 63 + '=' if i['skip'] == 'txid' and self.group else str(i['txid'])
+			s = fs % (str(n+1)+')', tx+','+str(i['vout']),addr,
+					i['mmid'],i['amt_fmt'].strip(),i['confirmations'],i['days'],i['comment'])
+			out.append(s.rstrip())
+
+		fs = 'Unspent outputs ({} UTC)\nSort order: {}\n\n{}\n\nTotal BTC: {}\n'
+		self.fmt_print = fs.format(
+				make_timestr(),
+				' '.join(self.sort_info(include_group=False)),
+				'\n'.join(out),
+				normalize_btc_amt(total))
+		return self.fmt_print
+
+	def display_total(self):
+		fs = '\nTotal unspent: %s BTC (%s outputs)'
+		msg(fs % (normalize_btc_amt(self.total), len(self.unspent)))
+
+	def view_and_sort(self):
+		from mmgen.term import do_pager
+		s = """
+Sort options: [t]xid, [a]mount, a[d]dress, [A]ge, [r]everse, [M]mgen addr
+Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen
+	""".strip()
+		self.display()
+		msg(s)
+
+		p = "('q' = quit sorting, 'p' = print to file, 'v' = pager view, 'w' = wide view): "
+		while True:
+			reply = get_char(p, immed_chars='atDdAMrgmeqpvw')
+			if   reply == 'a': self.do_sort('amount')
+			elif reply == 't': self.do_sort('txid')
+			elif reply == 'D': self.show_days = not self.show_days
+			elif reply == 'd': self.do_sort('address')
+			elif reply == 'A': self.do_sort('age')
+			elif reply == 'M': self.do_sort('mmaddr'); self.show_mmaddr = True
+			elif reply == 'r': self.unspent.reverse(); self.reverse = not self.reverse
+			elif reply == 'g': self.group = not self.group
+			elif reply == 'm': self.show_mmaddr = not self.show_mmaddr
+			elif reply == 'e': msg("\n%s\n%s\n%s" % (self.fmt_display,s,p))
+			elif reply == 'q': return self.unspent
+			elif reply == 'p':
+				of = 'listunspent[%s].out' % ','.join(self.sort_info(include_group=False))
+				write_data_to_file(of,self.format_for_printing(),'unspent outputs listing')
+				m = yellow("Data written to '%s'" % of)
+				msg('\n%s\n\n%s\n\n%s' % (self.fmt_display,m,s))
+				continue
+			elif reply == 'v':
+				do_pager(self.fmt_display)
+				continue
+			elif reply == 'w':
+				do_pager(self.format_for_printing())
+				continue
+			else:
+				msg('\nInvalid input')
+				continue
+
+			self.display()
+			msg(s)

+ 22 - 21
mmgen/tx.py

@@ -176,6 +176,9 @@ def mmaddr2btcaddr_addrdata(mmaddr,addr_data,source=''):
 from mmgen.obj import *
 from mmgen.obj import *
 
 
 class MMGenTX(MMGenObject):
 class MMGenTX(MMGenObject):
+	ext  = g.rawtx_ext
+	desc = 'transaction'
+
 	def __init__(self,filename=None):
 	def __init__(self,filename=None):
 		self.inputs      = []
 		self.inputs      = []
 		self.outputs     = {}
 		self.outputs     = {}
@@ -191,7 +194,10 @@ class MMGenTX(MMGenObject):
 		self.chksum      = ''
 		self.chksum      = ''
 		self.fmt_data    = ''
 		self.fmt_data    = ''
 		self.blockcount  = 0
 		self.blockcount  = 0
-		self.isSigned    = False
+		if filename:
+			if get_extension(filename) == g.sigtx_ext:
+				self.mark_signed()
+			self.parse_tx_file(filename)
 
 
 	def add_output(self,btcaddr,amt):
 	def add_output(self,btcaddr,amt):
 		self.outputs[btcaddr] = (amt,)
 		self.outputs[btcaddr] = (amt,)
@@ -261,17 +267,8 @@ class MMGenTX(MMGenObject):
 		return sum([i['amount'] for i in self.inputs])
 		return sum([i['amount'] for i in self.inputs])
 
 
 	def create_raw(self,c):
 	def create_raw(self,c):
-		inputs,outputs = [],{}
-		def dec2str(d):
-			tmp = {}
-			for k in d:
-				tmp[k] = str(d[k]) if type(d[k]) is Decimal else d[k]
-			return tmp
-		inputs = [dec2str(d) for d in self.inputs]
-		for k in self.outputs:
-			outputs[k] = str(self.outputs[k][0])
-#		mdie(inputs,outputs)
-		self.hex = c.createrawtransaction(inputs,outputs)
+		o = dict([(k,v[0]) for k,v in self.outputs.items()])
+		self.hex = c.createrawtransaction(self.inputs,o)
 		self.txid = make_chksum_6(unhexlify(self.hex)).upper()
 		self.txid = make_chksum_6(unhexlify(self.hex)).upper()
 
 
 # 	def make_b2m_map(self,ail_w,ail_f):
 # 	def make_b2m_map(self,ail_w,ail_f):
@@ -327,17 +324,22 @@ class MMGenTX(MMGenObject):
 		if sig_tx['complete']:
 		if sig_tx['complete']:
 			msg('OK')
 			msg('OK')
 			self.hex = sig_tx['hex']
 			self.hex = sig_tx['hex']
-			self.isSigned = True
+			self.mark_signed()
 			return True
 			return True
 		else:
 		else:
 			msg('failed\nBitcoind returned the following errors:')
 			msg('failed\nBitcoind returned the following errors:')
 			pp_msg(sig_tx['errors'])
 			pp_msg(sig_tx['errors'])
 			return False
 			return False
 
 
+	def mark_signed(self):
+		self.desc = 'signed transaction'
+		self.ext = g.sigtx_ext
+
 	def check_signed(self,c):
 	def check_signed(self,c):
 		d = c.decoderawtransaction(self.hex)
 		d = c.decoderawtransaction(self.hex)
-		self.isSigned = bool(d['vin'][0]['scriptSig']['hex'])
-		return self.isSigned
+		ret = bool(d['vin'][0]['scriptSig']['hex'])
+		if ret: self.mark_signed()
+		return ret
 
 
 	def send(self,c,bogus=False):
 	def send(self,c,bogus=False):
 		if bogus:
 		if bogus:
@@ -354,13 +356,12 @@ class MMGenTX(MMGenObject):
 			ask_write=ask_write,
 			ask_write=ask_write,
 			ask_write_default_yes=ask_write_default_yes)
 			ask_write_default_yes=ask_write_default_yes)
 
 
-	def write_to_file(self,ask_write=True,ask_write_default_yes=False):
+	def write_to_file(self,add_desc='',ask_write=True,ask_write_default_yes=False):
 		if ask_write == False:
 		if ask_write == False:
 			ask_write_default_yes=True
 			ask_write_default_yes=True
 		self.format()
 		self.format()
-		fn = '%s[%s].%s' % (self.txid,self.send_amt,
-			(g.rawtx_ext,g.sigtx_ext)[self.isSigned])
-		write_data_to_file(fn,self.fmt_data,'transaction',
+		fn = '%s[%s].%s' % (self.txid,self.send_amt,self.ext)
+		write_data_to_file(fn,self.fmt_data,self.desc+add_desc,
 			ask_write=ask_write,
 			ask_write=ask_write,
 			ask_write_default_yes=ask_write_default_yes)
 			ask_write_default_yes=ask_write_default_yes)
 
 
@@ -453,9 +454,9 @@ class MMGenTX(MMGenObject):
 
 
 		return out
 		return out
 
 
-	def parse_tx_file(self,infile,desc):
+	def parse_tx_file(self,infile):
 
 
-		self.parse_tx_data(get_lines_from_file(infile,desc))
+		self.parse_tx_data(get_lines_from_file(infile,self.desc+' data'))
 
 
 	def parse_tx_data(self,tx_data):
 	def parse_tx_data(self,tx_data):
 
 

+ 12 - 12
mmgen/util.py

@@ -70,6 +70,18 @@ def die(ev,s):
 def Die(ev,s):
 def Die(ev,s):
 	sys.stdout.write(s+'\n'); sys.exit(ev)
 	sys.stdout.write(s+'\n'); sys.exit(ev)
 
 
+def pp_format(d):
+	import pprint
+	return pprint.PrettyPrinter(indent=4).pformat(d)
+
+def pp_die(d):
+	import pprint
+	die(1,pprint.PrettyPrinter(indent=4).pformat(d))
+
+def pp_msg(d):
+	import pprint
+	msg(pprint.PrettyPrinter(indent=4).pformat(d))
+
 def is_mmgen_wallet_label(s):
 def is_mmgen_wallet_label(s):
 	if len(s) > g.max_wallet_label_len:
 	if len(s) > g.max_wallet_label_len:
 		msg('ERROR: wallet label length (%s chars) > maximum allowed (%s chars)' % (len(s),g.max_wallet_label_len))
 		msg('ERROR: wallet label length (%s chars) > maximum allowed (%s chars)' % (len(s),g.max_wallet_label_len))
@@ -804,15 +816,3 @@ def bitcoin_connection():
 	import mmgen.rpc
 	import mmgen.rpc
 	return mmgen.rpc.BitcoinRPCConnection(
 	return mmgen.rpc.BitcoinRPCConnection(
 				host,port,cfg[user],cfg[passwd],auth_cookie=auth_cookie)
 				host,port,cfg[user],cfg[passwd],auth_cookie=auth_cookie)
-
-def pp_format(d):
-	import pprint
-	return pprint.PrettyPrinter(indent=4).pformat(d)
-
-def pp_die(d):
-	import pprint
-	die(1,pprint.PrettyPrinter(indent=4).pformat(d))
-
-def pp_msg(d):
-	import pprint
-	msg(pprint.PrettyPrinter(indent=4).pformat(d))

+ 6 - 7
scripts/tx-old2new.py

@@ -99,18 +99,17 @@ tx.send_amt = Decimal(send_amt)
 
 
 c = bitcoin_connection()
 c = bitcoin_connection()
 
 
-tx.blockcount = find_block_by_time(c,tx.timestamp)
+tx.copy_inputs(inputs)
+if tx.check_signed(c):
+	msg('Transaction is signed')
 
 
 dec_tx = c.decoderawtransaction(tx.hex)
 dec_tx = c.decoderawtransaction(tx.hex)
-
-tx.copy_inputs(inputs)
 tx.outputs = dict([(i['scriptPubKey']['addresses'][0],(i['value'],)) for i in dec_tx['vout']])
 tx.outputs = dict([(i['scriptPubKey']['addresses'][0],(i['value'],)) for i in dec_tx['vout']])
+
+tx.blockcount = find_block_by_time(c,tx.timestamp)
+
 for k in tx.outputs:
 for k in tx.outputs:
 	if k in b2m_map:
 	if k in b2m_map:
 		tx.outputs[k] += b2m_map[k]
 		tx.outputs[k] += b2m_map[k]
 
 
-#tx.view_with_prompt('View decoded transaction?')
-#tx.format()
-#print(str(tx))
-# do_pager('OLD:\n' + pp_format(inputs) + '\nNEW:\n' + pp_format(tx.inputs))
 tx.write_to_file(ask_write=False)
 tx.write_to_file(ask_write=False)

+ 1 - 0
setup.py

@@ -47,6 +47,7 @@ setup(
 			'mmgen.test',
 			'mmgen.test',
 			'mmgen.tool',
 			'mmgen.tool',
 			'mmgen.tx',
 			'mmgen.tx',
+			'mmgen.tw',
 			'mmgen.util',
 			'mmgen.util',
 
 
 			'mmgen.main',
 			'mmgen.main',

+ 55 - 44
test/test.py

@@ -77,8 +77,8 @@ cfgs = {
 			pwfile:        'walletgen',
 			pwfile:        'walletgen',
 			'mmdat':       'walletgen',
 			'mmdat':       'walletgen',
 			'addrs':       'addrgen',
 			'addrs':       'addrgen',
-			'raw':         'txcreate',
-			'sig':         'txsign',
+			'rawtx':         'txcreate',
+			'sigtx':         'txsign',
 			'mmwords':     'export_mnemonic',
 			'mmwords':     'export_mnemonic',
 			'mmseed':      'export_seed',
 			'mmseed':      'export_seed',
 			'mmincog':     'export_incog',
 			'mmincog':     'export_incog',
@@ -96,8 +96,8 @@ cfgs = {
 		'dep_generators': {
 		'dep_generators': {
 			'mmdat':       'walletgen2',
 			'mmdat':       'walletgen2',
 			'addrs':       'addrgen2',
 			'addrs':       'addrgen2',
-			'raw':         'txcreate2',
-			'sig':         'txsign2',
+			'rawtx':         'txcreate2',
+			'sigtx':         'txsign2',
 			'mmwords':     'export_mnemonic2',
 			'mmwords':     'export_mnemonic2',
 		},
 		},
 	},
 	},
@@ -108,8 +108,8 @@ cfgs = {
 		'dep_generators': {
 		'dep_generators': {
 			'mmdat':       'walletgen3',
 			'mmdat':       'walletgen3',
 			'addrs':       'addrgen3',
 			'addrs':       'addrgen3',
-			'raw':         'txcreate3',
-			'sig':         'txsign3'
+			'rawtx':         'txcreate3',
+			'sigtx':         'txsign3'
 		},
 		},
 	},
 	},
 	'4': {
 	'4': {
@@ -121,8 +121,8 @@ cfgs = {
 			'mmdat':       'walletgen4',
 			'mmdat':       'walletgen4',
 			'mmbrain':     'walletgen4',
 			'mmbrain':     'walletgen4',
 			'addrs':       'addrgen4',
 			'addrs':       'addrgen4',
-			'raw':         'txcreate4',
-			'sig':         'txsign4',
+			'rawtx':         'txcreate4',
+			'sigtx':         'txsign4',
 		},
 		},
 		'bw_filename': 'brainwallet.mmbrain',
 		'bw_filename': 'brainwallet.mmbrain',
 		'bw_params':   '192,1',
 		'bw_params':   '192,1',
@@ -203,7 +203,7 @@ cfgs = {
 		'ref_keyaddrfile_chksum': '9F2D D781 1812 8BAD C396 9DEB',
 		'ref_keyaddrfile_chksum': '9F2D D781 1812 8BAD C396 9DEB',
 
 
 #		'ref_fake_unspent_data':'98831F3A_unspent.json',
 #		'ref_fake_unspent_data':'98831F3A_unspent.json',
-		'ref_tx_file':     'tx_FFB367[1.234].raw',
+		'ref_tx_file':     'FFB367[1.234].rawtx',
 		'ic_wallet':       '98831F3A-5482381C-18460FB1[256,1].mmincog',
 		'ic_wallet':       '98831F3A-5482381C-18460FB1[256,1].mmincog',
 		'ic_wallet_hex':   '98831F3A-1630A9F2-870376A9[256,1].mmincox',
 		'ic_wallet_hex':   '98831F3A-1630A9F2-870376A9[256,1].mmincox',
 
 
@@ -256,8 +256,8 @@ cmd_group['main'] = OrderedDict([
 	['addrgen',         (1,'address generation',       [[['mmdat',pwfile],1]],1)],
 	['addrgen',         (1,'address generation',       [[['mmdat',pwfile],1]],1)],
 	['addrimport',      (1,'address import',           [[['addrs'],1]],1)],
 	['addrimport',      (1,'address import',           [[['addrs'],1]],1)],
 	['txcreate',        (1,'transaction creation',     [[['addrs'],1]],1)],
 	['txcreate',        (1,'transaction creation',     [[['addrs'],1]],1)],
-	['txsign',          (1,'transaction signing',      [[['mmdat','raw',pwfile],1]],1)],
-	['txsend',          (1,'transaction sending',      [[['sig'],1]])],
+	['txsign',          (1,'transaction signing',      [[['mmdat','rawtx',pwfile],1]],1)],
+	['txsend',          (1,'transaction sending',      [[['sigtx'],1]])],
 
 
 	['export_seed',     (1,'seed export to mmseed format',   [[['mmdat'],1]])],
 	['export_seed',     (1,'seed export to mmseed format',   [[['mmdat'],1]])],
 	['export_mnemonic', (1,'seed export to mmwords format',  [[['mmdat'],1]])],
 	['export_mnemonic', (1,'seed export to mmwords format',  [[['mmdat'],1]])],
@@ -272,23 +272,23 @@ cmd_group['main'] = OrderedDict([
 	['addrgen_incog_hidden',(1,'address generation from hidden mmincog file', [[[hincog_fn,'addrs'],1]])],
 	['addrgen_incog_hidden',(1,'address generation from hidden mmincog file', [[[hincog_fn,'addrs'],1]])],
 
 
 	['keyaddrgen',    (1,'key-address file generation', [[['mmdat',pwfile],1]])],
 	['keyaddrgen',    (1,'key-address file generation', [[['mmdat',pwfile],1]])],
-	['txsign_keyaddr',(1,'transaction signing with key-address file', [[['akeys.mmenc','raw'],1]])],
+	['txsign_keyaddr',(1,'transaction signing with key-address file', [[['akeys.mmenc','rawtx'],1]])],
 
 
 	['walletgen2',(2,'wallet generation (2), 128-bit seed',     [])],
 	['walletgen2',(2,'wallet generation (2), 128-bit seed',     [])],
 	['addrgen2',  (2,'address generation (2)',    [[['mmdat'],2]])],
 	['addrgen2',  (2,'address generation (2)',    [[['mmdat'],2]])],
 	['txcreate2', (2,'transaction creation (2)',  [[['addrs'],2]])],
 	['txcreate2', (2,'transaction creation (2)',  [[['addrs'],2]])],
-	['txsign2',   (2,'transaction signing, two transactions',[[['mmdat','raw'],1],[['mmdat','raw'],2]])],
+	['txsign2',   (2,'transaction signing, two transactions',[[['mmdat','rawtx'],1],[['mmdat','rawtx'],2]])],
 	['export_mnemonic2', (2,'seed export to mmwords format (2)',[[['mmdat'],2]])],
 	['export_mnemonic2', (2,'seed export to mmwords format (2)',[[['mmdat'],2]])],
 
 
 	['walletgen3',(3,'wallet generation (3)',                  [])],
 	['walletgen3',(3,'wallet generation (3)',                  [])],
 	['addrgen3',  (3,'address generation (3)',                 [[['mmdat'],3]])],
 	['addrgen3',  (3,'address generation (3)',                 [[['mmdat'],3]])],
 	['txcreate3', (3,'tx creation with inputs and outputs from two wallets', [[['addrs'],1],[['addrs'],3]])],
 	['txcreate3', (3,'tx creation with inputs and outputs from two wallets', [[['addrs'],1],[['addrs'],3]])],
-	['txsign3',   (3,'tx signing with inputs and outputs from two wallets',[[['mmdat'],1],[['mmdat','raw'],3]])],
+	['txsign3',   (3,'tx signing with inputs and outputs from two wallets',[[['mmdat'],1],[['mmdat','rawtx'],3]])],
 
 
 	['walletgen4',(4,'wallet generation (4) (brainwallet)',    [])],
 	['walletgen4',(4,'wallet generation (4) (brainwallet)',    [])],
 	['addrgen4',  (4,'address generation (4)',                 [[['mmdat'],4]])],
 	['addrgen4',  (4,'address generation (4)',                 [[['mmdat'],4]])],
 	['txcreate4', (4,'tx creation with inputs and outputs from four seed sources, plus non-MMGen inputs and outputs', [[['addrs'],1],[['addrs'],2],[['addrs'],3],[['addrs'],4]])],
 	['txcreate4', (4,'tx creation with inputs and outputs from four seed sources, plus non-MMGen inputs and outputs', [[['addrs'],1],[['addrs'],2],[['addrs'],3],[['addrs'],4]])],
-	['txsign4',   (4,'tx signing with inputs and outputs from incog file, mnemonic file, wallet and brainwallet, plus non-MMGen inputs and outputs', [[['mmincog'],1],[['mmwords'],2],[['mmdat'],3],[['mmbrain','raw'],4]])],
+	['txsign4',   (4,'tx signing with inputs and outputs from incog file, mnemonic file, wallet and brainwallet, plus non-MMGen inputs and outputs', [[['mmincog'],1],[['mmwords'],2],[['mmdat'],3],[['mmbrain','rawtx'],4]])],
 ])
 ])
 
 
 cmd_group['tool'] = OrderedDict([
 cmd_group['tool'] = OrderedDict([
@@ -428,6 +428,7 @@ meta_cmds = OrderedDict([
 ])
 ])
 
 
 del cmd_group
 del cmd_group
+log_file = 'test.py_log'
 
 
 opts_data = {
 opts_data = {
 #	'sets': [('non_interactive',bool,'verbose',None)],
 #	'sets': [('non_interactive',bool,'verbose',None)],
@@ -441,15 +442,17 @@ opts_data = {
                     debugging only).
                     debugging only).
 -e, --exact-output  Show the exact output of the MMGen script(s) being run.
 -e, --exact-output  Show the exact output of the MMGen script(s) being run.
 -l, --list-cmds     List and describe the commands in the test suite.
 -l, --list-cmds     List and describe the commands in the test suite.
+-L, --log           Log commands to file {lf}
 -n, --names         Display command names instead of descriptions.
 -n, --names         Display command names instead of descriptions.
 -I, --non-interactive Non-interactive operation (MS Windows mode)
 -I, --non-interactive Non-interactive operation (MS Windows mode)
 -p, --pause         Pause between tests, resuming on keypress.
 -p, --pause         Pause between tests, resuming on keypress.
 -q, --quiet         Produce minimal output.  Suppress dependency info.
 -q, --quiet         Produce minimal output.  Suppress dependency info.
 -s, --system        Test scripts and modules installed on system rather
 -s, --system        Test scripts and modules installed on system rather
                     than those in the repo root.
                     than those in the repo root.
+-S, --skip-deps     Skip dependency checking for command
 -t, --traceback     Run the command inside the '{tb_cmd}' script.
 -t, --traceback     Run the command inside the '{tb_cmd}' script.
 -v, --verbose       Produce more verbose output.
 -v, --verbose       Produce more verbose output.
-""".format(tb_cmd=tb_cmd),
+""".format(tb_cmd=tb_cmd,lf=log_file),
 	'notes': """
 	'notes': """
 
 
 If no command is given, the whole suite of tests is run.
 If no command is given, the whole suite of tests is run.
@@ -457,6 +460,9 @@ If no command is given, the whole suite of tests is run.
 }
 }
 
 
 cmd_args = opts.init(opts_data)
 cmd_args = opts.init(opts_data)
+if opt.log:
+	log_fd = open(log_file,'a')
+	log_fd.write('\nLog started: %s\n' % make_timestr())
 
 
 if opt.system: sys.path.pop(0)
 if opt.system: sys.path.pop(0)
 ni = bool(opt.non_interactive)
 ni = bool(opt.non_interactive)
@@ -616,11 +622,11 @@ class MMGenExpect(object):
 			mmgen_cmd = os.path.join(os.curdir,mmgen_cmd)
 			mmgen_cmd = os.path.join(os.curdir,mmgen_cmd)
 		desc = (cmd_data[name][1],name)[bool(opt.names)]
 		desc = (cmd_data[name][1],name)[bool(opt.names)]
 		if extra_desc: desc += ' ' + extra_desc
 		if extra_desc: desc += ' ' + extra_desc
+		cmd_str = mmgen_cmd + ' ' + ' '.join(cmd_args)
+		if opt.log:
+			log_fd.write(cmd_str+'\n')
 		if opt.verbose or opt.exact_output:
 		if opt.verbose or opt.exact_output:
-			sys.stderr.write(
-				green('Testing: %s\nExecuting ' % desc) +
-				cyan("'%s %s'\n" % (mmgen_cmd,' '.join(cmd_args)))
-			)
+			sys.stderr.write(green('Testing: %s\nExecuting %s\n' % (desc,cyan(cmd_str))))
 		else:
 		else:
 			m = 'Testing %s: ' % desc
 			m = 'Testing %s: ' % desc
 			msg_r((m,yellow(m))[ni])
 			msg_r((m,yellow(m))[ni])
@@ -845,7 +851,8 @@ def check_needs_rerun(
 		if rerun:
 		if rerun:
 			for fn in fns:
 			for fn in fns:
 				if not root: os.unlink(fn)
 				if not root: os.unlink(fn)
-			ts.do_cmd(cmd)
+			if not (dpy and opt.skip_deps):
+				ts.do_cmd(cmd)
 			if not root: do_between()
 			if not root: do_between()
 	else:
 	else:
 		# If prog produces multiple files:
 		# If prog produces multiple files:
@@ -888,6 +895,7 @@ def check_deps(cmds):
 
 
 
 
 def clean(usr_dirs=[]):
 def clean(usr_dirs=[]):
+	if opt.skip_deps: return
 	all_dirs = MMGenTestSuite().list_tmp_dirs()
 	all_dirs = MMGenTestSuite().list_tmp_dirs()
 	dirs = (usr_dirs or all_dirs)
 	dirs = (usr_dirs or all_dirs)
 	for d in sorted(dirs):
 	for d in sorted(dirs):
@@ -1149,14 +1157,15 @@ class MMGenTestSuite(object):
 		t.written_to_file('Transaction')
 		t.written_to_file('Transaction')
 		ok()
 		ok()
 
 
-	def txsign_end(self,t,tnum=None):
+	def txsign_end(self,t,tnum=None,has_comment=False):
 		t.expect('Signing transaction')
 		t.expect('Signing transaction')
-		t.expect('Edit transaction comment? (y/N): ','\n')
-		t.expect('Save signed transaction? (Y/n): ','y')
+		cprompt = ('Add a comment to transaction','Edit transaction comment')[has_comment]
+		t.expect('%s? (y/N): ' % cprompt,'\n')
+		t.expect('Save signed transaction.*?\? \(Y/n\): ','y',regex=True)
 		add = ' #' + tnum if tnum else ''
 		add = ' #' + tnum if tnum else ''
 		t.written_to_file('Signed transaction' + add, oo=True)
 		t.written_to_file('Signed transaction' + add, oo=True)
 
 
-	def txsign(self,name,txfile,wf,pf='',save=True):
+	def txsign(self,name,txfile,wf,pf='',save=True,has_comment=False):
 		add_args = ([],['-q','-P',pf])[ni]
 		add_args = ([],['-q','-P',pf])[ni]
 		if ni:
 		if ni:
 			m = '\nAnswer the interactive prompts as follows:\n  ENTER, ENTER, ENTER'
 			m = '\nAnswer the interactive prompts as follows:\n  ENTER, ENTER, ENTER'
@@ -1167,9 +1176,10 @@ class MMGenTestSuite(object):
 		t.tx_view()
 		t.tx_view()
 		t.passphrase('MMGen wallet',cfg['wpasswd'])
 		t.passphrase('MMGen wallet',cfg['wpasswd'])
 		if save:
 		if save:
-			self.txsign_end(t)
+			self.txsign_end(t,has_comment=has_comment)
 		else:
 		else:
-			t.expect('Edit transaction comment? (y/N): ','\n')
+			cprompt = ('Add a comment to transaction','Edit transaction comment')[has_comment]
+			t.expect('%s? (y/N): ' % cprompt,'\n')
 			t.close()
 			t.close()
 		ok()
 		ok()
 
 
@@ -1177,7 +1187,7 @@ class MMGenTestSuite(object):
 		t = MMGenExpect(name,'mmgen-txsend', ['-d',cfg['tmpdir'],sigfile])
 		t = MMGenExpect(name,'mmgen-txsend', ['-d',cfg['tmpdir'],sigfile])
 		t.license()
 		t.license()
 		t.tx_view()
 		t.tx_view()
-		t.expect('Edit transaction comment? (y/N): ','\n')
+		t.expect('Add a comment to transaction? (y/N): ','\n')
 		t.expect('broadcast this transaction to the network?')
 		t.expect('broadcast this transaction to the network?')
 		t.expect("'YES, I REALLY WANT TO DO THIS' to confirm: ",'\n')
 		t.expect("'YES, I REALLY WANT TO DO THIS' to confirm: ",'\n')
 		t.expect('Exiting at user request')
 		t.expect('Exiting at user request')
@@ -1601,7 +1611,7 @@ class MMGenTestSuite(object):
 		wf = os.path.join(ref_dir,cfg['ref_wallet'])
 		wf = os.path.join(ref_dir,cfg['ref_wallet'])
 		write_to_tmpfile(cfg,pwfile,cfg['wpasswd'])
 		write_to_tmpfile(cfg,pwfile,cfg['wpasswd'])
 		pf = get_tmpfile_fn(cfg,pwfile)
 		pf = get_tmpfile_fn(cfg,pwfile)
-		self.txsign(name,tf,wf,pf,save=False)
+		self.txsign(name,tf,wf,pf,save=False,has_comment=True)
 
 
 	def ref_tool_decrypt(self,name):
 	def ref_tool_decrypt(self,name):
 		f = os.path.join(ref_dir,ref_enc_fn)
 		f = os.path.join(ref_dir,ref_enc_fn)
@@ -1760,21 +1770,22 @@ ts = MMGenTestSuite()
 
 
 # Laggy flash media cause pexpect to crash, so read and write all temporary
 # Laggy flash media cause pexpect to crash, so read and write all temporary
 # files to volatile memory in '/dev/shm'
 # files to volatile memory in '/dev/shm'
-if sys.platform[:3] == 'win':
-	for cfg in sorted(cfgs): mk_tmpdir(cfgs[cfg])
-else:
-	d,pfx = '/dev/shm','mmgen-test-'
-	try:
-		import subprocess
-		subprocess.call('rm -rf %s/%s*'%(d,pfx),shell=True)
-	except Exception as e:
-		die(2,'Unable to delete directory tree %s/%s* (%s)'%(d,pfx,e))
-	try:
-		import tempfile
-		shm_dir = tempfile.mkdtemp('',pfx,d)
-	except Exception as e:
-		die(2,'Unable to create temporary directory in %s (%s)'%(d,e))
-	for cfg in sorted(cfgs): mk_tmpdir_path(shm_dir,cfgs[cfg])
+if not opt.skip_deps:
+	if sys.platform[:3] == 'win':
+		for cfg in sorted(cfgs): mk_tmpdir(cfgs[cfg])
+	else:
+		d,pfx = '/dev/shm','mmgen-test-'
+		try:
+			import subprocess
+			subprocess.call('rm -rf %s/%s*'%(d,pfx),shell=True)
+		except Exception as e:
+			die(2,'Unable to delete directory tree %s/%s* (%s)'%(d,pfx,e))
+		try:
+			import tempfile
+			shm_dir = tempfile.mkdtemp('',pfx,d)
+		except Exception as e:
+			die(2,'Unable to create temporary directory in %s (%s)'%(d,e))
+		for cfg in sorted(cfgs): mk_tmpdir_path(shm_dir,cfgs[cfg])
 
 
 try:
 try:
 	if cmd_args:
 	if cmd_args: