Browse Source

Support for Segwit (P2SH-P2WPKH) addresses:

- Generate Segwit addresses by invoking 'mmgen-addrgen' with the
  '--type segwit' option
- Import Segwit addresses into the tracking wallet as usual
- Segwit and legacy MMGen addresses are distinguished by 'S' and 'L'
  identifiers in the tracking wallet and command line

Transaction example:

  mmgen-txcreate F00BAA12:L:21,1.23 F00BAA12:S:1

(spend 1.23 BTC to legacy address 21 of your default wallet (with Seed ID
F00BAA12) and send the change to Segwit address 1)

Segwit and legacy addresses for a given seed are generated from different
sub-seeds so are cryptographically unrelated to each other.

Since MMGen's legacy P2PKH addresses are uncompressed, use of the new Segwit
addresses significantly reduces transaction size.

Until Segwit activation on mainnet, users can try out the new functionality on
testnet or in regtest mode.
philemon 7 years ago
parent
commit
3b0257358b

+ 3 - 0
data_files/mmgen.cfg

@@ -17,6 +17,9 @@
 # Uncomment to force 256-color output when 'color' is true:
 # force_256_color true
 
+# Uncomment to use regtest mode (this also sets testnet to true):
+# regtest true
+
 # Uncomment to use testnet instead of mainnet:
 # testnet true
 

+ 191 - 144
mmgen/addr.py

@@ -23,31 +23,17 @@ addr.py:  Address generation/display routines for the MMGen suite
 from hashlib import sha256,sha512
 from binascii import hexlify,unhexlify
 from mmgen.common import *
-from mmgen.bitcoin import privnum2addr,hex2wif,wif2hex
+from mmgen.bitcoin import hex2wif,wif2hex,wif_is_compressed
 from mmgen.obj import *
 from mmgen.tx import *
 from mmgen.tw import *
 
 pnm = g.proj_name
 
-def _test_for_keyconv(silent=False):
-	no_keyconv_errmsg = """
-Executable '{kconv}' unavailable.  Please install '{kconv}' from the {vgen}
-package on your system or specify the secp256k1 library.
-""".format(kconv=g.keyconv_exec, vgen='vanitygen')
-	from subprocess import check_output,STDOUT
-	try:
-		check_output([g.keyconv_exec, '-G'],stderr=STDOUT)
-	except:
-		if not silent: msg(no_keyconv_errmsg.strip())
-		return False
-	return True
-
 def _test_for_secp256k1(silent=False):
 	no_secp256k1_errmsg = """
-secp256k1 library unavailable.  Will use '{kconv}', or failing that, the (slow)
-native Python ECDSA library for address generation.
-""".format(kconv=g.keyconv_exec)
+secp256k1 library unavailable.  Using (slow) native Python ECDSA library for address generation.
+"""
 	try:
 		from mmgen.secp256k1 import priv2pub
 		assert priv2pub(os.urandom(32),1)
@@ -56,58 +42,68 @@ native Python ECDSA library for address generation.
 		return False
 	return True
 
-def _wif2addr_python(wif):
-	privhex = wif2hex(wif)
-	if not privhex: return False
-	return privnum2addr(int(privhex,16),wif[0] != ('5','9')[g.testnet])
-
-def _wif2addr_keyconv(wif):
-	if wif[0] == ('5','9')[g.testnet]:
-		from subprocess import check_output
-		return check_output(['keyconv', wif]).split()[1]
+def _pubhex2addr(pubhex,mmtype):
+	if mmtype == 'L':
+		from mmgen.bitcoin import hexaddr2addr,hash160
+		return hexaddr2addr(hash160(pubhex))
+	elif mmtype == 'S':
+		from mmgen.bitcoin import pubhex2segwitaddr
+		return pubhex2segwitaddr(pubhex)
 	else:
-		return _wif2addr_python(wif)
+		die(2,"'{}': mmtype unrecognized".format(mmtype))
 
-def _wif2addr_secp256k1(wif):
-	return _privhex2addr_secp256k1(wif2hex(wif),wif[0] != ('5','9')[g.testnet])
+def _privhex2addr_python(privhex,compressed,mmtype):
+	assert compressed or mmtype != 'S'
+	from mmgen.bitcoin import privnum2pubhex
+	pubhex = privnum2pubhex(int(privhex,16),compressed=compressed)
+	return _pubhex2addr(pubhex,mmtype=mmtype)
 
-def _privhex2addr_python(privhex,compressed=False):
-	return privnum2addr(int(privhex,16),compressed)
+def _privhex2addr_secp256k1(privhex,compressed,mmtype):
+	assert compressed or mmtype != 'S'
+	from mmgen.secp256k1 import priv2pub
+	pubhex = hexlify(priv2pub(unhexlify(privhex),int(compressed)))
+	return _pubhex2addr(pubhex,mmtype=mmtype)
 
-def _privhex2addr_keyconv(privhex,compressed=False):
-	if compressed:
-		return privnum2addr(int(privhex,16),compressed)
-	else:
-		from subprocess import check_output
-		return check_output(['keyconv', hex2wif(privhex,compressed=False)]).split()[1]
+def _wif2addr_python(wif,mmtype):
+	privhex = wif2hex(wif)
+	if not privhex: return False
+	return _privhex2addr_python(privhex,wif_is_compressed(wif),mmtype=mmtype)
 
-def _privhex2addr_secp256k1(privhex,compressed=False):
-	from mmgen.secp256k1 import priv2pub
-	from mmgen.bitcoin import hexaddr2addr,pubhex2hexaddr
-	pubkey = priv2pub(unhexlify(privhex),int(compressed))
-	return hexaddr2addr(pubhex2hexaddr(hexlify(pubkey)))
-
-def _keygen_selector(generator=None):
-	if generator:
-		if generator == 3 and _test_for_secp256k1():             return 2
-		elif generator in (2,3) and _test_for_keyconv():         return 1
-	else:
-		if opt.key_generator == 3 and _test_for_secp256k1():     return 2
-		elif opt.key_generator in (2,3) and _test_for_keyconv(): return 1
+def _wif2addr_secp256k1(wif,mmtype):
+	privhex = wif2hex(wif)
+	if not privhex: return False
+	return _privhex2addr_secp256k1(privhex,wif_is_compressed(wif),mmtype=mmtype)
+
+def keygen_wif2pubhex(wif,selector):
+	privhex = wif2hex(wif)
+	if not privhex: return False
+	if selector == 1:
+		from mmgen.secp256k1 import priv2pub
+		return hexlify(priv2pub(unhexlify(privhex),int(wif_is_compressed(wif))))
+	elif selector == 0:
+		from mmgen.bitcoin import privnum2pubhex
+		return privnum2pubhex(int(privhex,16),compressed=wif_is_compressed(wif))
+
+def keygen_selector(generator=None):
+	if _test_for_secp256k1() and generator != 1:
+		if opt.key_generator != 1:
+			return 1
 	msg('Using (slow) native Python ECDSA library for address generation')
 	return 0
 
 def get_wif2addr_f(generator=None):
-	gen = _keygen_selector(generator=generator)
-	return (_wif2addr_python,_wif2addr_keyconv,_wif2addr_secp256k1)[gen]
+	gen = keygen_selector(generator=generator)
+	return (_wif2addr_python,_wif2addr_secp256k1)[gen]
 
 def get_privhex2addr_f(generator=None):
-	gen = _keygen_selector(generator=generator)
-	return (_privhex2addr_python,_privhex2addr_keyconv,_privhex2addr_secp256k1)[gen]
+	gen = keygen_selector(generator=generator)
+	return (_privhex2addr_python,_privhex2addr_secp256k1)[gen]
+
 
 class AddrListEntry(MMGenListItem):
 	attrs = 'idx','addr','label','wif','sec'
 	idx   = MMGenListItemAttr('idx','AddrIdx')
+	wif   = MMGenListItemAttr('wif','WifKey')
 
 class AddrListChksum(str,Hilite):
 	color = 'pink'
@@ -119,7 +115,7 @@ class AddrListChksum(str,Hilite):
 #		print '[{}]'.format(' '.join(lines))
 		return str.__new__(cls,make_chksum_N(' '.join(lines), nchars=16, sep=True))
 
-class AddrListID(unicode,Hilite):
+class AddrListIDStr(unicode,Hilite):
 	color = 'green'
 	trunc_ok = False
 	def __new__(cls,addrlist,fmt_str=None):
@@ -138,7 +134,15 @@ class AddrListID(unicode,Hilite):
 					ret += ',', i
 				prev = i
 			s = ''.join([unicode(i) for i in ret])
-		return unicode.__new__(cls,fmt_str.format(s) if fmt_str else '{}[{}]'.format(addrlist.seed_id,s))
+
+		if fmt_str:
+			ret = fmt_str.format(s)
+		elif addrlist.al_id.mmtype == 'L':
+			ret = '{}[{}]'.format(addrlist.al_id.sid,s)
+		else:
+			ret = '{}-{}[{}]'.format(addrlist.al_id.sid,addrlist.al_id.mmtype,s)
+
+		return unicode.__new__(cls,ret)
 
 class AddrList(MMGenObject): # Address info for a single seed ID
 	msgs = {
@@ -150,7 +154,7 @@ class AddrList(MMGenObject): # Address info for a single seed ID
 # 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=MMGenAddrLabel.max_len,pnm=pnm),
+""".strip().format(n=TwComment.max_len,pnm=pnm),
 	'record_chksum': """
 Record this checksum: it will be used to verify the address file in the future
 """.strip(),
@@ -169,40 +173,46 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file
 	gen_keys = False
 	has_keys = False
 	ext      = 'addrs'
+	dfl_mmtype = MMGenAddrType('L')
+	cook_hash_rounds = 10  # not too many rounds, so hand decoding can still be feasible
 
-	def __init__(self,addrfile='',sid='',adata=[],seed='',addr_idxs='',src='',
-					addrlist='',keylist='',do_chksum=True,chksum_only=False):
+	def __init__(self,addrfile='',al_id='',adata=[],seed='',addr_idxs='',src='',
+					addrlist='',keylist='',mmtype=None,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
+		mmtype = mmtype or self.dfl_mmtype
+		assert mmtype in MMGenAddrType.mmtypes
+
+		if seed and addr_idxs:   # data from seed + idxs
+			self.al_id,src = AddrListID(seed.sid,mmtype),'gen'
+			adata = self.generate(seed,addr_idxs,compressed=(mmtype=='S'))
+		elif addrfile:           # data from MMGen address file
+			adata = self.parse_file(addrfile) # sets self.al_id
+		elif al_id and adata:    # data from tracking wallet
+			self.al_id = al_id
 			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 set(addrlist)]
+			self.al_id = None
+			adata = AddrListList([AddrListEntry(addr=a) for a in set(addrlist)])
 		elif keylist:            # data from flat key list
-			sid,do_chksum = None,False
-			adata = [AddrListEntry(wif=k) for k in set(keylist)]
+			self.al_id = None
+			adata = AddrListList([AddrListEntry(wif=k) for k in set(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')
+		elif al_id or adata:
+			die(3,'Must specify both al_id and adata')
 		else:
 			die(3,'Incorrect arguments for %s' % type(self).__name__)
 
-		# sid,adata now set
-		self.seed_id = sid
+		# al_id,adata now set
 		self.data = adata
 		self.num_addrs = len(adata)
 		self.fmt_data = ''
-		self.id_str = None
 		self.chksum = None
-		self.id_str = AddrListID(self)
+
+		if self.al_id == None: return
+
+		self.id_str = AddrListIDStr(self)
 
 		if type(self) == KeyList: return
 
@@ -221,17 +231,17 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file
 				if k not in self.msgs:
 					self.msgs[k] = AddrList.msgs[k]
 
-	def generate(self,seed,addrnums):
+	def generate(self,seed,addrnums,compressed):
 		assert type(addrnums) is AddrIdxList
-		self.seed_id = SeedID(seed=seed)
-		seed = seed.get_data()
+		assert compressed in (True,False,None)
 
+		seed = seed.get_data()
 		seed = self.cook_seed(seed)
 
 		if self.gen_addrs:
-			privhex2addr_f = get_privhex2addr_f()
+			privhex2addr_f = get_privhex2addr_f() # choose internal ECDSA or secp256k1 generator
 
-		t_addrs,num,pos,out = len(addrnums),0,0,[]
+		t_addrs,num,pos,out = len(addrnums),0,0,AddrListList()
 
 		while pos != t_addrs:
 			seed = sha512(seed).digest()
@@ -250,10 +260,10 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file
 			sec = sha256(sha256(seed).digest()).hexdigest()
 
 			if self.gen_addrs:
-				e.addr = privhex2addr_f(sec,compressed=False)
+				e.addr = privhex2addr_f(sec,compressed=compressed,mmtype=self.al_id.mmtype)
 
 			if self.gen_keys:
-				e.wif = hex2wif(sec,compressed=False)
+				e.wif = hex2wif(sec,compressed=compressed)
 				if opt.b16: e.sec = sec
 
 			if self.gen_passwds:
@@ -261,14 +271,31 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file
 				dmsg('Key {:>03}: {}'.format(pos,sec))
 
 			out.append(e)
+			if g.debug: print 'generate():\n', e.pformat()
 
 		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))
+				self.al_id.hl(),t_addrs,self.gen_desc,suf(t_addrs,self.gen_desc_pl),' '*15))
 		return out
 
-	def chk_addr_or_pw(self,addr): return is_btc_addr(addr)
+	def is_mainnet(self):
+		return self.data[0].addr.is_mainnet()
+
+	def is_for_current_chain(self):
+		return self.data[0].addr.is_for_current_chain()
 
-	def cook_seed(self,seed): return seed
+	def chk_addr_or_pw(self,addr):
+		return {'L':'p2pkh','S':'p2sh'}[self.al_id.mmtype] == is_btc_addr(addr).addr_fmt
+
+	def cook_seed(self,seed):
+		if self.al_id.mmtype == 'L':
+			return seed
+		else:
+			from mmgen.crypto import sha256_rounds
+			import hmac
+			key = self.al_id.mmtype.name
+			cseed = hmac.new(seed,key,sha256).digest()
+			dmsg('Seed:  {}\nKey: {}\nCseed: {}\nCseed len: {}'.format(hexlify(seed),key,hexlify(cseed),len(cseed)))
+			return sha256_rounds(cseed,self.cook_hash_rounds)
 
 	def encrypt(self,desc='new key list'):
 		from mmgen.crypto import mmgen_encrypt
@@ -284,7 +311,7 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file
 		return [e.idx for e in self.data]
 
 	def addrs(self):
-		return ['%s:%s'%(self.seed_id,e.idx) for e in self.data]
+		return ['%s:%s'%(self.al_id.sid,e.idx) for e in self.data]
 
 	def addrpairs(self):
 		return [(e.idx,e.addr) for e in self.data]
@@ -313,21 +340,18 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file
 				e.label = comment
 
 	def make_reverse_dict(self,btcaddrs):
-		d,b = {},btcaddrs
+		d,b = MMGenDict(),btcaddrs
 		for e in self.data:
 			try:
-				d[b[b.index(e.addr)]] = ('%s:%s'%(self.seed_id,e.idx),e.label)
+				d[b[b.index(e.addr)]] = MMGenID('{}:{}'.format(self.al_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]
+		return [AddrListFlatEntry(mmid='{}:{}'.format(self.al_id,e.idx),addr=e.addr,wif=e.wif)
+			for e in self.data]
 
 	def remove_dups(self,cmplist,key='wif'):
 		pop_list = []
@@ -338,7 +362,7 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file
 					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')))
+			vmsg(self.msgs['removed_dups'] % (len(pop_list),suf(removed,'s')))
 
 	def add_wifs(self,al_key):
 		if not al_key: return
@@ -355,13 +379,15 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file
 
 	def get_addrs(self): return self.get('addr')
 	def get_wifs(self):  return self.get('wif')
+	def get_addr_wif_pairs(self):
+		return [(d.addr,d.wif) for d in self.data if hasattr(d,'wif')]
 
-	def generate_addrs(self):
+	def generate_addrs_from_keylist(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)
+			e.addr = wif2addr_f(e.wif,mmtype='L') # 'L' == p2pkh
 		qmsg('\rGenerated addresses from keylist: %s/%s ' % (n,len(d)))
 
 	def format(self,enable_comments=False):
@@ -385,9 +411,11 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file
 
 		if type(self) == PasswordList:
 			out.append(u'{} {} {}:{} {{'.format(
-				self.seed_id,self.pw_id_str,self.pw_fmt,self.pw_len))
+				self.al_id.sid,self.pw_id_str,self.pw_fmt,self.pw_len))
+		elif self.al_id.mmtype == 'L':
+			out.append('{} {{'.format(self.al_id.sid))
 		else:
-			out.append('{} {{'.format(self.seed_id))
+			out.append('{} {} {{'.format(self.al_id.sid,MMGenAddrType.mmtypes[self.al_id.mmtype].upper()))
 
 		fs = '  {:<%s}  {:<34}{}' % len(str(self.data[-1].idx))
 		for e in self.data:
@@ -410,7 +438,7 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file
 		if self.has_keys and len(lines) % 2:
 			return 'Key-address file has odd number of lines'
 
-		ret = []
+		ret = AddrListList()
 
 		while lines:
 			l = lines.pop(0)
@@ -444,7 +472,7 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file
 			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):
+				if e.addr != wif2addr_f(e.wif,mmtype=self.al_id.mmtype):
 					return "Key doesn't match address!\n  %s\n  %s" % (e.wif,e.addr)
 			msg(' - done')
 
@@ -463,29 +491,44 @@ Removed %s duplicate wif key%s from keylist (also in {pnm} key-address file
 			return do_error("Too few lines in address file (%s)" % len(lines))
 
 		ls = lines[0].split()
-		ls_len = (2,4)[type(self)==PasswordList]
-		if len(ls) != ls_len:
+		if not 1 < len(ls) < 5:
 			return do_error("Invalid first line for {} file: '{}'".format(self.gen_desc,lines[0]))
-		if ls[-1] != '{':
+		if ls.pop() != '{':
 			return do_error("'%s': invalid first line" % ls)
 		if lines[-1] != '}':
 			return do_error("'%s': invalid last line" % lines[-1])
-		if not is_mmgen_seed_id(ls[0]):
+
+		sid = ls.pop(0)
+		if not is_mmgen_seed_id(sid):
 			return do_error("'%s': invalid Seed ID" % ls[0])
 
-		if type(self) == PasswordList:
-			self.pw_id_str = MMGenPWIDString(ls[1])
-			ss = ls[2].split(':')
+		if type(self) == PasswordList and len(ls) == 2:
+			ss = ls.pop().split(':')
 			if len(ss) != 2:
 				return do_error("'%s': invalid password length specifier (must contain colon)" % ls[2])
 			self.set_pw_fmt(ss[0])
 			self.set_pw_len(ss[1])
+			self.pw_id_str = MMGenPWIDString(ls.pop())
+			mmtype = MMGenPasswordType('P')
+		elif len(ls) == 1:
+			mmtype = ls.pop().lower()
+			try:
+				mmtype = MMGenAddrType(mmtype)
+			except:
+				return do_error(u"'{}': invalid address type in address file. Must be one of: {}".format(
+					mmtype,' '.join(MMGenAddrType.mmtypes.values()).upper()))
+		elif len(ls) == 0:
+			mmtype = MMGenAddrType('L')
+		else:
+			return do_error(u"Invalid first line for {} file: '{}'".format(self.gen_desc,lines[0]))
+
+		self.al_id = AddrListID(SeedID(sid=sid),mmtype)
 
-		ret = self.parse_file_body(lines[1:-1])
-		if type(ret) != list:
-			return do_error(ret)
+		data = self.parse_file_body(lines[1:-1])
+		if not issubclass(type(data),list):
+			return do_error(data)
 
-		return ls[0],ret
+		return data
 
 class KeyAddrList(AddrList):
 	data_desc = 'key-address'
@@ -525,7 +568,7 @@ class PasswordList(AddrList):
 # A text label of {n} characters or less may be added to the right of each
 # password.  The label may contain any printable ASCII symbol.
 #
-""".strip().format(n=MMGenAddrLabel.max_len,pnm=pnm),
+""".strip().format(n=TwComment.max_len,pnm=pnm),
 	'record_chksum': """
 Record this checksum: it will be used to verify the password file in the future
 """.strip()
@@ -546,7 +589,6 @@ Record this checksum: it will be used to verify the password file in the future
 		'b58': { 'min_len': 8 , 'max_len': 36 ,'dfl_len': 20, 'desc': 'base-58 password' },
 		'b32': { 'min_len': 10 ,'max_len': 42 ,'dfl_len': 24, 'desc': 'base-32 password' }
 		}
-	cook_hash_rounds = 10  # not too many rounds, so hand decoding can still be feasible
 
 	def __init__(self,infile=None,seed=None,pw_idxs=None,pw_id_str=None,pw_len=None,pw_fmt=None,
 				chksum_only=False,chk_params_only=False):
@@ -554,7 +596,7 @@ Record this checksum: it will be used to verify the password file in the future
 		self.update_msgs()
 
 		if infile:
-			(self.seed_id,self.data) = self.parse_file(infile) # sets self.pw_id_str,self.pw_fmt,self.pw_len
+			self.data = self.parse_file(infile) # sets self.pw_id_str,self.pw_fmt,self.pw_len
 		else:
 			for k in seed,pw_idxs: assert chk_params_only or k
 			for k in pw_id_str,pw_fmt: assert k
@@ -562,8 +604,8 @@ Record this checksum: it will be used to verify the password file in the future
 			self.set_pw_fmt(pw_fmt)
 			self.set_pw_len(pw_len)
 			if chk_params_only: return
-			self.seed_id = seed.sid
-			self.data = self.generate(seed,pw_idxs)
+			self.al_id = AddrListID(seed.sid,MMGenPasswordType('P'))
+			self.data = self.generate(seed,pw_idxs,compressed=None)
 
 		self.num_addrs = len(self.data)
 		self.fmt_data = ''
@@ -572,8 +614,8 @@ Record this checksum: it will be used to verify the password file in the future
 		if chksum_only:
 			Msg(self.chksum)
 		else:
-			self.id_str = AddrListID(self,fmt_str=u'{}-{}-{}-{}[{{}}]'.format(
-				self.seed_id,self.pw_id_str,self.pw_fmt,self.pw_len))
+			fs = u'{}-{}-{}-{}[{{}}]'.format(self.al_id.sid,self.pw_id_str,self.pw_fmt,self.pw_len)
+			self.id_str = AddrListIDStr(self,fs)
 			qmsg(u'Checksum for {} data {}: {}'.format(self.data_desc,self.id_str.hl(),self.chksum.hl()))
 			qmsg(self.msgs[('record_chksum','check_chksum')[bool(infile)]])
 
@@ -626,6 +668,7 @@ Record this checksum: it will be used to verify the password file in the future
 		from mmgen.crypto import sha256_rounds
 		# Changing either pw_fmt, pw_len or id_str will cause a different, unrelated set of
 		# passwords to be generated: this is what we want
+		# NB: In original implementation, pw_id_str was 'baseN', not 'bN'
 		fid_str = '{}:{}:{}'.format(self.pw_fmt,self.pw_len,self.pw_id_str.encode('utf8'))
 		dmsg(u'Full ID string: {}'.format(fid_str.decode('utf8')))
 		# Original implementation was 'cseed = seed + fid_str'; hmac was not used
@@ -646,54 +689,58 @@ re-import your addresses.
 	}
 
 	def __init__(self,source=None):
-		self.sids = {}
+		self.al_ids = {}
 		if source == 'tw': self.add_tw_data()
 
 	def seed_ids(self):
-		return self.sids.keys()
+		return self.al_ids.keys()
 
-	def addrlist(self,sid):
-		# TODO: Validate sid
-		if sid in self.sids:
-			return self.sids[sid]
+	def addrlist(self,al_id):
+		# TODO: Validate al_id
+		if al_id in self.al_ids:
+			return self.al_ids[al_id]
 
 	def mmaddr2btcaddr(self,mmaddr):
+		al_id,idx = MMGenID(mmaddr).rsplit(':',1)
 		btcaddr = ''
-		sid,idx = mmaddr.split(':')
-		if sid in self.seed_ids():
-			btcaddr = self.addrlist(sid).btcaddr(int(idx))
-		return btcaddr
+		if al_id in self.al_ids:
+			btcaddr = self.addrlist(al_id).btcaddr(int(idx))
+		return btcaddr or None
+
+	def btcaddr2mmaddr(self,btcaddr):
+		d = self.make_reverse_dict([btcaddr])
+		return (d.values()[0][0]) if d else None
 
 	def add_tw_data(self):
