Browse Source

TX script improvements, added features (tx's with both Satoshi and MMGen inputs)
modified: mmgen-addrgen
modified: mmgen-addrimport
modified: mmgen-txcreate
modified: mmgen-txsend
modified: mmgen-txsign
modified: mmgen-walletchk
modified: mmgen-walletgen
modified: mmgen/addr.py
modified: mmgen/config.py
modified: mmgen/tx.py
modified: mmgen/utils.py

philemon 11 years ago
parent
commit
7cfca299a3
11 changed files with 257 additions and 97 deletions
  1. 7 4
      mmgen-addrgen
  2. 7 3
      mmgen-addrimport
  3. 26 2
      mmgen-txcreate
  4. 1 1
      mmgen-txsend
  5. 115 46
      mmgen-txsign
  6. 3 2
      mmgen-walletchk
  7. 5 2
      mmgen-walletgen
  8. 28 19
      mmgen/addr.py
  9. 1 0
      mmgen/config.py
  10. 57 11
      mmgen/tx.py
  11. 7 7
      mmgen/utils.py

+ 7 - 4
mmgen-addrgen

@@ -72,9 +72,12 @@ Address range may be a single number or a range in the form XXX-YYY{}
 If available, external 'keyconv' program will be used for address generation
 If available, external 'keyconv' program will be used for address generation
 
 
 Data for the --from-<what> options will be taken from <infile> if <infile>
 Data for the --from-<what> options will be taken from <infile> if <infile>
-is specified.  Otherwise, the user will be prompted to enter the data.  Note
-that passphrase data in a file may be arranged in free-form fashion, using
-any combination of spaces, tabs or newlines to separate words
+is specified.  Otherwise, the user will be prompted to enter the data.
+
+For passphrases all combinations of whitespace are equal, and leading and
+trailing space are ignored.  This permits reading passphrase data from a
+multi-line file with free spacing and indentation.  This is particularly
+convenient for long brainwallet passphrases, for example.
 
 
 BRAINWALLET NOTE:
 BRAINWALLET NOTE:
 
 
@@ -158,7 +161,7 @@ else:
 
 
 seed          = get_seed(infile,opts)
 seed          = get_seed(infile,opts)
 seed_id       = make_chksum_8(seed)
 seed_id       = make_chksum_8(seed)
-addr_data     = generate_addrs(seed, start, end, opts)
+addr_data     = generate_addrs(seed, range(start, end+1), opts)
 addr_data_str = format_addr_data(addr_data, seed_id, opts)
 addr_data_str = format_addr_data(addr_data, seed_id, opts)
 
 
 # Output data:
 # Output data:

+ 7 - 3
mmgen-addrimport

@@ -50,7 +50,12 @@ check_infile(cmd_args[0])
 
 
 seed_id,addr_data = parse_addrs_file(cmd_args[0])
 seed_id,addr_data = parse_addrs_file(cmd_args[0])
 
 
-from mmgen.tx import connect_to_bitcoind
+from mmgen.tx import check_wallet_addr_label,connect_to_bitcoind
+
+for i in addr_data:
+	i[2] = " ".join(i[2:]) if len(i) > 2 else ""
+	check_wallet_addr_label(i[2])
+
 c = connect_to_bitcoind(http_timeout=3600)
 c = connect_to_bitcoind(http_timeout=3600)
 
 
 message = """
 message = """
@@ -60,8 +65,7 @@ low-powered computer such as a netbook.
 confirm_or_exit(message, "continue", expect="YES")
 confirm_or_exit(message, "continue", expect="YES")
 
 
 for n,i in enumerate(addr_data):
 for n,i in enumerate(addr_data):
-	comment = " " + " ".join(i[2:]) if len(i) > 2 else ""
-	label = "%s:%s%s" % (seed_id,str(i[0]),comment)
+	label = "%s:%s %s" % (seed_id,str(i[0]),i[2])
 	msg("Importing %-6s %-34s (%s)" % (
 	msg("Importing %-6s %-34s (%s)" % (
 				("%s/%s:" % (n+1,len(addr_data))),
 				("%s/%s:" % (n+1,len(addr_data))),
 				i[1], label)
 				i[1], label)

+ 26 - 2
mmgen-txcreate

@@ -94,7 +94,30 @@ msg("Total amount to spend: %s BTC" % send_amt)
 msg("%s unspent outputs total" % len(unspent))
 msg("%s unspent outputs total" % len(unspent))
 
 
 while True:
 while True:
-	sel_unspent = select_outputs(unspent,"Choose the outputs to spend: ")
+	sel_nums = select_outputs(unspent,"Choose the outputs to spend: ")
+	sel_unspent = [unspent[i] for i in sel_nums]
+	mmgen_sel,other_sel = [],[]
+	for i in sel_nums:
+		if verify_mmgen_label(unspent[i].account):
+			mmgen_sel.append(i)
+		else:
+			other_sel.append(i)
+
+	if mmgen_sel and other_sel:
+		keygen_args = [unspent[i].account.split()[0][9:] for i in mmgen_sel]
+		msg("""
+NOTE: This transaction uses a mixture of both mmgen and non-mmgen inputs,
+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 mmgen-txsign's '-k' option.  Alternatively, you may import the
+mmgen keys into the wallet.dat of your offline bitcoind, first running
+mmgen-keygen with address list '%s' to generate the keys.  Finally, run
+mmgen-txsign with the '-f' option to force the use of wallet.dat as the
+key source.
+""".strip() % ",".join(sorted(keygen_args)))
+		if not user_confirm("Accept?"):
+			continue
+
 	total_in = trim_exponent(sum([o.amount for o in sel_unspent]))
 	total_in = trim_exponent(sum([o.amount for o in sel_unspent]))
 	change   = trim_exponent(total_in - (send_amt + tx_fee))
 	change   = trim_exponent(total_in - (send_amt + tx_fee))
 
 
@@ -102,8 +125,9 @@ while True:
 		prompt = "Transaction produces %s BTC in change.  OK?" % change
 		prompt = "Transaction produces %s BTC in change.  OK?" % change
 		if user_confirm(prompt,default_yes=True):
 		if user_confirm(prompt,default_yes=True):
 			break
 			break
+	else:
+		msg(txmsg['not_enough_btc'] % change)
 
 
-	msg(txmsg['not_enough_btc'] % change)
 
 
 if change > 0 and not change_addr:
 if change > 0 and not change_addr:
 	msg(txmsg['throwaway_change'] % (change, total_in-tx_fee))
 	msg(txmsg['throwaway_change'] % (change, total_in-tx_fee))

+ 1 - 1
mmgen-txsend

@@ -74,7 +74,7 @@ except:
 
 
 if not 'quiet' in opts: do_license_msg()
 if not 'quiet' in opts: do_license_msg()
 
 
-msg("Signed transaction file '%s' appears valid" % infile)
+msg("\nSigned transaction file '%s' appears valid" % infile)
 
 
 warn   = "Once this transaction is sent, there's no taking it back!"
 warn   = "Once this transaction is sent, there's no taking it back!"
 what   = "broadcast this transaction to the network"
 what   = "broadcast this transaction to the network"

+ 115 - 46
mmgen-txsign

@@ -26,47 +26,98 @@ from mmgen.Opts import *
 from mmgen.license import *
 from mmgen.license import *
 from mmgen.config import *
 from mmgen.config import *
 from mmgen.tx import *
 from mmgen.tx import *
-from mmgen.utils import check_opts, msg, user_confirm, check_infile, get_lines_from_file, my_getpass, my_raw_input
-
-prog_name = sys.argv[0].split("/")[-1]
+from mmgen.utils import *
 
 
 help_data = {
 help_data = {
-	'prog_name': prog_name,
+	'prog_name': sys.argv[0].split("/")[-1],
 	'desc':    "Sign a Bitcoin transaction generated by mmgen-txcreate",
 	'desc':    "Sign a Bitcoin transaction generated by mmgen-txcreate",
-	'usage':   "[opts] <transaction file>",
+	'usage':   "[opts] <transaction file> [mmgen wallet/seed/mnemonic file]",
 	'options': """
 	'options': """
 -h, --help               Print this help message
 -h, --help               Print this help message
 -d, --outdir          d  Specify an alternate directory 'd' for output
 -d, --outdir          d  Specify an alternate directory 'd' for output
 -e, --echo-passphrase    Print passphrase to screen when typing it
 -e, --echo-passphrase    Print passphrase to screen when typing it
+-f, --force-wallet-dat   Force the use of wallet.dat as a key source
 -i, --info               Display information about the transaction and exit
 -i, --info               Display information about the transaction and exit
+-k, --keys-from-file  k  Provide additional key data from file 'k'
 -q, --quiet              Suppress warnings; overwrite files without asking
 -q, --quiet              Suppress warnings; overwrite files without asking
-"""
+
+-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)
+-m, --from-mnemonic      Generate keys from an electrum-like mnemonic
+-s, --from-seed          Generate keys from a seed in .{} format
+
+Data for the --from-<what> options will be taken from a file if a second
+file is specified on the command line.  Otherwise, the user will be
+prompted to enter the data.
+""".format(seed_ext)
 }
 }
 
 
-short_opts = "hd:eiq"
-long_opts  = "help","outdir=","echo_passphrase","info","quiet"
+short_opts = "hd:efik:qb:ms"
+long_opts  = "help","outdir=","echo_passphrase","force_wallet_dat","info",\
+		  "keys_from_file=","quiet","from_brain=","from_mnemonic","from_seed"
 
 
 opts,cmd_args = process_opts(sys.argv,help_data,short_opts,long_opts)
 opts,cmd_args = process_opts(sys.argv,help_data,short_opts,long_opts)
 
 
 # Exits on invalid input
 # Exits on invalid input
-check_opts(opts, ('outdir',))
+check_opts(opts, ('outdir','from_brain'))
+if 'keys_from_file' in opts: check_infile(opts['keys_from_file'])
 
 
 if debug:
 if debug:
 	print "Processed options:     %s" % repr(opts)
 	print "Processed options:     %s" % repr(opts)
 	print "Cmd args:              %s" % repr(cmd_args)
 	print "Cmd args:              %s" % repr(cmd_args)
 
 
-if len(cmd_args) == 1:
-	infile = cmd_args[0]
-	check_infile(infile)
+if len(cmd_args) in (1,2):
+	tx_file = cmd_args[0]
+	check_infile(tx_file)
 else: usage(help_data)
 else: usage(help_data)
 
 
 # Begin execution
 # Begin execution
 
 
 c = connect_to_bitcoind()
 c = connect_to_bitcoind()
 
 
-tx_data = get_lines_from_file(infile,"transaction data")
-
-metadata,tx_hex,sig_data,inputs_data =  parse_tx_data(tx_data)
+tx_data = get_lines_from_file(tx_file,"transaction data")
+
+metadata,tx_hex,sig_data,inputs_data = parse_tx_data(tx_data,tx_file)
+
+# Are inputs mmgen addresses?
+infile,mmgen_addrs,other_addrs = "",[],[]
+# Check that all the seed IDs are the same:
+
+for i in inputs_data:
+	if verify_mmgen_label(i['account']):
+		mmgen_addrs.append(i)
+	else:
+		other_addrs.append(i)
+
+if mmgen_addrs:
+	a_ids = list(set([i['account'][:8] for i in mmgen_addrs]))
+	if len(a_ids) != 1:
+		msg("Addresses come from different seeds! (%s)" % " ".join(a_ids))
+		sys.exit(3)
+
+	if len(cmd_args) == 2:
+		infile = cmd_args[1]
+	else:
+		if "from_brain" in opts \
+			or "from_mnemonic" in opts \
+				or "from_seed" in opts:
+			infile = ""
+		else:
+			msg("Inputs contain mmgen addresses.  An MMGen wallet file must be specified on the command line (or use the '-b', '-m' or '-s' options).".strip())
+			sys.exit(2)
+
+	if other_addrs:
+		if 'keys_from_file' in opts:
+			add_keys = get_lines_from_file(opts['keys_from_file'])
+		else:
+			msg("""
+A key file must be supplied (option '-f') for the following non-mmgen
+address%s: %s""" % (
+	"" if len(other_addrs) == 1 else "es",
+	" ".join([i['address'] for i in other_addrs])
+	  ))
+			sys.exit(2)
 
 
 if 'info' in opts:
 if 'info' in opts:
 	view_tx_data(c,inputs_data,tx_hex,metadata)
 	view_tx_data(c,inputs_data,tx_hex,metadata)
@@ -74,46 +125,64 @@ if 'info' in opts:
 
 
 if not 'quiet' in opts and not 'info' in opts: do_license_msg()
 if not 'quiet' in opts and not 'info' in opts: do_license_msg()
 
 
-msg("Successfully opened transaction file '%s'" % infile)
+msg("Successfully opened transaction file '%s'" % tx_file)
 
 
 if user_confirm("View transaction data? ",default_yes=False):
 if user_confirm("View transaction data? ",default_yes=False):
 	view_tx_data(c,inputs_data,tx_hex,metadata)
 	view_tx_data(c,inputs_data,tx_hex,metadata)
 
 
-prompt = "Enter passphrase for bitcoind wallet: "
-if 'echo_passphrase' in opts:
-	password = my_raw_input(prompt)
+if mmgen_addrs:
+	seed    = get_seed(infile,opts)
+	seed_id = make_chksum_8(seed)
+
+	if seed_id != a_ids[0]:
+		msg("Seed ID of wallet (%s) doesn't match that of addresses (%s)"
+				% (seed_id,a_ids[0]))
+		sys.exit(3)
+	addr_nums = [int(i['account'].split()[0][9:]) for i in mmgen_addrs]
+	from mmgen.addr import generate_addrs
+	o = {'no_addresses': True, 'gen_what': "keys"}
+	keys = [i['wif'] for i in generate_addrs(seed, addr_nums, o)]
+	if other_addrs: keys += add_keys
+	try:
+		sig_tx = c.signrawtransaction(tx_hex,sig_data,keys)
+	except:
+		msg("Failed to sign transaction")
+		sys.exit(3)
 else:
 else:
-	password = my_getpass(prompt)
-
-wallet_enc = True
-from bitcoinrpc import exceptions
-
-try:
-	c.walletpassphrase(password, 9999)
-except exceptions.WalletWrongEncState:
-	msg("Wallet is unencrypted")
-	wallet_enc = False
-except exceptions.WalletPassphraseIncorrect:
-	msg("Passphrase incorrect")
-	sys.exit(3)
-except exceptions.WalletAlreadyUnlocked:
-	msg("WARNING: Wallet already unlocked!")
-else:
-	msg("Passphrase OK")
+	prompt = "Enter passphrase for bitcoind wallet: "
+	if 'echo_passphrase' in opts:
+		password = my_raw_input(prompt)
+	else:
+		password = my_getpass(prompt)
+
+	wallet_enc = True
+	from mmgen.rpc import exceptions
+
+	try:
+		c.walletpassphrase(password, 9999)
+	except exceptions.WalletWrongEncState:
+		msg("Wallet is unencrypted")
+		wallet_enc = False
+	except exceptions.WalletPassphraseIncorrect:
+		msg("Passphrase incorrect")
+		sys.exit(3)
+	except exceptions.WalletAlreadyUnlocked:
+		msg("WARNING: Wallet already unlocked!")
+	else:
+		msg("Passphrase OK")
+
+	try:
+		sig_tx = c.signrawtransaction(tx_hex,sig_data)
+	except:
+		msg("Failed to sign transaction")
+		if wallet_enc:
+			c.walletlock()
+			msg("Locking wallet")
+		sys.exit(3)
 
 
-try:
-	sig_tx = c.signrawtransaction(tx_hex,sig_data)
-#	sig_tx = {'hex':"deadbeef0123456789ab",'complete':True}
-except:
-	msg("Failed to sign transaction")
 	if wallet_enc:
 	if wallet_enc:
 		c.walletlock()
 		c.walletlock()
 		msg("Locking wallet")
 		msg("Locking wallet")
-	sys.exit(3)
-
-if wallet_enc:
-	c.walletlock()
-	msg("Locking wallet")
 
 
 if sig_tx['complete']:
 if sig_tx['complete']:
 	msg("Signing completed")
 	msg("Signing completed")

+ 3 - 2
mmgen-walletchk

@@ -21,11 +21,12 @@ mmgen-walletchk: Check integrity of a mmgen deterministic wallet, display
 """
 """
 
 
 import sys
 import sys
-import mmgen.Opts as Opts
+from mmgen.Opts import *
 
 
 from mmgen.utils import *
 from mmgen.utils import *
 
 
 help_data = {
 help_data = {
+	'prog_name': sys.argv[0].split("/")[-1],
 	'desc':  """Check integrity of a %s deterministic wallet, display
 	'desc':  """Check integrity of a %s deterministic wallet, display
                     its information and export seed and mnemonic data."""\
                     its information and export seed and mnemonic data."""\
 					% proj_name,
 					% proj_name,
@@ -45,7 +46,7 @@ short_opts = "hd:emsSv"
 long_opts  = "help","outdir=","echo_passphrase","export_mnemonic",\
 long_opts  = "help","outdir=","echo_passphrase","export_mnemonic",\
 				"export_seed","stdout","verbose"
 				"export_seed","stdout","verbose"
 
 
-opts,cmd_args = Opts.process_opts(sys.argv,help_data,short_opts,long_opts)
+opts,cmd_args = process_opts(sys.argv,help_data,short_opts,long_opts)
 
 
 check_opts(opts, ('outdir',))
 check_opts(opts, ('outdir',))
 
 

+ 5 - 2
mmgen-walletgen

@@ -59,8 +59,11 @@ By default (i.e. when invoked without any of the '--from-<what>' options),
 
 
 Data for the --from-<what> options will be taken from <infile> if <infile>
 Data for the --from-<what> options will be taken from <infile> if <infile>
 is specified.  Otherwise, the user will be prompted to enter the data.
 is specified.  Otherwise, the user will be prompted to enter the data.
-Note that passphrase data may be input in free-form fashion, using any
-combination of spaces or tabs (or newlines, in a file) between words.
+
+For passphrases all combinations of whitespace are equal, and leading and
+trailing space are ignored.  This permits reading passphrase data from a
+multi-line file with free spacing and indentation.  This is particularly
+convenient for long brainwallet passphrases, for example.
 
 
 BRAINWALLET NOTE:
 BRAINWALLET NOTE:
 
 

+ 28 - 19
mmgen/addr.py

@@ -24,6 +24,7 @@ from hashlib import sha256, sha512
 from binascii import hexlify, unhexlify
 from binascii import hexlify, unhexlify
 
 
 from mmgen.bitcoin import numtowif
 from mmgen.bitcoin import numtowif
+from mmgen.config import *
 
 
 def test_for_keyconv():
 def test_for_keyconv():
 	"""
 	"""
@@ -47,15 +48,15 @@ address generation.
 		return True
 		return True
 
 
 
 
-def generate_addrs(seed, start, end, opts):
+def generate_addrs(seed, addrnums, opts):
 	"""
 	"""
 	generate_addresses(start, end, seed, opts)  => None
 	generate_addresses(start, end, seed, opts)  => None
 
 
-	Generate a series of Bitcoin addresses from start to end based on a
-	seed, optionally outputting secret keys
+	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
+	The 'keyconv' utility will be used for address generation if installed.
+	Otherwise an internal function is used
 
 
 	Supported options:
 	Supported options:
 		print_secret, no_addresses, no_keyconv, gen_what
 		print_secret, no_addresses, no_keyconv, gen_what
@@ -73,17 +74,16 @@ def generate_addrs(seed, start, end, opts):
 			from subprocess import Popen, PIPE
 			from subprocess import Popen, PIPE
 			keyconv = "keyconv"
 			keyconv = "keyconv"
 
 
-	total_addrs = end - start + 1
+	a,t_addrs,i,out = sorted(addrnums),len(addrnums),0,[]
 
 
-	addrlist = []
+	while a:
+		seed = sha512(seed).digest(); i += 1   # round /i/
+		if i < a[0]: continue
 
 
-	for i in range(1, end+1):
-		seed = sha512(seed).digest() # round /i/
+		a.pop(0)
 
 
-		if i < start: continue
-
-		sys.stderr.write("\rGenerating %s: %s of %s" %
-			(opts['gen_what'], (i-start)+1, total_addrs))
+		sys.stderr.write("\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/
 		# Secret key is double sha256 of seed hash round /i/
 		sec = sha256(sha256(seed).digest()).hexdigest()
 		sec = sha256(sha256(seed).digest()).hexdigest()
@@ -98,18 +98,18 @@ def generate_addrs(seed, start, end, opts):
 		if not 'no_addresses' in opts:
 		if not 'no_addresses' in opts:
 			if keyconv:
 			if keyconv:
 				p = Popen([keyconv, wif], stdout=PIPE)
 				p = Popen([keyconv, wif], stdout=PIPE)
-				addr = dict([j.split() for j in p.stdout.readlines()])['Address:']
+				addr = dict([j.split() for j in \
+						p.stdout.readlines()])['Address:']
 			else:
 			else:
 				addr = privnum2addr(int(sec,16))
 				addr = privnum2addr(int(sec,16))
 
 
 			el['addr'] = addr
 			el['addr'] = addr
 
 
-		addrlist.append(el)
+		out.append(el)
 
 
-	sys.stderr.write("\rGenerated %s %s-%s%s\n" %
-		(opts['gen_what'], start, end, " "*9))
+	sys.stderr.write("\rGenerated %s %s%s\n"%(t_addrs,opts['gen_what']," "*15))
 
 
-	return addrlist
+	return out
 
 
 
 
 def format_addr_data(addrlist, seed_chksum, opts):
 def format_addr_data(addrlist, seed_chksum, opts):
@@ -139,7 +139,16 @@ def format_addr_data(addrlist, seed_chksum, opts):
 			(5 if 'print_secret' in opts else 1) + len(wif_msg)
 			(5 if 'print_secret' in opts else 1) + len(wif_msg)
 		)
 		)
 
 
-	data = []
+	header = """
+# MMGen address file
+#
+# This file is editable.
+# Everything following a hash symbol '#' is ignored.
+# A label may be added to the right of each address, and it will be
+# appended to the bitcoind wallet label upon import (max. 24 characters,
+# allowed characters: A-Za-z0-9, plus '{}').
+""".format("', '".join(wallet_addr_label_symbols)).strip()
+	data = [header + "\n"]
 	data.append("%s {" % seed_chksum.upper())
 	data.append("%s {" % seed_chksum.upper())
 
 
 	for el in addrlist:
 	for el in addrlist:

+ 1 - 0
mmgen/config.py

@@ -48,3 +48,4 @@ hash_presets = {
 	'4': [15, 8, 12],
 	'4': [15, 8, 12],
 	'5': [16, 8, 16],
 	'5': [16, 8, 16],
 }
 }
+wallet_addr_label_symbols = ".","_",",","-"," "

+ 57 - 11
mmgen/tx.py

@@ -20,7 +20,7 @@ tx.py:  Bitcoin transaction routines
 """
 """
 
 
 from binascii import unhexlify
 from binascii import unhexlify
-from mmgen.utils import msg,msg_r,write_to_file,my_raw_input,get_char,make_chksum_8,make_timestamp
+from mmgen.utils import *
 import sys, os
 import sys, os
 from decimal import Decimal
 from decimal import Decimal
 from mmgen.config import *
 from mmgen.config import *
@@ -110,7 +110,7 @@ def get_cfg_options(cfg_keys):
 def print_tx_to_file(tx,sel_unspent,send_amt,opts):
 def print_tx_to_file(tx,sel_unspent,send_amt,opts):
 	sig_data = [{"txid":i.txid,"vout":i.vout,"scriptPubKey":i.scriptPubKey}
 	sig_data = [{"txid":i.txid,"vout":i.vout,"scriptPubKey":i.scriptPubKey}
 					for i in sel_unspent]
 					for i in sel_unspent]
-	tx_id = make_chksum_8(unhexlify(tx))
+	tx_id = make_chksum_6(unhexlify(tx)).upper()
 	outfile = "%s[%s].tx" % (tx_id,send_amt)
 	outfile = "%s[%s].tx" % (tx_id,send_amt)
 	if 'outdir' in opts:
 	if 'outdir' in opts:
 		outfile = "%s/%s" % (opts['outdir'], outfile)
 		outfile = "%s/%s" % (opts['outdir'], outfile)
@@ -124,7 +124,7 @@ def print_tx_to_file(tx,sel_unspent,send_amt,opts):
 
 
 
 
 def print_signed_tx_to_file(tx,sig_tx,metadata,opts):
 def print_signed_tx_to_file(tx,sig_tx,metadata,opts):
-	tx_id = make_chksum_8(unhexlify(tx))
+	tx_id = make_chksum_6(unhexlify(tx)).upper()
 	outfile = "{}[{}].txsig".format(*metadata[:2])
 	outfile = "{}[{}].txsig".format(*metadata[:2])
 	if 'outdir' in opts:
 	if 'outdir' in opts:
 		outfile = "%s/%s" % (opts['outdir'], outfile)
 		outfile = "%s/%s" % (opts['outdir'], outfile)
@@ -148,10 +148,11 @@ def sort_and_view(unspent):
 		return cmp("%s %03s" % (a.txid,a.vout), "%s %03s" % (b.txid,b.vout))
 		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_addr(a,b): return cmp(a.address,b.address)
 	def s_age(a,b):  return cmp(b.confirmations,a.confirmations)
 	def s_age(a,b):  return cmp(b.confirmations,a.confirmations)
+	def s_mmgen(a,b): return cmp(a.account,b.account)
 
 
 	fs =     " %-4s %-11s %-2s %-34s %13s %-s"
 	fs =     " %-4s %-11s %-2s %-34s %13s %-s"
 	fs_hdr = " %-4s %-11s %-4s %-35s %-9s %-s"
 	fs_hdr = " %-4s %-11s %-4s %-35s %-9s %-s"
-	sort,group,reverse = "",False,False
+	sort,group,mmaddr,reverse = "",False,False,False
 
 
 	from copy import deepcopy
 	from copy import deepcopy
 	msg("")
 	msg("")
@@ -178,7 +179,16 @@ def sort_and_view(unspent):
 		for n,i in enumerate(out):
 		for n,i in enumerate(out):
 			amt = str(trim_exponent(i.amount))
 			amt = str(trim_exponent(i.amount))
 			fill = 8 - len(amt.split(".")[-1]) if "." in amt else 9
 			fill = 8 - len(amt.split(".")[-1]) if "." in amt else 9
-			addr = " |" + "-"*32 if i.skip == "d" else i.address
+			if i.skip == "d":
+				addr = " |" + "-"*32
+			else:
+				if mmaddr:
+					if i.account and verify_mmgen_label(i.account):
+						addr = "%s.. %s" % (i.address[:4],i.account)
+					else:
+						addr = i.address
+				else:
+					addr = i.address
 			txid = "       |---" if i.skip == "t" else i.txid[:8]+"..."
 			txid = "       |---" if i.skip == "t" else i.txid[:8]+"..."
 			days = int(i.confirmations * mins_per_block / (60*24))
 			days = int(i.confirmations * mins_per_block / (60*24))
 
 
@@ -187,24 +197,44 @@ def sort_and_view(unspent):
 		while True:
 		while True:
 			reply = get_char("\n".join(output) +
 			reply = get_char("\n".join(output) +
 """\n
 """\n
-Sort options: [t]xid, [a]mount, a[d]dress, [A]ge, [r]everse, [g]roup
+Sort options: [t]xid, [a]mount, a[d]dress, [A]ge, [r]everse, [M]mgen addr
+View options: [g]roup, show [m]mgen addr
 (Type 'q' to quit sorting): """).strip()
 (Type 'q' to quit sorting): """).strip()
 			if   reply == 'a': unspent.sort(s_amt);  sort = "amount"; break
 			if   reply == 'a': unspent.sort(s_amt);  sort = "amount"; break
 			elif reply == 't': unspent.sort(s_txid); sort = "txid"; break
 			elif reply == 't': unspent.sort(s_txid); sort = "txid"; break
 			elif reply == 'd': unspent.sort(s_addr); sort = "address"; break
 			elif reply == 'd': unspent.sort(s_addr); sort = "address"; break
 			elif reply == 'A': unspent.sort(s_age);  sort = "age"; break
 			elif reply == 'A': unspent.sort(s_age);  sort = "age"; break
+			elif reply == 'M': unspent.sort(s_mmgen); mmaddr,sort=True,"mmgen"; break
 			elif reply == 'r': 
 			elif reply == 'r': 
 				reverse = False if reverse else True
 				reverse = False if reverse else True
 				unspent.reverse()
 				unspent.reverse()
 				break
 				break
 			elif reply == 'g': group = False if group else True; break
 			elif reply == 'g': group = False if group else True; break
+			elif reply == 'm': mmaddr = False if mmaddr else True; break
 			elif reply == 'q': break
 			elif reply == 'q': break
 			else: msg("Invalid input")
 			else: msg("Invalid input")
 
 
 		msg("\n")
 		msg("\n")
 		if reply == 'q': break
 		if reply == 'q': break
 
 
-	return unspent
+	return tuple(unspent)
+
+
+def verify_mmgen_label(s,return_str=False):
+
+	fail    = "" if return_str else False
+	success = s  if return_str else True
+
+	if not s: return fail
+
+	label = s.split()[0]
+	if label[8] != ':': return fail
+	for i in label[:8]:
+		if not i in "01234567890ABCDEF": return fail
+	for i in label[9:]:
+		if not i in "0123456789": return fail
+
+	return success
 
 
 
 
 def view_tx_data(c,inputs_data,tx_hex,metadata=[]):
 def view_tx_data(c,inputs_data,tx_hex,metadata=[]):
@@ -226,10 +256,11 @@ def view_tx_data(c,inputs_data,tx_hex,metadata=[]):
 				msg(" " + """
 				msg(" " + """
 %-2s tx,vout: %s,%s
 %-2s tx,vout: %s,%s
     address:        %s
     address:        %s
+    label:          %s
     amount:         %s BTC
     amount:         %s BTC
     confirmations:  %s (around %s days)
     confirmations:  %s (around %s days)
 """.strip() %
 """.strip() %
-	(n+1,i['txid'],i['vout'],j['address'],
+	(n+1,i['txid'],i['vout'],j['address'],verify_mmgen_label(j['account'],True),
 		trim_exponent(j['amount']),j['confirmations'],days)+"\n")
 		trim_exponent(j['amount']),j['confirmations'],days)+"\n")
 				break
 				break
 
 
@@ -251,7 +282,7 @@ def view_tx_data(c,inputs_data,tx_hex,metadata=[]):
 	msg("TX fee:       %s BTC\n" % trim_exponent(total_in-total_out))
 	msg("TX fee:       %s BTC\n" % trim_exponent(total_in-total_out))
 
 
 
 
-def parse_tx_data(tx_data):
+def parse_tx_data(tx_data,infile):
 
 
 	if len(tx_data) != 4:
 	if len(tx_data) != 4:
 		msg("'%s': not a transaction file" % infile)
 		msg("'%s': not a transaction file" % infile)
@@ -288,7 +319,7 @@ def select_outputs(unspent,prompt):
 	while True:
 	while True:
 		reply = my_raw_input(prompt).strip()
 		reply = my_raw_input(prompt).strip()
 		if reply:
 		if reply:
-			selected = ()
+			selected = []
 			try:
 			try:
 				selected = [int(i) - 1 for i in reply.split()]
 				selected = [int(i) - 1 for i in reply.split()]
 			except: pass
 			except: pass
@@ -302,7 +333,7 @@ def select_outputs(unspent,prompt):
 
 
 		msg("'%s': Invalid input" % reply)
 		msg("'%s': Invalid input" % reply)
 
 
-	return [unspent[i] for i in selected]
+	return list(set(selected))
 
 
 
 
 def make_tx_out(rcpt_arg):
 def make_tx_out(rcpt_arg):
@@ -323,3 +354,18 @@ def make_tx_out(rcpt_arg):
 		sys.exit(3)
 		sys.exit(3)
 
 
 	return tx_out
 	return tx_out
+
+def check_wallet_addr_label(label):
+
+	if len(label) > 16:
+		msg("'%s': illegal label (length must be <= 16 characters)" % label)
+		sys.exit(3)
+
+	from string import ascii_letters, digits
+	chrs = tuple(ascii_letters + digits) + wallet_addr_label_symbols
+	for ch in list(label):
+		if ch not in chrs:
+			msg("'%s': illegal character in label '%s'" % (ch,label))
+			msg("Permitted characters: A-Za-z0-9, plus '%s'" %
+					"', '".join(wallet_addr_label_symbols))
+			sys.exit(3)

+ 7 - 7
mmgen/utils.py

@@ -250,7 +250,7 @@ def make_chksum_8(s):
 	from hashlib import sha256
 	from hashlib import sha256
 	return sha256(sha256(s).digest()).hexdigest()[:8].upper()
 	return sha256(sha256(s).digest()).hexdigest()[:8].upper()
 
 
-def _make_chksum_6(s):
+def make_chksum_6(s):
 	from hashlib import sha256
 	from hashlib import sha256
 	return sha256(s).hexdigest()[:6]
 	return sha256(s).hexdigest()[:6]
 
 
@@ -438,7 +438,7 @@ def write_seed(seed, opts):
 
 
 	from mmgen.bitcoin import b58encode_pad
 	from mmgen.bitcoin import b58encode_pad
 	data = col4(b58encode_pad(seed))
 	data = col4(b58encode_pad(seed))
-	chk = _make_chksum_6(b58encode_pad(seed))
+	chk = make_chksum_6(b58encode_pad(seed))
 
 
 	o = "%s %s\n" % (chk,data)
 	o = "%s %s\n" % (chk,data)
 
 
@@ -522,11 +522,11 @@ def write_wallet_to_file(seed, passwd, key_id, salt, enc_seed, opts):
 		label,
 		label,
 		"{} {} {} {} {}".format(*metadata),
 		"{} {} {} {} {}".format(*metadata),
 		"{}: {} {} {}".format(hash_preset,*_get_hash_params(hash_preset)),
 		"{}: {} {} {}".format(hash_preset,*_get_hash_params(hash_preset)),
-		"{} {}".format(_make_chksum_6(sf),  col4(sf)),
-		"{} {}".format(_make_chksum_6(esf), col4(esf))
+		"{} {}".format(make_chksum_6(sf),  col4(sf)),
+		"{} {}".format(make_chksum_6(esf), col4(esf))
 	)
 	)
 
 
-	chk = _make_chksum_6(" ".join(lines))
+	chk = make_chksum_6(" ".join(lines))
 
 
 	confirm = False if 'quiet' in opts else True
 	confirm = False if 'quiet' in opts else True
 	write_to_file(outfile, "\n".join((chk,)+lines)+"\n", confirm)
 	write_to_file(outfile, "\n".join((chk,)+lines)+"\n", confirm)
@@ -593,7 +593,7 @@ def	check_wallet_format(infile, lines, opts):
 
 
 
 
 def _check_chksum_6(chk,val,desc,infile):
 def _check_chksum_6(chk,val,desc,infile):
-	comp_chk = _make_chksum_6(val)
+	comp_chk = make_chksum_6(val)
 	if chk != comp_chk:
 	if chk != comp_chk:
 		msg("%s checksum incorrect in file '%s'!" % (desc,infile))
 		msg("%s checksum incorrect in file '%s'!" % (desc,infile))
 		msg("Checksum: %s. Computed value: %s" % (chk,comp_chk))
 		msg("Checksum: %s. Computed value: %s" % (chk,comp_chk))
@@ -682,7 +682,7 @@ def get_seed_from_seed_data(words):
 	stored_chk = words[0]
 	stored_chk = words[0]
 	seed_b58 = "".join(words[1:])
 	seed_b58 = "".join(words[1:])
 
 
-	chk = _make_chksum_6(seed_b58)
+	chk = make_chksum_6(seed_b58)
 	msg_r("Validating %s checksum..." % seed_ext)
 	msg_r("Validating %s checksum..." % seed_ext)
 
 
 	if compare_checksums(chk, "from seed", stored_chk, "from input"):
 	if compare_checksums(chk, "from seed", stored_chk, "from input"):