diff --git a/dev-requirements.txt b/dev-requirements.txt index c298dc1..3abf915 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,3 +1,4 @@ jupyter pytest -black \ No newline at end of file +black +pylance \ No newline at end of file diff --git a/src/config.py b/src/config.py index e23c2b9..5cccec1 100644 --- a/src/config.py +++ b/src/config.py @@ -1,12 +1,13 @@ from dotenv import load_dotenv import os import subprocess +import logging - -load_dotenv() +load_dotenv(override=True) TEZOS_RPC = os.getenv("TEZOS_RPC") SECRET_KEY_CMD = os.getenv("SECRET_KEY_CMD") +LEVEL = os.getenv("LEVEL", logging.INFO) if SECRET_KEY_CMD is not None: command = SECRET_KEY_CMD.split() @@ -18,3 +19,8 @@ assert TEZOS_RPC is not None, "Please specify a TEZOS_RPC" assert SECRET_KEY is not None and len(SECRET_KEY) > 0, "Could not read secret key" + + +# -- LOGGING -- + +logging.basicConfig(level=LEVEL, format="%(levelname)s: %(message)s") diff --git a/src/crud.py b/src/crud.py index e806b61..4ae7d00 100644 --- a/src/crud.py +++ b/src/crud.py @@ -2,8 +2,7 @@ from pydantic import UUID4 from sqlalchemy.orm import Session -from src.utils import ContractNotFound, CreditNotFound -from src.utils import EntrypointNotFound, UserNotFound +from .utils import ContractNotFound, CreditNotFound, EntrypointNotFound, UserNotFound from . import models, schemas from sqlalchemy.exc import NoResultFound diff --git a/src/database.py b/src/database.py index cd44330..708b458 100644 --- a/src/database.py +++ b/src/database.py @@ -3,7 +3,8 @@ from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker -from src.utils import ConfigurationError +from .utils import ConfigurationError +from .config import logging def config(filename="sql/database.ini", section="postgresql"): @@ -30,7 +31,7 @@ def config(filename="sql/database.ini", section="postgresql"): SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) Base = declarative_base() except Exception as e: - print(e) + logging.error(f"Error occurred on database configuration : {e}") raise ConfigurationError("Cannot connect to database.") diff --git a/src/models.py b/src/models.py index d0c64c1..60bdb6c 100644 --- a/src/models.py +++ b/src/models.py @@ -9,6 +9,11 @@ class User(Base): __tablename__ = "users" + def __repr__(self): + return "User(id='{}', name='{}', address='{}')".format( + self.id, self.name, self.address + ) + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) name = Column(String) address = Column(String, unique=True) @@ -66,6 +71,11 @@ def __repr__(self): class Credit(Base): __tablename__ = "credits" + def __repr__(self): + return "Credit(id='{}', amount='{}', owner_id='{}')".format( + self.id, self.amount, self.owner_id + ) + id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) amount = Column(Integer, default=0) owner_id = Column(UUID(as_uuid=True), ForeignKey("users.id")) diff --git a/src/routes.py b/src/routes.py index f4cd8a5..6d9fed8 100644 --- a/src/routes.py +++ b/src/routes.py @@ -1,16 +1,18 @@ -from src import database from fastapi import APIRouter, HTTPException, status, Depends -from typing import List -import src.crud as crud -import src.schemas as schemas -import uuid import asyncio from sqlalchemy.orm import Session -from . import tezos +from . import tezos, crud, schemas, database from pytezos.rpc.errors import MichelsonError from pytezos.crypto.encoding import is_address -from .utils import ContractNotFound, CreditNotFound, EntrypointNotFound, UserNotFound +from .utils import ( + ContractNotFound, + CreditNotFound, + EntrypointNotFound, + UserNotFound, + OperationNotFound, +) +from .config import logging router = APIRouter() @@ -74,7 +76,7 @@ async def update_credits( status_code=status.HTTP_404_NOT_FOUND, detail=f"Credit not found.", ) - except tezos.OperationNotFound: + except OperationNotFound: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Could not find the operation.", @@ -255,23 +257,24 @@ async def post_operation( except ContractNotFound: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=f"Target {contract_address} is not allowed", + detail=f"{contract_address} is not found", ) entrypoint_name = operation["parameters"]["entrypoint"] - print(contract_address, entrypoint_name) + try: crud.get_entrypoint(db, str(contract.address), entrypoint_name) except EntrypointNotFound: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, - detail=f"Entrypoint {entrypoint_name} is not allowed", + detail=f"Entrypoint {entrypoint_name} is not found", ) try: # Simulate the operation alone without sending it # TODO: log the result op = tezos.simulate_transaction(call_data.operations) + logging.debug(f"Result of operation simulation : {op}") op_estimated_fees = [(int(x["fee"]), x["destination"]) for x in op.contents] estimated_fees = tezos.group_fees(op_estimated_fees) if not tezos.check_credits(db, estimated_fees): @@ -281,14 +284,14 @@ async def post_operation( result = await tezos.tezos_manager.queue_operation(call_data.sender_address, op) except MichelsonError as e: print("Received failing operation, discarding") - print(e) + logging.error(f"Invalid operation {e}") raise HTTPException( # FIXME? Is this the best one? status_code=status.HTTP_400_BAD_REQUEST, detail=f"Operation is invalid", ) except Exception as e: - print(e) + logging.error(f"Unknown error on /operation : {e}") raise HTTPException( # FIXME? Is this the best one? status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, @@ -311,7 +314,7 @@ async def signed_operation( status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid signature." ) address = tezos.public_key_hash(call_data.sender_key) - call_data = schemas.UnsignedCall( + call_data_unsigned = schemas.UnsignedCall( sender_address=address, operations=call_data.operations ) - return await post_operation(call_data, db) + return await post_operation(call_data_unsigned, db) diff --git a/src/tezos.py b/src/tezos.py index 7d49bcc..eee5e5b 100644 --- a/src/tezos.py +++ b/src/tezos.py @@ -2,15 +2,15 @@ import asyncio from typing import Union -from src import database -# from .pytezos import ptz, pytezos -from . import crud, schemas +from . import crud, schemas, config, database +from .utils import OperationNotFound from pytezos.rpc.errors import MichelsonError -from pytezos.michelson.types import MichelsonType +from pytezos.michelson.types.base import MichelsonType import pytezos -from . import config + +log = config.logging # Config stuff for pytezos assert ( @@ -19,15 +19,14 @@ admin_key = pytezos.pytezos.key.from_encoded_key(config.SECRET_KEY) ptz = pytezos.pytezos.using(config.TEZOS_RPC, admin_key) -print(f"INFO: API address is {ptz.key.public_key_hash()}") +log.info(f"API address is {ptz.key.public_key_hash()}") constants = ptz.shell.block.context.constants() -class OperationNotFound(Exception): - pass - - async def find_transaction(tx_hash): + """Finds the transaction from its hash. + This function searches the last 10 blocks + """ block_time = int(constants["minimal_block_delay"]) nb_try = 0 while nb_try < 4: @@ -74,7 +73,7 @@ def check_credits(db, estimated_fees): for address, total_fee in estimated_fees.items(): credits = crud.get_credits_from_contract_address(db, address) if total_fee > credits.amount: - print( + log.warning( f"Unsufficient credits {credits.amount} for contract" + f"{address}; total fees are {total_fee}." ) @@ -85,10 +84,6 @@ def check_credits(db, estimated_fees): async def confirm_deposit(tx_hash, payer, amount: Union[int, str]): receiver = ptz.key.public_key_hash() op_result = await find_transaction(tx_hash) - print(op_result["contents"]) - print("payer " + str(payer)) - print("receiver " + str(receiver)) - print("amount " + str(amount)) return any( op for op in op_result["contents"] @@ -100,6 +95,9 @@ async def confirm_deposit(tx_hash, payer, amount: Union[int, str]): async def confirm_withdraw(tx_hash, db, user_id, withdraw): + """Ensure withdraw transaction is successful to update credits user. \n + Can raise an OperationNotFound exception if transaction is not found. + """ await find_transaction(tx_hash) credit_update = schemas.CreditUpdate( id=withdraw.id, amount=-withdraw.amount, owner_id=user_id, operation_hash="" @@ -142,6 +140,7 @@ def check_signature(pair_data, signature, public_key, pair_type=None): # .verify raises an exception when the verification fails return public_key.verify(message=packed_pair, signature=signature) except ValueError: + log.error(f"Signature {signature} for {public_key} is not valid.") return False @@ -167,7 +166,6 @@ def __init__(self, ptz): # Receive an operation from sender and add it to the waiting queue; # blocks until there is a result in self.results async def queue_operation(self, sender, operation): - print(operation) self.results[sender] = "waiting" self.ops_queue[sender] = operation while self.results[sender] == "waiting": @@ -176,6 +174,7 @@ async def queue_operation(self, sender, operation): await asyncio.sleep(1) if self.results[sender] == "waiting": + log.error(f"Still waiting for transaction from {sender}... Abort") raise Exception() return { @@ -209,7 +208,7 @@ async def main_loop(self): # TODO catch errors n_ops = len(self.ops_queue) - print(f"found {n_ops} operations to send") + log.debug(f"found {n_ops} operations to send") acceptable_operations = OrderedDict() for sender in self.ops_queue: op = self.ops_queue[sender] @@ -223,8 +222,9 @@ async def main_loop(self): self.results[sender] = "failing" n_ops = len(acceptable_operations) - print(f"found {n_ops} valid operations to send") + log.debug(f"found {n_ops} valid operations to send") if n_ops > 0: + log.info(f"{n_ops} operations to process and send") # Post all the correct operations together and get the # result from the RPC to know what the real fees were posted_tx = ptz.bulk(*acceptable_operations.values()).send() @@ -233,9 +233,10 @@ async def main_loop(self): self.results[k] = {"transaction": posted_tx} asyncio.create_task(self.update_fees(posted_tx)) self.ops_queue = dict() - print("Tezos loop executed") - except Exception: + log.debug("Tezos loop executed") + except Exception as e: # FIXME: Should we raise an Exception here ? + log.error(f"Error occurred on main loop : {e}") pass diff --git a/src/utils.py b/src/utils.py index ba11c52..3b8b7a3 100644 --- a/src/utils.py +++ b/src/utils.py @@ -1,6 +1,4 @@ # -- EXCEPTIONS -- - - class UserNotFound(Exception): pass @@ -19,3 +17,7 @@ class CreditNotFound(Exception): class ConfigurationError(Exception): pass + + +class OperationNotFound(Exception): + pass