""" Utilities for managing subscription type resources for a Moonstream application. """ import argparse import json from typing import Dict, List, Optional from bugout.data import BugoutResource, BugoutResources from sqlalchemy.sql.expression import update from ..data import SubscriptionTypeResourceData from ..settings import ( BUGOUT_REQUEST_TIMEOUT_SECONDS, MOONSTREAM_ADMIN_ACCESS_TOKEN, MOONSTREAM_APPLICATION_ID, ) from ..settings import bugout_client as bc CANONICAL_SUBSCRIPTION_TYPES = { "ethereum_smartcontract": SubscriptionTypeResourceData( id="ethereum_smartcontract", name="Ethereum smartcontracts", blockchain="ethereum", choices=["input:address", "tag:erc721"], description="Contracts events and tx_calls of contract of Ethereum blockchain", icon_url="https://s3.amazonaws.com/static.simiotics.com/moonstream/assets/ethereum/eth-diamond-purple.png", stripe_product_id=None, stripe_price_id=None, active=True, ), "polygon_smartcontract": SubscriptionTypeResourceData( id="polygon_smartcontract", name="Polygon smartcontracts", blockchain="polygon", choices=["input:address", "tag:erc721"], description="Contracts events and tx_calls of contract of Polygon blockchain", icon_url="https://s3.amazonaws.com/static.simiotics.com/moonstream/assets/matic-token-inverted-icon.png", stripe_product_id=None, stripe_price_id=None, active=True, ), "mumbai_smartcontract": SubscriptionTypeResourceData( id="mumbai_smartcontract", name="Mumbai smartcontracts", blockchain="mumbai", choices=["input:address", "tag:erc721"], description="Contracts events and tx_calls of contract of Mumbai blockchain", icon_url="https://s3.amazonaws.com/static.simiotics.com/moonstream/assets/matic-token-inverted-icon.png", stripe_product_id=None, stripe_price_id=None, active=True, ), "xdai_smartcontract": SubscriptionTypeResourceData( id="xdai_smartcontract", name="XDai smartcontract", blockchain="xdai", choices=["input:address", "tag:erc721"], description="Contracts events and tx_calls of contract of XDai blockchain.", icon_url="https://s3.amazonaws.com/static.simiotics.com/moonstream/assets/xdai-token-logo.png", stripe_product_id=None, stripe_price_id=None, active=True, ), "wyrm_smartcontract": SubscriptionTypeResourceData( id="wyrm_smartcontract", name="Wyrm smartcontract", blockchain="wyrm", choices=["input:address", "tag:erc721"], description="Contracts events and tx_calls of contract of Wyrm blockchain.", icon_url="https://s3.amazonaws.com/static.simiotics.com/moonstream/assets/great-wyrm-network-logo.png", stripe_product_id=None, stripe_price_id=None, active=True, ), "ethereum_blockchain": SubscriptionTypeResourceData( id="ethereum_blockchain", name="Ethereum transactions", blockchain="ethereum", choices=["input:address", "tag:erc721"], description="Transactions that have been mined into the Ethereum blockchain", icon_url="https://s3.amazonaws.com/static.simiotics.com/moonstream/assets/ethereum/eth-diamond-purple.png", stripe_product_id=None, stripe_price_id=None, active=True, ), "polygon_blockchain": SubscriptionTypeResourceData( id="polygon_blockchain", name="Polygon transactions", blockchain="polygon", choices=["input:address", "tag:erc721"], description="Transactions that have been mined into the Polygon blockchain", icon_url="https://s3.amazonaws.com/static.simiotics.com/moonstream/assets/matic-token-inverted-icon.png", stripe_product_id=None, stripe_price_id=None, active=True, ), "mumbai_blockchain": SubscriptionTypeResourceData( id="mumbai_blockchain", name="Mumbai transactions", blockchain="mumbai", choices=["input:address", "tag:erc721"], description="Transactions that have been mined into the Mumbai blockchain", icon_url="https://s3.amazonaws.com/static.simiotics.com/moonstream/assets/matic-token-inverted-icon.png", stripe_product_id=None, stripe_price_id=None, active=True, ), "xdai_blockchain": SubscriptionTypeResourceData( id="xdai_blockchain", name="XDai transactions", blockchain="xdai", choices=["input:address", "tag:erc721"], description="Gnosis chain transactions subscription.", icon_url="https://s3.amazonaws.com/static.simiotics.com/moonstream/assets/xdai-token-logo.png", stripe_product_id=None, stripe_price_id=None, active=True, ), "wyrm_blockchain": SubscriptionTypeResourceData( id="wyrm_blockchain", name="Wyrm transactions", blockchain="wyrm", choices=["input:address", "tag:erc721"], description="Wyrm chain transactions subscription.", icon_url="https://s3.amazonaws.com/static.simiotics.com/moonstream/assets/great-wyrm-network-logo.png", stripe_product_id=None, stripe_price_id=None, active=True, ), "ethereum_whalewatch": SubscriptionTypeResourceData( id="ethereum_whalewatch", name="Ethereum whale watch", blockchain="ethereum", description="Ethereum accounts that have experienced a lot of recent activity", choices=[], # Icon taken from: https://www.maxpixel.net/Whale-Cetacean-Wildlife-Symbol-Ocean-Sea-Black-99310 icon_url="https://s3.amazonaws.com/static.simiotics.com/moonstream/assets/whalewatch.png", stripe_product_id=None, stripe_price_id=None, active=True, ), "ethereum_txpool": SubscriptionTypeResourceData( id="ethereum_txpool", name="Ethereum transaction pool", blockchain="ethereum", description="Transactions that have been submitted into the Ethereum transaction pool but not necessarily mined yet", choices=["input:address", "tag:erc721"], icon_url="https://s3.amazonaws.com/static.simiotics.com/moonstream/assets/ethereum/eth-diamond-rainbow.png", stripe_product_id=None, stripe_price_id=None, active=True, ), } class ConflictingSubscriptionTypesError(Exception): """ Raised when caller tries to add a resource that conflicts with an existing resource. """ pass class SubscriptionTypeNotFoundError(Exception): """ Raised when a subscription type is expected to exist as a Brood resource but is not found. """ class UnexpectedError(Exception): pass BUGOUT_RESOURCE_TYPE = "subscription_type" def create_subscription_type( id: str, name: str, description: str, icon_url: str, choices: List[str] = [], stripe_product_id: Optional[str] = None, stripe_price_id: Optional[str] = None, active: bool = False, ) -> BugoutResource: """ Add a new Moonstream subscription type as a Brood resource. Args: - id: Moonstream ID for the subscription type. Examples: "ethereum_blockchain", "ethereum_txpool", "ethereum_whalewatch", etc. - name: Human-friendly name for the subscription type, which can be displayed to users. - description: Detailed description of the subscription type for users who would like more information. - icon_url: URL to the icon for this subscription type - stripe_product_id: Optional product ID from Stripe account dashboard. - stripe_price_id: Optional price ID from Stripe account dashboard. - active: Set to True if you would like the subscription type to immediately be available for subscriptions. If you set this to False (which is the default), users will not be able to create subscriptions of this type until you later on set to true. """ params = {"type": BUGOUT_RESOURCE_TYPE, "id": id} response: BugoutResources = bc.list_resources( token=MOONSTREAM_ADMIN_ACCESS_TOKEN, params=params, timeout=BUGOUT_REQUEST_TIMEOUT_SECONDS, ) if response.resources: raise ConflictingSubscriptionTypesError( f"There is already a subscription_type with id: {id}" ) subscription_data = { "type": BUGOUT_RESOURCE_TYPE, "id": id, "name": name, "description": description, "choices": choices, "icon_url": icon_url, "stripe_product_id": stripe_product_id, "stripe_price_id": stripe_price_id, "active": active, } resource = bc.create_resource( token=MOONSTREAM_ADMIN_ACCESS_TOKEN, application_id=MOONSTREAM_APPLICATION_ID, resource_data=subscription_data, ) return resource def cli_create_subscription_type(args: argparse.Namespace) -> None: """ Handler for "mnstr subtypes create". """ result = create_subscription_type( args.id, args.name, args.description, args.icon, args.choices, args.stripe_product_id, args.stripe_price_id, args.active, ) print(result.json()) def list_subscription_types(active_only: bool = False) -> BugoutResources: """ Lists all subscription types registered as Brood resources for this Moonstream application. Args: - active_only: Set this to true if you only want to list active subscription types. By default, all subscription types are listed, be they active or inactive. """ response = bc.list_resources( token=MOONSTREAM_ADMIN_ACCESS_TOKEN, params={"type": BUGOUT_RESOURCE_TYPE}, timeout=BUGOUT_REQUEST_TIMEOUT_SECONDS, ) # TODO(kompotkot): Currently, we cannot filter using non-string fields in Brood resources. This means # that we have to implement the active_only filter in this API instead of just setting a query parameter # in the Brood API call. This should be fixed. if not active_only: return response active_resources = [ resource for resource in response.resources if resource.resource_data["active"] ] return BugoutResources(resources=active_resources) def cli_list_subscription_types(args: argparse.Namespace) -> None: """ Handler for "mnstr subtypes list". """ results = list_subscription_types(args.active) print(results.json()) def get_subscription_type(id: str) -> Optional[BugoutResource]: """ Retrieves the resource representing the subscription type with the given ID. Args: - id: Moonstream ID for the subscription type (not the Brood resource ID). Examples - "ethereum_blockchain", "ethereum_whalewatch", etc. Returns: None if there is no subscription type with that ID. Otherwise, returns the full Brood resource. To access the subscription type itself, use the "resource_data" member of the return value. If more than one subscription type is found with the given ID, raises a ConflictingSubscriptionTypesError. """ response = bc.list_resources( token=MOONSTREAM_ADMIN_ACCESS_TOKEN, params={"type": BUGOUT_RESOURCE_TYPE, "id": id}, timeout=BUGOUT_REQUEST_TIMEOUT_SECONDS, ) resources = response.resources if not resources: return None if len(resources) > 1: raise ConflictingSubscriptionTypesError( f"More than one resource with the given ID:\n{json.dumps(resources, indent=2)}" ) return resources[0] def cli_get_subscription_type(args: argparse.Namespace) -> None: """ Handler for "mnstr subtypes get". """ resource = get_subscription_type(args.id) if resource is None: print(f"Could not find resource with ID: {id}") else: print(resource.json()) def update_subscription_type( id: str, name: Optional[str] = None, description: Optional[str] = None, choices: Optional[List[str]] = None, icon_url: Optional[str] = None, stripe_product_id: Optional[str] = None, stripe_price_id: Optional[str] = None, active: Optional[bool] = None, ) -> BugoutResource: """ Update a Moonstream subscription type using the Brood Resources API. Args: - id: Moonstream ID for the subscription type. Examples: "ethereum_blockchain", "ethereum_txpool", "ethereum_whalewatch", etc. - name: Human-friendly name for the subscription type, which can be displayed to users. - description: Detailed description of the subscription type for users who would like more information. - icon_url: URL to the icon for this subscription type - stripe_product_id: Optional product ID from Stripe account dashboard. - stripe_price_id: Optional price ID from Stripe account dashboard. - active: Set to True if you would like the subscription type to immediately be available for subscriptions. If you set this to False (which is the default), users will not be able to create subscriptions of this type until you later on set to true. """ resource = get_subscription_type(id) if resource is None: raise SubscriptionTypeNotFoundError( f"Could not find subscription type with ID: {id}." ) brood_resource_id = resource.id updated_resource_data = resource.resource_data if name is not None: updated_resource_data["name"] = name if description is not None: updated_resource_data["description"] = description if choices is not None: updated_resource_data["choices"] = choices if icon_url is not None: updated_resource_data["icon_url"] = icon_url if stripe_product_id is not None: updated_resource_data["stripe_product_id"] = stripe_product_id if stripe_price_id is not None: updated_resource_data["stripe_price_id"] = stripe_price_id if active is not None: updated_resource_data["active"] = active # TODO(zomglings): This was written with an outdated bugout-python client. # New client has an update_resource method which is what we should be using # here. try: new_resource = bc.update_resource( token=MOONSTREAM_ADMIN_ACCESS_TOKEN, resource_id=brood_resource_id, resource_data={"update": updated_resource_data}, timeout=BUGOUT_REQUEST_TIMEOUT_SECONDS, ) except Exception as e: raise ConflictingSubscriptionTypesError( f"Unable to delete old subscription type with ID: {id}. Error:\n{repr(e)}" ) return new_resource def cli_update_subscription_type(args: argparse.Namespace) -> None: """ Handler for "mnstr subtypes update". """ result = update_subscription_type( args.id, args.name, args.description, args.choices, args.icon, args.stripe_product_id, args.stripe_price_id, args.active, ) print(result.json()) def delete_subscription_type(id: str) -> Optional[BugoutResource]: """ Deletes the subscription type resource with the given ID. Args: - id: Moonstream ID of the subscription type you would like to delete. Examples - "ethereum_blockchain", "ethereum_whalewatch", etc. Returns: The BugoutResource that was deleted. If no such resource existed in the first place, returns None. If multiple resources existed with the given Moonstream ID, raises a ConflictingSubscriptionTypesError and does not delete anything! """ # ConflictingSubscriptionTypesError raised here if there are multiple resources with the given id. resource = get_subscription_type(id) if resource is None: return None resource = bc.delete_resource( token=MOONSTREAM_ADMIN_ACCESS_TOKEN, resource_id=resource.id, timeout=BUGOUT_REQUEST_TIMEOUT_SECONDS, ) return resource def cli_delete_subscription_type(args: argparse.Namespace) -> None: """ Handler for "mnstr subtypes delete". """ result = delete_subscription_type(args.id) if result is None: print(f"Could not find resource with ID: {id}") else: print(result.json()) def ensure_canonical_subscription_types() -> BugoutResources: """ Ensures that the connected Brood API has at least the canonical subscription types. If any of the canonical subscription types does not exist as a Brood resource, this API creates the corresponding resource. If any of the canonical subscription types exists as a Brood resource but has been modified, this method does not change it on the server. Args: None Returns: A list of the resources representing the canonical subscription types as they exist on the connected Brood API. """ existing_canonical_subscription_types: Dict[str, BugoutResource] = {} for id, canonical_subscription_type in CANONICAL_SUBSCRIPTION_TYPES.items(): resource = get_subscription_type(canonical_subscription_type.id) if resource is not None: existing_canonical_subscription_types[id] = resource for id in CANONICAL_SUBSCRIPTION_TYPES.keys(): if existing_canonical_subscription_types.get(id) is None: canonical_subscription_type = CANONICAL_SUBSCRIPTION_TYPES[id] resource = create_subscription_type( id, canonical_subscription_type.name, canonical_subscription_type.description, canonical_subscription_type.icon_url, canonical_subscription_type.choices, canonical_subscription_type.stripe_product_id, canonical_subscription_type.stripe_price_id, canonical_subscription_type.active, ) existing_canonical_subscription_types[id] = resource else: canonical_subscription_type = CANONICAL_SUBSCRIPTION_TYPES[id] resource = update_subscription_type( id, canonical_subscription_type.name, canonical_subscription_type.description, canonical_subscription_type.choices, canonical_subscription_type.icon_url, canonical_subscription_type.stripe_product_id, canonical_subscription_type.stripe_price_id, canonical_subscription_type.active, ) existing_canonical_subscription_types[id] = resource return BugoutResources( resources=list(existing_canonical_subscription_types.values()) ) def cli_ensure_canonical_subscription_types(args: argparse.Namespace) -> None: """ Handler for "mnstr subtypes ensure-canonical """ resources = ensure_canonical_subscription_types() print(resources.json())