From cbc1b449ba9c716fe414bfd8bb9daa2bc1b6478e Mon Sep 17 00:00:00 2001 From: William Luke Date: Tue, 21 Dec 2021 21:15:45 +0300 Subject: [PATCH] switch to fastapi :-/ --- apps/cic-eth/cic_eth/runnable/__init__.py | 0 .../cic_eth/runnable/daemons/__init__.py | 0 .../cic_eth/runnable/daemons/server.py | 158 +++++-- apps/cic-eth/cic_eth/server/__init__.py | 4 +- apps/cic-eth/cic_eth/server/cache.py | 6 +- apps/cic-eth/cic_eth/server/celery.py | 3 + apps/cic-eth/cic_eth/server/config.py | 3 +- apps/cic-eth/cic_eth/server/models.py | 92 ++++ .../server/openapi/UWSGIOpenAPIRequest.py | 57 --- .../cic_eth/server/openapi/__init__.py | 2 - .../cic_eth/server/openapi/server.yaml | 395 ------------------ .../cic_eth/server/openapi/validate.py | 31 -- apps/cic-eth/cic_eth/server/routes.py | 129 ------ apps/cic-eth/cic_eth/server/uwsgi.py | 18 - apps/cic-eth/requirements.txt | 8 +- apps/cic-eth/tests/test_server.py | 104 +++-- docker-compose.yml | 9 +- 17 files changed, 308 insertions(+), 711 deletions(-) create mode 100644 apps/cic-eth/cic_eth/runnable/__init__.py create mode 100644 apps/cic-eth/cic_eth/runnable/daemons/__init__.py create mode 100644 apps/cic-eth/cic_eth/server/models.py delete mode 100644 apps/cic-eth/cic_eth/server/openapi/UWSGIOpenAPIRequest.py delete mode 100644 apps/cic-eth/cic_eth/server/openapi/__init__.py delete mode 100644 apps/cic-eth/cic_eth/server/openapi/server.yaml delete mode 100644 apps/cic-eth/cic_eth/server/openapi/validate.py delete mode 100644 apps/cic-eth/cic_eth/server/routes.py delete mode 100644 apps/cic-eth/cic_eth/server/uwsgi.py diff --git a/apps/cic-eth/cic_eth/runnable/__init__.py b/apps/cic-eth/cic_eth/runnable/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/cic-eth/cic_eth/runnable/daemons/__init__.py b/apps/cic-eth/cic_eth/runnable/daemons/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/apps/cic-eth/cic_eth/runnable/daemons/server.py b/apps/cic-eth/cic_eth/runnable/daemons/server.py index 59198c73..389fe553 100644 --- a/apps/cic-eth/cic_eth/runnable/daemons/server.py +++ b/apps/cic-eth/cic_eth/runnable/daemons/server.py @@ -1,9 +1,15 @@ -# standard imports import logging -from urllib.parse import urlparse +import sys +from typing import List, Optional, Union import redis -from cic_eth.server import args, cache, config, openapi, routes +# standard imports +from cic_eth.server import cache, celery, converters +from cic_eth.server.config import args, config +from cic_eth.server.models import (DefaultToken, Token, TokenBalance, + Transaction) +# standard imports +from fastapi import FastAPI, Query # define log levels if args.vv: @@ -13,28 +19,130 @@ elif args.v: log = logging.getLogger(__name__) -# TODO Censor relevant things -log.debug(f"{config}") - # define universal redis cache access -cache.Cache.store = redis.StrictRedis(host=config.get('REDIS_HOST'), - port=config.get('REDIS_PORT'), - db=config.get('REDIS_DB'), - decode_responses=True) - -# uwsgi application +cache.Cache.store = redis.StrictRedis(host=config.get('REDIS_HOST'), port=config.get( + 'REDIS_PORT'), db=config.get('REDIS_DB'), decode_responses=True) -def application(env, start_response): - # Validate incoming request against the open api spec - errors, request = openapi.validate.request(env, start_response) - if errors: - return errors - parsed_url = urlparse(env.get('REQUEST_URI')) - path = parsed_url.path - query = dict(request.parameters.query) - handler = routes.get(path) - if handler: - return handler(start_response, query) - start_response('404 This is not the path you\'re looking for', []) - return [] +app = FastAPI(debug=True, + title="Grassroots Economics", + description="CIC ETH API", + version="0.0.1", + terms_of_service="https://www.grassrootseconomics.org/pages/terms-and-conditions.html", + contact={ + "name": "Grassroots Economics", + "url": "https://www.grassrootseconomics.org", + "email": "will@grassecon.org" + }, + license_info={ + "name": "GPLv3", + }) + + +@app.get("/transactions", response_model=List[Transaction]) +def transactions(address: str, limit: Optional[str] = 10): + return celery.call('list', address, limit=limit) + + +@app.get("/balance", response_model=List[TokenBalance]) +def balance(token_symbol: str, address: str = Query(..., title="Address", min_length=40, max_length=42), include_pending: bool = True): + log.info(f"address: {address}") + log.info(f"token_symbol: {token_symbol}") + data = celery.call('balance', address, token_symbol, + include_pending=include_pending) + for b in data: + token = get_token(token_symbol) + b['balance_network'] = converters.from_wei( + token.decimals, int(b['balance_network'])) + b['balance_incoming'] = converters.from_wei( + token.decimals, int(b['balance_incoming'])) + b['balance_outgoing'] = converters.from_wei( + token.decimals, int(b['balance_outgoing'])) + + b.update({ + "balance_available": int(b['balance_network']) + int(b['balance_incoming']) - int(b['balance_outgoing']) + }) + return data + + +@app.post("/create_account") +def create_account(password: Optional[str] = None, register: bool = True): + data = celery.call('create_account', password=password, register=register) + return data + + +# def refill_gas(start_response, query: dict): +# address = query.pop('address') +# data = celery.call('refill_gas', address) +# return data + + +# def ping(start_response, query: dict): +# data = celery.call('ping', **query) +# return data + +@app.post("/transfer") +def transfer(from_address: str, to_address: str, value: int, token_symbol: str): + token = get_token( + token_symbol) + wei_value = converters.to_wei(token.decimals, int(value)) + data = celery.call('transfer', from_address, + to_address, wei_value, token_symbol) + return data + + +@app.post("/transfer_from") +def transfer_from(from_address: str, to_address: str, value: int, token_symbol: str, spender_address: str): + token = get_token( + token_symbol) + wei_value = converters.to_wei(token.decimals, int(value)) + data = celery.call('transfer_from', from_address, to_address, + wei_value, token_symbol, spender_address) + return data + + +@app.get("/token", response_model=Token) +def token(token_symbol: str, proof: Optional[str] = None): + token = get_token(token_symbol) + if token == None: + sys.stderr.write(f"Cached Token {token_symbol} not found") + data = celery.call('token', token_symbol, proof=proof) + cache.set_token_data(token_symbol, data) + token = Token.new(data) + sys.stderr.write(f"Token {token}") + + return token + + +@app.get("/tokens", response_model=Token) +def tokens(token_symbols: Optional[List[str]] = Query(None), proof: Optional[Union[str, List[str], List[List[str]]]] = None): + data = celery.call('tokens', token_symbols, proof=proof) + if data: + return Token.new(data) + return None + + +@app.get("/default_token", response_model=DefaultToken) +def default_token(): + data = cache.get_default_token() + if data is None: + data = celery.call('default_token') + if data is not None: + cache.set_default_token(data) + return data + + +def get_token(token_symbol: str): + data = cache.get_token_data(token_symbol) + log.debug(f"cached token data: {data}") + if data == None: + data = celery.call('token', token_symbol) + log.debug( + f"No token data setting token data for: {token_symbol} to {data}") + cache.set_token_data(token_symbol, data) + return Token.new(data) + + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=5000, log_level="info") diff --git a/apps/cic-eth/cic_eth/server/__init__.py b/apps/cic-eth/cic_eth/server/__init__.py index a567f984..ef60ef84 100644 --- a/apps/cic-eth/cic_eth/server/__init__.py +++ b/apps/cic-eth/cic_eth/server/__init__.py @@ -1,5 +1,3 @@ from . import converters from . import cache -from . import uwsgi -from .routes import routes -from .config import config, args \ No newline at end of file +from . import celery diff --git a/apps/cic-eth/cic_eth/server/cache.py b/apps/cic-eth/cic_eth/server/cache.py index 97e1c78e..c801f2bf 100644 --- a/apps/cic-eth/cic_eth/server/cache.py +++ b/apps/cic-eth/cic_eth/server/cache.py @@ -3,7 +3,7 @@ import hashlib import json import logging from typing import Optional, Union - +from cic_eth.server.models import Token # external imports from cic_types.condiments import MetadataPointer from redis import Redis @@ -35,7 +35,7 @@ def get_token_data(token_symbol: str): return token_data -def set_token_data(token_symbol: str, data: dict): +def set_token_data(token_symbol: str, token: dict): """ :param token_symbol: :type token_symbol: @@ -44,7 +44,7 @@ def set_token_data(token_symbol: str, data: dict): """ identifier = [token_symbol.encode('utf-8')] key = cache_data_key(identifier, MetadataPointer.TOKEN_DATA) - cache_data(key, json.dumps(data)) + cache_data(key, json.dumps(token)) logg.debug(f'Cached token data for: {token_symbol} at: {key}') diff --git a/apps/cic-eth/cic_eth/server/celery.py b/apps/cic-eth/cic_eth/server/celery.py index 79bf7446..d6dd9cd4 100644 --- a/apps/cic-eth/cic_eth/server/celery.py +++ b/apps/cic-eth/cic_eth/server/celery.py @@ -10,9 +10,11 @@ from cic_eth.server.config import config log = logging.getLogger(__name__) + celery_app = cic_eth.cli.CeleryApp.from_config(config) celery_app.set_default() + chain_spec = config.get('CHAIN_SPEC') celery_queue = config.get('CELERY_QUEUE') @@ -24,6 +26,7 @@ redis_db = config.get('REDIS_DB') def call(method, *args, **kwargs): """ Creates a redis channel and calls `cic_eth.api` with the provided `method` and `*args`. Returns the result of the api call """ + log.debug(f"Using chainspec: {chain_spec}") redis_channel = str(uuid.uuid4()) r = redis.Redis(redis_host, redis_port, redis_db) ps = r.pubsub() diff --git a/apps/cic-eth/cic_eth/server/config.py b/apps/cic-eth/cic_eth/server/config.py index 149ab243..35e60169 100644 --- a/apps/cic-eth/cic_eth/server/config.py +++ b/apps/cic-eth/cic_eth/server/config.py @@ -3,7 +3,6 @@ import cic_eth.cli arg_flags = cic_eth.cli.argflag_std_base local_arg_flags = cic_eth.cli.argflag_local_taskcallback argparser = cic_eth.cli.ArgumentParser(arg_flags) - argparser.process_local_flags(local_arg_flags) -args = argparser.parse_args() +args = argparser.parse_args([]) config = cic_eth.cli.Config.from_args(args, arg_flags, local_arg_flags) diff --git a/apps/cic-eth/cic_eth/server/models.py b/apps/cic-eth/cic_eth/server/models.py new file mode 100644 index 00000000..b60195a2 --- /dev/null +++ b/apps/cic-eth/cic_eth/server/models.py @@ -0,0 +1,92 @@ +from __future__ import annotations + +from typing import List, Optional + +from pydantic import BaseModel, Field + + +class Transaction(BaseModel): + block_number: Optional[int] = Field(None, example=24531) + date_checked: Optional[str] = Field( + None, example='2021-11-12T09:36:40.725296') + date_created: Optional[str] = Field( + None, example='2021-11-12T09:36:40.131292') + date_updated: Optional[str] = Field( + None, example='2021-11-12T09:36:40.131292') + destination_token: Optional[str] = Field( + None, example=365185044137427460620354810422988491181438940190 + ) + destination_token_decimals: Optional[int] = Field(None, example=6) + destination_token_symbol: Optional[str] = Field(None, example='COFE') + from_value: Optional[int] = Field(None, example=100000000) + hash: Optional[str] = Field( + None, + example=90380195350511178677041624165156640995490505896556680958001954705731707874291, + ) + nonce: Optional[int] = Field(None, example=1) + recipient: Optional[str] = Field( + None, example='872e1ec9d499b242ebfcfd0a279a4c3e0cd472c0' + ) + sender: Optional[str] = Field( + None, example='1a92b05e0b880127a4c26ac0f68a52df3ac6b89d' + ) + signed_tx: Optional[str] = Field( + None, + example=1601943273486236942256143665779318355236220334071247753507187634376562549990085710958441113013370129915441072693447256942510246386178938683325073160349857879326297351587330623503997011254644396580777843154770873208185332563272343361515226115860084201932230246018679661802320007832375955345977725551120479084062615799940692628221555193198194825737613358738414884130187144700126061702642574663703095161159219410608270, + ) + source_token: Optional[str] = Field( + None, example=365185044137427460620354810422988491181438940190 + ) + source_token_decimals: Optional[int] = Field(None, example=6) + source_token_symbol: Optional[str] = Field(None, example='COFE') + status: Optional[str] = Field(None, example='SUCCESS') + status_code: Optional[int] = Field(None, example=4104) + timestamp: Optional[int] = Field(None, example=1636709800) + to_value: Optional[int] = Field(None, example=100000000) + tx_hash: Optional[str] = Field( + None, + example=90380195350511178677041624165156640995490505896556680958001954705731707874291, + ) + tx_index: Optional[int] = Field(None, example=0) + + +class DefaultToken(BaseModel): + symbol: Optional[str] = Field(None, description='Token Symbol') + address: Optional[str] = Field(None, description='Token Address') + name: Optional[str] = Field(None, description='Token Name') + decimals: Optional[int] = Field(None, description='Decimals') + + +class TokenBalance(BaseModel): + address: Optional[str] = None + converters: Optional[List[str]] = None + balance_network: Optional[int] = None + balance_incoming: Optional[int] = None + balance_outgoing: Optional[int] = None + balance_available: Optional[int] = None + + +class Token(BaseModel): + decimals: Optional[int] = None + name: Optional[str] = None + address: Optional[str] = None + symbol: Optional[str] = None + proofs: Optional[List[str]] = None + converters: Optional[List[str]] = None + proofs_with_signers: Optional[List[Proof]] = None + + @staticmethod + def new(data: List[dict]) -> Token: + proofs_with_signers = [{"proof": proof, "signers": signers} + for (proof, signers) in data[1].items()] + return Token(**data[0], + proofs_with_signers=proofs_with_signers, + ) + + +class Proof(BaseModel): + proof: Optional[str] = None + signers: Optional[List[str]] = None + + +Token.update_forward_refs() diff --git a/apps/cic-eth/cic_eth/server/openapi/UWSGIOpenAPIRequest.py b/apps/cic-eth/cic_eth/server/openapi/UWSGIOpenAPIRequest.py deleted file mode 100644 index 97273848..00000000 --- a/apps/cic-eth/cic_eth/server/openapi/UWSGIOpenAPIRequest.py +++ /dev/null @@ -1,57 +0,0 @@ -"""OpenAPI core contrib requests requests module""" - -from urllib.parse import parse_qs, urlparse - -from openapi_core.validation.request.datatypes import (OpenAPIRequest, - RequestParameters) -from werkzeug import Request -from werkzeug.datastructures import Headers, ImmutableMultiDict - - -class OpenAPIRequestFactory: - @classmethod - def create(cls, env): - """ - Converts the uwsgi request environment to a request that can be validated against an OpenAPI schema - - """ - request = Request(env) - - # Method - method = request.method.lower() - - # Preparing a request formats the URL with params, strip them out again - o = urlparse(request.url) - params = parse_qs(o.query) - # extract the URL without query parameters - url = o._replace(query=None).geturl() - - # Order matters because all python requests issued from a session - # include Accept */* which does not necessarily match the content type - mimetype = request.headers.get("Content-Type") or request.headers.get( - "Accept" - ) - - # Headers - request.headers is not an instance of Headers - # which is expected - header = Headers(dict(request.headers)) - - # Body - body = request.get_data() - - # Path gets deduced by path finder against spec - parameters = RequestParameters( - query=ImmutableMultiDict(params), - header=header, - cookie=request.cookies, - ) - return OpenAPIRequest( - full_url_pattern=url, - method=method, - parameters=parameters, - body=body, - mimetype=mimetype, - ) - - -UWSGIOpenAPIRequest = OpenAPIRequestFactory.create diff --git a/apps/cic-eth/cic_eth/server/openapi/__init__.py b/apps/cic-eth/cic_eth/server/openapi/__init__.py deleted file mode 100644 index d5b8da38..00000000 --- a/apps/cic-eth/cic_eth/server/openapi/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from . import validate -from . import UWSGIOpenAPIRequest \ No newline at end of file diff --git a/apps/cic-eth/cic_eth/server/openapi/server.yaml b/apps/cic-eth/cic_eth/server/openapi/server.yaml deleted file mode 100644 index 340b6206..00000000 --- a/apps/cic-eth/cic_eth/server/openapi/server.yaml +++ /dev/null @@ -1,395 +0,0 @@ -openapi: 3.0.3 -info: - title: Grassroots Economics - description: CIC ETH API - termsOfService: https://www.grassrootseconomics.org/pages/terms-and-conditions.html - contact: - name: Grassroots Economics - url: https://www.grassrootseconomics.org - email: will@grassecon.org - license: - name: GPLv3 - version: 0.0.1 -servers: - - url: / -paths: - /tokens: - get: - tags: - - Token - description: Perform a token data lookup from the token index. The token index will enforce unique associations between token symbol and contract address. - operationId: get_tokens - parameters: - - name: token_symbols - in: query - description: Token symbol to look up - required: true - schema: - type: array - example: ["GFT"] - items: - type: string - - name: proof - in: query - description: | - Proofs to add to signature verification for the token - - None (Default), in which case proof checks are skipped (although there may still be builtin proof checks being performed) - - Single string, where the same proof is used for each token lookup - - Array of strings, where the respective proof is used for the respective token. number of proofs must match the number of tokens. - - Array of lists, where the respective proofs in each list is used for the respective token. number of lists of proofs must match the number of tokens. - - required: false - schema: - oneOf: - - type: array - items: - type: string - - type: string - - responses: - "200": - description: OK. - content: - application/json: - schema: - $ref: "#/components/schemas/Token" - /token: - get: - tags: - - Token - description: Single-token alias for tokens method. - operationId: get_token - parameters: - - name: token_symbol - in: query - description: Token symbol to look up - required: true - schema: - type: string - example: "GFT" - - name: proof - in: query - description: Proofs to add to signature verification for the token - required: false - schema: - type: string - - responses: - "200": - description: OK. - content: - application/json: - schema: - $ref: "#/components/schemas/Token" - /default_token: - get: - tags: - - Token - description: Retrieves the default fallback token of the custodial network. - operationId: get_default_token - responses: - "200": - description: OK. - content: - application/json: - schema: - $ref: "#/components/schemas/Token" - /balance: - get: - tags: - - Account - description: Retrieves the current token balance of the given address - operationId: account_balance - parameters: - - name: address - in: query - required: true - schema: - type: string - example: "0xe88ba386f0efc7117e8d0d17a750fce492516ecb" - - name: token_symbol - in: query - required: true - schema: - type: string - - name: include_pending - in: query - required: false - schema: - type: boolean - default: true - responses: - "200": - description: OK. - content: - application/json: - schema: - type: array - items: - $ref: "#/components/schemas/TokenBalance" - - /transactions: - get: - tags: - - Account - description: Retrieve an aggregate list of latest transactions of internal and (optionally) external origin in reverse chronological order. - operationId: list_transactions - parameters: - - name: address - in: query - required: true - schema: - type: string - example: "0xe88ba386f0efc7117e8d0d17a750fce492516ecb" - - name: limit - in: query - required: false - schema: - type: integer - default: 10 - responses: - "200": - description: OK. - content: - application/json: - schema: - type: array - items: - $ref: "#/components/schemas/Transaction" - /create_account: - post: - tags: - - Account - description: Creates a new blockchain address encrypted with the given password and returns the new address - operationId: create_account_post - parameters: - - name: password - in: query - required: false - allowReserved: true - schema: - type: string - - name: register - in: query - required: false - allowReserved: true - schema: - type: boolean - default: true - responses: - "200": - description: Address of the new account - content: - application/json: - schema: - type: string - example: "bea54d37f1a469515bda96e116695cedd323c182" - /transfer: - post: - tags: - - Account - description: Performs a transfer of ERC20 tokens from one address to another. - operationId: transfer - parameters: - - name: from_address - in: query - required: true - schema: - type: string - example: "0xbea54d37f1a469515bda96e116695cedd323c182" - - name: to_address - in: query - required: true - schema: - type: string - example: "0x8264d4c224d0c74c98295bfab2f216c2d7b18c8c" - - name: value - in: query - required: true - schema: - type: integer - - name: token_symbol - in: query - required: true - schema: - type: string - responses: - "200": - description: Transaction hash for transfer operation - content: - application/json: - schema: - type: string - /transfer_from: - post: - tags: - - Account - description: - Performs a transfer of ERC20 tokens by one address on behalf of - another address to a third party. - operationId: transfer_from - parameters: - - name: from_address - in: query - description: Ethereum address of sender - required: true - schema: - type: string - - name: to_address - in: query - description: Ethereum address of recipient - required: true - schema: - type: string - - name: value - in: query - description: Estimated return from conversion - required: true - schema: - type: integer - - name: token_symbol - in: query - description: ERC20 token symbol of token to send - required: true - schema: - type: string - - name: spender_address - in: query - description: Ethereum address of recipient - required: true - schema: - type: string - responses: - "200": - description: Transaction hash for transfer operation - content: - application/json: - schema: - type: string -components: - schemas: - Transaction: - type: object - properties: - block_number: - type: integer - format: int32 - example: 24531 - date_checked: - type: string - example: 2021-11-12T09:36:40.725296 - date_created: - type: string - example: 2021-11-12T09:36:40.131292 - date_updated: - type: string - example: 2021-11-12T09:36:40.131292 - destination_token: - type: string - example: 0x3ff776b6f888980def9d4220858803f9dc5e341e - destination_token_decimals: - type: integer - format: int32 - example: 6 - destination_token_symbol: - type: string - example: COFE - from_value: - type: integer - format: int32 - example: 100000000 - hash: - type: string - example: 0xc7d160b4f1c89f09cbccbc2c4f6a72760bc3c1634a88438870c31b2e4d9e2bf3 - nonce: - type: integer - format: int32 - example: 1 - recipient: - type: string - example: 872e1ec9d499b242ebfcfd0a279a4c3e0cd472c0 - sender: - type: string - example: 1a92b05e0b880127a4c26ac0f68a52df3ac6b89d - signed_tx: - type: string - example: 0xf8aa018310c8e0837a1200943ff776b6f888980def9d4220858803f9dc5e341e80b844a9059cbb000000000000000000000000872e1ec9d499b242ebfcfd0a279a4c3e0cd472c00000000000000000000000000000000000000000000000000000000005f5e10082466ca0617d50ea726dfe61d6dc5e8a4a85cf7469514f394250cecb019006317cfb94d3a04930e14524f0a87db623a80e0f841ab613f693f5031c6a136873052ae7bba08e - source_token: - type: string - example: 0x3ff776b6f888980def9d4220858803f9dc5e341e - source_token_decimals: - type: integer - format: int32 - example: 6 - source_token_symbol: - type: string - example: COFE - status: - type: string - example: SUCCESS - status_code: - type: integer - format: int32 - example: 4104 - timestamp: - type: integer - format: int32 - example: 1636709800 - to_value: - type: integer - format: int32 - example: 100000000 - tx_hash: - type: string - example: 0xc7d160b4f1c89f09cbccbc2c4f6a72760bc3c1634a88438870c31b2e4d9e2bf3 - tx_index: - type: integer - format: int32 - example: 0 - - Token: - type: object - properties: - symbol: - type: string - description: Token Symbol - address: - type: string - description: Token Address - name: - type: string - description: Token Name - decimals: - type: integer - description: Decimals - example: - symbol: "GFT" - address: "3FF776B6f888980DEf9d4220858803f9dC5e341e" - decimals: 6 - name: "Gift Token" - TokenBalance: - type: object - properties: - address: - type: string - converters: - type: array - items: - type: string - balance_network: - type: integer - balance_incoming: - type: integer - balance_outgoing: - type: integer - balance_available: - type: integer - example: - balance_network: 0 - address: address - balance_incoming: 6 - balance_available: 5 - converters: - - converters - - converters - balance_outgoing: 1 diff --git a/apps/cic-eth/cic_eth/server/openapi/validate.py b/apps/cic-eth/cic_eth/server/openapi/validate.py deleted file mode 100644 index 8c6ba4f1..00000000 --- a/apps/cic-eth/cic_eth/server/openapi/validate.py +++ /dev/null @@ -1,31 +0,0 @@ -import json -from os import path -from typing import Tuple, Type, Union, List - -from openapi_core.validation.request.datatypes import OpenAPIRequest - -from cic_eth.server.openapi.UWSGIOpenAPIRequest import UWSGIOpenAPIRequest -from openapi_core import create_spec -from openapi_core.validation.request.validators import RequestValidator -from openapi_spec_validator.schemas import read_yaml_file - -spec_dict = read_yaml_file(path.join(path.dirname( - __file__), './server.yaml')) -spec = create_spec(spec_dict) - - -def request(env, start_response) -> Tuple[Union[List[bytes], None], Type[OpenAPIRequest]]: - oAPIRequest = UWSGIOpenAPIRequest(env) - validator = RequestValidator(spec) - result = validator.validate(oAPIRequest) - - if result.errors: - json_data = json.dumps(list(map(lambda e: str(e), result.errors))) - content = json_data.encode('utf-8') - headers = [] - headers.append(('Content-Length', str(len(content))),) - headers.append(('Access-Control-Allow-Origin', '*',)) - headers.append(('Content-Type', 'application/json',)) - start_response('400 Invalid Request', headers) - return ([content], oAPIRequest) - return (None, oAPIRequest) diff --git a/apps/cic-eth/cic_eth/server/routes.py b/apps/cic-eth/cic_eth/server/routes.py deleted file mode 100644 index 15137525..00000000 --- a/apps/cic-eth/cic_eth/server/routes.py +++ /dev/null @@ -1,129 +0,0 @@ -""" -Contains all handlers for valid routes for the server -""" - -# standard imports -import logging -from typing import Any, Callable, Dict, List, Type, Union - -from cic_eth.server import cache, celery, converters, uwsgi -from cic_eth.server.openapi.UWSGIOpenAPIRequest import UWSGIOpenAPIRequest - -log = logging.getLogger(__name__) - - -def handle_transactions(start_response, query: dict) -> List[bytes]: - address = query.pop('address') - data = celery.call('list', address, **query) - return uwsgi.respond(start_response, data) - - -def handle_balance(start_response, query: dict) -> List[bytes]: - address = query.pop('address') - token_symbol = query.pop('token_symbol') - log.info(f"token_symbol: {token_symbol}") - log.info(f"query: {query}") - data = celery.call('balance', address, token_symbol, - **query) - for b in data: - token_data = get_token_data(token_symbol) - b['balance_network'] = converters.from_wei(token_data.get( - 'decimals'), int(b['balance_network'])) - b['balance_incoming'] = converters.from_wei(token_data.get( - 'decimals'), int(b['balance_incoming'])) - b['balance_outgoing'] = converters.from_wei(token_data.get( - 'decimals'), int(b['balance_outgoing'])) - - b.update({ - "balance_available": int(b['balance_network']) + int(b['balance_incoming']) - int(b['balance_outgoing']) - }) - return uwsgi.respond(start_response, data) - - -def handle_create_account(start_response, query: dict) -> List[bytes]: - data = celery.call('create_account', **query) - return uwsgi.respond(start_response, data) - - -# def handle_refill_gas(start_response, query: dict) -> List[bytes]: -# address = query.pop('address') -# data = celery.call('refill_gas', address) -# return uwsgi.respond(start_response, data) - - -# def handle_ping(start_response, query: dict) -> List[bytes]: -# data = celery.call('ping', **query) -# return uwsgi.respond(start_response, data) - - -def handle_transfer(start_response, query: dict) -> List[bytes]: - from_address = query.pop('from_address') - to_address = query.pop('to_address') - value = query.pop('value') - token_symbol = query.pop('token_symbol') - wei_value = converters.to_wei(get_token_data( - token_symbol).get('decimals'), int(value)) - data = celery.call('transfer', from_address, - to_address, wei_value, token_symbol) - return uwsgi.respond(start_response, data) - - -def handle_transfer_from(start_response, query: dict) -> List[bytes]: - from_address = query.pop('from_address') - to_address = query.pop('to_address') - value = query.pop('value') - token_symbol = query.pop('token_symbol') - spender_address = query.pop('spender_address') - wei_value = converters.to_wei(get_token_data( - token_symbol).get('decimals'), int(value)) - data = celery.call('transfer_from', from_address, to_address, - wei_value, token_symbol, spender_address) - return uwsgi.respond(start_response, data) - - -def handle_token(start_response, query: dict) -> List[bytes]: - token_symbol = query.pop('token_symbol') - data = cache.get_token_data(token_symbol) - if data == None: - data = celery.call('token', token_symbol, **query) - cache.set_token_data(token_symbol, data) - return uwsgi.respond(start_response, data) - - -def handle_tokens(start_response, query: dict) -> List[bytes]: - token_symbols = query.pop('token_symbols') - data = celery.call('tokens', token_symbols, **query) - return uwsgi.respond(start_response, data) - - -def handle_default_token(start_response, _query) -> List[bytes]: - data = cache.get_default_token() - if data is None: - data = celery.call('default_token') - if data is not None: - cache.set_default_token(data) - return uwsgi.respond(start_response, data) - - -def get_token_data(token_symbol: str) -> List[bytes]: - data = cache.get_token_data(token_symbol) - log.debug(f"cached token data: {data}") - if data == None: - data = celery.call('token', token_symbol) - log.debug( - f"No token data setting token data for: {token_symbol} to {data}") - cache.set_token_data(token_symbol, data) - # TODO What is the second item in the list - return data[0] - - -routes: Dict[str, Callable[[Any, Union[None, dict]], List[bytes]]] = { - "/transactions": handle_transactions, - "/balance": handle_balance, - "/create_account": handle_create_account, - "/transfer": handle_transfer, - "/transfer_from": handle_transfer_from, - "/token": handle_token, - "/tokens": handle_tokens, - "/default_token": handle_default_token, -} diff --git a/apps/cic-eth/cic_eth/server/uwsgi.py b/apps/cic-eth/cic_eth/server/uwsgi.py deleted file mode 100644 index 92e1e5a9..00000000 --- a/apps/cic-eth/cic_eth/server/uwsgi.py +++ /dev/null @@ -1,18 +0,0 @@ -# standard imports -import json - -DEFAULT_HEADERS = [ - ('Access-Control-Allow-Origin', '*'), - ('Content-Type', 'application/json'), -] - - -def respond(start_response, data): - json_data = json.dumps(data) - content = json_data.encode('utf-8') - headers = [ - *DEFAULT_HEADERS, - ('Content-Type', 'application/json',) - ] - start_response('200 OK', headers) - return [content] diff --git a/apps/cic-eth/requirements.txt b/apps/cic-eth/requirements.txt index 192e172b..9f7c0fd6 100644 --- a/apps/cic-eth/requirements.txt +++ b/apps/cic-eth/requirements.txt @@ -3,8 +3,8 @@ celery==4.4.7 chainlib-eth>=0.0.10a16,<0.1.0 semver==2.13.0 crypto-dev-signer>=0.4.15rc2,<0.5.0 -uwsgi==2.0.19.1 + setuptools >= 21.0.0 -openapi-core >= 0.14.2 -openapi-spec-validator >= 0.3.1 -Werkzeug>=2.0.2 + +fastapi[all]==0.70.1 +uvicorn[standard]<0.16.0 \ No newline at end of file diff --git a/apps/cic-eth/tests/test_server.py b/apps/cic-eth/tests/test_server.py index 20f34da3..bf5aa64e 100644 --- a/apps/cic-eth/tests/test_server.py +++ b/apps/cic-eth/tests/test_server.py @@ -5,28 +5,66 @@ import logging import time import hexathon -import pytest -import requests -from cic_eth.runnable.daemons.server import application -from pytest_localserver.http import WSGIServer +from cic_eth.runnable.daemons.server import app +from fastapi.testclient import TestClient log = logging.getLogger(__name__) - -@pytest.fixture -def testserver(request): - """Defines the testserver funcarg""" - server = WSGIServer(application=application) - server.start() - request.addfinalizer(server.stop) - return server +client = TestClient(app) -def test_account(testserver): +def test_default_token(): # Default Token - response = requests.get( - testserver.url + '/default_token', - ) + response = client.get('/default_token') + log.debug(f"balance response {response}") + default_token = response.json() + assert default_token == { + 'address': '3FF776B6f888980DEf9d4220858803f9dC5e341e', + 'decimals': 6, + 'name': 'Giftable Token', + 'symbol': 'GFT', + } + + +def test_token(): + # Default Token + response = client.get('/token?token_symbol=GFT') + log.debug(f"token response {response}") + token = response.json() + assert token == { + 'address': '3ff776b6f888980def9d4220858803f9dc5e341e', + 'converters': [], + 'decimals': 6, + 'name': 'Giftable Token', + 'proofs': ['3af82fa124235f84e78145f008054b11fe477e2b043ac5e4979c3afa737fd328'], + 'proofs_with_signers': [{'proof': '3af82fa124235f84e78145f008054b11fe477e2b043ac5e4979c3afa737fd328', + 'signers': ['Eb3907eCad74a0013c259D5874AE7f22DcBcC95C']}], + 'symbol': 'GFT', + } + + +def test_tokens(): + # Default Token + response = client.get('/tokens', params={'token_symbols': ['GFT', 'COFE']}) + + log.debug(f"tokens response {response}") + tokens = response.json() + assert tokens == { + 'address': '3ff776b6f888980def9d4220858803f9dc5e341e', + 'converters': [], + 'decimals': 6, + 'name': 'Giftable Token', + 'proofs': ['3af82fa124235f84e78145f008054b11fe477e2b043ac5e4979c3afa737fd328'], + 'proofs_with_signers': [{'proof': '3af82fa124235f84e78145f008054b11fe477e2b043ac5e4979c3afa737fd328', + 'signers': ['Eb3907eCad74a0013c259D5874AE7f22DcBcC95C']}], + 'symbol': 'GFT', + } + + +def test_account(): + # Default Token + response = client.get('/default_token', + ) log.debug(f"balance response {response}") default_token = response.json() @@ -35,8 +73,8 @@ def test_account(testserver): 'password': '', 'register': True } - response = requests.post( - testserver.url + '/create_account', + response = client.post( + '/create_account', params=params) address_1 = hexathon.valid(response.json()) @@ -45,9 +83,8 @@ def test_account(testserver): 'password': '', 'register': True } - response = requests.post( - testserver.url + '/create_account', - params=params) + response = client.post('/create_account', + params=params) address_2 = hexathon.valid(response.json()) time.sleep(30) # Required to allow balance to show @@ -57,9 +94,8 @@ def test_account(testserver): 'token_symbol': 'COFE', 'include_pending': True } - response = requests.get( - testserver.url + '/balance', - params=params) + response = client.get('/balance', + params=params) balance = response.json() assert (balance[0] == { @@ -78,9 +114,8 @@ def test_account(testserver): 'value': 100, 'token_symbol': 'COFE' } - response = requests.post( - testserver.url + '/transfer', - params=params) + response = client.post('/transfer', + params=params) transfer = response.json() # Balance Account 1 @@ -89,9 +124,8 @@ def test_account(testserver): 'token_symbol': 'COFE', 'include_pending': True } - response = requests.get( - testserver.url + '/balance', - params=params) + response = client.get('/balance', + params=params) balance_after_transfer = response.json() assert (balance_after_transfer[0] == { "address": default_token.get('address').lower(), @@ -107,13 +141,11 @@ def test_account(testserver): 'address': address_1, 'limit': 10 } - response = requests.get( - testserver.url + '/transactions', - params=params) + response = client.get('/transactions', + params=params) transactions = response.json() - ## TODO: What are the other 2 transactions + # TODO: What are the other 2 transactions assert len(transactions) == 3 - ## Check the transaction is correct + # Check the transaction is correct # TODO wtf is READSEND (Ready to send? Or already sent) assert transactions[0].status == 'READYSEND' - exit(1) # Forcing it to fail to i get logs out of pytest diff --git a/docker-compose.yml b/docker-compose.yml index 0ba7e466..3f8ec734 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -186,6 +186,7 @@ services: environment: REDIS_PORT: 6379 REDIS_HOST: redis + CHAIN_SPEC: ${CHAIN_SPEC:-evm:byzantium:8996:bloxberg} CELERY_BROKER_URL: ${CELERY_BROKER_URL:-redis://redis} CELERY_RESULT_URL: ${CELERY_RESULT_URL:-redis://redis} CELERY_DEBUG: ${CELERY_DEBUG:-1} @@ -193,7 +194,7 @@ services: depends_on: - cic-eth-tasker volumes: - # - ./apps/cic-eth/:/root Useful for developing locally + # - ./apps/cic-eth/:/root - signer-data:/run/crypto-dev-signer - contract-config:/tmp/cic/config/:ro command: @@ -203,11 +204,7 @@ services: set -a if [[ -f /tmp/cic/config/env_reset ]]; then source /tmp/cic/config/env_reset; fi set +a - /usr/local/bin/uwsgi \ - --wsgi-file /root/cic_eth/runnable/daemons/server.py \ - --http :5000 \ - --pyargv "-vv" - + python -m cic_eth.runnable.daemons.server cic-eth-tracker: image: ${IMAGE_BASE_URL:-registry.gitlab.com/grassrootseconomics/cic-internal-integration}/cic-eth:${TAG:-latest} build: