Browse Source

Version 0.7.0

philemon 11 years ago
parent
commit
08c5b76805
28 changed files with 934 additions and 627 deletions
  1. 12 12
      MANIFEST
  2. 2 2
      README.md
  3. 1 1
      doc/MMGenBuildBitcoindMSWin.md
  4. 2 6
      doc/MMGenGettingStarted.md
  5. 1 1
      doc/MMGenInstallLinux.md
  6. 1 1
      doc/MMGenTextEditors.md
  7. 5 3
      mmgen-addrgen
  8. 11 12
      mmgen-addrimport
  9. 14 11
      mmgen-passchg
  10. 10 16
      mmgen-pywallet
  11. 73 63
      mmgen-txcreate
  12. 3 4
      mmgen-txsend
  13. 49 38
      mmgen-txsign
  14. 8 5
      mmgen-walletchk
  15. 22 13
      mmgen-walletgen
  16. 4 2
      mmgen/Opts.py
  17. 11 25
      mmgen/addr.py
  18. 32 21
      mmgen/bitcoin.py
  19. 12 5
      mmgen/config.py
  20. 4 0
      mmgen/license.py
  21. 1 1
      mmgen/rpc/proxy.py
  22. 191 0
      mmgen/term.py
  23. 48 20
      mmgen/tests/bitcoin.py
  24. 1 1
      mmgen/tests/test.py
  25. 347 197
      mmgen/tx.py
  26. 63 163
      mmgen/util.py
  27. 4 3
      mmgen/walletgen.py
  28. 2 1
      setup.py

+ 12 - 12
MANIFEST

@@ -1,7 +1,7 @@
 # file GENERATED by distutils, do NOT edit
+__init__.py
 mmgen-addrgen
 mmgen-addrimport
-mmgen-keygen
 mmgen-passchg
 mmgen-pywallet
 mmgen-txcreate
@@ -19,8 +19,9 @@ mmgen/license.py
 mmgen/mn_electrum.py
 mmgen/mn_tirosh.py
 mmgen/mnemonic.py
+mmgen/term.py
 mmgen/tx.py
-mmgen/utils.py
+mmgen/util.py
 mmgen/walletgen.py
 mmgen/rpc/__init__.py
 mmgen/rpc/config.py
@@ -29,13 +30,12 @@ mmgen/rpc/data.py
 mmgen/rpc/exceptions.py
 mmgen/rpc/proxy.py
 mmgen/rpc/util.py
-scripts/bitcoind-walletunlock.py
-scripts/deinstall.sh
-tests/addr.py
-tests/bitcoin.py
-tests/mn_electrum.py
-tests/mn_tirosh.py
-tests/mnemonic.py
-tests/test.py
-tests/utils.py
-tests/walletgen.py
+mmgen/tests/__init__.py
+mmgen/tests/addr.py
+mmgen/tests/bitcoin.py
+mmgen/tests/mn_electrum.py
+mmgen/tests/mn_tirosh.py
+mmgen/tests/mnemonic.py
+mmgen/tests/test.py
+mmgen/tests/util.py
+mmgen/tests/walletgen.py

+ 2 - 2
README.md

@@ -13,8 +13,8 @@ addresses can be tracked and spent as well, creating an easy migration path from
 other wallets.
 
 To track address balances, MMGen relies on a bitcoin daemon modified by
-Bitcoin's core developer team to support watch-only addresses.  This feature
-is soon to be included in the mainline Satoshi build.  In the meantime,
+Bitcoin core developers to support watch-only addresses.  This feature
+will one day be included in the mainline Satoshi build.  In the meantime,
 instructions are provided below for compiling the modified bitcoind.  Under
 Linux, this is a trivial task for even a casual user.  Unfortunately, the same
 can't be said for Windows, but the thoroughly tested, step-by-step Windows build

+ 1 - 1
doc/MMGenBuildBitcoindMSWin.md

@@ -66,7 +66,7 @@ time-consuming process of compiling the whole boost package.
 
 #### 5. Build Bitcoind:
 
-Download Sipa's watchonly bitcoind [zip archive][05] (commit a13f1e8 [[check][]])
+Download Sipa's watchonly bitcoind [zip archive][05] (commit #a13f1e8 [[check][]])
 from GitHub and unpack it.  At the MSYS prompt, run:
 
 		$ cd /c/bitcoin-watchonly

+ 2 - 6
doc/MMGenGettingStarted.md

@@ -154,7 +154,7 @@ sending it to the addresses labeled "Storage 1", "Storage 2" and "Storage 3"
 
 To refresh your memory, here are the three addresses in question:
 
-		$ cat my_addrs
+		$ cat my.addrs
 		# My first MMGen addresses
 		89ABCDEF {
 		  1    16bNmyYISiptuvJG3X7MPwiiS4HYvD7ksE  Donations
@@ -175,11 +175,7 @@ the change amount (3.399 BTC in this case) automatically.
 Alternatively, and more conveniently, you can list your three addresses in MMGen
 format:
 
-		$ mmgen-txcreate 89ABCDEF:2,3.3 89ABCDEF:3,3.3 89ABCDEF:4 my_addrs
-
-Note that an MMGen address file containing the requested output addresses must
-be provided on the command line.  Any extra addresses in the file will be
-ignored.
+		$ mmgen-txcreate 89ABCDEF:2,3.3 89ABCDEF:3,3.3 89ABCDEF:4
 
 Now hit ENTER, choose the transaction's input from the list (10 BTC, address
 1F9495H8EJL..., txid 04f97185...,2), and confirm.  If all goes well,

+ 1 - 1
doc/MMGenInstallLinux.md

@@ -58,7 +58,7 @@ the version should be 1.48 or greater):
 		libboost-test-dev
 		libboost-thread-dev
 
-Download the bitcoin-watchonly [zip archive][00] (commit a13f1e8 [[check][]])
+Download the bitcoin-watchonly [zip archive][00] (commit #a13f1e8 [[check][]])
 from GitHub, configure, and build:
 
 		$ unzip watchonly.zip

+ 1 - 1
doc/MMGenTextEditors.md

@@ -2,7 +2,7 @@ MMGen = Multi-Mode GENerator
 ============================
 ##### a Bitcoin cold storage solution for the command line
 
-#### A word on text editors:
+### A word on text editors:
 
 The text file editors that come with Windows, "edit" and "notepad", are
 unsuitable for editing source code for many reasons, but above all because they

+ 5 - 3
mmgen-addrgen

@@ -121,6 +121,8 @@ if invoked_as == "addrgen":
 opts,cmd_args = process_opts(sys.argv,help_data,"".join(short_opts),long_opts)
 
 if 'show_hash_presets' in opts: show_hash_presets()
+if 'quiet' in opts: g.quiet = True
+if 'verbose' in opts: g.verbose = True
 
 opts['gen_what'] = gen_what
 
@@ -143,10 +145,10 @@ addr_list = parse_address_list(addr_list_arg)
 
 if not addr_list: sys.exit(2)
 
-if not 'quiet' in opts: do_license_msg()
+do_license_msg()
 
 # Interact with user:
-if invoked_as == "keygen" and not 'quiet' in opts:
+if invoked_as == "keygen" and not g.quiet:
 	confirm_or_exit(cmessages['unencrypted_secret_keys'], 'continue')
 
 # Generate data:
@@ -163,7 +165,7 @@ addr_data_str = format_addr_data(addr_data, seed_id, opts)
 
 # Output data:
 if 'stdout' in opts:
-	if invoked_as == "keygen" and not 'quiet' in opts:
+	if invoked_as == "keygen" and not g.quiet:
 		confirm = True
 	else: confirm = False
 	write_to_stdout(addr_data_str,"secret keys",confirm)

+ 11 - 12
mmgen-addrimport

@@ -23,9 +23,8 @@ mmgen-addrimport: Import addresses into a bitcoind watching wallet.
 import sys
 from mmgen.Opts   import *
 from mmgen.license import *
-from mmgen.util import check_infile,confirm_or_exit,msg,msg_r,secs_to_hms,get_lines_from_file
+from mmgen.util import *
 from mmgen.tx import connect_to_bitcoind,parse_addrs_file
-from mmgen.bitcoin import verify_addr
 
 help_data = {
 	'prog_name': sys.argv[0].split("/")[-1],
@@ -43,6 +42,7 @@ short_opts = "hl:q"
 long_opts  = "help", "addrlist=", "quiet"
 
 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)")
@@ -57,28 +57,27 @@ else:
 	seed_id,addr_data = "",[]
 
 if 'addrlist' in opts:
-	l = get_lines_from_file(
-			opts['addrlist'],"non-mmgen addresses",remove_comments=True)
+	l = get_lines_from_file(opts['addrlist'],"non-mmgen addresses",
+			remove_comments=True)
 	addr_data += [(None,i) for i in l]
 
-msg_r("Validating addresses...")
+from mmgen.bitcoin import verify_addr
+qmsg_r("Validating addresses...")
 for i in addr_data:
-	if not verify_addr(i[1]):
+	if not verify_addr(i[1],verbose=True):
 		msg("%s: invalid address" % i)
 		sys.exit(2)
-
-msg("OK")
+qmsg("OK")
 
 import mmgen.config as g
 g.http_timeout = 3600
 
 c = connect_to_bitcoind()
 
-if not 'quiet' in opts:
-	message = """
+m = """
 Importing addresses can take a long time (>30 min.) on a low-powered computer.
-	"""
-	confirm_or_exit(message, "continue", expect="YES")
+""".strip()
+confirm_or_exit(m, "continue", expect="YES")
 
 import threading
 import time

+ 14 - 11
mmgen-passchg

@@ -39,6 +39,8 @@ help_data = {
 -p, --hash-preset        p  Change scrypt.hash() parameters to preset 'p'
                             (default: '{}')
 -P, --passwd-file        f  Get new passphrase from file 'f'
+-q, --quiet                 Suppress warnings; overwrite files without
+                            prompting
 -v, --verbose               Produce more verbose output
 
 NOTE: The key ID will change if either the passphrase or hash preset
@@ -46,23 +48,24 @@ NOTE: The key ID will change if either the passphrase or hash preset
 """.format(g.hash_preset)
 }
 
-short_opts = "hd:HkL:p:P:v"
+short_opts = "hd:HkL:p:P:qv"
 long_opts  = "help","outdir=","show_hash_presets","keep_old_passphrase",\
-			  "label=","hash_preset=","passwd_file=","verbose"
+			  "label=","hash_preset=","passwd_file=","quiet","verbose"
 
 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()
 
-check_opts(opts,long_opts)
-
 if len(cmd_args) != 1:
 	msg("One input file must be specified")
 	sys.exit(2)
 infile = cmd_args[0]
 
 # Old key:
-label,metadata,hash_preset,salt,enc_seed = get_data_from_wallet(infile,opts)
+label,metadata,hash_preset,salt,enc_seed = get_data_from_wallet(infile)
 seed_id,key_id = metadata[:2]
 
 # Repeat on incorrect pw entry
@@ -86,7 +89,7 @@ else: opts['label'] = label  # Copy the old label
 
 if 'hash_preset' in opts:
 	if hash_preset != opts['hash_preset']:
-		msg("Hash preset has changed (%s -> %s)" %
+		qmsg("Hash preset has changed (%s -> %s)" %
 			(hash_preset, opts['hash_preset']))
 		changed['preset'] = True
 	else:
@@ -100,14 +103,14 @@ else:
 	new_passwd = get_new_passphrase("new passphrase", opts)
 
 	if new_passwd == passwd:
-		msg("Passphrase is unchanged")
+		qmsg("Passphrase is unchanged")
 	else:
-		msg("Passphrase has changed")
+		qmsg("Passphrase has changed")
 		passwd = new_passwd
 		changed['passwd'] = True
 
 if 'preset' in changed or 'passwd' in changed: # Update key ID, salt
-	msg("Will update salt and key ID")
+	qmsg("Will update salt and key ID")
 
 	from hashlib import sha256
 	from Crypto import Random
@@ -115,9 +118,9 @@ if 'preset' in changed or 'passwd' in changed: # Update key ID, salt
 	salt = sha256(salt + Random.new().read(128)).digest()[:g.salt_len]
 	key = make_key(passwd, salt, opts['hash_preset'])
 	new_key_id = make_chksum_8(key)
-	msg("Key ID changed: %s -> %s" % (key_id,new_key_id))
+	qmsg("Key ID changed: %s -> %s" % (key_id,new_key_id))
 	key_id = new_key_id
-	enc_seed = encrypt_seed(seed, key, opts)
+	enc_seed = encrypt_seed(seed, key)
 elif not 'label' in changed:
 	msg("Data unchanged.  No file will be written")
 	sys.exit(2)

+ 10 - 16
mmgen-pywallet

@@ -45,6 +45,7 @@ mmgen-pywallet: Dump contents of a bitcoind wallet to file
 
 from mmgen.Opts import *
 from mmgen.util import msg
+import mmgen.config as g
 from bsddb.db import *
 import sys, time
 import json
@@ -81,21 +82,18 @@ help_data = {
 -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'
--q, --quiet            Suppress warnings; overwrite files without prompting
 -S, --stdout           Dump to stdout rather than file
 """
 }
 
-short_opts = "hd:ejkaK:P:qS"
+short_opts = "hd:ejkaK:P:S"
 long_opts  = "help","outdir=","echo_passphrase","json","keys","addrs",\
-			  "keysforaddrs=","passwd_file=","quiet","stdout"
+			  "keysforaddrs=","passwd_file=","stdout"
 
 opts,cmd_args = process_opts(sys.argv,help_data,short_opts,long_opts)
-
-from mmgen.util import check_infile
-
 check_opts(opts,long_opts)
 
+from mmgen.util import check_infile
 if len(cmd_args) == 1:
 	check_infile(cmd_args[0])
 else:
@@ -1683,14 +1681,11 @@ elif 'addrs' in opts:
 
 elif 'keysforaddrs' in opts:
 	from mmgen.util import get_lines_from_file
-	usr_addrs = get_lines_from_file(opts['keysforaddrs'],"addresses")
-	for addr in usr_addrs:
-		try:
-			idx = wallet_addrs.index(addr)
-			data.append(json_db['keys'][idx]['sec'])
-		except:
-			msg("WARNING: Address '%s' not found" % addr)
-	data,ext,what = sorted(data),"keys","private keys"
+	usr_addrs = set(get_lines_from_file(opts['keysforaddrs'],"addresses",remove_comments=True))
+	data = [i['sec'] for i in json_db['keys'] if i['addr'] in usr_addrs]
+	ext,what = "keys","private keys"
+	if len(data) < len(usr_addrs):
+		msg("Warning: not all requested keys found")
 
 len_arg = "%s" % len(wallet_addrs) \
    if len(data) == len(wallet_addrs) or ext == "json" \
@@ -1703,8 +1698,7 @@ data = "\n".join(data) + "\n"
 
 # Output data
 if 'stdout' in opts:
-	if 'addrs' in opts or 'quiet' in opts: confirm = False
-	else: confirm = True
+	confirm = False if 'addrs' in opts else True
 	write_to_stdout(data,"secret keys",confirm)
 elif not sys.stdout.isatty():
 	write_to_stdout(data,"secret keys",confirm=False)

+ 73 - 63
mmgen-txcreate

@@ -51,77 +51,82 @@ via an interactive menu.
 Ages of transactions are approximate based on an average block creation
 interval of %s minutes.
 
-Addresses on the command line can be Bitcoin addresses or MMGen
-addresses in the form <seed ID>:<number>
+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)
 }
 
-short_opts = "ha:d:eiqf:"
-long_opts  = "help","addr_file","outdir=","echo_passphrase","info","quiet",\
-				"tx_fee="
+short_opts = "hd:eiqf:"
+long_opts  = "help","outdir=","echo_passphrase","info","quiet","tx_fee="
 
 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)
 
+c = connect_to_bitcoind()
+
 if not 'info' in opts:
 
-	outputs,addr_files,change_addr = [],[],""
+	tx_out,addr_data,b2m_map,acct_data,change_addr = {},[],{},[],""
 
-	for a in cmd_args:
-		if a.split(".")[-1] == g.addrfile_ext:
+	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_files.append(a)
-		elif "," in a:
-			outputs.append(a)
-		else:
+			addr_data.append(parse_addrs_file(a))
+
+	def mm2btc_addr_proc(c,mmadr,acct_data,addr_data,b2m_map):
+		btaddr,label = mmgen_addr_to_walletd(c,mmadr,acct_data)
+		if not btaddr:
+			btaddr,label = mmgen_addr_to_addr_data(mmadr,addr_data)
+		b2m_map[btaddr] = mmadr,label
+		return btaddr
+
+	for a in [i for i in cmd_args if not match_ext(i,g.addrfile_ext)]:
+		if "," in a:
+			a1,a2 = a.split(",")
+			if is_mmgen_addr(a1) or is_btc_addr(a1):
+				if is_mmgen_addr(a1):
+					btaddr = mm2btc_addr_proc(c,a1,acct_data,addr_data,b2m_map)
+				if is_btc_amt(a2):
+					tx_out[btaddr] = check_btc_amt(a2)
+				else:
+					msg("%s: invalid amount in argument '%s'" % (a2,a))
+					sys.exit(2)
+			else:
+				msg("%s: unrecognized subargument in argument '%s'" % (a1,a))
+				sys.exit(2)
+		elif is_mmgen_addr(a) or is_btc_addr(a):
 			if change_addr:
 				msg("More than one change address specified: %s, %s" %
 						(change_addr, a))
 				sys.exit(2)
-			change_addr = a
+			if is_mmgen_addr(a):
+				change_addr = mm2btc_addr_proc(c,a,acct_data,addr_data,b2m_map)
+			else:
+				change_addr = a
+			tx_out[change_addr] = 0
+		else:
+			msg("%s: unrecognized argument" % a)
+			sys.exit(2)
 
-	if not outputs:
+	if not tx_out:
 		msg("At least one output must be specified on the command line")
 		sys.exit(2)
 
-	addr_data = [parse_addrs_file(f) for f in addr_files]
-
 	tx_fee = opts['tx_fee'] if 'tx_fee' in opts else g.tx_fee
-
-	try:
-		tx_fee = Decimal(tx_fee)
-	except:
-		msg("Invalid transaction fee format: %s" % tx_fee)
-		sys.exit(2)
-
+	tx_fee = check_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)
 
-	if change_addr:
-		if ":" in change_addr:
-			change_addr = mmgen_addr_to_btc_addr(change_addr,addr_data)
-		else:
-			check_address(change_addr)
-
-
-	tx_out = make_tx_out(outputs,addr_data)
-
-	for i in tx_out.keys():   check_address(i)
-	for i in tx_out.values(): check_btc_amt(i)
-
-	tx_fee = check_btc_amt(tx_fee)
-
 if g.debug: show_opts_and_cmd_args(opts,cmd_args)
 
-# Begin execution
-c = connect_to_bitcoind()
-
-if not 'quiet' in opts and not 'info' in opts:
-	do_license_msg(immed=True)
+if not 'info' in opts: do_license_msg(immed=True)
 
 # Begin test
 # import mmgen.rpc
@@ -131,10 +136,9 @@ if not 'quiet' in opts and not 'info' in opts:
 us = c.listunspent()
 
 if not us:
-	msg_r("""
+	msg("""
 No spendable outputs found!  Import addresses with balances into your
-watch-only wallet using 'mmgen-addrimport' and then re-run this program.
-""")
+watch-only wallet using 'mmgen-addrimport' and then re-run this program.""")
 	sys.exit(2)
 
 # write_to_file("listunspent.json",repr(us))
@@ -146,8 +150,9 @@ total = trim_exponent(sum([i.amount for i in unspent]))
 msg("Total unspent: %s BTC (%s outputs)" % (total, len(unspent)))
 if 'info' in opts: sys.exit(0)
 
