kopia lustrzana https://github.com/bugout-dev/moonstream
Merge branch 'main' into return-jobs-endpoint
commit
9a722abcbb
|
@ -82,7 +82,7 @@ def get_entity_subscription_collection_id(
|
|||
resource_type: str,
|
||||
token: Union[uuid.UUID, str],
|
||||
user_id: uuid.UUID,
|
||||
) -> Optional[str]:
|
||||
) -> str:
|
||||
"""
|
||||
Get collection_id from brood resources. If collection not exist and create_if_not_exist is True
|
||||
"""
|
||||
|
|
|
@ -9,41 +9,39 @@ from typing import Any, Dict, List
|
|||
from uuid import UUID
|
||||
|
||||
import boto3 # type: ignore
|
||||
from bugout.data import BugoutResource
|
||||
from entity.data import EntityResponse # type: ignore
|
||||
from bugout.data import BugoutJournalEntity, BugoutResource
|
||||
from fastapi import BackgroundTasks, FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from moonstreamdb.blockchain import (
|
||||
AvailableBlockchainType,
|
||||
get_label_model,
|
||||
get_block_model,
|
||||
get_label_model,
|
||||
get_transaction_model,
|
||||
)
|
||||
|
||||
from sqlalchemy import text
|
||||
|
||||
from .actions import (
|
||||
generate_s3_access_links,
|
||||
query_parameter_hash,
|
||||
get_entity_subscription_collection_id,
|
||||
EntityCollectionNotFoundException,
|
||||
)
|
||||
from . import data
|
||||
from .actions import (
|
||||
EntityCollectionNotFoundException,
|
||||
generate_s3_access_links,
|
||||
get_entity_subscription_collection_id,
|
||||
query_parameter_hash,
|
||||
)
|
||||
from .middleware import MoonstreamHTTPException
|
||||
from .settings import (
|
||||
BUGOUT_RESOURCE_TYPE_SUBSCRIPTION,
|
||||
BUGOUT_RESOURCE_TYPE_ENTITY_SUBSCRIPTION,
|
||||
MOONSTREAM_ADMIN_ACCESS_TOKEN,
|
||||
BUGOUT_RESOURCE_TYPE_SUBSCRIPTION,
|
||||
DOCS_TARGET_PATH,
|
||||
LINKS_EXPIRATION_TIME,
|
||||
MOONSTREAM_ADMIN_ACCESS_TOKEN,
|
||||
MOONSTREAM_S3_QUERIES_BUCKET,
|
||||
MOONSTREAM_S3_QUERIES_BUCKET_PREFIX,
|
||||
MOONSTREAM_S3_SMARTCONTRACTS_ABI_BUCKET,
|
||||
MOONSTREAM_S3_SMARTCONTRACTS_ABI_PREFIX,
|
||||
NB_CONTROLLER_ACCESS_ID,
|
||||
ORIGINS,
|
||||
LINKS_EXPIRATION_TIME,
|
||||
MOONSTREAM_S3_SMARTCONTRACTS_ABI_BUCKET,
|
||||
)
|
||||
from .settings import bugout_client as bc, entity_client as ec
|
||||
from .settings import bugout_client as bc
|
||||
from .stats_worker import dashboard, queries
|
||||
from .version import MOONCRAWL_VERSION
|
||||
|
||||
|
@ -115,12 +113,11 @@ async def status_handler(
|
|||
)
|
||||
|
||||
try:
|
||||
collection_id = get_entity_subscription_collection_id(
|
||||
journal_id = get_entity_subscription_collection_id(
|
||||
resource_type=BUGOUT_RESOURCE_TYPE_ENTITY_SUBSCRIPTION,
|
||||
token=MOONSTREAM_ADMIN_ACCESS_TOKEN,
|
||||
user_id=UUID(stats_update.user_id),
|
||||
)
|
||||
|
||||
except EntityCollectionNotFoundException as e:
|
||||
raise MoonstreamHTTPException(
|
||||
status_code=404,
|
||||
|
@ -136,20 +133,19 @@ async def status_handler(
|
|||
|
||||
s3_client = boto3.client("s3")
|
||||
|
||||
subscription_by_id: Dict[str, EntityResponse] = {}
|
||||
subscription_by_id: Dict[str, BugoutJournalEntity] = {}
|
||||
|
||||
for dashboard_subscription_filters in dashboard_resource.resource_data[
|
||||
"subscription_settings"
|
||||
]:
|
||||
# get subscription by id
|
||||
|
||||
subscription: EntityResponse = ec.get_entity(
|
||||
subscription: BugoutJournalEntity = bc.get_entity(
|
||||
token=stats_update.token,
|
||||
collection_id=collection_id,
|
||||
journal_id=journal_id,
|
||||
entity_id=dashboard_subscription_filters["subscription_id"],
|
||||
)
|
||||
|
||||
subscription_by_id[str(subscription.entity_id)] = subscription
|
||||
subscription_by_id[str(subscription.id)] = subscription
|
||||
|
||||
try:
|
||||
background_tasks.add_task(
|
||||
|
@ -182,7 +178,7 @@ async def status_handler(
|
|||
subscriprions_type = reqired_field["subscription_type_id"]
|
||||
|
||||
for timescale in stats_update.timescales:
|
||||
presigned_urls_response[subscription_entity.entity_id] = {}
|
||||
presigned_urls_response[subscription_entity.id] = {}
|
||||
|
||||
try:
|
||||
result_key = f"{MOONSTREAM_S3_SMARTCONTRACTS_ABI_PREFIX}/{dashboard.blockchain_by_subscription_id[subscriprions_type]}/contracts_data/{subscription_entity.address}/{stats_update.dashboard_id}/v1/{timescale}.json"
|
||||
|
@ -201,7 +197,7 @@ async def status_handler(
|
|||
HttpMethod="GET",
|
||||
)
|
||||
|
||||
presigned_urls_response[subscription_entity.entity_id][timescale] = {
|
||||
presigned_urls_response[subscription_entity.id][timescale] = {
|
||||
"url": stats_presigned_url,
|
||||
"headers": {
|
||||
"If-Modified-Since": (
|
||||
|
|
|
@ -2,11 +2,11 @@ import argparse
|
|||
import json
|
||||
import logging
|
||||
import os
|
||||
from typing import Any, Dict
|
||||
from typing import cast, List
|
||||
import uuid
|
||||
|
||||
import requests # type: ignore
|
||||
|
||||
from bugout.data import BugoutSearchResult
|
||||
|
||||
from .utils import get_results_for_moonstream_query
|
||||
from ..settings import (
|
||||
|
@ -37,7 +37,7 @@ def handle_leaderboards(args: argparse.Namespace) -> None:
|
|||
|
||||
### get leaderboard journal
|
||||
|
||||
query = "#leaderboard"
|
||||
query = "#leaderboard #status:active"
|
||||
|
||||
if args.leaderboard_id: # way to run only one leaderboard
|
||||
query += f" #leaderboard_id:{args.leaderboard_id}"
|
||||
|
@ -47,19 +47,20 @@ def handle_leaderboards(args: argparse.Namespace) -> None:
|
|||
journal_id=MOONSTREAM_LEADERBOARD_GENERATOR_JOURNAL_ID,
|
||||
query=query,
|
||||
limit=100,
|
||||
timeout=BUGOUT_REQUEST_TIMEOUT_SECONDS,
|
||||
timeout=10,
|
||||
)
|
||||
leaderboards_results = cast(List[BugoutSearchResult], leaderboards.results)
|
||||
except Exception as e:
|
||||
logger.error(f"Could not get leaderboards from journal: {e}")
|
||||
return
|
||||
|
||||
if len(leaderboards.results) == 0:
|
||||
if len(leaderboards_results) == 0:
|
||||
logger.error("No leaderboard found")
|
||||
return
|
||||
|
||||
logger.info(f"Found {len(leaderboards.results)} leaderboards")
|
||||
logger.info(f"Found {len(leaderboards_results)} leaderboards")
|
||||
|
||||
for leaderboard in leaderboards.results:
|
||||
for leaderboard in leaderboards_results:
|
||||
logger.info(
|
||||
f"Processing leaderboard: {leaderboard.title} with id: {[tag for tag in leaderboard.tags if tag.startswith('leaderboard_id')]}"
|
||||
)
|
||||
|
@ -109,7 +110,7 @@ def handle_leaderboards(args: argparse.Namespace) -> None:
|
|||
logger.error(f"Could not get results for query {query_name} in time")
|
||||
continue
|
||||
|
||||
leaderboard_push_api_url = f"{MOONSTREAM_ENGINE_URL}/leaderboard/{leaderboard_id}/scores?normalize_addresses={leaderboard_data['normalize_addresses']}"
|
||||
leaderboard_push_api_url = f"{MOONSTREAM_ENGINE_URL}/leaderboard/{leaderboard_id}/scores?normalize_addresses={leaderboard_data['normalize_addresses']}&overwrite=true"
|
||||
|
||||
leaderboard_api_headers = {
|
||||
"Authorization": f"Bearer {args.query_api_access_token}",
|
||||
|
|
|
@ -189,7 +189,11 @@ def handle_historical_crawl(args: argparse.Namespace) -> None:
|
|||
)
|
||||
|
||||
if addresses_filter:
|
||||
filtered_function_call_jobs = [job for job in all_function_call_jobs]
|
||||
filtered_function_call_jobs = [
|
||||
job
|
||||
for job in all_function_call_jobs
|
||||
if job.contract_address in addresses_filter
|
||||
]
|
||||
else:
|
||||
filtered_function_call_jobs = all_function_call_jobs
|
||||
|
||||
|
@ -267,6 +271,8 @@ def handle_historical_crawl(args: argparse.Namespace) -> None:
|
|||
|
||||
end_block = args.end
|
||||
|
||||
start_block = args.start
|
||||
|
||||
# get set of addresses from event jobs and function call jobs
|
||||
if args.find_deployed_blocks:
|
||||
addresses_set = set()
|
||||
|
|
|
@ -195,7 +195,7 @@ def get_crawl_job_entries(
|
|||
query += f" created_at:>={created_at_filter}"
|
||||
|
||||
current_offset = 0
|
||||
entries = []
|
||||
entries: List[BugoutSearchResult] = []
|
||||
while True:
|
||||
search_result = bugout_client.search(
|
||||
token=MOONSTREAM_ADMIN_ACCESS_TOKEN,
|
||||
|
@ -205,10 +205,11 @@ def get_crawl_job_entries(
|
|||
limit=limit,
|
||||
timeout=BUGOUT_REQUEST_TIMEOUT_SECONDS,
|
||||
)
|
||||
entries.extend(search_result.results)
|
||||
search_results = cast(List[BugoutSearchResult], search_result.results)
|
||||
entries.extend(search_results)
|
||||
|
||||
# if len(entries) >= search_result.total_results:
|
||||
if len(search_result.results) == 0:
|
||||
if len(search_results) == 0:
|
||||
break
|
||||
current_offset += limit
|
||||
return entries
|
||||
|
@ -402,8 +403,9 @@ def _get_heartbeat_entry_id(
|
|||
limit=1,
|
||||
timeout=BUGOUT_REQUEST_TIMEOUT_SECONDS,
|
||||
)
|
||||
if entries.results:
|
||||
return entries.results[0].entry_url.split("/")[-1]
|
||||
search_results = cast(List[BugoutSearchResult], entries.results)
|
||||
if search_results:
|
||||
return search_results[0].entry_url.split("/")[-1]
|
||||
else:
|
||||
logger.info(f"No {crawler_type} heartbeat entry found, creating one")
|
||||
entry = bugout_client.create_entry(
|
||||
|
|
|
@ -3,10 +3,8 @@ from typing import Dict, Optional
|
|||
from uuid import UUID
|
||||
|
||||
from bugout.app import Bugout
|
||||
from entity.client import Entity # type: ignore
|
||||
from moonstreamdb.blockchain import AvailableBlockchainType
|
||||
|
||||
|
||||
# Bugout
|
||||
BUGOUT_BROOD_URL = os.environ.get("BUGOUT_BROOD_URL", "https://auth.bugout.dev")
|
||||
BUGOUT_SPIRE_URL = os.environ.get("BUGOUT_SPIRE_URL", "https://spire.bugout.dev")
|
||||
|
@ -14,15 +12,6 @@ BUGOUT_SPIRE_URL = os.environ.get("BUGOUT_SPIRE_URL", "https://spire.bugout.dev"
|
|||
bugout_client = Bugout(brood_api_url=BUGOUT_BROOD_URL, spire_api_url=BUGOUT_SPIRE_URL)
|
||||
|
||||
|
||||
# Entity
|
||||
|
||||
|
||||
MOONSTREAM_ENTITY_URL = os.environ.get("MOONSTREAM_ENTITY_URL", "")
|
||||
if MOONSTREAM_ENTITY_URL == "":
|
||||
raise ValueError("MOONSTREAM_ENTITY_URL environment variable must be set")
|
||||
|
||||
entity_client = Entity(MOONSTREAM_ENTITY_URL)
|
||||
|
||||
MOONSTREAM_API_URL = os.environ.get("MOONSTREAM_API_URL", "https://api.moonstream.to")
|
||||
MOONSTREAM_ENGINE_URL = os.environ.get(
|
||||
"MOONSTREAM_ENGINE_URL", "https://engineapi.moonstream.to"
|
||||
|
|
|
@ -6,21 +6,25 @@ import hashlib
|
|||
import json
|
||||
import logging
|
||||
import time
|
||||
import traceback
|
||||
from datetime import datetime, timedelta
|
||||
from enum import Enum
|
||||
from typing import Any, Callable, Dict, List, Optional, Union
|
||||
from typing import Any, Callable, cast, Dict, List, Optional, Union
|
||||
from uuid import UUID
|
||||
|
||||
import boto3 # type: ignore
|
||||
from bugout.data import BugoutResource, BugoutResources
|
||||
from entity.data import EntityResponse, EntityCollectionResponse # type: ignore
|
||||
from bugout.data import (
|
||||
BugoutJournalEntity,
|
||||
BugoutResource,
|
||||
BugoutResources,
|
||||
BugoutSearchResultAsEntity,
|
||||
)
|
||||
from moonstreamdb.blockchain import (
|
||||
AvailableBlockchainType,
|
||||
get_label_model,
|
||||
get_transaction_model,
|
||||
)
|
||||
from sqlalchemy import and_, cast, distinct, extract, func, text
|
||||
from sqlalchemy import and_, distinct, extract, func, text
|
||||
from sqlalchemy import cast as sqlalchemy_cast
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.sql.operators import in_op
|
||||
from web3 import Web3
|
||||
|
@ -29,14 +33,14 @@ from ..blockchain import connect
|
|||
from ..db import yield_db_read_only_session_ctx
|
||||
from ..reporter import reporter
|
||||
from ..settings import (
|
||||
BUGOUT_RESOURCE_TYPE_ENTITY_SUBSCRIPTION,
|
||||
CRAWLER_LABEL,
|
||||
MOONSTREAM_ADMIN_ACCESS_TOKEN,
|
||||
MOONSTREAM_S3_SMARTCONTRACTS_ABI_BUCKET,
|
||||
MOONSTREAM_S3_SMARTCONTRACTS_ABI_PREFIX,
|
||||
NB_CONTROLLER_ACCESS_ID,
|
||||
MOONSTREAM_S3_SMARTCONTRACTS_ABI_BUCKET,
|
||||
BUGOUT_RESOURCE_TYPE_ENTITY_SUBSCRIPTION,
|
||||
)
|
||||
from ..settings import bugout_client as bc, entity_client as ec
|
||||
from ..settings import bugout_client as bc
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -157,11 +161,13 @@ def generate_data(
|
|||
.filter(in_op(label_model.label_data["name"].astext, functions))
|
||||
.filter(
|
||||
label_model.block_timestamp
|
||||
>= cast(extract("epoch", start), label_model.block_timestamp.type)
|
||||
>= sqlalchemy_cast(
|
||||
extract("epoch", start), label_model.block_timestamp.type
|
||||
)
|
||||
)
|
||||
.filter(
|
||||
label_model.block_timestamp
|
||||
< cast(
|
||||
< sqlalchemy_cast(
|
||||
extract("epoch", (start + timescales_delta[timescale]["timedelta"])),
|
||||
label_model.block_timestamp.type,
|
||||
)
|
||||
|
@ -652,25 +658,26 @@ def stats_generate_handler(args: argparse.Namespace):
|
|||
|
||||
address_dashboard_id_subscription_id_tree: Dict[str, Any] = {}
|
||||
|
||||
for user_id, collection_id in user_collection_by_id.items():
|
||||
for user_id, journal_id in user_collection_by_id.items():
|
||||
# request all subscriptions for user
|
||||
|
||||
user_subscriptions: EntityCollectionResponse = ec.search_entities(
|
||||
user_subscriptions = bc.search(
|
||||
token=MOONSTREAM_ADMIN_ACCESS_TOKEN,
|
||||
collection_id=collection_id,
|
||||
required_field=[
|
||||
"subscription_type_id:{}".format(
|
||||
subscription_id_by_blockchain[args.blockchain]
|
||||
)
|
||||
],
|
||||
journal_id=journal_id,
|
||||
query=f"tag:subscription_type_id:{subscription_id_by_blockchain[args.blockchain]}",
|
||||
representation="entity",
|
||||
)
|
||||
|
||||
user_subscriptions_results = cast(
|
||||
List[BugoutSearchResultAsEntity], user_subscriptions.results
|
||||
)
|
||||
logger.info(
|
||||
f"Amount of user subscriptions: {len(user_subscriptions.entities)}"
|
||||
f"Amount of user subscriptions: {len(user_subscriptions_results)}"
|
||||
)
|
||||
|
||||
for subscription in user_subscriptions.entities:
|
||||
subscription_id = str(subscription.entity_id)
|
||||
for subscription in user_subscriptions_results:
|
||||
entity_url_list = subscription.entity_url.split("/")
|
||||
subscription_id = entity_url_list[len(entity_url_list) - 1]
|
||||
|
||||
if subscription_id not in dashboards_by_subscription:
|
||||
logger.info(
|
||||
|
@ -1014,7 +1021,7 @@ def stats_generate_handler(args: argparse.Namespace):
|
|||
def stats_generate_api_task(
|
||||
timescales: List[str],
|
||||
dashboard: BugoutResource,
|
||||
subscription_by_id: Dict[str, EntityResponse],
|
||||
subscription_by_id: Dict[str, BugoutJournalEntity],
|
||||
access_id: Optional[UUID] = None,
|
||||
):
|
||||
"""
|
||||
|
|
|
@ -2,4 +2,4 @@
|
|||
Moonstream crawlers version.
|
||||
"""
|
||||
|
||||
MOONCRAWL_VERSION = "0.3.3"
|
||||
MOONCRAWL_VERSION = "0.3.4"
|
||||
|
|
|
@ -4,9 +4,6 @@ export BUGOUT_SPIRE_URL="https://spire.bugout.dev"
|
|||
export HUMBUG_REPORTER_CRAWLERS_TOKEN="<Bugout_Humbug_token_for_crash_reports>"
|
||||
|
||||
|
||||
# Entity environment variables
|
||||
export MOONSTREAM_ENTITY_URL="https://api.moonstream.to/entity"
|
||||
|
||||
# Engine environment variables
|
||||
export MOONSTREAM_ENGINE_URL="https://engineapi.moonstream.to"
|
||||
|
||||
|
|
|
@ -34,12 +34,11 @@ setup(
|
|||
zip_safe=False,
|
||||
install_requires=[
|
||||
"boto3",
|
||||
"bugout>=0.2.8",
|
||||
"bugout>=0.2.13",
|
||||
"chardet",
|
||||
"fastapi",
|
||||
"moonstreamdb>=0.3.4",
|
||||
"moonstream>=0.1.1",
|
||||
"moonstream-entity>=0.0.5",
|
||||
"moonworm[moonstream]>=0.6.2",
|
||||
"humbug",
|
||||
"pydantic==1.9.2",
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
"""Request ID decimal column
|
||||
|
||||
Revision ID: 040f2dfde5a5
|
||||
Revises: b4257b10daaf
|
||||
Create Date: 2023-08-10 08:58:22.052336
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '040f2dfde5a5'
|
||||
down_revision = 'b4257b10daaf'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('call_requests', sa.Column('request_id', sa.DECIMAL(), nullable=True))
|
||||
op.create_index(op.f('ix_call_requests_request_id'), 'call_requests', ['request_id'], unique=False)
|
||||
op.create_unique_constraint(op.f('uq_call_requests_registered_contract_id'), 'call_requests', ['registered_contract_id', 'request_id'])
|
||||
|
||||
# Manual
|
||||
# Fetch IDs of duplicates for 'dropper-v0.2.0' call_request_type and delete it
|
||||
op.execute("""WITH Duplicates AS (
|
||||
SELECT
|
||||
id,
|
||||
registered_contract_id,
|
||||
call_request_type_name,
|
||||
parameters->'requestID' AS request_id,
|
||||
created_at,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY
|
||||
registered_contract_id,
|
||||
call_request_type_name,
|
||||
parameters->'requestID'
|
||||
ORDER BY created_at ASC
|
||||
) AS row_num
|
||||
FROM call_requests
|
||||
WHERE call_request_type_name = 'dropper-v0.2.0'
|
||||
),
|
||||
DeleteDuplicates AS (
|
||||
SELECT id
|
||||
FROM
|
||||
Duplicates
|
||||
WHERE
|
||||
row_num < (
|
||||
SELECT COUNT(*) FROM Duplicates d2
|
||||
WHERE d2.registered_contract_id = Duplicates.registered_contract_id
|
||||
AND d2.call_request_type_name = Duplicates.call_request_type_name
|
||||
AND d2.request_id = Duplicates.request_id
|
||||
)
|
||||
)
|
||||
DELETE FROM call_requests WHERE id IN (SELECT id FROM DeleteDuplicates);""")
|
||||
|
||||
# Fulfill not empty requestID values
|
||||
op.execute("UPDATE call_requests SET request_id = CAST(parameters->>'requestID' AS DECIMAL) WHERE parameters->>'requestID' IS NOT NULL;")
|
||||
# Fulfill raw types with random requestID
|
||||
op.execute("UPDATE call_requests SET request_id = FLOOR(RANDOM()* 120500600 + 120400600) WHERE parameters->>'requestID' IS NULL;")
|
||||
op.alter_column("call_requests", "request_id", nullable=False)
|
||||
|
||||
# Other
|
||||
op.create_unique_constraint(op.f('uq_blockchains_id'), 'blockchains', ['id'])
|
||||
op.create_unique_constraint(op.f('uq_call_request_types_name'), 'call_request_types', ['name'])
|
||||
op.create_unique_constraint(op.f('uq_metatx_requesters_id'), 'metatx_requesters', ['id'])
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f('ix_call_requests_request_id'), table_name='call_requests')
|
||||
op.drop_column('call_requests', 'request_id')
|
||||
# ### end Alembic commands ###
|
|
@ -0,0 +1,167 @@
|
|||
"""Call request types and Metatx requesters
|
||||
|
||||
Revision ID: b4257b10daaf
|
||||
Revises: dedd8a7d0624
|
||||
Create Date: 2023-08-02 18:28:14.724453
|
||||
|
||||
"""
|
||||
import uuid
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'b4257b10daaf'
|
||||
down_revision = 'dedd8a7d0624'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
|
||||
# Blockchains
|
||||
op.create_table('blockchains',
|
||||
sa.Column('id', sa.UUID(), nullable=False),
|
||||
sa.Column('name', sa.VARCHAR(length=128), nullable=False),
|
||||
sa.Column('chain_id', sa.Integer(), nullable=False),
|
||||
sa.Column('testnet', sa.Boolean(), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id', name=op.f('pk_blockchains')),
|
||||
sa.UniqueConstraint('id', name=op.f('uq_blockchains_id'))
|
||||
)
|
||||
|
||||
op.create_index(op.f('ix_blockchains_chain_id'), 'blockchains', ['chain_id'], unique=False)
|
||||
op.create_index(op.f('ix_blockchains_name'), 'blockchains', ['name'], unique=True)
|
||||
|
||||
op.add_column('registered_contracts', sa.Column('blockchain_id', sa.UUID(), nullable=True))
|
||||
op.create_foreign_key(op.f('fk_registered_contracts_blockchain_id_blockchains'), 'registered_contracts', 'blockchains', ['blockchain_id'], ['id'], ondelete='CASCADE')
|
||||
|
||||
# Manual - Start
|
||||
op.execute(f"INSERT INTO blockchains (id, name, chain_id, testnet) VALUES ('{str(uuid.uuid4())}', 'ethereum', 1, FALSE),('{str(uuid.uuid4())}', 'polygon', 137, FALSE),('{str(uuid.uuid4())}', 'mumbai', 80001, TRUE),('{str(uuid.uuid4())}', 'wyrm', 322, FALSE),('{str(uuid.uuid4())}', 'zksync_era', 324, FALSE),('{str(uuid.uuid4())}', 'zksync_era_testnet', 280, TRUE),('{str(uuid.uuid4())}', 'gnosis', 100, FALSE);")
|
||||
op.execute("UPDATE registered_contracts SET blockchain_id = (SELECT id FROM blockchains WHERE blockchains.name = registered_contracts.blockchain);")
|
||||
op.alter_column("registered_contracts", "blockchain_id", nullable=False)
|
||||
# Manual - End
|
||||
|
||||
op.drop_constraint('uq_registered_contracts_blockchain', 'registered_contracts', type_='unique')
|
||||
op.drop_index('ix_registered_contracts_blockchain', table_name='registered_contracts')
|
||||
op.drop_column('registered_contracts', 'blockchain')
|
||||
|
||||
# Types
|
||||
op.create_table('call_request_types',
|
||||
sa.Column('name', sa.VARCHAR(length=128), nullable=False),
|
||||
sa.Column('description', sa.String(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('name', name=op.f('pk_call_request_types')),
|
||||
sa.UniqueConstraint('name', name=op.f('uq_call_request_types_name'))
|
||||
)
|
||||
|
||||
op.add_column('call_requests', sa.Column('call_request_type_name', sa.VARCHAR(length=128), nullable=True))
|
||||
op.create_foreign_key(op.f('fk_call_requests_call_request_type_name_call_request_types'), 'call_requests', 'call_request_types', ['call_request_type_name'], ['name'], ondelete='CASCADE')
|
||||
|
||||
# Manual - Start
|
||||
op.execute(f"INSERT INTO call_request_types (name, description) VALUES ('raw','A generic smart contract. You can ask users to submit arbitrary calldata to this contract.'),('dropper-v0.2.0','A Dropper v0.2.0 contract. You can authorize users to submit claims against this contract.');")
|
||||
op.execute("UPDATE call_requests SET call_request_type_name = (SELECT call_request_types.name FROM call_request_types INNER JOIN registered_contracts ON call_requests.registered_contract_id = registered_contracts.id WHERE call_request_types.name = registered_contracts.contract_type);")
|
||||
op.alter_column("call_requests", "call_request_type_name", nullable=False)
|
||||
# Manual - End
|
||||
|
||||
op.drop_index('ix_registered_contracts_contract_type', table_name='registered_contracts')
|
||||
op.drop_column('registered_contracts', 'contract_type')
|
||||
|
||||
# Holders
|
||||
op.create_table('metatx_requesters',
|
||||
sa.Column('id', sa.UUID(), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text("TIMEZONE('utc', statement_timestamp())"), nullable=False),
|
||||
sa.PrimaryKeyConstraint('id', name=op.f('pk_metatx_requesters')),
|
||||
sa.UniqueConstraint('id', name=op.f('uq_metatx_requesters_id'))
|
||||
)
|
||||
|
||||
op.add_column('call_requests', sa.Column('metatx_requester_id', sa.UUID(), nullable=True))
|
||||
op.create_foreign_key(op.f('fk_call_requests_metatx_requester_id_metatx_requesters'), 'call_requests', 'metatx_requesters', ['metatx_requester_id'], ['id'], ondelete='CASCADE')
|
||||
op.add_column('registered_contracts', sa.Column('metatx_requester_id', sa.UUID(), nullable=True))
|
||||
op.create_foreign_key(op.f('fk_registered_contracts_metatx_requester_id_metatx_requesters'), 'registered_contracts', 'metatx_requesters', ['metatx_requester_id'], ['id'], ondelete='CASCADE')
|
||||
|
||||
# Manual - Start
|
||||
op.execute("INSERT INTO metatx_requesters (id) SELECT DISTINCT moonstream_user_id FROM registered_contracts ON CONFLICT (id) DO NOTHING;")
|
||||
op.execute("INSERT INTO metatx_requesters (id) SELECT DISTINCT moonstream_user_id FROM call_requests ON CONFLICT (id) DO NOTHING;")
|
||||
op.execute("UPDATE registered_contracts SET metatx_requester_id = moonstream_user_id;")
|
||||
op.execute("UPDATE call_requests SET metatx_requester_id = moonstream_user_id;")
|
||||
op.alter_column("call_requests", "metatx_requester_id", nullable=False)
|
||||
op.alter_column("registered_contracts", "metatx_requester_id", nullable=False)
|
||||
# Manual - End
|
||||
|
||||
op.drop_index('ix_call_requests_moonstream_user_id', table_name='call_requests')
|
||||
op.drop_column('call_requests', 'moonstream_user_id')
|
||||
op.drop_index('ix_registered_contracts_moonstream_user_id', table_name='registered_contracts')
|
||||
op.drop_column('registered_contracts', 'moonstream_user_id')
|
||||
|
||||
# Other
|
||||
op.create_unique_constraint(op.f('uq_registered_contracts_blockchain_id'), 'registered_contracts', ['blockchain_id', 'metatx_requester_id', 'address'])
|
||||
|
||||
# op.create_unique_constraint(op.f('uq_call_requests_id'), 'call_requests', ['id'])
|
||||
# op.create_unique_constraint(op.f('uq_registered_contracts_id'), 'registered_contracts', ['id'])
|
||||
# op.create_unique_constraint(op.f('uq_leaderboard_scores_id'), 'leaderboard_scores', ['id'])
|
||||
# op.create_unique_constraint(op.f('uq_leaderboards_id'), 'leaderboards', ['id'])
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
|
||||
# Blockchains
|
||||
op.add_column('registered_contracts', sa.Column('blockchain', sa.VARCHAR(length=128), autoincrement=False, nullable=True))
|
||||
op.create_index('ix_registered_contracts_blockchain', 'registered_contracts', ['blockchain'], unique=False)
|
||||
|
||||
# Manual - Start
|
||||
op.execute("UPDATE registered_contracts SET blockchain = (SELECT blockchains.name FROM blockchains WHERE blockchains.id = registered_contracts.blockchain_id);")
|
||||
op.alter_column("registered_contracts", "blockchain", nullable=False)
|
||||
# Manual - End
|
||||
|
||||
op.drop_constraint(op.f('fk_registered_contracts_blockchain_id_blockchains'), 'registered_contracts', type_='foreignkey')
|
||||
op.drop_constraint(op.f('uq_registered_contracts_blockchain_id'), 'registered_contracts', type_='unique')
|
||||
op.drop_column('registered_contracts', 'blockchain_id')
|
||||
|
||||
op.drop_index(op.f('ix_blockchains_name'), table_name='blockchains')
|
||||
op.drop_index(op.f('ix_blockchains_chain_id'), table_name='blockchains')
|
||||
op.drop_table('blockchains')
|
||||
|
||||
# Types
|
||||
op.add_column('registered_contracts', sa.Column('contract_type', sa.VARCHAR(length=128), autoincrement=False, nullable=True))
|
||||
op.create_index('ix_registered_contracts_contract_type', 'registered_contracts', ['contract_type'], unique=False)
|
||||
|
||||
# Manual - Start
|
||||
# Hardcoded to set `dropper-v0.2.0`
|
||||
op.execute("UPDATE registered_contracts SET contract_type = 'dropper-v0.2.0';")
|
||||
op.alter_column("registered_contracts", "contract_type", nullable=False)
|
||||
# Manual - End
|
||||
|
||||
op.drop_constraint(op.f('fk_call_requests_call_request_type_id_call_request_types'), 'call_requests', type_='foreignkey')
|
||||
op.drop_column('call_requests', 'call_request_type_id')
|
||||
|
||||
op.drop_index(op.f('ix_call_request_types_request_type'), table_name='call_request_types')
|
||||
op.drop_table('call_request_types')
|
||||
|
||||
# Holders
|
||||
op.add_column('registered_contracts', sa.Column('moonstream_user_id', sa.UUID(), autoincrement=False, nullable=True))
|
||||
op.create_index('ix_registered_contracts_moonstream_user_id', 'registered_contracts', ['moonstream_user_id'], unique=False)
|
||||
op.add_column('call_requests', sa.Column('moonstream_user_id', sa.UUID(), autoincrement=False, nullable=True))
|
||||
op.create_index('ix_call_requests_moonstream_user_id', 'call_requests', ['moonstream_user_id'], unique=False)
|
||||
|
||||
# Manual - Start
|
||||
op.execute("UPDATE registered_contracts SET moonstream_user_id = metatx_requester_id;")
|
||||
op.execute("UPDATE call_requests SET moonstream_user_id = metatx_requester_id;")
|
||||
op.alter_column("registered_contracts", "moonstream_user_id", nullable=False)
|
||||
op.alter_column("call_requests", "moonstream_user_id", nullable=False)
|
||||
# Manual - End
|
||||
|
||||
op.drop_constraint(op.f('fk_registered_contracts_metatx_requester_id_metatx_requesters'), 'registered_contracts', type_='foreignkey')
|
||||
op.drop_column('registered_contracts', 'metatx_requester_id')
|
||||
op.drop_constraint(op.f('fk_call_requests_metatx_requester_id_metatx_requesters'), 'call_requests', type_='foreignkey')
|
||||
op.drop_column('call_requests', 'metatx_requester_id')
|
||||
|
||||
op.drop_table('metatx_requesters')
|
||||
|
||||
# Other
|
||||
op.create_unique_constraint('uq_registered_contracts_blockchain', 'registered_contracts', ['blockchain', 'moonstream_user_id', 'address', 'contract_type'])
|
||||
|
||||
# ### end Alembic commands ###
|
|
@ -1,6 +1,6 @@
|
|||
from datetime import datetime
|
||||
from collections import Counter
|
||||
from typing import List, Any, Optional, Dict, Union
|
||||
from typing import List, Any, Optional, Dict, Union, Tuple
|
||||
import uuid
|
||||
import logging
|
||||
|
||||
|
@ -11,6 +11,7 @@ import requests # type: ignore
|
|||
from sqlalchemy.dialects.postgresql import insert
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func, text, or_
|
||||
from sqlalchemy.engine import Row
|
||||
from web3 import Web3
|
||||
from web3.types import ChecksumAddress
|
||||
|
||||
|
@ -64,6 +65,18 @@ class LeaderboardDeleteScoresError(Exception):
|
|||
pass
|
||||
|
||||
|
||||
class LeaderboardCreateError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class LeaderboardUpdateError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class LeaderboardDeleteError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
BATCH_SIGNATURE_PAGE_SIZE = 500
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -208,7 +221,7 @@ def delete_claim(db_session: Session, dropper_claim_id):
|
|||
"""
|
||||
|
||||
claim = (
|
||||
db_session.query(DropperClaim).filter(DropperClaim.id == dropper_claim_id).one()
|
||||
db_session.query(DropperClaim).filter(DropperClaim.id == dropper_claim_id).one() # type: ignore
|
||||
)
|
||||
|
||||
db_session.delete(claim)
|
||||
|
@ -260,7 +273,7 @@ def activate_drop(db_session: Session, dropper_claim_id: uuid.UUID):
|
|||
"""
|
||||
|
||||
claim = (
|
||||
db_session.query(DropperClaim).filter(DropperClaim.id == dropper_claim_id).one()
|
||||
db_session.query(DropperClaim).filter(DropperClaim.id == dropper_claim_id).one() # type: ignore
|
||||
)
|
||||
|
||||
claim.active = True
|
||||
|
@ -275,7 +288,7 @@ def deactivate_drop(db_session: Session, dropper_claim_id: uuid.UUID):
|
|||
"""
|
||||
|
||||
claim = (
|
||||
db_session.query(DropperClaim).filter(DropperClaim.id == dropper_claim_id).one()
|
||||
db_session.query(DropperClaim).filter(DropperClaim.id == dropper_claim_id).one() # type: ignore
|
||||
)
|
||||
|
||||
claim.active = False
|
||||
|
@ -300,7 +313,7 @@ def update_drop(
|
|||
"""
|
||||
|
||||
claim = (
|
||||
db_session.query(DropperClaim).filter(DropperClaim.id == dropper_claim_id).one()
|
||||
db_session.query(DropperClaim).filter(DropperClaim.id == dropper_claim_id).one() # type: ignore
|
||||
)
|
||||
|
||||
if title:
|
||||
|
@ -619,7 +632,7 @@ def get_drop(db_session: Session, dropper_claim_id: uuid.UUID):
|
|||
Return particular drop
|
||||
"""
|
||||
drop = (
|
||||
db_session.query(DropperClaim).filter(DropperClaim.id == dropper_claim_id).one()
|
||||
db_session.query(DropperClaim).filter(DropperClaim.id == dropper_claim_id).one() # type: ignore
|
||||
)
|
||||
return drop
|
||||
|
||||
|
@ -833,7 +846,7 @@ def refetch_drop_signatures(
|
|||
)
|
||||
.join(DropperContract, DropperClaim.dropper_contract_id == DropperContract.id)
|
||||
.filter(DropperClaim.id == dropper_claim_id)
|
||||
).one()
|
||||
).one() # type: ignore
|
||||
|
||||
if claim.claim_block_deadline is None:
|
||||
raise DropWithNotSettedBlockDeadline(
|
||||
|
@ -932,7 +945,7 @@ def refetch_drop_signatures(
|
|||
return claimant_objects
|
||||
|
||||
|
||||
def get_leaderboard_total_count(db_session: Session, leaderboard_id):
|
||||
def get_leaderboard_total_count(db_session: Session, leaderboard_id) -> int:
|
||||
"""
|
||||
Get the total number of claimants in the leaderboard
|
||||
"""
|
||||
|
@ -943,18 +956,83 @@ def get_leaderboard_total_count(db_session: Session, leaderboard_id):
|
|||
)
|
||||
|
||||
|
||||
def get_leaderboard(db_session: Session, leaderboard_id: uuid.UUID) -> Leaderboard:
|
||||
def get_leaderboard_info(
|
||||
db_session: Session, leaderboard_id: uuid.UUID
|
||||
) -> Row[Tuple[uuid.UUID, str, str, int, Optional[datetime]]]:
|
||||
"""
|
||||
Get the leaderboard from the database
|
||||
Get the leaderboard from the database with users count
|
||||
"""
|
||||
|
||||
leaderboard = (
|
||||
db_session.query(Leaderboard).filter(Leaderboard.id == leaderboard_id).one()
|
||||
db_session.query(
|
||||
Leaderboard.id,
|
||||
Leaderboard.title,
|
||||
Leaderboard.description,
|
||||
func.count(LeaderboardScores.id).label("users_count"),
|
||||
func.max(LeaderboardScores.updated_at).label("last_update"),
|
||||
)
|
||||
.join(
|
||||
LeaderboardScores,
|
||||
LeaderboardScores.leaderboard_id == Leaderboard.id,
|
||||
isouter=True,
|
||||
)
|
||||
.filter(Leaderboard.id == leaderboard_id)
|
||||
.group_by(Leaderboard.id, Leaderboard.title, Leaderboard.description)
|
||||
.one()
|
||||
)
|
||||
|
||||
return leaderboard
|
||||
|
||||
|
||||
def get_leaderboard_scores_changes(
|
||||
db_session: Session, leaderboard_id: uuid.UUID
|
||||
) -> List[Row[Tuple[int, datetime]]]:
|
||||
"""
|
||||
Return the leaderboard scores changes timeline changes of leaderboard scores
|
||||
"""
|
||||
|
||||
leaderboard_scores_changes = (
|
||||
db_session.query(
|
||||
func.count(LeaderboardScores.address).label("players_count"),
|
||||
# func.extract("epoch", LeaderboardScores.updated_at).label("timestamp"),
|
||||
LeaderboardScores.updated_at.label("date"),
|
||||
)
|
||||
.filter(LeaderboardScores.leaderboard_id == leaderboard_id)
|
||||
.group_by(LeaderboardScores.updated_at)
|
||||
.order_by(LeaderboardScores.updated_at.desc())
|
||||
).all()
|
||||
|
||||
return leaderboard_scores_changes
|
||||
|
||||
|
||||
def get_leaderboard_scores_by_timestamp(
|
||||
db_session: Session,
|
||||
leaderboard_id: uuid.UUID,
|
||||
date: datetime,
|
||||
limit: int,
|
||||
offset: int,
|
||||
) -> List[LeaderboardScores]:
|
||||
"""
|
||||
Return the leaderboard scores by timestamp
|
||||
"""
|
||||
|
||||
leaderboard_scores = (
|
||||
db_session.query(
|
||||
LeaderboardScores.leaderboard_id,
|
||||
LeaderboardScores.address,
|
||||
LeaderboardScores.score,
|
||||
LeaderboardScores.points_data,
|
||||
)
|
||||
.filter(LeaderboardScores.leaderboard_id == leaderboard_id)
|
||||
.filter(LeaderboardScores.updated_at == date)
|
||||
.order_by(LeaderboardScores.score.desc())
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
)
|
||||
|
||||
return leaderboard_scores
|
||||
|
||||
|
||||
def get_leaderboards(
|
||||
db_session: Session,
|
||||
token: Union[str, uuid.UUID],
|
||||
|
@ -987,7 +1065,7 @@ def get_leaderboards(
|
|||
|
||||
def get_position(
|
||||
db_session: Session, leaderboard_id, address, window_size, limit: int, offset: int
|
||||
):
|
||||
) -> List[Row[Tuple[str, int, int, int, Any]]]:
|
||||
"""
|
||||
|
||||
Return position by address with window size
|
||||
|
@ -1039,7 +1117,7 @@ def get_position(
|
|||
|
||||
def get_leaderboard_positions(
|
||||
db_session: Session, leaderboard_id, limit: int, offset: int
|
||||
):
|
||||
) -> List[Row[Tuple[uuid.UUID, str, int, str, int]]]:
|
||||
"""
|
||||
Get the leaderboard positions
|
||||
"""
|
||||
|
@ -1064,7 +1142,9 @@ def get_leaderboard_positions(
|
|||
return query
|
||||
|
||||
|
||||
def get_qurtiles(db_session: Session, leaderboard_id):
|
||||
def get_qurtiles(
|
||||
db_session: Session, leaderboard_id
|
||||
) -> Tuple[Row[Tuple[str, float, int]], ...]:
|
||||
"""
|
||||
Get the leaderboard qurtiles
|
||||
https://docs.sqlalchemy.org/en/14/core/functions.html#sqlalchemy.sql.functions.percentile_disc
|
||||
|
@ -1098,7 +1178,7 @@ def get_qurtiles(db_session: Session, leaderboard_id):
|
|||
return q1, q2, q3
|
||||
|
||||
|
||||
def get_ranks(db_session: Session, leaderboard_id):
|
||||
def get_ranks(db_session: Session, leaderboard_id) -> List[Row[Tuple[int, int, int]]]:
|
||||
"""
|
||||
Get the leaderboard rank buckets(rank, size, score)
|
||||
"""
|
||||
|
@ -1126,7 +1206,7 @@ def get_rank(
|
|||
rank: int,
|
||||
limit: Optional[int] = None,
|
||||
offset: Optional[int] = None,
|
||||
):
|
||||
) -> List[Row[Tuple[uuid.UUID, str, int, str, int]]]:
|
||||
"""
|
||||
Get bucket in leaderboard by rank
|
||||
"""
|
||||
|
@ -1157,33 +1237,114 @@ def get_rank(
|
|||
return positions
|
||||
|
||||
|
||||
def create_leaderboard(db_session: Session, title: str, description: str):
|
||||
def create_leaderboard(
|
||||
db_session: Session,
|
||||
title: str,
|
||||
description: Optional[str],
|
||||
token: Optional[Union[uuid.UUID, str]] = None,
|
||||
) -> Leaderboard:
|
||||
"""
|
||||
Create a leaderboard
|
||||
"""
|
||||
|
||||
leaderboard = Leaderboard(title=title, description=description)
|
||||
db_session.add(leaderboard)
|
||||
if not token:
|
||||
token = uuid.UUID(MOONSTREAM_ADMIN_ACCESS_TOKEN)
|
||||
try:
|
||||
leaderboard = Leaderboard(title=title, description=description)
|
||||
db_session.add(leaderboard)
|
||||
db_session.commit()
|
||||
|
||||
resource = create_leaderboard_resource(
|
||||
leaderboard_id=str(leaderboard.id),
|
||||
token=token,
|
||||
)
|
||||
|
||||
leaderboard.resource_id = resource.id
|
||||
|
||||
db_session.commit()
|
||||
except Exception as e:
|
||||
db_session.rollback()
|
||||
logger.error(f"Error creating leaderboard: {e}")
|
||||
raise LeaderboardCreateError(f"Error creating leaderboard: {e}")
|
||||
|
||||
return leaderboard
|
||||
|
||||
|
||||
def delete_leaderboard(
|
||||
db_session: Session, leaderboard_id: uuid.UUID, token: uuid.UUID
|
||||
) -> Leaderboard:
|
||||
"""
|
||||
Delete a leaderboard
|
||||
"""
|
||||
try:
|
||||
leaderboard = (
|
||||
db_session.query(Leaderboard).filter(Leaderboard.id == leaderboard_id).one() # type: ignore
|
||||
)
|
||||
|
||||
if leaderboard.resource_id is not None:
|
||||
try:
|
||||
bc.delete_resource(
|
||||
token=token,
|
||||
resource_id=leaderboard.resource_id,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting leaderboard resource: {e}")
|
||||
else:
|
||||
logger.error(
|
||||
f"Leaderboard {leaderboard_id} has no resource id. Skipping. Better delete it manually."
|
||||
)
|
||||
|
||||
db_session.delete(leaderboard)
|
||||
db_session.commit()
|
||||
except Exception as e:
|
||||
db_session.rollback()
|
||||
logger.error(e)
|
||||
raise LeaderboardDeleteError(f"Error deleting leaderboard: {e}")
|
||||
|
||||
return leaderboard
|
||||
|
||||
|
||||
def update_leaderboard(
|
||||
db_session: Session,
|
||||
leaderboard_id: uuid.UUID,
|
||||
title: Optional[str],
|
||||
description: Optional[str],
|
||||
) -> Leaderboard:
|
||||
"""
|
||||
Update a leaderboard
|
||||
"""
|
||||
|
||||
leaderboard = (
|
||||
db_session.query(Leaderboard).filter(Leaderboard.id == leaderboard_id).one() # type: ignore
|
||||
)
|
||||
|
||||
if title is not None:
|
||||
leaderboard.title = title
|
||||
if description is not None:
|
||||
leaderboard.description = description
|
||||
|
||||
db_session.commit()
|
||||
|
||||
return leaderboard.id
|
||||
return leaderboard
|
||||
|
||||
|
||||
def get_leaderboard_by_id(db_session: Session, leaderboard_id):
|
||||
def get_leaderboard_by_id(db_session: Session, leaderboard_id) -> Leaderboard:
|
||||
"""
|
||||
Get the leaderboard by id
|
||||
"""
|
||||
return db_session.query(Leaderboard).filter(Leaderboard.id == leaderboard_id).one()
|
||||
return db_session.query(Leaderboard).filter(Leaderboard.id == leaderboard_id).one() # type: ignore
|
||||
|
||||
|
||||
def get_leaderboard_by_title(db_session: Session, title):
|
||||
def get_leaderboard_by_title(db_session: Session, title) -> Leaderboard:
|
||||
"""
|
||||
Get the leaderboard by title
|
||||
"""
|
||||
return db_session.query(Leaderboard).filter(Leaderboard.title == title).one()
|
||||
return db_session.query(Leaderboard).filter(Leaderboard.title == title).one() # type: ignore
|
||||
|
||||
|
||||
def list_leaderboards(db_session: Session, limit: int, offset: int):
|
||||
def list_leaderboards(
|
||||
db_session: Session, limit: int, offset: int
|
||||
) -> List[Row[Tuple[uuid.UUID, str, str]]]:
|
||||
"""
|
||||
List all leaderboards
|
||||
"""
|
||||
|
@ -1265,8 +1426,7 @@ def add_scores(
|
|||
|
||||
|
||||
def create_leaderboard_resource(
|
||||
leaderboard_id: uuid.UUID,
|
||||
token: Optional[uuid.UUID] = None,
|
||||
leaderboard_id: str, token: Union[Optional[uuid.UUID], str] = None
|
||||
) -> BugoutResource:
|
||||
resource_data: Dict[str, Any] = {
|
||||
"type": LEADERBOARD_RESOURCE_TYPE,
|
||||
|
@ -1275,19 +1435,22 @@ def create_leaderboard_resource(
|
|||
|
||||
if token is None:
|
||||
token = MOONSTREAM_ADMIN_ACCESS_TOKEN
|
||||
|
||||
resource = bc.create_resource(
|
||||
token=MOONSTREAM_ADMIN_ACCESS_TOKEN,
|
||||
application_id=MOONSTREAM_APPLICATION_ID,
|
||||
resource_data=resource_data,
|
||||
timeout=10,
|
||||
)
|
||||
try:
|
||||
resource = bc.create_resource(
|
||||
token=token,
|
||||
application_id=MOONSTREAM_APPLICATION_ID,
|
||||
resource_data=resource_data,
|
||||
timeout=10,
|
||||
)
|
||||
except Exception as e:
|
||||
raise LeaderboardCreateError(f"Error creating leaderboard resource: {e}")
|
||||
return resource
|
||||
|
||||
|
||||
def assign_resource(
|
||||
db_session: Session,
|
||||
leaderboard_id: uuid.UUID,
|
||||
user_token: Union[uuid.UUID, str],
|
||||
resource_id: Optional[uuid.UUID] = None,
|
||||
):
|
||||
"""
|
||||
|
@ -1295,19 +1458,17 @@ def assign_resource(
|
|||
"""
|
||||
|
||||
leaderboard = (
|
||||
db_session.query(Leaderboard).filter(Leaderboard.id == leaderboard_id).one()
|
||||
db_session.query(Leaderboard).filter(Leaderboard.id == leaderboard_id).one() # type: ignore
|
||||
)
|
||||
|
||||
if leaderboard.resource_id is not None:
|
||||
raise Exception("Leaderboard already has a resource")
|
||||
|
||||
if resource_id is not None:
|
||||
leaderboard.resource_id = resource_id
|
||||
else:
|
||||
# Create resource via admin token
|
||||
|
||||
resource = create_leaderboard_resource(
|
||||
leaderboard_id=leaderboard_id,
|
||||
leaderboard_id=str(leaderboard_id),
|
||||
token=user_token,
|
||||
)
|
||||
|
||||
leaderboard.resource_id = resource.id
|
||||
|
@ -1330,7 +1491,9 @@ def list_leaderboards_resources(
|
|||
return query.all()
|
||||
|
||||
|
||||
def revoke_resource(db_session: Session, leaderboard_id: uuid.UUID):
|
||||
def revoke_resource(
|
||||
db_session: Session, leaderboard_id: uuid.UUID
|
||||
) -> Optional[uuid.UUID]:
|
||||
"""
|
||||
Revoke a resource handler to a leaderboard
|
||||
"""
|
||||
|
@ -1338,7 +1501,7 @@ def revoke_resource(db_session: Session, leaderboard_id: uuid.UUID):
|
|||
# TODO(ANDREY): Delete resource via admin token
|
||||
|
||||
leaderboard = (
|
||||
db_session.query(Leaderboard).filter(Leaderboard.id == leaderboard_id).one()
|
||||
db_session.query(Leaderboard).filter(Leaderboard.id == leaderboard_id).one() # type: ignore
|
||||
)
|
||||
|
||||
if leaderboard.resource_id is None:
|
||||
|
@ -1354,12 +1517,12 @@ def revoke_resource(db_session: Session, leaderboard_id: uuid.UUID):
|
|||
|
||||
def check_leaderboard_resource_permissions(
|
||||
db_session: Session, leaderboard_id: uuid.UUID, token: uuid.UUID
|
||||
):
|
||||
) -> bool:
|
||||
"""
|
||||
Check if the user has permissions to access the leaderboard
|
||||
"""
|
||||
leaderboard = (
|
||||
db_session.query(Leaderboard).filter(Leaderboard.id == leaderboard_id).one()
|
||||
db_session.query(Leaderboard).filter(Leaderboard.id == leaderboard_id).one() # type: ignore
|
||||
)
|
||||
|
||||
permission_url = f"{bc.brood_url}/resources/{leaderboard.resource_id}/holders"
|
||||
|
|
|
@ -3,16 +3,23 @@ import json
|
|||
import logging
|
||||
import uuid
|
||||
from datetime import timedelta
|
||||
from typing import Any, Dict, List, Optional
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from sqlalchemy import func, text
|
||||
from sqlalchemy.dialects.postgresql import insert
|
||||
from sqlalchemy.engine import Row
|
||||
from sqlalchemy.exc import IntegrityError, NoResultFound
|
||||
from sqlalchemy.orm import Session
|
||||
from web3 import Web3
|
||||
|
||||
from . import data, db
|
||||
from .data import ContractType
|
||||
from .models import CallRequest, RegisteredContract
|
||||
from .models import (
|
||||
Blockchain,
|
||||
CallRequest,
|
||||
CallRequestType,
|
||||
MetatxRequester,
|
||||
RegisteredContract,
|
||||
)
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -30,66 +37,151 @@ class InvalidAddressFormat(Exception):
|
|||
"""
|
||||
|
||||
|
||||
class UnsupportedCallRequestType(Exception):
|
||||
"""
|
||||
Raised when unsupported call request type specified.
|
||||
"""
|
||||
|
||||
|
||||
class UnsupportedBlockchain(Exception):
|
||||
"""
|
||||
Raised when unsupported blockchain specified.
|
||||
"""
|
||||
|
||||
|
||||
class CallRequestMethodValueError(Exception):
|
||||
"""
|
||||
Raised when method not acceptable for specified request type.
|
||||
"""
|
||||
|
||||
|
||||
class CallRequestRequiredParamsValueError(Exception):
|
||||
"""
|
||||
Raised when required params not acceptable for specified request type.
|
||||
"""
|
||||
|
||||
|
||||
class ContractAlreadyRegistered(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class CallRequestAlreadyRegistered(Exception):
|
||||
"""
|
||||
Raised when call request with same parameters registered.
|
||||
"""
|
||||
|
||||
|
||||
def parse_registered_contract_response(
|
||||
obj: Tuple[RegisteredContract, Blockchain]
|
||||
) -> data.RegisteredContractResponse:
|
||||
return data.RegisteredContractResponse(
|
||||
id=obj[0].id,
|
||||
blockchain=obj[1].name,
|
||||
address=obj[0].address,
|
||||
metatx_requester_id=obj[0].metatx_requester_id,
|
||||
title=obj[0].title,
|
||||
description=obj[0].description,
|
||||
image_uri=obj[0].image_uri,
|
||||
created_at=obj[0].created_at,
|
||||
updated_at=obj[0].updated_at,
|
||||
)
|
||||
|
||||
|
||||
def parse_call_request_response(
|
||||
obj: Tuple[CallRequest, RegisteredContract]
|
||||
) -> data.CallRequestResponse:
|
||||
return data.CallRequestResponse(
|
||||
id=obj[0].id,
|
||||
contract_id=obj[0].registered_contract_id,
|
||||
contract_address=obj[1].address,
|
||||
metatx_requester_id=obj[0].metatx_requester_id,
|
||||
call_request_type=obj[0].call_request_type_name,
|
||||
caller=obj[0].caller,
|
||||
method=obj[0].method,
|
||||
request_id=str(obj[0].request_id),
|
||||
parameters=obj[0].parameters,
|
||||
expires_at=obj[0].expires_at,
|
||||
created_at=obj[0].created_at,
|
||||
updated_at=obj[0].updated_at,
|
||||
)
|
||||
|
||||
|
||||
def validate_method_and_params(
|
||||
contract_type: ContractType, method: str, parameters: Dict[str, Any]
|
||||
) -> None:
|
||||
call_request_type: str, method: str, parameters: Dict[str, Any]
|
||||
) -> str:
|
||||
"""
|
||||
Validate the given method and parameters for the specified contract_type.
|
||||
"""
|
||||
if contract_type == ContractType.raw:
|
||||
if method != "":
|
||||
raise ValueError("Method must be empty string for raw contract type")
|
||||
if set(parameters.keys()) != {"calldata"}:
|
||||
raise ValueError(
|
||||
"Parameters must have only 'calldata' key for raw contract type"
|
||||
)
|
||||
elif contract_type == ContractType.dropper:
|
||||
if call_request_type == "dropper-v0.2.0":
|
||||
if method != "claim":
|
||||
raise ValueError("Method must be 'claim' for dropper contract type")
|
||||
raise CallRequestMethodValueError(
|
||||
"Method must be 'claim' for dropper contract type"
|
||||
)
|
||||
required_params = {
|
||||
"dropId",
|
||||
"requestID",
|
||||
"blockDeadline",
|
||||
"amount",
|
||||
"signer",
|
||||
"signature",
|
||||
}
|
||||
if set(parameters.keys()) != required_params:
|
||||
raise ValueError(
|
||||
raise CallRequestRequiredParamsValueError(
|
||||
f"Parameters must have {required_params} keys for dropper contract type"
|
||||
)
|
||||
try:
|
||||
Web3.toChecksumAddress(parameters["signer"])
|
||||
except:
|
||||
raise InvalidAddressFormat("Parameter signer must be a valid address")
|
||||
required_params["amount"] = str(required_params["amount"])
|
||||
|
||||
elif call_request_type == "raw":
|
||||
if method != "":
|
||||
raise CallRequestMethodValueError(
|
||||
"Method must be empty string for raw contract type"
|
||||
)
|
||||
if set(parameters.keys()) != {"calldata"}:
|
||||
raise CallRequestRequiredParamsValueError(
|
||||
"Parameters must have only 'calldata' key for raw contract type"
|
||||
)
|
||||
|
||||
else:
|
||||
raise ValueError(f"Unknown contract type {contract_type}")
|
||||
raise UnsupportedCallRequestType(f"Unknown contract type {call_request_type}")
|
||||
|
||||
return call_request_type
|
||||
|
||||
|
||||
def register_contract(
|
||||
db_session: Session,
|
||||
blockchain: str,
|
||||
blockchain_name: str,
|
||||
address: str,
|
||||
contract_type: ContractType,
|
||||
moonstream_user_id: uuid.UUID,
|
||||
metatx_requester_id: uuid.UUID,
|
||||
title: Optional[str],
|
||||
description: Optional[str],
|
||||
image_uri: Optional[str],
|
||||
) -> data.RegisteredContract:
|
||||
) -> Tuple[RegisteredContract, Blockchain]:
|
||||
"""
|
||||
Register a contract against the Engine instance
|
||||
"""
|
||||
try:
|
||||
blockchain = (
|
||||
db_session.query(Blockchain)
|
||||
.filter(Blockchain.name == blockchain_name)
|
||||
.one_or_none()
|
||||
)
|
||||
if blockchain is None:
|
||||
raise UnsupportedBlockchain("Unsupported blockchain specified")
|
||||
|
||||
metatx_requester_stmt = insert(MetatxRequester.__table__).values(
|
||||
id=metatx_requester_id
|
||||
)
|
||||
metatx_requester_stmt_do_nothing_stmt = (
|
||||
metatx_requester_stmt.on_conflict_do_nothing()
|
||||
)
|
||||
db_session.execute(metatx_requester_stmt_do_nothing_stmt)
|
||||
|
||||
contract = RegisteredContract(
|
||||
blockchain=blockchain,
|
||||
blockchain_id=blockchain.id,
|
||||
address=Web3.toChecksumAddress(address),
|
||||
contract_type=contract_type.value,
|
||||
moonstream_user_id=moonstream_user_id,
|
||||
metatx_requester_id=metatx_requester_id,
|
||||
title=title,
|
||||
description=description,
|
||||
image_uri=image_uri,
|
||||
|
@ -104,38 +196,42 @@ def register_contract(
|
|||
logger.error(repr(err))
|
||||
raise
|
||||
|
||||
return contract
|
||||
return (contract, blockchain)
|
||||
|
||||
|
||||
def update_registered_contract(
|
||||
db_session: Session,
|
||||
moonstream_user_id: uuid.UUID,
|
||||
metatx_requester_id: uuid.UUID,
|
||||
contract_id: uuid.UUID,
|
||||
title: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
image_uri: Optional[str] = None,
|
||||
ignore_nulls: bool = True,
|
||||
) -> data.RegisteredContract:
|
||||
) -> Tuple[RegisteredContract, Blockchain]:
|
||||
"""
|
||||
Update the registered contract with the given contract ID provided that the user with moonstream_user_id
|
||||
Update the registered contract with the given contract ID provided that the user with metatx_requester_id
|
||||
has access to it.
|
||||
"""
|
||||
query = db_session.query(RegisteredContract).filter(
|
||||
RegisteredContract.id == contract_id,
|
||||
RegisteredContract.moonstream_user_id == moonstream_user_id,
|
||||
contract_with_blockchain = (
|
||||
db_session.query(RegisteredContract, Blockchain)
|
||||
.join(Blockchain, Blockchain.id == RegisteredContract.blockchain_id)
|
||||
.filter(
|
||||
RegisteredContract.id == contract_id,
|
||||
RegisteredContract.metatx_requester_id == metatx_requester_id,
|
||||
)
|
||||
.one()
|
||||
)
|
||||
|
||||
contract = query.one()
|
||||
registered_contract, blockchain = contract_with_blockchain
|
||||
|
||||
if not (title is None and ignore_nulls):
|
||||
contract.title = title
|
||||
registered_contract.title = title
|
||||
if not (description is None and ignore_nulls):
|
||||
contract.description = description
|
||||
registered_contract.description = description
|
||||
if not (image_uri is None and ignore_nulls):
|
||||
contract.image_uri = image_uri
|
||||
registered_contract.image_uri = image_uri
|
||||
|
||||
try:
|
||||
db_session.add(contract)
|
||||
db_session.add(registered_contract)
|
||||
db_session.commit()
|
||||
except Exception as err:
|
||||
logger.error(
|
||||
|
@ -144,90 +240,95 @@ def update_registered_contract(
|
|||
db_session.rollback()
|
||||
raise
|
||||
|
||||
return contract
|
||||
return (registered_contract, blockchain)
|
||||
|
||||
|
||||
def get_registered_contract(
|
||||
db_session: Session,
|
||||
moonstream_user_id: uuid.UUID,
|
||||
metatx_requester_id: uuid.UUID,
|
||||
contract_id: uuid.UUID,
|
||||
) -> RegisteredContract:
|
||||
) -> Tuple[RegisteredContract, Blockchain]:
|
||||
"""
|
||||
Get registered contract by ID.
|
||||
"""
|
||||
contract = (
|
||||
db_session.query(RegisteredContract)
|
||||
.filter(RegisteredContract.moonstream_user_id == moonstream_user_id)
|
||||
contract_with_blockchain = (
|
||||
db_session.query(RegisteredContract, Blockchain)
|
||||
.join(Blockchain, Blockchain.id == RegisteredContract.blockchain_id)
|
||||
.filter(RegisteredContract.metatx_requester_id == metatx_requester_id)
|
||||
.filter(RegisteredContract.id == contract_id)
|
||||
.one()
|
||||
)
|
||||
return contract
|
||||
registered_contract, blockchain = contract_with_blockchain
|
||||
|
||||
return (registered_contract, blockchain)
|
||||
|
||||
|
||||
def lookup_registered_contracts(
|
||||
db_session: Session,
|
||||
moonstream_user_id: uuid.UUID,
|
||||
metatx_requester_id: uuid.UUID,
|
||||
blockchain: Optional[str] = None,
|
||||
address: Optional[str] = None,
|
||||
contract_type: Optional[ContractType] = None,
|
||||
limit: int = 10,
|
||||
offset: Optional[int] = None,
|
||||
) -> List[RegisteredContract]:
|
||||
) -> List[Row[Tuple[RegisteredContract, Blockchain]]]:
|
||||
"""
|
||||
Lookup a registered contract
|
||||
"""
|
||||
query = db_session.query(RegisteredContract).filter(
|
||||
RegisteredContract.moonstream_user_id == moonstream_user_id
|
||||
query = (
|
||||
db_session.query(RegisteredContract, Blockchain)
|
||||
.join(Blockchain, Blockchain.id == RegisteredContract.blockchain_id)
|
||||
.filter(RegisteredContract.metatx_requester_id == metatx_requester_id)
|
||||
)
|
||||
|
||||
if blockchain is not None:
|
||||
query = query.filter(RegisteredContract.blockchain == blockchain)
|
||||
query = query.filter(Blockchain.name == blockchain)
|
||||
|
||||
if address is not None:
|
||||
query = query.filter(
|
||||
RegisteredContract.address == Web3.toChecksumAddress(address)
|
||||
)
|
||||
|
||||
if contract_type is not None:
|
||||
query = query.filter(RegisteredContract.contract_type == contract_type.value)
|
||||
|
||||
if offset is not None:
|
||||
query = query.offset(offset)
|
||||
|
||||
query = query.limit(limit)
|
||||
contracts_with_blockchain = query.limit(limit).all()
|
||||
|
||||
return query.all()
|
||||
return contracts_with_blockchain
|
||||
|
||||
|
||||
def delete_registered_contract(
|
||||
db_session: Session,
|
||||
moonstream_user_id: uuid.UUID,
|
||||
metatx_requester_id: uuid.UUID,
|
||||
registered_contract_id: uuid.UUID,
|
||||
) -> RegisteredContract:
|
||||
) -> Tuple[RegisteredContract, Blockchain]:
|
||||
"""
|
||||
Delete a registered contract
|
||||
"""
|
||||
try:
|
||||
registered_contract = (
|
||||
db_session.query(RegisteredContract)
|
||||
.filter(RegisteredContract.moonstream_user_id == moonstream_user_id)
|
||||
contract_with_blockchain = (
|
||||
db_session.query(RegisteredContract, Blockchain)
|
||||
.join(Blockchain, Blockchain.id == RegisteredContract.blockchain_id)
|
||||
.filter(RegisteredContract.metatx_requester_id == metatx_requester_id)
|
||||
.filter(RegisteredContract.id == registered_contract_id)
|
||||
.one()
|
||||
)
|
||||
contract = contract_with_blockchain[0]
|
||||
|
||||
db_session.delete(registered_contract)
|
||||
db_session.delete(contract)
|
||||
db_session.commit()
|
||||
except Exception as err:
|
||||
db_session.rollback()
|
||||
logger.error(repr(err))
|
||||
raise
|
||||
|
||||
return registered_contract
|
||||
registered_contract, blockchain = contract_with_blockchain
|
||||
|
||||
return (registered_contract, blockchain)
|
||||
|
||||
|
||||
def request_calls(
|
||||
db_session: Session,
|
||||
moonstream_user_id: uuid.UUID,
|
||||
metatx_requester_id: uuid.UUID,
|
||||
registered_contract_id: Optional[uuid.UUID],
|
||||
contract_address: Optional[str],
|
||||
call_specs: List[data.CallSpecification],
|
||||
|
@ -251,7 +352,7 @@ def request_calls(
|
|||
|
||||
# Check that the moonstream_user_id matches a RegisteredContract with the given id or address
|
||||
query = db_session.query(RegisteredContract).filter(
|
||||
RegisteredContract.moonstream_user_id == moonstream_user_id
|
||||
RegisteredContract.metatx_requester_id == metatx_requester_id
|
||||
)
|
||||
|
||||
if registered_contract_id is not None:
|
||||
|
@ -265,24 +366,32 @@ def request_calls(
|
|||
try:
|
||||
registered_contract = query.one()
|
||||
except NoResultFound:
|
||||
raise ValueError("Invalid registered_contract_id or moonstream_user_id")
|
||||
raise ValueError("Invalid registered_contract_id or metatx_requester_id")
|
||||
|
||||
# Normalize the caller argument using Web3.toChecksumAddress
|
||||
contract_type = ContractType(registered_contract.contract_type)
|
||||
for specification in call_specs:
|
||||
normalized_caller = Web3.toChecksumAddress(specification.caller)
|
||||
|
||||
# Validate the method and parameters for the contract_type
|
||||
try:
|
||||
validate_method_and_params(
|
||||
contract_type, specification.method, specification.parameters
|
||||
call_request_type = validate_method_and_params(
|
||||
call_request_type=specification.call_request_type,
|
||||
method=specification.method,
|
||||
parameters=specification.parameters,
|
||||
)
|
||||
except UnsupportedCallRequestType as err:
|
||||
raise UnsupportedCallRequestType(err)
|
||||
except CallRequestMethodValueError as err:
|
||||
raise CallRequestMethodValueError(err)
|
||||
except CallRequestRequiredParamsValueError as err:
|
||||
raise CallRequestRequiredParamsValueError(err)
|
||||
except InvalidAddressFormat as err:
|
||||
raise InvalidAddressFormat(err)
|
||||
except Exception as err:
|
||||
logger.error(
|
||||
f"Unhandled error occurred during methods and parameters validation, err: {err}"
|
||||
)
|
||||
raise Exception()
|
||||
|
||||
expires_at = None
|
||||
if ttl_days is not None:
|
||||
|
@ -290,9 +399,11 @@ def request_calls(
|
|||
|
||||
request = CallRequest(
|
||||
registered_contract_id=registered_contract.id,
|
||||
call_request_type_name=call_request_type,
|
||||
metatx_requester_id=metatx_requester_id,
|
||||
caller=normalized_caller,
|
||||
moonstream_user_id=moonstream_user_id,
|
||||
method=specification.method,
|
||||
request_id=specification.request_id,
|
||||
parameters=specification.parameters,
|
||||
expires_at=expires_at,
|
||||
)
|
||||
|
@ -301,6 +412,9 @@ def request_calls(
|
|||
# Insert the new rows into the database in a single transaction
|
||||
try:
|
||||
db_session.commit()
|
||||
except IntegrityError as err:
|
||||
db_session.rollback()
|
||||
raise CallRequestAlreadyRegistered()
|
||||
except Exception as e:
|
||||
db_session.rollback()
|
||||
raise e
|
||||
|
@ -311,7 +425,7 @@ def request_calls(
|
|||
def get_call_requests(
|
||||
db_session: Session,
|
||||
request_id: uuid.UUID,
|
||||
) -> data.CallRequest:
|
||||
) -> Tuple[CallRequest, RegisteredContract]:
|
||||
"""
|
||||
Get call request by ID.
|
||||
"""
|
||||
|
@ -328,11 +442,26 @@ def get_call_requests(
|
|||
raise CallRequestNotFound("Call request with given ID not found")
|
||||
elif len(results) != 1:
|
||||
raise Exception(
|
||||
f"Incorrect number of results found for moonstream_user_id {moonstream_user_id} and request_id {request_id}"
|
||||
f"Incorrect number of results found for request_id {request_id}"
|
||||
)
|
||||
return data.CallRequest(
|
||||
contract_address=results[0][1].address, **results[0][0].__dict__
|
||||
)
|
||||
|
||||
call_request, registered_contract = results[0]
|
||||
|
||||
return (call_request, registered_contract)
|
||||
|
||||
|
||||
def list_blockchains(
|
||||
db_session: Session,
|
||||
) -> List[Blockchain]:
|
||||
blockchains = db_session.query(Blockchain).all()
|
||||
return blockchains
|
||||
|
||||
|
||||
def list_call_request_types(
|
||||
db_session: Session,
|
||||
) -> List[CallRequestType]:
|
||||
call_request_types = db_session.query(CallRequestType).all()
|
||||
return call_request_types
|
||||
|
||||
|
||||
def list_call_requests(
|
||||
|
@ -343,7 +472,7 @@ def list_call_requests(
|
|||
limit: int = 10,
|
||||
offset: Optional[int] = None,
|
||||
show_expired: bool = False,
|
||||
) -> List[data.CallRequest]:
|
||||
) -> List[Row[Tuple[CallRequest, RegisteredContract, CallRequestType]]]:
|
||||
"""
|
||||
List call requests for the given moonstream_user_id
|
||||
"""
|
||||
|
@ -383,12 +512,7 @@ def list_call_requests(
|
|||
|
||||
query = query.limit(limit)
|
||||
results = query.all()
|
||||
return [
|
||||
data.CallRequest(
|
||||
contract_address=registered_contract.address, **call_request.__dict__
|
||||
)
|
||||
for call_request, registered_contract in results
|
||||
]
|
||||
return results
|
||||
|
||||
|
||||
# TODO(zomglings): What should the delete functionality for call requests look like?
|
||||
|
@ -403,7 +527,7 @@ def list_call_requests(
|
|||
|
||||
def delete_requests(
|
||||
db_session: Session,
|
||||
moonstream_user_id: uuid.UUID,
|
||||
metatx_requester_id: uuid.UUID,
|
||||
request_ids: List[uuid.UUID] = [],
|
||||
) -> int:
|
||||
"""
|
||||
|
@ -412,7 +536,7 @@ def delete_requests(
|
|||
try:
|
||||
requests_to_delete_query = (
|
||||
db_session.query(CallRequest)
|
||||
.filter(CallRequest.moonstream_user_id == moonstream_user_id)
|
||||
.filter(CallRequest.metatx_requester_id == metatx_requester_id)
|
||||
.filter(CallRequest.id.in_(request_ids))
|
||||
)
|
||||
requests_to_delete_num: int = requests_to_delete_query.delete(
|
||||
|
@ -573,8 +697,6 @@ def generate_cli() -> argparse.ArgumentParser:
|
|||
register_parser.add_argument(
|
||||
"-c",
|
||||
"--contract-type",
|
||||
type=ContractType,
|
||||
choices=ContractType,
|
||||
required=True,
|
||||
help="The type of the contract",
|
||||
)
|
||||
|
@ -634,8 +756,6 @@ def generate_cli() -> argparse.ArgumentParser:
|
|||
list_contracts_parser.add_argument(
|
||||
"-c",
|
||||
"--contract-type",
|
||||
type=ContractType,
|
||||
choices=ContractType,
|
||||
required=False,
|
||||
default=None,
|
||||
help="The type of the contract",
|
||||
|
|
|
@ -203,15 +203,35 @@ class DropUpdatedResponse(BaseModel):
|
|||
active: bool = True
|
||||
|
||||
|
||||
class ContractType(Enum):
|
||||
raw = "raw"
|
||||
dropper = "dropper-v0.2.0"
|
||||
class CallRequestTypeResponse(BaseModel):
|
||||
name: str
|
||||
description: str
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class CallRequestTypesResponse(BaseModel):
|
||||
call_request_types: List[CallRequestTypeResponse] = Field(default_factory=list)
|
||||
|
||||
|
||||
class BlockchainResponse(BaseModel):
|
||||
id: UUID
|
||||
name: str
|
||||
chain_id: int
|
||||
testnet: bool
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class BlockchainsResponse(BaseModel):
|
||||
blockchains: List[BlockchainResponse] = Field(default_factory=list)
|
||||
|
||||
|
||||
class RegisterContractRequest(BaseModel):
|
||||
blockchain: str
|
||||
address: str
|
||||
contract_type: ContractType
|
||||
title: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
image_uri: Optional[str] = None
|
||||
|
@ -224,19 +244,18 @@ class UpdateContractRequest(BaseModel):
|
|||
ignore_nulls: bool = True
|
||||
|
||||
|
||||
class RegisteredContract(BaseModel):
|
||||
class RegisteredContractResponse(BaseModel):
|
||||
id: UUID
|
||||
blockchain: str
|
||||
blockchain: Optional[str] = None
|
||||
address: str
|
||||
contract_type: str
|
||||
moonstream_user_id: UUID
|
||||
metatx_requester_id: UUID
|
||||
title: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
image_uri: Optional[str] = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
@validator("id", "moonstream_user_id")
|
||||
@validator("id", "metatx_requester_id")
|
||||
def validate_uuids(cls, v):
|
||||
return str(v)
|
||||
|
||||
|
@ -251,6 +270,8 @@ class RegisteredContract(BaseModel):
|
|||
class CallSpecification(BaseModel):
|
||||
caller: str
|
||||
method: str
|
||||
call_request_type: str = "dropper-v0.2.0"
|
||||
request_id: str
|
||||
parameters: Dict[str, Any]
|
||||
|
||||
@validator("caller")
|
||||
|
@ -274,22 +295,24 @@ class CreateCallRequestsAPIRequest(BaseModel):
|
|||
return values
|
||||
|
||||
|
||||
class CallRequest(BaseModel):
|
||||
class CallRequestResponse(BaseModel):
|
||||
id: UUID
|
||||
contract_id: UUID = Field(alias="registered_contract_id")
|
||||
contract_id: UUID
|
||||
contract_address: Optional[str] = None
|
||||
moonstream_user_id: UUID
|
||||
metatx_requester_id: UUID
|
||||
call_request_type: Optional[str] = None
|
||||
caller: str
|
||||
method: str
|
||||
request_id: str
|
||||
parameters: Dict[str, Any]
|
||||
expires_at: Optional[datetime]
|
||||
expires_at: Optional[datetime] = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
@validator("id", "contract_id", "moonstream_user_id")
|
||||
@validator("id", "contract_id", "metatx_requester_id")
|
||||
def validate_uuids(cls, v):
|
||||
return str(v)
|
||||
|
||||
|
@ -355,3 +378,53 @@ class LeaderboardInfoResponse(BaseModel):
|
|||
id: UUID
|
||||
title: str
|
||||
description: Optional[str] = None
|
||||
users_count: int
|
||||
last_updated_at: Optional[datetime] = None
|
||||
|
||||
|
||||
class LeaderboardCreateRequest(BaseModel):
|
||||
title: str
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
class LeaderboardCreatedResponse(BaseModel):
|
||||
id: UUID
|
||||
title: str
|
||||
description: Optional[str] = None
|
||||
resource_id: Optional[UUID] = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class LeaderboardUpdatedResponse(BaseModel):
|
||||
id: UUID
|
||||
title: str
|
||||
description: Optional[str] = None
|
||||
resource_id: Optional[UUID] = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class LeaderboardUpdateRequest(BaseModel):
|
||||
title: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
class LeaderboardDeletedResponse(BaseModel):
|
||||
id: UUID
|
||||
title: str
|
||||
description: Optional[str] = None
|
||||
resource_id: Optional[UUID] = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
|
||||
class LeaderboardScoresChangesResponse(BaseModel):
|
||||
players_count: int
|
||||
date: datetime
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import uuid
|
||||
|
||||
from sqlalchemy import (
|
||||
DECIMAL,
|
||||
VARCHAR,
|
||||
BigInteger,
|
||||
Boolean,
|
||||
|
@ -8,6 +9,7 @@ from sqlalchemy import (
|
|||
DateTime,
|
||||
ForeignKey,
|
||||
Index,
|
||||
Integer,
|
||||
MetaData,
|
||||
String,
|
||||
UniqueConstraint,
|
||||
|
@ -15,6 +17,7 @@ from sqlalchemy import (
|
|||
from sqlalchemy.dialects.postgresql import JSONB, UUID
|
||||
from sqlalchemy.ext.compiler import compiles
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import and_, expression
|
||||
|
||||
"""
|
||||
|
@ -155,14 +158,78 @@ class DropperClaimant(Base): # type: ignore
|
|||
)
|
||||
|
||||
|
||||
class CallRequestType(Base): # type: ignore
|
||||
"""
|
||||
CallRequestType contains versions of call requests like:
|
||||
raw or dropper-v0.2.0.
|
||||
"""
|
||||
|
||||
__tablename__ = "call_request_types"
|
||||
|
||||
name = Column(
|
||||
VARCHAR(128),
|
||||
primary_key=True,
|
||||
unique=True,
|
||||
)
|
||||
description = Column(String, nullable=True)
|
||||
|
||||
|
||||
class MetatxRequester(Base): # type: ignore
|
||||
"""
|
||||
MetatxRequester represents id of user from bugout authorization.
|
||||
"""
|
||||
|
||||
__tablename__ = "metatx_requesters"
|
||||
|
||||
id = Column(
|
||||
UUID(as_uuid=True),
|
||||
primary_key=True,
|
||||
unique=True,
|
||||
)
|
||||
|
||||
created_at = Column(
|
||||
DateTime(timezone=True), server_default=utcnow(), nullable=False
|
||||
)
|
||||
|
||||
registered_contracts = relationship(
|
||||
"RegisteredContract",
|
||||
back_populates="metatx_requester",
|
||||
cascade="all, delete, delete-orphan",
|
||||
)
|
||||
call_requests = relationship(
|
||||
"CallRequest",
|
||||
back_populates="metatx_requester",
|
||||
cascade="all, delete, delete-orphan",
|
||||
)
|
||||
|
||||
|
||||
class Blockchain(Base): # type: ignore
|
||||
__tablename__ = "blockchains"
|
||||
|
||||
id = Column(
|
||||
UUID(as_uuid=True),
|
||||
primary_key=True,
|
||||
default=uuid.uuid4,
|
||||
unique=True,
|
||||
)
|
||||
name = Column(VARCHAR(128), nullable=False, index=True, unique=True)
|
||||
chain_id = Column(Integer, nullable=False, index=True, unique=False)
|
||||
testnet = Column(Boolean, default=False, nullable=False)
|
||||
|
||||
registered_contracts = relationship(
|
||||
"RegisteredContract",
|
||||
back_populates="blockchain",
|
||||
cascade="all, delete, delete-orphan",
|
||||
)
|
||||
|
||||
|
||||
class RegisteredContract(Base): # type: ignore
|
||||
__tablename__ = "registered_contracts"
|
||||
__table_args__ = (
|
||||
UniqueConstraint(
|
||||
"blockchain",
|
||||
"moonstream_user_id",
|
||||
"blockchain_id",
|
||||
"metatx_requester_id",
|
||||
"address",
|
||||
"contract_type",
|
||||
),
|
||||
)
|
||||
|
||||
|
@ -172,14 +239,21 @@ class RegisteredContract(Base): # type: ignore
|
|||
default=uuid.uuid4,
|
||||
unique=True,
|
||||
)
|
||||
blockchain = Column(VARCHAR(128), nullable=False, index=True)
|
||||
metatx_requester_id = Column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("metatx_requesters.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
)
|
||||
blockchain_id = Column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("blockchains.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
address = Column(VARCHAR(256), nullable=False, index=True)
|
||||
contract_type = Column(VARCHAR(128), nullable=False, index=True)
|
||||
title = Column(VARCHAR(128), nullable=False)
|
||||
description = Column(String, nullable=True)
|
||||
image_uri = Column(String, nullable=True)
|
||||
# User ID of the Moonstream user who registered this contract.
|
||||
moonstream_user_id = Column(UUID(as_uuid=True), nullable=False, index=True)
|
||||
|
||||
created_at = Column(
|
||||
DateTime(timezone=True), server_default=utcnow(), nullable=False
|
||||
|
@ -191,9 +265,26 @@ class RegisteredContract(Base): # type: ignore
|
|||
nullable=False,
|
||||
)
|
||||
|
||||
call_requests = relationship(
|
||||
"CallRequest",
|
||||
back_populates="registered_contract",
|
||||
cascade="all, delete, delete-orphan",
|
||||
)
|
||||
|
||||
metatx_requester = relationship(
|
||||
"MetatxRequester", back_populates="registered_contracts"
|
||||
)
|
||||
blockchain = relationship("Blockchain", back_populates="registered_contracts")
|
||||
|
||||
|
||||
class CallRequest(Base):
|
||||
__tablename__ = "call_requests"
|
||||
__table_args__ = (
|
||||
UniqueConstraint(
|
||||
"registered_contract_id",
|
||||
"request_id",
|
||||
),
|
||||
)
|
||||
|
||||
id = Column(
|
||||
UUID(as_uuid=True),
|
||||
|
@ -202,21 +293,25 @@ class CallRequest(Base):
|
|||
unique=True,
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
registered_contract_id = Column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("registered_contracts.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
)
|
||||
call_request_type_name = Column(
|
||||
VARCHAR(128),
|
||||
ForeignKey("call_request_types.name", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
)
|
||||
metatx_requester_id = Column(
|
||||
UUID(as_uuid=True),
|
||||
ForeignKey("metatx_requesters.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
caller = Column(VARCHAR(256), nullable=False, index=True)
|
||||
# User ID of the Moonstream user who requested this call.
|
||||
# For now, this duplicates the moonstream_user_id in the registered_contracts table. Nevertheless,
|
||||
# we keep this column here for auditing purposes. In the future, we will add a group_id column to
|
||||
# the registered_contracts table, and this column will be used to track the user from that group
|
||||
# who made each call request.
|
||||
moonstream_user_id = Column(UUID(as_uuid=True), nullable=False, index=True)
|
||||
method = Column(String, nullable=False, index=True)
|
||||
# TODO(zomglings): Should we conditional indices on parameters depending on the contract type?
|
||||
request_id = Column(DECIMAL, nullable=False, index=True)
|
||||
parameters = Column(JSONB, nullable=False)
|
||||
|
||||
expires_at = Column(DateTime(timezone=True), nullable=True, index=True)
|
||||
|
@ -231,6 +326,11 @@ class CallRequest(Base):
|
|||
nullable=False,
|
||||
)
|
||||
|
||||
registered_contract = relationship(
|
||||
"RegisteredContract", back_populates="call_requests"
|
||||
)
|
||||
metatx_requester = relationship("MetatxRequester", back_populates="call_requests")
|
||||
|
||||
|
||||
class Leaderboard(Base): # type: ignore
|
||||
__tablename__ = "leaderboards"
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
"""
|
||||
Leaderboard API.
|
||||
"""
|
||||
from datetime import datetime
|
||||
import logging
|
||||
from uuid import UUID
|
||||
|
||||
from web3 import Web3
|
||||
from fastapi import FastAPI, Request, Depends, Response
|
||||
from fastapi import FastAPI, Request, Depends, Response, Query, Path, Body, Header
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.orm.exc import NoResultFound
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
@ -25,18 +26,43 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
tags_metadata = [
|
||||
{"name": "leaderboard", "description": "Moonstream Engine leaderboard API"}
|
||||
{
|
||||
"name": "Public Endpoints",
|
||||
"description": "Endpoints under this tag can be accessed without any authentication. They are open to all and do not require any specific headers or tokens to be passed. Suitable for general access and non-sensitive operations.",
|
||||
},
|
||||
{
|
||||
"name": "Authorized Endpoints",
|
||||
"description": """
|
||||
Endpoints under this tag require authentication. To access these endpoints, a valid `moonstream token` must be included in the request header as:
|
||||
|
||||
```
|
||||
Authorization: Bearer <moonstream token>
|
||||
```
|
||||
|
||||
Failure to provide a valid token will result in unauthorized access errors. These endpoints are suitable for operations that involve sensitive data or actions that only authenticated users are allowed to perform.""",
|
||||
},
|
||||
]
|
||||
|
||||
AuthHeader = Header(
|
||||
..., description="The expected format is 'Bearer YOUR_MOONSTREAM_ACCESS_TOKEN'."
|
||||
)
|
||||
|
||||
|
||||
leaderboad_whitelist = {
|
||||
f"/leaderboard/{DOCS_TARGET_PATH}": "GET",
|
||||
"/leaderboard/openapi.json": "GET",
|
||||
"/leaderboard/info": "GET",
|
||||
"/leaderboard/scores/changes": "GET",
|
||||
"/leaderboard/quartiles": "GET",
|
||||
"/leaderboard/count/addresses": "GET",
|
||||
"/leaderboard/position": "GET",
|
||||
"/leaderboard": "GET",
|
||||
"/leaderboard/": "GET",
|
||||
"/leaderboard/rank": "GET",
|
||||
"/leaderboard/ranks": "GET",
|
||||
"/scores/changes": "GET",
|
||||
"/leaderboard/docs": "GET",
|
||||
"/leaderboard/openapi.json": "GET",
|
||||
}
|
||||
|
||||
app = FastAPI(
|
||||
|
@ -60,16 +86,21 @@ app.add_middleware(
|
|||
)
|
||||
|
||||
|
||||
@app.get("/info", response_model=data.LeaderboardInfoResponse)
|
||||
async def get_leadeboard(
|
||||
leaderboard_id: UUID,
|
||||
@app.get("", response_model=List[data.LeaderboardPosition], tags=["Public Endpoints"])
|
||||
@app.get("/", response_model=List[data.LeaderboardPosition], tags=["Public Endpoints"])
|
||||
async def leaderboard(
|
||||
leaderboard_id: UUID = Query(..., description="Leaderboard ID"),
|
||||
limit: int = Query(10),
|
||||
offset: int = Query(0),
|
||||
db_session: Session = Depends(db.yield_db_session),
|
||||
) -> data.LeaderboardInfoResponse:
|
||||
) -> List[data.LeaderboardPosition]:
|
||||
"""
|
||||
Returns leaderboard info.
|
||||
Returns the leaderboard positions.
|
||||
"""
|
||||
|
||||
### Check if leaderboard exists
|
||||
try:
|
||||
leaderboard = actions.get_leaderboard(db_session, leaderboard_id)
|
||||
actions.get_leaderboard_by_id(db_session, leaderboard_id)
|
||||
except NoResultFound as e:
|
||||
raise EngineHTTPException(
|
||||
status_code=404,
|
||||
|
@ -79,16 +110,201 @@ async def get_leadeboard(
|
|||
logger.error(f"Error while getting leaderboard: {e}")
|
||||
raise EngineHTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
return data.LeaderboardInfoResponse(
|
||||
id=leaderboard.id,
|
||||
title=leaderboard.title,
|
||||
description=leaderboard.description,
|
||||
leaderboard_positions = actions.get_leaderboard_positions(
|
||||
db_session, leaderboard_id, limit, offset
|
||||
)
|
||||
result = [
|
||||
data.LeaderboardPosition(
|
||||
address=position.address,
|
||||
score=position.score,
|
||||
rank=position.rank,
|
||||
points_data=position.points_data,
|
||||
)
|
||||
for position in leaderboard_positions
|
||||
]
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@app.post(
|
||||
"", response_model=data.LeaderboardCreatedResponse, tags=["Authorized Endpoints"]
|
||||
)
|
||||
@app.post(
|
||||
"/", response_model=data.LeaderboardCreatedResponse, tags=["Authorized Endpoints"]
|
||||
)
|
||||
async def create_leaderboard(
|
||||
request: Request,
|
||||
leaderboard: data.LeaderboardCreateRequest = Body(...),
|
||||
db_session: Session = Depends(db.yield_db_session),
|
||||
Authorization: str = AuthHeader,
|
||||
) -> data.LeaderboardCreatedResponse:
|
||||
"""
|
||||
|
||||
Create leaderboard.
|
||||
"""
|
||||
|
||||
token = request.state.token
|
||||
|
||||
try:
|
||||
created_leaderboard = actions.create_leaderboard(
|
||||
db_session,
|
||||
title=leaderboard.title,
|
||||
description=leaderboard.description,
|
||||
token=token,
|
||||
)
|
||||
except actions.LeaderboardCreateError as e:
|
||||
logger.error(f"Error while creating leaderboard: {e}")
|
||||
raise EngineHTTPException(
|
||||
status_code=500,
|
||||
detail="Leaderboard creation failed. Please try again.",
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error while creating leaderboard: {e}")
|
||||
raise EngineHTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
# Add resource to the leaderboard
|
||||
|
||||
return data.LeaderboardCreatedResponse(
|
||||
id=created_leaderboard.id, # type: ignore
|
||||
title=created_leaderboard.title, # type: ignore
|
||||
description=created_leaderboard.description, # type: ignore
|
||||
resource_id=created_leaderboard.resource_id, # type: ignore
|
||||
created_at=created_leaderboard.created_at, # type: ignore
|
||||
updated_at=created_leaderboard.updated_at, # type: ignore
|
||||
)
|
||||
|
||||
|
||||
@app.get("/leaderboards", response_model=List[data.Leaderboard])
|
||||
@app.put(
|
||||
"/{leaderboard_id}",
|
||||
response_model=data.LeaderboardUpdatedResponse,
|
||||
tags=["Authorized Endpoints"],
|
||||
)
|
||||
async def update_leaderboard(
|
||||
request: Request,
|
||||
leaderboard_id: UUID = Path(..., description="Leaderboard ID"),
|
||||
leaderboard: data.LeaderboardUpdateRequest = Body(...),
|
||||
db_session: Session = Depends(db.yield_db_session),
|
||||
Authorization: str = AuthHeader,
|
||||
) -> data.LeaderboardUpdatedResponse:
|
||||
"""
|
||||
Update leaderboard.
|
||||
"""
|
||||
|
||||
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 access != True:
|
||||
raise EngineHTTPException(
|
||||
status_code=403, detail="You don't have access to this leaderboard."
|
||||
)
|
||||
|
||||
try:
|
||||
updated_leaderboard = actions.update_leaderboard(
|
||||
db_session=db_session,
|
||||
leaderboard_id=leaderboard_id,
|
||||
title=leaderboard.title,
|
||||
description=leaderboard.description,
|
||||
)
|
||||
except actions.LeaderboardUpdateError as e:
|
||||
logger.error(f"Error while updating leaderboard: {e}")
|
||||
raise EngineHTTPException(
|
||||
status_code=500,
|
||||
detail="Leaderboard update failed. Please try again.",
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error while updating leaderboard: {e}")
|
||||
raise EngineHTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
return data.LeaderboardUpdatedResponse(
|
||||
id=updated_leaderboard.id, # type: ignore
|
||||
title=updated_leaderboard.title, # type: ignore
|
||||
description=updated_leaderboard.description, # type: ignore
|
||||
resource_id=updated_leaderboard.resource_id, # type: ignore
|
||||
created_at=updated_leaderboard.created_at, # type: ignore
|
||||
updated_at=updated_leaderboard.updated_at, # type: ignore
|
||||
)
|
||||
|
||||
|
||||
@app.delete(
|
||||
"/{leaderboard_id}",
|
||||
response_model=data.LeaderboardDeletedResponse,
|
||||
tags=["Authorized Endpoints"],
|
||||
)
|
||||
async def delete_leaderboard(
|
||||
request: Request,
|
||||
leaderboard_id: UUID = Path(..., description="Leaderboard ID"),
|
||||
db_session: Session = Depends(db.yield_db_session),
|
||||
Authorization: str = AuthHeader,
|
||||
) -> data.LeaderboardDeletedResponse:
|
||||
"""
|
||||
Delete leaderboard.
|
||||
"""
|
||||
|
||||
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 access != True:
|
||||
raise EngineHTTPException(
|
||||
status_code=403, detail="You don't have access to this leaderboard."
|
||||
)
|
||||
|
||||
try:
|
||||
deleted_leaderboard = actions.delete_leaderboard(
|
||||
db_session=db_session,
|
||||
leaderboard_id=leaderboard_id,
|
||||
token=token,
|
||||
)
|
||||
except actions.LeaderboardDeleteError as e:
|
||||
logger.error(f"Error while deleting leaderboard: {e}")
|
||||
raise EngineHTTPException(
|
||||
status_code=500,
|
||||
detail="Leaderboard deletion failed. Please try again.",
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error while deleting leaderboard: {e}")
|
||||
raise EngineHTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
return data.LeaderboardDeletedResponse(
|
||||
id=deleted_leaderboard.id, # type: ignore
|
||||
title=deleted_leaderboard.title, # type: ignore
|
||||
description=deleted_leaderboard.description, # type: ignore
|
||||
created_at=deleted_leaderboard.created_at, # type: ignore
|
||||
updated_at=deleted_leaderboard.updated_at, # type: ignore
|
||||
)
|
||||
|
||||
|
||||
@app.get(
|
||||
"/leaderboards",
|
||||
response_model=List[data.Leaderboard],
|
||||
tags=["Authorized Endpoints"],
|
||||
)
|
||||
async def get_leaderboards(
|
||||
request: Request, db_session: Session = Depends(db.yield_db_session)
|
||||
request: Request,
|
||||
db_session: Session = Depends(db.yield_db_session),
|
||||
Authorization: str = AuthHeader,
|
||||
) -> List[data.Leaderboard]:
|
||||
"""
|
||||
Returns leaderboard list to which user has access.
|
||||
|
@ -109,12 +325,12 @@ async def get_leaderboards(
|
|||
|
||||
results = [
|
||||
data.Leaderboard(
|
||||
id=leaderboard.id,
|
||||
title=leaderboard.title,
|
||||
description=leaderboard.description,
|
||||
resource_id=leaderboard.resource_id,
|
||||
created_at=leaderboard.created_at,
|
||||
updated_at=leaderboard.updated_at,
|
||||
id=leaderboard.id, # type: ignore
|
||||
title=leaderboard.title, # type: ignore
|
||||
description=leaderboard.description, # type: ignore
|
||||
resource_id=leaderboard.resource_id, # type: ignore
|
||||
created_at=leaderboard.created_at, # type: ignore
|
||||
updated_at=leaderboard.updated_at, # type: ignore
|
||||
)
|
||||
for leaderboard in leaderboards
|
||||
]
|
||||
|
@ -122,9 +338,13 @@ async def get_leaderboards(
|
|||
return results
|
||||
|
||||
|
||||
@app.get("/count/addresses", response_model=data.CountAddressesResponse)
|
||||
@app.get(
|
||||
"/count/addresses",
|
||||
response_model=data.CountAddressesResponse,
|
||||
tags=["Public Endpoints"],
|
||||
)
|
||||
async def count_addresses(
|
||||
leaderboard_id: UUID,
|
||||
leaderboard_id: UUID = Query(..., description="Leaderboard ID"),
|
||||
db_session: Session = Depends(db.yield_db_session),
|
||||
) -> data.CountAddressesResponse:
|
||||
"""
|
||||
|
@ -148,9 +368,70 @@ async def count_addresses(
|
|||
return data.CountAddressesResponse(count=count)
|
||||
|
||||
|
||||
@app.get("/quartiles", response_model=data.QuartilesResponse)
|
||||
@app.get(
|
||||
"/info", response_model=data.LeaderboardInfoResponse, tags=["Public Endpoints"]
|
||||
)
|
||||
async def leadeboard_info(
|
||||
leaderboard_id: UUID = Query(..., description="Leaderboard ID"),
|
||||
db_session: Session = Depends(db.yield_db_session),
|
||||
) -> data.LeaderboardInfoResponse:
|
||||
"""
|
||||
Returns leaderboard info.
|
||||
"""
|
||||
try:
|
||||
leaderboard = actions.get_leaderboard_info(db_session, leaderboard_id)
|
||||
except NoResultFound as e:
|
||||
raise EngineHTTPException(
|
||||
status_code=404,
|
||||
detail="Leaderboard not found.",
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error while getting leaderboard: {e}")
|
||||
raise EngineHTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
return data.LeaderboardInfoResponse(
|
||||
id=leaderboard.id,
|
||||
title=leaderboard.title,
|
||||
description=leaderboard.description,
|
||||
users_count=leaderboard.users_count,
|
||||
last_updated_at=leaderboard.last_update,
|
||||
)
|
||||
|
||||
|
||||
@app.get(
|
||||
"/scores/changes",
|
||||
response_model=List[data.LeaderboardScoresChangesResponse],
|
||||
tags=["Public Endpoints"],
|
||||
)
|
||||
async def get_scores_changes(
|
||||
leaderboard_id: UUID = Query(..., description="Leaderboard ID"),
|
||||
db_session: Session = Depends(db.yield_db_session),
|
||||
) -> List[data.LeaderboardScoresChangesResponse]:
|
||||
"""
|
||||
Returns the score history for the given address.
|
||||
"""
|
||||
|
||||
try:
|
||||
scores = actions.get_leaderboard_scores_changes(db_session, leaderboard_id)
|
||||
except actions.LeaderboardIsEmpty:
|
||||
raise EngineHTTPException(status_code=204, detail="Leaderboard is empty.")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error while getting scores: {e}")
|
||||
raise EngineHTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
return [
|
||||
data.LeaderboardScoresChangesResponse(
|
||||
players_count=score.players_count,
|
||||
date=score.date,
|
||||
)
|
||||
for score in scores
|
||||
]
|
||||
|
||||
|
||||
@app.get("/quartiles", response_model=data.QuartilesResponse, tags=["Public Endpoints"])
|
||||
async def quartiles(
|
||||
leaderboard_id: UUID,
|
||||
leaderboard_id: UUID = Query(..., description="Leaderboard ID"),
|
||||
db_session: Session = Depends(db.yield_db_session),
|
||||
) -> data.QuartilesResponse:
|
||||
"""
|
||||
|
@ -184,14 +465,20 @@ async def quartiles(
|
|||
)
|
||||
|
||||
|
||||
@app.get("/position", response_model=List[data.LeaderboardPosition])
|
||||
@app.get(
|
||||
"/position",
|
||||
response_model=List[data.LeaderboardPosition],
|
||||
tags=["Public Endpoints"],
|
||||
)
|
||||
async def position(
|
||||
leaderboard_id: UUID,
|
||||
address: str,
|
||||
window_size: int = 1,
|
||||
limit: int = 10,
|
||||
offset: int = 0,
|
||||
normalize_addresses: bool = True,
|
||||
leaderboard_id: UUID = Query(..., description="Leaderboard ID"),
|
||||
address: str = Query(..., description="Address to get position for."),
|
||||
window_size: int = Query(1, description="Amount of positions up and down."),
|
||||
limit: int = Query(10),
|
||||
offset: int = Query(0),
|
||||
normalize_addresses: bool = Query(
|
||||
True, description="Normalize addresses to checksum."
|
||||
),
|
||||
db_session: Session = Depends(db.yield_db_session),
|
||||
) -> List[data.LeaderboardPosition]:
|
||||
"""
|
||||
|
@ -231,52 +518,14 @@ async def position(
|
|||
return results
|
||||
|
||||
|
||||
@app.get("", response_model=List[data.LeaderboardPosition])
|
||||
@app.get("/", response_model=List[data.LeaderboardPosition])
|
||||
async def leaderboard(
|
||||
leaderboard_id: UUID,
|
||||
limit: int = 10,
|
||||
offset: int = 0,
|
||||
db_session: Session = Depends(db.yield_db_session),
|
||||
) -> List[data.LeaderboardPosition]:
|
||||
"""
|
||||
Returns the leaderboard positions.
|
||||
"""
|
||||
|
||||
### Check if leaderboard exists
|
||||
try:
|
||||
actions.get_leaderboard_by_id(db_session, leaderboard_id)
|
||||
except NoResultFound as e:
|
||||
raise EngineHTTPException(
|
||||
status_code=404,
|
||||
detail="Leaderboard not found.",
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error while getting leaderboard: {e}")
|
||||
raise EngineHTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
leaderboard_positions = actions.get_leaderboard_positions(
|
||||
db_session, leaderboard_id, limit, offset
|
||||
)
|
||||
result = [
|
||||
data.LeaderboardPosition(
|
||||
address=position.address,
|
||||
score=position.score,
|
||||
rank=position.rank,
|
||||
points_data=position.points_data,
|
||||
)
|
||||
for position in leaderboard_positions
|
||||
]
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@app.get("/rank", response_model=List[data.LeaderboardPosition])
|
||||
@app.get(
|
||||
"/rank", response_model=List[data.LeaderboardPosition], tags=["Public Endpoints"]
|
||||
)
|
||||
async def rank(
|
||||
leaderboard_id: UUID,
|
||||
rank: int = 1,
|
||||
limit: Optional[int] = None,
|
||||
offset: Optional[int] = None,
|
||||
leaderboard_id: UUID = Query(..., description="Leaderboard ID"),
|
||||
rank: int = Query(1, description="Rank to get."),
|
||||
limit: Optional[int] = Query(None),
|
||||
offset: Optional[int] = Query(None),
|
||||
db_session: Session = Depends(db.yield_db_session),
|
||||
) -> List[data.LeaderboardPosition]:
|
||||
"""
|
||||
|
@ -310,9 +559,10 @@ async def rank(
|
|||
return results
|
||||
|
||||
|
||||
@app.get("/ranks", response_model=List[data.RanksResponse])
|
||||
@app.get("/ranks", response_model=List[data.RanksResponse], tags=["Public Endpoints"])
|
||||
async def ranks(
|
||||
leaderboard_id: UUID, db_session: Session = Depends(db.yield_db_session)
|
||||
leaderboard_id: UUID = Query(..., description="Leaderboard ID"),
|
||||
db_session: Session = Depends(db.yield_db_session),
|
||||
) -> List[data.RanksResponse]:
|
||||
"""
|
||||
Returns the leaderboard rank buckets overview with score and size of bucket.
|
||||
|
@ -342,41 +592,47 @@ async def ranks(
|
|||
return results
|
||||
|
||||
|
||||
@app.put("/{leaderboard_id}/scores", response_model=List[data.LeaderboardScore])
|
||||
@app.put(
|
||||
"/{leaderboard_id}/scores",
|
||||
response_model=List[data.LeaderboardScore],
|
||||
tags=["Authorized Endpoints"],
|
||||
)
|
||||
async def leaderboard_push_scores(
|
||||
request: Request,
|
||||
leaderboard_id: UUID,
|
||||
scores: List[data.Score],
|
||||
overwrite: bool = False,
|
||||
normalize_addresses: bool = True,
|
||||
leaderboard_id: UUID = Path(..., description="Leaderboard ID"),
|
||||
scores: List[data.Score] = Body(
|
||||
..., description="Scores to put to the leaderboard."
|
||||
),
|
||||
overwrite: bool = Query(
|
||||
False,
|
||||
description="If enabled, this will delete all current scores and replace them with the new scores provided.",
|
||||
),
|
||||
normalize_addresses: bool = Query(
|
||||
True, description="Normalize addresses to checksum."
|
||||
),
|
||||
db_session: Session = Depends(db.yield_db_session),
|
||||
Authorization: str = AuthHeader,
|
||||
) -> List[data.LeaderboardScore]:
|
||||
"""
|
||||
Put the leaderboard to the database.
|
||||
"""
|
||||
|
||||
access = actions.check_leaderboard_resource_permissions(
|
||||
db_session=db_session,
|
||||
leaderboard_id=leaderboard_id,
|
||||
token=request.state.token,
|
||||
)
|
||||
|
||||
if not access:
|
||||
raise EngineHTTPException(
|
||||
status_code=403, detail="You don't have access to this leaderboard."
|
||||
)
|
||||
|
||||
### Check if leaderboard exists
|
||||
token = request.state.token
|
||||
try:
|
||||
actions.get_leaderboard_by_id(db_session, leaderboard_id)
|
||||
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.",
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error while getting leaderboard: {e}")
|
||||
raise EngineHTTPException(status_code=500, detail="Internal server error")
|
||||
|
||||
if not access:
|
||||
raise EngineHTTPException(
|
||||
status_code=403, detail="You don't have access to this leaderboard."
|
||||
)
|
||||
|
||||
try:
|
||||
leaderboard_points = actions.add_scores(
|
||||
|
|
|
@ -9,12 +9,12 @@ import logging
|
|||
from typing import Dict, List, Optional
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import Body, Depends, FastAPI, Query, Request, Path
|
||||
from fastapi import Body, Depends, FastAPI, Path, Query, Request
|
||||
from sqlalchemy.exc import NoResultFound
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .. import contracts_actions, data, db
|
||||
from ..middleware import BroodAuthMiddleware, EngineHTTPException, BugoutCORSMiddleware
|
||||
from ..middleware import BroodAuthMiddleware, BugoutCORSMiddleware, EngineHTTPException
|
||||
from ..settings import DOCS_TARGET_PATH
|
||||
from ..version import VERSION
|
||||
|
||||
|
@ -37,7 +37,9 @@ tags_metadata = [
|
|||
whitelist_paths = {
|
||||
"/metatx/openapi.json": "GET",
|
||||
f"/metatx/{DOCS_TARGET_PATH}": "GET",
|
||||
"/metatx/blockchains": "GET",
|
||||
"/metatx/contracts/types": "GET",
|
||||
"/metatx/requests/types": "GET",
|
||||
"/metatx/requests": "GET",
|
||||
}
|
||||
|
||||
|
@ -62,63 +64,79 @@ app.add_middleware(
|
|||
)
|
||||
|
||||
|
||||
@app.get("/contracts/types", tags=["contracts"])
|
||||
async def contract_types() -> Dict[str, str]:
|
||||
@app.get("/blockchains", tags=["blockchains"], response_model=data.BlockchainsResponse)
|
||||
async def blockchains_route(
|
||||
db_session: Session = Depends(db.yield_db_read_only_session),
|
||||
) -> data.BlockchainsResponse:
|
||||
"""
|
||||
Describes the contract_types that users can register contracts as against this API.
|
||||
Returns supported list of blockchains.
|
||||
"""
|
||||
return {
|
||||
data.ContractType.raw.value: "A generic smart contract. You can ask users to submit arbitrary calldata to this contract.",
|
||||
data.ContractType.dropper.value: "A Dropper contract. You can authorize users to submit claims against this contract.",
|
||||
}
|
||||
try:
|
||||
blockchains = contracts_actions.list_blockchains(
|
||||
db_session=db_session,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(repr(e))
|
||||
raise EngineHTTPException(status_code=500)
|
||||
return data.BlockchainsResponse(
|
||||
blockchains=[blockchain for blockchain in blockchains]
|
||||
)
|
||||
|
||||
|
||||
@app.get("/contracts", tags=["contracts"], response_model=List[data.RegisteredContract])
|
||||
async def list_registered_contracts(
|
||||
@app.get(
|
||||
"/contracts",
|
||||
tags=["contracts"],
|
||||
response_model=List[data.RegisteredContractResponse],
|
||||
)
|
||||
async def list_registered_contracts_route(
|
||||
request: Request,
|
||||
blockchain: Optional[str] = Query(None),
|
||||
address: Optional[str] = Query(None),
|
||||
contract_type: Optional[data.ContractType] = Query(None),
|
||||
limit: int = Query(10),
|
||||
offset: Optional[int] = Query(None),
|
||||
db_session: Session = Depends(db.yield_db_read_only_session),
|
||||
) -> List[data.RegisteredContract]:
|
||||
) -> List[data.RegisteredContractResponse]:
|
||||
"""
|
||||
Users can use this endpoint to look up the contracts they have registered against this API.
|
||||
"""
|
||||
try:
|
||||
contracts = contracts_actions.lookup_registered_contracts(
|
||||
db_session=db_session,
|
||||
moonstream_user_id=request.state.user.id,
|
||||
blockchain=blockchain,
|
||||
address=address,
|
||||
contract_type=contract_type,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
registered_contracts_with_blockchain = (
|
||||
contracts_actions.lookup_registered_contracts(
|
||||
db_session=db_session,
|
||||
metatx_requester_id=request.state.user.id,
|
||||
blockchain=blockchain,
|
||||
address=address,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
)
|
||||
except Exception as err:
|
||||
logger.error(repr(err))
|
||||
raise EngineHTTPException(status_code=500)
|
||||
return [contract for contract in contracts]
|
||||
|
||||
return [
|
||||
contracts_actions.parse_registered_contract_response(rc)
|
||||
for rc in registered_contracts_with_blockchain
|
||||
]
|
||||
|
||||
|
||||
@app.get(
|
||||
"/contracts/{contract_id}",
|
||||
tags=["contracts"],
|
||||
response_model=data.RegisteredContract,
|
||||
response_model=data.RegisteredContractResponse,
|
||||
)
|
||||
async def get_registered_contract(
|
||||
async def get_registered_contract_route(
|
||||
request: Request,
|
||||
contract_id: UUID = Path(...),
|
||||
db_session: Session = Depends(db.yield_db_read_only_session),
|
||||
) -> List[data.RegisteredContract]:
|
||||
) -> List[data.RegisteredContractResponse]:
|
||||
"""
|
||||
Get the contract by ID.
|
||||
"""
|
||||
try:
|
||||
contract = contracts_actions.get_registered_contract(
|
||||
contract_with_blockchain = contracts_actions.get_registered_contract(
|
||||
db_session=db_session,
|
||||
moonstream_user_id=request.state.user.id,
|
||||
metatx_requester_id=request.state.user.id,
|
||||
contract_id=contract_id,
|
||||
)
|
||||
except NoResultFound:
|
||||
|
@ -129,57 +147,71 @@ async def get_registered_contract(
|
|||
except Exception as err:
|
||||
logger.error(repr(err))
|
||||
raise EngineHTTPException(status_code=500)
|
||||
return contract
|
||||
|
||||
return contracts_actions.parse_registered_contract_response(
|
||||
contract_with_blockchain
|
||||
)
|
||||
|
||||
|
||||
@app.post("/contracts", tags=["contracts"], response_model=data.RegisteredContract)
|
||||
async def register_contract(
|
||||
@app.post(
|
||||
"/contracts", tags=["contracts"], response_model=data.RegisteredContractResponse
|
||||
)
|
||||
async def register_contract_route(
|
||||
request: Request,
|
||||
contract: data.RegisterContractRequest = Body(...),
|
||||
db_session: Session = Depends(db.yield_db_session),
|
||||
) -> data.RegisteredContract:
|
||||
) -> data.RegisteredContractResponse:
|
||||
"""
|
||||
Allows users to register contracts.
|
||||
"""
|
||||
try:
|
||||
registered_contract = contracts_actions.register_contract(
|
||||
contract_with_blockchain = contracts_actions.register_contract(
|
||||
db_session=db_session,
|
||||
moonstream_user_id=request.state.user.id,
|
||||
blockchain=contract.blockchain,
|
||||
metatx_requester_id=request.state.user.id,
|
||||
blockchain_name=contract.blockchain,
|
||||
address=contract.address,
|
||||
contract_type=contract.contract_type,
|
||||
title=contract.title,
|
||||
description=contract.description,
|
||||
image_uri=contract.image_uri,
|
||||
)
|
||||
except contracts_actions.UnsupportedBlockchain:
|
||||
raise EngineHTTPException(
|
||||
status_code=400, detail="Unsupported blockchain specified"
|
||||
)
|
||||
except contracts_actions.ContractAlreadyRegistered:
|
||||
raise EngineHTTPException(
|
||||
status_code=409,
|
||||
detail="Contract already registered",
|
||||
)
|
||||
return registered_contract
|
||||
except Exception as err:
|
||||
logger.error(repr(err))
|
||||
raise EngineHTTPException(status_code=500)
|
||||
|
||||
return contracts_actions.parse_registered_contract_response(
|
||||
contract_with_blockchain
|
||||
)
|
||||
|
||||
|
||||
@app.put(
|
||||
"/contracts/{contract_id}",
|
||||
tags=["contracts"],
|
||||
response_model=data.RegisteredContract,
|
||||
response_model=data.RegisteredContractResponse,
|
||||
)
|
||||
async def update_contract(
|
||||
async def update_contract_route(
|
||||
request: Request,
|
||||
contract_id: UUID = Path(...),
|
||||
update_info: data.UpdateContractRequest = Body(...),
|
||||
db_session: Session = Depends(db.yield_db_session),
|
||||
) -> data.RegisteredContract:
|
||||
) -> data.RegisteredContractResponse:
|
||||
try:
|
||||
contract = contracts_actions.update_registered_contract(
|
||||
db_session,
|
||||
request.state.user.id,
|
||||
contract_id,
|
||||
update_info.title,
|
||||
update_info.description,
|
||||
update_info.image_uri,
|
||||
update_info.ignore_nulls,
|
||||
contract_with_blockchain = contracts_actions.update_registered_contract(
|
||||
db_session=db_session,
|
||||
metatx_requester_id=request.state.user.id,
|
||||
contract_id=contract_id,
|
||||
title=update_info.title,
|
||||
description=update_info.description,
|
||||
image_uri=update_info.image_uri,
|
||||
ignore_nulls=update_info.ignore_nulls,
|
||||
)
|
||||
except NoResultFound:
|
||||
raise EngineHTTPException(
|
||||
|
@ -190,37 +222,64 @@ async def update_contract(
|
|||
logger.error(repr(err))
|
||||
raise EngineHTTPException(status_code=500)
|
||||
|
||||
return contract
|
||||
return contracts_actions.parse_registered_contract_response(
|
||||
contract_with_blockchain
|
||||
)
|
||||
|
||||
|
||||
@app.delete(
|
||||
"/contracts/{contract_id}",
|
||||
tags=["contracts"],
|
||||
response_model=data.RegisteredContract,
|
||||
response_model=data.RegisteredContractResponse,
|
||||
)
|
||||
async def delete_contract(
|
||||
async def delete_contract_route(
|
||||
request: Request,
|
||||
contract_id: UUID,
|
||||
contract_id: UUID = Path(...),
|
||||
db_session: Session = Depends(db.yield_db_session),
|
||||
) -> data.RegisteredContract:
|
||||
) -> data.RegisteredContractResponse:
|
||||
"""
|
||||
Allows users to delete contracts that they have registered.
|
||||
"""
|
||||
try:
|
||||
deleted_contract = contracts_actions.delete_registered_contract(
|
||||
deleted_contract_with_blockchain = contracts_actions.delete_registered_contract(
|
||||
db_session=db_session,
|
||||
moonstream_user_id=request.state.user.id,
|
||||
metatx_requester_id=request.state.user.id,
|
||||
registered_contract_id=contract_id,
|
||||
)
|
||||
except Exception as err:
|
||||
logger.error(repr(err))
|
||||
raise EngineHTTPException(status_code=500)
|
||||
|
||||
return deleted_contract
|
||||
return contracts_actions.parse_registered_contract_response(
|
||||
deleted_contract_with_blockchain
|
||||
)
|
||||
|
||||
|
||||
@app.get("/requests", tags=["requests"], response_model=List[data.CallRequest])
|
||||
async def list_requests(
|
||||
# TODO(kompotkot): route `/contracts/types` deprecated
|
||||
@app.get("/contracts/types", tags=["contracts"])
|
||||
@app.get(
|
||||
"/requests/types",
|
||||
tags=["requests"],
|
||||
response_model=List[data.CallRequestTypeResponse],
|
||||
)
|
||||
async def call_request_types_route(
|
||||
db_session: Session = Depends(db.yield_db_read_only_session),
|
||||
) -> List[data.CallRequestTypeResponse]:
|
||||
"""
|
||||
Describes the call_request_types that users can register call requests as against this API.
|
||||
"""
|
||||
try:
|
||||
call_request_types = contracts_actions.list_call_request_types(
|
||||
db_session=db_session,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(repr(e))
|
||||
raise EngineHTTPException(status_code=500)
|
||||
return call_request_types
|
||||
|
||||
|
||||
@app.get("/requests", tags=["requests"], response_model=List[data.CallRequestResponse])
|
||||
async def list_requests_route(
|
||||
contract_id: Optional[UUID] = Query(None),
|
||||
contract_address: Optional[str] = Query(None),
|
||||
caller: str = Query(...),
|
||||
|
@ -228,7 +287,7 @@ async def list_requests(
|
|||
offset: Optional[int] = Query(None),
|
||||
show_expired: Optional[bool] = Query(False),
|
||||
db_session: Session = Depends(db.yield_db_read_only_session),
|
||||
) -> List[data.CallRequest]:
|
||||
) -> List[data.CallRequestResponse]:
|
||||
"""
|
||||
Allows API user to see all unexpired call requests for a given caller against a given contract.
|
||||
|
||||
|
@ -251,21 +310,23 @@ async def list_requests(
|
|||
logger.error(repr(e))
|
||||
raise EngineHTTPException(status_code=500)
|
||||
|
||||
return requests
|
||||
return [contracts_actions.parse_call_request_response(r) for r in requests]
|
||||
|
||||
|
||||
@app.get("/requests/{request_id}", tags=["requests"], response_model=data.CallRequest)
|
||||
@app.get(
|
||||
"/requests/{request_id}", tags=["requests"], response_model=data.CallRequestResponse
|
||||
)
|
||||
async def get_request(
|
||||
request_id: UUID = Path(...),
|
||||
db_session: Session = Depends(db.yield_db_read_only_session),
|
||||
) -> List[data.CallRequest]:
|
||||
) -> List[data.CallRequestResponse]:
|
||||
"""
|
||||
Allows API user to see call request.
|
||||
|
||||
At least one of `contract_id` or `contract_address` must be provided as query parameters.
|
||||
"""
|
||||
try:
|
||||
result = contracts_actions.get_call_requests(
|
||||
request = contracts_actions.get_call_requests(
|
||||
db_session=db_session,
|
||||
request_id=request_id,
|
||||
)
|
||||
|
@ -278,7 +339,7 @@ async def get_request(
|
|||
logger.error(repr(e))
|
||||
raise EngineHTTPException(status_code=500)
|
||||
|
||||
return result
|
||||
return contracts_actions.parse_call_request_response(request)
|
||||
|
||||
|
||||
@app.post("/requests", tags=["requests"], response_model=int)
|
||||
|
@ -295,7 +356,7 @@ async def create_requests(
|
|||
try:
|
||||
num_requests = contracts_actions.request_calls(
|
||||
db_session=db_session,
|
||||
moonstream_user_id=request.state.user.id,
|
||||
metatx_requester_id=request.state.user.id,
|
||||
registered_contract_id=data.contract_id,
|
||||
contract_address=data.contract_address,
|
||||
call_specs=data.specifications,
|
||||
|
@ -306,6 +367,26 @@ async def create_requests(
|
|||
status_code=400,
|
||||
detail=f"Address not passed web3checksum validation, err: {err}",
|
||||
)
|
||||
except contracts_actions.UnsupportedCallRequestType as err:
|
||||
raise EngineHTTPException(
|
||||
status_code=400,
|
||||
detail=f"Unsupported call request type specified, err: {err}",
|
||||
)
|
||||
except contracts_actions.CallRequestMethodValueError as err:
|
||||
raise EngineHTTPException(
|
||||
status_code=400,
|
||||
detail=f"Unacceptable call request method specified, err: {err}",
|
||||
)
|
||||
except contracts_actions.CallRequestRequiredParamsValueError as err:
|
||||
raise EngineHTTPException(
|
||||
status_code=400,
|
||||
detail=f"Unacceptable call request required params specified, err: {err}",
|
||||
)
|
||||
except contracts_actions.CallRequestAlreadyRegistered:
|
||||
raise EngineHTTPException(
|
||||
status_code=409,
|
||||
detail="Call request with same request_id already registered",
|
||||
)
|
||||
except Exception as err:
|
||||
logger.error(repr(err))
|
||||
raise EngineHTTPException(status_code=500)
|
||||
|
@ -325,7 +406,7 @@ async def delete_requests(
|
|||
try:
|
||||
deleted_requests = contracts_actions.delete_requests(
|
||||
db_session=db_session,
|
||||
moonstream_user_id=request.state.user.id,
|
||||
metatx_requester_id=request.state.user.id,
|
||||
request_ids=request_ids,
|
||||
)
|
||||
except Exception as err:
|
||||
|
|
|
@ -1 +1 @@
|
|||
0.0.4
|
||||
0.0.6
|
||||
|
|
|
@ -7,7 +7,7 @@ base58==2.1.1
|
|||
bitarray==2.7.6
|
||||
boto3==1.27.0
|
||||
botocore==1.30.0
|
||||
bugout==0.2.9
|
||||
bugout==0.2.14
|
||||
certifi==2023.5.7
|
||||
charset-normalizer==3.1.0
|
||||
click==8.1.3
|
||||
|
@ -61,4 +61,4 @@ uvicorn==0.22.0
|
|||
varint==1.0.2
|
||||
web3==5.31.4
|
||||
websockets==9.1
|
||||
yarl==1.9.2
|
||||
yarl==1.9.2
|
||||
|
|
|
@ -13,7 +13,7 @@ setup(
|
|||
packages=find_packages(),
|
||||
install_requires=[
|
||||
"boto3",
|
||||
"bugout>=0.2.2",
|
||||
"bugout>=0.2.14",
|
||||
"eip712==0.1.0",
|
||||
"eth-typing>=2.3.0",
|
||||
"fastapi",
|
||||
|
|
|
@ -1,28 +1,27 @@
|
|||
from collections import OrderedDict
|
||||
import hashlib
|
||||
import json
|
||||
from itertools import chain
|
||||
import logging
|
||||
from typing import List, Optional, Dict, Any, Union
|
||||
from enum import Enum
|
||||
import uuid
|
||||
from collections import OrderedDict
|
||||
from enum import Enum
|
||||
from itertools import chain
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
import boto3 # type: ignore
|
||||
|
||||
from bugout.data import (
|
||||
BugoutSearchResults,
|
||||
BugoutSearchResult,
|
||||
BugoutJournal,
|
||||
BugoutJournals,
|
||||
BugoutResource,
|
||||
BugoutResources,
|
||||
BugoutSearchResult,
|
||||
BugoutSearchResults,
|
||||
)
|
||||
from bugout.journal import SearchOrder
|
||||
from bugout.exceptions import BugoutResponseException
|
||||
from entity.data import EntityCollectionsResponse, EntityCollectionResponse # type: ignore
|
||||
from entity.exceptions import EntityUnexpectedResponse # type: ignore
|
||||
from bugout.journal import SearchOrder
|
||||
from ens.utils import is_valid_ens_name # type: ignore
|
||||
from eth_utils.address import is_address # type: ignore
|
||||
from moonstreamdb.models import EthereumLabel
|
||||
from moonstreamdb.blockchain import AvailableBlockchainType
|
||||
from moonstreamdb.models import EthereumLabel
|
||||
from slugify import slugify # type: ignore
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.orm import Session
|
||||
|
@ -32,24 +31,20 @@ from web3._utils.validation import validate_abi
|
|||
from . import data
|
||||
from .middleware import MoonstreamHTTPException
|
||||
from .reporter import reporter
|
||||
from .selectors_storage import selectors
|
||||
from .settings import (
|
||||
BUGOUT_REQUEST_TIMEOUT_SECONDS,
|
||||
ETHERSCAN_SMARTCONTRACTS_BUCKET,
|
||||
MOONSTREAM_ADMIN_ACCESS_TOKEN,
|
||||
MOONSTREAM_APPLICATION_ID,
|
||||
MOONSTREAM_DATA_JOURNAL_ID,
|
||||
MOONSTREAM_MOONWORM_TASKS_JOURNAL,
|
||||
MOONSTREAM_S3_SMARTCONTRACTS_ABI_BUCKET,
|
||||
MOONSTREAM_S3_SMARTCONTRACTS_ABI_PREFIX,
|
||||
MOONSTREAM_MOONWORM_TASKS_JOURNAL,
|
||||
MOONSTREAM_ADMIN_ACCESS_TOKEN,
|
||||
support_interfaces,
|
||||
supportsInterface_abi,
|
||||
multicall_contracts,
|
||||
)
|
||||
from .settings import bugout_client as bc, entity_client as ec
|
||||
from .web3_provider import multicall, FunctionSignature, connect
|
||||
from .selectors_storage import selectors
|
||||
|
||||
from .settings import bugout_client as bc
|
||||
from .settings import multicall_contracts, support_interfaces, supportsInterface_abi
|
||||
from .web3_provider import FunctionSignature, connect, multicall
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -88,9 +83,9 @@ class ResourceQueryFetchException(Exception):
|
|||
"""
|
||||
|
||||
|
||||
class EntityCollectionNotFoundException(Exception):
|
||||
class EntityJournalNotFoundException(Exception):
|
||||
"""
|
||||
Raised when entity collection is not found
|
||||
Raised when journal (collection prev.) with entities not found.
|
||||
"""
|
||||
|
||||
|
||||
|
@ -482,7 +477,7 @@ def get_all_entries_from_search(
|
|||
|
||||
results: List[BugoutSearchResult] = []
|
||||
|
||||
existing_metods = bc.search(
|
||||
existing_methods = bc.search(
|
||||
token=token,
|
||||
journal_id=journal_id,
|
||||
query=search_query,
|
||||
|
@ -491,11 +486,11 @@ def get_all_entries_from_search(
|
|||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
results.extend(existing_metods.results)
|
||||
results.extend(existing_methods.results) # type: ignore
|
||||
|
||||
if len(results) != existing_metods.total_results:
|
||||
for offset in range(limit, existing_metods.total_results, limit):
|
||||
existing_metods = bc.search(
|
||||
if len(results) != existing_methods.total_results:
|
||||
for offset in range(limit, existing_methods.total_results, limit):
|
||||
existing_methods = bc.search(
|
||||
token=token,
|
||||
journal_id=journal_id,
|
||||
query=search_query,
|
||||
|
@ -504,7 +499,7 @@ def get_all_entries_from_search(
|
|||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
results.extend(existing_metods.results)
|
||||
results.extend(existing_methods.results) # type: ignore
|
||||
|
||||
return results
|
||||
|
||||
|
@ -641,14 +636,14 @@ def get_query_by_name(query_name: str, token: uuid.UUID) -> str:
|
|||
return query_id
|
||||
|
||||
|
||||
def get_entity_subscription_collection_id(
|
||||
def get_entity_subscription_journal_id(
|
||||
resource_type: str,
|
||||
token: Union[uuid.UUID, str],
|
||||
user_id: uuid.UUID,
|
||||
create_if_not_exist: bool = False,
|
||||
) -> Optional[str]:
|
||||
) -> str:
|
||||
"""
|
||||
Get collection_id from brood resources. If collection not exist and create_if_not_exist is True
|
||||
Get collection_id (journal_id) from brood resources. If journal not exist and create_if_not_exist is True
|
||||
"""
|
||||
|
||||
params = {
|
||||
|
@ -668,52 +663,49 @@ def get_entity_subscription_collection_id(
|
|||
|
||||
if len(resources.resources) == 0:
|
||||
if not create_if_not_exist:
|
||||
raise EntityCollectionNotFoundException(
|
||||
"Subscription collection not found."
|
||||
)
|
||||
collection_id = generate_collection_for_user(resource_type, token, user_id)
|
||||
raise EntityJournalNotFoundException("Subscription journal not found.")
|
||||
journal_id = generate_journal_for_user(resource_type, token, user_id)
|
||||
|
||||
return collection_id
|
||||
return journal_id
|
||||
|
||||
else:
|
||||
resource = resources.resources[0]
|
||||
return resource.resource_data["collection_id"]
|
||||
|
||||
|
||||
def generate_collection_for_user(
|
||||
def generate_journal_for_user(
|
||||
resource_type: str,
|
||||
token: Union[uuid.UUID, str],
|
||||
user_id: uuid.UUID,
|
||||
) -> str:
|
||||
try:
|
||||
# try get collection
|
||||
# Try get journal
|
||||
|
||||
collections: EntityCollectionsResponse = ec.list_collections(token=token)
|
||||
journals: BugoutJournals = bc.list_journals(token=token)
|
||||
|
||||
available_collections: Dict[str, str] = {
|
||||
collection.name: collection.collection_id
|
||||
for collection in collections.collections
|
||||
available_journals: Dict[str, str] = {
|
||||
journal.name: str(journal.id) for journal in journals.journals
|
||||
}
|
||||
|
||||
subscription_collection_name = f"subscriptions_{user_id}"
|
||||
subscription_journal_name = f"subscriptions_{user_id}"
|
||||
|
||||
if subscription_collection_name not in available_collections:
|
||||
collection: EntityCollectionResponse = ec.add_collection(
|
||||
token=token, name=subscription_collection_name
|
||||
if subscription_journal_name not in available_journals:
|
||||
journal: BugoutJournal = bc.create_journal(
|
||||
token=token, name=subscription_journal_name
|
||||
)
|
||||
collection_id = collection.collection_id
|
||||
journal_id = str(journal.id)
|
||||
else:
|
||||
collection_id = available_collections[subscription_collection_name]
|
||||
except EntityUnexpectedResponse as e:
|
||||
logger.error(f"Error create collection, error: {str(e)}")
|
||||
journal_id = available_journals[subscription_journal_name]
|
||||
except Exception as e:
|
||||
logger.error(f"Error create journal, error: {str(e)}")
|
||||
raise MoonstreamHTTPException(
|
||||
status_code=500, detail="Can't create collection for subscriptions"
|
||||
status_code=500, detail="Can't create journal for subscriptions"
|
||||
)
|
||||
|
||||
resource_data = {
|
||||
"type": resource_type,
|
||||
"user_id": str(user_id),
|
||||
"collection_id": str(collection_id),
|
||||
"collection_id": journal_id,
|
||||
}
|
||||
|
||||
try:
|
||||
|
@ -727,14 +719,14 @@ def generate_collection_for_user(
|
|||
except Exception as e:
|
||||
logger.error(f"Error creating subscription resource: {str(e)}")
|
||||
logger.error(
|
||||
f"Required create resource data: {resource_data}, and grand access to journal: {collection_id}, for user: {user_id}"
|
||||
f"Required create resource data: {resource_data}, and grand access to journal: {journal_id}, for user: {user_id}"
|
||||
)
|
||||
raise MoonstreamHTTPException(status_code=500, internal_error=e)
|
||||
|
||||
try:
|
||||
bc.update_journal_scopes(
|
||||
token=MOONSTREAM_ADMIN_ACCESS_TOKEN,
|
||||
journal_id=collection_id,
|
||||
journal_id=journal_id,
|
||||
holder_type="user",
|
||||
holder_id=user_id,
|
||||
permission_list=[
|
||||
|
@ -746,16 +738,16 @@ def generate_collection_for_user(
|
|||
],
|
||||
)
|
||||
logger.info(
|
||||
f"Grand access to journal: {collection_id}, for user: {user_id} successfully"
|
||||
f"Grand access to journal: {journal_id}, for user: {user_id} successfully"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating journal scopes: {str(e)}")
|
||||
logger.error(
|
||||
f"Required grand access to journal: {collection_id}, for user: {user_id}"
|
||||
f"Required grand access to journal: {journal_id}, for user: {user_id}"
|
||||
)
|
||||
raise MoonstreamHTTPException(status_code=500, internal_error=e)
|
||||
|
||||
return collection_id
|
||||
return journal_id
|
||||
|
||||
|
||||
def generate_s3_access_links(
|
||||
|
@ -896,14 +888,14 @@ def get_list_of_support_interfaces(
|
|||
|
||||
list_of_interfaces.sort()
|
||||
|
||||
for interaface in list_of_interfaces:
|
||||
for interface in list_of_interfaces:
|
||||
calls.append(
|
||||
(
|
||||
contract.address,
|
||||
FunctionSignature(
|
||||
contract.get_function_by_name("supportsInterface")
|
||||
)
|
||||
.encode_data([bytes.fromhex(interaface)])
|
||||
.encode_data([bytes.fromhex(interface)])
|
||||
.hex(),
|
||||
)
|
||||
)
|
||||
|
|
|
@ -2,65 +2,65 @@
|
|||
Generate entity subscriptions from existing brood resources subscriptions
|
||||
"""
|
||||
import hashlib
|
||||
import logging
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import traceback
|
||||
from typing import List, Optional, Dict, Any, Union, Tuple
|
||||
import uuid
|
||||
import time
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
|
||||
import boto3 # type: ignore
|
||||
from bugout.data import BugoutResources, BugoutResource
|
||||
from bugout.exceptions import BugoutResponseException
|
||||
from entity.exceptions import EntityUnexpectedResponse # type: ignore
|
||||
from entity.data import EntityCollectionResponse, EntityResponse # type: ignore
|
||||
from bugout.data import (
|
||||
BugoutJournal,
|
||||
BugoutJournalEntity,
|
||||
BugoutResource,
|
||||
BugoutResources,
|
||||
)
|
||||
from bugout.exceptions import BugoutResponseException, BugoutUnexpectedResponse
|
||||
|
||||
from ...settings import (
|
||||
BUGOUT_REQUEST_TIMEOUT_SECONDS,
|
||||
MOONSTREAM_ADMIN_ACCESS_TOKEN,
|
||||
BUGOUT_RESOURCE_TYPE_SUBSCRIPTION,
|
||||
BUGOUT_RESOURCE_TYPE_ENTITY_SUBSCRIPTION,
|
||||
MOONSTREAM_APPLICATION_ID,
|
||||
BUGOUT_RESOURCE_TYPE_DASHBOARD,
|
||||
BUGOUT_RESOURCE_TYPE_ENTITY_SUBSCRIPTION,
|
||||
BUGOUT_RESOURCE_TYPE_SUBSCRIPTION,
|
||||
MOONSTREAM_ADMIN_ACCESS_TOKEN,
|
||||
MOONSTREAM_APPLICATION_ID,
|
||||
)
|
||||
from ...settings import bugout_client as bc, entity_client as ec
|
||||
from ...settings import bugout_client as bc
|
||||
from ..subscription_types import CANONICAL_SUBSCRIPTION_TYPES
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
### create collection for user
|
||||
### Create journal for user
|
||||
|
||||
|
||||
def create_collection_for_user(user_id: uuid.UUID) -> str:
|
||||
def create_journal_for_user(user_id: uuid.UUID) -> str:
|
||||
"""
|
||||
Create collection for user if not exist
|
||||
Create journal (collection) for user if not exist
|
||||
"""
|
||||
try:
|
||||
# try get collection
|
||||
|
||||
collection: EntityCollectionResponse = ec.add_collection(
|
||||
# Try to get journal
|
||||
journal: BugoutJournal = bc.create_journal(
|
||||
token=MOONSTREAM_ADMIN_ACCESS_TOKEN, name=f"subscriptions_{user_id}"
|
||||
)
|
||||
collection_id = collection.collection_id
|
||||
|
||||
except EntityUnexpectedResponse as e:
|
||||
logger.error(f"Error create collection, error: {str(e)}")
|
||||
return str(collection_id)
|
||||
journal_id = journal.id
|
||||
except BugoutUnexpectedResponse as e:
|
||||
logger.error(f"Error create journal, error: {str(e)}")
|
||||
return str(journal_id)
|
||||
|
||||
|
||||
def add_entity_subscription(
|
||||
user_id: uuid.UUID,
|
||||
subscription_type_id: str,
|
||||
collection_id: str,
|
||||
journal_id: str,
|
||||
address: str,
|
||||
color: str,
|
||||
label: str,
|
||||
content: Dict[str, Any],
|
||||
) -> EntityResponse:
|
||||
) -> BugoutJournalEntity:
|
||||
"""
|
||||
Add subscription to collection
|
||||
Add subscription to journal (collection).
|
||||
"""
|
||||
|
||||
if subscription_type_id not in CANONICAL_SUBSCRIPTION_TYPES:
|
||||
|
@ -68,17 +68,18 @@ def add_entity_subscription(
|
|||
f"Unknown subscription type ID: {subscription_type_id}. "
|
||||
f"Known subscription type IDs: {CANONICAL_SUBSCRIPTION_TYPES.keys()}"
|
||||
)
|
||||
elif CANONICAL_SUBSCRIPTION_TYPES[subscription_type_id].blockchain is None:
|
||||
blockchain = CANONICAL_SUBSCRIPTION_TYPES[subscription_type_id].blockchain
|
||||
if blockchain is None:
|
||||
raise ValueError(
|
||||
f"Subscription type ID {subscription_type_id} is not a blockchain subscription type."
|
||||
)
|
||||
|
||||
entity = ec.add_entity(
|
||||
entity = bc.create_entity(
|
||||
token=MOONSTREAM_ADMIN_ACCESS_TOKEN,
|
||||
collection_id=collection_id,
|
||||
journal_id=journal_id,
|
||||
address=address,
|
||||
blockchain=CANONICAL_SUBSCRIPTION_TYPES[subscription_type_id].blockchain,
|
||||
name=label,
|
||||
blockchain=blockchain,
|
||||
title=label,
|
||||
required_fields=[
|
||||
{"type": "subscription"},
|
||||
{"subscription_type_id": f"{subscription_type_id}"},
|
||||
|
@ -105,34 +106,19 @@ def get_abi_from_s3(s3_path: str, bucket: str):
|
|||
logger.error(f"Error get ABI from S3: {str(e)}")
|
||||
|
||||
|
||||
def revoke_collection_permissions_from_user(
|
||||
user_id: uuid.UUID, collection_id: str, permissions: List[str]
|
||||
):
|
||||
"""
|
||||
Remove all permissions from user
|
||||
"""
|
||||
bc.delete_journal_scopes(
|
||||
token=MOONSTREAM_ADMIN_ACCESS_TOKEN,
|
||||
journal_id=collection_id,
|
||||
holder_type="user",
|
||||
holder_id=user_id,
|
||||
permission_list=permissions,
|
||||
)
|
||||
|
||||
|
||||
def find_user_collection(
|
||||
def find_user_journal(
|
||||
user_id: uuid.UUID,
|
||||
create_if_not_exists: bool = False,
|
||||
) -> Tuple[Optional[str], Optional[str]]:
|
||||
"""
|
||||
Find user collection in Brood resources
|
||||
Can create new collection if not exists and create_if_not_exists = True
|
||||
Find user journal (collection) in Brood resources
|
||||
Can create new journal (collection) if not exists and create_if_not_exists = True
|
||||
"""
|
||||
params = {
|
||||
"type": BUGOUT_RESOURCE_TYPE_ENTITY_SUBSCRIPTION,
|
||||
"user_id": str(user_id),
|
||||
}
|
||||
logger.info(f"Looking for collection for user {user_id}")
|
||||
logger.info(f"Looking for journal (collection) for user {user_id}")
|
||||
try:
|
||||
user_entity_resources: BugoutResources = bc.list_resources(
|
||||
token=MOONSTREAM_ADMIN_ACCESS_TOKEN, params=params
|
||||
|
@ -147,18 +133,16 @@ def find_user_collection(
|
|||
)
|
||||
|
||||
if len(user_entity_resources.resources) > 0:
|
||||
collection_id = user_entity_resources.resources[0].resource_data[
|
||||
"collection_id"
|
||||
]
|
||||
journal_id = user_entity_resources.resources[0].resource_data["collection_id"]
|
||||
logger.info(
|
||||
f"Collection found for user {user_id}. collection_id: {collection_id}"
|
||||
f"Journal (collection) found for user {user_id}. journal_id: {journal_id}"
|
||||
)
|
||||
return collection_id, str(user_entity_resources.resources[0].id)
|
||||
return journal_id, str(user_entity_resources.resources[0].id)
|
||||
elif create_if_not_exists:
|
||||
# Create collection new collection for user
|
||||
logger.info(f"Creating new collection")
|
||||
collection = create_collection_for_user(user_id)
|
||||
return collection, None
|
||||
# Create new journal for user
|
||||
logger.info(f"Creating new journal (collection)")
|
||||
journal_id = create_journal_for_user(user_id)
|
||||
return journal_id, None
|
||||
|
||||
return None, None
|
||||
|
||||
|
@ -223,33 +207,35 @@ def generate_entity_subscriptions_from_brood_resources() -> None:
|
|||
|
||||
logger.info(f"parsed users: {len(users_subscriptions)}")
|
||||
|
||||
### Create collections and add subscriptions
|
||||
### Create journals (collections) and add subscriptions
|
||||
|
||||
try:
|
||||
for user_id, subscriptions in users_subscriptions.items():
|
||||
user_id = str(user_id)
|
||||
|
||||
collection_id = None
|
||||
journal_id = None
|
||||
resource_id_of_user_collection = None
|
||||
|
||||
### Collection can already exist in stages.json
|
||||
### Journal can already exist in stages.json
|
||||
if "collection_id" in stages[user_id]:
|
||||
collection_id = stages[user_id]["collection_id"]
|
||||
journal_id = stages[user_id]["collection_id"]
|
||||
if "subscription_resource_id" in stages[user_id]:
|
||||
resource_id_of_user_collection = stages[user_id][
|
||||
"subscription_resource_id"
|
||||
]
|
||||
else:
|
||||
### look for collection in brood resources
|
||||
collection_id, resource_id_of_user_collection = find_user_collection(
|
||||
journal_id, resource_id_of_user_collection = find_user_journal(
|
||||
user_id, create_if_not_exists=True
|
||||
)
|
||||
|
||||
if collection_id is None:
|
||||
logger.info(f"Collection not found or create for user {user_id}")
|
||||
if journal_id is None:
|
||||
logger.info(
|
||||
f"Journal (collection) not found or create for user {user_id}"
|
||||
)
|
||||
continue
|
||||
|
||||
stages[user_id]["collection_id"] = collection_id
|
||||
stages[user_id]["collection_id"] = journal_id
|
||||
|
||||
# Create user subscription collection resource
|
||||
|
||||
|
@ -262,7 +248,7 @@ def generate_entity_subscriptions_from_brood_resources() -> None:
|
|||
resource_data = {
|
||||
"type": BUGOUT_RESOURCE_TYPE_ENTITY_SUBSCRIPTION,
|
||||
"user_id": str(user_id),
|
||||
"collection_id": str(collection_id),
|
||||
"collection_id": str(journal_id),
|
||||
"version": "1.0.0",
|
||||
}
|
||||
|
||||
|
@ -318,11 +304,11 @@ def generate_entity_subscriptions_from_brood_resources() -> None:
|
|||
|
||||
# Add subscription to collection
|
||||
|
||||
logger.info(f"Add subscription to collection: {collection_id}")
|
||||
logger.info(f"Add subscription to journal (collection): {journal_id}")
|
||||
|
||||
entity = add_entity_subscription(
|
||||
user_id=user_id,
|
||||
collection_id=collection_id,
|
||||
journal_id=journal_id,
|
||||
subscription_type_id=subscription_type_id,
|
||||
address=address,
|
||||
color=color,
|
||||
|
@ -331,7 +317,7 @@ def generate_entity_subscriptions_from_brood_resources() -> None:
|
|||
)
|
||||
stages[user_id]["processed_subscriptions"][
|
||||
str(subscription["subscription_id"])
|
||||
] = {"entity_id": str(entity.entity_id), "dashboard_ids": []}
|
||||
] = {"entity_id": str(entity.id), "dashboard_ids": []}
|
||||
|
||||
# Add permissions to user
|
||||
|
||||
|
@ -342,7 +328,7 @@ def generate_entity_subscriptions_from_brood_resources() -> None:
|
|||
try:
|
||||
bc.update_journal_scopes(
|
||||
token=MOONSTREAM_ADMIN_ACCESS_TOKEN,
|
||||
journal_id=collection_id,
|
||||
journal_id=journal_id,
|
||||
holder_type="user",
|
||||
holder_id=user_id,
|
||||
permission_list=[
|
||||
|
@ -361,12 +347,12 @@ def generate_entity_subscriptions_from_brood_resources() -> None:
|
|||
continue
|
||||
else:
|
||||
logger.warn(
|
||||
f"User {user_id} == {admin_user_id} permissions not changed. Unexpected behaivior!"
|
||||
f"User {user_id} == {admin_user_id} permissions not changed. Unexpected behavior!"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
logger.error(f"Failed to proccess user subscriptions: {str(e)}")
|
||||
logger.error(f"Failed to process user subscriptions: {str(e)}")
|
||||
finally:
|
||||
try:
|
||||
with open("stages.json", "w") as f:
|
||||
|
@ -561,18 +547,18 @@ def delete_generated_entity_subscriptions_from_brood_resources():
|
|||
|
||||
logger.info(f"parsed users: {len(users_subscriptions)}")
|
||||
|
||||
### Create collections and add subscriptions
|
||||
### Create journals and add subscriptions
|
||||
|
||||
try:
|
||||
for user_id, _ in users_subscriptions.items():
|
||||
user_id = str(user_id)
|
||||
|
||||
collection_id = None
|
||||
journal_id = None
|
||||
resource_id_of_user_collection = None
|
||||
|
||||
### Collection can already exist in stages.json
|
||||
if "collection_id" in stages[user_id]:
|
||||
collection_id = stages[user_id]["collection_id"]
|
||||
journal_id = stages[user_id]["collection_id"]
|
||||
|
||||
if "subscription_resource_id" in stages[user_id]:
|
||||
resource_id_of_user_collection = stages[user_id][
|
||||
|
@ -581,35 +567,37 @@ def delete_generated_entity_subscriptions_from_brood_resources():
|
|||
|
||||
else:
|
||||
### look for collection in brood resources
|
||||
collection_id, resource_id_of_user_collection = find_user_collection(
|
||||
journal_id, resource_id_of_user_collection = find_user_journal(
|
||||
user_id, create_if_not_exists=False
|
||||
)
|
||||
|
||||
if collection_id is None:
|
||||
if journal_id is None:
|
||||
logger.info(f"Collection not found or create for user {user_id}")
|
||||
continue
|
||||
|
||||
### Delete collection
|
||||
|
||||
try:
|
||||
ec.delete_collection(
|
||||
token=MOONSTREAM_ADMIN_ACCESS_TOKEN, collection_id=collection_id
|
||||
bc.delete_journal(
|
||||
token=MOONSTREAM_ADMIN_ACCESS_TOKEN, journal_id=journal_id
|
||||
)
|
||||
logger.info(f"Collection deleted {collection_id}")
|
||||
logger.info(f"Journal (collection) deleted {journal_id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete collection: {str(e)}")
|
||||
logger.error(f"Failed to delete journal (collection): {str(e)}")
|
||||
|
||||
### Delete collection resource
|
||||
|
||||
try:
|
||||
logger.info(f"Collection resource id {resource_id_of_user_collection}")
|
||||
logger.info(
|
||||
f"Journal (collection) resource id {resource_id_of_user_collection}"
|
||||
)
|
||||
bc.delete_resource(
|
||||
token=MOONSTREAM_ADMIN_ACCESS_TOKEN,
|
||||
resource_id=resource_id_of_user_collection,
|
||||
)
|
||||
logger.info(
|
||||
f"Collection resource deleted {resource_id_of_user_collection}"
|
||||
f"Journal (collection) resource deleted {resource_id_of_user_collection}"
|
||||
)
|
||||
|
||||
# clear stages
|
||||
|
@ -617,12 +605,14 @@ def delete_generated_entity_subscriptions_from_brood_resources():
|
|||
stages[user_id] = {}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to delete collection resource: {str(e)}")
|
||||
logger.error(
|
||||
f"Failed to delete journal (collection) resource: {str(e)}"
|
||||
)
|
||||
continue
|
||||
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
logger.error(f"Failed to proccess user subscriptions: {str(e)}")
|
||||
logger.error(f"Failed to process user subscriptions: {str(e)}")
|
||||
|
||||
|
||||
def restore_dashboard_state():
|
||||
|
@ -659,7 +649,7 @@ def restore_dashboard_state():
|
|||
|
||||
dashboards_by_user[user_id].append(dashboard)
|
||||
|
||||
### Retunr all dashboards to old state
|
||||
### Return all dashboards to old state
|
||||
|
||||
logger.info(f"Amount of users: {len(dashboards_by_user)}")
|
||||
|
||||
|
@ -738,41 +728,41 @@ def fix_duplicates_keys_in_entity_subscription():
|
|||
timeout=BUGOUT_REQUEST_TIMEOUT_SECONDS,
|
||||
)
|
||||
|
||||
# get collection ids from that resources
|
||||
# get journal ids from that resources
|
||||
|
||||
collection_id_user_id_mappig = {}
|
||||
collection_id_user_id_mapping = {}
|
||||
|
||||
for subscription in subscriptions.resources:
|
||||
if "collection_id" in subscription.resource_data:
|
||||
if (
|
||||
subscription.resource_data["collection_id"]
|
||||
not in collection_id_user_id_mappig
|
||||
not in collection_id_user_id_mapping
|
||||
):
|
||||
collection_id_user_id_mappig[
|
||||
collection_id_user_id_mapping[
|
||||
subscription.resource_data["collection_id"]
|
||||
] = subscription.resource_data["user_id"]
|
||||
else:
|
||||
raise Exception(
|
||||
f"Duplicate collection_id {subscription.resource_data['collection_id']} in subscriptions"
|
||||
)
|
||||
# go through all collections and fix entities.
|
||||
# go through all journals and fix entities.
|
||||
# Will creating one new entity with same data but without "type:subscription" in required_fields
|
||||
|
||||
for collection_id, user_id in collection_id_user_id_mappig.items():
|
||||
# get collection entities
|
||||
|
||||
collection_entities = ec.search_entities(
|
||||
for journal_id, user_id in collection_id_user_id_mapping.items():
|
||||
# get journal entities
|
||||
journal_entities = bc.search(
|
||||
token=MOONSTREAM_ADMIN_ACCESS_TOKEN,
|
||||
collection_id=collection_id,
|
||||
journal_id=journal_id,
|
||||
required_field=[f"type:subscription"],
|
||||
limit=1000,
|
||||
representation="entity",
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Amount of entities in user: {user_id} collection {collection_id}: {len(collection_entities.entities)}"
|
||||
f"Amount of entities in user: {user_id} journal (collection) {journal_id}: {len(journal_entities.entities)}"
|
||||
)
|
||||
|
||||
for entity in collection_entities.entities:
|
||||
for entity in journal_entities.entities:
|
||||
# get entity data
|
||||
|
||||
if entity.secondary_fields is None:
|
||||
|
@ -785,12 +775,7 @@ def fix_duplicates_keys_in_entity_subscription():
|
|||
|
||||
secondary_fields = secondary_fields["secondary_fields"]
|
||||
|
||||
# get entity id
|
||||
|
||||
entity_id = entity.entity_id
|
||||
|
||||
# get entity type
|
||||
|
||||
entity_type = None
|
||||
|
||||
# extract required fields
|
||||
|
@ -811,45 +796,45 @@ def fix_duplicates_keys_in_entity_subscription():
|
|||
new_required_fields.append(
|
||||
{"type": "copy_of_malformed_entity_20230213"}
|
||||
)
|
||||
new_required_fields.append({"entity_id": str(entity_id)})
|
||||
new_required_fields.append({"entity_id": str(entity.id)})
|
||||
|
||||
new_entity = ec.add_entity(
|
||||
new_entity = bc.create_entity(
|
||||
token=MOONSTREAM_ADMIN_ACCESS_TOKEN,
|
||||
collection_id=collection_id,
|
||||
journal_id=journal_id,
|
||||
blockchain=entity.blockchain,
|
||||
address=entity.address,
|
||||
name=entity.name,
|
||||
title=entity.title,
|
||||
required_fields=new_required_fields,
|
||||
secondary_fields=entity.secondary_fields,
|
||||
)
|
||||
logger.info(
|
||||
f"Entity {new_entity.entity_id} created successfully for collection {collection_id}"
|
||||
f"Entity {new_entity.id} created successfully for journal (collection) {journal_id}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to create entity {entity_id} for collection {collection_id}: {str(e)}, user_id: {user_id}"
|
||||
f"Failed to create entity {entity.id} for journal (collection) {journal_id}: {str(e)}, user_id: {user_id}"
|
||||
)
|
||||
continue
|
||||
|
||||
# Update old entity without secondary_fields duplicate
|
||||
|
||||
try:
|
||||
ec.update_entity(
|
||||
bc.update_entity(
|
||||
token=MOONSTREAM_ADMIN_ACCESS_TOKEN,
|
||||
collection_id=collection_id,
|
||||
entity_id=entity_id,
|
||||
journal_id=journal_id,
|
||||
entity_id=entity.id,
|
||||
blockchain=entity.blockchain,
|
||||
address=entity.address,
|
||||
name=entity.name,
|
||||
title=entity.title,
|
||||
required_fields=entity.required_fields,
|
||||
secondary_fields=secondary_fields,
|
||||
)
|
||||
logger.info(
|
||||
f"Entity {entity_id} updated successfully for collection {collection_id}"
|
||||
f"Entity {entity.id} updated successfully for journal (collection) {journal_id}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to update entity {entity_id} for collection {collection_id}: {str(e)}, user_id: {user_id}"
|
||||
f"Failed to update entity {entity.id} for journal (collection) {journal_id}: {str(e)}, user_id: {user_id}"
|
||||
)
|
||||
|
|
|
@ -1,25 +1,23 @@
|
|||
import argparse
|
||||
from collections import Counter
|
||||
import json
|
||||
import logging
|
||||
import textwrap
|
||||
from typing import Any, Dict
|
||||
|
||||
from bugout.data import BugoutResources
|
||||
from bugout.exceptions import BugoutResponseException
|
||||
from moonstream.client import Moonstream # type: ignore
|
||||
import logging
|
||||
from typing import Dict, Any
|
||||
import textwrap
|
||||
from sqlalchemy import text
|
||||
|
||||
|
||||
from ..actions import get_all_entries_from_search, name_normalization
|
||||
from ..data import BUGOUT_RESOURCE_QUERY_RESOLVER
|
||||
from ..settings import (
|
||||
BUGOUT_REQUEST_TIMEOUT_SECONDS,
|
||||
MOONSTREAM_ADMIN_ACCESS_TOKEN,
|
||||
MOONSTREAM_QUERIES_JOURNAL_ID,
|
||||
MOONSTREAM_QUERY_TEMPLATE_CONTEXT_TYPE,
|
||||
)
|
||||
from ..settings import bugout_client as bc, MOONSTREAM_QUERY_TEMPLATE_CONTEXT_TYPE
|
||||
from ..actions import get_all_entries_from_search, name_normalization
|
||||
|
||||
from ..settings import bugout_client as bc
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
"""
|
||||
Pydantic schemas for the Moonstream HTTP API
|
||||
"""
|
||||
from datetime import datetime
|
||||
import json
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List, Optional, Union, Literal
|
||||
from typing import Any, Dict, List, Literal, Optional, Union
|
||||
from uuid import UUID
|
||||
from xmlrpc.client import Boolean
|
||||
|
||||
|
@ -12,7 +12,6 @@ from fastapi import Form
|
|||
from pydantic import BaseModel, Field, validator
|
||||
from sqlalchemy import false
|
||||
|
||||
|
||||
USER_ONBOARDING_STATE = "onboarding_state"
|
||||
|
||||
BUGOUT_RESOURCE_QUERY_RESOLVER = "query_name_resolver"
|
||||
|
@ -58,13 +57,6 @@ class SubscriptionResourceData(BaseModel):
|
|||
updated_at: Optional[datetime]
|
||||
|
||||
|
||||
class CreateSubscriptionRequest(BaseModel):
|
||||
address: str
|
||||
color: str
|
||||
label: str
|
||||
subscription_type_id: str
|
||||
|
||||
|
||||
class PingResponse(BaseModel):
|
||||
"""
|
||||
Schema for ping response
|
||||
|
@ -244,7 +236,7 @@ class OnboardingState(BaseModel):
|
|||
steps: Dict[str, int]
|
||||
|
||||
|
||||
class SubdcriptionsAbiResponse(BaseModel):
|
||||
class SubscriptionsAbiResponse(BaseModel):
|
||||
abi: str
|
||||
|
||||
|
||||
|
|
|
@ -4,10 +4,10 @@ Event providers powered by Bugout journals.
|
|||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
from typing import Dict, List, Optional, Tuple, Union
|
||||
|
||||
from bugout.app import Bugout
|
||||
from bugout.data import BugoutResource, BugoutSearchResult
|
||||
from bugout.data import BugoutResource, BugoutSearchResult, BugoutSearchResultAsEntity
|
||||
from bugout.journal import SearchOrder
|
||||
from dateutil.parser import isoparse
|
||||
from dateutil.tz import UTC
|
||||
|
@ -155,7 +155,7 @@ class BugoutEventProvider:
|
|||
timeout=self.timeout,
|
||||
order=SearchOrder.DESCENDING,
|
||||
)
|
||||
events.extend([self.entry_event(entry) for entry in search_results.results])
|
||||
events.extend([self.entry_event(entry) for entry in search_results.results]) # type: ignore
|
||||
offset = search_results.next_offset
|
||||
|
||||
return stream_boundary, events
|
||||
|
@ -192,7 +192,7 @@ class BugoutEventProvider:
|
|||
timeout=self.timeout,
|
||||
order=SearchOrder.DESCENDING,
|
||||
)
|
||||
return [self.entry_event(entry) for entry in search_results.results]
|
||||
return [self.entry_event(entry) for entry in search_results.results] # type: ignore
|
||||
|
||||
def next_event(
|
||||
self,
|
||||
|
@ -233,7 +233,7 @@ class BugoutEventProvider:
|
|||
)
|
||||
if not search_results.results:
|
||||
return None
|
||||
return self.entry_event(search_results.results[0])
|
||||
return self.entry_event(search_results.results[0]) # type: ignore
|
||||
|
||||
def previous_event(
|
||||
self,
|
||||
|
@ -274,7 +274,7 @@ class BugoutEventProvider:
|
|||
)
|
||||
if not search_results.results:
|
||||
return None
|
||||
return self.entry_event(search_results.results[0])
|
||||
return self.entry_event(search_results.results[0]) # type: ignore
|
||||
|
||||
|
||||
class EthereumTXPoolProvider(BugoutEventProvider):
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
import json
|
||||
import logging
|
||||
from os import read
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
from typing import Any, Dict, List, Optional, Union, cast
|
||||
from uuid import UUID
|
||||
|
||||
import boto3 # type: ignore
|
||||
import requests # type: ignore
|
||||
from bugout.data import BugoutResource, BugoutResources
|
||||
from bugout.data import BugoutResource, BugoutResources, BugoutSearchResultAsEntity
|
||||
from bugout.exceptions import BugoutResponseException
|
||||
from entity.data import EntitiesResponse, EntityResponse # type: ignore
|
||||
from fastapi import APIRouter, Body, Path, Query, Request
|
||||
|
||||
from .. import actions, data
|
||||
|
@ -16,15 +14,15 @@ from ..middleware import MoonstreamHTTPException
|
|||
from ..reporter import reporter
|
||||
from ..settings import (
|
||||
BUGOUT_REQUEST_TIMEOUT_SECONDS,
|
||||
BUGOUT_RESOURCE_TYPE_ENTITY_SUBSCRIPTION,
|
||||
MOONSTREAM_ADMIN_ACCESS_TOKEN,
|
||||
MOONSTREAM_APPLICATION_ID,
|
||||
MOONSTREAM_CRAWLERS_SERVER_URL,
|
||||
MOONSTREAM_CRAWLERS_SERVER_PORT,
|
||||
MOONSTREAM_CRAWLERS_SERVER_URL,
|
||||
MOONSTREAM_S3_SMARTCONTRACTS_ABI_BUCKET,
|
||||
MOONSTREAM_S3_SMARTCONTRACTS_ABI_PREFIX,
|
||||
BUGOUT_RESOURCE_TYPE_ENTITY_SUBSCRIPTION,
|
||||
)
|
||||
from ..settings import bugout_client as bc, entity_client as ec
|
||||
from ..settings import bugout_client as bc
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -52,27 +50,29 @@ async def add_dashboard_handler(
|
|||
|
||||
subscription_settings = dashboard.subscription_settings
|
||||
|
||||
# Get user collection id
|
||||
# Get user journal (collection) id
|
||||
|
||||
collection_id = actions.get_entity_subscription_collection_id(
|
||||
journal_id = actions.get_entity_subscription_journal_id(
|
||||
resource_type=BUGOUT_RESOURCE_TYPE_ENTITY_SUBSCRIPTION,
|
||||
user_id=user.id,
|
||||
token=MOONSTREAM_ADMIN_ACCESS_TOKEN,
|
||||
)
|
||||
|
||||
subscriprions_list = ec.search_entities(
|
||||
subscriptions_list = bc.search(
|
||||
token=token,
|
||||
collection_id=collection_id,
|
||||
required_field=[f"type:subscription"],
|
||||
journal_id=journal_id,
|
||||
query="tag:type:subscription",
|
||||
limit=1000,
|
||||
representation="entity",
|
||||
)
|
||||
|
||||
# process existing subscriptions with supplied ids
|
||||
|
||||
available_subscriptions_ids: Dict[Union[UUID, str], EntityResponse] = {
|
||||
subscription.entity_id: subscription
|
||||
for subscription in subscriprions_list.entities
|
||||
}
|
||||
available_subscriptions_ids: Dict[Union[UUID, str], BugoutSearchResultAsEntity]
|
||||
for result in subscriptions_list.results:
|
||||
entity = cast(BugoutSearchResultAsEntity, result)
|
||||
entity_url_list = entity.entity_url.split("/")
|
||||
subscription_id = entity_url_list[len(entity_url_list) - 1]
|
||||
available_subscriptions_ids[subscription_id] = entity
|
||||
|
||||
for dashboard_subscription in subscription_settings:
|
||||
if dashboard_subscription.subscription_id in available_subscriptions_ids.keys():
|
||||
|
@ -137,7 +137,7 @@ async def add_dashboard_handler(
|
|||
tags=["subscriptions"],
|
||||
response_model=BugoutResource,
|
||||
)
|
||||
async def delete_subscription_handler(request: Request, dashboard_id: str):
|
||||
async def delete_subscription_handler(request: Request, dashboard_id: str = Path(...)):
|
||||
"""
|
||||
Delete subscriptions.
|
||||
"""
|
||||
|
@ -181,9 +181,9 @@ async def get_dashboards_handler(
|
|||
return resources
|
||||
|
||||
|
||||
@router.get("/{dashboarsd_id}", tags=["dashboards"], response_model=BugoutResource)
|
||||
@router.get("/{dashboard_id}", tags=["dashboards"], response_model=BugoutResource)
|
||||
async def get_dashboard_handler(
|
||||
request: Request, dashboarsd_id: UUID
|
||||
request: Request, dashboard_id: UUID = Path(...)
|
||||
) -> BugoutResource:
|
||||
"""
|
||||
Get user's subscriptions.
|
||||
|
@ -193,7 +193,7 @@ async def get_dashboard_handler(
|
|||
try:
|
||||
resource: BugoutResource = bc.get_resource(
|
||||
token=token,
|
||||
resource_id=dashboarsd_id,
|
||||
resource_id=dashboard_id,
|
||||
timeout=BUGOUT_REQUEST_TIMEOUT_SECONDS,
|
||||
)
|
||||
except BugoutResponseException as e:
|
||||
|
@ -211,7 +211,7 @@ async def get_dashboard_handler(
|
|||
@router.put("/{dashboard_id}", tags=["dashboards"], response_model=BugoutResource)
|
||||
async def update_dashboard_handler(
|
||||
request: Request,
|
||||
dashboard_id: str,
|
||||
dashboard_id: str = Path(...),
|
||||
dashboard: data.DashboardUpdate = Body(...),
|
||||
) -> BugoutResource:
|
||||
"""
|
||||
|
@ -224,25 +224,28 @@ async def update_dashboard_handler(
|
|||
|
||||
subscription_settings = dashboard.subscription_settings
|
||||
|
||||
# Get user collection id
|
||||
# Get user journal (collection) id
|
||||
|
||||
collection_id = actions.get_entity_subscription_collection_id(
|
||||
journal_id = actions.get_entity_subscription_journal_id(
|
||||
resource_type=BUGOUT_RESOURCE_TYPE_ENTITY_SUBSCRIPTION,
|
||||
user_id=user.id,
|
||||
token=MOONSTREAM_ADMIN_ACCESS_TOKEN,
|
||||
)
|
||||
|
||||
subscriprions_list = ec.search_entities(
|
||||
subscriptions_list = bc.search(
|
||||
token=token,
|
||||
collection_id=collection_id,
|
||||
required_field=[f"type:subscription"],
|
||||
journal_id=journal_id,
|
||||
query="tag:type:subscription",
|
||||
limit=1000,
|
||||
representation="entity",
|
||||
)
|
||||
|
||||
available_subscriptions_ids: Dict[Union[UUID, str], EntityResponse] = {
|
||||
subscription.entity_id: subscription
|
||||
for subscription in subscriprions_list.entities
|
||||
}
|
||||
available_subscriptions_ids: Dict[Union[UUID, str], BugoutSearchResultAsEntity]
|
||||
for result in subscriptions_list.results:
|
||||
entity = cast(BugoutSearchResultAsEntity, result)
|
||||
entity_url_list = entity.entity_url.split("/")
|
||||
subscription_id = entity_url_list[len(entity_url_list) - 1]
|
||||
available_subscriptions_ids[subscription_id] = entity
|
||||
|
||||
for dashboard_subscription in subscription_settings:
|
||||
if dashboard_subscription.subscription_id in available_subscriptions_ids:
|
||||
|
@ -259,12 +262,10 @@ async def update_dashboard_handler(
|
|||
status_code=404,
|
||||
detail=f"Error on dashboard resource {dashboard_subscription.subscription_id} does not have an abi",
|
||||
)
|
||||
|
||||
abi = json.loads(
|
||||
available_subscriptions_ids[
|
||||
dashboard_subscription.subscription_id
|
||||
].secondary_fields.get("abi")
|
||||
)
|
||||
abi_raw = available_subscriptions_ids[
|
||||
dashboard_subscription.subscription_id
|
||||
].secondary_fields.get("abi")
|
||||
abi = json.loads(abi_raw if abi_raw is not None else "")
|
||||
|
||||
actions.dashboards_abi_validation(dashboard_subscription, abi)
|
||||
|
||||
|
@ -301,7 +302,7 @@ async def update_dashboard_handler(
|
|||
|
||||
@router.get("/{dashboard_id}/stats", tags=["dashboards"])
|
||||
async def get_dashboard_data_links_handler(
|
||||
request: Request, dashboard_id: str
|
||||
request: Request, dashboard_id: str = Path(...)
|
||||
) -> Dict[Union[UUID, str], Any]:
|
||||
"""
|
||||
Get s3 presign urls for dashboard grafics
|
||||
|
@ -328,20 +329,21 @@ async def get_dashboard_data_links_handler(
|
|||
|
||||
# get subscriptions
|
||||
|
||||
collection_id = actions.get_entity_subscription_collection_id(
|
||||
journal_id = actions.get_entity_subscription_journal_id(
|
||||
resource_type=BUGOUT_RESOURCE_TYPE_ENTITY_SUBSCRIPTION,
|
||||
user_id=user.id,
|
||||
token=MOONSTREAM_ADMIN_ACCESS_TOKEN,
|
||||
)
|
||||
|
||||
subscriprions_list = ec.search_entities(
|
||||
subscriptions_list = bc.search(
|
||||
token=token,
|
||||
collection_id=collection_id,
|
||||
required_field=[f"type:subscription"],
|
||||
journal_id=journal_id,
|
||||
query="tag:type:subscription",
|
||||
limit=1000,
|
||||
representation="entity",
|
||||
)
|
||||
|
||||
# filter out dasboards
|
||||
# filter out dashboards
|
||||
|
||||
subscriptions_ids = [
|
||||
subscription_meta["subscription_id"]
|
||||
|
@ -350,11 +352,13 @@ async def get_dashboard_data_links_handler(
|
|||
]
|
||||
]
|
||||
|
||||
dashboard_subscriptions: Dict[Union[UUID, str], EntitiesResponse] = {
|
||||
subscription.entity_id: subscription
|
||||
for subscription in subscriprions_list.entities
|
||||
if str(subscription.entity_id) in subscriptions_ids
|
||||
}
|
||||
dashboard_subscriptions: Dict[Union[UUID, str], BugoutSearchResultAsEntity]
|
||||
for result in subscriptions_list.results:
|
||||
entity = cast(BugoutSearchResultAsEntity, result)
|
||||
entity_url_list = entity.entity_url.split("/")
|
||||
subscription_id = entity_url_list[len(entity_url_list) - 1]
|
||||
if str(subscription_id) in subscriptions_ids:
|
||||
dashboard_subscriptions[subscription_id] = entity
|
||||
|
||||
# generate s3 links
|
||||
|
||||
|
|
|
@ -1,40 +1,43 @@
|
|||
"""
|
||||
The Moonstream queries HTTP API
|
||||
"""
|
||||
from datetime import datetime
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List, Optional, Tuple, Union, cast
|
||||
from uuid import UUID
|
||||
|
||||
|
||||
from bugout.data import BugoutResources, BugoutJournalEntryContent, BugoutJournalEntry
|
||||
from bugout.exceptions import BugoutResponseException
|
||||
from fastapi import APIRouter, Body, Request
|
||||
import requests # type: ignore
|
||||
from bugout.data import (
|
||||
BugoutJournalEntry,
|
||||
BugoutJournalEntryContent,
|
||||
BugoutResources,
|
||||
BugoutSearchResult,
|
||||
)
|
||||
from bugout.exceptions import BugoutResponseException
|
||||
from fastapi import APIRouter, Body, Path, Request
|
||||
from moonstreamdb.blockchain import AvailableBlockchainType
|
||||
from sqlalchemy import text
|
||||
|
||||
|
||||
from .. import data
|
||||
from ..actions import (
|
||||
NameNormalizationException,
|
||||
generate_s3_access_links,
|
||||
get_query_by_name,
|
||||
name_normalization,
|
||||
NameNormalizationException,
|
||||
query_parameter_hash,
|
||||
generate_s3_access_links,
|
||||
)
|
||||
from ..middleware import MoonstreamHTTPException
|
||||
from ..settings import (
|
||||
MOONSTREAM_ADMIN_ACCESS_TOKEN,
|
||||
MOONSTREAM_APPLICATION_ID,
|
||||
MOONSTREAM_CRAWLERS_SERVER_URL,
|
||||
MOONSTREAM_CRAWLERS_SERVER_PORT,
|
||||
MOONSTREAM_CRAWLERS_SERVER_URL,
|
||||
MOONSTREAM_QUERIES_JOURNAL_ID,
|
||||
MOONSTREAM_QUERY_TEMPLATE_CONTEXT_TYPE,
|
||||
MOONSTREAM_S3_QUERIES_BUCKET,
|
||||
MOONSTREAM_S3_QUERIES_BUCKET_PREFIX,
|
||||
MOONSTREAM_QUERIES_JOURNAL_ID,
|
||||
)
|
||||
from ..settings import bugout_client as bc, MOONSTREAM_QUERY_TEMPLATE_CONTEXT_TYPE
|
||||
|
||||
from ..settings import bugout_client as bc
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -199,7 +202,9 @@ def get_suggested_queries(
|
|||
|
||||
interfaces: Dict[str, Any] = {}
|
||||
|
||||
for entry in queries.results:
|
||||
queries_results = cast(List[BugoutSearchResult], queries.results)
|
||||
|
||||
for entry in queries_results:
|
||||
for tag in entry.tags:
|
||||
if tag.startswith("interface:"):
|
||||
interface = tag.split(":")[1]
|
||||
|
@ -210,7 +215,7 @@ def get_suggested_queries(
|
|||
interfaces[interface].append(entry)
|
||||
|
||||
return data.SuggestedQueriesResponse(
|
||||
queries=queries.results,
|
||||
queries=queries_results,
|
||||
interfaces=interfaces,
|
||||
)
|
||||
|
||||
|
@ -232,7 +237,6 @@ async def get_query_handler(
|
|||
)
|
||||
|
||||
# check in templates
|
||||
|
||||
try:
|
||||
entries = bc.search(
|
||||
token=MOONSTREAM_ADMIN_ACCESS_TOKEN,
|
||||
|
@ -277,9 +281,11 @@ async def get_query_handler(
|
|||
status_code=403, detail="Query not approved yet."
|
||||
)
|
||||
else:
|
||||
query_id = entries.results[0].entry_url.split("/")[-1]
|
||||
entries_results = cast(List[BugoutSearchResult], entries.results)
|
||||
query_id = entries_results[0].entry_url.split("/")[-1]
|
||||
|
||||
entry = entries.results[0]
|
||||
entries_results = cast(List[BugoutSearchResult], entries.results)
|
||||
entry = entries_results[0]
|
||||
|
||||
try:
|
||||
if entry.content is None:
|
||||
|
@ -390,7 +396,6 @@ async def update_query_data_handler(
|
|||
)
|
||||
|
||||
# check in templates
|
||||
|
||||
try:
|
||||
entries = bc.search(
|
||||
token=MOONSTREAM_ADMIN_ACCESS_TOKEN,
|
||||
|
@ -435,14 +440,16 @@ async def update_query_data_handler(
|
|||
status_code=403, detail="Query not approved yet."
|
||||
)
|
||||
else:
|
||||
query_id = entries.results[0].entry_url.split("/")[-1]
|
||||
entries_results = cast(List[BugoutSearchResult], entries.results)
|
||||
query_id = entries_results[0].entry_url.split("/")[-1]
|
||||
|
||||
s3_response = None
|
||||
|
||||
if entries.results[0].content:
|
||||
content = entries.results[0].content
|
||||
entries_results = cast(List[BugoutSearchResult], entries.results)
|
||||
if entries_results[0].content:
|
||||
content = entries_results[0].content
|
||||
|
||||
tags = entries.results[0].tags
|
||||
tags = entries_results[0].tags
|
||||
|
||||
file_type = "json"
|
||||
|
||||
|
@ -497,7 +504,6 @@ async def get_access_link_handler(
|
|||
)
|
||||
|
||||
# check in templattes
|
||||
|
||||
try:
|
||||
entries = bc.search(
|
||||
token=MOONSTREAM_ADMIN_ACCESS_TOKEN,
|
||||
|
@ -540,13 +546,14 @@ async def get_access_link_handler(
|
|||
status_code=403, detail="Query not approved yet."
|
||||
)
|
||||
|
||||
entries_results = cast(List[BugoutSearchResult], entries.results)
|
||||
try:
|
||||
s3_response = None
|
||||
|
||||
if entries.results[0].content:
|
||||
if entries_results[0].content:
|
||||
passed_params = dict(request_update.params)
|
||||
|
||||
tags = entries.results[0].tags
|
||||
tags = entries_results[0].tags
|
||||
|
||||
file_type = "json"
|
||||
|
||||
|
|
|
@ -1,43 +1,39 @@
|
|||
"""
|
||||
The Moonstream subscriptions HTTP API
|
||||
"""
|
||||
from concurrent.futures import as_completed, ProcessPoolExecutor, ThreadPoolExecutor
|
||||
import hashlib
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional
|
||||
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor, as_completed
|
||||
from typing import Any, Dict, List, Optional, Union, cast
|
||||
|
||||
from bugout.data import BugoutSearchResult, BugoutSearchResultAsEntity
|
||||
from bugout.exceptions import BugoutResponseException
|
||||
from bugout.data import BugoutSearchResult
|
||||
from fastapi import APIRouter, Depends, Request, Form, BackgroundTasks
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, Form, Path, Query, Request
|
||||
from moonstreamdb.blockchain import AvailableBlockchainType
|
||||
from web3 import Web3
|
||||
|
||||
from .. import data
|
||||
from ..actions import (
|
||||
AddressNotSmartContractException,
|
||||
validate_abi_json,
|
||||
EntityJournalNotFoundException,
|
||||
apply_moonworm_tasks,
|
||||
get_entity_subscription_collection_id,
|
||||
EntityCollectionNotFoundException,
|
||||
check_if_smart_contract,
|
||||
get_entity_subscription_journal_id,
|
||||
get_list_of_support_interfaces,
|
||||
get_moonworm_tasks,
|
||||
validate_abi_json,
|
||||
)
|
||||
from ..admin import subscription_types
|
||||
from .. import data
|
||||
from ..admin import subscription_types
|
||||
from ..middleware import MoonstreamHTTPException
|
||||
from ..reporter import reporter
|
||||
from ..settings import bugout_client as bc, entity_client as ec
|
||||
from ..settings import (
|
||||
MOONSTREAM_ADMIN_ACCESS_TOKEN,
|
||||
MOONSTREAM_ENTITIES_RESERVED_TAGS,
|
||||
THREAD_TIMEOUT_SECONDS,
|
||||
)
|
||||
from ..web3_provider import (
|
||||
yield_web3_provider,
|
||||
)
|
||||
|
||||
from ..settings import bugout_client as bc
|
||||
from ..web3_provider import yield_web3_provider
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -141,7 +137,7 @@ async def add_subscription_handler(
|
|||
if description:
|
||||
content["description"] = description
|
||||
|
||||
allowed_required_fields = []
|
||||
allowed_required_fields: List[Any] = []
|
||||
if tags:
|
||||
allowed_required_fields = [
|
||||
item
|
||||
|
@ -149,7 +145,7 @@ async def add_subscription_handler(
|
|||
if not any(key in item for key in MOONSTREAM_ENTITIES_RESERVED_TAGS)
|
||||
]
|
||||
|
||||
required_fields = [
|
||||
required_fields: List[Dict[str, Union[str, bool, int, List[Any]]]] = [
|
||||
{"type": "subscription"},
|
||||
{"subscription_type_id": f"{subscription_type_id}"},
|
||||
{"color": f"{color}"},
|
||||
|
@ -161,53 +157,58 @@ async def add_subscription_handler(
|
|||
required_fields.extend(allowed_required_fields)
|
||||
|
||||
try:
|
||||
collection_id = get_entity_subscription_collection_id(
|
||||
journal_id = get_entity_subscription_journal_id(
|
||||
resource_type=BUGOUT_RESOURCE_TYPE_ENTITY_SUBSCRIPTION,
|
||||
token=MOONSTREAM_ADMIN_ACCESS_TOKEN,
|
||||
user_id=user.id,
|
||||
create_if_not_exist=True,
|
||||
)
|
||||
|
||||
entity = ec.add_entity(
|
||||
blockchain = subscription_types.CANONICAL_SUBSCRIPTION_TYPES[
|
||||
subscription_type_id
|
||||
].blockchain
|
||||
entity = bc.create_entity(
|
||||
token=token,
|
||||
collection_id=collection_id,
|
||||
journal_id=journal_id,
|
||||
address=address,
|
||||
blockchain=subscription_types.CANONICAL_SUBSCRIPTION_TYPES[
|
||||
subscription_type_id
|
||||
].blockchain,
|
||||
name=label,
|
||||
blockchain=blockchain if blockchain is not None else "",
|
||||
title=label,
|
||||
required_fields=required_fields,
|
||||
secondary_fields=content,
|
||||
)
|
||||
except EntityCollectionNotFoundException as e:
|
||||
except EntityJournalNotFoundException as e:
|
||||
raise MoonstreamHTTPException(
|
||||
status_code=404,
|
||||
detail="User subscriptions collection not found",
|
||||
detail="User subscriptions journal not found",
|
||||
internal_error=e,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get collection id")
|
||||
logger.error(f"Failed to get journal id")
|
||||
raise MoonstreamHTTPException(
|
||||
status_code=500,
|
||||
internal_error=e,
|
||||
detail="Currently unable to get collection id",
|
||||
detail="Currently unable to get journal id",
|
||||
)
|
||||
|
||||
entity_required_fields = (
|
||||
entity.required_fields if entity.required_fields is not None else []
|
||||
)
|
||||
entity_secondary_fields = (
|
||||
entity.secondary_fields if entity.secondary_fields is not None else {}
|
||||
)
|
||||
normalized_entity_tags = [
|
||||
f"{key}:{value}"
|
||||
for tag in entity.required_fields
|
||||
for tag in entity_required_fields
|
||||
for key, value in tag.items()
|
||||
if key not in MOONSTREAM_ENTITIES_RESERVED_TAGS
|
||||
]
|
||||
|
||||
return data.SubscriptionResourceData(
|
||||
id=str(entity.entity_id),
|
||||
id=str(entity.id),
|
||||
user_id=str(user.id),
|
||||
address=address,
|
||||
color=color,
|
||||
label=label,
|
||||
abi=entity.secondary_fields.get("abi"),
|
||||
description=entity.secondary_fields.get("description"),
|
||||
abi=entity_secondary_fields.get("abi"),
|
||||
description=entity_secondary_fields.get("description"),
|
||||
tags=normalized_entity_tags,
|
||||
subscription_type_id=subscription_type_id,
|
||||
updated_at=entity.updated_at,
|
||||
|
@ -220,28 +221,29 @@ async def add_subscription_handler(
|
|||
tags=["subscriptions"],
|
||||
response_model=data.SubscriptionResourceData,
|
||||
)
|
||||
async def delete_subscription_handler(request: Request, subscription_id: str):
|
||||
async def delete_subscription_handler(
|
||||
request: Request, subscription_id: str = Path(...)
|
||||
):
|
||||
"""
|
||||
Delete subscriptions.
|
||||
"""
|
||||
token = request.state.token
|
||||
user = request.state.user
|
||||
try:
|
||||
collection_id = get_entity_subscription_collection_id(
|
||||
journal_id = get_entity_subscription_journal_id(
|
||||
resource_type=BUGOUT_RESOURCE_TYPE_ENTITY_SUBSCRIPTION,
|
||||
token=MOONSTREAM_ADMIN_ACCESS_TOKEN,
|
||||
user_id=user.id,
|
||||
)
|
||||
|
||||
deleted_entity = ec.delete_entity(
|
||||
deleted_entity = bc.delete_entity(
|
||||
token=token,
|
||||
collection_id=collection_id,
|
||||
journal_id=journal_id,
|
||||
entity_id=subscription_id,
|
||||
)
|
||||
except EntityCollectionNotFoundException as e:
|
||||
except EntityJournalNotFoundException as e:
|
||||
raise MoonstreamHTTPException(
|
||||
status_code=404,
|
||||
detail="User subscriptions collection not found",
|
||||
detail="User subscriptions journal not found",
|
||||
internal_error=e,
|
||||
)
|
||||
except Exception as e:
|
||||
|
@ -251,36 +253,46 @@ async def delete_subscription_handler(request: Request, subscription_id: str):
|
|||
detail="Internal error",
|
||||
)
|
||||
|
||||
tags = deleted_entity.required_fields
|
||||
tags_raw = (
|
||||
deleted_entity.required_fields
|
||||
if deleted_entity.required_fields is not None
|
||||
else {}
|
||||
)
|
||||
|
||||
subscription_type_id = None
|
||||
color = None
|
||||
label = None
|
||||
abi = None
|
||||
description = None
|
||||
|
||||
if tags is not None:
|
||||
for tag in tags:
|
||||
if "subscription_type_id" in tag:
|
||||
subscription_type_id = tag["subscription_type_id"]
|
||||
for tag in tags_raw:
|
||||
if "subscription_type_id" in tag:
|
||||
subscription_type_id = tag["subscription_type_id"]
|
||||
if "color" in tag:
|
||||
color = tag["color"]
|
||||
if "label" in tag:
|
||||
label = tag["label"]
|
||||
|
||||
if "color" in tag:
|
||||
color = tag["color"]
|
||||
|
||||
if "label" in tag:
|
||||
label = tag["label"]
|
||||
normalized_entity_tags = [
|
||||
f"{key}:{value}"
|
||||
for tag in tags_raw
|
||||
for key, value in tag.items()
|
||||
if key not in MOONSTREAM_ENTITIES_RESERVED_TAGS
|
||||
]
|
||||
|
||||
if deleted_entity.secondary_fields is not None:
|
||||
abi = deleted_entity.secondary_fields.get("abi")
|
||||
description = deleted_entity.secondary_fields.get("description")
|
||||
|
||||
return data.SubscriptionResourceData(
|
||||
id=str(deleted_entity.entity_id),
|
||||
id=str(deleted_entity.id),
|
||||
user_id=str(user.id),
|
||||
address=deleted_entity.address,
|
||||
color=color,
|
||||
label=label,
|
||||
abi=abi,
|
||||
description=deleted_entity.secondary_fields.get("description"),
|
||||
tags=deleted_entity.required_fields,
|
||||
description=description,
|
||||
tags=normalized_entity_tags,
|
||||
subscription_type_id=subscription_type_id,
|
||||
updated_at=deleted_entity.updated_at,
|
||||
created_at=deleted_entity.created_at,
|
||||
|
@ -290,8 +302,8 @@ async def delete_subscription_handler(request: Request, subscription_id: str):
|
|||
@router.get("/", tags=["subscriptions"], response_model=data.SubscriptionsListResponse)
|
||||
async def get_subscriptions_handler(
|
||||
request: Request,
|
||||
limit: Optional[int] = 10,
|
||||
offset: Optional[int] = 0,
|
||||
limit: int = Query(10),
|
||||
offset: int = Query(0),
|
||||
) -> data.SubscriptionsListResponse:
|
||||
"""
|
||||
Get user's subscriptions.
|
||||
|
@ -299,25 +311,25 @@ async def get_subscriptions_handler(
|
|||
token = request.state.token
|
||||
user = request.state.user
|
||||
try:
|
||||
collection_id = get_entity_subscription_collection_id(
|
||||
journal_id = get_entity_subscription_journal_id(
|
||||
resource_type=BUGOUT_RESOURCE_TYPE_ENTITY_SUBSCRIPTION,
|
||||
token=MOONSTREAM_ADMIN_ACCESS_TOKEN,
|
||||
user_id=user.id,
|
||||
create_if_not_exist=True,
|
||||
)
|
||||
|
||||
subscriprions_list = ec.search_entities(
|
||||
subscriptions_list: Any = bc.search(
|
||||
token=token,
|
||||
collection_id=collection_id,
|
||||
required_field=[f"type:subscription"],
|
||||
journal_id=journal_id,
|
||||
query="tag:type:subscription",
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
representation="entity",
|
||||
)
|
||||
|
||||
except EntityCollectionNotFoundException as e:
|
||||
except EntityJournalNotFoundException as e:
|
||||
raise MoonstreamHTTPException(
|
||||
status_code=404,
|
||||
detail="User subscriptions collection not found",
|
||||
detail="User subscriptions journal not found",
|
||||
internal_error=e,
|
||||
)
|
||||
except Exception as e:
|
||||
|
@ -329,7 +341,11 @@ async def get_subscriptions_handler(
|
|||
|
||||
subscriptions = []
|
||||
|
||||
for subscription in subscriprions_list.entities:
|
||||
user_subscriptions_results = cast(
|
||||
List[BugoutSearchResultAsEntity], subscriptions_list.results
|
||||
)
|
||||
|
||||
for subscription in user_subscriptions_results:
|
||||
tags = subscription.required_fields
|
||||
|
||||
label, color, subscription_type_id = None, None, None
|
||||
|
@ -353,7 +369,7 @@ async def get_subscriptions_handler(
|
|||
|
||||
subscriptions.append(
|
||||
data.SubscriptionResourceData(
|
||||
id=str(subscription.entity_id),
|
||||
id=str(subscription.entity_url.split("/")[-1]),
|
||||
user_id=str(user.id),
|
||||
address=subscription.address,
|
||||
color=color,
|
||||
|
@ -379,8 +395,8 @@ async def get_subscriptions_handler(
|
|||
)
|
||||
async def update_subscriptions_handler(
|
||||
request: Request,
|
||||
subscription_id: str,
|
||||
background_tasks: BackgroundTasks,
|
||||
subscription_id: str = Path(...),
|
||||
) -> data.SubscriptionResourceData:
|
||||
"""
|
||||
Get user's subscriptions.
|
||||
|
@ -402,46 +418,51 @@ async def update_subscriptions_handler(
|
|||
tags = form_data.tags
|
||||
|
||||
try:
|
||||
collection_id = get_entity_subscription_collection_id(
|
||||
journal_id = get_entity_subscription_journal_id(
|
||||
resource_type=BUGOUT_RESOURCE_TYPE_ENTITY_SUBSCRIPTION,
|
||||
token=MOONSTREAM_ADMIN_ACCESS_TOKEN,
|
||||
user_id=user.id,
|
||||
)
|
||||
|
||||
# get subscription entity
|
||||
subscription_entity = ec.get_entity(
|
||||
subscription_entity = bc.get_entity(
|
||||
token=token,
|
||||
collection_id=collection_id,
|
||||
journal_id=journal_id,
|
||||
entity_id=subscription_id,
|
||||
)
|
||||
|
||||
update_required_fields = []
|
||||
if subscription_entity.required_fields is not None:
|
||||
update_required_fields = [
|
||||
field
|
||||
for field in subscription_entity.required_fields
|
||||
if any(key in field for key in MOONSTREAM_ENTITIES_RESERVED_TAGS)
|
||||
]
|
||||
|
||||
update_secondary_fields = (
|
||||
subscription_entity.secondary_fields
|
||||
if subscription_entity.secondary_fields is not None
|
||||
else {}
|
||||
)
|
||||
|
||||
subscription_type_id = None
|
||||
|
||||
update_required_fields = [
|
||||
field
|
||||
for field in subscription_entity.required_fields
|
||||
if any(key in field for key in MOONSTREAM_ENTITIES_RESERVED_TAGS)
|
||||
]
|
||||
|
||||
update_secondary_fields = subscription_entity.secondary_fields
|
||||
|
||||
for field in update_required_fields:
|
||||
if "subscription_type_id" in field:
|
||||
subscription_type_id = field["subscription_type_id"]
|
||||
|
||||
if not subscription_type_id:
|
||||
logger.error(
|
||||
f"Subscription entity {subscription_id} in collection {collection_id} has no subscription_type_id malformed subscription entity"
|
||||
f"Subscription entity {subscription_id} in journal (collection) {journal_id} has no subscription_type_id malformed subscription entity"
|
||||
)
|
||||
raise MoonstreamHTTPException(
|
||||
status_code=409,
|
||||
detail="Not valid subscription entity",
|
||||
)
|
||||
|
||||
except EntityCollectionNotFoundException as e:
|
||||
except EntityJournalNotFoundException as e:
|
||||
raise MoonstreamHTTPException(
|
||||
status_code=404,
|
||||
detail="User subscriptions collection not found",
|
||||
detail="User subscriptions journal not found",
|
||||
internal_error=e,
|
||||
)
|
||||
except Exception as e:
|
||||
|
@ -488,18 +509,27 @@ async def update_subscriptions_handler(
|
|||
|
||||
if allowed_required_fields:
|
||||
update_required_fields.extend(allowed_required_fields)
|
||||
|
||||
address = subscription_entity.address
|
||||
if address is None:
|
||||
logger.error(f"Lost address at entity {subscription_id} for subscription")
|
||||
raise MoonstreamHTTPException(status_code=500)
|
||||
|
||||
try:
|
||||
subscription = ec.update_entity(
|
||||
subscription = bc.update_entity(
|
||||
token=token,
|
||||
collection_id=collection_id,
|
||||
journal_id=journal_id,
|
||||
entity_id=subscription_id,
|
||||
address=subscription_entity.address,
|
||||
blockchain=subscription_entity.blockchain,
|
||||
name=subscription_entity.name,
|
||||
title=subscription_entity.title
|
||||
if subscription_entity.title is not None
|
||||
else "",
|
||||
address=address,
|
||||
blockchain=subscription_entity.blockchain
|
||||
if subscription_entity.blockchain is not None
|
||||
else "",
|
||||
required_fields=update_required_fields,
|
||||
secondary_fields=update_secondary_fields,
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error update user subscriptions: {str(e)}")
|
||||
raise MoonstreamHTTPException(status_code=500, internal_error=e)
|
||||
|
@ -509,24 +539,32 @@ async def update_subscriptions_handler(
|
|||
apply_moonworm_tasks,
|
||||
subscription_type_id,
|
||||
json_abi,
|
||||
subscription.address,
|
||||
address,
|
||||
)
|
||||
subscription_required_fields = (
|
||||
subscription.required_fields if subscription.required_fields is not None else {}
|
||||
)
|
||||
subscription_secondary_fields = (
|
||||
subscription.secondary_fields
|
||||
if subscription.secondary_fields is not None
|
||||
else {}
|
||||
)
|
||||
|
||||
normalized_entity_tags = [
|
||||
f"{key}:{value}"
|
||||
for tag in subscription.required_fields
|
||||
for tag in subscription_required_fields
|
||||
for key, value in tag.items()
|
||||
if key not in MOONSTREAM_ENTITIES_RESERVED_TAGS
|
||||
]
|
||||
|
||||
return data.SubscriptionResourceData(
|
||||
id=str(subscription.entity_id),
|
||||
id=str(subscription.id),
|
||||
user_id=str(user.id),
|
||||
address=subscription.address,
|
||||
color=color,
|
||||
label=label,
|
||||
abi=subscription.secondary_fields.get("abi"),
|
||||
description=subscription.secondary_fields.get("description"),
|
||||
abi=subscription_secondary_fields.get("abi"),
|
||||
description=subscription_secondary_fields.get("description"),
|
||||
tags=normalized_entity_tags,
|
||||
subscription_type_id=subscription_type_id,
|
||||
updated_at=subscription_entity.updated_at,
|
||||
|
@ -537,43 +575,47 @@ async def update_subscriptions_handler(
|
|||
@router.get(
|
||||
"/{subscription_id}/abi",
|
||||
tags=["subscriptions"],
|
||||
response_model=data.SubdcriptionsAbiResponse,
|
||||
response_model=data.SubscriptionsAbiResponse,
|
||||
)
|
||||
async def get_subscription_abi_handler(
|
||||
request: Request,
|
||||
subscription_id: str,
|
||||
) -> data.SubdcriptionsAbiResponse:
|
||||
subscription_id: str = Path(...),
|
||||
) -> data.SubscriptionsAbiResponse:
|
||||
token = request.state.token
|
||||
user = request.state.user
|
||||
|
||||
try:
|
||||
collection_id = get_entity_subscription_collection_id(
|
||||
journal_id = get_entity_subscription_journal_id(
|
||||
resource_type=BUGOUT_RESOURCE_TYPE_ENTITY_SUBSCRIPTION,
|
||||
token=MOONSTREAM_ADMIN_ACCESS_TOKEN,
|
||||
user_id=user.id,
|
||||
)
|
||||
|
||||
# get subscription entity
|
||||
subscription_resource = ec.get_entity(
|
||||
subscription_resource = bc.get_entity(
|
||||
token=token,
|
||||
collection_id=collection_id,
|
||||
journal_id=journal_id,
|
||||
entity_id=subscription_id,
|
||||
)
|
||||
|
||||
except EntityCollectionNotFoundException as e:
|
||||
except EntityJournalNotFoundException as e:
|
||||
raise MoonstreamHTTPException(
|
||||
status_code=404,
|
||||
detail="User subscriptions collection not found",
|
||||
detail="User subscriptions journal not found",
|
||||
internal_error=e,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error get subscriptions for user ({user}), error: {str(e)}")
|
||||
raise MoonstreamHTTPException(status_code=500, internal_error=e)
|
||||
|
||||
if subscription_resource.secondary_fields is None:
|
||||
raise MoonstreamHTTPException(
|
||||
status_code=500, detail=f"Malformed subscription entity {subscription_id}"
|
||||
)
|
||||
if "abi" not in subscription_resource.secondary_fields.keys():
|
||||
raise MoonstreamHTTPException(status_code=404, detail="Abi not found")
|
||||
|
||||
return data.SubdcriptionsAbiResponse(
|
||||
return data.SubscriptionsAbiResponse(
|
||||
abi=subscription_resource.secondary_fields["abi"]
|
||||
)
|
||||
|
||||
|
@ -614,29 +656,27 @@ async def get_subscription_jobs_handler(
|
|||
user = request.state.user
|
||||
|
||||
try:
|
||||
collection_id = get_entity_subscription_collection_id(
|
||||
journal_id = get_entity_subscription_journal_id(
|
||||
resource_type=BUGOUT_RESOURCE_TYPE_ENTITY_SUBSCRIPTION,
|
||||
token=MOONSTREAM_ADMIN_ACCESS_TOKEN,
|
||||
user_id=user.id,
|
||||
)
|
||||
|
||||
# get subscription entity
|
||||
subscription_resource = ec.get_entity(
|
||||
subscription_resource = bc.get_entity(
|
||||
token=token,
|
||||
collection_id=collection_id,
|
||||
journal_id=journal_id,
|
||||
entity_id=subscription_id,
|
||||
)
|
||||
|
||||
except EntityCollectionNotFoundException as e:
|
||||
except EntityJournalNotFoundException as e:
|
||||
raise MoonstreamHTTPException(
|
||||
status_code=404,
|
||||
detail="User subscriptions collection not found",
|
||||
detail="User subscriptions journal not found",
|
||||
internal_error=e,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Error get subscriptions for user ({user}) with token ({token}), error: {str(e)}"
|
||||
)
|
||||
logger.error(f"Error get subscriptions for user ({user}), error: {str(e)}")
|
||||
raise MoonstreamHTTPException(status_code=500, internal_error=e)
|
||||
|
||||
for field in subscription_resource.required_fields:
|
||||
|
@ -659,7 +699,7 @@ async def get_subscription_jobs_handler(
|
|||
tags=["subscriptions"],
|
||||
response_model=data.ContractInfoResponse,
|
||||
)
|
||||
async def address_info(request: Request, address: str):
|
||||
async def address_info(request: Request, address: str = Query(...)):
|
||||
"""
|
||||
Looking if address is contract
|
||||
"""
|
||||
|
@ -722,8 +762,8 @@ async def address_info(request: Request, address: str):
|
|||
)
|
||||
def get_contract_interfaces(
|
||||
request: Request,
|
||||
address: str,
|
||||
blockchain: str,
|
||||
address: str = Query(...),
|
||||
blockchain: str = Query(...),
|
||||
):
|
||||
"""
|
||||
Request contract interfaces from web3
|
||||
|
|
|
@ -10,7 +10,7 @@ from bugout.exceptions import BugoutResponseException
|
|||
from fastapi import APIRouter, Body, Form, Request
|
||||
|
||||
from .. import data
|
||||
from ..actions import create_onboarding_resource, generate_collection_for_user
|
||||
from ..actions import create_onboarding_resource
|
||||
from ..middleware import MoonstreamHTTPException
|
||||
from ..settings import BUGOUT_REQUEST_TIMEOUT_SECONDS, MOONSTREAM_APPLICATION_ID
|
||||
from ..settings import bugout_client as bc
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
selectors = {
|
||||
from typing import Any, Dict
|
||||
|
||||
selectors: Dict[str, Any] = {
|
||||
"274c7b3c": {
|
||||
"name": "ERC20PresetMinterPauser",
|
||||
"selector": "274c7b3c",
|
||||
|
|
|
@ -1,12 +1,9 @@
|
|||
import os
|
||||
from typing import Optional, Dict
|
||||
from uuid import UUID
|
||||
from typing import Dict, Optional
|
||||
|
||||
from bugout.app import Bugout
|
||||
from entity.client import Entity # type: ignore
|
||||
from moonstreamdb.blockchain import AvailableBlockchainType
|
||||
|
||||
|
||||
# Bugout
|
||||
BUGOUT_BROOD_URL = os.environ.get("BUGOUT_BROOD_URL", "https://auth.bugout.dev")
|
||||
BUGOUT_SPIRE_URL = os.environ.get("BUGOUT_SPIRE_URL", "https://spire.bugout.dev")
|
||||
|
@ -14,15 +11,6 @@ BUGOUT_SPIRE_URL = os.environ.get("BUGOUT_SPIRE_URL", "https://spire.bugout.dev"
|
|||
|
||||
bugout_client = Bugout(brood_api_url=BUGOUT_BROOD_URL, spire_api_url=BUGOUT_SPIRE_URL)
|
||||
|
||||
# Entity
|
||||
|
||||
MOONSTREAM_ENTITY_URL = os.environ.get("MOONSTREAM_ENTITY_URL", "")
|
||||
if MOONSTREAM_ENTITY_URL == "":
|
||||
raise ValueError("MOONSTREAM_ENTITY_URL environment variable must be set")
|
||||
|
||||
entity_client = Entity(MOONSTREAM_ENTITY_URL)
|
||||
|
||||
|
||||
BUGOUT_REQUEST_TIMEOUT_SECONDS = 5
|
||||
|
||||
HUMBUG_REPORTER_BACKEND_TOKEN = os.environ.get("HUMBUG_REPORTER_BACKEND_TOKEN")
|
||||
|
|
|
@ -2,4 +2,4 @@
|
|||
Moonstream library and API version.
|
||||
"""
|
||||
|
||||
MOONSTREAMAPI_VERSION = "0.2.8"
|
||||
MOONSTREAMAPI_VERSION = "0.2.9"
|
||||
|
|
|
@ -8,3 +8,9 @@ ignore_missing_imports = True
|
|||
|
||||
[mypy-pyevmasm.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-requests.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-dateutil.*]
|
||||
ignore_missing_imports = True
|
||||
|
|
|
@ -9,7 +9,7 @@ base58==2.1.1
|
|||
bitarray==2.6.0
|
||||
boto3==1.26.5
|
||||
botocore==1.29.5
|
||||
bugout>=0.2.10
|
||||
bugout>=0.2.13
|
||||
certifi==2022.9.24
|
||||
charset-normalizer==2.1.1
|
||||
click==8.1.3
|
||||
|
@ -36,7 +36,6 @@ jsonschema==4.17.0
|
|||
lru-dict==1.1.8
|
||||
Mako==1.2.3
|
||||
MarkupSafe==2.1.1
|
||||
moonstream-entity==0.0.5
|
||||
moonstreamdb==0.3.4
|
||||
multiaddr==0.0.9
|
||||
multidict==6.0.2
|
||||
|
@ -66,4 +65,4 @@ uvicorn==0.19.0
|
|||
varint==1.0.2
|
||||
web3==5.31.1
|
||||
websockets==9.1
|
||||
yarl==1.8.1
|
||||
yarl==1.8.1
|
||||
|
|
|
@ -13,12 +13,11 @@ setup(
|
|||
install_requires=[
|
||||
"appdirs",
|
||||
"boto3",
|
||||
"bugout>=0.2.10",
|
||||
"moonstream-entity>=0.0.5",
|
||||
"bugout>=0.2.13",
|
||||
"fastapi",
|
||||
"moonstreamdb>=0.3.4",
|
||||
"humbug",
|
||||
"pydantic",
|
||||
"pydantic==1.10.2",
|
||||
"pyevmasm",
|
||||
"python-dateutil",
|
||||
"python-multipart",
|
||||
|
|
Ładowanie…
Reference in New Issue