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
 """
 
-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.bitcoin import numtowif
-from mmgen.tx import *
 from mmgen.obj import *
+from mmgen.tx import *
+from mmgen.tw import *
 
 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.
 Please install '{kconv}' from the {vgen} package on your system for much
 faster address generation.
 """.format(kconv=g.keyconv_exec, vgen='vanitygen')
-}
-
-def test_for_keyconv(silent=False):
 
 	from subprocess import check_output,STDOUT
 	try:
 		check_output([g.keyconv_exec, '-G'],stderr=STDOUT)
 	except:
-		if not silent: msg(addrmsgs['no_keyconv_msg'])
+		if not silent: msg(no_keyconv_errmsg)
 		return False
 
 	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)
 			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):
-		return self.data.keys()
+		return self.sids.keys()
 
-	def addrinfo(self,sid):
+	def addrlist(self,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):
 		btcaddr = ''
 		sid,idx = mmaddr.split(':')
 		if sid in self.seed_ids():
-			btcaddr = self.addrinfo(sid).btcaddr(int(idx))
+			btcaddr = self.addrlist(sid).btcaddr(int(idx))
 		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)
 		data,i = {},0
 		alists = c.getaddressesbyaccount([[k] for k in accts],batch=True)
 		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
-#				addrlist = c.getaddressesbyaccount(acct)
 				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:
 					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(
 				n=i,pnm=pnm,m=len(accts)))
 		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
 		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):
 		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
-
-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):
 	if b58num == '': return ''
 	# 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
-	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:
 

+ 0 - 9
mmgen/crypto.py

@@ -242,15 +242,6 @@ def mmgen_decrypt(data,desc='data',hash_preset=''):
 		msg('Incorrect passphrase or hash preset')
 		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'):
 	while True:
 		d_dec = mmgen_decrypt(d,desc)

+ 16 - 8
mmgen/filename.py

