Browse Source

OO rewrite mostly done
Colored output

philemon 8 years ago
parent
commit
680ea8a5fc

+ 387 - 331
mmgen/addr.py

@@ -20,422 +20,478 @@
 addr.py:  Address generation/display routines for the MMGen suite
 addr.py:  Address generation/display routines for the MMGen suite
 """
 """
 
 
-from hashlib import sha256, sha512, new as hashlib_new
-from binascii import hexlify, unhexlify
-
+from hashlib import sha256, sha512
 from mmgen.common import *
 from mmgen.common import *
 from mmgen.bitcoin import numtowif
 from mmgen.bitcoin import numtowif
-from mmgen.tx import *
 from mmgen.obj import *
 from mmgen.obj import *
+from mmgen.tx import *
+from mmgen.tw import *
 
 
 pnm = g.proj_name
 pnm = g.proj_name
 
 
-addrmsgs = {
-	'too_many_acct_addresses': """
-ERROR: More than one address found for account: '%s'.
-Your 'wallet.dat' file appears to have been altered by a non-{pnm} program.
-Please restore your tracking wallet from a backup or create a new one and
-re-import your addresses.
-""".strip().format(pnm=pnm),
-	'addrfile_header': """
-# {pnm} address file
-#
-# This file is editable.
-# Everything following a hash symbol '#' is a comment and ignored by {pnm}.
-# A text label of {n} characters or less may be added to the right of each
-# address, and it will be appended to the bitcoind wallet label upon import.
-# The label may contain any printable ASCII symbol.
-""".strip().format(n=g.max_addr_label_len,pnm=pnm),
-	'keyfile_header': """
-# {pnm} key file
-#
-# This file is editable.
-# Everything following a hash symbol '#' is a comment and ignored by {pnm}.
-""".strip().format(pnm=pnm),
-	'no_keyconv_msg': """
+def test_for_keyconv(silent=False):
+	no_keyconv_errmsg = """
 Executable '{kconv}' unavailable. Falling back on (slow) internal ECDSA library.
 Executable '{kconv}' unavailable. Falling back on (slow) internal ECDSA library.
 Please install '{kconv}' from the {vgen} package on your system for much
 Please install '{kconv}' from the {vgen} package on your system for much
 faster address generation.
 faster address generation.
 """.format(kconv=g.keyconv_exec, vgen='vanitygen')
 """.format(kconv=g.keyconv_exec, vgen='vanitygen')
-}
-
-def test_for_keyconv(silent=False):
 
 
 	from subprocess import check_output,STDOUT
 	from subprocess import check_output,STDOUT
 	try:
 	try:
 		check_output([g.keyconv_exec, '-G'],stderr=STDOUT)
 		check_output([g.keyconv_exec, '-G'],stderr=STDOUT)
 	except:
 	except:
-		if not silent: msg(addrmsgs['no_keyconv_msg'])
+		if not silent: msg(no_keyconv_errmsg)
 		return False
 		return False
 
 
 	return True
 	return True
 
 
+class AddrListEntry(MMGenListItem):
+	attrs = 'idx','addr','label','wif','sec'
+	label = MMGenListItemAttr('label','MMGenAddrLabel')
+	idx   = MMGenListItemAttr('idx','AddrIdx')
+
+class AddrListChksum(str,Hilite):
+	color = 'pink'
+	trunc_ok = False
+	def __new__(cls,addrlist):
+		lines=[' '.join([str(e.idx),e.addr]+([e.wif] if addrlist.has_keys else []))
+						for e in addrlist.data]
+		return str.__new__(cls,make_chksum_N(' '.join(lines), nchars=16, sep=True))
+
+class AddrListID(str,Hilite):
+	color = 'green'
+	trunc_ok = False
+	def __new__(cls,addrlist):
+		try: int(addrlist.data[0].idx)
+		except:
+			s = '(no idxs)'
+		else:
+			idxs = [e.idx for e in addrlist.data]
+			prev = idxs[0]
+			ret = prev,
+			for i in idxs[1:]:
+				if i == prev + 1:
+					if i == idxs[-1]: ret += '-', i
+				else:
+					if prev != ret[-1]: ret += '-', prev
+					ret += ',', i
+				prev = i
+			s = ''.join([str(i) for i in ret])
+		return str.__new__(cls,'%s[%s]' % (addrlist.seed_id,s))
+
+class AddrList(MMGenObject): # Address info for a single seed ID
+	msgs = {
+	'file_header': """
+# {pnm} address file
+#
+# This file is editable.
+# Everything following a hash symbol '#' is a comment and ignored by {pnm}.
+# A text label of {n} characters or less may be added to the right of each
+# address, and it will be appended to the bitcoind wallet label upon import.
+# The label may contain any printable ASCII symbol.
+""".strip().format(n=g.max_addr_label_len,pnm=pnm),
+	'record_chksum': """
+Record this checksum: it will be used to verify the address file in the future
+""".strip(),
+	'check_chksum': 'Check this value against your records',
+	'removed_dups': """
+Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file
+""".strip().format(pnm=pnm)
+	}
+	data_desc = 'address'
+	file_desc = 'addresses'
+	gen_desc = 'address'
+	gen_desc_pl = 'es'
+	gen_addrs = True
+	gen_keys = False
+	has_keys = False
+	ext      = 'addrs'
+
+	def __init__(self,addrfile='',sid='',adata=[],seed='',addr_idxs='',src='',
+					addrlist='',keylist='',do_chksum=True,chksum_only=False):
+
+		self.update_msgs()
+
+		if addrfile:             # data from MMGen address file
+			(sid,adata) = self.parse_file(addrfile)
+		elif sid and adata:      # data from tracking wallet
+			do_chksum = False
+		elif seed and addr_idxs: # data from seed + idxs
+			sid,src = seed.sid,'gen'
+			adata = self.generate(seed,addr_idxs)
+		elif addrlist:           # data from flat address list
+			sid = None
+			adata = [AddrListEntry(addr=a) for a in addrlist]
+		elif keylist:            # data from flat key list
+			sid,do_chksum = None,False
+			adata = [AddrListEntry(wif=k) for k in keylist]
+		elif seed or addr_idxs:
+			die(3,'Must specify both seed and addr indexes')
+		elif sid or adata:
+			die(3,'Must specify both seed_id and adata')
+		else:
+			die(3,'Incorrect arguments for %s' % type(self).__name__)
+
+		# sid,adata now set
+		self.seed_id = sid
+		self.data = adata
+		self.num_addrs = len(adata)
+		self.fmt_data = ''
+		self.id_str = None
+		self.chksum = None
+
+		if type(self) == KeyList:
+			self.id_str = AddrListID(self)
+			return
 
 
-def generate_addrs(seed, addrnums, source='addrgen'):
+		if do_chksum:
+			self.chksum = AddrListChksum(self)
+			if chksum_only:
+				Msg(self.chksum)
+			else:
+				self.id_str = AddrListID(self)
+				qmsg('Checksum for %s data %s: %s' %
+						(self.data_desc,self.id_str.hl(),self.chksum.hl()))
+				qmsg(self.msgs[('check_chksum','record_chksum')[src=='gen']])
+
+	def update_msgs(self):
+		if type(self).msgs and type(self) != AddrList:
+			for k in AddrList.msgs:
+				if k not in self.msgs:
+					self.msgs[k] = AddrList.msgs[k]
+
+	def generate(self,seed,addrnums):
+		assert type(addrnums) is AddrIdxList
+		self.seed_id = SeedID(seed=seed)
+		seed = seed.get_data()
+
+		if self.gen_addrs:
+			if opt.no_keyconv or test_for_keyconv() == False:
+				msg('Using (slow) internal ECDSA library for address generation')
+				from mmgen.bitcoin import privnum2addr
+				keyconv = False
+			else:
+				from subprocess import check_output
+				keyconv = 'keyconv'
 
 
-	from util import make_chksum_8
-	seed_id = make_chksum_8(seed) # Must do this before seed gets clobbered
+		t_addrs,num,pos,out = len(addrnums),0,0,[]
 
 
-	if 'a' in opt.gen_what:
-		if opt.no_keyconv or test_for_keyconv() == False:
-			msg('Using (slow) internal ECDSA library for address generation')
-			from mmgen.bitcoin import privnum2addr
-			keyconv = False
-		else:
-			from subprocess import check_output
-			keyconv = 'keyconv'
+		while pos != t_addrs:
+			seed = sha512(seed).digest()
+			num += 1 # round
 
 
-	addrnums = sorted(set(addrnums)) # don't trust the calling function
-	t_addrs,num,pos,out = len(addrnums),0,0,[]
+			if num != addrnums[pos]: continue
 
 
-	w = {
-		'ka': ('key/address pair','s'),
-		'k':  ('key','s'),
-		'a':  ('address','es')
-	}[opt.gen_what]
+			pos += 1
 
 
-	from mmgen.addr import AddrInfoEntry,AddrInfo
+			qmsg_r('\rGenerating %s #%s (%s of %s)' % (self.gen_desc,num,pos,t_addrs))
 
 
-	while pos != t_addrs:
-		seed = sha512(seed).digest()
-		num += 1 # round
+			e = AddrListEntry(idx=num)
 
 
-		if num != addrnums[pos]: continue
+			# Secret key is double sha256 of seed hash round /num/
+			sec = sha256(sha256(seed).digest()).hexdigest()
+			wif = numtowif(int(sec,16))
 
 
-		pos += 1
+			if self.gen_addrs:
+				if keyconv:
+					e.addr = check_output([keyconv, wif]).split()[1]
+				else:
+					e.addr = privnum2addr(int(sec,16))
 
 
-		qmsg_r('\rGenerating %s #%s (%s of %s)' % (w[0],num,pos,t_addrs))
+			if self.gen_keys:
+				e.wif = wif
+				if opt.b16: e.sec = sec
 
 
-		e = AddrInfoEntry()
-		e.idx = num
+			out.append(e)
 
 
-		# Secret key is double sha256 of seed hash round /num/
-		sec = sha256(sha256(seed).digest()).hexdigest()
-		wif = numtowif(int(sec,16))
+		qmsg('\r%s: %s %s%s generated%s' % (
+				self.seed_id.hl(),t_addrs,self.gen_desc,suf(t_addrs,self.gen_desc_pl),' '*15))
+		return out
 
 
-		if 'a' in opt.gen_what:
-			if keyconv:
-				e.addr = check_output([keyconv, wif]).split()[1]
-			else:
-				e.addr = privnum2addr(int(sec,16))
+	def encrypt(self):
+		from mmgen.crypto import mmgen_encrypt
+		self.fmt_data = mmgen_encrypt(self.fmt_data,'new key list','')
+		self.ext += '.'+g.mmenc_ext
 
 
-		if 'k' in opt.gen_what: e.wif = wif
-		if opt.b16: e.sec = sec
+	def write_to_file(self,ask_tty=True,ask_write_default_yes=False):
+		fn = '{}.{}'.format(self.id_str,self.ext)
+		ask_tty = self.has_keys and not opt.quiet
+		write_data_to_file(fn,self.fmt_data,self.file_desc,ask_tty=ask_tty)
 
 
-		out.append(e)
+	def idxs(self):
+		return [e.idx for e in self.data]
 
 
-	m = w[0] if t_addrs == 1 else w[0]+w[1]
-	qmsg('\r%s: %s %s generated%s' % (seed_id,t_addrs,m,' '*15))
-	a = AddrInfo(has_keys='k' in opt.gen_what, source=source)
-	a.initialize(seed_id,out)
-	return a
+	def addrs(self):
+		return ['%s:%s'%(self.seed_id,e.idx) for e in self.data]
 
 
-def _parse_addrfile_body(lines,has_keys=False,check=False):
+	def addrpairs(self):
+		return [(e.idx,e.addr) for e in self.data]
 
 
-	if has_keys and len(lines) % 2:
-		return 'Key-address file has odd number of lines'
+	def btcaddrs(self):
+		return [e.addr for e in self.data]
 
 
-	ret = []
-	while lines:
-		a = AddrInfoEntry()
-		l = lines.pop(0)
-		d = l.split(None,2)
+	def comments(self):
+		return [e.label for e in self.data]
 
 
-		if not is_mmgen_idx(d[0]):
-			return "'%s': invalid address num. in line: '%s'" % (d[0],l)
-		if not is_btc_addr(d[1]):
-			return "'%s': invalid Bitcoin address" % d[1]
+	def entry(self,idx):
+		for e in self.data:
+			if idx == e.idx: return e
 
 
-		if len(d) == 3: check_addr_label(d[2])
-		else:           d.append('')
+	def btcaddr(self,idx):
+		for e in self.data:
+			if idx == e.idx: return e.addr
 
 
-		a.idx,a.addr,a.comment = int(d[0]),unicode(d[1]),unicode(d[2])
+	def comment(self,idx):
+		for e in self.data:
+			if idx == e.idx: return e.label
 
 
-		if has_keys:
+	def set_comment(self,idx,comment):
+		for e in self.data:
+			if idx == e.idx:
+				e.label = comment
+
+	def make_reverse_dict(self,btcaddrs):
+		d,b = {},btcaddrs
+		for e in self.data:
+			try:
+				d[b[b.index(e.addr)]] = ('%s:%s'%(self.seed_id,e.idx),e.label)
+			except: pass
+		return d
+
+	def flat_list(self):
+		class AddrListFlatEntry(AddrListEntry):
+			attrs = 'mmid','addr','wif'
+		return [AddrListFlatEntry(
+					mmid='{}:{}'.format(self.seed_id,e.idx),
+					addr=e.addr,
+					wif=e.wif)
+						for e in self.data]
+
+	def remove_dups(self,cmplist,key='wif'):
+		pop_list = []
+		for n,d in enumerate(self.data):
+			if getattr(d,key) == None: continue
+			for e in cmplist.data:
+				if getattr(e,key) and getattr(e,key) == getattr(d,key):
+					pop_list.append(n)
+		for n in reversed(pop_list): self.data.pop(n)
+		if pop_list:
+			vmsg(self.msgs['removed_dups'] % (len(pop_list),suf(removed,'k')))
+
+	def add_wifs(self,al_key):
+		for d in self.data:
+			for e in al_key.data:
+				if e.addr and e.wif and e.addr == d.addr:
+					d.wif = e.wif
+
+	def list_missing(self,key):
+		return [d for d in self.data if not getattr(d,key)]
+
+	def get(self,key):
+		return [getattr(d,key) for d in self.data if getattr(d,key)]
+
+	def get_addrs(self): return self.get('addr')
+	def get_wifs(self):  return self.get('wif')
+
+	def generate_addrs(self):
+		wif2addr_f = get_wif2addr_f()
+		d = self.data
+		for n,e in enumerate(d,1):
+			qmsg_r('\rGenerating addresses from keylist: %s/%s' % (n,len(d)))
+			e.addr = wif2addr_f(e.wif)
+		qmsg('\rGenerated addresses from keylist: %s/%s ' % (n,len(d)))
+
+	def format(self,enable_comments=False):
+
+		def check_attrs(key,desc):
+			for e in self.data:
+				if not getattr(e,key):
+					die(3,'missing %s in addr data' % desc)
+
+		if type(self) != KeyList: check_attrs('addr','addresses')
+		if self.has_keys:
+			if opt.b16: check_attrs('sec','hex keys')
+			check_attrs('wif','wif keys')
+
+		out = [self.msgs['file_header']+'\n']
+		if self.chksum:
+			out.append('# {} data checksum for {}: {}'.format(
+						self.data_desc.capitalize(),self.id_str,self.chksum))
+			out.append('# Record this value to a secure location.\n')
+		out.append('%s {' % self.seed_id)
+
+		fs = '  {:<%s}  {:<34}{}' % len(str(self.data[-1].idx))
+		for e in self.data:
+			c = ' '+e.label if enable_comments and e.label else ''
+			if type(self) == KeyList:
+				out.append(fs.format(e.idx, 'wif: '+e.wif,c))
+			else: # First line with idx
+				out.append(fs.format(e.idx, e.addr,c))
+				if self.has_keys:
+					if opt.b16: out.append(fs.format('', 'hex: '+e.sec,c))
+					out.append(fs.format('', 'wif: '+e.wif,c))
+
+		out.append('}')
+		self.fmt_data = '\n'.join([l.rstrip() for l in out]) + '\n'
+
+	def parse_file_body(self,lines):
+
+		if self.has_keys and len(lines) % 2:
+			return 'Key-address file has odd number of lines'
+
+		ret = []
+		while lines:
 			l = lines.pop(0)
 			l = lines.pop(0)
 			d = l.split(None,2)
 			d = l.split(None,2)
 
 
-			if d[0] != 'wif:':
-				return "Invalid key line in file: '%s'" % l
-			if not is_wif(d[1]):
-				return "'%s': invalid Bitcoin key" % d[1]
+			if not is_mmgen_idx(d[0]):
+				return "'%s': invalid address num. in line: '%s'" % (d[0],l)
+			if not is_btc_addr(d[1]):
+				return "'%s': invalid Bitcoin address" % d[1]
 
 
-			a.wif = unicode(d[1])
+			if len(d) != 3: d.append('')
 
 
-		ret.append(a)
+			a = AddrListEntry(idx=int(d[0]),addr=d[1],label=d[2])
 
 
-	if has_keys and keypress_confirm('Check key-to-address validity?'):
-		wif2addr_f = get_wif2addr_f()
-		llen = len(ret)
-		for n,e in enumerate(ret):
-			msg_r('\rVerifying keys %s/%s' % (n+1,llen))
-			if e.addr != wif2addr_f(e.wif):
-				return "Key doesn't match address!\n  %s\n  %s" % (e.wif,e.addr)
-		msg(' - done')
+			if self.has_keys:
+				l = lines.pop(0)
+				d = l.split(None,2)
 
 
-	return ret
+				if d[0] != 'wif:':
+					return "Invalid key line in file: '%s'" % l
+				if not is_wif(d[1]):
+					return "'%s': invalid Bitcoin key" % d[1]
 
 
+				a.wif = d[1]
 
 
-def _parse_addrfile(fn,buf=[],has_keys=False,exit_on_error=True):
+			ret.append(a)
 
 
-	if buf: lines = remove_comments(buf.splitlines()) # DOS-safe
-	else:   lines = get_lines_from_file(fn,'address data',trim_comments=True)
+		if self.has_keys and keypress_confirm('Check key-to-address validity?'):
+			wif2addr_f = get_wif2addr_f()
+			llen = len(ret)
+			for n,e in enumerate(ret):
+				msg_r('\rVerifying keys %s/%s' % (n+1,llen))
+				if e.addr != wif2addr_f(e.wif):
+					return "Key doesn't match address!\n  %s\n  %s" % (e.wif,e.addr)
+			msg(' - done')
 
 
-	try:
-		sid,obrace = lines[0].split()
-	except:
-		errmsg = "Invalid first line: '%s'" % lines[0]
-	else:
-		cbrace = lines[-1]
-		if obrace != '{':
-			errmsg = "'%s': invalid first line" % lines[0]
-		elif cbrace != '}':
-			errmsg = "'%s': invalid last line" % cbrace
-		elif not is_mmgen_seed_id(sid):
-			errmsg = "'%s': invalid Seed ID" % sid
-		else:
-			ret = _parse_addrfile_body(lines[1:-1],has_keys)
-			if type(ret) == list: return sid,ret
-			else: errmsg = ret
+		return ret
 
 
-	if exit_on_error: die(3,errmsg)
-	else:             return False
+	def parse_file(self,fn,buf=[],exit_on_error=True):
 
 
+		lines = get_lines_from_file(fn,self.data_desc+' data',trim_comments=True)
 
 
-def _parse_keyaddr_file(fn):
-	from mmgen.crypto import mmgen_decrypt_file_maybe
-	d = mmgen_decrypt_file_maybe(fn,'key-address file')
-	return _parse_addrfile('',buf=d,has_keys=True,exit_on_error=False)
+		try:
+			sid,obrace = lines[0].split()
+		except:
+			errmsg = "Invalid first line: '%s'" % lines[0]
+		else:
+			cbrace = lines[-1]
+			if obrace != '{':
+				errmsg = "'%s': invalid first line" % lines[0]
+			elif cbrace != '}':
+				errmsg = "'%s': invalid last line" % cbrace
+			elif not is_mmgen_seed_id(sid):
+				errmsg = "'%s': invalid Seed ID" % sid
+			else:
+				ret = self.parse_file_body(lines[1:-1])
+				if type(ret) == list:
+					return sid,ret
+				else:
+					errmsg = ret
 
 
+		if exit_on_error: die(3,errmsg)
+		msg(errmsg)
+		return False
 
 
-class AddrInfoList(MMGenObject):
+class KeyAddrList(AddrList):
+	data_desc = 'key-address'
+	file_desc = 'secret keys'
+	gen_desc = 'key/address pair'
+	gen_desc_pl = 's'
+	gen_addrs = True
+	gen_keys = True
+	has_keys = True
+	ext      = 'akeys'
+
+class KeyList(AddrList):
+	msgs = {
+	'file_header': """
+# {pnm} key file
+#
+# This file is editable.
+# Everything following a hash symbol '#' is a comment and ignored by {pnm}.
+""".strip().format(pnm=pnm)
+	}
+	data_desc = 'key'
+	file_desc = 'secret keys'
+	gen_desc = 'key'
+	gen_desc_pl = 's'
+	gen_addrs = False
+	gen_keys = True
+	has_keys = True
+	ext      = 'keys'
+
+
+class AddrData(MMGenObject):
+	msgs = {
+	'too_many_acct_addresses': """
+ERROR: More than one address found for account: '%s'.
+Your 'wallet.dat' file appears to have been altered by a non-{pnm} program.
+Please restore your tracking wallet from a backup or create a new one and
+re-import your addresses.
+""".strip().format(pnm=pnm)
+	}
 
 
-	def __init__(self,addrinfo=None,bitcoind_connection=None):
-		self.data = {}
-		if bitcoind_connection:
-			self.add_wallet_data(bitcoind_connection)
+	def __init__(self,source=None):
+		self.sids = {}
+		if source == 'tw': self.add_tw_data()
 
 
 	def seed_ids(self):
 	def seed_ids(self):
-		return self.data.keys()
+		return self.sids.keys()
 
 
-	def addrinfo(self,sid):
+	def addrlist(self,sid):
 		# TODO: Validate sid
 		# TODO: Validate sid
-		if sid in self.data:
-			return self.data[sid]
+		if sid in self.sids:
+			return self.sids[sid]
 
 
 	def mmaddr2btcaddr(self,mmaddr):
 	def mmaddr2btcaddr(self,mmaddr):
 		btcaddr = ''
 		btcaddr = ''
 		sid,idx = mmaddr.split(':')
 		sid,idx = mmaddr.split(':')
 		if sid in self.seed_ids():
 		if sid in self.seed_ids():
-			btcaddr = self.addrinfo(sid).btcaddr(int(idx))
+			btcaddr = self.addrlist(sid).btcaddr(int(idx))
 		return btcaddr
 		return btcaddr
 
 
-	def add_wallet_data(self,c):
-		vmsg_r('Getting account data from wallet...')
+	def add_tw_data(self):
+		vmsg_r('Getting address data from tracking wallet...')
+		c = bitcoin_connection()
 		accts = c.listaccounts(0,True)
 		accts = c.listaccounts(0,True)
 		data,i = {},0
 		data,i = {},0
 		alists = c.getaddressesbyaccount([[k] for k in accts],batch=True)
 		alists = c.getaddressesbyaccount([[k] for k in accts],batch=True)
 		for acct,addrlist in zip(accts,alists):
 		for acct,addrlist in zip(accts,alists):
-			ma,comment = parse_mmgen_label(acct)
-			if ma:
+			maddr,label = parse_tw_acct_label(acct)
+			if maddr:
 				i += 1
 				i += 1
-#				addrlist = c.getaddressesbyaccount(acct)
 				if len(addrlist) != 1:
 				if len(addrlist) != 1:
-					die(2,addrmsgs['too_many_acct_addresses'] % acct)
-				seed_id,idx = ma.split(':')
+					die(2,self.msgs['too_many_acct_addresses'] % acct)
+				seed_id,idx = maddr.split(':')
 				if seed_id not in data:
 				if seed_id not in data:
 					data[seed_id] = []
 					data[seed_id] = []
-				a = AddrInfoEntry()
-				a.idx,a.addr,a.comment = \
-					int(idx),unicode(addrlist[0]),unicode(comment)
-				data[seed_id].append(a)
+				data[seed_id].append(AddrListEntry(idx=idx,addr=addrlist[0],label=label))
 		vmsg('{n} {pnm} addresses found, {m} accounts total'.format(
 		vmsg('{n} {pnm} addresses found, {m} accounts total'.format(
 				n=i,pnm=pnm,m=len(accts)))
 				n=i,pnm=pnm,m=len(accts)))
 		for sid in data:
 		for sid in data:
-			self.add(AddrInfo(sid=sid,adata=data[sid]))
+			self.add(AddrList(sid=sid,adata=data[sid]))
 
 
-	def add(self,addrinfo):
-		if type(addrinfo) == AddrInfo:
-			self.data[addrinfo.seed_id] = addrinfo
+	def add(self,addrlist):
+		if type(addrlist) == AddrList:
+			self.sids[addrlist.seed_id] = addrlist
 			return True
 			return True
 		else:
 		else:
-			die(1,'Error: object %s is not of type AddrInfo' % repr(addrinfo))
+			raise TypeError, 'Error: object %s is not of type AddrList' % repr(addrlist)
 
 
 	def make_reverse_dict(self,btcaddrs):
 	def make_reverse_dict(self,btcaddrs):
 		d = {}
 		d = {}
-		for k in self.data.keys():
-			d.update(self.data[k].make_reverse_dict(btcaddrs))
+		for sid in self.sids:
+			d.update(self.sids[sid].make_reverse_dict(btcaddrs))
 		return d
 		return d
-
-class AddrInfoEntry(MMGenObject):
-
-	def __init__(self): pass
-
-class AddrInfo(MMGenObject):
-
-	def __init__(self,addrfile='',has_keys=False,sid='',adata=[],source='',caller=''):
-		self.has_keys = has_keys
-		self.caller = caller
-		do_chksum = True
-		if addrfile:
-			f = (_parse_addrfile,_parse_keyaddr_file)[bool(has_keys)]
-			sid,adata = f(addrfile)
-			self.source = 'addrfile'
-		elif sid and adata: # data from wallet
-			self.source = 'wallet'
-		elif sid or adata:
-			die(3,'Must specify address file, or seed_id + adata')
-		else:
-			self.source = source if source else 'unknown'
-			return
-
-		self.initialize(sid,adata)
-
-	def initialize(self,seed_id,addrdata):
-		if seed_id in self.__dict__:
-			msg('Seed ID already set for object %s' % self)
-			return False
-		self.seed_id = seed_id
-		self.addrdata = addrdata
-		self.num_addrs = len(addrdata)
-		if self.source in ('wallet','txsign'):
-			self.checksum = None
-			self.idxs_fmt = None
-		elif self.source == 'addrgen' and opt.gen_what == 'k':
-			self.checksum = None
-			self.fmt_addr_idxs()
-		else: # self.source in addrfile, addrgen
-			self.make_addrdata_chksum()
-			if self.caller == 'tool':
-				Msg(self.checksum)
-			else:
-				self.fmt_addr_idxs()
-				w = ('address','key-address')[bool(self.has_keys)]
-				qmsg('Checksum for %s data %s[%s]: %s' %
-						(w,self.seed_id,self.idxs_fmt,self.checksum))
-				if self.source == 'addrgen':
-					qmsg(
-	'Record this checksum: it will be used to verify the address file in the future')
-				elif self.source == 'addrfile':
-					qmsg('Check this value against your records')
-
-	def idxs(self):
-		return [e.idx for e in self.addrdata]
-
-	def addrs(self):
-		return ['%s:%s'%(self.seed_id,e.idx) for e in self.addrdata]
-
-	def addrpairs(self):
-		return [(e.idx,e.addr) for e in self.addrdata]
-
-	def btcaddrs(self):
-		return [e.addr for e in self.addrdata]
-
-	def comments(self):
-		return [e.comment for e in self.addrdata]
-
-	def entry(self,idx):
-		for e in self.addrdata:
-			if idx == e.idx: return e
-
-	def btcaddr(self,idx):
-		for e in self.addrdata:
-			if idx == e.idx: return e.addr
-
-	def comment(self,idx):
-		for e in self.addrdata:
-			if idx == e.idx: return e.comment
-
-	def set_comment(self,idx,comment):
-		for e in self.addrdata:
-			if idx == e.idx:
-				if is_valid_tx_comment(comment):
-					e.comment = comment
-				else:
-					sys.exit(2)
-
-	def make_reverse_dict(self,btcaddrs):
-		d,b = {},btcaddrs
-		for e in self.addrdata:
-			try:
-				d[b[b.index(e.addr)]] = ('%s:%s'%(self.seed_id,e.idx),e.comment)
-			except: pass
-		return d
-
-
-	def make_addrdata_chksum(self):
-		lines=[' '.join([str(e.idx),e.addr]+([e.wif] if self.has_keys else []))
-						for e in self.addrdata]
-		self.checksum = make_chksum_N(' '.join(lines), nchars=24, sep=True)
-
-
-	def fmt_data(self,enable_comments=False):
-		# Check data integrity - either all or none must exist for each attr
-		attrs  = ['addr','wif','sec']
-		status = [0,0,0]
-		for d in self.addrdata:
-			for j,attr in enumerate(attrs):
-				if hasattr(d,attr):
-					status[j] += 1
-
-		for i,s in enumerate(status):
-			if s != 0 and s != self.num_addrs:
-				die(3,'%s missing %s in addr data'% (self.num_addrs-s,attrs[i]))
-
-		if status[0] == status[1] == 0:
-			die(3,'Addr data contains neither addresses nor keys')
-
-		# Header
-		out = []
-		k = ('addrfile_header','keyfile_header')[status[0]==0]
-		out.append(addrmsgs[k]+'\n')
-		if self.checksum:
-			w = ('Key-address','Address')[status[1]==0]
-			out.append('# {} data checksum for {}[{}]: {}'.format(
-						w, self.seed_id, self.idxs_fmt, self.checksum))
-			out.append('# Record this value to a secure location.\n')
-		out.append('%s {' % self.seed_id)
-
-		# Body
-		fs = '  {:<%s}  {:<34}{}' % len(str(self.addrdata[-1].idx))
-		for e in self.addrdata:
-			c = ''
-			if enable_comments:
-				try:    c = ' '+e.comment
-				except: pass
-			if status[0]:  # First line with idx
-				out.append(fs.format(e.idx, e.addr,c))
-			else:
-				out.append(fs.format(e.idx, 'wif: '+e.wif,c))
-
-			if status[1]:   # Subsequent lines
-				if status[2]:
-					out.append(fs.format('', 'hex: '+e.sec,c))
-				if status[0]:
-					out.append(fs.format('', 'wif: '+e.wif,c))
-
-		out.append('}')
-
-		return '\n'.join([l.rstrip() for l in out]) + '\n'
-
-
-	def fmt_addr_idxs(self):
-
-		try: int(self.addrdata[0].idx)
-		except:
-			self.idxs_fmt = '(no idxs)'
-			return
-
-		addr_idxs = [e.idx for e in self.addrdata]
-		prev = addr_idxs[0]
-		ret = prev,
-
-		for i in addr_idxs[1:]:
-			if i == prev + 1:
-				if i == addr_idxs[-1]: ret += '-', i
-			else:
-				if prev != ret[-1]: ret += '-', prev
-				ret += ',', i
-			prev = i
-
-		self.idxs_fmt = ''.join([str(i) for i in ret])

