You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
bitcoind-ncurses2/rpc.py

164 lines
4.7 KiB
Python

# 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