diff options
-rw-r--r-- | rpcblockchainexplorer/api.py | 152 | ||||
-rw-r--r-- | rpcblockchainexplorer/config.py | 20 | ||||
-rw-r--r-- | rpcblockchainexplorer/flaskapp.py | 82 | ||||
-rw-r--r-- | rpcblockchainexplorer/rpc.py | 18 | ||||
-rw-r--r-- | run.py | 3 |
5 files changed, 275 insertions, 0 deletions
diff --git a/rpcblockchainexplorer/api.py b/rpcblockchainexplorer/api.py new file mode 100644 index 0000000..d7a6d32 --- /dev/null +++ b/rpcblockchainexplorer/api.py @@ -0,0 +1,152 @@ +""" +Autogenerate API endpoints by inspecting python-bitcoinlib's RPC client because +maximum lazy. +""" + +from copy import copy + +# HACK: view_func is weird +import types + +# For using python-bitcoinlib for details about the Bitcoin Core RPC protocol. +import inspect + +# For some even more devious trickery involving partilaly-pre-specified +# function calls. +import functools + +# Proxy class is used to get information about accepted parameters. Yeah, +# pretty lame but it works. +from bitcoin.rpc import Proxy + +# because of the interwebs... +from flask import ( + Blueprint, + request, + g, +) + +api = Blueprint("api", __name__, url_prefix="") + +ALLOWED_COMMANDS = [ + # blockchain + "getbestblockhash", + "getblock", + "getblockchaininfo", + "getblockcount", + "getblockhash", + "getchaintips", + "getdifficulty", + "getmempoolinfo", + "gettxout", + + # network + "getconnectioncount", + "getnettotals", + "getnetworkinfo", + "getpeerinfo", + + # transactions + "getrawtransaction", + "decoderawtransaction", +] + +# All API endpoints conform to the following template, which can also be used +# to construct valid urls. +API_ENDPOINT_TEMPLATE = "/{command_name}" + +def create_api_endpoints(commands=ALLOWED_COMMANDS): + """ + Automatically generate all API endpoints by (ab)using python-bitcoinlib. + """ + + # Only generate API endpoints for the explicitly whitelisted commands. + for command in commands: + + # store any additional arguments here + keyword_arguments = {} + + if command in Proxy.__dict__.keys(): + # get a reference to the function + rpcfunction = Proxy.__dict__[command] + + # investigate the function signature + argument_spec = inspect.getargspec(rpcfunction) + + # only look at the arguments + arguments = argument_spec.args + + # nobody cares about the self + arguments.remove("self") + + # "self" is never a default value + defaults = argument_spec.defaults + + # Preserve this information for later when constructing the API + # endpoints. + for (argument_index, argument) in enumerate(arguments): + # Perhaps there are not always default values? Who knows. + if defaults and len(defaults) > argument_index: + some_default = defaults[argument_index] + else: + some_default = None + + keyword_arguments[argument] = some_default + + # Construct an API endpoint that accepts the computed arguments. Begin + # by constructing the API endpoint uri from the template. + + # endpoint is always based on the command name + api_uri = API_ENDPOINT_TEMPLATE.format(command_name=command) + + def some_command_endpoint(): + """ + Autogenerated API endpoint. + """ + + # Allow the defaults or user-passed values to be used. + params = {} + + # Use request.args to get the user-passed value, otherwise use the + # default as defined in python-bitcoinlib. + for (argument_name, default_value) in keyword_arguments.items(): + params[argument_name] = request.args.get(argument_name, default_value) + + # Get a reference to the command, if any. + if command in Proxy.__dict__.keys(): + callable_function = g.bitcoin_rpc_client.__dict__[command] + rpc_function = functools.partial(callable_function) + else: + callable_function = g.bitcoin_rpc_client._call + _self = g.bitcoin_rpc_client + rpc_function = functools.partial(callable_function, command) + + # Call the RPC service command and pass in all of the given + # parameters. + results = rpc_function(**params) + + # That's all, folks. + return repr(results) + + #temporary_function = types.FunctionType( + # # TODO: use func_name in python2 + # copy(some_command_endpoint.__code__), + # + # # use func_globals in python2 + # copy(some_command_endpoint.__globals__), + # + # name=command, + # + # # use func_defaults in python2 + # argdefs=copy(some_command_endpoint.__defaults__), + # + # # use just "closure" in python2 + # closure=copy(some_command_endpoint.__closure__), + #) + + api.add_url_rule(api_uri, endpoint=command, view_func=some_command_endpoint, methods=["GET"]) + +# Always create all the endpoints when importing this module. This will attach +# the endpoints to the blueprint, which can then be attached to flask +# application instances. +create_api_endpoints() diff --git a/rpcblockchainexplorer/config.py b/rpcblockchainexplorer/config.py new file mode 100644 index 0000000..020f36b --- /dev/null +++ b/rpcblockchainexplorer/config.py @@ -0,0 +1,20 @@ +""" +Application configuration. +""" + +import os + +# use python-bitcoinlib +import bitcoin + +DEFAULT_BITCOIN_MODE = "regtest" + +# Set python-bitcoinlib mode to the same as bitcoind's mode, based on +# environment variable and a default in its absence. +BITCOIN_MODE = os.environ.get("BITCOIN_MODE", DEFAULT_BITCOIN_MODE) + +# python-bitcoinlib has a mode :-( +bitcoin.SelectParams(BITCOIN_MODE) + +# flask application name +DEFAULT_APP_NAME = "rpcblockchainexplorer" diff --git a/rpcblockchainexplorer/flaskapp.py b/rpcblockchainexplorer/flaskapp.py new file mode 100644 index 0000000..afee252 --- /dev/null +++ b/rpcblockchainexplorer/flaskapp.py @@ -0,0 +1,82 @@ +""" +Web application gears. +""" + +# use default logging framework +import logging + +from flask import ( + # need to make a flask application + Flask, + + # request handler thing + g, +) + +# and the application must have a name +from .config import DEFAULT_APP_NAME + +# used to create an rpc client on every request +from .rpc import get_bitcoin_rpc_client + +# get the only blueprint that matters in this app +from .api import api + +DEFAULT_BLUEPRINTS = [ + api, +] + +def create_app(config=None, app_name=DEFAULT_APP_NAME, blueprints=DEFAULT_BLUEPRINTS): + """ + Construct a flask application object. + """ + + app = Flask(app_name) + + configure_app(app, config) + configure_logging(app) + configure_hook(app) + configure_blueprints(app, blueprints) + + return app + +def configure_app(app, config=None): + """ + Really simple configuration for flask applications. + """ + + if config: + app.config.from_object(config) + elif not app.config.get("testing"): + app.debug = True + +def configure_logging(app): + """ + Use python logging. + """ + + # no reason to hide messages yet + app.logger.setLevel(logging.DEBUG) + +def configure_hook(app): + """ + Setup the before_request hook. + """ + + # http://flask.pocoo.org/docs/0.10/api/#flask.Blueprint.before_request + @app.before_request + def before_request(): + """ + Make a new bitcoin RPC client instance for every request. + """ + + # store client on request context object + g.bitcoin_rpc_client = get_bitcoin_rpc_client() + +def configure_blueprints(app, blueprints): + """ + Setup flask blueprints on the flask application. + """ + + for blueprint in blueprints: + app.register_blueprint(blueprint) diff --git a/rpcblockchainexplorer/rpc.py b/rpcblockchainexplorer/rpc.py new file mode 100644 index 0000000..75a33e9 --- /dev/null +++ b/rpcblockchainexplorer/rpc.py @@ -0,0 +1,18 @@ +""" +RPC connection details. +""" + +# use python-bitcoinlib for RPC +import bitcoin + +from .config import BITCOIN_MODE + +def get_bitcoin_rpc_client(): + """ + Create a new RPC connection to bitcoind. + """ + + # use local bitcoind config from ~/.bitcoin/bitcoin.conf + bitcoin_rpc_client = bitcoin.rpc.Proxy() + + return bitcoin_rpc_client @@ -0,0 +1,3 @@ +from rpcblockchainexplorer.flaskapp import create_app +app = create_app() +app.run(debug=True) |