From 171c6fc29f877434fd1878b8d79f082eba3c6a59 Mon Sep 17 00:00:00 2001
From: adam-gf <adam@golem.foundation>
Date: Mon, 23 Dec 2024 10:28:02 +0100
Subject: [PATCH 01/14] wip - working on regression

---
 backend/v2/epoch_snapshots/repositories.py    |  16 +-
 backend/v2/epochs/dependencies.py             |  35 +-
 backend/v2/epochs/subgraphs.py                |  45 +-
 .../v2/project_rewards/capped_quadriatic.py   |   6 +-
 backend/v2/project_rewards/repositories.py    |  37 ++
 backend/v2/project_rewards/router.py          | 478 +++++++++++++++++-
 backend/v2/project_rewards/sablier.py         | 112 ++++
 backend/v2/project_rewards/schemas.py         | 112 ++--
 backend/v2/project_rewards/services.py        |  43 ++
 backend/v2/project_rewards/user_events.py     | 203 ++++++++
 backend/v2/projects/dependencies.py           |  14 +-
 backend/v2/user_patron_mode/repositories.py   |  24 +-
 12 files changed, 1056 insertions(+), 69 deletions(-)
 create mode 100644 backend/v2/project_rewards/repositories.py
 create mode 100644 backend/v2/project_rewards/sablier.py
 create mode 100644 backend/v2/project_rewards/user_events.py

diff --git a/backend/v2/epoch_snapshots/repositories.py b/backend/v2/epoch_snapshots/repositories.py
index b1e3117d90..c392a8cd9b 100644
--- a/backend/v2/epoch_snapshots/repositories.py
+++ b/backend/v2/epoch_snapshots/repositories.py
@@ -1,4 +1,7 @@
-from app.infrastructure.database.models import PendingEpochSnapshot
+from app.infrastructure.database.models import (
+    FinalizedEpochSnapshot,
+    PendingEpochSnapshot,
+)
 from sqlalchemy import select
 from sqlalchemy.ext.asyncio import AsyncSession
 
@@ -10,3 +13,14 @@ async def get_pending_epoch_snapshot(
         select(PendingEpochSnapshot).filter(PendingEpochSnapshot.epoch == epoch_number)
     )
     return result.scalar_one_or_none()
+
+
+async def get_finalized_epoch_snapshot(
+    session: AsyncSession, epoch_number: int
+) -> FinalizedEpochSnapshot | None:
+    result = await session.execute(
+        select(FinalizedEpochSnapshot).filter(
+            FinalizedEpochSnapshot.epoch == epoch_number
+        )
+    )
+    return result.scalar_one_or_none()
diff --git a/backend/v2/epochs/dependencies.py b/backend/v2/epochs/dependencies.py
index 67f391f599..da4495df6d 100644
--- a/backend/v2/epochs/dependencies.py
+++ b/backend/v2/epochs/dependencies.py
@@ -8,7 +8,12 @@
 
 from app.exceptions import InvalidEpoch
 from app.modules.staking.proceeds.core import ESTIMATED_STAKING_REWARDS_RATE
-from v2.core.dependencies import OctantSettings, Web3
+from app.context.epoch_state import EpochState
+from v2.epoch_snapshots.repositories import (
+    get_finalized_epoch_snapshot,
+    get_pending_epoch_snapshot,
+)
+from v2.core.dependencies import GetSession, OctantSettings, Web3
 from v2.core.exceptions import AllocationWindowClosed
 from v2.core.transformers import transform_to_checksum_address
 from v2.core.types import Address
@@ -81,6 +86,34 @@ async def get_rewards_rate(epoch_number: int) -> float:
     return ESTIMATED_STAKING_REWARDS_RATE
 
 
