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:
parent
1fb022d151
commit
f1844789d7
9 changed files with 470 additions and 404 deletions
236
mmgen/base_proto/bitcoin/rpc.py
Executable file
236
mmgen/base_proto/bitcoin/rpc.py
Executable 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
100
mmgen/base_proto/ethereum/rpc.py
Executable 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',
|
||||
)
|
||||
|
|
@ -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
112
mmgen/base_proto/monero/rpc.py
Executable 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')
|
||||
|
|
@ -1 +1 @@
|
|||
13.1.dev015
|
||||
13.1.dev016
|
||||
|
|
|
|||
412
mmgen/rpc.py
412
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 )
|
||||
|
|
|
|||
|
|
@ -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 = (
|
||||
|
|
|
|||
|
|
@ -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',[
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue