Skip to content

Commit

Permalink
Merge branch 'develop' into feature/oct-1857-octant-client-v1.5
Browse files Browse the repository at this point in the history
  • Loading branch information
jmikolajczyk committed Sep 30, 2024
2 parents b765886 + bb0acd2 commit a6192bd
Show file tree
Hide file tree
Showing 27 changed files with 739 additions and 1,039 deletions.
945 changes: 472 additions & 473 deletions backend/app/constants.py

Large diffs are not rendered by default.

29 changes: 28 additions & 1 deletion backend/app/infrastructure/routes/epochs.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@

from app.extensions import api, epochs
from app.infrastructure import OctantResource, graphql
from app.modules.octant_rewards.controller import get_octant_rewards
from app.modules.octant_rewards.controller import (
get_octant_rewards,
get_epoch_rewards_rate,
)

ns = Namespace("epochs", description="Octant epochs")
api.add_namespace(ns)
Expand Down Expand Up @@ -113,6 +116,15 @@ def get(self):
},
)

epoch_rewards_rate_model = api.model(
"EpochRewardsRate",
{
"rewardsRate": fields.Float(
required=True, description="Rewards rate for the given epoch."
)
},
)


@ns.route("/info/<int:epoch>")
@ns.doc(
Expand All @@ -131,3 +143,18 @@ def get(self, epoch: int):
app.logger.debug(f"Got: {stats}")

return stats.to_dict()


@ns.route("/rewards-rate/<int:epoch>")
@ns.doc(
description="Returns a rewards rate for given epoch. Returns data for all states of epochs.",
params={
"epoch": "Epoch number",
},
)
class EpochRewardsRate(OctantResource):
@ns.marshal_with(epoch_rewards_rate_model)
@ns.response(200, "Epoch's rewards rate successfully retrieved.")
def get(self, epoch: int):
app.logger.debug(f"Getting rewards rate for epoch {epoch}")
return {"rewardsRate": get_epoch_rewards_rate(epoch)}
25 changes: 16 additions & 9 deletions backend/app/infrastructure/routes/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@
"address": fields.String(
required=True, description="Project address"
),
"epoch": fields.String(
required=True, description="Project epoch"
),
},
)
),
Expand Down Expand Up @@ -69,33 +72,37 @@ def get(self, epoch):
}


@ns.route("/details/epoch/<int:epoch>")
@ns.route("/details")
@ns.doc(
description="Returns projects details for a given epoch and searchPhrase.",
params={
"epoch": "Epoch number",
"searchPhrase": "Search phrase (query parameter)",
"epochs": "Epochs numbers (query parameter)",
"searchPhrases": "Search phrase (query parameter)",
},
)
class ProjectsDetails(OctantResource):
@ns.marshal_with(projects_details_model)
@ns.response(200, "Projects metadata is successfully retrieved")
def get(self, epoch: int):
search_phrase = request.args.get("searchPhrase", "")
def get(self):
search_phrases = request.args.get("searchPhrases", "").split(",")
epochs = list(map(int, request.args.get("epochs", "").split(",")))

