Browse Source

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

philemon 8 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
 
 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': """
 # {pnm} address file
 #
@@ -228,13 +234,14 @@ class AddrInfoList(MMGenObject):
 		vmsg_r('Getting account data from wallet...')
 		accts = c.listaccounts(0,True)
 		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)
 			if ma:
 				i += 1
-				addrlist = c.getaddressesbyaccount(acct)
+#				addrlist = c.getaddressesbyaccount(acct)
 				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(':')
 				if seed_id not in data:
 					data[seed_id] = []
@@ -380,7 +387,6 @@ class AddrInfo(MMGenObject):
 
 		# Header
 		out = []
-		from mmgen.addr import addrmsgs
 		k = ('addrfile_header','keyfile_header')[status[0]==0]
 		out.append(addrmsgs[k]+'\n')
 		if self.checksum:

+ 1 - 2
mmgen/main_tool.py

@@ -39,8 +39,7 @@ opts_data = {
 	'notes': """
 
 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)
 }
 

+ 9 - 203
mmgen/main_txcreate.py

@@ -25,7 +25,7 @@ from decimal import Decimal
 
 from mmgen.common import *
 from mmgen.tx import *
-from mmgen.term import get_char
+from mmgen.tw import *
 
 pnm = g.proj_name
 
@@ -68,12 +68,6 @@ one address with no amount on the command line.
 }
 
 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': """
 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
@@ -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.
 Please import this address into your tracking wallet or supply an address file
 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(),
 	'mixed_inputs': """
 NOTE: This transaction uses a mixture of both {pnm} and non-{pnm} inputs, which
@@ -110,163 +100,6 @@ was specified.
 """.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):
 
 	while True:
@@ -285,20 +118,7 @@ def select_outputs(unspent,prompt):
 		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
 	btc_addr = ail_w.mmaddr2btcaddr(mmaddr)
@@ -369,7 +189,7 @@ if not opt.info:
 			if is_btc_addr(a1):
 				btc_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:
 				die(2,"%s: unrecognized subargument in argument '%s'" % (a1,a))
 
@@ -382,7 +202,7 @@ if not opt.info:
 			if tx.change_addr:
 				die(2,'ERROR: More than one change address specified: %s, %s' %
 						(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'))
 		else:
 			die(2,'%s: unrecognized argument' % a)
@@ -395,24 +215,10 @@ if not opt.info:
 
 	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()
 
 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)])
 
 while True:
