Jsonrpc backend refactor (#82)

* Refactoring `backends` submodule
Co-authored-by: Jeffrey Ryan <jeffaryan7@gmail.com>
pull/89/head
Michał Sałaban 3 years ago committed by GitHub
parent 91a533e6eb
commit 73ad95650e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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

@ -1,214 +1,21 @@
from __future__ import unicode_literals
import binascii
from datetime import datetime
import operator
import json
import logging
import operator
import requests
import six
from .. import exceptions
from ..account import Account
from ..address import address, Address, SubAddress
from ..block import Block
from ..const import NET_MAIN, NET_TEST, NET_STAGE
from ..numbers import from_atomic, to_atomic, PaymentID
from ..seed import Seed
from ..transaction import Transaction, IncomingPayment, OutgoingPayment
from ... import exceptions
from ...account import Account
from ...address import address, Address, SubAddress
from ...numbers import from_atomic, to_atomic, PaymentID
from ...seed import Seed
from ...transaction import Transaction, IncomingPayment, OutgoingPayment
from .exceptions import RPCError, Unauthorized, MethodNotFound
_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']
class JSONRPCWallet(object):
"""
JSON RPC backend for Monero wallet (``monero-wallet-rpc``)
@ -511,19 +318,6 @@ class JSONRPCWallet(object):
"message: {message}".format(method=method, data=data, result=result, **err))
return result['result']
class RPCError(exceptions.BackendException):
pass
class Unauthorized(RPCError):
pass
class MethodNotFound(RPCError):
pass
_err2exc = {
-2: exceptions.WrongAddress,
-4: exceptions.GenericTransferError,

@ -36,7 +36,7 @@ class JSONRPCWalletTestCase(JSONTestCase):
'total_balance': 236153709446071,
'total_unlocked_balance': 236153709446071}}
@patch('monero.backends.jsonrpc.requests.post')
@patch('monero.backends.jsonrpc.wallet.requests.post')
def test_seed(self, mock_post):
mock_post.return_value.status_code = 200
mock_post.return_value.json.return_value = self.accounts_result
@ -52,7 +52,7 @@ class JSONRPCWalletTestCase(JSONTestCase):
self.assertIsInstance(seed, Seed)
self.assertEqual(seed.phrase, phrase)
@patch('monero.backends.jsonrpc.requests.post')
@patch('monero.backends.jsonrpc.wallet.requests.post')
def test_balance(self, mock_post):
mock_post.return_value.status_code = 200
mock_post.return_value.json.return_value = self.accounts_result
@ -158,7 +158,7 @@ class JSONRPCWalletTestCase(JSONTestCase):
self.assertIsInstance(subaddr, SubAddress)
self.assertIsInstance(index, int)
@patch('monero.backends.jsonrpc.requests.post')
@patch('monero.backends.jsonrpc.wallet.requests.post')
def test_incoming_confirmed(self, mock_post):
mock_post.return_value.status_code = 200
mock_post.return_value.json.return_value = self.accounts_result
@ -285,7 +285,7 @@ class JSONRPCWalletTestCase(JSONTestCase):
self.assertIsInstance(pmt.transaction.fee, Decimal)
self.assertIsInstance(pmt.transaction.height, int)
@patch('monero.backends.jsonrpc.requests.post')
@patch('monero.backends.jsonrpc.wallet.requests.post')
def test_incoming_confirmed_and_unconfirmed(self, mock_post):
mock_post.return_value.status_code = 200
mock_post.return_value.json.return_value = self.accounts_result
@ -437,7 +437,7 @@ class JSONRPCWalletTestCase(JSONTestCase):
self.assertIsInstance(pmt.transaction.height, (int, type(None)))
@patch('monero.backends.jsonrpc.requests.post')
@patch('monero.backends.jsonrpc.wallet.requests.post')
def test_incoming_unconfirmed(self, mock_post):
mock_post.return_value.status_code = 200
mock_post.return_value.json.return_value = self.accounts_result
@ -639,7 +639,7 @@ class JSONRPCWalletTestCase(JSONTestCase):
self.assertEqual(pmts[0].amount, Decimal('0.52'))
self.assertEqual(pmts[1].amount, Decimal('0.0212'))
@patch('monero.backends.jsonrpc.requests.post')
@patch('monero.backends.jsonrpc.wallet.requests.post')
def test_incoming_by_payment_ids(self, mock_post):
# These queries will use get_bulk_payments RPC method instead of get_transfers
mock_post.return_value.status_code = 200
@ -726,7 +726,7 @@ class JSONRPCWalletTestCase(JSONTestCase):
self.assertIsInstance(pmt.transaction.height, int)
self.assertIn(pmt.payment_id, ids)
@patch('monero.backends.jsonrpc.requests.post')
@patch('monero.backends.jsonrpc.wallet.requests.post')
def test_outgoing(self, mock_post):
mock_post.return_value.status_code = 200
mock_post.return_value.json.return_value = self.accounts_result
@ -894,7 +894,7 @@ class JSONRPCWalletTestCase(JSONTestCase):
self.assertIsInstance(pmt.transaction.fee, Decimal)
self.assertIsInstance(pmt.transaction.height, int)
@patch('monero.backends.jsonrpc.requests.post')
@patch('monero.backends.jsonrpc.wallet.requests.post')
def test_outgoing_confirmed_and_unconfirmed(self, mock_post):
mock_post.return_value.status_code = 200
mock_post.return_value.json.return_value = self.accounts_result
@ -1074,7 +1074,7 @@ class JSONRPCWalletTestCase(JSONTestCase):
self.assertIsInstance(pmt.transaction.fee, Decimal)
self.assertIsInstance(pmt.transaction.height, (int, type(None)))
@patch('monero.backends.jsonrpc.requests.post')
@patch('monero.backends.jsonrpc.wallet.requests.post')
def test_outgoing_unconfirmed_only(self, mock_post):
mock_post.return_value.status_code = 200
mock_post.return_value.json.return_value = self.accounts_result
@ -1122,7 +1122,7 @@ class JSONRPCWalletTestCase(JSONTestCase):
self.assertEqual(len(pmt.destinations), 2)
self.assertEqual(pmt.destinations[0][1] + pmt.destinations[1][1], pmt.amount)
@patch('monero.backends.jsonrpc.requests.post')
@patch('monero.backends.jsonrpc.wallet.requests.post')
def test_send_transfer(self, mock_post):
mock_post.return_value.status_code = 200
mock_post.return_value.json.return_value = self.accounts_result
@ -1168,7 +1168,7 @@ class JSONRPCWalletTestCase(JSONTestCase):
self.assertIsInstance(result[0], Transaction)
self.assertEqual(Decimal('111.086545699972'), result[1])
@patch('monero.backends.jsonrpc.requests.post')
@patch('monero.backends.jsonrpc.wallet.requests.post')
def test_dynamic_ring_size_deprecation(self, mock_post):
mock_post.return_value.status_code = 200
mock_post.return_value.json.return_value = self.accounts_result
@ -1183,7 +1183,7 @@ class JSONRPCWalletTestCase(JSONTestCase):
'tx_hash_list': ['401d8021975a0fee16fe84acbfc4d8ba6312e563fa245baba2aac382e787fb60'],
'tx_key_list': ['7061d4d939b563a11e344c60938410e2e63ea72c43741fae81b8805cebe5570a']}}
@patch('monero.backends.jsonrpc.requests.post')
@patch('monero.backends.jsonrpc.wallet.requests.post')
def test_export_import_outputs(self, mock_post):
mock_post.return_value.status_code = 200
mock_post.return_value.json.return_value = self.accounts_result
@ -1196,7 +1196,7 @@ class JSONRPCWalletTestCase(JSONTestCase):
mock_post.return_value.json.return_value = {u'id': 0, u'jsonrpc': u'2.0', u'result': {u'num_imported': 9}}
self.assertEqual(self.wallet.import_outputs(outs_hex), 9)
@patch('monero.backends.jsonrpc.requests.post')
@patch('monero.backends.jsonrpc.wallet.requests.post')
def test_export_import_key_images(self, mock_post):
mock_post.return_value.status_code = 200
mock_post.return_value.json.return_value = self.accounts_result

Loading…
Cancel
Save