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

API Redesign #1716

Closed
wants to merge 117 commits into from
Closed
Show file tree
Hide file tree
Changes from 96 commits
Commits
Show all changes
117 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
ae7dfca
Merge branch 'newapi' into develop
Ouziel Apr 19, 2024
d5f6d43
fix merge
Ouziel Apr 19, 2024
881505e
disable missing params check for API v1
Ouziel Apr 19, 2024
e91815c
fixes
Ouziel Apr 19, 2024
234f989
more tests; fixes
Ouziel Apr 22, 2024
1ec5960
compare result with fixtures
Ouziel Apr 22, 2024
01cf546
Test and fix unpack route
Ouziel Apr 22, 2024
c2f5a77
add content-type header
Ouziel Apr 22, 2024
4962ead
compose routes return json
Ouziel Apr 22, 2024
e2dce12
/transactions/info returns also unpacked data
Ouziel Apr 22, 2024
5b5be3a
script to generate blueprint doc
Ouziel Apr 22, 2024
1d8812b
Add required/optional and default value in doc
Ouziel Apr 22, 2024
c0885bd
Tweak `genapidoc.py`
adamkrellenstein Apr 23, 2024
9551a01
remove \n
Ouziel Apr 22, 2024
9c12f63
Use example args to generate output example
Ouziel Apr 23, 2024
064b0cb
Add quotes for docusaurus
Ouziel Apr 23, 2024
129c17f
progress in example args/output
Ouziel Apr 23, 2024
becf57c
All routes with output example except compose and backend; Add some l…
Ouziel Apr 23, 2024
2c37b65
Add cache to generate doc faster
Ouziel Apr 23, 2024
5762f8d
tweak gendoc cache
Ouziel Apr 23, 2024
3b3973a
support float args
Ouziel Apr 23, 2024
0f7ffd3
More output example for compose
Ouziel Apr 23, 2024
961c332
catch correctly compose error
Ouziel Apr 23, 2024
66cdc2e
log unexpected API error
Ouziel Apr 23, 2024
e6070ad
Standardize API result
Ouziel Apr 24, 2024
8a4d607
Output example for all compose routes; fixes
Ouziel Apr 24, 2024
0f71f80
Don't pass db if not needed; Mempool output examples; fixes
Ouziel Apr 24, 2024
92968bf
All routes with output example
Ouziel Apr 24, 2024
8a58374
Refactor function to return data
Ouziel Apr 24, 2024
0f1b4ed
Add notes in API documentation
Ouziel Apr 24, 2024
eddff66
Merge branch 'develop' into newapi2
Ouziel Apr 24, 2024
9041dfd
fix fixtures
Ouziel Apr 24, 2024
d3e13b2
update relase notes
Ouziel Apr 24, 2024
4f1b4d6
lint
Ouziel Apr 24, 2024
f48bc0c
Remove success from result
Ouziel Apr 25, 2024
e206819
db is already initialized
Ouziel Apr 25, 2024
cd0ba21
fix typo
Ouziel Apr 25, 2024
6f78ef5
_by_event -> _by_name
Ouziel Apr 25, 2024
09a64a8
fix fixtures
Ouziel Apr 25, 2024
99ef600
Remove success and add root path in doc
Ouziel Apr 25, 2024
56ff76a
Don't indent json result; Fix typo
Ouziel Apr 25, 2024
611b263
tweak indentation
Ouziel Apr 25, 2024
b71b1fe
order_hash, bet_hash and dispenser_hash instead tx_hash in routes
Ouziel Apr 25, 2024
102b9f1
get_balance_object -> get_balance_by_address_and_asset
Ouziel Apr 25, 2024
a5370ea
handle_healthz_route_v2 -> check_server_status
Ouziel Apr 25, 2024
16ae417
generate also apib compatible with apiary
Ouziel Apr 25, 2024
405b2ed
Introduce counterparty-core.apib; Fix blueprint semantic issues
Ouziel Apr 25, 2024
06a0309
Fix API root paragraph
Ouziel Apr 25, 2024
ba4e5f8
fix blueprint links in Apiary
Ouziel Apr 25, 2024
af4ff31
fix typos
Ouziel Apr 25, 2024
87e367f
Root path always return 200
Ouziel Apr 25, 2024
016b3a9
Rename blueprint; Include unpacked data in get transaction by hash re…
Ouziel Apr 25, 2024
67f0b95
fix apiary filename
Ouziel Apr 25, 2024
8ff2f07
Add Port to Host
adamkrellenstein Apr 25, 2024
6a9f477
replace /old by /v1
Ouziel Apr 25, 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
92 changes: 37 additions & 55 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 @@ -364,59 +397,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,
)

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 @@ -442,7 +424,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
236 changes: 236 additions & 0 deletions counterparty-core/counterpartycore/lib/api/api_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
import argparse
import logging
import multiprocessing
import traceback
from multiprocessing import Process
from threading import Timer

