kopia lustrzana https://github.com/bugout-dev/moonstream
Merge pull request #916 from moonstream-to/leaderboard-config-management
Add initiate leaderboard config managment.pull/925/head
commit
6292aecd3a
|
@ -1,10 +1,11 @@
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from collections import Counter
|
from collections import Counter
|
||||||
from typing import List, Any, Optional, Dict, Union, Tuple
|
import json
|
||||||
|
from typing import List, Any, Optional, Dict, Union, Tuple, cast
|
||||||
import uuid
|
import uuid
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from bugout.data import BugoutResource
|
from bugout.data import BugoutResource, BugoutSearchResult
|
||||||
from eth_typing import Address
|
from eth_typing import Address
|
||||||
from hexbytes import HexBytes
|
from hexbytes import HexBytes
|
||||||
import requests # type: ignore
|
import requests # type: ignore
|
||||||
|
@ -15,7 +16,7 @@ from sqlalchemy.engine import Row
|
||||||
from web3 import Web3
|
from web3 import Web3
|
||||||
from web3.types import ChecksumAddress
|
from web3.types import ChecksumAddress
|
||||||
|
|
||||||
from .data import Score, LeaderboardScore
|
from .data import Score, LeaderboardScore, LeaderboardConfigUpdate, LeaderboardConfig
|
||||||
from .contracts import Dropper_interface, ERC20_interface, Terminus_interface
|
from .contracts import Dropper_interface, ERC20_interface, Terminus_interface
|
||||||
from .models import (
|
from .models import (
|
||||||
DropperClaimant,
|
DropperClaimant,
|
||||||
|
@ -26,11 +27,12 @@ from .models import (
|
||||||
)
|
)
|
||||||
from . import signatures
|
from . import signatures
|
||||||
from .settings import (
|
from .settings import (
|
||||||
|
bugout_client as bc,
|
||||||
BLOCKCHAIN_WEB3_PROVIDERS,
|
BLOCKCHAIN_WEB3_PROVIDERS,
|
||||||
LEADERBOARD_RESOURCE_TYPE,
|
LEADERBOARD_RESOURCE_TYPE,
|
||||||
MOONSTREAM_APPLICATION_ID,
|
MOONSTREAM_APPLICATION_ID,
|
||||||
MOONSTREAM_ADMIN_ACCESS_TOKEN,
|
MOONSTREAM_ADMIN_ACCESS_TOKEN,
|
||||||
bugout_client as bc,
|
MOONSTREAM_LEADERBOARD_CONFIGURATION_JOURNAL_ID,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -77,6 +79,18 @@ class LeaderboardDeleteError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class LeaderboardConfigNotFound(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class LeaderboardConfigAlreadyActive(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class LeaderboardConfigAlreadyInactive(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
BATCH_SIGNATURE_PAGE_SIZE = 500
|
BATCH_SIGNATURE_PAGE_SIZE = 500
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -1491,6 +1505,130 @@ def list_leaderboards_resources(
|
||||||
return query.all()
|
return query.all()
|
||||||
|
|
||||||
|
|
||||||
|
def get_leaderboard_config_entry(
|
||||||
|
leaderboard_id: uuid.UUID,
|
||||||
|
) -> BugoutSearchResult:
|
||||||
|
query = f"#leaderboard_id:{leaderboard_id}"
|
||||||
|
configs = bc.search(
|
||||||
|
token=MOONSTREAM_ADMIN_ACCESS_TOKEN,
|
||||||
|
journal_id=MOONSTREAM_LEADERBOARD_CONFIGURATION_JOURNAL_ID,
|
||||||
|
query=query,
|
||||||
|
limit=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
results = cast(List[BugoutSearchResult], configs.results)
|
||||||
|
|
||||||
|
if len(configs.results) == 0 or results[0].content is None:
|
||||||
|
raise LeaderboardConfigNotFound(
|
||||||
|
f"Leaderboard config not found for {leaderboard_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return results[0]
|
||||||
|
|
||||||
|
|
||||||
|
def get_leaderboard_config(
|
||||||
|
leaderboard_id: uuid.UUID,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Return leaderboard config from leaderboard generator journal
|
||||||
|
"""
|
||||||
|
|
||||||
|
entry = get_leaderboard_config_entry(leaderboard_id)
|
||||||
|
|
||||||
|
content = json.loads(entry.content) # type: ignore
|
||||||
|
|
||||||
|
if "status:active" not in entry.tags:
|
||||||
|
content["leaderboard_auto_update_active"] = False
|
||||||
|
else:
|
||||||
|
content["leaderboard_auto_update_active"] = True
|
||||||
|
|
||||||
|
return content
|
||||||
|
|
||||||
|
|
||||||
|
def update_leaderboard_config(
|
||||||
|
leaderboard_id: uuid.UUID, config: LeaderboardConfigUpdate
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Update leaderboard config in leaderboard generator journal
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
entry_config = get_leaderboard_config_entry(leaderboard_id)
|
||||||
|
|
||||||
|
current_config = LeaderboardConfig(**json.loads(entry_config.content)) # type: ignore
|
||||||
|
|
||||||
|
new_params = config.params
|
||||||
|
|
||||||
|
for key, value in new_params.items():
|
||||||
|
if key not in current_config.params:
|
||||||
|
continue
|
||||||
|
|
||||||
|
current_config.params[key] = value
|
||||||
|
|
||||||
|
# we replace values of parameters that are not None
|
||||||
|
|
||||||
|
entry = bc.update_entry_content(
|
||||||
|
token=MOONSTREAM_ADMIN_ACCESS_TOKEN,
|
||||||
|
journal_id=MOONSTREAM_LEADERBOARD_CONFIGURATION_JOURNAL_ID,
|
||||||
|
title=entry_config.title,
|
||||||
|
entry_id=entry_config.entry_url.split("/")[-1],
|
||||||
|
content=json.dumps(current_config.dict()),
|
||||||
|
)
|
||||||
|
|
||||||
|
new_config = json.loads(entry.content)
|
||||||
|
|
||||||
|
if "status:active" not in entry.tags:
|
||||||
|
new_config["leaderboard_auto_update_active"] = False
|
||||||
|
else:
|
||||||
|
new_config["leaderboard_auto_update_active"] = True
|
||||||
|
|
||||||
|
return new_config
|
||||||
|
|
||||||
|
|
||||||
|
def activate_leaderboard_config(
|
||||||
|
leaderboard_id: uuid.UUID,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Add tag status:active to leaderboard config journal entry
|
||||||
|
"""
|
||||||
|
|
||||||
|
entry_config = get_leaderboard_config_entry(leaderboard_id)
|
||||||
|
|
||||||
|
if "status:active" in entry_config.tags:
|
||||||
|
raise LeaderboardConfigAlreadyActive(
|
||||||
|
f"Leaderboard config {leaderboard_id} already active"
|
||||||
|
)
|
||||||
|
|
||||||
|
bc.create_tags(
|
||||||
|
token=MOONSTREAM_ADMIN_ACCESS_TOKEN,
|
||||||
|
journal_id=MOONSTREAM_LEADERBOARD_CONFIGURATION_JOURNAL_ID,
|
||||||
|
entry_id=entry_config.entry_url.split("/")[-1],
|
||||||
|
tags=["status:active"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def deactivate_leaderboard_config(
|
||||||
|
leaderboard_id: uuid.UUID,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Remove tag status:active from leaderboard config journal entry
|
||||||
|
"""
|
||||||
|
|
||||||
|
entry_config = get_leaderboard_config_entry(leaderboard_id)
|
||||||
|
|
||||||
|
if "status:active" not in entry_config.tags:
|
||||||
|
raise LeaderboardConfigAlreadyInactive(
|
||||||
|
f"Leaderboard config {leaderboard_id} not active"
|
||||||
|
)
|
||||||
|
|
||||||
|
bc.delete_tag(
|
||||||
|
token=MOONSTREAM_ADMIN_ACCESS_TOKEN,
|
||||||
|
journal_id=MOONSTREAM_LEADERBOARD_CONFIGURATION_JOURNAL_ID,
|
||||||
|
entry_id=entry_config.entry_url.split("/")[-1],
|
||||||
|
tag="status:active",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def revoke_resource(
|
def revoke_resource(
|
||||||
db_session: Session, leaderboard_id: uuid.UUID
|
db_session: Session, leaderboard_id: uuid.UUID
|
||||||
) -> Optional[uuid.UUID]:
|
) -> Optional[uuid.UUID]:
|
||||||
|
@ -1529,6 +1667,7 @@ def check_leaderboard_resource_permissions(
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": f"Bearer {token}",
|
"Authorization": f"Bearer {token}",
|
||||||
}
|
}
|
||||||
|
|
||||||
# If user don't have at least read permission return 404
|
# If user don't have at least read permission return 404
|
||||||
result = requests.get(url=permission_url, headers=headers, timeout=10)
|
result = requests.get(url=permission_url, headers=headers, timeout=10)
|
||||||
|
|
||||||
|
|
|
@ -428,3 +428,17 @@ class LeaderboardDeletedResponse(BaseModel):
|
||||||
class LeaderboardScoresChangesResponse(BaseModel):
|
class LeaderboardScoresChangesResponse(BaseModel):
|
||||||
players_count: int
|
players_count: int
|
||||||
date: datetime
|
date: datetime
|
||||||
|
|
||||||
|
|
||||||
|
class LeaderboardConfig(BaseModel):
|
||||||
|
leaderboard_id: str
|
||||||
|
leaderboard_auto_update_active: bool = False
|
||||||
|
query_name: str
|
||||||
|
params: Dict[str, int]
|
||||||
|
normalize_addresses: bool
|
||||||
|
|
||||||
|
|
||||||
|
class LeaderboardConfigUpdate(BaseModel):
|
||||||
|
query_name: Optional[str] = None
|
||||||
|
params: Dict[str, int]
|
||||||
|
normalize_addresses: Optional[bool] = None
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
"""
|
"""
|
||||||
Leaderboard API.
|
Leaderboard API.
|
||||||
"""
|
"""
|
||||||
from datetime import datetime
|
|
||||||
import logging
|
import logging
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
|
from bugout.exceptions import BugoutResponseException
|
||||||
from web3 import Web3
|
from web3 import Web3
|
||||||
from fastapi import FastAPI, Request, Depends, Response, Query, Path, Body, Header
|
from fastapi import FastAPI, Request, Depends, Response, Query, Path, Body, Header
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
@ -668,3 +668,215 @@ async def leaderboard_push_scores(
|
||||||
]
|
]
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@app.get(
|
||||||
|
"/{leaderboard_id}/config",
|
||||||
|
response_model=data.LeaderboardConfig,
|
||||||
|
tags=["Authorized Endpoints"],
|
||||||
|
)
|
||||||
|
async def leaderboard_config(
|
||||||
|
request: Request,
|
||||||
|
leaderboard_id: UUID = Path(..., description="Leaderboard ID"),
|
||||||
|
db_session: Session = Depends(db.yield_db_session),
|
||||||
|
Authorization: str = AuthHeader,
|
||||||
|
) -> data.LeaderboardConfig:
|
||||||
|
"""
|
||||||
|
Get leaderboard config.
|
||||||
|
"""
|
||||||
|
token = request.state.token
|
||||||
|
try:
|
||||||
|
access = actions.check_leaderboard_resource_permissions(
|
||||||
|
db_session=db_session,
|
||||||
|
leaderboard_id=leaderboard_id,
|
||||||
|
token=token,
|
||||||
|
)
|
||||||
|
except NoResultFound as e:
|
||||||
|
raise EngineHTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="Leaderboard not found.",
|
||||||
|
)
|
||||||
|
|
||||||
|
if not access:
|
||||||
|
raise EngineHTTPException(
|
||||||
|
status_code=403, detail="You don't have access to this leaderboard."
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
leaderboard_config = actions.get_leaderboard_config(
|
||||||
|
leaderboard_id=leaderboard_id,
|
||||||
|
)
|
||||||
|
except BugoutResponseException as e:
|
||||||
|
raise EngineHTTPException(status_code=e.status_code, detail=e.detail)
|
||||||
|
except actions.LeaderboardConfigNotFound as e:
|
||||||
|
raise EngineHTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="Leaderboard config not found.",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error while getting leaderboard config: {e}")
|
||||||
|
raise EngineHTTPException(status_code=500, detail="Internal server error")
|
||||||
|
|
||||||
|
return data.LeaderboardConfig(**leaderboard_config)
|
||||||
|
|
||||||
|
|
||||||
|
@app.put(
|
||||||
|
"/{leaderboard_id}/config",
|
||||||
|
response_model=data.LeaderboardConfig,
|
||||||
|
tags=["Authorized Endpoints"],
|
||||||
|
)
|
||||||
|
async def leaderboard_config_update(
|
||||||
|
request: Request,
|
||||||
|
leaderboard_id: UUID = Path(..., description="Leaderboard ID"),
|
||||||
|
config: data.LeaderboardConfigUpdate = Body(..., description="Leaderboard config."),
|
||||||
|
db_session: Session = Depends(db.yield_db_session),
|
||||||
|
Authorization: str = AuthHeader,
|
||||||
|
) -> data.LeaderboardConfig:
|
||||||
|
"""
|
||||||
|
Update leaderboard config.
|
||||||
|
"""
|
||||||
|
token = request.state.token
|
||||||
|
try:
|
||||||
|
access = actions.check_leaderboard_resource_permissions(
|
||||||
|
db_session=db_session,
|
||||||
|
leaderboard_id=leaderboard_id,
|
||||||
|
token=token,
|
||||||
|
)
|
||||||
|
except NoResultFound as e:
|
||||||
|
raise EngineHTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="Leaderboard not found.",
|
||||||
|
)
|
||||||
|
|
||||||
|
if not access:
|
||||||
|
raise EngineHTTPException(
|
||||||
|
status_code=403, detail="You don't have access to this leaderboard."
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
leaderboard_config = actions.update_leaderboard_config(
|
||||||
|
leaderboard_id=leaderboard_id,
|
||||||
|
config=config,
|
||||||
|
)
|
||||||
|
except BugoutResponseException as e:
|
||||||
|
raise EngineHTTPException(status_code=e.status_code, detail=e.detail)
|
||||||
|
except actions.LeaderboardConfigNotFound as e:
|
||||||
|
raise EngineHTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="Leaderboard config not found.",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error while updating leaderboard config: {e}")
|
||||||
|
raise EngineHTTPException(status_code=500, detail="Internal server error")
|
||||||
|
|
||||||
|
return data.LeaderboardConfig(**leaderboard_config)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post(
|
||||||
|
"/{leaderboard_id}/config/activate",
|
||||||
|
response_model=bool,
|
||||||
|
tags=["Authorized Endpoints"],
|
||||||
|
)
|
||||||
|
async def leaderboard_config_activate(
|
||||||
|
request: Request,
|
||||||
|
leaderboard_id: UUID = Path(..., description="Leaderboard ID"),
|
||||||
|
db_session: Session = Depends(db.yield_db_session),
|
||||||
|
Authorization: str = AuthHeader,
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Activate leaderboard config.
|
||||||
|
"""
|
||||||
|
token = request.state.token
|
||||||
|
try:
|
||||||
|
access = actions.check_leaderboard_resource_permissions(
|
||||||
|
db_session=db_session,
|
||||||
|
leaderboard_id=leaderboard_id,
|
||||||
|
token=token,
|
||||||
|
)
|
||||||
|
except NoResultFound as e:
|
||||||
|
raise EngineHTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="Leaderboard not found.",
|
||||||
|
)
|
||||||
|
|
||||||
|
if not access:
|
||||||
|
raise EngineHTTPException(
|
||||||
|
status_code=403, detail="You don't have access to this leaderboard."
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
actions.activate_leaderboard_config(
|
||||||
|
leaderboard_id=leaderboard_id,
|
||||||
|
)
|
||||||
|
except BugoutResponseException as e:
|
||||||
|
raise EngineHTTPException(status_code=e.status_code, detail=e.detail)
|
||||||
|
except actions.LeaderboardConfigNotFound as e:
|
||||||
|
raise EngineHTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="Leaderboard config not found.",
|
||||||
|
)
|
||||||
|
except actions.LeaderboardConfigAlreadyActive as e:
|
||||||
|
raise EngineHTTPException(
|
||||||
|
status_code=409,
|
||||||
|
detail="Leaderboard config is already active.",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error while activating leaderboard config: {e}")
|
||||||
|
raise EngineHTTPException(status_code=500, detail="Internal server error")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@app.post(
|
||||||
|
"/{leaderboard_id}/config/deactivate",
|
||||||
|
response_model=bool,
|
||||||
|
tags=["Authorized Endpoints"],
|
||||||
|
)
|
||||||
|
async def leaderboard_config_deactivate(
|
||||||
|
request: Request,
|
||||||
|
leaderboard_id: UUID = Path(..., description="Leaderboard ID"),
|
||||||
|
db_session: Session = Depends(db.yield_db_session),
|
||||||
|
Authorization: str = AuthHeader,
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Deactivate leaderboard config.
|
||||||
|
"""
|
||||||
|
token = request.state.token
|
||||||
|
try:
|
||||||
|
access = actions.check_leaderboard_resource_permissions(
|
||||||
|
db_session=db_session,
|
||||||
|
leaderboard_id=leaderboard_id,
|
||||||
|
token=token,
|
||||||
|
)
|
||||||
|
except NoResultFound as e:
|
||||||
|
raise EngineHTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="Leaderboard not found.",
|
||||||
|
)
|
||||||
|
|
||||||
|
if not access:
|
||||||
|
raise EngineHTTPException(
|
||||||
|
status_code=403, detail="You don't have access to this leaderboard."
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
actions.deactivate_leaderboard_config(
|
||||||
|
leaderboard_id=leaderboard_id,
|
||||||
|
)
|
||||||
|
except BugoutResponseException as e:
|
||||||
|
raise EngineHTTPException(status_code=e.status_code, detail=e.detail)
|
||||||
|
except actions.LeaderboardConfigNotFound as e:
|
||||||
|
raise EngineHTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail="Leaderboard config not found.",
|
||||||
|
)
|
||||||
|
except actions.LeaderboardConfigAlreadyInactive as e:
|
||||||
|
raise EngineHTTPException(
|
||||||
|
status_code=409,
|
||||||
|
detail="Leaderboard config is already inactive.",
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error while deactivating leaderboard config: {e}")
|
||||||
|
raise EngineHTTPException(status_code=500, detail="Internal server error")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
|
@ -198,3 +198,11 @@ if MOONSTREAM_ADMIN_ACCESS_TOKEN == "":
|
||||||
MOONSTREAM_ADMIN_ID = os.environ.get("MOONSTREAM_ADMIN_ID", "")
|
MOONSTREAM_ADMIN_ID = os.environ.get("MOONSTREAM_ADMIN_ID", "")
|
||||||
if MOONSTREAM_ADMIN_ID == "":
|
if MOONSTREAM_ADMIN_ID == "":
|
||||||
raise ValueError("MOONSTREAM_ADMIN_ID environment variable must be set")
|
raise ValueError("MOONSTREAM_ADMIN_ID environment variable must be set")
|
||||||
|
|
||||||
|
MOONSTREAM_LEADERBOARD_CONFIGURATION_JOURNAL_ID = os.environ.get(
|
||||||
|
"MOONSTREAM_LEADERBOARD_CONFIGURATION_JOURNAL_ID", ""
|
||||||
|
)
|
||||||
|
if MOONSTREAM_LEADERBOARD_CONFIGURATION_JOURNAL_ID == "":
|
||||||
|
raise ValueError(
|
||||||
|
"MOONSTREAM_LEADERBOARD_CONFIGURATION_JOURNAL_ID environment variable must be set"
|
||||||
|
)
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
0.0.6
|
0.0.7
|
||||||
|
|
|
@ -19,3 +19,6 @@ export MOONSTREAM_MUMBAI_WEB3_PROVIDER_URI="<JSON_RPC_API_URL>"
|
||||||
export MOONSTREAM_POLYGON_WEB3_PROVIDER_URI="<JSON_RPC_API_URL>"
|
export MOONSTREAM_POLYGON_WEB3_PROVIDER_URI="<JSON_RPC_API_URL>"
|
||||||
export MOONSTREAM_XDAI_WEB3_PROVIDER_URI="<JSON_RPC_API_URL>"
|
export MOONSTREAM_XDAI_WEB3_PROVIDER_URI="<JSON_RPC_API_URL>"
|
||||||
export ENGINE_NODEBALANCER_ACCESS_ID="<access_id_for_Moonstream_Node_Balancer-if_provided_it_is_interpolated_into_provider_URIs>"
|
export ENGINE_NODEBALANCER_ACCESS_ID="<access_id_for_Moonstream_Node_Balancer-if_provided_it_is_interpolated_into_provider_URIs>"
|
||||||
|
|
||||||
|
# leaderboard config
|
||||||
|
export MOONSTREAM_LEADERBOARD_CONFIGURATION_JOURNAL_ID="<config_journal_id>"
|
||||||
|
|
Ładowanie…
Reference in New Issue