@@ -29,16 +29,24 @@ class Filename(MMGenObject):
 		self.name     = fn
 		self.dirname  = os.path.dirname(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
 		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')
 bogus_wallet_data    = os.getenv('MMGEN_BOGUS_WALLET_DATA')
 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
 
 seed_len     = 256
@@ -58,7 +56,7 @@ version   = '0.8.4'
 required_opts = [
 	'quiet','verbose','debug','outdir','echo_passphrase','passwd_file',
 	'usr_randchars','stdout','show_hash_presets','label',
-	'keep_passphrase','keep_hash_preset','brain_params'
+	'keep_passphrase','keep_hash_preset','brain_params','b16'
 ]
 incompatible_opts = (
 	('quiet','verbose'),
@@ -66,46 +64,27 @@ incompatible_opts = (
 	('tx_id', '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
-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'
 
 mins_per_block   = 9
 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
 
 hash_presets = {
@@ -125,9 +104,8 @@ mmgen_idx_max_digits = 7
 
 printable_nonl = [chr(i+32) for i in range(95)]
 printable = printable_nonl + ['\n','\t']
-
 addr_label_symbols = wallet_label_symbols = printable_nonl
 
-max_addr_label_len = 32
+max_addr_label_len   = 32
 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)):
 	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()
 
-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
 
 from mmgen.common import *
-from mmgen.addr import AddrInfo,AddrInfoEntry
+from mmgen.addr import AddrList,KeyAddrList
 
 opts_data = {
 	'desc': """Import addresses (both {pnm} and non-{pnm}) into a bitcoind
@@ -53,14 +53,9 @@ if len(cmd_args) == 1:
 	if opt.addrlist:
 		lines = get_lines_from_file(
 			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:
-		ai = AddrInfo(infile,has_keys=opt.keyaddr_file)
+		ai = (AddrList,KeyAddrList)[bool(opt.keyaddr_file)](infile)
 else:
 	die(1,"""
 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
 qmsg_r('Validating addresses...')
-for e in ai.addrdata:
+for e in ai.data:
 	if not verify_addr(e.addr,verbose=True):
 		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("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 = []
-for n,e in enumerate(ai.addrdata):
+for n,e in enumerate(ai.data):
 	if 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)
 
 	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
 """
 
-from decimal import Decimal
-
 from mmgen.common import *
 from mmgen.tx 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
 for it on the command line.
 """.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()),
 	'not_enough_btc': """
 Not enough BTC in the inputs for this transaction (%s BTC)
@@ -100,7 +97,7 @@ was specified.
 """.strip(),
 }
 
-def select_outputs(unspent,prompt):
+def select_unspent(unspent,prompt):
 
 	while True:
 		reply = my_raw_input(prompt).strip()
@@ -118,14 +115,14 @@ def select_outputs(unspent,prompt):
 		return selected
 
 
-def mmaddr2baddr(c,mmaddr,ail_w,ail_f):
+def mmaddr2baddr(c,mmaddr,ad_w,ad_f):
 
 	# assume mmaddr has already been checked
-	btc_addr = ail_w.mmaddr2btcaddr(mmaddr)
+	btc_addr = ad_w.mmaddr2btcaddr(mmaddr)
 
 	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:
 				msg(wmsg['addr_in_addrfile_only'].format(mmgenaddr=mmaddr))
 				if not keypress_confirm('Continue anyway?'):
@@ -135,16 +132,16 @@ def mmaddr2baddr(c,mmaddr,ail_w,ail_f):
 		else:
 			die(2,wmsg['addr_not_found_no_addrfile'].format(pnm=pnm,mmgenaddr=mmaddr))
 
-	return btc_addr
+	return BTCAddr(btc_addr)
 
 
 def get_fee_estimate():
-	if 'tx_fee' in opt.set_by_user:
+	if 'tx_fee' in opt.set_by_user: # TODO
 		return None
 	else:
 		ret = c.estimatefee(opt.tx_confs)
 		if ret != -1:
-			return ret
+			return BTCAmt(ret)
 		else:
 			m = """
 Fee estimation failed!
@@ -172,46 +169,41 @@ c = bitcoin_connection()
 if not opt.info:
 	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)
 
-	from mmgen.addr import AddrInfo,AddrInfoList
-	ail_f = AddrInfoList()
+	ad_f = AddrData()
 	for a in addrfiles:
 		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:
 		if ',' in a:
-			a1,a2 = split2(a,',')
+			a1,a2 = a.split(',',1)
 			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:
 				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:
 				die(2,'ERROR: More than one change address specified: %s, %s' %
 						(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:
 			die(2,'%s: unrecognized argument' % a)
 
 	if not tx.outputs:
 		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()
 
@@ -223,10 +215,10 @@ if opt.info: sys.exit()
 
 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:
-	sel_nums = select_outputs(tw.unspent,
+	sel_nums = select_unspent(tw.unspent,
 			'Enter a range or space-separated list of outputs to spend: ')
 	msg('Selected output%s: %s' % (
 			('s','')[len(sel_nums)==1],
@@ -234,31 +226,31 @@ while True:
 		))
 	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?'):
 			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
 
 	change_amt = tx.sum_inputs() - tx.send_amt - tx.fee
 
 	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):
 			break
 	else:
 		msg(wmsg['not_enough_btc'] % change_amt)
 
 if change_amt > 0:
+	change_amt = BTCAmt(change_amt)
 	if not tx.change_addr:
 		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:
 	msg('Warning: Change address will be unused as transaction produces no change')
 	tx.del_output(tx.change_addr)
@@ -270,7 +262,7 @@ dmsg('tx: %s' % tx)
 
 tx.add_comment()   # edits an existing comment
 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_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.seed import *
 from mmgen.tx import *
-from mmgen.seed import SeedSource
+from mmgen.addr import *
 
 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.
 
 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:
   {f}
 """.format(
 		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 = {
-	'mm2btc_mapping_error': """
+	'mapping_error': """
 {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),
+	'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):
@@ -121,30 +123,47 @@ def get_seed_for_seed_id(seed_id,infiles,saved_seeds):
 		elif opt.in_fmt:
 			qmsg('Need seed data for Seed ID %s' % seed_id)
 			ss = SeedSource()
-			msg('User input produced Seed ID %s' % make_chksum_8(seed))
+			msg('User input produced Seed ID %s' % ss.seed.sid)
 		else:
 			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):
-
 	seed_ids = set([i[:8] for i in mmgen_addrs])
 	vmsg('Need seed%s: %s' % (suf(seed_ids,'k'),' '.join(seed_ids)))
 	d = []
-
-	from mmgen.addr import generate_addrs
+	from mmgen.addr import KeyAddrList
 	for seed_id in seed_ids:
 		# Returns only if seed is found
 		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
 
+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
 # def sign_tx_with_bitcoind_wallet(c,tx,tx_num_str,keys):
 # 	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
 #
-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
 
@@ -262,22 +201,27 @@ for i in infiles: check_infile(i)
 c = bitcoin_connection()
 
 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:
 	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!')
 
 if not opt.info and not opt.terse_info:
 	do_license_msg(immed=True)
 
-key_data = { 'kafile':{}, 'klfile':{} }
+kal,kl = None,None
 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:
-	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 = ''
 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)
 
 	# 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:
 		msg('Unused Seed ID%s: %s' %
 			(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.seed import SeedSource
+from mmgen.obj import MMGenWalletLabel
 
 bn = os.path.basename(sys.argv[0])
 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)
 
+if opt.label:
+	opt.label = MMGenWalletLabel(opt.label,msg="Error in option '--label'")
+
 if len(cmd_args) < nargs \
 		and not opt.hidden_incog_input_params and not opt.in_fmt:
 	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' \
 			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'))
 
 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()

+ 350 - 5
mmgen/obj.py

@@ -19,9 +19,8 @@
 """
 obj.py:  The MMGenObject class and methods
 """
-import mmgen.globalvars as g
-from decimal import Decimal
 
+from decimal import *
 lvl = 0
 
 class MMGenObject(object):
@@ -40,11 +39,12 @@ class MMGenObject(object):
 		def conv(v,col_w):
 			vret = ''
 			if type(v) in (str,unicode):
+				import mmgen.globalvars as g
 				if not (set(list(v)) <= set(list(g.printable))):
 					vret = repr(v)
 				else:
 					vret = fix_linebreaks(v,fixed_indent=0)
-			elif type(v) in (int,long,Decimal):
+			elif type(v) in (int,long,BTCAmt):
 				vret = str(v)
 			elif type(v) == dict:
 				sep = '\n{}{}'.format(indent,' '*4)
@@ -68,8 +68,8 @@ class MMGenObject(object):
 
 		out = []
 		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)
 
 		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)))
 
 		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':
 			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'):
 			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:
 				return opt_unrecognized(key,val,'format code')
 			if key == 'out_fmt':