-		vmsg_r('Getting address data from tracking wallet...')
+		vmsg('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):
-			maddr,label = parse_tw_acct_label(acct)
-			if maddr:
+			l = TwLabel(acct,on_fail='silent')
+			if l and l.mmid.type == 'mmgen':
+				obj = l.mmid.obj
 				i += 1
 				if len(addrlist) != 1:
 					die(2,self.msgs['too_many_acct_addresses'] % acct)
-				seed_id,idx = maddr.split(':')
-				if seed_id not in data:
-					data[seed_id] = []
-				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(AddrList(sid=sid,adata=data[sid]))
+				al_id = AddrListID(SeedID(sid=obj.sid),MMGenAddrType(obj.mmtype))
+				if al_id not in data:
+					data[al_id] = []
+				data[al_id].append(AddrListEntry(idx=obj.idx,addr=addrlist[0],label=l.comment))
+		vmsg('{n} {pnm} addresses found, {m} accounts total'.format(n=i,pnm=pnm,m=len(accts)))
+		for al_id in data:
+			self.add(AddrList(al_id=al_id,adata=AddrListList(sorted(data[al_id],key=lambda a: a.idx))))
 
 	def add(self,addrlist):
 		if type(addrlist) == AddrList:
-			self.sids[addrlist.seed_id] = addrlist
+			self.al_ids[addrlist.al_id] = addrlist
 			return True
 		else:
 			raise TypeError, 'Error: object %s is not of type AddrList' % repr(addrlist)
 
 	def make_reverse_dict(self,btcaddrs):
-		d = {}
-		for sid in self.sids:
-			d.update(self.sids[sid].make_reverse_dict(btcaddrs))
+		d = MMGenDict()
+		for al_id in self.al_ids:
+			d.update(self.al_ids[al_id].make_reverse_dict(btcaddrs))
 		return d

+ 42 - 22
mmgen/bitcoin.py

@@ -53,33 +53,40 @@ b58a='123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
 
 from mmgen.globalvars import g
 
-def hash256(hexnum): # take hex, return hex - OP_HASH256
-	return sha256(sha256(unhexlify(hexnum)).digest()).hexdigest()
-
 def hash160(hexnum): # take hex, return hex - OP_HASH160
 	return hashlib_new('ripemd160',sha256(unhexlify(hexnum)).digest()).hexdigest()
 
-pubhex2hexaddr = hash160
+def hash256(hexnum): # take hex, return hex - OP_HASH256
+	return sha256(sha256(unhexlify(hexnum)).digest()).hexdigest()
+
+# devdoc/ref_transactions.md:
+btc_ver_nums = {
+	'p2pkh': (('00','1'),('6f','mn')),
+	'p2sh':  (('05','3'),('c4','2'))
+}
+addr_pfxs = { 'mainnet': '13', 'testnet': 'mn2', 'regtest': 'mn2' }
+vnum_all = tuple([k for k,v in btc_ver_nums['p2pkh'] + btc_ver_nums['p2sh']])
 
 def hexaddr2addr(hexaddr,p2sh=False):
-	# devdoc/ref_transactions.md:
-	s = ('00','6f','05','c4')[g.testnet+(2*p2sh)] + hexaddr.strip()
+	s = vnum_all[g.testnet+(2*p2sh)] + hexaddr.strip()
 	lzeroes = (len(s) - len(s.lstrip('0'))) / 2
 	return ('1' * lzeroes) + _numtob58(int(s+hash256(s)[:8],16))
 
-def verify_addr(addr,verbose=False,return_hex=False):
+def verify_addr(addr,verbose=False,return_hex=False,return_type=False):
 	addr = addr.strip()
-	for vers_num,ldigit in ('00','1'),('05','3'),('6f','mn'),('c4','2'):
-		if addr[0] not in ldigit: continue
-		num = _b58tonum(addr)
-		if num == False: break
-		addr_hex = '{:050x}'.format(num)
-		if addr_hex[:2] != vers_num: continue
-		if hash256(addr_hex[:42])[:8] == addr_hex[42:]:
-			return addr_hex[2:42] if return_hex else True
-		else:
-			if verbose: Msg("Invalid checksum in address '%s'" % addr)
-			break
+
+	for k in ('p2pkh','p2sh'):
+		for ver_num,ldigit in btc_ver_nums[k]:
+			if addr[0] not in ldigit: continue
+			num = _b58tonum(addr)
+			if num == False: break
+			addr_hex = '{:050x}'.format(num)
+			if addr_hex[:2] != ver_num: continue
+			if hash256(addr_hex[:42])[:8] == addr_hex[42:]:
+				return addr_hex[2:42] if return_hex else k if return_type else True
+			else:
+				if verbose: Msg("Invalid checksum in address '%s'" % addr)
+				break
 
 	if verbose: Msg("Invalid address '%s'" % addr)
 	return False
@@ -97,7 +104,7 @@ def _b58tonum(b58num):
 	b58num = b58num.strip()
 	for i in b58num:
 		if not i in b58a: return False
-	return sum([b58a.index(n) * (58**i) for i,n in enumerate(list(b58num[::-1]))])
+	return sum(b58a.index(n) * (58**i) for i,n in enumerate(list(b58num[::-1])))
 
 # The following are MMGen internal (non-Bitcoin) b58 functions
 
@@ -146,9 +153,11 @@ def b58decode_pad(s):
 
 # Compressed address support:
 
+def wif_is_compressed(wif): return wif[0] != ('5','9')[g.testnet]
+
 def wif2hex(wif):
 	wif = wif.strip()
-	compressed = wif[0] != ('5','9')[g.testnet]
+	compressed = wif_is_compressed(wif)
 	num = _b58tonum(wif)
 	if num == False: return False
 	key = '{:x}'.format(num)
@@ -178,5 +187,16 @@ def privnum2pubhex(numpriv,compressed=False):
 	else:
 		return '04'+pubkey
 
-def privnum2addr(numpriv,compressed=False):
-	return hexaddr2addr(pubhex2hexaddr(privnum2pubhex(numpriv,compressed)))
+def privnum2addr(numpriv,compressed=False,segwit=False): # used only by tool and testsuite
+	pubhex = privnum2pubhex(numpriv,compressed)
+	return pubhex2segwitaddr(pubhex) if segwit else hexaddr2addr(hash160(pubhex))
+
+# Segwit:
+def pubhex2redeem_script(pubhex):
+	# https://bitcoincore.org/en/segwit_wallet_dev/
+	# The P2SH redeemScript is always 22 bytes. It starts with a OP_0, followed
+	# by a canonical push of the keyhash (i.e. 0x0014{20-byte keyhash})
+	return '0014' + hash160(pubhex)
+
+def pubhex2segwitaddr(pubhex):
+	return hexaddr2addr(hash160(pubhex2redeem_script(pubhex)),p2sh=True)

+ 14 - 0
mmgen/common.py

@@ -25,3 +25,17 @@ from mmgen.globalvars import g
 import mmgen.opts as opts
 from mmgen.opts import opt
 from mmgen.util import *
+
+pw_note = """
+For passphrases all combinations of whitespace are equal and leading and
+trailing space is ignored.  This permits reading passphrase or brainwallet
+data from a multi-line file with free spacing and indentation.
+""".strip()
+
+bw_note = """
+BRAINWALLET NOTE:
+
+To thwart dictionary attacks, it's recommended to use a strong hash preset
+with brainwallets.  For a brainwallet passphrase to generate the correct
+seed, the same seed length and hash preset parameters must always be used.
+""".strip()

+ 23 - 1
mmgen/filename.py

@@ -32,11 +32,15 @@ class Filename(MMGenObject):
 		self.basename = os.path.basename(fn)
 		self.ext      = get_extension(fn)
 		self.ftype    = None # the file's associated class
+		self.mtime    = None
+		self.ctime    = None
+		self.atime    = None
 
 		from mmgen.seed import SeedSource
+		from mmgen.tx import MMGenTX
 		if ftype:
 			if type(ftype) == type:
-				if issubclass(ftype,SeedSource):
+				if issubclass(ftype,SeedSource) or issubclass(ftype,MMGenTX):
 					self.ftype = ftype
 				# elif: # other MMGen file types
 				else:
@@ -44,6 +48,7 @@ class Filename(MMGenObject):
 			else:
 				die(3,"'%s': not a class" % ftype)
 		else:
+			# TODO: other file types
 			self.ftype = SeedSource.ext_to_type(self.ext)
 			if not self.ftype:
 				die(3,"'%s': not a recognized extension for SeedSource" % self.ext)
@@ -64,6 +69,23 @@ class Filename(MMGenObject):
 				os.close(fd)
 		else:
 			self.size = os.stat(fn).st_size
+			self.mtime = os.stat(fn).st_mtime
+			self.ctime = os.stat(fn).st_ctime
+			self.atime = os.stat(fn).st_atime
+
+class MMGenFileList(list,MMGenObject):
+
+	def __init__(self,fns,ftype):
+		flist = [Filename(fn,ftype) for fn in fns]
+		return list.__init__(self,flist)
+
+	def names(self):
+		return [f.name for f in self]
+
+	def sort_by_age(self,key='mtime',reverse=False):
+		if key not in ('atime','ctime','mtime'):
+			die(1,"'{}': illegal sort key".format(key))
+		self.sort(key=lambda a: getattr(a,key),reverse=reverse)
 
 def find_files_in_dir(ftype,fdir,no_dups=False):
 	if type(ftype) != type:

+ 13 - 8
mmgen/globalvars.py

@@ -31,13 +31,15 @@ from mmgen.obj import BTCAmt
 
 class g(object):
 
+	skip_segwit_active_check = bool(os.getenv('MMGEN_TEST_SUITE'))
+
 	def die(ev=0,s=''):
 		if s: sys.stderr.write(s+'\n')
 		sys.exit(ev)
 	# Variables - these might be altered at runtime:
 
-	version      = '0.9.1'
-	release_date = 'May 2017'
+	version      = '0.9.199'
+	release_date = 'July 2017'
 
 	proj_name = 'MMGen'
 	proj_url  = 'https://github.com/mmgen/mmgen'
@@ -70,6 +72,10 @@ class g(object):
 	color                = (False,True)[sys.stdout.isatty()]
 	force_256_color      = False
 	testnet              = False
+	regtest              = False
+	chain                = None # set by first call to bitcoin_connection()
+	chains               = 'mainnet','testnet','regtest'
+	bitcoind_version     = None # set by first call to bitcoin_connection()
 	rpc_host             = ''
 	rpc_port             = 0
 	rpc_user             = ''
@@ -97,7 +103,7 @@ class g(object):
 	# User opt sets global var:
 	common_opts = (
 		'color','no_license','rpc_host','rpc_port','testnet','rpc_user','rpc_password',
-		'bitcoin_data_dir','force_256_color'
+		'bitcoin_data_dir','force_256_color','regtest'
 	)
 	required_opts = (
 		'quiet','verbose','debug','outdir','echo_passphrase','passwd_file','stdout',
@@ -114,7 +120,7 @@ class g(object):
 	cfg_file_opts = (
 		'color','debug','hash_preset','http_timeout','no_license','rpc_host','rpc_port',
 		'quiet','tx_fee_adj','usr_randchars','testnet','rpc_user','rpc_password',
-		'bitcoin_data_dir','force_256_color','max_tx_fee'
+		'bitcoin_data_dir','force_256_color','max_tx_fee','regtest'
 	)
 	env_opts = (
 		'MMGEN_BOGUS_WALLET_DATA',
@@ -127,6 +133,7 @@ class g(object):
 		'MMGEN_NO_LICENSE',
 		'MMGEN_RPC_HOST',
 		'MMGEN_TESTNET'
+		'MMGEN_REGTEST'
 	)
 
 	min_screen_width = 80
@@ -136,8 +143,6 @@ class g(object):
 	global_sets_opt = ['minconf','seed_len','hash_preset','usr_randchars','debug',
 						'quiet','tx_confs','tx_fee_adj','key_generator']
 
-	keyconv_exec = 'keyconv'
-
 	mins_per_block   = 9
 	passwd_max_tries = 5
 
@@ -151,8 +156,8 @@ class g(object):
 	aesctr_iv_len  = 16
 	hincog_chk_len = 8
 
-	key_generators = 'python-ecdsa','keyconv','secp256k1' # 1,2,3
-	key_generator  = 3 # secp256k1 is default
+	key_generators = 'python-ecdsa','secp256k1' # '1','2'
+	key_generator  = 2 # secp256k1 is default
 
 	hash_presets = {
 #   Scrypt params:

+ 20 - 10
mmgen/main_addrgen.py

@@ -25,18 +25,19 @@ from mmgen.common import *
 from mmgen.crypto import *
 from mmgen.addr import *
 from mmgen.seed import SeedSource
+MAT = MMGenAddrType
 
 if sys.argv[0].split('-')[-1] == 'keygen':
 	gen_what = 'keys'
 	gen_desc = 'secret keys'
 	opt_filter = None
-	note2 = 'By default, both addresses and secret keys are generated.\n\n'
+	note_addrkey = 'By default, both addresses and secret keys are generated.\n\n'
 else:
 	gen_what = 'addresses'
 	gen_desc = 'addresses'
-	opt_filter = 'hbcdeiHOKlpzPqrSv-'
-	note2 = ''
-note1 = """
+	opt_filter = 'hbcdeiHOKlpzPqrStv-'
+	note_addrkey = ''
+note_secp256k1 = """
 If available, the secp256k1 library will be used for address generation.
 """.strip()
 
@@ -70,6 +71,8 @@ opts_data = {
 -r, --usr-randchars=n Get 'n' characters of additional randomness from user
                       (min={g.min_urandchars}, max={g.max_urandchars}, default={g.usr_randchars})
 -S, --stdout          Print {what} to stdout
+-t, --type=t          Choose address type. Options: see ADDRESS TYPES below
+                      (default: {dmat})
 -v, --verbose         Produce more verbose output
 -x, --b16             Print secret keys in hexadecimal too
 """.format(
@@ -77,7 +80,8 @@ opts_data = {
 	pnm=g.proj_name,
 	kgs=' '.join(['{}:{}'.format(n,k) for n,k in enumerate(g.key_generators,1)]),
 	kg=g.key_generator,
-	what=gen_what,g=g
+	what=gen_what,g=g,
+	dmat="'{}' or '{}'".format(MAT.dfl_mmtype,MAT.mmtypes[MAT.dfl_mmtype])
 ),
 	'notes': """
 
@@ -87,26 +91,32 @@ opts_data = {
 Address indexes are given as a comma-separated list and/or hyphen-separated
 range(s).
 
-{n2}{n1}
+{n_addrkey}{n_secp}
 
+ADDRESS TYPES:
+  {n_at}
 
                       NOTES FOR ALL GENERATOR COMMANDS
 
-{o.pw_note}
+{pwn}
 
-{o.bw_note}
+{bwn}
 
 FMT CODES:
   {f}
 """.format(
-		n1=note1,n2=note2,
+		n_secp=note_secp256k1,n_addrkey=note_addrkey,pwn=pw_note,bwn=bw_note,
 		f='\n  '.join(SeedSource.format_fmt_codes().splitlines()),
+		n_at='\n  '.join(["'{}', '{}'".format(k,v) for k,v in MAT.mmtypes.items()]),
 		o=opts
 	)
 }
 
 cmd_args = opts.init(opts_data,add_opts=['b16'],opt_filter=opt_filter)
 
+errmsg = "'{}': invalid parameter for --type option".format(opt.type)
+addr_type = MAT(opt.type or MAT.dfl_mmtype,errmsg=errmsg)
+
 if len(cmd_args) < 1: opts.usage()
 idxs = AddrIdxList(fmt_str=cmd_args.pop())
 
@@ -117,7 +127,7 @@ do_license_msg()
 ss = SeedSource(sf)
 
 i = (gen_what=='addresses') or bool(opt.no_addresses)*2
-al = (KeyAddrList,AddrList,KeyList)[i](seed=ss.seed,addr_idxs=idxs)
+al = (KeyAddrList,AddrList,KeyList)[i](seed=ss.seed,addr_idxs=idxs,mmtype=addr_type)
 al.format()
 
 if al.gen_addrs and opt.print_checksum:

+ 46 - 20
mmgen/main_addrimport.py

@@ -24,6 +24,7 @@ import time
 
 from mmgen.common import *
 from mmgen.addr import AddrList,KeyAddrList
+from mmgen.obj import TwLabel
 
 # In batch mode, bitcoind just rescans each address separately anyway, so make
 # --batch and --rescan incompatible.
@@ -35,6 +36,7 @@ opts_data = {
 	'options': """
 -h, --help         Print this help message
 --, --longhelp     Print help message for long options (common options)
+-a, --address=a    Import the single Bitcoin address 'a'
 -b, --batch        Import all addresses in one RPC call.
 -l, --addrlist     Address source is a flat list of (non-MMGen) Bitcoin addresses
 -k, --keyaddr-file Address source is a key-address file
@@ -53,29 +55,46 @@ The --batch and --rescan options cannot be used together.
 
 cmd_args = opts.init(opts_data)
 
+def import_mmgen_list(infile):
+	al = (AddrList,KeyAddrList)[bool(opt.keyaddr_file)](infile)
+	if al.al_id.mmtype == 'S':
+		from mmgen.tx import segwit_is_active
+		if not segwit_is_active():
+			rdie(2,'Segwit is not active on this chain. Cannot import Segwit addresses')
+	return al
+
+def import_flat_list(lines):
+	al = AddrList(addrlist=lines)
+	from mmgen.bitcoin import verify_addr
+	qmsg_r('Validating addresses...')
+	for e in al.data:
+		if not verify_addr(e.addr,verbose=True):
+			die(2,'\n%s: invalid address' % e.addr)
+		if e.addr.addr_fmt == 'p2sh':
+			fs = "\n'{}':\n  Non-{} P2SH addresses may not be imported into the tracking wallet"
+			rdie(2,fs.format(e.addr,g.proj_name))
+	return al
+
 if len(cmd_args) == 1:
 	infile = cmd_args[0]
 	check_infile(infile)
 	if opt.addrlist:
 		lines = get_lines_from_file(
 			infile,'non-{pnm} addresses'.format(pnm=g.proj_name),trim_comments=True)
-		ai = AddrList(addrlist=lines)
+		al = import_flat_list(lines)
 	else:
-		ai = (AddrList,KeyAddrList)[bool(opt.keyaddr_file)](infile)
+		al = import_mmgen_list(infile)
+elif len(cmd_args) == 0 and opt.address:
+	al = import_flat_list([opt.address])
+	infile = 'command line'
 else:
 	die(1,"""
-You must specify an {pnm} address file (or a list of non-{pnm} addresses
-with the '--addrlist' option)
+You must specify an {pnm} address file, a single address, or a list of
+non-{pnm} addresses with the '--addrlist' option)
 """.strip().format(pnm=g.proj_name))
 
-from mmgen.bitcoin import verify_addr
-qmsg_r('Validating addresses...')
-for e in ai.data:
-	if not verify_addr(e.addr,verbose=True):
-		die(2,'%s: invalid address' % e.addr)
-
-m = (' from Seed ID %s' % ai.seed_id) if ai.seed_id else ''
-qmsg('OK. %s addresses%s' % (ai.num_addrs,m))
+m = ' from Seed ID {}'.format(al.al_id.sid) if hasattr(al.al_id,'sid') else ''
+qmsg('OK. {} addresses{}'.format(al.num_addrs,m))
 
 if not opt.test:
 	c = bitcoin_connection()
@@ -104,8 +123,8 @@ def import_address(addr,label,rescan):
 		global err_flag
 		err_flag = True
 
-w_n_of_m = len(str(ai.num_addrs)) * 2 + 2
-w_mmid   = '' if opt.addrlist else len(str(max(ai.idxs()))) + 12
+w_n_of_m = len(str(al.num_addrs)) * 2 + 2
+w_mmid   = '' if opt.addrlist else len(str(max(al.idxs()))) + 12
 
 if opt.rescan:
 	import threading
@@ -113,19 +132,26 @@ if opt.rescan:
 else:
 	msg_fmt = '\r%-{}s %-34s %s'.format(w_n_of_m, w_mmid)
 
-msg("Importing %s addresses from '%s'%s" %
-		(len(ai.data),infile,('',' (batch mode)')[bool(opt.batch)]))
+msg("Importing {} address{} from {}{}".format(
+		len(al.data), suf(al.data,'es'), infile,
+		('',' (batch mode)')[bool(opt.batch)]
+	))
+
+if not al.is_for_current_chain():
+	die(2,"Address{} not compatible with {} chain!".format((' list','')[bool(opt.address)],g.chain))
 
 arg_list = []
-for n,e in enumerate(ai.data):
+for n,e in enumerate(al.data):
 	if e.idx:
-		label = '%s:%s' % (ai.seed_id,e.idx)
+		label = '{}:{}'.format(al.al_id,e.idx)
 		if e.label: label += ' ' + e.label
 		m = label
 	else:
 		label = 'btc:{}'.format(e.addr)
 		m = 'non-'+g.proj_name
 
+	label = TwLabel(label)
+
 	if opt.batch:
 		arg_list.append((e.addr,label,False))
 	elif opt.rescan:
@@ -138,7 +164,7 @@ for n,e in enumerate(ai.data):
 		while True:
 			if t.is_alive():
 				elapsed = int(time.time() - start)
-				count = '%s/%s:' % (n+1, ai.num_addrs)
+				count = '%s/%s:' % (n+1, al.num_addrs)
 				msg_r(msg_fmt % (secs_to_hms(elapsed),count,e.addr,'(%s)' % m))
 				time.sleep(1)
 			else:
@@ -147,7 +173,7 @@ for n,e in enumerate(ai.data):
 				break
 	else:
 		import_address(e.addr,label,False)
-		count = '%s/%s:' % (n+1, ai.num_addrs)
+		count = '%s/%s:' % (n+1, al.num_addrs)
 		msg_r(msg_fmt % (count, e.addr, '(%s)' % m))
 		if err_flag: die(2,'\nImport failed')
 		msg(' - OK')

+ 3 - 2
mmgen/main_passgen.py

@@ -98,9 +98,9 @@ EXAMPLE:
 
                       NOTES FOR ALL GENERATOR COMMANDS
 
-{o.pw_note}
+{pwn}
 
-{o.bw_note}
+{bwn}
 
 FMT CODES:
   {f}
@@ -108,6 +108,7 @@ FMT CODES:
 		f='\n  '.join(SeedSource.format_fmt_codes().splitlines()),
 		o=opts,g=g,d58=dfl_len['b58'],d32=dfl_len['b32'],
 		ml=MMGenPWIDString.max_len,
+		pwn=pw_note,bwn=bw_note,
 		fs="', '".join(MMGenPWIDString.forbidden)
 	)
 }

+ 10 - 17
mmgen/main_tool.py

@@ -39,30 +39,23 @@ opts_data = {
 """.format(g=g),
 	'notes': """
 
-COMMANDS:{}
+                               COMMANDS
+{}
 Type '{} help <command> for help on a particular command
 """.format(tool.cmd_help,g.prog_name)
 }
 
-cmd_args = opts.init(opts_data,
-	add_opts=[
-		'hidden_incog_input_params',
-		'in_fmt'
-		])
+cmd_args = opts.init(opts_data,add_opts=['hidden_incog_input_params','in_fmt'])
 
-if len(cmd_args) < 1:
-	opts.usage()
-	sys.exit(1)
+if len(cmd_args) < 1: opts.usage()
 
-command = cmd_args.pop(0)
+Command = cmd_args.pop(0).capitalize()
 
-if command not in tool.cmd_data:
-	die(1,"'%s': no such command" % command)
+if Command == 'Help' and not cmd_args: tool.usage(None)
 
-if cmd_args and cmd_args[0] == '--help':
-	tool.tool_usage(g.prog_name, command)
-	sys.exit()
+if Command not in tool.cmd_data:
+	die(1,"'%s': no such command" % Command.lower())
 
-args,kwargs = tool.process_args(g.prog_name, command, cmd_args)
-ret = tool.__dict__[command](*args,**kwargs)
+args,kwargs = tool.process_args(Command,cmd_args)
+ret = tool.__dict__[Command](*args,**kwargs)
 sys.exit(0 if ret in (None,True) else 1) # some commands die, some return False on failure

+ 1 - 1
mmgen/main_txcreate.py

@@ -51,5 +51,5 @@ opts_data = {
 
 cmd_args = opts.init(opts_data)
 do_license_msg()
-tx = txcreate(opt,cmd_args,do_info=opt.info)
+tx = txcreate(cmd_args,do_info=opt.info)
 tx.write_to_file(ask_write=not opt.yes,ask_overwrite=not opt.yes,ask_write_default_yes=False)

+ 1 - 1
mmgen/main_txdo.py

@@ -83,7 +83,7 @@ kal = get_keyaddrlist(opt)
 kl = get_keylist(opt)
 if kl and kal: kl.remove_dups(kal,key='wif')
 
-tx = txcreate(opt,cmd_args,caller='txdo')
+tx = txcreate(cmd_args,caller='txdo')
 txsign(opt,c,tx,seed_files,kl,kal)
 tx.write_to_file(ask_write=False)
 

+ 3 - 7
mmgen/main_txsend.py

@@ -44,17 +44,13 @@ if len(cmd_args) == 1:
 else: opts.usage()
 
 do_license_msg()
-tx = MMGenTX(infile)
 c = bitcoin_connection()
+tx = MMGenTX(infile) # sig check performed here
+qmsg("Signed transaction file '%s' is valid" % infile)
 
-if not tx.check_signed(c):
+if not tx.marked_signed(c):
 	die(1,'Transaction is not signed!')
 
-if tx.btc_txid:
-	msg('Warning: transaction has already been sent!')
-
-qmsg("Signed transaction file '%s' is valid" % infile)
-
 if not opt.yes:
 	tx.view_with_prompt('View transaction data?')
 	if tx.add_comment(): # edits an existing comment, returns true if changed

+ 1 - 1
mmgen/main_txsign.py

@@ -90,7 +90,7 @@ for tx_num,tx_file in enumerate(tx_files,1):
 		tx_num_str = ' #%s' % tx_num
 	tx = MMGenTX(tx_file)
 
-	if tx.check_signed(c):
+	if tx.marked_signed():
 		die(1,'Transaction is already signed!')
 	vmsg("Successfully opened transaction file '%s'" % tx_file)
 

+ 5 - 8
mmgen/main_wallet.py

@@ -30,8 +30,6 @@ usage = '[opts] [infile]'
 nargs = 1
 iaction = 'convert'
 oaction = 'convert'
-bw_note = opts.bw_note
-pw_note = opts.pw_note
 
 invoked_as = 'passchg' if g.prog_name == 'mmgen-passchg' else g.prog_name.partition('-wallet')[2]
 
@@ -99,14 +97,14 @@ opts_data = {
 	),
 	'notes': """
 
-{pw_note}{bw_note}
+{pwn}{bwn}
 
 FMT CODES:
   {f}
 """.format(
 	f='\n  '.join(SeedSource.format_fmt_codes().splitlines()),
-	pw_note=pw_note,
-	bw_note=('','\n\n' + bw_note)[bool(bw_note)]
+	pwn=pw_note,
+	bwn=('','\n\n' + bw_note)[bool(bw_note)]
 	)
 }
 
@@ -125,12 +123,11 @@ if invoked_as in ('conv','passchg'):
 	msg(green('Processing input wallet')+dw_msg)
 
 ss_in = None if invoked_as == 'gen' else SeedSource(sf,passchg=(invoked_as=='passchg'))
-
 if invoked_as == 'chk':
 	lbl = ss_in.ssdata.label.hl() if hasattr(ss_in.ssdata,'label') else 'NONE'
 	vmsg('Wallet label: {}'.format(lbl))
 	# TODO: display creation date
-	sys.exit()
+	sys.exit(0)
 
 if invoked_as in ('conv','passchg'):
 	msg(green('Processing output wallet'))
@@ -141,7 +138,7 @@ if invoked_as == 'gen':
 	qmsg("This wallet's Seed ID: %s" % ss_out.seed.sid.hl())
 
 if invoked_as == 'passchg':
-	if not (opt.force_update or [k for k in 'passwd','hash_preset','label'
+	if not (opt.force_update or [k for k in ('passwd','hash_preset','label')
 		if getattr(ss_out.ssdata,k) != getattr(ss_in.ssdata,k)]):
 		die(1,'Password, hash preset and label are unchanged.  Taking no action')
 

+ 237 - 53
mmgen/obj.py

@@ -20,6 +20,7 @@
 obj.py:  MMGen native classes
 """
 
+import sys
 from decimal import *
 from mmgen.color import *
 lvl = 0
@@ -27,38 +28,73 @@ lvl = 0
 class MMGenObject(object):
 
 	# Pretty-print any object of type MMGenObject, recursing into sub-objects - WIP
-	def pprint(self):  print self.pformat()
+# 	def pmsg(self):  sys.stderr.write(self.pformat()+'\n')
+# 	def pdie(self):  sys.stderr.write(self.pformat()+'\n'); sys.exit(0)
+	def pmsg(self):  print(self.pformat())
+	def pdie(self):  print(self.pformat()); sys.exit(0)
 	def pformat(self,lvl=0):
-		def do_list(out,e,lvl=0):
-			add_spc = False
-			if e and type(e[0]) not in (str,unicode):
-				out.append('\n')
+		from decimal import Decimal
+		scalars = (str,unicode,int,float,Decimal)
+		def do_list(out,e,lvl=0,is_dict=False):
+			out.append('\n')
 			for i in e:
-				if hasattr(i,'pformat'):
-					out.append('{:>{l}}{}'.format('',i.pformat(lvl=lvl+1),l=(lvl+1)*8))
-				elif type(i) in (str,unicode):
-					add_spc = True
-					out.append(u' {}'.format(repr(i)))
-				elif type(i) == list:
-					out.append(u'{:>{l}}{:16}'.format('','<'+type(i).__name__+'>',l=(lvl*8)+4))
-					do_list(out,i,lvl=lvl)
+				el = i if not is_dict else e[i]
+				if is_dict:
+					out.append('{s}{:<{l}}'.format(i,s=' '*(4*lvl+8),l=10,l2=8*(lvl+1)+8))
+				if hasattr(el,'pformat'):
+					out.append('{:>{l}}{}'.format('',el.pformat(lvl=lvl+1),l=(lvl+1)*8))
+				elif type(el) in scalars:
+					if isList(e):
+						out.append(u'{:>{l}}{:16}\n'.format('',repr(el),l=lvl*8))
+					else:
+						out.append(u' {}'.format(repr(el)))
+				elif isList(el) or isDict(el):
+					indent = 1 if is_dict else lvl*8+4
+					out.append(u'{:>{l}}{:16}'.format('','<'+type(el).__name__+'>',l=indent))
+					if isList(el) and type(el[0]) in scalars: out.append('\n')
+					do_list(out,el,lvl=lvl+1,is_dict=isDict(el))
 				else:
-					out.append(u'{:>{l}}{:16} {}\n'.format('','<'+type(i).__name__+'>',repr(i),l=(lvl*8)+8))
+					out.append(u'{:>{l}}{:16} {}\n'.format('','<'+type(el).__name__+'>',repr(el),l=(lvl*8)+8))
+				out.append('\n')
 			if not e: out.append('{}\n'.format(repr(e)))
-			if add_spc: out.append('\n')
-		out = []
-		out.append(u'<{}>\n'.format(type(self).__name__))
-		d = self.__dict__
-		for k in d:
+
+		from collections import OrderedDict
+		def isDict(obj):
+			return issubclass(type(obj),dict) or issubclass(type(obj),OrderedDict)
+		def isList(obj):
+			return issubclass(type(obj),list) and type(obj) != OrderedDict
+		def isScalar(obj):
+			return any(issubclass(type(obj),t) for t in scalars)
+
+# 		print type(self)
+# 		print dir(self)
+# 		print self.__dict__ # *attributes* of object
+# 		print self.__dict__.keys() # *attributes* of object
+# 		print self.keys()
+
+		out = [u'<{}>{}\n'.format(type(self).__name__,' '+repr(self) if isScalar(self) else '')]
+		if isList(self) or isDict(self):
+			do_list(out,self,lvl=lvl,is_dict=isDict(self))
+
+#		print repr(self.__dict__.keys())
+
+		for k in self.__dict__:
+			if k in ('_OrderedDict__root', '_OrderedDict__map'): continue # exclude these because of recursion
 			e = getattr(self,k)
-			if type(e) == list:
+			if isList(e) or isDict(e):
 				out.append(u'{:>{l}}{:<10} {:16}'.format('',k,'<'+type(e).__name__+'>',l=(lvl*8)+4))
-				do_list(out,e,lvl=lvl)
+				do_list(out,e,lvl=lvl,is_dict=isDict(e))
 			elif hasattr(e,'pformat') and type(e) != type:
 				out.append(u'{:>{l}}{:10} {}'.format('',k,e.pformat(lvl=lvl+1),l=(lvl*8)+4))
 			else:
-				out.append(u'{:>{l}}{:<10} {:16} {}\n'.format('',k,'<'+type(e).__name__+'>',repr(e),l=(lvl*8)+4))
-		return ''.join(out)
+				out.append(u'{:>{l}}{:<10} {:16} {}\n'.format(
+					'',k,'<'+type(e).__name__+'>',repr(e),l=(lvl*8)+4))
+
+		import re
+		return re.sub('\n+','\n',''.join(out))
+
+class MMGenList(list,MMGenObject): pass
+class MMGenDict(dict,MMGenObject): pass
 
 # Descriptor: https://docs.python.org/2/howto/descriptor.html
 class MMGenListItemAttr(object):
@@ -75,10 +111,10 @@ class MMGenListItemAttr(object):
 
 class MMGenListItem(MMGenObject):
 
-	addr = MMGenListItemAttr('addr','BTCAddr')
-	amt  = MMGenListItemAttr('amt','BTCAmt')
-	mmid = MMGenListItemAttr('mmid','MMGenID')
-	label = MMGenListItemAttr('label','MMGenAddrLabel')
+	addr  = MMGenListItemAttr('addr','BTCAddr')
+	amt   = MMGenListItemAttr('amt','BTCAmt')
+	mmid  = MMGenListItemAttr('mmid','MMGenID')
+	label = MMGenListItemAttr('label','TwComment')
 
 	attrs = ()
 	attrs_priv = ()
@@ -91,7 +127,8 @@ class MMGenListItem(MMGenObject):
 			"'{}': 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__','pformat')
+	attrs_base = ('attrs','attrs_priv','attrs_reassign','attrs_base','attr_error','set_error',
+				'__dict__','pformat','pmsg','pdie')
 
 	def __init__(self,*args,**kwargs):
 		if args:
@@ -133,7 +170,7 @@ 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__
+		assert on_fail in ('die','return','silent','raise'),"arg_chk in class %s" % cls.__name__
 
 	@staticmethod
 	def init_fail(m,on_fail,silent=False):
@@ -142,8 +179,8 @@ class InitErrors(object):
 		if on_fail == 'die':      die(1,m)
 		elif on_fail == 'return':
 			if m: msg(m)
-			return None
-		elif on_fail == 'silent': return None
+			return None # TODO: change to False
+		elif on_fail == 'silent': return None # same here
 		elif on_fail == 'raise':  raise ValueError,m
 
 class AddrIdx(int,InitErrors):
@@ -167,7 +204,7 @@ class AddrIdx(int,InitErrors):
 
 		return cls.init_fail(m,on_fail)
 
-class AddrIdxList(list,InitErrors):
+class AddrIdxList(list,InitErrors,MMGenObject):
 
 	max_len = 1000000
 
@@ -176,7 +213,7 @@ class AddrIdxList(list,InitErrors):
 		assert fmt_str or idx_list
 		if idx_list:
 			# dies on failure
-			return list.__init__(self,sorted(set([AddrIdx(i) for i in idx_list])))
+			return list.__init__(self,sorted(set(AddrIdx(i) for i in idx_list)))
 		elif fmt_str:
 			desc = fmt_str
 			ret,fs = [],"'%s': value cannot be converted to address index"
@@ -315,17 +352,19 @@ class BTCAmt(Decimal,Hilite,InitErrors):
 	def __neg__(self,other,context=None):
 		return type(self)(Decimal.__neg__(self,other,context))
 
-class BTCAddr(str,Hilite,InitErrors):
+class BTCAddr(str,Hilite,InitErrors,MMGenObject):
 	color = 'cyan'
-	width = 34
+	width = 35 # max len of testnet p2sh addr
 	def __new__(cls,s,on_fail='die'):
 		cls.arg_chk(cls,on_fail)
+		m = "'%s': value is not a Bitcoin address" % s
 		me = str.__new__(cls,s)
-		from mmgen.bitcoin import verify_addr
-		if type(s) in (str,unicode,BTCAddr) and verify_addr(s):
-			return me
-		else:
-			m = "'%s': value is not a Bitcoin address" % s
+		from mmgen.bitcoin import verify_addr,addr_pfxs
+		if type(s) in (str,unicode,BTCAddr):
+			me.addr_fmt = verify_addr(s,return_type=True)
+			me.testnet = s[0] in addr_pfxs['testnet']
+			if me.addr_fmt:
+				return me
 		return cls.init_fail(m,on_fail)
 
 	@classmethod
@@ -338,6 +377,21 @@ class BTCAddr(str,Hilite,InitErrors):
 			s = s[:kwargs['width']-2] +  '..'
 		return Hilite.fmtc(s,**kwargs)
 
+	def is_for_current_chain(self):
+		from mmgen.globalvars import g
+		assert g.chain, 'global chain variable unset'
+		from bitcoin import addr_pfxs
+		return self[0] in addr_pfxs[g.chain]
+
+	def is_mainnet(self):
+		from bitcoin import addr_pfxs
+		return self[0] in addr_pfxs['mainnet']
+
+	def is_in_tracking_wallet(self):
+		from mmgen.rpc import bitcoin_connection
+		d = bitcoin_connection().validateaddress(self)
+		return d['iswatchonly'] and 'account' in d
+
 class SeedID(str,Hilite,InitErrors):
 	color = 'blue'
 	width = 8
@@ -351,6 +405,7 @@ class SeedID(str,Hilite,InitErrors):
 			if type(seed) == Seed:
 				return str.__new__(cls,make_chksum_8(seed.get_data()))
 		elif sid:
+			sid = str(sid)
 			from string import hexdigits
 			if len(sid) == cls.width and set(sid) <= set(hexdigits.upper()):
 				return str.__new__(cls,sid)
@@ -358,7 +413,7 @@ class SeedID(str,Hilite,InitErrors):
 		m = "'%s': value cannot be converted to SeedID" % str(seed or sid)
 		return cls.init_fail(m,on_fail)
 
-class MMGenID(str,Hilite,InitErrors):
+class MMGenID(str,Hilite,InitErrors,MMGenObject):
 
 	color = 'orange'
 	width = 0
@@ -367,15 +422,83 @@ class MMGenID(str,Hilite,InitErrors):
 	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='silent')
-			if sid:
-				idx = AddrIdx(b,on_fail='silent')
-				if idx:
-					return str.__new__(cls,'%s:%s' % (sid,idx))
-
-		m = "'%s': value cannot be converted to MMGenID" % s
+		try:
+			ss = s.split(':')
+			assert len(ss) in (2,3)
+			sid = SeedID(sid=ss[0],on_fail='silent')
+			assert sid
+			idx = AddrIdx(ss[-1],on_fail='silent')
+			assert idx
+			t = MMGenAddrType((MMGenAddrType.dfl_mmtype,ss[1])[len(ss) != 2],on_fail='silent')
+			assert t
+			me = str.__new__(cls,'{}:{}:{}'.format(sid,t,idx))
+			me.sid = sid
+			me.mmtype = t
+			me.idx = idx
+			me.al_id = AddrListID(sid,me.mmtype) # key with colon!
+			assert me.al_id
+			me.sort_key = '{}:{}:{:0{w}}'.format(sid,t,idx,w=idx.max_digits)
+			return me
+		except:
+			m = "'%s': value cannot be converted to MMGenID" % s
+			return cls.init_fail(m,on_fail)
+
+class TwMMGenID(str,Hilite,InitErrors,MMGenObject):
+
+	color = 'orange'
+	width = 0
+	trunc_ok = False
+
+	def __new__(cls,s,on_fail='die'):
+		cls.arg_chk(cls,on_fail)
+		obj,sort_key = None,None
+		try:
+			obj = MMGenID(s,on_fail='silent')
+			sort_key,t = obj.sort_key,'mmgen'
+		except:
+			try:
+				assert len(s) > 4 and s[:4] == 'btc:'
+				obj,sort_key,t = str(s),'z_'+s,'non-mmgen'
+			except:
+				pass
+
+		if obj and sort_key:
+			me = str.__new__(cls,obj)
+			me.obj = obj
+			me.sort_key = sort_key
+			me.type = t
+			return me
+
+		m = "'{}': value cannot be converted to {}".format(s,cls.__name__)
+		return cls.init_fail(m,on_fail)
+
+# contains TwMMGenID,TwComment.  Not for display
+class TwLabel(str,InitErrors,MMGenObject):
+
+	def __new__(cls,s,on_fail='die'):
+		cls.arg_chk(cls,on_fail)
+		try:
+			ss = s.split(None,1)
+			me = str.__new__(cls,s)
+			me.mmid = TwMMGenID(ss[0],on_fail='silent')
+			assert me.mmid
+			me.comment = TwComment(ss[1] if len(ss) == 2 else '',on_fail='silent')
+			assert me.comment != None
+			return me
+		except:
+			m = "'{}': value cannot be converted to {}".format(s,cls.__name__)
+			return cls.init_fail(m,on_fail)
+
+class HexStr(str,Hilite,InitErrors):
+	color = 'red'
+	trunc_ok = False
+	def __new__(cls,s,on_fail='die',case='lower'):
+		assert case in ('upper','lower')
+		cls.arg_chk(cls,on_fail)
+		from string import hexdigits
+		if set(s) <= set(getattr(hexdigits,case)()) and not len(s) % 2:
+			return str.__new__(cls,s)
+		m = "'{}': value cannot be converted to {}".format(s,cls.__name__)
 		return cls.init_fail(m,on_fail)
 
 class MMGenTxID(str,Hilite,InitErrors):
@@ -396,6 +519,65 @@ class BitcoinTxID(MMGenTxID):
 	width = 64
 	hexcase = 'lower'
 
+class WifKey(str,Hilite,InitErrors):
+	width = 53
+	color = 'blue'
+	desc = 'WIF key'
+	def __new__(cls,s,on_fail='die',errmsg=None):
+		cls.arg_chk(cls,on_fail)
+		from mmgen.tx import is_wif
+		if is_wif(s):
+			me = str.__new__(cls,s)
+			return me
+		m = errmsg or "'{}': invalid value for {}".format(s,cls.desc)
+		return cls.init_fail(m,on_fail)
+
+class MMGenAddrType(str,Hilite,InitErrors):
+	width = 1
+	trunc_ok = False
+	color = 'blue'
+	mmtypes = {
+		# TODO 'L' is ambiguous: For user, it means MMGen legacy uncompressed address.
+		# For generator functions, 'L' means any p2pkh address, and 'S' any ps2h address
+		'L': 'legacy',
+		'S': 'segwit',
+# 		'l': 'litecoin',
+# 		'e': 'ethereum',
+# 		'E': 'ethereum_classic',
+# 		'm': 'monero',
+# 		'z': 'zcash',
+	}
+	dfl_mmtype = 'L'
+	def __new__(cls,s,on_fail='die',errmsg=None):
+		cls.arg_chk(cls,on_fail)
+		for k,v in cls.mmtypes.items():
+			if s in (k,v):
+				if s == v: s = k
+				me = str.__new__(cls,s)
+				me.name = cls.mmtypes[s]
+				return me
+		m = errmsg or "'{}': invalid value for {}".format(s,cls.__name__)
+		return cls.init_fail(m,on_fail)
+
+class MMGenPasswordType(MMGenAddrType):
+	mmtypes = { 'P': 'password' }
+
+class AddrListID(str,Hilite,InitErrors):
+	width = 10
+	trunc_ok = False
+	color = 'yellow'
+	def __new__(cls,sid,mmtype,on_fail='die'):
+		cls.arg_chk(cls,on_fail)
+		m = "'{}': not a SeedID. Cannot create {}".format(sid,cls.__name__)
+		if type(sid) == SeedID:
+			m = "'{}': not an MMGenAddrType object. Cannot create {}".format(mmtype,cls.__name__)
+			if type(mmtype) in (MMGenAddrType,MMGenPasswordType):
+				me = str.__new__(cls,sid+':'+mmtype) # colon in key is OK
+				me.sid = sid
+				me.mmtype = mmtype
+				return me
+		return cls.init_fail(m,on_fail)
+
 class MMGenLabel(unicode,Hilite,InitErrors):
 
 	color = 'pink'
@@ -425,7 +607,7 @@ class MMGenLabel(unicode,Hilite,InitErrors):
 			elif cls.allowed and not set(list(s)).issubset(set(cls.allowed)):
 				m = u"{} '{}' contains non-allowed symbols: {}".format(capfirst(cls.desc),s,
 					' '.join(set(list(s)) - set(cls.allowed)))
-			elif cls.forbidden and any([ch in s for ch in cls.forbidden]):
+			elif cls.forbidden and any(ch in s for ch in cls.forbidden):
 				m = u"{} '{}' contains one of these forbidden symbols: '{}'".format(capfirst(cls.desc),s,
 					"', '".join(cls.forbidden))
 			else:
@@ -437,10 +619,10 @@ class MMGenWalletLabel(MMGenLabel):
 	allowed = [unichr(i+32) for i in range(95)]
 	desc = 'wallet label'
 
-class MMGenAddrLabel(MMGenLabel):
+class TwComment(MMGenLabel):
 	max_len = 32
 	allowed = [unichr(i+32) for i in range(95)]
-	desc = 'address label'
+	desc = 'tracking wallet comment'
 
 class MMGenTXLabel(MMGenLabel):
 	max_len = 72
@@ -451,3 +633,5 @@ class MMGenPWIDString(MMGenLabel):
 	min_len = 1
 	desc = 'password ID string'
 	forbidden = list(u' :/\\')
+
+class AddrListList(list,MMGenObject): pass

+ 13 - 22
mmgen/opts.py

@@ -27,26 +27,6 @@ from mmgen.globalvars import g
 import mmgen.share.Opts
 from mmgen.util import *
 
-pw_note = """
-For passphrases all combinations of whitespace are equal and leading and
-trailing space is ignored.  This permits reading passphrase or brainwallet
-data from a multi-line file with free spacing and indentation.
-""".strip()
-
-bw_note = """
-BRAINWALLET NOTE:
-
-To thwart dictionary attacks, it's recommended to use a strong hash preset
-with brainwallets.  For a brainwallet passphrase to generate the correct
-seed, the same seed length and hash preset parameters must always be used.
-""".strip()
-
-version_info = """
-{pgnm_uc} version {g.version}
-Part of the {pnm} suite, a Bitcoin cold-storage solution for the command line.
-Copyright (C) {g.Cdates} {g.author} {g.email}
-""".format(pnm=g.proj_name, g=g, pgnm_uc=g.prog_name.upper()).strip()
-
 def usage(): Die(2,'USAGE: %s %s' % (g.prog_name, usage_txt))
 
 def die_on_incompatible_opts(incompat_list):
@@ -76,6 +56,7 @@ common_opts_data = """
 --, --rpc-port=p          Communicate with bitcoind listening on port 'p'
 --, --rpc-user=user       Override 'rpcuser' in bitcoin.conf
 --, --rpc-password=pass   Override 'rpcpassword' in bitcoin.conf
+--, --regtest=0|1         Disable or enable regtest mode
 --, --testnet=0|1         Disable or enable testnet
 --, --skip-cfg-file       Skip reading the configuration file
 --, --version             Print version information and exit
@@ -179,6 +160,13 @@ def override_from_env():
 			setattr(g,gname,set_for_type(val,getattr(g,gname),name,invert_bool))
 
 def init(opts_data,add_opts=[],opt_filter=None):
+
+	version_info = """
+    {pgnm_uc} version {g.version}
+    Part of the {pnm} suite, a Bitcoin cold-storage solution for the command line.
+    Copyright (C) {g.Cdates} {g.author} {g.email}
+	""".format(pnm=g.proj_name, g=g, pgnm_uc=g.prog_name.upper()).strip()
+
 	opts_data['long_options'] = common_opts_data
 
 	uopts,args,short_opts,long_opts,skipped_opts = \
@@ -218,6 +206,8 @@ def init(opts_data,add_opts=[],opt_filter=None):
 		val = getattr(opt,k)
 		if val != None: setattr(g,k,set_for_type(val,getattr(g,k),'--'+k))
 
+	if g.regtest: g.testnet = True # These are equivalent for now
+
 #	Global vars are now final, including g.testnet, so we can set g.data_dir
 	g.data_dir=os.path.normpath(os.path.join(g.data_dir_root,('',g.testnet_name)[g.testnet]))
 
@@ -238,15 +228,16 @@ def init(opts_data,add_opts=[],opt_filter=None):
 
 	if opt.show_hash_presets:
 		_show_hash_presets()
-		sys.exit()
+		sys.exit(0)
 
-	if g.debug: opt_postproc_debug()
 	if opt.verbose: opt.quiet = None
 
 	die_on_incompatible_opts(g.incompatible_opts)
 
 	opt_postproc_initializations()
 
+	if g.debug: opt_postproc_debug()
+
 	return args
 
 def check_opts(usr_opts):       # Returns false if any check fails

+ 13 - 10
mmgen/rpc.py

@@ -28,8 +28,6 @@ from mmgen.obj import BTCAmt
 
 class BitcoinRPCConnection(object):
 
-	client_version = 0
-
 	def __init__(
 				self,
 				host=g.rpc_host,port=(8332,18332)[g.testnet],
@@ -78,7 +76,7 @@ class BitcoinRPCConnection(object):
 			p = {'method':cmd,'params':args,'id':1}
 
 		def die_maybe(*args):
-			if cf['on_fail'] == 'return':
+			if cf['on_fail'] in ('return','silent'):
 				return 'rpcfail',args
 			else:
 				die(*args[1:])
@@ -89,7 +87,7 @@ class BitcoinRPCConnection(object):
 		class MyJSONEncoder(json.JSONEncoder):
 			def default(self, obj):
 				if isinstance(obj, BTCAmt):
-					return (float,str)[caller.client_version>=120000](obj)
+					return (float,str)[g.bitcoind_version>=120000](obj)
 				return json.JSONEncoder.default(self, obj)
 
 # Can't do UTF-8 labels yet: httplib only ascii?
@@ -111,8 +109,9 @@ class BitcoinRPCConnection(object):
 		dmsg('    RPC GETRESPONSE data ==> %s\n' % r.__dict__)
 
 		if r.status != 200:
-			msg_r(yellow('Bitcoind RPC Error: '))
-			msg(red('{} {}'.format(r.status,r.reason)))
+			if cf['on_fail'] != 'silent':
+				msg_r(yellow('Bitcoind RPC Error: '))
+				msg(red('{} {}'.format(r.status,r.reason)))
 			e1 = r.read()
 			try:
 				e3 = json.loads(e1)['error']
@@ -142,26 +141,30 @@ class BitcoinRPCConnection(object):
 
 		return ret if cf['batch'] else ret[0]
 
-
 	rpcmethods = (
-		'createrawtransaction',
 		'backupwallet',
+		'createrawtransaction',
 		'decoderawtransaction',
 		'disconnectnode',
 		'estimatefee',
 		'getaddressesbyaccount',
 		'getbalance',
 		'getblock',
+		'getblockchaininfo',
 		'getblockcount',
 		'getblockhash',
-		'getinfo',
+		'getmempoolentry',
+		'getnetworkinfo',
 		'getpeerinfo',
+		'getrawmempool',
+		'getrawtransaction',
+		'gettransaction',
 		'importaddress',
 		'listaccounts',
 		'listunspent',
 		'sendrawtransaction',
 		'signrawtransaction',
-		'getrawmempool',
+		'validateaddress',
 		'walletpassphrase',
 	)
 

+ 15 - 12
mmgen/seed.py

@@ -159,18 +159,21 @@ class SeedSource(MMGenObject):
 			msg('Trying again...')
 
 	@classmethod
-	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
+	def get_subclasses_str(cls): # returns name of calling class too
+		return cls.__name__ + ' ' + ''.join([c.get_subclasses_str() for c in cls.__subclasses__()])
 
 	@classmethod
-	def get_subclasses_str(cls):
-		def GetSubclassesTree(cls):
-			return ''.join([c.__name__ +' '+ GetSubclassesTree(c) for c in cls.__subclasses__()])
-		return GetSubclassesTree(cls)
+	def get_subclasses_easy(cls,acc=[]):
+		return [globals()[c] for c in cls.get_subclasses_str().split()]
+
+	@classmethod
+	def get_subclasses(cls): # returns calling class too
+		def GetSubclassesTree(cls,acc):
+			acc += [cls]
+			for c in cls.__subclasses__(): GetSubclassesTree(c,acc)
+		acc = []
+		GetSubclassesTree(cls,acc)
+		return acc
 
 	@classmethod
 	def get_extensions(cls):
@@ -1027,8 +1030,8 @@ harder to find, you're advised to choose a much larger file size than this.
 					msg('File size must be an integer no less than %s' %
 							min_fsize)
 
-				from mmgen.tool import rand2file
-				rand2file(fn, str(fsize))
+				from mmgen.tool import Rand2file # threaded routine
+				Rand2file(fn,str(fsize))
 				check_offset = False
 			else:
 				die(1,'Exiting at user request')

+ 3 - 3
mmgen/share/Opts.py

@@ -21,7 +21,7 @@ Opts.py:  Generic options handling
 """
 
 import sys, getopt
-# from mmgen.util import mdie,die,pp_die,pp_msg # DEBUG
+# from mmgen.util import mdie,die,pdie,pmsg # DEBUG
 
 def usage(opts_data):
 	print 'USAGE: %s %s' % (opts_data['prog_name'], opts_data['usage'])
@@ -54,8 +54,8 @@ def process_opts(argv,opts_data,short_opts,long_opts):
 	opts = {}
 
 	for opt, arg in cl_opts:
-		if   opt in ('-h','--help'): print_help(opts_data); sys.exit()
-		elif opt == '--longhelp':    print_help(opts_data,longhelp=True); sys.exit()
+		if   opt in ('-h','--help'): print_help(opts_data); sys.exit(0)
+		elif opt == '--longhelp':    print_help(opts_data,longhelp=True); sys.exit(0)
 		elif opt[:2] == '--' and opt[2:] in long_opts:
 			opts[opt[2:].replace('-','_')] = True
 		elif opt[:2] == '--' and opt[2:]+'=' in long_opts:

+ 2 - 1
mmgen/test.py

@@ -48,7 +48,8 @@ def mk_tmpdir(d):
 	try: os.mkdir(d,0755)
 	except OSError as e:
 		if e.errno != 17: raise
-	else: msg("Created directory '%s'" % d)
+	else:
+		qmsg("Created directory '%s'" % d)
 
 def mk_tmpdir_path(path,cfg):
 	try:

+ 347 - 242
mmgen/tool.py

@@ -1,4 +1,5 @@
 #!/usr/bin/env python
+# -*- coding: UTF-8 -*-
 #
 # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
 # Copyright (C)2013-2017 Philemon <mmgen-py@yandex.com>
@@ -31,84 +32,99 @@ pnm = g.proj_name
 
 from collections import OrderedDict
 cmd_data = OrderedDict([
-	('help',         ['<tool command> [str]']),
-	('usage',        ['<tool command> [str]']),
-	('strtob58',     ['<string> [str-]','pad [int=0]']),
-	('b58tostr',     ['<b58 number> [str-]']),
-	('hextob58',     ['<hex number> [str-]','pad [int=0]']),
-	('b58tohex',     ['<b58 number> [str-]','pad [int=0]']),
-	('b58randenc',   []),
-	('b32tohex',     ['<b32 num> [str-]','pad [int=0]']),
-	('hextob32',     ['<hex num> [str-]','pad [int=0]']),
-	('randhex',      ['nbytes [int=32]']),
-	('id8',          ['<infile> [str]']),
-	('id6',          ['<infile> [str]']),
-	('sha256x2',     ['<str, hexstr or filename> [str]', # TODO handle stdin
+	('Help',         ['<tool command> [str]']),
+	('Usage',        ['<tool command> [str]']),
+	('Strtob58',     ['<string> [str-]','pad [int=0]']),
+	('B58tostr',     ['<b58 number> [str-]']),
+	('Hextob58',     ['<hex number> [str-]','pad [int=0]']),
+	('B58tohex',     ['<b58 number> [str-]','pad [int=0]']),
+	('B58randenc',   []),
+	('B32tohex',     ['<b32 num> [str-]','pad [int=0]']),
+	('Hextob32',     ['<hex num> [str-]','pad [int=0]']),
+	('Randhex',      ['nbytes [int=32]']),
+	('Id8',          ['<infile> [str]']),
+	('Id6',          ['<infile> [str]']),
+	('Hash160',      ['<hexadecimal string> [str-]']),
+	('Hash256',      ['<str, hexstr or filename> [str]', # TODO handle stdin
 							'hex_input [bool=False]','file_input [bool=False]']),
-	('str2id6',      ['<string (spaces are ignored)> [str-]']),
-	('hexdump',      ['<infile> [str]', 'cols [int=8]', 'line_nums [bool=True]']),
-	('unhexdump',    ['<infile> [str]']),
-	('hexreverse',   ['<hexadecimal string> [str-]']),
-	('hexlify',      ['<string> [str-]']),
-	('rand2file',    ['<outfile> [str]','<nbytes> [str]','threads [int=4]','silent [bool=False]']),
-
-	('randwif',    ['compressed [bool=False]']),
-	('randpair',   ['compressed [bool=False]']),
-	('hex2wif',    ['<private key in hex format> [str-]', 'compressed [bool=False]']),
-	('wif2hex',    ['<wif> [str-]', 'compressed [bool=False]']),
-	('wif2addr',   ['<wif> [str-]', 'compressed [bool=False]']),
-	('hexaddr2addr', ['<btc address in hex format> [str-]']),
-	('addr2hexaddr', ['<btc address> [str-]']),
-	('pubkey2addr',  ['<public key in hex format> [str-]']),
-	('pubkey2hexaddr', ['<public key in hex format> [str-]']),
-	('privhex2addr', ['<private key in hex format> [str-]','compressed [bool=False]']),
-
-	('hex2mn',       ['<hexadecimal string> [str-]',"wordlist [str='electrum']"]),
-	('mn2hex',       ['<mnemonic> [str-]', "wordlist [str='electrum']"]),
-	('mn_rand128',   ["wordlist [str='electrum']"]),
-	('mn_rand192',   ["wordlist [str='electrum']"]),
-	('mn_rand256',   ["wordlist [str='electrum']"]),
-	('mn_stats',     ["wordlist [str='electrum']"]),
-	('mn_printlist', ["wordlist [str='electrum']"]),
-
-	('listaddresses',["addrs [str='']",'minconf [int=1]','showempty [bool=False]','pager [bool=False]','showbtcaddrs [bool=False]']),
-	('getbalance',   ['minconf [int=1]']),
-	('txview',       ['<{} TX file> [str]'.format(pnm),'pager [bool=False]','terse [bool=False]']),
-	('twview',       ["sort [str='age']",'reverse [bool=False]','minconf [int=1]','wide [bool=False]','pager [bool=False]']),
-
-	('add_label',       ['<{} address> [str]'.format(pnm),'<label> [str]']),
-	('remove_label',    ['<{} address> [str]'.format(pnm)]),
-	('addrfile_chksum', ['<{} addr file> [str]'.format(pnm)]),
-	('keyaddrfile_chksum', ['<{} addr file> [str]'.format(pnm)]),
-	('passwdfile_chksum', ['<{} password file> [str]'.format(pnm)]),
-	('find_incog_data', ['<file or device name> [str]','<Incog ID> [str]','keep_searching [bool=False]']),
-
-	('encrypt',      ['<infile> [str]',"outfile [str='']","hash_preset [str='']"]),
-	('decrypt',      ['<infile> [str]',"outfile [str='']","hash_preset [str='']"]),
-	('bytespec',     ['<bytespec> [str]']),
+	('Str2id6',      ['<string (spaces are ignored)> [str-]']),
+	('Hexdump',      ['<infile> [str]', 'cols [int=8]', 'line_nums [bool=True]']),
+	('Unhexdump',    ['<infile> [str]']),
+	('Hexreverse',   ['<hexadecimal string> [str-]']),
+	('Hexlify',      ['<string> [str-]']),
+	('Rand2file',    ['<outfile> [str]','<nbytes> [str]','threads [int=4]','silent [bool=False]']),
+
+	('Randwif',    ['compressed [bool=False]']),
+	('Randpair',   ['compressed [bool=False]','segwit [bool=False]']),
+	('Hex2wif',    ['<private key in hex format> [str-]','compressed [bool=False]']),
+	('Wif2hex',    ['<wif> [str-]']),
+	('Wif2addr',   ['<wif> [str-]','segwit [bool=False]']),
+	('Wif2segwit_pair',['<wif> [str-]']),
+	('Hexaddr2addr', ['<btc address in hex format> [str-]']),
+	('Addr2hexaddr', ['<btc address> [str-]']),
+	('Privhex2addr', ['<private key in hex format> [str-]','compressed [bool=False]','segwit [bool=False]']),
+	('Privhex2pubhex',['<private key in hex format> [str-]','compressed [bool=False]']),
+	('Pubhex2addr',  ['<public key in hex format> [str-]','p2sh [bool=False]']), # new
+	('Pubhex2redeem_script',['<public key in hex format> [str-]']), # new
+	('Wif2redeem_script', ['<private key in WIF format> [str-]']), # new
+
+	('Hex2mn',       ['<hexadecimal string> [str-]',"wordlist [str='electrum']"]),
+	('Mn2hex',       ['<mnemonic> [str-]', "wordlist [str='electrum']"]),
+	('Mn_rand128',   ["wordlist [str='electrum']"]),
+	('Mn_rand192',   ["wordlist [str='electrum']"]),
+	('Mn_rand256',   ["wordlist [str='electrum']"]),
+	('Mn_stats',     ["wordlist [str='electrum']"]),
+	('Mn_printlist', ["wordlist [str='electrum']"]),
+
+	('Listaddress',['<{} address> [str]'.format(pnm),'minconf [int=1]','pager [bool=False]','showempty [bool=True]''showbtcaddr [bool=True]']),
+	('Listaddresses',["addrs [str='']",'minconf [int=1]','showempty [bool=False]','pager [bool=False]','showbtcaddrs [bool=False]']),
+	('Getbalance',   ['minconf [int=1]']),
+	('Txview',       ['<{} TX file(s)> [str]'.format(pnm),'pager [bool=False]','terse [bool=False]',"sort [str='mtime'] (options: 'ctime','atime')",'MARGS']),
+	('Twview',       ["sort [str='age']",'reverse [bool=False]','show_days [bool=True]','show_mmid [bool=True]','minconf [int=1]','wide [bool=False]','pager [bool=False]']),
+
+	('Add_label',       ['<{} address> [str]'.format(pnm),'<label> [str]']),
+	('Remove_label',    ['<{} address> [str]'.format(pnm)]),
+	('Addrfile_chksum', ['<{} addr file> [str]'.format(pnm)]),
+	('Keyaddrfile_chksum', ['<{} addr file> [str]'.format(pnm)]),
+	('Passwdfile_chksum', ['<{} password file> [str]'.format(pnm)]),
+	('Find_incog_data', ['<file or device name> [str]','<Incog ID> [str]','keep_searching [bool=False]']),
+
+	('Encrypt',      ['<infile> [str]',"outfile [str='']","hash_preset [str='']"]),
+	('Decrypt',      ['<infile> [str]',"outfile [str='']","hash_preset [str='']"]),
+	('Bytespec',     ['<bytespec> [str]']),
 ])
 
+stdin_msg = """
+To force a command to read from STDIN in place of its first argument (for
+supported commands), use '-' as the first argument.
+""".strip()
+
 cmd_help = """
-  Bitcoin address/key operations (compressed public keys supported):
-  addr2hexaddr - convert Bitcoin address from base58 to hex format
-  hex2wif      - convert a private key from hex to WIF format
-  hexaddr2addr - convert Bitcoin address from hex to base58 format
-  privhex2addr - generate Bitcoin address from private key in hex format
-  pubkey2addr  - convert Bitcoin public key to address
-  pubkey2hexaddr - convert Bitcoin public key to address in hex format
-  randpair     - generate a random private key/address pair
-  randwif      - generate a random private key in WIF format
-  wif2addr     - generate a Bitcoin address from a key in WIF format
-  wif2hex      - convert a private key from WIF to hex format
-
-  Wallet/TX operations (bitcoind must be running):
+Bitcoin address/key operations (compressed public keys supported):
+  addr2hexaddr   - convert Bitcoin address from base58 to hex format
+  hex2wif        - convert a private key from hex to WIF format
+  hexaddr2addr   - convert Bitcoin address from hex to base58 format
+  privhex2addr   - generate Bitcoin address from private key in hex format
+  privhex2pubhex - generate a hex public key from a hex private key
+  pubhex2addr    - convert a hex pubkey to an address
+  pubhex2redeem_script - convert a hex pubkey to a witness redeem script
+  wif2redeem_script - convert a WIF private key to a witness redeem script
+  wif2segwit_pair - generate both a Segwit redeem script and address from WIF
+  pubkey2addr    - convert Bitcoin public key to address
+  randpair       - generate a random private key/address pair
+  randwif        - generate a random private key in WIF format
+  wif2addr       - generate a Bitcoin address from a key in WIF format
+  wif2hex        - convert a private key from WIF to hex format
+
+Wallet/TX operations (bitcoind must be running):
   getbalance    - like 'bitcoin-cli getbalance' but shows confirmed/unconfirmed,
                   spendable/unspendable balances for individual {pnm} wallets
+  listaddress   - list the specified {pnm} address and its balance
   listaddresses - list {pnm} addresses and their balances
   txview        - show raw/signed {pnm} transaction in human-readable form
   twview        - view tracking wallet
 
-  General utilities:
+General utilities:
   hexdump      - encode data into formatted hexadecimal form (file or stdin)
   unhexdump    - decode formatted hexadecimal data (file or stdin)
   bytespec     - convert a byte specifier such as '1GB' into an integer
@@ -116,7 +132,8 @@ cmd_help = """
   hexreverse   - reverse bytes of a hexadecimal string
   rand2file    - write 'n' bytes of random data to specified file
   randhex      - print 'n' bytes (default 32) of random data in hex format
-  sha256x2     - compute a double sha256 hash of data
+  hash256      - compute sha256(sha256(data)) (double sha256)
+  hash160      - compute ripemd160(sha256(data)) (converts hexpubkey to hexaddr)
   b58randenc   - generate a random 32-byte number and convert it to base 58
   b58tostr     - convert a base 58 number to a string
   strtob58     - convert a string to base 58
@@ -125,7 +142,7 @@ cmd_help = """
   b32tohex     - convert a base 32 number to hexadecimal
   hextob32     - convert a hexadecimal number to base 32
 
-  File encryption:
+File encryption:
   encrypt      - encrypt a file
   decrypt      - decrypt a file
     {pnm} encryption suite:
@@ -133,7 +150,7 @@ cmd_help = """
       * Enc: AES256_CTR, 16-byte rand IV, sha256 hash + 32-byte nonce + data
       * The encrypted file is indistinguishable from random data
 
-  {pnm}-specific operations:
+{pnm}-specific operations:
   add_label    - add descriptive label for {pnm} address in tracking wallet
   remove_label - remove descriptive label for {pnm} address in tracking wallet
   addrfile_chksum    - compute checksum for {pnm} address file
@@ -144,7 +161,7 @@ cmd_help = """
   id8          - generate 8-character {pnm} ID for a file (or stdin)
   str2id6      - generate 6-character {pnm} ID for a string, ignoring spaces
 
-  Mnemonic operations (choose 'electrum' (default), 'tirosh' or 'all'
+Mnemonic operations (choose 'electrum' (default), 'tirosh' or 'all'
   wordlists):
   mn_rand128   - generate random 128-bit mnemonic
   mn_rand192   - generate random 192-bit mnemonic
@@ -156,63 +173,97 @@ cmd_help = """
 
   IMPORTANT NOTE: Though {pnm} mnemonics use the Electrum wordlist, they're
   computed using a different algorithm and are NOT Electrum-compatible!
-""".format(pnm=pnm)
 
-def tool_usage(prog_name, command):
-	if command in cmd_data:
+  {sm}
+""".format(pnm=pnm,sm='\n  '.join(stdin_msg.split('\n')))
+
+def usage(command):
+
+	for v in cmd_data.values():
+		if v and v[0][-2:] == '-]':
+			v[0] = v[0][:-2] + ' or STDIN]'
+		if 'MARGS' in v: v.remove('MARGS')
+
+	if not command:
+		Msg('Usage information for mmgen-tool commands:')
+		for k,v in cmd_data.items():
+			Msg('  {:18} {}'.format(k.lower(),' '.join(v)))
+		Msg('\n  '+'\n  '.join(stdin_msg.split('\n')))
+		sys.exit(0)
+
+	Command = command.capitalize()
+	if Command in cmd_data:
+		import re
 		for line in cmd_help.split('\n'):
-			if '  ' + command in line:
+			if re.match(r'\s+{}\s+'.format(command),line):
 				c,h = line.split('-',1)
 				Msg('MMGEN-TOOL {}: {}'.format(c.strip().upper(),h.strip()))
-		cd = cmd_data[command]
-		if cd and cd[0][-2:] == '-]':
-			cd[0] = cd[0][:-2] + ' or STDIN]'
-		msg('USAGE: %s %s %s' % (prog_name, command, ' '.join(cd)))
+		cd = cmd_data[Command]
+		msg('USAGE: %s %s %s' % (g.prog_name, command, ' '.join(cd)))
 	else:
 		msg("'%s': no such tool command" % command)
 	sys.exit(1)
 
-def process_args(prog_name, command, cmd_args):
+Help = usage
+
+def process_args(command,cmd_args):
+	if 'MARGS' in cmd_data[command]:
+		cmd_data[command].remove('MARGS')
+		margs = True
+	else:
+		margs = False
+
 	c_args = [[i.split(' [')[0],i.split(' [')[1][:-1]]
 		for i in cmd_data[command] if '=' not in i]
 	c_kwargs = dict([[
 			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   = [a for a in cmd_args[:len(c_args)]]
 
-	if c_args and c_args[0][1][-1] == '-':
-		c_args[0][1] = c_args[0][1][:-1] # [str-] -> [str]
-		# If we're reading from a pipe, make the input the first argument
-		if len(u_args) < len(c_kwargs) + len(c_args):
-			if not sys.stdin.isatty():
-				u_args = [sys.stdin.read()] + u_args
+	if not margs:
+		u_args = [a for a in cmd_args[:len(c_args)]]
+
+		if c_args and c_args[0][1][-1] == '-':
+			c_args[0][1] = c_args[0][1][:-1] # [str-] -> [str]
+			# If we're reading from a pipe, replace '-' with output of previous command
+			if u_args and u_args[0] == '-':
+				if not sys.stdin.isatty():
+					u_args[0] = sys.stdin.read().strip()
+					if not u_args[0]:
+						die(2,'{}: ERROR: no output from previous command in pipe'.format(command.lower()))
 
-	if len(u_args) < len(c_args):
-		m1 = 'Command requires exactly %s non-keyword argument%s'
-		msg(m1 % (len(c_args),suf(c_args,'k')))
-		tool_usage(prog_name,command)
+		if not margs and len(u_args) < len(c_args):
+			m1 = 'Command requires exactly %s non-keyword argument%s'
+			msg(m1 % (len(c_args),suf(c_args,'s')))
+			usage(command)
 
-#	print u_args
 	extra_args = len(cmd_args) - len(c_args)
 	u_kwargs = {}
-	if extra_args > 0:
+	if margs:
+		t = [a.split('=') for a in cmd_args if '=' in a]
+		tk = [a[0] for a in t]
+		tk_bad = [a for a in tk if a not in c_kwargs]
+		if set(tk_bad) != set(tk[:len(tk_bad)]):
+			die(1,"'{}': illegal keyword argument".format(tk_bad[-1]))
+		u_kwargs = dict(t[len(tk_bad):])
+		u_args = cmd_args[:-len(u_kwargs) or None]
+	elif 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)
+				% (len(c_args),suf(c_args,'s')))
+			usage(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)
+				% (len(c_kwargs),suf(c_kwargs,'s')))
+			usage(command)
 
 #	mdie(c_args,c_kwargs,u_args,u_kwargs)
 
 	for k in u_kwargs:
 		if k not in c_kwargs:
 			msg("'%s': invalid keyword argument" % k)
-			tool_usage(prog_name,command)
+			usage(command)
 
 	def conv_type(arg,arg_name,arg_type):
 		if arg_type == 'str': arg_type = 'unicode'
@@ -221,17 +272,19 @@ def process_args(prog_name, command, cmd_args):
 			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)
+				usage(command)
 		try:
 			return __builtins__[arg_type](arg)
 		except:
 			die(1,"'%s': Invalid argument for argument %s ('%s' required)" % \
 				(arg, arg_name, arg_type))
 
-	args = [conv_type(u_args[i],c_args[i][0],c_args[i][1]) for i in range(len(c_args))]
+	if margs:
+		args = [conv_type(u_args[i],c_args[0][0],c_args[0][1]) for i in range(len(u_args))]
+	else:
+		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
@@ -252,53 +305,86 @@ def print_convert_results(indata,enc,dec,dtype):
 	if error:
 		die(3,"Error! Recoded data doesn't match input!")
 
-def usage(cmd):
-	tool_usage(g.prog_name, cmd)
-
-help = usage
-
-def hexdump(infile, cols=8, line_nums=True):
+def Hexdump(infile, cols=8, line_nums=True):
 	Msg(pretty_hexdump(
 			get_data_from_file(infile,dash=True,silent=True,binary=True),
 				cols=cols,line_nums=line_nums))
 
-def unhexdump(infile):
+def Unhexdump(infile):
 	if g.platform == 'win':
 		import msvcrt
 		msvcrt.setmode(sys.stdout.fileno(),os.O_BINARY)
 	sys.stdout.write(decode_pretty_hexdump(
 			get_data_from_file(infile,dash=True,silent=True)))
 
-def b58randenc():
+def B58randenc():
 	r = get_random(32)
 	enc = mmb.b58encode(r)
 	dec = mmb.b58decode(enc)
 	print_convert_results(r,enc,dec,'str')
 
-def randhex(nbytes='32'):
+def Randhex(nbytes='32'):
 	Msg(ba.hexlify(get_random(int(nbytes))))
 
-def randwif(compressed=False):
+def Randwif(compressed=False):
 	r_hex = ba.hexlify(get_random(32))
 	enc = mmb.hex2wif(r_hex,compressed)
-	dec = mmb.wif2hex(enc)
+	dec = wif2hex(enc)
 	print_convert_results(r_hex,enc,dec,'hex')
 
-def randpair(compressed=False):
+def Randpair(compressed=False,segwit=False):
+	if segwit: compressed = True
 	r_hex = ba.hexlify(get_random(32))
 	wif = mmb.hex2wif(r_hex,compressed)
-	addr = mmb.privnum2addr(int(r_hex,16),compressed)
+	addr = mmb.privnum2addr(int(r_hex,16),compressed,segwit=segwit)
 	Vmsg('Key (hex):  %s' % r_hex)
 	Vmsg_r('Key (WIF):  '); Msg(wif)
 	Vmsg_r('Addr:       '); Msg(addr)
 
-def wif2addr(wif,compressed=False):
-	s_enc = mmb.wif2hex(wif)
-	if s_enc == False:
-		die(1,'Invalid address')
-	addr = mmb.privnum2addr(int(s_enc,16),compressed)
+def Wif2addr(wif,segwit=False):
+	compressed = mmb.wif_is_compressed(wif)
+	if segwit and not compressed:
+		die(1,'Segwit address cannot be generated from uncompressed WIF')
+	privhex = wif2hex(wif)
+	addr = mmb.privnum2addr(int(privhex,16),compressed,segwit=segwit)
 	Vmsg_r('Addr: '); Msg(addr)
 
+def Wif2segwit_pair(wif):
+	if not mmb.wif_is_compressed(wif):
+		die(1,'Segwit address cannot be generated from uncompressed WIF')
+	privhex = wif2hex(wif)
+	pubhex = mmb.privnum2pubhex(int(privhex,16),compressed=True)
+	rs = mmb.pubhex2redeem_script(pubhex)
+	addr = mmb.hexaddr2addr(mmb.hash160(rs),p2sh=True)
+	addr_chk = mmb.privnum2addr(int(privhex,16),compressed=True,segwit=True)
+	assert addr == addr_chk
+	Msg('{}\n{}'.format(rs,addr))
+
+def Hexaddr2addr(hexaddr):                     Msg(mmb.hexaddr2addr(hexaddr))
+def Addr2hexaddr(addr):                        Msg(mmb.verify_addr(addr,return_hex=True))
+def Hash160(pubkeyhex):                        Msg(mmb.hash160(pubkeyhex))
+def Pubhex2addr(pubkeyhex,p2sh=False):         Msg(mmb.hexaddr2addr(mmb.hash160(pubkeyhex),p2sh=p2sh))
+def Wif2hex(wif):                              Msg(wif2hex(wif))
+def Hex2wif(hexpriv,compressed=False):
+	Msg(mmb.hex2wif(hexpriv,compressed))
+def Privhex2addr(privhex,compressed=False,segwit=False):
+	if segwit and not compressed:
+		die(1,'Segwit address can be generated only from a compressed pubkey')
+	Msg(mmb.privnum2addr(int(privhex,16),compressed,segwit=segwit))
+def Privhex2pubhex(privhex,compressed=False): # new
+	Msg(mmb.privnum2pubhex(int(privhex,16),compressed))
+def Pubhex2redeem_script(pubhex): # new
+	Msg(mmb.pubhex2redeem_script(pubhex))
+def Wif2redeem_script(wif): # new
+	if not mmb.wif_is_compressed(wif):
+		die(1,'Witness redeem script cannot be generated from uncompressed WIF')
+	pubhex = mmb.privnum2pubhex(int(wif2hex(wif),16),compressed=True)
+	Msg(mmb.pubhex2redeem_script(pubhex))
+
+def wif2hex(wif): # wrapper
+	ret = mmb.wif2hex(wif)
+	return ret or die(1,'{}: Invalid WIF'.format(wif))
+
 wordlists = 'electrum','tirosh'
 dfl_wl_id = 'electrum'
 
@@ -311,123 +397,157 @@ def do_random_mn(nbytes,wordlist):
 		mn = baseconv.fromhex(hexrand,wl_id)
 		Msg(' '.join(mn))
 
-def mn_rand128(wordlist=dfl_wl_id): do_random_mn(16,wordlist)
-def mn_rand192(wordlist=dfl_wl_id): do_random_mn(24,wordlist)
-def mn_rand256(wordlist=dfl_wl_id): do_random_mn(32,wordlist)
+def Mn_rand128(wordlist=dfl_wl_id): do_random_mn(16,wordlist)
+def Mn_rand192(wordlist=dfl_wl_id): do_random_mn(24,wordlist)
+def Mn_rand256(wordlist=dfl_wl_id): do_random_mn(32,wordlist)
 
-def hex2mn(s,wordlist=dfl_wl_id): Msg(' '.join(baseconv.fromhex(s,wordlist)))
-def mn2hex(s,wordlist=dfl_wl_id): Msg(baseconv.tohex(s.split(),wordlist))
+def Hex2mn(s,wordlist=dfl_wl_id): Msg(' '.join(baseconv.fromhex(s,wordlist)))
+def Mn2hex(s,wordlist=dfl_wl_id): Msg(baseconv.tohex(s.split(),wordlist))
 
-def strtob58(s,pad=None): Msg(''.join(baseconv.fromhex(ba.hexlify(s),'b58',pad)))
-def hextob58(s,pad=None): Msg(''.join(baseconv.fromhex(s,'b58',pad)))
-def hextob32(s,pad=None): Msg(''.join(baseconv.fromhex(s,'b32',pad)))
-def b58tostr(s):          Msg(ba.unhexlify(baseconv.tohex(s,'b58')))
-def b58tohex(s,pad=None): Msg(baseconv.tohex(s,'b58',pad))
-def b32tohex(s,pad=None): Msg(baseconv.tohex(s.upper(),'b32',pad))
+def Strtob58(s,pad=None): Msg(''.join(baseconv.fromhex(ba.hexlify(s),'b58',pad)))
+def Hextob58(s,pad=None): Msg(''.join(baseconv.fromhex(s,'b58',pad)))
+def Hextob32(s,pad=None): Msg(''.join(baseconv.fromhex(s,'b32',pad)))
+def B58tostr(s):          Msg(ba.unhexlify(baseconv.tohex(s,'b58')))
+def B58tohex(s,pad=None): Msg(baseconv.tohex(s,'b58',pad))
+def B32tohex(s,pad=None): Msg(baseconv.tohex(s.upper(),'b32',pad))
 
 from mmgen.seed import Mnemonic
-def mn_stats(wordlist=dfl_wl_id):
+def Mn_stats(wordlist=dfl_wl_id):
 	wordlist in baseconv.digits or die(1,"'{}': not a valid wordlist".format(wordlist))
 	baseconv.check_wordlist(wordlist)
-def mn_printlist(wordlist=dfl_wl_id):
+def Mn_printlist(wordlist=dfl_wl_id):
 	wordlist in baseconv.digits or die(1,"'{}': not a valid wordlist".format(wordlist))
 	Msg('\n'.join(baseconv.digits[wordlist]))
 
-def id8(infile):
+def Id8(infile):
 	Msg(make_chksum_8(
 		get_data_from_file(infile,dash=True,silent=True,binary=True)
 	))
-def id6(infile):
+def Id6(infile):
 	Msg(make_chksum_6(
 		get_data_from_file(infile,dash=True,silent=True,binary=True)
 	))
-def str2id6(s): # retain ignoring of space for backwards compat
+def Str2id6(s): # retain ignoring of space for backwards compat
 	Msg(make_chksum_6(''.join(s.split())))
 
-# List MMGen addresses and their balances:
-def listaddresses(addrs='',minconf=1,showempty=False,pager=False,showbtcaddrs=False):
+def Listaddress(addr,minconf=1,pager=False,showempty=True,showbtcaddr=True):
+	return Listaddresses(addrs=addr,minconf=minconf,pager=pager,showempty=showempty,showbtcaddrs=showbtcaddr)
+
+# List MMGen addresses and their balances.  TODO: move this code to AddrList
+def Listaddresses(addrs='',minconf=1,showempty=False,pager=False,showbtcaddrs=False):
+
+	c = bitcoin_connection()
+
+	def check_dup_mmid(accts):
+		help_msg = """
+    Your tracking wallet is corrupted or has been altered by a non-{pnm} program.
+
+    You might be able to salvage your wallet by determining which of the offending
+    addresses doesn't belong to {pnm} ID {mid} and then typing:
+
+        bitcoin-cli importaddress <offending address> "" false
+	"""
+		m_prev = None
+
+		for m in sorted([l.mmid for l in accts]):
+			if m == m_prev:
+				msg('Duplicate MMGen ID ({}) discovered in tracking wallet!\n'.format(m))
+				bad_accts = MMGenList([l for l in accts if l.mmid == m])
+				msg('  Affected Bitcoin RPC accounts:\n    {}\n'.format('\n    '.join(bad_accts)))
+				bad_addrs = [a[0] for a in c.getaddressesbyaccount([[a] for a in bad_accts],batch=True)]
+				if len(set(bad_addrs)) != 1:
+					msg('  Offending addresses:\n    {}'.format('\n    '.join(bad_addrs)))
+					msg(help_msg.format(mid=m,pnm=pnm))
+				die(3,red('Exiting on error'))
+			m_prev = m
 
-	# TODO - move some or all of this code to AddrList
 	usr_addr_list = []
 	if addrs:
-		sid_s,idxs = split2(addrs,':')
-		sid = SeedID(sid=sid_s)
-		usr_addr_list = ['{}:{}'.format(sid,a) for a in AddrIdxList(idxs)]
+		a = addrs.rsplit(':',1)
+		if len(a) != 2:
+			m = "'{}': invalid address list argument (must be in form <seed ID>:[<type>:]<idx list>)"
+			die(1,m.format(addrs))
+		usr_addr_list = [MMGenID('{}:{}'.format(a[0],i)) for i in AddrIdxList(a[1])]
 
-	c = bitcoin_connection()
-	addrs = {} # reusing variable name!
+	class TwAddrList(dict,MMGenObject): pass
+
+	addrs = TwAddrList() # reusing name!
 	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 (mmaddr[:4] == 'btc:' or is_mmgen_id(mmaddr)) and d['confirmations'] >= minconf:
-			key = mmaddr.replace(':','_')
-			if key in addrs:
-				if addrs[key][2] != d['address']:
+		if not 'account' in d: continue  # skip coinbase outputs with missing account
+		if d['confirmations'] < minconf: continue
+		label = TwLabel(d['account'],on_fail='silent')
+		if label:
+			if usr_addr_list and (label.mmid not in usr_addr_list): continue
+			if label.mmid in addrs:
+				if addrs[label.mmid]['addr'] != d['address']:
 					die(2,'duplicate BTC address ({}) for this MMGen address! ({})'.format(
-							(d['address'], addrs[key][2])))
+							(d['address'], addrs[label.mmid]['addr'])))
 			else:
-				addrs[key] = [BTCAmt('0'),MMGenAddrLabel(comment),BTCAddr(d['address'])]
-			addrs[key][0] += d['amount']
+				addrs[label.mmid] = { 'amt':BTCAmt('0'), 'lbl':label, 'addr':BTCAddr(d['address']) }
+			addrs[label.mmid]['amt'] += d['amount']
 			total += d['amount']
 
 	# We use listaccounts only for empty addresses, as it shows false positive balances
 	if showempty:
-		accts = c.listaccounts(0,True) # minconf,watchonly
-		save_a = []
-		for acct in accts:
-			mmaddr,comment = split2(acct)
-			if usr_addr_list and (mmaddr not in usr_addr_list): continue
-			if mmaddr[:4] == 'btc:' or is_mmgen_id(mmaddr):
-				key = mmaddr.replace(':','_')
-				if key not in addrs:
-					if showbtcaddrs: save_a.append([acct])
-					addrs[key] = [BTCAmt('0'),MMGenAddrLabel(comment),'']
-
-		for acct,addr in zip(save_a,c.getaddressesbyaccount(save_a,batch=True)):
-			if len(addr) != 1:
-				die(2,"Account '%s' has more or less than one BTC address!" % addr)
-			key = split2(acct[0])[0].replace(':','_')
-			addrs[key][2] = BTCAddr(addr[0])
+		# args: minconf,watchonly
+		accts = MMGenList([b for b in [TwLabel(a,on_fail='silent') for a in c.listaccounts(0,True)] if b])
+		check_dup_mmid(accts)
+		acct_addrs = c.getaddressesbyaccount([[a] for a in accts],batch=True)
+		assert len(accts) == len(acct_addrs), 'listaccounts() and getaddressesbyaccount() not of same length'
+		for a in acct_addrs:
+			if len(a) != 1:
+				die(2,"'{}': more than one BTC address in account!".format(a))
+		for label,addr in zip(accts,[b[0] for b in acct_addrs]):
+			if usr_addr_list and (label.mmid not in usr_addr_list): continue
+			if label.mmid not in addrs:
+				addrs[label.mmid] = { 'amt':BTCAmt('0'), 'lbl':label, 'addr':'' }
+				if showbtcaddrs:
+					addrs[label.mmid]['addr'] = BTCAddr(addr)
 
 	if not addrs:
-		die(0,('No addresses with balances!','No tracked addresses!')[showempty])
+		die(0,('No tracked addresses with balances!','No tracked addresses!')[showempty])
 
-	fs = ('{mid} {lbl} {amt}','{mid} {addr} {lbl} {amt}')[showbtcaddrs]
-	max_mmid_len = max([len(k) for k in addrs if k[:4] != 'btc_'] or [10])
-	max_lbl_len =  max(len(addrs[k][1]) for k in addrs) or 7
-	out = [fs.format(
+	out = ([],[green('Chain: {}'.format(g.chain.upper()))])[g.chain in ('testnet','regtest')]
+
+	fs = ('{mid} {cmt} {amt}','{mid} {addr} {cmt} {amt}')[showbtcaddrs]
+	mmaddrs = [k for k in addrs.keys() if k.type == 'mmgen']
+	max_mmid_len = max(len(k) for k in mmaddrs) + 2 if mmaddrs else 10
+	max_cmt_len =  max(max(len(addrs[k]['lbl'].comment) for k in addrs),7)
+	out += [fs.format(
 			mid=MMGenID.fmtc('MMGenID',width=max_mmid_len),
 			addr=BTCAddr.fmtc('ADDRESS'),
-			lbl=MMGenAddrLabel.fmtc('COMMENT',width=max_lbl_len),
+			cmt=TwComment.fmtc('COMMENT',width=max_cmt_len),
 			amt='BALANCE'
 			)]
 
-	old_sid = ''
-	def s_mmgen(k):
-		s = k.split('_')
-		a,b = s if len(s) == 2 else (k,'')
-		return '{}_{:>0{w}}'.format(a,b,w=AddrIdx.max_digits+9)
-	for k in sorted(addrs,key=s_mmgen):
-		if old_sid and old_sid != k.split('_')[0]: out.append('')
-		old_sid = k.split('_')[0]
-		m = 'non-'+g.proj_name if k[:4] == 'btc_' else k.replace('_',':')
+	al_id_save = None
+	for mmid in sorted(addrs,key=lambda j: j.sort_key):
+		if mmid.type == 'mmgen':
+			if al_id_save and al_id_save != mmid.obj.al_id:
+				out.append('')
+			al_id_save = mmid.obj.al_id
+			mmid_disp = mmid
+		else:
+			if al_id_save:
+				out.append('')
+				al_id_save = None
+			mmid_disp = mmid.type
 		out.append(fs.format(
-			mid = MMGenID.fmtc(m,width=max_mmid_len,color=True),
-			addr=(addrs[k][2].fmt(color=True) if showbtcaddrs else None),
-			lbl=addrs[k][1].fmt(width=max_lbl_len,color=True,nullrepl='-'),
-			amt=addrs[k][0].fmt('3.0',color=True)))
+			mid = MMGenID.fmtc(mmid_disp,width=max_mmid_len,color=True),
+			addr=(addrs[mmid]['addr'].fmt(color=True) if showbtcaddrs else None),
+			cmt=addrs[mmid]['lbl'].comment.fmt(width=max_cmt_len,color=True,nullrepl='-'),
+			amt=addrs[mmid]['amt'].fmt('3.0',color=True)))
 
 	out.append('\nTOTAL: %s BTC' % total.hl(color=True))
 	o = '\n'.join(out)
-	if pager: do_pager(o)
-	else: Msg(o)
-
-def getbalance(minconf=1):
+	return do_pager(o) if pager else Msg(o)
 
+def Getbalance(minconf=1):
 	accts = {}
 	for d in bitcoin_connection().listunspent(0):
-		ma = split2(d['account'])[0]
+		ma = split2(d['account'] if 'account' in d else '')[0] # include coinbase outputs if spendable
 		keys = ['TOTAL']
 		if d['spendable']: keys += ['SPENDABLE']
 		if is_mmgen_id(ma): keys += [ma.split(':')[0]]
@@ -445,77 +565,62 @@ def getbalance(minconf=1):
 		*[s.ljust(16) for s in ' Unconfirmed',' <%s %s'%(mc,lbl),' >=%s %s'%(mc,lbl)]))
 	for key in sorted(accts.keys()):
 		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()
-	tx = MMGenTX(infile)
-	tx.view(pager,pause=False,terse=terse)
-
-def twview(pager=False,reverse=False,wide=False,minconf=1,sort='age'):
+	if 'SPENDABLE' in accts:
+		Msg(red('Warning: this wallet contains PRIVATE KEYS for the SPENDABLE balance!'))
+
+def Txview(*infiles,**kwargs):
+	from mmgen.filename import MMGenFileList
+	pager = 'pager' in kwargs and kwargs['pager']
+	terse = 'terse' in kwargs and kwargs['terse']
+	sort_key = kwargs['sort'] if 'sort' in kwargs else 'mtime'
+	flist = MMGenFileList(infiles,ftype=MMGenTX)
+	flist.sort_by_age(key=sort_key) # in-place sort
+	from mmgen.term import get_terminal_size
+	sep = u'—'*get_terminal_size()[0]+'\n'
+	out = sep.join([MMGenTX(fn).format_view(terse=terse) for fn in flist.names()])
+	(Msg,do_pager)[pager](out.rstrip())
+
+def Twview(pager=False,reverse=False,wide=False,minconf=1,sort='age',show_days=True,show_mmid=True):
 	from mmgen.tw import MMGenTrackingWallet
 	tw = MMGenTrackingWallet(minconf=minconf)
 	tw.do_sort(sort,reverse=reverse)
+	tw.show_days = show_days
+	tw.show_mmid = show_mmid
 	out = tw.format_for_printing(color=True) if wide else tw.format_for_display()
-	do_pager(out) if pager else sys.stdout.write(out)
+	(Msg_r,do_pager)[pager](out)
 
-def add_label(mmaddr,label):
+def Add_label(mmaddr,label):
 	from mmgen.tw import MMGenTrackingWallet
-	if MMGenTrackingWallet.add_label(mmaddr,label): # returns on failure
-		s = '{pnm} address {a} in tracking wallet'.format(a=mmaddr,pnm=pnm)
-		if label: msg("Added label '{}' for {}".format(label,s))
-		else:     msg('Removed label for {}'.format(s))
-	else:
-		die(1,'Label could not be %s' % ('removed','added')[bool(label)])
+	MMGenTrackingWallet.add_label(mmaddr,label) # dies on failure
 
-def remove_label(mmaddr): add_label(mmaddr,'')
+def Remove_label(mmaddr): Add_label(mmaddr,'')
 
-def addrfile_chksum(infile):
+def Addrfile_chksum(infile):
 	from mmgen.addr import AddrList
 	AddrList(infile,chksum_only=True)
 
-def keyaddrfile_chksum(infile):
+def Keyaddrfile_chksum(infile):
 	from mmgen.addr import KeyAddrList
 	KeyAddrList(infile,chksum_only=True)
 
-def passwdfile_chksum(infile):
+def Passwdfile_chksum(infile):
 	from mmgen.addr import PasswordList
 	PasswordList(infile=infile,chksum_only=True)
 
-def hexreverse(s):
+def Hexreverse(s):
 	Msg(ba.hexlify(ba.unhexlify(s.strip())[::-1]))
 
-def hexlify(s):
+def Hexlify(s):
 	Msg(ba.hexlify(s))
 
-def sha256x2(s, file_input=False, hex_input=False):
+def Hash256(s, file_input=False, hex_input=False):
 	from hashlib import sha256
 	if file_input:  b = get_data_from_file(s,binary=True)
 	elif hex_input: b = decode_pretty_hexdump(s)
 	else:           b = s
 	Msg(sha256(sha256(b).digest()).hexdigest())
 
-def hexaddr2addr(hexaddr):
-	Msg(mmb.hexaddr2addr(hexaddr))
-
-def addr2hexaddr(addr):
-	Msg(mmb.verify_addr(addr,return_hex=True))
-
-def pubkey2hexaddr(pubkeyhex):
-	Msg(mmb.pubhex2hexaddr(pubkeyhex))
-
-def pubkey2addr(pubkeyhex):
-	Msg(mmb.hexaddr2addr(mmb.pubhex2hexaddr(pubkeyhex)))
-
-def privhex2addr(privkeyhex,compressed=False):
-	Msg(mmb.privnum2addr(int(privkeyhex,16),compressed))
-
-def wif2hex(wif,compressed=False):
-	Msg(mmb.wif2hex(wif))
-
-def hex2wif(hexpriv,compressed=False):
-	Msg(mmb.hex2wif(hexpriv,compressed))
-
-def encrypt(infile,outfile='',hash_preset=''):
+def Encrypt(infile,outfile='',hash_preset=''):
 	data = get_data_from_file(infile,'data for encryption',binary=True)
 	enc_d = mmgen_encrypt(data,'user data',hash_preset)
 	if not outfile:
@@ -523,7 +628,7 @@ def encrypt(infile,outfile='',hash_preset=''):
 
 	write_data_to_file(outfile,enc_d,'encrypted data',binary=True)
 
-def decrypt(infile,outfile='',hash_preset=''):
+def Decrypt(infile,outfile='',hash_preset=''):
 	enc_d = get_data_from_file(infile,'encrypted data',binary=True)
 	while True:
 		dec_d = mmgen_decrypt(enc_d,'user data',hash_preset)
@@ -537,7 +642,7 @@ def decrypt(infile,outfile='',hash_preset=''):
 
 	write_data_to_file(outfile,dec_d,'decrypted data',binary=True)
 
-def find_incog_data(filename,iv_id,keep_searching=False):
+def Find_incog_data(filename,iv_id,keep_searching=False):
 	ivsize,bsize,mod = g.aesctr_iv_len,4096,4096*8
 	n,carry = 0,' '*ivsize
 	flgs = os.O_RDONLY|os.O_BINARY if g.platform == 'win' else os.O_RDONLY
@@ -554,7 +659,7 @@ def find_incog_data(filename,iv_id,keep_searching=False):
 				if n+i < ivsize: continue
 				msg('\rIncog data for ID %s found at offset %s' %
 					(iv_id,n+i-ivsize))
-				if not keep_searching: sys.exit()
+				if not keep_searching: sys.exit(0)
 		carry = d[len(d)-ivsize:]
 		n += bsize
 		if not n % mod: msg_r('\rSearched: %s bytes' % n)
@@ -562,7 +667,7 @@ def find_incog_data(filename,iv_id,keep_searching=False):
 	msg('')
 	os.close(f)
 
-def rand2file(outfile, nbytes, threads=4, silent=False):
+def Rand2file(outfile, nbytes, threads=4, silent=False):
 	nbytes = parse_nbytes(nbytes)
 	from Crypto import Random
 	rh = Random.new()
@@ -620,4 +725,4 @@ def rand2file(outfile, nbytes, threads=4, silent=False):
 	q2.join()
 	f.close()
 
-def bytespec(s): Msg(str(parse_nbytes(s)))
+def Bytespec(s): Msg(str(parse_nbytes(s)))

+ 142 - 96
mmgen/tw.py

@@ -34,22 +34,25 @@ def parse_tw_acct_label(s):
 		a2 = ret[1] if len(ret) == 2 else None
 	return a1,a2
 
-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):
+
+	class MMGenTwOutputList(list,MMGenObject): pass
+
+	class MMGenTwOutput(MMGenListItem):
+		twmmid = MMGenListItemAttr('twmmid','TwMMGenID')
+		txid   = MMGenListItemAttr('txid','BitcoinTxID')
+		attrs_reassign = 'label','skip'
+		attrs = 'txid','vout','amt','label','twmmid','addr','confs','scriptPubKey','days','skip'
+
 	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)
+""".strip().format(g.proj_name.lower())
 	}
-	sort_keys = 'addr','age','amt','txid','mmid'
 
 	def __init__(self,minconf=1):
-		self.unspent      = []
+		self.unspent      = self.MMGenTwOutputList()
 		self.fmt_display  = ''
 		self.fmt_print    = ''
 		self.cols         = None
@@ -58,57 +61,62 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
 		self.show_days    = True
 		self.show_mmid    = True
 		self.minconf      = minconf
-		self.get_data()
+		self.get_unspent_data()
 		self.sort_key     = 'age'
 		self.do_sort()
 		self.total        = self.get_total_btc()
 
 	def get_total_btc(self):
-		return sum([i.amt for i in self.unspent])
+		return sum(i.amt for i in self.unspent)
 
-	def get_data(self):
+	def get_unspent_data(self):
 		if g.bogus_wallet_data: # for debugging purposes only
 			us_rpc = eval(get_data_from_file(g.bogus_wallet_data))
 		else:
 			us_rpc = bitcoin_connection().listunspent(self.minconf)
 #		write_data_to_file('bogus_unspent.json', repr(us), 'bogus unspent data')
-#		sys.exit()
+#		sys.exit(0)
 
 		if not us_rpc: die(0,self.wmsg['no_spendable_outputs'])
+		mm_rpc = self.MMGenTwOutputList()
 		for o in us_rpc:
-			o['mmid'],o['label'] = parse_tw_acct_label(o['account']) if 'account' in o else ('','')
-			o['days'] = int(o['confirmations'] * g.mins_per_block / (60*24))
-			o['amt'] = o['amount'] # TODO
-			o['addr'] = o['address']
-			o['confs'] = o['confirmations']
-		self.unspent = [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([pp_format(i)+'\n' for i in us_rpc]))
-#		die(1,''.join([str(i)+'\n' for i in self.unspent]))
-
-	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=AddrIdx.max_digits)
-		else: return 'G' + (i.label or '')
-
-	def do_sort(self,key=None,reverse=None):
-		if not key: key = self.sort_key
-		assert key
+			if not 'account' in o: continue          # coinbase outputs have no account field
+			l = TwLabel(o['account'],on_fail='silent')
+			if l:
+				o.update({
+					'twmmid': l.mmid,
+					'label':  l.comment,
+					'days':   int(o['confirmations'] * g.mins_per_block / (60*24)),
+					'amt':    o['amount'], # TODO
+					'addr':   o['address'],
+					'confs':  o['confirmations']
+				})
+				mm_rpc.append(o)
+		self.unspent = self.MMGenTwOutputList([self.MMGenTwOutput(**dict([(k,v) for k,v in o.items() if k in self.MMGenTwOutput.attrs])) for o in mm_rpc])
+		for u in self.unspent:
+			if u.label == None: u.label = ''
+		if not self.unspent:
+			die(1,'No tracked unspent outputs in tracking wallet!')
+
+	def do_sort(self,key=None,reverse=False):
+		sort_funcs = {
+			'addr':  lambda i: i.addr,
+			'age':   lambda i: 0 - i.confs,
+			'amt':   lambda i: i.amt,
+			'txid':  lambda i: '%s %03s' % (i.txid,i.vout),
+			'mmid':  lambda i: i.twmmid.sort_key
+		}
+		key = key or self.sort_key
+		if key not in sort_funcs:
+			die(1,"'{}': invalid sort key.  Valid options: {}".format(key,' '.join(sort_funcs.keys())))
 		self.sort_key = key
-		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.unspent.sort(key=getattr(self,'s_'+key),reverse=reverse)
+		assert type(reverse) == bool
+		self.unspent.sort(key=sort_funcs[key],reverse=reverse or self.reverse)
 
 	def sort_info(self,include_group=True):
 		ret = ([],['Reverse'])[self.reverse]
-		ret.append(capfirst(self.sort_key).replace('Mmid','MMGenID'))
-		if include_group and self.group and (self.sort_key in ('addr','txid','mmid')):
+		ret.append(capfirst(self.sort_key).replace('Twmmid','MMGenID'))
+		if include_group and self.group and (self.sort_key in ('addr','txid','twmmid')):
 			ret.append('Grouped')
 		return ret
 
@@ -126,42 +134,42 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
 		msg(self.format_for_display())
 
 	def format_for_display(self):
-		unsp = [MMGenTWOutput(**i.__dict__) for i in self.unspent]
+		unsp = self.unspent
+# 		unsp.pdie()
 		self.set_term_columns()
 
-		for i in unsp:
-			if i.label == None: i.label = ''
-			i.skip = ''
-
-		mmid_w = max(len(i.mmid or '') for i in unsp) or 10
-		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)))
+		# Field widths
+		min_mmid_w = 12 # DEADBEEF:S:1
+		mmid_w = max(len(('',i.twmmid)[i.twmmid.type=='mmgen']) for i in unsp) or min_mmid_w
+		max_acct_w = max(len(i.label) for i in unsp) + mmid_w + 1
+		addr_w = min(35+(0,1+max_acct_w)[self.show_mmid],self.cols-45)
+		acct_w = min(max_acct_w, 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 %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_key in ('addr','txid','mmid')):
+		fs = ' %-4s %-{}s %-2s %s %s %s'.format(tx_w)
+
+		for i in unsp: i.skip = None
+		if self.group and (self.sort_key in ('addr','txid','twmmid')):
 			for a,b in [(unsp[i],unsp[i+1]) for i in range(len(unsp)-1)]:
-				for k in ('addr','txid','mmid'):
+				for k in ('addr','txid','twmmid'):
 					if self.sort_key == k and getattr(a,k) == getattr(b,k):
-						b.skip = (k,'addr')[k=='mmid']
+						b.skip = (k,'addr')[k=='twmmid']
 
 		hdr_fmt   = 'UNSPENT OUTPUTS (sort order: %s)  Total BTC: %s'
-		out  = [hdr_fmt % (' '.join(self.sort_info()), self.total.hl()), table_hdr]
+		out  = [hdr_fmt % (' '.join(self.sort_info()), self.total.hl())]
+		if g.chain in ('testnet','regtest'):
+			out += [green('Chain: {}'.format(g.chain.upper()))]
+		af = BTCAddr.fmtc('Address',width=addr_w+1)
+		cf = ('Conf.','Age(d)')[self.show_days]
+		out += [fs % ('Num','TX id'.ljust(tx_w - 5) + ' Vout','',af,'Amt(BTC) ',cf)]
 
 		for n,i in enumerate(unsp):
 			addr_dots = '|' + '.'*33
 			mmid_disp = MMGenID.fmtc('.'*mmid_w if i.skip=='addr'
-				else i.mmid or 'Non-{}'.format(g.proj_name),width=mmid_w,color=True)
+				else i.twmmid if i.twmmid.type=='mmgen'
+					else 'Non-{}'.format(g.proj_name),width=mmid_w,color=True)
 			if self.show_mmid:
 				addr_out = '%s %s' % (
 					type(i.addr).fmtc(addr_dots,width=btaddr_w,color=True) if i.skip == 'addr' \
@@ -180,6 +188,7 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
 						i.days if self.show_days else i.confs))
 
 		self.fmt_display = '\n'.join(out) + '\n'
