Browse Source

modularize daemon classes

- protocol-independent base classes remain in `daemon.py`
- protocol-dependent subclasses are moved to `base_proto/{name}/daemon.py`
The MMGen Project 3 years ago
parent
commit
b17b6f6275

+ 159 - 0
mmgen/base_proto/bitcoin/daemon.py

@@ -0,0 +1,159 @@
+#!/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
+
+"""
+base_proto.bitcoin.daemon: Bitcoin base protocol daemon classes
+"""
+
+import os
+
+from ...globalvars import g
+from ...util import list_gen
+from ...daemon import CoinDaemon,_nw,_dd
+
+class bitcoin_core_daemon(CoinDaemon):
+	daemon_data = _dd('Bitcoin Core', 220000, '22.0.0')
+	exec_fn = 'bitcoind'
+	cli_fn = 'bitcoin-cli'
+	testnet_dir = 'testnet3'
+	cfg_file_hdr = '# Bitcoin Core config file\n'
+	tracking_wallet_name = 'mmgen-tracking-wallet'
+	rpc_ports = _nw(8332, 18332, 18443)
+	cfg_file = 'bitcoin.conf'
+	datadirs = {
+		'linux': [g.home_dir,'.bitcoin'],
+		'win':   [os.getenv('APPDATA'),'Bitcoin']
+	}
+	nonstd_datadir = False
+
+	def init_datadir(self):
+		if self.network == 'regtest' and not self.test_suite:
+			return os.path.join( g.data_dir_root, 'regtest', g.coin.lower() )
+		else:
+			return super().init_datadir()
+
+	@property
+	def network_datadir(self):
+		"location of the network's blockchain data and authentication cookie"
+		return os.path.join (
+			self.datadir, {
+				'mainnet': '',
+				'testnet': self.testnet_dir,
+				'regtest': 'regtest',
+			}[self.network] )
+
+	def init_subclass(self):
+
+		if self.network == 'regtest':
+			"""
+			fall back on hard-coded credentials
+			"""
+			from ...regtest import MMGenRegtest
+			self.rpc_user = MMGenRegtest.rpc_user
+			self.rpc_password = MMGenRegtest.rpc_password
+
+		self.shared_args = list_gen(
+			[f'--datadir={self.datadir}',         self.nonstd_datadir or self.non_dfl_datadir],
+			[f'--rpcport={self.rpc_port}'],
+			[f'--rpcuser={self.rpc_user}',         self.network == 'regtest'],
+			[f'--rpcpassword={self.rpc_password}', self.network == 'regtest'],
+			['--testnet',                          self.network == 'testnet'],
+			['--regtest',                          self.network == 'regtest'],
+		)
+
+		self.coind_args = list_gen(
+			['--listen=0'],
+			['--keypool=1'],
+			['--rpcallowip=127.0.0.1'],
+			[f'--rpcbind=127.0.0.1:{self.rpc_port}'],
+			['--pid='+self.pidfile,    self.use_pidfile],
+			['--daemon',               self.platform == 'linux' and not self.opt.no_daemonize],
+			['--fallbackfee=0.0002',   self.coin == 'BTC' and self.network == 'regtest'],
+			['--usecashaddr=0',        self.coin == 'BCH'],
+			['--mempoolreplacement=1', self.coin == 'LTC'],
+			['--txindex=1',            self.coin == 'LTC' or self.network == 'regtest'],
+		)
+
+		self.lockfile = os.path.join(self.network_datadir,'.cookie')
+
+	@property
+	def state(self):
+		cp = self.cli('getblockcount',silent=True)
+		err = cp.stderr.decode()
+		if ("error: couldn't connect" in err
+			or "error: Could not connect" in err
+			or "does not exist" in err ):
+			# regtest has no cookie file, so test will always fail
+			ret = 'busy' if (self.lockfile and os.path.exists(self.lockfile)) else 'stopped'
+		elif cp.returncode == 0:
+			ret = 'ready'
+		else:
+			ret = 'busy'
+		if self.debug:
+			print(f'State: {ret!r}')
+		return ret
+
+	@property
+	def stop_cmd(self):
+		return self.cli_cmd('stop')
+
+	def set_label_args(self,rpc,coinaddr,lbl):
+		if 'label_api' in rpc.caps:
+			return ('setlabel',coinaddr,lbl)
+		else:
+			# NOTE: this works because importaddress() removes the old account before
+			# associating the new account with the address.
+			# RPC args: addr,label,rescan[=true],p2sh[=none]
+			return ('importaddress',coinaddr,lbl,False)
+
+	def estimatefee_args(self,rpc):
+		return (opt.tx_confs,)
+
+	def sigfail_errmsg(self,e):
+		return e.args[0]
+
+class bitcoin_cash_node_daemon(bitcoin_core_daemon):
+	daemon_data = _dd('Bitcoin Cash Node', 24000000, '24.0.0')
+	exec_fn = 'bitcoind-bchn'
+	cli_fn = 'bitcoin-cli-bchn'
+	rpc_ports = _nw(8432, 18432, 18543) # use non-standard ports (core+100)
+	datadirs = {
+		'linux': [g.home_dir,'.bitcoin-bchn'],
+		'win':   [os.getenv('APPDATA'),'Bitcoin_ABC']
+	}
+	cfg_file_hdr = '# Bitcoin Cash Node config file\n'
+	nonstd_datadir = True
+
+	def set_label_args(self,rpc,coinaddr,lbl):
+		# bitcoin-{abc,bchn} '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
+		return ('importaddress',coinaddr,lbl,False)
+
+	def estimatefee_args(self,rpc):
+		return () if rpc.daemon_version >= 190100 else (opt.tx_confs,)
+
+	def sigfail_errmsg(self,e):
+		return (
+			'This is not the BCH chain.\nRe-run the script without the --coin=bch option.'
+				if 'Invalid sighash param' in e.args[0] else
+			e.args[0] )
+
+class litecoin_core_daemon(bitcoin_core_daemon):
+	daemon_data = _dd('Litecoin Core', 180100, '0.18.1')
+	exec_fn = 'litecoind'
+	cli_fn = 'litecoin-cli'
+	testnet_dir = 'testnet4'
+	rpc_ports = _nw(9332, 19332, 19443)
+	cfg_file = 'litecoin.conf'
+	datadirs = {
+		'linux': [g.home_dir,'.litecoin'],
+		'win':   [os.getenv('APPDATA'),'Litecoin']
+	}
+	cfg_file_hdr = '# Litecoin Core config file\n'