+ 4 - 3
mmgen/bitcoin.py

@@ -120,10 +120,11 @@ def b58encode(s):
 def b58decode(b58num):
 def b58decode(b58num):
 	if b58num == '': return ''
 	if b58num == '': return ''
 	# Zap all spaces:
 	# Zap all spaces:
-	num = _b58tonum(b58num.translate(None,' \t\n\r'))
+	# Use translate() only with str, not unicode
+	num = _b58tonum(str(b58num).translate(None,' \t\n\r'))
 	if num == False: return False
 	if num == False: return False
-	out = '{:x}'.format(num)
-	return unhexlify('0'*(len(out)%2) + out)
+	out = u'{:x}'.format(num)
+	return unhexlify(u'0'*(len(out)%2) + out)
 
 
 # These yield bytewise equivalence in our special cases:
 # These yield bytewise equivalence in our special cases:
 
 

+ 0 - 9
mmgen/crypto.py

@@ -242,15 +242,6 @@ def mmgen_decrypt(data,desc='data',hash_preset=''):
 		msg('Incorrect passphrase or hash preset')
 		msg('Incorrect passphrase or hash preset')
 		return False
 		return False
 
 
-def mmgen_decrypt_file_maybe(fn,desc):
-	d = get_data_from_file(fn,'{} data'.format(desc),binary=True)
-	have_enc_ext = get_extension(fn) == g.mmenc_ext
-	if have_enc_ext or not is_ascii(d):
-		m = ('Attempting to decrypt','Decrypting')[have_enc_ext]
-		msg('%s %s %s' % (m,desc,fn))
-		d = mmgen_decrypt_retry(d,desc)
-	return d
-
 def mmgen_decrypt_retry(d,desc='data'):
 def mmgen_decrypt_retry(d,desc='data'):
 	while True:
 	while True:
 		d_dec = mmgen_decrypt(d,desc)
 		d_dec = mmgen_decrypt(d,desc)

+ 16 - 8
mmgen/filename.py

@@ -29,16 +29,24 @@ class Filename(MMGenObject):
 		self.name     = fn
 		self.name     = fn
 		self.dirname  = os.path.dirname(fn)
 		self.dirname  = os.path.dirname(fn)
 		self.basename = os.path.basename(fn)
 		self.basename = os.path.basename(fn)
-		self.ext      = None
-		self.ftype    = ftype
+		self.ext      = get_extension(fn)
+		self.ftype    = None # the file's associated class
 
 
-# This should be done before license msg instead
-#		check_infile(fn)
+		from mmgen.seed import SeedSource
+		if ftype:
+			if type(ftype) == type:
+				if issubclass(ftype,SeedSource):
+					self.ftype = ftype
+				# elif: # other MMGen file types
+				else:
+					die(3,"'%s': not a recognized file type for SeedSource" % ftype)
+			else:
+				die(3,"'%s': not a class" % ftype)
+		else:
+			self.ftype = SeedSource.ext_to_type(self.ext)
+			if not self.ftype:
+				die(3,"'%s': not a recognized extension for SeedSource" % self.ext)
 
 
-		if not ftype:
-			self.ext = get_extension(fn)
-			if not (self.ext):
-				die(2,"Unrecognized extension '.%s' for file '%s'" % (self.ext,fn))
 
 
 		# TODO: Check for Windows
 		# TODO: Check for Windows
 		mode = (os.O_RDONLY,os.O_RDWR)[bool(write)]
 		mode = (os.O_RDONLY,os.O_RDWR)[bool(write)]

+ 18 - 40
mmgen/globalvars.py

@@ -34,13 +34,11 @@ debug                = os.getenv('MMGEN_DEBUG')
 no_license           = os.getenv('MMGEN_NOLICENSE')
 no_license           = os.getenv('MMGEN_NOLICENSE')
 bogus_wallet_data    = os.getenv('MMGEN_BOGUS_WALLET_DATA')
 bogus_wallet_data    = os.getenv('MMGEN_BOGUS_WALLET_DATA')
 disable_hold_protect = os.getenv('MMGEN_DISABLE_HOLD_PROTECT')
 disable_hold_protect = os.getenv('MMGEN_DISABLE_HOLD_PROTECT')
+color = (False,True)[sys.stdout.isatty() and not os.getenv('MMGEN_DISABLE_COLOR')]
 
 
-btc_amt_decimal_places = 8
-
-from decimal import Decimal
-tx_fee        = Decimal('0.0003')
-max_tx_fee    = Decimal('0.01')
-tx_fee_adj    = Decimal('1.0')
+from mmgen.obj import BTCAmt
+tx_fee        = BTCAmt('0.0003')
+tx_fee_adj    = 1.0
 tx_confs      = 3
 tx_confs      = 3
 
 
 seed_len     = 256
 seed_len     = 256
@@ -58,7 +56,7 @@ version   = '0.8.4'
 required_opts = [
 required_opts = [
 	'quiet','verbose','debug','outdir','echo_passphrase','passwd_file',
 	'quiet','verbose','debug','outdir','echo_passphrase','passwd_file',
 	'usr_randchars','stdout','show_hash_presets','label',
 	'usr_randchars','stdout','show_hash_presets','label',
-	'keep_passphrase','keep_hash_preset','brain_params'
+	'keep_passphrase','keep_hash_preset','brain_params','b16'
 ]
 ]
 incompatible_opts = (
 incompatible_opts = (
 	('quiet','verbose'),
 	('quiet','verbose'),
@@ -66,46 +64,27 @@ incompatible_opts = (
 	('tx_id', 'info'),
 	('tx_id', 'info'),
 	('tx_id', 'terse_info'),
 	('tx_id', 'terse_info'),
 )
 )
-min_screen_width = 80
 
 
-wallet_ext    = 'mmdat'
-seed_ext      = 'mmseed'
-mn_ext        = 'mmwords'
-brain_ext     = 'mmbrain'
-incog_ext     = 'mmincog'
-incog_hex_ext = 'mmincox'
-
-seedfile_exts = (
-	wallet_ext, seed_ext, mn_ext, brain_ext, incog_ext, incog_hex_ext
-)
-
-rawtx_ext           = 'rawtx'
-sigtx_ext           = 'sigtx'
-txid_ext            = 'txid'
-addrfile_ext        = 'addrs'
-addrfile_chksum_ext = 'chk'
-keyfile_ext         = 'keys'
-keyaddrfile_ext     = 'akeys'
-mmenc_ext           = 'mmenc'
-
-default_wordlist    = 'electrum'
-#default_wordlist    = 'tirosh'
+min_screen_width = 80
 
 
 # Global value sets user opt
 # Global value sets user opt
-dfl_vars = 'seed_len','hash_preset','usr_randchars','debug','tx_fee','tx_confs','tx_fee_adj'
-
-seed_lens = 128,192,256
-mn_lens = [i / 32 * 3 for i in seed_lens]
+dfl_vars = 'seed_len','hash_preset','usr_randchars','debug','tx_confs','tx_fee_adj','tx_fee'
 
 
 keyconv_exec = 'keyconv'
 keyconv_exec = 'keyconv'
 
 
 mins_per_block   = 9
 mins_per_block   = 9
 passwd_max_tries = 5
 passwd_max_tries = 5
 
 
-max_urandchars,min_urandchars = 80,10
+max_urandchars = 80
+_x = os.getenv('MMGEN_MIN_URANDCHARS')
+min_urandchars = int(_x) if _x and int(_x) else 10
 
 
-salt_len      = 16
-aesctr_iv_len = 16
+seed_lens = 128,192,256
+mn_lens = [i / 32 * 3 for i in seed_lens]
+
+mmenc_ext      = 'mmenc'
+salt_len       = 16
+aesctr_iv_len  = 16
 hincog_chk_len = 8
 hincog_chk_len = 8
 
 
 hash_presets = {
 hash_presets = {
@@ -125,9 +104,8 @@ mmgen_idx_max_digits = 7
 
 
 printable_nonl = [chr(i+32) for i in range(95)]
 printable_nonl = [chr(i+32) for i in range(95)]
 printable = printable_nonl + ['\n','\t']
 printable = printable_nonl + ['\n','\t']
-
 addr_label_symbols = wallet_label_symbols = printable_nonl
 addr_label_symbols = wallet_label_symbols = printable_nonl
 
 
-max_addr_label_len = 32
+max_addr_label_len   = 32
 max_wallet_label_len = 48
 max_wallet_label_len = 48
-max_tx_comment_len = 72   # Comment is b58 encoded, so can permit all UTF-8
+max_tx_comment_len   = 72 # Comment is b58 encoded, so can permit UTF-8

+ 11 - 25
mmgen/main_addrgen.py

@@ -101,35 +101,21 @@ if len(cmd_args) < nargs and not (opt.hidden_incog_input_params or opt.in_fmt):
 elif len(cmd_args) > nargs - int(bool(opt.hidden_incog_input_params)):
 elif len(cmd_args) > nargs - int(bool(opt.hidden_incog_input_params)):
 	opts.usage()
 	opts.usage()
 
 
-addrlist_arg = cmd_args.pop()
-addr_idxs = parse_addr_idxs(addrlist_arg)
-if not addr_idxs:
-	die(1,"'%s': invalid address list argument" % addrlist_arg)
+addridxlist_str = cmd_args.pop()
+idxs = AddrIdxList(fmt_str=addridxlist_str)
 
 
 do_license_msg()
 do_license_msg()
 
 
-opt.gen_what = 'a' if gen_what == 'addresses' \
-					else 'k' if opt.no_addresses else 'ka'
+ss = SeedSource(*cmd_args) # *(cmd_args[0] if cmd_args else [])
 
 
-# Generate data:
-ss = SeedSource(*cmd_args)
+i = (gen_what=='addresses') or bool(opt.no_addresses)*2
+al = (KeyAddrList,AddrList,KeyList)[i](seed=ss.seed,addr_idxs=idxs)
+al.format()
 
 
-ainfo = generate_addrs(ss.seed.data,addr_idxs)
+if al.gen_addrs and opt.print_checksum:
+	Die(0,al.checksum)
 
 
-addrdata_str = ainfo.fmt_data()
-outfile_base = '{}[{}]'.format(ss.seed.sid, ainfo.idxs_fmt)
+if al.gen_keys and keypress_confirm('Encrypt key list?'):
+	al.encrypt()
 
 
-if 'a' in opt.gen_what and opt.print_checksum:
-	Die(0,ainfo.checksum)
-
-if 'k' in opt.gen_what and keypress_confirm('Encrypt key list?'):
-	addrdata_str = mmgen_encrypt(addrdata_str,'new key list','')
-	enc_ext = '.' + g.mmenc_ext
-else: enc_ext = ''
-
-ext = (g.keyfile_ext,g.keyaddrfile_ext)['ka' in opt.gen_what]
-ext = (g.addrfile_ext,ext)['k' in opt.gen_what]
-outfile = '%s.%s%s' % (outfile_base, ext, enc_ext)
-ask_tty = 'k' in opt.gen_what and not opt.quiet
-if gen_what == 'keys': gen_what = 'secret keys'
-write_data_to_file(outfile,addrdata_str,gen_what,ask_tty=ask_tty)
+al.write_to_file()

+ 7 - 12
mmgen/main_addrimport.py

@@ -23,7 +23,7 @@ mmgen-addrimport: Import addresses into a MMGen bitcoind tracking wallet
 import time
 import time
 
 
 from mmgen.common import *
 from mmgen.common import *
-from mmgen.addr import AddrInfo,AddrInfoEntry
+from mmgen.addr import AddrList,KeyAddrList
 
 
 opts_data = {
 opts_data = {
 	'desc': """Import addresses (both {pnm} and non-{pnm}) into a bitcoind
 	'desc': """Import addresses (both {pnm} and non-{pnm}) into a bitcoind
@@ -53,14 +53,9 @@ if len(cmd_args) == 1:
 	if opt.addrlist:
 	if opt.addrlist:
 		lines = get_lines_from_file(
 		lines = get_lines_from_file(
 			infile,'non-{pnm} addresses'.format(pnm=g.proj_name),trim_comments=True)
 			infile,'non-{pnm} addresses'.format(pnm=g.proj_name),trim_comments=True)
-		ai,adata = AddrInfo(),[]
-		for btcaddr in lines:
-			a = AddrInfoEntry()
-			a.idx,a.addr,a.comment = None,btcaddr,None
-			adata.append(a)
-		ai.initialize(None,adata)
+		ai = AddrList(addrlist=lines)
 	else:
 	else:
-		ai = AddrInfo(infile,has_keys=opt.keyaddr_file)
+		ai = (AddrList,KeyAddrList)[bool(opt.keyaddr_file)](infile)
 else:
 else:
 	die(1,"""
 	die(1,"""
 You must specify an {pnm} address file (or a list of non-{pnm} addresses
 You must specify an {pnm} address file (or a list of non-{pnm} addresses
@@ -69,7 +64,7 @@ with the '--addrlist' option)
 
 
 from mmgen.bitcoin import verify_addr
 from mmgen.bitcoin import verify_addr
 qmsg_r('Validating addresses...')
 qmsg_r('Validating addresses...')
-for e in ai.addrdata:
+for e in ai.data:
 	if not verify_addr(e.addr,verbose=True):
 	if not verify_addr(e.addr,verbose=True):
 		die(2,'%s: invalid address' % e.addr)
 		die(2,'%s: invalid address' % e.addr)
 
 
@@ -114,13 +109,13 @@ else:
 	msg_fmt = '\r%-{}s %-34s %s'.format(w_n_of_m, w_mmid)
 	msg_fmt = '\r%-{}s %-34s %s'.format(w_n_of_m, w_mmid)
 
 
 msg("Importing %s addresses from '%s'%s" %
 msg("Importing %s addresses from '%s'%s" %
-		(len(ai.addrdata),infile,('',' (batch mode)')[bool(opt.batch)]))
+		(len(ai.data),infile,('',' (batch mode)')[bool(opt.batch)]))
 
 
 arg_list = []
 arg_list = []
-for n,e in enumerate(ai.addrdata):
+for n,e in enumerate(ai.data):
 	if e.idx:
 	if e.idx:
 		label = '%s:%s' % (ai.seed_id,e.idx)
 		label = '%s:%s' % (ai.seed_id,e.idx)
-		if e.comment: label += ' ' + e.comment
+		if e.label: label += ' ' + e.label
 	else: label = 'non-{pnm}'.format(pnm=g.proj_name)
 	else: label = 'non-{pnm}'.format(pnm=g.proj_name)
 
 
 	if opt.batch:
 	if opt.batch:

+ 40 - 48
mmgen/main_txcreate.py

@@ -21,8 +21,6 @@ mmgen-txcreate: Create a Bitcoin transaction to and from MMGen- or non-MMGen
                 inputs and outputs
                 inputs and outputs
 """
 """
 
 
-from decimal import Decimal
-
 from mmgen.common import *
 from mmgen.common import *
 from mmgen.tx import *
 from mmgen.tx import *
 from mmgen.tw import *
 from mmgen.tw import *
@@ -83,13 +81,12 @@ No data for {pnm} address {mmgenaddr} could be found in the tracking wallet.
 Please import this address into your tracking wallet or supply an address file
 Please import this address into your tracking wallet or supply an address file
 for it on the command line.
 for it on the command line.
 """.strip(),
 """.strip(),
-	'mixed_inputs': """
-NOTE: This transaction uses a mixture of both {pnm} and non-{pnm} inputs, which
-makes the signing process more complicated.  When signing the transaction, keys
-for the non-{pnm} inputs must be supplied to '{pnl}-txsign' in a file with the
-'--keys-from-file' option.
-
-Selected mmgen inputs: %s
+	'non_mmgen_inputs': """
+NOTE: This transaction includes non-{pnm} inputs, which makes the signing
+process more complicated.  When signing the transaction, keys for non-{pnm}
+inputs must be supplied to '{pnl}-txsign' in a file with the '--keys-from-file'
+option.
+Selected non-{pnm} inputs: %s
 """.strip().format(pnm=pnm,pnl=pnm.lower()),
 """.strip().format(pnm=pnm,pnl=pnm.lower()),
 	'not_enough_btc': """
 	'not_enough_btc': """
 Not enough BTC in the inputs for this transaction (%s BTC)
 Not enough BTC in the inputs for this transaction (%s BTC)
@@ -100,7 +97,7 @@ was specified.
 """.strip(),
 """.strip(),
 }
 }
 
 
-def select_outputs(unspent,prompt):
+def select_unspent(unspent,prompt):
 
 
 	while True:
 	while True:
 		reply = my_raw_input(prompt).strip()
 		reply = my_raw_input(prompt).strip()
@@ -118,14 +115,14 @@ def select_outputs(unspent,prompt):
 		return selected
 		return selected
 
 
 
 
-def mmaddr2baddr(c,mmaddr,ail_w,ail_f):
+def mmaddr2baddr(c,mmaddr,ad_w,ad_f):
 
 
 	# assume mmaddr has already been checked
 	# assume mmaddr has already been checked
-	btc_addr = ail_w.mmaddr2btcaddr(mmaddr)
+	btc_addr = ad_w.mmaddr2btcaddr(mmaddr)
 
 
 	if not btc_addr:
 	if not btc_addr:
-		if ail_f:
-			btc_addr = ail_f.mmaddr2btcaddr(mmaddr)
+		if ad_f:
+			btc_addr = ad_f.mmaddr2btcaddr(mmaddr)
 			if btc_addr:
 			if btc_addr:
 				msg(wmsg['addr_in_addrfile_only'].format(mmgenaddr=mmaddr))
 				msg(wmsg['addr_in_addrfile_only'].format(mmgenaddr=mmaddr))
 				if not keypress_confirm('Continue anyway?'):
 				if not keypress_confirm('Continue anyway?'):
@@ -135,16 +132,16 @@ def mmaddr2baddr(c,mmaddr,ail_w,ail_f):
 		else:
 		else:
 			die(2,wmsg['addr_not_found_no_addrfile'].format(pnm=pnm,mmgenaddr=mmaddr))
 			die(2,wmsg['addr_not_found_no_addrfile'].format(pnm=pnm,mmgenaddr=mmaddr))
 
 
-	return btc_addr
+	return BTCAddr(btc_addr)
 
 
 
 
 def get_fee_estimate():
 def get_fee_estimate():
-	if 'tx_fee' in opt.set_by_user:
+	if 'tx_fee' in opt.set_by_user: # TODO
 		return None
 		return None
 	else:
 	else:
 		ret = c.estimatefee(opt.tx_confs)
 		ret = c.estimatefee(opt.tx_confs)
 		if ret != -1:
 		if ret != -1:
-			return ret
+			return BTCAmt(ret)
 		else:
 		else:
 			m = """
 			m = """
 Fee estimation failed!
 Fee estimation failed!
@@ -172,46 +169,41 @@ c = bitcoin_connection()
 if not opt.info:
 if not opt.info:
 	do_license_msg(immed=True)
 	do_license_msg(immed=True)
 
 
-	addrfiles = [a for a in cmd_args if get_extension(a) == g.addrfile_ext]
+	from mmgen.addr import AddrList,AddrData
+	addrfiles = [a for a in cmd_args if get_extension(a) == AddrList.ext]
 	cmd_args = set(cmd_args) - set(addrfiles)
 	cmd_args = set(cmd_args) - set(addrfiles)
 
 
-	from mmgen.addr import AddrInfo,AddrInfoList
-	ail_f = AddrInfoList()
+	ad_f = AddrData()
 	for a in addrfiles:
 	for a in addrfiles:
 		check_infile(a)
 		check_infile(a)
-		ail_f.add(AddrInfo(a))
+		ad_f.add(AddrList(a))
 
 
-	ail_w = AddrInfoList(bitcoind_connection=c)
+	ad_w = AddrData(source='tw')
 
 
 	for a in cmd_args:
 	for a in cmd_args:
 		if ',' in a:
 		if ',' in a:
-			a1,a2 = split2(a,',')
+			a1,a2 = a.split(',',1)
 			if is_btc_addr(a1):
 			if is_btc_addr(a1):
-				btc_addr = a1
-			elif is_mmgen_addr(a1):
-				btc_addr = mmaddr2baddr(c,a1,ail_w,ail_f)
+				btc_addr = BTCAddr(a1)
+			elif is_mmgen_id(a1):
+				btc_addr = mmaddr2baddr(c,a1,ad_w,ad_f)
 			else:
 			else:
 				die(2,"%s: unrecognized subargument in argument '%s'" % (a1,a))
 				die(2,"%s: unrecognized subargument in argument '%s'" % (a1,a))
-
-			btc_amt = convert_to_btc_amt(a2)
-			if btc_amt:
-				tx.add_output(btc_addr,btc_amt)
-			else:
-				die(2,"%s: invalid amount in argument '%s'" % (a2,a))
-		elif is_mmgen_addr(a) or is_btc_addr(a):
+			tx.add_output(btc_addr,BTCAmt(a2))
+		elif is_mmgen_id(a) or is_btc_addr(a):
 			if tx.change_addr:
 			if tx.change_addr:
 				die(2,'ERROR: More than one change address specified: %s, %s' %
 				die(2,'ERROR: More than one change address specified: %s, %s' %
 						(change_addr, a))
 						(change_addr, a))
-			tx.change_addr = a if is_btc_addr(a) else mmaddr2baddr(c,a,ail_w,ail_f)
-			tx.add_output(tx.change_addr,Decimal('0'))
+			tx.change_addr = mmaddr2baddr(c,a,ad_w,ad_f) if is_mmgen_id(a) else BTCAddr(a)
+			tx.add_output(tx.change_addr,BTCAmt('0'))
 		else:
 		else:
 			die(2,'%s: unrecognized argument' % a)
 			die(2,'%s: unrecognized argument' % a)
 
 
 	if not tx.outputs:
 	if not tx.outputs:
 		die(2,'At least one output must be specified on the command line')
 		die(2,'At least one output must be specified on the command line')
 
 
-	if opt.tx_fee > g.max_tx_fee:
-		die(2,'Transaction fee too large: %s > %s' % (opt.tx_fee,g.max_tx_fee))
+	if opt.tx_fee > tx.max_fee:
+		die(2,'Transaction fee too large: %s > %s' % (opt.tx_fee,tx.max_fee))
 
 
 	fee_estimate = get_fee_estimate()
 	fee_estimate = get_fee_estimate()
 
 
@@ -223,10 +215,10 @@ if opt.info: sys.exit()
 
 
 tx.send_amt = tx.sum_outputs()
 tx.send_amt = tx.sum_outputs()
 
 
-msg('Total amount to spend: %s' % ('Unknown','%s BTC'%tx.send_amt,)[bool(tx.send_amt)])
+msg('Total amount to spend: %s' % ('Unknown','%s BTC'%tx.send_amt.hl())[bool(tx.send_amt)])
 
 
 while True:
 while True:
-	sel_nums = select_outputs(tw.unspent,
+	sel_nums = select_unspent(tw.unspent,
 			'Enter a range or space-separated list of outputs to spend: ')
 			'Enter a range or space-separated list of outputs to spend: ')
 	msg('Selected output%s: %s' % (
 	msg('Selected output%s: %s' % (
 			('s','')[len(sel_nums)==1],
 			('s','')[len(sel_nums)==1],
@@ -234,31 +226,31 @@ while True:
 		))
 		))
 	sel_unspent = [tw.unspent[i-1] for i in sel_nums]
 	sel_unspent = [tw.unspent[i-1] for i in sel_nums]
 
 
-	mmaddrs = set([i['mmid'] for i in sel_unspent])
-
-	if '' in mmaddrs and len(mmaddrs) > 1:
-		mmaddrs.discard('')
-		msg(wmsg['mixed_inputs'] % ', '.join(sorted(mmaddrs)))
+	non_mmaddrs = [i for i in sel_unspent if i.mmid == None]
+	if non_mmaddrs:
+		msg(wmsg['non_mmgen_inputs'] % ', '.join(set(sorted([a.addr.hl() for a in non_mmaddrs]))))
 		if not keypress_confirm('Accept?'):
 		if not keypress_confirm('Accept?'):
 			continue
 			continue
 
 
-	tx.copy_inputs(sel_unspent)              # makes tx.inputs
+	tx.copy_inputs_from_tw(sel_unspent)      # makes tx.inputs
 
 
 	tx.calculate_size_and_fee(fee_estimate)  # sets tx.size, tx.fee
 	tx.calculate_size_and_fee(fee_estimate)  # sets tx.size, tx.fee
 
 
 	change_amt = tx.sum_inputs() - tx.send_amt - tx.fee
 	change_amt = tx.sum_inputs() - tx.send_amt - tx.fee
 
 
 	if change_amt >= 0:
 	if change_amt >= 0:
-		prompt = 'Transaction produces %s BTC in change.  OK?' % change_amt
+		prompt = 'Transaction produces %s BTC in change.  OK?' % change_amt.hl()
 		if keypress_confirm(prompt,default_yes=True):
 		if keypress_confirm(prompt,default_yes=True):
 			break
 			break
 	else:
 	else:
 		msg(wmsg['not_enough_btc'] % change_amt)
 		msg(wmsg['not_enough_btc'] % change_amt)
 
 
 if change_amt > 0:
 if change_amt > 0:
+	change_amt = BTCAmt(change_amt)
 	if not tx.change_addr:
 	if not tx.change_addr:
 		die(2,wmsg['throwaway_change'] % change_amt)
 		die(2,wmsg['throwaway_change'] % change_amt)
-	tx.add_output(tx.change_addr,change_amt)
+	tx.del_output(tx.change_addr)
+	tx.add_output(BTCAddr(tx.change_addr),change_amt)
 elif tx.change_addr:
 elif tx.change_addr:
 	msg('Warning: Change address will be unused as transaction produces no change')
 	msg('Warning: Change address will be unused as transaction produces no change')
 	tx.del_output(tx.change_addr)
 	tx.del_output(tx.change_addr)
@@ -270,7 +262,7 @@ dmsg('tx: %s' % tx)
 
 
 tx.add_comment()   # edits an existing comment
 tx.add_comment()   # edits an existing comment
 tx.create_raw(c)   # creates tx.hex, tx.txid
 tx.create_raw(c)   # creates tx.hex, tx.txid
-tx.add_mmaddrs_to_outputs(ail_w,ail_f)
+tx.add_mmaddrs_to_outputs(ad_w,ad_f)
 tx.add_timestamp()
 tx.add_timestamp()
 tx.add_blockcount(c)
 tx.add_blockcount(c)
 
 

+ 73 - 129
mmgen/main_txsign.py

@@ -21,8 +21,9 @@ mmgen-txsign: Sign a transaction generated by 'mmgen-txcreate'
 """
 """
 
 
 from mmgen.common import *
 from mmgen.common import *
+from mmgen.seed import *
 from mmgen.tx import *
 from mmgen.tx import *
-from mmgen.seed import SeedSource
+from mmgen.addr import *
 
 
 pnm = g.proj_name
 pnm = g.proj_name
 
 
@@ -86,28 +87,29 @@ mappings are verified.  Therefore, seed material or a key-address file for
 these addresses must be supplied on the command line.
 these addresses must be supplied on the command line.
 
 
 Seed data supplied in files must have the following extensions:
 Seed data supplied in files must have the following extensions:
-   wallet:      '.{g.wallet_ext}'
-   seed:        '.{g.seed_ext}'
-   mnemonic:    '.{g.mn_ext}'
-   brainwallet: '.{g.brain_ext}'
+   wallet:      '.{w.ext}'
+   seed:        '.{s.ext}'
+   mnemonic:    '.{m.ext}'
+   brainwallet: '.{b.ext}'
 
 
 FMT CODES:
 FMT CODES:
   {f}
   {f}
 """.format(
 """.format(
 		f='\n  '.join(SeedSource.format_fmt_codes().splitlines()),
 		f='\n  '.join(SeedSource.format_fmt_codes().splitlines()),
-		g=g,pnm=pnm,pnl=pnm.lower()
+		pnm=pnm,pnl=pnm.lower(),
+		w=Wallet,s=SeedFile,m=Mnemonic,b=Brainwallet
 	)
 	)
 }
 }
 
 
 wmsg = {
 wmsg = {
-	'mm2btc_mapping_error': """
+	'mapping_error': """
 {pnm} -> BTC address mappings differ!
 {pnm} -> BTC address mappings differ!
-From %-18s %s -> %s
-From %-18s %s -> %s
-""".strip().format(pnm=pnm),
-	'removed_dups': """
-Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file
+%-23s %s -> %s
+%-23s %s -> %s
 """.strip().format(pnm=pnm),
 """.strip().format(pnm=pnm),
+	'missing_keys_error': """
+A key file must be supplied for the following non-{pnm} address%s:\n    %s
+""".format(pnm=pnm).strip()
 }
 }
 
 
 def get_seed_for_seed_id(seed_id,infiles,saved_seeds):
 def get_seed_for_seed_id(seed_id,infiles,saved_seeds):
@@ -121,30 +123,47 @@ def get_seed_for_seed_id(seed_id,infiles,saved_seeds):
 		elif opt.in_fmt:
 		elif opt.in_fmt:
 			qmsg('Need seed data for Seed ID %s' % seed_id)
 			qmsg('Need seed data for Seed ID %s' % seed_id)
 			ss = SeedSource()
 			ss = SeedSource()
-			msg('User input produced Seed ID %s' % make_chksum_8(seed))
+			msg('User input produced Seed ID %s' % ss.seed.sid)
 		else:
 		else:
 			die(2,'ERROR: No seed source found for Seed ID: %s' % seed_id)
 			die(2,'ERROR: No seed source found for Seed ID: %s' % seed_id)
 
 
-		saved_seeds[ss.seed.sid] = ss.seed.data
-
-		if ss.seed.sid == seed_id: return ss.seed.data
+		saved_seeds[ss.seed.sid] = ss.seed
+		if ss.seed.sid == seed_id: return ss.seed
 
 
 def generate_keys_for_mmgen_addrs(mmgen_addrs,infiles,saved_seeds):
 def generate_keys_for_mmgen_addrs(mmgen_addrs,infiles,saved_seeds):
-
 	seed_ids = set([i[:8] for i in mmgen_addrs])
 	seed_ids = set([i[:8] for i in mmgen_addrs])
 	vmsg('Need seed%s: %s' % (suf(seed_ids,'k'),' '.join(seed_ids)))
 	vmsg('Need seed%s: %s' % (suf(seed_ids,'k'),' '.join(seed_ids)))
 	d = []
 	d = []
-
-	from mmgen.addr import generate_addrs
+	from mmgen.addr import KeyAddrList
 	for seed_id in seed_ids:
 	for seed_id in seed_ids:
 		# Returns only if seed is found
 		# Returns only if seed is found
 		seed = get_seed_for_seed_id(seed_id,infiles,saved_seeds)
 		seed = get_seed_for_seed_id(seed_id,infiles,saved_seeds)
-		addr_nums = [int(i[9:]) for i in mmgen_addrs if i[:8] == seed_id]
-		opt.gen_what = 'ka'
-		ai = generate_addrs(seed,addr_nums,source='txsign')
-		d += [('{}:{}'.format(seed_id,e.idx),e.addr,e.wif) for e in ai.addrdata]
+		addr_idxs = AddrIdxList(idx_list=[int(i[9:]) for i in mmgen_addrs if i[:8] == seed_id])
+		d += KeyAddrList(seed=seed,addr_idxs=addr_idxs,do_chksum=False).flat_list()
 	return d
 	return d
 
 
+def add_keys(tx,src,infiles=None,saved_seeds=None,keyaddr_list=None):
+	need_keys = [e for e in getattr(tx,src) if e.mmid and not e.have_wif]
+	if not need_keys: return []
+	desc,m1 = ('key-address file','From key-address file:') if keyaddr_list else \
+					('seed(s)','Generated from seed:')
+	qmsg('Checking {} -> BTC address mappings for {} (from {})'.format(pnm,src,desc))
+	d = keyaddr_list.flat_list() if keyaddr_list else \
+		generate_keys_for_mmgen_addrs([e.mmid for e in need_keys],infiles,saved_seeds)
+	new_keys = []
+	for e in need_keys:
+		for f in d:
+			if f.mmid == e.mmid:
+				if f.addr == e.addr:
+					e.have_wif = True
+					if src == 'inputs':
+						new_keys.append(f.wif)
+				else:
+					die(3,wmsg['mapping_error'] % (m1,f.mmid,f.addr,'tx file:',e.mmid,e.addr))
+	if new_keys:
+		vmsg('Added %s wif key%s from %s' % (len(new_keys),suf(new_keys,'k'),desc))
+	return new_keys
+
 # # function unneeded - use bitcoin-cli walletdump instead
 # # function unneeded - use bitcoin-cli walletdump instead
 # def sign_tx_with_bitcoind_wallet(c,tx,tx_num_str,keys):
 # def sign_tx_with_bitcoind_wallet(c,tx,tx_num_str,keys):
 # 	ok = tx.sign(c,tx_num_str,keys) # returns false on failure
 # 	ok = tx.sign(c,tx_num_str,keys) # returns false on failure
@@ -171,86 +190,6 @@ def generate_keys_for_mmgen_addrs(mmgen_addrs,infiles,saved_seeds):
 #
 #
 # 		return ok
 # 		return ok
 #
 #
-def missing_keys_errormsg(addrs):
-	Msg("""
-A key file must be supplied (or use the '--use-wallet-dat' option)
-for the following non-{pnm} address{suf}:\n    {l}""".format(
-	pnm=pnm, suf=suf(addrs,'a'), l='\n    '.join(addrs)).strip())
-
-def parse_mmgen_keyaddr_file():
-	from mmgen.addr import AddrInfo
-	ai = AddrInfo(opt.mmgen_keys_from_file,has_keys=True)
-	vmsg('Found %s wif key%s for Seed ID %s' %
-			(ai.num_addrs, suf(ai.num_addrs,'k'), ai.seed_id))
-	# idx: (0=addr, 1=comment 2=wif) -> mmaddr: (0=addr, 1=wif)
-	return dict(
-		[('%s:%s'%(ai.seed_id,e.idx), (e.addr,e.wif)) for e in ai.addrdata])
-
-def parse_keylist(key_data):
-	fn = opt.keys_from_file
-	from mmgen.crypto import mmgen_decrypt_file_maybe
-	dec = mmgen_decrypt_file_maybe(fn,'non-{} keylist file'.format(pnm))
-	# Key list could be bitcoind dump, so remove first space and everything following
-	keys_all = set([line.split()[0] for line in remove_comments(dec.splitlines())]) # DOS-safe
-	dmsg(repr(keys_all))
-	ka_keys = [d[k][1] for k in key_data['kafile']]
-	keys = [k for k in keys_all if k not in ka_keys]
-	removed = len(keys_all) - len(keys)
-	if removed:
-		vmsg(wmsg['removed_dups'] % (removed,suf(removed,'k')))
-	addrs = []
-	wif2addr_f = get_wif2addr_f()
-	for n,k in enumerate(keys,1):
-		qmsg_r('\rGenerating addresses from keylist: %s/%s' % (n,len(keys)))
-		addrs.append(wif2addr_f(k))
-	qmsg('\rGenerated addresses from keylist: %s/%s ' % (n,len(keys)))
-
-	return dict(zip(addrs,keys))
-
-# Check inputs and outputs maps against key-address file, deleting entries:
-def check_maps_from_kafile(io_map,desc,kadata,return_keys=False):
-	if not kadata: return []
-	qmsg('Checking {pnm} -> BTC address mappings for {w}s (from key-address file)'.format(pnm=pnm,w=desc))
-	ret = []
-	for k in io_map.keys():
-		if k in kadata:
-			if kadata[k][0] == io_map[k]:
-				del io_map[k]
-				ret += [kadata[k][1]]
-			else:
-				kl,il = 'key-address file:','tx file:'
-				die(2,wmsg['mm2btc_mapping_error']%(kl,k,kadata[k][0],il,k,io_map[k]))
-	if ret: vmsg('Removed %s address%s from %ss map' % (len(ret),suf(ret,'a'),desc))
-	if return_keys:
-		vmsg('Added %s wif key%s from %ss map' % (len(ret),suf(ret,'k'),desc))
-		return ret
-
-# Check inputs and outputs maps against values generated from seeds
-def check_maps_from_seeds(io_map,desc,infiles,saved_seeds,return_keys=False):
-
-	if not io_map: return []
-	qmsg('Checking {pnm} -> BTC address mappings for {w}s (from seed(s))'.format(
-				pnm=pnm,w=desc))
-	d = generate_keys_for_mmgen_addrs(io_map.keys(),infiles,saved_seeds)
-#	0=mmaddr 1=addr 2=wif
-	m = dict([(e[0],e[1]) for e in d])
-	for a,b in zip(sorted(m),sorted(io_map)):
-		if a != b:
-			al,bl = 'generated seed:','tx file:'
-			die(3,wmsg['mm2btc_mapping_error'] % (al,a,m[a],bl,b,io_map[b]))
-	if return_keys:
-		vmsg('Added %s wif key%s from seeds' % (len(d),suf(d,'k')))
-		return [e[2] for e in d]
-
-def get_keys_from_keylist(kldata,addrs):
-	ret = []
-	for addr in addrs[:]:
-		if addr in kldata:
-			ret += [kldata[addr]]
-			addrs.remove(addr)
-	vmsg('Added %s wif key%s from user-supplied keylist' %
-			(len(ret),suf(ret,'k')))
-	return ret
 
 
 # main(): execution begins here
 # main(): execution begins here
 
 
@@ -262,22 +201,27 @@ for i in infiles: check_infile(i)
 c = bitcoin_connection()
 c = bitcoin_connection()
 
 
 saved_seeds = {}
 saved_seeds = {}
-tx_files   = [i for i in infiles if get_extension(i) == g.rawtx_ext]
-seed_files = [i for i in infiles if get_extension(i) in g.seedfile_exts]
+tx_files   = [i for i in infiles if get_extension(i) == MMGenTX.raw_ext]
+seed_files = [i for i in infiles if get_extension(i) in SeedSource.get_extensions()]
 
 
 if not tx_files:
 if not tx_files:
 	die(1,'You must specify a raw transaction file!')
 	die(1,'You must specify a raw transaction file!')
-if not (seed_files or opt.mmgen_keys_from_file or opt.keys_from_file or opt.use_wallet_dat):
+if not (seed_files or opt.mmgen_keys_from_file or opt.keys_from_file): # or opt.use_wallet_dat):
 	die(1,'You must specify a seed or key source!')
 	die(1,'You must specify a seed or key source!')
 
 
 if not opt.info and not opt.terse_info:
 if not opt.info and not opt.terse_info:
 	do_license_msg(immed=True)
 	do_license_msg(immed=True)
 
 
-key_data = { 'kafile':{}, 'klfile':{} }
+kal,kl = None,None
 if opt.mmgen_keys_from_file:
 if opt.mmgen_keys_from_file:
-	key_data['kafile'] = parse_mmgen_keyaddr_file() or {}
+	kal = KeyAddrList(opt.mmgen_keys_from_file)
+
 if opt.keys_from_file:
 if opt.keys_from_file:
-	key_data['klfile'] = parse_keylist(key_data) or {}
+	l = get_lines_from_file(opt.keys_from_file,'key-address data',trim_comments=True)
+	kl = KeyAddrList(keylist=l)
+	if kal: kl.remove_dups(kal,key='wif')
+	kl.generate_addrs()
+# pp_die(kl)
 
 
 tx_num_str = ''
 tx_num_str = ''
 for tx_num,tx_file in enumerate(tx_files,1):
 for tx_num,tx_file in enumerate(tx_files,1):
@@ -301,26 +245,26 @@ for tx_num,tx_file in enumerate(tx_files,1):
 	tx.view_with_prompt('View data for transaction%s?' % tx_num_str)
 	tx.view_with_prompt('View data for transaction%s?' % tx_num_str)
 
 
 	# Start
 	# Start
-	other_addrs = list(set([i['address'] for i in tx.inputs if not i['mmid']]))
-
-	# should remove all elements from other_addrs
-	keys = get_keys_from_keylist(key_data['klfile'],other_addrs)
-
-	if other_addrs and not opt.use_wallet_dat:
-		missing_keys_errormsg(other_addrs)
-		sys.exit(2)
-
-	imap = dict([(i['mmid'],i['address']) for i in tx.inputs if i['mmid']])
-	omap = dict([(tx.outputs[k][1],k) for k in tx.outputs if len(tx.outputs[k]) > 1])
-	sids = set([i[:8] for i in imap])
-
-	keys += check_maps_from_kafile(imap,'input',key_data['kafile'],True)
-	check_maps_from_kafile(omap,'output',key_data['kafile'])
-
-	keys += check_maps_from_seeds(imap,'input',seed_files,saved_seeds,True)
-	check_maps_from_seeds(omap,'output',seed_files,saved_seeds)
-
-	extra_sids = set(saved_seeds) - sids
+	keys = []
+	non_mm_addrs = tx.get_non_mmaddrs('inputs')
+	if non_mm_addrs:
+		tmp = KeyAddrList(addrlist=non_mm_addrs,do_chksum=False)
+		tmp.add_wifs(kl)
+		m = tmp.list_missing('wif')
+		if m: die(2,wmsg['missing_keys_error'] % (suf(m,'es'),'\n    '.join(m)))
+		keys += tmp.get_wifs()
+
+	if opt.mmgen_keys_from_file:
+		keys += add_keys(tx,'inputs',keyaddr_list=kal)
+		add_keys(tx,'outputs',keyaddr_list=kal)
+
+	keys += add_keys(tx,'inputs',seed_files,saved_seeds)
+	add_keys(tx,'outputs',seed_files,saved_seeds)
+
+	tx.delete_attrs('inputs','have_wif')
+	tx.delete_attrs('outputs','have_wif')
+
+	extra_sids = set(saved_seeds) - tx.get_input_sids()
 	if extra_sids:
 	if extra_sids:
 		msg('Unused Seed ID%s: %s' %
 		msg('Unused Seed ID%s: %s' %
 			(suf(extra_sids,'k'),' '.join(extra_sids)))
 			(suf(extra_sids,'k'),' '.join(extra_sids)))

+ 8 - 2
mmgen/main_wallet.py

@@ -24,6 +24,7 @@ import os,re
 
 
 from mmgen.common import *
 from mmgen.common import *
 from mmgen.seed import SeedSource
 from mmgen.seed import SeedSource
+from mmgen.obj import MMGenWalletLabel
 
 
 bn = os.path.basename(sys.argv[0])
 bn = os.path.basename(sys.argv[0])
 invoked_as = re.sub(r'^wallet','',bn.split('-')[-1])
 invoked_as = re.sub(r'^wallet','',bn.split('-')[-1])
@@ -110,6 +111,9 @@ FMT CODES:
 
 
 cmd_args = opts.init(opts_data,opt_filter=opt_filter)
 cmd_args = opts.init(opts_data,opt_filter=opt_filter)
 
 
+if opt.label:
+	opt.label = MMGenWalletLabel(opt.label,msg="Error in option '--label'")
+
 if len(cmd_args) < nargs \
 if len(cmd_args) < nargs \
 		and not opt.hidden_incog_input_params and not opt.in_fmt:
 		and not opt.hidden_incog_input_params and not opt.in_fmt:
 	die(1,'An input file or input format must be specified')
 	die(1,'An input file or input format must be specified')
@@ -128,10 +132,12 @@ if invoked_as in ('conv','passchg'): msg(green('Processing input wallet'))
 ss_in = None if invoked_as == 'gen' \
 ss_in = None if invoked_as == 'gen' \
 			else SeedSource(*cmd_args,passchg=invoked_as=='passchg')
 			else SeedSource(*cmd_args,passchg=invoked_as=='passchg')
 
 
-if invoked_as == 'chk':
-	sys.exit()
+if invoked_as == 'chk': sys.exit()
 
 
 if invoked_as in ('conv','passchg'): msg(green('Processing output wallet'))
 if invoked_as in ('conv','passchg'): msg(green('Processing output wallet'))
 
 
 ss_out = SeedSource(ss=ss_in,passchg=invoked_as=='passchg')
 ss_out = SeedSource(ss=ss_in,passchg=invoked_as=='passchg')
+
+if invoked_as == 'gen': qmsg("This wallet's Seed ID: %s" % ss_out.seed.sid.hl())
+
 ss_out.write_to_file()
 ss_out.write_to_file()

+ 350 - 5
mmgen/obj.py

@@ -19,9 +19,8 @@
 """
 """
 obj.py:  The MMGenObject class and methods
 obj.py:  The MMGenObject class and methods
 """
 """
-import mmgen.globalvars as g
-from decimal import Decimal
 
 
+from decimal import *
 lvl = 0
 lvl = 0
 
 
 class MMGenObject(object):
 class MMGenObject(object):
@@ -40,11 +39,12 @@ class MMGenObject(object):
 		def conv(v,col_w):
 		def conv(v,col_w):
 			vret = ''
 			vret = ''
 			if type(v) in (str,unicode):
 			if type(v) in (str,unicode):
+				import mmgen.globalvars as g
 				if not (set(list(v)) <= set(list(g.printable))):
 				if not (set(list(v)) <= set(list(g.printable))):
 					vret = repr(v)
 					vret = repr(v)
 				else:
 				else:
 					vret = fix_linebreaks(v,fixed_indent=0)
 					vret = fix_linebreaks(v,fixed_indent=0)
-			elif type(v) in (int,long,Decimal):
+			elif type(v) in (int,long,BTCAmt):
 				vret = str(v)
 				vret = str(v)
 			elif type(v) == dict:
 			elif type(v) == dict:
 				sep = '\n{}{}'.format(indent,' '*4)
 				sep = '\n{}{}'.format(indent,' '*4)
@@ -68,8 +68,8 @@ class MMGenObject(object):
 
 
 		out = []
 		out = []
 		def f(k): return k[:2] != '__'
 		def f(k): return k[:2] != '__'
-		keys = filter(f, dir(self))
-		col_w = max(len(k) for k in keys)
+		keys = filter(f, self.__dict__.keys())
+		col_w = max(len(k) for k in keys) if keys else 1
 		fs = '{}%-{}s: %s'.format(indent,col_w)
 		fs = '{}%-{}s: %s'.format(indent,col_w)
 
 
 		methods = [k for k in keys if repr(getattr(self,k))[:14] == '<bound method ']
 		methods = [k for k in keys if repr(getattr(self,k))[:14] == '<bound method ']
@@ -91,3 +91,348 @@ class MMGenObject(object):
 				out.append(fs % (k, conv(val,col_w)))
 				out.append(fs % (k, conv(val,col_w)))
 
 
 		return repr(self) + '\n    ' + '\n    '.join(out)
 		return repr(self) + '\n    ' + '\n    '.join(out)
+
+class MMGenListItemAttr(object):
+	def __init__(self,name,dtype):
+		self.name = name
+		self.dtype = dtype
+	def __get__(self,instance,owner):
+		return instance.__dict__[self.name]
+	def __set__(self,instance,value):
+#		if self.name == 'mmid': print repr(instance), repr(value) # DEBUG
+		instance.__dict__[self.name] = globals()[self.dtype](value)
+	def __delete__(self,instance):
+		del instance.__dict__[self.name]
+
+class MMGenListItem(MMGenObject):
+
+	addr = MMGenListItemAttr('addr','BTCAddr')
+	amt  = MMGenListItemAttr('amt','BTCAmt')
+	mmid = MMGenListItemAttr('mmid','MMGenID')
+	label = MMGenListItemAttr('label','MMGenLabel')
+
+	attrs = ()
+	attrs_priv = ()
+	attrs_reassign = 'label',
+
+	def attr_error(self,arg):
+		raise AttributeError, "'{}': invalid attribute for {}".format(arg,type(self).__name__)
+	def set_error(self,attr,val):
+		raise ValueError, \
+			"'{}': attribute '{}' in instance of class '{}' cannot be reassigned".format(
+				val,attr,type(self).__name__)
+
+	attrs_base = ('attrs','attrs_priv','attrs_reassign','attrs_base','attr_error','set_error','__dict__')
+
+	def __init__(self,*args,**kwargs):
+		if args:
+			raise ValueError, 'Non-keyword args not allowed'
+		for k in kwargs:
+			if kwargs[k] != None:
+				setattr(self,k,kwargs[k])
+
+	def __getattribute__(self,name):
+		ga = object.__getattribute__
+		if name in ga(self,'attrs') + ga(self,'attrs_priv') + ga(self,'attrs_base'):
+			try:
+				return ga(self,name)
+			except:
+				return None
+		else:
+			self.attr_error(name)
+
+	def __setattr__(self,name,val):
+		if name in (self.attrs + self.attrs_priv + self.attrs_base):
+			if getattr(self,name) == None or name in self.attrs_reassign:
+				object.__setattr__(self,name,val)
+			else:
+#				object.__setattr__(self,name,val) # DEBUG
+				self.set_error(name,val)
+		else:
+			self.attr_error(name)
+
+	def __delattr__(self,name):
+		if name in (self.attrs + self.attrs_priv + self.attrs_base):
+			try: # don't know why this is necessary
+				object.__delattr__(self,name)
+			except:
+				pass
+		else:
+			self.attr_error(name)
+
+class InitErrors(object):
+
+	@staticmethod
+	def arg_chk(cls,on_fail):
+		assert on_fail in ('die','return','silent','raise'),"'on_fail' in class %s" % cls.__name__
+
+	@staticmethod
+	def init_fail(m,on_fail,silent=False):
+		if silent: m = ''
+		from mmgen.util import die,msg
+		if on_fail == 'die':      die(1,m)
+		elif on_fail == 'return':
+			if m: msg(m)
+			return None
+		elif on_fail == 'silent': return None
+		elif on_fail == 'raise':  raise ValueError,m
+
+class AddrIdx(int,InitErrors):
+
+	max_digits = 7
+
+	def __new__(cls,num,on_fail='die'):
+		cls.arg_chk(cls,on_fail)
+		try:
+			assert type(num) is not float
+			me = int.__new__(cls,num)
+		except:
+			m = "'%s': value cannot be converted to addr idx" % num
+		else:
+			if len(str(me)) > cls.max_digits:
+				m = "'%s': too many digits in addr idx" % num
+			elif me < 1:
+				m = "'%s': addr idx cannot be less than one" % num
+			else:
+				return me
+		return cls.init_fail(m,on_fail)
+
+class AddrIdxList(list,InitErrors):
+
+	max_len = 1000000
+
+	def __init__(self,fmt_str=None,idx_list=None,on_fail='die',sep=','):
+		self.arg_chk(type(self),on_fail)
+		assert fmt_str or idx_list
+		if idx_list:
+			return list.__init__(self,sorted(set(idx_list)))
+		elif fmt_str:
+			ret,fs = [],"'%s': value cannot be converted to addr idx"
+			from mmgen.util import msg
+			for i in (fmt_str.split(sep)):
+				j = i.split('-')
+				if len(j) == 1:
+					idx = AddrIdx(i,on_fail='return')
+					if not idx: break
+					ret.append(idx)
+				elif len(j) == 2:
+					beg = AddrIdx(j[0],on_fail='return')
+					if not beg: break
+					end = AddrIdx(j[1],on_fail='return')
+					if not beg: break
+					if end < beg:
+						msg(fs % "%s-%s (invalid range)" % (beg,end)); break
+					ret.extend([AddrIdx(x) for x in range(beg,end+1)])
+				else:
+					msg((fs % i) + ' list'); break
+			else:
+				return list.__init__(self,sorted(set(ret))) # fell off end of loop - success
+
+		return self.init_fail(fs % err,on_fail,silent=True)
+
+class Hilite(object):
+
+	color = 'red'
+	color_always = False
+	width = 0
+	trunc_ok = True
+
+	@classmethod
+	def fmtc(cls,s,width=None,color=False,encl='',trunc_ok=None):
+		if width == None: width = cls.width
+		if trunc_ok == None: trunc_ok = cls.trunc_ok
+		assert width > 0
+		assert type(encl) is str and len(encl) in (0,2)
+		a,b = list(encl) if encl else ('','')
+		if trunc_ok and len(s) > width: s = s[:width]
+		return cls.colorize((a+s+b).ljust(width),color=color)
+
+	def fmt(self,width=None,color=False,encl='',trunc_ok=None):
+		if width == None: width = self.width
+		if trunc_ok == None: trunc_ok = self.trunc_ok
+		return self.fmtc(self,width=width,color=color,encl=encl,trunc_ok=trunc_ok)
+
+	@classmethod
+	def hlc(cls,s,color=True):
+		return cls.colorize(s,color=color)
+
+	def hl(self,color=True):
+		return self.colorize(self,color=color)
+
+	def __str__(self):
+		return self.colorize(self,color=False)
+
+	@classmethod
+	def colorize(cls,s,color=True):
+		import mmgen.globalvars as g
+		from mmgen.util import red,blue,green,yellow,pink,cyan,gray,orange,magenta
+		return locals()[cls.color](s) if (color or cls.color_always) and g.color else s
+
+class BTCAmt(Decimal,Hilite,InitErrors):
+	color = 'yellow'
+	max_prec = 8
+	max_amt = 21000000
+
+	def __new__(cls,num,on_fail='die'):
+		cls.arg_chk(cls,on_fail)
+		try:
+			me = Decimal.__new__(cls,str(num))
+		except:
+			m = "'%s': value cannot be converted to decimal" % num
+		else:
+			if me.normalize().as_tuple()[-1] < -cls.max_prec:
+				m = "'%s': too many decimal places in BTC amount" % num
+			elif me > cls.max_amt:
+				m = "'%s': BTC amount too large (>%s)" % (num,cls.max_amt)
+#			elif me.as_tuple()[0]:
+#				m = "'%s': BTC amount cannot be negative" % num
+			else:
+				return me
+		return cls.init_fail(m,on_fail)
+
+	@classmethod
+	def fmtc(cls):
+		raise NotImplemented
+
+	def fmt(self,fs='3.8',color=False,suf=''):
+		s = self.__str__(color=False)
+		if '.' in fs:
+			p1,p2 = [int(i) for i in fs.split('.',1)]
+			ss = s.split('.',1)
+			if len(ss) == 2:
+				a,b = ss
+				ret = a.rjust(p1) + '.' + (b+suf).ljust(p2+len(suf))
+			else:
+				ret = s.rjust(p1) + suf + ' ' * (p2+1)
+		else:
+			ret = s.ljust(int(fs))
+		return self.colorize(ret,color=color)
+
+	def hl(self,color=True):
+		return self.__str__(color=color)
+
+	def __str__(self,color=False): # format simply, no exponential notation
+		if int(self) == self:
+			ret = str(int(self))
+		else:
+			ret = self.normalize().__format__('f')
+		return self.colorize(ret,color=color)
+
+	def __repr__(self):
+		return "{}('{}')".format(type(self).__name__,self.__str__())
+
+	def __add__(self,other,context=None):
+		return type(self)(Decimal.__add__(self,other,context))
+	__radd__ = __add__
+
+	def __sub__(self,other,context=None):
+		return type(self)(Decimal.__sub__(self,other,context))
+
+	def __mul__(self,other,context=None):
+		return type(self)('{:0.8f}'.format(Decimal.__mul__(self,Decimal(other),context)))
+
+	def __div__(self,other,context=None):
+		return type(self)('{:0.8f}'.format(Decimal.__div__(self,Decimal(other),context)))
+
+	def __neg__(self,other,context=None):
+		return type(self)(Decimal.__neg__(self,other,context))
+
+
+class BTCAddr(str,Hilite,InitErrors):
+	color = 'cyan'
+	width = 34
+	def __new__(cls,s,on_fail='die'):
+		cls.arg_chk(cls,on_fail)
+		me = str.__new__(cls,s)
+		from mmgen.bitcoin import verify_addr
+		if verify_addr(s):
+			return me
+		else:
+			m = "'%s': value is not a Bitcoin address" % s
+		return cls.init_fail(m,on_fail)
+
+	def fmt(self,width=width,color=False):
+		return self.fmtc(self,width=width,color=color)
+
+	@classmethod
+	def fmtc(cls,s,width=width,color=False):
+		if width >= len(s):
+			s = s.ljust(width)
+		else:
+			s = s[:width-2] +  '..'
+		return cls.colorize(s,color=color)
+
+class SeedID(str,Hilite,InitErrors):
+	color = 'blue'
+	width = 8
+	trunc_ok = False
+	def __new__(cls,seed=None,sid=None,on_fail='die'):
+		cls.arg_chk(cls,on_fail)
+		assert seed or sid
+		if seed:
+			from mmgen.seed import Seed
+			from mmgen.util import make_chksum_8
+			assert type(seed) == Seed
+			return str.__new__(cls,make_chksum_8(seed.get_data()))
+		elif sid:
+			from string import hexdigits
+			assert len(sid) == cls.width and set(sid) <= set(hexdigits.upper())
+			return str.__new__(cls,sid)
+		m = "'%s': value cannot be converted to SeedID" % s
+		return cls.init_fail(m,on_fail)
+
+class MMGenID(str,Hilite,InitErrors):
+
+	color = 'orange'
+	width = 0
+	trunc_ok = False
+
+	def __new__(cls,s,on_fail='die'):
+		cls.arg_chk(cls,on_fail)
+		s = str(s)
+		if ':' in s:
+			a,b = s.split(':',1)
+			sid = SeedID(sid=a,on_fail='return')
+			if sid:
+				idx = AddrIdx(b,on_fail='return')
+				if idx:
+					return str.__new__(cls,'%s:%s' % (sid,idx))
+
+		m = "'%s': value cannot be converted to MMGenID" % s
+		return cls.init_fail(m,on_fail)
+
+class MMGenLabel(unicode,Hilite,InitErrors):
+
+	color = 'pink'
+	allowed = u''
+	max_len = 0
+	desc = 'label'
+
+	def __new__(cls,s,on_fail='die',msg=None):
+		cls.arg_chk(cls,on_fail)
+		try:
+			s = s.decode('utf8').strip()
+		except:
+			m = "'%s: value is not a valid UTF-8 string" % s
+		else:
+			if len(s) > cls.max_len:
+				m = '%s too long (>%s symbols)' % (cls.desc.capitalize(),cls.max_len)
+			elif cls.allowed and not set(list(s)).issubset(set(list(cls.allowed))):
+				m = '%s contains non-permitted symbols: %s' % (cls.desc.capitalize(),
+					' '.join(set(list(s)) - set(list(cls.allowed))))
+			else:
+				return unicode.__new__(cls,s)
+		return cls.init_fail((msg+'\n' if msg else '') + m,on_fail)
+
+class MMGenWalletLabel(MMGenLabel):
+	max_len = 48
+	allowed = [chr(i+32) for i in range(95)]
+	desc = 'wallet label'
+
+class MMGenAddrLabel(MMGenLabel):
+	max_len = 32
+	desc = 'address label'
+
+class MMGenTXLabel(MMGenLabel):
+	max_len = 72
+	desc = 'transaction label'

+ 2 - 6
mmgen/opts.py

@@ -230,14 +230,10 @@ def check_opts(usr_opts):       # Returns false if any check fails
 
 
 		if key == 'outdir':
 		if key == 'outdir':
 			check_outdir(val)  # exits on error
 			check_outdir(val)  # exits on error
-		elif key == 'label':
-			if not is_mmgen_wallet_label(val):
-				msg("Illegal value for option '%s': '%s'" % (fmt_opt(key),val))
-				return False
-		# NEW
+# 		# NEW
 		elif key in ('in_fmt','out_fmt'):
 		elif key in ('in_fmt','out_fmt'):
 			from mmgen.seed import SeedSource,IncogWallet,Brainwallet,IncogWalletHidden
 			from mmgen.seed import SeedSource,IncogWallet,Brainwallet,IncogWalletHidden
-			sstype = SeedSource.fmt_code_to_sstype(val)
+			sstype = SeedSource.fmt_code_to_type(val)
 			if not sstype:
 			if not sstype:
 				return opt_unrecognized(key,val,'format code')
 				return opt_unrecognized(key,val,'format code')
 			if key == 'out_fmt':
 			if key == 'out_fmt':

+ 7 - 6
mmgen/rpc.py

@@ -78,17 +78,17 @@ class BitcoinRPCConnection(object):
 		dmsg('=== rpc.py debug ===')
 		dmsg('=== rpc.py debug ===')
 		dmsg('    RPC POST data ==> %s\n' % p)
 		dmsg('    RPC POST data ==> %s\n' % p)
 
 
-		from decimal import Decimal
-		class JSONDecEncoder(json.JSONEncoder):
+		from mmgen.obj import BTCAmt
+		class MyJSONEncoder(json.JSONEncoder):
 			def default(self, obj):
 			def default(self, obj):
-				if isinstance(obj, Decimal):
+				if isinstance(obj, BTCAmt):
 					return str(obj)
 					return str(obj)
 				return json.JSONEncoder.default(self, obj)
 				return json.JSONEncoder.default(self, obj)
 
 
-#		pp_msg(json.dumps(p,cls=JSONDecEncoder))
+#		pp_msg(json.dumps(p,cls=MyJSONEncoder))
 
 
 		try:
 		try:
-			c.request('POST', '/', json.dumps(p,cls=JSONDecEncoder), {
+			c.request('POST', '/', json.dumps(p,cls=MyJSONEncoder), {
 				'Host': self.host,
 				'Host': self.host,
 				'Authorization': 'Basic ' + base64.b64encode(self.auth_str)
 				'Authorization': 'Basic ' + base64.b64encode(self.auth_str)
 			})
 			})
@@ -112,7 +112,8 @@ class BitcoinRPCConnection(object):
 		if not r2:
 		if not r2:
 			return die_maybe(r,2,'Error: empty reply')
 			return die_maybe(r,2,'Error: empty reply')
 
 
-		r3 = json.loads(r2.decode('utf8'), parse_float=decimal.Decimal)
+		from decimal import Decimal
+		r3 = json.loads(r2.decode('utf8'), parse_float=Decimal)
 		ret = []
 		ret = []
 
 
 		for resp in r3 if cf['batch'] else [r3]:
 		for resp in r3 if cf['batch'] else [r3]:

+ 51 - 52
mmgen/seed.py

@@ -37,7 +37,6 @@ def check_usr_seed_len(seed_len):
 			"doesn't match seed length of source (%s)"
 			"doesn't match seed length of source (%s)"
 		die(1, m % (opt.seed_len,seed_len))
 		die(1, m % (opt.seed_len,seed_len))
 
 
-
 class Seed(MMGenObject):
 class Seed(MMGenObject):
 	def __init__(self,seed_bin=None):
 	def __init__(self,seed_bin=None):
 		if not seed_bin:
 		if not seed_bin:
@@ -48,9 +47,12 @@ class Seed(MMGenObject):
 
 
 		self.data      = seed_bin
 		self.data      = seed_bin
 		self.hexdata   = hexlify(seed_bin)
 		self.hexdata   = hexlify(seed_bin)
-		self.sid       = make_chksum_8(seed_bin)
+		self.sid       = SeedID(seed=self)
 		self.length    = len(seed_bin) * 8
 		self.length    = len(seed_bin) * 8
 
 
+	def get_data(self):
+		return self.data
+
 
 
 class SeedSource(MMGenObject):
 class SeedSource(MMGenObject):
 
 
@@ -64,21 +66,17 @@ class SeedSource(MMGenObject):
 
 
 	class SeedSourceData(MMGenObject): pass
 	class SeedSourceData(MMGenObject): pass
 
 
-	def __new__(cls,fn=None,ss=None,seed=None,
-				ignore_in_fmt=False,passchg=False):
+	def __new__(cls,fn=None,ss=None,seed=None,ignore_in_fmt=False,passchg=False):
 
 
 		def die_on_opt_mismatch(opt,sstype):
 		def die_on_opt_mismatch(opt,sstype):
-			opt_sstype = cls.fmt_code_to_sstype(opt)
+			opt_sstype = cls.fmt_code_to_type(opt)
 			compare_or_die(
 			compare_or_die(
 				opt_sstype.__name__, 'input format requested on command line',
 				opt_sstype.__name__, 'input format requested on command line',
 				sstype.__name__,     'input file format'
 				sstype.__name__,     'input file format'
 			)
 			)
 
 
 		if ss:
 		if ss:
-			if passchg:
-				sstype = ss.__class__
-			else:
-				sstype = cls.fmt_code_to_sstype(opt.out_fmt)
+			sstype = ss.__class__ if passchg else cls.fmt_code_to_type(opt.out_fmt)
 			me = super(cls,cls).__new__(sstype or Wallet) # default: Wallet
 			me = super(cls,cls).__new__(sstype or Wallet) # default: Wallet
 			me.seed = ss.seed
 			me.seed = ss.seed
 			me.ss_in = ss
 			me.ss_in = ss
@@ -86,32 +84,28 @@ class SeedSource(MMGenObject):
 		elif fn or opt.hidden_incog_input_params:
 		elif fn or opt.hidden_incog_input_params:
 			if fn:
 			if fn:
 				f = Filename(fn)
 				f = Filename(fn)
-				sstype = cls.ext_to_sstype(f.ext)
 			else:
 			else:
 				fn = opt.hidden_incog_input_params.split(',')[0]
 				fn = opt.hidden_incog_input_params.split(',')[0]
-				f  = Filename(fn,ftype='hincog')
-				sstype = cls.fmt_code_to_sstype('hincog')
-
+				f = Filename(fn,ftype=IncogWalletHidden)
 			if opt.in_fmt and not ignore_in_fmt:
 			if opt.in_fmt and not ignore_in_fmt:
-				die_on_opt_mismatch(opt.in_fmt,sstype)
-
-			me = super(cls,cls).__new__(sstype)
+				die_on_opt_mismatch(opt.in_fmt,f.ftype)
+			me = super(cls,cls).__new__(f.ftype)
 			me.infile = f
 			me.infile = f
 			me.op = ('old','pwchg_old')[bool(passchg)]
 			me.op = ('old','pwchg_old')[bool(passchg)]
 		elif opt.in_fmt:  # Input format
 		elif opt.in_fmt:  # Input format
-			sstype = cls.fmt_code_to_sstype(opt.in_fmt)
+			sstype = cls.fmt_code_to_type(opt.in_fmt)
 			me = super(cls,cls).__new__(sstype)
 			me = super(cls,cls).__new__(sstype)
 			me.op = ('old','pwchg_old')[bool(passchg)]
 			me.op = ('old','pwchg_old')[bool(passchg)]
 		else: # Called with no inputs - initialize with random seed
 		else: # Called with no inputs - initialize with random seed
-			sstype = cls.fmt_code_to_sstype(opt.out_fmt)
+			sstype = cls.fmt_code_to_type(opt.out_fmt)
 			me = super(cls,cls).__new__(sstype or Wallet) # default: Wallet
 			me = super(cls,cls).__new__(sstype or Wallet) # default: Wallet
 			me.seed = Seed(seed_bin=seed or None)
 			me.seed = Seed(seed_bin=seed or None)
 			me.op = 'new'
 			me.op = 'new'
+#			die(1,me.seed.sid.hl()) # DEBUG
 
 
 		return me
 		return me
 
 
-	def __init__(self,fn=None,ss=None,seed=None,
-					ignore_in_fmt=False,passchg=False):
+	def __init__(self,fn=None,ss=None,seed=None,ignore_in_fmt=False,passchg=False):
 
 
 		self.ssdata = self.SeedSourceData()
 		self.ssdata = self.SeedSourceData()
 		self.msg = {}
 		self.msg = {}
@@ -135,7 +129,7 @@ class SeedSource(MMGenObject):
 			self._decrypt_retry()
 			self._decrypt_retry()
 
 
 		m = ('',', seed length %s' % self.seed.length)[self.seed.length!=256]
 		m = ('',', seed length %s' % self.seed.length)[self.seed.length!=256]
-		qmsg('Valid %s for Seed ID %s%s' % (self.desc,self.seed.sid,m))
+		qmsg('Valid %s for Seed ID %s%s' % (self.desc,self.seed.sid.hl(),m))
 
 
 	def _get_data(self):
 	def _get_data(self):
 		if hasattr(self,'infile'):
 		if hasattr(self,'infile'):
@@ -162,36 +156,36 @@ class SeedSource(MMGenObject):
 				die(2,'Passphrase from password file, so exiting')
 				die(2,'Passphrase from password file, so exiting')
 			msg('Trying again...')
 			msg('Trying again...')
 
 
-	subclasses = []
-
 	@classmethod
 	@classmethod
-	def _get_subclasses(cls):
-
-		if cls.subclasses: return cls.subclasses
+	def get_subclasses(cls):
+		if not hasattr(cls,'subclasses'):
+			gl = globals()
+			setattr(cls,'subclasses',
+				[gl[k] for k in gl if type(gl[k]) == type and issubclass(gl[k],cls)])
+		return cls.subclasses
 
 
-		ret,gl = [],globals()
-		for c in [gl[k] for k in gl]:
-			try:
-				if issubclass(c,cls):
-					ret.append(c)
-			except:
-				pass
+	@classmethod
+	def get_subclasses_str(cls):
+		def GetSubclassesTree(cls):
+			return ''.join([c.__name__ +' '+ GetSubclassesTree(c) for c in cls.__subclasses__()])
+		return GetSubclassesTree(cls)
 
 
-		cls.subclasses = ret
-		return ret
+	@classmethod
+	def get_extensions(cls):
+		return [s.ext for s in cls.get_subclasses() if hasattr(s,'ext')]
 
 
 	@classmethod
 	@classmethod
-	def fmt_code_to_sstype(cls,fmt_code):
+	def fmt_code_to_type(cls,fmt_code):
 		if not fmt_code: return None
 		if not fmt_code: return None
-		for c in cls._get_subclasses():
+		for c in cls.get_subclasses():
 			if hasattr(c,'fmt_codes') and fmt_code in c.fmt_codes:
 			if hasattr(c,'fmt_codes') and fmt_code in c.fmt_codes:
 				return c
 				return c
 		return None
 		return None
 
 
 	@classmethod
 	@classmethod
-	def ext_to_sstype(cls,ext):
+	def ext_to_type(cls,ext):
 		if not ext: return None
 		if not ext: return None
-		for c in cls._get_subclasses():
+		for c in cls.get_subclasses():
 			if hasattr(c,'ext') and ext == c.ext:
 			if hasattr(c,'ext') and ext == c.ext:
 				return c
 				return c
 		return None
 		return None
@@ -199,7 +193,7 @@ class SeedSource(MMGenObject):
 	@classmethod
 	@classmethod
 	def format_fmt_codes(cls):
 	def format_fmt_codes(cls):
 		d = [(c.__name__,('.'+c.ext if c.ext else c.ext),','.join(c.fmt_codes))
 		d = [(c.__name__,('.'+c.ext if c.ext else c.ext),','.join(c.fmt_codes))
-					for c in cls._get_subclasses()
+					for c in cls.get_subclasses()
 				if hasattr(c,'fmt_codes')]
 				if hasattr(c,'fmt_codes')]
 		w = max([len(a) for a,b,c in d])
 		w = max([len(a) for a,b,c in d])
 		ret = ['{:<{w}}  {:<9} {}'.format(a,b,c,w=w) for a,b,c in [
 		ret = ['{:<{w}}  {:<9} {}'.format(a,b,c,w=w) for a,b,c in [
@@ -365,6 +359,8 @@ class Mnemonic (SeedSourceUnenc):
 	}
 	}
 	mn_base = 1626
 	mn_base = 1626
 	wordlists = sorted(wl_checksums)
 	wordlists = sorted(wl_checksums)
+	dfl_wordlist = 'electrum'
+	# dfl_wordlist = 'tirosh'
 
 
 	@staticmethod
 	@staticmethod
 	def _mn2hex_pad(mn): return len(mn) * 8 / 3
 	def _mn2hex_pad(mn): return len(mn) * 8 / 3
@@ -399,7 +395,7 @@ class Mnemonic (SeedSourceUnenc):
 
 
 	@classmethod
 	@classmethod
 	def get_wordlist(cls,wordlist=None):
 	def get_wordlist(cls,wordlist=None):
-		wordlist = wordlist or g.default_wordlist
+		wordlist = wordlist or cls.dfl_wordlist
 		if wordlist not in cls.wordlists:
 		if wordlist not in cls.wordlists:
 			die(1,"'%s': invalid wordlist.  Valid choices: '%s'" %
 			die(1,"'%s': invalid wordlist.  Valid choices: '%s'" %
 				(wordlist,"' '".join(cls.wordlists)))
 				(wordlist,"' '".join(cls.wordlists)))
@@ -536,28 +532,31 @@ class Wallet (SeedSourceEnc):
 	ext = 'mmdat'
 	ext = 'mmdat'
 
 
 	def _get_label_from_user(self,old_lbl=''):
 	def _get_label_from_user(self,old_lbl=''):
-		d = ("to reuse the label '%s'" % old_lbl) if old_lbl else 'for no label'
+		d = ("to reuse the label '%s'" % old_lbl.hl()) if old_lbl else 'for no label'
 		p = 'Enter a wallet label, or hit ENTER %s: ' % d
 		p = 'Enter a wallet label, or hit ENTER %s: ' % d
 		while True:
 		while True:
-			ret = my_raw_input(p)
+			msg_r(p)
+			ret = my_raw_input('')
 			if ret:
 			if ret:
-				if is_mmgen_wallet_label(ret):
-					self.ssdata.label = ret; return ret
+				self.ssdata.label = MMGenWalletLabel(ret,on_fail='return')
+				if self.ssdata.label:
+					break
 				else:
 				else:
 					msg('Invalid label.  Trying again...')
 					msg('Invalid label.  Trying again...')
 			else:
 			else:
-				ret = old_lbl or 'No Label'
-				self.ssdata.label = ret; return ret
+				self.ssdata.label = old_lbl or MMGenWalletLabel('No Label')
+				break
+		return self.ssdata.label
 
 
 	# nearly identical to _get_hash_preset() - factor?
 	# nearly identical to _get_hash_preset() - factor?
 	def _get_label(self):
 	def _get_label(self):
 		if hasattr(self,'ss_in') and hasattr(self.ss_in.ssdata,'label'):
 		if hasattr(self,'ss_in') and hasattr(self.ss_in.ssdata,'label'):
 			old_lbl = self.ss_in.ssdata.label
 			old_lbl = self.ss_in.ssdata.label
 			if opt.keep_label:
 			if opt.keep_label:
-				qmsg("Reusing label '%s' at user request" % old_lbl)
+				qmsg("Reusing label '%s' at user request" % old_lbl.hl())
 				self.ssdata.label = old_lbl
 				self.ssdata.label = old_lbl
 			elif opt.label:
 			elif opt.label:
-				qmsg("Using label '%s' requested on command line" % opt.label)
+				qmsg("Using label '%s' requested on command line" % opt.label.hl())
 				lbl = self.ssdata.label = opt.label
 				lbl = self.ssdata.label = opt.label
 			else: # Prompt, using old value as default
 			else: # Prompt, using old value as default
 				lbl = self._get_label_from_user(old_lbl)
 				lbl = self._get_label_from_user(old_lbl)
@@ -566,7 +565,7 @@ class Wallet (SeedSourceEnc):
 				m = ("changed to '%s'" % lbl,'unchanged')[lbl==old_lbl]
 				m = ("changed to '%s'" % lbl,'unchanged')[lbl==old_lbl]
 				qmsg('Label %s' % m)
 				qmsg('Label %s' % m)
 		elif opt.label:
 		elif opt.label:
-			qmsg("Using label '%s' requested on command line" % opt.label)
+			qmsg("Using label '%s' requested on command line" % opt.label.hl())
 			self.ssdata.label = opt.label
 			self.ssdata.label = opt.label
 		else:
 		else:
 			self._get_label_from_user()
 			self._get_label_from_user()
@@ -619,7 +618,7 @@ class Wallet (SeedSourceEnc):
 		if not check_master_chksum(lines,self.desc): return False
 		if not check_master_chksum(lines,self.desc): return False
 
 
 		d = self.ssdata
 		d = self.ssdata
-		d.label = lines[1]
+		d.label = MMGenWalletLabel(lines[1])
 
 
 		d1,d2,d3,d4,d5 = lines[2].split()
 		d1,d2,d3,d4,d5 = lines[2].split()
 		d.seed_id = d1.upper()
 		d.seed_id = d1.upper()
@@ -994,7 +993,7 @@ harder to find, you're advised to choose a much larger file size than this.
 			else:
 			else:
 				die(1,'Exiting at user request')
 				die(1,'Exiting at user request')
 
 
-		self.outfile = f = Filename(fn,ftype=self.fmt_codes[0],write=True)
+		f = Filename(fn,ftype=type(self),write=True)
 
 
 		dmsg('%s data len %s, offset %s' % (
 		dmsg('%s data len %s, offset %s' % (
 				capfirst(self.desc),d.target_data_len,d.hincog_offset))
 				capfirst(self.desc),d.target_data_len,d.hincog_offset))

+ 61 - 69
mmgen/tool.py

@@ -162,10 +162,11 @@ def tool_usage(prog_name, command):
 		for line in cmd_help.split('\n'):
 		for line in cmd_help.split('\n'):
 			if '  ' + command in line:
 			if '  ' + command in line:
 				c,h = line.split('-',1)
 				c,h = line.split('-',1)
-				Msg('{}: {}'.format(c.strip(),h.strip()))
-		Msg('USAGE: %s %s %s' % (prog_name, command, ' '.join(cmd_data[command])))
+				Msg('MMGEN-TOOL {}: {}'.format(c.strip().upper(),h.strip()))
+		msg('USAGE: %s %s %s' % (prog_name, command, ' '.join(cmd_data[command])))
 	else:
 	else:
-		Msg("'%s': no such tool command" % command)
+		msg("'%s': no such tool command" % command)
+	sys.exit(1)
 
 
 def process_args(prog_name, command, cmd_args):
 def process_args(prog_name, command, cmd_args):
 	c_args = [[i.split(' [')[0],i.split(' [')[1][:-1]]
 	c_args = [[i.split(' [')[0],i.split(' [')[1][:-1]]
@@ -174,61 +175,53 @@ def process_args(prog_name, command, cmd_args):
 			i.split(' [')[0],
 			i.split(' [')[0],
 			[i.split(' [')[1].split('=')[0], i.split(' [')[1].split('=')[1][:-1]]
 			[i.split(' [')[1].split('=')[0], i.split(' [')[1].split('=')[1][:-1]]
 		] for i in cmd_data[command] if '=' in i])
 		] for i in cmd_data[command] if '=' in i])