import flask
from counterpartycore import server
from counterpartycore.lib import (
blocks,
config,
database,
exceptions,
ledger,
)
from counterpartycore.lib.api.routes import ROUTES
from counterpartycore.lib.api.util import (
function_needs_db,
get_backend_height,
init_api_access_log,
remove_rowids,
to_json,
)
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)
ouziel-slama marked this conversation as resolved.
Show resolved Hide resolved
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, route in ROUTES.item():

Check warning

Code scanning / pylint

Instance of 'dict' has no 'item' member; maybe 'items'?. Warning

Instance of 'dict' has no 'item' member; maybe 'items'?.
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 is_server_ready():
return ledger.CURRENT_BLOCK_INDEX >= BACKEND_HEIGHT - 1


def is_cachable(rule):
if rule.startswith("/blocks"):
return True
if rule.startswith("/transactions"):
return True
if rule.startswith("/backend"):
return True
return False


def return_result_if_not_ready(rule):
return is_cachable(rule) or rule == "/"


def return_result(success, http_code, result=None, error=None):
api_result = {"success": success}
ouziel-slama marked this conversation as resolved.
Show resolved Hide resolved
if result is not None:
api_result["result"] = result
if error is not None:
api_result["error"] = error
response = flask.make_response(to_json(api_result), http_code)
response.headers["X-COUNTERPARTY-HEIGHT"] = ledger.CURRENT_BLOCK_INDEX
response.headers["X-COUNTERPARTY-READY"] = is_server_ready()
response.headers["X-BACKEND-HEIGHT"] = BACKEND_HEIGHT
response.headers["Content-Type"] = "application/json"
return response


def prepare_args(route, **kwargs):
function_args = dict(kwargs)
# inject args from request.args
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
elif arg["type"] == "float":
try:
function_args[arg_name] = float(str_arg)
except ValueError as e:
raise ValueError(f"Invalid float: {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)
ouziel-slama marked this conversation as resolved.
Show resolved Hide resolved

rule = str(request.url_rule.rule)

# check if server must be ready
if not is_server_ready() and not return_result_if_not_ready(rule):
return return_result(False, 503, error="Counterparty not ready")

if rule == "/":
return return_result(True, 200, result=api_root())

route = ROUTES.get(rule)

# parse args
try:
function_args = prepare_args(route, **kwargs)
except ValueError as e:
return return_result(False, 400, error=str(e))

# call the function
try:
if function_needs_db(route["function"]):
result = route["function"](db, **function_args)
else:
result = route["function"](**function_args)
except (exceptions.ComposeError, exceptions.UnpackError) as e:
return return_result(False, 503, error=str(e))
except Exception as e:

Check warning

Code scanning / pylint

Catching too general exception Exception. Warning

Catching too general exception Exception.
logger.exception("Error in API: %s", e)
traceback.print_exc()
return return_result(False, 503, error="Unknwon error")

# clean up and return the result
result = remove_rowids(result)
return return_result(True, 200, result=result)


def run_api_server(args):
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.
ouziel-slama marked this conversation as resolved.
Show resolved Hide resolved
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