From f8a312e407feadd72c03e98c1666ac8e1c10353d Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Mon, 27 Jan 2025 16:01:53 +0000 Subject: [PATCH] coin-specific and protocol-specific configuration options Rationale: to enable communication with multiple coin daemons on multiple hosts in a single program invocation, making possible the implementation of asset swap functionality, for instance Coin-specific options are prefixed with a coin symbol, proto-specific options with a coin symbol plus a network name. Coin- and protocol-specific options override their non-prefixed counterparts. They are available via the command line, configuration file and Config API. Currently available options: Option Supported Prefixes tw_name btc ltc bch rpc_user btc ltc bch rpc_password btc ltc bch rpc_host btc ltc bch eth etc rpc_port btc ltc bch eth etc xmr ignore_daemon_version btc ltc bch eth etc xmr max_tx_fee btc ltc bch eth etc chain_names eth_mainnet eth_testnet etc_mainnet etc_testnet Example: $ mmgen-tool --coin=ltc --ltc-tw-name=ltc2 --ltc-ignore-daemon-version twview Help: $ mmgen-tool --longhelp $ view mmgen/data/mmgen.cfg Testing: $ test/daemontest.py rpc.btc rpc.geth $ test/cmdtest.py help opts cfgfile --- mmgen/cfg.py | 78 +++++++++++------------ mmgen/cfgfile.py | 16 ++--- mmgen/daemon.py | 3 +- mmgen/data/mmgen.cfg | 60 +++++++++--------- mmgen/data/version | 2 +- mmgen/help/__init__.py | 19 +++--- mmgen/opts.py | 53 ++++++++++++++-- mmgen/proto/bch/params.py | 1 - mmgen/proto/btc/params.py | 12 +++- mmgen/proto/btc/rpc.py | 24 ++++--- mmgen/proto/etc/params.py | 1 - mmgen/proto/eth/params.py | 12 +++- mmgen/proto/eth/rpc.py | 2 +- mmgen/proto/ltc/params.py | 1 - mmgen/proto/xmr/params.py | 8 ++- mmgen/protocol.py | 42 ++++++++++++- test/cmdtest_d/ct_cfgfile.py | 6 +- test/cmdtest_d/ct_opts.py | 118 ++++++++++++++++++++++++++++++++++- test/daemontest_d/ut_rpc.py | 32 +++++++++- test/misc/opts_main.py | 5 ++ 20 files changed, 377 insertions(+), 118 deletions(-) diff --git a/mmgen/cfg.py b/mmgen/cfg.py index fd817053..1de18b58 100755 --- a/mmgen/cfg.py +++ b/mmgen/cfg.py @@ -280,6 +280,8 @@ class Config(Lockable): ('autosign', 'outdir'), ) + # proto-specific only: eth_mainnet_chain_names eth_testnet_chain_names + # coin-specific only: bch_cashaddr (alias of cashaddr) _cfg_file_opts = ( 'autochg_ignore_labels', 'color', @@ -289,6 +291,7 @@ class Config(Lockable): 'force_256_color', 'hash_preset', 'http_timeout', + 'ignore_daemon_version', # also coin-specific 'macos_autosign_ramdisk_size', 'max_input_size', 'max_tx_file_size', @@ -298,27 +301,15 @@ class Config(Lockable): 'no_license', 'quiet', 'regtest', - 'rpc_host', - 'rpc_password', - 'rpc_port', - 'rpc_user', + 'rpc_host', # also coin-specific + 'rpc_password', # also coin-specific + 'rpc_port', # also coin-specific + 'rpc_user', # also coin-specific 'scroll', 'subseeds', 'testnet', - 'usr_randchars', - 'bch_cashaddr', - 'bch_max_tx_fee', - 'btc_max_tx_fee', - 'eth_max_tx_fee', - 'ltc_max_tx_fee', - 'bch_ignore_daemon_version', - 'btc_ignore_daemon_version', - 'etc_ignore_daemon_version', - 'eth_ignore_daemon_version', - 'ltc_ignore_daemon_version', - 'xmr_ignore_daemon_version', - 'eth_mainnet_chain_names', - 'eth_testnet_chain_names') + 'tw_name', # also coin-specific + 'usr_randchars') # Supported environmental vars # The corresponding attributes (lowercase, without 'mmgen_') must exist in the class. @@ -563,6 +554,9 @@ class Config(Lockable): del self._cloned + if hasattr(self, 'bch_cashaddr') and not hasattr(self, 'cashaddr'): + self.cashaddr = self.bch_cashaddr + self._lock() if need_proto: @@ -634,34 +628,29 @@ class Config(Lockable): non_auto_opts = [] already_set = tuple(self._uopts) + env_cfg + def set_opt(d, obj, name, refval): + val = ucfg.parse_value(d.value, refval) + if not val: + die('CfgFileParseError', f'Parse error in file {ucfg.fn!r}, line {d.lineno}') + val_conv = conv_type(name, val, refval, src=ucfg.fn) + setattr(obj, name, val_conv) + non_auto_opts.append(name) + for d in ucfg.get_lines(): if d.name in self._cfg_file_opts: - ns = d.name.split('_') - if ns[0] in gc.core_coins: - if not need_proto: - continue - nse, tn = ( - (ns[2:], ns[1]=='testnet') if len(ns) > 2 and ns[1] in ('mainnet', 'testnet') else - (ns[1:], False) - ) - # no instance yet, so override _class_ attr: - cls = init_proto(self, ns[0], tn, need_amt=True, return_cls=True) - attr = '_'.join(nse) - else: - cls = self - attr = d.name - refval = getattr(cls, attr) - val = ucfg.parse_value(d.value, refval) - if not val: - die('CfgFileParseError', f'Parse error in file {ucfg.fn!r}, line {d.lineno}') - val_conv = conv_type(attr, val, refval, src=ucfg.fn) - if not attr in already_set: - setattr(cls, attr, val_conv) - non_auto_opts.append(attr) + if not d.name in already_set: + set_opt(d, self, d.name, getattr(self, d.name)) elif d.name in self._autoset_opts: autoset_opts[d.name] = d.value elif d.name in self._auto_typeset_opts: auto_typeset_opts[d.name] = d.value + elif any(d.name.startswith(coin + '_') for coin in gc.rpc_coins): + if need_proto and not d.name in already_set: + try: + refval = init_proto(self, d.name.split('_', 1)[0]).get_opt_clsval(self, d.name) + except AttributeError: + die('CfgFileParseError', f'{d.name!r}: unrecognized option in {ucfg.fn!r}, line {d.lineno}') + set_opt(d, self, d.name, refval) else: die('CfgFileParseError', f'{d.name!r}: unrecognized option in {ucfg.fn!r}, line {d.lineno}') @@ -946,7 +935,8 @@ def conv_type(name, val, refval, src, invert_bool=False): d = '' if src in ('cmdline', 'cfg', 'env') else f' in {src!r}', e = type(refval).__name__)) - if type(refval) is bool: + # refval is None = boolean opt with no cmdline parameter + if type(refval) is bool or refval is None: v = str(val).lower() ret = ( True if v in ('true', 'yes', '1', 'on') else @@ -954,6 +944,12 @@ def conv_type(name, val, refval, src, invert_bool=False): None ) return do_fail() if ret is None else (not ret) if invert_bool else ret + elif isinstance(refval, (list, tuple)): + if src == 'cmdline': + return type(refval)(val.split(',')) + else: + assert isinstance(val, (list, tuple)), f'{val}: not a list or tuple' + return type(refval)(val) else: try: return type(refval)(not val if invert_bool else val) diff --git a/mmgen/cfgfile.py b/mmgen/cfgfile.py index 37b95433..2dbfcb92 100755 --- a/mmgen/cfgfile.py +++ b/mmgen/cfgfile.py @@ -254,15 +254,17 @@ class CfgFileSampleUsr(cfg_file_sample): def show_changes(self, diff): ymsg('Warning: configuration file options have changed!\n') for desc in ('added', 'removed'): - data = diff[desc] - if data: - opts = fmt_list([i.name for i in data], fmt='bare') - msg(f' The following option{suf(data, verb="has")} been {desc}:\n {opts}\n') - if desc == 'removed' and data: + changed_opts = [i.name for i in diff[desc] + # workaround for coin-specific opts previously listed in sample file: + if not (i.name.endswith('_ignore_daemon_version') and desc == 'removed') + ] + if changed_opts: + msg(f' The following option{suf(changed_opts, verb="has")} been {desc}:') + msg(f' {fmt_list(changed_opts, fmt="bare")}\n') + if desc == 'removed': uc = mmgen_cfg_file(self.cfg, 'usr') usr_names = [i.name for i in uc.get_lines()] - rm_names = [i.name for i in data] - bad = sorted(set(usr_names).intersection(rm_names)) + bad = sorted(set(usr_names).intersection(changed_opts)) if bad: m = f""" The following removed option{suf(bad, verb='is')} set in {uc.fn!r} diff --git a/mmgen/daemon.py b/mmgen/daemon.py index 960a7b7c..600b3e55 100755 --- a/mmgen/daemon.py +++ b/mmgen/daemon.py @@ -420,7 +420,8 @@ class CoinDaemon(Daemon): ps_adj = (port_shift or 0) + (self.test_suite_port_shift if test_suite else 0) # user-set values take precedence - self.rpc_port = (cfg.rpc_port or 0) + (port_shift or 0) if cfg.rpc_port else ps_adj + self.get_rpc_port() + usr_rpc_port = self.proto.rpc_port or cfg.rpc_port + self.rpc_port = usr_rpc_port + (port_shift or 0) if usr_rpc_port else ps_adj + self.get_rpc_port() self.p2p_port = ( p2p_port or ( self.get_p2p_port() + ps_adj if self.get_p2p_port() and (test_suite or ps_adj) else None diff --git a/mmgen/data/mmgen.cfg b/mmgen/data/mmgen.cfg index 5e20a101..a6307a6d 100644 --- a/mmgen/data/mmgen.cfg +++ b/mmgen/data/mmgen.cfg @@ -26,18 +26,6 @@ # Uncomment to use testnet instead of mainnet: # testnet true -# Set the RPC host (the host the coin daemon is running on): -# rpc_host localhost - -# Set the RPC host's port number: -# rpc_port 8332 - -# Uncomment to override 'rpcuser' from coin daemon config file: -# rpc_user myusername - -# Uncomment to override 'rpcpassword' from coin daemon config file: -# rpc_password mypassword - # Choose the backend to use for JSON-RPC connections. Valid choices: # 'auto' (defaults to 'httplib'), 'httplib', 'requests', 'curl', 'aiohttp': # rpc_backend auto @@ -89,27 +77,44 @@ # setups with unusually large Monero wallets: # macos_autosign_ramdisk_size 10 -############################ -## Ignore daemon versions ## -############################ +# Ignore coin daemon version. This option also has coin-specific variants +# (see below): +# ignore_daemon_version false -# Ignore Bitcoin Core version: -# btc_ignore_daemon_version false +# Specify the tracking wallet name. This option also has coin-specific +# variants (see below): +# tw_name my-other-tracking-wallet -# Ignore Litecoin Core version: -# ltc_ignore_daemon_version false +##################################################################### +## RPC options. These also have coin-specific variants (see below) ## +##################################################################### -# Ignore Bitcoin Cash Node version: -# bch_ignore_daemon_version false +# Set the RPC host (the host the coin daemon is running on): +# rpc_host localhost -# Ignore OpenEthereum version for ETH: -# eth_ignore_daemon_version false +# Set the RPC host's port number: +# rpc_port 8332 -# Ignore OpenEthereum version for ETC: -# etc_ignore_daemon_version false +# Uncomment to override 'rpcuser' from coin daemon config file: +# rpc_user myusername -# Ignore daemon version for Monero: -# xmr_ignore_daemon_version false +# Uncomment to override 'rpcpassword' from coin daemon config file: +# rpc_password mypassword + +####################################################################### +####################### COIN-SPECIFIC OPTIONS ####################### +####################################################################### +## OPTION SUPPORTED PREFIXES ## +## tw_name btc ltc bch ## +## rpc_user btc ltc bch ## +## rpc_password btc ltc bch ## +## rpc_host btc ltc bch eth etc ## +## rpc_port btc ltc bch eth etc xmr ## +## ignore_daemon_version btc ltc bch eth etc xmr ## +## max_tx_fee btc ltc bch eth etc ## +## Note: prefix is followed by an underscore, e.g. ‘xmr_rpc_port’ ## +####################################################################### +####################################################################### ##################### ## Altcoin options ## @@ -139,7 +144,6 @@ # Set the Monero wallet RPC password to something secure: # monero_wallet_rpc_password passw0rd - ####################################################################### ## The following options are probably of interest only to developers ## ####################################################################### diff --git a/mmgen/data/version b/mmgen/data/version index 8764a1d1..9eb885ae 100644 --- a/mmgen/data/version +++ b/mmgen/data/version @@ -1 +1 @@ -15.1.dev11 +15.1.dev12 diff --git a/mmgen/help/__init__.py b/mmgen/help/__init__.py index 5b4e138e..44098db1 100755 --- a/mmgen/help/__init__.py +++ b/mmgen/help/__init__.py @@ -142,17 +142,18 @@ class GlobalHelp(Help): data_desc = 'global_opts_data' def gen_text(self, opts): - from ..opts import global_opts_pat + from ..opts import global_opts_help_pat skipping = False for line in opts.global_opts_data['text']['options'][1:-3].splitlines(): - if m := global_opts_pat.match(line): - if m[1] in opts.global_opts_filter.coin and m[2] in opts.global_opts_filter.cmd: - yield ' --{} {}'.format(m[3], m[5]) - skipping = False - else: - skipping = True - elif not skipping: - yield line[4:] + m = global_opts_help_pat.match(line) + if m[1] == '+': + if not skipping: + yield line[4:] + elif m[1] in opts.global_opts_filter.coin and m[2] in opts.global_opts_filter.cmd: + yield ' --{} {}'.format(m[3], m[5]) if m[3] else m[5] + skipping = False + else: + skipping = True def print_help(cfg, opts): diff --git a/mmgen/opts.py b/mmgen/opts.py index 870ff9fc..6f5a36bf 100755 --- a/mmgen/opts.py +++ b/mmgen/opts.py @@ -43,7 +43,7 @@ def get_opt_by_substring(opt, opts): from .util import die die('CmdlineOptError', f'--{opt}: ambiguous option (not unique substring)') -def process_uopts(opts_data, opts): +def process_uopts(cfg, opts_data, opts, need_proto): from .util import die @@ -83,6 +83,32 @@ def process_uopts(opts_data, opts): if parm: die('CmdlineOptError', f'option --{_opt} requires no parameter') yield (negated_opts(opts)[_opt].name, False) + elif ( + need_proto + and (not gc.cmd_caps or gc.cmd_caps.rpc) + and any(opt.startswith(coin + '-') for coin in gc.rpc_coins)): + opt_name = opt.replace('-', '_') + from .protocol import init_proto + try: + refval = init_proto(cfg, opt.split('-', 1)[0], return_cls=True).get_opt_clsval(cfg, opt_name) + except AttributeError: + die('CmdlineOptError', f'--{opt}: unrecognized option') + else: + if refval is None: # None == no parm + if parm: + die('CmdlineOptError', f'option --{opt} requires no parameter') + yield (opt_name, True) + else: + from .cfg import conv_type + if parm: + yield (opt_name, + conv_type(opt_name, parm, refval, src='cmdline')) + else: + idx += 1 + if idx == argv_len or (parm := sys.argv[idx]).startswith('-'): + die('CmdlineOptError', f'missing parameter for option --{opt}') + yield (opt_name, + conv_type(opt_name, parm, refval, src='cmdline')) else: die('CmdlineOptError', f'--{opt}: unrecognized option') elif arg[0] == '-' and len(arg) > 1: @@ -125,10 +151,11 @@ def process_uopts(opts_data, opts): return uopts, uargs cmd_opts_pat = re.compile(r'^-([a-zA-Z0-9-]), --([a-zA-Z0-9-]{2,64})(=| )(.+)') -global_opts_pat = re.compile(r'^\t\t\t(.)(.) --([a-zA-Z0-9-]{2,64})(=| )(.+)') +global_opts_pat = re.compile(r'^\t\t\t(.)(.) --([a-z0-9-]{2,64})(=| )(.+)') +global_opts_help_pat = re.compile(r'^\t\t\t(.)(.) (?:--([{}a-zA-Z0-9-]{2,64})(=| ))?(.+)') opt_tuple = namedtuple('cmdline_option', ['name', 'has_parm']) -def parse_opts(opts_data, opt_filter, global_opts_data, global_opts_filter): +def parse_opts(cfg, opts_data, opt_filter, global_opts_data, global_opts_filter, need_proto): def parse_cmd_opts_text(): for line in opts_data['text']['options'].strip().splitlines(): @@ -146,7 +173,7 @@ def parse_opts(opts_data, opt_filter, global_opts_data, global_opts_filter): opts = tuple(parse_cmd_opts_text()) + tuple(parse_global_opts_text()) - uopts, uargs = process_uopts(opts_data, dict(opts)) + uopts, uargs = process_uopts(cfg, opts_data, dict(opts), need_proto) return namedtuple('parsed_cmd_opts', ['user_opts', 'cmd_args', 'opts'])( uopts, # dict @@ -214,10 +241,12 @@ class Opts: self.opts_data = opts_data po = parsed_opts or parse_opts( + cfg, opts_data, opt_filter, self.global_opts_data, - self.global_opts_filter) + self.global_opts_filter, + need_proto) cfg._args = po.cmd_args cfg._uopts = uopts = po.user_opts @@ -283,6 +312,20 @@ class UserOpts(Opts): b- --bob Specify user ‘Bob’ in MMGen regtest mode b- --alice Specify user ‘Alice’ in MMGen regtest mode b- --carol Specify user ‘Carol’ in MMGen regtest mode + rr COIN-SPECIFIC OPTIONS: + rr For descriptions, refer to the non-prefixed versions of these options above + rr Prefixed options override their non-prefixed counterparts + rr OPTION SUPPORTED PREFIXES + rr --PREFIX-ignore-daemon-version btc ltc bch eth etc xmr + br --PREFIX-tw-name btc ltc bch + Rr --PREFIX-rpc-host btc ltc bch eth etc + rr --PREFIX-rpc-port btc ltc bch eth etc xmr + br --PREFIX-rpc-user btc ltc bch + br --PREFIX-rpc-password btc ltc bch + Rr --PREFIX-max-tx-fee btc ltc bch eth etc + Rr PROTO-SPECIFIC OPTIONS: + Rr Option Supported Prefixes + Rr --PREFIX-chain-names eth-mainnet eth-testnet etc-mainnet etc-testnet """, }, 'code': { diff --git a/mmgen/proto/bch/params.py b/mmgen/proto/bch/params.py index 7e47b17e..cd68d3a4 100755 --- a/mmgen/proto/bch/params.py +++ b/mmgen/proto/bch/params.py @@ -28,7 +28,6 @@ class mainnet(mainnet): caps = () coin_amt = 'BCHAmt' max_tx_fee = 0.1 - ignore_daemon_version = False cashaddr_pfx = 'bitcoincash' cashaddr = True diff --git a/mmgen/proto/btc/params.py b/mmgen/proto/btc/params.py index 9be6006c..d82e0177 100755 --- a/mmgen/proto/btc/params.py +++ b/mmgen/proto/btc/params.py @@ -50,9 +50,19 @@ class mainnet(CoinProtocol.Secp256k1): # chainparams.cpp diff_adjust_interval = 2016 max_halvings = 64 start_subsidy = 50 - ignore_daemon_version = False max_int = 0xffffffff + coin_cfg_opts = ( + 'ignore_daemon_version', + 'rpc_host', + 'rpc_port', + 'rpc_user', + 'rpc_password', + 'tw_name', + 'max_tx_fee', + 'cashaddr', + ) + def encode_wif(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_bytes, f'{pubkey_type!r}: invalid pubkey_type' diff --git a/mmgen/proto/btc/rpc.py b/mmgen/proto/btc/rpc.py index ceccc6c0..489d95b4 100755 --- a/mmgen/proto/btc/rpc.py +++ b/mmgen/proto/btc/rpc.py @@ -124,11 +124,13 @@ class BitcoinRPCClient(RPCClient, metaclass=AsyncInit): self.proto = proto self.daemon = daemon self.call_sigs = getattr(CallSigs, daemon.id)(cfg) - self.twname = TrackingWalletName(cfg.regtest_user or cfg.tw_name or self.dfl_twname) + self.twname = TrackingWalletName(cfg.regtest_user or proto.tw_name or cfg.tw_name or self.dfl_twname) super().__init__( cfg = cfg, - host = 'localhost' if cfg.test_suite or cfg.network == 'regtest' else (cfg.rpc_host or 'localhost'), + host = ( + 'localhost' if cfg.test_suite or cfg.network == 'regtest' + else (proto.rpc_host or cfg.rpc_host or 'localhost')), port = daemon.rpc_port) self.set_auth() @@ -210,14 +212,15 @@ class BitcoinRPCClient(RPCClient, metaclass=AsyncInit): """ if self.cfg.network == 'regtest': from .regtest import MMGenRegtest - user, passwd = (MMGenRegtest.rpc_user, MMGenRegtest.rpc_password) - elif self.cfg.rpc_user: - user, passwd = (self.cfg.rpc_user, self.cfg.rpc_password) + user = MMGenRegtest.rpc_user + passwd = MMGenRegtest.rpc_password else: - user, passwd = self.get_daemon_cfg_options(('rpcuser', 'rpcpassword')).values() - - if not (user and passwd): - user, passwd = (self.daemon.rpc_user, self.daemon.rpc_password) + user = ( + self.proto.rpc_user or self.cfg.rpc_user or self.get_daemon_cfg_option('rpcuser') + or self.daemon.rpc_user) + passwd = ( + self.proto.rpc_password or self.cfg.rpc_password or self.get_daemon_cfg_option('rpcpassword') + or self.daemon.rpc_password) if user and passwd: self.auth = auth_data(user, passwd) @@ -260,6 +263,9 @@ class BitcoinRPCClient(RPCClient, metaclass=AsyncInit): (os.path.dirname(self.cfg.data_dir) if self.proto.regtest else self.daemon.datadir), self.daemon.cfg_file) + def get_daemon_cfg_option(self, req_key): + return list(self.get_daemon_cfg_options([req_key]).values())[0] + def get_daemon_cfg_options(self, req_keys): fn = self.get_daemon_cfg_fn() diff --git a/mmgen/proto/etc/params.py b/mmgen/proto/etc/params.py index dcea505c..a312f19e 100755 --- a/mmgen/proto/etc/params.py +++ b/mmgen/proto/etc/params.py @@ -18,7 +18,6 @@ class mainnet(mainnet): chain_names = ['classic', 'ethereum_classic'] max_tx_fee = 0.005 coin_amt = 'ETCAmt' - ignore_daemon_version = False class testnet(mainnet): chain_names = ['morden', 'morden_testnet', 'classic-testnet'] diff --git a/mmgen/proto/eth/params.py b/mmgen/proto/eth/params.py index 1646f5b2..105caef3 100755 --- a/mmgen/proto/eth/params.py +++ b/mmgen/proto/eth/params.py @@ -35,7 +35,6 @@ class mainnet(CoinProtocol.DummyWIF, CoinProtocol.Secp256k1): base_proto_coin = 'ETH' base_coin = 'ETH' avg_bdi = 15 - ignore_daemon_version = False decimal_prec = 36 chain_ids = { @@ -52,6 +51,17 @@ class mainnet(CoinProtocol.DummyWIF, CoinProtocol.Secp256k1): 711: 'ethereum', # geth mainnet (empty chain) } + coin_cfg_opts = ( + 'ignore_daemon_version', + 'rpc_host', + 'rpc_port', + 'max_tx_fee', + ) + + proto_cfg_opts = ( + 'chain_names', + ) + @property def dcoin(self): return self.tokensym or self.coin diff --git a/mmgen/proto/eth/rpc.py b/mmgen/proto/eth/rpc.py index e76d2f87..6595e379 100755 --- a/mmgen/proto/eth/rpc.py +++ b/mmgen/proto/eth/rpc.py @@ -48,7 +48,7 @@ class EthereumRPCClient(RPCClient, metaclass=AsyncInit): super().__init__( cfg = cfg, - host = 'localhost' if cfg.test_suite else (cfg.rpc_host or 'localhost'), + host = 'localhost' if cfg.test_suite else (proto.rpc_host or cfg.rpc_host or 'localhost'), port = daemon.rpc_port) await self.set_backend_async(backend) diff --git a/mmgen/proto/ltc/params.py b/mmgen/proto/ltc/params.py index 2a78e0f2..58f2e991 100755 --- a/mmgen/proto/ltc/params.py +++ b/mmgen/proto/ltc/params.py @@ -26,7 +26,6 @@ class mainnet(mainnet): 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' diff --git a/mmgen/proto/xmr/params.py b/mmgen/proto/xmr/params.py index 5854ece6..5749da01 100755 --- a/mmgen/proto/xmr/params.py +++ b/mmgen/proto/xmr/params.py @@ -23,7 +23,7 @@ class MoneroViewKey(HexStr): color, width, hexcase = 'cyan', 64, 'lower' # FIXME - no checking performed # https://github.com/monero-project/monero/blob/master/src/cryptonote_config.h -class mainnet(CoinProtocol.DummyWIF, CoinProtocol.Base): +class mainnet(CoinProtocol.RPC, CoinProtocol.DummyWIF, CoinProtocol.Base): network_names = _nw('mainnet', 'stagenet', None) base_proto = 'Monero' @@ -37,10 +37,14 @@ class mainnet(CoinProtocol.DummyWIF, CoinProtocol.Base): avg_bdi = 120 privkey_len = 32 mmcaps = ('rpc',) - ignore_daemon_version = False coin_amt = 'XMRAmt' sign_mode = 'standalone' + coin_cfg_opts = ( + 'ignore_daemon_version', + 'rpc_port', + ) + def get_addr_len(self, addr_fmt): return (64, 72)[addr_fmt == 'monero_integrated'] diff --git a/mmgen/protocol.py b/mmgen/protocol.py index 56ce90f1..3aed8a5a 100755 --- a/mmgen/protocol.py +++ b/mmgen/protocol.py @@ -116,6 +116,11 @@ class CoinProtocol(MMGenObject): self.coin_amt = None self.max_tx_fee = None + self.set_cfg_opts() + + def set_cfg_opts(self): + pass + @property def dcoin(self): return self.coin @@ -192,8 +197,43 @@ class CoinProtocol(MMGenObject): else: return getattr(importlib.import_module(modpath), clsname) + class RPC: - class Secp256k1(Base): + # prefixed with coin, e.g. ‘ltc_rpc_host’: refvals taken from proto class + coin_cfg_opts = () + + # prefixed with coin + network, e.g. ‘eth_mainnet_chain_names’: refvals taken from proto class + proto_cfg_opts = () + + # default vals (refvals): bool(val) must be False (val = None -> option takes no parameter) + ignore_daemon_version = None + rpc_host = '' + rpc_port = 0 + rpc_user = '' + rpc_password = '' + tw_name = '' + + @classmethod + def get_opt_clsval(cls, cfg, opt): + coin, *rem = opt.split('_', 2) + network = rem[0] if rem[0] in init_proto(cfg, coin, return_cls=True).network_names else None + opt_name = '_'.join(rem[bool(network):]) + if ((network is None and opt_name in cls.coin_cfg_opts) or + (network and opt_name in cls.proto_cfg_opts)): + # raises AttributeError on failure: + return getattr(init_proto(cfg, coin, network=network, return_cls=True), opt_name) + else: + raise AttributeError(f'{opt_name}: unrecognized attribute') + + def set_cfg_opts(self): + for opt in self.cfg.__dict__: + if opt.startswith(self.coin.lower() + '_'): + res = opt.split('_', 2)[1:] + network = res[0] if res[0] in self.network_names else None + if network is None or network == self.network: + setattr(self, '_'.join(res[bool(network):]), getattr(self.cfg, opt)) + + class Secp256k1(RPC, Base): """ Bitcoin and Ethereum protocols inherit from this class """ diff --git a/test/cmdtest_d/ct_cfgfile.py b/test/cmdtest_d/ct_cfgfile.py index 0b1338c3..acd56929 100755 --- a/test/cmdtest_d/ct_cfgfile.py +++ b/test/cmdtest_d/ct_cfgfile.py @@ -203,10 +203,10 @@ class CmdTestCfgFile(CmdTestBase): for coin, res1_chk, res2_chk, res2_chk_eq in ( ('BTC', 'True', '1.2345', True), - ('LTC', 'False', '1.2345', False), - ('BCH', 'False', '1.2345', False), + ('LTC', 'None', '1.2345', False), + ('BCH', 'None', '1.2345', False), ('ETH', 'True', '5.4321', True), - ('ETC', 'False', '5.4321', False) + ('ETC', 'None', '5.4321', False) ): if cfg.no_altcoin and coin != 'BTC': continue diff --git a/test/cmdtest_d/ct_opts.py b/test/cmdtest_d/ct_opts.py index e0f75dce..2d919030 100755 --- a/test/cmdtest_d/ct_opts.py +++ b/test/cmdtest_d/ct_opts.py @@ -46,6 +46,13 @@ class CmdTestOpts(CmdTestBase): ('opt_good22', (41, 'good cmdline opt (opt + negated opt [substring])', [])), ('opt_good23', (41, 'good cmdline opt (negated negative opt [substring])', [])), ('opt_good24', (41, 'good cmdline opt (negated opt + opt [substring])', [])), + ('opt_good25', (41, 'good cmdline opt (--btc-rpc-host)', [])), + ('opt_good26', (41, 'good cmdline opt (--btc-rpc-port)', [])), + ('opt_good27', (41, 'good cmdline opt (--btc-ignore-daemon-version)', [])), + ('opt_good28', (41, 'good cmdline opt (--bch-cashaddr)', [])), + ('opt_good29', (41, 'good cmdline opt (--etc-max-tx-fee=0.1)', [])), + ('opt_good30', (41, 'good cmdline opt (--eth-chain-names=foo,bar)', [])), + ('opt_good31', (41, 'good cmdline opt (--xmr-rpc-port=28081)', [])), ('opt_bad_param', (41, 'bad global opt (--pager=1)', [])), ('opt_bad_infile', (41, 'bad infile parameter', [])), ('opt_bad_outdir', (41, 'bad outdir parameter', [])), @@ -65,6 +72,23 @@ class CmdTestOpts(CmdTestBase): ('opt_invalid_14', (41, 'invalid cmdline opt (long opt substring too short)', [])), ('opt_invalid_15', (41, 'invalid cmdline (too many args)', [])), ('opt_invalid_16', (41, 'invalid cmdline (overlong arg)', [])), + ('opt_invalid_17', (41, 'invalid cmdline opt (--btc-rpc-host without ‘need_proto’)', [])), + ('opt_invalid_18', (41, 'invalid cmdline opt (--btc-rpc-port without ‘need_proto’)', [])), + ('opt_invalid_19', (41, 'invalid cmdline opt (--btc-rpc-port with non-integer param)', [])), + ('opt_invalid_21', (41, 'invalid cmdline opt (--btc-foo)', [])), + ('opt_invalid_22', (41, 'invalid cmdline opt (--btc-rpc-host with missing param)', [])), + ('opt_invalid_23', (41, 'invalid cmdline opt (--btc-ignore-daemon-version with param)', [])), + ('opt_invalid_24', (41, 'invalid cmdline opt (--bch-cashaddr without ‘need_proto’)', [])), + ('opt_invalid_25', (41, 'invalid cmdline opt (--bch-cashaddr without parameter)', [])), + ('opt_invalid_26', (41, 'invalid cmdline opt (--bch-cashaddr with non-bool parameter)', [])), + ('opt_invalid_27', (41, 'invalid cmdline opt (--ltc-cashaddr)', [])), + ('opt_invalid_28', (41, 'invalid cmdline opt (--xmr-max-tx-fee)', [])), + ('opt_invalid_29', (41, 'invalid cmdline opt (--eth-max-tx-fee without parameter)', [])), + ('opt_invalid_30', (41, 'invalid cmdline opt (--eth-max-tx-fee with non-numeric parameter)', [])), + ('opt_invalid_31', (41, 'invalid cmdline opt (--bch-cashaddr without --coin=bch)', [])), + ('opt_invalid_32', (41, 'invalid cmdline opt (--eth-chain-names without --coin=eth)', [])), + ('opt_invalid_33', (41, 'invalid cmdline opt (--xmr-rpc-host)', [])), + ('opt_invalid_34', (41, 'invalid cmdline opt (--eth-rpc-user)', [])), ) def spawn_prog(self, args, opts=[], exit_val=None, need_proto=False): @@ -242,6 +266,45 @@ class CmdTestOpts(CmdTestBase): def opt_good24(self): return self.check_vals(['--no-pag', '--pag'], (('cfg.pager', 'True'),)) + def opt_good25(self): + return self.check_vals( + ['--btc-rpc-host=pi5'], + (('cfg.btc_rpc_host', 'pi5'), ('proto.rpc_host', 'pi5')), + need_proto=True) + + def opt_good26(self): + return self.check_vals( + ['--btc-rpc-port=7272'], + (('cfg.btc_rpc_port', '7272'), ('proto.rpc_port', '7272')), + need_proto=True) + + def opt_good27(self): + return self.check_vals( + ['--btc-ignore-daemon-version'], + (('cfg.btc_ignore_daemon_version', 'True'), ('proto.ignore_daemon_version', 'True'),), + need_proto = True) + + def opt_good28(self): + return self.check_vals( + ['--coin=bch', '--bch-cashaddr=yes'], + (('cfg.bch_cashaddr', 'True'), ('proto.cashaddr', 'True'),), + need_proto = True) + + def opt_good29(self): + return self.check_vals(['--etc-max-tx-fee=0.1'], (('cfg.etc_max_tx_fee', '0.1'),), need_proto=True) + + def opt_good30(self): + return self.check_vals( + ['--coin=eth', '--eth-mainnet-chain-names=foo,bar'], + (('cfg.eth_mainnet_chain_names', r"\['foo', 'bar'\]"), ('proto.chain_names', r"\['foo', 'bar'\]")), + need_proto = True) + + def opt_good31(self): + return self.check_vals( + ['--coin=xmr', '--xmr-rpc-port=28081'], + (('cfg.xmr_rpc_port', '28081'),('proto.rpc_port', '28081'),), + need_proto = True) + def opt_bad_param(self): return self.do_run(['--pager=1'], 'no parameter', 1) @@ -259,8 +322,8 @@ class CmdTestOpts(CmdTestBase): def opt_bad_autoset(self): return self.do_run(['--fee-estimate-mode=Fubar'], 'not unique substring', 1) - def opt_invalid(self, args, expect, exit_val=1): - t = self.spawn_prog(args, exit_val=exit_val) + def opt_invalid(self, args, expect, opts=[], need_proto=False, exit_val=1): + t = self.spawn_prog(args, opts=opts, exit_val=exit_val, need_proto=need_proto) t.expect(expect) return t @@ -305,3 +368,54 @@ class CmdTestOpts(CmdTestBase): def opt_invalid_16(self): return self.opt_invalid(['e' * 4097], 'too long') + + def opt_invalid_17(self): + return self.opt_invalid(['--btc-rpc-host'], 'unrecognized option') + + def opt_invalid_18(self): + return self.opt_invalid(['--btc-rpc-port'], 'unrecognized option') + + def opt_invalid_19(self): + return self.opt_invalid(['--btc-rpc-port=foo'], "must be of type 'int'", need_proto=True) + + def opt_invalid_21(self): + return self.opt_invalid(['--btc-foo'], 'unrecognized option') + + def opt_invalid_22(self): + return self.opt_invalid(['--btc-rpc-host'], 'missing parameter', need_proto=True) + + def opt_invalid_23(self): + return self.opt_invalid(['--btc-ignore-daemon-version=1'], 'requires no parameter', need_proto=True) + + def opt_invalid_24(self): + return self.opt_invalid(['--bch-cashaddr'], 'unrecognized option') + + def opt_invalid_25(self): + return self.opt_invalid(['--bch-cashaddr'], 'missing parameter', need_proto=True) + + def opt_invalid_26(self): + return self.opt_invalid(['--bch-cashaddr=foo'], "must be of type 'bool'", need_proto=True) + + def opt_invalid_27(self): + return self.opt_invalid(['--ltc-cashaddr'], 'unrecognized option', need_proto=True) + + def opt_invalid_28(self): + return self.opt_invalid(['--xmr-max-tx-fee=0.1'], 'unrecognized option', need_proto=True) + + def opt_invalid_29(self): + return self.opt_invalid(['--eth-max-tx-fee'], 'missing parameter', need_proto=True) + + def opt_invalid_30(self): + return self.opt_invalid(['--eth-max-tx-fee=true'], 'must be of type', need_proto=True) + + def opt_invalid_31(self): + return self.opt_invalid(['--bch-cashaddr=true'], 'has no attribute', opts=['--show-opts=bch_cashaddr'], need_proto=True) + + def opt_invalid_32(self): + return self.opt_invalid(['--eth-chain-names=foo,bar'], 'unrecognized option', need_proto=True) + + def opt_invalid_33(self): + return self.opt_invalid(['--xmr-rpc-host=solaris'], 'unrecognized option', need_proto=True) + + def opt_invalid_34(self): + return self.opt_invalid(['--eth-rpc-user=bob'], 'unrecognized option', need_proto=True) diff --git a/test/daemontest_d/ut_rpc.py b/test/daemontest_d/ut_rpc.py index ee421223..03f331b6 100755 --- a/test/daemontest_d/ut_rpc.py +++ b/test/daemontest_d/ut_rpc.py @@ -78,6 +78,9 @@ async def print_daemon_info(rpc): WALLETINFO: {fmt_dict(await rpc.walletinfo)} """.rstrip()) + if rpc.proto.base_proto == 'Ethereum': + msg(f' CHAIN_NAMES: {" ".join(rpc.daemon.proto.chain_names)}') + msg('') def do_msg(rpc, backend): @@ -92,7 +95,9 @@ class init_test: do_msg(rpc, backend) wi = await rpc.walletinfo - assert wi['walletname'] == cfg_override['tw_name'] + assert wi['walletname'] == cfg_override['btc_tw_name'] + assert wi['walletname'] == rpc.cfg._proto.tw_name, f'{wi["walletname"]!r} != {rpc.cfg._proto.tw_name!r}' + assert daemon.bind_port == cfg_override['btc_rpc_port'] bh = (await rpc.call('getblockchaininfo', timeout=300))['bestblockhash'] await rpc.gathered_call('getblock', ((bh,), (bh, 1)), timeout=300) @@ -112,6 +117,9 @@ class init_test: rpc = await rpc_init(cfg, daemon.proto, backend, daemon) do_msg(rpc, backend) await rpc.call('eth_blockNumber', timeout=300) + if rpc.proto.network == 'testnet': + assert daemon.proto.chain_names == cfg_override['eth_testnet_chain_names'] + assert daemon.bind_port == cfg_override['eth_rpc_port'] return rpc etc = eth @@ -166,7 +174,15 @@ class unit_tests: return await run_test( ['btc', 'btc_tn'], test_cf_auth = True, - cfg_override = {'_clone': cfg, 'tw_name': 'alternate-tracking-wallet'}) + cfg_override = { + '_clone': cfg, + 'btc_rpc_port': 19777, + 'rpc_port': 32323, # ignored + 'btc_tw_name': 'alternate-tracking-wallet', + 'tw_name': 'this-is-overridden', + 'ltc_tw_name': 'this-is-ignored', + 'eth_mainnet_chain_names': ['also', 'ignored'], + }) async def ltc(self, name, ut): return await run_test(['ltc', 'ltc_tn'], test_cf_auth=True) @@ -176,7 +192,17 @@ class unit_tests: async def geth(self, name, ut): # mainnet returns EIP-155 error on empty blockchain: - return await run_test(['eth_tn', 'eth_rt'], daemon_ids=['geth']) + return await run_test( + ['eth_tn', 'eth_rt'], + daemon_ids = ['geth'], + cfg_override = { + '_clone': cfg, + 'eth_rpc_port': 19777, + 'rpc_port': 32323, # ignored + 'btc_tw_name': 'ignored', + 'tw_name': 'also-ignored', + 'eth_testnet_chain_names': ['goerli', 'foo', 'bar', 'baz'], + }) async def erigon(self, name, ut): return await run_test(['eth', 'eth_tn', 'eth_rt'], daemon_ids=['erigon']) diff --git a/test/misc/opts_main.py b/test/misc/opts_main.py index 7484cc22..7cc65a24 100755 --- a/test/misc/opts_main.py +++ b/test/misc/opts_main.py @@ -60,6 +60,11 @@ if cfg.show_opts: col1_w = max(len(s) for s in opts) + 5 for opt in opts: msg('{:{w}} {}'.format(f'cfg.{opt}:', getattr(cfg, opt), w=col1_w)) + if cfg._proto: + coin, *rem = opt.split('_') + network = rem[0] if rem[0] in cfg._proto.network_names else None + opt_name = '_'.join(rem[bool(network):]) + msg('{:{w}} {}'.format(f'proto.{opt_name}:', getattr(cfg._proto, opt_name), w=col1_w)) msg('') for n, arg in enumerate(cfg._args, 1):