Skip to content

Commit

Permalink
🐛 fix server error when add duplicated contract && improve logging on…
Browse files Browse the repository at this point in the history
… routes and tezos module
  • Loading branch information
Quentin Burg committed Dec 13, 2023
1 parent 0405bc2 commit 25f9d8e
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 14 deletions.
37 changes: 24 additions & 13 deletions src/crud.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
from typing import Optional, List
from psycopg2.errors import UniqueViolation
from pydantic import UUID4
from sqlalchemy.orm import Session

from .utils import ContractNotFound, CreditNotFound, EntrypointNotFound, UserNotFound
from .utils import (
ContractAlreadyRegistered,
ContractNotFound,
CreditNotFound,
EntrypointNotFound,
UserNotFound,
)
from . import models, schemas
from sqlalchemy.exc import NoResultFound

Expand Down Expand Up @@ -102,18 +109,22 @@ def get_entrypoint(

def create_contract(db: Session, contract: schemas.ContractCreation):
# TODO rewrite this with transaction or something else better
c = {k: v for k, v in contract.model_dump().items() if k not in ["entrypoints"]}
db_contract = models.Contract(**c)
db.add(db_contract)
db.commit()
db.refresh(db_contract)
db_entrypoints = [
models.Entrypoint(**e.model_dump(), contract_id=db_contract.id)
for e in contract.entrypoints
]
db.add_all(db_entrypoints)
db.commit()
return db_contract
try:
contract = get_contract_by_address(db, contract.address)
raise ContractAlreadyRegistered(f"Contract {contract.address} already added.")
except ContractNotFound:
c = {k: v for k, v in contract.model_dump().items() if k not in ["entrypoints"]}
db_contract = models.Contract(**c)
db.add(db_contract)
db.commit()
db.refresh(db_contract)
db_entrypoints = [
models.Entrypoint(**e.model_dump(), contract_id=db_contract.id)
for e in contract.entrypoints
]
db.add_all(db_entrypoints)
db.commit()
return db_contract


def update_entrypoints(db: Session, entrypoints: list[schemas.EntrypointUpdate]):
Expand Down
35 changes: 34 additions & 1 deletion src/routes.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from math import log
from fastapi import APIRouter, HTTPException, status, Depends
import asyncio

Expand All @@ -6,6 +7,7 @@
from pytezos.rpc.errors import MichelsonError
from pytezos.crypto.encoding import is_address
from .utils import (
ContractAlreadyRegistered,
ContractNotFound,
CreditNotFound,
EntrypointNotFound,
Expand Down Expand Up @@ -40,7 +42,14 @@ async def create_user(
async def create_contract(
contract: schemas.ContractCreation, db: Session = Depends(database.get_db)
):
return crud.create_contract(db, contract)
try:
return crud.create_contract(db, contract)
except ContractAlreadyRegistered:
logging.warn(f"Contract {contract.address} is already registered")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Contract {contract.address} is already registered.",
)


# PUT endpoints
Expand All @@ -61,22 +70,26 @@ async def update_credits(
amount = credits.amount
is_confirmed = await tezos.confirm_deposit(op_hash, payer_address, amount)
if not is_confirmed:
logging.warning(f"Could not find confirmation for {amount} with {op_hash}")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Could not find confirmation for {amount} with {op_hash}",
)
return crud.update_credits(db, credits)
except ContractNotFound:
logging.warning(f"Contrat not found.")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Contrat not found.",
)
except CreditNotFound:
logging.warning(f"Credit not found.")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Credit not found.",
)
except OperationNotFound:
logging.warning(f"Could not find the operation.")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Could not find the operation.",
Expand All @@ -90,18 +103,21 @@ async def withdraw_credits(
try:
credits = crud.get_credits(db, withdraw.id)
except CreditNotFound:
logging.warning(f"Credit not found.")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Credit not found.",
)
if credits.amount < withdraw.amount:
logging.warning(f"Not enough funds to withdraw credit ID {withdraw.id}.")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Not enough funds to withdraw.",
)

