Browse Source

* better options handling
* code cleanups
* new commands in 'mmgen-tool'
* tx view in 'mmgen-txsend'

philemon 10 years ago
parent
commit
38e0a954d0
19 changed files with 579 additions and 661 deletions
  1. 0 1
      MANIFEST
  2. 48 62
      mmgen-addrgen
  3. 5 9
      mmgen-addrimport
  4. 12 15
      mmgen-passchg
  5. 5 10
      mmgen-pywallet
  6. 4 4
      mmgen-tool
  7. 36 36
      mmgen-txcreate
  8. 17 28
      mmgen-txsend
  9. 20 27
      mmgen-txsign
  10. 7 15
      mmgen-walletchk
  11. 17 28
      mmgen-walletgen
  12. 72 50
      mmgen/Opts.py
  13. 26 35
      mmgen/addr.py
  14. 10 4
      mmgen/config.py
  15. 3 3
      mmgen/license.py
  16. 109 90
      mmgen/tool.py
  17. 156 198
      mmgen/tx.py
  18. 31 45
      mmgen/util.py
  19. 1 1
      setup.py

+ 0 - 1
MANIFEST

@@ -41,4 +41,3 @@ mmgen/tests/mnemonic.py
 mmgen/tests/test.py
 mmgen/tests/test.py
 mmgen/tests/util.py
 mmgen/tests/util.py
 mmgen/tests/walletgen.py
 mmgen/tests/walletgen.py
-test/test.py

+ 48 - 62
mmgen-addrgen

@@ -30,51 +30,51 @@ from mmgen.license import *
 from mmgen.util import *
 from mmgen.util import *
 from mmgen.addr import *
 from mmgen.addr import *
 
 
