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/util.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.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 = {
 	'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>",
 	'options': """
 -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
 -H, --show-hash-presets  Show information on available hash presets
 -K, --no-keyconv         Use internal libraries for address generation
                          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
                          prompting
--S, --stdout             Print {W} to stdout
+-S, --stdout             Print {what} to stdout
 -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
                          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'
--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
 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 '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 '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
 
-check_opts(opts,long_opts)
-
 if g.debug: show_opts_and_cmd_args(opts,cmd_args)
 
 if len(cmd_args) == 1 and (
@@ -158,12 +134,12 @@ if not addr_list: sys.exit(2)
 do_license_msg()
 
 # 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')
 
 # Generate data:
 
-if invoked_as == "addrgen":
+if gen_what == "addresses":
 	opts['print_addresses_only'] = True
 else:
 	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)
 
 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:
 if 'stdout' in opts:
-	if invoked_as == "keygen" and not g.quiet:
+	if gen_what == "keys" and not g.quiet:
 		confirm = True
 	else: confirm = False
 	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)
 else:
 	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]",
 	'options': """
 -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
                   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 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)")
 	sys.exit(1)
 
-check_opts(opts,long_opts)
-
 if cmd_args:
 	check_infile(cmd_args[0])
 	seed_id,addr_data = parse_addrs_file(cmd_args[0])
@@ -59,9 +55,9 @@ else:
 	seed_id,addr_data = "",[]
 
 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)
-	addr_data += [(None,i) for i in l]
+	addr_data += [(None,l) for l in lines]
 
 from mmgen.bitcoin import verify_addr
 qmsg_r("Validating addresses...")

+ 12 - 15
mmgen-passchg