+#		unsp.pdie()
 		return self.fmt_display
 
 	def format_for_printing(self,color=False):
@@ -190,13 +199,14 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
 
 		max_lbl_len = max([len(i.label) for i in self.unspent if i.label] or [1])
 		for n,i in enumerate(self.unspent):
-			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)
+			addr = '|'+'.' * 34 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,
-					MMGenID.fmtc(i.mmid or 'Non-{}'.format(g.proj_name),width=14,color=color),
+					MMGenID.fmtc(i.twmmid if i.twmmid.type=='mmgen'
+						else 'Non-{}'.format(g.proj_name),width=14,color=color),
 					i.amt.fmt(color=color),i.confs,i.days,
 					i.label.hl(color=color) if i.label else
-						MMGenAddrLabel.fmtc('',color=color,nullrepl='-',width=max_lbl_len))
+						TwComment.fmtc('',color=color,nullrepl='-',width=max_lbl_len))
 			out.append(s.rstrip())
 
 		fs = 'Unspent outputs ({} UTC)\nSort order: {}\n\n{}\n\nTotal BTC: {}\n'
@@ -232,14 +242,15 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
 							"Removing label for address #%s.  Is this what you want?" % n):
 							return n,s
 					elif s:
-						if MMGenAddrLabel(s,on_fail='return'):
+						if TwComment(s,on_fail='return'):
 							return n,s
 
