# Copyright (c) 2014-2017 esotericnonsense (Daniel Edgecumbe) # Distributed under the MIT software license, see the accompanying # file COPYING or https://opensource.org/licenses/mit-license.php import aiohttp import asyncio import async_timeout import base64 import os try: import ujson as json except ImportError: import json import config def craft_url(proto, ip, port): return "{}://{}:{}".format(proto, ip, port) def get_url_from_datadir(datadir): configfile = os.path.join(datadir, "bitcoin.conf") try: cfg = config.parse_file(configfile) except IOError: return craft_url("http", "localhost", 8332) proto = cfg["protocol"] if "protocol" in cfg else "http" ip = cfg["rpcip"] if "rpcip" in cfg else "localhost" try: port = cfg["rpcport"] except KeyError: # If both regtest and testnet are set, bitcoind will not run. if "regtest" in cfg and cfg["regtest"] == "1": port = 18332 elif "testnet" in cfg and cfg["testnet"] == "1": port = 18332 else: port = 8332 return craft_url(proto, ip, port) def get_auth_from_datadir(datadir): def craft_auth_from_credentials(user, password): details = ":".join([user, password]) return base64.b64encode(bytes(details, "utf-8")).decode("utf-8") def get_auth_from_cookiefile(cookiefile): # Raises IOError if file does not exist with open(cookiefile, "r") as f: return base64.b64encode(bytes(f.readline(), "utf-8")).decode("utf-8") cookiefile = os.path.join(datadir, ".cookie") try: return get_auth_from_cookiefile(cookiefile) except FileNotFoundError: print("cookiefile not found, falling back to password authentication") # Fall back to credential-based authentication configfile = os.path.join(datadir, "bitcoin.conf") try: cfg = config.parse_file(configfile) except IOError: print("configuration file not found; aborting.") raise try: rpcuser = cfg["rpcuser"] rpcpassword = cfg["rpcpassword"] except KeyError: if not ("rpcuser" in cfg): print("rpcuser not in configuration file.") if not ("rpcpassword" in cfg): print("rpcpassword not in configuration file.") raise return craft_auth_from_credentials(rpcuser, rpcpassword) class RPCError(BaseException): """ Parent class for the RPC errors. """ pass class RPCContentError(RPCError): # TODO: include the error code, etc. pass class RPCTimeoutError(RPCError): # TODO: include the timeout value? pass class RPCConnectionError(RPCError): pass class BitcoinRPCClient(object): def __init__(self, url, auth): self._url = url self._headers = { "Authorization": "Basic {}".format(auth), "Content-Type": "text/plain", } @staticmethod async def _craft_request(req, params, ident): d = { # "jsonrpc": "2.0", # Currently ignored by Bitcoin Core. "method": req, } if params is not None: d["params"] = params if ident is not None: d["id"] = ident return json.dumps(d) async def _fetch(self, session, req): try: with async_timeout.timeout(5): async with session.post(self._url, headers=self._headers, data=req) as response: return await response.text() except asyncio.TimeoutError: raise RPCTimeoutError except aiohttp.client_exceptions.ClientOSError: raise RPCConnectionError @staticmethod async def _json_loads(j): return json.loads(j) async def request(self, method, params=None, ident=None, callback=None): async with aiohttp.ClientSession() as session: req = await self._craft_request(method, params, ident) j = await self._fetch(session, req) d = await self._json_loads(j) try: error = d["error"] except KeyError: raise RPCContentError("RPC response seems malformed (no error field)") if error is not None: # TODO: pass the error up the stack; tweak RPCError raise RPCContentError("RPC response returned error {}".format(error)) try: result = d["result"] except KeyError: raise RPCContentError("RPC response seems malformed (no result field)") if result is None: # Is there a case in which a query can return None? raise RPCContentError("RPC response returned a null result") return d