expected_counter = credits.owner.withdraw_counter or 0
if expected_counter != withdraw.withdraw_counter:
logging.warning(f"Withdraw counter provided is not the expected counter.")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="Bad withdraw counter."
)
Expand All @@ -113,6 +129,7 @@ async def withdraw_credits(
withdraw.to_micheline_pair(), withdraw.micheline_signature, public_key
)
if not is_valid:
logging.warning(f"Invalid signature")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid signature."
)
Expand All @@ -121,6 +138,7 @@ async def withdraw_credits(
crud.update_user_withdraw_counter(db, str(user.id), withdraw.withdraw_counter + 1)
result = await tezos.withdraw(tezos.tezos_manager, owner_address, withdraw.amount)
if result["result"] == "ok":
logging.debug(f"Start to confirm withdraw for {result['transaction_hash']}")
# Starts a independent loop to check that the operation
# has been confirmed
asyncio.create_task(
Expand All @@ -141,6 +159,7 @@ async def get_user(address_or_id: str, db: Session = Depends(database.get_db)):
else:
return crud.get_user(db, address_or_id)
except UserNotFound:
logging.warning(f"User {address_or_id} not found")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"User not found.",
Expand All @@ -157,6 +176,7 @@ async def credits_for_user(
else:
return crud.get_user(db, user_address_or_id).credits
except UserNotFound:
logging.warning(f"User {user_address_or_id} not found")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"User not found.",
Expand All @@ -169,6 +189,7 @@ async def get_user_contracts(user_address: str, db: Session = Depends(database.g
try:
return crud.get_contracts_by_user(db, user_address)
except UserNotFound:
logging.warning(f"User {user_address} not found.")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=f"User not found."
)
Expand All @@ -179,6 +200,7 @@ async def get_credit(credit_id: str, db: Session = Depends(database.get_db)):
try:
return crud.get_contracts_by_credit(db, credit_id)
except CreditNotFound:
logging.warning(f"Credit {credit_id} not found.")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=f"Credit not found."
)
Expand All @@ -191,6 +213,7 @@ async def get_contract(address_or_id: str, db: Session = Depends(database.get_db
else:
contract = crud.get_contract(db, address_or_id)
if not contract:
logging.warning(f"Contract {address_or_id} not found.")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=f"Contract not found."
)
Expand All @@ -209,10 +232,12 @@ async def get_entrypoints(
assert is_address(contract_address_or_id)
return crud.get_entrypoints(db, contract_address_or_id)
except ContractNotFound:
logging.warning(f"Contract {contract_address_or_id} not found.")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=f"Contract not found."
)
except AssertionError:
logging.warning(f"Invalid address {contract_address_or_id} provided")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid address."
)
Expand All @@ -227,6 +252,7 @@ async def get_entrypoint(
try:
return crud.get_entrypoint(db, contract_address_or_id, name)
except EntrypointNotFound:
logging.warning(f"Entrypoint {contract_address_or_id} not found.")
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND, detail=f"Entrypoint not found."
)
Expand All @@ -238,6 +264,7 @@ async def post_operation(
call_data: schemas.UnsignedCall, db: Session = Depends(database.get_db)
):
if len(call_data.operations) == 0:
logging.warning(f"Operations list is empty")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Empty operations list",
Expand All @@ -248,13 +275,15 @@ async def post_operation(

# Transfers to implicit accounts are always refused
if not contract_address.startswith("KT"):
logging.warning(f"Target {contract_address} is not allowed")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Target {contract_address} is not allowed",
)
try:
contract = crud.get_contract_by_address(db, contract_address)
except ContractNotFound:
logging.warning(f"{contract_address} is not found")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"{contract_address} is not found",
Expand All @@ -265,6 +294,7 @@ async def post_operation(
try:
crud.get_entrypoint(db, str(contract.address), entrypoint_name)
except EntrypointNotFound:
logging.warning(f"Entrypoint {entrypoint_name} is not found")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Entrypoint {entrypoint_name} is not found",
Expand All @@ -277,7 +307,9 @@ async def post_operation(
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)
logging.debug(f"Estimated fees for {op.hash()}: {estimated_fees}")
if not tezos.check_credits(db, estimated_fees):
logging.warning(f"Not enough funds to pay estimated fees.")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="Not enough funds."
)
Expand Down Expand Up @@ -310,6 +342,7 @@ async def signed_operation(
if not tezos.check_signature(
signed_data, call_data.signature, call_data.sender_key, call_data.micheline_type
):
logging.warning("Invalid signature.")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid signature."
)
Expand Down
3 changes: 3 additions & 0 deletions src/tezos.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,9 @@ async def main_loop(self):
except MichelsonError:
# The last operation conflicts with some of the others;
# we refuse it
log.error(
f"Last operation ({acceptable_operations[sender]}) failed and conflicts with some of the others so we discard it."
)
acceptable_operations.pop(sender)
self.results[sender] = "failing"

Expand Down
4 changes: 4 additions & 0 deletions src/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,7 @@ class ConfigurationError(Exception):

class OperationNotFound(Exception):
pass


class ContractAlreadyRegistered(Exception):
pass

0 comments on commit 25f9d8e

Please sign in to comment.