-	def view_and_sort(self):
+	def view_and_sort(self,tx):
+		txos = 'Total to spend, excluding fees: {} BTC\n\n'.format(tx.sum_outputs().hl()) if tx.outputs else ''
 		prompt = """
-Sort options: [t]xid, [a]mount, a[d]dress, [A]ge, [r]everse, [M]mgen addr
+{}Sort options: [t]xid, [a]mount, a[d]dress, [A]ge, [r]everse, [M]mgen addr
 Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen
-	""".strip()
+	""".format(txos).strip()
 		self.display()
 		msg(prompt)
 
@@ -257,8 +268,8 @@ Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen
 				idx,lbl = self.get_idx_and_label_from_user()
 				if idx:
 					e = self.unspent[idx-1]
-					if type(self).add_label(e.mmid,lbl,addr=e.addr):
-						self.get_data()
+					if type(self).add_label(e.twmmid,lbl,addr=e.addr):
+						self.get_unspent_data()
 						self.do_sort()
 						msg('%s\n%s\n%s' % (self.fmt_display,prompt,p))
 					else:
@@ -291,36 +302,71 @@ Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen
 
 	# returns on failure
 	@classmethod
-	def add_label(cls,arg1,label='',addr=None):
+	def add_label(cls,arg1,label='',addr=None,silent=False):
 		from mmgen.tx import is_mmgen_id,is_btc_addr
+		mmaddr,btcaddr = None,None
+		if is_btc_addr(addr or arg1):
+			btcaddr = BTCAddr(addr or arg1,on_fail='return')
 		if is_mmgen_id(arg1):
-			mmaddr = MMGenID(arg1)
-		elif is_btc_addr(arg1):             # called from 'mmgen-tool add_label'
-			addr = arg1
-			mmaddr = 'btc:'+arg1
-		elif not arg1 and is_btc_addr(addr): # called from view_and_sort(), non-MMGen addr
-			mmaddr = 'btc:'+addr
-		else:
-			die(3,'{}: not a BTC address or {} ID'.format(arg1,g.proj_name))
+			mmaddr = TwMMGenID(arg1)
 
-		if addr:
-			if not BTCAddr(addr,on_fail='return'): return False
-		else:
+		if not btcaddr and not mmaddr:
+			msg("Address '{}' invalid or not found in tracking wallet".format(addr or arg1))
+			return False
+
+		if not btcaddr:
 			from mmgen.addr import AddrData
-			addr = AddrData(source='tw').mmaddr2btcaddr(mmaddr)
-			if not addr:
-				msg('{} address {} not found in tracking wallet'.format(g.proj_name,mmaddr))
-				return False
+			btcaddr = AddrData(source='tw').mmaddr2btcaddr(mmaddr)
+
+		if not btcaddr:
+			msg("{} address '{}' not found in tracking wallet".format(g.proj_name,mmaddr))
+			return False
 
-		label = MMGenAddrLabel(label,on_fail='return')
-		if not label and label != '': return False
+		# Checked that the user isn't importing a random address
+		if not btcaddr.is_in_tracking_wallet():
+			msg("Address '{}' not in tracking wallet".format(btcaddr))
+			return False
+
+		c = bitcoin_connection()
+		if not btcaddr.is_for_current_chain():
+			msg("Address '{}' not valid for chain {}".format(btcaddr,g.chain.upper()))
+			return False
+
+		# Allow for the possibility that BTC addr of MMGen addr was entered.
+		# Do reverse lookup, so that MMGen addr will not be marked as non-MMGen.
+		if not mmaddr:
+			from mmgen.addr import AddrData
+			ad = AddrData(source='tw')
+			mmaddr = ad.btcaddr2mmaddr(btcaddr)
+
+		if not mmaddr: mmaddr = 'btc:'+btcaddr
+
+		mmaddr = TwMMGenID(mmaddr)
+
+		cmt = TwComment(label,on_fail='return')
+		if cmt in (False,None): return False
+
+		lbl = TwLabel(mmaddr + ('',' '+cmt)[bool(cmt)]) # label is ASCII for now
+
+		# NOTE: this works because importaddress() removes the old account before
+		# associating the new account with the address.
+		# Will be replaced by setlabel() with new RPC label API
+		# RPC args: addr,label,rescan[=true],p2sh[=none]
+		ret = c.importaddress(btcaddr,lbl,False,on_fail='return')
 
-		acct = mmaddr + (' ' + label if label else '') # label is ASCII for now
-		# return on failure - args: addr,label,rescan,p2sh
-		ret = bitcoin_connection().importaddress(addr,acct,False,on_fail='return')
 		from mmgen.rpc import rpc_error,rpc_errmsg
-		if rpc_error(ret): msg('From bitcoind: ' + rpc_errmsg(ret))
-		return not rpc_error(ret)
+		if rpc_error(ret):
+			msg('From bitcoind: ' + rpc_errmsg(ret))
+			if not silent:
+				msg('Label could not be {}'.format(('removed','added')[bool(label)]))
+			return False
+		else:
+			m = mmaddr.type.replace('mmg','MMG')
+			a = mmaddr.replace('btc:','')
+			s = '{} address {} in tracking wallet'.format(m,a)
+			if label: msg("Added label '{}' to {}".format(label,s))
+			else:     msg('Removed label from {}'.format(s))
+			return True
 
 	@classmethod
 	def remove_label(cls,mmaddr): cls.add_label(mmaddr,'')

+ 334 - 84
mmgen/tx.py

@@ -17,7 +17,7 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 """
-tx.py:  Bitcoin transaction routines
+tx.py:  Transaction routines for the MMGen suite
 """
 
 import sys,os
@@ -30,24 +30,97 @@ 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_addrlist_id(s):   return AddrListID(s,on_fail='silent')
+def is_tw_label(s):      return TwLabel(s,on_fail='silent')
 
 def is_wif(s):
 	if s == '': return False
 	from mmgen.bitcoin import wif2hex
 	return bool(wif2hex(s))
 
-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',
-
-class MMGenTxInput(MMGenListItem):
-	attrs = 'txid','vout','amt','label','mmid','addr','confs','scriptPubKey','have_wif','sequence'
-	label = MMGenListItemAttr('label','MMGenAddrLabel')
+def segwit_is_active(exit_on_error=False):
+	d = bitcoin_connection().getblockchaininfo()
+	if d['chain'] == 'regtest' or d['bip9_softforks']['segwit']['status'] == 'active':
+		return True
+	if g.skip_segwit_active_check: return True
+	if exit_on_error:
+		die(2,'Segwit not active on this chain.  Exiting')
+	else:
+		return False
 
-class MMGenTxOutput(MMGenListItem):
-	attrs = 'txid','vout','amt','label','mmid','addr','have_wif','is_chg'
-	label = MMGenListItemAttr('label','MMGenAddrLabel')
+def bytes2int(hex_bytes):
+	r = hexlify(unhexlify(hex_bytes)[::-1])
+	if r[0] in '89abcdef':
+		die(3,"{}: Negative values not permitted in transaction!".format(hex_bytes))
+	return int(r,16)
+
+def bytes2btc(hex_bytes):
+	return bytes2int(hex_bytes) * g.satoshi
+
+from collections import OrderedDict
+class DeserializedTX(OrderedDict,MMGenObject): # need to add MMGen types
+	def __init__(self,txhex):
+		tx = list(unhexlify(txhex))
+		tx_copy = tx[:]
+
+		def hshift(l,n,reverse=False):
+			ret = l[:n]
+			del l[:n]
+			return hexlify(''.join(ret[::-1] if reverse else ret))
+
+		# https://bitcoin.org/en/developer-reference#compactsize-unsigned-integers
+		# For example, the number 515 is encoded as 0xfd0302.
+		def readVInt(l):
+			s = int(hexlify(l[0]),16)
+			bytes_len = 1 if s < 0xfd else 2 if s == 0xfd else 4 if s == 0xfe else 8
+			if bytes_len != 1: del l[0]
+			ret = int(hexlify(''.join(l[:bytes_len][::-1])),16)
+			del l[:bytes_len]
+			return ret
+
+		d = { 'version': bytes2int(hshift(tx,4)) }
+		has_witness = (False,True)[hexlify(tx[0])=='00']
+		if has_witness:
+			u = hshift(tx,2)[2:]
+			if u != '01':
+				die(2,"'{}': Illegal value for flag in transaction!".format(u))
+			del tx_copy[-len(tx)-2:-len(tx)]
+
+		d['num_txins'] = readVInt(tx)
+		d['txins'] = MMGenList([OrderedDict((
+			('txid',      hshift(tx,32,reverse=True)),
+			('vout',      bytes2int(hshift(tx,4))),
+			('scriptSig', hshift(tx,readVInt(tx))),
+			('nSeq',      hshift(tx,4,reverse=True))
+		)) for i in range(d['num_txins'])])
+
+		d['num_txouts'] = readVInt(tx)
+		d['txouts'] = MMGenList([OrderedDict((
+			('amount',       bytes2btc(hshift(tx,8))),
+			('scriptPubKey', hshift(tx,readVInt(tx)))
+		)) for i in range(d['num_txouts'])])
+
+		d['witness_size'] = 0
+		if has_witness:
+			# https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki
+			# A non-witness program (defined hereinafter) txin MUST be associated with an empty
+			# witness field, represented by a 0x00.
+			del tx_copy[-len(tx):-4]
+			wd,tx = tx[:-4],tx[-4:]
+			d['witness_size'] = len(wd) + 2 # add marker and flag
+			for i in range(len(d['txins'])):
+				if hexlify(wd[0]) == '00':
+					hshift(wd,1)
+					continue
+				d['txins'][i]['witness'] = [hshift(wd,readVInt(wd)) for item in range(readVInt(wd))]
+			if wd:
+				die(3,'More witness data than inputs with witnesses!')
+
+		d['lock_time'] = bytes2int(hshift(tx,4))
+		d['txid'] = hexlify(sha256(sha256(''.join(tx_copy)).digest()).digest()[::-1])
+
+		keys = 'txid','version','lock_time','witness_size','num_txins','txins','num_txouts','txouts'
+		return OrderedDict.__init__(self, ((k,d[k]) for k in keys))
 
 class MMGenTX(MMGenObject):
 	ext      = 'rawtx'
@@ -56,11 +129,25 @@ class MMGenTX(MMGenObject):
 	txid_ext = 'txid'
 	desc = 'transaction'
 
+	class MMGenTxInput(MMGenListItem):
+		attrs = 'txid','vout','amt','label','mmid','addr','confs','scriptPubKey','have_wif','sequence'
+		txid = MMGenListItemAttr('txid','BitcoinTxID')
+		scriptPubKey = MMGenListItemAttr('scriptPubKey','HexStr')
+
+	class MMGenTxOutput(MMGenListItem):
+		attrs = 'txid','vout','amt','label','mmid','addr','have_wif','is_chg'
+
+	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',
+
+	class MMGenTxInputList(list,MMGenObject): pass
+	class MMGenTxOutputList(list,MMGenObject): pass
+
 	def __init__(self,filename=None):
-		self.inputs      = []
-		self.inputs_enc  = []
-		self.outputs     = []
-		self.outputs_enc = []
+		self.inputs      = self.MMGenTxInputList()
+		self.outputs     = self.MMGenTxOutputList()
 		self.send_amt    = BTCAmt('0')  # total amt minus change
 		self.hex         = ''           # raw serialized hex transaction
 		self.label       = MMGenTXLabel('')
@@ -70,13 +157,21 @@ class MMGenTX(MMGenObject):
 		self.chksum      = ''
 		self.fmt_data    = ''
 		self.blockcount  = 0
+		self.chain       = None
+
 		if filename:
-			if get_extension(filename) == self.sig_ext:
-				self.mark_signed()
 			self.parse_tx_file(filename)
+			self.check_sigs() # marks the tx as signed
+
+		# repeat with sign and send, because bitcoind could be restarted
+		self.die_if_incorrect_chain()
+
+	def die_if_incorrect_chain(self):
+		if self.chain and g.chain and self.chain != g.chain:
+			die(2,'Transaction is for {}, but current chain is {}!'.format(self.chain,g.chain))
 
 	def add_output(self,btcaddr,amt,is_chg=None):
-		self.outputs.append(MMGenTxOutput(addr=btcaddr,amt=amt,is_chg=is_chg))
+		self.outputs.append(self.MMGenTxOutput(addr=btcaddr,amt=amt,is_chg=is_chg))
 
 	def get_chg_output_idx(self):
 		for i in range(len(self.outputs)):
@@ -87,7 +182,7 @@ class MMGenTX(MMGenObject):
 	def update_output_amt(self,idx,amt):
 		o = self.outputs[idx].__dict__
 		o['amt'] = amt
-		self.outputs[idx] = MMGenTxOutput(**o)
+		self.outputs[idx] = self.MMGenTxOutput(**o)
 
 	def del_output(self,idx):
 		self.outputs.pop(idx)
@@ -95,7 +190,7 @@ class MMGenTX(MMGenObject):
 	def sum_outputs(self,exclude=None):
 		olist = self.outputs if exclude == None else \
 			self.outputs[:exclude] + self.outputs[exclude+1:]
-		return BTCAmt(sum([e.amt for e in olist]))
+		return BTCAmt(sum(e.amt for e in olist))
 
 	def add_mmaddrs_to_outputs(self,ad_w,ad_f):
 		a = [e.addr for e in self.outputs]
@@ -107,7 +202,7 @@ class MMGenTX(MMGenObject):
 				if f: e.label = f
 
 #	def encode_io(self,desc):
-# 		tr = getattr((MMGenTxOutput,MMGenTxInput)[desc=='inputs'],'tr')
+# 		tr = getattr((self.MMGenTxOutput,self.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)]
@@ -140,24 +235,89 @@ class MMGenTX(MMGenObject):
 	def edit_comment(self):
 		return self.add_comment(self)
 
-	# https://bitcoin.stackexchange.com/questions/1195/
-	# how-to-calculate-transaction-size-before-sending
+	def has_segwit_inputs(self):
+		return any(i.mmid and i.mmid.mmtype == 'S' for i in self.inputs)
+
+	# https://bitcoin.stackexchange.com/questions/1195/how-to-calculate-transaction-size-before-sending
 	# 180: uncompressed, 148: compressed
-	def get_size(self):
+	def estimate_size_old(self):
 		if not self.inputs or not self.outputs: return None
 		return len(self.inputs)*180 + len(self.outputs)*34 + 10
 
+	# https://bitcoincore.org/en/segwit_wallet_dev/
+	# vsize: 3 times of the size with original serialization, plus the size with new
+	# serialization, divide the result by 4 and round up to the next integer.
+
+	# TODO: results differ slightly from actual transaction size
+	def estimate_vsize(self):
+		if not self.inputs or not self.outputs: return None
+
+		sig_size = 72 # sig in DER format
+		pubkey_size = { 'compressed':33, 'uncompressed':65 }
+		outpoint_size = 36 # txid + vout
+
+		def get_inputs_size():
+			segwit_isize = outpoint_size + 1 + 23 + 4 # (txid,vout) [scriptSig size] scriptSig nSeq # = 64
+			# txid vout [scriptSig size] scriptSig (<sig> <pubkey>) nSeq
+			legacy_isize = outpoint_size + 1 + 2 + sig_size + pubkey_size['uncompressed'] + 4 # = 180
+			compressed_isize = outpoint_size + 1 + 2 + sig_size + pubkey_size['compressed'] + 4 # = 148
+			ret = sum((legacy_isize,segwit_isize)[i.mmid.mmtype=='S'] for i in self.inputs if i.mmid)
+			# assume all non-MMGen pubkeys are compressed (we have no way of knowing
+			# until we see the key).  TODO: add user option to specify this?
+			return ret + sum(compressed_isize for i in self.inputs if not i.mmid)
+
+		def get_outputs_size():
+			return sum((34,32)[o.addr.addr_fmt=='p2sh'] for o in self.outputs)
+
+		# https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki
+		# The witness is a serialization of all witness data of the transaction. Each txin is
+		# associated with a witness field. A witness field starts with a var_int to indicate the
+		# number of stack items for the txin. It is followed by stack items, with each item starts
+		# with a var_int to indicate the length. Witness data is NOT script.
+
+		# A non-witness program txin MUST be associated with an empty witness field, represented
+		# by a 0x00. If all txins are not witness program, a transaction's wtxid is equal to its txid.
+		def get_witness_size():
+			if not self.has_segwit_inputs(): return 0
+			wf_size = 1 + 1 + sig_size + 1 + pubkey_size['compressed'] # vInt vInt sig vInt pubkey = 108
+			return sum((1,wf_size)[bool(i.mmid) and i.mmid.mmtype=='S'] for i in self.inputs)
+
+		isize = get_inputs_size()
+		osize = get_outputs_size()
+		wsize = get_witness_size()
+#  		pmsg([i.mmid and i.mmid.mmtype == 'S' for i in self.inputs])
+#  		pmsg([i.mmid for i in self.inputs])
+#  		pmsg([i.mmid for i in self.outputs])
+#  		pmsg('isize',isize)
+#  		pmsg('osize',osize)
+#  		pmsg('wsize',wsize)
+
+		# TODO: compute real varInt sizes instead of assuming 1 byte
+		# old serialization: [nVersion]              [vInt][txins][vInt][txouts]         [nLockTime]
+		old_size =           4                     + 1   + isize + 1  + osize          + 4
+		# new serialization: [nVersion][marker][flag][vInt][txins][vInt][txouts][witness][nLockTime]
+		new_size =           4       + 1     + 1   + 1   + isize + 1  + osize + wsize  + 4 \
+				if wsize else old_size
+
+		ret = (old_size * 3 + new_size) / 4
+# 		pmsg('old_size',old_size) # This should be equal to the size of serialized signed tx
+# 		pmsg('ret',ret)
+# 		pmsg('estimate_size_old',self.estimate_size_old())
+		return ret
+
+	estimate_size = estimate_vsize
+
 	def get_fee(self):
 		return self.sum_inputs() - self.sum_outputs()
 
 	def btc2spb(self,btc_fee):
-		return int(btc_fee/g.satoshi/self.get_size())
+		return int(btc_fee/g.satoshi/self.estimate_size())
 
 	def get_relay_fee(self):
-		assert self.get_size()
-		kb_fee = BTCAmt(bitcoin_connection().getinfo()['relayfee'])
+		assert self.estimate_size()
+		kb_fee = BTCAmt(bitcoin_connection().getnetworkinfo()['relayfee'])
 		vmsg('Relay fee: {} BTC/kB'.format(kb_fee))
-		return kb_fee * self.get_size() / 1024
+		return kb_fee * self.estimate_size() / 1024
 
 	def convert_fee_spec(self,tx_fee,tx_size,on_fail='throw'):
 		if BTCAmt(tx_fee,on_fail='silent'):
@@ -174,7 +334,7 @@ class MMGenTX(MMGenObject):
 				assert False, "'{}': invalid tx-fee argument".format(tx_fee)
 
 	def get_usr_fee(self,tx_fee,desc='Missing description'):
-		btc_fee = self.convert_fee_spec(tx_fee,self.get_size(),on_fail='return')
+		btc_fee = self.convert_fee_spec(tx_fee,self.estimate_size(),on_fail='return')
 		if btc_fee == None:
 			msg("'{}': cannot convert satoshis-per-byte to BTC because transaction size is unknown".format(tx_fee))
 			assert False  # because we shouldn't be calling this if tx size is unknown
@@ -215,28 +375,35 @@ class MMGenTX(MMGenObject):
 			if hasattr(e,attr): delattr(e,attr)
 
 	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]
+		io,il = (
+			(self.MMGenTxOutput,self.MMGenTxOutputList),
+			(self.MMGenTxInput,self.MMGenTxInputList)
+		)[desc=='inputs']
+		return il([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
+		io = self.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 copy_inputs_from_tw(self,data):
-		self.inputs = self.decode_io('inputs',[e.__dict__ for e in data])
+	def copy_inputs_from_tw(self,tw_unspent_data):
+		txi,self.inputs = self.MMGenTxInput,self.MMGenTxInputList()
+		for d in tw_unspent_data:
+			t = txi(**dict([(attr,getattr(d,attr)) for attr in d.__dict__ if attr in txi.attrs]))
+			if d.twmmid.type == 'mmgen': t.mmid = d.twmmid # twmmid -> mmid
+			self.inputs.append(t)
 
 	def get_input_sids(self):
-		return set([e.mmid[:8] for e in self.inputs if e.mmid])
+		return set(e.mmid.sid for e in self.inputs if e.mmid)
 
 	def get_output_sids(self):
-		return set([e.mmid[:8] for e in self.outputs if e.mmid])
+		return set(e.mmid.sid for e in self.outputs if e.mmid)
 
 	def sum_inputs(self):
-		return sum([e.amt for e in self.inputs])
+		return sum(e.amt for e in self.inputs)
 
 	def add_timestamp(self):
 		self.timestamp = make_timestamp()
@@ -247,7 +414,8 @@ class MMGenTX(MMGenObject):
 	def format(self):
 		from mmgen.bitcoin import b58encode
 		lines = [
-			'{} {} {} {}'.format(
+			'{} {} {} {} {}'.format(
+				self.chain.upper() if self.chain else 'Unknown',
 				self.txid,
 				self.send_amt,
 				self.timestamp,
@@ -266,68 +434,136 @@ class MMGenTX(MMGenObject):
 		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 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):
 
+		self.die_if_incorrect_chain()
+
 		if not keys:
 			msg('No keys. Cannot sign!')
 			return False
 
-		qmsg('Passing %s key%s to bitcoind' % (len(keys),suf(keys,'k')))
-		dmsg('Keys:\n  %s' % '\n  '.join(keys))
-
-		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)
-
+		qmsg('Passing %s key%s to bitcoind' % (len(keys),suf(keys,'s')))
+
+		sig_data = []
+		for d in self.inputs:
+			e = dict([(k,getattr(d,k)) for k in 'txid','vout','scriptPubKey','amt'])
+			e['amount'] = e['amt']
+			del e['amt']
+			wif = keys[d.addr]
+			if d.mmid and d.mmid.mmtype == 'S':
+				from mmgen.bitcoin import pubhex2redeem_script
+				from mmgen.addr import keygen_wif2pubhex,keygen_selector
+				pubhex = keygen_wif2pubhex(wif,keygen_selector())
+				e['redeemScript'] = pubhex2redeem_script(pubhex)
+			sig_data.append(e)
+
+		from mmgen.bitcoin import hash256
 		msg_r('Signing transaction{}...'.format(tx_num_str))
-		sig_tx = c.signrawtransaction(self.hex,sig_data,keys)
+		# sighashtype defaults to 'ALL'
+		sig_tx = c.signrawtransaction(self.hex,sig_data,keys.values())
 
 		if sig_tx['complete']:
-			msg('OK')
 			self.hex = sig_tx['hex']
-			self.mark_signed()
 			vmsg('Signed transaction size: {}'.format(len(self.hex)/2))
+			dt = DeserializedTX(self.hex)
+			txid = dt['txid']
+			self.check_sigs(dt)
+			assert txid == c.decoderawtransaction(self.hex)['txid'], 'txid mismatch (after signing)'
+			self.btc_txid = BitcoinTxID(txid,on_fail='return')
+			msg('OK')
 			return True
 		else:
 			msg('failed\nBitcoind returned the following errors:')
-			pp_msg(sig_tx['errors'])
+			pmsg(sig_tx['errors'])
 			return False
 
 	def mark_raw(self):
 		self.desc = 'transaction'
 		self.ext = self.raw_ext
 
-	def mark_signed(self):
+	def mark_signed(self): # called ONLY by check_sigs()
 		self.desc = 'signed transaction'
 		self.ext = self.sig_ext
 
-	def is_signed(self,color=False):
+	def marked_signed(self,color=False):
 		ret = self.desc == 'signed transaction'
 		return (red,green)[ret](str(ret)) if color else ret
 
-	def check_signed(self,c):
-		d = c.decoderawtransaction(self.hex)
-		ret = bool(d['vin'][0]['scriptSig']['hex'])
-		if ret: self.mark_signed()
-		return ret
+	def check_sigs(self,deserial_tx=None): # return False if no sigs, die on error
+		txins = (deserial_tx or DeserializedTX(self.hex))['txins']
+		has_ss = any(ti['scriptSig'] for ti in txins)
+		has_witness = any('witness' in ti and ti['witness'] for ti in txins)
+		if not (has_ss or has_witness):
+			return False
+		for ti in txins:
+			if ti['scriptSig'][:6] == '160014' and len(ti['scriptSig']) == 46: # P2SH-P2WPKH
+				assert 'witness' in ti, 'missing witness'
+				assert type(ti['witness']) == list and len(ti['witness']) == 2, 'malformed witness'
+				assert len(ti['witness'][1]) == 66, 'incorrect witness pubkey length'
+			elif ti['scriptSig'] == '': # native P2WPKH
+				die(3,('TX has missing signature','Native P2WPKH not implemented')['witness' in ti])
+			else: # non-witness
+				assert not 'witness' in ti, 'non-witness input has witness'
+				# sig_size 72 (DER format), pubkey_size 'compressed':33, 'uncompressed':65
+				assert (200 < len(ti['scriptSig']) < 300), 'malformed scriptSig' # VERY rough check
+		self.mark_signed()
+		return True
+
+	def has_segwit_outputs(self):
+		return any(o.mmid and o.mmid.mmtype == 'S' for o in self.outputs)
+
+	def is_in_mempool(self,c):
+		return 'size' in c.getmempoolentry(self.btc_txid,on_fail='silent')
+
+	def is_in_wallet(self,c):
+		ret = c.gettransaction(self.btc_txid,on_fail='silent')
+		return 'confirmations' in ret and ret['confirmations'] > 0
+
+	def is_replaced(self,c):
+		if self.is_in_mempool(c): return False
+		ret = c.gettransaction(self.btc_txid,on_fail='silent')
+		if not 'bip125-replaceable' in ret or not 'confirmations' in ret or ret['confirmations'] > 0:
+			return False
+		return -ret['confirmations'] + 1 # 1: replacement in mempool, 2: replacement confirmed
+
+	def is_in_utxos(self,c):
+		return 'txid' in c.getrawtransaction(self.btc_txid,True,on_fail='silent')
 
 	def send(self,c,prompt_user=True):
 
+		self.die_if_incorrect_chain()
+
+		bogus_send = os.getenv('MMGEN_BOGUS_SEND')
+
+		if self.has_segwit_outputs() and not segwit_is_active() and not bogus_send:
+			m = 'Transaction has MMGen Segwit outputs, but this blockchain does not support Segwit'
+			die(2,m+' at the current height')
+
 		if self.get_fee() > g.max_tx_fee:
 			die(2,'Transaction fee ({}) greater than max_tx_fee ({})!'.format(self.get_fee(),g.max_tx_fee))
 
+		if self.is_in_mempool(c):
+			msg('Warning: transaction is in mempool!')
+		elif self.is_in_wallet(c):
+			die(1,'Transaction has been confirmed!')
+		elif self.is_in_utxos(c):
+			die(2,red('ERROR: transaction is in the blockchain (but not in the tracking wallet)!'))
+
+		ret = self.is_replaced(c) # 1: replacement in mempool, 2: replacement confirmed
+		if ret:
+			die(1,'Transaction has been replaced'+('',', and the replacement TX is confirmed')[ret==2]+'!')
+
 		if prompt_user:
 			m1 = ("Once this transaction is sent, there's no taking it back!",'')[bool(opt.quiet)]
-			m2 = 'broadcast this transaction to the network'
+			m2 = 'broadcast this transaction to the {} network'.format(g.chain.upper())
 			m3 = ('YES, I REALLY WANT TO DO THIS','YES')[bool(opt.quiet or opt.yes)]
 			confirm_or_exit(m1,m2,m3)
 
 		msg('Sending transaction')
-		if os.getenv('MMGEN_BOGUS_SEND'):
+		if bogus_send:
 			ret = 'deadbeef' * 8
 			m = 'BOGUS transaction NOT sent: %s'
 		else:
@@ -335,15 +571,15 @@ class MMGenTX(MMGenObject):
 			m = 'Transaction sent: %s'
 
 		if ret:
-			self.btc_txid = BitcoinTxID(ret,on_fail='return')
-			if self.btc_txid:
-				self.desc = 'sent transaction'
-				msg(m % self.btc_txid.hl())
-				self.add_timestamp()
-				self.add_blockcount(c)
-				return True
-
-		# rpc implementation exits on failure, so we won't get here
+			if not bogus_send:
+				assert ret == self.btc_txid, 'txid mismatch (after sending)'
+			self.desc = 'sent transaction'
+			msg(m % self.btc_txid.hl())
+			self.add_timestamp()
+			self.add_blockcount(c)
+			return True
+
+		# rpc call exits on failure, so we won't get here
 		msg('Sending of transaction {} failed'.format(self.txid))
 		return False
 
@@ -375,7 +611,7 @@ class MMGenTX(MMGenObject):
 		o = self.format_view(terse=terse).encode('utf8')
 		if pager: do_pager(o)
 		else:
-			sys.stdout.write(o)
+			Msg_r(o)
 			from mmgen.term import get_char
 			if pause:
 				get_char('Press any key to continue: ')
@@ -397,6 +633,7 @@ class MMGenTX(MMGenObject):
 		self.inputs[0].sequence = g.max_int - 2
 
 	def format_view(self,terse=False):
+#		self.pdie()
 		try:
 			blockcount = bitcoin_connection().getblockcount()
 		except:
@@ -408,9 +645,10 @@ class MMGenTX(MMGenObject):
 		)[bool(terse)]
 
 		out = hdr_fs.format(self.txid.hl(),self.send_amt.hl(),self.timestamp,
-				self.is_rbf(color=True),self.is_signed(color=True))
+				self.is_rbf(color=True),self.marked_signed(color=True))
 
 		enl = ('\n','')[bool(terse)]
+		if self.chain in ('testnet','regtest'): out += green('Chain: {}\n'.format(self.chain.upper()))
 		if self.btc_txid: out += 'Bitcoin TxID: {}\n'.format(self.btc_txid.hl())
 		out += enl
 
@@ -418,9 +656,8 @@ class MMGenTX(MMGenObject):
 			out += 'Comment: %s\n%s' % (self.label.hl(),enl)
 		out += 'Inputs:\n' + enl
 
-		nonmm_str = '(non-{pnm} address){s}'.format(pnm=g.proj_name,s=('',' ')[terse])
-#		for i in self.inputs: print i #DEBUG
-		for n,e in enumerate(self.inputs):
+		nonmm_str = '(non-{pnm} address){s}  '.format(pnm=g.proj_name,s=('',' ')[terse])
+		for n,e in enumerate(sorted(self.inputs,key=lambda o: o.mmid.sort_key if o.mmid else o.addr)):
 			if blockcount:
 				confs = e.confs + blockcount - self.blockcount
 				days = int(confs * g.mins_per_block / (60*24))
@@ -440,7 +677,7 @@ class MMGenTX(MMGenObject):
 			out += '\n'
 
 		out += 'Outputs:\n' + enl
-		for n,e in enumerate(self.outputs):
+		for n,e in enumerate(sorted(self.outputs,key=lambda o: o.mmid.sort_key if o.mmid else o.addr)):
 			if e.mmid:
 				app=('',' (chg)')[bool(e.is_chg and terse)]
 				mmid_fmt = e.mmid.fmt(width=len(nonmm_str),encl='()',color=True,
@@ -474,7 +711,11 @@ class MMGenTX(MMGenObject):
 		)
 		if opt.verbose:
 			ts = len(self.hex)/2 if self.hex else 'unknown'
-			out += 'Transaction size: estimated - {}, actual - {}\n'.format(self.get_size(),ts)
+			out += 'Transaction size: Vsize={} Actual={}'.format(self.estimate_size(),ts)
+			if self.marked_signed():
+				ws = DeserializedTX(self.hex)['witness_size']
+				out += ' Base={} Witness={}'.format(ts-ws,ws)
+			out += '\n'
 
 		# TX label might contain non-ascii chars
 		return out
@@ -489,7 +730,7 @@ class MMGenTX(MMGenObject):
 
 		if len(tx_data) < 5: do_err('number of lines')
 
-		self.chksum = tx_data.pop(0)
+		self.chksum = HexStr(tx_data.pop(0))
 		if self.chksum != make_chksum_6(' '.join(tx_data)):
 			do_err('checksum')
 
@@ -517,12 +758,17 @@ class MMGenTX(MMGenObject):
 		else:
 			do_err('number of lines')
 
-		if len(metadata.split()) != 4: do_err('metadata')
+		metadata = metadata.split()
+		if len(metadata) not in (4,5): do_err('metadata')
+		if len(metadata) == 5:
+			t = metadata.pop(0)
+			self.chain = (t.lower(),None)[t=='Unknown']
 
-		self.txid,send_amt,self.timestamp,blockcount = metadata.split()
+		self.txid,send_amt,self.timestamp,blockcount = metadata
 		self.txid = MMGenTxID(self.txid)
 		self.send_amt = BTCAmt(send_amt)
 		self.blockcount = int(blockcount)
+		self.hex = HexStr(self.hex)
 
 		try: unhexlify(self.hex)
 		except: do_err('hex data')
@@ -530,6 +776,9 @@ class MMGenTX(MMGenObject):
 		try: self.inputs = self.decode_io('inputs',eval(inputs_data))
 		except: do_err('inputs data')
 
+		if not self.chain and not self.inputs[0].addr.testnet:
+			self.chain = 'mainnet'
+
 		try: self.outputs = self.decode_io('outputs',eval(outputs_data))
 		except: do_err('btc-to-mmgen address map data')
 
@@ -545,9 +794,9 @@ class MMGenBumpTX(MMGenTX):
 		if not self.is_rbf():
 			die(1,"Transaction '{}' is not replaceable (RBF)".format(self.txid))
 
-		# If sending, require tx to have been signed and broadcast
+		# If sending, require tx to have been signed
 		if send:
-			if not self.is_signed():
+			if not self.marked_signed():
 				die(1,"File '{}' is not a signed {} transaction file".format(filename,g.proj_name))
 			if not self.btc_txid:
 				die(1,"Transaction '{}' was not broadcast to the network".format(self.txid,g.proj_name))
@@ -560,7 +809,8 @@ class MMGenBumpTX(MMGenTX):
 		init_reply = opt.output_to_reduce
 		while True:
 			if init_reply == None:
-				reply = my_raw_input('Which output do you wish to deduct the fee from? ')
+				m = 'Choose an output to deduct the fee from (Hit ENTER for the change output): '
+				reply = my_raw_input(m) or 'c'
 			else:
 				reply,init_reply = init_reply,None
 			if chg_idx == None and not is_int(reply):

+ 77 - 64
mmgen/txcreate.py

@@ -121,7 +121,10 @@ def mmaddr2baddr(c,mmaddr,ad_w,ad_f):
 
 	return BTCAddr(btc_addr)
 
-def get_fee_from_estimate_or_usr(tx,c,estimate_fail_msg_shown=[]):
+def get_fee_from_estimate_or_user(tx,estimate_fail_msg_shown=[]):
+
+	c = bitcoin_connection()
+
 	if opt.tx_fee:
 		desc = 'User-selected'
 		start_fee = opt.tx_fee
@@ -134,107 +137,116 @@ def get_fee_from_estimate_or_usr(tx,c,estimate_fail_msg_shown=[]):
 				estimate_fail_msg_shown.append(True)
 			start_fee = None
 		else:
-			start_fee = BTCAmt(ret) * opt.tx_fee_adj * tx.get_size() / 1024
+			start_fee = BTCAmt(ret) * opt.tx_fee_adj * tx.estimate_size() / 1024
 			if opt.verbose:
 				msg('{} fee ({} confs): {} BTC/kB'.format(desc,opt.tx_confs,ret))
-				msg('TX size (estimated): {}'.format(tx.get_size()))
+				msg('TX size (estimated): {}'.format(tx.estimate_size()))
 
 	return tx.get_usr_fee_interactive(start_fee,desc=desc)
 
-def txcreate(opt,cmd_args,do_info=False,caller='txcreate'):
+def get_outputs_from_cmdline(cmd_args,tx):
+	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)
 
-	tx = MMGenTX()
+	ad_f = AddrData()
+	for a in addrfiles:
+		check_infile(a)
+		ad_f.add(AddrList(a))
 
-	if opt.comment_file: tx.add_comment(opt.comment_file)
+	ad_w = AddrData(source='tw')
 
-	c = bitcoin_connection()
-
-	if not do_info:
-		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)
-
-		ad_f = AddrData()
-		for a in addrfiles:
-			check_infile(a)
-			ad_f.add(AddrList(a))
-
-		ad_w = AddrData(source='tw')
-
-		for a in cmd_args:
-			if ',' in a:
-				a1,a2 = a.split(',',1)
-				if is_mmgen_id(a1) or is_btc_addr(a1):
-					btc_addr = mmaddr2baddr(c,a1,ad_w,ad_f) if is_mmgen_id(a1) else BTCAddr(a1)
-					tx.add_output(btc_addr,BTCAmt(a2))
-				else:
-					die(2,"%s: unrecognized subargument in argument '%s'" % (a1,a))
-			elif is_mmgen_id(a) or is_btc_addr(a):
-				if tx.get_chg_output_idx() != None:
-					die(2,'ERROR: More than one change address listed on command line')
-				btc_addr = mmaddr2baddr(c,a,ad_w,ad_f) if is_mmgen_id(a) else BTCAddr(a)
-				tx.add_output(btc_addr,BTCAmt('0'),is_chg=True)
+	for a in cmd_args:
+		if ',' in a:
+			a1,a2 = a.split(',',1)
+			if is_mmgen_id(a1) or is_btc_addr(a1):
+				btc_addr = mmaddr2baddr(c,a1,ad_w,ad_f) if is_mmgen_id(a1) else BTCAddr(a1)
+				tx.add_output(btc_addr,BTCAmt(a2))
 			else:
-				die(2,'%s: unrecognized argument' % a)
-
-		if not tx.outputs:
-			die(2,'At least one output must be specified on the command line')
+				die(2,"%s: unrecognized subargument in argument '%s'" % (a1,a))
+		elif is_mmgen_id(a) or is_btc_addr(a):
+			if tx.get_chg_output_idx() != None:
+				die(2,'ERROR: More than one change address listed on command line')
+			btc_addr = mmaddr2baddr(c,a,ad_w,ad_f) if is_mmgen_id(a) else BTCAddr(a)
+			tx.add_output(btc_addr,BTCAmt('0'),is_chg=True)
+		else:
+			die(2,'%s: unrecognized argument' % a)
 
-		if tx.get_chg_output_idx() == None:
-			die(2,('ERROR: No change output specified',wmsg['no_change_output'])[len(tx.outputs) == 1])
+	if not tx.outputs:
+		die(2,'At least one output must be specified on the command line')
 
+	if tx.get_chg_output_idx() == None:
+		die(2,('ERROR: No change output specified',wmsg['no_change_output'])[len(tx.outputs) == 1])
 
-	tw = MMGenTrackingWallet(minconf=opt.minconf)
-	tw.view_and_sort()
-	tw.display_total()
+	tx.add_mmaddrs_to_outputs(ad_w,ad_f)
 
-	if do_info: sys.exit()
+	if not segwit_is_active() and tx.has_segwit_outputs():
+		fs = '{} Segwit address requested on the command line, but Segwit is not active on this chain'
+		rdie(2,fs.format(g.proj_name))
 
-	tx.send_amt = tx.sum_outputs()
-
-	msg('Total amount to spend: %s' % ('Unknown','%s BTC'%tx.send_amt.hl())[bool(tx.send_amt)])
+def get_inputs_from_user(tw,tx,caller):
 
 	while True:
-		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],
-				' '.join(str(i) for i in sel_nums)
-			))
+		m = 'Enter a range or space-separated list of outputs to spend: '
+		sel_nums = select_unspent(tw.unspent,m)
+		msg('Selected output%s: %s' % (suf(sel_nums,'s'),' '.join(str(i) for i in sel_nums)))
 
-		sel_unspent = [tw.unspent[i-1] for i in sel_nums]
+		sel_unspent = tw.MMGenTwOutputList([tw.unspent[i-1] for i in sel_nums])
 
 		t_inputs = sum(s.amt for s in sel_unspent)
 		if t_inputs < tx.send_amt:
 			msg(wmsg['not_enough_btc'] % (tx.send_amt - t_inputs))
 			continue
 
-		non_mmaddrs = [i for i in sel_unspent if i.mmid == None]
+		non_mmaddrs = [i for i in sel_unspent if i.twmmid.type == 'non-mmgen']
 		if non_mmaddrs and caller != 'txdo':
 			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_from_tw(sel_unspent)      # makes tx.inputs
-
-		if opt.rbf: tx.signal_for_rbf()          # only after we have inputs
+		tx.copy_inputs_from_tw(sel_unspent)  # makes tx.inputs
 
-		change_amt = tx.sum_inputs() - tx.send_amt - get_fee_from_estimate_or_usr(tx,c)
+		change_amt = tx.sum_inputs() - tx.send_amt - get_fee_from_estimate_or_user(tx)
 
 		if change_amt >= 0:
 			p = 'Transaction produces %s BTC in change' % change_amt.hl()
 			if opt.yes or keypress_confirm(p+'.  OK?',default_yes=True):
 				if opt.yes: msg(p)
-				break
+				return change_amt
 		else:
 			msg(wmsg['not_enough_btc'] % abs(change_amt))
 
+def txcreate(cmd_args,do_info=False,caller='txcreate'):
+
+	tx = MMGenTX()
+
+	if opt.comment_file: tx.add_comment(opt.comment_file)
+
+	c = bitcoin_connection()
+
+	if not do_info: get_outputs_from_cmdline(cmd_args,tx)
+
+	tw = MMGenTrackingWallet(minconf=opt.minconf)
+	tw.view_and_sort(tx)
+	tw.display_total()
+
+	if do_info: sys.exit(0)
+
+	tx.send_amt = tx.sum_outputs()
+
+	msg('Total amount to spend: %s' % ('Unknown','%s BTC'%tx.send_amt.hl())[bool(tx.send_amt)])
+
+	change_amt = get_inputs_from_user(tw,tx,caller)
+
+	if opt.rbf: tx.signal_for_rbf() # only after we have inputs
+
 	chg_idx = tx.get_chg_output_idx()
-	if change_amt > 0:
-		tx.update_output_amt(chg_idx,BTCAmt(change_amt))
-	else:
+
+	if change_amt == 0:
 		msg('Warning: Change address will be deleted as transaction produces no change')
 		tx.del_output(chg_idx)
+	else:
+		tx.update_output_amt(chg_idx,BTCAmt(change_amt))
 
 	if not tx.send_amt:
 		tx.send_amt = change_amt
@@ -244,11 +256,12 @@ def txcreate(opt,cmd_args,do_info=False,caller='txcreate'):
 	if not opt.yes:
 		tx.add_comment()   # edits an existing comment
 	tx.create_raw(c)       # creates tx.hex, tx.txid
-	tx.add_mmaddrs_to_outputs(ad_w,ad_f)
+
 	tx.add_timestamp()
 	tx.add_blockcount(c)
+	tx.chain = g.chain
 
-	assert tx.get_fee() <= g.max_tx_fee
+	assert tx.sum_inputs() - tx.sum_outputs() <= g.max_tx_fee
 
 	qmsg('Transaction successfully created')
 

+ 43 - 38
mmgen/txsign.py

@@ -68,34 +68,37 @@ ERROR: a key file must be supplied for the following non-{pnm} address%s:\n    %
 
 saved_seeds = {}
 
-def get_seed_for_seed_id(seed_id,infiles,saved_seeds):
+def get_seed_for_seed_id(sid,infiles,saved_seeds):
 
-	if seed_id in saved_seeds:
-		return saved_seeds[seed_id]
+	if sid in saved_seeds:
+		return saved_seeds[sid]
 
 	while True:
 		if infiles:
 			ss = SeedSource(infiles.pop(0),ignore_in_fmt=True)
 		elif opt.in_fmt:
-			qmsg('Need seed data for Seed ID %s' % seed_id)
+			qmsg('Need seed data for Seed ID %s' % sid)
 			ss = SeedSource()
 			msg('User input produced Seed ID %s' % ss.seed.sid)
 		else:
-			die(2,'ERROR: No seed source found for Seed ID: %s' % seed_id)
+			die(2,'ERROR: No seed source found for Seed ID: %s' % sid)
 
 		saved_seeds[ss.seed.sid] = ss.seed
-		if ss.seed.sid == seed_id: return ss.seed
+		if ss.seed.sid == sid: 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 = []
+	sids = set(i.sid for i in mmgen_addrs)
+	vmsg('Need seed%s: %s' % (suf(sids,'s'),' '.join(sids)))
+	d = AddrListList()
 	from mmgen.addr import KeyAddrList
-	for seed_id in seed_ids:
+	for sid in sids:
 		# Returns only if seed is found
-		seed = get_seed_for_seed_id(seed_id,infiles,saved_seeds)
-		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()
+		seed = get_seed_for_seed_id(sid,infiles,saved_seeds)
+		for t in MMGenAddrType.mmtypes:
+			idx_list = [i.idx for i in mmgen_addrs if i.sid == sid and i.mmtype == t]
+			if idx_list:
+				addr_idxs = AddrIdxList(idx_list=idx_list)
+				d += KeyAddrList(seed=seed,addr_idxs=addr_idxs,do_chksum=False,mmtype=MMGenAddrType(t)).flat_list()
 	return d
 
 def add_keys(tx,src,infiles=None,saved_seeds=None,keyaddr_list=None):
@@ -113,54 +116,57 @@ def add_keys(tx,src,infiles=None,saved_seeds=None,keyaddr_list=None):
 				if f.addr == e.addr:
 					e.have_wif = True
 					if src == 'inputs':
-						new_keys.append(f.wif)
+						new_keys.append((f.addr,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))
+		vmsg('Added %s wif key%s from %s' % (len(new_keys),suf(new_keys,'s'),desc))
 	return new_keys
 
-def get_tx_files(opt,args): # strips found args
-	def is_tx(i): return get_extension(i) == MMGenTX.raw_ext
-	ret = [args.pop(i) for i in range(len(args)-1,-1,-1) if is_tx(args[i])]
-	if not ret:
-		die(1,'You must specify a raw transaction file!')
-	return list(reversed(ret))
+def _pop_and_return(args,cmplist): # strips found args
+	return list(reversed([args.pop(args.index(a)) for a in reversed(args) if get_extension(a) in cmplist]))
 
-def get_seed_files(opt,args): # strips found args
-	def is_seed(i): return get_extension(i) in SeedSource.get_extensions()
-	ret = [args.pop(i) for i in range(len(args)-1,-1,-1) if is_seed(args[i])]
+def get_tx_files(opt,args):
+	ret = _pop_and_return(args,[MMGenTX.raw_ext])
+	if not ret: die(1,'You must specify a raw transaction file!')
+	return ret
+
+def get_seed_files(opt,args):
+	# favor unencrypted seed sources first, as they don't require passwords
+	u,e = SeedSourceUnenc,SeedSourceEnc
+	ret = _pop_and_return(args,u.get_extensions())
 	from mmgen.filename import find_file_in_dir
-	wf = find_file_in_dir(Wallet,g.data_dir)
+	wf = find_file_in_dir(Wallet,g.data_dir) # Make this the first encrypted ss in the list
 	if wf: ret.append(wf)
-	if not (ret or opt.mmgen_keys_from_file or opt.keys_from_file): # or opt.use_wallet_dat):
+	ret += _pop_and_return(args,e.get_extensions())
+	if not (ret 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!')
-	return list(reversed(ret))
+	return ret
 
 def get_keyaddrlist(opt):
-	ret = None
 	if opt.mmgen_keys_from_file:
-		ret = KeyAddrList(opt.mmgen_keys_from_file)
-	return ret
+		return KeyAddrList(opt.mmgen_keys_from_file)
+	return None
 
 def get_keylist(opt):
-	ret = None
 	if opt.keys_from_file:
 		l = get_lines_from_file(opt.keys_from_file,'key-address data',trim_comments=True)
 		ret = KeyAddrList(keylist=[m.split()[0] for m in l]) # accept bitcoind wallet dumps
-		ret.generate_addrs()
-	return ret
+		ret.generate_addrs_from_keylist()
+		return ret
+	return None
 
 def txsign(opt,c,tx,seed_files,kl,kal,tx_num_str=''):
 	# Start
 	keys = []
+#	tx.pmsg()
 	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()
+		keys += tmp.get_addr_wif_pairs()
 
 	if opt.mmgen_keys_from_file:
 		keys += add_keys(tx,'inputs',keyaddr_list=kal)
@@ -174,10 +180,9 @@ def txsign(opt,c,tx,seed_files,kl,kal,tx_num_str=''):
 
 	extra_sids = set(saved_seeds) - tx.get_input_sids() - tx.get_output_sids()
 	if extra_sids:
-		msg('Unused Seed ID%s: %s' %
-			(suf(extra_sids,'k'),' '.join(extra_sids)))
+		msg('Unused Seed ID{}: {}'.format(suf(extra_sids,'s'),' '.join(extra_sids)))
 
-	if tx.sign(c,tx_num_str,keys):
+	if tx.sign(c,tx_num_str,dict(keys)):
 		return tx
 	else:
-		die(3,'failed\nSome keys were missing.  Transaction %scould not be signed.' % tx_num_str)
+		die(3,'failed\nSome keys were missing.  Transaction {}could not be signed.'.format(tx_num_str))

+ 40 - 15
mmgen/util.py

@@ -35,7 +35,7 @@ def msgred(s): msg(red(s))
 def mmsg(*args):
 	for d in args: Msg(repr(d))
 def mdie(*args):
-	mmsg(*args); sys.exit()
+	mmsg(*args); sys.exit(0)
 
 def die_wait(delay,ev=0,s=''):
 	assert type(delay) == int
@@ -57,11 +57,18 @@ def Die(ev=0,s=''):
 	if s: Msg(s)
 	sys.exit(ev)
 
-def pp_format(d):
+def rdie(ev=0,s=''): die(ev,red(s))
+def ydie(ev=0,s=''): die(ev,yellow(s))
+
+def pformat(d):
 	import pprint
 	return pprint.PrettyPrinter(indent=4).pformat(d)
-def pp_die(d): die(1,pp_format(d))
-def pp_msg(d): msg(pp_format(d))
+def pmsg(*args):
+	if not args: return
+	Msg(pformat(args if len(args) > 1 else args[0]))
+def pdie(*args):
+	if not args: sys.exit(1)
+	Die(1,(pformat(args if len(args) > 1 else args[0])))
 
 def set_for_type(val,refval,desc,invert_bool=False,src=None):
 	src_str = (''," in '{}'".format(src))[bool(src)]
@@ -132,17 +139,16 @@ def dmsg(s):
 	if opt.debug: msg(s)
 
 def suf(arg,suf_type):
+	suf_types = { 's':  ('s',''), 'es': ('es','') }
+	assert suf_type in suf_types
 	t = type(arg)
 	if t == int:
 		n = arg
-	elif t in (list,tuple,set,dict):
+	elif any(issubclass(t,c) for c in (list,tuple,set,dict)):
 		n = len(arg)
 	else:
-		msg('%s: invalid parameter' % arg)
-		return ''
-
-	if suf_type in ('a','es'): return ('es','')[n == 1]
-	if suf_type in ('k','s'):  return ('s','')[n == 1]
+		die(2,'%s: invalid parameter for suf()' % arg)
+	return suf_types[suf_type][n==1]
 
 def get_extension(f):
 	a,b = os.path.splitext(f)
@@ -159,9 +165,12 @@ def make_chksum_N(s,nchars,sep=False):
 	return sep.join([s[i*4:i*4+4] for i in range(nchars/4)])
 
 def make_chksum_8(s,sep=False):
-	s = sha256(sha256(s).digest()).hexdigest()[:8].upper()
+	from mmgen.obj import HexStr
+	s = HexStr(sha256(sha256(s).digest()).hexdigest()[:8].upper(),case='upper')
 	return '{} {}'.format(s[:4],s[4:]) if sep else s
-def make_chksum_6(s): return sha256(s).hexdigest()[:6]
+def make_chksum_6(s):
+	from mmgen.obj import HexStr
+	return HexStr(sha256(s).hexdigest()[:6])
 def is_chksum_6(s): return len(s) == 6 and is_hex_str_lc(s)
 
 def make_iv_chksum(s): return sha256(s).hexdigest()[:8].upper()
@@ -700,12 +709,14 @@ def do_pager(text):
 
 	pagers,shell = ['less','more'],False
 	# --- Non-MSYS Windows code deleted ---
-	# raw, chop, scroll right 1 char, disable buggy line chopping for Windows
-	os.environ['LESS'] = (('-RS -#1'),('-cR -#1'))[g.platform=='win']
+	# raw, chop, horiz scroll 8 chars, disable buggy line chopping in MSYS
+	os.environ['LESS'] = (('--shift 8 -RS'),('-cR -#1'))[g.platform=='win']
 
 	if 'PAGER' in os.environ and os.environ['PAGER'] != pagers[0]:
 		pagers = [os.environ['PAGER']] + pagers
 
+	text = text.encode('utf8')
+
 	for pager in pagers:
 		end = ('\n(end of text)\n','')[pager=='less']
 		try:
@@ -771,5 +782,19 @@ def bitcoin_connection():
 				g.rpc_password or cfg['rpcpassword'],
 				auth_cookie=get_bitcoind_auth_cookie())
 	# do an RPC call so we exit immediately if we can't connect
-	c.client_version = int(c.getinfo()['version'])
+	if not g.bitcoind_version:
+		g.bitcoind_version = int(c.getnetworkinfo()['version'])
+		g.chain = c.getblockchaininfo()['chain']
+		if g.chain != 'regtest': g.chain += 'net'
+		assert g.chain in g.chains
+		err = None
+		if g.regtest and g.chain != 'regtest':
+			err = '--regtest option'
+		elif g.testnet and g.chain == 'mainnet':
+			err = '--testnet option'
+		# we won't actually get here, as connect will fail first
+		elif (not g.testnet) and g.chain != 'mainnet':
+			err = 'mainnet'
+		if err:
+			die(1,'{} selected but chain is {}'.format(err,g.chain))
 	return c

+ 20 - 0
scripts/traceback.py

@@ -0,0 +1,20 @@
+#!/usr/bin/env python
+import sys,traceback,os
+sys.path.insert(0,'.')
+
+if 'TMUX' in os.environ: del os.environ['TMUX']
+
+f = open('my.err','w')
+
+try:
+	sys.argv.pop(0)
+	execfile(sys.argv[0])
+except SystemExit:
+	sys.exit(int(str(sys.exc_info()[1])))
+except:
+	l = traceback.format_exception(*sys.exc_info())
+	exc = l.pop()
+	def red(s):    return '{e}[31;1m{}{e}[0m'.format(s,e='\033')
+	def yellow(s): return '{e}[33;1m{}{e}[0m'.format(s,e='\033')
+	sys.stdout.write('{}{} {}'.format(yellow(''.join(l)),red(exc),str(e[1])))
+	traceback.print_exc(file=f)

+ 19 - 17
test/gentest.py

@@ -29,7 +29,7 @@ from binascii import hexlify
 
 # Import these _after_ local path's been added to sys.path
 from mmgen.common import *
-from mmgen.bitcoin import hex2wif,privnum2addr
+from mmgen.bitcoin import hex2wif
 
 rounds = 100
 opts_data = {
@@ -39,6 +39,7 @@ opts_data = {
 -h, --help       Print this help message
 --, --longhelp   Print help message for long options (common options)
 -q, --quiet      Produce quieter output
+-s, --segwit     Generate Segwit (P2SH-P2WPKH) addresses
 -v, --verbose    Produce more verbose output
 """,
 	'notes': """
@@ -48,15 +49,14 @@ opts_data = {
        Compare: {prog} a <dump file> (compare output of a key generator against wallet dump)
           where a and b are one of:
              '1' - native Python ecdsa library (very slow)
-             '2' - 'keyconv' utility from the 'vanitygen' package (old default)
-             '3' - bitcoincore.org's secp256k1 library (default from v0.8.6)
+             '2' - bitcoincore.org's secp256k1 library (default from v0.8.6)
 
 EXAMPLES:
-  {prog} 2:3 1000
-    (compare output of 'keyconv' with secp256k1 library, 1000 rounds)
-  {prog} 3 1000
+  {prog} 1:2 100
+    (compare output of native Python ECDSA with secp256k1 library, 100 rounds)
+  {prog} 2 1000
     (test speed of secp256k1 library address generation, 1000 rounds)
-  {prog} 3 my.dump
+  {prog} 2 my.dump
     (compare addrs generated with secp256k1 library to bitcoind wallet dump)
 """.format(prog='gentest.py',pnm=g.proj_name,snum=rounds)
 }