-invoked_as = sys.argv[0].split("-")[-1]
-gen_what = "addresses" if invoked_as == "addrgen" else "keys"
-
-if invoked_as == "keygen":
-	extra_help_data = (
-		"\n-A, --no-addresses       Print only secret keys, no addresses",
-		"\n-x, --b16                Print secret keys in hexadecimal too",
-		"\nBy default, both addresses and secret keys are generated."
-	)
-else: extra_help_data = ("","","")
+gen_what = "addresses" if sys.argv[0].split("-")[-1] == "addrgen" else "keys"
 
 
 help_data = {
 help_data = {
 	'prog_name': sys.argv[0].split("/")[-1],
 	'prog_name': sys.argv[0].split("/")[-1],
-	'desc': """Generate a list or range of {} from a {} wallet,
-                  mnemonic, seed or password""".format(gen_what,g.proj_name),
+	'desc': """Generate a list or range of {} from an {g.proj_name} wallet,
+                  mnemonic, seed or password""".format(gen_what,g=g),
 	'usage':"[opts] [infile] <address list>",
 	'usage':"[opts] [infile] <address list>",
 	'options': """
 	'options': """
 -h, --help               Print this help message{}
 -h, --help               Print this help message{}
--d, --outdir          d  Specify an alternate directory 'd' for output
+-d, --outdir=         d  Specify an alternate directory 'd' for output
 -e, --echo-passphrase    Echo passphrase or mnemonic to screen upon entry
 -e, --echo-passphrase    Echo passphrase or mnemonic to screen upon entry
 -H, --show-hash-presets  Show information on available hash presets
 -H, --show-hash-presets  Show information on available hash presets
 -K, --no-keyconv         Use internal libraries for address generation
 -K, --no-keyconv         Use internal libraries for address generation
                          instead of 'keyconv'
                          instead of 'keyconv'
--l, --seed-len        N  Length of seed.  Options: {}
-                         (default: {})
--p, --hash-preset     p  Use scrypt.hash() parameters from preset 'p'
-                         when hashing password (default: '{}')
--P, --passwd-file     f  Get passphrase from file 'f'
+-l, --seed-len=       N  Length of seed.  Options: {seed_lens}
+                         (default: {g.seed_len})
+-p, --hash-preset=    p  Use scrypt.hash() parameters from preset 'p' when
+                         hashing password (default: '{g.hash_preset}')
+-P, --passwd-file=    f  Get passphrase from file 'f'
 -q, --quiet              Suppress warnings; overwrite files without
 -q, --quiet              Suppress warnings; overwrite files without
                          prompting
                          prompting
--S, --stdout             Print {W} to stdout
+-S, --stdout             Print {what} to stdout
 -v, --verbose            Produce more verbose output{}
 -v, --verbose            Produce more verbose output{}
 
 
--b, --from-brain     l,p Generate {W} from a user-created password,
+-b, --from-brain=    l,p Generate {what} from a user-created password,
                          i.e. a "brainwallet", using seed length 'l' and
                          i.e. a "brainwallet", using seed length 'l' and
                          hash preset 'p' (comma-separated)
                          hash preset 'p' (comma-separated)
--g, --from-incog         Generate {W} from an incognito wallet
--X, --from-incog-hex     Generate {W} from an incognito hexadecimal wallet
--G, --from-incog-hidden  f,o,l  Generate {W} from incognito data in file
+-g, --from-incog         Generate {what} from an incognito wallet
+-X, --from-incog-hex     Generate {what} from incognito hexadecimal wallet
+-G, --from-incog-hidden= f,o,l  Generate {what} from incognito data in file
                          'f' at offset 'o', with seed length of 'l'
                          'f' at offset 'o', with seed length of 'l'
--m, --from-mnemonic      Generate {W} from an electrum-like mnemonic
--s, --from-seed          Generate {W} from a seed in .{S} format
-
-Addresses are given in a comma-separated list.  Hyphen-separated
-ranges are also allowed.{}
+-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 gen_what == "keys" else ("","")),
+		seed_lens=", ".join([str(i) for i in g.seed_lens]),
+		what=gen_what, g=g
+),
+	'notes': """
+
+Addresses are given in a comma-separated list.  Hyphen-separated ranges are
+also allowed.{}
 
 
 If available, the external 'keyconv' program will be used for address
 If available, the external 'keyconv' program will be used for address
 generation.
 generation.
@@ -96,36 +96,14 @@ the 'p' parameter of the '--from-brain' option
 The '--from-brain' option also requires the user to specify a seed length
 The '--from-brain' option also requires the user to specify a seed length
 (the 'l' parameter)
 (the 'l' parameter)
 
 
-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(
-		extra_help_data[0],
-		", ".join([str(i) for i in g.seed_lens]),
-		g.seed_len,
-		g.hash_preset,
-		extra_help_data[1],
-		extra_help_data[2],
-		W=gen_what,
-		S=g.seed_ext
-	)
+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 gen_what == "keys" else "")
 }
 }
 
 
-short_opts = ["h","A","d:","e",
-			"H","K","l:","p:",
-			"P:","q","S","v","x","b:",
-			"g","X","G:","m","s"]
-long_opts = ["help","no_addresses","outdir=","echo_passphrase",
-			"show_hash_presets","no_keyconv","seed_len=","hash_preset=",
-			"passwd_file=","quiet","stdout","verbose","b16","from_brain=",
-			"from_incog","from_incog_hex","from_incog_hidden=",
-			"from_mnemonic","from_seed"]
-
-if invoked_as == "addrgen":
-	for i in "A","x":              short_opts.remove(i)
-	for i in "no_addresses","b16": long_opts.remove(i)
-
-opts,cmd_args = process_opts(sys.argv,help_data,"".join(short_opts),long_opts)
+opts,cmd_args = parse_opts(sys.argv,help_data)
 
 
 if 'show_hash_presets' in opts: show_hash_presets()
 if 'show_hash_presets' in opts: show_hash_presets()
 if 'verbose' in opts: g.verbose = True
 if 'verbose' in opts: g.verbose = True
@@ -135,8 +113,6 @@ if 'from_incog_hex' in opts or 'from_incog_hidden' in opts:
 
 
 opts['gen_what'] = gen_what
 opts['gen_what'] = gen_what
 
 
-check_opts(opts,long_opts)
-
 if g.debug: show_opts_and_cmd_args(opts,cmd_args)
 if g.debug: show_opts_and_cmd_args(opts,cmd_args)
 
 
 if len(cmd_args) == 1 and (
 if len(cmd_args) == 1 and (
@@ -158,12 +134,12 @@ if not addr_list: sys.exit(2)
 do_license_msg()
 do_license_msg()
 
 
 # Interact with user:
 # Interact with user:
-if invoked_as == "keygen" and not g.quiet:
+if gen_what == "keys" and not g.quiet:
 	confirm_or_exit(cmessages['unencrypted_secret_keys'], 'continue')
 	confirm_or_exit(cmessages['unencrypted_secret_keys'], 'continue')
 
 
 # Generate data:
 # Generate data:
 
 
-if invoked_as == "addrgen":
+if gen_what == "addresses":
 	opts['print_addresses_only'] = True
 	opts['print_addresses_only'] = True
 else:
 else:
 	if not 'no_addresses' in opts: opts['print_secret'] = True
 	if not 'no_addresses' in opts: opts['print_secret'] = True
@@ -172,11 +148,16 @@ seed          = get_seed_retry(infile,opts)
 seed_id       = make_chksum_8(seed)
 seed_id       = make_chksum_8(seed)
 
 
 addr_data     = generate_addrs(seed, addr_list, opts)
 addr_data     = generate_addrs(seed, addr_list, opts)
-addr_data_str = format_addr_data(addr_data, seed_id, opts)
+addr_data_chksum = make_chksum_8(
+	" ".join(["{} {}".format(a['num'],a['addr']) for a in addr_data]),
+	sep=True
+)
+addr_data_str = format_addr_data(
+		addr_data, addr_data_chksum, seed_id, addr_list, opts)
 
 
 # Output data:
 # Output data:
 if 'stdout' in opts:
 if 'stdout' in opts:
-	if invoked_as == "keygen" and not g.quiet:
+	if gen_what == "keys" and not g.quiet:
 		confirm = True
 		confirm = True
 	else: confirm = False
 	else: confirm = False
 	write_to_stdout(addr_data_str,"secret keys",confirm)
 	write_to_stdout(addr_data_str,"secret keys",confirm)
@@ -184,3 +165,8 @@ elif not sys.stdout.isatty():
 	write_to_stdout(addr_data_str,"secret keys",confirm=False)
 	write_to_stdout(addr_data_str,"secret keys",confirm=False)
 else:
 else:
 	write_addr_data_to_file(seed, addr_data_str, addr_list, opts)
 	write_addr_data_to_file(seed, addr_data_str, addr_list, opts)
+
+msg("""
+Checksum for address data {}[{}]: {}
+Remember this checksum or save it to a secure location
+""".format(seed_id, fmt_addr_list(addr_list), addr_data_chksum).strip())

+ 5 - 9
mmgen-addrimport

@@ -33,25 +33,21 @@ help_data = {
 	'usage':"[opts] [mmgen address file]",
 	'usage':"[opts] [mmgen address file]",
 	'options': """
 	'options': """
 -h, --help        Print this help message
 -h, --help        Print this help message
--l, --addrlist f  Import the non-mmgen Bitcoin addresses listed in file 'f'
+-l, --addrlist= f Import the non-mmgen Bitcoin addresses listed in file 'f'
+-q, --quiet       Suppress warnings
 -r, --rescan      Rescan the blockchain.  Required if address to import is
 -r, --rescan      Rescan the blockchain.  Required if address to import is
                   on the blockchain and has a balance.  Rescanning is slow.
                   on the blockchain and has a balance.  Rescanning is slow.
--q, --quiet       Suppress warnings
 """
 """
 }
 }
 
 
-short_opts = "hl:qr"
-long_opts  = "help", "addrlist=", "quiet", "rescan"
+opts,cmd_args = parse_opts(sys.argv,help_data)
 
 
-opts,cmd_args = process_opts(sys.argv,help_data,"".join(short_opts),long_opts)
 if 'quiet' in opts: g.quiet = True
 if 'quiet' in opts: g.quiet = True
 
 
 if len(cmd_args) != 1 and not 'addrlist' in opts:
 if len(cmd_args) != 1 and not 'addrlist' in opts:
 	msg("You must specify an mmgen address list (and/or non-mmgen addresses with the '--addrlist' option)")
 	msg("You must specify an mmgen address list (and/or non-mmgen addresses with the '--addrlist' option)")
 	sys.exit(1)
 	sys.exit(1)
 
 
-check_opts(opts,long_opts)
-
 if cmd_args:
 if cmd_args:
 	check_infile(cmd_args[0])
 	check_infile(cmd_args[0])
 	seed_id,addr_data = parse_addrs_file(cmd_args[0])
 	seed_id,addr_data = parse_addrs_file(cmd_args[0])
@@ -59,9 +55,9 @@ else:
 	seed_id,addr_data = "",[]
 	seed_id,addr_data = "",[]
 
 
 if 'addrlist' in opts:
 if 'addrlist' in opts:
-	l = get_lines_from_file(opts['addrlist'],"non-mmgen addresses",
+	lines = get_lines_from_file(opts['addrlist'],"non-mmgen addresses",
 			remove_comments=True)
 			remove_comments=True)
-	addr_data += [(None,i) for i in l]
+	addr_data += [(None,l) for l in lines]
 
 
 from mmgen.bitcoin import verify_addr
 from mmgen.bitcoin import verify_addr
 qmsg_r("Validating addresses...")
 qmsg_r("Validating addresses...")

+ 12 - 15
mmgen-passchg

@@ -26,37 +26,34 @@ from mmgen.util import *
 import mmgen.config as g
 import mmgen.config as g
 
 
 help_data = {
 help_data = {
-	'desc':  """Change the passphrase, hash preset or label of a {}
+	'desc':  """Change the passphrase, hash preset or label of an {}
                   deterministic wallet""".format(g.proj_name),
                   deterministic wallet""".format(g.proj_name),
 	'usage':   "[opts] [filename]",
 	'usage':   "[opts] [filename]",
 	'options': """
 	'options': """
 -h, --help                  Print this help message
 -h, --help                  Print this help message
--d, --outdir             d  Specify an alternate directory 'd' for output
+-d, --outdir=            d  Specify an alternate directory 'd' for output
 -H, --show-hash-presets     Show information on available hash presets
 -H, --show-hash-presets     Show information on available hash presets
 -k, --keep-old-passphrase   Keep old passphrase (use when changing hash
 -k, --keep-old-passphrase   Keep old passphrase (use when changing hash
                             strength or label only)
                             strength or label only)
--L, --label              l  Change the wallet's label to 'l'
--p, --hash-preset        p  Change scrypt.hash() parameters to preset 'p'
-                            (default: '{}')
--P, --passwd-file        f  Get new passphrase from file 'f'
+-L, --label=             l  Change the wallet's label to 'l'
+-p, --hash-preset=       p  Change scrypt.hash() parameters to preset 'p'
+                            (default: '{g.hash_preset}')
+-P, --passwd-file=       f  Get new passphrase from file 'f'
 -q, --quiet                 Suppress warnings; overwrite files without
 -q, --quiet                 Suppress warnings; overwrite files without
                             prompting
                             prompting
 -v, --verbose               Produce more verbose output
 -v, --verbose               Produce more verbose output
+""".format(g=g),
+	'notes': """
 
 
-NOTE: The key ID will change if either the passphrase or hash preset
-      are changed
-""".format(g.hash_preset)
+NOTE: The key ID will change if either the passphrase or hash preset are
+      changed
+"""
 }
 }
 
 
-short_opts = "hd:HkL:p:P:qv"
-long_opts  = "help","outdir=","show_hash_presets","keep_old_passphrase",\
-			"label=","hash_preset=","passwd_file=","quiet","verbose"
+opts,cmd_args = parse_opts(sys.argv,help_data)
 
 
-opts,cmd_args = process_opts(sys.argv,help_data,short_opts,long_opts)
 if 'quiet' in opts: g.quiet = True
 if 'quiet' in opts: g.quiet = True
 if 'verbose' in opts: g.verbose = True
 if 'verbose' in opts: g.verbose = True
-check_opts(opts,long_opts)
-
 if 'show_hash_presets' in opts: show_hash_presets()
 if 'show_hash_presets' in opts: show_hash_presets()
 
 
 if len(cmd_args) != 1:
 if len(cmd_args) != 1:

+ 5 - 10
mmgen-pywallet

@@ -75,26 +75,21 @@ help_data = {
 	'usage':   "[opts] <bitcoind wallet file>",
 	'usage':   "[opts] <bitcoind wallet file>",
 	'options': """
 	'options': """
 -h, --help             Print this help message
 -h, --help             Print this help message
--d, --outdir        d  Specify an alternate directory 'd' for output
+-d, --outdir=       d  Specify an alternate directory 'd' for output
 -e, --echo-passphrase  Display passphrase on screen upon entry
 -e, --echo-passphrase  Display passphrase on screen upon entry
 -j, --json             Dump wallet in json format
 -j, --json             Dump wallet in json format
 -k, --keys             Dump all private keys (flat list)
 -k, --keys             Dump all private keys (flat list)
 -a, --addrs            Dump all addresses (flat list)
 -a, --addrs            Dump all addresses (flat list)
--K, --keysforaddrs  f  Dump private keys for addresses listed in file 'f'
--P, --passwd-file   f  Get passphrase from file 'f'
+-K, --keysforaddrs= f  Dump private keys for addresses listed in file 'f'
+-P, --passwd-file=  f  Get passphrase from file 'f'
 -S, --stdout           Dump to stdout rather than file
 -S, --stdout           Dump to stdout rather than file
 """
 """
 }
 }
 
 
-short_opts = "hd:ejkaK:P:S"
-long_opts  = "help","outdir=","echo_passphrase","json","keys","addrs",\
-			"keysforaddrs=","passwd_file=","stdout"
+opts,cmd_args = parse_opts(sys.argv,help_data)
 
 
-opts,cmd_args = process_opts(sys.argv,help_data,short_opts,long_opts)
-check_opts(opts,long_opts)
-
-from mmgen.util import check_infile
 if len(cmd_args) == 1:
 if len(cmd_args) == 1:
+	from mmgen.util import check_infile
 	check_infile(cmd_args[0])
 	check_infile(cmd_args[0])
 else:
 else:
 	usage(help_data)
 	usage(help_data)

+ 4 - 4
mmgen-tool

@@ -36,6 +36,8 @@ help_data = {
 -h, --help                 Print this help message
 -h, --help                 Print this help message
 -q, --quiet                Produce quieter output
 -q, --quiet                Produce quieter output
 -v, --verbose              Produce more verbose output
 -v, --verbose              Produce more verbose output
+""",
+	'notes': """
 
 
 COMMANDS:{}
 COMMANDS:{}
 Type '{} <command> --help for usage information on a particular
 Type '{} <command> --help for usage information on a particular
@@ -43,11 +45,9 @@ command
 """.format(command_help,prog_name)
 """.format(command_help,prog_name)
 }
 }
 
 
-short_opts = "hqv"
-long_opts  = "help","quiet","verbose"
+opts,cmd_args = parse_opts(sys.argv,help_data)
 
 
-opts,cmd_args = process_opts(sys.argv,help_data,short_opts,long_opts)
-if 'quiet' in opts: g.quiet = True
+if 'quiet' in opts:   g.quiet = True
 if 'verbose' in opts: g.verbose = True
 if 'verbose' in opts: g.verbose = True
 
 
 if len(cmd_args) < 1:
 if len(cmd_args) < 1:

+ 36 - 36
mmgen-txcreate

@@ -37,34 +37,32 @@ help_data = {
 	'usage':   "[opts]  <addr,amt> ... [change addr] [addr file] ...",
 	'usage':   "[opts]  <addr,amt> ... [change addr] [addr file] ...",
 	'options': """
 	'options': """
 -h, --help                Print this help message
 -h, --help                Print this help message
--d, --outdir           d  Specify an alternate directory 'd' for output
+-d, --outdir=          d  Specify an alternate directory 'd' for output
 -e, --echo-passphrase     Print passphrase to screen when typing it
 -e, --echo-passphrase     Print passphrase to screen when typing it
+-f, --tx-fee=          f  Transaction fee (default: {g.tx_fee} BTC)
 -i, --info                Display unspent outputs and exit
 -i, --info                Display unspent outputs and exit
 -q, --quiet               Suppress warnings; overwrite files without
 -q, --quiet               Suppress warnings; overwrite files without
                           prompting
                           prompting
-
--f, --tx-fee           f  Transaction fee (default: %s BTC)
+""".format(g=g),
+	'notes': """
 
 
 Transaction inputs are chosen from a list of the user's unpent outputs
 Transaction inputs are chosen from a list of the user's unpent outputs
 via an interactive menu.
 via an interactive menu.
 
 
 Ages of transactions are approximate based on an average block creation
 Ages of transactions are approximate based on an average block creation
-interval of %s minutes.
+interval of {g.mins_per_block} minutes.
 
 
 Addresses on the command line can be Bitcoin addresses or MMGen addresses
 Addresses on the command line can be Bitcoin addresses or MMGen addresses
 of the form <seed ID>:<number>.
 of the form <seed ID>:<number>.
 
 
 To send all inputs (minus TX fee) to a single output, specify one address
 To send all inputs (minus TX fee) to a single output, specify one address
 with no amount on the command line.
 with no amount on the command line.
-""" % (Decimal(g.tx_fee),g.mins_per_block)
+""".format(g=g)
 }
 }
 
 
-short_opts = "hd:eiqf:"
-long_opts  = "help","outdir=","echo_passphrase","info","quiet","tx_fee="
+opts,cmd_args = parse_opts(sys.argv,help_data)
 
 
-opts,cmd_args = process_opts(sys.argv,help_data,short_opts,long_opts)
 if 'quiet' in opts: g.quiet = True
 if 'quiet' in opts: g.quiet = True
-check_opts(opts,long_opts)
 
 
 if g.debug: show_opts_and_cmd_args(opts,cmd_args)
 if g.debug: show_opts_and_cmd_args(opts,cmd_args)
 
 
@@ -74,31 +72,39 @@ if not 'info' in opts:
 
 
 	tx_out,addr_data,b2m_map,acct_data,change_addr = {},[],{},[],""
 	tx_out,addr_data,b2m_map,acct_data,change_addr = {},[],{},[],""
 
 
-	for a in [i for i in cmd_args if match_ext(i,g.addrfile_ext)]:
-		if match_ext(a,g.addrfile_ext):
-			check_infile(a)
-			addr_data.append(parse_addrs_file(a))
+	addrfiles = [a for a in cmd_args if get_extension(a) == g.addrfile_ext]
+	cmd_args = set(cmd_args) - set(addrfiles)
+
+	for a in addrfiles:
+		check_infile(a)
+		addr_data.append(parse_addrs_file(a))
 
 
-	def mm_addr2btc_addr(c,mmadr,acct_data,addr_data,b2m_map):
-		btcaddr,label = mmgen_addr_to_walletd(c,mmadr,acct_data)
+	def mmaddr2btcaddr(c,mmaddr,acct_data,addr_data,b2m_map):
+		# assume mmaddr has already been checked
+		btcaddr,label = mmaddr2btcaddr_bitcoind(c,mmaddr,acct_data)
 		if not btcaddr:
 		if not btcaddr:
-			btcaddr,label = mmgen_addr_to_addr_data(mmadr,addr_data)
-		b2m_map[btcaddr] = mmadr,label
+			if addr_data:
+				btcaddr,label = mmaddr2btcaddr_addrfile(mmaddr,addr_data)
+			else:
+				msg(txmsg['addrfile_no_data_msg'] % mmaddr)
+				sys.exit(2)
+
+		b2m_map[btcaddr] = mmaddr,label
 		return btcaddr
 		return btcaddr
 
 
-	for a in [i for i in cmd_args if not match_ext(i,g.addrfile_ext)]:
+	for a in cmd_args:
 		if "," in a:
 		if "," in a:
 			a1,a2 = a.split(",")
 			a1,a2 = a.split(",")
 			if is_btc_addr(a1):
 			if is_btc_addr(a1):
 				btcaddr = a1
 				btcaddr = a1
 			elif is_mmgen_addr(a1):
 			elif is_mmgen_addr(a1):
-				btcaddr = mm_addr2btc_addr(c,a1,acct_data,addr_data,b2m_map)
+				btcaddr = mmaddr2btcaddr(c,a1,acct_data,addr_data,b2m_map)
 			else:
 			else:
 				msg("%s: unrecognized subargument in argument '%s'" % (a1,a))
 				msg("%s: unrecognized subargument in argument '%s'" % (a1,a))
 				sys.exit(2)
 				sys.exit(2)
 
 
 			if is_btc_amt(a2):
 			if is_btc_amt(a2):
-				tx_out[btcaddr] = check_btc_amt(a2)
+				tx_out[btcaddr] = normalize_btc_amt(a2)
 			else:
 			else:
 				msg("%s: invalid amount in argument '%s'" % (a2,a))
 				msg("%s: invalid amount in argument '%s'" % (a2,a))
 				sys.exit(2)
 				sys.exit(2)
@@ -108,7 +114,7 @@ if not 'info' in opts:
 						(change_addr, a))
 						(change_addr, a))
 				sys.exit(2)
 				sys.exit(2)
 			change_addr = a if is_btc_addr(a) else \
 			change_addr = a if is_btc_addr(a) else \
-				mm_addr2btc_addr(c,a,acct_data,addr_data,b2m_map)
+				mmaddr2btcaddr(c,a,acct_data,addr_data,b2m_map)
 			tx_out[change_addr] = 0
 			tx_out[change_addr] = 0
 		else:
 		else:
 			msg("%s: unrecognized argument" % a)
 			msg("%s: unrecognized argument" % a)
@@ -119,7 +125,7 @@ if not 'info' in opts:
 		sys.exit(2)
 		sys.exit(2)
 
 
 	tx_fee = opts['tx_fee'] if 'tx_fee' in opts else g.tx_fee
 	tx_fee = opts['tx_fee'] if 'tx_fee' in opts else g.tx_fee
-	tx_fee = check_btc_amt(tx_fee)
+	tx_fee = normalize_btc_amt(tx_fee)
 	if tx_fee > g.max_tx_fee:
 	if tx_fee > g.max_tx_fee:
 		msg("Transaction fee too large: %s > %s" % (tx_fee,g.max_tx_fee))
 		msg("Transaction fee too large: %s > %s" % (tx_fee,g.max_tx_fee))
 		sys.exit(2)
 		sys.exit(2)
@@ -128,21 +134,15 @@ if g.debug: show_opts_and_cmd_args(opts,cmd_args)
 
 
 if not 'info' in opts: do_license_msg(immed=True)
 if not 'info' in opts: do_license_msg(immed=True)
 
 
-# Begin test
-# import mmgen.rpc
-# us = eval(get_data_from_file("listunspent.json"))
-# End test
-
-us = c.listunspent()
+#write_to_file("bogus_unspent.json", repr(us)); sys.exit()
+if g.bogus_wallet_data:
+	import mmgen.rpc
+	us = eval(get_data_from_file("bogus_unspent.json"))
+else:
+	us = c.listunspent()
 
 
-if not us:
-	msg("""
-No spendable outputs found!  Import addresses with balances into your
-watch-only wallet using 'mmgen-addrimport' and then re-run this program.""")
-	sys.exit(2)
+if not us: msg(txmsg['no_spendable_outputs']); sys.exit(2)
 
 
-# write_to_file("listunspent.json",repr(us))
-# sys.exit()
 unspent = sort_and_view(us)
 unspent = sort_and_view(us)
 
 
 total = trim_exponent(sum([i.amount for i in unspent]))
 total = trim_exponent(sum([i.amount for i in unspent]))
@@ -209,6 +209,6 @@ if reply and reply in "YyVv":
 
 
 prompt = "Save transaction?"
 prompt = "Save transaction?"
 if user_confirm(prompt,default_yes=True):
 if user_confirm(prompt,default_yes=True):
-	print_tx_to_file(tx_hex,sel_unspent,send_amt or change,b2m_map,opts)
+	write_tx_to_file(tx_hex,sel_unspent,send_amt or change,b2m_map,opts)
 else:
 else:
 	msg("Transaction not saved")
 	msg("Transaction not saved")

+ 17 - 28
mmgen-txsend

@@ -35,46 +35,37 @@ help_data = {
 	'usage':   "[opts] <signed transaction file>",
 	'usage':   "[opts] <signed transaction file>",
 	'options': """
 	'options': """
 -h, --help          Print this help message
 -h, --help          Print this help message
--d, --outdir     d  Specify an alternate directory 'd' for output
+-d, --outdir=    d  Specify an alternate directory 'd' for output
 -q, --quiet         Suppress warnings; overwrite files without prompting
 -q, --quiet         Suppress warnings; overwrite files without prompting
 """
 """
 }
 }
 
 
-short_opts = "hd:q"
-long_opts  = "help","outdir=","quiet"
+opts,cmd_args = parse_opts(sys.argv,help_data)
 
 
-opts,cmd_args = process_opts(sys.argv,help_data,short_opts,long_opts)
 if 'quiet' in opts: g.quiet = True
 if 'quiet' in opts: g.quiet = True
-check_opts(opts,long_opts)
 
 
 if len(cmd_args) == 1:
 if len(cmd_args) == 1:
-	infile = cmd_args[0]
-	check_infile(infile)
+	infile = cmd_args[0]; check_infile(infile)
 else: usage(help_data)
 else: usage(help_data)
 
 
 # Begin execution
 # Begin execution
 
 
-try:
-	metadata,tx_sig = get_lines_from_file(infile,"signed transaction")
-except:
-	msg("Invalid signed transaction file")
-	sys.exit(3)
-
-metadata = metadata.split()
-if len(metadata) != 3:
-	msg("Invalid file header")
-	sys.exit(3)
+do_license_msg()
 
 
-from binascii import unhexlify
-try: unhexlify(tx_sig)
-except:
-	msg("Invalid signed transaction data")
-	sys.exit(3)
+tx_data = get_lines_from_file(infile,"signed transaction data")
 
 
-do_license_msg()
+metadata,tx_hex,inputs_data,b2m_map = parse_tx_data(tx_data,infile)
 
 
 qmsg("Signed transaction file '%s' is valid" % infile)
 qmsg("Signed transaction file '%s' is valid" % infile)
 
 
+c = connect_to_bitcoind()
+
+prompt = "View transaction data? (y)es, (N)o, (v)iew in pager"
+reply = prompt_and_get_char(prompt,"YyNnVv",enter_ok=True)
+if reply and reply in "YyVv":
+	p = True if reply in "Vv" else False
+	view_tx_data(c,inputs_data,tx_hex,b2m_map,metadata,pager=p)
+
 warn   = "Once this transaction is sent, there's no taking it back!"
 warn   = "Once this transaction is sent, there's no taking it back!"
 what   = "broadcast this transaction to the network"
 what   = "broadcast this transaction to the network"
 expect =  "YES, I REALLY WANT TO DO THIS"
 expect =  "YES, I REALLY WANT TO DO THIS"
@@ -85,14 +76,12 @@ confirm_or_exit(warn, what, expect)
 
 
 msg("Sending transaction")
 msg("Sending transaction")
 
 
-c = connect_to_bitcoind()
-
 try:
 try:
-	tx = c.sendrawtransaction(tx_sig)
+	tx_id = c.sendrawtransaction(tx_hex)
 except:
 except:
 	msg("Unable to send transaction")
 	msg("Unable to send transaction")
 	sys.exit(3)
 	sys.exit(3)
 
 
-msg("Transaction sent: %s" % tx)
+msg("Transaction sent: %s" % tx_id)
 
 
-print_sent_tx_to_file(tx,metadata,opts)
+write_sent_tx_num_to_file(tx_id,metadata,opts)

+ 20 - 27
mmgen-txsign

@@ -33,26 +33,27 @@ help_data = {
 	'usage':   "[opts] <transaction file>,.. [mmgen wallet/seed/words/brainwallet file]...",
 	'usage':   "[opts] <transaction file>,.. [mmgen wallet/seed/words/brainwallet file]...",
 	'options': """
 	'options': """
 -h, --help               Print this help message
 -h, --help               Print this help message
--d, --outdir          d  Specify an alternate directory 'd' for output
+-d, --outdir=         d  Specify an alternate directory 'd' for output
 -e, --echo-passphrase    Print passphrase to screen when typing it
 -e, --echo-passphrase    Print passphrase to screen when typing it
 -i, --info               Display information about the transaction and exit
 -i, --info               Display information about the transaction and exit
 -I, --tx-id              Display transaction ID and exit
 -I, --tx-id              Display transaction ID and exit
--k, --keys-from-file  k  Provide additional key data from file 'k'
--P, --passwd-file     f  Get passphrase from file 'f'
+-k, --keys-from-file= k  Provide additional key data from file 'k'
+-P, --passwd-file=    f  Get passphrase from file 'f'
 -q, --quiet              Suppress warnings; overwrite files without
 -q, --quiet              Suppress warnings; overwrite files without
                          prompting
                          prompting
 -V, --skip-key-preverify Skip optional key pre-verification step
 -V, --skip-key-preverify Skip optional key pre-verification step
-
--b, --from-brain     l,p Generate keys from a user-created password,
+-b, --from-brain=    l,p Generate keys from a user-created password,
                          i.e. a "brainwallet", using seed length 'l' and
                          i.e. a "brainwallet", using seed length 'l' and
                          hash preset 'p'
                          hash preset 'p'
 -w, --use-wallet-dat     Get keys from a running bitcoind
 -w, --use-wallet-dat     Get keys from a running bitcoind
 -g, --from-incog         Generate keys from an incognito wallet
 -g, --from-incog         Generate keys from an incognito wallet
 -X, --from-incog-hex     Generate keys from an incognito hexadecimal wallet
 -X, --from-incog-hex     Generate keys from an incognito hexadecimal wallet
--G, --from-incog-hidden  f,o,l  Generate keys from incognito data in file
+-G, --from-incog-hidden= f,o,l  Generate keys from incognito data in file
                                  'f' at offset 'o', with seed length of 'l'
                                  'f' at offset 'o', with seed length of 'l'
 -m, --from-mnemonic      Generate keys from an electrum-like mnemonic
 -m, --from-mnemonic      Generate keys from an electrum-like mnemonic
--s, --from-seed          Generate keys from a seed in .{} format
+-s, --from-seed          Generate keys from a seed in .{g.seed_ext} format
+""".format(g=g),
+	'notes': """
 
 
 Transactions with either mmgen or non-mmgen input addresses may be signed.
 Transactions with either mmgen or non-mmgen input addresses may be signed.
 For non-mmgen inputs, the bitcoind wallet.dat is used as the key source.
 For non-mmgen inputs, the bitcoind wallet.dat is used as the key source.
@@ -74,34 +75,25 @@ mappings are verified.  Therefore, seed material for these addresses must
 be supplied on the command line.
 be supplied on the command line.
 
 
 Seed data supplied in files must have the following extensions:
 Seed data supplied in files must have the following extensions:
-   wallet:      '.{}'
-   seed:        '.{}'
-   mnemonic:    '.{}'
-   brainwallet: '.{}'
-""".format(g.seed_ext,g.wallet_ext,g.seed_ext,g.mn_ext,g.brain_ext)
+   wallet:      '.{g.wallet_ext}'
+   seed:        '.{g.seed_ext}'
+   mnemonic:    '.{g.mn_ext}'
+   brainwallet: '.{g.brain_ext}'
+""".format(g=g)
 }
 }
 
 
-short_opts = "hd:eiIk:P:qVb:wgXG:ms"
-long_opts  = "help","outdir=","echo_passphrase","info","tx_id",\
-			"keys_from_file=","passwd_file=","quiet","skip_key_preverify",\
-			"from_brain=","use_wallet_dat",\
-			"from_incog","from_incog_hex","from_incog_hidden=",\
-			"from_mnemonic","from_seed"
-
-opts,infiles = process_opts(sys.argv,help_data,short_opts,long_opts)
+opts,infiles = parse_opts(sys.argv,help_data)
 
 
 if "quiet" in opts: g.quiet = True
 if "quiet" in opts: g.quiet = True
 if 'from_incog_hex' in opts or 'from_incog_hidden' in opts:
 if 'from_incog_hex' in opts or 'from_incog_hidden' in opts:
 	opts['from_incog'] = True
 	opts['from_incog'] = True
 
 
-check_opts(opts,long_opts)
-
 if not infiles: usage(help_data)
 if not infiles: usage(help_data)
 for i in infiles: check_infile(i)
 for i in infiles: check_infile(i)
 
 
 c = connect_to_bitcoind()
 c = connect_to_bitcoind()
 
 
-seeds = {}
+saved_seeds = {}
 tx_files = [i for i in set(infiles) if get_extension(i) == g.rawtx_ext]
 tx_files = [i for i in set(infiles) if get_extension(i) == g.rawtx_ext]
 infiles  = list(set(infiles) - set(tx_files))
 infiles  = list(set(infiles) - set(tx_files))
 
 
@@ -139,9 +131,10 @@ for tx_file in tx_files:
 		preverify_keys(a, keys)
 		preverify_keys(a, keys)
 		opts['skip_key_preverify'] = True
 		opts['skip_key_preverify'] = True
 
 
-	check_mmgen_to_btc_addr_mappings(inputs_data,b2m_map,infiles,seeds,opts)
+	check_mmgen_to_btc_addr_mappings(inputs_data,b2m_map,infiles,saved_seeds,opts)
 
 
-	if len(tx_files) > 1: msg("\nTransaction #%s:" % (tx_files.index(tx_file)+1))
+	if len(tx_files) > 1:
+		msg("\nTransaction %s/%s:" % (tx_files.index(tx_file)+1,len(tx_files)))
 	qmsg("Successfully opened transaction file '%s'" % tx_file)
 	qmsg("Successfully opened transaction file '%s'" % tx_file)
 
 
 	prompt = "View transaction data? (y)es, (N)o, (v)iew in pager"
 	prompt = "View transaction data? (y)es, (N)o, (v)iew in pager"
@@ -156,7 +149,7 @@ for tx_file in tx_files:
 
 
 	if mmgen_addrs:
 	if mmgen_addrs:
 		ml = [i['account'].split()[0] for i in mmgen_addrs]
 		ml = [i['account'].split()[0] for i in mmgen_addrs]
-		keys += get_keys_for_mmgen_addrs(ml,infiles,seeds,opts)
+		keys += get_keys_for_mmgen_addrs(ml,infiles,saved_seeds,opts)
 
 
 		if 'use_wallet_dat' in opts:
 		if 'use_wallet_dat' in opts:
 			sig_tx = sign_tx_with_bitcoind_wallet(c,tx_hex,sig_data,keys,opts)
 			sig_tx = sign_tx_with_bitcoind_wallet(c,tx_hex,sig_data,keys,opts)
@@ -171,7 +164,7 @@ for tx_file in tx_files:
 	if sig_tx['complete']:
 	if sig_tx['complete']:
 		prompt = "OK\nSave signed transaction?"
 		prompt = "OK\nSave signed transaction?"
 		if user_confirm(prompt,default_yes=True):
 		if user_confirm(prompt,default_yes=True):
-			print_signed_tx_to_file(tx_hex,sig_tx['hex'],metadata,opts)
+			write_signed_tx_to_file(tx_hex,sig_tx['hex'],metadata,inputs_data,b2m_map,opts)
 	else:
 	else:
 		msg("failed\nSome keys were missing.  Transaction could not be signed.")
 		msg("failed\nSome keys were missing.  Transaction could not be signed.")
 		sys.exit(3)
 		sys.exit(3)

+ 7 - 15
mmgen-walletchk

@@ -27,42 +27,34 @@ from mmgen.util import *
 
 
 help_data = {
 help_data = {
 	'prog_name': sys.argv[0].split("/")[-1],
 	'prog_name': sys.argv[0].split("/")[-1],
-	'desc':  """Check integrity of a %s deterministic wallet, display
-                    its information and export seed and mnemonic data."""\
-					% g.proj_name,
+	'desc':  """Check integrity of an {} deterministic wallet, display
+                    its information, and export seed and mnemonic data.
+             """.format(g.proj_name),
 	'usage':   "[opts] [filename]",
 	'usage':   "[opts] [filename]",
 	'options': """
 	'options': """
 -h, --help             Print this help message
 -h, --help             Print this help message
--d, --outdir        d  Specify an alternate directory 'd' for output
+-d, --outdir=       d  Specify an alternate directory 'd' for output
 -e, --echo-passphrase  Print passphrase to screen when typing it
 -e, --echo-passphrase  Print passphrase to screen when typing it
--P, --passwd-file   f  Get passphrase from file 'f'
+-P, --passwd-file=  f  Get passphrase from file 'f'
 -q, --quiet            Suppress warnings; overwrite files without prompting
 -q, --quiet            Suppress warnings; overwrite files without prompting
 -S, --stdout           Print seed or mnemonic data to standard output
 -S, --stdout           Print seed or mnemonic data to standard output
 -v, --verbose          Produce more verbose output
 -v, --verbose          Produce more verbose output
 -g, --export-incog     Export wallet to incognito format
 -g, --export-incog     Export wallet to incognito format
 -X, --export-incog-hex Export wallet to incognito hexadecimal format
 -X, --export-incog-hex Export wallet to incognito hexadecimal format
--G, --export-incog-hidden  f,o  Hide incognito data in existing file 'f'
+-G, --export-incog-hidden= f,o  Hide incognito data in existing file 'f'
                            at offset 'o' (comma-separated)
                            at offset 'o' (comma-separated)
 -m, --export-mnemonic  Export the wallet's mnemonic to file
 -m, --export-mnemonic  Export the wallet's mnemonic to file
 -s, --export-seed      Export the wallet's seed to file
 -s, --export-seed      Export the wallet's seed to file
 """
 """
 }
 }
 
 
-short_opts = "hd:eP:qSvgXG:ms"
-long_opts  = "help","outdir=","echo_passphrase","passwd_file=","quiet",\
-			"stdout","verbose",\
-			"export_incog","export_incog_hex","export_incog_hidden=",\
-			"export_mnemonic","export_seed"
+opts,cmd_args = parse_opts(sys.argv,help_data)
 
 
-opts,cmd_args = process_opts(sys.argv,help_data,short_opts,long_opts)
 if 'quiet' in opts: g.quiet = True
 if 'quiet' in opts: g.quiet = True
 if 'verbose' in opts: g.verbose = True
 if 'verbose' in opts: g.verbose = True
 if 'export_incog_hidden' in opts or 'export_incog_hex' in opts:
 if 'export_incog_hidden' in opts or 'export_incog_hex' in opts:
 	opts['export_incog'] = True
 	opts['export_incog'] = True
 
 
-# Argument sanity checks and processing:
-check_opts(opts,long_opts)
-
 if len(cmd_args) != 1: usage(help_data)
 if len(cmd_args) != 1: usage(help_data)
 
 
 check_infile(cmd_args[0])
 check_infile(cmd_args[0])

+ 17 - 28
mmgen-walletgen

@@ -31,35 +31,37 @@ prog_name = sys.argv[0].split("/")[-1]
 
 
 help_data = {
 help_data = {
 	'prog_name': prog_name,
 	'prog_name': prog_name,
-	'desc':    "Generate a {} deterministic wallet".format(g.proj_name),
+	'desc':    "Generate an {} deterministic wallet".format(g.proj_name),
 	'usage':   "[opts] [infile]",
 	'usage':   "[opts] [infile]",
 	'options': """
 	'options': """
 -h, --help                 Print this help message
 -h, --help                 Print this help message
--d, --outdir            d  Specify an alternate directory 'd' for output
+-d, --outdir=           d  Specify an alternate directory 'd' for output
 -e, --echo-passphrase      Print passphrase to screen when typing it
 -e, --echo-passphrase      Print passphrase to screen when typing it
 -H, --show-hash-presets    Show information on available hash presets
 -H, --show-hash-presets    Show information on available hash presets
--l, --seed-len          n  Create seed of length 'n'. Options: {}
-                           (default: {})
--L, --label             l  Label to identify this wallet (32 chars max.
+-l, --seed-len=         n  Create seed of length 'n'. Options: {seed_lens}
+                           (default: {g.seed_len})
+-L, --label=            l  Label to identify this wallet (32 chars max.
                            Allowed symbols: A-Z, a-z, 0-9, " ", "_", ".")
                            Allowed symbols: A-Z, a-z, 0-9, " ", "_", ".")
--p, --hash-preset       p  Use scrypt.hash() parameters from preset 'p'
-                           (default: '{}')
--P, --passwd-file       f  Get passphrase from file 'f'
+-p, --hash-preset=      p  Use scrypt.hash() parameters from preset 'p'
+                           (default: '{g.hash_preset}')
+-P, --passwd-file=      f  Get passphrase from file 'f'
 -q, --quiet                Produce quieter output; overwrite files without
 -q, --quiet                Produce quieter output; overwrite files without
                            prompting
                            prompting
--u, --usr-randlen       n  Get 'n' characters of randomness from the user
-                           (default: {})
+-u, --usr-randlen=      n  Get 'n' characters of randomness from the user
+                           (default: {g.usr_randlen})
 -v, --verbose              Produce more verbose output
 -v, --verbose              Produce more verbose output
 
 
--b, --from-brain       l,p Generate wallet from a user-created passphrase,
+-b, --from-brain=      l,p Generate wallet from a user-created passphrase,
                            i.e. a "brainwallet", using seed length 'l' and
                            i.e. a "brainwallet", using seed length 'l' and
                            hash preset 'p' (comma-separated)
                            hash preset 'p' (comma-separated)
 -g, --from-incog           Generate wallet from an incognito-format wallet
 -g, --from-incog           Generate wallet from an incognito-format wallet
 -m, --from-mnemonic        Generate wallet from an Electrum-like mnemonic
 -m, --from-mnemonic        Generate wallet from an Electrum-like mnemonic
--s, --from-seed            Generate wallet from a seed in .{S} format
+-s, --from-seed            Generate wallet from a seed in .{g.seed_ext} format
+""".format(seed_lens=",".join([str(i) for i in g.seed_lens]), g=g),
+	'notes': """
 
 
 By default (i.e. when invoked without any of the '--from-<what>' options),
 By default (i.e. when invoked without any of the '--from-<what>' options),
-{} generates a wallet based on a random seed.
+{prog_name} generates a wallet based on a random seed.
 
 
 Data for the --from-<what> options will be taken from <infile> if <infile>
 Data for the --from-<what> options will be taken from <infile> if <infile>
 is specified.  Otherwise, the user will be prompted to enter the data.
 is specified.  Otherwise, the user will be prompted to enter the data.
@@ -84,28 +86,15 @@ the '--seed-len' option.
 For a brainwallet passphrase to always generate the same keys and
 For a brainwallet passphrase to always generate the same keys and
 addresses, the same 'l' and 'p' parameters to '--from-brain' must be used
 addresses, the same 'l' and 'p' parameters to '--from-brain' must be used
 in all future invocations with that passphrase.
 in all future invocations with that passphrase.
-""".format(
-		",".join([str(i) for i in g.seed_lens]),
-		g.seed_len,
-		g.hash_preset,
-		g.usr_randlen,
-		prog_name,
-		S=g.seed_ext,
-	)
+""".format(prog_name=prog_name)
 }
 }
 
 
-short_opts = "hd:eHl:L:p:P:qu:vb:gms"
-long_opts  = "help","outdir=","echo_passphrase","show_hash_presets","seed_len=",\
-			"label=","hash_preset=","passwd_file=","quiet","usr_randlen=","verbose",\
-			"from_brain=","from_incog","from_mnemonic","from_seed"
+opts,cmd_args = parse_opts(sys.argv,help_data)
 
 
-opts,cmd_args = process_opts(sys.argv,help_data,short_opts,long_opts)
 if 'quiet' in opts: g.quiet = True
 if 'quiet' in opts: g.quiet = True
 if 'verbose' in opts: g.verbose = True
 if 'verbose' in opts: g.verbose = True
 if 'show_hash_presets' in opts: show_hash_presets()
 if 'show_hash_presets' in opts: show_hash_presets()
 
 
-check_opts(opts,long_opts)
-
 if g.debug: show_opts_and_cmd_args(opts,cmd_args)
 if g.debug: show_opts_and_cmd_args(opts,cmd_args)
 
 
 if len(cmd_args) == 1:
 if len(cmd_args) == 1:

+ 72 - 50
mmgen/Opts.py

@@ -23,19 +23,29 @@ def usage(hd):
 	print "USAGE: %s %s" % (hd['prog_name'], hd['usage'])
 	print "USAGE: %s %s" % (hd['prog_name'], hd['usage'])
 	sys.exit(2)
 	sys.exit(2)
 
 
+def print_version_info(progname):
+	print """
+'{}' version {g.version}.  Part of the {g.proj_name} suite.
+Copyright (C) {g.Cdates} by {g.author} {g.email}.
+""".format(progname, g=g).strip()
 
 
 def print_help(progname,help_data):
 def print_help(progname,help_data):
 	pn_len = str(len(progname)+2)
 	pn_len = str(len(progname)+2)
-	print ("  %-"+pn_len+"s %s")    % (progname.upper()+":", help_data['desc'])
-	print ("  %-"+pn_len+"s %s %s") % ("USAGE:", progname, help_data['usage'])
+	print ("  %-"+pn_len+"s %s") % (progname.upper()+":", help_data['desc'].strip())
+	print ("  %-"+pn_len+"s %s %s")%("USAGE:", progname, help_data['usage'].strip())
 	sep = "\n    "
 	sep = "\n    "
 	print "  OPTIONS:"+sep+"%s" % sep.join(help_data['options'].strip().split("\n"))
 	print "  OPTIONS:"+sep+"%s" % sep.join(help_data['options'].strip().split("\n"))
+	if "notes" in help_data:
+		print "  %s" % "\n  ".join(help_data['notes'][1:-1].split("\n"))
 
 
 
 
 def process_opts(argv,help_data,short_opts,long_opts):
 def process_opts(argv,help_data,short_opts,long_opts):
 
 
 	progname = argv[0].split("/")[-1]
 	progname = argv[0].split("/")[-1]
 
 
+	if len(argv) == 2 and argv[1] == '--version': # MMGen only!
+		print_version_info(progname); sys.exit()
+
 	if g.debug:
 	if g.debug:
 		print "Short opts: %s" % repr(short_opts)
 		print "Short opts: %s" % repr(short_opts)
 		print "Long opts:  %s" % repr(long_opts)
 		print "Long opts:  %s" % repr(long_opts)
@@ -69,6 +79,26 @@ def process_opts(argv,help_data,short_opts,long_opts):
 	return opts,args
 	return opts,args
 
 
 
 
+def parse_opts(argv,help_data):
+
+	lines = help_data['options'].strip().split("\n")
+	import re
+	pat = r"^-([a-zA-Z0-9]), --([a-zA-Z0-9-]{1,64})(=*) (.*)"
+	opt_data = [m.groups() for m in [re.match(pat,l) for l in lines] if m]
+
+	short_opts = "".join([d[0]+(":" if d[2] else "") for d in opt_data if d])
+	long_opts = [d[1].replace("-","_")+d[2] for d in opt_data if d]
+	help_data['options'] = "\n".join(
+		["-{0}, --{1}{w} {3}".format(w=" " if m.group(3) else "", *m.groups())
+			if m else k for m,k in [(re.match(pat,l),l) for l in lines]]
+	)
+	opts,infiles = process_opts(argv,help_data,short_opts,long_opts)
+
+	if not check_opts(opts,long_opts): sys.exit(1) # MMGen only!
+
+	return opts,infiles
+
+
 def show_opts_and_cmd_args(opts,cmd_args):
 def show_opts_and_cmd_args(opts,cmd_args):
 	print "Processed options:     %s" % repr(opts)
 	print "Processed options:     %s" % repr(opts)
 	print "Cmd args:              %s" % repr(cmd_args)
 	print "Cmd args:              %s" % repr(cmd_args)
@@ -85,51 +115,41 @@ def check_opts(opts,long_opts):
 		if i+"=" in long_opts:
 		if i+"=" in long_opts:
 			set_if_unset_and_typeconvert(opts,i)
 			set_if_unset_and_typeconvert(opts,i)
 
 
-	for opt in opts.keys():
+	for opt,val in opts.items():
 
 
-		val = opts[opt]
 		what = "parameter for '--%s' option" % opt.replace("_","-")
 		what = "parameter for '--%s' option" % opt.replace("_","-")
 
 
 		# Check for file existence and readability
 		# Check for file existence and readability
-		for i in 'keys_from_file','addrlist','passwd_file','keysforaddrs':
-			if opt == i:
-				check_infile(val)
-				return
+		if opt in ('keys_from_file','addrlist','passwd_file','keysforaddrs'):
+			check_infile(val)
+			return True
 
 
 		if opt == 'outdir':
 		if opt == 'outdir':
 			what = "output directory"
 			what = "output directory"
-			import re, os, stat
-			# TODO Non-portable:
-			d = re.sub(r'/*$','', val)
-			opts[opt] = d
-
-			try: mode = os.stat(d).st_mode
-			except:
-				msg("Unable to stat requested %s '%s'" % (what,d))
-				sys.exit(1)
-
-			if not stat.S_ISDIR(mode):
-				msg("Requested %s '%s' is not a directory" % (what,d))
-				sys.exit(1)
+			import os
+			if os.path.isdir(val):
+				if os.access(val, os.W_OK|os.X_OK):
+					opts[opt] = os.path.normpath(val)
+				else:
+					msg("Requested %s '%s' is unwritable by you" % (what,val))
+					return False
+			else:
+				msg("Requested %s '%s' doen not exist" % (what,val))
+				return False
 
 
-			if not os.access(d, os.W_OK|os.X_OK):
-				msg("Requested %s '%s' is unwritable by you" % (what,d))
-				sys.exit(1)
 		elif opt == 'label':
 		elif opt == 'label':
-			label = val.strip()
-			opts[opt] = label
 
 
-			if len(label) > g.max_wallet_label_len:
+			if len(val) > g.max_wallet_label_len:
 				msg("Label must be %s characters or less" %
 				msg("Label must be %s characters or less" %
 					g.max_wallet_label_len)
 					g.max_wallet_label_len)
-				sys.exit(1)
-
-			for ch in list(label):
-				if ch not in g.wallet_label_symbols:
-					msg("""
-"%s": illegal character in label.  Only ASCII characters are permitted.
-""".strip() % ch)
-					sys.exit(1)
+				return False
+
+			for ch in list(val):
+				chs = g.wallet_label_symbols
+				if ch not in chs:
+					msg("'%s': ERROR: label contains an illegal symbol" % val)
+					msg("The following symbols are permitted:\n%s" % "".join(chs))
+					return False
 		elif opt == 'export_incog_hidden' or opt == 'from_incog_hidden':
 		elif opt == 'export_incog_hidden' or opt == 'from_incog_hidden':
 			try:
 			try:
 				if opt == 'export_incog_hidden':
 				if opt == 'export_incog_hidden':
@@ -138,87 +158,89 @@ def check_opts(opts,long_opts):
 					outfile,offset,seed_len = val.split(",")
 					outfile,offset,seed_len = val.split(",")
 			except:
 			except:
 				msg("'%s': invalid %s" % (val,what))
 				msg("'%s': invalid %s" % (val,what))
-				sys.exit(1)
+				return False
 
 
 			try:
 			try:
 				o = int(offset)
 				o = int(offset)
 			except:
 			except:
 				msg("'%s': invalid 'o' %s (not an integer)" % (offset,what))
 				msg("'%s': invalid 'o' %s (not an integer)" % (offset,what))
-				sys.exit(1)
+				return False
 
 
 			if o < 0:
 			if o < 0:
 				msg("'%s': invalid 'o' %s (less than zero)" % (offset,what))
 				msg("'%s': invalid 'o' %s (less than zero)" % (offset,what))
-				sys.exit(1)
+				return False
 
 
 			if opt == 'from_incog_hidden':
 			if opt == 'from_incog_hidden':
 				try:
 				try:
 					sl = int(seed_len)
 					sl = int(seed_len)
 				except:
 				except:
 					msg("'%s': invalid 'l' %s (not an integer)" % (sl,what))
 					msg("'%s': invalid 'l' %s (not an integer)" % (sl,what))
-					sys.exit(1)
+					return False
 
 
 				if sl not in g.seed_lens:
 				if sl not in g.seed_lens:
 					msg("'%s': invalid 'l' %s (valid choices: %s)" %
 					msg("'%s': invalid 'l' %s (valid choices: %s)" %
 						(sl,what," ".join(str(i) for i in g.seed_lens)))
 						(sl,what," ".join(str(i) for i in g.seed_lens)))
-					sys.exit(1)
+					return False
 
 
 			import os, stat
 			import os, stat
 			try: mode = os.stat(outfile).st_mode
 			try: mode = os.stat(outfile).st_mode
 			except:
 			except:
 				msg("Unable to stat requested %s '%s'" % (what,outfile))
 				msg("Unable to stat requested %s '%s'" % (what,outfile))
-				sys.exit(1)
+				return False
 
 
 			if not (stat.S_ISREG(mode) or stat.S_ISBLK(mode)):
 			if not (stat.S_ISREG(mode) or stat.S_ISBLK(mode)):
 				msg("Requested %s '%s' is not a file or block device" %
 				msg("Requested %s '%s' is not a file or block device" %
 						(what,outfile))
 						(what,outfile))
-				sys.exit(1)
+				return False
 
 
 			ac,m = (os.W_OK,"writ") \
 			ac,m = (os.W_OK,"writ") \
 				if "export_incog_hidden" in opts else (os.R_OK,"read")
 				if "export_incog_hidden" in opts else (os.R_OK,"read")
 			if not os.access(outfile, ac):
 			if not os.access(outfile, ac):
 				msg("Requested %s '%s' is un%sable by you" % (what,outfile,m))
 				msg("Requested %s '%s' is un%sable by you" % (what,outfile,m))
-				sys.exit(1)
+				return False
 
 
 		elif opt == 'from_brain':
 		elif opt == 'from_brain':
 			try:
 			try:
 				l,p = val.split(",")
 				l,p = val.split(",")
 			except:
 			except:
 				msg("'%s': invalid %s" % (val,what))
 				msg("'%s': invalid %s" % (val,what))
-				sys.exit(2)
+				return False
 
 
 			try:
 			try:
 				int(l)
 				int(l)
 			except:
 			except:
 				msg("'%s': invalid 'l' %s (not an integer)" % (l,what))
 				msg("'%s': invalid 'l' %s (not an integer)" % (l,what))
-				sys.exit(1)
+				return False
 
 
 			if int(l) not in g.seed_lens:
 			if int(l) not in g.seed_lens:
 				msg("'%s': invalid 'l' %s.  Options: %s" %
 				msg("'%s': invalid 'l' %s.  Options: %s" %
 						(l, what, ", ".join([str(i) for i in g.seed_lens])))
 						(l, what, ", ".join([str(i) for i in g.seed_lens])))
-				sys.exit(1)
+				return False
 
 
 			if p not in g.hash_presets:
 			if p not in g.hash_presets:
 				hps = ", ".join([i for i in sorted(g.hash_presets.keys())])
 				hps = ", ".join([i for i in sorted(g.hash_presets.keys())])
 				msg("'%s': invalid 'p' %s.  Options: %s" % (p, what, hps))
 				msg("'%s': invalid 'p' %s.  Options: %s" % (p, what, hps))
-				sys.exit(1)
+				return False
 		elif opt == 'seed_len':
 		elif opt == 'seed_len':
 			if val not in g.seed_lens:
 			if val not in g.seed_lens:
 				msg("'%s': invalid %s.  Options: %s"
 				msg("'%s': invalid %s.  Options: %s"
 				% (val,what,", ".join([str(i) for i in g.seed_lens])))
 				% (val,what,", ".join([str(i) for i in g.seed_lens])))
-				sys.exit(2)
+				return False
 		elif opt == 'hash_preset':
 		elif opt == 'hash_preset':
 			if val not in g.hash_presets:
 			if val not in g.hash_presets:
 				msg("'%s': invalid %s.  Options: %s"
 				msg("'%s': invalid %s.  Options: %s"
 				% (val,what,", ".join(sorted(g.hash_presets.keys()))))
 				% (val,what,", ".join(sorted(g.hash_presets.keys()))))
-				sys.exit(2)
+				return False
 		elif opt == 'usr_randlen':
 		elif opt == 'usr_randlen':
 			if val > g.max_randlen or val < g.min_randlen:
 			if val > g.max_randlen or val < g.min_randlen:
 				msg("'%s': invalid %s (must be >= %s and <= %s)"
 				msg("'%s': invalid %s (must be >= %s and <= %s)"
 				% (val,what,g.min_randlen,g.max_randlen))
 				% (val,what,g.min_randlen,g.max_randlen))
-				sys.exit(2)
+				return False
 		else:
 		else:
 			if g.debug: print "check_opts(): No test for opt '%s'" % opt
 			if g.debug: print "check_opts(): No test for opt '%s'" % opt
 
 
+	return True
+
 
 
 def set_if_unset_and_typeconvert(opts,opt):
 def set_if_unset_and_typeconvert(opts,opt):
 
 

+ 26 - 35
mmgen/addr.py

@@ -21,12 +21,25 @@ addr.py:  Address generation/display routines for mmgen suite
 
 
 import sys
 import sys
 from hashlib import sha256, sha512
 from hashlib import sha256, sha512
+from hashlib import new as hashlib_new
 from binascii import hexlify, unhexlify
 from binascii import hexlify, unhexlify
 
 
 from mmgen.bitcoin import numtowif
 from mmgen.bitcoin import numtowif
 from mmgen.util import msg,qmsg,qmsg_r
 from mmgen.util import msg,qmsg,qmsg_r
 import mmgen.config as g
 import mmgen.config as g
 
 
+addrmsgs = {
+	'addrfile_header': """
+# MMGen address file
+#
+# This file is editable.
+# Everything following a hash symbol '#' is a comment and ignored by {}.
+# A text label of {} characters or less may be added to the right of each
+# address, and it will be appended to the bitcoind wallet label upon import.
+# The label may contain any printable ASCII symbol.
+""".strip().format(g.proj_name,g.max_addr_label_len)
+}
+
 def test_for_keyconv():
 def test_for_keyconv():
 	"""
 	"""
 	Test for the presence of 'keyconv' utility on system
 	Test for the presence of 'keyconv' utility on system
@@ -112,22 +125,10 @@ def generate_keys(seed, addrnums):
 	return generate_addrs(seed, addrnums, o)
 	return generate_addrs(seed, addrnums, o)
 
 
 
 
-def format_addr_data(addrlist, seed_chksum, opts):
-	"""
-	print_addresses(addrs, opts)  => None
-
-	Print out the addresses and/or keys generated by generate_addresses()
-
-	By default, prints addresses only
-
-	Output can be customized with the following command line options:
-		print_secret
-		no_addresses
-		b16
-	"""
+def format_addr_data(addr_data, addr_data_chksum, seed_id, addr_list, opts):
 
 
-	start = addrlist[0]['num']
-	end   = addrlist[-1]['num']
+	start = addr_data[0]['num']
+	end   = addr_data[-1]['num']
 
 
 	wif_msg = ""
 	wif_msg = ""
 	if ('b16' in opts and 'print_secret' in opts) \
 	if ('b16' in opts and 'print_secret' in opts) \
@@ -139,21 +140,14 @@ def format_addr_data(addrlist, seed_chksum, opts):
 			(5 if 'print_secret' in opts else 1) + len(wif_msg)
 			(5 if 'print_secret' in opts else 1) + len(wif_msg)
 		)
 		)
 
 
-	header = """
-# MMGen address file
-#
-# This file is editable.
-# Everything following a hash symbol '#' is a comment and ignored by {}.
-# A text label of {} characters or less may be added to the right of each
-# address, and it will be appended to the bitcoind wallet label upon import.
-# The label may contain any printable ASCII symbol.
-""".strip().format(g.proj_name_cap,g.max_addr_label_len)
-
 	data = []
 	data = []
-	if not 'stdout' in opts: data.append(header + "\n")
-	data.append("%s {" % seed_chksum.upper())
+	if not 'stdout' in opts: data.append(addrmsgs['addrfile_header'] + "\n")
+	data.append("# Address data checksum for {}[{}]: {}".format(
+				seed_id, fmt_addr_list(addr_list), addr_data_chksum))
+	data.append("# Record this value to a secure location\n")
+	data.append("%s {" % seed_id.upper())
 
 
-	for el in addrlist:
+	for el in addr_data:
 		col1 = el['num']
 		col1 = el['num']
 		if 'no_addresses' in opts:
 		if 'no_addresses' in opts:
 			if 'b16' in opts:
 			if 'b16' in opts:
@@ -179,23 +173,23 @@ def format_addr_data(addrlist, seed_chksum, opts):
 
 
 def fmt_addr_list(addr_list):
 def fmt_addr_list(addr_list):
 
 
+	addr_list = list(set(sorted(addr_list)))
+
 	prev = addr_list[0]
 	prev = addr_list[0]
 	ret = prev,
 	ret = prev,
 
 
 	for i in addr_list[1:]:
 	for i in addr_list[1:]:
-
 		if i == prev + 1:
 		if i == prev + 1:
 			if i == addr_list[-1]: ret += "-", i
 			if i == addr_list[-1]: ret += "-", i
 		else:
 		else:
 			if prev != ret[-1]: ret += "-", prev
 			if prev != ret[-1]: ret += "-", prev
 			ret += ",", i
 			ret += ",", i
-
 		prev = i
 		prev = i
 
 
 	return "".join([str(i) for i in ret])
 	return "".join([str(i) for i in ret])
 
 
 
 
-def write_addr_data_to_file(seed, data, addr_list, opts):
+def write_addr_data_to_file(seed, addr_data_str, addr_list, opts):
 
 
 	if 'print_addresses_only' in opts: ext = g.addrfile_ext
 	if 'print_addresses_only' in opts: ext = g.addrfile_ext
 	elif 'no_addresses' in opts:       ext = g.keyfile_ext
 	elif 'no_addresses' in opts:       ext = g.keyfile_ext
@@ -211,10 +205,7 @@ def write_addr_data_to_file(seed, data, addr_list, opts):
 	if 'outdir' in opts:
 	if 'outdir' in opts:
 		outfile = "%s/%s" % (opts['outdir'], outfile)
 		outfile = "%s/%s" % (opts['outdir'], outfile)
 
 
-	write_to_file(outfile,data)
+	write_to_file(outfile,addr_data_str)
 
 
 	dtype = "Address" if 'print_addresses_only' in opts else "Key"
 	dtype = "Address" if 'print_addresses_only' in opts else "Key"
 	msg("%s data saved to file '%s'" % (dtype,outfile))
 	msg("%s data saved to file '%s'" % (dtype,outfile))
-
-if __name__ == "__main__":
-	print fmt_addr_list(sorted(set([1,3,5,2,8,9,10,12,13,14,16])))

+ 10 - 4
mmgen/config.py

@@ -18,15 +18,20 @@
 """
 """
 config.py:  Constants and configuration options for the mmgen suite
 config.py:  Constants and configuration options for the mmgen suite
 """
 """
+
+author = "Philemon"
+email = "<mmgen-py@yandex.com>"
+Cdates = '2013-2014'
+version = '0.7.4'
+
 quiet,verbose = False,False
 quiet,verbose = False,False
 min_screen_width = 80
 min_screen_width = 80
 
 
 from decimal import Decimal
 from decimal import Decimal
-tx_fee        = Decimal("0.001")
-max_tx_fee    = Decimal("0.1")
+tx_fee        = Decimal("0.0001")
+max_tx_fee    = Decimal("0.01")
 
 
-proj_name     = "mmgen"
-proj_name_cap = "MMGen"
+proj_name     = "MMGen"
 
 
 wallet_ext    = "mmdat"
 wallet_ext    = "mmdat"
 seed_ext      = "mmseed"
 seed_ext      = "mmseed"
@@ -59,6 +64,7 @@ keyconv_exec = "keyconv"
 from os import getenv
 from os import getenv
 debug      = True if getenv("MMGEN_DEBUG") else False
 debug      = True if getenv("MMGEN_DEBUG") else False
 no_license = True if getenv("MMGEN_NOLICENSE") else False
 no_license = True if getenv("MMGEN_NOLICENSE") else False
+bogus_wallet_data = True if getenv("MMGEN_BOGUS_WALLET_DATA") else False
 
 
 mins_per_block = 8.5
 mins_per_block = 8.5
 passwd_max_tries = 5
 passwd_max_tries = 5

+ 3 - 3
mmgen/license.py

@@ -21,13 +21,14 @@ license.py:  Show the license
 
 
 import sys
 import sys
 from mmgen.util import msg, msg_r, get_char
 from mmgen.util import msg, msg_r, get_char
+import mmgen.config as g
 
 
 gpl = {
 gpl = {
 	'warning': """
 	'warning': """
-  MMGen Copyright (C) 2013-2014 by Philemon <mmgen-py@yandex.com>.  This
+  MMGen Copyright (C) {g.Cdates} by {g.author} {g.email}.  This
   program comes with ABSOLUTELY NO WARRANTY.  This is free software, and
   program comes with ABSOLUTELY NO WARRANTY.  This is free software, and
   you are welcome to redistribute it under certain conditions.
   you are welcome to redistribute it under certain conditions.
-""",
+""".format(g=g),
 	'prompt': """
 	'prompt': """
 Press 'w' for conditions and warranty info, or 'c' to continue:
 Press 'w' for conditions and warranty info, or 'c' to continue:
 """,
 """,
@@ -586,7 +587,6 @@ copy of the Program in return for a fee.
 
 
 def do_license_msg(immed=False):
 def do_license_msg(immed=False):
 
 
-	import mmgen.config as g
 	if g.quiet or g.no_license: return
 	if g.quiet or g.no_license: return
 
 
 	msg(gpl['warning'])
 	msg(gpl['warning'])

+ 109 - 90
mmgen/tool.py

@@ -23,6 +23,7 @@ import sys
 import mmgen.bitcoin as bitcoin
 import mmgen.bitcoin as bitcoin
 
 
 from mmgen.util import *
 from mmgen.util import *
+from mmgen.tx import *
 
 
 commands = {
 commands = {
 #	"keyconv_compare":          ['wif [str]'],
 #	"keyconv_compare":          ['wif [str]'],
@@ -54,44 +55,47 @@ commands = {
 	"mn_printlist": ['wordlist [str="electrum"]'],
 	"mn_printlist": ['wordlist [str="electrum"]'],
 	"id8":          ['<infile> [str]'],
 	"id8":          ['<infile> [str]'],
 	"id6":          ['<infile> [str]'],
 	"id6":          ['<infile> [str]'],
-	"listaccounts": ['minconf [int=1]'],
+	"listaddresses": ['minconf [int=1]', 'showempty [bool=False]'],
 	"getbalance":   ['minconf [int=1]'],
 	"getbalance":   ['minconf [int=1]'],
+	"viewtx":       ['<MMGen tx file> [str]'],
+	"check_addrfile":  ['<MMGen addr file> [str]']
 }
 }
 
 
 command_help = """
 command_help = """
-File operations
-hexdump      - encode data into formatted hexadecimal form (file or stdin)
-unhexdump    - decode formatted hexadecimal data (file or stdin)
-
-MMGen-specific operations
-id8          - generate 8-character MMGen ID checksum for file (or stdin)
-id6          - generate 6-character MMGen ID checksum for file (or stdin)
-
-Bitcoin operations:
-strtob58     - convert a string to base 58
-hextob58     - convert a hexadecimal number to base 58
-b58tohex     - convert a base 58 number to hexadecimal
-b58randenc   - generate a random 32-byte number and convert it to base 58
-randwif      - generate a random private key in WIF format
-randpair     - generate a random private key/address pair
-wif2addr     - generate a Bitcoin address from a key in WIF format
-
-Mnemonic operations (choose "electrum" (default), "tirosh" or "all"
-wordlists):
-mn_rand128   - generate random 128-bit mnemonic
-mn_rand192   - generate random 192-bit mnemonic
-mn_rand256   - generate random 256-bit mnemonic
-mn_stats     - show stats for mnemonic wordlist
-mn_printlist - print mnemonic wordlist
-
-Bitcoind operations (bitcoind must be running):
-listaccounts - like 'bitcoind listaccounts' but shows MMGen wallet balances
-               too
-getbalance   - like 'bitcoind getbalance' but shows confirmed/unconfirmed,
-               spendable/unspendable
-
-IMPORTANT NOTE: Though MMGen mnemonics use the Electrum wordlist, they're
-computed using a different algorithm and are NOT Electrum-compatible!
+  File operations
+  hexdump      - encode data into formatted hexadecimal form (file or stdin)
+  unhexdump    - decode formatted hexadecimal data (file or stdin)
+
+  MMGen-specific operations
+  id8          - generate 8-character MMGen ID checksum for file (or stdin)
+  id6          - generate 6-character MMGen ID checksum for file (or stdin)
+
+  Bitcoin operations:
+  strtob58     - convert a string to base 58
+  hextob58     - convert a hexadecimal number to base 58
+  b58tohex     - convert a base 58 number to hexadecimal
+  b58randenc   - generate a random 32-byte number and convert it to base 58
+  randwif      - generate a random private key in WIF format
+  randpair     - generate a random private key/address pair
+  wif2addr     - generate a Bitcoin address from a key in WIF format
+
+  Mnemonic operations (choose "electrum" (default), "tirosh" or "all"
+  wordlists):
+  mn_rand128   - generate random 128-bit mnemonic
+  mn_rand192   - generate random 192-bit mnemonic
+  mn_rand256   - generate random 256-bit mnemonic
+  mn_stats     - show stats for mnemonic wordlist
+  mn_printlist - print mnemonic wordlist
+
+  Bitcoind operations (bitcoind must be running):
+  listaddresses - show MMGen addresses and their balances
+  getbalance    - like 'bitcoind getbalance' but shows confirmed/unconfirmed,
+                  spendable/unspendable
+  viewtx        - show raw transaction in human-readable form
+  check_addrfile - compute checksum and address list for MMGen address file
+
+  IMPORTANT NOTE: Though MMGen mnemonics use the Electrum wordlist, they're
+  computed using a different algorithm and are NOT Electrum-compatible!
 """
 """
 
 
 def tool_usage(prog_name, command):
 def tool_usage(prog_name, command):
@@ -170,14 +174,10 @@ def print_convert_results(indata,enc,dec,no_recode=False):
 			msg("WARNING! Recoded number doesn't match input stringwise!")
 			msg("WARNING! Recoded number doesn't match input stringwise!")
 
 
 def hexdump(infile, cols=8, line_nums=True):
 def hexdump(infile, cols=8, line_nums=True):
-	d = sys.stdin.read() if infile == "-" else get_data_from_file(infile)
-	o = pretty_hexdump(d, 2, cols, line_nums)
-	print o
+	print pretty_hexdump(get_data_from_file(infile,dash=True), 2, cols, line_nums)
 
 
 def unhexdump(infile):
 def unhexdump(infile):
-	d = sys.stdin.read() if infile == "-" else get_data_from_file(infile)
-	o = decode_pretty_hexdump(d)
-	sys.stdout.write(o)
+	sys.stdout.write(decode_pretty_hexdump(get_data_from_file(infile,dash=True)))
 
 
 def strtob58(s):
 def strtob58(s):
 	enc = bitcoin.b58encode(s)
 	enc = bitcoin.b58encode(s)
@@ -264,60 +264,79 @@ def mn_printlist(wordlist="electrum"):
 	l = get_wordlist(wordlist)
 	l = get_wordlist(wordlist)
 	print "%s" % l.strip()
 	print "%s" % l.strip()
 
 
-def id8(infile):
-	d = sys.stdin.read() if infile == "-" else get_data_from_file(infile)
-	print make_chksum_8(d)
+def id8(infile): print make_chksum_8(get_data_from_file(infile,dash=True))
+def id6(infile): print make_chksum_6(get_data_from_file(infile,dash=True))
 
 
-def id6(infile):
-	d = sys.stdin.read() if infile == "-" else get_data_from_file(infile)
-	print make_chksum_6(d)
-
-
-def listaccounts(minconf=1):
+# List MMGen addresses and their balances:
+def listaddresses(minconf=1,showempty=False):
 	from mmgen.tx import connect_to_bitcoind,trim_exponent,is_mmgen_addr
 	from mmgen.tx import connect_to_bitcoind,trim_exponent,is_mmgen_addr
-	def s_mmgen(i):
-		ma = i[0].split(" ")[0] if " " in i[0] else i[0]
-		if is_mmgen_addr(ma):
-			mmid,idx = ma.split(":")
-			return mmid + ":" + ("%04i" % int(idx))
-		else:
-			return "G"+i[0]
-
 	c = connect_to_bitcoind()
 	c = connect_to_bitcoind()
-	data = [(a,c.getbalance(a,minconf)) for a in c.listaccounts()]
-	data.sort(key=s_mmgen)
-	col_w = max([len(d[0]) for d in data])
-	fs = "%-"+str(col_w)+"s   %s"
-	print fs % ("ACCOUNT","BALANCE")
-	totals = {}
-	for d in data:
-		ma = d[0].split(" ")[0] if " " in d[0] else d[0]
-		if is_mmgen_addr(ma):
-			mmid = ma.split(":")[0]
-			if mmid not in totals: totals[mmid] = 0
-			totals[mmid] += d[1]
-		print fs % (
-			d[0] if d[0] else 'TOTAL:',
-			trim_exponent(d[1])
-		)
-	print "\nMMGEN WALLET BALANCES"
-	for k in totals.keys():
-		print "%s: %s" % (k, trim_exponent(totals[k]))
+
+	addrs = {}
+	for d in c.listunspent(0):
+		ma,comment = split2(d.account)
+		if is_mmgen_addr(ma) and d.confirmations >= minconf:
+			key = "_".join(ma.split(":"))
+			if key not in addrs: addrs[key] = [0,comment]
+			addrs[key][0] += d.amount
+
+	# "bitcoind getbalance <account>" can produce a false balance
+	# (sipa watchonly bitcoind), so use only for empty accounts
+	if showempty:
+		# Show accts with not enough confirmations as empty!
+		# A feature, not a bug!
+		for (ma,comment),bal in [(split2(a),c.getbalance(a,minconf=minconf))
+			for a in c.listaccounts(0)]:
+			if is_mmgen_addr(ma) and bal == 0:
+				key = "_".join(ma.split(":"))
+				if key not in addrs: addrs[key] = [0,comment]
+
+	fs = "%-{}s  %-{}s   %s".format(
+		max([len(k) for k in addrs.keys()]),
+		max([len(str(addrs[k][1])) for k in addrs.keys()])
+	)
+	print fs % ("ADDRESS","COMMENT","BALANCE")
+
+	def s_mmgen(ma):
+		return "{}:{:>0{w}}".format(w=g.mmgen_idx_max_digits, *ma.split("_"))
+
+	old_sid = ""
+	for k in sorted(addrs.keys(),key=s_mmgen):
+		sid,num = k.split("_")
+		if old_sid and old_sid != sid: print
+		old_sid = sid
+		print fs % (sid+":"+num, addrs[k][1], trim_exponent(addrs[k][0]))
+
 
 
 def getbalance(minconf=1):
 def getbalance(minconf=1):
 	from mmgen.tx import connect_to_bitcoind,trim_exponent,is_mmgen_addr
 	from mmgen.tx import connect_to_bitcoind,trim_exponent,is_mmgen_addr
+
+	accts = {}
+	for d in connect_to_bitcoind().listunspent(0):
+		ma = split2(d.account)[0]
+		keys = ["TOTAL"]
+		if d.spendable: keys += ["SPENDABLE"]
+		if is_mmgen_addr(ma): keys += [ma.split(":")[0]]
+		c = d.confirmations
+		i = 2 if c >= minconf else 1
+
+		for key in keys:
+			if key not in accts: accts[key] = [0,0,0]
+			for j in ([0] if c == 0 else []) + [i]:
+				accts[key][j] += d.amount
+
+	fs = "{:12}  {:<%s} {:<%s} {:<}" % (16,16)
+	mc,lbl = str(minconf),"confirms"
+	print fs.format("Wallet","Unconfirmed",
+			"<%s %s"%(mc,lbl),">=%s %s"%(mc,lbl))
+	for key in sorted(accts.keys()):
+		print fs.format(key+":", *[str(trim_exponent(a))+" BTC" for a in accts[key]])
+
+def viewtx(infile):
 	c = connect_to_bitcoind()
 	c = connect_to_bitcoind()
-	data = c.listunspent(0)
-	o = [0,0,0,0,0,0] # su,sb,sc, uu,ub,uc
-	for d in data:
-		j = 0 if d.spendable else 3
-		if d.confirmations == 0: o[j] += d.amount
-		k = 1 if d.confirmations < minconf else 2
-		o[j+k] += d.amount
-
-	fs = "{}:\n  {:<12} unconfirmed\n  {:<12} <{M}  {C}\n  {:<12} >={M} {C}"
-	for lbl,n in ("Spendable",0),("Unspendable",3):
-		if sum(o[n:3+n]) == 0:
-			print "{}: {}".format(lbl,"NONE")
-		else:
-			print fs.format(lbl,o[n+0],o[n+1],o[n+2],M=minconf,C="confirmations")
+	tx_data = get_lines_from_file(infile,"transaction data")
+
+	metadata,tx_hex,inputs_data,b2m_map = parse_tx_data(tx_data,infile)
+	view_tx_data(c,inputs_data,tx_hex,b2m_map,metadata)
+
+def check_addrfile(infile): parse_addrs_file(infile)

+ 156 - 198
mmgen/tx.py

@@ -37,7 +37,31 @@ which makes the signing process more complicated.  When signing the
 transaction, keys for the non-mmgen inputs must be supplied in a separate
 transaction, keys for the non-mmgen inputs must be supplied in a separate
 file using the '-k' option to mmgen-txsign.
 file using the '-k' option to mmgen-txsign.
 
 
-Selected mmgen inputs: %s"""
+Selected mmgen inputs: %s""",
+'too_many_acct_addresses': """
+ERROR: More than one address found for account: "%s".
+The tracking "wallet.dat" file appears to have been altered by a non-{g.proj_name}
+program.  Please restore "wallet.dat" from a backup or create a new wallet
+and re-import your addresses.""".strip().format(g=g),
+	'addrfile_no_data_msg': """
+No data found for MMgen address '%s'. Please import this address into
+your tracking wallet, or supply an address file for it on the command line.
+""".strip(),
+	'addrfile_warn_msg': """
+Warning: no data for address '{mmaddr}' was found in the tracking wallet, so
+this information was taken from the user-supplied address file. You're strongly
+advised to import this address into your tracking wallet before proceeding with
+this transaction.  The address will not be tracked until you do so.
+""".strip(),
+	'addrfile_fail_msg': """
+No data for MMgen address '{mmaddr}' could be found in either the tracking
+wallet or the supplied address file.  Please import this address into your
+tracking wallet, or supply an address file for it on the command line.
+""".strip(),
+	'no_spendable_outputs': """
+No spendable outputs found!  Import addresses with balances into your
+watch-only wallet using 'mmgen-addrimport' and then re-run this program.
+""".strip()
 }
 }
 
 
 # Deleted text:
 # Deleted text:
@@ -73,6 +97,7 @@ def trim_exponent(n):
 
 
 
 
 def is_btc_amt(amt):
 def is_btc_amt(amt):
+	# amt must be a string!
 
 
 	from decimal import Decimal
 	from decimal import Decimal
 	try:
 	try:
@@ -94,12 +119,11 @@ def is_btc_amt(amt):
 
 
 	return trim_exponent(ret)
 	return trim_exponent(ret)
 
 
-def check_btc_amt(amt):
+
+def normalize_btc_amt(amt):
 	ret = is_btc_amt(amt)
 	ret = is_btc_amt(amt)
-	if ret:
-		return ret
-	else:
-		sys.exit(3)
+	if ret: return ret
+	else:   sys.exit(3)
 
 
 
 
 def get_bitcoind_cfg_options(cfg_keys):
 def get_bitcoind_cfg_options(cfg_keys):
@@ -116,37 +140,24 @@ def get_bitcoind_cfg_options(cfg_keys):
 		msg("Don't know where to look for 'bitcoin.conf'")
 		msg("Don't know where to look for 'bitcoin.conf'")
 		sys.exit(3)
 		sys.exit(3)
 
 
-	try:
-		f = open(cfg_file)
-	except:
-		msg("Unable to open file '%s' for reading" % cfg_file)
-		sys.exit(2)
-
-	cfg = {}
-
-	for line in f.readlines():
-		s = line.translate(None,"\n\t ").split("=")
-		for k in cfg_keys:
-			if s[0] == k: cfg[k] = s[1]
+	cfg = dict([(k,v) for k,v in [split2(line.translate(None,"\t "),"=")
+			for line in get_lines_from_file(cfg_file)] if k in cfg_keys])
 
 
-	f.close()
-
-	for k in cfg_keys:
-		if not k in cfg:
-			msg("Configuration option '%s' must be set in %s" % (k,cfg_file))
-			sys.exit(2)
+	for k in set(cfg_keys) - set(cfg.keys()):
+		msg("Configuration option '%s' must be set in %s" % (k,cfg_file))
+		sys.exit(2)
 
 
 	return cfg
 	return cfg
 
 
 
 
-def print_tx_to_file(tx,sel_unspent,send_amt,b2m_map,opts):
+def write_tx_to_file(tx,sel_unspent,send_amt,b2m_map,opts):
 	tx_id = make_chksum_6(unhexlify(tx)).upper()
 	tx_id = make_chksum_6(unhexlify(tx)).upper()
 	outfile = "tx_%s[%s].%s" % (tx_id,send_amt,g.rawtx_ext)
 	outfile = "tx_%s[%s].%s" % (tx_id,send_amt,g.rawtx_ext)
 	if 'outdir' in opts:
 	if 'outdir' in opts:
 		outfile = "%s/%s" % (opts['outdir'], outfile)
 		outfile = "%s/%s" % (opts['outdir'], outfile)
-	metadata = "%s %s %s" % (tx_id, send_amt, make_timestamp())
-	data = "%s\n%s\n%s\n%s\n" % (
-			metadata, tx,
+	data = "{} {} {}\n{}\n{}\n{}\n".format(
+			tx_id, send_amt, make_timestamp(),
+			tx,
 			repr([i.__dict__ for i in sel_unspent]),
 			repr([i.__dict__ for i in sel_unspent]),
 			repr(b2m_map)
 			repr(b2m_map)
 		)
 		)
@@ -154,17 +165,22 @@ def print_tx_to_file(tx,sel_unspent,send_amt,b2m_map,opts):
 	msg("Transaction data saved to file '%s'" % outfile)
 	msg("Transaction data saved to file '%s'" % outfile)
 
 
 
 
-def print_signed_tx_to_file(tx,sig_tx,metadata,opts):
+def write_signed_tx_to_file(tx,sig_tx,metadata,inputs_data,b2m_map,opts):
 	tx_id = make_chksum_6(unhexlify(tx)).upper()
 	tx_id = make_chksum_6(unhexlify(tx)).upper()
 	outfile = "tx_%s[%s].%s" % (metadata[0],metadata[1],g.sigtx_ext)
 	outfile = "tx_%s[%s].%s" % (metadata[0],metadata[1],g.sigtx_ext)
 	if 'outdir' in opts:
 	if 'outdir' in opts:
 		outfile = "%s/%s" % (opts['outdir'], outfile)
 		outfile = "%s/%s" % (opts['outdir'], outfile)
-	data = "%s\n%s\n" % (" ".join(metadata),sig_tx)
+	data = "{}\n{}\n{}\n{}\n".format(
+			" ".join(metadata[:2] + [make_timestamp()]),
+			sig_tx,
+			repr(inputs_data),
+			repr(b2m_map)
+		)
 	write_to_file(outfile,data,confirm=False)
 	write_to_file(outfile,data,confirm=False)
 	msg("Signed transaction saved to file '%s'" % outfile)
 	msg("Signed transaction saved to file '%s'" % outfile)
 
 
 
 
-def print_sent_tx_to_file(tx,metadata,opts):
+def write_sent_tx_num_to_file(tx,metadata,opts):
 	outfile = "tx_{}[{}].out".format(*metadata[:2])
 	outfile = "tx_{}[{}].out".format(*metadata[:2])
 	if 'outdir' in opts:
 	if 'outdir' in opts:
 		outfile = "%s/%s" % (opts['outdir'], outfile)
 		outfile = "%s/%s" % (opts['outdir'], outfile)
@@ -176,7 +192,7 @@ def format_unspent_outputs_for_printing(out,sort_info,total):
 
 
 	pfs  = " %-4s %-67s %-34s %-12s %-13s %-8s %-10s %s"
 	pfs  = " %-4s %-67s %-34s %-12s %-13s %-8s %-10s %s"
 	pout = [pfs % ("Num","TX id,Vout","Address","MMgen ID",
 	pout = [pfs % ("Num","TX id,Vout","Address","MMgen ID",
-		"Amount (BTC)","Confirms","Age (days)", "Comment")]
+		"Amount (BTC)","Conf.","Age (days)", "Comment")]
 
 
 	for n,i in enumerate(out):
 	for n,i in enumerate(out):
 		addr = "=" if i.skip == "addr" and "grouped" in sort_info else i.address
 		addr = "=" if i.skip == "addr" and "grouped" in sort_info else i.address
@@ -199,7 +215,10 @@ def sort_and_view(unspent):
 	def s_txid(i):  return "%s %03s" % (i.txid,i.vout)
 	def s_txid(i):  return "%s %03s" % (i.txid,i.vout)
 	def s_addr(i):  return i.address
 	def s_addr(i):  return i.address
 	def s_age(i):   return i.confirmations
 	def s_age(i):   return i.confirmations
-	def s_mmgen(i): return i.account
+	def s_mmgen(i):
+		m = parse_mmgen_label(i.account)[0]
+		if m: return "{}:{:>0{w}}".format(w=g.mmgen_idx_max_digits, *m.split(":"))
+		else: return "G" + i.account
 
 
 	sort,group,show_days,show_mmaddr,reverse = "age",False,False,True,True
 	sort,group,show_days,show_mmaddr,reverse = "age",False,False,True,True
 	unspent.sort(key=s_age,reverse=reverse) # Reverse age sort by default
 	unspent.sort(key=s_age,reverse=reverse) # Reverse age sort by default
@@ -216,7 +235,7 @@ Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen
 "('q' = quit sorting, 'p' = print to file, 'v' = pager view, 'w' = wide view): "
 "('q' = quit sorting, 'p' = print to file, 'v' = pager view, 'w' = wide view): "
 
 
 	from copy import deepcopy
 	from copy import deepcopy
-	print_to_file_msg = ""
+	write_to_file_msg = ""
 	msg("")
 	msg("")
 
 
 	from mmgen.term import get_terminal_size
 	from mmgen.term import get_terminal_size
@@ -233,7 +252,7 @@ Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen
 		addr_w = min(34+((1+max_acct_len) if show_mmaddr else 0),cols-46)
 		addr_w = min(34+((1+max_acct_len) if show_mmaddr else 0),cols-46)
 		tx_w = max(11,min(64, cols-addr_w-32))
 		tx_w = max(11,min(64, cols-addr_w-32))
 		fs = " %-4s %-" + str(tx_w) + "s %-2s %-" + str(addr_w) + "s %-13s %-s"
 		fs = " %-4s %-" + str(tx_w) + "s %-2s %-" + str(addr_w) + "s %-13s %-s"
-		a = "Age(d)" if show_days else "Confirms"
+		a = "Age(d)" if show_days else "Conf."
 		table_hdr = fs % ("Num","TX id  Vout","","Address", "Amount (BTC)",a)
 		table_hdr = fs % ("Num","TX id  Vout","","Address", "Amount (BTC)",a)
 
 
 		unsp = deepcopy(unspent)
 		unsp = deepcopy(unspent)
@@ -283,14 +302,13 @@ Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen
 			d = i.days if show_days else i.confirmations
 			d = i.days if show_days else i.confirmations
 			out.append(fs % (str(n+1)+")",i.tx,i.vout,i.addr,i.amt,d))
 			out.append(fs % (str(n+1)+")",i.tx,i.vout,i.addr,i.amt,d))
 
 
-		msg("\n".join(out) +"\n\n" + print_to_file_msg + options_msg)
-		print_to_file_msg = ""
+		msg("\n".join(out) +"\n\n" + write_to_file_msg + options_msg)
+		write_to_file_msg = ""
 
 
-		immed_chars = "atDdAMrgmeqpvw"
 		skip_prompt = False
 		skip_prompt = False
 
 
 		while True:
 		while True:
-			reply = get_char(prompt, immed_chars=immed_chars)
+			reply = get_char(prompt, immed_chars="atDdAMrgmeqpvw")
 
 
 			if   reply == 'a': unspent.sort(key=s_amt);  sort = "amount"
 			if   reply == 'a': unspent.sort(key=s_amt);  sort = "amount"
 			elif reply == 't': unspent.sort(key=s_txid); sort = "txid"
 			elif reply == 't': unspent.sort(key=s_txid); sort = "txid"
@@ -311,7 +329,7 @@ Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen
 				data = format_unspent_outputs_for_printing(unsp,sort_info,total)
 				data = format_unspent_outputs_for_printing(unsp,sort_info,total)
 				outfile = "listunspent[%s].out" % ",".join(sort_info)
 				outfile = "listunspent[%s].out" % ",".join(sort_info)
 				write_to_file(outfile, data)
 				write_to_file(outfile, data)
-				print_to_file_msg = "Data written to '%s'\n\n" % outfile
+				write_to_file_msg = "Data written to '%s'\n\n" % outfile
 			elif reply == 'v':
 			elif reply == 'v':
 				do_pager("\n".join(out))
 				do_pager("\n".join(out))
 				continue
 				continue
@@ -332,15 +350,10 @@ Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen
 
 
 
 
 def parse_mmgen_label(s,check_label_len=False):
 def parse_mmgen_label(s,check_label_len=False):
-
-	if not s: return "",""
-
-	try:    w1,w2 = s.split(None,1)
-	except: w1,w2 = s,""
-
-	if not is_mmgen_addr(w1): return "",w1
-	if check_label_len: check_addr_label(w2)
-	return w1,w2
+	l = split2(s)
+	if not is_mmgen_addr(l[0]): return "",s
+	if check_label_len: check_addr_label(l[1])
+	return tuple(l)
 
 
 
 
 def view_tx_data(c,inputs_data,tx_hex,b2m_map,metadata=[],pager=False):
 def view_tx_data(c,inputs_data,tx_hex,b2m_map,metadata=[],pager=False):
@@ -401,53 +414,51 @@ def view_tx_data(c,inputs_data,tx_hex,b2m_map,metadata=[],pager=False):
 	out += "TX fee:       %s BTC\n" % trim_exponent(total_in-total_out)
 	out += "TX fee:       %s BTC\n" % trim_exponent(total_in-total_out)
 
 
 	if pager: do_pager(out)
 	if pager: do_pager(out)
-	else:     msg("\n"+out)
+	else:     print "\n"+out
 
 
 
 
 def parse_tx_data(tx_data,infile):
 def parse_tx_data(tx_data,infile):
 
 
-	if len(tx_data) != 4:
+	try:
+		metadata,tx_hex,inputs_data,outputs_data = tx_data
+	except:
 		msg("'%s': not a transaction file" % infile)
 		msg("'%s': not a transaction file" % infile)
 		sys.exit(2)
 		sys.exit(2)
 
 
 	err_fmt = "Transaction %s is invalid"
 	err_fmt = "Transaction %s is invalid"
 
 
-	if len(tx_data[0].split()) != 3:
+	if len(metadata.split()) != 3:
 		msg(err_fmt % "metadata")
 		msg(err_fmt % "metadata")
 		sys.exit(2)
 		sys.exit(2)
 
 
-	try: unhexlify(tx_data[1])
+	try: unhexlify(tx_hex)
 	except:
 	except:
 		msg(err_fmt % "hex data")
 		msg(err_fmt % "hex data")
 		sys.exit(2)
 		sys.exit(2)
 	else:
 	else:
-		if not tx_data:
+		if not tx_hex:
 			msg("Transaction is empty!")
 			msg("Transaction is empty!")
 			sys.exit(2)
 			sys.exit(2)
 
 
 	try:
 	try:
-		inputs_data = eval(tx_data[2])
+		inputs_data = eval(inputs_data)
 	except:
 	except:
 		msg(err_fmt % "inputs data")
 		msg(err_fmt % "inputs data")
 		sys.exit(2)
 		sys.exit(2)
-	else:
-		if not inputs_data:
-			msg("Transaction has no inputs!")
-			sys.exit(2)
 
 
 	try:
 	try:
-		map_data = eval(tx_data[3])
+		outputs_data = eval(outputs_data)
 	except:
 	except:
 		msg(err_fmt % "mmgen to btc address map data")
 		msg(err_fmt % "mmgen to btc address map data")
 		sys.exit(2)
 		sys.exit(2)
 
 
-	return tx_data[0].split(),tx_data[1],inputs_data,map_data
+	return metadata.split(),tx_hex,inputs_data,outputs_data
 
 
 
 
 def select_outputs(unspent,prompt):
 def select_outputs(unspent,prompt):
 
 
 	while True:
 	while True:
-		reply = my_raw_input(prompt,allowed_chars="0123456789 -").strip()
+		reply = my_raw_input(prompt).strip()
 
 
 		if not reply: continue
 		if not reply: continue
 
 
@@ -462,111 +473,61 @@ def select_outputs(unspent,prompt):
 
 
 		return selected
 		return selected
 
 
-def is_mmgen_seed(s):
+def is_mmgen_seed_id(s):
 	import re
 	import re
-	return len(s) == 8 and re.match(r"^[0123456789ABCDEF]*$",s)
+	return True if re.match(r"^[0123456789ABCDEF]{8}$",s) else False
 
 
-def is_mmgen_num(s):
+def is_mmgen_idx(s):
 	import re
 	import re
-	return len(s) <= g.mmgen_idx_max_digits \
-		and re.match(r"^[123456789]+[0123456789]*$",s)
+	m = g.mmgen_idx_max_digits
+	return True if re.match(r"^[0123456789]{1,"+str(m)+r"}$",s) else False
 
 
 def is_mmgen_addr(s):
 def is_mmgen_addr(s):
-	import re
-	return len(s) > 9 and s[8] == ':' \
-		and re.match(r"^[0123456789ABCDEF]*$",s[:8]) \
-		and len(s[9:]) <= g.mmgen_idx_max_digits \
-		and re.match(r"^[123456789]+[0123456789]*$",s[9:])
+	seed_id,idx = split2(s,":")
+	return is_mmgen_seed_id(seed_id) and is_mmgen_idx(idx)
 
 
 def is_btc_addr(s):
 def is_btc_addr(s):
 	from mmgen.bitcoin import verify_addr
 	from mmgen.bitcoin import verify_addr
 	return verify_addr(s)
 	return verify_addr(s)
 
 
 
 
-def btc_addr_to_mmgen_addr(btc_addr,b2m_map):
-	if btc_addr in b2m_map:
-		return b2m_map[btc_addr]
-	return "",""
-
-
-def mmgen_addr_to_walletd(c,mmaddr,acct_data):
+def mmaddr2btcaddr_bitcoind(c,mmaddr,acct_data):
 
 
 	# We don't want to create a new object, so we'll use append()
 	# We don't want to create a new object, so we'll use append()
 	if not acct_data:
 	if not acct_data:
 		for i in c.listaccounts():
 		for i in c.listaccounts():
 			acct_data.append(i)
 			acct_data.append(i)
 
 
-	for a in acct_data:
-		if not a: continue
-		try:
-			w1,w2 = a.split(None,1)
-		except:
-			w1,w2 = a,""
-		if w1 == mmaddr:
-			acct = a
-			break
-	else:
-		return "",""
-
-	alist = c.getaddressesbyaccount(acct)
+	for acct in acct_data:
+		m,comment = parse_mmgen_label(acct)
+		if m == mmaddr:
+			addrlist = c.getaddressesbyaccount(acct)
+			if len(addrlist) == 1:
+				return addrlist[0],comment
+			else:
+				msg(txmsg['too_many_acct_addresses'] % acct); sys.exit(2)
 
 
-	if len(alist) != 1:
-		msg("""
-ERROR: More than one address found for account: "%s".
-The tracking "wallet.dat" file appears to have been altered by a non-%s
-program.  Please restore "wallet.dat" from a backup or create a new wallet
-and re-import your addresses.
-""".strip() % (acct,g.proj_name_cap))
-		sys.exit(3)
+	return "",""
 
 
-	return alist[0],w2
 
 
+def mmaddr2btcaddr_addrfile(mmaddr,addr_data):
 
 
-def mmgen_addr_to_addr_data(m,addr_data):
+	mmid,mmidx = mmaddr.split(":")
 
 
-	no_data_msg = """
-No data found for MMgen address '%s'. Please import this address into
-your tracking wallet, or supply an address file for it on the command line.
-""".strip() % m
-	warn_msg = """
-Warning: no data for address '%s' exists in the wallet, so it was
-taken from the user-supplied address file.  You're strongly advised to
-import this address into your tracking wallet before proceeding with
-this transaction.  The address will not be tracked until you do so.
-""".strip() % m
-	fail_msg = """
-No data found for MMgen address '%s' in either wallet or supplied
-address file.  Please import this address into your tracking wallet, or
-supply an address file for it on the command line.
-""".strip() % m
-
-	ID,num = m.split(":")
-	from binascii import unhexlify
-	try: unhexlify(ID)
-	except: pass
-	else:
-		try: num = int(num)
-		except: pass
-		else:
-			if not addr_data:
-				msg(no_data_msg)
-				sys.exit(2)
-			for i in addr_data:
-				if ID == i[0]:
-					for j in i[1]:
-						if j[0] == num:
-							msg(warn_msg)
-							if not user_confirm("Continue anyway?"):
-								sys.exit(1)
-							return j[1],(j[2] if len(j) == 3 else "")
-			msg(fail_msg)
-			sys.exit(2)
+	for ad in addr_data:
+		if mmid == ad[0]:
+			for j in ad[1]:
+				if j[0] == mmidx:
+					msg(txmsg['addrfile_warn_msg'].format(mmaddr=mmaddr))
+					if not user_confirm("Continue anyway?"):
+						sys.exit(1)
+					return j[1:] if len(j) == 3 else (j[1],"")
 
 
-	msg("Invalid format: %s" % m)
-	sys.exit(3)
+	msg(txmsg['addrfile_fail_msg'].format(mmaddr=mmaddr))
+	sys.exit(2)
 
 
 
 
-def check_mmgen_to_btc_addr_mappings(inputs_data,b2m_map,infiles,seeds,opts):
+def check_mmgen_to_btc_addr_mappings(inputs_data,b2m_map,infiles,saved_seeds,opts):
 	in_maplist = [(i['account'].split()[0],i['address'])
 	in_maplist = [(i['account'].split()[0],i['address'])
 		for i in inputs_data if i['account']
 		for i in inputs_data if i['account']
 			and is_mmgen_addr(i['account'].split()[0])]
 			and is_mmgen_addr(i['account'].split()[0])]
@@ -578,7 +539,7 @@ def check_mmgen_to_btc_addr_mappings(inputs_data,b2m_map,infiles,seeds,opts):
 		mmaddrs = [i[0] for i in maplist]
 		mmaddrs = [i[0] for i in maplist]
 		from copy import deepcopy
 		from copy import deepcopy
 		pairs = get_keys_for_mmgen_addrs(mmaddrs,
 		pairs = get_keys_for_mmgen_addrs(mmaddrs,
-				deepcopy(infiles),seeds,opts,gen_pairs=True)
+				infiles,saved_seeds,opts,gen_pairs=True)
 		for a,b in zip(sorted(pairs),sorted(maplist)):
 		for a,b in zip(sorted(pairs),sorted(maplist)):
 			if a != b:
 			if a != b:
 				msg("""
 				msg("""
@@ -607,6 +568,17 @@ Only ASCII printable characters are permitted.
 			sys.exit(3)
 			sys.exit(3)
 
 
 
 
+def check_addr_data_hash(seed_id,addr_data):
+	from hashlib import new as hashlib_new
+	addr_data_chksum = make_chksum_8(
+		" ".join(["{} {}".format(*d[:2]) for d in addr_data]), sep=True
+	)
+	from mmgen.addr import fmt_addr_list
+	fl = fmt_addr_list([int(a[0]) for a in addr_data])
+	msg("Computed address data checksum for '{}[{}]': {}".format(
+				seed_id,fl,addr_data_chksum))
+	msg("Check this value against your records")
+
 def parse_addrs_file(f):
 def parse_addrs_file(f):
 
 
 	lines = get_lines_from_file(f,"address data",remove_comments=True)
 	lines = get_lines_from_file(f,"address data",remove_comments=True)
@@ -623,14 +595,14 @@ def parse_addrs_file(f):
 		msg("'%s': invalid first line" % lines[0])
 		msg("'%s': invalid first line" % lines[0])
 	elif cbrace != '}':
 	elif cbrace != '}':
 		msg("'%s': invalid last line" % cbrace)
 		msg("'%s': invalid last line" % cbrace)
-	elif not is_mmgen_seed(seed_id):
+	elif not is_mmgen_seed_id(seed_id):
 		msg("'%s': invalid Seed ID" % seed_id)
 		msg("'%s': invalid Seed ID" % seed_id)
 	else:
 	else:
-		ret = []
+		addr_data = []
 		for i in lines[1:-1]:
 		for i in lines[1:-1]:
 			d = i.split(None,2)
 			d = i.split(None,2)
 
 
-			if not is_mmgen_num(d[0]):
+			if not is_mmgen_idx(d[0]):
 				msg("'%s': invalid address num. in line: %s" % (d[0],d))
 				msg("'%s': invalid address num. in line: %s" % (d[0],d))
 				sys.exit(3)
 				sys.exit(3)
 
 
@@ -641,9 +613,11 @@ def parse_addrs_file(f):
 			if len(d) == 3:
 			if len(d) == 3:
 				check_addr_label(d[2])
 				check_addr_label(d[2])
 
 
-			ret.append(tuple(d))
+			addr_data.append(tuple(d))
 
 
-		return seed_id,ret
+		check_addr_data_hash(seed_id,addr_data)
+
+		return seed_id,addr_data
 
 
 	sys.exit(3)
 	sys.exit(3)
 
 
@@ -664,62 +638,46 @@ def sign_transaction(c,tx_hex,sig_data,keys=None):
 
 
 	return sig_tx
 	return sig_tx
 
 
+def get_seed_for_seed_id(seed_id,infiles,saved_seeds,opts):
+
+	if seed_id in saved_seeds.keys():
+		return saved_seeds[seed_id]
+
+	while True:
+		if infiles:
+			seed = get_seed_retry(infiles.pop(0),opts)
+		elif "from_brain" in opts or "from_mnemonic" in opts \
+			or "from_seed" in opts or "from_incog" in opts:
+			msg("Need data for seed ID %s" % seed_id)
+			seed = get_seed_retry("",opts)
+		else:
+			msg("ERROR: No seed source found for seed ID: %s" % seed_id)
+			sys.exit(2)
+
+		s_id = make_chksum_8(seed)
+		saved_seeds[s_id] = seed
+
+		if s_id == seed_id: return seed
 
 
-def get_keys_for_mmgen_addrs(mmgen_addrs,infiles,seeds,opts,gen_pairs=False):
+
+def get_keys_for_mmgen_addrs(mmgen_addrs,infiles,saved_seeds,opts,gen_pairs=False):
 
 
 	seed_ids = list(set([i[:8] for i in mmgen_addrs]))
 	seed_ids = list(set([i[:8] for i in mmgen_addrs]))
 	seed_ids_save = seed_ids[0:]  # deep copy
 	seed_ids_save = seed_ids[0:]  # deep copy
 	ret = []
 	ret = []
 
 
-	seeds_keys = [i for i in seed_ids if i in seeds]
+	for seed_id in seed_ids:
+		# Returns only if seed is found
+		seed = get_seed_for_seed_id(seed_id,infiles,saved_seeds,opts)
 
 
-	while seed_ids:
-		if seeds_keys:
-			seed = seeds[seeds_keys.pop(0)]
-		else:
-			infile = False
-			if infiles:
-				infile = infiles.pop(0)
-				seed = get_seed_retry(infile,opts)
-			elif "from_brain" in opts or "from_mnemonic" in opts \
-				or "from_seed" in opts or "from_incog" in opts:
-				msg("Need data for seed ID %s" % seed_ids[0])
-				seed = get_seed_retry("",opts)
-			else:
-				b,p,v = ("A seed","","is") if len(seed_ids) == 1 \
-						else ("Seed","s","are")
-				msg("ERROR: %s source%s %s required for the following seed ID%s: %s"%
-						(b,p,v,p," ".join(seed_ids)))
-				sys.exit(2)
-
-		seed_id = make_chksum_8(seed)
-		if seed_id in seed_ids:
-			seed_ids.remove(seed_id)
-			addr_ids = [int(i[9:]) for i in mmgen_addrs if i[:8] == seed_id]
-			seeds[seed_id] = seed
-			from mmgen.addr import generate_keys,generate_addrs
-			if gen_pairs:
-				o = {"gen_what":"addresses"}
-				ret += [("%s:%s" % (seed_id,i['num']),i['addr'])
-					for i in generate_addrs(seed, addr_ids, o)]
-			else:
-				ret += [i['wif'] for i in generate_keys(seed, addr_ids)]
+		addr_ids = [int(i[9:]) for i in mmgen_addrs if i[:8] == seed_id]
+		from mmgen.addr import generate_keys,generate_addrs
+		if gen_pairs:
+			o = {"gen_what":"addresses"}
+			ret += [("%s:%s" % (seed_id,i['num']),i['addr'])
+				for i in generate_addrs(seed, addr_ids, o)]
 		else:
 		else:
-			if seed_id in seed_ids_save:
-				msg_r("Ignoring duplicate seed source")
-				if infile: msg(" '%s'" % infile)
-				else:      msg(" for ID %s" % seed_id)
-			else:
-				msg("Seed source produced an invalid seed ID (%s)" % seed_id)
-				if "from_incog" in opts or infile.split(".")[-1] == g.incog_ext:
-					msg(
-"""Incorrect hash preset, password or incognito wallet data
-
-Trying again...""")
-					infiles.insert(0,infile) # ugly!
-				elif infile:
-					msg("Invalid input file '%s'" % infile)
-					sys.exit(2)
+			ret += [i['wif'] for i in generate_keys(seed, addr_ids)]
 
 
 	return ret
 	return ret
 
 
@@ -759,7 +717,7 @@ def preverify_keys(addrs_orig, keys_orig):
 
 
 	if len(keys) < len(addrs):
 	if len(keys) < len(addrs):
 		msg("ERROR: not enough keys (%s) for number of non-%s addresses (%s)" %
 		msg("ERROR: not enough keys (%s) for number of non-%s addresses (%s)" %
-				(len(keys),g.proj_name_cap,len(addrs)))
+				(len(keys),g.proj_name,len(addrs)))
 		sys.exit(2)
 		sys.exit(2)
 
 
 	import mmgen.bitcoin as b
 	import mmgen.bitcoin as b
@@ -803,7 +761,7 @@ def preverify_keys(addrs_orig, keys_orig):
 		if addrs:
 		if addrs:
 			s = "" if len(addrs) == 1 else "es"
 			s = "" if len(addrs) == 1 else "es"
 			msg("No keys found for the following non-%s address%s:" %
 			msg("No keys found for the following non-%s address%s:" %
-					(g.proj_name_cap,s))
+					(g.proj_name,s))
 			print "  %s" % "\n  ".join(addrs)
 			print "  %s" % "\n  ".join(addrs)
 			sys.exit(2)
 			sys.exit(2)
 
 

+ 31 - 45
mmgen/util.py

@@ -43,10 +43,10 @@ def vmsg_r(s):
 def bail(): sys.exit(9)
 def bail(): sys.exit(9)
 
 
 def get_extension(f):
 def get_extension(f):
-	try: return f.split(".")[-1]
-	except: return ""
+	import os
+	return os.path.splitext(f)[1][1:]
 
 
-def my_raw_input(prompt,echo=True,allowed_chars=""):
+def my_raw_input(prompt,echo=True):
 	try:
 	try:
 		if echo:
 		if echo:
 			reply = raw_input(prompt)
 			reply = raw_input(prompt)
@@ -61,33 +61,10 @@ def my_raw_input(prompt,echo=True,allowed_chars=""):
 	return reply
 	return reply
 
 
 
 
-def my_raw_input_old(prompt,echo=True,allowed_chars=""):
-
-	msg_r(prompt)
-	reply = ""
-
-	while True:
-		ch = get_char(immed_chars="ALL_EXCEPT_ENTER")
-		if allowed_chars and ch not in allowed_chars+"\n\r\b"+chr(0x7f):
-			continue
-		if echo:
-			if ch in "\b"+chr(0x7f): # WIP
-				pass
-				# reply.pop(0)
-			else: msg_r(ch)
-		if ch in "\n\r":
-			if not echo: msg("")
-			break
-		reply += ch
-
-	return reply
-
-
 def _get_hash_params(hash_preset):
 def _get_hash_params(hash_preset):
 	if hash_preset in g.hash_presets:
 	if hash_preset in g.hash_presets:
 		return g.hash_presets[hash_preset] # N,p,r,buflen
 		return g.hash_presets[hash_preset] # N,p,r,buflen
-	else:
-		# Shouldn't be here
+	else: # Shouldn't be here
 		msg("%s: invalid 'hash_preset' value" % hash_preset)
 		msg("%s: invalid 'hash_preset' value" % hash_preset)
 		sys.exit(3)
 		sys.exit(3)
 
 
@@ -126,7 +103,7 @@ cmessages = {
 	'unencrypted_secret_keys': """
 	'unencrypted_secret_keys': """
 This program generates secret keys from your {} seed, outputting them in
 This program generates secret keys from your {} seed, outputting them in
 UNENCRYPTED form.  Generate only the key(s) you need and guard them carefully.
 UNENCRYPTED form.  Generate only the key(s) you need and guard them carefully.
-""".format(g.proj_name_cap),
+""".format(g.proj_name),
 	'brain_warning': """
 	'brain_warning': """
 ############################## EXPERTS ONLY! ##############################
 ############################## EXPERTS ONLY! ##############################
 
 
@@ -154,12 +131,10 @@ def confirm_or_exit(message, question, expect="YES"):
 
 
 	conf_msg = "Type uppercase '%s' to confirm: " % expect
 	conf_msg = "Type uppercase '%s' to confirm: " % expect
 
 
-	if question[0].isupper():
-		prompt = question + "  " + conf_msg
-	else:
-		prompt = "Are you sure you want to %s?\n%s" % (question,conf_msg)
+	p = question+"  "+conf_msg if question[0].isupper() else \
+		"Are you sure you want to %s?\n%s" % (question,conf_msg)
 
 
-	if my_raw_input(prompt).strip() != expect:
+	if my_raw_input(p).strip() != expect:
 		msg("Exiting at user request")
 		msg("Exiting at user request")
 		sys.exit(2)
 		sys.exit(2)
 
 
@@ -196,9 +171,10 @@ def prompt_and_get_char(prompt,chars,enter_ok=False,verbose=False):
 		else: msg_r("\r")
 		else: msg_r("\r")
 
 
 
 
-def make_chksum_8(s):
+def make_chksum_8(s,sep=False):
 	from hashlib import sha256
 	from hashlib import sha256
-	return sha256(sha256(s).digest()).hexdigest()[:8].upper()
+	s = sha256(sha256(s).digest()).hexdigest()[:8].upper()
+	return "{} {}".format(s[:4],s[4:]) if sep else s
 
 
 def make_chksum_6(s):
 def make_chksum_6(s):
 	from hashlib import sha256
 	from hashlib import sha256
@@ -222,12 +198,14 @@ def check_infile(f):
 		msg("Requested input file '%s' is unreadable by you" % f)
 		msg("Requested input file '%s' is unreadable by you" % f)
 		sys.exit(1)
 		sys.exit(1)
 
 
+	return True
+
 
 
 def _validate_addr_num(n):
 def _validate_addr_num(n):
 
 
 	try: n = int(n)
 	try: n = int(n)
 	except:
 	except:
-		msg("'%s': invalid argument for address" % n)
+		msg("'%s': address must be an integer" % n)
 		return False
 		return False
 
 
 	if n < 1:
 	if n < 1:
@@ -321,7 +299,7 @@ def _get_seed_from_brain_passphrase(words,opts):
 def encrypt_seed(seed, key, iv=1):
 def encrypt_seed(seed, key, iv=1):
 	"""
 	"""
 	Encrypt a seed for an {} deterministic wallet
 	Encrypt a seed for an {} deterministic wallet
-	""".format(g.proj_name_cap)
+	""".format(g.proj_name)
 
 
 	# 192-bit seed is 24 bytes -> not multiple of 16.  Must use MODE_CTR
 	# 192-bit seed is 24 bytes -> not multiple of 16.  Must use MODE_CTR
 	from Crypto.Cipher import AES
 	from Crypto.Cipher import AES
@@ -439,6 +417,13 @@ def _display_control_data(label,metadata,hash_preset,salt,enc_seed):
 	): msg(fs.format(*i))
 	): msg(fs.format(*i))
 
 
 
 
+def splitN(s,n,sep=None):                      # always return an n-element list
+	ret = s.split(sep,n-1)
+	return ret + ["" for i in range(n-len(ret))]
+
+def split2(s,sep=None): return splitN(s,2,sep) # always return a 2-element list
+def split3(s,sep=None): return splitN(s,3,sep) # always return a 3-element list
+
 def col4(s):
 def col4(s):
 	nondiv = 1 if len(s) % 4 else 0
 	nondiv = 1 if len(s) % 4 else 0
 	return " ".join([s[4*i:4*i+4] for i in range(len(s)/4 + nondiv)])
 	return " ".join([s[4*i:4*i+4] for i in range(len(s)/4 + nondiv)])
@@ -578,7 +563,7 @@ def get_data_from_wallet(infile,silent=False):
 	# Don't make this a qmsg: User will be prompted for passphrase and must see
 	# Don't make this a qmsg: User will be prompted for passphrase and must see
 	# the filename.
 	# the filename.
 	if not silent:
 	if not silent:
-		msg("Getting {} wallet data from file '{}'".format(g.proj_name_cap,infile))
+		msg("Getting {} wallet data from file '{}'".format(g.proj_name,infile))
 
 
 	f = open_file_or_exit(infile, 'r')
 	f = open_file_or_exit(infile, 'r')
 
 
@@ -653,7 +638,8 @@ def get_lines_from_file(infile,what="",remove_comments=False):
 		return lines
 		return lines
 
 
 
 
-def get_data_from_file(infile,what="data"):
+def get_data_from_file(infile,what="data",dash=False):
+	if dash and infile == "-": return sys.stdin.read()
 	qmsg("Getting %s from file '%s'" % (what,infile))
 	qmsg("Getting %s from file '%s'" % (what,infile))
 	f = open_file_or_exit(infile,'r')
 	f = open_file_or_exit(infile,'r')
 	data = f.read()
 	data = f.read()
@@ -682,7 +668,7 @@ def _get_seed_from_seed_data(words):
 		vmsg("%s data produces seed ID: %s" % (g.seed_ext,make_chksum_8(seed)))
 		vmsg("%s data produces seed ID: %s" % (g.seed_ext,make_chksum_8(seed)))
 		return seed
 		return seed
 	else:
 	else:
-		msg("Invalid checksum for {} seed".format(g.proj_name_cap))
+		msg("Invalid checksum for {} seed".format(g.proj_name))
 		return False
 		return False
 
 
 
 
@@ -717,7 +703,7 @@ def get_bitcoind_passphrase(prompt,opts):
 def get_seed_from_wallet(
 def get_seed_from_wallet(
 		infile,
 		infile,
 		opts,
 		opts,
-		prompt="Enter {} wallet passphrase: ".format(g.proj_name_cap),
+		prompt="Enter {} wallet passphrase: ".format(g.proj_name),
 		silent=False
 		silent=False
 		):
 		):
 
 
@@ -770,7 +756,7 @@ def get_hidden_incog_data(opts):
 def get_seed_from_incog_wallet(
 def get_seed_from_incog_wallet(
 		infile,
 		infile,
 		opts,
 		opts,
-		prompt="Enter %s wallet passphrase: " % g.proj_name_cap,
+		prompt="Enter {} wallet passphrase: ".format(g.proj_name),
 		silent=False,
 		silent=False,
 		hex_input=False
 		hex_input=False
 	):
 	):
@@ -806,7 +792,7 @@ def get_seed_from_incog_wallet(
 	msg("Configured hash presets: %s" % " ".join(sorted(g.hash_presets)))
 	msg("Configured hash presets: %s" % " ".join(sorted(g.hash_presets)))
 	while True:
 	while True:
 		p = "Enter hash preset for %s wallet (default='%s'): "
 		p = "Enter hash preset for %s wallet (default='%s'): "
-		hp = my_raw_input(p % (g.proj_name_cap, g.hash_preset))
+		hp = my_raw_input(p % (g.proj_name, g.hash_preset))
 		if not hp:
 		if not hp:
 			hp = g.hash_preset; break
 			hp = g.hash_preset; break
 		elif hp in g.hash_presets:
 		elif hp in g.hash_presets:
@@ -890,7 +876,7 @@ def _get_words(infile,what,prompt,opts):
 
 
 def get_seed(infile,opts,silent=False):
 def get_seed(infile,opts,silent=False):
 
 
-	ext = infile.split(".")[-1]
+	ext = get_extension(infile)
 
 
 	if   ext == g.mn_ext:           source = "mnemonic"
 	if   ext == g.mn_ext:           source = "mnemonic"
 	elif ext == g.brain_ext:        source = "brainwallet"
 	elif ext == g.brain_ext:        source = "brainwallet"
@@ -922,7 +908,7 @@ def get_seed(infile,opts,silent=False):
 		if not g.quiet:
 		if not g.quiet:
 			confirm_or_exit(
 			confirm_or_exit(
 				cmessages['brain_warning'].format(
 				cmessages['brain_warning'].format(
-					g.proj_name_cap, *_get_from_brain_opt_params(opts)),
+					g.proj_name, *_get_from_brain_opt_params(opts)),
 				"continue")
 				"continue")
 		prompt = "Enter brainwallet passphrase: "
 		prompt = "Enter brainwallet passphrase: "
 		words = _get_words(infile,"brainwallet data",prompt,opts)
 		words = _get_words(infile,"brainwallet data",prompt,opts)

+ 1 - 1
setup.py

@@ -3,7 +3,7 @@ from distutils.core import setup
 
 
 setup(
 setup(
 		name         = 'mmgen',
 		name         = 'mmgen',
-		version      = '0.7.4',
+		version      = '0.7.5',
 		author       = 'Philemon',
 		author       = 'Philemon',
 		author_email = 'mmgen-py@yandex.com',
 		author_email = 'mmgen-py@yandex.com',
 		url          = 'https://github.com/mmgen/mmgen',
 		url          = 'https://github.com/mmgen/mmgen',