From 7c61e0486fbafd266c35e1ad9dce1b9a7f1c9361 Mon Sep 17 00:00:00 2001 From: Andrey Dolgolev <andrey@simiotics.com> Date: Tue, 7 Sep 2021 15:35:05 +0300 Subject: [PATCH 01/14] Add changes for subscription and merge main. --- backend/moonstream/__init__.py | 7 + backend/moonstream/actions.py | 9 +- backend/moonstream/admin/cli.py | 14 ++ .../moonstream/admin/subscription_types.py | 10 + backend/moonstream/data.py | 2 +- backend/moonstream/middleware.py | 25 ++- backend/moonstream/providers/__init__.py | 15 +- backend/moonstream/providers/bugout.py | 5 +- .../providers/ethereum_blockchain.py | 3 +- backend/moonstream/reporter.py | 18 ++ backend/moonstream/routes/address_info.py | 12 +- backend/moonstream/routes/streams.py | 125 ++++++++----- backend/moonstream/routes/subscriptions.py | 41 +++-- backend/moonstream/routes/txinfo.py | 5 +- backend/moonstream/routes/users.py | 37 ++-- backend/moonstream/settings.py | 2 + backend/requirements.txt | 2 + backend/sample.env | 3 +- backend/setup.py | 11 +- crawlers/ethtxpool/main.go | 37 ++-- crawlers/ethtxpool/sample.env | 1 + crawlers/mooncrawl/mooncrawl/__init__.py | 7 + crawlers/mooncrawl/mooncrawl/ethcrawler.py | 2 +- crawlers/mooncrawl/mooncrawl/reporter.py | 18 ++ crawlers/mooncrawl/mooncrawl/settings.py | 6 +- crawlers/mooncrawl/sample.env | 1 + crawlers/mooncrawl/setup.py | 4 +- ..._add_opensea_state_table_and_add_index_.py | 49 +++++ db/moonstreamdb/models.py | 21 ++- frontend/pages/index.js | 64 ++++--- frontend/src/AppContext.js | 14 +- frontend/src/components/Footer.js | 2 +- frontend/src/components/NewSubscription.js | 93 ++++++++-- frontend/src/components/Scrollable.js | 6 +- frontend/src/core/hooks/index.js | 1 - frontend/src/core/hooks/useAnalytics.js | 38 ---- frontend/src/core/hooks/useLogin.js | 16 -- frontend/src/core/hooks/useLogout.js | 9 +- frontend/src/core/hooks/useSignUp.js | 16 +- frontend/src/core/hooks/useToast.js | 19 +- .../providers/AnalyticsProvider/constants.js | 15 +- .../core/providers/AnalyticsProvider/index.js | 174 +++++++++++++++--- .../src/core/providers/UIProvider/index.js | 17 +- 43 files changed, 684 insertions(+), 292 deletions(-) create mode 100644 backend/moonstream/reporter.py create mode 100644 crawlers/mooncrawl/mooncrawl/reporter.py create mode 100644 db/alembic/versions/ecb7817db377_add_opensea_state_table_and_add_index_.py delete mode 100644 frontend/src/core/hooks/useAnalytics.js diff --git a/backend/moonstream/__init__.py b/backend/moonstream/__init__.py index e69de29b..5df6f535 100644 --- a/backend/moonstream/__init__.py +++ b/backend/moonstream/__init__.py @@ -0,0 +1,7 @@ +from .reporter import reporter +from .version import MOONSTREAM_VERSION + +# Reporting +reporter.tags.append(f"version:{MOONSTREAM_VERSION}") +reporter.system_report(publish=True) +reporter.setup_excepthook(publish=True) diff --git a/backend/moonstream/actions.py b/backend/moonstream/actions.py index 22612cd8..4ecf8bd4 100644 --- a/backend/moonstream/actions.py +++ b/backend/moonstream/actions.py @@ -1,16 +1,18 @@ import json import logging -from typing import Dict, Any, List, Optional +from typing import Optional from enum import Enum + import boto3 # type: ignore from moonstreamdb.models import ( EthereumAddress, EthereumLabel, ) from sqlalchemy import text -from sqlalchemy.orm import Session, query, query_expression +from sqlalchemy.orm import Session from . import data +from .reporter import reporter from .settings import ETHERSCAN_SMARTCONTRACTS_BUCKET logger = logging.getLogger(__name__) @@ -46,8 +48,9 @@ def get_contract_source_info( abi=obj_data["ABI"], ) return contract_source_info - except: + except Exception as e: logger.error(f"Failed to load smart contract {object_uri}") + reporter.error_report(e) return None diff --git a/backend/moonstream/admin/cli.py b/backend/moonstream/admin/cli.py index af24efe8..e95ae61a 100644 --- a/backend/moonstream/admin/cli.py +++ b/backend/moonstream/admin/cli.py @@ -71,6 +71,13 @@ This CLI is configured to work with the following API URLs: type=str, help="Detailed description of the subscription type", ) + parser_subscription_types_create.add_argument( + "-c", + "--choices", + nargs="*", + help="Available subscription options for from builder.", + required=True, + ) parser_subscription_types_create.add_argument( "--icon", required=True, @@ -146,6 +153,13 @@ This CLI is configured to work with the following API URLs: type=str, help="Detailed description of the subscription type", ) + parser_subscription_types_update.add_argument( + "-c", + "--choices", + nargs="*", + help="Available subscription options for form builder.", + required=False, + ) parser_subscription_types_update.add_argument( "--icon", required=False, diff --git a/backend/moonstream/admin/subscription_types.py b/backend/moonstream/admin/subscription_types.py index a44efb1a..0c668910 100644 --- a/backend/moonstream/admin/subscription_types.py +++ b/backend/moonstream/admin/subscription_types.py @@ -20,6 +20,7 @@ CANONICAL_SUBSCRIPTION_TYPES = { "ethereum_blockchain": SubscriptionTypeResourceData( id="ethereum_blockchain", name="Ethereum transactions", + choices=["input:Address", "tag:nfts"], 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, @@ -30,6 +31,7 @@ CANONICAL_SUBSCRIPTION_TYPES = { id="ethereum_whalewatch", name="Ethereum whale watch", 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, @@ -40,6 +42,7 @@ CANONICAL_SUBSCRIPTION_TYPES = { id="ethereum_txpool", name="Ethereum transaction pool", description="Transactions that have been submitted into the Ethereum transaction pool but not necessarily mined yet", + choices=["input:Address", "tag:nfts"], icon_url="https://s3.amazonaws.com/static.simiotics.com/moonstream/assets/ethereum/eth-diamond-rainbow.png", stripe_product_id=None, stripe_price_id=None, @@ -73,6 +76,7 @@ def create_subscription_type( id: str, name: str, description: str, + choices: List[str], icon_url: str, stripe_product_id: Optional[str] = None, stripe_price_id: Optional[str] = None, @@ -134,6 +138,7 @@ def cli_create_subscription_type(args: argparse.Namespace) -> None: args.id, args.name, args.description, + args.choices, args.icon, args.stripe_product_id, args.stripe_price_id, @@ -220,6 +225,7 @@ 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, @@ -254,6 +260,8 @@ def update_subscription_type( 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: @@ -295,6 +303,7 @@ def cli_update_subscription_type(args: argparse.Namespace) -> None: args.id, args.name, args.description, + args.choices, args.icon, args.stripe_product_id, args.stripe_price_id, @@ -365,6 +374,7 @@ def ensure_canonical_subscription_types() -> BugoutResources: 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, diff --git a/backend/moonstream/data.py b/backend/moonstream/data.py index e8e36c5e..7b7979a7 100644 --- a/backend/moonstream/data.py +++ b/backend/moonstream/data.py @@ -10,6 +10,7 @@ class SubscriptionTypeResourceData(BaseModel): id: str name: str description: str + choices: Optional[List[str]] icon_url: str stripe_product_id: Optional[str] = None stripe_price_id: Optional[str] = None @@ -22,7 +23,6 @@ class SubscriptionTypesListResponse(BaseModel): class SubscriptionResourceData(BaseModel): id: str - address: str color: Optional[str] label: Optional[str] user_id: str diff --git a/backend/moonstream/middleware.py b/backend/moonstream/middleware.py index e2a15175..5e089aa6 100644 --- a/backend/moonstream/middleware.py +++ b/backend/moonstream/middleware.py @@ -1,16 +1,36 @@ import logging -from typing import Awaitable, Callable, Dict, Optional +from typing import Any, Awaitable, Callable, Dict, Optional from bugout.data import BugoutUser from bugout.exceptions import BugoutResponseException +from fastapi import HTTPException, Request, Response +from starlette.background import BackgroundTask from starlette.middleware.base import BaseHTTPMiddleware -from fastapi import Request, Response +from .reporter import reporter from .settings import MOONSTREAM_APPLICATION_ID, bugout_client as bc logger = logging.getLogger(__name__) +class MoonstreamHTTPException(HTTPException): + """ + Extended HTTPException to handle 500 Internal server errors + and send crash reports. + """ + + def __init__( + self, + status_code: int, + detail: Any = None, + headers: Optional[Dict[str, Any]] = None, + internal_error: Exception = None, + ): + super().__init__(status_code, detail, headers) + if internal_error is not None: + reporter.error_report(internal_error) + + class BroodAuthMiddleware(BaseHTTPMiddleware): """ Checks the authorization header on the request. If it represents a verified Brood user, @@ -61,6 +81,7 @@ class BroodAuthMiddleware(BaseHTTPMiddleware): return Response(status_code=e.status_code, content=e.detail) except Exception as e: logger.error(f"Error processing Brood response: {str(e)}") + reporter.error_report(e) return Response(status_code=500, content="Internal server error") request.state.user = user diff --git a/backend/moonstream/providers/__init__.py b/backend/moonstream/providers/__init__.py index fbb2ec7d..f884fe1b 100644 --- a/backend/moonstream/providers/__init__.py +++ b/backend/moonstream/providers/__init__.py @@ -39,6 +39,13 @@ from ..stream_queries import StreamQuery logger = logging.getLogger(__name__) logger.setLevel(logging.WARN) + +class ReceivingEventsException(Exception): + """ + Raised when error occurs during receiving events from provider. + """ + + event_providers: Dict[str, Any] = { ethereum_blockchain.event_type: ethereum_blockchain, bugout.whalewatch_provider.event_type: bugout.whalewatch_provider, @@ -91,7 +98,7 @@ def get_events( f"Error receiving events from provider: {provider_name}:\n{repr(e)}" ) else: - raise e + raise ReceivingEventsException(e) events = [event for _, event_list in results.values() for event in event_list] if sort_events: @@ -149,7 +156,7 @@ def latest_events( f"Error receiving events from provider: {provider_name}:\n{repr(e)}" ) else: - raise e + raise ReceivingEventsException(e) events = [event for event_list in results.values() for event in event_list] if sort_events: @@ -202,7 +209,7 @@ def next_event( f"Error receiving events from provider: {provider_name}:\n{repr(e)}" ) else: - raise e + raise ReceivingEventsException(e) event: Optional[data.Event] = None for candidate in results.values(): @@ -258,7 +265,7 @@ def previous_event( f"Error receiving events from provider: {provider_name}:\n{repr(e)}" ) else: - raise e + raise ReceivingEventsException(e) event: Optional[data.Event] = None for candidate in results.values(): diff --git a/backend/moonstream/providers/bugout.py b/backend/moonstream/providers/bugout.py index 4ba9629e..5d7ef530 100644 --- a/backend/moonstream/providers/bugout.py +++ b/backend/moonstream/providers/bugout.py @@ -19,6 +19,7 @@ from ..stream_queries import StreamQuery logger = logging.getLogger(__name__) logger.setLevel(logging.WARN) +allowed_tags = ["nfts"] class BugoutEventProviderError(Exception): @@ -296,9 +297,9 @@ class EthereumTXPoolProvider(BugoutEventProvider): for subscription in relevant_subscriptions ] subscriptions_filters = [] - for adress in addresses: + for address in addresses: subscriptions_filters.extend( - [f"?#from_address:{adress}", f"?#to_address:{adress}"] + [f"?#from_address:{address}", f"?#to_address:{address}"] ) return subscriptions_filters diff --git a/backend/moonstream/providers/ethereum_blockchain.py b/backend/moonstream/providers/ethereum_blockchain.py index b46e56f4..5a478a84 100644 --- a/backend/moonstream/providers/ethereum_blockchain.py +++ b/backend/moonstream/providers/ethereum_blockchain.py @@ -174,7 +174,8 @@ def query_ethereum_transactions( EthereumTransaction.value, EthereumBlock.timestamp.label("timestamp"), ).join( - EthereumBlock, EthereumTransaction.block_number == EthereumBlock.block_number + EthereumBlock, + EthereumTransaction.block_number == EthereumBlock.block_number, ) if stream_boundary.include_start: diff --git a/backend/moonstream/reporter.py b/backend/moonstream/reporter.py new file mode 100644 index 00000000..1ba0997b --- /dev/null +++ b/backend/moonstream/reporter.py @@ -0,0 +1,18 @@ +import uuid + +from humbug.consent import HumbugConsent +from humbug.report import HumbugReporter + +from .settings import HUMBUG_REPORTER_BACKEND_TOKEN + +session_id = str(uuid.uuid4()) +client_id = "moonstream-backend" + +reporter = HumbugReporter( + name="moonstream", + consent=HumbugConsent(True), + client_id=client_id, + session_id=session_id, + bugout_token=HUMBUG_REPORTER_BACKEND_TOKEN, + tags=[], +) diff --git a/backend/moonstream/routes/address_info.py b/backend/moonstream/routes/address_info.py index 2b95bb92..c985c683 100644 --- a/backend/moonstream/routes/address_info.py +++ b/backend/moonstream/routes/address_info.py @@ -3,14 +3,14 @@ from typing import Dict, List, Optional from sqlalchemy.sql.expression import true -from fastapi import FastAPI, Depends, Query, HTTPException +from fastapi import FastAPI, Depends, Query from fastapi.middleware.cors import CORSMiddleware from moonstreamdb.db import yield_db_session from sqlalchemy.orm import Session from .. import actions from .. import data -from ..middleware import BroodAuthMiddleware +from ..middleware import BroodAuthMiddleware, MoonstreamHTTPException from ..settings import DOCS_TARGET_PATH, ORIGINS, DOCS_PATHS from ..version import MOONSTREAM_VERSION @@ -73,15 +73,15 @@ async def addresses_labels_bulk_handler( about known addresses. """ if limit > 100: - raise HTTPException( + raise MoonstreamHTTPException( status_code=406, detail="The limit cannot exceed 100 addresses" ) try: addresses_response = actions.get_address_labels( db_session=db_session, start=start, limit=limit, addresses=addresses ) - except Exception as err: - logger.error(f"Unable to get info about Ethereum addresses {err}") - raise HTTPException(status_code=500) + except Exception as e: + logger.error(f"Unable to get info about Ethereum addresses {e}") + raise MoonstreamHTTPException(status_code=500, internal_error=e) return addresses_response diff --git a/backend/moonstream/routes/streams.py b/backend/moonstream/routes/streams.py index be46bc58..10d0426c 100644 --- a/backend/moonstream/routes/streams.py +++ b/backend/moonstream/routes/streams.py @@ -5,15 +5,16 @@ import logging from typing import Dict, List, Optional from bugout.data import BugoutResource -from fastapi import FastAPI, HTTPException, Request, Query, Depends +from fastapi import FastAPI, Request, Query, Depends from fastapi.middleware.cors import CORSMiddleware from moonstreamdb import db from sqlalchemy.orm import Session from .. import data -from ..middleware import BroodAuthMiddleware +from ..middleware import BroodAuthMiddleware, MoonstreamHTTPException from ..providers import ( + ReceivingEventsException, event_providers, get_events, latest_events, @@ -121,17 +122,25 @@ async def stream_handler( if q.strip() != "": query = stream_queries.parse_query_string(q) - _, events = get_events( - db_session, - bc, - MOONSTREAM_DATA_JOURNAL_ID, - MOONSTREAM_ADMIN_ACCESS_TOKEN, - stream_boundary, - query, - user_subscriptions, - result_timeout=10.0, - raise_on_error=True, - ) + try: + _, events = get_events( + db_session, + bc, + MOONSTREAM_DATA_JOURNAL_ID, + MOONSTREAM_ADMIN_ACCESS_TOKEN, + stream_boundary, + query, + user_subscriptions, + result_timeout=10.0, + raise_on_error=True, + ) + except ReceivingEventsException as e: + logger.error("Error receiving events from provider") + raise MoonstreamHTTPException(status_code=500, internal_error=e) + except Exception as e: + logger.error("Unable to get events") + raise MoonstreamHTTPException(status_code=500, internal_error=e) + response = data.GetEventsResponse(stream_boundary=stream_boundary, events=events) return response @@ -155,18 +164,26 @@ async def latest_events_handler( if q.strip() != "": query = stream_queries.parse_query_string(q) - events = latest_events( - db_session, - bc, - MOONSTREAM_DATA_JOURNAL_ID, - MOONSTREAM_ADMIN_ACCESS_TOKEN, - query, - 1, - user_subscriptions, - result_timeout=6.0, - raise_on_error=True, - sort_events=True, - ) + try: + events = latest_events( + db_session, + bc, + MOONSTREAM_DATA_JOURNAL_ID, + MOONSTREAM_ADMIN_ACCESS_TOKEN, + query, + 1, + user_subscriptions, + result_timeout=6.0, + raise_on_error=True, + sort_events=True, + ) + except ReceivingEventsException as e: + logger.error("Error receiving events from provider") + raise MoonstreamHTTPException(status_code=500, internal_error=e) + except Exception as e: + logger.error("Unable to get latest events") + raise MoonstreamHTTPException(status_code=500, internal_error=e) + return events @@ -203,17 +220,24 @@ async def next_event_handler( if q.strip() != "": query = stream_queries.parse_query_string(q) - event = next_event( - db_session, - bc, - MOONSTREAM_DATA_JOURNAL_ID, - MOONSTREAM_ADMIN_ACCESS_TOKEN, - stream_boundary, - query, - user_subscriptions, - result_timeout=6.0, - raise_on_error=True, - ) + try: + event = next_event( + db_session, + bc, + MOONSTREAM_DATA_JOURNAL_ID, + MOONSTREAM_ADMIN_ACCESS_TOKEN, + stream_boundary, + query, + user_subscriptions, + result_timeout=6.0, + raise_on_error=True, + ) + except ReceivingEventsException as e: + logger.error("Error receiving events from provider") + raise MoonstreamHTTPException(status_code=500, internal_error=e) + except Exception as e: + logger.error("Unable to get next events") + raise MoonstreamHTTPException(status_code=500, internal_error=e) return event @@ -251,16 +275,23 @@ async def previous_event_handler( if q.strip() != "": query = stream_queries.parse_query_string(q) - event = previous_event( - db_session, - bc, - MOONSTREAM_DATA_JOURNAL_ID, - MOONSTREAM_ADMIN_ACCESS_TOKEN, - stream_boundary, - query, - user_subscriptions, - result_timeout=6.0, - raise_on_error=True, - ) + try: + event = previous_event( + db_session, + bc, + MOONSTREAM_DATA_JOURNAL_ID, + MOONSTREAM_ADMIN_ACCESS_TOKEN, + stream_boundary, + query, + user_subscriptions, + result_timeout=6.0, + raise_on_error=True, + ) + except ReceivingEventsException as e: + logger.error("Error receiving events from provider") + raise MoonstreamHTTPException(status_code=500, internal_error=e) + except Exception as e: + logger.error("Unable to get previous events") + raise MoonstreamHTTPException(status_code=500, internal_error=e) return event diff --git a/backend/moonstream/routes/subscriptions.py b/backend/moonstream/routes/subscriptions.py index 75c854dc..f86f4686 100644 --- a/backend/moonstream/routes/subscriptions.py +++ b/backend/moonstream/routes/subscriptions.py @@ -6,12 +6,13 @@ from typing import Dict, List, Optional from bugout.data import BugoutResource, BugoutResources from bugout.exceptions import BugoutResponseException -from fastapi import FastAPI, HTTPException, Request, Form +from fastapi import FastAPI, Request, Form from fastapi.middleware.cors import CORSMiddleware from ..admin import subscription_types from .. import data -from ..middleware import BroodAuthMiddleware +from ..middleware import BroodAuthMiddleware, MoonstreamHTTPException +from ..reporter import reporter from ..settings import ( DOCS_TARGET_PATH, DOCS_PATHS, @@ -77,7 +78,7 @@ async def add_subscription_handler( ] if subscription_type_id not in available_subscription_type_ids: - raise HTTPException( + raise MoonstreamHTTPException( status_code=404, detail=f"Invalid subscription type: {subscription_type_id}.", ) @@ -99,10 +100,11 @@ async def add_subscription_handler( application_id=MOONSTREAM_APPLICATION_ID, resource_data=resource_data, ) + except BugoutResponseException as e: + raise MoonstreamHTTPException(status_code=e.status_code, detail=e.detail) except Exception as e: - logger.error("Error creating subscription resource:") - logger.error(e) - raise HTTPException(status_code=500) + logger.error(f"Error creating subscription resource: {str(e)}") + raise MoonstreamHTTPException(status_code=500, internal_error=e) return data.SubscriptionResourceData( id=str(resource.id), @@ -123,14 +125,14 @@ async def delete_subscription_handler(request: Request, subscription_id: str): """ Delete subscriptions. """ - token = request.state.token try: deleted_resource = bc.delete_resource(token=token, resource_id=subscription_id) except BugoutResponseException as e: - raise HTTPException(status_code=e.status_code, detail=e.detail) + raise MoonstreamHTTPException(status_code=e.status_code, detail=e.detail) except Exception as e: - raise HTTPException(status_code=500) + logger.error(f"Error deleting subscription: {str(e)}") + raise MoonstreamHTTPException(status_code=500, internal_error=e) return data.SubscriptionResourceData( id=str(deleted_resource.id), @@ -154,12 +156,14 @@ async def get_subscriptions_handler(request: Request) -> data.SubscriptionsListR } 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 listing subscriptions for user ({request.user.id}) with token ({request.state.token})" + f"Error listing subscriptions for user ({request.user.id}) with token ({request.state.token}), error: {str(e)}" ) - logger.error(e) - raise HTTPException(status_code=500) + reporter.error_report(e) + raise MoonstreamHTTPException(status_code=500, internal_error=e) return data.SubscriptionsListResponse( subscriptions=[ @@ -190,7 +194,6 @@ async def update_subscriptions_handler( """ Get user's subscriptions. """ - token = request.state.token update = {} @@ -210,9 +213,10 @@ async def update_subscriptions_handler( ).dict(), ) except BugoutResponseException as e: - raise HTTPException(status_code=e.status_code, detail=e.detail) + raise MoonstreamHTTPException(status_code=e.status_code, detail=e.detail) except Exception as e: - raise HTTPException(status_code=500) + logger.error(f"Error getting user subscriptions: {str(e)}") + raise MoonstreamHTTPException(status_code=500, internal_error=e) return data.SubscriptionResourceData( id=str(resource.id), @@ -238,9 +242,10 @@ async def list_subscription_types() -> data.SubscriptionTypesListResponse: data.SubscriptionTypeResourceData.validate(resource.resource_data) for resource in response.resources ] + except BugoutResponseException as e: + raise MoonstreamHTTPException(status_code=e.status_code, detail=e.detail) except Exception as e: - logger.error("Error reading subscription types from Brood API:") - logger.error(e) - raise HTTPException(status_code=500) + logger.error(f"Error reading subscription types from Brood API: {str(e)}") + raise MoonstreamHTTPException(status_code=500, internal_error=e) return data.SubscriptionTypesListResponse(subscription_types=results) diff --git a/backend/moonstream/routes/txinfo.py b/backend/moonstream/routes/txinfo.py index 20eec3c7..8ab9a5ec 100644 --- a/backend/moonstream/routes/txinfo.py +++ b/backend/moonstream/routes/txinfo.py @@ -6,9 +6,7 @@ transactions, etc.) with side information and return objects that are better sui end users. """ import logging -from typing import Dict, Optional - -from sqlalchemy.sql.expression import true +from typing import Dict from fastapi import FastAPI, Depends from fastapi.middleware.cors import CORSMiddleware @@ -54,6 +52,7 @@ app.add_middleware(BroodAuthMiddleware, whitelist=whitelist_paths) # TODO(zomglings): Factor out the enrichment logic into a separate action, because it may be useful # independently from serving API calls (e.g. data processing). +# TODO(kompotkot): Re-organize function to be able handle each steps with exceptions. @app.post( "/ethereum_blockchain", tags=["txinfo"], diff --git a/backend/moonstream/routes/users.py b/backend/moonstream/routes/users.py index 5101a25c..4407a67e 100644 --- a/backend/moonstream/routes/users.py +++ b/backend/moonstream/routes/users.py @@ -7,15 +7,10 @@ import uuid from bugout.data import BugoutToken, BugoutUser from bugout.exceptions import BugoutResponseException -from fastapi import ( - FastAPI, - Form, - HTTPException, - Request, -) +from fastapi import FastAPI, Form, Request from fastapi.middleware.cors import CORSMiddleware -from ..middleware import BroodAuthMiddleware +from ..middleware import BroodAuthMiddleware, MoonstreamHTTPException from ..settings import ( MOONSTREAM_APPLICATION_ID, DOCS_TARGET_PATH, @@ -75,9 +70,9 @@ async def create_user_handler( application_id=MOONSTREAM_APPLICATION_ID, ) except BugoutResponseException as e: - raise HTTPException(status_code=e.status_code, detail=e.detail) + raise MoonstreamHTTPException(status_code=e.status_code, detail=e.detail) except Exception as e: - raise HTTPException(status_code=500) + raise MoonstreamHTTPException(status_code=500, internal_error=e) return user @@ -92,9 +87,9 @@ async def restore_password_handler(email: str = Form(...)) -> Dict[str, Any]: try: response = bc.restore_password(email=email) except BugoutResponseException as e: - raise HTTPException(status_code=e.status_code, detail=e.detail) + raise MoonstreamHTTPException(status_code=e.status_code, detail=e.detail) except Exception as e: - raise HTTPException(status_code=500) + raise MoonstreamHTTPException(status_code=500, internal_error=e) return response @@ -105,9 +100,9 @@ async def reset_password_handler( try: response = bc.reset_password(reset_id=reset_id, new_password=new_password) except BugoutResponseException as e: - raise HTTPException(status_code=e.status_code, detail=e.detail) + raise MoonstreamHTTPException(status_code=e.status_code, detail=e.detail) except Exception as e: - raise HTTPException(status_code=500) + raise MoonstreamHTTPException(status_code=500, internal_error=e) return response @@ -121,9 +116,9 @@ async def change_password_handler( token=token, current_password=current_password, new_password=new_password ) except BugoutResponseException as e: - raise HTTPException(status_code=e.status_code, detail=e.detail) + raise MoonstreamHTTPException(status_code=e.status_code, detail=e.detail) except Exception as e: - raise HTTPException(status_code=500) + raise MoonstreamHTTPException(status_code=500, internal_error=e) return user @@ -136,9 +131,9 @@ async def delete_user_handler( try: user = bc.delete_user(token=token, user_id=user.id, password=password) except BugoutResponseException as e: - raise HTTPException(status_code=e.status_code, detail=e.detail) + raise MoonstreamHTTPException(status_code=e.status_code, detail=e.detail) except Exception as e: - raise HTTPException(status_code=500) + raise MoonstreamHTTPException(status_code=500, internal_error=e) return user @@ -153,11 +148,11 @@ async def login_handler( application_id=MOONSTREAM_APPLICATION_ID, ) except BugoutResponseException as e: - raise HTTPException( + raise MoonstreamHTTPException( status_code=e.status_code, detail=f"Error from Brood API: {e.detail}" ) except Exception as e: - raise HTTPException(status_code=500) + raise MoonstreamHTTPException(status_code=500, internal_error=e) return token @@ -167,7 +162,7 @@ async def logout_handler(request: Request) -> uuid.UUID: try: token_id: uuid.UUID = bc.revoke_token(token=token) except BugoutResponseException as e: - raise HTTPException(status_code=e.status_code, detail=e.detail) + raise MoonstreamHTTPException(status_code=e.status_code, detail=e.detail) except Exception as e: - raise HTTPException(status_code=500) + raise MoonstreamHTTPException(status_code=500, internal_error=e) return token_id diff --git a/backend/moonstream/settings.py b/backend/moonstream/settings.py index 6408b1d8..0a90d751 100644 --- a/backend/moonstream/settings.py +++ b/backend/moonstream/settings.py @@ -9,6 +9,8 @@ bugout_client = Bugout(brood_api_url=BUGOUT_BROOD_URL, spire_api_url=BUGOUT_SPIR BUGOUT_REQUEST_TIMEOUT_SECONDS = 5 +HUMBUG_REPORTER_BACKEND_TOKEN = os.environ.get("HUMBUG_REPORTER_BACKEND_TOKEN") + # Default value is "" instead of None so that mypy understands that MOONSTREAM_APPLICATION_ID is a string MOONSTREAM_APPLICATION_ID = os.environ.get("MOONSTREAM_APPLICATION_ID", "") if MOONSTREAM_APPLICATION_ID == "": diff --git a/backend/requirements.txt b/backend/requirements.txt index 44e49539..4cc7c386 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -11,6 +11,7 @@ fastapi==0.66.0 h11==0.12.0 idna==3.2 jmespath==0.10.0 +humbug==0.2.7 -e git+https://git@github.com/bugout-dev/moonstream.git@94135b054cabb9dc11b0a2406431619279979469#egg=moonstreamdb&subdirectory=db mypy==0.910 mypy-extensions==0.4.3 @@ -28,5 +29,6 @@ toml==0.10.2 tomli==1.0.4 types-python-dateutil==0.1.6 typing-extensions==3.10.0.0 +types-requests==2.25.6 urllib3==1.26.6 uvicorn==0.14.0 diff --git a/backend/sample.env b/backend/sample.env index 054e040d..779bbb0c 100644 --- a/backend/sample.env +++ b/backend/sample.env @@ -5,6 +5,7 @@ export MOONSTREAM_DATA_JOURNAL_ID="<bugout_journal_id_to_store_blockchain_data>" export MOONSTREAM_DB_URI="postgresql://<username>:<password>@<db_host>:<db_port>/<db_name>" export MOONSTREAM_POOL_SIZE=0 export MOONSTREAM_ADMIN_ACCESS_TOKEN="<Access token to application resources>" -export AWS_S3_SMARTCONTRACT_BUCKET="" +export AWS_S3_SMARTCONTRACT_BUCKET="<AWS S3 bucket to store smart contracts>" export BUGOUT_BROOD_URL="https://auth.bugout.dev" export BUGOUT_SPIRE_URL="https://spire.bugout.dev" +export HUMBUG_REPORTER_BACKEND_TOKEN="<Bugout Humbug token for crash reports>" diff --git a/backend/setup.py b/backend/setup.py index 0cd9b6e5..06a2a807 100644 --- a/backend/setup.py +++ b/backend/setup.py @@ -10,7 +10,16 @@ setup( name="moonstream", version=MOONSTREAM_VERSION, packages=find_packages(), - install_requires=["boto3", "bugout >= 0.1.17", "fastapi", "python-dateutil", "uvicorn", "types-python-dateutil"], + install_requires=[ + "boto3", + "bugout >= 0.1.17", + "fastapi", + "humbug>=0.2.7", + "python-dateutil", + "uvicorn", + "types-python-dateutil", + "types-requests", + ], extras_require={ "dev": ["black", "mypy"], "distribute": ["setuptools", "twine", "wheel"], diff --git a/crawlers/ethtxpool/main.go b/crawlers/ethtxpool/main.go index 224ae7dc..e4aa4f55 100644 --- a/crawlers/ethtxpool/main.go +++ b/crawlers/ethtxpool/main.go @@ -10,23 +10,20 @@ import ( "encoding/json" "flag" "fmt" + "math/big" "os" "time" humbug "github.com/bugout-dev/humbug/go/pkg" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" - + "github.com/ethereum/go-ethereum/params" "github.com/ethereum/go-ethereum/rpc" "github.com/google/uuid" ) -// Generate humbug client to be able write data in Bugout journal. -func humbugClientFromEnv() (*humbug.HumbugReporter, error) { - clientID := os.Getenv("ETHTXPOOL_HUMBUG_CLIENT_ID") - humbugToken := os.Getenv("ETHTXPOOL_HUMBUG_TOKEN") - sessionID := uuid.New().String() - +// Generate humbug client +func humbugClient(sessionID string, clientID string, humbugToken string) (*humbug.HumbugReporter, error) { consent := humbug.CreateHumbugConsent(humbug.True) reporter, err := humbug.CreateHumbugReporter(consent, clientID, sessionID, humbugToken) return reporter, err @@ -124,11 +121,6 @@ func PollTxpoolContent(gethClient *rpc.Client, interval int, reporter *humbug.Hu continue } - // TODO(kompotkot, zomglings): Humbug API (on Spire) support bulk publication of reports. We should modify - // Humbug go client to use the bulk publish endpoint. Currently, if we have to publish all transactions - // pending in txpool, we *will* get rate limited. We may want to consider adding a publisher to the - // Humbug go client that can listen on a channel and will handle rate limiting, bulk publication etc. itself - // (without user having to worry about it). ReportTitle := "Ethereum: Pending transaction: " + transactionHash.String() ReportTags := []string{ "hash:" + transactionHash.String(), @@ -138,6 +130,7 @@ func PollTxpoolContent(gethClient *rpc.Client, interval int, reporter *humbug.Hu fmt.Sprintf("max_priority_fee_per_gas:%d", pendingTx.Transaction.MaxPriorityFeePerGas.ToInt()), fmt.Sprintf("max_fee_per_gas:%d", pendingTx.Transaction.MaxFeePerGas.ToInt()), fmt.Sprintf("gas:%d", pendingTx.Transaction.Gas), + fmt.Sprintf("value:%d", new(big.Float).Quo(new(big.Float).SetInt(transaction.Value.ToInt()), big.NewFloat(params.Ether))), "crawl_type:ethereum_txpool", } report := humbug.Report{ @@ -188,6 +181,23 @@ func main() { flag.IntVar(&intervalSeconds, "interval", 1, "Number of seconds to wait between RPC calls to query the transaction pool (default: 1)") flag.Parse() + sessionID := uuid.New().String() + + // Humbug crash client to collect errors + crashReporter, err := humbugClient(sessionID, "moonstream-crawlers", os.Getenv("HUMBUG_REPORTER_CRAWLERS_TOKEN")) + if err != nil { + panic(fmt.Sprintf("Invalid Humbug Crash configuration: %s", err.Error())) + } + crashReporter.Publish(humbug.SystemReport()) + + defer func() { + message := recover() + if message != nil { + fmt.Printf("Error: %s\n", message) + crashReporter.Publish(humbug.PanicReport(message)) + } + }() + // Set connection with Ethereum blockchain via geth gethClient, err := rpc.Dial(gethConnectionString) if err != nil { @@ -195,7 +205,8 @@ func main() { } defer gethClient.Close() - reporter, err := humbugClientFromEnv() + // Humbug client to be able write data in Bugout journal + reporter, err := humbugClient(sessionID, os.Getenv("ETHTXPOOL_HUMBUG_CLIENT_ID"), os.Getenv("ETHTXPOOL_HUMBUG_TOKEN")) if err != nil { panic(fmt.Sprintf("Invalid Humbug configuration: %s", err.Error())) } diff --git a/crawlers/ethtxpool/sample.env b/crawlers/ethtxpool/sample.env index abd4bc8e..809cc325 100644 --- a/crawlers/ethtxpool/sample.env +++ b/crawlers/ethtxpool/sample.env @@ -1,2 +1,3 @@ export ETHTXPOOL_HUMBUG_CLIENT_ID="<client id for the crawling machine>" export ETHTXPOOL_HUMBUG_TOKEN="<Generate an integration and a Humbug token from https://bugout.dev/account/teams>" +export HUMBUG_REPORTER_CRAWLERS_TOKEN="<Bugout Humbug token for crash reports>" diff --git a/crawlers/mooncrawl/mooncrawl/__init__.py b/crawlers/mooncrawl/mooncrawl/__init__.py index e69de29b..9548e297 100644 --- a/crawlers/mooncrawl/mooncrawl/__init__.py +++ b/crawlers/mooncrawl/mooncrawl/__init__.py @@ -0,0 +1,7 @@ +from .reporter import reporter +from .version import MOONCRAWL_VERSION + +# Reporting +reporter.tags.append(f"version:{MOONCRAWL_VERSION}") +reporter.system_report(publish=True) +reporter.setup_excepthook(publish=True) diff --git a/crawlers/mooncrawl/mooncrawl/ethcrawler.py b/crawlers/mooncrawl/mooncrawl/ethcrawler.py index 793ab3ea..5e9275c4 100644 --- a/crawlers/mooncrawl/mooncrawl/ethcrawler.py +++ b/crawlers/mooncrawl/mooncrawl/ethcrawler.py @@ -48,7 +48,7 @@ def yield_blocks_numbers_lists( print( "Wrong format provided, expected {bottom_block}-{top_block}, as ex. 105-340" ) - return + raise Exception starting_block = max(input_start_block, input_end_block) ending_block = min(input_start_block, input_end_block) diff --git a/crawlers/mooncrawl/mooncrawl/reporter.py b/crawlers/mooncrawl/mooncrawl/reporter.py new file mode 100644 index 00000000..0cf170ac --- /dev/null +++ b/crawlers/mooncrawl/mooncrawl/reporter.py @@ -0,0 +1,18 @@ +import uuid + +from humbug.consent import HumbugConsent +from humbug.report import HumbugReporter + +from .settings import HUMBUG_REPORTER_CRAWLERS_TOKEN + +session_id = str(uuid.uuid4()) +client_id = "moonstream-crawlers" + +reporter = HumbugReporter( + name="moonstream-crawlers", + consent=HumbugConsent(True), + client_id=client_id, + session_id=session_id, + bugout_token=HUMBUG_REPORTER_CRAWLERS_TOKEN, + tags=[], +) diff --git a/crawlers/mooncrawl/mooncrawl/settings.py b/crawlers/mooncrawl/mooncrawl/settings.py index 82b13772..307f2e8a 100644 --- a/crawlers/mooncrawl/mooncrawl/settings.py +++ b/crawlers/mooncrawl/mooncrawl/settings.py @@ -1,5 +1,9 @@ import os +# Bugout +HUMBUG_REPORTER_CRAWLERS_TOKEN = os.environ.get("HUMBUG_REPORTER_CRAWLERS_TOKEN") + +# Geth MOONSTREAM_IPC_PATH = os.environ.get("MOONSTREAM_IPC_PATH", None) MOONSTREAM_CRAWL_WORKERS = 4 @@ -12,5 +16,5 @@ except: f"Could not parse MOONSTREAM_CRAWL_WORKERS as int: {MOONSTREAM_CRAWL_WORKERS_RAW}" ) - +# Etherscan MOONSTREAM_ETHERSCAN_TOKEN = os.environ.get("MOONSTREAM_ETHERSCAN_TOKEN") diff --git a/crawlers/mooncrawl/sample.env b/crawlers/mooncrawl/sample.env index 5ebad6e0..5d4ac548 100644 --- a/crawlers/mooncrawl/sample.env +++ b/crawlers/mooncrawl/sample.env @@ -6,3 +6,4 @@ export MOONSTREAM_ETHERSCAN_TOKEN="<Token for etherscan>" export AWS_S3_SMARTCONTRACT_BUCKET="<AWS S3 bucket for smart contracts>" export MOONSTREAM_HUMBUG_TOKEN="<Token for crawlers store data via Humbug>" export COINMARKETCAP_API_KEY="<API key to parse conmarketcap>" +export HUMBUG_REPORTER_CRAWLERS_TOKEN="<Bugout Humbug token for crash reports>" diff --git a/crawlers/mooncrawl/setup.py b/crawlers/mooncrawl/setup.py index 1e893bd4..f26b82d5 100644 --- a/crawlers/mooncrawl/setup.py +++ b/crawlers/mooncrawl/setup.py @@ -1,6 +1,5 @@ from setuptools import find_packages, setup -from mooncrawl.version import MOONCRAWL_VERSION long_description = "" with open("README.md") as ifp: @@ -8,7 +7,7 @@ with open("README.md") as ifp: setup( name="mooncrawl", - version=MOONCRAWL_VERSION, + version="0.0.3", author="Bugout.dev", author_email="engineers@bugout.dev", license="Apache License 2.0", @@ -34,6 +33,7 @@ setup( zip_safe=False, install_requires=[ "moonstreamdb @ git+https://git@github.com/bugout-dev/moonstream.git@39d2b8e36a49958a9ae085ec2cc1be3fc732b9d0#egg=moonstreamdb&subdirectory=db", + "humbug", "python-dateutil", "requests", "tqdm", diff --git a/db/alembic/versions/ecb7817db377_add_opensea_state_table_and_add_index_.py b/db/alembic/versions/ecb7817db377_add_opensea_state_table_and_add_index_.py new file mode 100644 index 00000000..98a07eb0 --- /dev/null +++ b/db/alembic/versions/ecb7817db377_add_opensea_state_table_and_add_index_.py @@ -0,0 +1,49 @@ +"""Add opensea state table and add index by label_data ->> name + +Revision ID: ecb7817db377 +Revises: ea8185bd24c7 +Create Date: 2021-08-31 17:44:24.139028 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "ecb7817db377" +down_revision = "ea8185bd24c7" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "opensea_crawler_state", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("query", sa.Text(), nullable=False), + sa.Column( + "crawled_at", + sa.DateTime(timezone=True), + server_default=sa.text("TIMEZONE('utc', statement_timestamp())"), + nullable=False, + ), + sa.Column("total_count", sa.Integer(), nullable=False), + sa.PrimaryKeyConstraint("id", name=op.f("pk_opensea_crawler_state")), + sa.UniqueConstraint("id", name=op.f("uq_opensea_crawler_state_id")), + ) + op.execute( + "ALTER TABLE ethereum_labels DROP CONSTRAINT IF EXISTS uq_ethereum_labels_label" + ) + + op.execute( + f"CREATE INDEX idx_ethereum_labels_opensea_nft_name ON ethereum_labels((label_data->>'name')) where label='opensea_nft';" + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("opensea_crawler_state") + op.drop_index("idx_ethereum_labels_opensea_nft_name") + # ### end Alembic commands ### diff --git a/db/moonstreamdb/models.py b/db/moonstreamdb/models.py index 3114133f..7d7798a4 100644 --- a/db/moonstreamdb/models.py +++ b/db/moonstreamdb/models.py @@ -11,7 +11,6 @@ from sqlalchemy import ( Numeric, Text, VARCHAR, - UniqueConstraint, ) from sqlalchemy.dialects.postgresql import JSONB, UUID from sqlalchemy.sql import expression @@ -136,7 +135,6 @@ class EthereumLabel(Base): # type: ignore """ __tablename__ = "ethereum_labels" - __table_args__ = (UniqueConstraint("label", "address_id"),) id = Column( UUID(as_uuid=True), @@ -212,3 +210,22 @@ class ESDEventSignature(Base): # type: ignore created_at = Column( DateTime(timezone=True), server_default=utcnow(), nullable=False ) + + +class OpenSeaCrawlingState(Base): # type: ignore + """ + Model for control opeansea crawling state. + """ + + __tablename__ = "opensea_crawler_state" + + id = Column(Integer, primary_key=True, unique=True, nullable=False) + query = Column(Text, nullable=False) + crawled_at = Column( + DateTime(timezone=True), + server_default=utcnow(), + onupdate=utcnow(), + nullable=False, + ) + + total_count = Column(Integer, nullable=False) diff --git a/frontend/pages/index.js b/frontend/pages/index.js index c3e21079..0bcfdeaa 100644 --- a/frontend/pages/index.js +++ b/frontend/pages/index.js @@ -24,12 +24,15 @@ import { } from "@chakra-ui/react"; import dynamic from "next/dynamic"; import useUser from "../src/core/hooks/useUser"; -import useAnalytics from "../src/core/hooks/useAnalytics"; import useModals from "../src/core/hooks/useModals"; import useRouter from "../src/core/hooks/useRouter"; -import { MIXPANEL_PROPS } from "../src/core/providers/AnalyticsProvider/constants"; +import { + MIXPANEL_PROPS, + MIXPANEL_EVENTS, +} from "../src/core/providers/AnalyticsProvider/constants"; import UIContext from "../src/core/providers/UIProvider/context"; import { AWS_ASSETS_PATH } from "../src/core/constants"; +import mixpanel from "mixpanel-browser"; const SplitWithImage = dynamic( () => import("../src/components/SplitWithImage"), { @@ -105,7 +108,6 @@ const Homepage = () => { const router = useRouter(); const { isInit } = useUser(); - const { MIXPANEL_EVENTS, track } = useAnalytics(); const { toggleModal } = useModals(); const [ isLargerThan720px, @@ -379,27 +381,30 @@ const Homepage = () => { label: "Crypto trader", link: "/#cryptoTrader", onClick: () => { - track(`${MIXPANEL_EVENTS.BUTTON_CLICKED}`, { - [`${MIXPANEL_PROPS.BUTTON_CLICKED}`]: `scroll to CryptoTrader`, - }); + mixpanel.get_distinct_id() && + mixpanel.track(`${MIXPANEL_EVENTS.BUTTON_CLICKED}`, { + [`${MIXPANEL_PROPS.BUTTON_NAME}`]: `scroll to CryptoTrader`, + }); }, }} button2={{ label: "Algorithmic Fund", link: "/#algoFund", onClick: () => { - track(`${MIXPANEL_EVENTS.BUTTON_CLICKED}`, { - [`${MIXPANEL_PROPS.BUTTON_CLICKED}`]: `scroll to AlgoFund`, - }); + mixpanel.get_distinct_id() && + mixpanel.track(`${MIXPANEL_EVENTS.BUTTON_CLICKED}`, { + [`${MIXPANEL_PROPS.BUTTON_NAME}`]: `scroll to AlgoFund`, + }); }, }} button3={{ label: "Developer", link: "/#smartDeveloper", onClick: () => { - track(`${MIXPANEL_EVENTS.BUTTON_CLICKED}`, { - [`${MIXPANEL_PROPS.BUTTON_CLICKED}`]: `scroll to Developer`, - }); + mixpanel.get_distinct_id() && + mixpanel.track(`${MIXPANEL_EVENTS.BUTTON_CLICKED}`, { + [`${MIXPANEL_PROPS.BUTTON_NAME}`]: `scroll to Developer`, + }); }, }} /> @@ -417,9 +422,10 @@ const Homepage = () => { cta={{ label: "I want early access!", onClick: () => { - track(`${MIXPANEL_EVENTS.BUTTON_CLICKED}`, { - [`${MIXPANEL_PROPS.BUTTON_CLICKED}`]: `Early access CTA: Crypto trader`, - }); + mixpanel.get_distinct_id() && + mixpanel.track(`${MIXPANEL_EVENTS.BUTTON_CLICKED}`, { + [`${MIXPANEL_PROPS.BUTTON_NAME}`]: `Early access CTA: Crypto trader`, + }); toggleModal("hubspot-trader"); }, }} @@ -464,9 +470,10 @@ const Homepage = () => { cta={{ label: "I want early access!", onClick: () => { - track(`${MIXPANEL_EVENTS.BUTTON_CLICKED}`, { - [`${MIXPANEL_PROPS.BUTTON_CLICKED}`]: `Early access CTA: Algo fund`, - }); + mixpanel.get_distinct_id() && + mixpanel.track(`${MIXPANEL_EVENTS.BUTTON_CLICKED}`, { + [`${MIXPANEL_PROPS.BUTTON_NAME}`]: `Early access CTA: Algo fund`, + }); toggleModal("hubspot-fund"); }, }} @@ -509,9 +516,10 @@ const Homepage = () => { cta={{ label: "I want early access!", onClick: () => { - track(`${MIXPANEL_EVENTS.BUTTON_CLICKED}`, { - [`${MIXPANEL_PROPS.BUTTON_CLICKED}`]: `Early access CTA: developer`, - }); + mixpanel.get_distinct_id() && + mixpanel.track(`${MIXPANEL_EVENTS.BUTTON_CLICKED}`, { + [`${MIXPANEL_PROPS.BUTTON_NAME}`]: `Early access CTA: developer`, + }); toggleModal("hubspot-developer"); }, }} @@ -520,9 +528,10 @@ const Homepage = () => { network: "github", label: "See our github", onClick: () => { - track(`${MIXPANEL_EVENTS.BUTTON_CLICKED}`, { - [`${MIXPANEL_PROPS.BUTTON_CLICKED}`]: `Github link in landing page`, - }); + mixpanel.get_distinct_id() && + mixpanel.track(`${MIXPANEL_EVENTS.BUTTON_CLICKED}`, { + [`${MIXPANEL_PROPS.BUTTON_NAME}`]: `Github link in landing page`, + }); }, }} elementName={"element3"} @@ -568,9 +577,10 @@ const Homepage = () => { colorScheme="suggested" id="test" onClick={() => { - track(`${MIXPANEL_EVENTS.BUTTON_CLICKED}`, { - [`${MIXPANEL_PROPS.BUTTON_CLICKED}`]: `Join our discord`, - }); + mixpanel.get_distinct_id() && + mixpanel.track(`${MIXPANEL_EVENTS.BUTTON_CLICKED}`, { + [`${MIXPANEL_PROPS.BUTTON_NAME}`]: `Join our discord`, + }); toggleModal("hubspot"); }} > diff --git a/frontend/src/AppContext.js b/frontend/src/AppContext.js index 7a238760..f2c56e74 100644 --- a/frontend/src/AppContext.js +++ b/frontend/src/AppContext.js @@ -13,13 +13,13 @@ const AppContext = (props) => { return ( <UserProvider> <ModalProvider> - <AnalyticsProvider> - <StripeProvider> - <ChakraProvider theme={theme}> - <UIProvider>{props.children}</UIProvider> - </ChakraProvider> - </StripeProvider> - </AnalyticsProvider> + <StripeProvider> + <ChakraProvider theme={theme}> + <UIProvider> + <AnalyticsProvider>{props.children}</AnalyticsProvider> + </UIProvider> + </ChakraProvider> + </StripeProvider> </ModalProvider> </UserProvider> ); diff --git a/frontend/src/components/Footer.js b/frontend/src/components/Footer.js index d212befe..2b95a9f6 100644 --- a/frontend/src/components/Footer.js +++ b/frontend/src/components/Footer.js @@ -6,7 +6,7 @@ import RouterLink from "next/link"; const ICONS = [ { social: "discord", - link: "https://discord.gg/FetK5BxD", + link: "https://discord.gg/K56VNUQGvA", }, { social: "twit", link: "https://twitter.com/moonstreamto" }, diff --git a/frontend/src/components/NewSubscription.js b/frontend/src/components/NewSubscription.js index 230ea755..284e3c3b 100644 --- a/frontend/src/components/NewSubscription.js +++ b/frontend/src/components/NewSubscription.js @@ -35,14 +35,35 @@ const _NewSubscription = ({ const [radioState, setRadioState] = useState( initialType ?? "ethereum_blockchain" ); + + const [subscriptionAdressFormatRadio, setsubscriptionAdressFormatRadio] = + useState(""); + let { getRootProps, getRadioProps } = useRadioGroup({ name: "type", defaultValue: radioState, onChange: setRadioState, }); + let { + getRootProps: getRootPropsSubscription, + getRadioProps: getRadioPropsSubscription, + } = useRadioGroup({ + name: "subscription", + onChange: setsubscriptionAdressFormatRadio, + }); + + console.log( + useRadioGroup({ + name: "subscription1", + onChange: setsubscriptionAdressFormatRadio, + }) + ); + const group = getRootProps(); + const subscriptionAddressTypeGroup = getRootPropsSubscription(); + useEffect(() => { if (setIsLoading) { setIsLoading(createSubscription.isLoading); @@ -57,6 +78,13 @@ const _NewSubscription = ({ const createSubscriptionWrapper = useCallback( (props) => { + if ( + subscriptionAdressFormatRadio.startsWith("tags") && + radioState != "ethereum_whalewatch" + ) { + props.address = subscriptionAdressFormatRadio.split(":")[1]; + } + createSubscription.mutate({ ...props, color: color, @@ -68,6 +96,16 @@ const _NewSubscription = ({ if (typesCache.isLoading) return <Spinner />; + function search(nameKey, myArray) { + for (var i = 0; i < myArray.length; i++) { + if (myArray[i].id === nameKey) { + return myArray[i]; + } + } + } + + console.log(radioState); + const handleChangeColorComplete = (color) => { setColor(color.hex); }; @@ -89,19 +127,6 @@ const _NewSubscription = ({ {errors?.label && errors?.label.message} </FormErrorMessage> </FormControl> - <FormControl isInvalid={errors?.address}> - <Input - type="text" - autoComplete="off" - my={2} - placeholder="Address to subscribe to" - name="address" - ref={register({ required: "address is required!" })} - ></Input> - <FormErrorMessage color="unsafe.400" pl="1"> - {errors?.address && errors?.address.message} - </FormErrorMessage> - </FormControl> <Stack my={4} direction="column"> {/* <Text fontWeight="600"> {isFreeOption @@ -119,6 +144,8 @@ const _NewSubscription = ({ !type.active || (isFreeOption && type.id !== "ethereum_blockchain"), }); + console.log(type); + return ( <RadioCard key={`subscription_type_${type.id}`} {...radio}> {type.name} @@ -126,6 +153,46 @@ const _NewSubscription = ({ ); })} </HStack> + <Stack my={4} direction="column"> + <FormControl isInvalid={errors?.subscription_type}> + <HStack {...subscriptionAddressTypeGroup} alignItems="stretch"> + {search(radioState, typesCache.data).choices.map( + (addition_selects) => { + const radio = getRadioPropsSubscription({ + value: addition_selects, + }); + console.log(radio); + return ( + <RadioCard + key={`subscription_tags_${addition_selects}`} + {...radio} + > + {addition_selects.split(":")[1]} + </RadioCard> + ); + // } + } + )} + </HStack> + </FormControl> + </Stack> + + {subscriptionAdressFormatRadio.startsWith("input") && + radioState != "ethereum_whalewatch" && ( + <FormControl isInvalid={errors?.address}> + <Input + type="text" + autoComplete="off" + my={2} + placeholder="Address to subscribe to" + name="address" + ref={register({ required: "address is required!" })} + ></Input> + <FormErrorMessage color="unsafe.400" pl="1"> + {errors?.address && errors?.address.message} + </FormErrorMessage> + </FormControl> + )} <Input type="hidden" placeholder="subscription_type" diff --git a/frontend/src/components/Scrollable.js b/frontend/src/components/Scrollable.js index 4ab9f6a7..bdcdf0f0 100644 --- a/frontend/src/components/Scrollable.js +++ b/frontend/src/components/Scrollable.js @@ -1,13 +1,13 @@ import { Flex, Box } from "@chakra-ui/react"; import React, { useEffect, useRef, useState } from "react"; -import { useRouter, useAnalytics } from "../core/hooks"; +import { useRouter } from "../core/hooks"; +import mixpanel from "mixpanel-browser"; const Scrollable = (props) => { const scrollerRef = useRef(); const router = useRouter(); const [path, setPath] = useState(); const [scrollDepth, setScrollDepth] = useState(0); - const { mixpanel, isLoaded } = useAnalytics(); const getScrollPrecent = ({ currentTarget }) => { const scroll_level = @@ -20,7 +20,7 @@ const Scrollable = (props) => { const currentScroll = Math.ceil(getScrollPrecent(e) / 10); if (currentScroll > scrollDepth) { setScrollDepth(currentScroll); - isLoaded && + mixpanel.get_distinct_id() && mixpanel.people.increment({ [`Scroll depth at: ${router.nextRouter.pathname}`]: currentScroll, }); diff --git a/frontend/src/core/hooks/index.js b/frontend/src/core/hooks/index.js index 328a74da..5625f907 100644 --- a/frontend/src/core/hooks/index.js +++ b/frontend/src/core/hooks/index.js @@ -1,5 +1,4 @@ export { queryCacheProps as hookCommon } from "./hookCommon"; -export { default as useAnalytics } from "./useAnalytics"; export { default as useAuthResultHandler } from "./useAuthResultHandler"; export { default as useChangePassword } from "./useChangePassword"; export { default as useClientID } from "./useClientID"; diff --git a/frontend/src/core/hooks/useAnalytics.js b/frontend/src/core/hooks/useAnalytics.js deleted file mode 100644 index fee7e9eb..00000000 --- a/frontend/src/core/hooks/useAnalytics.js +++ /dev/null @@ -1,38 +0,0 @@ -import AnalyticsContext from "../providers/AnalyticsProvider/context"; -import { useContext } from "react"; -import { useState, useEffect, useCallback } from "react"; -const useAnalytics = () => { - const { mixpanel, isLoaded, MIXPANEL_EVENTS, MIXPANEL_PROPS } = - useContext(AnalyticsContext); - const [trackProps, setTrackProps] = useState({ - event: null, - props: null, - queued: false, - }); - - const track = useCallback((e, props) => { - setTrackProps({ event: e, props: props, queued: true }); - }, []); - - useEffect(() => { - if (isLoaded && trackProps.queued === true) { - mixpanel.track(trackProps.event, trackProps.props); - setTrackProps({ event: null, props: null, queued: false }); - } - }, [isLoaded, mixpanel, trackProps]); - - const withTracking = (fn, event, props) => { - track(event, props); - return fn; - }; - - return { - mixpanel, - isLoaded, - track, - MIXPANEL_PROPS, - MIXPANEL_EVENTS, - withTracking, - }; -}; -export default useAnalytics; diff --git a/frontend/src/core/hooks/useLogin.js b/frontend/src/core/hooks/useLogin.js index c8dd3bfc..b410cc9a 100644 --- a/frontend/src/core/hooks/useLogin.js +++ b/frontend/src/core/hooks/useLogin.js @@ -1,7 +1,6 @@ import { useMutation } from "react-query"; import { useToast, useUser, useInviteAccept } from "."; import { AuthService } from "../services"; -import { useAnalytics } from "."; const LOGIN_TYPES = { MANUAL: true, @@ -10,7 +9,6 @@ const LOGIN_TYPES = { const useLogin = (loginType) => { const { getUser } = useUser(); const toast = useToast(); - const analytics = useAnalytics(); const { inviteAccept } = useInviteAccept(); const { mutate: login, @@ -34,20 +32,6 @@ const useLogin = (loginType) => { inviteAccept(invite_code); } getUser(); - if (analytics.isLoaded) { - analytics.mixpanel.people.set_once({ - [`${analytics.MIXPANEL_EVENTS.FIRST_LOGIN_DATE}`]: - new Date().toISOString(), - }); - analytics.mixpanel.people.set({ - [`${analytics.MIXPANEL_EVENTS.LAST_LOGIN_DATE}`]: - new Date().toISOString(), - }); - analytics.mixpanel.track( - `${analytics.MIXPANEL_EVENTS.USER_LOGS_IN}`, - {} - ); - } } }, onError: (error) => { diff --git a/frontend/src/core/hooks/useLogout.js b/frontend/src/core/hooks/useLogout.js index ff605d80..c8be9a86 100644 --- a/frontend/src/core/hooks/useLogout.js +++ b/frontend/src/core/hooks/useLogout.js @@ -1,21 +1,14 @@ import { useCallback, useContext } from "react"; import { useMutation, useQueryClient } from "react-query"; -import { useUser, useRouter, useAnalytics } from "."; +import { useUser, useRouter } from "."; import UIContext from "../providers/UIProvider/context"; import { AuthService } from "../services"; const useLogout = () => { const { setLoggingOut } = useContext(UIContext); const router = useRouter(); - const analytics = useAnalytics(); const { mutate: revoke } = useMutation(AuthService.revoke, { onSuccess: () => { - if (analytics.isLoaded) { - analytics.mixpanel.track( - `${analytics.MIXPANEL_EVENTS.USER_LOGS_OUT}`, - {} - ); - } localStorage.removeItem("MOONSTREAM_ACCESS_TOKEN"); cache.clear(); setUser(null); diff --git a/frontend/src/core/hooks/useSignUp.js b/frontend/src/core/hooks/useSignUp.js index f6dca897..6ae77f7e 100644 --- a/frontend/src/core/hooks/useSignUp.js +++ b/frontend/src/core/hooks/useSignUp.js @@ -1,16 +1,18 @@ import { useContext } from "react"; import { useMutation } from "react-query"; import { AuthService } from "../services"; -import { useUser, useToast, useInviteAccept, useRouter, useAnalytics } from "."; +import { useUser, useToast, useInviteAccept, useRouter } from "."; import UIContext from "../providers/UIProvider/context"; +import mixpanel from "mixpanel-browser"; +import { MIXPANEL_EVENTS } from "../providers/AnalyticsProvider/constants"; const useSignUp = (source) => { const ui = useContext(UIContext); + const router = useRouter(); const { getUser } = useUser(); const toast = useToast(); const { inviteAccept } = useInviteAccept(); - const analytics = useAnalytics(); const { mutate: signUp, @@ -26,11 +28,11 @@ const useSignUp = (source) => { inviteAccept(invite_code); } - if (analytics.isLoaded) { - analytics.mixpanel.track( - `${analytics.MIXPANEL_EVENTS.CONVERT_TO_USER}`, - { full_url: router.nextRouter.asPath, code: source } - ); + if (mixpanel.get_distinct_id()) { + mixpanel.track(`${MIXPANEL_EVENTS.CONVERT_TO_USER}`, { + full_url: router.nextRouter.asPath, + code: source, + }); } getUser(); ui.setisOnboardingComplete(false); diff --git a/frontend/src/core/hooks/useToast.js b/frontend/src/core/hooks/useToast.js index eeae3e7e..6d78ed9a 100644 --- a/frontend/src/core/hooks/useToast.js +++ b/frontend/src/core/hooks/useToast.js @@ -1,21 +1,18 @@ import { useToast as useChakraToast, Box } from "@chakra-ui/react"; import React, { useCallback } from "react"; -import { useAnalytics } from "."; +import mixpanel from "mixpanel-browser"; +import { MIXPANEL_EVENTS } from "../providers/AnalyticsProvider/constants"; const useToast = () => { const chakraToast = useChakraToast(); - const analytics = useAnalytics(); const toast = useCallback( (message, type) => { - if (analytics.isLoaded && type === "error") { - analytics.mixpanel.track( - `${analytics.MIXPANEL_EVENTS.TOAST_ERROR_DISPLAYED}`, - { - status: message?.response?.status, - detail: message?.response?.data.detail, - } - ); + if (mixpanel.get_distinct_id() && type === "error") { + mixpanel.track(`${MIXPANEL_EVENTS.TOAST_ERROR_DISPLAYED}`, { + status: message?.response?.status, + detail: message?.response?.data.detail, + }); } const background = type === "error" ? "unsafe.500" : "suggested.500"; const userMessage = @@ -43,7 +40,7 @@ const useToast = () => { ), }); }, - [chakraToast, analytics] + [chakraToast] ); return toast; diff --git a/frontend/src/core/providers/AnalyticsProvider/constants.js b/frontend/src/core/providers/AnalyticsProvider/constants.js index 309532e1..d8dc2a09 100644 --- a/frontend/src/core/providers/AnalyticsProvider/constants.js +++ b/frontend/src/core/providers/AnalyticsProvider/constants.js @@ -8,6 +8,12 @@ export const MIXPANEL_PROPS = { USER_SPECIALITY: "user speciality", }; +export const MIXPANEL_GROUPS = { + DEVELOPERS: "developers", + TRADERS: "traders", + FUND: "funds", +}; + export const MIXPANEL_EVENTS = { FIRST_LOGIN_DATE: "First login date", LAST_LOGIN_DATE: "Last login date", @@ -20,7 +26,14 @@ export const MIXPANEL_EVENTS = { PAGEVIEW: "Page view", PRICING_PLAN_CLICKED: "Pricing Plan clicked", BUTTON_CLICKED: "Button clicked", - LEFT_PAGE: "Left page", + BEACON: "beacon", + ONBOARDING_COMPLETED: "Onbording complete", + SESSIONS_COUNT: "Sessions Counter", + ONBOARDING_STEP: "Onboarding step", + ONBOARDING_STATE: "Onboarding state", + TIMES_VISITED: "Page visit times", + FORM_SUBMITTED: "form submitted", + PAGEVIEW_DURATION: "Time spent on page", }; export default MIXPANEL_EVENTS; diff --git a/frontend/src/core/providers/AnalyticsProvider/index.js b/frontend/src/core/providers/AnalyticsProvider/index.js index eddffb8a..f05b2d85 100644 --- a/frontend/src/core/providers/AnalyticsProvider/index.js +++ b/frontend/src/core/providers/AnalyticsProvider/index.js @@ -1,66 +1,161 @@ -import React, { useEffect, useState } from "react"; +import React, { useContext, useEffect, useState } from "react"; import mixpanel from "mixpanel-browser"; import AnalyticsContext from "./context"; import { useClientID, useUser, useRouter } from "../../hooks"; import { MIXPANEL_EVENTS, MIXPANEL_PROPS } from "./constants"; +import UIContext from "../UIProvider/context"; const AnalyticsProvider = ({ children }) => { const clientID = useClientID(); const analytics = process.env.NEXT_PUBLIC_MIXPANEL_TOKEN; - const { user } = useUser(); - const [isLoaded, setIsLoaded] = useState(false); + const { user, isInit } = useUser(); + const [isMixpanelReady, setIsLoaded] = useState(false); const router = useRouter(); + const ui = useContext(UIContext); + // ********** OBOARDING STATE ************** + useEffect(() => { + if (ui.onboardingState && isMixpanelReady) { + mixpanel.people.set(MIXPANEL_EVENTS.ONBOARDING_STATE, { + state: { ...ui.onboardingState }, + }); + } + }, [ui.onboardingState, isMixpanelReady]); + + useEffect(() => { + if (ui.isOnboardingComplete && isMixpanelReady && user) { + mixpanel.people.set(MIXPANEL_EVENTS.ONBOARDING_COMPLETED, true); + } + }, [ui.isOnboardingComplete, isMixpanelReady, user]); + + // ********** ONBOARDING STEP and TIMING ************** + const [previousOnboardingStep, setPreviousOnboardingStep] = useState(false); + + useEffect(() => { + if (isMixpanelReady && router.nextRouter.pathname === "/welcome") { + if (!previousOnboardingStep) { + mixpanel.time_event(MIXPANEL_EVENTS.ONBOARDING_STEP); + setPreviousOnboardingStep(ui.onboardingStep); + } + if ( + previousOnboardingStep && + previousOnboardingStep !== ui.onboardingStep + ) { + mixpanel.track(MIXPANEL_EVENTS.ONBOARDING_STEP, { + step: previousOnboardingStep, + isBeforeUnload: false, + }); + setPreviousOnboardingStep(false); + } + } else if (previousOnboardingStep) { + mixpanel.track(MIXPANEL_EVENTS.ONBOARDING_STEP, { + step: previousOnboardingStep, + isBeforeUnload: false, + }); + setPreviousOnboardingStep(false); + } + }, [ + previousOnboardingStep, + ui.onboardingStep, + isMixpanelReady, + router.nextRouter.pathname, + ]); + + // ********** PING_PONG ************** useEffect(() => { let durationSeconds = 0; const intervalId = - isLoaded && + isMixpanelReady && setInterval(() => { - durationSeconds = durationSeconds + 1; + durationSeconds = durationSeconds + 30; mixpanel.track( - MIXPANEL_EVENTS.LEFT_PAGE, + MIXPANEL_EVENTS.BEACON, { duration_seconds: durationSeconds, url: router.nextRouter.pathname, - query: router.query, - pathParams: router.params, }, { transport: "sendBeacon" } ); }, 30000); return () => clearInterval(intervalId); - // eslint-disable-next-line - }, [isLoaded]); + }, [isMixpanelReady, router.nextRouter.pathname]); + + // ********** TIME SPENT ON PATH************** + + const [previousPathname, setPreviousPathname] = useState(false); useEffect(() => { - isLoaded && + if (isMixpanelReady) { + if (!previousPathname) { + mixpanel.time_event(MIXPANEL_EVENTS.PAGEVIEW_DURATION); + setPreviousPathname(router.nextRouter.pathname); + } + if (previousPathname && previousPathname !== router.nextRouter.pathname) { + mixpanel.track(MIXPANEL_EVENTS.PAGEVIEW_DURATION, { + url: previousPathname, + isBeforeUnload: false, + }); + setPreviousPathname(false); + } + } + }, [router.nextRouter.pathname, previousPathname, isMixpanelReady]); + + // ********** PAGES VIEW ************** + useEffect(() => { + if (isMixpanelReady && ui.sessionId && router.nextRouter.pathname) { mixpanel.track(MIXPANEL_EVENTS.PAGEVIEW, { url: router.nextRouter.pathname, - query: router.query, - pathParams: router.params, + sessionID: ui.sessionId, }); - }, [router.nextRouter.pathname, router.query, router.params, isLoaded]); - useEffect(() => { - try { - mixpanel.init(analytics, { - api_host: "https://api.mixpanel.com", - loaded: () => { - setIsLoaded(true); - mixpanel.identify(clientID); - }, + mixpanel.people.increment([ + `${MIXPANEL_EVENTS.TIMES_VISITED} ${router.nextRouter.pathname}`, + ]); + } + const urlForUnmount = router.nextRouter.pathname; + const closeListener = () => { + mixpanel.track(MIXPANEL_EVENTS.PAGEVIEW_DURATION, { + url: urlForUnmount, + isBeforeUnload: true, }); - } catch (error) { - console.warn("loading mixpanel failed:", error); + }; + window.addEventListener("beforeunload", closeListener); + //cleanup function fires on useEffect unmount + //https://reactjs.org/docs/hooks-effect.html + return () => { + window.removeEventListener("beforeunload", closeListener); + }; + }, [router.nextRouter.pathname, isMixpanelReady, ui.sessionId]); + + // ********** SESSION STATE ************** + useEffect(() => { + if (clientID) { + try { + mixpanel.init(analytics, { + api_host: "https://api.mixpanel.com", + loaded: () => { + setIsLoaded(true); + mixpanel.identify(clientID); + }, + }); + } catch (error) { + console.warn("loading mixpanel failed:", error); + } } }, [analytics, clientID]); + useEffect(() => { + isMixpanelReady && mixpanel.register("sessionId", ui.sessionId); + }, [ui.sessionId, isMixpanelReady]); + + // ********** USER STATE ************** + useEffect(() => { if (user) { try { - if (isLoaded) { + if (isMixpanelReady) { mixpanel.people.set({ [`${MIXPANEL_EVENTS.LAST_VISITED}`]: new Date().toISOString(), }); @@ -74,11 +169,36 @@ const AnalyticsProvider = ({ children }) => { console.error("could not set up people in mixpanel:", err); } } - }, [user, isLoaded, clientID]); + }, [user, isMixpanelReady, clientID]); + + useEffect(() => { + if (isMixpanelReady && user) { + mixpanel.people.set_once({ + [`${MIXPANEL_EVENTS.FIRST_LOGIN_DATE}`]: new Date().toISOString(), + }); + mixpanel.people.set({ + [`${MIXPANEL_EVENTS.LAST_LOGIN_DATE}`]: new Date().toISOString(), + }); + mixpanel.track(`${MIXPANEL_EVENTS.USER_LOGS_IN}`, {}); + } + }, [user, isMixpanelReady]); + + useEffect(() => { + if (isMixpanelReady && ui.isLoggingOut) { + mixpanel.track(`${MIXPANEL_EVENTS.USER_LOGS_OUT}`, {}); + } + }, [ui.isLoggingOut, isMixpanelReady]); + + // ********** USER BOUNCE TIME ************** + useEffect(() => { + if (!user && isInit && isMixpanelReady) { + mixpanel.time_event(MIXPANEL_EVENTS.CONVERT_TO_USER); + } + }, [user, isInit, isMixpanelReady]); return ( <AnalyticsContext.Provider - value={{ mixpanel, isLoaded, MIXPANEL_EVENTS, MIXPANEL_PROPS }} + value={{ mixpanel, isMixpanelReady, MIXPANEL_EVENTS, MIXPANEL_PROPS }} > {children} </AnalyticsContext.Provider> diff --git a/frontend/src/core/providers/UIProvider/index.js b/frontend/src/core/providers/UIProvider/index.js index 9c92d494..d20223a8 100644 --- a/frontend/src/core/providers/UIProvider/index.js +++ b/frontend/src/core/providers/UIProvider/index.js @@ -181,10 +181,19 @@ const UIProvider = ({ children }) => { ); const [onboardingStep, setOnboardingStep] = useState(() => { - const step = onboardingSteps.findIndex( - (event) => onboardingState[event.step] === 0 + //First find onboarding step that was viewed once (resume where left) + // If none - find step that was never viewed + // if none - set onboarding to zero + let step = onboardingSteps.findIndex( + (event) => onboardingState[event.step] === 1 ); - if (step === -1 && isOnboardingComplete) return onboardingSteps.length - 1; + step = + step === -1 + ? onboardingSteps.findIndex( + (event) => onboardingState[event.step] === 0 + ) + : step; + if (step === -1 && isOnboardingComplete) return 0; else if (step === -1 && !isOnboardingComplete) return 0; else return step; }); @@ -263,6 +272,8 @@ const UIProvider = ({ children }) => { setisOnboardingComplete, onboardingSteps, setOnboardingState, + onboardingState, + isLoggingOut, }} > {children} From 212ce8bb3c66c229120c815e4a73f9e50a8c4cdf Mon Sep 17 00:00:00 2001 From: Andrey Dolgolev <andrey@simiotics.com> Date: Tue, 7 Sep 2021 16:02:40 +0300 Subject: [PATCH 02/14] Fix. --- backend/moonstream/data.py | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/moonstream/data.py b/backend/moonstream/data.py index 901ef78f..df4b6537 100644 --- a/backend/moonstream/data.py +++ b/backend/moonstream/data.py @@ -25,6 +25,7 @@ class SubscriptionTypesListResponse(BaseModel): class SubscriptionResourceData(BaseModel): id: str + address: str color: Optional[str] label: Optional[str] user_id: str From be233e644a4a0536d7353cc25230ee773299df9e Mon Sep 17 00:00:00 2001 From: Andrey Dolgolev <andrey@simiotics.com> Date: Tue, 7 Sep 2021 18:45:48 +0300 Subject: [PATCH 03/14] Add fixes. --- frontend/pages/subscriptions.js | 18 ++- frontend/src/components/NewSubscription.js | 137 ++++++++++++--------- 2 files changed, 90 insertions(+), 65 deletions(-) diff --git a/frontend/pages/subscriptions.js b/frontend/pages/subscriptions.js index fe0724cd..b8185eea 100644 --- a/frontend/pages/subscriptions.js +++ b/frontend/pages/subscriptions.js @@ -12,10 +12,13 @@ import { Button, Modal, useDisclosure, + ModalHeader, + ModalCloseButton, + ModalBody, ModalOverlay, ModalContent, } from "@chakra-ui/react"; -import NewSubscription from "../src/components/NewModalSubscripton"; +import NewSubscription from "../src/components/NewSubscription"; import { AiOutlinePlusCircle } from "react-icons/ai"; const Subscriptions = () => { @@ -52,10 +55,15 @@ const Subscriptions = () => { <ModalOverlay /> <ModalContent> - <NewSubscription - isFreeOption={isAddingFreeSubscription} - onClose={onClose} - /> + <ModalHeader>Subscribe to a new address</ModalHeader> + <ModalCloseButton /> + <ModalBody> + <NewSubscription + isFreeOption={isAddingFreeSubscription} + onClose={onClose} + isModal={true} + /> + </ModalBody> </ModalContent> </Modal> {subscriptionsCache.isLoading ? ( diff --git a/frontend/src/components/NewSubscription.js b/frontend/src/components/NewSubscription.js index 284e3c3b..fa5efd72 100644 --- a/frontend/src/components/NewSubscription.js +++ b/frontend/src/components/NewSubscription.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback } from "react"; +import React, { useState, useEffect, useCallback, Fragment } from "react"; import { useSubscriptions } from "../core/hooks"; import { Input, @@ -19,6 +19,7 @@ import RadioCard from "./RadioCard"; // import { useForm } from "react-hook-form"; import { CirclePicker } from "react-color"; import { BiRefresh } from "react-icons/bi"; +import { GithubPicker } from "react-color"; import { makeColor } from "../core/utils/makeColor"; import { useForm } from "react-hook-form"; const _NewSubscription = ({ @@ -27,6 +28,7 @@ const _NewSubscription = ({ setIsLoading, initialAddress, initialType, + isModal, }) => { const [color, setColor] = useState(makeColor()); const { handleSubmit, errors, register } = useForm({}); @@ -36,8 +38,13 @@ const _NewSubscription = ({ initialType ?? "ethereum_blockchain" ); + const mapper = { + "tag:nfts": "NFTs", + "input:address": "Address", + }; + const [subscriptionAdressFormatRadio, setsubscriptionAdressFormatRadio] = - useState(""); + useState("input:address"); let { getRootProps, getRadioProps } = useRadioGroup({ name: "type", @@ -50,6 +57,7 @@ const _NewSubscription = ({ getRadioProps: getRadioPropsSubscription, } = useRadioGroup({ name: "subscription", + defaultValue: subscriptionAdressFormatRadio, onChange: setsubscriptionAdressFormatRadio, }); @@ -78,11 +86,13 @@ const _NewSubscription = ({ const createSubscriptionWrapper = useCallback( (props) => { + props.label = "Address"; if ( subscriptionAdressFormatRadio.startsWith("tags") && radioState != "ethereum_whalewatch" ) { - props.address = subscriptionAdressFormatRadio.split(":")[1]; + props.address = subscriptionAdressFormatRadio; + props.label = "tags: NFTs"; } createSubscription.mutate({ @@ -104,8 +114,6 @@ const _NewSubscription = ({ } } - console.log(radioState); - const handleChangeColorComplete = (color) => { setColor(color.hex); }; @@ -114,26 +122,7 @@ const _NewSubscription = ({ return ( <form onSubmit={handleSubmit(createSubscriptionWrapper)}> - <FormControl isInvalid={errors?.label}> - <Input - my={2} - type="text" - autoComplete="off" - placeholder="Name of subscription (you can change it later)" - name="label" - ref={register({ required: "label is required!" })} - ></Input> - <FormErrorMessage color="unsafe.400" pl="1"> - {errors?.label && errors?.label.message} - </FormErrorMessage> - </FormControl> <Stack my={4} direction="column"> - {/* <Text fontWeight="600"> - {isFreeOption - ? `Right now you can subscribe only to ethereum blockchain` - : `On which source?`} - </Text> */} - <FormControl isInvalid={errors?.subscription_type}> <HStack {...group} alignItems="stretch"> {typesCache.data.map((type) => { @@ -144,7 +133,6 @@ const _NewSubscription = ({ !type.active || (isFreeOption && type.id !== "ethereum_blockchain"), }); - console.log(type); return ( <RadioCard key={`subscription_type_${type.id}`} {...radio}> @@ -158,19 +146,19 @@ const _NewSubscription = ({ <HStack {...subscriptionAddressTypeGroup} alignItems="stretch"> {search(radioState, typesCache.data).choices.map( (addition_selects) => { + console.log(typeof addition_selects); const radio = getRadioPropsSubscription({ value: addition_selects, + isDisabled: addition_selects.startsWith("tag"), }); - console.log(radio); return ( <RadioCard key={`subscription_tags_${addition_selects}`} {...radio} > - {addition_selects.split(":")[1]} + {mapper[addition_selects]} </RadioCard> ); - // } } )} </HStack> @@ -207,40 +195,69 @@ const _NewSubscription = ({ </FormControl> </Stack> <FormControl isInvalid={errors?.color}> - <Flex direction="row" pb={2} flexWrap="wrap"> - <Stack pt={2} direction="row" h="min-content"> - <Text fontWeight="600" alignSelf="center"> - Label color - </Text>{" "} - <IconButton - size="md" - // colorScheme="primary" - color={"white.100"} - _hover={{ bgColor: { color } }} - bgColor={color} - variant="outline" - onClick={() => setColor(makeColor())} - icon={<BiRefresh />} - /> - <Input - type="input" - placeholder="color" - name="color" - ref={register({ required: "color is required!" })} - value={color} - onChange={() => null} - w="200px" - ></Input> - </Stack> - <Flex p={2}> - <CirclePicker - onChangeComplete={handleChangeColorComplete} - circleSpacing={1} - circleSize={24} - /> + {!isModal ? ( + <Flex direction="row" pb={2} flexWrap="wrap"> + <Stack pt={2} direction="row" h="min-content"> + <Text fontWeight="600" alignSelf="center"> + Label color + </Text>{" "} + <IconButton + size="md" + // colorScheme="primary" + color={"white.100"} + _hover={{ bgColor: { color } }} + bgColor={color} + variant="outline" + onClick={() => setColor(makeColor())} + icon={<BiRefresh />} + /> + <Input + type="input" + placeholder="color" + name="color" + ref={register({ required: "color is required!" })} + value={color} + onChange={() => null} + w="200px" + ></Input> + </Stack> + <Flex p={2}> + <CirclePicker + onChangeComplete={handleChangeColorComplete} + circleSpacing={1} + circleSize={24} + /> + </Flex> </Flex> - </Flex> + ) : ( + <> + <Stack direction="row" pb={2}> + <Text fontWeight="600" alignSelf="center"> + Label color + </Text>{" "} + <IconButton + size="md" + color={"white.100"} + _hover={{ bgColor: { color } }} + bgColor={color} + variant="outline" + onClick={() => setColor(makeColor())} + icon={<BiRefresh />} + /> + <Input + type="input" + placeholder="color" + name="color" + ref={register({ required: "color is required!" })} + value={color} + onChange={() => null} + w="200px" + ></Input> + </Stack> + <GithubPicker onChangeComplete={handleChangeColorComplete} /> + </> + )} <FormErrorMessage color="unsafe.400" pl="1"> {errors?.color && errors?.color.message} </FormErrorMessage> From 5a7b261b006373120a7f297994c5441401f0376f Mon Sep 17 00:00:00 2001 From: Andrey Dolgolev <andrey@simiotics.com> Date: Tue, 7 Sep 2021 18:56:12 +0300 Subject: [PATCH 04/14] Update subscriptions cli. --- backend/moonstream/admin/subscription_types.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/backend/moonstream/admin/subscription_types.py b/backend/moonstream/admin/subscription_types.py index 0c668910..6f97c21e 100644 --- a/backend/moonstream/admin/subscription_types.py +++ b/backend/moonstream/admin/subscription_types.py @@ -20,7 +20,7 @@ CANONICAL_SUBSCRIPTION_TYPES = { "ethereum_blockchain": SubscriptionTypeResourceData( id="ethereum_blockchain", name="Ethereum transactions", - choices=["input:Address", "tag:nfts"], + choices=["input:address", "tag:nfts"], 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, @@ -42,7 +42,7 @@ CANONICAL_SUBSCRIPTION_TYPES = { id="ethereum_txpool", name="Ethereum transaction pool", description="Transactions that have been submitted into the Ethereum transaction pool but not necessarily mined yet", - choices=["input:Address", "tag:nfts"], + choices=["input:address", "tag:nfts"], icon_url="https://s3.amazonaws.com/static.simiotics.com/moonstream/assets/ethereum/eth-diamond-rainbow.png", stripe_product_id=None, stripe_price_id=None, @@ -274,19 +274,15 @@ def update_subscription_type( # 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. - new_resource = bc.create_resource( - token=MOONSTREAM_ADMIN_ACCESS_TOKEN, - application_id=MOONSTREAM_APPLICATION_ID, - resource_data=updated_resource_data, - timeout=BUGOUT_REQUEST_TIMEOUT_SECONDS, - ) try: - bc.delete_resource( + 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)}" From 6105a4402f98b2530460401b3a2a60255be87370 Mon Sep 17 00:00:00 2001 From: Andrey Dolgolev <andrey@simiotics.com> Date: Tue, 7 Sep 2021 19:42:31 +0300 Subject: [PATCH 05/14] Update ensure canonical. --- backend/moonstream/admin/subscription_types.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/backend/moonstream/admin/subscription_types.py b/backend/moonstream/admin/subscription_types.py index 6f97c21e..51e9976a 100644 --- a/backend/moonstream/admin/subscription_types.py +++ b/backend/moonstream/admin/subscription_types.py @@ -76,7 +76,7 @@ def create_subscription_type( id: str, name: str, description: str, - choices: List[str], + choices: Optional[List[str]], icon_url: str, stripe_product_id: Optional[str] = None, stripe_price_id: Optional[str] = None, @@ -115,6 +115,7 @@ def create_subscription_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, @@ -377,6 +378,19 @@ def ensure_canonical_subscription_types() -> BugoutResources: 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()) From 42f75b263f5bcba323a238e205b492ed7b410358 Mon Sep 17 00:00:00 2001 From: Tim Pechersky <tim.pechersky@gmail.com> Date: Tue, 7 Sep 2021 22:53:48 +0200 Subject: [PATCH 06/14] layout improvements --- frontend/pages/welcome.js | 21 +- frontend/src/Theme/Tooltip/index.js | 19 ++ .../src/components/NewModalSubscripton.js | 182 ------------------ frontend/src/components/NewSubscription.js | 174 +++++++++-------- frontend/src/components/RadioCard.js | 74 ++++--- .../stream-cards/EthereumWhalewatch.js | 2 +- 6 files changed, 171 insertions(+), 301 deletions(-) delete mode 100644 frontend/src/components/NewModalSubscripton.js diff --git a/frontend/pages/welcome.js b/frontend/pages/welcome.js index f5b8c643..d861a770 100644 --- a/frontend/pages/welcome.js +++ b/frontend/pages/welcome.js @@ -23,6 +23,7 @@ import { AccordionButton, AccordionPanel, AccordionIcon, + Divider, } from "@chakra-ui/react"; import StepProgress from "../src/components/StepProgress"; import { ArrowLeftIcon, ArrowRightIcon } from "@chakra-ui/icons"; @@ -95,7 +96,7 @@ const Welcome = () => { <Fade in> <Stack spacing={4}> <Stack - px={12} + px={[0, 12, null]} // mt={24} bgColor="gray.50" borderRadius="xl" @@ -119,7 +120,7 @@ const Welcome = () => { </Text> </Stack> <Stack - px={12} + px={[0, 12, null]} // mt={24} bgColor="gray.50" borderRadius="xl" @@ -176,7 +177,7 @@ const Welcome = () => { </Accordion> </Stack> <Stack - px={12} + px={[0, 12, null]} // mt={24} bgColor="gray.50" borderRadius="xl" @@ -298,7 +299,7 @@ const Welcome = () => { <Fade in> <Stack px="7%"> <Stack - px={12} + px={[0, 12, null]} // mt={24} bgColor="gray.50" borderRadius="xl" @@ -345,14 +346,18 @@ const Welcome = () => { )} <SubscriptionsList /> {showSubscriptionForm && ( - <> - <Heading pt={12}>{`Let's add new subscription!`}</Heading> + <Flex direction="column" pt={6}> + <Divider bgColor="gray.500" borderWidth="2px" /> + <Heading + size="md" + pt={2} + >{`Let's add new subscription!`}</Heading> <NewSubscription isFreeOption={false} onClose={SubscriptonCreatedCallback} /> - </> + </Flex> )} {!showSubscriptionForm && ( <Button @@ -370,7 +375,7 @@ const Welcome = () => { <Fade in> <Stack> <Stack - px={12} + px={[0, 12, null]} // mt={24} bgColor="gray.50" borderRadius="xl" diff --git a/frontend/src/Theme/Tooltip/index.js b/frontend/src/Theme/Tooltip/index.js index 94c8b27a..1095da35 100644 --- a/frontend/src/Theme/Tooltip/index.js +++ b/frontend/src/Theme/Tooltip/index.js @@ -18,6 +18,24 @@ const baseStyle = (props) => { }; }; +const variantSuggestion = (props) => { + const bg = mode("primary.700", "primary.300")(props); + return { + "--tooltip-bg": `colors.${bg}`, + px: "8px", + py: "2px", + bg: "var(--tooltip-bg)", + "--popper-arrow-bg": "var(--tooltip-bg)", + color: mode("whiteAlpha.900", "gray.900")(props), + borderRadius: "md", + fontWeight: "medium", + fontSize: "sm", + boxShadow: "md", + maxW: "320px", + zIndex: "tooltip", + }; +}; + const variantOnboarding = (props) => { const bg = mode("secondary.700", "secondary.300")(props); return { @@ -38,6 +56,7 @@ const variantOnboarding = (props) => { const variants = { onboarding: variantOnboarding, + suggestion: variantSuggestion, }; export default { diff --git a/frontend/src/components/NewModalSubscripton.js b/frontend/src/components/NewModalSubscripton.js deleted file mode 100644 index 9fb40a2b..00000000 --- a/frontend/src/components/NewModalSubscripton.js +++ /dev/null @@ -1,182 +0,0 @@ -import React, { useState, useEffect } from "react"; -import { useSubscriptions } from "../core/hooks"; -import { - Input, - Stack, - Text, - HStack, - useRadioGroup, - FormControl, - FormErrorMessage, - ModalBody, - ModalCloseButton, - ModalHeader, - Button, - ModalFooter, - Spinner, - IconButton, -} from "@chakra-ui/react"; -import RadioCard from "./RadioCard"; -import { useForm } from "react-hook-form"; -import { GithubPicker } from "react-color"; -import { BiRefresh } from "react-icons/bi"; -import { makeColor } from "../core/utils/makeColor"; -const NewSubscription = ({ - isFreeOption, - onClose, - initialAddress, - initialType, -}) => { - const [color, setColor] = useState(makeColor()); - const { typesCache, createSubscription } = useSubscriptions(); - const { handleSubmit, errors, register } = useForm({}); - const [radioState, setRadioState] = useState( - initialType ?? "ethereum_blockchain" - ); - let { getRootProps, getRadioProps } = useRadioGroup({ - name: "type", - defaultValue: radioState, - onChange: setRadioState, - }); - - const group = getRootProps(); - - useEffect(() => { - if (createSubscription.isSuccess) { - onClose(); - } - }, [createSubscription.isSuccess, onClose]); - - if (typesCache.isLoading) return <Spinner />; - - const createSubscriptionWrap = (props) => { - createSubscription.mutate({ - ...props, - color: color, - type: isFreeOption ? "free" : radioState, - }); - }; - - const handleChangeColorComplete = (color) => { - setColor(color.hex); - }; - - return ( - <form onSubmit={handleSubmit(createSubscriptionWrap)}> - <ModalHeader>Subscribe to a new address</ModalHeader> - <ModalCloseButton /> - <ModalBody> - <FormControl isInvalid={errors.label}> - <Input - my={2} - type="text" - autoComplete="off" - placeholder="Enter label" - name="label" - ref={register({ required: "label is required!" })} - ></Input> - <FormErrorMessage color="unsafe.400" pl="1"> - {errors.label && errors.label.message} - </FormErrorMessage> - </FormControl> - <FormControl isInvalid={errors.address}> - <Input - type="text" - autoComplete="off" - my={2} - placeholder="Enter address" - defaultValue={initialAddress ?? undefined} - isReadOnly={!!initialAddress} - name="address" - ref={register({ required: "address is required!" })} - ></Input> - <FormErrorMessage color="unsafe.400" pl="1"> - {errors.address && errors.address.message} - </FormErrorMessage> - </FormControl> - <Stack my={16} direction="column"> - <Text fontWeight="600"> - {isFreeOption - ? `Free subscription is only availible Ethereum blockchain source` - : `On which source?`} - </Text> - - <FormControl isInvalid={errors.subscription_type}> - <HStack {...group} alignItems="stretch"> - {typesCache.data.map((type) => { - const radio = getRadioProps({ - value: type.id, - isDisabled: - (initialAddress && initialType) || - !type.active || - (isFreeOption && type.id !== "ethereum_blockchain"), - }); - return ( - <RadioCard key={`subscription_type_${type.id}`} {...radio}> - {type.name} - </RadioCard> - ); - })} - </HStack> - <Input - type="hidden" - placeholder="subscription_type" - name="subscription_type" - ref={register({ required: "select type" })} - value={radioState} - onChange={() => null} - ></Input> - <FormErrorMessage color="unsafe.400" pl="1"> - {errors.subscription_type && errors.subscription_type.message} - </FormErrorMessage> - </FormControl> - </Stack> - <FormControl isInvalid={errors.color}> - <Stack direction="row" pb={2}> - <Text fontWeight="600" alignSelf="center"> - Label color - </Text>{" "} - <IconButton - size="md" - color={"white.100"} - _hover={{ bgColor: { color } }} - bgColor={color} - variant="outline" - onClick={() => setColor(makeColor())} - icon={<BiRefresh />} - /> - <Input - type="input" - placeholder="color" - name="color" - ref={register({ required: "color is required!" })} - value={color} - onChange={() => null} - w="200px" - ></Input> - </Stack> - - <GithubPicker onChangeComplete={handleChangeColorComplete} /> - - <FormErrorMessage color="unsafe.400" pl="1"> - {errors.color && errors.color.message} - </FormErrorMessage> - </FormControl> - </ModalBody> - <ModalFooter> - <Button - type="submit" - colorScheme="suggested" - isLoading={createSubscription.isLoading} - > - Confirm - </Button> - <Button colorScheme="gray" onClick={onClose}> - Cancel - </Button> - </ModalFooter> - </form> - ); -}; - -export default NewSubscription; diff --git a/frontend/src/components/NewSubscription.js b/frontend/src/components/NewSubscription.js index fa5efd72..259f8319 100644 --- a/frontend/src/components/NewSubscription.js +++ b/frontend/src/components/NewSubscription.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useCallback, Fragment } from "react"; +import React, { useState, useEffect, useCallback } from "react"; import { useSubscriptions } from "../core/hooks"; import { Input, @@ -12,7 +12,6 @@ import { Spinner, IconButton, ButtonGroup, - Spacer, Flex, } from "@chakra-ui/react"; import RadioCard from "./RadioCard"; @@ -33,7 +32,7 @@ const _NewSubscription = ({ const [color, setColor] = useState(makeColor()); const { handleSubmit, errors, register } = useForm({}); const { typesCache, createSubscription } = useSubscriptions(); - // const { handleSubmit, errors, register } = useForm({}); + const [radioState, setRadioState] = useState( initialType ?? "ethereum_blockchain" ); @@ -46,31 +45,19 @@ const _NewSubscription = ({ const [subscriptionAdressFormatRadio, setsubscriptionAdressFormatRadio] = useState("input:address"); - let { getRootProps, getRadioProps } = useRadioGroup({ + let { getRadioProps } = useRadioGroup({ name: "type", defaultValue: radioState, onChange: setRadioState, }); - let { - getRootProps: getRootPropsSubscription, - getRadioProps: getRadioPropsSubscription, - } = useRadioGroup({ + let { getRadioProps: getRadioPropsSubscription } = useRadioGroup({ name: "subscription", defaultValue: subscriptionAdressFormatRadio, onChange: setsubscriptionAdressFormatRadio, }); - console.log( - useRadioGroup({ - name: "subscription1", - onChange: setsubscriptionAdressFormatRadio, - }) - ); - const group = getRootProps(); - - const subscriptionAddressTypeGroup = getRootPropsSubscription(); useEffect(() => { if (setIsLoading) { @@ -101,7 +88,13 @@ const _NewSubscription = ({ type: isFreeOption ? "ethereum_blockchain" : radioState, }); }, - [createSubscription, isFreeOption, color, radioState] + [ + createSubscription, + isFreeOption, + color, + radioState, + subscriptionAdressFormatRadio, + ] ); if (typesCache.isLoading) return <Spinner />; @@ -123,8 +116,16 @@ const _NewSubscription = ({ return ( <form onSubmit={handleSubmit(createSubscriptionWrapper)}> <Stack my={4} direction="column"> - <FormControl isInvalid={errors?.subscription_type}> - <HStack {...group} alignItems="stretch"> + <Stack spacing={1} w="100%"> + <Text fontWeight="600">Source:</Text> + {/* position must be relative otherwise radio boxes add strange spacing on selection */} + <Stack + spacing={1} + w="100%" + direction="row" + flexWrap="wrap" + position="relative" + > {typesCache.data.map((type) => { const radio = getRadioProps({ value: type.id, @@ -135,72 +136,88 @@ const _NewSubscription = ({ }); return ( - <RadioCard key={`subscription_type_${type.id}`} {...radio}> - {type.name} + <RadioCard + px="8px" + py="4px" + mt="2px" + w="190px" + {...radio} + key={`subscription_type_${type.id}`} + label={type.description} + iconURL={type.icon_url} + > + {type.name.slice(9, type.name.length)} </RadioCard> ); })} - </HStack> - <Stack my={4} direction="column"> - <FormControl isInvalid={errors?.subscription_type}> - <HStack {...subscriptionAddressTypeGroup} alignItems="stretch"> - {search(radioState, typesCache.data).choices.map( - (addition_selects) => { - console.log(typeof addition_selects); - const radio = getRadioPropsSubscription({ - value: addition_selects, - isDisabled: addition_selects.startsWith("tag"), - }); - return ( - <RadioCard - key={`subscription_tags_${addition_selects}`} - {...radio} - > - {mapper[addition_selects]} - </RadioCard> - ); - } - )} - </HStack> - </FormControl> </Stack> + </Stack> + <Flex direction="row" w="100%" flexWrap="wrap" pt={4}> + {/* position must be relative otherwise radio boxes add strange spacing on selection */} + <HStack flexGrow={0} flexBasis="140px" position="relative"> + {search(radioState, typesCache.data).choices.length > 0 && ( + <Text fontWeight="600">Type:</Text> + )} + {search(radioState, typesCache.data).choices.map( + (addition_selects) => { + const radio = getRadioPropsSubscription({ + value: addition_selects, + isDisabled: addition_selects.startsWith("tag"), + }); + return ( + <RadioCard + px="4px" + py="2px" + key={`subscription_tags_${addition_selects}`} + {...radio} + > + {mapper[addition_selects]} + </RadioCard> + ); + } + )} + </HStack> {subscriptionAdressFormatRadio.startsWith("input") && radioState != "ethereum_whalewatch" && ( - <FormControl isInvalid={errors?.address}> - <Input - type="text" - autoComplete="off" - my={2} - placeholder="Address to subscribe to" - name="address" - ref={register({ required: "address is required!" })} - ></Input> - <FormErrorMessage color="unsafe.400" pl="1"> - {errors?.address && errors?.address.message} - </FormErrorMessage> - </FormControl> + <Flex flexBasis="240px" flexGrow={1}> + <FormControl isInvalid={errors?.address}> + <Input + type="text" + autoComplete="off" + my={2} + placeholder="Address to subscribe to" + name="address" + ref={register({ required: "address is required!" })} + ></Input> + <FormErrorMessage color="unsafe.400" pl="1"> + {errors?.address && errors?.address.message} + </FormErrorMessage> + </FormControl> + </Flex> )} - <Input - type="hidden" - placeholder="subscription_type" - name="subscription_type" - ref={register({ required: "select type" })} - value={radioState} - onChange={() => null} - ></Input> - <FormErrorMessage color="unsafe.400" pl="1"> - {errors?.subscription_type && errors?.subscription_type.message} - </FormErrorMessage> - </FormControl> + </Flex> + <Input + type="hidden" + placeholder="subscription_type" + name="subscription_type" + ref={register({ required: "select type" })} + value={radioState} + onChange={() => null} + ></Input> </Stack> <FormControl isInvalid={errors?.color}> {!isModal ? ( - <Flex direction="row" pb={2} flexWrap="wrap"> - <Stack pt={2} direction="row" h="min-content"> - <Text fontWeight="600" alignSelf="center"> - Label color - </Text>{" "} + <Flex direction="row" pb={2} flexWrap="wrap" alignItems="baseline"> + <Text fontWeight="600" alignSelf="center"> + Label color + </Text>{" "} + <Stack + // pt={2} + direction={["row", "row", null]} + h="min-content" + alignSelf="center" + > <IconButton size="md" // colorScheme="primary" @@ -221,8 +238,9 @@ const _NewSubscription = ({ w="200px" ></Input> </Stack> - <Flex p={2}> + <Flex p={2} flexBasis="120px" flexGrow={1} alignSelf="center"> <CirclePicker + width="100%" onChangeComplete={handleChangeColorComplete} circleSpacing={1} circleSize={24} @@ -263,7 +281,7 @@ const _NewSubscription = ({ </FormErrorMessage> </FormControl> - <ButtonGroup direction="row" justifyContent="space-evenly"> + <ButtonGroup direction="row" justifyContent="flex-end" w="100%"> <Button type="submit" colorScheme="suggested" @@ -271,7 +289,7 @@ const _NewSubscription = ({ > Confirm </Button> - <Spacer /> + <Button colorScheme="gray" onClick={onClose}> Cancel </Button> diff --git a/frontend/src/components/RadioCard.js b/frontend/src/components/RadioCard.js index c6cc46cb..17882fdd 100644 --- a/frontend/src/components/RadioCard.js +++ b/frontend/src/components/RadioCard.js @@ -1,5 +1,5 @@ import React from "react"; -import { useRadio, Box, Flex } from "@chakra-ui/react"; +import { useRadio, Box, Flex, Tooltip, Image } from "@chakra-ui/react"; const RadioCard = (props) => { const { getInputProps, getCheckboxProps } = useRadio(props); @@ -8,38 +8,48 @@ const RadioCard = (props) => { const checkbox = getCheckboxProps(); return ( - <Flex as="label" h="fill-availible"> - <input {...input} /> - <Box - justifyContent="left" - alignContent="center" - {...checkbox} - cursor="pointer" - borderWidth="1px" - borderRadius="md" - boxShadow="md" - _disabled={{ - bg: "gray.300", - color: "gray.900", - borderColor: "gray.300", - }} - _checked={{ - bg: "secondary.900", - color: "white", - borderColor: "secondary.900", - }} - _focus={{ - boxShadow: "outline", - }} - px={5} - py={3} - fontWeight="600" - > - {props.children} - </Box> - </Flex> + <Tooltip + hidden={props.label ? false : true} + label={props.label} + variant="suggestion" + openDelay={500} + > + <Flex as="label" h="fill-availible"> + <input {...input} /> + <Box + alignContent="center" + {...checkbox} + cursor="pointer" + borderWidth="1px" + borderRadius="lg" + boxShadow="md" + _disabled={{ + bg: "gray.300", + color: "gray.900", + borderColor: "gray.300", + }} + _checked={{ + // bg: "secondary.900", + + color: "secondary.900", + borderColor: "secondary.900", + borderWidth: "4px", + }} + justifyContent="center" + px={props.px} + mt={props.mt} + py={props.py} + w={props.w} + fontWeight="600" + > + {props.iconURL && ( + <Image display="inline-block" w="16px" src={props.iconURL} /> + )}{" "} + {props.children} + </Box> + </Flex> + </Tooltip> ); }; -// const RadioCard = chakra(RadioCard_); export default RadioCard; diff --git a/frontend/src/components/stream-cards/EthereumWhalewatch.js b/frontend/src/components/stream-cards/EthereumWhalewatch.js index c412a8d0..b1f5cb31 100644 --- a/frontend/src/components/stream-cards/EthereumWhalewatch.js +++ b/frontend/src/components/stream-cards/EthereumWhalewatch.js @@ -21,7 +21,7 @@ import { useToast } from "../../core/hooks"; import { useSubscriptions } from "../../core/hooks"; import moment from "moment"; import { AiOutlineMonitor } from "react-icons/ai"; -import NewSubscription from "../NewModalSubscripton"; +import NewSubscription from "../NewSubscription"; const EthereumWhalewatchCard_ = ({ entry, From 436f87020f82b12cd6f659bd867f52ebbe8bb254 Mon Sep 17 00:00:00 2001 From: Andrey Dolgolev <andrey@simiotics.com> Date: Wed, 8 Sep 2021 13:20:19 +0300 Subject: [PATCH 07/14] Add sorting for subscriptions. --- frontend/src/components/NewSubscription.js | 52 +++++++++++----------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/frontend/src/components/NewSubscription.js b/frontend/src/components/NewSubscription.js index 259f8319..e921e773 100644 --- a/frontend/src/components/NewSubscription.js +++ b/frontend/src/components/NewSubscription.js @@ -57,8 +57,6 @@ const _NewSubscription = ({ onChange: setsubscriptionAdressFormatRadio, }); - - useEffect(() => { if (setIsLoading) { setIsLoading(createSubscription.isLoading); @@ -126,30 +124,34 @@ const _NewSubscription = ({ flexWrap="wrap" position="relative" > - {typesCache.data.map((type) => { - const radio = getRadioProps({ - value: type.id, - isDisabled: - (initialAddress && initialType) || - !type.active || - (isFreeOption && type.id !== "ethereum_blockchain"), - }); + {typesCache.data + .sort((a, b) => + a?.name > b?.name ? 1 : b?.name > a?.name ? -1 : 0 + ) + .map((type) => { + const radio = getRadioProps({ + value: type.id, + isDisabled: + (initialAddress && initialType) || + !type.active || + (isFreeOption && type.id !== "ethereum_blockchain"), + }); - return ( - <RadioCard - px="8px" - py="4px" - mt="2px" - w="190px" - {...radio} - key={`subscription_type_${type.id}`} - label={type.description} - iconURL={type.icon_url} - > - {type.name.slice(9, type.name.length)} - </RadioCard> - ); - })} + return ( + <RadioCard + px="8px" + py="4px" + mt="2px" + w="190px" + {...radio} + key={`subscription_type_${type.id}`} + label={type.description} + iconURL={type.icon_url} + > + {type.name.slice(9, type.name.length)} + </RadioCard> + ); + })} </Stack> </Stack> From 134b785c9ecb8cd0fa150c1a8c742d0136d8924b Mon Sep 17 00:00:00 2001 From: Andrey Dolgolev <andrey@simiotics.com> Date: Wed, 8 Sep 2021 15:14:52 +0300 Subject: [PATCH 08/14] Black formating. --- backend/moonstream/actions.py | 7 +++++-- backend/moonstream/routes/users.py | 3 ++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/backend/moonstream/actions.py b/backend/moonstream/actions.py index c80c623f..1c786e9b 100644 --- a/backend/moonstream/actions.py +++ b/backend/moonstream/actions.py @@ -1,6 +1,5 @@ import json import logging - from typing import Optional, Dict, Any from enum import Enum import uuid @@ -158,7 +157,11 @@ def create_onboarding_resource( token: uuid.UUID, resource_data: Dict[str, Any] = { "type": data.USER_ONBOARDING_STATE, - "steps": {"welcome": 0, "subscriptions": 0, "stream": 0,}, + "steps": { + "welcome": 0, + "subscriptions": 0, + "stream": 0, + }, "is_complete": False, }, ) -> BugoutResource: diff --git a/backend/moonstream/routes/users.py b/backend/moonstream/routes/users.py index 58b44a2f..19cefc8f 100644 --- a/backend/moonstream/routes/users.py +++ b/backend/moonstream/routes/users.py @@ -180,7 +180,8 @@ async def logout_handler(request: Request) -> uuid.UUID: @app.post("/onboarding", tags=["users"], response_model=data.OnboardingState) async def set_onboarding_state( - request: Request, onboarding_data: data.OnboardingState = Body(...), + request: Request, + onboarding_data: data.OnboardingState = Body(...), ) -> data.OnboardingState: token = request.state.token From b9d9584bfeea2732af268fac133d5e4db04e14ee Mon Sep 17 00:00:00 2001 From: Andrey Dolgolev <andrey@simiotics.com> Date: Wed, 8 Sep 2021 18:06:12 +0300 Subject: [PATCH 09/14] Add fixes. --- backend/moonstream/admin/subscription_types.py | 2 +- backend/moonstream/data.py | 2 +- backend/moonstream/providers/bugout.py | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/backend/moonstream/admin/subscription_types.py b/backend/moonstream/admin/subscription_types.py index 51e9976a..f07b2d95 100644 --- a/backend/moonstream/admin/subscription_types.py +++ b/backend/moonstream/admin/subscription_types.py @@ -76,8 +76,8 @@ def create_subscription_type( id: str, name: str, description: str, - choices: Optional[List[str]], icon_url: str, + choices: Optional[List[str]] = [], stripe_product_id: Optional[str] = None, stripe_price_id: Optional[str] = None, active: bool = False, diff --git a/backend/moonstream/data.py b/backend/moonstream/data.py index df4b6537..d3c0f07b 100644 --- a/backend/moonstream/data.py +++ b/backend/moonstream/data.py @@ -12,7 +12,7 @@ class SubscriptionTypeResourceData(BaseModel): id: str name: str description: str - choices: Optional[List[str]] + choices: List[str] = Field(default_factory=list) icon_url: str stripe_product_id: Optional[str] = None stripe_price_id: Optional[str] = None diff --git a/backend/moonstream/providers/bugout.py b/backend/moonstream/providers/bugout.py index 5d7ef530..020733be 100644 --- a/backend/moonstream/providers/bugout.py +++ b/backend/moonstream/providers/bugout.py @@ -19,7 +19,6 @@ from ..stream_queries import StreamQuery logger = logging.getLogger(__name__) logger.setLevel(logging.WARN) -allowed_tags = ["nfts"] class BugoutEventProviderError(Exception): From 64d67556cfef508020f362ab3cf2d904c6b34433 Mon Sep 17 00:00:00 2001 From: Andrey Dolgolev <andrey@simiotics.com> Date: Wed, 8 Sep 2021 18:43:34 +0300 Subject: [PATCH 10/14] Add mypy fixes. --- backend/moonstream/admin/subscription_types.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/moonstream/admin/subscription_types.py b/backend/moonstream/admin/subscription_types.py index f07b2d95..bee40e00 100644 --- a/backend/moonstream/admin/subscription_types.py +++ b/backend/moonstream/admin/subscription_types.py @@ -139,8 +139,8 @@ def cli_create_subscription_type(args: argparse.Namespace) -> None: args.id, args.name, args.description, - args.choices, args.icon, + args.choices, args.stripe_product_id, args.stripe_price_id, args.active, @@ -371,8 +371,8 @@ def ensure_canonical_subscription_types() -> BugoutResources: id, canonical_subscription_type.name, canonical_subscription_type.description, - canonical_subscription_type.choices, 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, From 6c68a21faf31b3349db5ddb3ba528c3d537de6ef Mon Sep 17 00:00:00 2001 From: Andrey Dolgolev <andrey@simiotics.com> Date: Wed, 8 Sep 2021 20:17:47 +0300 Subject: [PATCH 11/14] Add fixes in labeling staff. --- frontend/src/components/NewSubscription.js | 5 ++--- frontend/src/components/SubscriptionsList.js | 11 ++++++++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/NewSubscription.js b/frontend/src/components/NewSubscription.js index e921e773..c4bd077d 100644 --- a/frontend/src/components/NewSubscription.js +++ b/frontend/src/components/NewSubscription.js @@ -73,11 +73,11 @@ const _NewSubscription = ({ (props) => { props.label = "Address"; if ( - subscriptionAdressFormatRadio.startsWith("tags") && + subscriptionAdressFormatRadio.startsWith("tag") && radioState != "ethereum_whalewatch" ) { props.address = subscriptionAdressFormatRadio; - props.label = "tags: NFTs"; + props.label = "Tag"; } createSubscription.mutate({ @@ -165,7 +165,6 @@ const _NewSubscription = ({ (addition_selects) => { const radio = getRadioPropsSubscription({ value: addition_selects, - isDisabled: addition_selects.startsWith("tag"), }); return ( <RadioCard diff --git a/frontend/src/components/SubscriptionsList.js b/frontend/src/components/SubscriptionsList.js index 13b50dc5..d398891f 100644 --- a/frontend/src/components/SubscriptionsList.js +++ b/frontend/src/components/SubscriptionsList.js @@ -21,6 +21,11 @@ import { useSubscriptions } from "../core/hooks"; import ConfirmationRequest from "./ConfirmationRequest"; import ColorSelector from "./ColorSelector"; +const mapper = { + "tag:nfts": "NFTs", + "input:address": "Address", +}; + const SubscriptionsList = ({ emptyCTA }) => { const { subscriptionsCache, @@ -94,7 +99,11 @@ const SubscriptionsList = ({ emptyCTA }) => { </Editable> </Td> <Td mr={4} p={0}> - <CopyButton>{subscription.address}</CopyButton> + {subscription.address?.startsWith("tag") ? ( + <CopyButton>{mapper[subscription.address]}</CopyButton> + ) : ( + <CopyButton>{subscription.address}</CopyButton> + )} </Td> <Td> <ColorSelector From 3841b9104a81157c76b34a9aaf3be7c019bf5956 Mon Sep 17 00:00:00 2001 From: Andrey Dolgolev <andrey@simiotics.com> Date: Wed, 8 Sep 2021 20:41:25 +0300 Subject: [PATCH 12/14] Change tag:nfts -> ERC721. --- backend/moonstream/admin/subscription_types.py | 4 ++-- frontend/src/components/NewSubscription.js | 2 +- frontend/src/components/SubscriptionsList.js | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/backend/moonstream/admin/subscription_types.py b/backend/moonstream/admin/subscription_types.py index bee40e00..ad45f004 100644 --- a/backend/moonstream/admin/subscription_types.py +++ b/backend/moonstream/admin/subscription_types.py @@ -20,7 +20,7 @@ CANONICAL_SUBSCRIPTION_TYPES = { "ethereum_blockchain": SubscriptionTypeResourceData( id="ethereum_blockchain", name="Ethereum transactions", - choices=["input:address", "tag:nfts"], + 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, @@ -42,7 +42,7 @@ CANONICAL_SUBSCRIPTION_TYPES = { id="ethereum_txpool", name="Ethereum transaction pool", description="Transactions that have been submitted into the Ethereum transaction pool but not necessarily mined yet", - choices=["input:address", "tag:nfts"], + 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, diff --git a/frontend/src/components/NewSubscription.js b/frontend/src/components/NewSubscription.js index c4bd077d..5202445b 100644 --- a/frontend/src/components/NewSubscription.js +++ b/frontend/src/components/NewSubscription.js @@ -38,7 +38,7 @@ const _NewSubscription = ({ ); const mapper = { - "tag:nfts": "NFTs", + "tag:erc721": "NFTs", "input:address": "Address", }; diff --git a/frontend/src/components/SubscriptionsList.js b/frontend/src/components/SubscriptionsList.js index d398891f..ef1b006e 100644 --- a/frontend/src/components/SubscriptionsList.js +++ b/frontend/src/components/SubscriptionsList.js @@ -22,7 +22,7 @@ import ConfirmationRequest from "./ConfirmationRequest"; import ColorSelector from "./ColorSelector"; const mapper = { - "tag:nfts": "NFTs", + "tag:erc721": "NFTs", "input:address": "Address", }; From 1199d173c1995cde202b5d2e1edfc9c84021a5a9 Mon Sep 17 00:00:00 2001 From: Andrey Dolgolev <andrey@simiotics.com> Date: Thu, 9 Sep 2021 14:08:43 +0300 Subject: [PATCH 13/14] Disable NFTs. --- frontend/src/components/NewSubscription.js | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/components/NewSubscription.js b/frontend/src/components/NewSubscription.js index 5202445b..7204e775 100644 --- a/frontend/src/components/NewSubscription.js +++ b/frontend/src/components/NewSubscription.js @@ -165,6 +165,7 @@ const _NewSubscription = ({ (addition_selects) => { const radio = getRadioPropsSubscription({ value: addition_selects, + isDisabled: addition_selects.startsWith("tag"), }); return ( <RadioCard From 69226ebd32b836c1cd279e393f52f872ca97e04b Mon Sep 17 00:00:00 2001 From: Andrey Dolgolev <andrey@simiotics.com> Date: Thu, 9 Sep 2021 15:05:38 +0300 Subject: [PATCH 14/14] Remove optional. --- backend/moonstream/admin/subscription_types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/moonstream/admin/subscription_types.py b/backend/moonstream/admin/subscription_types.py index ad45f004..e666aebf 100644 --- a/backend/moonstream/admin/subscription_types.py +++ b/backend/moonstream/admin/subscription_types.py @@ -77,7 +77,7 @@ def create_subscription_type( name: str, description: str, icon_url: str, - choices: Optional[List[str]] = [], + choices: List[str] = [], stripe_product_id: Optional[str] = None, stripe_price_id: Optional[str] = None, active: bool = False,