@@ -105,7 +105,7 @@ else:
 		die(1,"%s: invalid generator IDs" % cmd_args[0])
 
 def match_error(sec,wif,a_addr,b_addr,a,b):
-	m = ['','py-ecdsa','keyconv','secp256k1','dump']
+	m = ['','py-ecdsa','secp256k1','dump']
 	qmsg_r(red('\nERROR: Addresses do not match!'))
 	die(3,"""
   sec key   : {}
@@ -114,13 +114,16 @@ def match_error(sec,wif,a_addr,b_addr,a,b):
   {b:10}: {}
 """.format(sec,wif,a_addr,b_addr,pnm=g.proj_name,a=m[a],b=m[b]).rstrip())
 
+# Begin execution
+mmtype = ('L','S')[bool(opt.segwit)]
+compressed = True
+
 if a and b:
 	m = "Comparing address generators '{}' and '{}'"
 	qmsg(green(m.format(g.key_generators[a-1],g.key_generators[b-1])))
 	from mmgen.addr import get_privhex2addr_f
 	gen_a = get_privhex2addr_f(generator=a)
 	gen_b = get_privhex2addr_f(generator=b)
-	compressed = False
 	last_t = time.time()
 
 	for i in range(rounds):
@@ -129,12 +132,12 @@ if a and b:
 			last_t = time.time()
 		sec = hexlify(os.urandom(32))
 		wif = hex2wif(sec,compressed=compressed)