@@ -26,37 +26,34 @@ from mmgen.util import *
 import mmgen.config as g
 
 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),
 	'usage':   "[opts] [filename]",
 	'options': """
 -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
 -k, --keep-old-passphrase   Keep old passphrase (use when changing hash
                             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
                             prompting
 -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 'verbose' in opts: g.verbose = True
-check_opts(opts,long_opts)
-
 if 'show_hash_presets' in opts: show_hash_presets()
 
 if len(cmd_args) != 1:

+ 5 - 10
mmgen-pywallet

@@ -75,26 +75,21 @@ help_data = {
 	'usage':   "[opts] <bitcoind wallet file>",
 	'options': """
 -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
 -j, --json             Dump wallet in json format
 -k, --keys             Dump all private keys (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
 """
 }
 
-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:
+	from mmgen.util import check_infile
 	check_infile(cmd_args[0])
 else:
 	usage(help_data)

+ 4 - 4
mmgen-tool

@@ -36,6 +36,8 @@ help_data = {
 -h, --help                 Print this help message
 -q, --quiet                Produce quieter output
 -v, --verbose              Produce more verbose output
+""",
+	'notes': """
 
 COMMANDS:{}
 Type '{} <command> --help for usage information on a particular
@@ -43,11 +45,9 @@ command
 """.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 len(cmd_args) < 1:

+ 36 - 36
mmgen-txcreate

@@ -37,34 +37,32 @@ help_data = {
 	'usage':   "[opts]  <addr,amt> ... [change addr] [addr file] ...",
 	'options': """
 -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
+-f, --tx-fee=          f  Transaction fee (default: {g.tx_fee} BTC)
 -i, --info                Display unspent outputs and exit
 -q, --quiet               Suppress warnings; overwrite files without
                           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
 via an interactive menu.
 
 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
 of the form <seed ID>:<number>.
 
 To send all inputs (minus TX fee) to a single output, specify one address
 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
-check_opts(opts,long_opts)
 
 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 = {},[],{},[],""
 
-	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:
-			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
 
-	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:
 			a1,a2 = a.split(",")
 			if is_btc_addr(a1):
 				btcaddr = 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:
 				msg("%s: unrecognized subargument in argument '%s'" % (a1,a))
 				sys.exit(2)
 
 			if is_btc_amt(a2):
-				tx_out[btcaddr] = check_btc_amt(a2)
+				tx_out[btcaddr] = normalize_btc_amt(a2)
 			else:
 				msg("%s: invalid amount in argument '%s'" % (a2,a))
 				sys.exit(2)
@@ -108,7 +114,7 @@ if not 'info' in opts:
 						(change_addr, a))
 				sys.exit(2)
 			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
 		else:
 			msg("%s: unrecognized argument" % a)
@@ -119,7 +125,7 @@ if not 'info' in opts:
 		sys.exit(2)
 
 	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:
 		msg("Transaction fee too large: %s > %s" % (tx_fee,g.max_tx_fee))
 		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)
 
-# 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)
 
 total = trim_exponent(sum([i.amount for i in unspent]))
@@ -209,6 +209,6 @@ if reply and reply in "YyVv":
 
 prompt = "Save transaction?"
 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:
 	msg("Transaction not saved")

+ 17 - 28
mmgen-txsend

@@ -35,46 +35,37 @@ help_data = {
 	'usage':   "[opts] <signed transaction file>",
 	'options': """
 -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
 """
 }
 
-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
-check_opts(opts,long_opts)
 
 if len(cmd_args) == 1:
-	infile = cmd_args[0]
-	check_infile(infile)
+	infile = cmd_args[0]; check_infile(infile)
 else: usage(help_data)
 
 # 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)
 
+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!"
 what   = "broadcast this transaction to the network"
 expect =  "YES, I REALLY WANT TO DO THIS"
@@ -85,14 +76,12 @@ confirm_or_exit(warn, what, expect)
 
 msg("Sending transaction")
 
-c = connect_to_bitcoind()
-
 try:
-	tx = c.sendrawtransaction(tx_sig)
+	tx_id = c.sendrawtransaction(tx_hex)
 except:
 	msg("Unable to send transaction")
 	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]...",
 	'options': """
 -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
 -i, --info               Display information about the transaction 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
                          prompting
 -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
                          hash preset 'p'
 -w, --use-wallet-dat     Get keys from a running bitcoind
 -g, --from-incog         Generate keys from an incognito 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'
 -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.
 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.
 
 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 'from_incog_hex' in opts or 'from_incog_hidden' in opts:
 	opts['from_incog'] = True
 
-check_opts(opts,long_opts)
-
 if not infiles: usage(help_data)
 for i in infiles: check_infile(i)
 
 c = connect_to_bitcoind()
 
-seeds = {}
+saved_seeds = {}
 tx_files = [i for i in set(infiles) if get_extension(i) == g.rawtx_ext]
 infiles  = list(set(infiles) - set(tx_files))
 
@@ -139,9 +131,10 @@ for tx_file in tx_files:
 		preverify_keys(a, keys)
 		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)
 
 	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:
 		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:
 			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']:
 		prompt = "OK\nSave signed transaction?"
 		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:
 		msg("failed\nSome keys were missing.  Transaction could not be signed.")
 		sys.exit(3)

+ 7 - 15
mmgen-walletchk

@@ -27,42 +27,34 @@ from mmgen.util import *
 
 help_data = {
 	'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]",
 	'options': """
 -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
--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
 -S, --stdout           Print seed or mnemonic data to standard output
 -v, --verbose          Produce more verbose output
 -g, --export-incog     Export wallet to incognito 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)
 -m, --export-mnemonic  Export the wallet's mnemonic 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 'verbose' in opts: g.verbose = True
 if 'export_incog_hidden' in opts or 'export_incog_hex' in opts:
 	opts['export_incog'] = True
 
-# Argument sanity checks and processing:
-check_opts(opts,long_opts)
-
 if len(cmd_args) != 1: usage(help_data)
 
 check_infile(cmd_args[0])

+ 17 - 28
mmgen-walletgen

@@ -31,35 +31,37 @@ prog_name = sys.argv[0].split("/")[-1]
 
 help_data = {
 	'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]",
 	'options': """
 -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
 -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, " ", "_", ".")
--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
                            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
 
--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
                            hash preset 'p' (comma-separated)
 -g, --from-incog           Generate wallet from an incognito-format wallet
 -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),