+ 7 - 6
mmgen/rpc.py

@@ -78,17 +78,17 @@ class BitcoinRPCConnection(object):
 		dmsg('=== rpc.py debug ===')
 		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):
-				if isinstance(obj, Decimal):
+				if isinstance(obj, BTCAmt):
 					return str(obj)
 				return json.JSONEncoder.default(self, obj)
 
-#		pp_msg(json.dumps(p,cls=JSONDecEncoder))
+#		pp_msg(json.dumps(p,cls=MyJSONEncoder))
 
 		try:
-			c.request('POST', '/', json.dumps(p,cls=JSONDecEncoder), {
+			c.request('POST', '/', json.dumps(p,cls=MyJSONEncoder), {
 				'Host': self.host,
 				'Authorization': 'Basic ' + base64.b64encode(self.auth_str)
 			})
@@ -112,7 +112,8 @@ class BitcoinRPCConnection(object):
 		if not r2:
 			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 = []
 
 		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)"
 		die(1, m % (opt.seed_len,seed_len))
 
-
 class Seed(MMGenObject):
 	def __init__(self,seed_bin=None):
 		if not seed_bin:
@@ -48,9 +47,12 @@ class Seed(MMGenObject):
 
 		self.data      = 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
 
+	def get_data(self):
+		return self.data
+
 
 class SeedSource(MMGenObject):
 
@@ -64,21 +66,17 @@ class SeedSource(MMGenObject):
 
 	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):
-			opt_sstype = cls.fmt_code_to_sstype(opt)
+			opt_sstype = cls.fmt_code_to_type(opt)
 			compare_or_die(
 				opt_sstype.__name__, 'input format requested on command line',
 				sstype.__name__,     'input file format'
 			)
 
 		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.seed = ss.seed
 			me.ss_in = ss
@@ -86,32 +84,28 @@ class SeedSource(MMGenObject):
 		elif fn or opt.hidden_incog_input_params:
 			if fn:
 				f = Filename(fn)
-				sstype = cls.ext_to_sstype(f.ext)
 			else:
 				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:
-				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.op = ('old','pwchg_old')[bool(passchg)]
 		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.op = ('old','pwchg_old')[bool(passchg)]
 		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.seed = Seed(seed_bin=seed or None)
 			me.op = 'new'
+#			die(1,me.seed.sid.hl()) # DEBUG
 
 		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.msg = {}
@@ -135,7 +129,7 @@ class SeedSource(MMGenObject):
 			self._decrypt_retry()
 
 		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):
 		if hasattr(self,'infile'):
@@ -162,36 +156,36 @@ class SeedSource(MMGenObject):
 				die(2,'Passphrase from password file, so exiting')
 			msg('Trying again...')
 
-	subclasses = []
-
 	@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
-	def fmt_code_to_sstype(cls,fmt_code):
+	def fmt_code_to_type(cls,fmt_code):
 		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:
 				return c
 		return None
 
 	@classmethod
-	def ext_to_sstype(cls,ext):
+	def ext_to_type(cls,ext):
 		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:
 				return c
 		return None