-		a_addr = gen_a(sec,compressed)
-		b_addr = gen_b(sec,compressed)
+		a_addr = gen_a(sec,compressed,mmtype=mmtype)
+		b_addr = gen_b(sec,compressed,mmtype=mmtype)
 		vmsg('\nkey:  %s\naddr: %s\n' % (wif,a_addr))
 		if a_addr != b_addr:
 			match_error(sec,wif,a_addr,b_addr,a,b)
-		if a != 2 and b != 2:
+		if not opt.segwit:
 			compressed = not compressed
 	qmsg_r('\rRound %s/%s ' % (i+1,rounds))
 
@@ -143,12 +146,11 @@ elif a and not fh:
 	m = "Testing speed of address generator '{}'"
 	qmsg(green(m.format(g.key_generators[a-1])))
 	from mmgen.addr import get_privhex2addr_f
-	gen_a = get_privhex2addr_f(generator=a)
+	gen = get_privhex2addr_f(generator=a)
 	from struct import pack,unpack
 	seed = os.urandom(28)
 	print 'Incrementing key with each round'
 	print 'Starting key:', hexlify(seed+pack('I',0))
-	compressed = False
 	import time
 	start = last_t = time.time()
 
@@ -158,9 +160,9 @@ elif a and not fh:
 			last_t = time.time()
 		sec = hexlify(seed+pack('I',i))
 		wif = hex2wif(sec,compressed=compressed)
