├── monerorpc ├── __init__.py ├── .gitignore └── authproxy.py ├── requirements.txt ├── jsonrpc ├── proxy.py ├── authproxy.py ├── __init__.py └── json.py ├── contributors.md ├── .gitignore ├── examples ├── test_daemon_rpc.py ├── test_daemon_rpc_params.py ├── test_rpc.py └── test_rpc_batch.py ├── setup.py ├── LICENSE ├── tests.py └── README.md /monerorpc/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /monerorpc/.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | pytest-cov 3 | requests 4 | responses -------------------------------------------------------------------------------- /jsonrpc/proxy.py: -------------------------------------------------------------------------------- 1 | from monerorpc.authproxy import AuthServiceProxy as ServiceProxy, JSONRPCException 2 | -------------------------------------------------------------------------------- /jsonrpc/authproxy.py: -------------------------------------------------------------------------------- 1 | from monerorpc.authproxy import AuthServiceProxy, JSONRPCException 2 | 3 | __all__ = ["AuthServiceProxy", "JSONRPCException"] 4 | -------------------------------------------------------------------------------- /jsonrpc/__init__.py: -------------------------------------------------------------------------------- 1 | from .json import loads, dumps, JSONEncodeException, JSONDecodeException 2 | from jsonrpc.proxy import ServiceProxy, JSONRPCException 3 | -------------------------------------------------------------------------------- /contributors.md: -------------------------------------------------------------------------------- 1 | **Gonçalo Valério** - [dethos](https://github.com/dethos/) 2 | 3 | **Luis Alvarez** - [lalvarezguillen](https://github.com/lalvarezguillen/) 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | 3 | MANIFEST 4 | dist/ 5 | build/ 6 | *egg-info/ 7 | .venv 8 | .vscode 9 | .autoenv 10 | .coverage 11 | .mypy_cache 12 | .pytest_cache -------------------------------------------------------------------------------- /jsonrpc/json.py: -------------------------------------------------------------------------------- 1 | _json = __import__("json") 2 | loads = _json.loads 3 | dumps = _json.dumps 4 | if hasattr(_json, "JSONEncodeException"): 5 | JSONEncodeException = _json.JSONEncodeException 6 | JSONDecodeException = _json.JSONDecodeException 7 | else: 8 | JSONEncodeException = TypeError 9 | JSONDecodeException = ValueError 10 | -------------------------------------------------------------------------------- /examples/test_daemon_rpc.py: -------------------------------------------------------------------------------- 1 | from monerorpc.authproxy import AuthServiceProxy, JSONRPCException 2 | import logging 3 | 4 | logging.basicConfig() 5 | logging.getLogger("MoneroRPC").setLevel(logging.DEBUG) 6 | log = logging.getLogger("wallet-rpc-lib") 7 | 8 | rpc = AuthServiceProxy("http://test:test@127.0.0.1:18081/json_rpc") 9 | # rpc = AuthServiceProxy('http://127.0.0.1:18081/json_rpc') 10 | try: 11 | rpc.get_info() 12 | except (JSONRPCException) as e: 13 | log.error(e) 14 | -------------------------------------------------------------------------------- /examples/test_daemon_rpc_params.py: -------------------------------------------------------------------------------- 1 | from monerorpc.authproxy import AuthServiceProxy, JSONRPCException 2 | import logging 3 | 4 | logging.basicConfig() 5 | logging.getLogger("MoneroRPC").setLevel(logging.DEBUG) 6 | log = logging.getLogger("wallet-rpc-lib") 7 | 8 | rpc = AuthServiceProxy("http://test:test@127.0.0.1:18081/json_rpc") 9 | # rpc = AuthServiceProxy('http://127.0.0.1:18081/json_rpc') 10 | try: 11 | rpc.get_block_count() 12 | params = [2] 13 | hash = rpc.on_get_block_hash(params) 14 | print(hash) 15 | except (JSONRPCException) as e: 16 | log.error(e) 17 | -------------------------------------------------------------------------------- /examples/test_rpc.py: -------------------------------------------------------------------------------- 1 | from monerorpc.authproxy import AuthServiceProxy, JSONRPCException 2 | import logging 3 | 4 | logging.basicConfig() 5 | logging.getLogger("MoneroRPC").setLevel(logging.DEBUG) 6 | log = logging.getLogger("wallet-rpc-lib") 7 | 8 | rpc = AuthServiceProxy("http://test:test@127.0.0.1:38083/json_rpc") 9 | # rpc = AuthServiceProxy('http://127.0.0.1:38083/json_rpc') 10 | try: 11 | rpc.get_balance() 12 | params = {"account_index": 0, "address_indices": [0, 1]} 13 | rpc.get_balance(params) 14 | destinations = { 15 | "destinations": [ 16 | { 17 | "address": "59McWTPGc745SRWrSMoh8oTjoXoQq6sPUgKZ66dQWXuKFQ2q19h9gvhJNZcFTizcnT12r63NFgHiGd6gBCjabzmzHAMoyD6", 18 | "amount": 1, 19 | } 20 | ], 21 | "mixin": 10, 22 | } 23 | rpc.transfer(destinations) 24 | except (JSONRPCException) as e: 25 | log.error(e) 26 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from setuptools import setup 4 | 5 | setup( 6 | name="python-monerorpc", 7 | version="0.5.5", 8 | description="Enhanced version of python-jsonrpc for Monero (monerod, monero-wallet-rpc).", 9 | long_description=open("README.md").read(), 10 | long_description_content_type="text/markdown", 11 | author="Norman Moeschter-Schenck", 12 | author_email="", 13 | maintainer="Norman Moeschter-Schenck", 14 | maintainer_email="", 15 | url="https://www.github.com/monero-ecosystem/python-monerorpc", 16 | download_url="https://github.com/monero-ecosystem/python-monerorpc/archive/0.5.5.tar.gz", 17 | packages=["monerorpc"], 18 | install_requires=["requests"], 19 | classifiers=[ 20 | "License :: OSI Approved :: MIT License", 21 | "Operating System :: OS Independent", 22 | "Programming Language :: Python :: 3", 23 | "Programming Language :: Python :: 2.7", 24 | ], 25 | ) 26 | -------------------------------------------------------------------------------- /examples/test_rpc_batch.py: -------------------------------------------------------------------------------- 1 | from monerorpc.authproxy import AuthServiceProxy, JSONRPCException 2 | import logging 3 | import pprint 4 | 5 | logging.basicConfig() 6 | logging.getLogger("MoneroRPC").setLevel(logging.DEBUG) 7 | log = logging.getLogger("wallet-rpc-lib") 8 | 9 | rpc = AuthServiceProxy("http://test:test@127.0.0.1:38083/json_rpc") 10 | # rpc = AuthServiceProxy('http://127.0.0.1:38083/json_rpc') 11 | try: 12 | 13 | params = {"account_index": 0, "address_indices": [0, 1]} 14 | result = rpc.batch_([["get_balance"], ["get_balance", params]]) 15 | pprint.pprint(result) 16 | 17 | destinations = { 18 | "destinations": [ 19 | { 20 | "address": "59McWTPGc745SRWrSMoh8oTjoXoQq6sPUgKZ66dQWXuKFQ2q19h9gvhJNZcFTizcnT12r63NFgHiGd6gBCjabzmzHAMoyD6", 21 | "amount": 1, 22 | } 23 | ], 24 | "mixin": 10, 25 | } 26 | result = rpc.batch_([["transfer", destinations], ["get_balance"]]) 27 | pprint.pprint(result) 28 | 29 | except (JSONRPCException) as e: 30 | log.error(e) 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 normoes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | import json 2 | import re 3 | from decimal import Decimal 4 | 5 | import pytest 6 | import responses 7 | from requests.exceptions import ConnectionError 8 | from monerorpc.authproxy import AuthServiceProxy, EncodeDecimal, JSONRPCException 9 | 10 | 11 | class TestEncodeDecimal: 12 | def test_encodes_ok(self): 13 | assert json.dumps(Decimal(2), default=EncodeDecimal) 14 | 15 | def test_encoding_fail(self): 16 | with pytest.raises(TypeError): 17 | json.dumps(self, default=EncodeDecimal) 18 | 19 | 20 | class TestAuthServiceProxy: 21 | dummy_url = "http://dummy-rpc:8000/json_rpc" 22 | 23 | @responses.activate 24 | def test_good_call(self): 25 | responses.add(responses.POST, self.dummy_url, json={"result": "dummy"}) 26 | client = AuthServiceProxy(self.dummy_url) 27 | resp = client.status() 28 | assert resp == "dummy" 29 | 30 | @responses.activate 31 | @pytest.mark.parametrize("code", (500, 404)) 32 | def test_http_error_raises_error(self, code): 33 | responses.add(responses.POST, self.dummy_url, status=code) 34 | client = AuthServiceProxy(self.dummy_url) 35 | with pytest.raises(JSONRPCException): 36 | client.dummy_method() 37 | 38 | @responses.activate 39 | def test_empty_response_raises_error(self): 40 | responses.add(responses.POST, self.dummy_url, status=200, json={}) 41 | client = AuthServiceProxy(self.dummy_url) 42 | with pytest.raises(JSONRPCException): 43 | client.dummy_method() 44 | 45 | @responses.activate 46 | def test_rpc_error_raises_error(self): 47 | responses.add( 48 | responses.POST, self.dummy_url, status=200, json={"error": "dummy error"} 49 | ) 50 | client = AuthServiceProxy(self.dummy_url) 51 | with pytest.raises(JSONRPCException): 52 | client.dummy_method() 53 | 54 | @responses.activate 55 | def test_connection_error(self): 56 | """Mock no connection to server error. 57 | """ 58 | responses.add(responses.POST, self.dummy_url, body=ConnectionError("")) 59 | client = AuthServiceProxy(self.dummy_url) 60 | with pytest.raises(JSONRPCException): 61 | client.get_balance() 62 | 63 | @responses.activate 64 | def test_calls_batch(self): 65 | for n in range(2): 66 | responses.add( 67 | responses.POST, 68 | self.dummy_url, 69 | status=200, 70 | json={"result": "dummy - {}".format(n)}, 71 | ) 72 | client = AuthServiceProxy(self.dummy_url) 73 | cases = [["dummy_method_1", {}], ["dummy_method_2", "dummy"]] 74 | results = client.batch_(cases) 75 | assert len(results) == len(cases) 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-monerorpc 2 | 3 | **python-monerorpc** is an improved version of python-jsonrpc for Monero (`monerod rpc`, `monero-wallet-rpc`). 4 | 5 | **python-monerorpc** was originally forked from [**python-bitcoinrpc**](https://github.com/jgarzik/python-bitcoinrpc). 6 | 7 | It includes the following generic improvements: 8 | 9 | - HTTP connections persist for the life of the `AuthServiceProxy` object using `requests.Session` 10 | - sends protocol 'jsonrpc', per JSON-RPC 2.0 11 | - sends proper, incrementing 'id' 12 | - uses standard Python json lib 13 | - can optionally log all RPC calls and results 14 | - JSON-2.0 batch support (mimicking batch) 15 | - JSON-2.0 batch doesn't seem to work with monero. 16 | - The batch functionality is mimicked and just requests the given methods one after another. 17 | - The result is a list of dictionaries. 18 | 19 | It also includes some more specific details: 20 | 21 | - sends Digest HTTP authentication headers 22 | - parses all JSON numbers that look like floats as Decimal, 23 | and serializes Decimal values to JSON-RPC connections. 24 | 25 | ## What does it do? 26 | 27 | **python-monerorpc** communicates with monero over RPC. 28 | 29 | That includes: 30 | 31 | - `monerod rpc` as well as 32 | - `monero-wallet-rpc`. 33 | 34 | **python-monerorpc** takes over the actual HTTP request containing all the necessary headers. 35 | 36 | ## Compared to similar projects: 37 | 38 | - [**monero-python**](https://github.com/emesik/monero-python) 39 | - **monero-python** 40 | - The module implements a json RPC backend (`monerod rpc`, `monero-wallet-rpc`). 41 | - It implements implementations around this backend (accounts, wallets, transactions, etc. ) 42 | - It offers helpful utilities like a monero wallet address validator. 43 | - A practical difference: 44 | 45 | - Should a RPC method change or a new one should be added, **monero-python** would have to adapt its backend and the implementations around it, while with **python-monerorpc** you just have to modify the property or use a new method like: 46 | 47 | ```python 48 | rpc_connection.getbalance() # -> rpc_connection.get_balance() 49 | rpc_connection.new_method() 50 | ``` 51 | 52 | ## Installation: 53 | 54 | ### From PyPI 55 | 56 | To install `python-monerorpc` from PyPI using `pip` you just need to: 57 | 58 | > \$ pip install python-monerorpc 59 | 60 | ### From Source 61 | 62 | > \$ python setup.py install --user 63 | 64 | **Note**: This will only install `monerorpc`. If you also want to install `jsonrpc` to preserve 65 | backwards compatibility, you have to replace `monerorpc` with `jsonrpc` in `setup.py` and run it again. 66 | 67 | ## Examples: 68 | 69 | Example usage `monerod` (get info): 70 | 71 | ```python 72 | from monerorpc.authproxy import AuthServiceProxy, JSONRPCException 73 | 74 | # initialisation, rpc_user and rpc_password are set as flags in the cli command 75 | rpc_connection = AuthServiceProxy('http://{0}:{1}@127.0.0.1:18081/json_rpc'.format(rpc_user, rpc_password)) 76 | 77 | info = rpc_connection.get_info() 78 | print(info) 79 | 80 | # rpc_user and rpc_password can also be left out (testing, develop, not recommended) 81 | rpc_connection = AuthServiceProxy('http://127.0.0.1:18081/json_rpc') 82 | ``` 83 | 84 | Example usage `monerod` (get network type): 85 | 86 | ```python 87 | from monerorpc.authproxy import AuthServiceProxy, JSONRPCException 88 | rpc_connection = AuthServiceProxy('http://{0}:{1}@127.0.0.1:18081/json_rpc'.format(rpc_user, rpc_password)) 89 | 90 | result = None 91 | network_type = None 92 | try: 93 | result = rpc_connection.get_info() 94 | except (requests.HTTPError, 95 | requests.ConnectionError, 96 | JSONRPCException) as e: 97 | logger.error('RPC Error on getting address' + str(e)) 98 | logger.exception(e) 99 | # Check network type 100 | network_type = result.get('nettype') 101 | if not network_type: 102 | raise ValueError('Error with: {0}'.format(result)) 103 | print(network_type) 104 | ``` 105 | 106 | Example usage `monerod` (on get block hash): 107 | 108 | ```python 109 | from monerorpc.authproxy import AuthServiceProxy, JSONRPCException 110 | rpc_connection = AuthServiceProxy('http://{0}:{1}@127.0.0.1:18081/json_rpc'.format(rpc_user, rpc_password)) 111 | 112 | params = [2] 113 | hash = rpc.on_get_block_hash(params) 114 | print(hash) 115 | ``` 116 | 117 | Example usage `monero-wallet-rpc` (get balance): 118 | 119 | ```python 120 | from monerorpc.authproxy import AuthServiceProxy, JSONRPCException 121 | 122 | # initialisation, rpc_user and rpc_password are set as flags in the cli command 123 | rpc_connection = AuthServiceProxy('http://{0}:{1}@127.0.0.1:18083/json_rpc'.format(rpc_user, rpc_password)) 124 | 125 | balance = rpc_connection.get_balance() 126 | print(balance) 127 | ``` 128 | 129 | Example usage `monero-wallet-rpc` (make transfer): 130 | 131 | ```python 132 | from monerorpc.authproxy import AuthServiceProxy, JSONRPCException 133 | 134 | # initialisation, rpc_user and rpc_password are set as flags in the cli command 135 | rpc_connection = AuthServiceProxy('http://{0}:{1}@127.0.0.1:18083/json_rpc'.format(rpc_user, rpc_password)) 136 | 137 | destinations = {"destinations": [{"address": "some_address", "amount": 1}], "mixin": 10} 138 | result = rpc_connection.transfer(destinations) 139 | print(result) 140 | ``` 141 | 142 | Example usage `monero-wallet-rpc` (batch): 143 | 144 | ```python 145 | from monerorpc.authproxy import AuthServiceProxy, JSONRPCException 146 | import pprint 147 | 148 | # initialisation, rpc_user and rpc_password are set as flags in the cli command 149 | rpc_connection = AuthServiceProxy('http://{0}:{1}@127.0.0.1:18083/json_rpc'.format(rpc_user, rpc_password)) 150 | 151 | # some example batch 152 | params={"account_index":0,"address_indices":[0,1]} 153 | result = rpc.batch_([ ["get_balance"], ["get_balance", params] ]) 154 | pprint.pprint(result) 155 | 156 | # make transfer and get balance in a batch 157 | destinations = {"destinations": [{"address": "some_address", "amount": 1}], "mixin": 10} 158 | result = rpc.batch_([ ["transfer", destinations], ["get_balance"] ]) 159 | pprint.pprint(result) 160 | ``` 161 | 162 | ## Logging: 163 | 164 | Logging all RPC calls to stderr: 165 | 166 | ```python 167 | from monerorpc.authproxy import AuthServiceProxy, JSONRPCException 168 | import logging 169 | 170 | logging.basicConfig() 171 | logging.getLogger("MoneroRPC").setLevel(logging.DEBUG) 172 | 173 | rpc_connection = AuthServiceProxy('http://{0}:{1}@127.0.0.1:18081/json_rpc'.format(rpc_user, rpc_password)) 174 | 175 | print(rpc_connection.get_info()) 176 | ``` 177 | 178 | Produces output on stderr like: 179 | 180 | ```bash 181 | DEBUG:MoneroRPC:-1-> get_info [] 182 | DEBUG:MoneroRPC:<-1- {u'result': {u'incoming_connections_count': 0, ...etc } 183 | ``` 184 | 185 | ## Errors: 186 | 187 | Possible errors and error codes: 188 | 189 | * `no code` 190 | - Returns the `error` contained in the RPC response. 191 | * `-341` 192 | - `could not establish a connection, original error: {}` 193 | - including the original exception message 194 | * `-342` 195 | - `missing HTTP response from server` 196 | * `-343` 197 | - `missing JSON-RPC result` 198 | * `-344` 199 | - `received HTTP status code {}` 200 | - including HTTP status code other than `200` 201 | 202 | ## Testing: 203 | 204 | Install the test requirements: 205 | ```bash 206 | virtualenv -q venv 207 | . venv/bin/activate 208 | pip install -r requirements.txt 209 | ``` 210 | 211 | Run unit tests using `pytest`: 212 | ```bash 213 | # virtualenv activated (see above) 214 | pytest tests.py 215 | ``` 216 | 217 | ## Authors 218 | 219 | * **Norman Moeschter-Schenck** - *Initial work* - [normoes](https://github.com/normoes) 220 | 221 | See also the list of [contributors](contributors.md) who participated in this project. 222 | -------------------------------------------------------------------------------- /monerorpc/authproxy.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright 2011 Jeff Garzik 3 | 4 | Forked by Norman Schenck from python-bitcoinrpc in 09/2018. 5 | python-monerorpc is based on this fork. 6 | 7 | 8 | AuthServiceProxy has the following improvements over python-jsonrpc's 9 | ServiceProxy class: 10 | 11 | - HTTP connections persist for the life of the AuthServiceProxy object 12 | (if server supports HTTP/1.1) 13 | - sends protocol 'jsonrpc', per JSON-RPC 2.0 14 | - sends proper, incrementing 'id' 15 | - sends Digest HTTP authentication headers 16 | - parses all JSON numbers that look like floats as Decimal 17 | - uses standard Python json lib 18 | 19 | Previous copyright, from python-jsonrpc/jsonrpc/proxy.py: 20 | 21 | Copyright (c) 2007 Jan-Klaas Kollhof 22 | 23 | This file is part of jsonrpc. 24 | 25 | jsonrpc is free software; you can redistribute it and/or modify 26 | it under the terms of the GNU Lesser General Public License as published by 27 | the Free Software Foundation; either version 2.1 of the License, or 28 | (at your option) any later version. 29 | 30 | This software is distributed in the hope that it will be useful, 31 | but WITHOUT ANY WARRANTY; without even the implied warranty of 32 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 33 | GNU Lesser General Public License for more details. 34 | 35 | You should have received a copy of the GNU Lesser General Public License 36 | along with this software; if not, write to the Free Software 37 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 38 | """ 39 | 40 | from requests import auth, Session, codes 41 | from requests.exceptions import ConnectionError 42 | import decimal 43 | import json 44 | import logging 45 | 46 | try: 47 | import urllib.parse as urlparse 48 | except ImportError: 49 | import urlparse 50 | 51 | USER_AGENT = "AuthServiceProxy/0.1" 52 | 53 | HTTP_TIMEOUT = 30 54 | 55 | log = logging.getLogger("MoneroRPC") 56 | 57 | 58 | class JSONRPCException(Exception): 59 | def __init__(self, rpc_error): 60 | parent_args = [] 61 | try: 62 | parent_args.append(rpc_error["message"]) 63 | except Exception: 64 | pass 65 | Exception.__init__(self, *parent_args) 66 | self.error = rpc_error 67 | self.code = rpc_error["code"] if "code" in rpc_error else None 68 | self.message = rpc_error["message"] if "message" in rpc_error else None 69 | 70 | def __str__(self): 71 | return "%d: %s" % (self.code, self.message) 72 | 73 | def __repr__(self): 74 | return "<%s '%s'>" % (self.__class__.__name__, self) 75 | 76 | 77 | def EncodeDecimal(o): 78 | if isinstance(o, decimal.Decimal): 79 | return float(round(o, 12)) 80 | raise TypeError(repr(o) + " is not JSON serializable") 81 | 82 | 83 | class AuthServiceProxy(object): 84 | """Extension of python-jsonrpc 85 | to communicate with Monero (monerod, monero-wallet-rpc) 86 | """ 87 | 88 | __id_count = 0 89 | 90 | def __init__( 91 | self, service_url, service_name=None, timeout=HTTP_TIMEOUT, connection=None 92 | ): 93 | """ 94 | :param service_url: http://user:passwd@host:port/json_rpc" 95 | :param service_name: method name of monero wallet RPC and monero daemon RPC 96 | """ 97 | self.__service_url = service_url 98 | self.__service_name = service_name 99 | self.__timeout = timeout 100 | self.__url = urlparse.urlparse(service_url) 101 | if self.__url.port is None: 102 | port = 80 103 | else: 104 | port = self.__url.port 105 | 106 | self.__rpc_url = ( 107 | self.__url.scheme 108 | + "://" 109 | + self.__url.hostname 110 | + ":" 111 | + str(port) 112 | + self.__url.path 113 | ) 114 | 115 | (user, passwd) = (self.__url.username, self.__url.password) 116 | 117 | # Digest Authentication 118 | authentication = None 119 | log.debug("{0}, {1}".format(user, passwd)) 120 | if user is not None and passwd is not None: 121 | authentication = auth.HTTPDigestAuth(user, passwd) 122 | 123 | headers = { 124 | "Content-Type": "application/json", 125 | "User-Agent": USER_AGENT, 126 | "Host": self.__url.hostname, 127 | } 128 | 129 | if connection: 130 | # Callables re-use the connection of the original proxy 131 | self.__conn = connection 132 | else: 133 | self.__conn = Session() 134 | self.__conn.auth = authentication 135 | self.__conn.headers = headers 136 | 137 | def __getattr__(self, name): 138 | if name.startswith("__") and name.endswith("__"): 139 | # Python internal stuff 140 | raise AttributeError 141 | if self.__service_name is not None: 142 | name = "{0}.{1}".format(self.__service_name, name) 143 | return AuthServiceProxy(self.__service_url, name, connection=self.__conn) 144 | 145 | def __call__(self, *args): 146 | AuthServiceProxy.__id_count += 1 147 | 148 | log.debug( 149 | "-{0}-> {1} {2}".format( 150 | AuthServiceProxy.__id_count, 151 | self.__service_name, 152 | json.dumps(args, default=EncodeDecimal), 153 | ) 154 | ) 155 | # args is tuple 156 | # monero RPC always gets one dictionary as parameter 157 | if args: 158 | args = args[0] 159 | 160 | postdata = json.dumps( 161 | { 162 | "jsonrpc": "2.0", 163 | "method": self.__service_name, 164 | "params": args, 165 | "id": AuthServiceProxy.__id_count, 166 | }, 167 | default=EncodeDecimal, 168 | ) 169 | return self._request(postdata) 170 | 171 | def batch_(self, rpc_calls): 172 | """Batch RPC call. 173 | Pass array of arrays: [ [ "method", params... ], ... ] 174 | Returns array of results. 175 | 176 | No real implementation of JSON RPC batch. 177 | Only requesting every method one after another. 178 | """ 179 | results = list() 180 | for rpc_call in rpc_calls: 181 | method = rpc_call.pop(0) 182 | params = rpc_call.pop(0) if rpc_call else dict() 183 | results.append(self.__getattr__(method)(params)) 184 | 185 | return results 186 | 187 | def _request(self, postdata): 188 | log.debug("--> {}".format(postdata)) 189 | try: 190 | r = self.__conn.post( 191 | url=self.__rpc_url, data=postdata, timeout=self.__timeout 192 | ) 193 | except (ConnectionError) as e: 194 | raise JSONRPCException( 195 | { 196 | "code": -341, 197 | "message": "could not establish a connection, original error: {}".format( 198 | str(e) 199 | ), 200 | } 201 | ) 202 | 203 | response = self._get_response(r) 204 | if response.get("error", None) is not None: 205 | raise JSONRPCException(response["error"]) 206 | elif "result" not in response: 207 | raise JSONRPCException({"code": -343, "message": "missing JSON-RPC result"}) 208 | else: 209 | return response["result"] 210 | 211 | def _get_response(self, r): 212 | if r.status_code != codes.ok: 213 | raise JSONRPCException( 214 | { 215 | "code": -344, 216 | "message": "received HTTP status code {}".format(r.status_code), 217 | } 218 | ) 219 | http_response = r.text 220 | if http_response is None: 221 | raise JSONRPCException( 222 | {"code": -342, "message": "missing HTTP response from server"} 223 | ) 224 | 225 | response = json.loads(http_response, parse_float=decimal.Decimal) 226 | if "error" in response and response.get("error", None) is None: 227 | log.error("error: {}".format(response)) 228 | log.debug( 229 | "<-{0}- {1}".format( 230 | response["id"], 231 | json.dumps(response["result"], default=EncodeDecimal), 232 | ) 233 | ) 234 | else: 235 | log.debug("<-- {}".format(response)) 236 | return response 237 | --------------------------------------------------------------------------------