-
-	u_args = cmd_args[:len(c_args)]
-	u_kwargs = cmd_args[len(c_args):]
+	u_args   = [a for a in cmd_args[:len(c_args)]]
 
 
 	if len(u_args) < len(c_args):
 	if len(u_args) < len(c_args):
-		msg('%s argument%s required' % (len(c_args),suf(c_args,'k')))
-		tool_usage(prog_name, command)
-		sys.exit(1)
-
-	if len(u_kwargs) > len(c_kwargs):
-		msg('Too many arguments')
-		tool_usage(prog_name, command)
-		sys.exit(1)
-
-	u_kwargs = dict([a.split('=') for a in u_kwargs])
-
-#	print c_args; print c_kwargs; print u_args; print u_kwargs; sys.exit()
+		msg('Command requires exactly %s non-keyword argument%s' % (len(c_args),suf(c_args,'k')))
+		tool_usage(prog_name,command)
+
+	extra_args = len(cmd_args) - len(c_args)
+	u_kwargs = {}
+	if extra_args > 0:
+		u_kwargs = dict([a.split('=') for a in cmd_args[len(c_args):] if '=' in a])
+		if len(u_kwargs) != extra_args:
+			msg('Command requires exactly %s non-keyword argument%s'
+				% (len(c_args),suf(c_args,'k')))
+			tool_usage(prog_name,command)
+		if len(u_kwargs) > len(c_kwargs):
+			msg('Command requires exactly %s keyword argument%s'
+				% (len(c_kwargs),suf(c_kwargs,'k')))
+			tool_usage(prog_name,command)
+
+#	mdie(c_args,c_kwargs,u_args,u_kwargs)
 
 
-	if set(u_kwargs) > set(c_kwargs):
-		die(1,'Invalid named argument')
+	for k in u_kwargs:
+		if k not in c_kwargs:
+			msg("'%s': invalid keyword argument" % k)
+			tool_usage(prog_name,command)
 
 
-	def convert_type(arg,arg_name,arg_type):
+	def conv_type(arg,arg_name,arg_type):
+		if arg_type == 'bool':
+			if arg.lower() in ('true','yes','1','on'): arg = True
+			elif arg.lower() in ('false','no','0','off'): arg = False
+			else:
+				msg("'%s': invalid boolean value for keyword argument" % arg)
+				tool_usage(prog_name,command)
 		try:
 		try:
 			return __builtins__[arg_type](arg)
 			return __builtins__[arg_type](arg)
 		except:
 		except:
 			die(1,"'%s': Invalid argument for argument %s ('%s' required)" % \
 			die(1,"'%s': Invalid argument for argument %s ('%s' required)" % \
 				(arg, arg_name, arg_type))
 				(arg, arg_name, arg_type))
 
 
-	def convert_to_bool_maybe(arg, arg_type):
-		if arg_type == 'bool':
-			if arg.lower() in ('true','yes','1','on'): return True
-			if arg.lower() in ('false','no','0','off'): return False
-		return arg
-
-	args = []
-	for i in range(len(c_args)):
-		arg_type = c_args[i][1]
-		arg = convert_to_bool_maybe(u_args[i], arg_type)
-		args.append(convert_type(arg,c_args[i][0],arg_type))
-
-	kwargs = {}
-	for k in u_kwargs:
-		arg_type = c_kwargs[k][0]
-		arg = convert_to_bool_maybe(u_kwargs[k], arg_type)
-		kwargs[k] = convert_type(arg,k,arg_type)
+	args = [conv_type(u_args[i],c_args[i][0],c_args[i][1]) for i in range(len(c_args))]
+	kwargs = dict([(k,conv_type(u_kwargs[k],k,c_kwargs[k][0])) for k in u_kwargs])
 
 
+#	mdie(args,kwargs)
 	return args,kwargs
 	return args,kwargs
 
 
 # Individual cmd_data
 # Individual cmd_data
 
 
-# def help():
-# 	Msg('Available commands:')
-# 	for k in sorted(cmd_data.keys()):
-# 		Msg('%-16s %s' % (k,' '.join(cmd_data[k])))
-
 def are_equal(a,b,dtype=''):
 def are_equal(a,b,dtype=''):
 	if dtype == 'str': return a.lstrip('\0') == b.lstrip('\0')
 	if dtype == 'str': return a.lstrip('\0') == b.lstrip('\0')
 	if dtype == 'hex': return a.lstrip('0') == b.lstrip('0')
 	if dtype == 'hex': return a.lstrip('0') == b.lstrip('0')