@@ -199,7 +193,7 @@ class SeedSource(MMGenObject):
 	@classmethod
 	def format_fmt_codes(cls):
 		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')]
 		w = max([len(a) for a,b,c in d])
 		ret = ['{:<{w}}  {:<9} {}'.format(a,b,c,w=w) for a,b,c in [
@@ -365,6 +359,8 @@ class Mnemonic (SeedSourceUnenc):
 	}
 	mn_base = 1626
 	wordlists = sorted(wl_checksums)
+	dfl_wordlist = 'electrum'
+	# dfl_wordlist = 'tirosh'
 
 	@staticmethod
 	def _mn2hex_pad(mn): return len(mn) * 8 / 3
@@ -399,7 +395,7 @@ class Mnemonic (SeedSourceUnenc):
 
 	@classmethod
 	def get_wordlist(cls,wordlist=None):
-		wordlist = wordlist or g.default_wordlist
+		wordlist = wordlist or cls.dfl_wordlist
 		if wordlist not in cls.wordlists:
 			die(1,"'%s': invalid wordlist.  Valid choices: '%s'" %
 				(wordlist,"' '".join(cls.wordlists)))
@@ -536,28 +532,31 @@ class Wallet (SeedSourceEnc):
 	ext = 'mmdat'
 
 	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
 		while True:
-			ret = my_raw_input(p)
+			msg_r(p)
+			ret = my_raw_input('')
 			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:
 					msg('Invalid label.  Trying again...')
 			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?
 	def _get_label(self):
 		if hasattr(self,'ss_in') and hasattr(self.ss_in.ssdata,'label'):
 			old_lbl = self.ss_in.ssdata.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
 			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
 			else: # Prompt, using old value as default
 				lbl = self._get_label_from_user(old_lbl)
@@ -566,7 +565,7 @@ class Wallet (SeedSourceEnc):
 				m = ("changed to '%s'" % lbl,'unchanged')[lbl==old_lbl]
 				qmsg('Label %s' % m)
 		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
 		else:
 			self._get_label_from_user()
@@ -619,7 +618,7 @@ class Wallet (SeedSourceEnc):
 		if not check_master_chksum(lines,self.desc): return False
 
 		d = self.ssdata
-		d.label = lines[1]
+		d.label = MMGenWalletLabel(lines[1])
 
 		d1,d2,d3,d4,d5 = lines[2].split()
 		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:
 				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' % (
 				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'):
 			if '  ' + command in line:
 				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:
-		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):
 	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(' [')[1].split('=')[0], i.split(' [')[1].split('=')[1][:-1]]
 		] 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):
-		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:
 			return __builtins__[arg_type](arg)
 		except:
 			die(1,"'%s': Invalid argument for argument %s ('%s' required)" % \
 				(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
 
 # 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=''):
 	if dtype == 'str': 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()
 
 	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):
 		mmaddr,comment = split2(d['account'])
 		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(':','_')
 			if key in addrs:
 				if addrs[key][2] != d['address']:
 					die(2,'duplicate BTC address ({}) for this MMGen address! ({})'.format(
 							(d['address'], addrs[key][2])))
 			else:
-				addrs[key] = [Decimal('0'),comment,d['address']]
+				addrs[key] = [BTCAmt('0'),comment,d['address']]
 			addrs[key][0] += d['amount']
 			total += d['amount']
 
@@ -410,11 +403,11 @@ def listaddresses(addrs='',minconf=1,showempty=False,pager=False,showbtcaddrs=Fa
 		for acct in accts:
 			mmaddr,comment = split2(acct)
 			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(':','_')
 				if key not in addrs:
 					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)):
 			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
 	)
 
-	def s_mmgen(key):
+	def s_mmgen(key): # TODO
 		return '{}:{:>0{w}}'.format(w=g.mmgen_idx_max_digits, *key.split('_'))
 
 	out = []
 	for k in sorted(addrs,key=s_mmgen):
 		if out and k.split('_')[0] != out[-1].split(':')[0]: out.append('')
 		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') % (
-			'ADDRESS','','COMMENT','BALANCE', '\n'.join(out), normalize_btc_amt(total)
+			'ADDRESS','','COMMENT',' BALANCE', '\n'.join(out), total.hl()
 		)
 	if pager: do_pager(o)
 	else: Msg(o)
@@ -456,21 +449,21 @@ def getbalance(minconf=1):
 		ma = split2(d['account'])[0]
 		keys = ['TOTAL']
 		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']
 		i = (1,2)[confs >= minconf]
 
 		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]:
 				accts[key][j] += d['amount']
 
-	fs = '{:12}  {:<%s} {:<%s} {:<}' % (16,16)
+	fs = '{:13} {} {} {}'
 	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()):
-		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):
 	c = bitcoin_connection()
@@ -481,23 +474,22 @@ def twview(pager=False,reverse=False,wide=False,sort='age'):
 	from mmgen.tw import MMGenTrackingWallet
 	tw = MMGenTrackingWallet()
 	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)
 
 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))
-	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:
 		die(1,'{pnm} address {a} not found in tracking wallet'.format(
 				pnm=pnm,a=mmaddr))
 
+	c = bitcoin_connection()
 	try:
 		l = ' ' + label if label else ''
 		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 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):
