rpc.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2022 The MMGen Project <mmgen@tuta.io>
  5. #
  6. # This program is free software: you can redistribute it and/or modify
  7. # it under the terms of the GNU General Public License as published by
  8. # the Free Software Foundation, either version 3 of the License, or
  9. # (at your option) any later version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. """
  19. rpc.py: Cryptocoin RPC library for the MMGen suite
  20. """
  21. import base64,json,asyncio
  22. from decimal import Decimal
  23. from .common import *
  24. from .fileutil import get_lines_from_file
  25. from .objmethods import Hilite,InitErrors
  26. from .base_obj import AsyncInit
  27. rpc_credentials_msg = '\n'+fmt("""
  28. Error: no {proto_name} RPC authentication method found
  29. RPC credentials must be supplied using one of the following methods:
  30. A) If daemon is local and running as same user as you:
  31. - no credentials required, or matching rpcuser/rpcpassword and
  32. rpc_user/rpc_password values in {cf_name}.conf and mmgen.cfg
  33. B) If daemon is running remotely or as different user:
  34. - matching credentials in {cf_name}.conf and mmgen.cfg as described above
  35. The --rpc-user/--rpc-password options may be supplied on the MMGen command line.
  36. They override the corresponding values in mmgen.cfg. Set them to an empty string
  37. to use cookie authentication with a local server when the options are set
  38. in mmgen.cfg.
  39. For better security, rpcauth should be used in {cf_name}.conf instead of
  40. rpcuser/rpcpassword.
  41. """,strip_char='\t')
  42. def dmsg_rpc(fs,data=None,is_json=False):
  43. if g.debug_rpc:
  44. msg(
  45. fs if data == None else
  46. fs.format(pp_fmt(json.loads(data) if is_json else data))
  47. )
  48. class IPPort(str,Hilite,InitErrors):
  49. color = 'yellow'
  50. width = 0
  51. trunc_ok = False
  52. min_len = 9 # 0.0.0.0:0
  53. max_len = 21 # 255.255.255.255:65535
  54. def __new__(cls,s):
  55. if type(s) == cls:
  56. return s
  57. try:
  58. m = re.fullmatch(r'{q}\.{q}\.{q}\.{q}:(\d{{1,10}})'.format(q=r'([0-9]{1,3})'),s)
  59. assert m is not None, f'{s!r}: invalid IP:HOST specifier'
  60. for e in m.groups():
  61. if len(e) != 1 and e[0] == '0':
  62. raise ValueError(f'{e}: leading zeroes not permitted in dotted decimal element or port number')
  63. res = [int(e) for e in m.groups()]
  64. for e in res[:4]:
  65. assert e <= 255, f'{e}: dotted decimal element > 255'
  66. assert res[4] <= 65535, f'{res[4]}: port number > 65535'
  67. me = str.__new__(cls,s)
  68. me.ip = '{}.{}.{}.{}'.format(*res)
  69. me.ip_num = sum( res[i] * ( 2 ** (-(i-3)*8) ) for i in range(4) )
  70. me.port = res[4]
  71. return me
  72. except Exception as e:
  73. return cls.init_fail(e,s)
  74. class json_encoder(json.JSONEncoder):
  75. def default(self,obj):
  76. if isinstance(obj,Decimal):
  77. return str(obj)
  78. else:
  79. return json.JSONEncoder.default(self,obj)
  80. class RPCBackends:
  81. class base:
  82. def __init__(self,caller):
  83. self.host = caller.host
  84. self.port = caller.port
  85. self.proxy = caller.proxy
  86. self.url = caller.url
  87. self.timeout = caller.timeout
  88. self.http_hdrs = caller.http_hdrs
  89. self.make_host_path = caller.make_host_path
  90. class aiohttp(base):
  91. def __init__(self,caller):
  92. super().__init__(caller)
  93. self.session = g.session
  94. if caller.auth_type == 'basic':
  95. import aiohttp
  96. self.auth = aiohttp.BasicAuth(*caller.auth,encoding='UTF-8')
  97. else:
  98. self.auth = None
  99. async def run(self,payload,timeout,wallet):
  100. dmsg_rpc('\n RPC PAYLOAD data (aiohttp) ==>\n{}\n',payload)
  101. async with self.session.post(
  102. url = self.url + self.make_host_path(wallet),
  103. auth = self.auth,
  104. data = json.dumps(payload,cls=json_encoder),
  105. timeout = timeout or self.timeout,
  106. ) as res:
  107. return (await res.text(),res.status)
  108. class requests(base):
  109. def __del__(self):
  110. self.session.close()
  111. def __init__(self,caller):
  112. super().__init__(caller)
  113. import requests,urllib3
  114. urllib3.disable_warnings()
  115. self.session = requests.Session()
  116. self.session.headers = caller.http_hdrs
  117. if caller.auth_type:
  118. auth = 'HTTP' + caller.auth_type.capitalize() + 'Auth'
  119. self.session.auth = getattr(requests.auth,auth)(*caller.auth)
  120. if self.proxy:
  121. self.session.proxies.update({
  122. 'http': f'socks5h://{self.proxy}',
  123. 'https': f'socks5h://{self.proxy}'
  124. })
  125. async def run(self,payload,timeout,wallet):
  126. dmsg_rpc('\n RPC PAYLOAD data (requests) ==>\n{}\n',payload)
  127. res = self.session.post(
  128. url = self.url + self.make_host_path(wallet),
  129. data = json.dumps(payload,cls=json_encoder),
  130. timeout = timeout or self.timeout,
  131. verify = False )
  132. return (res.content,res.status_code)
  133. class httplib(base):
  134. def __del__(self):
  135. self.session.close()
  136. def __init__(self,caller):
  137. super().__init__(caller)
  138. import http.client
  139. self.session = http.client.HTTPConnection(caller.host,caller.port,caller.timeout)
  140. if caller.auth_type == 'basic':
  141. auth_str = f'{caller.auth.user}:{caller.auth.passwd}'
  142. auth_str_b64 = 'Basic ' + base64.b64encode(auth_str.encode()).decode()
  143. self.http_hdrs.update({ 'Host': self.host, 'Authorization': auth_str_b64 })
  144. dmsg_rpc(' RPC AUTHORIZATION data ==> raw: [{}]\n{:>31}enc: [{}]\n'.format(
  145. auth_str,
  146. '',
  147. auth_str_b64 ))
  148. async def run(self,payload,timeout,wallet):
  149. dmsg_rpc('\n RPC PAYLOAD data (httplib) ==>\n{}\n',payload)
  150. if timeout:
  151. import http.client
  152. s = http.client.HTTPConnection(self.host,self.port,timeout)
  153. else:
  154. s = self.session
  155. try:
  156. s.request(
  157. method = 'POST',
  158. url = self.make_host_path(wallet),
  159. body = json.dumps(payload,cls=json_encoder),
  160. headers = self.http_hdrs )
  161. r = s.getresponse() # => http.client.HTTPResponse instance
  162. except Exception as e:
  163. die( 'RPCFailure', str(e) )
  164. if timeout:
  165. ret = ( r.read(), r.status )
  166. s.close()
  167. return ret
  168. else:
  169. return ( r.read(), r.status )
  170. class curl(base):
  171. def __init__(self,caller):
  172. def gen_opts():
  173. for k,v in caller.http_hdrs.items():
  174. for s in ('--header',f'{k}: {v}'):
  175. yield s
  176. if caller.auth_type:
  177. """
  178. Authentication with curl is insecure, as it exposes the user's credentials
  179. via the command line. Use for testing only.
  180. """
  181. for s in ('--user',f'{caller.auth.user}:{caller.auth.passwd}'):
  182. yield s
  183. if caller.auth_type == 'digest':
  184. yield '--digest'
  185. if caller.network_proto == 'https' and caller.verify_server == False:
  186. yield '--insecure'
  187. super().__init__(caller)
  188. self.exec_opts = list(gen_opts()) + ['--silent']
  189. self.arg_max = 8192 # set way below system ARG_MAX, just to be safe
  190. async def run(self,payload,timeout,wallet):
  191. data = json.dumps(payload,cls=json_encoder)
  192. if len(data) > self.arg_max:
  193. return self.httplib(payload,timeout=timeout)
  194. dmsg_rpc('\n RPC PAYLOAD data (curl) ==>\n{}\n',payload)
  195. exec_cmd = [
  196. 'curl',
  197. '--proxy', f'socks5h://{self.proxy}' if self.proxy else '',
  198. '--connect-timeout', str(timeout or self.timeout),
  199. '--write-out', '%{http_code}',
  200. '--data-binary', data
  201. ] + self.exec_opts + [self.url + self.make_host_path(wallet)]
  202. dmsg_rpc(' RPC curl exec data ==>\n{}\n',exec_cmd)
  203. from subprocess import run,PIPE
  204. res = run(exec_cmd,stdout=PIPE,check=True).stdout.decode()
  205. # res = run(exec_cmd,stdout=PIPE,check=True,text='UTF-8').stdout # Python 3.7+
  206. return (res[:-3],int(res[-3:]))
  207. from collections import namedtuple
  208. auth_data = namedtuple('rpc_auth_data',['user','passwd'])
  209. class CallSigs:
  210. class Bitcoin:
  211. class bitcoin_core:
  212. @classmethod
  213. def createwallet(cls,wallet_name,no_keys=True,blank=True,passphrase='',load_on_startup=True):
  214. """
  215. Quirk: when --datadir is specified (even if standard), wallet is created directly in
  216. datadir, otherwise in datadir/wallets
  217. """
  218. return (
  219. 'createwallet',
  220. wallet_name, # 1. wallet_name
  221. no_keys, # 2. disable_private_keys
  222. blank, # 3. blank (no keys or seed)
  223. passphrase, # 4. passphrase (empty string for non-encrypted)
  224. False, # 5. avoid_reuse (track address reuse)
  225. False, # 6. descriptors (native descriptor wallet)
  226. load_on_startup # 7. load_on_startup
  227. )
  228. class litecoin_core(bitcoin_core):
  229. @classmethod
  230. def createwallet(cls,wallet_name,no_keys=True,blank=True,passphrase='',load_on_startup=True):
  231. return (
  232. 'createwallet',
  233. wallet_name, # 1. wallet_name
  234. no_keys, # 2. disable_private_keys
  235. blank, # 3. blank (no keys or seed)
  236. )
  237. class bitcoin_cash_node(litecoin_core): pass
  238. class Ethereum: pass
  239. class RPCClient(MMGenObject):
  240. json_rpc = True
  241. auth_type = None
  242. has_auth_cookie = False
  243. network_proto = 'http'
  244. host_path = ''
  245. proxy = None
  246. def __init__(self,host,port,test_connection=True):
  247. dmsg_rpc(f'=== {type(self).__name__}.__init__() debug ===')
  248. dmsg_rpc(f' cls [{type(self).__name__}] host [{host}] port [{port}]\n')
  249. if test_connection:
  250. import socket
  251. try:
  252. socket.create_connection((host,port),timeout=1).close()
  253. except:
  254. die( 'SocketError', f'Unable to connect to {host}:{port}' )
  255. self.http_hdrs = { 'Content-Type': 'application/json' }
  256. self.url = f'{self.network_proto}://{host}:{port}{self.host_path}'
  257. self.host = host
  258. self.port = port
  259. self.timeout = g.http_timeout
  260. self.auth = None
  261. @staticmethod
  262. def make_host_path(foo):
  263. return ''
  264. def set_backend(self,backend=None):
  265. bn = backend or opt.rpc_backend
  266. if bn == 'auto':
  267. self.backend = {'linux':RPCBackends.httplib,'win':RPCBackends.curl}[g.platform](self)
  268. else:
  269. self.backend = getattr(RPCBackends,bn)(self)
  270. def set_auth(self):
  271. """
  272. MMGen's credentials override coin daemon's
  273. """
  274. if g.rpc_user:
  275. user,passwd = (g.rpc_user,g.rpc_password)
  276. else:
  277. user,passwd = self.get_daemon_cfg_options(('rpcuser','rpcpassword')).values()
  278. if not (user and passwd):
  279. user,passwd = (self.daemon.rpc_user,self.daemon.rpc_password)
  280. if user and passwd:
  281. self.auth = auth_data(user,passwd)
  282. return
  283. if self.has_auth_cookie:
  284. cookie = self.get_daemon_auth_cookie()
  285. if cookie:
  286. self.auth = auth_data(*cookie.split(':'))
  287. return
  288. die(1,rpc_credentials_msg.format(
  289. proto_name = self.proto.name,
  290. cf_name = (self.proto.is_fork_of or self.proto.name).lower(),
  291. ))
  292. # Call family of methods - direct-to-daemon RPC call:
  293. # positional params are passed to the daemon, 'timeout' and 'wallet' kwargs to the backend
  294. async def call(self,method,*params,timeout=None,wallet=None):
  295. """
  296. default call: call with param list unrolled, exactly as with cli
  297. """
  298. return await self.process_http_resp(self.backend.run(
  299. payload = {'id': 1, 'jsonrpc': '2.0', 'method': method, 'params': params },
  300. timeout = timeout,
  301. wallet = wallet
  302. ))
  303. async def batch_call(self,method,param_list,timeout=None,wallet=None):
  304. """
  305. Make a single call with a list of tuples as first argument
  306. For RPC calls that return a list of results
  307. """
  308. return await self.process_http_resp(self.backend.run(
  309. payload = [{
  310. 'id': n,
  311. 'jsonrpc': '2.0',
  312. 'method': method,
  313. 'params': params } for n,params in enumerate(param_list,1) ],
  314. timeout = timeout,
  315. wallet = wallet
  316. ),batch=True)
  317. async def gathered_call(self,method,args_list,timeout=None,wallet=None):
  318. """
  319. Perform multiple RPC calls, returning results in a list
  320. Can be called two ways:
  321. 1) method = methodname, args_list = [args_tuple1, args_tuple2,...]
  322. 2) method = None, args_list = [(methodname1,args_tuple1), (methodname2,args_tuple2), ...]
  323. """
  324. cmd_list = args_list if method == None else tuple(zip([method] * len(args_list), args_list))
  325. cur_pos = 0
  326. chunk_size = 1024
  327. ret = []
  328. while cur_pos < len(cmd_list):
  329. tasks = [self.process_http_resp(self.backend.run(
  330. payload = {'id': n, 'jsonrpc': '2.0', 'method': method, 'params': params },
  331. timeout = timeout,
  332. wallet = wallet
  333. )) for n,(method,params) in enumerate(cmd_list[cur_pos:chunk_size+cur_pos],1)]
  334. ret.extend(await asyncio.gather(*tasks))
  335. cur_pos += chunk_size
  336. return ret
  337. # Icall family of methods - indirect RPC call using CallSigs mechanism:
  338. # - 'timeout' and 'wallet' kwargs are passed to corresponding Call method
  339. # - remaining kwargs are passed to CallSigs method
  340. # - CallSigs method returns method and positional params for Call method
  341. def icall(self,method,**kwargs):
  342. timeout = kwargs.pop('timeout',None)
  343. wallet = kwargs.pop('wallet',None)
  344. return self.call(
  345. *getattr(self.call_sigs,method)(**kwargs),
  346. timeout = timeout,
  347. wallet = wallet )
  348. async def process_http_resp(self,coro,batch=False):
  349. text,status = await coro
  350. if status == 200:
  351. dmsg_rpc(' RPC RESPONSE data ==>\n{}\n',text,is_json=True)
  352. if batch:
  353. return [r['result'] for r in json.loads(text,parse_float=Decimal)]
  354. else:
  355. try:
  356. if self.json_rpc:
  357. return json.loads(text,parse_float=Decimal)['result']
  358. else:
  359. return json.loads(text,parse_float=Decimal)
  360. except:
  361. t = json.loads(text)
  362. try:
  363. m = t['error']['message']
  364. except:
  365. try: m = t['error']
  366. except: m = t
  367. die( 'RPCFailure', m )
  368. else:
  369. import http
  370. m,s = ( '', http.HTTPStatus(status) )
  371. if text:
  372. try:
  373. m = json.loads(text)['error']['message']
  374. except:
  375. try: m = text.decode()
  376. except: m = text
  377. die( 'RPCFailure', f'{s.value} {s.name}: {m}' )
  378. async def stop_daemon(self,quiet=False,silent=False):
  379. if self.daemon.state == 'ready':
  380. if not (quiet or silent):
  381. msg(f'Stopping {self.daemon.desc} on port {self.daemon.bind_port}')
  382. ret = await self.do_stop_daemon(silent=silent)
  383. if self.daemon.wait:
  384. self.daemon.wait_for_state('stopped')
  385. return ret
  386. else:
  387. if not (quiet or silent):
  388. msg(f'{self.daemon.desc} on port {self.daemon.bind_port} not running')
  389. return True
  390. async def restart_daemon(self,quiet=False,silent=False):
  391. await self.stop_daemon(quiet=quiet,silent=silent)
  392. return self.daemon.start(silent=silent)
  393. class BitcoinRPCClient(RPCClient,metaclass=AsyncInit):
  394. auth_type = 'basic'
  395. has_auth_cookie = True
  396. async def __init__(self,proto,daemon,backend):
  397. self.proto = proto
  398. self.daemon = daemon
  399. self.call_sigs = getattr(getattr(CallSigs,proto.base_proto),daemon.id,None)
  400. super().__init__(
  401. host = 'localhost' if g.test_suite else (g.rpc_host or 'localhost'),
  402. port = daemon.rpc_port )
  403. self.set_auth() # set_auth() requires cookie, so must be called after __init__() tests daemon is listening
  404. self.set_backend(backend) # backend requires self.auth
  405. self.cached = {}
  406. (
  407. self.cached['networkinfo'],
  408. self.blockcount,
  409. self.cached['blockchaininfo'],
  410. block0
  411. ) = await self.gathered_call(None, (
  412. ('getnetworkinfo',()),
  413. ('getblockcount',()),
  414. ('getblockchaininfo',()),
  415. ('getblockhash',(0,)),
  416. ))
  417. self.daemon_version = self.cached['networkinfo']['version']
  418. self.daemon_version_str = self.cached['networkinfo']['subversion']
  419. self.chain = self.cached['blockchaininfo']['chain']
  420. tip = await self.call('getblockhash',self.blockcount)
  421. self.cur_date = (await self.call('getblockheader',tip))['time']
  422. if self.chain != 'regtest':
  423. self.chain += 'net'
  424. assert self.chain in self.proto.networks
  425. async def check_chainfork_mismatch(block0):
  426. try:
  427. if block0 != self.proto.block0:
  428. raise ValueError(f'Invalid Genesis block for {self.proto.cls_name} protocol')
  429. for fork in self.proto.forks:
  430. if fork.height == None or self.blockcount < fork.height:
  431. break
  432. if fork.hash != await self.call('getblockhash',fork.height):
  433. die(3,f'Bad block hash at fork block {fork.height}. Is this the {fork.name} chain?')
  434. except Exception as e:
  435. die(2,'{!s}\n{c!r} requested, but this is not the {c} chain!'.format(e,c=self.proto.coin))
  436. if self.chain == 'mainnet': # skip this for testnet, as Genesis block may change
  437. await check_chainfork_mismatch(block0)
  438. self.caps = ('full_node',)
  439. for func,cap in (
  440. ('setlabel','label_api'),
  441. ('signrawtransactionwithkey','sign_with_key') ):
  442. if len((await self.call('help',func)).split('\n')) > 3:
  443. self.caps += (cap,)
  444. if not self.chain == 'regtest':
  445. await self.check_tracking_wallet()
  446. async def check_tracking_wallet(self,wallet_checked=[]):
  447. if not wallet_checked:
  448. wallets = await self.call('listwallets')
  449. if len(wallets) == 0:
  450. wname = self.daemon.tracking_wallet_name
  451. await self.icall('createwallet',wallet_name=wname)
  452. ymsg(f'Created {self.daemon.coind_name} wallet {wname!r}')
  453. elif len(wallets) > 1: # support only one loaded wallet for now
  454. die(4,f'ERROR: more than one {self.daemon.coind_name} wallet loaded: {wallets}')
  455. wallet_checked.append(True)
  456. def get_daemon_cfg_fn(self):
  457. # Use dirname() to remove 'bob' or 'alice' component
  458. return os.path.join(
  459. (os.path.dirname(g.data_dir) if self.proto.regtest else self.daemon.datadir),
  460. self.daemon.cfg_file )
  461. def get_daemon_auth_cookie_fn(self):
  462. return os.path.join(self.daemon.network_datadir,'.cookie')
  463. def get_daemon_cfg_options(self,req_keys):
  464. fn = self.get_daemon_cfg_fn()
  465. try:
  466. lines = get_lines_from_file(fn,'daemon config file',silent=not opt.verbose)
  467. except:
  468. vmsg(f'Warning: {fn!r} does not exist or is unreadable')
  469. return dict((k,None) for k in req_keys)
  470. def gen():
  471. for key in req_keys:
  472. val = None
  473. for l in lines:
  474. if l.startswith(key):
  475. res = l.split('=',1)
  476. if len(res) == 2 and not ' ' in res[1].strip():
  477. val = res[1].strip()
  478. yield (key,val)
  479. return dict(gen())
  480. def get_daemon_auth_cookie(self):
  481. fn = self.get_daemon_auth_cookie_fn()
  482. return get_lines_from_file(fn,'cookie',quiet=True)[0] if os.access(fn,os.R_OK) else ''
  483. @staticmethod
  484. def make_host_path(wallet):
  485. return (
  486. '/wallet/{}'.format('bob' if g.bob else 'alice') if (g.bob or g.alice) else
  487. '/wallet/{}'.format(wallet) if wallet else '/'
  488. )
  489. def info(self,info_id):
  490. def segwit_is_active():
  491. d = self.cached['blockchaininfo']
  492. if d['chain'] == 'regtest':
  493. return True
  494. try:
  495. if d['softforks']['segwit']['active'] == True:
  496. return True
  497. except:
  498. pass
  499. try:
  500. if d['bip9_softforks']['segwit']['status'] == 'active':
  501. return True
  502. except:
  503. pass
  504. if g.test_suite:
  505. return True
  506. return False
  507. return locals()[info_id]()
  508. rpcmethods = (
  509. 'backupwallet',
  510. 'createrawtransaction',
  511. 'decoderawtransaction',
  512. 'disconnectnode',
  513. 'estimatefee',
  514. 'estimatesmartfee',
  515. 'getaddressesbyaccount',
  516. 'getaddressesbylabel',
  517. 'getblock',
  518. 'getblockchaininfo',
  519. 'getblockcount',
  520. 'getblockhash',
  521. 'getblockheader',
  522. 'getblockstats', # mmgen-node-tools
  523. 'getmempoolinfo',
  524. 'getmempoolentry',
  525. 'getnettotals',
  526. 'getnetworkinfo',
  527. 'getpeerinfo',
  528. 'getrawmempool',
  529. 'getmempoolentry',
  530. 'getrawtransaction',
  531. 'gettransaction',
  532. 'importaddress',
  533. 'listaccounts',
  534. 'listlabels',
  535. 'listunspent',
  536. 'setlabel',
  537. 'sendrawtransaction',
  538. 'signrawtransaction',
  539. 'signrawtransactionwithkey', # method new to Core v0.17.0
  540. 'validateaddress',
  541. 'walletpassphrase',
  542. )
  543. class EthereumRPCClient(RPCClient,metaclass=AsyncInit):
  544. async def __init__(self,proto,daemon,backend):
  545. self.proto = proto
  546. self.daemon = daemon
  547. self.call_sigs = getattr(getattr(CallSigs,proto.base_proto),daemon.id,None)
  548. super().__init__(
  549. host = 'localhost' if g.test_suite else (g.rpc_host or 'localhost'),
  550. port = daemon.rpc_port )
  551. self.set_backend(backend)
  552. vi,bh,ci = await self.gathered_call(None, (
  553. ('web3_clientVersion',()),
  554. ('eth_getBlockByNumber',('latest',False)),
  555. ('eth_chainId',()),
  556. ))
  557. import re
  558. vip = re.match(self.daemon.version_pat,vi,re.ASCII)
  559. if not vip:
  560. die(2,fmt(f"""
  561. Aborting on daemon mismatch:
  562. Requested daemon: {self.daemon.id}
  563. Running daemon: {vi}
  564. """,strip_char='\t').rstrip())
  565. self.daemon_version = int('{:d}{:03d}{:03d}'.format(*[int(e) for e in vip.groups()]))
  566. self.daemon_version_str = '{}.{}.{}'.format(*vip.groups())
  567. self.daemon_version_info = vi
  568. self.blockcount = int(bh['number'],16)
  569. self.cur_date = int(bh['timestamp'],16)
  570. self.caps = ()
  571. from .obj import Int
  572. if self.daemon.id in ('parity','openethereum'):
  573. if (await self.call('parity_nodeKind'))['capability'] == 'full':
  574. self.caps += ('full_node',)
  575. self.chainID = None if ci == None else Int(ci,16) # parity/oe return chainID only for dev chain
  576. self.chain = (await self.call('parity_chain')).replace(' ','_').replace('_testnet','')
  577. elif self.daemon.id in ('geth','erigon'):
  578. if self.daemon.network == 'mainnet':
  579. daemon_warning(self.daemon.id)
  580. self.caps += ('full_node',)
  581. self.chainID = Int(ci,16)
  582. self.chain = self.proto.chain_ids[self.chainID]
  583. rpcmethods = (
  584. 'eth_blockNumber',
  585. 'eth_call',
  586. # Returns the EIP155 chain ID used for transaction signing at the current best block.
  587. # Parity: Null is returned if not available, ID not required in transactions
  588. # Erigon: always returns ID, requires ID in transactions
  589. 'eth_chainId',
  590. 'eth_gasPrice',
  591. 'eth_getBalance',
  592. 'eth_getCode',
  593. 'eth_getTransactionCount',
  594. 'eth_getTransactionReceipt',
  595. 'eth_sendRawTransaction',
  596. 'parity_chain',
  597. 'parity_nodeKind',
  598. 'parity_pendingTransactions',
  599. 'txpool_content',
  600. )
  601. class MoneroRPCClient(RPCClient):
  602. auth_type = None
  603. network_proto = 'https'
  604. host_path = '/json_rpc'
  605. verify_server = False
  606. def __init__(self,host,port,user,passwd,test_connection=True,proxy=None,daemon=None):
  607. if proxy is not None:
  608. self.proxy = IPPort(proxy)
  609. test_connection = False
  610. if host.endswith('.onion'):
  611. self.network_proto = 'http'
  612. super().__init__(host,port,test_connection)
  613. if self.auth_type:
  614. self.auth = auth_data(user,passwd)
  615. if True:
  616. self.set_backend('requests')
  617. else: # insecure, for debugging only
  618. self.set_backend('curl')
  619. self.backend.exec_opts.remove('--silent')
  620. self.backend.exec_opts.append('--verbose')
  621. self.daemon = daemon
  622. async def call(self,method,*params,**kwargs):
  623. assert params == (), f'{type(self).__name__}.call() accepts keyword arguments only'
  624. return await self.process_http_resp(self.backend.run(
  625. payload = {'id': 0, 'jsonrpc': '2.0', 'method': method, 'params': kwargs },
  626. timeout = 3600, # allow enough time to sync ≈1,000,000 blocks
  627. wallet = None
  628. ))
  629. rpcmethods = ( 'get_info', )
  630. class MoneroRPCClientRaw(MoneroRPCClient):
  631. json_rpc = False
  632. host_path = '/'
  633. async def call(self,method,*params,**kwargs):
  634. assert params == (), f'{type(self).__name__}.call() accepts keyword arguments only'
  635. return await self.process_http_resp(self.backend.run(
  636. payload = kwargs,
  637. timeout = self.timeout,
  638. wallet = method
  639. ))
  640. @staticmethod
  641. def make_host_path(arg):
  642. return arg
  643. async def do_stop_daemon(self,silent=False):
  644. return await self.call('stop_daemon')
  645. rpcmethods = ( 'get_height', 'send_raw_transaction', 'stop_daemon' )
  646. class MoneroWalletRPCClient(MoneroRPCClient):
  647. auth_type = 'digest'
  648. def __init__(self,daemon,test_connection=True):
  649. RPCClient.__init__(
  650. self,
  651. daemon.host,
  652. daemon.rpc_port,
  653. test_connection = test_connection )
  654. self.daemon = daemon
  655. self.auth = auth_data(daemon.user,daemon.passwd)
  656. self.set_backend('requests')
  657. rpcmethods = (
  658. 'get_version',
  659. 'get_height', # sync height of the open wallet
  660. 'get_balance', # account_index=0, address_indices=[]
  661. 'create_wallet', # filename, password, language="English"
  662. 'open_wallet', # filename, password
  663. 'close_wallet',
  664. # filename,password,seed (restore_height,language,seed_offset,autosave_current)
  665. 'restore_deterministic_wallet',
  666. 'refresh', # start_height
  667. )
  668. async def do_stop_daemon(self,silent=False):
  669. """
  670. NB: the 'stop_wallet' RPC call closes the open wallet before shutting down the daemon,
  671. returning an error if no wallet is open
  672. """
  673. return await self.call('stop_wallet')
  674. class daemon_warning(oneshot_warning_group):
  675. class geth:
  676. color = 'yellow'
  677. message = 'Geth has not been tested on mainnet. You may experience problems.'
  678. class erigon:
  679. color = 'red'
  680. message = 'Erigon support is EXPERIMENTAL. Use at your own risk!!!'
  681. class version:
  682. color = 'yellow'
  683. message = 'ignoring unsupported {} daemon version at user request'
  684. def handle_unsupported_daemon_version(rpc,name,warn_only):
  685. if warn_only:
  686. daemon_warning('version',div=name,fmt_args=[rpc.daemon.coind_name])
  687. else:
  688. name = rpc.daemon.coind_name
  689. die(2,'\n'+fmt(f"""
  690. The running {name} daemon has version {rpc.daemon_version_str}.
  691. This version of MMGen is tested only on {name} v{rpc.daemon.coind_version_str} and below.
  692. To avoid this error, downgrade your daemon to a supported version.
  693. Alternatively, you may invoke the command with the --ignore-daemon-version
  694. option, in which case you proceed at your own risk.
  695. """,indent=' '))
  696. async def rpc_init(proto,backend=None,daemon=None,ignore_daemon_version=False):
  697. if not 'rpc' in proto.mmcaps:
  698. die(1,f'Coin daemon operations not supported for {proto.name} protocol!')
  699. from .daemon import CoinDaemon
  700. rpc = await {
  701. 'Bitcoin': BitcoinRPCClient,
  702. 'Ethereum': EthereumRPCClient,
  703. }[proto.base_proto](
  704. proto = proto,
  705. daemon = daemon or CoinDaemon(proto=proto,test_suite=g.test_suite),
  706. backend = backend or opt.rpc_backend )
  707. if rpc.daemon_version > rpc.daemon.coind_version:
  708. handle_unsupported_daemon_version(
  709. rpc,
  710. proto.name,
  711. ignore_daemon_version or proto.ignore_daemon_version or g.ignore_daemon_version )
  712. if rpc.chain not in proto.chain_names:
  713. die( 'RPCChainMismatch', '\n' + fmt(f"""
  714. Protocol: {proto.cls_name}
  715. Valid chain names: {fmt_list(proto.chain_names,fmt='bare')}
  716. RPC client chain: {rpc.chain}
  717. """,indent=' ').rstrip() )
  718. return rpc