Browse Source

test.py: refactor, modularize, cleanup code, make fully OO

- test groups are now separate classes in separate modules
- test data and code is loaded on an as-needed basis
- new TestSuiteRunner and CmdGroupMgr classes
- simplified invocation: if arguments are omitted, all default tests relevant
  for given network and option are run.  The following set of invocations
  provides nearly complete coverage of MMGen's core functionality:

    test/test.py
    test/test.py --segwit-random
    test/test.py --bech32

    test/test.py --coin=ltc
    test/test.py --coin=ltc --segwit-random
    test/test.py --coin=ltc --bech32

    test/test.py --coin=bch
    test/test.py --coin=eth
    test/test.py --coin=etc
MMGen 6 years ago
parent
commit
91410dd96c

+ 1 - 0
MANIFEST.in

@@ -1,6 +1,7 @@
 include README.md SIGNING_KEYS.pub LICENSE INSTALL
 include doc/wiki/using-mmgen/*
 include test/*.py
+include test/test_py_d/*.py
 include test/ref/*
 include test/ref/litecoin/*
 include test/ref/ethereum/*

+ 17 - 20
scripts/test-release.sh

@@ -171,7 +171,7 @@ i_alts='Gen-only altcoin'
 s_alts='The following tests will test generation operations for all supported altcoins'
 t_alts=(
 	"$scrambletest_py"
-	"$test_py ref_alt"
+	"$test_py ref_altcoin"
 	"$gentest_py --coin=btc 2 $rounds"
 	"$gentest_py --coin=btc --type=compressed 2 $rounds"
 	"$gentest_py --coin=btc --type=segwit 2 $rounds"
@@ -247,9 +247,6 @@ f_monero='Monero tests completed'
 i_eth='Ethereum'
 s_eth='Testing transaction and tracking wallet operations for Ethereum and Ethereum Classic'
 t_eth=(
-	"$test_py --coin=eth ref_tx_chk"
-	"$test_py --coin=eth --testnet=1 ref_tx_chk"
-	"$test_py --coin=etc ref_tx_chk"
 	"$test_py --coin=eth ethdev"
 	"$test_py --coin=etc ethdev"
 )
@@ -275,10 +272,10 @@ f_autosign_live='Autosign Live test complete'
 i_btc='Bitcoin mainnet'
 s_btc='The bitcoin (mainnet) daemon must both be running for the following tests'
 t_btc=(
-	"$test_py"
-	"$test_py --segwit dfl_wallet main ref ref_files"
-	"$test_py --segwit-random dfl_wallet main"
-	"$test_py --bech32 dfl_wallet main ref ref_files"
+	"$test_py --exclude regtest"
+	"$test_py --segwit"
+	"$test_py --segwit-random"
+	"$test_py --bech32"
 	"$python scripts/compute-file-chksum.py $REFDIR/*testnet.rawtx >/dev/null 2>&1")
 f_btc='You may stop the bitcoin (mainnet) daemon if you wish'
 
@@ -286,9 +283,9 @@ i_btc_tn='Bitcoin testnet'
 s_btc_tn='The bitcoin testnet daemon must both be running for the following tests'
 t_btc_tn=(
 	"$test_py --testnet=1"
-	"$test_py --testnet=1 --segwit dfl_wallet main ref ref_files"
-	"$test_py --testnet=1 --segwit-random dfl_wallet main"
-	"$test_py --testnet=1 --bech32 dfl_wallet main ref ref_files")
+	"$test_py --testnet=1 --segwit"
+	"$test_py --testnet=1 --segwit-random"
+	"$test_py --testnet=1 --bech32")
 f_btc_tn='You may stop the bitcoin testnet daemon if you wish'
 
 i_btc_rt='Bitcoin regtest'
@@ -300,7 +297,7 @@ f_btc_rt='Regtest (Bob and Alice) mode tests for BTC completed'
 
 i_bch='Bitcoin cash (BCH)'
 s_bch='The bitcoin cash daemon (Bitcoin ABC) must both be running for the following tests'
-t_bch=("$test_py --coin=bch dfl_wallet main ref ref_files")
+t_bch=("$test_py --coin=bch --exclude regtest")
 f_bch='You may stop the Bitcoin ABC daemon if you wish'
 
 i_bch_rt='Bitcoin cash (BCH) regtest'
@@ -311,19 +308,19 @@ f_bch_rt='Regtest (Bob and Alice) mode tests for BCH completed'
 i_ltc='Litecoin'
 s_ltc='The litecoin daemon must both be running for the following tests'
 t_ltc=(
-	"$test_py --coin=ltc dfl_wallet main ref ref_files"
-	"$test_py --coin=ltc --segwit dfl_wallet main ref ref_files"
-	"$test_py --coin=ltc --segwit-random dfl_wallet main"
-	"$test_py --coin=ltc --bech32 dfl_wallet main ref ref_files")
+	"$test_py --coin=ltc --exclude regtest"
+	"$test_py --coin=ltc --segwit"
+	"$test_py --coin=ltc --segwit-random"
+	"$test_py --coin=ltc --bech32")
 f_ltc='You may stop the litecoin daemon if you wish'
 
 i_ltc_tn='Litecoin testnet'
 s_ltc_tn='The litecoin testnet daemon must both be running for the following tests'
 t_ltc_tn=(
-	"$test_py --coin=ltc --testnet=1"
-	"$test_py --coin=ltc --testnet=1 --segwit dfl_wallet main ref ref_files"
-	"$test_py --coin=ltc --testnet=1 --segwit-random dfl_wallet main"
-	"$test_py --coin=ltc --testnet=1 --bech32 dfl_wallet main ref ref_files")
+	"$test_py --coin=ltc --testnet=1 --exclude regtest"
+	"$test_py --coin=ltc --testnet=1 --segwit"
+	"$test_py --coin=ltc --testnet=1 --segwit-random"
+	"$test_py --coin=ltc --testnet=1 --bech32")
 f_ltc_tn='You may stop the litecoin testnet daemon if you wish'
 
 i_ltc_rt='Litecoin regtest'

+ 0 - 1
setup.py

@@ -138,7 +138,6 @@ setup(
 			'mmgen.seed',
 			'mmgen.sha256',
 			'mmgen.term',
-			'mmgen.test',
 			'mmgen.tool',
 			'mmgen.tw',
 			'mmgen.tx',

+ 50 - 52
mmgen/test.py → test/common.py

@@ -17,41 +17,43 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 """
-test.py:  Shared routines for the test suites
+common.py: Shared routines for the test suites
 """
 
+class TestSuiteException(Exception): pass
+class TestSuiteFatalException(Exception): pass
+
 import os
 from binascii import hexlify
-
 from mmgen.common import *
 
+def getrandnum(n): return int(hexlify(os.urandom(n)),16)
+def getrandhex(n): return hexlify(os.urandom(n)).decode()
+def getrandnum_range(nbytes,rn_max):
+	while True:
+		rn = int(hexlify(os.urandom(nbytes)),16)
+		if rn < rn_max: return rn
+
+def getrandstr(num_chars,no_space=False):
+	n,m = 95,32
+	if no_space: n,m = 94,33
+	return ''.join([chr(i%n+m) for i in list(os.urandom(num_chars))])
+
 # Windows uses non-UTF8 encodings in filesystem, so use raw bytes here
-def cleandir(d):
+def cleandir(d,do_msg=False):
 	d_enc = d.encode()
 
 	try:    files = os.listdir(d_enc)
 	except: return
 
 	from shutil import rmtree
-	gmsg("Cleaning directory '{}'".format(d))
+	if do_msg: gmsg("Cleaning directory '{}'".format(d))
 	for f in files:
 		try:
 			os.unlink(os.path.join(d_enc,f))
 		except:
 			rmtree(os.path.join(d_enc,f))
 
-def getrandnum(n): return int(hexlify(os.urandom(n)),16)
-def getrandhex(n): return hexlify(os.urandom(n)).decode()
-def getrandnum_range(nbytes,rn_max):
-	while True:
-		rn = int(hexlify(os.urandom(nbytes)),16)
-		if rn < rn_max: return rn
-
-def getrandstr(num_chars,no_space=False):
-	n,m = 95,32
-	if no_space: n,m = 94,33
-	return ''.join([chr(i%n+m) for i in list(os.urandom(num_chars))])
-
 def mk_tmpdir(d):
 	try: os.mkdir(d,0o755)
 	except OSError as e:
@@ -59,31 +61,33 @@ def mk_tmpdir(d):
 	else:
 		vmsg("Created directory '{}'".format(d))
 
-def mk_tmpdir_path(path,cfg):
-	try:
-		name = os.path.split(cfg['tmpdir'])[-1]
-		src = os.path.join(path,name)
-		try:
-			os.unlink(cfg['tmpdir'])
-		except OSError as e:
-			if e.errno != 2: raise
-		finally:
-			os.mkdir(src)
-			os.symlink(src,cfg['tmpdir'])
-	except OSError as e:
-		if e.errno != 17: raise
-	else: msg("Created directory '{}'".format(cfg['tmpdir']))
-
-def get_tmpfile_fn(cfg,fn):
+# def mk_tmpdir_path(path,cfg):
+# 	try:
+# 		name = os.path.split(cfg['tmpdir'])[-1]
+# 		src = os.path.join(path,name)
+# 		try:
+# 			os.unlink(cfg['tmpdir'])
+# 		except OSError as e:
+# 			if e.errno != 2: raise
+# 		finally:
+# 			os.mkdir(src)
+# 			os.symlink(src,cfg['tmpdir'])
+# 	except OSError as e:
+# 		if e.errno != 17: raise
+# 	else: msg("Created directory '{}'".format(cfg['tmpdir']))
+
+def get_tmpfile(cfg,fn):
 	return os.path.join(cfg['tmpdir'],fn)
 
+def write_to_file(fn,data,binary=False):
+	write_data_to_file( fn,
+						data,
+						silent = True,
+						binary = binary,
+						ignore_opt_outdir = True )
+
 def write_to_tmpfile(cfg,fn,data,binary=False):
-	write_data_to_file(
-		os.path.join(cfg['tmpdir'],fn),
-		data,
-		silent=True,
-		binary=binary,
-		ignore_opt_outdir=True)
+	write_to_file(  os.path.join(cfg['tmpdir'],fn), data=data, binary=binary )
 
 def read_from_file(fn,binary=False):
 	from mmgen.util import get_data_from_file
@@ -92,26 +96,20 @@ def read_from_file(fn,binary=False):
 def read_from_tmpfile(cfg,fn,binary=False):
 	return read_from_file(os.path.join(cfg['tmpdir'],fn),binary=binary)
 
+def joinpath(*args,**kwargs):
+	return os.path.join(*args,**kwargs)
+
 def ok():
 	if opt.profile: return
 	if opt.verbose or opt.exact_output:
 		gmsg('OK')
 	else: msg(' OK')
 
-def ok_or_die(val,chk_func,s,skip_ok=False):
-	try: ret = chk_func(val)
-	except: ret = False
-	if ret:
-		if not skip_ok: ok()
-	else:
-		rdie(3,"Returned value '{}' is not a {}".format((val,s)))
-
-def cmp_or_die(s,t,skip_ok=False):
-	if s == t:
-		if not skip_ok: ok()
-	else:
-		m = 'ERROR: recoded data:\n{}\ndiffers from original data:\n{}'
-		rdie(3,m.format(repr(t),repr(s)))
+def cmp_or_die(s,t,desc=None):
+	if s != t:
+		m = 'ERROR: recoded data:\n{!r}\ndiffers from original data:\n{!r}'
+		if desc: m = 'For {}:\n{}'.format(desc,m)
+		raise TestSuiteFatalException(m.format(t,s))
 
 def init_coverage():
 	coverdir = os.path.join('test','trace')

+ 0 - 292
test/mmgen_pexpect.py

@@ -1,292 +0,0 @@
-#!/usr/bin/env python3
-#
-# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
-# Copyright (C)2013-2019 The MMGen Project <mmgen@tuta.io>
-#
-# 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/>.
-
-"""
-test/mmgen_pexpect.py: pexpect implementation for MMGen test suites
-"""
-
-from mmgen.common import *
-from mmgen.test import getrandstr,ok,init_coverage
-
-try:
-	import pexpect
-	from pexpect.popen_spawn import PopenSpawn
-except:
-	die(2,red('Pexpect module is missing.  Cannnot run test suite'))
-
-if opt.buf_keypress:
-	send_delay = 0.3
-else:
-	send_delay = 0
-	os.environ['MMGEN_DISABLE_HOLD_PROTECT'] = '1'
-
-def my_send(p,t,delay=send_delay,s=False):
-	if delay: time.sleep(delay)
-	ret = p.send(t) # returns num bytes written
-	if delay: time.sleep(delay)
-	if opt.verbose:
-		ls = (' ','')[bool(opt.debug or not s)]
-		es = ('  ','')[bool(s)]
-		msg('{}SEND {}{}'.format(ls,es,yellow("'{}'".format(t.replace('\n',r'\n')))))
-	return ret
-
-def my_expect(p,s,t='',delay=send_delay,regex=False,nonl=False,silent=False):
-
-	quo = ('',"'")[type(s) == str]
-
-	if not silent:
-		if opt.verbose: msg_r('EXPECT {}'.format(yellow(quo+str(s)+quo)))
-		elif not opt.exact_output: msg_r('+')
-
-	try:
-		if s == '': ret = 0
-		else:
-			f = (p.expect_exact,p.expect)[bool(regex)]
-			ret = f(s,timeout=(60,5)[bool(opt.debug_pexpect)])
-	except pexpect.TIMEOUT:
-		if opt.debug_pexpect: raise
-		rdie(1,red('\nERROR.  Expect {}{}{} timed out.  Exiting'.format(quo,s,quo)))
-	debug_pexpect_msg(p)
-
-	if opt.verbose and type(s) != str:
-		msg_r(' ==> {} '.format(ret))
-
-	if ret == -1:
-		rdie(1,'Error.  Expect returned {}'.format(ret))
-	else:
-		if t == '':
-			if not nonl and not silent: vmsg('')
-		else:
-			my_send(p,t,delay,s)
-		return ret
-
-def debug_pexpect_msg(p):
-	if opt.debug_pexpect:
-		msg('\n{}{}{}'.format(red('BEFORE ['),p.before,red(']')))
-		msg('{}{}{}'.format(red('MATCH ['),p.after,red(']')))
-
-data_dir = os.path.join('test','data_dir'+('','-α')[bool(os.getenv('MMGEN_DEBUG_UTF8'))])
-
-class MMGenPexpect(object):
-
-	NL = '\r\n'
-	if g.platform == 'linux' and opt.popen_spawn:
-		import atexit
-		atexit.register(lambda: os.system('stty sane'))
-		NL = '\n'
-
-	def __init__(self,name,mmgen_cmd,cmd_args,desc,
-				no_output=False,passthru_args=[],msg_only=False,no_msg=False,log_fd=None):
-		cmd_args = ['--{}{}'.format(k.replace('_','-'),
-			'='+getattr(opt,k) if getattr(opt,k) != True else ''
-			) for k in passthru_args if getattr(opt,k)] \
-			+ ['--data-dir='+data_dir] + cmd_args
-
-		if g.platform == 'win': cmd,args = 'python3',[mmgen_cmd]+cmd_args
-		else:                   cmd,args = mmgen_cmd,cmd_args
-
-		for i in args:
-			if type(i) is not str:
-				m1 = 'Error: missing input files in cmd line?:'
-				m2 = '\nName: {}\nCmd: {}\nCmd args: {}'
-				die(2,(m1+m2).format(name,cmd,args))
-
-#		if opt.popen_spawn:
-		if True:
-			args = ['{q}{}{q}'.format(a,q="'" if ' ' in a else '') for a in args]
-
-		cmd_str = '{} {}'.format(cmd,' '.join(args)).replace('\\','/')
-		if opt.coverage:
-			fs = 'python3 -m trace --count --coverdir={} --file={} {c}'
-			cmd_str = fs.format(*init_coverage(),c=cmd_str)
-
-		if opt.log:
-			log_fd.write(cmd_str.encode()+'\n')
-
-		if not no_msg:
-			if opt.verbose or opt.print_cmdline or opt.exact_output:
-				clr1,clr2,eol = ((green,cyan,'\n'),(nocolor,nocolor,' '))[bool(opt.print_cmdline)]
-				msg_r(green('Testing: {}\n'.format(desc)))
-				if not msg_only:
-					s = repr(cmd_str) if g.platform == 'win' else cmd_str
-					msg_r(clr1('Executing {}{}'.format(clr2(s),eol)))
-			else:
-				m = 'Testing {}: '.format(desc)
-				msg_r(m)
-
-		if msg_only: return
-
-		if opt.direct_exec:
-			msg('')
-			from subprocess import call,check_output
-			f = (call,check_output)[bool(no_output)]
-			ret = f([cmd] + args)
-			if f == call and ret != 0:
-				die(1,red('ERROR: process returned a non-zero exit status ({})'.format(ret)))
-		else:
-			if opt.traceback:
-				tc = 'scripts/traceback_run.py'
-				cmd,args = tc,[cmd]+args
-				cmd_str = tc + ' ' + cmd_str
-			# Msg('\ncmd_str: {}'.format(cmd_str))
-			if opt.popen_spawn:
-				# NOTE: the following is outdated for Python 3
-				# PopenSpawn() requires cmd string to be bytes.  However, it autoconverts unicode
-				# input to bytes, though this behavior seems to be undocumented.  Setting 'encoding'
-				# to 'UTF-8' will cause pexpect to reject non-unicode string input.
-				self.p = PopenSpawn(cmd_str,encoding='utf8')
-			else:
-				self.p = pexpect.spawn(cmd_str,encoding='utf8')
-				self.p.delaybeforesend = 0
-			if opt.exact_output: self.p.logfile = sys.stdout
-
-	def do_decrypt_ka_data(self,hp,pw,desc='key-address data',check=True):
-		self.hash_preset(desc,hp)
-		self.passphrase(desc,pw)
-		self.expect('Check key-to-address validity? (y/N): ',('n','y')[check])
-
-	def view_tx(self,view):
-		self.expect('View.* transaction.*\? .*: ',view,regex=True)
-		if view not in 'n\n':
-			self.expect('to continue: ','\n')
-
-	def do_comment(self,add_comment,has_label=False):
-		p = ('Add a comment to transaction','Edit transaction comment')[has_label]
-		self.expect('{}? (y/N): '.format(p),('n','y')[bool(add_comment)])
-		if add_comment:
-			self.expect('Comment: ',add_comment+'\n')
-
-	def ok(self,exit_val=0):
-		ret = self.p.wait()
-		if ret not in (exit_val,None) and not opt.coverage: # Some cmds exit with None
-			die(1,red('test.py: spawned program exited with value {}'.format(ret)))
-		if opt.profile: return
-		if opt.verbose or opt.exact_output:
-			sys.stderr.write(green('OK\n'))
-		else: msg(' OK')
-
-	def cmp_or_die(self,s,t,skip_ok=False,exit_val=0):
-		ret = self.p.wait()
-		if ret != exit_val:
-			rdie(1,'test.py: spawned program exited with value {}'.format(ret))
-		if s == t:
-			if not skip_ok: ok()
-		else:
-			fs = 'ERROR: recoded data:\n{}\ndiffers from original data:\n{}'
-			rdie(3,fs.format(repr(t),repr(s)))
-
-	def license(self):
-		if 'MMGEN_NO_LICENSE' in os.environ: return
-		p = "'w' for conditions and warranty info, or 'c' to continue: "
-		my_expect(self.p,p,'c')
-
-	def label(self,label='Test Label (UTF-8) α'):
-		p = 'Enter a wallet label, or hit ENTER for no label: '
-		my_expect(self.p,p,label+'\n')
-
-	def usr_rand_out(self,saved=False):
-		fs = 'Generating encryption key from OS random data plus {}user-supplied entropy'
-		my_expect(self.p,fs.format(('','saved ')[saved]))
-
-	def usr_rand(self,num_chars):
-		if opt.usr_random:
-			self.interactive()
-			my_send(self.p,'\n')
-		else:
-			rand_chars = list(getrandstr(num_chars,no_space=True))
-			vmsg_r('SEND ')
-			while rand_chars:
-				ch = rand_chars.pop(0)
-				msg_r(yellow(ch)+' ' if opt.verbose else '+')
-				ret = my_expect(self.p,'left: ',ch,delay=0.005)
-			my_expect(self.p,'ENTER to continue: ','\n')
-
-	def passphrase_new(self,desc,passphrase):
-		my_expect(self.p,'Enter passphrase for {}: '.format(desc),passphrase+'\n')
-		my_expect(self.p,'Repeat passphrase: ',passphrase+'\n')
-
-	def passphrase(self,desc,passphrase,pwtype=''):
-		if pwtype: pwtype += ' '
-		my_expect(self.p,
-				'Enter {}passphrase for {}.*?: '.format(pwtype,desc),
-				passphrase+'\n',regex=True)
-
-	def hash_preset(self,desc,preset=''):
-		my_expect(self.p,'Enter hash preset for {}'.format(desc))
-		my_expect(self.p,'or hit ENTER .*?:',str(preset)+'\n',regex=True)
-
-	def written_to_file(self,desc,overwrite_unlikely=False,query='Overwrite?  ',oo=False):
-		s1 = '{} written to file '.format(desc)
-		s2 = query + "Type uppercase 'YES' to confirm: "
-		ret = my_expect(self.p,([s1,s2],s1)[overwrite_unlikely])
-		if ret == 1:
-			my_send(self.p,'YES\n')
-#			if oo:
-			outfile = self.expect_getend("Overwriting file '").rstrip("'")
-			return outfile
-# 			else:
-# 				ret = my_expect(self.p,s1)
-		self.expect(self.NL,nonl=True)
-		outfile = self.p.before.strip().strip("'")
-		if opt.debug_pexpect: rmsg('Outfile [{}]'.format(outfile))
-		vmsg('{} file: {}'.format(desc,cyan(outfile.replace("'",''))))
-		return outfile
-
-	def no_overwrite(self):
-		self.expect("Overwrite?  Type uppercase 'YES' to confirm: ",'\n')
-		self.expect('Exiting at user request')
-
-	def expect_getend(self,s,regex=False):
-		ret = self.expect(s,regex=regex,nonl=True)
-		debug_pexpect_msg(self.p)
-#		end = self.readline().strip()
-		# readline() of partial lines doesn't work with PopenSpawn, so do this instead:
-		self.expect(self.NL,nonl=True,silent=True)
-		debug_pexpect_msg(self.p)
-		end = self.p.before
-		if not g.debug:
-			vmsg(' ==> {}'.format(cyan(end)))
-		return end
-
-	def interactive(self):
-		return self.p.interact() # interact() not available with popen_spawn
-
-	def kill(self,signal):
-		return self.p.kill(signal)
-
-	def logfile(self,arg):
-		self.p.logfile = arg
-
-	def expect(self,*args,**kwargs):
-		return my_expect(self.p,*args,**kwargs)
-
-	def send(self,*args,**kwargs):
-		return my_send(self.p,*args,**kwargs)
-
-# 	def readline(self):
-# 		return self.p.readline()
-# 	def readlines(self):
-# 		return [l.rstrip()+'\n' for l in self.p.readlines()]
-
-	def read(self,n=None):
-		if n: return self.p.read(n)
-		else: return self.p.read()
-
-	def close(self):
-		if not opt.popen_spawn:
-			self.p.close()

+ 217 - 0
test/pexpect.py

@@ -0,0 +1,217 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
+# Copyright (C)2013-2019 The MMGen Project <mmgen@tuta.io>
+#
+# 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/>.
+
+"""
+test/pexpect.py: pexpect implementation for MMGen test suites
+"""
+
+import sys,os,time
+from mmgen.globalvars import g
+from mmgen.opts import opt
+from mmgen.util import msg,msg_r,vmsg,vmsg_r,rmsg,red,yellow,green,cyan,die,rdie
+from test.common import getrandstr
+
+try:
+	import pexpect
+	from pexpect.popen_spawn import PopenSpawn
+except:
+	die(2,red('Pexpect module is missing.  Cannnot run test suite'))
+
+def debug_pexpect_msg(p):
+	if opt.debug_pexpect:
+		msg('\n{}{}{}'.format(red('BEFORE ['),p.before,red(']')))
+		msg('{}{}{}'.format(red('MATCH ['),p.after,red(']')))
+
+if g.platform == 'linux' and not opt.pexpect_spawn:
+	import atexit
+	atexit.register(lambda: os.system('stty sane'))
+	NL = '\n'
+else:
+	NL = '\r\n'
+
+class MMGenPexpect(object):
+
+	def __init__(self,args,no_output=False):
+
+		if opt.direct_exec:
+			msg('')
+			from subprocess import call,check_output
+			f = (call,check_output)[bool(no_output)]
+			ret = f([args[0]] + args[1:])
+			if f == call and ret != 0:
+				die(1,red('ERROR: process returned a non-zero exit status ({})'.format(ret)))
+		else:
+			if opt.pexpect_spawn:
+				self.p = pexpect.spawn(args[0],args[1:],encoding='utf8')
+				self.p.delaybeforesend = 0
+			else:
+				self.p = PopenSpawn(args,encoding='utf8')
+#				self.p.delaybeforesend = 0 # TODO: try this here too
+
+			if opt.exact_output: self.p.logfile = sys.stdout
+
+		self.req_exit_val = 0
+		self.skip_ok = False
+
+	def do_decrypt_ka_data(self,hp,pw,desc='key-address data',check=True):
+		self.hash_preset(desc,hp)
+		self.passphrase(desc,pw)
+		self.expect('Check key-to-address validity? (y/N): ',('n','y')[check])
+
+	def view_tx(self,view):
+		self.expect('View.* transaction.*\? .*: ',view,regex=True)
+		if view not in 'n\n':
+			self.expect('to continue: ','\n')
+
+	def do_comment(self,add_comment,has_label=False):
+		p = ('Add a comment to transaction','Edit transaction comment')[has_label]
+		self.expect('{}? (y/N): '.format(p),('n','y')[bool(add_comment)])
+		if add_comment:
+			self.expect('Comment: ',add_comment+'\n')
+
+	def ok(self):
+		ret = self.p.wait()
+		if ret != self.req_exit_val and not opt.coverage:
+			die(1,red('test.py: spawned program exited with value {}'.format(ret)))
+		if opt.profile: return
+		if not self.skip_ok:
+			sys.stderr.write(green('OK\n') if opt.exact_output or opt.verbose else (' OK\n'))
+		return self
+
+	def license(self):
+		if 'MMGEN_NO_LICENSE' in os.environ: return
+		self.expect("'w' for conditions and warranty info, or 'c' to continue: ",'c')
+
+	def label(self,label='Test Label (UTF-8) α'):
+		self.expect('Enter a wallet label, or hit ENTER for no label: ',label+'\n')
+
+	def usr_rand_out(self,saved=False):
+		fs = 'Generating encryption key from OS random data plus {}user-supplied entropy'
+		self.expect(fs.format(('','saved ')[saved]))
+
+	def usr_rand(self,num_chars):
+		if opt.usr_random:
+			self.interactive()
+			self.send('\n')
+		else:
+			rand_chars = list(getrandstr(num_chars,no_space=True))
+			vmsg_r('SEND ')
+			while rand_chars:
+				ch = rand_chars.pop(0)
+				msg_r(yellow(ch)+' ' if opt.verbose else '+')
+				ret = self.expect('left: ',ch,delay=0.005)
+			self.expect('ENTER to continue: ','\n')
+
+	def passphrase_new(self,desc,passphrase):
+		self.expect('Enter passphrase for {}: '.format(desc),passphrase+'\n')
+		self.expect('Repeat passphrase: ',passphrase+'\n')
+
+	def passphrase(self,desc,passphrase,pwtype=''):
+		if pwtype: pwtype += ' '
+		self.expect('Enter {}passphrase for {}.*?: '.format(pwtype,desc),passphrase+'\n',regex=True)
+
+	def hash_preset(self,desc,preset=''):
+		self.expect('Enter hash preset for {}'.format(desc))
+		self.expect('or hit ENTER .*?:',str(preset)+'\n',regex=True)
+
+	def written_to_file(self,desc,overwrite_unlikely=False,query='Overwrite?  ',oo=False):
+		s1 = '{} written to file '.format(desc)
+		s2 = query + "Type uppercase 'YES' to confirm: "
+		ret = self.expect(([s1,s2],s1)[overwrite_unlikely])
+		if ret == 1:
+			self.send('YES\n')
+			return self.expect_getend("Overwriting file '").rstrip("'")
+		self.expect(NL,nonl=True)
+		outfile = self.p.before.strip().strip("'")
+		if opt.debug_pexpect:
+			rmsg('Outfile [{}]'.format(outfile))
+		vmsg('{} file: {}'.format(desc,cyan(outfile.replace("'",''))))
+		return outfile
+
+	def no_overwrite(self):
+		self.expect("Overwrite?  Type uppercase 'YES' to confirm: ",'\n')
+		self.expect('Exiting at user request')
+
+	def expect_getend(self,s,regex=False):
+		ret = self.expect(s,regex=regex,nonl=True)
+		debug_pexpect_msg(self.p)
+		# readline() of partial lines doesn't work with PopenSpawn, so do this instead:
+		self.expect(NL,nonl=True,silent=True)
+		debug_pexpect_msg(self.p)
+		end = self.p.before
+		if not g.debug:
+			vmsg(' ==> {}'.format(cyan(end)))
+		return end
+
+	def interactive(self):
+		return self.p.interact() # interact() not available with popen_spawn
+
+	def kill(self,signal):
+		return self.p.kill(signal)
+
+	def expect(self,s,t='',delay=None,regex=False,nonl=False,silent=False):
+		delay = delay or (0,0.3)[bool(opt.buf_keypress)]
+
+		if not silent:
+			if opt.verbose:
+				quo = ('',"'")[type(s) == str]
+				msg_r('EXPECT {}'.format(yellow(quo+str(s)+quo)))
+			elif not opt.exact_output: msg_r('+')
+
+		try:
+			if s == '':
+				ret = 0
+			else:
+				f = (self.p.expect_exact,self.p.expect)[bool(regex)]
+				ret = f(s,timeout=(60,5)[bool(opt.debug_pexpect)])
+		except pexpect.TIMEOUT:
+			if opt.debug_pexpect: raise
+			quo = ('',"'")[type(s) == str]
+			rdie(1,red('\nERROR.  Expect {}{}{} timed out.  Exiting'.format(quo,s,quo)))
+
+		debug_pexpect_msg(self.p)
+
+		if opt.verbose and type(s) != str:
+			msg_r(' ==> {} '.format(ret))
+
+		if ret == -1:
+			rdie(1,'Error.  Expect returned {}'.format(ret))
+		else:
+			if t == '':
+				if not nonl and not silent: vmsg('')
+			else:
+				self.send(t,delay,s)
+			return ret
+
+	def send(self,t,delay=None,s=False):
+		delay = delay or (0,0.3)[bool(opt.buf_keypress)]
+		if delay: time.sleep(delay)
+		ret = self.p.send(t) # returns num bytes written
+		if delay: time.sleep(delay)
+		if opt.verbose:
+			ls = (' ','')[bool(opt.debug or not s)]
+			es = ('  ','')[bool(s)]
+			msg('{}SEND {}{}'.format(ls,es,yellow("'{}'".format(t.replace('\n',r'\n')))))
+		return ret
+
+	def read(self,n=-1):
+		return self.p.read(n)
+
+	def close(self):
+		if opt.pexpect_spawn:
+			self.p.close()

+ 1 - 1
test/scrambletest.py

@@ -28,7 +28,7 @@ os.environ['MMGEN_TEST_SUITE'] = '1'
 
 # Import this _after_ local path's been added to sys.path
 from mmgen.common import *
-from mmgen.test import init_coverage
+from test.common import init_coverage
 
 opts_data = lambda: {
 	'desc': 'Test seed scrambling and addrlist data generation for all supported altcoins',

+ 690 - 3974
test/test.py

@@ -17,86 +17,18 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 """
-test/test.py:  Test suite for the MMGen suite
+test/test.py: Test suite for the MMGen wallet system
 """
 
-import sys,os,subprocess,shutil,time,re,json
-from decimal import Decimal
+def check_segwit_opts():
+	for k,m in (('segwit','S'),('segwit_random','S'),('bech32','B')):
+		if getattr(opt,k) and m not in g.proto.mmtypes:
+			die(1,'--{} option incompatible with {}'.format(k.replace('_','-'),g.proto.__name__))
 
-repo_root = os.path.normpath(os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]),os.pardir)))
-os.chdir(repo_root)
-sys.path.__setitem__(0,repo_root)
-
-try: os.unlink(os.path.join(repo_root,'my.err'))
-except: pass
-
-# Import these _after_ local path's been added to sys.path
-from mmgen.common import *
-from mmgen.test import *
-from mmgen.protocol import CoinProtocol,init_coin
-
-def omsg(s): sys.stderr.write(s+'\n')
-def omsg_r(s): sys.stderr.write(s)
-
-class TestSuiteException(Exception): pass
-class TestSuiteFatalException(Exception): pass
-
-set_debug_all()
-
-g.quiet = False # if 'quiet' was set in config file, disable here
-os.environ['MMGEN_QUIET'] = '0' # and for the spawned scripts
-
-log_file = 'test.py_log'
-
-hincog_fn      = 'rand_data'
-hincog_bytes   = 1024*1024
-hincog_offset  = 98765
-hincog_seedlen = 256
-
-incog_id_fn  = 'incog_id'
-non_mmgen_fn = 'coinkey'
-pwfile = 'passwd_file'
-
-ref_dir = os.path.join('test','ref')
-
-rt_pw = 'abc-α'
-ref_wallet_brainpass = 'abc'
-ref_wallet_hash_preset = '1'
-ref_wallet_incog_offset = 123
-
-from mmgen.obj import MMGenTXLabel,PrivKey,ETHAmt
-from mmgen.addr import AddrGenerator,KeyGenerator,AddrList,AddrData,AddrIdxList
-
-ref_tx_label_jp = '必要なのは、信用ではなく暗号化された証明に基づく電子取引システムであり、これにより希望する二者が信用できる第三者機関を介さずに直接取引できるよう' # 72 chars ('W'ide)
-ref_tx_label_zh = '所以,我們非常需要這樣一種電子支付系統,它基於密碼學原理而不基於信用,使得任何達成一致的雙方,能夠直接進行支付,從而不需要協力廠商仲介的參與。。' # 72 chars ('F'ull + 'W'ide)
-ref_tx_label_lat_cyr_gr = ''.join(map(chr,
-									list(range(65,91)) +
-									list(range(1040,1072)) + # cyrillic
-									list(range(913,939)) +   # greek
-									list(range(97,123))))[:MMGenTXLabel.max_len] # 72 chars
-ref_bw_hash_preset = '1'
-ref_bw_file        = 'wallet.mmbrain'
-ref_bw_file_spc    = 'wallet-spaced.mmbrain'
-
-ref_kafile_pass        = 'kafile password'
-ref_kafile_hash_preset = '1'
-
-ref_enc_fn = 'sample-text.mmenc'
-tool_enc_passwd = "Scrypt it, don't hash it!"
-sample_text = \
-	'The Times 03/Jan/2009 Chancellor on brink of second bailout for banks\n'
-
-chksum_pat = r'\b[A-F0-9]{4} [A-F0-9]{4} [A-F0-9]{4} [A-F0-9]{4}\b'
-
-# Laggy flash media cause pexpect to crash, so create a temporary directory
-# under '/dev/shm' and put datadir and temp files here.
-shortopts = ''.join([e[1:] for e in sys.argv if len(e) > 1 and e[0] == '-' and e[1] != '-'])
-shortopts = ['-'+e for e in list(shortopts)]
-data_dir_basename = 'data_dir' + ('','-α')[bool(os.getenv('MMGEN_DEBUG_UTF8'))]
-data_dir = os.path.join('test',data_dir_basename)
-trash_dir = os.path.join('test','trash')
-
-if not any(e in ('--skip-deps','--resume','-S','-r') for e in sys.argv+shortopts):
+def create_shm_dir(data_dir,trash_dir):
+	# Laggy flash media can cause pexpect to fail, so create a temporary directory
+	# under '/dev/shm' and put datadir and tmpdirs here.
+	import shutil
 	if g.platform == 'win':
 		for tdir in (data_dir,trash_dir):
 			try: os.listdir(tdir)
@@ -111,22 +43,45 @@ if not any(e in ('--skip-deps','--resume','-S','-r') for e in sys.argv+shortopts
 						shutil.rmtree(tdir)
 			os.mkdir(tdir,0o755)
 	else:
-		d,pfx = '/dev/shm','mmgen-test-'
+		tdir,pfx = '/dev/shm','mmgen-test-'
 		try:
-			subprocess.call('rm -rf {}/{}*'.format(d,pfx),shell=True)
+			subprocess.call('rm -rf {}/{}*'.format(tdir,pfx),shell=True)
 		except Exception as e:
-			die(2,'Unable to delete directory tree {}/{}* ({})'.format(d,pfx,e.args[0]))
+			die(2,'Unable to delete directory tree {}/{}* ({})'.format(tdir,pfx,e.args[0]))
 		try:
 			import tempfile
-			shm_dir = str(tempfile.mkdtemp('',pfx,d))
+			shm_dir = str(tempfile.mkdtemp('',pfx,tdir))
 		except Exception as e:
-			die(2,'Unable to create temporary directory in {} ({})'.format(d,e.args[0]))
-		for tdir in (data_dir,trash_dir):
-			dd = os.path.join(shm_dir,os.path.basename(tdir))
-			os.mkdir(dd,0o755)
-			try: os.unlink(tdir)
-			except: pass
-			os.symlink(dd,tdir)
+			die(2,'Unable to create temporary directory in {} ({})'.format(tdir,e.args[0]))
+
+		dest = os.path.join(shm_dir,os.path.basename(trash_dir))
+		os.mkdir(dest,0o755)
+		try: os.unlink(trash_dir)
+		except: pass
+		os.symlink(dest,trash_dir)
+
+		dest = os.path.join(shm_dir,os.path.basename(data_dir))
+		shutil.move(data_dir,dest) # data_dir was created by opts.init()
+		os.symlink(dest,data_dir)
+
+	return shm_dir
+
+import sys,os,time
+
+repo_root = os.path.normpath(os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]),os.pardir)))
+os.chdir(repo_root)
+sys.path.__setitem__(0,repo_root)
+
+try: os.unlink(os.path.join(repo_root,'my.err'))
+except: pass
+
+# Import these _after_ local path's been added to sys.path
+from mmgen.common import *
+from test.common import *
+from test.test_py_d.common import *
+
+g.quiet = False # if 'quiet' was set in config file, disable here
+os.environ['MMGEN_QUIET'] = '0' # for this script and spawned scripts
 
 opts_data = lambda: {
 	'desc': 'Test suite for the MMGen suite',
@@ -144,12 +99,13 @@ opts_data = lambda: {
 -E, --direct-exec    Bypass pexpect and execute a command directly (for
                      debugging only)
 -e, --exact-output   Show the exact output of the MMGen script(s) being run
+-G, --exclude-groups=G Exclude the specified command groups (comma-separated)
 -l, --list-cmds      List and describe the commands in the test suite
 -L, --list-cmd-groups Output a list of command groups, with no descriptions
 -n, --names          Display command names instead of descriptions
 -o, --log            Log commands to file {lf}
--O, --popen-spawn    Use pexpect's popen_spawn instead of popen (much faster,
-                     but doesn't emulate terminal)
+-O, --pexpect-spawn  Use pexpect.spawn instead of popen_spawn (much slower,
+                     kut does real terminal emulation)
 -p, --pause          Pause between tests, resuming on keypress
 -P, --profile        Record the execution time of each script
 -q, --quiet          Produce minimal output.  Suppress dependency info
@@ -167,563 +123,204 @@ opts_data = lambda: {
 """.format(tbc='scripts/traceback_run.py',lf=log_file),
 	'notes': """
 
-If no command is given, the whole suite of tests is run.
+If no command is given, the whole test suite is run.
 """
 }
 
-sys.argv = [sys.argv[0]] + ['--data-dir',data_dir] + sys.argv[1:]
+data_dir = os.path.join('test','data_dir' + ('','-α')[bool(os.getenv('MMGEN_DEBUG_UTF8'))])
 
-cmd_args = opts.init(opts_data)
+# we need the values of two opts before running opts.init, so parse without initializing:
+uopts = opts.init(opts_data,parse_only=True)[0]
 
-if opt.popen_spawn: os.environ['MMGEN_TEST_SUITE_POPEN_SPAWN'] = '1'
-if not opt.system: os.environ['PYTHONPATH'] = repo_root
-
-lbl_id = ('account','label')[g.coin=='BTC'] # update as other coins adopt Core's label API
-ref_subdir = '' if g.proto.base_coin == 'BTC' else 'ethereum_classic' if g.coin == 'ETC' else g.proto.name
-altcoin_pfx = '' if g.proto.base_coin == 'BTC' else '-'+g.proto.base_coin
-tn_ext = ('','.testnet')[g.testnet]
+# step 1: delete data_dir symlink in ./test;
+if not ('resume' in uopts or 'skip_deps' in uopts):
+	try: os.unlink(data_dir)
+	except: pass
 
-coin_sel = g.coin.lower()
-if g.coin.lower() in ('eth','etc'): coin_sel = 'btc'
+sys.argv = [sys.argv[0]] + ['--data-dir='+data_dir] + sys.argv[1:]
 
-fork       = {'bch':'btc','btc':'btc','ltc':'ltc'}[coin_sel]
-tx_fee     = {'btc':'0.0001','bch':'0.001','ltc':'0.01'}[coin_sel]
-txbump_fee = {'btc':'123s','bch':'567s','ltc':'12345s'}[coin_sel]
+# step 2: opts.init will create new data_dir in ./test (if not 'resume' or 'skip_deps'):
+usr_args = opts.init(opts_data)
 
-rtFundAmt  = {'btc':'500','bch':'500','ltc':'5500'}[coin_sel]
-rtFee = {
-	'btc': ('20s','10s','60s','31s','10s','20s'),
-	'bch': ('20s','10s','60s','0.0001','10s','20s'),
-	'ltc': ('1000s','500s','1500s','0.05','400s','1000s')
-}[coin_sel]
-rtBals = {
-	'btc': ('499.9999488','399.9998282','399.9998147','399.9996877',
-			'52.99990000','946.99933647','999.99923647','52.9999',
-			'946.99933647'),
-	'bch': ('499.9999484','399.9999194','399.9998972','399.9997692',
-			'46.78900000','953.20966920','999.99866920','46.789',
-			'953.2096692'),
-	'ltc': ('5499.99744','5399.994425','5399.993885','5399.987535',
-			'52.99000000','10946.93753500','10999.92753500','52.99',
-			'10946.937535'),
-}[coin_sel]
-rtBals_gb = {
-	'btc': ('116.77629233','283.22339537'),
-	'bch': ('116.77637483','283.22339437'),
-	'ltc': ('5116.77036263','283.21717237')
-}[coin_sel]
-rtBobOp3 = {'btc':'S:2','bch':'L:3','ltc':'S:2'}[coin_sel]
-rtAmts = {
-	'btc': ('500',),
-	'bch': ('500',),
-	'ltc': ('5500',)
-}[coin_sel]
+# step 3: move data_dir to /dev/shm and symlink it back to ./test:
+trash_dir = os.path.join('test','trash')
+if not ('resume' in uopts or 'skip_deps' in uopts):
+	shm_dir = create_shm_dir(data_dir,trash_dir)
 
-if opt.segwit and 'S' not in g.proto.mmtypes:
-	die(1,'--segwit option incompatible with {}'.format(g.proto.__name__))
-if opt.bech32 and 'B' not in g.proto.mmtypes:
-	die(1,'--bech32 option incompatible with {}'.format(g.proto.__name__))
+check_segwit_opts()
 
-def randbool():
-	return hexlify(os.urandom(1))[1] in b'12345678'
-def get_segwit_bool():
-	return randbool() if opt.segwit_random else True if opt.segwit or opt.bech32 else False
+if opt.profile: opt.names = True
+if opt.resume: opt.skip_deps = True
 
-def disable_debug():
-	global save_debug
-	save_debug = {}
-	for k in g.env_opts:
-		if k[:11] == 'MMGEN_DEBUG':
-			save_debug[k] = os.getenv(k)
-			os.environ[k] = ''
-def restore_debug():
-	for k in save_debug:
-		os.environ[k] = save_debug[k] or ''
+if opt.exact_output:
+	def msg(s): pass
+	qmsg = qmsg_r = vmsg = vmsg_r = msg_r = msg
 
-cfgs = {
-	'15': {
-		'tmpdir':        os.path.join('test','tmp15'),
-		'wpasswd':       'Dorian-α',
-		'kapasswd':      'Grok the blockchain',
-		'addr_idx_list': '12,99,5-10,5,12', # 8 addresses
-		'dep_generators':  {
-			pwfile:        'walletgen_dfl_wallet',
-			'addrs':       'addrgen_dfl_wallet',
-			'rawtx':       'txcreate_dfl_wallet',
-			'sigtx':       'txsign_dfl_wallet',
-			'mmseed':      'export_seed_dfl_wallet',
-			'del_dw_run':  'delete_dfl_wallet',
-		},
-		'segwit': get_segwit_bool()
+cfgs = { # addr_idx_lists (except 31,32,33,34) must contain exactly 8 addresses
+	'1':  { 'wpasswd':       'Dorian-α',
+			'kapasswd':      'Grok the blockchain',
+			'addr_idx_list': '12,99,5-10,5,12',
+			'dep_generators':  {
+				pwfile:        'walletgen',
+				'mmdat':       'walletgen',
+				'addrs':       'addrgen',
+				'rawtx':       'txcreate',
+				'txbump':      'txbump',
+				'sigtx':       'txsign',
+				'mmwords':     'export_mnemonic',
+				'mmseed':      'export_seed',
+				'mmhex':       'export_hex',
+				'mmincog':     'export_incog',
+				'mmincox':     'export_incog_hex',
+				hincog_fn:     'export_incog_hidden',
+				incog_id_fn:   'export_incog_hidden',
+				'akeys.mmenc': 'keyaddrgen'
+			},
 	},
-	'16': {
-		'tmpdir':        os.path.join('test','tmp16'),
-		'wpasswd':       'My changed password',
-		'hash_preset':   '2',
-		'dep_generators': {
-			pwfile:        'passchg_dfl_wallet',
-		},
-		'segwit': get_segwit_bool()
+	'2':  { 'wpasswd':       'Hodling away',
+			'addr_idx_list': '37,45,3-6,22-23',
+			'seed_len':      128,
+			'dep_generators': {
+				'mmdat':   'walletgen2',
+				'addrs':   'addrgen2',
+				'rawtx':   'txcreate2',
+				'sigtx':   'txsign2',
+				'mmwords': 'export_mnemonic2',
+			},
 	},
-	'17': { 'tmpdir': os.path.join('test','tmp17') },
-	'18': { 'tmpdir': os.path.join('test','tmp18') },
-#	'19': { 'tmpdir': os.path.join(u'test',u'tmp19'), 'wpasswd':'abc' }, B2X
-
-	'31': { 'tmpdir': os.path.join('test','tmp31'), # L
-			'addr_idx_list':'1-2', 'segwit': False,
-			'dep_generators': {'addrs':'ref_tx_addrgen1'} },
-	'32': { 'tmpdir': os.path.join('test','tmp32'), # C
-			'addr_idx_list':'1-2', 'segwit': False,
-			'dep_generators': {'addrs':'ref_tx_addrgen2'} },
-	'33': { 'tmpdir': os.path.join('test','tmp33'), # S
-			'addr_idx_list':'1-2', 'segwit': True,
-			'dep_generators': {'addrs':'ref_tx_addrgen3'} },
-	'34': { 'tmpdir': os.path.join('test','tmp34'), # B
-			'addr_idx_list':'1-2', 'segwit': True,
-			'dep_generators': {'addrs':'ref_tx_addrgen4'} },
-
-	'1': {
-		'tmpdir':        os.path.join('test','tmp1'),
-		'wpasswd':       'Dorian-α',
-		'kapasswd':      'Grok the blockchain',
-		'addr_idx_list': '12,99,5-10,5,12', # 8 addresses
-		'dep_generators':  {
-			pwfile:        'walletgen',
-			'mmdat':       'walletgen',
-			'addrs':       'addrgen',
-			'rawtx':       'txcreate',
-			'txbump':      'txbump',
-			'sigtx':       'txsign',
-			'mmwords':     'export_mnemonic',
-			'mmseed':      'export_seed',
-			'mmhex':       'export_hex',
-			'mmincog':     'export_incog',
-			'mmincox':     'export_incog_hex',
-			hincog_fn:     'export_incog_hidden',
-			incog_id_fn:   'export_incog_hidden',
-			'akeys.mmenc': 'keyaddrgen'
-		},
-		'segwit': get_segwit_bool()
+	'3':  { 'wpasswd':       'Major miner',
+			'addr_idx_list': '73,54,1022-1023,2-5',
+			'dep_generators': {
+				'mmdat': 'walletgen3',
+				'addrs': 'addrgen3',
+				'rawtx': 'txcreate3',
+				'sigtx': 'txsign3'
+			},
 	},
-	'2': {
-		'tmpdir':        os.path.join('test','tmp2'),
-		'wpasswd':       'Hodling away',
-		'addr_idx_list': '37,45,3-6,22-23',  # 8 addresses
-		'seed_len':      128,
-		'dep_generators': {
-			'mmdat':       'walletgen2',
-			'addrs':       'addrgen2',
-			'rawtx':         'txcreate2',
-			'sigtx':         'txsign2',
-			'mmwords':     'export_mnemonic2',
-		},
-		'segwit': get_segwit_bool()
+	'4':  { 'wpasswd':       'Hashrate good',
+			'addr_idx_list': '63,1004,542-544,7-9',
+			'seed_len':      192,
+			'dep_generators': {
+				'mmdat':   'walletgen4',
+				'mmbrain': 'walletgen4',
+				'addrs':   'addrgen4',
+				'rawtx':   'txcreate4',
+				'sigtx':   'txsign4',
+				'txdo':    'txdo4',
+			},
+			'bw_filename': 'brainwallet.mmbrain',
+			'bw_params':   '192,1',
 	},
-	'20': {
-		'tmpdir':        os.path.join('test','tmp20'),
-		'wpasswd':       'Vsize it',
-		'addr_idx_list': '1-8',  # 8 addresses
-		'seed_len':      256,
-		'dep_generators': {
-			'mmdat':       'walletgen5',
-			'addrs':       'addrgen5',
-			'rawtx':       'txcreate5',
-			'sigtx':       'txsign5',
-		},
-		'segwit': get_segwit_bool()
+	'5':  { 'wpasswd':     'My changed password-α',
+			'hash_preset': '2',
+			'dep_generators': {
+				'mmdat': 'passchg',
+				pwfile:  'passchg',
+			},
 	},
-	'21': {
-		'tmpdir':        os.path.join('test','tmp21'),
-		'wpasswd':       'Vsize it',
-		'addr_idx_list': '1-8',  # 8 addresses
-		'seed_len':      256,
-		'dep_generators': {
-			'mmdat':       'walletgen6',
-			'addrs':       'addrgen6',
-			'rawtx':       'txcreate6',
-			'sigtx':       'txsign6',
-		},
-		'segwit': get_segwit_bool()
+	'6':  { 'seed_len':       128,
+			'seed_id':        'FE3C6545',
+			'ref_bw_seed_id': '33F10310',
+			'wpasswd':        'reference password',
+			'kapasswd':      '',
+			'dep_generators':  {
+				'mmdat':       'refwalletgen_1',
+				pwfile:        'refwalletgen_1',
+				'addrs':       'refaddrgen_1',
+				'akeys.mmenc': 'refkeyaddrgen_1'
+			},
 	},
-	'22': {
-		'tmpdir': os.path.join('test','tmp22'),
-		'parity_pidfile': 'parity.pid',
-		'parity_keyfile': 'parity.devkey',
-		},
-	'3': {
-		'tmpdir':        os.path.join('test','tmp3'),
-		'wpasswd':       'Major miner',
-		'addr_idx_list': '73,54,1022-1023,2-5', # 8 addresses
-		'dep_generators': {
-			'mmdat':       'walletgen3',
-			'addrs':       'addrgen3',
-			'rawtx':         'txcreate3',
-			'sigtx':         'txsign3'
-		},
-		'segwit': get_segwit_bool()
+	'7':  { 'seed_len':       192,
+			'seed_id':        '1378FC64',
+			'ref_bw_seed_id': 'CE918388',
+			'wpasswd':        'reference password',
+			'kapasswd':      '',
+			'dep_generators':  {
+				'mmdat':       'refwalletgen_2',
+				pwfile:        'refwalletgen_2',
+				'addrs':       'refaddrgen_2',
+				'akeys.mmenc': 'refkeyaddrgen_2'
+			},
 	},
-	'4': {
-		'tmpdir':        os.path.join('test','tmp4'),
-		'wpasswd':       'Hashrate good',
-		'addr_idx_list': '63,1004,542-544,7-9', # 8 addresses
-		'seed_len':      192,
-		'dep_generators': {
-			'mmdat':       'walletgen4',
-			'mmbrain':     'walletgen4',
-			'addrs':       'addrgen4',
-			'rawtx':       'txcreate4',
-			'sigtx':       'txsign4',
-			'txdo':        'txdo4',
-		},
-		'bw_filename': 'brainwallet.mmbrain',
-		'bw_params':   '192,1',
-		'segwit': get_segwit_bool()
+	'8':  { 'seed_len':       256,
+			'seed_id':        '98831F3A',
+			'ref_bw_seed_id': 'B48CD7FC',
+			'wpasswd':        'reference password',
+			'kapasswd':      '',
+			'dep_generators':  {
+				'mmdat':       'refwalletgen_3',
+				pwfile:        'refwalletgen_3',
+				'addrs':       'refaddrgen_3',
+				'akeys.mmenc': 'refkeyaddrgen_3'
+			},
 	},
-	'14': {
-		'kapasswd':      'Maxwell',
-		'tmpdir':        os.path.join('test','tmp14'),
-		'wpasswd':       'The Halving',
-		'addr_idx_list': '61,998,502-504,7-9', # 8 addresses
-		'seed_len':      256,
-		'dep_generators': {
-			'mmdat':       'walletgen14',
-			'addrs':       'addrgen14',
-			'akeys.mmenc': 'keyaddrgen14',
-		},
-		'segwit': get_segwit_bool()
+	'9':  { 'tool_enc_infn': 'tool_encrypt.in',
+			'dep_generators': {
+				'tool_encrypt.in':       'tool_encrypt',
+				'tool_encrypt.in.mmenc': 'tool_encrypt',
+			},
 	},
-	'5': {
-		'tmpdir':        os.path.join('test','tmp5'),
-		'wpasswd':       'My changed password-α',
-		'hash_preset':   '2',
-		'dep_generators': {
-			'mmdat':       'passchg',
-			pwfile:        'passchg',
-		},
-		'segwit': get_segwit_bool()
+	'14': { 'kapasswd':      'Maxwell',
+			'wpasswd':       'The Halving',
+			'addr_idx_list': '61,998,502-504,7-9',
+			'seed_len':      256,
+			'dep_generators': {
+				'mmdat':       'walletgen14',
+				'addrs':       'addrgen14',
+				'akeys.mmenc': 'keyaddrgen14',
+			},
 	},
-	'6': {
-		'name':            'reference wallet check (128-bit)',
-		'seed_len':        128,
-		'seed_id':         'FE3C6545',
-		'ref_bw_seed_id':  '33F10310',
-		'addrfile_chk': {
-			'btc': ('B230 7526 638F 38CB','A9DC 5A13 12CB 1317'),
-			'ltc': ('2B23 5E97 848A B961','AEC3 E774 0B21 0202'),
-		},
-		'addrfile_segwit_chk': {
-			'btc': ('9914 6D10 2307 F348','83C8 A6B6 ADA8 25B2'),
-			'ltc': ('CC09 A190 B7DF B7CD','0425 7893 C6F1 ECA3'),
-		},
-		'addrfile_bech32_chk': {
-			'btc': ('C529 D686 31AA ACD4','21D0 26AD 3A22 5465'),
-			'ltc': ('3DFB CFCC E180 DC9D','8C72 D5C2 07E0 5F7B'),
-		},
-		'addrfile_compressed_chk': {
-			'btc': ('95EB 8CC0 7B3B 7856','16E6 6170 154D 2202'),
-			'ltc': ('35D5 8ECA 9A42 46C3','15B3 5492 D3D3 6854'),
-		},
-		'keyaddrfile_chk': {
-			'btc': ('CF83 32FB 8A8B 08E2','1F67 B73A FF8C 5D15'),
-			'ltc': ('1896 A26C 7F14 2D01','FA0E CD4E ADAF DBF4'),
-		},
-		'keyaddrfile_segwit_chk': {
-			'btc': ('C13B F717 D4E8 CF59','BB71 175C 5416 19D8'),
-			'ltc': ('054B 9794 55B4 5D82','DE85 3CF3 9636 FE2E'),
-		},
-		'keyaddrfile_bech32_chk': {
-			'btc': ('934F 1C33 6C06 B18C','A283 5BAB 7AF3 3EA4'),
-			'ltc': ('A6AD DF53 5968 7B6A','9572 43E0 A4DC 0B2E'),
-		},
-		'keyaddrfile_compressed_chk': {
-			'btc': ('E43A FA46 5751 720A','FDEE 8E45 1C0A 02AD'),
-			'ltc': ('7603 2FE3 2145 FFAD','3FE0 5A8E 5FBE FF3E'),
-		},
-		'passfile_chk':    'EB29 DC4F 924B 289F',
-		'passfile32_chk':  '37B6 C218 2ABC 7508',
-		'passfilehex_chk': '523A F547 0E69 8323',
-		'wpasswd':         'reference password',
-		'ref_wallet':      'FE3C6545-D782B529[128,1].mmdat',
-		'ic_wallet':       'FE3C6545-E29303EA-5E229E30[128,1].mmincog',
-		'ic_wallet_hex':   'FE3C6545-BC4BE3F2-32586837[128,1].mmincox',
-
-		'hic_wallet':       'FE3C6545-161E495F-BEB7548E[128,1].incog-offset123',
-		'hic_wallet_old':   'FE3C6545-161E495F-9860A85B[128,1].incog-old.offset123',
-
-		'tmpdir':        os.path.join('test','tmp6'),
-		'kapasswd':      '',
-		'addr_idx_list': '1010,500-501,31-33,1,33,500,1011', # 8 addresses
-		'pass_idx_list': '1,4,9-11,1100',
-		'dep_generators':  {
-			'mmdat':       'refwalletgen1',
-			pwfile:        'refwalletgen1',
-			'addrs':       'refaddrgen1',
-			'akeys.mmenc': 'refkeyaddrgen1'
-		},
-		'segwit': get_segwit_bool()
+	'15': { 'wpasswd':       'Dorian-α',
+			'kapasswd':      'Grok the blockchain',
+			'addr_idx_list': '12,99,5-10,5,12',
+			'dep_generators':  {
+				pwfile:       'walletgen_dfl_wallet',
+				'addrs':      'addrgen_dfl_wallet',
+				'rawtx':      'txcreate_dfl_wallet',
+				'sigtx':      'txsign_dfl_wallet',
+				'mmseed':     'export_seed_dfl_wallet',
+				'del_dw_run': 'delete_dfl_wallet',
+			},
 	},
-	'7': {
-		'name':            'reference wallet check (192-bit)',
-		'seed_len':        192,
-		'seed_id':         '1378FC64',
-		'ref_bw_seed_id':  'CE918388',
-		'addrfile_chk': {
-			'btc': ('8C17 A5FA 0470 6E89','764C 66F9 7502 AAEA'),
-			'ltc': ('2B77 A009 D5D0 22AD','51D1 979D 0A35 F24B'),
-		},
-		'addrfile_segwit_chk': {
-			'btc': ('91C4 0414 89E4 2089','BF9F C67F ED22 A47B'),
-			'ltc': ('8F12 FA7B 9F12 594C','2609 8494 A23C F836'),
-		},
-		'addrfile_bech32_chk': {
-			'btc': ('2AA3 78DF B965 82EB','027B 1C1F 7FB2 D859'),
-			'ltc': ('951C 8FB2 FCA5 87D1','4A5D 67E0 8210 FEF2'),
-		},
-		'addrfile_compressed_chk': {
-			'btc': ('2615 8401 2E98 7ECA','A386 EE07 A356 906D'),
-			'ltc': ('197C C48C 3C37 AB0F','8DDC 5FE3 BFF9 1226'),
-		},
-		'keyaddrfile_chk': {
-			'btc': ('9648 5132 B98E 3AD9','1BD3 5A36 D51C 256D'),
-			'ltc': ('DBD4 FAB6 7E46 CD07','8822 3FDF FEC0 6A8C'),
-		},
-		'keyaddrfile_segwit_chk': {
-			'btc': ('C98B DF08 A3D5 204B','7E7F DF50 FE04 6F68'),
-			'ltc': ('1829 7FE7 2567 CB91','BE92 D19C 7589 EF30'),
-		},
-		'keyaddrfile_bech32_chk': {
-			'btc': ('4A6B 3762 DF30 9368','12DD 1888 36BA 85F7'),
-			'ltc': ('5C12 FDD4 17AB F179','E195 B28C 59C4 C5EC'),
-		},
-		'keyaddrfile_compressed_chk': {
-			'btc': ('6D6D 3D35 04FD B9C3','94BF 4BCF 10B2 394B'),
-			'ltc': ('F5DA 9D60 6798 C4E9','7918 88DE 9096 DD7A'),
-		},
-		'passfile_chk':    'ADEA 0083 094D 489A',
-		'passfile32_chk':  '2A28 C5C7 36EC 217A',
-		'passfilehex_chk': 'B11C AC6A 1464 608D',
-		'wpasswd':         'reference password',
-		'ref_wallet':      '1378FC64-6F0F9BB4[192,1].mmdat',
-		'ic_wallet':       '1378FC64-2907DE97-F980D21F[192,1].mmincog',
-		'ic_wallet_hex':   '1378FC64-4DCB5174-872806A7[192,1].mmincox',
-
-		'hic_wallet':      '1378FC64-B55E9958-77256FC1[192,1].incog.offset123',
-		'hic_wallet_old':  '1378FC64-B55E9958-D85FF20C[192,1].incog-old.offset123',
-
-		'tmpdir':        os.path.join('test','tmp7'),
-		'kapasswd':      '',
-		'addr_idx_list': '1010,500-501,31-33,1,33,500,1011', # 8 addresses
-		'pass_idx_list': '1,4,9-11,1100',
-		'dep_generators':  {
-			'mmdat':       'refwalletgen2',
-			pwfile:       'refwalletgen2',
-			'addrs':       'refaddrgen2',
-			'akeys.mmenc': 'refkeyaddrgen2'
-		},
-		'segwit': get_segwit_bool()
+	'16': { 'wpasswd':     'My changed password',
+			'hash_preset': '2',
+			'dep_generators': {
+				pwfile: 'passchg_dfl_wallet',
+			},
 	},
-	'8': {
-		'name':            'reference wallet check (256-bit)',
-		'seed_len':        256,
-		'seed_id':         '98831F3A',
-		'ref_bw_seed_id':  'B48CD7FC',
-		'addrfile_chk': {
-			'btc': ('6FEF 6FB9 7B13 5D91','424E 4326 CFFE 5F51'),
-			'ltc': ('AD52 C3FE 8924 AAF0','4EBE 2E85 E969 1B30'),
-		},
-		'addrfile_segwit_chk': {
-			'btc': ('06C1 9C87 F25C 4EE6','072C 8B07 2730 CB7A'),
-			'ltc': ('63DF E42A 0827 21C3','5DD1 D186 DBE1 59F2'),
-		},
-		'addrfile_bech32_chk': {
-			'btc': ('9D2A D4B6 5117 F02E','0527 9C39 6C1B E39A'),
-			'ltc': ('FF1C 7939 5967 AB82','ED3D 8AA4 BED4 0B40'),
-		},
-		'addrfile_compressed_chk': {
-			'btc': ('A33C 4FDE F515 F5BC','6C48 AA57 2056 C8C8'),
-			'ltc': ('3FC0 8F03 C2D6 BD19','4C0A 49B6 2DD1 1BE0'),
-		},
-		'keyaddrfile_chk': {
-			'btc': ('9F2D D781 1812 8BAD','88CC 5120 9A91 22C2'),
-			'ltc': ('B804 978A 8796 3ED4','98B5 AC35 F334 0398'),
-		},
-		'keyaddrfile_segwit_chk': {
-			'btc': ('A447 12C2 DD14 5A9B','C770 7391 C415 21F9'),
-			'ltc': ('E8A3 9F6E E164 A521','D3D5 BFDD F5D5 20BD'),
-		},
-		'keyaddrfile_bech32_chk': {
-			'btc': ('D0DD BDE3 87BE 15AE','7552 D70C AAB8 DEAA'),
-			'ltc': ('74A0 7DD5 963B 6326','2CDA A007 4B9F E9A5'),
-		},
-		'keyaddrfile_compressed_chk': {
-			'btc': ('420A 8EB5 A9E2 7814','F43A CB4A 81F3 F735'),
-			'ltc': ('8D1C 781F EB7F 44BC','05F3 5C68 FD31 FCEF'),
+	'17': {},
+	'18': {},
+	'19': { 'wpasswd':'abc' }, # B2X
+	'20': { 'wpasswd':       'Vsize it',
+			'addr_idx_list': '1-8',
+			'seed_len':      256,
+			'dep_generators': {
+				'mmdat': 'walletgen5',
+				'addrs': 'addrgen5',
+				'rawtx': 'txcreate5',
+				'sigtx': 'txsign5',
 		},
-		'passfile_chk':    '2D6D 8FBA 422E 1315',
-		'passfile32_chk':  'F6C1 CDFB 97D9 FCAE',
-		'passfilehex_chk': 'BD4F A0AC 8628 4BE4',
-		'wpasswd':         'reference password',
-		'ref_wallet':      '98831F3A-{}[256,1].mmdat'.format(('27F2BF93','E2687906')[g.testnet]),
-		'ref_addrfile':    '98831F3A{}[1,31-33,500-501,1010-1011]{}.addrs',
-		'ref_segwitaddrfile':'98831F3A{}-S[1,31-33,500-501,1010-1011]{}.addrs',
-		'ref_bech32addrfile':'98831F3A{}-B[1,31-33,500-501,1010-1011]{}.addrs',
-		'ref_keyaddrfile': '98831F3A{}[1,31-33,500-501,1010-1011]{}.akeys.mmenc',
-		'ref_passwdfile':  '98831F3A-фубар@crypto.org-b58-20[1,4,9-11,1100].pws',
-		'ref_addrfile_chksum': {
-			'btc': ('6FEF 6FB9 7B13 5D91','424E 4326 CFFE 5F51'),
-			'ltc': ('AD52 C3FE 8924 AAF0','4EBE 2E85 E969 1B30'),
-		},
-		'ref_segwitaddrfile_chksum': {
-			'btc': ('06C1 9C87 F25C 4EE6','072C 8B07 2730 CB7A'),
-			'ltc': ('63DF E42A 0827 21C3','5DD1 D186 DBE1 59F2'),
-		},
-		'ref_bech32addrfile_chksum': {
-			'btc': ('9D2A D4B6 5117 F02E','0527 9C39 6C1B E39A'),
-			'ltc': ('FF1C 7939 5967 AB82','ED3D 8AA4 BED4 0B40'),
-		},
-		'ref_keyaddrfile_chksum': {
-			'btc': ('9F2D D781 1812 8BAD','88CC 5120 9A91 22C2'),
-			'ltc': ('B804 978A 8796 3ED4','98B5 AC35 F334 0398'),
-		},
-		'ref_addrfile_chksum_zec': '903E 7225 DD86 6E01',
-		'ref_addrfile_chksum_zec_z': '9C7A 72DC 3D4A B3AF',
-		'ref_addrfile_chksum_xmr': '4369 0253 AC2C 0E38',
-		'ref_addrfile_chksum_dash':'FBC1 6B6A 0988 4403',
-		'ref_addrfile_chksum_eth': 'E554 076E 7AF6 66A3',
-		'ref_addrfile_chksum_etc': 'E97A D796 B495 E8BC',
-		'ref_keyaddrfile_chksum_zec': 'F05A 5A5C 0C8E 2617',
-		'ref_keyaddrfile_chksum_zec_z': '6B87 9B2D 0D8D 8D1E',
-		'ref_keyaddrfile_chksum_xmr': 'E0D7 9612 3D67 404A',
-		'ref_keyaddrfile_chksum_dash': 'E83D 2C63 FEA2 4142',
-		'ref_keyaddrfile_chksum_eth': 'E400 70D9 0AE3 C7C2',
-		'ref_keyaddrfile_chksum_etc': 'EF49 967D BD6C FE45',
-		'ref_passwdfile_chksum':   'A983 DAB9 5514 27FB',
-		'ref_tx_file': {
-			'btc': ('0B8D5A[15.31789,14,tl=1320969600].rawtx',
-					'0C7115[15.86255,14,tl=1320969600].testnet.rawtx'),
-			'bch': ('460D4D-BCH[10.19764,tl=1320969600].rawtx',
-					'359FD5-BCH[6.68868,tl=1320969600].testnet.rawtx'),
-			'ltc': ('AF3CDF-LTC[620.76194,1453,tl=1320969600].rawtx',
-					'A5A1E0-LTC[1454.64322,1453,tl=1320969600].testnet.rawtx'),
-			'eth': ('88FEFD-ETH[23.45495,40000].rawtx',
-					'B472BD-ETH[23.45495,40000].testnet.rawtx'),
-			'erc20': ('5881D2-MM1[1.23456,50000].rawtx',
-					'6BDB25-MM1[1.23456,50000].testnet.rawtx'),
-			'etc': ('ED3848-ETC[1.2345,40000].rawtx','')
-		},
-		'ic_wallet':       '98831F3A-5482381C-18460FB1[256,1].mmincog',
-		'ic_wallet_hex':   '98831F3A-1630A9F2-870376A9[256,1].mmincox',
-
-		'hic_wallet':       '98831F3A-F59B07A0-559CEF19[256,1].incog.offset123',
-		'hic_wallet_old':   '98831F3A-F59B07A0-848535F3[256,1].incog-old.offset123',
-
-		'tmpdir':        os.path.join('test','tmp8'),
-		'kapasswd':      '',
-		'addr_idx_list': '1010,500-501,31-33,1,33,500,1011', # 8 addresses
-		'pass_idx_list': '1,4,9-11,1100',
-
-		'dep_generators':  {
-			'mmdat':       'refwalletgen3',
-			pwfile:       'refwalletgen3',
-			'addrs':       'refaddrgen3',
-			'akeys.mmenc': 'refkeyaddrgen3'
-		},
-		'segwit': get_segwit_bool()
 	},
-	'9': {
-		'tmpdir':        os.path.join('test','tmp9'),
-		'tool_enc_infn':      'tool_encrypt.in',
-#		'tool_enc_ref_infn':  'tool_encrypt_ref.in',
-		'wpasswd':         'reference password',
-		'dep_generators': {
-			'tool_encrypt.in':            'tool_encrypt',
-			'tool_encrypt.in.mmenc':      'tool_encrypt',
-#			'tool_encrypt_ref.in':        'tool_encrypt_ref',
-#			'tool_encrypt_ref.in.mmenc':  'tool_encrypt_ref',
+	'21': { 'wpasswd':       'Vsize it',
+			'addr_idx_list': '1-8',
+			'seed_len':      256,
+			'dep_generators': {
+				'mmdat': 'walletgen6',
+				'addrs': 'addrgen6',
+				'rawtx': 'txcreate6',
+				'sigtx': 'txsign6',
 		},
 	},
+	'22': {},
+	'31': {},
+	'32': {},
+	'33': {},
+	'34': {},
 }
 
-dfl_words = os.path.join(ref_dir,cfgs['8']['seed_id']+'.mmwords')
-
-eth_rem_addrs = ('4','1')
-if g.coin in ('ETH','ETC'):
-	# The Parity dev address with lots of coins.  Create with "ethkey -b info ''":
-	eth_addr = '00a329c0648769a73afac7f9381e08fb43dbea72'
-	eth_addr_chk = '00a329c0648769A73afAc7F9381E08FB43dBEA72'
-	eth_key = '4d5db4107d237df6a3d58ee5f70ae63d73d7658d4026f2eefd2f204c81682cb7'
-	eth_burn_addr = 'deadbeef'*5
-	eth_amt1 = '999999.12345689012345678'
-	eth_amt2 = '888.111122223333444455'
-
-	# Token sends require varying amounts of gas, depending on compiler version
-	solc_ver = re.search(r'Version:\s*(.*)',
-					subprocess.Popen(['solc','--version'],stdout=subprocess.PIPE
-						).stdout.read().decode()).group(1)
-
-	if re.match(r'\b0.5.1\b',solc_ver): # Raspbian Stretch
-		vbal1 = '1.2288337'
-		vbal2 = '99.997085083'
-		vbal3 = '1.23142165'
-		vbal4 = '127.0287837'
-	elif re.match(r'\b0.5.3\b',solc_ver): # Ubuntu Bionic
-		vbal1 = '1.2288487'
-		vbal2 = '99.997092733'
-		vbal3 = '1.23142915'
-		vbal4 = '127.0287987'
-
-	eth_bals = {
-		'1': [  ('98831F3A:E:1','123.456')],
-		'2': [  ('98831F3A:E:1','123.456'),('98831F3A:E:11','1.234')],
-		'3': [  ('98831F3A:E:1','123.456'),('98831F3A:E:11','1.234'),('98831F3A:E:21','2.345')],
-		'4': [  ('98831F3A:E:1','100'),
-				('98831F3A:E:2','23.45495'),
-				('98831F3A:E:11','1.234'),
-				('98831F3A:E:21','2.345')],
-		'5': [  ('98831F3A:E:1','100'),
-				('98831F3A:E:2','23.45495'),
-				('98831F3A:E:11','1.234'),
-				('98831F3A:E:21','2.345'),
-				(eth_burn_addr + '\s+Non-MMGen',eth_amt1)],
-		'8': [  ('98831F3A:E:1','0'),
-				('98831F3A:E:2','23.45495'),
-				('98831F3A:E:11',vbal1,'a'),
-				('98831F3A:E:12','99.99895'),
-				('98831F3A:E:21','2.345'),
-				(eth_burn_addr + '\s+Non-MMGen',eth_amt1)],
-		'9': [  ('98831F3A:E:1','0'),
-				('98831F3A:E:2','23.45495'),
-				('98831F3A:E:11',vbal1,'a'),
-				('98831F3A:E:12',vbal2),
-				('98831F3A:E:21','2.345'),
-				(eth_burn_addr + '\s+Non-MMGen',eth_amt1)]
-	}
-	eth_token_bals = {
-		'1': [  ('98831F3A:E:11','1000','1.234')],
-		'2': [  ('98831F3A:E:11','998.76544',vbal3,'a'),
-				('98831F3A:E:12','1.23456','0')],
-		'3': [  ('98831F3A:E:11','110.654317776666555545',vbal1,'a'),
-				('98831F3A:E:12','1.23456','0')],
-		'4': [  ('98831F3A:E:11','110.654317776666555545',vbal1,'a'),
-				('98831F3A:E:12','1.23456','0'),
-				(eth_burn_addr + '\s+Non-MMGen',eth_amt2,eth_amt1)],
-		'5': [  ('98831F3A:E:11','110.654317776666555545',vbal1,'a'),
-				('98831F3A:E:12','1.23456','99.99895'),
-				(eth_burn_addr + '\s+Non-MMGen',eth_amt2,eth_amt1)],
-		'6': [  ('98831F3A:E:11','110.654317776666555545',vbal1,'a'),
-				('98831F3A:E:12','0',vbal2),
-				('98831F3A:E:13','1.23456','0'),
-				(eth_burn_addr + '\s+Non-MMGen',eth_amt2,eth_amt1)]
-	}
-	eth_token_bals_getbalance = {
-		'1': (vbal4,'999999.12345689012345678'),
-		'2': ('111.888877776666555545','888.111122223333444455')
-	}
-
-	def eth_args():
-		if g.coin not in ('ETH','ETC'):
-			raise TestSuiteException('for ethdev tests, --coin must be set to either ETH or ETC')
-		return ['--outdir={}'.format(cfgs['22']['tmpdir']),'--rpc-port=8549','--quiet']
+for k in cfgs:
+	cfgs[k]['tmpdir'] = os.path.join('test','tmp{}'.format(k))
+	cfgs[k]['segwit'] = randbool() if opt.segwit_random else bool(opt.segwit or opt.bech32)
 
 from copy import deepcopy
 for a,b in (('6','11'),('7','12'),('8','13')):
@@ -733,3433 +330,552 @@ for a,b in (('6','11'),('7','12'),('8','13')):
 if g.debug_utf8:
 	for k in cfgs: cfgs[k]['tmpdir'] += '-α'
 
-from collections import OrderedDict
-
-cmd_group = OrderedDict()
-
-cmd_group['help'] = OrderedDict([
-#     test               description                  depends
-	['helpscreens',     (1,'help screens',             [])],
-	['longhelpscreens', (1,'help screens (--longhelp)',[])],
-])
-
-cmd_group['dfl_wallet'] = OrderedDict([
-	['walletgen_dfl_wallet', (15,'wallet generation (default wallet)',[[[],15]])],
-	['export_seed_dfl_wallet',(15,'seed export to mmseed format (default wallet)',[[[pwfile],15]])],
-	['addrgen_dfl_wallet',(15,'address generation (default wallet)',[[[pwfile],15]])],
-	['txcreate_dfl_wallet',(15,'transaction creation (default wallet)',[[['addrs'],15]])],
-	['txsign_dfl_wallet',(15,'transaction signing (default wallet)',[[['rawtx',pwfile],15]])],
-	['passchg_dfl_wallet',(16,'password, label and hash preset change (default wallet)',[[[pwfile],15]])],
-	['walletchk_newpass_dfl_wallet',(16,'wallet check with new pw, label and hash preset',[[[pwfile],16]])],
-	['delete_dfl_wallet',(15,'delete default wallet',[[[pwfile],15]])],
-])
-
-cmd_group['main'] = OrderedDict([
-	['walletgen',       (1,'wallet generation',        [[['del_dw_run'],15]])],
-#	['walletchk',       (1,'wallet check',             [[['mmdat'],1]])],
-	['passchg',         (5,'password, label and hash preset change',[[['mmdat',pwfile],1]])],
-	['passchg_keeplabel',(5,'password, label and hash preset change (keep label)',[[['mmdat',pwfile],1]])],
-	['passchg_usrlabel',(5,'password, label and hash preset change (interactive label)',[[['mmdat',pwfile],1]])],
-	['walletchk_newpass',(5,'wallet check with new pw, label and hash preset',[[['mmdat',pwfile],5]])],
-	['addrgen',         (1,'address generation',       [[['mmdat',pwfile],1]])],
-	['txcreate',        (1,'transaction creation',     [[['addrs'],1]])],
-	['txbump',          (1,'transaction fee bumping (no send)',[[['rawtx'],1]])],
-	['txsign',          (1,'transaction signing',      [[['mmdat','rawtx',pwfile,'txbump'],1]])],
-	['txsend',          (1,'transaction sending',      [[['sigtx'],1]])],
-	# txdo must go after txsign
-	['txdo',            (1,'online transaction',       [[['sigtx','mmdat'],1]])],
-
-	['export_hex',      (1,'seed export to hexadecimal format',  [[['mmdat'],1]])],
-	['export_seed',     (1,'seed export to mmseed format',   [[['mmdat'],1]])],
-	['export_mnemonic', (1,'seed export to mmwords format',  [[['mmdat'],1]])],
-	['export_incog',    (1,'seed export to mmincog format',  [[['mmdat'],1]])],
-	['export_incog_hex',(1,'seed export to mmincog hex format', [[['mmdat'],1]])],
-	['export_incog_hidden',(1,'seed export to hidden mmincog format', [[['mmdat'],1]])],
-
-	['addrgen_hex',     (1,'address generation from mmhex file', [[['mmhex','addrs'],1]])],
-	['addrgen_seed',    (1,'address generation from mmseed file', [[['mmseed','addrs'],1]])],
-	['addrgen_mnemonic',(1,'address generation from mmwords file',[[['mmwords','addrs'],1]])],
-	['addrgen_incog',   (1,'address generation from mmincog file',[[['mmincog','addrs'],1]])],
-	['addrgen_incog_hex',(1,'address generation from mmincog hex file',[[['mmincox','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]])],
-	['txsign_keyaddr',(1,'transaction signing with key-address file', [[['akeys.mmenc','rawtx'],1]])],
-
-	['txcreate_ni',   (1,'transaction creation (non-interactive)',     [[['addrs'],1]])],
-
-	['walletgen2',(2,'wallet generation (2), 128-bit seed',     [[['del_dw_run'],15]])],
-	['addrgen2',  (2,'address generation (2)',    [[['mmdat'],2]])],
-	['txcreate2', (2,'transaction creation (2)',  [[['addrs'],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)',                  [[['del_dw_run'],15]])],
-	['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','rawtx'],3]])],
-
-	['walletgen14', (14,'wallet generation (14)',        [[['del_dw_run'],15]],14)],
-	['addrgen14',   (14,'address generation (14)',        [[['mmdat'],14]])],
-	['keyaddrgen14',(14,'key-address file generation (14)', [[['mmdat'],14]],14)],
-	['walletgen4',(4,'wallet generation (4) (brainwallet)',    [[['del_dw_run'],15]])],
-	['addrgen4',  (4,'address generation (4)',                 [[['mmdat'],4]])],
-	['txcreate4', (4,'tx creation with inputs and outputs from four seed sources, key-address file and non-MMGen inputs and outputs', [[['addrs'],1],[['addrs'],2],[['addrs'],3],[['addrs'],4],[['addrs','akeys.mmenc'],14]])],
-	['txsign4',   (4,'tx signing with inputs and outputs from incog file, mnemonic file, wallet, brainwallet, key-address file and non-MMGen inputs and outputs', [[['mmincog'],1],[['mmwords'],2],[['mmdat'],3],[['mmbrain','rawtx'],4],[['akeys.mmenc'],14]])],
-	['txdo4', (4,'tx creation,signing and sending with inputs and outputs from four seed sources, key-address file and non-MMGen inputs and outputs', [[['addrs'],1],[['addrs'],2],[['addrs'],3],[['addrs'],4],[['addrs','akeys.mmenc'],14],[['mmincog'],1],[['mmwords'],2],[['mmdat'],3],[['mmbrain','rawtx'],4],[['akeys.mmenc'],14]])], # must go after txsign4
-	['txbump4', (4,'tx fee bump + send with inputs and outputs from four seed sources, key-address file and non-MMGen inputs and outputs', [[['akeys.mmenc'],14],[['mmincog'],1],[['mmwords'],2],[['mmdat'],3],[['akeys.mmenc'],14],[['mmbrain','sigtx','mmdat','txdo'],4]])], # must go after txsign4
-
-	['walletgen5',(20,'wallet generation (5)',                   [[['del_dw_run'],15]],20)],
-	['addrgen5',  (20,'address generation (5)',                  [[['mmdat'],20]])],
-	['txcreate5', (20,'transaction creation with bad vsize (5)', [[['addrs'],20]])],
-	['txsign5',   (20,'transaction signing with bad vsize',      [[['mmdat','rawtx'],20]])],
-	['walletgen6',(21,'wallet generation (6)',                   [[['del_dw_run'],15]],21)],
-	['addrgen6',  (21,'address generation (6)',                  [[['mmdat'],21]])],
-	['txcreate6', (21,'transaction creation with corrected vsize (6)', [[['addrs'],21]])],
-	['txsign6',   (21,'transaction signing with corrected vsize',      [[['mmdat','rawtx'],21]])],
-])
-
-cmd_group['tool'] = OrderedDict([
-	['tool_encrypt',     (9,"'mmgen-tool encrypt' (random data)",     [])],
-	['tool_decrypt',     (9,"'mmgen-tool decrypt' (random data)", [[[cfgs['9']['tool_enc_infn'],cfgs['9']['tool_enc_infn']+'.mmenc'],9]])],
-#	['tool_encrypt_ref', (9,"'mmgen-tool encrypt' (reference text)",  [])],
-	['tool_find_incog_data', (9,"'mmgen-tool find_incog_data'", [[[hincog_fn],1],[[incog_id_fn],1]])],
-#	['pywallet', (9,"'mmgen-pywallet'", [])],
-])
-
-# generated reference data
-cmd_group['ref'] = (
-	# reading
-	('ref_wallet_chk', ([],'saved reference wallet')),
-	('ref_seed_chk',   ([],'saved seed file')),
-	('ref_hex_chk',    ([],'saved mmhex file')),
-	('ref_mn_chk',     ([],'saved mnemonic file')),
-	('ref_hincog_chk', ([],'saved hidden incog reference wallet')),
-	('ref_brain_chk',  ([],'saved brainwallet')),
-	# generating new reference ('abc' brainwallet) files:
-	('refwalletgen',   ([],'gen new refwallet')),
-	('refaddrgen',     (['mmdat',pwfile],'new refwallet addr chksum')),
-	('refkeyaddrgen',  (['mmdat',pwfile],'new refwallet key-addr chksum')),
-	('refaddrgen_compressed',    (['mmdat',pwfile],'new refwallet addr chksum (compressed)')),
-	('refkeyaddrgen_compressed', (['mmdat',pwfile],'new refwallet key-addr chksum (compressed)')),
-	('refpasswdgen',   (['mmdat',pwfile],'new refwallet passwd file chksum')),
-	('ref_b32passwdgen',(['mmdat',pwfile],'new refwallet passwd file chksum (base32)')),
-	('ref_hexpasswdgen',(['mmdat',pwfile],'new refwallet passwd file chksum (base32)')),
-)
-
-# reference files
-cmd_group['ref_files'] = (
-	('ref_addrfile_chk',   'saved reference address file'), # TODO: move to tooltest2
-	('ref_segwitaddrfile_chk','saved reference address file (segwit)'), # TODO: move to tooltest2
-	('ref_bech32addrfile_chk','saved reference address file (bech32)'), # TODO: move to tooltest2
-	('ref_keyaddrfile_chk','saved reference key-address file'),
-	('ref_passwdfile_chk', 'saved reference password file'),
-#	Create the fake inputs:
-#	('txcreate8',          'transaction creation (8)'),
-	('ref_tx_chk',         'saved reference tx file'), # TODO: move to tooltest2
-	('ref_brain_chk_spc3', 'saved brainwallet (non-standard spacing)'),
-	('ref_tool_decrypt',   'decryption of saved MMGen-encrypted file'),
-)
-
-# mmgen-walletconv:
-cmd_group['conv_in'] = ( # reading
-	('ref_wallet_conv',    'conversion of saved reference wallet'),
-	('ref_mn_conv',        'conversion of saved mnemonic'),
-	('ref_seed_conv',      'conversion of saved seed file'),
-	('ref_hex_conv',       'conversion of saved hexadecimal seed file'),
-	('ref_brain_conv',     'conversion of ref brainwallet'),
-	('ref_incog_conv',     'conversion of saved incog wallet'),
-	('ref_incox_conv',     'conversion of saved hex incog wallet'),
-	('ref_hincog_conv',    'conversion of saved hidden incog wallet'),
-	('ref_hincog_conv_old','conversion of saved hidden incog wallet (old format)')
-)
-
-cmd_group['conv_out'] = ( # writing
-	('ref_wallet_conv_out', 'ref seed conversion to wallet'),
-	('ref_mn_conv_out',     'ref seed conversion to mnemonic'),
-	('ref_hex_conv_out',    'ref seed conversion to hex seed'),
-	('ref_seed_conv_out',   'ref seed conversion to seed'),
-	('ref_incog_conv_out',  'ref seed conversion to incog data'),
-	('ref_incox_conv_out',  'ref seed conversion to hex incog data'),
-	('ref_hincog_conv_out', 'ref seed conversion to hidden incog data'),
-	('ref_hincog_blkdev_conv_out', 'ref seed conversion to hidden incog data on block device')
-)
-
-cmd_group['regtest'] = (
-	('regtest_setup',              'regtest (Bob and Alice) mode setup'),
-	('regtest_walletgen_bob',      'wallet generation (Bob)'),
-	('regtest_walletgen_alice',    'wallet generation (Alice)'),
-	('regtest_addrgen_bob',        'address generation (Bob)'),
-	('regtest_addrgen_alice',      'address generation (Alice)'),
-	('regtest_addrimport_bob',     "importing Bob's addresses"),
-	('regtest_addrimport_alice',   "importing Alice's addresses"),
-	('regtest_fund_bob',           "funding Bob's wallet"),
-	('regtest_fund_alice',         "funding Alice's wallet"),
-	('regtest_bob_bal1',           "Bob's balance"),
-	('regtest_bob_add_label',      "adding a 40-character UTF-8 encoded label"),
-	('regtest_bob_twview',         "viewing Bob's tracking wallet"),
-	('regtest_bob_split1',         "splitting Bob's funds"),
-	('regtest_generate',           'mining a block'),
-	('regtest_bob_bal2',           "Bob's balance"),
-	('regtest_bob_bal2a',          "Bob's balance (age_fmt=confs)"),
-	('regtest_bob_bal2b',          "Bob's balance (showempty=1)"),
-	('regtest_bob_bal2c',          "Bob's balance (showempty=1 minconf=2 age_fmt=days)"),
-	('regtest_bob_bal2d',          "Bob's balance (minconf=2)"),
-	('regtest_bob_bal2e',          "Bob's balance (showempty=1 sort=age)"),
-	('regtest_bob_bal2f',          "Bob's balance (showempty=1 sort=age,reverse)"),
-	('regtest_bob_rbf_send',       'sending funds to Alice (RBF)'),
-	('regtest_get_mempool1',       'mempool (before RBF bump)'),
-	('regtest_bob_rbf_bump',       'bumping RBF transaction'),
-	('regtest_get_mempool2',       'mempool (after RBF bump)'),
-	('regtest_generate',           'mining a block'),
-	('regtest_bob_bal3',           "Bob's balance"),
-	('regtest_bob_pre_import',     'sending to non-imported address'),
-	('regtest_generate',           'mining a block'),
-	('regtest_bob_import_addr',    'importing non-MMGen address with --rescan'),
-	('regtest_bob_bal4',           "Bob's balance (after import with rescan)"),
-	('regtest_bob_import_list',    'importing flat address list'),
-	('regtest_bob_split2',         "splitting Bob's funds"),
-	('regtest_generate',           'mining a block'),
-	('regtest_bob_bal5',           "Bob's balance"),
-	('regtest_bob_bal5_getbalance',"Bob's balance"),
-	('regtest_bob_send_non_mmgen', 'sending funds to Alice (from non-MMGen addrs)'),
-	('regtest_generate',           'mining a block'),
-	('regtest_alice_add_label1',   'adding a label'),
-	('regtest_alice_chk_label1',   'the label'),
-	('regtest_alice_add_label2',   'adding a label'),
-	('regtest_alice_chk_label2',   'the label'),
-	('regtest_alice_edit_label1',  'editing a label'),
-	('regtest_alice_chk_label3',   'the label'),
-	('regtest_alice_remove_label1','removing a label'),
-	('regtest_alice_chk_label4',   'the label'),
-	('regtest_alice_add_label_coinaddr','adding a label using the coin address'),
-	('regtest_alice_chk_label_coinaddr','the label'),
-	('regtest_alice_add_label_badaddr1','adding a label with invalid address'),
-	('regtest_alice_add_label_badaddr2','adding a label with invalid address for this chain'),
-	('regtest_alice_add_label_badaddr3','adding a label with wrong MMGen address'),
-	('regtest_alice_add_label_badaddr4','adding a label with wrong coin address'),
-	('regtest_alice_bal_rpcfail','RPC failure code'),
-	('regtest_alice_send_estimatefee','tx creation with no fee on command line'),
-	('regtest_generate',           'mining a block'),
-	('regtest_bob_bal6',           "Bob's balance"),
-	('regtest_bob_alice_bal',      "Bob and Alice's balances"),
-	('regtest_alice_bal2',         "Alice's balance"),
-	('regtest_stop',               'stopping regtest daemon'),
-)
-
-cmd_group['regtest_split'] = (
-	('regtest_split_setup',        'regtest forking scenario setup'),
-	('regtest_walletgen_bob',      "generating Bob's wallet"),
-	('regtest_addrgen_bob',        "generating Bob's addresses"),
-	('regtest_addrimport_bob',     "importing Bob's addresses"),
-	('regtest_fund_bob',           "funding Bob's wallet"),
-	('regtest_split_fork',         'regtest split fork'),
-	('regtest_split_start_btc',    'start regtest daemon (BTC)'),
-	('regtest_split_start_b2x',    'start regtest daemon (B2X)'),
-	('regtest_split_gen_btc',      'mining a block (BTC)'),
-	('regtest_split_gen_b2x',      'mining 100 blocks (B2X)'),
-	('regtest_split_do_split',     'creating coin splitting transactions'),
-	('regtest_split_sign_b2x',     'signing B2X split transaction'),
-	('regtest_split_sign_btc',     'signing BTC split transaction'),
-	('regtest_split_send_b2x',     'sending B2X split transaction'),
-	('regtest_split_send_btc',     'sending BTC split transaction'),
-	('regtest_split_gen_btc',      'mining a block (BTC)'),
-	('regtest_split_gen_b2x2',     'mining a block (B2X)'),
-	('regtest_split_txdo_timelock_bad_btc', 'sending transaction with bad locktime (BTC)'),
-	('regtest_split_txdo_timelock_good_btc','sending transaction with good locktime (BTC)'),
-	('regtest_split_txdo_timelock_bad_b2x', 'sending transaction with bad locktime (B2X)'),
-	('regtest_split_txdo_timelock_good_b2x','sending transaction with good locktime (B2X)'),
-)
-
-cmd_group['ethdev'] = (
-	('ethdev_setup',               'Ethereum Parity dev mode tests for coin {} (start parity)'.format(g.coin)),
-	('ethdev_addrgen',             'generating addresses'),
-	('ethdev_addrimport',          'importing addresses'),
-	('ethdev_addrimport_dev_addr', "importing Parity dev address 'Ox00a329c..'"),
-
-	('ethdev_txcreate1',           'creating a transaction (spend from dev address to address :1)'),
-	('ethdev_txsign1',             'signing the transaction'),
-	('ethdev_txsign1_ni',          'signing the transaction (non-interactive)'),
-	('ethdev_txsend1',             'sending the transaction'),
-	('ethdev_bal1',                'the {} balance'.format(g.coin)),
-
-	('ethdev_txcreate2',           'creating a transaction (spend from dev address to address :11)'),
-	('ethdev_txsign2',             'signing the transaction'),
-	('ethdev_txsend2',             'sending the transaction'),
-	('ethdev_bal2',                'the {} balance'.format(g.coin)),
-
-	('ethdev_txcreate3',           'creating a transaction (spend from dev address to address :21)'),
-	('ethdev_txsign3',             'signing the transaction'),
-	('ethdev_txsend3',             'sending the transaction'),
-	('ethdev_bal3',                'the {} balance'.format(g.coin)),
-
-	('ethdev_tx_status1',          'getting the transaction status'),
-
-	('ethdev_txcreate4',           'creating a transaction (spend from MMGen address, low TX fee)'),
-	('ethdev_txbump',              'bumping the transaction fee'),
-
-	('ethdev_txsign4',             'signing the transaction'),
-	('ethdev_txsend4',             'sending the transaction'),
-	('ethdev_bal4',                'the {} balance'.format(g.coin)),
-
-	('ethdev_txcreate5',           'creating a transaction (fund burn address)'),
-	('ethdev_txsign5',             'signing the transaction'),
-	('ethdev_txsend5',             'sending the transaction'),
-
-	('ethdev_addrimport_burn_addr',"importing burn address"),
-	('ethdev_bal5',                'the {} balance'.format(g.coin)),
-
-	('ethdev_add_label',           'adding a UTF-8 label'),
-	('ethdev_chk_label',           'the label'),
-	('ethdev_remove_label',        'removing the label'),
-
-	('ethdev_token_compile1',       'compiling ERC20 token #1'),
-
-	('ethdev_token_deploy1a',       'deploying ERC20 token #1 (SafeMath)'),
-	('ethdev_token_deploy1b',       'deploying ERC20 token #1 (Owned)'),
-	('ethdev_token_deploy1c',       'deploying ERC20 token #1 (Token)'),
-
-	('ethdev_tx_status2',           'getting the transaction status'),
-	('ethdev_bal6',                 'the {} balance'.format(g.coin)),
+utils = {
+#	'check_deps': 'check dependencies for specified command (WIP)', # TODO
+	'clean':      'clean specified tmp dir(s) (specify by integer, no arg = all dirs)',
+}
 
-	('ethdev_token_compile2',       'compiling ERC20 token #2'),
+def list_cmds():
+	gm = CmdGroupMgr()
+	cw,d = 0,[]
+	Msg(green('AVAILABLE COMMANDS:'))
+	for gname in gm.cmd_groups:
+		ts = gm.init_group(None,gname,None)
+		d.append((gname,ts.__doc__.strip(),gm.cmd_list,gm.dpy_data))
+		cw = max(max(len(k) for k in gm.dpy_data),cw)
 
-	('ethdev_token_deploy2a',       'deploying ERC20 token #2 (SafeMath)'),
-	('ethdev_token_deploy2b',       'deploying ERC20 token #2 (Owned)'),
-	('ethdev_token_deploy2c',       'deploying ERC20 token #2 (Token)'),
+	for gname,gdesc,clist,dpdata in d:
+		Msg('\n'+green('{!r} - {}:'.format(gname,gdesc)))
+		for cmd in clist:
+			data = dpdata[cmd]
+			Msg('    {:{w}} - {}'.format(cmd,data if type(data) == str else data[1],w=cw))
 
-	('ethdev_contract_deploy',      'deploying contract (create,sign,send)'),
+	w = max(map(len,utils))
+	Msg('\n'+green('AVAILABLE UTILITIES:'))
+	for cmd in sorted(utils):
+		Msg('  {:{w}} - {}'.format(cmd,utils[cmd],w=w))
 
-	('ethdev_token_fund_users',     'transferring token funds from dev to user'),
-	('ethdev_token_user_bals',      'show balances after transfer'),
-	('ethdev_token_addrgen',       'generating token addresses'),
-	('ethdev_token_addrimport_badaddr1','importing token addresses (no token address)'),
-	('ethdev_token_addrimport_badaddr2','importing token addresses (bad token address)'),
-	('ethdev_token_addrimport',    'importing token addresses'),
+	sys.exit(0)
 
-	('ethdev_bal7',                'the {} balance'.format(g.coin)),
-	('ethdev_token_bal1',          'the {} balance and token balance'.format(g.coin)),
+def do_between():
+	if opt.pause:
+		confirm_continue()
+	elif opt.verbose or opt.exact_output:
+		sys.stderr.write('\n')
 
-	('ethdev_token_txcreate1',     'creating a token transaction'),
-	('ethdev_token_txsign1',       'signing the transaction'),
-	('ethdev_token_txsend1',       'sending the transaction'),
-	('ethdev_token_bal2',          'the {} balance and token balance'.format(g.coin)),
+def list_tmpdirs():
+	return dict(((k,cfgs[k]['tmpdir']) for k in cfgs))
 
-	('ethdev_token_txcreate2',     'creating a token transaction (to burn address)'),
-	('ethdev_token_txbump',        'bumping the transaction fee'),
+def clean(usr_dirs=None):
+	if opt.skip_deps: return
+	all_dirs = list_tmpdirs()
+	dirnums = map(int,(usr_dirs if usr_dirs is not None else all_dirs))
+	dirlist = list(map(str,sorted(dirnums)))
+	for d in dirlist:
+		if d in all_dirs:
+			cleandir(all_dirs[d])
+		else:
+			die(1,'{}: invalid directory number'.format(d))
+	if dirlist:
+		iqmsg(green('Cleaned tmp director{} {}'.format(suf(dirlist,'y'),' '.join(dirlist))))
+	cleandir(data_dir)
+	cleandir(trash_dir)
+	iqmsg(green("Cleaned directories '{}'".format("' '".join([data_dir,trash_dir]))))
 
-	('ethdev_token_txsign2',       'signing the transaction'),
-	('ethdev_token_txsend2',       'sending the transaction'),
-	('ethdev_token_bal3',          'the {} balance and token balance'.format(g.coin)),
+def create_tmp_dirs(shm_dir):
+	if g.platform == 'win':
+		for cfg in sorted(cfgs):
+			mk_tmpdir(cfgs[cfg]['tmpdir'])
+	else:
+		for cfg in sorted(cfgs):
+			src = os.path.join(shm_dir,cfgs[cfg]['tmpdir'].split('/')[-1])
+			mk_tmpdir(src)
+			try:
+				os.unlink(cfgs[cfg]['tmpdir'])
+			except OSError as e:
+				if e.errno != 2: raise
+			finally:
+				os.symlink(src,cfgs[cfg]['tmpdir'])
 
-	('ethdev_del_dev_addr',        "deleting the dev address"),
+def set_environ_for_spawned_scripts():
+	if os.getenv('MMGEN_DEBUG_ALL'):
+		for name in g.env_opts:
+			if name[:11] == 'MMGEN_DEBUG':
+				os.environ[name] = '1'
+	if not opt.pexpect_spawn: os.environ['MMGEN_TEST_SUITE_POPEN_SPAWN'] = '1'
+	if not opt.system: os.environ['PYTHONPATH'] = repo_root
+	if not opt.buf_keypress:
+		os.environ['MMGEN_DISABLE_HOLD_PROTECT'] = '1'
+
+	# If test.py itself is running under traceback, the spawned script shouldn't be, so disable this:
+	if os.getenv('MMGEN_TRACEBACK') and not opt.traceback:
+		os.environ['MMGEN_TRACEBACK'] = ''
+
+	# Disable color in spawned scripts so pexpect can parse their output
+	os.environ['MMGEN_DISABLE_COLOR'] = '1'
+	os.environ['MMGEN_NO_LICENSE'] = '1'
+	os.environ['MMGEN_MIN_URANDCHARS'] = '3'
+	os.environ['MMGEN_BOGUS_SEND'] = '1'
+	# Tell spawned programs they're running in the test suite
+	os.environ['MMGEN_TEST_SUITE'] = '1'
+
+def set_restore_term_at_exit():
+	import termios,atexit
+	fd = sys.stdin.fileno()
+	old = termios.tcgetattr(fd)
+	def at_exit():
+		termios.tcsetattr(fd, termios.TCSADRAIN, old)
+	atexit.register(at_exit)
 
-	('ethdev_bal1_getbalance',     'the {} balance (getbalance)'.format(g.coin)),
+class CmdGroupMgr(object):
+
+	cmd_groups = {
+		'helpscreens':      ('TestSuiteHelp',{'modname':'misc','full_data':True}),
+		'main':             ('TestSuiteMain',{'full_data':True}),
+		'conv':             ('TestSuiteWalletConv',{'is3seed':True,'modname':'wallet'}),
+		'ref3':             ('TestSuiteRef3Seed',{'is3seed':True,'modname':'ref_3seed'}),
+		'ref':              ('TestSuiteRef',{}),
+		'ref_altcoin':      ('TestSuiteRefAltcoin',{}),
+		'tool':             ('TestSuiteTool',{'modname':'misc','full_data':True}),
+		'regtest':          ('TestSuiteRegtest',{}),
+#		'chainsplit':       ('TestSuiteChainsplit',{}),
+		'ethdev':           ('TestSuiteEthdev',{}),
+		'autosign':         ('TestSuiteAutosign',{}),
+		'autosign_minimal': ('TestSuiteAutosignMinimal',{'modname':'autosign'}),
+		'autosign_live':    ('TestSuiteAutosignLive',{'modname':'autosign'}),
+		'create_ref_tx':    ('TestSuiteRefTX',{'modname':'misc','full_data':True}),
+	}
 
-	('ethdev_addrimport_token_burn_addr',"importing the token burn address"),
+	dfl_groups =  ( 'helpscreens',
+					'main',
+					'conv',
+					'ref',
+					'ref3',
+					'ref_altcoin',
+					'tool',
+					'autosign_minimal',
+					'regtest',
+					'ethdev')
+
+	def load_mod(self,gname,modname=None):
+		clsname,kwargs = self.cmd_groups[gname]
+		if modname == None and 'modname' in kwargs:
+			modname = kwargs['modname']
+		gl = globals()
+		exec('from test.test_py_d import ts_{}'.format(modname or gname),gl,gl)
+		exec('from test.test_py_d.ts_{} import {}'.format(modname or gname,clsname),gl,gl)
+		return clsname
+
+	def create_group(self,gname,full_data=False,modname=None,is3seed=False,add_dpy=False):
+		"""
+		Initializes the list 'cmd_list' and dict 'dpy_data' from module's cmd_group data.
+		Alternatively, if called with 'add_dpy=True', updates 'dpy_data' from module data
+		without touching 'cmd_list'
+		"""
+		clsname = self.load_mod(gname,modname)
+		tmpdir_nums = globals()[clsname].tmpdir_nums
+
+		cdata = []
+		for a,b in getattr(globals()[clsname],'cmd_group'):
+			if is3seed:
+				for n,(i,j) in enumerate(zip(tmpdir_nums,(128,192,256))):
+					k = '{}_{}'.format(a,n+1)
+					if type(b) == str:
+						cdata.append( (k, (i,'{} ({}-bit)'.format(b,j),[[[],i]])) )
+					else:
+						cdata.append( (k, (i,'{} ({}-bit)'.format(b[1],j),[[b[0],i]])) )
+			else:
+				cdata.append( (a, b if full_data else (tmpdir_nums[0],b,[[[],tmpdir_nums[0]]])) )
 
-	('ethdev_token_bal4',          'the {} balance and token balance'.format(g.coin)),
-	('ethdev_token_bal_getbalance','the token balance (getbalance)'),
+		if add_dpy:
+			self.dpy_data.update(dict(cdata))
+		else:
+			self.cmd_list = tuple(e[0] for e in cdata)
+			self.dpy_data = dict(cdata)
+
+		return clsname
+
+	def init_group(self,trunner,gname,spawn_prog):
+		clsname,kwargs = self.cmd_groups[gname]
+		self.create_group(gname,**kwargs)
+		return globals()[clsname](trunner,cfgs,spawn_prog)
+
+	def find_cmd_in_groups(self,cmd,group=None):
+		"""
+		Search for a test command in specified group or all configured command groups
+		and return it as a string.  Loads modules but alters no global variables.
+		"""
+		if group:
+			if not group in [e[0] for e in self.cmd_groups]:
+				die(1,'{!r}: unrecognized group'.format(group))
+			groups = [self.cmd_groups[group]]
+		else:
+			groups = self.cmd_groups
+
+		for gname in groups:
+			clsname,kwargs = self.cmd_groups[gname]
+			self.load_mod(gname,kwargs['modname'] if 'modname' in kwargs else None)
+			if cmd in dict(globals()[clsname].cmd_group):       # first search the class
+				return gname
+			if cmd in dir(globals()[clsname](None,None,None)):  # then a throwaway instance
+				return gname # cmd might be in several groups - we'll go with the first
+		return None
+
+class TestSuiteRunner(object):
+	'test suite runner'
+
+	def __init__(self,data_dir,trash_dir):
+		self.data_dir = data_dir
+		self.trash_dir = trash_dir
+		self.cmd_total = 0
+		from collections import OrderedDict
+		self.rebuild_list = OrderedDict()
+		self.gm = CmdGroupMgr()
+
+		if opt.log:
+			self.log_fd = open(log_file,'a')
+			self.log_fd.write('\nLog started: {} UTC\n'.format(make_timestr()))
+			omsg('INFO → Logging to file {!r}'.format(log_file))
+		else:
+			self.log_fd = None
 
-	('ethdev_txcreate_noamt',     'creating a transaction (full amount send)'),
-	('ethdev_txsign_noamt',       'signing the transaction'),
-	('ethdev_txsend_noamt',       'sending the transaction'),
+		if opt.coverage:
+			self.coverdir,self.accfile = init_coverage()
+			omsg('INFO → Writing coverage files to {!r}'.format(self.coverdir))
 
-	('ethdev_bal8',                'the {} balance'.format(g.coin)),
-	('ethdev_token_bal5',          'the token balance'),
+	def spawn_wrapper(  self, cmd,
+						args       = [],
+						extra_desc = '',
+						no_output  = False,
+						msg_only   = False,
+						no_msg     = False,
+						cmd_dir    = 'cmds' ):
 
-	('ethdev_token_txcreate_noamt', 'creating a token transaction (full amount send)'),
-	('ethdev_token_txsign_noamt',   'signing the transaction'),
-	('ethdev_token_txsend_noamt',   'sending the transaction'),
+		desc = self.ts.test_name if opt.names else self.gm.dpy_data[self.ts.test_name][1]
+		if extra_desc: desc += ' ' + extra_desc
 
-	('ethdev_bal9',                'the {} balance'.format(g.coin)),
-	('ethdev_token_bal6',          'the token balance'),
+		if not opt.system:
+			cmd = os.path.relpath(os.path.join(repo_root,cmd_dir,cmd))
+		elif g.platform == 'win':
+			cmd = os.path.join('/mingw64','opt','bin',cmd)
 
-	('ethdev_listaddresses1',      'listaddresses'),
-	('ethdev_listaddresses2',      'listaddresses minconf=999999999 (ignored)'),
-	('ethdev_listaddresses3',      'listaddresses sort=age (ignored)'),
-	('ethdev_listaddresses4',      'listaddresses showempty=1 sort=age (ignored)'),
+		passthru_opts = ['--{}{}'.format(k.replace('_','-'),
+							'=' + getattr(opt,k) if getattr(opt,k) != True else '')
+								for k in self.ts.passthru_opts if getattr(opt,k)]
 
-	('ethdev_token_listaddresses1','listaddresses --token=mm1'),
-	('ethdev_token_listaddresses2','listaddresses --token=mm1 showempty=1'),
+		args = [cmd] + passthru_opts + ['--data-dir='+self.data_dir] + args
 
-	('ethdev_twview1','twview'),
-	('ethdev_twview2','twview wide=1'),
-	('ethdev_twview3','twview wide=1 sort=age (ignored)'),
-	('ethdev_twview4','twview wide=1 minconf=999999999 (ignored)'),
-	('ethdev_twview5','twview wide=1 minconf=0 (ignored)'),
-	('ethdev_twview6','twview age_fmt=days (ignored)'),
+		if g.platform == 'win':
+			args = ['python3'] + args
 
-	('ethdev_token_twview1','twview --token=mm1'),
-	('ethdev_token_twview2','twview --token=mm1 wide=1'),
-	('ethdev_token_twview3','twview --token=mm1 wide=1 sort=age (ignored)'),
+		for i in args:
+			if type(i) != str:
+				m = 'Error: missing input files in cmd line?:\nName: {}\nCmdline: {!r}'
+				die(2,m.format(self.ts.test_name,args))
 
-	('ethdev_edit_label1','adding label to addr #{} in {} tracking wallet'.format(eth_rem_addrs[0],g.coin)),
-	('ethdev_edit_label2','adding label to addr #{} in {} tracking wallet'.format(eth_rem_addrs[1],g.coin)),
-	('ethdev_edit_label3','removing label from addr #{} in {} tracking wallet'.format(eth_rem_addrs[0],g.coin)),
+		if opt.coverage:
+			args = ['python3','-m','trace','--count','--coverdir='+self.coverdir,'--file='+self.accfile] + args
+		elif opt.traceback:
+			args = ['scripts/traceback_run.py'] + args
 
-	('ethdev_remove_addr1','removing addr #{} from {} tracking wallet'.format(eth_rem_addrs[0],g.coin)),
-	('ethdev_remove_addr2','removing addr #{} from {} tracking wallet'.format(eth_rem_addrs[1],g.coin)),
-	('ethdev_remove_token_addr1','removing addr #{} from {} token tracking wallet'.format(eth_rem_addrs[0],g.coin)),
-	('ethdev_remove_token_addr2','removing addr #{} from {} token tracking wallet'.format(eth_rem_addrs[1],g.coin)),
+		qargs = ['{q}{}{q}'.format(a,q=('',"'")[' ' in a]) for a in args]
+		cmd_disp = ' '.join(qargs).replace('\\','/') # for mingw
 
-	('ethdev_stop',                'stopping parity'),
-)
+		if not no_msg:
+			if opt.verbose or opt.print_cmdline or opt.exact_output:
+				clr1,clr2 = ((green,cyan),(nocolor,nocolor))[bool(opt.print_cmdline)]
+				omsg(green('Testing: {}'.format(desc)))
+				if not msg_only:
+					s = repr(cmd_disp) if g.platform == 'win' else cmd_disp
+					omsg(clr1('Executing: ') + clr2(s))
+			else:
+				omsg_r('Testing {}: '.format(desc))
 
-cmd_group['autosign'] = (
-	('autosign', 'transaction autosigning (BTC,BCH,LTC,ETH,ETC)'),
-)
-cmd_group['autosign_minimal'] = (
-	('autosign_minimal', 'transaction autosigning (BTC,ETH,ETC)'),
-)
-cmd_group['autosign_live'] = (
-	('autosign_live', 'transaction autosigning (BTC,ETH,ETC - test device insertion/removal + LED)'),
-)
+		if msg_only: return
 
-cmd_group['ref_alt'] = (
-	('ref_addrfile_gen_eth',  'generate address file (ETH)'),
-	('ref_addrfile_gen_etc',  'generate address file (ETC)'),
-	('ref_addrfile_gen_dash', 'generate address file (DASH)'),
-	('ref_addrfile_gen_zec',  'generate address file (ZEC-T)'),
-	('ref_addrfile_gen_zec_z','generate address file (ZEC-Z)'),
-	('ref_addrfile_gen_xmr',  'generate address file (XMR)'),
-	# we test the old ed25519 library in test-release.sh, so skip this
-#	('ref_addrfile_gen_xmr_old','generate address file (XMR - old (slow) ed25519 library)'),
+		if opt.log: self.log_fd.write(cmd_disp+'\n')
 
-	('ref_keyaddrfile_gen_eth',  'generate key-address file (ETH)'),
-	('ref_keyaddrfile_gen_etc',  'generate key-address file (ETC)'),
-	('ref_keyaddrfile_gen_dash', 'generate key-address file (DASH)'),
-	('ref_keyaddrfile_gen_zec',  'generate key-address file (ZEC-T)'),
-	('ref_keyaddrfile_gen_zec_z','generate key-address file (ZEC-Z)'),
-	('ref_keyaddrfile_gen_xmr',  'generate key-address file (XMR)'),
+		from test.pexpect import MMGenPexpect
+		return MMGenPexpect(args,no_output=no_output)
 
-	('ref_addrfile_chk_eth', 'reference address file (ETH)'),
-	('ref_addrfile_chk_etc', 'reference address file (ETC)'),
-	('ref_addrfile_chk_dash','reference address file (DASH)'),
-	('ref_addrfile_chk_zec', 'reference address file (ZEC-T)'),
-	('ref_addrfile_chk_zec_z','reference address file (ZEC-Z)'),
-	('ref_addrfile_chk_xmr', 'reference address file (XMR)'),
+	def end_msg(self):
+		t = int(time.time()) - self.start_time
+		m = '{} test{} performed.  Elapsed time: {:02d}:{:02d}\n'
+		sys.stderr.write(green(m.format(self.cmd_total,suf(self.cmd_total),t//60,t%60)))
 
-	('ref_keyaddrfile_chk_eth', 'reference key-address file (ETH)'),
-	('ref_keyaddrfile_chk_etc', 'reference key-address file (ETC)'),
-	('ref_keyaddrfile_chk_dash','reference key-address file (DASH)'),
-	('ref_keyaddrfile_chk_zec', 'reference key-address file (ZEC-T)'),
-	('ref_keyaddrfile_chk_zec_z','reference key-address file (ZEC-Z)'),
-	('ref_keyaddrfile_chk_xmr', 'reference key-address file (XMR)'),
-)
+	def init_group(self,gname,cmd=None):
+		ts_cls = globals()[CmdGroupMgr().load_mod(gname)]
 
-# undocumented admin cmds - precede with 'admin'
-cmd_group_admin = OrderedDict()
-cmd_group_admin['create_ref_tx'] = OrderedDict([
-	['ref_tx_addrgen1', (31,'address generation (legacy)', [[[],1]])],
-	['ref_tx_addrgen2', (32,'address generation (compressed)', [[[],1]])],
-	['ref_tx_addrgen3', (33,'address generation (segwit)', [[[],1]])],
-	['ref_tx_addrgen4', (34,'address generation (bech32)', [[[],1]])],
-	['ref_tx_txcreate', (31,'transaction creation', [[['addrs'],31],[['addrs'],32],[['addrs'],33],[['addrs'],34]])],
-])
-cmd_list_admin = OrderedDict()
-for k in cmd_group_admin: cmd_list_admin[k] = []
+		for k in ('segwit','segwit_random','bech32'):
+			if getattr(opt,k):
+				segwit_opt = k
+				break
+		else:
+			segwit_opt = None
+
+		m1 = ('test group {g!r}','{g}:{c}')[bool(cmd)].format(g=gname,c=cmd)
+		m2 = ' for {} {}net'.format(g.coin.lower(),'test' if g.testnet else 'main') \
+				if len(ts_cls.networks) != 1 else ''
+		m3 = ' (--{})'.format(segwit_opt.replace('_','-')) if segwit_opt else ''
+		m = m1 + m2 + m3
+
+		if segwit_opt and not getattr(ts_cls,'segwit_opts_ok'):
+			iqmsg('INFO → skipping ' + m)
+			return False
+
+		# 'networks = ()' means all networks allowed
+		nws = [(e.split('_')[0],'testnet') if '_' in e else (e,'mainnet') for e in ts_cls.networks]
+		if nws:
+			coin = g.coin.lower()
+			nw = ('mainnet','testnet')[g.testnet]
+			for a,b in nws:
+				if a == coin and b == nw:
+					break
+			else:
+				iqmsg('INFO → skipping ' + m)
+				return False
 
-cmd_data_admin = OrderedDict()
-for k,v in [('create_ref_tx',('reference transaction creation',[31,32,33,34]))]:
-	cmd_data_admin['info_'+k] = v
-	for i in cmd_group_admin[k]:
-		cmd_list_admin[k].append(i)
-		cmd_data_admin[i] = cmd_group_admin[k][i]
+		bmsg('Executing ' + m)
 
-cmd_data_admin['info_create_ref_tx'] = 'create reference tx',[8]
+		self.ts = self.gm.init_group(self,gname,self.spawn_wrapper)
 
-cmd_list = OrderedDict()
-for k in cmd_group: cmd_list[k] = []
+		if opt.exit_after and opt.exit_after not in self.gm.cmd_list:
+			die(1,'{!r}: command not recognized'.format(opt.exit_after))
 
-cmd_data = OrderedDict()
-for k,v in (
-		('help', ('help screens',[])),
-		('dfl_wallet', ('basic operations with default wallet',[15,16])),
-		('main', ('basic operations',[1,2,3,4,5,6,15,16])),
-		('tool', ('tools',[9]))
-	):
-	cmd_data['info_'+k] = v
-	for i in cmd_group[k]:
-		cmd_list[k].append(i)
-		cmd_data[i] = cmd_group[k][i]
+		return True
 
-cmd_data['info_ref'] = 'generated reference data',[6,7,8]
-for a,b in cmd_group['ref']:
-	for i,j in ((1,128),(2,192),(3,256)):
-		k = a+str(i)
-		cmd_list['ref'].append(k)
-		cmd_data[k] = (5+i,'{} ({}-bit)'.format(b[1],j),[[b[0],5+i]])
+	def run_tests(self,usr_args):
+		self.start_time = int(time.time())
+		if usr_args:
+			for arg in usr_args:
+				if arg in self.gm.cmd_groups:
+					if not self.init_group(arg): continue
+					clean(self.ts.tmpdir_nums)
+					for cmd in self.gm.cmd_list:
+						self.check_needs_rerun(cmd,build=True)
+						do_between()
+				elif arg in utils:
+					params = usr_args[usr_args.index(arg)+1:]
+					globals()[arg](*params)
+					sys.exit(0)
+				else:
+					if ':' in arg:
+						gname,arg = arg.split(':')
+					else:
+						gname = self.gm.find_cmd_in_groups(arg)
+					if gname:
+						if not self.init_group(gname,arg): continue
+						clean(self.ts.tmpdir_nums)
+						self.check_needs_rerun(arg,build=True)
+						do_between()
+					else:
+						die(1,'{!r}: command not recognized'.format(arg))
+		else:
+			if opt.exclude_groups:
+				exclude = opt.exclude_groups.split(',')
+				for e in exclude:
+					if e not in self.gm.dfl_groups:
+						die(1,'{!r}: group not recognized'.format(e))
+			for gname in self.gm.dfl_groups:
+				if opt.exclude_groups and gname in exclude: continue
+				if not self.init_group(gname): continue
+				clean(self.ts.tmpdir_nums)
+				for cmd in self.gm.cmd_list:
+					self.check_needs_rerun(cmd,build=True)
+					do_between()
+
+		self.end_msg()
+
+	def check_needs_rerun(self,
+			cmd,
+			build=False,
+			root=True,
+			force_delete=False,
+			dpy=False
+		):
 
-cmd_data['info_ref_files'] = 'reference files',[8]
-for a,b in cmd_group['ref_files']:
-	cmd_list['ref_files'].append(a)
-	cmd_data[a] = (8,b,[[[],8]])
+		rerun = root # force_delete is not passed to recursive call
+
+		fns = []
+		if force_delete or not root:
+			# does cmd produce a needed dependency(ies)?
+			ret = self.get_num_exts_for_cmd(cmd,dpy)
+			if ret:
+				for ext in ret[1]:
+					fn = get_file_with_ext(cfgs[ret[0]]['tmpdir'],ext,delete=build)
+					if fn:
+						if force_delete: os.unlink(fn)
+						else: fns.append(fn)
+					else: rerun = True
+
+		fdeps = self.generate_file_deps(cmd)
+		cdeps = self.generate_cmd_deps(fdeps)
+
+		for fn in fns:
+			my_age = os.stat(fn).st_mtime
+			for num,ext in fdeps:
+				f = get_file_with_ext(cfgs[num]['tmpdir'],ext,delete=build)
+				if f and os.stat(f).st_mtime > my_age:
+					rerun = True
+
+		for cdep in cdeps:
+			if self.check_needs_rerun(cdep,build=build,root=False,dpy=cmd):
+				rerun = True
 
-cmd_data['info_conv_in'] = 'wallet conversion from reference data',[11,12,13]
-for a,b in cmd_group['conv_in']:
-	for i,j in ((1,128),(2,192),(3,256)):
-		k = a+str(i)
-		cmd_list['conv_in'].append(k)
-		cmd_data[k] = (10+i,'{} ({}-bit)'.format(b,j),[[[],10+i]])
+		if build:
+			if rerun:
+				for fn in fns:
+					if not root: os.unlink(fn)
+				if not (dpy and opt.skip_deps):
+					self.run_test(cmd)
+				if not root: do_between()
+		else:
+			# If prog produces multiple files:
+			if cmd not in self.rebuild_list or rerun == True:
+				self.rebuild_list[cmd] = (rerun,fns[0] if fns else '') # FIX
 
-cmd_data['info_conv_out'] = 'wallet conversion to reference data',[11,12,13]
-for a,b in cmd_group['conv_out']:
-	for i,j in ((1,128),(2,192),(3,256)):
-		k = a+str(i)
-		cmd_list['conv_out'].append(k)
-		cmd_data[k] = (10+i,'{} ({}-bit)'.format(b,j),[[[],10+i]])
+		return rerun
 
-cmd_data['info_regtest'] = 'regtest mode',[17]
-for a,b in cmd_group['regtest']:
-	cmd_list['regtest'].append(a)
-	cmd_data[a] = (17,b,[[[],17]])
+	def run_test(self,cmd):
 
-cmd_data['info_ethdev'] = 'Ethereum tracking wallet and transaction ops',[22]
-for a,b in cmd_group['ethdev']:
-	cmd_list['ethdev'].append(a)
-	cmd_data[a] = (22,b,[[[],22]])
+		# delete files produced by this cmd
+# 		for ext,tmpdir in find_generated_exts(cmd):
+# 			print cmd, get_file_with_ext(tmpdir,ext)
 
-cmd_data['info_autosign'] = 'autosign',[18]
-for a,b in cmd_group['autosign']:
-	cmd_list['autosign'].append(a)
-	cmd_data[a] = (18,b,[[[],18]])
+		d = [(str(num),ext) for exts,num in self.gm.dpy_data[cmd][2] for ext in exts]
 
-cmd_data['info_autosign_minimal'] = 'autosign_minimal',[18]
-for a,b in cmd_group['autosign_minimal']:
-	cmd_list['autosign_minimal'].append(a)
-	cmd_data[a] = (18,b,[[[],18]])
+		# delete files depended on by this cmd
+		arg_list = [get_file_with_ext(cfgs[num]['tmpdir'],ext) for num,ext in d]
 
-cmd_data['info_autosign_live'] = 'autosign_live',[18]
-for a,b in cmd_group['autosign_live']:
-	cmd_list['autosign_live'].append(a)
-	cmd_data[a] = (18,b,[[[],18]])
 
-cmd_data['info_ref_alt'] = 'altcoin reference files',[8]
-for a,b in cmd_group['ref_alt']:
-	cmd_list['ref_alt'].append(a)
-	cmd_data[a] = (8,b,[[[],8]])
+		if opt.resume:
+			if cmd == opt.resume:
+				bmsg('Resuming at {!r}'.format(cmd))
+				opt.resume = False
+				opt.skip_deps = False
+			else:
+				return
 
-utils = {
-	'check_deps': 'check dependencies for specified command',
-	'clean':      'clean specified tmp dir(s) 1,2,3,4,5 or 6 (no arg = all dirs)',
-}
+		if opt.profile: start = time.time()
 
-addrs_per_wallet = 8
+		cdata = self.gm.dpy_data[cmd]
+		self.ts.test_name = cmd
+#		self.ts.test_dpydata = cdata
+		self.ts.tmpdir_num = cdata[0]
+#		self.ts.cfg = cfgs[str(cdata[0])] # will remove this eventually
+		cfg = cfgs[str(cdata[0])]
+		for k in (  'seed_len', 'seed_id',
+					'wpasswd', 'kapasswd',
+					'segwit', 'hash_preset',
+					'bw_filename', 'bw_params', 'ref_bw_seed_id',
+					'addr_idx_list', 'pass_idx_list' ):
+			if k in cfg:
+				setattr(self.ts,k,cfg[k])
+
+		self.process_retval(cmd,getattr(self.ts,cmd)(*arg_list)) # run the test
 
-meta_cmds = OrderedDict([
-	['gen',  ('walletgen','addrgen')],
-	['pass', ('passchg','walletchk_newpass')],
-	['tx',   ('txcreate','txsign','txsend')],
-	['export', [k for k in cmd_data if k[:7] == 'export_' and cmd_data[k][0] == 1]],
-	['gen_sp', [k for k in cmd_data if k[:8] == 'addrgen_' and cmd_data[k][0] == 1]],
-	['online', ('keyaddrgen','txsign_keyaddr')],
-	['2', [k for k in cmd_data if cmd_data[k][0] == 2]],
-	['3', [k for k in cmd_data if cmd_data[k][0] == 3]],
-	['4', [k for k in cmd_data if cmd_data[k][0] == 4]],
-	['5', [k for k in cmd_data if cmd_data[k][0] == 20]],
-	['6', [k for k in cmd_data if cmd_data[k][0] == 21]],
+		if opt.profile:
+			omsg('\r\033[50C{:.4f}'.format(time.time() - start))
 
-	['ref1', [c[0]+'1' for c in cmd_group['ref']]],
-	['ref2', [c[0]+'2' for c in cmd_group['ref']]],
-	['ref3', [c[0]+'3' for c in cmd_group['ref']]],
+		if cmd == opt.exit_after:
+			sys.exit(0)
 
-	['conv_in1', [c[0]+'1' for c in cmd_group['conv_in']]],
-	['conv_in2', [c[0]+'2' for c in cmd_group['conv_in']]],
-	['conv_in3', [c[0]+'3' for c in cmd_group['conv_in']]],
+	def process_retval(self,cmd,ret):
+		if type(ret).__name__ == 'MMGenPexpect':
+			ret.ok()
+			self.cmd_total += 1
+		elif ret == 'ok':
+			ok()
+			self.cmd_total += 1
+		elif ret == 'skip':
+			pass
+		else:
+			rdie(1,'{!r} returned {}'.format(cmd,ret))
 
-	['conv_out1', [c[0]+'1' for c in cmd_group['conv_out']]],
-	['conv_out2', [c[0]+'2' for c in cmd_group['conv_out']]],
-	['conv_out3', [c[0]+'3' for c in cmd_group['conv_out']]],
-])
+	def check_deps(self,cmds): # TODO: broken
+		if len(cmds) != 1:
+			die(1,'Usage: {} check_deps <command>'.format(g.prog_name))
 
-del cmd_group
+		cmd = cmds[0]
 
-if opt.profile: opt.names = True
-if opt.resume: opt.skip_deps = True
+		if cmd not in self.gm.cmd_list:
+			die(1,'{!r}: unrecognized command'.format(cmd))
 
-log_fd = None
-if opt.log:
-	log_fd = open(log_file,'a')
-	log_fd.write('\nLog started: {}\n'.format(make_timestr()))
+		if not opt.quiet:
+			omsg('Checking dependencies for {!r}'.format(cmd))
 
-usr_rand_chars = (5,30)[bool(opt.usr_random)]
-usr_rand_arg = '-r{}'.format(usr_rand_chars)
-cmd_total = 0
+		self.check_needs_rerun(self.ts,cmd,build=False)
 
-# Disable color in spawned scripts so pexpect can parse their output
-os.environ['MMGEN_DISABLE_COLOR'] = '1'
-os.environ['MMGEN_NO_LICENSE'] = '1'
-os.environ['MMGEN_MIN_URANDCHARS'] = '3'
-os.environ['MMGEN_BOGUS_SEND'] = '1'
-# Tell spawned programs they're running in the test suite
-os.environ['MMGEN_TEST_SUITE'] = '1'
+		w = max(map(len,self.rebuild_list)) + 1
+		for cmd in self.rebuild_list:
+			c = self.rebuild_list[cmd]
+			m = 'Rebuild' if (c[0] and c[1]) else 'Build' if c[0] else 'OK'
+			omsg('cmd {:<{w}} {}'.format(cmd+':', m, w=w))
 
-def get_segwit_arg(cfg):
-	return ['--type='+('segwit','bech32')[bool(opt.bech32)]] if cfg['segwit'] else []
+	def generate_file_deps(self,cmd):
+		return [(str(n),e) for exts,n in self.gm.dpy_data[cmd][2] for e in exts]
 
-if opt.exact_output:
-	def imsg(s):   os.write(2,s.encode() + b'\n')
-	def imsg_r(s): os.write(2,s.encode())
-	def msg(s): pass
-	qmsg = qmsg_r = vmsg = vmsg_r = msg_r = msg
-else:
-	def imsg(s): pass
-	def imsg_r(s): pass
+	def generate_cmd_deps(self,fdeps):
+		return [cfgs[str(n)]['dep_generators'][ext] for n,ext in fdeps]
 
-devnull_fh = open('/dev/null','w')
+	def get_num_exts_for_cmd(self,cmd,dpy=False): # dpy ignored here
+		try:
+			num = str(self.gm.dpy_data[cmd][0])
+		except KeyError:
+			qmsg_r('Missing dependency {!r}'.format(cmd))
+			gname = self.gm.find_cmd_in_groups(cmd)
+			if gname:
+				kwargs = self.gm.cmd_groups[gname][1]
+				kwargs.update({'add_dpy':True})
+				self.gm.create_group(gname,**kwargs)
+				num = str(self.gm.dpy_data[cmd][0])
+				qmsg(' found in group {!r}'.format(gname))
+			else:
+				qmsg(' not found in any command group!')
+				raise
+		dgl = cfgs[num]['dep_generators']
+		if cmd in dgl.values():
+			exts = [k for k in dgl if dgl[k] == cmd]
+			return (num,exts)
+		else:
+			return None
 
-def silence():
-	if not (opt.verbose or opt.exact_output):
-		g.stderr_fileno = g.stdout_fileno = devnull_fh.fileno()
+# main()
 
-def end_silence():
-	if not (opt.verbose or opt.exact_output):
-		g.stderr_fileno = 2
-		g.stdout_fileno = 1
+if not opt.skip_deps: # do this before list cmds exit, so we stay in sync with shm_dir
+	create_tmp_dirs(shm_dir)
 
 if opt.list_cmd_groups:
-	Msg(' '.join(cmd_list))
-	sys.exit(0)
-
-if opt.list_cmds:
-	from mmgen.term import get_terminal_size
-	tw = get_terminal_size()[0]
-	fs = '  {:<{w}} - {}'
-
-	Msg(green('AVAILABLE COMMANDS:'))
-	w = max(map(len,cmd_data))
-	for cmd in cmd_data:
-		if cmd[:5] == 'info_':
-			Msg(green('  {}:'.format(capfirst(cmd_data[cmd][0]))))
-			continue
-		Msg('  '+fs.format(cmd,cmd_data[cmd][1],w=w))
+	Die(0,' '.join(CmdGroupMgr.cmd_groups))
+elif opt.list_cmds:
+	list_cmds()
 
-	for cl,lbl in ((meta_cmds,'METACOMMANDS'),(cmd_list,'COMMAND GROUPS')):
-		w = max(map(len,cl))
-		Msg('\n'+green('AVAILABLE {}:'.format(lbl)))
-		for cmd in cl:
-			ft = format_par(' '.join(cl[cmd]),width=tw,indent=4,as_list=True)
-			sep = '' if not ft else ' ' if len(ft[0]) + len(cmd) < tw - 4 else '\n    '
-			Msg('  {}{}{}'.format(yellow(cmd+':'),sep,'\n'.join(ft).lstrip()))
-
-	Msg('\n'+green('AVAILABLE UTILITIES:'))
-	w = max(map(len,utils))
-	for cmd in sorted(utils):
-		Msg(fs.format(cmd,utils[cmd],w=w))
-
-	sys.exit(0)
-
-NL = ('\r\n','\n')[g.platform=='linux' and bool(opt.popen_spawn)]
-
-def get_file_with_ext(ext,mydir,delete=True,no_dot=False,return_list=False,delete_all=False):
-
-	dot = ('.','')[bool(no_dot)]
-	flist = [os.path.join(mydir,f) for f in os.listdir(mydir) if f == ext or f[-len(dot+ext):] == dot+ext]
-
-	if not flist: return False
-	if return_list: return flist
-
-	if len(flist) > 1 or delete_all:
-		if delete or delete_all:
-			if not opt.quiet:
-				msg("Multiple *.{} files in '{}' - deleting".format(ext,mydir))
-			for f in flist:
-				os.unlink(f)
-		return False
-	else:
-		return flist[0]
-
-def find_generated_exts(cmd):
-	out = []
-	for k in cfgs:
-		for ext,prog in list(cfgs[k]['dep_generators'].items()):
-			if prog == cmd:
-				out.append((ext,cfgs[k]['tmpdir']))
-	return out
-
-def get_addrfile_checksum(display=False):
-	addrfile = get_file_with_ext('addrs',cfg['tmpdir'])
-	silence()
-	chk = AddrList(addrfile).chksum
-	if opt.verbose and display: msg('Checksum: {}'.format(cyan(chk)))
-	end_silence()
-	return chk
-
-def verify_checksum_or_exit(checksum,chk):
-	if checksum != chk:
-		raise TestSuiteFatalException('Checksum error: {}'.format(chk))
-	vmsg(green('Checksums match: ') + cyan(chk))
+if opt.pause:
+	set_restore_term_at_exit()
 
-from test.mmgen_pexpect import MMGenPexpect
-class MMGenExpect(MMGenPexpect):
-
-	def __init__(self,name,mmgen_cmd,cmd_args=[],extra_desc='',no_output=False,msg_only=False,no_msg=False):
-
-		desc = ((cmd_data[name][1],name)[bool(opt.names)] + (' ' + extra_desc)).strip()
-		passthru_args = ['testnet','rpc_host','rpc_port','regtest','coin']
-
-		if not opt.system:
-			mmgen_cmd = os.path.relpath(os.path.join(repo_root,'cmds',mmgen_cmd))
-		elif g.platform == 'win':
-			mmgen_cmd = os.path.join('/mingw64','opt','bin',mmgen_cmd)
-
-		return MMGenPexpect.__init__(
-			self,
-			name,
-			mmgen_cmd,
-			cmd_args,
-			desc,
-			no_output=no_output,
-			passthru_args=passthru_args,
-			msg_only=msg_only,
-			no_msg=no_msg,
-			log_fd=log_fd)
-
-def create_fake_unspent_entry(coinaddr,al_id=None,idx=None,lbl=None,non_mmgen=False,segwit=False):
-	if 'S' not in g.proto.mmtypes: segwit = False
-	if lbl: lbl = ' ' + lbl
-	k = coinaddr.addr_fmt
-	if not segwit and k == 'p2sh': k = 'p2pkh'
-	s_beg,s_end = { 'p2pkh':  ('76a914','88ac'),
-					'p2sh':   ('a914','87'),
-					'bech32': (g.proto.witness_vernum_hex.decode()+'14','') }[k]
-	amt1,amt2 = {'btc':(10,40),'bch':(10,40),'ltc':(1000,4000)}[coin_sel]
-	ret = {
-		lbl_id: '{}:{}'.format(g.proto.base_coin.lower(),coinaddr) if non_mmgen \
-			else ('{}:{}{}'.format(al_id,idx,lbl)),
-		'vout': int(getrandnum(4) % 8),
-		'txid': hexlify(os.urandom(32)),
-		'amount': g.proto.coin_amt('{}.{}'.format(amt1 + getrandnum(4) % amt2, getrandnum(4) % 100000000)),
-		'address': coinaddr,
-		'spendable': False,
-		'scriptPubKey': '{}{}{}'.format(s_beg,coinaddr.hex.decode(),s_end).encode(),
-		'confirmations': getrandnum(3) // 2 # max: 8388608 (7 digits)
-	}
-	return ret
-
-labels = [
-	"Automotive",
-	"Travel expenses",
-	"Healthcare",
-	ref_tx_label_jp[:40],
-	ref_tx_label_zh[:40],
-	"Alice's allowance",
-	"Bob's bequest",
-	"House purchase",
-	"Real estate fund",
-	"Job 1",
-	"XYZ Corp.",
-	"Eddie's endowment",
-	"Emergency fund",
-	"Real estate fund",
-	"Ian's inheritance",
-	"",
-	"Rainy day",
-	"Fred's funds",
-	"Job 2",
-	"Carl's capital",
-]
-
-def get_label(do_shuffle=False):
-	from random import shuffle
-	global label_iter
-	try:
-		return str(next(label_iter))
-	except:
-		if do_shuffle: shuffle(labels)
-		label_iter = iter(labels)
-		return str(next(label_iter))
-
-def create_fake_unspent_data(adata,tx_data,non_mmgen_input='',non_mmgen_input_compressed=True):
-
-	out = []
-	for d in tx_data.values():
-		al = adata.addrlist(d['al_id'])
-		for n,(idx,coinaddr) in enumerate(al.addrpairs()):
-			lbl = get_label(do_shuffle=True)
-			out.append(create_fake_unspent_entry(coinaddr,d['al_id'],idx,lbl,segwit=d['segwit']))
-			if n == 0:  # create a duplicate address. This means addrs_per_wallet += 1
-				out.append(create_fake_unspent_entry(coinaddr,d['al_id'],idx,lbl,segwit=d['segwit']))
-
-	if non_mmgen_input:
-		privkey = PrivKey(os.urandom(32),compressed=non_mmgen_input_compressed,pubkey_type='std')
-		rand_coinaddr = AddrGenerator('p2pkh').to_addr(KeyGenerator('std').to_pubhex(privkey))
-		of = os.path.join(cfgs[non_mmgen_input]['tmpdir'],non_mmgen_fn)
-		write_data_to_file(of,  privkey.wif+'\n','compressed {} key'.format(g.proto.name),
-								silent=True,ignore_opt_outdir=True)
-		out.append(create_fake_unspent_entry(rand_coinaddr,non_mmgen=True,segwit=False))
-
-#	msg('\n'.join([repr(o) for o in out])); sys.exit(0)
-	return out
-
-def write_fake_data_to_file(d):
-	unspent_data_file = os.path.join(cfg['tmpdir'],'unspent.json')
-	write_data_to_file(unspent_data_file,d,'Unspent outputs',silent=True,ignore_opt_outdir=True)
-	os.environ['MMGEN_BOGUS_WALLET_DATA'] = unspent_data_file
-	bwd_msg = 'MMGEN_BOGUS_WALLET_DATA={}'.format(unspent_data_file)
-	if opt.print_cmdline: msg(bwd_msg)
-	if opt.log: log_fd.write(bwd_msg + ' ')
-	if opt.verbose or opt.exact_output:
-		sys.stderr.write("Fake transaction wallet data written to file {!r}\n".format(unspent_data_file))
-
-def create_tx_data(sources,addrs_per_wallet=addrs_per_wallet):
-	tx_data,ad = {},AddrData()
-	for s in sources:
-		afile = get_file_with_ext('addrs',cfgs[s]['tmpdir'])
-		al = AddrList(afile)
-		ad.add(al)
-		aix = AddrIdxList(fmt_str=cfgs[s]['addr_idx_list'])
-		if len(aix) != addrs_per_wallet:
-			raise TestSuiteFatalException(
-				'Address index list length != {}: {}'.format(addrs_per_wallet,repr(aix)))
-		tx_data[s] = {
-			'addrfile': afile,
-			'chk': al.chksum,
-			'al_id': al.al_id,
-			'addr_idxs': aix[-2:],
-			'segwit': cfgs[s]['segwit']
-		}
-	return ad,tx_data
-
-def make_txcreate_cmdline(tx_data):
-	privkey = PrivKey(os.urandom(32),compressed=True,pubkey_type='std')
-	t = ('p2pkh','segwit')['S' in g.proto.mmtypes]
-	rand_coinaddr = AddrGenerator(t).to_addr(KeyGenerator('std').to_pubhex(privkey))
-
-	# total of two outputs must be < 10 BTC (<1000 LTC)
-	mods = {'btc':(6,4),'bch':(6,4),'ltc':(600,400)}[coin_sel]
-	for k in cfgs:
-		cfgs[k]['amts'] = [None,None]
-		for idx,mod in enumerate(mods):
-			cfgs[k]['amts'][idx] = '{}.{}'.format(getrandnum(4) % mod, str(getrandnum(4))[:5])
-
-	cmd_args = ['--outdir='+cfg['tmpdir']]
-	for num in tx_data:
-		s = tx_data[num]
-		cmd_args += [
-			'{}:{},{}'.format(s['al_id'],s['addr_idxs'][0],cfgs[num]['amts'][0]),
-		]
-		# + one change address and one BTC address
-		if num is list(tx_data.keys())[-1]:
-			cmd_args += ['{}:{}'.format(s['al_id'],s['addr_idxs'][1])]
-			cmd_args += ['{},{}'.format(rand_coinaddr,cfgs[num]['amts'][1])]
-
-	return cmd_args + [tx_data[num]['addrfile'] for num in tx_data]
-
-def add_comments_to_addr_file(addrfile,outfile,use_labels=False):
-	silence()
-	gmsg("Adding comments to address file '{}'".format(addrfile))
-	a = AddrList(addrfile)
-	for n,idx in enumerate(a.idxs(),1):
-		if use_labels:
-			a.set_comment(idx,get_label())
-		else:
-			if n % 2: a.set_comment(idx,'Test address {}'.format(n))
-	a.format(enable_comments=True)
-	write_data_to_file(outfile,a.fmt_data,silent=True,ignore_opt_outdir=True)
-	end_silence()
-
-# 100 words chosen randomly from here:
-#   https://github.com/bitcoin/bips/pull/432/files/6332230d63149a950d05db78964a03bfd344e6b0
-rwords = """
-	алфавит алый амнезия амфора артист баян белый биатлон брат бульвар веревка вернуть весть возраст
-	восток горло горный десяток дятел ежевика жест жизнь жрать заговор здание зона изделие итог кабина
-	кавалер каждый канал керосин класс клятва князь кривой крыша крючок кузнец кукла ландшафт мальчик
-	масса масштаб матрос мрак муравей мычать негодяй носок ночной нрав оборот оружие открытие оттенок
-	палуба пароход период пехота печать письмо позор полтора понятие поцелуй почему приступ пруд пятно
-	ранее режим речь роса рынок рябой седой сердце сквозь смех снимок сойти соперник спичка стон
-	сувенир сугроб суть сцена театр тираж толк удивить улыбка фирма читатель эстония эстрада юность
-	"""
-def make_brainwallet_file(fn):
-	# Print random words with random whitespace in between
-	wl = rwords.split()
-	nwords,ws_list,max_spaces = 10,'    \n',5
-	def rand_ws_seq():
-		nchars = getrandnum(1) % max_spaces + 1
-		return ''.join([ws_list[getrandnum_range(1,200) % len(ws_list)] for i in range(nchars)])
-	rand_pairs = [wl[getrandnum_range(1,200) % len(wl)] + rand_ws_seq() for i in range(nwords)]
-	d = ''.join(rand_pairs).rstrip() + '\n'
-	if opt.verbose: msg_r('Brainwallet password:\n{}'.format(cyan(d)))
-	write_data_to_file(fn,d,'brainwallet password',silent=True,ignore_opt_outdir=True)
-
-def confirm_continue():
-	if keypress_confirm(blue('Continue?'),default_yes=True):
-		if opt.verbose or opt.exact_output: sys.stderr.write('\n')
-	else:
-		raise KeyboardInterrupt('Exiting at user request')
-
-def do_between():
-	if opt.pause:
-		confirm_continue()
-	elif opt.verbose or opt.exact_output:
-		sys.stderr.write('\n')
-
-rebuild_list = OrderedDict()
-
-def check_needs_rerun(
-		ts,
-		cmd,
-		build=False,
-		root=True,
-		force_delete=False,
-		dpy=False
-	):
-
-	rerun = (False,True)[root] # force_delete is not passed to recursive call
-
-	fns = []
-	if force_delete or not root:
-		# does cmd produce a needed dependency(ies)?
-		ret = ts.get_num_exts_for_cmd(cmd,dpy)
-		if ret:
-			for ext in ret[1]:
-				fn = get_file_with_ext(ext,cfgs[ret[0]]['tmpdir'],delete=build)
-				if fn:
-					if force_delete: os.unlink(fn)
-					else: fns.append(fn)
-				else: rerun = True
-
-	fdeps = ts.generate_file_deps(cmd)
-	cdeps = ts.generate_cmd_deps(fdeps)
-#	print 'cmd,fdeps,cdeps,fns: ',cmd,fdeps,cdeps,fns # DEBUG
-
-	for fn in fns:
-		my_age = os.stat(fn).st_mtime
-		for num,ext in fdeps:
-			f = get_file_with_ext(ext,cfgs[num]['tmpdir'],delete=build)
-			if f and os.stat(f).st_mtime > my_age:
-				rerun = True
-
-	for cdep in cdeps:
-		if check_needs_rerun(ts,cdep,build=build,root=False,dpy=cmd):
-			rerun = True
-
-	if build:
-		if rerun:
-			for fn in fns:
-				if not root: os.unlink(fn)
-			if not (dpy and opt.skip_deps):
-				ts.do_cmd(cmd)
-			if not root: do_between()
-	else:
-		# If prog produces multiple files:
-		if cmd not in rebuild_list or rerun == True:
-			rebuild_list[cmd] = (rerun,fns[0] if fns else '') # FIX
-
-	return rerun
-
-def refcheck(desc,chk,refchk):
-	vmsg("Comparing {} '{}' to stored reference".format(desc,chk))
-	if chk == refchk:
-		ok()
-	else:
-		m = "\nFatal error - {} '{}' does not match reference value '{}'.  Aborting test"
-		raise TestSuiteFatalException(m.format(desc,chk,refchk))
-
-def check_deps(cmds):
-	if len(cmds) != 1:
-		die(1,'Usage: {} check_deps <command>'.format(g.prog_name))
-
-	cmd = cmds[0]
-
-	if cmd not in cmd_data:
-		die(1,"'{}': unrecognized command".format(cmd))
-
-	if not opt.quiet:
-		msg("Checking dependencies for '{}'".format(cmd))
-
-	check_needs_rerun(ts,cmd,build=False)
-
-	w = max(map(len,rebuild_list)) + 1
-	for cmd in rebuild_list:
-		c = rebuild_list[cmd]
-		m = 'Rebuild' if (c[0] and c[1]) else 'Build' if c[0] else 'OK'
-		msg('cmd {:<{w}} {}'.format(cmd+':', m, w=w))
-#			mmsg(cmd,c)
-
-
-def clean(usr_dirs=[]):
-	if opt.skip_deps: return
-	all_dirs = MMGenTestSuite().list_tmp_dirs()
-	dirnums = (usr_dirs or all_dirs)
-	for d in sorted(dirnums):
-		if str(d) in all_dirs:
-			cleandir(all_dirs[str(d)])
-		else:
-			die(1,'{}: invalid directory number'.format(d))
-	cleandir(data_dir)
-	cleandir(trash_dir)
-
-def skip_for_win():
-	if g.platform == 'win':
-		import traceback
-		f = traceback.extract_stack()[-2][-2]
-		msg("Skipping test '{}': not supported on Windows platform".format(f))
-		return True
-	else:
-		return False
-
-class MMGenTestSuite(object):
-
-	def __init__(self):
-		pass
-
-	def list_tmp_dirs(self):
-		d = {}
-		for k in cfgs: d[k] = cfgs[k]['tmpdir']
-		return d
-
-	def get_num_exts_for_cmd(self,cmd,dpy=False): # dpy ignored here
-		num = str(cmd_data[cmd][0])
-		dgl = cfgs[num]['dep_generators']
-#	mmsg(num,cmd,dgl)
-		if cmd in dgl.values():
-			exts = [k for k in dgl if dgl[k] == cmd]
-			return (num,exts)
-		else:
-			return None
-
-	def do_cmd(self,cmd):
-
-		# delete files produced by this cmd
-# 		for ext,tmpdir in find_generated_exts(cmd):
-# 			print cmd, get_file_with_ext(ext,tmpdir)
-
-		d = [(str(num),ext) for exts,num in cmd_data[cmd][2] for ext in exts]
-
-		# delete files depended on by this cmd
-		al = [get_file_with_ext(ext,cfgs[num]['tmpdir']) for num,ext in d]
-
-		global cfg
-		cfg = cfgs[str(cmd_data[cmd][0])]
-
-		if opt.resume:
-			if cmd == opt.resume:
-				ymsg("Resuming at '{}'".format(cmd))
-				opt.resume = False
-				opt.skip_deps = False
-			else:
-				return
-
-		if opt.profile: start = time.time()
-		self.__class__.__dict__[cmd](*([self,cmd] + al))
-		if opt.profile:
-			msg('\r\033[50C{:.4f}'.format(time.time() - start))
-		global cmd_total
-		cmd_total += 1
-
-		if cmd == opt.exit_after:
-			sys.exit(0)
-
-	def generate_file_deps(self,cmd):
-		return [(str(n),e) for exts,n in cmd_data[cmd][2] for e in exts]
-
-	def generate_cmd_deps(self,fdeps):
-		return [cfgs[str(n)]['dep_generators'][ext] for n,ext in fdeps]
-
-	def helpscreens(self,name,arg='--help'):
-		scripts = (
-			'walletgen','walletconv','walletchk','txcreate','txsign','txsend','txdo','txbump',
-			'addrgen','addrimport','keygen','passchg','tool','passgen','regtest','autosign')
-		for s in scripts:
-			t = MMGenExpect(name,('mmgen-'+s),[arg],extra_desc='(mmgen-{})'.format(s),no_output=True)
-			t.ok()
-
-	def longhelpscreens(self,name): self.helpscreens(name,arg='--longhelp')
-
-	def walletgen(self,name,del_dw_run='dummy',seed_len=None,gen_dfl_wallet=False):
-		write_to_tmpfile(cfg,pwfile,cfg['wpasswd']+'\n')
-		args = ['-p1']
-		if not gen_dfl_wallet: args += ['-d',cfg['tmpdir']]
-		if seed_len: args += ['-l',str(seed_len)]
-		t = MMGenExpect(name,'mmgen-walletgen', args + [usr_rand_arg])
-		t.license()
-		t.usr_rand(usr_rand_chars)
-		t.expect('Generating')
-		t.passphrase_new('new MMGen wallet',cfg['wpasswd'])
-		t.label()
-		global have_dfl_wallet
-		if not have_dfl_wallet and gen_dfl_wallet:
-			t.expect('move it to the data directory? (Y/n): ','y')
-			have_dfl_wallet = True
-		t.written_to_file('MMGen wallet')
-		t.ok()
-
-	def walletgen_dfl_wallet(self,name,seed_len=None):
-		self.walletgen(name,seed_len=seed_len,gen_dfl_wallet=True)
-
-	def brainwalletgen_ref(self,name):
-		sl_arg = '-l{}'.format(cfg['seed_len'])
-		hp_arg = '-p{}'.format(ref_wallet_hash_preset)
-		label = "test.py ref. wallet (pw '{}', seed len {}) α".format(ref_wallet_brainpass,cfg['seed_len'])
-		bf = 'ref.mmbrain'
-		args = ['-d',cfg['tmpdir'],hp_arg,sl_arg,'-ib','-L',label]
-		write_to_tmpfile(cfg,bf,ref_wallet_brainpass)
-		write_to_tmpfile(cfg,pwfile,cfg['wpasswd'])
-		t = MMGenExpect(name,'mmgen-walletconv', args + [usr_rand_arg])
-		t.license()
-		t.expect('Enter brainwallet: ', ref_wallet_brainpass+'\n')
-		t.passphrase_new('new MMGen wallet',cfg['wpasswd'])
-		t.usr_rand(usr_rand_chars)
-		sid = os.path.basename(t.written_to_file('MMGen wallet')).split('-')[0]
-		refcheck('Seed ID',sid,cfg['seed_id'])
-
-	def refwalletgen(self,name): self.brainwalletgen_ref(name)
-
-	def passchg(self,name,wf,pf,label_action='cmdline'):
-		silence()
-		write_to_tmpfile(cfg,pwfile,get_data_from_file(pf))
-		end_silence()
-		add_args = {'cmdline': ['-d',cfg['tmpdir'],'-L','Changed label (UTF-8) α'],
-					'keep':    ['-d',trash_dir,'--keep-label'],
-					'user':    ['-d',trash_dir]
-					}[label_action]
-		t = MMGenExpect(name,'mmgen-passchg', add_args + [usr_rand_arg, '-p2'] + ([],[wf])[bool(wf)])
-		t.license()
-		t.passphrase('MMGen wallet',cfgs['1']['wpasswd'],pwtype='old')
-		t.expect_getend('Hash preset changed to ')
-		t.passphrase('MMGen wallet',cfg['wpasswd'],pwtype='new') # reuse passphrase?
-		t.expect('Repeat passphrase: ',cfg['wpasswd']+'\n')
-		t.usr_rand(usr_rand_chars)
-		if label_action == 'user':
-			t.expect('Enter a wallet label.*: ','Interactive Label (UTF-8) α\n',regex=True)
-		t.expect_getend(('Label changed to ','Reusing label ')[label_action=='keep'])
-#		t.expect_getend('Key ID changed: ')
-		if not wf:
-			t.expect("Type uppercase 'YES' to confirm: ",'YES\n')
-			t.written_to_file('New wallet')
-			t.expect('Securely deleting old wallet')
-#			t.expect('Okay to WIPE 1 regular file ? (Yes/No)','Yes\n')
-			t.expect('Wallet passphrase has changed')
-			t.expect_getend('has been changed to ')
-		else:
-			t.written_to_file('MMGen wallet')
-		t.ok()
-
-	def passchg_keeplabel(self,name,wf,pf):
-		return self.passchg(name,wf,pf,label_action='keep')
-
-	def passchg_usrlabel(self,name,wf,pf):
-		return self.passchg(name,wf,pf,label_action='user')
-
-	def passchg_dfl_wallet(self,name,pf):
-		return self.passchg(name=name,wf=None,pf=pf)
-
-	def walletchk(self,name,wf,pf,desc='MMGen wallet',add_args=[],sid=None,pw=False,extra_desc=''):
-		args = []
-		hp = cfg['hash_preset'] if 'hash_preset' in cfg else '1'
-		wf_arg = [wf] if wf else []
-		t = MMGenExpect(name,'mmgen-walletchk',
-				add_args+args+['-p',hp]+wf_arg,
-				extra_desc=extra_desc)
-		if desc != 'hidden incognito data':
-			t.expect("Getting {} from file '".format(desc))
-		if pw:
-			t.passphrase(desc,cfg['wpasswd'])
-			t.expect(['Passphrase is OK', 'Passphrase.* are correct'],regex=True)
-		chk = t.expect_getend('Valid {} for Seed ID '.format(desc))[:8]
-		if sid: t.cmp_or_die(chk,sid)
-		else: t.ok()
-
-	def walletchk_newpass(self,name,wf,pf):
-		return self.walletchk(name,wf,pf,pw=True)
-
-	def walletchk_newpass_dfl_wallet(self,name,pf):
-		return self.walletchk_newpass(name,wf=None,pf=pf)
-
-	def delete_dfl_wallet(self,name,pf):
-		with open(os.path.join(cfg['tmpdir'],'del_dw_run'),'w') as f: pass
-		if opt.no_dw_delete: return True
-		for wf in [f for f in os.listdir(g.data_dir) if f[-6:]=='.mmdat']:
-			os.unlink(os.path.join(g.data_dir,wf))
-		MMGenExpect(name,'',msg_only=True)
-		global have_dfl_wallet
-		have_dfl_wallet = False
-		ok()
-
-	def addrgen(self,name,wf,pf=None,check_ref=False,ftype='addr',id_str=None,extra_args=[],mmtype=None):
-		if ftype[:4] != 'pass' and not mmtype:
-			if cfg['segwit']: mmtype = ('segwit','bech32')[bool(opt.bech32)]
-		cmd_pfx = (ftype,'pass')[ftype[:4]=='pass']
-		t = MMGenExpect(name,'mmgen-{}gen'.format(cmd_pfx),
-				['-d',cfg['tmpdir']] +
-				extra_args +
-				([],['--type='+str(mmtype)])[bool(mmtype)] +
-				([],[wf])[bool(wf)] +
-				([id_str] if id_str else []) +
-				[cfg['{}_idx_list'.format(cmd_pfx)]],
-				extra_desc='({})'.format(mmtype) if mmtype in ('segwit','bech32') else '')
-		t.license()
-		t.passphrase('MMGen wallet',cfg['wpasswd'])
-		t.expect('Passphrase is OK')
-		desc = ('address','password')[ftype[:4]=='pass']
-		chk = t.expect_getend(r'Checksum for {} data .*?: '.format(desc),regex=True)
-		if ftype[:4] == 'pass':
-			t.expect('Encrypt password list? (y/N): ','\n')
-			t.written_to_file('Password list',oo=True)
-		else:
-			t.written_to_file('Addresses',oo=True)
-		if check_ref:
-			try:    k =   { 'pass32':  'passfile32_chk',
-							'passhex': 'passfilehex_chk',
-							'pass':    'passfile_chk'}[ftype]
-			except: k = '{}file{}_chk'.format(ftype,'_'+mmtype if mmtype else '')
-			chk_ref = cfg[k] if ftype[:4] == 'pass' else cfg[k][fork][g.testnet]
-			refcheck('{}list data checksum'.format(ftype),chk,chk_ref)
-		else:
-			t.ok()
-
-	def addrgen_dfl_wallet(self,name,pf=None,check_ref=False):
-		return self.addrgen(name,wf=None,pf=pf,check_ref=check_ref)
-
-	def refaddrgen(self,name,wf,pf):
-		self.addrgen(name,wf,pf=pf,check_ref=True)
-
-	def refaddrgen_compressed(self,name,wf,pf):
-		if opt.segwit or opt.bech32:
-			msg('Skipping non-Segwit address generation'); return True
-		self.addrgen(name,wf,pf=pf,check_ref=True,mmtype='compressed')
-
-	def txcreate_ui_common(self,t,name,
-							menu=[],inputs='1',
-							file_desc='Transaction',
-							input_sels_prompt='to spend',
-							bad_input_sels=False,non_mmgen_inputs=0,
-							interactive_fee='',
-							fee_desc='transaction fee',fee_res=None,eth_fee_res=None,
-							add_comment='',view='t',save=True,no_ok=False):
-		for choice in menu + ['q']:
-			t.expect(r'\[q\]uit view, .*?:.',choice,regex=True)
-		if bad_input_sels:
-			for r in ('x','3-1','9999'):
-				t.expect(input_sels_prompt+': ',r+'\n')
-		t.expect(input_sels_prompt+': ',inputs+'\n')
-
-		if not name[:4] == 'txdo':
-			for i in range(non_mmgen_inputs):
-				t.expect('Accept? (y/N): ','y')
-
-		have_est_fee = t.expect([fee_desc+': ','OK? (Y/n): ']) == 1
-		if have_est_fee and not interactive_fee:
-			t.send('y')
-		else:
-			if have_est_fee: t.send('n')
-			if eth_fee_res:
-				t.expect('or gas price: ',interactive_fee+'\n')
-			else:
-				t.send(interactive_fee+'\n')
-			if fee_res: t.expect(fee_res)
-			t.expect('OK? (Y/n): ','y')
-
-		t.expect('(Y/n): ','\n')     # chg amt OK?
-		t.do_comment(add_comment)
-		t.view_tx(view)
-		if not name[:4] == 'txdo':
-			t.expect('(y/N): ',('n','y')[save])
-			t.written_to_file(file_desc)
-			if not no_ok: t.ok()
-
-	def txsign_ui_common(self,t,name,   view='t',add_comment='',
-										ni=False,save=True,do_passwd=False,
-										file_desc='Signed transaction',no_ok=False,has_label=False):
-		txdo = name[:4] == 'txdo'
-
-		if do_passwd:
-			t.passphrase('MMGen wallet',cfg['wpasswd'])
-
-		if not ni and not txdo:
-			t.view_tx(view)
-			t.do_comment(add_comment,has_label=has_label)
-			t.expect('(Y/n): ',('n','y')[save])
-
-		t.written_to_file(file_desc)
-
-		if not txdo and not no_ok: t.ok()
-
-	def do_confirm_send(self,t,quiet=False,confirm_send=True):
-		t.expect('Are you sure you want to broadcast this')
-		m = ('YES, I REALLY WANT TO DO THIS','YES')[quiet]
-		t.expect("'{}' to confirm: ".format(m),('',m)[confirm_send]+'\n')
-
-	def txsend_ui_common(self,t,name,   view='n',add_comment='',
-										confirm_send=True,bogus_send=True,quiet=False,
-										file_desc='Sent transaction',no_ok=False,has_label=False):
-
-		txdo = name[:4] == 'txdo'
-		if not txdo:
-			t.license() # MMGEN_NO_LICENSE is set, so does nothing
-			t.view_tx(view)
-			t.do_comment(add_comment,has_label=has_label)
-
-		self.do_confirm_send(t,quiet=quiet,confirm_send=confirm_send)
-
-		if bogus_send:
-			txid = ''
-			t.expect('BOGUS transaction NOT sent')
-		else:
-			txid = t.expect_getend('Transaction sent: ')
-			assert len(txid) == 64,"'{}': Incorrect txid length!".format(txid)
-
-		t.written_to_file(file_desc)
-		if not txdo and not no_ok: t.ok()
-
-		return txid
-
-	def txcreate_common(self,name,
-						sources=['1'],
-						non_mmgen_input='',
-						do_label=False,
-						txdo_args=[],
-						add_args=[],
-						view='n',
-						addrs_per_wallet=addrs_per_wallet,
-						non_mmgen_input_compressed=True,
-						cmdline_inputs=False):
-
-		if opt.verbose or opt.exact_output:
-			sys.stderr.write(green('Generating fake tracking wallet info\n'))
-
-		silence()
-		ad,tx_data = create_tx_data(sources,addrs_per_wallet)
-		dfake = create_fake_unspent_data(ad,tx_data,non_mmgen_input,non_mmgen_input_compressed)
-		write_fake_data_to_file(repr(dfake))
-		cmd_args = make_txcreate_cmdline(tx_data)
-		if cmdline_inputs:
-			from mmgen.tx import TwLabel
-			cmd_args = ['--inputs={},{},{},{},{},{}'.format(
-				TwLabel(dfake[0][lbl_id]).mmid,dfake[1]['address'],
-				TwLabel(dfake[2][lbl_id]).mmid,dfake[3]['address'],
-				TwLabel(dfake[4][lbl_id]).mmid,dfake[5]['address']
-				),'--outdir='+trash_dir] + cmd_args[1:]
-		end_silence()
-
-		if opt.verbose or opt.exact_output: sys.stderr.write('\n')
-
-		t = MMGenExpect(name,
-			'mmgen-'+('txcreate','txdo')[bool(txdo_args)],
-			([],['--rbf'])[g.proto.cap('rbf')] +
-			['-f',tx_fee,'-B'] + add_args + cmd_args + txdo_args)
-
-		if t.expect([('Get','Transac')[cmdline_inputs],'Unable to connect to \S+'],regex=True) == 1:
-			raise TestSuiteException('\n'+t.p.after)
-
-		if cmdline_inputs:
-			t.written_to_file('tion')
-			t.ok()
-			return
-
-		t.license()
-
-		if txdo_args and add_args: # txdo4
-			t.do_decrypt_ka_data(hp='1',pw=cfgs['14']['kapasswd'])
-
-		for num in tx_data:
-			t.expect_getend('ting address data from file ')
-			chk=t.expect_getend(r'Checksum for address data .*?: ',regex=True)
-			verify_checksum_or_exit(tx_data[num]['chk'],chk)
-
-		# not in tracking wallet warning, (1 + num sources) times
-		for num in range(len(tx_data) + 1):
-			t.expect('Continue anyway? (y/N): ','y')
-
-		outputs_list = [(addrs_per_wallet+1)*i + 1 for i in range(len(tx_data))]
-		if non_mmgen_input: outputs_list.append(len(tx_data)*(addrs_per_wallet+1) + 1)
-
-		self.txcreate_ui_common(t,name,
-					menu=(['M'],['M','D','m','g'])[name=='txcreate'],
-					inputs=' '.join(map(str,outputs_list)),
-					add_comment=('',ref_tx_label_lat_cyr_gr)[do_label],
-					non_mmgen_inputs=(0,1)[bool(non_mmgen_input and not txdo_args)],
-					view=view)
-
-		return t
-
-	def txcreate(self,name,addrfile):
-		self.txcreate_common(name,sources=['1'],add_args=['--vsize-adj=1.01'])
-
-	def txcreate_ni(self,name,addrfile):
-		self.txcreate_common(name,sources=['1'],cmdline_inputs=True,add_args=['--yes'])
-
-	def txbump(self,name,txfile,prepend_args=[],seed_args=[]):
-		if not g.proto.cap('rbf'):
-			msg('Skipping RBF'); return True
-		args = prepend_args + ['--quiet','--outdir='+cfg['tmpdir'],txfile] + seed_args
-		t = MMGenExpect(name,'mmgen-txbump',args)
-		if seed_args:
-			t.do_decrypt_ka_data(hp='1',pw=cfgs['14']['kapasswd'])
-		t.expect('deduct the fee from (Hit ENTER for the change output): ','1\n')
-		# Fee must be > tx_fee + network relay fee (currently 0.00001)
-		t.expect('OK? (Y/n): ','\n')
-		t.expect('Enter transaction fee: ',txbump_fee+'\n')
-		t.expect('OK? (Y/n): ','\n')
-		if seed_args: # sign and send
-			t.do_comment(False,has_label=True)
-			for cnum,desc in (('1','incognito data'),('3','MMGen wallet'),('4','MMGen wallet')):
-				t.passphrase(desc,cfgs[cnum]['wpasswd'])
-			self.do_confirm_send(t,quiet=not g.debug,confirm_send=True)
-			if g.debug:
-				t.written_to_file('Transaction')
-		else:
-			t.do_comment(False)
-			t.expect('Save transaction? (y/N): ','y')
-			t.written_to_file('Transaction')
-		os.unlink(txfile) # our tx file replaces the original
-		cmd = 'touch ' + os.path.join(cfg['tmpdir'],'txbump')
-		os.system(cmd.encode())
-		t.ok()
-
-	def txdo(self,name,addrfile,wallet):
-		t = self.txcreate_common(name,sources=['1'],txdo_args=[wallet])
-		self.txsign_ui_common(t,name,view='n',do_passwd=True)
-		self.txsend_ui_common(t,name)
-		t.ok()
-
-	def txcreate_dfl_wallet(self,name,addrfile):
-		self.txcreate_common(name,sources=['15'])
-
-	def txsign_end(self,t,tnum=None,has_label=False):
-		t.expect('Signing transaction')
-		t.do_comment(False,has_label=has_label)
-		t.expect('Save signed transaction.*?\? \(Y/n\): ','y',regex=True)
-		t.written_to_file('Signed transaction' + (' #' + tnum if tnum else ''), oo=True)
-
-	def txsign(self,name,txfile,wf,pf='',bumpf='',save=True,has_label=False,do_passwd=True,extra_opts=[]):
-		t = MMGenExpect(name,'mmgen-txsign', extra_opts + ['-d',cfg['tmpdir'],txfile]+([],[wf])[bool(wf)])
-		t.license()
-		t.view_tx('n')
-		if do_passwd: t.passphrase('MMGen wallet',cfg['wpasswd'])
-		if save:
-			self.txsign_end(t,has_label=has_label)
-			t.ok()
-		else:
-			t.do_comment(False,has_label=has_label)
-			t.expect('Save signed transaction? (Y/n): ','n')
-			t.ok(exit_val=1)
-
-	def txsign_dfl_wallet(self,name,txfile,pf='',save=True,has_label=False):
-		return self.txsign(name,txfile,wf=None,pf=pf,save=save,has_label=has_label)
-
-	def txsend(self,name,sigfile,bogus_send=True,extra_opts=[]):
-		if not bogus_send: os.environ['MMGEN_BOGUS_SEND'] = ''
-		t = MMGenExpect(name,'mmgen-txsend', extra_opts + ['-d',cfg['tmpdir'],sigfile])
-		if not bogus_send: os.environ['MMGEN_BOGUS_SEND'] = '1'
-		self.txsend_ui_common(t,name,view='t',add_comment='')
-
-	def walletconv_export(self,name,wf,desc,uargs=[],out_fmt='w',pf=None,out_pw=False):
-		opts = ['-d',cfg['tmpdir'],'-o',out_fmt] + uargs + \
-			([],[wf])[bool(wf)] + ([],['-P',pf])[bool(pf)]
-		t = MMGenExpect(name,'mmgen-walletconv',opts)
-		t.license()
-		if not pf:
-			t.passphrase('MMGen wallet',cfg['wpasswd'])
-		if out_pw:
-			t.passphrase_new('new '+desc,cfg['wpasswd'])
-			t.usr_rand(usr_rand_chars)
-
-		if ' '.join(desc.split()[-2:]) == 'incognito data':
-			m = 'Generating encryption key from OS random data '
-			t.expect(m); t.expect(m)
-			ic_id = t.expect_getend('New Incog Wallet ID: ')
-			t.expect(m)
-		if desc == 'hidden incognito data':
-			write_to_tmpfile(cfg,incog_id_fn,ic_id)
-			ret = t.expect(['Create? (Y/n): ',"'YES' to confirm: "])
-			if ret == 0:
-				t.send('\n')
-				t.expect('Enter file size: ',str(hincog_bytes)+'\n')
-			else:
-				t.send('YES\n')
-		if out_fmt == 'w': t.label()
-		return t.written_to_file(capfirst(desc),oo=True),t
-
-	def export_seed(self,name,wf,desc='seed data',out_fmt='seed',pf=None):
-		f,t = self.walletconv_export(name,wf,desc=desc,out_fmt=out_fmt,pf=pf)
-		silence()
-		msg('{}: {}'.format(capfirst(desc),cyan(get_data_from_file(f,desc))))
-		end_silence()
-		t.ok()
-
-	def export_hex(self,name,wf,desc='hexadecimal seed data',out_fmt='hex',pf=None):
-		self.export_seed(name,wf,desc=desc,out_fmt=out_fmt,pf=pf)
-
-	def export_seed_dfl_wallet(self,name,pf,desc='seed data',out_fmt='seed'):
-		self.export_seed(name,wf=None,desc=desc,out_fmt=out_fmt,pf=pf)
-
-	def export_mnemonic(self,name,wf):
-		self.export_seed(name,wf,desc='mnemonic data',out_fmt='words')
-
-	def export_incog(self,name,wf,desc='incognito data',out_fmt='i',add_args=[]):
-		uargs = ['-p1',usr_rand_arg] + add_args
-		f,t = self.walletconv_export(name,wf,desc=desc,out_fmt=out_fmt,uargs=uargs,out_pw=True)
-		t.ok()
-
-	def export_incog_hex(self,name,wf):
-		self.export_incog(name,wf,desc='hex incognito data',out_fmt='xi')
-
-	# TODO: make outdir and hidden incog compatible (ignore --outdir and warn user?)
-	def export_incog_hidden(self,name,wf):
-		rf = os.path.join(cfg['tmpdir'],hincog_fn)
-		add_args = ['-J','{},{}'.format(rf,hincog_offset)]
-		self.export_incog(
-			name,wf,desc='hidden incognito data',out_fmt='hi',add_args=add_args)
-
-	def addrgen_seed(self,name,wf,foo,desc='seed data',in_fmt='seed'):
-		stdout = (False,True)[desc=='seed data'] #capture output to screen once
-		add_args = ([],['-S'])[bool(stdout)] + get_segwit_arg(cfg)
-		t = MMGenExpect(name,'mmgen-addrgen', add_args +
-				['-i'+in_fmt,'-d',cfg['tmpdir'],wf,cfg['addr_idx_list']])
-		t.license()
-		t.expect_getend('Valid {} for Seed ID '.format(desc))
-		vmsg('Comparing generated checksum with checksum from previous address file')
-		chk = t.expect_getend(r'Checksum for address data .*?: ',regex=True)
-		if stdout: t.read()
-		verify_checksum_or_exit(get_addrfile_checksum(),chk)
-		if in_fmt == 'seed':
-			t.ok()
-		else:
-			t.no_overwrite()
-			t.ok(exit_val=1)
-
-	def addrgen_hex(self,name,wf,foo,desc='hexadecimal seed data',in_fmt='hex'):
-		self.addrgen_seed(name,wf,foo,desc=desc,in_fmt=in_fmt)
-
-	def addrgen_mnemonic(self,name,wf,foo):
-		self.addrgen_seed(name,wf,foo,desc='mnemonic data',in_fmt='words')
-
-	def addrgen_incog(self,name,wf=[],foo='',in_fmt='i',desc='incognito data',args=[]):
-		t = MMGenExpect(name,'mmgen-addrgen', args + get_segwit_arg(cfg) + ['-i'+in_fmt,'-d',cfg['tmpdir']]+
-				([],[wf])[bool(wf)] + [cfg['addr_idx_list']])
-		t.license()
-		t.expect_getend('Incog Wallet ID: ')
-		t.hash_preset(desc,'1')
-		t.passphrase('{} \w{{8}}'.format(desc),cfg['wpasswd'])
-		vmsg('Comparing generated checksum with checksum from address file')
-		chk = t.expect_getend(r'Checksum for address data .*?: ',regex=True)
-		verify_checksum_or_exit(get_addrfile_checksum(),chk)
-		t.no_overwrite()
-		t.ok(exit_val=1)
-
-	def addrgen_incog_hex(self,name,wf,foo):
-		self.addrgen_incog(name,wf,'',in_fmt='xi',desc='hex incognito data')
-
-	def addrgen_incog_hidden(self,name,wf,foo):
-		rf = os.path.join(cfg['tmpdir'],hincog_fn)
-		self.addrgen_incog(name,[],'',in_fmt='hi',desc='hidden incognito data',
-			args=['-H','{},{}'.format(rf,hincog_offset),'-l',str(hincog_seedlen)])
-
-	def keyaddrgen(self,name,wf,pf=None,check_ref=False,mmtype=None):
-		if cfg['segwit'] and not mmtype:
-			mmtype = ('segwit','bech32')[bool(opt.bech32)]
-		args = ['-d',cfg['tmpdir'],usr_rand_arg,wf,cfg['addr_idx_list']]
-		t = MMGenExpect(name,'mmgen-keygen',
-				([],['--type='+str(mmtype)])[bool(mmtype)] + args,
-				extra_desc='({})'.format(mmtype) if mmtype in ('segwit','bech32') else '')
-		t.license()
-		t.passphrase('MMGen wallet',cfg['wpasswd'])
-		chk = t.expect_getend(r'Checksum for key-address data .*?: ',regex=True)
-		if check_ref:
-			k = 'keyaddrfile{}_chk'.format('_'+mmtype if mmtype else '')
-			refcheck('key-address data checksum',chk,cfg[k][fork][g.testnet])
-			return
-		t.expect('Encrypt key list? (y/N): ','y')
-		t.usr_rand(usr_rand_chars)
-		t.hash_preset('new key list','1')
-		t.passphrase_new('new key list',cfg['kapasswd'])
-		t.written_to_file('Encrypted secret keys',oo=True)
-		t.ok()
-
-	def refkeyaddrgen(self,name,wf,pf):
-		self.keyaddrgen(name,wf,pf,check_ref=True)
-
-	def refkeyaddrgen_compressed(self,name,wf,pf):
-		if opt.segwit or opt.bech32:
-			msg('Skipping non-Segwit key-address generation'); return True
-		self.keyaddrgen(name,wf,pf,check_ref=True,mmtype='compressed')
-
-	def refpasswdgen(self,name,wf,pf):
-		self.addrgen(name,wf,pf,check_ref=True,ftype='pass',id_str='alice@crypto.org')
-
-	def ref_b32passwdgen(self,name,wf,pf):
-		ea = ['--base32','--passwd-len','17']
-		self.addrgen(name,wf,pf,check_ref=True,ftype='pass32',id_str='фубар@crypto.org',extra_args=ea)
-
-	def ref_hexpasswdgen(self,name,wf,pf):
-		ea = ['--hex']
-		self.addrgen(name,wf,pf,check_ref=True,ftype='passhex',id_str='фубар@crypto.org',extra_args=ea)
-
-	def txsign_keyaddr(self,name,keyaddr_file,txfile):
-		t = MMGenExpect(name,'mmgen-txsign', ['-d',cfg['tmpdir'],'-M',keyaddr_file,txfile])
-		t.license()
-		t.do_decrypt_ka_data(hp='1',pw=cfg['kapasswd'])
-		t.view_tx('n')
-		self.txsign_end(t)
-		t.ok()
-
-	def walletgen2(self,name,del_dw_run='dummy'):
-		self.walletgen(name,seed_len=128)
-
-	def addrgen2(self,name,wf):
-		self.addrgen(name,wf,pf='')
-
-	def txcreate2(self,name,addrfile):
-		self.txcreate_common(name,sources=['2'])
-
-	def txsign2(self,name,txf1,wf1,txf2,wf2):
-		t = MMGenExpect(name,'mmgen-txsign', ['-d',cfg['tmpdir'],txf1,wf1,txf2,wf2])
-		t.license()
-		for cnum in ('1','2'):
-			t.view_tx('n')
-			t.passphrase('MMGen wallet',cfgs[cnum]['wpasswd'])
-			self.txsign_end(t,cnum)
-		t.ok()
-
-	def export_mnemonic2(self,name,wf):
-		self.export_mnemonic(name,wf)
-
-	def walletgen3(self,name,del_dw_run='dummy'):
-		self.walletgen(name)
-
-	def addrgen3(self,name,wf):
-		self.addrgen(name,wf,pf='')
-
-	def txcreate3(self,name,addrfile1,addrfile2):
-		self.txcreate_common(name,sources=['1','3'])
-
-	def txsign3(self,name,wf1,wf2,txf2):
-		t = MMGenExpect(name,'mmgen-txsign', ['-d',cfg['tmpdir'],wf1,wf2,txf2])
-		t.license()
-		t.view_tx('n')
-		for cnum in ('1','3'):
-			t.passphrase('MMGen wallet',cfgs[cnum]['wpasswd'])
-		self.txsign_end(t)
-		t.ok()
-
-	def walletgen4(self,name,del_dw_run='dummy'):
-		bwf = os.path.join(cfg['tmpdir'],cfg['bw_filename'])
-		make_brainwallet_file(bwf)
-		seed_len = str(cfg['seed_len'])
-		args = ['-d',cfg['tmpdir'],'-p1',usr_rand_arg,'-l'+seed_len,'-ib']
-		t = MMGenExpect(name,'mmgen-walletconv', args + [bwf])
-		t.license()
-		t.passphrase_new('new MMGen wallet',cfg['wpasswd'])
-		t.usr_rand(usr_rand_chars)
-		t.label()
-		t.written_to_file('MMGen wallet')
-		t.ok()
-
-	def addrgen4(self,name,wf):
-		self.addrgen(name,wf,pf='')
-
-	def txcreate4(self,name,f1,f2,f3,f4,f5,f6):
-		self.txcreate_common(name,sources=['1','2','3','4','14'],non_mmgen_input='4',do_label=True,view='y')
-
-	def txdo4(self,name,f1,f2,f3,f4,f5,f6,f7,f8,f9,f10,f11,f12):
-		non_mm_fn = os.path.join(cfg['tmpdir'],non_mmgen_fn)
-		add_args = ['-d',cfg['tmpdir'],'-i','brain','-b'+cfg['bw_params'],'-p1','-k',non_mm_fn,'-M',f12]
-		get_file_with_ext('sigtx',cfg['tmpdir'],delete_all=True) # delete tx signed by txsign4
-		t = self.txcreate_common(name,sources=['1','2','3','4','14'],
-					non_mmgen_input='4',do_label=True,txdo_args=[f7,f8,f9,f10],add_args=add_args)
-
-		for cnum,desc in (('1','incognito data'),('3','MMGen wallet')):
-			t.passphrase('{}'.format(desc),cfgs[cnum]['wpasswd'])
-
-		self.txsign_ui_common(t,name)
-		self.txsend_ui_common(t,name)
-
-		cmd = 'touch ' + os.path.join(cfg['tmpdir'],'txdo')
-		os.system(cmd.encode())
-		t.ok()
-
-	def txsign4(self,name,f1,f2,f3,f4,f5,f6):
-		non_mm_fn = os.path.join(cfg['tmpdir'],non_mmgen_fn)
-		a = ['-d',cfg['tmpdir'],'-i','brain','-b'+cfg['bw_params'],'-p1','-k',non_mm_fn,'-M',f6,f1,f2,f3,f4,f5]
-		t = MMGenExpect(name,'mmgen-txsign',a)
-		t.license()
-		t.do_decrypt_ka_data(hp='1',pw=cfgs['14']['kapasswd'])
-		t.view_tx('t')
-
-		for cnum,desc in (('1','incognito data'),('3','MMGen wallet')):
-			t.passphrase('{}'.format(desc),cfgs[cnum]['wpasswd'])
-
-		self.txsign_end(t,has_label=True)
-		t.ok()
-
-	def txbump4(self,name,f1,f2,f3,f4,f5,f6,f7,f8,f9): # f7:txfile,f9:'txdo'
-		non_mm_fn = os.path.join(cfg['tmpdir'],non_mmgen_fn)
-		self.txbump(name,f7,prepend_args=['-p1','-k',non_mm_fn,'-M',f1],seed_args=[f2,f3,f4,f5,f6,f8])
-
-	def walletgen5(self,name,del_dw_run='dummy'):
-		self.walletgen(name)
-
-	def addrgen5(self,name,wf):
-		self.addrgen(name,wf,pf='')
-
-	def txcreate5(self,name,addrfile):
-		self.txcreate_common(name,sources=['20'],non_mmgen_input='20',non_mmgen_input_compressed=False)
-
-	def txsign5(self,name,txf,wf,bad_vsize=True,add_args=[]):
-		non_mm_fn = os.path.join(cfg['tmpdir'],non_mmgen_fn)
-		t = MMGenExpect(name,'mmgen-txsign', add_args + ['-d',cfg['tmpdir'],'-k',non_mm_fn,txf,wf])
-		t.license()
-		t.view_tx('n')
-		t.passphrase('MMGen wallet',cfgs['20']['wpasswd'])
-		if bad_vsize:
-			t.expect('Estimated transaction vsize')
-			t.expect('1 transaction could not be signed')
-			exit_val = 2
-		else:
-			t.do_comment(False)
-			t.expect('Save signed transaction? (Y/n): ','y')
-			exit_val = 0
-		t.read()
-		t.ok(exit_val=exit_val)
-
-	def walletgen6(self,name,del_dw_run='dummy'):
-		self.walletgen(name)
-
-	def addrgen6(self,name,wf):
-		self.addrgen(name,wf,pf='')
-
-	def txcreate6(self,name,addrfile):
-		self.txcreate_common(
-			name,sources=['21'],non_mmgen_input='21',non_mmgen_input_compressed=False,add_args=['--vsize-adj=1.08'])
-
-	def txsign6(self,name,txf,wf):
-		return self.txsign5(name,txf,wf,bad_vsize=False,add_args=['--vsize-adj=1.08'])
-
-
-	def tool_encrypt(self,name,infile=''):
-		if infile:
-			infn = infile
-		else:
-			d = os.urandom(1033)
-			tmp_fn = cfg['tool_enc_infn']
-			write_to_tmpfile(cfg,tmp_fn,d,binary=True)
-			infn = get_tmpfile_fn(cfg,tmp_fn)
-		t = MMGenExpect(name,'mmgen-tool',['-d',cfg['tmpdir'],usr_rand_arg,'encrypt',infn])
-		t.usr_rand(usr_rand_chars)
-		t.hash_preset('user data','1')
-		t.passphrase_new('user data',tool_enc_passwd)
-		t.written_to_file('Encrypted data')
-		t.ok()
-
-# Generate the reference mmenc file
-# 	def tool_encrypt_ref(self,name):
-# 		infn = get_tmpfile_fn(cfg,cfg['tool_enc_ref_infn'])
-# 		write_data_to_file(infn,cfg['tool_enc_reftext'],silent=True)
-# 		self.tool_encrypt(name,infn)
-
-	def tool_decrypt(self,name,f1,f2):
-		of = name + '.out'
-		pre = []
-		t = MMGenExpect(name,'mmgen-tool',
-			pre+['-d',cfg['tmpdir'],'decrypt',f2,'outfile='+of,'hash_preset=1'])
-		t.passphrase('user data',tool_enc_passwd)
-		t.written_to_file('Decrypted data')
-		d1 = read_from_file(f1,binary=True)
-		d2 = read_from_file(get_tmpfile_fn(cfg,of),binary=True)
-		cmp_or_die(d1,d2,skip_ok=False)
-
-	def tool_find_incog_data(self,name,f1,f2):
-		i_id = read_from_file(f2).rstrip()
-		vmsg('Incog ID: {}'.format(cyan(i_id)))
-		t = MMGenExpect(name,'mmgen-tool',
-				['-d',cfg['tmpdir'],'find_incog_data',f1,i_id])
-		o = t.expect_getend('Incog data for ID {} found at offset '.format(i_id))
-		os.unlink(f1)
-		cmp_or_die(hincog_offset,int(o))
-
-	def autosign_live(self,name):
-		return self.autosign_minimal(name,live=True)
-
-	def autosign_minimal(self,name,live=False):
-		return self.autosign(name,
-					coins=['btc','eth'],
-					txfiles=['btc','eth','erc20','etc'],
-					txcount=7,
-					live=live)
-
-	# tests everything except device detection, mount/unmount
-	def autosign(   self,name,
-					coins=['btc','bch','ltc','eth'],
-					txfiles=['btc','bch','ltc','eth','erc20','etc'],
-					txcount=11,
-					live=False):
-
-		if skip_for_win(): return
-
-		def make_wallet(opts):
-			t = MMGenExpect(name,'mmgen-autosign',opts+['gen_key'],extra_desc='(gen_key)')
-			t.expect_getend('Wrote key file ')
-			t.ok()
-
-			t = MMGenExpect(name,'mmgen-autosign',opts+['setup'],extra_desc='(setup)')
-			t.expect('words: ','3')
-			t.expect('OK? (Y/n): ','\n')
-			mn_fn = os.path.join(ref_dir,cfgs['8']['seed_id']+'.mmwords') # 98831F3A
-			mn = read_from_file(mn_fn).strip().split()
-			mn = ['foo'] + mn[:5] + ['realiz','realized'] + mn[5:]
-			wnum = 1
-			max_wordlen = 12
-
-			def get_pad_chars(n):
-				ret = ''
-				for i in range(n):
-					m = int(hexlify(os.urandom(1)),16) % 32
-					ret += r'123579!@#$%^&*()_+-=[]{}"?/,.<>|'[m]
-				return ret
-
-			for i in range(len(mn)):
-				w = mn[i]
-				if len(w) > 5:
-					w = w + '\n'
-				else:
-					w = get_pad_chars(3 if randbool() else 0) + w[0] + get_pad_chars(3) + w[1:] + get_pad_chars(7)
-					w = w[:max_wordlen+1]
-				em,rm = 'Enter word #{}: ','Repeat word #{}: '
-				ret = t.expect((em.format(wnum),rm.format(wnum-1)))
-				if ret == 0: wnum += 1
-				for j in range(len(w)):
-					t.send(w[j])
-					time.sleep(0.005)
-			wf = t.written_to_file('Autosign wallet')
-			t.ok()
-
-		def copy_files(mountpoint,remove_signed_only=False,include_bad_tx=True):
-			fdata_in = (('btc',''),
-						('bch',''),
-						('ltc','litecoin'),
-						('eth','ethereum'),
-						('erc20','ethereum'),
-						('etc','ethereum_classic'))
-			fdata = [e for e in fdata_in if e[0] in txfiles]
-			tfns  = [cfgs['8']['ref_tx_file'][c][1] for c,d in fdata] + \
-					[cfgs['8']['ref_tx_file'][c][0] for c,d in fdata]
-			tfs = [os.path.join(ref_dir,d[1],fn) for d,fn in zip(fdata+fdata,tfns)]
-
-			for f,fn in zip(tfs,tfns):
-				if fn: # use empty fn to skip file
-					target = os.path.join(mountpoint,'tx',fn)
-					remove_signed_only or shutil.copyfile(f,target)
-					try: os.unlink(target.replace('.rawtx','.sigtx'))
-					except: pass
-
-			# make a bad tx file
-			bad_tx = os.path.join(mountpoint,'tx','bad.rawtx')
-			if include_bad_tx and not remove_signed_only:
-				with open(bad_tx,'w') as f:
-					f.write('bad tx data')
-			if not include_bad_tx:
-				try: os.unlink(bad_tx)
-				except: pass
-
-		def do_autosign_live(opts,mountpoint,led_opts=[],gen_wallet=True):
-
-			def do_mount():
-				try: subprocess.check_call(['mount',mountpoint])
-				except: pass
-
-			def do_unmount():
-				try: subprocess.check_call(['umount',mountpoint])
-				except: pass
-				omsg_r(blue('\nRemove removable device and then hit ENTER '))
-				input()
-
-			if gen_wallet: make_wallet(opts)
-			else: do_mount()
-
-			copy_files(mountpoint,include_bad_tx=not led_opts)
-
-			desc = '(sign)'
-			m1 = "Running 'mmgen-autosign wait'"
-			m2 = 'Insert removable device '
-
-			if led_opts:
-				if led_opts == ['--led']:
-					m1 = "Running 'mmgen-autosign wait' with --led. The LED should start blinking slowly now"
-				elif led_opts == ['--stealth-led']:
-					m1 = "Running 'mmgen-autosign wait' with --stealth-led. You should see no LED activity now"
-				m2 = 'Insert removable device and watch for fast LED activity during signing'
-				desc = '(sign - {})'.format(led_opts[0])
-
-			def do_loop():
-				omsg(blue(m2))
-				t.expect('{} transactions signed'.format(txcount))
-				if not led_opts:
-					t.expect('1 transaction failed to sign')
-				t.expect('Waiting')
-
-			do_unmount()
-			omsg(green(m1))
-			t = MMGenExpect(name,'mmgen-autosign',opts+led_opts+['wait'],extra_desc=desc)
-			if not opt.exact_output: omsg('')
-			do_loop()
-			do_mount() # race condition due to device insertion detection
-			copy_files(mountpoint,remove_signed_only=True,include_bad_tx=not led_opts)
-			do_unmount()
-			do_loop()
-			t.kill(2)
-			t.ok(exit_val=1)
-
-		def do_autosign(opts,mountpoint):
-			make_wallet(opts)
-			copy_files(mountpoint,include_bad_tx=True)
-			t = MMGenExpect(name,'mmgen-autosign',opts+['wait'],extra_desc='(sign)')
-			t.expect('{} transactions signed'.format(txcount))
-			t.expect('1 transaction failed to sign')
-			t.expect('Waiting')
-			t.kill(2)
-			t.ok(exit_val=1)
-
-		if live:
-			mountpoint = '/mnt/tx'
-			if not os.path.ismount(mountpoint):
-				try:
-					subprocess.check_call(['mount',mountpoint])
-					imsg("Mounted '{}'".format(mountpoint))
-				except:
-					imsg("Could not mount '{}'!  Exiting".format(mountpoint))
-					return
-
-			txdir = os.path.join(mountpoint,'tx')
-			if not os.path.isdir(txdir):
-				imsg("Directory '{}' does not exist!  Exiting".format(mountpoint))
-				return
-
-			opts = ['--coins='+','.join(coins)]
-			led_files = {   'opi': ('/sys/class/leds/orangepi:red:status/brightness',),
-							'rpi': ('/sys/class/leds/led0/brightness','/sys/class/leds/led0/trigger') }
-			for k in ('opi','rpi'):
-				if os.path.exists(led_files[k][0]):
-					led_support = k
-					break
-			else:
-				led_support = None
-
-			if led_support:
-				for fn in (led_files[led_support]):
-					subprocess.check_call(['sudo','chmod','0666',fn])
-				omsg(purple('Running autosign test with no LED'))
-				do_autosign_live(opts,mountpoint)
-				omsg(purple("Running autosign test with '--led'"))
-				do_autosign_live(opts,mountpoint,led_opts=['--led'],gen_wallet=False)
-				omsg(purple("Running autosign test with '--stealth-led'"))
-				do_autosign_live(opts,mountpoint,led_opts=['--stealth-led'],gen_wallet=False)
-			else:
-				do_autosign_live(opts,mountpoint)
-		else:
-			mountpoint = cfg['tmpdir']
-			opts = ['--no-insert-check','--mountpoint='+mountpoint,'--coins='+','.join(coins)]
-			try: os.mkdir(os.path.join(mountpoint,'tx'))
-			except: pass
-			do_autosign(opts,mountpoint)
-
-
-	# Saved reference file tests
-	def ref_wallet_conv(self,name):
-		wf = os.path.join(ref_dir,cfg['ref_wallet'])
-		self.walletconv_in(name,wf,'MMGen wallet',pw=True,oo=True)
-
-	def ref_mn_conv(self,name,ext='mmwords',desc='Mnemonic data'):
-		wf = os.path.join(ref_dir,cfg['seed_id']+'.'+ext)
-		self.walletconv_in(name,wf,desc,oo=True)
-
-	def ref_seed_conv(self,name):
-		self.ref_mn_conv(name,ext='mmseed',desc='Seed data')
-
-	def ref_hex_conv(self,name):
-		self.ref_mn_conv(name,ext='mmhex',desc='Hexadecimal seed data')
-
-	def ref_brain_conv(self,name):
-		uopts = ['-i','b','-p','1','-l',str(cfg['seed_len'])]
-		self.walletconv_in(name,None,'brainwallet',uopts,oo=True)
-
-	def ref_incog_conv(self,name,wfk='ic_wallet',in_fmt='i',desc='incognito data'):
-		uopts = ['-i',in_fmt,'-p','1','-l',str(cfg['seed_len'])]
-		wf = os.path.join(ref_dir,cfg[wfk])
-		self.walletconv_in(name,wf,desc,uopts,oo=True,pw=True)
-
-	def ref_incox_conv(self,name):
-		self.ref_incog_conv(name,in_fmt='xi',wfk='ic_wallet_hex',desc='hex incognito data')
-
-	def ref_hincog_conv(self,name,wfk='hic_wallet',add_uopts=[]):
-		ic_f = os.path.join(ref_dir,cfg[wfk])
-		uopts = ['-i','hi','-p','1','-l',str(cfg['seed_len'])] + add_uopts
-		hi_opt = ['-H','{},{}'.format(ic_f,ref_wallet_incog_offset)]
-		self.walletconv_in(name,None,'hidden incognito data',uopts+hi_opt,oo=True,pw=True)
-
-	def ref_hincog_conv_old(self,name):
-		self.ref_hincog_conv(name,wfk='hic_wallet_old',add_uopts=['-O'])
-
-	def ref_wallet_conv_out(self,name):
-		self.walletconv_out(name,'MMGen wallet','w',pw=True)
-
-	def ref_mn_conv_out(self,name):
-		self.walletconv_out(name,'mnemonic data','mn')
-
-	def ref_seed_conv_out(self,name):
-		self.walletconv_out(name,'seed data','seed')
-
-	def ref_hex_conv_out(self,name):
-		self.walletconv_out(name,'hexadecimal seed data','hexseed')
-
-	def ref_incog_conv_out(self,name):
-		self.walletconv_out(name,'incognito data',out_fmt='i',pw=True)
-
-	def ref_incox_conv_out(self,name):
-		self.walletconv_out(name,'hex incognito data',out_fmt='xi',pw=True)
-
-	def ref_hincog_conv_out(self,name,ic_f=None):
-		if not ic_f: ic_f = os.path.join(cfg['tmpdir'],hincog_fn)
-		hi_parms = '{},{}'.format(ic_f,ref_wallet_incog_offset)
-		sl_parm = '-l' + str(cfg['seed_len'])
-		self.walletconv_out(name,
-			'hidden incognito data', 'hi',
-			uopts=['-J',hi_parms,sl_parm],
-			uopts_chk=['-H',hi_parms,sl_parm],
-			pw=True
-		)
-
-	def ref_hincog_blkdev_conv_out(self,name):
-		imsg('Creating block device image file')
-		ic_img = os.path.join(cfg['tmpdir'],'hincog_blkdev_img')
-		subprocess.check_output(['dd','if=/dev/zero','of='+ic_img,'bs=1K','count=1'],stderr=subprocess.PIPE)
-		ic_dev = subprocess.check_output(['losetup','-f']).strip().decode()
-		ic_dev_mode_orig = '{:o}'.format(os.stat(ic_dev).st_mode & 0xfff)
-		ic_dev_mode = '0666'
-		imsg("Changing permissions on loop device to '{}'".format(ic_dev_mode))
-		subprocess.check_output(['sudo','chmod',ic_dev_mode,ic_dev],stderr=subprocess.PIPE)
-		imsg("Attaching loop device '{}'".format(ic_dev))
-		subprocess.check_output(['losetup',ic_dev,ic_img])
-		self.ref_hincog_conv_out(name,ic_f=ic_dev)
-		imsg("Detaching loop device '{}'".format(ic_dev))
-		subprocess.check_output(['losetup','-d',ic_dev])
-		imsg("Resetting permissions on loop device to '{}'".format(ic_dev_mode_orig))
-		subprocess.check_output(['sudo','chmod',ic_dev_mode_orig,ic_dev],stderr=subprocess.PIPE)
-
-	def ref_wallet_chk(self,name):
-		wf = os.path.join(ref_dir,cfg['ref_wallet'])
-		self.walletchk(name,wf,pf=None,pw=True,sid=cfg['seed_id'])
-
-	def ref_ss_chk(self,name,ss=None):
-		wf = os.path.join(ref_dir,'{}.{}'.format(cfg['seed_id'],ss.ext))
-		self.walletchk(name,wf,pf=None,desc=ss.desc,sid=cfg['seed_id'])
-
-	def ref_seed_chk(self,name):
-		from mmgen.seed import SeedFile
-		self.ref_ss_chk(name,ss=SeedFile)
-
-	def ref_hex_chk(self,name):
-		from mmgen.seed import HexSeedFile
-		self.ref_ss_chk(name,ss=HexSeedFile)
-
-	def ref_mn_chk(self,name):
-		from mmgen.seed import Mnemonic
-		self.ref_ss_chk(name,ss=Mnemonic)
-
-	def ref_brain_chk(self,name,bw_file=ref_bw_file):
-		wf = os.path.join(ref_dir,bw_file)
-		add_args = ['-l{}'.format(cfg['seed_len']), '-p'+ref_bw_hash_preset]
-		self.walletchk(name,wf,pf=None,add_args=add_args,
-			desc='brainwallet',sid=cfg['ref_bw_seed_id'])
-
-	def ref_brain_chk_spc3(self,name):
-		self.ref_brain_chk(name,bw_file=ref_bw_file_spc)
-
-	def ref_hincog_chk(self,name,desc='hidden incognito data'):
-		for wtype,edesc,of_arg in ('hic_wallet','',[]), \
-								('hic_wallet_old','(old format)',['-O']):
-			ic_arg = ['-H{},{}'.format(os.path.join(ref_dir,cfg[wtype]),ref_wallet_incog_offset)]
-			slarg = ['-l{} '.format(cfg['seed_len'])]
-			hparg = ['-p1']
-			if wtype == 'hic_wallet_old' and opt.profile: msg('')
-			t = MMGenExpect(name,'mmgen-walletchk',
-				slarg + hparg + of_arg + ic_arg,
-				extra_desc=edesc)
-			t.passphrase(desc,cfg['wpasswd'])
-			if wtype == 'hic_wallet_old':
-				t.expect('Is the Seed ID correct? (Y/n): ','\n')
-			chk = t.expect_getend('Seed ID: ')
-			t.close()
-			cmp_or_die(cfg['seed_id'],chk)
-
-	def ref_addrfile_chk(self,name,ftype='addr',coin=None,subdir=None,pfx=None,mmtype=None,add_args=[]):
-		af_key = 'ref_{}file'.format(ftype)
-		af_fn = cfg[af_key].format(pfx or altcoin_pfx,'' if coin else tn_ext)
-		af = os.path.join(ref_dir,(subdir or ref_subdir,'')[ftype=='passwd'],af_fn)
-		coin_arg = [] if coin == None else ['--coin='+coin]
-		tool_cmd = ftype.replace('segwit','').replace('bech32','')+'file_chksum'
-		t = MMGenExpect(name,'mmgen-tool',coin_arg+[tool_cmd,af]+add_args)
-		if ftype == 'keyaddr':
-			t.do_decrypt_ka_data(hp=ref_kafile_hash_preset,pw=ref_kafile_pass)
-		rc = cfg[   'ref_' + ftype + 'file_chksum' +
-					('_'+coin.lower() if coin else '') +
-					('_'+mmtype if mmtype else '')]
-		ref_chksum = rc if (ftype == 'passwd' or coin) else rc[g.proto.base_coin.lower()][g.testnet]
-		t.expect(chksum_pat,regex=True)
-		m = t.p.match.group(0)
-		t.read()
-		cmp_or_die(ref_chksum,m)
-
-	def ref_altcoin_addrgen(self,name,coin,mmtype,gen_what='addr',coin_suf='',add_args=[]):
-		wf = os.path.join(ref_dir,cfg['seed_id']+'.mmwords')
-		t = MMGenExpect(name,'mmgen-{}gen'.format(gen_what),
-				['-Sq','--coin='+coin] +
-				(['--type='+mmtype] if mmtype else []) +
-				add_args +
-				[wf,cfg['addr_idx_list']])
-		if gen_what == 'key':
-			t.expect('Encrypt key list? (y/N): ','N')
-		chk = t.expect_getend(r'.* data checksum for \S*: ',regex=True)
-		chk_ref = cfg['ref_{}addrfile_chksum_{}{}'.format(('','key')[gen_what=='key'],coin.lower(),coin_suf)]
-		t.read()
-		refcheck('{}list data checksum'.format(gen_what),chk,chk_ref)
-
-	def ref_addrfile_gen_eth(self,name):
-		self.ref_altcoin_addrgen(name,coin='ETH',mmtype='ethereum')
-
-	def ref_addrfile_gen_etc(self,name):
-		self.ref_altcoin_addrgen(name,coin='ETC',mmtype='ethereum')
-
-	def ref_addrfile_gen_dash(self,name):
-		self.ref_altcoin_addrgen(name,coin='DASH',mmtype='compressed')
-
-	def ref_addrfile_gen_zec(self,name):
-		self.ref_altcoin_addrgen(name,coin='ZEC',mmtype='compressed')
-
-	def ref_addrfile_gen_zec_z(self,name):
-		self.ref_altcoin_addrgen(name,coin='ZEC',mmtype='zcash_z',coin_suf='_z')
-
-	def ref_addrfile_gen_xmr(self,name):
-		self.ref_altcoin_addrgen(name,coin='XMR',mmtype='monero')
-
-	def ref_addrfile_gen_xmr_old(self,name):
-		self.ref_altcoin_addrgen(name,coin='XMR',mmtype='monero',add_args=['--use-old-ed25519'])
-
-	def ref_keyaddrfile_gen_eth(self,name):
-		self.ref_altcoin_addrgen(name,coin='ETH',mmtype='ethereum',gen_what='key')
-
-	def ref_keyaddrfile_gen_etc(self,name):
-		self.ref_altcoin_addrgen(name,coin='ETC',mmtype='ethereum',gen_what='key')
-
-	def ref_keyaddrfile_gen_dash(self,name):
-		self.ref_altcoin_addrgen(name,coin='DASH',mmtype='compressed',gen_what='key')
-
-	def ref_keyaddrfile_gen_zec(self,name):
-		self.ref_altcoin_addrgen(name,coin='ZEC',mmtype='compressed',gen_what='key')
-
-	def ref_keyaddrfile_gen_zec_z(self,name):
-		self.ref_altcoin_addrgen(name,coin='ZEC',mmtype='zcash_z',coin_suf='_z',gen_what='key')
-
-	def ref_keyaddrfile_gen_xmr(self,name):
-		self.ref_altcoin_addrgen(name,coin='XMR',mmtype='monero',gen_what='key')
-
-
-	def ref_addrfile_chk_eth(self,name):
-		self.ref_addrfile_chk(name,ftype='addr',coin='ETH',subdir='ethereum',pfx='-ETH')
-
-	def ref_addrfile_chk_etc(self,name):
-		self.ref_addrfile_chk(name,ftype='addr',coin='ETC',subdir='ethereum_classic',pfx='-ETC')
-
-	def ref_addrfile_chk_dash(self,name):
-		self.ref_addrfile_chk(name,ftype='addr',coin='DASH',subdir='dash',pfx='-DASH-C')
-
-	def ref_addrfile_chk_zec(self,name):
-		self.ref_addrfile_chk(name,ftype='addr',coin='ZEC',subdir='zcash',pfx='-ZEC-C')
-
-	def ref_addrfile_chk_zec_z(self,name):
-		if skip_for_win(): return
-		self.ref_addrfile_chk(name,ftype='addr',coin='ZEC',subdir='zcash',pfx='-ZEC-Z',mmtype='z')
-
-	def ref_addrfile_chk_xmr(self,name):
-		self.ref_addrfile_chk(name,ftype='addr',coin='XMR',subdir='monero',pfx='-XMR-M')
-
-
-	def ref_keyaddrfile_chk_eth(self,name):
-		self.ref_addrfile_chk(name,ftype='keyaddr',coin='ETH',subdir='ethereum',pfx='-ETH')
-
-	def ref_keyaddrfile_chk_etc(self,name):
-		self.ref_addrfile_chk(name,ftype='keyaddr',coin='ETC',subdir='ethereum_classic',pfx='-ETC')
-
-	def ref_keyaddrfile_chk_dash(self,name):
-		self.ref_addrfile_chk(name,ftype='keyaddr',coin='DASH',subdir='dash',pfx='-DASH-C')
-
-	def ref_keyaddrfile_chk_zec(self,name):
-		self.ref_addrfile_chk(name,ftype='keyaddr',coin='ZEC',subdir='zcash',pfx='-ZEC-C')
-
-	def ref_keyaddrfile_chk_zec_z(self,name):
-		if skip_for_win(): return
-		self.ref_addrfile_chk(name,ftype='keyaddr',coin='ZEC',subdir='zcash',pfx='-ZEC-Z',mmtype='z')
-
-	def ref_keyaddrfile_chk_xmr(self,name):
-		self.ref_addrfile_chk(name,ftype='keyaddr',coin='XMR',subdir='monero',pfx='-XMR-M')
-
-
-	def ref_keyaddrfile_chk(self,name):
-		self.ref_addrfile_chk(name,ftype='keyaddr')
-
-	def ref_passwdfile_chk(self,name):
-		self.ref_addrfile_chk(name,ftype='passwd')
-
-	def ref_segwitaddrfile_chk(self,name):
-		if not 'S' in g.proto.mmtypes:
-			msg_r('Skipping {} (not supported)'.format(name)); ok()
-		else:
-			self.ref_addrfile_chk(name,ftype='segwitaddr')
-
-	def ref_bech32addrfile_chk(self,name):
-		if not 'B' in g.proto.mmtypes:
-			msg_r('Skipping {} (not supported)'.format(name)); ok()
-		else:
-			self.ref_addrfile_chk(name,ftype='bech32addr')
-
-#	def txcreate8(self,name,addrfile):
-#		self.txcreate_common(name,sources=['8'])
-
-	def ref_tx_chk(self,name):
-		fn = cfg['ref_tx_file'][g.coin.lower()][bool(tn_ext)]
-		if not fn: return
-		tf = os.path.join(ref_dir,ref_subdir,fn)
-		wf = dfl_words
-		write_to_tmpfile(cfg,pwfile,cfg['wpasswd'])
-		pf = get_tmpfile_fn(cfg,pwfile)
-		self.txsign(name,tf,wf,pf,save=False,has_label=True,do_passwd=False)
-
-	def ref_tool_decrypt(self,name):
-		f = os.path.join(ref_dir,ref_enc_fn)
-		disable_debug()
-		dec_fn = get_tmpfile_fn(cfg,'famous.txt')
-		t = MMGenExpect(name,'mmgen-tool', ['-q','decrypt',f,'outfile='+dec_fn,'hash_preset=1'])
-		restore_debug()
-		t.passphrase('user data',tool_enc_passwd)
-		t.written_to_file('Decrypted data')
-		dec_txt = read_from_file(dec_fn)
-		imsg_r(dec_txt)
-		cmp_or_die(sample_text,dec_txt)
-
-	# wallet conversion tests
-	def walletconv_in(self,name,infile,desc,uopts=[],pw=False,oo=False):
-		opts = ['-d',cfg['tmpdir'],'-o','words',usr_rand_arg]
-		if_arg = [infile] if infile else []
-		d = '(convert)'
-		t = MMGenExpect(name,'mmgen-walletconv',opts+uopts+if_arg,extra_desc=d)
-		t.license()
-		if desc == 'brainwallet':
-			t.expect('Enter brainwallet: ',ref_wallet_brainpass+'\n')
-		if pw:
-			t.passphrase(desc,cfg['wpasswd'])
-			if name[:19] == 'ref_hincog_conv_old':
-				t.expect('Is the Seed ID correct? (Y/n): ','\n')
-			else:
-				t.expect(['Passphrase is OK',' are correct'])
-		# Output
-		wf = t.written_to_file('Mnemonic data',oo=oo)
-		t.p.wait()
-		t.ok()
-		# back check of result
-		if opt.profile: msg('')
-		self.walletchk(name,wf,pf=None,
-				desc='mnemonic data',
-				sid=cfg['seed_id'],
-				extra_desc='(check)'
-				)
-
-	def walletconv_out(self,name,desc,out_fmt='w',uopts=[],uopts_chk=[],pw=False):
-		opts = ['-d',cfg['tmpdir'],'-p1','-o',out_fmt] + uopts
-		infile = os.path.join(ref_dir,cfg['seed_id']+'.mmwords')
-		t = MMGenExpect(name,'mmgen-walletconv',[usr_rand_arg]+opts+[infile],extra_desc='(convert)')
-
-		add_args = ['-l{}'.format(cfg['seed_len'])]
-		t.license()
-		if pw:
-			t.passphrase_new('new '+desc,cfg['wpasswd'])
-			t.usr_rand(usr_rand_chars)
-		if ' '.join(desc.split()[-2:]) == 'incognito data':
-			for i in (1,2,3):
-				t.expect('Generating encryption key from OS random data ')
-		if desc == 'hidden incognito data':
-			ret = t.expect(['Create? (Y/n): ',"'YES' to confirm: "])
-			if ret == 0:
-				t.send('\n')
-				t.expect('Enter file size: ',str(hincog_bytes)+'\n')
-			else:
-				t.send('YES\n')
-		if out_fmt == 'w': t.label()
-		wf = t.written_to_file(capfirst(desc),oo=True)
-		pf = None
-		t.ok()
-
-		if desc == 'hidden incognito data':
-			add_args += uopts_chk
-			wf = None
-		if opt.profile: msg('')
-		self.walletchk(name,wf,pf=pf,
-			desc=desc,sid=cfg['seed_id'],pw=pw,
-			add_args=add_args,
-			extra_desc='(check)')
-
-	def regtest_setup(self,name):
-		os.environ['MMGEN_BOGUS_WALLET_DATA'] = ''
-		if g.testnet:
-			die(2,'--testnet option incompatible with regtest test suite')
-		try: shutil.rmtree(os.path.join(data_dir,'regtest'))
-		except: pass
-		os.environ['MMGEN_TEST_SUITE'] = '' # mnemonic is piped to stdin, so stop being a terminal
-		t = MMGenExpect(name,'mmgen-regtest',['-n','setup'])
-		os.environ['MMGEN_TEST_SUITE'] = '1'
-		for s in 'Starting setup','Creating','Mined','Creating','Creating','Setup complete':
-			t.expect(s)
-		t.ok()
-
-	def regtest_walletgen(self,name,user):
-		t = MMGenExpect(name,'mmgen-walletgen',['-q','-r0','-p1','--'+user])
-		t.passphrase_new('new MMGen wallet',rt_pw)
-		t.label()
-		t.expect('move it to the data directory? (Y/n): ','y')
-		t.written_to_file('MMGen wallet')
-		t.ok()
-
-	def regtest_walletgen_bob(self,name):   return self.regtest_walletgen(name,'bob')
-	def regtest_walletgen_alice(self,name): return self.regtest_walletgen(name,'alice')
-
-	@staticmethod
-	def regtest_user_dir(user,coin=None):
-		return os.path.join(data_dir,'regtest',coin or g.coin.lower(),user)
-
-	def regtest_user_sid(self,user):
-		return os.path.basename(get_file_with_ext('mmdat',self.regtest_user_dir(user)))[:8]
-
-	def regtest_addrgen(self,name,user,wf=None,passwd=rt_pw,addr_range='1-5'):
-		from mmgen.addr import MMGenAddrType
-		for mmtype in g.proto.mmtypes:
-			t = MMGenExpect(name,'mmgen-addrgen',
-				['--quiet','--'+user,'--type='+mmtype,'--outdir={}'.format(self.regtest_user_dir(user))] +
-				([],[wf])[bool(wf)] + [addr_range],
-				extra_desc='({})'.format(MMGenAddrType.mmtypes[mmtype]['name']))
-			t.passphrase('MMGen wallet',passwd)
-			t.written_to_file('Addresses')
-			t.ok()
-
-	def regtest_addrgen_bob(self,name):   self.regtest_addrgen(name,'bob')
-	def regtest_addrgen_alice(self,name): self.regtest_addrgen(name,'alice')
-
-	def regtest_addrimport(self,name,user,sid=None,addr_range='1-5',num_addrs=5):
-		id_strs = { 'legacy':'', 'compressed':'-C', 'segwit':'-S', 'bech32':'-B' }
-		if not sid: sid = self.regtest_user_sid(user)
-		from mmgen.addr import MMGenAddrType
-		for mmtype in g.proto.mmtypes:
-			desc = MMGenAddrType.mmtypes[mmtype]['name']
-			fn = os.path.join(self.regtest_user_dir(user),
-				'{}{}{}[{}]{x}.testnet.addrs'.format(
-					sid,altcoin_pfx,id_strs[desc],addr_range,
-					x='-α' if g.debug_utf8 else ''))
-			if mmtype == g.proto.mmtypes[0] and user == 'bob':
-				psave = g.proto
-				g.proto = CoinProtocol(g.coin,True)
-				add_comments_to_addr_file(fn,fn,use_labels=True)
-				g.proto = psave
-			t = MMGenExpect(name,'mmgen-addrimport', ['--quiet','--'+user,'--batch',fn],extra_desc='('+desc+')')
-			if g.debug:
-				t.expect("Type uppercase 'YES' to confirm: ",'YES\n')
-			t.expect('Importing')
-			t.expect('{} addresses imported'.format(num_addrs))
-			t.ok()
-
-	def regtest_addrimport_bob(self,name):   self.regtest_addrimport(name,'bob')
-	def regtest_addrimport_alice(self,name): self.regtest_addrimport(name,'alice')
-
-	def regtest_fund_wallet(self,name,user,mmtype,amt,sid=None,addr_range='1-5'):
-		if not sid: sid = self.regtest_user_sid(user)
-		addr = self.get_addr_from_regtest_addrlist(user,sid,mmtype,0,addr_range=addr_range)
-		t = MMGenExpect(name,'mmgen-regtest', ['send',str(addr),str(amt)])
-		t.expect('Sending {} {}'.format(amt,g.coin))
-		t.expect('Mined 1 block')
-		t.ok()
-
-	def regtest_fund_bob(self,name):   self.regtest_fund_wallet(name,'bob','C',rtFundAmt)
-	def regtest_fund_alice(self,name): self.regtest_fund_wallet(name,'alice',('L','S')[g.proto.cap('segwit')],rtFundAmt)
-
-	def regtest_user_twview(self,name,user):
-		t = MMGenExpect(name,'mmgen-tool',['--'+user,'twview'])
-		t.expect(r'1\).*\b{}\b'.format(rtAmts[0]),regex=True)
-		t.read()
-		t.ok()
-
-	def regtest_bob_twview(self,name):
-		return self.regtest_user_twview(name,'bob')
-
-	def regtest_user_bal(self,name,user,bal,args=['showempty=1'],skip_check=False,exit_val=0):
-		t = MMGenExpect(name,'mmgen-tool',['--'+user,'listaddresses'] + args)
-		if skip_check:
-			t.read()
-			t.ok(exit_val=exit_val)
-		else:
-			total = t.expect_getend('TOTAL: ')
-			cmp_or_die('{} {}'.format(bal,g.coin),total)
-
-	def regtest_alice_bal1(self,name):
-		return self.regtest_user_bal(name,'alice',rtFundAmt)
-
-	def regtest_alice_bal2(self,name):
-		return self.regtest_user_bal(name,'alice',rtBals[8])
-
-	def regtest_bob_bal1(self,name):
-		return self.regtest_user_bal(name,'bob',rtFundAmt)
-
-	def regtest_bob_bal2(self,name):
-		return self.regtest_user_bal(name,'bob',rtBals[0])
-
-	def regtest_bob_bal2a(self,name):
-		return self.regtest_user_bal(name,'bob',rtBals[0],args=['showempty=1','age_fmt="confs"'])
-
-	def regtest_bob_bal2b(self,name):
-		return self.regtest_user_bal(name,'bob',rtBals[0],args=['showempty=1'])
-
-	def regtest_bob_bal2c(self,name):
-		return self.regtest_user_bal(name,'bob',rtBals[0],args=['showempty=1','minconf=2','age_fmt="days"'],skip_check=True)
-
-	def regtest_bob_bal2d(self,name):
-		return self.regtest_user_bal(name,'bob',rtBals[0],args=['minconf=2'],skip_check=True)
-
-	def regtest_bob_bal2e(self,name):
-		return self.regtest_user_bal(name,'bob',rtBals[0],args=['showempty=1','sort=age'])
-
-	def regtest_bob_bal2f(self,name):
-		return self.regtest_user_bal(name,'bob',rtBals[0],args=['showempty=1','sort=age,reverse'])
-
-	def regtest_bob_bal3(self,name):
-		return self.regtest_user_bal(name,'bob',rtBals[1])
-
-	def regtest_bob_bal4(self,name):
-		return self.regtest_user_bal(name,'bob',rtBals[2])
-
-	def regtest_bob_bal5(self,name):
-		return self.regtest_user_bal(name,'bob',rtBals[3])
-
-	def regtest_bob_bal6(self,name):
-		return self.regtest_user_bal(name,'bob',rtBals[7])
-
-	def regtest_bob_bal5_getbalance(self,name):
-		t_ext,t_mmgen = rtBals_gb[0],rtBals_gb[1]
-		assert Decimal(t_ext) + Decimal(t_mmgen) == Decimal(rtBals[3])
-		t = MMGenExpect(name,'mmgen-tool',['--bob','getbalance'])
-		t.expect(r'\n[0-9A-F]{8}: .* '+t_mmgen,regex=True)
-		t.expect(r'\nNon-MMGen: .* '+t_ext,regex=True)
-		t.expect(r'\nTOTAL: .* '+rtBals[3],regex=True)
-		t.read()
-		t.ok()
-
-	def regtest_bob_alice_bal(self,name):
-		t = MMGenExpect(name,'mmgen-regtest',['get_balances'])
-		t.expect('Switching')
-		ret = t.expect_getend("Bob's balance:").strip()
-		cmp_or_die(rtBals[4],ret,skip_ok=True)
-		ret = t.expect_getend("Alice's balance:").strip()
-		cmp_or_die(rtBals[5],ret,skip_ok=True)
-		ret = t.expect_getend("Total balance:").strip()
-		cmp_or_die(rtBals[6],ret,skip_ok=True)
-		t.ok()
-
-	def regtest_user_txdo(  self,name,user,fee,
-							outputs_cl,
-							outputs_list,
-							extra_args=[],
-							wf=None,
-							pw=rt_pw,
-							do_label=False,
-							bad_locktime=False,
-							full_tx_view=False):
-		os.environ['MMGEN_BOGUS_SEND'] = ''
-		t = MMGenExpect(name,'mmgen-txdo',
-			['-d',cfg['tmpdir'],'-B','--'+user] +
-			(['--tx-fee='+fee] if fee else []) +
-			extra_args + ([],[wf])[bool(wf)] + outputs_cl)
-		os.environ['MMGEN_BOGUS_SEND'] = '1'
-
-		self.txcreate_ui_common(t,'txdo',
-								menu=['M'],inputs=outputs_list,
-								file_desc='Signed transaction',
-								interactive_fee=(tx_fee,'')[bool(fee)],
-								add_comment=ref_tx_label_jp,
-								view='t',save=True)
-
-		t.passphrase('MMGen wallet',pw)
-		t.written_to_file('Signed transaction')
-		self.do_confirm_send(t)
-		s,exit_val = (('Transaction sent',0),("can't be included",1))[bad_locktime]
-		t.expect(s)
-		t.ok(exit_val)
-
-	def regtest_bob_split1(self,name):
-		sid = self.regtest_user_sid('bob')
-		outputs_cl = [sid+':C:1,100', sid+':L:2,200',sid+':'+rtBobOp3]
-		return self.regtest_user_txdo(name,'bob',rtFee[0],outputs_cl,'1',do_label=True,full_tx_view=True)
-
-	def get_addr_from_regtest_addrlist(self,user,sid,mmtype,idx,addr_range='1-5'):
-		id_str = { 'L':'', 'S':'-S', 'C':'-C', 'B':'-B' }[mmtype]
-		ext = '{}{}{}[{}]{x}.testnet.addrs'.format(
-			sid,altcoin_pfx,id_str,addr_range,x='-α' if g.debug_utf8 else '')
-		fn = get_file_with_ext(ext,self.regtest_user_dir(user),no_dot=True)
-		psave = g.proto
-		g.proto = CoinProtocol(g.coin,True)
-		if hasattr(g.proto,'bech32_hrp_rt'):
-			g.proto.bech32_hrp = g.proto.bech32_hrp_rt
-		silence()
-		addr = AddrList(fn).data[idx].addr
-		end_silence()
-		g.proto = psave
-		return addr
-
-	def create_tx_outputs(self,user,data):
-		sid = self.regtest_user_sid(user)
-		return [self.get_addr_from_regtest_addrlist(user,sid,mmtype,idx-1)+amt_str for mmtype,idx,amt_str in data]
-
-	def regtest_bob_rbf_send(self,name):
-		outputs_cl = self.create_tx_outputs('alice',(('L',1,',60'),('C',1,',40'))) # alice_sid:L:1, alice_sid:C:1
-		outputs_cl += [self.regtest_user_sid('bob')+':'+rtBobOp3]
-		return self.regtest_user_txdo(name,'bob',rtFee[1],outputs_cl,'3',
-					extra_args=([],['--rbf'])[g.proto.cap('rbf')])
-
-	def regtest_bob_send_non_mmgen(self,name):
-		outputs_cl = self.create_tx_outputs('alice',(
-			(('L','S')[g.proto.cap('segwit')],2,',10'),
-			(('L','S')[g.proto.cap('segwit')],3,'')
-		)) # alice_sid:S:2, alice_sid:S:3
-		fn = os.path.join(cfg['tmpdir'],'non-mmgen.keys')
-		return self.regtest_user_txdo(name,'bob',rtFee[3],outputs_cl,'1,4-10',
-			extra_args=['--keys-from-file='+fn,'--vsize-adj=1.02'])
-
-	def regtest_alice_send_estimatefee(self,name):
-		outputs_cl = self.create_tx_outputs('bob',(('L',1,''),)) # bob_sid:L:1
-		return self.regtest_user_txdo(name,'alice',None,outputs_cl,'1') # fee=None
-
-	def regtest_user_txbump(self,name,user,txfile,fee,red_op):
-		if not g.proto.cap('rbf'):
-			msg('Skipping RBF'); return True
-		os.environ['MMGEN_BOGUS_SEND'] = ''
-		t = MMGenExpect(name,'mmgen-txbump',
-			['-d',cfg['tmpdir'],'--send','--'+user,'--tx-fee='+fee,'--output-to-reduce='+red_op] + [txfile])
-		os.environ['MMGEN_BOGUS_SEND'] = '1'
-		t.expect('OK? (Y/n): ','y') # output OK?
-		t.expect('OK? (Y/n): ','y') # fee OK?
-		t.do_comment(False,has_label=True)
-		t.passphrase('MMGen wallet',rt_pw)
-		t.written_to_file('Signed transaction')
-		self.txsend_ui_common(t,'txdo',bogus_send=False,file_desc='Signed transaction')
-		t.read()
-		t.ok()
-
-	def regtest_bob_rbf_bump(self,name):
-		ext = ',{}]{x}.testnet.sigtx'.format(rtFee[1][:-1],x='-α' if g.debug_utf8 else '')
-		txfile = get_file_with_ext(ext,cfg['tmpdir'],delete=False,no_dot=True)
-		return self.regtest_user_txbump(name,'bob',txfile,rtFee[2],'c')
-
-	def regtest_generate(self,name,coin=None,num_blocks=1):
-		int(num_blocks)
-		if coin: opt.coin = coin
-		t = MMGenExpect(name,'mmgen-regtest',['generate',str(num_blocks)])
-		t.expect('Mined {} block'.format(num_blocks))
-		t.ok()
-
-	def regtest_get_mempool(self,name):
-		disable_debug()
-		ret = MMGenExpect(name,'mmgen-regtest',['show_mempool']).read()
-		restore_debug()
-		from ast import literal_eval
-		return literal_eval(ret.split('\n')[0]) # allow for extra output by handler at end
-
-	def regtest_get_mempool1(self,name):
-		mp = self.regtest_get_mempool(name)
-		if len(mp) != 1:
-			rdie(2,'Mempool has more or less than one TX!')
-		write_to_tmpfile(cfg,'rbf_txid',mp[0]+'\n')
-		ok()
-
-	def regtest_get_mempool2(self,name):
-		if not g.proto.cap('rbf'):
-			msg('Skipping post-RBF mempool check'); return True
-		mp = self.regtest_get_mempool(name)
-		if len(mp) != 1:
-			rdie(2,'Mempool has more or less than one TX!')
-		chk = read_from_tmpfile(cfg,'rbf_txid')
-		if chk.strip() == mp[0]:
-			rdie(2,'TX in mempool has not changed!  RBF bump failed')
-		ok()
-
-	@staticmethod
-	def gen_pairs(n):
-		disable_debug()
-		ret = [subprocess.check_output(
-						['python3',os.path.join('cmds','mmgen-tool'),'--testnet=1'] +
-						(['--type=compressed'],[])[i==0] +
-						['-r0','randpair']
-					).decode().split() for i in range(n)]
-		restore_debug()
-		return ret
-
-	def regtest_bob_pre_import(self,name):
-		pairs = self.gen_pairs(5)
-		write_to_tmpfile(cfg,'non-mmgen.keys','\n'.join([a[0] for a in pairs])+'\n')
-		write_to_tmpfile(cfg,'non-mmgen.addrs','\n'.join([a[1] for a in pairs])+'\n')
-		return self.regtest_user_txdo(name,'bob',rtFee[4],[pairs[0][1]],'3')
-
-	def regtest_user_import(self,name,user,args):
-		t = MMGenExpect(name,'mmgen-addrimport',['--quiet','--'+user]+args)
-		if g.debug:
-			t.expect("Type uppercase 'YES' to confirm: ",'YES\n')
-		t.expect('Importing')
-		t.expect('OK')
-		t.ok()
-
-	def regtest_bob_import_addr(self,name):
-		addr = read_from_tmpfile(cfg,'non-mmgen.addrs').split()[0]
-		return self.regtest_user_import(name,'bob',['--rescan','--address='+addr])
-
-	def regtest_bob_import_list(self,name):
-		fn = os.path.join(cfg['tmpdir'],'non-mmgen.addrs')
-		return self.regtest_user_import(name,'bob',['--addrlist',fn])
-
-	def regtest_bob_split2(self,name):
-		addrs = read_from_tmpfile(cfg,'non-mmgen.addrs').split()
-		amts = (1.12345678,2.87654321,3.33443344,4.00990099,5.43214321)
-		outputs1 = list(map('{},{}'.format,addrs,amts))
-		sid = self.regtest_user_sid('bob')
-		l1,l2 = (':S',':B') if 'B' in g.proto.mmtypes else (':S',':S') if g.proto.cap('segwit') else (':L',':L')
-		outputs2 = [sid+':C:2,6.333', sid+':L:3,6.667',sid+l1+':4,0.123',sid+l2+':5']
-		return self.regtest_user_txdo(name,'bob',rtFee[5],outputs1+outputs2,'1-2')
-
-	def regtest_user_add_label(self,name,user,addr,label):
-		t = MMGenExpect(name,'mmgen-tool',['--'+user,'add_label',addr,label])
-		t.expect('Added label.*in tracking wallet',regex=True)
-		t.ok()
-
-	def regtest_user_remove_label(self,name,user,addr):
-		t = MMGenExpect(name,'mmgen-tool',['--'+user,'remove_label',addr])
-		t.expect('Removed label.*in tracking wallet',regex=True)
-		t.ok()
-
-# 	utf8_label     =  u'Edited label (40 characters, UTF8/JP) 月へ' # '\xe6\x9c\x88\xe3\x81\xb8' (Jp.)
-# 	utf8_label_pat = ur'Edited label \(40 characters, UTF8/JP\) ......'
-	utf8_label     = ref_tx_label_zh[:40]
-	utf8_label_pat = utf8_label
-
-	def regtest_bob_add_label(self,name):
-		sid = self.regtest_user_sid('bob')
-		return self.regtest_user_add_label(name,'bob',sid+':C:1',self.utf8_label)
-
-	def regtest_alice_add_label1(self,name):
-		sid = self.regtest_user_sid('alice')
-		return self.regtest_user_add_label(name,'alice',sid+':C:1','Original Label - 月へ')
-
-	def regtest_alice_add_label2(self,name):
-		sid = self.regtest_user_sid('alice')
-		return self.regtest_user_add_label(name,'alice',sid+':C:1','Replacement Label')
-
-	def regtest_alice_add_label_coinaddr(self,name):
-		mmaddr = self.regtest_user_sid('alice') + ':C:2'
-		t = MMGenExpect(name,'mmgen-tool',['--alice','listaddress',mmaddr],no_msg=True)
-		btcaddr = [i for i in t.read().splitlines() if i.lstrip()[0:len(mmaddr)] == mmaddr][0].split()[1]
-		return self.regtest_user_add_label(name,'alice',btcaddr,'Label added using coin address')
-
-	def regtest_alice_chk_label_coinaddr(self,name):
-		sid = self.regtest_user_sid('alice')
-		return self.regtest_user_chk_label(name,'alice',sid+':C:2','Label added using coin address')
-
-	def regtest_alice_add_label_badaddr(self,name,addr,reply):
-		t = MMGenExpect(name,'mmgen-tool',['--alice','add_label',addr,'(none)'])
-		t.expect(reply,regex=True)
-		t.ok()
-
-	def regtest_alice_add_label_badaddr1(self,name):
-		return self.regtest_alice_add_label_badaddr(name,rt_pw,'Invalid coin address for this chain: '+rt_pw)
-
-	def regtest_alice_add_label_badaddr2(self,name):
-		addr = g.proto.pubhash2addr(b'00'*20,False) # mainnet zero address
-		return self.regtest_alice_add_label_badaddr(name,addr,'Invalid coin address for this chain: '+addr)
-
-	def regtest_alice_add_label_badaddr3(self,name):
-		addr = self.regtest_user_sid('alice') + ':C:123'
-		return self.regtest_alice_add_label_badaddr(name,addr,
-			"MMGen address '{}' not found in tracking wallet".format(addr))
-
-	def regtest_alice_add_label_badaddr4(self,name):
-		addr = CoinProtocol(g.coin,True).pubhash2addr(b'00'*20,False) # testnet zero address
-		return self.regtest_alice_add_label_badaddr(name,addr,
-			"Address '{}' not found in tracking wallet".format(addr))
-
-	def regtest_alice_bal_rpcfail(self,name):
-		addr = self.regtest_user_sid('alice') + ':C:2'
-		os.environ['MMGEN_RPC_FAIL_ON_COMMAND'] = 'listunspent'
-		t = MMGenExpect(name,'mmgen-tool',['--alice','getbalance'])
-		os.environ['MMGEN_RPC_FAIL_ON_COMMAND'] = ''
-		t.expect('Method not found')
-		t.read()
-		ok()
-
-	def regtest_alice_remove_label1(self,name):
-		sid = self.regtest_user_sid('alice')
-		return self.regtest_user_remove_label(name,'alice',sid+':C:1')
-
-	def regtest_user_chk_label(self,name,user,addr,label,label_pat=None):
-		t = MMGenExpect(name,'mmgen-tool',['--'+user,'listaddresses','all_labels=1'])
-		t.expect(r'{}\s+\S{{30}}\S+\s+{}\s+'.format(addr,(label_pat or label)),regex=True)
-		t.ok()
-
-	def regtest_alice_chk_label1(self,name):
-		sid = self.regtest_user_sid('alice')
-		return self.regtest_user_chk_label(name,'alice',sid+':C:1','Original Label - 月へ')
-
-	def regtest_alice_chk_label2(self,name):
-		sid = self.regtest_user_sid('alice')
-		return self.regtest_user_chk_label(name,'alice',sid+':C:1','Replacement Label')
-
-	def regtest_alice_edit_label1(self,name):
-		return self.regtest_user_edit_label(name,'alice','1',self.utf8_label)
-
-	def regtest_alice_chk_label3(self,name):
-		sid = self.regtest_user_sid('alice')
-		return self.regtest_user_chk_label(name,'alice',sid+':C:1',self.utf8_label,label_pat=self.utf8_label_pat)
-
-	def regtest_alice_chk_label4(self,name):
-		sid = self.regtest_user_sid('alice')
-		return self.regtest_user_chk_label(name,'alice',sid+':C:1','-')
-
-	def regtest_user_edit_label(self,name,user,output,label):
-		t = MMGenExpect(name,'mmgen-txcreate',['-B','--'+user,'-i'])
-		t.expect(r'add \[l\]abel:.','M',regex=True)
-		t.expect(r'add \[l\]abel:.','l',regex=True)
-		t.expect(r"Enter unspent.*return to main menu\):.",output+'\n',regex=True)
-		t.expect(r"Enter label text.*return to main menu\):.",label+'\n',regex=True)
-		t.expect(r'\[q\]uit view, .*?:.','q',regex=True)
-		t.ok()
-
-	def regtest_stop(self,name):
-		if opt.no_daemon_stop:
-			MMGenExpect(name,'',msg_only=True)
-			msg_r('(leaving daemon running by user request)')
-			ok()
-		else:
-			t = MMGenExpect(name,'mmgen-regtest',['stop'])
-			t.ok()
-
-	def regtest_split_setup(self,name):
-		if g.coin != 'BTC': die(1,'Test valid only for coin BTC')
-		opt.coin = 'BTC'
-		return self.regtest_setup(name)
-
-	def regtest_split_fork(self,name):
-		opt.coin = 'B2X'
-		t = MMGenExpect(name,'mmgen-regtest',['fork','btc'])
-		t.expect('Creating fork from coin')
-		t.expect('successfully created')
-		t.ok()
-
-	def regtest_split_start(self,name,coin):
-		opt.coin = coin
-		t = MMGenExpect(name,'mmgen-regtest',['bob'])
-		t.expect('Starting')
-		t.expect('done')
-		t.ok()
-
-	def regtest_split_start_btc(self,name): self.regtest_split_start(name,coin='BTC')
-	def regtest_split_start_b2x(self,name): self.regtest_split_start(name,coin='B2X')
-	def regtest_split_gen_btc(self,name):   self.regtest_generate(name,coin='BTC')
-	def regtest_split_gen_b2x(self,name):   self.regtest_generate(name,coin='B2X',num_blocks=100)
-	def regtest_split_gen_b2x2(self,name):  self.regtest_generate(name,coin='B2X')
-
-	def regtest_split_do_split(self,name):
-		opt.coin = 'B2X'
-		sid = self.regtest_user_sid('bob')
-		t = MMGenExpect(name,'mmgen-split',[
-			'--bob',
-			'--outdir='+cfg['tmpdir'],
-			'--tx-fees=0.0001,0.0003',
-			sid+':S:1',sid+':S:2'])
-		t.expect(r'\[q\]uit view, .*?:.','q', regex=True)
-		t.expect('outputs to spend: ','1\n')
-
-		for tx in ('timelocked','split'):
-			for q in ('fee','change'): t.expect('OK? (Y/n): ','y')
-			t.do_comment(False)
-			t.view_tx('t')
-
-		t.written_to_file('Long chain (timelocked) transaction')
-		t.written_to_file('Short chain transaction')
-		t.ok()
-
-	def regtest_split_sign(self,name,coin,ext):
-		wf = get_file_with_ext('mmdat',self.regtest_user_dir('bob',coin=coin.lower()))
-		txfile = get_file_with_ext(ext,cfg['tmpdir'],no_dot=True)
-		opt.coin = coin
-		self.txsign(name,txfile,wf,extra_opts=['--bob'])
-
-	def regtest_split_sign_b2x(self,name):
-		return self.regtest_split_sign(name,coin='B2X',ext='533].rawtx')
-
-	def regtest_split_sign_btc(self,name):
-		return self.regtest_split_sign(name,coin='BTC',ext='9997].rawtx')
-
-	def regtest_split_send(self,name,coin,ext):
-		opt.coin = coin
-		txfile = get_file_with_ext(ext,cfg['tmpdir'],no_dot=True)
-		self.txsend(name,txfile,bogus_send=False,extra_opts=['--bob'])
-
-	def regtest_split_send_b2x(self,name):
-		return self.regtest_split_send(name,coin='B2X',ext='533].sigtx')
-
-	def regtest_split_send_btc(self,name):
-		return self.regtest_split_send(name,coin='BTC',ext='9997].sigtx')
-
-	def regtest_split_txdo_timelock(self,name,coin,locktime,bad_locktime):
-		opt.coin = coin
-		sid = self.regtest_user_sid('bob')
-		self.regtest_user_txdo(
-			name,'bob','0.0001',[sid+':S:5'],'1',pw=rt_pw,
-			extra_args=['--locktime='+str(locktime)],
-			bad_locktime=bad_locktime)
-
-	def regtest_split_txdo_timelock_bad_btc(self,name):
-		self.regtest_split_txdo_timelock(name,'BTC',locktime=8888,bad_locktime=True)
-	def regtest_split_txdo_timelock_good_btc(self,name):
-		self.regtest_split_txdo_timelock(name,'BTC',locktime=1321009871,bad_locktime=False)
-	def regtest_split_txdo_timelock_bad_b2x(self,name):
-		self.regtest_split_txdo_timelock(name,'B2X',locktime=8888,bad_locktime=True)
-	def regtest_split_txdo_timelock_good_b2x(self,name):
-		self.regtest_split_txdo_timelock(name,'B2X',locktime=1321009871,bad_locktime=False)
-
-	def ethdev_setup(self,name):
-		MMGenExpect(name,'',msg_only=True)
-		os.environ['MMGEN_BOGUS_WALLET_DATA'] = ''
-		if subprocess.call(['which','parity'],stdout=subprocess.PIPE) == 0:
-			lf_arg = '--log-file=' + os.path.join(data_dir,'parity.log')
-			ss = 'parity.*--log-file=test/data_dir.*/parity.log' # allow for UTF8_DEBUG
-			try:
-				pid = subprocess.check_output(['pgrep','-af',ss]).split()[0]
-				os.kill(int(pid),9)
-			except: pass
-			# '--base-path' doesn't work together with daemon mode, so we have to clobber the main dev chain
-			dc_dir = os.path.join(os.environ['HOME'],'.local/share/io.parity.ethereum/chains/DevelopmentChain')
-			shutil.rmtree(dc_dir,ignore_errors=True)
-			bdir = os.path.join(data_dir,'parity')
-			try: os.mkdir(bdir)
-			except: pass
-			pid_fn = get_tmpfile_fn(cfg,cfg['parity_pidfile'])
-			opts = ['--ports-shift=4','--config=dev']
-			redir = None if opt.exact_output else subprocess.PIPE
-			subprocess.check_call(['parity',lf_arg] + opts + ['daemon',pid_fn],stderr=redir,stdout=redir)
-			time.sleep(3) # race condition
-			pid = read_from_tmpfile(cfg,cfg['parity_pidfile'])
-		elif subprocess.call('netstat -tnl | grep -q 127.0.0.1:8549',shell=True) == 0:
-			m1 = 'No parity executable found on system, but port 8549 is active!'
-			m2 = 'Before continuing, you should probably run the command'
-			m3 = 'test/test.py ethdev_setup'
-			m4 = 'on the remote host.'
-			sys.stderr.write('{}\n{}\n{} {}\n'.format(m1,m2,cyan(m3),m4))
-			confirm_continue()
-		else:
-			die(1,'No parity executable found!')
-		ok()
-
-	def ethdev_addrgen(self,name,addrs='1-3,11-13,21-23'):
-		from mmgen.addr import MMGenAddrType
-		t = MMGenExpect(name,'mmgen-addrgen', eth_args() + [dfl_words,addrs])
-		t.written_to_file('Addresses')
-		t.read()
-		t.ok()
-
-	def ethdev_addrimport(self,name,ext='21-23]{}.addrs',expect='9/9',add_args=[],bad_input=False):
-		ext = ext.format('-α' if g.debug_utf8 else '')
-		fn = get_file_with_ext(ext,cfg['tmpdir'],no_dot=True,delete=False)
-		t = MMGenExpect(name,'mmgen-addrimport', eth_args()[1:] + add_args + [fn])
-		if bad_input:
-			t.read(); t.ok(2); return
-		if g.debug: t.expect("Type uppercase 'YES' to confirm: ",'YES\n')
-		t.expect('Importing')
-		t.expect(expect)
-		t.read()
-		t.ok()
-
-	def ethdev_addrimport_one_addr(self,name,addr=None,extra_args=[]):
-		t = MMGenExpect(name,'mmgen-addrimport', eth_args()[1:] + extra_args + ['--address='+addr])
-		t.expect('OK')
-		t.ok()
-
-	def ethdev_addrimport_dev_addr(self,name):
-		self.ethdev_addrimport_one_addr(name,addr=eth_addr)
-
-	def ethdev_addrimport_burn_addr(self,name):
-		self.ethdev_addrimport_one_addr(name,addr=eth_burn_addr)
-
-	def ethdev_txcreate(self,name,args=[],menu=[],acct='1',non_mmgen_inputs=0,
-						interactive_fee='50G',
-						fee_res='0.00105 {} (50 gas price in Gwei)'.format(g.coin),
-						eth_fee_res=None,
-						fee_desc = 'gas price'):
-		t = MMGenExpect(name,'mmgen-txcreate', eth_args() + ['-B'] + args)
-		t.expect(r'add \[l\]abel, .*?:.','p', regex=True)
-		t.written_to_file('Account balances listing')
-		self.txcreate_ui_common(t,name,
-								menu=menu,
-								input_sels_prompt='to spend from',
-								inputs=acct,file_desc='Ethereum transaction',
-								bad_input_sels=True,non_mmgen_inputs=non_mmgen_inputs,
-								interactive_fee=interactive_fee,fee_res=fee_res,
-								fee_desc=fee_desc,eth_fee_res=eth_fee_res,
-								add_comment=ref_tx_label_jp)
-
-	def ethdev_txsign(self,name,ni=False,ext='{}.rawtx',add_args=[]):
-		ext = ext.format('-α' if g.debug_utf8 else '')
-		key_fn = get_tmpfile_fn(cfg,cfg['parity_keyfile'])
-		write_to_tmpfile(cfg,cfg['parity_keyfile'],eth_key+'\n')
-		tx_fn = get_file_with_ext(ext,cfg['tmpdir'],no_dot=True)
-		t = MMGenExpect(name,'mmgen-txsign',eth_args()+add_args + ([],['--yes'])[ni] + ['-k',key_fn,tx_fn,dfl_words])
-		self.txsign_ui_common(t,name,ni=ni,has_label=True)
-
-	def ethdev_txsend(self,name,ni=False,bogus_send=False,ext='{}.sigtx',add_args=[]):
-		ext = ext.format('-α' if g.debug_utf8 else '')
-		tx_fn = get_file_with_ext(ext,cfg['tmpdir'],no_dot=True)
-		if not bogus_send: os.environ['MMGEN_BOGUS_SEND'] = ''
-		t = MMGenExpect(name,'mmgen-txsend', eth_args()+add_args + [tx_fn])
-		if not bogus_send: os.environ['MMGEN_BOGUS_SEND'] = '1'
-		self.txsend_ui_common(t,name,quiet=True,bogus_send=bogus_send,has_label=True)
-
-	def ethdev_txcreate1(self,name):
-		# valid_keypresses = 'adrMmeqpvwl'
-		menu = ['a','d','r','M','D','e','m','m'] # include one invalid keypress, 'D'
-		args = ['98831F3A:E:1,123.456']
-		return self.ethdev_txcreate(name,args=args,menu=menu,acct='1',non_mmgen_inputs=1)
-
-	def ethdev_txsign1(self,name): self.ethdev_txsign(name)
-	def ethdev_txsign1_ni(self,name): self.ethdev_txsign(name,ni=True)
-	def ethdev_txsend1(self,name): self.ethdev_txsend(name)
-	def ethdev_bal1(self,name): self.ethdev_bal(name,n='1')
-
-	def ethdev_txcreate2(self,name):
-		args = ['98831F3A:E:11,1.234']
-		return self.ethdev_txcreate(name,args=args,acct='10',non_mmgen_inputs=1)
-	def ethdev_txsign2(self,name): self.ethdev_txsign(name,ni=True,ext='1.234,50000]{}.rawtx')
-	def ethdev_txsend2(self,name): self.ethdev_txsend(name,ext='1.234,50000]{}.sigtx')
-	def ethdev_bal2(self,name): self.ethdev_bal(name,n='2')
-
-	def ethdev_txcreate3(self,name):
-		args = ['98831F3A:E:21,2.345']
-		return self.ethdev_txcreate(name,args=args,acct='10',non_mmgen_inputs=1)
-	def ethdev_txsign3(self,name): self.ethdev_txsign(name,ni=True,ext='2.345,50000]{}.rawtx')
-	def ethdev_txsend3(self,name): self.ethdev_txsend(name,ext='2.345,50000]{}.sigtx')
-	def ethdev_bal3(self,name): self.ethdev_bal(name,n='3')
-
-	def ethdev_tx_status(self,name,ext,expect_str):
-		ext = ext.format('-α' if g.debug_utf8 else '')
-		tx_fn = get_file_with_ext(ext,cfg['tmpdir'],no_dot=True)
-		t = MMGenExpect(name,'mmgen-txsend', eth_args() + ['--status',tx_fn])
-		t.expect(expect_str)
-		t.read()
-		t.ok()
-
-	def ethdev_tx_status1(self,name):
-		self.ethdev_tx_status(name,ext='2.345,50000]{}.sigtx',expect_str='has 1 confirmation')
-
-	def ethdev_txcreate4(self,name):
-		args = ['98831F3A:E:2,23.45495']
-		interactive_fee='40G'
-		fee_res='0.00084 {} (40 gas price in Gwei)'.format(g.coin)
-		return self.ethdev_txcreate(name,args=args,acct='1',non_mmgen_inputs=0,
-					interactive_fee=interactive_fee,fee_res=fee_res,eth_fee_res=True)
-
-	def ethdev_txbump(self,name,ext=',40000]{}.rawtx',fee='50G',add_args=[]):
-		ext = ext.format('-α' if g.debug_utf8 else '')
-		tx_fn = get_file_with_ext(ext,cfg['tmpdir'],no_dot=True)
-		t = MMGenExpect(name,'mmgen-txbump', eth_args() + add_args + ['--yes',tx_fn])
-		t.expect('or gas price: ',fee+'\n')
-		t.read()
-		t.ok()
-
-	def ethdev_txsign4(self,name): self.ethdev_txsign(name,ni=True,ext='.45495,50000]{}.rawtx')
-	def ethdev_txsend4(self,name): self.ethdev_txsend(name,ext='.45495,50000]{}.sigtx')
-	def ethdev_bal4(self,name): self.ethdev_bal(name,n='4')
-
-	def ethdev_txcreate5(self,name):
-		args = [eth_burn_addr + ','+eth_amt1]
-		return self.ethdev_txcreate(name,args=args,acct='10',non_mmgen_inputs=1)
-	def ethdev_txsign5(self,name): self.ethdev_txsign(name,ni=True,ext=eth_amt1+',50000]{}.rawtx')
-	def ethdev_txsend5(self,name): self.ethdev_txsend(name,ext=eth_amt1+',50000]{}.sigtx')
-	def ethdev_bal5(self,name): self.ethdev_bal(name,n='5')
-
-	bal_corr = Decimal('0.0000032') # gas use for token sends varies between ETH and ETC!
-	def ethdev_bal(self,name,n=None):
-		t = MMGenExpect(name,'mmgen-tool', eth_args() + ['twview','wide=1'])
-		for b in eth_bals[n]:
-			addr,amt,adj = b if len(b) == 3 else b + (False,)
-			if adj and g.coin == 'ETC': amt = str(Decimal(amt) + self.bal_corr)
-			pat = r'{}\s+{}\s'.format(addr,amt.replace('.',r'\.'))
-			t.expect(pat,regex=True)
-		t.read()
-		t.ok()
-
-	def ethdev_token_bal(self,name,n=None):
-		t = MMGenExpect(name,'mmgen-tool', eth_args() + ['--token=mm1','twview','wide=1'])
-		for b in eth_token_bals[n]:
-			addr,amt1,amt2,adj = b if len(b) == 4 else b + (False,)
-			if adj and g.coin == 'ETC': amt2 = str(Decimal(amt2) + self.bal_corr)
-			pat = r'{}\s+{}\s+{}\s'.format(addr,amt1.replace('.',r'\.'),amt2.replace('.',r'\.'))
-			t.expect(pat,regex=True)
-		t.read()
-		t.ok()
-
-	def ethdev_bal_getbalance(self,name,idx,etc_adj=False,extra_args=[]):
-		bal1 = eth_token_bals_getbalance[idx][0]
-		bal2 = eth_token_bals_getbalance[idx][1]
-		bal1 = Decimal(bal1)
-		if etc_adj and g.coin == 'ETC': bal1 += self.bal_corr
-		t = MMGenExpect(name,'mmgen-tool', eth_args() + extra_args + ['getbalance'])
-		t.expect(r'\n[0-9A-F]{8}: .* '+str(bal1),regex=True)
-		t.expect(r'\nNon-MMGen: .* '+bal2,regex=True)
-		total = t.expect_getend(r'\nTOTAL:\s+',regex=True).split()[0]
-		t.read()
-		assert Decimal(bal1) + Decimal(bal2) == Decimal(total)
-		t.ok()
-
-	def ethdev_add_label(self,name,addr='98831F3A:E:3',lbl=utf8_label):
-		t = MMGenExpect(name,'mmgen-tool', eth_args() + ['add_label',addr,lbl])
-		t.expect('Added label.*in tracking wallet',regex=True)
-		t.ok()
-
-	def ethdev_chk_label(self,name,addr='98831F3A:E:3',label_pat=utf8_label_pat):
-		t = MMGenExpect(name,'mmgen-tool', eth_args() + ['listaddresses','all_labels=1'])
-		t.expect(r'{}\s+\S{{30}}\S+\s+{}\s+'.format(addr,(label_pat or label)),regex=True)
-		t.ok()
-
-	def ethdev_remove_label(self,name,addr='98831F3A:E:3'):
-		t = MMGenExpect(name,'mmgen-tool', eth_args() + ['remove_label',addr])
-		t.expect('Removed label.*in tracking wallet',regex=True)
-		t.ok()
-
-	def ethdev_rpc_init(self):
-		init_coin(g.coin)
-		g.proto.rpc_port = 8549
-		rpc_init()
-
-	def ethdev_token_compile(self,name,token_data={}):
-		MMGenExpect(name,'',msg_only=True)
-		cmd_args = ['--{}={}'.format(k,v) for k,v in list(token_data.items())]
-		imsg("Compiling solidity token contract '{}' with 'solc'".format(token_data['symbol']))
-		cmd = ['scripts/create-token.py','--coin='+g.coin,'--outdir='+cfg['tmpdir']] + cmd_args + [eth_addr_chk]
-		imsg("Executing: {}".format(' '.join(cmd)))
-		subprocess.check_output(cmd,stderr=subprocess.STDOUT)
-		imsg("ERC20 token '{}' compiled".format(token_data['symbol']))
-		ok()
-
-	def ethdev_token_compile1(self,name):
-		token_data = { 'name':'MMGen Token 1', 'symbol':'MM1', 'supply':10**26, 'decimals':18 }
-		self.ethdev_token_compile(name,token_data)
-
-	def ethdev_token_compile2(self,name):
-		token_data = { 'name':'MMGen Token 2', 'symbol':'MM2', 'supply':10**18, 'decimals':10 }
-		self.ethdev_token_compile(name,token_data)
-
-	def ethdev_token_deploy(self,name,num,key,gas,mmgen_cmd='txdo',tx_fee='8G'):
-		self.ethdev_rpc_init()
-		key_fn = get_tmpfile_fn(cfg,cfg['parity_keyfile'])
-		fn = os.path.join(cfg['tmpdir'],key+'.bin')
-		os.environ['MMGEN_BOGUS_SEND'] = ''
-		args = ['-B','--tx-fee='+tx_fee,'--tx-gas={}'.format(gas),'--contract-data='+fn,'--inputs='+eth_addr,'--yes']
-		if mmgen_cmd == 'txdo': args += ['-k',key_fn]
-		t = MMGenExpect(name,'mmgen-'+mmgen_cmd, eth_args() + args)
-		if mmgen_cmd == 'txcreate':
-			t.written_to_file('Ethereum transaction')
-			ext = '[0,8000]{}.rawtx'.format('-α' if g.debug_utf8 else '')
-			tx_fn = get_file_with_ext(ext,cfg['tmpdir'],no_dot=True)
-			t = MMGenExpect(name,'mmgen-txsign', eth_args() + ['--yes','-k',key_fn,tx_fn],no_msg=True)
-			self.txsign_ui_common(t,name,ni=True,no_ok=True)
-			tx_fn = tx_fn.replace('.rawtx','.sigtx')
-			t = MMGenExpect(name,'mmgen-txsend', eth_args() + [tx_fn],no_msg=True)
-
-		os.environ['MMGEN_BOGUS_SEND'] = '1'
-		txid = self.txsend_ui_common(t,mmgen_cmd,quiet=True,bogus_send=False,no_ok=True)
-		addr = t.expect_getend('Contract address: ')
-		from mmgen.altcoins.eth.tx import EthereumMMGenTX as etx
-		assert etx.get_exec_status(txid.encode(),True) != 0,(
-			"Contract '{}:{}' failed to execute. Aborting".format(num,key))
-		if key == 'Token':
-			write_to_tmpfile(cfg,'token_addr{}'.format(num),addr+'\n')
-			imsg('\nToken #{} ({}) deployed!'.format(num,addr))
-		t.ok()
-
-	def ethdev_token_deploy1a(self,name): self.ethdev_token_deploy(name,num=1,key='SafeMath',gas=200000)
-	def ethdev_token_deploy1b(self,name): self.ethdev_token_deploy(name,num=1,key='Owned',gas=250000)
-	def ethdev_token_deploy1c(self,name): self.ethdev_token_deploy(name,num=1,key='Token',gas=1100000,tx_fee='7G')
-
-	def ethdev_tx_status2(self,name):
-		self.ethdev_tx_status(name,ext=g.coin+'[0,7000]{}.sigtx',expect_str='successfully executed')
-
-	def ethdev_bal6(self,name): return self.ethdev_bal5(name)
-
-	def ethdev_token_deploy2a(self,name): self.ethdev_token_deploy(name,num=2,key='SafeMath',gas=200000)
-	def ethdev_token_deploy2b(self,name): self.ethdev_token_deploy(name,num=2,key='Owned',gas=250000)
-	def ethdev_token_deploy2c(self,name): self.ethdev_token_deploy(name,num=2,key='Token',gas=1100000)
-
-	def ethdev_contract_deploy(self,name): # test create,sign,send
-		self.ethdev_token_deploy(name,num=2,key='SafeMath',gas=1100000,mmgen_cmd='txcreate')
-
-	def ethdev_token_transfer_ops(self,name,op,amt=1000):
-		MMGenExpect(name,'',msg_only=True)
-		sid = cfgs['8']['seed_id']
-		from mmgen.tool import MMGenToolCmd
-		usr_mmaddrs = ['{}:E:{}'.format(sid,i) for i in (11,21)]
-		usr_addrs = [MMGenToolCmd().gen_addr(addr,dfl_words) for addr in usr_mmaddrs]
-		self.ethdev_rpc_init()
-
-		from mmgen.altcoins.eth.contract import Token
-		from mmgen.altcoins.eth.tx import EthereumMMGenTX as etx
-		def do_transfer():
-			for i in range(2):
-				tk = Token(read_from_tmpfile(cfg,'token_addr{}'.format(i+1)).strip())
-				imsg_r('\n'+tk.info())
-				imsg('dev token balance (pre-send): {}'.format(tk.balance(eth_addr)))
-				imsg('Sending {} {} to address {} ({})'.format(amt,g.coin,usr_addrs[i],usr_mmaddrs[i]))
-				txid = tk.transfer(eth_addr,usr_addrs[i],amt,eth_key,
-									start_gas=ETHAmt(60000,'wei'),gasPrice=ETHAmt(8,'Gwei'))
-				assert etx.get_exec_status(txid,True) != 0,'Transfer of token funds failed. Aborting'
-
-		def show_bals():
-			for i in range(2):
-				tk = Token(read_from_tmpfile(cfg,'token_addr{}'.format(i+1)).strip())
-				imsg('Token: {}'.format(tk.symbol()))
-				imsg('dev token balance: {}'.format(tk.balance(eth_addr)))
-				imsg('usr token balance: {} ({} {})'.format(
-						tk.balance(usr_addrs[i]),usr_mmaddrs[i],usr_addrs[i]))
-
-		silence()
-		if op == 'show_bals': show_bals()
-		elif op == 'do_transfer': do_transfer()
-		end_silence()
-		ok()
-
-	def ethdev_token_fund_users(self,name):
-		return self.ethdev_token_transfer_ops(name,op='do_transfer')
-
-	def ethdev_token_user_bals(self,name):
-		return self.ethdev_token_transfer_ops(name,op='show_bals')
-
-	def ethdev_token_addrgen(self,name):
-		self.ethdev_addrgen(name,addrs='11-13')
-		self.ethdev_addrgen(name,addrs='21-23')
-
-	def ethdev_token_addrimport_badaddr1(self,name):
-		self.ethdev_addrimport(name,ext='[11-13]{}.addrs',add_args=['--token=abc'],bad_input=True)
-
-	def ethdev_token_addrimport_badaddr2(self,name):
-		self.ethdev_addrimport(name,ext='[11-13]{}.addrs',add_args=['--token='+'00deadbeef'*4],bad_input=True)
-
-	def ethdev_token_addrimport(self,name):
-		for n,r in ('1','11-13'),('2','21-23'):
-			tk_addr = read_from_tmpfile(cfg,'token_addr'+n).strip()
-			self.ethdev_addrimport(name,ext='['+r+']{}.addrs',expect='3/3',add_args=['--token='+tk_addr])
-
-	def ethdev_bal7(self,name): return self.ethdev_bal5(name)
-	def ethdev_token_bal1(self,name): self.ethdev_token_bal(name,n='1')
-
-	def ethdev_token_txcreate(self,name,args=[],token='',inputs='1',fee='50G'):
-		t = MMGenExpect(name,'mmgen-txcreate', eth_args() + ['--token='+token,'-B','--tx-fee='+fee] + args)
-		self.txcreate_ui_common(t,name,menu=[],
-								input_sels_prompt='to spend from',
-								inputs=inputs,file_desc='Ethereum token transaction',
-								add_comment=ref_tx_label_lat_cyr_gr)
-		return t
-	def ethdev_token_txsign(self,name,ext='',token=''):
-		self.ethdev_txsign(name,ni=True,ext=ext,add_args=['--token='+token])
-	def ethdev_token_txsend(self,name,ext='',token=''):
-		self.ethdev_txsend(name,ext=ext,add_args=['--token=mm1'])
-
-	def ethdev_token_txcreate1(self,name):
-		return self.ethdev_token_txcreate(name,args=['98831F3A:E:12,1.23456'],token='mm1')
-	def ethdev_token_txsign1(self,name):
-		self.ethdev_token_txsign(name,ext='1.23456,50000]{}.rawtx',token='mm1')
-	def ethdev_token_txsend1(self,name):
-		self.ethdev_token_txsend(name,ext='1.23456,50000]{}.sigtx',token='mm1')
-	def ethdev_token_bal2(self,name): self.ethdev_token_bal(name,n='2')
-
-	def ethdev_twview(self,name,args=[],expect_str='',tool_args=[],exit_val=0):
-		t = MMGenExpect(name,'mmgen-tool', eth_args() + args + ['twview'] + tool_args)
-		if expect_str:
-			t.expect(expect_str,regex=True)
-		t.read()
-		t.ok(exit_val=exit_val)
-
-	def ethdev_token_txcreate2(self,name):
-		return self.ethdev_token_txcreate(name,args=[eth_burn_addr+','+eth_amt2],token='mm1')
-
-	def ethdev_token_txbump(self,name):
-		self.ethdev_txbump(name,ext=eth_amt2+',50000]{}.rawtx',fee='56G',add_args=['--token=mm1'])
-
-	def ethdev_token_txsign2(self,name):
-		self.ethdev_token_txsign(name,ext=eth_amt2+',50000]{}.rawtx',token='mm1')
-	def ethdev_token_txsend2(self,name):
-		self.ethdev_token_txsend(name,ext=eth_amt2+',50000]{}.sigtx',token='mm1')
-
-	def ethdev_token_bal3(self,name): self.ethdev_token_bal(name,n='3')
-
-	def ethdev_del_dev_addr(self,name):
-		t = MMGenExpect(name,'mmgen-tool', eth_args() + ['remove_address',eth_addr])
-		t.read() # TODO
-		t.ok()
-
-	def ethdev_bal1_getbalance(self,name):
-		self.ethdev_bal_getbalance(name,'1',etc_adj=True)
-
-	def ethdev_addrimport_token_burn_addr(self,name):
-		self.ethdev_addrimport_one_addr(name,addr=eth_burn_addr,extra_args=['--token=mm1'])
-
-	def ethdev_token_bal4(self,name): self.ethdev_token_bal(name,n='4')
-
-	def ethdev_token_bal_getbalance(self,name):
-		self.ethdev_bal_getbalance(name,'2',extra_args=['--token=mm1'])
-
-	def ethdev_txcreate_noamt(self,name):
-		return self.ethdev_txcreate(name,args=['98831F3A:E:12'],eth_fee_res=True)
-	def ethdev_txsign_noamt(self,name):
-		self.ethdev_txsign(name,ext='99.99895,50000]{}.rawtx')
-	def ethdev_txsend_noamt(self,name):
-		self.ethdev_txsend(name,ext='99.99895,50000]{}.sigtx')
-
-	def ethdev_bal8(self,name):       self.ethdev_bal(name,n='8')
-	def ethdev_token_bal5(self,name): self.ethdev_token_bal(name,n='5')
-
-	def ethdev_token_txcreate_noamt(self,name):
-		return self.ethdev_token_txcreate(name,args=['98831F3A:E:13'],token='mm1',inputs='2',fee='51G')
-	def ethdev_token_txsign_noamt(self,name):
-		self.ethdev_token_txsign(name,ext='1.23456,51000]{}.rawtx',token='mm1')
-	def ethdev_token_txsend_noamt(self,name):
-		self.ethdev_token_txsend(name,ext='1.23456,51000]{}.sigtx',token='mm1')
-
-	def ethdev_bal9(self,name):       self.ethdev_bal(name,n='9')
-	def ethdev_token_bal6(self,name): self.ethdev_token_bal(name,n='6')
-
-	def ethdev_listaddresses(self,name,args=[],tool_args=['all_labels=1'],exit_val=0):
-		t = MMGenExpect(name,'mmgen-tool', eth_args() + args + ['listaddresses'] + tool_args)
-		t.read()
-		t.ok(exit_val=exit_val)
-
-	def ethdev_listaddresses1(self,name):
-		return self.ethdev_listaddresses(name)
-	def ethdev_listaddresses2(self,name):
-		return self.ethdev_listaddresses(name,tool_args=['minconf=999999999'])
-	def ethdev_listaddresses3(self,name):
-		return self.ethdev_listaddresses(name,tool_args=['sort=age'])
-	def ethdev_listaddresses4(self,name):
-		return self.ethdev_listaddresses(name,tool_args=['sort=age','showempty=1'])
-
-	def ethdev_token_listaddresses1(self,name):
-		return self.ethdev_listaddresses(name,args=['--token=mm1'])
-	def ethdev_token_listaddresses2(self,name):
-		return self.ethdev_listaddresses(name,args=['--token=mm1'],tool_args=['showempty=1'])
-
-	def ethdev_twview1(self,name):
-		return self.ethdev_twview(name)
-	def ethdev_twview2(self,name):
-		return self.ethdev_twview(name,tool_args=['wide=1'])
-	def ethdev_twview3(self,name):
-		return self.ethdev_twview(name,tool_args=['wide=1','sort=age'])
-	def ethdev_twview4(self,name):
-		return self.ethdev_twview(name,tool_args=['wide=1','minconf=999999999'])
-	def ethdev_twview5(self,name):
-		return self.ethdev_twview(name,tool_args=['wide=1','minconf=0'])
-	def ethdev_twview6(self,name):
-		return self.ethdev_twview(name,tool_args=['age_fmt=days'])
-
-	def ethdev_token_twview1(self,name):
-		return self.ethdev_twview(name,args=['--token=mm1'])
-	def ethdev_token_twview2(self,name):
-		return self.ethdev_twview(name,args=['--token=mm1'],tool_args=['wide=1'])
-	def ethdev_token_twview3(self,name):
-		return self.ethdev_twview(name,args=['--token=mm1'],tool_args=['wide=1','sort=age'])
-
-	def ethdev_edit_label(self,name,out_num,args=[],action='l',label_text=None):
-		t = MMGenExpect(name,'mmgen-txcreate', eth_args() + args + ['-B','-i'])
-		p1,p2 = ('emove address:\b','return to main menu): ')
-		p3,r3 = (p2,label_text+'\n') if label_text is not None else ('(y/N): ','y')
-		p4,r4 = (('(y/N): ',),('y',)) if label_text == '' else ((),())
-		for p,r in zip((p1,p1,p2,p3)+p4+(p1,p1),('M',action,out_num+'\n',r3)+r4+('M','q')):
-			t.expect(p,r)
-		t.ok()
-
-	def ethdev_edit_label1(self,name):
-		self.ethdev_edit_label(name,out_num=eth_rem_addrs[0],label_text='First added label-α')
-	def ethdev_edit_label2(self,name):
-		self.ethdev_edit_label(name,out_num=eth_rem_addrs[1],label_text='Second added label')
-	def ethdev_edit_label3(self,name):
-		self.ethdev_edit_label(name,out_num=eth_rem_addrs[0],label_text='')
-
-	def ethdev_remove_addr1(self,name):
-		self.ethdev_edit_label(name,out_num=eth_rem_addrs[0],action='R')
-	def ethdev_remove_addr2(self,name):
-		self.ethdev_edit_label(name,out_num=eth_rem_addrs[1],action='R')
-	def ethdev_remove_token_addr1(self,name):
-		self.ethdev_edit_label(name,out_num=eth_rem_addrs[0],args=['--token=mm1'],action='R')
-	def ethdev_remove_token_addr2(self,name):
-		self.ethdev_edit_label(name,out_num=eth_rem_addrs[1],args=['--token=mm1'],action='R')
-
-	def ethdev_stop(self,name):
-		MMGenExpect(name,'',msg_only=True)
-		if subprocess.call(['which','parity'],stdout=subprocess.PIPE) == 0:
-			pid = read_from_tmpfile(cfg,cfg['parity_pidfile'])
-			if opt.no_daemon_stop:
-				msg_r('(leaving daemon running by user request)')
-			else:
-				subprocess.check_call(['kill',pid])
-		else:
-			imsg('No parity executable found on system. Ignoring')
-		ok()
-
-	# undocumented admin commands
-	def ref_tx_addrgen(self,name,atype='L'):
-		if atype not in g.proto.mmtypes: return
-		t = MMGenExpect(name,'mmgen-addrgen',['--outdir='+cfg['tmpdir'],'--type='+atype,dfl_words,'1-2'])
-		t.read()
-
-	def ref_tx_addrgen1(self,name): self.ref_tx_addrgen(name,atype='L')
-	def ref_tx_addrgen2(self,name): self.ref_tx_addrgen(name,atype='C')
-	def ref_tx_addrgen3(self,name): self.ref_tx_addrgen(name,atype='S')
-	def ref_tx_addrgen4(self,name): self.ref_tx_addrgen(name,atype='B')
-
-	def ref_tx_txcreate(self,name,f1,f2,f3,f4):
-		sources = ['31','32']
-		if 'S' in g.proto.mmtypes: sources += ['33']
-		if 'B' in g.proto.mmtypes: sources += ['34']
-		self.txcreate_common(name,  sources=sources,
-									addrs_per_wallet=2,
-									add_args=['--locktime=1320969600'],
-									do_label=True)
-
-	# END methods
-	for k in (
-			'ref_wallet_conv',
-			'ref_mn_conv',
-			'ref_seed_conv',
-			'ref_hex_conv',
-			'ref_brain_conv',
-			'ref_incog_conv',
-			'ref_incox_conv',
-			'ref_hincog_conv',
-			'ref_hincog_conv_old',
-			'ref_wallet_conv_out',
-			'ref_mn_conv_out',
-			'ref_seed_conv_out',
-			'ref_hex_conv_out',
-			'ref_incog_conv_out',
-			'ref_incox_conv_out',
-			'ref_hincog_conv_out',
-			'ref_hincog_blkdev_conv_out',
-			'ref_wallet_chk',
-			'refwalletgen',
-			'ref_seed_chk',
-			'ref_hex_chk',
-			'ref_mn_chk',
-			'ref_brain_chk',
-			'ref_hincog_chk',
-			'refaddrgen',
-			'refkeyaddrgen',
-			'refaddrgen_compressed',
-			'refkeyaddrgen_compressed',
-			'refpasswdgen',
-			'ref_b32passwdgen',
-			'ref_hexpasswdgen'
-		):
-		for i in ('1','2','3'):
-			locals()[k+i] = locals()[k]
-
-	for k in ('walletgen','addrgen','keyaddrgen'): locals()[k+'14'] = locals()[k]
-
-# create temporary dirs
-if not opt.resume and not opt.skip_deps:
-	if g.platform == 'win':
-		for cfg in sorted(cfgs):
-			mk_tmpdir(cfgs[cfg]['tmpdir'])
-	else:
-		for cfg in sorted(cfgs):
-			src = os.path.join(shm_dir,cfgs[cfg]['tmpdir'].split('/')[-1])
-			mk_tmpdir(src)
-			try:
-				os.unlink(cfgs[cfg]['tmpdir'])
-			except OSError as e:
-				if e.errno != 2: raise
-			finally:
-				os.symlink(src,cfgs[cfg]['tmpdir'])
-
-have_dfl_wallet = False
-
-# main()
-if opt.pause:
-	import termios,atexit
-	fd = sys.stdin.fileno()
-	old = termios.tcgetattr(fd)
-	def at_exit():
-		termios.tcsetattr(fd, termios.TCSADRAIN, old)
-	atexit.register(at_exit)
-
-start_time = int(time.time())
-
-def end_msg():
-	t = int(time.time()) - start_time
-	m = '{} test{} performed.  Elapsed time: {:02d}:{:02d}\n'
-	sys.stderr.write(green(m.format(cmd_total,suf(cmd_total),t//60,t%60)))
-
-ts = MMGenTestSuite()
-
-if cmd_args and cmd_args[0] == 'admin':
-	cmd_args.pop(0)
-	cmd_data = cmd_data_admin
-	cmd_list = cmd_list_admin
-
-if opt.exit_after:
-	if opt.exit_after not in cmd_data.keys():
-		die(1,"'{}': command not recognized".format(opt.exit_after))
+set_environ_for_spawned_scripts()
 
 try:
-	if cmd_args:
-		for arg in cmd_args:
-			if arg in utils:
-				globals()[arg](cmd_args[cmd_args.index(arg)+1:])
-				sys.exit(0)
-			elif 'info_'+arg in cmd_data:
-				dirs = cmd_data['info_'+arg][1]
-				if dirs: clean(dirs)
-				for cmd in cmd_list[arg]:
-					check_needs_rerun(ts,cmd,build=True)
-			elif arg in meta_cmds:
-				for cmd in meta_cmds[arg]:
-					check_needs_rerun(ts,cmd,build=True)
-			elif arg in cmd_data:
-				check_needs_rerun(ts,arg,build=True)
-			else:
-				die(1,'{}: unrecognized command'.format(arg))
-	else:
-		clean()
-		for cmd in cmd_data:
-			if cmd == 'info_regtest': break # don't run everything after this by default
-			if cmd[:5] == 'info_':
-				gmsg('{}Testing {}'.format(('\n','')[bool(opt.resume)],cmd_data[cmd][0]))
-				continue
-			ts.do_cmd(cmd)
-			if cmd is not list(cmd_data.keys())[-1]: do_between()
+	tr = TestSuiteRunner(data_dir,trash_dir)
+	tr.run_tests(usr_args)
 except KeyboardInterrupt:
 	die(1,'\nExiting at user request')
 except TestSuiteException as e:
 	ydie(1,e.args[0])
 except TestSuiteFatalException as e:
 	rdie(1,e.args[0])
-except opt.traceback and Exception:
-	import traceback
-	print(''.join(traceback.format_exception(*sys.exc_info())))
-	try:
-		os.stat('my.err')
-		with open('my.err') as f:
-			t = f.readlines()
-			if t: msg_r('\n'+yellow(''.join(t[:-1]))+red(t[-1]))
-	except: pass
-	die(1,blue('Test script exited with error'))
+except Exception:
+	if opt.traceback:
+		import traceback
+		print(''.join(traceback.format_exception(*sys.exc_info())))
+		try:
+			os.stat('my.err')
+			with open('my.err') as f:
+				t = f.readlines()
+				if t: msg_r('\n'+yellow(''.join(t[:-1]))+red(t[-1]))
+		except: pass
+		die(1,blue('Test script exited with error'))
+	else:
+		raise
 except:
 	raise
-
-end_msg()

+ 0 - 0
test/test_py_d/___init__.py


+ 174 - 0
test/test_py_d/common.py

@@ -0,0 +1,174 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
+# Copyright (C)2013-2019 The MMGen Project <mmgen@tuta.io>
+#
+# 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/>.
+
+"""
+common.py: Shared routines and data for the test.py test suite
+"""
+
+import os,subprocess
+from mmgen.common import *
+
+log_file = 'test.py.log'
+
+rt_pw = 'abc-α'
+ref_wallet_brainpass = 'abc'
+ref_wallet_hash_preset = '1'
+ref_wallet_incog_offset = 123
+
+dfl_seed_id = '98831F3A'
+dfl_addr_idx_list = '1010,500-501,31-33,1,33,500,1011'
+dfl_wpasswd = 'reference password'
+
+pwfile = 'passwd_file'
+hincog_fn = 'rand_data'
+hincog_bytes = 1024*1024
+hincog_offset = 98765
+hincog_seedlen = 256
+
+incog_id_fn = 'incog_id'
+non_mmgen_fn = 'coinkey'
+
+ref_dir = os.path.join('test','ref')
+dfl_words_file = os.path.join(ref_dir,'98831F3A.mmwords')
+
+from mmgen.obj import MMGenTXLabel
+
+ref_tx_label_jp = '必要なのは、信用ではなく暗号化された証明に基づく電子取引システムであり、これにより希望する二者が信用できる第三者機関を介さずに直接取引できるよう' # 72 chars ('W'ide)
+ref_tx_label_zh = '所以,我們非常需要這樣一種電子支付系統,它基於密碼學原理而不基於信用,使得任何達成一致的雙方,能夠直接進行支付,從而不需要協力廠商仲介的參與。。' # 72 chars ('F'ull + 'W'ide)
+ref_tx_label_lat_cyr_gr = ''.join(map(chr,
+									list(range(65,91)) +
+									list(range(1040,1072)) + # cyrillic
+									list(range(913,939)) +   # greek
+									list(range(97,123))))[:MMGenTXLabel.max_len] # 72 chars
+utf8_label = ref_tx_label_zh[:40]
+utf8_label_pat = utf8_label
+
+ref_bw_hash_preset = '1'
+ref_bw_file = 'wallet.mmbrain'
+ref_bw_file_spc = 'wallet-spaced.mmbrain'
+
+ref_kafile_pass = 'kafile password'
+ref_kafile_hash_preset = '1'
+
+ref_enc_fn = 'sample-text.mmenc'
+tool_enc_passwd = "Scrypt it, don't hash it!"
+sample_text = 'The Times 03/Jan/2009 Chancellor on brink of second bailout for banks\n'
+chksum_pat = r'\b[A-F0-9]{4} [A-F0-9]{4} [A-F0-9]{4} [A-F0-9]{4}\b'
+
+def ok_msg():
+	if opt.profile: return
+	os.write(2,green('\nOK\n').encode() if opt.exact_output or opt.verbose else b' OK\n')
+
+def skip(name,reason=None):
+	msg('Skipping {}{}'.format(name,' ({})'.format(reason) if reason else ''))
+	return 'skip'
+
+def confirm_continue():
+	if keypress_confirm(blue('Continue? (Y/n): '),default_yes=True,complete_prompt=True):
+		if opt.verbose or opt.exact_output: sys.stderr.write('\n')
+	else:
+		raise KeyboardInterrupt('Exiting at user request')
+
+def omsg(s):
+	os.write(2,s.encode() + b'\n')
+def omsg_r(s):
+	os.write(2,s.encode())
+def imsg(s):
+	if opt.exact_output or opt.verbose: omsg(s)
+def imsg_r(s):
+	if opt.exact_output or opt.verbose: omsg_r(s)
+def iqmsg(s):
+	if not opt.quiet: omsg(s)
+def iqmsg_r(s):
+	if not opt.quiet: omsg_r(s)
+
+devnull_fh = open('/dev/null','w')
+def silence():
+	if not (opt.verbose or opt.exact_output):
+		g.stderr_fileno = g.stdout_fileno = devnull_fh.fileno()
+
+def end_silence():
+	if not (opt.verbose or opt.exact_output):
+		g.stderr_fileno = 2
+		g.stdout_fileno = 1
+
+def randbool():
+	return hexlify(os.urandom(1))[1] in b'12345678'
+
+def disable_debug():
+	global save_debug
+	save_debug = {}
+	for k in g.env_opts:
+		if k[:11] == 'MMGEN_DEBUG':
+			save_debug[k] = os.getenv(k)
+			os.environ[k] = ''
+
+def restore_debug():
+	for k in save_debug:
+		os.environ[k] = save_debug[k] or ''
+
+def get_file_with_ext(tdir,ext,delete=True,no_dot=False,return_list=False,delete_all=False):
+
+	dot = ('.','')[bool(no_dot)]
+	flist = [os.path.join(tdir,f) for f in os.listdir(tdir) if f == ext or f[-len(dot+ext):] == dot+ext]
+
+	if not flist: return False
+	if return_list: return flist
+
+	if len(flist) > 1 or delete_all:
+		if delete or delete_all:
+			if not opt.quiet:
+				msg("Multiple *.{} files in '{}' - deleting".format(ext,tdir))
+			for f in flist:
+				os.unlink(f)
+		return False
+	else:
+		return flist[0]
+
+labels = [
+	"Automotive",
+	"Travel expenses",
+	"Healthcare",
+	ref_tx_label_jp[:40],
+	ref_tx_label_zh[:40],
+	"Alice's allowance",
+	"Bob's bequest",
+	"House purchase",
+	"Real estate fund",
+	"Job 1",
+	"XYZ Corp.",
+	"Eddie's endowment",
+	"Emergency fund",
+	"Real estate fund",
+	"Ian's inheritance",
+	"",
+	"Rainy day",
+	"Fred's funds",
+	"Job 2",
+	"Carl's capital",
+]
+
+def get_label(do_shuffle=False):
+	from random import shuffle
+	global label_iter
+	try:
+		return next(label_iter)
+	except:
+		if do_shuffle: shuffle(labels)
+		label_iter = iter(labels)
+		return next(label_iter)

+ 236 - 0
test/test_py_d/ts_autosign.py

@@ -0,0 +1,236 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
+# Copyright (C)2013-2019 The MMGen Project <mmgen@tuta.io>
+#
+# 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/>.
+
+"""
+ts_autosign.py: Autosign tests for the test.py test suite
+"""
+
+import os,shutil
+from mmgen.globalvars import g
+from mmgen.opts import opt
+from test.common import read_from_file
+from test.test_py_d.common import *
+
+from test.test_py_d.ts_base import *
+from test.test_py_d.ts_shared import *
+
+class TestSuiteAutosign(TestSuiteBase):
+	'autosigning with BTC, BCH, LTC, ETH and ETC'
+	networks = ('btc',)
+	tmpdir_nums = [18]
+	cmd_group = (
+		('autosign', 'transaction autosigning (BTC,BCH,LTC,ETH,ETC)'),
+	)
+
+	def autosign_live(self):
+		return self.autosign_minimal(live=True)
+
+	def autosign_minimal(self,live=False):
+		return self.autosign(
+					coins=['btc','eth'],
+					txfiles=['btc','eth','mm1','etc'],
+					txcount=7,
+					live=live)
+
+	# tests everything except device detection, mount/unmount
+	def autosign(   self,
+					coins=['btc','bch','ltc','eth'],
+					txfiles=['btc','bch','ltc','eth','mm1','etc'],
+					txcount=11,
+					live=False):
+
+		if self.skip_for_win(): return
+
+		def make_wallet(opts):
+			t = self.spawn('mmgen-autosign',opts+['gen_key'],extra_desc='(gen_key)')
+			t.expect_getend('Wrote key file ')
+			t.ok()
+
+			t = self.spawn('mmgen-autosign',opts+['setup'],extra_desc='(setup)')
+			t.expect('words: ','3')
+			t.expect('OK? (Y/n): ','\n')
+			mn_file = dfl_words_file
+			mn = read_from_file(mn_file).strip().split()
+			mn = ['foo'] + mn[:5] + ['realiz','realized'] + mn[5:]
+			wnum = 1
+			max_wordlen = 12
+
+			def get_pad_chars(n):
+				ret = ''
+				for i in range(n):
+					m = int(hexlify(os.urandom(1)),16) % 32
+					ret += r'123579!@#$%^&*()_+-=[]{}"?/,.<>|'[m]
+				return ret
+
+			for i in range(len(mn)):
+				w = mn[i]
+				if len(w) > 5:
+					w = w + '\n'
+				else:
+					w = get_pad_chars(3 if randbool() else 0) + w[0] + get_pad_chars(3) + w[1:] + get_pad_chars(7)
+					w = w[:max_wordlen+1]
+				em,rm = 'Enter word #{}: ','Repeat word #{}: '
+				ret = t.expect((em.format(wnum),rm.format(wnum-1)))
+				if ret == 0: wnum += 1
+				for j in range(len(w)):
+					t.send(w[j])
+					time.sleep(0.005)
+			wf = t.written_to_file('Autosign wallet')
+			t.ok()
+
+		def copy_files(mountpoint,remove_signed_only=False,include_bad_tx=True):
+			fdata_in = (('btc',''),
+						('bch',''),
+						('ltc','litecoin'),
+						('eth','ethereum'),
+						('mm1','ethereum'),
+						('etc','ethereum_classic'))
+			fdata = [e for e in fdata_in if e[0] in txfiles]
+			from test.test_py_d.ts_ref import TestSuiteRef
+			tfns  = [TestSuiteRef.sources['ref_tx_file'][c][1] for c,d in fdata] + \
+					[TestSuiteRef.sources['ref_tx_file'][c][0] for c,d in fdata]
+			tfs = [joinpath(ref_dir,d[1],fn) for d,fn in zip(fdata+fdata,tfns)]
+
+			for f,fn in zip(tfs,tfns):
+				if fn: # use empty fn to skip file
+					target = joinpath(mountpoint,'tx',fn)
+					remove_signed_only or shutil.copyfile(f,target)
+					try: os.unlink(target.replace('.rawtx','.sigtx'))
+					except: pass
+
+			# make a bad tx file
+			bad_tx = joinpath(mountpoint,'tx','bad.rawtx')
+			if include_bad_tx and not remove_signed_only:
+				with open(bad_tx,'w') as f:
+					f.write('bad tx data')
+			if not include_bad_tx:
+				try: os.unlink(bad_tx)
+				except: pass
+
+		def do_autosign_live(opts,mountpoint,led_opts=[],gen_wallet=True):
+
+			def do_mount():
+				try: subprocess.check_call(['mount',mountpoint])
+				except: pass
+
+			def do_unmount():
+				try: subprocess.check_call(['umount',mountpoint])
+				except: pass
+				omsg_r(blue('\nRemove removable device and then hit ENTER '))
+				input()
+
+			if gen_wallet: make_wallet(opts)
+			else: do_mount()
+
+			copy_files(mountpoint,include_bad_tx=not led_opts)
+
+			desc = '(sign)'
+			m1 = "Running 'mmgen-autosign wait'"
+			m2 = 'Insert removable device '
+
+			if led_opts:
+				if led_opts == ['--led']:
+					m1 = "Running 'mmgen-autosign wait' with --led. The LED should start blinking slowly now"
+				elif led_opts == ['--stealth-led']:
+					m1 = "Running 'mmgen-autosign wait' with --stealth-led. You should see no LED activity now"
+				m2 = 'Insert removable device and watch for fast LED activity during signing'
+				desc = '(sign - {})'.format(led_opts[0])
+
+			def do_loop():
+				omsg(blue(m2))
+				t.expect('{} transactions signed'.format(txcount))
+				if not led_opts:
+					t.expect('1 transaction failed to sign')
+				t.expect('Waiting')
+
+			do_unmount()
+			omsg(green(m1))
+			t = self.spawn('mmgen-autosign',opts+led_opts+['wait'],extra_desc=desc)
+			if not opt.exact_output: omsg('')
+			do_loop()
+			do_mount() # race condition due to device insertion detection
+			copy_files(mountpoint,remove_signed_only=True,include_bad_tx=not led_opts)
+			do_unmount()
+			do_loop()
+			t.kill(2) # 2 = SIGINT
+			t.req_exit_val = 1
+			return t
+
+		def do_autosign(opts,mountpoint):
+			make_wallet(opts)
+			copy_files(mountpoint,include_bad_tx=True)
+			t = self.spawn('mmgen-autosign',opts+['wait'],extra_desc='(sign)')
+			t.expect('{} transactions signed'.format(txcount))
+			t.expect('1 transaction failed to sign')
+			t.expect('Waiting')
+			t.kill(2)
+			t.req_exit_val = 1
+			return t
+
+		if live:
+			mountpoint = '/mnt/tx'
+			if not os.path.ismount(mountpoint):
+				try:
+					subprocess.check_call(['mount',mountpoint])
+					imsg("Mounted '{}'".format(mountpoint))
+				except:
+					ydie(1,"Could not mount '{}'!  Exiting".format(mountpoint))
+
+			txdir = joinpath(mountpoint,'tx')
+			if not os.path.isdir(txdir):
+				ydie(1,"Directory '{}' does not exist!  Exiting".format(mountpoint))
+
+			opts = ['--coins='+','.join(coins)]
+			led_files = {   'opi': ('/sys/class/leds/orangepi:red:status/brightness',),
+							'rpi': ('/sys/class/leds/led0/brightness','/sys/class/leds/led0/trigger') }
+			for k in ('opi','rpi'):
+				if os.path.exists(led_files[k][0]):
+					led_support = k
+					break
+			else:
+				led_support = None
+
+			if led_support:
+				for fn in (led_files[led_support]):
+					subprocess.check_call(['sudo','chmod','0666',fn])
+				omsg(purple('Running autosign test with no LED'))
+				do_autosign_live(opts,mountpoint)
+				omsg(purple("Running autosign test with '--led'"))
+				do_autosign_live(opts,mountpoint,led_opts=['--led'],gen_wallet=False)
+				omsg(purple("Running autosign test with '--stealth-led'"))
+				return do_autosign_live(opts,mountpoint,led_opts=['--stealth-led'],gen_wallet=False)
+			else:
+				return do_autosign_live(opts,mountpoint)
+		else:
+			mountpoint = self.tmpdir
+			opts = ['--no-insert-check','--mountpoint='+mountpoint,'--coins='+','.join(coins)]
+			try: os.mkdir(joinpath(mountpoint,'tx'))
+			except: pass
+			return do_autosign(opts,mountpoint)
+
+class TestSuiteAutosignMinimal(TestSuiteAutosign):
+	'autosigning with BTC, ETH and ETC'
+	cmd_group = (
+		('autosign_minimal', 'transaction autosigning (BTC,ETH,ETC)'),
+	)
+
+class TestSuiteAutosignLive(TestSuiteAutosignMinimal):
+	'live autosigning operations'
+	cmd_group = (
+		('autosign_live', 'transaction autosigning (BTC,ETH,ETC - test device insertion/removal + LED)'),
+	)

+ 73 - 0
test/test_py_d/ts_base.py

@@ -0,0 +1,73 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
+# Copyright (C)2013-2019 The MMGen Project <mmgen@tuta.io>
+#
+# 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/>.
+
+"""
+ts_base.py: Base class for the test.py test suite
+"""
+
+import os
+from mmgen.globalvars import g
+from mmgen.opts import opt
+from test.common import *
+from test.test_py_d.common import *
+
+class TestSuiteBase(object):
+	'initializer class for the test.py test suite'
+	passthru_opts = ()
+	networks = ()
+	segwit_opts_ok = False
+
+	def __init__(self,trunner,cfgs,spawn):
+		self.tr = trunner
+		self.cfgs = cfgs
+		self.spawn = spawn
+		self.have_dfl_wallet = False
+		self.usr_rand_chars = (5,30)[bool(opt.usr_random)]
+		self.usr_rand_arg = '-r{}'.format(self.usr_rand_chars)
+		self.altcoin_pfx = '' if g.proto.base_coin == 'BTC' else '-'+g.proto.base_coin
+		self.tn_ext = ('','.testnet')[g.testnet]
+		d = {'bch':'btc','btc':'btc','ltc':'ltc'}
+		self.fork = d[g.coin.lower()] if g.coin.lower() in d else None
+
+	@property
+	def tmpdir(self):
+		return os.path.join('test','tmp{}{}'.format(self.tmpdir_num,'-α' if g.debug_utf8 else ''))
+
+	@property
+	def segwit_mmtype(self):
+		return ('segwit','bech32')[bool(opt.bech32)] if self.segwit else None
+
+	@property
+	def segwit_arg(self):
+		return ['--type=' + self.segwit_mmtype] if self.segwit_mmtype else []
+
+	def get_file_with_ext(self,ext,**kwargs):
+		return get_file_with_ext(self.tmpdir,ext,**kwargs)
+
+	def read_from_tmpfile(self,fn,binary=False):
+		return read_from_file(os.path.join(self.tmpdir,fn),binary=binary)
+
+	def write_to_tmpfile(self,fn,data,binary=False):
+		return write_to_file(os.path.join(self.tmpdir,fn),data,binary=binary)
+
+	def skip_for_win(self):
+		if g.platform == 'win':
+			msg("Skipping test '{}': not supported on MinGW platform".format(self.test_name))
+			return True
+		else:
+			return False

+ 143 - 0
test/test_py_d/ts_chainsplit.py

@@ -0,0 +1,143 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
+# Copyright (C)2013-2019 The MMGen Project <mmgen@tuta.io>
+#
+# 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/>.
+
+"""
+ts_chainsplit.py: Forking scenario tests for the test.py test suite
+This module is currently non-functional
+"""
+import os
+from mmgen.globalvars import g
+from mmgen.opts import opt
+from mmgen.util import die
+from test.common import *
+from test.test_py_d.common import *
+
+from test.test_py_d.ts_base import *
+from test.test_py_d.ts_shared import *
+from test.test_py_d.ts_regtest import *
+
+class TestSuiteChainsplit(TestSuiteRegtest):
+	'forking scenario tests for the test.py test suite'
+	cmd_group = (
+		('split_setup',        'regtest forking scenario setup'),
+		('walletgen_bob',      "generating Bob's wallet"),
+		('addrgen_bob',        "generating Bob's addresses"),
+		('addrimport_bob',     "importing Bob's addresses"),
+		('fund_bob',           "funding Bob's wallet"),
+		('split_fork',         'regtest split fork'),
+		('split_start_btc',    'start regtest daemon (BTC)'),
+		('split_start_b2x',    'start regtest daemon (B2X)'),
+		('split_gen_btc',      'mining a block (BTC)'),
+		('split_gen_b2x',      'mining 100 blocks (B2X)'),
+		('split_do_split',     'creating coin splitting transactions'),
+		('split_sign_b2x',     'signing B2X split transaction'),
+		('split_sign_btc',     'signing BTC split transaction'),
+		('split_send_b2x',     'sending B2X split transaction'),
+		('split_send_btc',     'sending BTC split transaction'),
+		('split_gen_btc',      'mining a block (BTC)'),
+		('split_gen_b2x2',     'mining a block (B2X)'),
+		('split_txdo_timelock_bad_btc', 'sending transaction with bad locktime (BTC)'),
+		('split_txdo_timelock_good_btc','sending transaction with good locktime (BTC)'),
+		('split_txdo_timelock_bad_b2x', 'sending transaction with bad locktime (B2X)'),
+		('split_txdo_timelock_good_b2x','sending transaction with good locktime (B2X)'),
+	)
+
+	def split_setup(self):
+		if g.coin != 'BTC': die(1,'Test valid only for coin BTC')
+		opt.coin = 'BTC'
+		return self.setup()
+
+	def split_fork(self):
+		opt.coin = 'B2X'
+		t = self.spawn('mmgen-regtest',['fork','btc'])
+		t.expect('Creating fork from coin')
+		t.expect('successfully created')
+		t.ok()
+
+	def split_start(self,coin):
+		opt.coin = coin
+		t = self.spawn('mmgen-regtest',['bob'])
+		t.expect('Starting')
+		t.expect('done')
+		t.ok()
+
+	def split_start_btc(self): self.regtest_start(coin='BTC')
+	def split_start_b2x(self): self.regtest_start(coin='B2X')
+	def split_gen_btc(self):   self.regtest_generate(coin='BTC')
+	def split_gen_b2x(self):   self.regtest_generate(coin='B2X',num_blocks=100)
+	def split_gen_b2x2(self):  self.regtest_generate(coin='B2X')
+
+	def split_do_split(self):
+		opt.coin = 'B2X'
+		sid = self.regtest_user_sid('bob')
+		t = self.spawn('mmgen-split',[
+			'--bob',
+			'--outdir='+self.tmpdir,
+			'--tx-fees=0.0001,0.0003',
+			sid+':S:1',sid+':S:2'])
+		t.expect(r'\[q\]uit view, .*?:.','q', regex=True)
+		t.expect('outputs to spend: ','1\n')
+
+		for tx in ('timelocked','split'):
+			for q in ('fee','change'): t.expect('OK? (Y/n): ','y')
+			t.do_comment(False)
+			t.view_tx('t')
+
+		t.written_to_file('Long chain (timelocked) transaction')
+		t.written_to_file('Short chain transaction')
+		t.ok()
+
+	def split_sign(self,coin,ext):
+		wf = get_file_with_ext(self.regtest_user_dir('bob',coin=coin.lower()),'mmdat')
+		txfile = self.get_file_with_ext(ext,no_dot=True)
+		opt.coin = coin
+		self.txsign(txfile,wf,extra_opts=['--bob'])
+
+	def split_sign_b2x(self):
+		return self.regtest_sign(coin='B2X',ext='533].rawtx')
+
+	def split_sign_btc(self):
+		return self.regtest_sign(coin='BTC',ext='9997].rawtx')
+
+	def split_send(self,coin,ext):
+		opt.coin = coin
+		txfile = self.get_file_with_ext(ext,no_dot=True)
+		self.txsend(txfile,bogus_send=False,extra_opts=['--bob'])
+
+	def split_send_b2x(self):
+		return self.regtest_send(coin='B2X',ext='533].sigtx')
+
+	def split_send_btc(self):
+		return self.regtest_send(coin='BTC',ext='9997].sigtx')
+
+	def split_txdo_timelock(self,coin,locktime,bad_locktime):
+		opt.coin = coin
+		sid = self.regtest_user_sid('bob')
+		self.regtest_user_txdo( 'bob','0.0001',[sid+':S:5'],'1',pw=rt_pw,
+								extra_args=['--locktime='+str(locktime)],
+								bad_locktime=bad_locktime)
+
+	def split_txdo_timelock_bad_btc(self):
+		self.regtest_txdo_timelock('BTC',locktime=8888,bad_locktime=True)
+	def split_txdo_timelock_good_btc(self):
+		self.regtest_txdo_timelock('BTC',locktime=1321009871,bad_locktime=False)
+	def split_txdo_timelock_bad_b2x(self):
+		self.regtest_txdo_timelock('B2X',locktime=8888,bad_locktime=True)
+	def split_txdo_timelock_good_b2x(self):
+		self.regtest_txdo_timelock('B2X',locktime=1321009871,bad_locktime=False)
+

+ 773 - 0
test/test_py_d/ts_ethdev.py

@@ -0,0 +1,773 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
+# Copyright (C)2013-2019 The MMGen Project <mmgen@tuta.io>
+#
+# 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/>.
+
+"""
+ts_ethdev.py: Ethdev tests for the test.py test suite
+"""
+
+import sys,os,subprocess,re,shutil
+from decimal import Decimal
+from mmgen.globalvars import g
+from mmgen.opts import opt
+from mmgen.util import die
+from mmgen.exception import *
+from test.common import *
+from test.test_py_d.common import *
+
+del_addrs = ('4','1')
+dfl_sid = '98831F3A'
+
+# The Parity dev address with lots of coins.  Create with "ethkey -b info ''":
+dfl_addr = '00a329c0648769a73afac7f9381e08fb43dbea72'
+dfl_addr_chk = '00a329c0648769A73afAc7F9381E08FB43dBEA72'
+dfl_privkey = '4d5db4107d237df6a3d58ee5f70ae63d73d7658d4026f2eefd2f204c81682cb7'
+burn_addr = 'deadbeef'*5
+amt1 = '999999.12345689012345678'
+amt2 = '888.111122223333444455'
+
+parity_pid_fn = 'parity.pid'
+parity_key_fn = 'parity.devkey'
+
+# Token sends require varying amounts of gas, depending on compiler version
+solc_ver = re.search(r'Version:\s*(.*)',
+				subprocess.Popen(['solc','--version'],stdout=subprocess.PIPE
+					).stdout.read().decode()).group(1)
+
+if re.match(r'\b0.5.1\b',solc_ver): # Raspbian Stretch
+	vbal1 = '1.2288337'
+	vbal2 = '99.997085083'
+	vbal3 = '1.23142165'
+	vbal4 = '127.0287837'
+elif re.match(r'\b0.5.3\b',solc_ver): # Ubuntu Bionic
+	vbal1 = '1.2288487'
+	vbal2 = '99.997092733'
+	vbal3 = '1.23142915'
+	vbal4 = '127.0287987'
+
+bals = {
+	'1': [  ('98831F3A:E:1','123.456')],
+	'2': [  ('98831F3A:E:1','123.456'),('98831F3A:E:11','1.234')],
+	'3': [  ('98831F3A:E:1','123.456'),('98831F3A:E:11','1.234'),('98831F3A:E:21','2.345')],
+	'4': [  ('98831F3A:E:1','100'),
+			('98831F3A:E:2','23.45495'),
+			('98831F3A:E:11','1.234'),
+			('98831F3A:E:21','2.345')],
+	'5': [  ('98831F3A:E:1','100'),
+			('98831F3A:E:2','23.45495'),
+			('98831F3A:E:11','1.234'),
+			('98831F3A:E:21','2.345'),
+			(burn_addr + '\s+Non-MMGen',amt1)],
+	'8': [  ('98831F3A:E:1','0'),
+			('98831F3A:E:2','23.45495'),
+			('98831F3A:E:11',vbal1,'a'),
+			('98831F3A:E:12','99.99895'),
+			('98831F3A:E:21','2.345'),
+			(burn_addr + '\s+Non-MMGen',amt1)],
+	'9': [  ('98831F3A:E:1','0'),
+			('98831F3A:E:2','23.45495'),
+			('98831F3A:E:11',vbal1,'a'),
+			('98831F3A:E:12',vbal2),
+			('98831F3A:E:21','2.345'),
+			(burn_addr + '\s+Non-MMGen',amt1)]
+}
+token_bals = {
+	'1': [  ('98831F3A:E:11','1000','1.234')],
+	'2': [  ('98831F3A:E:11','998.76544',vbal3,'a'),
+			('98831F3A:E:12','1.23456','0')],
+	'3': [  ('98831F3A:E:11','110.654317776666555545',vbal1,'a'),
+			('98831F3A:E:12','1.23456','0')],
+	'4': [  ('98831F3A:E:11','110.654317776666555545',vbal1,'a'),
+			('98831F3A:E:12','1.23456','0'),
+			(burn_addr + '\s+Non-MMGen',amt2,amt1)],
+	'5': [  ('98831F3A:E:11','110.654317776666555545',vbal1,'a'),
+			('98831F3A:E:12','1.23456','99.99895'),
+			(burn_addr + '\s+Non-MMGen',amt2,amt1)],
+	'6': [  ('98831F3A:E:11','110.654317776666555545',vbal1,'a'),
+			('98831F3A:E:12','0',vbal2),
+			('98831F3A:E:13','1.23456','0'),
+			(burn_addr + '\s+Non-MMGen',amt2,amt1)]
+}
+token_bals_getbalance = {
+	'1': (vbal4,'999999.12345689012345678'),
+	'2': ('111.888877776666555545','888.111122223333444455')
+}
+
+from test.test_py_d.ts_base import *
+from test.test_py_d.ts_shared import *
+
+class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
+	'Ethereum transacting, token deployment and tracking wallet operations'
+	networks = ('eth','etc')
+	passthru_opts = ('coin',)
+	tmpdir_nums = [22]
+	cmd_group = (
+		('setup',               'Ethereum Parity dev mode tests for coin {} (start parity)'.format(g.coin)),
+		('addrgen',             'generating addresses'),
+		('addrimport',          'importing addresses'),
+		('addrimport_dev_addr', "importing Parity dev address 'Ox00a329c..'"),
+
+		('txcreate1',           'creating a transaction (spend from dev address to address :1)'),
+		('txsign1',             'signing the transaction'),
+		('txsign1_ni',          'signing the transaction (non-interactive)'),
+		('txsend1',             'sending the transaction'),
+		('bal1',                'the {} balance'.format(g.coin)),
+
+		('txcreate2',           'creating a transaction (spend from dev address to address :11)'),
+		('txsign2',             'signing the transaction'),
+		('txsend2',             'sending the transaction'),
+		('bal2',                'the {} balance'.format(g.coin)),
+
+		('txcreate3',           'creating a transaction (spend from dev address to address :21)'),
+		('txsign3',             'signing the transaction'),
+		('txsend3',             'sending the transaction'),
+		('bal3',                'the {} balance'.format(g.coin)),
+
+		('tx_status1',          'getting the transaction status'),
+
+		('txcreate4',           'creating a transaction (spend from MMGen address, low TX fee)'),
+		('txbump',              'bumping the transaction fee'),
+
+		('txsign4',             'signing the transaction'),
+		('txsend4',             'sending the transaction'),
+		('bal4',                'the {} balance'.format(g.coin)),
+
+		('txcreate5',           'creating a transaction (fund burn address)'),
+		('txsign5',             'signing the transaction'),
+		('txsend5',             'sending the transaction'),
+
+		('addrimport_burn_addr',"importing burn address"),
+		('bal5',                'the {} balance'.format(g.coin)),
+
+		('add_label',           'adding a UTF-8 label'),
+		('chk_label',           'the label'),
+		('remove_label',        'removing the label'),
+
+		('token_compile1',       'compiling ERC20 token #1'),
+
+		('token_deploy1a',       'deploying ERC20 token #1 (SafeMath)'),
+		('token_deploy1b',       'deploying ERC20 token #1 (Owned)'),
+		('token_deploy1c',       'deploying ERC20 token #1 (Token)'),
+
+		('tx_status2',           'getting the transaction status'),
+		('bal6',                 'the {} balance'.format(g.coin)),
+
+		('token_compile2',       'compiling ERC20 token #2'),
+
+		('token_deploy2a',       'deploying ERC20 token #2 (SafeMath)'),
+		('token_deploy2b',       'deploying ERC20 token #2 (Owned)'),
+		('token_deploy2c',       'deploying ERC20 token #2 (Token)'),
+
+		('contract_deploy',      'deploying contract (create,sign,send)'),
+
+		('token_fund_users',     'transferring token funds from dev to user'),
+		('token_user_bals',      'show balances after transfer'),
+		('token_addrgen',        'generating token addresses'),
+		('token_addrimport_badaddr1','importing token addresses (no token address)'),
+		('token_addrimport_badaddr2','importing token addresses (bad token address)'),
+		('token_addrimport',    'importing token addresses'),
+
+		('bal7',                'the {} balance'.format(g.coin)),
+		('token_bal1',          'the {} balance and token balance'.format(g.coin)),
+
+		('token_txcreate1',     'creating a token transaction'),
+		('token_txsign1',       'signing the transaction'),
+		('token_txsend1',       'sending the transaction'),
+		('token_bal2',          'the {} balance and token balance'.format(g.coin)),
+
+		('token_txcreate2',     'creating a token transaction (to burn address)'),
+		('token_txbump',        'bumping the transaction fee'),
+
+		('token_txsign2',       'signing the transaction'),
+		('token_txsend2',       'sending the transaction'),
+		('token_bal3',          'the {} balance and token balance'.format(g.coin)),
+
+		('del_dev_addr',        "deleting the dev address"),
+
+		('bal1_getbalance',     'the {} balance (getbalance)'.format(g.coin)),
+
+		('addrimport_token_burn_addr',"importing the token burn address"),
+
+		('token_bal4',          'the {} balance and token balance'.format(g.coin)),
+		('token_bal_getbalance','the token balance (getbalance)'),
+
+		('txcreate_noamt',     'creating a transaction (full amount send)'),
+		('txsign_noamt',       'signing the transaction'),
+		('txsend_noamt',       'sending the transaction'),
+
+		('bal8',                'the {} balance'.format(g.coin)),
+		('token_bal5',          'the token balance'),
+
+		('token_txcreate_noamt', 'creating a token transaction (full amount send)'),
+		('token_txsign_noamt',   'signing the transaction'),
+		('token_txsend_noamt',   'sending the transaction'),
+
+		('bal9',                'the {} balance'.format(g.coin)),
+		('token_bal6',          'the token balance'),
+
+		('listaddresses1',      'listaddresses'),
+		('listaddresses2',      'listaddresses minconf=999999999 (ignored)'),
+		('listaddresses3',      'listaddresses sort=age (ignored)'),
+		('listaddresses4',      'listaddresses showempty=1 sort=age (ignored)'),
+
+		('token_listaddresses1','listaddresses --token=mm1'),
+		('token_listaddresses2','listaddresses --token=mm1 showempty=1'),
+
+		('twview1','twview'),
+		('twview2','twview wide=1'),
+		('twview3','twview wide=1 sort=age (ignored)'),
+		('twview4','twview wide=1 minconf=999999999 (ignored)'),
+		('twview5','twview wide=1 minconf=0 (ignored)'),
+		('twview6','twview age_fmt=days (ignored)'),
+
+		('token_twview1','twview --token=mm1'),
+		('token_twview2','twview --token=mm1 wide=1'),
+		('token_twview3','twview --token=mm1 wide=1 sort=age (ignored)'),
+
+		('edit_label1','adding label to addr #{} in {} tracking wallet'.format(del_addrs[0],g.coin)),
+		('edit_label2','adding label to addr #{} in {} tracking wallet'.format(del_addrs[1],g.coin)),
+		('edit_label3','removing label from addr #{} in {} tracking wallet'.format(del_addrs[0],g.coin)),
+
+		('remove_addr1','removing addr #{} from {} tracking wallet'.format(del_addrs[0],g.coin)),
+		('remove_addr2','removing addr #{} from {} tracking wallet'.format(del_addrs[1],g.coin)),
+		('remove_token_addr1','removing addr #{} from {} token tracking wallet'.format(del_addrs[0],g.coin)),
+		('remove_token_addr2','removing addr #{} from {} token tracking wallet'.format(del_addrs[1],g.coin)),
+
+		('stop',                'stopping parity'),
+	)
+
+	@property
+	def eth_args(self):
+		return ['--outdir={}'.format(self.tmpdir),'--coin='+g.coin,'--rpc-port=8549','--quiet']
+
+	def setup(self):
+		self.spawn('',msg_only=True)
+		os.environ['MMGEN_BOGUS_WALLET_DATA'] = ''
+		if subprocess.call(['which','parity'],stdout=subprocess.PIPE) == 0:
+			lf_arg = '--log-file=' + joinpath(self.tr.data_dir,'parity.log')
+			ss = 'parity.*--log-file=test/data_dir.*/parity.log' # allow for UTF8_DEBUG
+			try:
+				pid = subprocess.check_output(['pgrep','-af',ss]).split()[0]
+				os.kill(int(pid),9)
+			except: pass
+			# '--base-path' doesn't work together with daemon mode, so we have to clobber the main dev chain
+			dc_dir = joinpath(os.environ['HOME'],'.local/share/io.parity.ethereum/chains/DevelopmentChain')
+			shutil.rmtree(dc_dir,ignore_errors=True)
+			bdir = joinpath(self.tr.data_dir,'parity')
+			try: os.mkdir(bdir)
+			except: pass
+			opts = ['--ports-shift=4','--config=dev']
+			redir = None if opt.exact_output else subprocess.PIPE
+			pidfile = joinpath(self.tmpdir,parity_pid_fn)
+			subprocess.check_call(['parity',lf_arg] + opts + ['daemon',pidfile],stderr=redir,stdout=redir)
+			time.sleep(3) # race condition
+			pid = self.read_from_tmpfile(parity_pid_fn)
+		elif subprocess.call('netstat -tnl | grep -q 127.0.0.1:8549',shell=True) == 0:
+			m1 = 'No parity executable found on system, but port 8549 is active!'
+			m2 = 'Before continuing, you should probably run the command'
+			m3 = 'test/test.py -X setup ethdev'
+			m4 = 'on the remote host.'
+			sys.stderr.write('{}\n{}\n{} {}\n'.format(m1,m2,cyan(m3),m4))
+			confirm_continue()
+		else:
+			die(1,'No parity executable found!')
+		return 'ok'
+
+	def addrgen(self,addrs='1-3,11-13,21-23'):
+		from mmgen.addr import MMGenAddrType
+		t = self.spawn('mmgen-addrgen', self.eth_args + [dfl_words_file,addrs])
+		t.written_to_file('Addresses')
+		t.read()
+		return t
+
+	def addrimport(self,ext='21-23]{}.addrs',expect='9/9',add_args=[],bad_input=False):
+		ext = ext.format('-α' if g.debug_utf8 else '')
+		fn = self.get_file_with_ext(ext,no_dot=True,delete=False)
+		t = self.spawn('mmgen-addrimport', self.eth_args[1:] + add_args + [fn])
+		if bad_input:
+			t.read()
+			t.req_exit_val = 2
+			return t
+		if g.debug: t.expect("Type uppercase 'YES' to confirm: ",'YES\n')
+		t.expect('Importing')
+		t.expect(expect)
+		t.read()
+		return t
+
+	def addrimport_one_addr(self,addr=None,extra_args=[]):
+		t = self.spawn('mmgen-addrimport', self.eth_args[1:] + extra_args + ['--address='+addr])
+		t.expect('OK')
+		return t
+
+	def addrimport_dev_addr(self):
+		return self.addrimport_one_addr(addr=dfl_addr)
+
+	def addrimport_burn_addr(self):
+		return self.addrimport_one_addr(addr=burn_addr)
+
+	def txcreate(self,args=[],menu=[],acct='1',non_mmgen_inputs=0,
+						interactive_fee = '50G',
+						eth_fee_res     = None,
+						fee_res_fs      = '0.00105 {} (50 gas price in Gwei)',
+						fee_desc        = 'gas price' ):
+		fee_res = fee_res_fs.format(g.coin)
+		t = self.spawn('mmgen-txcreate', self.eth_args + ['-B'] + args)
+		t.expect(r'add \[l\]abel, .*?:.','p', regex=True)
+		t.written_to_file('Account balances listing')
+		return self.txcreate_ui_common( t,self.test_name,menu=menu,
+										input_sels_prompt = 'to spend from',
+										inputs            = acct,
+										file_desc         = 'Ethereum transaction',
+										bad_input_sels    = True,
+										non_mmgen_inputs  = non_mmgen_inputs,
+										interactive_fee   = interactive_fee,
+										fee_res           = fee_res,
+										fee_desc          = fee_desc,
+										eth_fee_res       = eth_fee_res,
+										add_comment       = ref_tx_label_jp )
+
+	def txsign(self,ni=False,ext='{}.rawtx',add_args=[]):
+		ext = ext.format('-α' if g.debug_utf8 else '')
+		keyfile = joinpath(self.tmpdir,parity_key_fn)
+		write_to_file(keyfile,dfl_privkey+'\n')
+		txfile = self.get_file_with_ext(ext,no_dot=True)
+		t = self.spawn( 'mmgen-txsign',
+						self.eth_args
+						+ add_args
+						+ ([],['--yes'])[ni]
+						+ ['-k', keyfile, txfile, dfl_words_file] )
+		return self.txsign_ui_common(t,self.test_name,ni=ni,has_label=True)
+
+	def txsend(self,ni=False,bogus_send=False,ext='{}.sigtx',add_args=[]):
+		ext = ext.format('-α' if g.debug_utf8 else '')
+		txfile = self.get_file_with_ext(ext,no_dot=True)
+		if not bogus_send: os.environ['MMGEN_BOGUS_SEND'] = ''
+		t = self.spawn('mmgen-txsend', self.eth_args + add_args + [txfile])
+		if not bogus_send: os.environ['MMGEN_BOGUS_SEND'] = '1'
+		txid = self.txsend_ui_common(t,self.test_name,quiet=True,bogus_send=bogus_send,has_label=True)
+		return t
+
+	def txcreate1(self):
+		# valid_keypresses = 'adrMmeqpvwl'
+		menu = ['a','d','r','M','D','e','m','m'] # include one invalid keypress, 'D'
+		args = ['98831F3A:E:1,123.456']
+		return self.txcreate(args=args,menu=menu,acct='1',non_mmgen_inputs=1)
+
+	def txsign1(self):    return self.txsign()
+	def txsign1_ni(self): return self.txsign(ni=True)
+	def txsend1(self):    return self.txsend()
+	def bal1(self):       return self.bal(n='1')
+
+	def txcreate2(self):
+		args = ['98831F3A:E:11,1.234']
+		return self.txcreate(args=args,acct='10',non_mmgen_inputs=1)
+	def txsign2(self): return self.txsign(ni=True,ext='1.234,50000]{}.rawtx')
+	def txsend2(self): return self.txsend(ext='1.234,50000]{}.sigtx')
+	def bal2(self):    return self.bal(n='2')
+
+	def txcreate3(self):
+		args = ['98831F3A:E:21,2.345']
+		return self.txcreate(args=args,acct='10',non_mmgen_inputs=1)
+	def txsign3(self): return self.txsign(ni=True,ext='2.345,50000]{}.rawtx')
+	def txsend3(self): return self.txsend(ext='2.345,50000]{}.sigtx')
+	def bal3(self):    return self.bal(n='3')
+
+	def tx_status(self,ext,expect_str):
+		ext = ext.format('-α' if g.debug_utf8 else '')
+		txfile = self.get_file_with_ext(ext,no_dot=True)
+		t = self.spawn('mmgen-txsend', self.eth_args + ['--status',txfile])
+		t.expect(expect_str)
+		t.read()
+		return t
+
+	def tx_status1(self):
+		return self.tx_status(ext='2.345,50000]{}.sigtx',expect_str='has 1 confirmation')
+
+	def txcreate4(self):
+		args = ['98831F3A:E:2,23.45495']
+		interactive_fee='40G'
+		fee_res_fs='0.00084 {} (40 gas price in Gwei)'
+		return self.txcreate(   args             = args,
+								acct             = '1',
+								non_mmgen_inputs = 0,
+								interactive_fee  = interactive_fee,
+								fee_res_fs       = fee_res_fs,
+								eth_fee_res      = True)
+
+	def txbump(self,ext=',40000]{}.rawtx',fee='50G',add_args=[]):
+		ext = ext.format('-α' if g.debug_utf8 else '')
+		txfile = self.get_file_with_ext(ext,no_dot=True)
+		t = self.spawn('mmgen-txbump', self.eth_args + add_args + ['--yes',txfile])
+		t.expect('or gas price: ',fee+'\n')
+		t.read()
+		return t
+
+	def txsign4(self): return self.txsign(ni=True,ext='.45495,50000]{}.rawtx')
+	def txsend4(self): return self.txsend(ext='.45495,50000]{}.sigtx')
+	def bal4(self):    return self.bal(n='4')
+
+	def txcreate5(self):
+		args = [burn_addr + ','+amt1]
+		return self.txcreate(args=args,acct='10',non_mmgen_inputs=1)
+	def txsign5(self): return self.txsign(ni=True,ext=amt1+',50000]{}.rawtx')
+	def txsend5(self): return self.txsend(ext=amt1+',50000]{}.sigtx')
+	def bal5(self):    return self.bal(n='5')
+
+	bal_corr = Decimal('0.0000032') # gas use for token sends varies between ETH and ETC!
+	def bal(self,n=None):
+		t = self.spawn('mmgen-tool', self.eth_args + ['twview','wide=1'])
+		for b in bals[n]:
+			addr,amt,adj = b if len(b) == 3 else b + (False,)
+			if adj and g.coin == 'ETC': amt = str(Decimal(amt) + self.bal_corr)
+			pat = r'{}\s+{}\s'.format(addr,amt.replace('.',r'\.'))
+			t.expect(pat,regex=True)
+		t.read()
+		return t
+
+	def token_bal(self,n=None):
+		t = self.spawn('mmgen-tool', self.eth_args + ['--token=mm1','twview','wide=1'])
+		for b in token_bals[n]:
+			addr,_amt1,_amt2,adj = b if len(b) == 4 else b + (False,)
+			if adj and g.coin == 'ETC': _amt2 = str(Decimal(_amt2) + self.bal_corr)
+			pat = r'{}\s+{}\s+{}\s'.format(addr,_amt1.replace('.',r'\.'),_amt2.replace('.',r'\.'))
+			t.expect(pat,regex=True)
+		t.read()
+		return t
+
+	def bal_getbalance(self,idx,etc_adj=False,extra_args=[]):
+		bal1 = token_bals_getbalance[idx][0]
+		bal2 = token_bals_getbalance[idx][1]
+		bal1 = Decimal(bal1)
+		if etc_adj and g.coin == 'ETC': bal1 += self.bal_corr
+		t = self.spawn('mmgen-tool', self.eth_args + extra_args + ['getbalance'])
+		t.expect(r'\n[0-9A-F]{8}: .* '+str(bal1),regex=True)
+		t.expect(r'\nNon-MMGen: .* '+bal2,regex=True)
+		total = t.expect_getend(r'\nTOTAL:\s+',regex=True).split()[0]
+		t.read()
+		assert Decimal(bal1) + Decimal(bal2) == Decimal(total)
+		return t
+
+	def add_label(self,addr='98831F3A:E:3',lbl=utf8_label):
+		t = self.spawn('mmgen-tool', self.eth_args + ['add_label',addr,lbl])
+		t.expect('Added label.*in tracking wallet',regex=True)
+		return t
+
+	def chk_label(self,addr='98831F3A:E:3',label_pat=utf8_label_pat):
+		t = self.spawn('mmgen-tool', self.eth_args + ['listaddresses','all_labels=1'])
+		t.expect(r'{}\s+\S{{30}}\S+\s+{}\s+'.format(addr,(label_pat or label)),regex=True)
+		return t
+
+	def remove_label(self,addr='98831F3A:E:3'):
+		t = self.spawn('mmgen-tool', self.eth_args + ['remove_label',addr])
+		t.expect('Removed label.*in tracking wallet',regex=True)
+		return t
+
+	def token_compile(self,token_data={}):
+		self.spawn('',msg_only=True)
+		cmd_args = ['--{}={}'.format(k,v) for k,v in list(token_data.items())]
+		imsg("Compiling solidity token contract '{}' with 'solc'".format(token_data['symbol']))
+		cmd = ['scripts/create-token.py','--coin='+g.coin,'--outdir='+self.tmpdir] + cmd_args + [dfl_addr_chk]
+		imsg("Executing: {}".format(' '.join(cmd)))
+		subprocess.check_output(cmd,stderr=subprocess.STDOUT)
+		imsg("ERC20 token '{}' compiled".format(token_data['symbol']))
+		return 'ok'
+
+	def token_compile1(self):
+		token_data = { 'name':'MMGen Token 1', 'symbol':'MM1', 'supply':10**26, 'decimals':18 }
+		return self.token_compile(token_data)
+
+	def token_compile2(self):
+		token_data = { 'name':'MMGen Token 2', 'symbol':'MM2', 'supply':10**18, 'decimals':10 }
+		return self.token_compile(token_data)
+
+	def _rpc_init(self):
+		g.proto.rpc_port = 8549
+		rpc_init()
+
+	def token_deploy(self,num,key,gas,mmgen_cmd='txdo',tx_fee='8G'):
+		self._rpc_init()
+		keyfile = joinpath(self.tmpdir,parity_key_fn)
+		fn = joinpath(self.tmpdir,key+'.bin')
+		os.environ['MMGEN_BOGUS_SEND'] = ''
+		args = ['-B',
+				'--tx-fee='+tx_fee,
+				'--tx-gas={}'.format(gas),
+				'--contract-data='+fn,
+				'--inputs='+dfl_addr,
+				'--yes' ]
+		if mmgen_cmd == 'txdo': args += ['-k',keyfile]
+		t = self.spawn( 'mmgen-'+mmgen_cmd, self.eth_args + args)
+		if mmgen_cmd == 'txcreate':
+			t.written_to_file('Ethereum transaction')
+			ext = '[0,8000]{}.rawtx'.format('-α' if g.debug_utf8 else '')
+			txfile = self.get_file_with_ext(ext,no_dot=True)
+			t = self.spawn('mmgen-txsign', self.eth_args + ['--yes','-k',keyfile,txfile],no_msg=True)
+			self.txsign_ui_common(t,self.test_name,ni=True)
+			txfile = txfile.replace('.rawtx','.sigtx')
+			t = self.spawn('mmgen-txsend', self.eth_args + [txfile],no_msg=True)
+
+		os.environ['MMGEN_BOGUS_SEND'] = '1'
+		txid = self.txsend_ui_common(t,mmgen_cmd,quiet=True,bogus_send=False)
+		addr = t.expect_getend('Contract address: ')
+		from mmgen.altcoins.eth.tx import EthereumMMGenTX as etx
+		assert etx.get_exec_status(txid.encode(),True) != 0,(
+			"Contract '{}:{}' failed to execute. Aborting".format(num,key))
+		if key == 'Token':
+			self.write_to_tmpfile('token_addr{}'.format(num),addr+'\n')
+			imsg('\nToken #{} ({}) deployed!'.format(num,addr))
+		return t
+
+	def token_deploy1a(self): return self.token_deploy(num=1,key='SafeMath',gas=200000)
+	def token_deploy1b(self): return self.token_deploy(num=1,key='Owned',gas=250000)
+	def token_deploy1c(self): return self.token_deploy(num=1,key='Token',gas=1100000,tx_fee='7G')
+
+	def tx_status2(self):
+		return self.tx_status(ext=g.coin+'[0,7000]{}.sigtx',expect_str='successfully executed')
+
+	def bal6(self): return self.bal5()
+
+	def token_deploy2a(self): return self.token_deploy(num=2,key='SafeMath',gas=200000)
+	def token_deploy2b(self): return self.token_deploy(num=2,key='Owned',gas=250000)
+	def token_deploy2c(self): return self.token_deploy(num=2,key='Token',gas=1100000)
+
+	def contract_deploy(self): # test create,sign,send
+		return self.token_deploy(num=2,key='SafeMath',gas=1100000,mmgen_cmd='txcreate')
+
+	def token_transfer_ops(self,op,amt=1000):
+		self.spawn('',msg_only=True)
+		sid = dfl_sid
+		from mmgen.tool import MMGenToolCmd
+		usr_mmaddrs = ['{}:E:{}'.format(sid,i) for i in (11,21)]
+		usr_addrs = [MMGenToolCmd().gen_addr(addr,dfl_words_file) for addr in usr_mmaddrs]
+		self._rpc_init()
+
+		from mmgen.altcoins.eth.contract import Token
+		from mmgen.altcoins.eth.tx import EthereumMMGenTX as etx
+		def do_transfer():
+			for i in range(2):
+				tk = Token(self.read_from_tmpfile('token_addr{}'.format(i+1)).strip())
+				imsg_r('\n'+tk.info())
+				imsg('dev token balance (pre-send): {}'.format(tk.balance(dfl_addr)))
+				imsg('Sending {} {} to address {} ({})'.format(amt,g.coin,usr_addrs[i],usr_mmaddrs[i]))
+				from mmgen.obj import ETHAmt
+				txid = tk.transfer( dfl_addr, usr_addrs[i], amt, dfl_privkey,
+									start_gas = ETHAmt(60000,'wei'),
+									gasPrice  = ETHAmt(8,'Gwei') )
+				assert etx.get_exec_status(txid,True) != 0,'Transfer of token funds failed. Aborting'
+
+		def show_bals():
+			for i in range(2):
+				tk = Token(self.read_from_tmpfile('token_addr{}'.format(i+1)).strip())
+				imsg('Token: {}'.format(tk.symbol()))
+				imsg('dev token balance: {}'.format(tk.balance(dfl_addr)))
+				imsg('usr token balance: {} ({} {})'.format(
+						tk.balance(usr_addrs[i]),usr_mmaddrs[i],usr_addrs[i]))
+
+		silence()
+		if op == 'show_bals': show_bals()
+		elif op == 'do_transfer': do_transfer()
+		end_silence()
+		return 'ok'
+
+	def token_fund_users(self):
+		return self.token_transfer_ops(op='do_transfer')
+
+	def token_user_bals(self):
+		return self.token_transfer_ops(op='show_bals')
+
+	def token_addrgen(self):
+		self.addrgen(addrs='11-13')
+		ok_msg()
+		return self.addrgen(addrs='21-23')
+
+	def token_addrimport_badaddr1(self):
+		t = self.addrimport(ext='[11-13]{}.addrs',add_args=['--token=abc'],bad_input=True)
+		t.req_exit_val = 2
+		return t
+
+	def token_addrimport_badaddr2(self):
+		t = self.addrimport(ext='[11-13]{}.addrs',add_args=['--token='+'00deadbeef'*4],bad_input=True)
+		t.req_exit_val = 2
+		return t
+
+	def token_addrimport(self):
+		for n,r in ('1','11-13'),('2','21-23'):
+			tk_addr = self.read_from_tmpfile('token_addr'+n).strip()
+			t = self.addrimport(ext='['+r+']{}.addrs',expect='3/3',add_args=['--token='+tk_addr])
+			t.p.wait()
+			ok_msg()
+		t.skip_ok = True
+		return t
+
+	def bal7(self):       return self.bal5()
+	def token_bal1(self): return self.token_bal(n='1')
+
+	def token_txcreate(self,args=[],token='',inputs='1',fee='50G'):
+		t = self.spawn('mmgen-txcreate', self.eth_args + ['--token='+token,'-B','--tx-fee='+fee] + args)
+		return self.txcreate_ui_common( t,self.test_name,
+										menu              = [],
+										inputs            = inputs,
+										input_sels_prompt = 'to spend from',
+										file_desc         = 'Ethereum token transaction',
+										add_comment       = ref_tx_label_lat_cyr_gr)
+	def token_txsign(self,ext='',token=''):
+		return self.txsign(ni=True,ext=ext,add_args=['--token='+token])
+	def token_txsend(self,ext='',token=''):
+		return self.txsend(ext=ext,add_args=['--token=mm1'])
+
+	def token_txcreate1(self):
+		return self.token_txcreate(args=['98831F3A:E:12,1.23456'],token='mm1')
+	def token_txsign1(self):
+		return self.token_txsign(ext='1.23456,50000]{}.rawtx',token='mm1')
+	def token_txsend1(self):
+		return self.token_txsend(ext='1.23456,50000]{}.sigtx',token='mm1')
+	def token_bal2(self):
+		return self.token_bal(n='2')
+
+	def twview(self,args=[],expect_str='',tool_args=[],exit_val=0):
+		t = self.spawn('mmgen-tool', self.eth_args + args + ['twview'] + tool_args)
+		if expect_str:
+			t.expect(expect_str,regex=True)
+		t.read()
+		t.req_exit_val = exit_val
+		return t
+
+	def token_txcreate2(self):
+		return self.token_txcreate(args=[burn_addr+','+amt2],token='mm1')
+	def token_txbump(self):
+		return self.txbump(ext=amt2+',50000]{}.rawtx',fee='56G',add_args=['--token=mm1'])
+	def token_txsign2(self):
+		return self.token_txsign(ext=amt2+',50000]{}.rawtx',token='mm1')
+	def token_txsend2(self):
+		return self.token_txsend(ext=amt2+',50000]{}.sigtx',token='mm1')
+
+	def token_bal3(self):
+		return self.token_bal(n='3')
+
+	def del_dev_addr(self):
+		t = self.spawn('mmgen-tool', self.eth_args + ['remove_address',dfl_addr])
+		t.read() # TODO
+		return t
+
+	def bal1_getbalance(self):
+		return self.bal_getbalance('1',etc_adj=True)
+
+	def addrimport_token_burn_addr(self):
+		return self.addrimport_one_addr(addr=burn_addr,extra_args=['--token=mm1'])
+
+	def token_bal4(self):
+		return self.token_bal(n='4')
+
+	def token_bal_getbalance(self):
+		return self.bal_getbalance('2',extra_args=['--token=mm1'])
+
+	def txcreate_noamt(self):
+		return self.txcreate(args=['98831F3A:E:12'],eth_fee_res=True)
+	def txsign_noamt(self):
+		return self.txsign(ext='99.99895,50000]{}.rawtx')
+	def txsend_noamt(self):
+		return self.txsend(ext='99.99895,50000]{}.sigtx')
+
+	def bal8(self):       return self.bal(n='8')
+	def token_bal5(self): return self.token_bal(n='5')
+
+	def token_txcreate_noamt(self):
+		return self.token_txcreate(args=['98831F3A:E:13'],token='mm1',inputs='2',fee='51G')
+	def token_txsign_noamt(self):
+		return self.token_txsign(ext='1.23456,51000]{}.rawtx',token='mm1')
+	def token_txsend_noamt(self):
+		return self.token_txsend(ext='1.23456,51000]{}.sigtx',token='mm1')
+
+	def bal9(self):       return self.bal(n='9')
+	def token_bal6(self): return self.token_bal(n='6')
+
+	def listaddresses(self,args=[],tool_args=['all_labels=1'],exit_val=0):
+		t = self.spawn('mmgen-tool', self.eth_args + args + ['listaddresses'] + tool_args)
+		t.read()
+		t.req_exit_val = exit_val
+		return t
+
+	def listaddresses1(self):
+		return self.listaddresses()
+	def listaddresses2(self):
+		return self.listaddresses(tool_args=['minconf=999999999'])
+	def listaddresses3(self):
+		return self.listaddresses(tool_args=['sort=age'])
+	def listaddresses4(self):
+		return self.listaddresses(tool_args=['sort=age','showempty=1'])
+
+	def token_listaddresses1(self):
+		return self.listaddresses(args=['--token=mm1'])
+	def token_listaddresses2(self):
+		return self.listaddresses(args=['--token=mm1'],tool_args=['showempty=1'])
+
+	def twview1(self):
+		return self.twview()
+	def twview2(self):
+		return self.twview(tool_args=['wide=1'])
+	def twview3(self):
+		return self.twview(tool_args=['wide=1','sort=age'])
+	def twview4(self):
+		return self.twview(tool_args=['wide=1','minconf=999999999'])
+	def twview5(self):
+		return self.twview(tool_args=['wide=1','minconf=0'])
+	def twview6(self):
+		return self.twview(tool_args=['age_fmt=days'])
+
+	def token_twview1(self):
+		return self.twview(args=['--token=mm1'])
+	def token_twview2(self):
+		return self.twview(args=['--token=mm1'],tool_args=['wide=1'])
+	def token_twview3(self):
+		return self.twview(args=['--token=mm1'],tool_args=['wide=1','sort=age'])
+
+	def edit_label(self,out_num,args=[],action='l',label_text=None):
+		t = self.spawn('mmgen-txcreate', self.eth_args + args + ['-B','-i'])
+		p1,p2 = ('emove address:\b','return to main menu): ')
+		p3,r3 = (p2,label_text+'\n') if label_text is not None else ('(y/N): ','y')
+		p4,r4 = (('(y/N): ',),('y',)) if label_text == '' else ((),())
+		for p,r in zip((p1,p1,p2,p3)+p4+(p1,p1),('M',action,out_num+'\n',r3)+r4+('M','q')):
+			t.expect(p,r)
+		return t
+
+	def edit_label1(self):
+		return self.edit_label(out_num=del_addrs[0],label_text='First added label-α')
+	def edit_label2(self):
+		return self.edit_label(out_num=del_addrs[1],label_text='Second added label')
+	def edit_label3(self):
+		return self.edit_label(out_num=del_addrs[0],label_text='')
+
+	def remove_addr1(self):
+		return self.edit_label(out_num=del_addrs[0],action='R')
+	def remove_addr2(self):
+		return self.edit_label(out_num=del_addrs[1],action='R')
+	def remove_token_addr1(self):
+		return self.edit_label(out_num=del_addrs[0],args=['--token=mm1'],action='R')
+	def remove_token_addr2(self):
+		return self.edit_label(out_num=del_addrs[1],args=['--token=mm1'],action='R')
+
+	def stop(self):
+		self.spawn('',msg_only=True)
+		if subprocess.call(['which','parity'],stdout=subprocess.PIPE) == 0:
+			pid = self.read_from_tmpfile(parity_pid_fn)
+			if opt.no_daemon_stop:
+				msg_r('(leaving daemon running by user request)')
+			else:
+				subprocess.check_call(['kill',pid])
+		else:
+			imsg('No parity executable found on system. Ignoring')
+		return 'ok'

+ 704 - 0
test/test_py_d/ts_main.py

@@ -0,0 +1,704 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
+# Copyright (C)2013-2019 The MMGen Project <mmgen@tuta.io>
+#
+# 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/>.
+
+"""
+ts_main.py: Basic operations tests for the test.py test suite
+"""
+
+from mmgen.globalvars import g
+from mmgen.opts import opt
+from test.common import *
+from test.test_py_d.common import *
+from test.test_py_d.ts_base import *
+from test.test_py_d.ts_shared import *
+
+def make_brainwallet_file(fn):
+	# Print random words with random whitespace in between
+	wl = rwords.split()
+	nwords,ws_list,max_spaces = 10,'    \n',5
+	def rand_ws_seq():
+		nchars = getrandnum(1) % max_spaces + 1
+		return ''.join([ws_list[getrandnum_range(1,200) % len(ws_list)] for i in range(nchars)])
+	rand_pairs = [wl[getrandnum_range(1,200) % len(wl)] + rand_ws_seq() for i in range(nwords)]
+	d = ''.join(rand_pairs).rstrip() + '\n'
+	if opt.verbose: msg_r('Brainwallet password:\n{}'.format(cyan(d)))
+	write_data_to_file(fn,d,'brainwallet password',silent=True,ignore_opt_outdir=True)
+
+def verify_checksum_or_exit(checksum,chk):
+	if checksum != chk:
+		raise TestSuiteFatalException('Checksum error: {}'.format(chk))
+	vmsg(green('Checksums match: ') + cyan(chk))
+
+addrs_per_wallet = 8
+
+# 100 words chosen randomly from here:
+#   https://github.com/bitcoin/bips/pull/432/files/6332230d63149a950d05db78964a03bfd344e6b0
+rwords = """
+	алфавит алый амнезия амфора артист баян белый биатлон брат бульвар веревка вернуть весть возраст
+	восток горло горный десяток дятел ежевика жест жизнь жрать заговор здание зона изделие итог кабина
+	кавалер каждый канал керосин класс клятва князь кривой крыша крючок кузнец кукла ландшафт мальчик
+	масса масштаб матрос мрак муравей мычать негодяй носок ночной нрав оборот оружие открытие оттенок
+	палуба пароход период пехота печать письмо позор полтора понятие поцелуй почему приступ пруд пятно
+	ранее режим речь роса рынок рябой седой сердце сквозь смех снимок сойти соперник спичка стон
+	сувенир сугроб суть сцена театр тираж толк удивить улыбка фирма читатель эстония эстрада юность
+	"""
+
+class TestSuiteMain(TestSuiteBase,TestSuiteShared):
+	'basic operations with emulated tracking wallet'
+	tmpdir_nums = [1,2,3,4,5,14,15,16,20,21]
+	networks = ('btc','btc_tn','ltc','ltc_tn','bch','bch_tn')
+	passthru_opts = ('coin','testnet')
+	segwit_opts_ok = True
+	cmd_group = (
+		('walletgen_dfl_wallet', (15,'wallet generation (default wallet)',[[[],15]])),
+		('export_seed_dfl_wallet',(15,'seed export to mmseed format (default wallet)',[[[pwfile],15]])),
+		('addrgen_dfl_wallet',(15,'address generation (default wallet)',[[[pwfile],15]])),
+		('txcreate_dfl_wallet',(15,'transaction creation (default wallet)',[[['addrs'],15]])),
+		('txsign_dfl_wallet',(15,'transaction signing (default wallet)',[[['rawtx',pwfile],15]])),
+		('passchg_dfl_wallet',(16,'password, label and hash preset change (default wallet)',[[[pwfile],15]])),
+		('walletchk_newpass_dfl_wallet',(16,'wallet check with new pw, label and hash preset',[[[pwfile],16]])),
+		('delete_dfl_wallet',(15,'delete default wallet',[[[pwfile],15]])),
+
+		('walletgen',       (1,'wallet generation',        [[['del_dw_run'],15]])),
+#		('walletchk',       (1,'wallet check',             [[['mmdat'],1]])),
+		('passchg',         (5,'password, label and hash preset change',[[['mmdat',pwfile],1]])),
+		('passchg_keeplabel',(5,'password, label and hash preset change (keep label)',[[['mmdat',pwfile],1]])),
+		('passchg_usrlabel',(5,'password, label and hash preset change (interactive label)',[[['mmdat',pwfile],1]])),
+		('walletchk_newpass',(5,'wallet check with new pw, label and hash preset',[[['mmdat',pwfile],5]])),
+		('addrgen',         (1,'address generation',       [[['mmdat',pwfile],1]])),
+		('txcreate',        (1,'transaction creation',     [[['addrs'],1]])),
+		('txbump',          (1,'transaction fee bumping (no send)',[[['rawtx'],1]])),
+		('txsign',          (1,'transaction signing',      [[['mmdat','rawtx',pwfile,'txbump'],1]])),
+		('txsend',          (1,'transaction sending',      [[['sigtx'],1]])),
+		# txdo must go after txsign
+		('txdo',            (1,'online transaction',       [[['sigtx','mmdat'],1]])),
+
+		('export_seed',     (1,'seed export to mmseed format',   [[['mmdat'],1]])),
+		('export_hex',      (1,'seed export to hexadecimal format',  [[['mmdat'],1]])),
+		('export_mnemonic', (1,'seed export to mmwords format',  [[['mmdat'],1]])),
+		('export_incog',    (1,'seed export to mmincog format',  [[['mmdat'],1]])),
+		('export_incog_hex',(1,'seed export to mmincog hex format', [[['mmdat'],1]])),
+		('export_incog_hidden',(1,'seed export to hidden mmincog format', [[['mmdat'],1]])),
+
+		('addrgen_seed',    (1,'address generation from mmseed file', [[['mmseed','addrs'],1]])),
+		('addrgen_hex',     (1,'address generation from mmhex file', [[['mmhex','addrs'],1]])),
+		('addrgen_mnemonic',(1,'address generation from mmwords file',[[['mmwords','addrs'],1]])),
+		('addrgen_incog',   (1,'address generation from mmincog file',[[['mmincog','addrs'],1]])),
+		('addrgen_incog_hex',(1,'address generation from mmincog hex file',[[['mmincox','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]])),
+		('txsign_keyaddr',(1,'transaction signing with key-address file', [[['akeys.mmenc','rawtx'],1]])),
+
+		('txcreate_ni',   (1,'transaction creation (non-interactive)',     [[['addrs'],1]])),
+
+		('walletgen2',(2,'wallet generation (2), 128-bit seed',     [[['del_dw_run'],15]])),
+		('addrgen2',  (2,'address generation (2)',    [[['mmdat'],2]])),
+		('txcreate2', (2,'transaction creation (2)',  [[['addrs'],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)',                  [[['del_dw_run'],15]])),
+		('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','rawtx'],3]])),
+
+		('walletgen14', (14,'wallet generation (14)',        [[['del_dw_run'],15]],14)),
+		('addrgen14',   (14,'address generation (14)',        [[['mmdat'],14]])),
+		('keyaddrgen14',(14,'key-address file generation (14)', [[['mmdat'],14]],14)),
+		('walletgen4',(4,'wallet generation (4) (brainwallet)',    [[['del_dw_run'],15]])),
+		('addrgen4',  (4,'address generation (4)',                 [[['mmdat'],4]])),
+		('txcreate4', (4,'tx creation with inputs and outputs from four seed sources, key-address file and non-MMGen inputs and outputs', [[['addrs'],1],[['addrs'],2],[['addrs'],3],[['addrs'],4],[['addrs','akeys.mmenc'],14]])),
+		('txsign4',   (4,'tx signing with inputs and outputs from incog file, mnemonic file, wallet, brainwallet, key-address file and non-MMGen inputs and outputs', [[['mmincog'],1],[['mmwords'],2],[['mmdat'],3],[['mmbrain','rawtx'],4],[['akeys.mmenc'],14]])),
+		('txdo4', (4,'tx creation,signing and sending with inputs and outputs from four seed sources, key-address file and non-MMGen inputs and outputs', [[['addrs'],1],[['addrs'],2],[['addrs'],3],[['addrs'],4],[['addrs','akeys.mmenc'],14],[['mmincog'],1],[['mmwords'],2],[['mmdat'],3],[['mmbrain','rawtx'],4],[['akeys.mmenc'],14]])), # must go after txsign4
+		('txbump4', (4,'tx fee bump + send with inputs and outputs from four seed sources, key-address file and non-MMGen inputs and outputs', [[['akeys.mmenc'],14],[['mmincog'],1],[['mmwords'],2],[['mmdat'],3],[['akeys.mmenc'],14],[['mmbrain','sigtx','mmdat','txdo'],4]])), # must go after txsign4
+
+		('walletgen5',(20,'wallet generation (5)',                   [[['del_dw_run'],15]],20)),
+		('addrgen5',  (20,'address generation (5)',                  [[['mmdat'],20]])),
+		('txcreate5', (20,'transaction creation with bad vsize (5)', [[['addrs'],20]])),
+		('txsign5',   (20,'transaction signing with bad vsize',      [[['mmdat','rawtx'],20]])),
+		('walletgen6',(21,'wallet generation (6)',                   [[['del_dw_run'],15]],21)),
+		('addrgen6',  (21,'address generation (6)',                  [[['mmdat'],21]])),
+		('txcreate6', (21,'transaction creation with corrected vsize (6)', [[['addrs'],21]])),
+		('txsign6',   (21,'transaction signing with corrected vsize',      [[['mmdat','rawtx'],21]])),
+	)
+
+	def __init__(self,trunner,cfgs,spawn):
+		self.lbl_id = ('account','label')[g.coin=='BTC'] # update as other coins adopt Core's label API
+		if g.coin in ('BTC','BCH','LTC'):
+			self.tx_fee     = {'btc':'0.0001','bch':'0.001','ltc':'0.01'}[g.coin.lower()]
+			self.txbump_fee = {'btc':'123s','bch':'567s','ltc':'12345s'}[g.coin.lower()]
+		return TestSuiteBase.__init__(self,trunner,cfgs,spawn)
+
+	def _get_addrfile_checksum(self,display=False):
+		addrfile = self.get_file_with_ext('addrs')
+		silence()
+		from mmgen.addr import AddrList
+		chk = AddrList(addrfile).chksum
+		if opt.verbose and display: msg('Checksum: {}'.format(cyan(chk)))
+		end_silence()
+		return chk
+
+	def walletgen_dfl_wallet(self,seed_len=None):
+		return self.walletgen(seed_len=seed_len,gen_dfl_wallet=True)
+
+	def export_seed_dfl_wallet(self,pf,desc='seed data',out_fmt='seed'):
+		return self.export_seed(wf=None,desc=desc,out_fmt=out_fmt,pf=pf)
+
+	def addrgen_dfl_wallet(self,pf=None,check_ref=False):
+		return self.addrgen(wf=None,pf=pf,check_ref=check_ref)
+
+	def txcreate_dfl_wallet(self,addrfile):
+		return self.txcreate_common(sources=['15'])
+
+	def txsign_dfl_wallet(self,txfile,pf='',save=True,has_label=False):
+		return self.txsign(txfile,wf=None,pf=pf,save=save,has_label=has_label)
+
+	def passchg_dfl_wallet(self,pf):
+		return self.passchg(wf=None,pf=pf)
+
+	def walletchk_newpass_dfl_wallet(self,pf):
+		return self.walletchk_newpass(wf=None,pf=pf)
+
+	def delete_dfl_wallet(self,pf):
+		self.write_to_tmpfile('del_dw_run',b'',binary=True)
+		if opt.no_dw_delete: return 'skip'
+		for wf in [f for f in os.listdir(g.data_dir) if f[-6:]=='.mmdat']:
+			os.unlink(joinpath(g.data_dir,wf))
+		self.spawn('',msg_only=True)
+		self.have_dfl_wallet = False
+		return 'ok'
+
+	def walletgen(self,del_dw_run='dummy',seed_len=None,gen_dfl_wallet=False):
+		self.write_to_tmpfile(pwfile,self.wpasswd+'\n')
+		args = ['-p1']
+		if not gen_dfl_wallet: args += ['-d',self.tmpdir]
+		if seed_len: args += ['-l',str(seed_len)]
+		t = self.spawn('mmgen-walletgen', args + [self.usr_rand_arg])
+		t.license()
+		t.usr_rand(self.usr_rand_chars)
+		t.expect('Generating')
+		t.passphrase_new('new MMGen wallet',self.wpasswd)
+		t.label()
+		if not self.have_dfl_wallet and gen_dfl_wallet:
+			t.expect('move it to the data directory? (Y/n): ','y')
+			self.have_dfl_wallet = True
+		t.written_to_file('MMGen wallet')
+		t.req_exit_val = 0
+		return t
+
+	def passchg(self,wf,pf,label_action='cmdline'):
+		silence()
+		self.write_to_tmpfile(pwfile,get_data_from_file(pf))
+		end_silence()
+		add_args = {'cmdline': ['-d',self.tmpdir,'-L','Changed label (UTF-8) α'],
+					'keep':    ['-d',self.tr.trash_dir,'--keep-label'],
+					'user':    ['-d',self.tr.trash_dir]
+					}[label_action]
+		t = self.spawn('mmgen-passchg', add_args + [self.usr_rand_arg, '-p2'] + ([],[wf])[bool(wf)])
+		t.license()
+		t.passphrase('MMGen wallet',self.cfgs['1']['wpasswd'],pwtype='old')
+		t.expect_getend('Hash preset changed to ')
+		t.passphrase('MMGen wallet',self.wpasswd,pwtype='new') # reuse passphrase?
+		t.expect('Repeat passphrase: ',self.wpasswd+'\n')
+		t.usr_rand(self.usr_rand_chars)
+		if label_action == 'user':
+			t.expect('Enter a wallet label.*: ','Interactive Label (UTF-8) α\n',regex=True)
+		t.expect_getend(('Label changed to ','Reusing label ')[label_action=='keep'])
+#		t.expect_getend('Key ID changed: ')
+		if not wf:
+			t.expect("Type uppercase 'YES' to confirm: ",'YES\n')
+			t.written_to_file('New wallet')
+			t.expect('Securely deleting old wallet')
+#			t.expect('Okay to WIPE 1 regular file ? (Yes/No)','Yes\n')
+			t.expect('Wallet passphrase has changed')
+			t.expect_getend('has been changed to ')
+		else:
+			t.written_to_file('MMGen wallet')
+		return t
+
+	def passchg_keeplabel(self,wf,pf):
+		return self.passchg(wf,pf,label_action='keep')
+
+	def passchg_usrlabel(self,wf,pf):
+		return self.passchg(wf,pf,label_action='user')
+
+	def walletchk_newpass(self,wf,pf):
+		return self.walletchk(wf,pf,pw=True)
+
+	def _write_fake_data_to_file(self,d):
+		unspent_data_file = joinpath(self.tmpdir,'unspent.json')
+		write_data_to_file(unspent_data_file,d,'Unspent outputs',silent=True,ignore_opt_outdir=True)
+		os.environ['MMGEN_BOGUS_WALLET_DATA'] = unspent_data_file
+		bwd_msg = 'MMGEN_BOGUS_WALLET_DATA={}'.format(unspent_data_file)
+		if opt.print_cmdline: msg(bwd_msg)
+		if opt.log: self.tr.log_fd.write(bwd_msg + ' ')
+		if opt.verbose or opt.exact_output:
+			sys.stderr.write("Fake transaction wallet data written to file {!r}\n".format(unspent_data_file))
+
+	def _create_fake_unspent_entry(self,coinaddr,al_id=None,idx=None,lbl=None,non_mmgen=False,segwit=False):
+		if 'S' not in g.proto.mmtypes: segwit = False
+		if lbl: lbl = ' ' + lbl
+		k = coinaddr.addr_fmt
+		if not segwit and k == 'p2sh': k = 'p2pkh'
+		s_beg,s_end = { 'p2pkh':  ('76a914','88ac'),
+						'p2sh':   ('a914','87'),
+						'bech32': (g.proto.witness_vernum_hex.decode()+'14','') }[k]
+		amt1,amt2 = {'btc':(10,40),'bch':(10,40),'ltc':(1000,4000)}[g.coin.lower()]
+		ret = {
+			self.lbl_id: '{}:{}'.format(g.proto.base_coin.lower(),coinaddr) if non_mmgen \
+				else ('{}:{}{}'.format(al_id,idx,lbl)),
+			'vout': int(getrandnum(4) % 8),
+			'txid': hexlify(os.urandom(32)),
+			'amount': g.proto.coin_amt('{}.{}'.format(amt1 + getrandnum(4) % amt2, getrandnum(4) % 100000000)),
+			'address': coinaddr,
+			'spendable': False,
+			'scriptPubKey': '{}{}{}'.format(s_beg,coinaddr.hex.decode(),s_end).encode(),
+			'confirmations': getrandnum(3) // 2 # max: 8388608 (7 digits)
+		}
+		return ret
+
+	def _create_fake_unspent_data(self,adata,tx_data,non_mmgen_input='',non_mmgen_input_compressed=True):
+
+		out = []
+		for d in tx_data.values():
+			al = adata.addrlist(d['al_id'])
+			for n,(idx,coinaddr) in enumerate(al.addrpairs()):
+				lbl = get_label(do_shuffle=True)
+				out.append(self._create_fake_unspent_entry(coinaddr,d['al_id'],idx,lbl,segwit=d['segwit']))
+				if n == 0:  # create a duplicate address. This means addrs_per_wallet += 1
+					out.append(self._create_fake_unspent_entry(coinaddr,d['al_id'],idx,lbl,segwit=d['segwit']))
+
+		if non_mmgen_input:
+			from mmgen.obj import PrivKey
+			privkey = PrivKey(os.urandom(32),compressed=non_mmgen_input_compressed,pubkey_type='std')
+			from mmgen.addr import AddrGenerator,KeyGenerator
+			rand_coinaddr = AddrGenerator('p2pkh').to_addr(KeyGenerator('std').to_pubhex(privkey))
+			of = joinpath(self.cfgs[non_mmgen_input]['tmpdir'],non_mmgen_fn)
+			write_data_to_file(of,  privkey.wif+'\n','compressed {} key'.format(g.proto.name),
+									silent=True,ignore_opt_outdir=True)
+			out.append(self._create_fake_unspent_entry(rand_coinaddr,non_mmgen=True,segwit=False))
+
+		return out
+
+	def _create_tx_data(self,sources,addrs_per_wallet=addrs_per_wallet):
+		from mmgen.addr import AddrData,AddrList
+		from mmgen.obj import AddrIdxList
+		tx_data,ad = {},AddrData()
+		for s in sources:
+			afile = get_file_with_ext(self.cfgs[s]['tmpdir'],'addrs')
+			al = AddrList(afile)
+			ad.add(al)
+			aix = AddrIdxList(fmt_str=self.cfgs[s]['addr_idx_list'])
+			if len(aix) != addrs_per_wallet:
+				raise TestSuiteFatalException(
+					'Address index list length != {}: {}'.format(addrs_per_wallet,repr(aix)))
+			tx_data[s] = {
+				'addrfile': afile,
+				'chk': al.chksum,
+				'al_id': al.al_id,
+				'addr_idxs': aix[-2:],
+				'segwit': self.cfgs[s]['segwit']
+			}
+		return ad,tx_data
+
+	def _make_txcreate_cmdline(self,tx_data):
+		from mmgen.obj import PrivKey
+		privkey = PrivKey(os.urandom(32),compressed=True,pubkey_type='std')
+		t = ('p2pkh','segwit')['S' in g.proto.mmtypes]
+		from mmgen.addr import AddrGenerator,KeyGenerator
+		rand_coinaddr = AddrGenerator(t).to_addr(KeyGenerator('std').to_pubhex(privkey))
+
+		# total of two outputs must be < 10 BTC (<1000 LTC)
+		mods = {'btc':(6,4),'bch':(6,4),'ltc':(600,400)}[g.coin.lower()]
+		for k in self.cfgs:
+			self.cfgs[k]['amts'] = [None,None]
+			for idx,mod in enumerate(mods):
+				self.cfgs[k]['amts'][idx] = '{}.{}'.format(getrandnum(4) % mod, str(getrandnum(4))[:5])
+
+		cmd_args = ['--outdir='+self.tmpdir]
+		for num in tx_data:
+			s = tx_data[num]
+			cmd_args += [
+				'{}:{},{}'.format(s['al_id'],s['addr_idxs'][0],self.cfgs[num]['amts'][0]),
+			]
+			# + one change address and one BTC address
+			if num is list(tx_data.keys())[-1]:
+				cmd_args += ['{}:{}'.format(s['al_id'],s['addr_idxs'][1])]
+				cmd_args += ['{},{}'.format(rand_coinaddr,self.cfgs[num]['amts'][1])]
+
+		return cmd_args + [tx_data[num]['addrfile'] for num in tx_data]
+
+	def txcreate_common(self,
+						sources=['1'],
+						non_mmgen_input='',
+						do_label=False,
+						txdo_args=[],
+						add_args=[],
+						view='n',
+						addrs_per_wallet=addrs_per_wallet,
+						non_mmgen_input_compressed=True,
+						cmdline_inputs=False):
+
+		if opt.verbose or opt.exact_output:
+			sys.stderr.write(green('Generating fake tracking wallet info\n'))
+
+		silence()
+		ad,tx_data = self._create_tx_data(sources,addrs_per_wallet)
+		dfake = self._create_fake_unspent_data(ad,tx_data,non_mmgen_input,non_mmgen_input_compressed)
+		self._write_fake_data_to_file(repr(dfake))
+		cmd_args = self._make_txcreate_cmdline(tx_data)
+		if cmdline_inputs:
+			from mmgen.tx import TwLabel
+			cmd_args = ['--inputs={},{},{},{},{},{}'.format(
+				TwLabel(dfake[0][self.lbl_id]).mmid,dfake[1]['address'],
+				TwLabel(dfake[2][self.lbl_id]).mmid,dfake[3]['address'],
+				TwLabel(dfake[4][self.lbl_id]).mmid,dfake[5]['address']
+				),'--outdir='+self.tr.trash_dir] + cmd_args[1:]
+		end_silence()
+
+		if opt.verbose or opt.exact_output: sys.stderr.write('\n')
+
+		t = self.spawn(
+			'mmgen-'+('txcreate','txdo')[bool(txdo_args)],
+			([],['--rbf'])[g.proto.cap('rbf')] +
+			['-f',self.tx_fee,'-B'] + add_args + cmd_args + txdo_args)
+
+		if t.expect([('Get','Transac')[cmdline_inputs],'Unable to connect to \S+'],regex=True) == 1:
+			raise TestSuiteException('\n'+t.p.after)
+
+		if cmdline_inputs:
+			t.written_to_file('tion')
+			return t
+
+		t.license()
+
+		if txdo_args and add_args: # txdo4
+			t.do_decrypt_ka_data(hp='1',pw=self.cfgs['14']['kapasswd'])
+
+		for num in tx_data:
+			t.expect_getend('ting address data from file ')
+			chk=t.expect_getend(r'Checksum for address data .*?: ',regex=True)
+			verify_checksum_or_exit(tx_data[num]['chk'],chk)
+
+		# not in tracking wallet warning, (1 + num sources) times
+		for num in range(len(tx_data) + 1):
+			t.expect('Continue anyway? (y/N): ','y')
+
+		outputs_list = [(addrs_per_wallet+1)*i + 1 for i in range(len(tx_data))]
+		if non_mmgen_input: outputs_list.append(len(tx_data)*(addrs_per_wallet+1) + 1)
+
+		self.txcreate_ui_common(t,self.test_name,
+					menu=(['M'],['M','D','m','g'])[self.test_name=='txcreate'],
+					inputs=' '.join(map(str,outputs_list)),
+					add_comment=('',ref_tx_label_lat_cyr_gr)[do_label],
+					non_mmgen_inputs=(0,1)[bool(non_mmgen_input and not txdo_args)],
+					view=view)
+
+		return t
+
+	def txcreate(self,addrfile):
+		return self.txcreate_common(sources=['1'],add_args=['--vsize-adj=1.01'])
+
+	def txbump(self,txfile,prepend_args=[],seed_args=[]):
+		if not g.proto.cap('rbf'):
+			msg('Skipping RBF'); return 'skip'
+		args = prepend_args + ['--quiet','--outdir='+self.tmpdir,txfile] + seed_args
+		t = self.spawn('mmgen-txbump',args)
+		if seed_args:
+			t.do_decrypt_ka_data(hp='1',pw=self.cfgs['14']['kapasswd'])
+		t.expect('deduct the fee from (Hit ENTER for the change output): ','1\n')
+		# Fee must be > tx_fee + network relay fee (currently 0.00001)
+		t.expect('OK? (Y/n): ','\n')
+		t.expect('Enter transaction fee: ',self.txbump_fee+'\n')
+		t.expect('OK? (Y/n): ','\n')
+		if seed_args: # sign and send
+			t.do_comment(False,has_label=True)
+			for cnum,desc in (('1','incognito data'),('3','MMGen wallet'),('4','MMGen wallet')):
+				t.passphrase(desc,self.cfgs[cnum]['wpasswd'])
+			self._do_confirm_send(t,quiet=not g.debug,confirm_send=True)
+			if g.debug:
+				t.written_to_file('Transaction')
+		else:
+			t.do_comment(False)
+			t.expect('Save transaction? (y/N): ','y')
+			t.written_to_file('Transaction')
+		os.unlink(txfile) # our tx file replaces the original
+		cmd = 'touch ' + joinpath(self.tmpdir,'txbump')
+		os.system(cmd.encode())
+		return t
+
+	def txsend(self,sigfile,bogus_send=True,extra_opts=[]):
+		if not bogus_send: os.environ['MMGEN_BOGUS_SEND'] = ''
+		t = self.spawn('mmgen-txsend', extra_opts + ['-d',self.tmpdir,sigfile])
+		if not bogus_send: os.environ['MMGEN_BOGUS_SEND'] = '1'
+		self.txsend_ui_common(t,self.test_name,view='t',add_comment='')
+		return t
+
+	def txdo(self,addrfile,wallet):
+		t = self.txcreate_common(sources=['1'],txdo_args=[wallet])
+		self.txsign_ui_common(t,self.test_name,view='n',do_passwd=True)
+		self.txsend_ui_common(t,self.test_name)
+		return t
+
+	def _walletconv_export(self,wf,desc,uargs=[],out_fmt='w',pf=None,out_pw=False):
+		opts = ['-d',self.tmpdir,'-o',out_fmt] + uargs + \
+			([],[wf])[bool(wf)] + ([],['-P',pf])[bool(pf)]
+		t = self.spawn('mmgen-walletconv',opts)
+		t.license()
+		if not pf:
+			t.passphrase('MMGen wallet',self.wpasswd)
+		if out_pw:
+			t.passphrase_new('new '+desc,self.wpasswd)
+			t.usr_rand(self.usr_rand_chars)
+
+		if ' '.join(desc.split()[-2:]) == 'incognito data':
+			m = 'Generating encryption key from OS random data '
+			t.expect(m); t.expect(m)
+			incog_id = t.expect_getend('New Incog Wallet ID: ')
+			t.expect(m)
+		if desc == 'hidden incognito data':
+			self.write_to_tmpfile(incog_id_fn,incog_id)
+			ret = t.expect(['Create? (Y/n): ',"'YES' to confirm: "])
+			if ret == 0:
+				t.send('\n')
+				t.expect('Enter file size: ',str(hincog_bytes)+'\n')
+			else:
+				t.send('YES\n')
+		if out_fmt == 'w': t.label()
+		return t.written_to_file(capfirst(desc),oo=True),t
+
+	def export_seed(self,wf,desc='seed data',out_fmt='seed',pf=None):
+		f,t = self._walletconv_export(wf,desc=desc,out_fmt=out_fmt,pf=pf)
+		silence()
+		msg('{}: {}'.format(capfirst(desc),cyan(get_data_from_file(f,desc))))
+		end_silence()
+		return t
+
+	def export_hex(self,wf,desc='hexadecimal seed data',out_fmt='hex',pf=None):
+		return self.export_seed(wf,desc=desc,out_fmt=out_fmt,pf=pf)
+
+	def export_mnemonic(self,wf):
+		return self.export_seed(wf,desc='mnemonic data',out_fmt='words')
+
+	def export_incog(self,wf,desc='incognito data',out_fmt='i',add_args=[]):
+		uargs = ['-p1',self.usr_rand_arg] + add_args
+		f,t = self._walletconv_export(wf,desc=desc,out_fmt=out_fmt,uargs=uargs,out_pw=True)
+		return t
+
+	def export_incog_hex(self,wf):
+		return self.export_incog(wf,desc='hex incognito data',out_fmt='xi')
+
+	# TODO: make outdir and hidden incog compatible (ignore --outdir and warn user?)
+	def export_incog_hidden(self,wf):
+		rf = joinpath(self.tmpdir,hincog_fn)
+		add_args = ['-J','{},{}'.format(rf,hincog_offset)]
+		return self.export_incog(
+			wf,desc='hidden incognito data',out_fmt='hi',add_args=add_args)
+
+	def addrgen_seed(self,wf,foo,desc='seed data',in_fmt='seed'):
+		stdout = (False,True)[desc=='seed data'] #capture output to screen once
+		add_args = ([],['-S'])[bool(stdout)] + self.segwit_arg
+		t = self.spawn('mmgen-addrgen', add_args +
+				['-i'+in_fmt,'-d',self.tmpdir,wf,self.addr_idx_list])
+		t.license()
+		t.expect_getend('Valid {} for Seed ID '.format(desc))
+		vmsg('Comparing generated checksum with checksum from previous address file')
+		chk = t.expect_getend(r'Checksum for address data .*?: ',regex=True)
+		if stdout: t.read()
+		verify_checksum_or_exit(self._get_addrfile_checksum(),chk)
+		if in_fmt != 'seed':
+			t.no_overwrite()
+			t.req_exit_val = 1
+		return t
+
+	def addrgen_hex(self,wf,foo,desc='hexadecimal seed data',in_fmt='hex'):
+		return self.addrgen_seed(wf,foo,desc=desc,in_fmt=in_fmt)
+
+	def addrgen_mnemonic(self,wf,foo):
+		return self.addrgen_seed(wf,foo,desc='mnemonic data',in_fmt='words')
+
+	def addrgen_incog(self,wf=[],foo='',in_fmt='i',desc='incognito data',args=[]):
+		t = self.spawn('mmgen-addrgen', args + self.segwit_arg + ['-i'+in_fmt,'-d',self.tmpdir]+
+				([],[wf])[bool(wf)] + [self.addr_idx_list])
+		t.license()
+		t.expect_getend('Incog Wallet ID: ')
+		t.hash_preset(desc,'1')
+		t.passphrase('{} \w{{8}}'.format(desc),self.wpasswd)
+		vmsg('Comparing generated checksum with checksum from address file')
+		chk = t.expect_getend(r'Checksum for address data .*?: ',regex=True)
+		verify_checksum_or_exit(self._get_addrfile_checksum(),chk)
+		t.no_overwrite()
+		t.req_exit_val = 1
+		return t
+
+	def addrgen_incog_hex(self,wf,foo):
+		return self.addrgen_incog(wf,'',in_fmt='xi',desc='hex incognito data')
+
+	def addrgen_incog_hidden(self,wf,foo):
+		rf = joinpath(self.tmpdir,hincog_fn)
+		return self.addrgen_incog([],'',in_fmt='hi',desc='hidden incognito data',
+			args=['-H','{},{}'.format(rf,hincog_offset),'-l',str(hincog_seedlen)])
+
+	def txsign_keyaddr(self,keyaddr_file,txfile):
+		t = self.spawn('mmgen-txsign', ['-d',self.tmpdir,'-M',keyaddr_file,txfile])
+		t.license()
+		t.do_decrypt_ka_data(hp='1',pw=self.kapasswd)
+		t.view_tx('n')
+		self.txsign_end(t)
+		return t
+
+	def txcreate_ni(self,addrfile):
+		return self.txcreate_common(sources=['1'],cmdline_inputs=True,add_args=['--yes'])
+
+	def walletgen2(self,del_dw_run='dummy'):
+		return self.walletgen(seed_len=128)
+
+	def addrgen2(self,wf):
+		return self.addrgen(wf,pf='')
+
+	def txcreate2(self,addrfile):
+		return self.txcreate_common(sources=['2'])
+
+	def txsign2(self,txf1,wf1,txf2,wf2):
+		t = self.spawn('mmgen-txsign', ['-d',self.tmpdir,txf1,wf1,txf2,wf2])
+		t.license()
+		for cnum in ('1','2'):
+			t.view_tx('n')
+			t.passphrase('MMGen wallet',self.cfgs[cnum]['wpasswd'])
+			self.txsign_end(t,cnum)
+		return t
+
+	def export_mnemonic2(self,wf):
+		return self.export_mnemonic(wf)
+
+	def walletgen3(self,del_dw_run='dummy'):
+		return self.walletgen()
+
+	def addrgen3(self,wf):
+		return self.addrgen(wf,pf='')
+
+	def txcreate3(self,addrfile1,addrfile2):
+		return self.txcreate_common(sources=['1','3'])
+
+	def txsign3(self,wf1,wf2,txf2):
+		t = self.spawn('mmgen-txsign', ['-d',self.tmpdir,wf1,wf2,txf2])
+		t.license()
+		t.view_tx('n')
+		for cnum in ('1','3'):
+			t.passphrase('MMGen wallet',self.cfgs[cnum]['wpasswd'])
+		self.txsign_end(t)
+		return t
+
+	walletgen14 = walletgen
+	addrgen14 = TestSuiteShared.addrgen
+	keyaddrgen14 = TestSuiteShared.keyaddrgen
+
+	def walletgen4(self,del_dw_run='dummy'):
+		bwf = joinpath(self.tmpdir,self.bw_filename)
+		make_brainwallet_file(bwf)
+		seed_len = str(self.seed_len)
+		args = ['-d',self.tmpdir,'-p1',self.usr_rand_arg,'-l'+seed_len,'-ib']
+		t = self.spawn('mmgen-walletconv', args + [bwf])
+		t.license()
+		t.passphrase_new('new MMGen wallet',self.wpasswd)
+		t.usr_rand(self.usr_rand_chars)
+		t.label()
+		t.written_to_file('MMGen wallet')
+		return t
+
+	def addrgen4(self,wf):
+		return self.addrgen(wf,pf='')
+
+	def txcreate4(self,f1,f2,f3,f4,f5,f6):
+		return self.txcreate_common(sources=['1','2','3','4','14'],non_mmgen_input='4',do_label=True,view='y')
+
+	def txsign4(self,f1,f2,f3,f4,f5,f6):
+		non_mm_file = joinpath(self.tmpdir,non_mmgen_fn)
+		a = ['-d',self.tmpdir,'-i','brain','-b'+self.bw_params,'-p1','-k',non_mm_file,'-M',f6,f1,f2,f3,f4,f5]
+		t = self.spawn('mmgen-txsign',a)
+		t.license()
+		t.do_decrypt_ka_data(hp='1',pw=self.cfgs['14']['kapasswd'])
+		t.view_tx('t')
+
+		for cnum,desc in (('1','incognito data'),('3','MMGen wallet')):
+			t.passphrase('{}'.format(desc),self.cfgs[cnum]['wpasswd'])
+
+		self.txsign_end(t,has_label=True)
+		return t
+
+	def txdo4(self,f1,f2,f3,f4,f5,f6,f7,f8,f9,f10,f11,f12):
+		non_mm_file = joinpath(self.tmpdir,non_mmgen_fn)
+		add_args = ['-d',self.tmpdir,'-i','brain','-b'+self.bw_params,'-p1','-k',non_mm_file,'-M',f12]
+		self.get_file_with_ext('sigtx',delete_all=True) # delete tx signed by txsign4
+		t = self.txcreate_common(sources=['1','2','3','4','14'],
+					non_mmgen_input='4',do_label=True,txdo_args=[f7,f8,f9,f10],add_args=add_args)
+
+		for cnum,desc in (('1','incognito data'),('3','MMGen wallet')):
+			t.passphrase('{}'.format(desc),self.cfgs[cnum]['wpasswd'])
+
+		self.txsign_ui_common(t,self.test_name)
+		self.txsend_ui_common(t,self.test_name)
+
+		cmd = 'touch ' + joinpath(self.tmpdir,'txdo')
+		os.system(cmd.encode())
+		return t
+
+	def txbump4(self,f1,f2,f3,f4,f5,f6,f7,f8,f9): # f7:txfile,f9:'txdo'
+		non_mm_file = joinpath(self.tmpdir,non_mmgen_fn)
+		return self.txbump(f7,prepend_args=['-p1','-k',non_mm_file,'-M',f1],seed_args=[f2,f3,f4,f5,f6,f8])
+
+	def walletgen5(self,del_dw_run='dummy'):
+		return self.walletgen()
+
+	def addrgen5(self,wf):
+		return self.addrgen(wf,pf='')
+
+	def txcreate5(self,addrfile):
+		return self.txcreate_common(sources=['20'],non_mmgen_input='20',non_mmgen_input_compressed=False)
+
+	def txsign5(self,txf,wf,bad_vsize=True,add_args=[]):
+		non_mm_file = joinpath(self.tmpdir,non_mmgen_fn)
+		t = self.spawn('mmgen-txsign', add_args + ['-d',self.tmpdir,'-k',non_mm_file,txf,wf])
+		t.license()
+		t.view_tx('n')
+		t.passphrase('MMGen wallet',self.cfgs['20']['wpasswd'])
+		if bad_vsize:
+			t.expect('Estimated transaction vsize')
+			t.expect('1 transaction could not be signed')
+			exit_val = 2
+		else:
+			t.do_comment(False)
+			t.expect('Save signed transaction? (Y/n): ','y')
+			exit_val = 0
+		t.read()
+		t.req_exit_val = exit_val
+		return t
+
+	def walletgen6(self,del_dw_run='dummy'):
+		return self.walletgen()
+
+	def addrgen6(self,wf):
+		return self.addrgen(wf,pf='')
+
+	def txcreate6(self,addrfile):
+		return self.txcreate_common(
+			sources=['21'],non_mmgen_input='21',non_mmgen_input_compressed=False,add_args=['--vsize-adj=1.08'])
+
+	def txsign6(self,txf,wf):
+		return self.txsign5(txf,wf,bad_vsize=False,add_args=['--vsize-adj=1.08'])

+ 162 - 0
test/test_py_d/ts_misc.py

@@ -0,0 +1,162 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
+# Copyright (C)2013-2019 The MMGen Project <mmgen@tuta.io>
+#
+# 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/>.
+
+"""
+ts_misc.py: Miscellaneous test groups for the test.py test suite
+"""
+
+from mmgen.globalvars import g
+from test.common import *
+from test.test_py_d.common import *
+from test.test_py_d.ts_base import *
+from test.test_py_d.ts_main import TestSuiteMain
+
+class TestSuiteHelp(TestSuiteBase):
+	'help and usage screens'
+	tmpdir_nums = []
+	passthru_opts = ('coin','testnet')
+	cmd_group = (
+		('helpscreens',     (1,'help screens',             [])),
+		('longhelpscreens', (1,'help screens (--longhelp)',[])),
+		('tool_help',       (1,"'mmgen-tool' usage screen",[])),
+		('test_help',       (1,"'test.py' help screens",[])),
+	)
+	def helpscreens(self,arg='--help'):
+		scripts = (
+			'walletgen','walletconv','walletchk','txcreate','txsign','txsend','txdo','txbump',
+			'addrgen','addrimport','keygen','passchg','tool','passgen','regtest','autosign')
+		for s in scripts:
+			t = self._run_cmd('mmgen-'+s,[arg],extra_desc='(mmgen-{})'.format(s),no_output=True)
+		return t
+
+	def longhelpscreens(self):
+		return self.helpscreens(arg='--longhelp')
+
+	def _run_cmd(   self, cmd_name,
+					cmd_args = [],
+					no_msg = False,
+					extra_desc = '',
+					cmd_dir = 'cmds',
+					no_output = False):
+		t = self.spawn( cmd_name,
+						args       = cmd_args,
+						no_msg     = no_msg,
+						extra_desc = extra_desc,
+						cmd_dir    = cmd_dir,
+						no_output  = no_output)
+		t.read()
+		ret = t.p.wait()
+		if ret == 0:
+			msg('OK')
+		else:
+			rdie(1,"\n'{}' returned {}".format(self.test_name,ret))
+		t.skip_ok = True
+		return t
+
+	def tool_help(self):
+		self._run_cmd('mmgen-tool',['help'],extra_desc="('mmgen-tool help')")
+		return self._run_cmd('mmgen-tool',['usage'],extra_desc="('mmgen-tool usage')")
+
+	def test_help(self):
+		self._run_cmd('test.py',['-h'],cmd_dir='test')
+		self._run_cmd('test.py',['-L'],cmd_dir='test',extra_desc='(cmd group list)')
+		return self._run_cmd('test.py',['-l'],cmd_dir='test',extra_desc='(cmd list)')
+
+class TestSuiteTool(TestSuiteMain,TestSuiteBase):
+	"tests for interactive 'mmgen-tool' commands"
+	networks = ('btc',)
+	segwit_opts_ok = False
+	tmpdir_nums = [9]
+	enc_infn = 'tool_encrypt.in'
+	cmd_group = (
+		('tool_find_incog_data', (9,"'mmgen-tool find_incog_data'", [[[hincog_fn],1],[[incog_id_fn],1]])),
+		('tool_encrypt',         (9,"'mmgen-tool encrypt' (random data)",     [])),
+		('tool_decrypt',         (9,"'mmgen-tool decrypt' (random data)", [[[enc_infn+'.mmenc'],9]])),
+		# ('tool_encrypt_ref', (9,"'mmgen-tool encrypt' (reference text)",  [])),
+	)
+
+	def tool_encrypt(self):
+		infile = joinpath(self.tmpdir,self.enc_infn)
+		write_to_file(infile,os.urandom(1033),binary=True)
+		t = self.spawn('mmgen-tool',['-d',self.tmpdir,self.usr_rand_arg,'encrypt',infile])
+		t.usr_rand(self.usr_rand_chars)
+		t.hash_preset('user data','1')
+		t.passphrase_new('user data',tool_enc_passwd)
+		t.written_to_file('Encrypted data')
+		return t
+
+	def tool_decrypt(self,f1):
+		out_fn = 'tool_encrypt.out'
+		t = self.spawn('mmgen-tool',['-d',self.tmpdir,'decrypt',f1,'outfile='+out_fn,'hash_preset=1'])
+		t.passphrase('user data',tool_enc_passwd)
+		t.written_to_file('Decrypted data')
+		d1 = self.read_from_tmpfile(self.enc_infn,binary=True)
+		d2 = self.read_from_tmpfile(out_fn,binary=True)
+		cmp_or_die(d1,d2)
+		return t
+
+	def tool_find_incog_data(self,f1,f2):
+		i_id = read_from_file(f2).rstrip()
+		vmsg('Incog ID: {}'.format(cyan(i_id)))
+		t = self.spawn('mmgen-tool',['-d',self.tmpdir,'find_incog_data',f1,i_id])
+		o = t.expect_getend('Incog data for ID {} found at offset '.format(i_id))
+		os.unlink(f1)
+		cmp_or_die(hincog_offset,int(o))
+		return t
+
+class TestSuiteRefTX(TestSuiteMain,TestSuiteBase):
+	'create a reference transaction file (administrative command)'
+	segwit_opts_ok = False
+	passthru_opts = ('coin','testnet')
+	tmpdir_nums = [31,32,33,34]
+	cmd_group = (
+		('ref_tx_addrgen1', (31,'address generation (legacy)', [[[],1]])),
+		('ref_tx_addrgen2', (32,'address generation (compressed)', [[[],1]])),
+		('ref_tx_addrgen3', (33,'address generation (segwit)', [[[],1]])),
+		('ref_tx_addrgen4', (34,'address generation (bech32)', [[[],1]])),
+		('ref_tx_txcreate', (31,'transaction creation',
+								([['addrs'],31],[['addrs'],32],[['addrs'],33],[['addrs'],34]))),
+	)
+
+	def __init__(self,trunner,cfgs,spawn):
+		for n in self.tmpdir_nums:
+			cfgs[str(n)].update({   'addr_idx_list': '1-2',
+									'segwit': n in (33,34),
+									'dep_generators': { 'addrs':'ref_tx_addrgen'+str(n)[-1] }})
+		return TestSuiteMain.__init__(self,trunner,cfgs,spawn)
+
+	def ref_tx_addrgen(self,atype):
+		if atype not in g.proto.mmtypes: return
+		t = self.spawn('mmgen-addrgen',['--outdir='+self.tmpdir,'--type='+atype,dfl_words_file,'1-2'])
+		t.read()
+		return t
+
+	def ref_tx_addrgen1(self): return self.ref_tx_addrgen(atype='L')
+	def ref_tx_addrgen2(self): return self.ref_tx_addrgen(atype='C')
+	def ref_tx_addrgen3(self): return self.ref_tx_addrgen(atype='S')
+	def ref_tx_addrgen4(self): return self.ref_tx_addrgen(atype='B')
+
+	def ref_tx_txcreate(self,f1,f2,f3,f4):
+		sources = ['31','32']
+		if 'S' in g.proto.mmtypes: sources += ['33']
+		if 'B' in g.proto.mmtypes: sources += ['34']
+		return self.txcreate_common(
+									addrs_per_wallet = 2,
+									sources          = sources,
+									add_args         = ['--locktime=1320969600'],
+									do_label         = True )

+ 165 - 0
test/test_py_d/ts_ref.py

@@ -0,0 +1,165 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
+# Copyright (C)2013-2019 The MMGen Project <mmgen@tuta.io>
+#
+# 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/>.
+
+"""
+ts_ref.py: Reference file tests for the test.py test suite
+"""
+
+import os
+from mmgen.globalvars import g
+from mmgen.opts import opt
+from test.common import *
+from test.test_py_d.common import *
+
+from test.test_py_d.ts_base import *
+from test.test_py_d.ts_shared import *
+
+wpasswd = 'reference password'
+
+class TestSuiteRef(TestSuiteBase,TestSuiteShared):
+	'saved reference files'
+	tmpdir_nums = [8]
+	networks = ('btc','btc_tn','ltc','ltc_tn')
+	passthru_opts = ('coin','testnet')
+	sources = {
+		'ref_addrfile':    '98831F3A{}[1,31-33,500-501,1010-1011]{}.addrs',
+		'ref_segwitaddrfile':'98831F3A{}-S[1,31-33,500-501,1010-1011]{}.addrs',
+		'ref_bech32addrfile':'98831F3A{}-B[1,31-33,500-501,1010-1011]{}.addrs',
+		'ref_keyaddrfile': '98831F3A{}[1,31-33,500-501,1010-1011]{}.akeys.mmenc',
+		'ref_passwdfile':  '98831F3A-фубар@crypto.org-b58-20[1,4,9-11,1100].pws',
+		'ref_tx_file': { # data shared with ref_altcoin, autosign
+			'btc': ('0B8D5A[15.31789,14,tl=1320969600].rawtx',
+					'0C7115[15.86255,14,tl=1320969600].testnet.rawtx'),
+			'ltc': ('AF3CDF-LTC[620.76194,1453,tl=1320969600].rawtx',
+					'A5A1E0-LTC[1454.64322,1453,tl=1320969600].testnet.rawtx'),
+			'bch': ('460D4D-BCH[10.19764,tl=1320969600].rawtx',
+					'359FD5-BCH[6.68868,tl=1320969600].testnet.rawtx'),
+			'eth': ('88FEFD-ETH[23.45495,40000].rawtx',
+					'B472BD-ETH[23.45495,40000].testnet.rawtx'),
+			'mm1': ('5881D2-MM1[1.23456,50000].rawtx',
+					'6BDB25-MM1[1.23456,50000].testnet.rawtx'),
+			'etc': ('ED3848-ETC[1.2345,40000].rawtx','')
+		},
+	}
+	chk_data = {
+		'ref_addrfile_chksum': {
+			'btc': ('6FEF 6FB9 7B13 5D91','424E 4326 CFFE 5F51'),
+			'ltc': ('AD52 C3FE 8924 AAF0','4EBE 2E85 E969 1B30'),
+		},
+		'ref_segwitaddrfile_chksum': {
+			'btc': ('06C1 9C87 F25C 4EE6','072C 8B07 2730 CB7A'),
+			'ltc': ('63DF E42A 0827 21C3','5DD1 D186 DBE1 59F2'),
+		},
+		'ref_bech32addrfile_chksum': {
+			'btc': ('9D2A D4B6 5117 F02E','0527 9C39 6C1B E39A'),
+			'ltc': ('FF1C 7939 5967 AB82','ED3D 8AA4 BED4 0B40'),
+		},
+		'ref_keyaddrfile_chksum': {
+			'btc': ('9F2D D781 1812 8BAD','88CC 5120 9A91 22C2'),
+			'ltc': ('B804 978A 8796 3ED4','98B5 AC35 F334 0398'),
+		},
+		'ref_passwdfile_chksum':   'A983 DAB9 5514 27FB',
+	}
+	cmd_group = ( # TODO: move to tooltest2
+		('ref_addrfile_chk',   'saved reference address file'),
+		('ref_segwitaddrfile_chk','saved reference address file (segwit)'),
+		('ref_bech32addrfile_chk','saved reference address file (bech32)'),
+		('ref_keyaddrfile_chk','saved reference key-address file'),
+		('ref_passwdfile_chk', 'saved reference password file'),
+#	Create the fake inputs:
+#	('txcreate8',          'transaction creation (8)'),
+		('ref_tx_chk',         'signing saved reference tx file'),
+		('ref_brain_chk_spc3', 'saved brainwallet (non-standard spacing)'),
+		('ref_tool_decrypt',   'decryption of saved MMGen-encrypted file'),
+	)
+
+	def _get_ref_subdir_by_coin(self,coin):
+		return {'btc': '',
+				'bch': '',
+				'ltc': 'litecoin',
+				'eth': 'ethereum',
+				'etc': 'ethereum_classic',
+				'xmr': 'monero',
+				'zec': 'zcash',
+				'dash': 'dash' }[coin.lower()]
+
+	@property
+	def ref_subdir(self):
+		return self._get_ref_subdir_by_coin(g.coin)
+
+	def ref_addrfile_chk(self,ftype='addr',coin=None,subdir=None,pfx=None,mmtype=None,add_args=[]):
+		af_key = 'ref_{}file'.format(ftype)
+		af_fn = TestSuiteRef.sources[af_key].format(pfx or self.altcoin_pfx,'' if coin else self.tn_ext)
+		af = joinpath(ref_dir,(subdir or self.ref_subdir,'')[ftype=='passwd'],af_fn)
+		coin_arg = [] if coin == None else ['--coin='+coin]
+		tool_cmd = ftype.replace('segwit','').replace('bech32','')+'file_chksum'
+		t = self.spawn('mmgen-tool',coin_arg+[tool_cmd,af]+add_args)
+		if ftype == 'keyaddr':
+			t.do_decrypt_ka_data(hp=ref_kafile_hash_preset,pw=ref_kafile_pass)
+		rc = self.chk_data[   'ref_' + ftype + 'file_chksum' +
+					('_'+coin.lower() if coin else '') +
+					('_'+mmtype if mmtype else '')]
+		ref_chksum = rc if (ftype == 'passwd' or coin) else rc[g.proto.base_coin.lower()][g.testnet]
+		t.expect(chksum_pat,regex=True)
+		m = t.p.match.group(0)
+		t.read()
+		cmp_or_die(ref_chksum,m)
+		return t
+
+	def ref_segwitaddrfile_chk(self):
+		if not 'S' in g.proto.mmtypes:
+			return skip('not supported')
+		else:
+			return self.ref_addrfile_chk(ftype='segwitaddr')
+
+	def ref_bech32addrfile_chk(self):
+		if not 'B' in g.proto.mmtypes:
+			return skip('not supported')
+		else:
+			return self.ref_addrfile_chk(ftype='bech32addr')
+
+	def ref_keyaddrfile_chk(self):
+		return self.ref_addrfile_chk(ftype='keyaddr')
+
+	def ref_passwdfile_chk(self):
+		return self.ref_addrfile_chk(ftype='passwd')
+
+	def ref_tx_chk(self):
+		fn = self.sources['ref_tx_file'][g.coin.lower()][bool(self.tn_ext)]
+		if not fn: return
+		tf = joinpath(ref_dir,self.ref_subdir,fn)
+		wf = dfl_words_file
+		self.write_to_tmpfile(pwfile,wpasswd)
+		pf = joinpath(self.tmpdir,pwfile)
+		return self.txsign(tf,wf,pf,save=False,has_label=True,do_passwd=False)
+
+	def ref_brain_chk_spc3(self):
+		return self.ref_brain_chk(bw_file=ref_bw_file_spc)
+
+	def ref_tool_decrypt(self):
+		f = joinpath(ref_dir,ref_enc_fn)
+		disable_debug()
+		dec_file = joinpath(self.tmpdir,'famous.txt')
+		t = self.spawn('mmgen-tool', ['-q','decrypt',f,'outfile='+dec_file,'hash_preset=1'])
+		restore_debug()
+		t.passphrase('user data',tool_enc_passwd)
+		t.written_to_file('Decrypted data')
+		dec_txt = read_from_file(dec_file)
+		imsg_r(dec_txt)
+		cmp_or_die(sample_text,dec_txt)
+		return t

+ 274 - 0
test/test_py_d/ts_ref_3seed.py

@@ -0,0 +1,274 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
+# Copyright (C)2013-2019 The MMGen Project <mmgen@tuta.io>
+#
+# 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/>.
+
+"""
+ts_ref_3seed.py: Saved and generated reference file tests for 128, 192 and
+                 256-bit seeds for the test.py test suite
+"""
+
+from mmgen.globalvars import g
+from mmgen.opts import opt
+from test.common import *
+from test.test_py_d.common import *
+from test.test_py_d.ts_base import *
+from test.test_py_d.ts_shared import *
+from test.test_py_d.ts_wallet import TestSuiteWalletConv
+
+class TestSuiteRef3Seed(TestSuiteBase,TestSuiteShared):
+	'saved and generated reference data for 128-, 192- and 256-bit seeds'
+	networks = ('btc','btc_tn','ltc','ltc_tn')
+	passthru_opts = ('coin','testnet')
+	mmtypes = (None,)
+	tmpdir_nums = [6,7,8]
+	addr_idx_list_in = '1010,500-501,31-33,1,33,500,1011'
+	pass_idx_list_in = '1,4,9-11,1100'
+	chk_data = {
+		'refaddrgen_legacy_1': {
+			'btc': ('B230 7526 638F 38CB','A9DC 5A13 12CB 1317'),
+			'ltc': ('2B23 5E97 848A B961','AEC3 E774 0B21 0202'),
+		},
+		'refaddrgen_segwit_1': {
+			'btc': ('9914 6D10 2307 F348','83C8 A6B6 ADA8 25B2'),
+			'ltc': ('CC09 A190 B7DF B7CD','0425 7893 C6F1 ECA3'),
+		},
+		'refaddrgen_bech32_1': {
+			'btc': ('C529 D686 31AA ACD4','21D0 26AD 3A22 5465'),
+			'ltc': ('3DFB CFCC E180 DC9D','8C72 D5C2 07E0 5F7B'),
+		},
+		'refaddrgen_compressed_1': {
+			'btc': ('95EB 8CC0 7B3B 7856','16E6 6170 154D 2202'),
+			'ltc': ('35D5 8ECA 9A42 46C3','15B3 5492 D3D3 6854'),
+		},
+		'refkeyaddrgen_legacy_1': {
+			'btc': ('CF83 32FB 8A8B 08E2','1F67 B73A FF8C 5D15'),
+			'ltc': ('1896 A26C 7F14 2D01','FA0E CD4E ADAF DBF4'),
+		},
+		'refkeyaddrgen_compressed_1': {
+			'btc': ('E43A FA46 5751 720A','FDEE 8E45 1C0A 02AD'),
+			'ltc': ('7603 2FE3 2145 FFAD','3FE0 5A8E 5FBE FF3E'),
+		},
+		'refkeyaddrgen_segwit_1': {
+			'btc': ('C13B F717 D4E8 CF59','BB71 175C 5416 19D8'),
+			'ltc': ('054B 9794 55B4 5D82','DE85 3CF3 9636 FE2E'),
+		},
+		'refkeyaddrgen_bech32_1': {
+			'btc': ('934F 1C33 6C06 B18C','A283 5BAB 7AF3 3EA4'),
+			'ltc': ('A6AD DF53 5968 7B6A','9572 43E0 A4DC 0B2E'),
+		},
+		'refpasswdgen_1':     'EB29 DC4F 924B 289F',
+		'ref_b32passwdgen_1': '37B6 C218 2ABC 7508',
+		'ref_hexpasswdgen_1': '523A F547 0E69 8323',
+		'refaddrgen_legacy_2': {
+			'btc': ('8C17 A5FA 0470 6E89','764C 66F9 7502 AAEA'),
+			'ltc': ('2B77 A009 D5D0 22AD','51D1 979D 0A35 F24B'),
+		},
+		'refaddrgen_compressed_2': {
+			'btc': ('2615 8401 2E98 7ECA','A386 EE07 A356 906D'),
+			'ltc': ('197C C48C 3C37 AB0F','8DDC 5FE3 BFF9 1226'),
+		},
+		'refaddrgen_segwit_2': {
+			'btc': ('91C4 0414 89E4 2089','BF9F C67F ED22 A47B'),
+			'ltc': ('8F12 FA7B 9F12 594C','2609 8494 A23C F836'),
+		},
+		'refaddrgen_bech32_2': {
+			'btc': ('2AA3 78DF B965 82EB','027B 1C1F 7FB2 D859'),
+			'ltc': ('951C 8FB2 FCA5 87D1','4A5D 67E0 8210 FEF2'),
+		},
+		'refkeyaddrgen_legacy_2': {
+			'btc': ('9648 5132 B98E 3AD9','1BD3 5A36 D51C 256D'),
+			'ltc': ('DBD4 FAB6 7E46 CD07','8822 3FDF FEC0 6A8C'),
+		},
+		'refkeyaddrgen_compressed_2': {
+			'btc': ('6D6D 3D35 04FD B9C3','94BF 4BCF 10B2 394B'),
+			'ltc': ('F5DA 9D60 6798 C4E9','7918 88DE 9096 DD7A'),
+		},
+		'refkeyaddrgen_segwit_2': {
+			'btc': ('C98B DF08 A3D5 204B','7E7F DF50 FE04 6F68'),
+			'ltc': ('1829 7FE7 2567 CB91','BE92 D19C 7589 EF30'),
+		},
+		'refkeyaddrgen_bech32_2': {
+			'btc': ('4A6B 3762 DF30 9368','12DD 1888 36BA 85F7'),
+			'ltc': ('5C12 FDD4 17AB F179','E195 B28C 59C4 C5EC'),
+		},
+		'refpasswdgen_2':     'ADEA 0083 094D 489A',
+		'ref_b32passwdgen_2': '2A28 C5C7 36EC 217A',
+		'ref_hexpasswdgen_2': 'B11C AC6A 1464 608D',
+		'refaddrgen_legacy_3': {
+			'btc': ('6FEF 6FB9 7B13 5D91','424E 4326 CFFE 5F51'),
+			'ltc': ('AD52 C3FE 8924 AAF0','4EBE 2E85 E969 1B30'),
+		},
+		'refaddrgen_compressed_3': {
+			'btc': ('A33C 4FDE F515 F5BC','6C48 AA57 2056 C8C8'),
+			'ltc': ('3FC0 8F03 C2D6 BD19','4C0A 49B6 2DD1 1BE0'),
+		},
+		'refaddrgen_segwit_3': {
+			'btc': ('06C1 9C87 F25C 4EE6','072C 8B07 2730 CB7A'),
+			'ltc': ('63DF E42A 0827 21C3','5DD1 D186 DBE1 59F2'),
+		},
+		'refaddrgen_bech32_3': {
+			'btc': ('9D2A D4B6 5117 F02E','0527 9C39 6C1B E39A'),
+			'ltc': ('FF1C 7939 5967 AB82','ED3D 8AA4 BED4 0B40'),
+		},
+		'refkeyaddrgen_legacy_3': {
+			'btc': ('9F2D D781 1812 8BAD','88CC 5120 9A91 22C2'),
+			'ltc': ('B804 978A 8796 3ED4','98B5 AC35 F334 0398'),
+		},
+		'refkeyaddrgen_compressed_3': {
+			'btc': ('420A 8EB5 A9E2 7814','F43A CB4A 81F3 F735'),
+			'ltc': ('8D1C 781F EB7F 44BC','05F3 5C68 FD31 FCEF'),
+		},
+		'refkeyaddrgen_segwit_3': {
+			'btc': ('A447 12C2 DD14 5A9B','C770 7391 C415 21F9'),
+			'ltc': ('E8A3 9F6E E164 A521','D3D5 BFDD F5D5 20BD'),
+		},
+		'refkeyaddrgen_bech32_3': {
+			'btc': ('D0DD BDE3 87BE 15AE','7552 D70C AAB8 DEAA'),
+			'ltc': ('74A0 7DD5 963B 6326','2CDA A007 4B9F E9A5'),
+		},
+		'refpasswdgen_3':     '2D6D 8FBA 422E 1315',
+		'ref_b32passwdgen_3': 'F6C1 CDFB 97D9 FCAE',
+		'ref_hexpasswdgen_3': 'BD4F A0AC 8628 4BE4',
+	}
+	cmd_group = (
+		# reading
+		('ref_wallet_chk', ([],'saved reference wallet')),
+		('ref_seed_chk',   ([],'saved seed file')),
+		('ref_hex_chk',    ([],'saved mmhex file')),
+		('ref_mn_chk',     ([],'saved mnemonic file')),
+		('ref_hincog_chk', ([],'saved hidden incog reference wallet')),
+		('ref_brain_chk',  ([],'saved brainwallet')), # in ts_shared
+		# generating new reference ('abc' brainwallet) files:
+		('refwalletgen',   ([],'gen new refwallet')),
+		('refaddrgen_legacy',     (['mmdat',pwfile],'new refwallet addr chksum (uncompressed)')),
+		('refaddrgen_compressed',     (['mmdat',pwfile],'new refwallet addr chksum (compressed)')),
+		('refaddrgen_segwit',     (['mmdat',pwfile],'new refwallet addr chksum (segwit)')),
+		('refaddrgen_bech32',     (['mmdat',pwfile],'new refwallet addr chksum (bech32)')),
+		('refkeyaddrgen_legacy',  (['mmdat',pwfile],'new refwallet key-addr chksum (uncompressed)')),
+		('refkeyaddrgen_compressed', (['mmdat',pwfile],'new refwallet key-addr chksum (compressed)')),
+		('refkeyaddrgen_segwit', (['mmdat',pwfile],'new refwallet key-addr chksum (segwit)')),
+		('refkeyaddrgen_bech32', (['mmdat',pwfile],'new refwallet key-addr chksum (bech32)')),
+		('refpasswdgen',   (['mmdat',pwfile],'new refwallet passwd file chksum')),
+		('ref_b32passwdgen',(['mmdat',pwfile],'new refwallet passwd file chksum (base32)')),
+		('ref_hexpasswdgen',(['mmdat',pwfile],'new refwallet passwd file chksum (base32)')),
+	)
+
+	def __init__(self,trunner,cfgs,spawn):
+		for k,j in self.cmd_group:
+			for n in (1,2,3): # 128,192,256 bits
+				setattr(self,'{}_{}'.format(k,n),getattr(self,k))
+		for n in self.tmpdir_nums:
+			cfgs[str(n)]['addr_idx_list'] = self.addr_idx_list_in
+			cfgs[str(n)]['pass_idx_list'] = self.pass_idx_list_in
+		return TestSuiteBase.__init__(self,trunner,cfgs,spawn)
+
+	def ref_wallet_chk(self):
+		wf = joinpath(ref_dir,TestSuiteWalletConv.sources[str(self.seed_len)]['ref_wallet'])
+		return self.walletchk(wf,pf=None,pw=True,sid=self.seed_id)
+
+	def ref_ss_chk(self,ss=None):
+		wf = joinpath(ref_dir,'{}.{}'.format(self.seed_id,ss.ext))
+		return self.walletchk(wf,pf=None,desc=ss.desc,sid=self.seed_id)
+
+	def ref_seed_chk(self):
+		from mmgen.seed import SeedFile
+		return self.ref_ss_chk(ss=SeedFile)
+
+	def ref_hex_chk(self):
+		from mmgen.seed import HexSeedFile
+		return self.ref_ss_chk(ss=HexSeedFile)
+
+	def ref_mn_chk(self):
+		from mmgen.seed import Mnemonic
+		return self.ref_ss_chk(ss=Mnemonic)
+
+	def ref_hincog_chk(self,desc='hidden incognito data'):
+		source = TestSuiteWalletConv.sources[str(self.seed_len)]
+		for wtype,edesc,of_arg in ('hic_wallet','',[]), \
+								('hic_wallet_old','(old format)',['-O']):
+			ic_arg = ['-H{},{}'.format(joinpath(ref_dir,source[wtype]),ref_wallet_incog_offset)]
+			slarg = ['-l{} '.format(self.seed_len)]
+			hparg = ['-p1']
+			if wtype == 'hic_wallet_old' and opt.profile: msg('')
+			t = self.spawn('mmgen-walletchk',
+				slarg + hparg + of_arg + ic_arg,
+				extra_desc=edesc)
+			t.passphrase(desc,self.wpasswd)
+			if wtype == 'hic_wallet_old':
+				t.expect('Is the Seed ID correct? (Y/n): ','\n')
+			chk = t.expect_getend('Seed ID: ')
+			t.close()
+			cmp_or_die(self.seed_id,chk)
+			ok_msg()
+		t.skip_ok = True
+		return t
+
+	def brainwalletgen_ref(self):
+		sl_arg = '-l{}'.format(self.seed_len)
+		hp_arg = '-p{}'.format(ref_wallet_hash_preset)
+		label = "test.py ref. wallet (pw '{}', seed len {}) α".format(ref_wallet_brainpass,self.seed_len)
+		bf = 'ref.mmbrain'
+		args = ['-d',self.tmpdir,hp_arg,sl_arg,'-ib','-L',label]
+		self.write_to_tmpfile(bf,ref_wallet_brainpass)
+		self.write_to_tmpfile(pwfile,self.wpasswd)
+		t = self.spawn('mmgen-walletconv', args + [self.usr_rand_arg])
+		t.license()
+		t.expect('Enter brainwallet: ', ref_wallet_brainpass+'\n')
+		t.passphrase_new('new MMGen wallet',self.wpasswd)
+		t.usr_rand(self.usr_rand_chars)
+		sid = os.path.basename(t.written_to_file('MMGen wallet')).split('-')[0]
+		cmp_or_die(sid,self.seed_id,desc='Seed ID')
+		return t
+
+	def refwalletgen(self):
+		return self.brainwalletgen_ref()
+
+	def refaddrgen_legacy(self,wf,pf):
+		return self.addrgen(wf,pf=pf,check_ref=True,mmtype='legacy')
+
+	def refaddrgen_compressed(self,wf,pf):
+		return self.addrgen(wf,pf=pf,check_ref=True,mmtype='compressed')
+
+	def refaddrgen_segwit(self,wf,pf):
+		return self.addrgen(wf,pf=pf,check_ref=True,mmtype='segwit')
+
+	def refaddrgen_bech32(self,wf,pf):
+		return self.addrgen(wf,pf=pf,check_ref=True,mmtype='bech32')
+
+	def refkeyaddrgen_legacy(self,wf,pf,mmtype='legacy'):
+		return self.keyaddrgen(wf,pf,check_ref=True)
+
+	def refkeyaddrgen_compressed(self,wf,pf):
+		return self.keyaddrgen(wf,pf=pf,check_ref=True,mmtype='compressed')
+
+	def refkeyaddrgen_segwit(self,wf,pf):
+		return self.keyaddrgen(wf,pf=pf,check_ref=True,mmtype='segwit')
+
+	def refkeyaddrgen_bech32(self,wf,pf):
+		return self.keyaddrgen(wf,pf=pf,check_ref=True,mmtype='bech32')
+
+	def refpasswdgen(self,wf,pf):
+		return self.addrgen(wf,pf,check_ref=True,ftype='pass',id_str='alice@crypto.org')
+
+	def ref_b32passwdgen(self,wf,pf):
+		ea = ['--base32','--passwd-len','17']
+		return self.addrgen(wf,pf,check_ref=True,ftype='pass32',id_str='фубар@crypto.org',extra_args=ea)
+
+	def ref_hexpasswdgen(self,wf,pf):
+		ea = ['--hex']
+		return self.addrgen(wf,pf,check_ref=True,ftype='passhex',id_str='фубар@crypto.org',extra_args=ea)
+

+ 205 - 0
test/test_py_d/ts_ref_altcoin.py

@@ -0,0 +1,205 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
+# Copyright (C)2013-2019 The MMGen Project <mmgen@tuta.io>
+#
+# 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/>.
+
+"""
+ts_ref_altcoin.py: Altcoin reference file tests for the test.py test suite
+"""
+
+import os
+from mmgen.globalvars import g
+from mmgen.opts import opt
+from test.test_py_d.common import *
+from test.test_py_d.ts_ref import *
+from test.test_py_d.ts_base import *
+
+class TestSuiteRefAltcoin(TestSuiteRef,TestSuiteBase):
+	'saved and generated altcoin reference files'
+	tmpdir_nums = [8]
+	networks = ('btc',)
+	chk_data = {
+		'ref_addrfile_chksum_zec': '903E 7225 DD86 6E01',
+		'ref_addrfile_chksum_zec_z': '9C7A 72DC 3D4A B3AF',
+		'ref_addrfile_chksum_xmr': '4369 0253 AC2C 0E38',
+		'ref_addrfile_chksum_dash':'FBC1 6B6A 0988 4403',
+		'ref_addrfile_chksum_eth': 'E554 076E 7AF6 66A3',
+		'ref_addrfile_chksum_etc': 'E97A D796 B495 E8BC',
+		'ref_keyaddrfile_chksum_zec': 'F05A 5A5C 0C8E 2617',
+		'ref_keyaddrfile_chksum_zec_z': '6B87 9B2D 0D8D 8D1E',
+		'ref_keyaddrfile_chksum_xmr': 'E0D7 9612 3D67 404A',
+		'ref_keyaddrfile_chksum_dash': 'E83D 2C63 FEA2 4142',
+		'ref_keyaddrfile_chksum_eth': 'E400 70D9 0AE3 C7C2',
+		'ref_keyaddrfile_chksum_etc': 'EF49 967D BD6C FE45',
+	}
+	cmd_group = (
+		('ref_altcoin_tx_chk',    'signing saved reference tx files'),
+		('ref_addrfile_gen_eth',  'generate address file (ETH)'),
+		('ref_addrfile_gen_etc',  'generate address file (ETC)'),
+		('ref_addrfile_gen_dash', 'generate address file (DASH)'),
+		('ref_addrfile_gen_zec',  'generate address file (ZEC-T)'),
+		('ref_addrfile_gen_zec_z','generate address file (ZEC-Z)'),
+		('ref_addrfile_gen_xmr',  'generate address file (XMR)'),
+		# we test the old ed25519 library in test-release.sh, so skip this
+#	('ref_addrfile_gen_xmr_old','generate address file (XMR - old (slow) ed25519 library)'),
+
+		('ref_keyaddrfile_gen_eth',  'generate key-address file (ETH)'),
+		('ref_keyaddrfile_gen_etc',  'generate key-address file (ETC)'),
+		('ref_keyaddrfile_gen_dash', 'generate key-address file (DASH)'),
+		('ref_keyaddrfile_gen_zec',  'generate key-address file (ZEC-T)'),
+		('ref_keyaddrfile_gen_zec_z','generate key-address file (ZEC-Z)'),
+		('ref_keyaddrfile_gen_xmr',  'generate key-address file (XMR)'),
+
+		('ref_addrfile_chk_eth', 'reference address file (ETH)'),
+		('ref_addrfile_chk_etc', 'reference address file (ETC)'),
+		('ref_addrfile_chk_dash','reference address file (DASH)'),
+		('ref_addrfile_chk_zec', 'reference address file (ZEC-T)'),
+		('ref_addrfile_chk_zec_z','reference address file (ZEC-Z)'),
+		('ref_addrfile_chk_xmr', 'reference address file (XMR)'),
+
+		('ref_keyaddrfile_chk_eth', 'reference key-address file (ETH)'),
+		('ref_keyaddrfile_chk_etc', 'reference key-address file (ETC)'),
+		('ref_keyaddrfile_chk_dash','reference key-address file (DASH)'),
+		('ref_keyaddrfile_chk_zec', 'reference key-address file (ZEC-T)'),
+		('ref_keyaddrfile_chk_zec_z','reference key-address file (ZEC-Z)'),
+		('ref_keyaddrfile_chk_xmr', 'reference key-address file (XMR)'),
+	)
+	# Check saved transaction files for *all* configured altcoins
+	# Though this basically duplicates the autosign test, here we do everything
+	# via the command line, so it's worth doing
+	def ref_altcoin_tx_chk(self):
+		self.write_to_tmpfile(pwfile,dfl_wpasswd)
+		pf = joinpath(self.tmpdir,pwfile)
+		from mmgen.protocol import init_coin
+		for k in ('bch','eth','mm1','etc'):
+			coin,token = ('eth','mm1') if k == 'mm1' else (k,None)
+			ref_subdir = self._get_ref_subdir_by_coin(coin)
+			for tn in (False,True):
+				if tn and coin == 'etc': continue
+				g.testnet = tn
+				init_coin(coin)
+				fn = TestSuiteRef.sources['ref_tx_file'][token or coin][bool(tn)]
+				tf = joinpath(ref_dir,ref_subdir,fn)
+				wf = dfl_words_file
+				e = ['--coin='+coin,'--testnet='+('0','1')[tn]]
+				if token: e += ['--token='+token]
+				t = self.txsign(tf, wf, pf,
+								save       = False,
+								has_label  = True,
+								do_passwd  = False,
+								extra_desc = '({}{})'.format(token or coin,' testnet' if tn else ''),
+								extra_opts = e )
+				ok_msg()
+		g.testnet = False
+		init_coin('btc')
+		t.skip_ok = True
+		return t
+
+	def ref_altcoin_addrgen(self,coin,mmtype,gen_what='addr',coin_suf='',add_args=[]):
+		wf = dfl_words_file
+		t = self.spawn('mmgen-{}gen'.format(gen_what),
+				['-Sq','--coin='+coin] +
+				(['--type='+mmtype] if mmtype else []) +
+				add_args +
+				[wf,dfl_addr_idx_list])
+		if gen_what == 'key':
+			t.expect('Encrypt key list? (y/N): ','N')
+		chk = t.expect_getend(r'.* data checksum for \S*: ',regex=True)
+		chk_ref = self.chk_data['ref_{}addrfile_chksum_{}{}'.format(('','key')[gen_what=='key'],coin.lower(),coin_suf)]
+		t.read()
+		cmp_or_die(chk,chk_ref,desc='{}list data checksum'.format(gen_what))
+		return t
+
+	def ref_addrfile_gen_eth(self):
+		return self.ref_altcoin_addrgen(coin='ETH',mmtype='ethereum')
+
+	def ref_addrfile_gen_etc(self):
+		return self.ref_altcoin_addrgen(coin='ETC',mmtype='ethereum')
+
+	def ref_addrfile_gen_dash(self):
+		return self.ref_altcoin_addrgen(coin='DASH',mmtype='compressed')
+
+	def ref_addrfile_gen_zec(self):
+		return self.ref_altcoin_addrgen(coin='ZEC',mmtype='compressed')
+
+	def ref_addrfile_gen_zec_z(self):
+		return self.ref_altcoin_addrgen(coin='ZEC',mmtype='zcash_z',coin_suf='_z')
+
+	def ref_addrfile_gen_xmr(self):
+		return self.ref_altcoin_addrgen(coin='XMR',mmtype='monero')
+
+	def ref_addrfile_gen_xmr_old(self):
+		return self.ref_altcoin_addrgen(coin='XMR',mmtype='monero',add_args=['--use-old-ed25519'])
+
+	def ref_keyaddrfile_gen_eth(self):
+		return self.ref_altcoin_addrgen(coin='ETH',mmtype='ethereum',gen_what='key')
+
+	def ref_keyaddrfile_gen_etc(self):
+		return self.ref_altcoin_addrgen(coin='ETC',mmtype='ethereum',gen_what='key')
+
+	def ref_keyaddrfile_gen_dash(self):
+		return self.ref_altcoin_addrgen(coin='DASH',mmtype='compressed',gen_what='key')
+
+	def ref_keyaddrfile_gen_zec(self):
+		return self.ref_altcoin_addrgen(coin='ZEC',mmtype='compressed',gen_what='key')
+
+	def ref_keyaddrfile_gen_zec_z(self):
+		return self.ref_altcoin_addrgen(coin='ZEC',mmtype='zcash_z',coin_suf='_z',gen_what='key')
+
+	def ref_keyaddrfile_gen_xmr(self):
+		return self.ref_altcoin_addrgen(coin='XMR',mmtype='monero',gen_what='key')
+
+
+	def ref_addrfile_chk_eth(self):
+		return self.ref_addrfile_chk(ftype='addr',coin='ETH',subdir='ethereum',pfx='-ETH')
+
+	def ref_addrfile_chk_etc(self):
+		return self.ref_addrfile_chk(ftype='addr',coin='ETC',subdir='ethereum_classic',pfx='-ETC')
+
+	def ref_addrfile_chk_dash(self):
+		return self.ref_addrfile_chk(ftype='addr',coin='DASH',subdir='dash',pfx='-DASH-C')
+
+	def ref_addrfile_chk_zec(self):
+		return self.ref_addrfile_chk(ftype='addr',coin='ZEC',subdir='zcash',pfx='-ZEC-C')
+
+	def ref_addrfile_chk_zec_z(self):
+		if self.skip_for_win(): return 'skip'
+		return self.ref_addrfile_chk(ftype='addr',coin='ZEC',subdir='zcash',pfx='-ZEC-Z',mmtype='z')
+
+	def ref_addrfile_chk_xmr(self):
+		return self.ref_addrfile_chk(ftype='addr',coin='XMR',subdir='monero',pfx='-XMR-M')
+
+
+	def ref_keyaddrfile_chk_eth(self):
+		return self.ref_addrfile_chk(ftype='keyaddr',coin='ETH',subdir='ethereum',pfx='-ETH')
+
+	def ref_keyaddrfile_chk_etc(self):
+		return self.ref_addrfile_chk(ftype='keyaddr',coin='ETC',subdir='ethereum_classic',pfx='-ETC')
+
+	def ref_keyaddrfile_chk_dash(self):
+		return self.ref_addrfile_chk(ftype='keyaddr',coin='DASH',subdir='dash',pfx='-DASH-C')
+
+	def ref_keyaddrfile_chk_zec(self):
+		return self.ref_addrfile_chk(ftype='keyaddr',coin='ZEC',subdir='zcash',pfx='-ZEC-C')
+
+	def ref_keyaddrfile_chk_zec_z(self):
+		if self.skip_for_win(): return 'skip'
+		return self.ref_addrfile_chk(ftype='keyaddr',coin='ZEC',subdir='zcash',pfx='-ZEC-Z',mmtype='z')
+
+	def ref_keyaddrfile_chk_xmr(self):
+		return self.ref_addrfile_chk(ftype='keyaddr',coin='XMR',subdir='monero',pfx='-XMR-M')
+
+

+ 598 - 0
test/test_py_d/ts_regtest.py

@@ -0,0 +1,598 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
+# Copyright (C)2013-2019 The MMGen Project <mmgen@tuta.io>
+#
+# 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/>.
+
+"""
+ts_regtest.py: Regtest tests for the test.py test suite
+"""
+
+import os,subprocess
+from decimal import Decimal
+from mmgen.globalvars import g
+from mmgen.opts import opt
+from mmgen.util import die,gmsg,write_data_to_file
+from mmgen.protocol import CoinProtocol
+from mmgen.addr import AddrList
+from test.common import *
+from test.test_py_d.common import *
+
+rt_pw = 'abc-α'
+rt_data = {
+	'tx_fee': {'btc':'0.0001','bch':'0.001','ltc':'0.01'},
+	'rtFundAmt': {'btc':'500','bch':'500','ltc':'5500'},
+	'rtFee': {
+		'btc': ('20s','10s','60s','31s','10s','20s'),
+		'bch': ('20s','10s','60s','0.0001','10s','20s'),
+		'ltc': ('1000s','500s','1500s','0.05','400s','1000s')
+	},
+	'rtBals': {
+		'btc': ('499.9999488','399.9998282','399.9998147','399.9996877',
+				'52.99990000','946.99933647','999.99923647','52.9999',
+				'946.99933647'),
+		'bch': ('499.9999484','399.9999194','399.9998972','399.9997692',
+				'46.78900000','953.20966920','999.99866920','46.789',
+				'953.2096692'),
+		'ltc': ('5499.99744','5399.994425','5399.993885','5399.987535',
+				'52.99000000','10946.93753500','10999.92753500','52.99',
+				'10946.937535'),
+	},
+	'rtBals_gb': {
+		'btc': ('116.77629233','283.22339537'),
+		'bch': ('116.77637483','283.22339437'),
+		'ltc': ('5116.77036263','283.21717237')
+	},
+	'rtBobOp3': {'btc':'S:2','bch':'L:3','ltc':'S:2'},
+	'rtAmts': {
+		'btc': ('500',),
+		'bch': ('500',),
+		'ltc': ('5500',)
+	}
+}
+
+from test.test_py_d.ts_base import *
+from test.test_py_d.ts_shared import *
+
+class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
+	'transacting and tracking wallet operations via regtest mode'
+	networks = ('btc','ltc','bch')
+	passthru_opts = ('coin',)
+	tmpdir_nums = [17]
+	cmd_group = (
+		('setup',                    'regtest (Bob and Alice) mode setup'),
+		('walletgen_bob',            'wallet generation (Bob)'),
+		('walletgen_alice',          'wallet generation (Alice)'),
+		('addrgen_bob',              'address generation (Bob)'),
+		('addrgen_alice',            'address generation (Alice)'),
+		('addrimport_bob',           "importing Bob's addresses"),
+		('addrimport_alice',         "importing Alice's addresses"),
+		('fund_bob',                 "funding Bob's wallet"),
+		('fund_alice',               "funding Alice's wallet"),
+		('bob_bal1',                 "Bob's balance"),
+		('bob_add_label',            "adding a 40-character UTF-8 encoded label"),
+		('bob_twview',               "viewing Bob's tracking wallet"),
+		('bob_split1',               "splitting Bob's funds"),
+		('generate',                 'mining a block'),
+		('bob_bal2',                 "Bob's balance"),
+		('bob_bal2a',                "Bob's balance (age_fmt=confs)"),
+		('bob_bal2b',                "Bob's balance (showempty=1)"),
+		('bob_bal2c',                "Bob's balance (showempty=1 minconf=2 age_fmt=days)"),
+		('bob_bal2d',                "Bob's balance (minconf=2)"),
+		('bob_bal2e',                "Bob's balance (showempty=1 sort=age)"),
+		('bob_bal2f',                "Bob's balance (showempty=1 sort=age,reverse)"),
+		('bob_rbf_send',             'sending funds to Alice (RBF)'),
+		('get_mempool1',             'mempool (before RBF bump)'),
+		('bob_rbf_bump',             'bumping RBF transaction'),
+		('get_mempool2',             'mempool (after RBF bump)'),
+		('generate',                 'mining a block'),
+		('bob_bal3',                 "Bob's balance"),
+		('bob_pre_import',           'sending to non-imported address'),
+		('generate',                 'mining a block'),
+		('bob_import_addr',          'importing non-MMGen address with --rescan'),
+		('bob_bal4',                 "Bob's balance (after import with rescan)"),
+		('bob_import_list',          'importing flat address list'),
+		('bob_split2',               "splitting Bob's funds"),
+		('generate',                 'mining a block'),
+		('bob_bal5',                 "Bob's balance"),
+		('bob_bal5_getbalance',      "Bob's balance"),
+		('bob_send_non_mmgen',       'sending funds to Alice (from non-MMGen addrs)'),
+		('generate',                 'mining a block'),
+		('alice_add_label1',         'adding a label'),
+		('alice_chk_label1',         'the label'),
+		('alice_add_label2',         'adding a label'),
+		('alice_chk_label2',         'the label'),
+		('alice_edit_label1',        'editing a label'),
+		('alice_chk_label3',         'the label'),
+		('alice_remove_label1',      'removing a label'),
+		('alice_chk_label4',         'the label'),
+		('alice_add_label_coinaddr', 'adding a label using the coin address'),
+		('alice_chk_label_coinaddr', 'the label'),
+		('alice_add_label_badaddr1', 'adding a label with invalid address'),
+		('alice_add_label_badaddr2', 'adding a label with invalid address for this chain'),
+		('alice_add_label_badaddr3', 'adding a label with wrong MMGen address'),
+		('alice_add_label_badaddr4', 'adding a label with wrong coin address'),
+		('alice_bal_rpcfail',        'RPC failure code'),
+		('alice_send_estimatefee',   'tx creation with no fee on command line'),
+		('generate',                 'mining a block'),
+		('bob_bal6',                 "Bob's balance"),
+		('bob_alice_bal',            "Bob and Alice's balances"),
+		('alice_bal2',               "Alice's balance"),
+		('stop',                     'stopping regtest daemon'),
+	)
+
+	def __init__(self,trunner,cfgs,spawn):
+		coin = g.coin.lower()
+		for k in rt_data:
+			globals()[k] = rt_data[k][coin] if coin in rt_data[k] else None
+		return TestSuiteBase.__init__(self,trunner,cfgs,spawn)
+
+	def _add_comments_to_addr_file(self,addrfile,outfile,use_labels=False):
+		silence()
+		gmsg("Adding comments to address file '{}'".format(addrfile))
+		a = AddrList(addrfile)
+		for n,idx in enumerate(a.idxs(),1):
+			if use_labels:
+				a.set_comment(idx,get_label())
+			else:
+				if n % 2: a.set_comment(idx,'Test address {}'.format(n))
+		a.format(enable_comments=True)
+		write_data_to_file(outfile,a.fmt_data,silent=True,ignore_opt_outdir=True)
+		end_silence()
+
+	def setup(self):
+		os.environ['MMGEN_BOGUS_WALLET_DATA'] = ''
+		if g.testnet:
+			die(2,'--testnet option incompatible with regtest test suite')
+		try: shutil.rmtree(joinpath(self.tr.data_dir,'regtest'))
+		except: pass
+		os.environ['MMGEN_TEST_SUITE'] = '' # mnemonic is piped to stdin, so stop being a terminal
+		t = self.spawn('mmgen-regtest',['-n','setup'])
+		os.environ['MMGEN_TEST_SUITE'] = '1'
+		for s in ('Starting setup','Creating','Mined','Creating','Creating','Setup complete'):
+			t.expect(s)
+		return t
+
+	def walletgen(self,user):
+		t = self.spawn('mmgen-walletgen',['-q','-r0','-p1','--'+user])
+		t.passphrase_new('new MMGen wallet',rt_pw)
+		t.label()
+		t.expect('move it to the data directory? (Y/n): ','y')
+		t.written_to_file('MMGen wallet')
+		return t
+
+	def walletgen_bob(self):   return self.walletgen('bob')
+	def walletgen_alice(self): return self.walletgen('alice')
+
+	def _user_dir(self,user,coin=None):
+		return joinpath(self.tr.data_dir,'regtest',coin or g.coin.lower(),user)
+
+	def _user_sid(self,user):
+		return os.path.basename(get_file_with_ext(self._user_dir(user),'mmdat'))[:8]
+
+	def addrgen(self,user,wf=None,addr_range='1-5'):
+		from mmgen.addr import MMGenAddrType
+		for mmtype in g.proto.mmtypes:
+			t = self.spawn('mmgen-addrgen',
+				['--quiet','--'+user,'--type='+mmtype,'--outdir={}'.format(self._user_dir(user))] +
+				([],[wf])[bool(wf)] + [addr_range],
+				extra_desc='({})'.format(MMGenAddrType.mmtypes[mmtype]['name']))
+			t.passphrase('MMGen wallet',rt_pw)
+			t.written_to_file('Addresses')
+			ok_msg()
+		t.skip_ok = True
+		return t
+
+	def addrgen_bob(self):   return self.addrgen('bob')
+	def addrgen_alice(self): return self.addrgen('alice')
+
+	def addrimport(self,user,sid=None,addr_range='1-5',num_addrs=5):
+		id_strs = { 'legacy':'', 'compressed':'-C', 'segwit':'-S', 'bech32':'-B' }
+		if not sid: sid = self._user_sid(user)
+		from mmgen.addr import MMGenAddrType
+		for mmtype in g.proto.mmtypes:
+			desc = MMGenAddrType.mmtypes[mmtype]['name']
+			addrfile = joinpath(self._user_dir(user),
+				'{}{}{}[{}]{x}.testnet.addrs'.format(
+					sid,self.altcoin_pfx,id_strs[desc],addr_range,
+					x='-α' if g.debug_utf8 else ''))
+			if mmtype == g.proto.mmtypes[0] and user == 'bob':
+				psave = g.proto
+				g.proto = CoinProtocol(g.coin,True)
+				self._add_comments_to_addr_file(addrfile,addrfile,use_labels=True)
+				g.proto = psave
+			t = self.spawn( 'mmgen-addrimport',
+							['--quiet', '--'+user, '--batch', addrfile],
+							extra_desc='({})'.format(desc))
+			if g.debug:
+				t.expect("Type uppercase 'YES' to confirm: ",'YES\n')
+			t.expect('Importing')
+			t.expect('{} addresses imported'.format(num_addrs))
+			ok_msg()
+
+		t.skip_ok = True
+		return t
+
+	def addrimport_bob(self):   return self.addrimport('bob')
+	def addrimport_alice(self): return self.addrimport('alice')
+
+	def fund_wallet(self,user,mmtype,amt,sid=None,addr_range='1-5'):
+		if not sid: sid = self._user_sid(user)
+		addr = self.get_addr_from_addrlist(user,sid,mmtype,0,addr_range=addr_range)
+		t = self.spawn('mmgen-regtest', ['send',str(addr),str(amt)])
+		t.expect('Sending {} {}'.format(amt,g.coin))
+		t.expect('Mined 1 block')
+		return t
+
+	def fund_bob(self):   return self.fund_wallet('bob','C',rtFundAmt)
+	def fund_alice(self): return self.fund_wallet('alice',('L','S')[g.proto.cap('segwit')],rtFundAmt)
+
+	def user_twview(self,user):
+		t = self.spawn('mmgen-tool',['--'+user,'twview'])
+		t.expect(r'1\).*\b{}\b'.format(rtAmts[0]),regex=True)
+		t.read()
+		return t
+
+	def bob_twview(self):
+		return self.user_twview('bob')
+
+	def user_bal(self,user,bal,args=['showempty=1'],skip_check=False,exit_val=0):
+		t = self.spawn('mmgen-tool',['--'+user,'listaddresses'] + args)
+		if skip_check:
+			t.read()
+		else:
+			total = t.expect_getend('TOTAL: ')
+			cmp_or_die('{} {}'.format(bal,g.coin),total)
+		t.req_exit_val = exit_val
+		return t
+
+	def alice_bal1(self):
+		return self.user_bal('alice',rtFundAmt)
+
+	def alice_bal2(self):
+		return self.user_bal('alice',rtBals[8])
+
+	def bob_bal1(self):
+		return self.user_bal('bob',rtFundAmt)
+
+	def bob_bal2(self):
+		return self.user_bal('bob',rtBals[0])
+
+	def bob_bal2a(self):
+		return self.user_bal('bob',rtBals[0],args=['showempty=1','age_fmt=confs'])
+
+	def bob_bal2b(self):
+		return self.user_bal('bob',rtBals[0],args=['showempty=1'])
+
+	def bob_bal2c(self):
+		return self.user_bal('bob',rtBals[0],args=['showempty=1','minconf=2','age_fmt=days'],skip_check=True)
+
+	def bob_bal2d(self):
+		return self.user_bal('bob',rtBals[0],args=['minconf=2'],skip_check=True)
+
+	def bob_bal2e(self):
+		return self.user_bal('bob',rtBals[0],args=['showempty=1','sort=age'])
+
+	def bob_bal2f(self):
+		return self.user_bal('bob',rtBals[0],args=['showempty=1','sort=age,reverse'])
+
+	def bob_bal3(self):
+		return self.user_bal('bob',rtBals[1])
+
+	def bob_bal4(self):
+		return self.user_bal('bob',rtBals[2])
+
+	def bob_bal5(self):
+		return self.user_bal('bob',rtBals[3])
+
+	def bob_bal6(self):
+		return self.user_bal('bob',rtBals[7])
+
+	def bob_bal5_getbalance(self):
+		t_ext,t_mmgen = rtBals_gb[0],rtBals_gb[1]
+		assert Decimal(t_ext) + Decimal(t_mmgen) == Decimal(rtBals[3])
+		t = self.spawn('mmgen-tool',['--bob','getbalance'])
+		t.expect(r'\n[0-9A-F]{8}: .* '+t_mmgen,regex=True)
+		t.expect(r'\nNon-MMGen: .* '+t_ext,regex=True)
+		t.expect(r'\nTOTAL: .* '+rtBals[3],regex=True)
+		t.read()
+		return t
+
+	def bob_alice_bal(self):
+		t = self.spawn('mmgen-regtest',['get_balances'])
+		t.expect('Switching')
+		ret = t.expect_getend("Bob's balance:").strip()
+		cmp_or_die(rtBals[4],ret)
+		ret = t.expect_getend("Alice's balance:").strip()
+		cmp_or_die(rtBals[5],ret)
+		ret = t.expect_getend("Total balance:").strip()
+		cmp_or_die(rtBals[6],ret)
+		return t
+
+	def user_txdo(  self, user, fee, outputs_cl, outputs_list,
+					extra_args   = [],
+					wf           = None,
+					do_label     = False,
+					bad_locktime = False,
+					full_tx_view = False ):
+		os.environ['MMGEN_BOGUS_SEND'] = ''
+		t = self.spawn('mmgen-txdo',
+			['-d',self.tmpdir,'-B','--'+user] +
+			(['--tx-fee='+fee] if fee else []) +
+			extra_args + ([],[wf])[bool(wf)] + outputs_cl)
+		os.environ['MMGEN_BOGUS_SEND'] = '1'
+
+		self.txcreate_ui_common(t,'txdo',
+								menu            = ['M'],
+								inputs          = outputs_list,
+								file_desc       = 'Signed transaction',
+								interactive_fee = (tx_fee,'')[bool(fee)],
+								add_comment     = ref_tx_label_jp,
+								view            = 't',save=True)
+
+		t.passphrase('MMGen wallet',rt_pw)
+		t.written_to_file('Signed transaction')
+		self._do_confirm_send(t)
+		s,exit_val = (('Transaction sent',0),("can't be included",1))[bad_locktime]
+		t.expect(s)
+		t.req_exit_val = exit_val
+		return t
+
+	def bob_split1(self):
+		sid = self._user_sid('bob')
+		outputs_cl = [sid+':C:1,100', sid+':L:2,200',sid+':'+rtBobOp3]
+		return self.user_txdo('bob',rtFee[0],outputs_cl,'1',do_label=True,full_tx_view=True)
+
+	def get_addr_from_addrlist(self,user,sid,mmtype,idx,addr_range='1-5'):
+		id_str = { 'L':'', 'S':'-S', 'C':'-C', 'B':'-B' }[mmtype]
+		ext = '{}{}{}[{}]{x}.testnet.addrs'.format(
+			sid,self.altcoin_pfx,id_str,addr_range,x='-α' if g.debug_utf8 else '')
+		addrfile = get_file_with_ext(self._user_dir(user),ext,no_dot=True)
+		psave = g.proto
+		g.proto = CoinProtocol(g.coin,True)
+		if hasattr(g.proto,'bech32_hrp_rt'):
+			g.proto.bech32_hrp = g.proto.bech32_hrp_rt
+		silence()
+		addr = AddrList(addrfile).data[idx].addr
+		end_silence()
+		g.proto = psave
+		return addr
+
+	def _create_tx_outputs(self,user,data):
+		sid = self._user_sid(user)
+		return [self.get_addr_from_addrlist(user,sid,mmtype,idx-1)+amt_str for mmtype,idx,amt_str in data]
+
+	def bob_rbf_send(self):
+		outputs_cl = self._create_tx_outputs('alice',(('L',1,',60'),('C',1,',40'))) # alice_sid:L:1, alice_sid:C:1
+		outputs_cl += [self._user_sid('bob')+':'+rtBobOp3]
+		return self.user_txdo('bob',rtFee[1],outputs_cl,'3',
+					extra_args=([],['--rbf'])[g.proto.cap('rbf')])
+
+	def bob_send_non_mmgen(self):
+		outputs_cl = self._create_tx_outputs('alice',(
+			(('L','S')[g.proto.cap('segwit')],2,',10'),
+			(('L','S')[g.proto.cap('segwit')],3,'')
+		)) # alice_sid:S:2, alice_sid:S:3
+		keyfile = joinpath(self.tmpdir,'non-mmgen.keys')
+		return self.user_txdo('bob',rtFee[3],outputs_cl,'1,4-10',
+			extra_args=['--keys-from-file='+keyfile,'--vsize-adj=1.02'])
+
+	def alice_send_estimatefee(self):
+		outputs_cl = self._create_tx_outputs('bob',(('L',1,''),)) # bob_sid:L:1
+		return self.user_txdo('alice',None,outputs_cl,'1') # fee=None
+
+	def user_txbump(self,user,txfile,fee,red_op):
+		if not g.proto.cap('rbf'):
+			msg('Skipping RBF'); return 'skip'
+		os.environ['MMGEN_BOGUS_SEND'] = ''
+		t = self.spawn('mmgen-txbump',
+			['-d',self.tmpdir,'--send','--'+user,'--tx-fee='+fee,'--output-to-reduce='+red_op] + [txfile])
+		os.environ['MMGEN_BOGUS_SEND'] = '1'
+		t.expect('OK? (Y/n): ','y') # output OK?
+		t.expect('OK? (Y/n): ','y') # fee OK?
+		t.do_comment(False,has_label=True)
+		t.passphrase('MMGen wallet',rt_pw)
+		t.written_to_file('Signed transaction')
+		self.txsend_ui_common(t,'txdo',bogus_send=False,file_desc='Signed transaction')
+		t.read()
+		return t
+
+	def bob_rbf_bump(self):
+		ext = ',{}]{x}.testnet.sigtx'.format(rtFee[1][:-1],x='-α' if g.debug_utf8 else '')
+		txfile = self.get_file_with_ext(ext,delete=False,no_dot=True)
+		return self.user_txbump('bob',txfile,rtFee[2],'c')
+
+	def generate(self,coin=None,num_blocks=1):
+		int(num_blocks)
+		if coin: opt.coin = coin
+		t = self.spawn('mmgen-regtest',['generate',str(num_blocks)])
+		t.expect('Mined {} block'.format(num_blocks))
+		return t
+
+	def _get_mempool(self):
+		disable_debug()
+		ret = self.spawn('mmgen-regtest',['show_mempool']).read()
+		restore_debug()
+		from ast import literal_eval
+		return literal_eval(ret.split('\n')[0]) # allow for extra output by handler at end
+
+	def get_mempool1(self):
+		mp = self._get_mempool()
+		if len(mp) != 1:
+			rdie(2,'Mempool has more or less than one TX!')
+		self.write_to_tmpfile('rbf_txid',mp[0]+'\n')
+		return 'ok'
+
+	def get_mempool2(self):
+		if not g.proto.cap('rbf'):
+			msg('Skipping post-RBF mempool check'); return 'skip'
+		mp = self._get_mempool()
+		if len(mp) != 1:
+			rdie(2,'Mempool has more or less than one TX!')
+		chk = self.read_from_tmpfile('rbf_txid')
+		if chk.strip() == mp[0]:
+			rdie(2,'TX in mempool has not changed!  RBF bump failed')
+		return 'ok'
+
+	@staticmethod
+	def _gen_pairs(n):
+		disable_debug()
+		ret = [subprocess.check_output(
+						['python3',joinpath('cmds','mmgen-tool'),'--testnet=1'] +
+						(['--type=compressed'],[])[i==0] +
+						['-r0','randpair']
+					).decode().split() for i in range(n)]
+		restore_debug()
+		return ret
+
+	def bob_pre_import(self):
+		pairs = self._gen_pairs(5)
+		self.write_to_tmpfile('non-mmgen.keys','\n'.join([a[0] for a in pairs])+'\n')
+		self.write_to_tmpfile('non-mmgen.addrs','\n'.join([a[1] for a in pairs])+'\n')
+		return self.user_txdo('bob',rtFee[4],[pairs[0][1]],'3')
+
+	def user_import(self,user,args):
+		t = self.spawn('mmgen-addrimport',['--quiet','--'+user]+args)
+		if g.debug:
+			t.expect("Type uppercase 'YES' to confirm: ",'YES\n')
+		t.expect('Importing')
+		t.expect('OK')
+		return t
+
+	def bob_import_addr(self):
+		addr = self.read_from_tmpfile('non-mmgen.addrs').split()[0]
+		return self.user_import('bob',['--rescan','--address='+addr])
+
+	def bob_import_list(self):
+		addrfile = joinpath(self.tmpdir,'non-mmgen.addrs')
+		return self.user_import('bob',['--addrlist',addrfile])
+
+	def bob_split2(self):
+		addrs = self.read_from_tmpfile('non-mmgen.addrs').split()
+		amts = (1.12345678,2.87654321,3.33443344,4.00990099,5.43214321)
+		outputs1 = list(map('{},{}'.format,addrs,amts))
+		sid = self._user_sid('bob')
+		l1,l2 = (':S',':B') if 'B' in g.proto.mmtypes else (':S',':S') if g.proto.cap('segwit') else (':L',':L')
+		outputs2 = [sid+':C:2,6.333', sid+':L:3,6.667',sid+l1+':4,0.123',sid+l2+':5']
+		return self.user_txdo('bob',rtFee[5],outputs1+outputs2,'1-2')
+
+	def user_add_label(self,user,addr,label):
+		t = self.spawn('mmgen-tool',['--'+user,'add_label',addr,label])
+		t.expect('Added label.*in tracking wallet',regex=True)
+		return t
+
+	def user_remove_label(self,user,addr):
+		t = self.spawn('mmgen-tool',['--'+user,'remove_label',addr])
+		t.expect('Removed label.*in tracking wallet',regex=True)
+		return t
+
+	def bob_add_label(self):
+		sid = self._user_sid('bob')
+		return self.user_add_label('bob',sid+':C:1',utf8_label)
+
+	def alice_add_label1(self):
+		sid = self._user_sid('alice')
+		return self.user_add_label('alice',sid+':C:1','Original Label - 月へ')
+
+	def alice_add_label2(self):
+		sid = self._user_sid('alice')
+		return self.user_add_label('alice',sid+':C:1','Replacement Label')
+
+	def alice_add_label_coinaddr(self):
+		mmaddr = self._user_sid('alice') + ':C:2'
+		t = self.spawn('mmgen-tool',['--alice','listaddress',mmaddr],no_msg=True)
+		btcaddr = [i for i in t.read().splitlines() if i.lstrip()[0:len(mmaddr)] == mmaddr][0].split()[1]
+		return self.user_add_label('alice',btcaddr,'Label added using coin address')
+
+	def alice_chk_label_coinaddr(self):
+		sid = self._user_sid('alice')
+		return self.user_chk_label('alice',sid+':C:2','Label added using coin address')
+
+	def alice_add_label_badaddr(self,addr,reply):
+		t = self.spawn('mmgen-tool',['--alice','add_label',addr,'(none)'])
+		t.expect(reply,regex=True)
+		return t
+
+	def alice_add_label_badaddr1(self):
+		return self.alice_add_label_badaddr(rt_pw,'Invalid coin address for this chain: '+rt_pw)
+
+	def alice_add_label_badaddr2(self):
+		addr = g.proto.pubhash2addr(b'00'*20,False) # mainnet zero address
+		return self.alice_add_label_badaddr(addr,'Invalid coin address for this chain: '+addr)
+
+	def alice_add_label_badaddr3(self):
+		addr = self._user_sid('alice') + ':C:123'
+		return self.alice_add_label_badaddr(addr,
+			"MMGen address '{}' not found in tracking wallet".format(addr))
+
+	def alice_add_label_badaddr4(self):
+		addr = CoinProtocol(g.coin,True).pubhash2addr(b'00'*20,False) # testnet zero address
+		return self.alice_add_label_badaddr(addr,
+			"Address '{}' not found in tracking wallet".format(addr))
+
+	def alice_bal_rpcfail(self):
+		addr = self._user_sid('alice') + ':C:2'
+		os.environ['MMGEN_RPC_FAIL_ON_COMMAND'] = 'listunspent'
+		t = self.spawn('mmgen-tool',['--alice','getbalance'])
+		os.environ['MMGEN_RPC_FAIL_ON_COMMAND'] = ''
+		t.expect('Method not found')
+		t.read()
+		t.req_exit_val = 3
+		return t
+
+	def alice_remove_label1(self):
+		sid = self._user_sid('alice')
+		return self.user_remove_label('alice',sid+':C:1')
+
+	def user_chk_label(self,user,addr,label,label_pat=None):
+		t = self.spawn('mmgen-tool',['--'+user,'listaddresses','all_labels=1'])
+		t.expect(r'{}\s+\S{{30}}\S+\s+{}\s+'.format(addr,(label_pat or label)),regex=True)
+		return t
+
+	def alice_chk_label1(self):
+		sid = self._user_sid('alice')
+		return self.user_chk_label('alice',sid+':C:1','Original Label - 月へ')
+
+	def alice_chk_label2(self):
+		sid = self._user_sid('alice')
+		return self.user_chk_label('alice',sid+':C:1','Replacement Label')
+
+	def alice_edit_label1(self):
+		return self.user_edit_label('alice','1',utf8_label)
+
+	def alice_chk_label3(self):
+		sid = self._user_sid('alice')
+		return self.user_chk_label('alice',sid+':C:1',utf8_label,label_pat=utf8_label_pat)
+
+	def alice_chk_label4(self):
+		sid = self._user_sid('alice')
+		return self.user_chk_label('alice',sid+':C:1','-')
+
+	def user_edit_label(self,user,output,label):
+		t = self.spawn('mmgen-txcreate',['-B','--'+user,'-i'])
+		t.expect(r'add \[l\]abel:.','M',regex=True)
+		t.expect(r'add \[l\]abel:.','l',regex=True)
+		t.expect(r"Enter unspent.*return to main menu\):.",output+'\n',regex=True)
+		t.expect(r"Enter label text.*return to main menu\):.",label+'\n',regex=True)
+		t.expect(r'\[q\]uit view, .*?:.','q',regex=True)
+		return t
+
+	def stop(self):
+		if opt.no_daemon_stop:
+			self.spawn('',msg_only=True)
+			msg_r('(leaving daemon running by user request)')
+			return 'ok'
+		else:
+			return self.spawn('mmgen-regtest',['stop'])

+ 223 - 0
test/test_py_d/ts_shared.py

@@ -0,0 +1,223 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
+# Copyright (C)2013-2019 The MMGen Project <mmgen@tuta.io>
+#
+# 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/>.
+
+"""
+ts_shared.py: Shared methods for the test.py test suite
+"""
+
+import os
+from mmgen.globalvars import g
+from mmgen.opts import opt
+from mmgen.util import ymsg
+from test.test_py_d.common import *
+from test.common import *
+
+class TestSuiteShared(object):
+	'shared methods for the test.py test suite'
+
+	def txcreate_ui_common(self,t,caller,
+							menu=[],inputs='1',
+							file_desc='Transaction',
+							input_sels_prompt='to spend',
+							bad_input_sels=False,non_mmgen_inputs=0,
+							interactive_fee='',
+							fee_desc='transaction fee',fee_res=None,eth_fee_res=None,
+							add_comment='',view='t',save=True):
+		for choice in menu + ['q']:
+			t.expect(r'\[q\]uit view, .*?:.',choice,regex=True)
+		if bad_input_sels:
+			for r in ('x','3-1','9999'):
+				t.expect(input_sels_prompt+': ',r+'\n')
+		t.expect(input_sels_prompt+': ',inputs+'\n')
+
+		if not caller[:4] == 'txdo':
+			for i in range(non_mmgen_inputs):
+				t.expect('Accept? (y/N): ','y')
+
+		have_est_fee = t.expect([fee_desc+': ','OK? (Y/n): ']) == 1
+		if have_est_fee and not interactive_fee:
+			t.send('y')
+		else:
+			if have_est_fee: t.send('n')
+			if eth_fee_res:
+				t.expect('or gas price: ',interactive_fee+'\n')
+			else:
+				t.send(interactive_fee+'\n')
+			if fee_res: t.expect(fee_res)
+			t.expect('OK? (Y/n): ','y')
+
+		t.expect('(Y/n): ','\n')     # chg amt OK?
+		t.do_comment(add_comment)
+		t.view_tx(view)
+		if not caller[:4] == 'txdo':
+			t.expect('(y/N): ',('n','y')[save])
+			t.written_to_file(file_desc)
+
+		return t
+
+	def txsign_ui_common(   self,t,caller,
+							view        = 't',
+							add_comment = '',
+							file_desc   = 'Signed transaction',
+							ni          = False,
+							save        = True,
+							do_passwd   = False,
+							has_label   = False ):
+		txdo = caller[:4] == 'txdo'
+
+		if do_passwd:
+			t.passphrase('MMGen wallet',self.wpasswd)
+
+		if not ni and not txdo:
+			t.view_tx(view)
+			t.do_comment(add_comment,has_label=has_label)
+			t.expect('(Y/n): ',('n','y')[save])
+
+		t.written_to_file(file_desc)
+
+		return t
+
+	def txsend_ui_common(   self,t,caller,
+							view         = 'n',
+							add_comment  = '',
+							file_desc    = 'Sent transaction',
+							confirm_send = True,
+							bogus_send   = True,
+							quiet        = False,
+							has_label    = False ):
+
+		txdo = caller[:4] == 'txdo'
+		if not txdo:
+			t.license() # MMGEN_NO_LICENSE is set, so does nothing
+			t.view_tx(view)
+			t.do_comment(add_comment,has_label=has_label)
+
+		self._do_confirm_send(t,quiet=quiet,confirm_send=confirm_send)
+
+		if bogus_send:
+			txid = ''
+			t.expect('BOGUS transaction NOT sent')
+		else:
+			txid = t.expect_getend('Transaction sent: ')
+			assert len(txid) == 64,"'{}': Incorrect txid length!".format(txid)
+
+		t.written_to_file(file_desc)
+
+		return txid
+
+	def txsign_end(self,t,tnum=None,has_label=False):
+		t.expect('Signing transaction')
+		t.do_comment(False,has_label=has_label)
+		t.expect('Save signed transaction.*?\? \(Y/n\): ','y',regex=True)
+		t.written_to_file('Signed transaction' + (' #' + tnum if tnum else ''), oo=True)
+		return t
+
+	def txsign( self, txfile, wf,
+				pf         = '',
+				bumpf      = '',
+				save       = True,
+				has_label  = False,
+				do_passwd  = True,
+				extra_opts = [],
+				extra_desc = '' ):
+		opts = extra_opts + ['-d',self.tmpdir,txfile] + ([wf] if wf else [])
+		t = self.spawn('mmgen-txsign', opts, extra_desc)
+		t.license()
+		t.view_tx('n')
+		if do_passwd: t.passphrase('MMGen wallet',self.wpasswd)
+		if save:
+			self.txsign_end(t,has_label=has_label)
+		else:
+			t.do_comment(False,has_label=has_label)
+			t.expect('Save signed transaction? (Y/n): ','n')
+			t.req_exit_val = 1
+		return t
+
+	def ref_brain_chk(self,bw_file=ref_bw_file):
+		wf = joinpath(ref_dir,bw_file)
+		add_args = ['-l{}'.format(self.seed_len), '-p'+ref_bw_hash_preset]
+		return self.walletchk(wf,pf=None,add_args=add_args,
+			desc='brainwallet',sid=self.ref_bw_seed_id)
+
+	def walletchk(self,wf,pf,desc='MMGen wallet',add_args=[],sid=None,pw=False,extra_desc=''):
+		args = []
+		hp = self.hash_preset if hasattr(self,'hash_preset') else '1'
+		wf_arg = [wf] if wf else []
+		t = self.spawn('mmgen-walletchk',
+				add_args+args+['-p',hp]+wf_arg,
+				extra_desc=extra_desc)
+		if desc != 'hidden incognito data':
+			t.expect("Getting {} from file '".format(desc))
+		if pw:
+			t.passphrase(desc,self.wpasswd)
+			t.expect(['Passphrase is OK', 'Passphrase.* are correct'],regex=True)
+		chk = t.expect_getend('Valid {} for Seed ID '.format(desc))[:8]
+		if sid: cmp_or_die(chk,sid)
+		return t
+
+	def addrgen(self,wf,pf=None,check_ref=False,ftype='addr',id_str=None,extra_args=[],mmtype=None):
+		if not mmtype and ftype[:4] != 'pass':
+			mmtype = self.segwit_mmtype
+		cmd_pfx = (ftype,'pass')[ftype[:4]=='pass']
+		t = self.spawn('mmgen-{}gen'.format(cmd_pfx),
+				['-d',self.tmpdir] + extra_args +
+				([],['--type='+str(mmtype)])[bool(mmtype)] +
+				([],[wf])[bool(wf)] +
+				([],[id_str])[bool(id_str)] +
+				[getattr(self,'{}_idx_list'.format(cmd_pfx))],
+				extra_desc='({})'.format(mmtype) if mmtype in ('segwit','bech32') else '')
+		t.license()
+		t.passphrase('MMGen wallet',self.wpasswd)
+		t.expect('Passphrase is OK')
+		desc = ('address','password')[ftype[:4]=='pass']
+		chk = t.expect_getend(r'Checksum for {} data .*?: '.format(desc),regex=True)
+		if ftype[:4] == 'pass':
+			t.expect('Encrypt password list? (y/N): ','\n')
+			t.written_to_file('Password list',oo=True)
+		else:
+			t.written_to_file('Addresses',oo=True)
+		if check_ref:
+			chk_ref = (self.chk_data[self.test_name] if ftype[:4] == 'pass' else
+						self.chk_data[self.test_name][self.fork][g.testnet])
+			cmp_or_die(chk,chk_ref,desc='{}list data checksum'.format(ftype))
+		return t
+
+	def keyaddrgen(self,wf,pf=None,check_ref=False,mmtype=None):
+		if not mmtype:
+			mmtype = self.segwit_mmtype
+		args = ['-d',self.tmpdir,self.usr_rand_arg,wf,self.addr_idx_list]
+		t = self.spawn('mmgen-keygen',
+				([],['--type='+str(mmtype)])[bool(mmtype)] + args,
+				extra_desc='({})'.format(mmtype) if mmtype in ('segwit','bech32') else '')
+		t.license()
+		t.passphrase('MMGen wallet',self.wpasswd)
+		chk = t.expect_getend(r'Checksum for key-address data .*?: ',regex=True)
+		if check_ref:
+			chk_ref = self.chk_data[self.test_name][self.fork][g.testnet]
+			cmp_or_die(chk,chk_ref,desc='key-address list data checksum')
+		t.expect('Encrypt key list? (y/N): ','y')
+		t.usr_rand(self.usr_rand_chars)
+		t.hash_preset('new key list','1')
+		t.passphrase_new('new key list',self.kapasswd)
+		t.written_to_file('Encrypted secret keys',oo=True)
+		return t
+
+	def _do_confirm_send(self,t,quiet=False,confirm_send=True):
+		t.expect('Are you sure you want to broadcast this')
+		m = ('YES, I REALLY WANT TO DO THIS','YES')[quiet]
+		t.expect("'{}' to confirm: ".format(m),('',m)[confirm_send]+'\n')

+ 227 - 0
test/test_py_d/ts_wallet.py

@@ -0,0 +1,227 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
+# Copyright (C)2013-2019 The MMGen Project <mmgen@tuta.io>
+#
+# 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/>.
+
+"""
+ts_wallet.py: Wallet conversion tests for the test.py test suite
+"""
+
+import os
+from mmgen.opts import opt
+from test.test_py_d.common import *
+from test.test_py_d.ts_base import *
+from test.test_py_d.ts_shared import *
+
+class TestSuiteWalletConv(TestSuiteBase,TestSuiteShared):
+	'wallet conversion to and from reference data'
+	networks = ('btc',)
+	tmpdir_nums = [11,12,13]
+	sources = { '128': {
+					'ref_wallet':      'FE3C6545-D782B529[128,1].mmdat',
+					'ic_wallet':       'FE3C6545-E29303EA-5E229E30[128,1].mmincog',
+					'ic_wallet_hex':   'FE3C6545-BC4BE3F2-32586837[128,1].mmincox',
+
+					'hic_wallet':       'FE3C6545-161E495F-BEB7548E[128,1].incog-offset123',
+					'hic_wallet_old':   'FE3C6545-161E495F-9860A85B[128,1].incog-old.offset123',
+				},
+				'192': {
+					'ref_wallet':      '1378FC64-6F0F9BB4[192,1].mmdat',
+					'ic_wallet':       '1378FC64-2907DE97-F980D21F[192,1].mmincog',
+					'ic_wallet_hex':   '1378FC64-4DCB5174-872806A7[192,1].mmincox',
+
+					'hic_wallet':      '1378FC64-B55E9958-77256FC1[192,1].incog.offset123',
+					'hic_wallet_old':  '1378FC64-B55E9958-D85FF20C[192,1].incog-old.offset123',
+				},
+				'256': {
+					'ref_wallet':      '98831F3A-{}[256,1].mmdat'.format(('27F2BF93','E2687906')[g.testnet]),
+					'ic_wallet':       '98831F3A-5482381C-18460FB1[256,1].mmincog',
+					'ic_wallet_hex':   '98831F3A-1630A9F2-870376A9[256,1].mmincox',
+
+					'hic_wallet':       '98831F3A-F59B07A0-559CEF19[256,1].incog.offset123',
+					'hic_wallet_old':   '98831F3A-F59B07A0-848535F3[256,1].incog-old.offset123',
+
+				},
+			}
+	cmd_group = (
+		# reading
+		('ref_wallet_conv',    'conversion of saved reference wallet'),
+		('ref_mn_conv',        'conversion of saved mnemonic'),
+		('ref_seed_conv',      'conversion of saved seed file'),
+		('ref_hex_conv',       'conversion of saved hexadecimal seed file'),
+		('ref_brain_conv',     'conversion of ref brainwallet'),
+		('ref_incog_conv',     'conversion of saved incog wallet'),
+		('ref_incox_conv',     'conversion of saved hex incog wallet'),
+		('ref_hincog_conv',    'conversion of saved hidden incog wallet'),
+		('ref_hincog_conv_old','conversion of saved hidden incog wallet (old format)'),
+		# writing
+		('ref_wallet_conv_out', 'ref seed conversion to wallet'),
+		('ref_mn_conv_out',     'ref seed conversion to mnemonic'),
+		('ref_hex_conv_out',    'ref seed conversion to hex seed'),
+		('ref_seed_conv_out',   'ref seed conversion to seed'),
+		('ref_incog_conv_out',  'ref seed conversion to incog data'),
+		('ref_incox_conv_out',  'ref seed conversion to hex incog data'),
+		('ref_hincog_conv_out', 'ref seed conversion to hidden incog data'),
+		('ref_hincog_blkdev_conv_out', 'ref seed conversion to hidden incog data on block device')
+	)
+
+	def __init__(self,trunner,cfgs,spawn):
+		for k,j in self.cmd_group:
+			for n in (1,2,3):
+				setattr(self,'{}_{}'.format(k,n),getattr(self,k))
+		return TestSuiteBase.__init__(self,trunner,cfgs,spawn)
+
+	def ref_wallet_conv(self):
+		wf = joinpath(ref_dir,self.sources[str(self.seed_len)]['ref_wallet'])
+		return self.walletconv_in(wf,'MMGen wallet',pw=True,oo=True)
+
+	def ref_mn_conv(self,ext='mmwords',desc='Mnemonic data'):
+		wf = joinpath(ref_dir,self.seed_id+'.'+ext)
+		return self.walletconv_in(wf,desc,oo=True)
+
+	def ref_seed_conv(self):
+		return self.ref_mn_conv(ext='mmseed',desc='Seed data')
+
+	def ref_hex_conv(self):
+		return self.ref_mn_conv(ext='mmhex',desc='Hexadecimal seed data')
+
+	def ref_brain_conv(self):
+		uopts = ['-i','b','-p','1','-l',str(self.seed_len)]
+		return self.walletconv_in(None,'brainwallet',uopts,oo=True)
+
+	def ref_incog_conv(self,wfk='ic_wallet',in_fmt='i',desc='incognito data'):
+		uopts = ['-i',in_fmt,'-p','1','-l',str(self.seed_len)]
+		wf = joinpath(ref_dir,self.sources[str(self.seed_len)][wfk])
+		return self.walletconv_in(wf,desc,uopts,oo=True,pw=True)
+
+	def ref_incox_conv(self):
+		return self.ref_incog_conv(in_fmt='xi',wfk='ic_wallet_hex',desc='hex incognito data')
+
+	def ref_hincog_conv(self,wfk='hic_wallet',add_uopts=[]):
+		ic_f = joinpath(ref_dir,self.sources[str(self.seed_len)][wfk])
+		uopts = ['-i','hi','-p','1','-l',str(self.seed_len)] + add_uopts
+		hi_opt = ['-H','{},{}'.format(ic_f,ref_wallet_incog_offset)]
+		return self.walletconv_in(None,'hidden incognito data',uopts+hi_opt,oo=True,pw=True)
+
+	def ref_hincog_conv_old(self):
+		return self.ref_hincog_conv(wfk='hic_wallet_old',add_uopts=['-O'])
+
+	def ref_wallet_conv_out(self):
+		return self.walletconv_out('MMGen wallet','w',pw=True)
+
+	def ref_mn_conv_out(self):
+		return self.walletconv_out('mnemonic data','mn')
+
+	def ref_seed_conv_out(self):
+		return self.walletconv_out('seed data','seed')
+
+	def ref_hex_conv_out(self):
+		return self.walletconv_out('hexadecimal seed data','hexseed')
+
+	def ref_incog_conv_out(self):
+		return self.walletconv_out('incognito data',out_fmt='i',pw=True)
+
+	def ref_incox_conv_out(self):
+		return self.walletconv_out('hex incognito data',out_fmt='xi',pw=True)
+
+	def ref_hincog_conv_out(self,ic_f=None):
+		if not ic_f: ic_f = joinpath(self.tmpdir,hincog_fn)
+		hi_parms = '{},{}'.format(ic_f,ref_wallet_incog_offset)
+		sl_parm = '-l' + str(self.seed_len)
+		return self.walletconv_out( 'hidden incognito data','hi',
+									uopts     = ['-J',hi_parms,sl_parm],
+									uopts_chk = ['-H',hi_parms,sl_parm],
+									pw        = True )
+
+	def ref_hincog_blkdev_conv_out(self):
+		imsg('Creating block device image file')
+		ic_img = joinpath(self.tmpdir,'hincog_blkdev_img')
+		subprocess.check_output(['dd','if=/dev/zero','of='+ic_img,'bs=1K','count=1'],stderr=subprocess.PIPE)
+		ic_dev = subprocess.check_output(['losetup','-f']).strip().decode()
+		ic_dev_mode_orig = '{:o}'.format(os.stat(ic_dev).st_mode & 0xfff)
+		ic_dev_mode = '0666'
+		imsg("Changing permissions on loop device to '{}'".format(ic_dev_mode))
+		subprocess.check_output(['sudo','chmod',ic_dev_mode,ic_dev],stderr=subprocess.PIPE)
+		imsg("Attaching loop device '{}'".format(ic_dev))
+		subprocess.check_output(['losetup',ic_dev,ic_img])
+		self.ref_hincog_conv_out(ic_f=ic_dev)
+		imsg("Detaching loop device '{}'".format(ic_dev))
+		subprocess.check_output(['losetup','-d',ic_dev])
+		imsg("Resetting permissions on loop device to '{}'".format(ic_dev_mode_orig))
+		subprocess.check_output(['sudo','chmod',ic_dev_mode_orig,ic_dev],stderr=subprocess.PIPE)
+		return 'ok'
+
+	# wallet conversion tests
+	def walletconv_in(self,infile,desc,uopts=[],pw=False,oo=False):
+		opts = ['-d',self.tmpdir,'-o','words',self.usr_rand_arg]
+		if_arg = [infile] if infile else []
+		d = '(convert)'
+		t = self.spawn('mmgen-walletconv',opts+uopts+if_arg,extra_desc=d)
+		t.license()
+		if desc == 'brainwallet':
+			t.expect('Enter brainwallet: ',ref_wallet_brainpass+'\n')
+		if pw:
+			t.passphrase(desc,self.wpasswd)
+			if self.test_name[:19] == 'ref_hincog_conv_old':
+				t.expect('Is the Seed ID correct? (Y/n): ','\n')
+			else:
+				t.expect(['Passphrase is OK',' are correct'])
+		# Output
+		wf = t.written_to_file('Mnemonic data',oo=oo)
+		t.p.wait()
+		# back check of result
+		msg('' if opt.profile else ' OK')
+		return self.walletchk(  wf,
+								pf         = None,
+								extra_desc = '(check)',
+								desc       = 'mnemonic data',
+								sid        = self.seed_id )
+
+	def walletconv_out(self,desc,out_fmt='w',uopts=[],uopts_chk=[],pw=False):
+		opts = ['-d',self.tmpdir,'-p1','-o',out_fmt] + uopts
+		infile = joinpath(ref_dir,self.seed_id+'.mmwords')
+		t = self.spawn('mmgen-walletconv',[self.usr_rand_arg]+opts+[infile],extra_desc='(convert)')
+
+		add_args = ['-l{}'.format(self.seed_len)]
+		t.license()
+		if pw:
+			t.passphrase_new('new '+desc,self.wpasswd)
+			t.usr_rand(self.usr_rand_chars)
+		if ' '.join(desc.split()[-2:]) == 'incognito data':
+			for i in (1,2,3):
+				t.expect('Generating encryption key from OS random data ')
+		if desc == 'hidden incognito data':
+			ret = t.expect(['Create? (Y/n): ',"'YES' to confirm: "])
+			if ret == 0:
+				t.send('\n')
+				t.expect('Enter file size: ',str(hincog_bytes)+'\n')
+			else:
+				t.send('YES\n')
+		if out_fmt == 'w': t.label()
+		wf = t.written_to_file(capfirst(desc),oo=True)
+		pf = None
+
+		if desc == 'hidden incognito data':
+			add_args += uopts_chk
+			wf = None
+		msg('' if opt.profile else ' OK')
+		return self.walletchk(  wf,
+								pf         = pf,
+								pw         = pw,
+								desc       = desc,
+								extra_desc = '(check)',
+								sid        = self.seed_id,
+								add_args   = add_args )

+ 19 - 4
test/tooltest.py

@@ -29,7 +29,7 @@ os.environ['MMGEN_TEST_SUITE'] = '1'
 
 # Import this _after_ local path's been added to sys.path
 from mmgen.common import *
-from mmgen.test import *
+from test.common import *
 
 opts_data = lambda: {
 	'desc': "Test suite for the 'mmgen-tool' utility",
@@ -295,7 +295,7 @@ class MMGenToolTestUtils(object):
 		s = os.urandom(128)
 		fn = name+'.in'
 		write_to_tmpfile(cfg,fn,s,binary=True)
-		ret = self.run_cmd(name,[get_tmpfile_fn(cfg,fn)],strip=strip,add_opts=add_opts)
+		ret = self.run_cmd(name,[get_tmpfile(cfg,fn)],strip=strip,add_opts=add_opts)
 		fn = name+'.out'
 		write_to_tmpfile(cfg,fn,ret+'\n')
 		ok()
@@ -303,6 +303,14 @@ class MMGenToolTestUtils(object):
 
 tu = MMGenToolTestUtils()
 
+def ok_or_die(val,chk_func,s,skip_ok=False):
+	try: ret = chk_func(val)
+	except: ret = False
+	if ret:
+		if not skip_ok: ok()
+	else:
+		rdie(3,"Returned value '{}' is not a {}".format((val,s)))
+
 class MMGenToolTestCmds(object):
 
 	# Util
@@ -327,6 +335,7 @@ class MMGenToolTestCmds(object):
 		ret2 = tu.run_cmd(name,[s2],extra_msg='spaced input')
 		cmp_or_die(ret1,ret2)
 		vmsg('Returned: {}'.format(ret1))
+		ok()
 	def hash160(self,name):        tu.run_cmd_out(name,getrandhex(16))
 	def hash256(self,name):        tu.run_cmd_out(name,getrandstr(16))
 	def hexreverse(self,name):     tu.run_cmd_out(name,getrandhex(24))
@@ -336,12 +345,14 @@ class MMGenToolTestCmds(object):
 		ret = tu.run_cmd(name,[fn2],strip=False,binary=True)
 		orig = read_from_file(fn1,binary=True)
 		cmp_or_die(orig,ret)
+		ok()
 	def rand2file(self,name):
 		of = name + '.out'
 		dlen = 1024
 		tu.run_cmd(name,[of,str(1024),'threads=4','silent=1'],strip=False)
 		d = read_from_tmpfile(cfg,of,binary=True)
 		cmp_or_die(dlen,len(d))
+		ok()
 
 	# Cryptocoin
 	def randwif(self,name):
@@ -382,6 +393,7 @@ class MMGenToolTestCmds(object):
 			iaddr = read_from_tmpfile(cfg,'randpair{}.out'.format(n+1)).split()[-1]
 			vmsg('Out: {}'.format(ret))
 			cmp_or_die(iaddr,ret)
+			ok()
 	def hex2wif(self,name,f1,f2,f3,f4):
 		for n,fi,fo,k in ((1,f1,f2,''),(2,f3,f4,maybe_compressed)):
 			ao = ['--type='+k] if k else []
@@ -412,11 +424,13 @@ class MMGenToolTestCmds(object):
 		addr1 = read_from_tmpfile(cfg,'pubhex2addr3.out').strip()
 		addr2 = read_from_tmpfile(cfg,'randpair3.out').split()[1]
 		cmp_or_die(addr1,addr2)
+		ok()
 	def wif2redeem_script(self,name,f1,f2,f3): # compare output with above
 		wif = read_from_file(f3).split()[0]
 		ret1 = tu.run_cmd_out(name,wif,add_opts=maybe_type_segwit,fn_idx=3,Return=True)
 		ret2 = read_from_tmpfile(cfg,'pubhex2redeem_script3.out').strip()
 		cmp_or_die(ret1,ret2)
+		ok()
 	def wif2segwit_pair(self,name,f1,f2): # does its own checking, so just run
 		wif = read_from_file(f2).split()[0]
 		tu.run_cmd_out(name,wif,add_opts=maybe_type_segwit,fn_idx=2)
@@ -440,6 +454,7 @@ class MMGenToolTestCmds(object):
 		res = p.stdout.read().decode().strip()
 		addr = read_from_tmpfile(cfg,'wif2addr3.out').strip()
 		cmp_or_die(res,addr)
+		ok()
 
 	# Mnemonic
 	def hex2mn(self,name):
@@ -488,12 +503,12 @@ try:
 			msg('Running tests for {}:'.format(cmd_data[cmd]['desc']))
 			do_cmds(cmd)
 		elif cmd == 'clean':
-			cleandir(cfg['tmpdir'])
+			cleandir(cfg['tmpdir'],do_msg=True)
 			sys.exit(0)
 		else:
 			die(1,"'{}': unrecognized command".format(cmd))
 	else:
-		cleandir(cfg['tmpdir'])
+		cleandir(cfg['tmpdir'],do_msg=True)
 		for cmd in cmd_data:
 			msg('Running tests for {}:'.format(cmd_data[cmd]['desc']))
 			do_cmds(cmd)

+ 1 - 1
test/tooltest2.py

@@ -34,7 +34,7 @@ os.environ['MMGEN_TEST_SUITE'] = '1'
 
 # Import these _after_ prepending repo_root to sys.path
 from mmgen.common import *
-from mmgen.test import *
+from test.common import *
 from mmgen.obj import is_wif,is_coin_addr
 
 opts_data = lambda: {