Browse Source

Various bugfixes.

philemon 10 years ago
parent
commit
d3f07f3c9f
16 changed files with 2103 additions and 368 deletions
  1. 14 10
      mmgen/addr.py
  2. 6 0
      mmgen/globalvars.py
  3. 11 14
      mmgen/main.py
  4. 23 21
      mmgen/main_addrgen.py
  5. 3 3
      mmgen/main_pywallet.py
  6. 0 5
      mmgen/main_txsign.py
  7. 124 0
      mmgen/main_wallet.py
  8. 0 89
      mmgen/main_walletconv.py
  9. 14 22
      mmgen/opts.py
  10. 179 59
      mmgen/seed.py
  11. 20 14
      mmgen/share/Opts.py
  12. 0 22
      mmgen/tool.py
  13. 57 21
      mmgen/util.py
  14. 1 1
      setup.py
  15. 1535 0
      test/test-oldscripts.py
  16. 116 87
      test/test.py

+ 14 - 10
mmgen/addr.py

@@ -45,6 +45,12 @@ addrmsgs = {
 # address, and it will be appended to the bitcoind wallet label upon import.
 # The label may contain any printable ASCII symbol.
 """.strip().format(n=g.max_addr_label_len,pnm=pnm),
+	'keyfile_header': """
+# {pnm} key file
+#
+# This file is editable.
+# Everything following a hash symbol '#' is a comment and ignored by {pnm}.
+""".strip().format(pnm=pnm),
 	'no_keyconv_msg': """
 Executable '{kconv}' unavailable. Falling back on (slow) internal ECDSA library.
 Please install '{kconv}' from the {vgen} package on your system for much
@@ -312,7 +318,7 @@ class AddrInfo(MMGenObject):
 					(w,self.seed_id,self.idxs_fmt,self.checksum))
 			if self.source == "addrgen":
 				qmsg(
-		"This checksum will be used to verify the address file in the future")
+"Record this checksum: it will be used to verify the address file in the future")
 			elif self.source == "addrfile":
 				qmsg("Check this value against your records")
 
@@ -368,35 +374,33 @@ class AddrInfo(MMGenObject):
 
 
 	def fmt_data(self,enable_comments=False):
-
 		# Check data integrity - either all or none must exist for each attr
 		attrs  = ['addr','wif','sec']
 		status = [0,0,0]
-		for i in range(self.num_addrs):
+		for d in self.addrdata:
 			for j,attr in enumerate(attrs):
-				try:
-					getattr(self.addrdata[i],attr)
+				if hasattr(d,attr):
 					status[j] += 1
-				except: pass
 
 		for i,s in enumerate(status):
 			if s != 0 and s != self.num_addrs:
 				msg("%s missing %s in addr data"% (self.num_addrs-s,attrs[i]))
 				sys.exit(3)
 
-		if status[0] == None and status[1] == None:
+		if status[0] == status[1] == 0:
 			msg("Addr data contains neither addresses nor keys")
 			sys.exit(3)
 
 		# Header
 		out = []
 		from mmgen.addr import addrmsgs
-		out.append(addrmsgs['addrfile_header'] + "\n")
+		k = ('addrfile_header','keyfile_header')[int(status[0]==0)]
+		out.append(addrmsgs[k]+"\n")
 		if self.checksum:
-			w = "Key-address" if status[1] else "Address"
+			w = ("Key-address","Address")[int(status[1]==0)]
 			out.append("# {} data checksum for {}[{}]: {}".format(
 						w, self.seed_id, self.idxs_fmt, self.checksum))
-			out.append("# Record this value to a secure location\n")
+			out.append("# Record this value to a secure location.\n")
 		out.append("%s {" % self.seed_id)
 
 		# Body

+ 6 - 0
mmgen/globalvars.py

@@ -56,6 +56,12 @@ required_opts = [
 	"usr_randchars","stdout","show_hash_presets","label",
 	"keep_passphrase","keep_hash_preset"
 ]
+incompatible_opts = (
+	("quiet","verbose"),
+	("label","keep_label"),
+	("tx_id", "info"),
+	("tx_id", "terse_info"),
+)
 min_screen_width = 80
 
 wallet_ext    = "mmdat"

+ 11 - 14
mmgen/main.py

@@ -21,21 +21,18 @@ main.py - Script launcher for the MMGen suite
 """
 
 def launch(what):
-	def launch_addrgen():    import mmgen.main_addrgen
-	def launch_addrimport(): import mmgen.main_addrimport
-	def launch_keygen():     import mmgen.main_addrgen
-	def launch_passchg():    import mmgen.main_passchg
-	def launch_pywallet():   import mmgen.main_pywallet
-	def launch_tool():       import mmgen.main_tool
-	def launch_txcreate():   import mmgen.main_txcreate
-	def launch_txsend():     import mmgen.main_txsend
-	def launch_txsign():     import mmgen.main_txsign
-	def launch_walletchk():  import mmgen.main_walletchk
-	def launch_walletconv(): import mmgen.main_walletconv
-	def launch_walletgen():  import mmgen.main_walletgen
+
+	import os
+	t = "MMGEN_USE_OLD_SCRIPTS"
+	if not (t in os.environ and os.environ[t]):
+		if what in ("walletgen","walletchk","passchg"):
+			what = "wallet"
+
+	if what == "walletconv": what = "wallet"
+	if what == "keygen":     what = "addrgen"
 
 	try: import termios
-	except: locals()["launch_"+what]() # Windows
+	except: __import__("mmgen.main_" + what) # Windows
 	else:
 		import sys,atexit
 		fd = sys.stdin.fileno()
@@ -43,7 +40,7 @@ def launch(what):
 		def at_exit():
 			termios.tcsetattr(fd, termios.TCSADRAIN, old)
 		atexit.register(at_exit)
-		try: locals()["launch_"+what]()
+		try: __import__("mmgen.main_" + what)
 		except KeyboardInterrupt:
 			sys.stderr.write("\nUser interrupt\n")
 		except EOFError:

+ 23 - 21
mmgen/main_addrgen.py

@@ -29,14 +29,20 @@ from mmgen.util import *
 from mmgen.crypto import *
 from mmgen.addr import *
 
-what = "keys" if sys.argv[0].split("-")[-1] == "keygen" else "addresses"
+if sys.argv[0].split("-")[-1] == "keygen":
+	gen_what = "keys"
+	opt_filter = None
+else:
+	gen_what = "addresses"
+	opt_filter = "hdceHKlpPqSvbgXGoms"
 
 opts_data = {
-	'desc': """Generate a range or list of {w} from an {pnm} wallet,
-                  mnemonic, seed or password""".format(w=what,pnm=g.proj_name),
+	'desc': """Generate a range or list of {what} from an {pnm} wallet,
+                  mnemonic, seed or password""".format(what=gen_what,pnm=g.proj_name),
 	'usage':"[opts] [infile] <address range or list>",
 	'options': """
--h, --help              Print this help message{}
+-h, --help              Print this help message
+-A, --no-addresses      Print only secret keys, no addresses
 -d, --outdir=       d   Specify an alternate directory 'd' for output
 -c, --save-checksum     Save address list checksum to file
 -e, --echo-passphrase   Echo passphrase or mnemonic to screen upon entry
