Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PR for statistic endpoint #98

Merged
merged 7 commits into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion web_app/api/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
)
async def get_dashboard(wallet_id: str) -> DashboardResponse:
"""
This endpoint fetches the user's dashboard data,
This endpoint fetches the user's dashboard data,
including balances, multipliers, start dates, and ZkLend position.

### Parameters:
Expand Down
6 changes: 4 additions & 2 deletions web_app/api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
license_info={
"name": "MIT License",
"url": "https://opensource.org/licenses/MIT",
}
},
)

# Set up the templates directory
Expand All @@ -36,7 +36,9 @@
# Add session middleware with a secret key
app.add_middleware(SessionMiddleware, secret_key=f"Secret:{str(uuid4())}")
# CORS middleware for React frontend
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_headers=["*"], allow_methods=["*"])
app.add_middleware(
CORSMiddleware, allow_origins=["*"], allow_headers=["*"], allow_methods=["*"]
)

# Include the form and login routers
app.include_router(position_router)
Expand Down
11 changes: 5 additions & 6 deletions web_app/api/position.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,21 @@
response_description="Returns the new position and transaction data.",
)
async def create_position_with_transaction_data(
form_data: PositionFormData
form_data: PositionFormData,
) -> LoopLiquidityData:
"""
This endpoint creates a new user position.

### Parameters:
- **wallet_id**: The wallet ID of the user.
- **token_symbol**: The symbol of the token used for the position.
- **amount**: The amount of the token being deposited.
- **multiplier**: The multiplier applied to the user's position.

### Returns:
The created position's details and transaction data.
"""


# Create a new position in the database
position = position_db_connector.create_position(
form_data.wallet_id,
Expand Down Expand Up @@ -80,7 +79,7 @@ async def get_repay_data(
:return: Dict containing the repay transaction data
:raises: HTTPException :return: Dict containing status code and detail
"""

if not wallet_id:
raise HTTPException(status_code=404, detail="Wallet not found")

Expand Down Expand Up @@ -129,7 +128,7 @@ async def open_position(position_id: str) -> str:
:return: str
:raises: HTTPException :return: Dict containing status code and detail
"""

if not position_id:
raise HTTPException(status_code=404, detail="Position not found")

Expand Down
9 changes: 9 additions & 0 deletions web_app/api/serializers/dashboard.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
This module defines the serializers for the dashboard data.
"""

from datetime import datetime
from decimal import Decimal
from typing import Dict, List, Optional, Any
Expand All @@ -13,6 +14,7 @@ class Data(BaseModel):
"""
Data class for position details.
"""

collateral: bool
debt: bool

Expand All @@ -21,6 +23,7 @@ class TotalBalances(RootModel):
"""
TotalBalances class for total balances.
"""

# Since the keys are dynamic (addresses), we use a generic Dict
root: Dict[str, str]

Expand All @@ -29,6 +32,7 @@ class Position(BaseModel):
"""
Position class for position details.
"""

data: Data
token_address: Optional[str] = Field(None, alias="tokenAddress")
total_balances: TotalBalances = Field(alias="totalBalances")
Expand All @@ -55,13 +59,15 @@ class Config:
"""
Configuration for the Position class.
"""

populate_by_name = True


class Product(BaseModel):
"""
Product class for product details.
"""

name: str
health_ratio: str
positions: List[Position]
Expand All @@ -71,6 +77,7 @@ class ZkLendPositionResponse(BaseModel):
"""
ZkLendPositionResponse class for ZkLend position details.
"""

products: List[Product] = Field(default_factory=list)

@field_validator("products", mode="before")
Expand All @@ -90,13 +97,15 @@ class Config:
"""
Configuration for the ZkLendPositionResponse class.
"""

populate_by_name = True


class DashboardResponse(BaseModel):
"""
DashboardResponse class for dashboard details.
"""