-send_amt = sum(tx_out.values())
-msg("Total amount to spend: %s BTC" % send_amt)
+send_amt = sum([tx_out[i] for i in tx_out.keys()])
+msg("Total amount to spend: %s%s" % (
+		(send_amt or "Unknown")," BTC" if send_amt else ""))
 
 while True:
 	sel_nums = select_outputs(unspent,
@@ -155,17 +160,15 @@ while True:
 	msg("Selected outputs: %s" % " ".join(str(i) for i in sel_nums))
 	sel_unspent = [unspent[i-1] for i in sel_nums]
 
-	lbls = set([verify_mmgen_label(
-				i.account,return_str=True,check_label_len=True)
-					for i in sel_unspent])
-	lbls.discard("")
+	mmaddrs = set([parse_mmgen_label(i.account)[0] for i in sel_unspent])
+	mmaddrs.discard("")
 
-	if lbls and len(lbls) < len(sel_unspent):
-		msg(txmsg['mixed_inputs'] % ", ".join(sorted(lbls)))
+	if mmaddrs and len(mmaddrs) < len(sel_unspent):
+		msg(txmsg['mixed_inputs'] % ", ".join(sorted(mmaddrs)))
 		if not user_confirm("Accept?"):
 			continue
 
-	total_in = trim_exponent(sum([o.amount for o in sel_unspent]))
+	total_in = trim_exponent(sum([i.amount for i in sel_unspent]))
 	change   = trim_exponent(total_in - (send_amt + tx_fee))
 
 	if change >= 0:
@@ -175,28 +178,35 @@ while True:
 	else:
 		msg(txmsg['not_enough_btc'] % change)
 
-
 if change > 0 and not change_addr:
-	msg(txmsg['throwaway_change'] % (change, total_in-tx_fee))
+	msg(txmsg['throwaway_change'] % change)
 	sys.exit(2)
 
+if change_addr in tx_out and not change:
+	msg("Warning: Change address will be unused as transaction produces no change")
+	del tx_out[change_addr]
+
+for k,v in tx_out.items(): tx_out[k] = float(v)
+
+if change > 0: tx_out[change_addr] = float(change)
+
 tx_in = [{"txid":i.txid, "vout":i.vout} for i in sel_unspent]
-for i in tx_out.keys(): tx_out[i] = float(tx_out[i])
-if change: tx_out[change_addr] = float(change)
+
 if g.debug:
 	print "tx_in:", repr(tx_in)
 	print "tx_out:", repr(tx_out)
-tx_hex = c.createrawtransaction(tx_in,tx_out)
 
-msg("Transaction successfully created")
+tx_hex = c.createrawtransaction(tx_in,tx_out)
+qmsg("Transaction successfully created")
 prompt = "View decoded transaction? (y)es, (N)o, (v)iew in pager"
 reply = prompt_and_get_char(prompt,"YyNnVv",enter_ok=True)
+
 if reply and reply in "YyVv":
 	pager = True if reply in "Vv" else False
-	view_tx_data(c,[i.__dict__ for i in sel_unspent],tx_hex,pager=pager)
+	view_tx_data(c,[i.__dict__ for i in sel_unspent],tx_hex,b2m_map,pager=pager)
 
 prompt = "Save transaction?"
 if user_confirm(prompt,default_yes=True):
-	print_tx_to_file(tx_hex,sel_unspent,send_amt,opts)
+	print_tx_to_file(tx_hex,sel_unspent,send_amt or change,b2m_map,opts)
 else:
 	msg("Transaction not saved")

+ 3 - 4
mmgen-txsend

@@ -44,7 +44,7 @@ short_opts = "hd:q"
 long_opts  = "help","outdir=","quiet"
 
 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:
@@ -71,9 +71,9 @@ except:
 	msg("Invalid signed transaction data")
 	sys.exit(3)
 
-if not 'quiet' in opts: do_license_msg()
+do_license_msg()
 
-msg("\nSigned transaction file '%s' appears valid" % infile)
+qmsg("Signed transaction file '%s' is valid" % infile)
 
 warn   = "Once this transaction is sent, there's no taking it back!"
 what   = "broadcast this transaction to the network"
@@ -86,7 +86,6 @@ c = connect_to_bitcoind()
 
 try:
 	tx = c.sendrawtransaction(tx_sig)
-#	tx = "deadbeef"
 except:
 	msg("Unable to send transaction")
 	sys.exit(3)

+ 49 - 38
mmgen-txsign

@@ -25,7 +25,7 @@ from mmgen.Opts import *
 from mmgen.license import *
 import mmgen.config as g
 from mmgen.tx import *
-from mmgen.util import msg
+from mmgen.util import msg,qmsg
 
 help_data = {
 	'prog_name': sys.argv[0].split("/")[-1],
@@ -36,18 +36,19 @@ help_data = {
 -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
+-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'
 -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,
                          i.e. a "brainwallet", using seed length 'l' and
-                         hash preset 'p' (comma-separated)
+                         hash preset 'p'
 -m, --from-mnemonic      Generate keys from an electrum-like mnemonic
 -s, --from-seed          Generate keys from a seed in .{} format
--w, --use-wallet-dat     Use the keys in the bitcoind wallet.dat file too
+-w, --use-wallet-dat     Get keys from a running bitcoind
 
 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.
@@ -60,9 +61,13 @@ prompted to enter the data.
 
 In cases of transactions with mixed mmgen and non-mmgen inputs, non-mmgen
 keys must be supplied in a separate file (WIF format, one key per line)
-using the '--keys-from-file' option.  Alternatively, one may import the
-required mmgen keys into the bitcoind wallet.dat and use the
-'--force-wallet-dat' option.
+using the '--keys-from-file' option.  Alternatively, one may get keys from
+a running bitcoind using the '--force-wallet-dat' option.  First import the
+required mmgen keys using 'bitcoind importprivkey'.
+
+For transaction outputs that are MMGen addresses, MMGen-to-Bitcoin address
+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:      '.{}'
@@ -72,78 +77,84 @@ Seed data supplied in files must have the following extensions:
 """.format(g.seed_ext,g.wallet_ext,g.seed_ext,g.mn_ext,g.brain_ext)
 }
 
-short_opts = "hd:eiIk:P:qb:msw"
+short_opts = "hd:eiIk:P:qVb:msw"
 long_opts  = "help","outdir=","echo_passphrase","info","tx_id",\
-			  "keys_from_file=","passwd_file=","quiet","from_brain=",\
-			  "from_mnemonic","from_seed","use_wallet_dat"
+			"keys_from_file=","passwd_file=","quiet","skip_key_preverify",\
+			"from_brain=","from_mnemonic","from_seed","use_wallet_dat"
 
 opts,infiles = process_opts(sys.argv,help_data,short_opts,long_opts)
-
+if "quiet" in opts: g.quiet = True
 check_opts(opts,long_opts)
 
 if not infiles: usage(help_data)
 for i in infiles: check_infile(i)
 
-# Begin execution
 c = connect_to_bitcoind()
 
 tx_file = infiles.pop(0)
 m = "" if 'tx_id' in opts else "transaction data"
 tx_data = get_lines_from_file(tx_file,m)
 
-metadata,tx_hex,sig_data,inputs_data = parse_tx_data(tx_data,tx_file)
+metadata,tx_hex,inputs_data,b2m_map = parse_tx_data(tx_data,tx_file)
 
 if 'tx_id' in opts:
 	msg(metadata[0])
 	sys.exit(0)
 
 if 'info' in opts:
-	view_tx_data(c,inputs_data,tx_hex,metadata)
+	view_tx_data(c,inputs_data,tx_hex,b2m_map,metadata)
 	sys.exit(0)
 
-if not 'quiet' in opts: do_license_msg(immed=True)
+do_license_msg(immed=True)
+
+# Are inputs mmgen addresses?
+mmgen_addrs = [i for i in inputs_data if parse_mmgen_label(i['account'])[0]]
+other_addrs = [i for i in inputs_data if not parse_mmgen_label(i['account'])[0]]
+
+keys = get_lines_from_file(opts['keys_from_file'],"key data",remove_comments=True) \
+	if 'keys_from_file' in opts else []
+
+if other_addrs and not keys and not 'use_wallet_dat' in opts:
+	missing_keys_errormsg(other_addrs)
+	sys.exit(2)
+
+if other_addrs and keys and not 'skip_key_preverify' in opts:
+	a = [i['address'] for i in other_addrs]
+	preverify_keys(a, keys)
+
+seeds = {}
+check_mmgen_to_btc_addr_mappings(inputs_data,b2m_map,infiles,seeds,opts)
 
-msg("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"
 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,metadata,pager=p)
+	view_tx_data(c,inputs_data,tx_hex,b2m_map,metadata,pager=p)
 
-# Are inputs mmgen addresses?
-mmgen_addrs = [i for i in inputs_data if verify_mmgen_label(i['account'])]
-other_addrs = [i for i in inputs_data if not verify_mmgen_label(i['account'])]
-
-keys = get_lines_from_file(opts['keys_from_file'],"key data") \
-	if 'keys_from_file' in opts else []
+sig_data = [
+	{"txid":i['txid'],"vout":i['vout'],"scriptPubKey":i['scriptPubKey']}
+		for i in inputs_data]
 
 if mmgen_addrs:
-	if other_addrs and not keys and not 'use_wallet_dat' in opts:
-		missing_keys_errormsg(other_addrs)
-		sys.exit(2)
-
-	keys += get_keys_for_mmgen_addrs(mmgen_addrs,infiles,opts)
+	ml = [i['account'].split()[0] for i in mmgen_addrs]
+	keys += get_keys_for_mmgen_addrs(ml,infiles,seeds,opts)
 
 	if 'use_wallet_dat' in opts:
 		sig_tx = sign_tx_with_bitcoind_wallet(c,tx_hex,sig_data,keys,opts)
 	else:
 		sig_tx = sign_transaction(c,tx_hex,sig_data,keys)
 elif other_addrs:
-	if 'use_wallet_dat' in opts:
-		sig_tx = sign_tx_with_bitcoind_wallet(c,tx_hex,sig_data,keys,opts)
+	if keys:
+		sig_tx = sign_transaction(c,tx_hex,sig_data,keys)
 	else:
-		if keys:
-			sig_tx = sign_transaction(c,tx_hex,sig_data,keys)
-		else:
-			missing_keys_errormsg(other_addrs)
-			sys.exit(2)
+		sig_tx = sign_tx_with_bitcoind_wallet(c,tx_hex,sig_data,keys,opts)
 
 if sig_tx['complete']:
-	msg("Signing completed")
-	prompt = "Save signed transaction?"
+	prompt = "OK\nSave signed transaction?"
 	if user_confirm(prompt,default_yes=True):
 		print_signed_tx_to_file(tx_hex,sig_tx['hex'],metadata,opts)
 else:
-	msg("Some keys were missing.  Transaction could not be signed.")
+	msg("failed\nSome keys were missing.  Transaction could not be signed.")
 	sys.exit(3)

+ 8 - 5
mmgen-walletchk

@@ -37,17 +37,20 @@ help_data = {
 -e, --echo-passphrase  Print passphrase to screen when typing it
 -m, --export-mnemonic  Export the wallet's mnemonic to file
 -P, --passwd-file   f  Get passphrase from file 'f'
+-q, --quiet            Suppress warnings; overwrite files without prompting
 -s, --export-seed      Export the wallet's seed to file
 -S, --stdout           Print seed or mnemonic data to standard output
 -v, --verbose          Produce more verbose output
 """
 }
 
-short_opts = "hd:emP:sSv"
+short_opts = "hd:emP:qsSv"
 long_opts  = "help","outdir=","echo_passphrase","export_mnemonic",\
-				"passwd_file=","export_seed","stdout","verbose"
+				"passwd_file=","quiet","export_seed","stdout","verbose"
 
 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
 
 # Argument sanity checks and processing:
 check_opts(opts,long_opts)
@@ -57,12 +60,12 @@ if len(cmd_args) != 1: usage(help_data)
 check_infile(cmd_args[0])
 
 if 'export_mnemonic' in opts:
-	msg("Exporting mnemonic data to file by user request")
+	qmsg("Exporting mnemonic data to file by user request")
 if 'export_seed' in opts:
-	msg("Exporting seed data to file by user request")
+	qmsg("Exporting seed data to file by user request")
 
 seed = get_seed_from_wallet(cmd_args[0], opts)
-if seed: msg("Wallet is OK")
+if seed: qmsg("Wallet is OK")
 
 if 'export_mnemonic' in opts:
 	wl = get_default_wordlist()

+ 22 - 13
mmgen-walletgen

@@ -45,10 +45,11 @@ help_data = {
 -p, --hash-preset       p  Use scrypt.hash() parameters from preset 'p'
                            (default: '{}')
 -P, --passwd-file       f  Get passphrase from file 'f'
--q, --quiet                Suppress warnings; overwrite files without
+-q, --quiet                Produce quieter output; overwrite files without
                            prompting
 -u, --usr-randlen       n  Get 'n' characters of randomness from the user
                            (default: {})
+-v, --verbose              Produce more verbose output
 
 -b, --from-brain       l,p Generate wallet from a user-created passphrase,
                            i.e. a "brainwallet", using seed length 'l' and
@@ -92,13 +93,14 @@ in all future invocations with that passphrase.
 	)
 }
 
-short_opts = "hd:eHl:L:p:P:qu:b:ms"
+short_opts = "hd:eHl:L:p:P:qu:vb:ms"
 long_opts  = "help","outdir=","echo_passphrase","show_hash_presets","seed_len=",\
 			"label=","hash_preset=","passwd_file=","quiet","usr_randlen=",\
-			"from_brain=","from_mnemonic","from_seed"
+		    "verbose","from_brain=","from_mnemonic","from_seed"
 
 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)
@@ -108,15 +110,23 @@ if g.debug: show_opts_and_cmd_args(opts,cmd_args)
 if len(cmd_args) == 1:
 	infile = cmd_args[0]
 	check_infile(infile)
+	ext = infile.split(".")[-1]
+	ok_exts = g.seed_ext, g.mn_ext, g.brain_ext
+	for e in ok_exts:
+		if e == ext: break
+	else:
+		msg(
+"Input file must have one of the following extensions: .%s" % ", .".join(ok_exts))
+		sys.exit(1)
 elif len(cmd_args) == 0:
 	infile = ""
 else: usage(help_data)
 
 # Begin execution
 
-if not 'quiet' in opts: do_license_msg()
+do_license_msg()
 
-msg_r("Acquiring random data from your computer...")
+qmsg_r("Acquiring random data from your computer...")
 
 from time import sleep
 sleep(0.25)
@@ -129,12 +139,12 @@ except:
 	msg("FAILED\nUnable to generate random numbers. Exiting")
 	sys.exit(2)
 
-msg("OK")
+qmsg("OK")
 
 if g.debug: display_os_random_data(os_rand_data)
 
 usr_keys,key_timings = get_random_data_from_user(opts)
-msg("")
+qmsg("")
 
 if g.debug: display_user_random_data(usr_keys,key_timings)
 
@@ -143,7 +153,8 @@ usr_rand_data = sha256(usr_keys).digest() + \
 
 for i in 'from_mnemonic','from_brain','from_seed':
 	if infile or (i in opts):
-		seed = get_seed_retry(infile,opts); break
+		seed = get_seed_retry(infile,opts)
+		qmsg(""); break
 else:
 	# Truncate random data for smaller seed lengths
 	seed = os_rand_data[0] + usr_rand_data
@@ -152,9 +163,7 @@ else:
 salt = os_rand_data[1] + usr_rand_data
 salt = sha256(salt).digest()[:g.salt_len]
 
