Merge branch 'main' into xai-crawlers

pull/1016/head
Andrey 2024-02-21 05:11:38 +02:00
commit 49065314ae
25 zmienionych plików z 1160 dodań i 243 usunięć

Wyświetl plik

@ -0,0 +1,28 @@
"""Live at for metatx
Revision ID: 6d07739cb13e
Revises: 71e888082a6d
Create Date: 2023-12-06 14:33:04.814144
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '6d07739cb13e'
down_revision = '71e888082a6d'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('call_requests', sa.Column('live_at', sa.DateTime(timezone=True), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('call_requests', 'live_at')
# ### end Alembic commands ###

Wyświetl plik

@ -0,0 +1,30 @@
"""Tx hash for call requests
Revision ID: 7191eb70e99e
Revises: 6d07739cb13e
Create Date: 2023-10-04 11:23:12.516797
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '7191eb70e99e'
down_revision = '6d07739cb13e'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('call_requests', sa.Column('tx_hash', sa.VARCHAR(length=256), nullable=True))
op.create_unique_constraint(op.f('uq_call_requests_tx_hash'), 'call_requests', ['tx_hash'])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(op.f('uq_call_requests_tx_hash'), 'call_requests', type_='unique')
op.drop_column('call_requests', 'tx_hash')
# ### end Alembic commands ###

Wyświetl plik

