Browse Source

eliminate use of global vars g.proto, g.coin, g.rpc and others

This patch eliminates nearly all the global variables that changed during the
execution of scripts.  With a few minor exceptions, global vars are now used
only during initialization or reserved for cfg file / cmdline options and other
unchanging values.

The result is a code base that's much more maintainable and extensible and less
error-prone.  The autosigning code, which supports signing of transactions for
multiple protocols and networks, has been greatly simplified.

Doing away with globals required many changes throughout the code base, and
other related (and not so related) changes and cleanups were made along the
way, resulting in an enormous patch.

Additional code changes include:

    - tx.py: complete reorganization of TX classes and use of nesting

    - protocol.py: separation of Regtest and Testnet into distinct subclasses
      with separate address and transaction files and file extensions

    - new module help.py for the help notes, loaded on demand

    - addr.py: rewrite of the address file label parsing code

    - tx.py,tw.py: use of generators to create formatted text

User-visible changes include:

    - importing of addresses for tokens not yet in the user's tracking wallet
      is now performed with the `--token-addr` option instead of `--token`

Testing:

    Testing this patch requires a full run of the test suite as described on the
    Test-Suite wiki page.
The MMGen Project 4 years ago
parent
commit
c3f185e8
63 changed files with 4295 additions and 3616 deletions
  1. 194 144
      mmgen/addr.py
  2. 21 18
      mmgen/altcoins/eth/contract.py
  3. 56 62
      mmgen/altcoins/eth/tw.py
  4. 494 483
      mmgen/altcoins/eth/tx.py
  5. 0 214
      mmgen/common.py
  6. 29 21
      mmgen/daemon.py
  7. 2 0
      mmgen/exception.py
  8. 1 12
      mmgen/globalvars.py
  9. 256 0
      mmgen/help.py
  10. 23 7
      mmgen/main_addrgen.py
  11. 27 23
      mmgen/main_addrimport.py
  12. 21 46
      mmgen/main_autosign.py
  13. 6 1
      mmgen/main_passgen.py
  14. 1 1
      mmgen/main_seedjoin.py
  15. 17 10
      mmgen/main_split.py
  16. 36 19
      mmgen/main_txbump.py
  17. 23 6
      mmgen/main_txcreate.py
  18. 28 16
      mmgen/main_txdo.py
  19. 12 13
      mmgen/main_txsend.py
  20. 26 25
      mmgen/main_txsign.py
  21. 1 1
      mmgen/main_wallet.py
  22. 78 73
      mmgen/obj.py
  23. 68 54
      mmgen/opts.py
  24. 106 27
      mmgen/protocol.py
  25. 9 8
      mmgen/regtest.py
  26. 97 56
      mmgen/rpc.py
  27. 34 13
      mmgen/share/Opts.py
  28. 74 47
      mmgen/tool.py
  29. 340 267
      mmgen/tw.py
  30. 1290 1223
      mmgen/tx.py
  31. 36 38
      mmgen/txfile.py
  32. 38 41
      mmgen/txsign.py
  33. 26 51
      mmgen/util.py
  34. 6 2
      scripts/create-token.py
  35. 0 60
      scripts/tx-btc2bch.py
  36. 1 0
      setup.py
  37. 39 33
      test/gentest.py
  38. 3 0
      test/include/common.py
  39. 5 5
      test/objattrtest.py
  40. 16 14
      test/objattrtest_py_d/oat_btc_mainnet.py
  41. 16 5
      test/objtest.py
  42. 128 38
      test/objtest_py_d/ot_btc_mainnet.py
  43. 43 19
      test/objtest_py_d/ot_btc_testnet.py
  44. 0 1
      test/objtest_py_d/ot_common.py
  45. 43 19
      test/objtest_py_d/ot_ltc_mainnet.py
  46. 43 19
      test/objtest_py_d/ot_ltc_testnet.py
  47. 28 14
      test/test.py
  48. 0 1
      test/test_py_d/ts_autosign.py
  49. 5 3
      test/test_py_d/ts_base.py
  50. 9 8
      test/test_py_d/ts_chainsplit.py
  51. 112 69
      test/test_py_d/ts_ethdev.py
  52. 48 39
      test/test_py_d/ts_main.py
  53. 3 3
      test/test_py_d/ts_misc.py
  54. 16 11
      test/test_py_d/ts_ref.py
  55. 33 35
      test/test_py_d/ts_ref_altcoin.py
  56. 68 78
      test/test_py_d/ts_regtest.py
  57. 11 9
      test/test_py_d/ts_shared.py
  58. 1 1
      test/test_py_d/ts_wallet.py
  59. 20 13
      test/tooltest.py
  60. 31 15
      test/tooltest2.py
  61. 51 62
      test/unit_tests_d/ut_rpc.py
  62. 29 7
      test/unit_tests_d/ut_tx.py
  63. 17 13
      test/unit_tests_d/ut_tx_deserialize.py

+ 194 - 144
mmgen/addr.py

@@ -24,6 +24,7 @@ from hashlib import sha256,sha512
 from .common import *
 from .obj import *
 from .baseconv import *
+from .protocol import init_proto
 
 pnm = g.proj_name
 
@@ -32,11 +33,11 @@ def dmsg_sc(desc,data):
 		Msg(f'sc_debug_{desc}: {data}')
 
 class AddrGenerator(MMGenObject):
-	def __new__(cls,addr_type):
+	def __new__(cls,proto,addr_type):
 		if type(addr_type) == str: # allow override w/o check
 			gen_method = addr_type
 		elif type(addr_type) == MMGenAddrType:
-			assert addr_type in proto.mmtypes, f'{addr_type}: invalid address type for coin {g.coin}'
+			assert addr_type in proto.mmtypes, f'{addr_type}: invalid address type for coin {proto.coin}'
 			gen_method = addr_type.gen_method
 		else:
 			raise TypeError(f'{type(addr_type)}: incorrect argument type for {cls.__name__}()')
@@ -50,13 +51,14 @@ class AddrGenerator(MMGenObject):
 		assert gen_method in gen_methods
 		me = super(cls,cls).__new__(gen_methods[gen_method])
 		me.desc = gen_methods
+		me.proto = proto
 		return me
 
 class AddrGeneratorP2PKH(AddrGenerator):
 	def to_addr(self,pubhex):
 		from .protocol import hash160
 		assert type(pubhex) == PubKey
-		return CoinAddr(g.proto.pubhash2addr(hash160(pubhex),p2sh=False))
+		return CoinAddr(self.proto,self.proto.pubhash2addr(hash160(pubhex),p2sh=False))
 
 	def to_segwit_redeem_script(self,pubhex):
 		raise NotImplementedError('Segwit redeem script not supported by this address type')
@@ -64,24 +66,24 @@ class AddrGeneratorP2PKH(AddrGenerator):
 class AddrGeneratorSegwit(AddrGenerator):
 	def to_addr(self,pubhex):
 		assert pubhex.compressed,'Uncompressed public keys incompatible with Segwit'
-		return CoinAddr(g.proto.pubhex2segwitaddr(pubhex))
+		return CoinAddr(self.proto,self.proto.pubhex2segwitaddr(pubhex))
 
 	def to_segwit_redeem_script(self,pubhex):
 		assert pubhex.compressed,'Uncompressed public keys incompatible with Segwit'
-		return HexStr(g.proto.pubhex2redeem_script(pubhex))
+		return HexStr(self.proto.pubhex2redeem_script(pubhex))
 
 class AddrGeneratorBech32(AddrGenerator):
 	def to_addr(self,pubhex):
 		assert pubhex.compressed,'Uncompressed public keys incompatible with Segwit'
 		from .protocol import hash160
-		return CoinAddr(g.proto.pubhash2bech32addr(hash160(pubhex)))
+		return CoinAddr(self.proto,self.proto.pubhash2bech32addr(hash160(pubhex)))
 
 	def to_segwit_redeem_script(self,pubhex):
 		raise NotImplementedError('Segwit redeem script not supported by this address type')
 
 class AddrGeneratorEthereum(AddrGenerator):
 
-	def __init__(self,addr_type):
+	def __init__(self,proto,addr_type):
 
 		try:
 			assert not g.use_internal_keccak_module
@@ -95,7 +97,7 @@ class AddrGeneratorEthereum(AddrGenerator):
 
 	def to_addr(self,pubhex):
 		assert type(pubhex) == PubKey
-		return CoinAddr(self.keccak_256(bytes.fromhex(pubhex[2:])).hexdigest()[24:])
+		return CoinAddr(self.proto,self.keccak_256(bytes.fromhex(pubhex[2:])).hexdigest()[24:])
 
 	def to_wallet_passwd(self,sk_hex):
 		return WalletPassword(self.hash256(sk_hex)[:32])
@@ -119,9 +121,9 @@ class AddrGeneratorZcashZ(AddrGenerator):
 		from nacl.bindings import crypto_scalarmult_base
 		p2 = crypto_scalarmult_base(self.zhash256(key,1))
 		from .protocol import _b58chk_encode
-		ver_bytes = g.proto.addr_fmt_to_ver_bytes('zcash_z')
+		ver_bytes = self.proto.addr_fmt_to_ver_bytes('zcash_z')
 		ret = _b58chk_encode(ver_bytes + self.zhash256(key,0) + p2)
-		return CoinAddr(ret)
+		return CoinAddr(self.proto,ret)
 
 	def to_viewkey(self,pubhex): # pubhex is really privhex
 		key = bytes.fromhex(pubhex)
@@ -131,16 +133,16 @@ class AddrGeneratorZcashZ(AddrGenerator):
 		vk[63] &= 0x7f
 		vk[63] |= 0x40
 		from .protocol import _b58chk_encode
-		ver_bytes = g.proto.addr_fmt_to_ver_bytes('viewkey')
+		ver_bytes = self.proto.addr_fmt_to_ver_bytes('viewkey')
 		ret = _b58chk_encode(ver_bytes + vk)
-		return ZcashViewKey(ret)
+		return ZcashViewKey(self.proto,ret)
 
 	def to_segwit_redeem_script(self,pubhex):
 		raise NotImplementedError('Zcash z-addresses incompatible with Segwit')
 
 class AddrGeneratorMonero(AddrGenerator):
 
-	def __init__(self,addr_type):
+	def __init__(self,proto,addr_type):
 
 		try:
 			assert not g.use_internal_keccak_module
@@ -189,9 +191,10 @@ class AddrGeneratorMonero(AddrGenerator):
 		vk_hex = self.to_viewkey(sk_hex)
 		pk_str  = self.encodepoint(scalarmultbase(hex2int_le(sk_hex)))
 		pvk_str = self.encodepoint(scalarmultbase(hex2int_le(vk_hex)))
-		addr_p1 = g.proto.addr_fmt_to_ver_bytes('monero') + pk_str + pvk_str
+		addr_p1 = self.proto.addr_fmt_to_ver_bytes('monero') + pk_str + pvk_str
 
 		return CoinAddr(
+			proto = self.proto,
 			addr = self.b58enc(addr_p1 + self.keccak_256(addr_p1).digest()[:4]) )
 
 	def to_wallet_passwd(self,sk_hex):
@@ -200,34 +203,36 @@ class AddrGeneratorMonero(AddrGenerator):
 	def to_viewkey(self,sk_hex):
 		assert len(sk_hex) == 64, f'{len(sk_hex)}: incorrect privkey length'
 		return MoneroViewKey(
-			g.proto.preprocess_key(self.keccak_256(bytes.fromhex(sk_hex)).digest(),None).hex() )
+			self.proto.preprocess_key(self.keccak_256(bytes.fromhex(sk_hex)).digest(),None).hex() )
 
 	def to_segwit_redeem_script(self,sk_hex):
 		raise NotImplementedError('Monero addresses incompatible with Segwit')
 
 class KeyGenerator(MMGenObject):
 
-	def __new__(cls,addr_type,generator=None,silent=False):
+	def __new__(cls,proto,addr_type,generator=None,silent=False):
 		if type(addr_type) == str: # allow override w/o check
 			pubkey_type = addr_type
 		elif type(addr_type) == MMGenAddrType:
-			assert addr_type in g.proto.mmtypes,'{}: invalid address type for coin {}'.format(addr_type,g.coin)
+			assert addr_type in proto.mmtypes, f'{address}: invalid address type for coin {proto.coin}'
 			pubkey_type = addr_type.pubkey_type
 		else:
-			raise TypeError('{}: incorrect argument type for {}()'.format(type(addr_type),cls.__name__))
+			raise TypeError(f'{type(addr_type)}: incorrect argument type for {cls.__name__}()')
 		if pubkey_type == 'std':
 			if cls.test_for_secp256k1(silent=silent) and generator != 1:
 				if not opt.key_generator or opt.key_generator == 2 or generator == 2:
-					return super(cls,cls).__new__(KeyGeneratorSecp256k1)
+					me = super(cls,cls).__new__(KeyGeneratorSecp256k1)
 			else:
 				qmsg('Using (slow) native Python ECDSA library for address generation')
-				return super(cls,cls).__new__(KeyGeneratorPython)
+				me = super(cls,cls).__new__(KeyGeneratorPython)
 		elif pubkey_type in ('zcash_z','monero'):
 			me = super(cls,cls).__new__(KeyGeneratorDummy)
 			me.desc = 'mmgen-'+pubkey_type
-			return me
 		else:
-			raise ValueError('{}: invalid pubkey_type argument'.format(pubkey_type))
+			raise ValueError(f'{pubkey_type}: invalid pubkey_type argument')
+
+		me.proto = proto
+		return me
 
 	@classmethod
 	def test_for_secp256k1(self,silent=False):
@@ -288,19 +293,25 @@ class KeyGeneratorDummy(KeyGenerator):
 		assert type(privhex) == PrivKey
 		return PubKey(privhex,compressed=privhex.compressed)
 
-class AddrListEntry(MMGenListItem):
-	addr          = ListItemAttr('CoinAddr')
+class AddrListEntryBase(MMGenListItem):
+	invalid_attrs = {'proto'}
+	def __init__(self,proto,**kwargs):
+		self.__dict__['proto'] = proto
+		MMGenListItem.__init__(self,**kwargs)
+
+class AddrListEntry(AddrListEntryBase):
+	addr          = ListItemAttr('CoinAddr',include_proto=True)
 	idx           = ListItemAttr('AddrIdx') # not present in flat addrlists
 	label         = ListItemAttr('TwComment',reassign_ok=True)
-	sec           = ListItemAttr(PrivKey,typeconv=False)
-	viewkey       = ListItemAttr('ViewKey')
+	sec           = ListItemAttr('PrivKey',include_proto=True)
+	viewkey       = ListItemAttr('ViewKey',include_proto=True)
 	wallet_passwd = ListItemAttr('WalletPassword')
 
-class PasswordListEntry(MMGenListItem):
+class PasswordListEntry(AddrListEntryBase):
 	passwd = ListItemAttr(str,typeconv=False) # TODO: create Password type
 	idx    = ImmutableAttr('AddrIdx')
 	label  = ListItemAttr('TwComment',reassign_ok=True)
-	sec    = ListItemAttr(PrivKey,typeconv=False)
+	sec    = ListItemAttr('PrivKey',include_proto=True)
 
 class AddrListChksum(str,Hilite):
 	color = 'pink'
@@ -335,7 +346,7 @@ class AddrListIDStr(str,Hilite):
 		if fmt_str:
 			ret = fmt_str.format(s)
 		else:
-			bc = (g.proto.base_coin,g.coin)[g.proto.base_coin=='ETH']
+			bc = (addrlist.proto.base_coin,addrlist.proto.coin)[addrlist.proto.base_coin=='ETH']
 			mt = addrlist.al_id.mmtype
 			ret = '{}{}{}[{}]'.format(addrlist.al_id.sid,('-'+bc,'')[bc=='BTC'],('-'+mt,'')[mt in ('L','E')],s)
 
@@ -376,7 +387,7 @@ Removed {{}} duplicate WIF key{{}} from keylist (also in {pnm} key-address file
 	chksum_rec_f = lambda foo,e: (str(e.idx), e.addr)
 	line_ctr = 0
 
-	def __init__(self,
+	def __init__(self,proto,
 		addrfile  = '',
 		al_id     = '',
 		adata     = [],
@@ -389,8 +400,14 @@ Removed {{}} duplicate WIF key{{}} from keylist (also in {pnm} key-address file
 
 		do_chksum = True
 		self.update_msgs()
-		mmtype = mmtype or g.proto.dfl_mmtype
-		assert mmtype in MMGenAddrType.mmtypes,'{}: mmtype not in {}'.format(mmtype,repr(MMGenAddrType.mmtypes))
+		mmtype = mmtype or proto.dfl_mmtype
+		assert mmtype in MMGenAddrType.mmtypes, f'{mmtype}: mmtype not in {MMGenAddrType.mmtypes!r}'
+
+		from .protocol import CoinProtocol
+		self.bitcoin_addrtypes = tuple(
+			MMGenAddrType(CoinProtocol.Bitcoin,key).name for key in CoinProtocol.Bitcoin.mmtypes)
+
+		self.proto = proto
 
 		if seed and addr_idxs:   # data from seed + idxs
 			self.al_id,src = AddrListID(seed.sid,mmtype),'gen'
@@ -403,10 +420,10 @@ Removed {{}} duplicate WIF key{{}} from keylist (also in {pnm} key-address file
 			do_chksum = False
 		elif addrlist:           # data from flat address list
 			self.al_id = None
-			adata = AddrListList([AddrListEntry(addr=a) for a in set(addrlist)])
+			adata = AddrListData([AddrListEntry(proto=proto,addr=a) for a in set(addrlist)])
 		elif keylist:            # data from flat key list
 			self.al_id = None
-			adata = AddrListList([AddrListEntry(sec=PrivKey(wif=k)) for k in set(keylist)])
+			adata = AddrListData([AddrListEntry(proto=proto,sec=PrivKey(proto=proto,wif=k)) for k in set(keylist)])
 		elif seed or addr_idxs:
 			die(3,'Must specify both seed and addr indexes')
 		elif al_id or adata:
@@ -448,10 +465,10 @@ Removed {{}} duplicate WIF key{{}} from keylist (also in {pnm} key-address file
 		gen_viewkey       = type(self) == KeyAddrList and 'viewkey' in self.al_id.mmtype.extra_attrs
 
 		if self.gen_addrs:
-			kg = KeyGenerator(self.al_id.mmtype)
-			ag = AddrGenerator(self.al_id.mmtype)
+			kg = KeyGenerator(self.proto,self.al_id.mmtype)
+			ag = AddrGenerator(self.proto,self.al_id.mmtype)
 
-		t_addrs,num,pos,out = len(addrnums),0,0,AddrListList()
+		t_addrs,num,pos,out = len(addrnums),0,0,AddrListData()
 		le = self.entry_type
 
 		while pos != t_addrs:
@@ -465,10 +482,11 @@ Removed {{}} duplicate WIF key{{}} from keylist (also in {pnm} key-address file
 			if not g.debug:
 				qmsg_r('\rGenerating {} #{} ({} of {})'.format(self.gen_desc,num,pos,t_addrs))
 
-			e = le(idx=num)
+			e = le(proto=self.proto,idx=num)
 
 			# Secret key is double sha256 of seed hash round /num/
 			e.sec = PrivKey(
+				self.proto,
 				sha256(sha256(seed).digest()).digest(),
 				compressed  = compressed,
 				pubkey_type = pubkey_type )
@@ -497,17 +515,17 @@ Removed {{}} duplicate WIF key{{}} from keylist (also in {pnm} key-address file
 		return True # format is checked when added to list entry object
 
 	def scramble_seed(self,seed):
-		is_btcfork = g.proto.base_coin == 'BTC'
-		if is_btcfork and self.al_id.mmtype == 'L' and not g.proto.testnet:
+		is_btcfork = self.proto.base_coin == 'BTC'
+		if is_btcfork and self.al_id.mmtype == 'L' and not self.proto.testnet:
 			dmsg_sc('str','(none)')
 			return seed
-		if g.proto.base_coin == 'ETH':
-			scramble_key = g.coin.lower()
+		if self.proto.base_coin == 'ETH':
+			scramble_key = self.proto.coin.lower()
 		else:
-			scramble_key = (g.coin.lower()+':','')[is_btcfork] + self.al_id.mmtype.name
+			scramble_key = (self.proto.coin.lower()+':','')[is_btcfork] + self.al_id.mmtype.name
 		from .crypto import scramble_seed
-		if g.proto.testnet:
-			scramble_key += ':testnet'
+		if self.proto.testnet:
+			scramble_key += ':' + self.proto.network
 		dmsg_sc('str',scramble_key)
 		return scramble_seed(seed,scramble_key.encode())
 
@@ -517,7 +535,7 @@ Removed {{}} duplicate WIF key{{}} from keylist (also in {pnm} key-address file
 		self.ext += '.'+g.mmenc_ext
 
 	def write_to_file(self,ask_tty=True,ask_write_default_yes=False,binary=False,desc=None):
-		tn = ('','.testnet')[g.proto.testnet]
+		tn = ('.' + self.proto.network) if self.proto.testnet else ''
 		fn = '{}{x}{}.{}'.format(self.id_str,tn,self.ext,x='-α' if g.debug_utf8 else '')
 		ask_tty = self.has_keys and not opt.quiet
 		write_data_to_file(fn,self.fmt_data,desc or self.file_desc,ask_tty=ask_tty,binary=binary)
@@ -554,12 +572,14 @@ Removed {{}} duplicate WIF key{{}} from keylist (also in {pnm} key-address file
 			if idx == e.idx:
 				e.label = comment
 
-	def make_reverse_dict(self,coinaddrs):
-		d,b = MMGenDict(),coinaddrs
+	def make_reverse_dict_addrlist(self,coinaddrs):
+		d = MMGenDict()
+		b = coinaddrs
 		for e in self.data:
 			try:
-				d[b[b.index(e.addr)]] = MMGenID('{}:{}'.format(self.al_id,e.idx)),e.label
-			except: pass
+				d[b[b.index(e.addr)]] = ( MMGenID(self.proto, f'{self.al_id}:{e.idx}'), e.label )
+			except ValueError:
+				pass
 		return d
 
 	def remove_dup_keys(self,cmplist):
@@ -585,9 +605,9 @@ Removed {{}} duplicate WIF key{{}} from keylist (also in {pnm} key-address file
 
 	def generate_addrs_from_keys(self):
 		# assume that the first listed mmtype is valid for flat key list
-		t = MMGenAddrType(g.proto.mmtypes[0])
-		kg = KeyGenerator(t.pubkey_type)
-		ag = AddrGenerator(t.gen_method)
+		t = self.proto.addr_type(self.proto.mmtypes[0])
+		kg = KeyGenerator(self.proto,t.pubkey_type)
+		ag = AddrGenerator(self.proto,t.gen_method)
 		d = self.data
 		for n,e in enumerate(d,1):
 			qmsg_r('\rGenerating addresses from keylist: {}/{}'.format(n,len(d)))
@@ -597,10 +617,10 @@ Removed {{}} duplicate WIF key{{}} from keylist (also in {pnm} key-address file
 		qmsg('\rGenerated addresses from keylist: {}/{} '.format(n,len(d)))
 
 	def make_label(self):
-		bc,mt = g.proto.base_coin,self.al_id.mmtype
-		l_coin = [] if bc == 'BTC' else [g.coin] if bc == 'ETH' else [bc]
-		l_type = [] if mt == 'E' or (mt == 'L' and not g.proto.testnet) else [mt.name.upper()]
-		l_tn   = [] if not g.proto.testnet else ['TESTNET']
+		bc,mt = self.proto.base_coin,self.al_id.mmtype
+		l_coin = [] if bc == 'BTC' else [self.proto.coin] if bc == 'ETH' else [bc]
+		l_type = [] if mt == 'E' or (mt == 'L' and not self.proto.testnet) else [mt.name.upper()]
+		l_tn   = [] if not self.proto.testnet else [self.proto.network.upper()]
 		lbl_p2 = ':'.join(l_coin+l_type+l_tn)
 		return self.al_id.sid + ('',' ')[bool(lbl_p2)] + lbl_p2
 
@@ -646,34 +666,36 @@ Removed {{}} duplicate WIF key{{}} from keylist (also in {pnm} key-address file
 
 	def parse_file_body(self,lines):
 
-		ret = AddrListList()
+		ret = AddrListData()
 		le = self.entry_type
 		iifs = "{!r}: invalid identifier [expected '{}:']"
 
 		while lines:
 			idx,addr,lbl = self.get_line(lines)
 
-			assert is_mmgen_idx(idx),'invalid address index {!r}'.format(idx)
+			assert is_mmgen_idx(idx), f'invalid address index {idx!r}'
 			self.check_format(addr)
 
-			a = le(**{ 'idx':int(idx), self.main_attr:addr, 'label':lbl })
+			a = le(**{ 'proto': self.proto, 'idx':int(idx), self.main_attr:addr, 'label':lbl })
 
 			if self.has_keys: # order: wif,(orig_hex),viewkey,wallet_passwd
 				d = self.get_line(lines)
 				assert d[0] == self.al_id.mmtype.wif_label+':',iifs.format(d[0],self.al_id.mmtype.wif_label)
-				a.sec = PrivKey(wif=d[1])
-				for k,dtype in (('viewkey',ViewKey),('wallet_passwd',WalletPassword)):
+				a.sec = PrivKey(proto=self.proto,wif=d[1])
+				for k,dtype,add_proto in (
+					('viewkey',ViewKey,True),
+					('wallet_passwd',WalletPassword,False) ):
 					if k in self.al_id.mmtype.extra_attrs:
 						d = self.get_line(lines)
 						assert d[0] == k+':',iifs.format(d[0],k)
-						setattr(a,k,dtype(d[1]))
+						setattr(a,k,dtype( *((self.proto,d[1]) if add_proto else (d[1],)) ) )
 
 			ret.append(a)
 
 		if self.has_keys:
 			if (hasattr(opt,'yes') and opt.yes) or keypress_confirm('Check key-to-address validity?'):
-				kg = KeyGenerator(self.al_id.mmtype)
-				ag = AddrGenerator(self.al_id.mmtype)
+				kg = KeyGenerator(self.proto,self.al_id.mmtype)
+				ag = AddrGenerator(self.proto,self.al_id.mmtype)
 				llen = len(ret)
 				for n,e in enumerate(ret):
 					qmsg_r('\rVerifying keys {}/{}'.format(n+1,llen))
@@ -685,35 +707,46 @@ Removed {{}} duplicate WIF key{{}} from keylist (also in {pnm} key-address file
 
 	def parse_file(self,fn,buf=[],exit_on_error=True):
 
-		def parse_addrfile_label(lbl): # we must maintain backwards compat, so parse is tricky
-			al_coin,al_mmtype = None,None
-			tn = lbl[-8:] == ':TESTNET'
-			if tn:
-				assert g.proto.testnet, f'{self.data_desc} file is testnet but protocol is mainnet!'
+		def parse_addrfile_label(lbl):
+			"""
+			label examples:
+			- Bitcoin legacy mainnet:   no label
+			- Bitcoin legacy testnet:   'LEGACY:TESTNET'
+			- Bitcoin Segwit:           'SEGWIT'
+			- Bitcoin Segwit testnet:   'SEGWIT:TESTNET'
+			- Bitcoin Bech32 regtest:   'BECH32:REGTEST'
+			- Litecoin legacy mainnet:  'LTC'
+			- Litecoin Bech32 mainnet:  'LTC:BECH32'
+			- Litecoin legacy testnet:  'LTC:LEGACY:TESTNET'
+			- Ethereum mainnet:         'ETH'
+			- Ethereum Classic mainnet: 'ETC'
+			- Ethereum regtest:         'ETH:REGTEST'
+			"""
+			lbl = lbl.lower()
+
+			# remove the network component:
+			if lbl.endswith(':testnet'):
+				network = 'testnet'
+				lbl = lbl[:-8]
+			elif lbl.endswith(':regtest'):
+				network = 'regtest'
 				lbl = lbl[:-8]
 			else:
-				assert not g.proto.testnet, f'{self.data_desc} file is mainnet but protocol is testnet!'
-			lbl = lbl.split(':',1)
-			if len(lbl) == 2:
-				al_coin,al_mmtype = lbl[0],lbl[1].lower()
-			else:
-				if lbl[0].lower() in MMGenAddrType.get_names():
-					al_mmtype = lbl[0].lower()
-				else:
-					al_coin = lbl[0]
-
-			# this block fails if al_mmtype is invalid for g.coin
-			if not al_mmtype:
-				mmtype = MMGenAddrType('E' if al_coin in ('ETH','ETC') else 'L',on_fail='raise')
-			else:
-				mmtype = MMGenAddrType(al_mmtype,on_fail='raise')
+				network = 'mainnet'
 
-			from .protocol import init_proto
-			return (init_proto(al_coin or 'BTC').base_coin, mmtype, tn)
+			if lbl in self.bitcoin_addrtypes:
+				coin,mmtype_key = ( 'BTC', lbl )
+			elif ':' in lbl: # first component is coin, second is mmtype_key
+				coin,mmtype_key = lbl.split(':')
+			else:            # only component is coin
+				coin,mmtype_key = ( lbl, None )
 
-		def check_coin_mismatch(base_coin): # die if addrfile coin doesn't match g.coin
-			m = '{} address file format, but base coin is {}!'
-			assert base_coin == g.proto.base_coin, m.format(base_coin,g.proto.base_coin)
+			proto = init_proto(coin=coin,network=network)
+
+			if mmtype_key == None:
+				mmtype_key = proto.mmtypes[0]
+
+			return ( proto, proto.addr_type(mmtype_key) )
 
 		lines = get_lines_from_file(fn,self.data_desc+' data',trim_comments=True)
 
@@ -732,20 +765,31 @@ Removed {{}} duplicate WIF key{{}} from keylist (also in {pnm} key-address file
 				self.set_pw_fmt(ss[0])
 				self.set_pw_len(ss[1])
 				self.pw_id_str = MMGenPWIDString(ls.pop())
-				base_coin,mmtype = None,MMGenPasswordType('P')
-				testnet = False
+				proto = init_proto('btc')# FIXME: dummy protocol
+				mmtype = MMGenPasswordType(proto,'P')
 			elif len(ls) == 1:
-				base_coin,mmtype,testnet = parse_addrfile_label(ls[0])
-				check_coin_mismatch(base_coin)
+				proto,mmtype = parse_addrfile_label(ls[0])
 			elif len(ls) == 0:
-				base_coin,mmtype = 'BTC',MMGenAddrType('L')
-				testnet = False
-				check_coin_mismatch(base_coin)
+				proto = init_proto('btc')
+				mmtype = proto.addr_type('L')
 			else:
-				raise ValueError("'{}': Invalid first line for {} file '{}'".format(lines[0],self.gen_desc,fn))
-
-			self.base_coin = base_coin
-			self.is_testnet = testnet
+				raise ValueError(f'{lines[0]}: Invalid first line for {self.gen_desc} file {fn!r}')
+
+			if type(self) != PasswordList:
+				if proto.base_coin != self.proto.base_coin or proto.network != self.proto.network:
+					"""
+					Having caller supply protocol and checking address file protocol against it here
+					allows us to catch all mismatches in one place.  This behavior differs from that of
+					transaction files, which determine the protocol independently, requiring the caller
+					to check for protocol mismatches (e.g. MMGenTX.check_correct_chain())
+					"""
+					raise ValueError(
+						f'{self.data_desc} file is '
+						+ f'{proto.base_coin} {proto.network} but protocol is '
+						+ f'{self.proto.base_coin} {self.proto.network}' )
+
+			self.base_coin = proto.base_coin
+			self.network = proto.network
 			self.al_id = AddrListID(SeedID(sid=sid),mmtype)
 
 			data = self.parse_file_body(lines[1:-1])
@@ -850,7 +894,7 @@ Record this checksum: it will be used to verify the password file in the future
 	feature_warn_fs = 'WARNING: {!r} is a potentially dangerous feature.  Use at your own risk!'
 	hex2bip39 = False
 
-	def __init__(self,
+	def __init__(self,proto,
 		infile          = None,
 		seed            = None,
 		pw_idxs         = None,
@@ -860,6 +904,7 @@ Record this checksum: it will be used to verify the password file in the future
 		chk_params_only = False
 		):
 
+		self.proto = proto # proto is ignored
 		self.update_msgs()
 
 		if infile:
@@ -877,7 +922,7 @@ Record this checksum: it will be used to verify the password file in the future
 			if self.hex2bip39:
 				ymsg(self.feature_warn_fs.format(pw_fmt))
 			self.set_pw_len_vs_seed_len(pw_len,seed)
-			self.al_id = AddrListID(seed.sid,MMGenPasswordType('P'))
+			self.al_id = AddrListID(seed.sid,MMGenPasswordType(self.proto,'P'))
 			self.data = self.generate(seed,pw_idxs)
 
 		if self.pw_fmt in ('bip39','xmrseed'):
@@ -983,7 +1028,6 @@ Record this checksum: it will be used to verify the password file in the future
 			pw_len_hex = baseconv.seedlen_map_rev['xmrseed'][self.pw_len] * 2
 			# take most significant part
 			bytes_trunc = bytes.fromhex(hex_sec[:pw_len_hex])
-			from .protocol import init_proto
 			bytes_preproc = init_proto('xmr').preprocess_key(bytes_trunc,None)
 			return ' '.join(baseconv.frombytes(bytes_preproc,wl_id='xmrseed'))
 		else:
@@ -1043,11 +1087,13 @@ re-import your addresses.
 """.strip().format(pnm=pnm)
 	}
 
-	def __new__(cls,*args,**kwargs):
-		return MMGenObject.__new__(altcoin_subclass(cls,'tw','AddrData'))
+	def __new__(cls,proto,*args,**kwargs):
+		return MMGenObject.__new__(altcoin_subclass(cls,proto,'tw'))
 
-	def __init__(self,*args,**kwargs):
+	def __init__(self,proto,*args,**kwargs):
 		self.al_ids = {}
+		self.proto = proto
+		self.rpc = None
 
 	def seed_ids(self):
 		return list(self.al_ids.keys())
@@ -1058,7 +1104,7 @@ re-import your addresses.
 			return self.al_ids[al_id]
 
 	def mmaddr2coinaddr(self,mmaddr):
-		al_id,idx = MMGenID(mmaddr).rsplit(':',1)
+		al_id,idx = MMGenID(self.proto,mmaddr).rsplit(':',1)
 		coinaddr = ''
 		if al_id in self.al_ids:
 			coinaddr = self.addrlist(al_id).coinaddr(int(idx))
@@ -1068,59 +1114,63 @@ re-import your addresses.
 		d = self.make_reverse_dict([coinaddr])
 		return (list(d.values())[0][0]) if d else None
 
-	@classmethod
-	async def get_tw_data(cls,wallet=None):
+	def add(self,addrlist):
+		if type(addrlist) == AddrList:
+			self.al_ids[addrlist.al_id] = addrlist
+			return True
+		else:
+			raise TypeError('Error: object {!r} is not of type AddrList'.format(addrlist))
+
+	def make_reverse_dict(self,coinaddrs):
+		d = MMGenDict()
+		for al_id in self.al_ids:
+			d.update(self.al_ids[al_id].make_reverse_dict_addrlist(coinaddrs))
+		return d
+
+class TwAddrData(AddrData,metaclass=aInitMeta):
+
+	def __new__(cls,proto,*args,**kwargs):
+		return MMGenObject.__new__(altcoin_subclass(cls,proto,'tw'))
+
+	def __init__(self,proto,*args,**kwargs):
+		pass
+
+	async def __ainit__(self,proto,wallet=None):
+		self.proto = proto
+		from .rpc import rpc_init
+		self.rpc = await rpc_init(proto)
+		self.al_ids = {}
+		await self.add_tw_data(wallet)
+
+	async def get_tw_data(self,wallet=None):
 		vmsg('Getting address data from tracking wallet')
-		if 'label_api' in g.rpc.caps:
-			accts = await g.rpc.call('listlabels')
-			ll = await g.rpc.batch_call('getaddressesbylabel',[(k,) for k in accts])
+		c = self.rpc
+		if 'label_api' in c.caps:
+			accts = await c.call('listlabels')
+			ll = await c.batch_call('getaddressesbylabel',[(k,) for k in accts])
 			alists = [list(a.keys()) for a in ll]
 		else:
-			accts = await g.rpc.call('listaccounts',0,True)
-			alists = await g.rpc.batch_call('getaddressesbyaccount',[(k,) for k in accts])
+			accts = await c.call('listaccounts',0,True)
+			alists = await c.batch_call('getaddressesbyaccount',[(k,) for k in accts])
 		return list(zip(accts,alists))
 
 	async def add_tw_data(self,wallet):
 
-		twd = await type(self).get_tw_data(wallet)
+		twd = await self.get_tw_data(wallet)
 		out,i = {},0
 		for acct,addr_array in twd:
-			l = TwLabel(acct,on_fail='silent')
+			l = TwLabel(self.proto,acct,on_fail='silent')
 			if l and l.mmid.type == 'mmgen':
 				obj = l.mmid.obj
 				if len(addr_array) != 1:
 					die(2,self.msgs['too_many_acct_addresses'].format(acct))
-				al_id = AddrListID(SeedID(sid=obj.sid),MMGenAddrType(obj.mmtype))
+				al_id = AddrListID(SeedID(sid=obj.sid),self.proto.addr_type(obj.mmtype))
 				if al_id not in out:
 					out[al_id] = []
-				out[al_id].append(AddrListEntry(idx=obj.idx,addr=addr_array[0],label=l.comment))
+				out[al_id].append(AddrListEntry(self.proto,idx=obj.idx,addr=addr_array[0],label=l.comment))
 				i += 1
 
 		vmsg('{n} {pnm} addresses found, {m} accounts total'.format(n=i,pnm=pnm,m=len(twd)))
 		for al_id in out:
-			self.add(AddrList(al_id=al_id,adata=AddrListList(sorted(out[al_id],key=lambda a: a.idx))))
-
-	def add(self,addrlist):
-		if type(addrlist) == AddrList:
-			self.al_ids[addrlist.al_id] = addrlist
-			return True
-		else:
-			raise TypeError('Error: object {!r} is not of type AddrList'.format(addrlist))
-
-	def make_reverse_dict(self,coinaddrs):
-		d = MMGenDict()
-		for al_id in self.al_ids:
-			d.update(self.al_ids[al_id].make_reverse_dict(coinaddrs))
-		return d
-
-class TwAddrData(AddrData,metaclass=aInitMeta):
-
-	def __new__(cls,*args,**kwargs):
-		return MMGenObject.__new__(altcoin_subclass(cls,'tw','TwAddrData'))
+			self.add(AddrList(self.proto,al_id=al_id,adata=AddrListData(sorted(out[al_id],key=lambda a: a.idx))))
 
-	def __init__(self,*args,**kwargs):
-		pass
-
-	async def __ainit__(self,wallet=None):
-		self.al_ids = {}
-		await self.add_tw_data(wallet)

+ 21 - 18
mmgen/altcoins/eth/contract.py

@@ -37,13 +37,13 @@ except:
 def parse_abi(s):
 	return [s[:8]] + [s[8+x*64:8+(x+1)*64] for x in range(len(s[8:])//64)]
 
-def create_method_id(sig): return keccak_256(sig.encode()).hexdigest()[:8]
+def create_method_id(sig):
+	return keccak_256(sig.encode()).hexdigest()[:8]
 
 class TokenBase(MMGenObject): # ERC20
 
-	@staticmethod
-	def transferdata2sendaddr(data): # online
-		return CoinAddr(parse_abi(data)[1][-40:])
+	def transferdata2sendaddr(self,data): # online
+		return CoinAddr(self.proto,parse_abi(data)[1][-40:])
 
 	def transferdata2amt(self,data): # online
 		return ETHAmt(int(parse_abi(data)[-1],16) * self.base_unit)
@@ -52,7 +52,7 @@ class TokenBase(MMGenObject): # ERC20
 		data = create_method_id(method_sig) + method_args
 		if g.debug:
 			msg('ETH_CALL {}:  {}'.format(method_sig,'\n  '.join(parse_abi(data))))
-		ret = await g.rpc.call('eth_call',{ 'to': '0x'+self.addr, 'data': '0x'+data })
+		ret = await self.rpc.call('eth_call',{ 'to': '0x'+self.addr, 'data': '0x'+data })
 		if toUnit:
 			return int(ret,16) * self.base_unit
 		else:
@@ -91,7 +91,7 @@ class TokenBase(MMGenObject): # ERC20
 						'total supply:',   await self.get_total_supply())
 
 	async def code(self):
-		return (await g.rpc.call('eth_getCode','0x'+self.addr))[2:]
+		return (await self.rpc.call('eth_getCode','0x'+self.addr))[2:]
 
 	def create_data(self,to_addr,amt,method_sig='transfer(address,uint256)',from_addr=None):
 		from_arg = from_addr.rjust(64,'0') if from_addr else ''
@@ -114,14 +114,13 @@ class TokenBase(MMGenObject): # ERC20
 		from .pyethereum.transactions import Transaction
 
 		if chain_id is None:
-			chain_id_method = ('parity_chainId','eth_chainId')['eth_chainId' in g.rpc.caps]
-			chain_id = int(await g.rpc.call(chain_id_method),16)
+			chain_id_method = ('parity_chainId','eth_chainId')['eth_chainId' in self.rpc.caps]
+			chain_id = int(await self.rpc.call(chain_id_method),16)
 		tx = Transaction(**tx_in).sign(key,chain_id)
 		hex_tx = rlp.encode(tx).hex()
 		coin_txid = CoinTxID(tx.hash.hex())
 		if tx.sender.hex() != from_addr:
-			m = "Sender address '{}' does not match address of key '{}'!"
-			die(3,m.format(from_addr,tx.sender.hex()))
+			die(3,f'Sender address {from_addr!r} does not match address of key {tx.sender.hex()!r}!')
 		if g.debug:
 			msg('TOKEN DATA:')
 			pp_msg(tx.to_dict())
@@ -131,7 +130,7 @@ class TokenBase(MMGenObject): # ERC20
 # The following are used for token deployment only:
 
 	async def txsend(self,hex_tx):
-		return (await g.rpc.call('eth_sendRawTransaction','0x'+hex_tx)).replace('0x','',1)
+		return (await self.rpc.call('eth_sendRawTransaction','0x'+hex_tx)).replace('0x','',1)
 
 	async def transfer(   self,from_addr,to_addr,amt,key,start_gas,gasPrice,
 					method_sig='transfer(address,uint256)',
@@ -140,7 +139,7 @@ class TokenBase(MMGenObject): # ERC20
 		tx_in = self.make_tx_in(
 					from_addr,to_addr,amt,
 					start_gas,gasPrice,
-					nonce = int(await g.rpc.call('parity_nextNonce','0x'+from_addr),16),
+					nonce = int(await self.rpc.call('parity_nextNonce','0x'+from_addr),16),
 					method_sig = method_sig,
 					from_addr2 = from_addr2 )
 		(hex_tx,coin_txid) = await self.txsign(tx_in,key,from_addr)
@@ -148,20 +147,24 @@ class TokenBase(MMGenObject): # ERC20
 
 class Token(TokenBase):
 
-	def __init__(self,addr,decimals):
-		self.addr = TokenAddr(addr)
+	def __init__(self,proto,addr,decimals,rpc=None):
+		self.proto = proto
+		self.addr = TokenAddr(proto,addr)
 		assert isinstance(decimals,int),f'decimals param must be int instance, not {type(decimals)}'
 		self.decimals = decimals
 		self.base_unit = Decimal('10') ** -self.decimals
+		self.rpc = rpc
 
 class TokenResolve(TokenBase,metaclass=aInitMeta):
 
-	def __init__(self,addr):
+	def __init__(self,*args,**kwargs):
 		return super().__init__()
 
-	async def __ainit__(self,addr):
-		self.addr = TokenAddr(addr)
+	async def __ainit__(self,proto,rpc,addr):
+		self.proto = proto
+		self.rpc = rpc
+		self.addr = TokenAddr(proto,addr)
 		decimals = await self.get_decimals() # requires self.addr!
 		if not decimals:
 			raise TokenNotInBlockchain(f'Token {addr!r} not in blockchain')
-		Token.__init__(self,addr,decimals)
+		Token.__init__(self,proto,addr,decimals,rpc)

+ 56 - 62
mmgen/altcoins/eth/tw.py

@@ -21,7 +21,7 @@ altcoins.eth.tw: Ethereum tracking wallet and related classes for the MMGen suit
 """
 
 from mmgen.common import *
-from mmgen.obj import ETHAmt,TwLabel,is_coin_addr,is_mmgen_id,MMGenListItem,ListItemAttr,ImmutableAttr
+from mmgen.obj import ETHAmt,TwLabel,is_coin_addr,is_mmgen_id,ListItemAttr,ImmutableAttr
 from mmgen.tw import TrackingWallet,TwAddrList,TwUnspentOutputs,TwGetBalance
 from mmgen.addr import AddrData,TwAddrData
 from .contract import Token,TokenResolve
@@ -36,7 +36,7 @@ class EthereumTrackingWallet(TrackingWallet):
 		return addr in self.data_root
 
 	def init_empty(self):
-		self.data = { 'coin': g.coin, 'accounts': {}, 'tokens': {} }
+		self.data = { 'coin': self.proto.coin, 'accounts': {}, 'tokens': {} }
 
 	def upgrade_wallet_maybe(self):
 
@@ -49,7 +49,7 @@ class EthereumTrackingWallet(TrackingWallet):
 				import json
 				self.data['accounts'] = json.loads(self.orig_data)
 			if not 'coin' in self.data:
-				self.data['coin'] = g.coin
+				self.data['coin'] = self.proto.coin
 			upgraded = True
 
 		def have_token_params_fields():
@@ -75,7 +75,7 @@ class EthereumTrackingWallet(TrackingWallet):
 			msg('{} upgraded successfully!'.format(self.desc))
 
 	async def rpc_get_balance(self,addr):
-		return ETHAmt(int(await g.rpc.call('eth_getBalance','0x'+addr),16),'wei')
+		return ETHAmt(int(await self.rpc.call('eth_getBalance','0x'+addr),16),'wei')
 
 	@write_mode
 	async def batch_import_address(self,args_list):
@@ -97,9 +97,9 @@ class EthereumTrackingWallet(TrackingWallet):
 	async def remove_address(self,addr):
 		r = self.data_root
 
-		if is_coin_addr(addr):
+		if is_coin_addr(self.proto,addr):
 			have_match = lambda k: k == addr
-		elif is_mmgen_id(addr):
+		elif is_mmgen_id(self.proto,addr):
 			have_match = lambda k: r[k]['mmid'] == addr
 		else:
 			die(1,f'{addr!r} is not an Ethereum address or MMGen ID')
@@ -107,7 +107,7 @@ class EthereumTrackingWallet(TrackingWallet):
 		for k in r:
 			if have_match(k):
 				# return the addr resolved to mmid if possible
-				ret = r[k]['mmid'] if is_mmgen_id(r[k]['mmid']) else addr
+				ret = r[k]['mmid'] if is_mmgen_id(self.proto,r[k]['mmid']) else addr
 				del r[k]
 				self.write()
 				return ret
@@ -152,30 +152,33 @@ class EthereumTokenTrackingWallet(EthereumTrackingWallet):
 	symbol = None
 	cur_eth_balances = {}
 
-	async def __ainit__(self,mode='r'):
-		await super().__ainit__(mode=mode)
+	async def __ainit__(self,proto,mode='r',token_addr=None):
+		await super().__ainit__(proto,mode=mode)
 
 		for v in self.data['tokens'].values():
 			self.conv_types(v)
 
-		if not is_coin_addr(g.token):
-			g.token = await self.sym2addr(g.token) # returns None on failure
-
-		if not is_coin_addr(g.token):
-			if self.importing:
-				m = 'When importing addresses for a new token, the token must be specified by address, not symbol.'
-				raise InvalidTokenAddress(f'{g.token!r}: invalid token address\n{m}')
-			else:
-				raise UnrecognizedTokenSymbol(f'Specified token {g.token!r} could not be resolved!')
-
-		if g.token in self.data['tokens']:
-			self.decimals = self.data['tokens'][g.token]['params']['decimals']
-			self.symbol = self.data['tokens'][g.token]['params']['symbol']
-		elif not self.importing:
-			raise TokenNotInWallet('Specified token {!r} not in wallet!'.format(g.token))
+		if self.importing and token_addr:
+			if not is_coin_addr(proto,token_addr):
+				raise InvalidTokenAddress(f'{token_addr!r}: invalid token address')
+		else:
+			assert token_addr == None,'EthereumTokenTrackingWallet_chk1'
+			token_addr = await self.sym2addr(proto.tokensym) # returns None on failure
+			if not is_coin_addr(proto,token_addr):
+				raise UnrecognizedTokenSymbol(f'Specified token {proto.tokensym!r} could not be resolved!')
+
+		from mmgen.obj import TokenAddr
+		self.token = TokenAddr(proto,token_addr)
+
+		if self.token in self.data['tokens']:
+			self.decimals = self.get_param('decimals')
+			self.symbol   = self.get_param('symbol')
+		elif self.importing:
+			await self.import_token(self.token) # sets self.decimals, self.symbol
+		else:
+			raise TokenNotInWallet(f'Specified token {self.token!r} not in wallet!')
 
-		self.token = g.token
-		g.proto.dcoin = self.symbol
+		proto.tokensym = self.symbol
 
 	async def is_in_wallet(self,addr):
 		return addr in self.data['tokens'][self.token]
@@ -189,7 +192,7 @@ class EthereumTokenTrackingWallet(EthereumTrackingWallet):
 		return 'token ' + self.get_param('symbol')
 
 	async def rpc_get_balance(self,addr):
-		return await Token(self.token,self.decimals).get_balance(addr)
+		return await Token(self.proto,self.token,self.decimals,self.rpc).get_balance(addr)
 
 	async def get_eth_balance(self,addr,force_rpc=False):
 		cache = self.cur_eth_balances
@@ -204,21 +207,19 @@ class EthereumTokenTrackingWallet(EthereumTrackingWallet):
 		return self.data['tokens'][self.token]['params'][param]
 
 	@write_mode
-	async def import_token(tw):
+	async def import_token(self,tokenaddr):
 		"""
 		Token 'symbol' and 'decimals' values are resolved from the network by the system just
 		once, upon token import.  Thereafter, token address, symbol and decimals are resolved
 		either from the tracking wallet (online operations) or transaction file (when signing).
 		"""
-		if not g.token in tw.data['tokens']:
-			t = await TokenResolve(g.token)
-			tw.token = g.token
-			tw.data['tokens'][tw.token] = {
-				'params': {
-					'symbol': await t.get_symbol(),
-					'decimals': t.decimals
-				}
+		t = await TokenResolve(self.proto,self.rpc,tokenaddr)
+		self.data['tokens'][tokenaddr] = {
+			'params': {
+				'symbol': await t.get_symbol(),
+				'decimals': t.decimals
 			}
+		}
 
 # No unspent outputs with Ethereum, but naming must be consistent
 class EthereumTwUnspentOutputs(TwUnspentOutputs):
@@ -242,10 +243,10 @@ Actions:         [q]uit view, [p]rint to file, pager [v]iew, [w]ide view,
 		'q':'a_quit','p':'a_print','v':'a_view','w':'a_view_wide',
 		'l':'a_lbl_add','D':'a_addr_delete','R':'a_balance_refresh' }
 
-	async def __ainit__(self,*args,**kwargs):
+	async def __ainit__(self,proto,*args,**kwargs):
 		if g.use_cached_balances:
 			self.hdr_fmt += '\n' + yellow('WARNING: Using cached balances. These may be out of date!')
-		await TwUnspentOutputs.__ainit__(self,*args,**kwargs)
+		await TwUnspentOutputs.__ainit__(self,proto,*args,**kwargs)
 
 	def do_sort(self,key=None,reverse=False):
 		if key == 'txid': return
@@ -256,22 +257,15 @@ Actions:         [q]uit view, [p]rint to file, pager [v]iew, [w]ide view,
 		if self.addrs:
 			wl = [d for d in wl if d['addr'] in self.addrs]
 		return [{
-				'account': TwLabel(d['mmid']+' '+d['comment'],on_fail='raise'),
+				'account': TwLabel(self.proto,d['mmid']+' '+d['comment'],on_fail='raise'),
 				'address': d['addr'],
 				'amount': await self.wallet.get_balance(d['addr']),
 				'confirmations': 0, # TODO
 				} for d in wl]
 
-	class MMGenTwUnspentOutput(MMGenListItem):
-		txid   = ListItemAttr('CoinTxID')
-		vout   = ListItemAttr(int,typeconv=False)
-		amt    = ImmutableAttr(lambda val:g.proto.coin_amt(val),typeconv=False)
-		amt2   = ListItemAttr(lambda val:g.proto.coin_amt(val),typeconv=False)
-		label  = ListItemAttr('TwComment',reassign_ok=True)
-		twmmid = ImmutableAttr('TwMMGenID')
-		addr   = ImmutableAttr('CoinAddr')
-		confs  = ImmutableAttr(int,typeconv=False)
-		skip   = ListItemAttr(str,typeconv=False,reassign_ok=True)
+	class MMGenTwUnspentOutput(TwUnspentOutputs.MMGenTwUnspentOutput):
+		valid_attrs = {'txid','vout','amt','amt2','label','twmmid','addr','confs','skip'}
+		invalid_attrs = {'proto'}
 
 	def age_disp(self,o,age_fmt): # TODO
 		return None
@@ -294,25 +288,26 @@ class EthereumTwAddrList(TwAddrList):
 
 	has_age = False
 
-	async def __ainit__(self,usr_addr_list,minconf,showempty,showbtcaddrs,all_labels,wallet=None):
+	async def __ainit__(self,proto,usr_addr_list,minconf,showempty,showbtcaddrs,all_labels,wallet=None):
 
-		self.wallet = wallet or await TrackingWallet(mode='w')
+		self.proto = proto
+		self.wallet = wallet or await TrackingWallet(self.proto,mode='w')
 		tw_dict = self.wallet.mmid_ordered_dict
-		self.total = g.proto.coin_amt('0')
+		self.total = self.proto.coin_amt('0')
 
 		from mmgen.obj import CoinAddr
 		for mmid,d in list(tw_dict.items()):
 #			if d['confirmations'] < minconf: continue # cannot get confirmations for eth account
-			label = TwLabel(mmid+' '+d['comment'],on_fail='raise')
+			label = TwLabel(self.proto,mmid+' '+d['comment'],on_fail='raise')
 			if usr_addr_list and (label.mmid not in usr_addr_list):
 				continue
 			bal = await self.wallet.get_balance(d['addr'])
 			if bal == 0 and not showempty:
 				if not label.comment or not all_labels:
 					continue
-			self[label.mmid] = {'amt': g.proto.coin_amt('0'), 'lbl':  label }
+			self[label.mmid] = {'amt': self.proto.coin_amt('0'), 'lbl':  label }
 			if showbtcaddrs:
-				self[label.mmid]['addr'] = CoinAddr(d['addr'])
+				self[label.mmid]['addr'] = CoinAddr(self.proto,d['addr'])
 			self[label.mmid]['lbl'].mmid.confs = None
 			self[label.mmid]['amt'] += bal
 			self.total += bal
@@ -326,9 +321,9 @@ class EthereumTwGetBalance(TwGetBalance):
 
 	fs = '{w:13} {c}\n' # TODO - for now, just suppress display of meaningless data
 
-	async def __ainit__(self,*args,**kwargs):
-		self.wallet = await TrackingWallet(mode='w')
-		await TwGetBalance.__ainit__(self,*args,**kwargs)
+	async def __ainit__(self,proto,*args,**kwargs):
+		self.wallet = await TrackingWallet(proto,mode='w')
+		await TwGetBalance.__ainit__(self,proto,*args,**kwargs)
 
 	async def create_data(self):
 		data = self.wallet.mmid_ordered_dict
@@ -336,7 +331,7 @@ class EthereumTwGetBalance(TwGetBalance):
 			if d.type == 'mmgen':
 				key = d.obj.sid
 				if key not in self.data:
-					self.data[key] = [g.proto.coin_amt('0')] * 4
+					self.data[key] = [self.proto.coin_amt('0')] * 4
 			else:
 				key = 'Non-MMGen'
 
@@ -350,10 +345,9 @@ class EthereumTwGetBalance(TwGetBalance):
 
 class EthereumTwAddrData(TwAddrData):
 
-	@classmethod
-	async def get_tw_data(cls,wallet=None):
+	async def get_tw_data(self,wallet=None):
 		vmsg('Getting address data from tracking wallet')
-		tw = (wallet or await TrackingWallet()).mmid_ordered_dict
+		tw = (wallet or await TrackingWallet(self.proto)).mmid_ordered_dict
 		# emulate the output of RPC 'listaccounts' and 'getaddressesbyaccount'
 		return [(mmid+' '+d['comment'],[d['addr']]) for mmid,d in list(tw.items())]
 

+ 494 - 483
mmgen/altcoins/eth/tx.py

@@ -25,523 +25,534 @@ from mmgen.common import *
 from mmgen.exception import TransactionChainMismatch
 from mmgen.obj import *
 
-from mmgen.tx import MMGenTX,MMGenBumpTX,MMGenSplitTX,MMGenTxForSigning
+from mmgen.tx import MMGenTX
 from mmgen.tw import TrackingWallet
 from .contract import Token
 
-class EthereumMMGenTX(MMGenTX):
-	desc = 'Ethereum transaction'
-	contract_desc = 'contract'
-	tx_gas = ETHAmt(21000,'wei')    # an approximate number, used for fee estimation purposes
-	start_gas = ETHAmt(21000,'wei') # the actual startgas amt used in the transaction
-									# for simple sends with no data, tx_gas = start_gas = 21000
-	fee_fail_fs = 'Network fee estimation failed'
-	no_chg_msg = 'Warning: Transaction leaves account with zero balance'
-	rel_fee_desc = 'gas price'
-	rel_fee_disp = 'gas price in Gwei'
-	txview_hdr_fs = 'TRANSACTION DATA\n\nID={i} ({a} {c}) UTC={t} Sig={s} Locktime={l}\n'
-	txview_hdr_fs_short = 'TX {i} ({a} {c}) UTC={t} Sig={s} Locktime={l}\n'
-	txview_ftr_fs = 'Total in account: {i} {d}\nTotal to spend:   {o} {d}\nTX fee:           {a} {c}{r}\n'
-	txview_ftr_fs_short = 'In {i} {d} - Out {o} {d}\nFee {a} {c}{r}\n'
-	fmt_keys = ('from','to','amt','nonce')
-	usr_fee_prompt = 'Enter transaction fee or gas price: '
-	fn_fee_unit = 'Mwei'
-	usr_rel_fee = None # not in MMGenTX
-	disable_fee_check = False
-	txobj  = None # ""
-	usr_contract_data = HexStr('')
-
-	def __init__(self,*args,**kwargs):
-		MMGenTX.__init__(self,*args,**kwargs)
-		if hasattr(opt,'tx_gas') and opt.tx_gas:
-			self.tx_gas = self.start_gas = ETHAmt(int(opt.tx_gas),'wei')
-		if hasattr(opt,'contract_data') and opt.contract_data:
-			m = "'--contract-data' option may not be used with token transaction"
-			assert not 'Token' in type(self).__name__, m
-			self.usr_contract_data = HexStr(open(opt.contract_data).read().strip())
-			self.disable_fee_check = True
-
-	def check_txfile_hex_data(self):
-		pass
+class EthereumMMGenTX:
 
-	@classmethod
-	async def get_exec_status(cls,txid,silent=False):
-		d = await g.rpc.call('eth_getTransactionReceipt','0x'+txid)
-		if not silent:
-			if 'contractAddress' in d and d['contractAddress']:
-				msg('Contract address: {}'.format(d['contractAddress'].replace('0x','')))
-		return int(d['status'],16)
+	class Base(MMGenTX.Base):
 
-	def is_replaceable(self): return True
+		rel_fee_desc = 'gas price'
+		rel_fee_disp = 'gas price in Gwei'
+		txobj  = None # ""
+		tx_gas = ETHAmt(21000,'wei')    # an approximate number, used for fee estimation purposes
+		start_gas = ETHAmt(21000,'wei') # the actual startgas amt used in the transaction
+										# for simple sends with no data, tx_gas = start_gas = 21000
+		contract_desc = 'contract'
+		usr_contract_data = HexStr('')
+		disable_fee_check = False
 
-	def get_fee(self):
-		return self.fee
+		# given absolute fee in ETH, return gas price in Gwei using tx_gas
+		def fee_abs2rel(self,abs_fee,to_unit='Gwei'):
+			ret = ETHAmt(int(abs_fee.toWei() // self.tx_gas.toWei()),'wei')
+			dmsg('fee_abs2rel() ==> {} ETH'.format(ret))
+			return ret if to_unit == 'eth' else ret.to_unit(to_unit,show_decimal=True)
 
-	def check_fee(self):
-		assert self.disable_fee_check or (self.fee <= g.proto.max_tx_fee)
+		def get_fee(self):
+			return self.fee
 
-	def get_hex_locktime(self): return None # TODO
+		def get_hex_locktime(self):
+			return None # TODO
 
-	def check_pubkey_scripts(self): pass
+		# given rel fee in wei, return absolute fee using tx_gas (not in MMGenTX)
+		def fee_rel2abs(self,rel_fee):
+			assert isinstance(rel_fee,int),"'{}': incorrect type for fee estimate (not an integer)".format(rel_fee)
+			return ETHAmt(rel_fee * self.tx_gas.toWei(),'wei')
 
-	def check_sigs(self,deserial_tx=None):
-		if is_hex_str(self.hex):
-			self.mark_signed()
+		def is_replaceable(self):
 			return True
-		return False
-
-	def parse_txfile_hex_data(self):
-		from .pyethereum.transactions import Transaction
-		from . import rlp
-		etx = rlp.decode(bytes.fromhex(self.hex),Transaction)
-		d = etx.to_dict() # ==> hex values have '0x' prefix, 0 is '0x'
-		for k in ('sender','to','data'):
-			if k in d: d[k] = d[k].replace('0x','',1)
-		o = {
-			'from':     CoinAddr(d['sender']),
-			'to':       CoinAddr(d['to']) if d['to'] else Str(''), # NB: for token, 'to' is token address
-			'amt':      ETHAmt(d['value'],'wei'),
-			'gasPrice': ETHAmt(d['gasprice'],'wei'),
-			'startGas': ETHAmt(d['startgas'],'wei'),
-			'nonce':    ETHNonce(d['nonce']),
-			'data':     HexStr(d['data']) }
-		if o['data'] and not o['to']: # token- or contract-creating transaction
-			o['token_addr'] = TokenAddr(etx.creates.hex()) # NB: could be a non-token contract address
-			self.disable_fee_check = True
-		txid = CoinTxID(etx.hash.hex())
-		assert txid == self.coin_txid,"txid in tx.hex doesn't match value in MMGen transaction file"
-		self.tx_gas = o['startGas'] # approximate, but better than nothing
-		self.fee = self.fee_rel2abs(o['gasPrice'].toWei())
-		self.txobj = o
-		return d # 'token_addr','decimals' required by Token subclass
-
-	async def get_nonce(self):
-		return ETHNonce(int(await g.rpc.call('parity_nextNonce','0x'+self.inputs[0].addr),16))
-
-	async def make_txobj(self): # called by create_raw()
-		chain_id_method = ('parity_chainId','eth_chainId')['eth_chainId' in g.rpc.caps]
-		self.txobj = {
-			'from': self.inputs[0].addr,
-			'to':   self.outputs[0].addr if self.outputs else Str(''),
-			'amt':  self.outputs[0].amt if self.outputs else ETHAmt('0'),
-			'gasPrice': self.usr_rel_fee or self.fee_abs2rel(self.fee,to_unit='eth'),
-			'startGas': self.start_gas,
-			'nonce': await self.get_nonce(),
-			'chainId': Int(await g.rpc.call(chain_id_method),16),
-			'data':  self.usr_contract_data,
-		}
-
-	# Instead of serializing tx data as with BTC, just create a JSON dump.
-	# This complicates things but means we avoid using the rlp library to deserialize the data,
-	# thus removing an attack vector
-	async def create_raw(self):
-		assert len(self.inputs) == 1,'Transaction has more than one input!'
-		o_num = len(self.outputs)
-		o_ok = 0 if self.usr_contract_data else 1
-		assert o_num == o_ok,'Transaction has {} output{} (should have {})'.format(o_num,suf(o_num),o_ok)
-		await self.make_txobj()
-		odict = { k: str(v) for k,v in self.txobj.items() if k != 'token_to' }
-		self.hex = json.dumps(odict)
-		self.update_txid()
-
-	def del_output(self,idx):
-		pass
 
-	def update_txid(self):
-		assert not is_hex_str(self.hex),'update_txid() must be called only when self.hex is not hex data'
-		self.txid = MMGenTxID(make_chksum_6(self.hex).upper())
-
-	def process_cmd_args(self,cmd_args,ad_f,ad_w):
-		lc = len(cmd_args)
-		if lc == 0 and self.usr_contract_data and not 'Token' in type(self).__name__:
-			return
-		if lc != 1:
-			fs = '{} output{} specified, but Ethereum transactions must have exactly one'
-			die(1,fs.format(lc,suf(lc)))
-
-		for a in cmd_args:
-			self.process_cmd_arg(a,ad_f,ad_w)
-
-	def select_unspent(self,unspent):
-		prompt = 'Enter an account to spend from: '
-		while True:
-			reply = my_raw_input(prompt).strip()
-			if reply:
-				if not is_int(reply):
-					msg('Account number must be an integer')
-				elif int(reply) < 1:
-					msg('Account number must be >= 1')
-				elif int(reply) > len(unspent):
-					msg('Account number must be <= {}'.format(len(unspent)))
-				else:
-					return [int(reply)]
-
-	# coin-specific fee routines:
-	@property
-	def relay_fee(self):
-		return ETHAmt('0') # TODO
-
-	# given absolute fee in ETH, return gas price in Gwei using tx_gas
-	def fee_abs2rel(self,abs_fee,to_unit='Gwei'):
-		ret = ETHAmt(int(abs_fee.toWei() // self.tx_gas.toWei()),'wei')
-		dmsg('fee_abs2rel() ==> {} ETH'.format(ret))
-		return ret if to_unit == 'eth' else ret.to_unit(to_unit,show_decimal=True)
-
-	# get rel_fee (gas price) from network, return in native wei
-	async def get_rel_fee_from_network(self):
-		return Int(await g.rpc.call('eth_gasPrice'),16),'eth_gasPrice' # ==> rel_fee,fe_type
-
-	# given rel fee and units, return absolute fee using tx_gas
-	def convert_fee_spec(self,foo,units,amt,unit):
-		self.usr_rel_fee = ETHAmt(int(amt),units[unit])
-		return ETHAmt(self.usr_rel_fee.toWei() * self.tx_gas.toWei(),'wei')
-
-	# given rel fee in wei, return absolute fee using tx_gas (not in MMGenTX)
-	def fee_rel2abs(self,rel_fee):
-		assert isinstance(rel_fee,int),"'{}': incorrect type for fee estimate (not an integer)".format(rel_fee)
-		return ETHAmt(rel_fee * self.tx_gas.toWei(),'wei')
-
-	# given fee estimate (gas price) in wei, return absolute fee, adjusting by opt.tx_fee_adj
-	def fee_est2abs(self,rel_fee,fe_type=None):
-		ret = self.fee_rel2abs(rel_fee) * opt.tx_fee_adj
-		if opt.verbose:
-			msg('Estimated fee: {} ETH'.format(ret))
-		return ret
-
-	def convert_and_check_fee(self,tx_fee,desc='Missing description'):
-		abs_fee = self.process_fee_spec(tx_fee,None)
-		if abs_fee == False:
-			return False
-		elif not self.disable_fee_check and (abs_fee > g.proto.max_tx_fee):
-			m = '{} {c}: {} fee too large (maximum fee: {} {c})'
-			msg(m.format(abs_fee.hl(),desc,g.proto.max_tx_fee.hl(),c=g.coin))
-			return False
-		else:
-			return abs_fee
-
-	def update_change_output(self,change_amt):
-		if self.outputs and self.outputs[0].is_chg:
-			self.update_output_amt(0,ETHAmt(change_amt))
-
-	def update_send_amt(self,foo):
-		if self.outputs:
-			self.send_amt = self.outputs[0].amt
-
-	def format_view_body(self,blockcount,nonmm_str,max_mmwid,enl,terse,sort):
-		m = {}
-		for k in ('inputs','outputs'):
-			if len(getattr(self,k)):
-				m[k] = getattr(self,k)[0].mmid if len(getattr(self,k)) else ''
-				m[k] = ' ' + m[k].hl() if m[k] else ' ' + MMGenID.hlc(nonmm_str)
-		fs = """From:      {}{f_mmid}
-				To:        {}{t_mmid}
-				Amount:    {} {c}
-				Gas price: {g} Gwei
-				Start gas: {G} Kwei
-				Nonce:     {}
-				Data:      {d}
-				\n""".replace('\t','')
-		ld = len(self.txobj['data'])
-		return fs.format(   *((self.txobj[k] if self.txobj[k] != '' else Str('None')).hl() for k in self.fmt_keys),
-							d='{}... ({} bytes)'.format(self.txobj['data'][:40],ld//2) if ld else Str('None'),
-							c=g.dcoin if len(self.outputs) else '',
-							g=yellow(str(self.txobj['gasPrice'].to_unit('Gwei',show_decimal=True))),
-							G=yellow(str(self.txobj['startGas'].toKwei())),
-							t_mmid=m['outputs'] if len(self.outputs) else '',
-							f_mmid=m['inputs'])
-
-	def format_view_abs_fee(self):
-		fee = self.fee_rel2abs(self.txobj['gasPrice'].toWei())
-		note = ' (max)' if self.txobj['data'] else ''
-		return fee.hl() + note
-
-	def format_view_rel_fee(self,terse): return ''
-	def format_view_verbose_footer(self): return '' # TODO
-
-	def resolve_g_token_from_txfile(self):
-		die(2,"The '--token' option must be specified for token transaction files")
-
-	def final_inputs_ok_msg(self,change_amt):
-		m = "Transaction leaves {} {} in the sender's account"
-		chg = '0' if (self.outputs and self.outputs[0].is_chg) else change_amt
-		return m.format(ETHAmt(chg).hl(),g.coin)
-
-	async def get_status(self,status=False):
-
-		class r(object): pass
-
-		async def is_in_mempool():
-			if not 'full_node' in g.rpc.caps:
+		async def get_exec_status(self,txid,silent=False):
+			d = await self.rpc.call('eth_getTransactionReceipt','0x'+txid)
+			if not silent:
+				if 'contractAddress' in d and d['contractAddress']:
+					msg('Contract address: {}'.format(d['contractAddress'].replace('0x','')))
+			return int(d['status'],16)
+
+	class New(Base,MMGenTX.New):
+		hexdata_type = 'hex'
+		desc = 'transaction'
+		fee_fail_fs = 'Network fee estimation failed'
+		no_chg_msg = 'Warning: Transaction leaves account with zero balance'
+		usr_fee_prompt = 'Enter transaction fee or gas price: '
+		usr_rel_fee = None # not in MMGenTX
+
+		def __init__(self,*args,**kwargs):
+			MMGenTX.New.__init__(self,*args,**kwargs)
+			if getattr(opt,'tx_gas',None):
+				self.tx_gas = self.start_gas = ETHAmt(int(opt.tx_gas),'wei')
+			if getattr(opt,'contract_data',None):
+				m = "'--contract-data' option may not be used with token transaction"
+				assert not 'Token' in type(self).__name__, m
+				self.usr_contract_data = HexStr(open(opt.contract_data).read().strip())
+				self.disable_fee_check = True
+
+		async def get_nonce(self):
+			return ETHNonce(int(await self.rpc.call('parity_nextNonce','0x'+self.inputs[0].addr),16))
+
+		async def make_txobj(self): # called by create_raw()
+			chain_id_method = ('parity_chainId','eth_chainId')['eth_chainId' in self.rpc.caps]
+			self.txobj = {
+				'from': self.inputs[0].addr,
+				'to':   self.outputs[0].addr if self.outputs else Str(''),
+				'amt':  self.outputs[0].amt if self.outputs else ETHAmt('0'),
+				'gasPrice': self.usr_rel_fee or self.fee_abs2rel(self.fee,to_unit='eth'),
+				'startGas': self.start_gas,
+				'nonce': await self.get_nonce(),
+				'chainId': Int(await self.rpc.call(chain_id_method),16),
+				'data':  self.usr_contract_data,
+			}
+
+		# Instead of serializing tx data as with BTC, just create a JSON dump.
+		# This complicates things but means we avoid using the rlp library to deserialize the data,
+		# thus removing an attack vector
+		async def create_raw(self):
+			assert len(self.inputs) == 1,'Transaction has more than one input!'
+			o_num = len(self.outputs)
+			o_ok = 0 if self.usr_contract_data else 1
+			assert o_num == o_ok,'Transaction has {} output{} (should have {})'.format(o_num,suf(o_num),o_ok)
+			await self.make_txobj()
+			odict = { k: str(v) for k,v in self.txobj.items() if k != 'token_to' }
+			self.hex = json.dumps(odict)
+			self.update_txid()
+
+		def update_txid(self):
+			assert not is_hex_str(self.hex),'update_txid() must be called only when self.hex is not hex data'
+			self.txid = MMGenTxID(make_chksum_6(self.hex).upper())
+
+		def del_output(self,idx):
+			pass
+
+		def process_cmd_args(self,cmd_args,ad_f,ad_w):
+			lc = len(cmd_args)
+			if lc == 0 and self.usr_contract_data and not 'Token' in type(self).__name__:
+				return
+			if lc != 1:
+				die(1,f'{lc} output{suf(lc)} specified, but Ethereum transactions must have exactly one')
+
+			for a in cmd_args:
+				self.process_cmd_arg(a,ad_f,ad_w)
+
+		def select_unspent(self,unspent):
+			prompt = 'Enter an account to spend from: '
+			while True:
+				reply = my_raw_input(prompt).strip()
+				if reply:
+					if not is_int(reply):
+						msg('Account number must be an integer')
+					elif int(reply) < 1:
+						msg('Account number must be >= 1')
+					elif int(reply) > len(unspent):
+						msg('Account number must be <= {}'.format(len(unspent)))
+					else:
+						return [int(reply)]
+
+		# coin-specific fee routines:
+		@property
+		def relay_fee(self):
+			return ETHAmt('0') # TODO
+
+		# get rel_fee (gas price) from network, return in native wei
+		async def get_rel_fee_from_network(self):
+			return Int(await self.rpc.call('eth_gasPrice'),16),'eth_gasPrice' # ==> rel_fee,fe_type
+
+		def check_fee(self):
+			assert self.disable_fee_check or (self.fee <= self.proto.max_tx_fee)
+
+		# given rel fee and units, return absolute fee using tx_gas
+		def convert_fee_spec(self,foo,units,amt,unit):
+			self.usr_rel_fee = ETHAmt(int(amt),units[unit])
+			return ETHAmt(self.usr_rel_fee.toWei() * self.tx_gas.toWei(),'wei')
+
+		# given fee estimate (gas price) in wei, return absolute fee, adjusting by opt.tx_fee_adj
+		def fee_est2abs(self,rel_fee,fe_type=None):
+			ret = self.fee_rel2abs(rel_fee) * opt.tx_fee_adj
+			if opt.verbose:
+				msg('Estimated fee: {} ETH'.format(ret))
+			return ret
+
+		def convert_and_check_fee(self,tx_fee,desc='Missing description'):
+			abs_fee = self.process_fee_spec(tx_fee,None)
+			if abs_fee == False:
 				return False
-			return '0x'+self.coin_txid in [x['hash'] for x in await g.rpc.call('parity_pendingTransactions')]
+			elif not self.disable_fee_check and (abs_fee > self.proto.max_tx_fee):
+				m = '{} {c}: {} fee too large (maximum fee: {} {c})'
+				msg(m.format(abs_fee.hl(),desc,self.proto.max_tx_fee.hl(),c=self.proto.coin))
+				return False
+			else:
+				return abs_fee
+
+		def update_change_output(self,change_amt):
+			if self.outputs and self.outputs[0].is_chg:
+				self.update_output_amt(0,ETHAmt(change_amt))
+
+		def update_send_amt(self,foo):
+			if self.outputs:
+				self.send_amt = self.outputs[0].amt
+
+		async def get_cmdline_input_addrs(self):
+			ret = []
+			if opt.inputs:
+				r = (await TrackingWallet(self.proto)).data_root # must create new instance here
+				m = 'Address {!r} not in tracking wallet'
+				for i in opt.inputs.split(','):
+					if is_mmgen_id(self.proto,i):
+						for addr in r:
+							if r[addr]['mmid'] == i:
+								ret.append(addr)
+								break
+						else:
+							raise UserAddressNotInWallet(m.format(i))
+					elif is_coin_addr(self.proto,i):
+						if not i in r:
+							raise UserAddressNotInWallet(m.format(i))
+						ret.append(i)
+					else:
+						die(1,"'{}': not an MMGen ID or coin address".format(i))
+			return ret
+
+		def final_inputs_ok_msg(self,change_amt):
+			m = "Transaction leaves {} {} in the sender's account"
+			chg = '0' if (self.outputs and self.outputs[0].is_chg) else change_amt
+			return m.format(ETHAmt(chg).hl(),self.proto.coin)
+
+	class Completed(Base,MMGenTX.Completed):
+		fn_fee_unit = 'Mwei'
+		txview_hdr_fs = 'TRANSACTION DATA\n\nID={i} ({a} {c}) UTC={t} Sig={s} Locktime={l}\n'
+		txview_hdr_fs_short = 'TX {i} ({a} {c}) UTC={t} Sig={s} Locktime={l}\n'
+		txview_ftr_fs = 'Total in account: {i} {d}\nTotal to spend:   {o} {d}\nTX fee:           {a} {c}{r}\n'
+		txview_ftr_fs_short = 'In {i} {d} - Out {o} {d}\nFee {a} {c}{r}\n'
+		fmt_keys = ('from','to','amt','nonce')
+
+		def check_txfile_hex_data(self):
+			pass
+
+		def format_view_body(self,blockcount,nonmm_str,max_mmwid,enl,terse,sort):
+			m = {}
+			for k in ('inputs','outputs'):
+				if len(getattr(self,k)):
+					m[k] = getattr(self,k)[0].mmid if len(getattr(self,k)) else ''
+					m[k] = ' ' + m[k].hl() if m[k] else ' ' + MMGenID.hlc(nonmm_str)
+			fs = """From:      {}{f_mmid}
+					To:        {}{t_mmid}
+					Amount:    {} {c}
+					Gas price: {g} Gwei
+					Start gas: {G} Kwei
+					Nonce:     {}
+					Data:      {d}
+					\n""".replace('\t','')
+			t = self.txobj
+			td = t['data']
+			return fs.format(
+				*((t[k] if t[k] != '' else Str('None')).hl() for k in self.fmt_keys),
+				d      = '{}... ({} bytes)'.format(td[:40],len(td)//2) if len(td) else Str('None'),
+				c      = self.proto.dcoin if len(self.outputs) else '',
+				g      = yellow(str(t['gasPrice'].to_unit('Gwei',show_decimal=True))),
+				G      = yellow(str(t['startGas'].toKwei())),
+				t_mmid = m['outputs'] if len(self.outputs) else '',
+				f_mmid = m['inputs'] )
+
+		def format_view_abs_fee(self):
+			fee = self.fee_rel2abs(self.txobj['gasPrice'].toWei())
+			note = ' (max)' if self.txobj['data'] else ''
+			return fee.hl() + note
+
+		def format_view_rel_fee(self,terse):
+			return ''
+
+		def format_view_verbose_footer(self):
+			if self.txobj['data']:
+				from .contract import parse_abi
+				return '\nParsed contract data: ' + pp_fmt(parse_abi(self.txobj['data']))
+			else:
+				return ''
 
-		async def is_in_wallet():
-			d = await g.rpc.call('eth_getTransactionReceipt','0x'+self.coin_txid)
-			if d and 'blockNumber' in d and d['blockNumber'] is not None:
-				r.confs = 1 + int(await g.rpc.call('eth_blockNumber'),16) - int(d['blockNumber'],16)
-				r.exec_status = int(d['status'],16)
+		def check_sigs(self,deserial_tx=None): # TODO
+			if is_hex_str(self.hex):
 				return True
 			return False
 
-		if await is_in_mempool():
-			msg('Transaction is in mempool' if status else 'Warning: transaction is in mempool!')
-			return
+		def check_pubkey_scripts(self):
+			pass
+
+	class Unsigned(Completed,MMGenTX.Unsigned):
+		hexdata_type = 'json'
+		desc = 'unsigned transaction'
+
+		def parse_txfile_hex_data(self):
+			d = json.loads(self.hex)
+			o = {
+				'from':     CoinAddr(self.proto,d['from']),
+				# NB: for token, 'to' is sendto address
+				'to':       CoinAddr(self.proto,d['to']) if d['to'] else Str(''),
+				'amt':      ETHAmt(d['amt']),
+				'gasPrice': ETHAmt(d['gasPrice']),
+				'startGas': ETHAmt(d['startGas']),
+				'nonce':    ETHNonce(d['nonce']),
+				'chainId':  Int(d['chainId']),
+				'data':     HexStr(d['data']) }
+			self.tx_gas = o['startGas'] # approximate, but better than nothing
+			self.fee = self.fee_rel2abs(o['gasPrice'].toWei())
+			self.txobj = o
+			return d # 'token_addr','decimals' required by Token subclass
+
+		async def do_sign(self,wif,tx_num_str):
+			o = self.txobj
+			o_conv = {
+				'to':       bytes.fromhex(o['to']),
+				'startgas': o['startGas'].toWei(),
+				'gasprice': o['gasPrice'].toWei(),
+				'value':    o['amt'].toWei() if o['amt'] else 0,
+				'nonce':    o['nonce'],
+				'data':     bytes.fromhex(o['data']) }
+
+			from .pyethereum.transactions import Transaction
+			etx = Transaction(**o_conv).sign(wif,o['chainId'])
+			assert etx.sender.hex() == o['from'],(
+				'Sender address recovered from signature does not match true sender')
+
+			from . import rlp
+			self.hex = rlp.encode(etx).hex()
+			self.coin_txid = CoinTxID(etx.hash.hex())
+
+			if o['data']:
+				if o['to']:
+					assert self.txobj['token_addr'] == TokenAddr(etx.creates.hex()),'Token address mismatch'
+				else: # token- or contract-creating transaction
+					self.txobj['token_addr'] = TokenAddr(self.proto,etx.creates.hex())
+
+			assert self.check_sigs(),'Signature check failed'
+
+		async def sign(self,tx_num_str,keys): # return TX object or False; don't exit or raise exception
 
-		if status:
-			if await is_in_wallet():
-				if self.txobj['data']:
-					cd = capfirst(self.contract_desc)
-					if r.exec_status == 0:
-						msg('{} failed to execute!'.format(cd))
-					else:
-						msg('{} successfully executed with status {}'.format(cd,r.exec_status))
-				die(0,'Transaction has {} confirmation{}'.format(r.confs,suf(r.confs)))
-			die(1,'Transaction is neither in mempool nor blockchain!')
+			try:
+				self.check_correct_chain()
+			except TransactionChainMismatch:
+				return False
+
+			msg_r(f'Signing transaction{tx_num_str}...')
+
+			try:
+				await self.do_sign(keys[0].sec.wif,tx_num_str)
+				msg('OK')
+				return MMGenTX.Signed(data=self.__dict__)
+			except Exception as e:
+				msg("{e!s}: transaction signing failed!")
+				if g.traceback:
+					import traceback
+					ymsg('\n'+''.join(traceback.format_exception(*sys.exc_info())))
+				return False
 
-	async def send(self,prompt_user=True,exit_on_fail=False):
+	class Signed(Completed,MMGenTX.Signed):
+
+		desc = 'signed transaction'
+
+		def parse_txfile_hex_data(self):
+			from .pyethereum.transactions import Transaction
+			from . import rlp
+			etx = rlp.decode(bytes.fromhex(self.hex),Transaction)
+			d = etx.to_dict() # ==> hex values have '0x' prefix, 0 is '0x'
+			for k in ('sender','to','data'):
+				if k in d:
+					d[k] = d[k].replace('0x','',1)
+			o = {
+				'from':     CoinAddr(self.proto,d['sender']),
+				# NB: for token, 'to' is token address
+				'to':       CoinAddr(self.proto,d['to']) if d['to'] else Str(''),
+				'amt':      ETHAmt(d['value'],'wei'),
+				'gasPrice': ETHAmt(d['gasprice'],'wei'),
+				'startGas': ETHAmt(d['startgas'],'wei'),
+				'nonce':    ETHNonce(d['nonce']),
+				'data':     HexStr(d['data']) }
+			if o['data'] and not o['to']: # token- or contract-creating transaction
+				# NB: could be a non-token contract address:
+				o['token_addr'] = TokenAddr(self.proto,etx.creates.hex())
+				self.disable_fee_check = True
+			txid = CoinTxID(etx.hash.hex())
+			assert txid == self.coin_txid,"txid in tx.hex doesn't match value in MMGen transaction file"
+			self.tx_gas = o['startGas'] # approximate, but better than nothing
+			self.fee = self.fee_rel2abs(o['gasPrice'].toWei())
+			self.txobj = o
+			return d # 'token_addr','decimals' required by Token subclass
+
+		async def get_status(self,status=False):
+
+			class r(object):
+				pass
+
+			async def is_in_mempool():
+				if not 'full_node' in self.rpc.caps:
+					return False
+				return '0x'+self.coin_txid in [
+					x['hash'] for x in await self.rpc.call('parity_pendingTransactions') ]
+
+			async def is_in_wallet():
+				d = await self.rpc.call('eth_getTransactionReceipt','0x'+self.coin_txid)
+				if d and 'blockNumber' in d and d['blockNumber'] is not None:
+					r.confs = 1 + int(await self.rpc.call('eth_blockNumber'),16) - int(d['blockNumber'],16)
+					r.exec_status = int(d['status'],16)
+					return True
+				return False
 
-		if not self.marked_signed():
-			die(1,'Transaction is not signed!')
+			if await is_in_mempool():
+				msg('Transaction is in mempool' if status else 'Warning: transaction is in mempool!')
+				return
 
-		self.check_correct_chain()
+			if status:
+				if await is_in_wallet():
+					if self.txobj['data']:
+						cd = capfirst(self.contract_desc)
+						if r.exec_status == 0:
+							msg('{} failed to execute!'.format(cd))
+						else:
+							msg('{} successfully executed with status {}'.format(cd,r.exec_status))
+					die(0,'Transaction has {} confirmation{}'.format(r.confs,suf(r.confs)))
+				die(1,'Transaction is neither in mempool nor blockchain!')
 
-		fee = self.fee_rel2abs(self.txobj['gasPrice'].toWei())
+		async def send(self,prompt_user=True,exit_on_fail=False):
 
-		if not self.disable_fee_check and (fee > g.proto.max_tx_fee):
-			die(2,'Transaction fee ({}) greater than {} max_tx_fee ({} {})!'.format(
-				fee,
-				g.proto.name,
-				g.proto.max_tx_fee,
-				g.proto.coin ))
+			self.check_correct_chain()
 
-		await self.get_status()
+			fee = self.fee_rel2abs(self.txobj['gasPrice'].toWei())
 
-		if prompt_user:
-			self.confirm_send()
+			if not self.disable_fee_check and (fee > self.proto.max_tx_fee):
+				die(2,'Transaction fee ({}) greater than {} max_tx_fee ({} {})!'.format(
+					fee,
+					self.proto.name,
+					self.proto.max_tx_fee,
+					self.proto.coin ))
+
+			await self.get_status()
+
+			if prompt_user:
+				self.confirm_send()
 
-		if g.bogus_send:
-			ret = None
-		else:
-			try:
-				ret = await g.rpc.call('eth_sendRawTransaction','0x'+self.hex)
-			except:
-				raise
-				ret = False
-
-		if ret == False:
-			msg(red('Send of MMGen transaction {} failed'.format(self.txid)))
-			if exit_on_fail:
-				sys.exit(1)
-			return False
-		else:
 			if g.bogus_send:
-				m = 'BOGUS transaction NOT sent: {}'
+				ret = None
 			else:
-				m = 'Transaction sent: {}'
-				assert ret == '0x'+self.coin_txid,'txid mismatch (after sending)'
-			self.desc = 'sent transaction'
-			msg(m.format(self.coin_txid.hl()))
-			self.add_timestamp()
-			self.add_blockcount()
-			return True
-
-	async def get_cmdline_input_addrs(self):
-		ret = []
-		if opt.inputs:
-			r = (await TrackingWallet()).data_root # must create new instance here
-			m = 'Address {!r} not in tracking wallet'
-			for i in opt.inputs.split(','):
-				if is_mmgen_id(i):
-					for addr in r:
-						if r[addr]['mmid'] == i:
-							ret.append(addr)
-							break
-					else:
-						raise UserAddressNotInWallet(m.format(i))
-				elif is_coin_addr(i):
-					if not i in r:
-						raise UserAddressNotInWallet(m.format(i))
-					ret.append(i)
+				try:
+					ret = await self.rpc.call('eth_sendRawTransaction','0x'+self.hex)
+				except:
+					raise
+					ret = False
+
+			if ret == False:
+				msg(red('Send of MMGen transaction {} failed'.format(self.txid)))
+				if exit_on_fail:
+					sys.exit(1)
+				return False
+			else:
+				if g.bogus_send:
+					m = 'BOGUS transaction NOT sent: {}'
 				else:
-					die(1,"'{}': not an MMGen ID or coin address".format(i))
-		return ret
-
-	def print_contract_addr(self):
-		if 'token_addr' in self.txobj:
-			msg('Contract address: {}'.format(self.txobj['token_addr'].hl()))
-
-class EthereumMMGenTxForSigning(EthereumMMGenTX,MMGenTxForSigning):
-
-	def parse_txfile_hex_data(self):
-		d = json.loads(self.hex)
-		o = {
-			'from':     CoinAddr(d['from']),
-			'to':       CoinAddr(d['to']) if d['to'] else Str(''), # NB: for token, 'to' is sendto address
-			'amt':      ETHAmt(d['amt']),
-			'gasPrice': ETHAmt(d['gasPrice']),
-			'startGas': ETHAmt(d['startGas']),
-			'nonce':    ETHNonce(d['nonce']),
-			'chainId':  Int(d['chainId']),
-			'data':     HexStr(d['data']) }
-		self.tx_gas = o['startGas'] # approximate, but better than nothing
-		self.fee = self.fee_rel2abs(o['gasPrice'].toWei())
-		self.txobj = o
-		return d # 'token_addr','decimals' required by Token subclass
-
-	async def do_sign(self,wif,tx_num_str):
-		o = self.txobj
-		o_conv = {
-			'to':       bytes.fromhex(o['to']),
-			'startgas': o['startGas'].toWei(),
-			'gasprice': o['gasPrice'].toWei(),
-			'value':    o['amt'].toWei() if o['amt'] else 0,
-			'nonce':    o['nonce'],
-			'data':     bytes.fromhex(o['data']) }
-
-		from .pyethereum.transactions import Transaction
-		etx = Transaction(**o_conv).sign(wif,o['chainId'])
-		assert etx.sender.hex() == o['from'],(
-			'Sender address recovered from signature does not match true sender')
-
-		from . import rlp
-		self.hex = rlp.encode(etx).hex()
-		self.coin_txid = CoinTxID(etx.hash.hex())
-
-		if o['data']:
-			if o['to']:
-				assert self.txobj['token_addr'] == TokenAddr(etx.creates.hex()),'Token address mismatch'
-			else: # token- or contract-creating transaction
-				self.txobj['token_addr'] = TokenAddr(etx.creates.hex())
-
-		assert self.check_sigs(),'Signature check failed'
-
-	async def sign(self,tx_num_str,keys): # return True or False; don't exit or raise exception
-
-		if self.marked_signed():
-			msg('Transaction is already signed!')
-			return False
+					m = 'Transaction sent: {}'
+					assert ret == '0x'+self.coin_txid,'txid mismatch (after sending)'
+				self.desc = 'sent transaction'
+				msg(m.format(self.coin_txid.hl()))
+				self.add_timestamp()
+				self.add_blockcount()
+				return True
 
-		try:
-			self.check_correct_chain()
-		except TransactionChainMismatch:
-			return False
+		def print_contract_addr(self):
+			if 'token_addr' in self.txobj:
+				msg('Contract address: {}'.format(self.txobj['token_addr'].hl()))
 
-		msg_r('Signing transaction{}...'.format(tx_num_str))
+	class Bump(MMGenTX.Bump,Completed,New):
 
-		try:
-			await self.do_sign(keys[0].sec.wif,tx_num_str)
-			msg('OK')
-			return True
-		except Exception as e:
-			m = "{!r}: transaction signing failed!"
-			msg(m.format(e.args[0]))
-			if g.traceback:
-				import traceback
-				ymsg('\n'+''.join(traceback.format_exception(*sys.exc_info())))
-			return False
+		@property
+		def min_fee(self):
+			return ETHAmt(self.fee * Decimal('1.101'))
 
-class EthereumTokenMMGenTX(EthereumMMGenTX):
-	desc = 'Ethereum token transaction'
-	contract_desc = 'token contract'
-	tx_gas = ETHAmt(52000,'wei')
-	start_gas = ETHAmt(60000,'wei')
-	fmt_keys = ('from','token_to','amt','nonce')
-	fee_is_approximate = True
-
-	def update_change_output(self,change_amt):
-		if self.outputs[0].is_chg:
-			self.update_output_amt(0,self.inputs[0].amt)
-
-	# token transaction, so check both eth and token balances
-	# TODO: add test with insufficient funds
-	async def precheck_sufficient_funds(self,inputs_sum,sel_unspent):
-		eth_bal = await self.tw.get_eth_balance(sel_unspent[0].addr)
-		if eth_bal == 0: # we don't know the fee yet
-			msg('This account has no ether to pay for the transaction fee!')
-			return False
-		return await super().precheck_sufficient_funds(inputs_sum,sel_unspent)
+		def update_fee(self,foo,fee):
+			self.fee = fee
 
-	def final_inputs_ok_msg(self,change_amt):
-		token_bal   = ( ETHAmt('0') if self.outputs[0].is_chg else
-						self.inputs[0].amt - self.outputs[0].amt )
-		m = "Transaction leaves ≈{} {} and {} {} in the sender's account"
-		return m.format( change_amt.hl(), g.coin, token_bal.hl(), g.dcoin )
+		async def get_nonce(self):
+			return self.txobj['nonce']
 
-	async def get_change_amt(self): # here we know the fee
-		eth_bal = await self.tw.get_eth_balance(self.inputs[0].addr)
-		return eth_bal - self.fee
+class EthereumTokenMMGenTX:
 
-	def resolve_g_token_from_txfile(self):
-		pass
+	class Base(EthereumMMGenTX.Base):
+		tx_gas = ETHAmt(52000,'wei')
+		start_gas = ETHAmt(60000,'wei')
+		contract_desc = 'token contract'
 
-	async def make_txobj(self): # called by create_raw()
-		await super().make_txobj()
-		t = Token(self.tw.token,self.tw.decimals)
-		o = self.txobj
-		o['token_addr'] = t.addr
-		o['decimals'] = t.decimals
-		o['token_to'] = o['to']
-		o['data'] = t.create_data(o['token_to'],o['amt'])
-
-	def parse_txfile_hex_data(self):
-		d = EthereumMMGenTX.parse_txfile_hex_data(self)
-		o = self.txobj
-		assert self.tw.token == o['to']
-		o['token_addr'] = TokenAddr(o['to'])
-		o['decimals']   = self.tw.decimals
-		t = Token(o['token_addr'],o['decimals'])
-		o['amt'] = t.transferdata2amt(o['data'])
-		o['token_to'] = type(t).transferdata2sendaddr(o['data'])
-
-	def format_view_body(self,*args,**kwargs):
-		return 'Token:     {d} {c}\n{r}'.format(
-			d=self.txobj['token_addr'].hl(),
-			c=blue('(' + g.dcoin + ')'),
-			r=super().format_view_body(*args,**kwargs))
-
-class EthereumTokenMMGenTxForSigning(EthereumTokenMMGenTX,EthereumMMGenTxForSigning):
-
-	def resolve_g_token_from_txfile(self):
-		d = json.loads(self.hex)
-		if g.token.upper() == self.dcoin:
-			g.token = d['token_addr']
-		elif g.token != d['token_addr']:
-			die(1,
-			"{!r}: invalid --token parameter for {t} {} token transaction file\nPlease use '--token={t}'".format(
-				g.token,
-				g.proto.name,
-				t = self.dcoin ))
-
-	def parse_txfile_hex_data(self):
-		d = EthereumMMGenTxForSigning.parse_txfile_hex_data(self)
-		o = self.txobj
-		o['token_addr'] = TokenAddr(d['token_addr'])
-		o['decimals'] = Int(d['decimals'])
-		t = Token(o['token_addr'],o['decimals'])
-		o['data'] = t.create_data(o['to'],o['amt'])
-		o['token_to'] = type(t).transferdata2sendaddr(o['data'])
-
-	async def do_sign(self,wif,tx_num_str):
-		o = self.txobj
-		t = Token(o['token_addr'],o['decimals'])
-		tx_in = t.make_tx_in(o['from'],o['to'],o['amt'],self.start_gas,o['gasPrice'],nonce=o['nonce'])
-		(self.hex,self.coin_txid) = await t.txsign(tx_in,wif,o['from'],chain_id=o['chainId'])
-		assert self.check_sigs(),'Signature check failed'
-
-class EthereumMMGenBumpTX(MMGenBumpTX,EthereumMMGenTxForSigning):
-
-	@property
-	def min_fee(self):
-		return ETHAmt(self.fee * Decimal('1.101'))
-
-	def update_fee(self,foo,fee):
-		self.fee = fee
-
-	async def get_nonce(self):
-		return self.txobj['nonce']
-
-class EthereumTokenMMGenBumpTX(EthereumMMGenBumpTX,EthereumTokenMMGenTxForSigning):
-	pass
-
-class EthereumMMGenSplitTX(MMGenSplitTX):
-	pass
+	class New(Base,EthereumMMGenTX.New):
+		desc = 'transaction'
+		fee_is_approximate = True
+
+		async def make_txobj(self): # called by create_raw()
+			await super().make_txobj()
+			t = Token(self.proto,self.tw.token,self.tw.decimals)
+			o = self.txobj
+			o['token_addr'] = t.addr
+			o['decimals'] = t.decimals
+			o['token_to'] = o['to']
+			o['data'] = t.create_data(o['token_to'],o['amt'])
+
+		def update_change_output(self,change_amt):
+			if self.outputs[0].is_chg:
+				self.update_output_amt(0,self.inputs[0].amt)
+
+		# token transaction, so check both eth and token balances
+		# TODO: add test with insufficient funds
+		async def precheck_sufficient_funds(self,inputs_sum,sel_unspent):
+			eth_bal = await self.tw.get_eth_balance(sel_unspent[0].addr)
+			if eth_bal == 0: # we don't know the fee yet
+				msg('This account has no ether to pay for the transaction fee!')
+				return False
+			return await super().precheck_sufficient_funds(inputs_sum,sel_unspent)
+
+		async def get_change_amt(self): # here we know the fee
+			eth_bal = await self.tw.get_eth_balance(self.inputs[0].addr)
+			return eth_bal - self.fee
+
+		def final_inputs_ok_msg(self,change_amt):
+			token_bal   = ( ETHAmt('0') if self.outputs[0].is_chg else
+							self.inputs[0].amt - self.outputs[0].amt )
+			m = "Transaction leaves ≈{} {} and {} {} in the sender's account"
+			return m.format( change_amt.hl(), self.proto.coin, token_bal.hl(), self.proto.dcoin )
+
+	class Completed(Base,EthereumMMGenTX.Completed):
+		fmt_keys = ('from','token_to','amt','nonce')
+
+		def format_view_body(self,*args,**kwargs):
+			return 'Token:     {d} {c}\n{r}'.format(
+				d=self.txobj['token_addr'].hl(),
+				c=blue('(' + self.proto.dcoin + ')'),
+				r=super().format_view_body(*args,**kwargs))
+
+	class Unsigned(Completed,EthereumMMGenTX.Unsigned):
+		desc = 'unsigned transaction'
+
+		def parse_txfile_hex_data(self):
+			d = EthereumMMGenTX.Unsigned.parse_txfile_hex_data(self)
+			o = self.txobj
+			o['token_addr'] = TokenAddr(self.proto,d['token_addr'])
+			o['decimals'] = Int(d['decimals'])
+			t = Token(self.proto,o['token_addr'],o['decimals'])
+			o['data'] = t.create_data(o['to'],o['amt'])
+			o['token_to'] = t.transferdata2sendaddr(o['data'])
+
+		async def do_sign(self,wif,tx_num_str):
+			o = self.txobj
+			t = Token(self.proto,o['token_addr'],o['decimals'])
+			tx_in = t.make_tx_in(o['from'],o['to'],o['amt'],self.start_gas,o['gasPrice'],nonce=o['nonce'])
+			(self.hex,self.coin_txid) = await t.txsign(tx_in,wif,o['from'],chain_id=o['chainId'])
+			assert self.check_sigs(),'Signature check failed'
+
+	class Signed(Completed,EthereumMMGenTX.Signed):
+		desc = 'signed transaction'
+
+		def parse_txfile_hex_data(self):
+			d = EthereumMMGenTX.Signed.parse_txfile_hex_data(self)
+			o = self.txobj
+			assert self.tw.token == o['to']
+			o['token_addr'] = TokenAddr(self.proto,o['to'])
+			o['decimals']   = self.tw.decimals
+			t = Token(self.proto,o['token_addr'],o['decimals'])
+			o['amt'] = t.transferdata2amt(o['data'])
+			o['token_to'] = t.transferdata2sendaddr(o['data'])
+
+	class Bump(EthereumMMGenTX.Bump,Completed,New):
+		pass

+ 0 - 214
mmgen/common.py

@@ -26,217 +26,3 @@ from .globalvars import *
 import mmgen.opts as opts
 from .opts import opt
 from .util import *
-
-def help_notes(k):
-	from .obj import SubSeedIdxRange,SeedShareIdx,SeedShareCount,MasterShareIdx
-	from .wallet import Wallet
-	from .tx import MMGenTX
-	def fee_spec_letters(use_quotes=False):
-		cu = g.proto.coin_amt.units
-		sep,conj = ((',',' or '),("','","' or '"))[use_quotes]
-		return sep.join(u[0] for u in cu[:-1]) + ('',conj)[len(cu)>1] + cu[-1][0]
-	def fee_spec_names():
-		cu = g.proto.coin_amt.units
-		return ', '.join(cu[:-1]) + ('',' and ')[len(cu)>1] + cu[-1] + ('',',\nrespectively')[len(cu)>1]
-	return {
-		'rel_fee_desc': MMGenTX().rel_fee_desc,
-		'fee_spec_letters': fee_spec_letters(),
-		'seedsplit': """
-COMMAND NOTES:
-
-This command generates shares one at a time.  Shares may be output to any
-MMGen wallet format, with one limitation: only one share in a given split may
-be in hidden incognito format, and it must be the master share in the case of
-a master-share split.
-
-If the command's optional first argument is omitted, the default wallet is
-used for the split.
-
-The last argument is a seed split specifier consisting of an optional split
-ID, a share index, and a share count, all separated by colons.  The split ID
-must be a valid UTF-8 string.  If omitted, the ID 'default' is used.  The
-share index (the index of the share being generated) must be in the range
-{sia}-{sib} and the share count (the total number of shares in the split)
-in the range {sca}-{scb}.
-
-Master Shares
-
-Each seed has a total of {msb} master shares, which can be used as the first
-shares in multiple splits if desired.  To generate a master share, use the
---master-share (-M) option with an index in the range {msa}-{msb} and omit
-the last argument.
-
-When creating and joining a split using a master share, ensure that the same
-master share index is used in all split and join commands.
-
-EXAMPLES:
-
-  Split a BIP39 seed phrase into two BIP39 shares.  Rejoin the split:
-
-    $ echo 'zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong' > sample.bip39
-
-    $ mmgen-seedsplit -o bip39 sample.bip39 1:2
-    BIP39 mnemonic data written to file '03BAE887-default-1of2[D51CB683][128].bip39'
-
-    $ mmgen-seedsplit -o bip39 sample.bip39 2:2
-    BIP39 mnemonic data written to file '03BAE887-default-2of2[67BFD36E][128].bip39'
-
-    $ mmgen-seedjoin -o bip39 \\
-        '03BAE887-default-2of2[67BFD36E][128].bip39' \\
-        '03BAE887-default-1of2[D51CB683][128].bip39'
-    BIP39 mnemonic data written to file '03BAE887[128].bip39'
-
-    $ cat '03BAE887[128].bip39'
-    zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong
-
-  Create a 3-way default split of your default wallet, outputting all shares
-  to default wallet format.  Rejoin the split:
-
-    $ mmgen-seedsplit 1:3 # Step A
-    $ mmgen-seedsplit 2:3 # Step B
-    $ mmgen-seedsplit 3:3 # Step C
-    $ mmgen-seedjoin <output_of_step_A> <output_of_step_B> <output_of_step_C>
-
-  Create a 2-way split of your default wallet with ID string 'alice',
-  outputting shares to MMGen native mnemonic format.  Rejoin the split:
-
-    $ mmgen-seedsplit -o words alice:1:2 # Step D
-    $ mmgen-seedsplit -o words alice:2:2 # Step E
-    $ mmgen-seedjoin <output_of_step_D> <output_of_step_E>
-
-  Create a 2-way split of your default wallet with ID string 'bob' using
-  master share #7, outputting share #1 (the master share) to default wallet
-  format and share #2 to BIP39 format.  Rejoin the split:
-
-    $ mmgen-seedsplit -M7                   # Step X
-    $ mmgen-seedsplit -M7 -o bip39 bob:2:2  # Step Y
-    $ mmgen-seedjoin -M7 --id-str=bob <output_of_step_X> <output_of_step_Y>
-
-  Create a 2-way split of your default wallet with ID string 'alice' using
-  master share #7.  Rejoin the split using master share #7 generated in the
-  previous example:
-
-    $ mmgen-seedsplit -M7 -o bip39 alice:2:2 # Step Z
-    $ mmgen-seedjoin -M7 --id-str=alice <output_of_step_X> <output_of_step_Z>
-
-  Create a 2-way default split of your default wallet with an incognito-format
-  master share hidden in file 'my.hincog' at offset 1325.  Rejoin the split:
-
-    $ mmgen-seedsplit -M4 -o hincog -J my.hincog,1325 1:2 # Step M (share A)
-    $ mmgen-seedsplit -M4 -o bip39 2:2                    # Step N (share B)
-    $ mmgen-seedjoin -M4 -H my.hincog,1325 <output_of_step_N>
-
-""".strip().format(
-	sia=SeedShareIdx.min_val,sib=SeedShareIdx.max_val,
-	sca=SeedShareCount.min_val,scb=SeedShareCount.max_val,
-	msa=MasterShareIdx.min_val,msb=MasterShareIdx.max_val),
-		'subwallet': """
-SUBWALLETS:
-
-Subwallets (subseeds) are specified by a "Subseed Index" consisting of:
-
-  a) an integer in the range 1-{}, plus
-  b) an optional single letter, 'L' or 'S'
-
-The letter designates the length of the subseed.  If omitted, 'L' is assumed.
-
-Long ('L') subseeds are the same length as their parent wallet's seed
-(typically 256 bits), while short ('S') subseeds are always 128-bit.
-The long and short subseeds for a given index are derived independently,
-so both may be used.
-
-MMGen has no notion of "depth", and to an outside observer subwallets are
-identical to ordinary wallets.  This is a feature rather than a bug, as it
-denies an attacker any way of knowing whether a given wallet has a parent.
-
-Since subwallets are just wallets, they may be used to generate other
-subwallets, leading to hierarchies of arbitrary depth.  However, this is
-inadvisable in practice for two reasons:  Firstly, it creates accounting
-complexity, requiring the user to independently keep track of a derivation
-tree.  More importantly, however, it leads to the danger of Seed ID
-collisions between subseeds at different levels of the hierarchy, as
-MMGen checks and avoids ID collisions only among sibling subseeds.
-
-An exception to this caveat would be a multi-user setup where sibling
-subwallets are distributed to different users as their default wallets.
-Since the subseeds derived from these subwallets are private to each user,
-Seed ID collisions among them doesn't present a problem.
-
-A safe rule of thumb, therefore, is for *each user* to derive all of his/her
-subwallets from a single parent.  This leaves each user with a total of two
-million subwallets, which should be enough for most practical purposes.
-""".strip().format(SubSeedIdxRange.max_idx),
-		'passwd': """
-PASSPHRASE NOTE:
-
-For passphrases all combinations of whitespace are equal, and leading and
-trailing space are ignored.  This permits reading passphrase or brainwallet
-data from a multi-line file with free spacing and indentation.
-""".strip(),
-		'brainwallet': """
-BRAINWALLET NOTE:
-
-To thwart dictionary attacks, it's recommended to use a strong hash preset
-with brainwallets.  For a brainwallet passphrase to generate the correct
-seed, the same seed length and hash preset parameters must always be used.
-""".strip(),
-		'txcreate': """
-The transaction's outputs are specified on the command line, while its inputs
-are chosen from a list of the user's unspent 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 network fee
-estimation for the default (or user-specified) number of confirmations.
-If network fee estimation fails, the user will be prompted for a fee.
-
-Network-estimated 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.avg_bdi} 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,pnu=g.proto.name),
-		'fee': """
-FEE SPECIFICATION: Transaction fees, both on the command line and at the
-interactive prompt, may be specified as either absolute {c} amounts, using
-a plain decimal number, or as {r}, using an integer followed by
-'{l}', for {u}.
-""".format( c=g.coin,
-			r=MMGenTX().rel_fee_desc,
-			l=fee_spec_letters(use_quotes=True),
-			u=fee_spec_names() ),
-		'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.
-""".format( dn=g.proto.daemon_name,
-			pnm=g.proj_name,
-			pnu=g.proto.name,
-			pnl=g.proj_name.lower())
-	}[k] + ('-α' if g.debug_utf8 else '')
-
-def exit_if_mswin(feature):
-	if g.platform == 'win':
-		m = capfirst(feature) + ' not supported on the MSWin / MSYS2 platform'
-		ydie(1,m)

+ 29 - 21
mmgen/daemon.py

@@ -306,20 +306,20 @@ class CoinDaemon(Daemon):
 'etc': cd('Ethereum Classic','Ethereum','parity',      'parity',      'parity.conf',   None,     8545, 8545,8545)
 	}
 
-	def __new__(cls,network_id,test_suite=False,flags=None):
+	def __new__(cls,network_id=None,test_suite=False,flags=None,proto=None):
 
-		network_id = network_id.lower()
-		assert network_id in cls.network_ids, '{!r}: invalid network ID'.format(network_id)
+		assert network_id or proto, 'CoinDaemon_chk1'
+		assert not (network_id and proto), 'CoinDaemon_chk2'
 
-		if network_id.endswith('_rt'):
-			network = 'regtest'
-			daemon_id = network_id[:-3]
-		elif network_id.endswith('_tn'):
-			network = 'testnet'
-			daemon_id = network_id[:-3]
+		if proto:
+			network_id = proto.network_id
+			network    = proto.network
+			daemon_id  = proto.coin.lower()
 		else:
-			network = 'mainnet'
-			daemon_id = network_id
+			network_id = network_id.lower()
+			assert network_id in cls.network_ids, '{!r}: invalid network ID'.format(network_id)
+			from mmgen.protocol import CoinProtocol
+			daemon_id,network = CoinProtocol.Base.parse_network_id(network_id)
 
 		me = Daemon.__new__(globals()[cls.daemon_ids[daemon_id].cls_pfx+'Daemon'])
 		me.network_id = network_id
@@ -336,22 +336,30 @@ class CoinDaemon(Daemon):
 					'regtest',
 					daemon_id )
 			else:
-				me.datadir = os.path.join(g.data_dir_root,'regtest',daemon_id)
+				datadir = os.path.join(g.data_dir_root,'regtest',daemon_id)
 		elif test_suite:
 			me.desc = 'test suite daemon'
 			rel_datadir = os.path.join('test','daemons',daemon_id)
 		else:
-			from .protocol import init_proto
-			me.datadir = init_proto(daemon_id,False).daemon_data_dir
+			if proto:
+				datadir = proto.daemon_data_dir
+			else:
+				from .protocol import init_proto
+				datadir = init_proto(coin=daemon_id,testnet=False).daemon_data_dir
 
 		if test_suite:
-			me.datadir = os.path.abspath(os.path.join(os.getcwd(),rel_datadir))
+			datadir = os.path.join(os.getcwd(),rel_datadir)
+
+		if g.daemon_data_dir: # user-set value must override
+			datadir = g.daemon_data_dir
+
+		me.datadir = os.path.abspath(datadir)
 
 		me.port_shift = 1237 if test_suite else 0
 		me.platform = g.platform
 		return me
 
-	def __init__(self,network_id,test_suite=False,flags=None):
+	def __init__(self,network_id=None,test_suite=False,flags=None,proto=None):
 		super().__init__()
 
 		self.testnet_arg = []
@@ -386,6 +394,9 @@ class CoinDaemon(Daemon):
 				'regtest': self.dfl_rpc_rt,
 			}[self.network] + self.port_shift
 
+		if g.rpc_port: # user-set value must override
+			self.rpc_port = g.rpc_port
+
 		self.net_desc = '{} {}'.format(self.coin,self.network)
 		self.subclass_init()
 
@@ -547,14 +558,11 @@ class EthereumDaemon(CoinDaemon):
 
 		# the following code does not work
 		async def do():
-			print(g.rpc)
-			ret = await g.rpc.call('eth_chainId')
-			print(ret)
+			ret = await self.rpc.call('eth_chainId')
 			return ('stopped','ready')[ret == '0x11']
 
-		from mmgen.protocol import init_proto
 		try:
-			return run_session(do(),proto=init_proto('eth')) # socket exception is not propagated
+			return run_session(do()) # socket exception is not propagated
 		except:# SocketError:
 			return 'stopped'
 

+ 2 - 0
mmgen/exception.py

@@ -47,9 +47,11 @@ class TransactionChainMismatch(Exception):mmcode = 2
 
 # 3: yellow hl, 'MMGen Error' + exception + message
 class RPCFailure(Exception):              mmcode = 3
+class RPCChainMismatch(Exception):        mmcode = 3
 class BadTxSizeEstimate(Exception):       mmcode = 3
 class MaxInputSizeExceeded(Exception):    mmcode = 3
 class MaxFileSizeExceeded(Exception):     mmcode = 3
+class MaxFeeExceeded(Exception):          mmcode = 3
 class WalletFileError(Exception):         mmcode = 3
 class HexadecimalStringError(Exception):  mmcode = 3
 class SeedLengthError(Exception):         mmcode = 3

+ 1 - 12
mmgen/globalvars.py

@@ -72,6 +72,7 @@ class GlobalContext:
 
 	# Constant vars - some of these might be overridden in opts.py, but they don't change thereafter
 
+	coin                 = ''
 	token                = ''
 	debug                = False
 	debug_opts           = False
@@ -86,9 +87,6 @@ class GlobalContext:
 	accept_defaults      = False
 	use_internal_keccak_module = False
 
-	chain                = None
-	chains               = ('mainnet','testnet','regtest')
-
 	# rpc:
 	rpc_host             = ''
 	rpc_port             = 0
@@ -98,7 +96,6 @@ class GlobalContext:
 	monero_wallet_rpc_user = 'monero'
 	monero_wallet_rpc_password = ''
 	rpc_fail_on_command  = ''
-	rpc                  = None # global RPC handle
 	aiohttp_rpc_queue_len = 16
 	use_cached_balances  = False
 
@@ -280,12 +277,4 @@ class GlobalContext:
 			if name[:11] == 'MMGEN_DEBUG':
 				os.environ[name] = '1'
 
-	@property
-	def coin(self):
-		return self.proto.coin
-
-	@property
-	def dcoin(self):
-		return self.proto.dcoin
-
 g = GlobalContext()

+ 256 - 0
mmgen/help.py

@@ -0,0 +1,256 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
+# Copyright (C)2013-2020 The MMGen Project <mmgen@tuta.io>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+"""
+help.py:  help notes for MMGen suite commands
+"""
+
+def help_notes_func(proto,k):
+	from .globalvars import g
+
+	def fee_spec_letters(use_quotes=False):
+		cu = proto.coin_amt.units
+		sep,conj = ((',',' or '),("','","' or '"))[use_quotes]
+		return sep.join(u[0] for u in cu[:-1]) + ('',conj)[len(cu)>1] + cu[-1][0]
+
+	def fee_spec_names():
+		cu = proto.coin_amt.units
+		return ', '.join(cu[:-1]) + ('',' and ')[len(cu)>1] + cu[-1] + ('',',\nrespectively')[len(cu)>1]
+
+	class help_notes:
+
+		def rel_fee_desc():
+			from .tx import MMGenTX
+			return MMGenTX.Base().rel_fee_desc
+
+		def fee_spec_letters():
+			return fee_spec_letters()
+
+		def fee():
+			from .tx import MMGenTX
+			return """
+FEE SPECIFICATION: Transaction fees, both on the command line and at the
+interactive prompt, may be specified as either absolute {c} amounts, using
+a plain decimal number, or as {r}, using an integer followed by
+'{l}', for {u}.
+""".format(
+	c = proto.coin,
+	r = MMGenTX.Base().rel_fee_desc,
+	l = fee_spec_letters(use_quotes=True),
+	u = fee_spec_names() )
+
+		def passwd():
+			return """
+PASSPHRASE NOTE:
+
+For passphrases all combinations of whitespace are equal, and leading and
+trailing space are ignored.  This permits reading passphrase or brainwallet
+data from a multi-line file with free spacing and indentation.
+""".strip()
+
+		def brainwallet():
+			return """
+BRAINWALLET NOTE:
+
+To thwart dictionary attacks, it's recommended to use a strong hash preset
+with brainwallets.  For a brainwallet passphrase to generate the correct
+seed, the same seed length and hash preset parameters must always be used.
+""".strip()
+
+		def txcreate():
+			return f"""
+The transaction's outputs are specified on the command line, while its inputs
+are chosen from a list of the user's unspent 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 network fee
+estimation for the default (or user-specified) number of confirmations.
+If network fee estimation fails, the user will be prompted for a fee.
+
+Network-estimated 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 {proto.avg_bdi} seconds.
+
+All addresses on the command line can be either {proto.name} addresses or {g.proj_name}
+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.
+"""
+
+		def txsign():
+			return """
+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.
+""".format(
+	dn  = proto.daemon_name,
+	pnm = g.proj_name,
+	pnu = proto.name,
+	pnl = g.proj_name.lower() )
+
+		def seedsplit():
+			from .obj import SeedShareIdx,SeedShareCount,MasterShareIdx
+			return """
+COMMAND NOTES:
+
+This command generates shares one at a time.  Shares may be output to any
+MMGen wallet format, with one limitation: only one share in a given split may
+be in hidden incognito format, and it must be the master share in the case of
+a master-share split.
+
+If the command's optional first argument is omitted, the default wallet is
+used for the split.
+
+The last argument is a seed split specifier consisting of an optional split
+ID, a share index, and a share count, all separated by colons.  The split ID
+must be a valid UTF-8 string.  If omitted, the ID 'default' is used.  The
+share index (the index of the share being generated) must be in the range
+{si.min_val}-{si.max_val} and the share count (the total number of shares in the split)
+in the range {sc.min_val}-{sc.max_val}.
+
+Master Shares
+
+Each seed has a total of {mi.max_val} master shares, which can be used as the first
+shares in multiple splits if desired.  To generate a master share, use the
+--master-share (-M) option with an index in the range {mi.min_val}-{mi.max_val} and omit
+the last argument.
+
+When creating and joining a split using a master share, ensure that the same
+master share index is used in all split and join commands.
+
+EXAMPLES:
+
+ Split a BIP39 seed phrase into two BIP39 shares.  Rejoin the split:
+
+   $ echo 'zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong' > sample.bip39
+
+   $ mmgen-seedsplit -o bip39 sample.bip39 1:2
+   BIP39 mnemonic data written to file '03BAE887-default-1of2[D51CB683][128].bip39'
+
+   $ mmgen-seedsplit -o bip39 sample.bip39 2:2
+   BIP39 mnemonic data written to file '03BAE887-default-2of2[67BFD36E][128].bip39'
+
+   $ mmgen-seedjoin -o bip39 \\
+       '03BAE887-default-2of2[67BFD36E][128].bip39' \\
+       '03BAE887-default-1of2[D51CB683][128].bip39'
+   BIP39 mnemonic data written to file '03BAE887[128].bip39'
+
+   $ cat '03BAE887[128].bip39'
+   zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong
+
+ Create a 3-way default split of your default wallet, outputting all shares
+ to default wallet format.  Rejoin the split:
+
+   $ mmgen-seedsplit 1:3 # Step A
+   $ mmgen-seedsplit 2:3 # Step B
+   $ mmgen-seedsplit 3:3 # Step C
+   $ mmgen-seedjoin <output_of_step_A> <output_of_step_B> <output_of_step_C>
+
+ Create a 2-way split of your default wallet with ID string 'alice',
+ outputting shares to MMGen native mnemonic format.  Rejoin the split:
+
+   $ mmgen-seedsplit -o words alice:1:2 # Step D
+   $ mmgen-seedsplit -o words alice:2:2 # Step E
+   $ mmgen-seedjoin <output_of_step_D> <output_of_step_E>
+
+ Create a 2-way split of your default wallet with ID string 'bob' using
+ master share #7, outputting share #1 (the master share) to default wallet
+ format and share #2 to BIP39 format.  Rejoin the split:
+
+   $ mmgen-seedsplit -M7                   # Step X
+   $ mmgen-seedsplit -M7 -o bip39 bob:2:2  # Step Y
+   $ mmgen-seedjoin -M7 --id-str=bob <output_of_step_X> <output_of_step_Y>
+
+ Create a 2-way split of your default wallet with ID string 'alice' using
+ master share #7.  Rejoin the split using master share #7 generated in the
+ previous example:
+
+   $ mmgen-seedsplit -M7 -o bip39 alice:2:2 # Step Z
+   $ mmgen-seedjoin -M7 --id-str=alice <output_of_step_X> <output_of_step_Z>
+
+ Create a 2-way default split of your default wallet with an incognito-format
+ master share hidden in file 'my.hincog' at offset 1325.  Rejoin the split:
+
+   $ mmgen-seedsplit -M4 -o hincog -J my.hincog,1325 1:2 # Step M (share A)
+   $ mmgen-seedsplit -M4 -o bip39 2:2                    # Step N (share B)
+   $ mmgen-seedjoin -M4 -H my.hincog,1325 <output_of_step_N>
+
+""".strip().format(
+	si = SeedShareIdx,
+	sc = SeedShareCount,
+	mi = MasterShareIdx )
+
+		def subwallet():
+			from .obj import SubSeedIdxRange
+			return f"""
+SUBWALLETS:
+
+Subwallets (subseeds) are specified by a "Subseed Index" consisting of:
+
+ a) an integer in the range 1-{SubSeedIdxRange.max_idx}, plus
+ b) an optional single letter, 'L' or 'S'
+
+The letter designates the length of the subseed.  If omitted, 'L' is assumed.
+
+Long ('L') subseeds are the same length as their parent wallet's seed
+(typically 256 bits), while short ('S') subseeds are always 128-bit.
+The long and short subseeds for a given index are derived independently,
+so both may be used.
+
+MMGen has no notion of "depth", and to an outside observer subwallets are
+identical to ordinary wallets.  This is a feature rather than a bug, as it
+denies an attacker any way of knowing whether a given wallet has a parent.
+
+Since subwallets are just wallets, they may be used to generate other
+subwallets, leading to hierarchies of arbitrary depth.  However, this is
+inadvisable in practice for two reasons:  Firstly, it creates accounting
+complexity, requiring the user to independently keep track of a derivation
+tree.  More importantly, however, it leads to the danger of Seed ID
+collisions between subseeds at different levels of the hierarchy, as
+MMGen checks and avoids ID collisions only among sibling subseeds.
+
+An exception to this caveat would be a multi-user setup where sibling
+subwallets are distributed to different users as their default wallets.
+Since the subseeds derived from these subwallets are private to each user,
+Seed ID collisions among them doesn't present a problem.
+
+A safe rule of thumb, therefore, is for *each user* to derive all of his/her
+subwallets from a single parent.  This leaves each user with a total of two
+million subwallets, which should be enough for most practical purposes.
+""".strip()
+
+	return getattr(help_notes,k)() + ('-α' if g.debug_utf8 else '')

+ 23 - 7
mmgen/main_addrgen.py

@@ -23,16 +23,18 @@ mmgen-addrgen: Generate a series or range of addresses from an MMGen
 
 from .common import *
 from .crypto import *
-from .addr import *
+from .addr import AddrList,KeyAddrList,KeyList,MMGenAddrType,AddrIdxList
 from .wallet import Wallet
 
 if g.prog_name == 'mmgen-keygen':
 	gen_what = 'keys'
+	gen_clsname = 'KeyAddrList'
 	gen_desc = 'secret keys'
 	opt_filter = None
 	note_addrkey = 'By default, both addresses and secret keys are generated.\n\n'
 else:
 	gen_what = 'addresses'
+	gen_clsname = 'AddrList'
 	gen_desc = 'addresses'
 	opt_filter = 'hbcdeEiHOkKlpzPqrStUv-'
 	note_addrkey = ''
@@ -102,16 +104,16 @@ FMT CODES:
 """
 	},
 	'code': {
-		'options': lambda s: s.format(
+		'options': lambda proto,s: s.format(
 			seed_lens=', '.join(map(str,g.seed_lens)),
-			dmat="'{}' or '{}'".format(g.proto.dfl_mmtype,MMGenAddrType.mmtypes[g.proto.dfl_mmtype].name),
+			dmat="'{}' or '{}'".format(proto.dfl_mmtype,MMGenAddrType.mmtypes[proto.dfl_mmtype].name),
 			kgs=' '.join(['{}:{}'.format(n,k) for n,k in enumerate(g.key_generators,1)]),
 			kg=g.key_generator,
 			pnm=g.proj_name,
 			what=gen_what,
 			g=g,
 		),
-		'notes': lambda s: s.format(
+		'notes': lambda help_notes,s: s.format(
 			n_addrkey=note_addrkey,
 			n_sw=help_notes('subwallet')+'\n\n',
 			n_pw=help_notes('passwd')+'\n\n',
@@ -126,7 +128,14 @@ FMT CODES:
 cmd_args = opts.init(opts_data,add_opts=['b16'],opt_filter=opt_filter)
 
 errmsg = "'{}': invalid parameter for --type option".format(opt.type)
-addr_type = MMGenAddrType(opt.type or g.proto.dfl_mmtype,errmsg=errmsg)
+
+from .protocol import init_proto_from_opts
+proto = init_proto_from_opts()
+
+addr_type = MMGenAddrType(
+	proto = proto,
+	id_str = opt.type or proto.dfl_mmtype,
+	errmsg = errmsg )
 
 if len(cmd_args) < 1: opts.usage()
 
@@ -143,8 +152,15 @@ ss = Wallet(sf)
 
 ss_seed = ss.seed if opt.subwallet is None else ss.seed.subseed(opt.subwallet,print_msg=True)
 
-i = (gen_what=='addresses') or bool(opt.no_addresses)*2
-al = (KeyAddrList,AddrList,KeyList)[i](seed=ss_seed,addr_idxs=idxs,mmtype=addr_type)
+if opt.no_addresses:
+	gen_clsname = 'KeyList'
+
+al = globals()[gen_clsname](
+	proto     = proto,
+	seed      = ss_seed,
+	addr_idxs = idxs,
+	mmtype    = addr_type )
+
 al.format()
 
 if al.gen_addrs and opt.print_checksum:

+ 27 - 23
mmgen/main_addrimport.py

@@ -24,7 +24,7 @@ import time
 
 from .common import *
 from .addr import AddrList,KeyAddrList
-from .obj import TwLabel,is_coin_addr
+from .obj import TwLabel
 
 ai_msgs = lambda k: {
 	'rescan': """
@@ -61,6 +61,7 @@ opts_data = {
 -q, --quiet        Suppress warnings
 -r, --rescan       Rescan the blockchain.  Required if address to import is
                    in the blockchain and has a balance.  Rescanning is slow.
+-t, --token-addr=A Import addresses for ERC20 token with address 'A'
 """,
 	'notes': """\n
 This command can also be used to update the comment fields of addresses
@@ -71,13 +72,12 @@ The --batch and --rescan options cannot be used together.
 	}
 }
 
-def parse_cmd_args(cmd_args):
+def parse_cmd_args(rpc,cmd_args):
 
 	def import_mmgen_list(infile):
-		al = (AddrList,KeyAddrList)[bool(opt.keyaddr_file)](infile)
+		al = (AddrList,KeyAddrList)[bool(opt.keyaddr_file)](proto,infile)
 		if al.al_id.mmtype in ('S','B'):
-			from .tx import segwit_is_active
-			if not segwit_is_active():
+			if not rpc.info('segwit_is_active'):
 				rdie(2,'Segwit is not active on this chain. Cannot import Segwit addresses')
 		return al
 
@@ -85,14 +85,14 @@ def parse_cmd_args(cmd_args):
 		infile = cmd_args[0]
 		check_infile(infile)
 		if opt.addrlist:
-			al = AddrList(addrlist=get_lines_from_file(
-				infile,
-				'non-{pnm} addresses'.format(pnm=g.proj_name),
-				trim_comments=True))
+			al = AddrList(
+				proto = proto,
+				addrlist = get_lines_from_file(infile,'non-{pnm} addresses'.format(pnm=g.proj_name),
+				trim_comments = True) )
 		else:
 			al = import_mmgen_list(infile)
 	elif len(cmd_args) == 0 and opt.address:
-		al = AddrList(addrlist=[opt.address])
+		al = AddrList(proto=proto,addrlist=[opt.address])
 		infile = 'command line'
 	else:
 		die(1,ai_msgs('bad_args'))
@@ -145,17 +145,28 @@ def make_args_list(tw,al,batch,rescan):
 			label = '{}:{}'.format(al.al_id,e.idx) + (' ' + e.label if e.label else '')
 			add_msg = label
 		else:
-			label = '{}:{}'.format(g.proto.base_coin.lower(),e.addr)
+			label = '{}:{}'.format(proto.base_coin.lower(),e.addr)
 			add_msg = 'non-'+g.proj_name
 
 		if batch:
-			yield (e.addr,TwLabel(label),False)
+			yield (e.addr,TwLabel(proto,label),False)
 		else:
 			msg_args = ( f'{num}/{al.num_addrs}:', e.addr, '('+add_msg+')' )
-			yield (tw,e.addr,TwLabel(label),rescan,fs,msg_args)
+			yield (tw,e.addr,TwLabel(proto,label),rescan,fs,msg_args)
 
 async def main():
-	al,infile = parse_cmd_args(cmd_args)
+	from .tw import TrackingWallet
+	if opt.token_addr:
+		proto.tokensym = 'foo' # hack to trigger 'Token' in altcoin_subclass()
+	tw = await TrackingWallet(
+		proto      = proto,
+		token_addr = opt.token_addr,
+		mode       = 'i' )
+
+	from .rpc import rpc_init
+	tw.rpc = await rpc_init(proto)
+
+	al,infile = parse_cmd_args(tw.rpc,cmd_args)
 
 	qmsg(
 		f'OK. {al.num_addrs} addresses'
@@ -165,17 +176,8 @@ async def main():
 		f'Importing {len(al.data)} address{suf(al.data,"es")} from {infile}'
 		+ (' (batch mode)' if opt.batch else '') )
 
-	if not al.data[0].addr.is_for_chain(g.chain):
-		die(2,f'Address{(" list","")[bool(opt.address)]} incompatible with {g.chain} chain!')
-
-	from .tw import TrackingWallet
-	tw = await TrackingWallet(mode='i')
-
 	batch,rescan = check_opts(tw)
 
-	if g.token:
-		await tw.import_token()
-
 	args_list = make_args_list(tw,al,batch,rescan)
 
 	if batch:
@@ -192,5 +194,7 @@ async def main():
 	del tw
 
 cmd_args = opts.init(opts_data)
+from .protocol import init_proto_from_opts
+proto = init_proto_from_opts()
 import asyncio
 run_session(main())

+ 21 - 46
mmgen/main_autosign.py

@@ -134,17 +134,13 @@ async def check_daemons_running():
 		coins = ['BTC']
 
 	for coin in coins:
-		g.proto = init_proto(coin,g.proto.testnet)
-		if g.proto.sign_mode == 'daemon':
-			if g.test_suite:
-				g.proto.daemon_data_dir = 'test/daemons/' + coin.lower()
-				g.rpc_port = CoinDaemon(get_network_id(coin,g.proto.testnet),test_suite=True).rpc_port
+		proto = init_proto(coin,testnet=g.testnet)
+		if proto.sign_mode == 'daemon':
 			vmsg(f'Checking {coin} daemon')
 			try:
-				await rpc_init()
-			except SystemExit as e:
-				if e.code != 0:
-					ydie(1,f'{coin} daemon not running or not listening on port {g.proto.rpc_port}')
+				await rpc_init(proto)
+			except SocketError as e:
+				ydie(1,f'{coin} daemon not running or not listening on port {proto.rpc_port}')
 
 def get_wallet_files():
 	try:
@@ -175,45 +171,23 @@ def do_umount():
 		msg(f'Unmounting {mountpoint}')
 		run(['umount',mountpoint],check=True)
 
-async def sign_tx_file(txfile,signed_txs):
+async def sign_tx_file(txfile):
+	from .tx import MMGenTX
 	try:
-		g.proto = init_proto('BTC',testnet=False)
-		tmp_tx = mmgen.tx.MMGenTX(txfile,metadata_only=True)
-		g.proto = init_proto(tmp_tx.coin)
-
-		if tmp_tx.chain != 'mainnet':
-			if tmp_tx.chain == 'testnet' or (
-				hasattr(g.proto,'chain_name') and tmp_tx.chain != g.proto.chain_name):
-				g.proto = init_proto(tmp_tx.coin,testnet=True)
-
-		if hasattr(g.proto,'chain_name'):
-			if tmp_tx.chain != g.proto.chain_name:
-				die(2, f'Chains do not match! tx file: {tmp_tx.chain}, proto: {g.proto.chain_name}')
-
-		g.chain = tmp_tx.chain
-		g.token = tmp_tx.dcoin
-		g.proto.dcoin = tmp_tx.dcoin or g.proto.coin
-
-		tx = mmgen.tx.MMGenTxForSigning(txfile)
-
-		if g.proto.sign_mode == 'daemon':
-			if g.test_suite:
-				g.proto.daemon_data_dir = 'test/daemons/' + g.coin.lower()
-				g.rpc_port = CoinDaemon(get_network_id(g.coin,g.proto.testnet),test_suite=True).rpc_port
-			await rpc_init()
-
-		if await txsign(tx,wfs,None,None):
-			tx.write_to_file(ask_write=False)
-			signed_txs.append(tx)
-			return True
+		tx1 = MMGenTX.Unsigned(filename=txfile)
+		if tx1.proto.sign_mode == 'daemon':
+			tx1.rpc = await rpc_init(tx1.proto)
+		tx2 = await txsign(tx1,wfs,None,None)
+		if tx2:
+			tx2.write_to_file(ask_write=False)
+			return tx2
 		else:
 			return False
 	except Exception as e:
-		msg(f'An error occurred: {e.args[0]}')
-		if g.debug or g.traceback:
-			print_stack_trace(f'AUTOSIGN {txfile}')
+		ymsg(f'An error occurred with transaction {txfile!r}:\n    {e!s}')
 		return False
 	except:
+		ymsg(f'An error occurred with transaction {txfile!r}')
 		return False
 
 async def sign():
@@ -224,8 +198,10 @@ async def sign():
 	if unsigned:
 		signed_txs,fails = [],[]
 		for txfile in unsigned:
-			ret = await sign_tx_file(txfile,signed_txs)
-			if not ret:
+			ret = await sign_tx_file(txfile)
+			if ret:
+				signed_txs.append(ret)
+			else:
 				fails.append(txfile)
 			qmsg('')
 		time.sleep(0.3)
@@ -265,7 +241,6 @@ def print_summary(signed_txs):
 		bmsg('\nAutosign summary:\n')
 		def gen():
 			for tx in signed_txs:
-				g.proto = init_proto(tx.coin,testnet=tx.chain=='testnet')
 				yield tx.format_view(terse=True)
 		msg_r(''.join(gen()))
 		return
@@ -444,4 +419,4 @@ async def main():
 	elif cmd_args[0] == 'wait':
 		await do_loop()
 
-run_session(main(),do_rpc_init=False)
+run_session(main())

+ 6 - 1
mmgen/main_passgen.py

@@ -120,7 +120,7 @@ FMT CODES:
 			dpf=PasswordList.dfl_pw_fmt,
 			kgs=' '.join(['{}:{}'.format(n,k) for n,k in enumerate(g.key_generators,1)])
 		),
-		'notes': lambda s: s.format(
+		'notes': lambda help_notes,s: s.format(
 				o=opts,g=g,i58=pwi['b58'],i32=pwi['b32'],i39=pwi['bip39'],
 				ml=MMGenPWIDString.max_len,
 				fs="', '".join(MMGenPWIDString.forbidden),
@@ -147,7 +147,11 @@ sf = get_seed_file(cmd_args,1)
 pw_fmt = opt.passwd_fmt or PasswordList.dfl_pw_fmt
 pw_len = pwi[pw_fmt].dfl_len // 2 if opt.passwd_len in ('h','H') else opt.passwd_len
 
+from .protocol import init_proto
+proto = init_proto('btc') # TODO: get rid of dummy proto
+
 PasswordList(
+	proto           = proto,
 	pw_id_str       = pw_id_str,
 	pw_len          = pw_len,
 	pw_fmt          = pw_fmt,
@@ -158,6 +162,7 @@ do_license_msg()
 ss = Wallet(sf)
 
 al = PasswordList(
+	proto     = proto,
 	seed      = ss.seed,
 	pw_idxs   = pw_idxs,
 	pw_id_str = pw_id_str,

+ 1 - 1
mmgen/main_seedjoin.py

@@ -82,7 +82,7 @@ FMT CODES:
 			ms_max=MasterShareIdx.max_val,
 			g=g,
 		),
-		'notes': lambda s: s.format(
+		'notes': lambda help_notes,s: s.format(
 			f='\n  '.join(Wallet.format_fmt_codes().splitlines()),
 			n_pw=help_notes('passwd'),
 		)

+ 17 - 10
mmgen/main_split.py

@@ -80,19 +80,23 @@ transaction reconfirmed before the timelock expires. Use at your own risk.
 """.format(pnm=g.proj_name)
 	},
 	'code': {
-		'options': lambda s: s.format(
-			oc=g.proto.forks[-1][2].upper(),
+		'options': lambda proto,s: s.format(
+			oc=proto.forks[-1][2].upper(),
 			bh='current block height'),
 	}
 }
 
 cmd_args = opts.init(opts_data,add_opts=['tx_fee','tx_fee_adj','comment_file'])
 
+from .protocol import init_proto_from_opts
+proto = init_proto_from_opts()
+
 die(1,'This command is disabled')
 
-opt.other_coin = opt.other_coin.upper() if opt.other_coin else g.proto.forks[-1][2].upper()
-if opt.other_coin.lower() not in [e[2] for e in g.proto.forks if e[3] == True]:
-	die(1,"'{}': not a replayable fork of {} chain".format(opt.other_coin,g.coin))
+# the following code is broken:
+opt.other_coin = opt.other_coin.upper() if opt.other_coin else proto.forks[-1][2].upper()
+if opt.other_coin.lower() not in [e[2] for e in proto.forks if e[3] == True]:
+	die(1,"'{}': not a replayable fork of {} chain".format(opt.other_coin,proto.coin))
 
 if len(cmd_args) != 2:
 	fs = 'This command requires exactly two {} addresses as arguments'
@@ -111,8 +115,8 @@ from .tx import MMGenSplitTX
 from .protocol import init_proto
 
 if opt.tx_fees:
-	for idx,g_coin in ((1,opt.other_coin),(0,g.coin)):
-		g.proto = init_proto(g_coin)
+	for idx,g_coin in ((1,opt.other_coin),(0,proto.coin)):
+		proto = init_proto(g_coin)
 		opt.tx_fee = opt.tx_fees.split(',')[idx]
 		opts.opt_is_tx_fee('foo',opt.tx_fee,'transaction fee') # raises exception on error
 
@@ -120,8 +124,11 @@ tx1 = MMGenSplitTX()
 opt.no_blank = True
 
 async def main():
-	gmsg("Creating timelocked transaction for long chain ({})".format(g.coin))
-	locktime = int(opt.locktime or 0) or await g.rpc.call('getblockcount')
+	gmsg("Creating timelocked transaction for long chain ({})".format(proto.coin))
+	locktime = int(opt.locktime)
+	if not locktime:
+		rpc = rpc_init(proto)
+		locktime = rpc.call('getblockcount')
 	tx1.create(mmids[0],locktime)
 
 	tx1.format()
@@ -129,7 +136,7 @@ async def main():
 
 	gmsg("\nCreating transaction for short chain ({})".format(opt.other_coin))
 
-	g.proto = init_proto(opt.other_coin)
+	proto = init_proto(opt.other_coin)
 
 	tx2 = MMGenSplitTX()
 	tx2.inputs = tx1.inputs

+ 36 - 19
mmgen/main_txbump.py

@@ -81,7 +81,7 @@ column below:
 """
 	},
 	'code': {
-		'options': lambda s: s.format(
+		'options': lambda help_notes,proto,s: s.format(
 			g=g,
 			pnm=g.proj_name,
 			pnl=g.proj_name.lower(),
@@ -89,8 +89,8 @@ column below:
 			fu=help_notes('rel_fee_desc'),fl=help_notes('fee_spec_letters'),
 			kgs=' '.join(['{}:{}'.format(n,k) for n,k in enumerate(g.key_generators,1)]),
 			kg=g.key_generator,
-			cu=g.coin),
-		'notes': lambda s: s.format(
+			cu=proto.coin),
+		'notes': lambda help_notes,s: s.format(
 			help_notes('fee'),
 			help_notes('txsign'),
 			f='\n  '.join(Wallet.format_fmt_codes().splitlines()))
@@ -107,41 +107,56 @@ from .txsign import *
 
 seed_files = get_seed_files(opt,cmd_args) if (cmd_args or opt.send) else None
 
-kal = get_keyaddrlist(opt)
-kl = get_keylist(opt)
-
-sign_and_send = bool(seed_files or kl or kal)
-
 do_license_msg()
 
 silent = opt.yes and opt.tx_fee != None and opt.output_to_reduce != None
 
+ext = get_extension(tx_file)
+ext_data = {
+	MMGenTX.Unsigned.ext: 'Unsigned',
+	MMGenTX.Signed.ext:   'Signed',
+}
+if ext not in ext_data:
+	die(1,f'{ext!r}: unrecognized file extension')
+
 async def main():
 
-	from .tw import TrackingWallet
-	tx = MMGenBumpTX(filename=tx_file,send=sign_and_send,tw=await TrackingWallet() if g.token else None)
+	orig_tx = getattr(MMGenTX,ext_data[ext])(filename=tx_file)
 
 	if not silent:
 		msg(green('ORIGINAL TRANSACTION'))
-		msg(tx.format_view(terse=True))
+		msg(orig_tx.format_view(terse=True))
+
+	kal = get_keyaddrlist(orig_tx.proto,opt)
+	kl = get_keylist(orig_tx.proto,opt)
+	sign_and_send = bool(seed_files or kl or kal)
+
+	from .tw import TrackingWallet
+	tx = MMGenTX.Bump(
+		data = orig_tx.__dict__,
+		send = sign_and_send,
+		tw   = await TrackingWallet(orig_tx.proto) if orig_tx.proto.tokensym else None )
+
+	from .rpc import rpc_init
+	tx.rpc = await rpc_init(tx.proto)
 
 	tx.check_bumpable() # needs cached networkinfo['relayfee']
 
-	msg('Creating new transaction')
+	msg('Creating replacement transaction')
 
 	op_idx = tx.choose_output()
 
 	if not silent:
-		msg('Minimum fee for new transaction: {} {}'.format(tx.min_fee.hl(),g.coin))
+		msg('Minimum fee for new transaction: {} {}'.format(tx.min_fee.hl(),tx.proto.coin))
 
 	fee = tx.get_usr_fee_interactive(tx_fee=opt.tx_fee,desc='User-selected')
 
 	tx.update_fee(op_idx,fee)
 
 	d = tx.get_fee()
-	assert d == fee and d <= g.proto.max_tx_fee
+	assert d == fee and d <= tx.proto.max_tx_fee
 
-	if g.proto.base_proto == 'Bitcoin':
+	if tx.proto.base_proto == 'Bitcoin':
 		tx.outputs.sort_bip69() # output amts have changed, so re-sort
 
 	if not opt.yes:
@@ -159,10 +174,12 @@ async def main():
 		msg_r(tx.format_view(terse=True))
 
 	if sign_and_send:
-		if await txsign(tx,seed_files,kl,kal):
-			tx.write_to_file(ask_write=False)
-			await tx.send(exit_on_fail=True)
-			tx.write_to_file(ask_write=False)
+		tx2 = MMGenTX.Unsigned(data=tx.__dict__)
+		tx3 = await txsign(tx2,seed_files,kl,kal)
+		if tx3:
+			tx3.write_to_file(ask_write=False)
+			await tx3.send(exit_on_fail=True)
+			tx3.write_to_file(ask_write=False)
 		else:
 			die(2,'Transaction could not be signed')
 	else:

+ 23 - 6
mmgen/main_txcreate.py

@@ -61,14 +61,14 @@ opts_data = {
 		'notes': '\n{}{}',
 	},
 	'code': {
-		'options': lambda s: s.format(
+		'options': lambda proto,help_notes,s: s.format(
 			fu=help_notes('rel_fee_desc'),
 			fl=help_notes('fee_spec_letters'),
 			fe_all=fmt_list(g.autoset_opts['fee_estimate_mode'].choices,fmt='no_spc'),
 			fe_dfl=g.autoset_opts['fee_estimate_mode'].choices[0],
-			cu=g.coin,
+			cu=proto.coin,
 			g=g),
-		'notes': lambda s: s.format(
+		'notes': lambda help_notes,s: s.format(
 			help_notes('txcreate'),
 			help_notes('fee'))
 	}
@@ -79,10 +79,27 @@ cmd_args = opts.init(opts_data)
 g.use_cached_balances = opt.cached_balances
 
 async def main():
+
+	from .protocol import init_proto_from_opts
+	proto = init_proto_from_opts()
+
 	from .tx import MMGenTX
 	from .tw import TrackingWallet
-	tx = MMGenTX(tw=await TrackingWallet() if g.token else None)
-	await tx.create(cmd_args,int(opt.locktime or 0),do_info=opt.info)
-	tx.write_to_file(ask_write=not opt.yes,ask_overwrite=not opt.yes,ask_write_default_yes=False)
+	tx1 = MMGenTX.New(
+		proto = proto,
+		tw    = await TrackingWallet(proto) if proto.tokensym else None )
+
+	from .rpc import rpc_init
+	tx1.rpc = await rpc_init(proto)
+
+	tx2 = await tx1.create(
+		cmd_args = cmd_args,
+		locktime = int(opt.locktime or 0),
+		do_info  = opt.info )
+
+	tx2.write_to_file(
+		ask_write             = not opt.yes,
+		ask_overwrite         = not opt.yes,
+		ask_write_default_yes = False )
 
 run_session(main())

+ 28 - 16
mmgen/main_txdo.py

@@ -93,7 +93,7 @@ column below:
 """
 	},
 	'code': {
-		'options': lambda s: s.format(
+		'options': lambda proto,help_notes,s: s.format(
 			g=g,pnm=g.proj_name,pnl=g.proj_name.lower(),
 			kgs=' '.join(['{}:{}'.format(n,k) for n,k in enumerate(g.key_generators,1)]),
 			fu=help_notes('rel_fee_desc'),
@@ -103,8 +103,8 @@ column below:
 			fe_all=fmt_list(g.autoset_opts['fee_estimate_mode'].choices,fmt='no_spc'),
 			fe_dfl=g.autoset_opts['fee_estimate_mode'].choices[0],
 			kg=g.key_generator,
-			cu=g.coin),
-		'notes': lambda s: s.format(
+			cu=proto.coin),
+		'notes': lambda help_notes,s: s.format(
 			help_notes('txcreate'),
 			help_notes('fee'),
 			help_notes('txsign'),
@@ -121,24 +121,36 @@ from .txsign import *
 
 seed_files = get_seed_files(opt,cmd_args)
 
-kal = get_keyaddrlist(opt)
-kl = get_keylist(opt)
-if kl and kal:
-	kl.remove_dup_keys(kal)
-
 async def main():
 	from .tw import TrackingWallet
-	tx1 = MMGenTX(caller='txdo',tw=await TrackingWallet() if g.token else None)
 
-	await tx1.create(cmd_args,int(opt.locktime or 0))
+	from .protocol import init_proto_from_opts
+	proto = init_proto_from_opts()
+
+	tx1 = MMGenTX.New(
+		proto = proto,
+		tw    = await TrackingWallet(proto) if proto.tokensym else None )
+
+	from .rpc import rpc_init
+	tx1.rpc = await rpc_init(proto)
+
+	tx2 = await tx1.create(
+		cmd_args = cmd_args,
+		locktime = int(opt.locktime or 0),
+		caller   = 'txdo' )
+
+	kal = get_keyaddrlist(proto,opt)
+	kl = get_keylist(proto,opt)
+	if kl and kal:
+		kl.remove_dup_keys(kal)
 
-	tx2 = MMGenTxForSigning(data=tx1.__dict__)
+	tx3 = await txsign(tx2,seed_files,kl,kal)
 
-	if await txsign(tx2,seed_files,kl,kal):
-		tx2.write_to_file(ask_write=False)
-		await tx2.send(exit_on_fail=True)
-		tx2.write_to_file(ask_overwrite=False,ask_write=False)
-		tx2.print_contract_addr()
+	if tx3:
+		tx3.write_to_file(ask_write=False)
+		await tx3.send(exit_on_fail=True)
+		tx3.write_to_file(ask_overwrite=False,ask_write=False)
+		tx3.print_contract_addr()
 	else:
 		die(2,'Transaction could not be signed')
 

+ 12 - 13
mmgen/main_txsend.py

@@ -41,37 +41,36 @@ opts_data = {
 cmd_args = opts.init(opts_data)
 
 if len(cmd_args) == 1:
-	infile = cmd_args[0]; check_infile(infile)
+	infile = cmd_args[0]
+	check_infile(infile)
 else:
 	opts.usage()
 
 if not opt.status:
 	do_license_msg()
 
-from .tx import *
-
 async def main():
 
-	from .tw import TrackingWallet
-	tx = MMGenTX(infile,quiet_open=True,tw=await TrackingWallet() if g.token else None)
+	from .tx import MMGenTX
 
-	if g.token:
-		from .tw import TrackingWallet
-		tx.tw = await TrackingWallet()
+	tx = MMGenTX.Signed(
+		filename   = infile,
+		quiet_open = True,
+		tw         = await MMGenTX.Signed.get_tracking_wallet(infile) )
 
-	vmsg("Signed transaction file '{}' is valid".format(infile))
+	from .rpc import rpc_init
+	tx.rpc = await rpc_init(tx.proto)
 
-	if not tx.marked_signed():
-		die(1,'Transaction is not signed!')
+	vmsg(f'Signed transaction file {infile!r} is valid')
 
 	if opt.status:
 		if tx.coin_txid:
-			qmsg('{} txid: {}'.format(g.coin,tx.coin_txid.hl()))
+			qmsg(f'{tx.proto.coin} txid: {tx.coin_txid.hl()}')
 		await tx.get_status(status=True)
 		sys.exit(0)
 
 	if not opt.yes:
-		tx.view_with_prompt('View transaction data?')
+		tx.view_with_prompt('View transaction details?')
 		if tx.add_comment(): # edits an existing comment, returns true if changed
 			tx.write_to_file(ask_write_default_yes=True)
 

+ 26 - 25
mmgen/main_txsign.py

@@ -77,17 +77,17 @@ column below:
 """
 	},
 	'code': {
-		'options': lambda s: s.format(
+		'options': lambda proto,s: s.format(
 			g=g,
 			pnm=g.proj_name,
 			pnl=g.proj_name.lower(),
-			dn=g.proto.daemon_name,
+			dn=proto.daemon_name,
 			kgs=' '.join(['{}:{}'.format(n,k) for n,k in enumerate(g.key_generators,1)]),
 			kg=g.key_generator,
 			ss=g.subseeds,
 			ss_max=SubSeedIdxRange.max_idx,
-			cu=g.coin),
-		'notes': lambda s: s.format(
+			cu=proto.coin),
+		'notes': lambda help_notes,s: s.format(
 			help_notes('txsign'),
 			f='\n  '.join(Wallet.format_fmt_codes().splitlines()))
 	}
@@ -108,43 +108,47 @@ from .txsign import *
 
 tx_files   = get_tx_files(opt,infiles)
 seed_files = get_seed_files(opt,infiles)
-kal        = get_keyaddrlist(opt)
-kl         = get_keylist(opt)
-
-if kl and kal:
-	kl.remove_dup_keys(kal)
 
 async def main():
+
 	bad_tx_count = 0
 	tx_num_disp = ''
+
 	for tx_num,tx_file in enumerate(tx_files,1):
+
 		if len(tx_files) > 1:
-			msg('\nTransaction #{} of {}:'.format(tx_num,len(tx_files)))
 			tx_num_disp = f' #{tx_num}'
+			msg(f'\nTransaction{tx_num_disp} of {len(tx_files)}:')
 
-		tx = MMGenTxForSigning(tx_file)
-
-		if tx.marked_signed():
-			msg('Transaction is already signed!')
-			continue
+		tx1 = MMGenTX.Unsigned(filename=tx_file)
 
 		vmsg(f'Successfully opened transaction file {tx_file!r}')
 
+		if tx1.proto.sign_mode == 'daemon':
+			from .rpc import rpc_init
+			tx1.rpc = await rpc_init(tx1.proto)
+
 		if opt.tx_id:
-			msg(tx.txid)
+			msg(tx1.txid)
 			continue
 
 		if opt.info or opt.terse_info:
-			tx.view(pause=False,terse=opt.terse_info)
+			tx1.view(pause=False,terse=opt.terse_info)
 			continue
 
 		if not opt.yes:
-			tx.view_with_prompt(f'View data for transaction{tx_num_disp}?')
+			tx1.view_with_prompt(f'View data for transaction{tx_num_disp}?')
+
+		kal = get_keyaddrlist(tx1.proto,opt)
+		kl = get_keylist(tx1.proto,opt)
+		if kl and kal:
+			kl.remove_dup_keys(kal)
 
-		if await txsign(tx,seed_files,kl,kal,tx_num_disp):
+		tx2 = await txsign(tx1,seed_files,kl,kal,tx_num_disp)
+		if tx2:
 			if not opt.yes:
-				tx.add_comment() # edits an existing comment
-			tx.write_to_file(ask_write=not opt.yes,ask_write_default_yes=True,add_desc=tx_num_disp)
+				tx2.add_comment() # edits an existing comment
+			tx2.write_to_file(ask_write=not opt.yes,ask_write_default_yes=True,add_desc=tx_num_disp)
 		else:
 			ymsg('Transaction could not be signed')
 			bad_tx_count += 1
@@ -152,7 +156,4 @@ async def main():
 	if bad_tx_count:
 		ydie(2,f'{bad_tx_count} transaction{suf(bad_tx_count)} could not be signed')
 
-run_session(
-	main(),
-	do_rpc_init = g.proto.sign_mode == 'daemon'
-)
+run_session(main())

+ 1 - 1
mmgen/main_wallet.py

@@ -132,7 +132,7 @@ FMT CODES:
 			ms_max=MasterShareIdx.max_val,
 			g=g,
 		),
-		'notes': lambda s: s.format(
+		'notes': lambda help_notes,s: s.format(
 			f='\n  '.join(Wallet.format_fmt_codes().splitlines()),
 			n_ss=('',help_notes('seedsplit')+'\n\n')[do_ss_note],
 			n_sw=('',help_notes('subwallet')+'\n\n')[do_sw_note],

+ 78 - 73
mmgen/obj.py

@@ -36,14 +36,13 @@ class aInitMeta(type):
 
 def is_mmgen_seed_id(s):        return SeedID(sid=s,on_fail='silent')
 def is_mmgen_idx(s):            return AddrIdx(s,on_fail='silent')
-def is_mmgen_id(s):             return MMGenID(s,on_fail='silent')
-def is_coin_addr(s):            return CoinAddr(s,on_fail='silent')
 def is_addrlist_id(s):          return AddrListID(s,on_fail='silent')
-def is_tw_label(s):             return TwLabel(s,on_fail='silent')
-def is_wif(s):                  return WifKey(s,on_fail='silent')
-def is_viewkey(s):              return ViewKey(s,on_fail='silent')
 def is_seed_split_specifier(s): return SeedSplitSpecifier(s,on_fail='silent')
 
+def is_mmgen_id(proto,s):  return MMGenID(proto,s,on_fail='silent')
+def is_coin_addr(proto,s): return CoinAddr(proto,s,on_fail='silent')
+def is_wif(proto,s):       return WifKey(proto,s,on_fail='silent')
+
 def truncate_str(s,width): # width = screen width
 	wide_count = 0
 	for i in range(len(s)):
@@ -87,7 +86,7 @@ class IndexedDict(dict):
 
 class MMGenList(list,MMGenObject): pass
 class MMGenDict(dict,MMGenObject): pass
-class AddrListList(list,MMGenObject): pass
+class AddrListData(list,MMGenObject): pass
 
 class InitErrors(object):
 
@@ -230,16 +229,21 @@ class ImmutableAttr: # Descriptor
 	"""
 	ok_dtypes = (str,type,type(None),type(lambda:0))
 
-	def __init__(self,dtype,typeconv=True,set_none_ok=False):
+	def __init__(self,dtype,typeconv=True,set_none_ok=False,include_proto=False):
 		assert isinstance(dtype,self.ok_dtypes), 'ImmutableAttr_check1'
+		if include_proto: assert typeconv and type(dtype) == str, 'ImmutableAttr_check2'
 		if set_none_ok:   assert typeconv and type(dtype) != str, 'ImmutableAttr_check3'
 
-		if type(dtype).__name__ == 'function':
-			self.conv = lambda instance,value: dtype(value)
+		if dtype is None:
+			'use instance-defined conversion function for this attribute'
+			self.conv = lambda instance,value: getattr(instance.conv_funcs,self.name)(instance,value)
 		elif typeconv:
 			"convert this attribute's type"
 			if type(dtype) == str:
-				self.conv = lambda instance,value: globals()[dtype](value,on_fail='raise')
+				if include_proto:
+					self.conv = lambda instance,value: globals()[dtype](instance.proto,value,on_fail='raise')
+				else:
+					self.conv = lambda instance,value: globals()[dtype](value,on_fail='raise')
 			else:
 				if set_none_ok:
 					self.conv = lambda instance,value: None if value is None else dtype(value)
@@ -280,10 +284,10 @@ class ListItemAttr(ImmutableAttr):
 	For attributes that might not be present in the data instance
 	Reassignment or deletion allowed if specified
 	"""
-	def __init__(self,dtype,typeconv=True,reassign_ok=False,delete_ok=False):
+	def __init__(self,dtype,typeconv=True,include_proto=False,reassign_ok=False,delete_ok=False):
 		self.reassign_ok = reassign_ok
 		self.delete_ok = delete_ok
-		ImmutableAttr.__init__(self,dtype,typeconv=typeconv)
+		ImmutableAttr.__init__(self,dtype,typeconv=typeconv,include_proto=include_proto)
 
 	def __get__(self,instance,owner):
 		"return None if attribute doesn't exist"
@@ -301,8 +305,7 @@ class ListItemAttr(ImmutableAttr):
 			ImmutableAttr.__delete__(self,instance)
 
 class MMGenListItem(MMGenObject):
-
-	valid_attrs = None
+	valid_attrs = set()
 	valid_attrs_extra = set()
 	invalid_attrs = {
 		'pfmt',
@@ -312,15 +315,20 @@ class MMGenListItem(MMGenObject):
 		'valid_attrs_extra',
 		'invalid_attrs',
 		'immutable_attr_init_check',
+		'conv_funcs',
+		'_asdict',
 	}
 
 	def __init__(self,*args,**kwargs):
-		if self.valid_attrs == None:
-			type(self).valid_attrs = (
-				( {e for e in dir(self) if e[:2] != '__'} | self.valid_attrs_extra ) - self.invalid_attrs )
+		# generate valid_attrs, or use the class valid_attrs if set
+		self.__dict__['valid_attrs'] = self.valid_attrs or (
+				( {e for e in dir(self) if e[:2] != '__'} | self.valid_attrs_extra )
+				- MMGenListItem.invalid_attrs
+				- self.invalid_attrs
+			)
 
 		if args:
-			raise ValueError('Non-keyword args not allowed in {!r} constructor'.format(type(self).__name__))
+			raise ValueError(f'Non-keyword args not allowed in {type(self).__name__!r} constructor')
 
 		for k,v in kwargs.items():
 			if v != None:
@@ -332,10 +340,12 @@ class MMGenListItem(MMGenObject):
 	# allow only valid attributes to be set
 	def __setattr__(self,name,value):
 		if name not in self.valid_attrs:
-			m = "'{}': no such attribute in class {}"
-			raise AttributeError(m.format(name,type(self)))
+			raise AttributeError(f'{name!r}: no such attribute in class {type(self)}')
 		return object.__setattr__(self,name,value)
 
+	def _asdict(self):
+		return dict((k,v) for k,v in self.__dict__.items() if k in self.valid_attrs)
+
 class MMGenIdx(Int): min_val = 1
 class SeedShareIdx(MMGenIdx): max_val = 1024
 class SeedShareCount(SeedShareIdx): min_val = 2
@@ -526,49 +536,38 @@ class CoinAddr(str,Hilite,InitErrors,MMGenObject):
 	hex_width = 40
 	width = 1
 	trunc_ok = False
-	def __new__(cls,s,on_fail='die'):
-		if type(s) == cls: return s
+	def __new__(cls,proto,addr,on_fail='die'):
+		if type(addr) == cls:
+			return addr
 		cls.arg_chk(on_fail)
 		try:
-			assert set(s) <= set(ascii_letters+digits),'contains non-alphanumeric characters'
-			me = str.__new__(cls,s)
-			ap = g.proto.parse_addr(s)
-			assert ap,'coin address {!r} could not be parsed'.format(s)
+			assert set(addr) <= set(ascii_letters+digits),'contains non-alphanumeric characters'
+			me = str.__new__(cls,addr)
+			ap = proto.parse_addr(addr)
+			assert ap, f'coin address {addr!r} could not be parsed'
 			me.addr_fmt = ap.fmt
 			me.hex = ap.bytes.hex()
+			me.proto = proto
 			return me
 		except Exception as e:
-			return cls.init_fail(e,s,objname=f'{g.proto.cls_name} address')
+			return cls.init_fail(e,addr,objname=f'{proto.cls_name} address')
 
 	@classmethod
-	def fmtc(cls,s,**kwargs):
+	def fmtc(cls,addr,**kwargs):
 		w = kwargs['width'] or cls.width
-		return super().fmtc(s[:w-2]+'..' if w < len(s) else s, **kwargs)
-
-	def is_for_chain(self,chain):
-
-		if g.proto.name.startswith('Ethereum'):
-			return True
-
-		from mmgen.protocol import init_proto
-		proto = init_proto(g.coin,network=chain)
-
-		if self.addr_fmt == 'bech32':
-			return self[:len(proto.bech32_hrp)] == proto.bech32_hrp
-		else:
-			return bool(proto.parse_addr(self))
+		return super().fmtc(addr[:w-2]+'..' if w < len(addr) else addr, **kwargs)
 
 class TokenAddr(CoinAddr):
 	color = 'blue'
 
 class ViewKey(object):
-	def __new__(cls,s,on_fail='die'):
-		if g.proto.name == 'Zcash':
-			return ZcashViewKey.__new__(ZcashViewKey,s,on_fail)
-		elif g.proto.name == 'Monero':
-			return MoneroViewKey.__new__(MoneroViewKey,s,on_fail)
+	def __new__(cls,proto,viewkey,on_fail='die'):
+		if proto.name == 'Zcash':
+			return ZcashViewKey.__new__(ZcashViewKey,proto,viewkey,on_fail)
+		elif proto.name == 'Monero':
+			return MoneroViewKey.__new__(MoneroViewKey,viewkey,on_fail)
 		else:
-			raise ValueError(f'{g.proto.name}: protocol does not support view keys')
+			raise ValueError(f'{proto.name}: protocol does not support view keys')
 
 class ZcashViewKey(CoinAddr): hex_width = 128
 
@@ -620,39 +619,40 @@ class MMGenID(str,Hilite,InitErrors,MMGenObject):
 	color = 'orange'
 	width = 0
 	trunc_ok = False
-	def __new__(cls,s,on_fail='die'):
+	def __new__(cls,proto,id_str,on_fail='die'):
 		cls.arg_chk(on_fail)
 		try:
-			ss = str(s).split(':')
+			ss = str(id_str).split(':')
 			assert len(ss) in (2,3),'not 2 or 3 colon-separated items'
-			t = MMGenAddrType((ss[1],g.proto.dfl_mmtype)[len(ss)==2],on_fail='raise')
+			t = proto.addr_type((ss[1],proto.dfl_mmtype)[len(ss)==2],on_fail='raise')
 			me = str.__new__(cls,'{}:{}:{}'.format(ss[0],t,ss[-1]))
 			me.sid = SeedID(sid=ss[0],on_fail='raise')
 			me.idx = AddrIdx(ss[-1],on_fail='raise')
 			me.mmtype = t
-			assert t in g.proto.mmtypes, f'{t}: invalid address type for {g.proto.cls_name}'
+			assert t in proto.mmtypes, f'{t}: invalid address type for {proto.cls_name}'
 			me.al_id = str.__new__(AddrListID,me.sid+':'+me.mmtype) # checks already done
 			me.sort_key = '{}:{}:{:0{w}}'.format(me.sid,me.mmtype,me.idx,w=me.idx.max_digits)
+			me.proto = proto
 			return me
 		except Exception as e:
-			return cls.init_fail(e,s)
+			return cls.init_fail(e,id_str)
 
 class TwMMGenID(str,Hilite,InitErrors,MMGenObject):
 	color = 'orange'
 	width = 0
 	trunc_ok = False
-	def __new__(cls,id_str,on_fail='die'):
+	def __new__(cls,proto,id_str,on_fail='die'):
 		if type(id_str) == cls:
 			return id_str
 		cls.arg_chk(on_fail)
 		ret = None
 		try:
-			ret = MMGenID(id_str,on_fail='raise')
+			ret = MMGenID(proto,id_str,on_fail='raise')
 			sort_key,idtype = ret.sort_key,'mmgen'
 		except Exception as e:
 			try:
-				assert id_str.split(':',1)[0] == g.proto.base_coin.lower(),(
-					"not a string beginning with the prefix '{}:'".format(g.proto.base_coin.lower()))
+				assert id_str.split(':',1)[0] == proto.base_coin.lower(),(
+					f'not a string beginning with the prefix {proto.base_coin.lower()!r}:' )
 				assert set(id_str[4:]) <= set(ascii_letters+digits),'contains non-alphanumeric characters'
 				assert len(id_str) > 4,'not more that four characters long'
 				ret,sort_key,idtype = str(id_str),'z_'+id_str,'non-mmgen'
@@ -663,21 +663,23 @@ class TwMMGenID(str,Hilite,InitErrors,MMGenObject):
 		me.obj = ret
 		me.sort_key = sort_key
 		me.type = idtype
+		me.proto = proto
 		return me
 
 # non-displaying container for TwMMGenID,TwComment
 class TwLabel(str,InitErrors,MMGenObject):
-	def __new__(cls,text,on_fail='die'):
+	def __new__(cls,proto,text,on_fail='die'):
 		if type(text) == cls:
 			return text
 		cls.arg_chk(on_fail)
 		try:
 			ts = text.split(None,1)
-			mmid = TwMMGenID(ts[0],on_fail='raise')
+			mmid = TwMMGenID(proto,ts[0],on_fail='raise')
 			comment = TwComment(ts[1] if len(ts) == 2 else '',on_fail='raise')
 			me = str.__new__(cls,'{}{}'.format(mmid,' {}'.format(comment) if comment else ''))
 			me.mmid = mmid
 			me.comment = comment
+			me.proto = proto
 			return me
 		except Exception as e:
 			return cls.init_fail(e,text)
@@ -704,7 +706,7 @@ class HexStr(str,Hilite,InitErrors):
 
 class CoinTxID(HexStr):       color,width,hexcase = 'purple',64,'lower'
 class WalletPassword(HexStr): color,width,hexcase = 'blue',32,'lower'
-class MoneroViewKey(HexStr):  color,width,hexcase = 'cyan',64,'lower'
+class MoneroViewKey(HexStr):  color,width,hexcase = 'cyan',64,'lower' # FIXME - no checking performed
 class MMGenTxID(HexStr):      color,width,hexcase = 'red',6,'upper'
 
 class WifKey(str,Hilite,InitErrors):
@@ -714,13 +716,13 @@ class WifKey(str,Hilite,InitErrors):
 	"""
 	width = 53
 	color = 'blue'
-	def __new__(cls,wif,on_fail='die'):
+	def __new__(cls,proto,wif,on_fail='die'):
 		if type(wif) == cls:
 			return wif
 		cls.arg_chk(on_fail)
 		try:
 			assert set(wif) <= set(ascii_letters+digits),'not an ascii alphanumeric string'
-			g.proto.parse_wif(wif) # raises exception on error
+			proto.parse_wif(wif) # raises exception on error
 			return str.__new__(cls,wif)
 		except Exception as e:
 			return cls.init_fail(e,wif)
@@ -751,7 +753,7 @@ class PrivKey(str,Hilite,InitErrors,MMGenObject):
 	wif        = ImmutableAttr(WifKey,typeconv=False)
 
 	# initialize with (priv_bin,compressed), WIF or self
-	def __new__(cls,s=None,compressed=None,wif=None,pubkey_type=None,on_fail='die'):
+	def __new__(cls,proto,s=None,compressed=None,wif=None,pubkey_type=None,on_fail='die'):
 
 		if type(s) == cls: return s
 		cls.arg_chk(on_fail)
@@ -760,18 +762,19 @@ class PrivKey(str,Hilite,InitErrors,MMGenObject):
 			try:
 				assert s == None,"'wif' and key hex args are mutually exclusive"
 				assert set(wif) <= set(ascii_letters+digits),'not an ascii alphanumeric string'
-				k = g.proto.parse_wif(wif) # raises exception on error
+				k = proto.parse_wif(wif) # raises exception on error
 				me = str.__new__(cls,k.sec.hex())
 				me.compressed = k.compressed
 				me.pubkey_type = k.pubkey_type
 				me.wif = str.__new__(WifKey,wif) # check has been done
 				me.orig_hex = None
-				if k.sec != g.proto.preprocess_key(k.sec,k.pubkey_type):
+				if k.sec != proto.preprocess_key(k.sec,k.pubkey_type):
 					raise PrivateKeyError(
-						f'{g.proto.cls_name} WIF key {me.wif!r} encodes private key with invalid value {me}')
+						f'{proto.cls_name} WIF key {me.wif!r} encodes private key with invalid value {me}')
+				me.proto = proto
 				return me
 			except Exception as e:
-				return cls.init_fail(e,s,objname='{} WIF key'.format(g.coin))
+				return cls.init_fail(e,s,objname=f'{proto.coin} WIF key')
 		else:
 			try:
 				assert s,'private key bytes data missing'
@@ -782,11 +785,12 @@ class PrivKey(str,Hilite,InitErrors,MMGenObject):
 				else:
 					assert compressed is not None, "'compressed' arg missing"
 					assert type(compressed) == bool,"{!r}: 'compressed' not of type 'bool'".format(compressed)
-					me = str.__new__(cls,g.proto.preprocess_key(s,pubkey_type).hex())
-					me.wif = WifKey(g.proto.hex2wif(me,pubkey_type,compressed),on_fail='raise')
+					me = str.__new__(cls,proto.preprocess_key(s,pubkey_type).hex())
+					me.wif = WifKey(proto,proto.hex2wif(me,pubkey_type,compressed),on_fail='raise')
 					me.compressed = compressed
 				me.pubkey_type = pubkey_type
 				me.orig_hex = s.hex() # save the non-preprocessed key
+				me.proto = proto
 				return me
 			except Exception as e:
 				return cls.init_fail(e,s)
@@ -915,20 +919,21 @@ class MMGenAddrType(str,Hilite,InitErrors,MMGenObject):
 		'Z': ati('zcash_z','zcash_z',False,'zcash_z', 'zcash_z', 'wif',     ('viewkey',),      'Zcash z-address'),
 		'M': ati('monero', 'monero', False,'monero',  'monero',  'spendkey',('viewkey','wallet_passwd'),'Monero address'),
 	}
-	def __new__(cls,id_str,on_fail='die',errmsg=None):
+	def __new__(cls,proto,id_str,on_fail='die',errmsg=None):
 		if type(id_str) == cls:
 			return id_str
 		cls.arg_chk(on_fail)
 		try:
-			for k,v in list(cls.mmtypes.items()):
+			for k,v in cls.mmtypes.items():
 				if id_str in (k,v.name):
 					if id_str == v.name:
 						id_str = k
 					me = str.__new__(cls,id_str)
 					for k in v._fields:
 						setattr(me,k,getattr(v,k))
-					if me not in g.proto.mmtypes + ('P',):
-						raise ValueError(f'{me.name!r}: invalid address type for {g.proto.cls_name}')
+					if me not in proto.mmtypes + ('P',):
+						raise ValueError(f'{me.name!r}: invalid address type for {proto.name} protocol')
+					me.proto = proto
 					return me
 			raise ValueError(f'{id_str}: unrecognized address type for protocol {proto.name}')
 		except Exception as e:

+ 68 - 54
mmgen/opts.py

@@ -44,18 +44,22 @@ def print_help(po,opts_data,opt_filter):
 	if not 'code' in opts_data:
 		opts_data['code'] = {}
 
+	from .protocol import init_proto_from_opts
+	proto = init_proto_from_opts()
+
 	if getattr(opt,'longhelp',None):
 		opts_data['code']['long_options'] = common_opts_data['code']
 		def remove_unneeded_long_opts():
 			d = opts_data['text']['long_options']
 			if g.prog_name != 'mmgen-tool':
 				d = '\n'.join(''+i for i in d.split('\n') if not '--monero-wallet' in i)
-			if g.proto.base_proto != 'Ethereum':
+			if proto.base_proto != 'Ethereum':
 				d = '\n'.join(''+i for i in d.split('\n') if not '--token' in i)
 			opts_data['text']['long_options'] = d
 		remove_unneeded_long_opts()
 
 	mmgen.share.Opts.print_help( # exits
+		proto,
 		po,
 		opts_data,
 		opt_filter )
@@ -76,6 +80,7 @@ def _show_hash_presets():
 	for i in sorted(g.hash_presets.keys()):
 		msg(fs.format(i,*g.hash_presets[i]))
 	msg('N = memory usage (power of two), p = iterations (rounds)')
+	sys.exit(0)
 
 def opt_preproc_debug(po):
 	d = (
@@ -205,10 +210,10 @@ common_opts_data = {
 --, --bob                  Switch to user "Bob" in MMGen regtest setup
 --, --alice                Switch to user "Alice" in MMGen regtest setup
 	""",
-	'code': lambda s: s.format(
+	'code': lambda proto,s: s.format(
 			pnm    = g.proj_name,
-			dn     = g.proto.daemon_name,
-			cu_dfl = g.coin,
+			dn     = proto.daemon_name,
+			cu_dfl = proto.coin,
 		)
 }
 
@@ -256,14 +261,24 @@ def init(opts_data=None,add_opts=[],opt_filter=None,parse_only=False):
 		version() # exits
 
 	# === begin global var initialization === #
-
-	# 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
-	# We must set g.data_dir_root from --data-dir before processing cfg file
-	g.data_dir_root = (
-			os.path.normpath(os.path.expanduser(opt.data_dir))
-		if opt.data_dir else
-			os.path.join(g.home_dir,'.'+g.proj_name.lower()) )
+	"""
+	NB: user opt --data-dir is actually data_dir_root
+	- data_dir is data_dir_root plus optionally 'regtest' or 'testnet', so for mainnet
+	  data_dir == data_dir_root
+	- As with Bitcoin Core, cfg file is in data_dir_root, wallets and other data are
+	  in data_dir
+	- Since cfg file is in data_dir_root, data_dir_root must be finalized before we
+	  can process cfg file
+	- Since data_dir depends on the values of g.testnet and g.regtest, these must be
+	  finalized before setting data_dir
+	"""
+	if opt.data_dir:
+		g.data_dir_root = os.path.normpath(os.path.expanduser(opt.data_dir))
+	elif os.getenv('MMGEN_TEST_SUITE'):
+		from test.include.common import get_data_dir
+		g.data_dir_root = get_data_dir()
+	else:
+		g.data_dir_root = os.path.join(g.home_dir,'.'+g.proj_name.lower())
 
 	check_or_create_dir(g.data_dir_root)
 
@@ -278,30 +293,18 @@ def init(opts_data=None,add_opts=[],opt_filter=None,parse_only=False):
 
 	# Set globals from opts, setting type from original global value
 	# Do here, before opts are set from globals below
-	# g.coin is finalized here
 	for k in (g.common_opts + g.opt_sets_global):
 		if hasattr(opt,k):
 			val = getattr(opt,k)
 			if val != None and hasattr(g,k):
 				setattr(g,k,set_for_type(val,getattr(g,k),'--'+k))
 
-	from .protocol import init_genonly_altcoins,init_proto
-
-	altcoin_trust_level = init_genonly_altcoins(
-		opt.coin or 'btc',
-		testnet = g.testnet or g.regtest )
-
-	g.proto = init_proto(
-		opt.coin or 'btc',
-		testnet = g.testnet,
-		regtest = g.regtest )
-
-	# this could have been set from long opts
-	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.data_dir = os.path.normpath(os.path.join(g.data_dir_root,g.proto.data_subdir))
+	"""
+	g.testnet and g.regtest are finalized, so we can set g.data_dir
+	"""
+	g.data_dir = os.path.normpath(os.path.join(
+		g.data_dir_root,
+		('regtest' if g.regtest else 'testnet' if g.testnet else '') ))
 
 	# Set user opts from globals:
 	# - if opt is unset, set it to global value
@@ -314,15 +317,14 @@ def init(opts_data=None,add_opts=[],opt_filter=None,parse_only=False):
 		else:
 			setattr(opt,k,getattr(g,k))
 
-	if opt.show_hash_presets:
+	if opt.show_hash_presets: # exits
 		_show_hash_presets()
-		sys.exit(0)
 
-	if opt.verbose:
-		opt.quiet = None
+	g.coin = g.coin.upper() or 'BTC'
+	g.token = g.token.upper() or None
 
 	if g.bob or g.alice:
-		g.proto = init_proto(g.coin,regtest=True)
+		g.regtest = True
 		g.rpc_host = 'localhost'
 		g.data_dir = os.path.join(g.data_dir_root,'regtest',g.coin.lower(),('alice','bob')[g.bob])
 		from .regtest import MMGenRegtest
@@ -330,14 +332,21 @@ def init(opts_data=None,add_opts=[],opt_filter=None,parse_only=False):
 		g.rpc_password = MMGenRegtest.rpc_password
 		g.rpc_port = MMGenRegtest(g.coin).d.rpc_port
 
-	# === end global var initialization === #
+	from .protocol import init_genonly_altcoins
+	altcoin_trust_level = init_genonly_altcoins(
+		g.coin,
+		testnet = g.testnet or g.regtest )
 
-	die_on_incompatible_opts(g.incompatible_opts)
+	# === end global var initialization === #
 
 	# print help screen only after global vars are initialized:
 	if getattr(opt,'help',None) or getattr(opt,'longhelp',None):
 		print_help(po,opts_data,opt_filter) # exits
 
+	warn_altcoins(g.coin,altcoin_trust_level)
+
+	die_on_incompatible_opts(g.incompatible_opts)
+
 	check_or_create_dir(g.data_dir) # g.data_dir is finalized, so we can create it
 
 	# Check user-set opts without modifying them
@@ -346,14 +355,15 @@ def init(opts_data=None,add_opts=[],opt_filter=None,parse_only=False):
 	# Check all opts against g.autoset_opts, setting if unset
 	check_and_set_autoset_opts()
 
+	if opt.verbose:
+		opt.quiet = None
+
 	if g.debug and g.prog_name != 'test.py':
 		opt.verbose,opt.quiet = (True,None)
 
 	if g.debug_opts:
 		opt_postproc_debug()
 
-	warn_altcoins(g.coin,altcoin_trust_level)
-
 	# We don't need this data anymore
 	del mmgen.share.Opts
 	for k in ('text','notes','code'):
@@ -362,6 +372,7 @@ def init(opts_data=None,add_opts=[],opt_filter=None,parse_only=False):
 
 	return po.cmd_args
 
+# DISABLED
 def opt_is_tx_fee(key,val,desc): # 'key' must remain a placeholder
 
 	# contract data or non-standard startgas: disable fee checking
@@ -371,18 +382,19 @@ def opt_is_tx_fee(key,val,desc): # 'key' must remain a placeholder
 		return
 
 	from .tx import MMGenTX
-	tx = MMGenTX()
+	from .protocol import init_proto_from_opts
+	tx = MMGenTX.New(init_proto_from_opts())
 	# Size of 224 is just a ball-park figure to eliminate the most extreme cases at startup
 	# This check will be performed again once we know the true size
 	ret = tx.process_fee_spec(val,224)
 
 	if ret == False:
 		raise UserOptError('{!r}: invalid {}\n(not a {} amount or {} specification)'.format(
-				val,desc,g.coin.upper(),tx.rel_fee_desc))
+				val,desc,tx.proto.coin.upper(),tx.rel_fee_desc))
 
-	if ret > g.proto.max_tx_fee:
+	if ret > tx.proto.max_tx_fee:
 		raise UserOptError('{!r}: invalid {}\n({} > max_tx_fee ({} {}))'.format(
-				val,desc,ret.fmt(fs='1.1'),g.proto.max_tx_fee,g.coin.upper()))
+				val,desc,ret.fmt(fs='1.1'),tx.proto.max_tx_fee,tx.proto.coin.upper()))
 
 def check_usr_opts(usr_opts): # Raises an exception if any check fails
 
@@ -519,10 +531,11 @@ def check_usr_opts(usr_opts): # Raises an exception if any check fails
 		from .protocol import CoinProtocol
 		opt_is_in_list(val.lower(),CoinProtocol.coins,'coin')
 
-	def chk_rbf(key,val,desc):
-		if not g.proto.cap('rbf'):
-			m = '--rbf requested, but {} does not support replace-by-fee transactions'
-			raise UserOptError(m.format(g.coin))
+# TODO: move this check elsewhere
+#	def chk_rbf(key,val,desc):
+#		if not proto.cap('rbf'):
+#			m = '--rbf requested, but {} does not support replace-by-fee transactions'
+#			raise UserOptError(m.format(proto.coin))
 
 	def chk_bob(key,val,desc):
 		m = "Regtest (Bob and Alice) mode not set up yet.  Run '{}-regtest setup' to initialize."
@@ -538,13 +551,14 @@ def check_usr_opts(usr_opts): # Raises an exception if any check fails
 		opt_is_int(val,desc)
 		opt_compares(int(val),'>',0,desc)
 
-	def chk_token(key,val,desc):
-		if not 'token' in g.proto.caps:
-			raise UserOptError('Coin {!r} does not support the --token option'.format(g.coin))
-		if len(val) == 40 and is_hex_str(val):
-			return
-		if len(val) > 20 or not all(s.isalnum() for s in val):
-			raise UserOptError('{!r}: invalid parameter for --token option'.format(val))
+# TODO: move this check elsewhere
+#	def chk_token(key,val,desc):
+#		if not 'token' in proto.caps:
+#			raise UserOptError('Coin {!r} does not support the --token option'.format(tx.coin))
+#		if len(val) == 40 and is_hex_str(val):
+#			return
+#		if len(val) > 20 or not all(s.isalnum() for s in val):
+#			raise UserOptError('{!r}: invalid parameter for --token option'.format(val))
 
 	cfuncs = { k:v for k,v in locals().items() if k.startswith('chk_') }
 

+ 106 - 27
mmgen/protocol.py

@@ -25,7 +25,7 @@ from collections import namedtuple
 
 from .util import msg,ymsg,Msg,ydie
 from .devtools import *
-from .obj import BTCAmt,LTCAmt,BCHAmt,B2XAmt,ETHAmt
+from .obj import BTCAmt,LTCAmt,BCHAmt,B2XAmt,ETHAmt,CoinAddr,MMGenAddrType,PrivKey
 from .globalvars import g
 import mmgen.bech32 as bech32
 
@@ -87,14 +87,61 @@ class CoinProtocol(MMGenObject):
 		is_fork_of = None
 		networks   = ('mainnet','testnet','regtest')
 
-		def __init__(self,coin,name,network):
-			self.coin     = coin.upper()
-			self.dcoin    = self.coin # display coin - for Ethereum, is set to ERC20 token name
-			self.name     = name
-			self.cls_name = type(self).__name__
-			self.network  = network
-			self.testnet  = network in ('testnet','regtest')
-			self.regtest  = network == 'regtest'
+		def __init__(self,coin,name,network,tokensym=None):
+			self.coin       = coin.upper()
+			self.name       = name
+			self.network    = network
+			self.tokensym   = tokensym
+			self.cls_name   = type(self).__name__
+			self.testnet    = network in ('testnet','regtest')
+			self.regtest    = network == 'regtest'
+			self.network_id = coin.lower() + {
+				'mainnet': '',
+				'testnet': '_tn',
+				'regtest': '_rt',
+			}[network]
+
+			if not hasattr(self,'chain_name'):
+				self.chain_name = self.network
+
+			if self.tokensym:
+				assert isinstance(self,CoinProtocol.Ethereum), 'CoinProtocol.Base_chk1'
+
+		@property
+		def dcoin(self):
+			return self.coin
+
+		@classmethod
+		def chain_name_to_network(cls,coin,chain_name):
+			"""
+			The generic networks 'mainnet', 'testnet' and 'regtest' are required for all coins
+			that support transaction operations.
+
+			For protocols that have specific names for chains corresponding to these networks,
+			the attribute 'chain_name' is used, while 'network' retains the generic name.
+			For Bitcoin and Bitcoin forks, 'network' and 'chain_name' are equivalent.
+			"""
+			for network,suf in (
+					('mainnet',''),
+					('testnet','Testnet'),
+					('regtest','Regtest' ),
+				):
+				name = CoinProtocol.coins[coin.lower()].name + suf
+				proto = getattr(CoinProtocol,name)
+				proto_chain_name = getattr(proto,'chain_name',None) or network
+				if chain_name == proto_chain_name:
+					return network
+			raise ValueError(f'{chain_name}: unrecognized chain name for coin {coin}')
+
+		@staticmethod
+		def parse_network_id(network_id):
+			nid = namedtuple('parsed_network_id',['coin','network'])
+			if network_id.endswith('_tn'):
+				return nid(network_id[:-3],'testnet')
+			elif network_id.endswith('_rt'):
+				return nid(network_id[:-3],'regtest')
+			else:
+				return nid(network_id,'mainnet')
 
 		def cap(self,s):
 			return s in self.caps
@@ -118,7 +165,19 @@ class CoinProtocol(MMGenObject):
 
 			return False
 
+		def coin_addr(self,addr):
+			return CoinAddr(proto=self,addr=addr)
+
+		def addr_type(self,id_str,on_fail='die'):
+			return MMGenAddrType(proto=self,id_str=id_str,on_fail=on_fail)
+
+		def priv_key(self,s,on_fail='die'):
+			return PrivKey(proto=self,s=s,on_fail=on_fail)
+
 	class Secp256k1(Base):
+		"""
+		Bitcoin and Ethereum protocols inherit from this class
+		"""
 		secp256k1_ge = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141
 		privkey_len  = 32
 
@@ -138,6 +197,9 @@ class CoinProtocol(MMGenObject):
 					return (pk % self.secp256k1_ge).to_bytes(self.privkey_len,'big')
 
 	class Bitcoin(Secp256k1): # chainparams.cpp
+		"""
+		All Bitcoin code and chain forks inherit from this class
+		"""
 		mod_clsname     = 'Bitcoin'
 		daemon_name     = 'bitcoind'
 		daemon_family   = 'bitcoind'
@@ -146,7 +208,6 @@ class CoinProtocol(MMGenObject):
 		wif_ver_num     = { 'std': '80' }
 		mmtypes         = ('L','C','S','B')
 		dfl_mmtype      = 'L'
-		data_subdir     = ''
 		rpc_port        = 8332
 		coin_amt        = BTCAmt
 		max_tx_fee      = BTCAmt('0.003')
@@ -237,7 +298,6 @@ class CoinProtocol(MMGenObject):
 	class BitcoinTestnet(Bitcoin):
 		addr_ver_bytes      = { '6f': 'p2pkh', 'c4': 'p2sh' }
 		wif_ver_num         = { 'std': 'ef' }
-		data_subdir         = 'testnet'
 		daemon_data_subdir  = 'testnet3'
 		rpc_port            = 18332
 		bech32_hrp          = 'tb'
@@ -268,7 +328,6 @@ class CoinProtocol(MMGenObject):
 		rpc_port       = 18442
 		addr_ver_bytes = { '6f': 'p2pkh', 'c4': 'p2sh' }
 		wif_ver_num    = { 'std': 'ef' }
-		data_subdir    = 'testnet'
 		daemon_data_subdir = 'testnet3'
 
 	class BitcoinCashRegtest(BitcoinCashTestnet):
@@ -289,7 +348,6 @@ class CoinProtocol(MMGenObject):
 	class B2XTestnet(B2X):
 		addr_ver_bytes     = { '6f': 'p2pkh', 'c4': 'p2sh' }
 		wif_ver_num        = { 'std': 'ef' }
-		data_subdir        = 'testnet'
 		daemon_data_subdir = 'testnet5'
 		rpc_port           = 18338
 
@@ -313,7 +371,6 @@ class CoinProtocol(MMGenObject):
 		# addr ver nums same as Bitcoin testnet, except for 'p2sh'
 		addr_ver_bytes     = { '6f':'p2pkh', '3a':'p2sh', 'c4':'p2sh' }
 		wif_ver_num        = { 'std': 'ef' } # same as Bitcoin testnet
-		data_subdir        = 'testnet'
 		daemon_data_subdir = 'testnet4'
 		rpc_port           = 19332
 		bech32_hrp         = 'tltc'
@@ -340,9 +397,10 @@ class CoinProtocol(MMGenObject):
 		base_coin     = 'ETH'
 		pubkey_type   = 'std' # required by DummyWIF
 
-		data_subdir   = ''
 		daemon_name   = 'parity'
 		daemon_family = 'parity'
+		daemon_data_dir = os.path.join(g.home_dir,'.local','share','io.parity.ethereum')
+		daemon_data_subdir = ''
 		rpc_port      = 8545
 		coin_amt      = ETHAmt
 		max_tx_fee    = ETHAmt('0.005')
@@ -353,6 +411,10 @@ class CoinProtocol(MMGenObject):
 		base_proto    = 'Ethereum'
 		avg_bdi       = 15
 
+		@property
+		def dcoin(self):
+			return self.tokensym or self.coin
+
 		def parse_addr(self,addr):
 			from .util import is_hex_str_lc
 			if is_hex_str_lc(addr) and len(addr) == self.addr_len * 2:
@@ -367,10 +429,12 @@ class CoinProtocol(MMGenObject):
 			return pubkey_hash
 
 	class EthereumTestnet(Ethereum):
-		data_subdir = 'testnet'
 		rpc_port    = 8547 # start Parity with --jsonrpc-port=8547 or --ports-shift=2
 		chain_name  = 'kovan'
 
+	class EthereumRegtest(EthereumTestnet):
+		chain_name  = 'developmentchain'
+
 	class EthereumClassic(Ethereum):
 		rpc_port   = 8555 # start Parity with --jsonrpc-port=8555 or --ports-shift=10
 		chain_name = 'ethereum_classic' # chain_id 0x3d (61)
@@ -379,6 +443,9 @@ class CoinProtocol(MMGenObject):
 		rpc_port   = 8557 # start Parity with --jsonrpc-port=8557 or --ports-shift=12
 		chain_name = 'classic-testnet' # aka Morden, chain_id 0x3e (62) (UNTESTED)
 
+	class EthereumClassicRegtest(EthereumClassicTestnet):
+		chain_name  = 'developmentchain'
+
 	class Zcash(Bitcoin):
 		base_coin      = 'ZEC'
 		addr_ver_bytes = { '1cb8': 'p2pkh', '1cbd': 'p2sh', '169a': 'zcash_z', 'a8abd3': 'viewkey' }
@@ -420,7 +487,6 @@ class CoinProtocol(MMGenObject):
 		dfl_mmtype     = 'M'
 		pubkey_type    = 'monero' # required by DummyWIF
 		avg_bdi        = 120
-		data_subdir    = ''
 		privkey_len    = 32
 		mmcaps         = ('key','addr')
 
@@ -455,17 +521,21 @@ class CoinProtocol(MMGenObject):
 	class MoneroTestnet(Monero):
 		addr_ver_bytes = { '35': 'monero', '3f': 'monero_sub' }
 
-def init_proto(coin,testnet=False,regtest=False,network=None):
+def init_proto(coin=None,testnet=False,regtest=False,network=None,network_id=None,tokensym=None):
 
 	assert type(testnet) == bool, 'init_proto_chk1'
 	assert type(regtest) == bool, 'init_proto_chk2'
-
-	if network is None:
-		network = 'regtest' if regtest else 'testnet' if testnet else 'mainnet'
+	assert coin or network_id, 'init_proto_chk3'
+	assert not (coin and network_id), 'init_proto_chk4'
+
+	if network_id:
+		coin,network = CoinProtocol.Base.parse_network_id(network_id)
+	elif network:
+		assert network in CoinProtocol.Base.networks, f'init_proto_chk5 - {network!r}: invalid network'
+		assert testnet == False, 'init_proto_chk6'
+		assert regtest == False, 'init_proto_chk7'
 	else:
-		assert network in CoinProtocol.Base.networks
-		assert testnet == False
-		assert regtest == False
+		network = 'regtest' if regtest else 'testnet' if testnet else 'mainnet'
 
 	coin = coin.lower()
 	if coin not in CoinProtocol.coins:
@@ -478,9 +548,18 @@ def init_proto(coin,testnet=False,regtest=False,network=None):
 	proto_name = name + ('' if network == 'mainnet' else network.capitalize())
 
 	return getattr(CoinProtocol,proto_name)(
-		coin    = coin,
-		name    = name,
-		network = network )
+		coin      = coin,
+		name      = name,
+		network   = network,
+		tokensym  = tokensym )
+
+def init_proto_from_opts():
+	from .opts import opt
+	return init_proto(
+		coin      = g.coin,
+		testnet   = g.testnet,
+		regtest   = g.regtest,
+		tokensym  = g.token )
 
 def init_genonly_altcoins(usr_coin=None,testnet=False):
 	"""

+ 9 - 8
mmgen/regtest.py

@@ -23,6 +23,7 @@ regtest: Coin daemon regression test mode setup and operations for the MMGen sui
 import os,time,shutil,re,json
 from subprocess import run,PIPE
 from .common import *
+from .protocol import init_proto
 from .daemon import CoinDaemon
 
 def create_data_dir(data_dir):
@@ -79,6 +80,7 @@ class MMGenRegtest(MMGenObject):
 
 	def __init__(self,coin):
 		self.coin = coin.lower()
+		self.proto = init_proto(self.coin,regtest=True)
 		self.test_suite = os.getenv('MMGEN_TEST_SUITE_REGTEST')
 		self.d = CoinDaemon(self.coin+'_rt',test_suite=self.test_suite)
 
@@ -152,12 +154,12 @@ class MMGenRegtest(MMGenObject):
 			err = cp.stderr.decode()
 			if err:
 				if "couldn't connect to server" in err:
-					rdie(1,f'Error stopping the {g.proto.name} daemon:\n{err}')
+					rdie(1,f'Error stopping the {self.proto.name} daemon:\n{err}')
 				else:
 					msg(err)
 
 	def current_user_unix(self,quiet=False):
-		cmd = ['pgrep','-af','{}.*--rpcport={}.*'.format(g.proto.daemon_name,self.d.rpc_port)]
+		cmd = ['pgrep','-af','{}.*--rpcport={}.*'.format(self.proto.daemon_name,self.d.rpc_port)]
 		cmdout = run(cmd,stdout=PIPE).stdout.decode()
 		if cmdout:
 			for k in self.users:
@@ -271,12 +273,11 @@ class MMGenRegtest(MMGenObject):
 
 	def fork(self,coin): # currently disabled
 
-		from .protocol import init_proto
-		forks = init_proto(coin,False).forks
-		if not [f for f in forks if f[2] == g.coin.lower() and f[3] == True]:
-			die(1,"Coin {} is not a replayable fork of coin {}".format(g.coin,coin))
+		proto = init_proto(coin,False)
+		if not [f for f in proto.forks if f[2] == proto.coin.lower() and f[3] == True]:
+			die(1,"Coin {} is not a replayable fork of coin {}".format(proto.coin,coin))
 
-		gmsg('Creating fork from coin {} to coin {}'.format(coin,g.coin))
+		gmsg('Creating fork from coin {} to coin {}'.format(coin,proto.coin))
 
 		source_rt = MMGenRegtest(coin)
 
@@ -300,4 +301,4 @@ class MMGenRegtest(MMGenObject):
 		self.start_daemon('miner',reindex=True)
 		self.stop_daemon()
 
-		gmsg('Fork {} successfully created'.format(g.coin))
+		gmsg('Fork {} successfully created'.format(proto.coin))

+ 97 - 56
mmgen/rpc.py

@@ -228,7 +228,7 @@ class RPCClient(MMGenObject):
 		if g.rpc_user:
 			user,passwd = (g.rpc_user,g.rpc_password)
 		else:
-			user,passwd = get_coin_daemon_cfg_options(('rpcuser','rpcpassword')).values()
+			user,passwd = self.get_daemon_cfg_options(('rpcuser','rpcpassword')).values()
 
 		if user and passwd:
 			self.auth = auth_data(user,passwd)
@@ -318,52 +318,29 @@ class RPCClient(MMGenObject):
 					except: m = f': {text}'
 			raise RPCFailure(f'{s.value} {s.name}{m}')
 
-
 class BitcoinRPCClient(RPCClient,metaclass=aInitMeta):
 
 	auth_type = 'basic'
 	has_auth_cookie = True
 
-	def __init__(self,*args,**kwargs): pass
-
-	async def __ainit__(self,proto,backend):
-
-		async def check_chainfork_mismatch(block0):
-			try:
-				if block0 != self.proto.block0:
-					raise ValueError(f'Invalid Genesis block for {self.proto.cls_name} protocol')
-				for fork in self.proto.forks:
-					if fork.height == None or self.blockcount < fork.height:
-						break
-					if fork.hash != await self.call('getblockhash',fork.height):
-						die(3,f'Bad block hash at fork block {fork.height}. Is this the {fork.name} chain?')
-			except Exception as e:
-				die(2,"{}\n'{c}' requested, but this is not the {c} chain!".format(e.args[0],c=g.coin))
+	def __init__(self,*args,**kwargs):
+		pass
 
-		def check_chaintype_mismatch():
-			try:
-				if g.proto.regtest:
-					assert g.chain == 'regtest', '--regtest option selected, but chain is not regtest'
-				if g.proto.testnet:
-					assert g.chain != 'mainnet', '--testnet option selected, but chain is mainnet'
-				else:
-					assert g.chain == 'mainnet', 'mainnet selected, but chain is not mainnet'
-			except Exception as e:
-				die(1,'{}\nChain is {}!'.format(e.args[0],g.chain))
+	async def __ainit__(self,proto,daemon,backend):
 
 		self.proto = proto
-		user,passwd = get_coin_daemon_cfg_options(('rpcuser','rpcpassword')).values()
+		self.daemon_data_dir = daemon.datadir
 
 		super().__init__(
-			host = g.rpc_host or 'localhost',
-			port = g.rpc_port or self.proto.rpc_port)
+			host = 'localhost' if g.test_suite else (g.rpc_host or 'localhost'),
+			port = daemon.rpc_port )
 
-		self.set_auth() # set_auth() requires cookie, so must be called after __init__() tests socket
+		self.set_auth() # set_auth() requires cookie, so must be called after __init__() tests daemon is listening
 		self.set_backend(backend) # backend requires self.auth
 
 		if g.bob or g.alice:
 			from .regtest import MMGenRegtest
-			MMGenRegtest(g.coin).switch_user(('alice','bob')[g.bob],quiet=True)
+			MMGenRegtest(self.proto.coin).switch_user(('alice','bob')[g.bob],quiet=True)
 
 		self.cached = {}
 		(
@@ -378,16 +355,27 @@ class BitcoinRPCClient(RPCClient,metaclass=aInitMeta):
 				('getblockhash',(0,)),
 			))
 		self.daemon_version = self.cached['networkinfo']['version']
-		g.chain = self.cached['blockchaininfo']['chain']
+		self.chain = self.cached['blockchaininfo']['chain']
 
 		tip = await self.call('getblockhash',self.blockcount)
 		self.cur_date = (await self.call('getblockheader',tip))['time']
-		if g.chain != 'regtest':
-			g.chain += 'net'
-		assert g.chain in g.chains
-		check_chaintype_mismatch()
+		if self.chain != 'regtest':
+			self.chain += 'net'
+		assert self.chain in self.proto.networks
 
-		if g.chain == 'mainnet': # skip this for testnet, as Genesis block may change
+		async def check_chainfork_mismatch(block0):
+			try:
+				if block0 != self.proto.block0:
+					raise ValueError(f'Invalid Genesis block for {self.proto.cls_name} protocol')
+				for fork in self.proto.forks:
+					if fork.height == None or self.blockcount < fork.height:
+						break
+					if fork.hash != await self.call('getblockhash',fork.height):
+						die(3,f'Bad block hash at fork block {fork.height}. Is this the {fork.name} chain?')
+			except Exception as e:
+				die(2,'{!s}\n{c!r} requested, but this is not the {c} chain!'.format(e,c=self.proto.coin))
+
+		if self.chain == 'mainnet': # skip this for testnet, as Genesis block may change
 			await check_chainfork_mismatch(block0)
 
 		self.caps = ('full_node',)
@@ -397,16 +385,60 @@ class BitcoinRPCClient(RPCClient,metaclass=aInitMeta):
 			if len((await self.call('help',func)).split('\n')) > 3:
 				self.caps += (cap,)
 
+	def get_daemon_cfg_fn(self):
+		# Use dirname() to remove 'bob' or 'alice' component
+		cfg_dir = os.path.dirname(g.data_dir) if self.proto.regtest else self.daemon_data_dir
+		return os.path.join(
+			cfg_dir,
+			(self.proto.is_fork_of or self.proto.name).lower() + '.conf' )
+
 	def get_daemon_auth_cookie_fn(self):
-		cdir = os.path.join(
-			self.proto.daemon_data_dir,
-			self.proto.daemon_data_subdir )
-		return os.path.join(cdir,'.cookie')
+		return os.path.join(
+			self.daemon_data_dir,
+			self.proto.daemon_data_subdir,
+			'.cookie' )
+
+	def get_daemon_cfg_options(self,req_keys):
+
+		fn = self.get_daemon_cfg_fn()
+		try:
+			lines = get_lines_from_file(fn,'',silent=not opt.verbose)
+		except:
+			vmsg(f'Warning: {fn!r} does not exist or is unreadable')
+			return dict((k,None) for k in req_keys)
+
+		def gen():
+			for key in req_keys:
+				val = None
+				for l in lines:
+					if l.startswith(key):
+						res = l.split('=',1)
+						if len(res) == 2 and not ' ' in res[1].strip():
+							val = res[1].strip()
+				yield (key,val)
+
+		return dict(gen())
 
 	def get_daemon_auth_cookie(self):
 		fn = self.get_daemon_auth_cookie_fn()
 		return get_lines_from_file(fn,'')[0] if file_is_readable(fn) else ''
 
+	def info(self,info_id):
+
+		def segwit_is_active():
+			d = self.cached['blockchaininfo']
+			if d['chain'] == 'regtest':
+				return True
+			if (    'bip9_softforks' in d
+					and 'segwit' in d['bip9_softforks']
+					and d['bip9_softforks']['segwit']['status'] == 'active'):
+				return True
+			if g.test_suite:
+				return True
+			return False
+
+		return locals()[info_id]()
+
 	rpcmethods = (
 		'backupwallet',
 		'createrawtransaction',
@@ -445,15 +477,16 @@ class BitcoinRPCClient(RPCClient,metaclass=aInitMeta):
 
 class EthereumRPCClient(RPCClient,metaclass=aInitMeta):
 
-	def __init__(self,*args,**kwargs): pass
-
-	async def __ainit__(self,proto,backend):
+	def __init__(self,*args,**kwargs):
+		pass
 
+	async def __ainit__(self,proto,daemon,backend):
 		self.proto = proto
+		self.daemon_data_dir = daemon.datadir
 
 		super().__init__(
-			host = g.rpc_host or 'localhost',
-			port = g.rpc_port or self.proto.rpc_port )
+			host = 'localhost' if g.test_suite else (g.rpc_host or 'localhost'),
+			port = daemon.rpc_port )
 
 		self.set_backend(backend)
 
@@ -468,7 +501,7 @@ class EthereumRPCClient(RPCClient,metaclass=aInitMeta):
 
 		self.daemon_version = vi['version']
 		self.cur_date = int(bh['timestamp'],16)
-		g.chain = ch.replace(' ','_')
+		self.chain = ch.replace(' ','_')
 		self.caps = ('full_node',) if nk['capability'] == 'full' else ()
 
 		try:
@@ -550,17 +583,25 @@ class MoneroWalletRPCClient(RPCClient):
 		'refresh',       # start_height
 	)
 
-async def rpc_init(proto=None,backend=None):
-
-	proto = proto or g.proto
-	backend = backend or opt.rpc_backend
+async def rpc_init(proto,backend=None):
 
 	if not 'rpc' in proto.mmcaps:
 		die(1,f'Coin daemon operations not supported for {proto.name} protocol!')
 
-	g.rpc = await {
+	from .daemon import CoinDaemon
+	rpc = await {
 		'bitcoind': BitcoinRPCClient,
 		'parity':   EthereumRPCClient,
-	}[proto.daemon_family](proto=proto,backend=backend)
-
-	return g.rpc
+	}[proto.daemon_family](
+		proto   = proto,
+		daemon  = CoinDaemon(proto=proto,test_suite=g.test_suite),
+		backend = backend or opt.rpc_backend )
+
+	if proto.chain_name != rpc.chain:
+		raise RPCChainMismatch(
+			'{} protocol chain is {}, but coin daemon chain is {}'.format(
+				proto.cls_name,
+				proto.chain_name.upper(),
+				rpc.chain.upper() ))
+
+	return rpc

+ 34 - 13
mmgen/share/Opts.py

@@ -29,7 +29,9 @@ def usage(opts_data):
 	print('USAGE: {} {}'.format(opts_data['prog_name'], opts_data['usage']))
 	sys.exit(2)
 
-def print_help(po,opts_data,opt_filter):
+def print_help(proto,po,opts_data,opt_filter):
+
+	from mmgen.util import pdie # DEBUG
 
 	def parse_lines(text):
 		filtered = False
@@ -47,21 +49,40 @@ def print_help(po,opts_data,opt_filter):
 	c = opts_data['code']
 	nl = '\n  '
 
-	text = nl.join(parse_lines(t[opts_type]))
-
 	pn = opts_data['prog_name']
-	out = (
-		'  {:<{p}} {}'.format(pn.upper()+':',t['desc'].strip(),p=len(pn)+1)
-		+ nl + '{:<{p}} {} {}'.format('USAGE:',pn,t['usage'].strip(),p=len(pn)+1)
-		+ nl + opts_type.upper().replace('_',' ') + ':'
-		+ nl + (c[opts_type](text) if opts_type in c else text)
-	)
 
-	if opts_type == 'options' and 'notes' in t:
-		ntext = c['notes'](t['notes']) if 'notes' in c else t['notes']
-		out += nl + nl.join(ntext.rstrip().splitlines())
+	from mmgen.help import help_notes_func
+	def help_notes(k):
+		return help_notes_func(proto,k)
+
+	def gen_arg_tuple(func,text):
+		d = {'proto': proto,'help_notes':help_notes}
+		for arg in func.__code__.co_varnames:
+			yield d[arg] if arg in d else text
+
+	def gen_text():
+		yield '  {:<{p}} {}'.format(pn.upper()+':',t['desc'].strip(),p=len(pn)+1)
+		yield '{:<{p}} {} {}'.format('USAGE:',pn,t['usage'].strip(),p=len(pn)+1)
+		yield opts_type.upper().replace('_',' ') + ':'
+
+		# process code for options
+		opts_text = nl.join(parse_lines(t[opts_type]))
+		if opts_type in c:
+			arg_tuple = tuple(gen_arg_tuple(c[opts_type],opts_text))
+			yield c[opts_type](*arg_tuple)
+		else:
+			yield opts_text
+
+		# process code for notes
+		if opts_type == 'options' and 'notes' in t:
+			notes_text = t['notes']
+			if 'notes' in c:
+				arg_tuple = tuple(gen_arg_tuple(c['notes'],notes_text))
+				notes_text = c['notes'](*arg_tuple)
+			for line in notes_text.splitlines():
+				yield line
 
-	print(out)
+	print(nl.join(gen_text()))
 	sys.exit(0)
 
 def process_uopts(opts_data,short_opts,long_opts):

+ 74 - 47
mmgen/tool.py

@@ -217,13 +217,6 @@ def _process_result(ret,pager=False,print_result=False):
 
 from .obj import MMGenAddrType
 
-def init_generators(arg=None):
-	global at,kg,ag
-	at = MMGenAddrType((hasattr(opt,'type') and opt.type) or g.proto.dfl_mmtype)
-	if arg != 'at':
-		kg = KeyGenerator(at)
-		ag = AddrGenerator(at)
-
 def conv_cls_bip39():
 	from .bip39 import bip39
 	return bip39
@@ -274,7 +267,22 @@ class MMGenToolCmdMeta(type):
 	def user_commands(cls):
 		return {k:v for k,v in cls.__dict__.items() if k in cls.methods}
 
-class MMGenToolCmds(metaclass=MMGenToolCmdMeta): pass
+class MMGenToolCmds(metaclass=MMGenToolCmdMeta):
+
+	def __init__(self,proto=None):
+		from .protocol import init_proto_from_opts
+		self.proto = proto or init_proto_from_opts()
+		if g.token:
+			self.proto.tokensym = g.token.upper()
+
+	def init_generators(self,arg=None):
+		global at,kg,ag
+		at = MMGenAddrType(
+			proto = self.proto,
+			id_str = getattr(opt,'type',None) or self.proto.dfl_mmtype )
+		if arg != 'at':
+			kg = KeyGenerator(self.proto,at)
+			ag = AddrGenerator(self.proto,at)
 
 class MMGenToolCmdMisc(MMGenToolCmds):
 	"miscellaneous commands"
@@ -408,16 +416,18 @@ class MMGenToolCmdCoin(MMGenToolCmds):
 	"""
 	def randwif(self):
 		"generate a random private key in WIF format"
-		init_generators('at')
+		self.init_generators('at')
 		return PrivKey(
+			self.proto,
 			get_random(32),
 			pubkey_type = at.pubkey_type,
 			compressed  = at.compressed ).wif
 
 	def randpair(self):
 		"generate a random private key/address pair"
-		init_generators()
+		self.init_generators()
 		privhex = PrivKey(
+			self.proto,
 			get_random(32),
 			pubkey_type = at.pubkey_type,
 			compressed  = at.compressed )
@@ -427,20 +437,23 @@ class MMGenToolCmdCoin(MMGenToolCmds):
 	def wif2hex(self,wifkey:'sstr'):
 		"convert a private key from WIF to hex format"
 		return PrivKey(
+			self.proto,
 			wif = wifkey )
 
 	def hex2wif(self,privhex:'sstr'):
 		"convert a private key from hex to WIF format"
-		init_generators('at')
+		self.init_generators('at')
 		return PrivKey(
+			self.proto,
 			bytes.fromhex(privhex),
 			pubkey_type = at.pubkey_type,
 			compressed  = at.compressed ).wif
 
 	def wif2addr(self,wifkey:'sstr'):
 		"generate a coin address from a key in WIF format"
-		init_generators()
+		self.init_generators()
 		privhex = PrivKey(
+			self.proto,
 			wif = wifkey )
 		addr = ag.to_addr(kg.to_pubhex(privhex))
 		return addr
@@ -448,16 +461,18 @@ class MMGenToolCmdCoin(MMGenToolCmds):
 	def wif2redeem_script(self,wifkey:'sstr'): # new
 		"convert a WIF private key to a Segwit P2SH-P2WPKH redeem script"
 		assert opt.type == 'segwit','This command is meaningful only for --type=segwit'
-		init_generators()
+		self.init_generators()
 		privhex = PrivKey(
+			self.proto,
 			wif = wifkey )
 		return ag.to_segwit_redeem_script(kg.to_pubhex(privhex))
 
 	def wif2segwit_pair(self,wifkey:'sstr'):
 		"generate both a Segwit P2SH-P2WPKH redeem script and address from WIF"
 		assert opt.type == 'segwit','This command is meaningful only for --type=segwit'
-		init_generators()
+		self.init_generators()
 		pubhex = kg.to_pubhex(PrivKey(
+			self.proto,
 			wif = wifkey ))
 		addr = ag.to_addr(pubhex)
 		rs = ag.to_segwit_redeem_script(pubhex)
@@ -465,8 +480,9 @@ class MMGenToolCmdCoin(MMGenToolCmds):
 
 	def privhex2addr(self,privhex:'sstr',output_pubhex=False):
 		"generate coin address from raw private key data in hexadecimal format"
-		init_generators()
+		self.init_generators()
 		pk = PrivKey(
+			self.proto,
 			bytes.fromhex(privhex),
 			compressed  = at.compressed,
 			pubkey_type = at.pubkey_type )
@@ -480,14 +496,14 @@ class MMGenToolCmdCoin(MMGenToolCmds):
 	def pubhex2addr(self,pubkeyhex:'sstr'):
 		"convert a hex pubkey to an address"
 		if opt.type == 'segwit':
-			return g.proto.pubhex2segwitaddr(pubkeyhex)
+			return self.proto.pubhex2segwitaddr(pubkeyhex)
 		else:
 			return self.pubhash2addr(hash160(pubkeyhex))
 
 	def pubhex2redeem_script(self,pubkeyhex:'sstr'): # new
 		"convert a hex pubkey to a Segwit P2SH-P2WPKH redeem script"
 		assert opt.type == 'segwit','This command is meaningful only for --type=segwit'
-		return g.proto.pubhex2redeem_script(pubkeyhex)
+		return self.proto.pubhex2redeem_script(pubkeyhex)
 
 	def redeem_script2addr(self,redeem_scripthex:'sstr'): # new
 		"convert a Segwit P2SH-P2WPKH redeem script to an address"
@@ -499,25 +515,25 @@ class MMGenToolCmdCoin(MMGenToolCmds):
 	def pubhash2addr(self,pubhashhex:'sstr'):
 		"convert public key hash to address"
 		if opt.type == 'bech32':
-			return g.proto.pubhash2bech32addr(pubhashhex)
+			return self.proto.pubhash2bech32addr(pubhashhex)
 		else:
-			init_generators('at')
-			return g.proto.pubhash2addr(pubhashhex,at.addr_fmt=='p2sh')
+			self.init_generators('at')
+			return self.proto.pubhash2addr(pubhashhex,at.addr_fmt=='p2sh')
 
 	def addr2pubhash(self,addr:'sstr'):
 		"convert coin address to public key hash"
 		from .tx import addr2pubhash
-		return addr2pubhash(CoinAddr(addr))
+		return addr2pubhash(self.proto,CoinAddr(self.proto,addr))
 
 	def addr2scriptpubkey(self,addr:'sstr'):
 		"convert coin address to scriptPubKey"
 		from .tx import addr2scriptPubKey
-		return addr2scriptPubKey(CoinAddr(addr))
+		return addr2scriptPubKey(self.proto,CoinAddr(self.proto,addr))
 
 	def scriptpubkey2addr(self,hexstr:'sstr'):
 		"convert scriptPubKey to coin address"
 		from .tx import scriptPubKey2addr
-		return scriptPubKey2addr(hexstr)[0]
+		return scriptPubKey2addr(self.proto,hexstr)[0]
 
 class MMGenToolCmdMnemonic(MMGenToolCmds):
 	"""
@@ -623,13 +639,13 @@ class MMGenToolCmdFile(MMGenToolCmds):
 		opt.yes = True
 		opt.quiet = True
 		from .addr import AddrList,KeyAddrList,PasswordList
-		ret = locals()[objname](mmgen_addrfile)
+		ret = locals()[objname](self.proto,mmgen_addrfile)
 		if opt.verbose:
 			if ret.al_id.mmtype.name == 'password':
 				fs = 'Passwd fmt:  {}\nPasswd len:  {}\nID string:   {}'
 				msg(fs.format(capfirst(ret.pw_info[ret.pw_fmt].desc),ret.pw_len,ret.pw_id_str))
 			else:
-				msg('Base coin:   {} {}'.format(ret.base_coin,('Mainnet','Testnet')[ret.is_testnet]))
+				msg(f'Base coin:   {ret.base_coin} {capfirst(ret.network)}')
 				msg('MMType:      {}'.format(capfirst(ret.al_id.mmtype.name)))
 			msg('List length: {}'.format(len(ret.data)))
 		return ret.chksum
@@ -646,7 +662,7 @@ class MMGenToolCmdFile(MMGenToolCmds):
 		"compute checksum for MMGen password file"
 		return self._file_chksum(mmgen_passwdfile,'PasswordList')
 
-	def txview( varargs_call_sig = { # hack to allow for multiple filenames
+	async def txview( varargs_call_sig = { # hack to allow for multiple filenames
 					'args': (
 						'mmgen_tx_file(s)',
 						'pager',
@@ -667,15 +683,23 @@ class MMGenToolCmdFile(MMGenToolCmds):
 		file_sort = kwargs.get('filesort') or 'mtime'
 
 		from .filename import MMGenFileList
-		from .tx import MMGenTX,MMGenTxForSigning
+		from .tx import MMGenTX
 		flist = MMGenFileList(infiles,ftype=MMGenTX)
 		flist.sort_by_age(key=file_sort) # in-place sort
 
-		def gen():
-			for fn in flist.names():
-				yield (MMGenTxForSigning,MMGenTX)[fn.endswith('.sigtx')](fn).format_view(terse=terse,sort=tx_sort)
+		async def process_file(fn):
+			if fn.endswith(MMGenTX.Signed.ext):
+				tx = MMGenTX.Signed(
+					filename   = fn,
+					quiet_open = True,
+					tw         = await MMGenTX.Signed.get_tracking_wallet(fn) )
+			else:
+				tx = MMGenTX.Unsigned(
+					filename   = fn,
+					quiet_open = True )
+			return tx.format_view(terse=terse,sort=tx_sort)
 
-		return ('—'*77+'\n').join(gen()).rstrip()
+		return ('—'*77+'\n').join([await process_file(fn) for fn in flist.names()]).rstrip()
 
 class MMGenToolCmdFileCrypt(MMGenToolCmds):
 	"""
@@ -841,7 +865,7 @@ class MMGenToolCmdWallet(MMGenToolCmds):
 
 	def gen_addr(self,mmgen_addr:str,wallet='',target='addr'):
 		"generate a single MMGen address from default or specified wallet"
-		addr = MMGenID(mmgen_addr)
+		addr = MMGenID(self.proto,mmgen_addr)
 		opt.quiet = True
 		sf = get_seed_file([wallet] if wallet else [],1)
 		from .wallet import Wallet
@@ -850,6 +874,7 @@ class MMGenToolCmdWallet(MMGenToolCmds):
 			m = 'Seed ID of requested address ({}) does not match wallet ({})'
 			die(1,m.format(addr.sid,ss.seed.sid))
 		al = AddrList(
+			proto     = self.proto,
 			seed      = ss.seed,
 			addr_idxs = AddrIdxList(str(addr.idx)),
 			mmtype    = addr.mmtype )
@@ -865,7 +890,7 @@ class MMGenToolCmdRPC(MMGenToolCmds):
 	async def getbalance(self,minconf=1,quiet=False,pager=False):
 		"list confirmed/unconfirmed, spendable/unspendable balances in tracking wallet"
 		from .tw import TwGetBalance
-		return (await TwGetBalance(minconf,quiet)).format()
+		return (await TwGetBalance(self.proto,minconf,quiet)).format()
 
 	async def listaddress(self,
 					mmgen_addr:str,
@@ -909,9 +934,9 @@ class MMGenToolCmdRPC(MMGenToolCmds):
 			if len(a) != 2:
 				m = "'{}': invalid address list argument (must be in form <seed ID>:[<type>:]<idx list>)"
 				die(1,m.format(mmgen_addrs))
-			usr_addr_list = [MMGenID('{}:{}'.format(a[0],i)) for i in AddrIdxList(a[1])]
+			usr_addr_list = [MMGenID(self.proto,f'{a[0]}:{i}') for i in AddrIdxList(a[1])]
 
-		al = await TwAddrList(usr_addr_list,minconf,showempty,showbtcaddrs,all_labels)
+		al = await TwAddrList(self.proto,usr_addr_list,minconf,showempty,showbtcaddrs,all_labels)
 		if not al:
 			die(0,('No tracked addresses with balances!','No tracked addresses!')[showempty])
 		return await al.format(showbtcaddrs,sort,show_age,age_fmt or 'confs')
@@ -926,7 +951,7 @@ class MMGenToolCmdRPC(MMGenToolCmds):
 				show_mmid = True,
 				wide_show_confs = True):
 		"view tracking wallet"
-		twuo = await TwUnspentOutputs(minconf=minconf)
+		twuo = await TwUnspentOutputs(self.proto,minconf=minconf)
 		await twuo.get_unspent_data(reverse_sort=reverse)
 		twuo.age_fmt = age_fmt
 		twuo.show_mmid = show_mmid
@@ -940,7 +965,7 @@ class MMGenToolCmdRPC(MMGenToolCmds):
 	async def add_label(self,mmgen_or_coin_addr:str,label:str):
 		"add descriptive label for address in tracking wallet"
 		from .tw import TrackingWallet
-		await (await TrackingWallet(mode='w')).add_label(mmgen_or_coin_addr,label,on_fail='raise')
+		await (await TrackingWallet(self.proto,mode='w')).add_label(mmgen_or_coin_addr,label,on_fail='raise')
 		return True
 
 	async def remove_label(self,mmgen_or_coin_addr:str):
@@ -951,7 +976,7 @@ class MMGenToolCmdRPC(MMGenToolCmds):
 	async def remove_address(self,mmgen_or_coin_addr:str):
 		"remove an address from tracking wallet"
 		from .tw import TrackingWallet
-		ret = await (await TrackingWallet(mode='w')).remove_address(mmgen_or_coin_addr) # returns None on failure
+		ret = await (await TrackingWallet(self.proto,mode='w')).remove_address(mmgen_or_coin_addr) # returns None on failure
 		if ret:
 			msg("Address '{}' deleted from tracking wallet".format(ret))
 		return ret
@@ -1083,9 +1108,9 @@ class MMGenToolCmdMonero(MMGenToolCmds):
 		async def process_wallets(op):
 			opt.accept_defaults = opt.accept_defaults or op.accept_defaults
 			from .protocol import init_proto
-			g.proto = init_proto('xmr')
+			proto = init_proto('xmr',network='mainnet')
 			from .addr import AddrList
-			al = KeyAddrList(infile)
+			al = KeyAddrList(proto,infile)
 			data = [d for d in al.data if addrs == '' or d.idx in AddrIdxList(addrs)]
 			dl = len(data)
 			assert dl,"No addresses in addrfile within range '{}'".format(addrs)
@@ -1139,7 +1164,7 @@ class MMGenToolCmdMonero(MMGenToolCmds):
 			'create': wo('create', 'Creat', 'Generat', create, False),
 			'sync':   wo('sync',   'Sync',  'Sync',    sync,   True) }[op]
 		try:
-			run_session(process_wallets(op),do_rpc_init=False)
+			run_session(process_wallets(op))
 		except KeyboardInterrupt:
 			rdie(1,'\nUser interrupt\n')
 		except EOFError:
@@ -1191,6 +1216,7 @@ class tool_api(
 		"""
 		Initializer - takes no arguments
 		"""
+		super().__init__()
 		if not hasattr(opt,'version'):
 			opts.init()
 		opt.use_old_ed25519 = None
@@ -1205,7 +1231,8 @@ class tool_api(
 		from .protocol import init_proto,init_genonly_altcoins
 		altcoin_trust_level = init_genonly_altcoins(coinsym,testnet=network in ('testnet','regtest'))
 		warn_altcoins(coinsym,altcoin_trust_level)
-		return init_proto(coinsym,network=network)
+		self.proto = init_proto(coinsym,network=network) # FIXME
+		return self.proto
 
 	@property
 	def coins(self):
@@ -1214,18 +1241,18 @@ class tool_api(
 		from .altcoin import CoinInfo
 		return sorted(set(
 			[c.upper() for c in CoinProtocol.coins]
-			+ [c.symbol for c in CoinInfo.get_supported_coins(g.proto.network)]
+			+ [c.symbol for c in CoinInfo.get_supported_coins(self.proto.network)]
 		))
 
 	@property
 	def coin(self):
 		"""The currently configured coin"""
-		return g.coin
+		return self.proto.coin
 
 	@property
 	def network(self):
 		"""The currently configured network"""
-		return g.proto.network
+		return self.proto.network
 
 	@property
 	def addrtypes(self):
@@ -1233,14 +1260,14 @@ class tool_api(
 		The available address types for current coin/network pair.  The
 		first-listed is the default
 		"""
-		return [MMGenAddrType(t).name for t in g.proto.mmtypes]
+		return [MMGenAddrType(proto=proto,id_str=id_str).name for id_str in self.proto.mmtypes]
 
 	def print_addrtypes(self):
 		"""
 		Print the available address types for current coin/network pair along with
 		a description.  The first-listed is the default
 		"""
-		for t in [MMGenAddrType(s) for s in g.proto.mmtypes]:
+		for t in [MMGenAddrType(proto=proto,id_str=id_str).name for id_str in self.proto.mmtypes]:
 			print('{:<12} - {}'.format(t.name,t.desc))
 
 	@property

+ 340 - 267
mmgen/tw.py

@@ -21,43 +21,45 @@ tw: Tracking wallet methods for the MMGen suite
 """
 
 import json
+from collections import namedtuple
 from .exception import *
 from .common import *
 from .obj import *
-from .tx import is_mmgen_id
+from .tx import is_mmgen_id,is_coin_addr
+from .rpc import rpc_init
 
 CUR_HOME,ERASE_ALL = '\033[H','\033[0J'
 def CUR_RIGHT(n): return '\033[{}C'.format(n)
 
-def get_tw_label(s):
-	try: return TwLabel(s,on_fail='raise')
+def get_tw_label(proto,s):
+	try: return TwLabel(proto,s,on_fail='raise')
 	except BadTwComment: raise
 	except: return None
 
 _date_formatter = {
-	'days':      lambda secs: (g.rpc.cur_date - secs) // 86400,
-	'date':      lambda secs: '{}-{:02}-{:02}'.format(*time.gmtime(secs)[:3])[2:],
-	'date_time': lambda secs: '{}-{:02}-{:02} {:02}:{:02}'.format(*time.gmtime(secs)[:5]),
+	'days':      lambda rpc,secs: (rpc.cur_date - secs) // 86400,
+	'date':      lambda rpc,secs: '{}-{:02}-{:02}'.format(*time.gmtime(secs)[:3])[2:],
+	'date_time': lambda rpc,secs: '{}-{:02}-{:02} {:02}:{:02}'.format(*time.gmtime(secs)[:5]),
 }
 
-async def _set_dates(us):
+async def _set_dates(rpc,us):
 	if us and us[0].date is None:
 		# 'blocktime' differs from 'time', is same as getblockheader['time']
-		dates = [o['blocktime'] for o in await g.rpc.gathered_call('gettransaction',[(o.txid,) for o in us])]
-		for o,date in zip(us,dates):
-			o.date = date
+		dates = [o['blocktime'] for o in await rpc.gathered_call('gettransaction',[(o.txid,) for o in us])]
+		for idx,o in enumerate(us):
+			o.date = dates[idx]
 
 if os.getenv('MMGEN_BOGUS_WALLET_DATA'):
 	# 1831006505 (09 Jan 2028) = projected time of block 1000000
-	_date_formatter['days'] = lambda date: (1831006505 - date) // 86400
-	async def _set_dates(us):
+	_date_formatter['days'] = lambda rpc,secs: (1831006505 - secs) // 86400
+	async def _set_dates(rpc,us):
 		for o in us:
 			o.date = 1831006505 - int(9.7 * 60 * (o.confs - 1))
 
 class TwUnspentOutputs(MMGenObject,metaclass=aInitMeta):
 
-	def __new__(cls,*args,**kwargs):
-		return MMGenObject.__new__(altcoin_subclass(cls,'tw','TwUnspentOutputs'))
+	def __new__(cls,proto,*args,**kwargs):
+		return MMGenObject.__new__(altcoin_subclass(cls,proto,'tw'))
 
 	txid_w = 64
 	disp_type = 'btc'
@@ -87,16 +89,30 @@ Actions: [q]uit view, [p]rint to file, pager [v]iew, [w]ide view, add [l]abel:
 	class MMGenTwUnspentOutput(MMGenListItem):
 		txid         = ListItemAttr('CoinTxID')
 		vout         = ListItemAttr(int,typeconv=False)
-		amt          = ImmutableAttr(lambda val:g.proto.coin_amt(val),typeconv=False)
-		amt2         = ListItemAttr(lambda val:g.proto.coin_amt(val),typeconv=False)
+		amt          = ImmutableAttr(None)
+		amt2         = ListItemAttr(None)
 		label        = ListItemAttr('TwComment',reassign_ok=True)
-		twmmid       = ImmutableAttr('TwMMGenID')
-		addr         = ImmutableAttr('CoinAddr')
+		twmmid       = ImmutableAttr('TwMMGenID',include_proto=True)
+		addr         = ImmutableAttr('CoinAddr',include_proto=True)
 		confs        = ImmutableAttr(int,typeconv=False)
 		date         = ListItemAttr(int,typeconv=False,reassign_ok=True)
 		scriptPubKey = ImmutableAttr('HexStr')
 		skip         = ListItemAttr(str,typeconv=False,reassign_ok=True)
 
+		# required by gen_unspent(); setting valid_attrs explicitly is also more efficient
+		valid_attrs = {'txid','vout','amt','amt2','label','twmmid','addr','confs','date','scriptPubKey','skip'}
+		invalid_attrs = {'proto'}
+
+		def __init__(self,proto,**kwargs):
+			self.__dict__['proto'] = proto
+			MMGenListItem.__init__(self,**kwargs)
+
+		class conv_funcs:
+			def amt(self,value):
+				return self.proto.coin_amt(value)
+			def amt2(self,value):
+				return self.proto.coin_amt(value)
+
 	wmsg = {
 	'no_spendable_outputs': """
 No spendable outputs found!  Import addresses with balances into your
@@ -104,7 +120,8 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
 """.strip().format(g.proj_name.lower())
 	}
 
-	async def __ainit__(self,minconf=1,addrs=[]):
+	async def __ainit__(self,proto,minconf=1,addrs=[]):
+		self.proto        = proto
 		self.unspent      = self.MMGenTwOutputList()
 		self.fmt_display  = ''
 		self.fmt_print    = ''
@@ -116,8 +133,11 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
 		self.addrs        = addrs
 		self.sort_key     = 'age'
 		self.disp_prec    = self.get_display_precision()
+		self.rpc          = await rpc_init(proto)
 
-		self.wallet = await TrackingWallet(mode='w')
+		self.wallet = await TrackingWallet(proto,mode='w')
+		if self.disp_type == 'token':
+			self.proto.tokensym = self.wallet.symbol
 
 	@property
 	def age_fmt(self):
@@ -130,7 +150,7 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
 		self._age_fmt = val
 
 	def get_display_precision(self):
-		return g.proto.coin_amt.max_prec
+		return self.proto.coin_amt.max_prec
 
 	@property
 	def total(self):
@@ -147,42 +167,40 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
 
 		# for now, self.addrs is just an empty list for Bitcoin and friends
 		add_args = (9999999,self.addrs) if self.addrs else ()
-		return await g.rpc.call('listunspent',self.minconf,*add_args)
+		return await self.rpc.call('listunspent',self.minconf,*add_args)
 
 	async def get_unspent_data(self,sort_key=None,reverse_sort=False):
 		if g.bogus_wallet_data: # for debugging purposes only
-			us_rpc = eval(get_data_from_file(g.bogus_wallet_data)) # testing, so ok
+			us_raw = eval(get_data_from_file(g.bogus_wallet_data)) # testing, so ok
 		else:
-			us_rpc = await self.get_unspent_rpc()
+			us_raw = await self.get_unspent_rpc()
 
-		if not us_rpc:
+		if not us_raw:
 			die(0,self.wmsg['no_spendable_outputs'])
 
-		tr_rpc = []
-		lbl_id = ('account','label')['label_api' in g.rpc.caps]
-
-		for o in us_rpc:
-			if not lbl_id in o:
-				continue # coinbase outputs have no account field
-			l = get_tw_label(o[lbl_id])
-			if l:
-				o.update({
-					'twmmid': l.mmid,
-					'label':  l.comment,
-					'amt':    g.proto.coin_amt(o['amount']),
-					'addr':   CoinAddr(o['address']),
-					'confs':  o['confirmations']
-				})
-				tr_rpc.append(o)
-
-		self.unspent = self.MMGenTwOutputList(
-						self.MMGenTwUnspentOutput(
-							**{k:v for k,v in o.items() if k in dir(self.MMGenTwUnspentOutput)}
-						) for o in tr_rpc)
-		for u in self.unspent:
-			if u.label == None: u.label = ''
+		lbl_id = ('account','label')['label_api' in self.rpc.caps]
+
+		def gen_unspent():
+			for o in us_raw:
+				if not lbl_id in o:
+					continue # coinbase outputs have no account field
+				l = get_tw_label(self.proto,o[lbl_id])
+				if l:
+					o.update({
+						'twmmid': l.mmid,
+						'label':  l.comment or '',
+						'amt':    self.proto.coin_amt(o['amount']),
+						'addr':   CoinAddr(self.proto,o['address']),
+						'confs':  o['confirmations']
+					})
+					yield self.MMGenTwUnspentOutput(
+						self.proto,
+						**{ k:v for k,v in o.items() if k in self.MMGenTwUnspentOutput.valid_attrs } )
+
+		self.unspent = self.MMGenTwOutputList(gen_unspent())
+
 		if not self.unspent:
-			die(1,'No tracked {}s in tracking wallet!'.format(self.item_desc))
+			die(1, f'No tracked {self.item_desc}s in tracking wallet!')
 
 		self.do_sort(key=sort_key,reverse=reverse_sort)
 
@@ -217,11 +235,10 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
 			m2 = 'Please resize your screen to at least {} characters and hit ENTER '
 			my_raw_input((m1+m2).format(g.min_screen_width))
 
-	async def format_for_display(self):
+	def get_display_constants(self):
 		unsp = self.unspent
-		if self.age_fmt in self.age_fmts_date_dependent:
-			await _set_dates(unsp)
-		self.set_term_columns()
+		for i in unsp:
+			i.skip = ''
 
 		# allow for 7-digit confirmation nums
 		col1_w = max(3,len(str(len(unsp)))+1) # num + ')'
@@ -236,117 +253,148 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
 		tx_w = min(self.txid_w,self.cols-addr_w-29-col1_w) # min=6 TODO
 		txdots = ('','..')[tx_w < self.txid_w]
 
-		for i in unsp: i.skip = ''
+		dc = namedtuple('display_constants',['col1_w','mmid_w','addr_w','btaddr_w','label_w','tx_w','txdots'])
+		return dc(col1_w,mmid_w,addr_w,btaddr_w,label_w,tx_w,txdots)
+
+	async def format_for_display(self):
+		unsp = self.unspent
+		if self.age_fmt in self.age_fmts_date_dependent:
+			await _set_dates(self.rpc,unsp)
+		self.set_term_columns()
+
+		c = getattr(self,'display_constants',None)
+		if not c:
+			c = self.display_constants = self.get_display_constants()
+
 		if self.group and (self.sort_key in ('addr','txid','twmmid')):
 			for a,b in [(unsp[i],unsp[i+1]) for i in range(len(unsp)-1)]:
 				for k in ('addr','txid','twmmid'):
 					if self.sort_key == k and getattr(a,k) == getattr(b,k):
 						b.skip = (k,'addr')[k=='twmmid']
 
-		out  = [self.hdr_fmt.format(' '.join(self.sort_info()),g.dcoin,self.total.hl())]
-		if g.chain != 'mainnet': out += ['Chain: '+green(g.chain.upper())]
-		fs = {  'btc':   ' {n:%s} {t:%s} {v:2} {a} {A} {c:<}' % (col1_w,tx_w),
-				'eth':   ' {n:%s} {a} {A}' % col1_w,
-				'token': ' {n:%s} {a} {A} {A2}' % col1_w }[self.disp_type]
-		fs_hdr = ' {n:%s} {t:%s} {a} {A} {c:<}' % (col1_w,tx_w) if self.disp_type == 'btc' else fs
-		date_hdr = {
-			'confs':     'Confs',
-			'block':     'Block',
-			'days':      'Age(d)',
-			'date':      'Date',
-			'date_time': 'Date',
-		}
-		out += [fs_hdr.format(
-							n='Num',
-							t='TXid'.ljust(tx_w - 2) + ' Vout',
-							a='Address'.ljust(addr_w),
-							A='Amt({})'.format(g.dcoin).ljust(self.disp_prec+5),
-							A2=' Amt({})'.format(g.coin).ljust(self.disp_prec+4),
-							c = date_hdr[self.age_fmt],
-						).rstrip()]
-
-		for n,i in enumerate(unsp):
-			addr_dots = '|' + '.'*(addr_w-1)
-			mmid_disp = MMGenID.fmtc('.'*mmid_w if i.skip=='addr'
-				else i.twmmid if i.twmmid.type=='mmgen'
-					else 'Non-{}'.format(g.proj_name),width=mmid_w,color=True)
-			if self.show_mmid:
-				addr_out = '{} {}'.format(
-					type(i.addr).fmtc(addr_dots,width=btaddr_w,color=True) if i.skip == 'addr' \
-							else i.addr.fmt(width=btaddr_w,color=True),
-					'{} {}'.format(mmid_disp,i.label.fmt(width=label_w,color=True) \
-							if label_w > 0 else ''))
-			else:
-				addr_out = type(i.addr).fmtc(addr_dots,width=addr_w,color=True) \
-					if i.skip=='addr' else i.addr.fmt(width=addr_w,color=True)
-
-			out.append(fs.format(   n=str(n+1)+')',
-									t='' if not i.txid else \
-										' ' * (tx_w-4) + '|...' if i.skip == 'txid' \
-											else i.txid[:tx_w-len(txdots)] + txdots,
-									v=i.vout,
-									a=addr_out,
-									A=i.amt.fmt(color=True,prec=self.disp_prec),
-									A2=(i.amt2.fmt(color=True,prec=self.disp_prec) if i.amt2 is not None else ''),
-									c=self.age_disp(i,self.age_fmt),
-									).rstrip())
-
-		self.fmt_display = '\n'.join(out) + '\n'
+		def gen_output():
+			yield self.hdr_fmt.format(' '.join(self.sort_info()),self.proto.dcoin,self.total.hl())
+			if self.proto.chain_name != 'mainnet':
+				yield 'Chain: '+green(self.proto.chain_name.upper())
+			fs = {  'btc':   ' {n:%s} {t:%s} {v:2} {a} {A} {c:<}' % (c.col1_w,c.tx_w),
+					'eth':   ' {n:%s} {a} {A}' % c.col1_w,
+					'token': ' {n:%s} {a} {A} {A2}' % c.col1_w }[self.disp_type]
+			fs_hdr = ' {n:%s} {t:%s} {a} {A} {c:<}' % (c.col1_w,c.tx_w) if self.disp_type == 'btc' else fs
+			date_hdr = {
+				'confs':     'Confs',
+				'block':     'Block',
+				'days':      'Age(d)',
+				'date':      'Date',
+				'date_time': 'Date',
+			}
+			yield fs_hdr.format(
+				n  = 'Num',
+				t  = 'TXid'.ljust(c.tx_w - 2) + ' Vout',
+				a  = 'Address'.ljust(c.addr_w),
+				A  = 'Amt({})'.format(self.proto.dcoin).ljust(self.disp_prec+5),
+				A2 = ' Amt({})'.format(self.proto.coin).ljust(self.disp_prec+4),
+				c  =  date_hdr[self.age_fmt],
+				).rstrip()
+
+			for n,i in enumerate(unsp):
+				addr_dots = '|' + '.'*(c.addr_w-1)
+				mmid_disp = MMGenID.fmtc('.'*c.mmid_w if i.skip=='addr'
+					else i.twmmid if i.twmmid.type=='mmgen'
+						else 'Non-{}'.format(g.proj_name),width=c.mmid_w,color=True)
+
+				if self.show_mmid:
+					addr_out = '{} {}{}'.format((
+						type(i.addr).fmtc(addr_dots,width=c.btaddr_w,color=True) if i.skip == 'addr' else
+						i.addr.fmt(width=c.btaddr_w,color=True)
+					),
+						mmid_disp,
+						(' ' + i.label.fmt(width=c.label_w,color=True)) if c.label_w > 0 else ''
+					)
+				else:
+					addr_out = (
+						type(i.addr).fmtc(addr_dots,width=c.addr_w,color=True) if i.skip=='addr' else
+						i.addr.fmt(width=c.addr_w,color=True) )
+
+				yield fs.format(
+					n  = str(n+1)+')',
+					t  = (
+						'' if not i.txid else
+						' ' * (c.tx_w-4) + '|...' if i.skip  == 'txid' else
+						i.txid[:c.tx_w-len(c.txdots)] + c.txdots ),
+					v  = i.vout,
+					a  = addr_out,
+					A  = i.amt.fmt(color=True,prec=self.disp_prec),
+					A2 = (i.amt2.fmt(color=True,prec=self.disp_prec) if i.amt2 is not None else ''),
+					c  = self.age_disp(i,self.age_fmt),
+					).rstrip()
+
+		self.fmt_display = '\n'.join(gen_output()) + '\n'
 		return self.fmt_display
 
 	async def format_for_printing(self,color=False,show_confs=True):
 		if self.age_fmt in self.age_fmts_date_dependent:
-			await _set_dates(self.unspent)
+			await _set_dates(self.rpc,self.unspent)
 		addr_w = max(len(i.addr) for i in self.unspent)
 		mmid_w = max(len(('',i.twmmid)[i.twmmid.type=='mmgen']) for i in self.unspent) or 12 # DEADBEEF:S:1
-		amt_w = g.proto.coin_amt.max_prec + 5
+		amt_w = self.proto.coin_amt.max_prec + 5
 		cfs = '{c:<8} ' if show_confs else ''
-		fs = {  'btc': (' {n:4} {t:%s} {a} {m} {A:%s} ' + cfs + '{b:<8} {D:<19} {l}') % (self.txid_w+3,amt_w),
-				'eth':   ' {n:4} {a} {m} {A:%s} {l}' % amt_w,
-				'token': ' {n:4} {a} {m} {A:%s} {A2:%s} {l}' % (amt_w,amt_w)
-				}[self.disp_type]
-		out = [fs.format(   n='Num',
-							t='Tx ID,Vout',
-							a='Address'.ljust(addr_w),
-							m='MMGen ID'.ljust(mmid_w),
-							A='Amount({})'.format(g.dcoin),
-							A2='Amount({})'.format(g.coin),
-							c='Confs',  # skipped for eth
-							b='Block',  # skipped for eth
-							D='Date',
-							l='Label')]
-
-		max_lbl_len = max([len(i.label) for i in self.unspent if i.label] or [2])
-		for n,i in enumerate(self.unspent):
-			addr = '|'+'.' * addr_w if i.skip == 'addr' and self.group else i.addr.fmt(color=color,width=addr_w)
-			out.append(fs.format(
-						n=str(n+1)+')',
-						t='{},{}'.format('|'+'.'*63 if i.skip == 'txid' and self.group else i.txid,i.vout),
-						a=addr,
-						m=MMGenID.fmtc(i.twmmid if i.twmmid.type=='mmgen'
-							else 'Non-{}'.format(g.proj_name),width=mmid_w,color=color),
-						A=i.amt.fmt(color=color),
-						A2=(i.amt2.fmt(color=color) if i.amt2 is not None else ''),
-						c=i.confs,
-						b=g.rpc.blockcount - (i.confs - 1),
-						D=self.age_disp(i,'date_time'),
-						l=i.label.hl(color=color) if i.label else
-							TwComment.fmtc('',color=color,nullrepl='-',width=max_lbl_len)).rstrip())
-
-		fs = '{} (block #{}, {} UTC)\nSort order: {}\n{}\n\nTotal {}: {}\n'
-		self.fmt_print = fs.format(
-				capfirst(self.desc),
-				g.rpc.blockcount,
-				make_timestr(g.rpc.cur_date),
-				' '.join(self.sort_info(include_group=False)),
-				'\n'.join(out),
-				g.dcoin,
-				self.total.hl(color=color))
+		fs = {
+			'btc': (' {n:4} {t:%s} {a} {m} {A:%s} ' + cfs + '{b:<8} {D:<19} {l}') % (self.txid_w+3,amt_w),
+			'eth':   ' {n:4} {a} {m} {A:%s} {l}' % amt_w,
+			'token': ' {n:4} {a} {m} {A:%s} {A2:%s} {l}' % (amt_w,amt_w)
+			}[self.disp_type]
+
+		def gen_output():
+			yield fs.format(
+				n  = 'Num',
+				t  = 'Tx ID,Vout',
+				a  = 'Address'.ljust(addr_w),
+				m  = 'MMGen ID'.ljust(mmid_w),
+				A  = 'Amount({})'.format(self.proto.dcoin),
+				A2 = 'Amount({})'.format(self.proto.coin),
+				c  = 'Confs',  # skipped for eth
+				b  = 'Block',  # skipped for eth
+				D  = 'Date',
+				l  = 'Label' )
+
+			max_lbl_len = max([len(i.label) for i in self.unspent if i.label] or [2])
+			for n,i in enumerate(self.unspent):
+				yield fs.format(
+					n  = str(n+1)+')',
+					t  = '{},{}'.format('|'+'.'*63 if i.skip == 'txid' and self.group else i.txid,i.vout),
+					a  = (
+						'|'+'.' * addr_w if i.skip == 'addr' and self.group else
+						i.addr.fmt(color=color,width=addr_w) ),
+					m  = MMGenID.fmtc(i.twmmid if i.twmmid.type=='mmgen' else
+						'Non-{}'.format(g.proj_name),width = mmid_w,color=color),
+					A  = i.amt.fmt(color=color),
+					A2 = ( i.amt2.fmt(color=color) if i.amt2 is not None else '' ),
+					c  = i.confs,
+					b  = self.rpc.blockcount - (i.confs - 1),
+					D  = self.age_disp(i,'date_time'),
+					l  = i.label.hl(color=color) if i.label else
+						TwComment.fmtc('',color = color,nullrepl='-',width=max_lbl_len) ).rstrip()
+
+		fs2 = '{} (block #{}, {} UTC)\n{}Sort order: {}\n{}\n\nTotal {}: {}\n'
+		self.fmt_print = fs2.format(
+			capfirst(self.desc),
+			self.rpc.blockcount,
+			make_timestr(self.rpc.cur_date),
+			('' if self.proto.chain_name == 'mainnet' else
+			'Chain: {}\n'.format(green(self.proto.chain_name.upper())) ),
+			' '.join(self.sort_info(include_group=False)),
+			'\n'.join(gen_output()),
+			self.proto.dcoin,
+			self.total.hl(color=color) )
+
 		return self.fmt_print
 
 	def display_total(self):
-		fs = '\nTotal unspent: {} {} ({} output%s)' % suf(self.unspent)
-		msg(fs.format(self.total.hl(),g.dcoin,len(self.unspent)))
+		msg('\nTotal unspent: {} {} ({} output{})'.format(
+			self.total.hl(),
+			self.proto.dcoin,
+			len(self.unspent),
+			suf(self.unspent) ))
 
 	def get_idx_from_user(self,action):
 		msg('')
@@ -420,7 +468,8 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
 					e = self.unspent[idx-1]
 					bal = await self.wallet.get_balance(e.addr,force_rpc=True)
 					await self.get_unspent_data()
-					oneshot_msg = yellow('{} balance for account #{} refreshed\n\n'.format(g.dcoin,idx))
+					oneshot_msg = yellow('{} balance for account #{} refreshed\n\n'.format(self.proto.dcoin,idx))
+				self.display_constants = self.get_display_constants()
 			elif action == 'a_lbl_add':
 				idx,lbl = self.get_idx_from_user(action)
 				if idx:
@@ -431,6 +480,7 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
 						oneshot_msg = yellow("Label {} {} #{}\n\n".format(a,self.item_desc,idx))
 					else:
 						oneshot_msg = red('Label could not be added\n\n')
+				self.display_constants = self.get_display_constants()
 			elif action == 'a_addr_delete':
 				idx = self.get_idx_from_user(action)
 				if idx:
@@ -440,8 +490,9 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
 						oneshot_msg = yellow("{} #{} removed\n\n".format(capfirst(self.item_desc),idx))
 					else:
 						oneshot_msg = red('Address could not be removed\n\n')
+				self.display_constants = self.get_display_constants()
 			elif action == 'a_print':
-				of = '{}-{}[{}].out'.format(self.dump_fn_pfx,g.dcoin,
+				of = '{}-{}[{}].out'.format(self.dump_fn_pfx,self.proto.dcoin,
 										','.join(self.sort_info(include_group=False)).lower())
 				msg('')
 				try:
@@ -460,22 +511,22 @@ watch-only wallet using '{}-addrimport' and then re-run this program.
 		if age_fmt == 'confs':
 			return o.confs
 		elif age_fmt == 'block':
-			return g.rpc.blockcount - (o.confs - 1)
+			return self.rpc.blockcount - (o.confs - 1)
 		else:
-			return _date_formatter[age_fmt](o.date)
+			return _date_formatter[age_fmt](self.rpc,o.date)
 
 class TwAddrList(MMGenDict,metaclass=aInitMeta):
 	has_age = True
 	age_fmts = TwUnspentOutputs.age_fmts
 	age_disp = TwUnspentOutputs.age_disp
 
-	def __new__(cls,*args,**kwargs):
-		return MMGenDict.__new__(altcoin_subclass(cls,'tw','TwAddrList'),*args,**kwargs)
+	def __new__(cls,proto,*args,**kwargs):
+		return MMGenDict.__new__(altcoin_subclass(cls,proto,'tw'),*args,**kwargs)
 
-	def __init__(self,*args,**kwargs):
+	def __init__(self,proto,*args,**kwargs):
 		pass
 
-	async def __ainit__(self,usr_addr_list,minconf,showempty,showbtcaddrs,all_labels,wallet=None):
+	async def __ainit__(self,proto,usr_addr_list,minconf,showempty,showbtcaddrs,all_labels,wallet=None):
 
 		def check_dup_mmid(acct_labels):
 			mmid_prev,err = None,False
@@ -493,18 +544,20 @@ class TwAddrList(MMGenDict,metaclass=aInitMeta):
 				if len(addrs) != 1:
 					err = True
 					if len(addrs) == 0:
-						msg("Label '{}': has no associated address!".format(label))
+						msg(f'Label {label!r}: has no associated address!')
 					else:
-						msg("'{}': more than one {} address in account!".format(addrs,g.coin))
+						msg(f'{addrs!r}: more than one {proto.coin} address in account!')
 			if err: rdie(3,'Tracking wallet is corrupted!')
 
-		self.total = g.proto.coin_amt('0')
+		self.rpc   = await rpc_init(proto)
+		self.total = proto.coin_amt('0')
+		self.proto = proto
 
-		lbl_id = ('account','label')['label_api' in g.rpc.caps]
-		for d in await g.rpc.call('listunspent',0):
+		lbl_id = ('account','label')['label_api' in self.rpc.caps]
+		for d in await self.rpc.call('listunspent',0):
 			if not lbl_id in d: continue  # skip coinbase outputs with missing account
 			if d['confirmations'] < minconf: continue
-			label = get_tw_label(d[lbl_id])
+			label = get_tw_label(proto,d[lbl_id])
 			if label:
 				lm = label.mmid
 				if usr_addr_list and (lm not in usr_addr_list):
@@ -512,14 +565,16 @@ class TwAddrList(MMGenDict,metaclass=aInitMeta):
 				if lm in self:
 					if self[lm]['addr'] != d['address']:
 						die(2,'duplicate {} address ({}) for this MMGen address! ({})'.format(
-								g.coin,d['address'],self[lm]['addr']))
+							proto.coin,
+							d['address'],
+							self[lm]['addr']) )
 				else:
 					lm.confs = d['confirmations']
 					lm.txid = d['txid']
 					lm.date = None
-					self[lm] = {'amt': g.proto.coin_amt('0'),
+					self[lm] = {'amt': proto.coin_amt('0'),
 								'lbl': label,
-								'addr': CoinAddr(d['address'])}
+								'addr': CoinAddr(proto,d['address'])}
 				self[lm]['amt'] += d['amount']
 				self.total += d['amount']
 
@@ -527,14 +582,14 @@ class TwAddrList(MMGenDict,metaclass=aInitMeta):
 		if showempty or all_labels:
 			# 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
-			if 'label_api' in g.rpc.caps:
-				acct_list = await g.rpc.call('listlabels')
-				aa = await g.rpc.batch_call('getaddressesbylabel',[(k,) for k in acct_list])
+			if 'label_api' in self.rpc.caps:
+				acct_list = await self.rpc.call('listlabels')
+				aa = await self.rpc.batch_call('getaddressesbylabel',[(k,) for k in acct_list])
 				acct_addrs = [list(a.keys()) for a in aa]
 			else:
-				acct_list = list((await g.rpc.call('listaccounts',0,True)).keys()) # raw list, no 'L'
-				acct_addrs = await g.rpc.batch_call('getaddressesbyaccount',[(a,) for a in acct_list]) # use raw list here
-			acct_labels = MMGenList([get_tw_label(a) for a in acct_list])
+				acct_list = list((await self.rpc.call('listaccounts',0,True)).keys()) # raw list, no 'L'
+				acct_addrs = await self.rpc.batch_call('getaddressesbyaccount',[(a,) for a in acct_list]) # use raw list here
+			acct_labels = MMGenList([get_tw_label(proto,a) for a in acct_list])
 			check_dup_mmid(acct_labels)
 			assert len(acct_list) == len(acct_addrs),(
 				'listaccounts() and getaddressesbyaccount() not equal in length')
@@ -545,9 +600,9 @@ class TwAddrList(MMGenDict,metaclass=aInitMeta):
 				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 label.mmid not in self:
-					self[label.mmid] = { 'amt':g.proto.coin_amt('0'), 'lbl':label, 'addr':'' }
+					self[label.mmid] = { 'amt':proto.coin_amt('0'), 'lbl':label, 'addr':'' }
 					if showbtcaddrs:
-						self[label.mmid]['addr'] = CoinAddr(addr_arr[0])
+						self[label.mmid]['addr'] = CoinAddr(proto,addr_arr[0])
 
 	def raw_list(self):
 		return [((k if k.type == 'mmgen' else 'Non-MMGen'),self[k]['addr'],self[k]['amt']) for k in self]
@@ -560,22 +615,13 @@ class TwAddrList(MMGenDict,metaclass=aInitMeta):
 			show_age = False
 		if age_fmt not in self.age_fmts:
 			raise BadAgeFormat("'{}': invalid age format (must be one of {!r})".format(age_fmt,self.age_fmts))
-		out = ['Chain: '+green(g.chain.upper())] if g.chain != 'mainnet' else []
 		fs = '{mid}' + ('',' {addr}')[showbtcaddrs] + ' {cmt} {amt}' + ('',' {age}')[show_age]
 		mmaddrs = [k for k in self.keys() if k.type == 'mmgen']
 		max_mmid_len = max(len(k) for k in mmaddrs) + 2 if mmaddrs else 10
 		max_cmt_width = max(max(v['lbl'].comment.screen_width for v in self.values()),7)
 		addr_width = max(len(self[mmid]['addr']) for mmid in self)
 
-		# fp: fractional part
 		max_fp_len = max([len(a.split('.')[1]) for a in [str(v['amt']) for v in self.values()] if '.' in a] or [1])
-		out += [fs.format(
-				mid=MMGenID.fmtc('MMGenID',width=max_mmid_len),
-				addr=(CoinAddr.fmtc('ADDRESS',width=addr_width) if showbtcaddrs else None),
-				cmt=TwComment.fmtc('COMMENT',width=max_cmt_width+1),
-				amt='BALANCE'.ljust(max_fp_len+4),
-				age=age_fmt.upper(),
-				).rstrip()]
 
 		def sort_algo(j):
 			if sort and 'age' in sort:
@@ -587,31 +633,47 @@ class TwAddrList(MMGenDict,metaclass=aInitMeta):
 			else:
 				return j.sort_key
 
-		al_id_save = None
 		mmids = sorted(self,key=sort_algo,reverse=bool(sort and 'reverse' in sort))
 		if show_age:
-			await _set_dates([o for o in mmids if hasattr(o,'confs')])
-		for mmid in mmids:
-			if mmid.type == 'mmgen':
-				if al_id_save and al_id_save != mmid.obj.al_id:
-					out.append('')
-				al_id_save = mmid.obj.al_id
-				mmid_disp = mmid
-			else:
-				if al_id_save:
-					out.append('')
-					al_id_save = None
-				mmid_disp = 'Non-MMGen'
-			e = self[mmid]
-			out.append(fs.format(
-				mid=MMGenID.fmtc(mmid_disp,width=max_mmid_len,color=True),
-				addr=(e['addr'].fmt(color=True,width=addr_width) if showbtcaddrs else None),
-				cmt=e['lbl'].comment.fmt(width=max_cmt_width,color=True,nullrepl='-'),
-				amt=e['amt'].fmt('4.{}'.format(max(max_fp_len,3)),color=True),
-				age=self.age_disp(mmid,age_fmt) if show_age and hasattr(mmid,'confs') else '-'
-				).rstrip())
-
-		return '\n'.join(out + ['\nTOTAL: {} {}'.format(self.total.hl(color=True),g.dcoin)])
+			await _set_dates(self.rpc,[o for o in mmids if hasattr(o,'confs')])
+
+		def gen_output():
+
+			if self.proto.chain_name != 'mainnet':
+				yield 'Chain: '+green(self.proto.chain_name.upper())
+
+			yield fs.format(
+					mid=MMGenID.fmtc('MMGenID',width=max_mmid_len),
+					addr=(CoinAddr.fmtc('ADDRESS',width=addr_width) if showbtcaddrs else None),
+					cmt=TwComment.fmtc('COMMENT',width=max_cmt_width+1),
+					amt='BALANCE'.ljust(max_fp_len+4),
+					age=age_fmt.upper(),
+				).rstrip()
+
+			al_id_save = None
+			for mmid in mmids:
+				if mmid.type == 'mmgen':
+					if al_id_save and al_id_save != mmid.obj.al_id:
+						yield ''
+					al_id_save = mmid.obj.al_id
+					mmid_disp = mmid
+				else:
+					if al_id_save:
+						yield ''
+						al_id_save = None
+					mmid_disp = 'Non-MMGen'
+				e = self[mmid]
+				yield fs.format(
+					mid=MMGenID.fmtc(mmid_disp,width=max_mmid_len,color=True),
+					addr=(e['addr'].fmt(color=True,width=addr_width) if showbtcaddrs else None),
+					cmt=e['lbl'].comment.fmt(width=max_cmt_width,color=True,nullrepl='-'),
+					amt=e['amt'].fmt('4.{}'.format(max(max_fp_len,3)),color=True),
+					age=self.age_disp(mmid,age_fmt) if show_age and hasattr(mmid,'confs') else '-'
+					).rstrip()
+
+			yield '\nTOTAL: {} {}'.format(self.total.hl(color=True),self.proto.dcoin)
+
+		return '\n'.join(gen_output())
 
 class TrackingWallet(MMGenObject,metaclass=aInitMeta):
 
@@ -621,10 +683,10 @@ class TrackingWallet(MMGenObject,metaclass=aInitMeta):
 	aggressive_sync = False
 	importing = False
 
-	def __new__(cls,*args,**kwargs):
-		return MMGenObject.__new__(altcoin_subclass(cls,'tw','TrackingWallet'))
+	def __new__(cls,proto,*args,**kwargs):
+		return MMGenObject.__new__(altcoin_subclass(cls,proto,'tw'))
 
-	async def __ainit__(self,mode='r'):
+	async def __ainit__(self,proto,mode='r',token_addr=None):
 
 		assert mode in ('r','w','i'), "{!r}: wallet mode must be 'r','w' or 'i'".format(mode)
 		if mode == 'i':
@@ -634,33 +696,35 @@ class TrackingWallet(MMGenObject,metaclass=aInitMeta):
 		if g.debug:
 			print_stack_trace('TW INIT {!r} {!r}'.format(mode,self))
 
+		self.rpc = await rpc_init(proto) # TODO: create on demand - only certain ops require RPC
+		self.proto = proto
 		self.mode = mode
-		self.desc = self.base_desc = f'{g.proto.name} tracking wallet'
+		self.desc = self.base_desc = f'{self.proto.name} tracking wallet'
 
 		if self.use_tw_file:
 			self.init_from_wallet_file()
 		else:
 			self.init_empty()
 
-		if self.data['coin'] != g.coin:
+		if self.data['coin'] != self.proto.coin: # TODO remove?
 			m = 'Tracking wallet coin ({}) does not match current coin ({})!'
-			raise WalletFileError(m.format(self.data['coin'],g.coin))
+			raise WalletFileError(m.format(self.data['coin'],self.proto.coin))
 
 		self.conv_types(self.data[self.data_key])
 		self.cur_balances = {} # cache balances to prevent repeated lookups per program invocation
 
 	def init_empty(self):
-		self.data = { 'coin': g.coin, 'addresses': {} }
+		self.data = { 'coin': self.proto.coin, 'addresses': {} }
 
 	def init_from_wallet_file(self):
-
 		tw_dir = (
-			os.path.join(g.data_dir,g.proto.data_subdir) if g.coin == 'BTC' else
+			os.path.join(g.data_dir) if self.proto.coin == 'BTC' else
 			os.path.join(
 				g.data_dir_root,
 				'altcoins',
-				g.coin.lower(),
-				g.proto.data_subdir) )
+				self.proto.coin.lower(),
+				('' if self.proto.network == 'mainnet' else 'testnet')
+			))
 		self.tw_fn = os.path.join(tw_dir,'tracking-wallet.json')
 
 		check_or_create_dir(tw_dir)
@@ -712,11 +776,10 @@ class TrackingWallet(MMGenObject,metaclass=aInitMeta):
 	def upgrade_wallet_maybe(self):
 		pass
 
-	@staticmethod
-	def conv_types(ad):
+	def conv_types(self,ad):
 		for k,v in ad.items():
 			if k not in ('params','coin'):
-				v['mmid'] = TwMMGenID(v['mmid'],on_fail='raise')
+				v['mmid'] = TwMMGenID(self.proto,v['mmid'],on_fail='raise')
 				v['comment'] = TwComment(v['comment'],on_fail='raise')
 
 	@property
@@ -737,11 +800,11 @@ class TrackingWallet(MMGenObject,metaclass=aInitMeta):
 
 	def get_cached_balance(self,addr,session_cache,data_root):
 		if addr in session_cache:
-			return g.proto.coin_amt(session_cache[addr])
+			return self.proto.coin_amt(session_cache[addr])
 		if not g.use_cached_balances:
 			return None
 		if addr in data_root and 'balance' in data_root[addr]:
-			return g.proto.coin_amt(data_root[addr]['balance'])
+			return self.proto.coin_amt(data_root[addr]['balance'])
 
 	async def get_balance(self,addr,force_rpc=False):
 		ret = None if force_rpc else self.get_cached_balance(addr,self.cur_balances,self.data_root)
@@ -768,11 +831,11 @@ class TrackingWallet(MMGenObject,metaclass=aInitMeta):
 
 	@write_mode
 	async def import_address(self,addr,label,rescan):
-		return await g.rpc.call('importaddress',addr,label,rescan,timeout=(False,3600)[rescan])
+		return await self.rpc.call('importaddress',addr,label,rescan,timeout=(False,3600)[rescan])
 
 	@write_mode
 	def batch_import_address(self,arg_list):
-		return g.rpc.batch_call('importaddress',arg_list)
+		return self.rpc.batch_call('importaddress',arg_list)
 
 	def force_write(self):
 		mode_save = self.mode
@@ -805,13 +868,13 @@ class TrackingWallet(MMGenObject,metaclass=aInitMeta):
 			msg('Data is unchanged\n')
 
 	async def is_in_wallet(self,addr):
-		return addr in (await TwAddrList([],0,True,True,True,wallet=self)).coinaddr_list()
+		return addr in (await TwAddrList(self.proto,[],0,True,True,True,wallet=self)).coinaddr_list()
 
 	@write_mode
 	async def set_label(self,coinaddr,lbl):
 		# bitcoin-abc 'setlabel' RPC is broken, so use old 'importaddress' method to set label
 		# broken behavior: new label is set OK, but old label gets attached to another address
-		if 'label_api' in g.rpc.caps and g.coin != 'BCH':
+		if 'label_api' in self.rpc.caps and self.proto.coin != 'BCH':
 			args = ('setlabel',coinaddr,lbl)
 		else:
 			# NOTE: this works because importaddress() removes the old account before
@@ -820,7 +883,7 @@ class TrackingWallet(MMGenObject,metaclass=aInitMeta):
 			args = ('importaddress',coinaddr,lbl,False)
 
 		try:
-			return await g.rpc.call(*args)
+			return await self.rpc.call(*args)
 		except Exception as e:
 			rmsg(e.args[0])
 			return False
@@ -828,19 +891,18 @@ class TrackingWallet(MMGenObject,metaclass=aInitMeta):
 	# returns on failure
 	@write_mode
 	async def add_label(self,arg1,label='',addr=None,silent=False,on_fail='return'):
-		from .tx import is_mmgen_id,is_coin_addr
 		mmaddr,coinaddr = None,None
-		if is_coin_addr(addr or arg1):
-			coinaddr = CoinAddr(addr or arg1,on_fail='return')
-		if is_mmgen_id(arg1):
-			mmaddr = TwMMGenID(arg1)
+		if is_coin_addr(self.proto,addr or arg1):
+			coinaddr = CoinAddr(self.proto,addr or arg1,on_fail='return')
+		if is_mmgen_id(self.proto,arg1):
+			mmaddr = TwMMGenID(self.proto,arg1)
 
 		if mmaddr and not coinaddr:
 			from .addr import TwAddrData
-			coinaddr = (await TwAddrData()).mmaddr2coinaddr(mmaddr)
+			coinaddr = (await TwAddrData(self.proto)).mmaddr2coinaddr(mmaddr)
 
 		try:
-			if not is_mmgen_id(arg1):
+			if not is_mmgen_id(self.proto,arg1):
 				assert coinaddr,"Invalid coin address for this chain: {}".format(arg1)
 			assert coinaddr,"{pn} address '{ma}' not found in tracking wallet"
 			assert await self.is_in_wallet(coinaddr),"Address '{ca}' not found in tracking wallet"
@@ -852,18 +914,18 @@ class TrackingWallet(MMGenObject,metaclass=aInitMeta):
 		# Do reverse lookup, so that MMGen addr will not be marked as non-MMGen.
 		if not mmaddr:
 			from .addr import TwAddrData
-			mmaddr = (await TwAddrData()).coinaddr2mmaddr(coinaddr)
+			mmaddr = (await TwAddrData(proto=self.proto)).coinaddr2mmaddr(coinaddr)
 
 		if not mmaddr:
-			mmaddr = '{}:{}'.format(g.proto.base_coin.lower(),coinaddr)
+			mmaddr = '{}:{}'.format(self.proto.base_coin.lower(),coinaddr)
 
-		mmaddr = TwMMGenID(mmaddr)
+		mmaddr = TwMMGenID(self.proto,mmaddr)
 
 		cmt = TwComment(label,on_fail=on_fail)
 		if cmt in (False,None):
 			return False
 
-		lbl = TwLabel(mmaddr + ('',' '+cmt)[bool(cmt)],on_fail=on_fail)
+		lbl = TwLabel(self.proto,mmaddr + ('',' '+cmt)[bool(cmt)],on_fail=on_fail)
 
 		if await self.set_label(coinaddr,lbl) == False:
 			if not silent:
@@ -871,7 +933,7 @@ class TrackingWallet(MMGenObject,metaclass=aInitMeta):
 			return False
 		else:
 			m = mmaddr.type.replace('mmg','MMG')
-			a = mmaddr.replace(g.proto.base_coin.lower()+':','')
+			a = mmaddr.replace(self.proto.base_coin.lower()+':','')
 			s = '{} address {} in tracking wallet'.format(m,a)
 			if label: msg("Added label '{}' to {}".format(label,s))
 			else:     msg('Removed label from {}'.format(s))
@@ -883,32 +945,34 @@ class TrackingWallet(MMGenObject,metaclass=aInitMeta):
 
 	@write_mode
 	async def remove_address(self,addr):
-		raise NotImplementedError('address removal not implemented for coin {}'.format(g.coin))
+		raise NotImplementedError('address removal not implemented for coin {}'.format(self.proto.coin))
 
 class TwGetBalance(MMGenObject,metaclass=aInitMeta):
 
-	fs = '{w:13} {u:<16} {p:<16} {c}\n'
+	fs = '{w:13} {u:<16} {p:<16} {c}'
 
-	def __new__(cls,*args,**kwargs):
-		return MMGenObject.__new__(altcoin_subclass(cls,'tw','TwGetBalance'))
+	def __new__(cls,proto,*args,**kwargs):
+		return MMGenObject.__new__(altcoin_subclass(cls,proto,'tw'))
 
-	async def __ainit__(self,minconf,quiet):
+	async def __ainit__(self,proto,minconf,quiet):
 
 		self.minconf = minconf
 		self.quiet = quiet
-		self.data = {k:[g.proto.coin_amt('0')] * 4 for k in ('TOTAL','Non-MMGen','Non-wallet')}
+		self.data = {k:[proto.coin_amt('0')] * 4 for k in ('TOTAL','Non-MMGen','Non-wallet')}
+		self.rpc = await rpc_init(proto)
+		self.proto = proto
 		await self.create_data()
 
 	async def create_data(self):
 		# 0: unconfirmed, 1: below minconf, 2: confirmed, 3: spendable (privkey in wallet)
-		lbl_id = ('account','label')['label_api' in g.rpc.caps]
-		for d in await g.rpc.call('listunspent',0):
-			lbl = get_tw_label(d[lbl_id])
+		lbl_id = ('account','label')['label_api' in self.rpc.caps]
+		for d in await self.rpc.call('listunspent',0):
+			lbl = get_tw_label(self.proto,d[lbl_id])
 			if lbl:
 				if lbl.mmid.type == 'mmgen':
 					key = lbl.mmid.obj.sid
 					if key not in self.data:
-						self.data[key] = [g.proto.coin_amt('0')] * 4
+						self.data[key] = [self.proto.coin_amt('0')] * 4
 				else:
 					key = 'Non-MMGen'
 			else:
@@ -927,22 +991,31 @@ class TwGetBalance(MMGenObject,metaclass=aInitMeta):
 				self.data[key][3] += d['amount']
 
 	def format(self):
-		if self.quiet:
-			o = str(self.data['TOTAL'][2] if self.data else 0) + '\n'
-		else:
-			o = self.fs.format( w='Wallet',
-								u=' Unconfirmed',
-								p=' <{} confirms'.format(self.minconf),
-								c=' >={} confirms'.format(self.minconf))
-			for key in sorted(self.data):
-				if not any(self.data[key]): continue
-				o += self.fs.format(**dict(zip(
-							('w','u','p','c'),
-							[key+':'] + [a.fmt(color=True,suf=' '+g.dcoin) for a in self.data[key]]
-							)))
-
-		for key,vals in list(self.data.items()):
-			if key == 'TOTAL': continue
-			if vals[3]:
-				o += red('Warning: this wallet contains PRIVATE KEYS for {} outputs!\n'.format(key))
-		return o.rstrip()
+		def gen_output():
+			if self.proto.chain_name != 'mainnet':
+				yield 'Chain: ' + green(self.proto.chain_name.upper())
+
+			if self.quiet:
+				yield str(self.data['TOTAL'][2] if self.data else 0)
+			else:
+				yield self.fs.format(
+					w = 'Wallet',
+					u = ' Unconfirmed',
+					p = f' <{self.minconf} confirms',
+					c = f' >={self.minconf} confirms' )
+
+				for key in sorted(self.data):
+					if not any(self.data[key]):
+						continue
+					yield self.fs.format(**dict(zip(
+						('w','u','p','c'),
+						[key+':'] + [a.fmt(color=True,suf=' '+self.proto.dcoin) for a in self.data[key]]
+						)))
+
+			for key,vals in list(self.data.items()):
+				if key == 'TOTAL':
+					continue
+				if vals[3]:
+					yield red(f'Warning: this wallet contains PRIVATE KEYS for {key} outputs!')
+
+		return '\n'.join(gen_output()).rstrip()

+ 1290 - 1223
mmgen/tx.py

@@ -60,9 +60,9 @@ def strfmt_locktime(num,terse=False):
 	elif num > 0:
 		return '{}{}'.format(('block height ','')[terse],num)
 	else:
-		die(2,"'{}': invalid locktime value!".format(num))
+		die(2,"'{}': invalid nLockTime value!".format(num))
 
-def mmaddr2coinaddr(mmaddr,ad_w,ad_f):
+def mmaddr2coinaddr(mmaddr,ad_w,ad_f,proto):
 
 	# assume mmaddr has already been checked
 	coin_addr = ad_w.mmaddr2coinaddr(mmaddr)
@@ -79,42 +79,27 @@ def mmaddr2coinaddr(mmaddr,ad_w,ad_f):
 		else:
 			die(2,wmsg('addr_not_found_no_addrfile').format(mmaddr))
 
-	return CoinAddr(coin_addr)
-
-def segwit_is_active(exit_on_error=False):
-	d = g.rpc.cached['blockchaininfo']
-	if d['chain'] == 'regtest':
-		return True
-	if (    'bip9_softforks' in d
-			and 'segwit' in d['bip9_softforks']
-			and d['bip9_softforks']['segwit']['status'] == 'active'):
-		return True
-	if g.test_suite:
-		return True
-	if exit_on_error:
-		die(2,'Segwit not active on this chain.  Exiting')
-	else:
-		return False
+	return CoinAddr(proto,coin_addr)
 
-def addr2pubhash(addr):
-	ap = g.proto.parse_addr(addr)
+def addr2pubhash(proto,addr):
+	ap = proto.parse_addr(addr)
 	assert ap,'coin address {!r} could not be parsed'.format(addr)
 	return ap.bytes.hex()
 
-def addr2scriptPubKey(addr):
+def addr2scriptPubKey(proto,addr):
 	return {
-		'p2pkh': '76a914' + addr2pubhash(addr) + '88ac',
-		'p2sh':  'a914' + addr2pubhash(addr) + '87',
-		'bech32': g.proto.witness_vernum_hex + '14' + addr2pubhash(addr)
+		'p2pkh': '76a914' + addr2pubhash(proto,addr) + '88ac',
+		'p2sh':  'a914' + addr2pubhash(proto,addr) + '87',
+		'bech32': proto.witness_vernum_hex + '14' + addr2pubhash(proto,addr)
 	}[addr.addr_fmt]
 
-def scriptPubKey2addr(s):
+def scriptPubKey2addr(proto,s):
 	if len(s) == 50 and s[:6] == '76a914' and s[-4:] == '88ac':
-		return g.proto.pubhash2addr(s[6:-4],p2sh=False),'p2pkh'
+		return proto.pubhash2addr(s[6:-4],p2sh=False),'p2pkh'
 	elif len(s) == 46 and s[:4] == 'a914' and s[-2:] == '87':
-		return g.proto.pubhash2addr(s[4:-2],p2sh=True),'p2sh'
-	elif len(s) == 44 and s[:4] == g.proto.witness_vernum_hex + '14':
-		return g.proto.pubhash2bech32addr(s[4:]),'bech32'
+		return proto.pubhash2addr(s[4:-2],p2sh=True),'p2sh'
+	elif len(s) == 44 and s[:4] == proto.witness_vernum_hex + '14':
+		return proto.pubhash2bech32addr(s[4:]),'bech32'
 	else:
 		raise NotImplementedError('Unknown scriptPubKey ({})'.format(s))
 
@@ -123,7 +108,7 @@ class DeserializedTX(dict,MMGenObject):
 	Parse a serialized Bitcoin transaction
 	For checking purposes, additionally reconstructs the raw (unsigned) tx hex from signed tx hex
 	"""
-	def __init__(self,txhex):
+	def __init__(self,proto,txhex):
 
 		def bytes2int(bytes_le):
 			if bytes_le[-1] & 0x80: # sign bit is set
@@ -131,7 +116,7 @@ class DeserializedTX(dict,MMGenObject):
 			return int(bytes_le[::-1].hex(),16)
 
 		def bytes2coin_amt(bytes_le):
-			return g.proto.coin_amt(bytes2int(bytes_le) * g.proto.coin_amt.min_coin_unit)
+			return proto.coin_amt(bytes2int(bytes_le) * proto.coin_amt.min_coin_unit)
 
 		def bshift(n,skip=False,sub_null=False):
 			ret = tx[self.idx:self.idx+n]
@@ -193,7 +178,7 @@ class DeserializedTX(dict,MMGenObject):
 		} for i in range(d['num_txouts'])])
 
 		for o in d['txouts']:
-			o['address'] = scriptPubKey2addr(o['scriptPubKey'])[0]
+			o['address'] = scriptPubKey2addr(proto,o['scriptPubKey'])[0]
 
 		if has_witness:
 			# https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki
@@ -223,1337 +208,1419 @@ class DeserializedTX(dict,MMGenObject):
 
 class MMGenTxIO(MMGenListItem):
 	vout     = ListItemAttr(int,typeconv=False)
-	amt      = ImmutableAttr(lambda val:g.proto.coin_amt(val),typeconv=False)
+	amt      = ImmutableAttr(None)
 	label    = ListItemAttr('TwComment',reassign_ok=True)
-	mmid     = ListItemAttr('MMGenID')
-	addr     = ImmutableAttr('CoinAddr')
-	confs    = ListItemAttr(int,typeconv=True) # confs of type long exist in the wild, so convert
+	mmid     = ListItemAttr('MMGenID',include_proto=True)
+	addr     = ImmutableAttr('CoinAddr',include_proto=True)
+	confs    = ListItemAttr(int) # confs of type long exist in the wild, so convert
 	txid     = ListItemAttr('CoinTxID')
 	have_wif = ListItemAttr(bool,typeconv=False,delete_ok=True)
 
+	invalid_attrs = {'proto','tw_copy_attrs'}
+
+	def __init__(self,proto,**kwargs):
+		self.__dict__['proto'] = proto
+		MMGenListItem.__init__(self,**kwargs)
+
+	class conv_funcs:
+		def amt(self,value):
+			return self.proto.coin_amt(value)
+
 class MMGenTxInput(MMGenTxIO):
 	scriptPubKey = ListItemAttr('HexStr')
 	sequence     = ListItemAttr(int,typeconv=False)
-	# required by copy_inputs_from_tw()
-	copy_attrs = { 'scriptPubKey','vout','amt','label','mmid','addr','confs','txid' }
+	tw_copy_attrs = { 'scriptPubKey','vout','amt','label','mmid','addr','confs','txid' }
 
 class MMGenTxOutput(MMGenTxIO):
 	is_chg = ListItemAttr(bool,typeconv=False)
 
-class MMGenTxInputList(list,MMGenObject):
+class MMGenTxIOList(MMGenObject):
+
+	def __init__(self,parent,data=None):
+		self.parent = parent
+		if data:
+			assert isinstance(data,list), 'MMGenTxIOList_check1'
+			self.data = data
+		else:
+			self.data = list()
+
+	def __getitem__(self,val):     return self.data.__getitem__(val)
+	def __setitem__(self,key,val): return self.data.__setitem__(key,val)
+	def __delitem__(self,val):     return self.data.__delitem__(val)
+	def __contains__(self,val):    return self.data.__contains__(val)
+	def __iter__(self):            return self.data.__iter__()
+	def __len__(self):             return self.data.__len__()
+	def __add__(self,val):         return self.data.__add__(val)
+	def __eq__(self,val):          return self.data.__eq__(val)
+	def append(self,val):          return self.data.append(val)
+	def sort(self,*args,**kwargs): return self.data.sort(*args,**kwargs)
+
+class MMGenTxInputList(MMGenTxIOList):
 
 	desc = 'transaction inputs'
 	member_type = 'MMGenTxInput'
 
-	def convert_coin(self,verbose=False):
-		if verbose:
-			msg(f'{self.desc}:')
-		for i in self:
-			setattr(i,'amt',g.proto.coin_amt(i.amt))
-
-	def check_coin_mismatch(self):
-		for i in self:
-			if type(i.amt) != g.proto.coin_amt:
-				die(2,f'Coin mismatch in transaction: amount {i.amt} not of type {g.proto.coin_amt}!')
+#	def convert_coin(self,verbose=False):
+#		if verbose:
+#			msg(f'{self.desc}:')
+#		for i in self:
+#			i.amt = self.parent.proto.coin_amt(i.amt)
 
 	# Lexicographical Indexing of Transaction Inputs and Outputs
 	# https://github.com/bitcoin/bips/blob/master/bip-0069.mediawiki
 	def sort_bip69(self):
-		from struct import pack
-		self.sort(key=lambda a: bytes.fromhex(a.txid) + pack('>i',a.vout))
+		def sort_func(a):
+			return (
+				bytes.fromhex(a.txid)
+				+ int.to_bytes(a.vout,4,'big') )
+		self.sort(key=sort_func)
 
-class MMGenTxOutputList(MMGenTxInputList):
+class MMGenTxOutputList(MMGenTxIOList):
 
 	desc = 'transaction outputs'
 	member_type = 'MMGenTxOutput'
 
 	def sort_bip69(self):
-		from struct import pack
-		self.sort(key=lambda a: pack('>q',a.amt.toSatoshi()) + bytes.fromhex(addr2scriptPubKey(a.addr)))
-
-class MMGenTX(MMGenObject):
-
-	def __new__(cls,*args,**kwargs):
-		return MMGenObject.__new__(altcoin_subclass(cls,'tx','MMGenTX'))
-
-	ext      = 'rawtx'
-	raw_ext  = 'rawtx'
-	sig_ext  = 'sigtx'
-	txid_ext = 'txid'
-	desc     = 'transaction'
-	hexdata_type = 'hex'
-	fee_fail_fs = 'Network fee estimation for {c} confirmations failed ({t})'
-	no_chg_msg = 'Warning: Change address will be deleted as transaction produces no change'
-	rel_fee_desc = 'satoshis per byte'
-	rel_fee_disp = 'satoshis per byte'
-	txview_hdr_fs = 'TRANSACTION DATA\n\nID={i} ({a} {c}) UTC={t} RBF={r} Sig={s} Locktime={l}\n'
-	txview_hdr_fs_short = 'TX {i} ({a} {c}) UTC={t} RBF={r} Sig={s} Locktime={l}\n'
-	txview_ftr_fs = 'Total input:  {i} {d}\nTotal output: {o} {d}\nTX fee:       {a} {c}{r}\n'
-	txview_ftr_fs_short = 'In {i} {d} - Out {o} {d}\nFee {a} {c}{r}\n'
-	usr_fee_prompt = 'Enter transaction fee: '
-	fee_is_approximate = False
-	fn_fee_unit = 'satoshi'
-	view_sort_orders = ('addr','raw')
-	dfl_view_sort_order = 'addr'
-
-	msg_wallet_low_coin = 'Wallet has insufficient funds for this transaction ({} {} needed)'
-	msg_low_coin = 'Selected outputs insufficient to fund this transaction ({} {} needed)'
-	msg_no_change_output = fmt("""
-		ERROR: No change address specified.  If you wish to create a transaction with
-		only one output, specify a single output address with no {} amount
-	""").strip()
-	msg_non_mmgen_inputs = fmt(f"""
-		NOTE: This transaction includes non-{g.proj_name} inputs, which makes the signing
-		process more complicated.  When signing the transaction, keys for non-{g.proj_name}
-		inputs must be supplied using the '--keys-from-file' option.  The key file
-		must contain exactly one key per line.
-		Selected non-{g.proj_name} inputs: {{}}
-	""").strip()
-
-	def __init__(self,filename=None,metadata_only=False,caller=None,quiet_open=False,data=None,tw=None):
-		if data:
-			assert type(data) is dict, type(data)
-			self.__dict__ = data
-			return
-		self.inputs      = MMGenTxInputList()
-		self.outputs     = MMGenTxOutputList()
-		self.send_amt    = g.proto.coin_amt('0')  # total amt minus change
-		self.fee         = g.proto.coin_amt('0')
-		self.hex         = ''                     # raw serialized hex transaction
-		self.label       = MMGenTxLabel('')
-		self.txid        = ''
-		self.coin_txid   = ''
-		self.timestamp   = ''
-		self.blockcount  = 0
-		self.chain       = None
-		self.coin        = None
-		self.dcoin       = None
-		self.caller      = caller
-		self.locktime    = None
-		self.tw          = tw
-
-		if filename:
-			from .txfile import MMGenTxFile
-			MMGenTxFile(self).parse(filename,metadata_only=metadata_only,quiet_open=quiet_open)
-			if metadata_only:
-				return
-			self.check_pubkey_scripts()
-			self.check_sigs() # marks the tx as signed
-
-		# repeat with sign and send, because coin daemon could be restarted
-		self.check_correct_chain()
-
-	def write_to_file(self,*args,**kwargs):
-		from .txfile import MMGenTxFile
-		MMGenTxFile(self).write(*args,**kwargs)
-
-	def check_correct_chain(self):
-		bad = self.chain and g.chain and self.chain != g.chain
-		if bad and hasattr(g.proto,'chain_name'):
-			bad = self.chain != g.proto.chain_name
-		if bad:
-			raise TransactionChainMismatch(f'Transaction is for {self.chain}, but current chain is {g.chain}!')
-
-	def add_output(self,coinaddr,amt,is_chg=None):
-		self.outputs.append(MMGenTxOutput(addr=coinaddr,amt=amt,is_chg=is_chg))
-
-	def get_chg_output_idx(self):
-		ch_ops = [x.is_chg for x in self.outputs]
-		try:
-			return ch_ops.index(True)
-		except ValueError:
-			return None
-
-	def update_output_amt(self,idx,amt):
-		o = self.outputs[idx].__dict__
-		o['amt'] = amt
-		self.outputs[idx] = MMGenTxOutput(**o)
-
-	def update_change_output(self,change_amt):
-		chg_idx = self.get_chg_output_idx()
-		if change_amt == 0:
-			msg(self.no_chg_msg)
-			self.del_output(chg_idx)
-		else:
-			self.update_output_amt(chg_idx,g.proto.coin_amt(change_amt))
-
-	def del_output(self,idx):
-		self.outputs.pop(idx)
-
-	def sum_outputs(self,exclude=None):
-		if exclude == None:
-			olist = self.outputs
-		else:
-			olist = self.outputs[:exclude] + self.outputs[exclude+1:]
-		if not olist:
-			return g.proto.coin_amt('0')
-		return g.proto.coin_amt(sum(e.amt for e in olist))
-
-	def add_mmaddrs_to_outputs(self,ad_w,ad_f):
-		a = [e.addr for e in self.outputs]
-		d = ad_w.make_reverse_dict(a)
-		if ad_f:
-			d.update(ad_f.make_reverse_dict(a))
-		for e in self.outputs:
-			if e.addr and e.addr in d:
-				e.mmid,f = d[e.addr]
-				if f:
-					e.label = f
-
-	def check_dup_addrs(self,io_str):
-		assert io_str in ('inputs','outputs')
-		addrs = [e.addr for e in getattr(self,io_str)]
-		if len(addrs) != len(set(addrs)):
-			die(2,f'{addrs}: duplicate address in transaction {io_str}')
-
-	def update_txid(self):
-		self.txid = MMGenTxID(make_chksum_6(bytes.fromhex(self.hex)).upper())
-
-	async def create_raw(self):
-		i = [{'txid':e.txid,'vout':e.vout} for e in self.inputs]
-		if self.inputs[0].sequence:
-			i[0]['sequence'] = self.inputs[0].sequence
-		o = {e.addr:e.amt for e in self.outputs}
-		self.hex = HexStr(await g.rpc.call('createrawtransaction',i,o))
-		self.update_txid()
-
-	def print_contract_addr(self):
-		pass
-
-	# returns true if comment added or changed
-	def add_comment(self,infile=None):
-		if infile:
-			self.label = MMGenTxLabel(get_data_from_file(infile,'transaction comment'))
-		else: # get comment from user, or edit existing comment
-			m = ('Add a comment to transaction?','Edit transaction comment?')[bool(self.label)]
-			if keypress_confirm(m,default_yes=False):
-				while True:
-					s = MMGenTxLabel(my_raw_input('Comment: ',insert_txt=self.label))
-					if s:
-						lbl_save = self.label
-						self.label = s
-						return (True,False)[lbl_save == self.label]
-					else:
-						msg('Invalid comment')
-			return False
-
-	def edit_comment(self):
-		return self.add_comment(self)
-
-	def get_fee(self):
-		return self.sum_inputs() - self.sum_outputs()
-
-	def has_segwit_inputs(self):
-		return any(i.mmid and i.mmid.mmtype in ('S','B') for i in self.inputs)
-
-	def compare_size_and_estimated_size(self,tx_decoded):
-		est_vsize = self.estimate_size()
-		d = tx_decoded
-		vsize = d['vsize'] if 'vsize' in d else d['size']
-		vmsg(f'\nVsize: {vsize} (true) {est_vsize} (estimated)')
-		ratio = float(est_vsize) / vsize
-		if not (0.95 < ratio < 1.05): # allow for 5% error
-			raise BadTxSizeEstimate(fmt(f"""
-				Estimated transaction vsize is {ratio:1.2f} times the true vsize
-				Your transaction fee estimates will be inaccurate
-				Please re-create and re-sign the transaction using the option --vsize-adj={1/ratio:1.2f}
-			""").strip())
-
-	# https://bitcoin.stackexchange.com/questions/1195/how-to-calculate-transaction-size-before-sending
-	# 180: uncompressed, 148: compressed
-	def estimate_size_old(self):
-		if not self.inputs or not self.outputs:
-			return None
-		return len(self.inputs)*180 + len(self.outputs)*34 + 10
-
-	# https://bitcoincore.org/en/segwit_wallet_dev/
-	# vsize: 3 times of the size with original serialization, plus the size with new
-	# serialization, divide the result by 4 and round up to the next integer.
-
-	# TODO: results differ slightly from actual transaction size
-	def estimate_size(self):
-		if not self.inputs or not self.outputs:
-			return None
-
-		sig_size = 72 # sig in DER format
-		pubkey_size_uncompressed = 65
-		pubkey_size_compressed = 33
-
-		def get_inputs_size():
-			# txid vout [scriptSig size (vInt)] scriptSig (<sig> <pubkey>) nSeq
-			isize_common = 32 + 4 + 1 + 4 # txid vout [scriptSig size] nSeq = 41
-			input_size = {
-				'L': isize_common + sig_size + pubkey_size_uncompressed, # = 180
-				'C': isize_common + sig_size + pubkey_size_compressed,   # = 148
-				'S': isize_common + 23,                                  # = 64
-				'B': isize_common + 0                                    # = 41
-			}
-			ret = sum(input_size[i.mmid.mmtype] for i in self.inputs if i.mmid)
-
-			# We have no way of knowing whether a non-MMGen addr is compressed or uncompressed until
-			# we see the key, so assume compressed for fee-estimation purposes.  If fee estimate is
-			# off by more than 5%, sign() aborts and user is instructed to use --vsize-adj option
-			return ret + sum(input_size['C'] for i in self.inputs if not i.mmid)
-
-		def get_outputs_size():
-			# output bytes = amt: 8, byte_count: 1+, pk_script
-			# pk_script bytes: p2pkh: 25, p2sh: 23, bech32: 22
-			return sum({'p2pkh':34,'p2sh':32,'bech32':31}[o.addr.addr_fmt] for o in self.outputs)
-
-		# https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki
-		# The witness is a serialization of all witness data of the transaction. Each txin is
-		# associated with a witness field. A witness field starts with a var_int to indicate the
-		# number of stack items for the txin. It is followed by stack items, with each item starts
-		# with a var_int to indicate the length. Witness data is NOT script.
-
-		# A non-witness program txin MUST be associated with an empty witness field, represented
-		# by a 0x00. If all txins are not witness program, a transaction's wtxid is equal to its txid.
-		def get_witness_size():
-			if not self.has_segwit_inputs():
-				return 0
-			wf_size = 1 + 1 + sig_size + 1 + pubkey_size_compressed # vInt vInt sig vInt pubkey = 108
-			return sum((1,wf_size)[bool(i.mmid) and i.mmid.mmtype in ('S','B')] for i in self.inputs)
-
-		isize = get_inputs_size()
-		osize = get_outputs_size()
-		wsize = get_witness_size()
-
-		# TODO: compute real varInt sizes instead of assuming 1 byte
-		# old serialization: [nVersion]              [vInt][txins][vInt][txouts]         [nLockTime]
-		old_size =           4                     + 1   + isize + 1  + osize          + 4
-		# marker = 0x00, flag = 0x01
-		# new serialization: [nVersion][marker][flag][vInt][txins][vInt][txouts][witness][nLockTime]
-		new_size =           4       + 1     + 1   + 1   + isize + 1  + osize + wsize  + 4 \
-				if wsize else old_size
-
-		ret = (old_size * 3 + new_size) // 4
-
-		dmsg('\nData from estimate_size():')
-		dmsg('  inputs size: {}, outputs size: {}, witness size: {}'.format(isize,osize,wsize))
-		dmsg('  size: {}, vsize: {}, old_size: {}'.format(new_size,ret,old_size))
-
-		return int(ret * float(opt.vsize_adj)) if hasattr(opt,'vsize_adj') and opt.vsize_adj else ret
-
-	# coin-specific fee routines
-	@property
-	def relay_fee(self):
-		kb_fee = g.proto.coin_amt(g.rpc.cached['networkinfo']['relayfee'])
-		ret = kb_fee * self.estimate_size() // 1024
-		vmsg('Relay fee: {} {c}/kB, for transaction: {} {c}'.format(kb_fee,ret,c=g.coin))
-		return ret
-
-	# convert absolute BTC fee to satoshis-per-byte using estimated size
-	def fee_abs2rel(self,abs_fee,to_unit=None):
-		unit = getattr(g.proto.coin_amt,to_unit or 'min_coin_unit')
-		return int(abs_fee // unit // self.estimate_size())
-
-	async def get_rel_fee_from_network(self):
-		try:
-			ret = await g.rpc.call('estimatesmartfee',opt.tx_confs,opt.fee_estimate_mode.upper())
-			fee_per_kb = ret['feerate'] if 'feerate' in ret else -2
-			fe_type = 'estimatesmartfee'
-		except:
-			args = () if g.coin=='BCH' and g.rpc.daemon_version >= 190100 else (opt.tx_confs,)
-			fee_per_kb = await g.rpc.call('estimatefee',*args)
-			fe_type = 'estimatefee'
-
-		return fee_per_kb,fe_type
-
-	# given tx size, rel fee and units, return absolute fee
-	def convert_fee_spec(self,tx_size,units,amt,unit):
-		self.usr_rel_fee = None # TODO
-		return g.proto.coin_amt(int(amt)*tx_size*getattr(g.proto.coin_amt,units[unit])) \
-			if tx_size else None
-
-	# given network fee estimate in BTC/kB, return absolute fee using estimated tx size
-	def fee_est2abs(self,fee_per_kb,fe_type=None):
-		tx_size = self.estimate_size()
-		f = fee_per_kb * opt.tx_fee_adj * tx_size / 1024
-		ret = g.proto.coin_amt(f,from_decimal=True)
-		if opt.verbose:
-			msg(fmt(f"""
-				{fe_type.upper()} fee for {opt.tx_confs} confirmations: {fee_per_kb} {g.coin}/kB
-				TX size (estimated): {tx_size} bytes
-				Fee adjustment factor: {opt.tx_fee_adj}
-				Absolute fee (fee_per_kb * adj_factor * tx_size / 1024): {ret} {g.coin}
-			""").strip())
-		return ret
-
-	def convert_and_check_fee(self,tx_fee,desc='Missing description'):
-		abs_fee = self.process_fee_spec(tx_fee,self.estimate_size())
-		if abs_fee == None: # we shouldn't be calling this method if tx size is unknown
-			raise ValueError(
-				f'{tx_fee}: cannot convert {self.rel_fee_desc} to {g.coin} because transaction size is unknown')
-		elif abs_fee == False:
-			msg(f'{tx_fee!r}: invalid TX fee (not a {g.coin} amount or {self.rel_fee_desc} specification)')
-			return False
-		elif abs_fee > g.proto.max_tx_fee:
-			msg(f'{abs_fee} {g.coin}: {desc} fee too large (maximum fee: {g.proto.max_tx_fee} {g.coin})')
-			return False
-		elif abs_fee < self.relay_fee:
-			msg(f'{abs_fee} {g.coin}: {desc} fee too small (less than relay fee of {self.relay_fee} {g.coin})')
-			return False
-		else:
-			return abs_fee
-
-	# non-coin-specific fee routines
-
-	# given tx size and absolute fee or fee spec, return absolute fee
-	# relative fee is N+<first letter of unit name>
-	def process_fee_spec(self,tx_fee,tx_size):
-		if g.proto.coin_amt(tx_fee,on_fail='silent'):
-			return g.proto.coin_amt(tx_fee)
-		else:
-			import re
-			units = {u[0]:u for u in g.proto.coin_amt.units}
-			pat = re.compile(r'([1-9][0-9]*)({})'.format('|'.join(units)))
-			if pat.match(tx_fee):
-				amt,unit = pat.match(tx_fee).groups()
-				return self.convert_fee_spec(tx_size,units,amt,unit)
-		return False
-
-	def get_usr_fee_interactive(self,tx_fee=None,desc='Starting'):
-		abs_fee = None
-		while True:
-			if tx_fee:
-				abs_fee = self.convert_and_check_fee(tx_fee,desc)
-			if abs_fee:
-				prompt = '{} TX fee{}: {}{} {} ({} {})\n'.format(
-						desc,
-						(f' (after {opt.tx_fee_adj}X adjustment)'
-							if opt.tx_fee_adj != 1 and desc.startswith('Network-estimated')
-								else ''),
-						('','≈')[self.fee_is_approximate],
-						abs_fee.hl(),
-						g.coin,
-						pink(str(self.fee_abs2rel(abs_fee))),
-						self.rel_fee_disp)
-				if opt.yes or keypress_confirm(prompt+'OK?',default_yes=True):
-					if opt.yes:
-						msg(prompt)
-					return abs_fee
-			tx_fee = my_raw_input(self.usr_fee_prompt)
-			desc = 'User-selected'
-
-	async def get_fee_from_user(self,have_estimate_fail=[]):
-
-		if opt.tx_fee:
-			desc = 'User-selected'
-			start_fee = opt.tx_fee
-		else:
-			desc = f'Network-estimated (mode: {opt.fee_estimate_mode.upper()})'
-			fee_per_kb,fe_type = await self.get_rel_fee_from_network()
-
-			if fee_per_kb < 0:
-				if not have_estimate_fail:
-					msg(self.fee_fail_fs.format(c=opt.tx_confs,t=fe_type))
-					have_estimate_fail.append(True)
-				start_fee = None
+		def sort_func(a):
+			return (
+				int.to_bytes(a.amt.toSatoshi(),8,'big')
+				+ bytes.fromhex(addr2scriptPubKey(self.parent.proto,a.addr)) )
+		self.sort(key=sort_func)
+
+class MMGenTX:
+
+	class Base(MMGenObject):
+		desc         = 'transaction'
+		hex          = ''                     # raw serialized hex transaction
+		label        = MMGenTxLabel('')
+		txid         = ''
+		coin_txid    = ''
+		timestamp    = ''
+		blockcount   = 0
+		coin         = None
+		dcoin        = None
+		locktime     = None
+		chain        = None
+		rel_fee_desc = 'satoshis per byte'
+		rel_fee_disp = 'satoshis per byte'
+		non_mmgen_inputs_msg = f"""
+			This transaction includes inputs with non-{g.proj_name} addresses.  When
+			signing the transaction, private keys for the addresses must be supplied using
+			the --keys-from-file option.  The key file must contain one key per line.
+			Please note that this transaction cannot be autosigned, as autosigning does
+			not support the use of key files.
+
+			Non-{g.proj_name} addresses found in inputs:
+			    {{}}
+		"""
+
+		def __new__(cls,*args,**kwargs):
+			"""
+			determine correct protocol and pass the proto to altcoin_subclass(), which returns the
+			transaction object
+			"""
+			assert args == (), f'MMGenTX.Base_chk1: only keyword args allowed in {cls.__name__} initializer'
+			if 'proto' in kwargs:
+				return MMGenObject.__new__(altcoin_subclass(cls,kwargs['proto'],'tx'))
+			elif 'data' in kwargs:
+				return MMGenObject.__new__(altcoin_subclass(cls,kwargs['data']['proto'],'tx'))
+			elif 'filename' in kwargs:
+				from .txfile import MMGenTxFile
+				tmp_tx = MMGenObject.__new__(cls)
+				MMGenTxFile(tmp_tx).parse(
+					infile        = kwargs['filename'],
+					quiet_open    = kwargs.get('quiet_open'),
+					metadata_only = True )
+				me = MMGenObject.__new__(altcoin_subclass(cls,tmp_tx.proto,'tx'))
+				me.proto = tmp_tx.proto
+				return me
+			elif cls.__name__ == 'Base' and args == () and kwargs == {}: # allow instantiation of empty Base()
+				return cls
 			else:
-				start_fee = self.fee_est2abs(fee_per_kb,fe_type)
-
-		return self.get_usr_fee_interactive(start_fee,desc=desc)
-
-	def delete_attrs(self,desc,attr):
-		for e in getattr(self,desc):
-			if hasattr(e,attr):
-				delattr(e,attr)
-
-	# inputs methods
-	def copy_inputs_from_tw(self,tw_unspent_data):
-		self.inputs = MMGenTxInputList()
-		for d in tw_unspent_data:
-			t = MMGenTxInput(**{attr:getattr(d,attr) for attr in d.__dict__ if attr in MMGenTxInput.copy_attrs})
-			if d.twmmid.type == 'mmgen':
-				t.mmid = d.twmmid # twmmid -> mmid
-			self.inputs.append(t)
-
-	def get_input_sids(self):
-		return set(e.mmid.sid for e in self.inputs if e.mmid)
-
-	def get_output_sids(self):
-		return set(e.mmid.sid for e in self.outputs if e.mmid)
-
-	def sum_inputs(self):
-		return sum(e.amt for e in self.inputs)
-
-	def add_timestamp(self):
-		self.timestamp = make_timestamp()
-
-	def get_hex_locktime(self):
-		return int(bytes.fromhex(self.hex[-8:])[::-1].hex(),16)
-
-	def set_hex_locktime(self,val):
-		assert isinstance(val,int),'locktime value not an integer'
-		self.hex = self.hex[:-8] + bytes.fromhex('{:08x}'.format(val))[::-1].hex()
-
-	def add_blockcount(self):
-		self.blockcount = g.rpc.blockcount
-
-	def get_non_mmaddrs(self,desc):
-		return {i.addr for i in getattr(self,desc) if not i.mmid}
-
-	def mark_raw(self):
-		self.desc = 'transaction'
-		self.ext = self.raw_ext
-
-	def mark_signed(self): # called ONLY by check_sigs()
-		self.desc = 'signed transaction'
-		self.ext = self.sig_ext
-
-	def marked_signed(self,color=False):
-		ret = self.desc == 'signed transaction'
-		return (red,green)[ret](str(ret)) if color else ret
-
-	# check that a malicious, compromised or malfunctioning coin daemon hasn't altered hex tx data:
-	# does not check witness or signature data
-	def check_hex_tx_matches_mmgen_tx(self,deserial_tx):
-		m = 'A malicious or malfunctioning coin daemon or other program may have altered your data!'
-
-		lt = deserial_tx['lock_time']
-		if lt != int(self.locktime or 0):
-			m2 = 'Transaction hex locktime ({}) does not match MMGen transaction locktime ({})\n{}'
-			raise TxHexMismatch(m2.format(lt,self.locktime,m))
-
-		def check_equal(desc,hexio,mmio):
-			if mmio != hexio:
-				msg('\nMMGen {}:\n{}'.format(desc,pp_fmt(mmio)))
-				msg('Hex {}:\n{}'.format(desc,pp_fmt(hexio)))
-				m2 = '{} in hex transaction data from coin daemon do not match those in MMGen transaction!\n'
-				raise TxHexMismatch((m2+m).format(desc.capitalize()))
-
-		seq_hex   = [int(i['nSeq'],16) for i in deserial_tx['txins']]
-		seq_mmgen = [i.sequence or g.max_int for i in self.inputs]
-		check_equal('sequence numbers',seq_hex,seq_mmgen)
-
-		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)
-
-		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)
-
-		uh = deserial_tx['unsigned_hex']
-		if str(self.txid) != make_chksum_6(bytes.fromhex(uh)).upper():
-			raise TxHexMismatch('MMGen TxID ({}) does not match hex transaction data!\n{}'.format(self.txid,m))
-
-	def check_pubkey_scripts(self):
-		for n,i in enumerate(self.inputs,1):
-			addr,fmt = scriptPubKey2addr(i.scriptPubKey)
-			if i.addr != addr:
-				if fmt != i.addr.addr_fmt:
-					m = 'Address format of scriptPubKey ({}) does not match that of address ({}) in input #{}'
-					msg(m.format(fmt,i.addr.addr_fmt,n))
-				m = 'ERROR: Address and scriptPubKey of transaction input #{} do not match!'
-				die(3,(m+'\n  {:23}{}'*3).format(n, 'address:',i.addr,
-													'scriptPubKey:',i.scriptPubKey,
-													'scriptPubKey->address:',addr ))
-
-	# check signature and witness data
-	def check_sigs(self,deserial_tx=None): # return False if no sigs, raise exception on error
-		txins = (deserial_tx or DeserializedTX(self.hex))['txins']
-		has_ss = any(ti['scriptSig'] for ti in txins)
-		has_witness = any('witness' in ti and ti['witness'] for ti in txins)
-		if not (has_ss or has_witness):
-			return False
-		fs = "Hex TX has {} scriptSig but input is of type '{}'!"
-		for n in range(len(txins)):
-			ti,mmti = txins[n],self.inputs[n]
-			if ti['scriptSig'] == '' or ( len(ti['scriptSig']) == 46 and # native P2WPKH or P2SH-P2WPKH
-					ti['scriptSig'][:6] == '16' + g.proto.witness_vernum_hex + '14' ):
-				assert 'witness' in ti, 'missing witness'
-				assert type(ti['witness']) == list and len(ti['witness']) == 2, 'malformed witness'
-				assert len(ti['witness'][1]) == 66, 'incorrect witness pubkey length'
-				assert mmti.mmid, fs.format('witness-type','non-MMGen')
-				assert mmti.mmid.mmtype == ('S','B')[ti['scriptSig']==''],(
-							fs.format('witness-type',mmti.mmid.mmtype))
-			else: # non-witness
-				if mmti.mmid:
-					assert mmti.mmid.mmtype not in ('S','B'), fs.format('signature in',mmti.mmid.mmtype)
-				assert not 'witness' in ti, 'non-witness input has witness'
-				# sig_size 72 (DER format), pubkey_size 'compressed':33, 'uncompressed':65
-				assert (200 < len(ti['scriptSig']) < 300), 'malformed scriptSig' # VERY rough check
-		self.mark_signed()
-		return True
-
-	def has_segwit_outputs(self):
-		return any(o.mmid and o.mmid.mmtype in ('S','B') for o in self.outputs)
-
-	async def get_status(self,status=False):
-
-		class r(object):
-			pass
-
-		async def is_in_wallet():
-			try: ret = await g.rpc.call('gettransaction',self.coin_txid)
-			except: return False
-			if 'confirmations' in ret and ret['confirmations'] > 0:
-				r.confs = ret['confirmations']
-				return True
+				raise ValueError(
+					f"MMGenTX.Base: {cls.__name__} must be instantiated with 'proto','data' or 'filename' keyword")
+
+		def __init__(self):
+			self.inputs   = MMGenTxInputList(self)
+			self.outputs  = MMGenTxOutputList(self)
+			self.name     = type(self).__name__
+
+		@property
+		def coin(self):
+			return self.proto.coin
+
+		@property
+		def dcoin(self):
+			return self.proto.dcoin
+
+		def check_correct_chain(self):
+			if hasattr(self,'rpc'):
+				if self.chain != self.rpc.chain:
+					raise TransactionChainMismatch(
+						f'Transaction is for {self.chain}, but coin daemon chain is {self.rpc.chain}!')
+
+		def sum_inputs(self):
+			return sum(e.amt for e in self.inputs)
+
+		def sum_outputs(self,exclude=None):
+			if exclude == None:
+				olist = self.outputs
 			else:
-				return False
-
-		async def is_in_utxos():
-			try: return 'txid' in await g.rpc.call('getrawtransaction',self.coin_txid,True)
-			except: return False
-
-		async def is_in_mempool():
-			try: return 'height' in await g.rpc.call('getmempoolentry',self.coin_txid)
-			except: return False
+				olist = self.outputs[:exclude] + self.outputs[exclude+1:]
+			if not olist:
+				return self.proto.coin_amt('0')
+			return self.proto.coin_amt(sum(e.amt for e in olist))
+
+		def has_segwit_inputs(self):
+			return any(i.mmid and i.mmid.mmtype in ('S','B') for i in self.inputs)
+
+		def has_segwit_outputs(self):
+			return any(o.mmid and o.mmid.mmtype in ('S','B') for o in self.outputs)
+
+		# https://bitcoin.stackexchange.com/questions/1195/how-to-calculate-transaction-size-before-sending
+		# 180: uncompressed, 148: compressed
+		def estimate_size_old(self):
+			if not self.inputs or not self.outputs:
+				return None
+			return len(self.inputs)*180 + len(self.outputs)*34 + 10
+
+		# https://bitcoincore.org/en/segwit_wallet_dev/
+		# vsize: 3 times of the size with original serialization, plus the size with new
+		# serialization, divide the result by 4 and round up to the next integer.
+
+		# TODO: results differ slightly from actual transaction size
+		def estimate_size(self):
+			if not self.inputs or not self.outputs:
+				return None
+
+			sig_size = 72 # sig in DER format
+			pubkey_size_uncompressed = 65
+			pubkey_size_compressed = 33
+
+			def get_inputs_size():
+				# txid vout [scriptSig size (vInt)] scriptSig (<sig> <pubkey>) nSeq
+				isize_common = 32 + 4 + 1 + 4 # txid vout [scriptSig size] nSeq = 41
+				input_size = {
+					'L': isize_common + sig_size + pubkey_size_uncompressed, # = 180
+					'C': isize_common + sig_size + pubkey_size_compressed,   # = 148
+					'S': isize_common + 23,                                  # = 64
+					'B': isize_common + 0                                    # = 41
+				}
+				ret = sum(input_size[i.mmid.mmtype] for i in self.inputs if i.mmid)
+
+				# We have no way of knowing whether a non-MMGen addr is compressed or uncompressed until
+				# we see the key, so assume compressed for fee-estimation purposes.  If fee estimate is
+				# off by more than 5%, sign() aborts and user is instructed to use --vsize-adj option
+				return ret + sum(input_size['C'] for i in self.inputs if not i.mmid)
+
+			def get_outputs_size():
+				# output bytes = amt: 8, byte_count: 1+, pk_script
+				# pk_script bytes: p2pkh: 25, p2sh: 23, bech32: 22
+				return sum({'p2pkh':34,'p2sh':32,'bech32':31}[o.addr.addr_fmt] for o in self.outputs)
 
-		async def is_replaced():
-			if await is_in_mempool():
-				return False
-			try:
-				ret = await g.rpc.call('gettransaction',self.coin_txid)
-			except:
+			# https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki
+			# The witness is a serialization of all witness data of the transaction. Each txin is
+			# associated with a witness field. A witness field starts with a var_int to indicate the
+			# number of stack items for the txin. It is followed by stack items, with each item starts
+			# with a var_int to indicate the length. Witness data is NOT script.
+
+			# A non-witness program txin MUST be associated with an empty witness field, represented
+			# by a 0x00. If all txins are not witness program, a transaction's wtxid is equal to its txid.
+			def get_witness_size():
+				if not self.has_segwit_inputs():
+					return 0
+				wf_size = 1 + 1 + sig_size + 1 + pubkey_size_compressed # vInt vInt sig vInt pubkey = 108
+				return sum((1,wf_size)[bool(i.mmid) and i.mmid.mmtype in ('S','B')] for i in self.inputs)
+
+			isize = get_inputs_size()
+			osize = get_outputs_size()
+			wsize = get_witness_size()
+
+			# TODO: compute real varInt sizes instead of assuming 1 byte
+			# old serialization: [nVersion]              [vInt][txins][vInt][txouts]         [nLockTime]
+			old_size =           4                     + 1   + isize + 1  + osize          + 4
+			# marker = 0x00, flag = 0x01
+			# new serialization: [nVersion][marker][flag][vInt][txins][vInt][txouts][witness][nLockTime]
+			new_size =           4       + 1     + 1   + 1   + isize + 1  + osize + wsize  + 4 \
+					if wsize else old_size
+
+			ret = (old_size * 3 + new_size) // 4
+
+			dmsg('\nData from estimate_size():')
+			dmsg(f'  inputs size: {isize}, outputs size: {osize}, witness size: {wsize}')
+			dmsg(f'  size: {new_size}, vsize: {ret}, old_size: {old_size}')
+
+			return int(ret * float(opt.vsize_adj)) if hasattr(opt,'vsize_adj') and opt.vsize_adj else ret
+
+		# convert absolute BTC fee to satoshis-per-byte using estimated size
+		def fee_abs2rel(self,abs_fee,to_unit=None):
+			unit = getattr(self.proto.coin_amt,to_unit or 'min_coin_unit')
+			return int(abs_fee // unit // self.estimate_size())
+
+		def get_fee(self):
+			return self.sum_inputs() - self.sum_outputs()
+
+		def get_hex_locktime(self):
+			return int(bytes.fromhex(self.hex[-8:])[::-1].hex(),16)
+
+		def set_hex_locktime(self,val):
+			assert isinstance(val,int),'locktime value not an integer'
+			self.hex = self.hex[:-8] + bytes.fromhex('{:08x}'.format(val))[::-1].hex()
+
+		def add_timestamp(self):
+			self.timestamp = make_timestamp()
+
+		def add_blockcount(self):
+			self.blockcount = self.rpc.blockcount
+
+		# returns true if comment added or changed
+		def add_comment(self,infile=None):
+			if infile:
+				self.label = MMGenTxLabel(get_data_from_file(infile,'transaction comment'))
+			else: # get comment from user, or edit existing comment
+				m = ('Add a comment to transaction?','Edit transaction comment?')[bool(self.label)]
+				if keypress_confirm(m,default_yes=False):
+					while True:
+						s = MMGenTxLabel(my_raw_input('Comment: ',insert_txt=self.label))
+						if s:
+							lbl_save = self.label
+							self.label = s
+							return (True,False)[lbl_save == self.label]
+						else:
+							msg('Invalid comment')
 				return False
-			else:
-				if 'bip125-replaceable' in ret and 'confirmations' in ret and ret['confirmations'] <= 0:
-					r.replacing_confs = -ret['confirmations']
-					r.replacing_txs = ret['walletconflicts']
-					return True
-				else:
-					return False
 
-		if await is_in_mempool():
-			if status:
-				d = await g.rpc.call('gettransaction',self.coin_txid)
-				brs = 'bip125-replaceable'
-				rep = '{}replaceable'.format(('NOT ','')[brs in d and d[brs]=='yes'])
-				t = d['timereceived']
-				m = 'Sent {} ({} h/m/s ago)'
-				b = m.format(time.strftime('%c',time.gmtime(t)),secs_to_dhms(int(time.time()-t)))
-				if opt.quiet:
-					msg('Transaction is in mempool')
+		def get_non_mmaddrs(self,desc):
+			return {i.addr for i in getattr(self,desc) if not i.mmid}
+
+		def check_non_mmgen_inputs(self,caller,non_mmaddrs=None):
+			non_mmaddrs = non_mmaddrs or self.get_non_mmaddrs('inputs')
+			if non_mmaddrs:
+				fs = fmt(self.non_mmgen_inputs_msg,strip_char='\t')
+				m = fs.format('\n    '.join(non_mmaddrs))
+				if caller in ('txdo','txsign'):
+					if not opt.keys_from_file:
+						raise UserOptError('ERROR: ' + m)
 				else:
-					msg('TX status: in mempool, {}\n{}'.format(rep,b))
-			else:
-				msg('Warning: transaction is in mempool!')
-		elif await is_in_wallet():
-			die(0,'Transaction has {} confirmation{}'.format(r.confs,suf(r.confs)))
-		elif await is_in_utxos():
-			die(2,red('ERROR: transaction is in the blockchain (but not in the tracking wallet)!'))
-		elif await is_replaced():
-			msg('Transaction has been replaced\nReplacement transaction ' + (
-					f'has {r.replacing_confs} confirmation{suf(r.replacing_confs)}'
-				if r.replacing_confs else
-					'is in mempool' ))
-			if not opt.quiet:
-				msg('Replacing transactions:')
-				d = []
-				for txid in r.replacing_txs:
-					try:    d.append(await g.rpc.call('getmempoolentry',txid))
-					except: d.append({})
-				for txid,mp_entry in zip(r.replacing_txs,d):
-					msg(f'  {txid}' + ('',' in mempool')['height' in mp_entry])
-			die(0,'')
-
-	def confirm_send(self):
-		m1 = ("Once this transaction is sent, there's no taking it back!",'')[bool(opt.quiet)]
-		m2 = 'broadcast this transaction to the {} network'.format(g.chain.upper())
-		m3 = ('YES, I REALLY WANT TO DO THIS','YES')[bool(opt.quiet or opt.yes)]
-		confirm_or_raise(m1,m2,m3)
-		msg('Sending transaction')
-
-	async def send(self,prompt_user=True,exit_on_fail=False):
-		if not self.marked_signed():
-			die(1,'Transaction is not signed!')
-
-		self.check_correct_chain()
-
-		self.check_pubkey_scripts()
-
-		self.check_hex_tx_matches_mmgen_tx(DeserializedTX(self.hex))
-
-		if self.has_segwit_outputs() and not segwit_is_active() and not g.bogus_send:
-			m = 'Transaction has MMGen Segwit outputs, but this blockchain does not support Segwit'
-			die(2,m+' at the current height')
-
-		if self.get_fee() > g.proto.max_tx_fee:
-			die(2,'Transaction fee ({}) greater than {} max_tx_fee ({} {})!'.format(
-				self.get_fee(),
-				g.proto.name,
-				g.proto.max_tx_fee,
-				g.proto.coin ))
-
-		await self.get_status()
-
-		if prompt_user:
-			self.confirm_send()
-
-		if g.bogus_send:
-			ret = None
-		else:
+					msg('WARNING: ' + m)
+					if not (opt.yes or keypress_confirm('Continue?',default_yes=True)):
+						die(1,'Exiting at user request')
+
+	class New(Base):
+		usr_fee_prompt = 'Enter transaction fee: '
+		fee_is_approximate = False
+		fee_fail_fs = 'Network fee estimation for {c} confirmations failed ({t})'
+		no_chg_msg = 'Warning: Change address will be deleted as transaction produces no change'
+		msg_wallet_low_coin = 'Wallet has insufficient funds for this transaction ({} {} needed)'
+		msg_low_coin = 'Selected outputs insufficient to fund this transaction ({} {} needed)'
+		msg_no_change_output = fmt("""
+			ERROR: No change address specified.  If you wish to create a transaction with
+			only one output, specify a single output address with no {} amount
+		""").strip()
+
+		def __init__(self,proto,tw=None): # tw required for resolving ERC20 token data
+			MMGenTX.Base.__init__(self)
+			self.proto = proto
+			self.tw    = tw
+
+		def get_chg_output_idx(self):
+			ch_ops = [x.is_chg for x in self.outputs]
 			try:
-				ret = await g.rpc.call('sendrawtransaction',self.hex)
-			except Exception as e:
-				ret = False
-
-		if ret == False:
-			errmsg = e
-			if 'Signature must use SIGHASH_FORKID' in errmsg:
-				m  = 'The Aug. 1 2017 UAHF has activated on this chain.'
-				m += "\nRe-run the script with the --coin=bch option."
-			elif 'Illegal use of SIGHASH_FORKID' in errmsg:
-				m  = 'The Aug. 1 2017 UAHF is not yet active on this chain.'
-				m += "\nRe-run the script without the --coin=bch option."
-			elif '64: non-final' in errmsg:
-				m2 = "Transaction with locktime '{}' can't be included in this block!"
-				m = m2.format(strfmt_locktime(self.get_hex_locktime()))
-			else:
-				m = errmsg
-			msg(yellow(m))
-			msg(red('Send of MMGen transaction {} failed'.format(self.txid)))
-			if exit_on_fail:
-				sys.exit(1)
-			return False
-		else:
-			if g.bogus_send:
-				m = 'BOGUS transaction NOT sent: {}'
-			else:
-				assert ret == self.coin_txid, 'txid mismatch (after sending)'
-				m = 'Transaction sent: {}'
-			self.desc = 'sent transaction'
-			msg(m.format(self.coin_txid.hl()))
-			self.add_timestamp()
-			self.add_blockcount()
-			return True
-
-	def view_with_prompt(self,prompt='',pause=True):
-		prompt += ' (y)es, (N)o, pager (v)iew, (t)erse view: '
-		from .term import get_char
-		ok_chars = 'YyNnVvTt'
-		while True:
-			reply = get_char(prompt,immed_chars=ok_chars).strip('\n\r')
-			msg('')
-			if reply == '' or reply in 'Nn':
-				break
-			elif reply in 'YyVvTt':
-				self.view(pager=reply in 'Vv',terse=reply in 'Tt',pause=pause)
-				break
-			else:
-				msg('Invalid reply')
-
-	def view(self,pager=False,pause=True,terse=False):
-		o = self.format_view(terse=terse)
-		if pager:
-			do_pager(o)
-		else:
-			msg_r(o)
-			from .term import get_char
-			if pause:
-				get_char('Press any key to continue: ')
-				msg('')
+				return ch_ops.index(True)
+			except ValueError:
+				return None
+
+		def del_output(self,idx):
+			self.outputs.pop(idx)
+
+		def update_output_amt(self,idx,amt):
+			o = self.outputs[idx]._asdict()
+			o['amt'] = amt
+			self.outputs[idx] = MMGenTxOutput(self.proto,**o)
+
+		def add_mmaddrs_to_outputs(self,ad_w,ad_f):
+			a = [e.addr for e in self.outputs]
+			d = ad_w.make_reverse_dict(a)
+			if ad_f:
+				d.update(ad_f.make_reverse_dict(a))
+			for e in self.outputs:
+				if e.addr and e.addr in d:
+					e.mmid,f = d[e.addr]
+					if f:
+						e.label = f
+
+		def check_dup_addrs(self,io_str):
+			assert io_str in ('inputs','outputs')
+			addrs = [e.addr for e in getattr(self,io_str)]
+			if len(addrs) != len(set(addrs)):
+				die(2,f'{addrs}: duplicate address in transaction {io_str}')
+
+		# coin-specific fee routines
+		@property
+		def relay_fee(self):
+			kb_fee = self.proto.coin_amt(self.rpc.cached['networkinfo']['relayfee'])
+			ret = kb_fee * self.estimate_size() // 1024
+			vmsg('Relay fee: {} {c}/kB, for transaction: {} {c}'.format(kb_fee,ret,c=self.coin))
+			return ret
 
-#	def is_replaceable_from_rpc(self):
-#		dec_tx = await g.rpc.call('decoderawtransaction',self.hex)
-#		return None < dec_tx['vin'][0]['sequence'] <= g.max_int - 2
-
-	def is_replaceable(self):
-		return self.inputs[0].sequence == g.max_int - 2
-
-	def format_view_body(self,blockcount,nonmm_str,max_mmwid,enl,terse,sort):
-
-		if sort not in self.view_sort_orders:
-			die(1,f'{sort!r}: invalid transaction view sort order. Valid options: {{}}'.format(
-					','.join(self.view_sort_orders) ))
-
-		def format_io(desc):
-			io = getattr(self,desc)
-			is_input = desc == 'inputs'
-			yield desc.capitalize() + ':\n' + enl
-			addr_w = max(len(e.addr) for e in io)
-			confs_per_day = 60*60*24 // g.proto.avg_bdi
-			io_sorted = {
-				# prepend '/' (sorts before '0') to ensure non-MMGen addrs are displayed first
-				'addr': lambda: sorted(io,key=lambda o: o.mmid.sort_key if o.mmid else '/'+o.addr),
-				'raw':  lambda: io
-			}[sort]
-			for n,e in enumerate(io_sorted()):
-				if is_input and blockcount:
-					confs = e.confs + blockcount - self.blockcount
-					days = int(confs // confs_per_day)
-				if e.mmid:
-					mmid_fmt = e.mmid.fmt(
-						width=max_mmwid,
-						encl='()',
-						color=True,
-						append_chars=('',' (chg)')[bool(not is_input and e.is_chg and terse)],
-						append_color='green')
-				else:
-					mmid_fmt = MMGenID.fmtc(nonmm_str,width=max_mmwid,color=True)
-				if terse:
-					yield '{:3} {} {} {} {}\n'.format(
-						n+1,
-						e.addr.fmt(color=True,width=addr_w),
-						mmid_fmt,
-						e.amt.hl(),
-						g.dcoin )
-				else:
-					def gen():
-						if is_input:
-							yield (n+1,      'tx,vout:', e.txid + ',' + str(e.vout))
-							yield ('',       'address:', e.addr.fmt(color=True,width=addr_w) + ' ' + mmid_fmt)
-						else:
-							yield (n+1,      'address:', e.addr.fmt(color=True,width=addr_w) + ' ' + mmid_fmt)
-						if e.label:
-							yield ('',       'comment:', e.label.hl())
-						yield     ('',       'amount:',  e.amt.hl() + ' ' + g.dcoin)
-						if is_input and blockcount:
-							yield ('',       'confirmations:', f'{confs} (around {days} days)')
-						if not is_input and e.is_chg:
-							yield ('',       'change:',  green('True'))
-					yield '\n'.join('{:>3} {:<8} {}'.format(*d) for d in gen()) + '\n\n'
-
-		return (
-			'Displaying inputs and outputs in {} sort order'.format({'raw':'raw','addr':'address'}[sort])
-			+ ('\n\n','\n')[terse]
-			+ ''.join(format_io('inputs'))
-			+ ''.join(format_io('outputs')) )
-
-	def format_view_rel_fee(self,terse):
-		return ' ({} {})\n'.format(
-			pink(str(self.fee_abs2rel(self.get_fee()))),
-			self.rel_fee_disp)
-
-	def format_view_abs_fee(self):
-		return g.proto.coin_amt(self.get_fee()).hl()
-
-	def format_view_verbose_footer(self):
-		tsize = len(self.hex)//2 if self.hex else 'unknown'
-		out = f'Transaction size: Vsize {self.estimate_size()} (estimated), Total {tsize}'
-		if self.marked_signed():
-			wsize = DeserializedTX(self.hex)['witness_size']
-			out += f', Base {tsize-wsize}, Witness {wsize}'
-		return out + '\n'
-
-	def format_view(self,terse=False,sort=dfl_view_sort_order):
-		blockcount = None
-		if g.proto.base_coin != 'ETH':
+		async def get_rel_fee_from_network(self):
 			try:
-				blockcount = g.rpc.blockcount
+				ret = await self.rpc.call('estimatesmartfee',opt.tx_confs,opt.fee_estimate_mode.upper())
+				fee_per_kb = ret['feerate'] if 'feerate' in ret else -2
+				fe_type = 'estimatesmartfee'
 			except:
-				pass
+				args = () if self.coin=='BCH' and self.rpc.daemon_version >= 190100 else (opt.tx_confs,)
+				fee_per_kb = await self.rpc.call('estimatefee',*args)
+				fe_type = 'estimatefee'
+
+			return fee_per_kb,fe_type
+
+		# given tx size, rel fee and units, return absolute fee
+		def convert_fee_spec(self,tx_size,units,amt,unit):
+			self.usr_rel_fee = None # TODO
+			return self.proto.coin_amt(int(amt)*tx_size*getattr(self.proto.coin_amt,units[unit])) \
+				if tx_size else None
+
+		# given network fee estimate in BTC/kB, return absolute fee using estimated tx size
+		def fee_est2abs(self,fee_per_kb,fe_type=None):
+			tx_size = self.estimate_size()
+			f = fee_per_kb * opt.tx_fee_adj * tx_size / 1024
+			ret = self.proto.coin_amt(f,from_decimal=True)
+			if opt.verbose:
+				msg(fmt(f"""
+					{fe_type.upper()} fee for {opt.tx_confs} confirmations: {fee_per_kb} {self.coin}/kB
+					TX size (estimated): {tx_size} bytes
+					Fee adjustment factor: {opt.tx_fee_adj}
+					Absolute fee (fee_per_kb * adj_factor * tx_size / 1024): {ret} {self.coin}
+				""").strip())
+			return ret
 
-		def get_max_mmwid(io):
-			if io == self.inputs:
-				sel_f = lambda o: len(o.mmid) + 2 # len('()')
+		def convert_and_check_fee(self,tx_fee,desc='Missing description'):
+			abs_fee = self.process_fee_spec(tx_fee,self.estimate_size())
+			if abs_fee == None:
+				raise ValueError(f'{tx_fee}: cannot convert {self.rel_fee_desc} to {self.coin}'
+									+ ' because transaction size is unknown')
+			if abs_fee == False:
+				err = f'{tx_fee!r}: invalid TX fee (not a {self.coin} amount or {self.rel_fee_desc} specification)'
+			elif abs_fee > self.proto.max_tx_fee:
+				err = f'{abs_fee} {self.coin}: {desc} fee too large (maximum fee: {self.proto.max_tx_fee} {self.coin})'
+			elif abs_fee < self.relay_fee:
+				err = f'{abs_fee} {self.coin}: {desc} fee too small (less than relay fee of {self.relay_fee} {self.coin})'
 			else:
-				sel_f = lambda o: len(o.mmid) + (2,8)[bool(o.is_chg)] # + len(' (chg)')
-			return  max(max([sel_f(o) for o in io if o.mmid] or [0]),len(nonmm_str))
-
-		nonmm_str = f'(non-{g.proj_name} address)'
-		max_mmwid = max(get_max_mmwid(self.inputs),get_max_mmwid(self.outputs))
-
-		def gen_view():
-			yield (self.txview_hdr_fs_short if terse else self.txview_hdr_fs).format(
-				i = self.txid.hl(),
-				a = self.send_amt.hl(),
-				c = g.dcoin,
-				t = self.timestamp,
-				r = (red('False'),green('True'))[self.is_replaceable()],
-				s = self.marked_signed(color=True),
-				l = (green('None'),orange(strfmt_locktime(self.locktime,terse=True)))[bool(self.locktime)] )
-
-			if self.chain != 'mainnet':
-				yield green(f'Chain: {self.chain.upper()}') + '\n'
-
-			if self.coin_txid:
-				yield f'{g.coin} TxID: {self.coin_txid.hl()}\n'
+				return abs_fee
+			msg(err)
+			return False
 
-			enl = ('\n','')[bool(terse)]
-			yield enl
+		# non-coin-specific fee routines
 
-			if self.label:
-				yield f'Comment: {self.label.hl()}\n{enl}'
+		# given tx size and absolute fee or fee spec, return absolute fee
+		# relative fee is N+<first letter of unit name>
+		def process_fee_spec(self,tx_fee,tx_size):
+			if self.proto.coin_amt(tx_fee,on_fail='silent'):
+				return self.proto.coin_amt(tx_fee)
+			else:
+				import re
+				units = {u[0]:u for u in self.proto.coin_amt.units}
+				pat = re.compile(r'([1-9][0-9]*)({})'.format('|'.join(units)))
+				if pat.match(tx_fee):
+					amt,unit = pat.match(tx_fee).groups()
+					return self.convert_fee_spec(tx_size,units,amt,unit)
+			return False
 
-			yield self.format_view_body(blockcount,nonmm_str,max_mmwid,enl,terse=terse,sort=sort)
+		def get_usr_fee_interactive(self,tx_fee=None,desc='Starting'):
+			abs_fee = None
+			while True:
+				if tx_fee:
+					abs_fee = self.convert_and_check_fee(tx_fee,desc)
+				if abs_fee:
+					prompt = '{} TX fee{}: {}{} {} ({} {})\n'.format(
+							desc,
+							(f' (after {opt.tx_fee_adj}X adjustment)'
+								if opt.tx_fee_adj != 1 and desc.startswith('Network-estimated')
+									else ''),
+							('','≈')[self.fee_is_approximate],
+							abs_fee.hl(),
+							self.coin,
+							pink(str(self.fee_abs2rel(abs_fee))),
+							self.rel_fee_disp)
+					if opt.yes or keypress_confirm(prompt+'OK?',default_yes=True):
+						if opt.yes:
+							msg(prompt)
+						return abs_fee
+				tx_fee = my_raw_input(self.usr_fee_prompt)
+				desc = 'User-selected'
+
+		async def get_fee_from_user(self,have_estimate_fail=[]):
+
+			if opt.tx_fee:
+				desc = 'User-selected'
+				start_fee = opt.tx_fee
+			else:
+				desc = f'Network-estimated (mode: {opt.fee_estimate_mode.upper()})'
+				fee_per_kb,fe_type = await self.get_rel_fee_from_network()
+
+				if fee_per_kb < 0:
+					if not have_estimate_fail:
+						msg(self.fee_fail_fs.format(c=opt.tx_confs,t=fe_type))
+						have_estimate_fail.append(True)
+					start_fee = None
+				else:
+					start_fee = self.fee_est2abs(fee_per_kb,fe_type)
 
-			yield (self.txview_ftr_fs_short if terse else self.txview_ftr_fs).format(
-				i = self.sum_inputs().hl(),
-				o = self.sum_outputs().hl(),
-				a = self.format_view_abs_fee(),
-				r = self.format_view_rel_fee(terse),
-				d = g.dcoin,
-				c = g.coin )
+			return self.get_usr_fee_interactive(start_fee,desc=desc)
 
-			if opt.verbose:
-				yield self.format_view_verbose_footer()
+		def add_output(self,coinaddr,amt,is_chg=None):
+			self.outputs.append(MMGenTxOutput(self.proto,addr=coinaddr,amt=amt,is_chg=is_chg))
 
-		return ''.join(gen_view()) # TX label might contain non-ascii chars
+		def process_cmd_arg(self,arg,ad_f,ad_w):
 
-	def check_txfile_hex_data(self):
-		self.hex = HexStr(self.hex,on_fail='raise')
+			def add_output_chk(addr,amt,err_desc):
+				if not amt and self.get_chg_output_idx() != None:
+					die(2,'ERROR: More than one change address listed on command line')
+				if is_mmgen_id(self.proto,addr) or is_coin_addr(self.proto,addr):
+					coin_addr = ( mmaddr2coinaddr(addr,ad_w,ad_f,self.proto) if is_mmgen_id(self.proto,addr)
+									else CoinAddr(self.proto,addr) )
+					self.add_output(coin_addr,self.proto.coin_amt(amt or '0'),is_chg=not amt)
+				else:
+					die(2,f'{addr}: invalid {err_desc} {{!r}}'.format(f'{addr},{amt}' if amt else addr))
 
-	def parse_txfile_hex_data(self):
-		pass
+			if ',' in arg:
+				addr,amt = arg.split(',',1)
+				add_output_chk(addr,amt,'coin argument in command-line argument')
+			else:
+				add_output_chk(arg,None,'command-line argument')
+
+		async def get_cmdline_input_addrs(self):
+			# Bitcoin full node, call doesn't go to the network, so just call listunspent with addrs=[]
+			return []
+
+		def process_cmd_args(self,cmd_args,ad_f,ad_w):
+
+			for a in cmd_args:
+				self.process_cmd_arg(a,ad_f,ad_w)
+
+			if self.get_chg_output_idx() == None:
+				die(2,( 'ERROR: No change output specified',
+						self.msg_no_change_output.format(self.dcoin))[len(self.outputs) == 1])
+
+			if not self.rpc.info('segwit_is_active') and self.has_segwit_outputs():
+				rdie(2,f'{g.proj_name} Segwit address requested on the command line, '
+						+ 'but Segwit is not active on this chain')
+
+			if not self.outputs:
+				die(2,'At least one output must be specified on the command line')
+
+		async def get_outputs_from_cmdline(self,cmd_args):
+			from .addr import AddrList,AddrData,TwAddrData
+			addrfiles = [a for a in cmd_args if get_extension(a) == AddrList.ext]
+			cmd_args = set(cmd_args) - set(addrfiles)
+
+			ad_f = AddrData(self.proto)
+			for a in addrfiles:
+				check_infile(a)
+				ad_f.add(AddrList(self.proto,a))
+
+			ad_w = await TwAddrData(self.proto,wallet=self.tw)
+
+			self.process_cmd_args(cmd_args,ad_f,ad_w)
+
+			self.add_mmaddrs_to_outputs(ad_w,ad_f)
+			self.check_dup_addrs('outputs')
+
+		# inputs methods
+		def select_unspent(self,unspent):
+			prompt = 'Enter a range or space-separated list of outputs to spend: '
+			while True:
+				reply = my_raw_input(prompt).strip()
+				if reply:
+					selected = AddrIdxList(fmt_str=','.join(reply.split()),on_fail='return')
+					if selected:
+						if selected[-1] <= len(unspent):
+							return selected
+						msg('Unspent output number must be <= {}'.format(len(unspent)))
+
+		def select_unspent_cmdline(self,unspent):
+
+			def idx2num(idx):
+				uo = unspent[idx]
+				mmid_disp = f' ({uo.twmmid})' if uo.twmmid.type == 'mmgen' else ''
+				msg(f'Adding input: {idx + 1} {uo.addr}{mmid_disp}')
+				return idx + 1
+
+			def get_uo_nums():
+				for addr in opt.inputs.split(','):
+					if is_mmgen_id(self.proto,addr):
+						attr = 'twmmid'
+					elif is_coin_addr(self.proto,addr):
+						attr = 'addr'
+					else:
+						die(1,f'{addr!r}: not an MMGen ID or {self.coin} address')
 
-	def process_cmd_arg(self,arg,ad_f,ad_w):
+					found = False
+					for idx in range(len(unspent)):
+						if getattr(unspent[idx],attr) == addr:
+							yield idx2num(idx)
+							found = True
 
-		def add_output_chk(addr,amt,err_desc):
-			if not amt and self.get_chg_output_idx() != None:
-				die(2,'ERROR: More than one change address listed on command line')
-			if is_mmgen_id(addr) or is_coin_addr(addr):
-				coin_addr = mmaddr2coinaddr(addr,ad_w,ad_f) if is_mmgen_id(addr) else CoinAddr(addr)
-				self.add_output(coin_addr,g.proto.coin_amt(amt or '0'),is_chg=not amt)
-			else:
-				die(2,f'{addr}: invalid {err_desc} {{!r}}'.format(f'{addr},{amt}' if amt else addr))
+					if not found:
+						die(1,f'{addr!r}: address not found in tracking wallet')
 
-		if ',' in arg:
-			addr,amt = arg.split(',',1)
-			add_output_chk(addr,amt,'coin argument in command-line argument')
-		else:
-			add_output_chk(arg,None,'command-line argument')
+			return set(get_uo_nums()) # silently discard duplicates
 
-	def process_cmd_args(self,cmd_args,ad_f,ad_w):
+		# we don't know fee yet, so perform preliminary check with fee == 0
+		async def precheck_sufficient_funds(self,inputs_sum,sel_unspent):
+			if self.twuo.total < self.send_amt:
+				msg(self.msg_wallet_low_coin.format(self.send_amt-inputs_sum,self.dcoin))
+				return False
+			if inputs_sum < self.send_amt:
+				msg(self.msg_low_coin.format(self.send_amt-inputs_sum,self.dcoin))
+				return False
+			return True
 
-		for a in cmd_args:
-			self.process_cmd_arg(a,ad_f,ad_w)
+		def copy_inputs_from_tw(self,tw_unspent_data):
+			def gen_inputs():
+				for d in tw_unspent_data:
+					i = MMGenTxInput(
+						self.proto,
+						**{attr:getattr(d,attr) for attr in d.__dict__ if attr in MMGenTxInput.tw_copy_attrs} )
+					if d.twmmid.type == 'mmgen':
+						i.mmid = d.twmmid # twmmid -> mmid
+					yield i
+			self.inputs = MMGenTxInputList(self,list(gen_inputs()))
 
-		if self.get_chg_output_idx() == None:
-			die(2,( 'ERROR: No change output specified',
-					self.msg_no_change_output.format(g.dcoin))[len(self.outputs) == 1])
+		async def get_change_amt(self):
+			return self.sum_inputs() - self.send_amt - self.fee
 
-		if not segwit_is_active() and self.has_segwit_outputs():
-			rdie(2,f'{g.proj_name} Segwit address requested on the command line, '
-					+ 'but Segwit is not active on this chain')
+		def final_inputs_ok_msg(self,change_amt):
+			return f'Transaction produces {self.proto.coin_amt(change_amt).hl()} {self.coin} in change'
 
-		if not self.outputs:
-			die(2,'At least one output must be specified on the command line')
+		def warn_insufficient_chg(self,change_amt):
+			msg(self.msg_low_coin.format(self.proto.coin_amt(-change_amt).hl(),self.coin))
 
-	async def get_outputs_from_cmdline(self,cmd_args):
-		from .addr import AddrList,AddrData,TwAddrData
-		addrfiles = [a for a in cmd_args if get_extension(a) == AddrList.ext]
-		cmd_args = set(cmd_args) - set(addrfiles)
+		async def get_inputs_from_user(self):
 
-		ad_f = AddrData()
-		for a in addrfiles:
-			check_infile(a)
-			ad_f.add(AddrList(a))
+			while True:
+				us_f = self.select_unspent_cmdline if opt.inputs else self.select_unspent
+				sel_nums = us_f(self.twuo.unspent)
 
-		ad_w = await TwAddrData(wallet=self.tw)
+				msg(f'Selected output{suf(sel_nums)}: {{}}'.format(' '.join(str(n) for n in sel_nums)))
+				sel_unspent = self.twuo.MMGenTwOutputList([self.twuo.unspent[i-1] for i in sel_nums])
 
-		self.process_cmd_args(cmd_args,ad_f,ad_w)
+				inputs_sum = sum(s.amt for s in sel_unspent)
+				if not await self.precheck_sufficient_funds(inputs_sum,sel_unspent):
+					continue
 
-		self.add_mmaddrs_to_outputs(ad_w,ad_f)
-		self.check_dup_addrs('outputs')
+				self.copy_inputs_from_tw(sel_unspent)  # makes self.inputs
 
-	def select_unspent(self,unspent):
-		prompt = 'Enter a range or space-separated list of outputs to spend: '
-		while True:
-			reply = my_raw_input(prompt).strip()
-			if reply:
-				selected = AddrIdxList(fmt_str=','.join(reply.split()),on_fail='return')
-				if selected:
-					if selected[-1] <= len(unspent):
-						return selected
-					msg('Unspent output number must be <= {}'.format(len(unspent)))
+				self.fee = await self.get_fee_from_user()
 
-	# we don't know fee yet, so perform preliminary check with fee == 0
-	async def precheck_sufficient_funds(self,inputs_sum,sel_unspent):
-		if self.twuo.total < self.send_amt:
-			msg(self.msg_wallet_low_coin.format(self.send_amt-inputs_sum,g.dcoin))
-			return False
-		if inputs_sum < self.send_amt:
-			msg(self.msg_low_coin.format(self.send_amt-inputs_sum,g.dcoin))
-			return False
-		return True
+				change_amt = await self.get_change_amt()
 
-	async def get_change_amt(self):
-		return self.sum_inputs() - self.send_amt - self.fee
+				if change_amt >= 0:
+					p = self.final_inputs_ok_msg(change_amt)
+					if opt.yes or keypress_confirm(p+'. OK?',default_yes=True):
+						if opt.yes:
+							msg(p)
+						return change_amt
+				else:
+					self.warn_insufficient_chg(change_amt)
 
-	def warn_insufficient_chg(self,change_amt):
-		msg(self.msg_low_coin.format(g.proto.coin_amt(-change_amt).hl(),g.coin))
+		def update_change_output(self,change_amt):
+			chg_idx = self.get_chg_output_idx()
+			if change_amt == 0:
+				msg(self.no_chg_msg)
+				self.del_output(chg_idx)
+			else:
+				self.update_output_amt(chg_idx,self.proto.coin_amt(change_amt))
+
+		def update_send_amt(self,change_amt):
+			if not self.send_amt:
+				self.send_amt = change_amt
+
+		def check_fee(self):
+			fee = self.sum_inputs() - self.sum_outputs()
+			if fee > self.proto.max_tx_fee:
+				c = self.proto.coin
+				raise MaxFeeExceeded(f'Transaction fee of {fee} {c} too high! (> {self.proto.max_tx_fee} {c})')
+
+		def update_txid(self):
+			self.txid = MMGenTxID(make_chksum_6(bytes.fromhex(self.hex)).upper())
+
+		async def create_raw(self):
+			i = [{'txid':e.txid,'vout':e.vout} for e in self.inputs]
+			if self.inputs[0].sequence:
+				i[0]['sequence'] = self.inputs[0].sequence
+			o = {e.addr:e.amt for e in self.outputs}
+			self.hex = HexStr(await self.rpc.call('createrawtransaction',i,o))
+			self.update_txid()
 
-	def final_inputs_ok_msg(self,change_amt):
-		return f'Transaction produces {g.proto.coin_amt(change_amt).hl()} {g.coin} in change'
+		async def create(self,cmd_args,locktime,do_info=False,caller='txcreate'):
 
-	def select_unspent_cmdline(self,unspent):
+			assert isinstance(locktime,int),'locktime must be of type int'
 
-		def idx2num(idx):
-			uo = unspent[idx]
-			mmid_disp = f' ({uo.twmmid})' if uo.twmmid.type == 'mmgen' else ''
-			msg(f'Adding input: {idx + 1} {uo.addr}{mmid_disp}')
-			return idx + 1
+			from .tw import TwUnspentOutputs
 
-		def get_uo_nums():
-			for addr in opt.inputs.split(','):
-				if is_mmgen_id(addr):
-					attr = 'twmmid'
-				elif is_coin_addr(addr):
-					attr = 'addr'
-				else:
-					die(1,f'{addr!r}: not an MMGen ID or {g.coin} address')
+			if opt.comment_file:
+				self.add_comment(opt.comment_file)
 
-				found = False
-				for idx in range(len(unspent)):
-					if getattr(unspent[idx],attr) == addr:
-						yield idx2num(idx)
-						found = True
+			twuo_addrs = await self.get_cmdline_input_addrs()
 
-				if not found:
-					die(1,f'{addr!r}: address not found in tracking wallet')
+			self.twuo = await TwUnspentOutputs(self.proto,minconf=opt.minconf,addrs=twuo_addrs)
+			await self.twuo.get_unspent_data()
 
-		return set(get_uo_nums()) # silently discard duplicates
+			if not do_info:
+				await self.get_outputs_from_cmdline(cmd_args)
 
-	async def get_cmdline_input_addrs(self):
-		# Bitcoin full node, call doesn't go to the network, so just call listunspent with addrs=[]
-		return []
+			do_license_msg()
 
-	async def get_inputs_from_user(self):
+			if not opt.inputs:
+				await self.twuo.view_and_sort(self)
 
-		while True:
-			us_f = self.select_unspent_cmdline if opt.inputs else self.select_unspent
-			sel_nums = us_f(self.twuo.unspent)
+			self.twuo.display_total()
 
-			msg(f'Selected output{suf(sel_nums)}: {{}}'.format(' '.join(str(n) for n in sel_nums)))
-			sel_unspent = self.twuo.MMGenTwOutputList([self.twuo.unspent[i-1] for i in sel_nums])
+			if do_info:
+				del self.twuo.wallet
+				sys.exit(0)
 
-			inputs_sum = sum(s.amt for s in sel_unspent)
-			if not await self.precheck_sufficient_funds(inputs_sum,sel_unspent):
-				continue
+			self.send_amt = self.sum_outputs()
 
-			non_mmaddrs = [i for i in sel_unspent if i.twmmid.type == 'non-mmgen']
-			if non_mmaddrs and self.caller != 'txdo':
-				msg(self.msg_non_mmgen_inputs.format(
-					', '.join(sorted({a.addr.hl() for a in non_mmaddrs}))))
-				if not (opt.yes or keypress_confirm('Accept?')):
-					continue
+			msg_r('Total amount to spend: ')
+			msg(f'{self.send_amt.hl()} {self.dcoin}' if self.send_amt else 'Unknown')
 
-			self.copy_inputs_from_tw(sel_unspent)  # makes self.inputs
+			change_amt = await self.get_inputs_from_user()
 
-			self.fee = await self.get_fee_from_user()
+			self.check_non_mmgen_inputs(caller)
 
-			change_amt = await self.get_change_amt()
+			self.update_change_output(change_amt)
+			self.update_send_amt(change_amt)
 
-			if change_amt >= 0:
-				p = self.final_inputs_ok_msg(change_amt)
-				if opt.yes or keypress_confirm(p+'. OK?',default_yes=True):
-					if opt.yes:
-						msg(p)
-					return change_amt
-			else:
-				self.warn_insufficient_chg(change_amt)
+			if self.proto.base_proto == 'Bitcoin':
+				self.inputs.sort_bip69()
+				self.outputs.sort_bip69()
+				# do this only after inputs are sorted
+				if opt.rbf:
+					self.inputs[0].sequence = g.max_int - 2 # handles the nLockTime case too
+				elif locktime:
+					self.inputs[0].sequence = g.max_int - 1
 
-	def check_fee(self):
-		assert self.sum_inputs() - self.sum_outputs() <= g.proto.max_tx_fee
+			if not opt.yes:
+				self.add_comment()  # edits an existing comment
 
-	def update_send_amt(self,change_amt):
-		if not self.send_amt:
-			self.send_amt = change_amt
+			await self.create_raw() # creates self.hex, self.txid
 
-	async def set_token_params(self):
-		pass
+			if self.proto.base_proto == 'Bitcoin' and locktime:
+				msg(f'Setting nLockTime to {strfmt_locktime(locktime)}!')
+				self.set_hex_locktime(locktime)
+				self.update_txid()
+				self.locktime = locktime
 
-	async def create(self,cmd_args,locktime,do_info=False):
-		assert isinstance(locktime,int),'locktime must be of type int'
+			self.add_timestamp()
+			self.add_blockcount()
+			self.chain = self.proto.chain_name
+			self.check_fee()
+
+			qmsg('Transaction successfully created')
+
+			new = MMGenTX.Unsigned(data=self.__dict__)
+
+			if not opt.yes:
+				new.view_with_prompt('View transaction details?')
+
+			del new.twuo.wallet
+			return new
+
+	class Completed(Base):
+		"""
+		signed or unsigned transaction with associated file
+		"""
+		fn_fee_unit = 'satoshi'
+		view_sort_orders = ('addr','raw')
+		dfl_view_sort_order = 'addr'
+		txview_hdr_fs = 'TRANSACTION DATA\n\nID={i} ({a} {c}) UTC={t} RBF={r} Sig={s} Locktime={l}\n'
+		txview_hdr_fs_short = 'TX {i} ({a} {c}) UTC={t} RBF={r} Sig={s} Locktime={l}\n'
+		txview_ftr_fs = 'Total input:  {i} {d}\nTotal output: {o} {d}\nTX fee:       {a} {c}{r}\n'
+		txview_ftr_fs_short = 'In {i} {d} - Out {o} {d}\nFee {a} {c}{r}\n'
+		parsed_hex = None
+
+		def __init__(self,filename=None,quiet_open=False,data=None):
+			MMGenTX.Base.__init__(self)
+			if data:
+				assert filename is None, 'MMGenTX.Completed_chk1'
+				assert type(data) is dict, 'MMGenTX.Completed_chk2'
+				self.__dict__ = data
+				return
+			elif filename:
+				assert data is None, 'MMGenTX.Completed_chk3'
+				from .txfile import MMGenTxFile
+				MMGenTxFile(self).parse(filename,quiet_open=quiet_open)
+				self.check_pubkey_scripts()
 
-		from .tw import TwUnspentOutputs
+			# repeat with sign and send, because coin daemon could be restarted
+			self.check_correct_chain()
 
-		if opt.comment_file:
-			self.add_comment(opt.comment_file)
+		# check signature and witness data
+		def check_sigs(self): # return False if no sigs, raise exception on error
+			txins = (self.parsed_hex or DeserializedTX(self.proto,self.hex))['txins']
+			has_ss = any(ti['scriptSig'] for ti in txins)
+			has_witness = any('witness' in ti and ti['witness'] for ti in txins)
+			if not (has_ss or has_witness):
+				return False
+			fs = "Hex TX has {} scriptSig but input is of type '{}'!"
+			for n in range(len(txins)):
+				ti,mmti = txins[n],self.inputs[n]
+				if ti['scriptSig'] == '' or ( len(ti['scriptSig']) == 46 and # native P2WPKH or P2SH-P2WPKH
+						ti['scriptSig'][:6] == '16' + self.proto.witness_vernum_hex + '14' ):
+					assert 'witness' in ti, 'missing witness'
+					assert type(ti['witness']) == list and len(ti['witness']) == 2, 'malformed witness'
+					assert len(ti['witness'][1]) == 66, 'incorrect witness pubkey length'
+					assert mmti.mmid, fs.format('witness-type','non-MMGen')
+					assert mmti.mmid.mmtype == ('S','B')[ti['scriptSig']==''],(
+								fs.format('witness-type',mmti.mmid.mmtype))
+				else: # non-witness
+					if mmti.mmid:
+						assert mmti.mmid.mmtype not in ('S','B'), fs.format('signature in',mmti.mmid.mmtype)
+					assert not 'witness' in ti, 'non-witness input has witness'
+					# sig_size 72 (DER format), pubkey_size 'compressed':33, 'uncompressed':65
+					assert (200 < len(ti['scriptSig']) < 300), 'malformed scriptSig' # VERY rough check
+			return True
 
-		twuo_addrs = await self.get_cmdline_input_addrs()
+		def check_pubkey_scripts(self):
+			for n,i in enumerate(self.inputs,1):
+				addr,fmt = scriptPubKey2addr(self.proto,i.scriptPubKey)
+				if i.addr != addr:
+					if fmt != i.addr.addr_fmt:
+						m = 'Address format of scriptPubKey ({}) does not match that of address ({}) in input #{}'
+						msg(m.format(fmt,i.addr.addr_fmt,n))
+					m = 'ERROR: Address and scriptPubKey of transaction input #{} do not match!'
+					die(3,(m+'\n  {:23}{}'*3).format(n, 'address:',i.addr,
+														'scriptPubKey:',i.scriptPubKey,
+														'scriptPubKey->address:',addr ))
+
+#		def is_replaceable_from_rpc(self):
+#			dec_tx = await self.rpc.call('decoderawtransaction',self.hex)
+#			return None < dec_tx['vin'][0]['sequence'] <= g.max_int - 2
+
+		def is_replaceable(self):
+			return self.inputs[0].sequence == g.max_int - 2
+
+		def check_txfile_hex_data(self):
+			self.hex = HexStr(self.hex,on_fail='raise')
+
+		def parse_txfile_hex_data(self):
+			pass
 
-		self.twuo = await TwUnspentOutputs(minconf=opt.minconf,addrs=twuo_addrs)
-		await self.twuo.get_unspent_data()
+		def write_to_file(self,*args,**kwargs):
+			from .txfile import MMGenTxFile
+			MMGenTxFile(self).write(*args,**kwargs)
+
+		def format_view_body(self,blockcount,nonmm_str,max_mmwid,enl,terse,sort):
+
+			if sort not in self.view_sort_orders:
+				die(1,f'{sort!r}: invalid transaction view sort order. Valid options: {{}}'.format(
+						','.join(self.view_sort_orders) ))
+
+			def format_io(desc):
+				io = getattr(self,desc)
+				is_input = desc == 'inputs'
+				yield desc.capitalize() + ':\n' + enl
+				addr_w = max(len(e.addr) for e in io)
+				confs_per_day = 60*60*24 // self.proto.avg_bdi
+				io_sorted = {
+					# prepend '/' (sorts before '0') to ensure non-MMGen addrs are displayed first
+					'addr': lambda: sorted(io,key=lambda o: o.mmid.sort_key if o.mmid else '/'+o.addr),
+					'raw':  lambda: io
+				}[sort]
+				for n,e in enumerate(io_sorted()):
+					if is_input and blockcount:
+						confs = e.confs + blockcount - self.blockcount
+						days = int(confs // confs_per_day)
+					if e.mmid:
+						mmid_fmt = e.mmid.fmt(
+							width=max_mmwid,
+							encl='()',
+							color=True,
+							append_chars=('',' (chg)')[bool(not is_input and e.is_chg and terse)],
+							append_color='green')
+					else:
+						mmid_fmt = MMGenID.fmtc(nonmm_str,width=max_mmwid,color=True)
+					if terse:
+						yield '{:3} {} {} {} {}\n'.format(
+							n+1,
+							e.addr.fmt(color=True,width=addr_w),
+							mmid_fmt,
+							e.amt.hl(),
+							self.dcoin )
+					else:
+						def gen():
+							if is_input:
+								yield (n+1,      'tx,vout:', e.txid + ',' + str(e.vout))
+								yield ('',       'address:', e.addr.fmt(color=True,width=addr_w) + ' ' + mmid_fmt)
+							else:
+								yield (n+1,      'address:', e.addr.fmt(color=True,width=addr_w) + ' ' + mmid_fmt)
+							if e.label:
+								yield ('',       'comment:', e.label.hl())
+							yield     ('',       'amount:',  e.amt.hl() + ' ' + self.dcoin)
+							if is_input and blockcount:
+								yield ('',       'confirmations:', f'{confs} (around {days} days)')
+							if not is_input and e.is_chg:
+								yield ('',       'change:',  green('True'))
+						yield '\n'.join('{:>3} {:<8} {}'.format(*d) for d in gen()) + '\n\n'
+
+			return (
+				'Displaying inputs and outputs in {} sort order'.format({'raw':'raw','addr':'address'}[sort])
+				+ ('\n\n','\n')[terse]
+				+ ''.join(format_io('inputs'))
+				+ ''.join(format_io('outputs')) )
+
+		def format_view_rel_fee(self,terse):
+			return ' ({} {})\n'.format(
+				pink(str(self.fee_abs2rel(self.get_fee()))),
+				self.rel_fee_disp)
+
+		def format_view_abs_fee(self):
+			return self.proto.coin_amt(self.get_fee()).hl()
+
+		def format_view_verbose_footer(self):
+			tsize = len(self.hex)//2 if self.hex else 'unknown'
+			out = f'Transaction size: Vsize {self.estimate_size()} (estimated), Total {tsize}'
+			if self.name == 'Signed':
+				wsize = DeserializedTX(self.proto,self.hex)['witness_size']
+				out += f', Base {tsize-wsize}, Witness {wsize}'
+			return out + '\n'
+
+		def format_view(self,terse=False,sort=dfl_view_sort_order):
+			blockcount = None
+			if self.proto.base_coin != 'ETH':
+				try:
+					blockcount = self.rpc.blockcount
+				except:
+					pass
+
+			def get_max_mmwid(io):
+				if io == self.inputs:
+					sel_f = lambda o: len(o.mmid) + 2 # len('()')
+				else:
+					sel_f = lambda o: len(o.mmid) + (2,8)[bool(o.is_chg)] # + len(' (chg)')
+				return  max(max([sel_f(o) for o in io if o.mmid] or [0]),len(nonmm_str))
 
-		if not do_info:
-			await self.get_outputs_from_cmdline(cmd_args)
+			nonmm_str = f'(non-{g.proj_name} address)'
+			max_mmwid = max(get_max_mmwid(self.inputs),get_max_mmwid(self.outputs))
 
-		do_license_msg()
+			def gen_view():
+				yield (self.txview_hdr_fs_short if terse else self.txview_hdr_fs).format(
+					i = self.txid.hl(),
+					a = self.send_amt.hl(),
+					c = self.dcoin,
+					t = self.timestamp,
+					r = (red('False'),green('True'))[self.is_replaceable()],
+					s = (red('False'),green('True'))[self.name == 'Signed'],
+					l = (green('None'),orange(strfmt_locktime(self.locktime,terse=True)))[bool(self.locktime)] )
 
-		if not opt.inputs:
-			await self.twuo.view_and_sort(self)
+				if self.chain != 'mainnet': # if mainnet has a coin-specific name, display it
+					yield green(f'Chain: {self.chain.upper()}') + '\n'
 
-		self.twuo.display_total()
+				if self.coin_txid:
+					yield f'{self.coin} TxID: {self.coin_txid.hl()}\n'
 
-		if do_info:
-			del self.twuo.wallet
-			sys.exit(0)
+				enl = ('\n','')[bool(terse)]
+				yield enl
 
-		self.send_amt = self.sum_outputs()
+				if self.label:
+					yield f'Comment: {self.label.hl()}\n{enl}'
 
-		msg_r('Total amount to spend: ')
-		msg(f'{self.send_amt.hl()} {g.dcoin}' if self.send_amt else 'Unknown')
+				yield self.format_view_body(blockcount,nonmm_str,max_mmwid,enl,terse=terse,sort=sort)
 
-		change_amt = await self.get_inputs_from_user()
+				yield (self.txview_ftr_fs_short if terse else self.txview_ftr_fs).format(
+					i = self.sum_inputs().hl(),
+					o = self.sum_outputs().hl(),
+					a = self.format_view_abs_fee(),
+					r = self.format_view_rel_fee(terse),
+					d = self.dcoin,
+					c = self.coin )
 
-		self.update_change_output(change_amt)
-		self.update_send_amt(change_amt)
+				if opt.verbose:
+					yield self.format_view_verbose_footer()
 
-		if g.proto.base_proto == 'Bitcoin':
-			self.inputs.sort_bip69()
-			self.outputs.sort_bip69()
-			# do this only after inputs are sorted
-			if opt.rbf:
-				self.inputs[0].sequence = g.max_int - 2 # handles the locktime case too
-			elif locktime:
-				self.inputs[0].sequence = g.max_int - 1
+			return ''.join(gen_view()) # TX label might contain non-ascii chars
 
-		if not opt.yes:
-			self.add_comment()  # edits an existing comment
+		def view_with_prompt(self,prompt='',pause=True):
+			prompt += ' (y)es, (N)o, pager (v)iew, (t)erse view: '
+			from .term import get_char
+			ok_chars = 'YyNnVvTt'
+			while True:
+				reply = get_char(prompt,immed_chars=ok_chars).strip('\n\r')
+				msg('')
+				if reply == '' or reply in 'Nn':
+					break
+				elif reply in 'YyVvTt':
+					self.view(pager=reply in 'Vv',terse=reply in 'Tt',pause=pause)
+					break
+				else:
+					msg('Invalid reply')
 
-		await self.create_raw()       # creates self.hex, self.txid
+		def view(self,pager=False,pause=True,terse=False):
+			o = self.format_view(terse=terse)
+			if pager:
+				do_pager(o)
+			else:
+				msg_r(o)
+				from .term import get_char
+				if pause:
+					get_char('Press any key to continue: ')
+					msg('')
 
-		if g.proto.base_proto == 'Bitcoin' and locktime:
-			msg(f'Setting nlocktime to {strfmt_locktime(locktime)}!')
-			self.set_hex_locktime(locktime)
-			self.update_txid()
-			self.locktime = locktime
+	class Unsigned(Completed):
+		desc = 'unsigned transaction'
+		ext  = 'rawtx'
 
-		self.add_timestamp()
-		self.add_blockcount()
-		self.chain = g.chain
+		def __init__(self,*args,**kwargs):
+			super().__init__(*args,**kwargs)
+			if self.check_sigs():
+				die(1,'Transaction is signed!')
 
-		self.check_fee()
+		def delete_attrs(self,desc,attr):
+			for e in getattr(self,desc):
+				if hasattr(e,attr):
+					delattr(e,attr)
 
-		qmsg('Transaction successfully created')
+		def get_input_sids(self):
+			return set(e.mmid.sid for e in self.inputs if e.mmid)
 
-		if not opt.yes:
-			self.view_with_prompt('View decoded transaction?')
+		def get_output_sids(self):
+			return set(e.mmid.sid for e in self.outputs if e.mmid)
 
-		del self.twuo.wallet
+		async def sign(self,tx_num_str,keys): # return signed object or False; don't exit or raise exception
 
-class MMGenTxForSigning(MMGenTX):
+			try:
+				self.check_correct_chain()
+			except TransactionChainMismatch:
+				return False
 
-	hexdata_type = 'json'
+			if (self.has_segwit_inputs() or self.has_segwit_outputs()) and not self.proto.cap('segwit'):
+				ymsg(f"TX has Segwit inputs or outputs, but {self.coin} doesn't support Segwit!")
+				return False
 
-	def __new__(cls,*args,**kwargs):
-		return MMGenObject.__new__(altcoin_subclass(cls,'tx','MMGenTxForSigning'))
+			self.check_pubkey_scripts()
 
-	async def sign(self,tx_num_str,keys): # return True or False; don't exit or raise exception
+			qmsg(f'Passing {len(keys)} key{suf(keys)} to {self.proto.daemon_name}')
 
-		if self.marked_signed():
-			msg('Transaction is already signed!')
-			return False
+			if self.has_segwit_inputs():
+				from .addr import KeyGenerator,AddrGenerator
+				kg = KeyGenerator(self.proto,'std')
+				ag = AddrGenerator(self.proto,'segwit')
+				keydict = MMGenDict([(d.addr,d.sec) for d in keys])
 
-		try:
-			self.check_correct_chain()
-		except TransactionChainMismatch:
-			return False
+			sig_data = []
+			for d in self.inputs:
+				e = {k:getattr(d,k) for k in ('txid','vout','scriptPubKey','amt')}
+				e['amount'] = e['amt']
+				del e['amt']
+				if d.mmid and d.mmid.mmtype == 'S':
+					e['redeemScript'] = ag.to_segwit_redeem_script(kg.to_pubhex(keydict[d.addr]))
+				sig_data.append(e)
 
-		if (self.has_segwit_inputs() or self.has_segwit_outputs()) and not g.proto.cap('segwit'):
-			ymsg(f"TX has Segwit inputs or outputs, but {g.coin} doesn't support Segwit!")
-			return False
+			msg_r(f'Signing transaction{tx_num_str}...')
+			wifs = [d.sec.wif for d in keys]
 
-		self.check_pubkey_scripts()
-
-		qmsg(f'Passing {len(keys)} key{suf(keys)} to {g.proto.daemon_name}')
-
-		if self.has_segwit_inputs():
-			from .addr import KeyGenerator,AddrGenerator
-			kg = KeyGenerator('std')
-			ag = AddrGenerator('segwit')
-			keydict = MMGenDict([(d.addr,d.sec) for d in keys])
-
-		sig_data = []
-		for d in self.inputs:
-			e = {k:getattr(d,k) for k in ('txid','vout','scriptPubKey','amt')}
-			e['amount'] = e['amt']
-			del e['amt']
-			if d.mmid and d.mmid.mmtype == 'S':
-				e['redeemScript'] = ag.to_segwit_redeem_script(kg.to_pubhex(keydict[d.addr]))
-			sig_data.append(e)
-
-		msg_r(f'Signing transaction{tx_num_str}...')
-		wifs = [d.sec.wif for d in keys]
-
-		try:
-			args = (
-				('signrawtransaction',       self.hex,sig_data,wifs,g.proto.sighash_type),
-				('signrawtransactionwithkey',self.hex,wifs,sig_data,g.proto.sighash_type)
-			)['sign_with_key' in g.rpc.caps]
-			ret = await g.rpc.call(*args)
-		except Exception as e:
-			msg(yellow((
-				e.args[0],
-				'This is not the BCH chain.\nRe-run the script without the --coin=bch option.'
-			)['Invalid sighash param' in e.args[0]]))
-			return False
+			try:
+				args = (
+					('signrawtransaction',       self.hex,sig_data,wifs,self.proto.sighash_type),
+					('signrawtransactionwithkey',self.hex,wifs,sig_data,self.proto.sighash_type)
+				)['sign_with_key' in self.rpc.caps]
+				ret = await self.rpc.call(*args)
+			except Exception as e:
+				msg(yellow((
+					e.args[0],
+					'This is not the BCH chain.\nRe-run the script without the --coin=bch option.'
+				)['Invalid sighash param' in e.args[0]]))
+				return False
 
-		try:
-			self.hex = HexStr(ret['hex'])
-			tx_decoded = await g.rpc.call('decoderawtransaction',ret['hex'])
-			self.compare_size_and_estimated_size(tx_decoded)
-			dt = DeserializedTX(self.hex)
-			self.check_hex_tx_matches_mmgen_tx(dt)
-			self.coin_txid = CoinTxID(dt['txid'],on_fail='raise')
-			self.check_sigs(dt)
-			if not self.coin_txid == tx_decoded['txid']:
-				raise BadMMGenTxID('txid mismatch (after signing)')
-			msg('OK')
-			return True
-		except Exception as e:
-			try: m = '{}'.format(e.args[0])
-			except: m = repr(e.args[0])
-			msg('\n'+yellow(m))
-			if g.traceback:
-				import traceback
-				ymsg('\n'+''.join(traceback.format_exception(*sys.exc_info())))
-			return False
+			try:
+				self.hex = HexStr(ret['hex'])
+				self.parsed_hex = dtx = DeserializedTX(self.proto,self.hex)
+				new = MMGenTX.Signed(data=self.__dict__)
+				tx_decoded = await self.rpc.call('decoderawtransaction',ret['hex'])
+				new.compare_size_and_estimated_size(tx_decoded)
+				new.check_hex_tx_matches_mmgen_tx(dtx)
+				new.coin_txid = CoinTxID(dtx['txid'],on_fail='raise')
+				if not new.coin_txid == tx_decoded['txid']:
+					raise BadMMGenTxID('txid mismatch (after signing)')
+				msg('OK')
+				return new
+			except Exception as e:
+				try: m = '{}'.format(e.args[0])
+				except: m = repr(e.args[0])
+				msg('\n'+yellow(m))
+				if g.traceback:
+					import traceback
+					ymsg('\n'+''.join(traceback.format_exception(*sys.exc_info())))
+				return False
 
-class MMGenBumpTX(MMGenTxForSigning):
+	class Signed(Completed):
+		desc = 'signed transaction'
+		ext  = 'sigtx'
+
+		def __init__(self,*args,**kwargs):
+			if 'tw' in kwargs:
+				self.tw = kwargs['tw']
+				del kwargs['tw']
+			super().__init__(*args,**kwargs)
+			if not self.check_sigs():
+				die(1,'Transaction is not signed!')
+
+		# check that a malicious, compromised or malfunctioning coin daemon hasn't altered hex tx data:
+		# does not check witness or signature data
+		def check_hex_tx_matches_mmgen_tx(self,dtx):
+			m = 'A malicious or malfunctioning coin daemon or other program may have altered your data!'
+
+			lt = dtx['lock_time']
+			if lt != int(self.locktime or 0):
+				m2 = 'Transaction hex nLockTime ({}) does not match MMGen transaction nLockTime ({})\n{}'
+				raise TxHexMismatch(m2.format(lt,self.locktime,m))
+
+			def check_equal(desc,hexio,mmio):
+				if mmio != hexio:
+					msg('\nMMGen {}:\n{}'.format(desc,pp_fmt(mmio)))
+					msg('Hex {}:\n{}'.format(desc,pp_fmt(hexio)))
+					m2 = '{} in hex transaction data from coin daemon do not match those in MMGen transaction!\n'
+					raise TxHexMismatch((m2+m).format(desc.capitalize()))
+
+			seq_hex   = [int(i['nSeq'],16) for i in dtx['txins']]
+			seq_mmgen = [i.sequence or g.max_int for i in self.inputs]
+			check_equal('sequence numbers',seq_hex,seq_mmgen)
+
+			d_hex   = sorted((i['txid'],i['vout']) for i in dtx['txins'])
+			d_mmgen = sorted((i.txid,i.vout) for i in self.inputs)
+			check_equal('inputs',d_hex,d_mmgen)
+
+			d_hex   = sorted((o['address'],self.proto.coin_amt(o['amount'])) for o in dtx['txouts'])
+			d_mmgen = sorted((o.addr,o.amt) for o in self.outputs)
+			check_equal('outputs',d_hex,d_mmgen)
+
+			uh = dtx['unsigned_hex']
+			if str(self.txid) != make_chksum_6(bytes.fromhex(uh)).upper():
+				raise TxHexMismatch('MMGen TxID ({}) does not match hex transaction data!\n{}'.format(self.txid,m))
+
+		def compare_size_and_estimated_size(self,tx_decoded):
+			est_vsize = self.estimate_size()
+			d = tx_decoded
+			vsize = d['vsize'] if 'vsize' in d else d['size']
+			vmsg(f'\nVsize: {vsize} (true) {est_vsize} (estimated)')
+			ratio = float(est_vsize) / vsize
+			if not (0.95 < ratio < 1.05): # allow for 5% error
+				raise BadTxSizeEstimate(fmt(f"""
+					Estimated transaction vsize is {ratio:1.2f} times the true vsize
+					Your transaction fee estimates will be inaccurate
+					Please re-create and re-sign the transaction using the option --vsize-adj={1/ratio:1.2f}
+				""").strip())
+
+		async def get_status(self,status=False):
+
+			class r(object):
+				pass
 
-	def __new__(cls,*args,**kwargs):
-		return MMGenTX.__new__(altcoin_subclass(cls,'tx','MMGenBumpTX'),*args,**kwargs)
+			async def is_in_wallet():
+				try: ret = await self.rpc.call('gettransaction',self.coin_txid)
+				except: return False
+				if ret.get('confirmations',0) > 0:
+					r.confs = ret['confirmations']
+					return True
+				else:
+					return False
 
-	min_fee = None
-	bump_output_idx = None
+			async def is_in_utxos():
+				try: return 'txid' in await self.rpc.call('getrawtransaction',self.coin_txid,True)
+				except: return False
 
-	def __init__(self,filename,send=False,tw=None):
-		super().__init__(filename,tw=tw)
+			async def is_in_mempool():
+				try: return 'height' in await self.rpc.call('getmempoolentry',self.coin_txid)
+				except: return False
 
-		if not self.is_replaceable():
-			die(1,f'Transaction {self.txid} is not replaceable')
+			async def is_replaced():
+				if await is_in_mempool():
+					return False
+				try:
+					ret = await self.rpc.call('gettransaction',self.coin_txid)
+				except:
+					return False
+				else:
+					if 'bip125-replaceable' in ret and ret.get('confirmations',1) <= 0:
+						r.replacing_confs = -ret['confirmations']
+						r.replacing_txs = ret['walletconflicts']
+						return True
+					else:
+						return False
 
-		# If sending, require tx to be signed
-		if send:
-			if not self.marked_signed():
-				die(1,'File {filename!r} is not a signed {g.proj_name} transaction file')
-			if not self.coin_txid:
-				die(1,'Transaction {self.txid!r} was not broadcast to the network')
+			if await is_in_mempool():
+				if status:
+					d = await self.rpc.call('gettransaction',self.coin_txid)
+					rep = ('' if d.get('bip125-replaceable') == 'yes' else 'NOT ') + 'replaceable'
+					t = d['timereceived']
+					if opt.quiet:
+						msg('Transaction is in mempool')
+					else:
+						msg(f'TX status: in mempool, {rep}')
+						msg('Sent {} ({} h/m/s ago)'.format(
+							time.strftime('%c',time.gmtime(t)),
+							secs_to_dhms(int(time.time()-t))) )
+				else:
+					msg('Warning: transaction is in mempool!')
+			elif await is_in_wallet():
+				die(0,f'Transaction has {r.confs} confirmation{suf(r.confs)}')
+			elif await is_in_utxos():
+				die(2,red('ERROR: transaction is in the blockchain (but not in the tracking wallet)!'))
+			elif await is_replaced():
+				msg('Transaction has been replaced')
+				msg('Replacement transaction ' + (
+						f'has {r.replacing_confs} confirmation{suf(r.replacing_confs)}'
+					if r.replacing_confs else
+						'is in mempool' ) )
+				if not opt.quiet:
+					msg('Replacing transactions:')
+					d = []
+					for txid in r.replacing_txs:
+						try:    d.append(await self.rpc.call('getmempoolentry',txid))
+						except: d.append({})
+					for txid,mp_entry in zip(r.replacing_txs,d):
+						msg(f'  {txid}' + (' in mempool' if 'height' in mp_entry else '') )
+				die(0,'')
+
+		def confirm_send(self):
+			confirm_or_raise(
+				('' if opt.quiet else "Once this transaction is sent, there's no taking it back!"),
+				f'broadcast this transaction to the {self.proto.coin} {self.proto.network.upper()} network',
+				('YES' if opt.quiet or opt.yes else 'YES, I REALLY WANT TO DO THIS') )
+			msg('Sending transaction')
+
+		async def send(self,prompt_user=True,exit_on_fail=False):
 
-		self.coin_txid = ''
-		self.mark_raw()
+			self.check_correct_chain()
+			self.check_pubkey_scripts()
+			self.check_hex_tx_matches_mmgen_tx(DeserializedTX(self.proto,self.hex))
 
-	def check_bumpable(self):
-		if not [o.amt for o in self.outputs if o.amt >= self.min_fee]:
-			die(1,
-				'Transaction cannot be bumped.\n' +
-				f'All outputs contain less than the minimum fee ({self.min_fee} {g.coin})')
+			if self.has_segwit_outputs() and not self.rpc.info('segwit_is_active') and not g.bogus_send:
+				die(2,'Transaction has Segwit outputs, but this blockchain does not support Segwit'
+						+ ' at the current height')
 
-	def choose_output(self):
-		chg_idx = self.get_chg_output_idx()
-		init_reply = opt.output_to_reduce
+			if self.get_fee() > self.proto.max_tx_fee:
+				die(2,'Transaction fee ({}) greater than {} max_tx_fee ({} {})!'.format(
+					self.get_fee(),
+					self.proto.name,
+					self.proto.max_tx_fee,
+					self.proto.coin ))
 
-		def check_sufficient_funds(o_amt):
-			if o_amt < self.min_fee:
-				msg(f'Minimum fee ({self.min_fee} {g.coin}) is greater than output amount ({o_amt} {g.coin})')
-				return False
-			return True
+			await self.get_status()
 
-		if len(self.outputs) == 1:
-			if check_sufficient_funds(self.outputs[0].amt):
-				self.bump_output_idx = 0
-				return 0
-			else:
-				die(1,'Insufficient funds to bump transaction')
+			if prompt_user:
+				self.confirm_send()
 
-		while True:
-			if init_reply == None:
-				m = 'Choose an output to deduct the fee from (Hit ENTER for the change output): '
-				reply = my_raw_input(m) or 'c'
+			if g.bogus_send:
+				ret = None
 			else:
-				reply,init_reply = init_reply,None
-			if chg_idx == None and not is_int(reply):
-				msg('Output must be an integer')
-			elif chg_idx != None and not is_int(reply) and reply != 'c':
-				msg("Output must be an integer, or 'c' for the change output")
+				try:
+					ret = await self.rpc.call('sendrawtransaction',self.hex)
+				except Exception as e:
+					ret = False
+
+			if ret == False:
+				errmsg = e
+				if 'Signature must use SIGHASH_FORKID' in errmsg:
+					m = ('The Aug. 1 2017 UAHF has activated on this chain.\n'
+						+ 'Re-run the script with the --coin=bch option.' )
+				elif 'Illegal use of SIGHASH_FORKID' in errmsg:
+					m  = ('The Aug. 1 2017 UAHF is not yet active on this chain.\n'
+						+ 'Re-run the script without the --coin=bch option.' )
+				elif '64: non-final' in errmsg:
+					m = "Transaction with nLockTime {!r} can't be included in this block!".format(
+						strfmt_locktime(self.get_hex_locktime()) )
+				else:
+					m = errmsg
+				ymsg(m)
+				rmsg(f'Send of MMGen transaction {self.txid} failed')
+				if exit_on_fail:
+					sys.exit(1)
+				return False
 			else:
-				idx = chg_idx if reply == 'c' else (int(reply) - 1)
-				if idx < 0 or idx >= len(self.outputs):
-					msg(f'Output must be in the range 1-{len(self.outputs)}')
+				if g.bogus_send:
+					m = 'BOGUS transaction NOT sent: {}'
 				else:
-					o_amt = self.outputs[idx].amt
-					cm = ' (change output)' if chg_idx == idx else ''
-					prompt = f'Fee will be deducted from output {idx+1}{cm} ({o_amt} {g.coin})'
-					if check_sufficient_funds(o_amt):
-						if opt.yes or keypress_confirm(prompt+'.  OK?',default_yes=True):
-							if opt.yes:
-								msg(prompt)
-							self.bump_output_idx = idx
-							return idx
-
-	@property
-	def min_fee(self):
-		return self.sum_inputs() - self.sum_outputs() + self.relay_fee
-
-	def update_fee(self,op_idx,fee):
-		amt = self.sum_inputs() - self.sum_outputs(exclude=op_idx) - fee
-		self.update_output_amt(op_idx,amt)
-
-	def convert_and_check_fee(self,tx_fee,desc):
-		ret = super().convert_and_check_fee(tx_fee,desc)
-		if ret < self.min_fee:
-			msg('{} {c}: {} fee too small. Minimum fee: {} {c} ({} {})'.format(
-				ret.hl(),
-				desc,
-				self.min_fee,
-				self.fee_abs2rel(self.min_fee.hl()),
-				self.rel_fee_desc,
-				c = g.coin ))
-			return False
-		output_amt = self.outputs[self.bump_output_idx].amt
-		if ret >= output_amt:
-			msg('{} {c}: {} fee too large. Maximum fee: <{} {c}'.format(
-				ret.hl(),
-				desc,
-				output_amt.hl(),
-				c = g.coin ))
-			return False
-		return ret
-
-# NOT MAINTAINED
-class MMGenSplitTX(MMGenTX):
-
-	async def get_outputs_from_cmdline(self,mmid): # TODO: check that addr is empty
-
-		from .addr import TwAddrData
-		ad_w = await TwAddrData()
+					m = 'Transaction sent: {}'
+					assert ret == self.coin_txid, 'txid mismatch (after sending)'
+				msg(m.format(self.coin_txid.hl()))
+				self.add_timestamp()
+				self.add_blockcount()
+				self.desc = 'sent transaction'
+				return True
 
-		if is_mmgen_id(mmid):
-			coin_addr = mmaddr2coinaddr(mmid,ad_w,None) if is_mmgen_id(mmid) else CoinAddr(mmid)
-			self.add_output(coin_addr,g.proto.coin_amt('0'),is_chg=True)
-		else:
-			die(2,'{}: invalid command-line argument'.format(mmid))
+		def print_contract_addr(self):
+			pass
 
-		self.add_mmaddrs_to_outputs(ad_w,None)
+		@staticmethod
+		async def get_tracking_wallet(filename):
+			from .txfile import MMGenTxFile
+			tmp_tx = MMGenTX.Base()
+			MMGenTxFile(tmp_tx).parse(filename,metadata_only=True)
+			if tmp_tx.proto.tokensym:
+				from .tw import TrackingWallet
+				return await TrackingWallet(tmp_tx.proto)
+			else:
+				return None
 
-		if not segwit_is_active() and self.has_segwit_outputs():
-			fs = '{} Segwit address requested on the command line, but Segwit is not active on this chain'
-			rdie(2,fs.format(g.proj_name))
+	class Bump(Completed,New):
+		desc = 'fee-bumped transaction'
+		ext  = 'rawtx'
 
-	def get_split_fee_from_user(self):
-		if opt.rpc_host2:
-			g.rpc_host = opt.rpc_host2
-		if opt.tx_fees:
-			opt.tx_fee = opt.tx_fees.split(',')[1]
-		return super().get_fee_from_user()
+		min_fee = None
+		bump_output_idx = None
 
-	async def create_split(self,mmid):
+		def __init__(self,data,send,tw=None):
+			MMGenTX.Completed.__init__(self,data=data)
+			self.tw = tw
 
-		self.outputs = self.MMGenTxOutputList()
-		await self.get_outputs_from_cmdline(mmid)
+			if not self.is_replaceable():
+				die(1,f'Transaction {self.txid} is not replaceable')
 
-		while True:
-			change_amt = self.sum_inputs() - self.get_split_fee_from_user()
-			if change_amt >= 0:
-				p = 'Transaction produces {} {} in change'.format(change_amt.hl(),g.coin)
-				if opt.yes or keypress_confirm(p+'.  OK?',default_yes=True):
-					if opt.yes:
-						msg(p)
-					break
-			else:
-				self.warn_insufficient_chg(change_amt)
+			# If sending, require original tx to be sent
+			if send and not self.coin_txid:
+				die(1,'Transaction {self.txid!r} was not broadcast to the network')
 
-		self.update_output_amt(0,change_amt)
-		self.send_amt = change_amt
+			self.coin_txid = ''
 
-		if not opt.yes:
-			self.add_comment()  # edits an existing comment
+		def check_bumpable(self):
+			if not [o.amt for o in self.outputs if o.amt >= self.min_fee]:
+				die(1,
+					'Transaction cannot be bumped.\n' +
+					f'All outputs contain less than the minimum fee ({self.min_fee} {self.coin})')
 
-		await self.create_raw()       # creates self.hex, self.txid
+		def choose_output(self):
+			chg_idx = self.get_chg_output_idx()
+			init_reply = opt.output_to_reduce
 
-		self.add_timestamp()
-		self.add_blockcount() # TODO
-		self.chain = g.chain
+			def check_sufficient_funds(o_amt):
+				if o_amt < self.min_fee:
+					msg(f'Minimum fee ({self.min_fee} {self.coin}) is greater than output amount ({o_amt} {self.coin})')
+					return False
+				return True
 
-		assert self.sum_inputs() - self.sum_outputs() <= g.proto.max_tx_fee
+			if len(self.outputs) == 1:
+				if check_sufficient_funds(self.outputs[0].amt):
+					self.bump_output_idx = 0
+					return 0
+				else:
+					die(1,'Insufficient funds to bump transaction')
 
-		qmsg('Transaction successfully created')
+			while True:
+				if init_reply == None:
+					m = 'Choose an output to deduct the fee from (Hit ENTER for the change output): '
+					reply = my_raw_input(m) or 'c'
+				else:
+					reply,init_reply = init_reply,None
+				if chg_idx == None and not is_int(reply):
+					msg('Output must be an integer')
+				elif chg_idx != None and not is_int(reply) and reply != 'c':
+					msg("Output must be an integer, or 'c' for the change output")
+				else:
+					idx = chg_idx if reply == 'c' else (int(reply) - 1)
+					if idx < 0 or idx >= len(self.outputs):
+						msg(f'Output must be in the range 1-{len(self.outputs)}')
+					else:
+						o_amt = self.outputs[idx].amt
+						cm = ' (change output)' if chg_idx == idx else ''
+						prompt = f'Fee will be deducted from output {idx+1}{cm} ({o_amt} {self.coin})'
+						if check_sufficient_funds(o_amt):
+							if opt.yes or keypress_confirm(prompt+'.  OK?',default_yes=True):
+								if opt.yes:
+									msg(prompt)
+								self.bump_output_idx = idx
+								return idx
+
+		@property
+		def min_fee(self):
+			return self.sum_inputs() - self.sum_outputs() + self.relay_fee
+
+		def update_fee(self,op_idx,fee):
+			amt = self.sum_inputs() - self.sum_outputs(exclude=op_idx) - fee
+			self.update_output_amt(op_idx,amt)
+
+		def convert_and_check_fee(self,tx_fee,desc):
+			ret = super().convert_and_check_fee(tx_fee,desc)
+			if ret < self.min_fee:
+				msg('{} {c}: {} fee too small. Minimum fee: {} {c} ({} {})'.format(
+					ret.hl(),
+					desc,
+					self.min_fee,
+					self.fee_abs2rel(self.min_fee.hl()),
+					self.rel_fee_desc,
+					c = self.coin ))
+				return False
+			output_amt = self.outputs[self.bump_output_idx].amt
+			if ret >= output_amt:
+				msg('{} {c}: {} fee too large. Maximum fee: <{} {c}'.format(
+					ret.hl(),
+					desc,
+					output_amt.hl(),
+					c = self.coin ))
+				return False
+			return ret
 
-		if not opt.yes:
-			self.view_with_prompt('View decoded transaction?')
+# NOT MAINTAINED
+#	class Split(Base):
+#
+#		async def get_outputs_from_cmdline(self,mmid): # TODO: check that addr is empty
+#
+#			from .addr import TwAddrData
+#			ad_w = await TwAddrData()
+#
+#			if is_mmgen_id(self.proto,mmid):
+#				coin_addr = mmaddr2coinaddr(mmid,ad_w,None) if is_mmgen_id(self.proto,mmid) else CoinAddr(mmid)
+#				self.add_output(coin_addr,self.proto.coin_amt('0'),is_chg=True)
+#			else:
+#				die(2,'{}: invalid command-line argument'.format(mmid))
+#
+#			self.add_mmaddrs_to_outputs(ad_w,None)
+#
+#			if not segwit_is_active() and self.has_segwit_outputs():
+#				fs = '{} Segwit address requested on the command line, but Segwit is not active on this chain'
+#				rdie(2,fs.format(g.proj_name))
+#
+#		def get_split_fee_from_user(self):
+#			if opt.rpc_host2:
+#				g.rpc_host = opt.rpc_host2
+#			if opt.tx_fees:
+#				opt.tx_fee = opt.tx_fees.split(',')[1]
+#			return super().get_fee_from_user()
+#
+#		async def create_split(self,mmid):
+#
+#			self.outputs = self.MMGenTxOutputList(self)
+#			await self.get_outputs_from_cmdline(mmid)
+#
+#			while True:
+#				change_amt = self.sum_inputs() - self.get_split_fee_from_user()
+#				if change_amt >= 0:
+#					p = 'Transaction produces {} {} in change'.format(change_amt.hl(),self.coin)
+#					if opt.yes or keypress_confirm(p+'.  OK?',default_yes=True):
+#						if opt.yes:
+#							msg(p)
+#						break
+#				else:
+#					self.warn_insufficient_chg(change_amt)
+#
+#			self.update_output_amt(0,change_amt)
+#			self.send_amt = change_amt
+#
+#			if not opt.yes:
+#				self.add_comment()  # edits an existing comment
+#
+#			await self.create_raw()       # creates self.hex, self.txid
+#
+#			self.add_timestamp()
+#			self.add_blockcount() # TODO
+#			self.chain = g.chain
+#
+#			assert self.sum_inputs() - self.sum_outputs() <= self.proto.max_tx_fee
+#
+#			qmsg('Transaction successfully created')
+#
+#			if not opt.yes:
+#				self.view_with_prompt('View transaction details?')

+ 36 - 38
mmgen/txfile.py

@@ -46,15 +46,15 @@ class MMGenTxFile:
 				import re
 				d = literal_eval(re.sub(r"[A-Za-z]+?\(('.+?')\)",r'\1',raw_data))
 			assert type(d) == list,'{} data not a list!'.format(desc)
-			if not (desc == 'outputs' and g.proto.base_coin == 'ETH'): # ETH txs can have no outputs
+			if not (desc == 'outputs' and tx.proto.base_coin == 'ETH'): # ETH txs can have no outputs
 				assert len(d),'no {}!'.format(desc)
 			for e in d:
-				e['amt'] = g.proto.coin_amt(e['amt'])
+				e['amt'] = tx.proto.coin_amt(e['amt'])
 			io,io_list = (
 				(MMGenTxOutput,MMGenTxOutputList),
 				(MMGenTxInput,MMGenTxInputList)
 			)[desc=='inputs']
-			return io_list(io(**e) for e in d)
+			return io_list(tx,[io(tx.proto,**e) for e in d])
 
 		tx_data = get_data_from_file(infile,tx.desc+' data',quiet=quiet_open)
 
@@ -94,58 +94,51 @@ class MMGenTxFile:
 				desc = 'locktime'
 				tx.locktime = int(metadata.pop()[3:])
 
-			tx.coin = metadata.pop(0) if len(metadata) == 6 else 'BTC'
-			if ':' in tx.coin:
-				tx.coin,tx.dcoin = tx.coin.split(':')
+			desc = 'coin token in metadata'
+			coin = metadata.pop(0) if len(metadata) == 6 else 'BTC'
+			coin,tokensym = coin.split(':') if ':' in coin else (coin,None)
 
-			if len(metadata) == 5:
-				t = metadata.pop(0)
-				tx.chain = (t.lower(),None)[t=='Unknown']
+			desc = 'chain token in metadata'
+			tx.chain = metadata.pop(0).lower() if len(metadata) == 5 else 'mainnet'
 
-			desc = 'metadata (4 items minimum required)'
+			from .protocol import CoinProtocol,init_proto
+			network = CoinProtocol.Base.chain_name_to_network(coin,tx.chain)
+
+			desc = 'initialization of protocol'
+			tx.proto = init_proto(coin,network=network)
+			if tokensym:
+				tx.proto.tokensym = tokensym
+
+			desc = 'metadata (4 items)'
 			txid,send_amt,tx.timestamp,blockcount = metadata
 
-			desc = 'txid in metadata'
+			desc = 'TxID in metadata'
 			tx.txid = MMGenTxID(txid,on_fail='raise')
 			desc = 'send amount in metadata'
-			tx.send_amt = UnknownCoinAmt(send_amt) # temporary, for 'metadata_only'
+			tx.send_amt = tx.proto.coin_amt(send_amt)
 			desc = 'block count in metadata'
 			tx.blockcount = int(blockcount)
 
 			if metadata_only:
 				return
 
-			desc = 'send amount in metadata'
-			tx.send_amt = g.proto.coin_amt(send_amt,on_fail='raise')
-
 			desc = 'transaction file hex data'
 			tx.check_txfile_hex_data()
-			desc = f'transaction file {tx.hexdata_type} data'
+			desc = 'Ethereum transaction file hex or json data'
 			tx.parse_txfile_hex_data()
-			# the following ops will all fail if g.coin doesn't match tx.coin
-			desc = 'coin type in metadata'
-			assert tx.coin == g.coin, tx.coin
 			desc = 'inputs data'
 			tx.inputs  = eval_io_data(inputs_data,'inputs')
 			desc = 'outputs data'
 			tx.outputs = eval_io_data(outputs_data,'outputs')
 		except Exception as e:
-			die(2,f'Invalid {desc} in transaction file: {e.args[0]}')
-
-		# is_for_chain() is no-op for Ethereum: test and mainnet addrs have same format
-		if not tx.chain and not tx.inputs[0].addr.is_for_chain('testnet'):
-			tx.chain = 'mainnet'
-
-		if tx.dcoin:
-			tx.resolve_g_token_from_txfile()
-			g.proto.dcoin = tx.dcoin
+			die(2,f'Invalid {desc} in transaction file: {e!s}')
 
 	def make_filename(self):
 		tx = self.tx
 		def gen_filename():
 			yield tx.txid
-			if g.coin != 'BTC':
-				yield '-' + g.dcoin
+			if tx.coin != 'BTC':
+				yield '-' + tx.dcoin
 			yield f'[{tx.send_amt!s}'
 			if tx.is_replaceable():
 				yield ',{}'.format(tx.fee_abs2rel(tx.get_fee(),to_unit=tx.fn_fee_unit))
@@ -154,24 +147,22 @@ class MMGenTxFile:
 			yield ']'
 			if g.debug_utf8:
 				yield '-α'
-			if g.proto.testnet:
-				yield '.testnet'
+			if tx.proto.testnet:
+				yield '.' + tx.proto.network
 			yield '.' + tx.ext
 		return ''.join(gen_filename())
 
 	def format(self):
 		tx = self.tx
-		tx.inputs.check_coin_mismatch()
-		tx.outputs.check_coin_mismatch()
 
 		def amt_to_str(d):
 			return {k: (str(d[k]) if k == 'amt' else d[k]) for k in d}
 
-		coin_id = '' if g.coin == 'BTC' else g.coin + ('' if g.coin == g.dcoin else ':'+g.dcoin)
+		coin_id = '' if tx.coin == 'BTC' else tx.coin + ('' if tx.coin == tx.dcoin else ':'+tx.dcoin)
 		lines = [
 			'{}{} {} {} {} {}{}'.format(
 				(coin_id+' ' if coin_id else ''),
-				tx.chain.upper() if tx.chain else 'Unknown',
+				tx.chain.upper(),
 				tx.txid,
 				tx.send_amt,
 				tx.timestamp,
@@ -179,8 +170,8 @@ class MMGenTxFile:
 				('',' LT={}'.format(tx.locktime))[bool(tx.locktime)]
 			),
 			tx.hex,
-			ascii([amt_to_str(e.__dict__) for e in tx.inputs]),
-			ascii([amt_to_str(e.__dict__) for e in tx.outputs])
+			ascii([amt_to_str(e._asdict()) for e in tx.inputs]),
+			ascii([amt_to_str(e._asdict()) for e in tx.outputs])
 		]
 
 		if tx.label:
@@ -222,3 +213,10 @@ class MMGenTxFile:
 			ask_write             = ask_write,
 			ask_tty               = ask_tty,
 			ask_write_default_yes = ask_write_default_yes )
+
+	@classmethod
+	def get_proto(cls,filename,quiet_open=False):
+		from .tx import MMGenTX
+		tmp_tx = MMGenTX.Base()
+		cls(tmp_tx).parse(filename,metadata_only=True,quiet_open=quiet_open)
+		return tmp_tx.proto

+ 38 - 41
mmgen/txsign.py

@@ -21,22 +21,10 @@ txsign: Sign a transaction generated by 'mmgen-txcreate'
 """
 
 from .common import *
-from .wallet import *
-from .tx import *
-from .addr import *
-
-pnm = g.proj_name
-
-wmsg = {
-	'mapping_error': """
-{pnm} -> {c} address mappings differ!
-{{:<23}} {{}} -> {{}}
-{{:<23}} {{}} -> {{}}
-""".strip().format(pnm=pnm,c=g.coin),
-	'missing_keys_error': """
-ERROR: a key file must be supplied for the following non-{pnm} address{{}}:\n    {{}}
-""".format(pnm=pnm).strip()
-}
+from .addr import AddrIdxList,KeyAddrList
+from .obj import MMGenAddrType,MMGenList
+from .wallet import Wallet,WalletUnenc,WalletEnc,MMGenWallet
+from .tx import MMGenTX
 
 saved_seeds = {}
 
@@ -67,21 +55,23 @@ def get_seed_for_seed_id(sid,infiles,saved_seeds):
 		else:
 			die(2,f'ERROR: No seed source found for Seed ID: {sid}')
 
-def generate_kals_for_mmgen_addrs(need_keys,infiles,saved_seeds):
+def generate_kals_for_mmgen_addrs(need_keys,infiles,saved_seeds,proto):
 	mmids = [e.mmid for e in need_keys]
 	sids = {i.sid for i in mmids}
-	vmsg('Need seed{}: {}'.format(suf(sids),' '.join(sids)))
-	d = MMGenList()
-	from .addr import KeyAddrList
-	for sid in sids:
-		# Returns only if seed is found
-		seed = get_seed_for_seed_id(sid,infiles,saved_seeds)
-		for t in MMGenAddrType.mmtypes:
-			idx_list = [i.idx for i in mmids if i.sid == sid and i.mmtype == t]
-			if idx_list:
-				addr_idxs = AddrIdxList(idx_list=idx_list)
-				d.append(KeyAddrList(seed=seed,addr_idxs=addr_idxs,mmtype=MMGenAddrType(t)))
-	return d
+	vmsg(f"Need seed{suf(sids)}: {' '.join(sids)}")
+	def gen_kals():
+		for sid in sids:
+			# Returns only if seed is found
+			seed = get_seed_for_seed_id(sid,infiles,saved_seeds)
+			for id_str in MMGenAddrType.mmtypes:
+				idx_list = [i.idx for i in mmids if i.sid == sid and i.mmtype == id_str]
+				if idx_list:
+					yield KeyAddrList(
+						proto     = proto,
+						seed      = seed,
+						addr_idxs = AddrIdxList(idx_list=idx_list),
+						mmtype    = MMGenAddrType(proto,id_str) )
+	return MMGenList(gen_kals())
 
 def add_keys(tx,src,infiles=None,saved_seeds=None,keyaddr_list=None):
 	need_keys = [e for e in getattr(tx,src) if e.mmid and not e.have_wif]
@@ -90,10 +80,10 @@ def add_keys(tx,src,infiles=None,saved_seeds=None,keyaddr_list=None):
 	desc,src_desc = (
 		('key-address file','From key-address file:') if keyaddr_list else
 		('seed(s)','Generated from seed:') )
-	qmsg(f'Checking {g.proj_name} -> {g.coin} address mappings for {src} (from {desc})')
+	qmsg(f'Checking {g.proj_name} -> {tx.proto.coin} address mappings for {src} (from {desc})')
 	d = (
 		MMGenList([keyaddr_list]) if keyaddr_list else
-		generate_kals_for_mmgen_addrs(need_keys,infiles,saved_seeds) )
+		generate_kals_for_mmgen_addrs(need_keys,infiles,saved_seeds,tx.proto) )
 	new_keys = []
 	for e in need_keys:
 		for kal in d:
@@ -105,7 +95,11 @@ def add_keys(tx,src,infiles=None,saved_seeds=None,keyaddr_list=None):
 						if src == 'inputs':
 							new_keys.append(f)
 					else:
-						die(3,wmsg['mapping_error'].format(src_desc,mmid,f.addr,'tx file:',e.mmid,e.addr))
+						die(3,fmt(f"""
+							{g.proj_name} -> {tx.proto.coin} address mappings differ!
+							{{src_desc:<23}} {{mmid}} -> {{f.addr}}
+							{{'tx file:':<23}} {{e.mmid}} -> {{e.addr}}
+							""").strip())
 	if new_keys:
 		vmsg(f'Added {len(new_keys)} wif key{suf(new_keys)} from {desc}')
 	return new_keys
@@ -114,7 +108,7 @@ def _pop_and_return(args,cmplist): # strips found args
 	return list(reversed([args.pop(args.index(a)) for a in reversed(args) if get_extension(a) in cmplist]))
 
 def get_tx_files(opt,args):
-	ret = _pop_and_return(args,[MMGenTX.raw_ext])
+	ret = _pop_and_return(args,[MMGenTX.Unsigned.ext])
 	if not ret:
 		die(1,'You must specify a raw transaction file!')
 	return ret
@@ -131,15 +125,15 @@ def get_seed_files(opt,args):
 		die(1,'You must specify a seed or key source!')
 	return ret
 
-def get_keyaddrlist(opt):
+def get_keyaddrlist(proto,opt):
 	if opt.mmgen_keys_from_file:
-		return KeyAddrList(opt.mmgen_keys_from_file)
+		return KeyAddrList(proto,opt.mmgen_keys_from_file)
 	return None
 
-def get_keylist(opt):
+def get_keylist(proto,opt):
 	if opt.keys_from_file:
 		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 coin daemon wallet dumps
+		kal = KeyAddrList(proto=proto,keylist=[m.split()[0] for m in l]) # accept coin daemon wallet dumps
 		kal.generate_addrs_from_keys()
 		return kal
 	return None
@@ -150,14 +144,17 @@ async def txsign(tx,seed_files,kl,kal,tx_num_str=''):
 	non_mmaddrs = tx.get_non_mmaddrs('inputs')
 
 	if non_mmaddrs:
-		if not kl:
-			die(2,'Transaction has non-{} inputs, but no flat key list is present'.format(g.proj_name))
+		tx.check_non_mmgen_inputs(caller='txsign',non_mmaddrs=non_mmaddrs)
 		tmp = KeyAddrList(
+			proto = tx.proto,
 			addrlist = non_mmaddrs )
 		tmp.add_wifs(kl)
 		m = tmp.list_missing('sec')
 		if m:
-			die(2,wmsg['missing_keys_error'].format(suf(m,'es'),'\n    '.join(m)))
+			die(2, fmt(f"""
+				ERROR: a key file must be supplied for the following non-{g.proj_name} address{suf(m,'es')}:
+				    {{}}
+				""".format('\n    '.join(m)),strip_char='\t').strip())
 		keys += tmp.data
 
 	if opt.mmgen_keys_from_file:
@@ -175,4 +172,4 @@ async def txsign(tx,seed_files,kl,kal,tx_num_str=''):
 	if extra_sids:
 		msg(f"Unused Seed ID{suf(extra_sids)}: {' '.join(extra_sids)}")
 
-	return await tx.sign(tx_num_str,keys) # returns True or False
+	return await tx.sign(tx_num_str,keys) # returns signed TX object or False

+ 26 - 51
mmgen/util.py

@@ -116,6 +116,11 @@ def fmt_list(l,fmt='dfl',indent=''):
 CUR_HIDE = '\033[?25l'
 CUR_SHOW = '\033[?25h'
 
+def exit_if_mswin(feature):
+	if g.platform == 'win':
+		m = capfirst(feature) + ' not supported on the MSWin / MSYS2 platform'
+		ydie(1,m)
+
 def warn_altcoins(coinsym,trust_level):
 	if trust_level > 3:
 		return
@@ -817,35 +822,6 @@ def do_license_msg(immed=False):
 			msg_r('\r')
 	msg('')
 
-# TODO: these belong in protocol.py
-def get_coin_daemon_cfg_fn():
-	# Use dirname() to remove 'bob' or 'alice' component
-	cfg_dir = os.path.dirname(g.data_dir) if g.proto.regtest else g.proto.daemon_data_dir
-	return os.path.join(
-		cfg_dir,
-		(g.proto.is_fork_of or g.proto.name).lower() + '.conf' )
-
-def get_coin_daemon_cfg_options(req_keys):
-
-	fn = get_coin_daemon_cfg_fn()
-	try:
-		lines = get_lines_from_file(fn,'',silent=not opt.verbose)
-	except:
-		vmsg(f'Warning: {fn!r} does not exist or is unreadable')
-		return dict((k,None) for k in req_keys)
-
-	def gen():
-		for key in req_keys:
-			val = None
-			for l in lines:
-				if l.startswith(key):
-					res = l.split('=',1)
-					if len(res) == 2 and not ' ' in res[1].strip():
-						val = res[1].strip()
-			yield (key,val)
-
-	return dict(gen())
-
 def format_par(s,indent=0,width=80,as_list=False):
 	words,lines = s.split(),[]
 	assert width >= indent + 4,'width must be >= indent + 4'
@@ -857,19 +833,29 @@ def format_par(s,indent=0,width=80,as_list=False):
 		lines.append(' '*indent + line)
 	return lines if as_list else '\n'.join(lines) + '\n'
 
-# module loading magic for tx.py and tw.py
-def altcoin_subclass(cls,mod_id,cls_name):
-	if cls.__name__ != cls_name:
+def altcoin_subclass(cls,proto,mod_dir):
+	"""
+	magic module loading and class retrieval
+	"""
+	from .protocol import CoinProtocol
+	if isinstance(proto,CoinProtocol.Bitcoin):
 		return cls
-	mod_dir = g.proto.base_coin.lower()
-	tname = 'Token' if g.token else ''
+
+	modname = f'mmgen.altcoins.{proto.base_coin.lower()}.{mod_dir}'
+
 	import importlib
-	modname = f'mmgen.altcoins.{mod_dir}.{mod_id}'
-	clsname = g.proto.mod_clsname + tname + cls_name
-	try:
+	if mod_dir == 'tx': # nested classes
+		outer_clsname,inner_clsname = (
+			proto.mod_clsname
+			+ ('Token' if proto.tokensym else '')
+			+ cls.__qualname__ ).split('.')
+		return getattr(getattr(importlib.import_module(modname),outer_clsname),inner_clsname)
+	else:
+		clsname = (
+			proto.mod_clsname
+			+ ('Token' if proto.tokensym else '')
+			+ cls.__name__ )
 		return getattr(importlib.import_module(modname),clsname)
-	except ImportError:
-		return cls
 
 # decorator for TrackingWallet
 def write_mode(orig_func):
@@ -880,13 +866,8 @@ def write_mode(orig_func):
 		return orig_func(self,*args,**kwargs)
 	return f
 
-def get_network_id(coin,testnet):
-	assert type(testnet) == bool
-	return coin.lower() + ('','_tn')[testnet]
-
-def run_session(callback,do_rpc_init=True,proto=None,backend=None):
+def run_session(callback,backend=None):
 	backend = backend or opt.rpc_backend
-	proto = proto or g.proto
 	import asyncio
 	async def do():
 		if backend == 'aiohttp':
@@ -895,16 +876,10 @@ def run_session(callback,do_rpc_init=True,proto=None,backend=None):
 				headers = { 'Content-Type': 'application/json' },
 				connector = aiohttp.TCPConnector(limit_per_host=g.aiohttp_rpc_queue_len),
 			) as g.session:
-				if do_rpc_init:
-					from .rpc import rpc_init
-					await rpc_init(proto=proto,backend=backend)
 				ret = await callback
 			g.session = None
 			return ret
 		else:
-			if do_rpc_init:
-				from .rpc import rpc_init
-				await rpc_init(proto=proto,backend=backend)
 			return await callback
 
 	# return asyncio.run(do()) # Python 3.7+

+ 6 - 2
scripts/create-token.py

@@ -52,9 +52,13 @@ opts_data = {
 }
 
 cmd_args = opts.init(opts_data)
-assert g.coin in ('ETH','ETC'),'--coin option must be set to ETH or ETC'
 
-if not len(cmd_args) == 1 or not is_coin_addr(cmd_args[0].lower()):
+from mmgen.protocol import init_proto_from_opts
+proto = init_proto_from_opts()
+
+assert proto.coin in ('ETH','ETC'),'--coin option must be set to ETH or ETC'
+
+if not len(cmd_args) == 1 or not is_coin_addr(proto,cmd_args[0].lower()):
 	opts.usage()
 
 owner_addr = '0x' + cmd_args[0]

+ 0 - 60
scripts/tx-btc2bch.py

@@ -1,60 +0,0 @@
-#!/usr/bin/env python3
-#
-# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
-# Copyright (C)2013-2020 The MMGen Project <mmgen@tuta.io>
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-"""
-tx-btc2bch: Convert MMGen transaction files from BTC to BCH format
-"""
-
-from mmgen.common import *
-
-opts_data = {
-	'text': {
-		'desc': """Convert {pnm} transaction files from BTC to BCH format""".format(pnm=g.proj_name),
-		'usage':'[opts] [mmgen transaction file]',
-		'options': """
--h, --help         Print this help message
---, --longhelp     Print help message for long options (common options)
--v, --verbose      Produce more verbose output
-"""
-	}
-}
-
-cmd_args = opts.init(opts_data)
-
-if g.coin != 'BTC':
-	die(1,"This program must be run with --coin set to 'BTC'")
-
-if len(cmd_args) != 1: opts.usage()
-
-import mmgen.tx
-tx = mmgen.tx.MMGenTX(cmd_args[0])
-
-if opt.verbose:
-	gmsg(f'Original transaction is in {g.coin} format')
-
-from mmgen.protocol import init_proto
-g.proto = init_proto('bch')
-
-if opt.verbose:
-	gmsg('Converting transaction to {} format'.format(g.coin))
-
-tx.inputs.convert_coin(verbose=opt.verbose)
-tx.outputs.convert_coin(verbose=opt.verbose)
-
-tx.desc = 'converted transaction'
-tx.write_to_file(ask_write=False,ask_overwrite=False)

+ 1 - 0
setup.py

@@ -113,6 +113,7 @@ setup(
 			'mmgen.exception',
 			'mmgen.filename',
 			'mmgen.globalvars',
+			'mmgen.help',
 			'mmgen.keccak',
 			'mmgen.led',
 			'mmgen.license',

+ 39 - 33
test/gentest.py

@@ -106,7 +106,7 @@ Supported external tools:
 			prog='test/gentest.py',
 			pnm=g.proj_name,
 			snum=rounds,
-			dn=g.proto.daemon_name)
+			dn=proto.daemon_name)
 	}
 }
 
@@ -117,6 +117,9 @@ cmd_args = opts.init(opts_data,add_opts=['exact_output','use_old_ed25519'])
 if not 1 <= len(cmd_args) <= 2:
 	opts.usage()
 
+from mmgen.protocol import init_proto_from_opts
+proto = init_proto_from_opts()
+
 from subprocess import run,PIPE,DEVNULL
 def get_cmd_output(cmd,input=None):
 	return run(cmd,input=input,stdout=PIPE,stderr=DEVNULL).stdout.decode().splitlines()
@@ -127,15 +130,15 @@ gtr = namedtuple('gen_tool_result',['wif','addr','vk'])
 class GenTool(object):
 
 	def run_tool(self,sec):
-		vcoin = 'BTC' if g.coin == 'BCH' else g.coin
+		vcoin = 'BTC' if proto.coin == 'BCH' else proto.coin
 		return self.run(sec,vcoin)
 
 class GenToolEthkey(GenTool):
 	desc = 'ethkey'
 	def __init__(self):
-		g.proto = init_proto('eth')
+		proto = init_proto('eth')
 		global addr_type
-		addr_type = MMGenAddrType('E')
+		addr_type = MMGenAddrType(proto,'E')
 
 	def run(self,sec,vcoin):
 		o = get_cmd_output(['ethkey','info',sec])
@@ -150,9 +153,9 @@ class GenToolKeyconv(GenTool):
 class GenToolZcash_mini(GenTool):
 	desc = 'zcash-mini'
 	def __init__(self):
-		g.proto = init_proto('zec')
+		proto = init_proto('zec')
 		global addr_type
-		addr_type = MMGenAddrType('Z')
+		addr_type = MMGenAddrType(proto,'Z')
 
 	def run(self,sec,vcoin):
 		o = get_cmd_output(['zcash-mini','-key','-simple'],input=(sec.wif+'\n').encode())
@@ -172,7 +175,7 @@ class GenToolPycoin(GenTool):
 		self.nfnc = network_for_netcode
 
 	def run(self,sec,vcoin):
-		if g.proto.testnet:
+		if proto.testnet:
 			vcoin = ci.external_tests['testnet']['pycoin'][vcoin]
 		network = self.nfnc(vcoin)
 		key = network.keys.private(secret_exponent=int(sec,16),is_compressed=addr_type.name != 'legacy')
@@ -200,10 +203,10 @@ class GenToolMoneropy(GenTool):
 			raise ImportError(m)
 
 		self.mpa = moneropy.account
-		g.proto = init_proto('xmr')
+		proto = init_proto('xmr')
 
 		global addr_type
-		addr_type = MMGenAddrType('M')
+		addr_type = MMGenAddrType(proto,'M')
 
 	def run(self,sec,vcoin):
 		sk_t,vk_t,addr_t = self.mpa.account_from_spend_key(sec) # VERY slow!
@@ -212,7 +215,7 @@ class GenToolMoneropy(GenTool):
 def get_tool(arg):
 
 	if arg not in ext_progs + ['ext']:
-		die(1,'{!r}: unsupported tool for network {}'.format(arg,g.proto.network))
+		die(1,'{!r}: unsupported tool for network {}'.format(arg,proto.network))
 
 	if opt.all:
 		if arg == 'ext':
@@ -220,9 +223,9 @@ def get_tool(arg):
 		return arg
 	else:
 		tool = ci.get_test_support(
-			g.coin,
+			proto.coin,
 			addr_type.name,
-			g.proto.network,
+			proto.network,
 			verbose = not opt.quiet,
 			tool = arg if arg in ext_progs else None )
 		if not tool:
@@ -251,12 +254,12 @@ def test_equal(desc,a_val,b_val,in_bytes,sec,wif,a_desc,b_desc):
 def gentool_test(kg_a,kg_b,ag,rounds):
 
 	m = "Comparing address generators '{A}' and '{B}' for {N} {c} ({n}), addrtype {a!r}"
-	e = ci.get_entry(g.coin,g.proto.network)
+	e = ci.get_entry(proto.coin,proto.network)
 	qmsg(green(m.format(
 		A = kg_a.desc,
 		B = kg_b.desc,
-		N = g.proto.network,
-		c = g.coin,
+		N = proto.network,
+		c = proto.coin,
 		n = e.name if e else '---',
 		a = addr_type.name )))
 
@@ -268,7 +271,7 @@ def gentool_test(kg_a,kg_b,ag,rounds):
 		if opt.verbose or time.time() - last_t >= 0.1:
 			qmsg_r('\rRound {}/{} '.format(i+1,trounds))
 			last_t = time.time()
-		sec = PrivKey(in_bytes,compressed=addr_type.compressed,pubkey_type=addr_type.pubkey_type)
+		sec = PrivKey(proto,in_bytes,compressed=addr_type.compressed,pubkey_type=addr_type.pubkey_type)
 		a_ph = kg_a.to_pubhex(sec)
 		a_addr = ag.to_addr(a_ph)
 		a_vk = None
@@ -311,7 +314,7 @@ def gentool_test(kg_a,kg_b,ag,rounds):
 
 def speed_test(kg,ag,rounds):
 	m = "Testing speed of address generator '{}' for coin {}"
-	qmsg(green(m.format(kg.desc,g.coin)))
+	qmsg(green(m.format(kg.desc,proto.coin)))
 	from struct import pack,unpack
 	seed = os.urandom(28)
 	qmsg('Incrementing key with each round')
@@ -323,7 +326,7 @@ def speed_test(kg,ag,rounds):
 		if time.time() - last_t >= 0.1:
 			qmsg_r('\rRound {}/{} '.format(i+1,rounds))
 			last_t = time.time()
-		sec = PrivKey(seed+pack('I',i),compressed=addr_type.compressed,pubkey_type=addr_type.pubkey_type)
+		sec = PrivKey(proto,seed+pack('I',i),compressed=addr_type.compressed,pubkey_type=addr_type.pubkey_type)
 		addr = ag.to_addr(kg.to_pubhex(sec))
 		vmsg('\nkey:  {}\naddr: {}\n'.format(sec.wif,addr))
 	qmsg_r('\rRound {}/{} '.format(i+1,rounds))
@@ -341,9 +344,9 @@ def dump_test(kg,ag,fh):
 	for count,(b_wif,b_addr) in enumerate(dump,1):
 		qmsg_r('\rKey {}/{} '.format(count,len(dump)))
 		try:
-			b_sec = PrivKey(wif=b_wif)
+			b_sec = PrivKey(proto,wif=b_wif)
 		except:
-			die(2,'\nInvalid {} WIF address in dump file: {}'.format(g.proto.network,b_wif))
+			die(2,'\nInvalid {} WIF address in dump file: {}'.format(proto.network,b_wif))
 		a_addr = ag.to_addr(kg.to_pubhex(b_sec))
 		vmsg('\nwif: {}\naddr: {}\n'.format(b_wif,b_addr))
 		tinfo = (bytes.fromhex(b_sec),b_sec,b_wif,kg.desc,fh.name)
@@ -366,12 +369,12 @@ def parse_arg1(arg,arg_id):
 	if arg_id == 'a':
 		if is_int(arg):
 			a_num = check_gen_num(arg)
-			return (KeyGenerator(addr_type,a_num),a_num)
+			return (KeyGenerator(proto,addr_type,a_num),a_num)
 		else:
 			die(1,m1)
 	elif arg_id == 'b':
 		if is_int(arg):
-			return KeyGenerator(addr_type,check_gen_num(arg))
+			return KeyGenerator(proto,addr_type,check_gen_num(arg))
 		elif arg in ext_progs + ['ext']:
 			return init_tool(get_tool(arg))
 		else:
@@ -395,8 +398,10 @@ from mmgen.altcoin import CoinInfo as ci
 from mmgen.obj import MMGenAddrType,PrivKey
 from mmgen.addr import KeyGenerator,AddrGenerator
 
-addr_type = MMGenAddrType(opt.type or g.proto.dfl_mmtype)
-ext_progs = list(ci.external_tests[g.proto.network])
+addr_type = MMGenAddrType(
+	proto = proto,
+	id_str = opt.type or proto.dfl_mmtype )
+ext_progs = list(ci.external_tests[proto.network])
 
 arg1 = cmd_args[0].split(':')
 if len(arg1) == 1:
@@ -413,7 +418,8 @@ if type(a) == type(b):
 
 arg2 = parse_arg2()
 
-ag = AddrGenerator(addr_type)
+if not opt.all:
+	ag = AddrGenerator(proto,addr_type)
 
 if not b and type(arg2) == int:
 	speed_test(a,ag,arg2)
@@ -422,18 +428,18 @@ elif not b and hasattr(arg2,'read'):
 elif a and b and type(arg2) == int:
 	if opt.all:
 		from mmgen.protocol import CoinProtocol,init_genonly_altcoins
-		init_genonly_altcoins(testnet=g.proto.testnet)
-		for coin in ci.external_tests[g.proto.network][b.desc]:
+		init_genonly_altcoins(testnet=proto.testnet)
+		for coin in ci.external_tests[proto.network][b.desc]:
 			if coin.lower() not in CoinProtocol.coins:
 #				ymsg('Coin {} not configured'.format(coin))
 				continue
-			g.proto = init_proto(coin)
-			if addr_type not in g.proto.mmtypes:
+			proto = init_proto(coin)
+			if addr_type not in proto.mmtypes:
 				continue
-			# g.proto has changed, so reinit kg and ag just to be on the safe side:
-			a = KeyGenerator(addr_type,a_num)
-			ag = AddrGenerator(addr_type)
-			b_chk = ci.get_test_support(g.coin,addr_type.name,g.proto.network,tool=b.desc,verbose=not opt.quiet)
+			# proto has changed, so reinit kg and ag
+			a = KeyGenerator(proto,addr_type,a_num)
+			ag = AddrGenerator(proto,addr_type)
+			b_chk = ci.get_test_support(proto.coin,addr_type.name,proto.network,tool=b.desc,verbose=not opt.quiet)
 			if b_chk == b.desc:
 				gentool_test(a,b,ag,arg2)
 	else:

+ 3 - 0
test/include/common.py

@@ -74,6 +74,9 @@ def getrandstr(num_chars,no_space=False):
 	if no_space: n,m = 94,33
 	return ''.join([chr(i%n+m) for i in list(os.urandom(num_chars))])
 
+def get_data_dir():
+	return os.path.join('test','data_dir' + ('','-α')[bool(os.getenv('MMGEN_DEBUG_UTF8'))])
+
 # Windows uses non-UTF8 encodings in filesystem, so use raw bytes here
 def cleandir(d,do_msg=False):
 	d_enc = d.encode()

+ 5 - 5
test/objattrtest.py

@@ -147,16 +147,16 @@ def test_object(test_data,objname):
 
 def do_loop():
 	import importlib
-	modname = 'test.objattrtest_py_d.oat_{}_{}'.format(g.coin.lower(),g.proto.network)
+	modname = f'test.objattrtest_py_d.oat_{proto.coin.lower()}_{proto.network}'
 	test_data = importlib.import_module(modname).tests
-	gmsg('Running immutable attribute tests for {} {}'.format(g.coin,g.proto.network))
+	gmsg(f'Running immutable attribute tests for {proto.coin} {proto.network}')
 
 	utests = cmd_args
 	for obj in test_data:
 		if utests and obj not in utests: continue
-		clr = blue if opt.verbose else nocolor
-		msg(clr('Testing {}'.format(obj)))
+		msg((blue if opt.verbose else nocolor)(f'Testing {obj}'))
 		test_object(test_data,obj)
 
-g.proto = init_proto(g.coin)
+from mmgen.protocol import init_proto_from_opts
+proto = init_proto_from_opts()
 do_loop()

+ 16 - 14
test/objattrtest_py_d/oat_btc_mainnet.py

@@ -9,14 +9,17 @@ objects
 """
 
 from .oat_common import *
+from mmgen.protocol import init_proto
+
+proto = init_proto('btc')
 
 sample_objs.update({
-	'PrivKey':   PrivKey(seed_bin,compressed=True,pubkey_type='std'),
-	'WifKey':    WifKey('5HwzecKMWD82ppJK3qMKpC7ohXXAwcyAN5VgdJ9PLFaAzpBG4sX'),
-	'CoinAddr':  CoinAddr('1111111111111111111114oLvT2'),
+	'PrivKey':   PrivKey(proto,seed_bin,compressed=True,pubkey_type='std'),
+	'WifKey':    WifKey(proto,'5HwzecKMWD82ppJK3qMKpC7ohXXAwcyAN5VgdJ9PLFaAzpBG4sX'),
+	'CoinAddr':  CoinAddr(proto,'1111111111111111111114oLvT2'),
 	'BTCAmt':    BTCAmt('0.01'),
-	'MMGenID':   MMGenID('F00F00BB:B:1'),
-	'TwMMGenID': TwMMGenID('F00F00BB:S:23'),
+	'MMGenID':   MMGenID(proto,'F00F00BB:B:1'),
+	'TwMMGenID': TwMMGenID(proto,'F00F00BB:S:23'),
 })
 
 tests = {
@@ -29,7 +32,7 @@ tests = {
 #		'viewkey':       (0b001, ViewKey),        # TODO
 #		'wallet_passwd': (0b001, WalletPassword), # TODO
 		},
-		[],
+		(proto,),
 		{}
 	),
 	'PasswordListEntry': atd({
@@ -38,7 +41,7 @@ tests = {
 		'label':  (0b101, TwComment),
 		'sec':    (0b001, PrivKey),
 		},
-		[],
+		(proto,),
 		{'passwd':'ΑlphaΩmega', 'idx':1 },
 	),
 	# obj.py
@@ -46,7 +49,7 @@ tests = {
 		'compressed': (0b001, bool),
 		'wif':        (0b001, WifKey),
 		},
-		[seed_bin],
+		(proto,seed_bin),
 		{'compressed':True, 'pubkey_type':'std'},
 	),
 	'MMGenAddrType': atd({
@@ -59,7 +62,7 @@ tests = {
 		'extra_attrs': (0b001, tuple),
 		'desc':        (0b001, str),
 		},
-		['S'],
+		(proto,'S'),
 		{},
 	),
 	# seed.py
@@ -118,7 +121,7 @@ tests = {
 		'scriptPubKey': (0b001, HexStr),
 		'skip':         (0b101, str),
 		},
-		[],
+		(proto,),
 		{
 			'amt':BTCAmt('0.01'),
 			'twmmid':'F00F00BB:B:17',
@@ -126,7 +129,6 @@ tests = {
 			'confs': 100000,
 			'scriptPubKey':'ff',
 		},
-
 	),
 	# tx.py
 	'MMGenTxInput': atd({
@@ -141,7 +143,7 @@ tests = {
 		'scriptPubKey': (0b001, HexStr),
 		'sequence':     (0b001, int),
 		},
-		[],
+		(proto,),
 		{ 'amt':BTCAmt('0.01'), 'addr':sample_objs['CoinAddr'] },
 	),
 	'MMGenTxOutput': atd({
@@ -155,9 +157,9 @@ tests = {
 		'have_wif':     (0b011, bool),
 		'is_chg':       (0b001, bool),
 		},
-		[],
+		(proto,),
 		{ 'amt':BTCAmt('0.01'), 'addr':sample_objs['CoinAddr'] },
 	),
 }
 
-tests['MMGenPasswordType'] = atd(tests['MMGenAddrType'].attrs, ['P'], {})
+tests['MMGenPasswordType'] = atd(tests['MMGenAddrType'].attrs, [proto,'P'], {})

+ 16 - 5
test/objtest.py

@@ -55,8 +55,10 @@ def run_test(test,arg,input_data):
 	arg_copy = arg
 	kwargs = {'on_fail':'silent'} if opt.silent else {'on_fail':'die'}
 	ret_chk = arg
+	ret_idx = None
 	exc_type = None
-	if input_data == 'good' and type(arg) == tuple: arg,ret_chk = arg
+	if input_data == 'good' and type(arg) == tuple:
+		arg,ret_chk = arg
 	if type(arg) == dict: # pass one arg + kwargs to constructor
 		arg_copy = arg.copy()
 		if 'arg' in arg:
@@ -70,6 +72,10 @@ def run_test(test,arg,input_data):
 			ret_chk = arg['ret']
 			del arg['ret']
 			del arg_copy['ret']
+		if 'ret_idx' in arg:
+			ret_idx = arg['ret_idx']
+			del arg['ret_idx']
+			del arg_copy['ret_idx']
 		if 'ExcType' in arg:
 			exc_type = arg['ExcType']
 			del arg['ExcType']
@@ -94,8 +100,11 @@ def run_test(test,arg,input_data):
 			raise UserWarning("Non-'None' return value {} with bad input data".format(repr(ret)))
 		if opt.silent and input_data=='good' and ret==bad_ret:
 			raise UserWarning("'None' returned with good input data")
-		if input_data=='good' and ret != ret_chk and repr(ret) != repr(ret_chk):
-			raise UserWarning("Return value ({!r}) doesn't match expected value ({!r})".format(ret,ret_chk))
+		if input_data=='good':
+			if ret_idx:
+				ret_chk = arg[list(arg.keys())[ret_idx]].encode()
+			if ret != ret_chk and repr(ret) != repr(ret_chk):
+				raise UserWarning("Return value ({!r}) doesn't match expected value ({!r})".format(ret,ret_chk))
 		if not opt.super_silent:
 			try: ret_disp = ret.decode()
 			except: ret_disp = ret
@@ -119,9 +128,9 @@ def run_test(test,arg,input_data):
 
 def do_loop():
 	import importlib
-	modname = 'test.objtest_py_d.ot_{}_{}'.format(g.coin.lower(),g.proto.network)
+	modname = f'test.objtest_py_d.ot_{proto.coin.lower()}_{proto.network}'
 	test_data = importlib.import_module(modname).tests
-	gmsg('Running data object tests for {} {}'.format(g.coin,g.proto.network))
+	gmsg(f'Running data object tests for {proto.coin} {proto.network}')
 
 	clr = None
 	utests = cmd_args
@@ -136,4 +145,6 @@ def do_loop():
 			for arg in test_data[test][k]:
 				run_test(test,arg,input_data=k)
 
+from mmgen.protocol import init_proto_from_opts
+proto = init_proto_from_opts()
 do_loop()

+ 128 - 38
test/objtest_py_d/ot_btc_mainnet.py

@@ -11,6 +11,10 @@ from mmgen.obj import *
 from mmgen.seed import *
 from .ot_common import *
 
+from mmgen.protocol import init_proto
+proto = init_proto('btc')
+tw_pfx = proto.base_coin.lower() + ':'
+
 ssm = str(SeedShareCount.max_val)
 
 tests = {
@@ -82,8 +86,15 @@ tests = {
 		)
 	},
 	'CoinAddr': {
-		'bad':  (1,'x','я'),
-		'good': ('1MjjELEy6EJwk8fSNfpS8b5teFRo4X5fZr','32GiSWo9zJQgkCmjAaLRrbPwXhKry2jHhj'),
+		'good':  (
+			{'addr':'1MjjELEy6EJwk8fSNfpS8b5teFRo4X5fZr', 'proto':proto},
+			{'addr':'32GiSWo9zJQgkCmjAaLRrbPwXhKry2jHhj', 'proto':proto},
+		),
+		'bad':  (
+			{'addr':1,   'proto':proto},
+			{'addr':'x', 'proto':proto},
+			{'addr':'я', 'proto':proto},
+		),
 	},
 	'SeedID': {
 		'bad':  (
@@ -93,7 +104,8 @@ tests = {
 			{'sid':1},
 			{'sid':'F00BAA123'},
 			{'sid':'f00baa12'},
-			'я',r32,'abc'),
+			'я',r32,'abc'
+			),
 		'good': (({'sid':'F00BAA12'},'F00BAA12'),(Seed(r16),Seed(r16).sid))
 	},
 	'SubSeedIdx': {
@@ -101,12 +113,41 @@ tests = {
 		'good': (('1','1L'),('1s','1S'),'20S','30L',('300l','300L'),('200','200L'),str(SubSeedIdxRange.max_idx)+'S')
 	},
 	'MMGenID': {
-		'bad':  ('x',1,'f00f00f','a:b','x:L:3','F00BAA12:0','F00BAA12:Z:99'),
-		'good': (('F00BAA12:99','F00BAA12:L:99'),'F00BAA12:L:99','F00BAA12:S:99')
+		'bad':  (
+			{'id_str':'x',             'proto':proto},
+			{'id_str':1,               'proto':proto},
+			{'id_str':'f00f00f',       'proto':proto},
+			{'id_str':'a:b',           'proto':proto},
+			{'id_str':'x:L:3',         'proto':proto},
+			{'id_str':'F00BAA12',      'proto':proto},
+			{'id_str':'F00BAA12:Z:99', 'proto':proto},
+		),
+		'good':  (
+			{'id_str':'F00BAA12:99',   'proto':proto, 'ret':'F00BAA12:L:99'},
+			{'id_str':'F00BAA12:L:99', 'proto':proto},
+			{'id_str':'F00BAA12:S:99', 'proto':proto},
+		),
 	},
 	'TwMMGenID': {
-		'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')
+		'bad':  (
+			{'id_str':'x',             'proto':proto},
+			{'id_str':'я',             'proto':proto},
+			{'id_str':'я:я',           'proto':proto},
+			{'id_str':1,               'proto':proto},
+			{'id_str':'f00f00f',       'proto':proto},
+			{'id_str':'a:b',           'proto':proto},
+			{'id_str':'x:L:3',         'proto':proto},
+			{'id_str':'F00BAA12:0',    'proto':proto},
+			{'id_str':'F00BAA12:Z:99', 'proto':proto},
+			{'id_str':tw_pfx,          'proto':proto},
+			{'id_str':tw_pfx+'я',      'proto':proto},
+		),
+		'good':  (
+			{'id_str':tw_pfx+'x',           'proto':proto},
+			{'id_str':'F00BAA12:99',        'proto':proto, 'ret':'F00BAA12:L:99'},
+			{'id_str':'F00BAA12:L:99',      'proto':proto},
+			{'id_str':'F00BAA12:S:9999999', 'proto':proto},
+		),
 	},
 	'TwLabel': {
 		'bad':  ('x x','x я','я:я',1,'f00f00f','a:b','x:L:3','F00BAA12:0 x',
@@ -120,6 +161,30 @@ tests = {
 			'F00BAA12:S:9999999 comment',
 			tw_pfx+'x comment')
 	},
+	'TwLabel': {
+		'bad':  (
+			{'text':'x x',           'proto':proto},
+			{'text':'x я',           'proto':proto},
+			{'text':'я:я',           'proto':proto},
+			{'text':1,               'proto':proto},
+			{'text':'f00f00f',       'proto':proto},
+			{'text':'a:b',           'proto':proto},
+			{'text':'x:L:3',         'proto':proto},
+			{'text':'F00BAA12:0 x',  'proto':proto},
+			{'text':'F00BAA12:Z:99', 'proto':proto},
+			{'text':tw_pfx+' x',     'proto':proto},
+			{'text':tw_pfx+'я x',    'proto':proto},
+			{'text':utf8_ctrl[:40],  'proto':proto},
+			{'text':'F00BAA12:S:1 '+ utf8_ctrl[:40], 'proto':proto, 'on_fail':'raise','ExcType':'BadTwComment'},
+		),
+		'good':  (
+			{'text':'F00BAA12:99 a comment',            'proto':proto, 'ret':'F00BAA12:L:99 a comment'},
+			{'text':'F00BAA12:L:99 a comment',          'proto':proto},
+			{'text': 'F00BAA12:L:99 comment (UTF-8) α', 'proto':proto},
+			{'text':'F00BAA12:S:9999999 comment',       'proto':proto},
+			{'text':tw_pfx+'x comment',                 'proto':proto},
+		),
+	},
 	'MMGenTxID': {
 		'bad':  (1,[],'\0','\1','я','g','gg','FF','f00','F00F0012'),
 		'good': ('DEADBE','F00BAA')
@@ -129,9 +194,23 @@ tests = {
 		'good': (r32.hex(),)
 	},
 	'WifKey': {
-		'bad':  (1,[],'\0','\1','я','g','gg','FF','f00',r16.hex(),'2MspvWFjBbkv2wzQGqhxJUYPCk3Y2jMaxLN'),
-		'good': ('5KXEpVzjWreTcQoG5hX357s1969MUKNLuSfcszF6yu84kpsNZKb',
-				'KwWr9rDh8KK5TtDa3HLChEvQXNYcUXpwhRFUPc5uSNnMtqNKLFhk'),
+		'bad': (
+			{'proto':proto, 'wif':1},
+			{'proto':proto, 'wif':[]},
+			{'proto':proto, 'wif':'\0'},
+			{'proto':proto, 'wif':'\1'},
+			{'proto':proto, 'wif':'я'},
+			{'proto':proto, 'wif':'g'},
+			{'proto':proto, 'wif':'gg'},
+			{'proto':proto, 'wif':'FF'},
+			{'proto':proto, 'wif':'f00'},
+			{'proto':proto, 'wif':r16.hex()},
+			{'proto':proto, 'wif':'2MspvWFjBbkv2wzQGqhxJUYPCk3Y2jMaxLN'},
+		),
+		'good': (
+			{'proto':proto, 'wif':'5KXEpVzjWreTcQoG5hX357s1969MUKNLuSfcszF6yu84kpsNZKb',  'ret_idx':1},
+			{'proto':proto, 'wif':'KwWr9rDh8KK5TtDa3HLChEvQXNYcUXpwhRFUPc5uSNnMtqNKLFhk', 'ret_idx':1},
+		)
 	},
 	'PubKey': {
 		'bad':  ({'arg':1,'compressed':False},{'arg':'F00BAA12','compressed':False},),
@@ -139,24 +218,24 @@ tests = {
 	},
 	'PrivKey': {
 		'bad': (
-			{'wif':1},
-			{'wif':'1'},
-			{'wif':'cMsqcmDYZP1LdKgqRh9L4ZRU9br28yvdmTPwW2YQwVSN9aQiMAoR'},
-			{'s':r32,'wif':'5KXEpVzjWreTcQoG5hX357s1969MUKNLuSfcszF6yu84kpsNZKb'},
-			{'pubkey_type':'std'},
-			{'s':r32},
-			{'s':r32,'compressed':'yes'},
-			{'s':r32,'compressed':'yes','pubkey_type':'std'},
-			{'s':r32,'compressed':True,'pubkey_type':'nonstd'},
-			{'s':r32+b'x','compressed':True,'pubkey_type':'std'}
+			{'proto':proto, 'wif':1},
+			{'proto':proto, 'wif':'1'},
+			{'proto':proto, 'wif':'cMsqcmDYZP1LdKgqRh9L4ZRU9br28yvdmTPwW2YQwVSN9aQiMAoR'},
+			{'proto':proto, 's':r32,'wif':'5KXEpVzjWreTcQoG5hX357s1969MUKNLuSfcszF6yu84kpsNZKb'},
+			{'proto':proto, 'pubkey_type':'std'},
+			{'proto':proto, 's':r32},
+			{'proto':proto, 's':r32,'compressed':'yes'},
+			{'proto':proto, 's':r32,'compressed':'yes','pubkey_type':'std'},
+			{'proto':proto, 's':r32,'compressed':True,'pubkey_type':'nonstd'},
+			{'proto':proto, 's':r32+b'x','compressed':True,'pubkey_type':'std'}
 		),
 		'good': (
-			{'wif':'5KXEpVzjWreTcQoG5hX357s1969MUKNLuSfcszF6yu84kpsNZKb',
+			{'proto':proto, 'wif':'5KXEpVzjWreTcQoG5hX357s1969MUKNLuSfcszF6yu84kpsNZKb',
 			'ret':'e0aef965b905a2fedf907151df8e0a6bac832aa697801c51f58bd2ecb4fd381c'},
-			{'wif':'KwWr9rDh8KK5TtDa3HLChEvQXNYcUXpwhRFUPc5uSNnMtqNKLFhk',
+			{'proto':proto, 'wif':'KwWr9rDh8KK5TtDa3HLChEvQXNYcUXpwhRFUPc5uSNnMtqNKLFhk',
 			'ret':'08d0ed83b64b68d56fa064be48e2385060ed205be2b1e63cd56d218038c3a05f'},
-			{'s':r32,'compressed':False,'pubkey_type':'std','ret':r32.hex()},
-			{'s':r32,'compressed':True,'pubkey_type':'std','ret':r32.hex()}
+			{'proto':proto, 's':r32,'compressed':False,'pubkey_type':'std','ret':r32.hex()},
+			{'proto':proto, 's':r32,'compressed':True,'pubkey_type':'std','ret':r32.hex()}
 		)
 	},
 	'AddrListID': { # a rather pointless test, but do it anyway
@@ -164,8 +243,8 @@ tests = {
 			{'sid':SeedID(sid='F00BAA12'),'mmtype':'Z','ret':'F00BAA12:Z'},
 		),
 		'good':  (
-			{'sid':SeedID(sid='F00BAA12'),'mmtype':MMGenAddrType('S'),'ret':'F00BAA12:S'},
-			{'sid':SeedID(sid='F00BAA12'),'mmtype':MMGenAddrType('L'),'ret':'F00BAA12:L'},
+			{'sid':SeedID(sid='F00BAA12'),'mmtype':proto.addr_type(id_str='S'),'ret':'F00BAA12:S'},
+			{'sid':SeedID(sid='F00BAA12'),'mmtype':proto.addr_type(id_str='L'),'ret':'F00BAA12:L'},
 		)
 	},
 	'MMGenWalletLabel': {
@@ -193,23 +272,34 @@ tests = {
 		'good': ('qwerty@яяя',)
 	},
 	'MMGenAddrType': {
-		'bad': ('U','z','xx',1,'dogecoin'),
+		'bad':  (
+			{'proto':proto, 'id_str':'U',        'ret':'L'},
+			{'proto':proto, 'id_str':'z',        'ret':'L'},
+			{'proto':proto, 'id_str':'xx',       'ret':'C'},
+			{'proto':proto, 'id_str':'dogecoin', 'ret':'C'},
+		),
 		'good':  (
-			{'s':'legacy','ret':'L'},
-			{'s':'L','ret':'L'},
-			{'s':'compressed','ret':'C'},
-			{'s':'C','ret':'C'},
-			{'s':'segwit','ret':'S'},
-			{'s':'S','ret':'S'},
-			{'s':'bech32','ret':'B'},
-			{'s':'B','ret':'B'}
+			{'proto':proto, 'id_str':'legacy',    'ret':'L'},
+			{'proto':proto, 'id_str':'L',         'ret':'L'},
+			{'proto':proto, 'id_str':'compressed','ret':'C'},
+			{'proto':proto, 'id_str':'C',         'ret':'C'},
+			{'proto':proto, 'id_str':'segwit',    'ret':'S'},
+			{'proto':proto, 'id_str':'S',         'ret':'S'},
+			{'proto':proto, 'id_str':'bech32',    'ret':'B'},
+			{'proto':proto, 'id_str':'B',         'ret':'B'}
 		)
 	},
 	'MMGenPasswordType': {
-		'bad': ('U','z','я',1,'passw0rd'),
+		'bad':  (
+			{'proto':proto, 'id_str':'U',        'ret':'L'},
+			{'proto':proto, 'id_str':'z',        'ret':'L'},
+			{'proto':proto, 'id_str':'я',        'ret':'C'},
+			{'proto':proto, 'id_str':1,          'ret':'C'},
+			{'proto':proto, 'id_str':'passw0rd', 'ret':'C'},
+		),
 		'good': (
-			{'s':'password','ret':'P'},
-			{'s':'P','ret':'P'},
+			{'proto':proto, 'id_str':'password', 'ret':'P'},
+			{'proto':proto, 'id_str':'P',        'ret':'P'},
 		)
 	},
 	'SeedSplitSpecifier': {

+ 43 - 19
test/objtest_py_d/ot_btc_testnet.py

@@ -10,36 +10,60 @@ test.objtest_py_d.ot_btc_testnet: BTC testnet test vectors for MMGen data object
 from mmgen.obj import *
 from .ot_common import *
 
+from mmgen.protocol import init_proto
+proto = init_proto('btc',network='testnet')
+
 tests = {
 	'CoinAddr': {
-		'bad':  (1,'x','я'),
-		'good': ('n2FgXPKwuFkCXF946EnoxWJDWF2VwQ6q8J','2MspvWFjBbkv2wzQGqhxJUYPCk3Y2jMaxLN'),
+		'bad':  (
+			{'addr':1,   'proto':proto},
+			{'addr':'x', 'proto':proto},
+			{'addr':'я', 'proto':proto},
+		),
+		'good':  (
+			{'addr':'n2FgXPKwuFkCXF946EnoxWJDWF2VwQ6q8J', 'proto':proto},
+			{'addr':'2MspvWFjBbkv2wzQGqhxJUYPCk3Y2jMaxLN','proto':proto},
+		),
 	},
 	'WifKey': {
-		'bad':  (1,[],'\0','\1','я','g','gg','FF','f00',r16.hex(),'2MspvWFjBbkv2wzQGqhxJUYPCk3Y2jMaxLN'),
-		'good': ('93HsQEpH75ibaUJYi3QwwiQxnkW4dUuYFPXZxcbcKds7XrqHkY6',
-				'cMsqcmDYZP1LdKgqRh9L4ZRU9br28yvdmTPwW2YQwVSN9aQiMAoR'),
+		'bad': (
+			{'proto':proto, 'wif':1},
+			{'proto':proto, 'wif':[]},
+			{'proto':proto, 'wif':'\0'},
+			{'proto':proto, 'wif':'\1'},
+			{'proto':proto, 'wif':'я'},
+			{'proto':proto, 'wif':'g'},
+			{'proto':proto, 'wif':'gg'},
+			{'proto':proto, 'wif':'FF'},
+			{'proto':proto, 'wif':'f00'},
+			{'proto':proto, 'wif':r16.hex()},
+			{'proto':proto, 'wif':'2MspvWFjBbkv2wzQGqhxJUYPCk3Y2jMaxLN'},
+		),
+		'good': (
+			{'proto':proto, 'wif':'93HsQEpH75ibaUJYi3QwwiQxnkW4dUuYFPXZxcbcKds7XrqHkY6',  'ret_idx':1},
+			{'proto':proto, 'wif':'cMsqcmDYZP1LdKgqRh9L4ZRU9br28yvdmTPwW2YQwVSN9aQiMAoR', 'ret_idx':1},
+		)
 	},
 	'PrivKey': {
 		'bad': (
-			{'wif':1},
-			{'wif':'1'},
-			{'wif':'5KXEpVzjWreTcQoG5hX357s1969MUKNLuSfcszF6yu84kpsNZKb'},
-			{'s':r32,'wif':'5KXEpVzjWreTcQoG5hX357s1969MUKNLuSfcszF6yu84kpsNZKb'},
-			{'pubkey_type':'std'},
-			{'s':r32},
-			{'s':r32,'compressed':'yes'},
-			{'s':r32,'compressed':'yes','pubkey_type':'std'},
-			{'s':r32,'compressed':True,'pubkey_type':'nonstd'},
-			{'s':r32+b'x','compressed':True,'pubkey_type':'std'}
+			{'proto':proto, 'wif':1},
+			{'proto':proto, 'wif':'1'},
+			{'proto':proto, 'wif':'5KXEpVzjWreTcQoG5hX357s1969MUKNLuSfcszF6yu84kpsNZKb'},
+			{'proto':proto, 's':r32,'wif':'5KXEpVzjWreTcQoG5hX357s1969MUKNLuSfcszF6yu84kpsNZKb'},
+			{'proto':proto, 'pubkey_type':'std'},
+			{'proto':proto, 's':r32},
+			{'proto':proto, 's':r32,'compressed':'yes'},
+			{'proto':proto, 's':r32,'compressed':'yes','pubkey_type':'std'},
+			{'proto':proto, 's':r32,'compressed':True,'pubkey_type':'nonstd'},
+			{'proto':proto, 's':r32+b'x','compressed':True,'pubkey_type':'std'}
 		),
 		'good': (
-			{'wif':'93HsQEpH75ibaUJYi3QwwiQxnkW4dUuYFPXZxcbcKds7XrqHkY6',
+			{'proto':proto, 'wif':'93HsQEpH75ibaUJYi3QwwiQxnkW4dUuYFPXZxcbcKds7XrqHkY6',
 			'ret':'e0aef965b905a2fedf907151df8e0a6bac832aa697801c51f58bd2ecb4fd381c'},
-			{'wif':'cMsqcmDYZP1LdKgqRh9L4ZRU9br28yvdmTPwW2YQwVSN9aQiMAoR',
+			{'proto':proto, 'wif':'cMsqcmDYZP1LdKgqRh9L4ZRU9br28yvdmTPwW2YQwVSN9aQiMAoR',
 			'ret':'08d0ed83b64b68d56fa064be48e2385060ed205be2b1e63cd56d218038c3a05f'},
-			{'s':r32,'compressed':False,'pubkey_type':'std','ret':r32.hex()},
-			{'s':r32,'compressed':True,'pubkey_type':'std','ret':r32.hex()}
+			{'proto':proto, 's':r32,'compressed':False,'pubkey_type':'std','ret':r32.hex()},
+			{'proto':proto, 's':r32,'compressed':True,'pubkey_type':'std','ret':r32.hex()}
 		),
 	},
 }

+ 0 - 1
test/objtest_py_d/ot_common.py

@@ -12,4 +12,3 @@ from mmgen.globalvars import g
 from ..include.common import *
 
 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()+':'

+ 43 - 19
test/objtest_py_d/ot_ltc_mainnet.py

@@ -10,40 +10,64 @@ test.objtest_py_d.ot_ltc_mainnet: LTC mainnet test vectors for MMGen data object
 from mmgen.obj import *
 from .ot_common import *
 
+from mmgen.protocol import init_proto
+proto = init_proto('ltc')
+
 tests = {
 	'LTCAmt': {
 		'bad':  ('-3.2','0.123456789',123,'123L','88000000',80999999.12345678),
 		'good': (('80999999.12345678',Decimal('80999999.12345678')),)
 	},
 	'CoinAddr': {
-		'bad':  (1,'x','я'),
-		'good': ('LXYx4j8PDGE8GEwDFnEQhcLyHFGsRxSJwt','MEnuCzUGHaQx9fK5WYvLwR1NK4SAo8HmSr'),
+		'bad':  (
+			{'addr':1,   'proto':proto},
+			{'addr':'x', 'proto':proto},
+			{'addr':'я', 'proto':proto},
+		),
+		'good':  (
+			{'addr':'LXYx4j8PDGE8GEwDFnEQhcLyHFGsRxSJwt', 'proto':proto},
+			{'addr':'MEnuCzUGHaQx9fK5WYvLwR1NK4SAo8HmSr', 'proto':proto},
+		),
 	},
 	'WifKey': {
-		'bad':  (1,[],'\0','\1','я','g','gg','FF','f00',r16.hex(),'2MspvWFjBbkv2wzQGqhxJUYPCk3Y2jMaxLN'),
-		'good': ('6udBAGS6B9RfGyvEQDkVDsWy3Kqv9eTULqtEfVkJtTJyHdLvojw',
-				'T7kCSp5E71jzV2zEJW4q5qU1SMB5CSz8D9VByxMBkamv1uM3Jjca'),
+		'bad': (
+			{'proto':proto, 'wif':1},
+			{'proto':proto, 'wif':[]},
+			{'proto':proto, 'wif':'\0'},
+			{'proto':proto, 'wif':'\1'},
+			{'proto':proto, 'wif':'я'},
+			{'proto':proto, 'wif':'g'},
+			{'proto':proto, 'wif':'gg'},
+			{'proto':proto, 'wif':'FF'},
+			{'proto':proto, 'wif':'f00'},
+			{'proto':proto, 'wif':r16.hex()},
+			{'proto':proto, 'wif':'2MspvWFjBbkv2wzQGqhxJUYPCk3Y2jMaxLN'},
+		),
+		'good': (
+			{'proto':proto, 'wif':'6udBAGS6B9RfGyvEQDkVDsWy3Kqv9eTULqtEfVkJtTJyHdLvojw',  'ret_idx':1},
+			{'proto':proto, 'wif':'T7kCSp5E71jzV2zEJW4q5qU1SMB5CSz8D9VByxMBkamv1uM3Jjca', 'ret_idx':1},
+		)
 	},
 	'PrivKey': {
 		'bad': (
-			{'wif':1},
-			{'wif':'1'},
-			{'wif':'cMsqcmDYZP1LdKgqRh9L4ZRU9br28yvdmTPwW2YQwVSN9aQiMAoR'},
-			{'s':r32,'wif':'6ufJhtQQiRYA3w2QvDuXNXuLgPFp15i3HR1Wp8An2mx1JnhhJAh'},
-			{'pubkey_type':'std'},
-			{'s':r32},
-			{'s':r32,'compressed':'yes'},
-			{'s':r32,'compressed':'yes','pubkey_type':'std'},
-			{'s':r32,'compressed':True,'pubkey_type':'nonstd'},
-			{'s':r32+b'x','compressed':True,'pubkey_type':'std'}
+			{'proto':proto, 'wif':1},
+			{'proto':proto, 'wif':'1'},
+			{'proto':proto, 'wif':'cMsqcmDYZP1LdKgqRh9L4ZRU9br28yvdmTPwW2YQwVSN9aQiMAoR'},
+			{'proto':proto, 's':r32,'wif':'6ufJhtQQiRYA3w2QvDuXNXuLgPFp15i3HR1Wp8An2mx1JnhhJAh'},
+			{'proto':proto, 'pubkey_type':'std'},
+			{'proto':proto, 's':r32},
+			{'proto':proto, 's':r32,'compressed':'yes'},
+			{'proto':proto, 's':r32,'compressed':'yes','pubkey_type':'std'},
+			{'proto':proto, 's':r32,'compressed':True,'pubkey_type':'nonstd'},
+			{'proto':proto, 's':r32+b'x','compressed':True,'pubkey_type':'std'}
 		),
 		'good': (
-			{'wif':'6ufJhtQQiRYA3w2QvDuXNXuLgPFp15i3HR1Wp8An2mx1JnhhJAh',
+			{'proto':proto, 'wif':'6ufJhtQQiRYA3w2QvDuXNXuLgPFp15i3HR1Wp8An2mx1JnhhJAh',
 			'ret':'470a974ffca9fca1299b706b09142077bea3acbab6d6480b87dbba79d5fd279b'},
-			{'wif':'T41Fm7J3mtZLKYPMCLVSFARz4QF8nvSDhLAfW97Ds56Zm9hRJgn8',
+			{'proto':proto, 'wif':'T41Fm7J3mtZLKYPMCLVSFARz4QF8nvSDhLAfW97Ds56Zm9hRJgn8',
 			'ret':'1c6feab55a4c3b4ad1823d4ecacd1565c64228c01828cf44fb4db1e2d82c3d56'},
-			{'s':r32,'compressed':False,'pubkey_type':'std','ret':r32.hex()},
-			{'s':r32,'compressed':True,'pubkey_type':'std','ret':r32.hex()}
+			{'proto':proto, 's':r32,'compressed':False,'pubkey_type':'std','ret':r32.hex()},
+			{'proto':proto, 's':r32,'compressed':True,'pubkey_type':'std','ret':r32.hex()}
 		)
 	},
 }

+ 43 - 19
test/objtest_py_d/ot_ltc_testnet.py

@@ -10,36 +10,60 @@ test.objtest_py_d.ot_ltc_testnet: LTC testnet test vectors for MMGen data object
 from mmgen.obj import *
 from .ot_common import *
 
+from mmgen.protocol import init_proto
+proto = init_proto('ltc',network='testnet')
+
 tests = {
 	'CoinAddr': {
-		'bad':  (1,'x','я'),
-		'good': ('n2D3joAy3yE5fqxUeCp38X6uPUcVn7EFw9','QN59YbnHsPQcbKWSq9PmTpjrhBnHGQqRmf')
+		'bad':  (
+			{'addr':1,   'proto':proto},
+			{'addr':'x', 'proto':proto},
+			{'addr':'я', 'proto':proto},
+		),
+		'good':  (
+			{'addr':'n2D3joAy3yE5fqxUeCp38X6uPUcVn7EFw9', 'proto':proto},
+			{'addr':'QN59YbnHsPQcbKWSq9PmTpjrhBnHGQqRmf', 'proto':proto},
+		),
 	},
 	'WifKey': {
-		'bad':  (1,[],'\0','\1','я','g','gg','FF','f00',r16.hex(),'2MspvWFjBbkv2wzQGqhxJUYPCk3Y2jMaxLN'),
-		'good': ('936Fd4qs3Zy2ZiYHH7vZ3UpT23KtCAiGiG2xBTkjHo7jE9aWA2f',
-				'cQY3EumdaSNuttvDSUuPdiMYLyw8aVmYfFqxo9kdPuWbJBN4Ny66')
+		'bad': (
+			{'proto':proto, 'wif':1},
+			{'proto':proto, 'wif':[]},
+			{'proto':proto, 'wif':'\0'},
+			{'proto':proto, 'wif':'\1'},
+			{'proto':proto, 'wif':'я'},
+			{'proto':proto, 'wif':'g'},
+			{'proto':proto, 'wif':'gg'},
+			{'proto':proto, 'wif':'FF'},
+			{'proto':proto, 'wif':'f00'},
+			{'proto':proto, 'wif':r16.hex()},
+			{'proto':proto, 'wif':'2MspvWFjBbkv2wzQGqhxJUYPCk3Y2jMaxLN'},
+		),
+		'good': (
+			{'proto':proto, 'wif':'936Fd4qs3Zy2ZiYHH7vZ3UpT23KtCAiGiG2xBTkjHo7jE9aWA2f',  'ret_idx':1},
+			{'proto':proto, 'wif':'cQY3EumdaSNuttvDSUuPdiMYLyw8aVmYfFqxo9kdPuWbJBN4Ny66', 'ret_idx':1},
+		)
 	},
 	'PrivKey': {
 		'bad': (
-			{'wif':1},
-			{'wif':'1'},
-			{'wif':'5KXEpVzjWreTcQoG5hX357s1969MUKNLuSfcszF6yu84kpsNZKb'},
-			{'s':r32,'wif':'5KXEpVzjWreTcQoG5hX357s1969MUKNLuSfcszF6yu84kpsNZKb'},
-			{'pubkey_type':'std'},
-			{'s':r32},
-			{'s':r32,'compressed':'yes'},
-			{'s':r32,'compressed':'yes','pubkey_type':'std'},
-			{'s':r32,'compressed':True,'pubkey_type':'nonstd'},
-			{'s':r32+b'x','compressed':True,'pubkey_type':'std'}
+			{'proto':proto, 'wif':1},
+			{'proto':proto, 'wif':'1'},
+			{'proto':proto, 'wif':'5KXEpVzjWreTcQoG5hX357s1969MUKNLuSfcszF6yu84kpsNZKb'},
+			{'proto':proto, 's':r32,'wif':'5KXEpVzjWreTcQoG5hX357s1969MUKNLuSfcszF6yu84kpsNZKb'},
+			{'proto':proto, 'pubkey_type':'std'},
+			{'proto':proto, 's':r32},
+			{'proto':proto, 's':r32,'compressed':'yes'},
+			{'proto':proto, 's':r32,'compressed':'yes','pubkey_type':'std'},
+			{'proto':proto, 's':r32,'compressed':True,'pubkey_type':'nonstd'},
+			{'proto':proto, 's':r32+b'x','compressed':True,'pubkey_type':'std'}
 		),
 		'good': (
-			{'wif':'92iqzh6NqiKawyB1ronw66YtEHrU4rxRJ5T4aHniZqvuSVZS21f',
+			{'proto':proto, 'wif':'92iqzh6NqiKawyB1ronw66YtEHrU4rxRJ5T4aHniZqvuSVZS21f',
 			'ret':'95b2aa7912550eacdd3844dcc14bee08ce7bc2434ad4858beb136021e945afeb'},
-			{'wif':'cSaJAXBAm9ooHpVJgoxqjDG3AcareFy29Cz8mhnNTRijjv2HLgta',
+			{'proto':proto, 'wif':'cSaJAXBAm9ooHpVJgoxqjDG3AcareFy29Cz8mhnNTRijjv2HLgta',
 			'ret':'94fa8b90c11fea8fb907c9376b919534b0a75b9a9621edf71a78753544b4101c'},
-			{'s':r32,'compressed':False,'pubkey_type':'std','ret':r32.hex()},
-			{'s':r32,'compressed':True,'pubkey_type':'std','ret':r32.hex()}
+			{'proto':proto, 's':r32,'compressed':False,'pubkey_type':'std','ret':r32.hex()},
+			{'proto':proto, 's':r32,'compressed':True,'pubkey_type':'std','ret':r32.hex()}
 		)
 	},
 }

+ 28 - 14
test/test.py

@@ -22,8 +22,8 @@ test/test.py: Test suite for the MMGen wallet system
 
 def check_segwit_opts():
 	for k,m in (('segwit','S'),('segwit_random','S'),('bech32','B')):
-		if getattr(opt,k) and m not in g.proto.mmtypes:
-			die(1,f'--{k.replace("_","-")} option incompatible with {g.proto.cls_name}')
+		if getattr(opt,k) and m not in proto.mmtypes:
+			die(1,f'--{k.replace("_","-")} option incompatible with {proto.cls_name}')
 
 def create_shm_dir(data_dir,trash_dir):
 	# Laggy flash media can cause pexpect to fail, so create a temporary directory
@@ -140,7 +140,7 @@ If no command is given, the whole test suite is run.
 	}
 }
 
-data_dir = os.path.join('test','data_dir' + ('','-α')[bool(os.getenv('MMGEN_DEBUG_UTF8'))])
+data_dir = get_data_dir() # include/common.py
 
 # we need some opt values before running opts.init, so parse without initializing:
 _uopts = opts.init(opts_data,parse_only=True).user_opts
@@ -150,21 +150,35 @@ if not ('resume' in _uopts or 'skip_deps' in _uopts):
 	try: os.unlink(data_dir)
 	except: pass
 
-def get_coin():
-	return (_uopts.get('coin') or 'btc').lower()
+def add_cmdline_opts():
+	"""
+	These are set automatically now when g.test_suite == True:
+	  --data-dir in opts.init()
+	  --daemon-data-dir and --rpc-port by CoinDaemon()
+	"""
+	def get_coin():
+		return (_uopts.get('coin') or 'btc').lower()
 
-network_id = get_network_id(get_coin(),bool(_uopts.get('testnet')))
+	network_id = get_coin().lower() + ('_tn' if _uopts.get('testnet') else '')
 
-sys.argv.insert(1,'--data-dir=' + data_dir)
-sys.argv.insert(1,'--daemon-data-dir=test/daemons/' + get_coin())
-sys.argv.insert(1,'--rpc-port={}'.format(CoinDaemon(network_id,test_suite=True).rpc_port))
+	sys.argv.insert(1,'--data-dir=' + data_dir)
+	sys.argv.insert(1,'--daemon-data-dir=test/daemons/' + get_coin())
+	sys.argv.insert(1,'--rpc-port={}'.format(CoinDaemon(network_id,test_suite=True).rpc_port))
+
+# add_cmdline_opts()
 
 # step 2: opts.init will create new data_dir in ./test (if not 'resume' or 'skip_deps'):
 usr_args = opts.init(opts_data)
 
+network_id = g.coin.lower() + ('_tn' if opt.testnet else '')
+
+from mmgen.protocol import init_proto_from_opts
+proto = init_proto_from_opts()
+
 # step 3: move data_dir to /dev/shm and symlink it back to ./test:
 trash_dir = os.path.join('test','trash')
-if not ('resume' in _uopts or 'skip_deps' in _uopts):
+
+if not (opt.resume or opt.skip_deps):
 	shm_dir = create_shm_dir(data_dir,trash_dir)
 
 check_segwit_opts()
@@ -673,7 +687,7 @@ class TestSuiteRunner(object):
 
 		if opt.log:
 			self.log_fd.write('[{}][{}:{}] {}\n'.format(
-				g.coin.lower(),
+				proto.coin.lower(),
 				self.ts.group_name,
 				self.ts.test_name,
 				cmd_disp))
@@ -699,7 +713,7 @@ class TestSuiteRunner(object):
 		def gen_msg():
 			yield ('{g}:{c}' if cmd else 'test group {g!r}').format(g=gname,c=cmd)
 			if len(ts_cls.networks) != 1:
-				yield ' for {} {}'.format(g.proto.coin,g.proto.network)
+				yield ' for {} {}'.format(proto.coin,proto.network)
 			if segwit_opt:
 				yield ' (--{})'.format(segwit_opt.replace('_','-'))
 
@@ -712,8 +726,8 @@ class TestSuiteRunner(object):
 		# 'networks = ()' means all networks allowed
 		nws = [(e.split('_')[0],'testnet') if '_' in e else (e,'mainnet') for e in ts_cls.networks]
 		if nws:
-			coin = g.coin.lower()
-			nw = ('mainnet','testnet')[g.proto.testnet]
+			coin = proto.coin.lower()
+			nw = ('mainnet','testnet')[proto.testnet]
 			for a,b in nws:
 				if a == coin and b == nw:
 					break

+ 0 - 1
test/test_py_d/ts_autosign.py

@@ -230,7 +230,6 @@ class TestSuiteAutosign(TestSuiteBase):
 		if simulate and not opt.exact_output:
 			rmsg('This command must be run with --exact-output enabled!')
 			return False
-
 		network_ids = [c+'_tn' for c in daemon_coins] + daemon_coins
 		start_test_daemons(*network_ids)
 

+ 5 - 3
test/test_py_d/ts_base.py

@@ -35,16 +35,18 @@ class TestSuiteBase(object):
 	segwit_opts_ok = False
 
 	def __init__(self,trunner,cfgs,spawn):
+		from mmgen.protocol import init_proto_from_opts
+		self.proto = init_proto_from_opts()
 		self.tr = trunner
 		self.cfgs = cfgs
 		self.spawn = spawn
 		self.have_dfl_wallet = False
 		self.usr_rand_chars = (5,30)[bool(opt.usr_random)]
 		self.usr_rand_arg = '-r{}'.format(self.usr_rand_chars)
-		self.altcoin_pfx = '' if g.proto.base_coin == 'BTC' else '-'+g.proto.base_coin
-		self.tn_ext = ('','.testnet')[g.proto.testnet]
+		self.altcoin_pfx = '' if self.proto.base_coin == 'BTC' else '-'+self.proto.base_coin
+		self.tn_ext = ('','.testnet')[self.proto.testnet]
 		d = {'bch':'btc','btc':'btc','ltc':'ltc'}
-		self.fork = d[g.coin.lower()] if g.coin.lower() in d else None
+		self.fork = d[self.proto.coin.lower()] if self.proto.coin.lower() in d else None
 
 	@property
 	def tmpdir(self):

+ 9 - 8
test/test_py_d/ts_chainsplit.py

@@ -58,19 +58,20 @@ class TestSuiteChainsplit(TestSuiteRegtest):
 	)
 
 	def split_setup(self):
-		if g.coin != 'BTC': die(1,'Test valid only for coin BTC')
-		opt.coin = 'BTC'
+		if self.proto.coin != 'BTC':
+			die(1,'Test valid only for coin BTC')
+		self.coin = 'BTC'
 		return self.setup()
 
 	def split_fork(self):
-		opt.coin = 'B2X'
+		self.coin = 'B2X'
 		t = self.spawn('mmgen-regtest',['fork','btc'])
 		t.expect('Creating fork from coin')
 		t.expect('successfully created')
 		t.ok()
 
 	def split_start(self,coin):
-		opt.coin = coin
+		self.coin = coin
 		t = self.spawn('mmgen-regtest',['bob'])
 		t.expect('Starting')
 		t.expect('done')
@@ -83,7 +84,7 @@ class TestSuiteChainsplit(TestSuiteRegtest):
 	def split_gen_b2x2(self):  self.regtest_generate(coin='B2X')
 
 	def split_do_split(self):
-		opt.coin = 'B2X'
+		self.coin = 'B2X'
 		sid = self.regtest_user_sid('bob')
 		t = self.spawn('mmgen-split',[
 			'--bob',
@@ -105,7 +106,7 @@ class TestSuiteChainsplit(TestSuiteRegtest):
 	def split_sign(self,coin,ext):
 		wf = get_file_with_ext(self.regtest_user_dir('bob',coin=coin.lower()),'mmdat')
 		txfile = self.get_file_with_ext(ext,no_dot=True)
-		opt.coin = coin
+		self.coin = coin
 		self.txsign(txfile,wf,extra_opts=['--bob'])
 
 	def split_sign_b2x(self):
@@ -115,7 +116,7 @@ class TestSuiteChainsplit(TestSuiteRegtest):
 		return self.regtest_sign(coin='BTC',ext='9997].rawtx')
 
 	def split_send(self,coin,ext):
-		opt.coin = coin
+		self.coin = coin
 		txfile = self.get_file_with_ext(ext,no_dot=True)
 		self.txsend(txfile,bogus_send=False,extra_opts=['--bob'])
 
@@ -126,7 +127,7 @@ class TestSuiteChainsplit(TestSuiteRegtest):
 		return self.regtest_send(coin='BTC',ext='9997].sigtx')
 
 	def split_txdo_timelock(self,coin,locktime,bad_locktime):
-		opt.coin = coin
+		self.coin = coin
 		sid = self.regtest_user_sid('bob')
 		self.regtest_user_txdo( 'bob','0.0001',[sid+':S:5'],'1',pw=rt_pw,
 								extra_args=['--locktime='+str(locktime)],

+ 112 - 69
test/test_py_d/ts_ethdev.py

@@ -141,6 +141,7 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
 	'Ethereum transacting, token deployment and tracking wallet operations'
 	networks = ('eth','etc')
 	passthru_opts = ('coin',)
+	extra_spawn_args = ['--regtest=1']
 	tmpdir_nums = [22]
 	solc_vers = ('0.5.1','0.5.3') # 0.5.1: Raspbian Stretch, 0.5.3: Ubuntu Bionic
 	cmd_group = (
@@ -152,7 +153,9 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
 		('addrimport_dev_addr', "importing Parity dev address 'Ox00a329c..'"),
 
 		('txcreate1',           'creating a transaction (spend from dev address to address :1)'),
+		('txview1_raw',         'viewing the raw transaction'),
 		('txsign1',             'signing the transaction'),
+		('txview1_sig',         'viewing the signed transaction'),
 		('tx_status0_bad',      'getting the transaction status'),
 		('txsign1_ni',          'signing the transaction (non-interactive)'),
 		('txsend1',             'sending the transaction'),
@@ -220,8 +223,10 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
 		('token_bal1',          'the {} balance and token balance'.format(coin)),
 
 		('token_txcreate1',     'creating a token transaction'),
+		('token_txview1_raw',   'viewing the raw transaction'),
 		('token_txsign1',       'signing the transaction'),
 		('token_txsend1',       'sending the transaction'),
+		('token_txview1_sig',   'viewing the signed transaction'),
 		('tx_status3',          'getting the transaction status'),
 		('token_bal2',          'the {} balance and token balance'.format(coin)),
 
@@ -301,14 +306,16 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
 	)
 
 	def __init__(self,trunner,cfgs,spawn):
+		TestSuiteBase.__init__(self,trunner,cfgs,spawn)
+		from mmgen.protocol import init_proto
+		self.proto = init_proto(g.coin,network='regtest')
 		from mmgen.daemon import CoinDaemon
-		self.rpc_port = CoinDaemon(g.coin,test_suite=True).rpc_port
+		self.rpc_port = CoinDaemon(proto=self.proto,test_suite=True).rpc_port
 		os.environ['MMGEN_BOGUS_WALLET_DATA'] = ''
-		return TestSuiteBase.__init__(self,trunner,cfgs,spawn)
 
 	@property
 	def eth_args(self):
-		return ['--outdir={}'.format(self.tmpdir),'--coin='+g.coin,'--rpc-port={}'.format(self.rpc_port),'--quiet']
+		return ['--outdir={}'.format(self.tmpdir),'--coin='+self.proto.coin,'--rpc-port={}'.format(self.rpc_port),'--quiet']
 
 	def setup(self):
 		self.spawn('',msg_only=True)
@@ -322,15 +329,15 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
 			from shutil import copytree
 			for d in ('mm1','mm2'):
 				copytree(os.path.join(srcdir,d),os.path.join(self.tmpdir,d))
-		restart_test_daemons(g.coin)
+		restart_test_daemons(self.proto.coin)
 		return 'ok'
 
 	def wallet_upgrade(self,src_file):
-		if g.coin == 'ETC':
+		if self.proto.coin == 'ETC':
 			msg('skipping test {!r} for ETC'.format(self.test_name))
 			return 'skip'
 		src_dir = joinpath(ref_dir,'ethereum')
-		dest_dir = joinpath(self.tr.data_dir,'altcoins',g.coin.lower())
+		dest_dir = joinpath(self.tr.data_dir,'altcoins',self.proto.coin.lower())
 		w_from = joinpath(src_dir,src_file)
 		w_to = joinpath(dest_dir,'tracking-wallet.json')
 		os.makedirs(dest_dir,mode=0o750,exist_ok=True)
@@ -345,13 +352,12 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
 	def wallet_upgrade2(self): return self.wallet_upgrade('tracking-wallet-v2.json')
 
 	def addrgen(self,addrs='1-3,11-13,21-23'):
-		from mmgen.addr import MMGenAddrType
 		t = self.spawn('mmgen-addrgen', self.eth_args + [dfl_words_file,addrs])
 		t.written_to_file('Addresses')
 		t.read()
 		return t
 
-	def addrimport(self,ext='21-23]{}.addrs',expect='9/9',add_args=[],bad_input=False):
+	def addrimport(self,ext='21-23]{}.regtest.addrs',expect='9/9',add_args=[],bad_input=False):
 		ext = ext.format('-α' if g.debug_utf8 else '')
 		fn = self.get_file_with_ext(ext,no_dot=True,delete=False)
 		t = self.spawn('mmgen-addrimport', self.eth_args[1:-1] + add_args + [fn])
@@ -379,40 +385,42 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
 						eth_fee_res     = None,
 						fee_res_fs      = '0.00105 {} (50 gas price in Gwei)',
 						fee_desc        = 'gas price',
-						no_read         = False):
-		fee_res = fee_res_fs.format(g.coin)
+						no_read         = False,
+						tweaks          = [] ):
+		fee_res = fee_res_fs.format(self.proto.coin)
 		t = self.spawn('mmgen-'+caller, self.eth_args + ['-B'] + args)
 		t.expect(r'add \[l\]abel, .*?:.','p', regex=True)
 		t.written_to_file('Account balances listing')
 		t = self.txcreate_ui_common( t, menu=menu, caller=caller,
 										input_sels_prompt = 'to spend from',
 										inputs            = acct,
-										file_desc         = 'Ethereum transaction',
+										file_desc         = 'transaction',
 										bad_input_sels    = True,
 										non_mmgen_inputs  = non_mmgen_inputs,
 										interactive_fee   = interactive_fee,
 										fee_res           = fee_res,
 										fee_desc          = fee_desc,
 										eth_fee_res       = eth_fee_res,
-										add_comment       = tx_label_jp )
+										add_comment       = tx_label_jp,
+										tweaks            = tweaks )
 		if not no_read:
 			t.read()
 		return t
 
-	def txsign(self,ni=False,ext='{}.rawtx',add_args=[]):
+	def txsign(self,ni=False,ext='{}.regtest.rawtx',add_args=[]):
 		ext = ext.format('-α' if g.debug_utf8 else '')
 		keyfile = joinpath(self.tmpdir,parity_key_fn)
 		write_to_file(keyfile,dfl_privkey+'\n')
 		txfile = self.get_file_with_ext(ext,no_dot=True)
 		t = self.spawn( 'mmgen-txsign',
-						['--outdir={}'.format(self.tmpdir),'--coin='+g.coin,'--quiet']
+						['--outdir={}'.format(self.tmpdir),'--coin='+self.proto.coin,'--quiet']
 						+ ['--rpc-host=bad_host'] # ETH signing must work without RPC
 						+ add_args
 						+ ([],['--yes'])[ni]
 						+ ['-k', keyfile, txfile, dfl_words_file] )
 		return self.txsign_ui_common(t,ni=ni,has_label=True)
 
-	def txsend(self,ni=False,bogus_send=False,ext='{}.sigtx',add_args=[]):
+	def txsend(self,ni=False,bogus_send=False,ext='{}.regtest.sigtx',add_args=[]):
 		ext = ext.format('-α' if g.debug_utf8 else '')
 		txfile = self.get_file_with_ext(ext,no_dot=True)
 		if not bogus_send: os.environ['MMGEN_BOGUS_SEND'] = ''
@@ -421,31 +429,41 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
 		txid = self.txsend_ui_common(t,quiet=not g.debug,bogus_send=bogus_send,has_label=True)
 		return t
 
+	def txview(self,ext_fs):
+		ext = ext_fs.format('-α' if g.debug_utf8 else '')
+		txfile = self.get_file_with_ext(ext,no_dot=True)
+		t = self.spawn( 'mmgen-tool',['--verbose','txview',txfile] )
+		t.read()
+		return t
+
 	def txcreate1(self):
 		# valid_keypresses = EthereumTwUnspentOutputs.key_mappings.keys()
 		menu = ['a','d','r','M','X','e','m','m'] # include one invalid keypress, 'X'
 		args = ['98831F3A:E:1,123.456']
-		return self.txcreate(args=args,menu=menu,acct='1',non_mmgen_inputs=1)
-
+		return self.txcreate(args=args,menu=menu,acct='1',non_mmgen_inputs=1,tweaks=['confirm_non_mmgen'])
+	def txview1_raw(self):
+		return self.txview(ext_fs='{}.regtest.rawtx')
 	def txsign1(self):    return self.txsign(add_args=['--use-internal-keccak-module'])
 	def tx_status0_bad(self):
-		return self.tx_status(ext='{}.sigtx',expect_str='neither in mempool nor blockchain',exit_val=1)
+		return self.tx_status(ext='{}.regtest.sigtx',expect_str='neither in mempool nor blockchain',exit_val=1)
 	def txsign1_ni(self): return self.txsign(ni=True)
 	def txsend1(self):    return self.txsend()
+	def txview1_sig(self): # do after send so that TxID is displayed
+		return self.txview(ext_fs='{}.regtest.sigtx')
 	def bal1(self):       return self.bal(n='1')
 
 	def txcreate2(self):
 		args = ['98831F3A:E:11,1.234']
-		return self.txcreate(args=args,acct='10',non_mmgen_inputs=1)
-	def txsign2(self): return self.txsign(ni=True,ext='1.234,50000]{}.rawtx')
-	def txsend2(self): return self.txsend(ext='1.234,50000]{}.sigtx')
+		return self.txcreate(args=args,acct='10',non_mmgen_inputs=1,tweaks=['confirm_non_mmgen'])
+	def txsign2(self): return self.txsign(ni=True,ext='1.234,50000]{}.regtest.rawtx')
+	def txsend2(self): return self.txsend(ext='1.234,50000]{}.regtest.sigtx')
 	def bal2(self):    return self.bal(n='2')
 
 	def txcreate3(self):
 		args = ['98831F3A:E:21,2.345']
-		return self.txcreate(args=args,acct='10',non_mmgen_inputs=1)
-	def txsign3(self): return self.txsign(ni=True,ext='2.345,50000]{}.rawtx')
-	def txsend3(self): return self.txsend(ext='2.345,50000]{}.sigtx')
+		return self.txcreate(args=args,acct='10',non_mmgen_inputs=1,tweaks=['confirm_non_mmgen'])
+	def txsign3(self): return self.txsign(ni=True,ext='2.345,50000]{}.regtest.rawtx')
+	def txsend3(self): return self.txsend(ext='2.345,50000]{}.regtest.sigtx')
 	def bal3(self):    return self.bal(n='3')
 
 	def tx_status(self,ext,expect_str,expect_str2='',add_args=[],exit_val=0):
@@ -460,10 +478,10 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
 		return t
 
 	def tx_status1(self):
-		return self.tx_status(ext='2.345,50000]{}.sigtx',expect_str='has 1 confirmation')
+		return self.tx_status(ext='2.345,50000]{}.regtest.sigtx',expect_str='has 1 confirmation')
 
 	def tx_status1a(self):
-		return self.tx_status(ext='2.345,50000]{}.sigtx',expect_str='has 2 confirmations')
+		return self.tx_status(ext='2.345,50000]{}.regtest.sigtx',expect_str='has 2 confirmations')
 
 	def txcreate4(self):
 		args = ['98831F3A:E:2,23.45495']
@@ -476,7 +494,7 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
 								fee_res_fs       = fee_res_fs,
 								eth_fee_res      = True )
 
-	def txbump(self,ext=',40000]{}.rawtx',fee='50G',add_args=[]):
+	def txbump(self,ext=',40000]{}.regtest.rawtx',fee='50G',add_args=[]):
 		ext = ext.format('-α' if g.debug_utf8 else '')
 		txfile = self.get_file_with_ext(ext,no_dot=True)
 		t = self.spawn('mmgen-txbump', self.eth_args + add_args + ['--yes',txfile])
@@ -484,23 +502,26 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
 		t.read()
 		return t
 
-	def txsign4(self): return self.txsign(ni=True,ext='.45495,50000]{}.rawtx')
-	def txsend4(self): return self.txsend(ext='.45495,50000]{}.sigtx')
+	def txsign4(self): return self.txsign(ni=True,ext='.45495,50000]{}.regtest.rawtx')
+	def txsend4(self): return self.txsend(ext='.45495,50000]{}.regtest.sigtx')
 	def bal4(self):    return self.bal(n='4')
 
 	def txcreate5(self):
 		args = [burn_addr + ','+amt1]
-		return self.txcreate(args=args,acct='10',non_mmgen_inputs=1)
-	def txsign5(self): return self.txsign(ni=True,ext=amt1+',50000]{}.rawtx')
-	def txsend5(self): return self.txsend(ext=amt1+',50000]{}.sigtx')
+		return self.txcreate(args=args,acct='10',non_mmgen_inputs=1,tweaks=['confirm_non_mmgen'])
+	def txsign5(self): return self.txsign(ni=True,ext=amt1+',50000]{}.regtest.rawtx')
+	def txsend5(self): return self.txsend(ext=amt1+',50000]{}.regtest.sigtx')
 	def bal5(self):    return self.bal(n='5')
 
-	bal_corr = Decimal('0.0000032') # gas use for token sends varies between ETH and ETC!
+	#bal_corr = Decimal('0.0000032') # gas use for token sends varies between ETH and ETC!
+	bal_corr = Decimal('0.0000000') # update: Parity team seems to have corrected this
+
 	def bal(self,n=None):
 		t = self.spawn('mmgen-tool', self.eth_args + ['twview','wide=1'])
 		for b in bals[n]:
 			addr,amt,adj = b if len(b) == 3 else b + (False,)
-			if adj and g.coin == 'ETC': amt = str(Decimal(amt) + Decimal(adj[1]) * self.bal_corr)
+			if adj and self.proto.coin == 'ETC':
+				amt = str(Decimal(amt) + Decimal(adj[1]) * self.bal_corr)
 			pat = r'{}\s+{}\s'.format(addr,amt.replace('.',r'\.'))
 			t.expect(pat,regex=True)
 		t.read()
@@ -510,7 +531,7 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
 		t = self.spawn('mmgen-tool', self.eth_args + ['--token=mm1','twview','wide=1'])
 		for b in token_bals[n]:
 			addr,_amt1,_amt2,adj = b if len(b) == 4 else b + (False,)
-			if adj and g.coin == 'ETC':
+			if adj and self.proto.coin == 'ETC':
 				_amt2 = str(Decimal(_amt2) + Decimal(adj[1]) * self.bal_corr)
 			pat = r'{}\s+{}\s+{}\s'.format(addr,_amt1.replace('.',r'\.'),_amt2.replace('.',r'\.'))
 			t.expect(pat,regex=True)
@@ -522,7 +543,7 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
 		bal1 = token_bals_getbalance[idx][0]
 		bal2 = token_bals_getbalance[idx][1]
 		bal1 = Decimal(bal1)
-		if etc_adj and g.coin == 'ETC':
+		if etc_adj and self.proto.coin == 'ETC':
 			bal1 += self.bal_corr
 		t = self.spawn('mmgen-tool', self.eth_args + extra_args + ['getbalance'])
 		t.expect(r'\n[0-9A-F]{8}: .* '+str(bal1),regex=True)
@@ -565,7 +586,7 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
 		cmd = [
 			'scripts/traceback_run.py',
 			'scripts/create-token.py',
-			'--coin=' + g.coin,
+			'--coin=' + self.proto.coin,
 			'--outdir=' + odir
 		] + cmd_args + [dfl_addr_chk]
 		imsg("Executing: {}".format(' '.join(cmd)))
@@ -583,6 +604,13 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
 		token_data = { 'name':'MMGen Token 2', 'symbol':'MM2', 'supply':10**18, 'decimals':10 }
 		return self.token_compile(token_data)
 
+	async def get_exec_status(self,txid):
+		from mmgen.tx import MMGenTX
+		tx = MMGenTX.New(proto=self.proto)
+		from mmgen.rpc import rpc_init
+		tx.rpc = await rpc_init(self.proto)
+		return await tx.get_exec_status(txid,True)
+
 	async def token_deploy(self,num,key,gas,mmgen_cmd='txdo',tx_fee='8G'):
 		keyfile = joinpath(self.tmpdir,parity_key_fn)
 		fn = joinpath(self.tmpdir,'mm'+str(num),key+'.bin')
@@ -596,8 +624,8 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
 		if mmgen_cmd == 'txdo': args += ['-k',keyfile]
 		t = self.spawn( 'mmgen-'+mmgen_cmd, self.eth_args + args)
 		if mmgen_cmd == 'txcreate':
-			t.written_to_file('Ethereum transaction')
-			ext = '[0,8000]{}.rawtx'.format('-α' if g.debug_utf8 else '')
+			t.written_to_file('transaction')
+			ext = '[0,8000]{}.regtest.rawtx'.format('-α' if g.debug_utf8 else '')
 			txfile = self.get_file_with_ext(ext,no_dot=True)
 			t = self.spawn('mmgen-txsign', self.eth_args + ['--yes','-k',keyfile,txfile],no_msg=True)
 			self.txsign_ui_common(t,ni=True)
@@ -609,12 +637,10 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
 			quiet = mmgen_cmd == 'txdo' or not g.debug,
 			bogus_send=False)
 		addr = t.expect_getend('Contract address: ')
-		from mmgen.altcoins.eth.tx import EthereumMMGenTX as etx
-		assert (await etx.get_exec_status(txid,True)) != 0,(
-			"Contract '{}:{}' failed to execute. Aborting".format(num,key))
+		assert (await self.get_exec_status(txid)) != 0, f'Contract {num}:{key} failed to execute. Aborting'
 		if key == 'Token':
-			self.write_to_tmpfile('token_addr{}'.format(num),addr+'\n')
-			imsg('\nToken MM{} deployed!'.format(num))
+			self.write_to_tmpfile( f'token_addr{num}', addr+'\n' )
+			imsg(f'\nToken MM{num} deployed!')
 		return t
 
 	async def token_deploy1a(self): return await self.token_deploy(num=1,key='SafeMath',gas=200000)
@@ -622,7 +648,7 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
 	async def token_deploy1c(self): return await self.token_deploy(num=1,key='Token',gas=1100000,tx_fee='7G')
 
 	def tx_status2(self):
-		return self.tx_status(ext=g.coin+'[0,7000]{}.sigtx',expect_str='successfully executed')
+		return self.tx_status(ext=self.proto.coin+'[0,7000]{}.regtest.sigtx',expect_str='successfully executed')
 
 	def bal6(self): return self.bal5()
 
@@ -638,17 +664,19 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
 		sid = dfl_sid
 		from mmgen.tool import MMGenToolCmdWallet
 		usr_mmaddrs = ['{}:E:{}'.format(sid,i) for i in (11,21)]
-		usr_addrs = [MMGenToolCmdWallet().gen_addr(addr,dfl_words_file) for addr in usr_mmaddrs]
+		usr_addrs = [MMGenToolCmdWallet(proto=self.proto).gen_addr(addr,dfl_words_file) for addr in usr_mmaddrs]
 
 		from mmgen.altcoins.eth.contract import TokenResolve
 		from mmgen.altcoins.eth.tx import EthereumMMGenTX as etx
-		async def do_transfer():
+		async def do_transfer(rpc):
 			for i in range(2):
 				tk = await TokenResolve(
+					self.proto,
+					rpc,
 					self.read_from_tmpfile(f'token_addr{i+1}').strip() )
 				imsg_r( '\n' + await tk.info() )
 				imsg('dev token balance (pre-send): {}'.format(await tk.get_balance(dfl_addr)))
-				imsg('Sending {} {} to address {} ({})'.format(amt,g.coin,usr_addrs[i],usr_mmaddrs[i]))
+				imsg('Sending {} {} to address {} ({})'.format(amt,self.proto.dcoin,usr_addrs[i],usr_mmaddrs[i]))
 				from mmgen.obj import ETHAmt
 				txid = await tk.transfer(
 					dfl_addr,
@@ -657,22 +685,27 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
 					dfl_privkey,
 					start_gas = ETHAmt(60000,'wei'),
 					gasPrice  = ETHAmt(8,'Gwei') )
-				assert (await etx.get_exec_status(txid,True)) != 0,'Transfer of token funds failed. Aborting'
+				assert (await self.get_exec_status(txid)) != 0,'Transfer of token funds failed. Aborting'
 
-		async def show_bals():
+		async def show_bals(rpc):
 			for i in range(2):
 				tk = await TokenResolve(
+					self.proto,
+					rpc,
 					self.read_from_tmpfile(f'token_addr{i+1}').strip() )
 				imsg('Token: {}'.format(await tk.get_symbol()))
 				imsg('dev token balance: {}'.format(await tk.get_balance(dfl_addr)))
 				imsg('usr token balance: {} ({} {})'.format(
 						await tk.get_balance(usr_addrs[i]),usr_mmaddrs[i],usr_addrs[i]))
 
+		from mmgen.rpc import rpc_init
+		rpc = await rpc_init(self.proto)
+
 		silence()
 		if op == 'show_bals':
-			await show_bals()
+			await show_bals(rpc)
 		elif op == 'do_transfer':
-			await do_transfer()
+			await do_transfer(rpc)
 		end_silence()
 		return 'ok'
 
@@ -688,19 +721,22 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
 		return self.addrgen(addrs='21-23')
 
 	def token_addrimport_badaddr1(self):
-		t = self.addrimport(ext='[11-13]{}.addrs',add_args=['--token=abc'],bad_input=True)
+		t = self.addrimport(ext='[11-13]{}.regtest.addrs',add_args=['--token=abc'],bad_input=True)
 		t.req_exit_val = 2
 		return t
 
 	def token_addrimport_badaddr2(self):
-		t = self.addrimport(ext='[11-13]{}.addrs',add_args=['--token='+'00deadbeef'*4],bad_input=True)
+		t = self.addrimport(ext='[11-13]{}.regtest.addrs',add_args=['--token='+'00deadbeef'*4],bad_input=True)
 		t.req_exit_val = 2
 		return t
 
 	def token_addrimport(self,extra_args=[],expect='3/3'):
 		for n,r in ('1','11-13'),('2','21-23'):
 			tk_addr = self.read_from_tmpfile('token_addr'+n).strip()
-			t = self.addrimport(ext='['+r+']{}.addrs',expect=expect,add_args=['--token='+tk_addr]+extra_args)
+			t = self.addrimport(
+				ext      = f'[{r}]{{}}.regtest.addrs',
+				expect   = expect,
+				add_args = ['--token-addr='+tk_addr]+extra_args )
 			t.p.wait()
 			ok_msg()
 		t.skip_ok = True
@@ -719,7 +755,6 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
 			menu              = [],
 			inputs            = inputs,
 			input_sels_prompt = 'to spend from',
-			file_desc         = 'Ethereum token transaction',
 			add_comment       = tx_label_lat_cyr_gr )
 		t.read()
 		return t
@@ -730,14 +765,18 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
 
 	def token_txcreate1(self):
 		return self.token_txcreate(args=['98831F3A:E:12,1.23456'],token='mm1')
+	def token_txview1_raw(self):
+		return self.txview(ext_fs='1.23456,50000]{}.regtest.rawtx')
 	def token_txsign1(self):
-		return self.token_txsign(ext='1.23456,50000]{}.rawtx',token='mm1')
+		return self.token_txsign(ext='1.23456,50000]{}.regtest.rawtx',token='mm1')
 	def token_txsend1(self):
-		return self.token_txsend(ext='1.23456,50000]{}.sigtx',token='mm1')
+		return self.token_txsend(ext='1.23456,50000]{}.regtest.sigtx',token='mm1')
+	def token_txview1_sig(self):
+		return self.txview(ext_fs='1.23456,50000]{}.regtest.sigtx')
 
 	def tx_status3(self):
 		return self.tx_status(
-			ext='1.23456,50000]{}.sigtx',
+			ext='1.23456,50000]{}.regtest.sigtx',
 			add_args=['--token=mm1'],
 			expect_str='successfully executed',
 			expect_str2='has 1 confirmation')
@@ -756,11 +795,11 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
 	def token_txcreate2(self):
 		return self.token_txcreate(args=[burn_addr+','+amt2],token='mm1')
 	def token_txbump(self):
-		return self.txbump(ext=amt2+',50000]{}.rawtx',fee='56G',add_args=['--token=mm1'])
+		return self.txbump(ext=amt2+',50000]{}.regtest.rawtx',fee='56G',add_args=['--token=mm1'])
 	def token_txsign2(self):
-		return self.token_txsign(ext=amt2+',50000]{}.rawtx',token='mm1')
+		return self.token_txsign(ext=amt2+',50000]{}.regtest.rawtx',token='mm1')
 	def token_txsend2(self):
-		return self.token_txsend(ext=amt2+',50000]{}.sigtx',token='mm1')
+		return self.token_txsend(ext=amt2+',50000]{}.regtest.sigtx',token='mm1')
 
 	def token_bal3(self):
 		return self.token_bal(n='3')
@@ -785,9 +824,9 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
 	def txcreate_noamt(self):
 		return self.txcreate(args=['98831F3A:E:12'],eth_fee_res=True)
 	def txsign_noamt(self):
-		return self.txsign(ext='99.99895,50000]{}.rawtx')
+		return self.txsign(ext='99.99895,50000]{}.regtest.rawtx')
 	def txsend_noamt(self):
-		return self.txsend(ext='99.99895,50000]{}.sigtx')
+		return self.txsend(ext='99.99895,50000]{}.regtest.sigtx')
 
 	def bal8(self):       return self.bal(n='8')
 	def token_bal5(self): return self.token_bal(n='5')
@@ -795,9 +834,9 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
 	def token_txcreate_noamt(self):
 		return self.token_txcreate(args=['98831F3A:E:13'],token='mm1',inputs='2',fee='51G')
 	def token_txsign_noamt(self):
-		return self.token_txsign(ext='1.23456,51000]{}.rawtx',token='mm1')
+		return self.token_txsign(ext='1.23456,51000]{}.regtest.rawtx',token='mm1')
 	def token_txsend_noamt(self):
-		return self.token_txsend(ext='1.23456,51000]{}.sigtx',token='mm1')
+		return self.token_txsend(ext='1.23456,51000]{}.regtest.sigtx',token='mm1')
 
 	def bal9(self):       return self.bal(n='9')
 	def token_bal6(self): return self.token_bal(n='6')
@@ -851,8 +890,12 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
 			args=['-B','--cached-balances','-i'],
 			total= '1000126.14829832312345678',
 			adj_total=True,
-			total_coin=g.coin):
-		if g.coin == 'ETC' and adj_total:
+			total_coin=None ):
+
+		if total_coin is None:
+			total_coin = self.proto.coin
+
+		if self.proto.coin == 'ETC' and adj_total:
 			total = str(Decimal(total) + self.bal_corr)
 		t = self.spawn('mmgen-txcreate', self.eth_args + args)
 		for n in bals:
@@ -936,5 +979,5 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared):
 
 	def stop(self):
 		self.spawn('',msg_only=True)
-		stop_test_daemons(g.coin)
+		stop_test_daemons(self.proto.coin)
 		return 'ok'

+ 48 - 39
test/test_py_d/ts_main.py

@@ -23,6 +23,7 @@ ts_main.py: Basic operations tests for the test.py test suite
 from mmgen.globalvars import g
 from mmgen.opts import opt
 from mmgen.wallet import Wallet,MMGenWallet,MMGenMnemonic,IncogWallet,MMGenSeedFile
+from mmgen.rpc import rpc_init
 from ..include.common import *
 from .common import *
 from .ts_base import *
@@ -144,21 +145,20 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared):
 	)
 
 	def __init__(self,trunner,cfgs,spawn):
-		if g.coin.lower() not in self.networks:
+		TestSuiteBase.__init__(self,trunner,cfgs,spawn)
+		if self.proto.coin.lower() not in self.networks:
 			return
-		from mmgen.rpc import rpc_init
-		self.rpc = run_session(rpc_init())
+		self.rpc = run_session(rpc_init(self.proto))
 		self.lbl_id = ('account','label')['label_api' in self.rpc.caps]
-		if g.coin in ('BTC','BCH','LTC'):
-			self.tx_fee     = {'btc':'0.0001','bch':'0.001','ltc':'0.01'}[g.coin.lower()]
-			self.txbump_fee = {'btc':'123s','bch':'567s','ltc':'12345s'}[g.coin.lower()]
-		return TestSuiteBase.__init__(self,trunner,cfgs,spawn)
+		if self.proto.coin in ('BTC','BCH','LTC'):
+			self.tx_fee     = {'btc':'0.0001','bch':'0.001','ltc':'0.01'}[self.proto.coin.lower()]
+			self.txbump_fee = {'btc':'123s','bch':'567s','ltc':'12345s'}[self.proto.coin.lower()]
 
 	def _get_addrfile_checksum(self,display=False):
 		addrfile = self.get_file_with_ext('addrs')
 		silence()
 		from mmgen.addr import AddrList
-		chk = AddrList(addrfile).chksum
+		chk = AddrList(self.proto,addrfile).chksum
 		if opt.verbose and display: msg('Checksum: {}'.format(cyan(chk)))
 		end_silence()
 		return chk
@@ -295,20 +295,20 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared):
 			sys.stderr.write("Fake transaction wallet data written to file {!r}\n".format(unspent_data_file))
 
 	def _create_fake_unspent_entry(self,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 self.proto.mmtypes: segwit = False
 		if lbl: lbl = ' ' + lbl
 		k = coinaddr.addr_fmt
 		if not segwit and k == 'p2sh': k = 'p2pkh'
 		s_beg,s_end = { 'p2pkh':  ('76a914','88ac'),
 						'p2sh':   ('a914','87'),
-						'bech32': (g.proto.witness_vernum_hex + '14','') }[k]
-		amt1,amt2 = {'btc':(10,40),'bch':(10,40),'ltc':(1000,4000)}[g.coin.lower()]
+						'bech32': (self.proto.witness_vernum_hex + '14','') }[k]
+		amt1,amt2 = {'btc':(10,40),'bch':(10,40),'ltc':(1000,4000)}[self.proto.coin.lower()]
 		ret = {
-			self.lbl_id: '{}:{}'.format(g.proto.base_coin.lower(),coinaddr) if non_mmgen \
+			self.lbl_id: '{}:{}'.format(self.proto.base_coin.lower(),coinaddr) if non_mmgen \
 				else ('{}:{}{}'.format(al_id,idx,lbl)),
 			'vout': int(getrandnum(4) % 8),
 			'txid': os.urandom(32).hex(),
-			'amount': g.proto.coin_amt('{}.{}'.format(amt1 + getrandnum(4) % amt2, getrandnum(4) % 100000000)),
+			'amount': self.proto.coin_amt('{}.{}'.format(amt1 + getrandnum(4) % amt2, getrandnum(4) % 100000000)),
 			'address': coinaddr,
 			'spendable': False,
 			'scriptPubKey': '{}{}{}'.format(s_beg,coinaddr.hex,s_end),
@@ -330,18 +330,20 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared):
 		if non_mmgen_input:
 			from mmgen.obj import PrivKey
 			privkey = PrivKey(
+				self.proto,
 				os.urandom(32),
 				compressed  = non_mmgen_input_compressed,
 				pubkey_type = 'std' )
 			from mmgen.addr import AddrGenerator,KeyGenerator
 			rand_coinaddr = AddrGenerator(
+				self.proto,
 				'p2pkh'
-				).to_addr(KeyGenerator(g.proto,'std').to_pubhex(privkey))
+				).to_addr(KeyGenerator(self.proto,'std').to_pubhex(privkey))
 			of = joinpath(self.cfgs[non_mmgen_input]['tmpdir'],non_mmgen_fn)
 			write_data_to_file(
 				outfile           = of,
 				data              = privkey.wif + '\n',
-				desc              = f'compressed {g.proto.name} key',
+				desc              = f'compressed {self.proto.name} key',
 				quiet             = True,
 				ignore_opt_outdir = True )
 			out.append(self._create_fake_unspent_entry(rand_coinaddr,non_mmgen=True,segwit=False))
@@ -351,10 +353,10 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared):
 	def _create_tx_data(self,sources,addrs_per_wallet=addrs_per_wallet):
 		from mmgen.addr import AddrData,AddrList
 		from mmgen.obj import AddrIdxList
-		tx_data,ad = {},AddrData()
+		tx_data,ad = {},AddrData(self.proto)
 		for s in sources:
 			afile = get_file_with_ext(self.cfgs[s]['tmpdir'],'addrs')
-			al = AddrList(afile)
+			al = AddrList(self.proto,afile)
 			ad.add(al)
 			aix = AddrIdxList(fmt_str=self.cfgs[s]['addr_idx_list'])
 			if len(aix) != addrs_per_wallet:
@@ -371,13 +373,13 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared):
 
 	def _make_txcreate_cmdline(self,tx_data):
 		from mmgen.obj import PrivKey
-		privkey = PrivKey(os.urandom(32),compressed=True,pubkey_type='std')
-		t = ('p2pkh','segwit')['S' in g.proto.mmtypes]
+		privkey = PrivKey(self.proto,os.urandom(32),compressed=True,pubkey_type='std')
+		t = ('p2pkh','segwit')['S' in self.proto.mmtypes]
 		from mmgen.addr import AddrGenerator,KeyGenerator
-		rand_coinaddr = AddrGenerator(t).to_addr(KeyGenerator('std').to_pubhex(privkey))
+		rand_coinaddr = AddrGenerator(self.proto,t).to_addr(KeyGenerator(self.proto,'std').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()]
+		mods = {'btc':(6,4),'bch':(6,4),'ltc':(600,400)}[self.proto.coin.lower()]
 		for k in self.cfgs:
 			self.cfgs[k]['amts'] = [None,None]
 			for idx,mod in enumerate(mods):
@@ -405,7 +407,8 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared):
 						view                       = 'n',
 						addrs_per_wallet           = addrs_per_wallet,
 						non_mmgen_input_compressed = True,
-						cmdline_inputs             = False )
+						cmdline_inputs             = False,
+						tweaks                     = [] ):
 
 		if opt.verbose or opt.exact_output:
 			sys.stderr.write(green('Generating fake tracking wallet info\n'))
@@ -415,13 +418,15 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared):
 		dfake = self._create_fake_unspent_data(ad,tx_data,non_mmgen_input,non_mmgen_input_compressed)
 		self._write_fake_data_to_file(repr(dfake))
 		cmd_args = self._make_txcreate_cmdline(tx_data)
+
 		if cmdline_inputs:
 			from mmgen.tx import TwLabel
 			cmd_args = ['--inputs={},{},{},{},{},{}'.format(
-				TwLabel(dfake[0][self.lbl_id]).mmid,dfake[1]['address'],
-				TwLabel(dfake[2][self.lbl_id]).mmid,dfake[3]['address'],
-				TwLabel(dfake[4][self.lbl_id]).mmid,dfake[5]['address']
+				TwLabel(self.proto,dfake[0][self.lbl_id]).mmid,dfake[1]['address'],
+				TwLabel(self.proto,dfake[2][self.lbl_id]).mmid,dfake[3]['address'],
+				TwLabel(self.proto,dfake[4][self.lbl_id]).mmid,dfake[5]['address']
 				),'--outdir='+self.tr.trash_dir] + cmd_args[1:]
+
 		end_silence()
 
 		if opt.verbose or opt.exact_output:
@@ -429,10 +434,10 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared):
 
 		t = self.spawn(
 			'mmgen-'+('txcreate','txdo')[bool(txdo_args)],
-			([],['--rbf'])[g.proto.cap('rbf')] +
+			([],['--rbf'])[self.proto.cap('rbf')] +
 			['-f',self.tx_fee,'-B'] + add_args + cmd_args + txdo_args)
 
-		if t.expect([('Get','Transac')[cmdline_inputs],'Unable to connect to \S+'],regex=True) == 1:
+		if t.expect([('Get','Unsigned transac')[cmdline_inputs],'Unable to connect to \S+'],regex=True) == 1:
 			raise TestSuiteException('\n'+t.p.after)
 
 		if cmdline_inputs:
@@ -441,9 +446,6 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared):
 
 		t.license()
 
-		if txdo_args and add_args: # txdo4
-			t.do_decrypt_ka_data(hp='1',pw=self.cfgs['14']['kapasswd'])
-
 		for num in tx_data:
 			t.expect_getend('ting address data from file ')
 			chk=t.expect_getend(r'Checksum for address data .*?: ',regex=True)
@@ -462,7 +464,11 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared):
 					inputs           = ' '.join(map(str,outputs_list)),
 					add_comment      = ('',tx_label_lat_cyr_gr)[do_label],
 					non_mmgen_inputs = (0,1)[bool(non_mmgen_input and not txdo_args)],
-					view             = view )
+					view             = view,
+					tweaks           = tweaks )
+
+		if txdo_args and add_args: # txdo4
+			t.do_decrypt_ka_data(hp='1',pw=self.cfgs['14']['kapasswd'])
 
 		return t
 
@@ -470,7 +476,7 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared):
 		return self.txcreate_common(sources=['1'],add_args=['--vsize-adj=1.01'])
 
 	def txbump(self,txfile,prepend_args=[],seed_args=[]):
-		if not g.proto.cap('rbf'):
+		if not self.proto.cap('rbf'):
 			msg('Skipping RBF'); return 'skip'
 		args = prepend_args + ['--quiet','--outdir='+self.tmpdir,txfile] + seed_args
 		t = self.spawn('mmgen-txbump',args)
@@ -490,8 +496,8 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared):
 				t.written_to_file('Transaction')
 		else:
 			t.do_comment(False)
-			t.expect('Save transaction? (y/N): ','y')
-			t.written_to_file('Transaction')
+			t.expect('Save fee-bumped transaction? (y/N): ','y')
+			t.written_to_file('Fee-bumped transaction')
 		os.unlink(txfile) # our tx file replaces the original
 		cmd = 'touch ' + joinpath(self.tmpdir,'txbump')
 		os.system(cmd)
@@ -619,8 +625,8 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared):
 	def txsign_keyaddr(self,keyaddr_file,txfile):
 		t = self.spawn('mmgen-txsign', ['-d',self.tmpdir,'-p1','-M',keyaddr_file,txfile])
 		t.license()
-		t.do_decrypt_ka_data(hp='1',pw=self.kapasswd)
 		t.view_tx('n')
+		t.do_decrypt_ka_data(hp='1',pw=self.kapasswd)
 		self.txsign_end(t)
 		return t
 
@@ -694,7 +700,8 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared):
 			sources         = ['1', '2', '3', '4', '14'],
 			non_mmgen_input = '4',
 			do_label        = True,
-			view            = 'y' )
+			view            = 'y',
+			tweaks          = ['confirm_non_mmgen'] )
 
 	def txsign4(self,f1,f2,f3,f4,f5,f6):
 		non_mm_file = joinpath(self.tmpdir,non_mmgen_fn)
@@ -708,8 +715,8 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared):
 			f1, f2, f3, f4, f5 ]
 		t = self.spawn('mmgen-txsign',add_args)
 		t.license()
-		t.do_decrypt_ka_data(hp='1',pw=self.cfgs['14']['kapasswd'])
 		t.view_tx('t')
+		t.do_decrypt_ka_data(hp='1',pw=self.cfgs['14']['kapasswd'])
 
 		for cnum,wcls in (('1',IncogWallet),('3',MMGenWallet)):
 			t.passphrase('{}'.format(wcls.desc),self.cfgs[cnum]['wpasswd'])
@@ -754,7 +761,8 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared):
 		return self.txcreate_common(
 			sources                    = ['20'],
 			non_mmgen_input            = '20',
-			non_mmgen_input_compressed = False )
+			non_mmgen_input_compressed = False,
+			tweaks                     = ['confirm_non_mmgen'] )
 
 	def txsign5(self,wf,txf,bad_vsize=True,add_args=[]):
 		non_mm_file = joinpath(self.tmpdir,non_mmgen_fn)
@@ -786,7 +794,8 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared):
 			sources                    = ['21'],
 			non_mmgen_input            = '21',
 			non_mmgen_input_compressed = False,
-			add_args                   = ['--vsize-adj=1.08'] )
+			add_args                   = ['--vsize-adj=1.08'],
+			tweaks                     = ['confirm_non_mmgen'] )
 
 	def txsign6(self,txf,wf):
 		return self.txsign5(txf,wf,bad_vsize=False,add_args=['--vsize-adj=1.08'])

+ 3 - 3
test/test_py_d/ts_misc.py

@@ -137,7 +137,7 @@ class TestSuiteRefTX(TestSuiteMain,TestSuiteBase):
 		return TestSuiteMain.__init__(self,trunner,cfgs,spawn)
 
 	def ref_tx_addrgen(self,atype):
-		if atype not in g.proto.mmtypes:
+		if atype not in self.proto.mmtypes:
 			return
 		t = self.spawn('mmgen-addrgen',['--outdir='+self.tmpdir,'--type='+atype,dfl_words_file,'1-2'])
 		t.read()
@@ -150,8 +150,8 @@ class TestSuiteRefTX(TestSuiteMain,TestSuiteBase):
 
 	def ref_tx_txcreate(self,f1,f2,f3,f4):
 		sources = ['31','32']
-		if 'S' in g.proto.mmtypes: sources += ['33']
-		if 'B' in g.proto.mmtypes: sources += ['34']
+		if 'S' in self.proto.mmtypes: sources += ['33']
+		if 'B' in self.proto.mmtypes: sources += ['34']
 		return self.txcreate_common(
 									addrs_per_wallet = 2,
 									sources          = sources,

+ 16 - 11
test/test_py_d/ts_ref.py

@@ -31,7 +31,6 @@ from .ts_base import *
 from .ts_shared import *
 
 wpasswd = 'reference password'
-nw_name = '{} {}'.format(g.coin,('Mainnet','Testnet')[g.proto.testnet])
 
 class TestSuiteRef(TestSuiteBase,TestSuiteShared):
 	'saved reference address, password and transaction files'
@@ -136,6 +135,10 @@ class TestSuiteRef(TestSuiteBase,TestSuiteShared):
 		('ref_tool_decrypt',   'decryption of saved MMGen-encrypted file'),
 	)
 
+	@property
+	def nw_desc(self):
+		return '{} {}'.format(self.proto.coin,('Mainnet','Testnet')[self.proto.testnet])
+
 	def _get_ref_subdir_by_coin(self,coin):
 		return {'btc': '',
 				'bch': '',
@@ -148,7 +151,7 @@ class TestSuiteRef(TestSuiteBase,TestSuiteShared):
 
 	@property
 	def ref_subdir(self):
-		return self._get_ref_subdir_by_coin(g.coin)
+		return self._get_ref_subdir_by_coin(self.proto.coin)
 
 	def ref_words_to_subwallet_chk1(self):
 		return self.ref_words_to_subwallet_chk('32L')
@@ -209,7 +212,9 @@ class TestSuiteRef(TestSuiteBase,TestSuiteShared):
 			mmtype   = None,
 			add_args = [],
 			id_key   = None,
-			pat      = '{}.*Legacy'.format(nw_name)):
+			pat      = None ):
+
+		pat = pat or f'{self.nw_desc}.*Legacy'
 		af_key = 'ref_{}file'.format(ftype) + ('_' + id_key if id_key else '')
 		af_fn = TestSuiteRef.sources[af_key].format(pfx or self.altcoin_pfx,'' if coin else self.tn_ext)
 		af = joinpath(ref_dir,(subdir or self.ref_subdir,'')[ftype=='passwd'],af_fn)
@@ -220,7 +225,7 @@ class TestSuiteRef(TestSuiteBase,TestSuiteShared):
 			t.do_decrypt_ka_data(hp=ref_kafile_hash_preset,pw=ref_kafile_pass,have_yes_opt=True)
 		chksum_key = '_'.join([af_key,'chksum'] + ([coin.lower()] if coin else []) + ([mmtype] if mmtype else []))
 		rc = self.chk_data[chksum_key]
-		ref_chksum = rc if (ftype == 'passwd' or coin) else rc[g.proto.base_coin.lower()][g.proto.testnet]
+		ref_chksum = rc if (ftype == 'passwd' or coin) else rc[self.proto.base_coin.lower()][self.proto.testnet]
 		if pat:
 			t.expect(pat,regex=True)
 		t.expect(chksum_pat,regex=True)
@@ -230,14 +235,14 @@ class TestSuiteRef(TestSuiteBase,TestSuiteShared):
 		return t
 
 	def ref_segwitaddrfile_chk(self):
-		if not 'S' in g.proto.mmtypes:
-			return skip(f'not supported by {g.protocol.cls_name} protocol')
-		return self.ref_addrfile_chk(ftype='segwitaddr',pat='{}.*Segwit'.format(nw_name))
+		if not 'S' in self.proto.mmtypes:
+			return skip(f'not supported by {self.proto.cls_name} protocol')
+		return self.ref_addrfile_chk(ftype='segwitaddr',pat='{}.*Segwit'.format(self.nw_desc))
 
 	def ref_bech32addrfile_chk(self):
-		if not 'B' in g.proto.mmtypes:
-			return skip(f'not supported by {g.protocol.cls_name} protocol')
-		return self.ref_addrfile_chk(ftype='bech32addr',pat='{}.*Bech32'.format(nw_name))
+		if not 'B' in self.proto.mmtypes:
+			return skip(f'not supported by {self.proto.cls_name} protocol')
+		return self.ref_addrfile_chk(ftype='bech32addr',pat='{}.*Bech32'.format(self.nw_desc))
 
 	def ref_keyaddrfile_chk(self):
 		return self.ref_addrfile_chk(ftype='keyaddr')
@@ -259,7 +264,7 @@ class TestSuiteRef(TestSuiteBase,TestSuiteShared):
 	def ref_passwdfile_chk_hex2bip39_12(self): return self.ref_passwdfile_chk(key='hex2bip39_12',pat=r'BIP39.*len.* 12\b')
 
 	def ref_tx_chk(self):
-		fn = self.sources['ref_tx_file'][g.coin.lower()][bool(self.tn_ext)]
+		fn = self.sources['ref_tx_file'][self.proto.coin.lower()][bool(self.tn_ext)]
 		if not fn: return
 		tf = joinpath(ref_dir,self.ref_subdir,fn)
 		wf = dfl_words_file

+ 33 - 35
test/test_py_d/ts_ref_altcoin.py

@@ -77,44 +77,42 @@ class TestSuiteRefAltcoin(TestSuiteRef,TestSuiteBase):
 		('ref_keyaddrfile_chk_zec_z','reference key-address file (ZEC-Z)'),
 		('ref_keyaddrfile_chk_xmr', 'reference key-address file (XMR)'),
 	)
-	# Check saved transaction files for *all* configured altcoins
-	# Though this basically duplicates the autosign test, here we do everything
-	# via the command line, so it's worth doing
+
 	def ref_altcoin_tx_chk(self):
+		"""
+		Check saved transaction files for *all* configured altcoins
+		Though this basically duplicates the autosign test, here we do everything
+		via the command line, so it's worth doing
+		"""
 		self.write_to_tmpfile(pwfile,dfl_wpasswd)
-		pf = joinpath(self.tmpdir,pwfile)
-		from mmgen.protocol import init_proto
-		from mmgen.daemon import CoinDaemon
-		for k in ('bch','eth','mm1','etc'):
-			coin,token = ('eth','mm1') if k == 'mm1' else (k,None)
-			ref_subdir = self._get_ref_subdir_by_coin(coin)
-			for tn in (False,True):
-				extra_opts = ['--coin='+coin,f'--testnet={int(tn)}']
-				if tn and coin == 'etc':
+		passfile = joinpath(self.tmpdir,pwfile)
+		from mmgen.txfile import MMGenTxFile
+		src = TestSuiteRef.sources['ref_tx_file']
+		for coin,files in src.items():
+			if coin == 'mm1':
+				coin = 'eth'
+				token_desc = ':MM1'
+			else:
+				token_desc = ''
+			for fn in files:
+				if not fn: # no etc testnet TX file
 					continue
-				if coin == 'bch':
-					network_id = get_network_id('bch',tn)
-					start_test_daemons(network_id)
-					extra_opts += [
-						'--daemon-data-dir=test/daemons/bch',
-						'--rpc-port={}'.format(CoinDaemon(network_id,test_suite=True).rpc_port) ]
-				g.proto = init_proto(coin,testnet=tn)
-				fn = TestSuiteRef.sources['ref_tx_file'][token or coin][bool(tn)]
-				tf = joinpath(ref_dir,ref_subdir,fn)
-				wf = dfl_words_file
-				if token:
-					extra_opts += ['--token='+token]
-				t = self.txsign(wf, tf, pf,
-								save       = False,
-								has_label  = True,
-								extra_desc = '({}{})'.format(token or coin,' testnet' if tn else ''),
-								extra_opts = extra_opts )
-				if coin == 'bch':
-					stop_test_daemons(network_id)
-				ok_msg()
-		g.proto = init_proto('btc')
-		t.skip_ok = True
-		return t
+				txfile = joinpath(
+					ref_dir,
+					self._get_ref_subdir_by_coin(coin),
+					fn )
+				proto = MMGenTxFile.get_proto(txfile,quiet_open=True)
+				if proto.sign_mode == 'daemon':
+					start_test_daemons(proto.network_id)
+				t = self.spawn(
+					'mmgen-txsign',
+					['--yes', f'--passwd-file={passfile}', dfl_words_file, txfile],
+					extra_desc = f'{proto.coin}{token_desc} {proto.network}')
+				t.read()
+				t.ok()
+				if proto.sign_mode == 'daemon':
+					stop_test_daemons(proto.network_id)
+		return 'ok'
 
 	def ref_altcoin_addrgen(self,coin,mmtype,gen_what='addr',coin_suf='',add_args=[]):
 		wf = dfl_words_file

+ 68 - 78
test/test_py_d/ts_regtest.py

@@ -138,6 +138,7 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
 	'transacting and tracking wallet operations via regtest mode'
 	networks = ('btc','ltc','bch')
 	passthru_opts = ('coin',)
+	extra_spawn_args = ['--regtest=1']
 	tmpdir_nums = [17]
 	cmd_group = (
 		('setup',                    'regtest (Bob and Alice) mode setup'),
@@ -244,18 +245,19 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
 	usr_subsids = { 'bob': {}, 'alice': {} }
 
 	def __init__(self,trunner,cfgs,spawn):
+		TestSuiteBase.__init__(self,trunner,cfgs,spawn)
 		os.environ['MMGEN_TEST_SUITE_REGTEST'] = '1'
-		from mmgen.regtest import MMGenRegtest
-		rt = MMGenRegtest(g.coin)
-		coin = g.coin.lower()
+		if self.proto.testnet:
+			die(2,'--testnet and --regtest options incompatible with regtest test suite')
+		self.proto = init_proto(self.proto.coin,network='regtest')
+		coin = self.proto.coin.lower()
 		for k in rt_data:
 			globals()[k] = rt_data[k][coin] if coin in rt_data[k] else None
-		return TestSuiteBase.__init__(self,trunner,cfgs,spawn)
 
 	def _add_comments_to_addr_file(self,addrfile,outfile,use_labels=False):
 		silence()
 		gmsg("Adding comments to address file '{}'".format(addrfile))
-		a = AddrList(addrfile)
+		a = AddrList(self.proto,addrfile)
 		for n,idx in enumerate(a.idxs(),1):
 			if use_labels:
 				a.set_comment(idx,get_label())
@@ -267,8 +269,6 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
 
 	def setup(self):
 		os.environ['MMGEN_BOGUS_WALLET_DATA'] = ''
-		if g.proto.testnet:
-			die(2,'--testnet option incompatible with regtest test suite')
 		try: shutil.rmtree(joinpath(self.tr.data_dir,'regtest'))
 		except: pass
 		os.environ['MMGEN_TEST_SUITE'] = '' # mnemonic is piped to stdin, so stop being a terminal
@@ -295,7 +295,7 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
 	def walletgen_alice(self): return self.walletgen('alice')
 
 	def _user_dir(self,user,coin=None):
-		return joinpath(self.tr.data_dir,'regtest',coin or g.coin.lower(),user)
+		return joinpath(self.tr.data_dir,'regtest',coin or self.proto.coin.lower(),user)
 
 	def _user_sid(self,user):
 		return os.path.basename(get_file_with_ext(self._user_dir(user),'mmdat'))[:8]
@@ -315,7 +315,7 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
 
 	def addrgen(self,user,wf=None,addr_range='1-5',subseed_idx=None,mmtypes=[]):
 		from mmgen.addr import MMGenAddrType
-		for mmtype in mmtypes or g.proto.mmtypes:
+		for mmtype in mmtypes or self.proto.mmtypes:
 			t = self.spawn('mmgen-addrgen',
 				['--quiet','--'+user,'--type='+mmtype,'--outdir={}'.format(self._user_dir(user))] +
 				([wf] if wf else []) +
@@ -335,17 +335,14 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
 		id_strs = { 'legacy':'', 'compressed':'-C', 'segwit':'-S', 'bech32':'-B' }
 		if not sid: sid = self._user_sid(user)
 		from mmgen.addr import MMGenAddrType
-		for mmtype in mmtypes or g.proto.mmtypes:
+		for mmtype in mmtypes or self.proto.mmtypes:
 			desc = MMGenAddrType.mmtypes[mmtype].name
 			addrfile = joinpath(self._user_dir(user),
-				'{}{}{}[{}]{x}.testnet.addrs'.format(
+				'{}{}{}[{}]{x}.regtest.addrs'.format(
 					sid,self.altcoin_pfx,id_strs[desc],addr_range,
 					x='-α' if g.debug_utf8 else ''))
-			if mmtype == g.proto.mmtypes[0] and user == 'bob':
-				psave = g.proto
-				g.proto = init_proto(g.coin,regtest=True)
+			if mmtype == self.proto.mmtypes[0] and user == 'bob':
 				self._add_comments_to_addr_file(addrfile,addrfile,use_labels=True)
-				g.proto = psave
 			t = self.spawn( 'mmgen-addrimport',
 							['--quiet', '--'+user, '--batch', addrfile],
 							extra_desc='({})'.format(desc))
@@ -365,7 +362,7 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
 		if not sid: sid = self._user_sid(user)
 		addr = self.get_addr_from_addrlist(user,sid,mmtype,0,addr_range=addr_range)
 		t = self.spawn('mmgen-regtest', ['send',str(addr),str(amt)])
-		t.expect('Sending {} miner {}'.format(amt,g.coin))
+		t.expect(f'Sending {amt} miner {self.proto.coin}')
 		t.expect('Mined 1 block')
 		return t
 
@@ -373,7 +370,7 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
 		return self.fund_wallet('bob','C',rtFundAmt)
 
 	def fund_alice(self):
-		return self.fund_wallet('alice',('L','S')[g.proto.cap('segwit')],rtFundAmt)
+		return self.fund_wallet('alice',('L','S')[self.proto.cap('segwit')],rtFundAmt)
 
 	def user_twview(self,user,chk=None,sort='age'):
 		t = self.spawn('mmgen-tool',['--'+user,'twview','sort='+sort])
@@ -388,8 +385,7 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
 		if skip_check:
 			t.read()
 		else:
-			total = t.expect_getend('TOTAL: ')
-			cmp_or_die('{} {}'.format(bal,g.coin),total)
+			cmp_or_die(f'{bal} {self.proto.coin}',t.expect_getend('TOTAL: '))
 		t.req_exit_val = exit_val
 		return t
 
@@ -451,9 +447,9 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
 	def bob_subwallet_fund(self):
 		sid1 = self._get_user_subsid('bob','29L')
 		sid2 = self._get_user_subsid('bob','127S')
-		chg_addr = self._user_sid('bob') + (':B:1',':L:1')[g.coin=='BCH']
+		chg_addr = self._user_sid('bob') + (':B:1',':L:1')[self.proto.coin=='BCH']
 		outputs_cl = [sid1+':C:2,0.29',sid2+':C:3,0.127',chg_addr]
-		inputs = ('3','1')[g.coin=='BCH']
+		inputs = ('3','1')[self.proto.coin=='BCH']
 		return self.user_txdo('bob',rtFee[1],outputs_cl,inputs,extra_args=['--subseeds=127'])
 
 	def bob_twview2(self):
@@ -471,7 +467,7 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
 		t = self.spawn('mmgen-txcreate',['-d',self.tmpdir,'-B','--bob'] + outputs_cl)
 		return self.txcreate_ui_common(t,
 								menu            = ['a'],
-								inputs          = ('1,2','2,3')[g.coin=='BCH'],
+								inputs          = ('1,2','2,3')[self.proto.coin=='BCH'],
 								interactive_fee = '0.00001')
 
 	def bob_subwallet_txsign(self):
@@ -486,12 +482,12 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
 
 	def bob_subwallet_txdo(self):
 		outputs_cl = [self._user_sid('bob')+':L:5']
-		inputs = ('1,2','2,3')[g.coin=='BCH']
+		inputs = ('1,2','2,3')[self.proto.coin=='BCH']
 		return self.user_txdo('bob',rtFee[5],outputs_cl,inputs,menu=['a'],extra_args=['--subseeds=127']) # sort: amt
 
 	def bob_twview4(self):
 		sid = self._user_sid('bob')
-		amt = ('0.4169328','0.41364')[g.coin=='LTC']
+		amt = ('0.4169328','0.41364')[self.proto.coin=='LTC']
 		return self.user_twview('bob',chk=r'\b{}:L:5\b\s+.*\s+\b{}\b'.format(sid,amt),sort='twmmid')
 
 	def bob_getbalance(self,bals,confs=1):
@@ -499,7 +495,7 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
 			assert Decimal(bals['mmgen'][i]) + Decimal(bals['nonmm'][i]) == Decimal(bals['total'][i])
 		t = self.spawn('mmgen-tool',['--bob','getbalance','minconf={}'.format(confs)])
 		for k in ('mmgen','nonmm','total'):
-			t.expect(r'\n\S+:\s+{} {c}\s+{} {c}\s+{} {c}'.format(*bals[k],c=g.coin),regex=True)
+			t.expect(r'\n\S+:\s+{} {c}\s+{} {c}\s+{} {c}'.format(*bals[k],c=self.proto.coin),regex=True)
 		t.read()
 		return t
 
@@ -566,15 +562,12 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
 
 	def get_addr_from_addrlist(self,user,sid,mmtype,idx,addr_range='1-5'):
 		id_str = { 'L':'', 'S':'-S', 'C':'-C', 'B':'-B' }[mmtype]
-		ext = '{}{}{}[{}]{x}.testnet.addrs'.format(
+		ext = '{}{}{}[{}]{x}.regtest.addrs'.format(
 			sid,self.altcoin_pfx,id_str,addr_range,x='-α' if g.debug_utf8 else '')
 		addrfile = get_file_with_ext(self._user_dir(user),ext,no_dot=True)
-		psave = g.proto
-		g.proto = init_proto(g.coin,regtest=True)
 		silence()
-		addr = AddrList(addrfile).data[idx].addr
+		addr = AddrList(self.proto,addrfile).data[idx].addr
 		end_silence()
-		g.proto = psave
 		return addr
 
 	def _create_tx_outputs(self,user,data):
@@ -582,16 +575,16 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
 		return [self.get_addr_from_addrlist(user,sid,mmtype,idx-1)+amt_str for mmtype,idx,amt_str in data]
 
 	def bob_rbf_1output_create(self):
-		if g.coin != 'BTC':
-			return 'skip' # non-coin-dependent test, so run just once for BTC
+		if self.proto.coin != 'BTC': # non-coin-dependent test, so run just once for BTC
+			return 'skip'
 		out_addr = self._create_tx_outputs('alice',(('B',5,''),))
 		t = self.spawn('mmgen-txcreate',['-d',self.tr.trash_dir,'-B','--bob','--rbf'] + out_addr)
 		return self.txcreate_ui_common(t,menu=[],inputs='3',interactive_fee='3s') # out amt: 199.99999343
 
 	def bob_rbf_1output_bump(self):
-		if g.coin != 'BTC':
+		if self.proto.coin != 'BTC':
 			return 'skip'
-		ext = '9343,3]{x}.testnet.rawtx'.format(x='-α' if g.debug_utf8 else '')
+		ext = '9343,3]{x}.regtest.rawtx'.format(x='-α' if g.debug_utf8 else '')
 		txfile = get_file_with_ext(self.tr.trash_dir,ext,delete=False,no_dot=True)
 		return self.user_txbump('bob',
 			self.tr.trash_dir,
@@ -605,12 +598,12 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
 		outputs_cl = self._create_tx_outputs('alice',(('L',1,',60'),('C',1,',40'))) # alice_sid:L:1, alice_sid:C:1
 		outputs_cl += [self._user_sid('bob')+':'+rtBobOp3]
 		return self.user_txdo('bob',rtFee[1],outputs_cl,'3',
-					extra_args=([],['--rbf'])[g.proto.cap('rbf')])
+					extra_args=([],['--rbf'])[self.proto.cap('rbf')])
 
 	def bob_send_non_mmgen(self):
 		outputs_cl = self._create_tx_outputs('alice',(
-			(('L','S')[g.proto.cap('segwit')],2,',10'),
-			(('L','S')[g.proto.cap('segwit')],3,'')
+			(('L','S')[self.proto.cap('segwit')],2,',10'),
+			(('L','S')[self.proto.cap('segwit')],3,'')
 		)) # alice_sid:S:2, alice_sid:S:3
 		keyfile = joinpath(self.tmpdir,'non-mmgen.keys')
 		return self.user_txdo('bob',rtFee[3],outputs_cl,'1,4-10',
@@ -621,7 +614,7 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
 		return self.user_txdo('alice',None,outputs_cl,'1') # fee=None
 
 	def user_txbump(self,user,outdir,txfile,fee,add_args=[],has_label=True,signed_tx=True,one_output=False):
-		if not g.proto.cap('rbf'):
+		if not self.proto.cap('rbf'):
 			return 'skip'
 		os.environ['MMGEN_BOGUS_SEND'] = ''
 		t = self.spawn('mmgen-txbump',
@@ -636,19 +629,18 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
 			t.written_to_file('Signed transaction')
 			self.txsend_ui_common(t,caller='txdo',bogus_send=False,file_desc='Signed transaction')
 		else:
-			t.expect('Save transaction? (y/N): ','y')
-			t.written_to_file('Transaction')
+			t.expect('Save fee-bumped transaction? (y/N): ','y')
+			t.written_to_file('Fee-bumped transaction')
 		t.read()
 		return t
 
 	def bob_rbf_bump(self):
-		ext = ',{}]{x}.testnet.sigtx'.format(rtFee[1][:-1],x='-α' if g.debug_utf8 else '')
+		ext = ',{}]{x}.regtest.sigtx'.format(rtFee[1][:-1],x='-α' if g.debug_utf8 else '')
 		txfile = self.get_file_with_ext(ext,delete=False,no_dot=True)
 		return self.user_txbump('bob',self.tmpdir,txfile,rtFee[2],add_args=['--send'])
 
 	def generate(self,coin=None,num_blocks=1):
 		int(num_blocks)
-		if coin: opt.coin = coin
 		t = self.spawn('mmgen-regtest',['generate',str(num_blocks)])
 		t.expect('Mined {} block'.format(num_blocks))
 		return t
@@ -670,19 +662,19 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
 		return 'ok'
 
 	def bob_rbf_status(self,fee,exp1,exp2=''):
-		if not g.proto.cap('rbf'):
+		if not self.proto.cap('rbf'):
 			return 'skip'
-		ext = ',{}]{x}.testnet.sigtx'.format(fee[:-1],x='-α' if g.debug_utf8 else '')
+		ext = ',{}]{x}.regtest.sigtx'.format(fee[:-1],x='-α' if g.debug_utf8 else '')
 		txfile = self.get_file_with_ext(ext,delete=False,no_dot=True)
 		return self.user_txsend_status('bob',txfile,exp1,exp2)
 
 	def bob_rbf_status1(self):
-		if not g.proto.cap('rbf'):
+		if not self.proto.cap('rbf'):
 			return 'skip'
 		return self.bob_rbf_status(rtFee[1],'in mempool, replaceable')
 
 	def get_mempool2(self):
-		if not g.proto.cap('rbf'):
+		if not self.proto.cap('rbf'):
 			return 'skip'
 		mp = self._get_mempool()
 		if len(mp) != 1:
@@ -694,19 +686,19 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
 		return 'ok'
 
 	def bob_rbf_status2(self):
-		if not g.proto.cap('rbf'):
+		if not self.proto.cap('rbf'):
 			return 'skip'
 		new_txid = self.read_from_tmpfile('rbf_txid2').strip()
 		return self.bob_rbf_status(rtFee[1],
 			'Transaction has been replaced','{} in mempool'.format(new_txid))
 
 	def bob_rbf_status3(self):
-		if not g.proto.cap('rbf'):
+		if not self.proto.cap('rbf'):
 			return 'skip'
 		return self.bob_rbf_status(rtFee[2],'status: in mempool, replaceable')
 
 	def bob_rbf_status4(self):
-		if not g.proto.cap('rbf'):
+		if not self.proto.cap('rbf'):
 			return 'skip'
 		new_txid = self.read_from_tmpfile('rbf_txid2').strip()
 		return self.bob_rbf_status(rtFee[1],
@@ -714,12 +706,12 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
 			'Replacing transactions:\s+{}'.format(new_txid))
 
 	def bob_rbf_status5(self):
-		if not g.proto.cap('rbf'):
+		if not self.proto.cap('rbf'):
 			return 'skip'
 		return self.bob_rbf_status(rtFee[2],'Transaction has 1 confirmation')
 
 	def bob_rbf_status6(self):
-		if not g.proto.cap('rbf'):
+		if not self.proto.cap('rbf'):
 			return 'skip'
 		new_txid = self.read_from_tmpfile('rbf_txid2').strip()
 		return self.bob_rbf_status(rtFee[1],
@@ -730,7 +722,7 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
 	def _gen_pairs(n):
 		disable_debug()
 		from subprocess import run,PIPE
-		ret = [run(['python3',joinpath('cmds','mmgen-tool'),'--testnet=1'] +
+		ret = [run(['python3',joinpath('cmds','mmgen-tool'),'--regtest=1'] +
 					(['--type=compressed'],[])[i==0] +
 					['-r0','randpair'],
 					stdout=PIPE,check=True
@@ -771,8 +763,8 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
 		outputs1 = list(map('{},{}'.format,addrs,amts))
 		sid = self._user_sid('bob')
 		l1,l2 = (
-			(':S',':B') if 'B' in g.proto.mmtypes else
-			(':S',':S') if g.proto.cap('segwit') else
+			(':S',':B') if 'B' in self.proto.mmtypes else
+			(':S',':S') if self.proto.cap('segwit') else
 			(':L',':L') )
 		outputs2 = [sid+':C:2,6.333', sid+':L:3,6.667',sid+l1+':4,0.123',sid+l2+':5']
 		return self.user_txdo('bob',rtFee[5],outputs1+outputs2,'1-2')
@@ -799,20 +791,20 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
 		sid = self._user_sid('alice')
 		return self.user_add_label('alice',sid+':C:1','Replacement Label')
 
-	def alice_add_label_coinaddr(self):
-		mmid = self._user_sid('alice') + (':S:1',':L:1')[g.coin=='BCH']
-		t = self.spawn('mmgen-tool',['--alice','listaddress',mmid],no_msg=True)
-		btcaddr = [i for i in t.read().splitlines() if i.lstrip()[0:len(mmid)] == mmid][0].split()[1]
-		return self.user_add_label('alice',btcaddr,'Label added using coin address')
-
-	def user_chk_label(self,user,addr,label):
+	def _user_chk_label(self,user,addr,label):
 		t = self.spawn('mmgen-tool',['--'+user,'listaddresses','all_labels=1'])
 		t.expect(r'{}\s+\S{{30}}\S+\s+{}\s+'.format(addr,label),regex=True)
 		return t
 
+	def alice_add_label_coinaddr(self):
+		mmid = self._user_sid('alice') + (':S:1',':L:1')[self.proto.coin=='BCH']
+		t = self.spawn('mmgen-tool',['--alice','listaddress',mmid],no_msg=True)
+		addr = [i for i in t.read().splitlines() if i.startswith(mmid)][0].split()[1]
+		return self.user_add_label('alice',addr,'Label added using coin address of MMGen address')
+
 	def alice_chk_label_coinaddr(self):
-		mmid = self._user_sid('alice') + (':S:1',':L:1')[g.coin=='BCH']
-		return self.user_chk_label('alice',mmid,'Label added using coin address')
+		mmid = self._user_sid('alice') + (':S:1',':L:1')[self.proto.coin=='BCH']
+		return self._user_chk_label('alice',mmid,'Label added using coin address of MMGen address')
 
 	def alice_add_label_badaddr(self,addr,reply):
 		t = self.spawn('mmgen-tool',['--alice','add_label',addr,'(none)'])
@@ -820,21 +812,19 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
 		return t
 
 	def alice_add_label_badaddr1(self):
-		return self.alice_add_label_badaddr(rt_pw,'Invalid coin address for this chain: ')
+		return self.alice_add_label_badaddr( rt_pw,'Invalid coin address for this chain: ')
 
 	def alice_add_label_badaddr2(self):
-		addr = g.proto.pubhash2addr('00'*20,False) # mainnet zero address
-		return self.alice_add_label_badaddr(addr,'Invalid coin address for this chain: '+addr)
+		addr = init_proto(self.proto.coin,network='mainnet').pubhash2addr('00'*20,False) # mainnet zero address
+		return self.alice_add_label_badaddr( addr, f'Invalid coin address for this chain: {addr}' )
 
 	def alice_add_label_badaddr3(self):
 		addr = self._user_sid('alice') + ':C:123'
-		return self.alice_add_label_badaddr(addr,
-			"MMGen address '{}' not found in tracking wallet".format(addr))
+		return self.alice_add_label_badaddr( addr, f'MMGen address {addr!r} not found in tracking wallet' )
 
 	def alice_add_label_badaddr4(self):
-		addr = init_proto(g.coin,regtest=True).pubhash2addr('00'*20,False) # testnet zero address
-		return self.alice_add_label_badaddr(addr,
-			"Address '{}' not found in tracking wallet".format(addr))
+		addr = self.proto.pubhash2addr('00'*20,False) # regtest (testnet) zero address
+		return self.alice_add_label_badaddr( addr, f'Address {addr!r} not found in tracking wallet' )
 
 	def alice_bal_rpcfail(self):
 		addr = self._user_sid('alice') + ':C:2'
@@ -848,29 +838,29 @@ class TestSuiteRegtest(TestSuiteBase,TestSuiteShared):
 
 	def alice_remove_label1(self):
 		sid = self._user_sid('alice')
-		mmid = sid + (':S:3',':L:3')[g.coin=='BCH']
+		mmid = sid + (':S:3',':L:3')[self.proto.coin=='BCH']
 		return self.user_remove_label('alice',mmid)
 
 	def alice_chk_label1(self):
 		sid = self._user_sid('alice')
-		return self.user_chk_label('alice',sid+':C:1','Original Label - 月へ')
+		return self._user_chk_label('alice',sid+':C:1','Original Label - 月へ')
 
 	def alice_chk_label2(self):
 		sid = self._user_sid('alice')
-		return self.user_chk_label('alice',sid+':C:1','Replacement Label')
+		return self._user_chk_label('alice',sid+':C:1','Replacement Label')
 
 	def alice_edit_label1(self): return self.user_edit_label('alice','4',tw_label_lat_cyr_gr)
 	def alice_edit_label2(self): return self.user_edit_label('alice','3',tw_label_zh)
 
 	def alice_chk_label3(self):
 		sid = self._user_sid('alice')
-		mmid = sid + (':S:3',':L:3')[g.coin=='BCH']
-		return self.user_chk_label('alice',mmid,tw_label_lat_cyr_gr)
+		mmid = sid + (':S:3',':L:3')[self.proto.coin=='BCH']
+		return self._user_chk_label('alice',mmid,tw_label_lat_cyr_gr)
 
 	def alice_chk_label4(self):
 		sid = self._user_sid('alice')
-		mmid = sid + (':S:3',':L:3')[g.coin=='BCH']
-		return self.user_chk_label('alice',mmid,'-')
+		mmid = sid + (':S:3',':L:3')[self.proto.coin=='BCH']
+		return self._user_chk_label('alice',mmid,'-')
 
 	def user_edit_label(self,user,output,label):
 		t = self.spawn('mmgen-txcreate',['-B','--'+user,'-i'])

+ 11 - 9
test/test_py_d/ts_shared.py

@@ -35,7 +35,7 @@ class TestSuiteShared(object):
 							caller            = None,
 							menu              = [],
 							inputs            = '1',
-							file_desc         = 'Transaction',
+							file_desc         = 'Unsigned transaction',
 							input_sels_prompt = 'to spend',
 							bad_input_sels    = False,
 							non_mmgen_inputs  = 0,
@@ -45,7 +45,8 @@ class TestSuiteShared(object):
 							eth_fee_res       = None,
 							add_comment       = '',
 							view              = 't',
-							save              = True ):
+							save              = True,
+							tweaks            = [] ):
 
 		txdo = (caller or self.test_name)[:4] == 'txdo'
 
@@ -54,11 +55,8 @@ class TestSuiteShared(object):
 		if bad_input_sels:
 			for r in ('x','3-1','9999'):
 				t.expect(input_sels_prompt+': ',r+'\n')
-		t.expect(input_sels_prompt+': ',inputs+'\n')
 
-		if not txdo:
-			for i in range(non_mmgen_inputs):
-				t.expect('Accept? (y/N): ','y')
+		t.expect(input_sels_prompt+': ',inputs+'\n')
 
 		have_est_fee = t.expect([fee_desc+': ','OK? (Y/n): ']) == 1
 		if have_est_fee and not interactive_fee:
@@ -66,7 +64,7 @@ class TestSuiteShared(object):
 		else:
 			if have_est_fee:
 				t.send('n')
-				if g.coin == 'BCH' or g.proto.base_coin == 'ETH': # TODO: pexpect race condition?
+				if self.proto.coin == 'BCH' or self.proto.base_coin == 'ETH': # TODO: pexpect race condition?
 					time.sleep(0.1)
 			if eth_fee_res:
 				t.expect('or gas price: ',interactive_fee+'\n')
@@ -76,6 +74,10 @@ class TestSuiteShared(object):
 			t.expect('OK? (Y/n): ','y')
 
 		t.expect('(Y/n): ','\n')     # chg amt OK?
+
+		if 'confirm_non_mmgen' in tweaks:
+			t.expect('Continue? (Y/n)','\n')
+
 		t.do_comment(add_comment)
 		t.view_tx(view)
 		if not txdo:
@@ -225,7 +227,7 @@ class TestSuiteShared(object):
 		t.read() if stdout else t.written_to_file(('Addresses','Password list')[passgen])
 		if check_ref:
 			chk_ref = (self.chk_data[self.test_name] if passgen else
-						self.chk_data[self.test_name][self.fork][g.proto.testnet])
+						self.chk_data[self.test_name][self.fork][self.proto.testnet])
 			cmp_or_die(chk,chk_ref,desc='{}list data checksum'.format(ftype))
 		return t
 
@@ -241,7 +243,7 @@ class TestSuiteShared(object):
 		t.passphrase(wcls.desc,self.wpasswd)
 		chk = t.expect_getend(r'Checksum for key-address data .*?: ',regex=True)
 		if check_ref:
-			chk_ref = self.chk_data[self.test_name][self.fork][g.proto.testnet]
+			chk_ref = self.chk_data[self.test_name][self.fork][self.proto.testnet]
 			cmp_or_die(chk,chk_ref,desc='key-address list data checksum')
 		t.expect('Encrypt key list? (y/N): ','y')
 		t.usr_rand(self.usr_rand_chars)

+ 1 - 1
test/test_py_d/ts_wallet.py

@@ -48,7 +48,7 @@ class TestSuiteWalletConv(TestSuiteBase,TestSuiteShared):
 					'hic_wallet_old':  '1378FC64-B55E9958-D85FF20C[192,1].incog-old.offset123',
 				},
 				'256': {
-					'ref_wallet':      '98831F3A-{}[256,1].mmdat'.format(('27F2BF93','E2687906')[g.proto.testnet]),
+					'ref_wallet':      '98831F3A-27F2BF93[256,1].mmdat',
 					'ic_wallet':       '98831F3A-5482381C-18460FB1[256,1].mmincog',
 					'ic_wallet_hex':   '98831F3A-1630A9F2-870376A9[256,1].mmincox',
 

+ 20 - 13
test/tooltest.py

@@ -54,6 +54,9 @@ sys.argv = [sys.argv[0]] + ['--skip-cfg-file'] + sys.argv[1:]
 
 cmd_args = opts.init(opts_data,add_opts=['exact_output','profile'])
 
+from mmgen.protocol import init_proto_from_opts
+proto = init_proto_from_opts()
+
 cmd_data = {
 	'cryptocoin': {
 		'desc': 'Cryptocoin address/key commands',
@@ -83,7 +86,7 @@ cmd_data = {
 	},
 }
 
-if g.coin in ('BTC','LTC'):
+if proto.coin in ('BTC','LTC'):
 	cmd_data['cryptocoin']['cmd_data'].update({
 		'pubhex2redeem_script': ('privhex2pubhex','o3'),
 		'wif2redeem_script':    ('randpair','o3'),
@@ -117,9 +120,9 @@ cfg = {
 	}
 }
 
-ref_subdir  = '' if g.proto.base_coin == 'BTC' else g.proto.name.lower()
-altcoin_pfx = '' if g.proto.base_coin == 'BTC' else '-'+g.proto.base_coin
-tn_ext = ('','.testnet')[g.proto.testnet]
+ref_subdir  = '' if proto.base_coin == 'BTC' else proto.name.lower()
+altcoin_pfx = '' if proto.base_coin == 'BTC' else '-'+proto.base_coin
+tn_ext = ('','.testnet')[proto.testnet]
 
 mmgen_cmd = 'mmgen-tool'
 
@@ -180,18 +183,22 @@ if opt.list_names:
 	die(0,'\n{}\n  {}'.format(yellow('Untested commands:'),'\n  '.join(uc)))
 
 from mmgen.tx import is_wif,is_coin_addr
+def is_wif_loc(s):
+	return is_wif(proto,s)
+def is_coin_addr_loc(s):
+	return is_coin_addr(proto,s)
 
 msg_w = 35
 def test_msg(m):
 	m2 = 'Testing {}'.format(m)
 	msg_r(green(m2+'\n') if opt.verbose else '{:{w}}'.format(m2,w=msg_w+8))
 
-compressed = ('','compressed')['C' in g.proto.mmtypes]
-segwit     = ('','segwit')['S' in g.proto.mmtypes]
-bech32     = ('','bech32')['B' in g.proto.mmtypes]
-type_compressed_arg = ([],['--type=compressed'])['C' in g.proto.mmtypes]
-type_segwit_arg     = ([],['--type=segwit'])['S' in g.proto.mmtypes]
-type_bech32_arg     = ([],['--type=bech32'])['B' in g.proto.mmtypes]
+compressed = ('','compressed')['C' in proto.mmtypes]
+segwit     = ('','segwit')['S' in proto.mmtypes]
+bech32     = ('','bech32')['B' in proto.mmtypes]
+type_compressed_arg = ([],['--type=compressed'])['C' in proto.mmtypes]
+type_segwit_arg     = ([],['--type=segwit'])['S' in proto.mmtypes]
+type_bech32_arg     = ([],['--type=bech32'])['B' in proto.mmtypes]
 
 class MMGenToolTestUtils(object):
 
@@ -293,13 +300,13 @@ class MMGenToolTestCmds(object):
 		for n,k in enumerate(['',compressed]):
 			ao = ['--type='+k] if k else []
 			ret = tu.run_cmd_out(name,add_opts=ao,Return=True,fn_idx=n+1)
-			ok_or_die(ret,is_wif,'WIF key')
+			ok_or_die(ret,is_wif_loc,'WIF key')
 	def randpair(self,name):
 		for n,k in enumerate(['',compressed,segwit,bech32]):
 			ao = ['--type='+k] if k else []
 			wif,addr = tu.run_cmd_out(name,add_opts=ao,Return=True,fn_idx=n+1,literal=True).split()
-			ok_or_die(wif,is_wif,'WIF key',skip_ok=True)
-			ok_or_die(addr,is_coin_addr,'Coin address')
+			ok_or_die(wif,is_wif_loc,'WIF key',skip_ok=True)
+			ok_or_die(addr,is_coin_addr_loc,'Coin address')
 	def wif2addr(self,name,f1,f2,f3,f4):
 		for n,f,k in (
 			(1,f1,''),

+ 31 - 15
test/tooltest2.py

@@ -30,16 +30,23 @@ from decimal import Decimal
 from include.tests_header import repo_root
 from mmgen.common import *
 from test.include.common import *
-from mmgen.obj import is_wif,is_coin_addr
 from mmgen.wallet import is_bip39_mnemonic,is_mmgen_mnemonic
 from mmgen.addr import is_xmrseed
 from mmgen.baseconv import *
 
+skipped_tests = ['mn2hex_interactive']
+
 NL = ('\n','\r\n')[g.platform=='win']
 
 def is_str(s):
 	return type(s) == str
 
+from mmgen.obj import is_wif,is_coin_addr
+def is_wif_loc(s):
+	return is_wif(proto,s)
+def is_coin_addr_loc(s):
+	return is_coin_addr(proto,s)
+
 def md5_hash(s):
 	from hashlib import md5
 	return md5(s.encode()).hexdigest()
@@ -559,12 +566,12 @@ tests = {
 			],
 		},
 		'randpair': {
-			'btc_mainnet': [ ( [], [is_wif,is_coin_addr], ['-r0'] ) ],
-			'btc_testnet': [ ( [], [is_wif,is_coin_addr], ['-r0'] ) ],
+			'btc_mainnet': [ ( [], [is_wif_loc,is_coin_addr_loc], ['-r0'] ) ],
+			'btc_testnet': [ ( [], [is_wif_loc,is_coin_addr_loc], ['-r0'] ) ],
 		},
 		'randwif': {
-			'btc_mainnet': [ ( [], is_wif, ['-r0'] ) ],
-			'btc_testnet': [ ( [], is_wif, ['-r0'] ) ],
+			'btc_mainnet': [ ( [], is_wif_loc, ['-r0'] ) ],
+			'btc_testnet': [ ( [], is_wif_loc, ['-r0'] ) ],
 		},
 		'wif2addr': {
 			'btc_mainnet': [
@@ -773,11 +780,13 @@ tests = {
 
 coin_dependent_groups = ('Coin','File') # TODO: do this as attr of each group in tool.py
 
-def run_test(gid,cmd_name):
+async def run_test(gid,cmd_name):
 	data = tests[gid][cmd_name]
-	# behavior is like test.py: run coin-dependent tests only if g.proto.testnet or g.coin != BTC
+	# behavior is like test.py: run coin-dependent tests only if proto.testnet or proto.coin != BTC
 	if gid in coin_dependent_groups:
-		k = '{}_{}net'.format((g.token.lower() if g.token else g.coin.lower()),('main','test')[g.proto.testnet])
+		k = '{}_{}'.format(
+			( g.token.lower() if proto.tokensym else proto.coin.lower() ),
+			('mainnet','testnet')[proto.testnet] )
 		if k in data:
 			data = data[k]
 			m2 = ' ({})'.format(k)
@@ -785,7 +794,7 @@ def run_test(gid,cmd_name):
 			qmsg(f'-- no data for {cmd_name} ({k}) - skipping')
 			return
 	else:
-		if g.coin != 'BTC' or g.proto.testnet:
+		if proto.coin != 'BTC' or proto.testnet:
 			return
 		m2 = ''
 	m = '{} {}{}'.format(purple('Testing'), cmd_name if opt.names else docstring_head(tc[cmd_name]),m2)
@@ -810,7 +819,7 @@ def run_test(gid,cmd_name):
 
 		return cmd_out.strip()
 
-	def run_func(cmd_name,args,out,opts,exec_code):
+	async def run_func(cmd_name,args,out,opts,exec_code):
 		vmsg('{}: {}{}'.format(purple('Running'),
 				' '.join([cmd_name]+[repr(e) for e in args]),
 				' '+exec_code if exec_code else '' ))
@@ -837,6 +846,8 @@ def run_test(gid,cmd_name):
 				sys.exit(0)
 		else:
 			ret = tc.call(cmd_name,*aargs,**kwargs)
+			if type(ret).__name__ == 'coroutine':
+				ret = await ret
 			opt.quiet = oq_save
 			return ret
 
@@ -873,7 +884,7 @@ def run_test(gid,cmd_name):
 			if stdin_input and g.platform == 'win':
 				msg('Skipping for MSWin - no os.fork()')
 				continue
-			cmd_out = run_func(cmd_name,args,out,opts,exec_code)
+			cmd_out = await run_func(cmd_name,args,out,opts,exec_code)
 
 		try:    vmsg('Output:\n{}\n'.format(cmd_out))
 		except: vmsg('Output:\n{}\n'.format(repr(cmd_out)))
@@ -925,12 +936,14 @@ def run_test(gid,cmd_name):
 def docstring_head(obj):
 	return obj.__doc__.strip().split('\n')[0]
 
-def do_group(gid):
+async def do_group(gid):
 	qmsg(blue('Testing ' +
 		f'command group {gid!r}' if opt.names else
 		docstring_head(tc.classes['MMGenToolCmd'+gid]) ))
 
 	for cname in tc.classes['MMGenToolCmd'+gid].user_commands:
+		if cname in skipped_tests:
+			continue
 		if cname not in tests[gid]:
 			m = f'No test for command {cname!r} in group {gid!r}!'
 			if opt.die_on_missing:
@@ -938,7 +951,7 @@ def do_group(gid):
 			else:
 				msg(m)
 				continue
-		run_test(gid,cname)
+		await run_test(gid,cname)
 
 def do_cmd_in_group(cmd):
 	for gid in tests:
@@ -956,6 +969,9 @@ sys.argv = [sys.argv[0]] + ['--skip-cfg-file'] + sys.argv[1:]
 
 cmd_args = opts.init(opts_data,add_opts=['use_old_ed25519'])
 
+from mmgen.protocol import init_proto_from_opts
+proto = init_proto_from_opts()
+
 if opt.tool_api:
 	del tests['Wallet']
 	del tests['File']
@@ -1003,7 +1019,7 @@ else:
 
 start_time = int(time.time())
 
-def main():
+async def main():
 	try:
 		if cmd_args:
 			for cmd in cmd_args:
@@ -1018,7 +1034,7 @@ def main():
 	except KeyboardInterrupt:
 		die(1,green('\nExiting at user request'))
 
-main()
+run_session(main())
 
 t = int(time.time()) - start_time
 gmsg('All requested tests finished OK, elapsed time: {:02}:{:02}'.format(t//60,t%60))

+ 51 - 62
test/unit_tests_d/ut_rpc.py

@@ -7,10 +7,10 @@ from mmgen.common import *
 from mmgen.exception import *
 
 from mmgen.protocol import init_proto
-from mmgen.rpc import MoneroWalletRPCClient
+from mmgen.rpc import rpc_init,MoneroWalletRPCClient
 from mmgen.daemon import CoinDaemon,MoneroWalletDaemon
 
-def auth_test(d):
+def auth_test(proto,d):
 	d.stop()
 	if g.platform != 'win':
 		qmsg(f'\n  Testing authentication with credentials from bitcoin.conf:')
@@ -24,77 +24,66 @@ def auth_test(d):
 		d.start()
 
 		async def do():
-			assert g.rpc.auth.user == 'ut_rpc', 'user is not ut_rpc!'
+			rpc = await rpc_init(proto)
+			assert rpc.auth.user == 'ut_rpc', 'user is not ut_rpc!'
 
 		run_session(do())
 		d.stop()
 
-class unit_tests:
-
-	def bch(self,name,ut):
-
-		async def run_test():
-			qmsg('  Testing backend {!r}'.format(type(g.rpc.backend).__name__))
-
-		d = CoinDaemon('bch',test_suite=True)
+def do_msg(rpc):
+	qmsg('  Testing backend {!r}'.format(type(rpc.backend).__name__))
+
+class init_test:
+
+	async def btc(proto,backend):
+		rpc = await rpc_init(proto,backend)
+		do_msg(rpc)
+		addrs = (
+			('bc1qvmqas4maw7lg9clqu6kqu9zq9cluvlln5hw97q','test address #1'), # deadbeef * 8
+			('bc1qe50rj25cldtskw5huxam335kyshtqtlrf4pt9x','test address #2'), # deadbeef * 7 + deadbeee
+		)
+		await rpc.batch_call('importaddress',addrs,timeout=120)
+		ret = await rpc.batch_call('getaddressesbylabel',[(l,) for a,l in addrs])
+		assert list(ret[0].keys())[0] == addrs[0][0]
+
+		bh = (await rpc.call('getblockchaininfo',timeout=300))['bestblockhash']
+		await rpc.gathered_call('getblock',((bh,),(bh,1)),timeout=300)
+		await rpc.gathered_call(None,(('getblock',(bh,)),('getblock',(bh,1))),timeout=300)
+
+	async def bch(proto,backend):
+		rpc = await rpc_init(proto,backend)
+		do_msg(rpc)
+
+	async def eth(proto,backend):
+		rpc = await rpc_init(proto,backend)
+		do_msg(rpc)
+		await rpc.call('parity_versionInfo',timeout=300)
+
+def run_test(coin,auth):
+	proto = init_proto(coin,network=('mainnet','regtest')[coin=='eth']) # FIXME CoinDaemon's network handling broken
+	d = CoinDaemon(network_id=coin,test_suite=True)
+	if auth:
 		d.remove_datadir()
-		d.start()
-		g.proto.daemon_data_dir = d.datadir # location of cookie file
-		g.rpc_port = d.rpc_port
-
-		for backend in g.autoset_opts['rpc_backend'].choices:
-			run_session(run_test(),backend=backend)
-
-		auth_test(d)
-		qmsg('  OK')
-		return True
-
-	def btc(self,name,ut):
-
-		async def run_test():
-			c = g.rpc
-			qmsg('  Testing backend {!r}'.format(type(c.backend).__name__))
-			addrs = (
-				('bc1qvmqas4maw7lg9clqu6kqu9zq9cluvlln5hw97q','test address #1'), # deadbeef * 8
-				('bc1qe50rj25cldtskw5huxam335kyshtqtlrf4pt9x','test address #2'), # deadbeef * 7 + deadbeee
-			)
+	d.start()
 
-			await c.batch_call('importaddress',addrs,timeout=120)
-			ret = await c.batch_call('getaddressesbylabel',[(l,) for a,l in addrs])
-			assert list(ret[0].keys())[0] == addrs[0][0]
+	for backend in g.autoset_opts['rpc_backend'].choices:
+		run_session(getattr(init_test,coin)(proto,backend),backend=backend)
 
-			bh = (await c.call('getblockchaininfo',timeout=300))['bestblockhash']
-			await c.gathered_call('getblock',((bh,),(bh,1)),timeout=300)
-			await c.gathered_call(None,(('getblock',(bh,)),('getblock',(bh,1))),timeout=300)
+	if auth:
+		auth_test(proto,d)
+	qmsg('  OK')
+	return True
 
+class unit_tests:
 
-		d = CoinDaemon('btc',test_suite=True)
-		d.remove_datadir()
-		d.start()
-		g.proto.daemon_data_dir = d.datadir # used by BitcoinRPCClient.set_auth() to find the cookie
-		g.rpc_port = d.rpc_port
-
-		for backend in g.autoset_opts['rpc_backend'].choices:
-			run_session(run_test(),backend=backend)
+	def bch(self,name,ut):
+		return run_test('bch',auth=True)
 
-		auth_test(d)
-		qmsg('  OK')
-		return True
+	def btc(self,name,ut):
+		return run_test('btc',auth=True)
 
 	def eth(self,name,ut):
-		ed = CoinDaemon('eth',test_suite=True)
-		ed.start()
-		g.rpc_port = CoinDaemon('eth',test_suite=True).rpc_port
-
-		async def run_test():
-			qmsg('  Testing backend {!r}'.format(type(g.rpc.backend).__name__))
-			ret = await g.rpc.call('parity_versionInfo',timeout=300)
-
-		for backend in g.autoset_opts['rpc_backend'].choices:
-			run_session(run_test(),proto=init_proto('eth'),backend=backend)
-
-		ed.stop()
-		return True
+		return run_test('eth',auth=False)
 
 	def xmr_wallet(self,name,ut):
 
@@ -124,5 +113,5 @@ class unit_tests:
 				md.wait = False
 				md.stop()
 
-		run_session(run(),do_rpc_init=False)
+		run_session(run())
 		return True

+ 29 - 7
test/unit_tests_d/ut_tx.py

@@ -7,11 +7,31 @@ import re
 from mmgen.common import *
 from mmgen.tx import MMGenTX
 from mmgen.txfile import MMGenTxFile
+from mmgen.rpc import rpc_init
+from mmgen.daemon import CoinDaemon
+from mmgen.protocol import init_proto
 
 class unit_tests:
 
-	def txfile(self,name,ut):
+	def tx(self,name,ut):
+		qmsg('  Testing transaction objects')
+		proto = init_proto('btc')
+		d = CoinDaemon('btc',test_suite=True)
+		d.start()
+		proto.daemon_data_dir = d.datadir # location of cookie file
+		proto.rpc_port = d.rpc_port
+
+		async def do():
+			tx = MMGenTX.New(proto=proto)
+			tx.rpc = await rpc_init(proto=proto)
 
+		run_session(do())
+
+		d.stop()
+		qmsg('  OK')
+		return True
+
+	def txfile(self,name,ut):
 		qmsg('  Testing TX file operations')
 
 		fns = ( # TODO: add altcoin TX files
@@ -21,22 +41,24 @@ class unit_tests:
 			'25EFA3[2.34].testnet.rawtx',
 		)
 		for fn in fns:
+			vmsg(f'    parsing: {fn}')
 			fpath = os.path.join('test','ref',fn)
-			tx = MMGenTX(filename=fpath,quiet_open=True)
+			tx = MMGenTX.Unsigned(filename=fpath,quiet_open=True)
 			f = MMGenTxFile(tx)
 
 			fn_gen = f.make_filename()
-			vmsg(f'    parsed: {fn_gen}')
 			assert fn_gen == fn, f'{fn_gen} != {fn}'
 
 			text = f.format()
 			# New in version 3.3: Support for the unicode legacy literal (u'value') was
 			# reintroduced to simplify the maintenance of dual Python 2.x and 3.x codebases.
 			# See PEP 414 for more information.
-			chk = re.subn(r"\bu'",r"'",open(fpath).read())[0] # remove Python2 'u' string prefixes from ref files
-			nLines = len([i for i in get_ndiff(chk,text) if i.startswith('-')])
-			assert nLines == 1, f'{nLines} lines differ: only checksum line should differ'
-			break # FIXME - test BCH, testnet
+			chk = re.subn(r"\bu(['\"])",r"\1",open(fpath).read())[0] # remove Python2 'u' string prefixes from ref files
+			diff = get_ndiff(chk,text)
+			#print('\n'.join(diff))
+			nLines = len([i for i in diff if i.startswith('-')])
+			assert nLines in (0,1), f'{nLines} lines differ: only checksum line may differ'
+			#break # FIXME - test BCH, testnet
 
 		qmsg('  OK')
 		return True

+ 17 - 13
test/unit_tests_d/ut_tx_deserialize.py

@@ -21,7 +21,7 @@ class unit_test(object):
 
 	def run_test(self,name,ut):
 
-		async def test_tx(txhex,desc,n):
+		async def test_tx(tx_proto,tx_hex,desc,n):
 
 			def has_nonstandard_outputs(outputs):
 				for o in outputs:
@@ -30,11 +30,12 @@ class unit_test(object):
 						return True
 				return False
 
-			d = await g.rpc.call('decoderawtransaction',txhex)
+			rpc = await rpc_init(proto=tx_proto)
+			d = await rpc.call('decoderawtransaction',tx_hex)
 
 			if has_nonstandard_outputs(d['vout']): return False
 
-			dt = DeserializedTX(txhex)
+			dt = DeserializedTX(tx_proto,tx_hex)
 
 			if opt.verbose:
 				Msg('\n====================================================')
@@ -101,8 +102,11 @@ class unit_test(object):
 			n = 1
 			for e in data:
 				if type(e[0]) == list:
-					await rpc_init()
-					await test_tx(e[1],desc,n)
+					await test_tx(
+						tx_proto = init_proto('btc'),
+						tx_hex   = e[1],
+						desc     = desc,
+						n        = n )
 					n += 1
 				else:
 					desc = e[0]
@@ -114,18 +118,18 @@ class unit_test(object):
 				#	('bch',False,'test/ref/460D4D-BCH[10.19764,tl=1320969600].rawtx')
 				)
 			print_info('test/ref/*rawtx','MMGen reference')
-			g.rpc_port = None
 			for n,(coin,testnet,fn) in enumerate(fns):
-				g.proto = init_proto(coin,testnet=testnet)
-				g.proto.daemon_data_dir = 'test/daemons/' + coin
-				g.proto.rpc_port = CoinDaemon(coin + ('','_tn')[testnet],test_suite=True).rpc_port
-				await rpc_init()
-				await test_tx(MMGenTX(fn).hex,fn,n+1)
+				tx = MMGenTX.Unsigned(filename=fn)
+				await test_tx(
+					tx_proto = tx.proto,
+					tx_hex   = tx.hex,
+					desc     = fn,
+					n        = n+1 )
 			Msg('OK')
 
 		start_test_daemons('btc','btc_tn') # ,'bch')
-		run_session(test_mmgen_txs(),do_rpc_init=False)
-		run_session(test_core_vectors(),do_rpc_init=False)
+		run_session(test_core_vectors())
+		run_session(test_mmgen_txs())
 		stop_test_daemons('btc','btc_tn') # ,'bch')
 
 		return True