Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] API Redesign #1607

Closed
wants to merge 62 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
a690940
introduce 'restapi.py'
Ouziel Apr 5, 2024
00181f9
DRY routes
Ouziel Apr 5, 2024
110e5c8
refactor 'get_asset_info()'
Ouziel Apr 5, 2024
ba61877
Add API authentification
Ouziel Apr 5, 2024
893c86e
move old api in v1/; rename restapi.py to api.py
Ouziel Apr 5, 2024
c1bf32b
--legacy-api -> --enable-v1-api
Ouziel Apr 5, 2024
ad47e52
lint
Ouziel Apr 5, 2024
3d434cf
Add routes for blocks
Ouziel Apr 5, 2024
a222bb8
fix tests
Ouziel Apr 5, 2024
a0d07f4
Add tests for new API
Ouziel Apr 5, 2024
4e6cc2d
use app context instead global variables
Ouziel Apr 5, 2024
840633e
tweaks
Ouziel Apr 5, 2024
6e75205
More routes for blocks, credits and debits
Ouziel Apr 6, 2024
b275944
clean imports
Ouziel Apr 6, 2024
6f4ffeb
More routes
Ouziel Apr 6, 2024
3ed4ba7
More routes: sends, resolutions, ..
Ouziel Apr 7, 2024
83bd420
Add 'event' field in mempool table
Ouziel Apr 7, 2024
38fb161
More routes: dispensers, sweeps, holders, ..
Ouziel Apr 7, 2024
092b312
Add routes for events
Ouziel Apr 7, 2024
46ebe88
Add routes for mempool and events count
Ouziel Apr 7, 2024
f8cc62c
module in subfolder
Ouziel Apr 7, 2024
313b747
Add route to compose transactions
Ouziel Apr 7, 2024
d699408
Add route for healthz and get_tx_info
Ouziel Apr 8, 2024
7d956f6
Add backend proxy routes
Ouziel Apr 8, 2024
d890979
Add route for root
Ouziel Apr 8, 2024
1e3a226
fix tests
Ouziel Apr 8, 2024
c506c0a
inject headers
Ouziel Apr 8, 2024
7cb5a40
Progress in unpack support
Ouziel Apr 8, 2024
71cfb0c
Add route to unpack; supports all type of message
Ouziel Apr 9, 2024
a2878c6
update ledger.CURRENT_BLOCK_INDEX on each request
Ouziel Apr 9, 2024
981d3a5
new flags and config values for the new API
Ouziel Apr 9, 2024
91e69d1
fix tests
Ouziel Apr 9, 2024
9fb4df0
lint
Ouziel Apr 9, 2024
b1b86ac
fix sql query
Ouziel Apr 9, 2024
590b965
Fix api v1 shutdown; Fix typos
Ouziel Apr 9, 2024
bc30b79
Put API v1 behind '/old'
Ouziel Apr 9, 2024
7a403e4
Add X-API-Warn header
Ouziel Apr 9, 2024
d8b4b63
use 'flask_cors' for cors
Ouziel Apr 9, 2024
861411c
fix config.RPC_WEBROOT
Ouziel Apr 9, 2024
366a060
new routes for mempool
Ouziel Apr 9, 2024
7916b62
API_NOT_READY_HTTP_CODE=503 by default
Ouziel Apr 9, 2024
9ca45d6
check -> check_type
Ouziel Apr 9, 2024
f27531c
lint
Ouziel Apr 9, 2024
4b0a7a6
fix rebase
Ouziel Apr 10, 2024
a4b4fd3
fix ruff alert
Ouziel Apr 10, 2024
45998bd
Fix log in file
Ouziel Apr 10, 2024
d607602
Add warning when using API v1
Ouziel Apr 10, 2024
2945b07
fix duplicate sigterm catch
Ouziel Apr 10, 2024
4a5b02e
Merge branch 'develop' into newapi
Ouziel Apr 17, 2024
69c522d
fix merge
Ouziel Apr 17, 2024
3e72e4d
Merge branch 'develop' into newapi
Ouziel Apr 17, 2024
d3d62ac
fix merge
Ouziel Apr 17, 2024
9cc97c1
Fix /healthz endpoint after merging
Ouziel Apr 17, 2024
68e7f1e
new route for compose: /address/<source>/compose/<transaction_name>
Ouziel Apr 17, 2024
07f933c
Add type to routes args
Ouziel Apr 17, 2024
3cc5406
Add type hints to all compose functions
Ouziel Apr 17, 2024
7474f5b
Routes args from function signature; Routes doc from function docstring
Ouziel Apr 18, 2024
c2dea42
Progress in routes documentation
Ouziel Apr 18, 2024
c22ee5f
All routes documented except compose
Ouziel Apr 18, 2024
1c3b049
One route, one function. Even for compose functions
Ouziel Apr 19, 2024
d89d5f3
progress in compose routes
Ouziel Apr 19, 2024
d989d7f
finish compose functions and routes
Ouziel Apr 19, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 37 additions & 56 deletions counterparty-core/counterpartycore/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from termcolor import cprint

from counterpartycore import server
from counterpartycore.lib import config, log, setup
from counterpartycore.lib import config, setup