-{} 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>
 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
 addresses, the same 'l' and 'p' parameters to '--from-brain' must be used
 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 'verbose' in opts: g.verbose = True
 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 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'])
 	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):
 	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    "
 	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):
 
 	progname = argv[0].split("/")[-1]
 
+	if len(argv) == 2 and argv[1] == '--version': # MMGen only!
+		print_version_info(progname); sys.exit()
+
 	if g.debug:
 		print "Short opts: %s" % repr(short_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
 
 
+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):
 	print "Processed options:     %s" % repr(opts)
 	print "Cmd args:              %s" % repr(cmd_args)
@@ -85,51 +115,41 @@ def check_opts(opts,long_opts):
 		if i+"=" in long_opts:
 			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("_","-")
 
 		# 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':
 			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':
-			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" %
 					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':
 			try:
 				if opt == 'export_incog_hidden':
@@ -138,87 +158,89 @@ def check_opts(opts,long_opts):
 					outfile,offset,seed_len = val.split(",")
 			except:
 				msg("'%s': invalid %s" % (val,what))
-				sys.exit(1)
+				return False
 
 			try:
 				o = int(offset)
 			except:
 				msg("'%s': invalid 'o' %s (not an integer)" % (offset,what))
-				sys.exit(1)
+				return False
 
 			if o < 0:
 				msg("'%s': invalid 'o' %s (less than zero)" % (offset,what))
-				sys.exit(1)
+				return False
 
 			if opt == 'from_incog_hidden':
 				try:
 					sl = int(seed_len)
 				except:
 					msg("'%s': invalid 'l' %s (not an integer)" % (sl,what))
-					sys.exit(1)
+					return False
 
 				if sl not in g.seed_lens:
 					msg("'%s': invalid 'l' %s (valid choices: %s)" %
 						(sl,what," ".join(str(i) for i in g.seed_lens)))
-					sys.exit(1)
+					return False
 
 			import os, stat
 			try: mode = os.stat(outfile).st_mode
 			except:
 				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)):
 				msg("Requested %s '%s' is not a file or block device" %
 						(what,outfile))
-				sys.exit(1)
+				return False
 
 			ac,m = (os.W_OK,"writ") \
 				if "export_incog_hidden" in opts else (os.R_OK,"read")
 			if not os.access(outfile, ac):
 				msg("Requested %s '%s' is un%sable by you" % (what,outfile,m))
-				sys.exit(1)
+				return False
 
 		elif opt == 'from_brain':
 			try:
 				l,p = val.split(",")
 			except:
 				msg("'%s': invalid %s" % (val,what))
-				sys.exit(2)
+				return False
 
 			try:
 				int(l)
 			except:
 				msg("'%s': invalid 'l' %s (not an integer)" % (l,what))
-				sys.exit(1)
+				return False
 
 			if int(l) not in g.seed_lens:
 				msg("'%s': invalid 'l' %s.  Options: %s" %
 						(l, what, ", ".join([str(i) for i in g.seed_lens])))
-				sys.exit(1)
+				return False
 
 			if p not in g.hash_presets:
 				hps = ", ".join([i for i in sorted(g.hash_presets.keys())])
 				msg("'%s': invalid 'p' %s.  Options: %s" % (p, what, hps))
-				sys.exit(1)
+				return False
 		elif opt == 'seed_len':
 			if val not in g.seed_lens:
 				msg("'%s': invalid %s.  Options: %s"
 				% (val,what,", ".join([str(i) for i in g.seed_lens])))
-				sys.exit(2)
+				return False
 		elif opt == 'hash_preset':
 			if val not in g.hash_presets:
 				msg("'%s': invalid %s.  Options: %s"
 				% (val,what,", ".join(sorted(g.hash_presets.keys()))))
-				sys.exit(2)
+				return False
 		elif opt == 'usr_randlen':
 			if val > g.max_randlen or val < g.min_randlen:
 				msg("'%s': invalid %s (must be >= %s and <= %s)"
 				% (val,what,g.min_randlen,g.max_randlen))