+ 178 - 0
mmgen/base_proto/ethereum/daemon.py

@@ -0,0 +1,178 @@
+#!/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
+
+"""
+base_proto.ethereum.daemon: Ethereum base protocol daemon classes
+"""
+
+import os
+
+from ...globalvars import g
+from ...util import list_gen,get_subclasses
+from ...daemon import CoinDaemon,RPCDaemon,_nw,_dd
+
+class ethereum_daemon(CoinDaemon):
+	chain_subdirs = _nw('ethereum','goerli','DevelopmentChain')
+	base_rpc_port = 8545  # same for all networks!
+	base_p2p_port = 30303 # same for all networks!
+	daemon_port_offset = 100
+	network_port_offsets = _nw(0,10,20)
+
+	def __init__(self,*args,**kwargs):
+
+		if not hasattr(self,'all_daemons'):
+			ethereum_daemon.all_daemons = get_subclasses(ethereum_daemon,names=True)
+
+		self.port_offset = (
+			self.all_daemons.index(self.id+'_daemon') * self.daemon_port_offset
+			+ getattr(self.network_port_offsets,self.network) )
+
+		return super().__init__(*args,**kwargs)
+
+	def get_rpc_port(self):
+		return self.base_rpc_port + self.port_offset
+
+	def get_p2p_port(self):
+		return self.base_p2p_port + self.port_offset
+
+	def init_datadir(self):
+		self.logdir = super().init_datadir()
+		return os.path.join(
+			self.logdir,
+			self.id,
+			getattr(self.chain_subdirs,self.network) )
+
+class openethereum_daemon(ethereum_daemon):
+	daemon_data = _dd('OpenEthereum', 3003000, '3.3.0')
+	version_pat = r'OpenEthereum//v(\d+)\.(\d+)\.(\d+)'
+	exec_fn = 'openethereum'
+	cfg_file = 'parity.conf'
+	datadirs = {
+		'linux': [g.home_dir,'.local','share','io.parity.ethereum'],
+		'win':   [os.getenv('LOCALAPPDATA'),'Parity','Ethereum']
+	}
+
+	def init_subclass(self):
+
+		ld = self.platform == 'linux' and not self.opt.no_daemonize
+
+		self.coind_args = list_gen(
+			['--no-ws'],
+			['--no-ipc'],
+			['--no-secretstore'],
+			[f'--jsonrpc-port={self.rpc_port}'],
+			[f'--port={self.p2p_port}', self.p2p_port],
+			[f'--base-path={self.datadir}', self.non_dfl_datadir],
+			[f'--chain={self.proto.chain_name}', self.network!='regtest'],
+			[f'--config=dev', self.network=='regtest'], # no presets for mainnet or testnet
+			['--mode=offline', self.test_suite or self.network=='regtest'],
+			[f'--log-file={self.logfile}', self.non_dfl_datadir],
+			['daemon', ld],
+			[self.pidfile, ld],
+		)
+
+class parity_daemon(openethereum_daemon):
+	daemon_data = _dd('Parity', 2007002, '2.7.2')
+	version_pat = r'Parity-Ethereum//v(\d+)\.(\d+)\.(\d+)'
+	exec_fn = 'parity'
+
+class geth_daemon(ethereum_daemon):
+	daemon_data = _dd('Geth', 1010014, '1.10.14')
+	version_pat = r'Geth/v(\d+)\.(\d+)\.(\d+)'
+	exec_fn = 'geth'
+	use_pidfile = False
+	use_threads = True
+	datadirs = {
+		'linux': [g.home_dir,'.ethereum','geth'],
+		'win':   [os.getenv('LOCALAPPDATA'),'Geth'] # FIXME
+	}
+
+	def init_subclass(self):
+		self.coind_args = list_gen(
+			['--verbosity=0'],
+			['--http'],
+			['--http.api=eth,web3,txpool'],
+			[f'--http.port={self.rpc_port}'],
+			[f'--port={self.p2p_port}', self.p2p_port], # geth binds p2p port even with --maxpeers=0
+			['--maxpeers=0', not self.opt.online],
+			[f'--datadir={self.datadir}', self.non_dfl_datadir],
+			['--goerli', self.network=='testnet'],
+			['--dev', self.network=='regtest'],
+		)
+
+# https://github.com/ledgerwatch/erigon
+class erigon_daemon(geth_daemon):
+	daemon_data = _dd('Erigon', 2021009005, '2021.09.5')
+	version_pat = r'erigon/(\d+)\.(\d+)\.(\d+)'
+	exec_fn = 'erigon'
+	private_ports = _nw(9090,9091,9092) # testnet and regtest are non-standard
+	datadirs = {
+		'linux': [g.home_dir,'.local','share','erigon'],
+		'win':   [os.getenv('LOCALAPPDATA'),'Erigon'] # FIXME
+	}
+
+	def init_subclass(self):
+		self.coind_args = list_gen(
+			['--verbosity=0'],
+			[f'--port={self.p2p_port}', self.p2p_port],
+			['--maxpeers=0', not self.opt.online],
+			[f'--private.api.addr=127.0.0.1:{self.private_port}'],
+			[f'--datadir={self.datadir}', self.non_dfl_datadir and not self.network=='regtest'],
+			['--chain=goerli', self.network=='testnet'],
+			['--chain=dev', self.network=='regtest'],
+			['--mine', self.network=='regtest'],
+		)
+		self.rpc_d = erigon_rpcdaemon(
+			proto        = self.proto,
+			rpc_port     = self.rpc_port,
+			private_port = self.private_port,
+			test_suite   = self.test_suite,
+			datadir      = self.datadir )
+
+	def start(self,quiet=False,silent=False):
+		super().start(quiet=quiet,silent=silent)
+		self.rpc_d.debug = self.debug
+		return self.rpc_d.start(quiet=quiet,silent=silent)
+
+	def stop(self,quiet=False,silent=False):
+		self.rpc_d.debug = self.debug
+		self.rpc_d.stop(quiet=quiet,silent=silent)
+		return super().stop(quiet=quiet,silent=silent)
+
+	@property
+	def start_cmds(self):
+		return [self.start_cmd,self.rpc_d.start_cmd]
+
+class erigon_rpcdaemon(RPCDaemon):
+
+	master_daemon = 'erigon_daemon'
+	rpc_type = 'Erigon'
+	exec_fn = 'rpcdaemon'
+	use_pidfile = False
+	use_threads = True
+
+	def __init__(self,proto,rpc_port,private_port,test_suite,datadir):
+
+		self.proto = proto
+		self.test_suite = test_suite
+
+		super().__init__()
+
+		self.network = proto.network
+		self.rpc_port = rpc_port
+		self.datadir = datadir
+
+		self.daemon_args = list_gen(
+			['--verbosity=0'],
+			[f'--private.api.addr=127.0.0.1:{private_port}'],
+			[f'--http.port={self.rpc_port}'],
+			[f'--datadir={self.datadir}', self.network != 'regtest'],
+			['--http.api=eth,web3,txpool'],
+		)

