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

Revert airdrop #347

Merged
merged 5 commits into from
Dec 12, 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
19 changes: 19 additions & 0 deletions spotnet_tracker/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@
- test_task: A simple test task that logs a confirmation message.
"""

import asyncio
import logging
import time

from web_app.contract_tools.mixins.alert import AlertMixin
from web_app.tasks.claim_airdrops import AirdropClaimer

from .celery_config import app

Expand All @@ -30,3 +33,19 @@ def check_users_health_ratio() -> None:
except Exception as e:
logger.error(f"Error in check_users_health_ratio task: {e}")


@app.task(name="claim_airdrop_task")
def claim_airdrop_task() -> None:
"""
Background task to claim user airdrops.

:return: None
"""
try:
logger.info("Running claim_airdrop_task.")
logger.info("Task started at: ",time.strftime("%a, %d %b %Y %H:%M:%S"))
airdrop_claimer = AirdropClaimer()
asyncio.run(airdrop_claimer.claim_airdrops())
logger.info("Task started at: ", time.strftime("%a, %d %b %Y %H:%M:%S"))
except Exception as e:
logger.error(f"Error in claiming airdrop task: {e}")
41 changes: 0 additions & 41 deletions web_app/alembic/versions/b6eaae01419c_remove_airdrop.py

This file was deleted.

21 changes: 21 additions & 0 deletions web_app/api/serializers/airdrop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""
Serializers for airdrop data.
"""

from typing import List
from pydantic import BaseModel


class AirdropItem(BaseModel):
"""Model for individual airdrop items."""

amount: str
proof: List[str] # This needs to be List[str], not str
is_claimed: bool
recipient: str


class AirdropResponseModel(BaseModel):
"""Model for the complete airdrop response."""

airdrops: List[AirdropItem]
73 changes: 73 additions & 0 deletions web_app/contract_tools/airdrop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""
This module defines the contract tools for the airdrop data.
"""

from typing import List
from web_app.api.serializers.airdrop import AirdropItem, AirdropResponseModel
from web_app.contract_tools.api_request import APIRequest
from web_app.contract_tools.constants import TokenParams


class ZkLendAirdrop:
"""
A class to fetch and validate airdrop data
for a specified contract.
"""

REWARD_API_ENDPOINT = "https://app.zklend.com/api/reward/all/"

def __init__(self):
"""
Initializes the ZkLendAirdrop class with an APIRequest instance.
"""
self.api = APIRequest(base_url=self.REWARD_API_ENDPOINT)

async def get_contract_airdrop(self, contract_id: str) -> AirdropResponseModel:
"""
Fetches all available airdrops
for a specific contract asynchronously.
Args:
contract_id (str): The ID of the contract
for which to fetch airdrop data.
Returns:
AirdropResponseModel: A validated list of airdrop items
for the specified contract.
Raises:
ValueError: If contract_id is None
"""
if contract_id is None:
raise ValueError("Contract ID cannot be None")

underlying_contract_id = TokenParams.add_underlying_address(contract_id)
response = await self.api.fetch(underlying_contract_id)
return self._validate_response(response)

@staticmethod
def _validate_response(data: List[dict]) -> AirdropResponseModel:
"""
Validates and formats the response data, keeping only necessary fields.
Args:
data (List[dict]): Raw response data from the API.
Returns:
AirdropResponseModel: Structured and validated airdrop data.
"""
validated_items = []
for item in data:
validated_item = AirdropItem(
amount=item["amount"],
proof=item[
"proof"
], # This is correct now as AirdropItem expects List[str]
is_claimed=item["is_claimed"],
recipient=item["recipient"],
)
validated_items.append(validated_item)
return AirdropResponseModel(airdrops=validated_items)


if __name__ == "__main__":
airdrop_fetcher = ZkLendAirdrop()
result = airdrop_fetcher.get_contract_airdrop(
"0x698b63df00be56ba39447c9b9ca576ffd0edba0526d98b3e8e4a902ffcf12f0"
)
print(result)
1 change: 1 addition & 0 deletions web_app/db/crud/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
This module contains the CRUD operations for the database.
"""

from .airdrop import *
from .base import *
from .deposit import *
from .position import *
Expand Down
70 changes: 70 additions & 0 deletions web_app/db/crud/airdrop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""
This module contains the database configuration for airdrops.
"""

import logging
import uuid
from datetime import datetime
from decimal import Decimal
from typing import List, TypeVar

from sqlalchemy.exc import SQLAlchemyError

from web_app.db.models import AirDrop, Base
from .base import DBConnector

logger = logging.getLogger(__name__)
ModelType = TypeVar("ModelType", bound=Base)


class AirDropDBConnector(DBConnector):
"""
Provides database connection and operations management for the AirDrop model.
"""

def save_claim_data(self, airdrop_id: uuid.UUID, amount: Decimal) -> None:
"""
Updates the AirDrop instance with claim data.
:param airdrop_id: uuid.UUID
:param amount: Decimal
"""
airdrop = self.get_object(AirDrop, airdrop_id)
if airdrop:
airdrop.amount = amount
airdrop.is_claimed = True
airdrop.claimed_at = datetime.now()
self.write_to_db(airdrop)
else:
logger.error(f"AirDrop with ID {airdrop_id} not found")

def get_all_unclaimed(self) -> List[AirDrop]:
"""
Returns all unclaimed AirDrop instances (where is_claimed is False).

