|
@@ -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):
|
|
|
-
|
|
|
- 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!!!'
|
|
|
+def handle_unsupported_daemon_version(rpc,name,warn_only):
|
|
|
|
|
|
- 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 )
|