switch to fastapi :-/
This commit is contained in:
parent
97aabdb460
commit
cbc1b449ba
0
apps/cic-eth/cic_eth/runnable/__init__.py
Normal file
0
apps/cic-eth/cic_eth/runnable/__init__.py
Normal file
0
apps/cic-eth/cic_eth/runnable/daemons/__init__.py
Normal file
0
apps/cic-eth/cic_eth/runnable/daemons/__init__.py
Normal file
@ -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")
|
||||
|
@ -1,5 +1,3 @@
|
||||
from . import converters
|
||||
from . import cache
|
||||
from . import uwsgi
|
||||
from .routes import routes
|
||||
from .config import config, args
|
||||
from . import celery
|
||||
|
@ -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}')
|
||||
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
92
apps/cic-eth/cic_eth/server/models.py
Normal file
92
apps/cic-eth/cic_eth/server/models.py
Normal file
@ -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()
|
@ -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
|
@ -1,2 +0,0 @@
|
||||
from . import validate
|
||||
from . import UWSGIOpenAPIRequest
|
@ -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
|
@ -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)
|
@ -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,
|
||||
}
|
@ -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]
|
@ -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
|
@ -5,27 +5,65 @@ 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,8 +83,7 @@ def test_account(testserver):
|
||||
'password': '',
|
||||
'register': True
|
||||
}
|
||||
response = requests.post(
|
||||
testserver.url + '/create_account',
|
||||
response = client.post('/create_account',
|
||||
params=params)
|
||||
address_2 = hexathon.valid(response.json())
|
||||
time.sleep(30) # Required to allow balance to show
|
||||
@ -57,8 +94,7 @@ def test_account(testserver):
|
||||
'token_symbol': 'COFE',
|
||||
'include_pending': True
|
||||
}
|
||||
response = requests.get(
|
||||
testserver.url + '/balance',
|
||||
response = client.get('/balance',
|
||||
params=params)
|
||||
balance = response.json()
|
||||
|
||||
@ -78,8 +114,7 @@ def test_account(testserver):
|
||||
'value': 100,
|
||||
'token_symbol': 'COFE'
|
||||
}
|
||||
response = requests.post(
|
||||
testserver.url + '/transfer',
|
||||
response = client.post('/transfer',
|
||||
params=params)
|
||||
transfer = response.json()
|
||||
|
||||
@ -89,8 +124,7 @@ def test_account(testserver):
|
||||
'token_symbol': 'COFE',
|
||||
'include_pending': True
|
||||
}
|
||||
response = requests.get(
|
||||
testserver.url + '/balance',
|
||||
response = client.get('/balance',
|
||||
params=params)
|
||||
balance_after_transfer = response.json()
|
||||
assert (balance_after_transfer[0] == {
|
||||
@ -107,13 +141,11 @@ def test_account(testserver):
|
||||
'address': address_1,
|
||||
'limit': 10
|
||||
}
|
||||
response = requests.get(
|
||||
testserver.url + '/transactions',
|
||||
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
|
||||
|
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user