:return: List of unclaimed AirDrop instances
"""
with self.Session() as db:
try:
unclaimed_instances = (
db.query(AirDrop).filter_by(is_claimed=False).all()
)
return unclaimed_instances
except SQLAlchemyError as e:
logger.error(
f"Failed to retrieve unclaimed AirDrop instances: {str(e)}"
)
return []

def delete_all_users_airdrop(self, user_id: uuid.UUID) -> None:
"""
Delete all airdrops for a user.
:param user_id: User ID
"""
with self.Session() as db:
try:
airdrops = db.query(AirDrop).filter_by(user_id=user_id).all()
for airdrop in airdrops:
db.delete(airdrop)
db.commit()
except SQLAlchemyError as e:
logger.error(f"Error deleting airdrops for user {user_id}: {str(e)}")
12 changes: 11 additions & 1 deletion web_app/db/crud/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from sqlalchemy.orm import scoped_session, sessionmaker

from web_app.db.database import SQLALCHEMY_DATABASE_URL
from web_app.db.models import Base
from web_app.db.models import AirDrop, Base

logger = logging.getLogger(__name__)
ModelType = TypeVar("ModelType", bound=Base)
Expand Down Expand Up @@ -130,3 +130,13 @@ def delete_object(self, object: Base) -> None:

finally:
db.close()

def create_empty_claim(self, user_id: uuid.UUID) -> AirDrop:
"""
Creates a new empty AirDrop instance for the given user_id.
:param user_id: uuid.UUID
:return: AirDrop
"""
airdrop = AirDrop(user_id=user_id)
self.write_to_db(airdrop)
return airdrop
5 changes: 3 additions & 2 deletions web_app/db/crud/position.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from sqlalchemy.exc import SQLAlchemyError

from .user import UserDBConnector
from web_app.db.models import Base, Position, Status, User
from web_app.db.models import AirDrop, Base, Position, Status, User

logger = logging.getLogger(__name__)
ModelType = TypeVar("ModelType", bound=Base)
Expand Down Expand Up @@ -207,7 +207,7 @@ def close_position(self, position_id: uuid) -> Position | None:

def open_position(self, position_id: uuid.UUID, current_prices: dict) -> str | None:
"""
Opens a position by updating its status.
Opens a position by updating its status and creating an AirDrop claim.
:param position_id: uuid.UUID
:param current_prices: dict
:return: str | None
Expand All @@ -216,6 +216,7 @@ def open_position(self, position_id: uuid.UUID, current_prices: dict) -> str | N
if position:
position.status = Status.OPENED.value
self.write_to_db(position)
self.create_empty_claim(position.user_id)
self.save_current_price(position, current_prices)
return position.status
else:
Expand Down
19 changes: 18 additions & 1 deletion web_app/db/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""
This module contains SQLAlchemy models for the application, including
User, Position, and TelegramUser. Each model represents a
User, Position, AirDrop, and TelegramUser. Each model represents a
table in the database and defines the structure and relationships
between the data entities.
"""
Expand Down Expand Up @@ -84,6 +84,23 @@ class Position(Base):
datetime_liquidation = Column(DateTime, nullable=True)


class AirDrop(Base):
"""
SQLAlchemy model for the airdrop table.
"""

__tablename__ = "airdrop"

id = Column(UUID(as_uuid=True), primary_key=True, default=uuid4)
user_id = Column(
UUID(as_uuid=True), ForeignKey("user.id"), index=True, nullable=False
)
created_at = Column(DateTime, nullable=False, default=func.now())
amount = Column(DECIMAL, nullable=True)
is_claimed = Column(Boolean, default=False, index=True)
claimed_at = Column(DateTime, nullable=True)


class TelegramUser(Base):
"""
SQLAlchemy model for the telegram_user table.
Expand Down
27 changes: 26 additions & 1 deletion web_app/db/seed_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import logging
from decimal import Decimal
from faker import Faker
from web_app.db.models import Status, User, Position, TelegramUser, Vault
from web_app.db.models import Status, User, Position, AirDrop, TelegramUser, Vault
from web_app.db.database import SessionLocal
from web_app.contract_tools.constants import TokenParams

Expand Down Expand Up @@ -74,6 +74,30 @@ def create_positions(session: SessionLocal, users: list[User]) -> None:
logger.info("No positions created.")


def create_airdrops(session: SessionLocal, users: list[User]) -> None:
"""
Create and save fake airdrop records for each user.
Args:
session (Session): SQLAlchemy session object.
users (list): List of User objects to associate with airdrops.
"""
airdrops = []
for user in users:
for _ in range(2):
airdrop = AirDrop(
user_id=user.id,
amount=Decimal(
fake.pydecimal(left_digits=5, right_digits=2, positive=True)
),
is_claimed=fake.boolean(),
claimed_at=fake.date_time_this_decade() if fake.boolean() else None,
)
airdrops.append(airdrop)
if airdrops:
session.bulk_save_objects(airdrops)
session.commit()


def create_telegram_users(session: SessionLocal, users: list[User]) -> None:
"""
Create and save fake Telegram user records to the database.
Expand Down Expand Up @@ -133,6 +157,7 @@ def create_vaults(session: SessionLocal, users: list[User]) -> None:
# Populate the database
users = create_users(session)
create_positions(session, users)
create_airdrops(session, users)
create_telegram_users(session, users)
create_vaults(session, users)

Expand Down
Loading
Loading