logger = logging.getLogger(config.LOGGER_NAME)

Expand Down Expand Up @@ -170,6 +170,35 @@
"help": f"number of RPC queries by batch (default: {config.DEFAULT_RPC_BATCH_SIZE})",
},
],
[
("--api-host",),
{
"default": "localhost",
"help": "the IP of the interface to bind to for providing API access (0.0.0.0 for all interfaces)",
},
],
[
("--api-port",),
{"type": int, "help": f"port on which to provide the {config.APP_NAME} API"},
],
[
("--api-user",),
{
"default": "api",
"help": f"required username to use the {config.APP_NAME} API (via HTTP basic auth)",
},
],
[
("--api-password",),
{
"default": "api",
"help": f"required password (for rpc-user) to use the {config.APP_NAME} API (via HTTP basic auth)",
},
],
[
("--api-no-allow-cors",),
{"action": "store_true", "default": False, "help": "allow ajax cross domain request"},
],
[
("--requests-timeout",),
{
Expand Down Expand Up @@ -204,6 +233,10 @@
"help": "log API requests to the specified file",
},
],
[
("--enable-api-v1",),
{"action": "store_true", "default": False, "help": "Enable the API v1"},
],
[
("--no-log-files",),
{"action": "store_true", "default": False, "help": "Don't write log files"},
Expand Down Expand Up @@ -368,60 +401,8 @@ def main():
parser.print_help()
exit(0)

# Configuration
init_args = dict(
database_file=args.database_file,
testnet=args.testnet,
testcoin=args.testcoin,
regtest=args.regtest,
customnet=args.customnet,
api_limit_rows=args.api_limit_rows,
backend_connect=args.backend_connect,
backend_port=args.backend_port,
backend_user=args.backend_user,
backend_password=args.backend_password,
backend_ssl=args.backend_ssl,
backend_ssl_no_verify=args.backend_ssl_no_verify,
backend_poll_interval=args.backend_poll_interval,
indexd_connect=args.indexd_connect,
indexd_port=args.indexd_port,
rpc_host=args.rpc_host,
rpc_port=args.rpc_port,
rpc_user=args.rpc_user,
rpc_password=args.rpc_password,
rpc_no_allow_cors=args.rpc_no_allow_cors,
requests_timeout=args.requests_timeout,
rpc_batch_size=args.rpc_batch_size,
check_asset_conservation=args.check_asset_conservation,
force=args.force,
p2sh_dust_return_pubkey=args.p2sh_dust_return_pubkey,
utxo_locks_max_addresses=args.utxo_locks_max_addresses,
utxo_locks_max_age=args.utxo_locks_max_age,
no_mempool=args.no_mempool,
skip_db_check=args.skip_db_check,
)

server.initialise_log_config(
verbose=args.verbose,
quiet=args.quiet,
log_file=args.log_file,
api_log_file=args.api_log_file,
no_log_files=args.no_log_files,
testnet=args.testnet,
testcoin=args.testcoin,
regtest=args.regtest,
json_log=args.json_log,
)

# set up logging
log.set_up(
verbose=config.VERBOSE,
quiet=config.QUIET,
log_file=config.LOG,
log_in_console=args.action == "start",
)

server.initialise_config(**init_args)
# Configuration and logging
server.initialise_log_and_config(args)

logger.info(f"Running v{APP_VERSION} of {APP_NAME}.")

Expand All @@ -447,7 +428,7 @@ def main():
)

elif args.action == "start":
server.start_all(catch_up=args.catch_up)
server.start_all(args)

elif args.action == "show-params":
server.show_params()
Expand Down
192 changes: 192 additions & 0 deletions counterparty-core/counterpartycore/lib/api/api_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
import argparse
import logging
import multiprocessing
import signal
from multiprocessing import Process
from threading import Timer

import flask
from counterpartycore import server
from counterpartycore.lib import (
blocks,
config,
database,
ledger,
)
from counterpartycore.lib.api.routes import ROUTES
from counterpartycore.lib.api.util import get_backend_height, init_api_access_log, remove_rowids
from flask import Flask, request
from flask import g as flask_globals
from flask_cors import CORS
from flask_httpauth import HTTPBasicAuth

multiprocessing.set_start_method("spawn", force=True)

logger = logging.getLogger(config.LOGGER_NAME)
auth = HTTPBasicAuth()

BACKEND_HEIGHT = 0
REFRESH_BACKEND_HEIGHT_INTERVAL = 10
BACKEND_HEIGHT_TIMER = None


def get_db():
"""Get the database connection."""
if not hasattr(flask_globals, "db"):
flask_globals.db = database.get_connection(read_only=True)
return flask_globals.db


@auth.verify_password
def verify_password(username, password):
return username == config.API_USER and password == config.API_PASSWORD


def api_root():
counterparty_height = blocks.last_db_index(get_db())
routes = []
for path in ROUTES:

Check warning

Code scanning / pylint

Consider iterating with .items(). Warning