-if not 'quiet' in opts:
-	msg("""
-Now you must choose a passphrase to encrypt the seed with.  A key will be
+qmsg("""Now you must choose a passphrase to encrypt the seed with.  A key will be
 generated from your passphrase using a hash preset of '%s'.  Please note that
 no strength checking of passphrases is performed.  For an empty passphrase,
 just hit ENTER twice.
@@ -164,6 +173,6 @@ passwd = get_new_passphrase("{} wallet passphrase".format(g.proj_name), opts)
 
 key = make_key(passwd, salt, opts['hash_preset'])
 
-enc_seed = encrypt_seed(seed, key, opts)
+enc_seed = encrypt_seed(seed, key)
 
 write_wallet_to_file(seed,passwd,make_chksum_8(key),salt,enc_seed,opts)

+ 4 - 2
mmgen/Opts.py

@@ -18,7 +18,7 @@
 
 import sys, getopt
 import mmgen.config as g
-from mmgen.util import msg
+from mmgen.util import msg,check_infile
 
 def usage(hd):
 	print "USAGE: %s %s" % (hd['prog_name'], hd['usage'])
@@ -117,7 +117,9 @@ def check_opts(opts,long_opts):
 
 			for ch in list(label):
 				if ch not in g.wallet_label_symbols:
-					msg("'%s': illegal character in label" % ch)
+					msg("""
+"%s": illegal character in label.  Only ASCII characters are permitted.
+""".strip() % ch)
 					sys.exit(1)
 		elif opt == 'from_brain':
 			try:

+ 11 - 25
mmgen/addr.py

@@ -24,6 +24,7 @@ from hashlib import sha256, sha512
 from binascii import hexlify, unhexlify
 
 from mmgen.bitcoin import numtowif
+from mmgen.util import msg,qmsg,qmsg_r
 import mmgen.config as g
 
 def test_for_keyconv():
@@ -47,27 +48,12 @@ faster address generation.
 
 
 def generate_addrs(seed, addrnums, opts):
-	"""
-	generate_addresses(start, end, seed, opts)  => None
-
-	Generate a Bitcoin address or addresses end based on a seed, optionally
-	outputting secret keys
-
-	The 'keyconv' utility will be used for address generation if installed.
-	Otherwise an internal function is used
-
-	Supported options:
-		print_secret, no_addresses, no_keyconv, gen_what
-
-	Addresses are returned in a list of dictionaries with the following keys:
-		num, sec, wif, addr
-	"""
 
 	if not 'no_addresses' in opts:
 		if 'no_keyconv' in opts or test_for_keyconv() == False:
-			sys.stderr.write("Using (slow) internal ECDSA library for address generation\n")
+			msg("Using (slow) internal ECDSA library for address generation")
 			from mmgen.bitcoin import privnum2addr
-			keyconv = ""
+			keyconv = False
 		else:
 			from subprocess import Popen, PIPE
 			keyconv = "keyconv"
@@ -75,12 +61,13 @@ def generate_addrs(seed, addrnums, opts):
 	a,t_addrs,i,out = sorted(addrnums),len(addrnums),0,[]
 
 	while a:
-		seed = sha512(seed).digest(); i += 1   # round /i/
+		seed = sha512(seed).digest()
+		i += 1 # round /i/
 		if i < a[0]: continue
 
 		a.pop(0)
 
-		sys.stderr.write("\rGenerating %s %s (%s of %s)" %
+		qmsg_r("\rGenerating %s %s (%s of %s)" %
 			(opts['gen_what'], i, t_addrs-len(a), t_addrs))
 
 		# Secret key is double sha256 of seed hash round /i/
@@ -110,7 +97,7 @@ def generate_addrs(seed, addrnums, opts):
 		import re
 		w = re.sub('e*s$','',w)
 
-	sys.stderr.write("\rGenerated %s %s%s\n"%(t_addrs, w, " "*15))
+	qmsg("\rGenerated %s %s%s"%(t_addrs, w, " "*15))
 
 	return out
 
@@ -153,10 +140,9 @@ def format_addr_data(addrlist, seed_chksum, opts):
 # 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 ASCII letters, numerals, and the symbols
-# '{}' and '{}'.
-""".format(g.proj_name.capitalize(),g.max_addr_label_len,
-		"', '".join(g.addr_label_punc[0:-1]), g.addr_label_punc[-1]).strip()
+# The label may contain printable ASCII symbols.
+""".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())
@@ -210,7 +196,7 @@ def write_addr_data_to_file(seed, data, addr_list, opts):
 	else:                              ext = "akeys"
 
 	if 'b16' in opts: ext = ext.replace("keys","xkeys")
-	from mmgen.util import write_to_file, make_chksum_8, msg
+	from mmgen.util import write_to_file, make_chksum_8
 	outfile = "{}[{}].{}".format(
 			make_chksum_8(seed),
 			fmt_addr_list(addr_list),

+ 32 - 21
mmgen/bitcoin.py

@@ -1,6 +1,6 @@
 #!/usr/bin/env python
 #
-# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
+# MMGen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
 # Copyright (C) 2013 by philemon <mmgen-py@yandex.com>
 #
 # This program is free software: you can redistribute it and/or modify
@@ -61,15 +61,10 @@ def pubhex2addr(pubhex):
 	pubkey = int(step2 + step4[:8], 16)
 	return "1" + ("1" * extra_ones) + _numtob58(pubkey)
 
-def privnum2addr(numpriv):
-	pko = ecdsa.SigningKey.from_secret_exponent(numpriv,secp256k1)
-	pubkey = hexlify(pko.get_verifying_key().to_string())
-	return pubhex2addr('04'+pubkey)
-
-def verify_addr(addr):
+def verify_addr(addr,verbose=False):
 
 	if addr[0] != "1":
-		print "%s: Invalid address" % addr
+		if verbose: print "%s: Invalid address" % addr
 		return False
 
   	num = _b58tonum(addr[1:])
@@ -80,7 +75,7 @@ def verify_addr(addr):
 	step2 = sha256(step1).hexdigest()
 
 	if step2[:8] != addr_hex[40:]:
-		print "Invalid checksum in address %s" % ("1" + addr)
+		if verbose: print "Invalid checksum in address %s" % ("1" + addr)
 		return False
 
 	return True
@@ -115,8 +110,7 @@ def numtowif(numpriv):
 	key = step1 + step3[:8]
 	return _numtob58(int(key,16))
 
-
-# The following are mmgen internal (non-bitcoin) b58 functions
+# The following are MMGen internal (non-Bitcoin) b58 functions
 
 # Drop-in replacements for b64encode() and b64decode():
 # (well, not exactly: they yield numeric but not bytewise equivalence)
@@ -159,19 +153,36 @@ def b58decode_pad(s):
 	return _b58_pad(s,
 		a=b58_lens,b=bin_lens,pad='\0',f=b58decode,w="base 58 numbers")
 
+# Compressed address support:
 
-################### FUNCTIONS UNUSED BY MMGEN: ###################
-
-# To check validity, recode with numtowif()
-def wiftonum(wifpriv):
+def wiftohex(wifpriv,compressed=False):
+	idx = 68 if compressed else 66
 	num = _b58tonum(wifpriv)
 	if num == False: return False
-	return (num % (1<<288)) >> 32
+	key = hex(num)[2:].rstrip('L')
+	if compressed and key[66:68] != '01': return False
+	round1 = sha256(unhexlify(key[:idx])).digest()
+	round2 = sha256(round1).hexdigest()
+	return key[2:66] if (key[:2] == '80' and key[idx:] == round2[:8]) else False
 
-def wiftohex(wifpriv):
+def hextowif(hexpriv,compressed=False):
+	step1 = '80' + hexpriv + ('01' if compressed else '')
+	step2 = sha256(unhexlify(step1)).digest()
+	step3 = sha256(step2).hexdigest()
+	key = step1 + step3[:8]
+	return _numtob58(int(key,16))
+
+def privnum2addr(numpriv,compressed=False):
+	pko = ecdsa.SigningKey.from_secret_exponent(numpriv,secp256k1)
+	pubkey = hexlify(pko.get_verifying_key().to_string())
+	if compressed:
+		p = '03' if pubkey[-1] in "13579bdf" else '02'
+		return pubhex2addr(p+pubkey[:64])
+	else:
+		return pubhex2addr('04'+pubkey)
+
+# Used only in test suite.  To check validity, recode with numtowif()
+def wiftonum(wifpriv):
 	num = _b58tonum(wifpriv)
 	if num == False: return False
-	key = hex(num)[2:].rstrip('L')
-	round1 = sha256(unhexlify(key[:66])).digest()
-	round2 = sha256(round1).hexdigest()
-	return key[2:66] if (key[:2] == '80' and key[66:] == round2[:8]) else False
+	return (num % (1<<288)) >> 32

+ 12 - 5
mmgen/config.py

@@ -18,12 +18,15 @@
 """
 config.py:  Constants and configuration options for the mmgen suite
 """
+quiet,verbose = False,False
+min_screen_width = 80
 
 from decimal import Decimal
 tx_fee        = Decimal("0.001")
 max_tx_fee    = Decimal("0.1")
 
 proj_name     = "mmgen"
+proj_name_cap = "MMGen"
 
 wallet_ext    = "mmdat"
 seed_ext      = "mmseed"
@@ -71,12 +74,16 @@ hash_presets = {
 	'6': [17, 8, 20],
 }
 
+mmgen_idx_max_digits = 7
+
 from string import ascii_letters, digits
 
-addr_label_punc = ".","_",",","-"," "
-addr_label_symbols = tuple(ascii_letters + digits) + addr_label_punc
-max_addr_label_len = 16
+addr_label_symbols = tuple([chr(i) for i in range(0x20,0x7f)])
+max_addr_label_len = 32
 
-wallet_label_punc = ".", "_", " "
-wallet_label_symbols = tuple(ascii_letters + digits) + wallet_label_punc
+wallet_label_symbols = addr_label_symbols
 max_wallet_label_len = 32
+
+#addr_label_punc = ".","_",",","-"," ","(",")"
+#addr_label_symbols = tuple(ascii_letters + digits) + addr_label_punc
+#wallet_label_punc = addr_label_punc

+ 4 - 0
mmgen/license.py

@@ -585,6 +585,10 @@ copy of the Program in return for a fee.
 }
 
 def do_license_msg(immed=False):
+
+	import mmgen.config as g
+	if g.quiet: return
+
 	msg(gpl['warning'])
 	prompt = "%s " % gpl['prompt'].strip()
 

+ 1 - 1
mmgen/rpc/proxy.py

@@ -106,7 +106,7 @@ class AuthServiceProxy(object):
 		except:
 			from mmgen.util import msg
 			import sys
-			msg("\nUnable to connect to bitcoind.")
+			msg("Unable to connect to bitcoind")
 			sys.exit(2)
 
 		httpresp = self.__conn.getresponse()

+ 191 - 0
mmgen/term.py

@@ -0,0 +1,191 @@
+#!/usr/bin/env python
+#
+# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
+# Copyright (C) 2013 by philemon <mmgen-py@yandex.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+"""
+term.py:  Terminal-handling routines for the mmgen suite
+"""
+
+import sys, os, struct
+
+def msg(s):   sys.stderr.write(s + "\n")
+def msg_r(s): sys.stderr.write(s)
+
+def _kb_hold_protect_unix():
+
+	fd = sys.stdin.fileno()
+	old = termios.tcgetattr(fd)
+	tty.setcbreak(fd)
+
+	timeout = float(0.3)
+
+	try:
+		while True:
+			key = select([sys.stdin], [], [], timeout)[0]
+			if key: sys.stdin.read(1)
+			else: break
+	except KeyboardInterrupt:
+		msg("\nUser interrupt")
+		sys.exit(1)
+	finally:
+		termios.tcsetattr(fd, termios.TCSADRAIN, old)
+
+
+def _get_keypress_unix(prompt="",immed_chars=""):
+
+	msg_r(prompt)
+	timeout = float(0.3)
+
+	fd = sys.stdin.fileno()
+	old = termios.tcgetattr(fd)
+	tty.setcbreak(fd)
+
+	try:
+		while True:
+			select([sys.stdin], [], [], False)
+			ch = sys.stdin.read(1)
+			if immed_chars == "ALL" or ch in immed_chars:
+				return ch
+			if immed_chars == "ALL_EXCEPT_ENTER" and not ch in "\n\r":
+				return ch
+			second_key = select([sys.stdin], [], [], timeout)[0]
+			if second_key: continue
+			else: return ch
+	except KeyboardInterrupt:
+		msg("\nUser interrupt")
+		sys.exit(1)
+	finally:
+		termios.tcsetattr(fd, termios.TCSADRAIN, old)
+
+
+def _kb_hold_protect_mswin():
+
+	timeout = float(0.5)
+
+	try:
+		while True:
+			hit_time = time.time()
+			while True:
+				if msvcrt.kbhit():
+					msvcrt.getch()
+					break
+				if float(time.time() - hit_time) > timeout:
+					return
+	except KeyboardInterrupt:
+		msg("\nUser interrupt")
+		sys.exit(1)
+
+
+def _get_keypress_mswin(prompt="",immed_chars=""):
+
+	msg_r(prompt)
+	timeout = float(0.5)
+
+	try:
+		while True:
+			if msvcrt.kbhit():
+				ch = msvcrt.getch()
+
+				if ord(ch) == 3: raise KeyboardInterrupt
+
+				if immed_chars == "ALL" or ch in immed_chars:
+					return ch
+				if immed_chars == "ALL_EXCEPT_ENTER" and not ch in "\n\r":
+					return ch
+
+				hit_time = time.time()
+
+				while True:
+					if msvcrt.kbhit(): break
+					if float(time.time() - hit_time) > timeout:
+						return ch
+	except KeyboardInterrupt:
+		msg("\nUser interrupt")
+		sys.exit(1)
+
+
+def _get_terminal_size_linux():
+
+	def ioctl_GWINSZ(fd):
+		try:
+			import fcntl
+			import termios
+			cr = struct.unpack('hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, '1234'))
+			return cr
+		except:
+			pass
+
+	cr = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2)
+
+	if not cr:
+		try:
+			fd = os.open(os.ctermid(), os.O_RDONLY)
+			cr = ioctl_GWINSZ(fd)
+			os.close(fd)
+		except:
+			pass
+
+	if not cr:
+		try:
+			cr = (os.environ['LINES'], os.environ['COLUMNS'])
+		except:
+			return 80,25
+
+	return int(cr[1]), int(cr[0])
+
+
+def _get_terminal_size_mswin():
+	try:
+		from ctypes import windll, create_string_buffer
+		# stdin handle is -10
+		# stdout handle is -11
+		# stderr handle is -12
+		h = windll.kernel32.GetStdHandle(-12)
+		csbi = create_string_buffer(22)
+		res = windll.kernel32.GetConsoleScreenBufferInfo(h, csbi)
+		if res:
+			(bufx, bufy, curx, cury, wattr,
+			 left, top, right, bottom,
+			 maxx, maxy) = struct.unpack("hhhhHhhhhhh", csbi.raw)
+			sizex = right - left + 1
+			sizey = bottom - top + 1
+			return sizex, sizey
+	except:
+		return 80,25
+
+try:
+	import tty, termios
+	from select import select
+	get_char = _get_keypress_unix
+	kb_hold_protect = _kb_hold_protect_unix
+	get_terminal_size = _get_terminal_size_linux
+except:
+	try:
+		import msvcrt, time
+		get_char = _get_keypress_mswin
+		kb_hold_protect = _kb_hold_protect_mswin
+		get_terminal_size = _get_terminal_size_mswin
+	except:
+		if not sys.platform.startswith("linux") \
+				and not sys.platform.startswith("win"):
+			msg("Unsupported platform: %s" % sys.platform)
+			msg("This program currently runs only on Linux and Windows")
+		else:
+			msg("Unable to set terminal mode")
+		sys.exit(2)
+
+if __name__ == "__main__":
+	print "columns: {}, rows: {}".format(*get_terminal_size())

+ 48 - 20
mmgen/tests/bitcoin.py

@@ -19,18 +19,19 @@
 bitcoin.py:  Test suite for mmgen.bitcoin module
 """
 
-from mmgen.bitcoin import *
-from mmgen.util import msg
-from test import *
+import mmgen.bitcoin as b
+from   mmgen.util import msg
+from   mmgen.tests.test import *
+from   binascii import hexlify, unhexlify
 
 import sys
 
 def b58_randenc():
 	r = get_random(24)
-	r_enc = b58encode(r)
+	r_enc = b.b58encode(r)
 	print "Data (hex):    %s" % hexlify(r)
 	print "Base 58:       %s" % r_enc
-	r_dec = b58decode(r_enc)
+	r_dec = b.b58decode(r_enc)
 	print "Decoded data:  %s" % hexlify(r_dec)
 	if r_dec != r:
 		print "ERROR!  Decoded data doesn't match original"
@@ -55,9 +56,8 @@ def keyconv_compare_randloop(loops, quiet=False):
 		else:
 			print "%s iterations completed" % i
 
-	except:
-		print "\nUser interrupt"
-
+	except KeyboardInterrupt:
+		msg("\nUser interrupt")
 
 def keyconv_compare(wif,quiet=False):
 	do_msg = nomsg if quiet else msg
@@ -69,7 +69,7 @@ def keyconv_compare(wif,quiet=False):
 		print "Error with execution of keyconv"
 		sys.exit(3)
 	kc_addr = dict([j.split() for j in p.stdout.readlines()])['Address:']
-	addr = privnum2addr(wiftonum(wif))
+	addr = b.privnum2addr(b.wiftonum(wif))
 	do_msg("Address (mmgen):   %s" % addr)
 	do_msg("Address (keyconv): %s" % kc_addr)
 	if (kc_addr != addr):
@@ -113,13 +113,13 @@ def numtowif_rand(quiet=False):
 
 def strtob58(s,quiet=False):
 	print "Input:         %s" % s
-	s_enc = b58encode(s)
+	s_enc = b.b58encode(s)
 	print "Encoded data:  %s" % s_enc
-	s_dec = b58decode(s_enc)
+	s_dec = b.b58decode(s_enc)
 	print "Decoded data:  %s" % s_dec
 	test_equality(s,s_dec,[""],quiet)
 
-def hextob58(s_in,f_enc=b58encode, f_dec=b58decode, quiet=False):
+def hextob58(s_in,f_enc=b.b58encode, f_dec=b.b58decode, quiet=False):
 	do_msg = nomsg if quiet else msg
 	do_msg("Input:         %s" % s_in)
 	s_bin = unhexlify(s_in)
@@ -129,7 +129,7 @@ def hextob58(s_in,f_enc=b58encode, f_dec=b58decode, quiet=False):
 	do_msg("Recoded data:  %s" % s_dec)
 	test_equality(s_in,s_dec,["0"],quiet)
 
-def b58tohex(s_in,f_dec=b58decode, f_enc=b58encode,quiet=False):
+def b58tohex(s_in,f_dec=b.b58decode, f_enc=b.b58encode,quiet=False):
 	print "Input:         %s" % s_in
 	s_dec = f_dec(s_in)
 	print "Decoded data:  %s" % hexlify(s_dec)
@@ -138,25 +138,25 @@ def b58tohex(s_in,f_dec=b58decode, f_enc=b58encode,quiet=False):
 	test_equality(s_in,s_enc,["1"],quiet)
 
 def hextob58_pad(s_in, quiet=False):
-	hextob58(s_in,f_enc=b58encode_pad, f_dec=b58decode_pad, quiet=quiet)
+	hextob58(s_in,f_enc=b.b58encode_pad, f_dec=b.b58decode_pad, quiet=quiet)
 
 def b58tohex_pad(s_in, quiet=False):
-	b58tohex(s_in,f_dec=b58decode_pad, f_enc=b58encode_pad, quiet=quiet)
+	b58tohex(s_in,f_dec=b.b58decode_pad, f_enc=b.b58encode_pad, quiet=quiet)
 
 def	hextob58_pad_randloop(loops, quiet=False):
 	try:
 		for i in range(1,int(loops)+1):
 			r = hexlify(get_random(32))
-			hextob58(r,f_enc=b58encode_pad, f_dec=b58decode_pad, quiet=quiet)
+			hextob58(r,f_enc=b.b58encode_pad, f_dec=b.b58decode_pad, quiet=quiet)
 			if not quiet: print
 			if not i % 100 and quiet:
 				sys.stderr.write("\riteration: %i " % i)
 
 		sys.stderr.write("\r%s iterations completed\n" % i)
-	except:
-		print "User interrupt"
+	except KeyboardInterrupt:
+		msg("\nUser interrupt")
 
-def test_wiftohex(s_in,f_dec=wiftohex,f_enc=numtowif):
+def test_wiftohex(s_in,f_dec=b.wiftohex,f_enc=b.numtowif):
 	print "Input:         %s" % s_in
 	s_dec = f_dec(s_in)
 	print "Decoded data:  %s" % s_dec
@@ -170,7 +170,31 @@ def hextosha256(s_in):
 
 def pubhextoaddr(s_in):
 	print "Entered data:   %s" % s_in
-	s_enc = pubhex2addr(s_in)
+	s_enc = b.pubhex2addr(s_in)
+	print "Encoded data:   %s" % s_enc
+
+def hextowif_comp(s_in):
+	print "Entered data:   %s" % s_in
+	s_enc = b.hextowif(s_in,compressed=True)
+	print "Encoded data:   %s" % s_enc
+	s_dec = b.wiftohex(s_enc,compressed=True)
+	print "Decoded data:   %s" % s_dec
+
+def wiftohex_comp(s_in):
+	print "Entered data:   %s" % s_in
+	s_enc = b.wiftohex(s_in,compressed=True)
+	print "Encoded data:   %s" % s_enc
+	s_dec = b.hextowif(s_enc,compressed=True)
+	print "Decoded data:   %s" % s_dec
+
+def privhextoaddr_comp(hexpriv):
+	print b.privnum2addr(int(hexpriv,16),compressed=True)
+
+def wiftoaddr_comp(s_in):
+	print "Entered data:   %s" % s_in
+	s_enc = b.wiftohex(s_in,compressed=True)
+	print "Encoded data:   %s" % s_enc
+	s_enc = b.privnum2addr(int(s_enc,16),compressed=True)
 	print "Encoded data:   %s" % s_enc
 
 tests = {
@@ -188,6 +212,10 @@ tests = {
 	"hextosha256":              ['hexnum [str]','quiet [bool=False]'],
 	"hextowiftopubkey":         ['hexnum [str]','quiet [bool=False]'],
 	"pubhextoaddr":             ['hexnum [str]','quiet [bool=False]'],
+	"hextowif_comp":            ['hexnum [str]'],
+	"wiftohex_comp":            ['wif [str]'],
+	"privhextoaddr_comp":       ['hexnum [str]'],
+	"wiftoaddr_comp":           ['wif [str]'],
 }
 
 

+ 1 - 1
mmgen/tests/test.py

@@ -20,8 +20,8 @@ test.py:  Shared routines for mmgen test suite
 """
 
 import sys
-from mmgen.util import msg
 
+def msg(s): print s
 def nomsg(s): pass
 
 def test_equality(num_in,num_out,wl,quiet=False):

+ 347 - 197
mmgen/tx.py

@@ -29,9 +29,7 @@ txmsg = {
 'not_enough_btc': "Not enough BTC in the inputs for this transaction (%s BTC)",
 'throwaway_change': """
 ERROR: This transaction produces change (%s BTC); however, no change
-address was specified.  Total inputs - transaction fee = %s BTC.
-To create a valid transaction with no change address, send this sum to the
-specified recipient address.
+address was specified.
 """.strip(),
 'mixed_inputs': """
 NOTE: This transaction uses a mixture of both mmgen and non-mmgen inputs,
@@ -73,29 +71,35 @@ def trim_exponent(n):
 	return d.quantize(Decimal(1)) if d == d.to_integral() else d.normalize()
 
 
-def check_address(rcpt_address):
-	from mmgen.bitcoin import verify_addr
-	if not verify_addr(rcpt_address):
-		sys.exit(3)
 
-
-def check_btc_amt(send_amt):
+def is_btc_amt(amt):
 
 	from decimal import Decimal
 	try:
-		retval = Decimal(send_amt)
+		ret = Decimal(amt)
 	except:
-		msg("%s: Invalid amount" % send_amt)
-		sys.exit(3)
+		msg("%s: Invalid amount" % amt)
+		return False
 
 	if g.debug:
-		print "Decimal(amt): %s\nAs tuple: %s" % (send_amt,repr(retval.as_tuple()))
+		print "Decimal(amt): %s\nAs tuple: %s" % (amt,repr(ret.as_tuple()))
 
-	if retval.as_tuple()[-1] < -8:
-		msg("%s: Too many decimal places in amount" % send_amt)
-		sys.exit(3)
+	if ret.as_tuple()[-1] < -8:
+		msg("%s: Too many decimal places in amount" % amt)
+		return False
 
-	return trim_exponent(retval)
+	if ret == 0:
+		msg("Requested zero BTC amount")
+		return False
+
+	return trim_exponent(ret)
+
+def check_btc_amt(amt):
+	ret = is_btc_amt(amt)
+	if ret:
+		return ret
+	else:
+		sys.exit(3)
 
 
 def get_bitcoind_cfg_options(cfg_keys):
@@ -108,7 +112,7 @@ def get_bitcoind_cfg_options(cfg_keys):
 		data_dir = r"Application Data\Bitcoin"
 		cfg_file = "%s\%s\%s" % (os.environ["HOMEPATH"],data_dir,"bitcoin.conf")
 	else:
-		msg("Neither $HOME nor %HOMEPATH% is set")
+		msg("Neither $HOME nor %HOMEPATH% are set")
 		msg("Don't know where to look for 'bitcoin.conf'")
 		sys.exit(3)
 
@@ -135,17 +139,16 @@ def get_bitcoind_cfg_options(cfg_keys):
 	return cfg
 
 
-def print_tx_to_file(tx,sel_unspent,send_amt,opts):
+def print_tx_to_file(tx,sel_unspent,send_amt,b2m_map,opts):
 	tx_id = make_chksum_6(unhexlify(tx)).upper()
 	outfile = "tx_%s[%s].raw" % (tx_id,send_amt)
 	if 'outdir' in opts:
 		outfile = "%s/%s" % (opts['outdir'], outfile)
 	metadata = "%s %s %s" % (tx_id, send_amt, make_timestamp())
-	sig_data = [{"txid":i.txid,"vout":i.vout,"scriptPubKey":i.scriptPubKey}
-					for i in sel_unspent]
 	data = "%s\n%s\n%s\n%s\n" % (
-			metadata, tx, repr(sig_data),
-			repr([i.__dict__ for i in sel_unspent])
+			metadata, tx,
+			repr([i.__dict__ for i in sel_unspent]),
+			repr(b2m_map)
 		)
 	write_to_file(outfile,data,confirm=False)
 	msg("Transaction data saved to file '%s'" % outfile)
@@ -176,17 +179,11 @@ def format_unspent_outputs_for_printing(out,sort_info,total):
 		"Amount (BTC)","Age (days)", "Comment")]
 
 	for n,i in enumerate(out):