+ 57 - 4
mmgen/base_proto/monero/daemon.py

@@ -9,15 +9,69 @@
 #   https://gitlab.com/mmgen/mmgen
 
 """
-base_proto.monero.daemon: Monero daemon classes
+base_proto.monero.daemon: Monero base protocol daemon classes
 """
 
 import os
 
 from ...globalvars import g
 from ...opts import opt
-from ...util import list_gen
-from ...daemon import CoinDaemon,RPCDaemon,_nw
+from ...util import list_gen,die
+from ...daemon import CoinDaemon,RPCDaemon,_nw,_dd
+
+class monero_daemon(CoinDaemon):
+	daemon_data = _dd('Monero', 'N/A', 'N/A')
+	networks = ('mainnet','testnet')
+	exec_fn = 'monerod'
+	testnet_dir = 'stagenet'
+	new_console_mswin = True
+	host = 'localhost' # FIXME
+	rpc_ports = _nw(18081, 38081, None) # testnet is stagenet
+	cfg_file = 'bitmonero.conf'
+	datadirs = {
+		'linux': [g.home_dir,'.bitmonero'],
+		'win':   ['/','c','ProgramData','bitmonero']
+	}
+
+	def init_datadir(self):
+		self.logdir = super().init_datadir()
+		return os.path.join(
+			self.logdir,
+			self.testnet_dir if self.network == 'testnet' else '' )
+
+	def get_p2p_port(self):
+		return self.rpc_port - 1
+
+	def init_subclass(self):
+
+		from ...rpc import MoneroRPCClientRaw
+		self.rpc = MoneroRPCClientRaw(
+			host   = self.host,
+			port   = self.rpc_port,
+			user   = None,
+			passwd = None,
+			test_connection = False,
+			daemon = self )
+
+		self.shared_args = list_gen(
+			[f'--no-zmq'],
+			[f'--p2p-bind-port={self.p2p_port}', self.p2p_port],
+			[f'--rpc-bind-port={self.rpc_port}'],
+			['--stagenet', self.network == 'testnet'],
+		)
+
+		self.coind_args = list_gen(
+			['--hide-my-port'],
+			['--no-igd'],
+			[f'--data-dir={self.datadir}', self.non_dfl_datadir],
+			[f'--pidfile={self.pidfile}', self.platform == 'linux'],
+			['--detach',                  not (self.opt.no_daemonize or self.platform=='win')],
+			['--offline',                 not self.opt.online],
+		)
+
+	@property
+	def stop_cmd(self):
+		return ['kill','-Wf',self.pid] if self.platform == 'win' else [self.exec_fn] + self.shared_args + ['exit']
 
 class MoneroWalletDaemon(RPCDaemon):
 