-	sel_nums = select_outputs(unspent,
+	sel_nums = select_outputs(tw.unspent,
 			'Enter a range or space-separated list of outputs to spend: ')
 	msg('Selected output%s: %s' % (
 			('s','')[len(sel_nums)==1],
 			' '.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])
 

+ 1 - 3
mmgen/main_txsend.py

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

+ 30 - 31
mmgen/main_txsign.py

@@ -26,7 +26,7 @@ from mmgen.seed import SeedSource
 
 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
 opts_data = {
 	'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]
 	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):
 	Msg("""
 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:'
 			die(3,wmsg['mm2btc_mapping_error'] % (al,a,m[a],bl,b,io_map[b]))
 	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]
 
 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)))
 		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):
 		die(1,'Transaction is already signed!')
@@ -335,6 +334,6 @@ for tx_num,tx_file in enumerate(tx_files,1):
 
 	if ok:
 		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:
 		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 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:
-			c.request('POST', '/', json.dumps(p), {
+			c.request('POST', '/', json.dumps(p,cls=JSONDecEncoder), {
 				'Host': self.host,
 				'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]']),
 	('getbalance',   ['minconf [int=1]']),
 	('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]']),
 	('remove_label',    ['<{} address> [str]'.format(pnm)]),
@@ -105,6 +106,7 @@ cmd_help = """
                   spendable/unspendable balances for individual {pnm} wallets
   listaddresses - list {pnm} addresses and their balances
   txview        - show raw/signed {pnm} transaction in human-readable form
+  twview        - view tracking wallet
 
   General utilities:
   hexdump      - encode data into formatted hexadecimal form (file or stdin)
@@ -157,6 +159,10 @@ cmd_help = """
 
 def tool_usage(prog_name, command):
 	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])))
 	else:
 		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(
 							(d['address'], addrs[key][2])))
 			else:
-				addrs[key] = [0,comment,d['address']]
+				addrs[key] = [Decimal('0'),comment,d['address']]
 			addrs[key][0] += d['amount']
 			total += d['amount']
 
@@ -408,7 +414,7 @@ def listaddresses(addrs='',minconf=1,showempty=False,pager=False,showbtcaddrs=Fa
 				key = mmaddr.replace(':','_')
 				if key not in addrs:
 					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)):
 			if len(addr) != 1:
@@ -468,10 +474,16 @@ def getbalance(minconf=1):
 
 def txview(infile,pager=False,terse=False):
 	c = bitcoin_connection()
-	tx = MMGenTX()
-	tx.parse_tx_file(infile,'transaction data')
+	tx = MMGenTX(infile)
 	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):
 	if not is_mmgen_addr(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 *
 
 class MMGenTX(MMGenObject):
+	ext  = g.rawtx_ext
+	desc = 'transaction'
+
 	def __init__(self,filename=None):
 		self.inputs      = []
 		self.outputs     = {}
@@ -191,7 +194,10 @@ class MMGenTX(MMGenObject):
 		self.chksum      = ''
 		self.fmt_data    = ''
 		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):
 		self.outputs[btcaddr] = (amt,)
@@ -261,17 +267,8 @@ class MMGenTX(MMGenObject):
 		return sum([i['amount'] for i in self.inputs])
 
 	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()
 
 # 	def make_b2m_map(self,ail_w,ail_f):
@@ -327,17 +324,22 @@ class MMGenTX(MMGenObject):
 		if sig_tx['complete']:
 			msg('OK')
 			self.hex = sig_tx['hex']
-			self.isSigned = True
+			self.mark_signed()
 			return True
 		else:
 			msg('failed\nBitcoind returned the following errors:')
 			pp_msg(sig_tx['errors'])
 			return False
 
+	def mark_signed(self):
+		self.desc = 'signed transaction'
+		self.ext = g.sigtx_ext
+
 	def check_signed(self,c):
 		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):
 		if bogus:
@@ -354,13 +356,12 @@ class MMGenTX(MMGenObject):
 			ask_write=ask_write,
 			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:
 			ask_write_default_yes=True
 		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_default_yes=ask_write_default_yes)
 
@@ -453,9 +454,9 @@ class MMGenTX(MMGenObject):
 
 		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):
 

+ 12 - 12
mmgen/util.py

@@ -70,6 +70,18 @@ def die(ev,s):
 def Die(ev,s):
 	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):
 	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))
@@ -804,15 +816,3 @@ def bitcoin_connection():
 	import mmgen.rpc
 	return mmgen.rpc.BitcoinRPCConnection(
 				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()
 
-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)
-
-tx.copy_inputs(inputs)
 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:
 	if k in b2m_map:
 		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)

+ 1 - 0
setup.py

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

+ 55 - 44
test/test.py

@@ -77,8 +77,8 @@ cfgs = {
 			pwfile:        'walletgen',
 			'mmdat':       'walletgen',
 			'addrs':       'addrgen',
-			'raw':         'txcreate',
-			'sig':         'txsign',
+			'rawtx':         'txcreate',
+			'sigtx':         'txsign',
 			'mmwords':     'export_mnemonic',
 			'mmseed':      'export_seed',
 			'mmincog':     'export_incog',
@@ -96,8 +96,8 @@ cfgs = {
 		'dep_generators': {
 			'mmdat':       'walletgen2',
 			'addrs':       'addrgen2',
-			'raw':         'txcreate2',
-			'sig':         'txsign2',
+			'rawtx':         'txcreate2',
+			'sigtx':         'txsign2',
 			'mmwords':     'export_mnemonic2',
 		},
 	},
@@ -108,8 +108,8 @@ cfgs = {
 		'dep_generators': {
 			'mmdat':       'walletgen3',
 			'addrs':       'addrgen3',
-			'raw':         'txcreate3',
-			'sig':         'txsign3'
+			'rawtx':         'txcreate3',
+			'sigtx':         'txsign3'
 		},
 	},
 	'4': {
@@ -121,8 +121,8 @@ cfgs = {
 			'mmdat':       'walletgen4',
 			'mmbrain':     'walletgen4',
 			'addrs':       'addrgen4',
-			'raw':         'txcreate4',
-			'sig':         'txsign4',
+			'rawtx':         'txcreate4',
+			'sigtx':         'txsign4',
 		},
 		'bw_filename': 'brainwallet.mmbrain',
 		'bw_params':   '192,1',
@@ -203,7 +203,7 @@ cfgs = {
 		'ref_keyaddrfile_chksum': '9F2D D781 1812 8BAD C396 9DEB',
 
 #		'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_hex':   '98831F3A-1630A9F2-870376A9[256,1].mmincox',
 
@@ -256,8 +256,8 @@ cmd_group['main'] = OrderedDict([
 	['addrgen',         (1,'address generation',       [[['mmdat',pwfile],1]],1)],
 	['addrimport',      (1,'address import',           [[['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_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]])],
 
 	['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',     [])],
 	['addrgen2',  (2,'address generation (2)',    [[['mmdat'],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]])],
 
 	['walletgen3',(3,'wallet generation (3)',                  [])],
 	['addrgen3',  (3,'address generation (3)',                 [[['mmdat'],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)',    [])],
 	['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]])],
-	['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([
@@ -428,6 +428,7 @@ meta_cmds = OrderedDict([
 ])
 
 del cmd_group
+log_file = 'test.py_log'
 
 opts_data = {
 #	'sets': [('non_interactive',bool,'verbose',None)],
@@ -441,15 +442,17 @@ opts_data = {
                     debugging only).
 -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, --log           Log commands to file {lf}
 -n, --names         Display command names instead of descriptions.
 -I, --non-interactive Non-interactive operation (MS Windows mode)
 -p, --pause         Pause between tests, resuming on keypress.
 -q, --quiet         Produce minimal output.  Suppress dependency info.
 -s, --system        Test scripts and modules installed on system rather
                     than those in the repo root.
+-S, --skip-deps     Skip dependency checking for command
 -t, --traceback     Run the command inside the '{tb_cmd}' script.
 -v, --verbose       Produce more verbose output.
-""".format(tb_cmd=tb_cmd),
+""".format(tb_cmd=tb_cmd,lf=log_file),
 	'notes': """
 
 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)
+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)
 ni = bool(opt.non_interactive)