-		if verify_mmgen_label(i.account):
-			s = i.account.split(None,1)
-			mmid,cmt = s[0],(s[1] if len(s) == 2 else "")
-		else:
-			mmid,cmt = "",i.account
-
 		addr = "=" if i.skip == "addr" and "grouped" in sort_info else i.address
 		tx = " " * 63 + "=" \
 			  if i.skip == "txid" and "grouped" in sort_info else str(i.txid)
 
-		s = pfs % (str(n+1)+")", tx+","+str(i.vout),addr,mmid,i.amt,i.days,cmt)
+		s = pfs % (str(n+1)+")", tx+","+str(i.vout),addr,i.mmid,i.amt,i.days,i.label)
 		pout.append(s.rstrip())
 
 	return \
@@ -197,23 +194,20 @@ def format_unspent_outputs_for_printing(out,sort_info,total):
 
 def sort_and_view(unspent):
 
-	def s_amt(a,b):  return cmp(a.amount,b.amount)
-	def s_txid(a,b):
-		return cmp("%s %03s" % (a.txid,a.vout), "%s %03s" % (b.txid,b.vout))
-	def s_addr(a,b): return cmp(a.address,b.address)
-	def s_age(a,b):  return cmp(b.confirmations,a.confirmations)
-	def s_mmgen(a,b): return cmp(a.account,b.account)
+	def s_amt(i):   return i.amount
+	def s_txid(i):  return "%s %03s" % (i.txid,i.vout)
+	def s_addr(i):  return i.address
+	def s_age(i):   return i.confirmations
+	def s_mmgen(i): return i.account
 