@@ -387,19 +380,19 @@ def listaddresses(addrs='',minconf=1,showempty=False,pager=False,showbtcaddrs=Fa
 	c = bitcoin_connection()
 	c = bitcoin_connection()
 
 
 	addrs = {} # reusing variable name!
 	addrs = {} # reusing variable name!
-	from decimal import Decimal
-	total = Decimal('0')
+	from mmgen.obj import BTCAmt
+	total = BTCAmt('0')
 	for d in c.listunspent(0):
 	for d in c.listunspent(0):
 		mmaddr,comment = split2(d['account'])
 		mmaddr,comment = split2(d['account'])
 		if usr_addr_list and (mmaddr not in usr_addr_list): continue
 		if usr_addr_list and (mmaddr not in usr_addr_list): continue
-		if is_mmgen_addr(mmaddr) and d['confirmations'] >= minconf:
+		if is_mmgen_id(mmaddr) and d['confirmations'] >= minconf:
 			key = mmaddr.replace(':','_')
 			key = mmaddr.replace(':','_')
 			if key in addrs:
 			if key in addrs:
 				if addrs[key][2] != d['address']:
 				if addrs[key][2] != d['address']:
 					die(2,'duplicate BTC address ({}) for this MMGen address! ({})'.format(
 					die(2,'duplicate BTC address ({}) for this MMGen address! ({})'.format(
 							(d['address'], addrs[key][2])))
 							(d['address'], addrs[key][2])))
 			else:
 			else:
-				addrs[key] = [Decimal('0'),comment,d['address']]
+				addrs[key] = [BTCAmt('0'),comment,d['address']]
 			addrs[key][0] += d['amount']
 			addrs[key][0] += d['amount']
 			total += d['amount']
 			total += d['amount']
 
 
@@ -410,11 +403,11 @@ def listaddresses(addrs='',minconf=1,showempty=False,pager=False,showbtcaddrs=Fa
 		for acct in accts:
 		for acct in accts:
 			mmaddr,comment = split2(acct)
 			mmaddr,comment = split2(acct)
 			if usr_addr_list and (mmaddr not in usr_addr_list): continue
 			if usr_addr_list and (mmaddr not in usr_addr_list): continue
-			if is_mmgen_addr(mmaddr):
+			if is_mmgen_id(mmaddr):
 				key = mmaddr.replace(':','_')
 				key = mmaddr.replace(':','_')
 				if key not in addrs:
 				if key not in addrs:
 					if showbtcaddrs: save_a.append([acct])
 					if showbtcaddrs: save_a.append([acct])
-					addrs[key] = [Decimal('0'),comment,'']
+					addrs[key] = [BTCAmt('0'),comment,'']
 
 
 		for acct,addr in zip(save_a,c.getaddressesbyaccount(save_a,batch=True)):
 		for acct,addr in zip(save_a,c.getaddressesbyaccount(save_a,batch=True)):
 			if len(addr) != 1:
 			if len(addr) != 1:
@@ -431,17 +424,17 @@ def listaddresses(addrs='',minconf=1,showempty=False,pager=False,showbtcaddrs=Fa
 		max(max(len(addrs[k][1]) for k in addrs) + 1,8) # pad 8 if no comments
 		max(max(len(addrs[k][1]) for k in addrs) + 1,8) # pad 8 if no comments
 	)
 	)
 
 
-	def s_mmgen(key):
+	def s_mmgen(key): # TODO
 		return '{}:{:>0{w}}'.format(w=g.mmgen_idx_max_digits, *key.split('_'))
 		return '{}:{:>0{w}}'.format(w=g.mmgen_idx_max_digits, *key.split('_'))
 
 
 	out = []
 	out = []
 	for k in sorted(addrs,key=s_mmgen):
 	for k in sorted(addrs,key=s_mmgen):
 		if out and k.split('_')[0] != out[-1].split(':')[0]: out.append('')
 		if out and k.split('_')[0] != out[-1].split(':')[0]: out.append('')
 		baddr = ' ' + addrs[k][2] if showbtcaddrs else ''
 		baddr = ' ' + addrs[k][2] if showbtcaddrs else ''
-		out.append(fs % (k.replace('_',':'), baddr, addrs[k][1], normalize_btc_amt(addrs[k][0])))
+		out.append(fs % (k.replace('_',':'), baddr, addrs[k][1], addrs[k][0].fmt('3.0',color=1)))
 
 
 	o = (fs + '\n%s\nTOTAL: %s BTC') % (
 	o = (fs + '\n%s\nTOTAL: %s BTC') % (
-			'ADDRESS','','COMMENT','BALANCE', '\n'.join(out), normalize_btc_amt(total)
+			'ADDRESS','','COMMENT',' BALANCE', '\n'.join(out), total.hl()
 		)
 		)
 	if pager: do_pager(o)
 	if pager: do_pager(o)
 	else: Msg(o)
 	else: Msg(o)
@@ -456,21 +449,21 @@ def getbalance(minconf=1):
 		ma = split2(d['account'])[0]
 		ma = split2(d['account'])[0]
 		keys = ['TOTAL']
 		keys = ['TOTAL']
 		if d['spendable']: keys += ['SPENDABLE']
 		if d['spendable']: keys += ['SPENDABLE']
-		if is_mmgen_addr(ma): keys += [ma.split(':')[0]]
+		if is_mmgen_id(ma): keys += [ma.split(':')[0]]
 		confs = d['confirmations']
 		confs = d['confirmations']
 		i = (1,2)[confs >= minconf]
 		i = (1,2)[confs >= minconf]
 
 
 		for key in keys:
 		for key in keys:
-			if key not in accts: accts[key] = [Decimal('0')] * 3
+			if key not in accts: accts[key] = [BTCAmt('0')] * 3
 			for j in ([],[0])[confs==0] + [i]:
 			for j in ([],[0])[confs==0] + [i]:
 				accts[key][j] += d['amount']
 				accts[key][j] += d['amount']
 
 
-	fs = '{:12}  {:<%s} {:<%s} {:<}' % (16,16)
+	fs = '{:13} {} {} {}'
 	mc,lbl = str(minconf),'confirms'
 	mc,lbl = str(minconf),'confirms'
-	Msg(fs.format('Wallet','Unconfirmed','<%s %s'%(mc,lbl),'>=%s %s'%(mc,lbl)))
+	Msg(fs.format('Wallet',
+		*[s.ljust(16) for s in ' Unconfirmed',' <%s %s'%(mc,lbl),' >=%s %s'%(mc,lbl)]))
 	for key in sorted(accts.keys()):
 	for key in sorted(accts.keys()):
-		line = [str(normalize_btc_amt(a))+' BTC' for a in accts[key]]
-		Msg(fs.format(key+':', *line))
+		Msg(fs.format(key+':', *[a.fmt(color=True,suf=' BTC') for a in accts[key]]))
 
 
 def txview(infile,pager=False,terse=False):
 def txview(infile,pager=False,terse=False):
 	c = bitcoin_connection()
 	c = bitcoin_connection()
@@ -481,23 +474,22 @@ def twview(pager=False,reverse=False,wide=False,sort='age'):
 	from mmgen.tw import MMGenTrackingWallet
 	from mmgen.tw import MMGenTrackingWallet
 	tw = MMGenTrackingWallet()
 	tw = MMGenTrackingWallet()
 	tw.do_sort(sort,reverse=reverse)
 	tw.do_sort(sort,reverse=reverse)
-	out = tw.format(wide=wide)
+	out = tw.format_for_printing(color=True) if wide else tw.format_for_display()
 	do_pager(out) if pager else sys.stdout.write(out)
 	do_pager(out) if pager else sys.stdout.write(out)
 
 
 def add_label(mmaddr,label,remove=False):
 def add_label(mmaddr,label,remove=False):
-	if not is_mmgen_addr(mmaddr):
+	if not is_mmgen_id(mmaddr):
 		die(1,'{a}: not a valid {pnm} address'.format(pnm=pnm,a=mmaddr))
 		die(1,'{a}: not a valid {pnm} address'.format(pnm=pnm,a=mmaddr))
-	check_addr_label(label)  # Exits on failure
+	MMGenAddrLabel(label)  # Exits on failure
 
 
-	c = bitcoin_connection()
-
-	from mmgen.addr import AddrInfoList
-	btcaddr = AddrInfoList(bitcoind_connection=c).mmaddr2btcaddr(mmaddr)
+	from mmgen.addr import AddrData
+	btcaddr = AddrData(source='tw').mmaddr2btcaddr(mmaddr)
 
 
 	if not btcaddr:
 	if not btcaddr:
 		die(1,'{pnm} address {a} not found in tracking wallet'.format(
 		die(1,'{pnm} address {a} not found in tracking wallet'.format(
 				pnm=pnm,a=mmaddr))
 				pnm=pnm,a=mmaddr))
 
 
+	c = bitcoin_connection()
 	try:
 	try:
 		l = ' ' + label if label else ''
 		l = ' ' + label if label else ''
 		c.importaddress(btcaddr,mmaddr+l,False) # addr,label,rescan,p2sh
 		c.importaddress(btcaddr,mmaddr+l,False) # addr,label,rescan,p2sh
@@ -511,12 +503,12 @@ def add_label(mmaddr,label,remove=False):
 def remove_label(mmaddr): add_label(mmaddr,'',remove=True)
 def remove_label(mmaddr): add_label(mmaddr,'',remove=True)
 
 
 def addrfile_chksum(infile):
 def addrfile_chksum(infile):
-	from mmgen.addr import AddrInfo
-	AddrInfo(infile,caller='tool')
+	from mmgen.addr import AddrList
+	AddrList(infile,chksum_only=True)
 
 
 def keyaddrfile_chksum(infile):
 def keyaddrfile_chksum(infile):
-	from mmgen.addr import AddrInfo
-	AddrInfo(infile,has_keys=True,caller='tool')
+	from mmgen.addr import KeyAddrList
+	KeyAddrList(infile,chksum_only=True)
 
 
 def hexreverse(hex_str):
 def hexreverse(hex_str):
 	Msg(ba.hexlify(decode_pretty_hexdump(hex_str)[::-1]))
 	Msg(ba.hexlify(decode_pretty_hexdump(hex_str)[::-1]))

+ 128 - 106
mmgen/tw.py

@@ -22,61 +22,48 @@ tw: Tracking wallet methods for the MMGen suite
 
 
 from mmgen.common import *
 from mmgen.common import *
 from mmgen.obj import *
 from mmgen.obj import *
-from mmgen.tx import parse_mmgen_label,normalize_btc_amt
 from mmgen.term import get_char
 from mmgen.term import get_char
 
 
-class MMGenTrackingWallet(MMGenObject):
+def parse_tw_acct_label(s):
+	ret = s.split(None,1)
+	if ret and MMGenID(ret[0],on_fail='silent'):
+		if len(ret) == 2:
+			return tuple(ret)
+		else:
+			return ret[0],None
+	else:
+		return None,None
+
+class MMGenTWOutput(MMGenListItem):
+	attrs_reassign = 'label','skip'
+	attrs = 'txid','vout','amt','label','mmid','addr','confs','scriptPubKey','days','skip'
+	label = MMGenListItemAttr('label','MMGenAddrLabel')
 
 
+class MMGenTrackingWallet(MMGenObject):
 	wmsg = {
 	wmsg = {
 	'no_spendable_outputs': """
 	'no_spendable_outputs': """
 No spendable outputs found!  Import addresses with balances into your
 No spendable outputs found!  Import addresses with balances into your
 watch-only wallet using '{}-addrimport' and then re-run this program.
 watch-only wallet using '{}-addrimport' and then re-run this program.
 """.strip().format(g.proj_name)
 """.strip().format(g.proj_name)
 	}
 	}
-
-	sort_keys = 'address','age','amount','txid','mmaddr'
-	def s_address(self,i):  return i['address']
-	def s_age(self,i):      return 0 - i['confirmations']
-	def s_amount(self,i):   return i['amount']
-	def s_txid(self,i):     return '%s %03s' % (i['txid'],i['vout'])
-	def s_mmaddr(self,i):
-		if i['mmid']:
-			return '{}:{:>0{w}}'.format(
-				*i['mmid'].split(':'), w=g.mmgen_idx_max_digits)
-		else: return 'G' + i['comment']
-
-	def do_sort(self,key,reverse=None):
-		if key not in self.sort_keys:
-			fs = "'{}': invalid sort key.  Valid keys: [{}]"
-			die(2,fs.format(key,' '.join(self.sort_keys)))
-		if reverse == None: reverse = self.reverse
-		self.sort = key
-		self.unspent.sort(key=getattr(self,'s_'+key),reverse=reverse)
-
-	def sort_info(self,include_group=True):
-		ret = ([],['reverse'])[self.reverse]
-		ret.append(self.sort)
-		if include_group and self.group and (self.sort in ('address','txid')):
-			ret.append('grouped')
-		return ret
-
 	def __init__(self):
 	def __init__(self):
 		if g.bogus_wallet_data: # for debugging purposes only
 		if g.bogus_wallet_data: # for debugging purposes only
-			us = eval(get_data_from_file(g.bogus_wallet_data))
+			us_rpc = eval(get_data_from_file(g.bogus_wallet_data))
 		else:
 		else:
-			us = bitcoin_connection().listunspent()
+			us_rpc = bitcoin_connection().listunspent()
 #		write_data_to_file('bogus_unspent.json', repr(us), 'bogus unspent data')
 #		write_data_to_file('bogus_unspent.json', repr(us), 'bogus unspent data')
 #		sys.exit()
 #		sys.exit()
 
 
-		if not us: die(2,self.wmsg['no_spendable_outputs'])
-		for o in us:
-			o['mmid'],o['comment'] = parse_mmgen_label(o['account'])
-			del o['account']
-			o['skip'] = ''
-			amt = str(normalize_btc_amt(o['amount']))
-			lfill = 3 - len(amt.split('.')[0]) if '.' in amt else 3 - len(amt)
-			o['amt_fmt'] = ' '*lfill + amt
+		if not us_rpc: die(2,self.wmsg['no_spendable_outputs'])
+		for o in us_rpc:
+			o['mmid'],o['label'] = parse_tw_acct_label(o['account'])
 			o['days'] = int(o['confirmations'] * g.mins_per_block / (60*24))
 			o['days'] = int(o['confirmations'] * g.mins_per_block / (60*24))
+			o['amt'] = o['amount'] # TODO
+			o['addr'] = o['address']
+			o['confs'] = o['confirmations']
+		us = [MMGenTWOutput(**dict([(k,v) for k,v in o.items() if k in MMGenTWOutput.attrs and o[k] not in (None,'')])) for o in us_rpc]
+#		die(1,''.join([str(i)+'\n' for i in us]))
+#		die(1,''.join([pp_format(i)+'\n' for i in us_rpc]))
 
 
 		self.unspent  = us
 		self.unspent  = us
 		self.fmt_display  = ''
 		self.fmt_display  = ''
@@ -85,84 +72,117 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
 		self.reverse      = False
 		self.reverse      = False
 		self.group        = False
 		self.group        = False
 		self.show_days    = True
 		self.show_days    = True
-		self.show_mmaddr  = True
+		self.show_mmid    = True
 		self.do_sort('age')
 		self.do_sort('age')
-		self.total = sum([i['amount'] for i in self.unspent])
+		self.total        = sum([i.amt for i in self.unspent])
+
+	sort_keys = 'addr','age','amt','txid','mmid'
+	def s_addr(self,i):  return i.addr
+	def s_age(self,i):   return 0 - i.confs
+	def s_amt(self,i):   return i.amt
+	def s_txid(self,i):  return '%s %03s' % (i.txid,i.vout)
+	def s_mmid(self,i):
+		if i.mmid:
+			return '{}:{:>0{w}}'.format(
+				*i.mmid.split(':'), w=g.mmgen_idx_max_digits)
+		else: return 'G' + (i.label or '')
 
 
-	def set_cols(self):
+	def do_sort(self,key,reverse=None):
+		if key not in self.sort_keys:
+			fs = "'{}': invalid sort key.  Valid keys: [{}]"
+			die(2,fs.format(key,' '.join(self.sort_keys)))
+		if reverse == None: reverse = self.reverse
+		self.sort = key
+		self.unspent.sort(key=getattr(self,'s_'+key),reverse=reverse)
+
+	def sort_info(self,include_group=True):
+		ret = ([],['Reverse'])[self.reverse]
+		ret.append(self.sort.capitalize().replace('Mmid','MMGenId'))
+		if include_group and self.group and (self.sort in ('addr','txid','mmid')):
+			ret.append('Grouped')
+		return ret
+
+	def set_term_columns(self):
 		from mmgen.term import get_terminal_size
 		from mmgen.term import get_terminal_size
-		self.cols = get_terminal_size()[0]
-		if self.cols < g.min_screen_width:
-			m = 'A screen at least {} characters wide is required to display the tracking wallet'
-			die(2,m.format(g.min_screen_width))
+		while True:
+			self.cols = get_terminal_size()[0]
+			if self.cols >= g.min_screen_width: break
+			m1 = 'Screen too narrow to display the tracking wallet'
+			m2 = 'Please resize your screen to at least {} characters and hit ENTER '
+			my_raw_input(m1+'\n'+m2.format(g.min_screen_width))
 
 
 	def display(self):
 	def display(self):
 		msg(self.format_for_display())
 		msg(self.format_for_display())
 
 
-	def format(self,wide=False):
-		return self.format_for_printing() if wide else self.format_for_display()
-
 	def format_for_display(self):
 	def format_for_display(self):
-		unspent = self.unspent
-		total = sum([i['amount'] for i in unspent])
-		mmid_w = max(len(i['mmid']) for i in unspent)
-		self.set_cols()
+		unsp = [MMGenTWOutput(**i.__dict__) for i in self.unspent]
+		self.set_term_columns()
+
+		for i in unsp:
+			if i.label == None: i.label = ''
+			i.skip = ''
 
 
-		max_acct_len = max([len(i['mmid']+i['comment'])+1 for i in self.unspent])
-		addr_w = min(34+((1+max_acct_len) if self.show_mmaddr else 0),self.cols-46)
+		mmid_w = max(len(i.mmid or '') for i in unsp)
+		max_acct_len = max([len((i.mmid or '')+i.label)+1 for i in unsp])
+		addr_w = min(34+((1+max_acct_len) if self.show_mmid else 0),self.cols-46) + 6
 		acct_w   = min(max_acct_len, max(24,int(addr_w-10)))
 		acct_w   = min(max_acct_len, max(24,int(addr_w-10)))
 		btaddr_w = addr_w - acct_w - 1
 		btaddr_w = addr_w - acct_w - 1
+		label_w = acct_w - mmid_w - 1
 		tx_w = max(11,min(64, self.cols-addr_w-32))
 		tx_w = max(11,min(64, self.cols-addr_w-32))
 		txdots = ('','...')[tx_w < 64]
 		txdots = ('','...')[tx_w < 64]
-		fs = ' %-4s %-' + str(tx_w) + 's %-2s %-' + str(addr_w) + 's %-13s %-s'
-		table_hdr = fs % ('Num','TX id  Vout','','Address','Amount (BTC)',
-							('Conf.','Age(d)')[self.show_days])
-
-		from copy import deepcopy
-		unsp = deepcopy(unspent)
-		for i in unsp: i['skip'] = ''
-		if self.group and (self.sort in ('address','txid')):
+		fs = ' %-4s %-' + str(tx_w) + 's %-2s %s %s %s'
+		table_hdr = fs % ('Num',
+			'TX id'.ljust(tx_w - 5) + ' Vout',
+			'',
+			BTCAddr.fmtc('Address',width=addr_w+1),
+			'Amt(BTC) ',
+			('Conf.','Age(d)')[self.show_days])
+
+		if self.group and (self.sort in ('addr','txid','mmid')):
 			for a,b in [(unsp[i],unsp[i+1]) for i in range(len(unsp)-1)]:
 			for a,b in [(unsp[i],unsp[i+1]) for i in range(len(unsp)-1)]:
-				if self.sort == 'address' and a['address'] == b['address']: b['skip'] = 'addr'
-				elif self.sort == 'txid' and a['txid'] == b['txid']:        b['skip'] = 'txid'
+				for k in ('addr','txid','mmid'):
+					if self.sort == k and getattr(a,k) == getattr(b,k):
+						b.skip = (k,'addr')[k=='mmid']
 
 
-		for i in unsp:
-			addr_disp = (i['address'],'|' + '.'*33)[i['skip']=='addr']
-			mmid_disp = (i['mmid'],'.'*len(i['mmid']))[i['skip']=='addr']
-			if self.show_mmaddr:
-				dots = ('','..')[btaddr_w < len(i['address'])]
-				i['addr'] = '%s%s %s' % (
-					addr_disp[:btaddr_w-len(dots)],
-					dots, (
-					('{:<{w}} '.format(mmid_disp,w=mmid_w) if i['mmid'] else '')
-						+ i['comment'])[:acct_w]
-					)
+		hdr_fmt   = 'UNSPENT OUTPUTS (sort order: %s)  Total BTC: %s'
+		out  = [hdr_fmt % (' '.join(self.sort_info()), self.total.hl()), table_hdr]
+
+		for n,i in enumerate(unsp):
+			addr_dots = '|' + '.'*33
+			mmid_disp = (MMGenID.hlc('.'*mmid_w) \
+							if i.skip=='addr' else i.mmid.fmt(width=mmid_w,color=True)) \
+								if i.mmid else ' ' * mmid_w
+			if self.show_mmid and i.mmid:
+				addr_out = '%s %s' % (
+					type(i.addr).fmtc(addr_dots,width=btaddr_w,color=True) if i.skip == 'addr' \
+						else i.addr.fmt(width=btaddr_w,color=True),
+					'{} {}'.format(mmid_disp,i.label.fmt(width=label_w,color=True))
+				)
 			else:
 			else:
-				i['addr'] = addr_disp
+				addr_out = type(i.addr).fmtc(addr_dots,width=addr_w,color=True) if i.skip=='addr' \
+								else i.addr.fmt(width=addr_w,color=True)
 
 
-			i['tx'] = ' ' * (tx_w-4) + '|...' if i['skip'] == 'txid' \
-					else i['txid'][:tx_w-len(txdots)]+txdots
+			tx = ' ' * (tx_w-4) + '|...' if i.skip == 'txid' \
+					else i.txid[:tx_w-len(txdots)]+txdots
 
 
-		hdr_fmt   = 'UNSPENT OUTPUTS (sort order: %s)  Total BTC: %s'
-		out  = [hdr_fmt % (' '.join(self.sort_info()), normalize_btc_amt(total)), table_hdr]
-		out += [fs % (str(n+1)+')',i['tx'],i['vout'],i['addr'],i['amt_fmt'],
-					i['days'] if self.show_days else i['confirmations'])
-						for n,i in enumerate(unsp)]
-		self.fmt_display = '\n'.join(out)
+			out.append(fs % (str(n+1)+')',tx,i.vout,addr_out,i.amt.fmt(color=True),
+						i.days if self.show_days else i.confs))
+
+		self.fmt_display = '\n'.join(out) + '\n'
 		return self.fmt_display
 		return self.fmt_display
 
 
-	def format_for_printing(self):
+	def format_for_printing(self,color=False):
 
 
-		total = sum([i['amount'] for i in self.unspent])
-		fs  = ' %-4s %-67s %-34s %-14s %-12s %-8s %-6s %s'
-		out = [fs % ('Num','Tx ID,Vout','Address','{} ID'.format(g.proj_name),
-			'Amount(BTC)','Conf.','Age(d)', 'Comment')]
+		fs  = ' %-4s %-67s %s %s %s %-8s %-6s %s'
+		out = [fs % ('Num','Tx ID,Vout','Address'.ljust(34),'MMGen ID'.ljust(15),
+			'Amount(BTC)','Conf.','Age(d)', 'Label')]
 
 
 		for n,i in enumerate(self.unspent):
 		for n,i in enumerate(self.unspent):
-			addr = '=' if i['skip'] == 'addr' and self.group else i['address']
-			tx = ' ' * 63 + '=' if i['skip'] == 'txid' and self.group else str(i['txid'])
-			s = fs % (str(n+1)+')', tx+','+str(i['vout']),addr,
-					i['mmid'],i['amt_fmt'].strip(),i['confirmations'],i['days'],i['comment'])
+			addr = '=' if i.skip == 'addr' and self.group else i.addr.fmt(color=color)
+			tx = ' ' * 63 + '=' if i.skip == 'txid' and self.group else str(i.txid)
+			s = fs % (str(n+1)+')', tx+','+str(i.vout),addr,
+					(i.mmid.fmt(14,color=color) if i.mmid else ''.ljust(14)),
+					i.amt.fmt(color=color),i.confs,i.days,i.label.hl(color=color) if i.label else '')
 			out.append(s.rstrip())
 			out.append(s.rstrip())
 
 
 		fs = 'Unspent outputs ({} UTC)\nSort order: {}\n\n{}\n\nTotal BTC: {}\n'
 		fs = 'Unspent outputs ({} UTC)\nSort order: {}\n\n{}\n\nTotal BTC: {}\n'
@@ -170,51 +190,53 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
 				make_timestr(),
 				make_timestr(),
 				' '.join(self.sort_info(include_group=False)),
 				' '.join(self.sort_info(include_group=False)),
 				'\n'.join(out),
 				'\n'.join(out),
-				normalize_btc_amt(total))
+				self.total.hl(color=color))
 		return self.fmt_print
 		return self.fmt_print
 
 
 	def display_total(self):
 	def display_total(self):
 		fs = '\nTotal unspent: %s BTC (%s outputs)'
 		fs = '\nTotal unspent: %s BTC (%s outputs)'
-		msg(fs % (normalize_btc_amt(self.total), len(self.unspent)))
+		msg(fs % (self.total.hl(),len(self.unspent)))
 
 
 	def view_and_sort(self):
 	def view_and_sort(self):
 		from mmgen.term import do_pager
 		from mmgen.term import do_pager
-		s = """
+		prompt = """
 Sort options: [t]xid, [a]mount, a[d]dress, [A]ge, [r]everse, [M]mgen addr
 Sort options: [t]xid, [a]mount, a[d]dress, [A]ge, [r]everse, [M]mgen addr
 Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen
 Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen
 	""".strip()
 	""".strip()
 		self.display()
 		self.display()
-		msg(s)
+		msg(prompt)
 
 
 		p = "('q' = quit sorting, 'p' = print to file, 'v' = pager view, 'w' = wide view): "
 		p = "('q' = quit sorting, 'p' = print to file, 'v' = pager view, 'w' = wide view): "
 		while True:
 		while True:
 			reply = get_char(p, immed_chars='atDdAMrgmeqpvw')
 			reply = get_char(p, immed_chars='atDdAMrgmeqpvw')
-			if   reply == 'a': self.do_sort('amount')
+			if   reply == 'a': self.do_sort('amt')
 			elif reply == 't': self.do_sort('txid')
 			elif reply == 't': self.do_sort('txid')
 			elif reply == 'D': self.show_days = not self.show_days
 			elif reply == 'D': self.show_days = not self.show_days
-			elif reply == 'd': self.do_sort('address')
+			elif reply == 'd': self.do_sort('addr')
 			elif reply == 'A': self.do_sort('age')
 			elif reply == 'A': self.do_sort('age')
-			elif reply == 'M': self.do_sort('mmaddr'); self.show_mmaddr = True
+			elif reply == 'M': self.do_sort('mmid'); self.show_mmid = True
 			elif reply == 'r': self.unspent.reverse(); self.reverse = not self.reverse
 			elif reply == 'r': self.unspent.reverse(); self.reverse = not self.reverse
 			elif reply == 'g': self.group = not self.group
 			elif reply == 'g': self.group = not self.group
-			elif reply == 'm': self.show_mmaddr = not self.show_mmaddr
-			elif reply == 'e': msg("\n%s\n%s\n%s" % (self.fmt_display,s,p))
+			elif reply == 'm': self.show_mmid = not self.show_mmid
+			elif reply == 'e': msg("\n%s\n%s\n%s" % (self.fmt_display,prompt,p))
 			elif reply == 'q': return self.unspent
 			elif reply == 'q': return self.unspent
 			elif reply == 'p':
 			elif reply == 'p':
 				of = 'listunspent[%s].out' % ','.join(self.sort_info(include_group=False))
 				of = 'listunspent[%s].out' % ','.join(self.sort_info(include_group=False))
+				msg('')
 				write_data_to_file(of,self.format_for_printing(),'unspent outputs listing')
 				write_data_to_file(of,self.format_for_printing(),'unspent outputs listing')
 				m = yellow("Data written to '%s'" % of)
 				m = yellow("Data written to '%s'" % of)
-				msg('\n%s\n\n%s\n\n%s' % (self.fmt_display,m,s))
+				msg('\n%s\n%s\n\n%s' % (self.fmt_display,m,prompt))
 				continue
 				continue
 			elif reply == 'v':
 			elif reply == 'v':
 				do_pager(self.fmt_display)
 				do_pager(self.fmt_display)
 				continue
 				continue
 			elif reply == 'w':
 			elif reply == 'w':
-				do_pager(self.format_for_printing())
+				do_pager(self.format_for_printing(color=True))
 				continue
 				continue
 			else:
 			else:
 				msg('\nInvalid input')
 				msg('\nInvalid input')
 				continue
 				continue
 
 
+			msg('\n')
 			self.display()
 			self.display()
-			msg(s)
+			msg(prompt)

+ 151 - 220
mmgen/tx.py

@@ -20,86 +20,17 @@
 tx.py:  Bitcoin transaction routines
 tx.py:  Bitcoin transaction routines
 """
 """
 
 
-import sys, os
+import sys,os
 from stat import *
 from stat import *
