From 77572ddcdc5922b64aa7dfb81f3a66a737b708e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kujawski?= Date: Thu, 14 Mar 2024 10:25:29 +0100 Subject: [PATCH] OCT-1354: move withdrawals to new arch (#65) --- .../infrastructure/database/allocations.py | 7 +- .../app/infrastructure/routes/withdrawals.py | 8 +- backend/app/legacy/controllers/rewards.py | 2 +- backend/app/legacy/controllers/withdrawals.py | 56 --- backend/app/legacy/core/allocations.py | 9 - backend/app/legacy/core/user/rewards.py | 35 -- backend/app/modules/common/merkle_tree.py | 7 +- backend/app/modules/dto.py | 14 + .../app/modules/modules_factory/finalized.py | 16 +- .../app/modules/modules_factory/finalizing.py | 5 + .../app/modules/modules_factory/pending.py | 5 + .../app/modules/modules_factory/protocols.py | 15 +- .../modules/snapshots/finalized/controller.py | 3 +- .../user/allocations/service/pending.py | 18 +- .../modules/user/allocations/service/saved.py | 15 +- .../user/rewards/service/calculated.py | 18 + backend/app/modules/withdrawals/controller.py | 21 + backend/app/modules/withdrawals/core.py | 61 +++ .../modules/withdrawals/service/finalized.py | 24 ++ .../modules/withdrawals/service/pending.py | 44 ++ backend/tests/conftest.py | 5 +- backend/tests/database/test_allocations_db.py | 4 +- backend/tests/legacy/test_user.py | 78 ---- backend/tests/legacy/test_withdrawals.py | 388 ------------------ .../modules_factory/test_modules_factory.py | 20 +- .../allocations/test_pending_allocations.py | 48 --- .../allocations/test_saved_allocations.py | 41 ++ .../user/rewards/test_calculated_rewards.py | 46 +++ .../withdrawals/test_withdrawals_core.py | 114 +++++ .../withdrawals/test_withdrawals_finalized.py | 44 ++ .../withdrawals/test_withdrawals_pending.py | 40 ++ 31 files changed, 548 insertions(+), 663 deletions(-) delete mode 100644 backend/app/legacy/controllers/withdrawals.py delete mode 100644 backend/app/legacy/core/user/rewards.py create mode 100644 backend/app/modules/withdrawals/controller.py create mode 100644 backend/app/modules/withdrawals/core.py create mode 100644 backend/app/modules/withdrawals/service/finalized.py create mode 100644 backend/app/modules/withdrawals/service/pending.py delete mode 100644 backend/tests/legacy/test_withdrawals.py create mode 100644 backend/tests/modules/withdrawals/test_withdrawals_core.py create mode 100644 backend/tests/modules/withdrawals/test_withdrawals_finalized.py create mode 100644 backend/tests/modules/withdrawals/test_withdrawals_pending.py diff --git a/backend/app/infrastructure/database/allocations.py b/backend/app/infrastructure/database/allocations.py index ee4d857565..9acb50e4db 100644 --- a/backend/app/infrastructure/database/allocations.py +++ b/backend/app/infrastructure/database/allocations.py @@ -123,7 +123,7 @@ def get_users_with_allocations(epoch_num: int) -> List[str]: return [u.address for u in users] -def get_alloc_sum_by_epoch_and_user_address(epoch: int) -> List[AccountFundsDTO]: +def get_users_alloc_sum_by_epoch(epoch: int) -> List[AccountFundsDTO]: allocations = ( db.session.query(User, Allocation) .join(User, User.id == Allocation.user_id) @@ -157,6 +157,11 @@ def get_alloc_sum_by_epoch(epoch: int) -> int: return sum([int(a.amount) for a in allocations]) +def get_user_alloc_sum_by_epoch(epoch: int, user_address: str) -> int: + allocations = get_all_by_user_addr_and_epoch(user_address, epoch) + return sum([int(a.amount) for a in allocations]) + + def add_all(epoch: int, user_id: int, nonce: int, allocations): now = datetime.utcnow() diff --git a/backend/app/infrastructure/routes/withdrawals.py b/backend/app/infrastructure/routes/withdrawals.py index 5c53937aad..c371b3a0d9 100644 --- a/backend/app/infrastructure/routes/withdrawals.py +++ b/backend/app/infrastructure/routes/withdrawals.py @@ -1,11 +1,9 @@ -import dataclasses - from flask import current_app as app from flask_restx import Namespace, fields -from app.legacy.controllers import withdrawals from app.extensions import api from app.infrastructure import OctantResource +from app.modules.withdrawals.controller import get_withdrawable_eth ns = Namespace("withdrawals", description="Octant withdrawals") api.add_namespace(ns) @@ -49,9 +47,7 @@ class Withdrawals(OctantResource): @ns.marshal_with(withdrawable_rewards_model) def get(self, address): app.logger.debug(f"Getting withdrawable eth for address: {address}") - result = [ - dataclasses.asdict(w) for w in withdrawals.get_withdrawable_eth(address) - ] + result = get_withdrawable_eth(address) app.logger.debug(f"Withdrawable eth for address: {address}: {result}") return result diff --git a/backend/app/legacy/controllers/rewards.py b/backend/app/legacy/controllers/rewards.py index 7716a5a8e8..fb6fa5225d 100644 --- a/backend/app/legacy/controllers/rewards.py +++ b/backend/app/legacy/controllers/rewards.py @@ -118,7 +118,7 @@ def get_rewards_merkle_tree(epoch: int) -> RewardsMerkleTree: if not core_epoch_snapshots.has_finalized_epoch_snapshot(epoch): raise exceptions.InvalidEpoch - mt = merkle_tree.get_merkle_tree_for_epoch(epoch) + mt = merkle_tree.get_rewards_merkle_tree_for_epoch(epoch) leaves = [ RewardsMerkleTreeLeaf(address=leaf.value[0], amount=leaf.value[1]) for leaf in mt.values diff --git a/backend/app/legacy/controllers/withdrawals.py b/backend/app/legacy/controllers/withdrawals.py deleted file mode 100644 index d594d530fd..0000000000 --- a/backend/app/legacy/controllers/withdrawals.py +++ /dev/null @@ -1,56 +0,0 @@ -from dataclasses import dataclass -from enum import StrEnum -from typing import List - -from app.extensions import vault, epochs -from app.infrastructure import database -from app.infrastructure.graphql.merkle_roots import get_all_vault_merkle_roots -from app.legacy.core.user.rewards import get_user_claimed_rewards -from app.modules.common.merkle_tree import get_proof_by_address_and_epoch - - -class WithdrawalStatus(StrEnum): - PENDING = "pending" - AVAILABLE = "available" - - -@dataclass(frozen=True) -class WithdrawableEth: - epoch: int - amount: int - proof: List[str] - status: WithdrawalStatus - - -def get_withdrawable_eth(address: str) -> List[WithdrawableEth]: - pending_epoch = epochs.get_pending_epoch() - last_claimed_epoch = vault.get_last_claimed_epoch(address) - rewards = database.rewards.get_by_address_and_epoch_gt(address, last_claimed_epoch) - merkle_roots_epochs = [mr["epoch"] for mr in get_all_vault_merkle_roots()] - - withdrawable_eth = [] - - if pending_epoch is not None: - pending_rewards = get_user_claimed_rewards(address, pending_epoch) - withdrawable_eth.append( - WithdrawableEth( - pending_epoch, pending_rewards, [], WithdrawalStatus.PENDING - ) - ) - - for r in rewards: - status = ( - WithdrawalStatus.AVAILABLE - if r.epoch in merkle_roots_epochs - else WithdrawalStatus.PENDING - ) - withdrawable_eth.append( - WithdrawableEth( - r.epoch, - int(r.amount), - get_proof_by_address_and_epoch(address, r.epoch), - status, - ) - ) - - return withdrawable_eth diff --git a/backend/app/legacy/core/allocations.py b/backend/app/legacy/core/allocations.py index 3e8dc1989d..c19efccaaf 100644 --- a/backend/app/legacy/core/allocations.py +++ b/backend/app/legacy/core/allocations.py @@ -130,12 +130,3 @@ def next_allocation_nonce(user: User | None) -> int: if user.allocation_nonce is None: return 0 return user.allocation_nonce + 1 - - -def has_user_allocated_rewards(user_address: str, epoch: int) -> List[str]: - allocation_signature = ( - database.allocations.get_allocation_request_by_user_and_epoch( - user_address, epoch - ) - ) - return allocation_signature is not None diff --git a/backend/app/legacy/core/user/rewards.py b/backend/app/legacy/core/user/rewards.py deleted file mode 100644 index 3d7d030091..0000000000 --- a/backend/app/legacy/core/user/rewards.py +++ /dev/null @@ -1,35 +0,0 @@ -from typing import List - -from app.infrastructure import database -from app.legacy.core.allocations import has_user_allocated_rewards -from app.legacy.core.common import AccountFunds -from app.legacy.core.user.budget import get_budget - - -def get_all_claimed_rewards(epoch: int) -> (List[AccountFunds], int): - rewards_sum = 0 - rewards = [] - - for allocation in database.allocations.get_alloc_sum_by_epoch_and_user_address( - epoch - ): - user_budget = get_budget(allocation.address, epoch) - claimed_rewards = user_budget - allocation.amount - if claimed_rewards > 0: - rewards.append(AccountFunds(allocation.address, claimed_rewards)) - rewards_sum += claimed_rewards - - return rewards, rewards_sum - - -def get_user_claimed_rewards(address: str, epoch: int) -> int: - if not has_user_allocated_rewards(address, epoch): - return 0 - - user_allocation = database.allocations.get_all_by_user_addr_and_epoch( - address, epoch - ) - user_budget = get_budget(address, epoch) - allocation_sum = sum([int(a.amount) for a in user_allocation]) - - return user_budget - allocation_sum diff --git a/backend/app/modules/common/merkle_tree.py b/backend/app/modules/common/merkle_tree.py index 6e047c4a68..34aac82fa9 100644 --- a/backend/app/modules/common/merkle_tree.py +++ b/backend/app/modules/common/merkle_tree.py @@ -9,12 +9,7 @@ LEAF_ENCODING: List[str] = ["address", "uint256"] -def get_proof_by_address_and_epoch(address: str, epoch: int) -> List[str]: - merkle_tree = get_merkle_tree_for_epoch(epoch) - return get_proof(merkle_tree, address) - - -def get_merkle_tree_for_epoch(epoch: int) -> StandardMerkleTree: +def get_rewards_merkle_tree_for_epoch(epoch: int) -> StandardMerkleTree: leaves = [ AccountFundsDTO(r.address, int(r.amount)) for r in database.rewards.get_by_epoch(epoch) diff --git a/backend/app/modules/dto.py b/backend/app/modules/dto.py index d37a84664c..6bc75c36e8 100644 --- a/backend/app/modules/dto.py +++ b/backend/app/modules/dto.py @@ -1,5 +1,6 @@ from dataclasses import dataclass from decimal import Decimal +from enum import StrEnum from typing import Optional, List from dataclass_wizard import JSONWizard @@ -55,3 +56,16 @@ class OctantRewardsDTO(JSONWizard): @dataclass(frozen=True) class AllocationDTO(AllocationPayload, JSONWizard): user_address: Optional[str] = None + + +class WithdrawalStatus(StrEnum): + PENDING = "pending" + AVAILABLE = "available" + + +@dataclass(frozen=True) +class WithdrawableEth: + epoch: int + amount: int + proof: list[str] + status: WithdrawalStatus diff --git a/backend/app/modules/modules_factory/finalized.py b/backend/app/modules/modules_factory/finalized.py index 24a2daf9c8..44d20f75bb 100644 --- a/backend/app/modules/modules_factory/finalized.py +++ b/backend/app/modules/modules_factory/finalized.py @@ -9,6 +9,7 @@ TotalEffectiveDeposits, Leverage, UserBudgets, + WithdrawalsService, ) from app.modules.octant_rewards.service.finalized import FinalizedOctantRewards from app.modules.user.allocations.service.saved import SavedUserAllocations @@ -16,6 +17,7 @@ from app.modules.user.deposits.service.saved import SavedUserDeposits from app.modules.user.patron_mode.service.events_based import EventsBasedUserPatronMode from app.modules.user.rewards.service.saved import SavedUserRewards +from app.modules.withdrawals.service.finalized import FinalizedWithdrawals from app.pydantic import Model @@ -34,12 +36,19 @@ class FinalizedServices(Model): user_patron_mode_service: UserPatronMode user_budgets_service: UserBudgets user_rewards_service: UserRewards + withdrawals_service: WithdrawalsService @staticmethod def create() -> "FinalizedServices": events_based_patron_mode = EventsBasedUserPatronMode() saved_user_allocations = SavedUserAllocations() saved_user_budgets = SavedUserBudgets() + user_rewards = SavedUserRewards( + user_budgets=saved_user_budgets, + patrons_mode=events_based_patron_mode, + allocations=saved_user_allocations, + ) + withdrawals_service = FinalizedWithdrawals(user_rewards=user_rewards) return FinalizedServices( user_deposits_service=SavedUserDeposits(), @@ -47,9 +56,6 @@ def create() -> "FinalizedServices": user_allocations_service=saved_user_allocations, user_patron_mode_service=events_based_patron_mode, user_budgets_service=saved_user_budgets, - user_rewards_service=SavedUserRewards( - user_budgets=saved_user_budgets, - patrons_mode=events_based_patron_mode, - allocations=saved_user_allocations, - ), + user_rewards_service=user_rewards, + withdrawals_service=withdrawals_service, ) diff --git a/backend/app/modules/modules_factory/finalizing.py b/backend/app/modules/modules_factory/finalizing.py index 53780c7359..d591c0e1e4 100644 --- a/backend/app/modules/modules_factory/finalizing.py +++ b/backend/app/modules/modules_factory/finalizing.py @@ -10,6 +10,7 @@ Leverage, UserBudgets, CreateFinalizedSnapshots, + WithdrawalsService, ) from app.modules.octant_rewards.service.pending import PendingOctantRewards from app.modules.snapshots.finalized.service.finalizing import FinalizingSnapshots @@ -18,6 +19,7 @@ from app.modules.user.deposits.service.saved import SavedUserDeposits from app.modules.user.patron_mode.service.events_based import EventsBasedUserPatronMode from app.modules.user.rewards.service.calculated import CalculatedUserRewards +from app.modules.withdrawals.service.pending import PendingWithdrawals from app.pydantic import Model @@ -37,6 +39,7 @@ class FinalizingServices(Model): user_budgets_service: UserBudgets user_rewards_service: UserRewards finalized_snapshots_service: CreateFinalizedSnapshots + withdrawals_service: WithdrawalsService @staticmethod def create() -> "FinalizingServices": @@ -54,6 +57,7 @@ def create() -> "FinalizingServices": user_rewards=user_rewards, patrons_mode=events_based_patron_mode, ) + withdrawals_service = PendingWithdrawals(user_rewards=user_rewards) return FinalizingServices( user_deposits_service=SavedUserDeposits(), @@ -63,4 +67,5 @@ def create() -> "FinalizingServices": user_budgets_service=saved_user_budgets, user_rewards_service=user_rewards, finalized_snapshots_service=finalized_snapshots_service, + withdrawals_service=withdrawals_service, ) diff --git a/backend/app/modules/modules_factory/pending.py b/backend/app/modules/modules_factory/pending.py index 39d932f671..99ad41adb7 100644 --- a/backend/app/modules/modules_factory/pending.py +++ b/backend/app/modules/modules_factory/pending.py @@ -11,6 +11,7 @@ SimulateAllocation, SimulateFinalizedSnapshots, UserBudgets, + WithdrawalsService, ) from app.modules.octant_rewards.service.pending import PendingOctantRewards from app.modules.snapshots.finalized.service.simulated import ( @@ -21,6 +22,7 @@ from app.modules.user.deposits.service.saved import SavedUserDeposits from app.modules.user.patron_mode.service.events_based import EventsBasedUserPatronMode from app.modules.user.rewards.service.calculated import CalculatedUserRewards +from app.modules.withdrawals.service.pending import PendingWithdrawals from app.pydantic import Model @@ -44,6 +46,7 @@ class PendingServices(Model): user_budgets_service: UserBudgets user_rewards_service: UserRewards finalized_snapshots_service: SimulateFinalizedSnapshots + withdrawals_service: WithdrawalsService @staticmethod def create() -> "PendingServices": @@ -61,6 +64,7 @@ def create() -> "PendingServices": user_rewards=user_rewards, patrons_mode=events_based_patron_mode, ) + withdrawals_service = PendingWithdrawals(user_rewards=user_rewards) return PendingServices( user_deposits_service=SavedUserDeposits(), @@ -70,4 +74,5 @@ def create() -> "PendingServices": finalized_snapshots_service=finalized_snapshots_service, user_budgets_service=saved_user_budgets, user_rewards_service=user_rewards, + withdrawals_service=withdrawals_service, ) diff --git a/backend/app/modules/modules_factory/protocols.py b/backend/app/modules/modules_factory/protocols.py index e7422c2ea7..2d613171ab 100644 --- a/backend/app/modules/modules_factory/protocols.py +++ b/backend/app/modules/modules_factory/protocols.py @@ -3,7 +3,12 @@ from app.context.manager import Context from app.engine.projects.rewards import ProjectRewardDTO from app.engine.user.effective_deposit import UserDeposit -from app.modules.dto import OctantRewardsDTO, AllocationDTO, FinalizedSnapshotDTO +from app.modules.dto import ( + OctantRewardsDTO, + AllocationDTO, + FinalizedSnapshotDTO, + WithdrawableEth, +) @runtime_checkable @@ -94,3 +99,11 @@ def simulate_finalized_epoch_snapshot( self, context: Context ) -> FinalizedSnapshotDTO: ... + + +@runtime_checkable +class WithdrawalsService(Protocol): + def get_withdrawable_eth( + self, context: Context, address: str + ) -> list[WithdrawableEth]: + ... diff --git a/backend/app/modules/snapshots/finalized/controller.py b/backend/app/modules/snapshots/finalized/controller.py index 33cc1fb7ae..3963fb04ca 100644 --- a/backend/app/modules/snapshots/finalized/controller.py +++ b/backend/app/modules/snapshots/finalized/controller.py @@ -3,7 +3,6 @@ from app.exceptions import InvalidEpoch from app.modules.dto import FinalizedSnapshotDTO from app.modules.modules_factory.finalizing import FinalizingServices -from app.modules.modules_factory.pending import PendingServices from app.modules.registry import get_services @@ -18,7 +17,7 @@ def create_finalized_epoch_snapshot() -> int | None: def simulate_finalized_epoch_snapshot() -> FinalizedSnapshotDTO | None: context = state_context(EpochState.PENDING) - services: PendingServices = get_services(EpochState.PENDING) + services = get_services(EpochState.PENDING) return services.finalized_snapshots_service.simulate_finalized_epoch_snapshot( context ) diff --git a/backend/app/modules/user/allocations/service/pending.py b/backend/app/modules/user/allocations/service/pending.py index f4bf2e1243..6089734753 100644 --- a/backend/app/modules/user/allocations/service/pending.py +++ b/backend/app/modules/user/allocations/service/pending.py @@ -3,9 +3,9 @@ from app.context.manager import Context from app.engine.projects.rewards import ProjectRewardDTO from app.infrastructure import database -from app.modules.dto import AllocationDTO, AccountFundsDTO +from app.modules.dto import AllocationDTO from app.modules.user.allocations import core -from app.pydantic import Model +from app.modules.user.allocations.service.saved import SavedUserAllocations @runtime_checkable @@ -14,21 +14,9 @@ def get_matched_rewards(self, context: Context) -> int: ... -class PendingUserAllocations(Model): +class PendingUserAllocations(SavedUserAllocations): octant_rewards: OctantRewards - def get_all_donors_addresses(self, context: Context) -> List[str]: - return database.allocations.get_users_with_allocations( - context.epoch_details.epoch_num - ) - - def get_all_users_with_allocations_sum( - self, context: Context - ) -> List[AccountFundsDTO]: - return database.allocations.get_alloc_sum_by_epoch_and_user_address( - context.epoch_details.epoch_num - ) - def simulate_allocation( self, context: Context, diff --git a/backend/app/modules/user/allocations/service/saved.py b/backend/app/modules/user/allocations/service/saved.py index 784977db23..8216cbbeed 100644 --- a/backend/app/modules/user/allocations/service/saved.py +++ b/backend/app/modules/user/allocations/service/saved.py @@ -15,6 +15,19 @@ def get_all_donors_addresses(self, context: Context) -> List[str]: def get_all_users_with_allocations_sum( self, context: Context ) -> List[AccountFundsDTO]: - return database.allocations.get_alloc_sum_by_epoch_and_user_address( + return database.allocations.get_users_alloc_sum_by_epoch( context.epoch_details.epoch_num ) + + def get_user_allocation_sum(self, context: Context, user_address: str) -> int: + return database.allocations.get_user_alloc_sum_by_epoch( + context.epoch_details.epoch_num, user_address + ) + + def has_user_allocated_rewards(self, context: Context, user_address: str) -> bool: + allocation_signature = ( + database.allocations.get_allocation_request_by_user_and_epoch( + user_address, context.epoch_details.epoch_num + ) + ) + return allocation_signature is not None diff --git a/backend/app/modules/user/rewards/service/calculated.py b/backend/app/modules/user/rewards/service/calculated.py index e0d537890c..e543067afe 100644 --- a/backend/app/modules/user/rewards/service/calculated.py +++ b/backend/app/modules/user/rewards/service/calculated.py @@ -16,12 +16,21 @@ def get_all_users_with_allocations_sum( ) -> List[AccountFundsDTO]: ... + def get_user_allocation_sum(self, context: Context, user_address: str) -> int: + ... + + def has_user_allocated_rewards(self, context: Context, user_address: str) -> bool: + ... + @runtime_checkable class UserBudgets(Protocol): def get_all_budgets(self, context: Context) -> Dict[str, int]: ... + def get_budget(self, context: Context, user_address: str) -> int: + ... + @runtime_checkable class UserPatronMode(Protocol): @@ -56,3 +65,12 @@ def get_claimed_rewards(self, context: Context) -> (List[AccountFundsDTO], int): rewards_sum += claimed_rewards return rewards, rewards_sum + + def get_user_claimed_rewards(self, context: Context, user_address: str) -> int: + if not self.allocations.has_user_allocated_rewards(context, user_address): + return 0 + + budget = self.user_budgets.get_budget(context, user_address) + allocation = self.allocations.get_user_allocation_sum(context, user_address) + + return budget - allocation diff --git a/backend/app/modules/withdrawals/controller.py b/backend/app/modules/withdrawals/controller.py new file mode 100644 index 0000000000..6e8e169123 --- /dev/null +++ b/backend/app/modules/withdrawals/controller.py @@ -0,0 +1,21 @@ +import dataclasses + +from app.context.epoch_state import EpochState +from app.context.manager import state_context, Context +from app.exceptions import InvalidEpoch +from app.modules.registry import get_services + + +def get_withdrawable_eth(address: str) -> list[dict]: + context = _get_context() + service = get_services(context.epoch_state).withdrawals_service + return [ + dataclasses.asdict(w) for w in service.get_withdrawable_eth(context, address) + ] + + +def _get_context() -> Context: + try: + return state_context(EpochState.PENDING) + except InvalidEpoch: + return state_context(EpochState.FINALIZED) diff --git a/backend/app/modules/withdrawals/core.py b/backend/app/modules/withdrawals/core.py new file mode 100644 index 0000000000..4c39373e21 --- /dev/null +++ b/backend/app/modules/withdrawals/core.py @@ -0,0 +1,61 @@ +from multiproof import StandardMerkleTree + +from app.context.manager import Context +from app.infrastructure.database.models import Reward +from app.modules.common.merkle_tree import get_proof +from app.modules.dto import WithdrawableEth, WithdrawalStatus + + +def get_withdrawals( + context: Context, + address: str, + pending_epoch_rewards: int, + finalized_epoch_rewards: list[Reward], + merkle_trees: dict[int, StandardMerkleTree], + merkle_roots_epochs: list[int], +) -> list[WithdrawableEth]: + withdrawable_eth = [] + + if pending_epoch_rewards: + pending_epoch_withdrawal = _create_pending_epoch_withdrawal( + context.epoch_details.epoch_num, pending_epoch_rewards + ) + withdrawable_eth.append(pending_epoch_withdrawal) + + finalized_epochs_withdrawals = create_finalized_epoch_withdrawals( + finalized_epoch_rewards, merkle_trees, merkle_roots_epochs, address + ) + withdrawable_eth.extend(finalized_epochs_withdrawals) + + return withdrawable_eth + + +def create_finalized_epoch_withdrawals( + rewards: list[Reward], + merkle_trees: dict[int, StandardMerkleTree], + merkle_roots_epochs: list[int], + address: str, +) -> list[WithdrawableEth]: + withdrawable_eth = [] + + for r in rewards: + merkle_tree = merkle_trees[r.epoch] + status = ( + WithdrawalStatus.AVAILABLE + if r.epoch in merkle_roots_epochs + else WithdrawalStatus.PENDING + ) + withdrawable_eth.append( + WithdrawableEth( + r.epoch, + int(r.amount), + get_proof(merkle_tree, address), + status, + ) + ) + + return withdrawable_eth + + +def _create_pending_epoch_withdrawal(epoch_num: int, user_rewards: int): + return WithdrawableEth(epoch_num, user_rewards, [], WithdrawalStatus.PENDING) diff --git a/backend/app/modules/withdrawals/service/finalized.py b/backend/app/modules/withdrawals/service/finalized.py new file mode 100644 index 0000000000..fd42e695d0 --- /dev/null +++ b/backend/app/modules/withdrawals/service/finalized.py @@ -0,0 +1,24 @@ +from app.context.manager import Context +from app.extensions import vault +from app.infrastructure import database +from app.infrastructure.graphql.merkle_roots import get_all_vault_merkle_roots +from app.modules.common.merkle_tree import get_rewards_merkle_tree_for_epoch +from app.modules.dto import WithdrawableEth +from app.modules.withdrawals.core import create_finalized_epoch_withdrawals +from app.pydantic import Model + + +class FinalizedWithdrawals(Model): + def get_withdrawable_eth(self, _: Context, address: str) -> list[WithdrawableEth]: + last_claimed_epoch = vault.get_last_claimed_epoch(address) + rewards = database.rewards.get_by_address_and_epoch_gt( + address, last_claimed_epoch + ) + merkle_trees = { + r.epoch: get_rewards_merkle_tree_for_epoch(r.epoch) for r in rewards + } + merkle_roots_epochs = [mr["epoch"] for mr in get_all_vault_merkle_roots()] + + return create_finalized_epoch_withdrawals( + rewards, merkle_trees, merkle_roots_epochs, address + ) diff --git a/backend/app/modules/withdrawals/service/pending.py b/backend/app/modules/withdrawals/service/pending.py new file mode 100644 index 0000000000..554a42e3b9 --- /dev/null +++ b/backend/app/modules/withdrawals/service/pending.py @@ -0,0 +1,44 @@ +from typing import runtime_checkable, Protocol + +from app.context.manager import Context +from app.extensions import vault +from app.infrastructure import database +from app.infrastructure.graphql.merkle_roots import get_all_vault_merkle_roots +from app.modules.common.merkle_tree import get_rewards_merkle_tree_for_epoch +from app.modules.dto import WithdrawableEth +from app.modules.withdrawals.core import ( + get_withdrawals, +) +from app.pydantic import Model + + +@runtime_checkable +class UserRewards(Protocol): + def get_user_claimed_rewards(self, context: Context, user_address: str) -> int: + ... + + +class PendingWithdrawals(Model): + user_rewards: UserRewards + + def get_withdrawable_eth( + self, context: Context, address: str + ) -> list[WithdrawableEth]: + last_claimed_epoch = vault.get_last_claimed_epoch(address) + rewards = database.rewards.get_by_address_and_epoch_gt( + address, last_claimed_epoch + ) + merkle_trees = { + r.epoch: get_rewards_merkle_tree_for_epoch(r.epoch) for r in rewards + } + merkle_roots_epochs = [mr["epoch"] for mr in get_all_vault_merkle_roots()] + claimed_rewards = self.user_rewards.get_user_claimed_rewards(context, address) + + return get_withdrawals( + context, + address, + claimed_rewards, + rewards, + merkle_trees, + merkle_roots_epochs, + ) diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 48d74607c0..bec5ff8b34 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -417,7 +417,6 @@ def patch_epochs(monkeypatch): monkeypatch.setattr("app.legacy.controllers.allocations.epochs", MOCK_EPOCHS) monkeypatch.setattr("app.legacy.controllers.snapshots.epochs", MOCK_EPOCHS) monkeypatch.setattr("app.legacy.controllers.rewards.epochs", MOCK_EPOCHS) - monkeypatch.setattr("app.legacy.controllers.withdrawals.epochs", MOCK_EPOCHS) monkeypatch.setattr("app.legacy.core.proposals.epochs", MOCK_EPOCHS) monkeypatch.setattr("app.context.epoch_state.epochs", MOCK_EPOCHS) monkeypatch.setattr("app.context.epoch_details.epochs", MOCK_EPOCHS) @@ -458,7 +457,8 @@ def patch_glm(monkeypatch): @pytest.fixture(scope="function") def patch_vault(monkeypatch): - monkeypatch.setattr("app.legacy.controllers.withdrawals.vault", MOCK_VAULT) + monkeypatch.setattr("app.modules.withdrawals.service.pending.vault", MOCK_VAULT) + monkeypatch.setattr("app.modules.withdrawals.service.finalized.vault", MOCK_VAULT) MOCK_VAULT.get_last_claimed_epoch.return_value = 0 @@ -515,7 +515,6 @@ def patch_last_finalized_snapshot(monkeypatch): @pytest.fixture(scope="function") def patch_user_budget(monkeypatch): monkeypatch.setattr("app.legacy.core.allocations.get_budget", MOCK_GET_USER_BUDGET) - monkeypatch.setattr("app.legacy.core.user.rewards.get_budget", MOCK_GET_USER_BUDGET) monkeypatch.setattr( "app.legacy.core.history.budget.get_budget", MOCK_GET_USER_BUDGET ) diff --git a/backend/tests/database/test_allocations_db.py b/backend/tests/database/test_allocations_db.py index 3e3169c062..693e57d790 100644 --- a/backend/tests/database/test_allocations_db.py +++ b/backend/tests/database/test_allocations_db.py @@ -5,9 +5,7 @@ def test_get_all_by_epoch_group_by_user_address( mock_allocations_db, user_accounts, proposal_accounts ): - result = database.allocations.get_alloc_sum_by_epoch_and_user_address( - MOCKED_PENDING_EPOCH_NO - ) + result = database.allocations.get_users_alloc_sum_by_epoch(MOCKED_PENDING_EPOCH_NO) assert len(result) == 2 assert result[0].address == user_accounts[1].address diff --git a/backend/tests/legacy/test_user.py b/backend/tests/legacy/test_user.py index e9bb64a6a1..da8482fc22 100644 --- a/backend/tests/legacy/test_user.py +++ b/backend/tests/legacy/test_user.py @@ -1,5 +1,4 @@ import pytest -from eth_account import Account from app import exceptions from app.extensions import db @@ -11,9 +10,7 @@ ) from app.legacy.core.allocations import add_allocations_to_db, Allocation from app.legacy.core.user.budget import get_budget -from app.legacy.core.user.rewards import get_all_claimed_rewards from tests.conftest import ( - allocate_user_rewards, MOCKED_PENDING_EPOCH_NO, USER1_BUDGET, MOCK_EPOCHS, @@ -49,81 +46,6 @@ def test_get_user_budget(user_accounts, mock_pending_epoch_snapshot_db): assert result == expected_result -@pytest.mark.parametrize( - # The structure of these parameters is as follows - # - # dict { int : List[(str, int)] } - # \ \ \______ allocation amount - # \ \___________ account index of one of the accounts generated - # \ by proposal_accounts() fixture - # \_____________________ account index of one of the accounts generated - # by user_accounts() fixture - # - # dict { int : int } - # \ \__________________ user claimed rewards - # \________________________ account index of one of the accounts generated - # by proposal_accounts() fixture - "user_allocations, expected_rewards", - [ - ( - { - 0: [ - (1, 300000_000000000), - (2, 100000_000000000), - (3, 100000_000000000), - ], - 1: [(1, 200000_000000000), (3, 400000_000000000)], - }, - { - 0: 1026868_989237987, - 1: 4998519_420519815, - }, - ), - # ------------------------------------ - ( - { - 0: [(1, 1526868_989237987)], - 1: [(2, 0)], - }, - { - 1: 5598519_420519815, - }, - ), - ], -) -def test_get_claimed_rewards( - user_accounts, - proposal_accounts, - mock_pending_epoch_snapshot_db, - user_allocations: dict, - expected_rewards: dict, -): - for user_index, allocations in user_allocations.items(): - user_account = user_accounts[user_index] - - for allocation in allocations: - proposal_account: Account = proposal_accounts[allocation[0]] - allocation_amount = allocation[1] - - nonce = allocations_controller.get_allocation_nonce(user_account.address) - allocate_user_rewards( - user_account, proposal_account, allocation_amount, nonce - ) - - expected = {} - - for user_index, expected_reward in expected_rewards.items(): - user_address = user_accounts[user_index].address - expected[user_address] = expected_reward - - user_rewards, rewards_sum = get_all_claimed_rewards(MOCKED_PENDING_EPOCH_NO) - assert len(user_rewards) == len(expected) - for user in user_rewards: - assert expected.get(user.address) == user.amount - - assert rewards_sum == sum(expected_rewards.values()) - - def test_get_patron_mode_status_return_false_when_user_does_not_exist(user_accounts): result = get_patron_mode_status(user_accounts[0].address) assert result is False diff --git a/backend/tests/legacy/test_withdrawals.py b/backend/tests/legacy/test_withdrawals.py deleted file mode 100644 index 102733da96..0000000000 --- a/backend/tests/legacy/test_withdrawals.py +++ /dev/null @@ -1,388 +0,0 @@ -import pytest - -from app import db -from app.infrastructure import database -from app.legacy.controllers.withdrawals import get_withdrawable_eth -from app.legacy.core.allocations import Allocation -from tests.conftest import ( - MOCK_VAULT, - mock_graphql, - MOCK_EPOCHS, - MOCKED_PENDING_EPOCH_NO, - MOCK_GET_USER_BUDGET, -) - - -@pytest.fixture(autouse=True) -def before(mocker, app, graphql_client, patch_vault, patch_epochs): - merkle_roots = [{"epoch": 1}, {"epoch": 2}, {"epoch": 3}, {"epoch": 4}] - mock_graphql(mocker, merkle_roots_events=merkle_roots) - - -@pytest.mark.parametrize( - # The structure of these parameters is as follows - # - # dict { int : List[(int, int, List(str))] } - # \ \ \ \___________ list of merkle proofs - # \ \ \_________________ reward amount - # \ \____________________ epoch number - # \ - # \______________________ account index of one of the accounts generated - # by user_accounts() fixture - "expected_rewards", - [ - # ------------------------------------ - ( - { - 0: [ - ( - 1, - 20000_000000000, - [ - "0xf3da88ce132940aced09d7220c38d231e0adf28b6f7cbc1d358efc0e9fc41ebd" - ], - ), - ( - 2, - 40000_000000000, - [ - "0x625199059363058a4a29bed6b0a8f82d3b7e4698c41d565f65c1c93605fb70a8" - ], - ), - ], - 1: [ - ( - 1, - 30000_000000000, - [ - "0xb48f1d90532a935cc6ab465a845fd13f22ee7fb58c1fe279e2c36038aac85ccd" - ], - ), - ( - 2, - 60000_000000000, - [ - "0xc66896c2196622197df63c63c7f626b597bc305d35d1fd78813d2dcdc7313f6e" - ], - ), - ], - } - ), - # ------------------------------------ - ( - { - 0: [ - ( - 1, - 10000_000000000, - [ - "0x6ec43b72d15d00ab268162d5e6af15f693c6d0caf93dfe664381753d463ad85a", - "0xf3da88ce132940aced09d7220c38d231e0adf28b6f7cbc1d358efc0e9fc41ebd", - ], - ), - ( - 2, - 40000_000000000, - [ - "0xd6c95435ed26baf36bd1c336338f749fee7e30094ea5ee7f95eee829214ae530", - "0x1f9f498784df87b93df88a813f7a02b3b73fcc621a0d1602199601146aafb8c2", - ], - ), - ], - 1: [ - ( - 1, - 30000_000000000, - [ - "0xbf8320ade05faf3ac755c43d87cc6b904767df9b265924a865801165117fb0b3" - ], - ), - ( - 2, - 10000_000000000, - [ - "0xc66896c2196622197df63c63c7f626b597bc305d35d1fd78813d2dcdc7313f6e", - "0x1f9f498784df87b93df88a813f7a02b3b73fcc621a0d1602199601146aafb8c2", - ], - ), - ( - 4, - 60000_000000000, - [ - "0x582a3b7c9ce265773e6a5fc188237fbad7d5bbfa076837f1b7f4308f864990f5", - "0x6f18eaeeb0361c5530c96c19850a9510c987f252ef7b6fab32b4a9c5463f8ce8", - ], - ), - ], - 2: [ - ( - 2, - 50000_000000000, - [ - "0x897f52d0675b4534d1dbbbb0c912dbb9a2c7677f224e1b41541a71b8c7b6ccb7", - "0x4ceb53828784b7a05e0ade5e6d77479163a5cca71f7fc3781c745cc21ba992c3", - ], - ), - ( - 3, - 50000_000000000, - [ - "0x40a412cca910411747e19bbb2fd15d39cd2bd04043be2fcd7cd6b88e05dad368" - ], - ), - ( - 4, - 50000_000000000, - [ - "0x1903e1f9d86fbf665a27334bc93e6eb7e55f41157fc4b32ad5c052da06067242" - ], - ), - ], - 3: [ - ( - 1, - 10000_000000000, - [ - "0x4ec277b40ceae243547922781f3a2424d555632e39018b10119283caa91d9daf", - "0xf3da88ce132940aced09d7220c38d231e0adf28b6f7cbc1d358efc0e9fc41ebd", - ], - ), - ( - 2, - 20000_000000000, - [ - "0x6f18eaeeb0361c5530c96c19850a9510c987f252ef7b6fab32b4a9c5463f8ce8", - "0x4ceb53828784b7a05e0ade5e6d77479163a5cca71f7fc3781c745cc21ba992c3", - ], - ), - ( - 3, - 50000_000000000, - [ - "0x6f18eaeeb0361c5530c96c19850a9510c987f252ef7b6fab32b4a9c5463f8ce8" - ], - ), - ( - 4, - 80000_000000000, - [ - "0x625199059363058a4a29bed6b0a8f82d3b7e4698c41d565f65c1c93605fb70a8", - "0x6f18eaeeb0361c5530c96c19850a9510c987f252ef7b6fab32b4a9c5463f8ce8", - ], - ), - ], - } - ), - ], -) -def test_get_withdrawable_eth(user_accounts, expected_rewards): - MOCK_EPOCHS.get_pending_epoch.return_value = None - - # Populate db - for user_index, rewards in expected_rewards.items(): - user_account = user_accounts[user_index].address - - for epoch, amount, _ in rewards: - database.rewards.add_user_reward(epoch, user_account, amount) - db.session.commit() - - # Asserts - for user_index, rewards in expected_rewards.items(): - user_account = user_accounts[user_index].address - result = get_withdrawable_eth(user_account) - - assert len(result) == len(rewards) - for act, exp in zip(result, rewards): - assert act.epoch == exp[0] - assert act.amount == exp[1] - assert act.proof == exp[2] - - -def test_get_withdrawable_eth_returns_only_user_not_claimed_rewards(user_accounts): - MOCK_EPOCHS.get_pending_epoch.return_value = None - MOCK_VAULT.get_last_claimed_epoch.return_value = 2 - - database.rewards.add_user_reward(1, user_accounts[0].address, 100_000000000) - database.rewards.add_user_reward(2, user_accounts[0].address, 200_000000000) - database.rewards.add_user_reward(3, user_accounts[0].address, 300_000000000) - database.rewards.add_user_reward(4, user_accounts[0].address, 400_000000000) - db.session.commit() - - result = get_withdrawable_eth(user_accounts[0].address) - - assert len(result) == 2 - assert result[0].epoch == 3 - assert result[0].amount == 300_000000000 - assert result[1].epoch == 4 - assert result[1].amount == 400_000000000 - - -def test_get_withdrawable_eth_returns_only_proposal_not_claimed_rewards( - proposal_accounts, -): - MOCK_EPOCHS.get_pending_epoch.return_value = None - MOCK_VAULT.get_last_claimed_epoch.return_value = 2 - - database.rewards.add_proposal_reward( - 1, proposal_accounts[0].address, 100_000000000, 50_000000000 - ) - database.rewards.add_proposal_reward( - 2, proposal_accounts[0].address, 200_000000000, 50_000000000 - ) - database.rewards.add_proposal_reward( - 3, proposal_accounts[0].address, 300_000000000, 50_000000000 - ) - database.rewards.add_proposal_reward( - 4, proposal_accounts[0].address, 400_000000000, 50_000000000 - ) - db.session.commit() - - result = get_withdrawable_eth(proposal_accounts[0].address) - - assert len(result) == 2 - assert result[0].epoch == 3 - assert result[0].amount == 300_000000000 - assert result[1].epoch == 4 - assert result[1].amount == 400_000000000 - - -def test_get_withdrawable_eth_result_sorted_by_epochs(user_accounts): - MOCK_EPOCHS.get_pending_epoch.return_value = None - - database.rewards.add_user_reward(2, user_accounts[0].address, 200_000000000) - database.rewards.add_user_reward(4, user_accounts[0].address, 400_000000000) - database.rewards.add_user_reward(1, user_accounts[0].address, 100_000000000) - database.rewards.add_user_reward(3, user_accounts[0].address, 300_000000000) - db.session.commit() - - result = get_withdrawable_eth(user_accounts[0].address) - - assert result[0].epoch == 1 - assert result[1].epoch == 2 - assert result[2].epoch == 3 - assert result[3].epoch == 4 - - -def test_get_withdrawable_eth_in_pending_epoch_with_allocation( - user_accounts, proposal_accounts, patch_user_budget -): - MOCK_GET_USER_BUDGET.return_value = 1 * 10**18 - user = database.user.get_or_add_user(user_accounts[0].address) - db.session.commit() - - user_allocations = [Allocation(proposal_accounts[0].address, 10_000000000)] - database.allocations.add_all(MOCKED_PENDING_EPOCH_NO, user.id, 0, user_allocations) - database.allocations.add_allocation_request( - user_accounts[0].address, MOCKED_PENDING_EPOCH_NO, 0, "0x00" - ) - db.session.commit() - - result = get_withdrawable_eth(user_accounts[0].address) - - assert len(result) == 1 - assert result[0].epoch == 1 - assert result[0].amount == 999999990_000000000 - assert result[0].proof == [] - assert result[0].status == "pending" - - -def test_get_withdrawable_eth_in_pending_epoch_without_allocation( - user_accounts, proposal_accounts -): - database.user.add_user(user_accounts[0].address) - db.session.commit() - result = get_withdrawable_eth(user_accounts[0].address) - - assert len(result) == 1 - assert result[0].epoch == 1 - assert result[0].amount == 0 - assert result[0].proof == [] - assert result[0].status == "pending" - - -def test_get_withdrawable_eth_in_pending_epoch_in_epoch_3( - user_accounts, proposal_accounts, patch_user_budget -): - MOCK_EPOCHS.get_pending_epoch.return_value = 2 - MOCK_GET_USER_BUDGET.return_value = 1 * 10**18 - - user = database.user.add_user(user_accounts[0].address) - database.rewards.add_user_reward(1, user_accounts[0].address, 100_000000000) - database.rewards.add_user_reward(1, user_accounts[1].address, 200_000000000) - db.session.commit() - - user_allocations = [Allocation(proposal_accounts[0].address, 10_000000000)] - database.allocations.add_all( - MOCKED_PENDING_EPOCH_NO + 1, user.id, 0, user_allocations - ) - database.allocations.add_allocation_request( - user_accounts[0].address, MOCKED_PENDING_EPOCH_NO + 1, 0, "0x00" - ) - - db.session.commit() - - result = get_withdrawable_eth(user_accounts[0].address) - - assert len(result) == 2 - assert result[0].epoch == 2 - assert result[0].amount == 999999990000000000 - assert result[0].proof == [] - assert result[0].status == "pending" - - assert result[1].epoch == 1 - assert result[1].amount == 100000000000 - assert result[1].proof == [ - "0xeba8e145c1102400c42b1fc5de1fea439aeaa93a6b46ef370ecbc8f15140a2dd" - ] - assert result[1].status == "available" - - -def test_get_withdrawable_eth_in_finalized_epoch_when_merkle_root_not_set_yet_epoch_1( - mocker, user_accounts, proposal_accounts, patch_user_budget -): - MOCK_EPOCHS.get_pending_epoch.return_value = None - mock_graphql(mocker, merkle_roots_events=[]) - - database.rewards.add_user_reward(1, user_accounts[0].address, 100_000000000) - database.rewards.add_user_reward(1, user_accounts[1].address, 200_000000000) - db.session.commit() - - result = get_withdrawable_eth(user_accounts[0].address) - - assert len(result) == 1 - assert result[0].epoch == 1 - assert result[0].amount == 100000000000 - assert result[0].proof == [ - "0xeba8e145c1102400c42b1fc5de1fea439aeaa93a6b46ef370ecbc8f15140a2dd" - ] - assert result[0].status == "pending" - - -def test_get_withdrawable_eth_in_finalized_epoch_when_merkle_root_not_set_yet_epoch_2( - mocker, user_accounts, proposal_accounts, patch_user_budget -): - MOCK_EPOCHS.get_pending_epoch.return_value = None - mock_graphql(mocker, merkle_roots_events=[{"epoch": 1}]) - - database.rewards.add_user_reward(1, user_accounts[0].address, 100_000000000) - database.rewards.add_user_reward(1, user_accounts[1].address, 200_000000000) - database.rewards.add_user_reward(2, user_accounts[0].address, 100_000000000) - database.rewards.add_user_reward(2, user_accounts[1].address, 200_000000000) - db.session.commit() - - result = get_withdrawable_eth(user_accounts[0].address) - - assert len(result) == 2 - assert result[0].epoch == 1 - assert result[0].amount == 100000000000 - assert result[0].proof == [ - "0xeba8e145c1102400c42b1fc5de1fea439aeaa93a6b46ef370ecbc8f15140a2dd" - ] - assert result[0].status == "available" - - assert result[1].epoch == 2 - assert result[1].amount == 100000000000 - assert result[1].proof == [ - "0xeba8e145c1102400c42b1fc5de1fea439aeaa93a6b46ef370ecbc8f15140a2dd" - ] - assert result[1].status == "pending" diff --git a/backend/tests/modules/modules_factory/test_modules_factory.py b/backend/tests/modules/modules_factory/test_modules_factory.py index 0eba5d3366..7aa3092d91 100644 --- a/backend/tests/modules/modules_factory/test_modules_factory.py +++ b/backend/tests/modules/modules_factory/test_modules_factory.py @@ -31,6 +31,8 @@ from app.modules.user.patron_mode.service.events_based import EventsBasedUserPatronMode from app.modules.user.rewards.service.calculated import CalculatedUserRewards from app.modules.user.rewards.service.saved import SavedUserRewards +from app.modules.withdrawals.service.finalized import FinalizedWithdrawals +from app.modules.withdrawals.service.pending import PendingWithdrawals def test_future_services_factory(): @@ -99,6 +101,7 @@ def test_pending_services_factory(): user_rewards=user_rewards, patrons_mode=events_based_patron_mode, ) + withdrawals_service = PendingWithdrawals(user_rewards=user_rewards) assert result.user_deposits_service == SavedUserDeposits() assert result.octant_rewards_service == octant_rewards @@ -106,6 +109,7 @@ def test_pending_services_factory(): assert result.user_patron_mode_service == events_based_patron_mode assert result.user_rewards_service == user_rewards assert result.finalized_snapshots_service == finalized_snapshots_service + assert result.withdrawals_service == withdrawals_service def test_finalizing_services_factory(): @@ -124,6 +128,7 @@ def test_finalizing_services_factory(): user_rewards=user_rewards, patrons_mode=events_based_patron_mode, ) + withdrawals_service = PendingWithdrawals(user_rewards=user_rewards) assert result.user_deposits_service == SavedUserDeposits() assert result.octant_rewards_service == octant_rewards @@ -131,6 +136,7 @@ def test_finalizing_services_factory(): assert result.user_patron_mode_service == events_based_patron_mode assert result.user_rewards_service == user_rewards assert result.finalized_snapshots_service == finalized_snapshots_service + assert result.withdrawals_service == withdrawals_service def test_finalized_services_factory(): @@ -138,12 +144,16 @@ def test_finalized_services_factory(): events_based_patron_mode = EventsBasedUserPatronMode() saved_user_allocations = SavedUserAllocations() - assert result.user_deposits_service == SavedUserDeposits() - assert result.octant_rewards_service == FinalizedOctantRewards() - assert result.user_allocations_service == saved_user_allocations - assert result.user_patron_mode_service == events_based_patron_mode - assert result.user_rewards_service == SavedUserRewards( + user_rewards = SavedUserRewards( user_budgets=SavedUserBudgets(), patrons_mode=events_based_patron_mode, allocations=saved_user_allocations, ) + withdrawals_service = FinalizedWithdrawals(user_rewards=user_rewards) + + assert result.user_deposits_service == SavedUserDeposits() + assert result.octant_rewards_service == FinalizedOctantRewards() + assert result.user_allocations_service == saved_user_allocations + assert result.user_patron_mode_service == events_based_patron_mode + assert result.user_rewards_service == user_rewards + assert result.withdrawals_service == withdrawals_service diff --git a/backend/tests/modules/user/allocations/test_pending_allocations.py b/backend/tests/modules/user/allocations/test_pending_allocations.py index e55d6da4de..4bc7d8e30c 100644 --- a/backend/tests/modules/user/allocations/test_pending_allocations.py +++ b/backend/tests/modules/user/allocations/test_pending_allocations.py @@ -1,9 +1,6 @@ -from unittest.mock import Mock - import pytest from app.engine.projects.rewards import ProjectRewardDTO -from app.extensions import db from app.infrastructure import database from app.modules.dto import AllocationDTO from app.modules.user.allocations.service.pending import PendingUserAllocations @@ -16,51 +13,6 @@ def before(app): pass -def test_get_all_donors_addresses(mock_users_db, proposal_accounts): - user1, user2, user3 = mock_users_db - - allocation = [ - AllocationDTO(proposal_accounts[0].address, 100), - ] - - database.allocations.add_all(1, user1.id, 0, allocation) - database.allocations.add_all(1, user2.id, 0, allocation) - database.allocations.add_all(2, user3.id, 0, allocation) - db.session.commit() - - context_epoch_1 = get_context(1) - context_epoch_2 = get_context(2) - - service = PendingUserAllocations(octant_rewards=Mock()) - - result_epoch_1 = service.get_all_donors_addresses(context_epoch_1) - result_epoch_2 = service.get_all_donors_addresses(context_epoch_2) - - assert result_epoch_1 == [user1.address, user2.address] - assert result_epoch_2 == [user3.address] - - -def test_return_only_not_removed_allocations(mock_users_db, proposal_accounts): - user1, user2, _ = mock_users_db - - allocation = [ - AllocationDTO(proposal_accounts[0].address, 100), - ] - - database.allocations.add_all(1, user1.id, 0, allocation) - database.allocations.add_all(1, user2.id, 0, allocation) - database.allocations.soft_delete_all_by_epoch_and_user_id(1, user2.id) - db.session.commit() - - context = get_context(1) - - service = PendingUserAllocations(octant_rewards=Mock()) - - result = service.get_all_donors_addresses(context) - - assert result == [user1.address] - - def test_simulate_allocation(mock_users_db, mock_octant_rewards): user1, _, _ = mock_users_db context = get_context() diff --git a/backend/tests/modules/user/allocations/test_saved_allocations.py b/backend/tests/modules/user/allocations/test_saved_allocations.py index aee8d5fa70..1cb4f2cc9c 100644 --- a/backend/tests/modules/user/allocations/test_saved_allocations.py +++ b/backend/tests/modules/user/allocations/test_saved_allocations.py @@ -55,3 +55,44 @@ def test_return_only_not_removed_allocations(mock_users_db, proposal_accounts): result = service.get_all_donors_addresses(context) assert result == [user1.address] + + +def test_get_user_allocation_sum(context, mock_users_db, proposal_accounts): + user1, user2, _ = mock_users_db + allocation = [ + AllocationDTO(proposal_accounts[0].address, 100), + AllocationDTO(proposal_accounts[1].address, 200), + ] + database.allocations.add_all(1, user1.id, 0, allocation) + database.allocations.add_all(1, user2.id, 0, allocation) + db.session.commit() + + service = SavedUserAllocations() + + result = service.get_user_allocation_sum(context, user1.address) + + assert result == 300 + + +def test_has_user_allocated_rewards(context, mock_users_db, proposal_accounts): + user1, _, _ = mock_users_db + database.allocations.add_allocation_request(user1.address, 1, 0, "0x00", False) + + db.session.commit() + + service = SavedUserAllocations() + + result = service.has_user_allocated_rewards(context, user1.address) + + assert result is True + + +def test_has_user_allocated_rewards_returns_false( + context, mock_users_db, proposal_accounts +): + user1, _, _ = mock_users_db + service = SavedUserAllocations() + + result = service.has_user_allocated_rewards(context, user1.address) + + assert result is False diff --git a/backend/tests/modules/user/rewards/test_calculated_rewards.py b/backend/tests/modules/user/rewards/test_calculated_rewards.py index ff0e3bf69d..7f83c6ce87 100644 --- a/backend/tests/modules/user/rewards/test_calculated_rewards.py +++ b/backend/tests/modules/user/rewards/test_calculated_rewards.py @@ -71,3 +71,49 @@ def test_get_claimed_rewards_when_all_budget_is_allocated( assert claimed_rewards == [] assert claimed_rewards_sum == 0 + + +def test_get_user_claimed_rewards( + context, + alice, + mock_users_db, + mock_user_budgets, + mock_user_allocations, + mock_patron_mode, +): + mock_user_budgets.get_budget.return_value = USER1_BUDGET + mock_user_allocations.get_user_allocation_sum.return_value = 100_000000000 + mock_user_allocations.has_user_allocated_rewards.return_value = True + + service = CalculatedUserRewards( + user_budgets=mock_user_budgets, + allocations=mock_user_allocations, + patrons_mode=mock_patron_mode, + ) + + result = service.get_user_claimed_rewards(context, alice.address) + + assert result == USER1_BUDGET - 100_000000000 + + +def test_get_user_claimed_rewards_returns_0_when_user_does_not_allocate( + context, + alice, + mock_users_db, + mock_user_budgets, + mock_user_allocations, + mock_patron_mode, +): + mock_user_budgets.get_budget.return_value = USER1_BUDGET + mock_user_allocations.get_user_allocation_sum.return_value = 100_000000000 + mock_user_allocations.has_user_allocated_rewards.return_value = False + + service = CalculatedUserRewards( + user_budgets=mock_user_budgets, + allocations=mock_user_allocations, + patrons_mode=mock_patron_mode, + ) + + result = service.get_user_claimed_rewards(context, alice.address) + + assert result == 0 diff --git a/backend/tests/modules/withdrawals/test_withdrawals_core.py b/backend/tests/modules/withdrawals/test_withdrawals_core.py new file mode 100644 index 0000000000..c482bf980e --- /dev/null +++ b/backend/tests/modules/withdrawals/test_withdrawals_core.py @@ -0,0 +1,114 @@ +import pytest + +from app.infrastructure.database.models import Reward +from app.modules.common.merkle_tree import build_merkle_tree +from app.modules.dto import AccountFundsDTO, WithdrawalStatus +from app.modules.withdrawals.core import get_withdrawals +from tests.helpers.constants import USER1_ADDRESS, USER2_ADDRESS +from tests.helpers.context import get_context + + +@pytest.fixture(scope="function") +def merkle_trees(): + return { + 1: build_merkle_tree( + [ + AccountFundsDTO(USER1_ADDRESS, 100_000000000), + AccountFundsDTO(USER2_ADDRESS, 200_000000000), + ], + ), + 2: build_merkle_tree( + [ + AccountFundsDTO(USER1_ADDRESS, 300_000000000), + AccountFundsDTO(USER2_ADDRESS, 400_000000000), + ], + ), + } + + +def test_get_finalized_withdrawable_eth(merkle_trees): + rewards = [ + Reward(epoch=1, address=USER1_ADDRESS, amount=100_000000000), + Reward(epoch=2, address=USER1_ADDRESS, amount=300_000000000), + ] + epochs_with_merkle_roots_set = [1, 2] + + result = get_withdrawals( + None, USER1_ADDRESS, 0, rewards, merkle_trees, epochs_with_merkle_roots_set + ) + + assert len(result) == 2 + assert result[0].epoch == 1 + assert result[0].amount == 100_000000000 + assert result[0].status == WithdrawalStatus.AVAILABLE + assert result[0].proof == [ + "0xeba8e145c1102400c42b1fc5de1fea439aeaa93a6b46ef370ecbc8f15140a2dd" + ] + assert result[1].epoch == 2 + assert result[1].amount == 300_000000000 + assert result[1].status == WithdrawalStatus.AVAILABLE + assert result[1].proof == [ + "0x339903fb80300f9cbf79e8bf8b9bb692c2896cf5607351cbdc3190c769b7f2bc" + ] + + +def test_get_finalized_withdrawable_eth_epoch_2_merkle_root_not_set(merkle_trees): + rewards = [ + Reward(epoch=1, address=USER1_ADDRESS, amount=100_000000000), + Reward(epoch=2, address=USER1_ADDRESS, amount=300_000000000), + ] + epochs_with_merkle_roots_set = [1] + + result = get_withdrawals( + None, USER1_ADDRESS, 0, rewards, merkle_trees, epochs_with_merkle_roots_set + ) + + assert len(result) == 2 + assert result[0].epoch == 1 + assert result[0].amount == 100_000000000 + assert result[0].status == WithdrawalStatus.AVAILABLE + assert result[0].proof == [ + "0xeba8e145c1102400c42b1fc5de1fea439aeaa93a6b46ef370ecbc8f15140a2dd" + ] + assert result[1].epoch == 2 + assert result[1].amount == 300_000000000 + assert result[1].status == WithdrawalStatus.PENDING + assert result[1].proof == [ + "0x339903fb80300f9cbf79e8bf8b9bb692c2896cf5607351cbdc3190c769b7f2bc" + ] + + +def test_get_pending_withdrawable_eth(merkle_trees): + context = get_context(3) + rewards = [ + Reward(epoch=1, address=USER1_ADDRESS, amount=100_000000000), + Reward(epoch=2, address=USER1_ADDRESS, amount=300_000000000), + ] + epochs_with_merkle_roots_set = [1, 2] + + result = get_withdrawals( + context, + USER1_ADDRESS, + 500_000000000, + rewards, + merkle_trees, + epochs_with_merkle_roots_set, + ) + + assert len(result) == 3 + assert result[0].epoch == 3 + assert result[0].amount == 500_000000000 + assert result[0].status == WithdrawalStatus.PENDING + assert result[0].proof == [] + assert result[1].epoch == 1 + assert result[1].amount == 100_000000000 + assert result[1].status == WithdrawalStatus.AVAILABLE + assert result[1].proof == [ + "0xeba8e145c1102400c42b1fc5de1fea439aeaa93a6b46ef370ecbc8f15140a2dd" + ] + assert result[2].epoch == 2 + assert result[2].amount == 300_000000000 + assert result[2].status == WithdrawalStatus.AVAILABLE + assert result[2].proof == [ + "0x339903fb80300f9cbf79e8bf8b9bb692c2896cf5607351cbdc3190c769b7f2bc" + ] diff --git a/backend/tests/modules/withdrawals/test_withdrawals_finalized.py b/backend/tests/modules/withdrawals/test_withdrawals_finalized.py new file mode 100644 index 0000000000..c2dbbb5dec --- /dev/null +++ b/backend/tests/modules/withdrawals/test_withdrawals_finalized.py @@ -0,0 +1,44 @@ +import pytest + +from app import db +from app.infrastructure import database +from app.modules.dto import WithdrawableEth, WithdrawalStatus +from app.modules.withdrawals.service.finalized import FinalizedWithdrawals +from tests.conftest import mock_graphql + + +@pytest.fixture(autouse=True) +def before(mocker, app, graphql_client, patch_vault): + merkle_roots = [{"epoch": 1}, {"epoch": 2}] + mock_graphql(mocker, merkle_roots_events=merkle_roots) + + +def test_get_withdrawable_eth(context, alice, bob): + database.rewards.add_user_reward(1, alice.address, 100_000000000) + database.rewards.add_user_reward(1, bob.address, 200_000000000) + database.rewards.add_user_reward(2, alice.address, 300_000000000) + database.rewards.add_user_reward(2, bob.address, 400_000000000) + db.session.commit() + + service = FinalizedWithdrawals() + + result = service.get_withdrawable_eth(context, alice.address) + + assert result == [ + WithdrawableEth( + epoch=1, + amount=100000000000, + proof=[ + "0xeba8e145c1102400c42b1fc5de1fea439aeaa93a6b46ef370ecbc8f15140a2dd" + ], + status=WithdrawalStatus.AVAILABLE, + ), + WithdrawableEth( + epoch=2, + amount=300000000000, + proof=[ + "0x339903fb80300f9cbf79e8bf8b9bb692c2896cf5607351cbdc3190c769b7f2bc" + ], + status=WithdrawalStatus.AVAILABLE, + ), + ] diff --git a/backend/tests/modules/withdrawals/test_withdrawals_pending.py b/backend/tests/modules/withdrawals/test_withdrawals_pending.py new file mode 100644 index 0000000000..6bde3e3f83 --- /dev/null +++ b/backend/tests/modules/withdrawals/test_withdrawals_pending.py @@ -0,0 +1,40 @@ +import pytest + +from app import db +from app.infrastructure import database +from app.modules.dto import WithdrawableEth, WithdrawalStatus +from app.modules.withdrawals.service.pending import PendingWithdrawals +from tests.conftest import mock_graphql +from tests.helpers.context import get_context + + +@pytest.fixture(autouse=True) +def before(mocker, app, graphql_client, patch_vault): + merkle_roots = [{"epoch": 1}] + mock_graphql(mocker, merkle_roots_events=merkle_roots) + + +def test_get_withdrawable_eth(alice, bob, mock_user_rewards): + context = get_context(2) + mock_user_rewards.get_user_claimed_rewards.return_value = 50_000000000 + database.rewards.add_user_reward(1, alice.address, 100_000000000) + database.rewards.add_user_reward(1, bob.address, 200_000000000) + db.session.commit() + + service = PendingWithdrawals(user_rewards=mock_user_rewards) + + result = service.get_withdrawable_eth(context, alice.address) + + assert result == [ + WithdrawableEth( + epoch=2, amount=50000000000, proof=[], status=WithdrawalStatus.PENDING + ), + WithdrawableEth( + epoch=1, + amount=100000000000, + proof=[ + "0xeba8e145c1102400c42b1fc5de1fea439aeaa93a6b46ef370ecbc8f15140a2dd" + ], + status=WithdrawalStatus.AVAILABLE, + ), + ]