From f1844789d774092f8670edadc6c1b437623c0f91 Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Mon, 7 Feb 2022 13:56:21 +0000 Subject: [PATCH] modularize RPC classes - protocol-independent base classes remain in `rpc.py` - protocol-dependent subclasses are moved to `base_proto/{name}/rpc.py` --- mmgen/base_proto/bitcoin/rpc.py | 236 +++++++++++++++++ mmgen/base_proto/ethereum/rpc.py | 100 ++++++++ mmgen/base_proto/monero/daemon.py | 4 +- mmgen/base_proto/monero/rpc.py | 112 ++++++++ mmgen/data/version | 2 +- mmgen/rpc.py | 412 +----------------------------- mmgen/xmrwallet.py | 3 +- test/test_py_d/ts_xmrwallet.py | 2 +- test/unit_tests_d/ut_rpc.py | 3 +- 9 files changed, 470 insertions(+), 404 deletions(-) create mode 100755 mmgen/base_proto/bitcoin/rpc.py create mode 100755 mmgen/base_proto/ethereum/rpc.py create mode 100755 mmgen/base_proto/monero/rpc.py diff --git a/mmgen/base_proto/bitcoin/rpc.py b/mmgen/base_proto/bitcoin/rpc.py new file mode 100755 index 00000000..47517ea7 --- /dev/null +++ b/mmgen/base_proto/bitcoin/rpc.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet +# Copyright (C)2013-2022 The MMGen Project +# 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.rpc: Bitcoin base protocol RPC client class +""" + +import os + +from ...globalvars import g +from ...base_obj import AsyncInit +from ...util import ymsg,vmsg,die +from ...fileutil import get_lines_from_file +from ...rpc import RPCClient + +class CallSigs: + + class bitcoin_core: + + @classmethod + def createwallet(cls,wallet_name,no_keys=True,blank=True,passphrase='',load_on_startup=True): + """ + Quirk: when --datadir is specified (even if standard), wallet is created directly in + datadir, otherwise in datadir/wallets + """ + return ( + 'createwallet', + wallet_name, # 1. wallet_name + no_keys, # 2. disable_private_keys + blank, # 3. blank (no keys or seed) + passphrase, # 4. passphrase (empty string for non-encrypted) + False, # 5. avoid_reuse (track address reuse) + False, # 6. descriptors (native descriptor wallet) + load_on_startup # 7. load_on_startup + ) + + class litecoin_core(bitcoin_core): + + @classmethod + def createwallet(cls,wallet_name,no_keys=True,blank=True,passphrase='',load_on_startup=True): + return ( + 'createwallet', + wallet_name, # 1. wallet_name + no_keys, # 2. disable_private_keys + blank, # 3. blank (no keys or seed) + ) + + class bitcoin_cash_node(litecoin_core): + pass + +class BitcoinRPCClient(RPCClient,metaclass=AsyncInit): + + auth_type = 'basic' + has_auth_cookie = True + + async def __init__(self,proto,daemon,backend): + + self.proto = proto + self.daemon = daemon + self.call_sigs = getattr(CallSigs,daemon.id,None) + + super().__init__( + host = 'localhost' if g.test_suite else (g.rpc_host or 'localhost'), + port = daemon.rpc_port ) + + self.set_auth() # set_auth() requires cookie, so must be called after __init__() tests daemon is listening + self.set_backend(backend) # backend requires self.auth + + self.cached = {} + ( + self.cached['networkinfo'], + self.blockcount, + self.cached['blockchaininfo'], + block0 + ) = await self.gathered_call(None, ( + ('getnetworkinfo',()), + ('getblockcount',()), + ('getblockchaininfo',()), + ('getblockhash',(0,)), + )) + self.daemon_version = self.cached['networkinfo']['version'] + self.daemon_version_str = self.cached['networkinfo']['subversion'] + self.chain = self.cached['blockchaininfo']['chain'] + + tip = await self.call('getblockhash',self.blockcount) + self.cur_date = (await self.call('getblockheader',tip))['time'] + if self.chain != 'regtest': + self.chain += 'net' + assert self.chain in self.proto.networks + + async def check_chainfork_mismatch(block0): + try: + if block0 != self.proto.block0: + raise ValueError(f'Invalid Genesis block for {self.proto.cls_name} protocol') + for fork in self.proto.forks: + if fork.height == None or self.blockcount < fork.height: + break + if fork.hash != await self.call('getblockhash',fork.height): + die(3,f'Bad block hash at fork block {fork.height}. Is this the {fork.name} chain?') + except Exception as e: + die(2,'{!s}\n{c!r} requested, but this is not the {c} chain!'.format(e,c=self.proto.coin)) + + if self.chain == 'mainnet': # skip this for testnet, as Genesis block may change + await check_chainfork_mismatch(block0) + + self.caps = ('full_node',) + for func,cap in ( + ('setlabel','label_api'), + ('signrawtransactionwithkey','sign_with_key') ): + if len((await self.call('help',func)).split('\n')) > 3: + self.caps += (cap,) + + if not self.chain == 'regtest': + await self.check_tracking_wallet() + + async def check_tracking_wallet(self,wallet_checked=[]): + if not wallet_checked: + wallets = await self.call('listwallets') + if len(wallets) == 0: + wname = self.daemon.tracking_wallet_name + await self.icall('createwallet',wallet_name=wname) + ymsg(f'Created {self.daemon.coind_name} wallet {wname!r}') + elif len(wallets) > 1: # support only one loaded wallet for now + die(4,f'ERROR: more than one {self.daemon.coind_name} wallet loaded: {wallets}') + wallet_checked.append(True) + + def get_daemon_cfg_fn(self): + # Use dirname() to remove 'bob' or 'alice' component + return os.path.join( + (os.path.dirname(g.data_dir) if self.proto.regtest else self.daemon.datadir), + self.daemon.cfg_file ) + + def get_daemon_auth_cookie_fn(self): + return os.path.join(self.daemon.network_datadir,'.cookie') + + def get_daemon_cfg_options(self,req_keys): + + fn = self.get_daemon_cfg_fn() + from ...opts import opt + try: + lines = get_lines_from_file(fn,'daemon config file',silent=not opt.verbose) + except: + vmsg(f'Warning: {fn!r} does not exist or is unreadable') + return dict((k,None) for k in req_keys) + + def gen(): + for key in req_keys: + val = None + for l in lines: + if l.startswith(key): + res = l.split('=',1) + if len(res) == 2 and not ' ' in res[1].strip(): + val = res[1].strip() + yield (key,val) + + return dict(gen()) + + def get_daemon_auth_cookie(self): + fn = self.get_daemon_auth_cookie_fn() + return get_lines_from_file(fn,'cookie',quiet=True)[0] if os.access(fn,os.R_OK) else '' + + @staticmethod + def make_host_path(wallet): + return ( + '/wallet/{}'.format('bob' if g.bob else 'alice') if (g.bob or g.alice) else + '/wallet/{}'.format(wallet) if wallet else '/' + ) + + def info(self,info_id): + + def segwit_is_active(): + d = self.cached['blockchaininfo'] + if d['chain'] == 'regtest': + return True + + try: + if d['softforks']['segwit']['active'] == True: + return True + except: + pass + + try: + if d['bip9_softforks']['segwit']['status'] == 'active': + return True + except: + pass + + if g.test_suite: + return True + + return False + + return locals()[info_id]() + + rpcmethods = ( + 'backupwallet', + 'createrawtransaction', + 'decoderawtransaction', + 'disconnectnode', + 'estimatefee', + 'estimatesmartfee', + 'getaddressesbyaccount', + 'getaddressesbylabel', + 'getblock', + 'getblockchaininfo', + 'getblockcount', + 'getblockhash', + 'getblockheader', + 'getblockstats', # mmgen-node-tools + 'getmempoolinfo', + 'getmempoolentry', + 'getnettotals', + 'getnetworkinfo', + 'getpeerinfo', + 'getrawmempool', + 'getmempoolentry', + 'getrawtransaction', + 'gettransaction', + 'importaddress', + 'listaccounts', + 'listlabels', + 'listunspent', + 'setlabel', + 'sendrawtransaction', + 'signrawtransaction', + 'signrawtransactionwithkey', # method new to Core v0.17.0 + 'validateaddress', + 'walletpassphrase', + ) diff --git a/mmgen/base_proto/ethereum/rpc.py b/mmgen/base_proto/ethereum/rpc.py new file mode 100755 index 00000000..4ccaa63a --- /dev/null +++ b/mmgen/base_proto/ethereum/rpc.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet +# Copyright (C)2013-2022 The MMGen Project +# 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.rpc: Ethereum base protocol RPC client class +""" + +import re + +from ...globalvars import g +from ...base_obj import AsyncInit +from ...obj import Int +from ...util import die,oneshot_warning_group +from ...rpc import RPCClient + +class daemon_warning(oneshot_warning_group): + + class geth: + color = 'yellow' + message = 'Geth has not been tested on mainnet. You may experience problems.' + + class erigon: + color = 'red' + message = 'Erigon support is EXPERIMENTAL. Use at your own risk!!!' + +class CallSigs: + pass + +class EthereumRPCClient(RPCClient,metaclass=AsyncInit): + + async def __init__(self,proto,daemon,backend): + self.proto = proto + self.daemon = daemon + self.call_sigs = getattr(CallSigs,daemon.id,None) + + super().__init__( + host = 'localhost' if g.test_suite else (g.rpc_host or 'localhost'), + port = daemon.rpc_port ) + + self.set_backend(backend) + + vi,bh,ci = await self.gathered_call(None, ( + ('web3_clientVersion',()), + ('eth_getBlockByNumber',('latest',False)), + ('eth_chainId',()), + )) + + vip = re.match(self.daemon.version_pat,vi,re.ASCII) + if not vip: + die(2,fmt(f""" + Aborting on daemon mismatch: + Requested daemon: {self.daemon.id} + Running daemon: {vi} + """,strip_char='\t').rstrip()) + + self.daemon_version = int('{:d}{:03d}{:03d}'.format(*[int(e) for e in vip.groups()])) + self.daemon_version_str = '{}.{}.{}'.format(*vip.groups()) + self.daemon_version_info = vi + + self.blockcount = int(bh['number'],16) + self.cur_date = int(bh['timestamp'],16) + + self.caps = () + if self.daemon.id in ('parity','openethereum'): + if (await self.call('parity_nodeKind'))['capability'] == 'full': + self.caps += ('full_node',) + self.chainID = None if ci == None else Int(ci,16) # parity/oe return chainID only for dev chain + self.chain = (await self.call('parity_chain')).replace(' ','_').replace('_testnet','') + elif self.daemon.id in ('geth','erigon'): + if self.daemon.network == 'mainnet': + daemon_warning(self.daemon.id) + self.caps += ('full_node',) + self.chainID = Int(ci,16) + self.chain = self.proto.chain_ids[self.chainID] + + rpcmethods = ( + 'eth_blockNumber', + 'eth_call', + # Returns the EIP155 chain ID used for transaction signing at the current best block. + # Parity: Null is returned if not available, ID not required in transactions + # Erigon: always returns ID, requires ID in transactions + 'eth_chainId', + 'eth_gasPrice', + 'eth_getBalance', + 'eth_getCode', + 'eth_getTransactionCount', + 'eth_getTransactionReceipt', + 'eth_sendRawTransaction', + 'parity_chain', + 'parity_nodeKind', + 'parity_pendingTransactions', + 'txpool_content', + ) diff --git a/mmgen/base_proto/monero/daemon.py b/mmgen/base_proto/monero/daemon.py index a4dbe0bb..1890abae 100755 --- a/mmgen/base_proto/monero/daemon.py +++ b/mmgen/base_proto/monero/daemon.py @@ -44,7 +44,7 @@ class monero_daemon(CoinDaemon): def init_subclass(self): - from ...rpc import MoneroRPCClientRaw + from .rpc import MoneroRPCClientRaw self.rpc = MoneroRPCClientRaw( host = self.host, port = self.rpc_port, @@ -139,5 +139,5 @@ class MoneroWalletDaemon(RPCDaemon): ['--stagenet', self.network == 'testnet'], ) - from ...rpc import MoneroWalletRPCClient + from .rpc import MoneroWalletRPCClient self.rpc = MoneroWalletRPCClient( daemon=self, test_connection=False ) diff --git a/mmgen/base_proto/monero/rpc.py b/mmgen/base_proto/monero/rpc.py new file mode 100755 index 00000000..296a97b4 --- /dev/null +++ b/mmgen/base_proto/monero/rpc.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, a command-line cryptocurrency wallet +# Copyright (C)2013-2022 The MMGen Project +# 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.monero.rpc: Monero base protocol RPC client class +""" + +from ...rpc import RPCClient,IPPort,auth_data + +class MoneroRPCClient(RPCClient): + + auth_type = None + network_proto = 'https' + host_path = '/json_rpc' + verify_server = False + + def __init__(self,host,port,user,passwd,test_connection=True,proxy=None,daemon=None): + + if proxy is not None: + self.proxy = IPPort(proxy) + test_connection = False + if host.endswith('.onion'): + self.network_proto = 'http' + + super().__init__(host,port,test_connection) + + if self.auth_type: + self.auth = auth_data(user,passwd) + + if True: + self.set_backend('requests') + + else: # insecure, for debugging only + self.set_backend('curl') + self.backend.exec_opts.remove('--silent') + self.backend.exec_opts.append('--verbose') + + self.daemon = daemon + + async def call(self,method,*params,**kwargs): + assert params == (), f'{type(self).__name__}.call() accepts keyword arguments only' + return await self.process_http_resp(self.backend.run( + payload = {'id': 0, 'jsonrpc': '2.0', 'method': method, 'params': kwargs }, + timeout = 3600, # allow enough time to sync ≈1,000,000 blocks + wallet = None + )) + + rpcmethods = ( 'get_info', ) + +class MoneroRPCClientRaw(MoneroRPCClient): + + json_rpc = False + host_path = '/' + + async def call(self,method,*params,**kwargs): + assert params == (), f'{type(self).__name__}.call() accepts keyword arguments only' + return await self.process_http_resp(self.backend.run( + payload = kwargs, + timeout = self.timeout, + wallet = method + )) + + @staticmethod + def make_host_path(arg): + return arg + + async def do_stop_daemon(self,silent=False): + return await self.call('stop_daemon') + + rpcmethods = ( 'get_height', 'send_raw_transaction', 'stop_daemon' ) + +class MoneroWalletRPCClient(MoneroRPCClient): + + auth_type = 'digest' + + def __init__(self,daemon,test_connection=True): + + RPCClient.__init__( + self, + daemon.host, + daemon.rpc_port, + test_connection = test_connection ) + + self.daemon = daemon + self.auth = auth_data(daemon.user,daemon.passwd) + self.set_backend('requests') + + rpcmethods = ( + 'get_version', + 'get_height', # sync height of the open wallet + 'get_balance', # account_index=0, address_indices=[] + 'create_wallet', # filename, password, language="English" + 'open_wallet', # filename, password + 'close_wallet', + # filename,password,seed (restore_height,language,seed_offset,autosave_current) + 'restore_deterministic_wallet', + 'refresh', # start_height + ) + + async def do_stop_daemon(self,silent=False): + """ + NB: the 'stop_wallet' RPC call closes the open wallet before shutting down the daemon, + returning an error if no wallet is open + """ + return await self.call('stop_wallet') diff --git a/mmgen/data/version b/mmgen/data/version index 5e920321..30205390 100644 --- a/mmgen/data/version +++ b/mmgen/data/version @@ -1 +1 @@ -13.1.dev015 +13.1.dev016 diff --git a/mmgen/rpc.py b/mmgen/rpc.py index c2457aa8..4dda0346 100755 --- a/mmgen/rpc.py +++ b/mmgen/rpc.py @@ -20,12 +20,14 @@ rpc.py: Cryptocoin RPC library for the MMGen suite """ -import base64,json,asyncio +import base64,json,asyncio,importlib from decimal import Decimal +from collections import namedtuple + from .common import * -from .fileutil import get_lines_from_file from .objmethods import Hilite,InitErrors -from .base_obj import AsyncInit + +auth_data = namedtuple('rpc_auth_data',['user','passwd']) rpc_credentials_msg = '\n'+fmt(""" Error: no {proto_name} RPC authentication method found @@ -243,47 +245,6 @@ class RPCBackends: # res = run(exec_cmd,stdout=PIPE,check=True,text='UTF-8').stdout # Python 3.7+ return (res[:-3],int(res[-3:])) -from collections import namedtuple -auth_data = namedtuple('rpc_auth_data',['user','passwd']) - -class CallSigs: - - class Bitcoin: - - class bitcoin_core: - - @classmethod - def createwallet(cls,wallet_name,no_keys=True,blank=True,passphrase='',load_on_startup=True): - """ - Quirk: when --datadir is specified (even if standard), wallet is created directly in - datadir, otherwise in datadir/wallets - """ - return ( - 'createwallet', - wallet_name, # 1. wallet_name - no_keys, # 2. disable_private_keys - blank, # 3. blank (no keys or seed) - passphrase, # 4. passphrase (empty string for non-encrypted) - False, # 5. avoid_reuse (track address reuse) - False, # 6. descriptors (native descriptor wallet) - load_on_startup # 7. load_on_startup - ) - - class litecoin_core(bitcoin_core): - - @classmethod - def createwallet(cls,wallet_name,no_keys=True,blank=True,passphrase='',load_on_startup=True): - return ( - 'createwallet', - wallet_name, # 1. wallet_name - no_keys, # 2. disable_private_keys - blank, # 3. blank (no keys or seed) - ) - - class bitcoin_cash_node(litecoin_core): pass - - class Ethereum: pass - class RPCClient(MMGenObject): json_rpc = True @@ -463,361 +424,14 @@ class RPCClient(MMGenObject): await self.stop_daemon(quiet=quiet,silent=silent) return self.daemon.start(silent=silent) -class BitcoinRPCClient(RPCClient,metaclass=AsyncInit): +def handle_unsupported_daemon_version(rpc,name,warn_only): - auth_type = 'basic' - has_auth_cookie = True - - async def __init__(self,proto,daemon,backend): - - self.proto = proto - self.daemon = daemon - self.call_sigs = getattr(getattr(CallSigs,proto.base_proto),daemon.id,None) - - super().__init__( - host = 'localhost' if g.test_suite else (g.rpc_host or 'localhost'), - port = daemon.rpc_port ) - - self.set_auth() # set_auth() requires cookie, so must be called after __init__() tests daemon is listening - self.set_backend(backend) # backend requires self.auth - - self.cached = {} - ( - self.cached['networkinfo'], - self.blockcount, - self.cached['blockchaininfo'], - block0 - ) = await self.gathered_call(None, ( - ('getnetworkinfo',()), - ('getblockcount',()), - ('getblockchaininfo',()), - ('getblockhash',(0,)), - )) - self.daemon_version = self.cached['networkinfo']['version'] - self.daemon_version_str = self.cached['networkinfo']['subversion'] - self.chain = self.cached['blockchaininfo']['chain'] - - tip = await self.call('getblockhash',self.blockcount) - self.cur_date = (await self.call('getblockheader',tip))['time'] - if self.chain != 'regtest': - self.chain += 'net' - assert self.chain in self.proto.networks - - async def check_chainfork_mismatch(block0): - try: - if block0 != self.proto.block0: - raise ValueError(f'Invalid Genesis block for {self.proto.cls_name} protocol') - for fork in self.proto.forks: - if fork.height == None or self.blockcount < fork.height: - break - if fork.hash != await self.call('getblockhash',fork.height): - die(3,f'Bad block hash at fork block {fork.height}. Is this the {fork.name} chain?') - except Exception as e: - die(2,'{!s}\n{c!r} requested, but this is not the {c} chain!'.format(e,c=self.proto.coin)) - - if self.chain == 'mainnet': # skip this for testnet, as Genesis block may change - await check_chainfork_mismatch(block0) - - self.caps = ('full_node',) - for func,cap in ( - ('setlabel','label_api'), - ('signrawtransactionwithkey','sign_with_key') ): - if len((await self.call('help',func)).split('\n')) > 3: - self.caps += (cap,) - - if not self.chain == 'regtest': - await self.check_tracking_wallet() - - async def check_tracking_wallet(self,wallet_checked=[]): - if not wallet_checked: - wallets = await self.call('listwallets') - if len(wallets) == 0: - wname = self.daemon.tracking_wallet_name - await self.icall('createwallet',wallet_name=wname) - ymsg(f'Created {self.daemon.coind_name} wallet {wname!r}') - elif len(wallets) > 1: # support only one loaded wallet for now - die(4,f'ERROR: more than one {self.daemon.coind_name} wallet loaded: {wallets}') - wallet_checked.append(True) - - def get_daemon_cfg_fn(self): - # Use dirname() to remove 'bob' or 'alice' component - return os.path.join( - (os.path.dirname(g.data_dir) if self.proto.regtest else self.daemon.datadir), - self.daemon.cfg_file ) - - def get_daemon_auth_cookie_fn(self): - return os.path.join(self.daemon.network_datadir,'.cookie') - - def get_daemon_cfg_options(self,req_keys): - - fn = self.get_daemon_cfg_fn() - try: - lines = get_lines_from_file(fn,'daemon config file',silent=not opt.verbose) - except: - vmsg(f'Warning: {fn!r} does not exist or is unreadable') - return dict((k,None) for k in req_keys) - - def gen(): - for key in req_keys: - val = None - for l in lines: - if l.startswith(key): - res = l.split('=',1) - if len(res) == 2 and not ' ' in res[1].strip(): - val = res[1].strip() - yield (key,val) - - return dict(gen()) - - def get_daemon_auth_cookie(self): - fn = self.get_daemon_auth_cookie_fn() - return get_lines_from_file(fn,'cookie',quiet=True)[0] if os.access(fn,os.R_OK) else '' - - @staticmethod - def make_host_path(wallet): - return ( - '/wallet/{}'.format('bob' if g.bob else 'alice') if (g.bob or g.alice) else - '/wallet/{}'.format(wallet) if wallet else '/' - ) - - def info(self,info_id): - - def segwit_is_active(): - d = self.cached['blockchaininfo'] - if d['chain'] == 'regtest': - return True - - try: - if d['softforks']['segwit']['active'] == True: - return True - except: - pass - - try: - if d['bip9_softforks']['segwit']['status'] == 'active': - return True - except: - pass - - if g.test_suite: - return True - - return False - - return locals()[info_id]() - - rpcmethods = ( - 'backupwallet', - 'createrawtransaction', - 'decoderawtransaction', - 'disconnectnode', - 'estimatefee', - 'estimatesmartfee', - 'getaddressesbyaccount', - 'getaddressesbylabel', - 'getblock', - 'getblockchaininfo', - 'getblockcount', - 'getblockhash', - 'getblockheader', - 'getblockstats', # mmgen-node-tools - 'getmempoolinfo', - 'getmempoolentry', - 'getnettotals', - 'getnetworkinfo', - 'getpeerinfo', - 'getrawmempool', - 'getmempoolentry', - 'getrawtransaction', - 'gettransaction', - 'importaddress', - 'listaccounts', - 'listlabels', - 'listunspent', - 'setlabel', - 'sendrawtransaction', - 'signrawtransaction', - 'signrawtransactionwithkey', # method new to Core v0.17.0 - 'validateaddress', - 'walletpassphrase', - ) - -class EthereumRPCClient(RPCClient,metaclass=AsyncInit): - - async def __init__(self,proto,daemon,backend): - self.proto = proto - self.daemon = daemon - self.call_sigs = getattr(getattr(CallSigs,proto.base_proto),daemon.id,None) - - super().__init__( - host = 'localhost' if g.test_suite else (g.rpc_host or 'localhost'), - port = daemon.rpc_port ) - - self.set_backend(backend) - - vi,bh,ci = await self.gathered_call(None, ( - ('web3_clientVersion',()), - ('eth_getBlockByNumber',('latest',False)), - ('eth_chainId',()), - )) - - import re - vip = re.match(self.daemon.version_pat,vi,re.ASCII) - if not vip: - die(2,fmt(f""" - Aborting on daemon mismatch: - Requested daemon: {self.daemon.id} - Running daemon: {vi} - """,strip_char='\t').rstrip()) - - self.daemon_version = int('{:d}{:03d}{:03d}'.format(*[int(e) for e in vip.groups()])) - self.daemon_version_str = '{}.{}.{}'.format(*vip.groups()) - self.daemon_version_info = vi - - self.blockcount = int(bh['number'],16) - self.cur_date = int(bh['timestamp'],16) - - self.caps = () - from .obj import Int - if self.daemon.id in ('parity','openethereum'): - if (await self.call('parity_nodeKind'))['capability'] == 'full': - self.caps += ('full_node',) - self.chainID = None if ci == None else Int(ci,16) # parity/oe return chainID only for dev chain - self.chain = (await self.call('parity_chain')).replace(' ','_').replace('_testnet','') - elif self.daemon.id in ('geth','erigon'): - if self.daemon.network == 'mainnet': - daemon_warning(self.daemon.id) - self.caps += ('full_node',) - self.chainID = Int(ci,16) - self.chain = self.proto.chain_ids[self.chainID] - - rpcmethods = ( - 'eth_blockNumber', - 'eth_call', - # Returns the EIP155 chain ID used for transaction signing at the current best block. - # Parity: Null is returned if not available, ID not required in transactions - # Erigon: always returns ID, requires ID in transactions - 'eth_chainId', - 'eth_gasPrice', - 'eth_getBalance', - 'eth_getCode', - 'eth_getTransactionCount', - 'eth_getTransactionReceipt', - 'eth_sendRawTransaction', - 'parity_chain', - 'parity_nodeKind', - 'parity_pendingTransactions', - 'txpool_content', - ) - -class MoneroRPCClient(RPCClient): - - auth_type = None - network_proto = 'https' - host_path = '/json_rpc' - verify_server = False - - def __init__(self,host,port,user,passwd,test_connection=True,proxy=None,daemon=None): - if proxy is not None: - self.proxy = IPPort(proxy) - test_connection = False - if host.endswith('.onion'): - self.network_proto = 'http' - super().__init__(host,port,test_connection) - if self.auth_type: - self.auth = auth_data(user,passwd) - if True: - self.set_backend('requests') - else: # insecure, for debugging only - self.set_backend('curl') - self.backend.exec_opts.remove('--silent') - self.backend.exec_opts.append('--verbose') - self.daemon = daemon - - async def call(self,method,*params,**kwargs): - assert params == (), f'{type(self).__name__}.call() accepts keyword arguments only' - return await self.process_http_resp(self.backend.run( - payload = {'id': 0, 'jsonrpc': '2.0', 'method': method, 'params': kwargs }, - timeout = 3600, # allow enough time to sync ≈1,000,000 blocks - wallet = None - )) - - rpcmethods = ( 'get_info', ) - -class MoneroRPCClientRaw(MoneroRPCClient): - - json_rpc = False - host_path = '/' - - async def call(self,method,*params,**kwargs): - assert params == (), f'{type(self).__name__}.call() accepts keyword arguments only' - return await self.process_http_resp(self.backend.run( - payload = kwargs, - timeout = self.timeout, - wallet = method - )) - - @staticmethod - def make_host_path(arg): - return arg - - async def do_stop_daemon(self,silent=False): - return await self.call('stop_daemon') - - rpcmethods = ( 'get_height', 'send_raw_transaction', 'stop_daemon' ) - -class MoneroWalletRPCClient(MoneroRPCClient): - - auth_type = 'digest' - - def __init__(self,daemon,test_connection=True): - - RPCClient.__init__( - self, - daemon.host, - daemon.rpc_port, - test_connection = test_connection ) - - self.daemon = daemon - self.auth = auth_data(daemon.user,daemon.passwd) - self.set_backend('requests') - - rpcmethods = ( - 'get_version', - 'get_height', # sync height of the open wallet - 'get_balance', # account_index=0, address_indices=[] - 'create_wallet', # filename, password, language="English" - 'open_wallet', # filename, password - 'close_wallet', - # filename,password,seed (restore_height,language,seed_offset,autosave_current) - 'restore_deterministic_wallet', - 'refresh', # start_height - ) - - async def do_stop_daemon(self,silent=False): - """ - NB: the 'stop_wallet' RPC call closes the open wallet before shutting down the daemon, - returning an error if no wallet is open - """ - return await self.call('stop_wallet') - -class daemon_warning(oneshot_warning_group): - - class geth: - color = 'yellow' - message = 'Geth has not been tested on mainnet. You may experience problems.' - - class erigon: - color = 'red' - message = 'Erigon support is EXPERIMENTAL. Use at your own risk!!!' - - class version: + class daemon_version_warning(oneshot_warning): color = 'yellow' message = 'ignoring unsupported {} daemon version at user request' -def handle_unsupported_daemon_version(rpc,name,warn_only): if warn_only: - daemon_warning('version',div=name,fmt_args=[rpc.daemon.coind_name]) + daemon_version_warning(div=name,fmt_args=[rpc.daemon.coind_name]) else: name = rpc.daemon.coind_name die(2,'\n'+fmt(f""" @@ -835,11 +449,13 @@ async def rpc_init(proto,backend=None,daemon=None,ignore_daemon_version=False): if not 'rpc' in proto.mmcaps: die(1,f'Coin daemon operations not supported for {proto.name} protocol!') + + cls = getattr( + importlib.import_module(f'mmgen.base_proto.{proto.base_proto.lower()}.rpc'), + proto.base_proto + 'RPCClient' ) + from .daemon import CoinDaemon - rpc = await { - 'Bitcoin': BitcoinRPCClient, - 'Ethereum': EthereumRPCClient, - }[proto.base_proto]( + rpc = await cls( proto = proto, daemon = daemon or CoinDaemon(proto=proto,test_suite=g.test_suite), backend = backend or opt.rpc_backend ) diff --git a/mmgen/xmrwallet.py b/mmgen/xmrwallet.py index ba4372fb..13b66423 100755 --- a/mmgen/xmrwallet.py +++ b/mmgen/xmrwallet.py @@ -30,7 +30,8 @@ from .protocol import init_proto from .proto.common import b58a from .addr import CoinAddr,AddrIdx from .addrlist import KeyAddrList,AddrIdxList -from .rpc import MoneroRPCClientRaw,MoneroWalletRPCClient,json_encoder +from .rpc import json_encoder +from .base_proto.monero.rpc import MoneroRPCClientRaw,MoneroWalletRPCClient from .base_proto.monero.daemon import MoneroWalletDaemon xmrwallet_uarg_info = ( diff --git a/test/test_py_d/ts_xmrwallet.py b/test/test_py_d/ts_xmrwallet.py index 2eb39a46..1d8cb045 100755 --- a/test/test_py_d/ts_xmrwallet.py +++ b/test/test_py_d/ts_xmrwallet.py @@ -185,7 +185,7 @@ class TestSuiteXMRWallet(TestSuiteBase): def init_users(self): from mmgen.daemon import CoinDaemon from mmgen.base_proto.monero.daemon import MoneroWalletDaemon - from mmgen.rpc import MoneroRPCClient,MoneroRPCClientRaw,MoneroWalletRPCClient + from mmgen.base_proto.monero.rpc import MoneroRPCClient,MoneroRPCClientRaw,MoneroWalletRPCClient self.users = {} n = self.tmpdir_nums[0] ud = namedtuple('user_data',[ diff --git a/test/unit_tests_d/ut_rpc.py b/test/unit_tests_d/ut_rpc.py index a1c6eca4..ceef3caf 100755 --- a/test/unit_tests_d/ut_rpc.py +++ b/test/unit_tests_d/ut_rpc.py @@ -6,8 +6,9 @@ test.unit_tests_d.ut_rpc: RPC unit test for the MMGen suite from mmgen.common import * from mmgen.protocol import init_proto -from mmgen.rpc import rpc_init,MoneroWalletRPCClient +from mmgen.rpc import rpc_init from mmgen.daemon import CoinDaemon +from mmgen.base_proto.monero.rpc import MoneroWalletRPCClient from mmgen.base_proto.monero.daemon import MoneroWalletDaemon def cfg_file_auth_test(proto,d):