-from binascii import hexlify,unhexlify
-from decimal import Decimal
-from collections import OrderedDict
-
+from binascii import unhexlify
 from mmgen.common import *
 from mmgen.common import *
+from mmgen.obj import *
 from mmgen.term import do_pager
 from mmgen.term import do_pager
 
 
-def normalize_btc_amt(amt):
-	'''Remove exponent and trailing zeros.
-	'''
-	# to_integral() needed to keep ints > 9 from being shown in exp. notation
-	if is_btc_amt(amt):
-		return amt.quantize(Decimal(1)) if amt == amt.to_integral() else amt.normalize()
-	else:
-		die(2,'%s: not a BTC amount' % amt)
-
-def is_btc_amt(amt):
-
-	if type(amt) is not Decimal:
-		msg('%s: not a decimal number' % amt)
-		return False
-
-	if amt.as_tuple()[-1] < -g.btc_amt_decimal_places:
-		msg('%s: Too many decimal places in amount' % amt)
-		return False
-
-	return True
-
-def convert_to_btc_amt(amt,return_on_fail=False):
-	# amt must be a string!
-
-	from decimal import Decimal
-	try:
-		ret = Decimal(amt)
-	except:
-		m = '%s: amount cannot be converted to decimal' % amt
-		if return_on_fail:
-			msg(m); return False
-		else:
-			die(2,m)
-
-	dmsg('Decimal(amt): %s' % repr(amt))
-
-	if ret.as_tuple()[-1] < -g.btc_amt_decimal_places:
-		m = '%s: Too many decimal places in amount' % amt
-		if return_on_fail:
-			msg(m); return False
-		else:
-			die(2,m)
-
-	if ret == 0:
-		msg('WARNING: BTC amount is zero')
-
-	return ret
-
-
-def parse_mmgen_label(s,check_label_len=False):
-	l = split2(s)
-	if not is_mmgen_addr(l[0]): return '',s
-	if check_label_len: check_addr_label(l[1])
-	return tuple(l)
-
-def is_mmgen_seed_id(s):
-	import re
-	return re.match(r'^[0123456789ABCDEF]{8}$',s) is not None
-
-def is_mmgen_idx(s):
-	try: int(s)
-	except: return False
-	return len(s) <= g.mmgen_idx_max_digits
-
-def is_mmgen_addr(s):
-	seed_id,idx = split2(s,':')
-	return is_mmgen_seed_id(seed_id) and is_mmgen_idx(idx)
-
-def is_btc_addr(s):
-	from mmgen.bitcoin import verify_addr
-	return verify_addr(s)
+def is_mmgen_seed_id(s): return SeedID(sid=s,on_fail='silent')
+def is_mmgen_idx(s):     return AddrIdx(s,on_fail='silent')
+def is_mmgen_id(s):      return MMGenID(s,on_fail='silent')
+def is_btc_addr(s):      return BTCAddr(s,on_fail='silent')
 
 
 def is_b58_str(s):
 def is_b58_str(s):
 	from mmgen.bitcoin import b58a
 	from mmgen.bitcoin import b58a
@@ -111,7 +42,7 @@ def is_wif(s):
 	from mmgen.bitcoin import wiftohex
 	from mmgen.bitcoin import wiftohex
 	return wiftohex(s,compressed) is not False
 	return wiftohex(s,compressed) is not False
 
 
-def wiftoaddr(s):
+def _wiftoaddr(s):
 	if s == '': return False
 	if s == '': return False
 	compressed = not s[0] == '5'
 	compressed = not s[0] == '5'
 	from mmgen.bitcoin import wiftohex,privnum2addr
 	from mmgen.bitcoin import wiftohex,privnum2addr
@@ -119,75 +50,51 @@ def wiftoaddr(s):
 	if not hex_key: return False
 	if not hex_key: return False
 	return privnum2addr(int(hex_key,16),compressed)
 	return privnum2addr(int(hex_key,16),compressed)
 
 
-
-def is_valid_tx_comment(s):
-
-	try: s = s.decode('utf8')
-	except:
-		msg('Invalid transaction comment (not UTF-8)')
-		return False
-
-	if len(s) > g.max_tx_comment_len:
-		msg('Invalid transaction comment (longer than %s characters)' %
-				g.max_tx_comment_len)
-		return False
-
-	return True
-
-
-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 label:
-		if ch not in g.addr_label_symbols:
-			msg("""
-'%s': illegal character in label '%s'.
-Only ASCII printable characters are permitted.
-""".strip() % (ch,label))
-			sys.exit(3)
-
-def wiftoaddr_keyconv(wif):
+def _wiftoaddr_keyconv(wif):
 	if wif[0] == '5':
 	if wif[0] == '5':
 		from subprocess import check_output
 		from subprocess import check_output
 		return check_output(['keyconv', wif]).split()[1]
 		return check_output(['keyconv', wif]).split()[1]
 	else:
 	else:
-		return wiftoaddr(wif)
+		return _wiftoaddr(wif)
 
 
 def get_wif2addr_f():
 def get_wif2addr_f():
-	if opt.no_keyconv: return wiftoaddr
+	if opt.no_keyconv: return _wiftoaddr
 	from mmgen.addr import test_for_keyconv
 	from mmgen.addr import test_for_keyconv
-	return (wiftoaddr,wiftoaddr_keyconv)[bool(test_for_keyconv())]
-
+	return (_wiftoaddr,_wiftoaddr_keyconv)[bool(test_for_keyconv())]
 
 
-def mmaddr2btcaddr_addrdata(mmaddr,addr_data,source=''):
-	seed_id,idx = mmaddr.split(':')
-	if seed_id in addr_data:
-		if idx in addr_data[seed_id]:
-			vmsg('%s -> %s%s' % (mmaddr,addr_data[seed_id][idx][0],
-				' (from %s)' % source if source else ''))
-			return addr_data[seed_id][idx]
+class MMGenTxInputOldFmt(MMGenListItem):  # for converting old tx files only
+	tr = {'amount':'amt', 'address':'addr', 'confirmations':'confs','comment':'label'}
+	attrs = 'txid','vout','amt','label','mmid','addr','confs','scriptPubKey','wif'
+	attrs_priv = 'tr',
 
 
-	return '',''
+class MMGenTxInput(MMGenListItem):
+	attrs = 'txid','vout','amt','label','mmid','addr','confs','scriptPubKey','have_wif'
+	label = MMGenListItemAttr('label','MMGenAddrLabel')
 
 
-from mmgen.obj import *
+class MMGenTxOutput(MMGenListItem):
+	attrs = 'txid','vout','amt','label','mmid','addr','have_wif'
+	label = MMGenListItemAttr('label','MMGenAddrLabel')
 
 
 class MMGenTX(MMGenObject):
 class MMGenTX(MMGenObject):
-	ext  = g.rawtx_ext
+	ext      = 'rawtx'
+	raw_ext  = 'rawtx'
+	sig_ext  = 'sigtx'
+	txid_ext = 'txid'
 	desc = 'transaction'
 	desc = 'transaction'
 
 
+	max_fee = BTCAmt('0.01')
+
 	def __init__(self,filename=None):
 	def __init__(self,filename=None):
 		self.inputs      = []
 		self.inputs      = []
-		self.outputs     = {}
+		self.inputs_enc  = []
+		self.outputs     = []
+		self.outputs_enc = []
 		self.change_addr = ''
 		self.change_addr = ''
 		self.size        = 0             # size of raw serialized tx
 		self.size        = 0             # size of raw serialized tx
-		self.fee         = Decimal('0')
-		self.send_amt    = Decimal('0')  # total amt minus change
+		self.fee         = BTCAmt('0')
+		self.send_amt    = BTCAmt('0')  # total amt minus change
 		self.hex         = ''            # raw serialized hex transaction
 		self.hex         = ''            # raw serialized hex transaction
-		self.comment     = ''
+		self.label       = MMGenTXLabel('')
 		self.txid        = ''
 		self.txid        = ''
 		self.btc_txid    = ''
 		self.btc_txid    = ''
 		self.timestamp   = ''
 		self.timestamp   = ''
@@ -195,37 +102,56 @@ class MMGenTX(MMGenObject):
 		self.fmt_data    = ''
 		self.fmt_data    = ''
 		self.blockcount  = 0
 		self.blockcount  = 0
 		if filename:
 		if filename:
-			if get_extension(filename) == g.sigtx_ext:
+			if get_extension(filename) == self.sig_ext:
 				self.mark_signed()
 				self.mark_signed()
 			self.parse_tx_file(filename)
 			self.parse_tx_file(filename)
 
 
-	def add_output(self,btcaddr,amt):
-		self.outputs[btcaddr] = (amt,)
+	def add_output(self,btcaddr,amt): # 'txid','vout','amount','label','mmid','address'
+		self.outputs.append(MMGenTxOutput(addr=btcaddr,amt=amt))
 
 
 	def del_output(self,btcaddr):
 	def del_output(self,btcaddr):
-		del self.outputs[btcaddr]
+		for i in range(len(self.outputs)):
+			if self.outputs[i].addr == btcaddr:
+				self.outputs.pop(i); return
+		raise ValueError
 
 
 	def sum_outputs(self):
 	def sum_outputs(self):
-		return sum([self.outputs[k][0] for k in self.outputs])
+		return BTCAmt(sum([e.amt for e in self.outputs]))
+
+	def add_mmaddrs_to_outputs(self,ad_w,ad_f):
+		a = [e.addr for e in self.outputs]
+		d = ad_w.make_reverse_dict(a)
+		d.update(ad_f.make_reverse_dict(a))
+		for e in self.outputs:
+			if e.addr and e.addr in d:
+				e.mmid,f = d[e.addr]
+				if f: e.label = f
+
+#	def encode_io(self,desc):
+# 		tr = getattr((MMGenTxOutput,MMGenTxInput)[desc=='inputs'],'tr')
+# 		tr_rev = dict([(v,k) for k,v in tr.items()])
+# 		return [dict([(tr_rev[e] if e in tr_rev else e,getattr(d,e)) for e in d.__dict__])
+# 					for d in getattr(self,desc)]
+#
+	def create_raw(self,c):
+		i = [{'txid':e.txid,'vout':e.vout} for e in self.inputs]
+		o = dict([(e.addr,e.amt) for e in self.outputs])
+		self.hex = c.createrawtransaction(i,o)
+		self.txid = make_chksum_6(unhexlify(self.hex)).upper()
 
 
 	# returns true if comment added or changed
 	# returns true if comment added or changed
 	def add_comment(self,infile=None):
 	def add_comment(self,infile=None):
 		if infile:
 		if infile:
-			s = get_data_from_file(infile,'transaction comment')
-			if is_valid_tx_comment(s):
-				self.comment = s.decode('utf8').strip()
-				return True
-			else:
-				sys.exit(2)
+			self.label = MMGenTXLabel(get_data_from_file(infile,'transaction comment'))
 		else: # get comment from user, or edit existing comment
 		else: # get comment from user, or edit existing comment
-			m = ('Add a comment to transaction?','Edit transaction comment?')[bool(self.comment)]
+			m = ('Add a comment to transaction?','Edit transaction comment?')[bool(self.label)]
 			if keypress_confirm(m,default_yes=False):
 			if keypress_confirm(m,default_yes=False):
 				while True:
 				while True:
-					s = my_raw_input('Comment: ',insert_txt=self.comment.encode('utf8'))
-					if is_valid_tx_comment(s):
-						csave = self.comment
-						self.comment = s.decode('utf8').strip()
-						return (True,False)[csave == self.comment]
+					s = MMGenTXLabel(my_raw_input('Comment: ',insert_txt=self.label))
+					if s:
+						lbl_save = self.label
+						self.label = s
+						return (True,False)[lbl_save == self.label]
 					else:
 					else:
 						msg('Invalid comment')
 						msg('Invalid comment')
 			return False
 			return False
@@ -237,53 +163,57 @@ class MMGenTX(MMGenObject):
 	def calculate_size_and_fee(self,fee_estimate):
 	def calculate_size_and_fee(self,fee_estimate):
 		self.size = len(self.inputs)*180 + len(self.outputs)*34 + 10
 		self.size = len(self.inputs)*180 + len(self.outputs)*34 + 10
 		if fee_estimate:
 		if fee_estimate:
-			ftype,fee = 'Calculated','{:.8f}'.format(fee_estimate*opt.tx_fee_adj*self.size / 1024)
+			ftype,fee = 'Calculated',fee_estimate*opt.tx_fee_adj*self.size / 1024
 		else:
 		else:
 			ftype,fee = 'User-selected',opt.tx_fee
 			ftype,fee = 'User-selected',opt.tx_fee
 
 
 		ufee = None
 		ufee = None
-		if not keypress_confirm('{} TX fee: {} BTC.  OK?'.format(ftype,fee),default_yes=True):
+		if not keypress_confirm('{} TX fee is {} BTC.  OK?'.format(ftype,fee.hl()),default_yes=True):
 			while True:
 			while True:
 				ufee = my_raw_input('Enter transaction fee: ')
 				ufee = my_raw_input('Enter transaction fee: ')
-				if convert_to_btc_amt(ufee,return_on_fail=True):
-					if Decimal(ufee) > g.max_tx_fee:
-						msg('{} BTC: fee too large (maximum fee: {} BTC)'.format(ufee,g.max_tx_fee))
+				if BTCAmt(ufee,on_fail='return'):
+					ufee = BTCAmt(ufee)
+					if ufee > self.max_fee:
+						msg('{} BTC: fee too large (maximum fee: {} BTC)'.format(ufee,self.max_fee))
 					else:
 					else:
 						fee = ufee
 						fee = ufee
 						break
 						break
-		self.fee = convert_to_btc_amt(fee)
+		self.fee = fee
 		vmsg('Inputs:{}  Outputs:{}  TX size:{}'.format(
 		vmsg('Inputs:{}  Outputs:{}  TX size:{}'.format(
 				len(self.inputs),len(self.outputs),self.size))
 				len(self.inputs),len(self.outputs),self.size))
 		vmsg('Fee estimate: {} (1024 bytes, {} confs)'.format(fee_estimate,opt.tx_confs))
 		vmsg('Fee estimate: {} (1024 bytes, {} confs)'.format(fee_estimate,opt.tx_confs))
 		m = ('',' (after %sx adjustment)' % opt.tx_fee_adj)[opt.tx_fee_adj != 1 and not ufee]
 		m = ('',' (after %sx adjustment)' % opt.tx_fee_adj)[opt.tx_fee_adj != 1 and not ufee]
 		vmsg('TX fee:       {}{}'.format(self.fee,m))
 		vmsg('TX fee:       {}{}'.format(self.fee,m))
 
 
-	def copy_inputs(self,source):
-		copy_keys = 'txid','vout','amount','comment','mmid','address',\
-					'confirmations','scriptPubKey'
-		self.inputs = [dict([(k,d[k] if k in d else '') for k in copy_keys]) for d in source]
+	# inputs methods
+	def list_wifs(self,desc,mmaddrs_only=False):
+		return [e.wif for e in getattr(self,desc) if e.mmid] if mmaddrs_only \
+			else [e.wif for e in getattr(self,desc)]
 
 
-	def sum_inputs(self):
-		return sum([i['amount'] for i in self.inputs])
+	def delete_attrs(self,desc,attr):
+		for e in getattr(self,desc):
+			if hasattr(e,attr): delattr(e,attr)
 
 
-	def create_raw(self,c):
-		o = dict([(k,v[0]) for k,v in self.outputs.items()])
-		self.hex = c.createrawtransaction(self.inputs,o)
-		self.txid = make_chksum_6(unhexlify(self.hex)).upper()
+	def decode_io(self,desc,data):
+		io = (MMGenTxOutput,MMGenTxInput)[desc=='inputs']
+		return [io(**dict([(k,d[k]) for k in io.attrs
+					if k in d and d[k] not in ('',None)])) for d in data]
+
+	def decode_io_oldfmt(self,data):
+		io = MMGenTxInputOldFmt
+		tr_rev = dict([(v,k) for k,v in io.tr.items()])
+		copy_keys = [tr_rev[k] if k in tr_rev else k for k in io.attrs]
+		return [io(**dict([(io.tr[k] if k in io.tr else k,d[k])
+					for k in copy_keys if k in d and d[k] != ''])) for d in data]
 
 
-# 	def make_b2m_map(self,ail_w,ail_f):
-# 		d = dict([(d['address'], (d['mmid'],d['comment']))
-# 					for d in self.inputs if d['mmid']])
-# 		d = ail_w.make_reverse_dict(self.outputs.keys())
-# 		d.update(ail_f.make_reverse_dict(self.outputs.keys()))
-# 		self.b2m_map = d
+	def copy_inputs_from_tw(self,data):
+		self.inputs = self.decode_io('inputs',[e.__dict__ for e in data])
 
 
-	def add_mmaddrs_to_outputs(self,ail_w,ail_f):
-		d = ail_w.make_reverse_dict(self.outputs.keys())
-		d.update(ail_f.make_reverse_dict(self.outputs.keys()))
-		for k in self.outputs:
-			if k in d:
-				self.outputs[k] += d[k]
+	def get_input_sids(self):
+		return set([e.mmid[:8] for e in self.inputs if e.mmid])
+
+	def sum_inputs(self):
+		return sum([e.amt for e in self.inputs])
 
 
 	def add_timestamp(self):
 	def add_timestamp(self):
 		self.timestamp = make_timestamp()
 		self.timestamp = make_timestamp()
@@ -301,20 +231,28 @@ class MMGenTX(MMGenObject):
 				(self.blockcount or 'None')
 				(self.blockcount or 'None')
 			),
 			),
 			self.hex,
 			self.hex,
-			repr(self.inputs),
-			repr(self.outputs)
-		) + ((b58encode(self.comment.encode('utf8')),) if self.comment else ())
+			repr([e.__dict__ for e in self.inputs]),
+			repr([e.__dict__ for e in self.outputs])
+		) + ((b58encode(self.label),) if self.label else ())
 		self.chksum = make_chksum_6(' '.join(lines))
 		self.chksum = make_chksum_6(' '.join(lines))
 		self.fmt_data = '\n'.join((self.chksum,) + lines)+'\n'
 		self.fmt_data = '\n'.join((self.chksum,) + lines)+'\n'
 
 
+
+	def get_non_mmaddrs(self,desc):
+		return list(set([i.addr for i in getattr(self,desc) if not i.mmid]))
+
 	# return true or false, don't exit
 	# return true or false, don't exit
-	def sign(self,c,tx_num_str,keys=None):
+	def sign(self,c,tx_num_str,keys):
+
+		if not keys:
+			msg('No keys. Cannot sign!')
+			return False
 
 
-		if keys:
-			qmsg('Passing %s key%s to bitcoind' % (len(keys),suf(keys,'k')))
-			dmsg('Keys:\n  %s' % '\n  '.join(keys))
+		qmsg('Passing %s key%s to bitcoind' % (len(keys),suf(keys,'k')))
+		dmsg('Keys:\n  %s' % '\n  '.join(keys))
 
 
-		sig_data = [dict([(k,d[k]) for k in 'txid','vout','scriptPubKey']) for d in self.inputs]
+		sig_data = [dict([(k,getattr(d,k)) for k in 'txid','vout','scriptPubKey'])
+						for d in self.inputs]
 		dmsg('Sig data:\n%s' % pp_format(sig_data))
 		dmsg('Sig data:\n%s' % pp_format(sig_data))
 		dmsg('Raw hex:\n%s' % self.hex)
 		dmsg('Raw hex:\n%s' % self.hex)
 
 
@@ -333,7 +271,7 @@ class MMGenTX(MMGenObject):
 
 
 	def mark_signed(self):
 	def mark_signed(self):
 		self.desc = 'signed transaction'
 		self.desc = 'signed transaction'
-		self.ext = g.sigtx_ext
+		self.ext = self.sig_ext
 
 
 	def check_signed(self,c):
 	def check_signed(self,c):
 		d = c.decoderawtransaction(self.hex)
 		d = c.decoderawtransaction(self.hex)
@@ -351,7 +289,7 @@ class MMGenTX(MMGenObject):
 		msg(m % self.btc_txid)
 		msg(m % self.btc_txid)
 
 
 	def write_txid_to_file(self,ask_write=False,ask_write_default_yes=True):
 	def write_txid_to_file(self,ask_write=False,ask_write_default_yes=True):
-		fn = '%s[%s].%s' % (self.txid,self.send_amt,g.txid_ext)
+		fn = '%s[%s].%s' % (self.txid,self.send_amt,self.txid_ext)
 		write_data_to_file(fn,self.btc_txid+'\n','transaction ID',
 		write_data_to_file(fn,self.btc_txid+'\n','transaction ID',
 			ask_write=ask_write,
 			ask_write=ask_write,
 			ask_write_default_yes=ask_write_default_yes)
 			ask_write_default_yes=ask_write_default_yes)
@@ -392,49 +330,45 @@ class MMGenTX(MMGenObject):
 			'Transaction {} - {} BTC - {} UTC\n'
 			'Transaction {} - {} BTC - {} UTC\n'
 		)[bool(terse)]
 		)[bool(terse)]
 
 
-		out = fs.format(self.txid,self.send_amt,self.timestamp)
+		out = fs.format(self.txid,self.send_amt.hl(),self.timestamp)
 
 
 		enl = ('\n','')[bool(terse)]
 		enl = ('\n','')[bool(terse)]
-		if self.comment:
-			out += 'Comment: %s\n%s' % (self.comment,enl)
+		if self.label:
+			out += 'Comment: %s\n%s' % (self.label.hl(),enl)
 		out += 'Inputs:\n' + enl
 		out += 'Inputs:\n' + enl
 
 
-		nonmm_str = 'non-{pnm} address'.format(pnm=g.proj_name)
-
-		for n,i in enumerate(self.inputs):
+		nonmm_str = '(non-{pnm} address)'.format(pnm=g.proj_name)
+#		for i in self.inputs: print i #DEBUG
+		for n,e in enumerate(self.inputs):
 			if blockcount:
 			if blockcount:
-				confirmations = i['confirmations'] + blockcount - self.blockcount
-				days = int(confirmations * g.mins_per_block / (60*24))
-			if not i['mmid']:
-				i['mmid'] = nonmm_str
-			mmid_fmt = ' ({:>{l}})'.format(i['mmid'],l=34-len(i['address']))
+				confs = e.confs + blockcount - self.blockcount
+				days = int(confs * g.mins_per_block / (60*24))
+			mmid_fmt = e.mmid.fmt(width=len(nonmm_str),encl='()',color=True) if e.mmid \
+						else MMGenID.hlc(nonmm_str)
 			if terse:
 			if terse:
-				out += '  %s: %-54s %s BTC' % (n+1,i['address'] + mmid_fmt,
-						normalize_btc_amt(i['amount']))
+				out += '%3s: %s %s %s BTC' % (n+1, e.addr.fmt(color=True),mmid_fmt, e.amt.hl())
 			else:
 			else:
 				for d in (
 				for d in (
-	(n+1, 'tx,vout:',       '%s,%s' % (i['txid'], i['vout'])),
-	('',  'address:',       i['address'] + mmid_fmt),
-	('',  'comment:',       i['comment']),
-	('',  'amount:',        '%s BTC' % normalize_btc_amt(i['amount'])),
-	('',  'confirmations:', '%s (around %s days)' % (confirmations,days) if blockcount else '')
+	(n+1, 'tx,vout:',       '%s,%s' % (e.txid, e.vout)),
+	('',  'address:',       e.addr.fmt(color=True) + ' ' + mmid_fmt),
+	('',  'comment:',       e.label.hl() if e.label else ''),
+	('',  'amount:',        '%s BTC' % e.amt.hl()),
+	('',  'confirmations:', '%s (around %s days)' % (confs,days) if blockcount else '')
 				):
 				):
 					if d[2]: out += ('%3s %-8s %s\n' % d)
 					if d[2]: out += ('%3s %-8s %s\n' % d)
 			out += '\n'
 			out += '\n'
 
 
 		out += 'Outputs:\n' + enl
 		out += 'Outputs:\n' + enl
-		for n,k in enumerate(self.outputs):
-			btcaddr = k
-			v = self.outputs[k]
-			btc_amt,mmid,comment = (v[0],'Non-MMGen address','') if len(v) == 1 else v
-			mmid_fmt = ' ({:>{l}})'.format(mmid,l=34-len(btcaddr))
+		for n,e in enumerate(self.outputs):
+			mmid_fmt = e.mmid.fmt(width=len(nonmm_str),encl='()',color=True) if e.mmid \
+						else MMGenID.hlc(nonmm_str)
 			if terse:
 			if terse:
-				out += '  %s: %-54s %s BTC' % (n+1, btcaddr+mmid_fmt, normalize_btc_amt(btc_amt))
+				out += '%3s: %s %s %s BTC' % (n+1, e.addr.fmt(color=True),mmid_fmt, e.amt.hl())
 			else:
 			else:
 				for d in (
 				for d in (
-						(n+1, 'address:',  btcaddr + mmid_fmt),
-						('',  'comment:',  comment),
-						('',  'amount:',   '%s BTC' % normalize_btc_amt(btc_amt))
+						(n+1, 'address:',  e.addr.fmt(color=True) + ' ' + mmid_fmt),
+						('',  'comment:',  e.label.hl() if e.label else ''),
+						('',  'amount:',   '%s BTC' % e.amt.hl())
 					):
 					):
 					if d[2]: out += ('%3s %-8s %s\n' % d)
 					if d[2]: out += ('%3s %-8s %s\n' % d)
 			out += '\n'
 			out += '\n'
@@ -447,9 +381,9 @@ class MMGenTX(MMGenObject):
 		total_in  = self.sum_inputs()
 		total_in  = self.sum_inputs()
 		total_out = self.sum_outputs()
 		total_out = self.sum_outputs()
 		out += fs % (
 		out += fs % (
-			normalize_btc_amt(total_in),
-			normalize_btc_amt(total_out),
-			normalize_btc_amt(total_in-total_out)
+			total_in.hl(),
+			total_out.hl(),
+			(total_in-total_out).hl()
 		)
 		)
 
 
 		return out
 		return out
@@ -477,15 +411,15 @@ class MMGenTX(MMGenObject):
 				err_str = 'metadata'
 				err_str = 'metadata'
 			else:
 			else:
 				self.txid,send_amt,self.timestamp,blockcount = metadata.split()
 				self.txid,send_amt,self.timestamp,blockcount = metadata.split()
-				self.send_amt = Decimal(send_amt)
+				self.send_amt = BTCAmt(send_amt)
 				self.blockcount = int(blockcount)
 				self.blockcount = int(blockcount)
 				try: unhexlify(self.hex)
 				try: unhexlify(self.hex)
 				except: err_str = 'hex data'
 				except: err_str = 'hex data'
 				else:
 				else:
-					try: self.inputs = eval(inputs_data)
+					try: self.inputs = self.decode_io('inputs',eval(inputs_data))
 					except: err_str = 'inputs data'
 					except: err_str = 'inputs data'
 					else:
 					else:
-						try: self.outputs = eval(outputs_data)
+						try: self.outputs = self.decode_io('outputs',eval(outputs_data))
 						except: err_str = 'btc-to-mmgen address map data'
 						except: err_str = 'btc-to-mmgen address map data'
 						else:
 						else:
 							if comment:
 							if comment:
@@ -494,13 +428,10 @@ class MMGenTX(MMGenObject):
 								if comment == False:
 								if comment == False:
 									err_str = 'encoded comment (not base58)'
 									err_str = 'encoded comment (not base58)'
 								else:
 								else:
-									if is_valid_tx_comment(comment):
-										self.comment = comment.decode('utf8')
-									else:
+									self.label = MMGenTXLabel(comment,on_fail='return')
+									if not self.label:
 										err_str = 'comment'
 										err_str = 'comment'
 
 
 		if err_str:
 		if err_str:
 			msg(err_fmt % err_str)
 			msg(err_fmt % err_str)
 			sys.exit(2)
 			sys.exit(2)
-
-

+ 48 - 96
mmgen/util.py

@@ -29,14 +29,31 @@ import mmgen.globalvars as g
 
 
 pnm = g.proj_name
 pnm = g.proj_name
 
 
-_red,_grn,_yel,_cya,_reset,_grnbg = \
-	['\033[%sm' % c for c in '31;1','32;1','33;1','36;1','0','30;102']
+# If 88- or 256-color support is compiled, the following apply.
+#    P s = 3 8 ; 5 ; P s -> Set foreground color to the second P s .
+#    P s = 4 8 ; 5 ; P s -> Set background color to the second P s .
+if os.environ['TERM'][-8:] == '256color':
+	_blk,_red,_grn,_yel,_blu,_mag,_cya,_bright,_dim,_ybright,_ydim,_pnk,_orng,_gry = [
+	'\033[38;5;%s;1m' % c for c in 232,210,121,229,75,90,122,231,245,187,243,218,215,246]
+	_redbg = '\033[38;5;232;48;5;210;1m'
+	_grnbg = '\033[38;5;232;48;5;121;1m'
+	_grybg = '\033[38;5;231;48;5;240;1m'
+	_reset = '\033[0m'
+else:
+	_blk,_red,_grn,_yel,_blu,_mag,_cya,_reset,_grnbg = \
+		['\033[%sm' % c for c in '30;1','31;1','32;1','33;1','34;1','35;1','36;1','0','30;102']
+	_gry = _orng = _pnk = _redbg = _ybright = _ydim = _bright = _dim = _grybg = _mag  # TODO
 
 
 def red(s):     return _red+s+_reset
 def red(s):     return _red+s+_reset
 def green(s):   return _grn+s+_reset
 def green(s):   return _grn+s+_reset
