Browse Source

modularize coin protocols

- protocols are now in individual modules under `proto`
The MMGen Project 3 years ago
parent
commit
7558c539a1

+ 1 - 1
mmgen/addrgen.py

@@ -20,7 +20,7 @@
 addrgen.py: Address and view key generation classes for the MMGen suite
 addrgen.py: Address and view key generation classes for the MMGen suite
 """
 """
 
 
-from .protocol import hash160,_b58chk_encode
+from .proto.btc import hash160,_b58chk_encode
 from .addr import CoinAddr,MMGenAddrType,MoneroViewKey,ZcashViewKey
 from .addr import CoinAddr,MMGenAddrType,MoneroViewKey,ZcashViewKey
 
 
 # decorator for to_addr() and to_viewkey()
 # decorator for to_addr() and to_viewkey()

+ 3 - 3
mmgen/addrlist.py

@@ -157,9 +157,9 @@ class AddrList(MMGenObject): # Address info for a single seed ID
 		mmtype = mmtype or proto.dfl_mmtype
 		mmtype = mmtype or proto.dfl_mmtype
 		assert mmtype in MMGenAddrType.mmtypes, f'{mmtype}: mmtype not in {MMGenAddrType.mmtypes!r}'
 		assert mmtype in MMGenAddrType.mmtypes, f'{mmtype}: mmtype not in {MMGenAddrType.mmtypes!r}'
 
 
-		from .protocol import CoinProtocol
+		from .proto.btc import mainnet
 		self.bitcoin_addrtypes = tuple(
 		self.bitcoin_addrtypes = tuple(
-			MMGenAddrType(CoinProtocol.Bitcoin,key).name for key in CoinProtocol.Bitcoin.mmtypes)
+			MMGenAddrType(mainnet,key).name for key in mainnet.mmtypes )
 
 
 		if seed and addr_idxs and mmtype: # data from seed + idxs
 		if seed and addr_idxs and mmtype: # data from seed + idxs
 			self.al_id,src = AddrListID(seed.sid,mmtype),'gen'
 			self.al_id,src = AddrListID(seed.sid,mmtype),'gen'
@@ -281,7 +281,7 @@ class AddrList(MMGenObject): # Address info for a single seed ID
 		return out
 		return out
 
 
 	def gen_wallet_passwd(self,privbytes):
 	def gen_wallet_passwd(self,privbytes):
-		from .protocol import hash256
+		from .proto.btc import hash256
 		return WalletPassword( hash256(privbytes)[:16].hex() )
 		return WalletPassword( hash256(privbytes)[:16].hex() )
 
 
 	def check_format(self,addr):
 	def check_format(self,addr):

+ 4 - 3
mmgen/altcoin.py

@@ -556,7 +556,7 @@ class CoinInfo(object):
 			return '1'
 			return '1'
 
 
 		def phash2addr(ver_num,pk_hash):
 		def phash2addr(ver_num,pk_hash):