@@ -616,11 +622,11 @@ class MMGenExpect(object):
 			mmgen_cmd = os.path.join(os.curdir,mmgen_cmd)
 		desc = (cmd_data[name][1],name)[bool(opt.names)]
 		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:
-			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:
 			m = 'Testing %s: ' % desc
 			msg_r((m,yellow(m))[ni])
@@ -845,7 +851,8 @@ def check_needs_rerun(
 		if rerun:
 			for fn in fns:
 				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()
 	else:
 		# If prog produces multiple files:
@@ -888,6 +895,7 @@ def check_deps(cmds):
 
 
 def clean(usr_dirs=[]):
+	if opt.skip_deps: return
 	all_dirs = MMGenTestSuite().list_tmp_dirs()
 	dirs = (usr_dirs or all_dirs)
 	for d in sorted(dirs):
@@ -1149,14 +1157,15 @@ class MMGenTestSuite(object):
 		t.written_to_file('Transaction')
 		ok()
 
-	def txsign_end(self,t,tnum=None):
+	def txsign_end(self,t,tnum=None,has_comment=False):
 		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 ''
 		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]
 		if ni:
 			m = '\nAnswer the interactive prompts as follows:\n  ENTER, ENTER, ENTER'
@@ -1167,9 +1176,10 @@ class MMGenTestSuite(object):
 		t.tx_view()
 		t.passphrase('MMGen wallet',cfg['wpasswd'])
 		if save:
-			self.txsign_end(t)
+			self.txsign_end(t,has_comment=has_comment)
 		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()
 		ok()
 
@@ -1177,7 +1187,7 @@ class MMGenTestSuite(object):
 		t = MMGenExpect(name,'mmgen-txsend', ['-d',cfg['tmpdir'],sigfile])
 		t.license()
 		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("'YES, I REALLY WANT TO DO THIS' to confirm: ",'\n')
 		t.expect('Exiting at user request')
@@ -1601,7 +1611,7 @@ class MMGenTestSuite(object):
 		wf = os.path.join(ref_dir,cfg['ref_wallet'])
 		write_to_tmpfile(cfg,pwfile,cfg['wpasswd'])
 		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):
 		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
 # 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:
 	if cmd_args: