Skip to content

Commit

Permalink
Migrate /rewards routes (#604)
Browse files Browse the repository at this point in the history
## Description

   - `GET /rewards/budget/{address}/epoch/{epoch}`
   - `GET /rewards/budget/{address}/upcoming`
   - `GET /rewards/budgets/epoch/{epoch}`
   - `POST /rewards/estimated_budget/by_days`
   - `POST /rewards/estimated_budget`
   - `GET /rewards/leverage/{epoch}`
   - `GET /rewards/merkle_tree/{epoch}`
   - `GET /rewards/projects/epoch/{epoch}`
   - `GET /rewards/projects/estimated`
   - `GET /rewards/threshold/{epoch}`
   - `GET /rewards/unused/{epoch}`
  • Loading branch information
adam-gf authored Jan 30, 2025
1 parent eb29027 commit a960b18
Show file tree
Hide file tree
Showing 45 changed files with 2,597 additions and 115 deletions.
5 changes: 4 additions & 1 deletion backend/app/modules/common/time.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@ def timestamp_s(self) -> float:
return self.timestamp_us() / 10**6

def datetime(self) -> DateTime:
return DateTime.fromtimestamp(self.timestamp_s())
# Make sure the timestamp is in UTC and not local
utc_timestamp = DateTime.fromtimestamp(self.timestamp_s(), timezone.utc)
# Remove timezone info
return utc_timestamp.replace(tzinfo=None)

def to_isoformat(self):
return self.datetime().isoformat()
Expand Down
12 changes: 12 additions & 0 deletions backend/tests/v2/factories/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
from tests.v2.factories.allocations import AllocationFactorySet
from tests.v2.factories.projects_details import ProjectsDetailsFactorySet
from tests.v2.factories.users import UserFactorySet
from tests.v2.factories.budgets import BudgetFactorySet
from tests.v2.factories.pending_snapshots import PendingEpochSnapshotFactorySet
from tests.v2.factories.finalized_snapshots import FinalizedEpochSnapshotFactorySet
from tests.v2.factories.patrons import PatronModeEventFactorySet
from dataclasses import dataclass


Expand All @@ -23,6 +27,10 @@ class FactoriesAggregator:
allocation_requests: AllocationRequestFactorySet
allocations: AllocationFactorySet
projects_details: ProjectsDetailsFactorySet
budgets: BudgetFactorySet
pending_snapshots: PendingEpochSnapshotFactorySet
finalized_snapshots: FinalizedEpochSnapshotFactorySet
patrons: PatronModeEventFactorySet

def __init__(self, fast_session: AsyncSession):
"""
Expand All @@ -32,3 +40,7 @@ def __init__(self, fast_session: AsyncSession):
self.allocation_requests = AllocationRequestFactorySet(fast_session)
self.allocations = AllocationFactorySet(fast_session)
self.projects_details = ProjectsDetailsFactorySet(fast_session)
self.budgets = BudgetFactorySet(fast_session)
self.pending_snapshots = PendingEpochSnapshotFactorySet(fast_session)
self.finalized_snapshots = FinalizedEpochSnapshotFactorySet(fast_session)
self.patrons = PatronModeEventFactorySet(fast_session)
55 changes: 55 additions & 0 deletions backend/tests/v2/factories/budgets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import random
from async_factory_boy.factory.sqlalchemy import AsyncSQLAlchemyFactory
from factory import LazyAttribute

from app.infrastructure.database.models import Budget, User
from tests.v2.factories.base import FactorySetBase
from tests.v2.factories.users import UserFactorySet
from v2.core.types import Address, BigInteger


class BudgetFactory(AsyncSQLAlchemyFactory):
class Meta:
model = Budget
sqlalchemy_session_persistence = "commit"

user_id = None
epoch = None
budget = LazyAttribute(
lambda _: str(random.randint(1, 1000) * 10**18)
) # Random amount in wei


class BudgetFactorySet(FactorySetBase):
_factories = {"budget": BudgetFactory}

async def create(
self,
user: User | Address,
epoch: int,
amount: BigInteger | None = None,
) -> Budget:
"""
Create a budget for a user in a specific epoch.
Args:
user: The user or user address to create the budget for
epoch: The epoch number
amount: Optional specific amount, otherwise random
Returns:
The created budget
"""
if not isinstance(user, User):
user = await UserFactorySet(self.session).get_or_create(user)

factory_kwargs = {
"user_id": user.id,
"epoch": epoch,
}

if amount is not None:
factory_kwargs["budget"] = str(amount)

budget = await BudgetFactory.create(**factory_kwargs)
return budget
67 changes: 67 additions & 0 deletions backend/tests/v2/factories/finalized_snapshots.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import random
from async_factory_boy.factory.sqlalchemy import AsyncSQLAlchemyFactory
from factory import LazyAttribute

from app.infrastructure.database.models import FinalizedEpochSnapshot
from tests.v2.factories.base import FactorySetBase
from v2.core.types import BigInteger


class FinalizedEpochSnapshotFactory(AsyncSQLAlchemyFactory):
class Meta:
model = FinalizedEpochSnapshot
sqlalchemy_session_persistence = "commit"

epoch = None
matched_rewards = LazyAttribute(lambda _: str(random.randint(1, 1000) * 10**18))
patrons_rewards = LazyAttribute(lambda _: str(random.randint(1, 1000) * 10**18))
leftover = LazyAttribute(lambda _: str(random.randint(1, 100) * 10**18))
withdrawals_merkle_root = LazyAttribute(
lambda _: "0x" + "".join(random.choices("0123456789abcdef", k=64))
)
total_withdrawals = LazyAttribute(lambda _: str(random.randint(1, 1000) * 10**18))


class FinalizedEpochSnapshotFactorySet(FactorySetBase):
_factories = {"finalized_snapshot": FinalizedEpochSnapshotFactory}

async def create(
self,
epoch: int,
matched_rewards: BigInteger | None = None,
patrons_rewards: BigInteger | None = None,
leftover: BigInteger | None = None,
withdrawals_merkle_root: str | None = None,
total_withdrawals: BigInteger | None = None,
) -> FinalizedEpochSnapshot:
"""
Create a finalized epoch snapshot.
Args:
epoch: The epoch number
matched_rewards: Optional matched rewards amount
patrons_rewards: Optional patrons rewards amount
leftover: Optional leftover amount
withdrawals_merkle_root: Optional withdrawals merkle root
total_withdrawals: Optional total withdrawals amount
Returns:
The created finalized epoch snapshot
"""
factory_kwargs = {
"epoch": epoch,
}

if matched_rewards is not None:
factory_kwargs["matched_rewards"] = str(matched_rewards)
if patrons_rewards is not None:
factory_kwargs["patrons_rewards"] = str(patrons_rewards)
if leftover is not None:
factory_kwargs["leftover"] = str(leftover)
if withdrawals_merkle_root is not None:
factory_kwargs["withdrawals_merkle_root"] = withdrawals_merkle_root
if total_withdrawals is not None:
factory_kwargs["total_withdrawals"] = str(total_withdrawals)

snapshot = await FinalizedEpochSnapshotFactory.create(**factory_kwargs)
return snapshot
52 changes: 52 additions & 0 deletions backend/tests/v2/factories/patrons.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from datetime import datetime, timezone
from async_factory_boy.factory.sqlalchemy import AsyncSQLAlchemyFactory

from app.infrastructure.database.models import PatronModeEvent, User
from tests.v2.factories.base import FactorySetBase
from tests.v2.factories.users import UserFactorySet
from v2.core.types import Address


class PatronModeEventFactory(AsyncSQLAlchemyFactory):
class Meta:
model = PatronModeEvent
sqlalchemy_session_persistence = "commit"

user_address = None
patron_mode_enabled = True
created_at = datetime.now(timezone.utc)


class PatronModeEventFactorySet(FactorySetBase):
_factories = {"patron_event": PatronModeEventFactory}

async def create(
self,
user: User | Address,
patron_mode_enabled: bool = True,
created_at: datetime | None = None,
) -> PatronModeEvent:
"""
Create a patron mode event.
Args:
user: The user or user address to create the event for
patron_mode_enabled: Whether patron mode is enabled (default: True)
created_at: Optional timestamp for when the event was created (default: now)
Returns:
The created patron mode event
"""
if not isinstance(user, User):
user = await UserFactorySet(self.session).get_or_create(user)

factory_kwargs = {
"user_address": user.address,
"patron_mode_enabled": patron_mode_enabled,
}

if created_at is not None:
factory_kwargs["created_at"] = created_at

event = await PatronModeEventFactory.create(**factory_kwargs)
return event
86 changes: 86 additions & 0 deletions backend/tests/v2/factories/pending_snapshots.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import random
from async_factory_boy.factory.sqlalchemy import AsyncSQLAlchemyFactory
from factory import LazyAttribute

from app.infrastructure.database.models import PendingEpochSnapshot
from tests.v2.factories.base import FactorySetBase
from v2.core.types import BigInteger


class PendingEpochSnapshotFactory(AsyncSQLAlchemyFactory):
class Meta:
model = PendingEpochSnapshot
sqlalchemy_session_persistence = "commit"

epoch = None
eth_proceeds = LazyAttribute(lambda _: str(random.randint(1, 1000) * 10**18))
total_effective_deposit = LazyAttribute(
lambda _: str(random.randint(1, 1000) * 10**18)
)
locked_ratio = LazyAttribute(lambda _: str(random.randint(1, 100)))
total_rewards = LazyAttribute(lambda _: str(random.randint(1, 1000) * 10**18))
vanilla_individual_rewards = LazyAttribute(
lambda _: str(random.randint(1, 1000) * 10**18)
)
operational_cost = LazyAttribute(lambda _: str(random.randint(1, 100) * 10**18))
ppf = LazyAttribute(lambda _: str(random.randint(1, 100) * 10**18))
community_fund = LazyAttribute(lambda _: str(random.randint(1, 100) * 10**18))


class PendingEpochSnapshotFactorySet(FactorySetBase):
_factories = {"pending_snapshot": PendingEpochSnapshotFactory}

async def create(
self,
epoch: int,
eth_proceeds: BigInteger | None = None,
total_effective_deposit: BigInteger | None = None,
locked_ratio: BigInteger | None = None,
total_rewards: BigInteger | None = None,
vanilla_individual_rewards: BigInteger | None = None,
operational_cost: BigInteger | None = None,
ppf: BigInteger | None = None,
community_fund: BigInteger | None = None,
) -> PendingEpochSnapshot:
"""
Create a pending epoch snapshot.
Args:
epoch: The epoch number
eth_proceeds: Optional ETH proceeds amount
total_effective_deposit: Optional total effective deposit amount
locked_ratio: Optional locked ratio
total_rewards: Optional total rewards amount
vanilla_individual_rewards: Optional vanilla individual rewards amount
operational_cost: Optional operational cost amount
ppf: Optional PPF amount
community_fund: Optional community fund amount
Returns:
The created pending epoch snapshot
"""
factory_kwargs = {
"epoch": epoch,
}

if eth_proceeds is not None:
factory_kwargs["eth_proceeds"] = str(eth_proceeds)
if total_effective_deposit is not None:
factory_kwargs["total_effective_deposit"] = str(total_effective_deposit)
if locked_ratio is not None:
factory_kwargs["locked_ratio"] = str(locked_ratio)
if total_rewards is not None:
factory_kwargs["total_rewards"] = str(total_rewards)
if vanilla_individual_rewards is not None:
factory_kwargs["vanilla_individual_rewards"] = str(
vanilla_individual_rewards
)
if operational_cost is not None:
factory_kwargs["operational_cost"] = str(operational_cost)
if ppf is not None:
factory_kwargs["ppf"] = str(ppf)
if community_fund is not None:
factory_kwargs["community_fund"] = str(community_fund)

snapshot = await PendingEpochSnapshotFactory.create(**factory_kwargs)
return snapshot
2 changes: 1 addition & 1 deletion backend/tests/v2/factories/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class Meta:
class UserFactorySet(FactorySetBase):
_factories = {"user": UserFactory}

async def create(self, address: Address) -> User:
async def create(self, address: Address | None = None) -> User:
factory_kwargs = {}

if address is not None:
Expand Down
15 changes: 15 additions & 0 deletions backend/tests/v2/fake_subgraphs/epochs.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from pydantic import TypeAdapter
from app import exceptions
from app.context.epoch.details import EpochDetails
from v2.epochs.subgraphs import EpochSubgraphItem
from tests.v2.fake_subgraphs.helpers import FakeEpochEventDetails
from v2.core.exceptions import EpochsNotFound

Expand All @@ -13,6 +15,19 @@ def __init__(self, epochs_events: list[FakeEpochEventDetails] | None = None):
lambda epoch_event: epoch_event.to_dict(), epochs_events
)

async def fetch_epoch_by_number(self, epoch_number: int) -> EpochSubgraphItem:
"""
Simulate fetching epoch details by epoch number.
"""
matching_epochs = [
epoch for epoch in self.epochs_events if epoch["epoch"] == epoch_number
]

if not matching_epochs:
raise exceptions.EpochNotIndexed(epoch_number)

return TypeAdapter(EpochSubgraphItem).validate_python(matching_epochs[0])

async def get_epoch_by_number(self, epoch_number: int) -> EpochDetails:
"""
Simulate fetching epoch details by epoch number.
Expand Down
Empty file.
2 changes: 2 additions & 0 deletions backend/tests/v2/project_rewards/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from tests.v2.fake_contracts.conftest import fake_epochs_contract_factory # noqa: F401
from tests.v2.fake_subgraphs.conftest import fake_epochs_subgraph_factory # noqa: F401
Loading

0 comments on commit a960b18

Please sign in to comment.