-	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):
 	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.obj import *
-from mmgen.tx import parse_mmgen_label,normalize_btc_amt
 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 = {
 	'no_spendable_outputs': """
 No spendable outputs found!  Import addresses with balances into your
 watch-only wallet using '{}-addrimport' and then re-run this program.
 """.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):
 		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:
-			us = bitcoin_connection().listunspent()
+			us_rpc = bitcoin_connection().listunspent()
 #		write_data_to_file('bogus_unspent.json', repr(us), 'bogus unspent data')
 #		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['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.fmt_display  = ''
@@ -85,84 +72,117 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
 		self.reverse      = False
 		self.group        = False
 		self.show_days    = True
-		self.show_mmaddr  = True
+		self.show_mmid    = True
 		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
-		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):
 		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):
-		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)))
 		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))
 		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)]:
-				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:
-				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
 
-	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):
-			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())
 
 		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(),
 				' '.join(self.sort_info(include_group=False)),
 				'\n'.join(out),
-				normalize_btc_amt(total))
+				self.total.hl(color=color))
 		return self.fmt_print
 
 	def display_total(self):
 		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):
 		from mmgen.term import do_pager
-		s = """
+		prompt = """
 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
 	""".strip()
 		self.display()
-		msg(s)
+		msg(prompt)
 
 		p = "('q' = quit sorting, 'p' = print to file, 'v' = pager view, 'w' = wide view): "
 		while True:
 			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 == '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 == '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 == '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 == 'p':
 				of = 'listunspent[%s].out' % ','.join(self.sort_info(include_group=False))
+				msg('')
 				write_data_to_file(of,self.format_for_printing(),'unspent outputs listing')
 				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
 			elif reply == 'v':
 				do_pager(self.fmt_display)
 				continue
 			elif reply == 'w':
-				do_pager(self.format_for_printing())
+				do_pager(self.format_for_printing(color=True))
 				continue
 			else:
 				msg('\nInvalid input')
 				continue
 
+			msg('\n')
 			self.display()
-			msg(s)
+			msg(prompt)

+ 151 - 220
mmgen/tx.py

@@ -20,86 +20,17 @@
 tx.py:  Bitcoin transaction routines
 """
 
-import sys, os
+import sys,os
 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.obj import *
 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):
 	from mmgen.bitcoin import b58a
@@ -111,7 +42,7 @@ def is_wif(s):
 	from mmgen.bitcoin import wiftohex
 	return wiftohex(s,compressed) is not False
 
-def wiftoaddr(s):
+def _wiftoaddr(s):
 	if s == '': return False
 	compressed = not s[0] == '5'
 	from mmgen.bitcoin import wiftohex,privnum2addr
@@ -119,75 +50,51 @@ def wiftoaddr(s):
 	if not hex_key: return False
 	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':
 		from subprocess import check_output
 		return check_output(['keyconv', wif]).split()[1]
 	else:
-		return wiftoaddr(wif)
+		return _wiftoaddr(wif)
 
 def get_wif2addr_f():
-	if opt.no_keyconv: return wiftoaddr
+	if opt.no_keyconv: return _wiftoaddr
 	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):
-	ext  = g.rawtx_ext
+	ext      = 'rawtx'
+	raw_ext  = 'rawtx'
+	sig_ext  = 'sigtx'
+	txid_ext = 'txid'
 	desc = 'transaction'
 
+	max_fee = BTCAmt('0.01')
+
 	def __init__(self,filename=None):
 		self.inputs      = []
-		self.outputs     = {}
+		self.inputs_enc  = []
+		self.outputs     = []
+		self.outputs_enc = []
 		self.change_addr = ''
 		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.comment     = ''
+		self.label       = MMGenTXLabel('')
 		self.txid        = ''
 		self.btc_txid    = ''
 		self.timestamp   = ''
@@ -195,37 +102,56 @@ class MMGenTX(MMGenObject):
 		self.fmt_data    = ''
 		self.blockcount  = 0
 		if filename:
-			if get_extension(filename) == g.sigtx_ext:
+			if get_extension(filename) == self.sig_ext:
 				self.mark_signed()
 			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):
-		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):
-		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
 	def add_comment(self,infile=None):
 		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
-			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):
 				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:
 						msg('Invalid comment')
 			return False
@@ -237,53 +163,57 @@ class MMGenTX(MMGenObject):
 	def calculate_size_and_fee(self,fee_estimate):
 		self.size = len(self.inputs)*180 + len(self.outputs)*34 + 10
 		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:
 			ftype,fee = 'User-selected',opt.tx_fee
 
 		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:
 				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:
 						fee = ufee
 						break