-				sys.exit(2)
+				return False
 		else:
 			if g.debug: print "check_opts(): No test for opt '%s'" % opt
 
+	return True
+
 
 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
 from hashlib import sha256, sha512
+from hashlib import new as hashlib_new
 from binascii import hexlify, unhexlify
 
 from mmgen.bitcoin import numtowif
 from mmgen.util import msg,qmsg,qmsg_r
 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():
 	"""
 	Test for the presence of 'keyconv' utility on system
@@ -112,22 +125,10 @@ def generate_keys(seed, addrnums):
 	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 = ""
 	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)
 		)
 
-	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 = []
-	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']
 		if 'no_addresses' in opts:
 			if 'b16' in opts:
@@ -179,23 +173,23 @@ def format_addr_data(addrlist, seed_chksum, opts):
 
 def fmt_addr_list(addr_list):
 
+	addr_list = list(set(sorted(addr_list)))
+
 	prev = addr_list[0]
 	ret = prev,
 
 	for i in addr_list[1:]:
-
 		if i == prev + 1:
 			if i == addr_list[-1]: ret += "-", i
 		else:
 			if prev != ret[-1]: ret += "-", prev
 			ret += ",", i
-
 		prev = i
 
 	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
 	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:
 		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"
 	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
 """
+
+author = "Philemon"
+email = "<mmgen-py@yandex.com>"
+Cdates = '2013-2014'
+version = '0.7.4'
+
 quiet,verbose = False,False
 min_screen_width = 80
 
 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"
 seed_ext      = "mmseed"
@@ -59,6 +64,7 @@ keyconv_exec = "keyconv"
 from os import getenv
 debug      = True if getenv("MMGEN_DEBUG") 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
 passwd_max_tries = 5

+ 3 - 3
mmgen/license.py

@@ -21,13 +21,14 @@ license.py:  Show the license
 
 import sys
 from mmgen.util import msg, msg_r, get_char
