Browse Source

Basic altcoin framework, full Litecoin support
- LTC: tested on mainnet, full test suite support
- simultaneous autosigning for all supported coins (BTC,BCH,LTC)

philemon 7 years ago
parent
commit
35d1091159
64 changed files with 2065 additions and 1105 deletions
  1. 1 0
      MANIFEST.in
  2. 1 0
      README.md
  3. 0 0
      cmds/mmgen-addrgen
  4. 0 0
      cmds/mmgen-addrimport
  5. 0 0
      cmds/mmgen-keygen
  6. 0 0
      cmds/mmgen-passchg
  7. 1 1
      cmds/mmgen-passgen
  8. 0 0
      cmds/mmgen-regtest
  9. 0 0
      cmds/mmgen-tool
  10. 0 0
      cmds/mmgen-txbump
  11. 0 0
      cmds/mmgen-txcreate
  12. 0 0
      cmds/mmgen-txdo
  13. 0 0
      cmds/mmgen-txsend
  14. 0 0
      cmds/mmgen-txsign
  15. 0 0
      cmds/mmgen-walletchk
  16. 0 0
      cmds/mmgen-walletconv
  17. 0 0
      cmds/mmgen-walletgen
  18. 11 5
      data_files/mmgen.cfg
  19. 60 39
      mmgen/addr.py
  20. 68 5
      mmgen/common.py
  21. 14 16
      mmgen/globalvars.py
  22. 8 5
      mmgen/main_addrgen.py
  23. 54 55
      mmgen/main_addrimport.py
  24. 7 6
      mmgen/main_passgen.py
  25. 4 3
      mmgen/main_regtest.py
  26. 19 14
      mmgen/main_tool.py
  27. 15 11
      mmgen/main_txbump.py
  28. 8 7
      mmgen/main_txcreate.py
  29. 13 11
      mmgen/main_txdo.py
  30. 8 7
      mmgen/main_txsend.py
  31. 11 8
      mmgen/main_txsign.py
  32. 5 5
      mmgen/main_wallet.py
  33. 36 29
      mmgen/obj.py
  34. 89 54
      mmgen/opts.py
  35. 97 50
      mmgen/protocol.py
  36. 22 18
      mmgen/regtest.py
  37. 24 20
      mmgen/rpc.py
  38. 185 162
      mmgen/tool.py
  39. 11 19
      mmgen/tw.py
  40. 187 157
      mmgen/tx.py
  41. 20 49
      mmgen/txcreate.py
  42. 5 31
      mmgen/txsign.py
  43. 43 35
      mmgen/util.py
  44. 80 44
      scripts/mmgen-autosign
  45. 137 37
      scripts/test-release.sh
  46. 1 1
      scripts/traceback.py
  47. 16 16
      setup.py
  48. 3 3
      test/gentest.py
  49. 14 9
      test/mmgen_pexpect.py
  50. 42 27
      test/objtest.py
  51. 6 0
      test/ref/99BE60-BCH[106.6789].rawtx
  52. 6 0
      test/ref/99BE60-BCH[106.6789].testnet.rawtx
  53. 6 0
      test/ref/litecoin/75F455-LTC[106.6789].rawtx
  54. 6 0
      test/ref/litecoin/75F455-LTC[106.6789].testnet.rawtx
  55. 19 0
      test/ref/litecoin/98831F3A-LTC-S[1,31-33,500-501,1010-1011].addrs
  56. 19 0
      test/ref/litecoin/98831F3A-LTC-S[1,31-33,500-501,1010-1011].testnet.addrs
  57. 19 0
      test/ref/litecoin/98831F3A-LTC[1,31-33,500-501,1010-1011].addrs
  58. BIN
      test/ref/litecoin/98831F3A-LTC[1,31-33,500-501,1010-1011].akeys.mmenc
  59. 19 0
      test/ref/litecoin/98831F3A-LTC[1,31-33,500-501,1010-1011].testnet.addrs
  60. BIN
      test/ref/litecoin/98831F3A-LTC[1,31-33,500-501,1010-1011].testnet.akeys.mmenc
  61. 109 0
      test/ref/litecoin/ltcwallet-testnet.dump
  62. 109 0
      test/ref/litecoin/ltcwallet.dump
  63. 380 121
      test/test.py
  64. 47 25
      test/tooltest.py

+ 1 - 0
MANIFEST.in

@@ -2,6 +2,7 @@ include README.md SIGNING_KEY.pub LICENSE INSTALL
 include doc/wiki/using-mmgen/*
 include doc/wiki/using-mmgen/*
 include test/*.py
 include test/*.py
 include test/ref/*
 include test/ref/*
+include test/ref/litecoin/*
 
 
 include scripts/bitcoind-walletunlock.py
 include scripts/bitcoind-walletunlock.py
 include scripts/compute-file-chksum.py
 include scripts/compute-file-chksum.py

+ 1 - 0
README.md

@@ -129,3 +129,4 @@ Donate: 15TLdmi5NYLdqmtCqczUs5pBPkJDXRs83w
 [9]: https://cloud.githubusercontent.com/assets/6071028/20677261/6ccab1bc-b58a-11e6-8ab6-094f88befef2.jpg
 [9]: https://cloud.githubusercontent.com/assets/6071028/20677261/6ccab1bc-b58a-11e6-8ab6-094f88befef2.jpg
 [r]: https://github.com/mmgen/mmgen/wiki/Recovering-Your-Keys-Without-the-MMGen-Software
 [r]: https://github.com/mmgen/mmgen/wiki/Recovering-Your-Keys-Without-the-MMGen-Software
 [x]: https://github.com/mmgen/mmgen/wiki/Getting-Started-with-MMGen#a_bch
 [x]: https://github.com/mmgen/mmgen/wiki/Getting-Started-with-MMGen#a_bch
+[z]: https://user-images.githubusercontent.com/6071028/31656274-a35a1252-b31a-11e7-93b7-3d666f50f70f.png

+ 0 - 0
mmgen-addrgen → cmds/mmgen-addrgen


+ 0 - 0
mmgen-addrimport → cmds/mmgen-addrimport


+ 0 - 0
mmgen-keygen → cmds/mmgen-keygen


+ 0 - 0
mmgen-passchg → cmds/mmgen-passchg


+ 1 - 1
mmgen-passgen → cmds/mmgen-passgen

@@ -17,7 +17,7 @@
 # this program.  If not, see <http://www.gnu.org/licenses/>.
 # this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
 """
 """
-mmgen-passgen: Generate a series or range of passwords from an MMGen
+mmgen-passgen: Generate a range or series of passwords from an MMGen
                deterministic wallet
                deterministic wallet
 """
 """
 
 

+ 0 - 0
mmgen-regtest → cmds/mmgen-regtest


+ 0 - 0
mmgen-tool → cmds/mmgen-tool


+ 0 - 0
mmgen-txbump → cmds/mmgen-txbump


+ 0 - 0
mmgen-txcreate → cmds/mmgen-txcreate


+ 0 - 0
mmgen-txdo → cmds/mmgen-txdo


+ 0 - 0
mmgen-txsend → cmds/mmgen-txsend


+ 0 - 0
mmgen-txsign → cmds/mmgen-txsign


+ 0 - 0
mmgen-walletchk → cmds/mmgen-walletchk


+ 0 - 0
mmgen-walletconv → cmds/mmgen-walletconv


+ 0 - 0
mmgen-walletgen → cmds/mmgen-walletgen


+ 11 - 5
data_files/mmgen.cfg

@@ -23,16 +23,16 @@
 # Uncomment to use testnet instead of mainnet:
 # Uncomment to use testnet instead of mainnet:
 # testnet true
 # testnet true
 
 
-# Set the RPC host (the host bitcoind is running on):
+# Set the RPC host (the host the coin daemon is running on):
 # rpc_host localhost
 # rpc_host localhost
 
 
 # Set the RPC host's port number
 # Set the RPC host's port number
 # rpc_port 8332
 # rpc_port 8332
 
 
-# Uncomment to override 'rpcuser' in bitcoin.conf
+# Uncomment to override 'rpcuser' from coin daemon config file
 # rpc_user myusername
 # rpc_user myusername
 
 
-# Uncomment to override 'rpcpassword' in bitcoin.conf
+# Uncomment to override 'rpcpassword' from coin daemon config file
 # rpc_password mypassword
 # rpc_password mypassword
 
 
 # Uncomment to set the coin daemon datadir
 # Uncomment to set the coin daemon datadir
@@ -46,8 +46,14 @@
 # A value of 0 disables user entropy, but this is not recommended:
 # A value of 0 disables user entropy, but this is not recommended:
 # usr_randchars 30
 # usr_randchars 30
 
 
-# Set the maximum transaction fee in BTC:
-# max_tx_fee 0.01
+# Set the maximum transaction fee for BTC:
+# btc_max_tx_fee 0.01
+
+# Set the maximum transaction fee for BCH:
+# bch_max_tx_fee 0.1
+
+# Set the maximum transaction fee for LTC:
+# ltc_max_tx_fee 0.3
 
 
 # Set the transaction fee adjustment factor. Auto-calculated fees are
 # Set the transaction fee adjustment factor. Auto-calculated fees are
 # multiplied by this value:
 # multiplied by this value:

+ 60 - 39
mmgen/addr.py

@@ -41,7 +41,7 @@ class AddrGeneratorP2PKH(AddrGenerator):
 	def to_addr(self,pubhex):
 	def to_addr(self,pubhex):
 		from mmgen.protocol import hash160
 		from mmgen.protocol import hash160
 		assert type(pubhex) == PubKey
 		assert type(pubhex) == PubKey
-		return CoinAddr(g.proto.hexaddr2addr(hash160(pubhex)))
+		return CoinAddr(g.proto.hexaddr2addr(hash160(pubhex),p2sh=False))
 
 
 	def to_segwit_redeem_script(self,pubhex):
 	def to_segwit_redeem_script(self,pubhex):
 		raise NotImplementedError
 		raise NotImplementedError
@@ -160,10 +160,9 @@ class AddrListIDStr(unicode,Hilite):
 
 
 		if fmt_str:
 		if fmt_str:
 			ret = fmt_str.format(s)
 			ret = fmt_str.format(s)
-		elif addrlist.al_id.mmtype == 'L':
-			ret = '{}[{}]'.format(addrlist.al_id.sid,s)
 		else:
 		else:
-			ret = '{}-{}[{}]'.format(addrlist.al_id.sid,addrlist.al_id.mmtype,s)
+			bc,mt = g.proto.base_coin,addrlist.al_id.mmtype
+			ret = '{}{}{}[{}]'.format(addrlist.al_id.sid,('-'+bc,'')[bc=='BTC'],('-'+mt,'')[mt=='L'],s)
 
 
 		return unicode.__new__(cls,ret)
 		return unicode.__new__(cls,ret)
 
 
@@ -175,7 +174,7 @@ class AddrList(MMGenObject): # Address info for a single seed ID
 # This file is editable.
 # This file is editable.
 # Everything following a hash symbol '#' is a comment and ignored by {pnm}.
 # Everything following a hash symbol '#' is a comment and ignored by {pnm}.
 # A text label of {n} characters or less may be added to the right of each
 # 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.
+# address, and it will be appended to the tracking wallet label upon import.
 # The label may contain any printable ASCII symbol.
 # The label may contain any printable ASCII symbol.
 """.strip().format(n=TwComment.max_len,pnm=pnm),
 """.strip().format(n=TwComment.max_len,pnm=pnm),
 	'record_chksum': """
 	'record_chksum': """
@@ -187,7 +186,7 @@ Removed %s duplicate WIF key%s from keylist (also in {pnm} key-address file
 """.strip().format(pnm=pnm)
 """.strip().format(pnm=pnm)
 	}
 	}
 	entry_type = AddrListEntry
 	entry_type = AddrListEntry
-	main_key  = 'addr'
+	main_attr = 'addr'
 	data_desc = 'address'
 	data_desc = 'address'
 	file_desc = 'addresses'
 	file_desc = 'addresses'
 	gen_desc  = 'address'
 	gen_desc  = 'address'
@@ -197,7 +196,6 @@ Removed %s duplicate WIF key%s from keylist (also in {pnm} key-address file
 	gen_keys = False
 	gen_keys = False
 	has_keys = False
 	has_keys = False
 	ext      = 'addrs'
 	ext      = 'addrs'
-	dfl_mmtype = MMGenAddrType('L')
 	cook_hash_rounds = 10  # not too many rounds, so hand decoding can still be feasible
 	cook_hash_rounds = 10  # not too many rounds, so hand decoding can still be feasible
 	chksum_rec_f = lambda foo,e: (str(e.idx), e.addr)
 	chksum_rec_f = lambda foo,e: (str(e.idx), e.addr)
 
 
@@ -205,7 +203,7 @@ Removed %s duplicate WIF key%s from keylist (also in {pnm} key-address file
 					addrlist='',keylist='',mmtype=None,do_chksum=True,chksum_only=False):
 					addrlist='',keylist='',mmtype=None,do_chksum=True,chksum_only=False):
 
 
 		self.update_msgs()
 		self.update_msgs()
-		mmtype = mmtype or self.dfl_mmtype
+		mmtype = mmtype or MMGenAddrType.dfl_mmtype
 		assert mmtype in MMGenAddrType.mmtypes
 		assert mmtype in MMGenAddrType.mmtypes
 
 
 		if seed and addr_idxs:   # data from seed + idxs
 		if seed and addr_idxs:   # data from seed + idxs
@@ -296,21 +294,16 @@ Removed %s duplicate WIF key%s from keylist (also in {pnm} key-address file
 				self.al_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
 		return out
 
 
-	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 check_format(self,addr): return True # format is checked when added to list entry object
 	def check_format(self,addr): return True # format is checked when added to list entry object
 
 
 	def cook_seed(self,seed):
 	def cook_seed(self,seed):
-		if self.al_id.mmtype == 'L':
+		is_btcfork = g.proto.base_coin == 'BTC'
+		if is_btcfork and self.al_id.mmtype == 'L':
 			return seed
 			return seed
 		else:
 		else:
 			from mmgen.crypto import sha256_rounds
 			from mmgen.crypto import sha256_rounds
 			import hmac
 			import hmac
-			key = self.al_id.mmtype.name
+			key = (g.coin.lower()+':','')[is_btcfork] + self.al_id.mmtype.name
 			cseed = hmac.new(seed,key,sha256).digest()
 			cseed = hmac.new(seed,key,sha256).digest()
 			dmsg('Seed:  {}\nKey: {}\nCseed: {}\nCseed len: {}'.format(hexlify(seed),key,hexlify(cseed),len(cseed)))
 			dmsg('Seed:  {}\nKey: {}\nCseed: {}\nCseed len: {}'.format(hexlify(seed),key,hexlify(cseed),len(cseed)))
 			return sha256_rounds(cseed,self.cook_hash_rounds)
 			return sha256_rounds(cseed,self.cook_hash_rounds)
@@ -406,10 +399,10 @@ Removed %s duplicate WIF key%s from keylist (also in {pnm} key-address file
 		if type(self) == PasswordList:
 		if type(self) == PasswordList:
 			out.append(u'{} {} {}:{} {{'.format(
 			out.append(u'{} {} {}:{} {{'.format(
 				self.al_id.sid,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:
 		else:
-			out.append('{} {} {{'.format(self.al_id.sid,self.al_id.mmtype.name.upper()))
+			bc,mt = g.proto.base_coin,self.al_id.mmtype
+			lbl = ':'.join(([bc],[])[bc=='BTC']+([mt.name.upper()],[])[mt=='L'])
+			out.append('{} {}{{'.format(self.al_id.sid,('',lbl+' ')[bool(lbl)]))
 
 
 		fs = '  {:<%s}  {:<34}{}' % len(str(self.data[-1].idx))
 		fs = '  {:<%s}  {:<34}{}' % len(str(self.data[-1].idx))
 		for e in self.data:
 		for e in self.data:
@@ -447,16 +440,16 @@ Removed %s duplicate WIF key%s from keylist (also in {pnm} key-address file
 
 
 			if len(d) != 3: d.append('')
 			if len(d) != 3: d.append('')
 
 
-			a = le(**{'idx':int(d[0]),self.main_key:d[1],'label':d[2]})
+			a = le(**{'idx':int(d[0]),self.main_attr:d[1],'label':d[2]})
 
 
 			if self.has_keys:
 			if self.has_keys:
 				l = lines.pop(0)
 				l = lines.pop(0)
 				d = l.split(None,2)
 				d = l.split(None,2)
 
 
 				if d[0] != 'wif:':
 				if d[0] != 'wif:':
-					return "Invalid key line in file: '%s'" % l
+					return "Invalid key line in file: '{}'".format(l)
 				if not is_wif(d[1]):
 				if not is_wif(d[1]):
-					return "'%s': invalid Bitcoin key" % d[1]
+					return "'{}': invalid {} key".format(d[1],g.proto.name.capitalize())
 
 
 				a.sec = PrivKey(wif=d[1])
 				a.sec = PrivKey(wif=d[1])
 
 
@@ -498,6 +491,38 @@ Removed %s duplicate WIF key%s from keylist (also in {pnm} key-address file
 		if not is_mmgen_seed_id(sid):
 		if not is_mmgen_seed_id(sid):
 			return do_error("'%s': invalid Seed ID" % ls[0])
 			return do_error("'%s': invalid Seed ID" % ls[0])
 
 
+		def parse_addrfile_label(lbl): # we must maintain backwards compat, so parse is tricky
+			al_base_coin,al_mmtype = None,None
+			lbl = lbl.split(':',1)
+			if len(lbl) == 2:
+				al_base_coin = lbl[0]
+				al_mmtype    = lbl[1].lower()
+			else:
+				if lbl[0].lower() in MMGenAddrType.get_names():
+					al_mmtype = lbl[0].lower()
+				else:
+					al_base_coin = lbl[0]
+
+			# this block fails if al_mmtype is invalid for g.coin
+			if not al_mmtype:
+				mmtype = MMGenAddrType('L')
+			else:
+				try:
+					mmtype = MMGenAddrType(al_mmtype)
+				except:
+					return do_error(u"'{}': invalid address type in address file. Must be one of: {}".format(
+						mmtype.upper(),' '.join([i['name'].upper() for i in MMGenAddrType.mmtypes.values()])))
+
+			from mmgen.protocol import CoinProtocol
+			base_coin = CoinProtocol(al_base_coin or 'BTC',testnet=False).base_coin
+			if not base_coin:
+				die(2,"'{}': unknown base coin in address file label!".format(al_base_coin))
+			return base_coin,mmtype
+
+		def check_coin_mismatch(base_coin): # die if addrfile coin doesn't match g.coin
+			if not base_coin == g.proto.base_coin:
+				die(2,'{} address file format, but base coin is {}!'.format(base_coin,g.proto.base_coin))
+
 		if type(self) == PasswordList and len(ls) == 2:
 		if type(self) == PasswordList and len(ls) == 2:
 			ss = ls.pop().split(':')
 			ss = ls.pop().split(':')
 			if len(ss) != 2:
 			if len(ss) != 2:
@@ -507,14 +532,11 @@ Removed %s duplicate WIF key%s from keylist (also in {pnm} key-address file
 			self.pw_id_str = MMGenPWIDString(ls.pop())
 			self.pw_id_str = MMGenPWIDString(ls.pop())
 			mmtype = MMGenPasswordType('P')
 			mmtype = MMGenPasswordType('P')
 		elif len(ls) == 1:
 		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.upper(),' '.join([i['name'].upper() for i in MMGenAddrType.mmtypes.values()])))
+			base_coin,mmtype = parse_addrfile_label(ls[0])
+			check_coin_mismatch(base_coin)
 		elif len(ls) == 0:
 		elif len(ls) == 0:
-			mmtype = MMGenAddrType('L')
+			base_coin,mmtype = 'BTC',MMGenAddrType('L')
+			check_coin_mismatch(base_coin)
 		else:
 		else:
 			return do_error(u"Invalid first line for {} file: '{}'".format(self.gen_desc,lines[0]))
 			return do_error(u"Invalid first line for {} file: '{}'".format(self.gen_desc,lines[0]))
 
 
@@ -572,7 +594,7 @@ Record this checksum: it will be used to verify the password file in the future
 """.strip()
 """.strip()
 	}
 	}
 	entry_type  = PasswordListEntry
 	entry_type  = PasswordListEntry
-	main_key    = 'passwd'
+	main_attr   = 'passwd'
 	data_desc   = 'password'
 	data_desc   = 'password'
 	file_desc   = 'passwords'
 	file_desc   = 'passwords'
 	gen_desc    = 'password'
 	gen_desc    = 'password'
@@ -666,14 +688,14 @@ Record this checksum: it will be used to verify the password file in the future
 
 
 	def cook_seed(self,seed):
 	def cook_seed(self,seed):
 		from mmgen.crypto import sha256_rounds
 		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
+		# Changing either pw_fmt, pw_len or cook_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'
 		# 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
+		cook_str = '{}:{}:{}'.format(self.pw_fmt,self.pw_len,self.pw_id_str.encode('utf8'))
+		dmsg(u'Full ID string: {}'.format(cook_str.decode('utf8')))
+		# Original implementation was 'cseed = seed + cook_str'; hmac was not used
 		import hmac
 		import hmac
-		cseed = hmac.new(seed,fid_str,sha256).digest()
+		cseed = hmac.new(seed,cook_str,sha256).digest()
 		dmsg('Seed: {}\nCooked seed: {}\nCooked seed len: {}'.format(hexlify(seed),hexlify(cseed),len(cseed)))
 		dmsg('Seed: {}\nCooked seed: {}\nCooked seed len: {}'.format(hexlify(seed),hexlify(cseed),len(cseed)))
 		return sha256_rounds(cseed,self.cook_hash_rounds)
 		return sha256_rounds(cseed,self.cook_hash_rounds)
 
 
@@ -712,10 +734,9 @@ re-import your addresses.
 
 
 	def add_tw_data(self):
 	def add_tw_data(self):
 		vmsg('Getting address data from tracking wallet')
 		vmsg('Getting address data from tracking wallet')
-		c = rpc_connection()
-		accts = c.listaccounts(0,True)
+		accts = g.rpch.listaccounts(0,True)
 		data,i = {},0
 		data,i = {},0
-		alists = c.getaddressesbyaccount([[k] for k in accts],batch=True)
+		alists = g.rpch.getaddressesbyaccount([[k] for k in accts],batch=True)
 		for acct,addrlist in zip(accts,alists):
 		for acct,addrlist in zip(accts,alists):
 			l = TwLabel(acct,on_fail='silent')
 			l = TwLabel(acct,on_fail='silent')
 			if l and l.mmid.type == 'mmgen':
 			if l and l.mmid.type == 'mmgen':

+ 68 - 5
mmgen/common.py

@@ -26,16 +26,79 @@ import mmgen.opts as opts
 from mmgen.opts import opt
 from mmgen.opts import opt
 from mmgen.util import *
 from mmgen.util import *
 
 
-pw_note = """
+def help_notes(k):
+	from mmgen.seed import SeedSource
+	return {
+		'passwd': """
 For passphrases all combinations of whitespace are equal and leading and
 For passphrases all combinations of whitespace are equal and leading and
 trailing space is ignored.  This permits reading passphrase or brainwallet
 trailing space is ignored.  This permits reading passphrase or brainwallet
 data from a multi-line file with free spacing and indentation.
 data from a multi-line file with free spacing and indentation.
-""".strip()
-
-bw_note = """
+""".strip(),
+		'brainwallet': """
 BRAINWALLET NOTE:
 BRAINWALLET NOTE:
 
 
 To thwart dictionary attacks, it's recommended to use a strong hash preset
 To thwart dictionary attacks, it's recommended to use a strong hash preset
 with brainwallets.  For a brainwallet passphrase to generate the correct
 with brainwallets.  For a brainwallet passphrase to generate the correct
 seed, the same seed length and hash preset parameters must always be used.
 seed, the same seed length and hash preset parameters must always be used.
-""".strip()
+""".strip(),
+		'txcreate': """
+The transaction's outputs are specified on the command line, while its inputs
+are chosen from a list of the user's unpent outputs via an interactive menu.
+
+If the transaction fee is not specified on the command line (see FEE
+SPECIFICATION below), it will be calculated dynamically using {dn}'s
+"estimatefee" function for the default (or user-specified) number of
+confirmations.  If "estimatefee" fails, the user will be prompted for a fee.
+
+Dynamic ("estimatefee") fees will be multiplied by the value of '--tx-fee-adj',
+if specified.
+
+Ages of transactions are approximate based on an average block discovery
+interval of one per {g.proto.secs_per_block} seconds.
+
+All addresses on the command line can be either {pnu} addresses or {pnm}
+addresses of the form <seed ID>:<index>.
+
+To send the value of all inputs (minus TX fee) to a single output, specify
+one address with no amount on the command line.
+""".format( g=g,
+			pnm=g.proj_name,
+			dn=g.proto.daemon_name,
+			pnu=g.proto.name.capitalize()),
+		'fee': """
+FEE SPECIFICATION: Transaction fees, both on the command line and at the
+interactive prompt, may be specified as either absolute {} amounts, using
+a plain decimal number, or as satoshis per byte, using an integer followed by
+the letter 's'.
+""".format(g.coin),
+		'txsign': """
+Transactions may contain both {pnm} or non-{pnm} input addresses.
+
+To sign non-{pnm} inputs, a {dn} wallet dump or flat key list is used
+as the key source ('--keys-from-file' option).
+
+To sign {pnm} inputs, key data is generated from a seed as with the
+{pnl}-addrgen and {pnl}-keygen commands.  Alternatively, a key-address file
+may be used (--mmgen-keys-from-file option).
+
+Multiple wallets or other seed files can be listed on the command line in
+any order.  If the seeds required to sign the transaction's inputs are not
+found in these files (or in the default wallet), the user will be prompted
+for seed data interactively.
+
+To prevent an attacker from crafting transactions with bogus {pnm}-to-{pnu}
+address mappings, all outputs to {pnm} addresses are verified with a seed
+source.  Therefore, seed files or a key-address file for all {pnm} outputs
+must also be supplied on the command line if the data can't be found in the
+default wallet.
+
+Seed source files must have the canonical extensions listed in the 'FileExt'
+column below:
+
+  {n_fmt}
+""".format( dn=g.proto.daemon_name,
+			n_fmt='\n  '.join(SeedSource.format_fmt_codes().splitlines()),
+			pnm=g.proj_name,
+			pnu=g.proto.name.capitalize(),
+			pnl=g.proj_name.lower())
+}[k]

+ 14 - 16
mmgen/globalvars.py

@@ -21,7 +21,6 @@ globalvars.py:  Constants and configuration options for the MMGen suite
 """
 """
 
 
 import sys,os
 import sys,os
-from mmgen.obj import BTCAmt
 
 
 # Global vars are set to dfl values in class g.
 # Global vars are set to dfl values in class g.
 # They're overridden in this order:
 # They're overridden in this order:
@@ -36,7 +35,8 @@ class g(object):
 	def die(ev=0,s=''):
 	def die(ev=0,s=''):
 		if s: sys.stderr.write(s+'\n')
 		if s: sys.stderr.write(s+'\n')
 		sys.exit(ev)
 		sys.exit(ev)
-	# Variables - these might be altered at runtime:
+
+	# Constants:
 
 
 	version      = '0.9.499'
 	version      = '0.9.499'
 	release_date = 'October 2017'
 	release_date = 'October 2017'
@@ -47,27 +47,24 @@ class g(object):
 	author    = 'Philemon'
 	author    = 'Philemon'
 	email     = '<mmgen@tuta.io>'
 	email     = '<mmgen@tuta.io>'
 	Cdates    = '2013-2017'
 	Cdates    = '2013-2017'
-	keywords  = 'Bitcoin, cryptocurrency, wallet, cold storage, offline, online, spending, open-source, command-line, Python, Linux, Bitcoin Core, bitcoind, hd, deterministic, hierarchical, secure, anonymous, Electrum, seed, mnemonic, brainwallet, Scrypt, utility, script, scriptable, blockchain, raw, transaction, permissionless, console, terminal, curses, ansi, color, tmux, remote, client, daemon, RPC, json, entropy, xterm, rxvt, PowerShell, MSYS, MinGW, mswin, Armbian, Raspbian, Raspberry Pi, Orange Pi'
+	keywords  = 'Bitcoin, BTC, cryptocurrency, wallet, cold storage, offline, online, spending, open-source, command-line, Python, Linux, Bitcoin Core, bitcoind, hd, deterministic, hierarchical, secure, anonymous, Electrum, seed, mnemonic, brainwallet, Scrypt, utility, script, scriptable, blockchain, raw, transaction, permissionless, console, terminal, curses, ansi, color, tmux, remote, client, daemon, RPC, json, entropy, xterm, rxvt, PowerShell, MSYS, MinGW, mswin, Armbian, Raspbian, Raspberry Pi, Orange Pi, BCash, BCH, Litecoin, LTC, altcoin'
+	max_int   = 0xffffffff
+	stdin_tty = bool(sys.stdin.isatty() or os.getenv('MMGEN_TEST_SUITE'))
+	http_timeout = 60
 
 
-	coin   = 'BTC'
-	coins  = 'BTC','BCH'
+	# Variables - these might be altered at runtime:
 
 
 	user_entropy   = ''
 	user_entropy   = ''
 	hash_preset    = '3'
 	hash_preset    = '3'
 	usr_randchars  = 30
 	usr_randchars  = 30
-	stdin_tty      = bool(sys.stdin.isatty() or os.getenv('MMGEN_TEST_SUITE'))
 
 
-	max_tx_fee   = BTCAmt('0.01')
 	tx_fee_adj   = 1.0
 	tx_fee_adj   = 1.0
 	tx_confs     = 3
 	tx_confs     = 3
-	satoshi      = BTCAmt('0.00000001') # One bitcoin equals 100,000,000 satoshis
 	seed_len     = 256
 	seed_len     = 256
 
 
-	http_timeout = 60
-	max_int      = 0xffffffff
-
-	# Constants - some of these might be overriden in opts.py, but they don't change thereafter
+	# Constant vars - some of these might be overriden in opts.py, but they don't change thereafter
 
 
+	coin                 = 'BTC'
 	debug                = False
 	debug                = False
 	quiet                = False
 	quiet                = False
 	no_license           = False
 	no_license           = False
@@ -76,13 +73,14 @@ class g(object):
 	force_256_color      = False
 	force_256_color      = False
 	testnet              = False
 	testnet              = False
 	regtest              = False
 	regtest              = False
-	chain                = None # set by first call to rpc_connection()
+	chain                = None # set by first call to rpc_init()
 	chains               = 'mainnet','testnet','regtest'
 	chains               = 'mainnet','testnet','regtest'
-	bitcoind_version     = None # set by first call to rpc_connection()
+	daemon_version       = None # set by first call to rpc_init()
 	rpc_host             = ''
 	rpc_host             = ''
 	rpc_port             = 0
 	rpc_port             = 0
 	rpc_user             = ''
 	rpc_user             = ''
 	rpc_password         = ''
 	rpc_password         = ''
+	rpch                 = None # global RPC handle
 
 
 	bob                  = False
 	bob                  = False
 	alice                = False
 	alice                = False
@@ -128,7 +126,8 @@ class g(object):
 	cfg_file_opts = (
 	cfg_file_opts = (
 		'color','debug','hash_preset','http_timeout','no_license','rpc_host','rpc_port',
 		'color','debug','hash_preset','http_timeout','no_license','rpc_host','rpc_port',
 		'quiet','tx_fee_adj','usr_randchars','testnet','rpc_user','rpc_password',
 		'quiet','tx_fee_adj','usr_randchars','testnet','rpc_user','rpc_password',
-		'daemon_data_dir','force_256_color','max_tx_fee','regtest'
+		'daemon_data_dir','force_256_color','regtest',
+		'btc_max_tx_fee','ltc_max_tx_fee','bch_max_tx_fee'
 	)
 	)
 	env_opts = (
 	env_opts = (
 		'MMGEN_BOGUS_WALLET_DATA',
 		'MMGEN_BOGUS_WALLET_DATA',
@@ -151,7 +150,6 @@ class g(object):
 	global_sets_opt = ['minconf','seed_len','hash_preset','usr_randchars','debug',
 	global_sets_opt = ['minconf','seed_len','hash_preset','usr_randchars','debug',
 						'quiet','tx_confs','tx_fee_adj','key_generator']
 						'quiet','tx_confs','tx_fee_adj','key_generator']
 
 
-	mins_per_block   = 9
 	passwd_max_tries = 5
 	passwd_max_tries = 5
 
 
 	max_urandchars = 80
 	max_urandchars = 80

+ 8 - 5
mmgen/main_addrgen.py

@@ -98,15 +98,18 @@ ADDRESS TYPES:
 
 
                       NOTES FOR ALL GENERATOR COMMANDS
                       NOTES FOR ALL GENERATOR COMMANDS
 
 
-{pwn}
+{n_pw}
 
 
-{bwn}
+{n_bw}
 
 
 FMT CODES:
 FMT CODES:
-  {f}
+  {n_fmt}
 """.format(
 """.format(
-		n_secp=note_secp256k1,n_addrkey=note_addrkey,pwn=pw_note,bwn=bw_note,
-		f='\n  '.join(SeedSource.format_fmt_codes().splitlines()),
+		n_secp=note_secp256k1,
+		n_addrkey=note_addrkey,
+		n_pw=help_notes('passwd'),
+		n_bw=help_notes('brainwallet'),
+		n_fmt='\n  '.join(SeedSource.format_fmt_codes().splitlines()),
 		n_at='\n  '.join(["'{}','{:<12} - {}".format(k,v['name']+"'",v['desc']) for k,v in MAT.mmtypes.items()]),
 		n_at='\n  '.join(["'{}','{:<12} - {}".format(k,v['name']+"'",v['desc']) for k,v in MAT.mmtypes.items()]),
 		o=opts
 		o=opts
 	)
 	)

+ 54 - 55
mmgen/main_addrimport.py

@@ -17,7 +17,7 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
 """
 """
-mmgen-addrimport: Import addresses into a MMGen bitcoind tracking wallet
+mmgen-addrimport: Import addresses into a MMGen coin daemon tracking wallet
 """
 """
 
 
 import time
 import time
@@ -26,7 +26,25 @@ from mmgen.common import *
 from mmgen.addr import AddrList,KeyAddrList
 from mmgen.addr import AddrList,KeyAddrList
 from mmgen.obj import TwLabel
 from mmgen.obj import TwLabel
 
 
-# In batch mode, bitcoind just rescans each address separately anyway, so make
+ai_msgs = lambda k: {
+	'rescan': """
+WARNING: You've chosen the '--rescan' option.  Rescanning the blockchain is
+necessary only if an address you're importing is already on the blockchain,
+has a balance and is not in your tracking wallet.  Note that the rescanning
+process is very slow (>30 min. for each imported address on a low-powered
+computer).
+	""".strip() if opt.rescan else """
+WARNING: If any of the addresses you're importing is already on the blockchain,
+has a balance and is not in your tracking wallet, you must exit the program now
+and rerun it using the '--rescan' option.
+""".strip(),
+	'bad_args': """
+You must specify an {pnm} address file, a single address with the '--address'
+option, or a list of non-{pnm} addresses with the '--addrlist' option
+""".strip().format(pnm=g.proj_name)
+}[k]
+
+# In batch mode, daemon just rescans each address separately anyway, so make
 # --batch and --rescan incompatible.
 # --batch and --rescan incompatible.
 
 
 opts_data = lambda: {
 opts_data = lambda: {
@@ -36,14 +54,13 @@ opts_data = lambda: {
 	'options': """
 	'options': """
 -h, --help         Print this help message
 -h, --help         Print this help message
 --, --longhelp     Print help message for long options (common options)
 --, --longhelp     Print help message for long options (common options)
--a, --address=a    Import the single Bitcoin address 'a'
+-a, --address=a    Import the single coin address 'a'
 -b, --batch        Import all addresses in one RPC call.
 -b, --batch        Import all addresses in one RPC call.
--l, --addrlist     Address source is a flat list of (non-MMGen) Bitcoin addresses
+-l, --addrlist     Address source is a flat list of non-MMGen coin addresses
 -k, --keyaddr-file Address source is a key-address file
 -k, --keyaddr-file Address source is a key-address file
 -q, --quiet        Suppress warnings
 -q, --quiet        Suppress warnings
 -r, --rescan       Rescan the blockchain.  Required if address to import is
 -r, --rescan       Rescan the blockchain.  Required if address to import is
                    on the blockchain and has a balance.  Rescanning is slow.
                    on the blockchain and has a balance.  Rescanning is slow.
--t, --test         Simulate operation; don't actually import addresses
 """,
 """,
 	'notes': """\n
 	'notes': """\n
 This command can also be used to update the comment fields of addresses already
 This command can also be used to update the comment fields of addresses already
@@ -63,109 +80,91 @@ def import_mmgen_list(infile):
 			rdie(2,'Segwit is not active on this chain. Cannot import Segwit addresses')
 			rdie(2,'Segwit is not active on this chain. Cannot import Segwit addresses')
 	return al
 	return al
 
 
+rpc_init()
+
 if len(cmd_args) == 1:
 if len(cmd_args) == 1:
 	infile = cmd_args[0]
 	infile = cmd_args[0]
 	check_infile(infile)
 	check_infile(infile)
 	if opt.addrlist:
 	if opt.addrlist:
-		lines = get_lines_from_file(
-			infile,'non-{pnm} addresses'.format(pnm=g.proj_name),trim_comments=True)
-		al = AddrList(addrlist=lines)
+		al = AddrList(addrlist=get_lines_from_file(
+			infile,
+			'non-{pnm} addresses'.format(pnm=g.proj_name),
+			trim_comments=True))
 	else:
 	else:
 		al = import_mmgen_list(infile)
 		al = import_mmgen_list(infile)
 elif len(cmd_args) == 0 and opt.address:
 elif len(cmd_args) == 0 and opt.address:
 	al = AddrList(addrlist=[opt.address])
 	al = AddrList(addrlist=[opt.address])
 	infile = 'command line'
 	infile = 'command line'
 else:
 else:
-	die(1,"""
-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))
+	die(1,ai_msgs('bad_args'))
 
 
 m = ' from Seed ID {}'.format(al.al_id.sid) if hasattr(al.al_id,'sid') else ''
 m = ' from Seed ID {}'.format(al.al_id.sid) if hasattr(al.al_id,'sid') else ''
 qmsg('OK. {} addresses{}'.format(al.num_addrs,m))
 qmsg('OK. {} addresses{}'.format(al.num_addrs,m))
 
 
-if not opt.test:
-	c = rpc_connection()
-
-m = """
-WARNING: You've chosen the '--rescan' option.  Rescanning the blockchain is
-necessary only if an address you're importing is already on the blockchain,
-has a balance and is not in your tracking wallet.  Note that the rescanning
-process is very slow (>30 min. for each imported address on a low-powered
-computer).
-	""".strip() if opt.rescan else """
-WARNING: If any of the addresses you're importing is already on the blockchain,
-has a balance and is not in your tracking wallet, you must exit the program now
-and rerun it using the '--rescan' option.
-""".strip()
-
-if not opt.quiet: confirm_or_exit(m, 'continue', expect='YES')
+if not opt.quiet: confirm_or_exit(ai_msgs('rescan'),'continue',expect='YES')
 
 
 err_flag = False
 err_flag = False
 
 
 def import_address(addr,label,rescan):
 def import_address(addr,label,rescan):
 	try:
 	try:
-		if not opt.test:
-			c.importaddress(addr,label,rescan,timeout=(False,3600)[rescan])
+		g.rpch.importaddress(addr,label,rescan,timeout=(False,3600)[rescan])
 	except:
 	except:
 		global err_flag
 		global err_flag
 		err_flag = True
 		err_flag = True
 
 
 w_n_of_m = len(str(al.num_addrs)) * 2 + 2
 w_n_of_m = len(str(al.num_addrs)) * 2 + 2
-w_mmid   = '' if opt.addrlist or opt.address else len(str(max(al.idxs()))) + 12
+w_mmid = 1 if opt.addrlist or opt.address else len(str(max(al.idxs()))) + 13
+msg_fmt = '{{:{}}} {{:34}} {{:{}}}'.format(w_n_of_m,w_mmid)
 
 
-if opt.rescan:
-	import threading
-	msg_fmt = '\r%s %-{}s %-34s %s'.format(w_n_of_m)
-else:
-	msg_fmt = '\r%-{}s %-34s %s'.format(w_n_of_m, w_mmid)
+if opt.rescan: import threading
 
 
-msg("Importing {} address{} from {}{}".format(
-		len(al.data), suf(al.data,'es'), 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))
+if not al.data[0].addr.is_for_chain(g.chain):
+	die(2,'Address{} not compatible with {} chain!'.format((' list','')[bool(opt.address)],g.chain))
 
 
-arg_list = []
 for n,e in enumerate(al.data):
 for n,e in enumerate(al.data):
 	if e.idx:
 	if e.idx:
 		label = '{}:{}'.format(al.al_id,e.idx)
 		label = '{}:{}'.format(al.al_id,e.idx)
 		if e.label: label += ' ' + e.label
 		if e.label: label += ' ' + e.label
 		m = label
 		m = label
 	else:
 	else:
-		label = 'btc:{}'.format(e.addr)
+		label = '{}:{}'.format(g.proto.base_coin.lower(),e.addr)
 		m = 'non-'+g.proj_name
 		m = 'non-'+g.proj_name
 
 
 	label = TwLabel(label)
 	label = TwLabel(label)
 
 
 	if opt.batch:
 	if opt.batch:
+		if n == 0: arg_list = []
 		arg_list.append((e.addr,label,False))
 		arg_list.append((e.addr,label,False))
-	elif opt.rescan:
+		continue
+
+	msg_data = ('{}/{}:'.format(n+1,al.num_addrs),e.addr,'({})'.format(m))
+
+	if opt.rescan:
 		t = threading.Thread(target=import_address,args=[e.addr,label,True])
 		t = threading.Thread(target=import_address,args=[e.addr,label,True])
 		t.daemon = True
 		t.daemon = True
 		t.start()
 		t.start()
-
 		start = int(time.time())
 		start = int(time.time())
-
 		while True:
 		while True:
 			if t.is_alive():
 			if t.is_alive():
-				elapsed = int(time.time() - start)
-				count = '%s/%s:' % (n+1, al.num_addrs)
-				msg_r(msg_fmt % (secs_to_hms(elapsed),count,e.addr,'(%s)' % m))
-				time.sleep(1)
+				elapsed = int(time.time()-start)
+				msg_r(('\r{} '+msg_fmt).format(secs_to_hms(elapsed),*msg_data))
+				time.sleep(0.5)
 			else:
 			else:
 				if err_flag: die(2,'\nImport failed')
 				if err_flag: die(2,'\nImport failed')
 				msg('\nOK')
 				msg('\nOK')
 				break
 				break
 	else:
 	else:
 		import_address(e.addr,label,False)
 		import_address(e.addr,label,False)
-		count = '%s/%s:' % (n+1, al.num_addrs)
-		msg_r(msg_fmt % (count, e.addr, '(%s)' % m))
+		msg_r('\r'+msg_fmt.format(*msg_data))
 		if err_flag: die(2,'\nImport failed')
 		if err_flag: die(2,'\nImport failed')
 		msg(' - OK')
 		msg(' - OK')
 
 
 if opt.batch:
 if opt.batch:
-	ret = c.importaddress(arg_list,batch=True)
-	msg('OK: %s addresses imported' % len(ret))
+	ret = g.rpch.importaddress(arg_list,batch=True)
+	msg('OK: {} addresses imported'.format(len(ret)))

+ 7 - 6
mmgen/main_passgen.py

@@ -98,18 +98,19 @@ EXAMPLE:
 
 
                       NOTES FOR ALL GENERATOR COMMANDS
                       NOTES FOR ALL GENERATOR COMMANDS
 
 
-{pwn}
+{n_pw}
 
 
-{bwn}
+{n_bw}
 
 
 FMT CODES:
 FMT CODES:
-  {f}
+  {n_fmt}
 """.format(
 """.format(
-		f='\n  '.join(SeedSource.format_fmt_codes().splitlines()),
 		o=opts,g=g,d58=dfl_len['b58'],d32=dfl_len['b32'],
 		o=opts,g=g,d58=dfl_len['b58'],d32=dfl_len['b32'],
 		ml=MMGenPWIDString.max_len,
 		ml=MMGenPWIDString.max_len,
-		pwn=pw_note,bwn=bw_note,
-		fs="', '".join(MMGenPWIDString.forbidden)
+		fs="', '".join(MMGenPWIDString.forbidden),
+		n_pw=help_notes('passwd'),
+		n_bw=help_notes('brainwallet'),
+		n_fmt='\n  '.join(SeedSource.format_fmt_codes().splitlines())
 	)
 	)
 }
 }
 
 

+ 4 - 3
mmgen/main_regtest.py

@@ -17,13 +17,14 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
 """
 """
-mmgen-regtest: Bitcoind regression test mode setup and operations for the MMGen
+mmgen-regtest: Coin daemon regression test mode setup and operations for the MMGen
                suite
                suite
 """
 """
 
 
 from mmgen.common import *
 from mmgen.common import *
+
 opts_data = lambda: {
 opts_data = lambda: {
-	'desc': 'Bitcoind regression test mode setup and operations for the {} suite'.format(g.proj_name),
+	'desc': 'Coin daemon regression test mode setup and operations for the {} suite'.format(g.proj_name),
 	'usage':   '[opts] <command>',
 	'usage':   '[opts] <command>',
 	'sets': ( ('yes', True, 'quiet', True), ),
 	'sets': ( ('yes', True, 'quiet', True), ),
 	'options': """
 	'options': """
@@ -40,7 +41,7 @@ opts_data = lambda: {
                          AVAILABLE COMMANDS
                          AVAILABLE COMMANDS
 
 
   setup          - set up system for regtest operation with MMGen
   setup          - set up system for regtest operation with MMGen
-  stop           - stop the regtest bitcoind
+  stop           - stop the regtest coin daemon
   bob            - switch to Bob's wallet, starting daemon if necessary
   bob            - switch to Bob's wallet, starting daemon if necessary
   alice          - switch to Alice's wallet, starting daemon if necessary
   alice          - switch to Alice's wallet, starting daemon if necessary
   user           - show current user
   user           - show current user

+ 19 - 14
mmgen/main_tool.py

@@ -17,7 +17,7 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
 """
 """
-mmgen-tool:  Perform various MMGen- and Bitcoin-related operations.
+mmgen-tool:  Perform various MMGen- and cryptocoin-related operations.
              Part of the MMGen suite
              Part of the MMGen suite
 """
 """
 
 
@@ -29,24 +29,24 @@ supported commands), use '-' as the first argument.
 """.strip()
 """.strip()
 
 
 cmd_help = """
 cmd_help = """
-Bitcoin address/key operations (compressed public keys supported):
-  addr2hexaddr   - convert Bitcoin address from base58 to hex format
+Cryptocoin address/key operations (compressed public keys supported):
+  addr2hexaddr   - convert coin address from base58 to hex format
   hex2wif        - convert a private key from hex to WIF 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
+  hexaddr2addr   - convert coin address from hex to base58 format
+  privhex2addr   - generate coin address from private key in hex format
   privhex2pubhex - generate a hex public key from a hex private key
   privhex2pubhex - generate a hex public key from a hex private key
   pubhex2addr    - convert a hex pubkey to an address
   pubhex2addr    - convert a hex pubkey to an address
   pubhex2redeem_script - convert a hex pubkey to a witness redeem script
   pubhex2redeem_script - convert a hex pubkey to a witness redeem script
   wif2redeem_script - convert a WIF private key 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
   wif2segwit_pair - generate both a Segwit redeem script and address from WIF
-  pubkey2addr    - convert Bitcoin public key to address
+  pubkey2addr    - convert coin public key to address
   randpair       - generate a random private key/address pair
   randpair       - generate a random private key/address pair
   randwif        - generate a random private key in WIF format
   randwif        - generate a random private key in WIF format
-  wif2addr       - generate a Bitcoin address from a key in WIF format
+  wif2addr       - generate a coin address from a key in WIF format
   wif2hex        - convert a private key from WIF to hex 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,
+Wallet/TX operations (coin daemon must be running):
+  getbalance    - like '{pn}-cli getbalance' but shows confirmed/unconfirmed,
                   spendable/unspendable balances for individual {pnm} wallets
                   spendable/unspendable balances for individual {pnm} wallets
   listaddress   - list the specified {pnm} address and its balance
   listaddress   - list the specified {pnm} address and its balance
   listaddresses - list {pnm} addresses and their balances
   listaddresses - list {pnm} addresses and their balances
@@ -104,10 +104,10 @@ Mnemonic operations (choose 'electrum' (default), 'tirosh' or 'all'
   computed using a different algorithm and are NOT Electrum-compatible!
   computed using a different algorithm and are NOT Electrum-compatible!
 
 
   {sm}
   {sm}
-""".format(pnm=g.proj_name,sm='\n  '.join(stdin_msg.split('\n')))
+"""
 
 
 opts_data = lambda: {
 opts_data = lambda: {
-	'desc':    'Perform various {pnm}- and Bitcoin-related operations'.format(pnm=g.proj_name),
+	'desc':    'Perform various {pnm}- and cryptocoin-related operations'.format(pnm=g.proj_name),
 	'usage':   '[opts] <command> <command args>',
 	'usage':   '[opts] <command> <command args>',
 	'options': """
 	'options': """
 -d, --outdir=       d Specify an alternate directory 'd' for output
 -d, --outdir=       d Specify an alternate directory 'd' for output
@@ -122,9 +122,14 @@ opts_data = lambda: {
 	'notes': """
 	'notes': """
 
 
                                COMMANDS
                                COMMANDS
-{}
-Type '{} help <command> for help on a particular command
-""".format(cmd_help,g.prog_name)
+{ch}
+Type '{pn} help <command> for help on a particular command
+""".format( pn=g.prog_name,
+			ch=cmd_help.format(
+				pn=g.proto.name,
+				pnm=g.proj_name,
+				sm='\n  '.join(stdin_msg.split('\n')))
+	)
 }
 }
 
 
 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'])

+ 15 - 11
mmgen/main_txbump.py

@@ -21,8 +21,8 @@ mmgen-txbump: Increase the fee on a replaceable (replace-by-fee) MMGen
               transaction, and optionally sign and send it
               transaction, and optionally sign and send it
 """
 """
 
 
-from mmgen.txcreate import *
-from mmgen.txsign import *
+from mmgen.common import *
+from mmgen.seed import SeedSource
 
 
 opts_data = lambda: {
 opts_data = lambda: {
 	'desc': 'Increase the fee on a replaceable (RBF) {g.proj_name} transaction, creating a new transaction, and optionally sign and send the new transaction'.format(g=g),
 	'desc': 'Increase the fee on a replaceable (RBF) {g.proj_name} transaction, creating a new transaction, and optionally sign and send the new transaction'.format(g=g),
@@ -58,29 +58,33 @@ opts_data = lambda: {
 -O, --old-incog-fmt    Specify old-format incognito input
 -O, --old-incog-fmt    Specify old-format incognito input
 -p, --hash-preset=   p Use the scrypt hash parameters defined by preset 'p'
 -p, --hash-preset=   p Use the scrypt hash parameters defined by preset 'p'
                        for password hashing (default: '{g.hash_preset}')
                        for password hashing (default: '{g.hash_preset}')
--P, --passwd-file=   f Get {pnm} wallet or bitcoind passphrase from file 'f'
+-P, --passwd-file=   f Get {pnm} wallet or {dn} passphrase from file 'f'
 -q, --quiet            Suppress warnings; overwrite files without prompting
 -q, --quiet            Suppress warnings; overwrite files without prompting
 -s, --send             Sign and send the transaction (the default if seed
 -s, --send             Sign and send the transaction (the default if seed
                        data is provided)
                        data is provided)
 -v, --verbose          Produce more verbose output
 -v, --verbose          Produce more verbose output
 -y, --yes             Answer 'yes' to prompts, suppress non-essential output
 -y, --yes             Answer 'yes' to prompts, suppress non-essential output
 -z, --show-hash-presets Show information on available hash presets
 -z, --show-hash-presets Show information on available hash presets
-""".format(g=g,pnm=pnm,pnl=pnm.lower(),
+""".format(g=g,pnm=g.proj_name,pnl=g.proj_name.lower(),dn=g.proto.daemon_name,
 		kgs=' '.join(['{}:{}'.format(n,k) for n,k in enumerate(g.key_generators,1)]),
 		kgs=' '.join(['{}:{}'.format(n,k) for n,k in enumerate(g.key_generators,1)]),
 		kg=g.key_generator,
 		kg=g.key_generator,
 		cu=g.coin
 		cu=g.coin
 		),
 		),
-	'notes': '\n' + fee_notes.format(g.coin) + txsign_notes
+	'notes': '\n' + help_notes('fee') + help_notes('txsign')
 }
 }
 
 
 cmd_args = opts.init(opts_data)
 cmd_args = opts.init(opts_data)
 
 
-c = rpc_connection()
+rpc_init()
 
 
 tx_file = cmd_args.pop(0)
 tx_file = cmd_args.pop(0)
 check_infile(tx_file)
 check_infile(tx_file)
 
 
+from mmgen.txcreate import *
+from mmgen.txsign import *
+
 seed_files = get_seed_files(opt,cmd_args) if (cmd_args or opt.send) else None
 seed_files = get_seed_files(opt,cmd_args) if (cmd_args or opt.send) else None
+
 kal = get_keyaddrlist(opt)
 kal = get_keyaddrlist(opt)
 kl = get_keylist(opt)
 kl = get_keylist(opt)
 
 
@@ -112,13 +116,13 @@ fee = tx.get_usr_fee_interactive(tx_fee=opt.tx_fee,desc='User-selected')
 tx.update_output_amt(op_idx,tx.sum_inputs()-tx.sum_outputs(exclude=op_idx)-fee)
 tx.update_output_amt(op_idx,tx.sum_inputs()-tx.sum_outputs(exclude=op_idx)-fee)
 
 
 d = tx.get_fee()
 d = tx.get_fee()
-assert d == fee and d <= g.max_tx_fee
+assert d == fee and d <= g.proto.max_tx_fee
 
 
 if not opt.yes:
 if not opt.yes:
 	tx.add_comment()   # edits an existing comment
 	tx.add_comment()   # edits an existing comment
-tx.create_raw(c)       # creates tx.hex, tx.txid
+tx.create_raw()        # creates tx.hex, tx.txid
 tx.add_timestamp()
 tx.add_timestamp()
-tx.add_blockcount(c)
+tx.add_blockcount()
 
 
 qmsg('Fee successfully increased')
 qmsg('Fee successfully increased')
 
 
@@ -127,9 +131,9 @@ if not silent:
 	msg_r(tx.format_view(terse=True))
 	msg_r(tx.format_view(terse=True))
 
 
 if seed_files or kl or kal:
 if seed_files or kl or kal:
-	txsign(opt,c,tx,seed_files,kl,kal)
+	txsign(tx,seed_files,kl,kal)
 	tx.write_to_file(ask_write=False)
 	tx.write_to_file(ask_write=False)
-	if tx.send(c):
+	if tx.send():
 		tx.write_to_file(ask_write=False)
 		tx.write_to_file(ask_write=False)
 else:
 else:
 	tx.write_to_file(ask_write=not opt.yes,ask_write_default_yes=False,ask_overwrite=not opt.yes)
 	tx.write_to_file(ask_write=not opt.yes,ask_write_default_yes=False,ask_overwrite=not opt.yes)

+ 8 - 7
mmgen/main_txcreate.py

@@ -17,14 +17,14 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
 """
 """
-mmgen-txcreate: Create a Bitcoin transaction to and from MMGen- or non-MMGen
+mmgen-txcreate: Create a cryptocoin transaction with MMGen- and/or non-MMGen
                 inputs and outputs
                 inputs and outputs
 """
 """
 
 
-from mmgen.txcreate import *
+from mmgen.common import *
 
 
 opts_data = lambda: {
 opts_data = lambda: {
-	'desc': 'Create a transaction with outputs to specified Bitcoin or {g.proj_name} addresses'.format(g=g),
+	'desc': 'Create a transaction with outputs to specified coin or {g.proj_name} addresses'.format(g=g),
 	'usage':   '[opts]  <addr,amt> ... [change addr] [addr file] ...',
 	'usage':   '[opts]  <addr,amt> ... [change addr] [addr file] ...',
 	'sets': ( ('yes', True, 'quiet', True), ),
 	'sets': ( ('yes', True, 'quiet', True), ),
 	'options': """
 	'options': """
@@ -37,7 +37,7 @@ opts_data = lambda: {
 -d, --outdir=      d Specify an alternate directory 'd' for output
 -d, --outdir=      d Specify an alternate directory 'd' for output
 -f, --tx-fee=      f Transaction fee, as a decimal {cu} amount or in satoshis
 -f, --tx-fee=      f Transaction fee, as a decimal {cu} amount or in satoshis
                      per byte (an integer followed by 's').  If omitted, fee
                      per byte (an integer followed by 's').  If omitted, fee
-                     will be calculated using bitcoind's 'estimatefee' call
+                     will be calculated using {dn}'s 'estimatefee' call
 -i, --info           Display unspent outputs and exit
 -i, --info           Display unspent outputs and exit
 -m, --minconf=     n Minimum number of confirmations required to spend
 -m, --minconf=     n Minimum number of confirmations required to spend
                      outputs (default: 1)
                      outputs (default: 1)
@@ -45,11 +45,12 @@ opts_data = lambda: {
 -r, --rbf            Make transaction BIP 125 replaceable (replace-by-fee)
 -r, --rbf            Make transaction BIP 125 replaceable (replace-by-fee)
 -v, --verbose        Produce more verbose output
 -v, --verbose        Produce more verbose output
 -y, --yes            Answer 'yes' to prompts, suppress non-essential output
 -y, --yes            Answer 'yes' to prompts, suppress non-essential output
-""".format(g=g,cu=g.coin),
-	'notes': '\n' + txcreate_notes + fee_notes.format(g.coin)
+""".format(g=g,cu=g.coin,dn=g.proto.daemon_name),
+	'notes': '\n' + help_notes('txcreate') + help_notes('fee')
 }
 }
 
 
 cmd_args = opts.init(opts_data)
 cmd_args = opts.init(opts_data)
-do_license_msg()
+
+from mmgen.txcreate import *
 tx = txcreate(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)
 tx.write_to_file(ask_write=not opt.yes,ask_overwrite=not opt.yes,ask_write_default_yes=False)

+ 13 - 11
mmgen/main_txdo.py

@@ -20,8 +20,8 @@
 mmgen-txdo: Create, sign and broadcast an online MMGen transaction
 mmgen-txdo: Create, sign and broadcast an online MMGen transaction
 """
 """
 
 
-from mmgen.txcreate import *
-from mmgen.txsign import *
+from mmgen.common import *
+from mmgen.seed import SeedSource
 
 
 opts_data = lambda: {
 opts_data = lambda: {
 	'desc': 'Create, sign and send an {g.proj_name} transaction'.format(g=g),
 	'desc': 'Create, sign and send an {g.proj_name} transaction'.format(g=g),
@@ -40,7 +40,7 @@ opts_data = lambda: {
 -e, --echo-passphrase  Print passphrase to screen when typing it
 -e, --echo-passphrase  Print passphrase to screen when typing it
 -f, --tx-fee=        f Transaction fee, as a decimal {cu} amount or in
 -f, --tx-fee=        f Transaction fee, as a decimal {cu} amount or in
                        satoshis per byte (an integer followed by 's').
                        satoshis per byte (an integer followed by 's').
-                       If omitted, bitcoind's 'estimatefee' will be used
+                       If omitted, {dn}'s 'estimatefee' will be used
                        to calculate the fee.
                        to calculate the fee.
 -H, --hidden-incog-input-params=f,o  Read hidden incognito data from file
 -H, --hidden-incog-input-params=f,o  Read hidden incognito data from file
                       'f' at offset 'o' (comma-separated)
                       'f' at offset 'o' (comma-separated)
@@ -68,27 +68,29 @@ opts_data = lambda: {
 -v, --verbose          Produce more verbose output
 -v, --verbose          Produce more verbose output
 -y, --yes             Answer 'yes' to prompts, suppress non-essential output
 -y, --yes             Answer 'yes' to prompts, suppress non-essential output
 -z, --show-hash-presets Show information on available hash presets
 -z, --show-hash-presets Show information on available hash presets
-""".format(g=g,pnm=pnm,pnl=pnm.lower(),
+""".format(g=g,pnm=g.proj_name,pnl=g.proj_name.lower(),dn=g.proto.daemon_name,
 		kgs=' '.join(['{}:{}'.format(n,k) for n,k in enumerate(g.key_generators,1)]),
 		kgs=' '.join(['{}:{}'.format(n,k) for n,k in enumerate(g.key_generators,1)]),
 		kg=g.key_generator,
 		kg=g.key_generator,
-		cu=g.coin
-		),
-	'notes': '\n' + txcreate_notes + fee_notes.format(g.coin) + txsign_notes
+		cu=g.coin),
+	'notes': '\n' + help_notes('txcreate') + help_notes('fee') + help_notes('txsign')
 }
 }
 
 
 cmd_args = opts.init(opts_data)
 cmd_args = opts.init(opts_data)
 
 
+rpc_init()
+
+from mmgen.txcreate import *
+from mmgen.txsign import *
+
 seed_files = get_seed_files(opt,cmd_args)
 seed_files = get_seed_files(opt,cmd_args)
-c = rpc_connection()
-do_license_msg()
 
 
 kal = get_keyaddrlist(opt)
 kal = get_keyaddrlist(opt)
 kl = get_keylist(opt)
 kl = get_keylist(opt)
 if kl and kal: kl.remove_dup_keys(kal)
 if kl and kal: kl.remove_dup_keys(kal)
 
 
 tx = txcreate(cmd_args,caller='txdo')
 tx = txcreate(cmd_args,caller='txdo')
-txsign(opt,c,tx,seed_files,kl,kal)
+txsign(tx,seed_files,kl,kal)
 tx.write_to_file(ask_write=False)
 tx.write_to_file(ask_write=False)
 
 
-if tx.send(c):
+if tx.send():
 	tx.write_to_file(ask_overwrite=False,ask_write=False)
 	tx.write_to_file(ask_overwrite=False,ask_write=False)

+ 8 - 7
mmgen/main_txsend.py

@@ -21,11 +21,9 @@ mmgen-txsend: Broadcast a transaction signed by 'mmgen-txsign' to the network
 """
 """
 
 
 from mmgen.common import *
 from mmgen.common import *
-from mmgen.tx import *
 
 
 opts_data = lambda: {
 opts_data = lambda: {
-	'desc':    'Send a Bitcoin transaction signed by {pnm}-txsign'.format(
-					pnm=g.proj_name.lower()),
+	'desc':    'Send a cryptocoin transaction signed by {pnm}-txsign'.format(pnm=g.proj_name.lower()),
 	'usage':   '[opts] <signed transaction file>',
 	'usage':   '[opts] <signed transaction file>',
 	'sets': ( ('yes', True, 'quiet', True), ),
 	'sets': ( ('yes', True, 'quiet', True), ),
 	'options': """
 	'options': """
@@ -40,22 +38,25 @@ opts_data = lambda: {
 
 
 cmd_args = opts.init(opts_data)
 cmd_args = opts.init(opts_data)
 
 
+rpc_init()
+
 if len(cmd_args) == 1:
 if len(cmd_args) == 1:
 	infile = cmd_args[0]; check_infile(infile)
 	infile = cmd_args[0]; check_infile(infile)
 else: opts.usage()
 else: opts.usage()
 
 
 if not opt.status: do_license_msg()
 if not opt.status: do_license_msg()
 
 
-c = rpc_connection()
+from mmgen.tx import *
+
 tx = MMGenTX(infile) # sig check performed here
 tx = MMGenTX(infile) # sig check performed here
 vmsg("Signed transaction file '%s' is valid" % infile)
 vmsg("Signed transaction file '%s' is valid" % infile)
 
 
-if not tx.marked_signed(c):
+if not tx.marked_signed():
 	die(1,'Transaction is not signed!')
 	die(1,'Transaction is not signed!')
 
 
 if opt.status:
 if opt.status:
 	if tx.coin_txid: qmsg('{} txid: {}'.format(g.coin,tx.coin_txid.hl()))
 	if tx.coin_txid: qmsg('{} txid: {}'.format(g.coin,tx.coin_txid.hl()))
-	tx.get_status(c,status=True)
+	tx.get_status(status=True)
 	sys.exit(0)
 	sys.exit(0)
 
 
 if not opt.yes:
 if not opt.yes:
@@ -63,5 +64,5 @@ if not opt.yes:
 	if tx.add_comment(): # edits an existing comment, returns true if changed
 	if tx.add_comment(): # edits an existing comment, returns true if changed
 		tx.write_to_file(ask_write_default_yes=True)
 		tx.write_to_file(ask_write_default_yes=True)
 
 
-if tx.send(c):
+if tx.send():
 	tx.write_to_file(ask_overwrite=False,ask_write=False)
 	tx.write_to_file(ask_overwrite=False,ask_write=False)

+ 11 - 8
mmgen/main_txsign.py

@@ -20,11 +20,12 @@
 mmgen-txsign: Sign a transaction generated by 'mmgen-txcreate'
 mmgen-txsign: Sign a transaction generated by 'mmgen-txcreate'
 """
 """
 
 
-from mmgen.txsign import *
+from mmgen.common import *
+from mmgen.seed import SeedSource
 
 
-# -w, --use-wallet-dat (keys from running bitcoind) removed: use bitcoin-cli walletdump instead
+# -w, --use-wallet-dat (keys from running coin daemon) removed: use walletdump rpc instead
 opts_data = lambda: {
 opts_data = lambda: {
-	'desc':    'Sign Bitcoin transactions generated by {pnl}-txcreate'.format(pnl=pnm.lower()),
+	'desc':    'Sign cryptocoin transactions generated by {pnl}-txcreate'.format(pnl=g.proj_name.lower()),
 	'usage':   '[opts] <transaction file>... [seed source]...',
 	'usage':   '[opts] <transaction file>... [seed source]...',
 	'sets': ( ('yes', True, 'quiet', True), ),
 	'sets': ( ('yes', True, 'quiet', True), ),
 	'options': """
 	'options': """
@@ -53,19 +54,19 @@ opts_data = lambda: {
                       online signing without an {pnm} seed source. The
                       online signing without an {pnm} seed source. The
                       key-address file is also used to verify {pnm}-to-{cu}
                       key-address file is also used to verify {pnm}-to-{cu}
                       mappings, so the user should record its checksum.
                       mappings, so the user should record its checksum.
--P, --passwd-file= f  Get {pnm} wallet or bitcoind passphrase from file 'f'
+-P, --passwd-file= f  Get {pnm} wallet or {dn} passphrase from file 'f'
 -q, --quiet           Suppress warnings; overwrite files without prompting
 -q, --quiet           Suppress warnings; overwrite files without prompting
 -I, --info            Display information about the transaction and exit
 -I, --info            Display information about the transaction and exit
 -t, --terse-info      Like '--info', but produce more concise output
 -t, --terse-info      Like '--info', but produce more concise output
 -v, --verbose         Produce more verbose output
 -v, --verbose         Produce more verbose output
 -y, --yes             Answer 'yes' to prompts, suppress non-essential output
 -y, --yes             Answer 'yes' to prompts, suppress non-essential output
 """.format(
 """.format(
-		g=g,pnm=pnm,pnl=pnm.lower(),
+		g=g,pnm=g.proj_name,pnl=g.proj_name.lower(),dn=g.proto.daemon_name,
 		kgs=' '.join(['{}:{}'.format(n,k) for n,k in enumerate(g.key_generators,1)]),
 		kgs=' '.join(['{}:{}'.format(n,k) for n,k in enumerate(g.key_generators,1)]),
 		kg=g.key_generator,
 		kg=g.key_generator,
 		cu=g.coin
 		cu=g.coin
 		),
 		),
-	'notes': '\n' + txsign_notes
+	'notes': '\n' + help_notes('txsign')
 }
 }
 
 
 infiles = opts.init(opts_data,add_opts=['b16'])
 infiles = opts.init(opts_data,add_opts=['b16'])
@@ -73,11 +74,13 @@ infiles = opts.init(opts_data,add_opts=['b16'])
 if not infiles: opts.usage()
 if not infiles: opts.usage()
 for i in infiles: check_infile(i)
 for i in infiles: check_infile(i)
 
 
-c = rpc_connection()
+rpc_init()
 
 
 if not opt.info and not opt.terse_info:
 if not opt.info and not opt.terse_info:
 	do_license_msg(immed=True)
 	do_license_msg(immed=True)
 
 
+from mmgen.txsign import *
+
 tx_files   = get_tx_files(opt,infiles)
 tx_files   = get_tx_files(opt,infiles)
 seed_files = get_seed_files(opt,infiles)
 seed_files = get_seed_files(opt,infiles)
 
 
@@ -105,7 +108,7 @@ for tx_num,tx_file in enumerate(tx_files,1):
 	if not opt.yes:
 	if not opt.yes:
 		tx.view_with_prompt('View data for transaction%s?' % tx_num_str)
 		tx.view_with_prompt('View data for transaction%s?' % tx_num_str)
 
 
-	txsign(opt,c,tx,seed_files,kl,kal,tx_num_str)
+	txsign(tx,seed_files,kl,kal,tx_num_str)
 
 
 	if not opt.yes:
 	if not opt.yes:
 		tx.add_comment()   # edits an existing comment
 		tx.add_comment()   # edits an existing comment

+ 5 - 5
mmgen/main_wallet.py

@@ -30,8 +30,8 @@ usage = '[opts] [infile]'
 nargs = 1
 nargs = 1
 iaction = 'convert'
 iaction = 'convert'
 oaction = 'convert'
 oaction = 'convert'
-
 invoked_as = 'passchg' if g.prog_name == 'mmgen-passchg' else g.prog_name.partition('-wallet')[2]
 invoked_as = 'passchg' if g.prog_name == 'mmgen-passchg' else g.prog_name.partition('-wallet')[2]
+bw_note = True
 
 
 # full: defhHiJkKlLmoOpPqrSvz-
 # full: defhHiJkKlLmoOpPqrSvz-
 if invoked_as == 'gen':
 if invoked_as == 'gen':
@@ -51,7 +51,7 @@ elif invoked_as == 'passchg':
 	desc = 'Change the passphrase, hash preset or label of an {pnm} wallet'
 	desc = 'Change the passphrase, hash preset or label of an {pnm} wallet'
 	opt_filter = 'efhdiHkKOlLmpPqrSvz-'
 	opt_filter = 'efhdiHkKOlLmpPqrSvz-'
 	iaction = 'input'
 	iaction = 'input'
-	bw_note = ''
+	bw_note = False
 else:
 else:
 	die(1,"'%s': unrecognized invocation" % g.prog_name)
 	die(1,"'%s': unrecognized invocation" % g.prog_name)
 
 
@@ -97,14 +97,14 @@ opts_data = lambda: {
 	),
 	),
 	'notes': """
 	'notes': """
 
 
-{pwn}{bwn}
+{n_pw}{n_bw}
 
 
 FMT CODES:
 FMT CODES:
   {f}
   {f}
 """.format(
 """.format(
 	f='\n  '.join(SeedSource.format_fmt_codes().splitlines()),
 	f='\n  '.join(SeedSource.format_fmt_codes().splitlines()),
-	pwn=pw_note,
-	bwn=('','\n\n' + bw_note)[bool(bw_note)]
+	n_pw=help_notes('passwd'),
+	n_bw=('','\n\n' + help_notes('brainwallet'))[bw_note]
 	)
 	)
 }
 }
 
 

+ 36 - 29
mmgen/obj.py

@@ -285,6 +285,8 @@ class BTCAmt(Decimal,Hilite,InitErrors):
 	color = 'yellow'
 	color = 'yellow'
 	max_prec = 8
 	max_prec = 8
 	max_amt = 21000000
 	max_amt = 21000000
+	min_coin_unit = Decimal('0.00000001')
+
 	def __new__(cls,num,on_fail='die'):
 	def __new__(cls,num,on_fail='die'):
 		if type(num) == cls: return num
 		if type(num) == cls: return num
 		cls.arg_chk(cls,on_fail)
 		cls.arg_chk(cls,on_fail)
@@ -297,8 +299,8 @@ class BTCAmt(Decimal,Hilite,InitErrors):
 			assert me >= 0,'coin amount cannot be negative'
 			assert me >= 0,'coin amount cannot be negative'
 			return me
 			return me
 		except Exception as e:
 		except Exception as e:
-			m = "{!r}: value cannot be converted to BTCAmt ({})"
-			return cls.init_fail(m.format(num,e[0]),on_fail)
+			m = "{!r}: value cannot be converted to {} ({})"
+			return cls.init_fail(m.format(num,cls.__name__,e[0]),on_fail)
 
 
 	@classmethod
 	@classmethod
 	def fmtc(cls):
 	def fmtc(cls):
@@ -347,24 +349,29 @@ class BTCAmt(Decimal,Hilite,InitErrors):
 	def __neg__(self,other,context=None):
 	def __neg__(self,other,context=None):
 		return type(self)(Decimal.__neg__(self,other,context))
 		return type(self)(Decimal.__neg__(self,other,context))
 
 
+class BCHAmt(BTCAmt):
+	pass
+class LTCAmt(BTCAmt):
+	max_amt = 84000000
+
 class CoinAddr(str,Hilite,InitErrors,MMGenObject):
 class CoinAddr(str,Hilite,InitErrors,MMGenObject):
 	color = 'cyan'
 	color = 'cyan'
 	width = 35 # max len of testnet p2sh addr
 	width = 35 # max len of testnet p2sh addr
 	def __new__(cls,s,on_fail='die'):
 	def __new__(cls,s,on_fail='die'):
 		if type(s) == cls: return s
 		if type(s) == cls: return s
 		cls.arg_chk(cls,on_fail)
 		cls.arg_chk(cls,on_fail)
+		from mmgen.globalvars import g
 		try:
 		try:
 			assert set(s) <= set(ascii_letters+digits),'contains non-ascii characters'
 			assert set(s) <= set(ascii_letters+digits),'contains non-ascii characters'
 			me = str.__new__(cls,s)
 			me = str.__new__(cls,s)
-			from mmgen.globalvars import g
 			va = g.proto.verify_addr(s,return_dict=True)
 			va = g.proto.verify_addr(s,return_dict=True)
 			assert va,'failed verification'
 			assert va,'failed verification'
 			me.addr_fmt = va['format']
 			me.addr_fmt = va['format']
 			me.hex = va['hex']
 			me.hex = va['hex']
 			return me
 			return me
 		except Exception as e:
 		except Exception as e:
-			m = "{!r}: value cannot be converted to Bitcoin address ({})"
-			return cls.init_fail(m.format(s,e[0]),on_fail)
+			m = "{!r}: value cannot be converted to {} address ({})"
+			return cls.init_fail(m.format(s,g.proto.__name__,e[0]),on_fail)
 
 
 	@classmethod
 	@classmethod
 	def fmtc(cls,s,**kwargs):
 	def fmtc(cls,s,**kwargs):
@@ -376,22 +383,17 @@ class CoinAddr(str,Hilite,InitErrors,MMGenObject):
 			s = s[:kwargs['width']-2] +  '..'
 			s = s[:kwargs['width']-2] +  '..'
 		return Hilite.fmtc(s,**kwargs)
 		return Hilite.fmtc(s,**kwargs)
 
 
-	def is_for_current_chain(self):
-		from mmgen.globalvars import g
-		assert g.chain,'global chain variable unset'
-		return self[0] in g.proto.get_chain_protocol(g.chain).addr_pfxs
-
-	def is_mainnet(self):
+	def is_for_chain(self,chain):
 		from mmgen.globalvars import g
 		from mmgen.globalvars import g
-		return self[0] in g.proto.get_chain_protocol('mainnet').addr_pfxs
-
-	def is_testnet(self):
-		from mmgen.globalvars import g
-		return self[0] in g.proto.get_chain_protocol('testnet').addr_pfxs
+		vn = g.proto.get_protocol_by_chain(chain).addr_ver_num
+		if self.addr_fmt == 'p2sh' and 'p2sh2' in vn:
+			return self[0] in vn['p2sh'][1] or self[0] in vn['p2sh2'][1]
+		else:
+			return self[0] in vn[self.addr_fmt][1]
 
 
 	def is_in_tracking_wallet(self):
 	def is_in_tracking_wallet(self):
-		from mmgen.rpc import rpc_connection
-		d = rpc_connection().validateaddress(self)
+		from mmgen.rpc import rpc_init
+		d = rpc_init().validateaddress(self)
 		return d['iswatchonly'] and 'account' in d
 		return d['iswatchonly'] and 'account' in d
 
 
 class SeedID(str,Hilite,InitErrors):
 class SeedID(str,Hilite,InitErrors):
@@ -452,7 +454,9 @@ class TwMMGenID(str,Hilite,InitErrors,MMGenObject):
 			sort_key,idtype = ret.sort_key,'mmgen'
 			sort_key,idtype = ret.sort_key,'mmgen'
 		except Exception as e:
 		except Exception as e:
 			try:
 			try:
-				assert s[:4] == 'btc:',"not a string beginning with the prefix 'btc:'"
+				from mmgen.globalvars import g
+				assert s.split(':',1)[0] == g.proto.base_coin.lower(),(
+					"not a string beginning with the prefix '{}:'".format(g.proto.base_coin.lower()))
 				assert set(s[4:]) <= set(ascii_letters+digits),'contains non-ascii characters'
 				assert set(s[4:]) <= set(ascii_letters+digits),'contains non-ascii characters'
 				assert len(s) > 4,'not more that four characters long'
 				assert len(s) > 4,'not more that four characters long'
 				ret,sort_key,idtype = str(s),'z_'+s,'non-mmgen'
 				ret,sort_key,idtype = str(s),'z_'+s,'non-mmgen'
@@ -514,7 +518,7 @@ class MMGenTxID(HexStr,Hilite,InitErrors):
 			m = "{}\n{!r}: value cannot be converted to {}"
 			m = "{}\n{!r}: value cannot be converted to {}"
 			return cls.init_fail(m.format(e[0],s,cls.__name__),on_fail)
 			return cls.init_fail(m.format(e[0],s,cls.__name__),on_fail)
 
 
-class BitcoinTxID(MMGenTxID):
+class CoinTxID(MMGenTxID):
 	color = 'purple'
 	color = 'purple'
 	width = 64
 	width = 64
 	hexcase = 'lower'
 	hexcase = 'lower'
@@ -667,32 +671,30 @@ class MMGenAddrType(str,Hilite,InitErrors,MMGenObject):
 				'comp':False,
 				'comp':False,
 				'gen':'p2pkh',
 				'gen':'p2pkh',
 				'fmt':'p2pkh',
 				'fmt':'p2pkh',
-				'desc':'Legacy uncompressed Bitcoin address'},
+				'desc':'Legacy uncompressed address'},
 		'S': {  'name':'segwit',
 		'S': {  'name':'segwit',
 				'comp':True,
 				'comp':True,
 				'gen':'segwit',
 				'gen':'segwit',
 				'fmt':'p2sh',
 				'fmt':'p2sh',
-				'desc':'Bitcoin Segwit P2SH-P2WPK address' },
+				'desc':'Segwit P2SH-P2WPKH address' },
 		'C': {  'name':'compressed',
 		'C': {  'name':'compressed',
 				'comp':True,
 				'comp':True,
 				'gen':'p2pkh',
 				'gen':'p2pkh',
 				'fmt':'p2pkh',
 				'fmt':'p2pkh',
-				'desc':'Compressed Bitcoin P2PKH address'}
-# 		'l': 'litecoin',
-# 		'e': 'ethereum',
-# 		'E': 'ethereum_classic',
-# 		'm': 'monero',
-# 		'z': 'zcash',
+				'desc':'Compressed P2PKH address'}
 	}
 	}
 	dfl_mmtype = 'L'
 	dfl_mmtype = 'L'
 	def __new__(cls,s,on_fail='die',errmsg=None):
 	def __new__(cls,s,on_fail='die',errmsg=None):
 		if type(s) == cls: return s
 		if type(s) == cls: return s
 		cls.arg_chk(cls,on_fail)
 		cls.arg_chk(cls,on_fail)
+		from mmgen.globalvars import g
 		try:
 		try:
 			for k,v in cls.mmtypes.items():
 			for k,v in cls.mmtypes.items():
 				if s in (k,v['name']):
 				if s in (k,v['name']):
 					if s == v['name']: s = k
 					if s == v['name']: s = k
 					me = str.__new__(cls,s)
 					me = str.__new__(cls,s)
+					assert me in g.proto.mmtypes + ('P',), (
+						"'{}': invalid address type for {}".format(me,g.proto.__name__))
 					me.name = v['name']
 					me.name = v['name']
 					me.compressed = v['comp']
 					me.compressed = v['comp']
 					me.gen_method = v['gen']
 					me.gen_method = v['gen']
@@ -701,9 +703,14 @@ class MMGenAddrType(str,Hilite,InitErrors,MMGenObject):
 					return me
 					return me
 			raise ValueError,'not found'
 			raise ValueError,'not found'
 		except Exception as e:
 		except Exception as e:
-			m = errmsg or '{!r}: invalid value for {} ({})'.format(s,cls.__name__,e[0])
+			m = '{}{!r}: invalid value for {} ({})'.format(
+				('{!r}\n'.format(errmsg) if errmsg else ''),s,cls.__name__,e[0])
 			return cls.init_fail(m,on_fail)
 			return cls.init_fail(m,on_fail)
 
 
+	@classmethod
+	def get_names(cls):
+		return [v['name'] for v in cls.mmtypes.values()]
+
 class MMGenPasswordType(MMGenAddrType):
 class MMGenPasswordType(MMGenAddrType):
 	mmtypes = {
 	mmtypes = {
 		'P': {  'name':'password',
 		'P': {  'name':'password',

+ 89 - 54
mmgen/opts.py

@@ -45,30 +45,6 @@ def _show_hash_presets():
 		msg(fs.format("'%s'" % i, *g.hash_presets[i]))
 		msg(fs.format("'%s'" % i, *g.hash_presets[i]))
 	msg('N = memory usage (power of two), p = iterations (rounds)')
 	msg('N = memory usage (power of two), p = iterations (rounds)')
 
 
-# most, but not all, of these set the corresponding global var
-common_opts_data = """
---, --coin=c              Choose coin unit. Default: {cu_dfl}. Options: {cu_all}
---, --color=0|1           Disable or enable color output
---, --force-256-color     Force 256-color output when color is enabled
---, --bitcoin-data-dir=d  Specify Bitcoin data directory location 'd'
---, --data-dir=d          Specify {pnm} data directory location 'd'
---, --no-license          Suppress the GPL license prompt
---, --rpc-host=h          Communicate with bitcoind running on host 'h'
---, --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
---, --bob                 Switch to user "Bob" in MMGen regtest setup
---, --alice               Switch to user "Alice" in MMGen regtest setup
-""".format(
-	pnm=g.proj_name,
-	cu_dfl=g.coin,
-	cu_all=' '.join(g.coins),
-	)
-
 def opt_preproc_debug(short_opts,long_opts,skipped_opts,uopts,args):
 def opt_preproc_debug(short_opts,long_opts,skipped_opts,uopts,args):
 	d = (
 	d = (
 		('Cmdline',            ' '.join(sys.argv)),
 		('Cmdline',            ' '.join(sys.argv)),
@@ -118,36 +94,55 @@ def set_data_dir_root():
 	# mainnet and testnet share cfg file, as with Core
 	# mainnet and testnet share cfg file, as with Core
 	g.cfg_file = os.path.join(g.data_dir_root,'{}.cfg'.format(g.proj_name.lower()))
 	g.cfg_file = os.path.join(g.data_dir_root,'{}.cfg'.format(g.proj_name.lower()))
 
 
-def get_data_from_config_file():
-	from mmgen.util import msg,die,check_or_create_dir
-	check_or_create_dir(g.data_dir_root) # dies on error
-
+def get_cfg_template_data():
 	# https://wiki.debian.org/Python:
 	# https://wiki.debian.org/Python:
 	#   Debian (Ubuntu) sys.prefix is '/usr' rather than '/usr/local, so add 'local'
 	#   Debian (Ubuntu) sys.prefix is '/usr' rather than '/usr/local, so add 'local'
 	# TODO - test for Windows
 	# TODO - test for Windows
 	# This must match the configuration in setup.py
 	# This must match the configuration in setup.py
-	data = u''
+	cfg_template = os.path.join(*([sys.prefix]
+				+ (['share'],['local','share'])[g.platform=='linux']
+				+ [g.proj_name.lower(),os.path.basename(g.cfg_file)]))
 	try:
 	try:
-		with open(g.cfg_file,'rb') as f: data = f.read().decode('utf8')
+		with open(cfg_template,'rb') as f:
+			return f.read()
 	except:
 	except:
-		cfg_template = os.path.join(*([sys.prefix]
-					+ (['share'],['local','share'])[g.platform=='linux']
-					+ [g.proj_name.lower(),os.path.basename(g.cfg_file)]))
+		msg("WARNING: configuration template not found at '{}'".format(cfg_template))
+		return u''
+
+def get_data_from_cfg_file():
+	from mmgen.util import msg,die,check_or_create_dir
+	check_or_create_dir(g.data_dir_root) # dies on error
+	template_data = get_cfg_template_data()
+	data = {}
+
+	def copy_template_data(fn):
 		try:
 		try:
-			with open(cfg_template,'rb') as f: template_data = f.read()
+			with open(fn,'wb') as f: f.write(template_data)
+			os.chmod(fn,0600)
 		except:
 		except:
-			msg("WARNING: configuration template not found at '{}'".format(cfg_template))
-		else:
-			try:
-				with open(g.cfg_file,'wb') as f: f.write(template_data)
-				os.chmod(g.cfg_file,0600)
-			except:
-				die(2,"ERROR: unable to write to datadir '{}'".format(g.data_dir))
-	return data
+			die(2,"ERROR: unable to write to datadir '{}'".format(g.data_dir))
+
+	for k,suf in (('cfg',''),('sample','.sample')):
+		try:
+			with open(g.cfg_file+suf,'rb') as f:
+				data[k] = f.read().decode('utf8')
+		except:
+			if template_data:
+				copy_template_data(g.cfg_file+suf)
+				data[k] = template_data
+			else:
+				data[k] = u''
+
+	if template_data and data['sample'] != template_data:
+		g.cfg_options_changed = True
+		copy_template_data(g.cfg_file+'.sample')
+
+	return data['cfg']
 
 
 def override_from_cfg_file(cfg_data):
 def override_from_cfg_file(cfg_data):
 	from mmgen.util import die,strip_comments,set_for_type
 	from mmgen.util import die,strip_comments,set_for_type
 	import re
 	import re
+	from mmgen.protocol import CoinProtocol
 	for n,l in enumerate(cfg_data.splitlines(),1): # DOS-safe
 	for n,l in enumerate(cfg_data.splitlines(),1): # DOS-safe
 		l = strip_comments(l)
 		l = strip_comments(l)
 		if l == '': continue
 		if l == '': continue
@@ -155,9 +150,16 @@ def override_from_cfg_file(cfg_data):
 		if not m: die(2,"Parse error in file '{}', line {}".format(g.cfg_file,n))
 		if not m: die(2,"Parse error in file '{}', line {}".format(g.cfg_file,n))
 		name,val = m.groups()
 		name,val = m.groups()
 		if name in g.cfg_file_opts:
 		if name in g.cfg_file_opts:
-			setattr(g,name,set_for_type(val,getattr(g,name),name,src=g.cfg_file))
+			pfx,cfg_var = name.split('_',1)
+			if pfx in CoinProtocol.coins:
+				cls,attr = CoinProtocol(pfx,False),cfg_var
+			else:
+				cls,attr = g,name
+			setattr(cls,attr,set_for_type(val,getattr(cls,attr),attr,src=g.cfg_file))
+		#	pmsg(cls,attr,getattr(cls,attr))
 		else:
 		else:
 			die(2,"'{}': unrecognized option in '{}'".format(name,g.cfg_file))
 			die(2,"'{}': unrecognized option in '{}'".format(name,g.cfg_file))
+#	pdie('xxx')
 
 
 def override_from_env():
 def override_from_env():
 	from mmgen.util import set_for_type
 	from mmgen.util import set_for_type
@@ -170,12 +172,37 @@ def override_from_env():
 
 
 def init(opts_f,add_opts=[],opt_filter=None):
 def init(opts_f,add_opts=[],opt_filter=None):
 
 
+	from mmgen.protocol import CoinProtocol,BitcoinProtocol
+	g.proto = BitcoinProtocol # this must be initialized to something before opts_f is called
+
+	# most, but not all, of these set the corresponding global var
+	common_opts_data = """
+--, --coin=c              Choose coin unit. Default: {cu_dfl}. Options: {cu_all}
+--, --color=0|1           Disable or enable color output
+--, --force-256-color     Force 256-color output when color is enabled
+--, --daemon-data-dir=d   Specify coin daemon data directory location 'd'
+--, --data-dir=d          Specify {pnm} data directory location 'd'
+--, --no-license          Suppress the GPL license prompt
+--, --rpc-host=h          Communicate with {dn} running on host 'h'
+--, --rpc-port=p          Communicate with {dn} listening on port 'p'
+--, --rpc-user=user       Override 'rpcuser' in {pn}.conf
+--, --rpc-password=pass   Override 'rpcpassword' in {pn}.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
+--, --bob                 Switch to user "Bob" in MMGen regtest setup
+--, --alice               Switch to user "Alice" in MMGen regtest setup
+	""".format( pnm=g.proj_name,pn=g.proto.name,dn=g.proto.daemon_name,
+				cu_dfl=g.coin,
+				cu_all=' '.join(CoinProtocol.coins))
+
 	opts_data = opts_f()
 	opts_data = opts_f()
 	opts_data['long_options'] = common_opts_data
 	opts_data['long_options'] = common_opts_data
 
 
 	version_info = """
 	version_info = """
     {pgnm_uc} version {g.version}
     {pgnm_uc} version {g.version}
-    Part of the {pnm} suite, a Bitcoin cold-storage solution for the command line.
+    Part of the {pnm} suite, an online/offline cryptocoin wallet for the command line.
     Copyright (C) {g.Cdates} {g.author} {g.email}
     Copyright (C) {g.Cdates} {g.author} {g.email}
 	""".format(pnm=g.proj_name, g=g, pgnm_uc=g.prog_name.upper()).strip()
 	""".format(pnm=g.proj_name, g=g, pgnm_uc=g.prog_name.upper()).strip()
 
 
@@ -199,11 +226,10 @@ def init(opts_f,add_opts=[],opt_filter=None):
 
 
 	# NB: user opt --data-dir is actually g.data_dir_root
 	# NB: user opt --data-dir is actually g.data_dir_root
 	# cfg file is in g.data_dir_root, wallet and other data are in g.data_dir
 	# cfg file is in g.data_dir_root, wallet and other data are in g.data_dir
-	# Must set g.data_dir_root and g.cfg_file from cmdline before processing cfg file
+	# We must set g.data_dir_root and g.cfg_file from cmdline before processing cfg file
 	set_data_dir_root()
 	set_data_dir_root()
 	if not opt.skip_cfg_file:
 	if not opt.skip_cfg_file:
-		cfg_data = get_data_from_config_file()
-		override_from_cfg_file(cfg_data)
+		override_from_cfg_file(get_data_from_cfg_file())
 	override_from_env()
 	override_from_env()
 
 
 	# User opt sets global var - do these here, before opt is set from g.global_sets_opt
 	# User opt sets global var - do these here, before opt is set from g.global_sets_opt
@@ -214,10 +240,10 @@ def init(opts_f,add_opts=[],opt_filter=None):
 	if g.regtest: g.testnet = True # These are equivalent for now
 	if g.regtest: g.testnet = True # These are equivalent for now
 
 
 	# g.testnet is set, so we can set g.proto
 	# g.testnet is set, so we can set g.proto
-	from mmgen.protocol import get_coin_protocol
-	g.proto = get_coin_protocol(g.coin,g.testnet)
+	g.proto = CoinProtocol(g.coin,g.testnet)
 
 
-	if not g.daemon_data_dir: g.daemon_data_dir = g.proto.daemon_data_dir
+	# global sets proto
+	if g.daemon_data_dir: g.proto.daemon_data_dir = g.daemon_data_dir
 
 
 #	g.proto is set, so we can set g.data_dir
 #	g.proto is set, so we can set g.data_dir
 	g.data_dir = os.path.normpath(os.path.join(g.data_dir_root,g.proto.data_subdir))
 	g.data_dir = os.path.normpath(os.path.join(g.data_dir_root,g.proto.data_subdir))
@@ -250,7 +276,7 @@ def init(opts_f,add_opts=[],opt_filter=None):
 
 
 	if g.bob or g.alice:
 	if g.bob or g.alice:
 		g.testnet = True
 		g.testnet = True
-		g.proto = get_coin_protocol(g.coin,g.testnet)
+		g.proto = CoinProtocol(g.coin,g.testnet)
 		g.data_dir = os.path.join(g.data_dir_root,'regtest',('alice','bob')[g.bob])
 		g.data_dir = os.path.join(g.data_dir_root,'regtest',('alice','bob')[g.bob])
 		check_or_create_dir(g.data_dir)
 		check_or_create_dir(g.data_dir)
 		import regtest as rt
 		import regtest as rt
@@ -263,6 +289,10 @@ def init(opts_f,add_opts=[],opt_filter=None):
 	if not check_opts(uopts):
 	if not check_opts(uopts):
 		sys.exit(1)
 		sys.exit(1)
 
 
+	if hasattr(g,'cfg_options_changed'):
+		ymsg("Warning: config file options have changed! See '{}' for details".format(g.cfg_file+'.sample'))
+		my_raw_input('Hit ENTER to continue: ')
+
 	if g.debug: opt_postproc_debug()
 	if g.debug: opt_postproc_debug()
 
 
 	# We don't need this data anymore
 	# We don't need this data anymore
@@ -308,8 +338,9 @@ def check_opts(usr_opts):       # Returns false if any check fails
 		if ret == False:
 		if ret == False:
 			msg("'{}': invalid {} (not a {} amount or satoshis-per-byte specification)".format(
 			msg("'{}': invalid {} (not a {} amount or satoshis-per-byte specification)".format(
 					val,desc,g.coin.upper()))
 					val,desc,g.coin.upper()))
-		elif ret != None and ret > g.max_tx_fee:
-			msg("'{}': invalid {} (> max_tx_fee ({} {}))".format(val,desc,g.max_tx_fee,g.coin.upper()))
+		elif ret != None and ret > g.proto.max_tx_fee:
+			msg("'{}': invalid {} (> max_tx_fee ({} {}))".format(
+					val,desc,g.proto.max_tx_fee,g.coin.upper()))
 		else:
 		else:
 			return True
 			return True
 		return False
 		return False
@@ -420,7 +451,11 @@ def check_opts(usr_opts):       # Returns false if any check fails
 			if not opt_compares(val,'<=',len(g.key_generators),desc): return False
 			if not opt_compares(val,'<=',len(g.key_generators),desc): return False
 			if not opt_compares(val,'>',0,desc): return False
 			if not opt_compares(val,'>',0,desc): return False
 		elif key == 'coin':
 		elif key == 'coin':
-			if not opt_is_in_list(val.upper(),g.coins,'coin'): return False
+			from mmgen.protocol import CoinProtocol
+			if not opt_is_in_list(val.lower(),CoinProtocol.coins.keys(),'coin'): return False
+		elif key == 'rbf':
+			if not g.proto.cap('rbf'):
+				die(1,'--rbf requested, but {} does not support replace-by-fee transactions'.format(g.coin))
 		elif key in ('bob','alice'):
 		elif key in ('bob','alice'):
 			from mmgen.regtest import daemon_dir
 			from mmgen.regtest import daemon_dir
 			m = "Regtest (Bob and Alice) mode not set up yet.  Run '{}-regtest setup' to initialize."
 			m = "Regtest (Bob and Alice) mode not set up yet.  Run '{}-regtest setup' to initialize."

+ 97 - 50
mmgen/protocol.py

@@ -23,6 +23,8 @@ protocol.py: Coin protocol functions, classes and methods
 import os,hashlib
 import os,hashlib
 from binascii import unhexlify
 from binascii import unhexlify
 from mmgen.util import msg,pmsg
 from mmgen.util import msg,pmsg
+from mmgen.obj import MMGenObject,BTCAmt,LTCAmt,BCHAmt
+from mmgen.globalvars import g
 
 
 def hash160(hexnum): # take hex, return hex - OP_HASH160
 def hash160(hexnum): # take hex, return hex - OP_HASH160
 	return hashlib.new('ripemd160',hashlib.sha256(unhexlify(hexnum)).digest()).hexdigest()
 	return hashlib.new('ripemd160',hashlib.sha256(unhexlify(hexnum)).digest()).hexdigest()
@@ -51,43 +53,38 @@ def _b58tonum(b58num):
 		if not i in _b58a: return False
 		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])))
 
 
-def get_coin_protocol(coin,testnet):
-	coin = coin.lower()
-	coins = {
-		'btc': (BitcoinProtocol,BitcoinTestnetProtocol),
-		'bch': (BitcoinCashProtocol,BitcoinCashTestnetProtocol),
-		'ltc': (LitecoinProtocol,LitecoinTestnetProtocol),
-		'eth': (EthereumProtocol,EthereumTestnetProtocol),
-	}
-	assert type(testnet) == bool
-	assert coin in coins
-	return coins[coin][testnet]
-
-from mmgen.obj import MMGenObject
-from mmgen.globalvars import g
-
 class BitcoinProtocol(MMGenObject):
 class BitcoinProtocol(MMGenObject):
-	# devdoc/ref_transactions.md:
-	addr_ver_num         = { 'p2pkh': ('00','1'), 'p2sh':  ('05','3') }
-	addr_pfxs            = '13'
-	uncompressed_wif_pfx = '5'
-	privkey_pfx          = '80'
-	mmtypes              = ('L','C','S')
-	data_subdir          = ''
-	rpc_port             = 8332
+	name            = 'bitcoin'
+	daemon_name     = 'bitcoind'
+	addr_ver_num    = { 'p2pkh': ('00','1'), 'p2sh':  ('05','3') } # chainparams.cpp
+	privkey_pfx     = '80'
+	mmtypes         = ('L','C','S')
+	data_subdir     = ''
+	rpc_port        = 8332
+	secs_per_block  = 600
+	coin_amt        = BTCAmt
+	max_tx_fee      = BTCAmt('0.01')
 	daemon_data_dir = os.path.join(os.getenv('APPDATA'),'Bitcoin') if g.platform == 'win' \
 	daemon_data_dir = os.path.join(os.getenv('APPDATA'),'Bitcoin') if g.platform == 'win' \
 						else os.path.join(g.home_dir,'.bitcoin')
 						else os.path.join(g.home_dir,'.bitcoin')
+	daemon_data_subdir = ''
 	sighash_type = 'ALL'
 	sighash_type = 'ALL'
 	block0 = '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f'
 	block0 = '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f'
 	forks = [
 	forks = [
 		(478559,'00000000000000000019f112ec0a9982926f1258cdcc558dd7c3b7e5dc7fa148','bch')
 		(478559,'00000000000000000019f112ec0a9982926f1258cdcc558dd7c3b7e5dc7fa148','bch')
 	]
 	]
+	caps = ('rbf','segwit')
+	base_coin = 'BTC'
+
+	@staticmethod
+	def get_protocol_by_chain(chain):
+		return CoinProtocol(g.coin,{'mainnet':False,'testnet':True,'regtest':True}[chain])
+
+	@staticmethod
+	def get_rpc_coin_amt_type():
+		return (float,str)[g.daemon_version>=120000]
 
 
 	@classmethod
 	@classmethod
-	def get_chain_protocol(cls,chain):
-		chain_protos = { 'mainnet':'', 'testnet':'Testnet', 'regtest':'Testnet' }
-		assert chain in chain_protos
-		return globals()['Bitcoin{}Protocol'.format(chain_protos[chain])]
+	def cap(cls,s): return s in cls.caps
 
 
 	@classmethod
 	@classmethod
 	def hex2wif(cls,hexpriv,compressed=False):
 	def hex2wif(cls,hexpriv,compressed=False):
@@ -99,9 +96,10 @@ class BitcoinProtocol(MMGenObject):
 		num = _b58tonum(wif)
 		num = _b58tonum(wif)
 		if num == False: return False
 		if num == False: return False
 		key = '{:x}'.format(num)
 		key = '{:x}'.format(num)
-		compressed = wif[0] != cls.uncompressed_wif_pfx
-		klen = (66,68)[bool(compressed)]
+		if len(key) not in (74,76): return False
+		compressed = len(key) == 76
 		if compressed and key[66:68] != '01': return False
 		if compressed and key[66:68] != '01': return False
+		klen = (66,68)[compressed]
 		if (key[:2] == cls.privkey_pfx and key[klen:] == hash256(key[:klen])[:8]):
 		if (key[:2] == cls.privkey_pfx and key[klen:] == hash256(key[:klen])[:8]):
 			return { 'hex':key[2:66], 'compressed':compressed }
 			return { 'hex':key[2:66], 'compressed':compressed }
 		else:
 		else:
@@ -109,7 +107,7 @@ class BitcoinProtocol(MMGenObject):
 
 
 	@classmethod
 	@classmethod
 	def verify_addr(cls,addr,verbose=False,return_dict=False):
 	def verify_addr(cls,addr,verbose=False,return_dict=False):
-		for addr_fmt in ('p2pkh','p2sh'):
+		for addr_fmt in cls.addr_ver_num:
 			ver_num,ldigit = cls.addr_ver_num[addr_fmt]
 			ver_num,ldigit = cls.addr_ver_num[addr_fmt]
 			if addr[0] not in ldigit: continue
 			if addr[0] not in ldigit: continue
 			num = _b58tonum(addr)
 			num = _b58tonum(addr)
@@ -117,7 +115,10 @@ class BitcoinProtocol(MMGenObject):
 			addr_hex = '{:050x}'.format(num)
 			addr_hex = '{:050x}'.format(num)
 			if addr_hex[:2] != ver_num: continue
 			if addr_hex[:2] != ver_num: continue
 			if hash256(addr_hex[:42])[:8] == addr_hex[42:]:
 			if hash256(addr_hex[:42])[:8] == addr_hex[42:]:
-				return { 'hex':addr_hex[2:42], 'format':addr_fmt } if return_dict else True
+				return {
+					'hex':    addr_hex[2:42],
+					'format': {'p2pkh':'p2pkh','p2sh':'p2sh','p2sh2':'p2sh'}[addr_fmt],
+				} if return_dict else True
 			else:
 			else:
 				if verbose: Msg("Invalid checksum in address '{}'".format(addr))
 				if verbose: Msg("Invalid checksum in address '{}'".format(addr))
 				break
 				break
@@ -125,9 +126,9 @@ class BitcoinProtocol(MMGenObject):
 		return False
 		return False
 
 
 	@classmethod
 	@classmethod
-	def hexaddr2addr(cls,hexaddr,p2sh=False):
+	def hexaddr2addr(cls,hexaddr,p2sh):
 		s = cls.addr_ver_num[('p2pkh','p2sh')[p2sh]][0] + hexaddr
 		s = cls.addr_ver_num[('p2pkh','p2sh')[p2sh]][0] + hexaddr
-		lzeroes = (len(s) - len(s.lstrip('0'))) / 2
+		lzeroes = (len(s) - len(s.lstrip('0'))) / 2 # non-zero only for ver num '00' (BTC p2pkh)
 		return ('1' * lzeroes) + _numtob58(int(s+hash256(s)[:8],16))
 		return ('1' * lzeroes) + _numtob58(int(s+hash256(s)[:8],16))
 
 
 	# Segwit:
 	# Segwit:
@@ -144,38 +145,84 @@ class BitcoinProtocol(MMGenObject):
 
 
 class BitcoinTestnetProtocol(BitcoinProtocol):
 class BitcoinTestnetProtocol(BitcoinProtocol):
 	addr_ver_num         = { 'p2pkh': ('6f','mn'), 'p2sh':  ('c4','2') }
 	addr_ver_num         = { 'p2pkh': ('6f','mn'), 'p2sh':  ('c4','2') }
-	addr_pfxs            = 'mn2'
-	uncompressed_wif_pfx = '9'
 	privkey_pfx          = 'ef'
 	privkey_pfx          = 'ef'
-	data_subdir          = 'testnet3'
+	data_subdir          = 'testnet'
+	daemon_data_subdir   = 'testnet3'
 	rpc_port             = 18332
 	rpc_port             = 18332
 
 
 class BitcoinCashProtocol(BitcoinProtocol):
 class BitcoinCashProtocol(BitcoinProtocol):
 	# TODO: assumes MSWin user installs in custom dir 'Bitcoin_ABC'
 	# TODO: assumes MSWin user installs in custom dir 'Bitcoin_ABC'
+	daemon_name    = 'bitcoind-abc'
 	daemon_data_dir = os.path.join(os.getenv('APPDATA'),'Bitcoin_ABC') if g.platform == 'win' \
 	daemon_data_dir = os.path.join(os.getenv('APPDATA'),'Bitcoin_ABC') if g.platform == 'win' \
 						else os.path.join(g.home_dir,'.bitcoin-abc')
 						else os.path.join(g.home_dir,'.bitcoin-abc')
-	rpc_port     = 8442
-	mmtypes      = ('L','C')
-	sighash_type = 'ALL|FORKID'
-	block0 = '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f'
+	rpc_port       = 8442
+	mmtypes        = ('L','C')
+	sighash_type   = 'ALL|FORKID'
 	forks = [
 	forks = [
 		(478559,'000000000000000000651ef99cb9fcbe0dadde1d424bd9f15ff20136191a5eec','btc')
 		(478559,'000000000000000000651ef99cb9fcbe0dadde1d424bd9f15ff20136191a5eec','btc')
 	]
 	]
+	caps = ()
+	coin_amt        = BCHAmt
+	max_tx_fee      = BCHAmt('0.1')
 
 
 	@classmethod
 	@classmethod
 	def pubhex2redeem_script(cls,pubhex): raise NotImplementedError
 	def pubhex2redeem_script(cls,pubhex): raise NotImplementedError
 	@classmethod
 	@classmethod
 	def pubhex2segwitaddr(cls,pubhex):    raise NotImplementedError
 	def pubhex2segwitaddr(cls,pubhex):    raise NotImplementedError
 
 
-class BitcoinCashTestnetProtocol(BitcoinTestnetProtocol):
-	rpc_port = 18442
-	@classmethod
-	def pubhex2redeem_script(cls,pubhex): raise NotImplementedError
-	@classmethod
-	def pubhex2segwitaddr(cls,pubhex):    raise NotImplementedError
-
-class LitecoinProtocol(BitcoinProtocol): pass
-class LitecoinTestnetProtocol(LitecoinProtocol): pass
+class BitcoinCashTestnetProtocol(BitcoinCashProtocol):
+	rpc_port      = 18442
+	addr_ver_num  = { 'p2pkh': ('6f','mn'), 'p2sh':  ('c4','2') }
+	privkey_pfx   = 'ef'
+	data_subdir   = 'testnet'
+	daemon_data_subdir = 'testnet3'
+
+class LitecoinProtocol(BitcoinProtocol):
+	block0         = '12a765e31ffd4059bada1e25190f6e98c99d9714d334efa41a195a7e7e04bfe2'
+	name           = 'litecoin'
+	daemon_name    = 'litecoind'
+	daemon_data_dir = os.path.join(os.getenv('APPDATA'),'Litecoin') if g.platform == 'win' \
+						else os.path.join(g.home_dir,'.litecoin')
+	addr_ver_num   = { 'p2pkh': ('30','L'), 'p2sh':  ('32','M'), 'p2sh2':  ('05','3') } # 'p2sh' is new fmt
+	privkey_pfx    = 'b0'
+	secs_per_block = 150
+	rpc_port       = 9332
+	coin_amt       = LTCAmt
+	max_tx_fee     = LTCAmt('0.3')
+	base_coin      = 'LTC'
+	forks          = []
+
+class LitecoinTestnetProtocol(LitecoinProtocol):
+	# addr ver nums same as Bitcoin testnet, except for 'p2sh'
+	addr_ver_num   = { 'p2pkh': ('6f','mn'), 'p2sh':  ('3a','Q'), 'p2sh2':  ('c4','2') }
+	privkey_pfx    = 'ef' # same as Bitcoin testnet
+	data_subdir    = 'testnet'
+	daemon_data_subdir = 'testnet4'
+	rpc_port       = 19332
+
+class EthereumProtocol(MMGenObject):
+	base_coin      = 'ETH'
 
 
-class EthereumProtocol(MMGenObject): pass
 class EthereumTestnetProtocol(EthereumProtocol): pass
 class EthereumTestnetProtocol(EthereumProtocol): pass
+
+class CoinProtocol(MMGenObject):
+	coins = {
+		'btc': (BitcoinProtocol,BitcoinTestnetProtocol),
+		'bch': (BitcoinCashProtocol,BitcoinCashTestnetProtocol),
+		'ltc': (LitecoinProtocol,LitecoinTestnetProtocol),
+#		'eth': (EthereumProtocol,EthereumTestnetProtocol),
+	}
+	def __new__(cls,coin,testnet):
+		coin = coin.lower()
+		assert type(testnet) == bool
+		if coin not in cls.coins:
+			from mmgen.util import die
+			die(1,"'{}': not a valid coin. Valid choices are '{}'".format(coin,"','".join(cls.coins)))
+		return cls.coins[coin][testnet]
+
+	@classmethod
+	def get_base_coin_from_name(cls,name):
+		for proto,foo in cls.coins.values():
+			if name == proto.__name__[:-8].lower():
+				return proto.base_coin
+		return False

+ 22 - 18
mmgen/regtest.py

@@ -17,7 +17,7 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
 """
 """
-regtest: Bitcoind regression test mode setup and operations for the MMGen suite
+regtest: Coin daemon regression test mode setup and operations for the MMGen suite
 """
 """
 
 
 import os,subprocess,time,shutil
 import os,subprocess,time,shutil
@@ -42,7 +42,7 @@ common_args = (
 
 
 def start_daemon(user,quiet=False,daemon=True):
 def start_daemon(user,quiet=False,daemon=True):
 	cmd = (
 	cmd = (
-		'bitcoind',
+		g.proto.daemon_name,
 		'-keypool=1',
 		'-keypool=1',
 		'-wallet={}'.format(os.path.basename(tr_wallet(user)))
 		'-wallet={}'.format(os.path.basename(tr_wallet(user)))
 	) + common_args
 	) + common_args
@@ -51,7 +51,7 @@ def start_daemon(user,quiet=False,daemon=True):
 	p = subprocess.Popen(cmd,stdout=PIPE,stderr=PIPE)
 	p = subprocess.Popen(cmd,stdout=PIPE,stderr=PIPE)
 	err = process_output(p,silent=False)[1]
 	err = process_output(p,silent=False)[1]
 	if err:
 	if err:
-		rdie(1,'Error starting the Bitcoin daemon:\n{}'.format(err))
+		rdie(1,'Error starting the {} daemon:\n{}'.format(g.proto.name.capitalize(),err))
 
 
 def start_daemon_mswin(user,quiet=False):
 def start_daemon_mswin(user,quiet=False):
 	import threading
 	import threading
@@ -63,7 +63,7 @@ def start_daemon_mswin(user,quiet=False):
 def start_cmd(*args,**kwargs):
 def start_cmd(*args,**kwargs):
 	cmd = args
 	cmd = args
 	if args[0] == 'cli':
 	if args[0] == 'cli':
-		cmd = ('bitcoin-cli',) + common_args + args[1:]
+		cmd = (g.proto.name+'-cli',) + common_args + args[1:]
 	if g.debug or not 'quiet' in kwargs:
 	if g.debug or not 'quiet' in kwargs:
 		vmsg('{}'.format(' '.join(cmd)))
 		vmsg('{}'.format(' '.join(cmd)))
 	ip = op = ep = (PIPE,None)['no_pipe' in kwargs and kwargs['no_pipe']]
 	ip = op = ep = (PIPE,None)['no_pipe' in kwargs and kwargs['no_pipe']]
@@ -93,19 +93,22 @@ def wait_for_daemon(state,silent=False,nonl=False):
 def get_balances():
 def get_balances():
 	user1 = get_current_user(quiet=True)
 	user1 = get_current_user(quiet=True)
 	if user1 == None:
 	if user1 == None:
-		die(1,'Regtest daemon not running')
+		user('bob')
+		user1 = get_current_user(quiet=True)
+#		die(1,'Regtest daemon not running')
 	user2 = ('bob','alice')[user1=='bob']
 	user2 = ('bob','alice')[user1=='bob']
 	tbal = 0
 	tbal = 0
-	from mmgen.obj import BTCAmt
+	# don't need to save and restore these, as we exit immediately
+	g.rpc_host = 'localhost'
+	g.rpc_port = rpc_port
+	g.rpc_user = rpc_user
+	g.rpc_password = rpc_password
+	g.testnet = True
+	rpc_init()
 	for u in (user1,user2):
 	for u in (user1,user2):
-		p = start_cmd('python','mmgen-tool',
-				'--{}'.format(u),'--data-dir='+g.data_dir,
-					'getbalance','quiet=1')
-		bal = p.stdout.read().replace(' \b','') # hack
+		bal = g.proto.coin_amt(g.rpch.getbalance('*',0,True))
 		if u == user1: user(user2)
 		if u == user1: user(user2)
-		bal = BTCAmt(bal)
-		ustr = "{}'s balance:".format(u.capitalize())
-		msg('{:<16} {:12}'.format(ustr,bal))
+		msg('{:<16} {:12}'.format(u.capitalize()+"'s balance:",bal))
 		tbal += bal
 		tbal += bal
 	msg('{:<16} {:12}'.format('Total balance:',tbal))
 	msg('{:<16} {:12}'.format('Total balance:',tbal))
 
 
@@ -131,7 +134,7 @@ def process_output(p,silent=False):
 	return out,err
 	return out,err
 
 
 def start_and_wait(user,silent=False,nonl=False):
 def start_and_wait(user,silent=False,nonl=False):
-	vmsg('Starting bitcoin regtest daemon')
+	vmsg('Starting {} regtest daemon'.format(g.proto.name))
 	(start_daemon_mswin,start_daemon)[g.platform=='linux'](user)
 	(start_daemon_mswin,start_daemon)[g.platform=='linux'](user)
 	wait_for_daemon('ready',silent=silent,nonl=nonl)
 	wait_for_daemon('ready',silent=silent,nonl=nonl)
 
 
@@ -141,7 +144,7 @@ def stop_and_wait(silent=False,nonl=False,stop_silent=False,ignore_noconnect_err
 
 
 def send(addr,amt):
 def send(addr,amt):
 	user('miner')
 	user('miner')
-	gmsg('Sending {} BTC to address {}'.format(amt,addr))
+	gmsg('Sending {} {} to address {}'.format(amt,g.coin,addr))
 	p = start_cmd('cli','sendtoaddress',addr,str(amt))
 	p = start_cmd('cli','sendtoaddress',addr,str(amt))
 	process_output(p)
 	process_output(p)
 	p.wait()
 	p.wait()
@@ -189,7 +192,7 @@ def get_current_user_win(quiet=False):
 	return None
 	return None
 
 
 def get_current_user_unix(quiet=False):
 def get_current_user_unix(quiet=False):
-	p = start_cmd('pgrep','-af', 'bitcoind.*-rpcuser={}.*'.format(rpc_user))
+	p = start_cmd('pgrep','-af','{}.*-rpcuser={}.*'.format(g.proto.daemon_name,rpc_user))
 	cmdline = p.stdout.read()
 	cmdline = p.stdout.read()
 	if not cmdline: return None
 	if not cmdline: return None
 	for k in ('miner','bob','alice'):
 	for k in ('miner','bob','alice'):
@@ -215,6 +218,7 @@ def user(user=None,quiet=False):
 			return True
 			return True
 		gmsg_r('Switching to user {}'.format(user.capitalize()))
 		gmsg_r('Switching to user {}'.format(user.capitalize()))
 		stop_and_wait(silent=False,nonl=True,stop_silent=True)
 		stop_and_wait(silent=False,nonl=True,stop_silent=True)
+		time.sleep(0.1) # file lock has race condition - TODO: test for lock file
 		start_and_wait(user,nonl=True)
 		start_and_wait(user,nonl=True)
 	else:
 	else:
 		gmsg_r('Starting regtest daemon with current user {}'.format(user.capitalize()))
 		gmsg_r('Starting regtest daemon with current user {}'.format(user.capitalize()))
@@ -223,12 +227,12 @@ def user(user=None,quiet=False):
 
 
 def stop(silent=False,ignore_noconnect_error=True):
 def stop(silent=False,ignore_noconnect_error=True):
 	if test_daemon() != 'stopped' and not silent:
 	if test_daemon() != 'stopped' and not silent:
-		gmsg('Stopping bitcoin regtest daemon')
+		gmsg('Stopping {} regtest daemon'.format(g.proto.name))
 	p = start_cmd('cli','stop')
 	p = start_cmd('cli','stop')
 	err = process_output(p)[1]
 	err = process_output(p)[1]
 	if err:
 	if err:
 		if "couldn't connect to server" in err and not ignore_noconnect_error:
 		if "couldn't connect to server" in err and not ignore_noconnect_error:
-			rdie(1,'Error stopping the Bitcoin daemon:\n{}'.format(err))
+			rdie(1,'Error stopping the {} daemon:\n{}'.format(g.proto.name.capitalize(),err))
 		msg(err)
 		msg(err)
 	return p.wait()
 	return p.wait()
 
 

+ 24 - 20
mmgen/rpc.py

@@ -17,20 +17,19 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
 """
 """
-rpc.py:  Bitcoin RPC library for the MMGen suite
+rpc.py:  Cryptocoin RPC library for the MMGen suite
 """
 """
 
 
 import httplib,base64,json
 import httplib,base64,json
 
 
 from mmgen.common import *
 from mmgen.common import *
 from decimal import Decimal
 from decimal import Decimal
-from mmgen.obj import BTCAmt
 
 
-class BitcoinRPCConnection(object):
+class CoinDaemonRPCConnection(object):
 
 
 	def __init__(self,host=None,port=None,user=None,passwd=None,auth_cookie=None):
 	def __init__(self,host=None,port=None,user=None,passwd=None,auth_cookie=None):
 
 
-		dmsg('=== BitcoinRPCConnection.__init__() debug ===')
+		dmsg('=== CoinDaemonRPCConnection.__init__() debug ===')
 		dmsg('    host [{}] port [{}] user [{}] passwd [{}] auth_cookie [{}]\n'.format(
 		dmsg('    host [{}] port [{}] user [{}] passwd [{}] auth_cookie [{}]\n'.format(
 			host,port,user,passwd,auth_cookie))
 			host,port,user,passwd,auth_cookie))
 
 
@@ -39,19 +38,23 @@ class BitcoinRPCConnection(object):
 		elif auth_cookie:
 		elif auth_cookie:
 			self.auth_str = auth_cookie
 			self.auth_str = auth_cookie
 		else:
 		else:
-			msg('Error: no Bitcoin RPC authentication method found')
-			if passwd: die(1,"'rpcuser' entry not found in bitcoin.conf or mmgen.cfg")
-			elif user: die(1,"'rpcpassword' entry not found in bitcoin.conf or mmgen.cfg")
+			msg('Error: no {} RPC authentication method found'.format(g.proto.name.capitalize()))
+			if passwd: die(1,"'rpcuser' entry not found in {}.conf or mmgen.cfg".format(g.proto.name))
+			elif user: die(1,"'rpcpassword' entry not found in {}.conf or mmgen.cfg".format(g.proto.name))
 			else:
 			else:
-				m1 = 'Either provide rpcuser/rpcpassword in bitcoin.conf or mmgen.cfg'
-				m2 = '(or, alternatively, copy the authentication cookie to Bitcoin data dir'
-				m3 = 'if {} and Bitcoin are running as different users)'.format(g.proj_name)
-				die(1,'\n'.join((m1,m2,m3)))
+				m1 = 'Either provide rpcuser/rpcpassword in {pn}.conf or mmgen.cfg\n'
+				m2 = '(or, alternatively, copy the authentication cookie to the {pnu}\n'
+				m3 = 'data dir if {pnm} and {dn} are running as different users)'
+				die(1,(m1+m2+m3).format(
+					pn=g.proto.name,
+					pnu=g.proto.name.capitalize(),
+					dn=g.proto.daemon_name,
+					pnm=g.proj_name))
 
 
 		self.host = host
 		self.host = host
 		self.port = port
 		self.port = port
 
 
-	# Normal mode: call with arg list unrolled, exactly as with 'bitcoin-cli'
+	# Normal mode: call with arg list unrolled, exactly as with cli
 	# Batch mode:  call with list of arg lists as first argument
 	# Batch mode:  call with list of arg lists as first argument
 	# kwargs are for local use and are not passed to server
 	# kwargs are for local use and are not passed to server
 
 
@@ -84,8 +87,8 @@ class BitcoinRPCConnection(object):
 		caller = self
 		caller = self
 		class MyJSONEncoder(json.JSONEncoder):
 		class MyJSONEncoder(json.JSONEncoder):
 			def default(self, obj):
 			def default(self, obj):
-				if isinstance(obj, BTCAmt):
-					return (float,str)[g.bitcoind_version>=120000](obj)
+				if isinstance(obj,g.proto.coin_amt):
+					return g.proto.get_rpc_coin_amt_type()(obj)
 				return json.JSONEncoder.default(self, obj)
 				return json.JSONEncoder.default(self, obj)
 
 
 		# TODO: UTF-8 labels
 		# TODO: UTF-8 labels
@@ -101,20 +104,20 @@ class BitcoinRPCConnection(object):
 				'Authorization': 'Basic {}'.format(base64.b64encode(self.auth_str))
 				'Authorization': 'Basic {}'.format(base64.b64encode(self.auth_str))
 			})
 			})
 		except Exception as e:
 		except Exception as e:
-			m = '{}\nUnable to connect to bitcoind at {}:{}'
-			return die_maybe(None,2,m.format(e,self.host,self.port))
+			m = '{}\nUnable to connect to {} at {}:{}'
+			return die_maybe(None,2,m.format(e,g.proto.daemon_name,self.host,self.port))
 
 
 		try:
 		try:
 			r = hc.getresponse() # returns HTTPResponse instance
 			r = hc.getresponse() # returns HTTPResponse instance
 		except Exception:
 		except Exception:
-			m = 'Unable to connect to bitcoind at {}:{} (but port is bound?)'
-			return die_maybe(None,2,m.format(self.host,self.port))
+			m = 'Unable to connect to {} at {}:{} (but port is bound?)'
+			return die_maybe(None,2,m.format(g.proto.daemon_name,self.host,self.port))
 
 
 		dmsg('    RPC GETRESPONSE data ==> %s\n' % r.__dict__)
 		dmsg('    RPC GETRESPONSE data ==> %s\n' % r.__dict__)
 
 
 		if r.status != 200:
 		if r.status != 200:
 			if cf['on_fail'] != 'silent':
 			if cf['on_fail'] != 'silent':
-				msg_r(yellow('Bitcoind RPC Error: '))
+				msg_r(yellow('{} RPC Error: '.format(g.proto.daemon_name.capitalize())))
 				msg(red('{} {}'.format(r.status,r.reason)))
 				msg(red('{} {}'.format(r.status,r.reason)))
 			e1 = r.read()
 			e1 = r.read()
 			try:
 			try:
@@ -137,7 +140,8 @@ class BitcoinRPCConnection(object):
 
 
 		for resp in r3 if cf['batch'] else [r3]:
 		for resp in r3 if cf['batch'] else [r3]:
 			if 'error' in resp and resp['error'] != None:
 			if 'error' in resp and resp['error'] != None:
-				return die_maybe(r,1,'Bitcoind returned an error: %s' % resp['error'])
+				return die_maybe(r,1,'{} returned an error: {}'.format(
+					g.proto.daemon_name.capitalize(),resp['error']))
 			elif 'result' not in resp:
 			elif 'result' not in resp:
 				return die_maybe(r,1, 'Missing JSON-RPC result\n' + repr(resps))
 				return die_maybe(r,1, 'Missing JSON-RPC result\n' + repr(resps))
 			else:
 			else:

+ 185 - 162
mmgen/tool.py

@@ -61,7 +61,7 @@ cmd_data = OrderedDict([
 	('Wif2hex',    ['<wif> [str-]']),
 	('Wif2hex',    ['<wif> [str-]']),
 	('Wif2addr',   ['<wif> [str-]','segwit [bool=False]']),
 	('Wif2addr',   ['<wif> [str-]','segwit [bool=False]']),
 	('Wif2segwit_pair',['<wif> [str-]']),
 	('Wif2segwit_pair',['<wif> [str-]']),
-	('Hexaddr2addr', ['<coin address in hex format> [str-]']),
+	('Hexaddr2addr', ['<coin address in hex format> [str-]','p2sh [bool=False]']),
 	('Addr2hexaddr', ['<coin address> [str-]']),
 	('Addr2hexaddr', ['<coin address> [str-]']),
 	('Privhex2addr', ['<private key in hex format> [str-]','compressed [bool=False]','segwit [bool=False]']),
 	('Privhex2addr', ['<private key in hex format> [str-]','compressed [bool=False]','segwit [bool=False]']),
 	('Privhex2pubhex',['<private key in hex format> [str-]','compressed [bool=False]']),
 	('Privhex2pubhex',['<private key in hex format> [str-]','compressed [bool=False]']),
@@ -78,9 +78,9 @@ cmd_data = OrderedDict([
 	('Mn_printlist', ["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]']),
 	('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=True]','all_labels [bool=False]']),
+	('Listaddresses',["addrs [str='']",'minconf [int=1]','showempty [bool=False]','pager [bool=False]','showbtcaddrs [bool=True]','all_labels [bool=False]',"sort [str=''] (options: reverse, age)"]),
 	('Getbalance',   ['minconf [int=1]','quiet [bool=False]']),
 	('Getbalance',   ['minconf [int=1]','quiet [bool=False]']),
-	('Txview',       ['<{} TX file(s)> [str]'.format(pnm),'pager [bool=False]','terse [bool=False]',"sort [str='mtime'] (options: 'ctime','atime')",'MARGS']),
+	('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]']),
 	('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]']),
 	('Add_label',       ['<{} address> [str]'.format(pnm),'<label> [str]']),
@@ -279,7 +279,7 @@ def Wif2segwit_pair(wif):
 	rs = ag.to_segwit_redeem_script(pubhex)
 	rs = ag.to_segwit_redeem_script(pubhex)
 	Msg('{}\n{}'.format(rs,addr))
 	Msg('{}\n{}'.format(rs,addr))
 
 
-def Hexaddr2addr(hexaddr):                     Msg(g.proto.hexaddr2addr(hexaddr))
+def Hexaddr2addr(hexaddr,p2sh=False):          Msg(g.proto.hexaddr2addr(hexaddr,p2sh=p2sh))
 def Addr2hexaddr(addr):                        Msg(g.proto.verify_addr(addr,return_dict=True)['hex'])
 def Addr2hexaddr(addr):                        Msg(g.proto.verify_addr(addr,return_dict=True)['hex'])
 def Hash160(pubkeyhex):                        Msg(hash160(pubkeyhex))
 def Hash160(pubkeyhex):                        Msg(hash160(pubkeyhex))
 def Pubhex2addr(pubkeyhex,p2sh=False):         Msg(g.proto.hexaddr2addr(hash160(pubkeyhex),p2sh=p2sh))
 def Pubhex2addr(pubkeyhex,p2sh=False):         Msg(g.proto.hexaddr2addr(hash160(pubkeyhex),p2sh=p2sh))
@@ -353,13 +353,160 @@ def Id6(infile):
 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())))
 	Msg(make_chksum_6(''.join(s.split())))
 
 
+def Addrfile_chksum(infile):
+	from mmgen.addr import AddrList
+	AddrList(infile,chksum_only=True)
+
+def Keyaddrfile_chksum(infile):
+	from mmgen.addr import KeyAddrList
+	KeyAddrList(infile,chksum_only=True)
+
+def Passwdfile_chksum(infile):
+	from mmgen.addr import PasswordList
+	PasswordList(infile=infile,chksum_only=True)
+
+def Hexreverse(s):
+	Msg(binascii.hexlify(binascii.unhexlify(s.strip())[::-1]))
+
+def Hexlify(s):
+	Msg(binascii.hexlify(s))
+
+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 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:
+		outfile = '%s.%s' % (os.path.basename(infile),g.mmenc_ext)
+
+	write_data_to_file(outfile,enc_d,'encrypted data',binary=True)
+
+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)
+		if dec_d: break
+		msg('Trying again...')
+
+	if not outfile:
+		o = os.path.basename(infile)
+		outfile = remove_extension(o,g.mmenc_ext)
+		if outfile == o: outfile += '.dec'
+
+	write_data_to_file(outfile,dec_d,'decrypted data',binary=True)
+
+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
+	f = os.open(filename,flgs)
+	for ch in iv_id:
+		if ch not in '0123456789ABCDEF':
+			die(2,"'%s': invalid Incog ID" % iv_id)
+	while True:
+		d = os.read(f,bsize)
+		if not d: break
+		d = carry + d
+		for i in range(bsize):
+			if sha256(d[i:i+ivsize]).hexdigest()[:8].upper() == iv_id:
+				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(0)
+		carry = d[len(d)-ivsize:]
+		n += bsize
+		if not n % mod: msg_r('\rSearched: %s bytes' % n)
+
+	msg('')
+	os.close(f)
+
+def Rand2file(outfile, nbytes, threads=4, silent=False):
+	nbytes = parse_nbytes(nbytes)
+	from Crypto import Random
+	rh = Random.new()
+	from Queue import Queue
+	from threading import Thread
+	bsize = 2**20
+	roll = bsize * 4
+	if opt.outdir: outfile = make_full_path(opt.outdir,outfile)
+	f = open(outfile,'wb')
+
+	from Crypto.Cipher import AES
+	from Crypto.Util import Counter
+
+	key = get_random(32)
+
+	def encrypt_worker(wid):
+		while True:
+			i,d = q1.get()
+			c = AES.new(key, AES.MODE_CTR,
+					counter=Counter.new(g.aesctr_iv_len*8,initial_value=i))
+			enc_data = c.encrypt(d)
+			q2.put(enc_data)
+			q1.task_done()
+
+	def output_worker():
+		while True:
+			data = q2.get()
+			f.write(data)
+			q2.task_done()
+
+	q1 = Queue()
+	for i in range(max(1,threads-2)):
+		t = Thread(target=encrypt_worker, args=(i,))
+		t.daemon = True
+		t.start()
+
+	q2 = Queue()
+	t = Thread(target=output_worker)
+	t.daemon = True
+	t.start()
+
+	i = 1; rbytes = nbytes
+	while rbytes > 0:
+		d = rh.read(min(bsize,rbytes))
+		q1.put((i,d))
+		rbytes -= bsize
+		i += 1
+		if not (bsize*i) % roll:
+			msg_r('\rRead: %s bytes' % (bsize*i))
+
+	if not silent:
+		msg('\rRead: %s bytes' % nbytes)
+		qmsg("\r%s bytes of random data written to file '%s'" % (nbytes,outfile))
+	q1.join()
+	q2.join()
+	f.close()
+
+def Bytespec(s): Msg(str(parse_nbytes(s)))
+
+def Regtest_setup():
+	print 'ok'
+	return
+	import subprocess as sp
+	sp.check_output()
+	pass
+
+# ================ RPC commands ================== #
+
 def Listaddress(addr,minconf=1,pager=False,showempty=True,showbtcaddr=True):
 def Listaddress(addr,minconf=1,pager=False,showempty=True,showbtcaddr=True):
 	return Listaddresses(addrs=addr,minconf=minconf,pager=pager,showempty=showempty,showbtcaddrs=showbtcaddr)
 	return Listaddresses(addrs=addr,minconf=minconf,pager=pager,showempty=showempty,showbtcaddrs=showbtcaddr)
 
 
 # List MMGen addresses and their balances.  TODO: move this code to AddrList
 # List MMGen addresses and their balances.  TODO: move this code to AddrList
-def Listaddresses(addrs='',minconf=1,showempty=False,pager=False,showbtcaddrs=True,all_labels=False):
+def Listaddresses(addrs='',minconf=1,showempty=False,pager=False,showbtcaddrs=True,all_labels=False,sort=None):
 
 
-	c = rpc_connection()
+	if sort:
+		sort = set(sort.split(','))
+		sort_params = set(['reverse','age'])
+		if not sort.issubset(sort_params):
+			die(1,"The sort option takes the following parameters: '{}'".format("','".join(sort_params)))
+
+	rpc_init()
 
 
 	def check_dup_mmid(acct_labels):
 	def check_dup_mmid(acct_labels):
 		mmid_prev,err = None,False
 		mmid_prev,err = None,False
@@ -393,9 +540,9 @@ def Listaddresses(addrs='',minconf=1,showempty=False,pager=False,showbtcaddrs=Tr
 	class TwAddrList(dict,MMGenObject): pass
 	class TwAddrList(dict,MMGenObject): pass
 
 
 	addrs = TwAddrList() # reusing name!
 	addrs = TwAddrList() # reusing name!
-	total = BTCAmt('0')
+	total = g.proto.coin_amt('0')
 
 
-	for d in c.listunspent(0):
+	for d in g.rpch.listunspent(0):
 		if not 'account' in d: continue  # skip coinbase outputs with missing account
 		if not 'account' in d: continue  # skip coinbase outputs with missing account
 		if d['confirmations'] < minconf: continue
 		if d['confirmations'] < minconf: continue
 		label = TwLabel(d['account'],on_fail='silent')
 		label = TwLabel(d['account'],on_fail='silent')
@@ -406,9 +553,10 @@ def Listaddresses(addrs='',minconf=1,showempty=False,pager=False,showbtcaddrs=Tr
 					die(2,'duplicate {} address ({}) for this MMGen address! ({})'.format(
 					die(2,'duplicate {} address ({}) for this MMGen address! ({})'.format(
 							g.coin,d['address'],addrs[label.mmid]['addr']))
 							g.coin,d['address'],addrs[label.mmid]['addr']))
 			else:
 			else:
-				addrs[label.mmid] = { 'amt':  BTCAmt('0'),
-									  'lbl':  label,
-									  'addr': CoinAddr(d['address'])}
+				addrs[label.mmid] = {'amt': g.proto.coin_amt('0'),
+									'lbl':  label,
+									'addr': CoinAddr(d['address'])}
+				addrs[label.mmid]['lbl'].mmid.confs = d['confirmations']
 			addrs[label.mmid]['amt'] += d['amount']
 			addrs[label.mmid]['amt'] += d['amount']
 			total += d['amount']
 			total += d['amount']
 
 
@@ -416,10 +564,10 @@ def Listaddresses(addrs='',minconf=1,showempty=False,pager=False,showbtcaddrs=Tr
 	if showempty or all_labels:
 	if showempty or all_labels:
 		# for compatibility with old mmids, must use raw RPC rather than native data for matching
 		# for compatibility with old mmids, must use raw RPC rather than native data for matching
 		# args: minconf,watchonly, MUST use keys() so we get list, not dict
 		# args: minconf,watchonly, MUST use keys() so we get list, not dict
-		acct_list = c.listaccounts(0,True).keys() # raw list, no 'L'
+		acct_list = g.rpch.listaccounts(0,True).keys() # raw list, no 'L'
 		acct_labels = MMGenList([TwLabel(a,on_fail='silent') for a in acct_list])
 		acct_labels = MMGenList([TwLabel(a,on_fail='silent') for a in acct_list])
 		check_dup_mmid(acct_labels)
 		check_dup_mmid(acct_labels)
-		acct_addrs = c.getaddressesbyaccount([[a] for a in acct_list],batch=True) # use raw list here
+		acct_addrs = g.rpch.getaddressesbyaccount([[a] for a in acct_list],batch=True) # use raw list here
 		assert len(acct_list) == len(acct_addrs), 'listaccounts() and getaddressesbyaccount() not equal in length'
 		assert len(acct_list) == len(acct_addrs), 'listaccounts() and getaddressesbyaccount() not equal in length'
 		addr_pairs = zip(acct_labels,acct_addrs)
 		addr_pairs = zip(acct_labels,acct_addrs)
 		check_addr_array_lens(addr_pairs)
 		check_addr_array_lens(addr_pairs)
@@ -428,7 +576,7 @@ def Listaddresses(addrs='',minconf=1,showempty=False,pager=False,showbtcaddrs=Tr
 			if all_labels and not showempty and not label.comment: continue
 			if all_labels and not showempty and not label.comment: continue
 			if usr_addr_list and (label.mmid not in usr_addr_list): continue
 			if usr_addr_list and (label.mmid not in usr_addr_list): continue
 			if label.mmid not in addrs:
 			if label.mmid not in addrs:
-				addrs[label.mmid] = { 'amt':BTCAmt('0'), 'lbl':label, 'addr':'' }
+				addrs[label.mmid] = { 'amt':g.proto.coin_amt('0'), 'lbl':label, 'addr':'' }
 				if showbtcaddrs:
 				if showbtcaddrs:
 					addrs[label.mmid]['addr'] = CoinAddr(addr_arr[0])
 					addrs[label.mmid]['addr'] = CoinAddr(addr_arr[0])
 
 
@@ -448,8 +596,17 @@ def Listaddresses(addrs='',minconf=1,showempty=False,pager=False,showbtcaddrs=Tr
 			amt='BALANCE'
 			amt='BALANCE'
 			)]
 			)]
 
 
+	def sort_algo(j):
+		if sort and 'age' in sort:
+			return '{}_{:>012}_{}'.format(
+				j.obj.rsplit(':',1)[0],
+				(1000000000-j.confs if hasattr(j,'confs') else 0), # Hack, but OK for the foreseeable future
+				j.sort_key)
+		else:
+			return j.sort_key
+
 	al_id_save = None
 	al_id_save = None
-	for mmid in sorted(addrs,key=lambda j: j.sort_key):
+	for mmid in sorted(addrs,key=sort_algo,reverse=bool(sort and 'reverse' in sort)):
 		if mmid.type == 'mmgen':
 		if mmid.type == 'mmgen':
 			if al_id_save and al_id_save != mmid.obj.al_id:
 			if al_id_save and al_id_save != mmid.obj.al_id:
 				out.append('')
 				out.append('')
@@ -465,14 +622,14 @@ def Listaddresses(addrs='',minconf=1,showempty=False,pager=False,showbtcaddrs=Tr
 			addr=(addrs[mmid]['addr'].fmt(color=True) if showbtcaddrs else None),
 			addr=(addrs[mmid]['addr'].fmt(color=True) if showbtcaddrs else None),
 			cmt=addrs[mmid]['lbl'].comment.fmt(width=max_cmt_len,color=True,nullrepl='-'),
 			cmt=addrs[mmid]['lbl'].comment.fmt(width=max_cmt_len,color=True,nullrepl='-'),
 			amt=addrs[mmid]['amt'].fmt('3.0',color=True)))
 			amt=addrs[mmid]['amt'].fmt('3.0',color=True)))
-
 	out.append('\nTOTAL: {} {}'.format(total.hl(color=True),g.coin))
 	out.append('\nTOTAL: {} {}'.format(total.hl(color=True),g.coin))
 	o = '\n'.join(out)
 	o = '\n'.join(out)
 	return do_pager(o) if pager else Msg(o)
 	return do_pager(o) if pager else Msg(o)
 
 
-def Getbalance(minconf=1,quiet=False):
+def Getbalance(minconf=1,quiet=False,return_val=False):
+	rpc_init()
 	accts = {}
 	accts = {}
-	for d in rpc_connection().listunspent(0):
+	for d in g.rpch.listunspent(0):
 		ma = split2(d['account'] if 'account' in d else '')[0] # include coinbase outputs if spendable
 		ma = split2(d['account'] if 'account' in d else '')[0] # include coinbase outputs if spendable
 		keys = ['TOTAL']
 		keys = ['TOTAL']
 		if d['spendable']: keys += ['SPENDABLE']
 		if d['spendable']: keys += ['SPENDABLE']
@@ -481,23 +638,26 @@ def Getbalance(minconf=1,quiet=False):
 		i = (1,2)[confs >= minconf]
 		i = (1,2)[confs >= minconf]
 
 
 		for key in keys:
 		for key in keys:
-			if key not in accts: accts[key] = [BTCAmt('0')] * 3
+			if key not in accts: accts[key] = [g.proto.coin_amt('0')] * 3
 			for j in ([],[0])[confs==0] + [i]:
 			for j in ([],[0])[confs==0] + [i]:
 				accts[key][j] += d['amount']
 				accts[key][j] += d['amount']
 
 
 	if quiet:
 	if quiet:
-		Msg('{}'.format(accts['TOTAL'][2] if accts else BTCAmt('0')))
+		o = ['{}'.format(accts['TOTAL'][2] if accts else g.proto.coin_amt('0'))]
 	else:
 	else:
 		fs = '{:13} {} {} {}'
 		fs = '{:13} {} {} {}'
 		mc,lbl = str(minconf),'confirms'
 		mc,lbl = str(minconf),'confirms'
-		Msg(fs.format('Wallet',
-			*[s.ljust(16) for s in ' Unconfirmed',' <%s %s'%(mc,lbl),' >=%s %s'%(mc,lbl)]))
+		o = [fs.format('Wallet', *[s.ljust(16) for s in ' Unconfirmed',' <%s %s'%(mc,lbl),' >=%s %s'%(mc,lbl)])]
 		for key in sorted(accts.keys()):
 		for key in sorted(accts.keys()):
-			Msg(fs.format(key+':', *[a.fmt(color=True,suf=' '+g.coin) for a in accts[key]]))
+			o += [fs.format(key+':', *[a.fmt(color=True,suf=' '+g.coin) for a in accts[key]])]
 
 
 	if 'SPENDABLE' in accts:
 	if 'SPENDABLE' in accts:
 		Msg(red('Warning: this wallet contains PRIVATE KEYS for the SPENDABLE balance!'))
 		Msg(red('Warning: this wallet contains PRIVATE KEYS for the SPENDABLE balance!'))
 
 
+	o = '\n'.join(o)
+	if return_val: return o
+	else:          Msg(o)
+
 def Txview(*infiles,**kwargs):
 def Txview(*infiles,**kwargs):
 	from mmgen.filename import MMGenFileList
 	from mmgen.filename import MMGenFileList
 	pager = 'pager' in kwargs and kwargs['pager']
 	pager = 'pager' in kwargs and kwargs['pager']
@@ -511,6 +671,7 @@ def Txview(*infiles,**kwargs):
 	(Msg,do_pager)[pager](out.rstrip())
 	(Msg,do_pager)[pager](out.rstrip())
 
 
 def Twview(pager=False,reverse=False,wide=False,minconf=1,sort='age',show_days=True,show_mmid=True):
 def Twview(pager=False,reverse=False,wide=False,minconf=1,sort='age',show_days=True,show_mmid=True):
+	rpc_init()
 	from mmgen.tw import MMGenTrackingWallet
 	from mmgen.tw import MMGenTrackingWallet
 	tw = MMGenTrackingWallet(minconf=minconf)
 	tw = MMGenTrackingWallet(minconf=minconf)
 	tw.do_sort(sort,reverse=reverse)
 	tw.do_sort(sort,reverse=reverse)
@@ -520,146 +681,8 @@ def Twview(pager=False,reverse=False,wide=False,minconf=1,sort='age',show_days=T
 	(Msg_r,do_pager)[pager](out)
 	(Msg_r,do_pager)[pager](out)
 
 
 def Add_label(mmaddr,label):
 def Add_label(mmaddr,label):
+	rpc_init()
 	from mmgen.tw import MMGenTrackingWallet
 	from mmgen.tw import MMGenTrackingWallet
 	MMGenTrackingWallet.add_label(mmaddr,label) # dies on failure
 	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):
-	from mmgen.addr import AddrList
-	AddrList(infile,chksum_only=True)
-
-def Keyaddrfile_chksum(infile):
-	from mmgen.addr import KeyAddrList
-	KeyAddrList(infile,chksum_only=True)
-
-def Passwdfile_chksum(infile):
-	from mmgen.addr import PasswordList
-	PasswordList(infile=infile,chksum_only=True)
-
-def Hexreverse(s):
-	Msg(binascii.hexlify(binascii.unhexlify(s.strip())[::-1]))
-
-def Hexlify(s):
-	Msg(binascii.hexlify(s))
-
-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 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:
-		outfile = '%s.%s' % (os.path.basename(infile),g.mmenc_ext)
-
-	write_data_to_file(outfile,enc_d,'encrypted data',binary=True)
-
-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)
-		if dec_d: break
-		msg('Trying again...')
-
-	if not outfile:
-		o = os.path.basename(infile)
-		outfile = remove_extension(o,g.mmenc_ext)
-		if outfile == o: outfile += '.dec'
-
-	write_data_to_file(outfile,dec_d,'decrypted data',binary=True)
-
-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
-	f = os.open(filename,flgs)
-	for ch in iv_id:
-		if ch not in '0123456789ABCDEF':
-			die(2,"'%s': invalid Incog ID" % iv_id)
-	while True:
-		d = os.read(f,bsize)
-		if not d: break
-		d = carry + d
-		for i in range(bsize):
-			if sha256(d[i:i+ivsize]).hexdigest()[:8].upper() == iv_id:
-				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(0)
-		carry = d[len(d)-ivsize:]
-		n += bsize
-		if not n % mod: msg_r('\rSearched: %s bytes' % n)
-
-	msg('')
-	os.close(f)
-
-def Rand2file(outfile, nbytes, threads=4, silent=False):
-	nbytes = parse_nbytes(nbytes)
-	from Crypto import Random
-	rh = Random.new()
-	from Queue import Queue
-	from threading import Thread
-	bsize = 2**20
-	roll = bsize * 4
-	if opt.outdir: outfile = make_full_path(opt.outdir,outfile)
-	f = open(outfile,'wb')
-
-	from Crypto.Cipher import AES
-	from Crypto.Util import Counter
-
-	key = get_random(32)
-
-	def encrypt_worker(wid):
-		while True:
-			i,d = q1.get()
-			c = AES.new(key, AES.MODE_CTR,
-					counter=Counter.new(g.aesctr_iv_len*8,initial_value=i))
-			enc_data = c.encrypt(d)
-			q2.put(enc_data)
-			q1.task_done()
-
-	def output_worker():
-		while True:
-			data = q2.get()
-			f.write(data)
-			q2.task_done()
-
-	q1 = Queue()
-	for i in range(max(1,threads-2)):
-		t = Thread(target=encrypt_worker, args=(i,))
-		t.daemon = True
-		t.start()
-
-	q2 = Queue()
-	t = Thread(target=output_worker)
-	t.daemon = True
-	t.start()
-
-	i = 1; rbytes = nbytes
-	while rbytes > 0:
-		d = rh.read(min(bsize,rbytes))
-		q1.put((i,d))
-		rbytes -= bsize
-		i += 1
-		if not (bsize*i) % roll:
-			msg_r('\rRead: %s bytes' % (bsize*i))
-
-	if not silent:
-		msg('\rRead: %s bytes' % nbytes)
-		qmsg("\r%s bytes of random data written to file '%s'" % (nbytes,outfile))
-	q1.join()
-	q2.join()
-	f.close()
-
-def Bytespec(s): Msg(str(parse_nbytes(s)))
-
-def Regtest_setup():
-	print 'ok'
-	return
-	import subprocess as sp
-	sp.check_output()
-	pass

+ 11 - 19
mmgen/tw.py

@@ -26,23 +26,15 @@ from mmgen.tx import is_mmgen_id
 
 
 CUR_HOME,ERASE_ALL = '\033[H','\033[0J'
 CUR_HOME,ERASE_ALL = '\033[H','\033[0J'
 
 
-def parse_tw_acct_label(s):
-	ret = s.split(None,1)
-	a1,a2 = None,None
-	if ret:
-		a1 = ret[0] if is_mmgen_id(ret[0]) else '' if ret[0][:4] == 'btc:' else None
-		a2 = ret[1] if len(ret) == 2 else None
-	return a1,a2
-
 class MMGenTrackingWallet(MMGenObject):
 class MMGenTrackingWallet(MMGenObject):
 
 
 	class MMGenTwOutputList(list,MMGenObject): pass
 	class MMGenTwOutputList(list,MMGenObject): pass
 
 
 	class MMGenTwUnspentOutput(MMGenListItem):
 	class MMGenTwUnspentOutput(MMGenListItem):
 	#	attrs = 'txid','vout','amt','label','twmmid','addr','confs','scriptPubKey','days','skip'
 	#	attrs = 'txid','vout','amt','label','twmmid','addr','confs','scriptPubKey','days','skip'
-		txid     = MMGenImmutableAttr('txid','BitcoinTxID')
+		txid     = MMGenImmutableAttr('txid','CoinTxID')
 		vout     = MMGenImmutableAttr('vout',int,typeconv=False),
 		vout     = MMGenImmutableAttr('vout',int,typeconv=False),
-		amt      = MMGenImmutableAttr('amt','BTCAmt'),
+		amt      = MMGenImmutableAttr('amt',g.proto.coin_amt.__name__),
 		label    = MMGenListItemAttr('label','TwComment',reassign_ok=True),
 		label    = MMGenListItemAttr('label','TwComment',reassign_ok=True),
 		twmmid   = MMGenImmutableAttr('twmmid','TwMMGenID')
 		twmmid   = MMGenImmutableAttr('twmmid','TwMMGenID')
 		addr     = MMGenImmutableAttr('addr','CoinAddr'),
 		addr     = MMGenImmutableAttr('addr','CoinAddr'),
@@ -80,12 +72,13 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
 		if g.bogus_wallet_data: # for debugging purposes only
 		if g.bogus_wallet_data: # for debugging purposes only
 			us_rpc = eval(get_data_from_file(g.bogus_wallet_data))
 			us_rpc = eval(get_data_from_file(g.bogus_wallet_data))
 		else:
 		else:
-			us_rpc = rpc_connection().listunspent(self.minconf)
+			us_rpc = g.rpch.listunspent(self.minconf)
 #		write_data_to_file('bogus_unspent.json', repr(us), 'bogus unspent data')
 #		write_data_to_file('bogus_unspent.json', repr(us), 'bogus unspent data')
 #		sys.exit(0)
 #		sys.exit(0)
 
 
 		if not us_rpc: die(0,self.wmsg['no_spendable_outputs'])
 		if not us_rpc: die(0,self.wmsg['no_spendable_outputs'])
 		mm_rpc = self.MMGenTwOutputList()
 		mm_rpc = self.MMGenTwOutputList()
+		confs_per_day = 60*60*24 / g.proto.secs_per_block
 		for o in us_rpc:
 		for o in us_rpc:
 			if not 'account' in o: continue          # coinbase outputs have no account field
 			if not 'account' in o: continue          # coinbase outputs have no account field
 			l = TwLabel(o['account'],on_fail='silent')
 			l = TwLabel(o['account'],on_fail='silent')
@@ -93,8 +86,8 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
 				o.update({
 				o.update({
 					'twmmid': l.mmid,
 					'twmmid': l.mmid,
 					'label':  l.comment,
 					'label':  l.comment,
-					'days':   int(o['confirmations'] * g.mins_per_block / (60*24)),
-					'amt':    BTCAmt(o['amount']), # TODO
+					'days':   int(o['confirmations'] / confs_per_day),
+					'amt':    g.proto.coin_amt(o['amount']), # TODO
 					'addr':   CoinAddr(o['address']), # TODO
 					'addr':   CoinAddr(o['address']), # TODO
 					'confs':  o['confirmations']
 					'confs':  o['confirmations']
 				})
 				})
@@ -336,8 +329,7 @@ Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen
 			msg("Address '{}' not in tracking wallet".format(coinaddr))
 			msg("Address '{}' not in tracking wallet".format(coinaddr))
 			return False
 			return False
 
 
-		c = rpc_connection()
-		if not coinaddr.is_for_current_chain():
+		if not coinaddr.is_for_chain(g.chain):
 			msg("Address '{}' not valid for chain {}".format(coinaddr,g.chain.upper()))
 			msg("Address '{}' not valid for chain {}".format(coinaddr,g.chain.upper()))
 			return False
 			return False
 
 
@@ -348,7 +340,7 @@ Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen
 			ad = AddrData(source='tw')
 			ad = AddrData(source='tw')
 			mmaddr = ad.coinaddr2mmaddr(coinaddr)
 			mmaddr = ad.coinaddr2mmaddr(coinaddr)
 
 
-		if not mmaddr: mmaddr = 'btc:'+coinaddr
+		if not mmaddr: mmaddr = '{}:{}'.format(g.proto.base_coin.lower(),coinaddr)
 
 
 		mmaddr = TwMMGenID(mmaddr)
 		mmaddr = TwMMGenID(mmaddr)
 
 
@@ -361,17 +353,17 @@ Display options: show [D]ays, [g]roup, show [m]mgen addr, r[e]draw screen
 		# associating the new account with the address.
 		# associating the new account with the address.
 		# Will be replaced by setlabel() with new RPC label API
 		# Will be replaced by setlabel() with new RPC label API
 		# RPC args: addr,label,rescan[=true],p2sh[=none]
 		# RPC args: addr,label,rescan[=true],p2sh[=none]
-		ret = c.importaddress(coinaddr,lbl,False,on_fail='return')
+		ret = g.rpch.importaddress(coinaddr,lbl,False,on_fail='return')
 
 
 		from mmgen.rpc import rpc_error,rpc_errmsg
 		from mmgen.rpc import rpc_error,rpc_errmsg
 		if rpc_error(ret):
 		if rpc_error(ret):
-			msg('From bitcoind: ' + rpc_errmsg(ret))
+			msg('From {}: {}'.format(g.proto.daemon_name,rpc_errmsg(ret)))
 			if not silent:
 			if not silent:
 				msg('Label could not be {}'.format(('removed','added')[bool(label)]))
 				msg('Label could not be {}'.format(('removed','added')[bool(label)]))
 			return False
 			return False
 		else:
 		else:
 			m = mmaddr.type.replace('mmg','MMG')
 			m = mmaddr.type.replace('mmg','MMG')
-			a = mmaddr.replace('btc:','')
+			a = mmaddr.replace(g.proto.base_coin.lower()+':','')
 			s = '{} address {} in tracking wallet'.format(m,a)
 			s = '{} address {} in tracking wallet'.format(m,a)
 			if label: msg("Added label '{}' to {}".format(label,s))
 			if label: msg("Added label '{}' to {}".format(label,s))
 			else:     msg('Removed label from {}'.format(s))
 			else:     msg('Removed label from {}'.format(s))

+ 187 - 157
mmgen/tx.py

@@ -27,7 +27,7 @@ from mmgen.common import *
 from mmgen.obj import *
 from mmgen.obj import *
 
 
 def segwit_is_active(exit_on_error=False):
 def segwit_is_active(exit_on_error=False):
-	d = rpc_connection().getblockchaininfo()
+	d = g.rpch.getblockchaininfo()
 	if d['chain'] == 'regtest':
 	if d['chain'] == 'regtest':
 		return True
 		return True
 	if 'segwit' in d['bip9_softforks'] and d['bip9_softforks']['segwit']['status'] == 'active':
 	if 'segwit' in d['bip9_softforks'] and d['bip9_softforks']['segwit']['status'] == 'active':
@@ -45,8 +45,8 @@ def bytes2int(hex_bytes):
 		die(3,"{}: Negative values not permitted in transaction!".format(hex_bytes))
 		die(3,"{}: Negative values not permitted in transaction!".format(hex_bytes))
 	return int(r,16)
 	return int(r,16)
 
 
-def bytes2btc(hex_bytes):
-	return bytes2int(hex_bytes) * g.satoshi
+def bytes2coin_amt(hex_bytes):
+	return g.proto.coin_amt(bytes2int(hex_bytes) * g.proto.coin_amt.min_coin_unit)
 
 
 def scriptPubKey2addr(s):
 def scriptPubKey2addr(s):
 	if len(s) == 50 and s[:6] == '76a914' and s[-4:] == '88ac': addr_hex,p2sh = s[6:-4],False
 	if len(s) == 50 and s[:6] == '76a914' and s[-4:] == '88ac': addr_hex,p2sh = s[6:-4],False
@@ -59,26 +59,30 @@ class DeserializedTX(OrderedDict,MMGenObject): # need to add MMGen types
 	def __init__(self,txhex):
 	def __init__(self,txhex):
 		tx = list(unhexlify(txhex))
 		tx = list(unhexlify(txhex))
 		tx_copy = tx[:]
 		tx_copy = tx[:]
+		d = { 'raw_tx':'' }
 
 
-		def hshift(l,n,reverse=False):
+		def hshift(l,n,reverse=False,skip=False):
 			ret = l[:n]
 			ret = l[:n]
+			if not skip: d['raw_tx'] += ''.join(ret)
 			del l[:n]
 			del l[:n]
 			return hexlify(''.join(ret[::-1] if reverse else ret))
 			return hexlify(''.join(ret[::-1] if reverse else ret))
 
 
 		# https://bitcoin.org/en/developer-reference#compactsize-unsigned-integers
 		# https://bitcoin.org/en/developer-reference#compactsize-unsigned-integers
 		# For example, the number 515 is encoded as 0xfd0302.
 		# For example, the number 515 is encoded as 0xfd0302.
-		def readVInt(l):
+		def readVInt(l,skip=False,sub_null=False):
 			s = int(hexlify(l[0]),16)
 			s = int(hexlify(l[0]),16)
 			bytes_len = 1 if s < 0xfd else 2 if s == 0xfd else 4 if s == 0xfe else 8
 			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]
 			if bytes_len != 1: del l[0]
 			ret = int(hexlify(''.join(l[:bytes_len][::-1])),16)
 			ret = int(hexlify(''.join(l[:bytes_len][::-1])),16)
+			if sub_null: d['raw_tx'] += '\0'
+			elif not skip: d['raw_tx'] += ''.join(l[:bytes_len])
 			del l[:bytes_len]
 			del l[:bytes_len]
 			return ret
 			return ret
 
 
-		d = { 'version': bytes2int(hshift(tx,4)) }
+		d['version'] = bytes2int(hshift(tx,4))
 		has_witness = (False,True)[hexlify(tx[0])=='00']
 		has_witness = (False,True)[hexlify(tx[0])=='00']
 		if has_witness:
 		if has_witness:
-			u = hshift(tx,2)[2:]
+			u = hshift(tx,2,skip=True)[2:]
 			if u != '01':
 			if u != '01':
 				die(2,"'{}': Illegal value for flag in transaction!".format(u))
 				die(2,"'{}': Illegal value for flag in transaction!".format(u))
 			del tx_copy[-len(tx)-2:-len(tx)]
 			del tx_copy[-len(tx)-2:-len(tx)]
@@ -87,13 +91,13 @@ class DeserializedTX(OrderedDict,MMGenObject): # need to add MMGen types
 		d['txins'] = MMGenList([OrderedDict((
 		d['txins'] = MMGenList([OrderedDict((
 			('txid',      hshift(tx,32,reverse=True)),
 			('txid',      hshift(tx,32,reverse=True)),
 			('vout',      bytes2int(hshift(tx,4))),
 			('vout',      bytes2int(hshift(tx,4))),
-			('scriptSig', hshift(tx,readVInt(tx))),
+			('scriptSig', hshift(tx,readVInt(tx,sub_null=True),skip=True)),
 			('nSeq',      hshift(tx,4,reverse=True))
 			('nSeq',      hshift(tx,4,reverse=True))
 		)) for i in range(d['num_txins'])])
 		)) for i in range(d['num_txins'])])
 
 
 		d['num_txouts'] = readVInt(tx)
 		d['num_txouts'] = readVInt(tx)
 		d['txouts'] = MMGenList([OrderedDict((
 		d['txouts'] = MMGenList([OrderedDict((
-			('amount',       bytes2btc(hshift(tx,8))),
+			('amount',       bytes2coin_amt(hshift(tx,8))),
 			('scriptPubKey', hshift(tx,readVInt(tx)))
 			('scriptPubKey', hshift(tx,readVInt(tx)))
 		)) for i in range(d['num_txouts'])])
 		)) for i in range(d['num_txouts'])])
 
 
@@ -110,26 +114,30 @@ class DeserializedTX(OrderedDict,MMGenObject): # need to add MMGen types
 			d['witness_size'] = len(wd) + 2 # add marker and flag
 			d['witness_size'] = len(wd) + 2 # add marker and flag
 			for i in range(len(d['txins'])):
 			for i in range(len(d['txins'])):
 				if hexlify(wd[0]) == '00':
 				if hexlify(wd[0]) == '00':
-					hshift(wd,1)
+					hshift(wd,1,skip=True)
 					continue
 					continue
-				d['txins'][i]['witness'] = [hshift(wd,readVInt(wd)) for item in range(readVInt(wd))]
+				d['txins'][i]['witness'] = [
+					hshift(wd,readVInt(wd,skip=True),skip=True) for item in range(readVInt(wd,skip=True))
+				]
 			if wd:
 			if wd:
 				die(3,'More witness data than inputs with witnesses!')
 				die(3,'More witness data than inputs with witnesses!')
 
 
 		d['lock_time'] = bytes2int(hshift(tx,4))
 		d['lock_time'] = bytes2int(hshift(tx,4))
 		d['txid'] = hexlify(sha256(sha256(''.join(tx_copy)).digest()).digest()[::-1])
 		d['txid'] = hexlify(sha256(sha256(''.join(tx_copy)).digest()).digest()[::-1])
+		d['unsigned_hex'] = hexlify(d['raw_tx'])
+		del d['raw_tx']
 
 
-		keys = 'txid','version','lock_time','witness_size','num_txins','txins','num_txouts','txouts'
+		keys = 'txid','version','lock_time','witness_size','num_txins','txins','num_txouts','txouts','unsigned_hex'
 		return OrderedDict.__init__(self, ((k,d[k]) for k in keys))
 		return OrderedDict.__init__(self, ((k,d[k]) for k in keys))
 
 
 txio_attrs = {
 txio_attrs = {
 	'vout':  MMGenListItemAttr('vout',int,typeconv=False),
 	'vout':  MMGenListItemAttr('vout',int,typeconv=False),
-	'amt':   MMGenImmutableAttr('amt','BTCAmt'),
+	'amt':   MMGenImmutableAttr('amt',g.proto.coin_amt,typeconv=False), # require amt to be of proper type
 	'label': MMGenListItemAttr('label','TwComment',reassign_ok=True),
 	'label': MMGenListItemAttr('label','TwComment',reassign_ok=True),
 	'mmid':  MMGenListItemAttr('mmid','MMGenID'),
 	'mmid':  MMGenListItemAttr('mmid','MMGenID'),
 	'addr':  MMGenImmutableAttr('addr','CoinAddr'),
 	'addr':  MMGenImmutableAttr('addr','CoinAddr'),
 	'confs': MMGenListItemAttr('confs',int,typeconv=True), # long confs exist in the wild, so convert
 	'confs': MMGenListItemAttr('confs',int,typeconv=True), # long confs exist in the wild, so convert
-	'txid':  MMGenListItemAttr('txid','BitcoinTxID'),
+	'txid':  MMGenListItemAttr('txid','CoinTxID'),
 	'have_wif': MMGenListItemAttr('have_wif',bool,typeconv=False,delete_ok=True)
 	'have_wif': MMGenListItemAttr('have_wif',bool,typeconv=False,delete_ok=True)
 }
 }
 
 
@@ -138,7 +146,7 @@ class MMGenTX(MMGenObject):
 	raw_ext  = 'rawtx'
 	raw_ext  = 'rawtx'
 	sig_ext  = 'sigtx'
 	sig_ext  = 'sigtx'
 	txid_ext = 'txid'
 	txid_ext = 'txid'
-	desc = 'transaction'
+	desc     = 'transaction'
 
 
 	class MMGenTxInput(MMGenListItem):
 	class MMGenTxInput(MMGenListItem):
 		for k in txio_attrs: locals()[k] = txio_attrs[k] # in lieu of inheritance
 		for k in txio_attrs: locals()[k] = txio_attrs[k] # in lieu of inheritance
@@ -152,10 +160,10 @@ class MMGenTX(MMGenObject):
 	class MMGenTxInputList(list,MMGenObject): pass
 	class MMGenTxInputList(list,MMGenObject): pass
 	class MMGenTxOutputList(list,MMGenObject): pass
 	class MMGenTxOutputList(list,MMGenObject): pass
 
 
-	def __init__(self,filename=None):
+	def __init__(self,filename=None,md_only=False):
 		self.inputs      = self.MMGenTxInputList()
 		self.inputs      = self.MMGenTxInputList()
 		self.outputs     = self.MMGenTxOutputList()
 		self.outputs     = self.MMGenTxOutputList()
-		self.send_amt    = BTCAmt('0')  # total amt minus change
+		self.send_amt    = g.proto.coin_amt('0')  # total amt minus change
 		self.hex         = ''           # raw serialized hex transaction
 		self.hex         = ''           # raw serialized hex transaction
 		self.label       = MMGenTXLabel('')
 		self.label       = MMGenTXLabel('')
 		self.txid        = ''
 		self.txid        = ''
@@ -165,12 +173,14 @@ class MMGenTX(MMGenObject):
 		self.fmt_data    = ''
 		self.fmt_data    = ''
 		self.blockcount  = 0
 		self.blockcount  = 0
 		self.chain       = None
 		self.chain       = None
+		self.coin        = None
 
 
 		if filename:
 		if filename:
-			self.parse_tx_file(filename)
+			self.parse_tx_file(filename,md_only=md_only)
+			if md_only: return
 			self.check_sigs() # marks the tx as signed
 			self.check_sigs() # marks the tx as signed
 
 
-		# repeat with sign and send, because bitcoind could be restarted
+		# repeat with sign and send, because coin daemon could be restarted
 		self.die_if_incorrect_chain()
 		self.die_if_incorrect_chain()
 
 
 	def die_if_incorrect_chain(self):
 	def die_if_incorrect_chain(self):
@@ -197,7 +207,7 @@ class MMGenTX(MMGenObject):
 	def sum_outputs(self,exclude=None):
 	def sum_outputs(self,exclude=None):
 		olist = self.outputs if exclude == None else \
 		olist = self.outputs if exclude == None else \
 			self.outputs[:exclude] + self.outputs[exclude+1:]
 			self.outputs[:exclude] + self.outputs[exclude+1:]
-		return BTCAmt(sum(e.amt for e in olist))
+		return g.proto.coin_amt(sum(e.amt for e in olist))
 
 
 	def add_mmaddrs_to_outputs(self,ad_w,ad_f):
 	def add_mmaddrs_to_outputs(self,ad_w,ad_f):
 		a = [e.addr for e in self.outputs]
 		a = [e.addr for e in self.outputs]
@@ -208,12 +218,22 @@ class MMGenTX(MMGenObject):
 				e.mmid,f = d[e.addr]
 				e.mmid,f = d[e.addr]
 				if f: e.label = f
 				if f: e.label = f
 
 
-	def create_raw(self,c):
+	def check_dup_addrs(self,io_str):
+		assert io_str in ('inputs','outputs')
+		io = getattr(self,io_str)
+		for k in ('mmid','addr'):
+			old_attr = None
+			for attr in sorted(getattr(e,k) for e in io):
+				if attr != None and attr == old_attr:
+					die(2,'{}: duplicate address in transaction {}'.format(attr,io_str))
+				old_attr = attr
+
+	def create_raw(self):
 		i = [{'txid':e.txid,'vout':e.vout} for e in self.inputs]
 		i = [{'txid':e.txid,'vout':e.vout} for e in self.inputs]
 		if self.inputs[0].sequence:
 		if self.inputs[0].sequence:
 			i[0]['sequence'] = self.inputs[0].sequence
 			i[0]['sequence'] = self.inputs[0].sequence
 		o = dict([(e.addr,e.amt) for e in self.outputs])
 		o = dict([(e.addr,e.amt) for e in self.outputs])
-		self.hex = c.createrawtransaction(i,o)
+		self.hex = g.rpch.createrawtransaction(i,o)
 		self.txid = MMGenTxID(make_chksum_6(unhexlify(self.hex)).upper())
 		self.txid = MMGenTxID(make_chksum_6(unhexlify(self.hex)).upper())
 
 
 	# returns true if comment added or changed
 	# returns true if comment added or changed
@@ -312,20 +332,20 @@ class MMGenTX(MMGenObject):
 		return self.sum_inputs() - self.sum_outputs()
 		return self.sum_inputs() - self.sum_outputs()
 
 
 	def btc2spb(self,coin_fee):
 	def btc2spb(self,coin_fee):
-		return int(coin_fee/g.satoshi/self.estimate_size())
+		return int(coin_fee/g.proto.coin_amt.min_coin_unit/self.estimate_size())
 
 
 	def get_relay_fee(self):
 	def get_relay_fee(self):
 		assert self.estimate_size()
 		assert self.estimate_size()
-		kb_fee = BTCAmt(rpc_connection().getnetworkinfo()['relayfee'])
+		kb_fee = g.proto.coin_amt(g.rpch.getnetworkinfo()['relayfee'])
 		vmsg('Relay fee: {} {}/kB'.format(kb_fee,g.coin))
 		vmsg('Relay fee: {} {}/kB'.format(kb_fee,g.coin))
 		return kb_fee * self.estimate_size() / 1024
 		return kb_fee * self.estimate_size() / 1024
 
 
 	def convert_fee_spec(self,tx_fee,tx_size,on_fail='throw'):
 	def convert_fee_spec(self,tx_fee,tx_size,on_fail='throw'):
-		if BTCAmt(tx_fee,on_fail='silent'):
-			return BTCAmt(tx_fee)
+		if g.proto.coin_amt(tx_fee,on_fail='silent'):
+			return g.proto.coin_amt(tx_fee)
 		elif len(tx_fee) >= 2 and tx_fee[-1] == 's' and is_int(tx_fee[:-1]) and int(tx_fee[:-1]) >= 1:
 		elif len(tx_fee) >= 2 and tx_fee[-1] == 's' and is_int(tx_fee[:-1]) and int(tx_fee[:-1]) >= 1:
 			if tx_size:
 			if tx_size:
-				return BTCAmt(int(tx_fee[:-1]) * tx_size * g.satoshi)
+				return g.proto.coin_amt(int(tx_fee[:-1]) * tx_size * g.proto.coin_amt.min_coin_unit)
 			else:
 			else:
 				return None
 				return None
 		else:
 		else:
@@ -344,9 +364,9 @@ class MMGenTX(MMGenObject):
 			m = "'{}': invalid TX fee (not a {} amount or satoshis-per-byte specification)"
 			m = "'{}': invalid TX fee (not a {} amount or satoshis-per-byte specification)"
 			msg(m.format(tx_fee,g.coin))
 			msg(m.format(tx_fee,g.coin))
 			return False
 			return False
-		elif coin_fee > g.max_tx_fee:
+		elif coin_fee > g.proto.max_tx_fee:
 			m = '{} {c}: {} fee too large (maximum fee: {} {c})'
 			m = '{} {c}: {} fee too large (maximum fee: {} {c})'
-			msg(m.format(coin_fee,desc,g.max_tx_fee,c=g.coin))
+			msg(m.format(coin_fee,desc,g.proto.max_tx_fee,c=g.coin))
 			return False
 			return False
 		elif coin_fee < self.get_relay_fee():
 		elif coin_fee < self.get_relay_fee():
 			m = '{} {c}: {} fee too small (below relay fee of {} {c})'
 			m = '{} {c}: {} fee too small (below relay fee of {} {c})'
@@ -411,12 +431,13 @@ class MMGenTX(MMGenObject):
 	def add_timestamp(self):
 	def add_timestamp(self):
 		self.timestamp = make_timestamp()
 		self.timestamp = make_timestamp()
 
 
-	def add_blockcount(self,c):
-		self.blockcount = int(c.getblockcount())
+	def add_blockcount(self):
+		self.blockcount = int(g.rpch.getblockcount())
 
 
 	def format(self):
 	def format(self):
 		lines = [
 		lines = [
-			'{} {} {} {} {}'.format(
+			'{}{} {} {} {} {}'.format(
+				(g.coin+' ','')[g.coin=='BTC'],
 				self.chain.upper() if self.chain else 'Unknown',
 				self.chain.upper() if self.chain else 'Unknown',
 				self.txid,
 				self.txid,
 				self.send_amt,
 				self.send_amt,
@@ -439,14 +460,17 @@ class MMGenTX(MMGenObject):
 		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
 	# return true or false; don't exit
-	def sign(self,c,tx_num_str,keys):
+	def sign(self,tx_num_str,keys):
+
+		if self.marked_signed():
+			die(1,'Transaction is already signed!')
 
 
 		self.die_if_incorrect_chain()
 		self.die_if_incorrect_chain()
 
 
-		if g.coin == 'BCH' and (self.has_segwit_inputs() or self.has_segwit_outputs()):
-			die(2,yellow("Segwit inputs cannot be spent or spent to on the BCH chain!"))
+		if (self.has_segwit_inputs() or self.has_segwit_outputs()) and not g.proto.cap('segwit'):
+			die(2,yellow("TX has Segwit inputs or outputs, but {} doesn't support Segwit!".format(g.coin)))
 
 
-		qmsg('Passing {} key{} to bitcoind'.format(len(keys),suf(keys,'s')))
+		qmsg('Passing {} key{} to {}'.format(len(keys),suf(keys,'s'),g.proto.daemon_name))
 
 
 		if self.has_segwit_inputs():
 		if self.has_segwit_inputs():
 			from mmgen.addr import KeyGenerator,AddrGenerator
 			from mmgen.addr import KeyGenerator,AddrGenerator
@@ -467,7 +491,7 @@ class MMGenTX(MMGenObject):
 		wifs = [d.sec.wif for d in keys]
 		wifs = [d.sec.wif for d in keys]
 #		keys.pmsg()
 #		keys.pmsg()
 #		pmsg(wifs)
 #		pmsg(wifs)
-		ret = c.signrawtransaction(self.hex,sig_data,wifs,g.proto.sighash_type,on_fail='return')
+		ret = g.rpch.signrawtransaction(self.hex,sig_data,wifs,g.proto.sighash_type,on_fail='return')
 
 
 		from mmgen.rpc import rpc_error,rpc_errmsg
 		from mmgen.rpc import rpc_error,rpc_errmsg
 		if rpc_error(ret):
 		if rpc_error(ret):
@@ -481,18 +505,20 @@ class MMGenTX(MMGenObject):
 			return False
 			return False
 		else:
 		else:
 			if ret['complete']:
 			if ret['complete']:
+#				Msg(pretty_hexdump(unhexlify(self.hex),cols=16)) # DEBUG
+#				pmsg(make_chksum_6(unhexlify(self.hex)).upper())
 				self.hex = ret['hex']
 				self.hex = ret['hex']
 				vmsg('Signed transaction size: {}'.format(len(self.hex)/2))
 				vmsg('Signed transaction size: {}'.format(len(self.hex)/2))
 				dt = DeserializedTX(self.hex)
 				dt = DeserializedTX(self.hex)
 				self.check_hex_tx_matches_mmgen_tx(dt)
 				self.check_hex_tx_matches_mmgen_tx(dt)
-				txid = dt['txid']
+				self.coin_txid = CoinTxID(dt['txid'],on_fail='return')
 				self.check_sigs(dt)
 				self.check_sigs(dt)
-				assert txid == c.decoderawtransaction(self.hex)['txid'], 'txid mismatch (after signing)'
-				self.coin_txid = BitcoinTxID(txid,on_fail='return')
+				assert self.coin_txid == g.rpch.decoderawtransaction(self.hex)['txid'],(
+											'txid mismatch (after signing)')
 				msg('OK')
 				msg('OK')
 				return True
 				return True
 			else:
 			else:
-				msg('failed\nBitcoind returned the following errors:')
+				msg('failed\n{} returned the following errors:'.format(g.proto.daemon_name.capitalize()))
 				msg(repr(ret['errors']))
 				msg(repr(ret['errors']))
 				return False
 				return False
 
 
@@ -508,7 +534,7 @@ class MMGenTX(MMGenObject):
 		ret = self.desc == 'signed transaction'
 		ret = self.desc == 'signed transaction'
 		return (red,green)[ret](str(ret)) if color else ret
 		return (red,green)[ret](str(ret)) if color else ret
 
 
-	# protect against an attack where a malicious, compromised or malfunctioning daemon could switch
+	# protect against an attack where a malicious, compromised or malfunctioning coin daemon could switch
 	# hex transaction data.
 	# hex transaction data.
 	def check_hex_tx_matches_mmgen_tx(self,deserial_tx):
 	def check_hex_tx_matches_mmgen_tx(self,deserial_tx):
 		m = 'Fatal error: a malicious or malfunctioning coin daemon or other program has altered your data!'
 		m = 'Fatal error: a malicious or malfunctioning coin daemon or other program has altered your data!'
@@ -516,21 +542,24 @@ class MMGenTX(MMGenObject):
 		if deserial_tx['lock_time'] != 0:
 		if deserial_tx['lock_time'] != 0:
 			rdie(3,'\nLock time is not zero!\n' + m)
 			rdie(3,'\nLock time is not zero!\n' + m)
 
 
-		def do_io_err(desc,mmio,hexio):
-			msg('\nMMGen {}:\n{}'.format(desc,pformat(mmio)))
-			msg('Hex {}:\n{}'.format(desc,pformat(hexio)))
-			m2 = '{} in hex transaction data from coin daemon do not match those in MMGen transaction!\n' + m
-			rdie(3,m2.format(desc.capitalize()))
+		def check_equal(desc,mmio,hexio):
+			if mmio != hexio:
+				msg('\nMMGen {}:\n{}'.format(desc,pformat(mmio)))
+				msg('Hex {}:\n{}'.format(desc,pformat(hexio)))
+				m2 = '{} in hex transaction data from coin daemon do not match those in MMGen transaction!\n' + m
+				rdie(3,m2.format(desc.capitalize()))
+
+		d_hex   = sorted((i['txid'],i['vout']) for i in deserial_tx['txins'])
+		d_mmgen = sorted((i.txid,i.vout) for i in self.inputs)
+		check_equal('inputs',d_hex,d_mmgen)
 
 
-		i_hex   = sorted((i['txid'],i['vout']) for i in deserial_tx['txins'])
-		i_mmgen = sorted((i.txid,i.vout) for i in self.inputs)
-		if i_hex != i_mmgen:
-			do_io_err('inputs',i_hex,i_mmgen)
+		d_hex   = sorted((o['address'],g.proto.coin_amt(o['amount'])) for o in deserial_tx['txouts'])
+		d_mmgen = sorted((o.addr,o.amt) for o in self.outputs)
+		check_equal('outputs',d_hex,d_mmgen)
 
 
-		o_hex   = sorted((o['address'],BTCAmt(o['amount'])) for o in deserial_tx['txouts'])
-		o_mmgen = sorted((o.addr,o.amt) for o in self.outputs)
-		if o_hex != o_mmgen:
-			do_io_err('outputs',o_hex,o_mmgen)
+		uh = deserial_tx['unsigned_hex']
+		if str(self.txid) != make_chksum_6(unhexlify(uh)).upper():
+			die(3,'MMGen TxID ({}) does not match hex transaction data!'.format(self.txid))
 
 
 	def check_sigs(self,deserial_tx=None): # return False if no sigs, die on error
 	def check_sigs(self,deserial_tx=None): # return False if no sigs, die on error
 		txins = (deserial_tx or DeserializedTX(self.hex))['txins']
 		txins = (deserial_tx or DeserializedTX(self.hex))['txins']
@@ -555,39 +584,42 @@ class MMGenTX(MMGenObject):
 	def has_segwit_outputs(self):
 	def has_segwit_outputs(self):
 		return any(o.mmid and o.mmid.mmtype == 'S' for o in self.outputs)
 		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.coin_txid,on_fail='silent')
+	def is_in_mempool(self):
+		return 'size' in g.rpch.getmempoolentry(self.coin_txid,on_fail='silent')
 
 
-	def is_in_wallet(self,c):
-		ret = c.gettransaction(self.coin_txid,on_fail='silent')
+	def is_in_wallet(self):
+		ret = g.rpch.gettransaction(self.coin_txid,on_fail='silent')
 		if 'confirmations' in ret and ret['confirmations'] > 0:
 		if 'confirmations' in ret and ret['confirmations'] > 0:
 			return ret['confirmations']
 			return ret['confirmations']
 		else:
 		else:
 			return False
 			return False
 
 
-	def is_replaced(self,c):
-		if self.is_in_mempool(c): return False
-		ret = c.gettransaction(self.coin_txid,on_fail='silent')
+	def is_replaced(self):
+		if self.is_in_mempool(): return False
+		ret = g.rpch.gettransaction(self.coin_txid,on_fail='silent')
 		if not 'bip125-replaceable' in ret or not 'confirmations' in ret or ret['confirmations'] > 0:
 		if not 'bip125-replaceable' in ret or not 'confirmations' in ret or ret['confirmations'] > 0:
 			return False
 			return False
 		return -ret['confirmations'] + 1 # 1: replacement in mempool, 2: replacement confirmed
 		return -ret['confirmations'] + 1 # 1: replacement in mempool, 2: replacement confirmed
 
 
-	def is_in_utxos(self,c):
-		return 'txid' in c.getrawtransaction(self.coin_txid,True,on_fail='silent')
+	def is_in_utxos(self):
+		return 'txid' in g.rpch.getrawtransaction(self.coin_txid,True,on_fail='silent')
 
 
-	def get_status(self,c,status=False):
-		if self.is_in_mempool(c):
+	def get_status(self,status=False):
+		if self.is_in_mempool():
 			msg(('Warning: transaction is in mempool!','Transaction is in mempool')[status])
 			msg(('Warning: transaction is in mempool!','Transaction is in mempool')[status])
-		elif self.is_in_wallet(c):
-			confs = self.is_in_wallet(c)
+		elif self.is_in_wallet():
+			confs = self.is_in_wallet()
 			die(0,'Transaction has {} confirmation{}'.format(confs,suf(confs,'s')))
 			die(0,'Transaction has {} confirmation{}'.format(confs,suf(confs,'s')))
-		elif self.is_in_utxos(c):
+		elif self.is_in_utxos():
 			die(2,red('ERROR: transaction is in the blockchain (but not in the tracking wallet)!'))
 			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
+		ret = self.is_replaced() # 1: replacement in mempool, 2: replacement confirmed
 		if ret:
 		if ret:
 			die(1,'Transaction has been replaced'+('',', and the replacement TX is confirmed')[ret==2]+'!')
 			die(1,'Transaction has been replaced'+('',', and the replacement TX is confirmed')[ret==2]+'!')
 
 
-	def send(self,c,prompt_user=True):
+	def send(self,prompt_user=True):
+
+		if not self.marked_signed():
+			die(1,'Transaction is not signed!')
 
 
 		self.die_if_incorrect_chain()
 		self.die_if_incorrect_chain()
 
 
@@ -599,10 +631,11 @@ class MMGenTX(MMGenObject):
 			m = 'Transaction has MMGen Segwit outputs, but this blockchain does not support Segwit'
 			m = 'Transaction has MMGen Segwit outputs, but this blockchain does not support Segwit'
 			die(2,m+' at the current height')
 			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.get_fee() > g.proto.max_tx_fee:
+			die(2,'Transaction fee ({}) greater than {} max_tx_fee ({} {})!'.format(
+				self.get_fee(),g.proto.name.capitalize(),g.proto.max_tx_fee,g.coin.upper()))
 
 
-		self.get_status(c)
+		self.get_status()
 
 
 		if prompt_user:
 		if prompt_user:
 			m1 = ("Once this transaction is sent, there's no taking it back!",'')[bool(opt.quiet)]
 			m1 = ("Once this transaction is sent, there's no taking it back!",'')[bool(opt.quiet)]
@@ -611,7 +644,7 @@ class MMGenTX(MMGenObject):
 			confirm_or_exit(m1,m2,m3)
 			confirm_or_exit(m1,m2,m3)
 
 
 		msg('Sending transaction')
 		msg('Sending transaction')
-		ret = None if bogus_send else c.sendrawtransaction(self.hex,on_fail='return')
+		ret = None if bogus_send else g.rpch.sendrawtransaction(self.hex,on_fail='return')
 
 
 		from mmgen.rpc import rpc_error,rpc_errmsg
 		from mmgen.rpc import rpc_error,rpc_errmsg
 		if rpc_error(ret):
 		if rpc_error(ret):
@@ -636,7 +669,7 @@ class MMGenTX(MMGenObject):
 			self.desc = 'sent transaction'
 			self.desc = 'sent transaction'
 			msg(m.format(self.coin_txid.hl()))
 			msg(m.format(self.coin_txid.hl()))
 			self.add_timestamp()
 			self.add_timestamp()
-			self.add_blockcount(c)
+			self.add_blockcount()
 			return True
 			return True
 
 
 	def write_txid_to_file(self,ask_write=False,ask_write_default_yes=True):
 	def write_txid_to_file(self,ask_write=False,ask_write_default_yes=True):
@@ -649,8 +682,12 @@ class MMGenTX(MMGenObject):
 		if ask_write == False:
 		if ask_write == False:
 			ask_write_default_yes=True
 			ask_write_default_yes=True
 		self.format()
 		self.format()
-		spbs = ('',',{}'.format(self.btc2spb(self.get_fee())))[self.is_rbf()]
-		fn = '{}[{}{}].{}'.format(self.txid,self.send_amt,spbs,self.ext)
+		fn = '{}{}[{}{}].{}'.format(
+			self.txid,
+			('-'+g.coin,'')[g.coin=='BTC'],
+			self.send_amt,
+			('',',{}'.format(self.btc2spb(self.get_fee())))[self.is_rbf()],
+			self.ext)
 		write_data_to_file(fn,self.fmt_data,self.desc+add_desc,
 		write_data_to_file(fn,self.fmt_data,self.desc+add_desc,
 			ask_overwrite=ask_overwrite,
 			ask_overwrite=ask_overwrite,
 			ask_write=ask_write,
 			ask_write=ask_write,
@@ -664,7 +701,7 @@ class MMGenTX(MMGenObject):
 			self.view(pager=reply in 'Vv',terse=reply in 'Tt')
 			self.view(pager=reply in 'Vv',terse=reply in 'Tt')
 
 
 	def view(self,pager=False,pause=True,terse=False):
 	def view(self,pager=False,pause=True,terse=False):
-		o = self.format_view(terse=terse).encode('utf8')
+		o = self.format_view(terse=terse)
 		if pager: do_pager(o)
 		if pager: do_pager(o)
 		else:
 		else:
 			msg_r(o)
 			msg_r(o)
@@ -673,24 +710,20 @@ class MMGenTX(MMGenObject):
 				get_char('Press any key to continue: ')
 				get_char('Press any key to continue: ')
 				msg('')
 				msg('')
 
 
-# 	def is_rbf_fromhex(self,color=False):
-# 		try:
-# 			dec_tx = rpc_connection().decoderawtransaction(self.hex)
-# 		except:
-# 			return yellow('Unknown') if color else None
-# 		rbf = bool(dec_tx['vin'][0]['sequence'] == g.max_int - 2)
-# 		return (red,green)[rbf](str(rbf)) if color else rbf
+# 	def is_rbf_from_rpc(self):
+# 		dec_tx = g.rpch.decoderawtransaction(self.hex)
+# 		return None < dec_tx['vin'][0]['sequence'] <= g.max_int - 2
 
 
-	def is_rbf(self,color=False):
-		ret = None < self.inputs[0].sequence <= g.max_int - 2
-		return (red,green)[ret](str(ret)) if color else ret
+	def is_rbf(self):
+		return self.inputs[0].sequence == g.max_int - 2
 
 
 	def signal_for_rbf(self):
 	def signal_for_rbf(self):
 		self.inputs[0].sequence = g.max_int - 2
 		self.inputs[0].sequence = g.max_int - 2
 
 
 	def format_view(self,terse=False):
 	def format_view(self,terse=False):
 		try:
 		try:
-			blockcount = rpc_connection().getblockcount()
+			rpc_init()
+			blockcount = g.rpch.getblockcount()
 		except:
 		except:
 			blockcount = None
 			blockcount = None
 
 
@@ -712,10 +745,11 @@ class MMGenTX(MMGenObject):
 		def format_io(io):
 		def format_io(io):
 			ip = io == self.inputs
 			ip = io == self.inputs
 			io_out = ''
 			io_out = ''
+			confs_per_day = 60*60*24 / g.proto.secs_per_block
 			for n,e in enumerate(sorted(io,key=lambda o: o.mmid.sort_key if o.mmid else o.addr)):
 			for n,e in enumerate(sorted(io,key=lambda o: o.mmid.sort_key if o.mmid else o.addr)):
-				if ip and blockcount:
+				if ip and blockcount != None:
 					confs = e.confs + blockcount - self.blockcount
 					confs = e.confs + blockcount - self.blockcount
-					days = int(confs * g.mins_per_block / (60*24))
+					days = int(confs / confs_per_day)
 				if e.mmid:
 				if e.mmid:
 					app=('',' (chg)')[bool(not ip and e.is_chg and terse)]
 					app=('',' (chg)')[bool(not ip and e.is_chg and terse)]
 					mmid_fmt = e.mmid.fmt(width=max_mmwid,encl='()',color=True,app=app,appcolor='green')
 					mmid_fmt = e.mmid.fmt(width=max_mmwid,encl='()',color=True,app=app,appcolor='green')
@@ -725,18 +759,18 @@ class MMGenTX(MMGenObject):
 					io_out += '{:3} {} {} {} {}\n'.format(n+1,e.addr.fmt(color=True),mmid_fmt,e.amt.hl(),g.coin)
 					io_out += '{:3} {} {} {} {}\n'.format(n+1,e.addr.fmt(color=True),mmid_fmt,e.amt.hl(),g.coin)
 				else:
 				else:
 					icommon = [
 					icommon = [
-						((n+1,'')[ip],    'address:', e.addr.fmt(color=True) + ' ' + mmid_fmt),
-						('',  'comment:', e.label.hl() if e.label else ''),
-						('',  'amount:',  '{} {}'.format(e.amt.hl(),g.coin))]
-					items = [(n+1, 'tx,vout:', '%s,%s' % (e.txid, e.vout))] + icommon + [
-						('',  'confirmations:', '%s (around %s days)' % (confs,days) if blockcount else '')
+						((n+1,'')[ip],'address:',e.addr.fmt(color=True) + ' '+mmid_fmt),
+						('','comment:',e.label.hl() if e.label else ''),
+						('','amount:','{} {}'.format(e.amt.hl(),g.coin))]
+					items = [(n+1, 'tx,vout:','{},{}'.format(e.txid,e.vout))] + icommon + [
+						('','confirmations:','{} (around {} days)'.format(confs,days) if blockcount!=None else '')
 					] if ip else icommon + [
 					] if ip else icommon + [
-						('',  'change:',        green('True') if e.is_chg else '')]
-					io_out += '\n'.join([('%3s %-8s %s' % d) for d in items if d[2]]) + '\n\n'
+						('','change:',green('True') if e.is_chg else '')]
+					io_out += '\n'.join([('{:>3} {:<8} {}'.format(*d)) for d in items if d[2]]) + '\n\n'
 			return io_out
 			return io_out
 
 
 		out = hdr_fs.format(self.txid.hl(),self.send_amt.hl(),g.coin,self.timestamp,
 		out = hdr_fs.format(self.txid.hl(),self.send_amt.hl(),g.coin,self.timestamp,
-				self.is_rbf(color=True),self.marked_signed(color=True))
+				(red('False'),green('True'))[self.is_rbf()],self.marked_signed(color=True))
 		if self.chain in ('testnet','regtest'):
 		if self.chain in ('testnet','regtest'):
 			out += green('Chain: {}\n'.format(self.chain.upper()))
 			out += green('Chain: {}\n'.format(self.chain.upper()))
 		if self.coin_txid:
 		if self.coin_txid:
@@ -767,67 +801,63 @@ class MMGenTX(MMGenObject):
 
 
 		return out # TX label might contain non-ascii chars
 		return out # TX label might contain non-ascii chars
 
 
-	def parse_tx_file(self,infile):
-
-		self.parse_tx_data(get_lines_from_file(infile,self.desc+' data'))
+	def parse_tx_file(self,infile,md_only=False):
 
 
-	def parse_tx_data(self,tx_data):
-
-		def do_err(s): die(2,'Invalid %s in transaction file' % s)
-
-		if len(tx_data) < 5: do_err('number of lines')
-
-		self.chksum = HexStr(tx_data.pop(0))
-		if self.chksum != make_chksum_6(' '.join(tx_data)):
-			do_err('checksum')
-
-		if len(tx_data) == 6:
-			self.coin_txid = BitcoinTxID(tx_data.pop(-1),on_fail='return')
-			if not self.coin_txid:
-				do_err('Bitcoin TxID')
-
-		if len(tx_data) == 5:
-			c = tx_data.pop(-1)
-			if c != '-':
-				comment = baseconv.b58decode(c).decode('utf8')
-				if comment == False:
-					do_err('encoded comment (not base58)')
-				else:
-					self.label = MMGenTXLabel(comment,on_fail='return')
-					if not self.label:
-						do_err('comment')
-		else:
-			comment = u''
+		tx_data = get_lines_from_file(infile,self.desc+' data')
 
 
-		if len(tx_data) == 4:
+		try:
+			desc = 'data'
+			assert len(tx_data) >= 5,'number of lines less than 5'
+			self.chksum = HexStr(tx_data.pop(0),on_fail='raise')
+			assert self.chksum == make_chksum_6(' '.join(tx_data)),'file data does not match checksum'
+
+			if len(tx_data) == 6:
+				desc = '{} TxID'.format(g.proto.name.capitalize())
+				self.coin_txid = CoinTxID(tx_data.pop(-1),on_fail='raise')
+
+			if len(tx_data) == 5:
+				c = tx_data.pop(-1)
+				if c != '-':
+					desc = 'encoded comment (not base58)'
+					comment = baseconv.b58decode(c).decode('utf8')
+					assert comment != False,'invalid comment'
+					desc = 'comment'
+					self.label = MMGenTXLabel(comment,on_fail='raise')
+
+			desc = 'number of lines' # four required lines
 			metadata,self.hex,inputs_data,outputs_data = tx_data
 			metadata,self.hex,inputs_data,outputs_data = tx_data
-		else:
-			do_err('number of lines')
-
-		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
-		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')
-
-		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.is_testnet():
+			metadata = metadata.split()
+
+			self.coin = metadata.pop(0) if len(metadata) == 6 else 'BTC'
+
+			if len(metadata) == 5:
+				t = metadata.pop(0)
+				self.chain = (t.lower(),None)[t=='Unknown']
+
+			desc = 'metadata (4 items minimum required)'
+			self.txid,send_amt,self.timestamp,blockcount = metadata
+			desc = 'metadata'
+			self.txid = MMGenTxID(self.txid,on_fail='raise')
+			self.send_amt = g.proto.coin_amt(send_amt,on_fail='raise')
+			desc = 'block count in metadata'
+			self.blockcount = int(blockcount)
+			desc = 'transaction hex data'
+			self.hex = HexStr(self.hex,on_fail='raise')
+			if md_only: return # the following ops will all fail if g.coin doesn't match tx.coin
+			desc = 'coin type in metadata'
+			assert self.coin == g.coin,'invalid coin type'
+			desc = 'inputs data'
+			self.inputs = self.decode_io('inputs',eval(inputs_data))
+			assert len(self.inputs),'no inputs!'
+			desc = '{}-to-MMGen address map data'.format(g.coin)
+			self.outputs = self.decode_io('outputs',eval(outputs_data))
+			assert len(self.outputs),'no outputs!'
+		except Exception as e:
+			die(2,'Invalid {} in transaction file: {}'.format(desc,e[0]))
+
+		if not self.chain and not self.inputs[0].addr.is_for_chain('testnet'):
 			self.chain = 'mainnet'
 			self.chain = 'mainnet'
 
 
-		try: self.outputs = self.decode_io('outputs',eval(outputs_data))
-		except: do_err('btc-to-mmgen address map data')
-
 class MMGenBumpTX(MMGenTX):
 class MMGenBumpTX(MMGenTX):
 
 
 	min_fee = None
 	min_fee = None
@@ -886,7 +916,7 @@ class MMGenBumpTX(MMGenTX):
 		ret = super(type(self),self).get_usr_fee(tx_fee,desc)
 		ret = super(type(self),self).get_usr_fee(tx_fee,desc)
 		if ret < self.min_fee:
 		if ret < self.min_fee:
 			msg('{} {c}: {} fee too small. Minimum fee: {} {c} ({} satoshis per byte)'.format(
 			msg('{} {c}: {} fee too small. Minimum fee: {} {c} ({} satoshis per byte)'.format(
-				ret,desc,self.min_fee,self.btc2spb(self.min_fee,c=g.coin)))
+				ret,desc,self.min_fee,self.btc2spb(self.min_fee),c=g.coin))
 			return False
 			return False
 		output_amt = self.outputs[self.bump_output_idx].amt
 		output_amt = self.outputs[self.bump_output_idx].amt
 		if ret >= output_amt:
 		if ret >= output_amt:

+ 20 - 49
mmgen/txcreate.py

@@ -17,8 +17,7 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
 """
 """
-txcreate: Create a Bitcoin transaction to and from MMGen- or non-MMGen inputs
-          and outputs
+txcreate: Create a cryptocoin transaction with MMGen- and/or non-MMGen inputs and outputs
 """
 """
 
 
 from mmgen.common import *
 from mmgen.common import *
@@ -27,35 +26,6 @@ from mmgen.tw import *
 
 
 pnm = g.proj_name
 pnm = g.proj_name
 
 
-txcreate_notes = """
-The transaction's outputs are specified on the command line, while its inputs
-are chosen from a list of the user's unpent outputs via an interactive menu.
-
-If the transaction fee is not specified on the command line (see FEE
-SPECIFICATION below), it will be calculated dynamically using bitcoind's
-"estimatefee" function for the default (or user-specified) number of
-confirmations.  If "estimatefee" fails, the user will be prompted for a fee.
-
-Dynamic ("estimatefee") fees will be multiplied by the value of '--tx-fee-adj',
-if specified.
-
-Ages of transactions are approximate based on an average block discovery
-interval of {g.mins_per_block} minutes.
-
-All addresses on the command line can be either Bitcoin addresses or {pnm}
-addresses of the form <seed ID>:<index>.
-
-To send the value of all inputs (minus TX fee) to a single output, specify
-one address with no amount on the command line.
-""".format(g=g,pnm=pnm)
-
-fee_notes = """
-FEE SPECIFICATION: Transaction fees, both on the command line and at the
-interactive prompt, may be specified as either absolute {} amounts, using
-a plain decimal number, or as satoshis per byte, using an integer followed by
-the letter 's'.
-"""
-
 wmsg = {
 wmsg = {
 	'addr_in_addrfile_only': """
 	'addr_in_addrfile_only': """
 Warning: output address {mmgenaddr} is not in the tracking wallet, which means
 Warning: output address {mmgenaddr} is not in the tracking wallet, which means
@@ -98,7 +68,7 @@ def select_unspent(unspent,prompt):
 					return selected
 					return selected
 				msg('Unspent output number must be <= %s' % len(unspent))
 				msg('Unspent output number must be <= %s' % len(unspent))
 
 
-def mmaddr2coinaddr(c,mmaddr,ad_w,ad_f):
+def mmaddr2coinaddr(mmaddr,ad_w,ad_f):
 
 
 	# assume mmaddr has already been checked
 	# assume mmaddr has already been checked
 	coin_addr = ad_w.mmaddr2coinaddr(mmaddr)
 	coin_addr = ad_w.mmaddr2coinaddr(mmaddr)
@@ -119,21 +89,19 @@ def mmaddr2coinaddr(c,mmaddr,ad_w,ad_f):
 
 
 def get_fee_from_estimate_or_user(tx,estimate_fail_msg_shown=[]):
 def get_fee_from_estimate_or_user(tx,estimate_fail_msg_shown=[]):
 
 
-	c = rpc_connection()
-
 	if opt.tx_fee:
 	if opt.tx_fee:
 		desc = 'User-selected'
 		desc = 'User-selected'
 		start_fee = opt.tx_fee
 		start_fee = opt.tx_fee
 	else:
 	else:
 		desc = 'Network-estimated'
 		desc = 'Network-estimated'
-		ret = c.estimatefee(opt.tx_confs)
+		ret = g.rpch.estimatefee(opt.tx_confs)
 		if ret == -1:
 		if ret == -1:
 			if not estimate_fail_msg_shown:
 			if not estimate_fail_msg_shown:
 				msg('Network fee estimation for {} confirmations failed'.format(opt.tx_confs))
 				msg('Network fee estimation for {} confirmations failed'.format(opt.tx_confs))
 				estimate_fail_msg_shown.append(True)
 				estimate_fail_msg_shown.append(True)
 			start_fee = None
 			start_fee = None
 		else:
 		else:
-			start_fee = BTCAmt(ret) * opt.tx_fee_adj * tx.estimate_size() / 1024
+			start_fee = g.proto.coin_amt(ret) * opt.tx_fee_adj * tx.estimate_size() / 1024
 			if opt.verbose:
 			if opt.verbose:
 				msg('{} fee ({} confs): {} {}/kB'.format(desc,opt.tx_confs,ret,g.coin))
 				msg('{} fee ({} confs): {} {}/kB'.format(desc,opt.tx_confs,ret,g.coin))
 				msg('TX size (estimated): {}'.format(tx.estimate_size()))
 				msg('TX size (estimated): {}'.format(tx.estimate_size()))
@@ -156,17 +124,17 @@ def get_outputs_from_cmdline(cmd_args,tx):
 		if ',' in a:
 		if ',' in a:
 			a1,a2 = a.split(',',1)
 			a1,a2 = a.split(',',1)
 			if is_mmgen_id(a1) or is_coin_addr(a1):
 			if is_mmgen_id(a1) or is_coin_addr(a1):
-				coin_addr = mmaddr2coinaddr(c,a1,ad_w,ad_f) if is_mmgen_id(a1) else CoinAddr(a1)
-				tx.add_output(coin_addr,BTCAmt(a2))
+				coin_addr = mmaddr2coinaddr(a1,ad_w,ad_f) if is_mmgen_id(a1) else CoinAddr(a1)
+				tx.add_output(coin_addr,g.proto.coin_amt(a2))
 			else:
 			else:
-				die(2,"%s: unrecognized subargument in argument '%s'" % (a1,a))
+				die(2,"%s: invalid subargument in command-line argument '%s'" % (a1,a))
 		elif is_mmgen_id(a) or is_coin_addr(a):
 		elif is_mmgen_id(a) or is_coin_addr(a):
 			if tx.get_chg_output_idx() != None:
 			if tx.get_chg_output_idx() != None:
 				die(2,'ERROR: More than one change address listed on command line')
 				die(2,'ERROR: More than one change address listed on command line')
-			coin_addr = mmaddr2coinaddr(c,a,ad_w,ad_f) if is_mmgen_id(a) else CoinAddr(a)
-			tx.add_output(coin_addr,BTCAmt('0'),is_chg=True)
+			coin_addr = mmaddr2coinaddr(a,ad_w,ad_f) if is_mmgen_id(a) else CoinAddr(a)
+			tx.add_output(coin_addr,g.proto.coin_amt('0'),is_chg=True)
 		else:
 		else:
-			die(2,'%s: unrecognized argument' % a)
+			die(2,'{}: invalid command-line argument'.format(a))
 
 
 	if not tx.outputs:
 	if not tx.outputs:
 		die(2,'At least one output must be specified on the command line')
 		die(2,'At least one output must be specified on the command line')
@@ -175,6 +143,7 @@ def get_outputs_from_cmdline(cmd_args,tx):
 		die(2,('ERROR: No change output specified',wmsg['no_change_output'])[len(tx.outputs) == 1])
 		die(2,('ERROR: No change output specified',wmsg['no_change_output'])[len(tx.outputs) == 1])
 
 
 	tx.add_mmaddrs_to_outputs(ad_w,ad_f)
 	tx.add_mmaddrs_to_outputs(ad_w,ad_f)
+	tx.check_dup_addrs('outputs')
 
 
 	if not segwit_is_active() and tx.has_segwit_outputs():
 	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'
 		fs = '{} Segwit address requested on the command line, but Segwit is not active on this chain'
@@ -214,14 +183,16 @@ def get_inputs_from_user(tw,tx,caller):
 
 
 def txcreate(cmd_args,do_info=False,caller='txcreate'):
 def txcreate(cmd_args,do_info=False,caller='txcreate'):
 
 
+	rpc_init()
+
 	tx = MMGenTX()
 	tx = MMGenTX()
 
 
 	if opt.comment_file: tx.add_comment(opt.comment_file)
 	if opt.comment_file: tx.add_comment(opt.comment_file)
 
 
-	c = rpc_connection()
-
 	if not do_info: get_outputs_from_cmdline(cmd_args,tx)
 	if not do_info: get_outputs_from_cmdline(cmd_args,tx)
 
 
+	do_license_msg()
+
 	tw = MMGenTrackingWallet(minconf=opt.minconf)
 	tw = MMGenTrackingWallet(minconf=opt.minconf)
 	tw.view_and_sort(tx)
 	tw.view_and_sort(tx)
 	tw.display_total()
 	tw.display_total()
@@ -244,7 +215,7 @@ def txcreate(cmd_args,do_info=False,caller='txcreate'):
 		msg('Warning: Change address will be deleted as transaction produces no change')
 		msg('Warning: Change address will be deleted as transaction produces no change')
 		tx.del_output(chg_idx)
 		tx.del_output(chg_idx)
 	else:
 	else:
-		tx.update_output_amt(chg_idx,BTCAmt(change_amt))
+		tx.update_output_amt(chg_idx,g.proto.coin_amt(change_amt))
 
 
 	if not tx.send_amt:
 	if not tx.send_amt:
 		tx.send_amt = change_amt
 		tx.send_amt = change_amt
@@ -252,14 +223,14 @@ def txcreate(cmd_args,do_info=False,caller='txcreate'):
 	dmsg('tx: %s' % tx)
 	dmsg('tx: %s' % tx)
 
 
 	if not opt.yes:
 	if not opt.yes:
-		tx.add_comment()   # edits an existing comment
-	tx.create_raw(c)       # creates tx.hex, tx.txid
+		tx.add_comment()  # edits an existing comment
+	tx.create_raw()       # creates tx.hex, tx.txid
 
 
 	tx.add_timestamp()
 	tx.add_timestamp()
-	tx.add_blockcount(c)
+	tx.add_blockcount()
 	tx.chain = g.chain
 	tx.chain = g.chain
 
 
-	assert tx.sum_inputs() - tx.sum_outputs() <= g.max_tx_fee
+	assert tx.sum_inputs() - tx.sum_outputs() <= g.proto.max_tx_fee
 
 
 	qmsg('Transaction successfully created')
 	qmsg('Transaction successfully created')
 
 

+ 5 - 31
mmgen/txsign.py

@@ -27,34 +27,6 @@ from mmgen.addr import *
 
 
 pnm = g.proj_name
 pnm = g.proj_name
 
 
-txsign_notes = """
-Transactions may contain both {pnm} or non-{pnm} input addresses.
-
-To sign non-{pnm} inputs, a bitcoind wallet dump or flat key list is used
-as the key source ('--keys-from-file' option).
-
-To sign {pnm} inputs, key data is generated from a seed as with the
-{pnl}-addrgen and {pnl}-keygen commands.  Alternatively, a key-address file
-may be used (--mmgen-keys-from-file option).
-
-Multiple wallets or other seed files can be listed on the command line in
-any order.  If the seeds required to sign the transaction's inputs are not
-found in these files (or in the default wallet), the user will be prompted
-for seed data interactively.
-
-To prevent an attacker from crafting transactions with bogus {pnm}-to-Bitcoin
-address mappings, all outputs to {pnm} addresses are verified with a seed
-source.  Therefore, seed files or a key-address file for all {pnm} outputs
-must also be supplied on the command line if the data can't be found in the
-default wallet.
-
-Seed source files must have the canonical extensions listed in the 'FileExt'
-column below:
-
-  {f}
-""".format(f='\n  '.join(SeedSource.format_fmt_codes().splitlines()),
-			pnm=pnm,pnl=pnm.lower())
-
 wmsg = {
 wmsg = {
 	'mapping_error': """
 	'mapping_error': """
 {pnm} -> {c} address mappings differ!
 {pnm} -> {c} address mappings differ!
@@ -154,17 +126,19 @@ def get_keyaddrlist(opt):
 def get_keylist(opt):
 def get_keylist(opt):
 	if opt.keys_from_file:
 	if opt.keys_from_file:
 		l = get_lines_from_file(opt.keys_from_file,'key-address data',trim_comments=True)
 		l = get_lines_from_file(opt.keys_from_file,'key-address data',trim_comments=True)
-		kal = KeyAddrList(keylist=[m.split()[0] for m in l]) # accept bitcoind wallet dumps
+		kal = KeyAddrList(keylist=[m.split()[0] for m in l]) # accept coin daemon wallet dumps
 		kal.generate_addrs_from_keys()
 		kal.generate_addrs_from_keys()
 		return kal
 		return kal
 	return None
 	return None
 
 
-def txsign(opt,c,tx,seed_files,kl,kal,tx_num_str=''):
+def txsign(tx,seed_files,kl,kal,tx_num_str=''):
 
 
 	keys = MMGenList() # list of AddrListEntry objects
 	keys = MMGenList() # list of AddrListEntry objects
 	non_mm_addrs = tx.get_non_mmaddrs('inputs')
 	non_mm_addrs = tx.get_non_mmaddrs('inputs')
 
 
 	if non_mm_addrs:
 	if non_mm_addrs:
+		if not kl:
+			die(2,'Transaction has non-{} inputs, but no flat key list is present'.format(g.proj_name))
 		tmp = KeyAddrList(addrlist=non_mm_addrs,do_chksum=False)
 		tmp = KeyAddrList(addrlist=non_mm_addrs,do_chksum=False)
 		tmp.add_wifs(kl)
 		tmp.add_wifs(kl)
 		m = tmp.list_missing('sec')
 		m = tmp.list_missing('sec')
@@ -186,7 +160,7 @@ def txsign(opt,c,tx,seed_files,kl,kal,tx_num_str=''):
 	if extra_sids:
 	if extra_sids:
 		msg('Unused Seed ID{}: {}'.format(suf(extra_sids,'s'),' '.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(tx_num_str,keys):
 		return tx
 		return tx
 	else:
 	else:
 		die(3,red('Transaction {}could not be signed.'.format(tx_num_str)))
 		die(3,red('Transaction {}could not be signed.'.format(tx_num_str)))

+ 43 - 35
mmgen/util.py

@@ -421,12 +421,12 @@ def compare_or_die(val1, desc1, val2, desc2, e='Error'):
 	dmsg('%s OK (%s)' % (capfirst(desc2),val2))
 	dmsg('%s OK (%s)' % (capfirst(desc2),val2))
 	return True
 	return True
 
 
-def open_file_or_exit(filename,mode):
+def open_file_or_exit(filename,mode,silent=False):
 	try:
 	try:
 		f = open(filename, mode)
 		f = open(filename, mode)
 	except:
 	except:
 		op = ('writing','reading')['r' in mode]
 		op = ('writing','reading')['r' in mode]
-		die(2,"Unable to open file '%s' for %s" % (filename,op))
+		die(2,("Unable to open file '{}' for {}".format(filename,op),'')[silent])
 	return f
 	return f
 
 
 def check_file_type_and_access(fname,ftype,blkdev_ok=False):
 def check_file_type_and_access(fname,ftype,blkdev_ok=False):
@@ -468,12 +468,15 @@ def make_full_path(outdir,outfile):
 def get_seed_file(cmd_args,nargs,invoked_as=None):
 def get_seed_file(cmd_args,nargs,invoked_as=None):
 	from mmgen.filename import find_file_in_dir
 	from mmgen.filename import find_file_in_dir
 	from mmgen.seed import Wallet
 	from mmgen.seed import Wallet
+
 	wf = find_file_in_dir(Wallet,g.data_dir)
 	wf = find_file_in_dir(Wallet,g.data_dir)
 
 
 	wd_from_opt = bool(opt.hidden_incog_input_params or opt.in_fmt) # have wallet data from opt?
 	wd_from_opt = bool(opt.hidden_incog_input_params or opt.in_fmt) # have wallet data from opt?
 
 
 	import mmgen.opts as opts
 	import mmgen.opts as opts
 	if len(cmd_args) + (wd_from_opt or bool(wf)) < nargs:
 	if len(cmd_args) + (wd_from_opt or bool(wf)) < nargs:
+		if not wf:
+			msg('No default wallet found, and no other seed source was specified')
 		opts.usage()
 		opts.usage()
 	elif len(cmd_args) > nargs:
 	elif len(cmd_args) > nargs:
 		opts.usage()
 		opts.usage()
@@ -628,8 +631,8 @@ def get_words(infile,desc,prompt):
 	else:
 	else:
 		return get_words_from_user(prompt)
 		return get_words_from_user(prompt)
 
 
-def mmgen_decrypt_file_maybe(fn,desc=''):
-	d = get_data_from_file(fn,desc,binary=True)
+def mmgen_decrypt_file_maybe(fn,desc='',silent=False):
+	d = get_data_from_file(fn,desc,binary=True,silent=silent)
 	have_enc_ext = get_extension(fn) == g.mmenc_ext
 	have_enc_ext = get_extension(fn) == g.mmenc_ext
 	if have_enc_ext or not is_utf8(d):
 	if have_enc_ext or not is_utf8(d):
 		m = ('Attempting to decrypt','Decrypting')[have_enc_ext]
 		m = ('Attempting to decrypt','Decrypting')[have_enc_ext]
@@ -638,11 +641,11 @@ def mmgen_decrypt_file_maybe(fn,desc=''):
 		d = mmgen_decrypt_retry(d,desc)
 		d = mmgen_decrypt_retry(d,desc)
 	return d
 	return d
 
 
-def get_lines_from_file(fn,desc='',trim_comments=False):
-	dec = mmgen_decrypt_file_maybe(fn,desc)
+def get_lines_from_file(fn,desc='',trim_comments=False,silent=False):
+	dec = mmgen_decrypt_file_maybe(fn,desc,silent=silent)
 	ret = dec.decode('utf8').splitlines() # DOS-safe
 	ret = dec.decode('utf8').splitlines() # DOS-safe
 	if trim_comments: ret = remove_comments(ret)
 	if trim_comments: ret = remove_comments(ret)
-	vmsg(u"Got {} lines from file '{}'".format(len(ret),fn))
+	dmsg(u"Got {} lines from file '{}'".format(len(ret),fn))
 	return ret
 	return ret
 
 
 def get_data_from_user(desc='data',silent=False):
 def get_data_from_user(desc='data',silent=False):
@@ -653,9 +656,9 @@ def get_data_from_user(desc='data',silent=False):
 
 
 def get_data_from_file(infile,desc='data',dash=False,silent=False,binary=False):
 def get_data_from_file(infile,desc='data',dash=False,silent=False,binary=False):
 	if dash and infile == '-': return sys.stdin.read()
 	if dash and infile == '-': return sys.stdin.read()
-	if not silent and desc:
+	if not opt.quiet and not silent and desc:
 		qmsg("Getting %s from file '%s'" % (desc,infile))
 		qmsg("Getting %s from file '%s'" % (desc,infile))
-	f = open_file_or_exit(infile,('r','rb')[bool(binary)])
+	f = open_file_or_exit(infile,('r','rb')[bool(binary)],silent=silent)
 	data = f.read()
 	data = f.read()
 	f.close()
 	f.close()
 	return data
 	return data
@@ -777,32 +780,35 @@ def do_license_msg(immed=False):
 			msg_r('\r')
 			msg_r('\r')
 	msg('')
 	msg('')
 
 
-def get_bitcoind_cfg_options(cfg_keys):
-
-	cfg_file = os.path.join(g.daemon_data_dir,'bitcoin.conf')
-
-	cfg = dict([(k,v) for k,v in [split2(str(line).translate(None,'\t '),'=')
-			for line in get_lines_from_file(cfg_file,'')] if k in cfg_keys]) \
-				if file_is_readable(cfg_file) else {}
-
+def get_daemon_cfg_options(cfg_keys):
+	cfg_file = os.path.join(g.proto.daemon_data_dir,g.proto.name+'.conf')
+	try:
+		cfg = dict([(k,v) for k,v in [
+			split2(str(line).translate(None,'\t '),'=')
+				for line in get_lines_from_file(cfg_file,'',silent=bool(opt.quiet))
+					] if k in cfg_keys])
+	except:
+		vmsg("Warning: '{}' does not exist or is unreadable".format(cfg_file))
+		cfg = {}
 	for k in set(cfg_keys) - set(cfg.keys()): cfg[k] = ''
 	for k in set(cfg_keys) - set(cfg.keys()): cfg[k] = ''
-
 	return cfg
 	return cfg
 
 
-def get_bitcoind_auth_cookie():
-	f = os.path.join(g.daemon_data_dir,g.proto.data_subdir,'.cookie')
+def get_coin_daemon_auth_cookie():
+	f = os.path.join(g.proto.daemon_data_dir,g.proto.daemon_data_subdir,'.cookie')
 	return get_lines_from_file(f,'')[0] if file_is_readable(f) else ''
 	return get_lines_from_file(f,'')[0] if file_is_readable(f) else ''
 
 
-def rpc_connection():
+def rpc_init(reinit=False):
 
 
-	def check_chainfork_mismatch(c):
-		block0 = c.getblockhash(0)
-		latest = c.getblockcount()
+	if g.rpch != None and not reinit: return g.rpch
+
+	def check_chainfork_mismatch(conn):
+		block0 = conn.getblockhash(0)
+		latest = conn.getblockcount()
 		try:
 		try:
 			assert block0 == g.proto.block0,'Incorrect Genesis block for {}'.format(g.proto.__name__)
 			assert block0 == g.proto.block0,'Incorrect Genesis block for {}'.format(g.proto.__name__)
 			for fork in g.proto.forks:
 			for fork in g.proto.forks:
 				if latest < fork[0]: break
 				if latest < fork[0]: break
-				bhash = c.getblockhash(fork[0])
+				bhash = conn.getblockhash(fork[0])
 				assert bhash == fork[1], (
 				assert bhash == fork[1], (
 					'Bad block hash at fork block {}. Is this the {} chain?'.format(fork[0],fork[2].upper()))
 					'Bad block hash at fork block {}. Is this the {} chain?'.format(fork[0],fork[2].upper()))
 		except Exception as e:
 		except Exception as e:
@@ -816,24 +822,26 @@ def rpc_connection():
 		except Exception as e:
 		except Exception as e:
 			die(1,'{}\nChain is {}!'.format(e,g.chain))
 			die(1,'{}\nChain is {}!'.format(e,g.chain))
 
 
-	cfg = get_bitcoind_cfg_options(('rpcuser','rpcpassword'))
+	cfg = get_daemon_cfg_options(('rpcuser','rpcpassword'))
 	import mmgen.rpc
 	import mmgen.rpc
-	c = mmgen.rpc.BitcoinRPCConnection(
+	conn = mmgen.rpc.CoinDaemonRPCConnection(
 				g.rpc_host or 'localhost',
 				g.rpc_host or 'localhost',
 				g.rpc_port or g.proto.rpc_port,
 				g.rpc_port or g.proto.rpc_port,
-				g.rpc_user or cfg['rpcuser'], # MMGen's rpcuser,rpcpassword override bitcoind's
+				g.rpc_user or cfg['rpcuser'], # MMGen's rpcuser,rpcpassword override coin daemon's
 				g.rpc_password or cfg['rpcpassword'],
 				g.rpc_password or cfg['rpcpassword'],
-				auth_cookie=get_bitcoind_auth_cookie())
+				auth_cookie=get_coin_daemon_auth_cookie())
 
 
-	if not g.bitcoind_version: # First call
+	if not g.daemon_version: # First call
 		if g.bob or g.alice:
 		if g.bob or g.alice:
 			import regtest as rt
 			import regtest as rt
 			rt.user(('alice','bob')[g.bob],quiet=True)
 			rt.user(('alice','bob')[g.bob],quiet=True)
-		g.bitcoind_version = int(c.getnetworkinfo()['version'])
-		g.chain = c.getblockchaininfo()['chain']
+		g.daemon_version = int(conn.getnetworkinfo()['version'])
+		g.chain = conn.getblockchaininfo()['chain']
 		if g.chain != 'regtest': g.chain += 'net'
 		if g.chain != 'regtest': g.chain += 'net'
 		assert g.chain in g.chains
 		assert g.chain in g.chains
 		check_chaintype_mismatch()
 		check_chaintype_mismatch()
-		if g.chain == 'mainnet':
-			check_chainfork_mismatch(c)
-	return c
+	if g.chain == 'mainnet': # skip this for testnet, as Genesis block may change
+		check_chainfork_mismatch(conn)
+
+	g.rpch = conn
+	return conn

+ 80 - 44
scripts/mmgen-autosign

@@ -23,6 +23,12 @@ mmgen-autosign: Auto-sign MMGen transactions
 import sys,os,subprocess,time,signal
 import sys,os,subprocess,time,signal
 from stat import *
 from stat import *
 
 
+mountpoint   = '/mnt/tx'
+tx_dir       = os.path.join(mountpoint,'tx')
+part_label   = 'MMGEN_TX'
+shm_dir      = '/dev/shm'
+secret_fn    = 'txsign-secret'
+
 from mmgen.common import *
 from mmgen.common import *
 prog_name = os.path.basename(sys.argv[0])
 prog_name = os.path.basename(sys.argv[0])
 opts_data = lambda: {
 opts_data = lambda: {
@@ -30,10 +36,11 @@ opts_data = lambda: {
 	'usage':'[opts] [command]',
 	'usage':'[opts] [command]',
 	'options': """
 	'options': """
 -h, --help           Print this help message
 -h, --help           Print this help message
---, --longhelp       Print help message for long options (common options)
+-c, --coins=c        Coins to sign for (comma-separated list)
 -l, --led            Use status LED to signal standby, busy and error
 -l, --led            Use status LED to signal standby, busy and error
 -s, --stealth-led    Stealth LED mode - signal busy and error only, and only
 -s, --stealth-led    Stealth LED mode - signal busy and error only, and only
                      after successful authorization.
                      after successful authorization.
+-q, --quiet          Produce quieter output
 -v, --verbose        Produce more verbose output
 -v, --verbose        Produce more verbose output
 """,
 """,
 	'notes': """
 	'notes': """
@@ -50,13 +57,13 @@ wait       - start in loop mode: wait - mount - sign - unmount - wait
 If invoked with no command, the program mounts the USB stick, signs any
 If invoked with no command, the program mounts the USB stick, signs any
 unsigned transactions, unmounts the USB stick and exits.
 unsigned transactions, unmounts the USB stick and exits.
 
 
-If invoked with 'wait', the program waits in a loop, mounting, signing
-and unmounting every time the USB stick is inserted.  The status LED
-indicates whether the program is busy or in standby mode, i.e. ready for
-USB stick insertion or removal.
+If invoked with 'wait', the program waits in a loop, mounting, signing and
+unmounting every time the USB stick is inserted.  On supported platforms,
+the status LED indicates whether the program is busy or in standby mode,
+i.e. ready for USB stick insertion or removal.
 
 
-The USB stick must have a partition with the label MMGEN_TX and a user-
-writable directory '/tx', where unsigned MMGen transactions are placed.
+The USB stick must have a partition labeled MMGEN_TX and a user-writable
+directory '/tx', where unsigned MMGen transactions are placed.
 
 
 On the signing machine the directory /mnt/tx must exist and /etc/fstab must
 On the signing machine the directory /mnt/tx must exist and /etc/fstab must
 contain the following entry:
 contain the following entry:
@@ -83,22 +90,32 @@ in the scripts directory of the MMGen repository root where it resides.
 """.format(prog_name)
 """.format(prog_name)
 }
 }
 
 
-cmd_args = opts.init(opts_data)
-if opt.stealth_led: opt.led = True
+cmd_args = opts.init(opts_data,add_opts=['mmgen_keys_from_file','in_fmt'])
 
 
-mountpoint   = '/mnt/tx'
-tx_dir       = os.path.join(mountpoint,'tx')
-part_label   = 'MMGEN_TX'
-shm_dir      = '/dev/shm'
-secret_fn    = 'txsign-secret'
-tn_arg       = ['--testnet={}'.format(opt.testnet or '0')]
-coin_arg     = ['--coin={}'.format(opt.coin or 'btc')]
+import mmgen.tx
+from mmgen.txsign import txsign
+from mmgen.protocol import CoinProtocol
+
+if opt.stealth_led: opt.led = True
+opt.outdir = tx_dir
 
 
-def check_daemon_running():
-	cmd = ['mmgen-tool'] + tn_arg + coin_arg + ['getbalance']
-	vmsg('Executing: {}'.format(' '.join(cmd)))
-	try: subprocess.check_output(cmd)
-	except: die(1,'Daemon not running')
+def check_daemons_running():
+	if opt.coin:
+		die(1,'--coin option not supported with this command.  Use --coins instead')
+	if opt.coins:
+		coins = opt.coins.split(',')
+	else:
+		ymsg('Warning: no coins specified, so defaulting to BTC only')
+		coins = ['btc']
+
+	for coin in coins:
+		cmd = [ 'mmgen-tool',
+				'--coin={}'.format(coin),
+				'--testnet={}'.format(opt.testnet or 0)
+				] + ([],['--quiet'])[bool(opt.quiet)] + ['getbalance']
+		vmsg('Executing: {}'.format(' '.join(cmd)))
+		try: subprocess.check_output(cmd)
+		except: die(1,'{} daemon not running'.format(coin.upper()))
 
 
 def get_wallet_files():
 def get_wallet_files():
 	wfs = [f for f in os.listdir(shm_dir) if f[-8:] == '.mmwords']
 	wfs = [f for f in os.listdir(shm_dir) if f[-8:] == '.mmwords']
@@ -134,6 +151,19 @@ def do_umount():
 		msg('Unmounting '+mountpoint)
 		msg('Unmounting '+mountpoint)
 		subprocess.call(['umount',mountpoint])
 		subprocess.call(['umount',mountpoint])
 
 
+def sign_tx_file(txfile):
+	try:
+		g.coin = mmgen.tx.MMGenTX(txfile,md_only=True).coin
+		g.proto = CoinProtocol(g.coin,g.testnet)
+		reload(sys.modules['mmgen.tx'])
+		tx = mmgen.tx.MMGenTX(txfile)
+		rpc_init(reinit=True)
+		txsign(tx,wfs,None,None)
+		tx.write_to_file(ask_write=False)
+		return True
+	except:
+		return False
+
 def sign():
 def sign():
 	dirlist  = os.listdir(tx_dir)
 	dirlist  = os.listdir(tx_dir)
 	raw      = [f      for f in dirlist if f[-6:] == '.rawtx']
 	raw      = [f      for f in dirlist if f[-6:] == '.rawtx']
@@ -141,16 +171,34 @@ def sign():
 	unsigned = [os.path.join(tx_dir,f) for f in raw if f[:-6] not in signed]
 	unsigned = [os.path.join(tx_dir,f) for f in raw if f[:-6] not in signed]
 
 
 	if unsigned:
 	if unsigned:
-		cmd = ['mmgen-txsign','--yes','--outdir='+tx_dir] + tn_arg + coin_arg + unsigned + wfs
-		vmsg('Executing: {}'.format(' '.join(cmd)))
-		ret = subprocess.call(cmd)
-		msg('')
+		fails = 0
+		for txfile in unsigned:
+			ret = sign_tx_file(txfile)
+			if not ret:
+				fails += 1
+			qmsg('')
+		if fails: ymsg('{} failed signs'.format(fails))
 		time.sleep(0.3)
 		time.sleep(0.3)
-		return (1,0)[ret==0]
+		return False if fails else True
 	else:
 	else:
 		msg('No unsigned transactions')
 		msg('No unsigned transactions')
 		time.sleep(1)
 		time.sleep(1)
-		return 0
+		return True
+
+def do_sign():
+	if not opt.stealth_led: set_led('busy')
+	do_mount()
+	ret = get_secret_in_dir(tx_dir,on_fail='return')
+	if ret == secret:
+		if opt.stealth_led: set_led('busy')
+		ret = sign()
+		do_umount()
+		set_led(('standby','off','error')[(not ret)*2 or bool(opt.stealth_led)])
+	else:
+		if ret != None:
+			msg('Secret is incorrect!')
+		do_umount()
+		if not opt.stealth_led: set_led('error')
 
 
 def wipe_existing_secret_files():
 def wipe_existing_secret_files():
 	for d in (tx_dir,shm_dir):
 	for d in (tx_dir,shm_dir):
@@ -203,33 +251,18 @@ def do_led(on,off):
 
 
 def set_led(cmd):
 def set_led(cmd):
 	if not opt.led: return
 	if not opt.led: return
+	vmsg("Setting LED state to '{}'".format(cmd))
 	timings = {
 	timings = {
 		'off':     ( 0, 0 ),
 		'off':     ( 0, 0 ),
 		'standby': ( 2.2, 0.2 ),
 		'standby': ( 2.2, 0.2 ),
 		'busy':    ( 0.06, 0.06 ),
 		'busy':    ( 0.06, 0.06 ),
 		'error':   ( 0.5, 0.5 )}[cmd]
 		'error':   ( 0.5, 0.5 )}[cmd]
-	vmsg("Executing command '{}'".format(cmd))
 	global led_thread
 	global led_thread
 	if led_thread:
 	if led_thread:
 		ev.set(); led_thread.join(); ev.clear()
 		ev.set(); led_thread.join(); ev.clear()
 	led_thread = threading.Thread(target=do_led,name='LED loop',args=timings)
 	led_thread = threading.Thread(target=do_led,name='LED loop',args=timings)
 	led_thread.start()
 	led_thread.start()
 
 
-def do_sign():
-	if not opt.stealth_led: set_led('busy')
-	do_mount()
-	ret = get_secret_in_dir(tx_dir,on_fail='return')
-	if ret == secret:
-		if opt.stealth_led: set_led('busy')
-		exit_val = sign()
-		do_umount()
-		set_led(('standby','off','error')[bool(exit_val)*2 or bool(opt.stealth_led)])
-	else:
-		if ret != None:
-			msg('Secret is incorrect!')
-		do_umount()
-		if not opt.stealth_led: set_led('error')
-
 def get_insert_status():
 def get_insert_status():
 	try: os.stat(os.path.join('/dev/disk/by-label/',part_label))
 	try: os.stat(os.path.join('/dev/disk/by-label/',part_label))
 	except: return False
 	except: return False
@@ -317,8 +350,11 @@ if opt.led:
 
 
 check_wipe_present()
 check_wipe_present()
 wfs = get_wallet_files()
 wfs = get_wallet_files()
+
 secret = get_secret_in_dir(shm_dir,on_fail='die')
 secret = get_secret_in_dir(shm_dir,on_fail='die')
-check_daemon_running()
+
+check_daemons_running()
+#sign(); sys.exit()
 
 
 signal.signal(signal.SIGTERM,handler)
 signal.signal(signal.SIGTERM,handler)
 signal.signal(signal.SIGINT,handler)
 signal.signal(signal.SIGINT,handler)

+ 137 - 37
scripts/test-release.sh

@@ -1,25 +1,36 @@
 #!/bin/bash
 #!/bin/bash
 # Tested on Linux, MinGW-64
 # Tested on Linux, MinGW-64
+# MinGW's bash 3.1.17 doesn't do ${var^^}
 
 
+dfl_tests='obj btc btc_tn btc_rt bch bch_rt ltc ltc_tn ltc_rt tool gen'
 PROGNAME=$(basename $0)
 PROGNAME=$(basename $0)
-while getopts hint OPT
+while getopts hinPt OPT
 do
 do
 	case "$OPT" in
 	case "$OPT" in
-	h)  printf "  %-16s Test MMGen release\n" "${PROGNAME^^}:"
+	h)  printf "  %-16s Test MMGen release\n" "${PROGNAME}:"
 		echo   "  USAGE:           $PROGNAME [options] branch [tests]"
 		echo   "  USAGE:           $PROGNAME [options] branch [tests]"
 		echo   "  OPTIONS: '-h'  Print this help message"
 		echo   "  OPTIONS: '-h'  Print this help message"
 		echo   "           '-i'  Install only; don't run tests"
 		echo   "           '-i'  Install only; don't run tests"
 		echo   "           '-n'  Don't install; test in place"
 		echo   "           '-n'  Don't install; test in place"
+		echo   "           '-P'  Don't pause between tests"
 		echo   "           '-t'  Print the tests without running them"
 		echo   "           '-t'  Print the tests without running them"
 		echo   "  AVAILABLE TESTS:"
 		echo   "  AVAILABLE TESTS:"
-		echo   "     1 - main"
-		echo   "     2 - tooltest"
-		echo   "     3 - gentest"
-		echo   "     4 - regtest"
+		echo   "     obj    - data objects"
+		echo   "     btc    - bitcoin"
+		echo   "     btc_tn - bitcoin testnet"
+		echo   "     btc_rt - bitcoin regtest"
+		echo   "     bch    - bitcoin cash (BCH)"
+		echo   "     bch_rt - bitcoin cash (BCH) regtest"
+		echo   "     ltc    - litecoin"
+		echo   "     ltc_tn - litecoin testnet"
+		echo   "     ltc_rt - litecoin regtest"
+		echo   "     tool   - tooltest (all supported coins)"
+		echo   "     gen    - gentest (all supported coins)"
 		echo   "  By default, all tests are run"
 		echo   "  By default, all tests are run"
 		exit ;;
 		exit ;;
 	i)  INSTALL_ONLY=1 ;;
 	i)  INSTALL_ONLY=1 ;;
 	n)  NO_INSTALL=1 ;;
 	n)  NO_INSTALL=1 ;;
+	P)  NO_PAUSE=1 ;;
 	t)  TESTING=1 ;;
 	t)  TESTING=1 ;;
 	*)  exit ;;
 	*)  exit ;;
 	esac
 	esac
@@ -27,9 +38,15 @@ done
 
 
 shift $((OPTIND-1))
 shift $((OPTIND-1))
 
 
-set -e
-GREEN="\e[32;1m" YELLOW="\e[33;1m" RESET="\e[0m"
+RED="\e[31;1m" GREEN="\e[32;1m" YELLOW="\e[33;1m" RESET="\e[0m"
+
 BRANCH=$1; shift
 BRANCH=$1; shift
+BRANCHES=$(git branch)
+FOUND_BRANCH=$(for b in ${BRANCHES/\*}; do [ "$b" == "$BRANCH" ] && echo ok; done)
+[ "$FOUND_BRANCH" ] || { echo "Branch '$BRANCH' not found!"; exit; }
+
+set -e
+
 REFDIR=test/ref
 REFDIR=test/ref
 if uname -a | grep -qi mingw; then SUDO='' MINGW=1; else SUDO='sudo' MINGW=''; fi
 if uname -a | grep -qi mingw; then SUDO='' MINGW=1; else SUDO='sudo' MINGW=''; fi
 
 
@@ -56,27 +73,112 @@ function install {
 	[ "$MINGW" ] && ./setup.py build --compiler=mingw32
 	[ "$MINGW" ] && ./setup.py build --compiler=mingw32
 	eval "$SUDO ./setup.py install"
 	eval "$SUDO ./setup.py install"
 }
 }
-[ -z "$TESTING" ] && LS='\n'
 function do_test {
 function do_test {
 	set +x
 	set +x
 	for i in "$@"; do
 	for i in "$@"; do
+		LS='\n'
+		[ "$TESTING" ] && LS=''
+		echo $i | grep -q 'gentest' && LS=''
 		echo -e "$LS${GREEN}Running:$RESET $YELLOW$i$RESET"
 		echo -e "$LS${GREEN}Running:$RESET $YELLOW$i$RESET"
-		[ "$TESTING" ] || eval "$i"
+		[ "$TESTING" ] || eval "$i" || { echo -e $RED'Test failed!'$RESET; exit; }
 	done
 	done
 }
 }
+i_obj='Data objects'
+s_obj='Testing data objects'
+t_obj=(
+    'test/objtest.py --coin=btc -S'
+    'test/objtest.py --coin=btc --testnet=1 -S'
+    'test/objtest.py --coin=ltc -S'
+    'test/objtest.py --coin=ltc --testnet=1 -S')
+f_obj='Data object test complete'
 
 
-T1=('test/test.py -On'
+i_btc='Bitcoin mainnet'
+s_btc='The bitcoin (mainnet) daemon must both be running for the following tests'
+t_btc=(
+    'test/test.py -On'
 	'test/test.py -On --segwit dfl_wallet main ref ref_other'
 	'test/test.py -On --segwit dfl_wallet main ref ref_other'
-	'test/test.py -On --coin=bch dfl_wallet main ref ref_other'
-	'test/test.py -On --segwit-random dfl_wallet main')
-T2=('test/tooltest.py' 'test/tooltest.py --testnet=1') # tooltest tests both segwit and non-segwit
-T3=("test/gentest.py -q 2 $REFDIR/btcwallet.dump"
-	"test/gentest.py -q --testnet=1 2 $REFDIR/btcwallet-testnet.dump"
+	'test/test.py -On --segwit-random dfl_wallet main'
+    'test/tooltest.py rpc'
+	"scripts/compute-file-chksum.py $REFDIR/*testnet.rawtx >/dev/null 2>&1")
+f_btc='You may stop the bitcoin (mainnet) daemon if you wish'
+
+i_btc_tn='Bitcoin testnet'
+s_btc_tn='The bitcoin testnet daemon must both be running for the following tests'
+t_btc_tn=(
+    'test/test.py -On --testnet=1'
+	'test/test.py -On --testnet=1 --segwit dfl_wallet main ref ref_other'
+	'test/test.py -On --testnet=1 --segwit-random dfl_wallet main'
+	'test/tooltest.py --testnet=1 rpc')
+f_btc_tn='You may stop the bitcoin testnet daemon if you wish'
+
+i_btc_rt='Bitcoin regtest'
+s_btc_rt="The following tests will test MMGen's regtest (Bob and Alice) mode"
+t_btc_rt=('test/test.py -On regtest')
+f_btc_rt="Regtest (Bob and Alice) mode tests for BTC completed"
+
+i_bch='Bitcoin cash (BCH)'
+s_bch='The bitcoin cash daemon (Bitcoin ABC) must both be running for the following tests'
+t_bch=('test/test.py -On --coin=bch dfl_wallet main ref ref_other')
+f_bch='You may stop the Bitcoin ABC daemon if you wish'
+
+i_bch_rt='Bitcoin cash (BCH) regtest'
+s_bch_rt="The following tests will test MMGen's regtest (Bob and Alice) mode"
+t_bch_rt=('test/test.py --coin=bch -On regtest')
+f_bch_rt="Regtest (Bob and Alice) mode tests for BCH completed"
+
+i_ltc='Litecoin'
+s_ltc='The litecoin daemon must both be running for the following tests'
+t_ltc=(
+    'test/test.py --coin=ltc -On dfl_wallet main'
+    'test/test.py --coin=ltc --segwit -On dfl_wallet main'
+    'test/test.py --coin=ltc --segwit-random -On dfl_wallet main'
+    'test/tooltest.py --coin=ltc rpc'
+)
+f_ltc='You may stop the litecoin daemon if you wish'
+
+i_ltc_tn='Litecoin testnet'
+s_ltc_tn='The litecoin testnet daemon must both be running for the following tests'
+t_ltc_tn=(
+    'test/test.py --coin=ltc -On --testnet=1'
+	'test/test.py --coin=ltc -On --testnet=1 --segwit dfl_wallet main ref ref_other'
+	'test/test.py --coin=ltc -On --testnet=1 --segwit-random dfl_wallet main'
+	'test/tooltest.py --coin=ltc --testnet=1 rpc')
+f_ltc_tn='You may stop the litecoin testnet daemon if you wish'
+
+i_ltc_rt='Litecoin regtest'
+s_ltc_rt="The following tests will test MMGen's regtest (Bob and Alice) mode"
+t_ltc_rt=('test/test.py --coin=ltc -On regtest')
+f_ltc_rt="Regtest (Bob and Alice) mode tests for LTC completed"
+
+i_tool='Tooltest'
+s_tool='The following tests will run test/tooltest.py for all supported coins'
+t_tool=(
+	'test/tooltest.py --coin=btc util'
+	'test/tooltest.py --coin=btc cryptocoin'
+	'test/tooltest.py --coin=btc mnemonic'
+	'test/tooltest.py --coin=ltc util'
+	'test/tooltest.py --coin=ltc cryptocoin'
+	'test/tooltest.py --coin=ltc mnemonic'
+	)
+f_tool="tooltest tests completed"
+
+i_gen='Gentest'
+s_gen='The following tests will run test/gentest.py on mainnet and testnet for all supported coins'
+t_gen=(
+	"test/gentest.py -q 2 $REFDIR/btcwallet.dump"
 	'test/gentest.py -q 1:2 10'
 	'test/gentest.py -q 1:2 10'
 	'test/gentest.py -q --segwit 1:2 10'
 	'test/gentest.py -q --segwit 1:2 10'
-#	"scripts/tx-old2new.py -S $REFDIR/tx_*raw >/dev/null 2>&1"
-	"scripts/compute-file-chksum.py $REFDIR/*testnet.rawtx >/dev/null 2>&1")
-T4=('test/test.py -On regtest')
+    "test/gentest.py -q --testnet=1 2 $REFDIR/btcwallet-testnet.dump"
+	'test/gentest.py -q --testnet=1 1:2 10'
+	'test/gentest.py -q --testnet=1 --segwit 1:2 10'
+    "test/gentest.py -q --coin=ltc 2 $REFDIR/litecoin/ltcwallet.dump"
+	'test/gentest.py -q --coin=ltc 1:2 10'
+	'test/gentest.py -q --coin=ltc --segwit 1:2 10'
+    "test/gentest.py -q --coin=ltc --testnet=1 2 $REFDIR/litecoin/ltcwallet-testnet.dump"
+	'test/gentest.py -q --coin=ltc --testnet=1 1:2 10'
+	'test/gentest.py -q --coin=ltc --testnet=1 --segwit 1:2 10'
+	)
+f_gen="gentest tests completed"
 
 
 [ -d .git -a -z "$NO_INSTALL"  -a -z "$TESTING" ] && {
 [ -d .git -a -z "$NO_INSTALL"  -a -z "$TESTING" ] && {
 	check
 	check
@@ -85,26 +187,24 @@ T4=('test/test.py -On regtest')
 }
 }
 [ "$INSTALL_ONLY" ] && exit
 [ "$INSTALL_ONLY" ] && exit
 
 
+function skip_maybe {
+    echo -n "Enter 's' to skip, or ENTER to continue: "; read
+    [ "$REPLY" == 's' ] && return 0
+    return 1
+}
 function run_tests {
 function run_tests {
 	for t in $1; do
 	for t in $1; do
-		[ $t == 4 ] && LS=''
-		eval "do_test \"\${T$t[@]}\""
+        eval echo -e \${GREEN}'###' Running $(echo \$i_$t) tests\$RESET
+        [ "$PAUSE" ] && { eval echo $(echo \$s_$t); skip_maybe && continue; }
+#       echo RUNNING
+		eval "do_test \"\${t_$t[@]}\""
+        eval echo -e \$GREEN$(echo \$f_$t)\$RESET
 	done
 	done
 }
 }
 
 
-if [ "$*" ]; then
-	run_tests "$*"
-else
-	echo 'Bitcoin and Bitcoin ABC must both be running for the following tests'
-	echo 'The bitcoin-abc daemon must be listening on RPC port 8442 (-rpcport 8442)'
-	echo -n 'Hit ENTER to continue: '; read
-	run_tests '1'
-	echo 'The bitcoin (mainnet) and testnet daemons must both be running for the following tests'
-	echo -n 'Hit ENTER to continue: '; read
-	run_tests '2 3'
-	echo 'You may stop the mainnet and testnet daemons now'
-	echo -n 'Hit ENTER to continue: '; read
-	run_tests '4'
-fi
-
-echo -e "$LS${GREEN}All OK$RESET"
+tests=$dfl_tests
+[ "$*" ] && tests="$*"
+[ "$NO_PAUSE" ] || PAUSE=1
+run_tests "$tests"
+
+echo -e "${GREEN}All OK$RESET"

+ 1 - 1
scripts/traceback.py

@@ -19,4 +19,4 @@ except:
 	def yellow(s): return '{e}[33;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)))
 	sys.stdout.write('{}{}'.format(yellow(''.join(l)),red(exc)))
 	traceback.print_exc(file=f)
 	traceback.print_exc(file=f)
-
+	sys.exit(1)

+ 16 - 16
setup.py

@@ -97,7 +97,7 @@ setup(
 		author_email = g.email,
 		author_email = g.email,
 		url          = g.proj_url,
 		url          = g.proj_url,
 		license      = 'GNU GPL v3',
 		license      = 'GNU GPL v3',
-		platforms    = 'Linux, MS Windows, Raspberry Pi',
+		platforms    = 'Linux, MS Windows, Raspberry Pi/Raspbian, Orange Pi/Armbian',
 		keywords     = g.keywords,
 		keywords     = g.keywords,
 		cmdclass     = { 'build_ext': my_build_ext, 'install_data': my_install_data },
 		cmdclass     = { 'build_ext': my_build_ext, 'install_data': my_install_data },
 		ext_modules  = [module1],
 		ext_modules  = [module1],
@@ -149,20 +149,20 @@ setup(
 			'mmgen.share.Opts',
 			'mmgen.share.Opts',
 		],
 		],
 		scripts=[
 		scripts=[
-			'mmgen-addrgen',
-			'mmgen-keygen',
-			'mmgen-passgen',
-			'mmgen-addrimport',
-			'mmgen-passchg',
-			'mmgen-regtest',
-			'mmgen-walletchk',
-			'mmgen-walletconv',
-			'mmgen-walletgen',
-			'mmgen-txcreate',
-			'mmgen-txbump',
-			'mmgen-txsign',
-			'mmgen-txsend',
-			'mmgen-txdo',
-			'mmgen-tool'
+			'cmds/mmgen-addrgen',
+			'cmds/mmgen-keygen',
+			'cmds/mmgen-passgen',
+			'cmds/mmgen-addrimport',
+			'cmds/mmgen-passchg',
+			'cmds/mmgen-regtest',
+			'cmds/mmgen-walletchk',
+			'cmds/mmgen-walletconv',
+			'cmds/mmgen-walletgen',
+			'cmds/mmgen-txcreate',
+			'cmds/mmgen-txbump',
+			'cmds/mmgen-txsign',
+			'cmds/mmgen-txsend',
+			'cmds/mmgen-txdo',
+			'cmds/mmgen-tool'
 		]
 		]
 	)
 	)

+ 3 - 3
test/gentest.py

@@ -17,7 +17,7 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
 """
 """
-test/gentest.py:  Bitcoin key/address generation tests for the MMGen suite
+test/gentest.py:  Cryptocoin key/address generation tests for the MMGen suite
 """
 """
 
 
 import sys,os
 import sys,os
@@ -56,8 +56,8 @@ EXAMPLES:
   {prog} 2 1000
   {prog} 2 1000
     (test speed of secp256k1 library address generation, 1000 rounds)
     (test speed of secp256k1 library address generation, 1000 rounds)
   {prog} 2 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)
+    (compare addrs generated with secp256k1 library to {dn} wallet dump)
+""".format(prog='gentest.py',pnm=g.proj_name,snum=rounds,dn=g.proto.daemon_name)
 }
 }
 
 
 sys.argv = [sys.argv[0]] + ['--skip-cfg-file'] + sys.argv[1:]
 sys.argv = [sys.argv[0]] + ['--skip-cfg-file'] + sys.argv[1:]

+ 14 - 9
test/mmgen_pexpect.py

@@ -92,36 +92,39 @@ class MMGenPexpect(object):
 		atexit.register(lambda: os.system('stty sane'))
 		atexit.register(lambda: os.system('stty sane'))
 		NL = '\n'
 		NL = '\n'
 
 
-	def __init__(self,name,mmgen_cmd,cmd_args,desc,no_output=False,passthru_args=[]):
+	def __init__(self,name,mmgen_cmd,cmd_args,desc,no_output=False,passthru_args=[],msg_only=False):
 		cmd_args = ['--{}{}'.format(k.replace('_','-'),
 		cmd_args = ['--{}{}'.format(k.replace('_','-'),
 			'='+getattr(opt,k) if getattr(opt,k) != True else ''
 			'='+getattr(opt,k) if getattr(opt,k) != True else ''
 			) for k in passthru_args if getattr(opt,k)] \
 			) for k in passthru_args if getattr(opt,k)] \
 			+ ['--data-dir='+os.path.join('test','data_dir')] + cmd_args
 			+ ['--data-dir='+os.path.join('test','data_dir')] + cmd_args
-		cmd = (('./','')[bool(opt.system)]+mmgen_cmd,'python')[g.platform=='win']
-		args = (cmd_args,[mmgen_cmd]+cmd_args)[g.platform=='win']
+
+		if g.platform == 'win': cmd,args = 'python',[mmgen_cmd]+cmd_args
+		else:                   cmd,args = mmgen_cmd,cmd_args
 
 
 		for i in args:
 		for i in args:
 			if type(i) not in (str,unicode):
 			if type(i) not in (str,unicode):
 				m1 = 'Error: missing input files in cmd line?:'
 				m1 = 'Error: missing input files in cmd line?:'
 				m2 = '\nName: {}\nCmd: {}\nCmd args: {}'
 				m2 = '\nName: {}\nCmd: {}\nCmd args: {}'
 				die(2,(m1+m2).format(name,cmd,args))
 				die(2,(m1+m2).format(name,cmd,args))
+
 		if opt.popen_spawn:
 		if opt.popen_spawn:
-			args = [("'"+a+"'" if ' ' in a else a) for a in args]
-		cmd_str = '{} {}'.format(cmd,' '.join(args))
-		if opt.popen_spawn:
-			cmd_str = cmd_str.replace('\\','/')
+			args = [(a,"'{}'".format(a))[' ' in a] for a in args]
+
+		cmd_str = '{} {}'.format(cmd,' '.join(args)).replace('\\','/')
 
 
 		if opt.log:
 		if opt.log:
 			log_fd.write(cmd_str+'\n')
 			log_fd.write(cmd_str+'\n')
+
 		if opt.verbose or opt.print_cmdline or opt.exact_output:
 		if opt.verbose or opt.print_cmdline or opt.exact_output:
 			clr1,clr2,eol = ((green,cyan,'\n'),(nocolor,nocolor,' '))[bool(opt.print_cmdline)]
 			clr1,clr2,eol = ((green,cyan,'\n'),(nocolor,nocolor,' '))[bool(opt.print_cmdline)]
 			sys.stderr.write(green('Testing: {}\n'.format(desc)))
 			sys.stderr.write(green('Testing: {}\n'.format(desc)))
-			sys.stderr.write(clr1('Executing {}{}'.format(clr2(cmd_str),eol)))
+			if not msg_only:
+				sys.stderr.write(clr1('Executing {}{}'.format(clr2(cmd_str),eol)))
 		else:
 		else:
 			m = 'Testing %s: ' % desc
 			m = 'Testing %s: ' % desc
 			msg_r(m)
 			msg_r(m)
 
 
-		if mmgen_cmd == '': return
+		if msg_only: return
 
 
 		if opt.direct_exec:
 		if opt.direct_exec:
 			msg('')
 			msg('')
@@ -135,6 +138,7 @@ class MMGenPexpect(object):
 			if opt.traceback:
 			if opt.traceback:
 				cmd,args = g.traceback_cmd,[cmd]+args
 				cmd,args = g.traceback_cmd,[cmd]+args
 				cmd_str = g.traceback_cmd + ' ' + cmd_str
 				cmd_str = g.traceback_cmd + ' ' + cmd_str
+#			Msg('\ncmd_str: {}'.format(cmd_str))
 			if opt.popen_spawn:
 			if opt.popen_spawn:
 				self.p = PopenSpawn(cmd_str)
 				self.p = PopenSpawn(cmd_str)
 			else:
 			else:
@@ -143,6 +147,7 @@ class MMGenPexpect(object):
 
 
 	def ok(self,exit_val=0):
 	def ok(self,exit_val=0):
 		ret = self.p.wait()
 		ret = self.p.wait()
+#		Msg('expect: {} got: {}'.format(exit_val,ret))
 		if ret != exit_val:
 		if ret != exit_val:
 			die(1,red('test.py: spawned program exited with value {}'.format(ret)))
 			die(1,red('test.py: spawned program exited with value {}'.format(ret)))
 		if opt.profile: return
 		if opt.profile: return

+ 42 - 27
test/objtest.py

@@ -96,6 +96,7 @@ def run_test(test,arg,input_data):
 		die(2,red('{}'.format(e[0])))
 		die(2,red('{}'.format(e[0])))
 
 
 r32,r24,r16,r17,r18 = os.urandom(32),os.urandom(24),os.urandom(16),os.urandom(17),os.urandom(18)
 r32,r24,r16,r17,r18 = os.urandom(32),os.urandom(24),os.urandom(16),os.urandom(17),os.urandom(18)
+tw_pfx = g.proto.base_coin.lower()+':'
 
 
 from collections import OrderedDict
 from collections import OrderedDict
 tests = OrderedDict([
 tests = OrderedDict([
@@ -114,14 +115,15 @@ tests = OrderedDict([
 		'bad':  ('-3.2','0.123456789',123L,'123L',22000000,20999999.12345678),
 		'bad':  ('-3.2','0.123456789',123L,'123L',22000000,20999999.12345678),
 		'good': (('20999999.12345678',Decimal('20999999.12345678')),)
 		'good': (('20999999.12345678',Decimal('20999999.12345678')),)
 		}),
 		}),
-	('BTCAddr', {
+	('CoinAddr', {
 		'bad':  (1,'x','я'),
 		'bad':  (1,'x','я'),
-		'good': (
-			'1MjjELEy6EJwk8fSNfpS8b5teFRo4X5fZr',
-			'32GiSWo9zJQgkCmjAaLRrbPwXhKry2jHhj',
-			'n2FgXPKwuFkCXF946EnoxWJDWF2VwQ6q8J',
-			'2MspvWFjBbkv2wzQGqhxJUYPCk3Y2jMaxLN'
-	)}),
+		'good': {
+			'btc': (('1MjjELEy6EJwk8fSNfpS8b5teFRo4X5fZr','32GiSWo9zJQgkCmjAaLRrbPwXhKry2jHhj'),
+					('n2FgXPKwuFkCXF946EnoxWJDWF2VwQ6q8J','2MspvWFjBbkv2wzQGqhxJUYPCk3Y2jMaxLN')),
+			'ltc': (('LXYx4j8PDGE8GEwDFnEQhcLyHFGsRxSJwt','MEnuCzUGHaQx9fK5WYvLwR1NK4SAo8HmSr'),
+					('n2D3joAy3yE5fqxUeCp38X6uPUcVn7EFw9','QN59YbnHsPQcbKWSq9PmTpjrhBnHGQqRmf'))
+		}[g.coin.lower()][bool(g.testnet)]
+	}),
 	('SeedID', {
 	('SeedID', {
 		'bad':  (
 		'bad':  (
 			{'sid':'я'},
 			{'sid':'я'},
@@ -138,8 +140,8 @@ tests = OrderedDict([
 		'good': (('F00BAA12:99','F00BAA12:L:99'),'F00BAA12:L:99','F00BAA12:S:99')
 		'good': (('F00BAA12:99','F00BAA12:L:99'),'F00BAA12:L:99','F00BAA12:S:99')
 	}),
 	}),
 	('TwMMGenID', {
 	('TwMMGenID', {
-		'bad':  ('x','я','я:я',1,'f00f00f','a:b','x:L:3','F00BAA12:0','F00BAA12:Z:99','btc:','btc:я'),
-		'good': (('F00BAA12:99','F00BAA12:L:99'),'F00BAA12:L:99','F00BAA12:S:9999999','btc:x')
+		'bad':  ('x','я','я:я',1,'f00f00f','a:b','x:L:3','F00BAA12:0','F00BAA12:Z:99',tw_pfx,tw_pfx+'я'),
+		'good': (('F00BAA12:99','F00BAA12:L:99'),'F00BAA12:L:99','F00BAA12:S:9999999',tw_pfx+'x')
 	}),
 	}),
 	('TwComment', {
 	('TwComment', {
 		'bad':  ('я',"comment too long for tracking wallet",),
 		'bad':  ('я',"comment too long for tracking wallet",),
@@ -147,12 +149,12 @@ tests = OrderedDict([
 	}),
 	}),
 	('TwLabel', {
 	('TwLabel', {
 		'bad':  ('x x','x я','я:я',1,'f00f00f','a:b','x:L:3','F00BAA12:0 x',
 		'bad':  ('x x','x я','я:я',1,'f00f00f','a:b','x:L:3','F00BAA12:0 x',
-				'F00BAA12:Z:99','F00BAA12:L:99 я','btc: x','btc:я x'),
+				'F00BAA12:Z:99','F00BAA12:L:99 я',tw_pfx+' x',tw_pfx+'я x'),
 		'good': (
 		'good': (
 			('F00BAA12:99 a comment','F00BAA12:L:99 a comment'),
 			('F00BAA12:99 a comment','F00BAA12:L:99 a comment'),
 			'F00BAA12:L:99 comment',
 			'F00BAA12:L:99 comment',
 			'F00BAA12:S:9999999 comment',
 			'F00BAA12:S:9999999 comment',
-			'btc:x comment')
+			tw_pfx+'x comment')
 	}),
 	}),
 	('HexStr', {
 	('HexStr', {
 		'bad':  (1,[],'\0','\1','я','g','gg','FF','f00'),
 		'bad':  (1,[],'\0','\1','я','g','gg','FF','f00'),
@@ -162,18 +164,22 @@ tests = OrderedDict([
 		'bad':  (1,[],'\0','\1','я','g','gg','FF','f00','F00F0012'),
 		'bad':  (1,[],'\0','\1','я','g','gg','FF','f00','F00F0012'),
 		'good': ('DEADBE','F00BAA')
 		'good': ('DEADBE','F00BAA')
 	}),
 	}),
-	('BitcoinTxID',{
+	('CoinTxID',{
 		'bad':  (1,[],'\0','\1','я','g','gg','FF','f00','F00F0012',hexlify(r16),hexlify(r32)+'ee'),
 		'bad':  (1,[],'\0','\1','я','g','gg','FF','f00','F00F0012',hexlify(r16),hexlify(r32)+'ee'),
 		'good': (hexlify(r32),)
 		'good': (hexlify(r32),)
 	}),
 	}),
 	('WifKey', {
 	('WifKey', {
 		'bad':  (1,[],'\0','\1','я','g','gg','FF','f00',hexlify(r16),'2MspvWFjBbkv2wzQGqhxJUYPCk3Y2jMaxLN'),
 		'bad':  (1,[],'\0','\1','я','g','gg','FF','f00',hexlify(r16),'2MspvWFjBbkv2wzQGqhxJUYPCk3Y2jMaxLN'),
-		'good': (
-			'5KXEpVzjWreTcQoG5hX357s1969MUKNLuSfcszF6yu84kpsNZKb',
-			'KwWr9rDh8KK5TtDa3HLChEvQXNYcUXpwhRFUPc5uSNnMtqNKLFhk',
-			{'arg':'93HsQEpH75ibaUJYi3QwwiQxnkW4dUuYFPXZxcbcKds7XrqHkY6','testnet':True},
-			{'arg':'cMsqcmDYZP1LdKgqRh9L4ZRU9br28yvdmTPwW2YQwVSN9aQiMAoR','testnet':True}
-		)
+		'good': {
+			'btc': (('5KXEpVzjWreTcQoG5hX357s1969MUKNLuSfcszF6yu84kpsNZKb',
+					'KwWr9rDh8KK5TtDa3HLChEvQXNYcUXpwhRFUPc5uSNnMtqNKLFhk'),
+					('93HsQEpH75ibaUJYi3QwwiQxnkW4dUuYFPXZxcbcKds7XrqHkY6',
+					'cMsqcmDYZP1LdKgqRh9L4ZRU9br28yvdmTPwW2YQwVSN9aQiMAoR')),
+			'ltc': (('6udBAGS6B9RfGyvEQDkVDsWy3Kqv9eTULqtEfVkJtTJyHdLvojw',
+					'T7kCSp5E71jzV2zEJW4q5qU1SMB5CSz8D9VByxMBkamv1uM3Jjca'),
+					('936Fd4qs3Zy2ZiYHH7vZ3UpT23KtCAiGiG2xBTkjHo7jE9aWA2f',
+					'cQY3EumdaSNuttvDSUuPdiMYLyw8aVmYfFqxo9kdPuWbJBN4Ny66'))
+		}[g.coin.lower()][bool(g.testnet)]
 	}),
 	}),
 	('PubKey', {
 	('PubKey', {
 		'bad':  ({'arg':1,'compressed':False},{'arg':'F00BAA12','compressed':False},),
 		'bad':  ({'arg':1,'compressed':False},{'arg':'F00BAA12','compressed':False},),
@@ -181,15 +187,24 @@ tests = OrderedDict([
 	}),
 	}),
 	('PrivKey', {
 	('PrivKey', {
 		'bad':  ({'wif':1},),
 		'bad':  ({'wif':1},),
-		'good': (
-			{'wif':'5KXEpVzjWreTcQoG5hX357s1969MUKNLuSfcszF6yu84kpsNZKb',
-			 'ret':'e0aef965b905a2fedf907151df8e0a6bac832aa697801c51f58bd2ecb4fd381c'},
-			{'wif':'KwWr9rDh8KK5TtDa3HLChEvQXNYcUXpwhRFUPc5uSNnMtqNKLFhk',
-			 'ret':'08d0ed83b64b68d56fa064be48e2385060ed205be2b1e63cd56d218038c3a05f'},
-			{'wif':'93HsQEpH75ibaUJYi3QwwiQxnkW4dUuYFPXZxcbcKds7XrqHkY6','testnet':True,
-			 'ret':'e0aef965b905a2fedf907151df8e0a6bac832aa697801c51f58bd2ecb4fd381c'},
-			{'wif':'cMsqcmDYZP1LdKgqRh9L4ZRU9br28yvdmTPwW2YQwVSN9aQiMAoR','testnet':True,
-			'ret':'08d0ed83b64b68d56fa064be48e2385060ed205be2b1e63cd56d218038c3a05f'},
+		'good': ({
+			'btc': (({'wif':'5KXEpVzjWreTcQoG5hX357s1969MUKNLuSfcszF6yu84kpsNZKb',
+					 'ret':'e0aef965b905a2fedf907151df8e0a6bac832aa697801c51f58bd2ecb4fd381c'},
+					{'wif':'KwWr9rDh8KK5TtDa3HLChEvQXNYcUXpwhRFUPc5uSNnMtqNKLFhk',
+					 'ret':'08d0ed83b64b68d56fa064be48e2385060ed205be2b1e63cd56d218038c3a05f'}),
+					({'wif':'93HsQEpH75ibaUJYi3QwwiQxnkW4dUuYFPXZxcbcKds7XrqHkY6',
+					 'ret':'e0aef965b905a2fedf907151df8e0a6bac832aa697801c51f58bd2ecb4fd381c'},
+					{'wif':'cMsqcmDYZP1LdKgqRh9L4ZRU9br28yvdmTPwW2YQwVSN9aQiMAoR',
+					'ret':'08d0ed83b64b68d56fa064be48e2385060ed205be2b1e63cd56d218038c3a05f'})),
+			'ltc': (({'wif':'6ufJhtQQiRYA3w2QvDuXNXuLgPFp15i3HR1Wp8An2mx1JnhhJAh',
+					 'ret':'470a974ffca9fca1299b706b09142077bea3acbab6d6480b87dbba79d5fd279b'},
+					{'wif':'T41Fm7J3mtZLKYPMCLVSFARz4QF8nvSDhLAfW97Ds56Zm9hRJgn8',
+					 'ret':'1c6feab55a4c3b4ad1823d4ecacd1565c64228c01828cf44fb4db1e2d82c3d56'}),
+					({'wif':'92iqzh6NqiKawyB1ronw66YtEHrU4rxRJ5T4aHniZqvuSVZS21f',
+					 'ret':'95b2aa7912550eacdd3844dcc14bee08ce7bc2434ad4858beb136021e945afeb'},
+					{'wif':'cSaJAXBAm9ooHpVJgoxqjDG3AcareFy29Cz8mhnNTRijjv2HLgta',
+					'ret':'94fa8b90c11fea8fb907c9376b919534b0a75b9a9621edf71a78753544b4101c'})),
+				}[g.coin.lower()][bool(g.testnet)],
 			{'s':r32,'compressed':False,'ret':hexlify(r32)},
 			{'s':r32,'compressed':False,'ret':hexlify(r32)},
 			{'s':r32,'compressed':True,'ret':hexlify(r32)}
 			{'s':r32,'compressed':True,'ret':hexlify(r32)}
 		)
 		)

+ 6 - 0
test/ref/99BE60-BCH[106.6789].rawtx

@@ -0,0 +1,6 @@
+8332a9
+BCH MAINNET 99BE60 106.6789 20171025_141647 434
+02000000030a30fe391cc62f8a94a43a51b581cf176a8567431fcb01318cd1005757d0168d0000000000ffffffff0a30fe391cc62f8a94a43a51b581cf176a8567431fcb01318cd1005757d0168d0100000000ffffffff0a30fe391cc62f8a94a43a51b581cf176a8567431fcb01318cd1005757d0168d0200000000ffffffff0400e40b54020000001976a9149316cedacbfcc41e63bdb903b43ae5157654044188ac30051d21000000001976a914b57ca511b3e5b54a7ec936b0fc6b44f595a7cffe88ac48126028090000001976a9142794da6d30fc6adced41313ac3bfab900096b44488ac202cb206000000001976a914cd88336614e1d3dadf2b1e9c5b598e28c8ffcf1288ac00000000
+[{'confs': 1, 'scriptPubKey': '76a9149b1a59d1411e2678a707b371e8e805bbacdde32288ac', 'addr': '1F97Jd89wwmu4ELadesAdGDzg3d8Y6j5iP', 'vout': 0, 'txid': '8d16d0575700d18c3101cb1f4367856a17cf81b5513aa4948a2fc61c39fe300a', 'mmid': '98831F3A:C:1', 'amt': BCHAmt('100'), 'label': u''}, {'confs': 1, 'scriptPubKey': '76a914abe58e1e45f6176910a4c1ac1ee62328d5cc4fd588ac', 'addr': '1GfuYaKHrhdiVybXMGCcjadSgfjvpdt2x9', 'vout': 1, 'txid': '8d16d0575700d18c3101cb1f4367856a17cf81b5513aa4948a2fc61c39fe300a', 'mmid': '98831F3A:L:2', 'amt': BCHAmt('200'), 'label': u''}, {'confs': 1, 'scriptPubKey': '76a914bba3993079ccdf40c9bbbe495473f0b3d2dc5eec88ac', 'addr': '1J79LtWctedRLnMfFNRgzzSFsozQqDeoKD', 'vout': 2, 'txid': '8d16d0575700d18c3101cb1f4367856a17cf81b5513aa4948a2fc61c39fe300a', 'mmid': '98831F3A:L:3', 'amt': BCHAmt('199.9999416'), 'label': u''}]
+[{'is_chg': True, 'addr': '14cHfz8dixc3GL3HFZEbicjinguTkaL1BJ', 'amt': BCHAmt('393.3209044'), 'mmid': '98831F3A:L:4'}, {'mmid': '98831F3A:C:5', 'amt': BCHAmt('5.5555'), 'addr': '1HYcdCFPmWakX2g8mP6ksxDDokDyRbeaAb'}, {'mmid': '98831F3A:C:3', 'amt': BCHAmt('1.1234'), 'addr': '1KjkgipjAyTR8Lc6Xrr8RoeSeiYXaExKHo'}, {'addr': '1EQjevrVa3XWzSnRq7cyc1D7wnNXsHx73x', 'amt': BCHAmt('100')}]
+TvwWgaAnrkQFpAxxjBa4PHvJ8NsJDsurtiv2HuzdnXWjQmY7LHyt6PZn5J7BNtB5VzHtBG7bUosCAMFon8yxUe2mYTZoH9e6dpoAz9E6JDZtUNYz9YnF1Z3jFND1X89RuKAk6YVBrfWseeyHR8vZDdaFzBPK5SPos

+ 6 - 0
test/ref/99BE60-BCH[106.6789].testnet.rawtx

@@ -0,0 +1,6 @@
+a27973
+BCH TESTNET 99BE60 106.6789 20171025_141647 434
+02000000030a30fe391cc62f8a94a43a51b581cf176a8567431fcb01318cd1005757d0168d0000000000ffffffff0a30fe391cc62f8a94a43a51b581cf176a8567431fcb01318cd1005757d0168d0100000000ffffffff0a30fe391cc62f8a94a43a51b581cf176a8567431fcb01318cd1005757d0168d0200000000ffffffff0400e40b54020000001976a9149316cedacbfcc41e63bdb903b43ae5157654044188ac30051d21000000001976a914b57ca511b3e5b54a7ec936b0fc6b44f595a7cffe88ac48126028090000001976a9142794da6d30fc6adced41313ac3bfab900096b44488ac202cb206000000001976a914cd88336614e1d3dadf2b1e9c5b598e28c8ffcf1288ac00000000
+[{'confs': 1, 'addr': 'muf4bgD8kyD9qLpCMDqYTBSKY3DqTWZR92', 'vout': 0, 'label': u'', 'mmid': '98831F3A:C:1', 'txid': '8d16d0575700d18c3101cb1f4367856a17cf81b5513aa4948a2fc61c39fe300a', 'amt': BCHAmt('100'), 'scriptPubKey': '76a9149b1a59d1411e2678a707b371e8e805bbacdde32288ac'}, {'confs': 1, 'addr': 'mwBrqdQGfj4yH6594qAzZVqmYfLdmB1C7W', 'vout': 1, 'label': u'', 'mmid': '98831F3A:L:2', 'txid': '8d16d0575700d18c3101cb1f4367856a17cf81b5513aa4948a2fc61c39fe300a', 'amt': BCHAmt('200'), 'scriptPubKey': '76a914abe58e1e45f6176910a4c1ac1ee62328d5cc4fd588ac'}, {'confs': 1, 'addr': 'mxd6dwbbhg4g7tqGxwQ4pueajob7kjWHBG', 'vout': 2, 'label': u'', 'mmid': '98831F3A:L:3', 'txid': '8d16d0575700d18c3101cb1f4367856a17cf81b5513aa4948a2fc61c39fe300a', 'amt': BCHAmt('199.9999416'), 'scriptPubKey': '76a914bba3993079ccdf40c9bbbe495473f0b3d2dc5eec88ac'}]
+[{'addr': 'mj8Ey3DcXz3J3SWty8CyYXx3egWAapMVMF', 'mmid': '98831F3A:L:4', 'amt': BCHAmt('393.3209044'), 'is_chg': True}, {'mmid': '98831F3A:C:5', 'addr': 'mx4ZvFLNaY21J99kUx58hsRYfjpgPYwpHn', 'amt': BCHAmt('5.5555')}, {'mmid': '98831F3A:C:3', 'addr': 'mzFhymuhyztfuT5iFRpWFirmWi9EWMrybs', 'amt': BCHAmt('1.1234')}, {'addr': 'mtvgwywUP4xmmZG3YgbMRvRSomyEh3ShVb', 'amt': BCHAmt('100')}]
+TvwWgaAnrkQFpAxxjBa4PHvJ8NsJDsurtiv2HuzdnXWjQmY7LHyt6PZn5J7BNtB5VzHtBG7bUosCAMFon8yxUe2mYTZoH9e6dpoAz9E6JDZtUNYz9YnF1Z3jFND1X89RuKAk6YVBrfWseeyHR8vZDdaFzBPK5SPos

+ 6 - 0
test/ref/litecoin/75F455-LTC[106.6789].rawtx

@@ -0,0 +1,6 @@
+2622b7
+LTC MAINNET 75F455 106.6789 20171018_182153 434
+0200000003040e4755ff327ef5edb98aa488a9de7dcd266f4f7faba383aadb502743eb8fc50200000000ffffffff040e4755ff327ef5edb98aa488a9de7dcd266f4f7faba383aadb502743eb8fc50100000000ffffffff040e4755ff327ef5edb98aa488a9de7dcd266f4f7faba383aadb502743eb8fc50000000000ffffffff0400e40b54020000001976a9141e43586462a093ef77a140f7fc33bc9b62b8ea4688ac30051d21000000001976a914f9c129a968a4242f6c29da9cc77dd2fc9408481488ac202cb2060000000017a914f17c92d1a9ca42be1b2bd5293f1e638d457b05848768bda6927d0000001976a914b2da98c692ed20d7d155ca8c13284bf55f7988a988ac00000000
+[{'confs': 1, 'scriptPubKey': '76a914cb4067ff77f8133dc20790c702a994006a2f956c88ac', 'addr': 'LdkebBKVXSs6NNoPJWGM8KciDnL8LhXXjb', 'vout': 2, 'txid': 'c58feb432750dbaa83a3ab7f4f6f26cd7ddea988a48ab9edf57e32ff55470e04', 'mmid': '98831F3A:C:1', 'amt': LTCAmt('100'), 'label': u''}, {'confs': 1, 'scriptPubKey': '76a9142fad2fae2cee09645766da65a5566e9e49dc305688ac', 'addr': 'LPa3VRMf4aYfd85bq2LyN69KfzV6KBJ9gy', 'vout': 1, 'txid': 'c58feb432750dbaa83a3ab7f4f6f26cd7ddea988a48ab9edf57e32ff55470e04', 'mmid': '98831F3A:L:2', 'amt': LTCAmt('200'), 'label': u''}, {'confs': 1, 'scriptPubKey': 'a9143d6021a6448977d93cbff97f884b10506ff625d987', 'addr': 'MDVgcTj9JGFE8xbSwprg7D9zgSRCBzr3CC', 'vout': 0, 'txid': 'c58feb432750dbaa83a3ab7f4f6f26cd7ddea988a48ab9edf57e32ff55470e04', 'mmid': '98831F3A:S:2', 'amt': LTCAmt('5199.99799'), 'label': u''}]
+[{'is_chg': True, 'addr': 'LbXeQjFrukhFJr1nmiUx8UftEQ3DszF3Ud', 'amt': LTCAmt('5393.31313'), 'mmid': '98831F3A:L:4'}, {'mmid': '98831F3A:C:5', 'amt': LTCAmt('5.5555'), 'addr': 'LhzXvAJzRhmn1s8nGSxsajF9wdS6c8AXhA'}, {'mmid': '98831F3A:S:3', 'amt': LTCAmt('1.1234'), 'addr': 'MVv2KBRyRT4Kpf6AokKwZAWmrRvM1DP5K9'}, {'addr': 'LMyyDCZWLnThGrCYcUM4SZdeTBEgV95odE', 'amt': LTCAmt('100')}]
+TvwWgaAnrkQFpAxxjBa4PHvJ8NsJDsurtiv2HuzdnXWjQmY7LHyt6PZn5J7BNtB5VzHtBG7bUosCAMFon8yxUe2mYTZoH9e6dpoAz9E6JDZtUNYz9YnF1Z3jFND1X89RuKAk6YVBrfWseeyHR8vZDdaFzBPK5SPos

+ 6 - 0
test/ref/litecoin/75F455-LTC[106.6789].testnet.rawtx

@@ -0,0 +1,6 @@
+68c9ce
+LTC TESTNET 75F455 106.6789 20171018_182153 434
+0200000003040e4755ff327ef5edb98aa488a9de7dcd266f4f7faba383aadb502743eb8fc50200000000ffffffff040e4755ff327ef5edb98aa488a9de7dcd266f4f7faba383aadb502743eb8fc50100000000ffffffff040e4755ff327ef5edb98aa488a9de7dcd266f4f7faba383aadb502743eb8fc50000000000ffffffff0400e40b54020000001976a9141e43586462a093ef77a140f7fc33bc9b62b8ea4688ac30051d21000000001976a914f9c129a968a4242f6c29da9cc77dd2fc9408481488ac202cb2060000000017a914f17c92d1a9ca42be1b2bd5293f1e638d457b05848768bda6927d0000001976a914b2da98c692ed20d7d155ca8c13284bf55f7988a988ac00000000
+[{'confs': 1, 'addr': 'mz3ed26eFp4HtgaqqwFRgDmGsZZZAHbryP', 'vout': 2, 'label': u'', 'mmid': '98831F3A:C:1', 'txid': 'c58feb432750dbaa83a3ab7f4f6f26cd7ddea988a48ab9edf57e32ff55470e04', 'amt': LTCAmt('100'), 'scriptPubKey': '76a914cb4067ff77f8133dc20790c702a994006a2f956c88ac'}, {'confs': 1, 'addr': 'mjs3XG8onwjs9Rs4NTL3uzHtKmiX5bCH6Y', 'vout': 1, 'label': u'', 'mmid': '98831F3A:L:2', 'txid': 'c58feb432750dbaa83a3ab7f4f6f26cd7ddea988a48ab9edf57e32ff55470e04', 'amt': LTCAmt('200'), 'scriptPubKey': '76a9142fad2fae2cee09645766da65a5566e9e49dc305688ac'}, {'confs': 1, 'addr': 'QSCWVL7SyhxEgRi99BXDzDLHiUUjn5cGSJ', 'vout': 0, 'label': u'', 'mmid': '98831F3A:S:2', 'txid': 'c58feb432750dbaa83a3ab7f4f6f26cd7ddea988a48ab9edf57e32ff55470e04', 'amt': LTCAmt('5199.99799'), 'scriptPubKey': 'a9143d6021a6448977d93cbff97f884b10506ff625d987'}]
+[{'addr': 'mwpeSa31e7tSq9oFK9U2gNpStBGeeKfNC4', 'mmid': '98831F3A:L:4', 'amt': LTCAmt('5393.31313'), 'is_chg': True}, {'mmid': '98831F3A:C:5', 'addr': 'n4HXx169A4xyYAvEoswx8dPibQfXPpjAMT', 'amt': LTCAmt('5.5555')}, {'mmid': '98831F3A:S:3', 'addr': 'QicrC3pH6tmLN8Cs16zVSAh4tTytgRSCTZ', 'amt': LTCAmt('1.1234')}, {'addr': 'miGyF3Lf59eto9z19uL8zTnD6xU7Kbiumg', 'amt': LTCAmt('100')}]
+TvwWgaAnrkQFpAxxjBa4PHvJ8NsJDsurtiv2HuzdnXWjQmY7LHyt6PZn5J7BNtB5VzHtBG7bUosCAMFon8yxUe2mYTZoH9e6dpoAz9E6JDZtUNYz9YnF1Z3jFND1X89RuKAk6YVBrfWseeyHR8vZDdaFzBPK5SPos

+ 19 - 0
test/ref/litecoin/98831F3A-LTC-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-LTC-S[1,31-33,500-501,1010-1011]: 63DF E42A 0827 21C3
+# Record this value to a secure location.
+98831F3A LTC:SEGWIT {
+  1     MQrY3vEbqKMBgegXrSaR93R2HoTDE5bKrY
+  31    MAEgtjR8MLZpLMqPaBjjas1HM3Rri119Uq
+  32    MDdv4ofTTdTQXZLFkBkgt2aotrdytaChyK
+  33    MAbECS5tZdVMQ5Fc3U3zW1ZLLvY7bY3mJU
+  500   MH7eBQ6tSzMWL41m9T2h96H3RaVpXYRLDF
+  501   MG6NdJs9iQkV9TLWTy4wahNXK8pMSkpFve
+  1010  MEzfeSh1kBmSDKAKQZoEm6293RCdFunhP3
+  1011  MBVCP3kQEZr2NwBQWHY6dP72sYkh9rRmWt
+}

+ 19 - 0
test/ref/litecoin/98831F3A-LTC-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-LTC-S[1,31-33,500-501,1010-1011]: 1A3F 3016 2E2B F33A
+# Record this value to a secure location.
+98831F3A LTC:SEGWIT {
+  1     QdZMvncuWm4CE7oE3oEy23bKKqWktQ4ySr
+  31    QNwWmboS2nGpspx5mYQHTsBaP5VQPqGVP5
+  32    QSLjwg3m95AR52SwwYREm2m6vthXbvMewN
+  33    QPJ45JUCF5CMwYNJEpiYP1jdNxbfFNXQ2Z
+  500   QVpU4GVC8S4WsX8TLohF26TLTcZNB5qxLw
+  501   QUoCWBFTPrTVgvTCfKjVThYpMAsuDqcQ4m
+  1010  QThVXK5KRdUSknH1bvTne6CS5TGAqpTbR2
+  1011  QQC2Fv8hv1Z2vQJ6heCeWPHKuapErrdaQs
+}

+ 19 - 0
test/ref/litecoin/98831F3A-LTC[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-LTC[1,31-33,500-501,1010-1011]: AD52 C3FE 8924 AAF0
+# Record this value to a secure location.
+98831F3A LTC {
+  1     LMxB474SVfxeYdqxNrM1WZDZMnifteSMv1
+  31    LhpYfXVizZFun9RRrqTdmiAie2Q5YqaRRm
+  32    LYvg2DDKxwZ6FBb6XcpsSK14swAMFLzH1h
+  33    LSkSht1wZs38gichnoznmXaMw4apJUDPMG
+  500   LWNJGQVj3MBWwVFJV6tGZUhZpPTUUxJJJi
+  501   LN6qgkswJRzbJzc4xz5EKNUHVzhFiXR8B4
+  1010  LKQhgeRuhz8ULXSnh5ctkroJocGDrXkQau
+  1011  LfCFQM5qHS324enT5eYXvPrv8UKKV4yT1A
+}

BIN
test/ref/litecoin/98831F3A-LTC[1,31-33,500-501,1010-1011].akeys.mmenc


+ 19 - 0
test/ref/litecoin/98831F3A-LTC[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-LTC[1,31-33,500-501,1010-1011]: 5738 5C4F 167C F9AE
+# Record this value to a secure location.
+98831F3A LTC {
+  1     miFB5wqbE39r4wdQvHL64TN81Zx6kRnocA
+  31    n47YhNGsivT7JTCtQGSiKcKHHodWFSFtnK
+  32    muDg43zUhJkHmVNZ53owzD9dXiPn2FEJN9
+  33    mo3Sjio6JEELD2QALEysKRivaqpF53jWfu
+  500   mrfJJFGsmiNiTo2m2XsM7Nr8UAguLqz6Rk
+  501   miPqibf62oBnqJPXWR4JsGcr9mvgSoF8hc
+  1010  mfhhiVD4SMKfrqEFEWbyJkwsTPVegXra11
+  1011  n1VFSBrz1oEDaxZud5XcUJ1UnFYkMScbVj
+}

BIN
test/ref/litecoin/98831F3A-LTC[1,31-33,500-501,1010-1011].testnet.akeys.mmenc


+ 109 - 0
test/ref/litecoin/ltcwallet-testnet.dump

@@ -0,0 +1,109 @@
+# Wallet dump created by Litecoin v0.15.0.1-ba8ed3a
+# * Created on 2017-10-09T12:59:12Z
+# * Best block at time of backup was 0 (4966625a4b2851d9fdee139e56211a0d88575f59ed816ff5e6a63deb4e3e29a0),
+#   mined on 2017-02-13T01:29:26Z
+
+# extended private masterkey: tprv8ZgxMBicQKsPf1UGSRxdjZvEHMWSWDDeg6b5amVfK6fh4ZPh42poHYokt7MWrkXNK6Wi9EMti4qxaCa5e9Sxy7qRnTM6dxqhk2KBpJ9ENmy
+
+cTfSojVNo6NkKNenUxGt5JH2DRawPE76Y38kmHZmoeQ5vN18aZZF 2017-10-06T18:38:08Z reserve=1 # addr=mfX1U3pcHtpHtuj34be5YZe56DPsXBLdur hdkeypath=m/0'/0'/222'
+cRnMYZCrfvuE68kHhsB4x2pPd4E6vUaZd1HMWPjioCXb5ZhV22ue 2017-10-06T18:38:08Z reserve=1 # addr=mfYWSEdAJcPd7n2R8y2HPzDT1XADTTNGht hdkeypath=m/0'/0'/257'
+cMsvBz2sARkgEDwTRhGZx9io1zKkFtRyYrurWPzbyMQxyqFkSYYs 2017-10-06T18:38:08Z reserve=1 # addr=mfZqvi6LYB4QcVConShKuj8NAPsnd2ES6B hdkeypath=m/0'/0'/696'
+cVweKrF9XGW4gVWrpZeZzL33NvuWnczMiHgAxcA717VXfe4HdxRd 2017-10-06T18:38:08Z reserve=1 # addr=mfbP1YxYbwnX3NkzspMy5U6YahjLhjWhaa hdkeypath=m/0'/0'/396'
+cMyeJtrVqyJXoU8GJnwuuSGFacUC5Gn3EKaA5Wc9wdrowvgiWpxS 2017-10-06T18:38:08Z reserve=1 # addr=mfeErXpgjYZJ4PTKpgiAYRCCQU1sDT66Rg hdkeypath=m/0'/0'/895'
+cTfU4XoRLthDPKb6ZXSXVPyWqwA3MVRt8RpUdF4pQF7oogivq3hp 2017-10-06T18:38:08Z reserve=1 # addr=mfm3cgPQ6tWwWWvube8efobFD1xupv6zZq hdkeypath=m/0'/0'/526'
+cPf63HwQ9BUPP8JXGAQvr7y7VfjuSQWRKGPuXPZB2Qc9GmAvkkLT 2017-10-06T18:38:08Z reserve=1 # addr=mfm9T6jghZjE3hvbujA6ARWYg4fFaRPTiT hdkeypath=m/0'/0'/675'
+cUJVqBZB9GGCkfJP33VmJvNCf6e17RNLxajFjdkwZnHEfkm6NZbt 2017-10-06T18:38:08Z reserve=1 # addr=mfmvf6PtusLPsi7j3rLVrLcEnSEtbXHdNf hdkeypath=m/0'/0'/424'
+cVLXQhVMGATwSuapySzdmNVriHqFk3vMt2aQo58Xyh7cUDvtR2nr 2017-10-06T18:38:08Z reserve=1 # addr=mfnBqHA4N7YZ7UqhJPu8uioGvMuYqkNGkv hdkeypath=m/0'/0'/89'
+cQvLdkqaxwAjtLtiw473HaMtk5zBrMPMevxs2YiZTB6LAPGdFnZB 2017-10-06T18:38:08Z reserve=1 # addr=mfnKMmMvScJyiotN15GZvVxjpEzv22fVLR hdkeypath=m/0'/0'/62'
+cVkEz2t3uT7f3vjcCvouwyVfqigKuP75oZjskaQFNXCK5pzsyQqs 2017-10-06T18:38:08Z reserve=1 # addr=mfo9qxjcoyTCjiFxQQozj9vdwksRLA9YgG hdkeypath=m/0'/0'/244'
+cT9mkrNuWNYuqkR9D4crVRidCv4NRw6WXtRwi8i39fZ1fYeuDPvC 2017-10-06T18:38:08Z reserve=1 # addr=mfoZyg1fJhgfRafjHYXgsYxJ7WUXPTER5E hdkeypath=m/0'/0'/24'
+cT2Kky9Cz8d2pLG9GNQskyMWckLWhEeFe2tqsDxs5wD7rUSddp6N 2017-10-06T18:38:08Z reserve=1 # addr=mfpw3EYUF1ru8iUoTkdx5J7qoAsyvqRawA hdkeypath=m/0'/0'/554'
+cNiR4SJ12Jxu4nucsQEvkrwFLb5K1Fbe5vKTBEHXfJChc2hwwxHH 2017-10-06T18:38:08Z reserve=1 # addr=mg139CQzWLQyffUrY7hLKxfaaXGhZdqmAG hdkeypath=m/0'/0'/68'
+cNetpXFz3EUU3BoGGnY4Kh6Ue6ZfMYWmZ8D5c7gzustozqSupa2L 2017-10-06T18:38:08Z reserve=1 # addr=mg5EFMwUNwAZr1ndMBcyR5eJfetmVpBdJe hdkeypath=m/0'/0'/51'
+cT4ckbsTWrLr175yKKTwHxJZPvHEBbdcGXckLBcwS3N1fGoLoM4v 2017-10-06T18:38:08Z reserve=1 # addr=mg5yt9VSeeXC7iQUibKrmxtoYmLh12GSjZ hdkeypath=m/0'/0'/629'
+cUb6YbBcqLJ3ddd2duuEV7bX3VmwoFTsELLe7xCyXTeDMYEkBYj1 2017-10-06T18:38:08Z reserve=1 # addr=mg6fGXUDetRAWqGg2L1ZatRTcMJ9WSotRq hdkeypath=m/0'/0'/378'
+cS28GCbKgyJcWqj5Buh5nkVHZnD38Bqzi7NZHDVJuPTH9nhdFMjx 2017-10-06T18:38:08Z reserve=1 # addr=mg6z4QrUdoBWAmy8UzNeHvEBnaRhF8g6p6 hdkeypath=m/0'/0'/226'
+cNdZNpbWsSa7BavGfNHidcpMrzTouuDw2Wtg9VnFSrXHvjNHKipw 2017-10-06T18:38:08Z reserve=1 # addr=mg7MFPaR9nsi3BUP4doURSYeN5TcxgsXRN hdkeypath=m/0'/0'/274'
+cNRzekgY99TzDimQf2W5T3AMSLRg4nyLeU3GFah25wUk191yc9Gk 2017-10-06T18:38:08Z reserve=1 # addr=mg7aup5gctnZRPyeihXWRzXWyPycHQ4b5Y hdkeypath=m/0'/0'/216'
+cRMiqxj56cnzxKDYt4knEtPqQdzU6NjFrTWeZsBYT8sHyhMvRSvB 2017-10-06T18:38:08Z reserve=1 # addr=mg7bG1LV4mBf6x1RqFwUSb6qvgt1NP8LbL hdkeypath=m/0'/0'/504'
+cQBNBhFRct6SZdjp7PoncfxEFhQthfJ9sYEG6u7squXNJ1t4h9SY 2017-10-06T18:38:08Z reserve=1 # addr=mgAMBGtbgFCkfa2dW7mxHGun4oR1BvPnDF hdkeypath=m/0'/0'/3'
+cMssEYDdr42pyjqgeb6uKkQFJ6LaKH9W2LiCPwUCRx3AztkdgVuk 2017-10-06T18:38:08Z reserve=1 # addr=mgAoszngNFWxVQHL79RKtPzEuDuRVrFRnu hdkeypath=m/0'/0'/176'
+cQUvFSm37naPrTVPJ1LJCVgQ1Eyh1WMktCZDaQSsAnkZ1D6gMJL1 2017-10-06T18:38:08Z reserve=1 # addr=mgDTVN4XfzYLFVfDua5BLLNzuSL73RYQkz hdkeypath=m/0'/0'/259'
+cNS2cyU8bsaB7jnj48GJEepVtdSTEWKCirwCpCLYhL7mFCUGqjan 2017-10-06T18:38:08Z reserve=1 # addr=mgEik2XWL8xUudKnReZwXBka8CpHeLw6qm hdkeypath=m/0'/0'/307'
+cSkjShf25EqD3cokhXLrnkgtshahs7EciMrei7UbYS7U15BepMRr 2017-10-06T18:38:08Z reserve=1 # addr=mgFL1XW5Qw3CTZoveqPafDUePJC6crVwmU hdkeypath=m/0'/0'/497'
+cNuPKwwF3zH4SfTkfzdg7AJ7wkmMYqNjPyX239dFCkWvBkGU9gmH 2017-10-06T18:38:08Z reserve=1 # addr=mgLKcw7yCR3A7jNtFMY4po9pwur24N5BhQ hdkeypath=m/0'/0'/255'
+cRpdQrXDm1XzznY6TiN7SByjnMua5YpzNfvHakcP85bU9MTBVayF 2017-10-06T18:38:08Z reserve=1 # addr=mgLvpJqR3VqwoGSA3GPDZs781PVWUNtCcX hdkeypath=m/0'/0'/109'
+cUML63DH1xCELDRkVrhozLjbj53QpJQfaiPU5U6ixmq8mWMJLgBP 2017-10-06T18:38:08Z reserve=1 # addr=mgLzzWz5U9FKNZkTiSDCf4yDpF5kpVJuBy hdkeypath=m/0'/0'/552'
+cSNToDVRd327zvzX95kxMUUdcBtAhJqVD7RqNTzNfpXZsFUkzmUf 2017-10-06T18:38:08Z reserve=1 # addr=mgN357wUpraQcwt6WueoMCb6tgmfiWEB5o hdkeypath=m/0'/0'/791'
+cVkcpMaEgCAKhdfuFuVHVEcqFT8cfaX2XNt8SyfPpCg13mrS5R5c 2017-10-06T18:38:08Z reserve=1 # addr=mgPPDAmEkEYfCkWbKEFMbTjpLqRwAtwMgP hdkeypath=m/0'/0'/32'
+cW6P7nFmP3J2Eznjx58SoDWMHRh1kAgPYdyaue9f9fbGVbrhwwty 2017-10-06T18:38:08Z reserve=1 # addr=mgQHtQ56kN6gebZdPDMMxqUFRxUwthb2fE hdkeypath=m/0'/0'/581'
+cUn8WeGVJK9NQzGbUZtYcNpyxYx9W1uCFwCqtFtY4bY3UisPMPE7 2017-10-06T18:38:08Z reserve=1 # addr=mgRasyyxCWi1EncuL9gcrQstGd4W3EbS1u hdkeypath=m/0'/0'/144'
+cNZQsndeUzGnDEnu8HpLViBiXhsbNFDyoTszgLhF1YmUnKEXdZ8g 2017-10-06T18:38:08Z reserve=1 # addr=mgRoMKgApf8EasvZ6JYyihqw8d3jcxFvuU hdkeypath=m/0'/0'/444'
+cMw5zMQiJN3pvLVEJjm2z72juXRA6jmkNEnnbroZ95FJyi1QX1Xh 2017-10-06T18:38:08Z reserve=1 # addr=mgSiW4vzbdFuiFDL4rQUcDwQoLByMfgNGT hdkeypath=m/0'/0'/301'
+cTKXALJSvLCCqmzWzmHpzPz6EJRynPbEu63BBJiVxZBLWFZs5Pxi 2017-10-06T18:38:08Z reserve=1 # addr=mgTPcsJ83L1htMUbQfBSXfSw8Vx5ZrP6TG hdkeypath=m/0'/0'/94'
+cVkFPnZeCVo4zBuZnpCDqug5YQkBN3bEmof9PxDLY4fXcj2M8H2G 2017-10-06T18:38:08Z reserve=1 # addr=mgTR6hV2CSQbTzAvrqUym9j7zNQkiLrHpj hdkeypath=m/0'/0'/230'
+cUZBhYFc93Y7CicHYBsmdMR451CVoE8t8KeYCavQugVzdkgZsw7V 2017-10-06T18:38:08Z reserve=1 # addr=mgTks6eVsZU4zfy3TW2i7sVM78dozmrrLC hdkeypath=m/0'/0'/100'
+cV6CH598xFZNL5vkmKcTXQHkMXCSHU69TdFafjsXJCTrs8Jj27s5 2017-10-06T18:38:08Z reserve=1 # addr=mgTof2bXaavwDMQjcenpa8ofzYRijT8rw9 hdkeypath=m/0'/0'/398'
+cVBetPPSNFjfG32rJt9SPfm5cuZ9fphrxMZW9NBKqt8RbECypTN1 2017-10-06T18:38:08Z reserve=1 # addr=mgUNCLtSsF622XiSw791NtH8ujKkMxJaEd hdkeypath=m/0'/0'/795'
+cT16XS9Pz982p5xUsogbSTp2x6wVcXBVPeRm5HDHk7oyurRu2zER 2017-10-06T18:38:08Z reserve=1 # addr=mgXeNtXr1iJTycXZMoePT76C1Q35fWMZ7k hdkeypath=m/0'/0'/618'
+cPLNphAvsS8HGgr5XA3wnRiQWP4DCbsGs5wrS6g8nTiNJoi6Yamk 2017-10-06T18:38:08Z reserve=1 # addr=mgXmaePXbdEY21MXP6ftmsqt1Sz3tP2aM7 hdkeypath=m/0'/0'/73'
+cVEv6LGsWcQn3JV7KMJhSK855JuT76jxqzn76R7HUZNwxtnBuZjk 2017-10-06T18:38:08Z reserve=1 # addr=mgZyswAnDxVEHn6Jo344cDGEDYUEU58QTg hdkeypath=m/0'/0'/468'
+cSmqnQAHd8kMjG89e2LyNyfa2ro7ofrEYfHoC9i3oPS2z8wXWAFo 2017-10-06T18:38:08Z reserve=1 # addr=mgb1Uxy9LXmjM4rCtpN4uMDxeZ5gEauWw8 hdkeypath=m/0'/0'/651'
+cQbG6taFvNatqKDeHB8e1MkG9Rq2iGR2yLC5ZnkMbXEmacbj6AJf 2017-10-06T18:38:08Z reserve=1 # addr=mghNiS5QnBh739KMdWB1cC2VoAN7RXh7Hw hdkeypath=m/0'/0'/239'
+cQ5jRv7wPFPwzzKSb4SNRSrMetQPpWBkuZ52At7xUuThfJuWoexC 2017-10-06T18:38:08Z reserve=1 # addr=mgpjL9oV3vRTNudLEjAArxVMVNW5oXY5tQ hdkeypath=m/0'/0'/796'
+cRPaKxxu6LfNg6p45c7BosyxTLN2PZiNY2rSEocrPAJ4jty8yMxW 2017-10-06T18:38:08Z reserve=1 # addr=mgqSg9PqoKD476N89jFGxxjNj9vPUN5hqC hdkeypath=m/0'/0'/628'
+cNbja8Pbp5exTeNrr37zC5RVLB5UK4XyY842sj6jHRuZiqeRBMLx 2017-10-06T18:38:08Z reserve=1 # addr=mgqeREKjbw3fyAioCcAtuh6GVGnGtVtAiW hdkeypath=m/0'/0'/102'
+cPV7eRmWs6VSUXLEX92xTCQxn8hJVCTgxa1CJcwF35E6GPrzkMY5 2017-10-06T18:38:08Z reserve=1 # addr=mgrfRRfV2dQ2miVrHGRokzEdbsxfCCoM34 hdkeypath=m/0'/0'/291'
+cRtAm5Qk7iqK78UPfLcndSCf338H89MCu4KAMVjyKLwSzf6TGxeu 2017-10-06T18:38:08Z reserve=1 # addr=mgrgHvL4DHYxaQvuptFr4bxSAUBaY9kAJw hdkeypath=m/0'/0'/260'
+cVbVLSiixfo8xz4UMUmX1G1Hrrb8USj5RAVRoiPrk9GNf4yV1utB 2017-10-06T18:38:08Z reserve=1 # addr=mgrzZEa5nMioBknjxYryUthz4rMmNZ4MWn hdkeypath=m/0'/0'/192'
+cQYc2XGRUYQikJay6k4pxGEsQ929TGdgNYNepaMK7NgK2os1vg7Z 2017-10-06T18:38:08Z reserve=1 # addr=mgvtmtwvyQTn4T8xoYPZL2TPTbGsWLeqjb hdkeypath=m/0'/0'/833'
+cS36V4Tp94TWmg7B7bf9Znqyw2HebE2HEo98CR4nfpvGvFo3sUqn 2017-10-06T18:38:08Z reserve=1 # addr=mgxzJhL5ugWTyGnW5PAJJfT2TNtCK1PLpd hdkeypath=m/0'/0'/406'
+cNGDfMkijnFLRQigTmtBvu2fGReQTHopNr2589BouR5gamDhFZjt 2017-10-06T18:38:08Z reserve=1 # addr=mgz8LY4EMFL1hVJaw86Fi17LZVcsfQetLq hdkeypath=m/0'/0'/543'
+cRrToL4Z2s8VRJtnuHgxWePWA1usRqFpTzjKPbBy85q9n4kGC1wt 2017-10-06T18:38:08Z reserve=1 # addr=mgzv9sEw8ySEJRnSRBhad6JkjqrKdqbVEj hdkeypath=m/0'/0'/502'
+cSZbkJGiskATiYif3g8n953ZTdnhPMYxE8NXBykEvVKfu76oV9PW 2017-10-06T18:38:08Z reserve=1 # addr=mh4RFcdrVMDd1crNHXPmQYG3VveonRoUcq hdkeypath=m/0'/0'/806'
+cVqnXBMF762WgzQjJd6CHeaL2GsTi5kpDPiRSzSJ2NkPMGXJZ9Rc 2017-10-06T18:38:08Z reserve=1 # addr=mh5DjZxLMmcpGjDym64LMsGdrAMn6NQ5MP hdkeypath=m/0'/0'/886'
+cVHXSMkEFQghd69Zi4zkzfvnC82G5mT4kGHrVb97UD3s2EjE6E4J 2017-10-06T18:38:08Z reserve=1 # addr=mh5npc3d29qnYcrkA7iNUvTmptSZ6YcHGU hdkeypath=m/0'/0'/268'
+cSq5mnLWPWYYzbtSXYf5tU2aBkhpiBnHYARyxsG3rbBL4dfy1BzE 2017-10-06T18:38:08Z reserve=1 # addr=mh65LQWgKi86kFgjezT779DbPjhZpvHoBM hdkeypath=m/0'/0'/344'
+cPfMmxPGbheNSJXz6DaExD6Htdtsd6Asw2wXS4bW2rrrTz3kJjwb 2017-10-06T18:38:08Z reserve=1 # addr=mh6wJ8cVhYFjx5xZ63UKPcreBRQFrFS9Ti hdkeypath=m/0'/0'/859'
+cV8ZUQERgfoCg82Uawhx2V1EWQ6gEbcvqinRSb9G8tLDdyQXzZEp 2017-10-06T18:38:08Z reserve=1 # addr=mh9F16b4aK4m3QB3P42osopoBxoT1MZpoU hdkeypath=m/0'/0'/107'
+cThVcaScHownY9Rp1uePnMpcVYpNsng4ohWGtsuHv3puR6MbMqPA 2017-10-06T18:38:08Z reserve=1 # addr=mhBhm6wR3JcCtaNwswjzCfBkuokiyjiXKv hdkeypath=m/0'/0'/262'
+cN5Krz8kVvz5WpKoZPCX2wZwYq6NvathWEKP8jGLf4gkg5cHmwHV 2017-10-06T18:38:08Z reserve=1 # addr=mhCLaBXb7ga5NDoo4dj4oSBS5wgFQ5uRtH hdkeypath=m/0'/0'/16'
+cUSXt2i38XpXSGoeuvvXPK6LqDyCo2Yh6sCPPZpzpyxVwK2dWgb2 2017-10-06T18:38:08Z reserve=1 # addr=mhCZPJhPjrsyw7JfomRfHaVX43bqAcnLxw hdkeypath=m/0'/0'/225'
+cUV4az3NxVWKxyfowZh2BSRymZMSBrAqNgHuJK7kLN6EKJUNYmkh 2017-10-06T18:38:08Z reserve=1 # addr=mhDabQN6XUNtvrvUBubLqhaAwxu56eZSFo hdkeypath=m/0'/0'/678'
+cRVbaPoEBq6NW5v4kHPX89N3249gDbWE4C4qFcszDrpSca5Lp1Hp 2017-10-06T18:38:08Z reserve=1 # addr=mhEmCCXdGbZk1htCuWA4gHK3DRYeNL3aZZ hdkeypath=m/0'/0'/735'
+cSqU9gdqdHVveyG29aYU8EYactfzkw4KyJDn6BRomnyXHzLGb7VE 2017-10-06T18:38:08Z reserve=1 # addr=mhF2kyuEDfPoPpo4JCbnuCfz3bkSPjS2yh hdkeypath=m/0'/0'/152'
+cTBKBgJa3MwchdNdBEXw6enFcPtgcSssqHxgaurL2cz1UbmhkVUb 2017-10-06T18:38:08Z reserve=1 # addr=mhGzk78bgBdTkEU7DqctoaqkR52a593tgo hdkeypath=m/0'/0'/60'
+cT3AzXdCyioPULgK4J95FLNdN9zFEtCw9ZucukUXFbcDHrvWrfv7 2017-10-06T18:38:08Z reserve=1 # addr=mhHERGnyumYMrfEFEb75H1fYbeQmbKvS4Y hdkeypath=m/0'/0'/637'
+cVra8SwAPA6qA7QuSBDCC1SL4fjpmgsomBMAG9uHVbzLTgHJKeiP 2017-10-06T18:38:08Z reserve=1 # addr=mhJDpv4hYqorcrcc4NTXPv9oyMgTU2KbZz hdkeypath=m/0'/0'/715'
+cSN6aRqxSWyDxDasH8Jzb5nd3UbKG4SLyEv5yENaz3ozNiFYbNxG 2017-10-06T18:38:08Z reserve=1 # addr=mhKdKUezYQQsDguahbe35KN5KPKMAExQAQ hdkeypath=m/0'/0'/300'
+cToYTpje3AQt3Z4k2LcUZo5cuREXhvH3strSNdKrjvqT7P6DqjN4 2017-10-06T18:38:08Z reserve=1 # addr=mhMjLs3vJWFvaQHktyGaMQEaGj6vo8kjkn hdkeypath=m/0'/0'/822'
+cPq6pTEGXmUXJ6gC2ioFnheVgYj5KrztN1J2ysnxGa2aCciCU3Ju 2017-10-06T18:38:08Z reserve=1 # addr=mhNLzmDKqkCpj6rBXk4yrBPw9jnXCSQicw hdkeypath=m/0'/0'/149'
+cNWTdjiujRKX48SNQAfvM66vcmifXtKjiD8ZdzKDCz1Nxkj3tCDA 2017-10-06T18:38:08Z reserve=1 # addr=mhPcRatBAatGxRs2eahHtNujMzKZKtmRWW hdkeypath=m/0'/0'/507'
+cUYZc66fy718SkyUpaADt4xUnT7qkv4WEoC7idQ8xx9SdFvx8sir 2017-10-06T18:38:08Z reserve=1 # addr=mhQoDtUq5fLd3GnEgv9im7QaJUwQ7rHUx3 hdkeypath=m/0'/0'/303'
+cRPstjfALMgyNJNPiC1APxuiEG22e5SwsVATp72sd3xqEtyqgrYT 2017-10-06T18:38:08Z reserve=1 # addr=mhWnFMD2MF4zqpdJA8L6pAfEu42kGc76Fe hdkeypath=m/0'/0'/39'
+cVz8YtybDe28bZtdmify2HJ6AYabZasPUVsyQ7XYXkCmjReCEBzn 2017-10-06T18:38:08Z reserve=1 # addr=mhX4puoMkZDpu5SH1sLBPqvhNNHeUausG1 hdkeypath=m/0'/0'/508'
+cQjzB7kepSdGJ9qjS6QJB6NL2zhqJQ7KeWH59ymqWWcmwz81DAEM 2017-10-06T18:38:08Z reserve=1 # addr=mhaPZQVYJR4QQsvBThZgucCiscrcMGfdZZ hdkeypath=m/0'/0'/288'
+cPy6grSXRUAQtuT8PHU6TjegwEPjFcKMejiFWTwJNdXMeQT2b7LX 2017-10-06T18:38:08Z reserve=1 # addr=mhbHymfYiJfw8PRckLzyG9D9wbYpbkbadM hdkeypath=m/0'/0'/888'
+cQRDMtRJEDZp3T2cecrvp3PczTC2MiwhJysYixAtmFBm9BhobZs6 2017-10-06T18:38:08Z reserve=1 # addr=mhbyBgjjcsLzqr18aLnm4SJLB3RJPkqLxP hdkeypath=m/0'/0'/34'
+cQRCUYtuxgySsj4rrzK1Cbv5jARsUktopuiGwQZP3cANTWWhRY4q 2017-10-06T18:38:08Z reserve=1 # addr=mhc7mwmKcFEmFutEL6KVvKfAY85H8ab9Rc hdkeypath=m/0'/0'/131'
+cTu1b1ArNMmRN6N1oP3iuSw8xBSseJ7VwL7yXny1kQ8T4HbfD7KY 2017-10-06T18:38:08Z reserve=1 # addr=mhdLJ9Y3R4FRfEXhLsjsHwU8NYC28JegYD hdkeypath=m/0'/0'/208'
+cMcWpk8kzzo3p89DfHLdnJbbU5K3ngsHhLPKmCDP28nsaHZ7rQJ9 2017-10-06T18:38:08Z reserve=1 # addr=mhdr9ExNJ1vSXHLxn46S7uoGPU3RB2D68G hdkeypath=m/0'/0'/429'
+cUhpH1YLBhbcfKmPTJ1mVq32MPiDQFa62b3rCYVGK8VaJzrD6hi6 2017-10-06T18:38:08Z reserve=1 # addr=mhgjf6iAtzEMd7mGEzPa3TWdBxGY8bEbG7 hdkeypath=m/0'/0'/707'
+cMsEWFzfteWraya6Da3RruDXeEdEZZkdoFhmxH4TpSNtVa9fuH9J 2017-10-06T18:38:08Z reserve=1 # addr=mhhr6KADc9oj8m4FgqJbaAe728meyX1BBz hdkeypath=m/0'/0'/431'
+cT1E4UEpASEpCrrRAZeuLZog7vjd9TyMxmau6SnWM6thTKwiTsYG 2017-10-06T18:38:08Z reserve=1 # addr=mhk2TwttzeZEtLZhAn54NkvdoEuZX9AKGW hdkeypath=m/0'/0'/813'
+cNkfBu9SobpreBjurtYb4fbbMxJmW6MmBB5H5KETio7AmFtHEzNQ 2017-10-06T18:38:08Z reserve=1 # addr=mhkRZiANGD4EqQVh97YAPUmd8AqGaJ5GPM hdkeypath=m/0'/0'/708'
+cTF7WKSXPR6WwNVGJRJyWadZyMVyT2spJejkYxk52HEsfsvjg9Sc 2017-10-06T18:38:08Z reserve=1 # addr=mhnjzKBkV42cfAWUFAjjcPnPHymTTBsR3g hdkeypath=m/0'/0'/283'
+cT43AQFZFdk9BFAw5aNc4DkoKREeiX2Adj4ji3NcicFJvHNfAPkB 2017-10-06T18:38:08Z reserve=1 # addr=mhp2mWKm6g4K1b86mS5uBfcnHmnm85dBn8 hdkeypath=m/0'/0'/769'
+cPR8QL82wnnbW5g7myRyoqxK9vorVNVh8xNmrYaBjFssUVWPErQo 2017-10-06T18:38:08Z reserve=1 # addr=mhq2wZqY3xpTAcsuzGbyTR2scqCfJQ26Wa hdkeypath=m/0'/0'/538'
+cSCf7a6e5uB9139xuEsEm9gqy31yx21z6XYoQaRRmocUDNaHU8Rh 2017-10-06T18:38:08Z reserve=1 # addr=mhtqcYx5s499XB6HNkPCQxtpHid1SixEUD hdkeypath=m/0'/0'/133'
+cRNUNFn4YEZL68nWrheHzu5tjYVgRLtJCYYb6M8vfq6SEqvtkixd 2017-10-06T18:38:08Z reserve=1 # addr=mhvR3xfh97yAsVYVEpqVTVkNwWLpTJvPsi hdkeypath=m/0'/0'/824'
+cS7fG5HSkrdmmEnxakNpqBHh474315cqTq5S9niLNQksV9fVPceQ 2017-10-06T18:38:08Z reserve=1 # addr=mhwSjr4NQBwYA1USCa6bjgjx4tAYafQUrM hdkeypath=m/0'/0'/302'
+cNzBs84ZsLXjadLaXQhWogYXAD4aR4GoEwmtEoCt4fvhGLg48JBe 2017-10-06T18:38:08Z reserve=1 # addr=mhxASedzwYXLu92G3cxrRTeMibWFYTjMt4 hdkeypath=m/0'/0'/442'
+cVqDxQzEKngKSdhrMXN2jdwVdXHVfmRrEPUaMVoHpSsHHtAu9hDq 2017-10-06T18:38:08Z reserve=1 # addr=mhxRUM62F1fGHbf6jNfq7EDF6u5Wh2nTGt hdkeypath=m/0'/0'/511'
+cV5KziDW15nPKNjrVvKcL5TBhBvUmfMAwxdLDwc3Nsoxb13aakBD 2017-10-06T18:38:08Z reserve=1 # addr=mi31DZm2CcRVaWDZvcfj5KCWiv1RNJfx4g hdkeypath=m/0'/0'/47'
+cQMvLZHkWVa6pKJ1RNf1N32gEg4LAMcz7cQ3GGeFXfcXvmUUYDna 2017-10-06T18:38:08Z reserve=1 # addr=mi3ZmmmRYDDvsTqFqqB3Yyqd3uCKb7BoBe hdkeypath=m/0'/0'/191'
+cVQUJrZa3W8XxAuYJ9F5ogRNMqX4Npp4CZ9oN819uEQe1dTPn64B 2017-10-06T18:38:08Z reserve=1 # addr=mi444dExNAN4ept7QLiepe48kBvfEpCw8r hdkeypath=m/0'/0'/551'
+cRZu1UY6QYhwUJXEvqXRCLf82cZ2mPMEbwpAdyarpFEm7y7bqERu 2017-10-06T18:38:08Z reserve=1 # addr=mi6zJtwDmAaKTibtxjxrucTTVppT6DSM5B hdkeypath=m/0'/0'/584'
+cPZN61KZaycb34VoQdfhbdMt8XkEDw7asTNruDUfJXk5TkC8vUdA 2017-10-06T18:38:08Z reserve=1 # addr=mi7MmAZMdvT9vLWnELBXxDBP2tqHxeqWJV hdkeypath=m/0'/0'/53'
+
+# End of dump

+ 109 - 0
test/ref/litecoin/ltcwallet.dump

@@ -0,0 +1,109 @@
+# Wallet dump created by Litecoin v0.15.0.1-ba8ed3a
+# * Created on 2017-10-05T11:37:22Z
+# * Best block at time of backup was 0 (12a765e31ffd4059bada1e25190f6e98c99d9714d334efa41a195a7e7e04bfe2),
+#   mined on 2011-10-07T07:31:05Z
+
+# extended private masterkey: xprv9s21ZrQH143K4HP5mfvPzp6QEGnH9GGAHam3sgePqEHMToeNqY7Mw9uEM3xCXJ2cx4wVEViaah6KAxVTBL7fY1DP6NbK9Pa3AobJykaf2zN
+
+T3mRqHyhXtPaT7RXXoMDJrcFme16Ntyx6w5A137GKMWx7tNi38Ck 2017-10-04T14:48:08Z reserve=1 # addr=LKJhRSuJLsdoGKpEPbRrTYQGo87VgGQPvM hdkeypath=m/0'/0'/43'
+T3Zsw6cXMEgrFShSqapn8ayRCJ1R1oCuvjZ6DuPz9TG8XF5UbTHj 2017-10-04T14:48:08Z reserve=1 # addr=LKLsbGqMKPdzh7pMvVNWuGJX5gPofppAbD hdkeypath=m/0'/0'/99'
+T9URDhvjdAJfrMUXHgpxnoJYpdiR5iyMqwr6cEUZBoQpYqKqn5g4 2017-10-04T14:48:08Z reserve=1 # addr=LKNL1SpaTgi9p8mwLvxoebiVr3ohwpWJSX hdkeypath=m/0'/0'/39'
+T7WJkRwxbxd4oL2qKYU1XUgDx3zEYv8k9BjWbZYdgZjAFhc1YFdi 2017-10-04T14:48:08Z reserve=1 # addr=LKZGfkQVjAejbKjHofKXfKA9SGBsa7wAEm hdkeypath=m/0'/0'/53'
+T7AQVL71gr89SwnVDVn5yNU4fjygFVNcLXiwU5pzMSF6FsRAMxGK 2017-10-04T14:48:08Z reserve=1 # addr=LKf1KstrhKZ3RAAySvvbD6nEuAm6kwLR7N hdkeypath=m/0'/0'/69'
+T56m5PFbjcRjvP9vgHnoEDJ5C1HhdtkGB5pnhNpMg5iUM8gfHyJC 2017-10-04T14:48:08Z reserve=1 # addr=LKwDjyntj2LiR4AAQBzfTi7yCsM9W3nWve hdkeypath=m/0'/0'/84'
+T6yGBGAChKaw3ZDfgpqvJ96MxuhhVNA9vFsJo5JWDAmfoNrS4xGA 2017-10-04T14:48:08Z reserve=1 # addr=LL4j1hdNRgAx9SiyunD6hrEdUujYhesL6C hdkeypath=m/0'/0'/11'
+T3KY6godaNL9Z1m6r9gzG1hNjzbBFbWbzQe7rhvW5hGACvxrW23K 2017-10-04T14:48:08Z reserve=1 # addr=LL8euS6Rc84mweN34mGdQJzVE2UqoQoeTM hdkeypath=m/0'/0'/123'
+T8v3zxDnMqbLS37PGVYdskRrSC9J15HVSgGDzDZsfQMNnnfRAtRE 2017-10-04T14:48:08Z reserve=1 # addr=LL99t8n8Yg4HASJPUY9nhbUe6CsG1iHYc8 hdkeypath=m/0'/0'/5'
+T3S2BNHng5E3xY93HtxJ6daHoru6MFJJrpKx8UZuC1kpEuyDysS4 2017-10-04T14:48:08Z reserve=1 # addr=LLL6q7ePmVkqZkJuvSnWLi9kNdH2AnGrPt hdkeypath=m/0'/0'/57'
+T5aDFhYA18k3qMm5g3RtehRUcXS4knhRiii6mWDSeXwTfcMhRUNc 2017-10-04T14:48:08Z reserve=1 # addr=LLXpFAhNxtcDo2Sq6j8kFk3LdjGWheLCJh hdkeypath=m/0'/0'/20'
+TA49m62urbcagKSsw3pNGkTykZYcwAC9we3X4kt3ytq7EqcgWh4X 2017-10-04T14:48:08Z reserve=1 # addr=LLbH361vNKhaVQHBkziwJF8rndsBpH1qYb hdkeypath=m/0'/0'/81'
+T3im3y8SydGGNA2awtGvExhqif4Tzf4RZ2yqV6ir4ZtW7WbAz9AS 2017-10-04T14:48:08Z reserve=1 # addr=LLeGTXr1XfyRiVPBkzLXHXLKgo7uH5pqV3 hdkeypath=m/0'/0'/25'
+TBGCREVwnB4v9H1bBJurDb48XDBqKt9RiSEykHN47PYcCr36NktV 2017-10-04T14:48:08Z reserve=1 # addr=LLmwq3ZDkjXgM666h9afpbq8KRUsP56172 hdkeypath=m/0'/0'/63'
+T8TCk9XQMQdahnyDkNY219KReRbyfWF9WENxAUkFTzVSs3f5sWh8 2017-10-04T14:48:08Z reserve=1 # addr=LLpF4bkgF5dosaq77BhUkBySLHsv9qDo8q hdkeypath=m/0'/0'/113'
+T7waGxxHek4U3hXPmidC3roZULxZb589hFRUhMTzaGb3CaBaeuad 2017-10-04T14:48:08Z reserve=1 # addr=LLwZW5cYjzEbz9YpGBDrG6SXTYtqazr5b8 hdkeypath=m/0'/0'/86'
+T3ifYRMcXk8fZqwf7Ty2EoGpnzaRRfLGgi7HoKNztD2q9hkKMSpD 2017-10-04T14:48:08Z reserve=1 # addr=LM2nGfEGb51sehZu8gAFpBv81utuM6T8jc hdkeypath=m/0'/0'/68'
+TAVsWRX5xJTDH3JhzRC38cNTmsqHHRDMcqgAJs19Kp3NShZwYnjR 2017-10-04T14:48:08Z reserve=1 # addr=LM9vyAVjR6ekRZMK8BbQUCmcoEN6ezTbAN hdkeypath=m/0'/0'/89'
+T7FaiQJUjgkwRgnwHa2EnsFKs2jFNykkbcMzCzXTZSofw9tPyqwq 2017-10-04T14:48:08Z reserve=1 # addr=LMBdBR6t9WW9C5YjAV8bRkybNt5gYXxGLr hdkeypath=m/0'/0'/46'
+T4SEJoanP7ogBLZ2ERBU1hqs21E9m6mTrc7qp7j9yJRixsSw2miJ 2017-10-04T14:48:08Z reserve=1 # addr=LMCZL9MpLjVGFjgyKn3BQiLKk23ptSVjaR hdkeypath=m/0'/0'/26'
+T9gEdgebLLf4uT8e6kJDYvfJionHBEdbKoRUBfRBhr8o2Lqmph6m 2017-10-04T14:48:08Z reserve=1 # addr=LMGrgVHuaiToUvF4u5Pr2sfKerjnToNFKz hdkeypath=m/0'/0'/72'
+T4CSAf5FcvHkjmXecXaSsYQXmjKKEtH7auw1EvnYWszG5R2BoiwF 2017-10-04T14:48:08Z reserve=1 # addr=LMLb5TTmGdwdYRw4HP7E9Rp6ukMiJDBou1 hdkeypath=m/0'/0'/3'
+T5vKzHJWZS6TtQGtzm8oj86GX8tmDzgpNyYiZJDfBzFebqcXHze2 2017-10-04T14:48:08Z reserve=1 # addr=LMNy5QsrfXykYqwP5hHhJLRRF9XnMRMekK hdkeypath=m/0'/0'/70'
+TBKGs27NR9oH7zU5fkJh5jSA7RZoEAbibvYTVLkNiR8ypteiLNK2 2017-10-04T14:48:08Z reserve=1 # addr=LMPznMFzgUNMs3LN1nu5m19QKDdm8XTZRR hdkeypath=m/0'/0'/109'
+T5tEyU2pDbtDQrujf6RSAdEx2M3z9mdVtiwoMF4cxSGxD7ow8Hw5 2017-10-04T14:48:08Z reserve=1 # addr=LMbvsr3Cn3rFvP1vYMpaV1APYjGLnCesSe hdkeypath=m/0'/0'/47'
+T7h9Hq5fRGW8wcVD3XVxDBQvQf9ox7WmW2NoSvPtRJAnusxVphET 2017-10-04T14:48:08Z reserve=1 # addr=LMch1stEinfMcVSoq3Ccf6TavAELL3CWVy hdkeypath=m/0'/0'/114'
+T4XJ9EFG1rtXpqH46b7sd2L2VJgv7ijsHSg8e9NxmUePJEUWazqY 2017-10-04T14:48:08Z reserve=1 # addr=LMhw816b4tieZ24RSwsb8mv9SRJNDkDcRv hdkeypath=m/0'/0'/18'
+TB9L7RRc1saYrMn3XWHra1ddiju99P46JoZFKm4X6gxXbgE1Ej7G 2017-10-04T14:48:08Z reserve=1 # addr=LMyUZsQD9Jda2hv7jokgrYxdGp7pCQNAeK hdkeypath=m/0'/0'/71'
+T6fwrz1QyJed2Yhar3xFKLkBLvHvJTBGtL15b6D2evEiGCTVxo5q 2017-10-04T14:48:08Z reserve=1 # addr=LN1RrbuXy48m2GBdwbHnHyx5VLGCothT2F hdkeypath=m/0'/0'/121'
+T8NC1dhgmnHZoALaBWU4EPVyxFP3nRAKWwupVBYVcKXdkvSaEx3W 2017-10-04T14:48:08Z reserve=1 # addr=LNLTYi9j47NWDvZZd8KbBj8r6o33y3i3QW hdkeypath=m/0'/0'/119'
+T5fKubJtf9oZow3XK2E4pU89Gj14PvKMoHvFgEWzmVykpSN6MrNn 2017-10-04T14:48:08Z reserve=1 # addr=LNkfJwnDqo6WM9sVZTcaehP6L3zGSwjuLr hdkeypath=m/0'/0'/15'
+T7y6DSPu6KpA35CYrd5UUfJMz9t9nJgHqTbkg7xQrDTgi1gFR4rr 2017-10-04T14:48:08Z reserve=1 # addr=LNm8Sa8bXGwaERLEZgHbAhY5iLT7hkFcyj hdkeypath=m/0'/0'/62'
+T4XvMvrpfAL2gbeDetTmKoqNCNsNkCNrMpbEmknTBieanN5PxHVK 2017-10-04T14:48:08Z reserve=1 # addr=LNpLNK3tWWzXKgnxGgKtwy1DwLfNAwqH1j hdkeypath=m/0'/0'/131'
+T9saJz47kbHCNkNPi4ESTyVLH6E8FWVa3sJtUY8T76uJZw6WTxPv 2017-10-04T14:48:08Z reserve=1 # addr=LNqeZzAdkP9r4XhrefiQ3YsqiyBEMheKj8 hdkeypath=m/0'/0'/97'
+TAwFJWpsUPubBBMcmikaPBFDZBb7CmBPiGYbizFMA8m73i1CeoVk 2017-10-04T14:48:08Z reserve=1 # addr=LPb7xnrs7ktMmpcmNzj19skDGcoRWuHDHG hdkeypath=m/0'/0'/133'
+T7oY3Npei8pZUoohD2PBZvRgzfWq2Cp5S5dog4B9c5AAfCVH8b23 2017-10-04T14:48:08Z reserve=1 # addr=LPf6u8uAN27rC9jkxLBRwqnHeaPHZUk2ZR hdkeypath=m/0'/0'/37'
+T7Q1ZG7t5B52K9cbj3crFAt6QqAs8b3wRCsSWetMw9kTYj9WD6JK 2017-10-04T14:48:08Z reserve=1 # addr=LPm2hPDqRcucUuv3BvAgr8zriH9vbsjjuS hdkeypath=m/0'/0'/122'
+TAL2Ka73nnTcZ43H9ZaiCBDASjVxEC3pDxhJkKpsVz69ZWB91s1H 2017-10-04T14:48:08Z reserve=1 # addr=LPn7x9LN3XKMaz366cYxgoutxTBMucivx9 hdkeypath=m/0'/0'/41'
+T3ViSsRvGziviXXwXVMS98VmRHJFdsG4FpF6mpxqieXEj3bvCm6t 2017-10-04T14:48:08Z reserve=1 # addr=LQUPqfwn6R6HGW7kPDzEKMFVZQsiSfoMsv hdkeypath=m/0'/0'/61'
+T8mAqqHziNFqw2n6Nizr3fxQFzTQhRRDZg2M5qXTcHokBUHHzXeL 2017-10-04T14:48:08Z reserve=1 # addr=LR5VYqWU2yRuTafPWySotV18zDdLaRwUyg hdkeypath=m/0'/0'/127'
+T6Z1pLvXvUb3azbA6tg2gGoBswMmFEcpvr3HyfSML5PzTZjGq4ud 2017-10-04T14:48:08Z reserve=1 # addr=LR6cWeyKQ1F3zWBZiecPPKr9pcYAbhwhJ6 hdkeypath=m/0'/0'/104'
+T4VPC6ohn7zQTsHc773ejRHDAS3gBbxjempw1DFqnQ2L5JoxX8Fn 2017-10-04T14:48:08Z reserve=1 # addr=LRAoVvvCJy6XPD9e9si6wZutkZT3a3pt8Y hdkeypath=m/0'/0'/8'
+T5UMAR6QxvsZPyxQdt6p84SYw31rxMq6NXRWQiiC32CowqMbJ6io 2017-10-04T14:48:08Z reserve=1 # addr=LRT6XNpCY84GRaNx56UsLhgXPZp3Q5bq72 hdkeypath=m/0'/0'/55'
+T9wwtntVALxqy9kqNJEntceYD9e2Yty8HJ56YQxa8mQMcCy3XsBJ 2017-10-04T14:48:08Z reserve=1 # addr=LRYhng4cTxiKJjMzUL5mQjRTFjWS8c7B8M hdkeypath=m/0'/0'/10'
+T5357iXwSm96M8iPQBdciNiNNNuSyi4Z3JBPUv54juLv6EffYVZC 2017-10-04T14:48:08Z reserve=1 # addr=LRjGRmJfDDyXoQBbB1ahTANb3SV8Q4xNzZ hdkeypath=m/0'/0'/45'
+T9QAYjgpcSdLsR7QjkqQ9FxVvpBRMaMSeLQCZctagkw6ho4X8451 2017-10-04T14:48:08Z reserve=1 # addr=LS5GmJqkm8wpqgg6aFixiUCuhu6SuUviF7 hdkeypath=m/0'/0'/19'
+T9Jpv1NhHj3y15tYySsWuCTh1X7jdhv5iTVaGD1LHu5LBgq6Z2Kk 2017-10-04T14:48:08Z reserve=1 # addr=LSqLEhLRqynv6JQrsV6To9m2qndNHVjeD5 hdkeypath=m/0'/0'/13'
+T85Fsin4NL8c1Xcx4XRL586H1vc1HBsR1Gbuh1v89AwbsdLC3icX 2017-10-04T14:48:08Z reserve=1 # addr=LSqWTEfJJDSPW8SWpxMBjtK3KmdHhWbuB3 hdkeypath=m/0'/0'/124'
+TAhJ3ZtJqdJ3akYhJ1anduLoNZcfcJcMRRRimkyFZgDSW1mcpGyU 2017-10-04T14:48:08Z hdmaster=1 # addr=LSs8CuV6p14VgrNrnEAge8e66nhFGjJpzF hdkeypath=m
+T9vUHqY7o9D13Enqzsv8bxz6FiGYzqC2q3KzFKPvf3em9CaAVdFr 2017-10-04T14:48:08Z reserve=1 # addr=LT3AZKLbHrNoxqy9FHUBacJQ6M5yUirRmK hdkeypath=m/0'/0'/1'
+T3RpVdSDvgxKUtcAjDnc74XgFVr2PE1jGpAgLryDNSAcuNTgs7gj 2017-10-04T14:48:08Z reserve=1 # addr=LT3Sgvkxt8SZjx1QJoQvmHWb44yHDhGc5C hdkeypath=m/0'/0'/91'
+T652xJjwRS8i1kyzEbzY4g2rqrDznrW9bKsAEw3RmAVQ5K2Zpx18 2017-10-04T14:48:08Z reserve=1 # addr=LT6UjMJPvPapdw7mdm13ev2MABQLfRJtry hdkeypath=m/0'/0'/95'
+TBLABZwhc8mVAJCj9uYvYGBHyaQDX2FFR7saK6A6PtwhxzwqWHu7 2017-10-04T14:48:08Z reserve=1 # addr=LT7Lj9HiWHUUxmrfWU4xWcyhfC2eyXWQZt hdkeypath=m/0'/0'/40'
+T3ttpVMBUMmnaZzpWqLr6NCunw4jzscKa3SJcHjd4Dw3FnW7N8dR 2017-10-04T14:48:08Z reserve=1 # addr=LTJ79HyN3NB5kHarLUt7731ixtuHUdUVKn hdkeypath=m/0'/0'/30'
+T3uQAJdV635ca3hkqduT9BRQd5EjxrYBM4VqZxhrmyXpaMokvo4s 2017-10-04T14:48:08Z label= # addr=LTUacuBvFqGqqZrAdjkNahVt457XJx1RhX hdkeypath=m/0'/0'/0'
+T66S2jTnPEAurJcSpW6W6PvTDmy9StCVZ4ButYyLSpM7vcjHmgSN 2017-10-04T14:48:08Z reserve=1 # addr=LTVAiWSvCfBV44Q9smZsHYPQCsNpdMsBd3 hdkeypath=m/0'/0'/35'
+T3tjMTqQZctdhgRySYHgH47rPnkub4h7jUq13cEntHmM2iHmXzzh 2017-10-04T14:48:08Z reserve=1 # addr=LTgW8DuC3rU2Wao3EGHoGwYuvJmpFned3B hdkeypath=m/0'/0'/24'
+T5t9je7mJkRiUwJVj8hzshZzxZR89N27SzkvWxnrPr21tmQYSS4x 2017-10-04T14:48:08Z reserve=1 # addr=LTge5NsL7FKYXAZQRx1L3vTQpzRyN9feGp hdkeypath=m/0'/0'/105'
+T8qzvk7JW9HF4aGgRAfQgP478zRzN2AsHf9FQdw759mMgPLkFwXQ 2017-10-04T14:48:08Z reserve=1 # addr=LU1foBG61ABDmoJpK429tzahKGY6ovexuq hdkeypath=m/0'/0'/78'
+TBFYbnK8myB5rxAw2WQtQEbic2B697Vqu7TmaDRizBFdc1H4U7Rh 2017-10-04T14:48:08Z reserve=1 # addr=LU3mk2HCmAY9EQZZ5eb6FKZT1QwDFU2F7U hdkeypath=m/0'/0'/103'
+T6e2vDPkK94tPEgZBhJKcJpL1d5QDASNi8yNra2orofhTw7Jajn6 2017-10-04T14:48:08Z reserve=1 # addr=LUDV4wUaBzpr5qYHFzkCJvioGnXNA5tv48 hdkeypath=m/0'/0'/65'
+TAjz7NCZys4MGZnzZKgdESYd8uuquBwU4Dtj84KtCxbGphNCwX3B 2017-10-04T14:48:08Z reserve=1 # addr=LULNzTTHcAfkDs3k8A8i1FrB4yTZd4kbBm hdkeypath=m/0'/0'/58'
+T4GLMgzgMRvBDN6kveq5tr42mqW34qbU9Gfqho4Q4zD74qrNeHk5 2017-10-04T14:48:08Z reserve=1 # addr=LUQoqMF79dwyi1r8QcxDN6XQjh9sKs2FVg hdkeypath=m/0'/0'/64'
+T5UpQNsQF4EqPQjDRqMDsHYNSYJxTr6FZ6Ut3BaMDiqgD7TPWvey 2017-10-04T14:48:08Z reserve=1 # addr=LUbaBs7ukwgQSkFW4EiCX6N5ThDtrVKeta hdkeypath=m/0'/0'/34'
+T3A6pr6tVUQeNFsASEssbUBhiuZjgkuwM2XYGbPz5CJgtt6WN9YX 2017-10-04T14:48:08Z reserve=1 # addr=LV2SZ4s3WBqoErBgT3tmLiSCJkKvZZFfwe hdkeypath=m/0'/0'/111'
+T5isXosFpoQiritzQMHxCBuzdjdTSkDt14vtQrJnGThqnwd25vGD 2017-10-04T14:48:08Z reserve=1 # addr=LV5WAzx7s78HrbSnh4TBjiHoNJtQQxJWKi hdkeypath=m/0'/0'/50'
+T3VfqdExhA9ahWMKqfHKTCdStp4bhSjMtxwKq9AtS6G7GimfK7Zs 2017-10-04T14:48:08Z reserve=1 # addr=LV5swfHkzzTob6F9fAXbsho4KxPdfJNLzd hdkeypath=m/0'/0'/27'
+TBJvhNwEg64DscQhGmbrpvbzwTHn3Dai4GYDpqZdpvFinW1W9N68 2017-10-04T14:48:08Z reserve=1 # addr=LVdrkgRihyEGxp9qakYx2LmtgcQboWvbU4 hdkeypath=m/0'/0'/117'
+TBNxbEcpBwDwLs8FUafheUDysbAmgbjxVzPwio9cGTrkpvRqZx2U 2017-10-04T14:48:08Z reserve=1 # addr=LVyf8AZ13XZz1zeTFafbnkUj2CCqcoW5rb hdkeypath=m/0'/0'/116'
+T5nK1i7ZmHhSv5z4Na7zowqvCSWxAnEr9FmdiMYRSsGJd9PQU238 2017-10-04T14:48:08Z reserve=1 # addr=LW1bD6A3oGNmfhm4sBZQBZAF7XKvydDKkz hdkeypath=m/0'/0'/129'
+T5WSerYuKKK6goT4g1rqN6N73Uswm9aTn7URG1Fe3n9PUgGwWxZe 2017-10-04T14:48:08Z reserve=1 # addr=LWAQx33RkWmDVdDoqCAP5JSQjuxbUhB885 hdkeypath=m/0'/0'/4'
+T48QfivUTozdCoFE4i3ch7EC82s4wE2f6hCtCNzpMmBbiPif1jQr 2017-10-04T14:48:08Z reserve=1 # addr=LWNjrkSajzPpAMwGZE1rfaqzSGQFcrE3pL hdkeypath=m/0'/0'/16'
+TBBnyTsQf8bqwY47k3ha21tPQZ5XVTj6thp3qty9d68cKwP7dxSt 2017-10-04T14:48:08Z reserve=1 # addr=LWa4gRn2ao35hMGRTKZxVhf6UHaqM9CWgP hdkeypath=m/0'/0'/85'
+T6ff3LL9UgjJVsExazw61k95aSTNDNjafYY8YHRwGfFUr6YKP8LA 2017-10-04T14:48:08Z reserve=1 # addr=LWqUrYetNChKyAi7oMAp4kYKZVWzmPotGw hdkeypath=m/0'/0'/120'
+T5AEoLWuBZPuyFxWs9uPYiAQpEXkReS5hbMJFG24C9g8f4bQBDV4 2017-10-04T14:48:08Z reserve=1 # addr=LXHc584XU4ntq8jAVpTvXEZKpLNXgkZN2N hdkeypath=m/0'/0'/87'
+T8sk7EZRBMYZkwzVvueZpoBuae1mEUdWACVJjpBgXynqMNQFMPfh 2017-10-04T14:48:08Z reserve=1 # addr=LXJBz1wcD4AC7LDYNLcuQoFafdeJLn7quA hdkeypath=m/0'/0'/67'
+T9FdBbGkSnXVR4tPYDXFvzzJuRtQLzMYyMrBkVJRbZJjfNFC31KU 2017-10-04T14:48:08Z reserve=1 # addr=LXKNcKa2iDmCbujVVoYU32HHM8ZeRVZWfv hdkeypath=m/0'/0'/75'
+T4ShKJhNgkkMc1XbaRKJvdfnX6TPVdrQzjmKHZ62iFEuo6B1C9gV 2017-10-04T14:48:08Z reserve=1 # addr=LXRWCdWmvTxt8f4ohtQFYjqE2G2ibxrTZL hdkeypath=m/0'/0'/93'
+T7wPrzTing6LShZNcpxDYWETZMSrns8eaM5gQobGSqpdxrtd9wh4 2017-10-04T14:48:08Z reserve=1 # addr=LXnf9uEdEJbEVeKDsJ7YeMeXAgaJFmTS8R hdkeypath=m/0'/0'/48'
+T3S1zdoA6XhjiRgfNdSFRzNQJVxZwBihqY1MSzWQ9eT1beEbrNPT 2017-10-04T14:48:08Z reserve=1 # addr=LYJSvV9zdRwuUVoqLN7dRmA9JU1rJtJhgb hdkeypath=m/0'/0'/28'
+T8wLmVTfBzTTMk9bx34rvkmeK4n2aPPicG7fXvbACyrv16Du4iN2 2017-10-04T14:48:08Z reserve=1 # addr=LYqNSgjc3Pp4aYnivHaaxJP6jKzCYhWY7J hdkeypath=m/0'/0'/132'
+T8h6FjvM4XaSm2jS22mktnC6W2NNupg9kiDPWMbk5HjQ5g2KGbGE 2017-10-04T14:48:08Z reserve=1 # addr=LZ5vE8TQMpoDAb58ayPH37yAY83cHiKhwA hdkeypath=m/0'/0'/88'
+T3ADW5C25CHF75kTdopcuFR75DupoWT2MiZpCTHxEPnSo9F32ZwF 2017-10-04T14:48:08Z reserve=1 # addr=LZFJYKx28dvjjfwdusUQtCisaTnSD9pF4Z hdkeypath=m/0'/0'/83'
+T3JjTDeBmLKz1YqxsRP6Eu3QrMdGh8wHtWb4Xpo5YtGsw5ovVPw8 2017-10-04T14:48:08Z reserve=1 # addr=LZXgvvxULDC8atG16FwDG2GR6svemhszMs hdkeypath=m/0'/0'/101'
+T6cWTMAHW5NhHJuvCSyqKPzxCGPKBSWtYwY1LXtGtBh64z7UFJgt 2017-10-04T14:48:08Z reserve=1 # addr=LZtoZXSZ3BFHKdmwVsGN3bYxayxcTxBaPr hdkeypath=m/0'/0'/51'
+TA6FuX5497DENJxcq6iBYJvY3Z9TYzmkGhVo7BJvz9t9dJoWX984 2017-10-04T14:48:08Z reserve=1 # addr=LaSMqKBaQGFCvyLEvb8w4b13csJLLTjcJm hdkeypath=m/0'/0'/42'
+T7SJaGyhLzwpPwsXgKZa9N8p9ZTvzHbS3Ej9KBGkpexQ2NFNLxZF 2017-10-04T14:48:08Z reserve=1 # addr=LaVEJXqbKksyTedriitBuiCLZUWFxCEVQE hdkeypath=m/0'/0'/125'
+T65jdsUF5XtzLP62uoB7VP9FAwD61eRrFv7VY7o6xagCoucvqDzX 2017-10-04T14:48:08Z reserve=1 # addr=LaiFAeRp7A9Qu3grwMDkebDv5fNGYzAvrY hdkeypath=m/0'/0'/112'
+TAJCqFg3JJBgYxqzuNVQG5hCfqghFJYgExMpYX6DrH8HFDyWVDCD 2017-10-04T14:48:08Z reserve=1 # addr=Lb1DPn8dCAWSohEqDVFALSW4foWF3xFVcw hdkeypath=m/0'/0'/107'
+T4nUsXxjxRF3q75mQt35Ht7yh9pQ6PWGeDPWdsFXaj4zJMRANYQv 2017-10-04T14:48:08Z reserve=1 # addr=Lb1F9TF4Dhy8diHHp1z2yRDoqvF3NdcWCT hdkeypath=m/0'/0'/77'
+T4GtbkhiNCLABbi6LPtetQk14AMS26GsecZgHN9ihFt3FXgbUYev 2017-10-04T14:48:08Z reserve=1 # addr=Lb7DAvX9nVUX5gLr7Y4hwME3CEyE56kPTR hdkeypath=m/0'/0'/44'
+TAvus3rhumh6xeQ4PWNrY9B1vsp9fyi4zLDjUXy2et2aqE1JdWpn 2017-10-04T14:48:08Z reserve=1 # addr=Lb8qYPrMUobbKG1KBwWXYQk6J4uCkAKkHQ hdkeypath=m/0'/0'/31'
+T4XnN2MaQvEy3f41bVnThyMdbBuTKsUQGZZKAH6nGNB6vgb16tti 2017-10-04T14:48:08Z reserve=1 # addr=LbNj9VPCSphjnuKnAPnu4uDDYiaS9mHaSc hdkeypath=m/0'/0'/22'
+T5P7qwR7eGBcAAJKg6N9f2zvfmXQEt2hHuE7fLASZ8y7GdSnSGsG 2017-10-04T14:48:08Z reserve=1 # addr=LbUPH7FWE7e3Ad3mXyftp5EVLRmnhJ9atp hdkeypath=m/0'/0'/38'
+T95uAMy842cfLYac9KWNVJxV4TqdQ8vvQLNCYgGCmcp9CRh1Uy5r 2017-10-04T14:48:08Z reserve=1 # addr=LbaaD1GoBUjnac4VWxDdShxZBrELFzkzJ3 hdkeypath=m/0'/0'/33'
+T7m2JkiNq27TZhp7YjMioKXJXe1NNGBnUcJNBe9VKoE3r48FWXap 2017-10-04T14:48:08Z reserve=1 # addr=Lc1LcydegW1YWvvuN1BYZibu7oJ78SMSWe hdkeypath=m/0'/0'/130'
+T7P2aaSimnXrgn646UmTVDvvVUf63EytQ29bsbkTTokfrzaJbGqg 2017-10-04T14:48:08Z reserve=1 # addr=LcGuePpkfsUiCc72yDAM792fiZnNSzivjm hdkeypath=m/0'/0'/56'
+T3psoe7VBETwXJKRhqEWDpX4kUbhyb4TUnzCWRtQmjbtzM9jiBTs 2017-10-04T14:48:08Z reserve=1 # addr=LcPzzyjZWu5fWSvsYnABajH7KZGWVeSoew hdkeypath=m/0'/0'/128'
+T5B3xQpZWz2PwV48rn8NK6SJ8zy91durzGESURLu5Q9NKkzzpejZ 2017-10-04T14:48:08Z reserve=1 # addr=LcVjwHr3mSwgDZczcKTKVGRrNz1YAnQoc3 hdkeypath=m/0'/0'/52'
+T86aRnWbJje7XkH9MgSVG8HdTeP52qt7Z1Rdmkt7ewizw6937ZmT 2017-10-04T14:48:08Z reserve=1 # addr=Lcp11fbnZBZo5bJqdKUGmx5xMK29goa5c7 hdkeypath=m/0'/0'/100'
+
+# End of dump

+ 380 - 121
test/test.py

@@ -22,14 +22,14 @@ test/test.py:  Test suite for the MMGen suite
 
 
 import sys,os,subprocess,shutil,time,re
 import sys,os,subprocess,shutil,time,re
 
 
-pn = os.path.dirname(sys.argv[0])
-os.chdir(os.path.join(pn,os.pardir))
-sys.path.__setitem__(0,os.path.abspath(os.curdir))
+repo_root = os.path.normpath(os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]),os.pardir)))
+os.chdir(repo_root)
+sys.path.__setitem__(0,repo_root)
 
 
 # Import these _after_ local path's been added to sys.path
 # Import these _after_ local path's been added to sys.path
 from mmgen.common import *
 from mmgen.common import *
 from mmgen.test import *
 from mmgen.test import *
-from mmgen.protocol import get_coin_protocol
+from mmgen.protocol import CoinProtocol
 
 
 g.quiet = False # if 'quiet' was set in config file, disable here
 g.quiet = False # if 'quiet' was set in config file, disable here
 os.environ['MMGEN_QUIET'] = '0' # and for the spawned scripts
 os.environ['MMGEN_QUIET'] = '0' # and for the spawned scripts
@@ -51,13 +51,12 @@ ref_wallet_brainpass = 'abc'
 ref_wallet_hash_preset = '1'
 ref_wallet_hash_preset = '1'
 ref_wallet_incog_offset = 123
 ref_wallet_incog_offset = 123
 
 
-from mmgen.obj import MMGenTXLabel,PrivKey,BTCAmt
+from mmgen.obj import MMGenTXLabel,PrivKey
 from mmgen.addr import AddrGenerator,KeyGenerator,AddrList,AddrData,AddrIdxList
 from mmgen.addr import AddrGenerator,KeyGenerator,AddrList,AddrData,AddrIdxList
 ref_tx_label = ''.join([unichr(i) for i in  range(65,91) +
 ref_tx_label = ''.join([unichr(i) for i in  range(65,91) +
 											range(1040,1072) + # cyrillic
 											range(1040,1072) + # cyrillic
 											range(913,939) +   # greek
 											range(913,939) +   # greek
 											range(97,123)])[:MMGenTXLabel.max_len]
 											range(97,123)])[:MMGenTXLabel.max_len]
-tx_fee             = '0.0001'
 ref_bw_hash_preset = '1'
 ref_bw_hash_preset = '1'
 ref_bw_file        = 'wallet.mmbrain'
 ref_bw_file        = 'wallet.mmbrain'
 ref_bw_file_spc    = 'wallet-spaced.mmbrain'
 ref_bw_file_spc    = 'wallet-spaced.mmbrain'
@@ -83,7 +82,7 @@ if not any(e in ('--skip-deps','--resume','-S','-r') for e in sys.argv+shortopts
 		else:
 		else:
 			try: shutil.rmtree(data_dir)
 			try: shutil.rmtree(data_dir)
 			except: # we couldn't remove data dir - perhaps regtest daemon is running
 			except: # we couldn't remove data dir - perhaps regtest daemon is running
-				try: subprocess.call(['python','mmgen-regtest','stop'])
+				try: subprocess.call(['python',os.path.join('cmds','mmgen-regtest'),'stop'])
 				except: rdie(1,'Unable to remove data dir!')
 				except: rdie(1,'Unable to remove data dir!')
 				else:
 				else:
 					time.sleep(2)
 					time.sleep(2)
@@ -148,11 +147,30 @@ sys.argv = [sys.argv[0]] + ['--data-dir',data_dir] + sys.argv[1:]
 cmd_args = opts.init(opts_data)
 cmd_args = opts.init(opts_data)
 opt.popen_spawn = True # popen has issues, so use popen_spawn always
 opt.popen_spawn = True # popen has issues, so use popen_spawn always
 
 
+ref_subdir = '' if g.proto.base_coin == 'BTC' else g.proto.name
+altcoin_pfx = '' if g.proto.base_coin == 'BTC' else '-'+g.proto.base_coin
+tn_ext = ('','.testnet')[g.testnet]
+
+fork       = {'bch':'btc','btc':'btc','ltc':'ltc'}[g.coin.lower()]
+tx_fee     = {'btc':'0.0001','bch':'0.001','ltc':'0.01'}[g.coin.lower()]
+txbump_fee = {'btc':'123s','bch':'567s','ltc':'12345s'}[g.coin.lower()]
+
+rtFundAmt  = {'btc':'500','bch':'500','ltc':'5500'}[g.coin.lower()]
+rtFee = {
+	'btc': ('20s','10s','60s','0.0001','10s','20s'),
+	'bch': ('20s','10s','60s','0.0001','10s','20s'),
+	'ltc': ('1000s','500s','1500s','0.05','400s','1000s')
+}[g.coin.lower()]
+rtBals = {
+	'btc': ('499.999942','399.9998214','399.9998079','399.9996799','13.00000000','986.99957990','999.99957990'),
+	'bch': ('499.9999416','399.9999124','399.99989','399.9997616','276.22339397','723.77626763','999.99966160'),
+	'ltc': ('5499.9971','5399.994085','5399.993545','5399.987145','13.00000000','10986.93714500','10999.93714500'),
+}[g.coin.lower()]
+rtBobOp3 = {'btc':'S:2','bch':'L:3','ltc':'S:2'}[g.coin.lower()]
+
 if opt.segwit and 'S' not in g.proto.mmtypes:
 if opt.segwit and 'S' not in g.proto.mmtypes:
 	die(1,'--segwit option incompatible with {}'.format(g.proto.__name__))
 	die(1,'--segwit option incompatible with {}'.format(g.proto.__name__))
 
 
-tn_desc = ('','.testnet')[g.testnet]
-
 def randbool():
 def randbool():
 	return hexlify(os.urandom(1))[1] in '12345678'
 	return hexlify(os.urandom(1))[1] in '12345678'
 def get_segwit_val():
 def get_segwit_val():
@@ -280,12 +298,30 @@ cfgs = {
 		'seed_len':        128,
 		'seed_len':        128,
 		'seed_id':         'FE3C6545',
 		'seed_id':         'FE3C6545',
 		'ref_bw_seed_id':  '33F10310',
 		'ref_bw_seed_id':  '33F10310',
-		'addrfile_chk':            ('B230 7526 638F 38CB','B64D 7327 EF2A 60FE'),
-		'addrfile_segwit_chk':     ('9914 6D10 2307 F348','7DBF 441F E188 8B37'),
-		'addrfile_compressed_chk': ('95EB 8CC0 7B3B 7856','629D FDE4 CDC0 F276'),
-		'keyaddrfile_chk':            ('CF83 32FB 8A8B 08E2','FEBF 7878 97BB CC35'),
-		'keyaddrfile_segwit_chk':     ('C13B F717 D4E8 CF59','4DB5 BAF0 45B7 6E81'),
-		'keyaddrfile_compressed_chk': ('E43A FA46 5751 720A','B995 A6CF D1CD FAD0'),
+		'addrfile_chk': {
+			'btc': ('B230 7526 638F 38CB','B64D 7327 EF2A 60FE'),
+			'ltc': ('2B23 5E97 848A B961','928D 3CB6 78FF 9829'),
+		},
+		'addrfile_segwit_chk': {
+			'btc': ('9914 6D10 2307 F348','7DBF 441F E188 8B37'),
+			'ltc': ('CC09 A190 B7DF B7CD','3676 4C49 14F8 1AD0'),
+		},
+		'addrfile_compressed_chk': {
+			'btc': ('95EB 8CC0 7B3B 7856','629D FDE4 CDC0 F276'),
+			'ltc': ('35D5 8ECA 9A42 46C3','37E9 A36E 94A2 010F'),
+		},
+		'keyaddrfile_chk': {
+			'btc': ('CF83 32FB 8A8B 08E2','FEBF 7878 97BB CC35'),
+			'ltc': ('1896 A26C 7F14 2D01','B41D BA63 0605 DD66'),
+		},
+		'keyaddrfile_segwit_chk': {
+			'btc': ('C13B F717 D4E8 CF59','4DB5 BAF0 45B7 6E81'),
+			'ltc': ('054B 9794 55B4 5D82','C373 0074 DEE6 B70A'),
+		},
+		'keyaddrfile_compressed_chk': {
+			'btc': ('E43A FA46 5751 720A','B995 A6CF D1CD FAD0'),
+			'ltc': ('7603 2FE3 2145 FFAD','3248 356A C707 4A41'),
+		},
 		'passfile_chk':    'EB29 DC4F 924B 289F',
 		'passfile_chk':    'EB29 DC4F 924B 289F',
 		'passfile32_chk':  '37B6 C218 2ABC 7508',
 		'passfile32_chk':  '37B6 C218 2ABC 7508',
 		'wpasswd':         'reference password',
 		'wpasswd':         'reference password',
@@ -302,7 +338,7 @@ cfgs = {
 		'pass_idx_list': '1,4,9-11,1100',
 		'pass_idx_list': '1,4,9-11,1100',
 		'dep_generators':  {
 		'dep_generators':  {
 			'mmdat':       'refwalletgen1',
 			'mmdat':       'refwalletgen1',
-			pwfile:       'refwalletgen1',
+			pwfile:        'refwalletgen1',
 			'addrs':       'refaddrgen1',
 			'addrs':       'refaddrgen1',
 			'akeys.mmenc': 'refkeyaddrgen1'
 			'akeys.mmenc': 'refkeyaddrgen1'
 		},
 		},
@@ -313,12 +349,30 @@ cfgs = {
 		'seed_len':        192,
 		'seed_len':        192,
 		'seed_id':         '1378FC64',
 		'seed_id':         '1378FC64',
 		'ref_bw_seed_id':  'CE918388',
 		'ref_bw_seed_id':  'CE918388',
-		'addrfile_chk':            ('8C17 A5FA 0470 6E89','0A59 C8CD 9439 8B81'),
-		'addrfile_segwit_chk':     ('91C4 0414 89E4 2089','3BA6 7494 8E2B 858D'),
-		'addrfile_compressed_chk': ('2615 8401 2E98 7ECA','DF38 22AB AAB0 124E'),
-		'keyaddrfile_chk':            ('9648 5132 B98E 3AD9','2F72 C83F 44C5 0FAC'),
-		'keyaddrfile_segwit_chk':     ('C98B DF08 A3D5 204B','25F2 AEB6 AAAC 8BBE'),
-		'keyaddrfile_compressed_chk': ('6D6D 3D35 04FD B9C3','B345 9CD8 9EAE 5489'),
+		'addrfile_chk': {
+			'btc': ('8C17 A5FA 0470 6E89','0A59 C8CD 9439 8B81'),
+			'ltc': ('2B77 A009 D5D0 22AD','FCEC 0032 9EF9 B201'),
+		},
+		'addrfile_segwit_chk': {
+			'btc': ('91C4 0414 89E4 2089','3BA6 7494 8E2B 858D'),
+			'ltc': ('8F12 FA7B 9F12 594C','E79E F55B 1536 56F2'),
+		},
+		'addrfile_compressed_chk': {
+			'btc': ('2615 8401 2E98 7ECA','DF38 22AB AAB0 124E'),
+			'ltc': ('197C C48C 3C37 AB0F','5072 15DA 1A90 5E99'),
+		},
+		'keyaddrfile_chk': {
+			'btc': ('9648 5132 B98E 3AD9','2F72 C83F 44C5 0FAC'),
+			'ltc': ('DBD4 FAB6 7E46 CD07','1DA9 C245 F669 670C'),
+		},
+		'keyaddrfile_segwit_chk': {
+			'btc': ('C98B DF08 A3D5 204B','25F2 AEB6 AAAC 8BBE'),
+			'ltc': ('1829 7FE7 2567 CB91','1305 9007 E515 B66A'),
+		},
+		'keyaddrfile_compressed_chk': {
+			'btc': ('6D6D 3D35 04FD B9C3','B345 9CD8 9EAE 5489'),
+			'ltc': ('F5DA 9D60 6798 C4E9','F928 113B C9D7 9DF5'),
+		},
 		'passfile_chk':    'ADEA 0083 094D 489A',
 		'passfile_chk':    'ADEA 0083 094D 489A',
 		'passfile32_chk':  '2A28 C5C7 36EC 217A',
 		'passfile32_chk':  '2A28 C5C7 36EC 217A',
 		'wpasswd':         'reference password',
 		'wpasswd':         'reference password',
@@ -346,26 +400,57 @@ cfgs = {
 		'seed_len':        256,
 		'seed_len':        256,
 		'seed_id':         '98831F3A',
 		'seed_id':         '98831F3A',
 		'ref_bw_seed_id':  'B48CD7FC',
 		'ref_bw_seed_id':  'B48CD7FC',
-		'addrfile_chk':            ('6FEF 6FB9 7B13 5D91','3C2C 8558 BB54 079E'),
-		'addrfile_segwit_chk':     ('06C1 9C87 F25C 4EE6','58D1 7B6C E9F9 9C14'),
-		'addrfile_compressed_chk': ('A33C 4FDE F515 F5BC','5186 02C2 535E B7D5'),
-		'keyaddrfile_chk':            ('9F2D D781 1812 8BAD','7410 8F95 4B33 B4B2'),
-		'keyaddrfile_segwit_chk':     ('A447 12C2 DD14 5A9B','0690 460D A600 D315'),
-		'keyaddrfile_compressed_chk': ('420A 8EB5 A9E2 7814','3243 DD92 809E FE8D'),
+		'addrfile_chk': {
+			'btc': ('6FEF 6FB9 7B13 5D91','3C2C 8558 BB54 079E'),
+			'ltc': ('AD52 C3FE 8924 AAF0','5738 5C4F 167C F9AE'),
+		},
+		'addrfile_segwit_chk': {
+			'btc': ('06C1 9C87 F25C 4EE6','58D1 7B6C E9F9 9C14'),
+			'ltc': ('63DF E42A 0827 21C3','1A3F 3016 2E2B F33A'),
+		},
+		'addrfile_compressed_chk': {
+			'btc': ('A33C 4FDE F515 F5BC','5186 02C2 535E B7D5'),
+			'ltc': ('3FC0 8F03 C2D6 BD19','535E 5CDC 1CA7 08D5'),
+		},
+		'keyaddrfile_chk': {
+			'btc': ('9F2D D781 1812 8BAD','7410 8F95 4B33 B4B2'),
+			'ltc': ('B804 978A 8796 3ED4','93A6 844C 8ECC BEF4'),
+		},
+		'keyaddrfile_segwit_chk': {
+			'btc': ('A447 12C2 DD14 5A9B','0690 460D A600 D315'),
+			'ltc': ('E8A3 9F6E E164 A521','70ED 8557 5882 08A5'),
+		},
+		'keyaddrfile_compressed_chk': {
+			'btc': ('420A 8EB5 A9E2 7814','3243 DD92 809E FE8D'),
+			'ltc': ('8D1C 781F EB7F 44BC','678E 8EF9 1396 B140'),
+		},
 		'passfile_chk':    '2D6D 8FBA 422E 1315',
 		'passfile_chk':    '2D6D 8FBA 422E 1315',
 		'passfile32_chk':  'F6C1 CDFB 97D9 FCAE',
 		'passfile32_chk':  'F6C1 CDFB 97D9 FCAE',
 		'wpasswd':         'reference password',
 		'wpasswd':         'reference password',
 		'ref_wallet':      '98831F3A-{}[256,1].mmdat'.format(('27F2BF93','E2687906')[g.testnet]),
 		'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_addrfile':    '98831F3A{}[1,31-33,500-501,1010-1011]{}.addrs'.format(altcoin_pfx,tn_ext),
+		'ref_segwitaddrfile':'98831F3A{}-S[1,31-33,500-501,1010-1011]{}.addrs'.format(altcoin_pfx,tn_ext),
+		'ref_keyaddrfile': '98831F3A{}[1,31-33,500-501,1010-1011]{}.akeys.mmenc'.format(altcoin_pfx,tn_ext),
 		'ref_passwdfile':  '98831F3A-фубар@crypto.org-b58-20[1,4,9-11,1100].pws',
 		'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_addrfile_chksum': {
+			'btc': ('6FEF 6FB9 7B13 5D91','3C2C 8558 BB54 079E'),
+			'ltc': ('AD52 C3FE 8924 AAF0','5738 5C4F 167C F9AE'),
+		},
+		'ref_segwitaddrfile_chksum': {
+			'btc': ('06C1 9C87 F25C 4EE6','58D1 7B6C E9F9 9C14'),
+			'ltc': ('63DF E42A 0827 21C3','1A3F 3016 2E2B F33A'),
+		},
+		'ref_keyaddrfile_chksum': {
+			'btc': ('9F2D D781 1812 8BAD','7410 8F95 4B33 B4B2'),
+			'ltc': ('B804 978A 8796 3ED4','93A6 844C 8ECC BEF4'),
+		},
 		'ref_passwdfile_chksum':  'A983 DAB9 5514 27FB',
 		'ref_passwdfile_chksum':  'A983 DAB9 5514 27FB',
 #		'ref_fake_unspent_data':'98831F3A_unspent.json',
 #		'ref_fake_unspent_data':'98831F3A_unspent.json',
-		'ref_tx_file':     'FFB367[1.234]{}.rawtx'.format(tn_desc),
+		'ref_tx_file': {
+			'btc': 'FFB367[1.234]{}.rawtx',
+			'bch': '99BE60-BCH[106.6789]{}.rawtx',
+			'ltc': '75F455-LTC[106.6789]{}.rawtx',
+		},
 		'ic_wallet':       '98831F3A-5482381C-18460FB1[256,1].mmincog',
 		'ic_wallet':       '98831F3A-5482381C-18460FB1[256,1].mmincog',
 		'ic_wallet_hex':   '98831F3A-1630A9F2-870376A9[256,1].mmincox',
 		'ic_wallet_hex':   '98831F3A-1630A9F2-870376A9[256,1].mmincox',
 
 
@@ -583,6 +668,28 @@ cmd_group['regtest'] = (
 	('regtest_stop',               'stopping regtest daemon'),
 	('regtest_stop',               'stopping regtest daemon'),
 )
 )
 
 
+# undocumented admin cmds
+cmd_group_admin = OrderedDict()
+cmd_group_admin['create_ref_tx'] = (
+	('ref_tx_setup',                     'regtest (Bob and Alice) mode setup'),
+	('ref_tx_addrgen_bob_ref_wallet',    'address generation (Bob - reference wallet)'),
+	('ref_tx_addrimport_bob_ref_wallet', "importing Bob's addresses (reference wallet)"),
+	('ref_tx_fund_bob',                  "funding Bob's wallet (reference wallet)"),
+	('ref_tx_bob_split',                 "splitting Bob's funds (reference wallet)"),
+	('ref_tx_generate',                  'mining a block'),
+	('ref_tx_bob_create_tx',             "creating reference transaction"),
+	('ref_tx_bob_modify_tx',             "modifying reference transaction (testnet+mainnet)"),
+)
+cmd_list_admin = OrderedDict()
+cmd_data_admin = OrderedDict()
+for k in cmd_group_admin: cmd_list_admin[k] = []
+
+cmd_data_admin['info_create_ref_tx'] = 'create reference tx',[8]
+for a,b in cmd_group_admin['create_ref_tx']:
+	cmd_list_admin['create_ref_tx'].append(a)
+	cmd_data_admin[a] = (8,b,[[[],8]])
+# end undocumented admin commands
+
 cmd_list = OrderedDict()
 cmd_list = OrderedDict()
 for k in cmd_group: cmd_list[k] = []
 for k in cmd_group: cmd_list[k] = []
 
 
@@ -636,12 +743,6 @@ utils = {
 
 
 addrs_per_wallet = 8
 addrs_per_wallet = 8
 
 
-# total of two outputs must be < 10 BTC
-for k in cfgs:
-	cfgs[k]['amts'] = [0,0]
-	for idx,mod in ((0,6),(1,4)):
-		cfgs[k]['amts'][idx] = '%s.%s' % ((getrandnum(2) % mod), str(getrandnum(4))[:5])
-
 meta_cmds = OrderedDict([
 meta_cmds = OrderedDict([
 	['ref1', ('refwalletgen1','refaddrgen1','refkeyaddrgen1')],
 	['ref1', ('refwalletgen1','refaddrgen1','refkeyaddrgen1')],
 	['ref2', ('refwalletgen2','refaddrgen2','refkeyaddrgen2')],
 	['ref2', ('refwalletgen2','refaddrgen2','refkeyaddrgen2')],
@@ -685,8 +786,6 @@ usr_rand_chars = (5,30)[bool(opt.usr_random)]
 usr_rand_arg = '-r%s' % usr_rand_chars
 usr_rand_arg = '-r%s' % usr_rand_chars
 cmd_total = 0
 cmd_total = 0
 
 
-if opt.system: sys.path.pop(0)
-
 # Disable color in spawned scripts so we can parse their output
 # Disable color in spawned scripts so we can parse their output
 os.environ['MMGEN_DISABLE_COLOR'] = '1'
 os.environ['MMGEN_DISABLE_COLOR'] = '1'
 os.environ['MMGEN_NO_LICENSE'] = '1'
 os.environ['MMGEN_NO_LICENSE'] = '1'
@@ -798,20 +897,39 @@ def verify_checksum_or_exit(checksum,chk):
 
 
 from test.mmgen_pexpect import MMGenPexpect
 from test.mmgen_pexpect import MMGenPexpect
 class MMGenExpect(MMGenPexpect):
 class MMGenExpect(MMGenPexpect):
-	def __init__(self,name,mmgen_cmd,cmd_args=[],extra_desc='',no_output=False):
+
+	def __init__(self,name,mmgen_cmd,cmd_args=[],extra_desc='',no_output=False,msg_only=False):
+
 		desc = (cmd_data[name][1],name)[bool(opt.names)] + (' ' + extra_desc).strip()
 		desc = (cmd_data[name][1],name)[bool(opt.names)] + (' ' + extra_desc).strip()
-		pa = ['testnet','rpc_host','rpc_port','regtest','coin']
-		return MMGenPexpect.__init__(self,name,mmgen_cmd,cmd_args,desc,no_output=no_output,passthru_args=pa)
+		passthru_args = ['testnet','rpc_host','rpc_port','regtest','coin']
+
+		if not opt.system:
+			os.environ['PYTHONPATH'] = repo_root
+			mmgen_cmd = os.path.relpath(os.path.join(repo_root,'cmds',mmgen_cmd))
+		elif g.platform == 'win':
+			mmgen_cmd = os.path.join('/mingw64','opt','bin',mmgen_cmd)
+
+		return MMGenPexpect.__init__(
+			self,
+			name,
+			mmgen_cmd,
+			cmd_args,
+			desc,
+			no_output=no_output,
+			passthru_args=passthru_args,
+			msg_only=msg_only)
 
 
 def create_fake_unspent_entry(coinaddr,al_id=None,idx=None,lbl=None,non_mmgen=False,segwit=False):
 def create_fake_unspent_entry(coinaddr,al_id=None,idx=None,lbl=None,non_mmgen=False,segwit=False):
 	if 'S' not in g.proto.mmtypes: segwit = False
 	if 'S' not in g.proto.mmtypes: segwit = False
 	if lbl: lbl = ' ' + lbl
 	if lbl: lbl = ' ' + lbl
 	spk1,spk2 = (('76a914','88ac'),('a914','87'))[segwit and coinaddr.addr_fmt=='p2sh']
 	spk1,spk2 = (('76a914','88ac'),('a914','87'))[segwit and coinaddr.addr_fmt=='p2sh']
+	amt1,amt2 = {'btc':(10,40),'bch':(10,40),'ltc':(1000,4000)}[g.coin.lower()]
 	return {
 	return {
-		'account': 'btc:{}'.format(coinaddr) if non_mmgen else (u'{}:{}{}'.format(al_id,idx,lbl.decode('utf8'))),
+		'account': '{}:{}'.format(g.proto.base_coin.lower(),coinaddr) if non_mmgen \
+			else (u'{}:{}{}'.format(al_id,idx,lbl.decode('utf8'))),
 		'vout': int(getrandnum(4) % 8),
 		'vout': int(getrandnum(4) % 8),
 		'txid': hexlify(os.urandom(32)).decode('utf8'),
 		'txid': hexlify(os.urandom(32)).decode('utf8'),
-		'amount': BTCAmt('%s.%s' % (10+(getrandnum(4) % 40), getrandnum(4) % 100000000)),
+		'amount': g.proto.coin_amt('%s.%s' % (amt1+(getrandnum(4) % amt2), getrandnum(4) % 100000000)),
 		'address': coinaddr,
 		'address': coinaddr,
 		'spendable': False,
 		'spendable': False,
 		'scriptPubKey': '{}{}{}'.format(spk1,coinaddr.hex,spk2),
 		'scriptPubKey': '{}{}{}'.format(spk1,coinaddr.hex,spk2),
@@ -860,7 +978,7 @@ def create_fake_unspent_data(adata,tx_data,non_mmgen_input=''):
 		privkey = PrivKey(os.urandom(32),compressed=True)
 		privkey = PrivKey(os.urandom(32),compressed=True)
 		coinaddr = AddrGenerator('p2pkh').to_addr(KeyGenerator().to_pubhex(privkey))
 		coinaddr = AddrGenerator('p2pkh').to_addr(KeyGenerator().to_pubhex(privkey))
 		of = os.path.join(cfgs[non_mmgen_input]['tmpdir'],non_mmgen_fn)
 		of = os.path.join(cfgs[non_mmgen_input]['tmpdir'],non_mmgen_fn)
-		write_data_to_file(of,privkey.wif+'\n','compressed bitcoin key',silent=True)
+		write_data_to_file(of,privkey.wif+'\n','compressed {} key'.format(g.proto.name),silent=True)
 		out.append(create_fake_unspent_entry(coinaddr,non_mmgen=True,segwit=False))
 		out.append(create_fake_unspent_entry(coinaddr,non_mmgen=True,segwit=False))
 
 
 #	msg('\n'.join([repr(o) for o in out])); sys.exit(0)
 #	msg('\n'.join([repr(o) for o in out])); sys.exit(0)
@@ -901,6 +1019,14 @@ def make_txcreate_cmdline(tx_data):
 	t = ('p2pkh','segwit')['S' in g.proto.mmtypes]
 	t = ('p2pkh','segwit')['S' in g.proto.mmtypes]
 	coinaddr = AddrGenerator(t).to_addr(KeyGenerator().to_pubhex(privkey))
 	coinaddr = AddrGenerator(t).to_addr(KeyGenerator().to_pubhex(privkey))
 
 
+	# total of two outputs must be < 10 BTC (<1000 LTC)
+	mods = {'btc':(6,4),'bch':(6,4),'ltc':(600,400)}[g.coin.lower()]
+	for k in cfgs:
+		cfgs[k]['amts'] = [None,None]
+		for idx,mod in enumerate(mods):
+			cfgs[k]['amts'][idx] = '%s.%s' % ((getrandnum(4) % mod), str(getrandnum(4))[:5])
+
+
 	cmd_args = ['-d',cfg['tmpdir']]
 	cmd_args = ['-d',cfg['tmpdir']]
 	for num in tx_data:
 	for num in tx_data:
 		s = tx_data[num]
 		s = tx_data[num]
@@ -1104,10 +1230,11 @@ class MMGenTestSuite(object):
 	def helpscreens(self,name,arg='--help'):
 	def helpscreens(self,name,arg='--help'):
 		scripts = (
 		scripts = (
 			'walletgen','walletconv','walletchk','txcreate','txsign','txsend','txdo','txbump',
 			'walletgen','walletconv','walletchk','txcreate','txsign','txsend','txdo','txbump',
-			'addrgen','addrimport','keygen','passchg','tool','passgen')
+			'addrgen','addrimport','keygen','passchg','tool','passgen','regtest')
 		for s in scripts:
 		for s in scripts:
 			t = MMGenExpect(name,('mmgen-'+s),[arg],extra_desc='(mmgen-%s)'%s,no_output=True)
 			t = MMGenExpect(name,('mmgen-'+s),[arg],extra_desc='(mmgen-%s)'%s,no_output=True)
-			t.read(); t.ok()
+			t.read()
+			t.ok()
 
 
 	def longhelpscreens(self,name): self.helpscreens(name,arg='--longhelp')
 	def longhelpscreens(self,name): self.helpscreens(name,arg='--longhelp')
 
 
@@ -1208,7 +1335,7 @@ class MMGenTestSuite(object):
 		if opt.no_dw_delete: return True
 		if opt.no_dw_delete: return True
 		for wf in [f for f in os.listdir(g.data_dir) if f[-6:]=='.mmdat']:
 		for wf in [f for f in os.listdir(g.data_dir) if f[-6:]=='.mmdat']:
 			os.unlink(os.path.join(g.data_dir,wf))
 			os.unlink(os.path.join(g.data_dir,wf))
-		MMGenExpect(name,'')
+		MMGenExpect(name,'',msg_only=True)
 		global have_dfl_wallet
 		global have_dfl_wallet
 		have_dfl_wallet = False
 		have_dfl_wallet = False
 		ok()
 		ok()
@@ -1222,7 +1349,7 @@ class MMGenTestSuite(object):
 				([],['--type='+str(mmtype)])[bool(mmtype)] +
 				([],['--type='+str(mmtype)])[bool(mmtype)] +
 				([],[wf])[bool(wf)] +
 				([],[wf])[bool(wf)] +
 				([],[id_str])[bool(id_str)] +
 				([],[id_str])[bool(id_str)] +
-				[cfg['{}_idx_list'.format(cmd_pfx)]])
+				[cfg['{}_idx_list'.format(cmd_pfx)]],extra_desc=('','(segwit)')[mmtype=='segwit'])
 		t.license()
 		t.license()
 		t.passphrase('MMGen wallet',cfg['wpasswd'])
 		t.passphrase('MMGen wallet',cfg['wpasswd'])
 		t.expect('Passphrase is OK')
 		t.expect('Passphrase is OK')
@@ -1232,7 +1359,7 @@ class MMGenTestSuite(object):
 			k = 'passfile32_chk' if ftype == 'pass32' \
 			k = 'passfile32_chk' if ftype == 'pass32' \
 					else 'passfile_chk' if ftype == 'pass' \
 					else 'passfile_chk' if ftype == 'pass' \
 						else '{}file{}_chk'.format(ftype,'_'+mmtype if mmtype else '')
 						else '{}file{}_chk'.format(ftype,'_'+mmtype if mmtype else '')
-			chk_ref = cfg[k] if ftype[:4] == 'pass' else cfg[k][g.testnet]
+			chk_ref = cfg[k] if ftype[:4] == 'pass' else cfg[k][fork][g.testnet]
 			refcheck('address data checksum',chk,chk_ref)
 			refcheck('address data checksum',chk,chk_ref)
 			return
 			return
 		t.written_to_file('Addresses',oo=True)
 		t.written_to_file('Addresses',oo=True)
@@ -1245,6 +1372,8 @@ class MMGenTestSuite(object):
 		self.addrgen(name,wf,pf=pf,check_ref=True)
 		self.addrgen(name,wf,pf=pf,check_ref=True)
 
 
 	def refaddrgen_compressed(self,name,wf,pf):
 	def refaddrgen_compressed(self,name,wf,pf):
+		if opt.segwit:
+			msg('Skipping non-Segwit address generation'); return True
 		self.addrgen(name,wf,pf=pf,check_ref=True,mmtype='compressed')
 		self.addrgen(name,wf,pf=pf,check_ref=True,mmtype='compressed')
 
 
 	def addrimport(self,name,addrfile):
 	def addrimport(self,name,addrfile):
@@ -1269,7 +1398,10 @@ class MMGenTestSuite(object):
 
 
 		if opt.verbose or opt.exact_output: sys.stderr.write('\n')
 		if opt.verbose or opt.exact_output: sys.stderr.write('\n')
 
 
-		t = MMGenExpect(name,'mmgen-'+('txcreate','txdo')[bool(txdo_args)],['--rbf','-f',tx_fee] + add_args + cmd_args + txdo_args)
+		t = MMGenExpect(name,
+			'mmgen-'+('txcreate','txdo')[bool(txdo_args)],
+			([],['--rbf'])[g.proto.cap('rbf')] +
+			['-f',tx_fee] + add_args + cmd_args + txdo_args)
 		t.license()
 		t.license()
 
 
 		if txdo_args and add_args: # txdo4
 		if txdo_args and add_args: # txdo4
@@ -1284,10 +1416,10 @@ class MMGenTestSuite(object):
 
 
 		# not in tracking wallet warning, (1 + num sources) times
 		# not in tracking wallet warning, (1 + num sources) times
 		if t.expect(['Continue anyway? (y/N): ',
 		if t.expect(['Continue anyway? (y/N): ',
-				'Unable to connect to bitcoind']) == 0:
+				'Unable to connect to {}'.format(g.proto.daemon_name)]) == 0:
 			t.send('y')
 			t.send('y')
 		else:
 		else:
-			errmsg(red('Error: unable to connect to bitcoind.  Exiting'))
+			errmsg(red('Error: unable to connect to {}.  Exiting'.format(g.proto.daemon_name)))
 			sys.exit(1)
 			sys.exit(1)
 
 
 		for num in tx_data:
 		for num in tx_data:
@@ -1315,6 +1447,8 @@ class MMGenTestSuite(object):
 		self.txcreate_common(name,sources=['1'])
 		self.txcreate_common(name,sources=['1'])
 
 
 	def txbump(self,name,txfile,prepend_args=[],seed_args=[]):
 	def txbump(self,name,txfile,prepend_args=[],seed_args=[]):
+		if not g.proto.cap('rbf'):
+			msg('Skipping RBF'); return True
 		args = prepend_args + ['-q','-d',cfg['tmpdir'],txfile] + seed_args
 		args = prepend_args + ['-q','-d',cfg['tmpdir'],txfile] + seed_args
 		t = MMGenExpect(name,'mmgen-txbump',args)
 		t = MMGenExpect(name,'mmgen-txbump',args)
 		if seed_args:
 		if seed_args:
@@ -1324,7 +1458,7 @@ class MMGenTestSuite(object):
 		t.expect('deduct the fee from (Hit ENTER for the change output): ','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)
 		# Fee must be > tx_fee + network relay fee (currently 0.00001)
 		t.expect('OK? (Y/n): ','\n')
 		t.expect('OK? (Y/n): ','\n')
-		t.expect('Enter transaction fee: ','124s\n')
+		t.expect('Enter transaction fee: ',txbump_fee+'\n')
 		t.expect('OK? (Y/n): ','\n')
 		t.expect('OK? (Y/n): ','\n')
 		if seed_args: # sign and send
 		if seed_args: # sign and send
 			t.expect('Edit transaction comment? (y/N): ','\n')
 			t.expect('Edit transaction comment? (y/N): ','\n')
@@ -1497,13 +1631,13 @@ class MMGenTestSuite(object):
 		if cfg['segwit'] and not mmtype: mmtype = 'segwit'
 		if cfg['segwit'] and not mmtype: mmtype = 'segwit'
 		args = ['-d',cfg['tmpdir'],usr_rand_arg,wf,cfg['addr_idx_list']]
 		args = ['-d',cfg['tmpdir'],usr_rand_arg,wf,cfg['addr_idx_list']]
 		t = MMGenExpect(name,'mmgen-keygen',
 		t = MMGenExpect(name,'mmgen-keygen',
-				([],['--type='+str(mmtype)])[bool(mmtype)] + args)
+				([],['--type='+str(mmtype)])[bool(mmtype)] + args,extra_desc=('','(segwit)')[mmtype=='segwit'])
 		t.license()
 		t.license()
 		t.passphrase('MMGen wallet',cfg['wpasswd'])
 		t.passphrase('MMGen wallet',cfg['wpasswd'])
 		chk = t.expect_getend(r'Checksum for key-address data .*?: ',regex=True)
 		chk = t.expect_getend(r'Checksum for key-address data .*?: ',regex=True)
 		if check_ref:
 		if check_ref:
 			k = 'keyaddrfile{}_chk'.format('_'+mmtype if mmtype else '')
 			k = 'keyaddrfile{}_chk'.format('_'+mmtype if mmtype else '')
-			refcheck('key-address data checksum',chk,cfg[k][g.testnet])
+			refcheck('key-address data checksum',chk,cfg[k][fork][g.testnet])
 			return
 			return
 		t.expect('Encrypt key list? (y/N): ','y')
 		t.expect('Encrypt key list? (y/N): ','y')
 		t.usr_rand(usr_rand_chars)
 		t.usr_rand(usr_rand_chars)
@@ -1517,6 +1651,8 @@ class MMGenTestSuite(object):
 		self.keyaddrgen(name,wf,pf,check_ref=True)
 		self.keyaddrgen(name,wf,pf,check_ref=True)
 
 
 	def refkeyaddrgen_compressed(self,name,wf,pf):
 	def refkeyaddrgen_compressed(self,name,wf,pf):
+		if opt.segwit:
+			msg('Skipping non-Segwit key-address generation'); return True
 		self.keyaddrgen(name,wf,pf,check_ref=True,mmtype='compressed')
 		self.keyaddrgen(name,wf,pf,check_ref=True,mmtype='compressed')
 
 
 	def refpasswdgen(self,name,wf,pf):
 	def refpasswdgen(self,name,wf,pf):
@@ -1784,15 +1920,18 @@ class MMGenTestSuite(object):
 			cmp_or_die(cfg['seed_id'],chk)
 			cmp_or_die(cfg['seed_id'],chk)
 
 
 	def ref_addrfile_chk(self,name,ftype='addr'):
 	def ref_addrfile_chk(self,name,ftype='addr'):
-		wf = os.path.join(ref_dir,cfg['ref_'+ftype+'file'])
-		t = MMGenExpect(name,'mmgen-tool',[ftype.replace('segwit','')+'file_chksum',wf])
+		af_key = 'ref_{}file'.format(ftype)
+		af = os.path.join(ref_dir,(ref_subdir,'')[ftype=='passwd'],cfg[af_key])
+		t = MMGenExpect(name,'mmgen-tool',[ftype.replace('segwit','')+'file_chksum',af])
 		if ftype == 'keyaddr':
 		if ftype == 'keyaddr':
 			w = 'key-address data'
 			w = 'key-address data'
 			t.hash_preset(w,ref_kafile_hash_preset)
 			t.hash_preset(w,ref_kafile_hash_preset)
 			t.passphrase(w,ref_kafile_pass)
 			t.passphrase(w,ref_kafile_pass)
 			t.expect('Check key-to-address validity? (y/N): ','y')
 			t.expect('Check key-to-address validity? (y/N): ','y')
 		o = t.read().strip().split('\n')[-1]
 		o = t.read().strip().split('\n')[-1]
-		cmp_or_die(cfg['ref_'+ftype+'file_chksum'],o)
+		rc = cfg['ref_'+ftype+'file_chksum']
+		ref_chksum = rc if ftype == 'passwd' else rc[g.proto.base_coin.lower()][g.testnet]
+		cmp_or_die(ref_chksum,o)
 
 
 	def ref_keyaddrfile_chk(self,name):
 	def ref_keyaddrfile_chk(self,name):
 		self.ref_addrfile_chk(name,ftype='keyaddr')
 		self.ref_addrfile_chk(name,ftype='keyaddr')
@@ -1801,13 +1940,17 @@ class MMGenTestSuite(object):
 		self.ref_addrfile_chk(name,ftype='passwd')
 		self.ref_addrfile_chk(name,ftype='passwd')
 
 
 	def ref_segwitaddrfile_chk(self,name):
 	def ref_segwitaddrfile_chk(self,name):
-		self.ref_addrfile_chk(name,ftype='segwitaddr')
+		if not 'S' in g.proto.mmtypes:
+			msg_r('Skipping {} (not supported)'.format(name))
+			ok()
+		else:
+			self.ref_addrfile_chk(name,ftype='segwitaddr')
 
 
 #	def txcreate8(self,name,addrfile):
 #	def txcreate8(self,name,addrfile):
 #		self.txcreate_common(name,sources=['8'])
 #		self.txcreate_common(name,sources=['8'])
 
 
 	def ref_tx_chk(self,name):
 	def ref_tx_chk(self,name):
-		tf = os.path.join(ref_dir,cfg['ref_tx_file'])
+		tf = os.path.join(ref_dir,ref_subdir,cfg['ref_tx_file'][g.coin.lower()].format(tn_ext))
 		wf = os.path.join(ref_dir,cfg['ref_wallet'])
 		wf = os.path.join(ref_dir,cfg['ref_wallet'])
 		write_to_tmpfile(cfg,pwfile,cfg['wpasswd'])
 		write_to_tmpfile(cfg,pwfile,cfg['wpasswd'])
 		pf = get_tmpfile_fn(cfg,pwfile)
 		pf = get_tmpfile_fn(cfg,pwfile)
@@ -1885,6 +2028,8 @@ class MMGenTestSuite(object):
 			extra_desc='(check)')
 			extra_desc='(check)')
 
 
 	def regtest_setup(self,name):
 	def regtest_setup(self,name):
+		if g.testnet:
+			die(2,'--testnet option incompatible with regtest test suite')
 		try: shutil.rmtree(os.path.join(data_dir,'regtest'))
 		try: shutil.rmtree(os.path.join(data_dir,'regtest'))
 		except: pass
 		except: pass
 		os.environ['MMGEN_TEST_SUITE'] = '' # mnemonic is piped to stdin, so stop being a terminal
 		os.environ['MMGEN_TEST_SUITE'] = '' # mnemonic is piped to stdin, so stop being a terminal
@@ -1912,79 +2057,86 @@ class MMGenTestSuite(object):
 	def regtest_user_sid(self,user):
 	def regtest_user_sid(self,user):
 		return os.path.basename(get_file_with_ext('mmdat',self.regtest_user_dir(user)))[:8]
 		return os.path.basename(get_file_with_ext('mmdat',self.regtest_user_dir(user)))[:8]
 
 
-	def regtest_addrgen(self,name,user):
-		for mmtype in ('legacy','compressed','segwit'):
+	def regtest_addrgen(self,name,user,wf=None,passwd='abc',addr_range='1-5'):
+		from mmgen.addr import MMGenAddrType
+		for mmtype in g.proto.mmtypes:
 			t = MMGenExpect(name,'mmgen-addrgen',
 			t = MMGenExpect(name,'mmgen-addrgen',
-				['--quiet','--'+user,'--type='+mmtype,
-				'--outdir={}'.format(self.regtest_user_dir(user)),
-				'1-5'],extra_desc='({})'.format(mmtype))
-			t.passphrase('MMGen wallet','abc')
+				['--quiet','--'+user,'--type='+mmtype,'--outdir={}'.format(self.regtest_user_dir(user))] +
+				([],[wf])[bool(wf)] + [addr_range],
+				extra_desc='({})'.format(MMGenAddrType.mmtypes[mmtype]['name']))
+			t.passphrase('MMGen wallet',passwd)
 			t.written_to_file('Addresses')
 			t.written_to_file('Addresses')
 			t.ok()
 			t.ok()
 
 
 	def regtest_addrgen_bob(self,name):   self.regtest_addrgen(name,'bob')
 	def regtest_addrgen_bob(self,name):   self.regtest_addrgen(name,'bob')
 	def regtest_addrgen_alice(self,name): self.regtest_addrgen(name,'alice')
 	def regtest_addrgen_alice(self,name): self.regtest_addrgen(name,'alice')
 
 
-	def regtest_addrimport(self,name,user):
+	def regtest_addrimport(self,name,user,sid=None,addr_range='1-5',num_addrs=5):
 		id_strs = { 'legacy':'', 'compressed':'-C', 'segwit':'-S' }
 		id_strs = { 'legacy':'', 'compressed':'-C', 'segwit':'-S' }
-		sid = self.regtest_user_sid(user)
-		for desc in ('legacy','compressed','segwit'):
-			fn = os.path.join(self.regtest_user_dir(user),'{}{}[1-5].addrs'.format(sid,id_strs[desc]))
+		if not sid: sid = self.regtest_user_sid(user)
+		from mmgen.addr import MMGenAddrType
+		for mmtype in g.proto.mmtypes:
+			desc = MMGenAddrType.mmtypes[mmtype]['name']
+			fn = os.path.join(self.regtest_user_dir(user),
+				'{}{}{}[{}].addrs'.format(sid,altcoin_pfx,id_strs[desc],addr_range))
 			t = MMGenExpect(name,'mmgen-addrimport', ['--quiet','--'+user,'--batch',fn],extra_desc='('+desc+')')
 			t = MMGenExpect(name,'mmgen-addrimport', ['--quiet','--'+user,'--batch',fn],extra_desc='('+desc+')')
 			t.expect('Importing')
 			t.expect('Importing')
-			t.expect('5 addresses imported')
+			t.expect('{} addresses imported'.format(num_addrs))
 			t.ok()
 			t.ok()
 
 
 	def regtest_addrimport_bob(self,name):   self.regtest_addrimport(name,'bob')
 	def regtest_addrimport_bob(self,name):   self.regtest_addrimport(name,'bob')
 	def regtest_addrimport_alice(self,name): self.regtest_addrimport(name,'alice')
 	def regtest_addrimport_alice(self,name): self.regtest_addrimport(name,'alice')
 
 
-	def regtest_fund_wallet(self,name,user,mmtype,amt):
-		sid = self.regtest_user_sid(user)
-		addr = self.get_addr_from_regtest_addrlist(user,sid,mmtype,0)
+	def regtest_fund_wallet(self,name,user,mmtype,amt,sid=None,addr_range='1-5'):
+		if not sid: sid = self.regtest_user_sid(user)
+		addr = self.get_addr_from_regtest_addrlist(user,sid,mmtype,0,addr_range=addr_range)
 		t = MMGenExpect(name,'mmgen-regtest', ['send',str(addr),str(amt)])
 		t = MMGenExpect(name,'mmgen-regtest', ['send',str(addr),str(amt)])
-		t.expect('Sending {} BTC'.format(amt))
+		t.expect('Sending {} {}'.format(amt,g.coin))
 		t.expect('Mined 1 block')
 		t.expect('Mined 1 block')
 		t.ok()
 		t.ok()
 
 
-	def regtest_fund_bob(self,name):   self.regtest_fund_wallet(name,'bob','C',500)
-	def regtest_fund_alice(self,name): self.regtest_fund_wallet(name,'alice','S',500)
+	def regtest_fund_bob(self,name):   self.regtest_fund_wallet(name,'bob','C',rtFundAmt)
+	def regtest_fund_alice(self,name): self.regtest_fund_wallet(name,'alice',('L','S')[g.proto.cap('segwit')],rtFundAmt)
 
 
 	def regtest_user_bal(self,name,user,bal):
 	def regtest_user_bal(self,name,user,bal):
 		t = MMGenExpect(name,'mmgen-tool',['--'+user,'listaddresses','showempty=1'])
 		t = MMGenExpect(name,'mmgen-tool',['--'+user,'listaddresses','showempty=1'])
 		total = t.expect_getend('TOTAL: ')
 		total = t.expect_getend('TOTAL: ')
-		cmp_or_die(total,'{} BTC'.format(bal))
+		cmp_or_die(total,'{} {}'.format(bal,g.coin))
 
 
 	def regtest_alice_bal1(self,name):
 	def regtest_alice_bal1(self,name):
-		return self.regtest_user_bal(name,'alice','500')
+		return self.regtest_user_bal(name,'alice',rtFundAmt)
 
 
 	def regtest_bob_bal1(self,name):
 	def regtest_bob_bal1(self,name):
-		return self.regtest_user_bal(name,'bob','500')
+		return self.regtest_user_bal(name,'bob',rtFundAmt)
 
 
 	def regtest_bob_bal2(self,name):
 	def regtest_bob_bal2(self,name):
-		return self.regtest_user_bal(name,'bob','499.999942')
+		return self.regtest_user_bal(name,'bob',rtBals[0])
 
 
 	def regtest_bob_bal3(self,name):
 	def regtest_bob_bal3(self,name):
-		return self.regtest_user_bal(name,'bob','399.9998214')
+		return self.regtest_user_bal(name,'bob',rtBals[1])
 
 
 	def regtest_bob_bal4(self,name):
 	def regtest_bob_bal4(self,name):
-		return self.regtest_user_bal(name,'bob','399.9998079')
+		return self.regtest_user_bal(name,'bob',rtBals[2])
 
 
 	def regtest_bob_bal5(self,name):
 	def regtest_bob_bal5(self,name):
-		return self.regtest_user_bal(name,'bob','399.9996799')
+		return self.regtest_user_bal(name,'bob',rtBals[3])
 
 
 	def regtest_bob_alice_bal(self,name):
 	def regtest_bob_alice_bal(self,name):
 		t = MMGenExpect(name,'mmgen-regtest',['get_balances'])
 		t = MMGenExpect(name,'mmgen-regtest',['get_balances'])
 		t.expect('Switching')
 		t.expect('Switching')
 		ret = t.expect_getend("Bob's balance:").strip()
 		ret = t.expect_getend("Bob's balance:").strip()
-		cmp_or_die(ret,'13.00000000',skip_ok=True)
+		cmp_or_die(ret,rtBals[4],skip_ok=True)
 		ret = t.expect_getend("Alice's balance:").strip()
 		ret = t.expect_getend("Alice's balance:").strip()
-		cmp_or_die(ret,'986.99957990',skip_ok=True)
+		cmp_or_die(ret,rtBals[5],skip_ok=True)
+		ret = t.expect_getend("Total balance:").strip()
+		cmp_or_die(ret,rtBals[6],skip_ok=True)
 		t.ok()
 		t.ok()
 
 
-	def regtest_user_txdo(self,name,user,fee,outputs_cl,outputs_prompt,extra_args=[],no_send=False):
+	def regtest_user_txdo(self,name,user,fee,outputs_cl,outputs_prompt,extra_args=[],wf=None,pw='abc',no_send=False,do_label=False):
 		os.environ['MMGEN_BOGUS_SEND'] = ''
 		os.environ['MMGEN_BOGUS_SEND'] = ''
 		t = MMGenExpect(name,'mmgen-txdo',
 		t = MMGenExpect(name,'mmgen-txdo',
-			['-d',cfg['tmpdir'],'-B','--'+user,'--tx-fee='+fee] + extra_args + outputs_cl)
+			['-d',cfg['tmpdir'],'-B','--'+user,'--tx-fee='+fee]
+			+ extra_args + ([],[wf])[bool(wf)] + outputs_cl)
 		os.environ['MMGEN_BOGUS_SEND'] = '1'
 		os.environ['MMGEN_BOGUS_SEND'] = '1'
 
 
 		t.expect(r"'q'=quit view, .*?:.",'M',regex=True) # sort by mmid
 		t.expect(r"'q'=quit view, .*?:.",'M',regex=True) # sort by mmid
@@ -1992,10 +2144,15 @@ class MMGenTestSuite(object):
 		t.expect('outputs to spend: ',outputs_prompt+'\n')
 		t.expect('outputs to spend: ',outputs_prompt+'\n')
 		t.expect('OK? (Y/n): ','y') # fee OK?
 		t.expect('OK? (Y/n): ','y') # fee OK?
 		t.expect('OK? (Y/n): ','y') # change OK?
 		t.expect('OK? (Y/n): ','y') # change OK?
-		t.expect('Add a comment to transaction? (y/N): ','\n')
-		t.expect('View decoded transaction\? .*?: ','t',regex=True)
-		t.expect('to continue: ','\n')
-		t.passphrase('MMGen wallet','abc')
+		if do_label:
+			t.expect('Add a comment to transaction? (y/N): ','y')
+			t.expect('Comment: ',ref_tx_label.encode('utf8')+'\n')
+			t.expect('View decoded transaction\? .*?: ','n',regex=True)
+		else:
+			t.expect('Add a comment to transaction? (y/N): ','\n')
+			t.expect('View decoded transaction\? .*?: ','t',regex=True)
+			t.expect('to continue: ','\n')
+		t.passphrase('MMGen wallet',pw)
 		t.written_to_file('Signed transaction')
 		t.written_to_file('Signed transaction')
 		if not no_send:
 		if not no_send:
 			t.expect('to confirm: ','YES, I REALLY WANT TO DO THIS\n')
 			t.expect('to confirm: ','YES, I REALLY WANT TO DO THIS\n')
@@ -2005,16 +2162,18 @@ class MMGenTestSuite(object):
 
 
 	def regtest_bob_split1(self,name):
 	def regtest_bob_split1(self,name):
 		sid = self.regtest_user_sid('bob')
 		sid = self.regtest_user_sid('bob')
-		outputs_cl = [sid+':C:1,100', sid+':L:2,200',sid+':S:2']
-		return self.regtest_user_txdo(name,'bob','20s',outputs_cl,'1')
+		outputs_cl = [sid+':C:1,100', sid+':L:2,200',sid+':'+rtBobOp3]
+		return self.regtest_user_txdo(name,'bob',rtFee[0],outputs_cl,'1',do_label=True)
 
 
-	def get_addr_from_regtest_addrlist(self,user,sid,mmtype,idx):
+	def get_addr_from_regtest_addrlist(self,user,sid,mmtype,idx,addr_range='1-5'):
 		id_str = { 'L':'', 'S':'-S', 'C':'-C' }[mmtype]
 		id_str = { 'L':'', 'S':'-S', 'C':'-C' }[mmtype]
-		fn = get_file_with_ext('{}{}[1-5].addrs'.format(sid,id_str),self.regtest_user_dir(user),no_dot=True)
+		ext = '{}{}{}[{}].addrs'.format(sid,altcoin_pfx,id_str,addr_range)
+		fn = get_file_with_ext(ext,self.regtest_user_dir(user),no_dot=True)
 		silence()
 		silence()
-		g.proto = get_coin_protocol(g.coin,True)
+		psave = g.proto
+		g.proto = CoinProtocol(g.coin,True)
 		addr = AddrList(fn).data[idx].addr
 		addr = AddrList(fn).data[idx].addr
-		g.proto = get_coin_protocol(g.coin,g.testnet)
+		g.proto = psave
 		end_silence()
 		end_silence()
 		return addr
 		return addr
 
 
@@ -2024,15 +2183,21 @@ class MMGenTestSuite(object):
 
 
 	def regtest_bob_rbf_send(self,name):
 	def regtest_bob_rbf_send(self,name):
 		outputs_cl = self.create_tx_outputs('alice',(('L',1,',60'),('C',1,',40'))) # alice_sid:L:1, alice_sid:C:1
 		outputs_cl = self.create_tx_outputs('alice',(('L',1,',60'),('C',1,',40'))) # alice_sid:L:1, alice_sid:C:1
-		outputs_cl += [self.regtest_user_sid('bob')+':S:2']
-		return self.regtest_user_txdo(name,'bob','10s',outputs_cl,'3',extra_args=['--rbf'])
+		outputs_cl += [self.regtest_user_sid('bob')+':'+rtBobOp3]
+		return self.regtest_user_txdo(name,'bob',rtFee[1],outputs_cl,'3',
+					extra_args=([],['--rbf'])[g.proto.cap('rbf')])
 
 
 	def regtest_bob_send_non_mmgen(self,name):
 	def regtest_bob_send_non_mmgen(self,name):
-		outputs_cl = self.create_tx_outputs('alice',(('S',2,',10'),('S',3,''))) # alice_sid:S:2, alice_sid:S:3
+		outputs_cl = self.create_tx_outputs('alice',(
+			(('L','S')[g.proto.cap('segwit')],2,',10'),
+			(('L','S')[g.proto.cap('segwit')],3,'')
+		)) # alice_sid:S:2, alice_sid:S:3
 		fn = os.path.join(cfg['tmpdir'],'non-mmgen.keys')
 		fn = os.path.join(cfg['tmpdir'],'non-mmgen.keys')
-		return self.regtest_user_txdo(name,'bob','0.0001',outputs_cl,'3-9',extra_args=['--keys-from-file='+fn])
+		return self.regtest_user_txdo(name,'bob',rtFee[3],outputs_cl,'3-9',extra_args=['--keys-from-file='+fn])
 
 
 	def regtest_user_txbump(self,name,user,txfile,fee,red_op,no_send=False):
 	def regtest_user_txbump(self,name,user,txfile,fee,red_op,no_send=False):
+		if not g.proto.cap('rbf'):
+			msg('Skipping RBF'); return True
 		os.environ['MMGEN_BOGUS_SEND'] = ''
 		os.environ['MMGEN_BOGUS_SEND'] = ''
 		t = MMGenExpect(name,'mmgen-txbump',
 		t = MMGenExpect(name,'mmgen-txbump',
 			['-d',cfg['tmpdir'],'--send','--'+user,'--tx-fee='+fee,'--output-to-reduce='+red_op] + [txfile])
 			['-d',cfg['tmpdir'],'--send','--'+user,'--tx-fee='+fee,'--output-to-reduce='+red_op] + [txfile])
@@ -2050,8 +2215,8 @@ class MMGenTestSuite(object):
 		t.ok()
 		t.ok()
 
 
 	def regtest_bob_rbf_bump(self,name):
 	def regtest_bob_rbf_bump(self,name):
-		txfile = get_file_with_ext(',10].sigtx',cfg['tmpdir'],delete=False,no_dot=True)
-		return self.regtest_user_txbump(name,'bob',txfile,'60s','c')
+		txfile = get_file_with_ext(',{}].sigtx'.format(rtFee[1][:-1]),cfg['tmpdir'],delete=False,no_dot=True)
+		return self.regtest_user_txbump(name,'bob',txfile,rtFee[2],'c')
 
 
 	def regtest_generate(self,name):
 	def regtest_generate(self,name):
 		t = MMGenExpect(name,'mmgen-regtest',['generate'])
 		t = MMGenExpect(name,'mmgen-regtest',['generate'])
@@ -2070,6 +2235,8 @@ class MMGenTestSuite(object):
 		ok()
 		ok()
 
 
 	def regtest_get_mempool2(self,name):
 	def regtest_get_mempool2(self,name):
+		if not g.proto.cap('rbf'):
+			msg('Skipping post-RBF mempool check'); return True
 		mp = self.regtest_get_mempool(name)
 		mp = self.regtest_get_mempool(name)
 		if len(mp) != 1:
 		if len(mp) != 1:
 			rdie(2,'Mempool has more or less than one TX!')
 			rdie(2,'Mempool has more or less than one TX!')
@@ -2081,14 +2248,14 @@ class MMGenTestSuite(object):
 	@staticmethod
 	@staticmethod
 	def gen_pairs(n):
 	def gen_pairs(n):
 		return [subprocess.check_output(
 		return [subprocess.check_output(
-					['python','mmgen-tool','--testnet=1','-r0','randpair','compressed={}'.format((i+1)%2)]).split()
+					['python',os.path.join('cmds','mmgen-tool'),'--testnet=1','-r0','randpair','compressed={}'.format((i+1)%2)]).split()
 						for i in range(n)]
 						for i in range(n)]
 
 
 	def regtest_bob_pre_import(self,name):
 	def regtest_bob_pre_import(self,name):
 		pairs = self.gen_pairs(5)
 		pairs = self.gen_pairs(5)
 		write_to_tmpfile(cfg,'non-mmgen.keys','\n'.join([a[0] for a in pairs])+'\n')
 		write_to_tmpfile(cfg,'non-mmgen.keys','\n'.join([a[0] for a in pairs])+'\n')
 		write_to_tmpfile(cfg,'non-mmgen.addrs','\n'.join([a[1] for a in pairs])+'\n')
 		write_to_tmpfile(cfg,'non-mmgen.addrs','\n'.join([a[1] for a in pairs])+'\n')
-		return self.regtest_user_txdo(name,'bob','10s',[pairs[0][1]],'3')
+		return self.regtest_user_txdo(name,'bob',rtFee[4],[pairs[0][1]],'3')
 
 
 	def regtest_user_import(self,name,user,args):
 	def regtest_user_import(self,name,user,args):
 		t = MMGenExpect(name,'mmgen-addrimport',['--quiet','--'+user]+args)
 		t = MMGenExpect(name,'mmgen-addrimport',['--quiet','--'+user]+args)
@@ -2109,8 +2276,8 @@ class MMGenTestSuite(object):
 		amts = (a for a in (1.12345678,2.87654321,3.33443344,4.00990099,5.43214321))
 		amts = (a for a in (1.12345678,2.87654321,3.33443344,4.00990099,5.43214321))
 		outputs1 = ['{},{}'.format(a,amts.next()) for a in addrs]
 		outputs1 = ['{},{}'.format(a,amts.next()) for a in addrs]
 		sid = self.regtest_user_sid('bob')
 		sid = self.regtest_user_sid('bob')
-		outputs2 = [sid+':C:2,6', sid+':L:3,7',sid+':S:3']
-		return self.regtest_user_txdo(name,'bob','20s',outputs1+outputs2,'1-2')
+		outputs2 = [sid+':C:2,6', sid+':L:3,7',sid+(':L:1',':S:3')[g.proto.cap('segwit')]]
+		return self.regtest_user_txdo(name,'bob',rtFee[5],outputs1+outputs2,'1-2')
 
 
 	def regtest_user_add_label(self,name,user,addr,label):
 	def regtest_user_add_label(self,name,user,addr,label):
 		t = MMGenExpect(name,'mmgen-tool',['--'+user,'add_label',addr,label])
 		t = MMGenExpect(name,'mmgen-tool',['--'+user,'add_label',addr,label])
@@ -2124,15 +2291,15 @@ class MMGenTestSuite(object):
 
 
 	def regtest_alice_add_label1(self,name):
 	def regtest_alice_add_label1(self,name):
 		sid = self.regtest_user_sid('alice')
 		sid = self.regtest_user_sid('alice')
-		return self.regtest_user_add_label(name,'alice',sid+':S:1','Original Label')
+		return self.regtest_user_add_label(name,'alice',sid+':C:1','Original Label')
 
 
 	def regtest_alice_add_label2(self,name):
 	def regtest_alice_add_label2(self,name):
 		sid = self.regtest_user_sid('alice')
 		sid = self.regtest_user_sid('alice')
-		return self.regtest_user_add_label(name,'alice',sid+':S:1','Replacement Label')
+		return self.regtest_user_add_label(name,'alice',sid+':C:1','Replacement Label')
 
 
 	def regtest_alice_remove_label1(self,name):
 	def regtest_alice_remove_label1(self,name):
 		sid = self.regtest_user_sid('alice')
 		sid = self.regtest_user_sid('alice')
-		return self.regtest_user_remove_label(name,'alice',sid+':S:1')
+		return self.regtest_user_remove_label(name,'alice',sid+':C:1')
 
 
 	def regtest_user_chk_label(self,name,user,addr,label):
 	def regtest_user_chk_label(self,name,user,addr,label):
 		t = MMGenExpect(name,'mmgen-tool',['--'+user,'listaddresses','all_labels=1'])
 		t = MMGenExpect(name,'mmgen-tool',['--'+user,'listaddresses','all_labels=1'])
@@ -2141,19 +2308,19 @@ class MMGenTestSuite(object):
 
 
 	def regtest_alice_chk_label1(self,name):
 	def regtest_alice_chk_label1(self,name):
 		sid = self.regtest_user_sid('alice')
 		sid = self.regtest_user_sid('alice')
-		return self.regtest_user_chk_label(name,'alice',sid+':S:1','Original Label')
+		return self.regtest_user_chk_label(name,'alice',sid+':C:1','Original Label')
 
 
 	def regtest_alice_chk_label2(self,name):
 	def regtest_alice_chk_label2(self,name):
 		sid = self.regtest_user_sid('alice')
 		sid = self.regtest_user_sid('alice')
-		return self.regtest_user_chk_label(name,'alice',sid+':S:1','Replacement Label')
+		return self.regtest_user_chk_label(name,'alice',sid+':C:1','Replacement Label')
 
 
 	def regtest_alice_chk_label3(self,name):
 	def regtest_alice_chk_label3(self,name):
 		sid = self.regtest_user_sid('alice')
 		sid = self.regtest_user_sid('alice')
-		return self.regtest_user_chk_label(name,'alice',sid+':S:1','Edited Label')
+		return self.regtest_user_chk_label(name,'alice',sid+':C:1','Edited Label')
 
 
 	def regtest_alice_chk_label4(self,name):
 	def regtest_alice_chk_label4(self,name):
 		sid = self.regtest_user_sid('alice')
 		sid = self.regtest_user_sid('alice')
-		return self.regtest_user_chk_label(name,'alice',sid+':S:1','-')
+		return self.regtest_user_chk_label(name,'alice',sid+':C:1','-')
 
 
 	def regtest_user_edit_label(self,name,user,output,label):
 	def regtest_user_edit_label(self,name,user,output,label):
 		t = MMGenExpect(name,'mmgen-txcreate',['-B','--'+user,'-i'])
 		t = MMGenExpect(name,'mmgen-txcreate',['-B','--'+user,'-i'])
@@ -2165,12 +2332,99 @@ class MMGenTestSuite(object):
 		t.ok()
 		t.ok()
 
 
 	def regtest_alice_edit_label1(self,name):
 	def regtest_alice_edit_label1(self,name):
-		return self.regtest_user_edit_label(name,'alice','3','Edited Label')
+		return self.regtest_user_edit_label(name,'alice','1','Edited Label')
 
 
 	def regtest_stop(self,name):
 	def regtest_stop(self,name):
 		t = MMGenExpect(name,'mmgen-regtest',['stop'])
 		t = MMGenExpect(name,'mmgen-regtest',['stop'])
 		t.ok()
 		t.ok()
 
 
+	# regtest undocumented admin commands
+	ref_tx_setup = regtest_setup
+	ref_tx_generate = regtest_generate
+
+	def ref_tx_addrgen_bob_ref_wallet(self,name):
+		wf = os.path.join(ref_dir,cfg['ref_wallet'])
+		self.regtest_addrgen(name,'bob',wf=wf,passwd=cfg['wpasswd'],addr_range='1-100')
+
+	def ref_tx_addrimport_bob_ref_wallet(self,name):
+		self.regtest_addrimport(name,'bob',sid=cfg['seed_id'],addr_range='1-100',num_addrs=100)
+
+	def ref_tx_fund_bob(self,name):
+		mmtype = g.proto.mmtypes[-1]
+		self.regtest_fund_wallet(name,'bob',mmtype,rtFundAmt,sid=cfg['seed_id'],addr_range='1-100')
+
+	def ref_tx_bob_split(self,name):
+		sid = cfg['seed_id']
+		outputs_cl = [sid+':C:1,100', sid+':L:2,200',sid+':'+rtBobOp3]
+		wf = os.path.join(ref_dir,cfg['ref_wallet'])
+		return self.regtest_user_txdo(name,'bob',rtFee[0],outputs_cl,'1',do_label=True,wf=wf,pw=cfg['wpasswd'])
+
+	def ref_tx_bob_create_tx(self,name):
+		sid = cfg['seed_id']
+		psave = g.proto
+		g.proto = CoinProtocol(g.coin,True)
+		privhex = PrivKey(os.urandom(32),compressed=True)
+		addr = AddrGenerator('p2pkh').to_addr(KeyGenerator().to_pubhex(privhex))
+		g.proto = psave
+		outputs_cl = [sid+':{}:3,1.1234'.format(g.proto.mmtypes[-1]), sid+':C:5,5.5555',sid+':L:4',addr+',100']
+		pw = cfg['wpasswd']
+		# create tx in cwd
+		t = MMGenExpect(name,'mmgen-txcreate',['-B','--bob','--tx-fee='+rtFee[0]] + outputs_cl)
+#			[os.path.join(ref_dir,cfg['ref_wallet'])])
+		t.expect(r"'q'=quit view, .*?:.",'M',regex=True) # sort by mmid
+		t.expect(r"'q'=quit view, .*?:.",'q',regex=True)
+		t.expect('outputs to spend: ','1 2 3\n')
+		t.expect('OK? (Y/n): ','y') # fee OK?
+		t.expect('OK? (Y/n): ','y') # change OK?
+		t.expect('Add a comment to transaction? (y/N): ','y')
+		t.expect('Comment: ',ref_tx_label.encode('utf8')+'\n')
+		t.expect('View decoded transaction\? .*?: ','n',regex=True)
+		t.expect('Save transaction? (y/N): ','y')
+		fn = t.written_to_file('Transaction')
+		write_to_tmpfile(cfg,'ref_tx_fn',fn)
+		t.ok()
+
+	def ref_tx_bob_modify_tx(self,name):
+		MMGenExpect(name,'',msg_only=True)
+		fn = read_from_tmpfile(cfg,'ref_tx_fn')
+		with open(fn) as f:
+			lines = f.read().splitlines()
+
+		from mmgen.obj import BTCAmt,LTCAmt,BCHAmt
+		tx = {}
+		for k,i in (('in',3),('out',4)):
+			tx[k] = eval(lines[i])
+			tx[k+'_addrs'] = [i['addr'] for i in tx[k]]
+
+		psave = g.proto
+		g.proto = CoinProtocol(g.coin,True)
+		from mmgen.obj import CoinAddr
+		for k in ('in_addrs','out_addrs'):
+			tx[k+'_hex'] = [(CoinAddr(a).hex,CoinAddr(a).addr_fmt) for a in tx[k]]
+		g.proto = psave
+
+		for k in ('in_addrs','out_addrs'):
+			tx[k+'_conv'] = [g.proto.hexaddr2addr(h,(False,True)[f=='p2sh']) for h,f in tx[k+'_hex']]
+
+		for k in ('in','out'):
+			for i in range(len(tx[k])):
+				tx[k][i]['addr'] = tx[k+'_addrs_conv'][i]
+
+		lines_tn = [lines[1].replace('REGTEST','TESTNET')] + lines[2:]
+		o_tn = '\n'.join([make_chksum_6(' '.join(lines_tn))] + lines_tn)+'\n'
+		fn_tn = fn.replace('.rawtx','.testnet.rawtx')
+
+		lines_mn = [lines[1].replace('REGTEST','MAINNET'),
+					lines[2],
+					repr(tx['in']),
+					repr(tx['out'])] + lines[5:]
+		o_mn = '\n'.join([make_chksum_6(' '.join(lines_mn))] + lines_mn)+'\n'
+		fn_mn = fn.replace('.rawtx','.mainnet.rawtx')
+		ok()
+
+		write_data_to_file(fn_tn,o_tn,'testnet TX data',ask_overwrite=False)
+		write_data_to_file(fn_mn,o_mn,'mainnet TX data',ask_overwrite=False)
+
 	# END methods
 	# END methods
 	for k in (
 	for k in (
 			'ref_wallet_conv',
 			'ref_wallet_conv',
@@ -2244,6 +2498,11 @@ def end_msg():
 
 
 ts = MMGenTestSuite()
 ts = MMGenTestSuite()
 
 
+if cmd_args and cmd_args[0] == 'admin':
+	cmd_args.pop(0)
+	cmd_data = cmd_data_admin
+	cmd_list = cmd_list_admin
+
 try:
 try:
 	if cmd_args:
 	if cmd_args:
 		for arg in cmd_args:
 		for arg in cmd_args:

+ 47 - 25
test/tooltest.py

@@ -21,9 +21,9 @@ test/tooltest.py:  Tests for the 'mmgen-tool' utility
 """
 """
 
 
 import sys,os,subprocess
 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))
+repo_root = os.path.normpath(os.path.abspath(os.path.join(os.path.dirname(sys.argv[0]),os.pardir)))
+os.chdir(repo_root)
+sys.path.__setitem__(0,repo_root)
 
 
 # Import this _after_ local path's been added to sys.path
 # Import this _after_ local path's been added to sys.path
 from mmgen.common import *
 from mmgen.common import *
@@ -54,8 +54,8 @@ cmd_data = OrderedDict([
 			])
 			])
 		}
 		}
 	),
 	),
-	('bitcoin', {
-			'desc': 'Bitcoin address/key commands',
+	('cryptocoin', {
+			'desc': 'Cryptocoin address/key commands',
 			'cmd_data': OrderedDict([
 			'cmd_data': OrderedDict([
 				('Randwif',        ()),
 				('Randwif',        ()),
 				('Randpair',       ()), # create 3 pairs: uncomp,comp,segwit
 				('Randpair',       ()), # create 3 pairs: uncomp,comp,segwit
@@ -91,7 +91,7 @@ cmd_data = OrderedDict([
 		}
 		}
 	),
 	),
 	('rpc', {
 	('rpc', {
-			'desc': 'Bitcoind RPC commands',
+			'desc': 'Coin daemon RPC commands',
 			'cmd_data': OrderedDict([
 			'cmd_data': OrderedDict([
 #				('keyaddrfile_chksum', ()), # interactive
 #				('keyaddrfile_chksum', ()), # interactive
 				('Addrfile_chksum', ()),
 				('Addrfile_chksum', ()),
@@ -110,9 +110,16 @@ cfg = {
 	'tmpdir':        'test/tmp10',
 	'tmpdir':        'test/tmp10',
 	'tmpdir_num':    10,
 	'tmpdir_num':    10,
 	'refdir':        'test/ref',
 	'refdir':        'test/ref',
-	'txfile':        'FFB367[1.234]{}.rawtx',
-	'addrfile':      '98831F3A[1,31-33,500-501,1010-1011]{}.addrs',
-	'addrfile_chk':  ('6FEF 6FB9 7B13 5D91','3C2C 8558 BB54 079E'),
+	'txfile': {
+		'btc': 'FFB367[1.234]{}.rawtx',
+		'bch': '99BE60-BCH[106.6789]{}.rawtx',
+		'ltc': '75F455-LTC[106.6789]{}.rawtx',
+	},
+	'addrfile': '98831F3A{}[1,31-33,500-501,1010-1011]{}.addrs',
+	'addrfile_chk':  {
+		'btc': ('6FEF 6FB9 7B13 5D91','3C2C 8558 BB54 079E'),
+		'ltc': ('AD52 C3FE 8924 AAF0','5738 5C4F 167C F9AE'),
+	}
 }
 }
 
 
 opts_data = lambda: {
 opts_data = lambda: {
@@ -136,14 +143,22 @@ If no command is given, the whole suite of tests is run.
 sys.argv = [sys.argv[0]] + ['--skip-cfg-file'] + sys.argv[1:]
 sys.argv = [sys.argv[0]] + ['--skip-cfg-file'] + sys.argv[1:]
 
 
 cmd_args = opts.init(opts_data,add_opts=['exact_output','profile'])
 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','regtest') if getattr(opt,k)]).split()
-add_spawn_args += [ '--data-dir', cfg['tmpdir']] # ignore ~/.mmgen
 
 
-if opt.system: sys.path.pop(0)
+ref_subdir  = '' if g.proto.base_coin == 'BTC' else g.proto.name
+altcoin_pfx = '' if g.proto.base_coin == 'BTC' else '-'+g.proto.base_coin
+tn_ext = ('','.testnet')[g.testnet]
+
+mmgen_cmd = 'mmgen-tool'
+
+if not opt.system:
+	os.environ['PYTHONPATH'] = repo_root
+	mmgen_cmd = os.path.relpath(os.path.join(repo_root,'cmds',mmgen_cmd))
+
+spawn_cmd = ([],['python'])[g.platform == 'win'] + [mmgen_cmd]
+
+add_spawn_args = ['--data-dir='+cfg['tmpdir']] + ['--{}{}'.format(
+		k.replace('_','-'),'='+getattr(opt,k) if getattr(opt,k) != True else '')
+			for k in ('testnet','rpc_host','regtest','coin') if getattr(opt,k)]
 
 
 if opt.list_cmds:
 if opt.list_cmds:
 	fs = '  {:<{w}} - {}'
 	fs = '  {:<{w}} - {}'
@@ -320,7 +335,7 @@ class MMGenToolTestSuite(object):
 		d = read_from_tmpfile(cfg,of,binary=True)
 		d = read_from_tmpfile(cfg,of,binary=True)
 		cmp_or_die(dlen,len(d))
 		cmp_or_die(dlen,len(d))
 
 
-	# Bitcoin
+	# Cryptocoin
 	def Randwif(self,name):
 	def Randwif(self,name):
 		for n,k in enumerate(['','compressed=1']):
 		for n,k in enumerate(['','compressed=1']):
 			ret = self.run_cmd_out(name,kwargs=k,Return=True,fn_idx=n+1)
 			ret = self.run_cmd_out(name,kwargs=k,Return=True,fn_idx=n+1)
@@ -329,7 +344,7 @@ class MMGenToolTestSuite(object):
 		for n,k in enumerate(['','compressed=1','segwit=1 compressed=1']):
 		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()
 			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(wif,is_wif,'WIF key',skip_ok=True)
-			ok_or_die(addr,is_coin_addr,'Bitcoin address')
+			ok_or_die(addr,is_coin_addr,'Coin address')
 	def Wif2addr(self,name,f1,f2,f3):
 	def Wif2addr(self,name,f1,f2,f3):
 		for n,f,k,m in ((1,f1,'',''),(2,f2,'','compressed'),(3,f3,'segwit=1','compressed')):
 		for n,f,k,m in ((1,f1,'',''),(2,f2,'','compressed'),(3,f3,'segwit=1','compressed')):
 			wif = read_from_file(f).split()[0]
 			wif = read_from_file(f).split()[0]
@@ -379,15 +394,22 @@ class MMGenToolTestSuite(object):
 		self.run_cmd_out(name,addr,fn_idx=3)
 		self.run_cmd_out(name,addr,fn_idx=3)
 
 
 	def Pipetest(self,name,f1,f2,f3):
 	def Pipetest(self,name,f1,f2,f3):
-		test_msg('command piping')
 		wif = read_from_file(f3).split()[0]
 		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))
+		cmd = ( '{c} {a} wif2hex {wif} | ' +
+				'{c} {a} privhex2pubhex - compressed=1 | ' +
+				'{c} {a} pubhex2redeem_script - | ' +
+				'{c} {a} pubhex2addr - p2sh=1').format(
+					c=' '.join(spawn_cmd),
+					a=' '.join(add_spawn_args),
+					wif=wif)
+		test_msg('command piping')
+		if opt.verbose:
+			sys.stderr.write(green('Executing ') + cyan(cmd) + '\n')
 		p = subprocess.Popen(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE,shell=True)
 		p = subprocess.Popen(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE,shell=True)
 		res = p.stdout.read().strip()
 		res = p.stdout.read().strip()
 		addr = read_from_tmpfile(cfg,'Wif2addr3.out').strip()
 		addr = read_from_tmpfile(cfg,'Wif2addr3.out').strip()
 		cmp_or_die(res,addr)
 		cmp_or_die(res,addr)
 
 
-
 	# Mnemonic
 	# Mnemonic
 	def Hex2mn(self,name):
 	def Hex2mn(self,name):
 		for n,size,m in ((1,16,'128-bit'),(2,24,'192-bit'),(3,32,'256-bit')):
 		for n,size,m in ((1,16,'128-bit'),(2,24,'192-bit'),(3,32,'256-bit')):
@@ -395,7 +417,7 @@ class MMGenToolTestSuite(object):
 			self.run_cmd_out(name,hexnum,fn_idx=n,extra_msg=m)
 			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')):
 		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)
+			self.run_cmd_chk(name,f_i,f_o,extra_msg=m,strip_hex=True)
 	def Mn_rand128(self,name): self.run_cmd_out(name)
 	def Mn_rand128(self,name): self.run_cmd_out(name)
 	def Mn_rand192(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_rand256(self,name): self.run_cmd_out(name)
@@ -406,8 +428,8 @@ class MMGenToolTestSuite(object):
 
 
 	# RPC
 	# RPC
 	def Addrfile_chksum(self,name):
 	def Addrfile_chksum(self,name):
-		fn = os.path.join(cfg['refdir'],cfg['addrfile'].format(('','.testnet')[g.testnet]))
-		self.run_cmd_out(name,fn,literal=True,chkdata=cfg['addrfile_chk'][g.testnet])
+		fn = os.path.join(cfg['refdir'],ref_subdir,cfg['addrfile'].format(altcoin_pfx,tn_ext))
+		self.run_cmd_out(name,fn,literal=True,chkdata=cfg['addrfile_chk'][g.coin.lower()][g.testnet])
 	def Getbalance(self,name):
 	def Getbalance(self,name):
 		self.run_cmd_out(name,literal=True)
 		self.run_cmd_out(name,literal=True)
 	def Listaddresses(self,name):
 	def Listaddresses(self,name):
@@ -415,7 +437,7 @@ class MMGenToolTestSuite(object):
 	def Twview(self,name):
 	def Twview(self,name):
 		self.run_cmd_out(name,literal=True)
 		self.run_cmd_out(name,literal=True)
 	def Txview(self,name):
 	def Txview(self,name):
-		fn = os.path.join(cfg['refdir'],cfg['txfile'].format(('','.testnet')[g.testnet]))
+		fn = os.path.join(cfg['refdir'],ref_subdir,cfg['txfile'][g.coin.lower()].format(tn_ext))
 		self.run_cmd_out(name,fn,literal=True)
 		self.run_cmd_out(name,fn,literal=True)
 
 
 # main()
 # main()