-			from .protocol import _b58chk_encode
+			from .proto.btc import _b58chk_encode
 			bl = ver_num.bit_length()
 			bl = ver_num.bit_length()
 			ver_bytes = int.to_bytes(ver_num,bl//8 + bool(bl%8),'big')
 			ver_bytes = int.to_bytes(ver_num,bl//8 + bool(bl%8),'big')
 			return _b58chk_encode(ver_bytes + pk_hash)
 			return _b58chk_encode(ver_bytes + pk_hash)
@@ -736,6 +736,7 @@ def init_genonly_altcoins(usr_coin=None,testnet=False):
 def create_altcoin_protos(data):
 def create_altcoin_protos(data):
 
 
 	from .protocol import CoinProtocol
 	from .protocol import CoinProtocol
+	from .proto.btc import mainnet
 
 
 	def make_proto(e,testnet=False):
 	def make_proto(e,testnet=False):
 
 
@@ -751,8 +752,8 @@ def create_altcoin_protos(data):
 			CoinProtocol,
 			CoinProtocol,
 			proto,
 			proto,
 			type(
 			type(
-				'CoinProtocol.' + proto,
-				(CoinProtocol.Bitcoin,),
+				proto,
+				(mainnet,),
 				{
 				{
 					'base_coin': e.symbol,
 					'base_coin': e.symbol,
 					'addr_ver_bytes': dict(
 					'addr_ver_bytes': dict(

+ 1 - 1
mmgen/data/version

@@ -1 +1 @@
-13.1.dev011
+13.1.dev012

+ 2 - 2
mmgen/help.py

@@ -158,7 +158,7 @@ one address with no amount on the command line.
 """
 """
 
 
 		def txsign():
 		def txsign():
-			from .protocol import CoinProtocol
+			from .proto.btc import mainnet
 			return """
 			return """
 Transactions may contain both {pnm} or non-{pnm} input addresses.
 Transactions may contain both {pnm} or non-{pnm} input addresses.
 
 
@@ -180,7 +180,7 @@ 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
 must also be supplied on the command line if the data can't be found in the
 default wallet.
 default wallet.
 """.format(
 """.format(
-	wd  = (f'{coind_exec()} wallet dump or ' if isinstance(proto,CoinProtocol.Bitcoin) else ''),
+	wd  = (f'{coind_exec()} wallet dump or ' if isinstance(proto,mainnet) else ''),
 	pnm = g.proj_name,
 	pnm = g.proj_name,
 	pnu = proto.name,
 	pnu = proto.name,
 	pnl = g.proj_name.lower() )
 	pnl = g.proj_name.lower() )

+ 2 - 2
mmgen/keygen.py

@@ -106,8 +106,8 @@ class keygen_backend:
 
 
 			def __init__(self):
 			def __init__(self):
 
 
-				from .protocol import CoinProtocol
-				self.proto_cls = CoinProtocol.Monero
+				from .proto.xmr import mainnet
+				self.proto_cls = mainnet
 
 
 				from .util import get_keccak
 				from .util import get_keccak
 				self.keccak_256 = get_keccak()
 				self.keccak_256 = get_keccak()

+ 0 - 0
mmgen/proto/__init__.py


+ 37 - 0
mmgen/proto/bch.py

@@ -0,0 +1,37 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
+# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen
+#   https://gitlab.com/mmgen/mmgen
+
+"""
+Bitcoin Cash protocol
+"""
+
+from .btc import mainnet,_finfo
+
+class mainnet(mainnet):
+	is_fork_of      = 'Bitcoin'
+	mmtypes         = ('L','C')
+	sighash_type    = 'ALL|FORKID'
+	forks = [
+		_finfo(478559,'000000000000000000651ef99cb9fcbe0dadde1d424bd9f15ff20136191a5eec','BTC',False)
+	]
+	caps = ()
+	coin_amt        = 'BCHAmt'
+	max_tx_fee      = '0.1'
+	ignore_daemon_version = False
+
+	def pubkey2redeem_script(self,pubkey): raise NotImplementedError
+	def pubkey2segwitaddr(self,pubkey):    raise NotImplementedError
+
+class testnet(mainnet):
+	addr_ver_bytes = { '6f': 'p2pkh', 'c4': 'p2sh' }
+	wif_ver_num    = { 'std': 'ef' }
+
+class regtest(testnet):
+	halving_interval = 150

+ 160 - 0
mmgen/proto/btc.py

@@ -0,0 +1,160 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
+# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen
+#   https://gitlab.com/mmgen/mmgen
+
+"""
+Bitcoin protocol
+"""
+
+from ..protocol import CoinProtocol,parsed_wif,parsed_addr,_finfo,_b58a,_nw
+import hashlib
+
+def hash160(in_bytes): # OP_HASH160
+	return hashlib.new('ripemd160',hashlib.sha256(in_bytes).digest()).digest()
+
+def hash256(in_bytes): # OP_HASH256
+	return hashlib.sha256(hashlib.sha256(in_bytes).digest()).digest()
+
+# From en.bitcoin.it:
+#  The Base58 encoding used is home made, and has some differences.
+#  Especially, leading zeroes are kept as single zeroes when conversion happens.
+# Test: 5JbQQTs3cnoYN9vDYaGY6nhQ1DggVsY4FJNBUfEfpSQqrEp3srk
+# The 'zero address':
+# 1111111111111111111114oLvT2 (pubkeyhash = '\0'*20)
+
+def _b58chk_encode(in_bytes):
+	lzeroes = len(in_bytes) - len(in_bytes.lstrip(b'\x00'))
+	def do_enc(n):
+		while n:
+			yield _b58a[n % 58]
+			n //= 58
+	return ('1' * lzeroes) + ''.join(do_enc(int.from_bytes(in_bytes+hash256(in_bytes)[:4],'big')))[::-1]
+
+def _b58chk_decode(s):
+	lzeroes = len(s) - len(s.lstrip('1'))
+	res = sum(_b58a.index(ch) * 58**n for n,ch in enumerate(s[::-1]))
+	bl = res.bit_length()
+	out = b'\x00' * lzeroes + res.to_bytes(bl//8 + bool(bl%8),'big')
+	if out[-4:] != hash256(out[:-4])[:4]:
+		raise ValueError('_b58chk_decode(): incorrect checksum')
+	return out[:-4]
+
+class mainnet(CoinProtocol.Secp256k1): # chainparams.cpp
+	"""
+	All Bitcoin code and chain forks inherit from this class
+	"""
+	mod_clsname     = 'Bitcoin'
+	network_names   = _nw('mainnet','testnet','regtest')
+	addr_ver_bytes  = { '00': 'p2pkh', '05': 'p2sh' }
+	addr_len        = 20
+	wif_ver_num     = { 'std': '80' }
+	mmtypes         = ('L','C','S','B')
+	dfl_mmtype      = 'L'
+	coin_amt        = 'BTCAmt'
+	max_tx_fee      = '0.003'
+	sighash_type    = 'ALL'
+	block0          = '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f'
+	forks           = [
+		_finfo(478559,'00000000000000000019f112ec0a9982926f1258cdcc558dd7c3b7e5dc7fa148','BCH',False),
+	]
+	caps            = ('rbf','segwit')
+	mmcaps          = ('key','addr','rpc','tx')
+	base_coin       = 'BTC'
+	base_proto      = 'Bitcoin'
+	# From BIP173: witness version 'n' is stored as 'OP_n'. OP_0 is encoded as 0x00,
+	# but OP_1 through OP_16 are encoded as 0x51 though 0x60 (81 to 96 in decimal).
+	witness_vernum_hex = '00'
+	witness_vernum  = int(witness_vernum_hex,16)
+	bech32_hrp      = 'bc'
+	sign_mode       = 'daemon'
+	avg_bdi         = int(9.7 * 60) # average block discovery interval (historical)
+	halving_interval = 210000
+	max_halvings    = 64
+	start_subsidy   = 50
+	ignore_daemon_version = False
+	max_int         = 0xffffffff
+
+	def bytes2wif(self,privbytes,pubkey_type,compressed): # input is preprocessed hex
+		assert len(privbytes) == self.privkey_len, f'{len(privbytes)} bytes: incorrect private key length!'
+		assert pubkey_type in self.wif_ver_num, f'{pubkey_type!r}: invalid pubkey_type'
+		return _b58chk_encode(
+			bytes.fromhex(self.wif_ver_num[pubkey_type])
+			+ privbytes
+			+ (b'',b'\x01')[bool(compressed)])
+
+	def parse_wif(self,wif):
+		key = _b58chk_decode(wif)
+
+		for k,v in self.wif_ver_num.items():
+			v = bytes.fromhex(v)
+			if key[:len(v)] == v:
+				pubkey_type = k
+				key = key[len(v):]
+				break
+		else:
+			raise ValueError('Invalid WIF version number')
+
+		if len(key) == self.privkey_len + 1:
+			assert key[-1] == 0x01, f'{key[-1]!r}: invalid compressed key suffix byte'
+			compressed = True
+		elif len(key) == self.privkey_len:
+			compressed = False
+		else:
+			raise ValueError(f'{len(key)}: invalid key length')
+
+		return parsed_wif(
+			sec         = key[:self.privkey_len],
+			pubkey_type = pubkey_type,
+			compressed  = compressed )
+
+	def parse_addr(self,addr):
+
+		if 'B' in self.mmtypes and addr[:len(self.bech32_hrp)] == self.bech32_hrp:
+			import mmgen.bech32 as bech32
+			ret = bech32.decode(self.bech32_hrp,addr)
+
+			if ret[0] != self.witness_vernum:
+				from ..util import msg
+				msg(f'{ret[0]}: Invalid witness version number')
+				return False
+
+			return parsed_addr( bytes(ret[1]), 'bech32' ) if ret[1] else False
+
+		return self.parse_addr_bytes(_b58chk_decode(addr))
+
+	def pubhash2addr(self,pubkey_hash,p2sh):
+		assert len(pubkey_hash) == 20, f'{len(pubkey_hash)}: invalid length for pubkey hash'
+		return _b58chk_encode(
+			self.addr_fmt_to_ver_bytes(('p2pkh','p2sh')[p2sh],return_hex=False) + pubkey_hash
+		)
+
+	# Segwit:
+	def pubkey2redeem_script(self,pubkey):
+		# https://bitcoincore.org/en/segwit_wallet_dev/
+		# The P2SH redeemScript is always 22 bytes. It starts with a OP_0, followed
+		# by a canonical push of the keyhash (i.e. 0x0014{20-byte keyhash})
+		return bytes.fromhex(self.witness_vernum_hex + '14') + hash160(pubkey)
+
+	def pubkey2segwitaddr(self,pubkey):
+		return self.pubhash2addr(
+			hash160( self.pubkey2redeem_script(pubkey)), p2sh=True )
+
+	def pubhash2bech32addr(self,pubhash):
+		d = list(pubhash)
+		import mmgen.bech32 as bech32
+		return bech32.bech32_encode(self.bech32_hrp,[self.witness_vernum]+bech32.convertbits(d,8,5))
+
+class testnet(mainnet):
+	addr_ver_bytes      = { '6f': 'p2pkh', 'c4': 'p2sh' }
+	wif_ver_num         = { 'std': 'ef' }
+	bech32_hrp          = 'tb'
+
+class regtest(testnet):
+	bech32_hrp          = 'bcrt'
+	halving_interval    = 150

+ 26 - 0
mmgen/proto/etc.py

@@ -0,0 +1,26 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
+# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen
+#   https://gitlab.com/mmgen/mmgen
+
+"""
+Ethereum Classic protocol
+"""
+
+from .eth import mainnet
+
+class mainnet(mainnet):
+	chain_names = ['classic','ethereum_classic']
+	max_tx_fee  = '0.005'
+	ignore_daemon_version = False
+
+class testnet(mainnet):
+	chain_names = ['morden','morden_testnet','classic-testnet']
+
+class regtest(testnet):
+	chain_names = ['developmentchain']

+ 76 - 0
mmgen/proto/eth.py

@@ -0,0 +1,76 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
+# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen
+#   https://gitlab.com/mmgen/mmgen
+
+"""
+Ethereum protocol
+"""
+
+from ..protocol import CoinProtocol,_nw,parsed_addr
+from ..util import is_hex_str_lc,Msg
+
+class mainnet(CoinProtocol.DummyWIF,CoinProtocol.Secp256k1):
+
+	network_names = _nw('mainnet','testnet','devnet')
+	addr_len      = 20
+	mmtypes       = ('E',)
+	dfl_mmtype    = 'E'
+	mod_clsname   = 'Ethereum'
+	base_coin     = 'ETH'
+	pubkey_type   = 'std' # required by DummyWIF
+
+	coin_amt      = 'ETHAmt'
+	max_tx_fee    = '0.005'
+	chain_names   = ['ethereum','foundation']
+	sign_mode     = 'standalone'
+	caps          = ('token',)
+	mmcaps        = ('key','addr','rpc','tx')
+	base_proto    = 'Ethereum'
+	avg_bdi       = 15
+	ignore_daemon_version = False
+
+	chain_ids = {
+		1:    'ethereum',         # ethereum mainnet
+		2:    'morden',           # morden testnet (deprecated)
+		3:    'ropsten',          # ropsten testnet
+		4:    'rinkeby',          # rinkeby testnet
+		5:    'goerli',           # goerli testnet
+		42:   'kovan',            # kovan testnet
+		61:   'classic',          # ethereum classic mainnet
+		62:   'morden',           # ethereum classic testnet
+		17:   'developmentchain', # parity dev chain
+		1337: 'developmentchain', # geth dev chain
+	}
+
+	@property
+	def dcoin(self):
+		return self.tokensym or self.coin
+
+	def parse_addr(self,addr):
+		if is_hex_str_lc(addr) and len(addr) == self.addr_len * 2:
+			return parsed_addr( bytes.fromhex(addr), 'ethereum' )
+		if g.debug:
+			Msg(f'Invalid address: {addr}')
+		return False
+
+	@classmethod
+	def checksummed_addr(cls,addr):
+		h = self.keccak_256(addr.encode()).digest().hex()
+		return ''.join(addr[i].upper() if int(h[i],16) > 7 else addr[i] for i in range(len(addr)))
+
+	def pubhash2addr(self,pubkey_hash,p2sh):
+		assert len(pubkey_hash) == 20, f'{len(pubkey_hash)}: invalid length for {self.name} pubkey hash'
+		assert not p2sh, f'{self.name} protocol has no P2SH address format'
+		return pubkey_hash.hex()
+
+class testnet(mainnet):
+	chain_names = ['kovan','goerli','rinkeby']
+
+class regtest(testnet):
+	chain_names = ['developmentchain']

+ 39 - 0
mmgen/proto/ltc.py

@@ -0,0 +1,39 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
+# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen
+#   https://gitlab.com/mmgen/mmgen
+
+"""
+Litecoin protocol
+"""
+
+from .btc import mainnet
+
+class mainnet(mainnet):
+	block0          = '12a765e31ffd4059bada1e25190f6e98c99d9714d334efa41a195a7e7e04bfe2'
+	addr_ver_bytes  = { '30': 'p2pkh', '32': 'p2sh', '05': 'p2sh' } # new p2sh ver 0x32 must come first
+	wif_ver_num     = { 'std': 'b0' }
+	mmtypes         = ('L','C','S','B')
+	coin_amt        = 'LTCAmt'
+	max_tx_fee      = '0.3'
+	base_coin       = 'LTC'
+	forks           = []
+	bech32_hrp      = 'ltc'
+	avg_bdi         = 150
+	halving_interval = 840000
+	ignore_daemon_version = False
+
+class testnet(mainnet):
+	# 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
+	bech32_hrp         = 'tltc'
+
+class regtest(testnet):
+	bech32_hrp         = 'rltc'
+	halving_interval   = 150

+ 62 - 0
mmgen/proto/xmr.py

@@ -0,0 +1,62 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
+# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen
+#   https://gitlab.com/mmgen/mmgen
+
+"""
+Monero protocol
+"""
+
+from ..protocol import CoinProtocol,_nw
+
+# https://github.com/monero-project/monero/blob/master/src/cryptonote_config.h
+class mainnet(CoinProtocol.DummyWIF,CoinProtocol.Base):
+
+	network_names  = _nw('mainnet','stagenet',None)
+	base_coin      = 'XMR'
+	addr_ver_bytes = { '12': 'monero', '2a': 'monero_sub' }
+	addr_len       = 68
+	wif_ver_num    = {}
+	pubkey_types   = ('monero',)
+	mmtypes        = ('M',)
+	dfl_mmtype     = 'M'
+	pubkey_type    = 'monero' # required by DummyWIF
+	avg_bdi        = 120
+	privkey_len    = 32
+	mmcaps         = ('key','addr')
+	ignore_daemon_version = False
+	coin_amt       = 'XMRAmt'
+
+	def preprocess_key(self,sec,pubkey_type): # reduce key
+		from ..ed25519 import l
+		return int.to_bytes(
+			int.from_bytes( sec[::-1], 'big' ) % l,
+			self.privkey_len,
+			'big' )[::-1]
+
+	def parse_addr(self,addr):
+
+		from ..baseconv import baseconv
+
+		def b58dec(addr_str):
+			bc = baseconv('b58')
+			l = len(addr_str)
+			a = b''.join([bc.tobytes( addr_str[i*11:i*11+11], pad=8 ) for i in range(l//11)])
+			b = bc.tobytes( addr_str[-(l%11):], pad=5 )
+			return a + b
+
+		ret = b58dec(addr)
+
+		chk = self.keccak_256(ret[:-4]).digest()[:4]
+
+		assert ret[-4:] == chk, f'{ret[-4:].hex()}: incorrect checksum.  Correct value: {chk.hex()}'
+
+		return self.parse_addr_bytes(ret)
+
+class testnet(mainnet): # use stagenet for testnet
+	addr_ver_bytes = { '18': 'monero', '24': 'monero_sub' } # testnet is ('35','3f')

+ 52 - 0
mmgen/proto/zec.py

@@ -0,0 +1,52 @@
+#!/usr/bin/env python3
+#
+# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet
+# Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
+# Licensed under the GNU General Public License, Version 3:
+#   https://www.gnu.org/licenses
+# Public project repositories:
+#   https://github.com/mmgen/mmgen
+#   https://gitlab.com/mmgen/mmgen
+
+"""
+Zcash protocol
+"""
+
+from .btc import mainnet
+
+class mainnet(mainnet):
+	base_coin      = 'ZEC'
+	addr_ver_bytes = { '1cb8': 'p2pkh', '1cbd': 'p2sh', '169a': 'zcash_z', 'a8abd3': 'viewkey' }
+	wif_ver_num    = { 'std': '80', 'zcash_z': 'ab36' }
+	pubkey_types   = ('std','zcash_z')
+	mmtypes        = ('L','C','Z')
+	mmcaps         = ('key','addr')
+	dfl_mmtype     = 'L'
+	avg_bdi        = 75
+
+	def __init__(self,*args,**kwargs):
+		super().__init__(*args,**kwargs)
+		from ..opts import opt
+		self.coin_id = 'ZEC-Z' if opt.type in ('zcash_z','Z') else 'ZEC-T'
+
+	def get_addr_len(self,addr_fmt):
+		return (20,64)[addr_fmt in ('zcash_z','viewkey')]
+
+	def preprocess_key(self,sec,pubkey_type):
+		if pubkey_type == 'zcash_z': # zero the first four bits
+			return bytes([sec[0] & 0x0f]) + sec[1:]
+		else:
+			return super().preprocess_key(sec,pubkey_type)
+
+	def pubhash2addr(self,pubkey_hash,p2sh):
+		hash_len = len(pubkey_hash)
+		if hash_len == 20:
+			return super().pubhash2addr(pubkey_hash,p2sh)
+		elif hash_len == 64:
+			raise NotImplementedError('Zcash z-addresses do not support pubhash2addr()')
+		else:
+			raise ValueError(f'{hash_len}: incorrect pubkey_hash length')
+
+class testnet(mainnet):
+	wif_ver_num  = { 'std': 'ef', 'zcash_z': 'ac08' }
+	addr_ver_bytes = { '1d25': 'p2pkh', '1cba': 'p2sh', '16b6': 'zcash_z', 'a8ac0c': 'viewkey' }

+ 15 - 352
mmgen/protocol.py

@@ -17,10 +17,9 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 
 """
 """
-protocol.py: Coin protocol functions, classes and methods
+protocol.py: Coin protocol base classes and initializer
 """
 """
 
 
-import sys,os,hashlib
 from collections import namedtuple
 from collections import namedtuple
 
 
 from .devtools import *
 from .devtools import *
@@ -29,41 +28,11 @@ from .globalvars import g
 parsed_wif = namedtuple('parsed_wif',['sec','pubkey_type','compressed'])
 parsed_wif = namedtuple('parsed_wif',['sec','pubkey_type','compressed'])
 parsed_addr = namedtuple('parsed_addr',['bytes','fmt'])
 parsed_addr = namedtuple('parsed_addr',['bytes','fmt'])
 
 
-def hash160(in_bytes): # OP_HASH160
-	return hashlib.new('ripemd160',hashlib.sha256(in_bytes).digest()).digest()
-
-def hash256(in_bytes): # OP_HASH256
-	return hashlib.sha256(hashlib.sha256(in_bytes).digest()).digest()
-
-_b58a='123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz'
-
-# From en.bitcoin.it:
-#  The Base58 encoding used is home made, and has some differences.
-#  Especially, leading zeroes are kept as single zeroes when conversion happens.
-# Test: 5JbQQTs3cnoYN9vDYaGY6nhQ1DggVsY4FJNBUfEfpSQqrEp3srk
-# The 'zero address':
-# 1111111111111111111114oLvT2 (pubkeyhash = '\0'*20)
-
-def _b58chk_encode(in_bytes):
-	lzeroes = len(in_bytes) - len(in_bytes.lstrip(b'\x00'))
-	def do_enc(n):
-		while n:
-			yield _b58a[n % 58]
-			n //= 58
-	return ('1' * lzeroes) + ''.join(do_enc(int.from_bytes(in_bytes+hash256(in_bytes)[:4],'big')))[::-1]
-
-def _b58chk_decode(s):
-	lzeroes = len(s) - len(s.lstrip('1'))
-	res = sum(_b58a.index(ch) * 58**n for n,ch in enumerate(s[::-1]))
-	bl = res.bit_length()
-	out = b'\x00' * lzeroes + res.to_bytes(bl//8 + bool(bl%8),'big')
-	if out[-4:] != hash256(out[:-4])[:4]:
-		raise ValueError('_b58chk_decode(): incorrect checksum')
-	return out[:-4]
-
 _finfo = namedtuple('fork_info',['height','hash','name','replayable'])
 _finfo = namedtuple('fork_info',['height','hash','name','replayable'])
 _nw = namedtuple('coin_networks',['mainnet','testnet','regtest'])
 _nw = namedtuple('coin_networks',['mainnet','testnet','regtest'])
 
 
+_b58a='123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' # shared by Bitcoin and Monero
+
 class CoinProtocol(MMGenObject):
 class CoinProtocol(MMGenObject):
 
 
 	proto_info = namedtuple('proto_info',['name','trust_level']) # trust levels: see altcoin.py
 	proto_info = namedtuple('proto_info',['name','trust_level']) # trust levels: see altcoin.py
@@ -107,7 +76,7 @@ class CoinProtocol(MMGenObject):
 				self.chain_names = [self.network]
 				self.chain_names = [self.network]
 
 
 			if self.tokensym:
 			if self.tokensym:
-				assert isinstance(self,CoinProtocol.Ethereum), 'CoinProtocol.Base_chk1'
+				assert self.name.startswith('Ethereum'), 'CoinProtocol.Base_chk1'
 
 
 			if self.base_coin in ('ETH','XMR'):
 			if self.base_coin in ('ETH','XMR'):
 				from .util import get_keccak
 				from .util import get_keccak
@@ -212,168 +181,10 @@ class CoinProtocol(MMGenObject):
 						ymsg(f'Warning: private key is greater than secp256k1 group order!:\n  {hexpriv}')
 						ymsg(f'Warning: private key is greater than secp256k1 group order!:\n  {hexpriv}')
 					return (pk % self.secp256k1_ge).to_bytes(self.privkey_len,'big')
 					return (pk % self.secp256k1_ge).to_bytes(self.privkey_len,'big')
 
 
-	class Bitcoin(Secp256k1): # chainparams.cpp
+	class DummyWIF:
 		"""
 		"""
-		All Bitcoin code and chain forks inherit from this class
+		Ethereum and Monero protocols inherit from this class
 		"""
 		"""
-		mod_clsname     = 'Bitcoin'
-		network_names   = _nw('mainnet','testnet','regtest')
-		addr_ver_bytes  = { '00': 'p2pkh', '05': 'p2sh' }
-		addr_len        = 20
-		wif_ver_num     = { 'std': '80' }
-		mmtypes         = ('L','C','S','B')
-		dfl_mmtype      = 'L'
-		coin_amt        = 'BTCAmt'
-		max_tx_fee      = '0.003'
-		sighash_type    = 'ALL'
-		block0          = '000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f'
-		forks           = [
-			_finfo(478559,'00000000000000000019f112ec0a9982926f1258cdcc558dd7c3b7e5dc7fa148','BCH',False),
-		]
-		caps            = ('rbf','segwit')
-		mmcaps          = ('key','addr','rpc','tx')
-		base_coin       = 'BTC'
-		base_proto      = 'Bitcoin'
-		# From BIP173: witness version 'n' is stored as 'OP_n'. OP_0 is encoded as 0x00,
-		# but OP_1 through OP_16 are encoded as 0x51 though 0x60 (81 to 96 in decimal).
-		witness_vernum_hex = '00'
-		witness_vernum  = int(witness_vernum_hex,16)
-		bech32_hrp      = 'bc'
-		sign_mode       = 'daemon'
-		avg_bdi         = int(9.7 * 60) # average block discovery interval (historical)
-		halving_interval = 210000
-		max_halvings    = 64
-		start_subsidy   = 50
-		ignore_daemon_version = False
-		max_int         = 0xffffffff
-
-		def bytes2wif(self,privbytes,pubkey_type,compressed): # input is preprocessed hex
-			assert len(privbytes) == self.privkey_len, f'{len(privbytes)} bytes: incorrect private key length!'
-			assert pubkey_type in self.wif_ver_num, f'{pubkey_type!r}: invalid pubkey_type'
-			return _b58chk_encode(
-				bytes.fromhex(self.wif_ver_num[pubkey_type])
-				+ privbytes
-				+ (b'',b'\x01')[bool(compressed)])
-
-		def parse_wif(self,wif):
-			key = _b58chk_decode(wif)
-
-			for k,v in self.wif_ver_num.items():
-				v = bytes.fromhex(v)
-				if key[:len(v)] == v:
-					pubkey_type = k
-					key = key[len(v):]
-					break
-			else:
-				raise ValueError('Invalid WIF version number')
-
-			if len(key) == self.privkey_len + 1:
-				assert key[-1] == 0x01, f'{key[-1]!r}: invalid compressed key suffix byte'
-				compressed = True
-			elif len(key) == self.privkey_len:
-				compressed = False
-			else:
-				raise ValueError(f'{len(key)}: invalid key length')
-
-			return parsed_wif(
-				sec         = key[:self.privkey_len],
-				pubkey_type = pubkey_type,
-				compressed  = compressed )
-
-		def parse_addr(self,addr):
-
-			if 'B' in self.mmtypes and addr[:len(self.bech32_hrp)] == self.bech32_hrp:
-				import mmgen.bech32 as bech32
-				ret = bech32.decode(self.bech32_hrp,addr)
-
-				if ret[0] != self.witness_vernum:
-					from .util import msg
-					msg(f'{ret[0]}: Invalid witness version number')
-					return False
-
-				return parsed_addr( bytes(ret[1]), 'bech32' ) if ret[1] else False
-
-			return self.parse_addr_bytes(_b58chk_decode(addr))
-
-		def pubhash2addr(self,pubkey_hash,p2sh):
-			assert len(pubkey_hash) == 20, f'{len(pubkey_hash)}: invalid length for pubkey hash'
-			return _b58chk_encode(
-				self.addr_fmt_to_ver_bytes(('p2pkh','p2sh')[p2sh],return_hex=False) + pubkey_hash
-			)
-
-		# Segwit:
-		def pubkey2redeem_script(self,pubkey):
-			# https://bitcoincore.org/en/segwit_wallet_dev/
-			# The P2SH redeemScript is always 22 bytes. It starts with a OP_0, followed
-			# by a canonical push of the keyhash (i.e. 0x0014{20-byte keyhash})
-			return bytes.fromhex(self.witness_vernum_hex + '14') + hash160(pubkey)
-
-		def pubkey2segwitaddr(self,pubkey):
-			return self.pubhash2addr(
-				hash160( self.pubkey2redeem_script(pubkey)), p2sh=True )
-
-		def pubhash2bech32addr(self,pubhash):
-			d = list(pubhash)
-			import mmgen.bech32 as bech32
-			return bech32.bech32_encode(self.bech32_hrp,[self.witness_vernum]+bech32.convertbits(d,8,5))
-
-	class BitcoinTestnet(Bitcoin):
-		addr_ver_bytes      = { '6f': 'p2pkh', 'c4': 'p2sh' }
-		wif_ver_num         = { 'std': 'ef' }
-		bech32_hrp          = 'tb'
-
-	class BitcoinRegtest(BitcoinTestnet):
-		bech32_hrp          = 'bcrt'
-		halving_interval    = 150
-
-	class BitcoinCash(Bitcoin):
-		is_fork_of      = 'Bitcoin'
-		mmtypes         = ('L','C')
-		sighash_type    = 'ALL|FORKID'
-		forks = [
-			_finfo(478559,'000000000000000000651ef99cb9fcbe0dadde1d424bd9f15ff20136191a5eec','BTC',False)
-		]
-		caps = ()
-		coin_amt        = 'BCHAmt'
-		max_tx_fee      = '0.1'
-		ignore_daemon_version = False
-
-		def pubkey2redeem_script(self,pubkey): raise NotImplementedError
-		def pubkey2segwitaddr(self,pubkey):    raise NotImplementedError
-
-	class BitcoinCashTestnet(BitcoinCash):
-		addr_ver_bytes = { '6f': 'p2pkh', 'c4': 'p2sh' }
-		wif_ver_num    = { 'std': 'ef' }
-
-	class BitcoinCashRegtest(BitcoinCashTestnet):
-		halving_interval = 150
-
-	class Litecoin(Bitcoin):
-		block0          = '12a765e31ffd4059bada1e25190f6e98c99d9714d334efa41a195a7e7e04bfe2'
-		addr_ver_bytes  = { '30': 'p2pkh', '32': 'p2sh', '05': 'p2sh' } # new p2sh ver 0x32 must come first
-		wif_ver_num     = { 'std': 'b0' }
-		mmtypes         = ('L','C','S','B')
-		coin_amt        = 'LTCAmt'
-		max_tx_fee      = '0.3'
-		base_coin       = 'LTC'
-		forks           = []
-		bech32_hrp      = 'ltc'
-		avg_bdi         = 150
-		halving_interval = 840000
-		ignore_daemon_version = False
-
-	class LitecoinTestnet(Litecoin):
-		# 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
-		bech32_hrp         = 'tltc'
-
-	class LitecoinRegtest(LitecoinTestnet):
-		bech32_hrp         = 'rltc'
-		halving_interval   = 150
-
-	class DummyWIF:
-
 		def bytes2wif(self,privbytes,pubkey_type,compressed):
 		def bytes2wif(self,privbytes,pubkey_type,compressed):
 			assert pubkey_type == self.pubkey_type, f'{pubkey_type}: invalid pubkey_type for {self.name} protocol!'
 			assert pubkey_type == self.pubkey_type, f'{pubkey_type}: invalid pubkey_type for {self.name} protocol!'
 			assert compressed == False, f'{self.name} protocol does not support compressed pubkeys!'
 			assert compressed == False, f'{self.name} protocol does not support compressed pubkeys!'
@@ -385,163 +196,6 @@ class CoinProtocol(MMGenObject):
 				pubkey_type = self.pubkey_type,
 				pubkey_type = self.pubkey_type,
 				compressed  = False )
 				compressed  = False )
 
 
-	class Ethereum(DummyWIF,Secp256k1):
-
-		network_names = _nw('mainnet','testnet','devnet')
-		addr_len      = 20
-		mmtypes       = ('E',)
-		dfl_mmtype    = 'E'
-		mod_clsname   = 'Ethereum'
-		base_coin     = 'ETH'
-		pubkey_type   = 'std' # required by DummyWIF
-
-		coin_amt      = 'ETHAmt'
-		max_tx_fee    = '0.005'
-		chain_names   = ['ethereum','foundation']
-		sign_mode     = 'standalone'
-		caps          = ('token',)
-		mmcaps        = ('key','addr','rpc','tx')
-		base_proto    = 'Ethereum'
-		avg_bdi       = 15
-		ignore_daemon_version = False
-
-		chain_ids = {
-			1:    'ethereum',         # ethereum mainnet
-			2:    'morden',           # morden testnet (deprecated)
-			3:    'ropsten',          # ropsten testnet
-			4:    'rinkeby',          # rinkeby testnet
-			5:    'goerli',           # goerli testnet
-			42:   'kovan',            # kovan testnet
-			61:   'classic',          # ethereum classic mainnet
-			62:   'morden',           # ethereum classic testnet
-			17:   'developmentchain', # parity dev chain
-			1337: 'developmentchain', # geth dev chain
-		}
-
-		@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:
-				return parsed_addr( bytes.fromhex(addr), 'ethereum' )
-			if g.debug:
-				from .util import Msg
-				Msg(f'Invalid address: {addr}')
-			return False
-
-		@classmethod
-		def checksummed_addr(cls,addr):
-			h = self.keccak_256(addr.encode()).digest().hex()
-			return ''.join(addr[i].upper() if int(h[i],16) > 7 else addr[i] for i in range(len(addr)))
-
-		def pubhash2addr(self,pubkey_hash,p2sh):
-			assert len(pubkey_hash) == 20, f'{len(pubkey_hash)}: invalid length for {self.name} pubkey hash'
-			assert not p2sh, f'{self.name} protocol has no P2SH address format'
-			return pubkey_hash.hex()
-
-	class EthereumTestnet(Ethereum):
-		chain_names = ['kovan','goerli','rinkeby']
-
-	class EthereumRegtest(EthereumTestnet):
-		chain_names = ['developmentchain']
-
-	class EthereumClassic(Ethereum):
-		chain_names = ['classic','ethereum_classic']
-		max_tx_fee  = '0.005'
-		ignore_daemon_version = False
-
-	class EthereumClassicTestnet(EthereumClassic):
-		chain_names = ['morden','morden_testnet','classic-testnet']
-
-	class EthereumClassicRegtest(EthereumClassicTestnet):
-		chain_names = ['developmentchain']
-
-	class Zcash(Bitcoin):
-		base_coin      = 'ZEC'
-		addr_ver_bytes = { '1cb8': 'p2pkh', '1cbd': 'p2sh', '169a': 'zcash_z', 'a8abd3': 'viewkey' }
-		wif_ver_num    = { 'std': '80', 'zcash_z': 'ab36' }
-		pubkey_types   = ('std','zcash_z')
-		mmtypes        = ('L','C','Z')
-		mmcaps         = ('key','addr')
-		dfl_mmtype     = 'L'
-		avg_bdi        = 75
-
-		def __init__(self,*args,**kwargs):
-			super().__init__(*args,**kwargs)
-			from .opts import opt
-			self.coin_id = 'ZEC-Z' if opt.type in ('zcash_z','Z') else 'ZEC-T'
-
-		def get_addr_len(self,addr_fmt):
-			return (20,64)[addr_fmt in ('zcash_z','viewkey')]
-
-		def preprocess_key(self,sec,pubkey_type):
-			if pubkey_type == 'zcash_z': # zero the first four bits
-				return bytes([sec[0] & 0x0f]) + sec[1:]
-			else:
-				return super().preprocess_key(sec,pubkey_type)
-
-		def pubhash2addr(self,pubkey_hash,p2sh):
-			hash_len = len(pubkey_hash)
-			if hash_len == 20:
-				return super().pubhash2addr(pubkey_hash,p2sh)
-			elif hash_len == 64:
-				raise NotImplementedError('Zcash z-addresses do not support pubhash2addr()')
-			else:
-				raise ValueError(f'{hash_len}: incorrect pubkey_hash length')
-
-	class ZcashTestnet(Zcash):
-		wif_ver_num  = { 'std': 'ef', 'zcash_z': 'ac08' }
-		addr_ver_bytes = { '1d25': 'p2pkh', '1cba': 'p2sh', '16b6': 'zcash_z', 'a8ac0c': 'viewkey' }
-
-	# https://github.com/monero-project/monero/blob/master/src/cryptonote_config.h
-	class Monero(DummyWIF,Base):
-
-		network_names  = _nw('mainnet','stagenet',None)
-		base_coin      = 'XMR'
-		addr_ver_bytes = { '12': 'monero', '2a': 'monero_sub' }
-		addr_len       = 68
-		wif_ver_num    = {}
-		pubkey_types   = ('monero',)
-		mmtypes        = ('M',)
-		dfl_mmtype     = 'M'
-		pubkey_type    = 'monero' # required by DummyWIF
-		avg_bdi        = 120
-		privkey_len    = 32
-		mmcaps         = ('key','addr')
-		ignore_daemon_version = False
-		coin_amt       = 'XMRAmt'
-
-		def preprocess_key(self,sec,pubkey_type): # reduce key
-			from .ed25519 import l
-			return int.to_bytes(
-				int.from_bytes( sec[::-1], 'big' ) % l,
-				self.privkey_len,
-				'big' )[::-1]
-
-		def parse_addr(self,addr):
-
-			from .baseconv import baseconv
-
-			def b58dec(addr_str):
-				bc = baseconv('b58')
-				l = len(addr_str)
-				a = b''.join([bc.tobytes( addr_str[i*11:i*11+11], pad=8 ) for i in range(l//11)])
-				b = bc.tobytes( addr_str[-(l%11):], pad=5 )
-				return a + b
-
-			ret = b58dec(addr)
-
-			chk = self.keccak_256(ret[:-4]).digest()[:4]
-
-			assert ret[-4:] == chk, f'{ret[-4:].hex()}: incorrect checksum.  Correct value: {chk.hex()}'
-
-			return self.parse_addr_bytes(ret)
-
-	class MoneroTestnet(Monero): # use stagenet for testnet
-		addr_ver_bytes = { '18': 'monero', '24': 'monero_sub' } # testnet is ('35','3f')
-
 def init_proto(coin=None,testnet=False,regtest=False,network=None,network_id=None,tokensym=None,need_amt=False):
 def init_proto(coin=None,testnet=False,regtest=False,network=None,network_id=None,tokensym=None,need_amt=False):
 
 
 	assert type(testnet) == bool, 'init_proto_chk1'
 	assert type(testnet) == bool, 'init_proto_chk1'
@@ -567,6 +221,14 @@ def init_proto(coin=None,testnet=False,regtest=False,network=None,network_id=Non
 	name = CoinProtocol.coins[coin].name
 	name = CoinProtocol.coins[coin].name
 	proto_name = name + ('' if network == 'mainnet' else network.capitalize())
 	proto_name = name + ('' if network == 'mainnet' else network.capitalize())
 
 
+	if not hasattr(CoinProtocol,proto_name):
+		import importlib
+		setattr(
+			CoinProtocol,
+			proto_name,
+			getattr(importlib.import_module(f'mmgen.proto.{coin}'),network)
+		)
+
 	return getattr(CoinProtocol,proto_name)(
 	return getattr(CoinProtocol,proto_name)(
 		coin      = coin,
 		coin      = coin,
 		name      = name,
 		name      = name,
@@ -622,4 +284,5 @@ def warn_trustlevel(coinsym):
 		return
 		return
 
 
 	if not keypress_confirm(warning,default_yes=True):
 	if not keypress_confirm(warning,default_yes=True):
+		import sys
 		sys.exit(0)
 		sys.exit(0)

+ 2 - 2
mmgen/tool/coin.py

@@ -133,7 +133,7 @@ class tool_cmd(tool_cmd_base):
 		if self.mmtype.name == 'segwit':
 		if self.mmtype.name == 'segwit':
 			return self.proto.pubkey2segwitaddr( pubkey )
 			return self.proto.pubkey2segwitaddr( pubkey )
 		else:
 		else:
-			from ..protocol import hash160
+			from ..proto.btc import hash160
 			return self.pubhash2addr( hash160(pubkey).hex() )
 			return self.pubhash2addr( hash160(pubkey).hex() )
 
 
 	def pubhex2redeem_script(self,pubkeyhex:'sstr'): # new
 	def pubhex2redeem_script(self,pubkeyhex:'sstr'): # new
@@ -146,7 +146,7 @@ class tool_cmd(tool_cmd_base):
 		assert self.mmtype.name == 'segwit', 'This command is meaningful only for --type=segwit'
 		assert self.mmtype.name == 'segwit', 'This command is meaningful only for --type=segwit'
 		assert redeem_scripthex[:4] == '0014', f'{redeem_scripthex!r}: invalid redeem script'
 		assert redeem_scripthex[:4] == '0014', f'{redeem_scripthex!r}: invalid redeem script'
 		assert len(redeem_scripthex) == 44, f'{len(redeem_scripthex)//2} bytes: invalid redeem script length'
 		assert len(redeem_scripthex) == 44, f'{len(redeem_scripthex)//2} bytes: invalid redeem script length'
-		from ..protocol import hash160
+		from ..proto.btc import hash160
 		return self.pubhash2addr( hash160(bytes.fromhex(redeem_scripthex)).hex() )
 		return self.pubhash2addr( hash160(bytes.fromhex(redeem_scripthex)).hex() )
 
 
 	def pubhash2addr(self,pubhashhex:'sstr'):
 	def pubhash2addr(self,pubhashhex:'sstr'):

+ 3 - 3
mmgen/tool/util.py

@@ -78,7 +78,7 @@ class tool_cmd(tool_cmd_base):
 
 
 	def hash160(self,hexstr:'sstr'):
 	def hash160(self,hexstr:'sstr'):
 		"compute ripemd160(sha256(data)) (convert hex pubkey to hex addr)"
 		"compute ripemd160(sha256(data)) (convert hex pubkey to hex addr)"
-		from ..protocol import hash160
+		from ..proto.btc import hash160
 		return hash160( bytes.fromhex(hexstr) ).hex()
 		return hash160( bytes.fromhex(hexstr) ).hex()
 
 
 	def hash256(self,string_or_bytes:str,file_input=False,hex_input=False): # TODO: handle stdin
 	def hash256(self,string_or_bytes:str,file_input=False,hex_input=False): # TODO: handle stdin
@@ -143,12 +143,12 @@ class tool_cmd(tool_cmd_base):
 
 
 	def hextob58chk(self,hexstr:'sstr'):
 	def hextob58chk(self,hexstr:'sstr'):
 		"convert a hexadecimal number to base58-check encoding"
 		"convert a hexadecimal number to base58-check encoding"
-		from ..protocol import _b58chk_encode
+		from ..proto.btc import _b58chk_encode
 		return _b58chk_encode( bytes.fromhex(hexstr) )
 		return _b58chk_encode( bytes.fromhex(hexstr) )
 
 
 	def b58chktohex(self,b58chk_num:'sstr'):
 	def b58chktohex(self,b58chk_num:'sstr'):
 		"convert a base58-check encoded number to hexadecimal"
 		"convert a base58-check encoded number to hexadecimal"
-		from ..protocol import _b58chk_decode
+		from ..proto.btc import _b58chk_decode
 		return _b58chk_decode(b58chk_num).hex()
 		return _b58chk_decode(b58chk_num).hex()
 
 
 	def hextob32(self,hexstr:'sstr',pad=0):
 	def hextob32(self,hexstr:'sstr',pad=0):

+ 2 - 2
mmgen/util.py

@@ -674,8 +674,8 @@ def altcoin_subclass(cls,proto,mod_dir):
 	"""
 	"""
 	magic module loading and class retrieval
 	magic module loading and class retrieval
 	"""
 	"""
-	from .protocol import CoinProtocol
-	if isinstance(proto,CoinProtocol.Bitcoin):
+
+	if proto.base_coin != 'ETH':
 		return cls
 		return cls
 
 
 	modname = f'mmgen.altcoins.{proto.base_coin.lower()}.{mod_dir}'
 	modname = f'mmgen.altcoins.{proto.base_coin.lower()}.{mod_dir}'

+ 1 - 1
mmgen/xmrwallet.py

@@ -26,7 +26,7 @@ from .common import *
 from .objmethods import Hilite,InitErrors
 from .objmethods import Hilite,InitErrors
 from .obj import CoinTxID
 from .obj import CoinTxID
 from .seed import SeedID
 from .seed import SeedID
-from .protocol import _b58a,init_proto
+from .protocol import init_proto,_b58a
 from .addr import CoinAddr,AddrIdx
 from .addr import CoinAddr,AddrIdx
 from .addrlist import KeyAddrList,AddrIdxList
 from .addrlist import KeyAddrList,AddrIdxList
 from .rpc import MoneroRPCClientRaw,MoneroWalletRPCClient,json_encoder
 from .rpc import MoneroRPCClientRaw,MoneroWalletRPCClient,json_encoder

+ 1 - 0
setup.cfg

@@ -42,6 +42,7 @@ install_requires =
 packages =
 packages =
 	mmgen
 	mmgen
 	mmgen.share
 	mmgen.share
+	mmgen.proto
 	mmgen.tool
 	mmgen.tool
 	mmgen.altcoins
 	mmgen.altcoins
 	mmgen.altcoins.eth
 	mmgen.altcoins.eth

+ 1 - 1
test/misc/tool_api_test.py

@@ -73,7 +73,7 @@ def run_test():
 	tool.usr_randchars = 0 # setter
 	tool.usr_randchars = 0 # setter
 	check_equal(tool.usr_randchars,0)
 	check_equal(tool.usr_randchars,0)
 
 
-	check_equal(f'{tool.coin} {tool.proto.cls_name} {tool.addrtype}', 'BTC Bitcoin L' )
+	check_equal(f'{tool.coin} {tool.proto.cls_name} {tool.addrtype}', 'BTC mainnet L' )
 
 
 	# test vectors from tooltest2.py:
 	# test vectors from tooltest2.py:
 
 

+ 1 - 0
test/overlay/__init__.py

@@ -37,6 +37,7 @@ def overlay_setup(repo_root):
 				'mmgen.data',
 				'mmgen.data',
 				'mmgen.share',
 				'mmgen.share',
 				'mmgen.tool',
 				'mmgen.tool',
+				'mmgen.proto',
 				'mmgen.altcoins',
 				'mmgen.altcoins',
 				'mmgen.altcoins.eth',
 				'mmgen.altcoins.eth',
 				'mmgen.altcoins.eth.pyethereum',
 				'mmgen.altcoins.eth.pyethereum',