@@ -65,7 +119,6 @@ class MoneroWalletDaemon(RPCDaemon):
 		assert self.host
 		assert self.user
 		if not self.passwd:
-			from ...util import die
 			die(1,
 				'You must set your Monero wallet RPC password.\n' +
 				'This can be done on the command line with the --wallet-rpc-password option\n' +

+ 15 - 367
mmgen/daemon.py

@@ -20,17 +20,15 @@
 daemon.py:  Daemon control interface for the MMGen suite
 """
 
-import os,shutil,time
+import os,time,importlib
 from subprocess import run,PIPE,CompletedProcess
 from collections import namedtuple
 
 from .globalvars import g
-from .opts import opt
-from .util import msg,die,list_gen,get_subclasses
+from .util import msg,die
 from .flags import *
 
 _dd = namedtuple('daemon_data',['coind_name','coind_version','coind_version_str']) # latest tested version
-_cd = namedtuple('coins_data',['coin_name','daemon_ids'])
 _nw = namedtuple('coin_networks',['mainnet','testnet','regtest'])
 
 class Daemon(Lockable):
@@ -256,22 +254,23 @@ class CoinDaemon(Daemon):
 	rpc_user = None
 	rpc_password = None
 
+	_cd = namedtuple('coins_data',['coin_name','networks','daemon_ids'])
 	coins = {
-		'BTC': _cd('Bitcoin',           ['bitcoin_core']),
-		'BCH': _cd('Bitcoin Cash Node', ['bitcoin_cash_node']),
-		'LTC': _cd('Litecoin',          ['litecoin_core']),
-		'XMR': _cd('Monero',            ['monero']),
-		'ETH': _cd('Ethereum',          ['openethereum','geth'] + (['erigon'] if g.enable_erigon else []) ),
-		'ETC': _cd('Ethereum Classic',  ['parity']),
+		'BTC': _cd('Bitcoin',           networks,              ['bitcoin_core']),
+		'BCH': _cd('Bitcoin Cash Node', networks,              ['bitcoin_cash_node']),
+		'LTC': _cd('Litecoin',          networks,              ['litecoin_core']),
+		'XMR': _cd('Monero',            ('mainnet','testnet'), ['monero']),
+		'ETH': _cd('Ethereum',          networks,              ['openethereum','geth']
+			+ (['erigon'] if g.enable_erigon else []) ),
+		'ETC': _cd('Ethereum Classic',  networks,              ['parity']),
 	}
 
 	@classmethod
 	def get_network_ids(cls): # FIXME: gets IDs for _default_ daemon only
 		from .protocol import CoinProtocol
-		import mmgen.daemon as daemon_mod
 		def gen():
 			for coin,data in cls.coins.items():
-				for network in getattr( daemon_mod, data.daemon_ids[0]+'_daemon' ).networks:
+				for network in data.networks:
 					yield CoinProtocol.Base.create_network_id(coin,network)
 		return list(gen())
 
@@ -306,8 +305,10 @@ class CoinDaemon(Daemon):
 		if daemon_id not in daemon_ids:
 			die(1,f'{daemon_id!r}: invalid daemon_id - valid choices: {fmt_list(daemon_ids)}')
 
-		import mmgen.daemon
-		me = Daemon.__new__( getattr(mmgen.daemon, daemon_id+'_daemon') )
+		me = Daemon.__new__(
+			getattr(
+				importlib.import_module(f'mmgen.base_proto.{proto.base_proto.lower()}.daemon'),
+				daemon_id + '_daemon' ))
 
 		assert network in me.networks, f'{network!r}: unsupported network for daemon {daemon_id}'
 		me.network = network
@@ -429,356 +430,3 @@ class CoinDaemon(Daemon):
 				pass
 		else:
 			msg(f'Cannot remove {self.network_datadir!r} - daemon is not stopped')
-
-class bitcoin_core_daemon(CoinDaemon):
-	daemon_data = _dd('Bitcoin Core', 220000, '22.0.0')
-	exec_fn = 'bitcoind'
-	cli_fn = 'bitcoin-cli'
-	testnet_dir = 'testnet3'
-	cfg_file_hdr = '# Bitcoin Core config file\n'
-	tracking_wallet_name = 'mmgen-tracking-wallet'
-	rpc_ports = _nw(8332, 18332, 18443)
-	cfg_file = 'bitcoin.conf'
-	datadirs = {
-		'linux': [g.home_dir,'.bitcoin'],
-		'win':   [os.getenv('APPDATA'),'Bitcoin']
-	}
-	nonstd_datadir = False
-
-	def init_datadir(self):
-		if self.network == 'regtest' and not self.test_suite:
-			return os.path.join( g.data_dir_root, 'regtest', g.coin.lower() )
-		else:
-			return super().init_datadir()
-
-	@property
-	def network_datadir(self):
-		"location of the network's blockchain data and authentication cookie"
-		return os.path.join (
-			self.datadir, {
-				'mainnet': '',
-				'testnet': self.testnet_dir,
-				'regtest': 'regtest',
-			}[self.network] )
-
-	def init_subclass(self):
-
-		if self.network == 'regtest':
-			"""
-			fall back on hard-coded credentials
-			"""
-			from .regtest import MMGenRegtest
-			self.rpc_user = MMGenRegtest.rpc_user
-			self.rpc_password = MMGenRegtest.rpc_password
-
-		self.shared_args = list_gen(
-			[f'--datadir={self.datadir}',         self.nonstd_datadir or self.non_dfl_datadir],
-			[f'--rpcport={self.rpc_port}'],
-			[f'--rpcuser={self.rpc_user}',         self.network == 'regtest'],
-			[f'--rpcpassword={self.rpc_password}', self.network == 'regtest'],
-			['--testnet',                          self.network == 'testnet'],
-			['--regtest',                          self.network == 'regtest'],
-		)
-
-		self.coind_args = list_gen(
-			['--listen=0'],
-			['--keypool=1'],
-			['--rpcallowip=127.0.0.1'],
-			[f'--rpcbind=127.0.0.1:{self.rpc_port}'],
-			['--pid='+self.pidfile,    self.use_pidfile],
-			['--daemon',               self.platform == 'linux' and not self.opt.no_daemonize],
-			['--fallbackfee=0.0002',   self.coin == 'BTC' and self.network == 'regtest'],
-			['--usecashaddr=0',        self.coin == 'BCH'],
-			['--mempoolreplacement=1', self.coin == 'LTC'],
-			['--txindex=1',            self.coin == 'LTC' or self.network == 'regtest'],
-		)
-
-		self.lockfile = os.path.join(self.network_datadir,'.cookie')
-
-	@property
-	def state(self):
-		cp = self.cli('getblockcount',silent=True)
-		err = cp.stderr.decode()
-		if ("error: couldn't connect" in err
-			or "error: Could not connect" in err
-			or "does not exist" in err ):
-			# regtest has no cookie file, so test will always fail
-			ret = 'busy' if (self.lockfile and os.path.exists(self.lockfile)) else 'stopped'
-		elif cp.returncode == 0:
-			ret = 'ready'
-		else:
-			ret = 'busy'
-		if self.debug:
-			print(f'State: {ret!r}')
-		return ret
-
-	@property
-	def stop_cmd(self):
-		return self.cli_cmd('stop')
-
-	def set_label_args(self,rpc,coinaddr,lbl):
-		if 'label_api' in rpc.caps:
-			return ('setlabel',coinaddr,lbl)
-		else:
-			# NOTE: this works because importaddress() removes the old account before
-			# associating the new account with the address.
-			# RPC args: addr,label,rescan[=true],p2sh[=none]
-			return ('importaddress',coinaddr,lbl,False)
-
-	def estimatefee_args(self,rpc):
-		return (opt.tx_confs,)
-
-	def sigfail_errmsg(self,e):
-		return e.args[0]
-
-class bitcoin_cash_node_daemon(bitcoin_core_daemon):
-	daemon_data = _dd('Bitcoin Cash Node', 24000000, '24.0.0')
-	exec_fn = 'bitcoind-bchn'
-	cli_fn = 'bitcoin-cli-bchn'
-	rpc_ports = _nw(8432, 18432, 18543) # use non-standard ports (core+100)
-	datadirs = {
-		'linux': [g.home_dir,'.bitcoin-bchn'],
-		'win':   [os.getenv('APPDATA'),'Bitcoin_ABC']
-	}
-	cfg_file_hdr = '# Bitcoin Cash Node config file\n'
-	nonstd_datadir = True
-
-	def set_label_args(self,rpc,coinaddr,lbl):
-		# bitcoin-{abc,bchn} '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
-		return ('importaddress',coinaddr,lbl,False)
-
-	def estimatefee_args(self,rpc):
-		return () if rpc.daemon_version >= 190100 else (opt.tx_confs,)
-
-	def sigfail_errmsg(self,e):
-		return (
-			'This is not the BCH chain.\nRe-run the script without the --coin=bch option.'
-				if 'Invalid sighash param' in e.args[0] else
-			e.args[0] )
-
-class litecoin_core_daemon(bitcoin_core_daemon):
-	daemon_data = _dd('Litecoin Core', 180100, '0.18.1')
-	exec_fn = 'litecoind'
-	cli_fn = 'litecoin-cli'
-	testnet_dir = 'testnet4'
-	rpc_ports = _nw(9332, 19332, 19443)
-	cfg_file = 'litecoin.conf'
-	datadirs = {
-		'linux': [g.home_dir,'.litecoin'],
-		'win':   [os.getenv('APPDATA'),'Litecoin']
-	}
-	cfg_file_hdr = '# Litecoin Core config file\n'
-
-class monero_daemon(CoinDaemon):
-	daemon_data = _dd('Monero', 'N/A', 'N/A')
-	networks = ('mainnet','testnet')
-	exec_fn = 'monerod'
-	testnet_dir = 'stagenet'
-	new_console_mswin = True
-	host = 'localhost' # FIXME
-	rpc_ports = _nw(18081, 38081, None) # testnet is stagenet
-	cfg_file = 'bitmonero.conf'
-	datadirs = {
-		'linux': [g.home_dir,'.bitmonero'],
-		'win':   ['/','c','ProgramData','bitmonero']
-	}
-
-	def init_datadir(self):
-		self.logdir = super().init_datadir()
-		return os.path.join(
-			self.logdir,
-			self.testnet_dir if self.network == 'testnet' else '' )
-
-	def get_p2p_port(self):
-		return self.rpc_port - 1
-
-	def init_subclass(self):
-
-		from .rpc import MoneroRPCClientRaw
-		self.rpc = MoneroRPCClientRaw(
-			host   = self.host,
-			port   = self.rpc_port,
-			user   = None,
-			passwd = None,
-			test_connection = False,
-			daemon = self )
-
-		self.shared_args = list_gen(
-			[f'--no-zmq'],
-			[f'--p2p-bind-port={self.p2p_port}', self.p2p_port],
-			[f'--rpc-bind-port={self.rpc_port}'],
-			['--stagenet', self.network == 'testnet'],
-		)
-
-		self.coind_args = list_gen(
-			['--hide-my-port'],
-			['--no-igd'],
-			[f'--data-dir={self.datadir}', self.non_dfl_datadir],
-			[f'--pidfile={self.pidfile}', self.platform == 'linux'],
-			['--detach',                  not (self.opt.no_daemonize or self.platform=='win')],
-			['--offline',                 not self.opt.online],
-		)
-
-	@property
-	def stop_cmd(self):
-		return ['kill','-Wf',self.pid] if self.platform == 'win' else [self.exec_fn] + self.shared_args + ['exit']
-
-class ethereum_daemon(CoinDaemon):
-	chain_subdirs = _nw('ethereum','goerli','DevelopmentChain')
-	base_rpc_port = 8545  # same for all networks!
-	base_p2p_port = 30303 # same for all networks!
-	daemon_port_offset = 100
-	network_port_offsets = _nw(0,10,20)
-
-	def __init__(self,*args,**kwargs):
-
-		if not hasattr(self,'all_daemons'):
-			ethereum_daemon.all_daemons = get_subclasses(ethereum_daemon,names=True)
-
-		self.port_offset = (
-			self.all_daemons.index(self.id+'_daemon') * self.daemon_port_offset
-			+ getattr(self.network_port_offsets,self.network) )
-
-		return super().__init__(*args,**kwargs)
-
-	def get_rpc_port(self):
-		return self.base_rpc_port + self.port_offset
-
-	def get_p2p_port(self):
-		return self.base_p2p_port + self.port_offset
-
-	def init_datadir(self):
-		self.logdir = super().init_datadir()
-		return os.path.join(
-			self.logdir,
-			self.id,
-			getattr(self.chain_subdirs,self.network) )
-
-class openethereum_daemon(ethereum_daemon):
-	daemon_data = _dd('OpenEthereum', 3003000, '3.3.0')
-	version_pat = r'OpenEthereum//v(\d+)\.(\d+)\.(\d+)'
-	exec_fn = 'openethereum'
-	cfg_file = 'parity.conf'
-	datadirs = {
-		'linux': [g.home_dir,'.local','share','io.parity.ethereum'],
-		'win':   [os.getenv('LOCALAPPDATA'),'Parity','Ethereum']
-	}
-
-	def init_subclass(self):
-
-		ld = self.platform == 'linux' and not self.opt.no_daemonize
-
-		self.coind_args = list_gen(
-			['--no-ws'],
-			['--no-ipc'],
-			['--no-secretstore'],
-			[f'--jsonrpc-port={self.rpc_port}'],
-			[f'--port={self.p2p_port}', self.p2p_port],
-			[f'--base-path={self.datadir}', self.non_dfl_datadir],
-			[f'--chain={self.proto.chain_name}', self.network!='regtest'],
-			[f'--config=dev', self.network=='regtest'], # no presets for mainnet or testnet
-			['--mode=offline', self.test_suite or self.network=='regtest'],
-			[f'--log-file={self.logfile}', self.non_dfl_datadir],
-			['daemon', ld],
-			[self.pidfile, ld],
-		)
-
-class parity_daemon(openethereum_daemon):
-	daemon_data = _dd('Parity', 2007002, '2.7.2')
-	version_pat = r'Parity-Ethereum//v(\d+)\.(\d+)\.(\d+)'
-	exec_fn = 'parity'
-
-class geth_daemon(ethereum_daemon):
-	daemon_data = _dd('Geth', 1010014, '1.10.14')
-	version_pat = r'Geth/v(\d+)\.(\d+)\.(\d+)'
-	exec_fn = 'geth'
-	use_pidfile = False
-	use_threads = True
-	datadirs = {
-		'linux': [g.home_dir,'.ethereum','geth'],
-		'win':   [os.getenv('LOCALAPPDATA'),'Geth'] # FIXME
-	}
-
-	def init_subclass(self):
-		self.coind_args = list_gen(
-			['--verbosity=0'],
-			['--http'],
-			['--http.api=eth,web3,txpool'],
-			[f'--http.port={self.rpc_port}'],
-			[f'--port={self.p2p_port}', self.p2p_port], # geth binds p2p port even with --maxpeers=0
-			['--maxpeers=0', not self.opt.online],
-			[f'--datadir={self.datadir}', self.non_dfl_datadir],
-			['--goerli', self.network=='testnet'],
-			['--dev', self.network=='regtest'],
-		)
-
-# https://github.com/ledgerwatch/erigon
-class erigon_daemon(geth_daemon):
-	daemon_data = _dd('Erigon', 2021009005, '2021.09.5')
-	version_pat = r'erigon/(\d+)\.(\d+)\.(\d+)'
-	exec_fn = 'erigon'
-	private_ports = _nw(9090,9091,9092) # testnet and regtest are non-standard
-	datadirs = {
-		'linux': [g.home_dir,'.local','share','erigon'],
-		'win':   [os.getenv('LOCALAPPDATA'),'Erigon'] # FIXME
-	}
-
-	def init_subclass(self):
-		self.coind_args = list_gen(
-			['--verbosity=0'],
-			[f'--port={self.p2p_port}', self.p2p_port],
-			['--maxpeers=0', not self.opt.online],
-			[f'--private.api.addr=127.0.0.1:{self.private_port}'],
-			[f'--datadir={self.datadir}', self.non_dfl_datadir and not self.network=='regtest'],
-			['--chain=goerli', self.network=='testnet'],
-			['--chain=dev', self.network=='regtest'],
-			['--mine', self.network=='regtest'],
-		)
-		self.rpc_d = erigon_rpcdaemon(
-			proto        = self.proto,
-			rpc_port     = self.rpc_port,
-			private_port = self.private_port,
-			test_suite   = self.test_suite,
-			datadir      = self.datadir )
-
-	def start(self,quiet=False,silent=False):
-		super().start(quiet=quiet,silent=silent)
-		self.rpc_d.debug = self.debug
-		return self.rpc_d.start(quiet=quiet,silent=silent)
-
-	def stop(self,quiet=False,silent=False):
-		self.rpc_d.debug = self.debug
-		self.rpc_d.stop(quiet=quiet,silent=silent)
-		return super().stop(quiet=quiet,silent=silent)
-
-	@property
-	def start_cmds(self):
-		return [self.start_cmd,self.rpc_d.start_cmd]
-
-class erigon_rpcdaemon(RPCDaemon):
-
-	master_daemon = 'erigon_daemon'
-	rpc_type = 'Erigon'
-	exec_fn = 'rpcdaemon'
-	use_pidfile = False
-	use_threads = True
-
-	def __init__(self,proto,rpc_port,private_port,test_suite,datadir):
-
-		self.proto = proto
-		self.test_suite = test_suite
-
-		super().__init__()
-
-		self.network = proto.network
-		self.rpc_port = rpc_port
-		self.datadir = datadir
-
-		self.daemon_args = list_gen(
-			['--verbosity=0'],
-			[f'--private.api.addr=127.0.0.1:{private_port}'],
-			[f'--http.port={self.rpc_port}'],
-			[f'--datadir={self.datadir}', self.network != 'regtest'],
-			['--http.api=eth,web3,txpool'],
-		)

+ 1 - 1
mmgen/data/release_date

@@ -1 +1 @@
-January 2022
+February 2022

+ 1 - 1
mmgen/data/version

@@ -1 +1 @@
-13.1.dev013
+13.1.dev014

+ 1 - 0
mmgen/proto/xmr.py

@@ -19,6 +19,7 @@ class mainnet(CoinProtocol.DummyWIF,CoinProtocol.Base):
 
 	network_names  = _nw('mainnet','stagenet',None)
 	base_coin      = 'XMR'
+	base_proto     = 'Monero'
 	addr_ver_bytes = { '12': 'monero', '2a': 'monero_sub' }
 	addr_len       = 68
 	wif_ver_num    = {}

+ 1 - 1
test/unit_tests_d/ut_daemon.py

@@ -62,7 +62,7 @@ def test_cmds(op):
 			for daemon_id in data.daemon_ids:
 				if daemon_id in arm_skip_daemons:
 					continue
-				for network in getattr( daemon_mod, daemon_id+'_daemon' ).networks:
+				for network in data.networks:
 					if opt.no_altcoin_deps and coin != 'BTC':
 						continue
 					d = CoinDaemon(