-def grnbg(s):    return _grnbg+s+_reset
+def grnbg(s):   return _grnbg+s+_reset
 def yellow(s):  return _yel+s+_reset
 def yellow(s):  return _yel+s+_reset
 def cyan(s):    return _cya+s+_reset
 def cyan(s):    return _cya+s+_reset
+def blue(s):    return _blu+s+_reset
+def pink(s):    return _pnk+s+_reset
+def orange(s):  return _orng+s+_reset
+def gray(s):    return _gry+s+_reset
+def magenta(s): return _mag+s+_reset
 def nocolor(s): return s
 def nocolor(s): return s
 
 
 def start_mscolor():
 def start_mscolor():
@@ -65,10 +82,12 @@ def mdie(*args):
 		sys.stdout.write(repr(d)+'\n')
 		sys.stdout.write(repr(d)+'\n')
 	sys.exit()
 	sys.exit()
 
 
-def die(ev,s):
-	sys.stderr.write(s+'\n'); sys.exit(ev)
-def Die(ev,s):
-	sys.stdout.write(s+'\n'); sys.exit(ev)
+def die(ev=0,s=''):
+	if s: sys.stderr.write(s+'\n')
+	sys.exit(ev)
+def Die(ev=0,s=''):
+	if s: sys.stdout.write(s+'\n')
+	sys.exit(ev)
 
 
 def pp_format(d):
 def pp_format(d):
 	import pprint
 	import pprint
@@ -144,7 +163,7 @@ def suf(arg,suf_type):
 	t = type(arg)
 	t = type(arg)
 	if t == int:
 	if t == int:
 		n = arg
 		n = arg
-	elif t == list or t == tuple or t == set:
+	elif t in (list,tuple,set,dict):
 		n = len(arg)
 		n = len(arg)
 	else:
 	else:
 		msg('%s: invalid parameter' % arg)
 		msg('%s: invalid parameter' % arg)
@@ -376,7 +395,7 @@ def _validate_addr_num(n):
 		msg("'%s': invalid %s address index" % (n,g.proj_name))
 		msg("'%s': invalid %s address index" % (n,g.proj_name))
 		return False
 		return False
 
 
-def parse_addr_idxs(arg,sep=','):
+def parse_addr_idxs(arg,sep=','):  # TODO - delete
 
 
 	ret = []
 	ret = []
 
 
@@ -517,52 +536,6 @@ def write_data_to_file(
 
 
 		return True
 		return True
 
 
-
-def _check_mmseed_format(words):
-
-	valid = False
-	desc = '%s data' % g.seed_ext
-	try:
-		chklen = len(words[0])
-	except:
-		return False
-
-	if len(words) < 3 or len(words) > 12:
-		msg('Invalid data length (%s) in %s' % (len(words),desc))
-	elif not is_hexstring(words[0]):
-		msg("Invalid format of checksum '%s' in %s"%(words[0], desc))
-	elif chklen != 6:
-		msg('Incorrect length of checksum (%s) in %s' % (chklen,desc))
-	else: valid = True
-
-	return valid
-
-
-def _check_wallet_format(infile, lines):
-
-	desc = "wallet file '%s'" % infile
-	valid = False
-	chklen = len(lines[0])
-	if len(lines) != 6:
-		vmsg('Invalid number of lines (%s) in %s' % (len(lines),desc))
-	elif chklen != 6:
-		vmsg('Incorrect length of Master checksum (%s) in %s' % (chklen,desc))
-	elif not is_hexstring(lines[0]):
-		vmsg("Invalid format of Master checksum '%s' in %s"%(lines[0], desc))
-	else: valid = True
-
-	if valid == False:
-		die(2,'Invalid %s' % desc)
-
-
-def _check_chksum_6(chk,val,desc,infile):
-	comp_chk = make_chksum_6(val)
-	if chk != comp_chk:
-		msg("%s checksum incorrect in file '%s'!" % (desc,infile))
-		die(2,'Checksum: %s. Computed value: %s' % (chk,comp_chk))
-	dmsg('%s checksum passed: %s' % (capfirst(desc),chk))
-
-
 def get_words_from_user(prompt):
 def get_words_from_user(prompt):
 	# split() also strips
 	# split() also strips
 	words = my_raw_input(prompt, echo=opt.echo_passphrase).split()
 	words = my_raw_input(prompt, echo=opt.echo_passphrase).split()
@@ -591,19 +564,25 @@ def remove_comments(lines):
 	# re.sub(pattern, repl, string, count=0, flags=0)
 	# re.sub(pattern, repl, string, count=0, flags=0)
 	ret = []
 	ret = []
 	for i in lines:
 	for i in lines:
-		i = re.sub('#.*','',i,1)
-		i = re.sub('\s+$','',i)
+		i = re.sub(ur'#.*',u'',i,1)
+		i = re.sub(ur'\s+$',u'',i)
 		if i: ret.append(i)
 		if i: ret.append(i)
 	return ret
 	return ret
 
 
-def get_lines_from_file(infile,desc='',trim_comments=False):
-	if desc != '':
-		qmsg("Getting %s from file '%s'" % (desc,infile))
-	f = open_file_or_exit(infile,'r')
-	lines = f.read().splitlines() # DOS-safe
-	f.close()
-	return remove_comments(lines) if trim_comments else lines
-
+def mmgen_decrypt_file_maybe(fn,desc=''):
+	d = get_data_from_file(fn,desc,binary=True)
+	have_enc_ext = get_extension(fn) == g.mmenc_ext
+	if have_enc_ext or not is_ascii(d):
+		m = ('Attempting to decrypt','Decrypting')[have_enc_ext]
+		msg("%s %s '%s'" % (m,desc,fn))
+		from mmgen.crypto import mmgen_decrypt_retry
+		d = mmgen_decrypt_retry(d,desc)
+	return d
+
+def get_lines_from_file(fn,desc='',trim_comments=False):
+	dec = mmgen_decrypt_file_maybe(fn,desc)
+	ret = dec.decode('utf8').splitlines() # DOS-safe
+	return remove_comments(ret) if trim_comments else ret
 
 
 def get_data_from_user(desc='data',silent=False):
 def get_data_from_user(desc='data',silent=False):
 	data = my_raw_input('Enter %s: ' % desc, echo=opt.echo_passphrase)
 	data = my_raw_input('Enter %s: ' % desc, echo=opt.echo_passphrase)
@@ -612,40 +591,13 @@ def get_data_from_user(desc='data',silent=False):
 
 
 def get_data_from_file(infile,desc='data',dash=False,silent=False,binary=False):
 def get_data_from_file(infile,desc='data',dash=False,silent=False,binary=False):
 	if dash and infile == '-': return sys.stdin.read()
 	if dash and infile == '-': return sys.stdin.read()
-	if not silent:
+	if not silent and desc:
 		qmsg("Getting %s from file '%s'" % (desc,infile))
 		qmsg("Getting %s from file '%s'" % (desc,infile))
 	f = open_file_or_exit(infile,('r','rb')[bool(binary)])
 	f = open_file_or_exit(infile,('r','rb')[bool(binary)])
 	data = f.read()
 	data = f.read()
 	f.close()
 	f.close()
 	return data
 	return data
 
 
-
-def get_seed_from_seed_data(words):
-
-	if not _check_mmseed_format(words):
-		msg('Invalid %s data' % g.seed_ext)
-		return False
-
-	stored_chk = words[0]
-	seed_b58 = ''.join(words[1:])
-
-	chk = make_chksum_6(seed_b58)
-	vmsg_r('Validating %s checksum...' % g.seed_ext)
-
-	if compare_chksums(chk, 'seed', stored_chk, 'input'):
-		from mmgen.bitcoin import b58decode_pad
-		seed = b58decode_pad(seed_b58)
-		if seed == False:
-			msg('Invalid b58 number: %s' % val)
-			return False
-
-		msg('Valid seed data for Seed ID %s' % make_chksum_8(seed))
-		return seed
-	else:
-		msg('Invalid checksum for {pnm} seed'.format(pnm=pnm))
-		return False
-
-
 passwd_file_used = False
 passwd_file_used = False
 
 
 def pwfile_reuse_warning():
 def pwfile_reuse_warning():
@@ -790,8 +742,8 @@ def get_bitcoind_cfg_options(cfg_keys):
 
 
 	cfg_file = os.path.join(get_homedir(), get_datadir(), 'bitcoin.conf')
 	cfg_file = os.path.join(get_homedir(), get_datadir(), 'bitcoin.conf')
 
 
-	cfg = dict([(k,v) for k,v in [split2(line.translate(None,'\t '),'=')
-			for line in get_lines_from_file(cfg_file)] if k in cfg_keys]) \
+	cfg = dict([(k,v) for k,v in [split2(str(line).translate(None,'\t '),'=')
+			for line in get_lines_from_file(cfg_file,'')] if k in cfg_keys]) \
 				if file_is_readable(cfg_file) else {}
 				if file_is_readable(cfg_file) else {}
 
 
 	for k in set(cfg_keys) - set(cfg.keys()): cfg[k] = ''
 	for k in set(cfg_keys) - set(cfg.keys()): cfg[k] = ''
@@ -803,7 +755,7 @@ def get_bitcoind_auth_cookie():
 	f = os.path.join(get_homedir(), get_datadir(), '.cookie')
 	f = os.path.join(get_homedir(), get_datadir(), '.cookie')
 
 
 	if file_is_readable(f):
 	if file_is_readable(f):
-		return get_lines_from_file(f)[0]
+		return get_lines_from_file(f,'')[0]
 	else:
 	else:
 		return ''
 		return ''
 
 

+ 23 - 0
scripts/compute-file-chksum.py

@@ -0,0 +1,23 @@
+#!/usr/bin/env python
+
+from mmgen.common import *
+from mmgen.util import *
+
+opts_data = {
+	'desc': 'Compute checksum for a MMGen data file',
+	'usage':'[opts] infile',
+	'options': """
+-h, --help               Print this help message.
+-i, --include-first-line Include the first line of the file (you probably don't want this)
+""".strip()
+}
+
+cmd_args = opts.init(opts_data)
+
+lines = get_lines_from_file(cmd_args[0])
+start = (1,0)[bool(opt.include_first_line)]
+a = make_chksum_6(' '.join(lines[start:]))
+if start == 1:
+	b = lines[0]
+	msg(("Checksum in file (%s) doesn't match computed value!" % b,"Checksum in file OK")[a==b])
+Msg(a)

+ 30 - 8
scripts/tx-old2new.py

@@ -94,22 +94,44 @@ def find_block_by_time(c,timestamp):
 
 
 tx = MMGenTX()
 tx = MMGenTX()
 
 
-[tx.txid,send_amt,tx.timestamp],tx.hex,inputs,b2m_map,tx.comment = parse_tx_file(cmd_args[0])
+[tx.txid,send_amt,tx.timestamp],tx.hex,inputs,b2m_map,tx.label = parse_tx_file(cmd_args[0])
 tx.send_amt = Decimal(send_amt)
 tx.send_amt = Decimal(send_amt)
 
 
 c = bitcoin_connection()
 c = bitcoin_connection()
 
 
-tx.copy_inputs(inputs)
+# attrs = 'txid','vout','amt','comment','mmid','addr','wif'
+#pp_msg(inputs)
+for i in inputs:
+	if not 'mmid' in i and 'account' in i:
+		from mmgen.tw import parse_tw_acct_label
+		a,b = parse_tw_acct_label(i['account'])
+		if a:
+			i['mmid'] = a.decode('utf8')
+			if b: i['comment'] = b.decode('utf8')
+
+#pp_msg(inputs)
+tx.inputs = tx.decode_io_oldfmt(inputs)
+
 if tx.check_signed(c):
 if tx.check_signed(c):
 	msg('Transaction is signed')
 	msg('Transaction is signed')
 
 
 dec_tx = c.decoderawtransaction(tx.hex)
 dec_tx = c.decoderawtransaction(tx.hex)
-tx.outputs = dict([(i['scriptPubKey']['addresses'][0],(i['value'],)) for i in dec_tx['vout']])
-
+tx.outputs = [MMGenTxOutput(addr=i['scriptPubKey']['addresses'][0],amt=i['value'])
+				for i in dec_tx['vout']]
+
+for e in tx.outputs:
+	if e.addr in b2m_map:
+		f = b2m_map[e.addr]
+		e.mmid = f[0]
+		if f[1]: e.label = f[1].decode('utf8')
+	else:
+		for f in tx.inputs:
+			if e.addr == f.addr and f.mmid:
+				e.mmid = f.mmid
+				if f.label: e.label = f.label.decode('utf8')
+#for i in tx.inputs: print i
+#for i in tx.outputs: print i
+#die(1,'')
 tx.blockcount = find_block_by_time(c,tx.timestamp)
 tx.blockcount = find_block_by_time(c,tx.timestamp)
 
 
-for k in tx.outputs:
-	if k in b2m_map:
-		tx.outputs[k] += b2m_map[k]
-
 tx.write_to_file(ask_write=False)
 tx.write_to_file(ask_write=False)

+ 6 - 0
test/ref/FFB367[1.234].rawtx

@@ -0,0 +1,6 @@
+2957e0
+FFB367 1.234 20150405_102927 350828
+01000000013364630b6d290a82c822facc2f7c1db4452cea459b2ce22371135530485a5d010600000000ffffffff0205d7d600010000001976a914bba3993079ccdf40c9bbbe495473f0b3d2dc5eec88ac40ef5a07000000001976a914abe58e1e45f6176910a4c1ac1ee62328d5cc4fd588ac00000000
+[{'label': u'Test Wallet', 'mmid': u'98831F3A:500', 'vout': 6, 'txid': u'015d5a483055137123e22c9b45ea2c45b41d7c2fccfa22c8820a296d0b636433', 'amt': BTCAmt('44.32452045'), 'confs': 495L, 'addr': u'1EZNuddPnaZFah9QVbGvzvTcP4KeRrRFt8', 'scriptPubKey': '76a91494b93bbe8a32f1db80b307482e83c25fa4e99b8c88ac'}]
+[{'amt': BTCAmt('43.09047045'), 'mmid': '98831F3A:3', 'addr': u'1J79LtWctedRLnMfFNRgzzSFsozQqDeoKD'}, {'amt': BTCAmt('1.23400000'), 'mmid': '98831F3A:2', 'addr': u'1GfuYaKHrhdiVybXMGCcjadSgfjvpdt2x9'}]
+3SBcsGkhcKRVB2gr98BmscU8HtWJ12HTXpJa5XmvbEUateQ3bJBEgvLd5kPGAzg1rFkzjVpZJgiKGwvnq5mJpwnbJqcHpVEAopWyALDmtjrDwEvPiTY

+ 0 - 5
test/ref/tx_FFB367[1.234].raw

@@ -1,5 +0,0 @@
-FFB367 1.234 20150405_102927
-01000000013364630b6d290a82c822facc2f7c1db4452cea459b2ce22371135530485a5d010600000000ffffffff0205d7d600010000001976a914bba3993079ccdf40c9bbbe495473f0b3d2dc5eec88ac40ef5a07000000001976a914abe58e1e45f6176910a4c1ac1ee62328d5cc4fd588ac00000000
-[{'comment': u'Test Wallet', 'mmid': u'98831F3A:500', 'vout': 6, 'txid': u'015d5a483055137123e22c9b45ea2c45b41d7c2fccfa22c8820a296d0b636433', 'amount': Decimal('44.32452045'), 'confirmations': 495L, 'address': u'1EZNuddPnaZFah9QVbGvzvTcP4KeRrRFt8', 'spendable': False, 'scriptPubKey': '76a91494b93bbe8a32f1db80b307482e83c25fa4e99b8c88ac'}]
-{u'1J79LtWctedRLnMfFNRgzzSFsozQqDeoKD': ('98831F3A:3', u''), u'1EZNuddPnaZFah9QVbGvzvTcP4KeRrRFt8': (u'98831F3A:500', u'Test Wallet'), u'1GfuYaKHrhdiVybXMGCcjadSgfjvpdt2x9': ('98831F3A:2', u'')}
-3SBcsGkhcKRVB2gr98BmscU8HtWJ12HTXpJa5XmvbEUateQ3bJBEgvLd5kPGAzg1rFkzjVpZJgiKGwvnq5mJpwnbJqcHpVEAopWyALDmtjrDwEvPiTY

BIN
test/ref/wallet-enc.dat


BIN
test/ref/wallet-unenc.dat


+ 171 - 64
test/test.py

@@ -22,6 +22,22 @@ test/test.py:  Test suite for the MMGen suite
 
 
 import sys,os
 import sys,os
 
 
+def run_in_tb():
+	fn = sys.argv[0]
+	source = open(fn)
+	try:
+		exec source in {'inside_tb':1}
+	except SystemExit:
+		pass
+	except:
+		def color(s): return '\033[36;1m' + s + '\033[0m'
+		e = sys.exc_info()
+		sys.stdout.write(color('\nTest script returned: %s\n' % (e[0].__name__)))
+
+if not 'inside_tb' in globals() and 'MMGEN_TEST_TRACEBACK' in os.environ:
+	run_in_tb()
+	sys.exit()
+
 pn = os.path.dirname(sys.argv[0])
 pn = os.path.dirname(sys.argv[0])
 os.chdir(os.path.join(pn,os.pardir))
 os.chdir(os.path.join(pn,os.pardir))
 sys.path.__setitem__(0,os.path.abspath(os.curdir))
 sys.path.__setitem__(0,os.path.abspath(os.curdir))
@@ -39,7 +55,7 @@ scripts = (
 	'walletchk', 'walletconv', 'walletgen'
 	'walletchk', 'walletconv', 'walletgen'
 )
 )
 
 
-tb_cmd = 'scripts/traceback.py'
+tb_cmd         = 'scripts/traceback.py'
 hincog_fn      = 'rand_data'
 hincog_fn      = 'rand_data'
 hincog_bytes   = 1024*1024
 hincog_bytes   = 1024*1024
 hincog_offset  = 98765
 hincog_offset  = 98765
@@ -114,19 +130,31 @@ cfgs = {
 	},
 	},
 	'4': {
 	'4': {
 		'tmpdir':        os.path.join('test','tmp4'),
 		'tmpdir':        os.path.join('test','tmp4'),
-		'wpasswd':       'Hashrate rising',
+		'wpasswd':       'Hashrate good',
 		'addr_idx_list': '63,1004,542-544,7-9', # 8 addresses
 		'addr_idx_list': '63,1004,542-544,7-9', # 8 addresses
 		'seed_len':      192,
 		'seed_len':      192,
 		'dep_generators': {
 		'dep_generators': {
 			'mmdat':       'walletgen4',
 			'mmdat':       'walletgen4',
 			'mmbrain':     'walletgen4',
 			'mmbrain':     'walletgen4',
 			'addrs':       'addrgen4',
 			'addrs':       'addrgen4',
-			'rawtx':         'txcreate4',
-			'sigtx':         'txsign4',
+			'rawtx':       'txcreate4',
+			'sigtx':       'txsign4',
 		},
 		},
 		'bw_filename': 'brainwallet.mmbrain',
 		'bw_filename': 'brainwallet.mmbrain',
 		'bw_params':   '192,1',
 		'bw_params':   '192,1',
 	},
 	},