-		self.fee = convert_to_btc_amt(fee)
+		self.fee = fee
 		vmsg('Inputs:{}  Outputs:{}  TX size:{}'.format(
 				len(self.inputs),len(self.outputs),self.size))
 		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]
 		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):
 		self.timestamp = make_timestamp()
@@ -301,20 +231,28 @@ class MMGenTX(MMGenObject):
 				(self.blockcount or 'None')
 			),
 			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.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
-	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('Raw hex:\n%s' % self.hex)
 
@@ -333,7 +271,7 @@ class MMGenTX(MMGenObject):
 
 	def mark_signed(self):
 		self.desc = 'signed transaction'
-		self.ext = g.sigtx_ext
+		self.ext = self.sig_ext
 
 	def check_signed(self,c):
 		d = c.decoderawtransaction(self.hex)
@@ -351,7 +289,7 @@ class MMGenTX(MMGenObject):
 		msg(m % self.btc_txid)
 
 	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',
 			ask_write=ask_write,
 			ask_write_default_yes=ask_write_default_yes)
@@ -392,49 +330,45 @@ class MMGenTX(MMGenObject):
 			'Transaction {} - {} BTC - {} UTC\n'
 		)[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)]
-		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
 
-		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:
-				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:
-				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:
 				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)
 			out += '\n'
 
 		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:
-				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:
 				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)
 			out += '\n'
@@ -447,9 +381,9 @@ class MMGenTX(MMGenObject):
 		total_in  = self.sum_inputs()
 		total_out = self.sum_outputs()
 		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
@@ -477,15 +411,15 @@ class MMGenTX(MMGenObject):
 				err_str = 'metadata'
 			else:
 				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)
 				try: unhexlify(self.hex)
 				except: err_str = 'hex data'
 				else:
-					try: self.inputs = eval(inputs_data)
+					try: self.inputs = self.decode_io('inputs',eval(inputs_data))
 					except: err_str = 'inputs data'
 					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'
 						else:
 							if comment:
@@ -494,13 +428,10 @@ class MMGenTX(MMGenObject):
 								if comment == False:
 									err_str = 'encoded comment (not base58)'
 								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'
 
 		if err_str:
 			msg(err_fmt % err_str)
 			sys.exit(2)
-
-

+ 48 - 96
mmgen/util.py

@@ -29,14 +29,31 @@ import mmgen.globalvars as g
 
 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 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 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 start_mscolor():
@@ -65,10 +82,12 @@ def mdie(*args):
 		sys.stdout.write(repr(d)+'\n')
 	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):
 	import pprint
@@ -144,7 +163,7 @@ def suf(arg,suf_type):
 	t = type(arg)
 	if t == int:
 		n = arg
-	elif t == list or t == tuple or t == set:
+	elif t in (list,tuple,set,dict):
 		n = len(arg)
 	else:
 		msg('%s: invalid parameter' % arg)
@@ -376,7 +395,7 @@ def _validate_addr_num(n):
 		msg("'%s': invalid %s address index" % (n,g.proj_name))
 		return False
 
-def parse_addr_idxs(arg,sep=','):
+def parse_addr_idxs(arg,sep=','):  # TODO - delete
 
 	ret = []
 
@@ -517,52 +536,6 @@ def write_data_to_file(
 
 		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):
 	# split() also strips
 	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)
 	ret = []
 	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)
 	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):
 	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):
 	if dash and infile == '-': return sys.stdin.read()
-	if not silent:
+	if not silent and desc:
 		qmsg("Getting %s from file '%s'" % (desc,infile))
 	f = open_file_or_exit(infile,('r','rb')[bool(binary)])
 	data = f.read()
 	f.close()
 	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
 
 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 = 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 {}
 
 	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')
 
 	if file_is_readable(f):
-		return get_lines_from_file(f)[0]
+		return get_lines_from_file(f,'')[0]
 	else:
 		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.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)
 
 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):
 	msg('Transaction is signed')
 
 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)
 
-for k in tx.outputs:
-	if k in b2m_map:
-		tx.outputs[k] += b2m_map[k]
-
 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
 
+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])
 os.chdir(os.path.join(pn,os.pardir))
 sys.path.__setitem__(0,os.path.abspath(os.curdir))
@@ -39,7 +55,7 @@ scripts = (
 	'walletchk', 'walletconv', 'walletgen'
 )
 
-tb_cmd = 'scripts/traceback.py'
+tb_cmd         = 'scripts/traceback.py'
 hincog_fn      = 'rand_data'
 hincog_bytes   = 1024*1024
 hincog_offset  = 98765