-	fs = " %-4s %-11s %-2s %-34s %-13s %-s"
 	sort,group,show_mmaddr,reverse = "",False,False,False
 	total = trim_exponent(sum([i.amount for i in unspent]))
 
 	hdr_fmt   = "UNSPENT OUTPUTS (sort order: %s)  Total BTC: %s"
-	table_hdr = fs % ("Num","TX id  Vout","","Address","Amount (BTC)", "Age(days)")
 
 	options_msg = """
 Sort options: [t]xid, [a]mount, a[d]dress, [A]ge, [r]everse, [M]mgen addr
-Format options: [g]roup, show [m]mgen addr
+Display options: [g]roup, show [m]mgen addr, r[e]draw screen
 """.strip()
 	prompt = \
 "('q' = quit sorting, 'p' = print to file, 'v' = pager view, 'w' = wide view): "
@@ -222,57 +216,82 @@ Format options: [g]roup, show [m]mgen addr
 	print_to_file_msg = ""
 	msg("")
 
+	from mmgen.term import get_terminal_size
+
+	max_acct_len = max([len(i.account) for i in unspent])
+
 	while True:
-		out = deepcopy(unspent)
-		for i in out: i.skip = ""
+		cols = get_terminal_size()[0]
+		if cols < g.min_screen_width:
+			msg("mmgen-txcreate requires a screen at least %s characters wide" %
+					g.min_screen_width)
+			sys.exit(2)
+
+		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"
+		table_hdr = fs % ("Num","TX id  Vout","","Address",
+							"Amount (BTC)","Age(d)")
+
+		unsp = deepcopy(unspent)
+		for i in unsp: i.skip = ""
 		if group and (sort == "address" or sort == "txid"):
-			for n in range(len(out)-1):
-				a,b = out[n],out[n+1]
+			for n in range(len(unsp)-1):
+				a,b = unsp[n],unsp[n+1]
 				if sort == "address" and a.address == b.address: b.skip = "addr"
 				elif sort == "txid" and a.txid == b.txid:        b.skip = "txid"
 
-		for i in out:
+		for i in unsp:
 			amt = str(trim_exponent(i.amount))
 			lfill = 3 - len(amt.split(".")[0]) if "." in amt else 3 - len(amt)
 			i.amt = " "*lfill + amt
 			i.days = int(i.confirmations * g.mins_per_block / (60*24))
 
+			i.mmid,i.label = parse_mmgen_label(i.account)
+
 			if i.skip == "addr":
 				i.addr = "|" + "." * 33
 			else:
-				if show_mmaddr:
-					if verify_mmgen_label(i.account):
-						i.addr = "%s.. %s" % (i.address[:4],i.account)
-					else:
-						i.addr = i.address
+				if show_mmaddr and i.mmid:
+					acct_w   = min(max_acct_len, max(24,int(addr_w-10)))
+					btaddr_w = addr_w - acct_w - 1
+
+					dots = ".." if btaddr_w < len(i.address) else ""
+
+					i.addr = "%s%s %s" % (
+						i.address[:btaddr_w-len(dots)],
+						dots,
+						i.account[:acct_w])
 				else:
 					i.addr = i.address
 
-			i.tx = "       |..." if i.skip == "txid" else i.txid[:8]+"..."
+			dots = "..." if tx_w < 64 else ""
+			i.tx = " " * (tx_w-4) + "|..." if i.skip == "txid" \
+					else i.txid[:tx_w-len(dots)]+dots
 
 		sort_info = ["reverse"] if reverse else []
 		sort_info.append(sort if sort else "unsorted")
 		if group and (sort == "address" or sort == "txid"):
 			sort_info.append("grouped")
 
-		output = [hdr_fmt % (" ".join(sort_info), total), table_hdr]
+		out = [hdr_fmt % (" ".join(sort_info), total), table_hdr]
 
-		for n,i in enumerate(out):
-			output.append(fs % (str(n+1)+")",i.tx,i.vout,i.addr,i.amt,i.days))
+		for n,i in enumerate(unsp):
+			out.append(fs % (str(n+1)+")",i.tx,i.vout,i.addr,i.amt,i.days))
 
-		msg("\n".join(output) +"\n\n" + print_to_file_msg + options_msg)
+		msg("\n".join(out) +"\n\n" + print_to_file_msg + options_msg)
 		print_to_file_msg = ""
 
-		immed_chars = "qpPtadArMgm"
+		immed_chars = "atdAMrgmeqpvw"
 		skip_prompt = False
 
 		while True:
 			reply = get_char(prompt, immed_chars=immed_chars)
 
-			if   reply == 'a': unspent.sort(s_amt);  sort = "amount"
-			elif reply == 't': unspent.sort(s_txid); sort = "txid"
-			elif reply == 'd': unspent.sort(s_addr); sort = "address"
-			elif reply == 'A': unspent.sort(s_age);  sort = "age"
+			if   reply == 'a': unspent.sort(key=s_amt);  sort = "amount"
+			elif reply == 't': unspent.sort(key=s_txid); sort = "txid"
+			elif reply == 'd': unspent.sort(key=s_addr); sort = "address"
+			elif reply == 'A': unspent.sort(key=s_age);  sort = "age"
 			elif reply == 'M':
 				unspent.sort(s_mmgen)
 				sort = "mmgen"
@@ -282,17 +301,18 @@ Format options: [g]roup, show [m]mgen addr
 				reverse = False if reverse else True
 			elif reply == 'g': group = False if group else True
 			elif reply == 'm': show_mmaddr = False if show_mmaddr else True
+			elif reply == 'e': pass
 			elif reply == 'q': pass
 			elif reply == 'p':
-				data = format_unspent_outputs_for_printing(out,sort_info,total)
+				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
 			elif reply == 'v':
-				do_pager("\n".join(output))
+				do_pager("\n".join(out))
 				continue
 			elif reply == 'w':
-				data = format_unspent_outputs_for_printing(out,sort_info,total)
+				data = format_unspent_outputs_for_printing(unsp,sort_info,total)
 				do_pager(data)
 				continue
 			else:
@@ -307,31 +327,19 @@ Format options: [g]roup, show [m]mgen addr
 	return tuple(unspent)
 
 
-def verify_mmgen_label(s,return_str=False,check_label_len=False):
+def parse_mmgen_label(s,check_label_len=False):
 
-	fail    = "" if return_str else False
-	success = s  if return_str else True
+	if not s: return "",""
 
-	if not s: return fail
+	try:    w1,w2 = s.split(None,1)
+	except: w1,w2 = s,""
 
-	try:
-		mminfo,comment = s.split(None,1)
-	except:
-		mminfo,comment = s,None
-
-	if mminfo[8] != ':': return fail
-	for i in mminfo[:8]:
-		if not i in "01234567890ABCDEF": return fail
-	for i in mminfo[9:]:
-		if not i in "0123456789": return fail
+	if not is_mmgen_addr(w1): return "",w1
+	if check_label_len: check_addr_label(w2)
+	return w1,w2
 
-	if check_label_len and comment:
-		check_addr_comment(comment)
 
-	return success
-
-
-def view_tx_data(c,inputs_data,tx_hex,metadata=[],pager=False):
+def view_tx_data(c,inputs_data,tx_hex,b2m_map,metadata=[],pager=False):
 
 	td = c.decoderawtransaction(tx_hex)
 
@@ -347,39 +355,51 @@ def view_tx_data(c,inputs_data,tx_hex,metadata=[],pager=False):
 			if j['txid'] == i['txid'] and j['vout'] == i['vout']:
 				days = int(j['confirmations'] * g.mins_per_block / (60*24))
 				total_in += j['amount']
-				out += (" " + """
-%-2s tx,vout: %s,%s
-    address:        %s
-    ID/label:       %s
-    amount:         %s BTC
-    confirmations:  %s (around %s days)
-""".strip() %
-	(n+1,i['txid'],i['vout'],j['address'],verify_mmgen_label(j['account'],True),
-		trim_exponent(j['amount']),j['confirmations'],days)+"\n\n")
-				break
+				addr = j['address']
 
-	out += "Total input: %s BTC\n\n" % trim_exponent(total_in)
+				if j['account']:
+					tmp = j['account'].split(None,1)
+					mmid,label = tmp if len(tmp) == 2 else (tmp[0],"")
+					label = label or ""
+				else:
+					mmid,label = "",""
+
+				mmid_str = ((34-len(addr))*" " + " (%s)" % mmid) if mmid else ""
+
+				for d in (
+	(n+1, "tx,vout:",       "%s,%s" % (i['txid'], i['vout'])),
+	("",  "address:",       addr + mmid_str),
+	("",  "label:",         label),
+	("",  "amount:",        "%s BTC" % trim_exponent(j['amount'])),
+	("",  "confirmations:", "%s (around %s days)" % (j['confirmations'], days))
+					):
+					if d[2]: out += ("%3s %-8s %s\n" % d)
+				out += "\n"
 
+				break
 	total_out = 0
 	out += "Outputs:\n\n"
 	for n,i in enumerate(td['vout']):
+		addr = i['scriptPubKey']['addresses'][0]
+		mmid,label = b2m_map[addr] if addr in b2m_map else ("","")
+		mmid_str = ((34-len(addr))*" " + " (%s)" % mmid) if mmid else ""
 		total_out += i['value']
-		out += (" " + """
-%-2s address: %s
-    amount:  %s BTC
-""".strip() % (
-		n,
-		i['scriptPubKey']['addresses'][0],
-		trim_exponent(i['value']))
-	+ "\n\n")
+		for d in (
+				(n+1, "address:",  addr + mmid_str),
+				("",  "label:",    label),
+				("",  "amount:",   trim_exponent(i['value']))
+			):
+			if d[2]: out += ("%3s %-8s %s\n" % d)
+		out += "\n"
+
+	out += "Total input:  %s BTC\n" % trim_exponent(total_in)
 	out += "Total output: %s BTC\n" % trim_exponent(total_out)
 	out += "TX fee:       %s BTC\n" % trim_exponent(total_in-total_out)
 
-	if pager: do_pager(out+"\n")
+	if pager: do_pager(out)
 	else:     msg("\n"+out)
 
 
-
 def parse_tx_data(tx_data,infile):
 
 	if len(tx_data) != 4:
@@ -396,20 +416,28 @@ def parse_tx_data(tx_data,infile):
 	except:
 		msg(err_fmt % "hex data")
 		sys.exit(2)
+	else:
+		if not tx_data:
+			msg("Transaction is empty!")
+			sys.exit(2)
 
 	try:
-		sig_data = eval(tx_data[2])
+		inputs_data = eval(tx_data[2])
 	except:
-		msg(err_fmt % "signature data")
+		msg(err_fmt % "inputs data")
 		sys.exit(2)
+	else:
+		if not inputs_data:
+			msg("Transaction has no inputs!")
+			sys.exit(2)
 
 	try:
-		inputs_data = eval(tx_data[3])
+		map_data = eval(tx_data[3])
 	except:
-		msg(err_fmt % "inputs data")
+		msg(err_fmt % "mmgen to btc address map data")
 		sys.exit(2)
 
-	return tx_data[0].split(),tx_data[1],sig_data,inputs_data
+	return tx_data[0].split(),tx_data[1],inputs_data,map_data
 
 
 def select_outputs(unspent,prompt):
@@ -430,8 +458,83 @@ def select_outputs(unspent,prompt):
 
 		return selected
 
+def is_mmgen_seed(s):
+	import re
+	return len(s) == 8 and re.match(r"^[0123456789ABCDEF]*$",s)
+
+def is_mmgen_num(s):
+	import re
+	return len(s) <= g.mmgen_idx_max_digits \
+		and re.match(r"^[123456789]+[0123456789]*$",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:])
+
+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):
+
+	# 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)
+
+	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 alist[0],w2
+
+
+def mmgen_addr_to_addr_data(m,addr_data):
 
-def mmgen_addr_to_btc_addr(m,addr_data):
+	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
@@ -442,68 +545,66 @@ def mmgen_addr_to_btc_addr(m,addr_data):
 		except: pass
 		else:
 			if not addr_data:
-				msg("Address data must be supplied for MMgen address '%s'" % m)
+				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:
-							return j[1]
-			msg("MMgen address '%s' not found in supplied address data" % m)
+							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)
 
 	msg("Invalid format: %s" % m)
 	sys.exit(3)
 
 
+def check_mmgen_to_btc_addr_mappings(inputs_data,b2m_map,infiles,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])]
+	out_maplist = [(i[1][0],i[0]) for i in b2m_map.items()]
+
+	for maplist,label in (in_maplist,"inputs"), (out_maplist,"outputs"):
+		if not maplist: continue
+		qmsg("Checking MMGen -> BTC address mappings for %s" % label)
+		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)
+		for a,b in zip(sorted(pairs),sorted(maplist)):
+			if a != b:
+				msg("""
+MMGen -> BTC address mappings differ!
+In transaction:      %s
+Generated from seed: %s
+	""".strip() % (" ".join(a)," ".join(b)))
+				sys.exit(3)
 