balances: Dict[str, Any] = Field(
...,
example={"ETH": 5.0, "USDC": 1000.0},
Expand Down
7 changes: 6 additions & 1 deletion web_app/api/serializers/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@ class PoolKey(BaseModel):
extension: str

@field_validator(
"token0", "token1", "fee", "tick_spacing", "extension", mode="before",
"token0",
"token1",
"fee",
"tick_spacing",
"extension",
mode="before",
)
def convert_int_to_str(cls, value) -> str:
"""
Expand Down
19 changes: 19 additions & 0 deletions web_app/api/serializers/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class CheckUserResponse(BaseModel):
"""
Pydantic model for the check user response.
"""

is_contract_deployed: bool = Field(
..., example=False, description="Indicates if the user's contract is deployed."
)
Expand All @@ -18,6 +19,7 @@ class UpdateUserContractResponse(BaseModel):
"""
Pydantic model for the update user contract response.
"""

is_contract_deployed: bool = Field(
..., example=False, description="Indicates if the user's contract is deployed."
)
Expand All @@ -27,8 +29,25 @@ class GetUserContractAddressResponse(BaseModel):
"""
Pydantic model for the get user contract address response.
"""

contract_address: str | None = Field(
None,
example="0xabc123...",
description="The contract address of the user, or None if not deployed.",
)


class GetStatsResponse(BaseModel):
"""
Pydantic model for the get_stats response.
"""
total_opened_amount: float = Field(
...,
example=1000.0,
description="Total amount for all open positions across all users.",
)
unique_users: int = Field(
...,
example=5,
description="Number of unique users in the database.",
)
90 changes: 61 additions & 29 deletions web_app/api/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,31 @@
This module handles user-related API endpoints.
"""

from fastapi import APIRouter, HTTPException
from web_app.db.crud import UserDBConnector
from fastapi import APIRouter, Request, HTTPException
from web_app.db.crud import PositionDBConnector, UserDBConnector
from web_app.api.serializers.transaction import UpdateUserContractRequest
from web_app.api.serializers.user import (
CheckUserResponse,
UpdateUserContractResponse,
GetUserContractAddressResponse
CheckUserResponse,
UpdateUserContractResponse,
GetUserContractAddressResponse,
GetStatsResponse,
)

router = APIRouter() # Initialize the router

user_db = UserDBConnector()

position_db = PositionDBConnector()


@router.get(
"/api/get-user-contract",
tags=["User Operations"],
summary="Get user's contract status",
response_description=(
"Returns 0 if the user is None or if the contract is not deployed. "
"Returns the transaction hash if the contract is deployed."
),
"/api/get-user-contract",
tags=["User Operations"],
summary="Get user's contract status",
response_description=(
"Returns 0 if the user is None or if the contract is not deployed. "
"Returns the transaction hash if the contract is deployed."
),
)
async def get_user_contract(wallet_id: str) -> str:
"""
Expand All @@ -42,11 +45,11 @@ async def get_user_contract(wallet_id: str) -> str:


@router.get(
"/api/check-user",
tags=["User Operations"],
summary="Check if user exists and contract status",
response_model=CheckUserResponse,
response_description="Returns whether the user's contract is deployed.",
"/api/check-user",
tags=["User Operations"],
summary="Check if user exists and contract status",
response_model=CheckUserResponse,
response_description="Returns whether the user's contract is deployed.",
)
async def check_user(wallet_id: str) -> CheckUserResponse:
"""
Expand All @@ -69,15 +72,17 @@ async def check_user(wallet_id: str) -> CheckUserResponse:
else:
return {"is_contract_deployed": True}


@router.post(
"/api/update-user-contract",
tags=["User Operations"],
summary="Update the user's contract",
response_model=UpdateUserContractResponse,
response_description="Returns if the contract is updated and deployed.",
"/api/update-user-contract",
tags=["User Operations"],
summary="Update the user's contract",
response_model=UpdateUserContractResponse,
response_description="Returns if the contract is updated and deployed.",
)
async def update_user_contract(data: UpdateUserContractRequest) -> UpdateUserContractResponse:
async def update_user_contract(
data: UpdateUserContractRequest,
) -> UpdateUserContractResponse:
"""
This endpoint updates the user's contract.

Expand All @@ -98,11 +103,11 @@ async def update_user_contract(data: UpdateUserContractRequest) -> UpdateUserCo


@router.get(
"/api/get-user-contract-address",
tags=["User Operations"],
summary="Get user's contract address",
response_model=GetUserContractAddressResponse,
response_description="Returns the contract address of the user or None if not deployed.",
"/api/get-user-contract-address",
tags=["User Operations"],
summary="Get user's contract address",
response_model=GetUserContractAddressResponse,
response_description="Returns the contract address of the user or None if not deployed.",
)
async def get_user_contract_address(wallet_id: str) -> GetUserContractAddressResponse:
"""
Expand All @@ -120,3 +125,30 @@ async def get_user_contract_address(wallet_id: str) -> GetUserContractAddressRes
return {"contract_address": contract_address}
else:
return {"contract_address": None}


@router.get(
"/api/get_stats",
tags=["User Operations"],
summary="Get total opened amounts and number of unique users",
response_model=GetStatsResponse,
response_description="Total amount for all open positions across all users & \
Number of unique users in the database.",
)
async def get_stats() -> GetStatsResponse:
"""
Retrieves the total amount for open positions and the count of unique users.

### Returns:
- total_opened_amount: Sum of amounts for all open positions.
- unique_users: Total count of unique users.
"""
try:
total_opened_amount = position_db.get_total_amounts_for_open_positions()
unique_users = user_db.get_unique_users_count()
return GetStatsResponse(total_opened_amount=total_opened_amount, unique_users=unique_users)

except AttributeError as e:
raise HTTPException(status_code=500, detail=f"AttributeError: {str(e)}")
except TypeError as e:
raise HTTPException(status_code=500, detail=f"TypeError: {str(e)}")
33 changes: 33 additions & 0 deletions web_app/db/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,3 +328,36 @@ def open_position(self, position_id: uuid) -> Position | None:
position.status = Status.OPENED.value
self.write_to_db(position)
return position.status

def get_unique_users_count(self) -> int:
"""
Retrieves the number of unique users in the database.
:return: The count of unique users.
"""
with self.Session() as db:
try:
# Query to count distinct users based on wallet ID
unique_users_count = db.query(User.wallet_id).distinct().count()
return unique_users_count

except SQLAlchemyError as e:
logger.error(f"Failed to retrieve unique users count: {str(e)}")
return 0

def get_total_amounts_for_open_positions(self) -> float | None:
"""
Calculates the total amount for all positions where status is 'OPENED'.

:return: Total amount for all opened positions, or None if no open positions are found
"""
with self.Session() as db:
try:
total_opened_amount = (
db.query(db.func.sum(Position.amount))
.filter(Position.status == Status.OPENED.value)
.scalar()
)
return total_opened_amount
except SQLAlchemyError as e:
logger.error(f"Error calculating total amount for open positions: {e}")
return None
Loading