-		a_addr = gen_a(sec,compressed)
+		a_addr = gen(sec,compressed,mmtype=mmtype)
 		vmsg('\nkey:  %s\naddr: %s\n' % (wif,a_addr))
-		if a != 2:
+		if not opt.segwit:
 			compressed = not compressed
 	qmsg_r('\rRound %s/%s ' % (i+1,rounds))
 
@@ -179,7 +181,7 @@ elif a and dump:
 		if sec == False:
 			die(2,'\nInvalid {}net WIF address in dump file: {}'.format(('main','test')[g.testnet],wif))
 		compressed = wif[0] != ('5','9')[g.testnet]
-		b_addr = gen_a(sec,compressed)
+		b_addr = gen_a(sec,compressed,'L')
 		if a_addr != b_addr:
 			match_error(sec,wif,a_addr,b_addr,1 if compressed and a==2 else a,4)
 	qmsg(green(('\n','')[bool(opt.verbose)] + 'OK'))

+ 19 - 0
test/ref/98831F3A-S[1,31-33,500-501,1010-1011].addrs

@@ -0,0 +1,19 @@
+# MMGen address file
+#
+# This file is editable.
+# Everything following a hash symbol '#' is a comment and ignored by MMGen.
+# A text label of 32 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.
+# Address data checksum for 98831F3A-S[1,31-33,500-501,1010-1011]: 06C1 9C87 F25C 4EE6
+# Record this value to a secure location.
+98831F3A SEGWIT {
+  1     36TvVzU5mxSjJ3D9qKAmYzCV7iUqtTDezF
+  31    33BhCyZJeXt2EUitVQUCKSN8hutAL3jnDh
+  32    37GSqHBi7yKUYN1y5WGwcVeKgxAkhNTJBs
+  33    3CWwH3GZrWh2BfbVjaMChHnuamSc4uxLxK
+  500   35QxzdbJTErNtxYaQJQvfhv3qiG876bhHA
+  501   3P1r25Ch3rp7CUTw9uoBxoZsDQ9smGQ8iZ
+  1010  37h2StmReijH9j3kd1AVMU1vfXe5HwMowX
+  1011  37UkuGYko84PeTE46qXLDWKKvGhMM82tpu
+}

+ 19 - 0
test/ref/98831F3A-S[1,31-33,500-501,1010-1011].testnet.addrs

@@ -0,0 +1,19 @@
+# MMGen address file
+#
+# This file is editable.
+# Everything following a hash symbol '#' is a comment and ignored by MMGen.
+# A text label of 32 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.
+# Address data checksum for 98831F3A-S[1,31-33,500-501,1010-1011]: 58D1 7B6C E9F9 9C14
+# Record this value to a secure location.
+98831F3A SEGWIT {
+  1     2Mx28ZjQ7PQx5VpqhWSneAwBkL4h1dsq6cY
+  31    2MtjuGiVLFzPNSGMSAY64wPMPvG6L8zYx4e
+  32    2Mxpeu27jjRppk9eWkdtpESdauJNvQnxHw6
+  33    2N459LnCbTyCNPTE3Qhy5KEnAo7emrkhMpq
+  500   2MvyB4NXL4hMj6kB85S2oHeuK44UHtYTjtF
+  501   2NEa45p8ifKKTQG6Uq3R4akZ8RkN3arccQY
+  1010  2MyFEWdhTGBEdMWgJJ8nMyR1BssrF8oR48c
+  1011  2My2xy1UnQaZjrErbmy9CqTJb8cuXDzyKiH
+}

+ 134 - 112
test/test.py

@@ -20,25 +20,8 @@
 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))
@@ -138,6 +121,8 @@ opts_data = {
 -D, --direct-exec   Bypass pexpect and execute a command directly (for
                     debugging only)
 -e, --exact-output  Show the exact output of the MMGen script(s) being run
+-g, --segwit        Generate and use Segwit addresses
+-G, --segwit-random Generate and use a random mix of Segwit and Legacy addrs
 -l, --list-cmds     List and describe the commands in the test suite
 -L, --log           Log commands to file {lf}
 -n, --names         Display command names instead of descriptions
@@ -167,6 +152,11 @@ cmd_args = opts.init(opts_data)
 
 tn_desc = ('','.testnet')[g.testnet]
 
+def randbool():
+	return hexlify(os.urandom(1))[1] in '12345678'
+def get_segwit_val():
+	return randbool() if opt.segwit_random else True if opt.segwit else False
+
 cfgs = {
 	'15': {
 		'tmpdir':        os.path.join('test','tmp15'),
@@ -181,6 +171,7 @@ cfgs = {
 			'mmseed':      'export_seed_dfl_wallet',
 			'del_dw_run':  'delete_dfl_wallet',
 		},
+		'segwit': get_segwit_val()
 	},
 	'16': {
 		'tmpdir':        os.path.join('test','tmp16'),
@@ -189,6 +180,7 @@ cfgs = {
 		'dep_generators': {
 			pwfile:        'passchg_dfl_wallet',
 		},
+		'segwit': get_segwit_val()
 	},
 	'1': {
 		'tmpdir':        os.path.join('test','tmp1'),
@@ -211,6 +203,7 @@ cfgs = {
 			incog_id_fn:   'export_incog_hidden',
 			'akeys.mmenc': 'keyaddrgen'
 		},
+		'segwit': get_segwit_val()
 	},
 	'2': {
 		'tmpdir':        os.path.join('test','tmp2'),
@@ -224,6 +217,7 @@ cfgs = {
 			'sigtx':         'txsign2',
 			'mmwords':     'export_mnemonic2',
 		},
+		'segwit': get_segwit_val()
 	},
 	'3': {
 		'tmpdir':        os.path.join('test','tmp3'),
@@ -235,6 +229,7 @@ cfgs = {
 			'rawtx':         'txcreate3',
 			'sigtx':         'txsign3'
 		},
+		'segwit': get_segwit_val()
 	},
 	'4': {
 		'tmpdir':        os.path.join('test','tmp4'),
@@ -251,6 +246,7 @@ cfgs = {
 		},
 		'bw_filename': 'brainwallet.mmbrain',
 		'bw_params':   '192,1',
+		'segwit': get_segwit_val()
 	},
 	'14': {
 		'kapasswd':      'Maxwell',
@@ -263,6 +259,7 @@ cfgs = {
 			'addrs':       'addrgen14',
 			'akeys.mmenc': 'keyaddrgen14',
 		},
+		'segwit': get_segwit_val()
 	},
 	'5': {
 		'tmpdir':        os.path.join('test','tmp5'),
@@ -272,14 +269,15 @@ cfgs = {
 			'mmdat':       'passchg',
 			pwfile:        'passchg',
 		},
+		'segwit': get_segwit_val()
 	},
 	'6': {
 		'name':            'reference wallet check (128-bit)',
 		'seed_len':        128,
 		'seed_id':         'FE3C6545',
 		'ref_bw_seed_id':  '33F10310',
-		'addrfile_chk':    ('B230 7526 638F 38CB','B64D 7327 EF2A 60FE')[g.testnet],
-		'keyaddrfile_chk': ('CF83 32FB 8A8B 08E2','FEBF 7878 97BB CC35')[g.testnet],
+'addrfile_chk': ('B230 7526 638F 38CB','B64D 7327 EF2A 60FE','9914 6D10 2307 F348','7DBF 441F E188 8B37'),
+'keyaddrfile_chk':('CF83 32FB 8A8B 08E2','FEBF 7878 97BB CC35','C13B F717 D4E8 CF59','4DB5 BAF0 45B7 6E81'),
 		'passfile_chk':    'EB29 DC4F 924B 289F',
 		'passfile32_chk':  '37B6 C218 2ABC 7508',
 		'wpasswd':         'reference password',
@@ -300,15 +298,15 @@ cfgs = {
 			'addrs':       'refaddrgen1',
 			'akeys.mmenc': 'refkeyaddrgen1'
 		},
-
+		'segwit': get_segwit_val()
 	},
 	'7': {
 		'name':            'reference wallet check (192-bit)',
 		'seed_len':        192,
 		'seed_id':         '1378FC64',
 		'ref_bw_seed_id':  'CE918388',
-		'addrfile_chk':    ('8C17 A5FA 0470 6E89','0A59 C8CD 9439 8B81')[g.testnet],
-		'keyaddrfile_chk': ('9648 5132 B98E 3AD9','2F72 C83F 44C5 0FAC')[g.testnet],
+'addrfile_chk': ('8C17 A5FA 0470 6E89','0A59 C8CD 9439 8B81','91C4 0414 89E4 2089','3BA6 7494 8E2B 858D'),
+'keyaddrfile_chk': ('9648 5132 B98E 3AD9','2F72 C83F 44C5 0FAC','C98B DF08 A3D5 204B','25F2 AEB6 AAAC 8BBE'),
 		'passfile_chk':    'ADEA 0083 094D 489A',
 		'passfile32_chk':  '2A28 C5C7 36EC 217A',
 		'wpasswd':         'reference password',
@@ -329,23 +327,25 @@ cfgs = {
 			'addrs':       'refaddrgen2',
 			'akeys.mmenc': 'refkeyaddrgen2'
 		},
-
+		'segwit': get_segwit_val()
 	},
 	'8': {
 		'name':            'reference wallet check (256-bit)',
 		'seed_len':        256,
 		'seed_id':         '98831F3A',
 		'ref_bw_seed_id':  'B48CD7FC',
-		'addrfile_chk':    ('6FEF 6FB9 7B13 5D91','3C2C 8558 BB54 079E')[g.testnet],
-		'keyaddrfile_chk': ('9F2D D781 1812 8BAD','7410 8F95 4B33 B4B2')[g.testnet],
+'addrfile_chk': ('6FEF 6FB9 7B13 5D91','3C2C 8558 BB54 079E','06C1 9C87 F25C 4EE6','58D1 7B6C E9F9 9C14'),
+'keyaddrfile_chk': ('9F2D D781 1812 8BAD','7410 8F95 4B33 B4B2','A447 12C2 DD14 5A9B','0690 460D A600 D315'),
 		'passfile_chk':    '2D6D 8FBA 422E 1315',
 		'passfile32_chk':  'F6C1 CDFB 97D9 FCAE',
 		'wpasswd':         'reference password',
 		'ref_wallet':      '98831F3A-{}[256,1].mmdat'.format(('27F2BF93','E2687906')[g.testnet]),
 		'ref_addrfile':    '98831F3A[1,31-33,500-501,1010-1011]{}.addrs'.format(tn_desc),
+		'ref_segwitaddrfile':'98831F3A-S[1,31-33,500-501,1010-1011]{}.addrs'.format(tn_desc),
 		'ref_keyaddrfile': '98831F3A[1,31-33,500-501,1010-1011]{}.akeys.mmenc'.format(tn_desc),
 		'ref_passwdfile':  '98831F3A-фубар@crypto.org-b58-20[1,4,9-11,1100].pws',
 		'ref_addrfile_chksum':    ('6FEF 6FB9 7B13 5D91','3C2C 8558 BB54 079E')[g.testnet],
+		'ref_segwitaddrfile_chksum':('06C1 9C87 F25C 4EE6','58D1 7B6C E9F9 9C14')[g.testnet],
 		'ref_keyaddrfile_chksum': ('9F2D D781 1812 8BAD','7410 8F95 4B33 B4B2')[g.testnet],
 		'ref_passwdfile_chksum':  'A983 DAB9 5514 27FB',
 #		'ref_fake_unspent_data':'98831F3A_unspent.json',
@@ -367,6 +367,7 @@ cfgs = {
 			'addrs':       'refaddrgen3',
 			'akeys.mmenc': 'refkeyaddrgen3'
 		},
+		'segwit': get_segwit_val()
 	},
 	'9': {
 		'tmpdir':        os.path.join('test','tmp9'),
@@ -489,6 +490,7 @@ cmd_group['ref'] = (
 # misc. saved reference data
 cmd_group['ref_other'] = (
 	('ref_addrfile_chk',   'saved reference address file'),
+	('ref_segwitaddrfile_chk','saved reference address file (segwit)'),
 	('ref_keyaddrfile_chk','saved reference key-address file'),
 	('ref_passwdfile_chk', 'saved reference password file'),
 #	Create the fake inputs:
@@ -609,7 +611,7 @@ del cmd_group
 add_spawn_args = ' '.join(['{} {}'.format(
 	'--'+k.replace('_','-'),
 	getattr(opt,k) if getattr(opt,k) != True else ''
-	) for k in 'testnet','rpc_host' if getattr(opt,k)]).split()
+	) for k in 'testnet','rpc_host','rpc_port','regtest' if getattr(opt,k)]).split()
 add_spawn_args += ['--data-dir',data_dir]
 
 if opt.profile: opt.names = True
@@ -631,6 +633,8 @@ os.environ['MMGEN_NO_LICENSE'] = '1'
 os.environ['MMGEN_MIN_URANDCHARS'] = '3'
 os.environ['MMGEN_BOGUS_SEND'] = '1'
 
+def get_segwit_arg(cfg): return ([],['--type','segwit'])[cfg['segwit']]
+
 # Tell spawned programs they're running in the test suite
 os.environ['MMGEN_TEST_SUITE'] = '1'
 
@@ -692,7 +696,7 @@ if opt.list_cmds:
 	w = max([len(i) for i in utils])
 	for cmd in sorted(utils):
 		Msg(fs.format(cmd,utils[cmd],w=w))
-	sys.exit()
+	sys.exit(0)
 
 import time,re
 if g.platform == 'linux':
@@ -718,7 +722,7 @@ else: # Windows
 		m3 = '.\nControl values should be checked against the program output.\nContinue?'
 		if not keypress_confirm(green(m1)+grnbg(m2)+green(m3),default_yes=True):
 			errmsg('Exiting at user request')
-			sys.exit()
+			sys.exit(0)
 
 def my_send(p,t,delay=send_delay,s=False):
 	if delay: time.sleep(delay)
@@ -770,8 +774,10 @@ def get_file_with_ext(ext,mydir,delete=True,no_dot=False):
 	if len(flist) > 1:
 		if delete:
 			if not opt.quiet:
-				msg("Multiple *.%s files in '%s' - deleting" % (ext,mydir))
-			for f in flist: os.unlink(f)
+				msg("Multiple *.{} files in '{}' - deleting".format(ext,mydir))
+			for f in flist:
+				msg(f)
+				os.unlink(f)
 		return False
 	else:
 		return flist[0]
@@ -845,6 +851,7 @@ class MMGenExpect(object):
 		else:
 			if opt.traceback:
 				cmd,args = tb_cmd,[cmd]+args
+				cmd_str = tb_cmd + ' ' + cmd_str
 			if use_popen_spawn:
 				self.p = PopenSpawn(cmd_str)
 			else:
@@ -854,7 +861,7 @@ class MMGenExpect(object):
 	def ok(self,exit_val=0):
 		ret = self.p.wait()
 		if ret != exit_val:
-			die(1,red('Program exited with value {}'.format(ret)))
+			die(1,red('test.py: spawned program exited with value {}'.format(ret)))
 		if opt.profile: return
 		if opt.verbose or opt.exact_output:
 			sys.stderr.write(green('OK\n'))
@@ -863,7 +870,7 @@ class MMGenExpect(object):
 	def cmp_or_die(self,s,t,skip_ok=False,exit_val=0):
 		ret = self.p.wait()
 		if ret != exit_val:
-			die(1,red('Program exited with value {}'.format(ret)))
+			die(1,red('test.py: spawned program exited with value {}'.format(ret)))
 		if s == t:
 			if not skip_ok: ok()
 		else:
@@ -977,16 +984,17 @@ class MMGenExpect(object):
 from mmgen.obj import BTCAmt
 from mmgen.bitcoin import verify_addr
 
-def create_fake_unspent_entry(address,sid=None,idx=None,lbl=None,non_mmgen=None):
+def create_fake_unspent_entry(btcaddr,al_id=None,idx=None,lbl=None,non_mmgen=False,segwit=False):
 	if lbl: lbl = ' ' + lbl
+	spk1,spk2 = (('76a914','88ac'),('a914','87'))[segwit and btcaddr.addr_fmt=='p2sh']
 	return {
-		'account': (non_mmgen or ('%s:%s%s' % (sid,idx,lbl))).decode('utf8'),
+		'account': 'btc:{}'.format(btcaddr) if non_mmgen else (u'{}:{}{}'.format(al_id,idx,lbl.decode('utf8'))),
 		'vout': int(getrandnum(4) % 8),
 		'txid': hexlify(os.urandom(32)).decode('utf8'),
 		'amount': BTCAmt('%s.%s' % (10+(getrandnum(4) % 40), getrandnum(4) % 100000000)),
-		'address': address,
+		'address': btcaddr,
 		'spendable': False,
-		'scriptPubKey': ('76a914'+verify_addr(address,return_hex=True)+'88ac'),
+		'scriptPubKey': (spk1+verify_addr(btcaddr,return_hex=True)+spk2),
 		'confirmations': getrandnum(4) % 50000
 	}
 
@@ -1013,35 +1021,82 @@ labels = [
 	"Carl's capital",
 ]
 label_iter = None
-def create_fake_unspent_data(adata,unspent_data_file,tx_data,non_mmgen_input=''):
+
+def create_fake_unspent_data(adata,tx_data,non_mmgen_input=''):
 
 	out = []
-	for s in tx_data:
-		sid = tx_data[s]['sid']
-		a = adata.addrlist(sid)
-		for n,(idx,btcaddr) in enumerate(a.addrpairs(),1):
+	for d in tx_data.values():
+		al = adata.addrlist(d['al_id'])
+		for n,(idx,btcaddr) in enumerate(al.addrpairs()):
 			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))
+			out.append(create_fake_unspent_entry(btcaddr,d['al_id'],idx,lbl,segwit=d['segwit']))
+			if n == 0:  # create a duplicate address. This means addrs_per_wallet += 1
+				out.append(create_fake_unspent_entry(btcaddr,d['al_id'],idx,lbl,segwit=d['segwit']))
 
 	if non_mmgen_input:
-		from mmgen.bitcoin import privnum2addr,hex2wif
 		privnum = getrandnum(32)
-		btcaddr = privnum2addr(privnum,compressed=True)
+		from mmgen.bitcoin import privnum2addr,hex2wif
+		from mmgen.obj import BTCAddr
+		btcaddr = BTCAddr(privnum2addr(privnum,compressed=True))
 		of = os.path.join(cfgs[non_mmgen_input]['tmpdir'],non_mmgen_fn)
 		wif = hex2wif('{:064x}'.format(privnum),compressed=True)
 #		Msg(yellow(wif + ' ' + btcaddr))
 		write_data_to_file(of,wif+'\n','compressed bitcoin key',silent=True)
+		out.append(create_fake_unspent_entry(btcaddr,non_mmgen=True,segwit=False))
 
-		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)
+#	msg('\n'.join([repr(o) for o in out])); sys.exit(0)
+	return out
 
+def	write_fake_data_to_file(d):
+	unspent_data_file = os.path.join(cfg['tmpdir'],'unspent.json')
+	write_data_to_file(unspent_data_file,d,'Unspent outputs',silent=True)
+	os.environ['MMGEN_BOGUS_WALLET_DATA'] = unspent_data_file
+	bwd_msg = 'MMGEN_BOGUS_WALLET_DATA=%s' % unspent_data_file
+	if opt.print_cmdline: msg(bwd_msg)
+	if opt.log: log_fd.write(bwd_msg + ' ')
+	if opt.verbose or opt.exact_output:
+		sys.stderr.write("Fake transaction wallet data written to file '%s'\n" % unspent_data_file)
+
+def create_tx_data(sources):
+	from mmgen.addr import AddrList,AddrData,AddrIdxList
+	tx_data,ad = {},AddrData()
+	for s in sources:
+		afile = get_file_with_ext('addrs',cfgs[s]['tmpdir'])
+		al = AddrList(afile)
+		ad.add(al)
+		aix = AddrIdxList(fmt_str=cfgs[s]['addr_idx_list'])
+		if len(aix) != addrs_per_wallet:
+			errmsg(red('Address index list length != %s: %s' %
+						(addrs_per_wallet,repr(aix))))
+			sys.exit(0)
+		tx_data[s] = {
+			'addrfile': afile,
+			'chk': al.chksum,
+			'al_id': al.al_id,
+			'addr_idxs': aix[-2:],
+			'segwit': cfgs[s]['segwit']
+		}
+	return ad,tx_data
+
+def make_txcreate_cmdline(tx_data):
+	from mmgen.bitcoin import privnum2addr
+	btcaddr = privnum2addr(getrandnum(32),compressed=True)
+
+	cmd_args = ['-d',cfg['tmpdir']]
+	for num in tx_data:
+		s = tx_data[num]
+		cmd_args += [
+			'{}:{},{}'.format(s['al_id'],s['addr_idxs'][0],cfgs[num]['amts'][0]),
+		]
+		# + one change address and one BTC address
+		if num is tx_data.keys()[-1]:
+			cmd_args += ['{}:{}'.format(s['al_id'],s['addr_idxs'][1])]
+			cmd_args += ['{},{}'.format(btcaddr,cfgs[num]['amts'][1])]
+
+	return cmd_args + [tx_data[num]['addrfile'] for num in tx_data]
 
 def add_comments_to_addr_file(addrfile,outfile):
 	silence()
@@ -1073,7 +1128,7 @@ def do_between():
 			if opt.verbose or opt.exact_output: sys.stderr.write('\n')
 		else:
 			errmsg('Exiting at user request')
-			sys.exit()
+			sys.exit(0)
 	elif opt.verbose or opt.exact_output:
 		sys.stderr.write('\n')
 
@@ -1370,7 +1425,7 @@ class MMGenTestSuite(object):
 
 	def addrgen(self,name,wf,pf=None,check_ref=False,ftype='addr',id_str=None,extra_args=[]):
 		ftype,chkfile = ((ftype,'{}file_chk'.format(ftype)),('pass','passfile32_chk'))[ftype=='pass32']
-		add_args = extra_args + ([],['-q'] + ([],['-P',pf])[bool(pf)])[ia]
+		add_args = extra_args + ([],['-q'] + ([],['-P',pf])[bool(pf)])[ia] + (get_segwit_arg(cfg),[])[ftype[:4]=='pass']
 		dlist = [id_str] if id_str else []
 		t = MMGenExpect(name,'mmgen-{}gen'.format(ftype), add_args +
 				['-d',cfg['tmpdir']] + ([],[wf])[bool(wf)] + dlist + [cfg['{}_idx_list'.format(ftype)]])
@@ -1381,7 +1436,8 @@ class MMGenTestSuite(object):
 		desc = ('address','password')[ftype=='pass']
 		chk = t.expect_getend(r'Checksum for {} data .*?: '.format(desc),regex=True)
 		if check_ref:
-			refcheck('address data checksum',chk,cfg[chkfile])
+			c = (cfg[chkfile][g.testnet + 2*cfg['segwit']],cfg[chkfile])[ftype=='pass']
+			refcheck('address data checksum',chk,c)
 			return
 		t.written_to_file('Addresses',oo=True)
 		t.ok()
@@ -1400,7 +1456,6 @@ class MMGenTestSuite(object):
 		t = MMGenExpect(name,'mmgen-addrimport', add_args + [outfile])
 		if ia: return
 		t.expect_getend(r'Checksum for address data .*\[.*\]: ',regex=True)
-		t.expect_getend('Validating addresses...OK. ')
 		t.expect("Type uppercase 'YES' to confirm: ",'\n')
 		vmsg('This is a simulation, so no addresses were actually imported into the tracking\nwallet')
 		t.ok(exit_val=1)
@@ -1408,50 +1463,14 @@ class MMGenTestSuite(object):
 	def txcreate_common(self,name,sources=['1'],non_mmgen_input='',do_label=False,txdo_args=[],add_args=[]):
 		if opt.verbose or opt.exact_output:
 			sys.stderr.write(green('Generating fake tracking wallet info\n'))