@ -0,0 +1,50 @@
"""leaderboard metadata
Revision ID: 71e888082a6d
Revises: cc80e886e153
Create Date: 2023-11-15 13:21:16.108399
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = "71e888082a6d"
down_revision = "cc80e886e153"
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
"leaderboards",
sa.Column(
"blockchain_ids",
sa.ARRAY(sa.Integer()),
nullable=False,
server_default="{}",
),
)
op.add_column(
"leaderboards",
sa.Column(
"wallet_connect", sa.Boolean(), nullable=False, server_default="false"
),
)
op.add_column(
"leaderboards",
sa.Column(
"columns_names", postgresql.JSONB(astext_type=sa.Text()), nullable=True
),
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("leaderboards", "columns_names")
op.drop_column("leaderboards", "wallet_connect")
op.drop_column("leaderboards", "blockchain_ids")
# ### end Alembic commands ###

Wyświetl plik

@ -5,7 +5,13 @@ from typing import List, Any, Optional, Dict, Union, Tuple, cast
import uuid import uuid
import logging import logging
from bugout.data import BugoutResource, BugoutSearchResult from bugout.data import (
BugoutResource,
BugoutSearchResult,
ResourcePermissions,
HolderType,
BugoutResourceHolder,
)
from eth_typing import Address from eth_typing import Address
from hexbytes import HexBytes from hexbytes import HexBytes
import requests # type: ignore import requests # type: ignore
@ -16,7 +22,14 @@ from sqlalchemy.engine import Row
from web3 import Web3 from web3 import Web3
from web3.types import ChecksumAddress from web3.types import ChecksumAddress
from .data import Score, LeaderboardScore, LeaderboardConfigUpdate, LeaderboardConfig from .data import (
Score,
LeaderboardScore,
LeaderboardConfigUpdate,
LeaderboardConfig,
LeaderboardPosition,
ColumnsNames,
)
from .contracts import Dropper_interface, ERC20_interface, Terminus_interface from .contracts import Dropper_interface, ERC20_interface, Terminus_interface
from .models import ( from .models import (
DropperClaimant, DropperClaimant,
@ -96,6 +109,10 @@ class LeaderboardVersionNotFound(Exception):
pass pass
class LeaderboardAssignResourceError(Exception):
pass
BATCH_SIGNATURE_PAGE_SIZE = 500 BATCH_SIGNATURE_PAGE_SIZE = 500
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -1268,7 +1285,7 @@ def get_leaderboard_score(
def get_leaderboard_positions( def get_leaderboard_positions(
db_session: Session, db_session: Session,
leaderboard_id, leaderboard_id: uuid.UUID,
limit: int, limit: int,
offset: int, offset: int,
version_number: Optional[int] = None, version_number: Optional[int] = None,
@ -1481,31 +1498,47 @@ def create_leaderboard(
title: str, title: str,
description: Optional[str], description: Optional[str],
token: Optional[Union[uuid.UUID, str]] = None, token: Optional[Union[uuid.UUID, str]] = None,
wallet_connect: bool = False,
blockchain_ids: List[int] = [],
columns_names: ColumnsNames = None,
) -> Leaderboard: ) -> Leaderboard:
""" """
Create a leaderboard Create a leaderboard
""" """
if columns_names is not None:
columns_names = columns_names.dict()
if not token: if not token:
token = uuid.UUID(MOONSTREAM_ADMIN_ACCESS_TOKEN) token = uuid.UUID(MOONSTREAM_ADMIN_ACCESS_TOKEN)
try: try:
leaderboard = Leaderboard(title=title, description=description) # deduplicate and sort
blockchain_ids = sorted(list(set(blockchain_ids)))
leaderboard = Leaderboard(
title=title,
description=description,
wallet_connect=wallet_connect,
blockchain_ids=blockchain_ids,
columns_names=columns_names,
)
db_session.add(leaderboard) db_session.add(leaderboard)
db_session.commit() db_session.commit()
user = None
if token is not None:
user = bc.get_user(token=token)
resource = create_leaderboard_resource( resource = create_leaderboard_resource(
leaderboard_id=str(leaderboard.id), leaderboard_id=str(leaderboard.id),
token=token, user_id=str(user.id) if user is not None else None,
) )
leaderboard.resource_id = resource.id leaderboard.resource_id = resource.id
db_session.commit() db_session.commit()
except Exception as e: except Exception as e:
db_session.rollback() db_session.rollback()
logger.error(f"Error creating leaderboard: {e}") logger.error(f"Error creating leaderboard: {e}")
raise LeaderboardCreateError(f"Error creating leaderboard: {e}") raise LeaderboardCreateError(f"Error creating leaderboard: {e}")
return leaderboard return leaderboard
@ -1548,6 +1581,9 @@ def update_leaderboard(
leaderboard_id: uuid.UUID, leaderboard_id: uuid.UUID,
title: Optional[str], title: Optional[str],
description: Optional[str], description: Optional[str],
wallet_connect: Optional[bool],
blockchain_ids: Optional[List[int]],
columns_names: Optional[ColumnsNames],
) -> Leaderboard: ) -> Leaderboard:
""" """
Update a leaderboard Update a leaderboard
@ -1561,6 +1597,23 @@ def update_leaderboard(
leaderboard.title = title leaderboard.title = title
if description is not None: if description is not None:
leaderboard.description = description leaderboard.description = description
if wallet_connect is not None:
leaderboard.wallet_connect = wallet_connect
if blockchain_ids is not None:
# deduplicate and sort
blockchain_ids = sorted(list(set(blockchain_ids)))
leaderboard.blockchain_ids = blockchain_ids
if columns_names is not None:
if leaderboard.columns_names is not None:
current_columns_names = ColumnsNames(**leaderboard.columns_names)
for key, value in columns_names.dict(exclude_none=True).items():
setattr(current_columns_names, key, value)
else:
current_columns_names = columns_names
leaderboard.columns_names = current_columns_names.dict()
db_session.commit() db_session.commit()
@ -1659,38 +1712,62 @@ def add_scores(
# leaderboard access actions # leaderboard access actions
def create_leaderboard_resource( def create_leaderboard_resource(leaderboard_id: str, user_id: Optional[str] = None):
leaderboard_id: str, token: Union[Optional[uuid.UUID], str] = None
) -> BugoutResource:
resource_data: Dict[str, Any] = { resource_data: Dict[str, Any] = {
"type": LEADERBOARD_RESOURCE_TYPE, "type": LEADERBOARD_RESOURCE_TYPE,
"leaderboard_id": leaderboard_id, "leaderboard_id": leaderboard_id,
} }
if token is None:
token = MOONSTREAM_ADMIN_ACCESS_TOKEN
try: try:
resource = bc.create_resource( resource = bc.create_resource(
token=token, token=MOONSTREAM_ADMIN_ACCESS_TOKEN,
application_id=MOONSTREAM_APPLICATION_ID, application_id=MOONSTREAM_APPLICATION_ID,
resource_data=resource_data, resource_data=resource_data,
timeout=10, timeout=10,
) )
except Exception as e: except Exception as e:
raise LeaderboardCreateError(f"Error creating leaderboard resource: {e}") raise LeaderboardCreateError(f"Error creating leaderboard resource: {e}")
if user_id is not None:
try:
bc.add_resource_holder_permissions(
token=MOONSTREAM_ADMIN_ACCESS_TOKEN,
resource_id=resource.id,
holder_permissions=BugoutResourceHolder(
holder_type=HolderType.user,
holder_id=user_id,
permissions=[
ResourcePermissions.ADMIN,
ResourcePermissions.READ,
ResourcePermissions.UPDATE,
ResourcePermissions.DELETE,
],
),
)
except Exception as e:
raise LeaderboardCreateError(
f"Error adding resource holder permissions: {e}"
)
return resource return resource
def assign_resource( def assign_resource(
db_session: Session, db_session: Session,
leaderboard_id: uuid.UUID, leaderboard_id: uuid.UUID,
user_token: Union[uuid.UUID, str], user_token: Optional[Union[uuid.UUID, str]] = None,
resource_id: Optional[uuid.UUID] = None, resource_id: Optional[uuid.UUID] = None,
): ):
""" """
Assign a resource handler to a leaderboard Assign a resource handler to a leaderboard
""" """
### get user_name from token
user = None
if user_token is not None:
user = bc.get_user(token=user_token)
leaderboard = ( leaderboard = (
db_session.query(Leaderboard).filter(Leaderboard.id == leaderboard_id).one() # type: ignore db_session.query(Leaderboard).filter(Leaderboard.id == leaderboard_id).one() # type: ignore
) )
@ -1698,11 +1775,9 @@ def assign_resource(
if resource_id is not None: if resource_id is not None:
leaderboard.resource_id = resource_id leaderboard.resource_id = resource_id
else: else:
# Create resource via admin token
resource = create_leaderboard_resource( resource = create_leaderboard_resource(
leaderboard_id=str(leaderboard_id), leaderboard_id=str(leaderboard_id),
token=user_token, user_id=user.id if user is not None else None,
) )
leaderboard.resource_id = resource.id leaderboard.resource_id = resource.id

Wyświetl plik

@ -19,23 +19,17 @@ import argparse
import base64 import base64
import json import json
import time import time
from typing import Any, cast, Dict from typing import Any, Dict, Optional, cast
import eth_keys
from eip712.messages import EIP712Message, _hash_eip191_message from eip712.messages import EIP712Message, _hash_eip191_message
from eth_account import Account from eth_account import Account
from eth_account._utils.signing import sign_message_hash from eth_account._utils.signing import sign_message_hash
import eth_keys from eth_typing import ChecksumAddress
from hexbytes import HexBytes from hexbytes import HexBytes
from web3 import Web3 from web3 import Web3
AUTH_PAYLOAD_NAME = "MoonstreamAuthorization"
AUTH_VERSION = "1"
# By default, authorizations will remain active for 24 hours.
DEFAULT_INTERVAL = 60 * 60 * 24
class MoonstreamAuthorizationVerificationError(Exception): class MoonstreamAuthorizationVerificationError(Exception):
""" """
Raised when invalid signer is provided. Raised when invalid signer is provided.
@ -48,12 +42,48 @@ class MoonstreamAuthorizationExpired(Exception):
""" """
class MoonstreamAuthorization(EIP712Message): class MoonstreamAuthorizationStructureError(Exception):
_name_: "string" """
_version_: "string" Raised when signature has incorrect structure.
"""
address: "address"
deadline: "uint256" class MoonstreamAuthorization(EIP712Message):
_name_: "string" # type: ignore
_version_: "string" # type: ignore
address: "address" # type: ignore
deadline: "uint256" # type: ignore
class MetaTXAuthorization(EIP712Message):
_name_: "string" # type: ignore
_version_: "string" # type: ignore
caller: "address" # type: ignore
expires_at: "uint256" # type: ignore
EIP712_AUTHORIZATION_TYPES = {
"MoonstreamAuthorization": {
"name": "MoonstreamAuthorization",
"version": "1",
"eip712_message_class": MoonstreamAuthorization,
"primary_types": [
{"name": "address", "type": "address"},
{"name": "deadline", "type": int},
],
},
"MetaTXAuthorization": {
"name": "MetaTXAuthorization",
"version": "1",
"eip712_message_class": MetaTXAuthorization,
"primary_types": [
{"name": "caller", "type": "address"},
{"name": "expires_at", "type": int},
],
},
}
def sign_message(message_hash_bytes: HexBytes, private_key: HexBytes) -> HexBytes: def sign_message(message_hash_bytes: HexBytes, private_key: HexBytes) -> HexBytes:
@ -64,52 +94,77 @@ def sign_message(message_hash_bytes: HexBytes, private_key: HexBytes) -> HexByte
return signed_message_bytes return signed_message_bytes
def authorize(deadline: int, address: str, private_key: HexBytes) -> Dict[str, Any]: def authorize(
message = MoonstreamAuthorization( authorization_type: Dict[str, Any],
_name_=AUTH_PAYLOAD_NAME, primary_types: Dict[str, Any],
_version_=AUTH_VERSION, private_key: HexBytes,
address=address, signature_name_output: str,
deadline=deadline, ) -> Dict[str, Any]:
) # Initializing instance of EIP712Message class
attrs: Dict[str, Any] = {
"_name_": authorization_type["name"],
"_version_": authorization_type["version"],
}
attrs.update(primary_types)
message = authorization_type["eip712_message_class"](**attrs)
# Generating message hash and signature
msg_hash_bytes = HexBytes(_hash_eip191_message(message.signable_message)) msg_hash_bytes = HexBytes(_hash_eip191_message(message.signable_message))
signed_message = sign_message(msg_hash_bytes, private_key) signed_message = sign_message(msg_hash_bytes, private_key)
api_payload: Dict[str, Any] = { api_payload: Dict[str, Any] = {signature_name_output: signed_message.hex()}
"address": address, api_payload.update(primary_types)
"deadline": deadline,
"signed_message": signed_message.hex(),
}
return api_payload return api_payload
def verify(authorization_payload: Dict[str, Any]) -> bool: def verify(
authorization_type: Dict[str, Any],
authorization_payload: Dict[str, Any],
signature_name_input: str,
) -> bool:
""" """
Verifies provided signature signer by correct address. Verifies provided signature signer by correct address.
**Important** Assume that not address field is timefield (live_at, expires_at, deadline, etc)
""" """
# Initializing instance of EIP712Message class
attrs: Dict[str, Any] = {
"_name_": authorization_type["name"],
"_version_": authorization_type["version"],
}
time_now = int(time.time()) time_now = int(time.time())
web3_client = Web3() web3_client = Web3()
address = Web3.toChecksumAddress(cast(str, authorization_payload["address"]))
deadline = cast(int, authorization_payload["deadline"])
signature = cast(str, authorization_payload["signed_message"])
message = MoonstreamAuthorization( address: Optional[ChecksumAddress] = None
_name_=AUTH_PAYLOAD_NAME, time_field: Optional[int] = None
_version_=AUTH_VERSION,
address=address,
deadline=deadline,
)
for pt in authorization_type["primary_types"]:
pt_name = pt["name"]
pt_type = pt["type"]
if pt_type == "address":
address = Web3.toChecksumAddress(cast(str, authorization_payload[pt_name]))
attrs[pt_name] = address
else:
time_field = cast(pt_type, authorization_payload[pt_name])
attrs[pt_name] = time_field
if address is None or time_field is None:
raise MoonstreamAuthorizationStructureError(
"Field address or time_field could not be None"
)
message = authorization_type["eip712_message_class"](**attrs)
signature = cast(str, authorization_payload[signature_name_input])
signer_address = web3_client.eth.account.recover_message( signer_address = web3_client.eth.account.recover_message(
message.signable_message, signature=signature message.signable_message, signature=signature
) )
if signer_address != address: if signer_address != address:
raise MoonstreamAuthorizationVerificationError("Invalid signer") raise MoonstreamAuthorizationVerificationError("Invalid signer")
if deadline < time_now: if time_field < time_now:
raise MoonstreamAuthorizationExpired("Deadline exceeded") raise MoonstreamAuthorizationExpired("Time field exceeded")
return True return True
@ -121,15 +176,37 @@ def decrypt_keystore(keystore_path: str, password: str) -> HexBytes:
def handle_authorize(args: argparse.Namespace) -> None: def handle_authorize(args: argparse.Namespace) -> None:
address, private_key = decrypt_keystore(args.signer, args.password) if args.authorization_type not in EIP712_AUTHORIZATION_TYPES:
authorization = authorize(args.deadline, address, private_key) raise Exception("Provided unsupported EIP712 Authorization type")
authorization_type = EIP712_AUTHORIZATION_TYPES[args.authorization_type]
primary_types = json.loads(args.primary_types)
for ptk in authorization_type["primary_types"]:
if ptk["name"] not in primary_types:
raise Exception(f"Lost primary type: {ptk}")
_, private_key = decrypt_keystore(args.signer, args.password)
authorization = authorize(
authorization_type=authorization_type,
primary_types=primary_types,
private_key=private_key,
signature_name_output=args.signature_name_output,
)
print(json.dumps(authorization)) print(json.dumps(authorization))
def handle_verify(args: argparse.Namespace) -> None: def handle_verify(args: argparse.Namespace) -> None:
if args.authorization_type not in EIP712_AUTHORIZATION_TYPES:
raise Exception("Provided unsupported EIP712 Authorization type")
authorization_type = EIP712_AUTHORIZATION_TYPES[args.authorization_type]
payload_json = base64.decodebytes(args.payload).decode("utf-8") payload_json = base64.decodebytes(args.payload).decode("utf-8")
payload = json.loads(payload_json) payload = json.loads(payload_json)
verify(payload) verify(
authorization_type=authorization_type,
authorization_payload=payload,
signature_name_input=args.signature_name_input,
)
print("Verified!") print("Verified!")
@ -140,13 +217,6 @@ def generate_cli() -> argparse.ArgumentParser:
subcommands = parser.add_subparsers() subcommands = parser.add_subparsers()
authorize_parser = subcommands.add_parser("authorize") authorize_parser = subcommands.add_parser("authorize")
authorize_parser.add_argument(
"-t",
"--deadline",
type=int,
default=int(time.time()) + DEFAULT_INTERVAL,
help="Authorization deadline (seconds since epoch timestamp).",
)
authorize_parser.add_argument( authorize_parser.add_argument(
"-s", "-s",
"--signer", "--signer",
@ -159,6 +229,30 @@ def generate_cli() -> argparse.ArgumentParser:
required=False, required=False,
help="(Optional) password for signing account. If you don't provide it here, you will be prompte for it.", help="(Optional) password for signing account. If you don't provide it here, you will be prompte for it.",
) )
authorize_parser.add_argument(
"-t",
"--authorization-type",
required=True,
choices=[k for k in EIP712_AUTHORIZATION_TYPES.keys()],
help="One of supported EIP712 Message authorization types",
)
authorize_parser.add_argument(
"--primary-types",
required=True,
help="Primary types for specified EIP712 Message authorization in JSON format {0}. Available keys: {1}".format(
{"name_1": "value", "name_2": "value"},
[
f"{v['primary_types']} for {k}"
for k, v in EIP712_AUTHORIZATION_TYPES.items()
],
),
)
authorize_parser.add_argument(
"--signature-name-output",
type=str,
default="signed_message",
help="Key in output dictionary of signature",
)
authorize_parser.set_defaults(func=handle_authorize) authorize_parser.set_defaults(func=handle_authorize)
verify_parser = subcommands.add_parser("verify") verify_parser = subcommands.add_parser("verify")
@ -168,6 +262,19 @@ def generate_cli() -> argparse.ArgumentParser:
required=True, required=True,
help="Base64-encoded payload to verify", help="Base64-encoded payload to verify",
) )
verify_parser.add_argument(
"-t",
"--authorization-type",
required=True,
choices=[k for k in EIP712_AUTHORIZATION_TYPES.keys()],
help="One of supported EIP712 Message authorization types",
)
verify_parser.add_argument(
"--signature-name-input",
type=str,
default="signed_message",
help="Key for signature in payload",
)
verify_parser.set_defaults(func=handle_verify) verify_parser.set_defaults(func=handle_verify)
return parser return parser

Wyświetl plik

@ -2,10 +2,10 @@ import argparse
import json import json
import logging import logging
import uuid import uuid
from datetime import timedelta from datetime import datetime, timedelta
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple
from sqlalchemy import func, text from sqlalchemy import func, or_, text
from sqlalchemy.dialects.postgresql import insert from sqlalchemy.dialects.postgresql import insert
from sqlalchemy.engine import Row from sqlalchemy.engine import Row
from sqlalchemy.exc import IntegrityError, NoResultFound from sqlalchemy.exc import IntegrityError, NoResultFound
@ -100,7 +100,9 @@ def parse_call_request_response(
method=obj[0].method, method=obj[0].method,
request_id=str(obj[0].request_id), request_id=str(obj[0].request_id),
parameters=obj[0].parameters, parameters=obj[0].parameters,
tx_hash=obj[0].tx_hash,
expires_at=obj[0].expires_at, expires_at=obj[0].expires_at,
live_at=obj[0].live_at,
created_at=obj[0].created_at, created_at=obj[0].created_at,
updated_at=obj[0].updated_at, updated_at=obj[0].updated_at,
) )
@ -326,13 +328,14 @@ def delete_registered_contract(
return (registered_contract, blockchain) return (registered_contract, blockchain)
def request_calls( def create_request_calls(
db_session: Session, db_session: Session,
metatx_requester_id: uuid.UUID, metatx_requester_id: uuid.UUID,
registered_contract_id: Optional[uuid.UUID], registered_contract_id: Optional[uuid.UUID],
contract_address: Optional[str], contract_address: Optional[str],
call_specs: List[data.CallSpecification], call_specs: List[data.CallSpecification],
ttl_days: Optional[int] = None, ttl_days: Optional[int] = None,
live_at: Optional[int] = None,
) -> int: ) -> int:
""" """
Batch creates call requests for the given registered contract. Batch creates call requests for the given registered contract.
@ -350,6 +353,11 @@ def request_calls(
if ttl_days <= 0: if ttl_days <= 0:
raise ValueError("ttl_days must be positive") raise ValueError("ttl_days must be positive")
if live_at is not None:
assert live_at == int(live_at)
if live_at <= 0:
raise ValueError("live_at must be positive")
# Check that the moonstream_user_id matches a RegisteredContract with the given id or address # Check that the moonstream_user_id matches a RegisteredContract with the given id or address
query = db_session.query(RegisteredContract).filter( query = db_session.query(RegisteredContract).filter(
RegisteredContract.metatx_requester_id == metatx_requester_id RegisteredContract.metatx_requester_id == metatx_requester_id
@ -406,6 +414,7 @@ def request_calls(
request_id=specification.request_id, request_id=specification.request_id,
parameters=specification.parameters, parameters=specification.parameters,
expires_at=expires_at, expires_at=expires_at,
live_at=datetime.fromtimestamp(live_at) if live_at is not None else None,
) )
db_session.add(request) db_session.add(request)
@ -422,7 +431,7 @@ def request_calls(
return len(call_specs) return len(call_specs)
def get_call_requests( def get_call_request(
db_session: Session, db_session: Session,
request_id: uuid.UUID, request_id: uuid.UUID,
) -> Tuple[CallRequest, RegisteredContract]: ) -> Tuple[CallRequest, RegisteredContract]:
@ -472,9 +481,14 @@ def list_call_requests(
limit: int = 10, limit: int = 10,
offset: Optional[int] = None, offset: Optional[int] = None,
show_expired: bool = False, show_expired: bool = False,
live_after: Optional[int] = None,
metatx_requester_id: Optional[uuid.UUID] = None,
) -> List[Row[Tuple[CallRequest, RegisteredContract, CallRequestType]]]: ) -> List[Row[Tuple[CallRequest, RegisteredContract, CallRequestType]]]:
""" """
List call requests for the given moonstream_user_id List call requests.
Argument moonstream_user_id took from authorization workflow. And if it is specified
then user has access to call_requests before live_at param.
""" """
if caller is None: if caller is None:
raise ValueError("caller must be specified") raise ValueError("caller must be specified")
@ -507,6 +521,23 @@ def list_call_requests(
CallRequest.expires_at > func.now(), CallRequest.expires_at > func.now(),
) )
# If user id not specified, do not show call_requests before live_at.
# Otherwise check show_before_live_at argument from query parameter
if metatx_requester_id is not None:
query = query.filter(
CallRequest.metatx_requester_id == metatx_requester_id,
)
else:
query = query.filter(
or_(CallRequest.live_at < func.now(), CallRequest.live_at == None)
)
if live_after is not None:
assert live_after == int(live_after)
if live_after <= 0:
raise ValueError("live_after must be positive")
query = query.filter(CallRequest.live_at >= datetime.fromtimestamp(live_after))
if offset is not None: if offset is not None:
query = query.offset(offset) query = query.offset(offset)
@ -551,6 +582,46 @@ def delete_requests(
return requests_to_delete_num return requests_to_delete_num
def complete_call_request(
db_session: Session,
tx_hash: str,
call_request_id: uuid.UUID,
caller: str,
) -> CallRequest:
results = (
db_session.query(CallRequest, RegisteredContract)
.join(
RegisteredContract,
CallRequest.registered_contract_id == RegisteredContract.id,
)
.filter(CallRequest.id == call_request_id)
.filter(CallRequest.caller == caller)
.all()
)
if len(results) == 0:
raise CallRequestNotFound("Call request with given ID not found")
elif len(results) != 1:
raise Exception(
f"Incorrect number of results found for request_id {call_request_id}"
)
call_request, registered_contract = results[0]
call_request.tx_hash = tx_hash
try:
db_session.add(call_request)
db_session.commit()
except Exception as err:
logger.error(
f"complete_call_request -- error updating in database: {repr(err)}"
)
db_session.rollback()
raise
return (call_request, registered_contract)
def handle_register(args: argparse.Namespace) -> None: def handle_register(args: argparse.Namespace) -> None:
""" """
Handles the register command. Handles the register command.
@ -633,7 +704,7 @@ def handle_request_calls(args: argparse.Namespace) -> None:
try: try:
with db.yield_db_session_ctx() as db_session: with db.yield_db_session_ctx() as db_session:
request_calls( create_request_calls(
db_session=db_session, db_session=db_session,
moonstream_user_id=args.moonstream_user_id, moonstream_user_id=args.moonstream_user_id,
registered_contract_id=args.registered_contract_id, registered_contract_id=args.registered_contract_id,

Wyświetl plik

@ -284,6 +284,7 @@ class CreateCallRequestsAPIRequest(BaseModel):
contract_address: Optional[str] = None contract_address: Optional[str] = None
specifications: List[CallSpecification] = Field(default_factory=list) specifications: List[CallSpecification] = Field(default_factory=list)
ttl_days: Optional[int] = None ttl_days: Optional[int] = None
live_at: Optional[int] = None
# Solution found thanks to https://github.com/pydantic/pydantic/issues/506 # Solution found thanks to https://github.com/pydantic/pydantic/issues/506
@root_validator @root_validator
@ -305,7 +306,9 @@ class CallRequestResponse(BaseModel):
method: str method: str
request_id: str request_id: str
parameters: Dict[str, Any] parameters: Dict[str, Any]
tx_hash: Optional[str] = None
expires_at: Optional[datetime] = None expires_at: Optional[datetime] = None
live_at: Optional[datetime] = None
created_at: datetime created_at: datetime
updated_at: datetime updated_at: datetime
@ -326,6 +329,10 @@ class CallRequestResponse(BaseModel):
return Web3.toChecksumAddress(v) return Web3.toChecksumAddress(v)
class CompleteCallRequestsAPIRequest(BaseModel):
tx_hash: str
class QuartilesResponse(BaseModel): class QuartilesResponse(BaseModel):
percentile_25: Dict[str, Any] percentile_25: Dict[str, Any]
percentile_50: Dict[str, Any] percentile_50: Dict[str, Any]
@ -365,11 +372,22 @@ class LeaderboardScore(BaseModel):
points_data: Dict[str, Any] points_data: Dict[str, Any]
class ColumnsNames(BaseModel):
rank: Optional[str] = None
address: Optional[str] = None
score: Optional[str] = None
points_data: Optional[str] = None
points_data_fields: Optional[Dict[str, str]] = None
class Leaderboard(BaseModel): class Leaderboard(BaseModel):
id: UUID id: UUID
title: str title: str
description: Optional[str] = None description: Optional[str] = None
resource_id: Optional[UUID] = None resource_id: Optional[UUID] = None
wallet_connect: bool = False
blockchain_ids: List[int] = Field(default_factory=list)
columns_names: Optional[ColumnsNames] = None
created_at: datetime created_at: datetime
updated_at: datetime updated_at: datetime
@ -385,6 +403,9 @@ class LeaderboardInfoResponse(BaseModel):
class LeaderboardCreateRequest(BaseModel): class LeaderboardCreateRequest(BaseModel):
title: str title: str
description: Optional[str] = None description: Optional[str] = None
wallet_connect: bool = False
blockchain_ids: List[int] = Field(default_factory=list)
columns_names: Optional[ColumnsNames] = None
class LeaderboardCreatedResponse(BaseModel): class LeaderboardCreatedResponse(BaseModel):
@ -392,6 +413,9 @@ class LeaderboardCreatedResponse(BaseModel):
title: str title: str
description: Optional[str] = None description: Optional[str] = None
resource_id: Optional[UUID] = None resource_id: Optional[UUID] = None
wallet_connect: bool = False
blockchain_ids: List[int] = Field(default_factory=list)
columns_names: Optional[ColumnsNames] = None
created_at: datetime created_at: datetime
updated_at: datetime updated_at: datetime
@ -404,6 +428,9 @@ class LeaderboardUpdatedResponse(BaseModel):
title: str title: str
description: Optional[str] = None description: Optional[str] = None
resource_id: Optional[UUID] = None resource_id: Optional[UUID] = None
wallet_connect: bool = False
blockchain_ids: List[int] = Field(default_factory=list)
columns_names: Optional[ColumnsNames] = None
created_at: datetime created_at: datetime
updated_at: datetime updated_at: datetime
@ -414,6 +441,9 @@ class LeaderboardUpdatedResponse(BaseModel):
class LeaderboardUpdateRequest(BaseModel): class LeaderboardUpdateRequest(BaseModel):
title: Optional[str] = None title: Optional[str] = None
description: Optional[str] = None description: Optional[str] = None
wallet_connect: bool = False
blockchain_ids: List[int] = Field(default_factory=list)
columns_names: Optional[ColumnsNames] = None
class LeaderboardDeletedResponse(BaseModel): class LeaderboardDeletedResponse(BaseModel):
@ -421,6 +451,9 @@ class LeaderboardDeletedResponse(BaseModel):
title: str title: str
description: Optional[str] = None description: Optional[str] = None
resource_id: Optional[UUID] = None resource_id: Optional[UUID] = None
wallet_connect: bool = False
blockchain_ids: List[int] = Field(default_factory=list)
columns_names: Optional[ColumnsNames] = None
created_at: datetime created_at: datetime
updated_at: datetime updated_at: datetime

Wyświetl plik

@ -6,17 +6,24 @@ from uuid import UUID
from bugout.data import BugoutResource, BugoutResources, BugoutUser from bugout.data import BugoutResource, BugoutResources, BugoutUser
from bugout.exceptions import BugoutResponseException from bugout.exceptions import BugoutResponseException
from fastapi import HTTPException, Request, Response from eip712.messages import EIP712Message, _hash_eip191_message
from eth_account.messages import encode_defunct
from fastapi import Depends, Header, HTTPException, Request, Response
from fastapi.security import OAuth2PasswordBearer
from hexbytes import HexBytes
from pydantic import AnyHttpUrl, parse_obj_as from pydantic import AnyHttpUrl, parse_obj_as
from starlette.middleware.base import BaseHTTPMiddleware from starlette.middleware.base import BaseHTTPMiddleware
from starlette.middleware.cors import CORSMiddleware from starlette.middleware.cors import CORSMiddleware
from starlette.responses import Response from starlette.responses import Response
from starlette.types import ASGIApp from starlette.types import ASGIApp
from web3 import Web3 from web3 import Web3
from web3.auto import w3 as w3_auto
from . import data from . import data
from .auth import ( from .auth import (
EIP712_AUTHORIZATION_TYPES,
MoonstreamAuthorizationExpired, MoonstreamAuthorizationExpired,
MoonstreamAuthorizationStructureError,
MoonstreamAuthorizationVerificationError, MoonstreamAuthorizationVerificationError,
verify, verify,
) )
@ -26,13 +33,170 @@ from .settings import (
BUGOUT_REQUEST_TIMEOUT_SECONDS, BUGOUT_REQUEST_TIMEOUT_SECONDS,
BUGOUT_RESOURCE_TYPE_APPLICATION_CONFIG, BUGOUT_RESOURCE_TYPE_APPLICATION_CONFIG,
MOONSTREAM_ADMIN_ACCESS_TOKEN, MOONSTREAM_ADMIN_ACCESS_TOKEN,
MOONSTREAM_APPLICATION_ID,
MOONSTREAM_ADMIN_ID, MOONSTREAM_ADMIN_ID,
MOONSTREAM_APPLICATION_ID,
) )
from .settings import bugout_client as bc from .settings import bugout_client as bc
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
class InvalidAuthHeaderFormat(Exception):
"""
Raised when authorization header not pass validation.
"""
class BugoutUnverifiedAuth(Exception):
"""
Raised when attempted access by unverified Brood account.
"""
class BugoutAuthWrongApp(Exception):
"""
Raised when user does not belong to this application.
"""
def parse_auth_header(auth_header: str) -> Tuple[str, str]:
"""
Returns: auth_format and user_token passed in authorization header.
"""
auth_list = auth_header.split()
if len(auth_list) != 2:
raise InvalidAuthHeaderFormat("Wrong authorization header")
return auth_list[0], auth_list[1]
def bugout_auth(token: str) -> BugoutUser:
"""
Extended bugout.get_user with additional checks.
"""
user: BugoutUser = bc.get_user(token)
if not user.verified:
raise BugoutUnverifiedAuth("Only verified accounts can have access")
if str(user.application_id) != str(MOONSTREAM_APPLICATION_ID):
raise BugoutAuthWrongApp("User does not belong to this application")
return user
def brood_auth(token: UUID) -> BugoutUser:
try:
user: BugoutUser = bugout_auth(token=token)
except BugoutUnverifiedAuth:
logger.info(f"Attempted access by unverified Brood account: {user.id}")
raise EngineHTTPException(
status_code=403,
detail="Only verified accounts can have access",
)
except BugoutAuthWrongApp:
raise EngineHTTPException(
status_code=403,
detail="User does not belong to this application",
)
except BugoutResponseException as e:
raise EngineHTTPException(
status_code=e.status_code,
detail=e.detail,
)
except Exception as e:
logger.error(f"Error processing Brood response: {str(e)}")
raise EngineHTTPException(
status_code=500,
detail="Internal server error",
)
return user
async def request_user_auth(
token: UUID = Depends(oauth2_scheme),
) -> BugoutUser:
user = brood_auth(token=token)
return user
async def request_none_or_user_auth(
authorization: str = Header(None),
) -> Optional[BugoutUser]:
"""
Fetch Bugout user if authorization token provided.
"""
user: Optional[BugoutUser] = None
if authorization is not None:
token: str = ""
try:
_, token = parse_auth_header(auth_header=authorization)
except InvalidAuthHeaderFormat:
raise EngineHTTPException(
status_code=403, detail="Wrong authorization header"
)
except Exception as e:
logger.error(f"Error parsing auth header: {str(e)}")
raise EngineHTTPException(status_code=500, detail="Internal server error")
if token != "":
user = brood_auth(token=token)
return user
async def metatx_verify_header(
authorization: str = Header(None),
) -> Optional[Dict[str, Any]]:
message: Optional[Dict[str, Any]] = None
if authorization is not None:
try:
auth_format, user_token = parse_auth_header(auth_header=authorization)
except InvalidAuthHeaderFormat:
raise EngineHTTPException(
status_code=403, detail="Wrong authorization header"
)
except Exception as e:
logger.error(f"Error parsing auth header: {str(e)}")
raise EngineHTTPException(status_code=500, detail="Internal server error")
if auth_format != "metatx":
raise EngineHTTPException(
status_code=403,
detail=f"Wrong authorization header format: {auth_format}",
)
try:
json_payload_str = base64.b64decode(user_token).decode("utf-8")
payload = json.loads(json_payload_str)
verify(
authorization_type=EIP712_AUTHORIZATION_TYPES["MetaTXAuthorization"],
authorization_payload=payload,
signature_name_input="signature",
)
message = {
"caller": Web3.toChecksumAddress(payload.get("caller")),
"expires_at": payload.get("expires_at"),
}
except MoonstreamAuthorizationVerificationError as e:
logger.info("MetaTX authorization verification error: %s", e)
raise EngineHTTPException(status_code=403, detail="Invalid signer")
except MoonstreamAuthorizationExpired as e:
logger.info("MetaTX authorization expired: %s", e)
raise EngineHTTPException(status_code=403, detail="Authorization expired")
except MoonstreamAuthorizationStructureError as e:
logger.info("MetaTX authorization incorrect structure error: %s", e)
raise EngineHTTPException(
status_code=403, detail="Incorrect signature structure"
)
except Exception as e:
logger.error("Unexpected exception: %s", e)
raise EngineHTTPException(status_code=500, detail="Internal server error")
return message
class BroodAuthMiddleware(BaseHTTPMiddleware): class BroodAuthMiddleware(BaseHTTPMiddleware):
""" """
@ -59,30 +223,33 @@ class BroodAuthMiddleware(BaseHTTPMiddleware):
if path in self.whitelist.keys() and self.whitelist[path] == method: if path in self.whitelist.keys() and self.whitelist[path] == method:
return await call_next(request) return await call_next(request)
authorization_header = request.headers.get("authorization") authorization = request.headers.get("authorization")
if authorization_header is None: if authorization is None:
return Response( return Response(
status_code=403, content="No authorization header passed with request" status_code=403,
content="No authorization header passed with request",
) )
user_token_list = authorization_header.split()
if len(user_token_list) != 2:
return Response(status_code=403, content="Wrong authorization header")
user_token: str = user_token_list[-1]
try: try:
user: BugoutUser = bc.get_user(user_token) _, user_token = parse_auth_header(auth_header=authorization)
if not user.verified: except InvalidAuthHeaderFormat:
logger.info( return Response(status_code=403, content="Wrong authorization header")
f"Attempted journal access by unverified Brood account: {user.id}" except Exception as e:
) logger.error(f"Error parsing auth header: {str(e)}")
return Response( return Response(status_code=500, content="Internal server error")
status_code=403,
content="Only verified accounts can access journals", try:
) user: BugoutUser = bugout_auth(token=user_token)
if str(user.application_id) != str(MOONSTREAM_APPLICATION_ID): except BugoutUnverifiedAuth:
return Response( logger.info(f"Attempted access by unverified Brood account: {user.id}")
status_code=403, content="User does not belong to this application" return Response(
) status_code=403,
content="Only verified accounts can have access",
)
except BugoutAuthWrongApp:
return Response(
status_code=403, content="User does not belong to this application"
)
except BugoutResponseException as e: except BugoutResponseException as e:
return Response(status_code=e.status_code, content=e.detail) return Response(status_code=e.status_code, content=e.detail)
except Exception as e: except Exception as e:
@ -139,9 +306,15 @@ class EngineAuthMiddleware(BaseHTTPMiddleware):
authorization_header_components[-1] authorization_header_components[-1]
).decode("utf-8") ).decode("utf-8")
json_payload = json.loads(json_payload_str) payload = json.loads(json_payload_str)
verified = verify(json_payload) verified = verify(
address = json_payload.get("address") authorization_type=EIP712_AUTHORIZATION_TYPES[
"MoonstreamAuthorization"
],
authorization_payload=payload,
signature_name_input="signed_message",
)
address = payload.get("address")
if address is not None: if address is not None:
address = Web3.toChecksumAddress(address) address = Web3.toChecksumAddress(address)
else: else:

Wyświetl plik

@ -1,6 +1,7 @@
import uuid import uuid
from sqlalchemy import ( from sqlalchemy import (
ARRAY,
DECIMAL, DECIMAL,
VARCHAR, VARCHAR,
BigInteger, BigInteger,
@ -314,8 +315,10 @@ class CallRequest(Base):
method = Column(String, nullable=False, index=True) method = Column(String, nullable=False, index=True)
request_id = Column(DECIMAL, nullable=False, index=True) request_id = Column(DECIMAL, nullable=False, index=True)
parameters = Column(JSONB, nullable=False) parameters = Column(JSONB, nullable=False)
tx_hash = Column(VARCHAR(256), unique=True, nullable=True)
expires_at = Column(DateTime(timezone=True), nullable=True, index=True) expires_at = Column(DateTime(timezone=True), nullable=True, index=True)
live_at = Column(DateTime(timezone=True), nullable=True)
created_at = Column( created_at = Column(
DateTime(timezone=True), server_default=utcnow(), nullable=False DateTime(timezone=True), server_default=utcnow(), nullable=False
@ -335,7 +338,6 @@ class CallRequest(Base):
class Leaderboard(Base): # type: ignore class Leaderboard(Base): # type: ignore
__tablename__ = "leaderboards" __tablename__ = "leaderboards"
# __table_args__ = (UniqueConstraint("dropper_contract_id", "address"),)
id = Column( id = Column(
UUID(as_uuid=True), UUID(as_uuid=True),
@ -347,6 +349,10 @@ class Leaderboard(Base): # type: ignore
title = Column(VARCHAR(128), nullable=False) title = Column(VARCHAR(128), nullable=False)
description = Column(String, nullable=True) description = Column(String, nullable=True)
resource_id = Column(UUID(as_uuid=True), nullable=True, index=True) resource_id = Column(UUID(as_uuid=True), nullable=True, index=True)
blockchain_ids = Column(ARRAY(Integer), nullable=False, default=[])
wallet_connect = Column(Boolean, default=False, nullable=False)
columns_names = Column(JSONB, nullable=True)
created_at = Column( created_at = Column(
DateTime(timezone=True), server_default=utcnow(), nullable=False DateTime(timezone=True), server_default=utcnow(), nullable=False
) )

Wyświetl plik

@ -2,7 +2,7 @@
Leaderboard API. Leaderboard API.
""" """
import logging import logging
from typing import Any, Dict, List, Optional from typing import Any, Dict, List, Optional, Any, Union
from uuid import UUID from uuid import UUID
from bugout.exceptions import BugoutResponseException from bugout.exceptions import BugoutResponseException
@ -92,9 +92,12 @@ app.add_middleware(
"", "",
response_model=List[data.LeaderboardPosition], response_model=List[data.LeaderboardPosition],
tags=["Public Endpoints"], tags=["Public Endpoints"],
include_in_schema=False,
) )
@app.get("/", response_model=List[data.LeaderboardPosition], tags=["Public Endpoints"]) @app.get(
"/",
response_model=List[data.LeaderboardPosition],
tags=["Public Endpoints"],
)
async def leaderboard( async def leaderboard(
leaderboard_id: UUID = Query(..., description="Leaderboard ID"), leaderboard_id: UUID = Query(..., description="Leaderboard ID"),
limit: int = Query(10), limit: int = Query(10),
@ -108,7 +111,7 @@ async def leaderboard(
### Check if leaderboard exists ### Check if leaderboard exists
try: try:
actions.get_leaderboard_by_id(db_session, leaderboard_id) leaderboard = actions.get_leaderboard_by_id(db_session, leaderboard_id)
except NoResultFound as e: except NoResultFound as e:
raise EngineHTTPException( raise EngineHTTPException(
status_code=404, status_code=404,
@ -119,8 +122,9 @@ async def leaderboard(
raise EngineHTTPException(status_code=500, detail="Internal server error") raise EngineHTTPException(status_code=500, detail="Internal server error")
leaderboard_positions = actions.get_leaderboard_positions( leaderboard_positions = actions.get_leaderboard_positions(
db_session, leaderboard_id, limit, offset, version db_session, leaderboard.id, limit, offset, version
) )
result = [ result = [
data.LeaderboardPosition( data.LeaderboardPosition(
address=position.address, address=position.address,
@ -150,7 +154,6 @@ async def create_leaderboard(
Authorization: str = AuthHeader, Authorization: str = AuthHeader,
) -> data.LeaderboardCreatedResponse: ) -> data.LeaderboardCreatedResponse:
""" """
Create leaderboard. Create leaderboard.
""" """
@ -162,6 +165,9 @@ async def create_leaderboard(
title=leaderboard.title, title=leaderboard.title,
description=leaderboard.description, description=leaderboard.description,
token=token, token=token,
wallet_connect=leaderboard.wallet_connect,
blockchain_ids=leaderboard.blockchain_ids,
columns_names=leaderboard.columns_names,
) )
except actions.LeaderboardCreateError as e: except actions.LeaderboardCreateError as e:
logger.error(f"Error while creating leaderboard: {e}") logger.error(f"Error while creating leaderboard: {e}")
@ -177,12 +183,15 @@ async def create_leaderboard(
# Add resource to the leaderboard # Add resource to the leaderboard
return data.LeaderboardCreatedResponse( return data.LeaderboardCreatedResponse(
id=created_leaderboard.id, # type: ignore id=created_leaderboard.id,
title=created_leaderboard.title, # type: ignore title=created_leaderboard.title,
description=created_leaderboard.description, # type: ignore description=created_leaderboard.description,
resource_id=created_leaderboard.resource_id, # type: ignore resource_id=created_leaderboard.resource_id,
created_at=created_leaderboard.created_at, # type: ignore wallet_connect=created_leaderboard.wallet_connect,
updated_at=created_leaderboard.updated_at, # type: ignore blockchain_ids=created_leaderboard.blockchain_ids,
columns_names=created_leaderboard.columns_names,
created_at=created_leaderboard.created_at,
updated_at=created_leaderboard.updated_at,
) )
@ -226,6 +235,9 @@ async def update_leaderboard(
leaderboard_id=leaderboard_id, leaderboard_id=leaderboard_id,
title=leaderboard.title, title=leaderboard.title,
description=leaderboard.description, description=leaderboard.description,
wallet_connect=leaderboard.wallet_connect,
blockchain_ids=leaderboard.blockchain_ids,
columns_names=leaderboard.columns_names,
) )
except actions.LeaderboardUpdateError as e: except actions.LeaderboardUpdateError as e:
logger.error(f"Error while updating leaderboard: {e}") logger.error(f"Error while updating leaderboard: {e}")
@ -239,12 +251,15 @@ async def update_leaderboard(
raise EngineHTTPException(status_code=500, detail="Internal server error") raise EngineHTTPException(status_code=500, detail="Internal server error")
return data.LeaderboardUpdatedResponse( return data.LeaderboardUpdatedResponse(
id=updated_leaderboard.id, # type: ignore id=updated_leaderboard.id,
title=updated_leaderboard.title, # type: ignore title=updated_leaderboard.title,
description=updated_leaderboard.description, # type: ignore description=updated_leaderboard.description,
resource_id=updated_leaderboard.resource_id, # type: ignore resource_id=updated_leaderboard.resource_id,
created_at=updated_leaderboard.created_at, # type: ignore wallet_connect=updated_leaderboard.wallet_connect,
updated_at=updated_leaderboard.updated_at, # type: ignore blockchain_ids=updated_leaderboard.blockchain_ids,
columns_names=updated_leaderboard.columns_names,
created_at=updated_leaderboard.created_at,
updated_at=updated_leaderboard.updated_at,
) )
@ -299,11 +314,15 @@ async def delete_leaderboard(
raise EngineHTTPException(status_code=500, detail="Internal server error") raise EngineHTTPException(status_code=500, detail="Internal server error")
return data.LeaderboardDeletedResponse( return data.LeaderboardDeletedResponse(
id=deleted_leaderboard.id, # type: ignore id=deleted_leaderboard.id,
title=deleted_leaderboard.title, # type: ignore title=deleted_leaderboard.title,
description=deleted_leaderboard.description, # type: ignore description=deleted_leaderboard.description,
created_at=deleted_leaderboard.created_at, # type: ignore resource_id=deleted_leaderboard.resource_id,
updated_at=deleted_leaderboard.updated_at, # type: ignore wallet_connect=deleted_leaderboard.wallet_connect,
blockchain_ids=deleted_leaderboard.blockchain_ids,
columns_names=deleted_leaderboard.columns_names,
created_at=deleted_leaderboard.created_at,
updated_at=deleted_leaderboard.updated_at,
) )
@ -336,12 +355,15 @@ async def get_leaderboards(
results = [ results = [
data.Leaderboard( data.Leaderboard(
id=leaderboard.id, # type: ignore id=leaderboard.id,
title=leaderboard.title, # type: ignore title=leaderboard.title,
description=leaderboard.description, # type: ignore description=leaderboard.description,
resource_id=leaderboard.resource_id, # type: ignore resource_id=leaderboard.resource_id,
created_at=leaderboard.created_at, # type: ignore wallet_connect=leaderboard.wallet_connect,
updated_at=leaderboard.updated_at, # type: ignore blockchain_ids=leaderboard.blockchain_ids,
columns_names=leaderboard.columns_names,
created_at=leaderboard.created_at,
updated_at=leaderboard.updated_at,
) )
for leaderboard in leaderboards for leaderboard in leaderboards
] ]
@ -453,7 +475,7 @@ async def quartiles(
""" """
### Check if leaderboard exists ### Check if leaderboard exists
try: try:
actions.get_leaderboard_by_id(db_session, leaderboard_id) leaderboard = actions.get_leaderboard_by_id(db_session, leaderboard_id)
except NoResultFound as e: except NoResultFound as e:
raise EngineHTTPException( raise EngineHTTPException(
status_code=404, status_code=404,
@ -472,12 +494,14 @@ async def quartiles(
logger.error(f"Error while getting quartiles: {e}") logger.error(f"Error while getting quartiles: {e}")
raise EngineHTTPException(status_code=500, detail="Internal server error") raise EngineHTTPException(status_code=500, detail="Internal server error")
return data.QuartilesResponse( result = data.QuartilesResponse(
percentile_25={"address": q1[0], "score": q1[1], "rank": q1[2]}, percentile_25={"address": q1.address, "rank": q1.rank, "score": q1.score},
percentile_50={"address": q2[0], "score": q2[1], "rank": q2[2]}, percentile_50={"address": q2.address, "rank": q2.rank, "score": q2.score},
percentile_75={"address": q3[0], "score": q3[1], "rank": q3[2]}, percentile_75={"address": q3.address, "rank": q3.rank, "score": q3.score},
) )
return result
@app.get( @app.get(
"/position", "/position",
@ -503,7 +527,7 @@ async def position(
### Check if leaderboard exists ### Check if leaderboard exists
try: try:
actions.get_leaderboard_by_id(db_session, leaderboard_id) leaderboard = actions.get_leaderboard_by_id(db_session, leaderboard_id)
except NoResultFound as e: except NoResultFound as e:
raise EngineHTTPException( raise EngineHTTPException(
status_code=404, status_code=404,
@ -540,7 +564,9 @@ async def position(
@app.get( @app.get(
"/rank", response_model=List[data.LeaderboardPosition], tags=["Public Endpoints"] "/rank",
response_model=List[data.LeaderboardPosition],
tags=["Public Endpoints"],
) )
async def rank( async def rank(
leaderboard_id: UUID = Query(..., description="Leaderboard ID"), leaderboard_id: UUID = Query(..., description="Leaderboard ID"),
@ -556,7 +582,7 @@ async def rank(
### Check if leaderboard exists ### Check if leaderboard exists
try: try:
actions.get_leaderboard_by_id(db_session, leaderboard_id) leaderboard = actions.get_leaderboard_by_id(db_session, leaderboard_id)
except NoResultFound as e: except NoResultFound as e:
raise EngineHTTPException( raise EngineHTTPException(
status_code=404, status_code=404,
@ -574,14 +600,15 @@ async def rank(
offset=offset, offset=offset,
version_number=version, version_number=version,
) )
results = [ results = [
data.LeaderboardPosition( data.LeaderboardPosition(
address=rank_position.address, address=position.address,
score=rank_position.score, score=position.score,
rank=rank_position.rank, rank=position.rank,
points_data=rank_position.points_data, points_data=position.points_data,
) )
for rank_position in leaderboard_rank for position in leaderboard_rank
] ]
return results return results

Wyświetl plik

@ -9,12 +9,19 @@ import logging
from typing import Dict, List, Optional from typing import Dict, List, Optional
from uuid import UUID from uuid import UUID
from fastapi import Body, Depends, FastAPI, Path, Query, Request from bugout.data import BugoutUser
from fastapi import Body, Depends, FastAPI, Form, Path, Query, Request
from sqlalchemy.exc import NoResultFound from sqlalchemy.exc import NoResultFound
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from .. import contracts_actions, data, db from .. import contracts_actions, data, db
from ..middleware import BroodAuthMiddleware, BugoutCORSMiddleware, EngineHTTPException from ..middleware import (
BugoutCORSMiddleware,
EngineHTTPException,
metatx_verify_header,
request_none_or_user_auth,
request_user_auth,
)
from ..settings import DOCS_TARGET_PATH from ..settings import DOCS_TARGET_PATH
from ..version import VERSION from ..version import VERSION
@ -34,15 +41,6 @@ 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",
}
app = FastAPI( app = FastAPI(
title=TITLE, title=TITLE,
description=DESCRIPTION, description=DESCRIPTION,
@ -53,9 +51,6 @@ app = FastAPI(
redoc_url=f"/{DOCS_TARGET_PATH}", redoc_url=f"/{DOCS_TARGET_PATH}",
) )
app.add_middleware(BroodAuthMiddleware, whitelist=whitelist_paths)
app.add_middleware( app.add_middleware(
BugoutCORSMiddleware, BugoutCORSMiddleware,
allow_credentials=True, allow_credentials=True,
@ -89,11 +84,11 @@ async def blockchains_route(
response_model=List[data.RegisteredContractResponse], response_model=List[data.RegisteredContractResponse],
) )
async def list_registered_contracts_route( async def list_registered_contracts_route(
request: Request,
blockchain: Optional[str] = Query(None), blockchain: Optional[str] = Query(None),
address: Optional[str] = Query(None), address: Optional[str] = Query(None),
limit: int = Query(10), limit: int = Query(10),
offset: Optional[int] = Query(None), offset: Optional[int] = Query(None),
user: BugoutUser = Depends(request_user_auth),
db_session: Session = Depends(db.yield_db_read_only_session), db_session: Session = Depends(db.yield_db_read_only_session),
) -> List[data.RegisteredContractResponse]: ) -> List[data.RegisteredContractResponse]:
""" """
@ -103,7 +98,7 @@ async def list_registered_contracts_route(
registered_contracts_with_blockchain = ( registered_contracts_with_blockchain = (
contracts_actions.lookup_registered_contracts( contracts_actions.lookup_registered_contracts(
db_session=db_session, db_session=db_session,
metatx_requester_id=request.state.user.id, metatx_requester_id=user.id,
blockchain=blockchain, blockchain=blockchain,
address=address, address=address,
limit=limit, limit=limit,
@ -126,8 +121,8 @@ async def list_registered_contracts_route(
response_model=data.RegisteredContractResponse, response_model=data.RegisteredContractResponse,
) )
async def get_registered_contract_route( async def get_registered_contract_route(
request: Request,
contract_id: UUID = Path(...), contract_id: UUID = Path(...),
user: BugoutUser = Depends(request_user_auth),
db_session: Session = Depends(db.yield_db_read_only_session), db_session: Session = Depends(db.yield_db_read_only_session),
) -> List[data.RegisteredContractResponse]: ) -> List[data.RegisteredContractResponse]:
""" """
@ -136,7 +131,7 @@ async def get_registered_contract_route(
try: try:
contract_with_blockchain = contracts_actions.get_registered_contract( contract_with_blockchain = contracts_actions.get_registered_contract(
db_session=db_session, db_session=db_session,
metatx_requester_id=request.state.user.id, metatx_requester_id=user.id,
contract_id=contract_id, contract_id=contract_id,
) )
except NoResultFound: except NoResultFound:
@ -157,8 +152,8 @@ async def get_registered_contract_route(
"/contracts", tags=["contracts"], response_model=data.RegisteredContractResponse "/contracts", tags=["contracts"], response_model=data.RegisteredContractResponse
) )
async def register_contract_route( async def register_contract_route(
request: Request,
contract: data.RegisterContractRequest = Body(...), contract: data.RegisterContractRequest = Body(...),
user: BugoutUser = Depends(request_user_auth),
db_session: Session = Depends(db.yield_db_session), db_session: Session = Depends(db.yield_db_session),
) -> data.RegisteredContractResponse: ) -> data.RegisteredContractResponse:
""" """
@ -167,7 +162,7 @@ async def register_contract_route(
try: try:
contract_with_blockchain = contracts_actions.register_contract( contract_with_blockchain = contracts_actions.register_contract(
db_session=db_session, db_session=db_session,
metatx_requester_id=request.state.user.id, metatx_requester_id=user.id,
blockchain_name=contract.blockchain, blockchain_name=contract.blockchain,
address=contract.address, address=contract.address,
title=contract.title, title=contract.title,
@ -198,15 +193,15 @@ async def register_contract_route(
response_model=data.RegisteredContractResponse, response_model=data.RegisteredContractResponse,
) )
async def update_contract_route( async def update_contract_route(
request: Request,
contract_id: UUID = Path(...), contract_id: UUID = Path(...),
update_info: data.UpdateContractRequest = Body(...), update_info: data.UpdateContractRequest = Body(...),
user: BugoutUser = Depends(request_user_auth),
db_session: Session = Depends(db.yield_db_session), db_session: Session = Depends(db.yield_db_session),
) -> data.RegisteredContractResponse: ) -> data.RegisteredContractResponse:
try: try:
contract_with_blockchain = contracts_actions.update_registered_contract( contract_with_blockchain = contracts_actions.update_registered_contract(
db_session=db_session, db_session=db_session,
metatx_requester_id=request.state.user.id, metatx_requester_id=user.id,
contract_id=contract_id, contract_id=contract_id,
title=update_info.title, title=update_info.title,
description=update_info.description, description=update_info.description,
@ -233,8 +228,8 @@ async def update_contract_route(
response_model=data.RegisteredContractResponse, response_model=data.RegisteredContractResponse,
) )
async def delete_contract_route( async def delete_contract_route(
request: Request,
contract_id: UUID = Path(...), contract_id: UUID = Path(...),
user: BugoutUser = Depends(request_user_auth),
db_session: Session = Depends(db.yield_db_session), db_session: Session = Depends(db.yield_db_session),
) -> data.RegisteredContractResponse: ) -> data.RegisteredContractResponse:
""" """
@ -243,7 +238,7 @@ async def delete_contract_route(
try: try:
deleted_contract_with_blockchain = contracts_actions.delete_registered_contract( deleted_contract_with_blockchain = contracts_actions.delete_registered_contract(
db_session=db_session, db_session=db_session,
metatx_requester_id=request.state.user.id, metatx_requester_id=user.id,
registered_contract_id=contract_id, registered_contract_id=contract_id,
) )
except Exception as err: except Exception as err:
@ -278,14 +273,20 @@ async def call_request_types_route(
return call_request_types return call_request_types
@app.get("/requests", tags=["requests"], response_model=List[data.CallRequestResponse]) @app.get(
"/requests",
tags=["requests"],
response_model=List[data.CallRequestResponse],
)
async def list_requests_route( async def list_requests_route(
contract_id: Optional[UUID] = Query(None), contract_id: Optional[UUID] = Query(None),
contract_address: Optional[str] = Query(None), contract_address: Optional[str] = Query(None),
caller: str = Query(...), caller: str = Query(...),
limit: int = Query(100), limit: int = Query(100),
offset: Optional[int] = Query(None), offset: Optional[int] = Query(None),
show_expired: Optional[bool] = Query(False), show_expired: bool = Query(False),
live_after: Optional[int] = Query(None),
user: Optional[BugoutUser] = Depends(request_none_or_user_auth),
db_session: Session = Depends(db.yield_db_read_only_session), db_session: Session = Depends(db.yield_db_read_only_session),
) -> List[data.CallRequestResponse]: ) -> List[data.CallRequestResponse]:
""" """
@ -302,6 +303,8 @@ async def list_requests_route(
limit=limit, limit=limit,
offset=offset, offset=offset,
show_expired=show_expired, show_expired=show_expired,
live_after=live_after,
metatx_requester_id=user.id if user is not None else None,
) )
except ValueError as e: except ValueError as e:
logger.error(repr(e)) logger.error(repr(e))
@ -318,6 +321,7 @@ async def list_requests_route(
) )
async def get_request( async def get_request(
request_id: UUID = Path(...), request_id: UUID = Path(...),
_: BugoutUser = Depends(request_user_auth),
db_session: Session = Depends(db.yield_db_read_only_session), db_session: Session = Depends(db.yield_db_read_only_session),
) -> List[data.CallRequestResponse]: ) -> List[data.CallRequestResponse]:
""" """
@ -326,7 +330,7 @@ async def get_request(
At least one of `contract_id` or `contract_address` must be provided as query parameters. At least one of `contract_id` or `contract_address` must be provided as query parameters.
""" """
try: try:
request = contracts_actions.get_call_requests( request = contracts_actions.get_call_request(
db_session=db_session, db_session=db_session,
request_id=request_id, request_id=request_id,
) )
@ -344,8 +348,8 @@ async def get_request(
@app.post("/requests", tags=["requests"], response_model=int) @app.post("/requests", tags=["requests"], response_model=int)
async def create_requests( async def create_requests(
request: Request,
data: data.CreateCallRequestsAPIRequest = Body(...), data: data.CreateCallRequestsAPIRequest = Body(...),
user: BugoutUser = Depends(request_user_auth),
db_session: Session = Depends(db.yield_db_session), db_session: Session = Depends(db.yield_db_session),
) -> int: ) -> int:
""" """
@ -354,13 +358,14 @@ async def create_requests(
At least one of `contract_id` or `contract_address` must be provided in the request body. At least one of `contract_id` or `contract_address` must be provided in the request body.
""" """
try: try:
num_requests = contracts_actions.request_calls( num_requests = contracts_actions.create_request_calls(
db_session=db_session, db_session=db_session,
metatx_requester_id=request.state.user.id, metatx_requester_id=user.id,
registered_contract_id=data.contract_id, registered_contract_id=data.contract_id,
contract_address=data.contract_address, contract_address=data.contract_address,
call_specs=data.specifications, call_specs=data.specifications,
ttl_days=data.ttl_days, ttl_days=data.ttl_days,
live_at=data.live_at,
) )
except contracts_actions.InvalidAddressFormat as err: except contracts_actions.InvalidAddressFormat as err:
raise EngineHTTPException( raise EngineHTTPException(
@ -396,8 +401,8 @@ async def create_requests(
@app.delete("/requests", tags=["requests"], response_model=int) @app.delete("/requests", tags=["requests"], response_model=int)
async def delete_requests( async def delete_requests(
request: Request,
request_ids: List[UUID] = Body(...), request_ids: List[UUID] = Body(...),
user: BugoutUser = Depends(request_user_auth),
db_session: Session = Depends(db.yield_db_session), db_session: Session = Depends(db.yield_db_session),
) -> int: ) -> int:
""" """
@ -406,7 +411,7 @@ async def delete_requests(
try: try:
deleted_requests = contracts_actions.delete_requests( deleted_requests = contracts_actions.delete_requests(
db_session=db_session, db_session=db_session,
metatx_requester_id=request.state.user.id, metatx_requester_id=user.id,
request_ids=request_ids, request_ids=request_ids,
) )
except Exception as err: except Exception as err:
@ -414,3 +419,32 @@ async def delete_requests(
raise EngineHTTPException(status_code=500) raise EngineHTTPException(status_code=500)
return deleted_requests return deleted_requests
@app.post("/requests/{request_id}/complete", tags=["requests"])
async def complete_call_request_route(
complete_request: data.CompleteCallRequestsAPIRequest = Body(...),
request_id: UUID = Path(...),
message=Depends(metatx_verify_header),
db_session: Session = Depends(db.yield_db_session),
):
"""
Set tx hash for specified call_request by verified account.
"""
try:
request = contracts_actions.complete_call_request(
db_session=db_session,
tx_hash=complete_request.tx_hash,
call_request_id=request_id,
caller=message["caller"],
)
except contracts_actions.CallRequestNotFound:
raise EngineHTTPException(
status_code=404,
detail="There is no call request with that ID.",
)
except Exception as e:
logger.error(repr(e))
raise EngineHTTPException(status_code=500)
return contracts_actions.parse_call_request_response(request)

Wyświetl plik

@ -1 +1 @@
0.0.7 0.0.8

Wyświetl plik

@ -0,0 +1,4 @@
[mypy]
[mypy-eth_keys.*]
ignore_missing_imports = True

Wyświetl plik

@ -7,7 +7,7 @@ base58==2.1.1
bitarray==2.7.6 bitarray==2.7.6
boto3==1.27.0 boto3==1.27.0
botocore==1.30.0 botocore==1.30.0
bugout==0.2.14 bugout==0.2.15
certifi==2023.5.7 certifi==2023.5.7
charset-normalizer==3.1.0 charset-normalizer==3.1.0
click==8.1.3 click==8.1.3

Wyświetl plik

@ -13,7 +13,7 @@ setup(
packages=find_packages(), packages=find_packages(),
install_requires=[ install_requires=[
"boto3", "boto3",
"bugout>=0.2.14", "bugout>=0.2.15",
"eip712==0.1.0", "eip712==0.1.0",
"eth-typing>=2.3.0", "eth-typing>=2.3.0",
"fastapi", "fastapi",

Wyświetl plik

@ -15,6 +15,9 @@ from bugout.data import (
BugoutResources, BugoutResources,
BugoutSearchResult, BugoutSearchResult,
BugoutSearchResults, BugoutSearchResults,
BugoutResourceHolder,
HolderType,
ResourcePermissions,
) )
from bugout.exceptions import BugoutResponseException from bugout.exceptions import BugoutResponseException
from bugout.journal import SearchOrder from bugout.journal import SearchOrder
@ -470,7 +473,7 @@ def upload_abi_to_s3(
def get_all_entries_from_search( def get_all_entries_from_search(
journal_id: str, search_query: str, limit: int, token: str journal_id: str, search_query: str, limit: int, token: str, content: bool = False
) -> List[BugoutSearchResult]: ) -> List[BugoutSearchResult]:
""" """
Get all required entries from journal using search interface Get all required entries from journal using search interface
@ -483,7 +486,7 @@ def get_all_entries_from_search(
token=token, token=token,
journal_id=journal_id, journal_id=journal_id,
query=search_query, query=search_query,
content=False, content=content,
timeout=10.0, timeout=10.0,
limit=limit, limit=limit,
offset=offset, offset=offset,
@ -496,7 +499,7 @@ def get_all_entries_from_search(
token=token, token=token,
journal_id=journal_id, journal_id=journal_id,
query=search_query, query=search_query,
content=False, content=content,
timeout=10.0, timeout=10.0,
limit=limit, limit=limit,
offset=offset, offset=offset,
@ -526,47 +529,45 @@ def apply_moonworm_tasks(
token=MOONSTREAM_ADMIN_ACCESS_TOKEN, token=MOONSTREAM_ADMIN_ACCESS_TOKEN,
) )
# create historical crawl task in journal
# will use create_entries_pack for creating entries in journal # will use create_entries_pack for creating entries in journal
existing_tags = [entry.tags for entry in entries] existing_tags = [entry.tags for entry in entries]
existing_hashes = [ existing_selectors = [
tag.split(":")[-1] tag.split(":")[-1] for tag in chain(*existing_tags) if "abi_selector" in tag
for tag in chain(*existing_tags)
if "abi_method_hash" in tag
] ]
abi_hashes_dict = { abi_selectors_dict = {
hashlib.md5(json.dumps(method).encode("utf-8")).hexdigest(): method Web3.keccak(
text=method["name"]
+ "("
+ ",".join(map(lambda x: x["type"], method["inputs"]))
+ ")"
)[:4].hex(): method
for method in abi for method in abi
if (method["type"] in ("event", "function")) if (method["type"] in ("event", "function"))
and (method.get("stateMutability", "") != "view") and (method.get("stateMutability", "") != "view")
} }
for hash in abi_hashes_dict: for abi_selector in abi_selectors_dict:
if hash not in existing_hashes: if abi_selector not in existing_selectors:
abi_selector = Web3.keccak( hash = hashlib.md5(
text=abi_hashes_dict[hash]["name"] json.dumps(abi_selectors_dict[abi_selector]).encode("utf-8")
+ "(" ).hexdigest()
+ ",".join(
map(lambda x: x["type"], abi_hashes_dict[hash]["inputs"])
)
+ ")"
)[:4].hex()
moonworm_abi_tasks_entries_pack.append( moonworm_abi_tasks_entries_pack.append(
{ {
"title": address, "title": address,
"content": json.dumps(abi_hashes_dict[hash], indent=4), "content": json.dumps(
abi_selectors_dict[abi_selector], indent=4
),
"tags": [ "tags": [
f"address:{address}", f"address:{address}",
f"type:{abi_hashes_dict[hash]['type']}", f"type:{abi_selectors_dict[abi_selector]['type']}",
f"abi_method_hash:{hash}", f"abi_method_hash:{hash}",
f"abi_selector:{abi_selector}", f"abi_selector:{abi_selector}",
f"subscription_type:{subscription_type}", f"subscription_type:{subscription_type}",
f"abi_name:{abi_hashes_dict[hash]['name']}", f"abi_name:{abi_selectors_dict[abi_selector]['name']}",
f"status:active", f"status:active",
f"task_type:moonworm", f"task_type:moonworm",
f"moonworm_task_pickedup:False", # True if task picked up by moonworm-crawler(default each 120 sec) f"moonworm_task_pickedup:False", # True if task picked up by moonworm-crawler(default each 120 sec)
@ -711,11 +712,7 @@ def generate_journal_for_user(
} }
try: try:
bc.create_resource( create_resource_for_user(user_id=user_id, resource_data=resource_data)
token=token,
application_id=MOONSTREAM_APPLICATION_ID,
resource_data=resource_data,
)
except BugoutResponseException as e: except BugoutResponseException as e:
raise MoonstreamHTTPException(status_code=e.status_code, detail=e.detail) raise MoonstreamHTTPException(status_code=e.status_code, detail=e.detail)
except Exception as e: except Exception as e:
@ -851,6 +848,8 @@ def get_list_of_support_interfaces(
Returns list of interfaces supported by given address Returns list of interfaces supported by given address
""" """
result = {}
try: try:
_, _, is_contract = check_if_smart_contract( _, _, is_contract = check_if_smart_contract(
blockchain_type=blockchain_type, address=address, user_token=user_token blockchain_type=blockchain_type, address=address, user_token=user_token
@ -866,8 +865,6 @@ def get_list_of_support_interfaces(
abi=supportsInterface_abi, abi=supportsInterface_abi,
) )
result = {}
if blockchain_type in multicall_contracts: if blockchain_type in multicall_contracts:
calls = [] calls = []
@ -952,3 +949,57 @@ def check_if_smart_contract(
is_contract = True is_contract = True
return blockchain_type, address, is_contract return blockchain_type, address, is_contract
def create_resource_for_user(
user_id: uuid.UUID,
resource_data: Dict[str, Any],
) -> BugoutResource:
"""
Create resource for user
"""
try:
resource = bc.create_resource(
token=MOONSTREAM_ADMIN_ACCESS_TOKEN,
application_id=MOONSTREAM_APPLICATION_ID,
resource_data=resource_data,
timeout=BUGOUT_REQUEST_TIMEOUT_SECONDS,
)
except BugoutResponseException as e:
raise MoonstreamHTTPException(status_code=e.status_code, detail=e.detail)
except Exception as e:
logger.error(f"Error creating resource: {str(e)}")
raise MoonstreamHTTPException(status_code=500, internal_error=e)
try:
bc.add_resource_holder_permissions(
token=MOONSTREAM_ADMIN_ACCESS_TOKEN,
resource_id=resource.id,
holder_permissions=BugoutResourceHolder(
holder_type=HolderType.user,
holder_id=user_id,
permissions=[
ResourcePermissions.ADMIN,
ResourcePermissions.READ,
ResourcePermissions.UPDATE,
ResourcePermissions.DELETE,
],
),
timeout=BUGOUT_REQUEST_TIMEOUT_SECONDS,
)
except BugoutResponseException as e:
logger.error(
f"Error adding resource holder permissions to resource resource {str(resource.id)} {str(e)}"
)
raise MoonstreamHTTPException(status_code=e.status_code, detail=e.detail)
except Exception as e:
bc.delete_resource(
token=MOONSTREAM_ADMIN_ACCESS_TOKEN,
resource_id=resource.id,
)
logger.error(
f"Error adding resource holder permissions to resource {str(resource.id)} {str(e)}"
)
raise MoonstreamHTTPException(status_code=500, internal_error=e)
return resource

Wyświetl plik

@ -12,7 +12,12 @@ from sqlalchemy.orm import with_expression
from moonstreamdb.db import SessionLocal from moonstreamdb.db import SessionLocal
from ..settings import BUGOUT_BROOD_URL, BUGOUT_SPIRE_URL, MOONSTREAM_APPLICATION_ID from ..settings import (
BUGOUT_BROOD_URL,
BUGOUT_SPIRE_URL,
MOONSTREAM_APPLICATION_ID,
MOONSTREAM_MOONWORM_TASKS_JOURNAL,
)
from ..web3_provider import yield_web3_provider from ..web3_provider import yield_web3_provider
from . import subscription_types, subscriptions, moonworm_tasks, queries from . import subscription_types, subscriptions, moonworm_tasks, queries
@ -20,6 +25,7 @@ from .migrations import (
checksum_address, checksum_address,
update_dashboard_subscription_key, update_dashboard_subscription_key,
generate_entity_subscriptions, generate_entity_subscriptions,
add_selectors,
) )
@ -87,6 +93,9 @@ steps:
- id: 20230501 - id: 20230501
name: fix_duplicates_keys_in_entity_subscription name: fix_duplicates_keys_in_entity_subscription
description: Fix entity duplicates keys for all subscriptions introduced in 20230213 description: Fix entity duplicates keys for all subscriptions introduced in 20230213
- id: 20230904
name fill_missing_selectors_in_moonworm_tasks
description: Get all moonworm jobs from moonworm journal and add selector tag if it not represent
""" """
logger.info(entity_migration_overview) logger.info(entity_migration_overview)
@ -117,6 +126,30 @@ def migrations_run(args: argparse.Namespace) -> None:
web3_session = yield_web3_provider() web3_session = yield_web3_provider()
db_session = SessionLocal() db_session = SessionLocal()
try: try:
if args.id == 20230904:
step_order = [
"fill_missing_selectors_in_moonworm_tasks",
"deduplicate_moonworm_tasks",
]
step_map: Dict[str, Dict[str, Any]] = {
"upgrade": {
"fill_missing_selectors_in_moonworm_tasks": {
"action": add_selectors.fill_missing_selectors_in_moonworm_tasks,
"description": "Get all moonworm jobs from moonworm journal and add selector tag if it not represent",
},
"deduplicate_moonworm_tasks": {
"action": add_selectors.deduplicate_moonworm_task_by_selector,
"description": "Deduplicate moonworm tasks by selector",
},
},
"downgrade": {},
}
if args.command not in ["upgrade", "downgrade"]:
logger.info("Wrong command. Please use upgrade or downgrade")
step = args.step
migration_run(step_map, args.command, step, step_order)
if args.id == 20230501: if args.id == 20230501:
# fix entity duplicates keys for all subscriptions introduced in 20230213 # fix entity duplicates keys for all subscriptions introduced in 20230213

Wyświetl plik

@ -0,0 +1,187 @@
"""
Add selectors to all moonworm tasks.
"""
import logging
import json
from bugout.exceptions import BugoutResponseException
from web3 import Web3
from ...settings import (
BUGOUT_REQUEST_TIMEOUT_SECONDS,
MOONSTREAM_ADMIN_ACCESS_TOKEN,
MOONSTREAM_MOONWORM_TASKS_JOURNAL,
)
from ...settings import bugout_client as bc
from ...actions import get_all_entries_from_search
logger = logging.getLogger(__name__)
def fill_missing_selectors_in_moonworm_tasks() -> None:
"""
Add selectors to all moonworm tasks.
"""
batch_size = 100
moonworm_tasks = get_all_entries_from_search(
journal_id=MOONSTREAM_MOONWORM_TASKS_JOURNAL,
token=MOONSTREAM_ADMIN_ACCESS_TOKEN,
search_query="#task_type:moonworm !#version:2.0",
limit=batch_size,
content=True,
)
logger.info(f"Found {len(moonworm_tasks)} moonworm tasks versions 1.0")
entries_tags = []
## batch tasks
for task_batch in [
moonworm_tasks[i : i + batch_size]
for i in range(0, len(moonworm_tasks), batch_size)
]:
count = 0
for task in task_batch:
tags = ["version:2.0"]
## get abi
try:
abi = json.loads(task.content)
except Exception as e:
logger.warn(
f"Unable to parse abi from task: {task.entry_url.split()[-1]}: {e}"
)
continue
if "name" not in abi:
logger.warn(
f"Unable to find abi name in task: {task.entry_url.split()[-1]}"
)
continue
if not any([tag.startswith("abi_selector:") for tag in task.tags]):
## generate selector
abi_selector = Web3.keccak(
text=abi["name"]
+ "("
+ ",".join(map(lambda x: x["type"], abi["inputs"]))
+ ")"
)[:4].hex()
tags.append(f"abi_selector:{abi_selector}")
count += 1
entries_tags.append(
{
"entry_id": task.entry_url.split("/")[-1], ## 😭
"tags": tags,
}
)
logger.info(f"Found {count} missing selectors in batch {len(task_batch)} tasks")
## update entries
try:
bc.create_entries_tags(
journal_id=MOONSTREAM_MOONWORM_TASKS_JOURNAL,
token=MOONSTREAM_ADMIN_ACCESS_TOKEN,
entries_tags=entries_tags,
timeout=15,
)
except BugoutResponseException as e:
logger.error(f"Unable to update entries tags: {e}")
continue
def deduplicate_moonworm_task_by_selector():
"""
Find moonworm tasks with same selector and remove old versions
"""
moonworm_tasks = get_all_entries_from_search(
journal_id=MOONSTREAM_MOONWORM_TASKS_JOURNAL,
token=MOONSTREAM_ADMIN_ACCESS_TOKEN,
search_query="#task_type:moonworm #version:2.0",
limit=100,
content=False,
)
logger.info(f"Found {len(moonworm_tasks)} moonworm tasks versions 2.0")
## loop over tasks
selectors = {}
for task in moonworm_tasks:
tags = task.tags
## get selector
selector = [tag for tag in tags if tag.startswith("abi_selector:")]
address = [tag for tag in tags if tag.startswith("address:")]
if len(selector) == 0:
logger.warn(
f"Unable to find selector in task: {task.entry_url.split()[-1]}"
)
continue
selector = selector[0].split(":")[1]
if len(address) == 0:
logger.warn(f"Unable to find address in task: {task.entry_url.split()[-1]}")
continue
address = address[0].split(":")[1]
if address not in selectors:
selectors[address] = {}
if selector not in selectors[address]:
selectors[address][selector] = {"entries": {}}
selectors[address][selector]["entries"][
task.entry_url.split("/")[-1]
] = task.created_at
logger.info(f"Found {len(selectors)} addresses")
for address, selectors_dict in selectors.items():
for selector, tasks_dict in selectors_dict.items():
if len(tasks_dict["entries"]) == 1:
continue
## find earliest task
earliest_task_id = min(
tasks_dict["entries"], key=lambda key: tasks_dict["entries"][key]
)
## remove all tasks except latest
logger.info(
f"Found {len(tasks_dict['entries'])} tasks with selector {selector} erliest task {earliest_task_id} with created_at: {tasks_dict['entries'][earliest_task_id]}"
)
for task_id in tasks_dict["entries"]:
if task_id == earliest_task_id:
continue
try:
bc.delete_entry(
journal_id=MOONSTREAM_MOONWORM_TASKS_JOURNAL,
entry_id=task_id,
token=MOONSTREAM_ADMIN_ACCESS_TOKEN,
)
except BugoutResponseException as e:
logger.error(f"Unable to delete entry with id {task_id} : {e}")
continue
logger.info(f"Deleted entry: {task_id}")

Wyświetl plik

@ -25,11 +25,11 @@ from ..actions import (
get_query_by_name, get_query_by_name,
name_normalization, name_normalization,
query_parameter_hash, query_parameter_hash,
create_resource_for_user,
) )
from ..middleware import MoonstreamHTTPException from ..middleware import MoonstreamHTTPException
from ..settings import ( from ..settings import (
MOONSTREAM_ADMIN_ACCESS_TOKEN, MOONSTREAM_ADMIN_ACCESS_TOKEN,
MOONSTREAM_APPLICATION_ID,
MOONSTREAM_CRAWLERS_SERVER_PORT, MOONSTREAM_CRAWLERS_SERVER_PORT,
MOONSTREAM_CRAWLERS_SERVER_URL, MOONSTREAM_CRAWLERS_SERVER_URL,
MOONSTREAM_INTERNAL_REQUEST_TIMEOUT_SECONDS, MOONSTREAM_INTERNAL_REQUEST_TIMEOUT_SECONDS,
@ -130,24 +130,16 @@ async def create_query_handler(
except Exception as e: except Exception as e:
raise MoonstreamHTTPException(status_code=500, internal_error=e) raise MoonstreamHTTPException(status_code=500, internal_error=e)
try: create_resource_for_user(
# create resource query_name_resolver user_id=user.id,
bc.create_resource( resource_data={
token=token, "type": data.BUGOUT_RESOURCE_QUERY_RESOLVER,
application_id=MOONSTREAM_APPLICATION_ID, "user_id": str(user.id),
resource_data={ "user": str(user.username),
"type": data.BUGOUT_RESOURCE_QUERY_RESOLVER, "name": query_name,
"user_id": str(user.id), "entry_id": str(entry.id),
"user": str(user.username), },
"name": query_name, )
"entry_id": str(entry.id),
},
)
except BugoutResponseException as e:
logger.error(f"Error creating name resolving resource: {str(e)}")
raise MoonstreamHTTPException(status_code=e.status_code, detail=e.detail)
except Exception as e:
raise MoonstreamHTTPException(status_code=500, internal_error=e)
try: try:
bc.update_tags( bc.update_tags(
@ -355,7 +347,7 @@ async def update_query_handler(
token=MOONSTREAM_ADMIN_ACCESS_TOKEN, token=MOONSTREAM_ADMIN_ACCESS_TOKEN,
journal_id=MOONSTREAM_QUERIES_JOURNAL_ID, journal_id=MOONSTREAM_QUERIES_JOURNAL_ID,
entry_id=query_id, entry_id=query_id,
title=query_name, title=f"Query:{query_name}",
content=request_update.query, content=request_update.query,
tags=["preapprove"], tags=["preapprove"],
) )
@ -620,7 +612,9 @@ async def remove_query_handler(
raise MoonstreamHTTPException(status_code=404, detail="Query does not exists") raise MoonstreamHTTPException(status_code=404, detail="Query does not exists")
try: try:
bc.delete_resource(token=token, resource_id=query_ids[query_name][0]) bc.delete_resource(
token=MOONSTREAM_ADMIN_ACCESS_TOKEN, resource_id=query_ids[query_name][0]
)
except BugoutResponseException as e: except BugoutResponseException as e:
raise MoonstreamHTTPException(status_code=e.status_code, detail=e.detail) raise MoonstreamHTTPException(status_code=e.status_code, detail=e.detail)
except Exception as e: except Exception as e:

Wyświetl plik

@ -9,7 +9,7 @@ base58==2.1.1
bitarray==2.6.0 bitarray==2.6.0
boto3==1.26.5 boto3==1.26.5
botocore==1.29.5 botocore==1.29.5
bugout>=0.2.13 bugout>=0.2.15
certifi==2022.9.24 certifi==2022.9.24
charset-normalizer==2.1.1 charset-normalizer==2.1.1
click==8.1.3 click==8.1.3

Wyświetl plik

@ -13,7 +13,7 @@ setup(
install_requires=[ install_requires=[
"appdirs", "appdirs",
"boto3", "boto3",
"bugout>=0.2.13", "bugout>=0.2.15",
"fastapi", "fastapi",
"moonstreamdb>=0.3.5", "moonstreamdb>=0.3.5",
"humbug", "humbug",

Wyświetl plik

@ -198,7 +198,7 @@ func (bpool *BlockchainPool) HealthCheck() {
for _, b := range bpool.Blockchains { for _, b := range bpool.Blockchains {
var timeout time.Duration var timeout time.Duration
getLatestBlockReq := `{"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["latest", false],"id":1}` getLatestBlockReq := `{"jsonrpc":"2.0","method":"eth_getBlockByNumber","params":["latest", false],"id":1}`
if b.Blockchain == "starknet" || b.Blockchain == "starknet-goerli" { if b.Blockchain == "starknet" || b.Blockchain == "starknet-goerli" || b.Blockchain == "starknet-sepolia" {
getLatestBlockReq = `{"jsonrpc":"2.0","method":"starknet_getBlockWithTxHashes","params":["latest"],"id":"0"}` getLatestBlockReq = `{"jsonrpc":"2.0","method":"starknet_getBlockWithTxHashes","params":["latest"],"id":"0"}`
timeout = NB_HEALTH_CHECK_CALL_TIMEOUT * 2 timeout = NB_HEALTH_CHECK_CALL_TIMEOUT * 2
} }
@ -241,7 +241,7 @@ func (bpool *BlockchainPool) HealthCheck() {
} }
var blockNumber uint64 var blockNumber uint64
if b.Blockchain == "starknet" || b.Blockchain == "starknet-goerli" { if b.Blockchain == "starknet" || b.Blockchain == "starknet-goerli" || b.Blockchain == "starknet-sepolia" {
blockNumber = statusResponse.Result.BlockNumber blockNumber = statusResponse.Result.BlockNumber
} else { } else {
blockNumberHex := strings.Replace(statusResponse.Result.Number, "0x", "", -1) blockNumberHex := strings.Replace(statusResponse.Result.Number, "0x", "", -1)

Wyświetl plik

@ -35,6 +35,7 @@ var (
NB_CONTROLLER_TOKEN = os.Getenv("NB_CONTROLLER_TOKEN") NB_CONTROLLER_TOKEN = os.Getenv("NB_CONTROLLER_TOKEN")
NB_CONTROLLER_ACCESS_ID = os.Getenv("NB_CONTROLLER_ACCESS_ID") NB_CONTROLLER_ACCESS_ID = os.Getenv("NB_CONTROLLER_ACCESS_ID")
MOONSTREAM_CORS_ALLOWED_ORIGINS = os.Getenv("MOONSTREAM_CORS_ALLOWED_ORIGINS") MOONSTREAM_CORS_ALLOWED_ORIGINS = os.Getenv("MOONSTREAM_CORS_ALLOWED_ORIGINS")
CORS_WHITELIST_MAP = make(map[string]bool)
NB_CONNECTION_RETRIES = 2 NB_CONNECTION_RETRIES = 2
NB_CONNECTION_RETRIES_INTERVAL = time.Millisecond * 10 NB_CONNECTION_RETRIES_INTERVAL = time.Millisecond * 10
@ -86,6 +87,9 @@ func CheckEnvVarSet() {
NB_CONTROLLER_ACCESS_ID = uuid.New().String() NB_CONTROLLER_ACCESS_ID = uuid.New().String()
log.Printf("Access ID for internal usage in NB_CONTROLLER_ACCESS_ID environment variable is not valid uuid, generated random one: %v", NB_CONTROLLER_ACCESS_ID) log.Printf("Access ID for internal usage in NB_CONTROLLER_ACCESS_ID environment variable is not valid uuid, generated random one: %v", NB_CONTROLLER_ACCESS_ID)
} }
for _, o := range strings.Split(MOONSTREAM_CORS_ALLOWED_ORIGINS, ",") {
CORS_WHITELIST_MAP[o] = true
}
} }
// Nodes configuration // Nodes configuration

Wyświetl plik

@ -368,19 +368,29 @@ func panicMiddleware(next http.Handler) http.Handler {
// CORS middleware // CORS middleware
func corsMiddleware(next http.Handler) http.Handler { func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodOptions { var allowedOrigin string
for _, allowedOrigin := range strings.Split(MOONSTREAM_CORS_ALLOWED_ORIGINS, ",") { if CORS_WHITELIST_MAP["*"] {
if r.Header.Get("Origin") == allowedOrigin { allowedOrigin = "*"
w.Header().Set("Access-Control-Allow-Origin", allowedOrigin) } else {
w.Header().Set("Access-Control-Allow-Methods", "GET,POST") origin := r.Header.Get("Origin")
// Credentials are cookies, authorization headers, or TLS client certificates if _, ok := CORS_WHITELIST_MAP[origin]; ok {
w.Header().Set("Access-Control-Allow-Credentials", "true") allowedOrigin = origin
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
}
} }
w.WriteHeader(http.StatusNoContent) }
if allowedOrigin != "" {
w.Header().Set("Access-Control-Allow-Origin", allowedOrigin)
w.Header().Set("Access-Control-Allow-Methods", "GET,POST,OPTIONS")
// Credentials are cookies, authorization headers, or TLS client certificates
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
}
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return return
} }
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
}) })
} }

Wyświetl plik

@ -31,7 +31,7 @@ var (
func initHealthCheck(debug bool) { func initHealthCheck(debug bool) {
healthCheckInterval, convErr := strconv.Atoi(NB_HEALTH_CHECK_INTERVAL) healthCheckInterval, convErr := strconv.Atoi(NB_HEALTH_CHECK_INTERVAL)
if convErr != nil { if convErr != nil {
healthCheckInterval = 5 healthCheckInterval = 30
} }
t := time.NewTicker(time.Second * time.Duration(healthCheckInterval)) t := time.NewTicker(time.Second * time.Duration(healthCheckInterval))
for { for {