rpc.py 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. #!/usr/bin/env python3
  2. #
  3. # mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution
  4. # Copyright (C)2013-2019 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 http.client,base64,json
  22. from mmgen.common import *
  23. from decimal import Decimal
  24. def dmsg_rpc(s):
  25. if g.debug_rpc: msg(s)
  26. class CoinDaemonRPCConnection(object):
  27. auth = True
  28. db_fs = ' host [{h}] port [{p}] user [{u}] passwd [{pw}] auth_cookie [{c}]\n'
  29. def __init__(self,host=None,port=None,user=None,passwd=None,auth_cookie=None):
  30. dmsg_rpc('=== {}.__init__() debug ==='.format(type(self).__name__))
  31. dmsg_rpc(self.db_fs.format(h=host,p=port,u=user,pw=passwd,c=auth_cookie))
  32. import socket
  33. try:
  34. socket.create_connection((host,port),timeout=3).close()
  35. except:
  36. die(1,'Unable to connect to {}:{}'.format(host,port))
  37. if not self.auth:
  38. pass
  39. elif user and passwd:
  40. self.auth_str = '{}:{}'.format(user,passwd)
  41. elif auth_cookie:
  42. self.auth_str = auth_cookie
  43. else:
  44. msg('Error: no {} RPC authentication method found'.format(g.proto.name.capitalize()))
  45. if passwd: die(1,"'rpcuser' entry not found in {}.conf or mmgen.cfg".format(g.proto.name))
  46. elif user: die(1,"'rpcpassword' entry not found in {}.conf or mmgen.cfg".format(g.proto.name))
  47. else:
  48. m1 = 'Either provide rpcuser/rpcpassword in {pn}.conf or mmgen.cfg\n'
  49. m2 = '(or, alternatively, copy the authentication cookie to the {pnu}\n'
  50. m3 = 'data dir if {pnm} and {dn} are running as different users)'
  51. die(1,(m1+m2+m3).format(
  52. pn=g.proto.name,
  53. pnu=g.proto.name.capitalize(),
  54. dn=g.proto.daemon_name,
  55. pnm=g.proj_name))
  56. self.host = host
  57. self.port = port
  58. for method in self.rpcmethods:
  59. exec('{c}.{m} = lambda self,*args,**kwargs: self.request("{m}",*args,**kwargs)'.format(
  60. c=type(self).__name__,m=method))
  61. # Normal mode: call with arg list unrolled, exactly as with cli
  62. # Batch mode: call with list of arg lists as first argument
  63. # kwargs are for local use and are not passed to server
  64. # By default, raises RPCFailure exception with an error msg on all errors and exceptions
  65. # on_fail is one of 'raise' (default), 'return' or 'silent'
  66. # With on_fail='return', returns 'rpcfail',(resp_object,(die_args))
  67. def request(self,cmd,*args,**kwargs):
  68. if g.rpc_fail_on_command == cmd:
  69. cmd = 'badcommand_' + cmd
  70. cf = { 'timeout':g.http_timeout, 'batch':False, 'on_fail':'raise' }
  71. if cf['on_fail'] not in ('raise','return','silent'):
  72. raise ValueError("request(): {}: illegal value for 'on_fail'".format(cf['on_fail']))
  73. for k in cf:
  74. if k in kwargs and kwargs[k]: cf[k] = kwargs[k]
  75. hc = http.client.HTTPConnection(self.host,self.port,cf['timeout'])
  76. if cf['batch']:
  77. p = [{'method':cmd,'params':r,'id':n,'jsonrpc':'2.0'} for n,r in enumerate(args[0],1)]
  78. else:
  79. p = {'method':cmd,'params':args,'id':1,'jsonrpc':'2.0'}
  80. def do_fail(*args):
  81. if cf['on_fail'] in ('return','silent'): return 'rpcfail',args
  82. try: s = '{}'.format(args[2])
  83. except: s = repr(args[2])
  84. raise RPCFailure(s)
  85. dmsg_rpc('=== request() debug ===')
  86. dmsg_rpc(' RPC POST data ==> {}\n'.format(p))
  87. ca_type = self.coin_amt_type if hasattr(self,'coin_amt_type') else str
  88. from mmgen.obj import HexStr
  89. class MyJSONEncoder(json.JSONEncoder):
  90. def default(self,obj):
  91. if isinstance(obj,g.proto.coin_amt):
  92. return ca_type(obj)
  93. elif isinstance(obj,HexStr):
  94. return obj
  95. else:
  96. return json.JSONEncoder.default(self,obj)
  97. http_hdr = { 'Content-Type': 'application/json' }
  98. if self.auth:
  99. fs = ' RPC AUTHORIZATION data ==> raw: [{}]\n{:>31}enc: [Basic {}]\n'
  100. as_enc = base64.b64encode(self.auth_str.encode())
  101. dmsg_rpc(fs.format(self.auth_str,'',as_enc))
  102. http_hdr.update({ 'Host':self.host, 'Authorization':'Basic {}'.format(as_enc.decode()) })
  103. try:
  104. hc.request('POST','/',json.dumps(p,cls=MyJSONEncoder),http_hdr)
  105. except Exception as e:
  106. m = '{}\nUnable to connect to {} at {}:{}'
  107. return do_fail(None,2,m.format(e.args[0],g.proto.daemon_name,self.host,self.port))
  108. try:
  109. r = hc.getresponse() # returns HTTPResponse instance
  110. except Exception:
  111. m = 'Unable to connect to {} at {}:{} (but port is bound?)'
  112. return do_fail(None,2,m.format(g.proto.daemon_name,self.host,self.port))
  113. dmsg_rpc(' RPC GETRESPONSE data ==> {}\n'.format(r.__dict__))
  114. if r.status != 200:
  115. if cf['on_fail'] not in ('silent','raise'):
  116. msg_r(yellow('{} RPC Error: '.format(g.proto.daemon_name.capitalize())))
  117. msg(red('{} {}'.format(r.status,r.reason)))
  118. e1 = r.read().decode()
  119. try:
  120. e3 = json.loads(e1)['error']
  121. e2 = '{} (code {})'.format(e3['message'],e3['code'])
  122. except:
  123. e2 = str(e1)
  124. return do_fail(r,1,e2)
  125. r2 = r.read().decode()
  126. dmsg_rpc(' RPC REPLY data ==> {}\n'.format(r2))
  127. if not r2:
  128. return do_fail(r,2,'Empty reply')
  129. r3 = json.loads(r2,parse_float=Decimal)
  130. ret = []
  131. for resp in r3 if cf['batch'] else [r3]:
  132. if 'error' in resp and resp['error'] != None:
  133. return do_fail(r,1,'{} returned an error: {}'.format(
  134. g.proto.daemon_name.capitalize(),resp['error']))
  135. elif 'result' not in resp:
  136. return do_fail(r,1, 'Missing JSON-RPC result\n' + repr(resps))
  137. else:
  138. ret.append(resp['result'])
  139. return ret if cf['batch'] else ret[0]
  140. rpcmethods = (
  141. 'backupwallet',
  142. 'createrawtransaction',
  143. 'decoderawtransaction',
  144. 'disconnectnode',
  145. 'estimatefee',
  146. 'estimatesmartfee',
  147. 'getaddressesbyaccount',
  148. 'getaddressesbylabel',
  149. 'getbalance',
  150. 'getblock',
  151. 'getblockchaininfo',
  152. 'getblockcount',
  153. 'getblockhash',
  154. 'getmempoolinfo',
  155. 'getmempoolentry',
  156. 'getnettotals',
  157. 'getnetworkinfo',
  158. 'getpeerinfo',
  159. 'getrawmempool',
  160. 'getmempoolentry',
  161. 'getrawtransaction',
  162. 'gettransaction',
  163. 'importaddress',
  164. 'listaccounts',
  165. 'listlabels',
  166. 'listunspent',
  167. 'setlabel',
  168. 'sendrawtransaction',
  169. 'signrawtransaction',
  170. 'signrawtransactionwithkey', # method new to Core v0.17.0
  171. 'validateaddress',
  172. 'walletpassphrase',
  173. )
  174. class EthereumRPCConnection(CoinDaemonRPCConnection):
  175. auth = False
  176. db_fs = ' host [{h}] port [{p}]\n'
  177. rpcmethods = (
  178. 'eth_accounts',
  179. 'eth_blockNumber',
  180. 'eth_call',
  181. # Returns the EIP155 chain ID used for transaction signing at the current best block.
  182. # Null is returned if not available.
  183. 'eth_chainId',
  184. 'eth_gasPrice',
  185. 'eth_getBalance',
  186. 'eth_getBlockByHash',
  187. 'eth_getBlockByNumber',
  188. 'eth_getCode',
  189. 'eth_getTransactionByHash',
  190. 'eth_getTransactionReceipt',
  191. 'eth_protocolVersion',
  192. 'eth_sendRawTransaction',
  193. 'eth_signTransaction',
  194. 'eth_syncing',
  195. 'net_listening',
  196. 'net_peerCount',
  197. 'net_version',
  198. 'parity_chain',
  199. 'parity_chainId', # superseded by eth_chainId
  200. 'parity_chainStatus',
  201. 'parity_composeTransaction',
  202. 'parity_gasCeilTarget',
  203. 'parity_gasFloorTarget',
  204. 'parity_localTransactions',
  205. 'parity_minGasPrice',
  206. 'parity_mode',
  207. 'parity_netPeers',
  208. 'parity_nextNonce',
  209. 'parity_nodeKind',
  210. 'parity_nodeName',
  211. 'parity_pendingTransactions',
  212. 'parity_pendingTransactionsStats',
  213. 'parity_versionInfo',
  214. )
  215. def rpc_error(ret):
  216. return type(ret) is tuple and ret and ret[0] == 'rpcfail'
  217. def rpc_errmsg(ret): return ret[1][2]