modularize RPC classes

- protocol-independent base classes remain in `rpc.py`
- protocol-dependent subclasses are moved to `base_proto/{name}/rpc.py`
This commit is contained in:
The MMGen Project 2022-02-07 13:56:21 +00:00
commit f1844789d7
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
9 changed files with 470 additions and 404 deletions

236
mmgen/base_proto/bitcoin/rpc.py Executable file
View file

@ -0,0 +1,236 @@
#!/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.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',
)

100
mmgen/base_proto/ethereum/rpc.py Executable file
View file

@ -0,0 +1,100 @@
#!/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.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',
)

View file

@ -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 )

112
mmgen/base_proto/monero/rpc.py Executable file
View file

@ -0,0 +1,112 @@
#!/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.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')

View file

@ -1 +1 @@
13.1.dev015
13.1.dev016

View file

@ -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 )

View file

@ -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 = (

View file

@ -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',[

View file

@ -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):