+import mmgen.config as g
 
 gpl = {
 	'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
   you are welcome to redistribute it under certain conditions.
-""",
+""".format(g=g),
 	'prompt': """
 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):
 
-	import mmgen.config as g
 	if g.quiet or g.no_license: return
 
 	msg(gpl['warning'])

+ 109 - 90
mmgen/tool.py

@@ -23,6 +23,7 @@ import sys
 import mmgen.bitcoin as bitcoin
 
 from mmgen.util import *
+from mmgen.tx import *
 
 commands = {
 #	"keyconv_compare":          ['wif [str]'],
@@ -54,44 +55,47 @@ commands = {
 	"mn_printlist": ['wordlist [str="electrum"]'],
 	"id8":          ['<infile> [str]'],
 	"id6":          ['<infile> [str]'],
-	"listaccounts": ['minconf [int=1]'],
+	"listaddresses": ['minconf [int=1]', 'showempty [bool=False]'],
 	"getbalance":   ['minconf [int=1]'],
+	"viewtx":       ['<MMGen tx file> [str]'],
+	"check_addrfile":  ['<MMGen addr file> [str]']
 }
 
 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):
@@ -170,14 +174,10 @@ def print_convert_results(indata,enc,dec,no_recode=False):
 			msg("WARNING! Recoded number doesn't match input stringwise!")
 
 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):
-	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):
 	enc = bitcoin.b58encode(s)
@@ -264,60 +264,79 @@ def mn_printlist(wordlist="electrum"):
 	l = get_wordlist(wordlist)
 	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
-	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()
-	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):
 	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()
-	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
 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:
@@ -73,6 +97,7 @@ def trim_exponent(n):
 
 
 def is_btc_amt(amt):
+	# amt must be a string!
 
 	from decimal import Decimal
 	try:
@@ -94,12 +119,11 @@ def is_btc_amt(amt):
 
 	return trim_exponent(ret)
 
-def check_btc_amt(amt):
+
+def normalize_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):
@@ -116,37 +140,24 @@ def get_bitcoind_cfg_options(cfg_keys):
 		msg("Don't know where to look for 'bitcoin.conf'")
 		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
 
 
-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()
 	outfile = "tx_%s[%s].%s" % (tx_id,send_amt,g.rawtx_ext)
 	if 'outdir' in opts:
 		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(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)
 
 
-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()
 	outfile = "tx_%s[%s].%s" % (metadata[0],metadata[1],g.sigtx_ext)
 	if 'outdir' in opts:
 		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)
 	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])
 	if 'outdir' in opts:
 		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"
 	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):
 		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_addr(i):  return i.address
 	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
 	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): "
 
 	from copy import deepcopy
-	print_to_file_msg = ""
+	write_to_file_msg = ""
 	msg("")
 
 	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)
 		tx_w = max(11,min(64, cols-addr_w-32))
 		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)
 
 		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
 			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
 
 		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"
 			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)
 				outfile = "listunspent[%s].out" % ",".join(sort_info)
 				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':
 				do_pager("\n".join(out))
 				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):
-
-	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):
@@ -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)
 
 	if pager: do_pager(out)
-	else:     msg("\n"+out)
+	else:     print "\n"+out
 
 
 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)
 		sys.exit(2)
 
 	err_fmt = "Transaction %s is invalid"
 
-	if len(tx_data[0].split()) != 3:
+	if len(metadata.split()) != 3:
 		msg(err_fmt % "metadata")
 		sys.exit(2)
 
-	try: unhexlify(tx_data[1])
+	try: unhexlify(tx_hex)
 	except:
 		msg(err_fmt % "hex data")
 		sys.exit(2)
 	else:
-		if not tx_data:
+		if not tx_hex:
 			msg("Transaction is empty!")
 			sys.exit(2)
 
 	try:
-		inputs_data = eval(tx_data[2])
+		inputs_data = eval(inputs_data)
 	except:
 		msg(err_fmt % "inputs data")
 		sys.exit(2)
-	else:
-		if not inputs_data:
-			msg("Transaction has no inputs!")
-			sys.exit(2)
 
 	try:
-		map_data = eval(tx_data[3])
+		outputs_data = eval(outputs_data)
 	except:
 		msg(err_fmt % "mmgen to btc address map data")
 		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):
 
 	while True:
-		reply = my_raw_input(prompt,allowed_chars="0123456789 -").strip()
+		reply = my_raw_input(prompt).strip()
 
 		if not reply: continue
 
@@ -462,111 +473,61 @@ def select_outputs(unspent,prompt):
 
 		return selected
 
-def is_mmgen_seed(s):
+def is_mmgen_seed_id(s):
 	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
-	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):
-	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):
 	from mmgen.bitcoin import verify_addr
 	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()
 	if not acct_data:
 		for i in c.listaccounts():
 			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'])
 		for i in inputs_data if i['account']
 			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]
 		from copy import deepcopy
 		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)):
 			if a != b:
 				msg("""
@@ -607,6 +568,17 @@ Only ASCII printable characters are permitted.
 			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):
 
 	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])
 	elif 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)
 	else:
-		ret = []
+		addr_data = []
 		for i in lines[1:-1]:
 			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))
 				sys.exit(3)
 
@@ -641,9 +613,11 @@ def parse_addrs_file(f):
 			if len(d) == 3:
 				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)
 
@@ -664,62 +638,46 @@ def sign_transaction(c,tx_hex,sig_data,keys=None):
 
 	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_save = seed_ids[0:]  # deep copy
 	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:
-			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
 
@@ -759,7 +717,7 @@ def preverify_keys(addrs_orig, keys_orig):
 
 	if len(keys) < len(addrs):
 		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)
 
 	import mmgen.bitcoin as b
@@ -803,7 +761,7 @@ def preverify_keys(addrs_orig, keys_orig):
 		if addrs:
 			s = "" if len(addrs) == 1 else "es"
 			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)
 			sys.exit(2)
 

+ 31 - 45
mmgen/util.py

@@ -43,10 +43,10 @@ def vmsg_r(s):
 def bail(): sys.exit(9)
 
 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:
 		if echo:
 			reply = raw_input(prompt)