app.logger.debug(
f"Getting projects details for epoch {epoch} and search phrase {search_phrase}"
f"Getting projects details for epochs {epochs} and search phrase {search_phrases}"
)
projects_details = projects_details_controller.get_projects_details(
epoch, search_phrase
projects_details = (
projects_details_controller.get_projects_details_for_multiple_params(
epochs, search_phrases
)
)
app.logger.debug(f"Projects details for epoch: {epoch}: {projects_details}")
app.logger.debug(f"Projects details for epochs {epochs}: {projects_details}")

return {
"projects_details": [
{
"name": project["name"],
"address": project["address"],
"epoch": project["epoch"],
}
for project in projects_details
]
Expand Down
24 changes: 19 additions & 5 deletions backend/app/infrastructure/routes/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,17 @@
"UserAntisybilStatus",
{
"status": fields.String(required=True, description="Unknown or Known"),
"expires_at": fields.String(
"expiresAt": fields.String(
required=False, description="Expiry date, unix timestamp"
),
"score": fields.String(
required=False,
description="Score, parses as a float",
),
"isOnTimeOutList": fields.Boolean(
required=False,
description="Flag indicating whether user is on timeout list",
),
},
)

Expand Down Expand Up @@ -192,15 +196,25 @@ class AntisybilStatus(OctantResource):
@ns.response(200, "User's cached antisybil status retrieved")
def get(self, user_address: str):
app.logger.debug(f"Getting user {user_address} cached antisybil status")

antisybil_status = get_user_antisybil_status(user_address)

app.logger.debug(f"User {user_address} antisybil status: {antisybil_status}")

if antisybil_status is None:
return {"status": "Unknown"}, 404
score, expires_at = antisybil_status

score, expires_at, is_on_timeout_list = (
antisybil_status.score,
antisybil_status.expires_at,
antisybil_status.is_on_timeout_list,
)

return {
"status": "Known",
"score": score,
"expires_at": int(expires_at.timestamp()),
"expiresAt": int(expires_at.timestamp()),
"isOnTimeOutList": is_on_timeout_list,
}, 200

@ns.doc(
Expand All @@ -211,9 +225,9 @@ def get(self, user_address: str):
@ns.response(504, "Could not refresh antisybil status. Upstream is unavailable.")
def put(self, user_address: str):
app.logger.info(f"Updating user {user_address} antisybil status")
score, expires_at = update_user_antisybil_status(user_address)
result = update_user_antisybil_status(user_address)
app.logger.info(
f"User {user_address} antisybil status refreshed {[score, expires_at]}"
f"User {user_address} antisybil status refreshed {[result.score, result.expires_at]}"
)

return {}, 204
Expand Down
6 changes: 6 additions & 0 deletions backend/app/modules/octant_rewards/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from app.exceptions import NotImplementedForGivenEpochState
from app.modules.dto import OctantRewardsDTO
from app.modules.registry import get_services
from app.modules.octant_rewards import core


def get_octant_rewards(epoch_num: int) -> OctantRewardsDTO:
Expand All @@ -24,3 +25,8 @@ def get_last_finalized_epoch_leverage() -> float:
service = get_services(context.epoch_state).octant_rewards_service

return service.get_leverage(context)


def get_epoch_rewards_rate(epoch_num: int) -> float:
context = epoch_context(epoch_num)
return core.get_rewards_rate(context.epoch_details.epoch_num)
9 changes: 9 additions & 0 deletions backend/app/modules/octant_rewards/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from app.engine.octant_rewards.operational_cost import OperationalCostPayload
from app.engine.octant_rewards.ppf import PPFPayload
from app.engine.octant_rewards.total_and_individual import TotalAndAllIndividualPayload
from app.modules.staking.proceeds.core import ESTIMATED_STAKING_REWARDS_RATE


@dataclass
Expand Down Expand Up @@ -66,3 +67,11 @@ def calculate_rewards(
ppf_value=ppf_value,
community_fund=cf_value,
)


def get_rewards_rate(_: int) -> float:
"""
Returns the rewards rate for the given epoch.
"""
# TODO Staking Rewards Rate is a static value for now but it may be calculated dynamically in the future: https://linear.app/golemfoundation/issue/OCT-1916/make-apr-a-dynamically-computed-one
return ESTIMATED_STAKING_REWARDS_RATE
17 changes: 16 additions & 1 deletion backend/app/modules/projects/details/controller.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
from typing import List, Dict

from app.context.manager import epoch_context
from app.modules.registry import get_services
from app.modules.projects.details.service.projects_details import ProjectsDetailsDTO


def get_projects_details(epoch: int, search_phrase: str):
def get_projects_details_for_multiple_params(
epochs: List[int], search_phrases: List[str]
) -> List[Dict[str, str]]:
searched_projects = []
for epoch in epochs:
for search_phrase in search_phrases:
project_details = get_projects_details(epoch, search_phrase)
searched_projects.extend(project_details)
return searched_projects


def get_projects_details(epoch: int, search_phrase: str) -> List[Dict[str, str]]:
context = epoch_context(epoch)

service = get_services(context.epoch_state).projects_details_service

filtered_projects: ProjectsDetailsDTO = (
Expand All @@ -14,6 +28,7 @@ def get_projects_details(epoch: int, search_phrase: str):
{
"name": project["name"],
"address": project["address"],
"epoch": project["epoch"],
}
for project in filtered_projects.projects_details
]
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,7 @@ def get_projects_details_by_search_phrase(

return ProjectsDetailsDTO(
projects_details=[
{
"name": project.name,
"address": project.address,
}
{"name": project.name, "address": project.address, "epoch": str(epoch)}
for project in filtered_projects_details
]
)
6 changes: 3 additions & 3 deletions backend/app/modules/staking/proceeds/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@
ESTIMATED_STAKED_AMOUNT = 100000_000000000_000000000

# TODO call an API to get a real value instead of hardcoding: https://linear.app/golemfoundation/issue/OCT-902/api-call-to-get-validators-api
ESTIMATED_STAKING_APR = 0.038
ESTIMATED_STAKING_REWARDS_RATE = 0.038


def estimate_staking_proceeds(
epoch_duration_sec: int,
staked_amount=ESTIMATED_STAKED_AMOUNT,
apr=ESTIMATED_STAKING_APR,
staking_rewards=ESTIMATED_STAKING_REWARDS_RATE,
) -> int:
if epoch_duration_sec <= 0:
return 0
return int(int(staked_amount * apr) / 31536000 * epoch_duration_sec)
return int(int(staked_amount * staking_rewards) / 31536000 * epoch_duration_sec)


def sum_mev(
Expand Down
6 changes: 3 additions & 3 deletions backend/app/modules/uq/service/preliminary.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
from datetime import datetime
from decimal import Decimal
from typing import Protocol, Optional, Tuple, runtime_checkable
from typing import Protocol, Optional, runtime_checkable

from app.context.manager import Context
from app.infrastructure.database.uniqueness_quotient import (
get_uq_by_address,
save_uq_from_address,
)
from app.modules.uq.core import calculate_uq
from app.modules.user.antisybil.dto import AntisybilStatusDTO
from app.pydantic import Model


@runtime_checkable
class Antisybil(Protocol):
def get_antisybil_status(
self, _: Context, user_address: str
) -> Optional[Tuple[float, datetime]]:
) -> Optional[AntisybilStatusDTO]:
...


Expand Down
8 changes: 4 additions & 4 deletions backend/app/modules/user/antisybil/controller.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
from datetime import datetime
from typing import Tuple
from typing import Optional

from app.context.epoch_state import EpochState
from app.context.manager import state_context
from app.modules.registry import get_services
from app.modules.user.antisybil.dto import AntisybilStatusDTO


def get_user_antisybil_status(user_address: str) -> Tuple[int, datetime]:
def get_user_antisybil_status(user_address: str) -> Optional[AntisybilStatusDTO]:
context = state_context(EpochState.CURRENT)
service = get_services(context.epoch_state).user_antisybil_service
return service.get_antisybil_status(context, user_address)


def update_user_antisybil_status(user_address: str) -> Tuple[int, datetime]:
def update_user_antisybil_status(user_address: str) -> Optional[AntisybilStatusDTO]:
context = state_context(EpochState.CURRENT)
service = get_services(context.epoch_state).user_antisybil_service

Expand Down
46 changes: 46 additions & 0 deletions backend/app/modules/user/antisybil/core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import json
from typing import Optional

from app.modules.user.antisybil.dto import AntisybilStatusDTO
from app.constants import TIMEOUT_LIST, GUEST_LIST, GUEST_LIST_STAMP_PROVIDERS
from app.infrastructure.database.models import GPStamps


def determine_antisybil_score(
score: GPStamps, user_address: str
) -> Optional[AntisybilStatusDTO]:
"""
Determine the antisybil score for a user.
Timeout list users will always have a score of 0.0 and has a higher priority than guest list users.
Guest list users will have a score increased by 21.0 if they have not been stamped by a guest list provider.
"""
if score is None:
return None

if user_address in TIMEOUT_LIST:
return AntisybilStatusDTO(
score=0.0, expires_at=score.expires_at, is_on_timeout_list=True
)
elif user_address in GUEST_LIST and not _has_guest_stamp_applied_by_gp(score):
return AntisybilStatusDTO(
score=score.score + 21.0,
expires_at=score.expires_at,
is_on_timeout_list=False,
)

return AntisybilStatusDTO(
score=score.score, expires_at=score.expires_at, is_on_timeout_list=False
)


def _has_guest_stamp_applied_by_gp(score: GPStamps) -> bool:
def get_provider(stamp) -> str:
return stamp["credential"]["credentialSubject"]["provider"]

all_stamps = json.loads(score.stamps)
stamps = [
stamp
for stamp in all_stamps
if get_provider(stamp) in GUEST_LIST_STAMP_PROVIDERS
]
return len(stamps) > 0
9 changes: 9 additions & 0 deletions backend/app/modules/user/antisybil/dto.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from dataclasses import dataclass
from datetime import datetime


@dataclass
class AntisybilStatusDTO:
score: float
expires_at: datetime
is_on_timeout_list: bool
Loading

0 comments on commit a6192bd

Please sign in to comment.