@@ -114,19 +130,31 @@ cfgs = {
 	},
 	'4': {
 		'tmpdir':        os.path.join('test','tmp4'),
-		'wpasswd':       'Hashrate rising',
+		'wpasswd':       'Hashrate good',
 		'addr_idx_list': '63,1004,542-544,7-9', # 8 addresses
 		'seed_len':      192,
 		'dep_generators': {
 			'mmdat':       'walletgen4',
 			'mmbrain':     'walletgen4',
 			'addrs':       'addrgen4',
-			'rawtx':         'txcreate4',
-			'sigtx':         'txsign4',
+			'rawtx':       'txcreate4',
+			'sigtx':       'txsign4',
 		},
 		'bw_filename': 'brainwallet.mmbrain',
 		'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': {
 		'tmpdir':        os.path.join('test','tmp5'),
 		'wpasswd':       'My changed password',
@@ -141,8 +169,8 @@ cfgs = {
 		'seed_len':        128,
 		'seed_id':         'FE3C6545',
 		'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',
 		'ref_wallet':      'FE3C6545-D782B529[128,1].mmdat',
 		'ic_wallet':       'FE3C6545-E29303EA-5E229E30[128,1].mmincog',
@@ -167,8 +195,8 @@ cfgs = {
 		'seed_len':        192,
 		'seed_id':         '1378FC64',
 		'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',
 		'ref_wallet':      '1378FC64-6F0F9BB4[192,1].mmdat',
 		'ic_wallet':       '1378FC64-2907DE97-F980D21F[192,1].mmincog',
@@ -193,14 +221,14 @@ cfgs = {
 		'seed_len':        256,
 		'seed_id':         '98831F3A',
 		'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',
 		'ref_wallet':      '98831F3A-27F2BF93[256,1].mmdat',
 		'ref_addrfile':    '98831F3A[1,31-33,500-501,1010-1011].addrs',
 		'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_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]])],
 	['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)',    [])],
 	['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([
@@ -447,6 +478,7 @@ opts_data = {
 -I, --non-interactive Non-interactive operation (MS Windows mode)
 -p, --pause         Pause between tests, resuming on keypress.
 -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
                     than those in the repo root.
 -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)
+
+if opt.resume: opt.skip_deps = True
 if opt.log:
 	log_fd = open(log_file,'a')
 	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
 os.environ['MMGEN_NOMSCOLOR'] = '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'
 
@@ -599,11 +635,19 @@ def get_file_with_ext(ext,mydir,delete=True,no_dot=False):
 	else:
 		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):
 	addrfile = get_file_with_ext('addrs',cfg['tmpdir'])
 	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))
 	end_silence()
 	return chk
@@ -622,6 +666,9 @@ class MMGenExpect(object):
 			mmgen_cmd = os.path.join(os.curdir,mmgen_cmd)
 		desc = (cmd_data[name][1],name)[bool(opt.names)]
 		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)
 		if opt.log:
 			log_fd.write(cmd_str+'\n')
@@ -737,30 +784,59 @@ class MMGenExpect(object):
 	def read(self,n=None):
 		return self.p.read(n)
 
-from decimal import Decimal
+from mmgen.obj import BTCAmt
 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),
-		'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,
 		'spendable': False,
 		'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=''):
 
 	out = []
 	for s in tx_data:
 		sid = tx_data[s]['sid']
-		a = adata.addrinfo(sid)
+		a = adata.addrlist(sid)
 		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:
 		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),
 					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()
 	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):
 	silence()
 	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):
 		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()
 
 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
 
+		# 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]
+
+		# delete files depended on by this cmd
 		al = [get_file_with_ext(ext,cfgs[num]['tmpdir']) for num,ext in d]
 
 		global cfg
 		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))
 
 	def generate_file_deps(self,cmd):
@@ -951,7 +1042,7 @@ class MMGenTestSuite(object):
 
 	def walletgen(self,name,seed_len=None):
 		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)]
 		args = ['-d',cfg['tmpdir'],'-p1']
 		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),
 							get_tmpfile_fn(cfg,bf)]
 		else:
-			add_args = ['-r10']
+			add_args = ['-r5']
 		t = MMGenExpect(name,'mmgen-walletconv', args + add_args)
 		if ni: return
 		t.license()
@@ -1038,8 +1129,8 @@ class MMGenTestSuite(object):
 	def walletchk_newpass (self,name,wf,pf):
 		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 +
 				['-d',cfg['tmpdir'],wf,cfg['addr_idx_list']])
 		if ni: return
@@ -1074,14 +1165,14 @@ class MMGenTestSuite(object):
 
 	def txcreate_common(self,name,sources=['1'],non_mmgen_input=''):
 		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()
