Compare commits

...

55 Commits

Author SHA1 Message Date
William Luke
8aab6c4c2b get some tests working 2022-01-10 15:43:10 +03:00
William Luke
64c9fa2538 docs(cic-eth): Add some basic testing docs 2022-01-10 15:42:54 +03:00
William Luke
4ae5e10779 add eth-tracker and eth-dispatcher and service dependancies 2022-01-05 14:31:10 +03:00
William Luke
6fe1f0108b Merge branch 'master' of https://gitlab.com/grassrootseconomics/cic-internal-integration into cic-eth-server 2022-01-05 12:30:40 +03:00
26616b7245 refactor 2022-01-05 12:16:48 +03:00
ea1198a898 add catch to celery wrapper 2021-12-23 10:36:29 +03:00
cbc1b449ba switch to fastapi :-/ 2021-12-21 21:15:45 +03:00
97aabdb460 Merge branch 'master' of https://gitlab.com/grassrootseconomics/cic-internal-integration into cic-eth-server 2021-12-21 17:37:55 +03:00
3e2947b301 fix: /default_token 2021-12-15 16:03:01 +03:00
William Luke
47af8993d3 fix url 2021-12-08 16:14:41 +03:00
William Luke
6df51f12e1 remove port from EXTRA_PIP_INDEX_URL 2021-12-08 15:53:54 +03:00
William Luke
2ead1b0e51 replace sys.exit with return None 2021-12-08 15:05:17 +03:00
William Luke
1cd3d44502 change GTF to GFT 2021-12-08 15:04:59 +03:00
William Luke
ca8a8d8bfb remove hardcoded DOCKER_REGISTRY 2021-12-08 14:57:09 +03:00
William Luke
447f4bbd41 Merge branch 'master' of https://gitlab.com/grassrootseconomics/cic-internal-integration into cic-eth-server 2021-12-08 14:54:23 +03:00
William Luke
f193fe1989 re-add default docker registery 2021-12-08 14:51:38 +03:00
William Luke
efa923b678 update tos link 2021-12-08 14:33:37 +03:00
William Luke
8d778cff2c remove refill_gas and ping 2021-12-08 14:32:56 +03:00
William Luke
535deb9974 improve docstring 2021-12-08 14:31:49 +03:00
William Luke
a67f3a0dc0 remove unused imports 2021-12-08 14:30:57 +03:00
William Luke
33ad2dbc2d ignore .envrc 2021-12-08 14:30:17 +03:00
William Luke
da890afa81 remove .envrc 2021-12-08 14:30:05 +03:00
William Luke
6fb903d37b revert back to celery.chain 2021-12-02 16:36:19 +03:00
William Luke
d3805022ff remove unintended changes 2021-12-02 16:33:08 +03:00
William Luke
b3df49f89d feat: add wei conversions and caching 2021-12-02 15:07:49 +03:00
William Luke
253725c24f Merge branch 'master' of https://gitlab.com/grassrootseconomics/cic-internal-integration into cic-eth-server 2021-12-01 10:07:36 +03:00
William Luke
ea7fa28b61 remove flask and graphql servers 2021-11-23 15:17:13 +03:00
William Luke
421e1cbf32 add token,tokens,refill_gas to api spec 2021-11-23 11:11:58 +03:00
William Luke
3c3745228e convert api tests to uwsgi 2021-11-22 12:28:09 +03:00
William Luke
6a0544b579 pop required params 2021-11-19 16:50:13 +03:00
William Luke
b42f076d85 use call 2021-11-18 18:03:01 +03:00
a379065891 switch back to uwsgi 2021-11-18 15:38:20 +03:00
William Luke
18efb9fcf3 Merge branch 'master' of https://gitlab.com/grassrootseconomics/cic-internal-integration into cic-eth-server 2021-11-17 15:22:29 +03:00
William Luke
3ba5ef9c8c get tests working 2021-11-17 15:21:23 +03:00
William Luke
11e9881009 setup initial testing 2021-11-16 16:37:16 +03:00
56cd5155f7 remove mocel0xAddress 2021-11-16 11:07:00 +03:00
William Luke
697dbd5c42 add Transaction schema 2021-11-12 12:42:12 +03:00
William Luke
fae434465d fix transactions call 2021-11-12 12:41:57 +03:00
William Luke
59c8895db5 add swagger server 2021-11-12 11:14:40 +03:00
William Luke
c48177c78c Merge branch 'cic-eth-server' of https://github.com/williamluke4/cic-internal-integration into cic-eth-server 2021-11-09 11:48:52 +03:00
b31433dcb5 add database debug 2021-11-09 10:26:04 +03:00
15ea02f540 Merge branch 'lash/token-checksum-address-fix' of https://gitlab.com/grassrootseconomics/cic-internal-integration into cic-eth-server 2021-11-08 12:57:24 +03:00
nolash
8600e282b3
Remove incoming balance in network check 2021-11-08 10:56:11 +01:00
d62cced0a8 Merge branch 'lash/token-checksum-address-fix' of https://gitlab.com/grassrootseconomics/cic-internal-integration into cic-eth-server 2021-11-08 12:44:11 +03:00
nolash
a42efb2529
Non-checksum address in erc20 tx_caches 2021-11-08 10:43:02 +01:00
66b2cb039b Merge branch 'master' of https://gitlab.com/grassrootseconomics/cic-internal-integration into cic-eth-server 2021-11-08 11:57:17 +03:00
f265108485 add eth-server 2021-11-02 18:50:14 +03:00
f28ba1098f revert api_task:balance changes 2021-11-01 13:56:12 +03:00
7c73c8b30f Merge branch 'master' of https://gitlab.com/grassrootseconomics/cic-internal-integration into cic-eth-server 2021-11-01 10:49:07 +03:00
c85692ab9e graphql server 2021-11-01 10:48:05 +03:00
William Luke
68a3d9fe3c save 2021-10-29 15:10:56 +03:00
William Luke
9dd442e716 Merge branch 'master' of https://gitlab.com/grassrootseconomics/cic-internal-integration into cic-eth-server 2021-10-29 15:10:05 +03:00
William Luke
0c84e970ad misc 2021-10-29 11:34:37 +03:00
William Luke
f856ca3f68 init graphene 2021-10-26 15:48:25 +03:00
William Luke
4a1008e75e feat(cic-eth): initial server setup 2021-10-25 17:51:45 +03:00
25 changed files with 892 additions and 17 deletions

1
.gitignore vendored
View File

@ -15,3 +15,4 @@ build/
.idea .idea
**/.vim **/.vim
**/*secret.yaml **/*secret.yaml
.envrc

View File

@ -1 +1,36 @@
# CIC-ETH # CIC-ETH
## Testing CIC-ETH locally.
### Setup a Virtual Env
```bash
python3 -m venv ./venv # Python 3.9
source ./venv/activate
```
### Running All Unit Tests
```bash
bash ./tests/run_tests.sh # This will also install required dependencies
```
### Running Specific Unit Tests
Ensure that:
- You have called `bash ./tests/run_tests.sh` at least once or run the following to install required dependencies
- You have activated the virtual environment
```
pip install --extra-index-url https://pip.grassrootseconomics.net --extra-index-url https://gitlab.com/api/v4/projects/27624814/packages/pypi/simple \
-r admin_requirements.txt \
-r services_requirements.txt \
-r test_requirements.txt
```
Then here is an example that only runs tests with the keyword(-k) `test_server`
```bash
pytest -s -v --log-cli-level DEBUG --log-level DEBUG -k test_server
```

View File

@ -8,9 +8,6 @@ import logging
# external imports # external imports
import celery import celery
from chainlib.chain import ChainSpec
from hexathon import strip_0x
# local imports # local imports
from cic_eth.api.base import ApiBase from cic_eth.api.base import ApiBase
from cic_eth.enum import LockEnum from cic_eth.enum import LockEnum

View File

@ -0,0 +1,12 @@
# standard imports
import os
import random
import uuid
def blockchain_address() -> str:
return os.urandom(20).hex().lower()

View File

@ -0,0 +1,132 @@
# standard imports
import os
# external imports
import pytest
from celery import uuid
# test imports
from cic_eth.pytest.helpers.accounts import blockchain_address
@pytest.fixture(scope='function')
def task_uuid():
return uuid()
@pytest.fixture(scope='function')
def default_token_data(foo_token_symbol, foo_token):
return {
'symbol': foo_token_symbol,
'address': foo_token,
'name': 'Giftable Token',
'decimals': 6,
"converters": []
}
@pytest.fixture(scope='function')
def mock_account_creation_task_request(mocker, task_uuid):
def mock_request(self):
mocked_task_request = mocker.patch('celery.app.task.Task.request')
mocked_task_request.id = task_uuid
return mocked_task_request
mocker.patch('cic_eth.api.api_task.Api.create_account', mock_request)
@pytest.fixture(scope='function')
def mock_account_creation_task_result(mocker, task_uuid):
def task_result(self):
sync_res = mocker.patch('celery.result.AsyncResult')
sync_res.id = task_uuid
sync_res.get.return_value = blockchain_address()
return sync_res
mocker.patch('cic_eth.api.api_task.Api.create_account', task_result)
@pytest.fixture(scope='function')
def mock_token_api_query(foo_token_symbol, foo_token, mocker, task_uuid):
def mock_query(self, token_symbol, proof=None):
sync_res = mocker.patch('celery.result.AsyncResult')
sync_res.id = task_uuid
sync_res.get.return_value = [
{
'address': foo_token,
'converters': [],
'decimals': 6,
'name': 'Giftable Token',
'proofs': ['5b1549818725ca07c19fc47fda5d8d85bbebb1283855d5ab99785dcb7d9051d3'],
'symbol': foo_token_symbol,
},{'5b1549818725ca07c19fc47fda5d8d85bbebb1283855d5ab99785dcb7d9051d3': ['Eb3907eCad74a0013c259D5874AE7f22DcBcC95C','Eb3907eCad74a0013c259D5874AE7f22DcBcC95C']}
]
return sync_res
mocker.patch('cic_eth.api.api_task.Api.token', mock_query)
@pytest.fixture(scope='function')
def mock_tokens_api_query(foo_token_symbol, foo_token, mocker, task_uuid):
def mock_query(self, token_symbol, proof=None):
sync_res = mocker.patch('celery.result.AsyncResult')
sync_res.id = task_uuid
sync_res.get.return_value = [[
{
'address': foo_token,
'converters': [],
'decimals': 6,
'name': 'Giftable Token',
'proofs': ['5b1549818725ca07c19fc47fda5d8d85bbebb1283855d5ab99785dcb7d9051d3'],
'symbol': foo_token_symbol,
},{'5b1549818725ca07c19fc47fda5d8d85bbebb1283855d5ab99785dcb7d9051d3': ['Eb3907eCad74a0013c259D5874AE7f22DcBcC95C','Eb3907eCad74a0013c259D5874AE7f22DcBcC95C']}
], [
{
'address': foo_token,
'converters': [],
'decimals': 6,
'name': 'Giftable Token',
'proofs': ['5b1549818725ca07c19fc47fda5d8d85bbebb1283855d5ab99785dcb7d9051d3'],
'symbol': foo_token_symbol,
},{'5b1549818725ca07c19fc47fda5d8d85bbebb1283855d5ab99785dcb7d9051d3': ['Eb3907eCad74a0013c259D5874AE7f22DcBcC95C','Eb3907eCad74a0013c259D5874AE7f22DcBcC95C']}
]]
return sync_res
mocker.patch('cic_eth.api.api_task.Api.tokens', mock_query)
@pytest.fixture(scope='function')
def mock_sync_balance_api_query(balances, mocker, task_uuid):
def sync_api_query(self, address: str, token_symbol: str):
sync_res = mocker.patch('celery.result.AsyncResult')
sync_res.id = task_uuid
sync_res.get.return_value = balances
return sync_res
mocker.patch('cic_eth.api.api_task.Api.balance', sync_api_query)
@pytest.fixture(scope='function')
def mock_sync_default_token_api_query(default_token_data, mocker, task_uuid):
def mock_query(self):
sync_res = mocker.patch('celery.result.AsyncResult')
sync_res.id = task_uuid
sync_res.get.return_value = default_token_data
return sync_res
mocker.patch('cic_eth.api.api_task.Api.default_token', mock_query)
@pytest.fixture(scope='function')
def mock_transaction_list_query(mocker):
query_args = {}
def mock_query(self, address: str, limit: int):
query_args['address'] = address
query_args['limit'] = limit
mocker.patch('cic_eth.api.api_task.Api.list', mock_query)
return query_args
@pytest.fixture(scope='function')
def mock_transfer_api(mocker):
transfer_args = {}
def mock_transfer(self, from_address: str, to_address: str, value: int, token_symbol: str):
transfer_args['from_address'] = from_address
transfer_args['to_address'] = to_address
transfer_args['value'] = value
transfer_args['token_symbol'] = token_symbol
mocker.patch('cic_eth.api.api_task.Api.transfer', mock_transfer)
return transfer_args

View File

@ -1,15 +1,11 @@
#!/usr/bin/python #!/usr/bin/python
import sys import sys
import os
import logging import logging
import uuid import uuid
import json import json
import argparse
# external imports # external imports
import redis import redis
from xdg.BaseDirectory import xdg_config_home
from chainlib.chain import ChainSpec
# local imports # local imports
import cic_eth.cli import cic_eth.cli

View File

@ -0,0 +1,38 @@
import logging
import cic_eth.cli
from cic_eth.server.app import create_app
from cic_eth.server.celery import create_celery_wrapper
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()
config = cic_eth.cli.Config.from_args(args, arg_flags, local_arg_flags)
# Define log levels
if args.vv:
logging.getLogger().setLevel(logging.DEBUG)
elif args.v:
logging.getLogger().setLevel(logging.INFO)
# Setup Celery App
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')
redis_host = config.get('REDIS_HOST')
redis_port = config.get('REDIS_PORT')
redis_db = config.get('REDIS_DB')
redis_timeout = config.get('REDIS_TIMEOUT')
celery_wrapper = create_celery_wrapper(celery_queue=celery_queue, chain_spec=chain_spec,
redis_db=redis_db, redis_host=redis_host, redis_port=redis_port, redis_timeout=redis_timeout)
app = create_app(celery_wrapper)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=5000, log_level="info")

View File

@ -0,0 +1,3 @@
from . import converters
from . import cache
from . import celery

View File

@ -0,0 +1,119 @@
import logging
import sys
from typing import List, Optional, Union
from cic_eth.server import cache, converters
from cic_eth.server.cache import setup_cache
from cic_eth.server.celery import create_celery_wrapper
from cic_eth.server.models import (DefaultToken, Token, TokenBalance,
Transaction)
from fastapi import FastAPI, Query
log = logging.getLogger(__name__)
def create_app(celery_wrapper):
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_wrapper('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_wrapper('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_wrapper(
'create_account', password=password, register=register)
return data
# def refill_gas(start_response, query: dict):
# address = query.pop('address')
# data = celery_wrapper('refill_gas', address)
# return data
# def ping(start_response, query: dict):
# data = celery_wrapper('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_wrapper('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_wrapper('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_wrapper('token', token_symbol, proof=proof)
token = Token.new(data)
sys.stderr.write(f"Token {token}")
return token
@app.get("/tokens", response_model=List[Token])
def tokens(token_symbols: Optional[List[str]] = Query(None), proof: Optional[Union[str, List[str], List[List[str]]]] = None):
data = celery_wrapper('tokens', token_symbols,
catch=len(token_symbols), proof=proof)
if data:
tokens = []
for token in data:
print(f"Token: {token}")
tokens.append(Token.new(token))
return tokens
return None
@app.get("/default_token", response_model=DefaultToken)
def default_token():
data = celery_wrapper('default_token')
return data
def get_token(token_symbol: str):
data = celery_wrapper('token', token_symbol)
return Token.new(data)
return app

View File

@ -0,0 +1,130 @@
# standard imports
import hashlib
import json
import logging
from typing import Optional, Union
from cic_eth.server.models import Token
from cic_types.condiments import MetadataPointer
from redis import Redis, StrictRedis
logg = logging.getLogger(__file__)
class Cache:
store: Redis = None
def setup_cache(redis_host, redis_port, redis_db):
# Define universal redis cache access
Cache.store = StrictRedis(
host=redis_host, port=redis_port, db=redis_db, decode_responses=True)
def get_token_data(token_symbol: str):
"""
:param token_symbol:
:type token_symbol:
:return:
:rtype:
"""
identifier = [token_symbol.encode('utf-8')]
key = cache_data_key(identifier, MetadataPointer.TOKEN_DATA)
logg.debug(f'Retrieving token data for: {token_symbol} at: {key}')
token_data_str = get_cached_data(key=key)
if(token_data_str is None):
logg.debug(f'No token data found for: {token_symbol}')
return None
else:
token_data = json.loads(token_data_str)
logg.debug(f'Retrieved token data: {token_data}')
return token_data
def set_token_data(token_symbol: str, token: dict):
"""
:param token_symbol:
:type token_symbol:
:return:
:rtype:
"""
identifier = [token_symbol.encode('utf-8')]
key = cache_data_key(identifier, MetadataPointer.TOKEN_DATA)
cache_data(key, json.dumps(token))
logg.debug(f'Cached token data for: {token_symbol} at: {key}')
def get_default_token() -> Optional[str]:
"""This function attempts to retrieve the default token's data from the redis cache.
:return:
:rtype:
"""
logg.debug(f'Retrieving default token from cache')
# TODO: What should the identifier be?
key = cache_data_key(identifier="ff".encode('utf-8'),
salt=MetadataPointer.TOKEN_DEFAULT)
default_token_str = get_cached_data(key=key)
if default_token_str is None:
logg.debug(f'No cached default token found: {key}')
return None
default_token = json.loads(default_token_str)
logg.debug(f'Retrieved default token data: {default_token}')
return default_token
def set_default_token(default_token: dict):
"""
:param default_token:
:type default_token:
:return:
:rtype:
"""
logg.debug(f'Setting default token in cache')
key = cache_data_key(identifier="ff".encode('utf-8'),
salt=MetadataPointer.TOKEN_DEFAULT)
cache_data(key, json.dumps(default_token))
def cache_data(key: str, data: str):
"""
:param key:
:type key:
:param data:
:type data:
:return:
:rtype:
"""
cache = Cache.store
cache.set(name=key, value=data)
cache.persist(name=key)
logg.debug(f'caching: {data} with key: {key}.')
def get_cached_data(key: str):
"""
:param key:
:type key:
:return:
:rtype:
"""
cache = Cache.store
return cache.get(name=key)
def cache_data_key(identifier: Union[list, bytes], salt: MetadataPointer):
"""
:param identifier:
:type identifier:
:param salt:
:type salt:
:return:
:rtype:
"""
hash_object = hashlib.new("sha256")
if isinstance(identifier, list):
for identity in identifier:
hash_object.update(identity)
else:
hash_object.update(identifier)
hash_object.update(salt.value.encode(encoding="utf-8"))
return hash_object.digest().hex()

View File

@ -0,0 +1,64 @@
import json
import logging
import sys
import uuid
import redis
from cic_eth.api.api_task import Api
log = logging.getLogger(__name__)
def create_celery_wrapper(chain_spec,
celery_queue,
redis_host,
redis_port,
redis_db,
redis_timeout):
def call(method, *args, catch=1, **kwargs):
""" Creates a redis channel and calls `cic_eth.api` with the provided `method` and `*args`. Returns the result of the api call. Catch allows you to specify how many messages to catch before returning.
"""
log.debug(f"Using redis: {redis_host}, {redis_port}, {redis_db}")
redis_channel = str(uuid.uuid4())
r = redis.Redis(redis_host, redis_port, redis_db)
ps = r.pubsub()
ps.subscribe(redis_channel)
api = Api(
chain_spec,
queue=celery_queue,
callback_param='{}:{}:{}:{}'.format(
redis_host, redis_port, redis_db, redis_channel),
callback_task='cic_eth.callbacks.redis.redis',
callback_queue=celery_queue,
)
getattr(api, method)(*args, **kwargs)
ps.get_message()
try:
data = []
if catch == 1:
message = ps.get_message(timeout=redis_timeout)
data = json.loads(message['data'])["result"]
raise data
else:
for _i in range(catch):
message = ps.get_message(
timeout=redis_timeout)
result = json.loads(message['data'])["result"]
data.append(result)
except TimeoutError as e:
sys.stderr.write(
f"cic_eth.api.{method}({args}, {kwargs}) timed out:\n {e}")
raise e
except Exception as e:
sys.stderr.write(
f'Unable to parse Data:\n{data}\n Error:\n{e}')
raise e
log.debug(
f"cic_eth.api.{method}(args={args}, kwargs={kwargs})\n {data}")
ps.unsubscribe()
return data
return call

View File

@ -0,0 +1,39 @@
# Stolen from ussd
from math import trunc
def from_wei(decimals: int, value: int) -> float:
"""This function converts values in Wei to a token in the cic network.
:param decimals: The decimals required for wei values.
:type decimals: int
:param value: Value in Wei
:type value: int
:return: SRF equivalent of value in Wei
:rtype: float
"""
value = float(value) / (10**decimals)
return truncate(value=value, decimals=2)
def to_wei(decimals: int, value: int) -> int:
"""This functions converts values from a token in the cic network to Wei.
:param decimals: The decimals required for wei values.
:type decimals: int
:param value: Value in SRF
:type value: int
:return: Wei equivalent of value in SRF
:rtype: int
"""
return int(value * (10**decimals))
def truncate(value: float, decimals: int) -> float:
"""This function truncates a value to a specified number of decimals places.
:param value: The value to be truncated.
:type value: float
:param decimals: The number of decimals for the value to be truncated to
:type decimals: int
:return: The truncated value.
:rtype: int
"""
stepper = 10.0**decimals
return trunc(stepper*value) / stepper

View 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()

View File

@ -0,0 +1,5 @@
[redis]
host=redis
database=0
password=
port=6379

View File

@ -5,3 +5,5 @@ urlybird~=0.0.1
cic-eth-registry~=0.6.6 cic-eth-registry~=0.6.6
cic-types~=0.2.1a8 cic-types~=0.2.1a8
cic-eth-aux-erc20-demurrage-token~=0.0.3 cic-eth-aux-erc20-demurrage-token~=0.0.3
fastapi[all]==0.70.1
uvicorn[standard]<0.16.0

View File

@ -36,6 +36,7 @@ packages =
cic_eth.db.models cic_eth.db.models
cic_eth.queue cic_eth.queue
cic_eth.ext cic_eth.ext
cic_eth.server
cic_eth.runnable cic_eth.runnable
cic_eth.runnable.daemons cic_eth.runnable.daemons
cic_eth.runnable.daemons.filters cic_eth.runnable.daemons.filters

View File

@ -24,6 +24,8 @@ from cic_eth.pytest.fixtures_database import *
from cic_eth.pytest.fixtures_role import * from cic_eth.pytest.fixtures_role import *
from cic_eth.pytest.fixtures_contract import * from cic_eth.pytest.fixtures_contract import *
from cic_eth.pytest.fixtures_token import * from cic_eth.pytest.fixtures_token import *
from cic_eth.pytest.patches.account import *
from chainlib.eth.pytest import * from chainlib.eth.pytest import *
from eth_contract_registry.pytest import * from eth_contract_registry.pytest import *
from cic_eth_registry.pytest.fixtures_contracts import * from cic_eth_registry.pytest.fixtures_contracts import *

View File

@ -2,9 +2,9 @@
set -e set -e
pip install --extra-index-url https://pip.grassrootseconomics.net --extra-index-url https://gitlab.com/api/v4/projects/27624814/packages/pypi/simple pip install --extra-index-url https://pip.grassrootseconomics.net --extra-index-url https://gitlab.com/api/v4/projects/27624814/packages/pypi/simple \
-r admin_requirements.txt -r admin_requirements.txt \
-r services_requirements.txt -r services_requirements.txt \
-r test_requirements.txt -r test_requirements.txt
export PYTHONPATH=. && pytest -x --cov=cic_eth --cov-fail-under=90 --cov-report term-missing tests export PYTHONPATH=. && pytest -x --cov=cic_eth --cov-fail-under=90 --cov-report term-missing tests

View File

@ -0,0 +1,171 @@
# coding: utf-8
from __future__ import absolute_import
import logging
import time
import hexathon
import pytest
from cic_eth.server.app import create_app
from cic_eth.server.celery import create_celery_wrapper
from fastapi.testclient import TestClient
log = logging.getLogger(__name__)
@pytest.fixture(scope='function')
def celery_wrapper(api):
""" Creates a redis channel and calls `cic_eth.api` with the provided `method` and `*args`. Returns the result of the api call. Catch allows you to specify how many messages to catch before returning.
"""
def wrapper(method, *args, catch=1, **kwargs):
t = getattr(api, method)(*args, **kwargs)
return t.get()
return wrapper
@pytest.fixture(scope='function')
def client(celery_wrapper):
app = create_app(celery_wrapper)
return TestClient(app)
def test_default_token(client,mock_sync_default_token_api_query):
# Default Token
response = client.get('/default_token')
log.debug(f"balance response {response}")
default_token = response.json()
assert default_token == {'symbol': 'FOO', 'address': '0xe7c559c40B297d7f039767A2c3677E20B24F1385', 'name': 'Giftable Token', 'decimals': 6}
def test_token(client, mock_token_api_query):
# Default Token
response = client.get('/token?token_symbol=FOO')
log.debug(f"token response {response}")
token = response.json()
assert token == {
'address': '0xe7c559c40B297d7f039767A2c3677E20B24F1385',
'converters': [],
'decimals': 6,
'name': 'Giftable Token',
'proofs': ['5b1549818725ca07c19fc47fda5d8d85bbebb1283855d5ab99785dcb7d9051d3'],
'proofs_with_signers': [{'proof': '5b1549818725ca07c19fc47fda5d8d85bbebb1283855d5ab99785dcb7d9051d3',
'signers': ['Eb3907eCad74a0013c259D5874AE7f22DcBcC95C', 'Eb3907eCad74a0013c259D5874AE7f22DcBcC95C']}],
'symbol': 'FOO',
}
def test_tokens(client, mock_tokens_api_query):
# Default Token
response = client.get(
'/tokens', params={'token_symbols': ['FOO', 'FOO']})
log.debug(f"tokens response {response}")
tokens = response.json()
assert tokens == [
{
'address': '0xe7c559c40B297d7f039767A2c3677E20B24F1385',
'converters': [],
'decimals': 6,
'name': 'Giftable Token',
'proofs': ['5b1549818725ca07c19fc47fda5d8d85bbebb1283855d5ab99785dcb7d9051d3'],
'proofs_with_signers': [{'proof': '5b1549818725ca07c19fc47fda5d8d85bbebb1283855d5ab99785dcb7d9051d3',
'signers': ['Eb3907eCad74a0013c259D5874AE7f22DcBcC95C', 'Eb3907eCad74a0013c259D5874AE7f22DcBcC95C']}],
'symbol': 'FOO',
},
{
'address': '0xe7c559c40B297d7f039767A2c3677E20B24F1385',
'converters': [],
'decimals': 6,
'name': 'Giftable Token',
'proofs': ['5b1549818725ca07c19fc47fda5d8d85bbebb1283855d5ab99785dcb7d9051d3'],
'proofs_with_signers': [{'proof': '5b1549818725ca07c19fc47fda5d8d85bbebb1283855d5ab99785dcb7d9051d3',
'signers': ['Eb3907eCad74a0013c259D5874AE7f22DcBcC95C', 'Eb3907eCad74a0013c259D5874AE7f22DcBcC95C']}],
'symbol': 'FOO',
},
]
@pytest.mark.skip("Not implemented")
def test_account(client):
# Default Token
response = client.get('/default_token')
log.debug(f"balance response {response}")
default_token = response.json()
# Create Account 1
params = {
'password': '',
'register': True
}
response = client.post(
'/create_account',
params=params)
address_1 = hexathon.valid(response.json())
# Create Account 2
params = {
'password': '',
'register': True
}
response = client.post('/create_account',
params=params)
address_2 = hexathon.valid(response.json())
time.sleep(30) # Required to allow balance to show
# Balance Account 1
params = {
'address': address_1,
'token_symbol': 'COFE',
'include_pending': True
}
response = client.get('/balance',
params=params)
balance = response.json()
assert (balance[0] == {
"address": default_token.get('address').lower(),
"balance_available": 30000,
"balance_incoming": 0,
"balance_network": 30000,
"balance_outgoing": 0,
"converters": []
})
# Transfer
params = {
'from_address': address_1,
'to_address': address_2,
'value': 100,
'token_symbol': 'COFE'
}
response = client.post('/transfer',
params=params)
transfer = response.json()
# Balance Account 1
params = {
'address': address_1,
'token_symbol': 'COFE',
'include_pending': True
}
response = client.get('/balance',
params=params)
balance_after_transfer = response.json()
assert (balance_after_transfer[0] == {
"address": default_token.get('address').lower(),
"balance_available": 29900,
"balance_incoming": 0,
"balance_network": 30000,
"balance_outgoing": 100,
"converters": []
})
# Transactions Account 1
params = {
'address': address_1,
'limit': 10
}
response = client.get('/transactions',
params=params)
transactions = response.json()
# TODO: What are the other 2 transactions
assert len(transactions) == 3
# Check the transaction is correct
# TODO wtf is READSEND (Ready to send? Or already sent)
assert transactions[0].status == 'READYSEND'

View File

@ -1,5 +1,5 @@
[PIP] [PIP]
extra_index_host = pip.grassrootseconomics.net extra_index_host = pip.grassrootseconomics.net
extra_index_port = 8433 extra_index_port =
extra_index_path = / extra_index_path = /
extra_index_proto = https extra_index_proto = https

View File

@ -1,5 +1,5 @@
[PIP] [PIP]
extra_index_host = pip.grassrootseconomics.net extra_index_host = pip.grassrootseconomics.net
extra_index_port = 8433 extra_index_port =
extra_index_path = / extra_index_path = /
extra_index_proto = https extra_index_proto = https

View File

@ -172,6 +172,42 @@ services:
set +a set +a
./start_tasker.sh --aux-all -q cic-eth -vv ./start_tasker.sh --aux-all -q cic-eth -vv
cic-eth-server:
image: ${DEV_DOCKER_REGISTRY:-registry.gitlab.com/grassrootseconomics}/cic-eth:${TAG:-latest}
ports:
- 5000:5000
build:
context: apps/cic-eth
dockerfile: docker/Dockerfile
args:
DOCKER_REGISTRY: ${DEV_DOCKER_REGISTRY:-registry.gitlab.com/grassrootseconomics}
PIP_INDEX_URL: ${PIP_INDEX_URL:-https://pypi.org/simple}
EXTRA_PIP_INDEX_URL: ${EXTRA_PIP_INDEX_URL:-https://pip.grassrootseconomics.net}
EXTRA_PIP_ARGS: $EXTRA_PIP_ARGS
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}
restart: unless-stopped
depends_on:
- cic-eth-tasker
- cic-eth-tracker
- cic-eth-dispatcher
volumes:
# - ./apps/cic-eth/:/root
- signer-data:/run/crypto-dev-signer
- contract-config:/tmp/cic/config/:ro
command:
- /bin/bash
- -c
- |
set -a
if [[ -f /tmp/cic/config/env_reset ]]; then source /tmp/cic/config/env_reset; fi
set +a
python -m cic_eth.runnable.daemons.server
cic-eth-tracker: cic-eth-tracker:
image: ${IMAGE_BASE_URL:-registry.gitlab.com/grassrootseconomics/cic-internal-integration}/cic-eth:${TAG:-latest} image: ${IMAGE_BASE_URL:-registry.gitlab.com/grassrootseconomics/cic-internal-integration}/cic-eth:${TAG:-latest}