From 1589c8c65ddba531ea26720a3aada4f91f918b5b Mon Sep 17 00:00:00 2001 From: kompotkot Date: Thu, 3 Aug 2023 12:36:13 +0000 Subject: [PATCH] Fixed metatx contract routes to work with new db model --- engineapi/engineapi/contracts_actions.py | 111 +++++++++++++-------- engineapi/engineapi/data.py | 24 ----- engineapi/engineapi/routes/metatx.py | 118 ++++++++++++++++------- 3 files changed, 157 insertions(+), 96 deletions(-) diff --git a/engineapi/engineapi/contracts_actions.py b/engineapi/engineapi/contracts_actions.py index feb02220..6427ff74 100644 --- a/engineapi/engineapi/contracts_actions.py +++ b/engineapi/engineapi/contracts_actions.py @@ -6,13 +6,21 @@ from datetime import timedelta from typing import Any, Dict, List, Optional, Tuple from sqlalchemy import func, text +from sqlalchemy.dialects.postgresql import insert +from sqlalchemy.engine import Row from sqlalchemy.exc import IntegrityError, NoResultFound from sqlalchemy.orm import Session from web3 import Web3 from . import data, db from .data import ContractType -from .models import Blockchain, CallRequest, CallRequestType, RegisteredContract +from .models import ( + Blockchain, + CallRequest, + CallRequestType, + MetatxHolder, + RegisteredContract, +) logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) @@ -30,6 +38,12 @@ class InvalidAddressFormat(Exception): """ +class UnsupportedBlockchain(Exception): + """ + Raised when unsupported blockchain specified. + """ + + class ContractAlreadyRegistered(Exception): pass @@ -72,23 +86,33 @@ def validate_method_and_params( def register_contract( db_session: Session, - blockchain: str, + blockchain_name: str, address: str, - contract_type: ContractType, - moonstream_user_id: uuid.UUID, + metatx_holder_id: uuid.UUID, title: Optional[str], description: Optional[str], image_uri: Optional[str], -) -> data.RegisteredContract: +) -> Tuple[RegisteredContract, Blockchain]: """ Register a contract against the Engine instance """ try: + blockchain = ( + db_session.query(Blockchain) + .filter(Blockchain.name == blockchain_name) + .one_or_none() + ) + if blockchain is None: + raise UnsupportedBlockchain("Unsupported blockchain specified") + + metatx_holder_stmt = insert(MetatxHolder.__table__).values(id=metatx_holder_id) + metatx_holder_stmt_do_nothing_stmt = metatx_holder_stmt.on_conflict_do_nothing() + db_session.execute(metatx_holder_stmt_do_nothing_stmt) + contract = RegisteredContract( - blockchain=blockchain, + blockchain_id=blockchain.id, address=Web3.toChecksumAddress(address), - contract_type=contract_type.value, - moonstream_user_id=moonstream_user_id, + metatx_holder_id=metatx_holder_id, title=title, description=description, image_uri=image_uri, @@ -103,38 +127,42 @@ def register_contract( logger.error(repr(err)) raise - return contract + return (contract, blockchain) def update_registered_contract( db_session: Session, - moonstream_user_id: uuid.UUID, + metatx_holder_id: uuid.UUID, contract_id: uuid.UUID, title: Optional[str] = None, description: Optional[str] = None, image_uri: Optional[str] = None, ignore_nulls: bool = True, -) -> data.RegisteredContract: +) -> Tuple[RegisteredContract, Blockchain]: """ - Update the registered contract with the given contract ID provided that the user with moonstream_user_id + Update the registered contract with the given contract ID provided that the user with metatx_holder_id has access to it. """ - query = db_session.query(RegisteredContract).filter( - RegisteredContract.id == contract_id, - RegisteredContract.moonstream_user_id == moonstream_user_id, + contract_with_blockchain = ( + db_session.query(RegisteredContract, Blockchain) + .join(Blockchain, Blockchain.id == RegisteredContract.blockchain_id) + .filter( + RegisteredContract.id == contract_id, + RegisteredContract.metatx_holder_id == metatx_holder_id, + ) + .one() ) - - contract = query.one() + registered_contract, blockchain = contract_with_blockchain if not (title is None and ignore_nulls): - contract.title = title + registered_contract.title = title if not (description is None and ignore_nulls): - contract.description = description + registered_contract.description = description if not (image_uri is None and ignore_nulls): - contract.image_uri = image_uri + registered_contract.image_uri = image_uri try: - db_session.add(contract) + db_session.add(registered_contract) db_session.commit() except Exception as err: logger.error( @@ -143,24 +171,27 @@ def update_registered_contract( db_session.rollback() raise - return contract + return (registered_contract, blockchain) def get_registered_contract( db_session: Session, - moonstream_user_id: uuid.UUID, + metatx_holder_id: uuid.UUID, contract_id: uuid.UUID, -) -> RegisteredContract: +) -> Tuple[RegisteredContract, Blockchain]: """ Get registered contract by ID. """ - contract = ( - db_session.query(RegisteredContract) - .filter(RegisteredContract.moonstream_user_id == moonstream_user_id) + contract_with_blockchain = ( + db_session.query(RegisteredContract, Blockchain) + .join(Blockchain, Blockchain.id == RegisteredContract.blockchain_id) + .filter(RegisteredContract.metatx_holder_id == metatx_holder_id) .filter(RegisteredContract.id == contract_id) .one() ) - return contract + registered_contract, blockchain = contract_with_blockchain + + return (registered_contract, blockchain) def lookup_registered_contracts( @@ -170,7 +201,7 @@ def lookup_registered_contracts( address: Optional[str] = None, limit: int = 10, offset: Optional[int] = None, -) -> List[Tuple[RegisteredContract, Blockchain]]: +) -> List[Row[Tuple[RegisteredContract, Blockchain]]]: """ Lookup a registered contract """ @@ -191,35 +222,39 @@ def lookup_registered_contracts( if offset is not None: query = query.offset(offset) - registered_contracts_with_blockchain = query.limit(limit).all() + contracts_with_blockchain = query.limit(limit).all() - return registered_contracts_with_blockchain + return contracts_with_blockchain def delete_registered_contract( db_session: Session, - moonstream_user_id: uuid.UUID, + metatx_holder_id: uuid.UUID, registered_contract_id: uuid.UUID, -) -> RegisteredContract: +) -> Tuple[RegisteredContract, Blockchain]: """ Delete a registered contract """ try: - registered_contract = ( - db_session.query(RegisteredContract) - .filter(RegisteredContract.moonstream_user_id == moonstream_user_id) + contract_with_blockchain = ( + db_session.query(RegisteredContract, Blockchain) + .join(Blockchain, Blockchain.id == RegisteredContract.blockchain_id) + .filter(RegisteredContract.metatx_holder_id == metatx_holder_id) .filter(RegisteredContract.id == registered_contract_id) .one() ) + contract = contract_with_blockchain[0] - db_session.delete(registered_contract) + db_session.delete(contract) db_session.commit() except Exception as err: db_session.rollback() logger.error(repr(err)) raise - return registered_contract + registered_contract, blockchain = contract_with_blockchain + + return (registered_contract, blockchain) def request_calls( diff --git a/engineapi/engineapi/data.py b/engineapi/engineapi/data.py index f519e218..0a646ac1 100644 --- a/engineapi/engineapi/data.py +++ b/engineapi/engineapi/data.py @@ -238,7 +238,6 @@ class BlockchainsResponse(BaseModel): class RegisterContractRequest(BaseModel): blockchain: str address: str - contract_type: ContractType title: Optional[str] = None description: Optional[str] = None image_uri: Optional[str] = None @@ -251,29 +250,6 @@ class UpdateContractRequest(BaseModel): ignore_nulls: bool = True -class RegisteredContract(BaseModel): - id: UUID - blockchain: str - address: str - metatx_holder_id: UUID - title: Optional[str] = None - description: Optional[str] = None - image_uri: Optional[str] = None - created_at: datetime - updated_at: datetime - - @validator("id", "metatx_holder_id") - def validate_uuids(cls, v): - return str(v) - - @validator("created_at", "updated_at") - def validate_datetimes(cls, v): - return v.isoformat() - - class Config: - orm_mode = True - - class RegisteredContractResponse(BaseModel): id: UUID blockchain: Optional[str] = None diff --git a/engineapi/engineapi/routes/metatx.py b/engineapi/engineapi/routes/metatx.py index 8701a539..d8f7577d 100644 --- a/engineapi/engineapi/routes/metatx.py +++ b/engineapi/engineapi/routes/metatx.py @@ -88,7 +88,7 @@ async def blockchains_route( tags=["contracts"], response_model=List[data.RegisteredContractResponse], ) -async def list_registered_contracts( +async def list_registered_contracts_route( request: Request, blockchain: Optional[str] = Query(None), address: Optional[str] = Query(None), @@ -133,20 +133,20 @@ async def list_registered_contracts( @app.get( "/contracts/{contract_id}", tags=["contracts"], - response_model=data.RegisteredContract, + response_model=data.RegisteredContractResponse, ) -async def get_registered_contract( +async def get_registered_contract_route( request: Request, contract_id: UUID = Path(...), db_session: Session = Depends(db.yield_db_read_only_session), -) -> List[data.RegisteredContract]: +) -> List[data.RegisteredContractResponse]: """ Get the contract by ID. """ try: - contract = contracts_actions.get_registered_contract( + contract_with_blockchain = contracts_actions.get_registered_contract( db_session=db_session, - moonstream_user_id=request.state.user.id, + metatx_holder_id=request.state.user.id, contract_id=contract_id, ) except NoResultFound: @@ -157,57 +157,87 @@ async def get_registered_contract( except Exception as err: logger.error(repr(err)) raise EngineHTTPException(status_code=500) - return contract + + return data.RegisteredContractResponse( + id=contract_with_blockchain[0].id, + blockchain=contract_with_blockchain[1].name, + address=contract_with_blockchain[0].address, + metatx_holder_id=contract_with_blockchain[0].metatx_holder_id, + title=contract_with_blockchain[0].title, + description=contract_with_blockchain[0].description, + image_uri=contract_with_blockchain[0].image_uri, + created_at=contract_with_blockchain[0].created_at, + updated_at=contract_with_blockchain[0].updated_at, + ) -@app.post("/contracts", tags=["contracts"], response_model=data.RegisteredContract) -async def register_contract( +@app.post( + "/contracts", tags=["contracts"], response_model=data.RegisteredContractResponse +) +async def register_contract_route( request: Request, contract: data.RegisterContractRequest = Body(...), db_session: Session = Depends(db.yield_db_session), -) -> data.RegisteredContract: +) -> data.RegisteredContractResponse: """ Allows users to register contracts. """ try: - registered_contract = contracts_actions.register_contract( + contract_with_blockchain = contracts_actions.register_contract( db_session=db_session, - moonstream_user_id=request.state.user.id, - blockchain=contract.blockchain, + metatx_holder_id=request.state.user.id, + blockchain_name=contract.blockchain, address=contract.address, - contract_type=contract.contract_type, title=contract.title, description=contract.description, image_uri=contract.image_uri, ) + except contracts_actions.UnsupportedBlockchain: + raise EngineHTTPException( + status_code=400, detail="Unsupported blockchain specified" + ) except contracts_actions.ContractAlreadyRegistered: raise EngineHTTPException( status_code=409, detail="Contract already registered", ) - return registered_contract + except Exception as err: + logger.error(repr(err)) + raise EngineHTTPException(status_code=500) + + return data.RegisteredContractResponse( + id=contract_with_blockchain[0].id, + blockchain=contract_with_blockchain[1].name, + address=contract_with_blockchain[0].address, + metatx_holder_id=contract_with_blockchain[0].metatx_holder_id, + title=contract_with_blockchain[0].title, + description=contract_with_blockchain[0].description, + image_uri=contract_with_blockchain[0].image_uri, + created_at=contract_with_blockchain[0].created_at, + updated_at=contract_with_blockchain[0].updated_at, + ) @app.put( "/contracts/{contract_id}", tags=["contracts"], - response_model=data.RegisteredContract, + response_model=data.RegisteredContractResponse, ) -async def update_contract( +async def update_contract_route( request: Request, contract_id: UUID = Path(...), update_info: data.UpdateContractRequest = Body(...), db_session: Session = Depends(db.yield_db_session), -) -> data.RegisteredContract: +) -> data.RegisteredContractResponse: try: - contract = contracts_actions.update_registered_contract( - db_session, - request.state.user.id, - contract_id, - update_info.title, - update_info.description, - update_info.image_uri, - update_info.ignore_nulls, + contract_with_blockchain = contracts_actions.update_registered_contract( + db_session=db_session, + metatx_holder_id=request.state.user.id, + contract_id=contract_id, + title=update_info.title, + description=update_info.description, + image_uri=update_info.image_uri, + ignore_nulls=update_info.ignore_nulls, ) except NoResultFound: raise EngineHTTPException( @@ -218,33 +248,53 @@ async def update_contract( logger.error(repr(err)) raise EngineHTTPException(status_code=500) - return contract + return data.RegisteredContractResponse( + id=contract_with_blockchain[0].id, + blockchain=contract_with_blockchain[1].name, + address=contract_with_blockchain[0].address, + metatx_holder_id=contract_with_blockchain[0].metatx_holder_id, + title=contract_with_blockchain[0].title, + description=contract_with_blockchain[0].description, + image_uri=contract_with_blockchain[0].image_uri, + created_at=contract_with_blockchain[0].created_at, + updated_at=contract_with_blockchain[0].updated_at, + ) @app.delete( "/contracts/{contract_id}", tags=["contracts"], - response_model=data.RegisteredContract, + response_model=data.RegisteredContractResponse, ) -async def delete_contract( +async def delete_contract_route( request: Request, - contract_id: UUID, + contract_id: UUID = Path(...), db_session: Session = Depends(db.yield_db_session), -) -> data.RegisteredContract: +) -> data.RegisteredContractResponse: """ Allows users to delete contracts that they have registered. """ try: - deleted_contract = contracts_actions.delete_registered_contract( + deleted_contract_with_blockchain = contracts_actions.delete_registered_contract( db_session=db_session, - moonstream_user_id=request.state.user.id, + metatx_holder_id=request.state.user.id, registered_contract_id=contract_id, ) except Exception as err: logger.error(repr(err)) raise EngineHTTPException(status_code=500) - return deleted_contract + return data.RegisteredContractResponse( + id=deleted_contract_with_blockchain[0].id, + blockchain=deleted_contract_with_blockchain[1].name, + address=deleted_contract_with_blockchain[0].address, + metatx_holder_id=deleted_contract_with_blockchain[0].metatx_holder_id, + title=deleted_contract_with_blockchain[0].title, + description=deleted_contract_with_blockchain[0].description, + image_uri=deleted_contract_with_blockchain[0].image_uri, + created_at=deleted_contract_with_blockchain[0].created_at, + updated_at=deleted_contract_with_blockchain[0].updated_at, + ) # TODO(kompotkot): route `/contracts/types` deprecated