Jsonrpc backend refactor (#82)
* Refactoring `backends` submodule Co-authored-by: Jeffrey Ryan <jeffaryan7@gmail.com>pull/89/head
parent
91a533e6eb
commit
73ad95650e
@ -0,0 +1,3 @@
|
||||
from .wallet import JSONRPCWallet
|
||||
from .daemon import JSONRPCDaemon
|
||||
from .exceptions import RPCError, Unauthorized, MethodNotFound
|
@ -0,0 +1,205 @@
|
||||
from __future__ import unicode_literals
|
||||
import binascii
|
||||
from datetime import datetime
|
||||
import json
|
||||
import logging
|
||||
import requests
|
||||
import six
|
||||
|
||||
from ...block import Block
|
||||
from ...const import NET_MAIN, NET_TEST, NET_STAGE
|
||||
from ...numbers import from_atomic
|
||||
from ...transaction import Transaction
|
||||
from .exceptions import RPCError, Unauthorized
|
||||
|
||||
|
||||
_log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
RESTRICTED_MAX_TRANSACTIONS = 100
|
||||
|
||||
|
||||
class JSONRPCDaemon(object):
|
||||
"""
|
||||
JSON RPC backend for Monero daemon
|
||||
|
||||
:param protocol: `http` or `https`
|
||||
:param host: host name or IP
|
||||
:param port: port number
|
||||
:param path: path for JSON RPC requests (should not be changed)
|
||||
:param timeout: request timeout
|
||||
:param verify_ssl_certs: verify SSL certificates when connecting
|
||||
:param proxy_url: a proxy to use
|
||||
"""
|
||||
|
||||
_net = None
|
||||
|
||||
def __init__(self, protocol='http', host='127.0.0.1', port=18081, path='/json_rpc',
|
||||
user='', password='', timeout=30, verify_ssl_certs=True, proxy_url=None):
|
||||
self.url = '{protocol}://{host}:{port}'.format(
|
||||
protocol=protocol,
|
||||
host=host,
|
||||
port=port)
|
||||
_log.debug("JSONRPC daemon backend URL: {url}".format(url=self.url))
|
||||
self.user = user
|
||||
self.password = password
|
||||
self.timeout = timeout
|
||||
self.verify_ssl_certs = verify_ssl_certs
|
||||
self.proxies = {protocol: proxy_url}
|
||||
|
||||
def _set_net(self, info):
|
||||
if info['mainnet']:
|
||||
self._net = NET_MAIN
|
||||
if info['testnet']:
|
||||
self._net = NET_TEST
|
||||
if info['stagenet']:
|
||||
self._net = NET_STAGE
|
||||
|
||||
def info(self):
|
||||
info = self.raw_jsonrpc_request('get_info')
|
||||
self._set_net(info)
|
||||
return info
|
||||
|
||||
def net(self):
|
||||
if self._net:
|
||||
return self._net
|
||||
self.info()
|
||||
return self._net
|
||||
|
||||
def send_transaction(self, blob, relay=True):
|
||||
res = self.raw_request('/sendrawtransaction', {
|
||||
'tx_as_hex': six.ensure_text(binascii.hexlify(blob)),
|
||||
'do_not_relay': not relay})
|
||||
if res['status'] == 'OK':
|
||||
return res
|
||||
raise exceptions.TransactionBroadcastError(
|
||||
"{status}: {reason}".format(**res),
|
||||
details=res)
|
||||
|
||||
def mempool(self):
|
||||
res = self.raw_request('/get_transaction_pool', {})
|
||||
txs = []
|
||||
for tx in res.get('transactions', []):
|
||||
txs.append(Transaction(
|
||||
hash=tx['id_hash'],
|
||||
fee=from_atomic(tx['fee']),
|
||||
timestamp=datetime.fromtimestamp(tx['receive_time']),
|
||||
blob=binascii.unhexlify(tx['tx_blob']),
|
||||
json=json.loads(tx['tx_json']),
|
||||
confirmations=0))
|
||||
return txs
|
||||
|
||||
def headers(self, start_height, end_height=None):
|
||||
end_height = end_height or start_height
|
||||
res = self.raw_jsonrpc_request('get_block_headers_range', {
|
||||
'start_height': start_height,
|
||||
'end_height': end_height})
|
||||
if res['status'] == 'OK':
|
||||
return res['headers']
|
||||
raise exceptions.BackendException(res['status'])
|
||||
|
||||
def block(self, bhash=None, height=None):
|
||||
data = {}
|
||||
if bhash:
|
||||
data['hash'] = bhash
|
||||
if height:
|
||||
data['height'] = height
|
||||
res = self.raw_jsonrpc_request('get_block', data)
|
||||
if res['status'] == 'OK':
|
||||
bhdr = res['block_header']
|
||||
sub_json = json.loads(res['json'])
|
||||
data = {
|
||||
'blob': res['blob'],
|
||||
'hash': bhdr['hash'],
|
||||
'height': bhdr['height'],
|
||||
'timestamp': datetime.fromtimestamp(bhdr['timestamp']),
|
||||
'version': (bhdr['major_version'], bhdr['minor_version']),
|
||||
'difficulty': bhdr['difficulty'],
|
||||
'nonce': bhdr['nonce'],
|
||||
'orphan': bhdr['orphan_status'],
|
||||
'prev_hash': bhdr['prev_hash'],
|
||||
'reward': from_atomic(bhdr['reward']),
|
||||
'transactions': self.transactions(
|
||||
[bhdr['miner_tx_hash']] + sub_json['tx_hashes']),
|
||||
}
|
||||
return Block(**data)
|
||||
raise exceptions.BackendException(res['status'])
|
||||
|
||||
def transactions(self, hashes):
|
||||
"""
|
||||
Returns a list of transactions for given hashes. Automatically chunks the request
|
||||
into amounts acceptable by a restricted RPC server.
|
||||
"""
|
||||
hashes = list(hashes)
|
||||
result = []
|
||||
while len(hashes):
|
||||
result.extend(self._do_get_transactions(hashes[:RESTRICTED_MAX_TRANSACTIONS]))
|
||||
hashes = hashes[RESTRICTED_MAX_TRANSACTIONS:]
|
||||
return result
|
||||
|
||||
def _do_get_transactions(self, hashes):
|
||||
res = self.raw_request('/get_transactions', {
|
||||
'txs_hashes': hashes,
|
||||
'decode_as_json': True})
|
||||
if res['status'] != 'OK':
|
||||
raise exceptions.BackendException(res['status'])
|
||||
txs = []
|
||||
for tx in res.get('txs', []):
|
||||
as_json = json.loads(tx['as_json'])
|
||||
fee = as_json.get('rct_signatures', {}).get('txnFee')
|
||||
txs.append(Transaction(
|
||||
hash=tx['tx_hash'],
|
||||
fee=from_atomic(fee) if fee else None,
|
||||
height=None if tx['in_pool'] else tx['block_height'],
|
||||
timestamp=datetime.fromtimestamp(
|
||||
tx['block_timestamp']) if 'block_timestamp' in tx else None,
|
||||
blob=binascii.unhexlify(tx['as_hex']),
|
||||
json=as_json))
|
||||
return txs
|
||||
|
||||
def raw_request(self, path, data):
|
||||
hdr = {'Content-Type': 'application/json'}
|
||||
_log.debug(u"Request: {path}\nData: {data}".format(
|
||||
path=path,
|
||||
data=json.dumps(data, indent=2, sort_keys=True)))
|
||||
auth = requests.auth.HTTPDigestAuth(self.user, self.password)
|
||||
rsp = requests.post(
|
||||
self.url + path, headers=hdr, data=json.dumps(data), auth=auth,
|
||||
timeout=self.timeout, verify=self.verify_ssl_certs, proxies=self.proxies)
|
||||
if rsp.status_code != 200:
|
||||
raise RPCError("Invalid HTTP status {code} for path {path}.".format(
|
||||
code=rsp.status_code,
|
||||
path=path))
|
||||
result = rsp.json()
|
||||
_ppresult = json.dumps(result, indent=2, sort_keys=True)
|
||||
_log.debug(u"Result:\n{result}".format(result=_ppresult))
|
||||
return result
|
||||
|
||||
def raw_jsonrpc_request(self, method, params=None):
|
||||
hdr = {'Content-Type': 'application/json'}
|
||||
data = {'jsonrpc': '2.0', 'id': 0, 'method': method, 'params': params or {}}
|
||||
_log.debug(u"Method: {method}\nParams:\n{params}".format(
|
||||
method=method,
|
||||
params=json.dumps(params, indent=2, sort_keys=True)))
|
||||
auth = requests.auth.HTTPDigestAuth(self.user, self.password)
|
||||
rsp = requests.post(
|
||||
self.url + '/json_rpc', headers=hdr, data=json.dumps(data), auth=auth,
|
||||
timeout=self.timeout, verify=self.verify_ssl_certs, proxies=self.proxies)
|
||||
|
||||
if rsp.status_code == 401:
|
||||
raise Unauthorized("401 Unauthorized. Invalid RPC user name or password.")
|
||||
elif rsp.status_code != 200:
|
||||
raise RPCError("Invalid HTTP status {code} for method {method}.".format(
|
||||
code=rsp.status_code,
|
||||
method=method))
|
||||
result = rsp.json()
|
||||
_ppresult = json.dumps(result, indent=2, sort_keys=True)
|
||||
_log.debug(u"Result:\n{result}".format(result=_ppresult))
|
||||
|
||||
if 'error' in result:
|
||||
err = result['error']
|
||||
_log.error(u"JSON RPC error:\n{result}".format(result=_ppresult))
|
||||
raise RPCError(
|
||||
"Method '{method}' failed with RPC Error of unknown code {code}, "
|
||||
"message: {message}".format(method=method, data=data, result=result, **err))
|
||||
return result['result']
|
@ -0,0 +1,12 @@
|
||||
from ... import exceptions
|
||||
|
||||
class RPCError(exceptions.BackendException):
|
||||
pass
|
||||
|
||||
|
||||
class Unauthorized(RPCError):
|
||||
pass
|
||||
|
||||
|
||||
class MethodNotFound(RPCError):
|
||||
pass
|
Loading…
Reference in new issue