+async def get_epoch_state(
+    session: GetSession, epochs_contracts: GetEpochsContracts, epoch_number: int
+) -> EpochState:
+    current_epoch_number = await get_current_epoch(epoch_number)
+    if epoch_number > current_epoch_number:
+        return EpochState.FUTURE
+
+    if epoch_number == current_epoch_number:
+        return EpochState.CURRENT
+
+    pending_epoch_number = await epochs_contracts.get_pending_epoch()
+    pending_snapshot = await get_pending_epoch_snapshot(session, epoch_number)
+    if epoch_number == pending_epoch_number:
+        if pending_snapshot is None:
+            return EpochState.PRE_PENDING
+        else:
+            return EpochState.PENDING
+
+    if pending_snapshot is None:
+        raise InvalidEpoch()
+
+    finalized_snapshot = await get_finalized_epoch_snapshot(session, epoch_number)
+    if finalized_snapshot is None:
+        return EpochState.FINALIZING
+    else:
+        return EpochState.FINALIZED
+
+
 GetEpochsSubgraph = Annotated[
     EpochsSubgraph,
     Depends(get_epochs_subgraph),
diff --git a/backend/v2/epochs/subgraphs.py b/backend/v2/epochs/subgraphs.py
index 8ddfd8a91c..9c446ff684 100644
--- a/backend/v2/epochs/subgraphs.py
+++ b/backend/v2/epochs/subgraphs.py
@@ -2,8 +2,10 @@
 from dataclasses import dataclass
 from typing import Callable, Sequence, Type, Union
 
+from pydantic import TypeAdapter
 import backoff
 from app import exceptions
+from v2.core.types import OctantModel
 from v2.core.exceptions import EpochsNotFound
 from app.context.epoch.details import EpochDetails
 from gql import Client, gql
@@ -42,6 +44,14 @@ class BackoffParams:
     giveup: Callable[[Exception], bool] = lambda e: False
 
 
+class EpochSubgraphItem(OctantModel):
+    epoch: int
+    fromTs: int
+    toTs: int
+    duration: int
+    decisionWindow: int
+
+
 class EpochsSubgraph:
     def __init__(
         self,
@@ -66,14 +76,8 @@ def __init__(
                 self.gql_client.execute_async
             )
 
-    async def get_epoch_by_number(self, epoch_number: int) -> EpochDetails:
+    async def fetch_epoch_by_number(self, epoch_number: int) -> EpochSubgraphItem:
         """Get EpochDetails from the subgraph for a given epoch number."""
-
-        logging.debug(
-            f"[Subgraph] Getting epoch properties for epoch number: {epoch_number}"
-        )
-
-        # Prepare query and variables
         query = gql(
             """\
             query GetEpoch($epochNo: Int!) {
@@ -85,31 +89,28 @@ async def get_epoch_by_number(self, epoch_number: int) -> EpochDetails:
                 decisionWindow
             }
             }
-        """
+            """
         )
         variables = {"epochNo": epoch_number}
-
-        # Execute query
+        
         response = await self.gql_client.execute_async(query, variable_values=variables)
-
-        # Raise exception if no data received
+        
         data = response["epoches"]
         if not data:
-            logging.warning(
-                f"[Subgraph] No epoch properties received for epoch number: {epoch_number}"
-            )
             raise exceptions.EpochNotIndexed(epoch_number)
 
-        # Parse response and return result
-        logging.debug(f"[Subgraph] Received epoch properties: {data[0]}")
+        return TypeAdapter(EpochSubgraphItem).validate_python(data[0])
 
-        epoch_details = data[0]
+    async def get_epoch_by_number(self, epoch_number: int) -> EpochDetails:
+        """Get EpochDetails from the subgraph for a given epoch number."""
+
+        epoch_details = await self.fetch_epoch_by_number(epoch_number)
 
         return EpochDetails(
-            epoch_num=epoch_details["epoch"],
-            start=epoch_details["fromTs"],
-            duration=epoch_details["duration"],
-            decision_window=epoch_details["decisionWindow"],
+            epoch_num=epoch_details.epoch,
+            start=epoch_details.fromTs,
+            duration=epoch_details.duration,
+            decision_window=epoch_details.decisionWindow,
             remaining_sec=0,
         )
 
diff --git a/backend/v2/project_rewards/capped_quadriatic.py b/backend/v2/project_rewards/capped_quadriatic.py
index 8fa3197c94..22751b3b17 100644
--- a/backend/v2/project_rewards/capped_quadriatic.py
+++ b/backend/v2/project_rewards/capped_quadriatic.py
@@ -5,11 +5,11 @@
 
 from v2.allocations.schemas import AllocationWithUserUQScore
 from v2.core.types import Address
-from v2.project_rewards.schemas import ProjectFundingSummary
+from v2.project_rewards.schemas import ProjectFundingSummaryV1
 
 
 class CappedQuadriaticFunding(NamedTuple):
-    project_fundings: dict[Address, ProjectFundingSummary]
+    project_fundings: dict[Address, ProjectFundingSummaryV1]
     amounts_total: Decimal  # Sum of all allocation amounts for all projects
     matched_total: Decimal  # Sum of all matched rewards for all projects
 
@@ -96,7 +96,7 @@ def capped_quadriatic_funding(
         matched_total += matched_capped
 
     project_fundings = {
-        project_address: ProjectFundingSummary(
+        project_address: ProjectFundingSummaryV1(
             address=project_address,
             allocated=int(amount_by_project[project_address]),
             matched=int(matched_by_project[project_address]),
diff --git a/backend/v2/project_rewards/repositories.py b/backend/v2/project_rewards/repositories.py
new file mode 100644
index 0000000000..359eea93fe
--- /dev/null
+++ b/backend/v2/project_rewards/repositories.py
@@ -0,0 +1,37 @@
+from email.headerregistry import Address
+from sqlalchemy import select
+from sqlalchemy.ext.asyncio import AsyncSession
+from app.infrastructure.database.models import Reward
+
+
+async def get_rewards_for_epoch(
+    session: AsyncSession, epoch_number: int
+) -> list[Reward]:
+    """
+    Get all rewards for a given epoch.
+    """
+
+    results = await session.scalars(
+        select(Reward)
+        .where(Reward.epoch == epoch_number)
+        .order_by(Reward.address.asc())
+    )
+
+    return list(results.all())
+
+
+async def get_rewards_for_projects_in_epoch(
+    session: AsyncSession, epoch_number: int, projects_addresses: list[Address]
+) -> list[Reward]:
+    """
+    Get all rewards for a given epoch for a list of projects.
+    """
+
+    results = await session.scalars(
+        select(Reward)
+        .where(Reward.epoch == epoch_number)
+        .where(Reward.address.in_(projects_addresses))
+        .order_by(Reward.address.asc())
+    )
+
+    return list(results.all())
diff --git a/backend/v2/project_rewards/router.py b/backend/v2/project_rewards/router.py
index 7a68f8e531..48f1a258b1 100644
--- a/backend/v2/project_rewards/router.py
+++ b/backend/v2/project_rewards/router.py
@@ -1,14 +1,378 @@
 from fastapi import APIRouter
+from app.context.epoch_state import EpochState
+from app.exceptions import NotImplementedForGivenEpochState
+from v2.allocations.repositories import (
+    get_donors_for_epoch,
+    sum_allocations_by_epoch,
+)
+from v2.core.types import Address
+from v2.epoch_snapshots.repositories import get_finalized_epoch_snapshot
+from v2.epochs.dependencies import (
+    GetEpochsContracts,
+    GetEpochsSubgraph,
+    get_epoch_state,
+)
+from v2.project_rewards.repositories import get_rewards_for_projects_in_epoch
+from v2.projects.dependencies import (
+    GetProjectsAllocationThresholdGetter,
+    GetProjectsContracts,
+)
+from v2.user_patron_mode.repositories import (
+    get_all_patrons_at_timestamp,
+    get_all_users_budgets_by_epoch,
+    get_budget_by_user_address_and_epoch,
+)
+from v2.core.dependencies import GetSession
+from v2.project_rewards.services import get_rewards_merkle_tree_for_epoch
 from v2.project_rewards.dependencies import GetProjectRewardsEstimator
-from v2.project_rewards.schemas import EstimatedProjectRewardsResponse
+from v2.project_rewards.schemas import (
+    EpochBudgetItemV1,
+    EpochBudgetsResponseV1,
+    EstimatedBudgetByDaysRequestV1,
+    EstimatedBudgetByEpochRequestV1,
+    EstimatedProjectRewardsResponseV1,
+    ProjectFundingSummaryV1,
+    RewardsLeverageResponseV1,
+    RewardsMerkleTreeResponseV1,
+    ThresholdResponseV1,
+    UnusedRewardsResponseV1,
+    UpcomingUserBudgetResponseV1,
+    UserBudgetResponseV1,
+    UserBudgetWithMatchedFundingResponseV1,
+)
 
-api = APIRouter(prefix="/rewards", tags=["Allocations"])
+api = APIRouter(prefix="/rewards", tags=["Rewards"])
+
+
+@api.get("/budget/{user_address}/epoch/{epoch_number}")
+async def get_user_budget_for_epoch_v1(
+    session: GetSession,
+    user_address: Address,
+    epoch_number: int,
+) -> UserBudgetResponseV1:
+    """
+    Returns user's rewards budget available to allocate for given epoch
+    """
+    budget = await get_budget_by_user_address_and_epoch(
+        session,
+        user_address,
+        epoch_number,
+    )
+
+    # Q: Should we return 0 or raise exception when user has no budget for the epoch?
+
+    return UserBudgetResponseV1(budget=budget or 0)
+
+
+@api.get("/budget/{user_address}/upcoming")
+async def get_user_budget_for_upcoming_epoch_v1(
+
+    user_address: Address,
+) -> UpcomingUserBudgetResponseV1:
+    """
+    Returns the upcoming user budget based on if allocation happened now.
+    """
+    # TODO we need to handle snapshots here unfortunatelly
+    upcoming_budget = await get_upcoming_user_budget(user_address)
+
+    # context = state_context(EpochState.SIMULATED, with_block_range=True)
+
+    # user_deposits = CalculatedUserDeposits(
+    #         events_generator=DbAndGraphEventsGenerator()
+    #     )
+    # octant_rewards = CalculatedOctantRewards(
+    #     staking_proceeds=EstimatedStakingProceeds(),
+    #     effective_deposits=user_deposits,
+    # )
+    # UpcomingUserBudgets(
+    #     simulated_pending_snapshot_service=SimulatedPendingSnapshots(
+    #         effective_deposits=user_deposits, octant_rewards=octant_rewards
+    #     )
+    # )
+
+
+
+    # simulated_snapshot = (
+    #     self.simulated_pending_snapshot_service.simulate_pending_epoch_snapshot(
+    #         context
+    #     )
+    # )
+
+    # def _calculate_pending_epoch_snapshot(self, context: Context) -> PendingSnapshotDTO:
+    # rewards = self.octant_rewards.get_octant_rewards(context)
+
+        # def get_octant_rewards(self, context: Context) -> OctantRewardsDTO:
+        #     eth_proceeds = self.staking_proceeds.get_staking_proceeds(context)
+            # def get_staking_proceeds(self, context: Context) -> int:
+            #     return estimate_staking_proceeds(context.epoch_details.duration_sec)
+
+                # epoch_details = await epochs_subgraph.fetch_epoch_by_number(epoch_number)
+                # duration_sec = epoch_details.duration
+                # return estimate_staking_proceeds(duration_sec)
+
+    #     total_effective_deposit = self.effective_deposits.get_total_effective_deposit(
+    #         context
+    #     )
+        # def get_total_effective_deposit(self, context: Context) -> int:
+        #    events = self.events_generator.get_all_users_events(context)
+        #    _, total = calculate_effective_deposits(
+        #        context.epoch_details, context.epoch_settings, events
+        #    )
+        #    return total
+
+
+    #     rewards_settings = context.epoch_settings.octant_rewards
+    #     octant_rewards = calculate_rewards(
+    #         rewards_settings, eth_proceeds, total_effective_deposit
+    #     )
+    #     (
+    #         locked_ratio,
+    #         total_rewards,
+    #         vanilla_individual_rewards,
+    #         op_cost,
+    #         ppf,
+    #         community_fund,
+    #     ) = (
+    #         octant_rewards.locked_ratio,
+    #         octant_rewards.total_rewards,
+    #         octant_rewards.vanilla_individual_rewards,
+    #         octant_rewards.operational_cost,
+    #         octant_rewards.ppf_value,
+    #         octant_rewards.community_fund,
+    #     )
+
+    #     return OctantRewardsDTO(
+    #         staking_proceeds=eth_proceeds,
+    #         locked_ratio=locked_ratio,
+    #         total_effective_deposit=total_effective_deposit,
+    #         total_rewards=total_rewards,
+    #         vanilla_individual_rewards=vanilla_individual_rewards,
+    #         operational_cost=op_cost,
+    #         ppf=ppf,
+    #         community_fund=community_fund,
+    #     )
+
+    # (
+    #     user_deposits,
+    #     _,
+    # ) = self.effective_deposits.get_all_effective_deposits(context)
+    # user_budgets = calculate_user_budgets(
+    #     context.epoch_settings.user.budget, rewards, user_deposits
+    # )
+
+    # return PendingSnapshotDTO(
+    #     rewards=rewards, user_deposits=user_deposits, user_budgets=user_budgets
+    # )
+
+    # user_budget = next(
+    #     filter(
+    #         lambda budget_info: budget_info.user_address == user_address,
+    #         upcoming_user_budgets,
+    #     ),
+    #     None,
+    # )
+    # if not user_budget:
+    #     return 0
+    # return user_budget.budget
+
+    # upcoming_budget = core.get_upcoming_budget(
+    #     user_address, simulated_snapshot.user_budgets
+    # )
+
+    # return upcoming_budget
+
+    return UpcomingUserBudgetResponseV1(upcoming_budget=upcoming_budget)
+
+
+
+
+
+@api.get("/budgets/epoch/{epoch_number}")
+async def get_epoch_budgets_v1(
+    session: GetSession,
+    epoch_number: int,
+) -> EpochBudgetsResponseV1:
+    """
+    Returns all users rewards budgets for the epoch.
+    """
+    budgets = await get_all_users_budgets_by_epoch(session, epoch_number)
+
+    return EpochBudgetsResponseV1(
+        budgets=[
+            EpochBudgetItemV1(address=address, amount=amount)
+            for address, amount in budgets.items()
+        ]
+    )
+
+
+@api.post("/estimated_budget")
+async def get_estimated_budget_v1(
+    request: EstimatedBudgetByEpochRequestV1,
+) -> UserBudgetWithMatchedFundingResponseV1:
+    # leverage = octant_rewards_controller.get_last_finalized_epoch_leverage()
+    #     context = state_context(EpochState.FINALIZED)
+    #     service = get_services(context.epoch_state).octant_rewards_service
+
+    #     return service.get_leverage(context)
+    #         allocations_sum = database.allocations.get_alloc_sum_by_epoch(
+    #             context.epoch_details.epoch_num
+    #         )
+    #         finalized_snapshot = database.finalized_epoch_snapshot.get_by_epoch(
+    #             context.epoch_details.epoch_num
+    #         )
+    #         matched_rewards = int(finalized_snapshot.matched_rewards)
+
+    #         return context.epoch_settings.project.rewards.leverage.calculate_leverage(
+    #             matched_rewards, allocations_sum
+    #         )
+            # Leverage:
+            # return matched_rewards / total_allocated if total_allocated else 0
+
+    # epochs_budget = budget_controller.estimate_epochs_budget(no_epochs, glm_amount)
+    #     validate_estimate_budget_by_epochs_inputs(no_epochs, glm_amount)
+
+    #     future_context = state_context(EpochState.FUTURE)
+    #     future_rewards_service = get_services(EpochState.FUTURE).octant_rewards_service
+    #     future_rewards = future_rewards_service.get_octant_rewards(future_context)
+
+        # def _get_future_epoch_details(epoch_num: int) -> EpochDetails:
+        #     epoch_details = epochs.get_future_epoch_props()
+        #     start = epoch_details[2]
+        #     duration = epoch_details[3]
+        #     decision_window = epoch_details[4]
+        #     return EpochDetails(
+        #         epoch_num=epoch_num,
+        #         start=start,
+        #         duration=duration,
+        #         decision_window=decision_window,
+        #         remaining_sec=duration,
+        #     )
+
+        # return FutureServices(
+        #     octant_rewards_service=CalculatedOctantRewards(
+        #         staking_proceeds=EstimatedStakingProceeds(),
+        #         effective_deposits=ContractBalanceUserDeposits(),
+        #     ),
+        #     projects_metadata_service=StaticProjectsMetadataService(),
+        #     projects_details_service=StaticProjectsDetailsService(),
+        # )
+
+        # future_rewards = await get_octant_rewards(session, epoch_number, start_sec, end_sec)
+
+
+    #     epoch_duration = future_context.epoch_details.duration_sec
+
+    #     return no_epochs * core.estimate_epoch_budget(
+    #         future_context, future_rewards, epoch_duration, glm_amount
+    #     )
+
+    matching_fund = budget_controller.get_matching_fund(epochs_budget, leverage)
+    # def get_matching_fund(budget: int, leverage: float) -> int:
+    # return core.calculate_matching_fund(budget, leverage)
+    # def calculate_matching_fund(budget: int, leverage: float) -> int:
+    # return int(budget * leverage)
+
+    return EstimatedRewardsDTO(
+        estimated_budget=epochs_budget, leverage=leverage, matching_fund=matching_fund
+    )
+
+
+@api.post("/estimated_budget/by_days")
+async def get_estimated_budget_by_days_v1(
+    request: EstimatedBudgetByDaysRequestV1,
+) -> UserBudgetResponseV1:
+    validate_estimate_budget_inputs(days, glm_amount)
+
+    lock_duration_sec = days_to_sec(days)
+    return estimate_budget(lock_duration_sec, glm_amount)
+    # current_context = state_context(EpochState.CURRENT)
+    # current_rewards_service = get_services(EpochState.CURRENT).octant_rewards_service
+    # current_rewards = current_rewards_service.get_octant_rewards(current_context)
+
+    # future_context = state_context(EpochState.FUTURE)
+    # future_rewards_service = get_services(EpochState.FUTURE).octant_rewards_service
+    # future_rewards = future_rewards_service.get_octant_rewards(future_context)
+
+    # return core.estimate_budget(
+    #     current_context,
+    #     future_context,
+    #     current_rewards,
+    #     future_rewards,
+    #     lock_duration_sec,
+    #     glm_amount,
+    # )
+
+
+@api.get("/leverage/{epoch_number}")
+async def get_rewards_leverage_v1(
+    session: GetSession,
+    epochs_contracts: GetEpochsContracts,
+    epoch_number: int,
+) -> RewardsLeverageResponseV1:
+    epoch_state = await get_epoch_state(session, epochs_contracts, epoch_number)
+
+    if epoch_state > EpochState.PENDING:
+        raise NotImplementedForGivenEpochState()
+
+    if epoch_state == EpochState.FINALIZED:
+        # We are in pending epoch, so we need to get the leverage from the pending epoch
+
+        allocations_sum = await sum_allocations_by_epoch(session, epoch_number)
+
+        # allocations_sum = database.allocations.get_alloc_sum_by_epoch(
+        #     context.epoch_details.epoch_num
+        # )
+
+        finalized_snapshot = await get_finalized_epoch_snapshot(session, epoch_number)
+        # finalized_snapshot = database.finalized_epoch_snapshot.get_by_epoch(
+        #     context.epoch_details.epoch_num
+        # )
+        matched_rewards = int(finalized_snapshot.matched_rewards)
+
+        return RewardsLeverageResponseV1(
+            leverage=matched_rewards / allocations_sum if allocations_sum else 0
+        )
+
+        # return context.epoch_settings.project.rewards.leverage.calculate_leverage(
+        #     matched_rewards, allocations_sum
+        # )
+        # def calculate_leverage(self, matched_rewards: int, total_allocated: int) -> float:
+        # return matched_rewards / total_allocated if total_allocated else 0
+
+    if epoch_state <= EpochState.PENDING:
+        allocations_sum = await sum_allocations_by_epoch(session, epoch_number)
+        # allocations_sum = database.allocations.get_alloc_sum_by_epoch(
+        #     context.epoch_details.epoch_num
+        # )
+        matched_rewards = self.get_matched_rewards(context)
+
+        return RewardsLeverageResponseV1(
+            leverage=matched_rewards / allocations_sum if allocations_sum else 0
+        )
+
+        # return context.epoch_settings.project.rewards.leverage.calculate_leverage(
+        #     matched_rewards, allocations_sum
+        # )
+        # def calculate_leverage(self, matched_rewards: int, total_allocated: int) -> float:
+        # return matched_rewards / total_allocated if total_allocated else 0
+
+
+@api.get("/merkle_tree/{epoch_number}")
+async def get_rewards_merkle_tree_v1(
+    session: GetSession,
+    epoch_number: int,
+) -> RewardsMerkleTreeResponseV1:
+    """
+    Returns the rewards merkle tree for a given epoch.
+    """
+
+    return await get_rewards_merkle_tree_for_epoch(session, epoch_number)
 
 
 @api.get("/projects/estimated")
-async def get_estimated_project_rewards(
+async def get_estimated_project_rewards_v1(
     project_rewards_estimator: GetProjectRewardsEstimator,
-) -> EstimatedProjectRewardsResponse:
+) -> EstimatedProjectRewardsResponseV1:
     """
     Returns foreach project current allocation sum and estimated matched rewards.
 
@@ -17,6 +381,110 @@ async def get_estimated_project_rewards(
 
     estimated_funding = await project_rewards_estimator.get()
 
-    return EstimatedProjectRewardsResponse(
+    return EstimatedProjectRewardsResponseV1(
         rewards=[f for f in estimated_funding.project_fundings.values()]
     )
+
+
+@api.get("/projects/epoch/{epoch_number}")
+async def get_rewards_for_projects_in_epoch_v1(
+    session: GetSession,
+    projects_contracts: GetProjectsContracts,
+    epoch_number: int,
+) -> EstimatedProjectRewardsResponseV1:
+    """
+    Returns projects with matched rewards for a given finalized epoch.
+
+    """
+
+    projects = await projects_contracts.get_project_addresses(epoch_number)
+    rewards = await get_rewards_for_projects_in_epoch(session, epoch_number, projects)
+
+    return EstimatedProjectRewardsResponseV1(
+        rewards=[
+            ProjectFundingSummaryV1(
+                address=reward.address,
+                allocated=int(reward.amount) - int(reward.matched),
+                matched=int(reward.matched),
+            )
+            for reward in rewards
+        ]
+    )
+
+
+@api.get("/threshold/{epoch_number}")
+async def get_rewards_threshold_v1(
+    epoch_number: int,
+    projects_allocation_threshold_getter: GetProjectsAllocationThresholdGetter,
+) -> ThresholdResponseV1:
+    threshold = await projects_allocation_threshold_getter.get()
+
+    return ThresholdResponseV1(threshold=threshold)
+
+
+@api.get("/unused/{epoch_number}")
+async def get_unused_rewards_v1(
+    session: GetSession,
+    epoch_subgraph: GetEpochsSubgraph,
+    epoch_number: int,
+) -> UnusedRewardsResponseV1:
+    """
+    Returns unallocated value and the number of users who didn't use their rewards in an epoch
+    """
+
+    ts = await epoch_subgraph.get_epoch_by_number(epoch_number)
+
+    budgets = await get_all_users_budgets_by_epoch(session, epoch_number)
+
+    # budgets = self.user_budgets.get_all_budgets(context)
+    #     budgets = Budget.query.filter_by(epoch=epoch).all()
+    # return {budget.user.address: int(budget.budget) for budget in budgets}
+
+    donors = await get_donors_for_epoch(session, epoch_number)
+    # donors = self.allocations.get_all_donors_addresses(context)
+    # users = User.query.filter(
+    #     User.allocations.any(epoch=epoch_num, deleted_at=None)
+    # ).all()
+    # return [u.address for u in users]
+
+    patrons = await get_all_patrons_at_timestamp(
+        session, ts.finalized_timestamp.datetime()
+    )
+
+    excluded_addresses = set(donors + patrons)
+
+    unused_budgets = {budget for budget in budgets if budget not in excluded_addresses}
+
+    return UnusedRewardsResponseV1(
+        addresses=list(unused_budgets.keys()), value=sum(unused_budgets.values())
+    )
+
+    # patrons = self.patrons_mode.get_all_patrons_addresses(context)
+    # patrons = self._get_patron_budgets(context.epoch_details, with_budget)
+    #     ts = epoch.finalized_timestamp
+    # patrons = database.patrons.get_all_patrons_at_timestamp(ts.datetime())
+
+    # if with_budget:
+    #     all_budgets = database.budgets.get_all_by_epoch(epoch.epoch_num)
+    #     return {
+    #         patron: all_budgets[patron]
+    #         for patron in patrons
+    #         if patron in all_budgets.keys()
+    #     }
+    # else:
+    #     return {patron: 0 for patron in patrons}
+
+    # return list(patrons.keys())
+
+    # return get_unused_rewards(budgets, donors, patrons)
+
+
+# def get_unused_rewards(
+#     budgets: Dict[str, int], donors: List[str], patrons: List[str]
+# ) -> Dict[str, int]:
+#     for donor in donors:
+#         budgets.pop(donor)
+#     for patron in patrons:
+#         budgets.pop(patron)
+
+#     return budgets
diff --git a/backend/v2/project_rewards/sablier.py b/backend/v2/project_rewards/sablier.py
new file mode 100644
index 0000000000..74f0230a95
--- /dev/null
+++ b/backend/v2/project_rewards/sablier.py
@@ -0,0 +1,112 @@
+
+
+
+from gql import Client, gql
+from gql.transport.aiohttp import AIOHTTPTransport
+
+
+import backoff
+from app.infrastructure.sablier.events import SablierStream
+from v2.epochs.subgraphs import BackoffParams
+
+class SablierEventsGenerator:
+
+    def __init__(
+        self,
+        url: str,
+        sender: str,
+        token_address: str,
+        backoff_params: BackoffParams | None = None,
+    ):
+        self.url = url
+        self.sender = sender
+        self.token_address = token_address
+
+        self.gql_client = Client(
+            transport=AIOHTTPTransport(url=self.url, timeout=2),
+            fetch_schema_from_transport=False,
+        )
+
+        if backoff_params is not None:
+            backoff_decorator = backoff.on_exception(
+                backoff.expo,
+                backoff_params.exception,
+                max_time=backoff_params.max_time,
+                giveup=backoff_params.giveup,
+            )
+
+            self.gql_client.execute_async = backoff_decorator(
+                self.gql_client.execute_async
+            )
+
+
+    async def _fetch_streams(self, query: str, variables: dict) -> list[SablierStream]:
+
+        all_streams = []
+        has_more = True
+        limit = 1000
+        skip = 0
+
+        while has_more:
+            variables.update({"limit": limit, "skip": skip})
+
+            # logger.debug(f"[Sablier Subgraph] Querying streams with skip: {skip}")
+            result = await self.gql_client.execute_async(
+                gql(query), variable_values=variables
+            )
+
+            streams = result.get("streams", [])
+
+            # app.logger.debug(f"[Sablier Subgraph] Received {len(streams)} streams.")
+
+            for stream in streams:
+                actions = stream.get("actions", [])
+                final_intact_amount = stream.get("intactAmount", 0)
+                all_streams.append(
+                    SablierStream(actions=actions, intactAmount=final_intact_amount)
+                )
+
+            if len(streams) < limit:
+                has_more = False
+            else:
+                skip += limit
+
+        return all_streams
+
+    async def get_all_streams_history(self) -> list[SablierStream]:
+        """
+        Get all the locks and unlocks in history.
+        """
+
+        query = """
+            query GetAllEvents($sender: String!, $tokenAddress: String!, $limit: Int!, $skip: Int!) {
+            streams(
+                where: {
+                sender: $sender
+                asset_: {address: $tokenAddress}
+                transferable: false
+                }
+                first: $limit
+                skip: $skip
+                orderBy: timestamp
+            ) {
+                id
+                intactAmount
+                actions(where: {category_in: [Cancel, Withdraw, Create]}, orderBy: timestamp) {
+                category
+                addressA
+                addressB
+                amountA
+                amountB
+                timestamp
+                hash
+                }
+            }
+            }
+        """
+        variables = {
+            "sender": self.sender,
+            "tokenAddress": self.token_address,
+        }
+
+        return await self._fetch_streams(query, variables)
diff --git a/backend/v2/project_rewards/schemas.py b/backend/v2/project_rewards/schemas.py
index 5c53fce910..d2ce4e6433 100644
--- a/backend/v2/project_rewards/schemas.py
+++ b/backend/v2/project_rewards/schemas.py
@@ -2,7 +2,7 @@
 from v2.core.types import Address, BigInteger, OctantModel
 
 
-class ProjectFundingSummary(OctantModel):
+class ProjectFundingSummaryV1(OctantModel):
     address: Address = Field(..., description="The address of the project")
     allocated: BigInteger = Field(
         ..., description="Sum of all allocation amounts for the project"
@@ -12,35 +12,89 @@ class ProjectFundingSummary(OctantModel):
     )
 
 
-class EstimatedProjectRewardsResponse(OctantModel):
-    rewards: list[ProjectFundingSummary] = Field(
+class EstimatedProjectRewardsResponseV1(OctantModel):
+    rewards: list[ProjectFundingSummaryV1] = Field(
         ..., description="List of project funding summaries"
     )
 
 
-# project_rewards = await project_rewards_estimator.get(pending_epoch_number)
-#     rewards = [
-#         {
-#             "address": project_address,
-#             "allocated": str(project_rewards.amounts_by_project[project_address]),
-#             "matched": str(project_rewards.matched_by_project[project_address]),
-#         }
-#         for project_address in project_rewards.amounts_by_project.keys()
-#     ]
-
-# @ns.doc(
-#     description="Returns project rewards with estimated matched rewards for the pending epoch"
-# )
-# @ns.response(
-#     200,
-#     "",
-# )
-# @ns.route("/projects/estimated")
-# class EstimatedProjectRewards(OctantResource):
-#     @ns.marshal_with(projects_rewards_model)
-#     def get(self):
-#         app.logger.debug("Getting project rewards for the pending epoch")
-#         project_rewards = get_estimated_project_rewards().rewards
-#         app.logger.debug(f"Project rewards in the pending epoch: {project_rewards}")
-
-#         return {"rewards": project_rewards}
+class EstimatedBudgetByEpochRequestV1(OctantModel):
+    number_of_epochs: int = Field(
+        ..., description="Number of epochs when GLM are locked", ge=0
+    )
+    glm_amount: BigInteger = Field(
+        ..., description="Amount of estimated GLM locked in wei", ge=0
+    )
+
+
+class UserBudgetWithMatchedFundingResponseV1(OctantModel):
+    budget: BigInteger = Field(..., description="User budget for given epoch in wei.")
+    matched_funding: BigInteger = Field(
+        ..., description="Matched funding for given epoch in wei."
+    )
+
+
+class EstimatedBudgetByDaysRequestV1(OctantModel):
+    days: int = Field(..., description="Number of days when GLM are locked")
+    glm_amount: BigInteger = Field(
+        ..., description="Amount of estimated GLM locked in wei"
+    )
+
+
+class RewardsMerkleTreeLeafV1(OctantModel):
+    address: Address = Field(..., description="User account or project address")
+    amount: BigInteger = Field(..., description="Assigned reward")
+
+
+class RewardsMerkleTreeResponseV1(OctantModel):
+    epoch: int = Field(..., description="Epoch number")
+    rewards_sum: BigInteger = Field(
+        ..., description="Sum of assigned rewards for epoch"
+    )
+    root: str = Field(..., description="Merkle Tree root for epoch")
+    leaves: list[RewardsMerkleTreeLeafV1] = Field(
+        ..., description="List of Merkle Tree leaves"
+    )
+    leaf_encoding: list[str] = Field(..., description="Merkle tree leaf encoding")
+
+
+class UserBudgetResponseV1(OctantModel):
+    budget: BigInteger = Field(..., description="User budget for given epoch in wei.")
+
+
+class EpochBudgetItemV1(OctantModel):
+    address: Address = Field(..., description="User address")
+    amount: BigInteger = Field(..., description="User budget for given epoch in wei.")
+
+
+class EpochBudgetsResponseV1(OctantModel):
+    budgets: list[EpochBudgetItemV1] = Field(
+        ..., description="List of user budgets for given epoch"
+    )
+
+
+class UpcomingUserBudgetResponseV1(OctantModel):
+    upcoming_budget: BigInteger = Field(
+        ..., description="Calculated upcoming user budget."
+    )
+
+
+class ThresholdResponseV1(OctantModel):
+    threshold: BigInteger = Field(
+        ...,
+        description="Threshold, that projects have to pass to be eligible for receiving rewards.",
+    )
+
+
+class RewardsLeverageResponseV1(OctantModel):
+    leverage: float = Field(..., description="Leverage of the allocated funds")
+
+
+class UnusedRewardsResponseV1(OctantModel):
+    addresses: list[Address] = Field(
+        ...,
+        description="List of addresses that neither allocated rewards nor toggled patron mode",
+    )
+    value: BigInteger = Field(
+        ..., description="Total unused rewards sum in an epoch (WEI)"
+    )
diff --git a/backend/v2/project_rewards/services.py b/backend/v2/project_rewards/services.py
index 76e6b43734..e139ba3e5c 100644
--- a/backend/v2/project_rewards/services.py
+++ b/backend/v2/project_rewards/services.py
@@ -1,7 +1,13 @@
 import asyncio
 from dataclasses import dataclass
+from multiproof import StandardMerkleTree
 
 from sqlalchemy.ext.asyncio import AsyncSession
+from v2.project_rewards.repositories import get_rewards_for_epoch
+from v2.project_rewards.schemas import (
+    RewardsMerkleTreeLeafV1,
+    RewardsMerkleTreeResponseV1,
+)
 from v2.allocations.repositories import get_allocations_with_user_uqs
 from v2.matched_rewards.services import MatchedRewardsEstimator
 from v2.project_rewards.capped_quadriatic import (
@@ -35,3 +41,40 @@ async def get(self) -> CappedQuadriaticFunding:
             allocations=allocations,
             matched_rewards=matched_rewards,
         )
+
+
+async def get_rewards_merkle_tree_for_epoch(
+    session: AsyncSession,
+    epoch_number: int,
+) -> RewardsMerkleTreeResponseV1 | None:
+    """
+    Get the rewards merkle tree for a given epoch.
+    """
+
+    # Merkle tree is based on rewards, so we need to get them first
+    rewards = await get_rewards_for_epoch(session, epoch_number)
+    if not rewards:
+        return None
+
+    # Build the merkle tree (using the leaf encoding)
+    LEAF_ENCODING: list[str] = ["address", "uint256"]
+    mt = StandardMerkleTree.of(
+        [[leaf.address, int(leaf.amount)] for leaf in rewards],
+        LEAF_ENCODING,
+    )
+
+    # Build response model
+    leaves = [
+        RewardsMerkleTreeLeafV1(address=leaf.value[0], amount=leaf.value[1])
+        for leaf in mt.values
+    ]
+
+    rewards_sum = sum(leaf.amount for leaf in leaves)
+
+    return RewardsMerkleTreeResponseV1(
+        epoch=epoch_number,
+        rewards_sum=rewards_sum,
+        root=mt.root,
+        leaves=leaves,
+        leaf_encoding=LEAF_ENCODING,
+    )
diff --git a/backend/v2/project_rewards/user_events.py b/backend/v2/project_rewards/user_events.py
new file mode 100644
index 0000000000..08b8a9bf51
--- /dev/null
+++ b/backend/v2/project_rewards/user_events.py
@@ -0,0 +1,203 @@
+
+
+from itertools import groupby
+from operator import attrgetter
+from sqlalchemy import select
+from sqlalchemy.ext.asyncio import AsyncSession
+from sqlalchemy.orm import selectinload
+from typing import Dict
+
+from app.engine.octant_rewards import OctantRewardsSettings
+from app.engine.user.budget.with_ppf import UserBudgetWithPPF
+from app.engine.user.effective_deposit import DepositEvent, EventType, UserDeposit, UserEffectiveDepositPayload
+from app.engine.user.effective_deposit.weighted_average.default_with_sablier_timebox import DefaultWeightedAverageWithSablierTimebox
+from app.infrastructure.database.models import Deposit
+from app.infrastructure.graphql.locks import get_locks_by_timestamp_range
+from app.infrastructure.graphql.unlocks import get_unlocks_by_timestamp_range
+from app.modules.common.sablier_events_mapper import FlattenStrategy, flatten_sablier_events, process_to_locks_and_unlocks
+from app.modules.dto import OctantRewardsDTO, PendingSnapshotDTO
+from app.modules.octant_rewards.core import calculate_rewards
+from app.modules.snapshots.pending.core import calculate_user_budgets
+from app.modules.user.budgets.core import get_upcoming_budget
+from app.modules.user.events_generator.core import unify_deposit_balances
+
+
+async def get_all_deposit_events_for_epoch(
+    session: AsyncSession,
+    epoch_number: int,
+) -> dict[str, list[DepositEvent]]:
+    
+    results = await session.scalars(
+        select(Deposit)
+        .options(selectinload(Deposit.user))
+        .where(Deposit.epoch == epoch_number)
+    )
+
+    return {
+        result.user.address: result for result in results
+    }
+
+
+async def get_all_user_events(
+        session: AsyncSession,
+        epoch_number: int,
+        start_sec: int,
+        end_sec: int,
+) -> dict[str, list[DepositEvent]]:
+    
+    # Get all locked amounts for the previous epoch
+    epoch_start_locked_amounts = await get_all_deposit_events_for_epoch(
+        session, epoch_number - 1
+    )
+    epoch_start_events = [
+        DepositEvent(
+            user=user,
+            type=EventType.LOCK,
+            timestamp=start_sec,
+            amount=0,  # it is not a deposit in fact
+            deposit_before=int(deposit.epoch_end_deposit),
+        )
+        for user, deposit in epoch_start_locked_amounts.items()
+    ]
+
+    sablier = SablierEventsGenerator()
+    sablier_streams = await sablier.get_all_streams_history()
+    mapped_streams = process_to_locks_and_unlocks(
+        sablier_streams, from_timestamp=start_sec, to_timestamp=end_sec
+    )
+    epoch_events = []
+    epoch_events += flatten_sablier_events(mapped_streams, FlattenStrategy.ALL)
+    epoch_events += get_locks_by_timestamp_range(start_sec, end_sec)
+    epoch_events += get_unlocks_by_timestamp_range(start_sec, end_sec)
+
+    epoch_events = [DepositEvent.from_dict(event) for event in epoch_events]
+    sorted_events = sorted(epoch_events, key=attrgetter("user", "timestamp"))
+
+    user_events = {
+        k: list(g) for k, g in groupby(sorted_events, key=attrgetter("user"))
+    }
+
+    for event in epoch_start_events:
+        if event.user in user_events:
+            user_events[event.user].insert(0, event)
+        else:
+            user_events[event.user] = [event]
+
+    epoch_start_users = list(map(attrgetter("user"), epoch_start_events))
+    for user_address in user_events:
+        if user_address not in epoch_start_users:
+            user_events[user_address].insert(
+                0,
+                DepositEvent(
+                    user_address,
+                    EventType.LOCK,
+                    timestamp=start_sec,
+                    amount=0,
+                    deposit_before=0,
+                ),
+            )
+
+        user_events[user_address] = unify_deposit_balances(
+            user_events[user_address]
+        )
+
+    return user_events
+
+
+def calculate_effective_deposits(
+    start_sec: int,
+    end_sec: int,
+    events: Dict[str, list[DepositEvent]],
+) -> tuple[list[UserDeposit], int]:
+    
+    effective_deposit_calculator = DefaultWeightedAverageWithSablierTimebox()
+    payload = UserEffectiveDepositPayload(
+        epoch_start=start_sec,
+        epoch_end=end_sec,
+        lock_events_by_addr=events,
+    )
+
+    return effective_deposit_calculator.calculate_users_effective_deposits(payload)
+
+
+async def get_octant_rewards(
+    session: AsyncSession,
+    epoch_number: int,
+    start_sec: int,
+    end_sec: int,
+) -> OctantRewardsDTO:
+    
+    # epoch_details = await epochs_subgraph.fetch_epoch_by_number(epoch_number)
+    # duration_sec = epoch_details.duration
+    # return estimate_staking_proceeds(duration_sec)
+    eth_proceeds = await get_staking_proceeds(session, epoch_number, start_sec, end_sec)
+
+    events = await get_all_user_events(session, epoch_number, start_sec, end_sec)
+    user_deposits, total_effective_deposit = calculate_effective_deposits(start_sec, end_sec, events)
+
+    rewards_settings = OctantRewardsSettings()
+
+    octant_rewards = calculate_rewards(
+        rewards_settings, eth_proceeds, total_effective_deposit
+    )
+
+    (
+        locked_ratio,
+        total_rewards,
+        vanilla_individual_rewards,
+        op_cost,
+        ppf,
+        community_fund,
+    ) = (
+        octant_rewards.locked_ratio,
+        octant_rewards.total_rewards,
+        octant_rewards.vanilla_individual_rewards,
+        octant_rewards.operational_cost,
+        octant_rewards.ppf_value,
+        octant_rewards.community_fund,
+    )
+
+    return OctantRewardsDTO(
+        staking_proceeds=eth_proceeds,
+        locked_ratio=locked_ratio,
+        total_effective_deposit=total_effective_deposit,
+        total_rewards=total_rewards,
+        vanilla_individual_rewards=vanilla_individual_rewards,
+        operational_cost=op_cost,
+        ppf=ppf,
+        community_fund=community_fund,
+    )
+
+
+async def calculate_pending_epoch_snapshot(
+    session: AsyncSession,
+    epoch_number: int
+) -> PendingSnapshotDTO:
+    
+    octant_rewards = await get_octant_rewards(session, epoch_number, start_sec, end_sec)
+
+    events = await get_all_user_events(session, epoch_number, start_sec, end_sec)
+    user_deposits, total_effective_deposit = calculate_effective_deposits(start_sec, end_sec, events)
+
+    user_budget_calculator = UserBudgetWithPPF()
+    user_budgets = calculate_user_budgets(
+        user_budget_calculator, octant_rewards, user_deposits
+    )
+
+    return PendingSnapshotDTO(
+        rewards=octant_rewards, user_deposits=user_deposits, user_budgets=user_budgets
+    )
+
+
+async def get_budget(
+    session: AsyncSession,
+    epoch_number: int,
+    user_address: str
+) -> int:
+    simulated_snapshot = await calculate_pending_epoch_snapshot(session, epoch_number)
+
+    upcoming_budget = get_upcoming_budget(
+        user_address, simulated_snapshot.user_budgets
+    )
+
+    return upcoming_budget
diff --git a/backend/v2/projects/dependencies.py b/backend/v2/projects/dependencies.py
index 3cf6bfef4e..586229e832 100644
--- a/backend/v2/projects/dependencies.py
+++ b/backend/v2/projects/dependencies.py
@@ -45,11 +45,15 @@ def get_projects_allocation_threshold_getter(
     epoch_number: GetOpenAllocationWindowEpochNumber,
     session: GetSession,
     projects: GetProjectsContracts,
-    settings: Annotated[
-        ProjectsAllocationThresholdSettings,
-        Depends(get_projects_allocation_threshold_settings),
-    ],
 ) -> ProjectsAllocationThresholdGetter:
+    project_count_multiplier = 2 if epoch_number <= 2 else 1
+
     return ProjectsAllocationThresholdGetter(
-        epoch_number, session, projects, settings.project_count_multiplier
+        epoch_number, session, projects, project_count_multiplier
     )
+
+
+GetProjectsAllocationThresholdGetter = Annotated[
+    ProjectsAllocationThresholdGetter,
+    Depends(get_projects_allocation_threshold_getter),
+]
diff --git a/backend/v2/user_patron_mode/repositories.py b/backend/v2/user_patron_mode/repositories.py
index 903660a5ef..2af10af2eb 100644
--- a/backend/v2/user_patron_mode/repositories.py
+++ b/backend/v2/user_patron_mode/repositories.py
@@ -4,13 +4,14 @@
 from sqlalchemy import Numeric, cast, func
 from sqlalchemy.ext.asyncio import AsyncSession
 from sqlalchemy.future import select
-from v2.core.types import Address
+from v2.core.types import Address, BigInteger
 from v2.users.repositories import get_user_by_address
+from v2.core.transformers import transform_to_checksum_address
 
 
 async def get_all_patrons_at_timestamp(
     session: AsyncSession, dt: datetime
-) -> list[str]:
+) -> list[Address]:
     """
     Get all the user addresses that at given timestamp have patron_mode_enabled=True.
     """
@@ -27,7 +28,7 @@ async def get_all_patrons_at_timestamp(
         )
     )
 
-    return [row[0] for row in results.all()]
+    return [transform_to_checksum_address(row[0]) for row in results.all()]
 
 
 async def get_budget_sum_by_users_addresses_and_epoch(
@@ -87,6 +88,23 @@ async def get_budget_by_user_address_and_epoch(
     return int(budget)
 
 
+async def get_all_users_budgets_by_epoch(
+    session: AsyncSession, epoch_number: int
+) -> dict[Address, BigInteger]:
+    """
+    Get all budgets for a given epoch.
+    """
+    results = await session.execute(
+        select(Budget.budget, User.address)
+        .join(Budget.user)
+        .where(Budget.epoch == epoch_number)
+    )
+    return {
+        transform_to_checksum_address(row.address): int(row.budget)
+        for row in results.all()
+    }
+
+
 async def user_is_patron_with_budget(
     session: AsyncSession,
     user_address: Address,

From 707e0de581c9fe1ecdab0da1556e2b9346eaf996 Mon Sep 17 00:00:00 2001
From: adam-gf <adam@golem.foundation>
Date: Tue, 14 Jan 2025 22:08:51 +0100
Subject: [PATCH 02/14] wip: All things are working 1:1 prod. Now let's make it
 a bit nicer and testable

---
 backend/app/context/epoch/block_range.py      |   6 +-
 backend/app/modules/common/time.py            |   5 +-
 backend/v2/core/contracts.py                  |   1 +
 backend/v2/deposits/dependencies.py           |  19 +-
 backend/v2/deposits/repositories.py           | 127 +++++
 backend/v2/epochs/contracts.py                |   2 +-
 backend/v2/epochs/dependencies.py             |   3 +-
 backend/v2/epochs/subgraphs.py                |  62 +++
 backend/v2/glms/contracts.py                  |   6 +-
 backend/v2/glms/dependencies.py               |  14 +
 backend/v2/project_rewards/dependencies.py    |   4 +-
 backend/v2/project_rewards/router.py          | 515 ++++++++++++++----
 backend/v2/project_rewards/user_events.py     | 193 +++----
 backend/v2/sablier/__init__.py                |   0
 backend/v2/sablier/dependencies.py            |  54 ++
 .../sablier.py => sablier/subgraphs.py}       |  29 +-
 backend/v2/user_patron_mode/repositories.py   |   1 +
 17 files changed, 784 insertions(+), 257 deletions(-)
 create mode 100644 backend/v2/deposits/repositories.py
 create mode 100644 backend/v2/sablier/__init__.py
 create mode 100644 backend/v2/sablier/dependencies.py
 rename backend/v2/{project_rewards/sablier.py => sablier/subgraphs.py} (79%)

diff --git a/backend/app/context/epoch/block_range.py b/backend/app/context/epoch/block_range.py
index 44806c88af..20af7a5111 100644
--- a/backend/app/context/epoch/block_range.py
+++ b/backend/app/context/epoch/block_range.py
@@ -12,7 +12,9 @@ def get_blocks_range(
     if not with_block_range:
         return None, None
 
-    start_block = get_block_num_from_ts(start_sec) if start_sec <= now_sec else None
-    end_block = get_block_num_from_ts(end_sec) if end_sec <= now_sec else None
+    # start_block = get_block_num_from_ts(start_sec) if start_sec <= now_sec else None
+    # end_block = get_block_num_from_ts(end_sec) if end_sec <= now_sec else None
 
+    start_block = None
+    end_block = None
     return start_block, end_block
diff --git a/backend/app/modules/common/time.py b/backend/app/modules/common/time.py
index a7738c7a8f..7080afb08a 100644
--- a/backend/app/modules/common/time.py
+++ b/backend/app/modules/common/time.py
@@ -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()
diff --git a/backend/v2/core/contracts.py b/backend/v2/core/contracts.py
index e73c01d134..39b66f5b85 100644
--- a/backend/v2/core/contracts.py
+++ b/backend/v2/core/contracts.py
@@ -8,4 +8,5 @@ class SmartContract:
     def __init__(self, w3: AsyncWeb3, abi: ABI, address: ChecksumAddress) -> None:
         self.abi = abi
         self.w3 = w3
+        self.address = address
         self.contract: AsyncContract = w3.eth.contract(address=address, abi=abi)
diff --git a/backend/v2/deposits/dependencies.py b/backend/v2/deposits/dependencies.py
index cbe9a36500..7691f73f31 100644
--- a/backend/v2/deposits/dependencies.py
+++ b/backend/v2/deposits/dependencies.py
@@ -1,7 +1,10 @@
 from typing import Annotated
 
 from fastapi import Depends
-from v2.core.dependencies import OctantSettings, Web3
+from v2.deposits.repositories import DepositEventsRepository
+from v2.epochs.dependencies import GetEpochsSubgraph
+from v2.sablier.dependencies import GetSablierSubgraph
+from v2.core.dependencies import GetSession, OctantSettings, Web3
 from v2.deposits.contracts import DEPOSITS_ABI, DepositsContracts
 
 
@@ -17,3 +20,17 @@ def get_deposits_contracts(
     w3: Web3, settings: Annotated[DepositsSettings, Depends(get_deposits_settings)]
 ) -> DepositsContracts:
     return DepositsContracts(w3, DEPOSITS_ABI, settings.deposits_contract_address)  # type: ignore[arg-type]
+
+
+
+def get_deposit_events_repository(
+    session: GetSession,
+    epochs_subgraph: GetEpochsSubgraph,
+    sublier_subgraph: GetSablierSubgraph,
+) -> DepositEventsRepository:
+    return DepositEventsRepository(session, epochs_subgraph, sublier_subgraph)
+
+
+# Annotated dependencies
+GetDepositsContracts = Annotated[DepositsContracts, Depends(get_deposits_contracts)]
+GetDepositEventsRepository = Annotated[DepositEventsRepository, Depends(get_deposit_events_repository)]
\ No newline at end of file
diff --git a/backend/v2/deposits/repositories.py b/backend/v2/deposits/repositories.py
new file mode 100644
index 0000000000..1191997e9f
--- /dev/null
+++ b/backend/v2/deposits/repositories.py
@@ -0,0 +1,127 @@
+
+
+from itertools import groupby
+from operator import attrgetter
+import os
+from sqlalchemy import select
+from sqlalchemy.ext.asyncio import AsyncSession
+from sqlalchemy.orm import selectinload
+from typing import Dict
+
+from app.engine.octant_rewards import OctantRewardsSettings
+from app.engine.user.budget.with_ppf import UserBudgetWithPPF
+from app.engine.user.effective_deposit import DepositEvent, EventType, UserDeposit, UserEffectiveDepositPayload
+from app.engine.user.effective_deposit.weighted_average.default_with_sablier_timebox import DefaultWeightedAverageWithSablierTimebox
+from app.infrastructure.database.models import Deposit
+from app.infrastructure.graphql.locks import get_locks_by_timestamp_range
+from app.infrastructure.graphql.unlocks import get_unlocks_by_timestamp_range
+from app.modules.common.sablier_events_mapper import FlattenStrategy, flatten_sablier_events, process_to_locks_and_unlocks
+from app.modules.dto import OctantRewardsDTO, PendingSnapshotDTO
+from app.modules.octant_rewards.core import calculate_rewards
+from app.modules.snapshots.pending.core import calculate_user_budgets
+from app.modules.user.budgets.core import get_upcoming_budget
+from app.modules.user.events_generator.core import unify_deposit_balances
+from app.modules.staking.proceeds.core import estimate_staking_proceeds
+from app.constants import SABLIER_SENDER_ADDRESS_SEPOLIA, SABLIER_TOKEN_ADDRESS_SEPOLIA, ZERO_ADDRESS
+from app.infrastructure import SubgraphEndpoints
+from v2.epochs.subgraphs import EpochsSubgraph
+from v2.sablier.subgraphs import SablierSubgraph
+
+
+
+async def get_all_deposit_events_for_epoch(
+    session: AsyncSession,
+    epoch_number: int,
+) -> dict[str, list[DepositEvent]]:
+    
+    results = await session.scalars(
+        select(Deposit)
+        .options(selectinload(Deposit.user))
+        .where(Deposit.epoch == epoch_number)
+    )
+
+    return {
+        result.user.address: result for result in results
+    }
+
+
+
+
+class DepositEventsRepository:
+    def __init__(
+            self, 
+            session: AsyncSession, 
+            epochs_subgraph: EpochsSubgraph, 
+            sablier_subgraph: SablierSubgraph
+        ):
+        self.session = session
+        self.epochs_subgraph = epochs_subgraph
+        self.sablier_subgraph = sablier_subgraph
+
+    async def get_all_users_events(
+        self,
+        epoch_number: int,
+        start_sec: int,
+        end_sec: int,
+    ) -> dict[str, list[DepositEvent]]:
+        """
+        Returns all user events (LOCK, UNLOCK) for a given epoch.
+        """
+        
+        # Get all locked amounts for the previous epoch
+        epoch_start_locked_amounts = await get_all_deposit_events_for_epoch(
+            self.session, epoch_number - 1
+        )
+        epoch_start_events = [
+            DepositEvent(
+                user=user,
+                type=EventType.LOCK,
+                timestamp=start_sec,
+                amount=0,  # it is not a deposit in fact
+                deposit_before=int(deposit.epoch_end_deposit),
+            )
+            for user, deposit in epoch_start_locked_amounts.items()
+        ]
+
+        sablier_streams = await self.sablier_subgraph.get_all_streams_history()
+        mapped_streams = process_to_locks_and_unlocks(
+            sablier_streams, from_timestamp=start_sec, to_timestamp=end_sec
+        )
+        # print("Mapped streams", mapped_streams)
+        epoch_events = []
+        epoch_events += flatten_sablier_events(mapped_streams, FlattenStrategy.ALL)
+        epoch_events += await self.epochs_subgraph.fetch_locks_by_timestamp_range(start_sec, end_sec)
+        epoch_events += await self.epochs_subgraph.fetch_unlocks_by_timestamp_range(start_sec, end_sec)
+
+        epoch_events = [DepositEvent.from_dict(event) for event in epoch_events]
+        sorted_events = sorted(epoch_events, key=attrgetter("user", "timestamp"))
+
+        user_events = {
+            k: list(g) for k, g in groupby(sorted_events, key=attrgetter("user"))
+        }
+
+        for event in epoch_start_events:
+            if event.user in user_events:
+                user_events[event.user].insert(0, event)
+            else:
+                user_events[event.user] = [event]
+
+        epoch_start_users = list(map(attrgetter("user"), epoch_start_events))
+        for user_address in user_events:
+            if user_address not in epoch_start_users:
+                user_events[user_address].insert(
+                    0,
+                    DepositEvent(
+                        user_address,
+                        EventType.LOCK,
+                        timestamp=start_sec,
+                        amount=0,
+                        deposit_before=0,
+                    ),
+                )
+
+            user_events[user_address] = unify_deposit_balances(
+                user_events[user_address]
+            )
+
+        return user_events
diff --git a/backend/v2/epochs/contracts.py b/backend/v2/epochs/contracts.py
index 8ef263bc96..ed45d7cf36 100644
--- a/backend/v2/epochs/contracts.py
+++ b/backend/v2/epochs/contracts.py
@@ -49,7 +49,7 @@ async def get_epoch_duration(self) -> int:
         logging.debug("[Epochs contract] Checking epoch duration")
         return await self.contract.functions.getEpochDuration().call()
 
-    async def get_future_epoch_props(self) -> Dict:
+    async def get_future_epoch_props(self) -> list[int]:
         logging.debug("[Epochs contract] Getting epoch props index")
         index = await self.contract.functions.epochPropsIndex().call()
         logging.debug("[Epochs contract] Getting next epoch props")
diff --git a/backend/v2/epochs/dependencies.py b/backend/v2/epochs/dependencies.py
index da4495df6d..7015fa0393 100644
--- a/backend/v2/epochs/dependencies.py
+++ b/backend/v2/epochs/dependencies.py
@@ -89,7 +89,8 @@ async def get_rewards_rate(epoch_number: int) -> float:
 async def get_epoch_state(
     session: GetSession, epochs_contracts: GetEpochsContracts, epoch_number: int
 ) -> EpochState:
-    current_epoch_number = await get_current_epoch(epoch_number)
+
+    current_epoch_number = await epochs_contracts.get_current_epoch()
     if epoch_number > current_epoch_number:
         return EpochState.FUTURE
 
diff --git a/backend/v2/epochs/subgraphs.py b/backend/v2/epochs/subgraphs.py
index 9c446ff684..41f8c36773 100644
--- a/backend/v2/epochs/subgraphs.py
+++ b/backend/v2/epochs/subgraphs.py
@@ -5,6 +5,8 @@
 from pydantic import TypeAdapter
 import backoff
 from app import exceptions
+from app.infrastructure.graphql.locks import LockEvent
+from app.infrastructure.graphql.unlocks import UnlockEvent
 from v2.core.types import OctantModel
 from v2.core.exceptions import EpochsNotFound
 from app.context.epoch.details import EpochDetails
@@ -76,6 +78,66 @@ def __init__(
                 self.gql_client.execute_async
             )
 
+    async def fetch_locks_by_timestamp_range(self, from_ts: int, to_ts: int) -> list[LockEvent]:
+        """
+        Get locks by timestamp range.
+        """
+        query = gql(
+            """
+            query GetLocks($fromTimestamp: Int!, $toTimestamp: Int!) {
+              lockeds(
+                first: 1000,
+                skip: 0,
+                orderBy: timestamp
+                where: {timestamp_gte: $fromTimestamp, timestamp_lt: $toTimestamp}
+              ) {
+                __typename
+                depositBefore
+                amount
+                timestamp
+                user
+                transactionHash
+              }
+            }
+            """
+        )
+        variables = {
+            "fromTimestamp": from_ts,
+            "toTimestamp": to_ts,
+        }
+        response = await self.gql_client.execute_async(query, variable_values=variables)
+        return response["lockeds"]
+
+    async def fetch_unlocks_by_timestamp_range(self, from_ts: int, to_ts: int) -> list[UnlockEvent]:
+        """
+        Get unlocks by timestamp range.
+        """
+        query = gql(
+            """
+            query GetUnlocks($fromTimestamp: Int!, $toTimestamp: Int!) {
+              unlockeds(
+                first: 1000,
+                skip: 0,
+                orderBy: timestamp
+                where: {timestamp_gte: $fromTimestamp, timestamp_lt: $toTimestamp}
+              ) {
+                __typename
+                depositBefore
+                amount
+                timestamp
+                user
+                transactionHash
+              }
+            }
+            """
+        )
+        variables = {
+            "fromTimestamp": from_ts,
+            "toTimestamp": to_ts,
+        }
+        response = await self.gql_client.execute_async(query, variable_values=variables)
+        return response["unlockeds"]
+
     async def fetch_epoch_by_number(self, epoch_number: int) -> EpochSubgraphItem:
         """Get EpochDetails from the subgraph for a given epoch number."""
         query = gql(
diff --git a/backend/v2/glms/contracts.py b/backend/v2/glms/contracts.py
index f5e9b26273..a324013f30 100644
--- a/backend/v2/glms/contracts.py
+++ b/backend/v2/glms/contracts.py
@@ -40,10 +40,10 @@ async def approve(self, owner: AddressKey, benefactor_address, wad: int):
             benefactor_address, wad
         ).build_transaction({"from": owner.address, "nonce": nonce})
         signed_tx = self.w3.eth.account.sign_transaction(transaction, owner.key)
-        return self.w3.eth.send_raw_transaction(signed_tx.rawTransaction)
+        return await self.w3.eth.send_raw_transaction(signed_tx.rawTransaction)
 
-    # def balance_of(self, owner: str) -> int:
-    #     return self.contract.functions.balanceOf(owner).call()
+    async def balance_of(self, owner: str) -> int:
+        return await self.contract.functions.balanceOf(owner).call()
 
 
 ERC20_ABI = [
diff --git a/backend/v2/glms/dependencies.py b/backend/v2/glms/dependencies.py
index 44debdc9ce..a97750c09c 100644
--- a/backend/v2/glms/dependencies.py
+++ b/backend/v2/glms/dependencies.py
@@ -1,6 +1,9 @@
+from __future__ import annotations
 from typing import Annotated
 
 from fastapi import Depends
+from v2.deposits.contracts import DepositsContracts
+from v2.deposits.dependencies import GetDepositsContracts
 from v2.core.dependencies import OctantSettings, Web3
 from v2.glms.contracts import ERC20_ABI, GLMContracts
 
@@ -17,3 +20,14 @@ def get_glm_contracts(
     w3: Web3, settings: Annotated[GLMSettings, Depends(get_glm_settings)]
 ) -> GLMContracts:
     return GLMContracts(w3, ERC20_ABI, settings.glm_contract_address)  # type: ignore[arg-type]
+
+
+async def get_glm_balance_of_deposits(
+    glm_contracts: GetGLMContracts,
+    deposits_contracts: GetDepositsContracts,
+) -> int:
+    return await glm_contracts.balance_of(deposits_contracts.address)
+
+
+GetGLMContracts = Annotated[GLMContracts, Depends(get_glm_contracts)]
+GetGLMBalanceOfDeposits = Annotated[int, Depends(get_glm_balance_of_deposits)]
\ No newline at end of file
diff --git a/backend/v2/project_rewards/dependencies.py b/backend/v2/project_rewards/dependencies.py
index fad325071e..98cbe24200 100644
--- a/backend/v2/project_rewards/dependencies.py
+++ b/backend/v2/project_rewards/dependencies.py
@@ -1,7 +1,8 @@
 from typing import Annotated
 
 from fastapi import Depends
-from v2.core.dependencies import GetSession
+from v2.sablier.subgraphs import SablierSubgraph
+from v2.core.dependencies import GetSession, OctantSettings
 from v2.epochs.dependencies import GetOpenAllocationWindowEpochNumber
 from v2.matched_rewards.dependencies import GetMatchedRewardsEstimator
 from v2.project_rewards.services import ProjectRewardsEstimator
@@ -21,7 +22,6 @@ async def get_project_rewards_estimator(
         epoch_number=epoch_number,
     )
 
-
 GetProjectRewardsEstimator = Annotated[
     ProjectRewardsEstimator,
     Depends(get_project_rewards_estimator),
diff --git a/backend/v2/project_rewards/router.py b/backend/v2/project_rewards/router.py
index 48f1a258b1..7379959983 100644
--- a/backend/v2/project_rewards/router.py
+++ b/backend/v2/project_rewards/router.py
@@ -1,6 +1,23 @@
-from fastapi import APIRouter
+from datetime import datetime, time, timezone
+import os
+from fastapi import APIRouter, Response, status
+import json
 from app.context.epoch_state import EpochState
 from app.exceptions import NotImplementedForGivenEpochState
+from app.constants import SABLIER_SENDER_ADDRESS_SEPOLIA, SABLIER_TOKEN_ADDRESS_SEPOLIA, ZERO_ADDRESS
+from app.infrastructure import SubgraphEndpoints
+from app.engine.user.budget import UserBudgetPayload
+from app.engine.user.budget.with_ppf import UserBudgetWithPPF
+from app.engine.octant_rewards import OctantRewardsSettings
+from app.modules.dto import OctantRewardsDTO, PendingSnapshotDTO
+from app.modules.octant_rewards.core import calculate_rewards
+from app.modules.snapshots.pending.core import calculate_user_budgets
+from app.modules.staking.proceeds.core import estimate_staking_proceeds
+from v2.deposits.dependencies import GetDepositEventsRepository
+from v2.sablier.dependencies import GetSablierSubgraph
+from v2.glms.dependencies import GetGLMBalanceOfDeposits, GetGLMContracts
+from v2.sablier.subgraphs import SablierSubgraph
+from v2.project_rewards.user_events import calculate_effective_deposits, calculate_pending_epoch_snapshot, simulate_user_events
 from v2.allocations.repositories import (
     get_donors_for_epoch,
     sum_allocations_by_epoch,
@@ -49,9 +66,11 @@ async def get_user_budget_for_epoch_v1(
     session: GetSession,
     user_address: Address,
     epoch_number: int,
-) -> UserBudgetResponseV1:
+    response: Response,
+) -> UserBudgetResponseV1 | None:
     """
-    Returns user's rewards budget available to allocate for given epoch
+    Returns user's rewards budget available to allocate for given epoch.
+    Returns 204 No Content if user does not have budget for given epoch.
     """
     budget = await get_budget_by_user_address_and_epoch(
         session,
@@ -59,23 +78,134 @@ async def get_user_budget_for_epoch_v1(
         epoch_number,
     )
 
-    # Q: Should we return 0 or raise exception when user has no budget for the epoch?
+    if not budget:
+        response.status_code = status.HTTP_204_NO_CONTENT
+        return None
 
-    return UserBudgetResponseV1(budget=budget or 0)
+    return UserBudgetResponseV1(budget=budget)
 
 
 @api.get("/budget/{user_address}/upcoming")
 async def get_user_budget_for_upcoming_epoch_v1(
-
+    session: GetSession,
+    epoch_contracts: GetEpochsContracts,
+    epoch_subgraph: GetEpochsSubgraph,
+    deposit_events: GetDepositEventsRepository,
     user_address: Address,
 ) -> UpcomingUserBudgetResponseV1:
     """
     Returns the upcoming user budget based on if allocation happened now.
     """
+
+    epoch_number = await epoch_contracts.get_current_epoch()
+    epoch_details = await epoch_subgraph.fetch_epoch_by_number(epoch_number)
+    
+    # We SIMULATE the epoch end as if it ended now
+    epoch_end = int(datetime.now(timezone.utc).timestamp())
+    epoch_start = epoch_details.fromTs
+
+    # pending_snapshot = await calculate_pending_epoch_snapshot(
+    #     deposit_events,
+    #     epoch_number, 
+    #     epoch_start, 
+    #     epoch_end
+    # )
+# async def calculate_pending_epoch_snapshot(
+#     deposit_events: DepositEventsRepository,
+#     epoch_number: int,
+#     epoch_start: int,
+#     epoch_end: int,
+# ) -> PendingSnapshotDTO:
+    
+    # Get octant rewards
+        # epoch_details = await epochs_subgraph.fetch_epoch_by_number(epoch_number)
+    # duration_sec = epoch_details.duration
+    # return estimate_staking_proceeds(duration_sec)
+    # eth_proceeds = await get_staking_proceeds(session, epoch_number, start_sec, end_sec)
+    eth_proceeds = estimate_staking_proceeds(epoch_end - epoch_start)
+
+    events = await deposit_events.get_all_users_events(
+        epoch_number,
+        epoch_start,
+        epoch_end
+    )
+    user_deposits, total_effective_deposit = calculate_effective_deposits(epoch_start, epoch_end, events)
+
+    # total_effective_deposit = 155654569757136462439580980
+    rewards_settings = OctantRewardsSettings()
+    octant_rewards = calculate_rewards(
+        rewards_settings, eth_proceeds, total_effective_deposit
+    )
+    rewards = OctantRewardsDTO(
+        staking_proceeds=eth_proceeds,
+        locked_ratio=octant_rewards.locked_ratio,
+        total_effective_deposit=total_effective_deposit,
+        total_rewards=octant_rewards.total_rewards,
+        vanilla_individual_rewards=octant_rewards.vanilla_individual_rewards,
+        operational_cost=octant_rewards.operational_cost,
+        ppf=octant_rewards.ppf_value,
+        community_fund=octant_rewards.community_fund,
+    )
+
+    # events = await get_all_user_events(
+    #     session,
+    #     epochs_subgraph,
+    #     sablier,
+    #     epoch_number,
+    #     epoch_start,
+    #     epoch_end
+    # )
+    # user_deposits, total_effective_deposit = calculate_effective_deposits(epoch_start, epoch_end, events)
+
+    user_budget_calculator = UserBudgetWithPPF()
+    user_budgets = calculate_user_budgets(
+        user_budget_calculator, rewards, user_deposits
+    )
+    # pending_snapshot = PendingSnapshotDTO(
+    #     rewards=rewards, user_deposits=user_deposits, user_budgets=user_budgets
+    # )
+    # return PendingSnapshotDTO(
+    #     rewards=rewards, user_deposits=user_deposits, user_budgets=user_budgets
+    # )
+
+
+# Getting unlocks in timestamp range 1728835200 - 1735308472
+    # upcoming_budget = pending_snapshot.user_budgets.get(user_address)
+
+    # print("Pending snapshot", pending_snapshot)
+
+    user_budget = next(
+        (budget.budget for budget in user_budgets if budget.user_address == user_address),
+        0  # Default value if user not found
+    )
+
+    # if not user_budget:
+    #     return UpcomingUserBudgetResponseV1(upcoming_budget=0)
+    
+    return UpcomingUserBudgetResponseV1(upcoming_budget=user_budget)
+    print("I'm here")
+
     # TODO we need to handle snapshots here unfortunatelly
-    upcoming_budget = await get_upcoming_user_budget(user_address)
+    # upcoming_budget = await get_upcoming_user_budget(user_address)
+
+
 
     # context = state_context(EpochState.SIMULATED, with_block_range=True)
+    # service: UpcomingUserBudgets = get_services(EpochState.CURRENT).user_budgets_service
+    # return service.get_budget(context, user_address)
+
+    # def get_budget(self, context: Context, user_address: str) -> int:
+        # simulated_snapshot = (
+        #     self.simulated_pending_snapshot_service.simulate_pending_epoch_snapshot(
+        #         context
+        #     )
+        # )
+        # upcoming_budget = core.get_upcoming_budget(
+        #     user_address, simulated_snapshot.user_budgets
+        # )
+
+        # return upcoming_budget
+
 
     # user_deposits = CalculatedUserDeposits(
     #         events_generator=DbAndGraphEventsGenerator()
@@ -207,92 +337,228 @@ async def get_epoch_budgets_v1(
 
 @api.post("/estimated_budget")
 async def get_estimated_budget_v1(
+    session: GetSession,
+    epoch_contracts: GetEpochsContracts,
+    glm_balance: GetGLMBalanceOfDeposits,
     request: EstimatedBudgetByEpochRequestV1,
 ) -> UserBudgetWithMatchedFundingResponseV1:
-    # leverage = octant_rewards_controller.get_last_finalized_epoch_leverage()
-    #     context = state_context(EpochState.FINALIZED)
-    #     service = get_services(context.epoch_state).octant_rewards_service
-
-    #     return service.get_leverage(context)
-    #         allocations_sum = database.allocations.get_alloc_sum_by_epoch(
-    #             context.epoch_details.epoch_num
-    #         )
-    #         finalized_snapshot = database.finalized_epoch_snapshot.get_by_epoch(
-    #             context.epoch_details.epoch_num
-    #         )
-    #         matched_rewards = int(finalized_snapshot.matched_rewards)
-
-    #         return context.epoch_settings.project.rewards.leverage.calculate_leverage(
-    #             matched_rewards, allocations_sum
-    #         )
-            # Leverage:
-            # return matched_rewards / total_allocated if total_allocated else 0
-
-    # epochs_budget = budget_controller.estimate_epochs_budget(no_epochs, glm_amount)
-    #     validate_estimate_budget_by_epochs_inputs(no_epochs, glm_amount)
-
-    #     future_context = state_context(EpochState.FUTURE)
-    #     future_rewards_service = get_services(EpochState.FUTURE).octant_rewards_service
-    #     future_rewards = future_rewards_service.get_octant_rewards(future_context)
-
-        # def _get_future_epoch_details(epoch_num: int) -> EpochDetails:
-        #     epoch_details = epochs.get_future_epoch_props()
-        #     start = epoch_details[2]
-        #     duration = epoch_details[3]
-        #     decision_window = epoch_details[4]
-        #     return EpochDetails(
-        #         epoch_num=epoch_num,
-        #         start=start,
-        #         duration=duration,
-        #         decision_window=decision_window,
-        #         remaining_sec=duration,
-        #     )
+    
+    # leverage
+    epoch_number = await epoch_contracts.get_finalized_epoch()
+    
+    # Calculate leverage based on the last finalized epoch
+    allocations_sum = await sum_allocations_by_epoch(session, epoch_number)
+    finalized_snapshot = await get_finalized_epoch_snapshot(session, epoch_number)
+    matched_rewards = int(finalized_snapshot.matched_rewards)
+
+    leverage = matched_rewards / allocations_sum if allocations_sum else 0
+
+    future_epoch = await epoch_contracts.get_future_epoch_props()
+    print("Future epoch", future_epoch)
+    # start_sec = future_epoch[2]
+    start_sec = future_epoch[2]
+    # duration = future_epoch[3]
+    duration = future_epoch[3]
+    end_sec = start_sec + duration
+    # decision_window = future_epoch[4]
+    decision_window = future_epoch[4]
+
+    # Estimate staking proceeds
+    eth_proceeds = estimate_staking_proceeds(duration)
+    total_effective_deposit = glm_balance
+
+    # total_effective_deposit = 155654569757136462439580980
+    rewards_settings = OctantRewardsSettings()
+    octant_rewards = calculate_rewards(
+        rewards_settings, eth_proceeds, total_effective_deposit
+    )
+    future_rewards = OctantRewardsDTO(
+        staking_proceeds=eth_proceeds,
+        locked_ratio=octant_rewards.locked_ratio,
+        total_effective_deposit=total_effective_deposit,
+        total_rewards=octant_rewards.total_rewards,
+        vanilla_individual_rewards=octant_rewards.vanilla_individual_rewards,
+        operational_cost=octant_rewards.operational_cost,
+        ppf=octant_rewards.ppf_value,
+        community_fund=octant_rewards.community_fund,
+    )
 
-        # return FutureServices(
-        #     octant_rewards_service=CalculatedOctantRewards(
-        #         staking_proceeds=EstimatedStakingProceeds(),
-        #         effective_deposits=ContractBalanceUserDeposits(),
-        #     ),
-        #     projects_metadata_service=StaticProjectsMetadataService(),
-        #     projects_details_service=StaticProjectsDetailsService(),
-        # )
 
-        # future_rewards = await get_octant_rewards(session, epoch_number, start_sec, end_sec)
+    # Simulate user events as if they deposited given amount of GLMs
+    events = {
+        ZERO_ADDRESS: simulate_user_events(
+            end_sec,
+            duration,
+            duration,
+            request.glm_amount
+        )
+    }
+    user_deposits, total_effective_deposit = calculate_effective_deposits(start_sec, end_sec, events)
+
+    # print("total_effective_deposit", total_effective_deposit)
+    # print("User deposits", user_deposits)
+    effective_deposit = (
+        user_deposits[0].effective_deposit if user_deposits else 0
+    )
+
+    # print("Effective deposit", effective_deposit)
+    # print("Total effective deposit", future_rewards.total_effective_deposit)
+    # print("Vanilla individual rewards", future_rewards.vanilla_individual_rewards)
+    # print("PPF", future_rewards.ppf)
+
+    budget_calculator = UserBudgetWithPPF()
+    epoch_budget = budget_calculator.calculate_budget(
+        UserBudgetPayload(
+            user_effective_deposit=effective_deposit,
+            total_effective_deposit=future_rewards.total_effective_deposit,
+            vanilla_individual_rewards=future_rewards.vanilla_individual_rewards,
+            ppf=future_rewards.ppf,
+        )
+    )
+    epochs_budget = request.number_of_epochs * epoch_budget
 
+    matching_fund = epochs_budget * leverage
 
-    #     epoch_duration = future_context.epoch_details.duration_sec
 
-    #     return no_epochs * core.estimate_epoch_budget(
-    #         future_context, future_rewards, epoch_duration, glm_amount
-    #     )
+    return UserBudgetWithMatchedFundingResponseV1(
+        budget=epochs_budget,
+        matched_funding=matching_fund
+    )
 
-    matching_fund = budget_controller.get_matching_fund(epochs_budget, leverage)
-    # def get_matching_fund(budget: int, leverage: float) -> int:
-    # return core.calculate_matching_fund(budget, leverage)
-    # def calculate_matching_fund(budget: int, leverage: float) -> int:
-    # return int(budget * leverage)
 
-    return EstimatedRewardsDTO(
-        estimated_budget=epochs_budget, leverage=leverage, matching_fund=matching_fund
+def estimate_epoch_budget(
+    start_sec: int, 
+    end_sec: int,
+    remaining_sec: int,
+    lock_duration: int,
+    glm_amount: int, 
+    rewards: OctantRewardsDTO
+) -> int:
+    events = {
+        ZERO_ADDRESS: simulate_user_events(
+            end_sec,
+            lock_duration,
+            remaining_sec,
+            glm_amount
+        )
+    }
+    user_deposits, total_effective_deposit = calculate_effective_deposits(start_sec, end_sec, events)
+
+    # print("total_effective_deposit", total_effective_deposit)
+    # print("User deposits", user_deposits)
+    effective_deposit = (
+        user_deposits[0].effective_deposit if user_deposits else 0
+    )
+
+    # print("Effective deposit", effective_deposit)
+    # print("Total effective deposit", future_rewards.total_effective_deposit)
+    # print("Vanilla individual rewards", future_rewards.vanilla_individual_rewards)
+    # print("PPF", future_rewards.ppf)
+
+    budget_calculator = UserBudgetWithPPF()
+    return budget_calculator.calculate_budget(
+        UserBudgetPayload(
+            user_effective_deposit=effective_deposit,
+            total_effective_deposit=rewards.total_effective_deposit,
+            vanilla_individual_rewards=rewards.vanilla_individual_rewards,
+            ppf=rewards.ppf,
+        )
     )
 
 
 @api.post("/estimated_budget/by_days")
 async def get_estimated_budget_by_days_v1(
+    epoch_contracts: GetEpochsContracts,
+    epoch_subgraph: GetEpochsSubgraph,
+    deposit_events: GetDepositEventsRepository,
+    glm_balance: GetGLMBalanceOfDeposits,
     request: EstimatedBudgetByDaysRequestV1,
 ) -> UserBudgetResponseV1:
-    validate_estimate_budget_inputs(days, glm_amount)
+    # validate_estimate_budget_inputs(days, glm_amount)
+
+    lock_duration_sec = request.days * 86400 # 24hours * 60minutes * 60seconds
+
+    current_epoch_number = await epoch_contracts.get_current_epoch()
+    current_epoch_details = await epoch_subgraph.fetch_epoch_by_number(current_epoch_number)
+    current_epoch_start = current_epoch_details.fromTs
+    current_epoch_duration = current_epoch_details.duration
+    current_epoch_remaining = current_epoch_start + current_epoch_duration - int(datetime.now().timestamp())
+
+    # CURRENT EPOCH REWARDS
+    current_eth_proceeds = estimate_staking_proceeds(current_epoch_duration)
+    current_events = await deposit_events.get_all_users_events(
+        current_epoch_number,
+        current_epoch_start,
+        current_epoch_start + current_epoch_duration
+    )
+    user_deposits, total_effective_deposit = calculate_effective_deposits(current_epoch_start, current_epoch_start + current_epoch_duration, current_events)
 
-    lock_duration_sec = days_to_sec(days)
-    return estimate_budget(lock_duration_sec, glm_amount)
+    current_rewards_settings = OctantRewardsSettings()
+    current_octant_rewards = calculate_rewards(
+        current_rewards_settings, current_eth_proceeds, total_effective_deposit
+    )
+    current_rewards = OctantRewardsDTO(
+        staking_proceeds=current_eth_proceeds,
+        locked_ratio=current_octant_rewards.locked_ratio,
+        total_effective_deposit=total_effective_deposit,
+        total_rewards=current_octant_rewards.total_rewards,
+        vanilla_individual_rewards=current_octant_rewards.vanilla_individual_rewards,
+        operational_cost=current_octant_rewards.operational_cost,
+        ppf=current_octant_rewards.ppf_value,
+        community_fund=current_octant_rewards.community_fund,
+    )
+
+
+    # return estimate_budget(lock_duration_sec, glm_amount)
     # current_context = state_context(EpochState.CURRENT)
     # current_rewards_service = get_services(EpochState.CURRENT).octant_rewards_service
+        # CURRENT
+        # is_mainnet = compare_blockchain_types(chain_id, ChainTypes.MAINNET)
+            # octant_rewards = CalculatedOctantRewards(
+            #     staking_proceeds=EstimatedStakingProceeds(),
+            #     effective_deposits=CalculatedUserDeposits(
+            #     events_generator=DbAndGraphEventsGenerator()
+            # )
+        # )
+
+
     # current_rewards = current_rewards_service.get_octant_rewards(current_context)
 
+    # FUTURE EPOCH REWARDS
+    future_epoch_number = await epoch_contracts.get_future_epoch_props()
+    future_epoch_start = future_epoch_number[2]
+    future_epoch_duration = future_epoch_number[3]
+    future_epoch_end = future_epoch_start + future_epoch_duration
+    future_eth_proceeds = estimate_staking_proceeds(future_epoch_duration)
+
+    future_total_effective_deposit = glm_balance
+
+    future_rewards_settings = OctantRewardsSettings()
+    future_octant_rewards = calculate_rewards(
+        future_rewards_settings, future_eth_proceeds, future_total_effective_deposit
+    )
+    future_rewards = OctantRewardsDTO(
+        staking_proceeds=future_eth_proceeds,
+        locked_ratio=future_octant_rewards.locked_ratio,
+        total_effective_deposit=future_total_effective_deposit,
+        total_rewards=future_octant_rewards.total_rewards,
+        vanilla_individual_rewards=future_octant_rewards.vanilla_individual_rewards,
+        operational_cost=future_octant_rewards.operational_cost,
+        ppf=future_octant_rewards.ppf_value,
+        community_fund=future_octant_rewards.community_fund,
+    )
+
     # future_context = state_context(EpochState.FUTURE)
     # future_rewards_service = get_services(EpochState.FUTURE).octant_rewards_service
+        # octant_rewards_service=CalculatedOctantRewards(
+        #     staking_proceeds=EstimatedStakingProceeds(),
+        #     effective_deposits=ContractBalanceUserDeposits(),
+        # ),
+
     # future_rewards = future_rewards_service.get_octant_rewards(future_context)
 
+
+
     # return core.estimate_budget(
     #     current_context,
     #     future_context,
@@ -302,6 +568,46 @@ async def get_estimated_budget_by_days_v1(
     #     glm_amount,
     # )
 
+    remaining_lock_duration = lock_duration_sec
+
+    budget = estimate_epoch_budget(
+        current_epoch_start,
+        current_epoch_start + current_epoch_duration,
+        current_epoch_remaining,
+        remaining_lock_duration,
+        request.glm_amount,
+        current_rewards,
+    )
+    remaining_lock_duration -= current_epoch_remaining
+
+    if remaining_lock_duration > 0:
+        full_epochs_num, remaining_future_epoch_sec = divmod(
+            remaining_lock_duration, future_epoch_duration
+        )
+        budget += full_epochs_num * estimate_epoch_budget(
+            future_epoch_start,
+            future_epoch_end,
+            future_epoch_duration,
+            future_epoch_duration,
+            request.glm_amount,
+            future_rewards,
+        )
+        remaining_lock_duration = remaining_future_epoch_sec
+
+    if remaining_lock_duration > 0:
+        budget += estimate_epoch_budget(
+            future_epoch_start,
+            future_epoch_end,
+            future_epoch_duration,
+            remaining_lock_duration,
+            request.glm_amount,
+            future_rewards,
+        )
+
+    return UserBudgetResponseV1(
+        budget=budget
+    )
+
 
 @api.get("/leverage/{epoch_number}")
 async def get_rewards_leverage_v1(
@@ -309,8 +615,16 @@ async def get_rewards_leverage_v1(
     epochs_contracts: GetEpochsContracts,
     epoch_number: int,
 ) -> RewardsLeverageResponseV1:
+    """
+    Returns leverage for a given epoch.
+
+    For finalized epochs it returns the leverage based on the data from the finalized snapshot.
+
+    """
+
     epoch_state = await get_epoch_state(session, epochs_contracts, epoch_number)
 
+    print("Epoch state", epoch_state)
     if epoch_state > EpochState.PENDING:
         raise NotImplementedForGivenEpochState()
 
@@ -376,7 +690,7 @@ async def get_estimated_project_rewards_v1(
     """
     Returns foreach project current allocation sum and estimated matched rewards.
 
-    This endpoint is available only for the pending epoch state.
+    This endpoint is available only while allocation window is open.
     """
 
     estimated_funding = await project_rewards_estimator.get()
@@ -397,6 +711,7 @@ async def get_rewards_for_projects_in_epoch_v1(
 
     """
 
+    # For epoch projects retrieve their rewards
     projects = await projects_contracts.get_project_addresses(epoch_number)
     rewards = await get_rewards_for_projects_in_epoch(session, epoch_number, projects)
 
@@ -429,62 +744,26 @@ async def get_unused_rewards_v1(
     epoch_number: int,
 ) -> UnusedRewardsResponseV1:
     """
-    Returns unallocated value and the number of users who didn't use their rewards in an epoch
-    """
+    Returns list of users who didn't use their rewards in an epoch and the total sum of all unused rewards.
 
-    ts = await epoch_subgraph.get_epoch_by_number(epoch_number)
+    This is based on available budgets and excluding donors (users who have allocated funds) and patrons.
+    """
 
+    # All users budgets, donors and patrons for the epoch
     budgets = await get_all_users_budgets_by_epoch(session, epoch_number)
-
-    # budgets = self.user_budgets.get_all_budgets(context)
-    #     budgets = Budget.query.filter_by(epoch=epoch).all()
-    # return {budget.user.address: int(budget.budget) for budget in budgets}
-
     donors = await get_donors_for_epoch(session, epoch_number)
-    # donors = self.allocations.get_all_donors_addresses(context)
-    # users = User.query.filter(
-    #     User.allocations.any(epoch=epoch_num, deleted_at=None)
-    # ).all()
-    # return [u.address for u in users]
-
+    epoch_details = await epoch_subgraph.get_epoch_by_number(epoch_number)
     patrons = await get_all_patrons_at_timestamp(
-        session, ts.finalized_timestamp.datetime()
+        session, epoch_details.finalized_timestamp.datetime()
     )
 
-    excluded_addresses = set(donors + patrons)
-
-    unused_budgets = {budget for budget in budgets if budget not in excluded_addresses}
+    # Exclude donors and patrons from the list of users who didn't use their rewards
+    unused_budgets = {
+        address: budget for address, budget in budgets.items() 
+        if address not in set(donors + patrons)
+    }
 
     return UnusedRewardsResponseV1(
-        addresses=list(unused_budgets.keys()), value=sum(unused_budgets.values())
+        addresses=sorted(list(unused_budgets.keys())), 
+        value=sum(unused_budgets.values()),
     )
-
-    # patrons = self.patrons_mode.get_all_patrons_addresses(context)
-    # patrons = self._get_patron_budgets(context.epoch_details, with_budget)
-    #     ts = epoch.finalized_timestamp
-    # patrons = database.patrons.get_all_patrons_at_timestamp(ts.datetime())
-
-    # if with_budget:
-    #     all_budgets = database.budgets.get_all_by_epoch(epoch.epoch_num)
-    #     return {
-    #         patron: all_budgets[patron]
-    #         for patron in patrons
-    #         if patron in all_budgets.keys()
-    #     }
-    # else:
-    #     return {patron: 0 for patron in patrons}
-
-    # return list(patrons.keys())
-
-    # return get_unused_rewards(budgets, donors, patrons)
-
-
-# def get_unused_rewards(
-#     budgets: Dict[str, int], donors: List[str], patrons: List[str]
-# ) -> Dict[str, int]:
-#     for donor in donors:
-#         budgets.pop(donor)
-#     for patron in patrons:
-#         budgets.pop(patron)
-
-#     return budgets
diff --git a/backend/v2/project_rewards/user_events.py b/backend/v2/project_rewards/user_events.py
index 08b8a9bf51..c44bbf77f7 100644
--- a/backend/v2/project_rewards/user_events.py
+++ b/backend/v2/project_rewards/user_events.py
@@ -2,6 +2,7 @@
 
 from itertools import groupby
 from operator import attrgetter
+import os
 from sqlalchemy import select
 from sqlalchemy.ext.asyncio import AsyncSession
 from sqlalchemy.orm import selectinload
@@ -20,89 +21,17 @@
 from app.modules.snapshots.pending.core import calculate_user_budgets
 from app.modules.user.budgets.core import get_upcoming_budget
 from app.modules.user.events_generator.core import unify_deposit_balances
+from app.modules.staking.proceeds.core import estimate_staking_proceeds
+from app.constants import SABLIER_SENDER_ADDRESS_SEPOLIA, SABLIER_TOKEN_ADDRESS_SEPOLIA, ZERO_ADDRESS
+from app.infrastructure import SubgraphEndpoints
+from v2.deposits.repositories import DepositEventsRepository
+from v2.epochs.subgraphs import EpochsSubgraph
+from v2.sablier.subgraphs import SablierSubgraph
 
 
-async def get_all_deposit_events_for_epoch(
-    session: AsyncSession,
-    epoch_number: int,
-) -> dict[str, list[DepositEvent]]:
-    
-    results = await session.scalars(
-        select(Deposit)
-        .options(selectinload(Deposit.user))
-        .where(Deposit.epoch == epoch_number)
-    )
 
-    return {
-        result.user.address: result for result in results
-    }
 
 
-async def get_all_user_events(
-        session: AsyncSession,
-        epoch_number: int,
-        start_sec: int,
-        end_sec: int,
-) -> dict[str, list[DepositEvent]]:
-    
-    # Get all locked amounts for the previous epoch
-    epoch_start_locked_amounts = await get_all_deposit_events_for_epoch(
-        session, epoch_number - 1
-    )
-    epoch_start_events = [
-        DepositEvent(
-            user=user,
-            type=EventType.LOCK,
-            timestamp=start_sec,
-            amount=0,  # it is not a deposit in fact
-            deposit_before=int(deposit.epoch_end_deposit),
-        )
-        for user, deposit in epoch_start_locked_amounts.items()
-    ]
-
-    sablier = SablierEventsGenerator()
-    sablier_streams = await sablier.get_all_streams_history()
-    mapped_streams = process_to_locks_and_unlocks(
-        sablier_streams, from_timestamp=start_sec, to_timestamp=end_sec
-    )
-    epoch_events = []
-    epoch_events += flatten_sablier_events(mapped_streams, FlattenStrategy.ALL)
-    epoch_events += get_locks_by_timestamp_range(start_sec, end_sec)
-    epoch_events += get_unlocks_by_timestamp_range(start_sec, end_sec)
-
-    epoch_events = [DepositEvent.from_dict(event) for event in epoch_events]
-    sorted_events = sorted(epoch_events, key=attrgetter("user", "timestamp"))
-
-    user_events = {
-        k: list(g) for k, g in groupby(sorted_events, key=attrgetter("user"))
-    }
-
-    for event in epoch_start_events:
-        if event.user in user_events:
-            user_events[event.user].insert(0, event)
-        else:
-            user_events[event.user] = [event]
-
-    epoch_start_users = list(map(attrgetter("user"), epoch_start_events))
-    for user_address in user_events:
-        if user_address not in epoch_start_users:
-            user_events[user_address].insert(
-                0,
-                DepositEvent(
-                    user_address,
-                    EventType.LOCK,
-                    timestamp=start_sec,
-                    amount=0,
-                    deposit_before=0,
-                ),
-            )
-
-        user_events[user_address] = unify_deposit_balances(
-            user_events[user_address]
-        )
-
-    return user_events
-
 
 def calculate_effective_deposits(
     start_sec: int,
@@ -110,6 +39,7 @@ def calculate_effective_deposits(
     events: Dict[str, list[DepositEvent]],
 ) -> tuple[list[UserDeposit], int]:
     
+    # TODO: We can do this better and nicer
     effective_deposit_calculator = DefaultWeightedAverageWithSablierTimebox()
     payload = UserEffectiveDepositPayload(
         epoch_start=start_sec,
@@ -120,72 +50,62 @@ def calculate_effective_deposits(
     return effective_deposit_calculator.calculate_users_effective_deposits(payload)
 
 
-async def get_octant_rewards(
-    session: AsyncSession,
+async def calculate_pending_epoch_snapshot(
+    deposit_events: DepositEventsRepository,
     epoch_number: int,
-    start_sec: int,
-    end_sec: int,
-) -> OctantRewardsDTO:
+    epoch_start: int,
+    epoch_end: int,
+) -> PendingSnapshotDTO:
     
-    # epoch_details = await epochs_subgraph.fetch_epoch_by_number(epoch_number)
+    # Get octant rewards
+        # epoch_details = await epochs_subgraph.fetch_epoch_by_number(epoch_number)
     # duration_sec = epoch_details.duration
     # return estimate_staking_proceeds(duration_sec)
-    eth_proceeds = await get_staking_proceeds(session, epoch_number, start_sec, end_sec)
+    # eth_proceeds = await get_staking_proceeds(session, epoch_number, start_sec, end_sec)
+    eth_proceeds = estimate_staking_proceeds(epoch_end - epoch_start)
 
-    events = await get_all_user_events(session, epoch_number, start_sec, end_sec)
-    user_deposits, total_effective_deposit = calculate_effective_deposits(start_sec, end_sec, events)
+    events = await deposit_events.get_all_users_events(
+        epoch_number,
+        epoch_start,
+        epoch_end
+    )
+    user_deposits, total_effective_deposit = calculate_effective_deposits(epoch_start, epoch_end, events)
 
+    # total_effective_deposit = 155654569757136462439580980
     rewards_settings = OctantRewardsSettings()
 
     octant_rewards = calculate_rewards(
         rewards_settings, eth_proceeds, total_effective_deposit
     )
 
-    (
-        locked_ratio,
-        total_rewards,
-        vanilla_individual_rewards,
-        op_cost,
-        ppf,
-        community_fund,
-    ) = (
-        octant_rewards.locked_ratio,
-        octant_rewards.total_rewards,
-        octant_rewards.vanilla_individual_rewards,
-        octant_rewards.operational_cost,
-        octant_rewards.ppf_value,
-        octant_rewards.community_fund,
-    )
-
-    return OctantRewardsDTO(
+    rewards = OctantRewardsDTO(
         staking_proceeds=eth_proceeds,
-        locked_ratio=locked_ratio,
+        locked_ratio=octant_rewards.locked_ratio,
         total_effective_deposit=total_effective_deposit,
-        total_rewards=total_rewards,
-        vanilla_individual_rewards=vanilla_individual_rewards,
-        operational_cost=op_cost,
-        ppf=ppf,
-        community_fund=community_fund,
+        total_rewards=octant_rewards.total_rewards,
+        vanilla_individual_rewards=octant_rewards.vanilla_individual_rewards,
+        operational_cost=octant_rewards.operational_cost,
+        ppf=octant_rewards.ppf_value,
+        community_fund=octant_rewards.community_fund,
     )
 
-
-async def calculate_pending_epoch_snapshot(
-    session: AsyncSession,
-    epoch_number: int
-) -> PendingSnapshotDTO:
-    
-    octant_rewards = await get_octant_rewards(session, epoch_number, start_sec, end_sec)
-
-    events = await get_all_user_events(session, epoch_number, start_sec, end_sec)
-    user_deposits, total_effective_deposit = calculate_effective_deposits(start_sec, end_sec, events)
+    # events = await get_all_user_events(
+    #     session,
+    #     epochs_subgraph,
+    #     sablier,
+    #     epoch_number,
+    #     epoch_start,
+    #     epoch_end
+    # )
+    # user_deposits, total_effective_deposit = calculate_effective_deposits(epoch_start, epoch_end, events)
 
     user_budget_calculator = UserBudgetWithPPF()
     user_budgets = calculate_user_budgets(
-        user_budget_calculator, octant_rewards, user_deposits
+        user_budget_calculator, rewards, user_deposits
     )
 
     return PendingSnapshotDTO(
-        rewards=octant_rewards, user_deposits=user_deposits, user_budgets=user_budgets
+        rewards=rewards, user_deposits=user_deposits, user_budgets=user_budgets
     )
 
 
@@ -201,3 +121,32 @@ async def get_budget(
     )
 
     return upcoming_budget
+
+
+def simulate_user_events(
+    end_sec: int,
+    lock_duration: int,
+    remaining_sec: int,
+    glm_amount: int
+) -> list[DepositEvent]:
+
+    user_events = [
+        DepositEvent(
+            user=ZERO_ADDRESS,
+            type=EventType.LOCK,
+            timestamp=end_sec - remaining_sec,
+            amount=glm_amount,
+            deposit_before=0,
+        )
+    ]
+    if lock_duration < remaining_sec:
+        user_events.append(
+            DepositEvent(
+                user=ZERO_ADDRESS,
+                type=EventType.UNLOCK,
+                timestamp=end_sec - remaining_sec + lock_duration,
+                amount=glm_amount,
+                deposit_before=glm_amount,
+            )
+        )
+    return user_events
\ No newline at end of file
diff --git a/backend/v2/sablier/__init__.py b/backend/v2/sablier/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/backend/v2/sablier/dependencies.py b/backend/v2/sablier/dependencies.py
new file mode 100644
index 0000000000..0f9ffb4fec
--- /dev/null
+++ b/backend/v2/sablier/dependencies.py
@@ -0,0 +1,54 @@
+from functools import lru_cache
+from typing import Annotated
+
+from fastapi import Depends
+from pydantic import Field
+from pydantic_settings import SettingsConfigDict
+from app.shared.blockchain_types import ChainTypes
+from v2.sablier.subgraphs import SablierSubgraph
+from v2.core.dependencies import GetChainSettings, GetSession, OctantSettings
+from v2.epochs.dependencies import GetOpenAllocationWindowEpochNumber
+from v2.matched_rewards.dependencies import GetMatchedRewardsEstimator
+from v2.project_rewards.services import ProjectRewardsEstimator
+from v2.projects.dependencies import GetProjectsContracts
+
+
+class SablierSubgraphSettings(OctantSettings):
+    # For mainnet
+    sablier_mainnet_subgraph_url: str
+    sablier_token_address: str = Field(
+        default="0x7DD9c5Cba05E151C895FDe1CF355C9A1D5DA6429",
+        alias="GLM_TOKEN_ADDRESS",
+    )
+    sablier_sender_address: str = ""
+    # For sepolia
+    sablier_sepolia_subgraph_url: str
+    sablier_token_address_sepolia: str = "0x71432dd1ae7db41706ee6a22148446087bdd0906"
+    sablier_sender_address_sepolia: str = "0xf86fD85672683c220709B9ED80bAD7a51800206a"
+
+
+@lru_cache(maxsize=1)
+def get_sablier_subgraph(chain_settings: GetChainSettings) -> SablierSubgraph:
+    """
+    Based on the chain type (mainnet or sepolia), return the appropriate SablierSubgraph.
+    """
+
+    sablier_settings = SablierSubgraphSettings()
+
+    if chain_settings.chain_id == ChainTypes.MAINNET:
+        return SablierSubgraph(
+            sablier_settings.sablier_mainnet_subgraph_url,
+            sablier_settings.sablier_sender_address,
+            sablier_settings.sablier_token_address,
+        )
+
+    return SablierSubgraph(
+        sablier_settings.sablier_sepolia_subgraph_url,
+        sablier_settings.sablier_sender_address_sepolia,
+        sablier_settings.sablier_token_address_sepolia,
+    )
+
+GetSablierSubgraph = Annotated[
+    SablierSubgraph,
+    Depends(get_sablier_subgraph),
+]
diff --git a/backend/v2/project_rewards/sablier.py b/backend/v2/sablier/subgraphs.py
similarity index 79%
rename from backend/v2/project_rewards/sablier.py
rename to backend/v2/sablier/subgraphs.py
index 74f0230a95..fa2e0357fe 100644
--- a/backend/v2/project_rewards/sablier.py
+++ b/backend/v2/sablier/subgraphs.py
@@ -1,15 +1,19 @@
-
-
-
+import logging
+import os
+from flask import json
 from gql import Client, gql
 from gql.transport.aiohttp import AIOHTTPTransport
+from gql.client import log as requests_logger
 
 
 import backoff
 from app.infrastructure.sablier.events import SablierStream
 from v2.epochs.subgraphs import BackoffParams
 
-class SablierEventsGenerator:
+from gql.transport.aiohttp import log as requests_logger
+requests_logger.setLevel(logging.WARNING)
+
+class SablierSubgraph:
 
     def __init__(
         self,
@@ -18,12 +22,17 @@ def __init__(
         token_address: str,
         backoff_params: BackoffParams | None = None,
     ):
+
+        requests_logger.setLevel(logging.WARNING)    
         self.url = url
         self.sender = sender
         self.token_address = token_address
 
         self.gql_client = Client(
-            transport=AIOHTTPTransport(url=self.url, timeout=2),
+            transport=AIOHTTPTransport(
+                url=self.url, 
+                timeout=2,
+            ),
             fetch_schema_from_transport=False,
         )
 
@@ -109,4 +118,12 @@ async def get_all_streams_history(self) -> list[SablierStream]:
             "tokenAddress": self.token_address,
         }
 
-        return await self._fetch_streams(query, variables)
+        # Cache into file to avoid rate limiting
+        if not os.path.exists("sablier_streams.json"):
+            streams = await self._fetch_streams(query, variables)
+            with open("sablier_streams.json", "w") as f:
+                json.dump(streams, f)
+        else:
+            with open("sablier_streams.json", "r") as f:
+                streams = json.load(f)
+        return streams
diff --git a/backend/v2/user_patron_mode/repositories.py b/backend/v2/user_patron_mode/repositories.py
index 2af10af2eb..422cc06832 100644
--- a/backend/v2/user_patron_mode/repositories.py
+++ b/backend/v2/user_patron_mode/repositories.py
@@ -98,6 +98,7 @@ async def get_all_users_budgets_by_epoch(
         select(Budget.budget, User.address)
         .join(Budget.user)
         .where(Budget.epoch == epoch_number)
+        .order_by(User.address)
     )
     return {
         transform_to_checksum_address(row.address): int(row.budget)

From b266640cb546979b1db59d3c4554175c32932373 Mon Sep 17 00:00:00 2001
From: adam-gf <adam@golem.foundation>
Date: Thu, 16 Jan 2025 01:27:17 +0100
Subject: [PATCH 03/14] Cleaned up the code and removed complexity for easier
 reading

---
 backend/app/settings.py                    |   5 +-
 backend/v2/core/dependencies.py            |  12 +
 backend/v2/deposits/dependencies.py        |  25 +-
 backend/v2/deposits/repositories.py        |  62 +-
 backend/v2/epochs/contracts.py             |   2 +-
 backend/v2/epochs/dependencies.py          |   1 -
 backend/v2/epochs/subgraphs.py             |  12 +-
 backend/v2/glms/dependencies.py            |   3 +-
 backend/v2/project_rewards/dependencies.py |   4 +-
 backend/v2/project_rewards/repositories.py |   2 +-
 backend/v2/project_rewards/router.py       | 649 ++++++---------------
 backend/v2/project_rewards/schemas.py      |   4 +
 backend/v2/project_rewards/services.py     |  85 +++
 backend/v2/project_rewards/user_events.py  |  72 +--
 backend/v2/projects/contracts.py           |   9 +-
 backend/v2/projects/dependencies.py        |  15 +-
 backend/v2/sablier/dependencies.py         |   8 +-
 backend/v2/sablier/subgraphs.py            |  10 +-
 18 files changed, 394 insertions(+), 586 deletions(-)

diff --git a/backend/app/settings.py b/backend/app/settings.py
index eb2fe7bc99..c1b5f1656f 100644
--- a/backend/app/settings.py
+++ b/backend/app/settings.py
@@ -117,7 +117,10 @@ class DevConfig(Config):
     CHAIN_ID = int(os.getenv("CHAIN_ID", 1337))
     # Put the db file in project root
     DB_PATH = os.path.join(Config.PROJECT_ROOT, DB_NAME)
-    SQLALCHEMY_DATABASE_URI = f"sqlite:///{DB_PATH}"
+    # SQLALCHEMY_DATABASE_URI = f"sqlite:///{DB_PATH}"
+    SQLALCHEMY_DATABASE_URI = (
+        f"postgresql://postgres:mysecretpassword@localhost:5433/postgres"
+    )
     SUBGRAPH_RETRY_TIMEOUT_SEC = 2
     X_REAL_IP_REQUIRED = parse_bool(os.getenv("X_REAL_IP_REQUIRED", "false"))
     CACHE_TYPE = "SimpleCache"
diff --git a/backend/v2/core/dependencies.py b/backend/v2/core/dependencies.py
index 4c238dcf0f..afd3dbe5ba 100644
--- a/backend/v2/core/dependencies.py
+++ b/backend/v2/core/dependencies.py
@@ -1,4 +1,5 @@
 from functools import lru_cache
+import time
 from typing import Annotated, AsyncGenerator
 
 from fastapi import Depends
@@ -8,6 +9,8 @@
 from web3 import AsyncHTTPProvider, AsyncWeb3
 from web3.middleware import async_geth_poa_middleware
 
+from app.shared.blockchain_types import ChainTypes
+
 
 class OctantSettings(BaseSettings):
     model_config = SettingsConfigDict(env_file=".env", extra="ignore", frozen=True)
@@ -107,6 +110,10 @@ class ChainSettings(OctantSettings):
         description="The chain id to use for the signature verification.",
     )
 
+    @property
+    def is_mainnet(self) -> bool:
+        return self.chain_id == ChainTypes.MAINNET
+
 
 def get_chain_settings() -> ChainSettings:
     return ChainSettings()
@@ -127,7 +134,12 @@ def get_socketio_settings() -> SocketioSettings:
     return SocketioSettings()  # type: ignore[call-arg]
 
 
+def get_current_timestamp() -> int:
+    return int(time.time())
+
+
 GetSocketioSettings = Annotated[SocketioSettings, Depends(get_socketio_settings)]
 GetChainSettings = Annotated[ChainSettings, Depends(get_chain_settings)]
 Web3 = Annotated[AsyncWeb3, Depends(get_w3)]
 GetSession = Annotated[AsyncSession, Depends(get_db_session)]
+GetCurrentTimestamp = Annotated[int, Depends(get_current_timestamp)]
diff --git a/backend/v2/deposits/dependencies.py b/backend/v2/deposits/dependencies.py
index 7691f73f31..c0c3509492 100644
--- a/backend/v2/deposits/dependencies.py
+++ b/backend/v2/deposits/dependencies.py
@@ -1,10 +1,14 @@
 from typing import Annotated
 
 from fastapi import Depends
+from app.constants import (
+    SABLIER_UNLOCK_GRACE_PERIOD_24_HRS,
+    TEST_SABLIER_UNLOCK_GRACE_PERIOD_15_MIN,
+)
 from v2.deposits.repositories import DepositEventsRepository
 from v2.epochs.dependencies import GetEpochsSubgraph
 from v2.sablier.dependencies import GetSablierSubgraph
-from v2.core.dependencies import GetSession, OctantSettings, Web3
+from v2.core.dependencies import GetChainSettings, GetSession, OctantSettings, Web3
 from v2.deposits.contracts import DEPOSITS_ABI, DepositsContracts
 
 
@@ -22,15 +26,28 @@ def get_deposits_contracts(
     return DepositsContracts(w3, DEPOSITS_ABI, settings.deposits_contract_address)  # type: ignore[arg-type]
 
 
-
 def get_deposit_events_repository(
     session: GetSession,
     epochs_subgraph: GetEpochsSubgraph,
     sublier_subgraph: GetSablierSubgraph,
+    chain_settings: GetChainSettings,
 ) -> DepositEventsRepository:
-    return DepositEventsRepository(session, epochs_subgraph, sublier_subgraph)
+    sablier_unlock_grace_period = (
+        SABLIER_UNLOCK_GRACE_PERIOD_24_HRS
+        if chain_settings.is_mainnet
+        else TEST_SABLIER_UNLOCK_GRACE_PERIOD_15_MIN
+    )
+
+    return DepositEventsRepository(
+        session,
+        epochs_subgraph,
+        sublier_subgraph,
+        sablier_unlock_grace_period,
+    )
 
 
 # Annotated dependencies
 GetDepositsContracts = Annotated[DepositsContracts, Depends(get_deposits_contracts)]
-GetDepositEventsRepository = Annotated[DepositEventsRepository, Depends(get_deposit_events_repository)]
\ No newline at end of file
+GetDepositEventsRepository = Annotated[
+    DepositEventsRepository, Depends(get_deposit_events_repository)
+]
diff --git a/backend/v2/deposits/repositories.py b/backend/v2/deposits/repositories.py
index 1191997e9f..c9ad6bdc91 100644
--- a/backend/v2/deposits/repositories.py
+++ b/backend/v2/deposits/repositories.py
@@ -1,39 +1,30 @@
-
-
 from itertools import groupby
 from operator import attrgetter
-import os
 from sqlalchemy import select
 from sqlalchemy.ext.asyncio import AsyncSession
 from sqlalchemy.orm import selectinload
-from typing import Dict
 
-from app.engine.octant_rewards import OctantRewardsSettings
-from app.engine.user.budget.with_ppf import UserBudgetWithPPF
-from app.engine.user.effective_deposit import DepositEvent, EventType, UserDeposit, UserEffectiveDepositPayload
-from app.engine.user.effective_deposit.weighted_average.default_with_sablier_timebox import DefaultWeightedAverageWithSablierTimebox
+from app.engine.user.effective_deposit import (
+    DepositEvent,
+    EventType,
+)
 from app.infrastructure.database.models import Deposit
-from app.infrastructure.graphql.locks import get_locks_by_timestamp_range
-from app.infrastructure.graphql.unlocks import get_unlocks_by_timestamp_range
-from app.modules.common.sablier_events_mapper import FlattenStrategy, flatten_sablier_events, process_to_locks_and_unlocks
-from app.modules.dto import OctantRewardsDTO, PendingSnapshotDTO
-from app.modules.octant_rewards.core import calculate_rewards
-from app.modules.snapshots.pending.core import calculate_user_budgets
-from app.modules.user.budgets.core import get_upcoming_budget
+from app.modules.common.sablier_events_mapper import (
+    FlattenStrategy,
+    flatten_sablier_events,
+    process_to_locks_and_unlocks,
+)
 from app.modules.user.events_generator.core import unify_deposit_balances
-from app.modules.staking.proceeds.core import estimate_staking_proceeds
-from app.constants import SABLIER_SENDER_ADDRESS_SEPOLIA, SABLIER_TOKEN_ADDRESS_SEPOLIA, ZERO_ADDRESS
-from app.infrastructure import SubgraphEndpoints
+from v2.core.types import Address
+from v2.core.transformers import transform_to_checksum_address
 from v2.epochs.subgraphs import EpochsSubgraph
 from v2.sablier.subgraphs import SablierSubgraph
 
 
-
 async def get_all_deposit_events_for_epoch(
     session: AsyncSession,
     epoch_number: int,
-) -> dict[str, list[DepositEvent]]:
-    
+) -> dict[Address, Deposit]:
     results = await session.scalars(
         select(Deposit)
         .options(selectinload(Deposit.user))
@@ -41,22 +32,22 @@ async def get_all_deposit_events_for_epoch(
     )
 
     return {
-        result.user.address: result for result in results
+        transform_to_checksum_address(result.user.address): result for result in results
     }
 
 
-
-
 class DepositEventsRepository:
     def __init__(
-            self, 
-            session: AsyncSession, 
-            epochs_subgraph: EpochsSubgraph, 
-            sablier_subgraph: SablierSubgraph
-        ):
+        self,
+        session: AsyncSession,
+        epochs_subgraph: EpochsSubgraph,
+        sablier_subgraph: SablierSubgraph,
+        sablier_unlock_grace_period: int,
+    ):
         self.session = session
         self.epochs_subgraph = epochs_subgraph
         self.sablier_subgraph = sablier_subgraph
+        self.sablier_unlock_grace_period = sablier_unlock_grace_period
 
     async def get_all_users_events(
         self,
@@ -67,7 +58,7 @@ async def get_all_users_events(
         """
         Returns all user events (LOCK, UNLOCK) for a given epoch.
         """
-        
+
         # Get all locked amounts for the previous epoch
         epoch_start_locked_amounts = await get_all_deposit_events_for_epoch(
             self.session, epoch_number - 1
@@ -90,8 +81,12 @@ async def get_all_users_events(
         # print("Mapped streams", mapped_streams)
         epoch_events = []
         epoch_events += flatten_sablier_events(mapped_streams, FlattenStrategy.ALL)
-        epoch_events += await self.epochs_subgraph.fetch_locks_by_timestamp_range(start_sec, end_sec)
-        epoch_events += await self.epochs_subgraph.fetch_unlocks_by_timestamp_range(start_sec, end_sec)
+        epoch_events += await self.epochs_subgraph.fetch_locks_by_timestamp_range(
+            start_sec, end_sec
+        )
+        epoch_events += await self.epochs_subgraph.fetch_unlocks_by_timestamp_range(
+            start_sec, end_sec
+        )
 
         epoch_events = [DepositEvent.from_dict(event) for event in epoch_events]
         sorted_events = sorted(epoch_events, key=attrgetter("user", "timestamp"))
@@ -121,7 +116,8 @@ async def get_all_users_events(
                 )
 
             user_events[user_address] = unify_deposit_balances(
-                user_events[user_address]
+                user_events[user_address],
+                self.sablier_unlock_grace_period,
             )
 
         return user_events
diff --git a/backend/v2/epochs/contracts.py b/backend/v2/epochs/contracts.py
index ed45d7cf36..48c76c6fa7 100644
--- a/backend/v2/epochs/contracts.py
+++ b/backend/v2/epochs/contracts.py
@@ -1,5 +1,5 @@
 import logging
-from typing import Dict, Optional
+from typing import Optional
 
 from v2.core.contracts import SmartContract
 from web3 import exceptions
diff --git a/backend/v2/epochs/dependencies.py b/backend/v2/epochs/dependencies.py
index 7015fa0393..f7ff2cc88e 100644
--- a/backend/v2/epochs/dependencies.py
+++ b/backend/v2/epochs/dependencies.py
@@ -89,7 +89,6 @@ async def get_rewards_rate(epoch_number: int) -> float:
 async def get_epoch_state(
     session: GetSession, epochs_contracts: GetEpochsContracts, epoch_number: int
 ) -> EpochState:
-
     current_epoch_number = await epochs_contracts.get_current_epoch()
     if epoch_number > current_epoch_number:
         return EpochState.FUTURE
diff --git a/backend/v2/epochs/subgraphs.py b/backend/v2/epochs/subgraphs.py
index 41f8c36773..621389af55 100644
--- a/backend/v2/epochs/subgraphs.py
+++ b/backend/v2/epochs/subgraphs.py
@@ -78,7 +78,9 @@ def __init__(
                 self.gql_client.execute_async
             )
 
-    async def fetch_locks_by_timestamp_range(self, from_ts: int, to_ts: int) -> list[LockEvent]:
+    async def fetch_locks_by_timestamp_range(
+        self, from_ts: int, to_ts: int
+    ) -> list[LockEvent]:
         """
         Get locks by timestamp range.
         """
@@ -108,7 +110,9 @@ async def fetch_locks_by_timestamp_range(self, from_ts: int, to_ts: int) -> list
         response = await self.gql_client.execute_async(query, variable_values=variables)
         return response["lockeds"]
 
-    async def fetch_unlocks_by_timestamp_range(self, from_ts: int, to_ts: int) -> list[UnlockEvent]:
+    async def fetch_unlocks_by_timestamp_range(
+        self, from_ts: int, to_ts: int
+    ) -> list[UnlockEvent]:
         """
         Get unlocks by timestamp range.
         """
@@ -154,9 +158,9 @@ async def fetch_epoch_by_number(self, epoch_number: int) -> EpochSubgraphItem:
             """
         )
         variables = {"epochNo": epoch_number}
-        
+
         response = await self.gql_client.execute_async(query, variable_values=variables)
-        
+
         data = response["epoches"]
         if not data:
             raise exceptions.EpochNotIndexed(epoch_number)
diff --git a/backend/v2/glms/dependencies.py b/backend/v2/glms/dependencies.py
index a97750c09c..084efb1be4 100644
--- a/backend/v2/glms/dependencies.py
+++ b/backend/v2/glms/dependencies.py
@@ -2,7 +2,6 @@
 from typing import Annotated
 
 from fastapi import Depends
-from v2.deposits.contracts import DepositsContracts
 from v2.deposits.dependencies import GetDepositsContracts
 from v2.core.dependencies import OctantSettings, Web3
 from v2.glms.contracts import ERC20_ABI, GLMContracts
@@ -30,4 +29,4 @@ async def get_glm_balance_of_deposits(
 
 
 GetGLMContracts = Annotated[GLMContracts, Depends(get_glm_contracts)]
-GetGLMBalanceOfDeposits = Annotated[int, Depends(get_glm_balance_of_deposits)]
\ No newline at end of file
+GetGLMBalanceOfDeposits = Annotated[int, Depends(get_glm_balance_of_deposits)]
diff --git a/backend/v2/project_rewards/dependencies.py b/backend/v2/project_rewards/dependencies.py
index 98cbe24200..fad325071e 100644
--- a/backend/v2/project_rewards/dependencies.py
+++ b/backend/v2/project_rewards/dependencies.py
@@ -1,8 +1,7 @@
 from typing import Annotated
 
 from fastapi import Depends
-from v2.sablier.subgraphs import SablierSubgraph
-from v2.core.dependencies import GetSession, OctantSettings
+from v2.core.dependencies import GetSession
 from v2.epochs.dependencies import GetOpenAllocationWindowEpochNumber
 from v2.matched_rewards.dependencies import GetMatchedRewardsEstimator
 from v2.project_rewards.services import ProjectRewardsEstimator
@@ -22,6 +21,7 @@ async def get_project_rewards_estimator(
         epoch_number=epoch_number,
     )
 
+
 GetProjectRewardsEstimator = Annotated[
     ProjectRewardsEstimator,
     Depends(get_project_rewards_estimator),
diff --git a/backend/v2/project_rewards/repositories.py b/backend/v2/project_rewards/repositories.py
index 359eea93fe..9836d6fecc 100644
--- a/backend/v2/project_rewards/repositories.py
+++ b/backend/v2/project_rewards/repositories.py
@@ -1,7 +1,7 @@
-from email.headerregistry import Address
 from sqlalchemy import select
 from sqlalchemy.ext.asyncio import AsyncSession
 from app.infrastructure.database.models import Reward
+from v2.core.types import Address
 
 
 async def get_rewards_for_epoch(
diff --git a/backend/v2/project_rewards/router.py b/backend/v2/project_rewards/router.py
index 7379959983..0b9f811b90 100644
--- a/backend/v2/project_rewards/router.py
+++ b/backend/v2/project_rewards/router.py
@@ -1,23 +1,19 @@
-from datetime import datetime, time, timezone
-import os
 from fastapi import APIRouter, Response, status
-import json
 from app.context.epoch_state import EpochState
-from app.exceptions import NotImplementedForGivenEpochState
-from app.constants import SABLIER_SENDER_ADDRESS_SEPOLIA, SABLIER_TOKEN_ADDRESS_SEPOLIA, ZERO_ADDRESS
-from app.infrastructure import SubgraphEndpoints
-from app.engine.user.budget import UserBudgetPayload
+from app.exceptions import (
+    InvalidEpoch,
+    MissingSnapshot,
+    NotImplementedForGivenEpochState,
+)
 from app.engine.user.budget.with_ppf import UserBudgetWithPPF
-from app.engine.octant_rewards import OctantRewardsSettings
-from app.modules.dto import OctantRewardsDTO, PendingSnapshotDTO
-from app.modules.octant_rewards.core import calculate_rewards
 from app.modules.snapshots.pending.core import calculate_user_budgets
 from app.modules.staking.proceeds.core import estimate_staking_proceeds
+from v2.matched_rewards.dependencies import GetMatchedRewardsEstimator
 from v2.deposits.dependencies import GetDepositEventsRepository
-from v2.sablier.dependencies import GetSablierSubgraph
-from v2.glms.dependencies import GetGLMBalanceOfDeposits, GetGLMContracts
-from v2.sablier.subgraphs import SablierSubgraph
-from v2.project_rewards.user_events import calculate_effective_deposits, calculate_pending_epoch_snapshot, simulate_user_events
+from v2.glms.dependencies import GetGLMBalanceOfDeposits
+from v2.project_rewards.user_events import (
+    calculate_effective_deposits,
+)
 from v2.allocations.repositories import (
     get_donors_for_epoch,
     sum_allocations_by_epoch,
@@ -39,8 +35,13 @@
     get_all_users_budgets_by_epoch,
     get_budget_by_user_address_and_epoch,
 )
-from v2.core.dependencies import GetSession
-from v2.project_rewards.services import get_rewards_merkle_tree_for_epoch
+from v2.core.dependencies import GetCurrentTimestamp, GetSession
+from v2.project_rewards.services import (
+    calculate_octant_rewards,
+    calculate_user_budget,
+    get_rewards_merkle_tree_for_epoch,
+    simulate_user_effective_deposits,
+)
 from v2.project_rewards.dependencies import GetProjectRewardsEstimator
 from v2.project_rewards.schemas import (
     EpochBudgetItemV1,
@@ -87,234 +88,67 @@ async def get_user_budget_for_epoch_v1(
 
 @api.get("/budget/{user_address}/upcoming")
 async def get_user_budget_for_upcoming_epoch_v1(
-    session: GetSession,
-    epoch_contracts: GetEpochsContracts,
-    epoch_subgraph: GetEpochsSubgraph,
-    deposit_events: GetDepositEventsRepository,
+    # Dependencies
+    epochs_contracts: GetEpochsContracts,
+    epochs_subgraph: GetEpochsSubgraph,
+    deposit_events_repository: GetDepositEventsRepository,
+    current_timestamp: GetCurrentTimestamp,
+    # Parameters
     user_address: Address,
 ) -> UpcomingUserBudgetResponseV1:
     """
-    Returns the upcoming user budget based on if allocation happened now.
+    Returns the upcoming user budget based on as-if allocation happened now.
+
+    This is done by simulating the epoch end as if it ended now.
+    So we simulate the pending snapshot for this epoch end.
+     - Get current epoch details
+     - Simulate the epoch end as if it ended now (end_time = now)
+     - Simulate the pending snapshot for this epoch end
+        - Estimate staking proceeds
+        - Calculate total effective deposit based on deposit events
+        - Calculate octant rewards
+     - Calculate the user budget for this pending snapshot
     """
 
-    epoch_number = await epoch_contracts.get_current_epoch()
-    epoch_details = await epoch_subgraph.fetch_epoch_by_number(epoch_number)
-    
+    # Get current epoch details
+    epoch_number = await epochs_contracts.get_current_epoch()
+    epoch_details = await epochs_subgraph.fetch_epoch_by_number(epoch_number)
+
     # We SIMULATE the epoch end as if it ended now
-    epoch_end = int(datetime.now(timezone.utc).timestamp())
+    epoch_end = current_timestamp
     epoch_start = epoch_details.fromTs
 
-    # pending_snapshot = await calculate_pending_epoch_snapshot(
-    #     deposit_events,
-    #     epoch_number, 
-    #     epoch_start, 
-    #     epoch_end
-    # )
-# async def calculate_pending_epoch_snapshot(
-#     deposit_events: DepositEventsRepository,
-#     epoch_number: int,
-#     epoch_start: int,
-#     epoch_end: int,
-# ) -> PendingSnapshotDTO:
-    
-    # Get octant rewards
-        # epoch_details = await epochs_subgraph.fetch_epoch_by_number(epoch_number)
-    # duration_sec = epoch_details.duration
-    # return estimate_staking_proceeds(duration_sec)
-    # eth_proceeds = await get_staking_proceeds(session, epoch_number, start_sec, end_sec)
+    # BEGIN: Calculate pending snapshot
     eth_proceeds = estimate_staking_proceeds(epoch_end - epoch_start)
 
-    events = await deposit_events.get_all_users_events(
-        epoch_number,
-        epoch_start,
-        epoch_end
-    )
-    user_deposits, total_effective_deposit = calculate_effective_deposits(epoch_start, epoch_end, events)
-
-    # total_effective_deposit = 155654569757136462439580980
-    rewards_settings = OctantRewardsSettings()
-    octant_rewards = calculate_rewards(
-        rewards_settings, eth_proceeds, total_effective_deposit
+    # Based on all deposit events, calculate effective deposits
+    events = await deposit_events_repository.get_all_users_events(
+        epoch_number, epoch_start, epoch_end
     )
-    rewards = OctantRewardsDTO(
-        staking_proceeds=eth_proceeds,
-        locked_ratio=octant_rewards.locked_ratio,
-        total_effective_deposit=total_effective_deposit,
-        total_rewards=octant_rewards.total_rewards,
-        vanilla_individual_rewards=octant_rewards.vanilla_individual_rewards,
-        operational_cost=octant_rewards.operational_cost,
-        ppf=octant_rewards.ppf_value,
-        community_fund=octant_rewards.community_fund,
+    user_deposits, total_effective_deposit = calculate_effective_deposits(
+        epoch_start, epoch_end, events
     )
 
-    # events = await get_all_user_events(
-    #     session,
-    #     epochs_subgraph,
-    #     sablier,
-    #     epoch_number,
-    #     epoch_start,
-    #     epoch_end
-    # )
-    # user_deposits, total_effective_deposit = calculate_effective_deposits(epoch_start, epoch_end, events)
+    # Calculate octant rewards
+    rewards = calculate_octant_rewards(eth_proceeds, total_effective_deposit)
 
+    # END: Calculate pending snapshot
+
+    # Calculate user budget
     user_budget_calculator = UserBudgetWithPPF()
     user_budgets = calculate_user_budgets(
         user_budget_calculator, rewards, user_deposits
     )
-    # pending_snapshot = PendingSnapshotDTO(
-    #     rewards=rewards, user_deposits=user_deposits, user_budgets=user_budgets
-    # )
-    # return PendingSnapshotDTO(
-    #     rewards=rewards, user_deposits=user_deposits, user_budgets=user_budgets
-    # )
-
-
-# Getting unlocks in timestamp range 1728835200 - 1735308472
-    # upcoming_budget = pending_snapshot.user_budgets.get(user_address)
-
-    # print("Pending snapshot", pending_snapshot)
-
     user_budget = next(
-        (budget.budget for budget in user_budgets if budget.user_address == user_address),
-        0  # Default value if user not found
+        (
+            budget.budget
+            for budget in user_budgets
+            if budget.user_address == user_address
+        ),
+        0,  # Default value if user not found
     )
 
-    # if not user_budget:
-    #     return UpcomingUserBudgetResponseV1(upcoming_budget=0)
-    
     return UpcomingUserBudgetResponseV1(upcoming_budget=user_budget)
-    print("I'm here")
-
-    # TODO we need to handle snapshots here unfortunatelly
-    # upcoming_budget = await get_upcoming_user_budget(user_address)
-
-
-
-    # context = state_context(EpochState.SIMULATED, with_block_range=True)
-    # service: UpcomingUserBudgets = get_services(EpochState.CURRENT).user_budgets_service
-    # return service.get_budget(context, user_address)
-
-    # def get_budget(self, context: Context, user_address: str) -> int:
-        # simulated_snapshot = (
-        #     self.simulated_pending_snapshot_service.simulate_pending_epoch_snapshot(
-        #         context
-        #     )
-        # )
-        # upcoming_budget = core.get_upcoming_budget(
-        #     user_address, simulated_snapshot.user_budgets
-        # )
-
-        # return upcoming_budget
-
-
-    # user_deposits = CalculatedUserDeposits(
-    #         events_generator=DbAndGraphEventsGenerator()
-    #     )
-    # octant_rewards = CalculatedOctantRewards(
-    #     staking_proceeds=EstimatedStakingProceeds(),
-    #     effective_deposits=user_deposits,
-    # )
-    # UpcomingUserBudgets(
-    #     simulated_pending_snapshot_service=SimulatedPendingSnapshots(
-    #         effective_deposits=user_deposits, octant_rewards=octant_rewards
-    #     )
-    # )
-
-
-
-    # simulated_snapshot = (
-    #     self.simulated_pending_snapshot_service.simulate_pending_epoch_snapshot(
-    #         context
-    #     )
-    # )
-
-    # def _calculate_pending_epoch_snapshot(self, context: Context) -> PendingSnapshotDTO:
-    # rewards = self.octant_rewards.get_octant_rewards(context)
-
-        # def get_octant_rewards(self, context: Context) -> OctantRewardsDTO:
-        #     eth_proceeds = self.staking_proceeds.get_staking_proceeds(context)
-            # def get_staking_proceeds(self, context: Context) -> int:
-            #     return estimate_staking_proceeds(context.epoch_details.duration_sec)
-
-                # epoch_details = await epochs_subgraph.fetch_epoch_by_number(epoch_number)
-                # duration_sec = epoch_details.duration
-                # return estimate_staking_proceeds(duration_sec)
-
-    #     total_effective_deposit = self.effective_deposits.get_total_effective_deposit(
-    #         context
-    #     )
-        # def get_total_effective_deposit(self, context: Context) -> int:
-        #    events = self.events_generator.get_all_users_events(context)
-        #    _, total = calculate_effective_deposits(
-        #        context.epoch_details, context.epoch_settings, events
-        #    )
-        #    return total
-
-
-    #     rewards_settings = context.epoch_settings.octant_rewards
-    #     octant_rewards = calculate_rewards(
-    #         rewards_settings, eth_proceeds, total_effective_deposit
-    #     )
-    #     (
-    #         locked_ratio,
-    #         total_rewards,
-    #         vanilla_individual_rewards,
-    #         op_cost,
-    #         ppf,
-    #         community_fund,
-    #     ) = (
-    #         octant_rewards.locked_ratio,
-    #         octant_rewards.total_rewards,
-    #         octant_rewards.vanilla_individual_rewards,
-    #         octant_rewards.operational_cost,
-    #         octant_rewards.ppf_value,
-    #         octant_rewards.community_fund,
-    #     )
-
-    #     return OctantRewardsDTO(
-    #         staking_proceeds=eth_proceeds,
-    #         locked_ratio=locked_ratio,
-    #         total_effective_deposit=total_effective_deposit,
-    #         total_rewards=total_rewards,
-    #         vanilla_individual_rewards=vanilla_individual_rewards,
-    #         operational_cost=op_cost,
-    #         ppf=ppf,
-    #         community_fund=community_fund,
-    #     )
-
-    # (
-    #     user_deposits,
-    #     _,
-    # ) = self.effective_deposits.get_all_effective_deposits(context)
-    # user_budgets = calculate_user_budgets(
-    #     context.epoch_settings.user.budget, rewards, user_deposits
-    # )
-
-    # return PendingSnapshotDTO(
-    #     rewards=rewards, user_deposits=user_deposits, user_budgets=user_budgets
-    # )
-
-    # user_budget = next(
-    #     filter(
-    #         lambda budget_info: budget_info.user_address == user_address,
-    #         upcoming_user_budgets,
-    #     ),
-    #     None,
-    # )
-    # if not user_budget:
-    #     return 0
-    # return user_budget.budget
-
-    # upcoming_budget = core.get_upcoming_budget(
-    #     user_address, simulated_snapshot.user_budgets
-    # )
-
-    # return upcoming_budget
-
-    return UpcomingUserBudgetResponseV1(upcoming_budget=upcoming_budget)
-
-
-
 
 
 @api.get("/budgets/epoch/{epoch_number}")
@@ -340,129 +174,61 @@ async def get_estimated_budget_v1(
     session: GetSession,
     epoch_contracts: GetEpochsContracts,
     glm_balance: GetGLMBalanceOfDeposits,
+    matched_rewards_estimator: GetMatchedRewardsEstimator,
     request: EstimatedBudgetByEpochRequestV1,
 ) -> UserBudgetWithMatchedFundingResponseV1:
-    
-    # leverage
-    epoch_number = await epoch_contracts.get_finalized_epoch()
-    
-    # Calculate leverage based on the last finalized epoch
-    allocations_sum = await sum_allocations_by_epoch(session, epoch_number)
-    finalized_snapshot = await get_finalized_epoch_snapshot(session, epoch_number)
-    matched_rewards = int(finalized_snapshot.matched_rewards)
-
-    leverage = matched_rewards / allocations_sum if allocations_sum else 0
+    """
+    Returns the estimated budget if the user deposits GLMs for a given number of FULL epochs.
+    Each epoch is a 90 day period.
+
+    This is done based on the last finalized snapshot.
+    - Get the last finalized epoch
+    - Calculate the leverage based on the last finalized epoch
+    - Get the future epoch details (start_time, duration, end_time)
+    - Calculate rewards:
+        - Estimate staking proceeds
+        - Assume total effective deposit as total GLMs from contract
+        - Calculate octant rewards
+    - Simulate user events as if they deposited given amount of GLMs
+    - Calculate user budget for this pending snapshot
+    """
 
     future_epoch = await epoch_contracts.get_future_epoch_props()
-    print("Future epoch", future_epoch)
-    # start_sec = future_epoch[2]
-    start_sec = future_epoch[2]
-    # duration = future_epoch[3]
-    duration = future_epoch[3]
-    end_sec = start_sec + duration
-    # decision_window = future_epoch[4]
-    decision_window = future_epoch[4]
-
-    # Estimate staking proceeds
-    eth_proceeds = estimate_staking_proceeds(duration)
+    epoch_start = future_epoch[2]
+    epoch_duration = future_epoch[3]
+    epoch_end = epoch_start + epoch_duration
+    epoch_remaining = epoch_duration
+
+    # We interpolate future rewards based on the future epoch
+    #  duration and total effective deposit of the finalized epoch
+    eth_proceeds = estimate_staking_proceeds(epoch_duration)
     total_effective_deposit = glm_balance
+    future_rewards = calculate_octant_rewards(eth_proceeds, total_effective_deposit)
 
-    # total_effective_deposit = 155654569757136462439580980
-    rewards_settings = OctantRewardsSettings()
-    octant_rewards = calculate_rewards(
-        rewards_settings, eth_proceeds, total_effective_deposit
+    # Simulate budget
+    user_effective_deposit = simulate_user_effective_deposits(
+        epoch_start,
+        epoch_end,
+        epoch_remaining,
+        epoch_duration,
+        request.glm_amount,
     )
-    future_rewards = OctantRewardsDTO(
-        staking_proceeds=eth_proceeds,
-        locked_ratio=octant_rewards.locked_ratio,
-        total_effective_deposit=total_effective_deposit,
-        total_rewards=octant_rewards.total_rewards,
-        vanilla_individual_rewards=octant_rewards.vanilla_individual_rewards,
-        operational_cost=octant_rewards.operational_cost,
-        ppf=octant_rewards.ppf_value,
-        community_fund=octant_rewards.community_fund,
+    user_budget = calculate_user_budget(
+        user_effective_deposit,
+        future_rewards,
     )
 
+    epochs_budget = request.number_of_epochs * user_budget
 
-    # Simulate user events as if they deposited given amount of GLMs
-    events = {
-        ZERO_ADDRESS: simulate_user_events(
-            end_sec,
-            duration,
-            duration,
-            request.glm_amount
-        )
-    }
-    user_deposits, total_effective_deposit = calculate_effective_deposits(start_sec, end_sec, events)
-
-    # print("total_effective_deposit", total_effective_deposit)
-    # print("User deposits", user_deposits)
-    effective_deposit = (
-        user_deposits[0].effective_deposit if user_deposits else 0
-    )
-
-    # print("Effective deposit", effective_deposit)
-    # print("Total effective deposit", future_rewards.total_effective_deposit)
-    # print("Vanilla individual rewards", future_rewards.vanilla_individual_rewards)
-    # print("PPF", future_rewards.ppf)
-
-    budget_calculator = UserBudgetWithPPF()
-    epoch_budget = budget_calculator.calculate_budget(
-        UserBudgetPayload(
-            user_effective_deposit=effective_deposit,
-            total_effective_deposit=future_rewards.total_effective_deposit,
-            vanilla_individual_rewards=future_rewards.vanilla_individual_rewards,
-            ppf=future_rewards.ppf,
-        )
+    # Matching fund is calculated based on the last epoch's leverage
+    epoch_number = await epoch_contracts.get_finalized_epoch()
+    rewards_leverage = await get_rewards_leverage_v1(
+        session, epoch_contracts, epoch_number, matched_rewards_estimator
     )
-    epochs_budget = request.number_of_epochs * epoch_budget
-
-    matching_fund = epochs_budget * leverage
-
 
     return UserBudgetWithMatchedFundingResponseV1(
         budget=epochs_budget,
-        matched_funding=matching_fund
-    )
-
-
-def estimate_epoch_budget(
-    start_sec: int, 
-    end_sec: int,
-    remaining_sec: int,
-    lock_duration: int,
-    glm_amount: int, 
-    rewards: OctantRewardsDTO
-) -> int:
-    events = {
-        ZERO_ADDRESS: simulate_user_events(
-            end_sec,
-            lock_duration,
-            remaining_sec,
-            glm_amount
-        )
-    }
-    user_deposits, total_effective_deposit = calculate_effective_deposits(start_sec, end_sec, events)
-
-    # print("total_effective_deposit", total_effective_deposit)
-    # print("User deposits", user_deposits)
-    effective_deposit = (
-        user_deposits[0].effective_deposit if user_deposits else 0
-    )
-
-    # print("Effective deposit", effective_deposit)
-    # print("Total effective deposit", future_rewards.total_effective_deposit)
-    # print("Vanilla individual rewards", future_rewards.vanilla_individual_rewards)
-    # print("PPF", future_rewards.ppf)
-
-    budget_calculator = UserBudgetWithPPF()
-    return budget_calculator.calculate_budget(
-        UserBudgetPayload(
-            user_effective_deposit=effective_deposit,
-            total_effective_deposit=rewards.total_effective_deposit,
-            vanilla_individual_rewards=rewards.vanilla_individual_rewards,
-            ppf=rewards.ppf,
-        )
+        matched_funding=int(epochs_budget * rewards_leverage.leverage),
     )
 
 
@@ -472,59 +238,66 @@ async def get_estimated_budget_by_days_v1(
     epoch_subgraph: GetEpochsSubgraph,
     deposit_events: GetDepositEventsRepository,
     glm_balance: GetGLMBalanceOfDeposits,
+    current_timestamp: GetCurrentTimestamp,
     request: EstimatedBudgetByDaysRequestV1,
 ) -> UserBudgetResponseV1:
-    # validate_estimate_budget_inputs(days, glm_amount)
+    """
+    Returns the estimated budget if the user deposits GLMs for a given number of days.
+
+    This is done by first filling up the current epoch and then calculating the remaining budget for the future epochs.
+    """
 
-    lock_duration_sec = request.days * 86400 # 24hours * 60minutes * 60seconds
+    remaining_lock_duration = request.lock_duration_sec
 
+    # Get the current epoch details
     current_epoch_number = await epoch_contracts.get_current_epoch()
-    current_epoch_details = await epoch_subgraph.fetch_epoch_by_number(current_epoch_number)
+    current_epoch_details = await epoch_subgraph.fetch_epoch_by_number(
+        current_epoch_number
+    )
     current_epoch_start = current_epoch_details.fromTs
     current_epoch_duration = current_epoch_details.duration
-    current_epoch_remaining = current_epoch_start + current_epoch_duration - int(datetime.now().timestamp())
+    current_epoch_remaining = (
+        current_epoch_start + current_epoch_duration - current_timestamp
+    )
 
     # CURRENT EPOCH REWARDS
     current_eth_proceeds = estimate_staking_proceeds(current_epoch_duration)
     current_events = await deposit_events.get_all_users_events(
         current_epoch_number,
         current_epoch_start,
-        current_epoch_start + current_epoch_duration
-    )
-    user_deposits, total_effective_deposit = calculate_effective_deposits(current_epoch_start, current_epoch_start + current_epoch_duration, current_events)
-
-    current_rewards_settings = OctantRewardsSettings()
-    current_octant_rewards = calculate_rewards(
-        current_rewards_settings, current_eth_proceeds, total_effective_deposit
+        current_epoch_start + current_epoch_duration,
     )
-    current_rewards = OctantRewardsDTO(
-        staking_proceeds=current_eth_proceeds,
-        locked_ratio=current_octant_rewards.locked_ratio,
-        total_effective_deposit=total_effective_deposit,
-        total_rewards=current_octant_rewards.total_rewards,
-        vanilla_individual_rewards=current_octant_rewards.vanilla_individual_rewards,
-        operational_cost=current_octant_rewards.operational_cost,
-        ppf=current_octant_rewards.ppf_value,
-        community_fund=current_octant_rewards.community_fund,
+    _, total_effective_deposit = calculate_effective_deposits(
+        current_epoch_start,
+        current_epoch_start + current_epoch_duration,
+        current_events,
     )
 
+    current_rewards = calculate_octant_rewards(
+        current_eth_proceeds, total_effective_deposit
+    )
 
-    # return estimate_budget(lock_duration_sec, glm_amount)
-    # current_context = state_context(EpochState.CURRENT)
-    # current_rewards_service = get_services(EpochState.CURRENT).octant_rewards_service
-        # CURRENT
-        # is_mainnet = compare_blockchain_types(chain_id, ChainTypes.MAINNET)
-            # octant_rewards = CalculatedOctantRewards(
-            #     staking_proceeds=EstimatedStakingProceeds(),
-            #     effective_deposits=CalculatedUserDeposits(
-            #     events_generator=DbAndGraphEventsGenerator()
-            # )
-        # )
-
+    # Simulate user budget till the end of the current epoch
+    user_effective_deposit = simulate_user_effective_deposits(
+        current_epoch_start,
+        current_epoch_start + current_epoch_duration,
+        current_epoch_remaining,
+        remaining_lock_duration,
+        request.glm_amount,
+    )
+    budget = calculate_user_budget(
+        user_effective_deposit,
+        current_rewards,
+    )
 
-    # current_rewards = current_rewards_service.get_octant_rewards(current_context)
+    # Subtract the current epoch remaining from the remaining lock duration
+    # If there's no remaining lock duration, we're done
+    remaining_lock_duration -= current_epoch_remaining
+    if remaining_lock_duration <= 0:
+        return UserBudgetResponseV1(budget=budget)
 
     # FUTURE EPOCH REWARDS
+    # Because we've already filled up the current epoch, we need to calculate based on the future epoch
     future_epoch_number = await epoch_contracts.get_future_epoch_props()
     future_epoch_start = future_epoch_number[2]
     future_epoch_duration = future_epoch_number[3]
@@ -532,81 +305,42 @@ async def get_estimated_budget_by_days_v1(
     future_eth_proceeds = estimate_staking_proceeds(future_epoch_duration)
 
     future_total_effective_deposit = glm_balance
-
-    future_rewards_settings = OctantRewardsSettings()
-    future_octant_rewards = calculate_rewards(
-        future_rewards_settings, future_eth_proceeds, future_total_effective_deposit
+    future_rewards = calculate_octant_rewards(
+        future_eth_proceeds, future_total_effective_deposit
     )
-    future_rewards = OctantRewardsDTO(
-        staking_proceeds=future_eth_proceeds,
-        locked_ratio=future_octant_rewards.locked_ratio,
-        total_effective_deposit=future_total_effective_deposit,
-        total_rewards=future_octant_rewards.total_rewards,
-        vanilla_individual_rewards=future_octant_rewards.vanilla_individual_rewards,
-        operational_cost=future_octant_rewards.operational_cost,
-        ppf=future_octant_rewards.ppf_value,
-        community_fund=future_octant_rewards.community_fund,
-    )
-
-    # future_context = state_context(EpochState.FUTURE)
-    # future_rewards_service = get_services(EpochState.FUTURE).octant_rewards_service
-        # octant_rewards_service=CalculatedOctantRewards(
-        #     staking_proceeds=EstimatedStakingProceeds(),
-        #     effective_deposits=ContractBalanceUserDeposits(),
-        # ),
-
-    # future_rewards = future_rewards_service.get_octant_rewards(future_context)
 
+    # Calculate the number of full future epochs and the remaining lock duration
+    full_epochs_num, remaining_future_epoch_sec = divmod(
+        remaining_lock_duration, future_epoch_duration
+    )
 
-
-    # return core.estimate_budget(
-    #     current_context,
-    #     future_context,
-    #     current_rewards,
-    #     future_rewards,
-    #     lock_duration_sec,
-    #     glm_amount,
-    # )
-
-    remaining_lock_duration = lock_duration_sec
-
-    budget = estimate_epoch_budget(
-        current_epoch_start,
-        current_epoch_start + current_epoch_duration,
-        current_epoch_remaining,
-        remaining_lock_duration,
+    # Simulate user budget for the full future epochs
+    user_effective_deposit = simulate_user_effective_deposits(
+        future_epoch_start,
+        future_epoch_end,
+        future_epoch_duration,
+        future_epoch_duration,
         request.glm_amount,
-        current_rewards,
     )
-    remaining_lock_duration -= current_epoch_remaining
+    budget += full_epochs_num * calculate_user_budget(
+        user_effective_deposit,
+        future_rewards,
+    )
 
-    if remaining_lock_duration > 0:
-        full_epochs_num, remaining_future_epoch_sec = divmod(
-            remaining_lock_duration, future_epoch_duration
-        )
-        budget += full_epochs_num * estimate_epoch_budget(
+    if remaining_future_epoch_sec > 0:
+        user_effective_deposit = simulate_user_effective_deposits(
             future_epoch_start,
             future_epoch_end,
             future_epoch_duration,
-            future_epoch_duration,
+            remaining_future_epoch_sec,
             request.glm_amount,
-            future_rewards,
         )
-        remaining_lock_duration = remaining_future_epoch_sec
-
-    if remaining_lock_duration > 0:
-        budget += estimate_epoch_budget(
-            future_epoch_start,
-            future_epoch_end,
-            future_epoch_duration,
-            remaining_lock_duration,
-            request.glm_amount,
+        budget += calculate_user_budget(
+            user_effective_deposit,
             future_rewards,
         )
 
-    return UserBudgetResponseV1(
-        budget=budget
-    )
+    return UserBudgetResponseV1(budget=budget)
 
 
 @api.get("/leverage/{epoch_number}")
@@ -614,61 +348,40 @@ async def get_rewards_leverage_v1(
     session: GetSession,
     epochs_contracts: GetEpochsContracts,
     epoch_number: int,
+    matched_rewards_estimator: GetMatchedRewardsEstimator,
 ) -> RewardsLeverageResponseV1:
     """
     Returns leverage for a given epoch.
 
     For finalized epochs it returns the leverage based on the data from the finalized snapshot.
-
+    For pending and finalizing epochs it returns the leverage based on the estimated matched rewards of pending epoch.
     """
 
+    print(f"Getting leverage for epoch {epoch_number}")
+
     epoch_state = await get_epoch_state(session, epochs_contracts, epoch_number)
 
-    print("Epoch state", epoch_state)
+    # This operations only makes sense for finalized, finalizing and pending epochs
     if epoch_state > EpochState.PENDING:
         raise NotImplementedForGivenEpochState()
 
+    # Figure out the matched rewards
     if epoch_state == EpochState.FINALIZED:
-        # We are in pending epoch, so we need to get the leverage from the pending epoch
-
-        allocations_sum = await sum_allocations_by_epoch(session, epoch_number)
-
-        # allocations_sum = database.allocations.get_alloc_sum_by_epoch(
-        #     context.epoch_details.epoch_num
-        # )
-
+        # For finalized epoch matched_rewards is just taken from the finalized snapshot
         finalized_snapshot = await get_finalized_epoch_snapshot(session, epoch_number)
-        # finalized_snapshot = database.finalized_epoch_snapshot.get_by_epoch(
-        #     context.epoch_details.epoch_num
-        # )
+        if finalized_snapshot is None:
+            raise MissingSnapshot()
         matched_rewards = int(finalized_snapshot.matched_rewards)
 
-        return RewardsLeverageResponseV1(
-            leverage=matched_rewards / allocations_sum if allocations_sum else 0
-        )
+    else:
+        # For pending or finalizing epoch matched_rewards is estimated based on current state of AW
+        matched_rewards = await matched_rewards_estimator.get()
 
-        # return context.epoch_settings.project.rewards.leverage.calculate_leverage(
-        #     matched_rewards, allocations_sum
-        # )
-        # def calculate_leverage(self, matched_rewards: int, total_allocated: int) -> float:
-        # return matched_rewards / total_allocated if total_allocated else 0
-
-    if epoch_state <= EpochState.PENDING:
-        allocations_sum = await sum_allocations_by_epoch(session, epoch_number)
-        # allocations_sum = database.allocations.get_alloc_sum_by_epoch(
-        #     context.epoch_details.epoch_num
-        # )
-        matched_rewards = self.get_matched_rewards(context)
-
-        return RewardsLeverageResponseV1(
-            leverage=matched_rewards / allocations_sum if allocations_sum else 0
-        )
+    allocations_sum = await sum_allocations_by_epoch(session, epoch_number)
 
-        # return context.epoch_settings.project.rewards.leverage.calculate_leverage(
-        #     matched_rewards, allocations_sum
-        # )
-        # def calculate_leverage(self, matched_rewards: int, total_allocated: int) -> float:
-        # return matched_rewards / total_allocated if total_allocated else 0
+    return RewardsLeverageResponseV1(
+        leverage=matched_rewards / allocations_sum if allocations_sum else 0
+    )
 
 
 @api.get("/merkle_tree/{epoch_number}")
@@ -680,7 +393,12 @@ async def get_rewards_merkle_tree_v1(
     Returns the rewards merkle tree for a given epoch.
     """
 
-    return await get_rewards_merkle_tree_for_epoch(session, epoch_number)
+    tree = await get_rewards_merkle_tree_for_epoch(session, epoch_number)
+    if tree is None:
+        # TODO: this could be better :)
+        raise InvalidEpoch()
+
+    return tree
 
 
 @api.get("/projects/estimated")
@@ -759,11 +477,12 @@ async def get_unused_rewards_v1(
 
     # Exclude donors and patrons from the list of users who didn't use their rewards
     unused_budgets = {
-        address: budget for address, budget in budgets.items() 
+        address: budget
+        for address, budget in budgets.items()
         if address not in set(donors + patrons)
     }
 
     return UnusedRewardsResponseV1(
-        addresses=sorted(list(unused_budgets.keys())), 
+        addresses=sorted(list(unused_budgets.keys())),
         value=sum(unused_budgets.values()),
     )
diff --git a/backend/v2/project_rewards/schemas.py b/backend/v2/project_rewards/schemas.py
index d2ce4e6433..314697e141 100644
--- a/backend/v2/project_rewards/schemas.py
+++ b/backend/v2/project_rewards/schemas.py
@@ -40,6 +40,10 @@ class EstimatedBudgetByDaysRequestV1(OctantModel):
         ..., description="Amount of estimated GLM locked in wei"
     )
 
+    @property
+    def lock_duration_sec(self) -> int:
+        return self.days * 86400  # 24hours * 60minutes * 60seconds
+
 
 class RewardsMerkleTreeLeafV1(OctantModel):
     address: Address = Field(..., description="User account or project address")
diff --git a/backend/v2/project_rewards/services.py b/backend/v2/project_rewards/services.py
index b0d4a79155..41eccad474 100644
--- a/backend/v2/project_rewards/services.py
+++ b/backend/v2/project_rewards/services.py
@@ -2,6 +2,16 @@
 from multiproof import StandardMerkleTree
 
 from sqlalchemy.ext.asyncio import AsyncSession
+from app.constants import ZERO_ADDRESS
+from app.engine.octant_rewards import OctantRewardsSettings
+from app.engine.user.budget import UserBudgetPayload
+from app.engine.user.budget.with_ppf import UserBudgetWithPPF
+from app.modules.dto import OctantRewardsDTO
+from app.modules.octant_rewards.core import calculate_rewards
+from v2.project_rewards.user_events import (
+    calculate_effective_deposits,
+    simulate_user_events,
+)
 from v2.project_rewards.repositories import get_rewards_for_epoch
 from v2.project_rewards.schemas import (
     RewardsMerkleTreeLeafV1,
@@ -81,3 +91,78 @@ async def get_rewards_merkle_tree_for_epoch(
         leaves=leaves,
         leaf_encoding=LEAF_ENCODING,
     )
+
+
+def calculate_octant_rewards(
+    eth_proceeds: int,
+    total_effective_deposit: int,
+) -> OctantRewardsDTO:
+    """
+    Calculate the octant rewards based on ETH proceeds and total effective deposit.
+    Proceeds are the what the octant gets from the staking.
+    Effective deposit is the amount of GLMs that are eligible for rewards.
+
+    Parameters:
+        eth_proceeds: int - The amount of ETH proceeds.
+        total_effective_deposit: int - The total amount of GLMs deposited.
+
+    Returns:
+        OctantRewardsDTO - The octant rewards distribution.
+    """
+
+    settings = OctantRewardsSettings()
+    rewards = calculate_rewards(settings, eth_proceeds, total_effective_deposit)
+
+    # fmt: off
+    return OctantRewardsDTO(
+        staking_proceeds            = eth_proceeds,
+        locked_ratio                = rewards.locked_ratio,
+        total_effective_deposit     = total_effective_deposit,
+        total_rewards               = rewards.total_rewards,
+        vanilla_individual_rewards  = rewards.vanilla_individual_rewards,
+        operational_cost            = rewards.operational_cost,
+        ppf                         = rewards.ppf_value,
+        community_fund              = rewards.community_fund,
+    )
+    # fmt: on
+
+
+def simulate_user_effective_deposits(
+    epoch_start: int,
+    epoch_end: int,
+    epoch_remaining: int,
+    lock_duration: int,
+    glm_amount: int,
+) -> int:
+    """
+    Simulate the user effective deposits for a given epoch.
+    """
+
+    events = {
+        ZERO_ADDRESS: simulate_user_events(
+            epoch_end, lock_duration, epoch_remaining, glm_amount
+        )
+    }
+    _, total_effective_deposit = calculate_effective_deposits(
+        epoch_start, epoch_end, events
+    )
+    return total_effective_deposit
+
+
+def calculate_user_budget(
+    user_effective_deposit: int,
+    rewards: OctantRewardsDTO,
+) -> int:
+    """
+    Calculate the user budget based on the user effective deposit and the octant rewards.
+    """
+
+    budget_calculator = UserBudgetWithPPF()
+    return budget_calculator.calculate_budget(
+        UserBudgetPayload(
+            user_effective_deposit=user_effective_deposit,
+            total_effective_deposit=rewards.total_effective_deposit,
+            vanilla_individual_rewards=rewards.vanilla_individual_rewards,
+            ppf=rewards.ppf,
+        )
+    )
diff --git a/backend/v2/project_rewards/user_events.py b/backend/v2/project_rewards/user_events.py
index c44bbf77f7..4d28d8f000 100644
--- a/backend/v2/project_rewards/user_events.py
+++ b/backend/v2/project_rewards/user_events.py
@@ -1,36 +1,24 @@
-
-
-from itertools import groupby
-from operator import attrgetter
-import os
-from sqlalchemy import select
-from sqlalchemy.ext.asyncio import AsyncSession
-from sqlalchemy.orm import selectinload
 from typing import Dict
 
 from app.engine.octant_rewards import OctantRewardsSettings
 from app.engine.user.budget.with_ppf import UserBudgetWithPPF
-from app.engine.user.effective_deposit import DepositEvent, EventType, UserDeposit, UserEffectiveDepositPayload
-from app.engine.user.effective_deposit.weighted_average.default_with_sablier_timebox import DefaultWeightedAverageWithSablierTimebox
-from app.infrastructure.database.models import Deposit
-from app.infrastructure.graphql.locks import get_locks_by_timestamp_range
-from app.infrastructure.graphql.unlocks import get_unlocks_by_timestamp_range
-from app.modules.common.sablier_events_mapper import FlattenStrategy, flatten_sablier_events, process_to_locks_and_unlocks
+from app.engine.user.effective_deposit import (
+    DepositEvent,
+    EventType,
+    UserDeposit,
+    UserEffectiveDepositPayload,
+)
 from app.modules.dto import OctantRewardsDTO, PendingSnapshotDTO
 from app.modules.octant_rewards.core import calculate_rewards
 from app.modules.snapshots.pending.core import calculate_user_budgets
-from app.modules.user.budgets.core import get_upcoming_budget
-from app.modules.user.events_generator.core import unify_deposit_balances
 from app.modules.staking.proceeds.core import estimate_staking_proceeds
-from app.constants import SABLIER_SENDER_ADDRESS_SEPOLIA, SABLIER_TOKEN_ADDRESS_SEPOLIA, ZERO_ADDRESS
-from app.infrastructure import SubgraphEndpoints
+from app.constants import (
+    ZERO_ADDRESS,
+)
+from app.engine.user.effective_deposit.weighted_average.default import (
+    DefaultWeightedAverageEffectiveDeposit,
+)
 from v2.deposits.repositories import DepositEventsRepository
-from v2.epochs.subgraphs import EpochsSubgraph
-from v2.sablier.subgraphs import SablierSubgraph
-
-
-
-
 
 
 def calculate_effective_deposits(
@@ -38,9 +26,8 @@ def calculate_effective_deposits(
     end_sec: int,
     events: Dict[str, list[DepositEvent]],
 ) -> tuple[list[UserDeposit], int]:
-    
     # TODO: We can do this better and nicer
-    effective_deposit_calculator = DefaultWeightedAverageWithSablierTimebox()
+    effective_deposit_calculator = DefaultWeightedAverageEffectiveDeposit()
     payload = UserEffectiveDepositPayload(
         epoch_start=start_sec,
         epoch_end=end_sec,
@@ -56,20 +43,19 @@ async def calculate_pending_epoch_snapshot(
     epoch_start: int,
     epoch_end: int,
 ) -> PendingSnapshotDTO:
-    
     # Get octant rewards
-        # epoch_details = await epochs_subgraph.fetch_epoch_by_number(epoch_number)
+    # epoch_details = await epochs_subgraph.fetch_epoch_by_number(epoch_number)
     # duration_sec = epoch_details.duration
     # return estimate_staking_proceeds(duration_sec)
     # eth_proceeds = await get_staking_proceeds(session, epoch_number, start_sec, end_sec)
     eth_proceeds = estimate_staking_proceeds(epoch_end - epoch_start)
 
     events = await deposit_events.get_all_users_events(
-        epoch_number,
-        epoch_start,
-        epoch_end
+        epoch_number, epoch_start, epoch_end
+    )
+    user_deposits, total_effective_deposit = calculate_effective_deposits(
+        epoch_start, epoch_end, events
     )
-    user_deposits, total_effective_deposit = calculate_effective_deposits(epoch_start, epoch_end, events)
 
     # total_effective_deposit = 155654569757136462439580980
     rewards_settings = OctantRewardsSettings()
@@ -109,27 +95,9 @@ async def calculate_pending_epoch_snapshot(
     )
 
 
-async def get_budget(
-    session: AsyncSession,
-    epoch_number: int,
-    user_address: str
-) -> int:
-    simulated_snapshot = await calculate_pending_epoch_snapshot(session, epoch_number)
-
-    upcoming_budget = get_upcoming_budget(
-        user_address, simulated_snapshot.user_budgets
-    )
-
-    return upcoming_budget
-
-
 def simulate_user_events(
-    end_sec: int,
-    lock_duration: int,
-    remaining_sec: int,
-    glm_amount: int
+    end_sec: int, lock_duration: int, remaining_sec: int, glm_amount: int
 ) -> list[DepositEvent]:
-
     user_events = [
         DepositEvent(
             user=ZERO_ADDRESS,
@@ -149,4 +117,4 @@ def simulate_user_events(
                 deposit_before=glm_amount,
             )
         )
-    return user_events
\ No newline at end of file
+    return user_events
diff --git a/backend/v2/projects/contracts.py b/backend/v2/projects/contracts.py
index 8656b1a7b1..4f5b733332 100644
--- a/backend/v2/projects/contracts.py
+++ b/backend/v2/projects/contracts.py
@@ -1,14 +1,19 @@
 import logging
 
+from v2.core.types import Address
+from v2.core.transformers import transform_to_checksum_address
 from v2.core.contracts import SmartContract
 
 
 class ProjectsContracts(SmartContract):
-    async def get_project_addresses(self, epoch_number: int) -> list[str]:
+    async def get_project_addresses(self, epoch_number: int) -> list[Address]:
         logging.debug(
             f"[Projects contract] Getting project addresses for epoch: {epoch_number}"
         )
-        return await self.contract.functions.getProposalAddresses(epoch_number).call()
+        addresses = await self.contract.functions.getProposalAddresses(
+            epoch_number
+        ).call()
+        return [transform_to_checksum_address(address) for address in addresses]
 
     async def get_project_cid(self) -> str:
         logging.debug("[Projects contract] Getting projects CID")
diff --git a/backend/v2/projects/dependencies.py b/backend/v2/projects/dependencies.py
index 2f6dc5264f..7dbe08868c 100644
--- a/backend/v2/projects/dependencies.py
+++ b/backend/v2/projects/dependencies.py
@@ -71,13 +71,16 @@ def get_projects_allocation_threshold_getter(
     epoch_number: GetOpenAllocationWindowEpochNumber,
     session: GetSession,
     projects: GetProjectsContracts,
+    settings: Annotated[
+        ProjectsAllocationThresholdSettings,
+        Depends(get_projects_allocation_threshold_settings),
+    ],
 ) -> ProjectsAllocationThresholdGetter:
-    project_count_multiplier = 2 if epoch_number <= 2 else 1
-
     return ProjectsAllocationThresholdGetter(
-        epoch_number, session, projects, project_count_multiplier
+        epoch_number, session, projects, settings.project_count_multiplier
     )
 
+
 async def get_projects_metadata_getter(
     epoch_number: EpochNumberPath,
     projects_contracts: GetProjectsContracts,
@@ -94,9 +97,11 @@ async def get_projects_metadata_getter(
 
 
 async def get_projects_details_getter(
-    session: GetSession, epochs: EpochsParameter, search_phrases: SearchPhrasesParameter
+    session: GetSession,
+    epochs: EpochsParameter,
+    search_phrases_param: SearchPhrasesParameter,
 ) -> ProjectsDetailsGetter:
-    epoch_numbers, search_phrases = process_search_params(epochs, search_phrases)
+    epoch_numbers, search_phrases = process_search_params(epochs, search_phrases_param)
     return ProjectsDetailsGetter(
         session=session, epoch_numbers=epoch_numbers, search_phrases=search_phrases
     )
diff --git a/backend/v2/sablier/dependencies.py b/backend/v2/sablier/dependencies.py
index 0f9ffb4fec..b58600f539 100644
--- a/backend/v2/sablier/dependencies.py
+++ b/backend/v2/sablier/dependencies.py
@@ -3,14 +3,9 @@
 
 from fastapi import Depends
 from pydantic import Field
-from pydantic_settings import SettingsConfigDict
 from app.shared.blockchain_types import ChainTypes
 from v2.sablier.subgraphs import SablierSubgraph
-from v2.core.dependencies import GetChainSettings, GetSession, OctantSettings
-from v2.epochs.dependencies import GetOpenAllocationWindowEpochNumber
-from v2.matched_rewards.dependencies import GetMatchedRewardsEstimator
-from v2.project_rewards.services import ProjectRewardsEstimator
-from v2.projects.dependencies import GetProjectsContracts
+from v2.core.dependencies import GetChainSettings, OctantSettings
 
 
 class SablierSubgraphSettings(OctantSettings):
@@ -48,6 +43,7 @@ def get_sablier_subgraph(chain_settings: GetChainSettings) -> SablierSubgraph:
         sablier_settings.sablier_token_address_sepolia,
     )
 
+
 GetSablierSubgraph = Annotated[
     SablierSubgraph,
     Depends(get_sablier_subgraph),
diff --git a/backend/v2/sablier/subgraphs.py b/backend/v2/sablier/subgraphs.py
index fa2e0357fe..c2b0785044 100644
--- a/backend/v2/sablier/subgraphs.py
+++ b/backend/v2/sablier/subgraphs.py
@@ -10,11 +10,10 @@
 from app.infrastructure.sablier.events import SablierStream
 from v2.epochs.subgraphs import BackoffParams
 
-from gql.transport.aiohttp import log as requests_logger
 requests_logger.setLevel(logging.WARNING)
 
-class SablierSubgraph:
 
+class SablierSubgraph:
     def __init__(
         self,
         url: str,
@@ -22,15 +21,14 @@ def __init__(
         token_address: str,
         backoff_params: BackoffParams | None = None,
     ):
-
-        requests_logger.setLevel(logging.WARNING)    
+        requests_logger.setLevel(logging.WARNING)
         self.url = url
         self.sender = sender
         self.token_address = token_address
 
         self.gql_client = Client(
             transport=AIOHTTPTransport(
-                url=self.url, 
+                url=self.url,
                 timeout=2,
             ),
             fetch_schema_from_transport=False,
@@ -48,9 +46,7 @@ def __init__(
                 self.gql_client.execute_async
             )
 
-
     async def _fetch_streams(self, query: str, variables: dict) -> list[SablierStream]:
-
         all_streams = []
         has_more = True
         limit = 1000

From 14ad8ce66b85d261b3cde1a80defc3d4523c7a9c Mon Sep 17 00:00:00 2001
From: adam-gf <adam@golem.foundation>
Date: Tue, 28 Jan 2025 00:23:23 +0100
Subject: [PATCH 04/14] Adds tests, and addresses some PR comments

---
 backend/tests/v2/factories/__init__.py        |   9 +
 backend/tests/v2/factories/budgets.py         |  54 +++++
 .../tests/v2/factories/finalized_snapshots.py |  66 ++++++
 .../tests/v2/factories/pending_snapshots.py   |  81 +++++++
 backend/tests/v2/fake_subgraphs/epochs.py     |  15 ++
 backend/tests/v2/project_rewards/__init__.py  |   0
 .../project_rewards/test_estimated_budged.py  | 207 ++++++++++++++++++
 .../test_get_budget_for_epoch.py              |  66 ++++++
 .../project_rewards/test_get_epoch_budgets.py | 114 ++++++++++
 .../test_get_rewards_leverage.py              | 191 ++++++++++++++++
 .../test_get_upcoming_budget.py               | 109 +++++++++
 backend/v2/allocations/dependencies.py        |   4 +-
 backend/v2/allocations/services.py            |   2 +-
 backend/v2/allocations/socket.py              |   6 +-
 backend/v2/core/dependencies.py               |   9 +-
 backend/v2/matched_rewards/dependencies.py    |  28 ++-
 ...pped_quadriatic.py => capped_quadratic.py} |  22 +-
 backend/v2/project_rewards/dependencies.py    |   4 +-
 backend/v2/project_rewards/router.py          |  26 ++-
 backend/v2/project_rewards/schemas.py         |   4 +-
 backend/v2/project_rewards/services.py        |  10 +-
 21 files changed, 985 insertions(+), 42 deletions(-)
 create mode 100644 backend/tests/v2/factories/budgets.py
 create mode 100644 backend/tests/v2/factories/finalized_snapshots.py
 create mode 100644 backend/tests/v2/factories/pending_snapshots.py
 create mode 100644 backend/tests/v2/project_rewards/__init__.py
 create mode 100644 backend/tests/v2/project_rewards/test_estimated_budged.py
 create mode 100644 backend/tests/v2/project_rewards/test_get_budget_for_epoch.py
 create mode 100644 backend/tests/v2/project_rewards/test_get_epoch_budgets.py
 create mode 100644 backend/tests/v2/project_rewards/test_get_rewards_leverage.py
 create mode 100644 backend/tests/v2/project_rewards/test_get_upcoming_budget.py
 rename backend/v2/project_rewards/{capped_quadriatic.py => capped_quadratic.py} (91%)

diff --git a/backend/tests/v2/factories/__init__.py b/backend/tests/v2/factories/__init__.py
index 561003f34f..4e5a0647ff 100644
--- a/backend/tests/v2/factories/__init__.py
+++ b/backend/tests/v2/factories/__init__.py
@@ -4,6 +4,9 @@
 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 dataclasses import dataclass
 
 
@@ -23,6 +26,9 @@ class FactoriesAggregator:
     allocation_requests: AllocationRequestFactorySet
     allocations: AllocationFactorySet
     projects_details: ProjectsDetailsFactorySet
+    budgets: BudgetFactorySet
+    pending_snapshots: PendingEpochSnapshotFactorySet
+    finalized_snapshots: FinalizedEpochSnapshotFactorySet
 
     def __init__(self, fast_session: AsyncSession):
         """
@@ -32,3 +38,6 @@ 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)
diff --git a/backend/tests/v2/factories/budgets.py b/backend/tests/v2/factories/budgets.py
new file mode 100644
index 0000000000..a14a0877fe
--- /dev/null
+++ b/backend/tests/v2/factories/budgets.py
@@ -0,0 +1,54 @@
+import random
+from async_factory_boy.factory.sqlalchemy import AsyncSQLAlchemyFactory
+from factory import LazyAttribute
+from sqlalchemy.ext.asyncio import AsyncSession
+
+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 
\ No newline at end of file
diff --git a/backend/tests/v2/factories/finalized_snapshots.py b/backend/tests/v2/factories/finalized_snapshots.py
new file mode 100644
index 0000000000..c66d967687
--- /dev/null
+++ b/backend/tests/v2/factories/finalized_snapshots.py
@@ -0,0 +1,66 @@
+import random
+from async_factory_boy.factory.sqlalchemy import AsyncSQLAlchemyFactory
+from factory import LazyAttribute
+from sqlalchemy.ext.asyncio import AsyncSession
+
+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 
\ No newline at end of file
diff --git a/backend/tests/v2/factories/pending_snapshots.py b/backend/tests/v2/factories/pending_snapshots.py
new file mode 100644
index 0000000000..9abaa33e71
--- /dev/null
+++ b/backend/tests/v2/factories/pending_snapshots.py
@@ -0,0 +1,81 @@
+import random
+from async_factory_boy.factory.sqlalchemy import AsyncSQLAlchemyFactory
+from factory import LazyAttribute
+from sqlalchemy.ext.asyncio import AsyncSession
+
+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 
\ No newline at end of file
diff --git a/backend/tests/v2/fake_subgraphs/epochs.py b/backend/tests/v2/fake_subgraphs/epochs.py
index 758a50943d..71c47c6239 100644
--- a/backend/tests/v2/fake_subgraphs/epochs.py
+++ b/backend/tests/v2/fake_subgraphs/epochs.py
@@ -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
 
@@ -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.
diff --git a/backend/tests/v2/project_rewards/__init__.py b/backend/tests/v2/project_rewards/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/backend/tests/v2/project_rewards/test_estimated_budged.py b/backend/tests/v2/project_rewards/test_estimated_budged.py
new file mode 100644
index 0000000000..9a38b870a0
--- /dev/null
+++ b/backend/tests/v2/project_rewards/test_estimated_budged.py
@@ -0,0 +1,207 @@
+from unittest.mock import MagicMock
+import pytest
+from httpx import AsyncClient
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from v2.matched_rewards.services import MatchedRewardsEstimator
+from v2.glms.dependencies import get_glm_balance_of_deposits
+from tests.v2.factories import FactoriesAggregator
+from tests.v2.fake_contracts.helpers import FakeEpochsContractDetails
+from tests.v2.fake_contracts.conftest import fake_epochs_contract_factory, FakeEpochsContractCallable
+from v2.matched_rewards.dependencies import GetMatchedRewardsEstimatorInAW, get_matched_rewards_estimator_only_in_aw
+from fastapi import FastAPI
+
+
+@pytest.mark.asyncio
+async def test_estimates_budget_for_single_epoch(
+    fake_epochs_contract_factory: FakeEpochsContractCallable,
+    fast_app: FastAPI,
+    fast_client: AsyncClient,
+    fast_session: AsyncSession,
+    factories: FactoriesAggregator,
+):
+    """Should correctly estimate budget for a single epoch deposit
+    We are testing this while in finalized epoch.
+    """
+    # Given
+    glm_amount = 1000 * 10**18  # 1000 GLM
+    epoch_duration = 90 * 24 * 60 * 60  # 90 days in seconds
+    epoch_start = 1000
+    finalized_epoch = 1
+    glm_balance = 5000 * 10**18  # 5000 GLM total in contract
+
+    # Mock contracts
+    fake_epochs_contract_factory(
+        FakeEpochsContractDetails(
+            finalized_epoch=finalized_epoch,
+            current_epoch=finalized_epoch + 1,
+            pending_epoch=None,
+            future_epoch_props=(0, 0, epoch_start, epoch_duration)
+        )
+    )
+
+    # Create pending epoch snapshot
+    pending_snapshot = await factories.pending_snapshots.create(
+        epoch=finalized_epoch
+    )
+
+    # finalize epoch snapshot
+    finalized_snapshot = await factories.finalized_snapshots.create(
+        epoch=finalized_epoch,
+    )
+
+    # Mock GLM balance
+    fast_app.dependency_overrides[get_glm_balance_of_deposits] = lambda: glm_balance
+
+    # Mock matched rewards estimator
+    # rewards_estimator_mock = MagicMock(spec=MatchedRewardsEstimator)
+    # rewards_estimator_mock.get.return_value = leverage
+    # fast_app.dependency_overrides[get_matched_rewards_estimator] = lambda: rewards_estimator_mock
+
+    request_data = {
+        "glm_amount": str(glm_amount),
+        "number_of_epochs": 1
+    }
+
+    async with fast_client as client:
+        resp = await client.post("/rewards/estimated_budget", json=request_data)
+        
+        print("resp", resp.json())
+        assert resp.status_code == 200
+        response = resp.json()
+        assert response["budget"] != "0"
+        assert response["matchedFunding"] == "0"
+        
+
+@pytest.mark.asyncio
+async def test_estimates_budget_for_multiple_epochs(
+    fake_epochs_contract_factory: FakeEpochsContractCallable,
+    fast_app: FastAPI,
+    fast_client: AsyncClient,
+    fast_session: AsyncSession,
+    factories: FactoriesAggregator,
+):
+    """Should correctly estimate budget for multiple epochs"""
+    # Given
+    glm_amount = 1000 * 10**18
+    number_of_epochs = 1
+    epoch_duration = 90 * 24 * 60 * 60
+    epoch_start = 1000
+    finalized_epoch = 1
+    glm_balance = 5000 * 10**18
+
+    alice = await factories.users.get_or_create_alice()
+    bob = await factories.users.get_or_create_bob()
+    alice_allocation = await factories.allocations.create(
+        user=alice,
+        epoch=finalized_epoch,
+    )
+    bob_allocation = await factories.allocations.create(
+        user=bob,
+        epoch=finalized_epoch,
+    )
+
+        # Create pending epoch snapshot
+    pending_snapshot = await factories.pending_snapshots.create(
+        epoch=finalized_epoch
+    )
+
+    # finalize epoch snapshot
+    finalized_snapshot = await factories.finalized_snapshots.create(
+        epoch=finalized_epoch,
+    )
+
+    # Mock contracts
+    fake_epochs_contract_factory(
+        FakeEpochsContractDetails(
+            finalized_epoch=finalized_epoch,
+            future_epoch_props=(0, 0, epoch_start, epoch_duration)
+        )
+    )
+
+    # Mock dependencies
+    fast_app.dependency_overrides[get_glm_balance_of_deposits] = lambda: glm_balance
+
+    # Mock matched rewards estimator
+    # rewards_estimator_mock = MagicMock(spec=GetMatchedRewardsEstimatorInAW)
+    # rewards_estimator_mock.get.return_value = leverage
+    # fast_app.dependency_overrides[GetMatchedRewardsEstimatorInAW] = lambda: rewards_estimator_mock
+
+    request_data = {
+        "glm_amount": str(glm_amount),
+        "number_of_epochs": number_of_epochs
+    }
+
+    async with fast_client as client:
+        resp = await client.post("/rewards/estimated_budget", json=request_data)
+        print("result", resp.json())
+        
+        assert resp.status_code == 200
+        result = resp.json()
+        
+        assert result["budget"] != "0"
+        assert result["matchedFunding"] != "0"
+        leverage = int(finalized_snapshot.matched_rewards) / (int(alice_allocation.amount) + int(bob_allocation.amount))
+        assert int(result["matchedFunding"]) == int(int(result["budget"]) * leverage)
+
+        one_epoch_budget = int(result["budget"])
+        one_epoch_matched_funding = int(result["matchedFunding"])
+
+        # Test two epochs
+        request_data = {
+            "glm_amount": str(glm_amount),
+            "number_of_epochs": 2
+        }
+
+        resp = await client.post("/rewards/estimated_budget", json=request_data)
+        
+        assert resp.status_code == 200
+        result = resp.json()
+        print("result", result)
+        assert int(result["budget"]) != "0"
+        assert int(result["matchedFunding"]) != "0"
+
+        assert int(result["budget"]) == one_epoch_budget * 2
+        assert int(result["matchedFunding"]) == one_epoch_matched_funding * 2
+
+
+        # Test zero GLM amount
+        request_data = {
+            "glm_amount": str(0),
+            "number_of_epochs": 1
+        }
+
+        resp = await client.post("/rewards/estimated_budget", json=request_data)
+        
+        assert resp.status_code == 200
+        result = resp.json()
+        assert int(result["budget"]) == 0
+        assert int(result["matchedFunding"]) == 0
+
+
+@pytest.mark.asyncio
+async def test_validates_request_parameters(
+    fast_client: AsyncClient,
+):
+    """Should validate request parameters"""
+    async with fast_client as client:
+        # Test negative GLM amount
+        resp = await client.post("/rewards/estimated_budget", json={
+            "glm_amount": "-1000",
+            "number_of_epochs": 1
+        })
+        assert resp.status_code == 422
+
+        # Test zero epochs
+        resp = await client.post("/rewards/estimated_budget", json={
+            "glm_amount": "1000",
+            "number_of_epochs": 0
+        })
+        assert resp.status_code == 422
+
+        # Test negative epochs
+        resp = await client.post("/rewards/estimated_budget", json={
+            "glm_amount": "1000",
+            "number_of_epochs": -1
+        })
+        assert resp.status_code == 422
\ No newline at end of file
diff --git a/backend/tests/v2/project_rewards/test_get_budget_for_epoch.py b/backend/tests/v2/project_rewards/test_get_budget_for_epoch.py
new file mode 100644
index 0000000000..776c5a26f8
--- /dev/null
+++ b/backend/tests/v2/project_rewards/test_get_budget_for_epoch.py
@@ -0,0 +1,66 @@
+import pytest
+from fastapi import status
+from httpx import AsyncClient
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from tests.v2.factories import FactoriesAggregator
+
+"""Test cases for the GET /rewards/budget/{user_address}/epoch/{epoch_number} endpoint"""
+
+
+@pytest.mark.asyncio
+async def test_returns_none_when_no_budget(
+    fast_client: AsyncClient, fast_session: AsyncSession, factories: FactoriesAggregator
+):
+    """Should return 204 No Content when user has no budget for the epoch"""
+    # Given: a user with no budget
+    alice = await factories.users.get_or_create_alice()
+
+    async with fast_client as client:
+        resp = await client.get(f"rewards/budget/{alice.address}/epoch/1")
+        assert resp.status_code == status.HTTP_204_NO_CONTENT
+        assert resp.text == ""
+
+
+@pytest.mark.asyncio
+async def test_returns_budget_when_it_exists(
+    fast_client: AsyncClient, factories: FactoriesAggregator
+):
+    """Should return user's budget when it exists for the epoch"""
+
+    # Given: users with budgets for different epochs
+    alice = await factories.users.get_or_create_alice()
+    budget1 = await factories.budgets.create(user=alice, epoch=1)
+    budget2 = await factories.budgets.create(user=alice, epoch=2)
+
+    async with fast_client as client:
+        # Budget for epoch 1
+        resp = await client.get(f"rewards/budget/{alice.address}/epoch/1")
+        assert resp.status_code == status.HTTP_200_OK
+        assert resp.json() == {"budget": budget1.budget}
+
+        # Budget for epoch 2
+        resp = await client.get(f"rewards/budget/{alice.address}/epoch/2")
+        assert resp.status_code == status.HTTP_200_OK
+        assert resp.json() == {"budget": budget2.budget}
+
+        # No budget for epoch 3
+        resp = await client.get(f"rewards/budget/{alice.address}/epoch/3")
+        assert resp.status_code == status.HTTP_204_NO_CONTENT
+        assert resp.text == ""
+
+
+@pytest.mark.asyncio
+async def test_returns_zero_budget_when_amount_is_zero(
+    fast_client: AsyncClient, factories: FactoriesAggregator
+):
+    """Should return budget even when amount is zero"""
+
+    # Given: a user with zero budget
+    alice = await factories.users.get_or_create_alice()
+    budget = await factories.budgets.create(user=alice, epoch=1, amount=0)
+
+    async with fast_client as client:
+        resp = await client.get(f"rewards/budget/{alice.address}/epoch/1")
+        assert resp.status_code == status.HTTP_200_OK
+        assert resp.json() == {"budget": "0"}
diff --git a/backend/tests/v2/project_rewards/test_get_epoch_budgets.py b/backend/tests/v2/project_rewards/test_get_epoch_budgets.py
new file mode 100644
index 0000000000..46f7ce52c0
--- /dev/null
+++ b/backend/tests/v2/project_rewards/test_get_epoch_budgets.py
@@ -0,0 +1,114 @@
+import pytest
+from httpx import AsyncClient
+from sqlalchemy.ext.asyncio import AsyncSession
+from tests.v2.factories import FactoriesAggregator
+
+
+@pytest.mark.asyncio
+async def test_returns_empty_budgets_when_no_users(
+    fast_client: AsyncClient,
+    fast_session: AsyncSession,
+):
+    """Should return empty budgets list when no users have budgets for the epoch"""
+    
+    async with fast_client as client:
+        resp = await client.get("/rewards/budgets/epoch/1")
+        assert resp.status_code == 200
+        assert resp.json() == {"budgets": []}
+
+
+@pytest.mark.asyncio
+async def test_returns_all_user_budgets_for_epoch(
+    fast_client: AsyncClient,
+    fast_session: AsyncSession,
+    factories: FactoriesAggregator,
+):
+    """Should return budgets for all users in the given epoch"""
+    
+    # Given: multiple users with budgets in different epochs
+    alice = await factories.users.get_or_create_alice()
+    bob = await factories.users.get_or_create_bob()
+    charlie = await factories.users.get_or_create_charlie()
+
+    # Create budgets for different epochs
+    alice_budget1 = await factories.budgets.create(user=alice, epoch=1, amount=100)
+    bob_budget1 = await factories.budgets.create(user=bob, epoch=1, amount=200)
+    charlie_budget1 = await factories.budgets.create(user=charlie, epoch=1, amount=300)
+    
+    # Create budgets for a different epoch
+    await factories.budgets.create(user=alice, epoch=2, amount=400)
+    await factories.budgets.create(user=bob, epoch=2, amount=500)
+
+    async with fast_client as client:
+        # When: requesting budgets for epoch 1
+        resp = await client.get("/rewards/budgets/epoch/1")
+        
+        # Then: should return all budgets for epoch 1
+        assert resp.status_code == 200
+        budgets = resp.json()["budgets"]
+        
+        # Convert to set for order-independent comparison
+        expected_budgets = [
+            {"address": alice.address, "amount": str(alice_budget1.budget)},
+            {"address": bob.address, "amount": str(bob_budget1.budget)},
+            {"address": charlie.address, "amount": str(charlie_budget1.budget)},
+        ]
+        assert len(budgets) == 3
+        assert {(b["address"], b["amount"]) for b in budgets} == {
+            (b["address"], b["amount"]) for b in expected_budgets
+        }
+
+
+@pytest.mark.asyncio
+async def test_returns_empty_budgets_for_future_epoch(
+    fast_client: AsyncClient,
+    fast_session: AsyncSession,
+    factories: FactoriesAggregator,
+):
+    """Should return empty budgets list for an epoch with no budgets"""
+    
+    # Given: users with budgets in epoch 1
+    alice = await factories.users.get_or_create_alice()
+    bob = await factories.users.get_or_create_bob()
+    
+    await factories.budgets.create(user=alice, epoch=1, amount=100)
+    await factories.budgets.create(user=bob, epoch=1, amount=200)
+
+    async with fast_client as client:
+        # When: requesting budgets for epoch 2 (which has no budgets)
+        resp = await client.get("/rewards/budgets/epoch/2")
+        
+        # Then: should return empty list
+        assert resp.status_code == 200
+        assert resp.json() == {"budgets": []}
+
+
+@pytest.mark.asyncio
+async def test_includes_zero_budgets(
+    fast_client: AsyncClient,
+    fast_session: AsyncSession,
+    factories: FactoriesAggregator,
+):
+    """Should include users with zero budgets in the response"""
+    
+    # Given: users with zero and non-zero budgets
+    alice = await factories.users.get_or_create_alice()
+    bob = await factories.users.get_or_create_bob()
+    
+    alice_budget = await factories.budgets.create(user=alice, epoch=1, amount=100)
+    bob_budget = await factories.budgets.create(user=bob, epoch=1, amount=0)
+
+    async with fast_client as client:
+        resp = await client.get("/rewards/budgets/epoch/1")
+        
+        assert resp.status_code == 200
+        budgets = resp.json()["budgets"]
+        
+        expected_budgets = [
+            {"address": alice.address, "amount": str(alice_budget.budget)},
+            {"address": bob.address, "amount": "0"},
+        ]
+        assert len(budgets) == 2
+        assert {(b["address"], b["amount"]) for b in budgets} == {
+            (b["address"], b["amount"]) for b in expected_budgets
+        }
\ No newline at end of file
diff --git a/backend/tests/v2/project_rewards/test_get_rewards_leverage.py b/backend/tests/v2/project_rewards/test_get_rewards_leverage.py
new file mode 100644
index 0000000000..3fe0d40093
--- /dev/null
+++ b/backend/tests/v2/project_rewards/test_get_rewards_leverage.py
@@ -0,0 +1,191 @@
+from fastapi import FastAPI
+import pytest
+from http import HTTPStatus
+from unittest.mock import MagicMock
+from httpx import AsyncClient
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from app.context.epoch_state import EpochState
+from app.exceptions import InvalidEpoch, MissingSnapshot, NotImplementedForGivenEpochState
+from v2.matched_rewards.services import MatchedRewardsEstimator
+from tests.v2.factories import FactoriesAggregator
+from tests.v2.fake_contracts.helpers import FakeEpochsContractDetails
+from tests.v2.fake_contracts.conftest import FakeEpochsContractCallable
+from v2.matched_rewards.dependencies import GetMatchedRewardsEstimator, get_matched_rewards_estimator
+from tests.v2.fake_contracts.conftest import fake_epochs_contract_factory, FakeEpochsContractCallable
+
+
+@pytest.mark.asyncio
+async def test_returns_leverage_for_finalized_epoch(
+    fake_epochs_contract_factory: FakeEpochsContractCallable,
+    fast_client: AsyncClient,
+    fast_session: AsyncSession,
+    factories: FactoriesAggregator,
+):
+    """Should return leverage based on finalized snapshot for finalized epoch"""
+    # Given: a finalized epoch with snapshot and allocations
+    epoch_number = 1
+
+    # Mock contracts to return finalized state
+    fake_epochs_contract_factory(
+        FakeEpochsContractDetails(
+            current_epoch=epoch_number + 1,  # Current epoch is ahead
+            finalized_epoch=epoch_number,
+            pending_epoch=None,
+        )
+    )
+
+    # Create pending snapshot
+    pending_snapshot = await factories.pending_snapshots.create(
+        epoch=epoch_number,
+    )
+
+    # Create finalized snapshot
+    finalized_snapshot = await factories.finalized_snapshots.create(
+        epoch=epoch_number,
+    )
+
+    # Create some allocations
+    alice = await factories.users.get_or_create_alice()
+    alice_allocation = await factories.allocations.create(
+        user=alice,
+        epoch=epoch_number,
+    )
+
+    async with fast_client as client:
+        resp = await client.get(f"rewards/leverage/{epoch_number}")
+        assert resp.status_code == HTTPStatus.OK
+        resp_leverage = int(resp.json()["leverage"])
+        expected_leverage = int(int(finalized_snapshot.matched_rewards) / int(alice_allocation.amount))
+        assert resp_leverage == expected_leverage
+
+
+@pytest.mark.asyncio
+async def test_returns_leverage_for_pending_epoch(
+    fake_epochs_contract_factory: FakeEpochsContractCallable,
+    fast_client: AsyncClient,
+    fast_session: AsyncSession,
+    factories: FactoriesAggregator,
+    fast_app: FastAPI,
+):
+    """Should return leverage based on estimated rewards for pending epoch"""
+    # Given: a pending epoch with allocations
+    epoch_number = 1
+    estimated_rewards = 100_000_000
+
+    # Mock contracts to return pending state
+    fake_epochs_contract_factory(
+        FakeEpochsContractDetails(
+            current_epoch=epoch_number + 1,  # Current epoch is ahead
+            finalized_epoch=epoch_number,
+            pending_epoch=None,
+        )
+    )
+
+    # Create pending snapshot
+    pending_snapshot = await factories.pending_snapshots.create(
+        epoch=epoch_number,
+    )
+
+    # Create some allocations
+    alice = await factories.users.get_or_create_alice()
+    alice_allocation = await factories.allocations.create(
+        user=alice,
+        epoch=epoch_number,
+    )
+
+    # Mock rewards estimator
+    rewards_estimator = MagicMock(spec=MatchedRewardsEstimator)
+    rewards_estimator.get.return_value = estimated_rewards
+    fast_app.dependency_overrides[get_matched_rewards_estimator] = lambda: rewards_estimator
+
+    async with fast_client as client:
+        resp = await client.get(f"rewards/leverage/{epoch_number}")
+        assert resp.status_code == HTTPStatus.OK
+        assert resp.json() == {"leverage": estimated_rewards / alice_allocation.amount}
+
+
+@pytest.mark.asyncio
+async def test_returns_zero_leverage_when_no_allocations(
+    fake_epochs_contract_factory: FakeEpochsContractCallable,
+    fast_client: AsyncClient,
+    fast_session: AsyncSession,
+    factories: FactoriesAggregator,
+):
+    """Should return zero leverage when there are no allocations"""
+    # Given: a finalized epoch with snapshot but no allocations
+    epoch_number = 1
+    matched_rewards = 1000 * 10**18
+
+    # Mock contracts to return finalized state
+    fake_epochs_contract_factory(
+        FakeEpochsContractDetails(
+            current_epoch=2,
+            finalized_epoch=epoch_number,
+        )
+    )
+
+    # Create pending snapshot
+    pending_snapshot = await factories.pending_snapshots.create(
+        epoch=epoch_number,
+    )
+
+    # Create finalized snapshot
+    finalized_snapshot = await factories.finalized_snapshots.create(
+        epoch=epoch_number,
+        matched_rewards=matched_rewards,
+    )
+
+    async with fast_client as client:
+        resp = await client.get(f"rewards/leverage/{epoch_number}")
+        assert resp.status_code == HTTPStatus.OK
+        assert resp.json() == {"leverage": 0}
+
+
+@pytest.mark.asyncio
+async def test_raises_error_when_snapshot_missing(
+    fake_epochs_contract_factory: FakeEpochsContractCallable,
+    fast_client: AsyncClient,
+    fast_session: AsyncSession,
+):
+    """Should raise MissingSnapshot when finalized snapshot is missing"""
+    # Given: a finalized epoch without snapshot
+    epoch_number = 1
+
+    # Mock contracts to return finalized state
+    fake_epochs_contract_factory(
+        FakeEpochsContractDetails(
+            current_epoch=2,
+            finalized_epoch=epoch_number,
+        )
+    )
+
+    async with fast_client as client:
+        resp = await client.get(f"rewards/leverage/{epoch_number}")
+        assert resp.status_code == HTTPStatus.BAD_REQUEST
+        assert resp.json() == {"message": InvalidEpoch.description}
+
+
+@pytest.mark.asyncio
+async def test_raises_error_for_invalid_epoch_state(
+    fake_epochs_contract_factory: FakeEpochsContractCallable,
+    fast_client: AsyncClient,
+    fast_session: AsyncSession,
+):
+    """Should raise NotImplementedForGivenEpochState for invalid epoch state"""
+    # Given: an epoch in invalid state (after PENDING)
+    epoch_number = 1
+
+    # Mock contracts to return future epoch
+    fake_epochs_contract_factory(
+        FakeEpochsContractDetails(
+            current_epoch=1,
+            finalized_epoch=0,
+            future_epoch_props=(0, 0, 2000, 1000),  # Future epoch
+        )
+    )
+
+    async with fast_client as client:
+        resp = await client.get(f"rewards/leverage/{epoch_number}")
+        assert resp.status_code == HTTPStatus.BAD_REQUEST
+        assert resp.json() == {"message": NotImplementedForGivenEpochState.description} 
\ No newline at end of file
diff --git a/backend/tests/v2/project_rewards/test_get_upcoming_budget.py b/backend/tests/v2/project_rewards/test_get_upcoming_budget.py
new file mode 100644
index 0000000000..cfad796046
--- /dev/null
+++ b/backend/tests/v2/project_rewards/test_get_upcoming_budget.py
@@ -0,0 +1,109 @@
+from unittest.mock import MagicMock
+import pytest
+from http import HTTPStatus
+from httpx import AsyncClient
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from app.engine.user.effective_deposit import DepositEvent, DepositSource, EventType
+from v2.deposits.dependencies import GetDepositEventsRepository, get_deposit_events_repository
+from tests.v2.factories import FactoriesAggregator
+from tests.v2.fake_contracts.helpers import FakeEpochsContractDetails
+from tests.v2.fake_contracts.conftest import fake_epochs_contract_factory, FakeEpochsContractCallable
+from tests.v2.fake_subgraphs.conftest import fake_epochs_subgraph_factory, FakeEpochsSubgraphCallable
+from tests.v2.fake_subgraphs.helpers import FakeEpochEventDetails
+from fastapi import FastAPI
+from v2.core.dependencies import get_current_timestamp
+
+
+@pytest.mark.asyncio
+async def test_returns_upcoming_budget_with_no_deposits(
+    fake_epochs_contract_factory: FakeEpochsContractCallable,
+    fake_epochs_subgraph_factory: FakeEpochsSubgraphCallable,
+    fast_app: FastAPI,
+    fast_client: AsyncClient,
+    fast_session: AsyncSession,
+    factories: FactoriesAggregator,
+):
+    """Should return zero budget when user has no deposits"""
+    # Given: a user and mocked epoch data
+    alice = await factories.users.get_or_create_alice()
+    current_epoch = 1
+    current_timestamp = 1000
+    epoch_start = 0
+
+    # Mock contracts and subgraph
+    fake_epochs_contract_factory(
+        FakeEpochsContractDetails(current_epoch=current_epoch)
+    )
+    
+    # Mock subgraph response
+    fake_epochs_subgraph_factory([
+        FakeEpochEventDetails(
+            epoch=current_epoch,
+            from_ts=epoch_start,
+            to_ts=2000,
+        )
+    ])
+
+    events_repository = MagicMock(spec=GetDepositEventsRepository)
+    events_repository.get_all_users_events.return_value = {}
+    fast_app.dependency_overrides[get_deposit_events_repository] = lambda: events_repository
+
+    print(f"Current timestamp: {current_timestamp}")
+    async with fast_client as client:
+        resp = await client.get(f"rewards/budget/{alice.address}/upcoming")
+        assert resp.json() == {"upcomingBudget": "0"}
+        assert resp.status_code == HTTPStatus.OK
+
+
+@pytest.mark.asyncio
+async def test_returns_upcoming_budget_with_deposits(
+    fake_epochs_contract_factory: FakeEpochsContractCallable,
+    fake_epochs_subgraph_factory: FakeEpochsSubgraphCallable,
+    fast_client: AsyncClient,
+    fast_session: AsyncSession,
+    factories: FactoriesAggregator,
+    fast_app: FastAPI,
+):
+    """Should calculate and return upcoming budget based on user's deposits"""
+    # Given: a user and mocked epoch data
+    alice = await factories.users.get_or_create_alice()
+    current_epoch = 1
+    current_timestamp = 1000
+    epoch_start = 0
+    deposit_amount = 100 * 10**18  # 100 tokens
+
+    # Mock contracts and subgraph
+    fake_epochs_contract_factory(
+        FakeEpochsContractDetails(current_epoch=current_epoch)
+    )
+    
+    # Mock subgraph response
+    fake_epochs_subgraph_factory([
+        FakeEpochEventDetails(
+            epoch=current_epoch,
+            from_ts=epoch_start,
+            to_ts=2000,
+        )
+    ])
+
+    events_repository = MagicMock(spec=GetDepositEventsRepository)
+    events_repository.get_all_users_events.return_value = {
+        alice.address: [
+            DepositEvent(
+                user=alice.address,
+                type=EventType.LOCK,
+        timestamp=epoch_start + 100,
+                amount=deposit_amount,
+                deposit_before=0,
+                source=DepositSource.OCTANT,
+    )
+        ]
+    }
+    fast_app.dependency_overrides[get_deposit_events_repository] = lambda: events_repository
+    fast_app.dependency_overrides[get_current_timestamp] = lambda: current_timestamp
+
+    async with fast_client as client:
+        resp = await client.get(f"rewards/budget/{alice.address}/upcoming")
+        assert resp.status_code == HTTPStatus.OK
+        assert resp.json() != {"upcomingBudget": "0"} 
diff --git a/backend/v2/allocations/dependencies.py b/backend/v2/allocations/dependencies.py
index df1ea54741..d2aca5e4ba 100644
--- a/backend/v2/allocations/dependencies.py
+++ b/backend/v2/allocations/dependencies.py
@@ -5,7 +5,7 @@
 from v2.allocations.validators import SignatureVerifier
 from v2.core.dependencies import GetChainSettings, GetSession
 from v2.epochs.dependencies import GetEpochsSubgraph, GetOpenAllocationWindowEpochNumber
-from v2.matched_rewards.dependencies import GetMatchedRewardsEstimator
+from v2.matched_rewards.dependencies import GetMatchedRewardsEstimatorInAW
 from v2.projects.dependencies import GetProjectsContracts
 from v2.uniqueness_quotients.dependencies import GetUQScoreGetter
 
@@ -30,7 +30,7 @@ async def get_allocator(
     signature_verifier: GetSignatureVerifier,
     uq_score_getter: GetUQScoreGetter,
     projects_contracts: GetProjectsContracts,
-    matched_rewards_estimator: GetMatchedRewardsEstimator,
+    matched_rewards_estimator: GetMatchedRewardsEstimatorInAW,
 ) -> Allocator:
     return Allocator(
         session,
diff --git a/backend/v2/allocations/services.py b/backend/v2/allocations/services.py
index a5b15dc9ef..46e1c8b06d 100644
--- a/backend/v2/allocations/services.py
+++ b/backend/v2/allocations/services.py
@@ -10,7 +10,7 @@
 from v2.allocations.schemas import AllocationWithUserUQScore, UserAllocationRequest
 from v2.allocations.validators import SignatureVerifier
 from v2.matched_rewards.services import MatchedRewardsEstimator
-from v2.project_rewards.capped_quadriatic import cqf_simulate_leverage
+from v2.project_rewards.capped_quadratic import cqf_simulate_leverage
 from v2.projects.contracts import ProjectsContracts
 from v2.uniqueness_quotients.dependencies import UQScoreGetter
 from v2.users.repositories import get_user_by_address
diff --git a/backend/v2/allocations/socket.py b/backend/v2/allocations/socket.py
index fee236a31f..20dd00411e 100644
--- a/backend/v2/allocations/socket.py
+++ b/backend/v2/allocations/socket.py
@@ -26,7 +26,7 @@
     get_open_allocation_window_epoch_number,
 )
 from v2.matched_rewards.dependencies import (
-    get_matched_rewards_estimator,
+    get_matched_rewards_estimator_only_in_aw,
     get_matched_rewards_estimator_settings,
 )
 from v2.project_rewards.dependencies import get_project_rewards_estimator
@@ -74,7 +74,7 @@ async def create_dependencies_on_connect() -> AsyncGenerator[
                 projects_contracts,
                 get_projects_allocation_threshold_settings(),
             )
-            estimated_matched_rewards = await get_matched_rewards_estimator(
+            estimated_matched_rewards = await get_matched_rewards_estimator_only_in_aw(
                 epoch_number,
                 session,
                 epochs_subgraph,
@@ -131,7 +131,7 @@ async def create_dependencies_on_allocate() -> AsyncGenerator[
                 projects_contracts,
                 get_projects_allocation_threshold_settings(),
             )
-            estimated_matched_rewards = await get_matched_rewards_estimator(
+            estimated_matched_rewards = await get_matched_rewards_estimator_only_in_aw(
                 epoch_number,
                 session,
                 epochs_subgraph,
diff --git a/backend/v2/core/dependencies.py b/backend/v2/core/dependencies.py
index afd3dbe5ba..508138d5a0 100644
--- a/backend/v2/core/dependencies.py
+++ b/backend/v2/core/dependencies.py
@@ -10,6 +10,7 @@
 from web3.middleware import async_geth_poa_middleware
 
 from app.shared.blockchain_types import ChainTypes
+from v2.core.logic import compare_blockchain_types
 
 
 class OctantSettings(BaseSettings):
@@ -110,9 +111,11 @@ class ChainSettings(OctantSettings):
         description="The chain id to use for the signature verification.",
     )
 
-    @property
-    def is_mainnet(self) -> bool:
-        return self.chain_id == ChainTypes.MAINNET
+    is_mainnet: bool = Field(
+        default_factory=lambda: compare_blockchain_types(
+            Field(validation_alias="chain_id"), ChainTypes.MAINNET
+        )
+    )
 
 
 def get_chain_settings() -> ChainSettings:
diff --git a/backend/v2/matched_rewards/dependencies.py b/backend/v2/matched_rewards/dependencies.py
index cdafa0ed3f..668ec571e4 100644
--- a/backend/v2/matched_rewards/dependencies.py
+++ b/backend/v2/matched_rewards/dependencies.py
@@ -28,7 +28,7 @@ def get_matched_rewards_estimator_settings() -> MatchedRewardsEstimatorSettings:
     return MatchedRewardsEstimatorSettings()
 
 
-async def get_matched_rewards_estimator(
+async def get_matched_rewards_estimator_only_in_aw(
     epoch_number: GetOpenAllocationWindowEpochNumber,
     session: GetSession,
     epochs_subgraph: Annotated[EpochsSubgraph, Depends(get_epochs_subgraph)],
@@ -47,7 +47,31 @@ async def get_matched_rewards_estimator(
     )
 
 
+async def get_matched_rewards_estimator(
+    epoch_number: int,
+    session: GetSession,
+    epochs_subgraph: Annotated[EpochsSubgraph, Depends(get_epochs_subgraph)],
+    settings: Annotated[
+        MatchedRewardsEstimatorSettings,
+        Depends(get_matched_rewards_estimator_settings),
+    ],
+) -> MatchedRewardsEstimator:
+    return MatchedRewardsEstimator(
+        session=session,
+        epochs_subgraph=epochs_subgraph,
+        tr_percent=settings.TR_PERCENT,
+        ire_percent=settings.IRE_PERCENT,
+        matched_rewards_percent=settings.MATCHED_REWARDS_PERCENT,
+        epoch_number=epoch_number,
+    )
+
+
+GetMatchedRewardsEstimatorInAW = Annotated[
+    MatchedRewardsEstimator,
+    Depends(get_matched_rewards_estimator_only_in_aw),
+]
+
 GetMatchedRewardsEstimator = Annotated[
     MatchedRewardsEstimator,
     Depends(get_matched_rewards_estimator),
-]
+]
\ No newline at end of file
diff --git a/backend/v2/project_rewards/capped_quadriatic.py b/backend/v2/project_rewards/capped_quadratic.py
similarity index 91%
rename from backend/v2/project_rewards/capped_quadriatic.py
rename to backend/v2/project_rewards/capped_quadratic.py
index 22751b3b17..00fbf2a959 100644
--- a/backend/v2/project_rewards/capped_quadriatic.py
+++ b/backend/v2/project_rewards/capped_quadratic.py
@@ -8,21 +8,21 @@
 from v2.project_rewards.schemas import ProjectFundingSummaryV1
 
 
-class CappedQuadriaticFunding(NamedTuple):
+class CappedQuadraticFunding(NamedTuple):
     project_fundings: dict[Address, ProjectFundingSummaryV1]
-    amounts_total: Decimal  # Sum of all allocation amounts for all projects
-    matched_total: Decimal  # Sum of all matched rewards for all projects
+    allocations_total_for_all_projects: Decimal  # Sum of all allocation amounts for all projects
+    matched_total_for_all_projects: Decimal  # Sum of all matched rewards for all projects
 
 
 MR_FUNDING_CAP_PERCENT = Decimal("0.2")
 
 
-def capped_quadriatic_funding(
+def capped_quadratic_funding(
     allocations: list[AllocationWithUserUQScore],
     matched_rewards: int,
     project_addresses: list[str],
     MR_FUNDING_CAP_PERCENT: Decimal = MR_FUNDING_CAP_PERCENT,
-) -> CappedQuadriaticFunding:
+) -> CappedQuadraticFunding:
     """
     Calculate capped quadratic funding based on a list of allocations.
 
@@ -33,7 +33,7 @@ def capped_quadriatic_funding(
         MR_FUNDING_CAP_PERCENT (float, optional): The maximum percentage of matched rewards that any single project can receive. Defaults to MR_FUNDING_CAP_PERCENT.
 
     Returns:
-        CappedQuadriaticFunding: A named tuple containing the total and per-project amounts and matched rewards.
+        CappedQuadraticFunding: A named tuple containing the total and per-project amounts and matched rewards.
     """
 
     # Group allocations by project
@@ -104,7 +104,7 @@ def capped_quadriatic_funding(
         for project_address in project_addresses
     }
 
-    return CappedQuadriaticFunding(
+    return CappedQuadraticFunding(
         project_fundings=project_fundings,
         amounts_total=amounts_total,
         matched_total=matched_total,
@@ -121,8 +121,8 @@ def cqf_calculate_total_leverage(matched_rewards: int, total_allocated: int) ->
 def cqf_calculate_individual_leverage(
     new_allocations_amount: int,
     project_addresses: list[Address],
-    before_allocation: CappedQuadriaticFunding,
-    after_allocation: CappedQuadriaticFunding,
+    before_allocation: CappedQuadraticFunding,
+    after_allocation: CappedQuadraticFunding,
 ) -> float:
     """Calculate the leverage of a user's new allocations in capped quadratic funding.
 
@@ -174,13 +174,13 @@ def cqf_simulate_leverage(
     ]
 
     # Calculate capped quadratic funding before and after the user's allocation
-    before_allocation = capped_quadriatic_funding(
+    before_allocation = capped_quadratic_funding(
         allocations_without_user,
         matched_rewards,
         project_addresses,
         MR_FUNDING_CAP_PERCENT,
     )
-    after_allocation = capped_quadriatic_funding(
+    after_allocation = capped_quadratic_funding(
         allocations_without_user + new_allocations,
         matched_rewards,
         project_addresses,
diff --git a/backend/v2/project_rewards/dependencies.py b/backend/v2/project_rewards/dependencies.py
index fad325071e..f98bd6d933 100644
--- a/backend/v2/project_rewards/dependencies.py
+++ b/backend/v2/project_rewards/dependencies.py
@@ -3,7 +3,7 @@
 from fastapi import Depends
 from v2.core.dependencies import GetSession
 from v2.epochs.dependencies import GetOpenAllocationWindowEpochNumber
-from v2.matched_rewards.dependencies import GetMatchedRewardsEstimator
+from v2.matched_rewards.dependencies import GetMatchedRewardsEstimatorInAW
 from v2.project_rewards.services import ProjectRewardsEstimator
 from v2.projects.dependencies import GetProjectsContracts
 
@@ -12,7 +12,7 @@ async def get_project_rewards_estimator(
     epoch_number: GetOpenAllocationWindowEpochNumber,
     session: GetSession,
     projects_contracts: GetProjectsContracts,
-    estimated_project_matched_rewards: GetMatchedRewardsEstimator,
+    estimated_project_matched_rewards: GetMatchedRewardsEstimatorInAW,
 ) -> ProjectRewardsEstimator:
     return ProjectRewardsEstimator(
         session=session,
diff --git a/backend/v2/project_rewards/router.py b/backend/v2/project_rewards/router.py
index 0b9f811b90..40ca5ac243 100644
--- a/backend/v2/project_rewards/router.py
+++ b/backend/v2/project_rewards/router.py
@@ -8,7 +8,7 @@
 from app.engine.user.budget.with_ppf import UserBudgetWithPPF
 from app.modules.snapshots.pending.core import calculate_user_budgets
 from app.modules.staking.proceeds.core import estimate_staking_proceeds
-from v2.matched_rewards.dependencies import GetMatchedRewardsEstimator
+from v2.matched_rewards.dependencies import GetMatchedRewardsEstimator, GetMatchedRewardsEstimatorInAW, get_matched_rewards_estimator
 from v2.deposits.dependencies import GetDepositEventsRepository
 from v2.glms.dependencies import GetGLMBalanceOfDeposits
 from v2.project_rewards.user_events import (
@@ -79,7 +79,7 @@ async def get_user_budget_for_epoch_v1(
         epoch_number,
     )
 
-    if not budget:
+    if budget is None:
         response.status_code = status.HTTP_204_NO_CONTENT
         return None
 
@@ -120,7 +120,6 @@ async def get_user_budget_for_upcoming_epoch_v1(
 
     # BEGIN: Calculate pending snapshot
     eth_proceeds = estimate_staking_proceeds(epoch_end - epoch_start)
-
     # Based on all deposit events, calculate effective deposits
     events = await deposit_events_repository.get_all_users_events(
         epoch_number, epoch_start, epoch_end
@@ -173,8 +172,8 @@ async def get_epoch_budgets_v1(
 async def get_estimated_budget_v1(
     session: GetSession,
     epoch_contracts: GetEpochsContracts,
+    # Parameters
     glm_balance: GetGLMBalanceOfDeposits,
-    matched_rewards_estimator: GetMatchedRewardsEstimator,
     request: EstimatedBudgetByEpochRequestV1,
 ) -> UserBudgetWithMatchedFundingResponseV1:
     """
@@ -222,13 +221,19 @@ async def get_estimated_budget_v1(
 
     # Matching fund is calculated based on the last epoch's leverage
     epoch_number = await epoch_contracts.get_finalized_epoch()
-    rewards_leverage = await get_rewards_leverage_v1(
-        session, epoch_contracts, epoch_number, matched_rewards_estimator
-    )
+
+    # Calculate the leverage based on the last finalized epoch
+    finalized_snapshot = await get_finalized_epoch_snapshot(session, epoch_number)
+    if finalized_snapshot is None:
+        raise MissingSnapshot()
+    matched_rewards = int(finalized_snapshot.matched_rewards)
+
+    allocations_sum = await sum_allocations_by_epoch(session, epoch_number)
+    leverage = matched_rewards / allocations_sum if allocations_sum else 0
 
     return UserBudgetWithMatchedFundingResponseV1(
         budget=epochs_budget,
-        matched_funding=int(epochs_budget * rewards_leverage.leverage),
+        matched_funding=int(epochs_budget * leverage),
     )
 
 
@@ -347,8 +352,9 @@ async def get_estimated_budget_by_days_v1(
 async def get_rewards_leverage_v1(
     session: GetSession,
     epochs_contracts: GetEpochsContracts,
-    epoch_number: int,
     matched_rewards_estimator: GetMatchedRewardsEstimator,
+    # Parameters
+    epoch_number: int,
 ) -> RewardsLeverageResponseV1:
     """
     Returns leverage for a given epoch.
@@ -357,8 +363,6 @@ async def get_rewards_leverage_v1(
     For pending and finalizing epochs it returns the leverage based on the estimated matched rewards of pending epoch.
     """
 
-    print(f"Getting leverage for epoch {epoch_number}")
-
     epoch_state = await get_epoch_state(session, epochs_contracts, epoch_number)
 
     # This operations only makes sense for finalized, finalizing and pending epochs
diff --git a/backend/v2/project_rewards/schemas.py b/backend/v2/project_rewards/schemas.py
index 314697e141..49e6cc5ffb 100644
--- a/backend/v2/project_rewards/schemas.py
+++ b/backend/v2/project_rewards/schemas.py
@@ -20,7 +20,7 @@ class EstimatedProjectRewardsResponseV1(OctantModel):
 
 class EstimatedBudgetByEpochRequestV1(OctantModel):
     number_of_epochs: int = Field(
-        ..., description="Number of epochs when GLM are locked", ge=0
+        ..., description="Number of epochs when GLM are locked", gt=0
     )
     glm_amount: BigInteger = Field(
         ..., description="Amount of estimated GLM locked in wei", ge=0
@@ -42,7 +42,7 @@ class EstimatedBudgetByDaysRequestV1(OctantModel):
 
     @property
     def lock_duration_sec(self) -> int:
-        return self.days * 86400  # 24hours * 60minutes * 60seconds
+        return self.days * 24 * 60 * 60
 
 
 class RewardsMerkleTreeLeafV1(OctantModel):
diff --git a/backend/v2/project_rewards/services.py b/backend/v2/project_rewards/services.py
index 41eccad474..422c2435fc 100644
--- a/backend/v2/project_rewards/services.py
+++ b/backend/v2/project_rewards/services.py
@@ -19,9 +19,9 @@
 )
 from v2.allocations.repositories import get_allocations_with_user_uqs
 from v2.matched_rewards.services import MatchedRewardsEstimator
-from v2.project_rewards.capped_quadriatic import (
-    CappedQuadriaticFunding,
-    capped_quadriatic_funding,
+from v2.project_rewards.capped_quadratic import (
+    CappedQuadraticFunding,
+    capped_quadratic_funding,
 )
 from v2.projects.contracts import ProjectsContracts
 
@@ -36,7 +36,7 @@ class ProjectRewardsEstimator:
     # Parameters
     epoch_number: int
 
-    async def get(self) -> CappedQuadriaticFunding:
+    async def get(self) -> CappedQuadraticFunding:
         # Gather all the necessary data for the calculation
         all_projects = await self.projects_contracts.get_project_addresses(
             self.epoch_number
@@ -49,7 +49,7 @@ async def get(self) -> CappedQuadriaticFunding:
         )
 
         # Calculate using the Capped Quadratic Funding formula
-        return capped_quadriatic_funding(
+        return capped_quadratic_funding(
             project_addresses=all_projects,
             allocations=allocations,
             matched_rewards=matched_rewards,

From f533ca76ad1b8fb041f8a046793bf5fbeba84d3e Mon Sep 17 00:00:00 2001
From: adam-gf <adam@golem.foundation>
Date: Tue, 28 Jan 2025 01:52:06 +0100
Subject: [PATCH 05/14] Small fixes and reformatting

---
 backend/app/context/epoch/block_range.py      |   4 +-
 backend/tests/v2/factories/__init__.py        |   3 +
 backend/tests/v2/factories/budgets.py         |   7 +-
 .../tests/v2/factories/finalized_snapshots.py |   7 +-
 backend/tests/v2/factories/patrons.py         |  52 +++++++
 .../tests/v2/factories/pending_snapshots.py   |  15 +-
 backend/tests/v2/factories/users.py           |   2 +-
 .../project_rewards/test_estimated_budged.py  |  87 ++++-------
 .../test_get_budget_for_epoch.py              |   2 +-
 .../project_rewards/test_get_epoch_budgets.py |  28 ++--
 .../test_get_rewards_leverage.py              |  24 +--
 .../test_get_unused_rewards.py                | 142 ++++++++++++++++++
 .../test_get_upcoming_budget.py               |  69 +++++----
 backend/v2/deposits/dependencies.py           |   8 +-
 backend/v2/deposits/repositories.py           | 101 -------------
 backend/v2/deposits/services.py               | 103 +++++++++++++
 backend/v2/matched_rewards/dependencies.py    |   2 +-
 .../v2/project_rewards/capped_quadratic.py    |   4 +-
 backend/v2/project_rewards/router.py          |   8 +-
 backend/v2/project_rewards/services.py        |  76 ++++++++--
 backend/v2/project_rewards/user_events.py     | 120 ---------------
 backend/v2/sablier/dependencies.py            |   2 +-
 backend/v2/sablier/subgraphs.py               |   2 +-
 23 files changed, 494 insertions(+), 374 deletions(-)
 create mode 100644 backend/tests/v2/factories/patrons.py
 create mode 100644 backend/tests/v2/project_rewards/test_get_unused_rewards.py
 create mode 100644 backend/v2/deposits/services.py
 delete mode 100644 backend/v2/project_rewards/user_events.py

diff --git a/backend/app/context/epoch/block_range.py b/backend/app/context/epoch/block_range.py
index 20af7a5111..95350c60c3 100644
--- a/backend/app/context/epoch/block_range.py
+++ b/backend/app/context/epoch/block_range.py
@@ -12,8 +12,8 @@ def get_blocks_range(
     if not with_block_range:
         return None, None
 
-    # start_block = get_block_num_from_ts(start_sec) if start_sec <= now_sec else None
-    # end_block = get_block_num_from_ts(end_sec) if end_sec <= now_sec else None
+    start_block = get_block_num_from_ts(start_sec) if start_sec <= now_sec else None
+    end_block = get_block_num_from_ts(end_sec) if end_sec <= now_sec else None
 
     start_block = None
     end_block = None
diff --git a/backend/tests/v2/factories/__init__.py b/backend/tests/v2/factories/__init__.py
index 4e5a0647ff..0ef06a28a1 100644
--- a/backend/tests/v2/factories/__init__.py
+++ b/backend/tests/v2/factories/__init__.py
@@ -7,6 +7,7 @@
 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
 
 
@@ -29,6 +30,7 @@ class FactoriesAggregator:
     budgets: BudgetFactorySet
     pending_snapshots: PendingEpochSnapshotFactorySet
     finalized_snapshots: FinalizedEpochSnapshotFactorySet
+    patrons: PatronModeEventFactorySet
 
     def __init__(self, fast_session: AsyncSession):
         """
@@ -41,3 +43,4 @@ def __init__(self, fast_session: AsyncSession):
         self.budgets = BudgetFactorySet(fast_session)
         self.pending_snapshots = PendingEpochSnapshotFactorySet(fast_session)
         self.finalized_snapshots = FinalizedEpochSnapshotFactorySet(fast_session)
+        self.patrons = PatronModeEventFactorySet(fast_session)
diff --git a/backend/tests/v2/factories/budgets.py b/backend/tests/v2/factories/budgets.py
index a14a0877fe..dbb769ca75 100644
--- a/backend/tests/v2/factories/budgets.py
+++ b/backend/tests/v2/factories/budgets.py
@@ -1,7 +1,6 @@
 import random
 from async_factory_boy.factory.sqlalchemy import AsyncSQLAlchemyFactory
 from factory import LazyAttribute
-from sqlalchemy.ext.asyncio import AsyncSession
 
 from app.infrastructure.database.models import Budget, User
 from tests.v2.factories.base import FactorySetBase
@@ -16,7 +15,9 @@ class Meta:
 
     user_id = None
     epoch = None
-    budget = LazyAttribute(lambda _: str(random.randint(1, 1000) * 10**18))  # Random amount in wei
+    budget = LazyAttribute(
+        lambda _: str(random.randint(1, 1000) * 10**18)
+    )  # Random amount in wei
 
 
 class BudgetFactorySet(FactorySetBase):
@@ -51,4 +52,4 @@ async def create(
             factory_kwargs["budget"] = str(amount)
 
         budget = await BudgetFactory.create(**factory_kwargs)
-        return budget 
\ No newline at end of file
+        return budget
diff --git a/backend/tests/v2/factories/finalized_snapshots.py b/backend/tests/v2/factories/finalized_snapshots.py
index c66d967687..0a2ee9713d 100644
--- a/backend/tests/v2/factories/finalized_snapshots.py
+++ b/backend/tests/v2/factories/finalized_snapshots.py
@@ -1,7 +1,6 @@
 import random
 from async_factory_boy.factory.sqlalchemy import AsyncSQLAlchemyFactory
 from factory import LazyAttribute
-from sqlalchemy.ext.asyncio import AsyncSession
 
 from app.infrastructure.database.models import FinalizedEpochSnapshot
 from tests.v2.factories.base import FactorySetBase
@@ -17,7 +16,9 @@ class Meta:
     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)))
+    withdrawals_merkle_root = LazyAttribute(
+        lambda _: "0x" + "".join(random.choices("0123456789abcdef", k=64))
+    )
     total_withdrawals = LazyAttribute(lambda _: str(random.randint(1, 1000) * 10**18))
 
 
@@ -63,4 +64,4 @@ async def create(
             factory_kwargs["total_withdrawals"] = str(total_withdrawals)
 
         snapshot = await FinalizedEpochSnapshotFactory.create(**factory_kwargs)
-        return snapshot 
\ No newline at end of file
+        return snapshot
diff --git a/backend/tests/v2/factories/patrons.py b/backend/tests/v2/factories/patrons.py
new file mode 100644
index 0000000000..a9cac8dc64
--- /dev/null
+++ b/backend/tests/v2/factories/patrons.py
@@ -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
diff --git a/backend/tests/v2/factories/pending_snapshots.py b/backend/tests/v2/factories/pending_snapshots.py
index 9abaa33e71..5baa787ddd 100644
--- a/backend/tests/v2/factories/pending_snapshots.py
+++ b/backend/tests/v2/factories/pending_snapshots.py
@@ -1,7 +1,6 @@
 import random
 from async_factory_boy.factory.sqlalchemy import AsyncSQLAlchemyFactory
 from factory import LazyAttribute
-from sqlalchemy.ext.asyncio import AsyncSession
 
 from app.infrastructure.database.models import PendingEpochSnapshot
 from tests.v2.factories.base import FactorySetBase
@@ -15,10 +14,14 @@ class Meta:
 
     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))
+    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))
+    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))
@@ -69,7 +72,9 @@ async def create(
         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)
+            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:
@@ -78,4 +83,4 @@ async def create(
             factory_kwargs["community_fund"] = str(community_fund)
 
         snapshot = await PendingEpochSnapshotFactory.create(**factory_kwargs)
-        return snapshot 
\ No newline at end of file
+        return snapshot
diff --git a/backend/tests/v2/factories/users.py b/backend/tests/v2/factories/users.py
index 5f0b960b53..b12fd95a5c 100644
--- a/backend/tests/v2/factories/users.py
+++ b/backend/tests/v2/factories/users.py
@@ -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:
diff --git a/backend/tests/v2/project_rewards/test_estimated_budged.py b/backend/tests/v2/project_rewards/test_estimated_budged.py
index 9a38b870a0..5fc6544b24 100644
--- a/backend/tests/v2/project_rewards/test_estimated_budged.py
+++ b/backend/tests/v2/project_rewards/test_estimated_budged.py
@@ -1,14 +1,11 @@
-from unittest.mock import MagicMock
 import pytest
 from httpx import AsyncClient
 from sqlalchemy.ext.asyncio import AsyncSession
 
-from v2.matched_rewards.services import MatchedRewardsEstimator
 from v2.glms.dependencies import get_glm_balance_of_deposits
 from tests.v2.factories import FactoriesAggregator
 from tests.v2.fake_contracts.helpers import FakeEpochsContractDetails
-from tests.v2.fake_contracts.conftest import fake_epochs_contract_factory, FakeEpochsContractCallable
-from v2.matched_rewards.dependencies import GetMatchedRewardsEstimatorInAW, get_matched_rewards_estimator_only_in_aw
+from tests.v2.fake_contracts.conftest import FakeEpochsContractCallable
 from fastapi import FastAPI
 
 
@@ -36,17 +33,15 @@ async def test_estimates_budget_for_single_epoch(
             finalized_epoch=finalized_epoch,
             current_epoch=finalized_epoch + 1,
             pending_epoch=None,
-            future_epoch_props=(0, 0, epoch_start, epoch_duration)
+            future_epoch_props=(0, 0, epoch_start, epoch_duration),
         )
     )
 
     # Create pending epoch snapshot
-    pending_snapshot = await factories.pending_snapshots.create(
-        epoch=finalized_epoch
-    )
+    await factories.pending_snapshots.create(epoch=finalized_epoch)
 
     # finalize epoch snapshot
-    finalized_snapshot = await factories.finalized_snapshots.create(
+    await factories.finalized_snapshots.create(
         epoch=finalized_epoch,
     )
 
@@ -58,20 +53,16 @@ async def test_estimates_budget_for_single_epoch(
     # rewards_estimator_mock.get.return_value = leverage
     # fast_app.dependency_overrides[get_matched_rewards_estimator] = lambda: rewards_estimator_mock
 
-    request_data = {
-        "glm_amount": str(glm_amount),
-        "number_of_epochs": 1
-    }
+    request_data = {"glm_amount": str(glm_amount), "number_of_epochs": 1}
 
     async with fast_client as client:
         resp = await client.post("/rewards/estimated_budget", json=request_data)
-        
-        print("resp", resp.json())
+
         assert resp.status_code == 200
         response = resp.json()
         assert response["budget"] != "0"
         assert response["matchedFunding"] == "0"
-        
+
 
 @pytest.mark.asyncio
 async def test_estimates_budget_for_multiple_epochs(
@@ -101,10 +92,8 @@ async def test_estimates_budget_for_multiple_epochs(
         epoch=finalized_epoch,
     )
 
-        # Create pending epoch snapshot
-    pending_snapshot = await factories.pending_snapshots.create(
-        epoch=finalized_epoch
-    )
+    # Create pending epoch snapshot
+    await factories.pending_snapshots.create(epoch=finalized_epoch)
 
     # finalize epoch snapshot
     finalized_snapshot = await factories.finalized_snapshots.create(
@@ -115,7 +104,7 @@ async def test_estimates_budget_for_multiple_epochs(
     fake_epochs_contract_factory(
         FakeEpochsContractDetails(
             finalized_epoch=finalized_epoch,
-            future_epoch_props=(0, 0, epoch_start, epoch_duration)
+            future_epoch_props=(0, 0, epoch_start, epoch_duration),
         )
     )
 
@@ -127,52 +116,42 @@ async def test_estimates_budget_for_multiple_epochs(
     # rewards_estimator_mock.get.return_value = leverage
     # fast_app.dependency_overrides[GetMatchedRewardsEstimatorInAW] = lambda: rewards_estimator_mock
 
-    request_data = {
-        "glm_amount": str(glm_amount),
-        "number_of_epochs": number_of_epochs
-    }
+    request_data = {"glm_amount": str(glm_amount), "number_of_epochs": number_of_epochs}
 
     async with fast_client as client:
         resp = await client.post("/rewards/estimated_budget", json=request_data)
-        print("result", resp.json())
-        
+
         assert resp.status_code == 200
         result = resp.json()
-        
+
         assert result["budget"] != "0"
         assert result["matchedFunding"] != "0"
-        leverage = int(finalized_snapshot.matched_rewards) / (int(alice_allocation.amount) + int(bob_allocation.amount))
+        leverage = int(finalized_snapshot.matched_rewards) / (
+            int(alice_allocation.amount) + int(bob_allocation.amount)
+        )
         assert int(result["matchedFunding"]) == int(int(result["budget"]) * leverage)
 
         one_epoch_budget = int(result["budget"])
         one_epoch_matched_funding = int(result["matchedFunding"])
 
         # Test two epochs
-        request_data = {
-            "glm_amount": str(glm_amount),
-            "number_of_epochs": 2
-        }
+        request_data = {"glm_amount": str(glm_amount), "number_of_epochs": 2}
 
         resp = await client.post("/rewards/estimated_budget", json=request_data)
-        
+
         assert resp.status_code == 200
         result = resp.json()
-        print("result", result)
         assert int(result["budget"]) != "0"
         assert int(result["matchedFunding"]) != "0"
 
         assert int(result["budget"]) == one_epoch_budget * 2
         assert int(result["matchedFunding"]) == one_epoch_matched_funding * 2
 
-
         # Test zero GLM amount
-        request_data = {
-            "glm_amount": str(0),
-            "number_of_epochs": 1
-        }
+        request_data = {"glm_amount": str(0), "number_of_epochs": 1}
 
         resp = await client.post("/rewards/estimated_budget", json=request_data)
-        
+
         assert resp.status_code == 200
         result = resp.json()
         assert int(result["budget"]) == 0
@@ -186,22 +165,22 @@ async def test_validates_request_parameters(
     """Should validate request parameters"""
     async with fast_client as client:
         # Test negative GLM amount
-        resp = await client.post("/rewards/estimated_budget", json={
-            "glm_amount": "-1000",
-            "number_of_epochs": 1
-        })
+        resp = await client.post(
+            "/rewards/estimated_budget",
+            json={"glm_amount": "-1000", "number_of_epochs": 1},
+        )
         assert resp.status_code == 422
 
         # Test zero epochs
-        resp = await client.post("/rewards/estimated_budget", json={
-            "glm_amount": "1000",
-            "number_of_epochs": 0
-        })
+        resp = await client.post(
+            "/rewards/estimated_budget",
+            json={"glm_amount": "1000", "number_of_epochs": 0},
+        )
         assert resp.status_code == 422
 
         # Test negative epochs
-        resp = await client.post("/rewards/estimated_budget", json={
-            "glm_amount": "1000",
-            "number_of_epochs": -1
-        })
-        assert resp.status_code == 422
\ No newline at end of file
+        resp = await client.post(
+            "/rewards/estimated_budget",
+            json={"glm_amount": "1000", "number_of_epochs": -1},
+        )
+        assert resp.status_code == 422
diff --git a/backend/tests/v2/project_rewards/test_get_budget_for_epoch.py b/backend/tests/v2/project_rewards/test_get_budget_for_epoch.py
index 776c5a26f8..a76122f602 100644
--- a/backend/tests/v2/project_rewards/test_get_budget_for_epoch.py
+++ b/backend/tests/v2/project_rewards/test_get_budget_for_epoch.py
@@ -58,7 +58,7 @@ async def test_returns_zero_budget_when_amount_is_zero(
 
     # Given: a user with zero budget
     alice = await factories.users.get_or_create_alice()
-    budget = await factories.budgets.create(user=alice, epoch=1, amount=0)
+    await factories.budgets.create(user=alice, epoch=1, amount=0)
 
     async with fast_client as client:
         resp = await client.get(f"rewards/budget/{alice.address}/epoch/1")
diff --git a/backend/tests/v2/project_rewards/test_get_epoch_budgets.py b/backend/tests/v2/project_rewards/test_get_epoch_budgets.py
index 46f7ce52c0..9c73c6f9c1 100644
--- a/backend/tests/v2/project_rewards/test_get_epoch_budgets.py
+++ b/backend/tests/v2/project_rewards/test_get_epoch_budgets.py
@@ -10,7 +10,7 @@ async def test_returns_empty_budgets_when_no_users(
     fast_session: AsyncSession,
 ):
     """Should return empty budgets list when no users have budgets for the epoch"""
-    
+
     async with fast_client as client:
         resp = await client.get("/rewards/budgets/epoch/1")
         assert resp.status_code == 200
@@ -24,7 +24,7 @@ async def test_returns_all_user_budgets_for_epoch(
     factories: FactoriesAggregator,
 ):
     """Should return budgets for all users in the given epoch"""
-    
+
     # Given: multiple users with budgets in different epochs
     alice = await factories.users.get_or_create_alice()
     bob = await factories.users.get_or_create_bob()
@@ -34,7 +34,7 @@ async def test_returns_all_user_budgets_for_epoch(
     alice_budget1 = await factories.budgets.create(user=alice, epoch=1, amount=100)
     bob_budget1 = await factories.budgets.create(user=bob, epoch=1, amount=200)
     charlie_budget1 = await factories.budgets.create(user=charlie, epoch=1, amount=300)
-    
+
     # Create budgets for a different epoch
     await factories.budgets.create(user=alice, epoch=2, amount=400)
     await factories.budgets.create(user=bob, epoch=2, amount=500)
@@ -42,11 +42,11 @@ async def test_returns_all_user_budgets_for_epoch(
     async with fast_client as client:
         # When: requesting budgets for epoch 1
         resp = await client.get("/rewards/budgets/epoch/1")
-        
+
         # Then: should return all budgets for epoch 1
         assert resp.status_code == 200
         budgets = resp.json()["budgets"]
-        
+
         # Convert to set for order-independent comparison
         expected_budgets = [
             {"address": alice.address, "amount": str(alice_budget1.budget)},
@@ -66,18 +66,18 @@ async def test_returns_empty_budgets_for_future_epoch(
     factories: FactoriesAggregator,
 ):
     """Should return empty budgets list for an epoch with no budgets"""
-    
+
     # Given: users with budgets in epoch 1
     alice = await factories.users.get_or_create_alice()
     bob = await factories.users.get_or_create_bob()
-    
+
     await factories.budgets.create(user=alice, epoch=1, amount=100)
     await factories.budgets.create(user=bob, epoch=1, amount=200)
 
     async with fast_client as client:
         # When: requesting budgets for epoch 2 (which has no budgets)
         resp = await client.get("/rewards/budgets/epoch/2")
-        
+
         # Then: should return empty list
         assert resp.status_code == 200
         assert resp.json() == {"budgets": []}
@@ -90,20 +90,20 @@ async def test_includes_zero_budgets(
     factories: FactoriesAggregator,
 ):
     """Should include users with zero budgets in the response"""
-    
+
     # Given: users with zero and non-zero budgets
     alice = await factories.users.get_or_create_alice()
     bob = await factories.users.get_or_create_bob()
-    
+
     alice_budget = await factories.budgets.create(user=alice, epoch=1, amount=100)
-    bob_budget = await factories.budgets.create(user=bob, epoch=1, amount=0)
+    await factories.budgets.create(user=bob, epoch=1, amount=0)
 
     async with fast_client as client:
         resp = await client.get("/rewards/budgets/epoch/1")
-        
+
         assert resp.status_code == 200
         budgets = resp.json()["budgets"]
-        
+
         expected_budgets = [
             {"address": alice.address, "amount": str(alice_budget.budget)},
             {"address": bob.address, "amount": "0"},
@@ -111,4 +111,4 @@ async def test_includes_zero_budgets(
         assert len(budgets) == 2
         assert {(b["address"], b["amount"]) for b in budgets} == {
             (b["address"], b["amount"]) for b in expected_budgets
-        }
\ No newline at end of file
+        }
diff --git a/backend/tests/v2/project_rewards/test_get_rewards_leverage.py b/backend/tests/v2/project_rewards/test_get_rewards_leverage.py
index 3fe0d40093..c22824f20b 100644
--- a/backend/tests/v2/project_rewards/test_get_rewards_leverage.py
+++ b/backend/tests/v2/project_rewards/test_get_rewards_leverage.py
@@ -5,14 +5,12 @@
 from httpx import AsyncClient
 from sqlalchemy.ext.asyncio import AsyncSession
 
-from app.context.epoch_state import EpochState
-from app.exceptions import InvalidEpoch, MissingSnapshot, NotImplementedForGivenEpochState
+from app.exceptions import InvalidEpoch, NotImplementedForGivenEpochState
 from v2.matched_rewards.services import MatchedRewardsEstimator
 from tests.v2.factories import FactoriesAggregator
 from tests.v2.fake_contracts.helpers import FakeEpochsContractDetails
 from tests.v2.fake_contracts.conftest import FakeEpochsContractCallable
-from v2.matched_rewards.dependencies import GetMatchedRewardsEstimator, get_matched_rewards_estimator
-from tests.v2.fake_contracts.conftest import fake_epochs_contract_factory, FakeEpochsContractCallable
+from v2.matched_rewards.dependencies import get_matched_rewards_estimator
 
 
 @pytest.mark.asyncio
@@ -36,7 +34,7 @@ async def test_returns_leverage_for_finalized_epoch(
     )
 
     # Create pending snapshot
-    pending_snapshot = await factories.pending_snapshots.create(
+    await factories.pending_snapshots.create(
         epoch=epoch_number,
     )
 
@@ -56,7 +54,9 @@ async def test_returns_leverage_for_finalized_epoch(
         resp = await client.get(f"rewards/leverage/{epoch_number}")
         assert resp.status_code == HTTPStatus.OK
         resp_leverage = int(resp.json()["leverage"])
-        expected_leverage = int(int(finalized_snapshot.matched_rewards) / int(alice_allocation.amount))
+        expected_leverage = int(
+            int(finalized_snapshot.matched_rewards) / int(alice_allocation.amount)
+        )
         assert resp_leverage == expected_leverage
 
 
@@ -83,7 +83,7 @@ async def test_returns_leverage_for_pending_epoch(
     )
 
     # Create pending snapshot
-    pending_snapshot = await factories.pending_snapshots.create(
+    await factories.pending_snapshots.create(
         epoch=epoch_number,
     )
 
@@ -97,7 +97,9 @@ async def test_returns_leverage_for_pending_epoch(
     # Mock rewards estimator
     rewards_estimator = MagicMock(spec=MatchedRewardsEstimator)
     rewards_estimator.get.return_value = estimated_rewards
-    fast_app.dependency_overrides[get_matched_rewards_estimator] = lambda: rewards_estimator
+    fast_app.dependency_overrides[
+        get_matched_rewards_estimator
+    ] = lambda: rewards_estimator
 
     async with fast_client as client:
         resp = await client.get(f"rewards/leverage/{epoch_number}")
@@ -126,12 +128,12 @@ async def test_returns_zero_leverage_when_no_allocations(
     )
 
     # Create pending snapshot
-    pending_snapshot = await factories.pending_snapshots.create(
+    await factories.pending_snapshots.create(
         epoch=epoch_number,
     )
 
     # Create finalized snapshot
-    finalized_snapshot = await factories.finalized_snapshots.create(
+    await factories.finalized_snapshots.create(
         epoch=epoch_number,
         matched_rewards=matched_rewards,
     )
@@ -188,4 +190,4 @@ async def test_raises_error_for_invalid_epoch_state(
     async with fast_client as client:
         resp = await client.get(f"rewards/leverage/{epoch_number}")
         assert resp.status_code == HTTPStatus.BAD_REQUEST
-        assert resp.json() == {"message": NotImplementedForGivenEpochState.description} 
\ No newline at end of file
+        assert resp.json() == {"message": NotImplementedForGivenEpochState.description}
diff --git a/backend/tests/v2/project_rewards/test_get_unused_rewards.py b/backend/tests/v2/project_rewards/test_get_unused_rewards.py
new file mode 100644
index 0000000000..439c145c9d
--- /dev/null
+++ b/backend/tests/v2/project_rewards/test_get_unused_rewards.py
@@ -0,0 +1,142 @@
+import pytest
+from http import HTTPStatus
+from datetime import datetime, timezone
+from httpx import AsyncClient
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from tests.v2.factories import FactoriesAggregator
+from tests.v2.fake_subgraphs.conftest import FakeEpochsSubgraphCallable
+from tests.v2.fake_subgraphs.helpers import FakeEpochEventDetails
+
+
+@pytest.mark.asyncio
+async def test_returns_empty_when_no_budgets(
+    fake_epochs_subgraph_factory: FakeEpochsSubgraphCallable,
+    fast_client: AsyncClient,
+    fast_session: AsyncSession,
+    factories: FactoriesAggregator,
+):
+    """Should return empty list and zero value when there are no budgets"""
+    # Given: an epoch with no budgets
+    epoch_number = 1
+
+    # Mock subgraph response
+    fake_epochs_subgraph_factory(
+        [
+            FakeEpochEventDetails(
+                epoch=epoch_number,
+            )
+        ]
+    )
+
+    async with fast_client as client:
+        resp = await client.get(f"rewards/unused/{epoch_number}")
+        assert resp.status_code == HTTPStatus.OK
+        assert resp.json() == {
+            "addresses": [],
+            "value": "0",
+        }
+
+
+@pytest.mark.asyncio
+async def test_returns_unused_budgets_excluding_donors_and_patrons(
+    fake_epochs_subgraph_factory: FakeEpochsSubgraphCallable,
+    fast_client: AsyncClient,
+    fast_session: AsyncSession,
+    factories: FactoriesAggregator,
+):
+    """Should return budgets excluding donors and patrons"""
+    # Given: an epoch with various users
+    epoch_number = 1
+
+    # Mock subgraph response
+    fake_epochs_subgraph_factory(
+        [
+            FakeEpochEventDetails(
+                epoch=epoch_number,
+            )
+        ]
+    )
+
+    # Create users with different roles
+    alice = await factories.users.get_or_create_alice()  # Will be a donor
+    bob = await factories.users.get_or_create_bob()  # Will be a patron
+    charlie = await factories.users.get_or_create_charlie()  # Will have unused budget
+    dave = await factories.users.create()  # Will have unused budget
+
+    # Create budgets for all users
+    await factories.budgets.create(user=alice, epoch=epoch_number, amount=100)
+    await factories.budgets.create(user=bob, epoch=epoch_number, amount=200)
+    charlie_budget = await factories.budgets.create(
+        user=charlie, epoch=epoch_number, amount=300
+    )
+    dave_budget = await factories.budgets.create(
+        user=dave, epoch=epoch_number, amount=400
+    )
+
+    # Make Alice a donor by creating an allocation
+    await factories.allocations.create(user=alice, epoch=epoch_number)
+
+    # Make Bob a patron
+    await factories.patrons.create(
+        user=bob,
+        patron_mode_enabled=True,
+        created_at=datetime.fromtimestamp(1500, timezone.utc),
+    )
+
+    async with fast_client as client:
+        resp = await client.get(f"rewards/unused/{epoch_number}")
+        assert resp.status_code == HTTPStatus.OK
+
+        result = resp.json()
+        assert sorted(result["addresses"]) == sorted([charlie.address, dave.address])
+        assert result["value"] == str(
+            int(charlie_budget.budget) + int(dave_budget.budget)
+        )
+
+
+@pytest.mark.asyncio
+async def test_returns_empty_when_all_users_are_donors_or_patrons(
+    fake_epochs_subgraph_factory: FakeEpochsSubgraphCallable,
+    fast_client: AsyncClient,
+    fast_session: AsyncSession,
+    factories: FactoriesAggregator,
+):
+    """Should return empty when all users with budgets are either donors or patrons"""
+    # Given: an epoch where all users are donors or patrons
+    epoch_number = 1
+
+    # Mock subgraph response
+    fake_epochs_subgraph_factory(
+        [
+            FakeEpochEventDetails(
+                epoch=epoch_number,
+            )
+        ]
+    )
+
+    # Create users
+    alice = await factories.users.get_or_create_alice()  # Will be a donor
+    bob = await factories.users.get_or_create_bob()  # Will be a patron
+
+    # Create budgets
+    await factories.budgets.create(user=alice, epoch=epoch_number)
+    await factories.budgets.create(user=bob, epoch=epoch_number)
+
+    # Make Alice a donor
+    await factories.allocations.create(user=alice, epoch=epoch_number)
+
+    # Make Bob a patron
+    await factories.patrons.create(
+        user=bob,
+        patron_mode_enabled=True,
+        created_at=datetime.fromtimestamp(1500, timezone.utc),
+    )
+
+    async with fast_client as client:
+        resp = await client.get(f"rewards/unused/{epoch_number}")
+        assert resp.status_code == HTTPStatus.OK
+        assert resp.json() == {
+            "addresses": [],
+            "value": "0",
+        }
diff --git a/backend/tests/v2/project_rewards/test_get_upcoming_budget.py b/backend/tests/v2/project_rewards/test_get_upcoming_budget.py
index cfad796046..eb9febe031 100644
--- a/backend/tests/v2/project_rewards/test_get_upcoming_budget.py
+++ b/backend/tests/v2/project_rewards/test_get_upcoming_budget.py
@@ -5,11 +5,14 @@
 from sqlalchemy.ext.asyncio import AsyncSession
 
 from app.engine.user.effective_deposit import DepositEvent, DepositSource, EventType
-from v2.deposits.dependencies import GetDepositEventsRepository, get_deposit_events_repository
+from v2.deposits.dependencies import (
+    GetDepositEventsRepository,
+    get_deposit_events_repository,
+)
 from tests.v2.factories import FactoriesAggregator
 from tests.v2.fake_contracts.helpers import FakeEpochsContractDetails
-from tests.v2.fake_contracts.conftest import fake_epochs_contract_factory, FakeEpochsContractCallable
-from tests.v2.fake_subgraphs.conftest import fake_epochs_subgraph_factory, FakeEpochsSubgraphCallable
+from tests.v2.fake_contracts.conftest import FakeEpochsContractCallable
+from tests.v2.fake_subgraphs.conftest import FakeEpochsSubgraphCallable
 from tests.v2.fake_subgraphs.helpers import FakeEpochEventDetails
 from fastapi import FastAPI
 from v2.core.dependencies import get_current_timestamp
@@ -28,28 +31,28 @@ async def test_returns_upcoming_budget_with_no_deposits(
     # Given: a user and mocked epoch data
     alice = await factories.users.get_or_create_alice()
     current_epoch = 1
-    current_timestamp = 1000
     epoch_start = 0
 
     # Mock contracts and subgraph
-    fake_epochs_contract_factory(
-        FakeEpochsContractDetails(current_epoch=current_epoch)
-    )
-    
+    fake_epochs_contract_factory(FakeEpochsContractDetails(current_epoch=current_epoch))
+
     # Mock subgraph response
-    fake_epochs_subgraph_factory([
-        FakeEpochEventDetails(
-            epoch=current_epoch,
-            from_ts=epoch_start,
-            to_ts=2000,
-        )
-    ])
+    fake_epochs_subgraph_factory(
+        [
+            FakeEpochEventDetails(
+                epoch=current_epoch,
+                from_ts=epoch_start,
+                to_ts=2000,
+            )
+        ]
+    )
 
     events_repository = MagicMock(spec=GetDepositEventsRepository)
     events_repository.get_all_users_events.return_value = {}
-    fast_app.dependency_overrides[get_deposit_events_repository] = lambda: events_repository
+    fast_app.dependency_overrides[
+        get_deposit_events_repository
+    ] = lambda: events_repository
 
-    print(f"Current timestamp: {current_timestamp}")
     async with fast_client as client:
         resp = await client.get(f"rewards/budget/{alice.address}/upcoming")
         assert resp.json() == {"upcomingBudget": "0"}
@@ -74,18 +77,18 @@ async def test_returns_upcoming_budget_with_deposits(
     deposit_amount = 100 * 10**18  # 100 tokens
 
     # Mock contracts and subgraph
-    fake_epochs_contract_factory(
-        FakeEpochsContractDetails(current_epoch=current_epoch)
-    )
-    
+    fake_epochs_contract_factory(FakeEpochsContractDetails(current_epoch=current_epoch))
+
     # Mock subgraph response
-    fake_epochs_subgraph_factory([
-        FakeEpochEventDetails(
-            epoch=current_epoch,
-            from_ts=epoch_start,
-            to_ts=2000,
-        )
-    ])
+    fake_epochs_subgraph_factory(
+        [
+            FakeEpochEventDetails(
+                epoch=current_epoch,
+                from_ts=epoch_start,
+                to_ts=2000,
+            )
+        ]
+    )
 
     events_repository = MagicMock(spec=GetDepositEventsRepository)
     events_repository.get_all_users_events.return_value = {
@@ -93,17 +96,19 @@ async def test_returns_upcoming_budget_with_deposits(
             DepositEvent(
                 user=alice.address,
                 type=EventType.LOCK,
-        timestamp=epoch_start + 100,
+                timestamp=epoch_start + 100,
                 amount=deposit_amount,
                 deposit_before=0,
                 source=DepositSource.OCTANT,
-    )
+            )
         ]
     }
-    fast_app.dependency_overrides[get_deposit_events_repository] = lambda: events_repository
+    fast_app.dependency_overrides[
+        get_deposit_events_repository
+    ] = lambda: events_repository
     fast_app.dependency_overrides[get_current_timestamp] = lambda: current_timestamp
 
     async with fast_client as client:
         resp = await client.get(f"rewards/budget/{alice.address}/upcoming")
         assert resp.status_code == HTTPStatus.OK
-        assert resp.json() != {"upcomingBudget": "0"} 
+        assert resp.json() != {"upcomingBudget": "0"}
diff --git a/backend/v2/deposits/dependencies.py b/backend/v2/deposits/dependencies.py
index c0c3509492..e1a350fcf6 100644
--- a/backend/v2/deposits/dependencies.py
+++ b/backend/v2/deposits/dependencies.py
@@ -5,7 +5,7 @@
     SABLIER_UNLOCK_GRACE_PERIOD_24_HRS,
     TEST_SABLIER_UNLOCK_GRACE_PERIOD_15_MIN,
 )
-from v2.deposits.repositories import DepositEventsRepository
+from v2.deposits.services import DepositEventsStore
 from v2.epochs.dependencies import GetEpochsSubgraph
 from v2.sablier.dependencies import GetSablierSubgraph
 from v2.core.dependencies import GetChainSettings, GetSession, OctantSettings, Web3
@@ -31,14 +31,14 @@ def get_deposit_events_repository(
     epochs_subgraph: GetEpochsSubgraph,
     sublier_subgraph: GetSablierSubgraph,
     chain_settings: GetChainSettings,
-) -> DepositEventsRepository:
+) -> DepositEventsStore:
     sablier_unlock_grace_period = (
         SABLIER_UNLOCK_GRACE_PERIOD_24_HRS
         if chain_settings.is_mainnet
         else TEST_SABLIER_UNLOCK_GRACE_PERIOD_15_MIN
     )
 
-    return DepositEventsRepository(
+    return DepositEventsStore(
         session,
         epochs_subgraph,
         sublier_subgraph,
@@ -49,5 +49,5 @@ def get_deposit_events_repository(
 # Annotated dependencies
 GetDepositsContracts = Annotated[DepositsContracts, Depends(get_deposits_contracts)]
 GetDepositEventsRepository = Annotated[
-    DepositEventsRepository, Depends(get_deposit_events_repository)
+    DepositEventsStore, Depends(get_deposit_events_repository)
 ]
diff --git a/backend/v2/deposits/repositories.py b/backend/v2/deposits/repositories.py
index c9ad6bdc91..b6d9083e64 100644
--- a/backend/v2/deposits/repositories.py
+++ b/backend/v2/deposits/repositories.py
@@ -1,24 +1,10 @@
-from itertools import groupby
-from operator import attrgetter
 from sqlalchemy import select
 from sqlalchemy.ext.asyncio import AsyncSession
 from sqlalchemy.orm import selectinload
 
-from app.engine.user.effective_deposit import (
-    DepositEvent,
-    EventType,
-)
 from app.infrastructure.database.models import Deposit
-from app.modules.common.sablier_events_mapper import (
-    FlattenStrategy,
-    flatten_sablier_events,
-    process_to_locks_and_unlocks,
-)
-from app.modules.user.events_generator.core import unify_deposit_balances
 from v2.core.types import Address
 from v2.core.transformers import transform_to_checksum_address
-from v2.epochs.subgraphs import EpochsSubgraph
-from v2.sablier.subgraphs import SablierSubgraph
 
 
 async def get_all_deposit_events_for_epoch(
@@ -34,90 +20,3 @@ async def get_all_deposit_events_for_epoch(
     return {
         transform_to_checksum_address(result.user.address): result for result in results
     }
-
-
-class DepositEventsRepository:
-    def __init__(
-        self,
-        session: AsyncSession,
-        epochs_subgraph: EpochsSubgraph,
-        sablier_subgraph: SablierSubgraph,
-        sablier_unlock_grace_period: int,
-    ):
-        self.session = session
-        self.epochs_subgraph = epochs_subgraph
-        self.sablier_subgraph = sablier_subgraph
-        self.sablier_unlock_grace_period = sablier_unlock_grace_period
-
-    async def get_all_users_events(
-        self,
-        epoch_number: int,
-        start_sec: int,
-        end_sec: int,
-    ) -> dict[str, list[DepositEvent]]:
-        """
-        Returns all user events (LOCK, UNLOCK) for a given epoch.
-        """
-
-        # Get all locked amounts for the previous epoch
-        epoch_start_locked_amounts = await get_all_deposit_events_for_epoch(
-            self.session, epoch_number - 1
-        )
-        epoch_start_events = [
-            DepositEvent(
-                user=user,
-                type=EventType.LOCK,
-                timestamp=start_sec,
-                amount=0,  # it is not a deposit in fact
-                deposit_before=int(deposit.epoch_end_deposit),
-            )
-            for user, deposit in epoch_start_locked_amounts.items()
-        ]
-
-        sablier_streams = await self.sablier_subgraph.get_all_streams_history()
-        mapped_streams = process_to_locks_and_unlocks(
-            sablier_streams, from_timestamp=start_sec, to_timestamp=end_sec
-        )
-        # print("Mapped streams", mapped_streams)
-        epoch_events = []
-        epoch_events += flatten_sablier_events(mapped_streams, FlattenStrategy.ALL)
-        epoch_events += await self.epochs_subgraph.fetch_locks_by_timestamp_range(
-            start_sec, end_sec
-        )
-        epoch_events += await self.epochs_subgraph.fetch_unlocks_by_timestamp_range(
-            start_sec, end_sec
-        )
-
-        epoch_events = [DepositEvent.from_dict(event) for event in epoch_events]
-        sorted_events = sorted(epoch_events, key=attrgetter("user", "timestamp"))
-
-        user_events = {
-            k: list(g) for k, g in groupby(sorted_events, key=attrgetter("user"))
-        }
-
-        for event in epoch_start_events:
-            if event.user in user_events:
-                user_events[event.user].insert(0, event)
-            else:
-                user_events[event.user] = [event]
-
-        epoch_start_users = list(map(attrgetter("user"), epoch_start_events))
-        for user_address in user_events:
-            if user_address not in epoch_start_users:
-                user_events[user_address].insert(
-                    0,
-                    DepositEvent(
-                        user_address,
-                        EventType.LOCK,
-                        timestamp=start_sec,
-                        amount=0,
-                        deposit_before=0,
-                    ),
-                )
-
-            user_events[user_address] = unify_deposit_balances(
-                user_events[user_address],
-                self.sablier_unlock_grace_period,
-            )
-
-        return user_events
diff --git a/backend/v2/deposits/services.py b/backend/v2/deposits/services.py
new file mode 100644
index 0000000000..4b37dd10e4
--- /dev/null
+++ b/backend/v2/deposits/services.py
@@ -0,0 +1,103 @@
+from itertools import groupby
+from operator import attrgetter
+from sqlalchemy.ext.asyncio import AsyncSession
+
+from app.engine.user.effective_deposit import (
+    DepositEvent,
+    EventType,
+)
+from app.modules.common.sablier_events_mapper import (
+    FlattenStrategy,
+    flatten_sablier_events,
+    process_to_locks_and_unlocks,
+)
+from app.modules.user.events_generator.core import unify_deposit_balances
+from v2.deposits.repositories import get_all_deposit_events_for_epoch
+from v2.epochs.subgraphs import EpochsSubgraph
+from v2.sablier.subgraphs import SablierSubgraph
+
+
+class DepositEventsStore:
+    def __init__(
+        self,
+        session: AsyncSession,
+        epochs_subgraph: EpochsSubgraph,
+        sablier_subgraph: SablierSubgraph,
+        sablier_unlock_grace_period: int,
+    ):
+        self.session = session
+        self.epochs_subgraph = epochs_subgraph
+        self.sablier_subgraph = sablier_subgraph
+        self.sablier_unlock_grace_period = sablier_unlock_grace_period
+
+    async def get_all_users_events(
+        self,
+        epoch_number: int,
+        start_sec: int,
+        end_sec: int,
+    ) -> dict[str, list[DepositEvent]]:
+        """
+        Returns all user events (LOCK, UNLOCK) for a given epoch.
+        """
+
+        # Get all locked amounts for the previous epoch
+        epoch_start_locked_amounts = await get_all_deposit_events_for_epoch(
+            self.session, epoch_number - 1
+        )
+        epoch_start_events = [
+            DepositEvent(
+                user=user,
+                type=EventType.LOCK,
+                timestamp=start_sec,
+                amount=0,  # it is not a deposit in fact
+                deposit_before=int(deposit.epoch_end_deposit),
+            )
+            for user, deposit in epoch_start_locked_amounts.items()
+        ]
+
+        sablier_streams = await self.sablier_subgraph.get_all_streams_history()
+        mapped_streams = process_to_locks_and_unlocks(
+            sablier_streams, from_timestamp=start_sec, to_timestamp=end_sec
+        )
+        epoch_events = []
+        epoch_events += flatten_sablier_events(mapped_streams, FlattenStrategy.ALL)
+        epoch_events += await self.epochs_subgraph.fetch_locks_by_timestamp_range(
+            start_sec, end_sec
+        )
+        epoch_events += await self.epochs_subgraph.fetch_unlocks_by_timestamp_range(
+            start_sec, end_sec
+        )
+
+        epoch_events = [DepositEvent.from_dict(event) for event in epoch_events]
+        sorted_events = sorted(epoch_events, key=attrgetter("user", "timestamp"))
+
+        user_events = {
+            k: list(g) for k, g in groupby(sorted_events, key=attrgetter("user"))
+        }
+
+        for event in epoch_start_events:
+            if event.user in user_events:
+                user_events[event.user].insert(0, event)
+            else:
+                user_events[event.user] = [event]
+
+        epoch_start_users = list(map(attrgetter("user"), epoch_start_events))
+        for user_address in user_events:
+            if user_address not in epoch_start_users:
+                user_events[user_address].insert(
+                    0,
+                    DepositEvent(
+                        user_address,
+                        EventType.LOCK,
+                        timestamp=start_sec,
+                        amount=0,
+                        deposit_before=0,
+                    ),
+                )
+
+            user_events[user_address] = unify_deposit_balances(
+                user_events[user_address],
+                self.sablier_unlock_grace_period,
+            )
+
+        return user_events
diff --git a/backend/v2/matched_rewards/dependencies.py b/backend/v2/matched_rewards/dependencies.py
index 668ec571e4..3abf80a79b 100644
--- a/backend/v2/matched_rewards/dependencies.py
+++ b/backend/v2/matched_rewards/dependencies.py
@@ -74,4 +74,4 @@ async def get_matched_rewards_estimator(
 GetMatchedRewardsEstimator = Annotated[
     MatchedRewardsEstimator,
     Depends(get_matched_rewards_estimator),
-]
\ No newline at end of file
+]
diff --git a/backend/v2/project_rewards/capped_quadratic.py b/backend/v2/project_rewards/capped_quadratic.py
index 00fbf2a959..996cc6986b 100644
--- a/backend/v2/project_rewards/capped_quadratic.py
+++ b/backend/v2/project_rewards/capped_quadratic.py
@@ -106,8 +106,8 @@ def capped_quadratic_funding(
 
     return CappedQuadraticFunding(
         project_fundings=project_fundings,
-        amounts_total=amounts_total,
-        matched_total=matched_total,
+        allocations_total_for_all_projects=amounts_total,
+        matched_total_for_all_projects=matched_total,
     )
 
 
diff --git a/backend/v2/project_rewards/router.py b/backend/v2/project_rewards/router.py
index 40ca5ac243..fd6838e835 100644
--- a/backend/v2/project_rewards/router.py
+++ b/backend/v2/project_rewards/router.py
@@ -8,12 +8,11 @@
 from app.engine.user.budget.with_ppf import UserBudgetWithPPF
 from app.modules.snapshots.pending.core import calculate_user_budgets
 from app.modules.staking.proceeds.core import estimate_staking_proceeds
-from v2.matched_rewards.dependencies import GetMatchedRewardsEstimator, GetMatchedRewardsEstimatorInAW, get_matched_rewards_estimator
+from v2.matched_rewards.dependencies import (
+    GetMatchedRewardsEstimator,
+)
 from v2.deposits.dependencies import GetDepositEventsRepository
 from v2.glms.dependencies import GetGLMBalanceOfDeposits
-from v2.project_rewards.user_events import (
-    calculate_effective_deposits,
-)
 from v2.allocations.repositories import (
     get_donors_for_epoch,
     sum_allocations_by_epoch,
@@ -37,6 +36,7 @@
 )
 from v2.core.dependencies import GetCurrentTimestamp, GetSession
 from v2.project_rewards.services import (
+    calculate_effective_deposits,
     calculate_octant_rewards,
     calculate_user_budget,
     get_rewards_merkle_tree_for_epoch,
diff --git a/backend/v2/project_rewards/services.py b/backend/v2/project_rewards/services.py
index 422c2435fc..6ab93bdd6d 100644
--- a/backend/v2/project_rewards/services.py
+++ b/backend/v2/project_rewards/services.py
@@ -8,10 +8,6 @@
 from app.engine.user.budget.with_ppf import UserBudgetWithPPF
 from app.modules.dto import OctantRewardsDTO
 from app.modules.octant_rewards.core import calculate_rewards
-from v2.project_rewards.user_events import (
-    calculate_effective_deposits,
-    simulate_user_events,
-)
 from v2.project_rewards.repositories import get_rewards_for_epoch
 from v2.project_rewards.schemas import (
     RewardsMerkleTreeLeafV1,
@@ -26,6 +22,60 @@
 from v2.projects.contracts import ProjectsContracts
 
 
+from typing import Dict
+
+from app.engine.user.effective_deposit import (
+    DepositEvent,
+    EventType,
+    UserDeposit,
+    UserEffectiveDepositPayload,
+)
+from app.engine.user.effective_deposit.weighted_average.default import (
+    DefaultWeightedAverageEffectiveDeposit,
+)
+
+
+def calculate_effective_deposits(
+    start_sec: int,
+    end_sec: int,
+    events: Dict[str, list[DepositEvent]],
+) -> tuple[list[UserDeposit], int]:
+    # TODO: We can do this better and nicer
+    effective_deposit_calculator = DefaultWeightedAverageEffectiveDeposit()
+    payload = UserEffectiveDepositPayload(
+        epoch_start=start_sec,
+        epoch_end=end_sec,
+        lock_events_by_addr=events,
+    )
+
+    return effective_deposit_calculator.calculate_users_effective_deposits(payload)
+
+
+def simulate_user_events(
+    end_sec: int, lock_duration: int, remaining_sec: int, glm_amount: int
+) -> list[DepositEvent]:
+    user_events = [
+        DepositEvent(
+            user=ZERO_ADDRESS,
+            type=EventType.LOCK,
+            timestamp=end_sec - remaining_sec,
+            amount=glm_amount,
+            deposit_before=0,
+        )
+    ]
+    if lock_duration < remaining_sec:
+        user_events.append(
+            DepositEvent(
+                user=ZERO_ADDRESS,
+                type=EventType.UNLOCK,
+                timestamp=end_sec - remaining_sec + lock_duration,
+                amount=glm_amount,
+                deposit_before=glm_amount,
+            )
+        )
+    return user_events
+
+
 @dataclass
 class ProjectRewardsEstimator:
     # Dependencies
@@ -113,18 +163,16 @@ def calculate_octant_rewards(
     settings = OctantRewardsSettings()
     rewards = calculate_rewards(settings, eth_proceeds, total_effective_deposit)
 
-    # fmt: off
     return OctantRewardsDTO(
-        staking_proceeds            = eth_proceeds,
-        locked_ratio                = rewards.locked_ratio,
-        total_effective_deposit     = total_effective_deposit,
-        total_rewards               = rewards.total_rewards,
-        vanilla_individual_rewards  = rewards.vanilla_individual_rewards,
-        operational_cost            = rewards.operational_cost,
-        ppf                         = rewards.ppf_value,
-        community_fund              = rewards.community_fund,
+        staking_proceeds=eth_proceeds,
+        locked_ratio=rewards.locked_ratio,
+        total_effective_deposit=total_effective_deposit,
+        total_rewards=rewards.total_rewards,
+        vanilla_individual_rewards=rewards.vanilla_individual_rewards,
+        operational_cost=rewards.operational_cost,
+        ppf=rewards.ppf_value,
+        community_fund=rewards.community_fund,
     )
-    # fmt: on
 
 
 def simulate_user_effective_deposits(
diff --git a/backend/v2/project_rewards/user_events.py b/backend/v2/project_rewards/user_events.py
deleted file mode 100644
index 4d28d8f000..0000000000
--- a/backend/v2/project_rewards/user_events.py
+++ /dev/null
@@ -1,120 +0,0 @@
-from typing import Dict
-
-from app.engine.octant_rewards import OctantRewardsSettings
-from app.engine.user.budget.with_ppf import UserBudgetWithPPF
-from app.engine.user.effective_deposit import (
-    DepositEvent,
-    EventType,
-    UserDeposit,
-    UserEffectiveDepositPayload,
-)
-from app.modules.dto import OctantRewardsDTO, PendingSnapshotDTO
-from app.modules.octant_rewards.core import calculate_rewards
-from app.modules.snapshots.pending.core import calculate_user_budgets
-from app.modules.staking.proceeds.core import estimate_staking_proceeds
-from app.constants import (
-    ZERO_ADDRESS,
-)
-from app.engine.user.effective_deposit.weighted_average.default import (
-    DefaultWeightedAverageEffectiveDeposit,
-)
-from v2.deposits.repositories import DepositEventsRepository
-
-
-def calculate_effective_deposits(
-    start_sec: int,
-    end_sec: int,
-    events: Dict[str, list[DepositEvent]],
-) -> tuple[list[UserDeposit], int]:
-    # TODO: We can do this better and nicer
-    effective_deposit_calculator = DefaultWeightedAverageEffectiveDeposit()
-    payload = UserEffectiveDepositPayload(
-        epoch_start=start_sec,
-        epoch_end=end_sec,
-        lock_events_by_addr=events,
-    )
-
-    return effective_deposit_calculator.calculate_users_effective_deposits(payload)
-
-
-async def calculate_pending_epoch_snapshot(
-    deposit_events: DepositEventsRepository,
-    epoch_number: int,
-    epoch_start: int,
-    epoch_end: int,
-) -> PendingSnapshotDTO:
-    # Get octant rewards
-    # epoch_details = await epochs_subgraph.fetch_epoch_by_number(epoch_number)
-    # duration_sec = epoch_details.duration
-    # return estimate_staking_proceeds(duration_sec)
-    # eth_proceeds = await get_staking_proceeds(session, epoch_number, start_sec, end_sec)
-    eth_proceeds = estimate_staking_proceeds(epoch_end - epoch_start)
-
-    events = await deposit_events.get_all_users_events(
-        epoch_number, epoch_start, epoch_end
-    )
-    user_deposits, total_effective_deposit = calculate_effective_deposits(
-        epoch_start, epoch_end, events
-    )
-
-    # total_effective_deposit = 155654569757136462439580980
-    rewards_settings = OctantRewardsSettings()
-
-    octant_rewards = calculate_rewards(
-        rewards_settings, eth_proceeds, total_effective_deposit
-    )
-
-    rewards = OctantRewardsDTO(
-        staking_proceeds=eth_proceeds,
-        locked_ratio=octant_rewards.locked_ratio,
-        total_effective_deposit=total_effective_deposit,
-        total_rewards=octant_rewards.total_rewards,
-        vanilla_individual_rewards=octant_rewards.vanilla_individual_rewards,
-        operational_cost=octant_rewards.operational_cost,
-        ppf=octant_rewards.ppf_value,
-        community_fund=octant_rewards.community_fund,
-    )
-
-    # events = await get_all_user_events(
-    #     session,
-    #     epochs_subgraph,
-    #     sablier,
-    #     epoch_number,
-    #     epoch_start,
-    #     epoch_end
-    # )
-    # user_deposits, total_effective_deposit = calculate_effective_deposits(epoch_start, epoch_end, events)
-
-    user_budget_calculator = UserBudgetWithPPF()
-    user_budgets = calculate_user_budgets(
-        user_budget_calculator, rewards, user_deposits
-    )
-
-    return PendingSnapshotDTO(
-        rewards=rewards, user_deposits=user_deposits, user_budgets=user_budgets
-    )
-
-
-def simulate_user_events(
-    end_sec: int, lock_duration: int, remaining_sec: int, glm_amount: int
-) -> list[DepositEvent]:
-    user_events = [
-        DepositEvent(
-            user=ZERO_ADDRESS,
-            type=EventType.LOCK,
-            timestamp=end_sec - remaining_sec,
-            amount=glm_amount,
-            deposit_before=0,
-        )
-    ]
-    if lock_duration < remaining_sec:
-        user_events.append(
-            DepositEvent(
-                user=ZERO_ADDRESS,
-                type=EventType.UNLOCK,
-                timestamp=end_sec - remaining_sec + lock_duration,
-                amount=glm_amount,
-                deposit_before=glm_amount,
-            )
-        )
-    return user_events
diff --git a/backend/v2/sablier/dependencies.py b/backend/v2/sablier/dependencies.py
index b58600f539..2d59a3137d 100644
--- a/backend/v2/sablier/dependencies.py
+++ b/backend/v2/sablier/dependencies.py
@@ -28,7 +28,7 @@ def get_sablier_subgraph(chain_settings: GetChainSettings) -> SablierSubgraph:
     Based on the chain type (mainnet or sepolia), return the appropriate SablierSubgraph.
     """
 
-    sablier_settings = SablierSubgraphSettings()
+    sablier_settings = SablierSubgraphSettings()  # type: ignore[call-arg]
 
     if chain_settings.chain_id == ChainTypes.MAINNET:
         return SablierSubgraph(
diff --git a/backend/v2/sablier/subgraphs.py b/backend/v2/sablier/subgraphs.py
index c2b0785044..6ec18b4a62 100644
--- a/backend/v2/sablier/subgraphs.py
+++ b/backend/v2/sablier/subgraphs.py
@@ -114,9 +114,9 @@ async def get_all_streams_history(self) -> list[SablierStream]:
             "tokenAddress": self.token_address,
         }
 
+        streams = await self._fetch_streams(query, variables)
         # Cache into file to avoid rate limiting
         if not os.path.exists("sablier_streams.json"):
-            streams = await self._fetch_streams(query, variables)
             with open("sablier_streams.json", "w") as f:
                 json.dump(streams, f)
         else:

From 8ab667143d9d23cd4221efc0a433a905c7e15b9c Mon Sep 17 00:00:00 2001
From: adam-gf <adam@golem.foundation>
Date: Tue, 28 Jan 2025 01:54:58 +0100
Subject: [PATCH 06/14] fix

---
 backend/app/settings.py | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/backend/app/settings.py b/backend/app/settings.py
index c1b5f1656f..eb2fe7bc99 100644
--- a/backend/app/settings.py
+++ b/backend/app/settings.py
@@ -117,10 +117,7 @@ class DevConfig(Config):
     CHAIN_ID = int(os.getenv("CHAIN_ID", 1337))
     # Put the db file in project root
     DB_PATH = os.path.join(Config.PROJECT_ROOT, DB_NAME)
-    # SQLALCHEMY_DATABASE_URI = f"sqlite:///{DB_PATH}"
-    SQLALCHEMY_DATABASE_URI = (
-        f"postgresql://postgres:mysecretpassword@localhost:5433/postgres"
-    )
+    SQLALCHEMY_DATABASE_URI = f"sqlite:///{DB_PATH}"
     SUBGRAPH_RETRY_TIMEOUT_SEC = 2
     X_REAL_IP_REQUIRED = parse_bool(os.getenv("X_REAL_IP_REQUIRED", "false"))
     CACHE_TYPE = "SimpleCache"

From 7aa499f00f5e687c29c6023e7459da865187688f Mon Sep 17 00:00:00 2001
From: adam-gf <adam@golem.foundation>
Date: Tue, 28 Jan 2025 01:56:35 +0100
Subject: [PATCH 07/14] fix

---
 backend/app/context/epoch/block_range.py | 2 --
 1 file changed, 2 deletions(-)

diff --git a/backend/app/context/epoch/block_range.py b/backend/app/context/epoch/block_range.py
index 95350c60c3..44806c88af 100644
--- a/backend/app/context/epoch/block_range.py
+++ b/backend/app/context/epoch/block_range.py
@@ -15,6 +15,4 @@ def get_blocks_range(
     start_block = get_block_num_from_ts(start_sec) if start_sec <= now_sec else None
     end_block = get_block_num_from_ts(end_sec) if end_sec <= now_sec else None
 
-    start_block = None
-    end_block = None
     return start_block, end_block

From 7054fb3484f255c5ca00435bfb670966618c884e Mon Sep 17 00:00:00 2001
From: adam-gf <adam@golem.foundation>
Date: Tue, 28 Jan 2025 02:12:04 +0100
Subject: [PATCH 08/14] fix

---
 backend/tests/v2/project_rewards/conftest.py | 2 ++
 1 file changed, 2 insertions(+)
 create mode 100644 backend/tests/v2/project_rewards/conftest.py

diff --git a/backend/tests/v2/project_rewards/conftest.py b/backend/tests/v2/project_rewards/conftest.py
new file mode 100644
index 0000000000..496fae94e6
--- /dev/null
+++ b/backend/tests/v2/project_rewards/conftest.py
@@ -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
\ No newline at end of file

From 9dee0232c05f409f26b453f89733cdc6318cb90d Mon Sep 17 00:00:00 2001
From: adam-gf <adam@golem.foundation>
Date: Tue, 28 Jan 2025 02:37:15 +0100
Subject: [PATCH 09/14] fix

---
 backend/tests/v2/project_rewards/conftest.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/backend/tests/v2/project_rewards/conftest.py b/backend/tests/v2/project_rewards/conftest.py
index 496fae94e6..651063bb62 100644
--- a/backend/tests/v2/project_rewards/conftest.py
+++ b/backend/tests/v2/project_rewards/conftest.py
@@ -1,2 +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
\ No newline at end of file
+from tests.v2.fake_subgraphs.conftest import fake_epochs_subgraph_factory  # noqa: F401

From 671592fac7ba7c7d567df9c6e520f0b5010822a3 Mon Sep 17 00:00:00 2001
From: adam-gf <adam@golem.foundation>
Date: Tue, 28 Jan 2025 09:21:04 +0100
Subject: [PATCH 10/14] adds missing fixture

---
 backend/tests/v2/project_rewards/test_estimated_budged.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/backend/tests/v2/project_rewards/test_estimated_budged.py b/backend/tests/v2/project_rewards/test_estimated_budged.py
index 5fc6544b24..04af41db6e 100644
--- a/backend/tests/v2/project_rewards/test_estimated_budged.py
+++ b/backend/tests/v2/project_rewards/test_estimated_budged.py
@@ -161,6 +161,7 @@ async def test_estimates_budget_for_multiple_epochs(
 @pytest.mark.asyncio
 async def test_validates_request_parameters(
     fast_client: AsyncClient,
+    fast_session: AsyncSession,
 ):
     """Should validate request parameters"""
     async with fast_client as client:

From 3d966f7d31b43ecc7816478e769eaf253dbae327 Mon Sep 17 00:00:00 2001
From: adam-gf <adam@golem.foundation>
Date: Wed, 29 Jan 2025 10:04:08 +0100
Subject: [PATCH 11/14] Adds allocation simulation

---
 backend/v2/allocations/router.py   | 39 +++++++++---
 backend/v2/allocations/schemas.py  | 10 ++--
 backend/v2/allocations/services.py | 95 +++++++++++++++++++++++++++++-
 3 files changed, 128 insertions(+), 16 deletions(-)

diff --git a/backend/v2/allocations/router.py b/backend/v2/allocations/router.py
index 58480c6f27..af07955cb5 100644
--- a/backend/v2/allocations/router.py
+++ b/backend/v2/allocations/router.py
@@ -1,5 +1,10 @@
 from typing import Annotated
 from fastapi import APIRouter, Query
+from v2.matched_rewards.dependencies import GetMatchedRewardsEstimator
+from v2.projects.dependencies import GetProjectsContracts
+from v2.uniqueness_quotients.dependencies import GetUQScoreGetter
+from v2.allocations.services import simulate_allocation
+from v2.epochs.dependencies import GetOpenAllocationWindowEpochNumber
 from v2.allocations.repositories import (
     get_all_allocations_for_epoch,
     get_allocations_by_user,
@@ -15,6 +20,8 @@
     EpochAllocationsResponseV1,
     EpochDonorsResponseV1,
     ProjectAllocationV1,
+    SimulateAllocationPayloadV1,
+    SimulateAllocationResponseV1,
     UserAllocationNonceV1,
     UserAllocationRequest,
     UserAllocationRequestV1,
@@ -83,15 +90,29 @@ async def get_all_allocations_for_epoch_v1(
     return EpochAllocationsResponseV1(allocations=donations)
 
 
-# @api.post("/leverage/{user_address}")
-# async def simulate_allocation_v1(
-#     user_address: Address, payload: SimulateAllocationPayloadV1
-# ) -> None:
-#     """
-#     Simulates an allocation and get the expected leverage, threshold and matched rewards.
-#     """
-#     # TODO: implement
-#     pass
+@api.post("/leverage/{user_address}")
+async def simulate_allocation_v1(
+    session: GetSession,
+    projects_contracts: GetProjectsContracts,
+    matched_rewards_estimator: GetMatchedRewardsEstimator,
+    uq_score_getter: GetUQScoreGetter,
+    pending_epoch_number: GetOpenAllocationWindowEpochNumber,
+    # Request Parameters
+    user_address: Address,
+    payload: SimulateAllocationPayloadV1,
+) -> SimulateAllocationResponseV1:
+    """
+    Simulates an allocation and get the expected leverage, threshold and matched rewards.
+    """
+    return await simulate_allocation(
+        session,
+        projects_contracts,
+        matched_rewards_estimator,
+        uq_score_getter,
+        pending_epoch_number,
+        user_address,
+        payload.allocations,
+    )
 
 
 @api.get("/project/{project_address}/epoch/{epoch_number}")
diff --git a/backend/v2/allocations/schemas.py b/backend/v2/allocations/schemas.py
index 633059b196..912a7b84c7 100644
--- a/backend/v2/allocations/schemas.py
+++ b/backend/v2/allocations/schemas.py
@@ -66,16 +66,16 @@ class UserAllocationsResponseV1(OctantModel):
     is_manually_edited: bool | None
 
 
-class SimulateAllocationPayloadV1(OctantModel):
-    allocations: list[AllocationRequestV1]
-
-
 class ProjectMatchedRewardsV1(OctantModel):
     address: Address
     value: BigInteger
 
 
+class SimulateAllocationPayloadV1(OctantModel):
+    allocations: list[AllocationRequestV1]
+
+
 class SimulateAllocationResponseV1(OctantModel):
     leverage: Decimal
-    threshold: int
+    threshold: BigInteger | None
     matched: list[ProjectMatchedRewardsV1]
diff --git a/backend/v2/allocations/services.py b/backend/v2/allocations/services.py
index 46e1c8b06d..f5117fe1bb 100644
--- a/backend/v2/allocations/services.py
+++ b/backend/v2/allocations/services.py
@@ -2,15 +2,27 @@
 
 from app import exceptions
 from sqlalchemy.ext.asyncio import AsyncSession
+from v2.core.types import Address
 from v2.allocations.repositories import (
     get_allocations_with_user_uqs,
     soft_delete_user_allocations_by_epoch,
     store_allocation_request,
 )
-from v2.allocations.schemas import AllocationWithUserUQScore, UserAllocationRequest
+from v2.allocations.schemas import (
+    AllocationRequestV1,
+    AllocationWithUserUQScore,
+    ProjectMatchedRewardsV1,
+    SimulateAllocationResponseV1,
+    UserAllocationRequest,
+)
 from v2.allocations.validators import SignatureVerifier
 from v2.matched_rewards.services import MatchedRewardsEstimator
-from v2.project_rewards.capped_quadratic import cqf_simulate_leverage
+from v2.project_rewards.capped_quadratic import (
+    MR_FUNDING_CAP_PERCENT,
+    capped_quadratic_funding,
+    cqf_calculate_individual_leverage,
+    cqf_simulate_leverage,
+)
 from v2.projects.contracts import ProjectsContracts
 from v2.uniqueness_quotients.dependencies import UQScoreGetter
 from v2.users.repositories import get_user_by_address
@@ -112,6 +124,85 @@ async def allocate(
     return request.user_address
 
 
+async def simulate_allocation(
+    # Component dependencies
+    session: AsyncSession,
+    projects_contracts: ProjectsContracts,
+    matched_rewards_estimator: MatchedRewardsEstimator,
+    uq_score_getter: UQScoreGetter,
+    # Arguments
+    epoch_number: int,
+    user_address: Address,
+    new_allocations: list[AllocationRequestV1],
+) -> SimulateAllocationResponseV1:
+    """
+    Simulate the allocation of the user and calculate the leverage and matched project rewards.
+    This just "ignores" the user's current allocations and calculates the leverage as if the user
+    had made the allocation.
+    """
+
+    # Get or calculate UQ score of the user
+    user_uq_score = await uq_score_getter.get_or_calculate(
+        epoch_number=epoch_number,
+        user_address=user_address,
+    )
+    new_allocations_with_uq = [
+        AllocationWithUserUQScore(
+            project_address=a.project_address,
+            amount=a.amount,
+            user_address=user_address,
+            user_uq_score=user_uq_score,
+        )
+        for a in new_allocations
+    ]
+
+    # Calculate leverage and matched project rewards
+    all_projects = await projects_contracts.get_project_addresses(epoch_number)
+    existing_allocations = await get_allocations_with_user_uqs(session, epoch_number)
+    matched_rewards = await matched_rewards_estimator.get()
+
+    allocations_without_user = [
+        a for a in existing_allocations if a.user_address != user_address
+    ]
+
+    # Calculate capped quadratic funding before and after the user's allocation
+    # Leverage is more or less a difference between the two (before - after)
+    before_allocation = capped_quadratic_funding(
+        allocations_without_user,
+        matched_rewards,
+        all_projects,
+        MR_FUNDING_CAP_PERCENT,
+    )
+    after_allocation = capped_quadratic_funding(
+        allocations_without_user + new_allocations_with_uq,
+        matched_rewards,
+        all_projects,
+        MR_FUNDING_CAP_PERCENT,
+    )
+
+    leverage = cqf_calculate_individual_leverage(
+        new_allocations_amount=sum(a.amount for a in new_allocations_with_uq),
+        project_addresses=[a.project_address for a in new_allocations_with_uq],
+        before_allocation=before_allocation,
+        after_allocation=after_allocation,
+    )
+
+    return SimulateAllocationResponseV1(
+        leverage=leverage,
+        threshold=None,
+        matched=sorted(
+            [
+                ProjectMatchedRewardsV1(
+                    address=p.address,
+                    value=p.matched,
+                )
+                for p in after_allocation.project_fundings.values()
+            ],
+            key=lambda x: x.address,
+        ),
+    )
+
+
 async def simulate_leverage(
     # Component dependencies
     session: AsyncSession,

From 8ed406dfd052949c8f56e154ce331bad6eb5a871 Mon Sep 17 00:00:00 2001
From: adam-gf <adam@golem.foundation>
Date: Wed, 29 Jan 2025 10:04:58 +0100
Subject: [PATCH 12/14] fix: pr comments

---
 backend/v2/project_rewards/capped_quadratic.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/backend/v2/project_rewards/capped_quadratic.py b/backend/v2/project_rewards/capped_quadratic.py
index 996cc6986b..1c3b27e9e1 100644
--- a/backend/v2/project_rewards/capped_quadratic.py
+++ b/backend/v2/project_rewards/capped_quadratic.py
@@ -10,8 +10,8 @@
 
 class CappedQuadraticFunding(NamedTuple):
     project_fundings: dict[Address, ProjectFundingSummaryV1]
-    allocations_total_for_all_projects: Decimal  # Sum of all allocation amounts for all projects
-    matched_total_for_all_projects: Decimal  # Sum of all matched rewards for all projects
+    allocations_total_for_all_projects: Decimal
+    matched_total_for_all_projects: Decimal
 
 
 MR_FUNDING_CAP_PERCENT = Decimal("0.2")

From b850e2fbf695a1ed091afaa8f3fbd6857e9ec457 Mon Sep 17 00:00:00 2001
From: adam-gf <adam@golem.foundation>
Date: Wed, 29 Jan 2025 12:16:55 +0100
Subject: [PATCH 13/14] fix

---
 backend/v2/sablier/subgraphs.py | 13 +------------
 1 file changed, 1 insertion(+), 12 deletions(-)

diff --git a/backend/v2/sablier/subgraphs.py b/backend/v2/sablier/subgraphs.py
index 6ec18b4a62..9bde21000c 100644
--- a/backend/v2/sablier/subgraphs.py
+++ b/backend/v2/sablier/subgraphs.py
@@ -55,15 +55,12 @@ async def _fetch_streams(self, query: str, variables: dict) -> list[SablierStrea
         while has_more:
             variables.update({"limit": limit, "skip": skip})
 
-            # logger.debug(f"[Sablier Subgraph] Querying streams with skip: {skip}")
             result = await self.gql_client.execute_async(
                 gql(query), variable_values=variables
             )
 
             streams = result.get("streams", [])
 
-            # app.logger.debug(f"[Sablier Subgraph] Received {len(streams)} streams.")
-
             for stream in streams:
                 actions = stream.get("actions", [])
                 final_intact_amount = stream.get("intactAmount", 0)
@@ -114,12 +111,4 @@ async def get_all_streams_history(self) -> list[SablierStream]:
             "tokenAddress": self.token_address,
         }
 
-        streams = await self._fetch_streams(query, variables)
-        # Cache into file to avoid rate limiting
-        if not os.path.exists("sablier_streams.json"):
-            with open("sablier_streams.json", "w") as f:
-                json.dump(streams, f)
-        else:
-            with open("sablier_streams.json", "r") as f:
-                streams = json.load(f)
-        return streams
+        return await self._fetch_streams(query, variables)

From 0e13fc9788996ee420af9d5f46e4224975a874be Mon Sep 17 00:00:00 2001
From: adam-gf <adam@golem.foundation>
Date: Wed, 29 Jan 2025 12:17:38 +0100
Subject: [PATCH 14/14] fix

---
 backend/v2/sablier/subgraphs.py | 2 --
 1 file changed, 2 deletions(-)

diff --git a/backend/v2/sablier/subgraphs.py b/backend/v2/sablier/subgraphs.py
index 9bde21000c..c6fce77843 100644
--- a/backend/v2/sablier/subgraphs.py
+++ b/backend/v2/sablier/subgraphs.py
@@ -1,6 +1,4 @@
 import logging
-import os
-from flask import json
 from gql import Client, gql
 from gql.transport.aiohttp import AIOHTTPTransport
 from gql.client import log as requests_logger