-def make_tx_out(tx_arg,addr_data):
-
-	tx = {}
-	for i in tx_arg:
-		addr,amt = i.split(",")
-
-		if ":" in addr:
-			addr = mmgen_addr_to_btc_addr(addr,addr_data)
-		else:
-			check_address(addr)
-
-		try: tx[addr] = amt
-		except:
-			msg("Invalid format: %s: %s" % (addr,amt))
-			sys.exit(3)
-
-	if g.debug:
-		print "TX (cl):   ", repr(tx_arg)
-		print "TX (proc): ", repr(tx)
-
-	import decimal
-	try:
-		for i in tx.keys():
-			tx[i] = trim_exponent(Decimal(tx[i]))
-	except decimal.InvalidOperation:
-		msg("Decimal conversion error in suboption '%s:%s'" % (i,tx[i]))
-		sys.exit(3)
-
-	return tx
+	qmsg("Address mappings OK\n")
 
 
-def check_addr_comment(label):
+def check_addr_label(label):
 
 	if len(label) > g.max_addr_label_len:
 		msg("'%s': overlong label (length must be <=%s)" %
 				(label,g.max_addr_label_len))
 		sys.exit(3)
 
-	for ch in list(label):
+	for ch in label:
 		if ch not in g.addr_label_symbols:
-			msg("'%s': illegal character in label '%s'" % (ch,label))
-			msg("Permitted characters: A-Za-z0-9, plus '%s'" %
-					"', '".join(g.addr_label_punc))
+			msg("""
+"%s": illegal character in label "%s".
+Only ASCII printable characters are permitted.
+""".strip() % (ch,label))
 			sys.exit(3)
 
 
 def parse_addrs_file(f):
+
 	lines = get_lines_from_file(f,"address data",remove_comments=True)
 
 	try:
@@ -518,30 +619,23 @@ def parse_addrs_file(f):
 		msg("'%s': invalid first line" % lines[0])
 	elif cbrace != '}':
 		msg("'%s': invalid last line" % cbrace)
-	elif len(seed_id) != 8:
+	elif not is_mmgen_seed(seed_id):
 		msg("'%s': invalid Seed ID" % seed_id)
 	else:
-		try:
-			unhexlify(seed_id)
-		except:
-			msg("'%s': invalid Seed ID" % seed_id)
-			sys.exit(3)
-
 		ret = []
 		for i in lines[1:-1]:
 			d = i.split(None,2)
 
-			try: d[0] = int(d[0])
-			except:
+			if not is_mmgen_num(d[0]):
 				msg("'%s': invalid address num. in line: %s" % (d[0],d))
 				sys.exit(3)
 
-			from mmgen.bitcoin import verify_addr
-			if not verify_addr(d[1]):
-				msg("'%s': invalid address" % d[1])
+			if not is_btc_addr(d[1]):
+				msg("'%s': invalid Bitcoin address" % d[1])
 				sys.exit(3)
 
-			if len(d) == 3: check_addr_comment(d[2])
+			if len(d) == 3:
+				check_addr_label(d[2])
 
 			ret.append(tuple(d))
 
@@ -553,52 +647,59 @@ def parse_addrs_file(f):
 def sign_transaction(c,tx_hex,sig_data,keys=None):
 
 	if keys:
-		msg("%s keys total" % len(keys))
+		qmsg("%s keys total" % len(keys))
 		if g.debug: print "Keys:\n  %s" % "\n  ".join(keys)
 
+	msg_r("Signing transaction...")
 	from mmgen.rpc import exceptions
-
 	try:
 		sig_tx = c.signrawtransaction(tx_hex,sig_data,keys)
 	except exceptions.InvalidAddressOrKey:
-		msg("Invalid address or key")
+		msg("failed\nInvalid address or key")
 		sys.exit(3)
-# 	except:
-# 		msg("Failed to sign transaction")
-# 		sys.exit(3)
 
 	return sig_tx
 
 
-def get_keys_for_mmgen_addrs(mmgen_addrs,infiles,opts):
+def get_keys_for_mmgen_addrs(mmgen_addrs,infiles,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 = []
 
-	seed_ids = list(set([i['account'][:8] for i in mmgen_addrs]))
-	seed_ids_save = seed_ids[0:]
-	keys = []
+	seeds_keys = [i for i in seed_ids if i in seeds]
 
 	while seed_ids:
-		infile = False
-		if infiles:
-			infile = infiles.pop()
-			seed = get_seed(infile,opts)
-		elif "from_brain" in opts or "from_mnemonic" in opts or "from_seed" in opts:
-			msg("Need data for seed ID %s" % seed_ids[0])
-			seed = get_seed_retry("",opts)
+		if seeds_keys:
+			seed = seeds[seeds_keys.pop()]
 		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)
+			infile = False
+			if infiles:
+				infile = infiles.pop()
+				seed = get_seed_retry(infile,opts)
+			elif "from_brain" in opts or "from_mnemonic" in opts \
+						or "from_seed" 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)
-			seed_id_addrs = [
-				int(i['account'].split()[0][9:]) for i in mmgen_addrs
-					if i['account'][:8] == seed_id]
-
-			from mmgen.addr import generate_keys
-			keys += [i['wif'] for i in generate_keys(seed, seed_id_addrs)]
+			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)]
 		else:
 			if seed_id in seed_ids_save:
 				msg_r("Ignoring duplicate seed source")
@@ -610,7 +711,7 @@ def get_keys_for_mmgen_addrs(mmgen_addrs,infiles,opts):
 					msg("Invalid input file: %s" % infile)
 					sys.exit(2)
 
-	return keys
+	return ret
 
 
 def sign_tx_with_bitcoind_wallet(c,tx_hex,sig_data,keys,opts):
@@ -642,15 +743,64 @@ def sign_tx_with_bitcoind_wallet(c,tx_hex,sig_data,keys,opts):
 	return sig_tx
 
 
+def preverify_keys(addrs_orig, keys_orig):
+
+	addrs,keys,wrong_keys = set(addrs_orig[0:]),set(keys_orig[0:]),[]
+
+	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)))
+		sys.exit(2)
+
+	import mmgen.bitcoin as b
+
+	qmsg_r('Checking that user-supplied key list contains valid keys...')
+
+	invalid_keys = []
+
+	for n,k in enumerate(keys,1):
+		c = False if k[0] == '5' else True
+		if b.wiftohex(k,compressed=c) == False:
+			invalid_keys.append(k)
+
+	if invalid_keys:
+		s = "" if len(invalid_keys) == 1 else "s"
+		msg("\n%s/%s invalid key%s in keylist!\n" % (len(invalid_keys),len(keys),s))
+		sys.exit(2)
+	else: qmsg("OK")
+
+	msg('Pre-verifying keys in user-supplied key list (Ctrl-C to skip)')
+
+	try:
+		for n,k in enumerate(keys,1):
+			msg_r("\rkey %s of %s" % (n,len(keys)))
+			c = False if k[0] == '5' else True
+			hexkey = b.wiftohex(k,compressed=c)
+			addr = b.privnum2addr(int(hexkey,16),compressed=c)
+			if addr in addrs:
+				addrs.remove(addr)
+				if not addrs: break
+			else:
+				wrong_keys.append(k)
+	except KeyboardInterrupt:
+		msg("\nSkipping")
+	else:
+		msg("")
+		if wrong_keys:
+			s = "" if len(wrong_keys) == 1 else "s"
+			msg("%s extra key%s found" % (len(wrong_keys),s))
+
+		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))
+			print "  %s" % "\n  ".join(addrs)
+			sys.exit(2)
+
+
 def missing_keys_errormsg(other_addrs):
 	msg("""
-A key file (option '-f') or wallet.dat (option '-w') must be supplied
-for the following non-mmgen address%s: %s""" %
-	("" if len(other_addrs) == 1 else "es",
-	" ".join([i['address'] for i in other_addrs])
-	  ))
-
-def get_addr_data(cmd_args):
-	for f in cmd_args:
-		data = parse_addrs_file(f)
-		print repr(data); sys.exit() # DEBUG
+A key file must be supplied (or use the "-w" option) for the following
+non-mmgen address%s:
+""".strip() % ("" if len(other_addrs) == 1 else "es"))
+	print "  %s" % "\n  ".join([i['address'] for i in other_addrs])

+ 63 - 163
mmgen/util.py

@@ -23,124 +23,21 @@ import sys
 import mmgen.config as g
 from binascii import hexlify,unhexlify
 from mmgen.bitcoin import b58decode_pad
+from mmgen.term import *
+
+def msg(s):    sys.stderr.write(s + "\n")
+def msg_r(s):  sys.stderr.write(s)
+def qmsg(s):
+	if not g.quiet: sys.stderr.write(s + "\n")
+def qmsg_r(s):
+	if not g.quiet: sys.stderr.write(s)
+def vmsg(s):
+	if g.verbose: sys.stderr.write(s + "\n")
+def vmsg_r(s):
+	if g.verbose: sys.stderr.write(s)
 
-def msg(s):   sys.stderr.write(s + "\n")
-def msg_r(s): sys.stderr.write(s)
 def bail(): sys.exit(9)
 
-def kb_hold_protect_unix():
-
-	fd = sys.stdin.fileno()
-	old = termios.tcgetattr(fd)
-	tty.setcbreak(fd)
-
-	timeout = float(0.3)
-
-	try:
-		while True:
-			key = select([sys.stdin], [], [], timeout)[0]
-			if key: sys.stdin.read(1)
-			else: break
-	except:
-		print "\nUser interrupt"
-		sys.exit(1)
-	finally:
-		termios.tcsetattr(fd, termios.TCSADRAIN, old)
-
-
-def get_keypress_unix(prompt="",immed_chars=""):
-
-	msg_r(prompt)
-	timeout = float(0.3)
-
-	fd = sys.stdin.fileno()
-	old = termios.tcgetattr(fd)
-	tty.setcbreak(fd)
-
-	try:
-		while True:
-			select([sys.stdin], [], [], False)
-			ch = sys.stdin.read(1)
-			if immed_chars == "ALL" or ch in immed_chars:
-				return ch
-			if immed_chars == "ALL_EXCEPT_ENTER" and not ch in "\n\r":
-				return ch
-			second_key = select([sys.stdin], [], [], timeout)[0]
-			if second_key: continue
-			else: return ch
-	except:
-		print "\nUser interrupt"
-		sys.exit(1)
-	finally:
-		termios.tcsetattr(fd, termios.TCSADRAIN, old)
-
-
-def kb_hold_protect_mswin():
-
-	timeout = float(0.5)
-
-	try:
-		while True:
-			hit_time = time.time()
-			while True:
-				if msvcrt.kbhit():
-					msvcrt.getch()
-					break
-				if float(time.time() - hit_time) > timeout:
-					return
-	except:
-		msg("\nUser interrupt")
-		sys.exit(1)
-
-
-def get_keypress_mswin(prompt="",immed_chars=""):
-
-	msg_r(prompt)
-	timeout = float(0.5)
-
-	try:
-		while True:
-			if msvcrt.kbhit():
-				ch = msvcrt.getch()
-
-				if ord(ch) == 3: raise KeyboardInterrupt
-
-				if immed_chars == "ALL" or ch in immed_chars:
-					return ch
-				if immed_chars == "ALL_EXCEPT_ENTER" and not ch in "\n\r":
-					return ch
-
-				hit_time = time.time()
-
-				while True:
-					if msvcrt.kbhit(): break
-					if float(time.time() - hit_time) > timeout:
-						return ch
-	except:
-		msg("\nUser interrupt")
-		sys.exit(1)
-
-
-try:
-	import tty, termios
-	from select import select
-	get_char = get_keypress_unix
-	kb_hold_protect = kb_hold_protect_unix
-except:
-	try:
-		import msvcrt, time
-		get_char = get_keypress_mswin
-		kb_hold_protect = kb_hold_protect_mswin
-	except:
-		if not sys.platform.startswith("linux") \
-				and not sys.platform.startswith("win"):
-			msg("Unsupported platform: %s" % sys.platform)
-			msg("This program currently runs only on Linux and Windows")
-		else:
-			msg("Unable to set terminal mode")
-		sys.exit(2)
-
-
 def my_raw_input(prompt,echo=True,allowed_chars=""):
 	try:
 		if echo:
@@ -148,8 +45,8 @@ def my_raw_input(prompt,echo=True,allowed_chars=""):
 		else:
 			from getpass import getpass
 			reply = getpass(prompt)
-	except:
-		print "\nUser interrupt"
+	except KeyboardInterrupt:
+		msg("\nUser interrupt")
 		sys.exit(1)
 
 	kb_hold_protect()
@@ -202,7 +99,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),
+""".format(g.proj_name_cap),
 	'brain_warning': """
 ############################## EXPERTS ONLY! ##############################
 
@@ -223,7 +120,7 @@ future, you must continue using these same parameters
 
 def confirm_or_exit(message, question, expect="YES"):
 
-	msg("")
+	vmsg("")
 
 	m = message.strip()
 	if m: msg(m)
@@ -353,7 +250,7 @@ def get_new_passphrase(what, opts):
 			pw2 = " ".join(_get_words_from_user(("Repeat %s: " % what),opts))
 			if g.debug: print "Passphrases: [%s] [%s]" % (pw,pw2)
 			if pw == pw2:
-				msg("%ss match" % what.capitalize())
+				vmsg("%ss match" % what.capitalize())
 				break
 			else:
 				msg("%ss do not match" % what.capitalize())
@@ -362,7 +259,7 @@ def get_new_passphrase(what, opts):
 					g.passwd_max_tries)
 			sys.exit(2)
 
-	if pw == "": msg("WARNING: Empty passphrase")
+	if pw == "": qmsg("WARNING: Empty passphrase")
 	return pw
 
 
@@ -384,17 +281,17 @@ def _get_seed_from_brain_passphrase(words,opts):
 	if g.debug: print "Sanitized brain passphrase: %s" % bp
 	seed_len,hash_preset = _get_from_brain_opt_params(opts)
 	if g.debug: print "Brainwallet l = %s, p = %s" % (seed_len,hash_preset)
-	msg_r("Hashing brainwallet data.  Please wait...")
+	vmsg_r("Hashing brainwallet data.  Please wait...")
 	# Use buflen arg of scrypt.hash() to get seed of desired length
 	seed = _scrypt_hash_passphrase(bp, "", hash_preset, buflen=seed_len/8)
-	msg("Done")
+	vmsg("Done")
 	return seed
 
 
-def encrypt_seed(seed, key, opts):
+def encrypt_seed(seed, key):
 	"""