-		from mmgen.addr import AddrInfo,AddrInfoList
-		tx_data,ail = {},AddrInfoList()
+		from mmgen.addr import AddrList,AddrData
+		tx_data,ad = {},AddrData()
 		for s in sources:
 			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'])
 			if len(aix) != addrs_per_wallet:
 				errmsg(red('Address index list length != %s: %s' %
@@ -1089,13 +1180,15 @@ class MMGenTestSuite(object):
 				sys.exit()
 			tx_data[s] = {
 				'addrfile': afile,
-				'chk': ai.checksum,
+				'chk': ai.chksum,
 				'sid': ai.seed_id,
 				'addr_idxs': aix[-2:],
 			}
 
 		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
 		from mmgen.bitcoin import privnum2addr
@@ -1144,8 +1237,8 @@ class MMGenTestSuite(object):
 			t.expect('Continue anyway? (y/N): ','y')
 		t.expect(r"'q' = quit sorting, .*?: ",'M', 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: ',
 				' '.join([str(i) for i in outputs_list])+'\n')
 		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')
 
 	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)
 		ok()
 
@@ -1282,12 +1375,12 @@ class MMGenTestSuite(object):
 		self.addrgen_incog(name,[],'',in_fmt='hi',desc='hidden incognito data',
 			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']]
 		if ni:
 			m = "\nAnswer 'n' at the interactive prompt"
 			msg(grnbg(m))
-			args = ['-q','-P',pf] + args
+			args = ['-q'] + ([],['-P',pf])[bool(pf)] + args
 		t = MMGenExpect(name,'mmgen-keygen', args)
 		if ni: return
 		t.license()
@@ -1308,8 +1401,8 @@ class MMGenTestSuite(object):
 	def txsign_keyaddr(self,name,keyaddr_file,txfile):
 		t = MMGenExpect(name,'mmgen-txsign', ['-d',cfg['tmpdir'],'-M',keyaddr_file,txfile])
 		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.tx_view()
 		self.txsign_end(t)
@@ -1359,7 +1452,7 @@ class MMGenTestSuite(object):
 		bwf = os.path.join(cfg['tmpdir'],cfg['bw_filename'])
 		make_brainwallet_file(bwf)
 		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.license()
 		t.passphrase_new('new MMGen wallet',cfg['wpasswd'])
@@ -1371,14 +1464,19 @@ class MMGenTestSuite(object):
 	def addrgen4(self,name,wf):
 		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)
-		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.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()
 
 		for cnum,desc in ('1','incognito data'),('3','MMGen wallet'):
@@ -1530,12 +1628,16 @@ class MMGenTestSuite(object):
 			pf = None
 		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))
-		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'])
 
-	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):
 		wf = os.path.join(ref_dir,bw_file)
@@ -1593,7 +1695,7 @@ class MMGenTestSuite(object):
 			msg(grnbg('%s %s' % (m,n)))
 			return
 		if ftype == 'keyaddr':
-			w = 'key-address file'
+			w = 'key-address data'
 			t.hash_preset(w,ref_kafile_hash_preset)
 			t.passphrase(w,ref_kafile_pass)
 			t.expect('Check key-to-address validity? (y/N): ','y')
@@ -1631,7 +1733,7 @@ class MMGenTestSuite(object):
 
 	# wallet conversion tests
 	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 []
 		d = '(convert)'
 		if ni:
@@ -1685,7 +1787,7 @@ class MMGenTestSuite(object):
 				rd = os.urandom(ref_wallet_incog_offset+128)
 				write_to_tmpfile(cfg,hincog_fn,rd)
 		else:
-			aa = ['-r10']
+			aa = ['-r5']
 		infile = os.path.join(ref_dir,cfg['seed_id']+'.mmwords')
 		t = MMGenExpect(name,'mmgen-walletconv',aa+opts+[infile],extra_desc='(convert)')
 
@@ -1696,7 +1798,7 @@ class MMGenTestSuite(object):
 			pf = get_tmpfile_fn(cfg,pfn)
 			if desc != 'hidden incognito data':
 				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
 				pre_ext = '[%s%s].' % (cfg['seed_len'],hps)
 				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'):
 			locals()[k+i] = locals()[k]
 
+	for k in ('walletgen','addrgen','keyaddrgen'): locals()[k+'14'] = locals()[k]
+
 
 # main()
 if opt.pause:
@@ -1809,10 +1913,13 @@ try:
 		clean()
 		for cmd in cmd_data:
 			if cmd[:5] == 'info_':
-				msg(green('\nTesting ' + cmd_data[cmd][0]))
+				msg(green('%sTesting %s' % (('\n','')[bool(opt.resume)],cmd_data[cmd][0])))
 				continue
 			ts.do_cmd(cmd)
 			if cmd is not cmd_data.keys()[-1]: do_between()
+except KeyboardInterrupt:
+	die(1,'\nExiting at user request')
+	raise
 except:
 	sys.stderr = stderr_save
 	raise

+ 1 - 1
test/tooltest.py

@@ -105,7 +105,7 @@ cfg = {
 	'refdir':        'test/ref',
 	'txfile':        'FFB367[1.234].rawtx',
 	'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 = {