@@ -61,33 +61,10 @@ def my_raw_input(prompt,echo=True,allowed_chars=""):
 	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):
 	if hash_preset in g.hash_presets:
 		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)
 		sys.exit(3)
 
@@ -126,7 +103,7 @@ cmessages = {
 	'unencrypted_secret_keys': """
 This program generates secret keys from your {} seed, outputting them in
 UNENCRYPTED form.  Generate only the key(s) you need and guard them carefully.
-""".format(g.proj_name_cap),
+""".format(g.proj_name),
 	'brain_warning': """
 ############################## EXPERTS ONLY! ##############################
 
@@ -154,12 +131,10 @@ def confirm_or_exit(message, question, expect="YES"):
 
 	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")
 		sys.exit(2)
 
@@ -196,9 +171,10 @@ def prompt_and_get_char(prompt,chars,enter_ok=False,verbose=False):
 		else: msg_r("\r")
 
 
-def make_chksum_8(s):
+def make_chksum_8(s,sep=False):
 	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):
 	from hashlib import sha256
@@ -222,12 +198,14 @@ def check_infile(f):
 		msg("Requested input file '%s' is unreadable by you" % f)
 		sys.exit(1)
 
+	return True
+
 
 def _validate_addr_num(n):
 
 	try: n = int(n)
 	except:
-		msg("'%s': invalid argument for address" % n)
+		msg("'%s': address must be an integer" % n)
 		return False
 
 	if n < 1:
@@ -321,7 +299,7 @@ def _get_seed_from_brain_passphrase(words,opts):
 def encrypt_seed(seed, key, iv=1):
 	"""
 	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
 	from Crypto.Cipher import AES
@@ -439,6 +417,13 @@ def _display_control_data(label,metadata,hash_preset,salt,enc_seed):
 	): 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):
 	nondiv = 1 if len(s) % 4 else 0
 	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
 	# the filename.
 	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')
 
@@ -653,7 +638,8 @@ def get_lines_from_file(infile,what="",remove_comments=False):
 		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))
 	f = open_file_or_exit(infile,'r')
 	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)))
 		return seed
 	else:
-		msg("Invalid checksum for {} seed".format(g.proj_name_cap))
+		msg("Invalid checksum for {} seed".format(g.proj_name))
 		return False
 
 
@@ -717,7 +703,7 @@ def get_bitcoind_passphrase(prompt,opts):
 def get_seed_from_wallet(
 		infile,
 		opts,
-		prompt="Enter {} wallet passphrase: ".format(g.proj_name_cap),
+		prompt="Enter {} wallet passphrase: ".format(g.proj_name),
 		silent=False
 		):
 
@@ -770,7 +756,7 @@ def get_hidden_incog_data(opts):
 def get_seed_from_incog_wallet(
 		infile,
 		opts,
-		prompt="Enter %s wallet passphrase: " % g.proj_name_cap,
+		prompt="Enter {} wallet passphrase: ".format(g.proj_name),
 		silent=False,
 		hex_input=False
 	):
@@ -806,7 +792,7 @@ def get_seed_from_incog_wallet(
 	msg("Configured hash presets: %s" % " ".join(sorted(g.hash_presets)))
 	while True:
 		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:
 			hp = g.hash_preset; break
 		elif hp in g.hash_presets:
@@ -890,7 +876,7 @@ def _get_words(infile,what,prompt,opts):
 
 def get_seed(infile,opts,silent=False):
 
-	ext = infile.split(".")[-1]
+	ext = get_extension(infile)
 
 	if   ext == g.mn_ext:           source = "mnemonic"
 	elif ext == g.brain_ext:        source = "brainwallet"
@@ -922,7 +908,7 @@ def get_seed(infile,opts,silent=False):
 		if not g.quiet:
 			confirm_or_exit(
 				cmessages['brain_warning'].format(
-					g.proj_name_cap, *_get_from_brain_opt_params(opts)),
+					g.proj_name, *_get_from_brain_opt_params(opts)),
 				"continue")
 		prompt = "Enter brainwallet passphrase: "
 		words = _get_words(infile,"brainwallet data",prompt,opts)

+ 1 - 1
setup.py

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