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

Migrate /rewards routes #604

Merged
merged 16 commits into from
Jan 30, 2025
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