-		silence()
-		from mmgen.addr import AddrList,AddrData,AddrIdxList
-		tx_data,ad = {},AddrData()
-		for s in sources:
-			afile = get_file_with_ext('addrs',cfgs[s]['tmpdir'])
-			ai = AddrList(afile)
-			ad.add(ai)
-			aix = AddrIdxList(fmt_str=cfgs[s]['addr_idx_list'])
-			if len(aix) != addrs_per_wallet:
-				errmsg(red('Address index list length != %s: %s' %
-							(addrs_per_wallet,repr(aix))))
-				sys.exit()
-			tx_data[s] = {
-				'addrfile': afile,
-				'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(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
-		btcaddr = privnum2addr(getrandnum(32),compressed=True)
 
-		cmd_args = ['-d',cfg['tmpdir']]
-		for num in tx_data:
-			s = tx_data[num]
-			cmd_args += [
-				'%s:%s,%s' % (s['sid'],s['addr_idxs'][0],cfgs[num]['amts'][0]),
-			]
-			# + one BTC address
-			# + one change address and one BTC address
-			if num is tx_data.keys()[-1]:
-				cmd_args += ['%s:%s' % (s['sid'],s['addr_idxs'][1])]
-				cmd_args += ['%s,%s' % (btcaddr,cfgs[num]['amts'][1])]
-
-		for num in tx_data: cmd_args += [tx_data[num]['addrfile']]
-
-		os.environ['MMGEN_BOGUS_WALLET_DATA'] = unspent_data_file
+		silence()
+		ad,tx_data = create_tx_data(sources)
+		dfake = create_fake_unspent_data(ad,tx_data,non_mmgen_input)
+		write_fake_data_to_file(repr(dfake))
+		cmd_args = make_txcreate_cmdline(tx_data)
 		end_silence()
+
 		if opt.verbose or opt.exact_output: sys.stderr.write('\n')
 
 		if ia:
@@ -1459,9 +1478,6 @@ class MMGenTestSuite(object):
 			m = '\nAnswer the interactive prompts as follows:\n' + \
 				" 'y', 'y', 'q', '1-9'<ENTER>, ENTER, ENTER, ENTER, ENTER, 'y'"
 			msg(grnbg(m))
-		bwd_msg = 'MMGEN_BOGUS_WALLET_DATA=%s' % unspent_data_file
-		if opt.print_cmdline: msg(bwd_msg)
-		if opt.log: log_fd.write(bwd_msg + ' ')
 		t = MMGenExpect(name,'mmgen-'+('txcreate','txdo')[bool(txdo_args)],['--rbf','-f',tx_fee] + add_args + cmd_args + txdo_args)
 		if ia: return
 		t.license()
@@ -1516,7 +1532,7 @@ class MMGenTestSuite(object):
 			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.expect('Which output do you wish to deduct the fee from? ','1\n')
+		t.expect('deduct the fee from (Hit ENTER for the change output): ','1\n')
 		# Fee must be > tx_fee + network relay fee (currently 0.00001)
 		t.expect('OK? (Y/n): ','\n')
 		t.expect('Enter transaction fee: ','124s\n')
@@ -1567,13 +1583,12 @@ class MMGenTestSuite(object):
 		if txdo_handle: return
 		if save:
 			self.txsign_end(t,has_label=has_label)
-			exit_val = 0
+			t.ok()
 		else:
 			cprompt = ('Add a comment to transaction','Edit transaction comment')[has_label]
 			t.expect('%s? (y/N): ' % cprompt,'\n')
 			t.expect('Save signed transaction? (Y/n): ','n')
-			exit_val = 1
-		t.ok(exit_val=exit_val)
+			t.ok(exit_val=1)
 
 	def txsign_dfl_wallet(self,name,txfile,pf='',save=True,has_label=False):
 		return self.txsign(name,txfile,wf=None,pf=pf,save=save,has_label=has_label)
@@ -1586,7 +1601,7 @@ class MMGenTestSuite(object):
 			t.license()
 			t.tx_view()
 			t.expect('Add a comment to transaction? (y/N): ','\n')
-		t.expect('broadcast this transaction to the network?')
+		t.expect('Are you sure you want to broadcast this')
 		m = 'YES, I REALLY WANT TO DO THIS'
 		t.expect("'%s' to confirm: " % m,m+'\n')
 		t.expect('BOGUS transaction NOT sent')
@@ -1655,8 +1670,8 @@ class MMGenTestSuite(object):
 
 	def addrgen_seed(self,name,wf,foo,desc='seed data',in_fmt='seed'):
 		stdout = (False,True)[desc=='seed data'] #capture output to screen once
-		add_arg = ([],['-S'])[bool(stdout)]
-		t = MMGenExpect(name,'mmgen-addrgen', add_arg +
+		add_args = ([],['-S'])[bool(stdout)] + get_segwit_arg(cfg)
+		t = MMGenExpect(name,'mmgen-addrgen', add_args +
 				['-i'+in_fmt,'-d',cfg['tmpdir'],wf,cfg['addr_idx_list']])
 		t.license()
 		t.expect_getend('Valid %s for Seed ID ' % desc)
@@ -1677,7 +1692,7 @@ class MMGenTestSuite(object):
 		self.addrgen_seed(name,wf,foo,desc='mnemonic data',in_fmt='words')
 
 	def addrgen_incog(self,name,wf=[],foo='',in_fmt='i',desc='incognito data',args=[]):
-		t = MMGenExpect(name,'mmgen-addrgen', args+['-i'+in_fmt,'-d',cfg['tmpdir']]+
+		t = MMGenExpect(name,'mmgen-addrgen', args + get_segwit_arg(cfg) + ['-i'+in_fmt,'-d',cfg['tmpdir']]+
 				([],[wf])[bool(wf)] + [cfg['addr_idx_list']])
 		t.license()
 		t.expect_getend('Incog Wallet ID: ')
@@ -1698,7 +1713,7 @@ class MMGenTestSuite(object):
 			args=['-H','%s,%s'%(rf,hincog_offset),'-l',str(hincog_seedlen)])
 
 	def keyaddrgen(self,name,wf,pf=None,check_ref=False):
-		args = ['-d',cfg['tmpdir'],usr_rand_arg,wf,cfg['addr_idx_list']]
+		args = get_segwit_arg(cfg) + ['-d',cfg['tmpdir'],usr_rand_arg,wf,cfg['addr_idx_list']]
 		if ia:
 			m = "\nAnswer 'n' at the interactive prompt"
 			msg(grnbg(m))
@@ -1709,7 +1724,7 @@ class MMGenTestSuite(object):
 		t.passphrase('MMGen wallet',cfg['wpasswd'])
 		chk = t.expect_getend(r'Checksum for key-address data .*?: ',regex=True)
 		if check_ref:
-			refcheck('key-address data checksum',chk,cfg['keyaddrfile_chk'])
+			refcheck('key-address data checksum',chk,cfg['keyaddrfile_chk'][g.testnet + 2*cfg['segwit']])
 			return
 		t.expect('Encrypt key list? (y/N): ','y')
 		t.usr_rand(usr_rand_chars)
@@ -2025,7 +2040,7 @@ class MMGenTestSuite(object):
 			aa = ['-P',get_tmpfile_fn(cfg,pfn)]
 		else:
 			aa = []
-		t = MMGenExpect(name,'mmgen-tool',aa+[ftype+'file_chksum',wf])
+		t = MMGenExpect(name,'mmgen-tool',aa+[ftype.replace('segwit','')+'file_chksum',wf])
 		if ia:
 			k = 'ref_%saddrfile_chksum' % ('','key')[ftype == 'keyaddr']
 			m = grnbg('Checksum should be:')
@@ -2046,6 +2061,9 @@ class MMGenTestSuite(object):
 	def ref_passwdfile_chk(self,name):
 		self.ref_addrfile_chk(name,ftype='passwd')
 
+	def ref_segwitaddrfile_chk(self,name):
+		self.ref_addrfile_chk(name,ftype='segwitaddr')
+
 #	def txcreate8(self,name,addrfile):
 #		self.txcreate_common(name,sources=['8'])
 
@@ -2253,7 +2271,7 @@ try:
 		for arg in cmd_args:
 			if arg in utils:
 				globals()[arg](cmd_args[cmd_args.index(arg)+1:])
-				sys.exit()
+				sys.exit(0)
 			elif 'info_'+arg in cmd_data:
 				dirs = cmd_data['info_'+arg][1]
 				if dirs: clean(dirs)
@@ -2276,7 +2294,11 @@ try:
 			if cmd is not cmd_data.keys()[-1]: do_between()
 except KeyboardInterrupt:
 	die(1,'\nExiting at user request')
-	raise
+except opt.traceback and Exception:
+	with open('my.err') as f:
+		t = f.readlines()
+		if t: msg_r('\n'+yellow(''.join(t[:-1]))+red(t[-1]))
+	die(1,blue('Test script exited with error'))
 except:
 	sys.stderr = stderr_save
 	raise

+ 185 - 139
test/tooltest.py

@@ -20,7 +20,7 @@
 test/tooltest.py:  Tests for the 'mmgen-tool' utility
 """
 
-import sys,os
+import sys,os,subprocess
 pn = os.path.dirname(sys.argv[0])
 os.chdir(os.path.join(pn,os.pardir))
 sys.path.__setitem__(0,os.path.abspath(os.curdir))
@@ -33,52 +33,60 @@ cmd_data = OrderedDict([
 	('util', {
 			'desc': 'base conversion, hashing and file utilities',
 			'cmd_data': OrderedDict([
-				('strtob58',     ()),
-				('b58tostr',     ('strtob58','io')),
-				('hextob58',     ()),
-				('b58tohex',     ('hextob58','io')),
-				('b58randenc',   ()),
-				('hextob32',     ()),
-				('b32tohex',     ('hextob32','io')),
-				('randhex',      ()),
-				('id8',          ()),
-				('id6',          ()),
-				('str2id6',      ()),
-				('sha256x2',     ()),
-				('hexreverse',   ()),
-				('hexlify',      ()),
-				('hexdump',      ()),
-				('unhexdump',    ('hexdump','io')),
-				('rand2file',    ()),
+				('Strtob58',     ()),
+				('B58tostr',     ('Strtob58','io')),
+				('Hextob58',     ()),
+				('B58tohex',     ('Hextob58','io')),
+				('B58randenc',   ()),
+				('Hextob32',     ()),
+				('B32tohex',     ('Hextob32','io')),
+				('Randhex',      ()),
+				('Id6',          ()),
+				('Id8',          ()),
+				('Str2id6',      ()),
+				('Hash160',      ()),
+				('Hash256',      ()),
+				('Hexreverse',   ()),
+				('Hexlify',      ()),
+				('Hexdump',      ()),
+				('Unhexdump',    ('Hexdump','io')),
+				('Rand2file',    ()),
 			])
 		}
 	),
 	('bitcoin', {
 			'desc': 'Bitcoin address/key commands',
 			'cmd_data': OrderedDict([
-				('randwif',      ()),
-				('randpair',     ()),
-				('wif2addr',     ('randpair','o2')),
-				('wif2hex',      ('randpair','o2')),
-				('privhex2addr', ('wif2hex','o2')), # wif from randpair o2
-				('hex2wif',      ('wif2hex','io2')),
-				('addr2hexaddr', ('randpair','o2')),
-				('hexaddr2addr', ('addr2hexaddr','io2')),
-# ('pubkey2addr',  ['<public key in hex format> [str]']),
-# ('pubkey2hexaddr', ['<public key in hex format> [str]']),
+				('Randwif',        ()),
+				('Randpair',       ()), # create 3 pairs: uncomp,comp,segwit
+				('Wif2addr',       ('Randpair','o3')),
+				('Wif2hex',        ('Randpair','o3')),
+
+				('Privhex2pubhex', ('Wif2hex','o3')),
+				('Pubhex2addr',    ('Privhex2pubhex','o3')),
+				('Pubhex2redeem_script', ('Privhex2pubhex','o3')),
+				('Wif2redeem_script', ('Randpair','o3')),
+				('Wif2segwit_pair',   ('Randpair','o2')),
+
+				('Privhex2addr',   ('Wif2hex','o3')), # compare with output of Randpair
+				('Hex2wif',        ('Wif2hex','io2')),
+				('Addr2hexaddr',   ('Randpair','o2')),
+				('Hexaddr2addr',   ('Addr2hexaddr','io2')),
+
+				('Pipetest',       ('Randpair','o3')),
 			])
 		}
 	),
 	('mnemonic', {
 			'desc': 'mnemonic commands',
 			'cmd_data': OrderedDict([
-				('hex2mn',       ()),
-				('mn2hex',       ('hex2mn','io3')),
-				('mn_rand128',   ()),
-				('mn_rand192',   ()),
-				('mn_rand256',   ()),
-				('mn_stats',     ()),
-				('mn_printlist', ()),
+				('Hex2mn',       ()),
+				('Mn2hex',       ('Hex2mn','io3')),
+				('Mn_rand128',   ()),
+				('Mn_rand192',   ()),
+				('Mn_rand256',   ()),
+				('Mn_stats',     ()),
+				('Mn_printlist', ()),
 			])
 		}
 	),
@@ -86,11 +94,11 @@ cmd_data = OrderedDict([
 			'desc': 'Bitcoind RPC commands',
 			'cmd_data': OrderedDict([
 #				('keyaddrfile_chksum', ()), # interactive
-				('addrfile_chksum', ()),
-				('getbalance',      ()),
-				('listaddresses',   ()),
-				('twview',          ()),
-				('txview',          ()),
+				('Addrfile_chksum', ()),
+				('Getbalance',      ()),
+				('Listaddresses',   ()),
+				('Twview',          ()),
+				('Txview',          ()),
 			])
 		}
 	),
@@ -113,7 +121,8 @@ opts_data = {
 	'options': """
 -h, --help          Print this help message
 --, --longhelp      Print help message for long options (common options)
--l, --list-cmds     List and describe the tests and commands in the test suite
+-l, --list-cmds     List and describe the tests and commands in this test suite
+-L, --list-names    List the names of all tested 'mmgen-tool' commands
 -s, --system        Test scripts and modules installed on system rather than
                     those in the repo root
 -v, --verbose       Produce more verbose output
@@ -127,10 +136,11 @@ If no command is given, the whole suite of tests is run.
 sys.argv = [sys.argv[0]] + ['--skip-cfg-file'] + sys.argv[1:]
 
 cmd_args = opts.init(opts_data,add_opts=['exact_output','profile'])
+spawn_cmd = ['python',os.path.join(os.curdir,'mmgen-tool') if not opt.system else 'mmgen-tool']
 add_spawn_args = ' '.join(['{} {}'.format(
 	'--'+k.replace('_','-'),
 	getattr(opt,k) if getattr(opt,k) != True else ''
-	) for k in 'testnet','rpc_host' if getattr(opt,k)]).split()
+	) for k in 'testnet','rpc_host','regtest' if getattr(opt,k)]).split()
 add_spawn_args += [ '--data-dir', cfg['tmpdir']] # ignore ~/.mmgen
 
 if opt.system: sys.path.pop(0)
@@ -143,12 +153,29 @@ if opt.list_cmds:
 		Msg(fs.format(cmd,cmd_data[cmd]['desc'],w=w))
 	Msg('\nAvailable utilities:')
 	Msg(fs.format('clean','Clean the tmp directory',w=w))
-	sys.exit()
+	sys.exit(0)
+if opt.list_names:
+	acc = []
+	for v in cmd_data.values():
+		acc += v['cmd_data'].keys()
+	tc = sorted(c.lower() for c in acc)
+	msg('{}\n{}'.format(green('Tested commands:'),'\n'.join(tc)))
+	import mmgen.tool
+	tested_in_tool = ('Encrypt','Decrypt','Find_incog_data','Keyaddrfile_chksum','Passwdfile_chksum')
+	ignore = ('Help','Usage')
+	uc = sorted(c.lower() for c in set(mmgen.tool.cmd_data.keys()) - set(acc) - set(ignore) - set(tested_in_tool))
+	msg('\n{}\n{}'.format(yellow('Untested commands:'),'\n'.join(uc)))
+	die()
 
 import binascii
 from mmgen.test import *
 from mmgen.tx import is_wif,is_btc_addr
 
+msg_w = 35
+def test_msg(m):
+	m2 = 'Testing {}'.format(m)
+	msg_r(green(m2+'\n') if opt.verbose else '{:{w}}'.format(m2,w=msg_w+8))
+
 class MMGenToolTestSuite(object):
 
 	def __init__(self):
@@ -179,35 +206,28 @@ class MMGenToolTestSuite(object):
 		for cmd in cdata: self.do_cmd(cmd,cdata[cmd])
 
 	def do_cmd(self,cmd,cdata):
-
 		fns = self.gen_deps_for_cmd(cmd,cdata)
-
 		file_list = [os.path.join(cfg['tmpdir'],fn) for fn in fns]
-
 		self.__class__.__dict__[cmd](*([self,cmd] + file_list))
 
-
 	def run_cmd(self,name,tool_args,kwargs='',extra_msg='',silent=False,strip=True):
-		mmgen_tool = 'mmgen-tool'
-		if not opt.system:
-			mmgen_tool = os.path.join(os.curdir,mmgen_tool)
-
-		sys_cmd = ['python',mmgen_tool] + add_spawn_args + ['-r0','-d',cfg['tmpdir'],name] + tool_args + kwargs.split()
-		if extra_msg: extra_msg = '(%s)' % extra_msg
-		full_name = ' '.join([name]+kwargs.split()+extra_msg.split())
+		sys_cmd = (
+			spawn_cmd +
+			add_spawn_args +
+			['-r0','-d',cfg['tmpdir'],name.lower()] +
+			tool_args +
+			kwargs.split()
+		)
+		if extra_msg: extra_msg = '({})'.format(extra_msg)
+		full_name = ' '.join([name.lower()]+kwargs.split()+extra_msg.split())
 		if not silent:
 			if opt.verbose:
-				sys.stderr.write(green('Testing %s\nExecuting ' % full_name))
-				sys.stderr.write('%s\n' % cyan(repr(sys_cmd)))
+				sys.stderr.write(green('Testing {}\nExecuting '.format(full_name)))
+				sys.stderr.write(cyan(' '.join(sys_cmd)+'\n'))
 			else:
-				msg_r('Testing %-31s%s' % (full_name+':',''))
-
-		import subprocess
-		p = subprocess.Popen(
-			sys_cmd,
-			stdout=subprocess.PIPE,
-			stderr=subprocess.PIPE,
-			)
+				msg_r('Testing {:{w}}'.format(full_name+':',w=msg_w))
+
+		p = subprocess.Popen(sys_cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE)
 		a,b = p.communicate()
 		retcode = p.wait()
 		if retcode != 0:
@@ -234,18 +254,19 @@ class MMGenToolTestSuite(object):
 		vmsg('Out:  ' + repr(ret))
 		return ret
 
-	def run_cmd_out(self,name,carg=None,Return=False,kwargs='',fn_idx='',extra_msg='',literal=False,chkdata=''):
+	def run_cmd_out(self,name,carg=None,Return=False,kwargs='',fn_idx='',extra_msg='',literal=False,chkdata='',hush=False):
 		if carg: write_to_tmpfile(cfg,'%s%s.in' % (name,fn_idx),carg+'\n')
 		ret = self.run_cmd(name,([],[carg])[bool(carg)],kwargs=kwargs,extra_msg=extra_msg)
 		if carg: vmsg('In:   ' + repr(carg))
-		vmsg('Out:  ' + (repr(ret),ret)[literal])
+		vmsg('Out:  ' + (repr(ret),ret.decode('utf8'))[literal])
 		if ret or ret == '':
 			write_to_tmpfile(cfg,'%s%s.out' % (name,fn_idx),ret+'\n')
 			if chkdata:
 				cmp_or_die(ret,chkdata)
 				return
 			if Return: return ret
-			else:   ok()
+			else:
+				if not hush: ok()
 		else:
 			die(3,red("Error for command '%s'" % name))
 
@@ -259,115 +280,141 @@ class MMGenToolTestSuite(object):
 		ok()
 		vmsg('Returned: %s' % ret)
 
-	def str2id6(self,name):
+	# Util
+	def Strtob58(self,name):       self.run_cmd_out(name,getrandstr(16))
+	def B58tostr(self,name,f1,f2): self.run_cmd_chk(name,f1,f2)
+	def Hextob58(self,name):       self.run_cmd_out(name,getrandhex(32))
+	def B58tohex(self,name,f1,f2): self.run_cmd_chk(name,f1,f2)
+	def B58randenc(self,name):
+		ret = self.run_cmd_out(name,Return=True)
+		ok_or_die(ret,is_b58_str,'base 58 string')
+	def Hextob32(self,name):       self.run_cmd_out(name,getrandhex(24))
+	def B32tohex(self,name,f1,f2): self.run_cmd_chk(name,f1,f2)
+	def Randhex(self,name):
+		ret = self.run_cmd_out(name,Return=True)
+		ok_or_die(ret,binascii.unhexlify,'hex string')
+	def Id6(self,name):     self.run_cmd_randinput(name)
+	def Id8(self,name):     self.run_cmd_randinput(name)
+	def Str2id6(self,name):
 		s = getrandstr(120,no_space=True)
 		s2 = ' %s %s %s %s %s ' % (s[:3],s[3:9],s[9:29],s[29:50],s[50:120])
 		ret1 = self.run_cmd(name,[s],extra_msg='unspaced input'); ok()
 		ret2 = self.run_cmd(name,[s2],extra_msg='spaced input')
 		cmp_or_die(ret1,ret2)
 		vmsg('Returned: %s' % ret1)
-
-	def mn_rand128(self,name):
-		self.run_cmd_out(name)
-
-	def mn_rand192(self,name):
-		self.run_cmd_out(name)
-
-	def mn_rand256(self,name):
-		self.run_cmd_out(name)
-
-	def mn_stats(self,name):
-		self.run_cmd_out(name)
-
-	def mn_printlist(self,name):
-		self.run_cmd(name,[])
-		ok()
-
-	def id6(self,name):     self.run_cmd_randinput(name)
-	def id8(self,name):     self.run_cmd_randinput(name)
-	def hexdump(self,name): self.run_cmd_randinput(name,strip=False)
-
-	def unhexdump(self,name,fn1,fn2):
+	def Hash160(self,name):        self.run_cmd_out(name,getrandhex(16))
+	def Hash256(self,name):        self.run_cmd_out(name,getrandstr(16))
+	def Hexreverse(self,name):     self.run_cmd_out(name,getrandhex(24))
+	def Hexlify(self,name):        self.run_cmd_out(name,getrandstr(24))
+	def Hexdump(self,name): self.run_cmd_randinput(name,strip=False)
+	def Unhexdump(self,name,fn1,fn2):
 		ret = self.run_cmd(name,[fn2],strip=False)
 		orig = read_from_file(fn1,binary=True)
 		cmp_or_die(orig,ret)
-
-	def rand2file(self,name):
+	def Rand2file(self,name):
 		of = name + '.out'
 		dlen = 1024
 		self.run_cmd(name,[of,str(1024),'threads=4','silent=1'],strip=False)
 		d = read_from_tmpfile(cfg,of,binary=True)
 		cmp_or_die(dlen,len(d))
 
-	def strtob58(self,name):       self.run_cmd_out(name,getrandstr(16))
-	def sha256x2(self,name):       self.run_cmd_out(name,getrandstr(16))
-	def hexreverse(self,name):     self.run_cmd_out(name,getrandhex(24))
-	def hexlify(self,name):        self.run_cmd_out(name,getrandstr(24))
-	def b58tostr(self,name,f1,f2): self.run_cmd_chk(name,f1,f2)
-	def hextob58(self,name):       self.run_cmd_out(name,getrandhex(32))
-	def b58tohex(self,name,f1,f2): self.run_cmd_chk(name,f1,f2)
-	def hextob32(self,name):       self.run_cmd_out(name,getrandhex(24))
-	def b32tohex(self,name,f1,f2): self.run_cmd_chk(name,f1,f2)
-	def b58randenc(self,name):
-		ret = self.run_cmd_out(name,Return=True)
-		ok_or_die(ret,is_b58_str,'base 58 string')
-	def randhex(self,name):
-		ret = self.run_cmd_out(name,Return=True)
-		ok_or_die(ret,binascii.unhexlify,'hex string')
-	def randwif(self,name):
+	# Bitcoin
+	def Randwif(self,name):
 		for n,k in enumerate(['','compressed=1']):
 			ret = self.run_cmd_out(name,kwargs=k,Return=True,fn_idx=n+1)
 			ok_or_die(ret,is_wif,'WIF key')
-	def randpair(self,name):
-		for n,k in enumerate(['','compressed=1']):
+	def Randpair(self,name):
+		for n,k in enumerate(['','compressed=1','segwit=1 compressed=1']):
 			wif,addr = self.run_cmd_out(name,kwargs=k,Return=True,fn_idx=n+1).split()
 			ok_or_die(wif,is_wif,'WIF key',skip_ok=True)
 			ok_or_die(addr,is_btc_addr,'Bitcoin address')
-	def hex2wif(self,name,f1,f2,f3,f4):
-		for n,fi,fo,k in (1,f1,f2,''),(2,f3,f4,'compressed=1'):
-			ret = self.run_cmd_chk(name,fi,fo,kwargs=k)
-	def wif2hex(self,name,f1,f2):
-		for n,f,k in (1,f1,''),(2,f2,'compressed=1'):
+	def Wif2addr(self,name,f1,f2,f3):
+		for n,f,k,m in (1,f1,'',''),(2,f2,'','compressed'),(3,f3,'segwit=1','compressed'):
 			wif = read_from_file(f).split()[0]
-			self.run_cmd_out(name,wif,kwargs=k,fn_idx=n)
-	def wif2addr(self,name,f1,f2):
-		for n,f,k in (1,f1,''),(2,f2,'compressed=1'):
+			self.run_cmd_out(name,wif,kwargs=k,fn_idx=n,extra_msg=m)
+	def Wif2hex(self,name,f1,f2,f3):
+		for n,f,m in (1,f1,''),(2,f2,'compressed'),(3,f3,'compressed for segwit'):
 			wif = read_from_file(f).split()[0]
-			self.run_cmd_out(name,wif,kwargs=k,fn_idx=n)
-	def addr2hexaddr(self,name,f1,f2):
+			self.run_cmd_out(name,wif,fn_idx=n,extra_msg=m)
+	def Privhex2addr(self,name,f1,f2,f3):
+		keys = [read_from_file(f).rstrip() for f in f1,f2,f3]
+		for n,k in enumerate(('','compressed=1','compressed=1 segwit=1')):
+			ret = self.run_cmd(name,[keys[n]],kwargs=k).rstrip()
+			iaddr = read_from_tmpfile(cfg,'Randpair{}.out'.format(n+1)).split()[-1]
+			cmp_or_die(iaddr,ret)
+	def Hex2wif(self,name,f1,f2,f3,f4):
+		for n,fi,fo,k in (1,f1,f2,''),(2,f3,f4,'compressed=1'):
+			ret = self.run_cmd_chk(name,fi,fo,kwargs=k)
+	def Addr2hexaddr(self,name,f1,f2):
 		for n,f,m in (1,f1,''),(2,f2,'from compressed'):
 			addr = read_from_file(f).split()[-1]
 			self.run_cmd_out(name,addr,fn_idx=n,extra_msg=m)
-	def hexaddr2addr(self,name,f1,f2,f3,f4):
+	def Hexaddr2addr(self,name,f1,f2,f3,f4):
 		for n,fi,fo,m in (1,f1,f2,''),(2,f3,f4,'from compressed'):
 			self.run_cmd_chk(name,fi,fo,extra_msg=m)
-	def privhex2addr(self,name,f1,f2):
-		key1 = read_from_file(f1).rstrip()
-		key2 = read_from_file(f2).rstrip()
-		for n,args in enumerate([[key1],[key2,'compressed=1']]):
-			ret = self.run_cmd(name,args).rstrip()
-			iaddr = read_from_tmpfile(cfg,'randpair%s.out' % (n+1)).split()[-1]
-			cmp_or_die(iaddr,ret)
-	def hex2mn(self,name):
+	def Privhex2pubhex(self,name,f1,f2,f3): # from hex2wif
+		addr = read_from_file(f3).strip()
+		self.run_cmd_out(name,addr,kwargs='compressed=1',fn_idx=3)
+	def Pubhex2redeem_script(self,name,f1,f2,f3): # from above
+		addr = read_from_file(f3).strip()
+		self.run_cmd_out(name,addr,fn_idx=3)
+		rs = read_from_tmpfile(cfg,name+'3.out').strip()
+		self.run_cmd_out('pubhex2addr',rs,kwargs='p2sh=1',fn_idx=3,hush=True)
+		addr1 = read_from_tmpfile(cfg,'pubhex2addr3.out').strip()
+		addr2 = read_from_tmpfile(cfg,'Randpair3.out').split()[1]
+		cmp_or_die(addr1,addr2)
+	def Wif2redeem_script(self,name,f1,f2,f3): # compare output with above
+		wif = read_from_file(f3).split()[0]
+		ret1 = self.run_cmd_out(name,wif,fn_idx=3,Return=True)
+		ret2 = read_from_tmpfile(cfg,'Pubhex2redeem_script3.out').strip()
+		cmp_or_die(ret1,ret2)
+	def Wif2segwit_pair(self,name,f1,f2): # does its own checking, so just run
+		wif = read_from_file(f2).split()[0]
+		self.run_cmd_out(name,wif,fn_idx=2)
+
+	def Pubhex2addr(self,name,f1,f2,f3):
+		addr = read_from_file(f3).strip()
+		self.run_cmd_out(name,addr,fn_idx=3)
+
+	def Pipetest(self,name,f1,f2,f3):
+		test_msg('command piping')
+		wif = read_from_file(f3).split()[0]
+		cmd = '{tc} {sa} wif2hex {wif} | {tc} privhex2pubhex - compressed=1 | {tc} pubhex2redeem_script - | {tc} {sa} pubhex2addr - p2sh=1'.format(wif=wif,sa=' '.join(add_spawn_args),tc=' '.join(spawn_cmd))
+		p = subprocess.Popen(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE,shell=True)
+		res = p.stdout.read().strip()
+		addr = read_from_tmpfile(cfg,'Wif2addr3.out').strip()
+		cmp_or_die(res,addr)
+
+
+	# Mnemonic
+	def Hex2mn(self,name):
 		for n,size,m in(1,16,'128-bit'),(2,24,'192-bit'),(3,32,'256-bit'):
 			hexnum = getrandhex(size)
 			self.run_cmd_out(name,hexnum,fn_idx=n,extra_msg=m)
-	def mn2hex(self,name,f1,f2,f3,f4,f5,f6):
+	def Mn2hex(self,name,f1,f2,f3,f4,f5,f6):
 		for f_i,f_o,m in (f1,f2,'128-bit'),(f3,f4,'192-bit'),(f5,f6,'256-bit'):
 			self.run_cmd_chk(name,f_i,f_o,extra_msg=m)
+	def Mn_rand128(self,name): self.run_cmd_out(name)
+	def Mn_rand192(self,name): self.run_cmd_out(name)
+	def Mn_rand256(self,name): self.run_cmd_out(name)
+	def Mn_stats(self,name):   self.run_cmd_out(name)
+	def Mn_printlist(self,name):
+		self.run_cmd(name,[])
+		ok()
 
-	def getbalance(self,name):
+	# RPC
+	def Addrfile_chksum(self,name):
+		fn = os.path.join(cfg['refdir'],cfg['addrfile'])
+		self.run_cmd_out(name,fn,literal=True,chkdata=cfg['addrfile_chk'])
+	def Getbalance(self,name):
 		self.run_cmd_out(name,literal=True)
-	def listaddresses(self,name):
+	def Listaddresses(self,name):
 		self.run_cmd_out(name,literal=True)
-	def twview(self,name):
+	def Twview(self,name):
 		self.run_cmd_out(name,literal=True)
-	def txview(self,name):
+	def Txview(self,name):
 		fn = os.path.join(cfg['refdir'],cfg['txfile'])
 		self.run_cmd_out(name,fn,literal=True)
-	def addrfile_chksum(self,name):
-		fn = os.path.join(cfg['refdir'],cfg['addrfile'])
-		self.run_cmd_out(name,fn,literal=True,chkdata=cfg['addrfile_chk'])
 
 # main()
 import time
@@ -378,14 +425,13 @@ mk_tmpdir(cfg['tmpdir'])
 if cmd_args:
 	if len(cmd_args) != 1:
 		die(1,'Only one command may be specified')
-
 	cmd = cmd_args[0]
 	if cmd in cmd_data:
 		msg('Running tests for %s:' % cmd_data[cmd]['desc'])
 		ts.do_cmds(cmd)
 	elif cmd == 'clean':
 		cleandir(cfg['tmpdir'])
-		sys.exit()
+		sys.exit(0)
 	else:
 		die(1,"'%s': unrecognized command" % cmd)
 else: