moonstream/backend/moonstreamapi/actions.py

603 wiersze
20 KiB
Python
Czysty Zwykły widok Historia

2021-12-03 20:53:05 +00:00
import hashlib
2021-08-23 17:03:45 +00:00
import json
2021-12-03 20:53:05 +00:00
from itertools import chain
2021-08-06 14:03:24 +00:00
import logging
from typing import List, Optional, Dict, Any
2021-12-16 13:26:04 +00:00
from enum import Enum
2021-09-06 17:28:16 +00:00
import uuid
2021-09-01 12:03:44 +00:00
2021-08-24 13:50:36 +00:00
import boto3 # type: ignore
2022-02-17 10:22:20 +00:00
from bugout.data import (
BugoutSearchResults,
BugoutSearchResult,
BugoutResource,
BugoutResources,
)
2021-09-29 16:19:31 +00:00
from bugout.journal import SearchOrder
2022-02-17 10:22:20 +00:00
from bugout.exceptions import BugoutResponseException
2021-10-26 10:06:25 +00:00
from ens.utils import is_valid_ens_name # type: ignore
from eth_utils.address import is_address # type: ignore
2021-12-16 13:26:04 +00:00
from moonstreamdb.models import EthereumLabel
2022-03-07 18:08:35 +00:00
from slugify import slugify # type: ignore
2021-08-27 15:28:47 +00:00
from sqlalchemy import text
2021-09-01 12:03:44 +00:00
from sqlalchemy.orm import Session
2021-12-16 13:26:04 +00:00
from web3 import Web3
2021-11-09 12:33:10 +00:00
from web3._utils.validation import validate_abi
2021-08-06 14:03:24 +00:00
from . import data
2021-10-28 15:48:15 +00:00
from .middleware import MoonstreamHTTPException
2021-12-16 13:26:04 +00:00
from .reporter import reporter
2021-09-06 17:21:05 +00:00
from .settings import (
BUGOUT_REQUEST_TIMEOUT_SECONDS,
2021-12-16 13:26:04 +00:00
ETHERSCAN_SMARTCONTRACTS_BUCKET,
2021-09-29 16:19:31 +00:00
MOONSTREAM_ADMIN_ACCESS_TOKEN,
2021-12-16 13:26:04 +00:00
MOONSTREAM_APPLICATION_ID,
2021-09-29 16:19:31 +00:00
MOONSTREAM_DATA_JOURNAL_ID,
MOONSTREAM_S3_SMARTCONTRACTS_ABI_BUCKET,
MOONSTREAM_S3_SMARTCONTRACTS_ABI_PREFIX,
2021-12-03 20:53:05 +00:00
MOONSTREAM_MOONWORM_TASKS_JOURNAL,
2021-09-06 17:21:05 +00:00
)
2021-12-16 13:26:04 +00:00
from .settings import bugout_client as bc
2021-08-06 14:03:24 +00:00
logger = logging.getLogger(__name__)
blockchain_by_subscription_id = {
"ethereum_blockchain": "ethereum",
"polygon_blockchain": "polygon",
2021-12-13 17:43:22 +00:00
"ethereum_smartcontract": "ethereum",
"polygon_smartcontract": "polygon",
}
2021-09-29 16:19:31 +00:00
class StatusAPIException(Exception):
"""
Raised during checking Moonstream API statuses.
"""
2022-03-08 15:06:50 +00:00
class NameNormalizationException(Exception):
"""
Raised on actions when slugify can't normalize name.
"""
2022-03-09 13:05:31 +00:00
2022-03-09 12:29:02 +00:00
class ResourceQueryFetchException(Exception):
"""
Exception in queries API
"""
2022-03-08 15:06:50 +00:00
2021-10-26 08:12:09 +00:00
class LabelNames(Enum):
ETHERSCAN_SMARTCONTRACT = "etherscan_smartcontract"
COINMARKETCAP_TOKEN = "coinmarketcap_token"
ERC721 = "erc721"
2021-08-24 12:05:17 +00:00
def get_contract_source_info(
db_session: Session, contract_address: str
) -> Optional[data.EthereumSmartContractSourceInfo]:
2021-10-26 08:12:09 +00:00
label = (
2021-10-14 15:01:00 +00:00
db_session.query(EthereumLabel)
.filter(EthereumLabel.address == contract_address)
2021-10-26 08:12:09 +00:00
.filter(EthereumLabel.label == LabelNames.ETHERSCAN_SMARTCONTRACT.value)
.one_or_none()
2021-08-06 14:03:24 +00:00
)
2021-10-26 08:12:09 +00:00
if label is None:
2021-10-14 15:01:00 +00:00
return None
2021-08-06 14:03:24 +00:00
2021-10-26 08:12:09 +00:00
object_uri = label.label_data["object_uri"]
key = object_uri.split("s3://etherscan-smart-contracts/")[1]
s3 = boto3.client("s3")
bucket = ETHERSCAN_SMARTCONTRACTS_BUCKET
try:
raw_obj = s3.get_object(Bucket=bucket, Key=key)
obj_data = json.loads(raw_obj["Body"].read().decode("utf-8"))["data"]
contract_source_info = data.EthereumSmartContractSourceInfo(
name=obj_data["ContractName"],
source_code=obj_data["SourceCode"],
compiler_version=obj_data["CompilerVersion"],
abi=obj_data["ABI"],
)
return contract_source_info
except Exception as e:
logger.error(f"Failed to load smart contract {object_uri}")
reporter.error_report(e)
2021-08-16 14:41:41 +00:00
return None
2021-08-16 14:41:41 +00:00
def get_ens_name(web3: Web3, address: str) -> Optional[str]:
try:
checksum_address = web3.toChecksumAddress(address)
except:
raise ValueError(f"{address} is invalid ethereum address is passed")
try:
ens_name = web3.ens.name(checksum_address)
return ens_name
except Exception as e:
reporter.error_report(e, ["web3", "ens"])
logger.error(
f"Cannot get ens name for address {checksum_address}. Probably node is down"
)
raise e
def get_ens_address(web3: Web3, name: str) -> Optional[str]:
if not is_valid_ens_name(name):
raise ValueError(f"{name} is not valid ens name")
try:
ens_checksum_address = web3.ens.address(name)
if ens_checksum_address is not None:
ordinary_address = ens_checksum_address.lower()
return ordinary_address
return None
except Exception as e:
reporter.error_report(e, ["web3", "ens"])
logger.error(f"Cannot get ens address for name {name}. Probably node is down")
raise e
2021-08-25 14:04:58 +00:00
def get_ethereum_address_info(
2021-10-26 08:12:09 +00:00
db_session: Session, web3: Web3, address: str
2021-08-25 14:04:58 +00:00
) -> Optional[data.EthereumAddressInfo]:
2021-08-27 15:28:47 +00:00
if not is_address(address):
raise ValueError(f"Invalid ethereum address : {address}")
2021-08-30 13:46:51 +00:00
address_info = data.EthereumAddressInfo(address=address)
try:
address_info.ens_name = get_ens_name(web3, address)
except:
pass
2021-08-30 13:46:51 +00:00
etherscan_address_url = f"https://etherscan.io/address/{address}"
etherscan_token_url = f"https://etherscan.io/token/{address}"
2021-08-30 13:46:51 +00:00
blockchain_com_url = f"https://www.blockchain.com/eth/address/{address}"
2021-10-26 08:12:09 +00:00
2021-08-27 15:28:47 +00:00
coinmarketcap_label: Optional[EthereumLabel] = (
db_session.query(EthereumLabel)
2021-10-14 15:01:00 +00:00
.filter(EthereumLabel.address == address)
2021-08-27 15:28:47 +00:00
.filter(EthereumLabel.label == LabelNames.COINMARKETCAP_TOKEN.value)
.order_by(text("created_at desc"))
.limit(1)
.one_or_none()
2021-08-25 14:04:58 +00:00
)
2021-10-26 08:12:09 +00:00
2021-08-27 15:28:47 +00:00
if coinmarketcap_label is not None:
2021-08-30 13:46:51 +00:00
address_info.token = data.EthereumTokenDetails(
name=coinmarketcap_label.label_data["name"],
symbol=coinmarketcap_label.label_data["symbol"],
external_url=[
coinmarketcap_label.label_data["coinmarketcap_url"],
etherscan_token_url,
2021-08-30 13:46:51 +00:00
blockchain_com_url,
],
)
2021-08-27 15:28:47 +00:00
# Checking for smart contract
etherscan_label: Optional[EthereumLabel] = (
db_session.query(EthereumLabel)
2021-10-14 15:01:00 +00:00
.filter(EthereumLabel.address == address)
2021-08-27 15:28:47 +00:00
.filter(EthereumLabel.label == LabelNames.ETHERSCAN_SMARTCONTRACT.value)
.order_by(text("created_at desc"))
.limit(1)
.one_or_none()
)
if etherscan_label is not None:
2021-08-30 13:46:51 +00:00
address_info.smart_contract = data.EthereumSmartContractDetails(
name=etherscan_label.label_data["name"],
external_url=[etherscan_address_url, blockchain_com_url],
)
2021-08-25 14:04:58 +00:00
# Checking for NFT
# Checking for smart contract
erc721_label: Optional[EthereumLabel] = (
db_session.query(EthereumLabel)
2021-10-14 15:01:00 +00:00
.filter(EthereumLabel.address == address)
.filter(EthereumLabel.label == LabelNames.ERC721.value)
.order_by(text("created_at desc"))
.limit(1)
.one_or_none()
)
if erc721_label is not None:
2021-09-06 13:24:14 +00:00
address_info.nft = data.EthereumNFTDetails(
name=erc721_label.label_data.get("name"),
symbol=erc721_label.label_data.get("symbol"),
2021-09-06 13:24:14 +00:00
total_supply=erc721_label.label_data.get("totalSupply"),
external_url=[etherscan_token_url, blockchain_com_url],
)
2021-08-25 14:04:58 +00:00
return address_info
2021-08-16 14:41:41 +00:00
def get_address_labels(
2021-08-24 13:50:36 +00:00
db_session: Session, start: int, limit: int, addresses: Optional[str] = None
) -> data.AddressListLabelsResponse:
2021-08-16 14:41:41 +00:00
"""
Attach labels to addresses.
"""
if addresses is not None:
addresses_list = addresses.split(",")
2021-10-14 15:01:00 +00:00
addresses_obj = addresses_list[start : start + limit]
else:
addresses_obj = []
2021-08-16 14:41:41 +00:00
addresses_response = data.AddressListLabelsResponse(addresses=[])
for address in addresses_obj:
labels_obj = (
db_session.query(EthereumLabel)
2021-10-14 15:01:00 +00:00
.filter(EthereumLabel.address == address)
2021-08-16 14:41:41 +00:00
.all()
)
addresses_response.addresses.append(
data.AddressLabelsResponse(
2021-10-14 15:01:00 +00:00
address=address,
2021-08-16 14:41:41 +00:00
labels=[
data.AddressLabelResponse(
label=label.label, label_data=label.label_data
)
for label in labels_obj
],
)
)
return addresses_response
2021-09-06 17:21:05 +00:00
def create_onboarding_resource(
token: uuid.UUID,
resource_data: Dict[str, Any] = {
"type": data.USER_ONBOARDING_STATE,
2022-03-08 15:10:06 +00:00
"steps": {
"welcome": 0,
"subscriptions": 0,
"stream": 0,
},
2021-09-06 17:21:05 +00:00
"is_complete": False,
},
) -> BugoutResource:
resource = bc.create_resource(
token=token,
application_id=MOONSTREAM_APPLICATION_ID,
resource_data=resource_data,
timeout=BUGOUT_REQUEST_TIMEOUT_SECONDS,
)
return resource
2021-09-29 16:19:31 +00:00
def check_api_status():
crawl_types_timestamp: Dict[str, Any] = {
"ethereum_txpool": None,
"ethereum_trending": None,
}
for crawl_type in crawl_types_timestamp.keys():
try:
search_results: BugoutSearchResults = bc.search(
token=MOONSTREAM_ADMIN_ACCESS_TOKEN,
journal_id=MOONSTREAM_DATA_JOURNAL_ID,
query=f"tag:crawl_type:{crawl_type}",
limit=1,
content=False,
timeout=10.0,
order=SearchOrder.DESCENDING,
)
if len(search_results.results) == 1:
crawl_types_timestamp[crawl_type] = search_results.results[0].created_at
except Exception:
raise StatusAPIException(
f"Unable to get status for crawler with type: {crawl_type}"
)
return crawl_types_timestamp
2021-10-28 15:48:15 +00:00
def json_type(evm_type: str) -> type:
if evm_type.startswith(("uint", "int")):
return int
elif evm_type.startswith("bytes") or evm_type == "string" or evm_type == "address":
return str
elif evm_type == "bool":
return bool
else:
raise ValueError(f"Cannot convert to python type {evm_type}")
def dashboards_abi_validation(
2022-03-08 15:10:06 +00:00
dashboard_subscription: data.DashboardMeta,
abi: Any,
s3_path: str,
2021-10-28 15:48:15 +00:00
):
"""
Validate current dashboard subscription : https://github.com/bugout-dev/moonstream/issues/345#issuecomment-953052444
with contract abi on S3
"""
# maybe its over but not found beter way
abi_functions = {
item["name"]: {inputs["name"]: inputs["type"] for inputs in item["inputs"]}
for item in abi
if item["type"] == "function"
}
if not dashboard_subscription.all_methods:
for method in dashboard_subscription.methods:
if method["name"] not in abi_functions:
# Method not exists
logger.error(
f"Error on dashboard resource validation method:{method['name']}"
f" of subscription: {dashboard_subscription.subscription_id}"
f"does not exists in Abi {s3_path}"
2021-10-28 15:48:15 +00:00
)
raise MoonstreamHTTPException(status_code=400)
2021-11-01 11:53:37 +00:00
if method.get("filters") and isinstance(method["filters"], dict):
2021-10-28 15:48:15 +00:00
for input_argument_name, value in method["filters"].items():
if input_argument_name not in abi_functions[method["name"]]:
# Argument not exists
logger.error(
f"Error on dashboard resource validation type argument: {input_argument_name} of method:{method['name']} "
f" of subscription: {dashboard_subscription.subscription_id} has incorrect"
f"does not exists in Abi {s3_path}"
2021-10-28 15:48:15 +00:00
)
raise MoonstreamHTTPException(status_code=400)
if not isinstance(
2021-11-01 11:53:37 +00:00
value,
2021-10-28 15:48:15 +00:00
json_type(abi_functions[method["name"]][input_argument_name]),
):
# Argument has incorrect type
logger.error(
f"Error on dashboard resource validation type argument: {input_argument_name} of method:{method['name']} "
f" of subscription: {dashboard_subscription.subscription_id} has incorrect type {type(value)}"
f" when {abi_functions[method['name']][input_argument_name]} required."
)
raise MoonstreamHTTPException(status_code=400)
abi_events = {
item["name"]: {inputs["name"]: inputs["type"] for inputs in item["inputs"]}
for item in abi
if item["type"] == "event"
}
if not dashboard_subscription.all_events:
for event in dashboard_subscription.events:
if event["name"] not in abi_events:
logger.error(
f"Error on dashboard resource validation event:{event['name']}"
f" of subscription: {dashboard_subscription.subscription_id}"
f"does not exists in Abi {s3_path}"
2021-10-28 15:48:15 +00:00
)
raise MoonstreamHTTPException(status_code=400)
2021-11-01 11:53:37 +00:00
if event.get("filters") and isinstance(event["filters"], dict):
2021-10-28 15:48:15 +00:00
for input_argument_name, value in event["filters"].items():
2021-11-01 11:53:37 +00:00
if input_argument_name not in abi_events[event["name"]]:
2021-10-28 15:48:15 +00:00
# Argument not exists
logger.error(
f"Error on dashboard resource validation type argument: {input_argument_name} of method:{event['name']} "
f" of subscription: {dashboard_subscription.subscription_id} has incorrect"
f"does not exists in Abi {s3_path}"
2021-10-28 15:48:15 +00:00
)
raise MoonstreamHTTPException(status_code=400)
if not isinstance(
2021-11-01 11:53:37 +00:00
value,
2021-10-28 15:48:15 +00:00
json_type(abi_events[event["name"]][input_argument_name]),
):
logger.error(
f"Error on dashboard resource validation type argument: {input_argument_name} of method:{event['name']} "
f" of subscription: {dashboard_subscription.subscription_id} has incorrect type {type(value)}"
2021-11-01 11:53:37 +00:00
f" when {abi_events[event['name']][input_argument_name]} required."
2021-10-28 15:48:15 +00:00
)
raise MoonstreamHTTPException(status_code=400)
2021-11-01 11:53:37 +00:00
return True
2021-11-11 14:43:51 +00:00
def validate_abi_json(abi: Any) -> None:
2021-11-09 12:33:10 +00:00
"""
Transform string to json and run validation
"""
try:
2021-11-11 14:37:19 +00:00
validate_abi(abi)
2021-11-09 12:33:10 +00:00
except ValueError as e:
raise MoonstreamHTTPException(status_code=400, detail=e)
except:
raise MoonstreamHTTPException(
status_code=400, detail="Error on abi valiadation."
)
def upload_abi_to_s3(
2022-03-08 15:10:06 +00:00
resource: BugoutResource,
abi: str,
update: Dict[str, Any],
2021-11-09 12:33:10 +00:00
) -> Dict[str, Any]:
"""
Uploading ABI to s3 bucket. Return object for updating resource.
"""
s3_client = boto3.client("s3")
bucket = MOONSTREAM_S3_SMARTCONTRACTS_ABI_BUCKET
2021-11-09 12:33:10 +00:00
result_bytes = abi.encode("utf-8")
result_key = f"{MOONSTREAM_S3_SMARTCONTRACTS_ABI_PREFIX}/{blockchain_by_subscription_id[resource.resource_data['subscription_type_id']]}/abi/{resource.resource_data['address']}/{resource.id}/abi.json"
2021-11-09 12:33:10 +00:00
s3_client.put_object(
Body=result_bytes,
Bucket=bucket,
Key=result_key,
ContentType="application/json",
Metadata={"Moonstream": "Abi data"},
)
update["abi"] = True
update["bucket"] = MOONSTREAM_S3_SMARTCONTRACTS_ABI_BUCKET
update["s3_path"] = result_key
2021-11-09 12:33:10 +00:00
return update
2021-12-03 20:53:05 +00:00
def get_all_entries_from_search(
journal_id: str, search_query: str, limit: int, token: str
) -> List[BugoutSearchResult]:
2021-12-03 20:53:05 +00:00
"""
Get all required entries from journal using search interface
"""
offset = 0
results: List[BugoutSearchResult] = []
2021-12-03 20:53:05 +00:00
2021-12-03 22:37:20 +00:00
try:
existing_metods = bc.search(
token=token,
journal_id=journal_id,
query=search_query,
content=False,
timeout=10.0,
limit=limit,
offset=offset,
)
results.extend(existing_metods.results)
2021-12-06 14:38:08 +00:00
except Exception as e:
2021-12-06 15:28:13 +00:00
reporter.error_report(e)
2021-12-03 22:37:20 +00:00
if len(results) != existing_metods.total_results:
for offset in range(limit, existing_metods.total_results, limit):
2021-12-03 20:53:05 +00:00
existing_metods = bc.search(
token=token,
journal_id=journal_id,
query=search_query,
content=False,
timeout=10.0,
limit=limit,
offset=offset,
)
2021-12-03 22:37:20 +00:00
results.extend(existing_metods.results)
2021-12-03 20:53:05 +00:00
return results
2022-03-08 15:10:06 +00:00
def apply_moonworm_tasks(
subscription_type: str,
abi: Any,
address: str,
) -> None:
2021-12-03 20:53:05 +00:00
"""
Get list of subscriptions loads abi and apply them as moonworm tasks if it not exist
"""
entries_pack = []
try:
entries = get_all_entries_from_search(
journal_id=MOONSTREAM_MOONWORM_TASKS_JOURNAL,
search_query=f"tag:address:{address} tag:subscription_type:{subscription_type}",
limit=100,
token=MOONSTREAM_ADMIN_ACCESS_TOKEN,
)
2021-12-06 15:21:13 +00:00
existing_tags = [entry.tags for entry in entries]
existing_hashes = [
tag.split(":")[-1]
for tag in chain(*existing_tags)
2021-12-09 14:11:39 +00:00
if "abi_method_hash" in tag
]
abi_hashes_dict = {
hashlib.md5(json.dumps(method).encode("utf-8")).hexdigest(): method
for method in abi
if (method["type"] in ("event", "function"))
and (method.get("stateMutability", "") != "view")
}
for hash in abi_hashes_dict:
if hash not in existing_hashes:
entries_pack.append(
{
"title": address,
"content": json.dumps(abi_hashes_dict[hash], indent=4),
"tags": [
f"address:{address}",
f"type:{abi_hashes_dict[hash]['type']}",
2021-12-09 14:11:39 +00:00
f"abi_method_hash:{hash}",
f"subscription_type:{subscription_type}",
f"abi_name:{abi_hashes_dict[hash]['name']}",
f"status:active",
],
}
2021-12-06 15:21:13 +00:00
)
except Exception as e:
reporter.error_report(e)
2021-12-03 20:53:05 +00:00
if len(entries_pack) > 0:
bc.create_entries_pack(
token=MOONSTREAM_ADMIN_ACCESS_TOKEN,
journal_id=MOONSTREAM_MOONWORM_TASKS_JOURNAL,
entries=entries_pack,
2021-12-06 15:28:13 +00:00
timeout=15,
2021-12-03 20:53:05 +00:00
)
2022-02-17 10:22:20 +00:00
2022-03-07 18:08:35 +00:00
def name_normalization(query_name: str) -> str:
"""
2022-03-09 12:29:02 +00:00
Sanitize provided query name.
2022-03-07 18:08:35 +00:00
"""
try:
2022-03-09 13:05:31 +00:00
normalized_query_name = slugify(
query_name, max_length=50, lowercase=False, separator="_"
)
2022-03-07 18:08:35 +00:00
except Exception as e:
2022-03-08 15:06:50 +00:00
logger.error(f"Error in query normalization. Error: {e}")
raise NameNormalizationException(f"Can't normalize name:{query_name}")
2022-03-07 18:08:35 +00:00
return normalized_query_name
2022-02-17 10:22:20 +00:00
def get_query_by_name(query_name: str, token: uuid.UUID) -> str:
2022-03-09 12:29:02 +00:00
"""
Fetch query_id from Brood resources.
"""
try:
query_name = name_normalization(query_name)
except Exception:
raise NameNormalizationException("Unable to normalize query name")
2022-03-07 18:08:35 +00:00
2022-03-07 17:22:00 +00:00
params = {"type": data.BUGOUT_RESOURCE_QUERY_RESOLVER, "name": query_name}
2022-02-17 10:22:20 +00:00
try:
resources: BugoutResources = bc.list_resources(token=token, params=params)
except BugoutResponseException as e:
raise MoonstreamHTTPException(status_code=e.status_code, detail=e.detail)
except Exception as e:
logger.error(f"Error get query, error: {str(e)}")
raise MoonstreamHTTPException(status_code=500, internal_error=e)
available_queries: Dict[str, str] = {
resource.resource_data["name"]: resource.resource_data["entry_id"]
for resource in resources.resources
}
if query_name not in available_queries:
raise MoonstreamHTTPException(status_code=404, detail="Query not found.")
query_id = available_queries[query_name]
return query_id