Consider iterating with .items().
route = ROUTES[path]
routes.append(
{
"path": path,
"args": route.get("args", []),
"description": route.get("description", ""),
}
)
network = "mainnet"
if config.TESTNET:
network = "testnet"
elif config.REGTEST:
network = "regtest"
elif config.TESTCOIN:
network = "testcoin"
return {
"server_ready": counterparty_height >= BACKEND_HEIGHT,
"network": network,
"version": config.VERSION_STRING,
"backend_height": BACKEND_HEIGHT,
"counterparty_height": counterparty_height,
"routes": routes,
}


def inject_headers(result, return_code=None):
server_ready = ledger.CURRENT_BLOCK_INDEX >= BACKEND_HEIGHT
http_code = 200
if return_code:
http_code = return_code
elif not server_ready:
http_code = config.API_NOT_READY_HTTP_CODE
if isinstance(result, flask.Response):
response = result
else:
response = flask.make_response(flask.jsonify(result), http_code)

Check warning

Code scanning / CodeQL

Information exposure through an exception Medium

Stack trace information
flows to this location and may be exposed to an external user.
response.headers["X-COUNTERPARTY-HEIGHT"] = ledger.CURRENT_BLOCK_INDEX
response.headers["X-COUNTERPARTY-READY"] = ledger.CURRENT_BLOCK_INDEX >= BACKEND_HEIGHT
response.headers["X-BACKEND-HEIGHT"] = BACKEND_HEIGHT
return response


def prepare_args(route, **kwargs):
function_args = dict(kwargs)
if "pass_all_args" in route and route["pass_all_args"]:
function_args = request.args | function_args
elif "args" in route:
for arg in route["args"]:
arg_name = arg["name"]
if arg_name in function_args:
continue
str_arg = request.args.get(arg_name)
if str_arg is None and arg["required"]:
raise ValueError(f"Missing required parameter: {arg_name}")
if str_arg is None:
function_args[arg_name] = arg["default"]
elif arg["type"] == "bool":
function_args[arg_name] = str_arg.lower() in ["true", "1"]
elif arg["type"] == "int":
try:
function_args[arg_name] = int(str_arg)
except ValueError as e:
raise ValueError(f"Invalid integer: {arg_name}") from e
else:
function_args[arg_name] = str_arg
return function_args


@auth.login_required
def handle_route(**kwargs):
db = get_db()
# update the current block index
ledger.CURRENT_BLOCK_INDEX = blocks.last_db_index(db)
rule = str(request.url_rule.rule)
if rule == "/":
result = api_root()
else:
route = ROUTES.get(rule)
try:
function_args = prepare_args(route, **kwargs)
except ValueError as e:
return inject_headers({"error": str(e)}, return_code=400)
result = route["function"](db, **function_args)
result = remove_rowids(result)
return inject_headers(result)


def run_api_server(args):
# default signal handlers
signal.signal(signal.SIGTERM, signal.SIG_DFL)
signal.signal(signal.SIGINT, signal.default_int_handler)

app = Flask(config.APP_NAME)
# Initialise log and config
server.initialise_log_and_config(argparse.Namespace(**args))
with app.app_context():
if not config.API_NO_ALLOW_CORS:
CORS(app)
# Initialise the API access log
init_api_access_log(app)
# Get the last block index
ledger.CURRENT_BLOCK_INDEX = blocks.last_db_index(get_db())
# Add routes
app.add_url_rule("/", view_func=handle_route)
for path in ROUTES:
app.add_url_rule(path, view_func=handle_route)
# run the scheduler to refresh the backend height
# `no_refresh_backend_height` used only for testing. TODO: find a way to mock it
if "no_refresh_backend_height" not in args or not args["no_refresh_backend_height"]:
refresh_backend_height()
try:
# Start the API server
app.run(host=config.API_HOST, port=config.API_PORT, debug=False)
finally:
# ensure timer is cancelled
if BACKEND_HEIGHT_TIMER:
BACKEND_HEIGHT_TIMER.cancel()


def refresh_backend_height():
global BACKEND_HEIGHT, BACKEND_HEIGHT_TIMER # noqa F811

Check warning

Code scanning / pylint

Using the global statement. Warning

Using the global statement.
BACKEND_HEIGHT = get_backend_height()
if BACKEND_HEIGHT_TIMER:
BACKEND_HEIGHT_TIMER.cancel()
BACKEND_HEIGHT_TIMER = Timer(REFRESH_BACKEND_HEIGHT_INTERVAL, refresh_backend_height)
BACKEND_HEIGHT_TIMER.start()


class APIServer(object):

Check warning

Code scanning / pylint

Class 'APIServer' inherits from object, can be safely removed from bases in python3. Warning

Class 'APIServer' inherits from object, can be safely removed from bases in python3.
def __init__(self):
self.process = None

def start(self, args):
if self.process is not None:
raise Exception("API server is already running")

Check warning

Code scanning / pylint

Raising too general exception: Exception. Warning

Raising too general exception: Exception.
self.process = Process(target=run_api_server, args=(vars(args),))
self.process.start()
return self.process

def stop(self):
logger.info("Stopping API server v2...")
if self.process and self.process.is_alive():
self.process.terminate()
self.process = None
Loading
Loading