+	'14': {
+		'kapasswd':      'Maxwell',
+		'tmpdir':        os.path.join('test','tmp14'),
+		'wpasswd':       'The Halving',
+		'addr_idx_list': '61,998,502-504,7-9', # 8 addresses
+		'seed_len':      256,
+		'dep_generators': {
+			'mmdat':       'walletgen14',
+			'addrs':       'addrgen14',
+			'akeys.mmenc': 'keyaddrgen14',
+		},
+	},
 	'5': {
 	'5': {
 		'tmpdir':        os.path.join('test','tmp5'),
 		'tmpdir':        os.path.join('test','tmp5'),
 		'wpasswd':       'My changed password',
 		'wpasswd':       'My changed password',
@@ -141,8 +169,8 @@ cfgs = {
 		'seed_len':        128,
 		'seed_len':        128,
 		'seed_id':         'FE3C6545',
 		'seed_id':         'FE3C6545',
 		'ref_bw_seed_id':  '33F10310',
 		'ref_bw_seed_id':  '33F10310',
-		'addrfile_chk':    'B230 7526 638F 38CB 8FDC 8B76',
-		'keyaddrfile_chk': 'CF83 32FB 8A8B 08E2 0F00 D601',
+		'addrfile_chk':    'B230 7526 638F 38CB',
+		'keyaddrfile_chk': 'CF83 32FB 8A8B 08E2',
 		'wpasswd':         'reference password',
 		'wpasswd':         'reference password',
 		'ref_wallet':      'FE3C6545-D782B529[128,1].mmdat',
 		'ref_wallet':      'FE3C6545-D782B529[128,1].mmdat',
 		'ic_wallet':       'FE3C6545-E29303EA-5E229E30[128,1].mmincog',
 		'ic_wallet':       'FE3C6545-E29303EA-5E229E30[128,1].mmincog',
@@ -167,8 +195,8 @@ cfgs = {
 		'seed_len':        192,
 		'seed_len':        192,
 		'seed_id':         '1378FC64',
 		'seed_id':         '1378FC64',
 		'ref_bw_seed_id':  'CE918388',
 		'ref_bw_seed_id':  'CE918388',
-		'addrfile_chk':    '8C17 A5FA 0470 6E89 3A87 8182',
-		'keyaddrfile_chk': '9648 5132 B98E 3AD9 6FC3 C5AD',
+		'addrfile_chk':    '8C17 A5FA 0470 6E89',
+		'keyaddrfile_chk': '9648 5132 B98E 3AD9',
 		'wpasswd':         'reference password',
 		'wpasswd':         'reference password',
 		'ref_wallet':      '1378FC64-6F0F9BB4[192,1].mmdat',
 		'ref_wallet':      '1378FC64-6F0F9BB4[192,1].mmdat',
 		'ic_wallet':       '1378FC64-2907DE97-F980D21F[192,1].mmincog',
 		'ic_wallet':       '1378FC64-2907DE97-F980D21F[192,1].mmincog',
@@ -193,14 +221,14 @@ cfgs = {
 		'seed_len':        256,
 		'seed_len':        256,
 		'seed_id':         '98831F3A',
 		'seed_id':         '98831F3A',
 		'ref_bw_seed_id':  'B48CD7FC',
 		'ref_bw_seed_id':  'B48CD7FC',
-		'addrfile_chk':    '6FEF 6FB9 7B13 5D91 854A 0BD3',
-		'keyaddrfile_chk': '9F2D D781 1812 8BAD C396 9DEB',
+		'addrfile_chk':    '6FEF 6FB9 7B13 5D91',
+		'keyaddrfile_chk': '9F2D D781 1812 8BAD',
 		'wpasswd':         'reference password',
 		'wpasswd':         'reference password',
 		'ref_wallet':      '98831F3A-27F2BF93[256,1].mmdat',
 		'ref_wallet':      '98831F3A-27F2BF93[256,1].mmdat',
 		'ref_addrfile':    '98831F3A[1,31-33,500-501,1010-1011].addrs',
 		'ref_addrfile':    '98831F3A[1,31-33,500-501,1010-1011].addrs',
 		'ref_keyaddrfile': '98831F3A[1,31-33,500-501,1010-1011].akeys.mmenc',
 		'ref_keyaddrfile': '98831F3A[1,31-33,500-501,1010-1011].akeys.mmenc',
-		'ref_addrfile_chksum':    '6FEF 6FB9 7B13 5D91 854A 0BD3',
-		'ref_keyaddrfile_chksum': '9F2D D781 1812 8BAD C396 9DEB',
+		'ref_addrfile_chksum':    '6FEF 6FB9 7B13 5D91',
+		'ref_keyaddrfile_chksum': '9F2D D781 1812 8BAD',
 
 
 #		'ref_fake_unspent_data':'98831F3A_unspent.json',
 #		'ref_fake_unspent_data':'98831F3A_unspent.json',
 		'ref_tx_file':     'FFB367[1.234].rawtx',
 		'ref_tx_file':     'FFB367[1.234].rawtx',
@@ -285,10 +313,13 @@ cmd_group['main'] = OrderedDict([
 	['txcreate3', (3,'tx creation with inputs and outputs from two wallets', [[['addrs'],1],[['addrs'],3]])],
 	['txcreate3', (3,'tx creation with inputs and outputs from two wallets', [[['addrs'],1],[['addrs'],3]])],
 	['txsign3',   (3,'tx signing with inputs and outputs from two wallets',[[['mmdat'],1],[['mmdat','rawtx'],3]])],
 	['txsign3',   (3,'tx signing with inputs and outputs from two wallets',[[['mmdat'],1],[['mmdat','rawtx'],3]])],
 
 
+	['walletgen14', (14,'wallet generation (14)',        [[[],14]],14)],
+	['addrgen14',   (14,'address generation (14)',        [[['mmdat'],14]])],
+	['keyaddrgen14',(14,'key-address file generation (14)', [[['mmdat'],14]],14)],
 	['walletgen4',(4,'wallet generation (4) (brainwallet)',    [])],
 	['walletgen4',(4,'wallet generation (4) (brainwallet)',    [])],
 	['addrgen4',  (4,'address generation (4)',                 [[['mmdat'],4]])],
 	['addrgen4',  (4,'address generation (4)',                 [[['mmdat'],4]])],
-	['txcreate4', (4,'tx creation with inputs and outputs from four seed sources, plus non-MMGen inputs and outputs', [[['addrs'],1],[['addrs'],2],[['addrs'],3],[['addrs'],4]])],
-	['txsign4',   (4,'tx signing with inputs and outputs from incog file, mnemonic file, wallet and brainwallet, plus non-MMGen inputs and outputs', [[['mmincog'],1],[['mmwords'],2],[['mmdat'],3],[['mmbrain','rawtx'],4]])],
+	['txcreate4', (4,'tx creation with inputs and outputs from four seed sources, key-address file and non-MMGen inputs and outputs', [[['addrs'],1],[['addrs'],2],[['addrs'],3],[['addrs'],4],[['addrs','akeys.mmenc'],14]])],
+	['txsign4',   (4,'tx signing with inputs and outputs from incog file, mnemonic file, wallet, brainwallet, key-address file and non-MMGen inputs and outputs', [[['mmincog'],1],[['mmwords'],2],[['mmdat'],3],[['mmbrain','rawtx'],4],[['akeys.mmenc'],14]])],
 ])
 ])
 
 
 cmd_group['tool'] = OrderedDict([
 cmd_group['tool'] = OrderedDict([
@@ -447,6 +478,7 @@ opts_data = {
 -I, --non-interactive Non-interactive operation (MS Windows mode)
 -I, --non-interactive Non-interactive operation (MS Windows mode)
 -p, --pause         Pause between tests, resuming on keypress.
 -p, --pause         Pause between tests, resuming on keypress.
 -q, --quiet         Produce minimal output.  Suppress dependency info.
 -q, --quiet         Produce minimal output.  Suppress dependency info.
+-r, --resume=c      Resume at command 'c' after interrupted run
 -s, --system        Test scripts and modules installed on system rather
 -s, --system        Test scripts and modules installed on system rather
                     than those in the repo root.
                     than those in the repo root.
 -S, --skip-deps     Skip dependency checking for command
 -S, --skip-deps     Skip dependency checking for command
@@ -460,6 +492,8 @@ If no command is given, the whole suite of tests is run.
 }
 }
 
 
 cmd_args = opts.init(opts_data)
 cmd_args = opts.init(opts_data)
+
+if opt.resume: opt.skip_deps = True
 if opt.log:
 if opt.log:
 	log_fd = open(log_file,'a')
 	log_fd = open(log_file,'a')
 	log_fd.write('\nLog started: %s\n' % make_timestr())
 	log_fd.write('\nLog started: %s\n' % make_timestr())
@@ -470,6 +504,8 @@ ni = bool(opt.non_interactive)
 # Disable MS color in spawned scripts due to bad interactions
 # Disable MS color in spawned scripts due to bad interactions
 os.environ['MMGEN_NOMSCOLOR'] = '1'
 os.environ['MMGEN_NOMSCOLOR'] = '1'
 os.environ['MMGEN_NOLICENSE'] = '1'
 os.environ['MMGEN_NOLICENSE'] = '1'
+os.environ['MMGEN_DISABLE_COLOR'] = '1'
+os.environ['MMGEN_MIN_URANDCHARS'] = '3'
 
 
 if opt.debug_scripts: os.environ['MMGEN_DEBUG'] = '1'
 if opt.debug_scripts: os.environ['MMGEN_DEBUG'] = '1'
 
 
@@ -599,11 +635,19 @@ def get_file_with_ext(ext,mydir,delete=True,no_dot=False):
 	else:
 	else:
 		return flist[0]
 		return flist[0]
 
 
+def find_generated_exts(cmd):
+	out = []
+	for k in cfgs:
+		for ext,prog in cfgs[k]['dep_generators'].items():
+			if prog == cmd:
+				out.append((ext,cfgs[k]['tmpdir']))
+	return out
+
 def get_addrfile_checksum(display=False):
 def get_addrfile_checksum(display=False):
 	addrfile = get_file_with_ext('addrs',cfg['tmpdir'])
 	addrfile = get_file_with_ext('addrs',cfg['tmpdir'])
 	silence()
 	silence()
-	from mmgen.addr import AddrInfo
-	chk = AddrInfo(addrfile).checksum
+	from mmgen.addr import AddrList
+	chk = AddrList(addrfile).chksum
 	if opt.verbose and display: msg('Checksum: %s' % cyan(chk))
 	if opt.verbose and display: msg('Checksum: %s' % cyan(chk))
 	end_silence()
 	end_silence()
 	return chk
 	return chk
@@ -622,6 +666,9 @@ class MMGenExpect(object):
 			mmgen_cmd = os.path.join(os.curdir,mmgen_cmd)
 			mmgen_cmd = os.path.join(os.curdir,mmgen_cmd)
 		desc = (cmd_data[name][1],name)[bool(opt.names)]
 		desc = (cmd_data[name][1],name)[bool(opt.names)]
 		if extra_desc: desc += ' ' + extra_desc
 		if extra_desc: desc += ' ' + extra_desc
+		for i in cmd_args:
+			if type(i) not in (str,unicode):
+				die(2,'Error: missing input files in cmd line?:\n%s' % cmd_args)
 		cmd_str = mmgen_cmd + ' ' + ' '.join(cmd_args)
 		cmd_str = mmgen_cmd + ' ' + ' '.join(cmd_args)
 		if opt.log:
 		if opt.log:
 			log_fd.write(cmd_str+'\n')
 			log_fd.write(cmd_str+'\n')
@@ -737,30 +784,59 @@ class MMGenExpect(object):
 	def read(self,n=None):
 	def read(self,n=None):
 		return self.p.read(n)
 		return self.p.read(n)
 
 
-from decimal import Decimal
+from mmgen.obj import BTCAmt
 from mmgen.bitcoin import verify_addr
 from mmgen.bitcoin import verify_addr
 
 
-def add_fake_unspent_entry(out,address,comment):
-	out.append({
-		'account': unicode(comment),
+def create_fake_unspent_entry(address,sid=None,idx=None,lbl=None,non_mmgen=None):
+	if lbl: lbl = ' ' + lbl
+	return {
+		'account': (non_mmgen or ('%s:%s%s' % (sid,idx,lbl))).decode('utf8'),
 		'vout': int(getrandnum(4) % 8),
 		'vout': int(getrandnum(4) % 8),
-		'txid': unicode(hexlify(os.urandom(32))),
-		'amount': Decimal('%s.%s' % (10+(getrandnum(4) % 40), getrandnum(4) % 100000000)),
+		'txid': hexlify(os.urandom(32)).decode('utf8'),
+		'amount': BTCAmt('%s.%s' % (10+(getrandnum(4) % 40), getrandnum(4) % 100000000)),
 		'address': address,
 		'address': address,
 		'spendable': False,
 		'spendable': False,
 		'scriptPubKey': ('76a914'+verify_addr(address,return_hex=True)+'88ac'),
 		'scriptPubKey': ('76a914'+verify_addr(address,return_hex=True)+'88ac'),
-		'confirmations': getrandnum(4) % 500
-	})
-
+		'confirmations': getrandnum(4) % 50000
+	}
+
+labels = [
+	"Automotive",
+	"Travel expenses",
+	"Healthcare",
+	"Freelancing 1",
+	"Freelancing 2",
+	"Alice's assets",
+	"Bob's bequest",
+	"House purchase",
+	"Real estate fund",
+	"Job 1",
+	"XYZ Corp.",
+	"Eddie's endowment",
+	"Emergency fund",
+	"Real estate fund",
+	"Ian's inheritance",
+	"",
+	"Rainy day",
+	"Fred's funds",
+	"Job 2",
+	"Carl's capital",
+]
+label_iter = None
 def create_fake_unspent_data(adata,unspent_data_file,tx_data,non_mmgen_input=''):
 def create_fake_unspent_data(adata,unspent_data_file,tx_data,non_mmgen_input=''):
 
 
 	out = []
 	out = []
 	for s in tx_data:
 	for s in tx_data:
 		sid = tx_data[s]['sid']
 		sid = tx_data[s]['sid']
-		a = adata.addrinfo(sid)
+		a = adata.addrlist(sid)
 		for n,(idx,btcaddr) in enumerate(a.addrpairs(),1):
 		for n,(idx,btcaddr) in enumerate(a.addrpairs(),1):
-			lbl = ('',' addr %02i' % n)[bool(n%3)]
-			add_fake_unspent_entry(out,btcaddr,'%s:%s%s' % (sid,idx,lbl))
+			while True:
+				try: lbl = next(label_iter)
+				except: label_iter = iter(labels)
+				else: break
+			out.append(create_fake_unspent_entry(btcaddr,sid,idx,lbl))
+			if n == 1:  # create a duplicate address. This means addrs_per_wallet += 1
+				out.append(create_fake_unspent_entry(btcaddr,sid,idx,lbl))
 
 
 	if non_mmgen_input:
 	if non_mmgen_input:
 		from mmgen.bitcoin import privnum2addr,hextowif
 		from mmgen.bitcoin import privnum2addr,hextowif
@@ -770,7 +846,7 @@ def create_fake_unspent_data(adata,unspent_data_file,tx_data,non_mmgen_input='')
 		write_data_to_file(of, hextowif('{:064x}'.format(privnum),
 		write_data_to_file(of, hextowif('{:064x}'.format(privnum),
 					compressed=True)+'\n','compressed bitcoin key',silent=True)
 					compressed=True)+'\n','compressed bitcoin key',silent=True)
 
 
-		add_fake_unspent_entry(out,btcaddr,'Non-MMGen address')
+		out.append(create_fake_unspent_entry(btcaddr,non_mmgen='Non-MMGen address'))
 
 
 #	msg('\n'.join([repr(o) for o in out])); sys.exit()
 #	msg('\n'.join([repr(o) for o in out])); sys.exit()
 	write_data_to_file(unspent_data_file,repr(out),'Unspent outputs',silent=True)
 	write_data_to_file(unspent_data_file,repr(out),'Unspent outputs',silent=True)
@@ -779,11 +855,12 @@ def create_fake_unspent_data(adata,unspent_data_file,tx_data,non_mmgen_input='')
 def add_comments_to_addr_file(addrfile,outfile):
 def add_comments_to_addr_file(addrfile,outfile):
 	silence()
 	silence()
 	msg(green("Adding comments to address file '%s'" % addrfile))
 	msg(green("Adding comments to address file '%s'" % addrfile))
-	from mmgen.addr import AddrInfo
-	a = AddrInfo(addrfile)
+	from mmgen.addr import AddrList
+	a = AddrList(addrfile)
 	for n,idx in enumerate(a.idxs(),1):
 	for n,idx in enumerate(a.idxs(),1):
 		if n % 2: a.set_comment(idx,'Test address %s' % n)
 		if n % 2: a.set_comment(idx,'Test address %s' % n)
-	write_data_to_file(outfile,a.fmt_data(enable_comments=True),silent=True)
+	a.format(enable_comments=True)
+	write_data_to_file(outfile,a.fmt_data,silent=True)
 	end_silence()
 	end_silence()
 
 
 def make_brainwallet_file(fn):
 def make_brainwallet_file(fn):
@@ -928,12 +1005,26 @@ class MMGenTestSuite(object):
 
 
 		if ni and (len(cmd_data[cmd]) < 4 or cmd_data[cmd][3] != 1): return
 		if ni and (len(cmd_data[cmd]) < 4 or cmd_data[cmd][3] != 1): return
 
 
+		# delete files produced by this cmd
+# 		for ext,tmpdir in find_generated_exts(cmd):
+# 			print cmd, get_file_with_ext(ext,tmpdir)
+
 		d = [(str(num),ext) for exts,num in cmd_data[cmd][2] for ext in exts]
 		d = [(str(num),ext) for exts,num in cmd_data[cmd][2] for ext in exts]
+
+		# delete files depended on by this cmd
 		al = [get_file_with_ext(ext,cfgs[num]['tmpdir']) for num,ext in d]
 		al = [get_file_with_ext(ext,cfgs[num]['tmpdir']) for num,ext in d]
 
 
 		global cfg
 		global cfg
 		cfg = cfgs[str(cmd_data[cmd][0])]
 		cfg = cfgs[str(cmd_data[cmd][0])]
 
 
+		if opt.resume:
+			if cmd == opt.resume:
+				msg(yellow("Resuming at '%s'" % cmd))
+				opt.resume = False
+				opt.skip_deps = False
+			else:
+				return
+
 		self.__class__.__dict__[cmd](*([self,cmd] + al))
 		self.__class__.__dict__[cmd](*([self,cmd] + al))
 
 
 	def generate_file_deps(self,cmd):
 	def generate_file_deps(self,cmd):
@@ -951,7 +1042,7 @@ class MMGenTestSuite(object):
 
 
 	def walletgen(self,name,seed_len=None):
 	def walletgen(self,name,seed_len=None):
 		write_to_tmpfile(cfg,pwfile,cfg['wpasswd']+'\n')
 		write_to_tmpfile(cfg,pwfile,cfg['wpasswd']+'\n')
-		add_args = (['-r10'],
+		add_args = (['-r5'],
 			['-q','-r0','-L','NI Wallet','-P',get_tmpfile_fn(cfg,pwfile)])[bool(ni)]
 			['-q','-r0','-L','NI Wallet','-P',get_tmpfile_fn(cfg,pwfile)])[bool(ni)]
 		args = ['-d',cfg['tmpdir'],'-p1']
 		args = ['-d',cfg['tmpdir'],'-p1']
 		if seed_len: args += ['-l',str(seed_len)]
 		if seed_len: args += ['-l',str(seed_len)]
@@ -977,7 +1068,7 @@ class MMGenTestSuite(object):
 			add_args = ['-r0', '-q', '-P%s' % get_tmpfile_fn(cfg,pwfile),
 			add_args = ['-r0', '-q', '-P%s' % get_tmpfile_fn(cfg,pwfile),
 							get_tmpfile_fn(cfg,bf)]
 							get_tmpfile_fn(cfg,bf)]
 		else:
 		else:
-			add_args = ['-r10']
+			add_args = ['-r5']
 		t = MMGenExpect(name,'mmgen-walletconv', args + add_args)
 		t = MMGenExpect(name,'mmgen-walletconv', args + add_args)
 		if ni: return
 		if ni: return
 		t.license()
 		t.license()
@@ -1038,8 +1129,8 @@ class MMGenTestSuite(object):
 	def walletchk_newpass (self,name,wf,pf):
 	def walletchk_newpass (self,name,wf,pf):
 		return self.walletchk(name,wf,pf,pw=True)
 		return self.walletchk(name,wf,pf,pw=True)
 
 
-	def addrgen(self,name,wf,pf,check_ref=False):
-		add_args = ([],['-P',pf,'-q'])[ni]
+	def addrgen(self,name,wf,pf=None,check_ref=False):
+		add_args = ([],['-q'] + ([],['-P',pf])[bool(pf)])[ni]
 		t = MMGenExpect(name,'mmgen-addrgen', add_args +
 		t = MMGenExpect(name,'mmgen-addrgen', add_args +
 				['-d',cfg['tmpdir'],wf,cfg['addr_idx_list']])
 				['-d',cfg['tmpdir'],wf,cfg['addr_idx_list']])
 		if ni: return
 		if ni: return
@@ -1074,14 +1165,14 @@ class MMGenTestSuite(object):
 
 
 	def txcreate_common(self,name,sources=['1'],non_mmgen_input=''):
 	def txcreate_common(self,name,sources=['1'],non_mmgen_input=''):
 		if opt.verbose or opt.exact_output:
 		if opt.verbose or opt.exact_output:
-			sys.stderr.write(green('Generating fake transaction info\n'))
+			sys.stderr.write(green('Generating fake tracking wallet info\n'))
 		silence()
 		silence()
-		from mmgen.addr import AddrInfo,AddrInfoList
-		tx_data,ail = {},AddrInfoList()
+		from mmgen.addr import AddrList,AddrData
+		tx_data,ad = {},AddrData()
 		for s in sources:
 		for s in sources:
 			afile = get_file_with_ext('addrs',cfgs[s]['tmpdir'])
 			afile = get_file_with_ext('addrs',cfgs[s]['tmpdir'])
-			ai = AddrInfo(afile)
-			ail.add(ai)
+			ai = AddrList(afile)
+			ad.add(ai)
 			aix = parse_addr_idxs(cfgs[s]['addr_idx_list'])
 			aix = parse_addr_idxs(cfgs[s]['addr_idx_list'])
 			if len(aix) != addrs_per_wallet:
 			if len(aix) != addrs_per_wallet:
 				errmsg(red('Address index list length != %s: %s' %
 				errmsg(red('Address index list length != %s: %s' %
@@ -1089,13 +1180,15 @@ class MMGenTestSuite(object):
 				sys.exit()
 				sys.exit()
 			tx_data[s] = {
 			tx_data[s] = {
 				'addrfile': afile,
 				'addrfile': afile,
-				'chk': ai.checksum,
+				'chk': ai.chksum,
 				'sid': ai.seed_id,
 				'sid': ai.seed_id,
 				'addr_idxs': aix[-2:],
 				'addr_idxs': aix[-2:],
 			}
 			}
 
 
 		unspent_data_file = os.path.join(cfg['tmpdir'],'unspent.json')
 		unspent_data_file = os.path.join(cfg['tmpdir'],'unspent.json')
-		create_fake_unspent_data(ail,unspent_data_file,tx_data,non_mmgen_input)
+		create_fake_unspent_data(ad,unspent_data_file,tx_data,non_mmgen_input)
+		if opt.verbose or opt.exact_output:
+			sys.stderr.write("Fake transaction wallet data written to file '%s'\n" % unspent_data_file)
 
 
 		# make the command line
 		# make the command line
 		from mmgen.bitcoin import privnum2addr
 		from mmgen.bitcoin import privnum2addr
@@ -1144,8 +1237,8 @@ class MMGenTestSuite(object):
 			t.expect('Continue anyway? (y/N): ','y')
 			t.expect('Continue anyway? (y/N): ','y')
 		t.expect(r"'q' = quit sorting, .*?: ",'M', regex=True)
 		t.expect(r"'q' = quit sorting, .*?: ",'M', regex=True)
 		t.expect(r"'q' = quit sorting, .*?: ",'q', regex=True)
 		t.expect(r"'q' = quit sorting, .*?: ",'q', regex=True)
-		outputs_list = [addrs_per_wallet*i + 1 for i in range(len(tx_data))]
-		if non_mmgen_input: outputs_list.append(len(tx_data)*addrs_per_wallet + 1)
+		outputs_list = [(addrs_per_wallet+1)*i + 1 for i in range(len(tx_data))]
+		if non_mmgen_input: outputs_list.append(len(tx_data)*(addrs_per_wallet+1) + 1)
 		t.expect('Enter a range or space-separated list of outputs to spend: ',
 		t.expect('Enter a range or space-separated list of outputs to spend: ',
 				' '.join([str(i) for i in outputs_list])+'\n')
 				' '.join([str(i) for i in outputs_list])+'\n')
 		if non_mmgen_input: t.expect('Accept? (y/N): ','y')
 		if non_mmgen_input: t.expect('Accept? (y/N): ','y')
@@ -1229,7 +1322,7 @@ class MMGenTestSuite(object):
 		self.export_seed(name,wf,desc='mnemonic data',out_fmt='words')
 		self.export_seed(name,wf,desc='mnemonic data',out_fmt='words')
 
 
 	def export_incog(self,name,wf,desc='incognito data',out_fmt='i',add_args=[]):
 	def export_incog(self,name,wf,desc='incognito data',out_fmt='i',add_args=[]):
-		uargs = ['-p1','-r10'] + add_args
+		uargs = ['-p1','-r5'] + add_args
 		self.walletconv_export(name,wf,desc=desc,out_fmt=out_fmt,uargs=uargs,pw=True)
 		self.walletconv_export(name,wf,desc=desc,out_fmt=out_fmt,uargs=uargs,pw=True)
 		ok()
 		ok()
 
 
@@ -1282,12 +1375,12 @@ class MMGenTestSuite(object):
 		self.addrgen_incog(name,[],'',in_fmt='hi',desc='hidden incognito data',
 		self.addrgen_incog(name,[],'',in_fmt='hi',desc='hidden incognito data',
 			args=['-H','%s,%s'%(rf,hincog_offset),'-l',str(hincog_seedlen)])
 			args=['-H','%s,%s'%(rf,hincog_offset),'-l',str(hincog_seedlen)])
 
 
-	def keyaddrgen(self,name,wf,pf,check_ref=False):
+	def keyaddrgen(self,name,wf,pf=None,check_ref=False):
 		args = ['-d',cfg['tmpdir'],wf,cfg['addr_idx_list']]
 		args = ['-d',cfg['tmpdir'],wf,cfg['addr_idx_list']]
 		if ni:
 		if ni:
 			m = "\nAnswer 'n' at the interactive prompt"
 			m = "\nAnswer 'n' at the interactive prompt"
 			msg(grnbg(m))
 			msg(grnbg(m))
-			args = ['-q','-P',pf] + args
+			args = ['-q'] + ([],['-P',pf])[bool(pf)] + args
 		t = MMGenExpect(name,'mmgen-keygen', args)
 		t = MMGenExpect(name,'mmgen-keygen', args)
 		if ni: return
 		if ni: return
 		t.license()
 		t.license()
@@ -1308,8 +1401,8 @@ class MMGenTestSuite(object):
 	def txsign_keyaddr(self,name,keyaddr_file,txfile):
 	def txsign_keyaddr(self,name,keyaddr_file,txfile):
 		t = MMGenExpect(name,'mmgen-txsign', ['-d',cfg['tmpdir'],'-M',keyaddr_file,txfile])
 		t = MMGenExpect(name,'mmgen-txsign', ['-d',cfg['tmpdir'],'-M',keyaddr_file,txfile])
 		t.license()
 		t.license()
-		t.hash_preset('key-address file','1')
-		t.passphrase('key-address file',cfg['kapasswd'])
+		t.hash_preset('key-address data','1')
+		t.passphrase('key-address data',cfg['kapasswd'])
 		t.expect('Check key-to-address validity? (y/N): ','y')
 		t.expect('Check key-to-address validity? (y/N): ','y')
 		t.tx_view()
 		t.tx_view()
 		self.txsign_end(t)
 		self.txsign_end(t)
@@ -1359,7 +1452,7 @@ class MMGenTestSuite(object):
 		bwf = os.path.join(cfg['tmpdir'],cfg['bw_filename'])
 		bwf = os.path.join(cfg['tmpdir'],cfg['bw_filename'])
 		make_brainwallet_file(bwf)
 		make_brainwallet_file(bwf)
 		seed_len = str(cfg['seed_len'])
 		seed_len = str(cfg['seed_len'])
-		args = ['-d',cfg['tmpdir'],'-p1','-r10','-l'+seed_len,'-ib']
+		args = ['-d',cfg['tmpdir'],'-p1','-r5','-l'+seed_len,'-ib']
 		t = MMGenExpect(name,'mmgen-walletconv', args + [bwf])
 		t = MMGenExpect(name,'mmgen-walletconv', args + [bwf])
 		t.license()
 		t.license()
 		t.passphrase_new('new MMGen wallet',cfg['wpasswd'])
 		t.passphrase_new('new MMGen wallet',cfg['wpasswd'])
@@ -1371,14 +1464,19 @@ class MMGenTestSuite(object):
 	def addrgen4(self,name,wf):
 	def addrgen4(self,name,wf):
 		self.addrgen(name,wf,pf='')
 		self.addrgen(name,wf,pf='')
 
 
-	def txcreate4(self,name,f1,f2,f3,f4):
-		self.txcreate_common(name,sources=['1','2','3','4'],non_mmgen_input='4')
+	def txcreate4(self,name,f1,f2,f3,f4,f5,f6):
+		self.txcreate_common(name,sources=['1','2','3','4','14'],non_mmgen_input='4')
 
 
-	def txsign4(self,name,f1,f2,f3,f4,f5):
+	def txsign4(self,name,f1,f2,f3,f4,f5,f6):
 		non_mm_fn = os.path.join(cfg['tmpdir'],non_mmgen_fn)
 		non_mm_fn = os.path.join(cfg['tmpdir'],non_mmgen_fn)
-		t = MMGenExpect(name,'mmgen-txsign',
-			['-d',cfg['tmpdir'],'-i','brain','-b'+cfg['bw_params'],'-p1','-k',non_mm_fn,f1,f2,f3,f4,f5])
+		a = ['-d',cfg['tmpdir'],'-i','brain','-b'+cfg['bw_params'],'-p1','-k',non_mm_fn,'-M',f6,f1,f2,f3,f4,f5]
+		t = MMGenExpect(name,'mmgen-txsign',a)
 		t.license()
 		t.license()
+
+		t.hash_preset('key-address data','1')
+		t.passphrase('key-address data',cfgs['14']['kapasswd'])
+		t.expect('Check key-to-address validity? (y/N): ','y')
+
 		t.tx_view()
 		t.tx_view()
 
 
 		for cnum,desc in ('1','incognito data'),('3','MMGen wallet'):
 		for cnum,desc in ('1','incognito data'),('3','MMGen wallet'):
@@ -1530,12 +1628,16 @@ class MMGenTestSuite(object):
 			pf = None
 			pf = None
 		self.walletchk(name,wf,pf=pf,pw=True,sid=cfg['seed_id'])
 		self.walletchk(name,wf,pf=pf,pw=True,sid=cfg['seed_id'])
 
 
-	def ref_seed_chk(self,name,ext=g.seed_ext):
+	from mmgen.seed import SeedFile
+	def ref_seed_chk(self,name,ext=SeedFile.ext):
 		wf = os.path.join(ref_dir,'%s.%s' % (cfg['seed_id'],ext))
 		wf = os.path.join(ref_dir,'%s.%s' % (cfg['seed_id'],ext))
-		desc = ('mnemonic data','seed data')[ext==g.seed_ext]
+		from mmgen.seed import SeedFile
+		desc = ('mnemonic data','seed data')[ext==SeedFile.ext]
 		self.walletchk(name,wf,pf=None,desc=desc,sid=cfg['seed_id'])
 		self.walletchk(name,wf,pf=None,desc=desc,sid=cfg['seed_id'])
 
 
-	def ref_mn_chk(self,name): self.ref_seed_chk(name,ext=g.mn_ext)
+	def ref_mn_chk(self,name):
+		from mmgen.seed import Mnemonic
+		self.ref_seed_chk(name,ext=Mnemonic.ext)
 
 
 	def ref_brain_chk(self,name,bw_file=ref_bw_file):
 	def ref_brain_chk(self,name,bw_file=ref_bw_file):
 		wf = os.path.join(ref_dir,bw_file)
 		wf = os.path.join(ref_dir,bw_file)
@@ -1593,7 +1695,7 @@ class MMGenTestSuite(object):
 			msg(grnbg('%s %s' % (m,n)))
 			msg(grnbg('%s %s' % (m,n)))
 			return
 			return
 		if ftype == 'keyaddr':
 		if ftype == 'keyaddr':
-			w = 'key-address file'
+			w = 'key-address data'
 			t.hash_preset(w,ref_kafile_hash_preset)
 			t.hash_preset(w,ref_kafile_hash_preset)
 			t.passphrase(w,ref_kafile_pass)
 			t.passphrase(w,ref_kafile_pass)
 			t.expect('Check key-to-address validity? (y/N): ','y')
 			t.expect('Check key-to-address validity? (y/N): ','y')
@@ -1631,7 +1733,7 @@ class MMGenTestSuite(object):
 
 
 	# wallet conversion tests
 	# wallet conversion tests
 	def walletconv_in(self,name,infile,desc,uopts=[],pw=False,oo=False):
 	def walletconv_in(self,name,infile,desc,uopts=[],pw=False,oo=False):
-		opts = ['-d',cfg['tmpdir'],'-o','words','-r10']
+		opts = ['-d',cfg['tmpdir'],'-o','words','-r5']
 		if_arg = [infile] if infile else []
 		if_arg = [infile] if infile else []
 		d = '(convert)'
 		d = '(convert)'
 		if ni:
 		if ni:
@@ -1685,7 +1787,7 @@ class MMGenTestSuite(object):
 				rd = os.urandom(ref_wallet_incog_offset+128)
 				rd = os.urandom(ref_wallet_incog_offset+128)
 				write_to_tmpfile(cfg,hincog_fn,rd)
 				write_to_tmpfile(cfg,hincog_fn,rd)
 		else:
 		else:
-			aa = ['-r10']
+			aa = ['-r5']
 		infile = os.path.join(ref_dir,cfg['seed_id']+'.mmwords')
 		infile = os.path.join(ref_dir,cfg['seed_id']+'.mmwords')
 		t = MMGenExpect(name,'mmgen-walletconv',aa+opts+[infile],extra_desc='(convert)')
 		t = MMGenExpect(name,'mmgen-walletconv',aa+opts+[infile],extra_desc='(convert)')
 
 
@@ -1696,7 +1798,7 @@ class MMGenTestSuite(object):
 			pf = get_tmpfile_fn(cfg,pfn)
 			pf = get_tmpfile_fn(cfg,pfn)
 			if desc != 'hidden incognito data':
 			if desc != 'hidden incognito data':
 				from mmgen.seed import SeedSource
 				from mmgen.seed import SeedSource
-				ext = SeedSource.fmt_code_to_sstype(out_fmt).ext
+				ext = SeedSource.fmt_code_to_type(out_fmt).ext
 				hps = ('',',1')[bool(pw)]   # TODO real hp
 				hps = ('',',1')[bool(pw)]   # TODO real hp
 				pre_ext = '[%s%s].' % (cfg['seed_len'],hps)
 				pre_ext = '[%s%s].' % (cfg['seed_len'],hps)
 				wf = get_file_with_ext(pre_ext+ext,cfg['tmpdir'],no_dot=True)
 				wf = get_file_with_ext(pre_ext+ext,cfg['tmpdir'],no_dot=True)
@@ -1755,6 +1857,8 @@ class MMGenTestSuite(object):
 		for i in ('1','2','3'):
 		for i in ('1','2','3'):
 			locals()[k+i] = locals()[k]
 			locals()[k+i] = locals()[k]
 
 
+	for k in ('walletgen','addrgen','keyaddrgen'): locals()[k+'14'] = locals()[k]
+
 
 
 # main()
 # main()
 if opt.pause:
 if opt.pause:
@@ -1809,10 +1913,13 @@ try:
 		clean()
 		clean()
 		for cmd in cmd_data:
 		for cmd in cmd_data:
 			if cmd[:5] == 'info_':
 			if cmd[:5] == 'info_':
-				msg(green('\nTesting ' + cmd_data[cmd][0]))
+				msg(green('%sTesting %s' % (('\n','')[bool(opt.resume)],cmd_data[cmd][0])))
 				continue
 				continue
 			ts.do_cmd(cmd)
 			ts.do_cmd(cmd)
 			if cmd is not cmd_data.keys()[-1]: do_between()
 			if cmd is not cmd_data.keys()[-1]: do_between()
+except KeyboardInterrupt:
+	die(1,'\nExiting at user request')
+	raise
 except:
 except:
 	sys.stderr = stderr_save
 	sys.stderr = stderr_save
 	raise
 	raise

+ 1 - 1
test/tooltest.py

@@ -105,7 +105,7 @@ cfg = {
 	'refdir':        'test/ref',
 	'refdir':        'test/ref',
 	'txfile':        'FFB367[1.234].rawtx',
 	'txfile':        'FFB367[1.234].rawtx',
 	'addrfile':      '98831F3A[1,31-33,500-501,1010-1011].addrs',
 	'addrfile':      '98831F3A[1,31-33,500-501,1010-1011].addrs',
-	'addrfile_chk':  '6FEF 6FB9 7B13 5D91 854A 0BD3',
+	'addrfile_chk':  '6FEF 6FB9 7B13 5D91',
 }
 }
 
 
 opts_data = {
 opts_data = {