-	Encrypt a seed for a {} deterministic wallet
-	""".format(g.proj_name)
+	Encrypt a seed for an {} deterministic wallet
+	""".format(g.proj_name_cap)
 
 	# 192-bit seed is 24 bytes -> not multiple of 16.  Must use MODE_CTR
 	from Crypto.Cipher import AES
@@ -403,14 +300,14 @@ def encrypt_seed(seed, key, opts):
 	c = AES.new(key, AES.MODE_CTR,counter=Counter.new(128))
 	enc_seed = c.encrypt(seed)
 
-	msg_r("Performing a test decryption of the seed...")
+	vmsg_r("Performing a test decryption of the seed...")
 
 	c = AES.new(key, AES.MODE_CTR,counter=Counter.new(128))
 	dec_seed = c.decrypt(enc_seed)
 
-	if dec_seed == seed: msg("done")
+	if dec_seed == seed: vmsg("done")
 	else:
-		msg("FAILED.\nDecrypted seed doesn't match original seed")
+		msg("ERROR.\nDecrypted seed doesn't match original seed")
 		sys.exit(2)
 
 	return enc_seed
@@ -576,11 +473,11 @@ def write_wallet_to_file(seed, passwd, key_id, salt, enc_seed, opts):
 
 	chk = make_chksum_6(" ".join(lines))
 
-	confirm = False if 'quiet' in opts else True
+	confirm = False if g.quiet else True
 	write_to_file(outfile, "\n".join((chk,)+lines)+"\n", confirm)
 
 	msg("Wallet saved to file '%s'" % outfile)
-	if 'verbose' in opts:
+	if g.verbose:
 		_display_control_data(label,metadata,hash_preset,salt,enc_seed)
 
 
@@ -592,10 +489,10 @@ def write_walletdat_dump_to_file(wallet_id,data,num_keys,ext,what,opts):
 	msg("wallet.dat %s saved to file '%s'" % (what,outfile))
 
 
-def compare_checksums(chksum1, desc1, chksum2, desc2):
+def _compare_checksums(chksum1, desc1, chksum2, desc2):
 
 	if chksum1.lower() == chksum2.lower():
-		msg("OK (%s)" % chksum1.upper())
+		vmsg("OK (%s)" % chksum1.upper())
 		return True
 	else:
 		if g.debug:
@@ -608,6 +505,8 @@ def _is_hex(s):
 	except: return False
 	else: return True
 
+def match_ext(addr,ext):
+	return addr.split(".")[-1] == ext
 
 def _check_mmseed_format(words):
 
@@ -629,10 +528,7 @@ def _check_mmseed_format(words):
 	return valid
 
 
-def check_wallet_format(infile, lines, opts):
-
-	def vmsg(s):
-		if 'verbose' in opts: msg(s)
+def _check_wallet_format(infile, lines):
 
 	what = "wallet file '%s'" % infile
 	valid = False
@@ -660,17 +556,19 @@ def _check_chksum_6(chk,val,desc,infile):
 		msg("%s checksum passed: %s" % (desc.capitalize(),chk))
 
 
-def get_data_from_wallet(infile,opts,silent=False):
+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,infile))
+		msg("Getting {} wallet data from file '{}'".format(g.proj_name_cap,infile))
 
 	f = open_file_or_exit(infile, 'r')
 
 	lines = [i.strip() for i in f.readlines()]
 	f.close()
 
-	check_wallet_format(infile, lines, opts)
+	_check_wallet_format(infile, lines)
 
 	label = lines[1]
 
@@ -711,7 +609,7 @@ def _get_words_from_user(prompt, opts):
 
 
 def _get_words_from_file(infile,what):
-	msg("Getting %s from file '%s'" % (what,infile))
+	qmsg("Getting %s from file '%s'" % (what,infile))
 	f = open_file_or_exit(infile, 'r')
 	# split() also strips
 	words = f.read().split()
@@ -721,7 +619,8 @@ def _get_words_from_file(infile,what):
 
 
 def get_lines_from_file(infile,what="",remove_comments=False):
-	if what != "": msg("Getting %s from file '%s'" % (what,infile))
+	if what != "":
+		qmsg("Getting %s from file '%s'" % (what,infile))
 	f = open_file_or_exit(infile,'r')
 	lines = f.read().splitlines(); f.close()
 	if remove_comments:
@@ -738,7 +637,7 @@ def get_lines_from_file(infile,what="",remove_comments=False):
 
 
 def get_data_from_file(infile,what="data"):
-	msg("Getting %s from file '%s'" % (what,infile))
+	qmsg("Getting %s from file '%s'" % (what,infile))
 	f = open_file_or_exit(infile,'r')
 	data = f.read()
 	f.close()
@@ -755,18 +654,18 @@ def _get_seed_from_seed_data(words):
 	seed_b58 = "".join(words[1:])
 
 	chk = make_chksum_6(seed_b58)
-	msg_r("Validating %s checksum..." % g.seed_ext)
+	vmsg_r("Validating %s checksum..." % g.seed_ext)
 
-	if compare_checksums(chk, "from seed", stored_chk, "from input"):
+	if _compare_checksums(chk, "from seed", stored_chk, "from input"):
 		seed = b58decode_pad(seed_b58)
 		if seed == False:
 			msg("Invalid b58 number: %s" % val)
 			return False
 
-		msg("%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
 	else:
-		msg("Invalid checksum for {} seed".format(g.proj_name))
+		msg("Invalid checksum for {} seed".format(g.proj_name_cap))
 		return False
 
 
@@ -791,7 +690,8 @@ def get_mmgen_passphrase(prompt,opts):
 def get_bitcoind_passphrase(prompt,opts):
 	if 'passwd_file' in opts:
 		mark_passwd_file_as_used(opts)
-		return get_data_from_file(opts['passwd_file'],"passphrase").strip("\r\n")
+		return get_data_from_file(opts['passwd_file'],
+				"passphrase").strip("\r\n")
 	else:
 		return my_raw_input(prompt,
 					echo=True if 'echo_passphrase' in opts else False)
@@ -800,14 +700,14 @@ def get_bitcoind_passphrase(prompt,opts):
 def get_seed_from_wallet(
 		infile,
 		opts,
-		prompt="Enter {} wallet passphrase: ".format(g.proj_name),
+		prompt="Enter {} wallet passphrase: ".format(g.proj_name_cap),
 		silent=False
 		):
 
-	wdata = get_data_from_wallet(infile,opts,silent=silent)
+	wdata = get_data_from_wallet(infile,silent=silent)
 	label,metadata,hash_preset,salt,enc_seed = wdata
 
-	if 'verbose' in opts: _display_control_data(*wdata)
+	if g.verbose: _display_control_data(*wdata)
 
 	passwd = get_mmgen_passphrase(prompt,opts)
 
@@ -818,21 +718,21 @@ def get_seed_from_wallet(
 
 def make_key(passwd, salt, hash_preset):
 
-	msg_r("Hashing passphrase.  Please wait...")
+	vmsg_r("Hashing passphrase.  Please wait...")
 	key = _scrypt_hash_passphrase(passwd, salt, hash_preset)
-	msg("done")
+	vmsg("done")
 	return key
 
 
 def decrypt_seed(enc_seed, key, seed_id, key_id):
 
-	msg_r("Checking key...")
+	vmsg_r("Checking key...")
 	chk = make_chksum_8(key)
-	if not compare_checksums(chk, "of key", key_id, "in header"):
+	if not _compare_checksums(chk, "of key", key_id, "in header"):
 		msg("Incorrect passphrase")
 		return False
 
-	msg_r("Decrypting seed with key...")
+	vmsg_r("Decrypting seed with key...")
 
 	from Crypto.Cipher import AES
 	from Crypto.Util import Counter
@@ -841,13 +741,13 @@ def decrypt_seed(enc_seed, key, seed_id, key_id):
 	dec_seed = c.decrypt(enc_seed)
 
 	chk = make_chksum_8(dec_seed)
-	if compare_checksums(chk,"of decrypted seed",seed_id,"in header"):
-		msg("Passphrase is OK")
+	if _compare_checksums(chk,"of decrypted seed",seed_id,"in header"):
+		qmsg("Passphrase is OK")
 	else:
 		if not g.debug:
 			msg_r("Checking key ID...")
 			chk = make_chksum_8(key)
-			if compare_checksums(chk, "of key", key_id, "in header"):
+			if _compare_checksums(chk, "of key", key_id, "in header"):
 				msg("Key ID is correct but decryption of seed failed")
 			else:
 				msg("Incorrect passphrase")
@@ -894,10 +794,10 @@ def get_seed(infile,opts,silent=False):
 		if 'from_brain' not in opts:
 			msg("'--from-brain' parameters must be specified for brainwallet file")
 			sys.exit(2)
-		if 'quiet' not in opts:
+		if not g.quiet:
 			confirm_or_exit(
 				cmessages['brain_warning'].format(
-					g.proj_name.capitalize(), *_get_from_brain_opt_params(opts)),
+					g.proj_name_cap, *_get_from_brain_opt_params(opts)),
 				"continue")
 		prompt = "Enter brainwallet passphrase: "
 		words = _get_words(infile,"brainwallet data",prompt,opts)
@@ -954,10 +854,10 @@ def do_pager(text):
 		else:
 			try:
 				p.communicate(text+end+"\n")
-			except:
+			except KeyboardInterrupt:
 				# Has no effect.  Why?
 				if pager != "less":
-					msg("\n(Interrupted by user)\n")
+					msg("\n(User interrupt)\n")
 			finally:
 				msg_r("\r")
 				break

+ 4 - 3
mmgen/walletgen.py

@@ -20,14 +20,15 @@ walletgen.py:  Routines used for seed generation and wallet creation
 """
 
 import sys
-from mmgen.util import msg, msg_r, get_char, prompt_and_get_char
+import mmgen.config as g
+from mmgen.util import msg, msg_r, qmsg, qmsg_r, get_char, prompt_and_get_char
 from binascii import hexlify
 
 def get_random_data_from_user(opts):
 
 	ulen = opts['usr_randlen']
 
-	if 'quiet' in opts:
+	if g.quiet:
 		msg("Enter %s random symbols" % ulen)
 	else:
 		msg("""
@@ -54,7 +55,7 @@ displayed on the screen.
 		intervals.append(now - saved_time)
 		saved_time = now
 
-	if 'quiet' in opts:
+	if g.quiet:
 		msg_r("\r")
 	else:
 		msg_r("\rThank you.  That's enough." + " "*15 + "\n\n")

+ 2 - 1
setup.py

@@ -3,7 +3,7 @@ from distutils.core import setup
 
 setup(
 		name         = 'mmgen',
-		version      = '0.6.9',
+		version      = '0.7.0',
 		author       = 'Philemon',
 		author_email = 'mmgen-py@yandex.com',
 		url          = 'https://github.com/mmgen/mmgen',
@@ -19,6 +19,7 @@ setup(
 			'mmgen.mnemonic',
 			'mmgen.mn_tirosh',
 			'mmgen.Opts',
+			'mmgen.term',
 			'mmgen.tx',
 			'mmgen.util',
 			'mmgen.walletgen',