@@ -51,7 +57,8 @@ opts_data = {
 -q, --quiet             Suppress warnings; overwrite files without
                         prompting
 -S, --stdout            Print {what} to stdout
--v, --verbose           Produce more verbose output{}
+-v, --verbose           Produce more verbose output
+-x, --b16               Print secret keys in hexadecimal too
 
 -b, --from-brain=  l,p  Generate {what} from a user-created password,
                         i.e. a "brainwallet", using seed length 'l' and
@@ -64,19 +71,14 @@ opts_data = {
 -m, --from-mnemonic     Generate {what} from an electrum-like mnemonic
 -s, --from-seed         Generate {what} from a seed in .{g.seed_ext} format
 """.format(
-		*(
-			(
-"\n-A, --no-addresses      Print only secret keys, no addresses",
-"\n-x, --b16               Print secret keys in hexadecimal too"
-			)
-		if what == "keys" else ("","")),
-		seed_lens=", ".join([str(i) for i in g.seed_lens]),
-		what=what,g=g,pnm=g.proj_name
+	seed_lens=", ".join([str(i) for i in g.seed_lens]),
+	pnm=g.proj_name,
+	what=gen_what,g=g
 ),
 	'notes': """
 
 Addresses are given in a comma-separated list.  Hyphen-separated ranges are
-also allowed.{}
+also allowed.{a}
 
 If available, the external 'keyconv' program will be used for address
 generation.
@@ -101,8 +103,8 @@ The '--from-brain' option also requires the user to specify a seed length
 For a brainwallet passphrase to always generate the same keys and addresses,
 the same 'l' and 'p' parameters to '--from-brain' must be used in all future
 invocations with that passphrase
-""".format("\n\nBy default, both addresses and secret keys are generated."
-				if what == "keys" else "")
+""".format(a="\n\nBy default, both addresses and secret keys are generated."
+				if gen_what == "keys" else "")
 }
 
 wmsg = {
@@ -112,7 +114,7 @@ UNENCRYPTED form.  Generate only the key(s) you need and guard them carefully.
 """.format(pnm=g.proj_name),
 }
 
-cmd_args = opt.opts.init(opts_data,add_opts=["b16"])
+cmd_args = opt.opts.init(opts_data,add_opts=["b16"],opt_filter=opt_filter)
 
 if opt.from_incog_hex or opt.from_incog_hidden: opt.from_incog = True
 
@@ -131,14 +133,14 @@ if not addr_idxs: sys.exit(2)
 do_license_msg()
 
 # Interact with user:
-if what == "keys" and not opt.quiet:
+if gen_what == "keys" and not opt.quiet:
 	confirm_or_exit(wmsg['unencrypted_secret_keys'], 'continue')
 
 # Generate data:
 
 seed = get_seed_retry(infile)
 
-opt.gen_what = "a" if what == "addresses" else (
+opt.gen_what = "a" if gen_what == "addresses" else (
 	"k" if opt.no_addresses else "ka")
 
 ainfo = generate_addrs(seed,addr_idxs)
@@ -161,11 +163,11 @@ if opt.stdout or not sys.stdout.isatty():
 	if enc_ext and sys.stdout.isatty():
 		msg("Cannot write encrypted data to screen.  Exiting")
 		sys.exit(2)
-	write_to_stdout(addrdata_str,what,ask_terminal=(what=="keys"
+	write_to_stdout(addrdata_str,gen_what,ask_terminal=(gen_what=="keys"
 						and not opt.quiet and sys.stdout.isatty()))
 else:
 	outfile = "%s.%s%s" % (outfile_base, (
 		g.keyaddrfile_ext if "ka" in opt.gen_what else (
 		g.keyfile_ext if "k" in opt.gen_what else
 		g.addrfile_ext)), enc_ext)
-	write_to_file(outfile,addrdata_str,what,not opt.quiet,True)
+	write_to_file(outfile,addrdata_str,gen_what,not opt.quiet,True)

+ 3 - 3
mmgen/main_pywallet.py

@@ -59,9 +59,9 @@ import hashlib
 import random
 import math
 
-import mmgen.config as g
+import mmgen.globalvars as g
 import mmgen.opt as opt
-from mmgen.util import msg,msgrepr,msgrepr_exit
+from mmgen.util import msg,mdie,mmsg
 
 max_version = 60000
 addrtype = 0
@@ -86,7 +86,7 @@ opts_data = {
 }
 
 cmd_args = opt.opts.init(opts_data)
-opt.opts.warn_incompatible_opts(['json','keys','addrs','keysforaddrs'])
+opt.opts.die_on_incompatible_opts(['json','keys','addrs','keysforaddrs'])
 
 if len(cmd_args) == 1:
 	from mmgen.util import check_infile

+ 0 - 5
mmgen/main_txsign.py

@@ -285,11 +285,6 @@ def get_keys_from_keylist(kldata,other_addrs):
 
 infiles = opt.opts.init(opts_data,add_opts=["b16"])
 
-for l in (
-('tx_id', 'info'),
-('tx_id', 'terse_info'),
-): opt.opts.die_on_incompatible_opts(l)
-
 if opt.from_incog_hex or opt.from_incog_hidden: opt.from_incog = True
 
 if not infiles: opt.opts.usage()

+ 124 - 0
mmgen/main_wallet.py

@@ -0,0 +1,124 @@
+#!/usr/bin/env python
+#
+# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
+# Copyright (C)2013-2015 Philemon <mmgen-py@yandex.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+"""
+mmgen/main_wallet:  Entry point for MMGen wallet-related scripts
+"""
+
+import sys,os,re
+import mmgen.globalvars as g
+import mmgen.opt as opt
+from mmgen.util import die,msg,green,do_license_msg,check_infile,mdie,mmsg,qmsg,capfirst
+from mmgen.seed import SeedSource
+
+bn = os.path.basename(sys.argv[0])
+invoked_as = re.sub(r'^wallet','',bn.split("-")[-1])
+
+usage = "[opts] [infile]"
+nargs = 1
+iaction = "convert"
+oaction = "convert"
+
+if invoked_as == "gen":
+	desc = "Generate an {pnm} wallet from a random seed"
+	opt_filter = "hdoJlLpPqrSv"
+	usage = "[opts]"
+	oaction = "output"
+	nargs = 0
+elif invoked_as == "conv":
+	desc = "Convert an {pnm} wallet from one format to another"
+	opt_filter = None
+elif invoked_as == "chk":
+	desc = "Check validity of an {pnm} wallet"
+	opt_filter = "hiHOlpPqrv"
+	iaction = "input"
+elif invoked_as == "passchg":
+	desc = "Change the password, hash preset or label of an {pnm} wallet"
+	opt_filter = "hdiHkKOlLmpPqrSv"
+	iaction = "input"
+else:
+	die(1,"'%s': unrecognized invocation" % bn)
+
+opts_data = {
+	'desc': desc.format(pnm=g.proj_name),
+	'usage': usage,
+	'options': """
+-h, --help            Print this help message.
+-d, --outdir=      d  Output files to directory 'd' instead of working dir.
+-i, --in-fmt=      f  {iaction} from wallet format 'f' (see FMT CODES below).
+-o, --out-fmt=     f  {oaction} to wallet format 'f' (see FMT CODES below).
+-H, --hidden-incog-input-params=f,o  Read hidden incognito data from file
+                      'f' at offset 'o' (comma-separated).
+-J, --hidden-incog-output-params=f,o  Write hidden incognito data to file
+                      'f' at offset 'o' (comma-separated).  If file 'f'
+                      doesn't exist, it will be created and filled with
+                      random data.
+-O, --old-incog-fmt   Specify old-format incognito input.
+-k, --keep-passphrase Reuse passphrase of input wallet for output wallet.
+-K, --keep-hash-preset Reuse hash preset of input wallet for output wallet.
+-l, --seed-len=    l  Specify wallet seed length of 'l' bits.  This option
+                      is required only for brainwallet and incognito inputs
+                      with non-standard (< {g.seed_len}-bit) seed lengths.
+-L, --label=       l  Specify a label 'l' for output wallet.
+-m, --keep-label      Reuse label of input wallet for output wallet.
+-p, --hash-preset= p  Use the scrypt hash parameters defined by preset 'p'
+                      for password hashing (default: '{g.hash_preset}').
+-P, --passwd-file= f  Get wallet passphrase from file 'f'
+-q, --quiet           Produce quieter output; suppress some warnings.
+-r, --usr-randchars=n Get 'n' characters of additional randomness from user
+                      (min={g.min_urandchars}, max={g.max_urandchars}, default={g.usr_randchars}).
+-S, --stdout          Write wallet data to stdout instead of file.
+-v, --verbose         Produce more verbose output.
+
+FMT CODES:
+  {f}
+""".format(
+		g=g,
+		iaction=capfirst(iaction),
+		oaction=capfirst(oaction),
+		f="\n  ".join(SeedSource.format_fmt_codes().split("\n"))
+	)
+}
+
+cmd_args = opt.opts.init(opts_data,opt_filter=opt_filter)
+
+if len(cmd_args) < nargs \
+		and not opt.hidden_incog_input_params and not opt.in_fmt:
+	die(1,"An input file or input format must be specified")
+elif len(cmd_args) > nargs \
+		or (len(cmd_args) == nargs and opt.hidden_incog_input_params):
+	msg("No input files may be specified" if invoked_as == "gen"
+			else "Too many input files specified")
+	opt.opts.usage()
+
+if cmd_args: check_infile(cmd_args[0])
+
+if not invoked_as == "chk": do_license_msg()
+
+if invoked_as in ("conv","passchg"): msg(green("Processing input wallet"))
+
+ss_in = None if invoked_as == "gen" \
+			else SeedSource(*cmd_args,passchg=invoked_as=="passchg")
+
+if invoked_as == "chk":
+	sys.exit()
+
+if invoked_as in ("conv","passchg"): msg(green("Processing output wallet"))
+
+ss_out = SeedSource(ss=ss_in,passchg=invoked_as=="passchg")
+ss_out.write_to_file()

+ 0 - 89
mmgen/main_walletconv.py

@@ -1,89 +0,0 @@
-#!/usr/bin/env python
-#
-# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
-# Copyright (C)2013-2015 Philemon <mmgen-py@yandex.com>
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-"""
-mmgen-walletconv: Convert an MMGen deterministic wallet from one format
-                  to another
-"""
-
-import sys
-import mmgen.globalvars as g
-import mmgen.opt as opt
-from mmgen.util import die,msg,green,do_license_msg,check_infile
-from mmgen.seed import SeedSource
-
-opts_data = {
-	'sets_disabled': (
-		('hidden_incog_input_params',  bool, 'in_fmt',  'hi'),
-		('hidden_incog_output_params', bool, 'out_fmt', 'hi')
-	),
-	'desc': "Convert an {pnm} wallet from one format to another".format(
-				pnm=g.proj_name),
-	'usage':"[opts] [infile]",
-	'options': """
--h, --help            Print this help message.
--d, --outdir=      d  Output files to directory 'd' instead of working dir.
--i, --in-fmt=      f  Convert from wallet format 'f' (see FMT CODES below).
--o, --out-fmt=     f  Convert to wallet format 'f' (see FMT CODES below).
--H, --hidden-incog-input-params=f,o  Use filename 'f' and offset 'o' (comma
-                      separated) for hidden incognito input.
--J, --hidden-incog-output-params=f,o  Same above, but for output.  If file
-                      'f' doesn't exist, it will be created and filled with
-                      random data.
--O, --old-incog-fmt   Specify old-format incognito input.
--k, --keep-passphrase Reuse input wallet passphrase for output wallet.
--K, --keep-hash-preset Reuse input wallet hash preset for output wallet.
--l, --seed-len=    l  Specify wallet seed length of 'l' bits.  This option
-                      is required only for brainwallet and incognito inputs
-                      with non-standard (< {g.seed_len}-bit) seed lengths.
--p, --hash-preset= p  Use the scrypt hash parameters defined by preset 'p'
-                      for password hashing (default: '{g.hash_preset}').
--q, --quiet           Produce quieter output; suppress some warnings.
--r, --usr-randchars=n Get 'n' characters of additional randomness from user
-                      (min={g.min_urandchars}, max={g.max_urandchars}, default={g.usr_randchars}).
--S, --stdout          Write wallet data to stdout instead of file.
--v, --verbose         Produce more verbose output.
-
-FMT CODES:
-  {f}
-""".format(g=g,f="\n  ".join(SeedSource.format_fmt_codes().split("\n")))
-}
-
-cmd_args = opt.opts.init(opts_data)
-
-if len(cmd_args) == 0 \
-	and not opt.hidden_incog_input_params \
-		and not opt.in_fmt:
-	die(1,"An input file or input format must be specified")
-
-if len(cmd_args) > 1 or (len(cmd_args) == 1 and opt.hidden_incog_input_params):
-	die(1,"Only one input file may be specified")
-
-if len(cmd_args) == 1:
-	check_infile(cmd_args[0])
-
-do_license_msg()
-
-msg(green("Processing input wallet"))
-
-ss_in = SeedSource(*cmd_args)
-
-msg(green("Processing output wallet"))
-
-ss_out = SeedSource(ss=ss_in)
-ss_out.write_to_file()

+ 14 - 22
mmgen/opts.py

@@ -24,7 +24,7 @@ import sys
 import mmgen.globalvars as g
 import mmgen.share.Opts
 import opt
-from mmgen.util import msg,msg_r,mdie,mmsg,Msg,die
+from mmgen.util import msg,msg_r,mdie,mmsg,Msg,die,is_mmgen_wallet_label
 
 def usage():
 	Msg("USAGE: %s %s" % (g.prog_name, usage_txt))
@@ -38,9 +38,10 @@ mand line.   Copyright (C) {g.Cdates} {g.author} {g.email}
 """.format(pnm=g.proj_name, g=g, pgnm_uc=g.prog_name.upper()).strip())
 
 def die_on_incompatible_opts(incompat_list):
-	bad = [k for k in opt.__dict__ if opt.__dict__[k] and k in incompat_list]
-	if len(bad) > 1:
-		die(1,"Conflicting options: %s" % ", ".join([fmt_opt(b) for b in bad]))
+	for group in incompat_list:
+		bad = [k for k in opt.__dict__ if opt.__dict__[k] and k in group]
+		if len(bad) > 1:
+			die(1,"Conflicting options: %s" % ", ".join([fmt_opt(b) for b in bad]))
 
 def _typeconvert_from_dfl(key):
 
@@ -80,18 +81,19 @@ def _show_hash_presets():
 		msg(fs.format("'%s'" % i, *g.hash_presets[i]))
 	msg("N = memory usage (power of two), p = iterations (rounds)")
 
-def init(opts_data,add_opts=[]):
+def init(opts_data,add_opts=[],opt_filter=None):
 
 	if len(sys.argv) == 2 and sys.argv[1] == '--version':
 		print_version_info(); sys.exit()
 
-	uopts,args,short_opts,long_opts = \
-		mmgen.share.Opts.parse_opts(sys.argv,opts_data)
+	uopts,args,short_opts,long_opts,skipped_opts = \
+		mmgen.share.Opts.parse_opts(sys.argv,opts_data,opt_filter=opt_filter)
 
 	if g.debug:
 		d = (
 			("Short opts",         short_opts),
 			("Long opts",          long_opts),
+			("Skipped opts",       skipped_opts),
 			("User-selected opts", uopts),
 			("Cmd args",           args),
 		)
@@ -112,7 +114,8 @@ def init(opts_data,add_opts=[]):
 		if k[:2] == "__": del opt.__dict__[k]
 
 	# Transfer uopts into opt, setting required opts to None if not set by user
-	for o in [s.rstrip("=") for s in long_opts] + g.required_opts + add_opts:
+	for o in [s.rstrip("=") for s in long_opts] + \
+			g.required_opts + add_opts + skipped_opts:
 		opt.__dict__[o] = uopts[o] if o in uopts else None
 
 	# A special case - do this here, before opt gets set from g.dfl_vars
@@ -144,12 +147,7 @@ def init(opts_data,add_opts=[]):
 				Msg("    %-18s: %-6s [%s]" % (k,v,type(v).__name__))
 		Msg("### END OPTS.PY ###\n")
 
-	for l in (
-	('from_incog_hidden','from_incog','from_seed','from_mnemonic','from_brain'),
-	('export_incog','export_incog_hex','export_incog_hidden','export_mnemonic',
-	'export_seed'),
-	('quiet','verbose')
-	): die_on_incompatible_opts(l)
+	die_on_incompatible_opts(g.incompatible_opts)
 
 	return args
 
@@ -225,15 +223,9 @@ def check_opts(usr_opts):       # Returns false if any check fails
 			from mmgen.util import check_outdir
 			check_outdir(val)  # exits on error
 		elif key == 'label':
-			if not opt_compares(len(val),"<=",g.max_wallet_label_len,"label length"):
+			if not is_mmgen_wallet_label(val):
+				msg("Illegal value for option '%s': '%s'" % (fmt_opt(key),val))
 				return False
-			try: val.decode("ascii")
-			except:
-				msg("ERROR: label contains a non-ASCII symbol")
-				return False
-			w = "character in label"
-			for ch in list(val):
-				if not opt_is_in_list(ch,g.wallet_label_symbols,w): return False
 		# NEW
 		elif key in ('in_fmt','out_fmt'):
 			from mmgen.seed import SeedSource,IncogWallet,Brainwallet,IncogWalletHidden

+ 179 - 59
mmgen/seed.py

@@ -59,24 +59,29 @@ class SeedSource(MMGenObject):
 	stdin_ok = False
 	ask_tty = True
 	no_tty  = False
+	op = None
 	_msg = {}
 
 	class SeedSourceData(MMGenObject): pass
 
-	def __new__(cls,fn=None,ss=None,ignore_in_fmt_opt=False):
+	def __new__(cls,fn=None,ss=None,ignore_in_fmt_opt=False,passchg=False):
 
 		def die_on_opt_mismatch(opt,sstype):
 			opt_sstype = cls.fmt_code_to_sstype(opt)
 			compare_or_die(
-				opt_sstype.__name__, "input format specified on command line",
+				opt_sstype.__name__, "input format requested on command line",
 				sstype.__name__,     "input file format"
 			)
 
 		if ss:
-			sstype = cls.fmt_code_to_sstype(opt.out_fmt)
-			me = super(cls,cls).__new__(sstype or Wallet) # output default: Wallet
+			if passchg:
+				sstype = ss.__class__
+			else:
+				sstype = cls.fmt_code_to_sstype(opt.out_fmt)
+			me = super(cls,cls).__new__(sstype or Wallet) # default: Wallet
 			me.seed = ss.seed
 			me.ss_in = ss
+			me.op = ("conv","pwchg_new")[int(passchg)]
 		elif fn or opt.hidden_incog_input_params:
 			if fn:
 				f = Filename(fn)
@@ -91,17 +96,20 @@ class SeedSource(MMGenObject):
 
 			me = super(cls,cls).__new__(sstype)
 			me.infile = f
+			me.op = ("old","pwchg_old")[int(passchg)]
 		elif opt.in_fmt:  # Input format
 			sstype = cls.fmt_code_to_sstype(opt.in_fmt)
 			me = super(cls,cls).__new__(sstype)
+			me.op = ("old","pwchg_old")[int(passchg)]
 		else: # Called with no inputs - initialize with random seed
 			sstype = cls.fmt_code_to_sstype(opt.out_fmt)
-			me = super(cls,cls).__new__(sstype or Wallet) # output default: Wallet
+			me = super(cls,cls).__new__(sstype or Wallet) # default: Wallet
 			me.seed = Seed()
+			me.op = "new"
 
 		return me
 
-	def __init__(self,fn=None,ss=None,ignore_in_fmt_opt=False):
+	def __init__(self,fn=None,ss=None,ignore_in_fmt_opt=False,passchg=False):
 
 		self.ssdata = self.SeedSourceData()
 		self.msg = {}
@@ -113,6 +121,7 @@ class SeedSource(MMGenObject):
 		if hasattr(self,'seed'):
 			g.use_urandchars = True
 			self._encrypt()
+			return
 		elif hasattr(self,'infile'):
 			self._deformat_once()
 			self._decrypt_retry()
@@ -123,6 +132,9 @@ class SeedSource(MMGenObject):
 			self._deformat_retry()
 			self._decrypt_retry()
 
+		m = (""," length %s" % self.seed.length)[int(self.seed.length != 256)]
+		qmsg("Valid %s for seed ID %s%s" % (self.desc,self.seed.sid,m))
+
 	def _get_data(self):
 		if hasattr(self,'infile'):
 			self.fmt_data = get_data_from_file(self.infile.name,self.desc)
@@ -217,41 +229,113 @@ an empty passphrase, just hit ENTER twice.
 	""".strip()
 	}
 
-	def _get_pw(self,desc=None):
-		self.ssdata.passwd = get_mmgen_passphrase(desc)
+	def _get_hash_preset_from_user(self,hp,desc_suf=""):
+# 					hp=a,
+		n = ("","old ")[int(self.op=="pwchg_old")]
+		m,n = (("to accept the default",n),("to reuse the old","new "))[
+						int(self.op=="pwchg_new")]
+		fs = "Enter {}hash preset for {}{}{},\n or hit ENTER {} value ('{}'): "
+		p = fs.format(
+			n,
+			("","new ")[int(self.op=="new")],
+			self.desc,
+			(""," "+desc_suf)[int(bool(desc_suf))],
+			m,
+			hp
+		)
+		while True:
+			ret = my_raw_input(p)
+			if ret:
+				if ret in g.hash_presets.keys():
+					self.ssdata.hash_preset = ret
+					return ret
+				else:
+					msg("Invalid input.  Valid choices are %s" %
+							", ".join(sorted(g.hash_presets.keys())))
+			else:
+				self.ssdata.hash_preset = hp
+				return hp
 
-	def _get_hash_preset(self,desc=None):
-		# Converting:
-		desc = desc or self.desc
-		if hasattr(self,'ss_in') and hasattr(self.ss_in.ssdata,'hash_preset'):
+	def _get_hash_preset(self,desc_suf=""):
+		if hasattr(self,"ss_in") and hasattr(self.ss_in.ssdata,"hash_preset"):
+			old_hp = self.ss_in.ssdata.hash_preset
 			if opt.keep_hash_preset:
-				a = self.ss_in.ssdata.hash_preset
-				qmsg("Reusing hash preset '%s' as per user request" % a)
+				qmsg("Reusing hash preset '%s' at user request" % old_hp)
+				self.ssdata.hash_preset = old_hp
 			elif 'hash_preset' in opt.set_by_user:
-				# Prompt, but use user-requested value as default
-				a = get_hash_preset_from_user(hp=opt.hash_preset,desc=desc)
-			else:
-				a = get_hash_preset_from_user(desc=desc)
+				hp = self.ssdata.hash_preset = opt.hash_preset
+				qmsg("Using hash preset '%s' requested on command line"
+						% opt.hash_preset)
+			else: # Prompt, using old value as default
+				hp = self._get_hash_preset_from_user(old_hp,desc_suf)
+
+			if (not opt.keep_hash_preset) and self.op == "pwchg_new":
+				m = ("changed to '%s'" % hp,"unchanged")[int(hp==old_hp)]
+				qmsg("Hash preset %s" % m)
 		elif 'hash_preset' in opt.set_by_user:
-			a = opt.hash_preset
-			qmsg("Using user-requested hash preset of '%s'" % a)
+			self.ssdata.hash_preset = opt.hash_preset
+			qmsg("Using hash preset '%s' requested on command line"%opt.hash_preset)
+		else:
+			self._get_hash_preset_from_user(opt.hash_preset,desc_suf)
+
+	def _get_new_passphrase(self):
+		desc = "{}passphrase for {}{}".format(
+				("","new ")[int(self.op=="pwchg_new")],
+				("","new ")[int(self.op in ("new","conv"))],
+				self.desc
+			)
+		if opt.passwd_file:
+			w = pwfile_reuse_warning()
+			pw = " ".join(get_words_from_file(opt.passwd_file,desc,silent=w))
+		elif opt.echo_passphrase:
+			pw = " ".join(get_words_from_user("Enter %s: " % desc))
+		else:
+			for i in range(g.passwd_max_tries):
+				pw = " ".join(get_words_from_user("Enter %s: " % desc))
+				pw2 = " ".join(get_words_from_user("Repeat passphrase: "))
+				dmsg("Passphrases: [%s] [%s]" % (pw,pw2))
+				if pw == pw2:
+					vmsg("Passphrases match"); break
+				else: msg("Passphrases do not match.  Try again.")
+			else:
+				msg("User failed to duplicate passphrase in %s attempts" %
+						g.passwd_max_tries)
+				sys.exit(2)
+
+		if pw == "": qmsg("WARNING: Empty passphrase")
+		self.ssdata.passwd = pw
+		return pw
+
+	def _get_passphrase(self,desc_suf=""):
+		desc ="{}passphrase for {}{}".format(
+			("","old ")[int(self.op=="pwchg_old")],
+			self.desc,
+			(""," "+desc_suf)[int(bool(desc_suf))]
+		)
+		if opt.passwd_file:
+			w = pwfile_reuse_warning()
+			ret = " ".join(get_words_from_file(opt.passwd_file,desc,silent=w))
 		else:
-			a = get_hash_preset_from_user(desc=self.desc)
-		self.ssdata.hash_preset = a
+			ret = " ".join(get_words_from_user("Enter %s: " % desc))
+		self.ssdata.passwd = ret
 
 	def _get_first_pw_and_hp_and_encrypt_seed(self):
 		d = self.ssdata
+		self._get_hash_preset()
 
-		if hasattr(self,'ss_in') and hasattr(self.ss_in.ssdata,'passwd') \
-				and opt.keep_passphrase:
-			d.passwd = self.ss_in.ssdata.passwd
-			qmsg("Reusing passphrase as per user request")
-
-		self._get_hash_preset(desc="new " + self.desc)
-
-		if not hasattr(d,'passwd'):
-			qmsg(self.msg['choose_passphrase'] % (self.desc,self.ssdata.hash_preset))
-			d.passwd = get_new_passphrase(desc="new " + self.desc)
+		if hasattr(self,'ss_in') and hasattr(self.ss_in.ssdata,'passwd'):
+			old_pw = self.ss_in.ssdata.passwd
+			if opt.keep_passphrase:
+				d.passwd = old_pw
+				qmsg("Reusing passphrase at user request")
+			else:
+				pw = self._get_new_passphrase()
+				if self.op == "pwchg_new":
+					m = ("changed","unchanged")[int(pw==old_pw)]
+					qmsg("Passphrase %s" % m)
+		else:
+			qmsg(self.msg['choose_passphrase'] % (self.desc,d.hash_preset))
+			self._get_new_passphrase()
 
 		d.salt     = sha256(get_random(128)).digest()[:g.salt_len]
 		key        = make_key(d.passwd, d.salt, d.hash_preset)
@@ -279,7 +363,7 @@ class Mnemonic (SeedSourceUnenc):
 		deconv =  [wl.index(words[::-1][i])*(base**i)
 					for i in range(len(words))]
 		ret = ("{:0%sx}" % pad).format(sum(deconv))
-		return "%s%s" % (('0' if len(ret) % 2 else ''), ret)
+		return ('','0')[len(ret) % 2] + ret
 
 	def _hextobaseN(self,base,hexnum,wl,pad=0):
 		num,ret = int(hexnum,16),[]
@@ -344,7 +428,6 @@ class Mnemonic (SeedSourceUnenc):
 
 		check_usr_seed_len(self.seed.length)
 
-		qmsg("Valid mnemonic for seed ID %s" % make_chksum_8(self.seed.data))
 		return True
 
 	def _filename(self):
@@ -403,8 +486,6 @@ class SeedFile (SeedSourceUnenc):
 
 		check_usr_seed_len(self.seed.length)
 
-		qmsg("Valid seed data for seed ID %s" % make_chksum_8(self.seed.data))
-
 		return True
 
 	def _filename(self):
@@ -417,11 +498,47 @@ class Wallet (SeedSourceEnc):
 	desc = g.proj_name + " wallet"
 	ext = "mmdat"
 
+	def _get_label_from_user(self,old_lbl=""):
+		d = ("to reuse the label '%s'" % old_lbl) if old_lbl else "for no label"
+		p = "Enter a wallet label, or hit ENTER %s: " % d
+		while True:
+			ret = my_raw_input(p)
+			if ret:
+				if is_mmgen_wallet_label(ret):
+					self.ssdata.label = ret; return ret
+				else:
+					msg("Invalid label.  Trying again...")
+			else:
+				ret = old_lbl or "No Label"
+				self.ssdata.label = ret; return ret
+
+	# nearly identical to _get_hash_preset() - factor?
+	def _get_label(self):
+		if hasattr(self,'ss_in') and hasattr(self.ss_in.ssdata,'label'):
+			old_lbl = self.ss_in.ssdata.label
+			if opt.keep_label:
+				qmsg("Reusing label '%s' at user request" % old_lbl)
+				self.ssdata.label = old_lbl
+			elif opt.label:
+				qmsg("Using label '%s' requested on command line" % opt.label)
+				lbl = self.ssdata.label = opt.label
+			else: # Prompt, using old value as default
+				lbl = self._get_label_from_user(old_lbl)
+
+			if (not opt.keep_label) and self.op == "pwchg_new":
+				m = ("changed to '%s'" % lbl,"unchanged")[int(lbl==old_lbl)]
+				qmsg("Label %s" % m)
+		elif opt.label:
+			qmsg("Using label '%s' requested on command line" % opt.label)
+			self.ssdata.label = opt.label
+		else:
+			self._get_label_from_user()
+
 	def _encrypt(self):
 		self._get_first_pw_and_hp_and_encrypt_seed()
+		self._get_label()
 		d = self.ssdata
-		d.label = opt.label or "No Label"
-		d.pw_status = "NE" if len(d.passwd) else "E"
+		d.pw_status = ("NE","E")[int(len(d.passwd)==0)]
 		d.timestamp = make_timestamp()
 
 	def _format(self):
@@ -477,9 +594,10 @@ class Wallet (SeedSourceEnc):
 
 		d.hash_preset = hp = hpdata[0][:-1]  # a string!
 		qmsg("Hash preset of wallet: '%s'" % hp)
-		uhp = opt.hash_preset
-		if uhp and 'hash_preset' in opt.set_by_user and uhp != hp:
-			msg("Warning: ignoring user-requested hash preset '%s'" % uhp)
+		if 'hash_preset' in opt.set_by_user:
+			uhp = opt.hash_preset
+			if uhp != hp:
+				qmsg("Warning: ignoring user-requested hash preset '%s'" % uhp)
 
 		hash_params = [int(i) for i in hpdata[1:]]
 
@@ -514,8 +632,8 @@ class Wallet (SeedSourceEnc):
 	def _decrypt(self):
 		d = self.ssdata
 		# Needed for multiple transactions with {}-txsign
-		add = " "+self.infile.name if opt.quiet else ""
-		self._get_pw(self.desc+add)
+		suf = ("",self.infile.name)[int(bool(opt.quiet))]
+		self._get_passphrase(desc_suf=suf)
 		key = make_key(d.passwd, d.salt, d.hash_preset)
 		ret = decrypt_seed(d.enc_seed, key, d.seed_id, d.key_id)
 		if ret:
@@ -572,6 +690,7 @@ class Brainwallet (SeedSourceEnc):
 	fmt_codes = "mmbrain","brainwallet","brain","bw","b"
 	desc = "brainwallet"
 	ext = "mmbrain"
+	# brainwallet warning message? TODO
 
 	def _deformat(self):
 		self.brainpasswd = " ".join(self.fmt_data.split())
@@ -620,7 +739,7 @@ to exit and re-run the program with the '--old-incog-fmt' option.
 	def _make_iv_chksum(self,s): return sha256(s).hexdigest()[:8].upper()
 
 	def _get_incog_data_len(self,seed_len):
-		e = 0 if opt.old_incog_fmt else g.hincog_chk_len
+		e = (g.hincog_chk_len,0)[int(bool(opt.old_incog_fmt))]
 		return g.aesctr_iv_len + g.salt_len + e + seed_len/8
 
 	def _incog_data_size_chk(self):
@@ -718,9 +837,8 @@ to exit and re-run the program with the '--old-incog-fmt' option.
 
 	def _decrypt(self):
 		d = self.ssdata
-		desc = self.desc+" "+d.incog_id
-		self._get_hash_preset(desc)
-		self._get_pw(desc)
+		self._get_hash_preset(desc_suf=d.incog_id)
+		self._get_passphrase(desc_suf=d.incog_id)
 
 		# IV is used BOTH to initialize counter and to salt password!
 		key = make_key(d.passwd, d.iv, d.hash_preset, "wrapper key")
@@ -731,10 +849,10 @@ to exit and re-run the program with the '--old-incog-fmt' option.
 		d.enc_seed = dd[g.salt_len:]
 
 		key = make_key(d.passwd, d.salt, d.hash_preset, "main key")
-		msg("Key ID: %s" % make_chksum_8(key))
+		qmsg("Key ID: %s" % make_chksum_8(key))
 
-		verify_seed = self._verify_seed_oldfmt if opt.old_incog_fmt else \
-						self._verify_seed_newfmt
+		verify_seed = getattr(self,"_verify_seed_"+
+						("newfmt","oldfmt")[int(bool(opt.old_incog_fmt))])
 
 		seed = verify_seed(decrypt_seed(d.enc_seed, key, "", ""))
 
@@ -749,7 +867,7 @@ to exit and re-run the program with the '--old-incog-fmt' option.
 class IncogWalletHex (IncogWallet):
 
 	desc = "hex incognito data"
-	fmt_codes = "mmincox","incog_hex","xincog","ix","xi"
+	fmt_codes = "mmincox","incox","incog_hex","xincog","ix","xi"
 	ext = "mmincox"
 	no_tty = False
 
@@ -800,7 +918,7 @@ harder to find, you're advised to choose a much larger file size than this.
 
 	def _check_valid_offset(self,fn,action):
 		d = self.ssdata
-		m = "Destination" if action == "write" else "Input"
+		m = ("Input","Destination")[int(action=="write")]
 		if fn.size < d.hincog_offset + d.target_data_len:
 			die(1,
 	"%s file has length %s, too short to %s %s bytes of data at offset %s"
@@ -830,9 +948,11 @@ harder to find, you're advised to choose a much larger file size than this.
 		self._format()
 		compare_or_die(d.target_data_len, "target data length",
 				len(self.fmt_data),"length of formatted " + self.desc)
-		fn,d.hincog_offset = self._get_hincog_params("output")
 
-		self.hincog_data_is_new = False
+		k = ("output","input")[int(self.op=="pwchg_new")]
+		fn,d.hincog_offset = self._get_hincog_params(k)
+
+		check_offset = True
 		try:
 			os.stat(fn)
 		except:
@@ -841,14 +961,14 @@ harder to find, you're advised to choose a much larger file size than this.
 				min_fsize = d.target_data_len + d.hincog_offset
 				msg(self.msg['choose_file_size'].format(min_fsize))
 				while True:
-					fsize = my_raw_input("Enter file size: ")
-					if is_int(fsize) and int(fsize) >= min_fsize: break
+					fsize = parse_nbytes(my_raw_input("Enter file size: "))
+					if fsize >= min_fsize: break
 					msg("File size must be an integer no less than %s" %
 							min_fsize)
 
 				from mmgen.tool import rand2file
 				rand2file(fn, str(fsize))
-				self.hincog_data_is_new = True
+				check_offset = False
 			else:
 				die(1,"Exiting at user request")
 
@@ -856,7 +976,7 @@ harder to find, you're advised to choose a much larger file size than this.
 
 		dmsg("Incog data len %s, offset %s" % (d.target_data_len,d.hincog_offset))
 
-		if not self.hincog_data_is_new:
+		if check_offset:
 			self._check_valid_offset(f,"write")
 			if not opt.quiet: confirm_or_exit("","alter file '%s'" % f.name)
 
@@ -865,5 +985,5 @@ harder to find, you're advised to choose a much larger file size than this.
 		os.write(fh, self.fmt_data)
 		os.close(fh)
 		msg("%s written to file '%s' at offset %s" % (
-				self.desc[0].upper()+self.desc[1:],
+				capfirst(self.desc),
 				f.name,d.hincog_offset))

+ 20 - 14
mmgen/share/Opts.py

@@ -36,7 +36,7 @@ def print_help(opts_data):
 def process_opts(argv,opts_data,short_opts,long_opts):
 
 	import os
-	opts_data['prog_name'] = os.path.split(sys.argv[0])[1]
+	opts_data['prog_name'] = os.path.basename(sys.argv[0])
 	long_opts  = [i.replace("_","-") for i in long_opts]
 
 	try: cl_opts, args = getopt.getopt(argv[1:], short_opts, long_opts)
@@ -80,22 +80,28 @@ def process_opts(argv,opts_data,short_opts,long_opts):
 	return opts,args
 
 
-def parse_opts(argv,opts_data):
+def parse_opts(argv,opts_data,opt_filter=None):
 
-	lines = opts_data['options'].strip().split("\n")
 	import re
-	pat = r"^-([a-zA-Z0-9]), --([a-zA-Z0-9-]{1,64})(=| )(.+)"
-	rep = r"-{0}, --{1}{w}{3}"
-	opt_data = [list(m.groups()) for m in [re.match(pat,l) for l in lines] if m]
-
-	for d in opt_data:
-		if d[2] == " ": d[2] = ""
-	short_opts = "".join([d[0]+d[2].replace("=",":") for d in opt_data])
-	long_opts = [d[1].replace("-","_")+d[2] for d in opt_data]
+	pat = r"^-([a-zA-Z0-9]), --([a-zA-Z0-9-]{2,64})(=| )(.+)"
+	od,skip = [],True
+
+	for l in opts_data['options'].strip().split("\n"):
+		m = re.match(pat,l)
+		if m:
+			skip = True if (opt_filter and m.group(1) not in opt_filter) else False
+			app = [':','='] if (m.group(3) == '=') else ['','']
+			od.append(list(m.groups()) + app + [skip])
+		else:
+			if not skip: od[-1][3] += "\n" + l
+
 	opts_data['options'] = "\n".join(
-		[rep.format(w=" ", *m.groups())
-			if m else k for m,k in [(re.match(pat,l),l) for l in lines]]
+		["-{}, --{} {}".format(d[0],d[1],d[3]) for d in od if d[6] == False]
 	)
+	short_opts    = "".join([d[0]+d[4] for d in od if d[6] == False])
+	long_opts     = [d[1].replace("-","_")+d[5] for d in od if d[6] == False]
+	skipped_opts  = [d[1].replace("-","_") for d in od if d[6] == True]
+
 	opts,args = process_opts(argv,opts_data,short_opts,long_opts)
 
-	return opts,args,short_opts,long_opts
+	return opts,args,short_opts,long_opts,skipped_opts

+ 0 - 22
mmgen/tool.py

@@ -556,28 +556,6 @@ def find_incog_data(filename,iv_id,keep_searching=False):
 	msg("")
 	os.close(f)
 
-# From "man dd":
-# c=1, w=2, b=512, kB=1000, K=1024, MB=1000*1000, M=1024*1024,
-# GB=1000*1000*1000, G=1024*1024*1024, and so on for T, P, E, Z, Y.
-
-def parse_nbytes(nbytes):
-	import re
-	m = re.match(r'([0123456789]+)(.*)',nbytes)
-	smap = ("c",1),("w",2),("b",512),("kB",1000),("K",1024),("MB",1000*1000),\
-			("M",1024*1024),("GB",1000*1000*1000),("G",1024*1024*1024)
-	if m:
-		if m.group(2):
-			for k,v in smap:
-				if k == m.group(2):
-					return int(m.group(1)) * v
-			else:
-				msg("Valid byte specifiers: '%s'" % "' '".join([i[0] for i in smap]))
-		else:
-			return int(nbytes)
-
-	msg("'%s': invalid byte specifier" % nbytes)
-	sys.exit(1)
-
 
 def rand2file(outfile, nbytes, threads=4, silent=False):
 	nbytes = parse_nbytes(nbytes)

+ 57 - 21
mmgen/util.py

@@ -55,6 +55,42 @@ def die(ev,s):
 def Die(ev,s):
 	sys.stdout.write(s+"\n"); sys.exit(ev)
 
+def is_mmgen_wallet_label(s):
+	if len(s) > g.max_wallet_label_len:
+		msg("ERROR: wallet label length (%s chars) > maximum allowed (%s chars)" % (len(s),g.max_wallet_label_len))
+		return False
+
+	try: s = s.decode("utf8")
+	except: pass
+
+	for ch in s:
+		if ch not in g.wallet_label_symbols:
+			msg("ERROR: wallet label contains illegal symbol (%s)" % ch)
+			return False
+	return True
+
+# From "man dd":
+# c=1, w=2, b=512, kB=1000, K=1024, MB=1000*1000, M=1024*1024,
+# GB=1000*1000*1000, G=1024*1024*1024, and so on for T, P, E, Z, Y.
+
+def parse_nbytes(nbytes):
+	import re
+	m = re.match(r'([0123456789]+)(.*)',nbytes)
+	smap = ("c",1),("w",2),("b",512),("kB",1000),("K",1024),("MB",1000*1000),\
+			("M",1024*1024),("GB",1000*1000*1000),("G",1024*1024*1024)
+	if m:
+		if m.group(2):
+			for k,v in smap:
+				if k == m.group(2):
+					return int(m.group(1)) * v
+			else:
+				msg("Valid byte specifiers: '%s'" % "' '".join([i[0] for i in smap]))
+		else:
+			return int(nbytes)
+
+	msg("'%s': invalid byte specifier" % nbytes)
+	sys.exit(1)
+
 import opt
 
 def qmsg(s,alt=False):
@@ -121,8 +157,8 @@ def split_into_cols(col_wid,s):
 					for i in range(len(s)/col_wid+1)]).rstrip()
 
 def capfirst(s):
-	if len(s) == 0: return s
-	return s[0].upper() + (s[1:] if len(s) > 1 else "")
+	return s if len(s) == 0 else \
+		(s[0].upper() + (s[1:] if len(s) > 1 else ""))
 
 def make_timestamp():
 	tv = time.gmtime(time.time())[:6]
@@ -168,8 +204,6 @@ def file_exists(f):
 	except:
 		return False
 
-import opt as opt
-
 def get_from_brain_opt_params():
 	l,p = opt.from_brain.split(",")
 	return(int(l),p)
@@ -209,7 +243,7 @@ def compare_chksums(chk1, desc1, chk2, desc2, hdr="", die_on_fail=False):
 		if die_on_fail:
 			die(3,m)
 		else:
-			msg(m)
+			vmsg(m)
 			return False
 
 	vmsg("%s checksum OK (%s)" % (capfirst(desc1),chk1))
@@ -320,13 +354,13 @@ def get_new_passphrase(desc,passchg=False):
 
 	w = "{}passphrase for {}".format("new " if passchg else "", desc)
 	if opt.passwd_file:
-		pw = " ".join(_get_words_from_file(opt.passwd_file,w))
+		pw = " ".join(get_words_from_file(opt.passwd_file,w))
 	elif opt.echo_passphrase:
-		pw = " ".join(_get_words_from_user("Enter {}: ".format(w)))
+		pw = " ".join(get_words_from_user("Enter {}: ".format(w)))
 	else:
 		for i in range(g.passwd_max_tries):
-			pw = " ".join(_get_words_from_user("Enter {}: ".format(w)))
-			pw2 = " ".join(_get_words_from_user("Repeat passphrase: "))
+			pw = " ".join(get_words_from_user("Enter {}: ".format(w)))
+			pw2 = " ".join(get_words_from_user("Repeat passphrase: "))
 			dmsg("Passphrases: [%s] [%s]" % (pw,pw2))
 			if pw == pw2:
 				vmsg("Passphrases match"); break
@@ -626,15 +660,16 @@ def get_data_from_wallet(infile,silent=False):
 	return label,metadata,hash_preset,res['salt'],res['enc_seed']
 
 
-def _get_words_from_user(prompt):
+def get_words_from_user(prompt):
 	# split() also strips
 	words = my_raw_input(prompt, echo=opt.echo_passphrase).split()
 	dmsg("Sanitized input: [%s]" % " ".join(words))
 	return words
 
 
-def _get_words_from_file(infile,desc):
-	qmsg("Getting %s from file '%s'" % (desc,infile))
+def get_words_from_file(infile,desc,silent=False):
+	if not silent:
+		qmsg("Getting %s from file '%s'" % (desc,infile))
 	f = open_file_or_exit(infile, 'r')
 	# split() also strips
 	words = f.read().split()
@@ -645,9 +680,9 @@ def _get_words_from_file(infile,desc):
 
 def get_words(infile,desc,prompt):
 	if infile:
-		return _get_words_from_file(infile,desc)
+		return get_words_from_file(infile,desc)
 	else:
-		return _get_words_from_user(prompt)
+		return get_words_from_user(prompt)
 
 def remove_comments(lines):
 	# re.sub(pattern, repl, string, count=0, flags=0)
@@ -709,26 +744,27 @@ def get_seed_from_seed_data(words):
 
 passwd_file_used = False
 
-def mark_passwd_file_as_used():
+def pwfile_reuse_warning():
 	global passwd_file_used
 	if passwd_file_used:
-		msg_r("WARNING: Reusing passphrase from file '%s'." % opt.passwd_file)
-		msg(" This may not be what you want!")
+		qmsg("Reusing passphrase from file '%s' at user request" % opt.passwd_file)
+		return True
 	passwd_file_used = True
+	return False
 
 
 def get_mmgen_passphrase(desc,passchg=False):
 	prompt ="Enter {}passphrase for {}: ".format("old " if passchg else "",desc)
 	if opt.passwd_file:
-		mark_passwd_file_as_used()
-		return " ".join(_get_words_from_file(opt.passwd_file,"passphrase"))
+		pwfile_reuse_warning()
+		return " ".join(get_words_from_file(opt.passwd_file,"passphrase"))
 	else:
-		return " ".join(_get_words_from_user(prompt))
+		return " ".join(get_words_from_user(prompt))
 
 
 def get_bitcoind_passphrase(prompt):
 	if opt.passwd_file:
-		mark_passwd_file_as_used()
+		pwfile_reuse_warning()
 		return get_data_from_file(opt.passwd_file,
 				"passphrase").strip("\r\n")
 	else:

+ 1 - 1
setup.py

@@ -58,8 +58,8 @@ setup(
 			'mmgen.main_txcreate',
 			'mmgen.main_txsend',
 			'mmgen.main_txsign',
+			'mmgen.main_wallet',
 			'mmgen.main_walletchk',
-			'mmgen.main_walletconv',
 			'mmgen.main_walletgen',
 
 			'mmgen.share.__init__',

+ 1535 - 0
test/test-oldscripts.py

@@ -0,0 +1,1535 @@
+#!/usr/bin/python
+
+# Chdir to repo root.
+# Since script is not in repo root, fix sys.path so that modules are
+# imported from repo, not system.
+import sys,os
+pn = os.path.dirname(sys.argv[0])
+os.chdir(os.path.join(pn,os.pardir))
+sys.path.__setitem__(0,os.path.abspath(os.curdir))
+
+import mmgen.globalvars as g
+import mmgen.opt as opt
+from mmgen.util import mmsg,mdie,Msg,die,capfirst
+from mmgen.test import *
+
+hincog_fn      = "rand_data"
+hincog_bytes   = 1024*1024
+hincog_offset  = 98765
+hincog_seedlen = 256
+
+incog_id_fn  = "incog_id"
+non_mmgen_fn = "btckey"
+
+ref_dir = os.path.join("test","ref")
+
+ref_wallet_brainpass = "abc"
+ref_wallet_hash_preset = "1"
+ref_wallet_incog_offset = 123
+
+ref_bw_hash_preset = "1"
+ref_bw_file        = "brainwallet"
+ref_bw_file_spc    = "brainwallet-spaced"
+
+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"
+
+cfgs = {
+	'6': {
+		'name':            "reference wallet check (128-bit)",
+		'seed_len':        128,
+		'seed_id':         "FE3C6545",
+		'ref_bw_seed_id':  "33F10310",
+		'addrfile_chk':    "B230 7526 638F 38CB 8FDC 8B76",
+		'keyaddrfile_chk': "CF83 32FB 8A8B 08E2 0F00 D601",
+		'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
+		'dep_generators':  {
+			'mmdat':       "refwalletgen1",
+			'addrs':       "refaddrgen1",
+			'akeys.mmenc': "refkeyaddrgen1"
+		},
+
+	},
+	'7': {
+		'name':            "reference wallet check (192-bit)",
+		'seed_len':        192,
+		'seed_id':         "1378FC64",
+		'ref_bw_seed_id':  "CE918388",
+		'addrfile_chk':    "8C17 A5FA 0470 6E89 3A87 8182",
+		'keyaddrfile_chk': "9648 5132 B98E 3AD9 6FC3 C5AD",
+		'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
+		'dep_generators':  {
+			'mmdat':       "refwalletgen2",
+			'addrs':       "refaddrgen2",
+			'akeys.mmenc': "refkeyaddrgen2"
+		},
+
+	},
+	'8': {
+		'name':            "reference wallet check (256-bit)",
+		'seed_len':        256,
+		'seed_id':         "98831F3A",
+		'ref_bw_seed_id':  "B48CD7FC",
+		'addrfile_chk':    "6FEF 6FB9 7B13 5D91 854A 0BD3",
+		'keyaddrfile_chk': "9F2D D781 1812 8BAD C396 9DEB",
+		'wpasswd':         "reference password",
+		'ref_wallet':      "98831F3A-27F2BF93[256,1].mmdat",
+		'ref_addrfile':    "98831F3A[1,31-33,500-501,1010-1011].addrs",
+		'ref_keyaddrfile': "98831F3A[1,31-33,500-501,1010-1011].akeys.mmenc",
+		'ref_addrfile_chksum':    "6FEF 6FB9 7B13 5D91 854A 0BD3",
+		'ref_keyaddrfile_chksum': "9F2D D781 1812 8BAD C396 9DEB",
+
+#		'ref_fake_unspent_data':"98831F3A_unspent.json",
+		'ref_tx_file':     "tx_FFB367[1.234].raw",
+		'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
+		'dep_generators':  {
+			'mmdat':       "refwalletgen3",
+			'addrs':       "refaddrgen3",
+			'akeys.mmenc': "refkeyaddrgen3"
+		},
+	},
+	'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':  {
+			'mmdat':       "walletgen",
+			'addrs':       "addrgen",
+			'raw':         "txcreate",
+			'sig':         "txsign",
+			'mmwords':     "export_mnemonic",
+			'mmseed':      "export_seed",
+			'mmincog':     "export_incog",
+			'mmincox':     "export_incog_hex",
+			hincog_fn:     "export_incog_hidden",
+			incog_id_fn:   "export_incog_hidden",
+			'akeys.mmenc': "keyaddrgen"
+		},
+	},
+	'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",
+			'raw':         "txcreate2",
+			'sig':         "txsign2",
+			'mmwords':     "export_mnemonic2",
+		},
+	},
+	'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",
+			'raw':         "txcreate3",
+			'sig':         "txsign3"
+		},
+	},
+	'4': {
+		'tmpdir':        os.path.join("test","tmp4"),
+		'wpasswd':       "Hashrate rising",
+		'addr_idx_list': "63,1004,542-544,7-9", # 8 addresses
+		'seed_len':      192,
+		'dep_generators': {
+			'mmdat':       "walletgen4",
+			'mmbrain':     "walletgen4",
+			'addrs':       "addrgen4",
+			'raw':         "txcreate4",
+			'sig':         "txsign4",
+		},
+		'bw_filename': "brainwallet.mmbrain",
+		'bw_params':   "192,1",
+	},
+	'5': {
+		'tmpdir':        os.path.join("test","tmp5"),
+		'wpasswd':       "My changed password",
+		'dep_generators': {
+			'mmdat':       "passchg",
+		},
+	},
+	'9': {
+		'tmpdir':        os.path.join("test","tmp9"),
+		'tool_enc_infn':      "tool_encrypt.in",
+#		'tool_enc_ref_infn':  "tool_encrypt_ref.in",
+		'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",
+		},
+	},
+}
+
+from copy import deepcopy
+for a,b in ('6','11'),('7','12'),('8','13'):
+	cfgs[b] = deepcopy(cfgs[a])
+	cfgs[b]['tmpdir'] = os.path.join("test","tmp"+b)
+
+from collections import OrderedDict
+cmd_data = OrderedDict([
+#     test               description                  depends
+	['walletgen',       (1,'wallet generation',        [[[],1]])],
+#	['walletchk',       (1,'wallet check',             [[["mmdat"],1]])],
+	['passchg',         (5,'password, label and hash preset change',[[["mmdat"],1]])],
+	['walletchk_newpass',(5,'wallet check with new pw, label and hash preset',[[["mmdat"],5]])],
+	['addrgen',         (1,'address generation',       [[["mmdat"],1]])],
+	['addrimport',      (1,'address import',           [[["addrs"],1]])],
+	['txcreate',        (1,'transaction creation',     [[["addrs"],1]])],
+	['txsign',          (1,'transaction signing',      [[["mmdat","raw"],1]])],
+	['txsend',          (1,'transaction sending',      [[["sig"],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_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"],1]])],
+	['txsign_keyaddr',(1,'transaction signing with key-address file', [[["akeys.mmenc","raw"],1]])],
+
+	['walletgen2',(2,'wallet generation (2), 128-bit seed',     [])],
+	['addrgen2',  (2,'address generation (2)',    [[["mmdat"],2]])],
+	['txcreate2', (2,'transaction creation (2)',  [[["addrs"],2]])],
+	['txsign2',   (2,'transaction signing, two transactions',[[["mmdat","raw"],1],[["mmdat","raw"],2]])],
+	['export_mnemonic2', (2,'seed export to mmwords format (2)',[[["mmdat"],2]])],
+
+	['walletgen3',(3,'wallet generation (3)',                  [])],
+	['addrgen3',  (3,'address generation (3)',                 [[["mmdat"],3]])],
+	['txcreate3', (3,'tx creation with inputs and outputs from two wallets', [[["addrs"],1],[["addrs"],3]])],
+	['txsign3',   (3,'tx signing with inputs and outputs from two wallets',[[["mmdat"],1],[["mmdat","raw"],3]])],
+
+	['walletgen4',(4,'wallet generation (4) (brainwallet)',    [])],
+	['addrgen4',  (4,'address generation (4)',                 [[["mmdat"],4]])],
+	['txcreate4', (4,'tx creation with inputs and outputs from four seed sources, plus non-MMGen inputs and outputs', [[["addrs"],1],[["addrs"],2],[["addrs"],3],[["addrs"],4]])],
+	['txsign4',   (4,'tx signing with inputs and outputs from incog file, mnemonic file, wallet and brainwallet, plus non-MMGen inputs and outputs', [[["mmincog"],1],[["mmwords"],2],[["mmdat"],3],[["mmbrain","raw"],4]])],
+	['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]])],
+])
+
+# saved reference data
+cmd_data_ref = (
+	# reading
+	('ref_wallet_chk', ([],'saved reference wallet')),
+	('ref_seed_chk',   ([],'saved seed 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"],'new refwallet addr chksum')),
+	('refkeyaddrgen',  (["mmdat"],'new refwallet key-addr chksum'))
+)
+
+# misc. saved reference data
+cmd_data_ref_other = (
+	('ref_addrfile_chk',   'saved reference address file'),
+	('ref_keyaddrfile_chk','saved reference key-address file'),
+#	Create the fake inputs:
+#	('txcreate8',          'transaction creation (8)'),
+	('ref_tx_chk',         'saved reference tx file'),
+	('ref_brain_chk_spc3', 'saved brainwallet (non-standard spacing)'),
+	('ref_tool_decrypt',   'decryption of saved MMGen-encrypted file'),
+)
+
+# mmgen-walletconv:
+cmd_data_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_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_data_conv_out = ( # writing
+	('ref_wallet_conv_out', 'ref seed conversion to wallet'),
+	('ref_mn_conv_out',     'ref seed conversion to mnemonic'),
+	('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')
+)
+
+cmd_groups = OrderedDict([
+	('main',      cmd_data.keys()),
+	('ref',       [c[0]+str(i) for c in cmd_data_ref for i in (1,2,3)]),
+	('ref_other', [c[0] for c in cmd_data_ref_other]),
+	('conv_in',   [c[0]+str(i) for c in cmd_data_conv_in for i in (1,2,3)]),
+	('conv_out',  [c[0]+str(i) for c in cmd_data_conv_out for i in (1,2,3)]),
+])
+
+for a,b in cmd_data_ref:
+	for i,j in (1,128),(2,192),(3,256):
+		cmd_data[a+str(i)] = (5+i,"%s (%s-bit)" % (b[1],j),[[b[0],5+i]])
+
+for a,b in cmd_data_ref_other:
+	cmd_data[a] = (8,b,[[[],8]])
+
+for a,b in cmd_data_conv_in:
+	for i,j in (1,128),(2,192),(3,256):
+		cmd_data[a+str(i)] = (10+i,"%s (%s-bit)" % (b,j),[[[],10+i]])
+
+for a,b in cmd_data_conv_out:
+	for i,j in (1,128),(2,192),(3,256):
+		cmd_data[a+str(i)] = (10+i,"%s (%s-bit)" % (b,j),[[[],10+i]])
+
+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)',
+}
+
+addrs_per_wallet = 8
+
+# total of two outputs must be < 10 BTC
+for k in cfgs.keys():
+	cfgs[k]['amts'] = [0,0]
+	for idx,mod in (0,6),(1,4):
+		cfgs[k]['amts'][idx] = "%s.%s" % ((getrandnum(2) % mod), str(getrandnum(4))[:5])
+
+meta_cmds = OrderedDict([
+	['ref1', ("refwalletgen1","refaddrgen1","refkeyaddrgen1")],
+	['ref2', ("refwalletgen2","refaddrgen2","refkeyaddrgen2")],
+	['ref3', ("refwalletgen3","refaddrgen3","refkeyaddrgen3")],
+	['gen',  ("walletgen","addrgen")],
+	['pass', ("passchg","walletchk_newpass")],
+	['tx',   ("addrimport","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]],
+
+	['tool', ("tool_encrypt","tool_decrypt","tool_find_incog_data")],
+
+	['saved_ref1', [c[0]+"1" for c in cmd_data_ref]],
+	['saved_ref2', [c[0]+"2" for c in cmd_data_ref]],
+	['saved_ref3', [c[0]+"3" for c in cmd_data_ref]],
+
+	['saved_ref_other', [c[0] for c in cmd_data_ref_other]],
+
+	['saved_ref_conv_in1', [c[0]+"1" for c in cmd_data_conv_in]],
+	['saved_ref_conv_in2', [c[0]+"2" for c in cmd_data_conv_in]],
+	['saved_ref_conv_in3', [c[0]+"3" for c in cmd_data_conv_in]],
+
+	['saved_ref_conv_out1', [c[0]+"1" for c in cmd_data_conv_out]],
+	['saved_ref_conv_out2', [c[0]+"2" for c in cmd_data_conv_out]],
+	['saved_ref_conv_out3', [c[0]+"3" for c in cmd_data_conv_out]],
+])
+
+opts_data = {
+	'desc': "Test suite for the MMGen suite",
+	'usage':"[options] [command(s) or metacommand(s)]",
+	'options': """
+-h, --help          Print this help message
+-b, --buf-keypress  Use buffered keypresses as with real human input
+-d, --debug-scripts Turn on debugging output in executed scripts
+-D, --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
+-l, --list-cmds     List and describe the tests and commands in the test suite
+-n, --names         Display command names instead of descriptions
+-p, --pause         Pause between tests, resuming on keypress
+-q, --quiet         Produce minimal output.  Suppress dependency info
+-s, --system        Test scripts and modules installed on system rather than
+                    those in the repo root
+-v, --verbose       Produce more verbose output
+""",
+	'notes': """
+
+If no command is given, the whole suite of tests is run.
+"""
+}
+
+cmd_args = opt.opts.init(opts_data)
+
+if opt.system: sys.path.pop(0)
+
+# temporary
+os.environ["MMGEN_USE_OLD_SCRIPTS"] = "1"
+
+if opt.debug_scripts: os.environ["MMGEN_DEBUG"] = "1"
+
+if opt.buf_keypress:
+	send_delay = 0.3
+else:
+	send_delay = 0
+	os.environ["MMGEN_DISABLE_HOLD_PROTECT"] = "1"
+
+if opt.exact_output:
+	def msg(s): pass
+	vmsg = vmsg_r = msg_r = msg
+else:
+	def msg(s): sys.stderr.write(s+"\n")
+	def vmsg(s):
+		if opt.verbose: sys.stderr.write(s+"\n")
+	def msg_r(s): sys.stderr.write(s)
+	def vmsg_r(s):
+		if opt.verbose: sys.stderr.write(s)
+
+stderr_save = sys.stderr
+
+def silence():
+	if not (opt.verbose or opt.exact_output):
+		sys.stderr = open("/dev/null","a")
+
+def end_silence():
+	if not (opt.verbose or opt.exact_output):
+		sys.stderr = stderr_save
+
+def errmsg(s): stderr_save.write(s+"\n")
+def errmsg_r(s): stderr_save.write(s)
+
+if opt.list_cmds:
+	fs = "  {:<{w}} - {}"
+	Msg("AVAILABLE COMMANDS:")
+	w = max([len(i) for i in cmd_data])
+	for cmd in cmd_data:
+		Msg(fs.format(cmd,cmd_data[cmd][1],w=w))
+
+	w = max([len(i) for i in meta_cmds])
+	Msg("\nAVAILABLE METACOMMANDS:")
+	for cmd in meta_cmds:
+		Msg(fs.format(cmd," ".join(meta_cmds[cmd]),w=w))
+
+	w = max([len(i) for i in cmd_groups.keys()])
+	Msg("\nAVAILABLE COMMAND GROUPS:")
+	for g in cmd_groups.keys():
+		Msg(fs.format(g," ".join(cmd_groups[g]),w=w))
+
+	Msg("\nAVAILABLE UTILITIES:")
+	w = max([len(i) for i in utils])
+	for cmd in sorted(utils):
+		Msg(fs.format(cmd,utils[cmd],w=w))
+	sys.exit()
+
+import pexpect,time,re
+from mmgen.util import get_data_from_file,write_to_file,get_lines_from_file
+
+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 = "" if opt.debug or not s else " "
+		es = "" if s else "  "
+		msg("%sSEND %s%s" % (ls,es,yellow("'%s'"%t.replace('\n',r'\n'))))
+	return ret
+
+def my_expect(p,s,t='',delay=send_delay,regex=False,nonl=False):
+	quo = "'" if type(s) == str else ""
+
+	if opt.verbose: msg_r("EXPECT %s" % yellow(quo+str(s)+quo))
+	else:       msg_r("+")
+
+	try:
+		if s == '': ret = 0
+		else:
+			f = p.expect if regex else p.expect_exact
+			ret = f(s,timeout=3)
+	except pexpect.TIMEOUT:
+		errmsg(red("\nERROR.  Expect %s%s%s timed out.  Exiting" % (quo,s,quo)))
+		sys.exit(1)
+
+	if opt.debug or (opt.verbose and type(s) != str): msg_r(" ==> %s " % ret)
+
+	if ret == -1:
+		errmsg("Error.  Expect returned %s" % ret)
+		sys.exit(1)
+	else:
+		if t == '':
+			if not nonl: vmsg("")
+		else:
+			my_send(p,t,delay,s)
+		return ret
+
+def get_file_with_ext(ext,mydir,delete=True):
+
+	flist = [os.path.join(mydir,f) for f in os.listdir(mydir)
+				if f == ext or f[-(len(ext)+1):] == "."+ext]
+
+	if not flist: return False
+
+	if len(flist) > 1:
+		if delete:
+			if not opt.quiet:
+				msg("Multiple *.%s files in '%s' - deleting" % (ext,mydir))
+			for f in flist: os.unlink(f)
+		return False
+	else:
+		return flist[0]
+
+def get_addrfile_checksum(display=False):
+	addrfile = get_file_with_ext("addrs",cfg['tmpdir'])
+	silence()
+	from mmgen.addr import AddrInfo
+	chk = AddrInfo(addrfile).checksum
+	if opt.verbose and display: msg("Checksum: %s" % cyan(chk))
+	end_silence()
+	return chk
+
+def verify_checksum_or_exit(checksum,chk):
+	if checksum != chk:
+		errmsg(red("Checksum error: %s" % chk))
+		sys.exit(1)
+	vmsg(green("Checksums match: %s") % (cyan(chk)))
+
+
+class MMGenExpect(object):
+
+	def __init__(self,name,mmgen_cmd,cmd_args=[],extra_desc=""):
+		if not opt.system:
+			mmgen_cmd = os.path.join(os.curdir,mmgen_cmd)
+		desc = (cmd_data[name][1],name)[int(bool(opt.names))]
+		if extra_desc: desc += " " + extra_desc
+		if opt.verbose or opt.exact_output:
+			sys.stderr.write(
+				green("Testing: %s\nExecuting " % desc) +
+				cyan("'%s %s'\n" % (mmgen_cmd," ".join(cmd_args)))
+			)
+		else:
+			msg_r("Testing %s: " % desc)
+
+		if opt.direct_exec:
+			os.system(" ".join([mmgen_cmd] + cmd_args))
+			sys.exit()
+		else:
+			self.p = pexpect.spawn(mmgen_cmd,cmd_args)
+			if opt.exact_output: self.p.logfile = sys.stdout
+
+	def license(self):
+		p = "'w' for conditions and warranty info, or 'c' to continue: "
+		my_expect(self.p,p,'c')
+
+	def label(self,label="Test Label"):
+		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):
+		m = "%suser-supplied entropy" % ("saved " if saved else "")
+		my_expect(self.p,"Generating encryption key from OS random data plus " + m)
+
+	def usr_rand(self,num_chars):
+		rand_chars = list(getrandstr(num_chars,no_space=True))
+		my_expect(self.p,'symbols left: ','x')
+		try:
+			vmsg_r("SEND ")
+			while self.p.expect('left: ',0.1) == 0:
+				ch = rand_chars.pop(0)
+				msg_r(yellow(ch)+" " if opt.verbose else "+")
+				self.p.send(ch)
+		except:
+			vmsg("EOT")
+		my_expect(self.p,"ENTER to continue: ",'\n')
+
+	def passphrase_new(self,desc,passphrase):
+		my_expect(self.p,("Enter passphrase for %s: " % 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 %spassphrase for %s.*?: " % (pwtype,desc)),
+				passphrase+"\n",regex=True)
+
+	def hash_preset(self,desc,preset=''):
+		my_expect(self.p,("Enter hash preset for %s," % 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 = "%s written to file " % desc
+		s2 = query + "Type uppercase 'YES' to confirm: "
+		ret = my_expect(self.p,s1 if overwrite_unlikely else [s1,s2])
+		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)
+		outfile = self.p.readline().strip().strip("'")
+		vmsg("%s file: %s" % (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 tx_view(self):
+		my_expect(self.p,r"View .*?transaction.*? \(y\)es, \(N\)o, pager \(v\)iew.*?: ","\n",regex=True)
+
+	def expect_getend(self,s,regex=False):
+		ret = self.expect(s,regex=regex,nonl=True)
+		end = self.readline().strip()
+		vmsg(" ==> %s" % cyan(end))
+		return end
+
+	def interactive(self):
+		return self.p.interact()
+
+	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 close(self):
+		return self.p.close()
+
+	def readlines(self):
+		return [l.rstrip()+"\n" for l in self.p.readlines()]
+
+	def read(self,n=None):
+		return self.p.read(n)
+
+from mmgen.rpc.data import TransactionInfo
+from decimal import Decimal
+from mmgen.bitcoin import verify_addr
+
+def add_fake_unspent_entry(out,address,comment):
+	out.append(TransactionInfo(
+		account = unicode(comment),
+		vout = int(getrandnum(4) % 8),
+		txid = unicode(hexlify(os.urandom(32))),
+		amount = Decimal("%s.%s" % (10+(getrandnum(4) % 40), getrandnum(4) % 100000000)),
+		address = address,
+		spendable = False,
+		scriptPubKey = ("76a914"+verify_addr(address,return_hex=True)+"88ac"),
+		confirmations = getrandnum(4) % 500
+	))
+
+def create_fake_unspent_data(adata,unspent_data_file,tx_data,non_mmgen_input=''):
+
+	out = []
+	for s in tx_data.keys():
+		sid = tx_data[s]['sid']
+		a = adata.addrinfo(sid)
+		for idx,btcaddr in a.addrpairs():
+			add_fake_unspent_entry(out,btcaddr,"%s:%s Test Wallet" % (sid,idx))
+
+	if non_mmgen_input:
+		from mmgen.bitcoin import privnum2addr,hextowif
+		privnum = getrandnum(32)
+		btcaddr = privnum2addr(privnum,compressed=True)
+		of = os.path.join(cfgs[non_mmgen_input]['tmpdir'],non_mmgen_fn)
+		write_to_file(of, hextowif("{:064x}".format(privnum),
+					compressed=True)+"\n","compressed bitcoin key")
+
+		add_fake_unspent_entry(out,btcaddr,"Non-MMGen address")
+
+#	msg("\n".join([repr(o) for o in out])); sys.exit()
+	write_to_file(unspent_data_file,repr(out),"Unspent outputs",verbose=True)
+
+
+def add_comments_to_addr_file(addrfile,tfile):
+	silence()
+	msg(green("Adding comments to address file '%s'" % addrfile))
+	from mmgen.addr import AddrInfo
+	a = AddrInfo(addrfile)
+	for i in a.idxs(): a.set_comment(idx,"Test address %s" % idx)
+	write_to_file(tfile,a.fmt_data(),{})
+	end_silence()
+
+def make_brainwallet_file(fn):
+	# Print random words with random whitespace in between
+	from mmgen.mn_tirosh import words
+	wl = words.split("\n")
+	nwords,ws_list,max_spaces = 10,"    \n",5
+	def rand_ws_seq():
+		nchars = getrandnum(1) % max_spaces + 1
+		return "".join([ws_list[getrandnum(1)%len(ws_list)] for i in range(nchars)])
+	rand_pairs = [wl[getrandnum(4) % len(wl)] + rand_ws_seq() for i in range(nwords)]
+	d = "".join(rand_pairs).rstrip() + "\n"
+	if opt.verbose: msg_r("Brainwallet password:\n%s" % cyan(d))
+	write_to_file(fn,d,"brainwallet password")
+
+def do_between():
+	if opt.pause:
+		from mmgen.util import keypress_confirm
+		if keypress_confirm(green("Continue?"),default_yes=True):
+			if opt.verbose or opt.exact_output: sys.stderr.write("\n")
+		else:
+			errmsg("Exiting at user request")
+			sys.exit()
+	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 = True if root else False  # 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)
+
+	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)
+			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 %s '%s' to stored reference" % (desc,chk))
+	if chk == refchk:
+		ok()
+	else:
+		if not opt.verbose: errmsg("")
+		errmsg(red("""
+Fatal error - %s '%s' does not match reference value '%s'.  Aborting test
+""".strip() % (desc,chk,refchk)))
+		sys.exit(3)
+
+def check_deps(cmds):
+	if len(cmds) != 1:
+		msg("Usage: %s check_deps <command>" % g.prog_name)
+		sys.exit(1)
+
+	cmd = cmds[0]
+
+	if cmd not in cmd_data:
+		msg("'%s': unrecognized command" % cmd)
+		sys.exit(1)
+
+	if not opt.quiet:
+		msg("Checking dependencies for '%s'" % (cmd))
+
+	check_needs_rerun(ts,cmd,build=False)
+
+	w = max(len(i) for i in 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(dirs=[]):
+	ts = MMGenTestSuite()
+	dirlist = ts.list_tmp_dirs()
+	if not dirs: dirs = dirlist.keys()
+	for d in sorted(dirs):
+		if d in dirlist:
+			cleandir(dirlist[d])
+		else:
+			msg("%s: invalid directory number" % d)
+			sys.exit(1)
+
+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):
+
+		d = [(str(num),ext) for exts,num in cmd_data[cmd][2] for ext in exts]
+		al = [get_file_with_ext(ext,cfgs[num]['tmpdir']) for num,ext in d]
+
+		global cfg
+		cfg = cfgs[str(cmd_data[cmd][0])]
+
+		self.__class__.__dict__[cmd](*([self,cmd] + al))
+
+	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 walletgen(self,name,brain=False,seed_len=None):
+
+		args = ["-d",cfg['tmpdir'],"-p1","-r10"]
+		if seed_len: args += ["-l",str(seed_len)]
+#        if 'seed_len' in cfg: args += ["-l",cfg['seed_len']]
+		if brain:
+			bwf = os.path.join(cfg['tmpdir'],cfg['bw_filename'])
+			args += ["-b",cfg['bw_params'],bwf]
+			make_brainwallet_file(bwf)
+
+		t = MMGenExpect(name,"mmgen-walletgen", args)
+		t.license()
+
+		if brain:
+			t.expect(
+	"A brainwallet will be secure only if you really know what you're doing")
+			t.expect("Type uppercase 'YES' to confirm: ","YES\n")
+
+		t.usr_rand(10)
+#		t.usr_rand_out()
+
+		t.passphrase_new("new MMGen wallet",cfg['wpasswd'])
+		t.written_to_file("Wallet")
+# 		if not brain:
+# 			t.usr_rand_out(saved=True)
+# 		t.label()
+# 		t.written_to_file("MMGen wallet")
+		ok()
+
+	def refwalletgen(self,name):
+		label = "test.py ref. wallet (pw '%s', seed len %s)" \
+					% (ref_wallet_brainpass,cfg['seed_len'])
+		bw_arg = "-b%s,%s" % (cfg['seed_len'], ref_wallet_hash_preset)
+		args = ["-d",cfg['tmpdir'],"-p1","-r10",bw_arg,"-L",label]
+		d = " (%s-bit seed)" % cfg['seed_len']
+		t = MMGenExpect(name,"mmgen-walletgen", args)
+		t.license()
+		t.expect("Type uppercase 'YES' to confirm: ","YES\n")
+		t.expect("passphrase: ",ref_wallet_brainpass+"\n")
+		t.usr_rand(10)
+		t.passphrase_new("new MMGen wallet",cfg['wpasswd'])
+		seed_id = t.written_to_file("Wallet").split("-")[0].split("/")[-1]
+		refcheck("seed ID",seed_id,cfg['seed_id'])
+
+	refwalletgen1 = refwalletgen2 = refwalletgen3 = refwalletgen
+
+	def passchg(self,name,walletfile):
+
+		t = MMGenExpect(name,"mmgen-passchg",
+			["-d",cfg['tmpdir'],"-p","2","-L","New Label","-r","16",walletfile])
+#		t.license()
+		t.passphrase("MMGen wallet",cfgs['1']['wpasswd'],pwtype="old")
+		t.expect_getend("Label changed: ")
+		t.expect_getend("Hash preset changed: ")
+		t.passphrase("MMGen wallet",cfg['wpasswd'],pwtype="new")
+		t.expect("Repeat passphrase: ",cfg['wpasswd']+"\n")
+		t.usr_rand(16)
+		t.expect_getend("Key ID changed: ")
+		t.written_to_file("Wallet")
+		ok()
+
+	def walletchk_beg(self,name,args):
+		t = MMGenExpect(name,"mmgen-walletchk", args)
+		t.expect("Getting MMGen wallet data from file '%s'" % args[-1])
+		t.passphrase("MMGen wallet",cfg['wpasswd'])
+		t.expect("Passphrase is OK")
+		t.expect("Wallet is OK")
+		return t
+
+	def walletchk(self,name,walletfile):
+		self.walletchk_beg(name,[walletfile])
+		ok()
+
+	walletchk_newpass = walletchk
+
+	def addrgen(self,name,walletfile,check_ref=False):
+		t = MMGenExpect(name,"mmgen-addrgen",["-d",cfg['tmpdir'],walletfile,cfg['addr_idx_list']])
+		t.license()
+		t.passphrase("MMGen wallet",cfg['wpasswd'])
+		t.expect("Passphrase is OK")
+		t.expect("[0-9]+ addresses generated",regex=True)
+		chk = t.expect_getend(r"Checksum for address data .*?: ",regex=True)
+		if check_ref:
+			refcheck("address data checksum",chk,cfg['addrfile_chk'])
+			return
+		t.written_to_file("Addresses")
+		ok()
+
+	def refaddrgen(self,name,walletfile):
+		d = " (%s-bit seed)" % cfg['seed_len']
+		self.addrgen(name,walletfile,check_ref=True)
+
+	refaddrgen1 = refaddrgen2 = refaddrgen3 = refaddrgen
+
+	def addrimport(self,name,addrfile):
+		outfile = os.path.join(cfg['tmpdir'],"addrfile_w_comments")
+		add_comments_to_addr_file(addrfile,outfile)
+		t = MMGenExpect(name,"mmgen-addrimport",[outfile])
+		t.expect_getend(r"Checksum for address data .*\[.*\]: ",regex=True)
+		t.expect_getend("Validating addresses...OK. ")
+		t.expect("Type uppercase 'YES' to confirm: ","\n")
+		vmsg("This is a simulation, so no addresses were actually imported into the tracking\nwallet")
+		ok()
+
+	def txcreate(self,name,addrfile):
+		self.txcreate_common(name,sources=['1'])
+
+	def txcreate_common(self,name,sources=['1'],non_mmgen_input=''):
+		if opt.verbose or opt.exact_output:
+			sys.stderr.write(green("Generating fake transaction info\n"))
+		silence()
+		from mmgen.addr import AddrInfo,AddrInfoList
+		tx_data,ail = {},AddrInfoList()
+		from mmgen.util import parse_addr_idxs
+		for s in sources:
+			afile = get_file_with_ext("addrs",cfgs[s]["tmpdir"])
+			ai = AddrInfo(afile)
+			ail.add(ai)
+			aix = parse_addr_idxs(cfgs[s]['addr_idx_list'])
+			if len(aix) != addrs_per_wallet:
+				errmsg(red("Address index list length != %s: %s" %
+							(addrs_per_wallet,repr(aix))))
+				sys.exit()
+			tx_data[s] = {
+				'addrfile': afile,
+				'chk': ai.checksum,
+				'sid': ai.seed_id,
+				'addr_idxs': aix[-2:],
+			}
+
+		unspent_data_file = os.path.join(cfg['tmpdir'],"unspent.json")
+		create_fake_unspent_data(ail,unspent_data_file,tx_data,non_mmgen_input)
+
+		# make the command line
+		from mmgen.bitcoin import privnum2addr
+		btcaddr = privnum2addr(getrandnum(32),compressed=True)
+
+		cmd_args = ["-d",cfg['tmpdir']]
+		for num in tx_data.keys():
+			s = tx_data[num]
+			cmd_args += [
+				"%s:%s,%s" % (s['sid'],s['addr_idxs'][0],cfgs[num]['amts'][0]),
+			]
+			# + one BTC address
+			# + one change address and one BTC address
+			if num is tx_data.keys()[-1]:
+				cmd_args += ["%s:%s" % (s['sid'],s['addr_idxs'][1])]
+				cmd_args += ["%s,%s" % (btcaddr,cfgs[num]['amts'][1])]
+
+		for num in tx_data: cmd_args += [tx_data[num]['addrfile']]
+
+		os.environ["MMGEN_BOGUS_WALLET_DATA"] = unspent_data_file
+		end_silence()
+		if opt.verbose or opt.exact_output: sys.stderr.write("\n")
+
+		t = MMGenExpect(name,"mmgen-txcreate",cmd_args)
+		t.license()
+		for num in tx_data.keys():
+			t.expect_getend("Getting 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
+		if t.expect(["Continue anyway? (y/N): ",
+				"Unable to connect to bitcoind"]) == 0:
+			t.send("y")
+		else:
+			errmsg(red("Error: unable to connect to bitcoind.  Exiting"))
+			sys.exit(1)
+
+		for num in tx_data.keys():
+			t.expect("Continue anyway? (y/N): ","y")
+		t.expect(r"'q' = quit sorting, .*?: ","M", regex=True)
+		t.expect(r"'q' = quit sorting, .*?: ","q", regex=True)
+		outputs_list = [addrs_per_wallet*i + 1 for i in range(len(tx_data))]
+		if non_mmgen_input: outputs_list.append(len(tx_data)*addrs_per_wallet + 1)
+		t.expect("Enter a range or space-separated list of outputs to spend: ",
+				" ".join([str(i) for i in outputs_list])+"\n")
+		if non_mmgen_input: t.expect("Accept? (y/N): ","y")
+		t.expect("OK? (Y/n): ","y")
+		t.expect("Add a comment to transaction? (y/N): ","\n")
+		t.tx_view()
+		t.expect("Save transaction? (y/N): ","y")
+		t.written_to_file("Transaction")
+		ok()
+
+	def txsign_end(self,t,tnum=None):
+		t.expect("Signing transaction")
+		t.expect("Edit transaction comment? (y/N): ","\n")
+		t.expect("Save signed transaction? (y/N): ","y")
+		add = " #" + tnum if tnum else ""
+		t.written_to_file("Signed transaction" + add)
+
+	def txsign(self,name,txfile,walletfile,save=True):
+		t = MMGenExpect(name,"mmgen-txsign",
+				["-d",cfg['tmpdir'],txfile,walletfile])
+		t.license()
+		t.tx_view()
+		t.passphrase("MMGen wallet",cfg['wpasswd'])
+		if save:
+			self.txsign_end(t)
+		else:
+			t.expect("Edit transaction comment? (y/N): ","\n")
+			t.expect("Save signed transaction? (y/N): ","\n")
+			t.expect("Signed transaction not saved")
+		ok()
+
+	def txsend(self,name,sigfile):
+		t = MMGenExpect(name,"mmgen-txsend", ["-d",cfg['tmpdir'],sigfile])
+		t.license()
+		t.tx_view()
+		t.expect("Edit transaction comment? (y/N): ","\n")
+		t.expect("broadcast this transaction to the network?")
+		t.expect("'YES, I REALLY WANT TO DO THIS' to confirm: ","\n")
+		t.expect("Exiting at user request")
+		vmsg("This is a simulation; no transaction was sent")
+		ok()
+
+	def export_seed(self,name,walletfile):
+		t = self.walletchk_beg(name,["-s","-d",cfg['tmpdir'],walletfile])
+		f = t.written_to_file("Seed data")
+		silence()
+		msg("Seed data: %s" % cyan(get_data_from_file(f,"seed data")))
+		end_silence()
+		ok()
+
+	def export_mnemonic(self,name,walletfile):
+		t = self.walletchk_beg(name,["-m","-d",cfg['tmpdir'],walletfile])
+		f = t.written_to_file("Mnemonic data")
+		silence()
+		msg_r("Mnemonic data: %s" % cyan(get_data_from_file(f,"mnemonic data")))
+		end_silence()
+		ok()
+
+	def export_incog(self,name,walletfile,args=["-g"]):
+		t = MMGenExpect(name,"mmgen-walletchk",args+["-d",cfg['tmpdir'],"-r","10",walletfile])
+		t.passphrase("MMGen wallet",cfg['wpasswd'])
+		t.usr_rand(10)
+		incog_id = t.expect_getend("Incog ID: ")
+		write_to_tmpfile(cfg,incog_id_fn,incog_id+"\n")
+		if args[0] == "-G": return t
+		t.written_to_file("Incognito wallet data",overwrite_unlikely=True)
+		ok()
+
+	def export_incog_hex(self,name,walletfile):
+		self.export_incog(name,walletfile,args=["-X"])
+
+	# TODO: make outdir and hidden incog compatible (ignore --outdir and warn user?)
+	def export_incog_hidden(self,name,walletfile):
+		rf,rd = os.path.join(cfg['tmpdir'],hincog_fn),os.urandom(hincog_bytes)
+		vmsg(green("Writing %s bytes of data to file '%s'" % (hincog_bytes,rf)))
+		write_to_file(rf,rd,verbose=opt.verbose)
+		t = self.export_incog(name,walletfile,args=["-G","%s,%s"%(rf,hincog_offset)])
+		t.written_to_file("Data",query="")
+		ok()
+
+	def addrgen_seed(self,name,walletfile,foo,desc="seed data",arg="-s"):
+		t = MMGenExpect(name,"mmgen-addrgen",
+				[arg,"-d",cfg['tmpdir'],walletfile,cfg['addr_idx_list']])
+		t.license()
+		t.expect_getend("Valid %s for seed ID " % desc)
+		vmsg("Comparing generated checksum with checksum from previous address file")
+		chk = t.expect_getend(r"Checksum for address data .*?: ",regex=True)
+		verify_checksum_or_exit(get_addrfile_checksum(),chk)
+		t.no_overwrite()
+		ok()
+
+	def addrgen_mnemonic(self,name,walletfile,foo):
+		self.addrgen_seed(name,walletfile,foo,desc="mnemonic",arg="-m")
+
+	def addrgen_incog(self,name,walletfile,foo,args=["-g"]):
+		t = MMGenExpect(name,"mmgen-addrgen",args+["-d",
+				cfg['tmpdir'],walletfile,cfg['addr_idx_list']])
+		t.license()
+		t.expect_getend("Incog ID: ")
+		t.passphrase("incognito wallet data \w{8}", cfg['wpasswd'])
+		t.hash_preset("incog wallet",'1')
+		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()
+		ok()
+
+	def addrgen_incog_hex(self,name,walletfile,foo):
+		self.addrgen_incog(name,walletfile,foo,args=["-X"])
+
+	def addrgen_incog_hidden(self,name,walletfile,foo):
+		rf = os.path.join(cfg['tmpdir'],hincog_fn)
+		self.addrgen_incog(name,walletfile,foo,
+				args=["-G","%s,%s,%s"%(rf,hincog_offset,hincog_seedlen)])
+
+	def keyaddrgen(self,name,walletfile,check_ref=False):
+		t = MMGenExpect(name,"mmgen-keygen",
+				["-d",cfg['tmpdir'],walletfile,cfg['addr_idx_list']])
+		t.license()
+		t.expect("Type uppercase 'YES' to confirm: ","YES\n")
+		t.passphrase("MMGen wallet",cfg['wpasswd'])
+		chk = t.expect_getend(r"Checksum for key-address data .*?: ",regex=True)
+		if check_ref:
+			refcheck("key-address data checksum",chk,cfg['keyaddrfile_chk'])
+			return
+		t.expect("Encrypt key list? (y/N): ","y")
+		t.hash_preset("new key list",'1')
+		t.passphrase_new("new key list",cfg['kapasswd'])
+		t.written_to_file("Keys")
+		ok()
+
+	def refkeyaddrgen(self,name,walletfile):
+		self.keyaddrgen(name,walletfile,check_ref=True)
+
+	refkeyaddrgen1 = refkeyaddrgen2 = refkeyaddrgen3 = refkeyaddrgen
+
+	def txsign_keyaddr(self,name,keyaddr_file,txfile):
+		t = MMGenExpect(name,"mmgen-txsign", ["-d",cfg['tmpdir'],"-M",keyaddr_file,txfile])
+		t.license()
+		t.hash_preset("key-address file",'1')
+		t.passphrase("key-address file",cfg['kapasswd'])
+		t.expect("Check key-to-address validity? (y/N): ","y")
+		t.tx_view()
+		self.txsign_end(t)
+		ok()
+
+	def walletgen2(self,name):
+		self.walletgen(name,seed_len=128)
+
+	def addrgen2(self,name,walletfile):
+		self.addrgen(name,walletfile)
+
+	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.tx_view()
+			t.passphrase("MMGen wallet",cfgs[cnum]['wpasswd'])
+			self.txsign_end(t,cnum)
+		ok()
+
+	def export_mnemonic2(self,name,walletfile):
+		self.export_mnemonic(name,walletfile)
+
+	def walletgen3(self,name):
+		self.walletgen(name)
+
+	def addrgen3(self,name,walletfile):
+		self.addrgen(name,walletfile)
+
+	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.tx_view()
+		for cnum in ('1','3'):
+			t.expect_getend("Getting MMGen wallet data from file ")
+			t.passphrase("MMGen wallet",cfgs[cnum]['wpasswd'])
+		self.txsign_end(t)
+		ok()
+
+	def walletgen4(self,name):
+		self.walletgen(name,brain=True)
+
+	def addrgen4(self,name,walletfile):
+		self.addrgen(name,walletfile)
+
+	def txcreate4(self,name,f1,f2,f3,f4):
+		self.txcreate_common(name,sources=['1','2','3','4'],non_mmgen_input='4')
+
+	def txsign4(self,name,f1,f2,f3,f4,f5):
+		non_mm_fn = os.path.join(cfg['tmpdir'],non_mmgen_fn)
+		t = MMGenExpect(name,"mmgen-txsign",
+			["-d",cfg['tmpdir'],"-b",cfg['bw_params'],"-k",non_mm_fn,f1,f2,f3,f4,f5])
+		t.license()
+		t.tx_view()
+
+		for cnum,desc,app in ('1',"incognito","incognito"),('3',"MMGen","MMGen"):
+			t.expect_getend("Getting %s wallet data from file " % desc)
+			t.passphrase("%s wallet"%app,cfgs[cnum]['wpasswd'])
+			if cnum == '1':
+				t.hash_preset("incog wallet",'1')
+
+		self.txsign_end(t)
+		ok()
+
+	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)
+			infn = get_tmpfile_fn(cfg,tmp_fn)
+		t = MMGenExpect(name,"mmgen-tool",["-d",cfg['tmpdir'],"encrypt",infn])
+		t.hash_preset("user data",'1')
+		t.passphrase_new("user data",tool_enc_passwd)
+		t.written_to_file("Encrypted data")
+		ok()
+# Generate the reference mmenc file
+# 	def tool_encrypt_ref(self,name):
+# 		infn = get_tmpfile_fn(cfg,cfg['tool_enc_ref_infn'])
+# 		write_to_file(infn,cfg['tool_enc_reftext'],silent=True)
+# 		self.tool_encrypt(name,infn)
+
+	def tool_decrypt(self,name,f1,f2):
+		of = name + ".out"
+		t = MMGenExpect(name,"mmgen-tool",
+			["-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)
+		d2 = read_from_file(get_tmpfile_fn(cfg,of))
+		cmp_or_die(d1,d2)
+
+	def tool_find_incog_data(self,name,f1,f2):
+		i_id = read_from_file(f2).rstrip()
+		vmsg("Incog ID: %s" % 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 \w{8} found at offset ",regex=True)
+		os.unlink(f1)
+		cmp_or_die(hincog_offset,int(o))
+
+	def walletconv_out(self,name,desc,out_fmt="w",uopts=[],uopts_chk=[],pw=False):
+		opts = ["-d",cfg['tmpdir'],"-r10","-p1","-o",out_fmt] + uopts
+		infile = os.path.join(ref_dir,cfg['seed_id']+".mmwords")
+		d = "(convert)"
+		t = MMGenExpect(name,"mmgen-walletconv",opts+[infile],extra_desc=d)
+		t.license()
+		if pw:
+			t.passphrase_new("new "+desc,cfg['wpasswd'])
+			t.usr_rand(10)
+		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: "],"YES\n")
+			if ret == 0:
+				t.expect("Enter file size: ","1234\n")
+		if out_fmt == "w": t.label()
+		wf = t.written_to_file(capfirst(desc),oo=True)
+		ok()
+
+		d = "(check)"
+		if desc == "hidden incognito data":
+			self.keygen_chksum_chk_hincog(name,cfg['seed_id'],uopts_chk)
+# 		elif pw:
+# 			self.walletchk_chksum_chk(name,wf,cfg['seed_id'],uopts=uopts_chk)
+		else:
+			self.keygen_chksum_chk(name,wf,cfg['seed_id'],pw=pw)
+
+	def walletconv_in(self,name,infile,desc,uopts=[],pw=False,oo=False):
+		opts = ["-d",cfg['tmpdir'],"-o","words","-r10"]
+		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.close()
+		ok()
+		# back check of result
+		d = "(check)"
+		self.keygen_chksum_chk(name,wf,cfg['seed_id'])
+
+	# 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_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","%s,%s" % (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_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,extra_uopts=[]):
+		ic_f = os.path.join(cfg['tmpdir'],"rand.data")
+		hi_parms = "%s,%s" % (ic_f,ref_wallet_incog_offset)
+		hi_parms_legacy = "%s,%s,%s"%(ic_f,ref_wallet_incog_offset,cfg['seed_len'])
+		self.walletconv_out(name,
+			"hidden incognito data", "hi",
+			uopts=["-J",hi_parms] + extra_uopts,
+			uopts_chk=["-G",hi_parms_legacy],
+			pw=True
+		)
+
+	ref_wallet_conv1 = ref_wallet_conv2 = ref_wallet_conv3 = ref_wallet_conv
+	ref_mn_conv1 = ref_mn_conv2 = ref_mn_conv3 = ref_mn_conv
+	ref_seed_conv1 = ref_seed_conv2 = ref_seed_conv3 = ref_seed_conv
+	ref_brain_conv1 = ref_brain_conv2 = ref_brain_conv3 = ref_brain_conv
+	ref_incog_conv1 = ref_incog_conv2 = ref_incog_conv3 = ref_incog_conv
+	ref_incox_conv1 = ref_incox_conv2 = ref_incox_conv3 = ref_incox_conv
+	ref_hincog_conv1 = ref_hincog_conv2 = ref_hincog_conv3 = ref_hincog_conv
+	ref_hincog_conv_old1 = ref_hincog_conv_old2 = ref_hincog_conv_old3 = ref_hincog_conv_old
+
+	ref_wallet_conv_out1 = ref_wallet_conv_out2 = ref_wallet_conv_out3 = ref_wallet_conv_out
+	ref_mn_conv_out1 = ref_mn_conv_out2 = ref_mn_conv_out3 = ref_mn_conv_out
+	ref_seed_conv_out1 = ref_seed_conv_out2 = ref_seed_conv_out3 = ref_seed_conv_out
+	ref_incog_conv_out1 = ref_incog_conv_out2 = ref_incog_conv_out3 = ref_incog_conv_out
+	ref_incox_conv_out1 = ref_incox_conv_out2 = ref_incox_conv_out3 = ref_incox_conv_out
+	ref_hincog_conv_out1 = ref_hincog_conv_out2 = ref_hincog_conv_out3 = ref_hincog_conv_out
+
+	def ref_wallet_chk(self,name):
+		wf = os.path.join(ref_dir,cfg['ref_wallet'])
+		self.walletchk(name,wf)
+
+	ref_wallet_chk1 = ref_wallet_chk2 = ref_wallet_chk3 = ref_wallet_chk
+
+	def ref_seed_chk(self,name,ext=g.seed_ext):
+		wf = os.path.join(ref_dir,"%s.%s" % (cfg['seed_id'],ext))
+		desc = "seed data" if ext == g.seed_ext else "mnemonic"
+		self.keygen_chksum_chk(name,wf,cfg['seed_id'])
+
+	ref_seed_chk1 = ref_seed_chk2 = ref_seed_chk3 = ref_seed_chk
+
+	def ref_mn_chk(self,name): self.ref_seed_chk(name,ext=g.mn_ext)
+
+	ref_mn_chk1 = ref_mn_chk2 = ref_mn_chk3 = ref_mn_chk
+
+	def ref_brain_chk(self,name,bw_file=ref_bw_file):
+		wf = os.path.join(ref_dir,bw_file)
+		arg = "-b%s,%s" % (cfg['seed_len'],ref_bw_hash_preset)
+		self.keygen_chksum_chk(name,wf,cfg['ref_bw_seed_id'],[arg])
+
+	def keygen_chksum_chk_hincog(self,name,seed_id,hincog_parm):
+		t = MMGenExpect(name,"mmgen-keygen", ["-p1","-q","-S","-A"]+hincog_parm+["1"],extra_desc="(check)")
+		t.passphrase("",cfg['wpasswd'])
+		t.expect("Encrypt key list? (y/N): ","\n")
+		t.expect("any printable ASCII symbol.\r\n")
+		chk = t.readline()[:8]
+		vmsg("Seed ID: %s" % cyan(chk))
+		cmp_or_die(seed_id,chk)
+
+	def keygen_chksum_chk(self,name,wf,seed_id,args=[],pw=False):
+		hp_arg = ["-p1"] if pw else []
+		t = MMGenExpect(name,"mmgen-keygen", ["-q","-S","-A"]+args+hp_arg+[wf,"1"],extra_desc="(check)")
+		if pw:
+			t.passphrase("",cfg['wpasswd'])
+		t.expect("Encrypt key list? (y/N): ","\n")
+		t.expect("any printable ASCII symbol.\r\n")
+		chk = t.readline()[:8]
+		vmsg("Seed ID: %s" % cyan(chk))
+		cmp_or_die(seed_id,chk)
+
+	# Use this for encrypted wallets instead of keygen_chksum_chk()
+	def walletchk_chksum_chk(self,name,wf,seed_id,uopts=[]):
+		t = MMGenExpect(name,"mmgen-walletchk",["-v", wf]+uopts,
+							extra_desc="(check)")
+		t.passphrase("",cfg['wpasswd'])
+		chk = t.expect_getend("Seed ID checksum OK (")[:8]
+		t.close()
+		cmp_or_die(seed_id,chk)
+
+	ref_brain_chk1 = ref_brain_chk2 = ref_brain_chk3 = ref_brain_chk
+
+	def ref_brain_chk_spc3(self,name):
+		self.ref_brain_chk(name,bw_file=ref_bw_file_spc)
+
+	def ref_hincog_chk(self,name):
+		for wtype,desc,earg in ('hic_wallet','',[]), \
+								('hic_wallet_old','(old format)',["-o"]):
+			ic_arg = "%s,%s,%s" % (
+						os.path.join(ref_dir,cfg[wtype]),
+						ref_wallet_incog_offset,cfg['seed_len']
+					)
+			t = MMGenExpect(name,"mmgen-keygen",
+					["-q","-A"]+earg+["-G"]+[ic_arg]+['1'],extra_desc=desc)
+			t.passphrase("incognito wallet",cfg['wpasswd'])
+			t.hash_preset("incog wallet","1")
+			if wtype == 'hic_wallet_old':
+				t.expect("Is the seed ID correct? (Y/n): ","\n")
+			chk = t.expect_getend("Valid incog data for seed ID ")
+			t.close()
+			cmp_or_die(cfg['seed_id'],chk)
+
+	ref_hincog_chk1 = ref_hincog_chk2 = ref_hincog_chk3 = ref_hincog_chk
+
+	def ref_addrfile_chk(self,name,ftype="addr"):
+		wf = os.path.join(ref_dir,cfg['ref_'+ftype+'file'])
+		t = MMGenExpect(name,"mmgen-tool",[ftype+"file_chksum",wf])
+		if ftype == "keyaddr":
+			w = "key-address file"
+			t.hash_preset(w,ref_kafile_hash_preset)
+			t.passphrase(w,ref_kafile_pass)
+			t.expect("Check key-to-address validity? (y/N): ","y")
+		o = t.expect_getend("Checksum for .*address data .*: ",regex=True)
+		cmp_or_die(cfg['ref_'+ftype+'file_chksum'],o)
+
+	def ref_keyaddrfile_chk(self,name):
+		self.ref_addrfile_chk(name,ftype="keyaddr")
+
+#	def txcreate8(self,name,addrfile):
+#		self.txcreate_common(name,sources=['8'])
+
+	def ref_tx_chk(self,name):
+		tf = os.path.join(ref_dir,cfg['ref_tx_file'])
+		wf = os.path.join(ref_dir,cfg['ref_wallet'])
+		self.txsign(name,tf,wf,save=False)
+
+	def ref_tool_decrypt(self,name):
+		f = os.path.join(ref_dir,ref_enc_fn)
+		t = MMGenExpect(name,"mmgen-tool",
+				["-q","decrypt",f,"outfile=-","hash_preset=1"])
+		t.passphrase("user data",tool_enc_passwd)
+		t.readline()
+		import re
+		o = re.sub('\r\n','\n',t.read())
+		cmp_or_die(sample_text,o)
+
+# 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())
+ts = MMGenTestSuite()
+
+for cfg in sorted(cfgs): mk_tmpdir(cfgs[cfg])
+
+try:
+	if cmd_args:
+		for arg in cmd_args:
+			if arg in utils:
+				globals()[arg](cmd_args[cmd_args.index(arg)+1:])
+				sys.exit()
+			elif arg in meta_cmds:
+				for cmd in meta_cmds[arg]:
+					check_needs_rerun(ts,cmd,build=True)
+			elif arg in cmd_groups.keys():
+				for cmd in cmd_groups[arg]:
+					check_needs_rerun(ts,cmd,build=True)
+			elif arg in cmd_data:
+				check_needs_rerun(ts,arg,build=True)
+			else:
+				die(1,"%s: unrecognized command" % arg)
+	else:
+		clean()
+		for cmd in cmd_data:
+			ts.do_cmd(cmd)
+			if cmd is not cmd_data.keys()[-1]: do_between()
+except:
+	sys.stderr = stderr_save
+	raise
+
+t = int(time.time()) - start_time
+sys.stderr.write(green(
+	"All requested tests finished OK, elapsed time: %02i:%02i\n"
+	% (t/60,t%60)))

+ 116 - 87
test/test.py

@@ -10,7 +10,7 @@ sys.path.__setitem__(0,os.path.abspath(os.curdir))
 
 import mmgen.globalvars as g
 import mmgen.opt as opt
-from mmgen.util import mmsg,mdie,Msg,die
+from mmgen.util import mmsg,mdie,Msg,die,capfirst
 from mmgen.test import *
 
 hincog_fn      = "rand_data"
@@ -376,6 +376,7 @@ opts_data = {
                     debugging only)
 -e, --exact-output  Show the exact output of the MMGen script(s) being run
 -l, --list-cmds     List and describe the tests and commands in the test suite
+-n, --names         Display command names instead of descriptions
 -p, --pause         Pause between tests, resuming on keypress
 -q, --quiet         Produce minimal output.  Suppress dependency info
 -s, --system        Test scripts and modules installed on system rather than
@@ -392,6 +393,9 @@ cmd_args = opt.opts.init(opts_data)
 
 if opt.system: sys.path.pop(0)
 
+# temporary
+#os.environ["MMGEN_USE_OLD_SCRIPTS"] = "1"
+
 if opt.debug_scripts: os.environ["MMGEN_DEBUG"] = "1"
 
 if opt.buf_keypress:
@@ -524,15 +528,15 @@ class MMGenExpect(object):
 	def __init__(self,name,mmgen_cmd,cmd_args=[],extra_desc=""):
 		if not opt.system:
 			mmgen_cmd = os.path.join(os.curdir,mmgen_cmd)
-		desc = cmd_data[name][1]
+		desc = (cmd_data[name][1],name)[int(bool(opt.names))]
 		if extra_desc: desc += " " + extra_desc
 		if opt.verbose or opt.exact_output:
 			sys.stderr.write(
-				green("Testing %s\nExecuting " % desc) +
+				green("Testing: %s\nExecuting " % desc) +
 				cyan("'%s %s'\n" % (mmgen_cmd," ".join(cmd_args)))
 			)
 		else:
-			msg_r("Testing %s " % (desc+":"))
+			msg_r("Testing %s: " % desc)
 
 		if opt.direct_exec:
 			os.system(" ".join([mmgen_cmd] + cmd_args))
@@ -545,6 +549,14 @@ class MMGenExpect(object):
 		p = "'w' for conditions and warranty info, or 'c' to continue: "
 		my_expect(self.p,p,'c')
 
+	def label(self,label="Test Label"):
+		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):
+		m = "%suser-supplied entropy" % ("saved " if saved else "")
+		my_expect(self.p,"Generating encryption key from OS random data plus " + m)
+
 	def usr_rand(self,num_chars):
 		rand_chars = list(getrandstr(num_chars,no_space=True))
 		my_expect(self.p,'symbols left: ','x')
@@ -645,8 +657,9 @@ def create_fake_unspent_data(adata,unspent_data_file,tx_data,non_mmgen_input='')
 	for s in tx_data.keys():
 		sid = tx_data[s]['sid']
 		a = adata.addrinfo(sid)
-		for idx,btcaddr in a.addrpairs():
-			add_fake_unspent_entry(out,btcaddr,"%s:%s Test Wallet" % (sid,idx))
+		for n,(idx,btcaddr) in enumerate(a.addrpairs(),1):
+			lbl = (""," addr %02i" % n)[int(bool(n%3))]
+			add_fake_unspent_entry(out,btcaddr,"%s:%s%s" % (sid,idx,lbl))
 
 	if non_mmgen_input:
 		from mmgen.bitcoin import privnum2addr,hextowif
@@ -821,47 +834,32 @@ class MMGenTestSuite(object):
 	def generate_cmd_deps(self,fdeps):
 		return [cfgs[str(n)]['dep_generators'][ext] for n,ext in fdeps]
 
-	def walletgen(self,name,brain=False,seed_len=None):
-
+	def walletgen(self,name,seed_len=None):
 		args = ["-d",cfg['tmpdir'],"-p1","-r10"]
 		if seed_len: args += ["-l",str(seed_len)]
-#        if 'seed_len' in cfg: args += ["-l",cfg['seed_len']]
-		if brain:
-			bwf = os.path.join(cfg['tmpdir'],cfg['bw_filename'])
-			args += ["-b",cfg['bw_params'],bwf]
-			make_brainwallet_file(bwf)
-
 		t = MMGenExpect(name,"mmgen-walletgen", args)
 		t.license()
-
-		if brain:
-			t.expect(
-	"A brainwallet will be secure only if you really know what you're doing")
-			t.expect("Type uppercase 'YES' to confirm: ","YES\n")
-
 		t.usr_rand(10)
-		for s in "user-supplied entropy","saved user-supplied entropy":
-			t.expect("Generating encryption key from OS random data plus %s" % s)
-			if brain: break
-
 		t.passphrase_new("new MMGen wallet",cfg['wpasswd'])
-		t.written_to_file("Wallet")
+		t.label()
+		t.written_to_file("MMGen wallet")
 		ok()
 
-	def refwalletgen(self,name):
+	def brainwalletgen_ref(self,name):
+		sl_arg = "-l%s" % cfg['seed_len']
+		hp_arg = "-p%s" % ref_wallet_hash_preset
 		label = "test.py ref. wallet (pw '%s', seed len %s)" \
-					% (ref_wallet_brainpass,cfg['seed_len'])
-		bw_arg = "-b%s,%s" % (cfg['seed_len'], ref_wallet_hash_preset)
-		args = ["-d",cfg['tmpdir'],"-p1","-r10",bw_arg,"-L",label]
-		d = " (%s-bit seed)" % cfg['seed_len']
-		t = MMGenExpect(name,"mmgen-walletgen", args)
+				% (ref_wallet_brainpass,cfg['seed_len'])
+		args = ["-d",cfg['tmpdir'],hp_arg,"-r10",sl_arg,"-ib","-L",label]
+		t = MMGenExpect(name,"mmgen-walletconv", args)
 		t.license()
-		t.expect("Type uppercase 'YES' to confirm: ","YES\n")
-		t.expect("passphrase: ",ref_wallet_brainpass+"\n")
-		t.usr_rand(10)
+		t.expect("Enter brainwallet: ", ref_wallet_brainpass+"\n")
 		t.passphrase_new("new MMGen wallet",cfg['wpasswd'])
-		seed_id = t.written_to_file("Wallet").split("-")[0].split("/")[-1]
-		refcheck("seed ID",seed_id,cfg['seed_id'])
+		t.usr_rand(10)
+		sid = t.written_to_file("MMGen wallet").split("-")[0].split("/")[-1]
+		refcheck("seed ID",sid,cfg['seed_id'])
+
+	def refwalletgen(self,name): self.brainwalletgen_ref(name)
 
 	refwalletgen1 = refwalletgen2 = refwalletgen3 = refwalletgen
 
@@ -869,22 +867,23 @@ class MMGenTestSuite(object):
 
 		t = MMGenExpect(name,"mmgen-passchg",
 			["-d",cfg['tmpdir'],"-p","2","-L","New Label","-r","16",walletfile])
+		t.license()
 		t.passphrase("MMGen wallet",cfgs['1']['wpasswd'],pwtype="old")
-		t.expect_getend("Label changed: ")
-		t.expect_getend("Hash preset changed: ")
+		t.expect_getend("Hash preset changed to ")
 		t.passphrase("MMGen wallet",cfg['wpasswd'],pwtype="new")
 		t.expect("Repeat passphrase: ",cfg['wpasswd']+"\n")
 		t.usr_rand(16)
-		t.expect_getend("Key ID changed: ")
-		t.written_to_file("Wallet")
+		t.expect_getend("Label changed to ")
+#		t.expect_getend("Key ID changed: ")
+		t.written_to_file("MMGen wallet")
 		ok()
 
 	def walletchk_beg(self,name,args):
 		t = MMGenExpect(name,"mmgen-walletchk", args)
-		t.expect("Getting MMGen wallet data from file '%s'" % args[-1])
+		t.expect("Getting MMGen wallet from file '%s'" % args[-1])
 		t.passphrase("MMGen wallet",cfg['wpasswd'])
 		t.expect("Passphrase is OK")
-		t.expect("Wallet is OK")
+		t.expect_getend("Valid MMGen wallet for seed ID ")
 		return t
 
 	def walletchk(self,name,walletfile):
@@ -1036,53 +1035,67 @@ class MMGenTestSuite(object):
 		vmsg("This is a simulation; no transaction was sent")
 		ok()
 
-	def export_seed(self,name,walletfile):
-		t = self.walletchk_beg(name,["-s","-d",cfg['tmpdir'],walletfile])
-		f = t.written_to_file("Seed data")
-		silence()
-		msg("Seed data: %s" % cyan(get_data_from_file(f,"seed data")))
-		end_silence()
-		ok()
+	def walletconv_export(self,name,wf,desc,uargs=[],out_fmt="w",pw=False):
+		opts = ["-d",cfg['tmpdir'],"-o",out_fmt] + uargs + [wf]
+		t = MMGenExpect(name,"mmgen-walletconv",opts)
+		t.license()
+		t.passphrase("MMGen wallet",cfg['wpasswd'])
+		if pw:
+			t.passphrase_new("new "+desc,cfg['wpasswd'])
+			t.usr_rand(10)
+		if " ".join(desc.split()[-2:]) == "incognito data":
+			t.expect("Generating encryption key from OS random data ")
+			t.expect("Generating encryption key from OS random data ")
+			ic_id = t.expect_getend("New Incog Wallet ID: ")
+			t.expect("Generating encryption key from OS random data ")
+		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)
 
-	def export_mnemonic(self,name,walletfile):
-		t = self.walletchk_beg(name,["-m","-d",cfg['tmpdir'],walletfile])
-		f = t.written_to_file("Mnemonic data")
+	def export_seed(self,name,wf,desc="seed data",out_fmt="seed"):
+		f = self.walletconv_export(name,wf,desc=desc,out_fmt=out_fmt)
 		silence()
-		msg_r("Mnemonic data: %s" % cyan(get_data_from_file(f,"mnemonic data")))
+		msg("%s: %s" % (capfirst(desc),cyan(get_data_from_file(f,desc))))
 		end_silence()
 		ok()
 
-	def export_incog(self,name,walletfile,args=["-g"]):
-		t = MMGenExpect(name,"mmgen-walletchk",args+["-d",cfg['tmpdir'],"-r","10",walletfile])
-		t.passphrase("MMGen wallet",cfg['wpasswd'])
-		t.usr_rand(10)
-		incog_id = t.expect_getend("Incog ID: ")
-		write_to_tmpfile(cfg,incog_id_fn,incog_id+"\n")
-		if args[0] == "-G": return t
-		t.written_to_file("Incognito wallet data",overwrite_unlikely=True)
+	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","-r10"] + add_args
+		self.walletconv_export(name,wf,desc=desc,out_fmt=out_fmt,uargs=uargs,pw=True)
 		ok()
 
-	def export_incog_hex(self,name,walletfile):
-		self.export_incog(name,walletfile,args=["-X"])
+	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,walletfile):
-		rf,rd = os.path.join(cfg['tmpdir'],hincog_fn),os.urandom(hincog_bytes)
-		vmsg(green("Writing %s bytes of data to file '%s'" % (hincog_bytes,rf)))
-		write_to_file(rf,rd,verbose=opt.verbose)
-		t = self.export_incog(name,walletfile,args=["-G","%s,%s"%(rf,hincog_offset)])
-		t.written_to_file("Data",query="")
-		ok()
+	def export_incog_hidden(self,name,wf):
+		rf = os.path.join(cfg['tmpdir'],hincog_fn)
+		add_args = ["-J","%s,%s"%(rf,hincog_offset)]
+		self.export_incog(
+			name,wf,desc="hidden incognito data",out_fmt="hi",add_args=add_args)
 
 	def addrgen_seed(self,name,walletfile,foo,desc="seed data",arg="-s"):
-		t = MMGenExpect(name,"mmgen-addrgen",
+		stdout = (False,True)[int(desc=="seed data")] #capture output to screen once
+		add_arg = ([],["-S"])[int(stdout)]
+		t = MMGenExpect(name,"mmgen-addrgen", add_arg +
 				[arg,"-d",cfg['tmpdir'],walletfile,cfg['addr_idx_list']])
 		t.license()
 		t.expect_getend("Valid %s for seed ID " % 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)
-		t.no_overwrite()
+#		t.no_overwrite()
 		ok()
 
 	def addrgen_mnemonic(self,name,walletfile,foo):
@@ -1097,8 +1110,9 @@ class MMGenTestSuite(object):
 		t.hash_preset("incog wallet",'1')
 		vmsg("Comparing generated checksum with checksum from address file")
 		chk = t.expect_getend(r"Checksum for address data .*?: ",regex=True)
+		t.close()
 		verify_checksum_or_exit(get_addrfile_checksum(),chk)
-		t.no_overwrite()
+#		t.no_overwrite()
 		ok()
 
 	def addrgen_incog_hex(self,name,walletfile,foo):
@@ -1180,8 +1194,20 @@ class MMGenTestSuite(object):
 		self.txsign_end(t)
 		ok()
 
-	def walletgen4(self,name):
-		self.walletgen(name,brain=True)
+	def brainwalletgen_pwfile(self,name):
+		bwf = os.path.join(cfg['tmpdir'],cfg['bw_filename'])
+		make_brainwallet_file(bwf)
+		seed_len = str(cfg['seed_len'])
+		args = ["-d",cfg['tmpdir'],"-p1","-r10","-l"+seed_len,"-ib"]
+		t = MMGenExpect(name,"mmgen-walletconv", args + [bwf])
+		t.license()
+		t.passphrase_new("new MMGen wallet",cfg['wpasswd'])
+		t.usr_rand(10)
+		t.label()
+		t.written_to_file("MMGen wallet")
+		ok()
+
+	def walletgen4(self,name): self.brainwalletgen_pwfile(name)
 
 	def addrgen4(self,name,walletfile):
 		self.addrgen(name,walletfile)
@@ -1239,30 +1265,33 @@ class MMGenTestSuite(object):
 		vmsg("Incog ID: %s" % 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 \w{8} found at offset ",regex=True)
+		o = t.expect_getend("Incog data for ID %s found at offset " % i_id)
+		os.unlink(f1)
 		cmp_or_die(hincog_offset,int(o))
 
 	def walletconv_out(self,name,desc,out_fmt="w",uopts=[],uopts_chk=[],pw=False):
 		opts = ["-d",cfg['tmpdir'],"-r10","-p1","-o",out_fmt] + uopts
 		infile = os.path.join(ref_dir,cfg['seed_id']+".mmwords")
-		d = "(convert)"
-		t = MMGenExpect(name,"mmgen-walletconv",opts+[infile],extra_desc=d)
+		t = MMGenExpect(name,"mmgen-walletconv",opts+[infile],extra_desc="(convert)")
 		t.license()
 		if pw:
 			t.passphrase_new("new "+desc,cfg['wpasswd'])
 			t.usr_rand(10)
+		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: "],"YES\n")
+			ret = t.expect(["Create? (Y/n): ","'YES' to confirm: "])
 			if ret == 0:
-				t.expect("Enter file size: ","1234\n")
-		wf = t.written_to_file(desc[0].upper()+desc[1:],oo=True)
+				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)
 		ok()
-
-		d = "(check)"
 		if desc == "hidden incognito data":
 			self.keygen_chksum_chk_hincog(name,cfg['seed_id'],uopts_chk)
-# 		elif pw:
-# 			self.walletchk_chksum_chk(name,wf,cfg['seed_id'],uopts=uopts_chk)
 		else:
 			self.keygen_chksum_chk(name,wf,cfg['seed_id'],pw=pw)
 
@@ -1389,7 +1418,7 @@ class MMGenTestSuite(object):
 		t = MMGenExpect(name,"mmgen-keygen", ["-p1","-q","-S","-A"]+hincog_parm+["1"],extra_desc="(check)")
 		t.passphrase("",cfg['wpasswd'])
 		t.expect("Encrypt key list? (y/N): ","\n")
-		t.expect("any printable ASCII symbol.\r\n")
+		t.expect("ignored by MMGen.\r\n")
 		chk = t.readline()[:8]
 		vmsg("Seed ID: %s" % cyan(chk))
 		cmp_or_die(seed_id,chk)
@@ -1400,7 +1429,7 @@ class MMGenTestSuite(object):
 		if pw:
 			t.passphrase("",cfg['wpasswd'])
 		t.expect("Encrypt key list? (y/N): ","\n")
-		t.expect("any printable ASCII symbol.\r\n")
+		t.expect("ignored by MMGen.\r\n")
 		chk = t.readline()[:8]
 		vmsg("Seed ID: %s" % cyan(chk))
 		cmp_or_die(seed_id,chk)