Merge branch 'main' into whale-watch

pull/105/head
Neeraj Kashyap 2021-08-18 23:06:17 -07:00
commit 828c6b1e61
36 zmienionych plików z 1522 dodań i 252 usunięć

Wyświetl plik

@ -1,7 +1,9 @@
name: Lint Moonstream backend
on:
push:
pull_request:
branches:
- "main"
paths:
- "backend/**"

Wyświetl plik

@ -1,7 +1,9 @@
name: Lint Moonstream crawlers
on:
push:
pull_request:
branches:
- "main"
paths:
- "crawlers/**"

Wyświetl plik

@ -1,7 +1,9 @@
name: Lint Moonstream db
on:
push:
pull_request:
branches:
- "main"
paths:
- "db/**"

Wyświetl plik

@ -1,9 +1,6 @@
name: Build Moonstream frontend
on:
push:
paths:
- "frontend/**"
pull_request:
branches:
- "main"

Wyświetl plik

@ -160,3 +160,17 @@ class TxinfoEthereumBlockchainResponse(BaseModel):
smart_contract_address: Optional[str] = None
abi: Optional[ContractABI] = None
errors: List[str] = Field(default_factory=list)
class AddressLabelResponse(BaseModel):
label: str
label_data: Optional[Dict[str, Any]] = None
class AddressLabelsResponse(BaseModel):
address: str
labels: List[AddressLabelResponse] = Field(default_factory=list)
class AddressListLabelsResponse(BaseModel):
addresses: List[AddressLabelsResponse] = Field(default_factory=list)

Wyświetl plik

@ -6,33 +6,26 @@ transactions, etc.) with side information and return objects that are better sui
end users.
"""
import logging
from typing import Any, Dict
from typing import Dict, Optional
from fastapi import (
FastAPI,
Depends,
)
from fastapi import FastAPI, Depends, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware
from moonstreamdb.db import yield_db_session
from moonstreamdb.models import EthereumAddress
from sqlalchemy.orm import Session
from ..abi_decoder import decode_abi
from ..data import TxinfoEthereumBlockchainRequest, TxinfoEthereumBlockchainResponse
from .. import actions
from .. import data
from ..middleware import BroodAuthMiddleware
from ..settings import (
DOCS_TARGET_PATH,
ORIGINS,
DOCS_PATHS,
bugout_client as bc,
)
from ..settings import DOCS_TARGET_PATH, ORIGINS, DOCS_PATHS
from ..version import MOONSTREAM_VERSION
logger = logging.getLogger(__name__)
tags_metadata = [
{"name": "users", "description": "Operations with users."},
{"name": "tokens", "description": "Operations with user tokens."},
{"name": "txinfo", "description": "Ethereum transactions info."},
{"name": "address info", "description": "Addresses public information."},
]
app = FastAPI(
@ -63,13 +56,13 @@ app.add_middleware(BroodAuthMiddleware, whitelist=whitelist_paths)
@app.post(
"/ethereum_blockchain",
tags=["txinfo"],
response_model=TxinfoEthereumBlockchainResponse,
response_model=data.TxinfoEthereumBlockchainResponse,
)
async def txinfo_ethereum_blockchain_handler(
txinfo_request: TxinfoEthereumBlockchainRequest,
txinfo_request: data.TxinfoEthereumBlockchainRequest,
db_session: Session = Depends(yield_db_session),
) -> TxinfoEthereumBlockchainResponse:
response = TxinfoEthereumBlockchainResponse(tx=txinfo_request.tx)
) -> data.TxinfoEthereumBlockchainResponse:
response = data.TxinfoEthereumBlockchainResponse(tx=txinfo_request.tx)
if txinfo_request.tx.input is not None:
try:
response.abi = decode_abi(txinfo_request.tx.input, db_session)
@ -92,3 +85,31 @@ async def txinfo_ethereum_blockchain_handler(
response.is_smart_contract_call = True
return response
@app.get(
"/addresses", tags=["address info"], response_model=data.AddressListLabelsResponse
)
async def addresses_labels_handler(
addresses: Optional[str] = Query(None),
start: Optional[int] = Query(0),
limit: Optional[int] = Query(100),
db_session: Session = Depends(yield_db_session),
) -> data.AddressListLabelsResponse:
"""
Fetch labels with additional public information
about known addresses.
"""
if limit > 100:
raise HTTPException(
status_code=406, detail="The limit cannot exceed 100 addresses"
)
try:
addresses = 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)
return addresses

Wyświetl plik

@ -0,0 +1,192 @@
import argparse
import boto3
import csv
import codecs
import json
import os
from sqlalchemy.orm import Session
from moonstreamdb.db import yield_db_session_ctx
import sys
import time
from datetime import datetime
from typing import Any, List, Optional, Tuple, Dict
from dataclasses import dataclass
from sqlalchemy.sql.expression import label, text
from .version import MOONCRAWL_VERSION
from moonstreamdb.models import EthereumAddress, EthereumLabel
import requests
from .settings import MOONSTREAM_ETHERSCAN_TOKEN
if MOONSTREAM_ETHERSCAN_TOKEN is None:
raise Exception("MOONSTREAM_ETHERSCAN_TOKEN environment variable must be set")
BASE_API_URL = "https://api.etherscan.io/api?module=contract&action=getsourcecode"
ETHERSCAN_SMARTCONTRACTS_LABEL_NAME = "etherscan_smartcontract"
bucket = os.environ.get("AWS_S3_SMARTCONTRACT_BUCKET")
if bucket is None:
raise ValueError("AWS_S3_SMARTCONTRACT_BUCKET must be set")
@dataclass
class VerifiedSmartContract:
name: str
address: str
tx_hash: str
def push_to_bucket(contract_data: Dict[str, Any], contract_file: str):
result_bytes = json.dumps(contract_data).encode("utf-8")
result_key = contract_file
s3 = boto3.client("s3")
s3.put_object(
Body=result_bytes,
Bucket=bucket,
Key=result_key,
ContentType="application/json",
Metadata={"source": "etherscan", "crawler_version": MOONCRAWL_VERSION},
)
def get_address_id(db_session: Session, contract_address: str) -> int:
"""
Searches for given address in EthereumAddress table,
If doesn't find one, creates new.
Returns id of address
"""
query = db_session.query(EthereumAddress.id).filter(
EthereumAddress.address == contract_address
)
id = query.one_or_none()
if id is not None:
return id[0]
latest_address_id = (
db_session.query(EthereumAddress.id).order_by(text("id desc")).limit(1).one()
)[0]
id = latest_address_id + 1
smart_contract = EthereumAddress(
id=id,
address=contract_address,
)
try:
db_session.add(smart_contract)
db_session.commit()
except:
db_session.rollback()
return id
def crawl_step(db_session: Session, contract: VerifiedSmartContract, crawl_url: str):
attempt = 0
current_interval = 2
success = False
response: Optional[requests.Response] = None
while (not success) and attempt < 3:
attempt += 1
try:
response = requests.get(crawl_url)
response.raise_for_status()
success = True
except:
current_interval *= 2
time.sleep(current_interval)
if response is None:
print(f"Could not process URL: {crawl_url}", file=sys.stderr)
return None
page = response.json()
result = page["result"][0]
contract_info = {
"data": result,
"crawl_version": MOONCRAWL_VERSION,
"crawled_at": f"{datetime.now()}",
}
object_key = f"/etherscan/v1/{contract.address}.json"
push_to_bucket(contract_info, object_key)
eth_address_id = get_address_id(db_session, contract.address)
eth_label = EthereumLabel(
label=ETHERSCAN_SMARTCONTRACTS_LABEL_NAME,
address_id=eth_address_id,
label_data={
"object_uri": f"s3://{bucket}/{object_key}",
"name": contract.name,
"tx_hash": contract.tx_hash,
},
)
try:
db_session.add(eth_label)
db_session.commit()
except:
db_session.rollback()
def crawl(
db_session: Session,
smart_contracts: List[VerifiedSmartContract],
interval: float,
start_step=0,
):
for i in range(start_step, len(smart_contracts)):
contract = smart_contracts[i]
print(f"Crawling {i}/{len(smart_contracts)} : {contract.address}")
query_url = (
BASE_API_URL
+ f"&address={contract.address}&apikey={MOONSTREAM_ETHERSCAN_TOKEN}"
)
crawl_step(db_session, contract, query_url)
time.sleep(interval)
def load_smart_contracts() -> List[VerifiedSmartContract]:
smart_contracts: List[VerifiedSmartContract] = []
s3 = boto3.client("s3")
data = s3.get_object(Bucket=bucket, Key="util/verified-contractaddress.csv")
for row in csv.DictReader(codecs.getreader("utf-8")(data["Body"])):
smart_contracts.append(
VerifiedSmartContract(
tx_hash=row["Txhash"],
address=row["ContractAddress"],
name=row["ContractName"],
)
)
return smart_contracts
def main():
parser = argparse.ArgumentParser(
description="Crawls smart contract sources from etherscan.io"
)
parser.add_argument(
"--interval",
type=float,
default=0.2,
help="Number of seconds to wait between requests to the etherscan.io (default: 0.2)",
)
parser.add_argument(
"--offset",
type=int,
default=0,
help="Number of smart contract to skip for crawling from smart contracts .csv file",
)
args = parser.parse_args()
with yield_db_session_ctx() as db_session:
crawl(
db_session,
load_smart_contracts(),
interval=args.interval,
start_step=args.offset,
)
if __name__ == "__main__":
main()

Wyświetl plik

@ -1,12 +1,12 @@
import argparse
import json
import os
import time
import requests
from sqlalchemy import text
from moonstreamdb.db import yield_db_session_ctx
from moonstreamdb.models import EthereumAddress
from moonstreamdb.models import EthereumAddress, EthereumLabel
COINMARKETCAP_API_KEY = os.environ.get("COINMARKETCAP_API_KEY")
if COINMARKETCAP_API_KEY is None:
@ -18,7 +18,7 @@ CRAWL_ORIGINS = {
}
def identities_cmc_handler(args: argparse.Namespace) -> None:
def identities_cmc_add_handler(args: argparse.Namespace) -> None:
"""
Parse metadata for Ethereum tokens.
"""
@ -37,7 +37,11 @@ def identities_cmc_handler(args: argparse.Namespace) -> None:
limit_n = 5000
while True:
params = {"start": start_n, "limit": limit_n}
params = {
"start": start_n,
"limit": limit_n,
"listing_status": args.listing_status,
}
try:
r = requests.get(url=url, headers=headers, params=params)
r.raise_for_status()
@ -50,20 +54,36 @@ def identities_cmc_handler(args: argparse.Namespace) -> None:
break
with yield_db_session_ctx() as db_session:
for crypto in response["data"]:
if crypto["platform"] is not None:
latest_address = (
db_session.query(EthereumAddress.id)
.order_by(text("id desc"))
.limit(1)
.one()
)[0]
for coin in response["data"]:
if coin["platform"] is not None:
if (
crypto["platform"]["id"] == 1027
and crypto["platform"]["token_address"] is not None
coin["platform"]["id"] == 1027
and coin["platform"]["token_address"] is not None
):
latest_address += 1
eth_token_id = latest_address
eth_token = EthereumAddress(
address=crypto["platform"]["token_address"],
name=crypto["name"],
symbol=crypto["symbol"],
id=eth_token_id,
address=coin["platform"]["token_address"],
)
db_session.add(eth_token)
print(f"Added {crypto['name']} token")
eth_token_label = EthereumLabel(
label="coinmarketcap_token",
address_id=eth_token_id,
label_data={
"name": coin["name"],
"symbol": coin["symbol"],
"coinmarketcap_url": f'https://coinmarketcap.com/currencies/{coin["slug"]}',
},
)
db_session.add(eth_token_label)
print(f"Added {coin['name']} token")
db_session.commit()
start_n += limit_n
@ -79,21 +99,31 @@ def main():
parser_cmc = subcommands.add_parser("cmc", description="Coinmarketcap commands")
parser_cmc.set_defaults(func=lambda _: parser_cmc.print_help())
subcommands_parser_cmc = parser_cmc.add_subparsers(
description="Ethereum blocks commands"
)
parser_cmc.add_argument(
"-s",
"--sandbox",
action="store_true",
help="Target to sandbox API",
)
parser_cmc.set_defaults(func=identities_cmc_handler)
parser_label_cloud = subcommands.add_parser(
"label_cloud", description="Etherscan label cloud commands"
subcommands_parser_cmc = parser_cmc.add_subparsers(
description="Ethereum blocks commands"
)
parser_label_cloud.set_defaults(func=identities_get_handler)
parser_cmc_add = subcommands_parser_cmc.add_parser(
"add", description="Add additional information about Ethereum addresses"
)
parser_cmc_add.add_argument(
"-s",
"--listing_status",
default="active,inactive,untracked",
help="Listing status of coin, by default all of them: active,inactive,untracked",
)
parser_cmc_add.set_defaults(func=identities_cmc_add_handler)
# parser_label_cloud = subcommands.add_parser(
# "label_cloud", description="Etherscan label cloud commands"
# )
# parser_label_cloud.set_defaults(func=identities_get_handler)
args = parser.parse_args()
args.func(args)

Wyświetl plik

@ -11,3 +11,6 @@ except:
raise Exception(
f"Could not parse MOONSTREAM_CRAWL_WORKERS as int: {MOONSTREAM_CRAWL_WORKERS_RAW}"
)
MOONSTREAM_ETHERSCAN_TOKEN = os.environ.get("MOONSTREAM_ETHERSCAN_TOKEN")

Wyświetl plik

@ -2,4 +2,4 @@
Moonstream crawlers version.
"""
MOONCRAWL_VERSION = "0.0.2"
MOONCRAWL_VERSION = "0.0.3"

Wyświetl plik

@ -2,4 +2,8 @@
export MOONSTREAM_IPC_PATH=null
export MOONSTREAM_CRAWL_WORKERS=4
export MOONSTREAM_DB_URI="postgresql://<username>:<password>@<db_host>:<db_port>/<db_name>"
export MOONSTREAM_ETHERSCAN_TOKEN="TOKEN"
export AWS_S3_SMARTCONTRACT_BUCKET=""
export MOONSTREAM_HUMBUG_TOKEN="<Token for crawlers store data via Humbug>"
export COINMARKETCAP_API_KEY="<API key to parse conmarketcap>"

Wyświetl plik

@ -38,6 +38,7 @@ setup(
"requests",
"tqdm",
"web3",
"boto3",
],
extras_require={"dev": ["black", "mypy", "types-requests"]},
entry_points={
@ -45,6 +46,7 @@ setup(
"ethcrawler=mooncrawl.ethcrawler:main",
"esd=mooncrawl.esd:main",
"identity=mooncrawl.identity:main",
"etherscan=mooncrawl.etherscan:main",
]
},
)

Wyświetl plik

@ -0,0 +1,32 @@
"""Unique const for addr
Revision ID: ea8185bd24c7
Revises: 40871a7807f6
Create Date: 2021-08-18 09:41:00.512462
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'ea8185bd24c7'
down_revision = '40871a7807f6'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index('ix_ethereum_addresses_address', table_name='ethereum_addresses')
op.create_index(op.f('ix_ethereum_addresses_address'), 'ethereum_addresses', ['address'], unique=True)
op.create_unique_constraint(op.f('uq_ethereum_labels_id'), 'ethereum_labels', ['id'])
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(op.f('uq_ethereum_labels_id'), 'ethereum_labels', type_='unique')
op.drop_index(op.f('ix_ethereum_addresses_address'), table_name='ethereum_addresses')
op.create_index('ix_ethereum_addresses_address', 'ethereum_addresses', ['address'], unique=False)
# ### end Alembic commands ###

Wyświetl plik

@ -113,7 +113,7 @@ class EthereumAddress(Base): # type: ignore
nullable=True,
index=True,
)
address = Column(VARCHAR(256), nullable=False, index=True)
address = Column(VARCHAR(256), nullable=False, unique=True, index=True)
created_at = Column(
DateTime(timezone=True), server_default=utcnow(), nullable=False
)

Wyświetl plik

@ -39,7 +39,6 @@ export default function CachingApp({ Component, pageProps }) {
router.events.on("routeChangeComplete", handleStop);
router.events.on("routeChangeError", handleStop);
console.log("_app", router.asPath);
return () => {
router.events.off("routeChangeStart", handleStart);
router.events.off("routeChangeComplete", handleStop);
@ -49,8 +48,6 @@ export default function CachingApp({ Component, pageProps }) {
const getLayout =
Component.getLayout || ((page) => <DefaultLayout>{page}</DefaultLayout>);
console.log("_app loaded", router.asPath);
return (
<>
<style global jsx>{`

Wyświetl plik

@ -10,7 +10,9 @@ import {
UnorderedList,
ListItem,
} from "@chakra-ui/react";
import RouteButton from "../../src/components/RouteButton";
const Entry = () => {
console.count("render stream!");
const ui = useContext(UIContext);
useEffect(() => {
@ -49,6 +51,14 @@ const Entry = () => {
subscription screen
</ListItem>
</UnorderedList>
<RouteButton
variant="solid"
size="md"
colorScheme="suggested"
href="/welcome"
>
Learn how to use moonstream
</RouteButton>
</Stack>
</>
</Box>

Wyświetl plik

@ -15,7 +15,7 @@ import {
ModalOverlay,
ModalContent,
} from "@chakra-ui/react";
import NewSubscription from "../src/components/NewSubscription";
import NewSubscription from "../src/components/NewModalSubscripton";
import { AiOutlinePlusCircle } from "react-icons/ai";
const Subscriptions = () => {

Wyświetl plik

@ -0,0 +1,468 @@
import React, { useContext, useEffect, useRef } from "react";
import { getLayout } from "../src/layouts/AppLayout";
import UIContext from "../src/core/providers/UIProvider/context";
import {
Heading,
Text,
Button,
Stack,
ButtonGroup,
Spacer,
Radio,
RadioGroup,
UnorderedList,
ListItem,
Fade,
chakra,
useBoolean,
Flex,
IconButton,
Tooltip,
} from "@chakra-ui/react";
import StepProgress from "../src/components/StepProgress";
import { ArrowLeftIcon, ArrowRightIcon } from "@chakra-ui/icons";
import Scrollable from "../src/components/Scrollable";
import AnalyticsContext from "../src/core/providers/AnalyticsProvider/context";
import NewSubscription from "../src/components/NewSubscription";
import StreamEntry from "../src/components/StreamEntry";
import SubscriptionsList from "../src/components/SubscriptionsList";
import { useSubscriptions } from "../src/core/hooks";
import router from "next/router";
import { FaFilter } from "react-icons/fa";
const Welcome = () => {
console.count("render welcome!");
const { subscriptionsCache } = useSubscriptions();
const ui = useContext(UIContext);
const { mixpanel, isLoaded, MIXPANEL_PROPS } = useContext(AnalyticsContext);
const [profile, setProfile] = React.useState();
const [showSubscriptionForm, setShowSubscriptionForm] = useBoolean(true);
useEffect(() => {
if (typeof window !== "undefined") {
document.title = `Welcome to moonstream.to!`;
}
}, []);
const progressButtonCallback = (index) => {
ui.setOnboardingStep(index);
};
useEffect(() => {
if (profile && isLoaded) {
mixpanel.people.set({
[`${MIXPANEL_PROPS.USER_SPECIALITY}`]: profile,
});
}
}, [profile, MIXPANEL_PROPS, isLoaded, mixpanel]);
const SubscriptonCreatedCallback = () => {
setShowSubscriptionForm.off();
};
const scrollRef = useRef();
const handleNextClick = () => {
if (ui.onboardingStep < ui.onboardingSteps.length - 1) {
ui.setOnboardingStep(ui.onboardingStep + 1);
scrollRef?.current?.scrollIntoView();
} else {
ui.setisOnboardingComplete(true);
router.push("/stream");
}
};
return (
<Scrollable>
<Stack px="7%" pt={4} w="100%" spacing={4} ref={scrollRef}>
<StepProgress
numSteps={ui.onboardingSteps.length}
currentStep={ui.onboardingStep}
colorScheme="primary"
buttonCallback={progressButtonCallback}
buttonTitles={[
"Moonstream basics",
"Setup subscriptions",
"How to read stream",
]}
style="arrows"
/>
{ui.onboardingStep === 0 && (
<Fade in>
<Stack spacing={4}>
<Stack
px={12}
// mt={24}
bgColor="gray.50"
borderRadius="xl"
boxShadow="xl"
py={4}
>
<Heading as="h4" size="md">
Greetings traveller!
</Heading>
<Text fontWeight="semibold" pl={2}>
{" "}
We are very excited to see you onboard!
</Text>
<Text fontWeight="semibold" pl={2}>
Moonstream is a product which helps anyone participate in
decentralized finance. From the most sophisticated flash
arbitrageurs to people looking for yield from currency that
would otherwise lie dormant in their exchange accounts.
</Text>
<Text fontWeight="semibold" pl={2}>
Moonstream is ment to give you critical insights needed to
succeed in your crypto quest!
</Text>
</Stack>
<Stack
px={12}
// mt={24}
bgColor="gray.50"
borderRadius="xl"
boxShadow="xl"
py={4}
>
<Heading as="h4" size="md">
How does Moonstream work?
</Heading>
<chakra.span fontWeight="semibold" pl={2}>
<Text fontWeight="bold" display="inline">
We run nodes
</Text>{" "}
- Now get most precise and accurate data you can just query
our database. You {`don't`} need to maintain your node, and
still have data that miners have access to!
</chakra.span>
<chakra.span fontWeight="semibold" pl={2}>
<Text fontWeight="bold" display="inline">
We crawl data
</Text>{" "}
We analyze millions of transactions, data, smart contract code
to link all them together
</chakra.span>
<chakra.span fontWeight="semibold" pl={2}>
<Text fontWeight="bold" display="inline">
We provide data
</Text>{" "}
We allow you to fetch data trough the website or trough API
</chakra.span>
<chakra.span fontWeight="semibold" pl={2}>
<Text fontWeight="bold" display="inline">
We analyze data
</Text>{" "}
We find most interesting stuff and show it to you!
</chakra.span>
</Stack>
<Stack
px={12}
// mt={24}
bgColor="gray.50"
borderRadius="xl"
boxShadow="xl"
py={4}
>
<Heading as="h4" size="md">
UI 101?
</Heading>
<Text fontWeight="semibold" pl={2}>
On the left side corner there is sidebar that you can use to
navigate across the website. There are following views you can
navigate to:
</Text>
<chakra.span fontWeight="semibold" pl={2}>
<Text fontWeight="bold" display="inline">
Subscriptions
</Text>{" "}
- Use this screen to set up addresses you would like to
monitor to.{" "}
<i>
NB: Without setting up subscriptions moonstream will have
quite empty feel!{" "}
</i>{" "}
No worries, we will help you to set up your subscriptions in
the next steps!
</chakra.span>
<chakra.span fontWeight="semibold" pl={2}>
<Text fontWeight="bold" display="inline">
Stream
</Text>{" "}
This view is somewhat similar to a bank statement where you
can define time range and see what happened in that time over
your subscriptions. In next steps we will show how you can
apply filters to it!
</chakra.span>
<chakra.span fontWeight="semibold" pl={2}>
<Text fontWeight="bold" display="inline">
Stream Entry
</Text>{" "}
You can see detailed view of stream cards with very specific
and essential data, like methods called in smart contracts
etc!!
</chakra.span>
<chakra.span fontWeight="semibold" pl={2}>
<Text fontWeight="bold" display="inline">
Analytics
</Text>{" "}
This section is under construction yet. Soon you will be able
to create your dashboard with data of your interest. We will
really appretiate if you visit that section, and fill up form
describing what analytical tools you would love to see there!
</chakra.span>
</Stack>
<Stack
px={12}
// mt={24}
bgColor="gray.50"
borderRadius="xl"
boxShadow="xl"
py={4}
>
<Heading as="h4" size="md">
Tell us more about your needs?
</Heading>
<Text fontWeight="semibold" pl={2}>
In order to fetch best possible experience, we would like to
know some details about you
</Text>
<Text fontWeight="semibold" pl={2}>
Please tell us what profile describes you best?{" "}
<i>
This is purely analytical data, you can change it anytime
later
</i>
</Text>
<RadioGroup
onChange={setProfile}
value={profile}
fontWeight="bold"
>
<Stack direction="row" justifyContent="space-evenly">
<Radio value="trader">I am trading crypto currency</Radio>
<Radio value="fund">I represent investment fund</Radio>
<Radio value="developer">I am developer</Radio>
</Stack>
</RadioGroup>
</Stack>
</Stack>
</Fade>
)}
{ui.onboardingStep === 1 && (
<Fade in>
<Stack px="7%">
<Stack
px={12}
// mt={24}
bgColor="gray.50"
borderRadius="xl"
boxShadow="xl"
py={4}
my={2}
>
<Heading as="h4" size="md">
Subscriptions
</Heading>
<chakra.span fontWeight="semibold" pl={2}>
Subscriptions is essential tool of Moonstream. We gather data
for you based on addresses you have subscribed for.
<br />
Subscribe to any addres which you are interested in and they
will become part of your stream!
<UnorderedList>
<ListItem>
Color - you can define color to easily identify this
subscription in your stream
</ListItem>
<ListItem>Address - thing you subscribe to</ListItem>
<ListItem>
Label - Its good idea to use human readible name that
represents address
</ListItem>
<ListItem>
Source - In Alpha we support only Ethereum blockchain,
more sources are coming soon!
</ListItem>
</UnorderedList>
</chakra.span>
</Stack>
{subscriptionsCache.data.subscriptions.length > 0 &&
!subscriptionsCache.isLoading && (
<>
<Heading>
{" "}
You already have some subscriptions set up
</Heading>
</>
)}
<SubscriptionsList />
{showSubscriptionForm && (
<>
<Heading pt={12}>{`Let's add new subscription!`}</Heading>
<NewSubscription
isFreeOption={true}
onClose={SubscriptonCreatedCallback}
/>
</>
)}
{!showSubscriptionForm && (
<Button
colorScheme="suggested"
variant="solid"
onClick={() => setShowSubscriptionForm.on()}
>
Add another subscription
</Button>
)}
</Stack>
</Fade>
)}
{ui.onboardingStep === 2 && (
<Fade in>
<Stack>
<Stack
px={12}
// mt={24}
bgColor="gray.50"
borderRadius="xl"
boxShadow="xl"
py={4}
my={2}
>
<Heading as="h4" size="md">
Stream
</Heading>
<chakra.span fontWeight="semibold" pl={2}>
We are almost done!
<br />
{`Stream is where you can read data you've subscribed for. Here
you have different cards for different subscription types.`}
<br />
If card has some extra details - there will be orange button
on right hand side inviting you to see more!
<br />
Below is typical card for ethereum blockchain event. Useful
information right on the card:
<UnorderedList py={2}>
<ListItem>Hash - unique ID of the event </ListItem>
<ListItem>
From - sender address. If it is one of your subscription
addresses - will appear in color and with label{" "}
</ListItem>
<ListItem>
To - receiver address. If it is one of your subscription
addresses - will appear in color and with label{" "}
</ListItem>
<ListItem>
Nonce - Counter how many transactions address has sent. It
also determines sequence of transaction!{" "}
</ListItem>
<ListItem>
Gas Price - this is how much ether is being paid per gas
unit
</ListItem>
<ListItem>
Gas - Ammount of gas this event consumes
</ListItem>
</UnorderedList>
</chakra.span>
</Stack>
<Stack
pb={ui.isMobileView ? 24 : 8}
w={ui.isMobileView ? "100%" : "calc(100% - 300px)"}
alignSelf="center"
>
<Flex h="3rem" w="100%" bgColor="gray.100" alignItems="center">
<Flex maxW="90%"></Flex>
<Spacer />
<Tooltip
variant="onboarding"
placement={ui.isMobileView ? "bottom" : "right"}
label="Filtering menu"
isOpen={true}
maxW="150px"
hasArrow
>
<IconButton
mr={4}
// onClick={onOpen}
colorScheme="primary"
variant="ghost"
icon={<FaFilter />}
/>
</Tooltip>
</Flex>
<StreamEntry
mt={20}
entry={{
from_address: "this is address from",
to_address: "this is to address",
hash: "this is hash",
}}
showOnboardingTooltips={true}
/>
</Stack>
<Stack
px={12}
// mt={24}
bgColor="gray.50"
borderRadius="xl"
boxShadow="xl"
py={4}
my={2}
>
<Heading as="h4" size="md">
Applying filters
</Heading>
<chakra.span fontWeight="semibold" pl={2}>
You can apply various filters by clicking filter menu button
<br />
{`Right now you can use it to select address from and to, we are adding more complex queries soon, stay tuna! `}
<br />
</chakra.span>
</Stack>
</Stack>
</Fade>
)}
<ButtonGroup>
<Button
colorScheme="secondary"
leftIcon={<ArrowLeftIcon />}
variant="outline"
hidden={ui.onboardingStep === 0}
disabled={ui.onboardingStep === 0}
onClick={() => {
ui.setOnboardingStep(ui.onboardingStep - 1);
scrollRef?.current?.scrollIntoView();
}}
>
Previous
</Button>
<Spacer />
<Button
colorScheme="secondary"
variant="solid"
rightIcon={<ArrowRightIcon />}
// hidden={!(ui.onboardingStep < ui.onboardingSteps.length - 1)}
// disabled={!(ui.onboardingStep < ui.onboardingSteps.length - 1)}
onClick={() => handleNextClick()}
>
{ui.onboardingStep < ui.onboardingSteps.length - 1
? `Next`
: `Finish `}
</Button>
</ButtonGroup>
</Stack>
</Scrollable>
);
};
Welcome.getLayout = getLayout;
export default Welcome;

Wyświetl plik

@ -1,13 +1,38 @@
const Spinner = {
baseStyle: {
color: "primary.400",
thickness: "4px",
speed: "1.5s",
my: 8,
const baseStyle = {
color: "primary.400",
thickness: "4px",
speed: "1.5s",
my: 8,
};
const variants = {
basic: { thickness: "4px", speed: "1.5s" },
};
const sizes = {
xs: {
"--spinner-size": "0.75rem",
},
variants: {
basic: { thickness: "4px", speed: "1.5s" },
sm: {
"--spinner-size": "1rem",
},
md: {
"--spinner-size": "1.5rem",
},
lg: {
"--spinner-size": "2rem",
},
xl: {
"--spinner-size": "3rem",
},
};
export default Spinner;
const defaultProps = {
size: "md",
};
export default {
baseStyle,
sizes,
defaultProps,
variants,
};

Wyświetl plik

@ -0,0 +1,46 @@
import { mode } from "@chakra-ui/theme-tools";
const baseStyle = (props) => {
const bg = mode("gray.700", "gray.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: "sm",
fontWeight: "medium",
fontSize: "sm",
boxShadow: "md",
maxW: "320px",
zIndex: "tooltip",
};
};
const variantOnboarding = (props) => {
const bg = mode("secondary.700", "secondary.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 variants = {
onboarding: variantOnboarding,
};
export default {
baseStyle,
variants,
};

Wyświetl plik

@ -8,6 +8,8 @@ import NumberInput from "./NumberInput";
import Badge from "./Badge";
import Checkbox from "./Checkbox";
import Table from "./Table";
import Tooltip from "./Tooltip";
import Spinner from "./Spinner";
import { createBreakpoints } from "@chakra-ui/theme-tools";
const breakpointsCustom = createBreakpoints({
@ -53,6 +55,8 @@ const theme = extendTheme({
Badge,
Checkbox,
Table,
Spinner,
Tooltip,
},
fonts: {

Wyświetl plik

@ -15,6 +15,7 @@ import {
PopoverCloseButton,
useBreakpointValue,
Spacer,
ButtonGroup,
} from "@chakra-ui/react";
import {
HamburgerIcon,
@ -26,6 +27,7 @@ import { MdTimeline } from "react-icons/md";
import useRouter from "../core/hooks/useRouter";
import UIContext from "../core/providers/UIProvider/context";
import AccountIconButton from "./AccountIconButton";
import RouteButton from "./RouteButton";
const AppNavbar = () => {
const ui = useContext(UIContext);
@ -95,6 +97,14 @@ const AppNavbar = () => {
<Flex width="100%" px={2}>
<Spacer />
<Flex placeSelf="flex-end">
<ButtonGroup spacing={4}>
{/* <RouteButton variant="link" href="/docs">
Docs
</RouteButton> */}
<RouteButton variant="link" href="/welcome">
Learn how to
</RouteButton>
</ButtonGroup>
<SupportPopover />
<AccountIconButton
colorScheme="primary"

Wyświetl plik

@ -204,7 +204,6 @@ const EntriesNavigation = () => {
};
const dropFilterArrayItem = (idx) => {
console.log("dropFilterArrayItem", idx, filterState);
const oldArray = [...filterState];
const newArray = oldArray.filter(function (ele) {
return ele != oldArray[idx];
@ -242,7 +241,6 @@ const EntriesNavigation = () => {
};
const handleFilterStateCallback = (props) => {
console.log("handleFilterStateCallback", props);
const currentFilterState = [...filterState];
currentFilterState.push({ ...props });
@ -529,6 +527,7 @@ const EntriesNavigation = () => {
?.sort((a, b) => b.timestamp - a.timestamp) // TODO(Andrey) improve that for bi chunks of data sorting can take time
.map((entry, idx) => (
<StreamEntry
showOnboardingTooltips={false}
key={`entry-list-${idx}`}
entry={entry}
disableDelete={!canDelete}

Wyświetl plik

@ -13,6 +13,7 @@ import { HamburgerIcon } from "@chakra-ui/icons";
import useModals from "../core/hooks/useModals";
import UIContext from "../core/providers/UIProvider/context";
import ChakraAccountIconButton from "./AccountIconButton";
import RouteButton from "./RouteButton";
const LandingNavbar = () => {
const ui = useContext(UIContext);
@ -52,6 +53,17 @@ const LandingNavbar = () => {
spacing={4}
pr={16}
>
{ui.isLoggedIn && (
<ButtonGroup spacing={4}>
{/* <RouteButton variant="link" href="/docs">
Docs
</RouteButton> */}
<RouteButton variant="link" href="/welcome">
Learn how to
</RouteButton>
</ButtonGroup>
)}
{ui.isLoggedIn && (
<RouterLink href="/stream" passHref>
<Button

Wyświetl plik

@ -0,0 +1,177 @@
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 }) => {
const [color, setColor] = useState(makeColor());
const { typesCache, createSubscription } = useSubscriptions();
const { handleSubmit, errors, register } = useForm({});
const [radioState, setRadioState] = useState("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"
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.subscriptions.map((type) => {
const radio = getRadioProps({
value: type.id,
isDisabled:
!type.active ||
(isFreeOption &&
type.subscription_type !== "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"
// 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>
<GithubPicker
// color={this.state.background}
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;

Wyświetl plik

@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useCallback } from "react";
import { useSubscriptions } from "../core/hooks";
import {
Input,
@ -8,23 +8,24 @@ import {
useRadioGroup,
FormControl,
FormErrorMessage,
ModalBody,
ModalCloseButton,
ModalHeader,
Button,
ModalFooter,
Spinner,
IconButton,
ButtonGroup,
Spacer,
Flex,
} from "@chakra-ui/react";
import RadioCard from "./RadioCard";
import { useForm } from "react-hook-form";
import { GithubPicker } from "react-color";
// import { useForm } from "react-hook-form";
import { CirclePicker } from "react-color";
import { BiRefresh } from "react-icons/bi";
import { makeColor } from "../core/utils/makeColor";
const NewSubscription = ({ isFreeOption, onClose }) => {
import { useForm } from "react-hook-form";
const _NewSubscription = ({ isFreeOption, onClose, setIsLoading }) => {
const [color, setColor] = useState(makeColor());
const { typesCache, createSubscription } = useSubscriptions();
const { handleSubmit, errors, register } = useForm({});
const { typesCache, createSubscription } = useSubscriptions();
// const { handleSubmit, errors, register } = useForm({});
const [radioState, setRadioState] = useState("ethereum_blockchain");
let { getRootProps, getRadioProps } = useRadioGroup({
name: "type",
@ -34,96 +35,105 @@ const NewSubscription = ({ isFreeOption, onClose }) => {
const group = getRootProps();
useEffect(() => {
if (setIsLoading) {
setIsLoading(createSubscription.isLoading);
}
}, [createSubscription.isLoading, setIsLoading]);
useEffect(() => {
if (createSubscription.isSuccess) {
onClose();
}
}, [createSubscription.isSuccess, onClose]);
if (typesCache.isLoading) return <Spinner />;
const createSubscriptionWrapper = useCallback(
(props) => {
createSubscription.mutate({
...props,
color: color,
type: isFreeOption ? 0 : radioState,
});
},
[createSubscription, isFreeOption, color, radioState]
);
const createSubscriptionWrap = (props) => {
createSubscription.mutate({
...props,
color: color,
type: isFreeOption ? "free" : radioState,
});
};
if (typesCache.isLoading) return <Spinner />;
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"
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>
if (!errors) return "";
<FormControl isInvalid={errors.subscription_type}>
<HStack {...group} alignItems="stretch">
{typesCache.data.subscriptions.map((type) => {
const radio = getRadioProps({
value: type.id,
isDisabled:
!type.active ||
(isFreeOption &&
type.subscription_type !== "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}>
return (
<form onSubmit={handleSubmit(createSubscriptionWrapper)}>
<FormControl isInvalid={errors?.label}>
<Input
my={2}
type="text"
autoComplete="off"
placeholder="Meaningful name of your subscription"
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="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
? `Right now you can subscribe only to ethereum blockchain`
: `On which source?`}
</Text> */}
<FormControl isInvalid={errors?.subscription_type}>
<HStack {...group} alignItems="stretch">
{typesCache.data.subscriptions.map((type) => {
const radio = getRadioProps({
value: type.id,
isDisabled:
!type.active ||
(isFreeOption &&
type.subscription_type !== "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}>
<Flex direction="row" pb={2} flexWrap="wrap">
<Stack pt={2} direction="row" h="min-content">
<Text fontWeight="600" alignSelf="center">
Label color
</Text>{" "}
@ -147,18 +157,21 @@ const NewSubscription = ({ isFreeOption, onClose }) => {
w="200px"
></Input>
</Stack>
<Flex p={2}>
<CirclePicker
onChangeComplete={handleChangeColorComplete}
circleSpacing={1}
circleSize={24}
/>
</Flex>
</Flex>
<GithubPicker
// color={this.state.background}
onChangeComplete={handleChangeColorComplete}
/>
<FormErrorMessage color="unsafe.400" pl="1">
{errors?.color && errors?.color.message}
</FormErrorMessage>
</FormControl>
<FormErrorMessage color="unsafe.400" pl="1">
{errors.color && errors.color.message}
</FormErrorMessage>
</FormControl>
</ModalBody>
<ModalFooter>
<ButtonGroup direction="row" justifyContent="space-evenly">
<Button
type="submit"
colorScheme="suggested"
@ -166,12 +179,13 @@ const NewSubscription = ({ isFreeOption, onClose }) => {
>
Confirm
</Button>
<Spacer />
<Button colorScheme="gray" onClick={onClose}>
Cancel
</Button>
</ModalFooter>
</ButtonGroup>
</form>
);
};
export default NewSubscription;
export default _NewSubscription;

Wyświetl plik

@ -0,0 +1,17 @@
import React from "react";
import { chakra, Button, Link } from "@chakra-ui/react";
import NextLink from "next/link";
const _RouteButton = (props) => {
return (
<NextLink href={props.href} passHref>
<Button as={Link} {...props}>
{props.children}
</Button>
</NextLink>
);
};
const RouteButton = chakra(_RouteButton, "button");
export default RouteButton;

Wyświetl plik

@ -14,6 +14,7 @@ import React from "react";
import { HamburgerIcon, ArrowLeftIcon, ArrowRightIcon } from "@chakra-ui/icons";
import { MdTimeline, MdSettings } from "react-icons/md";
import { ImStatsBars } from "react-icons/im";
import { HiAcademicCap } from "react-icons/hi";
const Sidebar = () => {
const ui = useContext(UIContext);
@ -79,6 +80,13 @@ const Sidebar = () => {
<RouterLink href="/subscriptions">Subscriptions </RouterLink>
</MenuItem>
</Menu>
{ui.isMobileView && (
<Menu iconShape="square">
<MenuItem icon={<HiAcademicCap />}>
<RouterLink href="/welcome">Learn how to</RouterLink>
</MenuItem>
</Menu>
)}
</SidebarContent>
)}
{!ui.isLoggedIn && (

Wyświetl plik

@ -36,17 +36,16 @@ const SteamEntryDetails = () => {
>
<HStack id="EntryHeader" width="100%" m={0}>
<Heading
overflow="hidden"
width={entry?.context_url ? "calc(100% - 28px)" : "100%"}
minH="36px"
style={{ marginLeft: "0" }}
m={0}
p={0}
fontWeight="600"
fontSize="1.5rem"
fontSize="md"
textAlign="left"
>
{entry && entry.tx.hash}
{entry && `Hash: ${entry.tx.hash}`}
</Heading>
</HStack>
</Skeleton>

Wyświetl plik

@ -0,0 +1,68 @@
import React, { useContext } from "react";
import { Box, Button, Progress, ButtonGroup } from "@chakra-ui/react";
import _ from "lodash";
import UIContext from "../core/providers/UIProvider/context";
const StepProgress = ({
numSteps,
currentStep,
colorScheme,
buttonCallback,
buttonTitles,
}) => {
const ui = useContext(UIContext);
return (
<Box w="100%" h="auto" pos="relative">
<ButtonGroup
display="inline-flex"
flexDirection="row"
justifyContent="space-between"
w="100%"
m={0}
p={0}
spacing={0}
>
{_.times(numSteps, (i) => {
const setActive = i === parseInt(currentStep) ? true : false;
return (
<Button
key={`${i}-progress-steps`}
size={ui.isMobileView ? "md" : "sm"}
borderRadius={ui.isMobileView ? "full" : "md"}
// size="sm"
// bgColor={`${colorScheme}.200`}
_active={{ bgColor: `${colorScheme}.1200` }}
zIndex={1}
m={0}
colorScheme={colorScheme}
isActive={setActive}
onClick={() => buttonCallback(i)}
>
{ui.isMobileView && i + 1}
{!ui.isMobileView && buttonTitles[i]}
</Button>
);
})}
</ButtonGroup>
<Progress
position="absolute"
top="50%"
transform="translateY(-50%)"
h={2}
w="full"
// hasStripe
// isAnimated
max={numSteps - 1}
min={0}
value={currentStep}
/>
{/* <Flex
h="1rem"
flexGrow={1}
flexBasis="10px"
backgroundColor={`${colorScheme}.300`}
/> */}
</Box>
);
};
export default StepProgress;

Wyświetl plik

@ -11,6 +11,7 @@ import {
useMediaQuery,
Spacer,
Spinner,
chakra,
} from "@chakra-ui/react";
import moment from "moment";
import { ArrowRightIcon } from "@chakra-ui/icons";
@ -18,7 +19,7 @@ import UIContext from "../core/providers/UIProvider/context";
import { useToast } from "../core/hooks";
import { useSubscriptions } from "../core/hooks";
const StreamEntry = ({ entry }) => {
const StreamEntry = ({ entry, showOnboardingTooltips, className }) => {
const { subscriptionsCache } = useSubscriptions();
const ui = useContext(UIContext);
const [copyString, setCopyString] = useState(false);
@ -49,6 +50,7 @@ const StreamEntry = ({ entry }) => {
return (
<Flex
className={className}
p={0}
m={1}
mr={2}
@ -89,35 +91,44 @@ const StreamEntry = ({ entry }) => {
overflowX="hidden"
overflowY="visible"
>
<Stack
className="title"
direction="row"
w="100%"
h="1.6rem"
minH="1.6rem"
textAlign="center"
spacing={0}
alignItems="center"
bgColor="gray.300"
<Tooltip
hasArrow
isOpen={showOnboardingTooltips}
// shouldWrapChildren
label="Top of card describes type of event. Ethereum blockchain in this case. It as unique hash shown here"
variant="onboarding"
placement="top"
>
<Image
boxSize="16px"
src={
"https://upload.wikimedia.org/wikipedia/commons/0/05/Ethereum_logo_2014.svg"
}
/>
<Heading px={1} size="xs">
Hash
</Heading>
<Spacer />
<Text
isTruncated
onClick={() => setCopyString(entry.hash)}
pr={12}
<Stack
className="title"
direction="row"
w="100%"
h="1.6rem"
minH="1.6rem"
textAlign="center"
spacing={0}
alignItems="center"
bgColor="gray.300"
>
{entry.hash}
</Text>
</Stack>
<Image
boxSize="16px"
src={
"https://upload.wikimedia.org/wikipedia/commons/0/05/Ethereum_logo_2014.svg"
}
/>
<Heading px={1} size="xs">
Hash
</Heading>
<Spacer />
<Text
isTruncated
onClick={() => setCopyString(entry.hash)}
pr={12}
>
{entry.hash}
</Text>
</Stack>
</Tooltip>
<Stack
className="CardAddressesRow"
direction={showFullView ? "row" : "column"}
@ -143,16 +154,25 @@ const StreamEntry = ({ entry }) => {
placeContent="center"
spacing={0}
>
<Text
bgColor="gray.600"
h="100%"
fontSize="sm"
py="2px"
px={2}
w={showFullView ? null : "120px"}
<Tooltip
hasArrow
isOpen={showOnboardingTooltips && !ui.isMobileView}
label="From and to addresses, clicking names will copy address to clipboard!"
variant="onboarding"
placement={ui.isMobileView ? "bottom" : "left"}
maxW="150px"
>
From:
</Text>
<Text
bgColor="gray.600"
h="100%"
fontSize="sm"
py="2px"
px={2}
w={showFullView ? null : "120px"}
>
From:
</Text>
</Tooltip>
<Tooltip label={entry.from_address} aria-label="From:">
<Text
mx={0}
@ -168,6 +188,7 @@ const StreamEntry = ({ entry }) => {
</Text>
</Tooltip>
</Stack>
<Stack
overflow="hidden"
textOverflow="ellipsis"
@ -322,22 +343,33 @@ const StreamEntry = ({ entry }) => {
</Stack>
)}
<Flex>
<IconButton
m={0}
onClick={() => ui.setCurrentTransaction(entry)}
h="full"
// minH="24px"
borderLeftRadius={0}
variant="solid"
px={0}
minW="24px"
colorScheme="secondary"
icon={<ArrowRightIcon w="24px" />}
/>
<Tooltip
hasArrow
isOpen={showOnboardingTooltips}
placement={ui.isMobileView ? "bottom" : "right"}
label="Clicking side arrow will bring up detailed view"
variant="onboarding"
maxW="150px"
>
<IconButton
m={0}
onClick={() => ui.setCurrentTransaction(entry)}
h="full"
// minH="24px"
borderLeftRadius={0}
variant="solid"
px={0}
minW="24px"
colorScheme="secondary"
icon={<ArrowRightIcon w="24px" />}
/>
</Tooltip>
</Flex>
</Stack>
</Flex>
);
};
export default StreamEntry;
const StreamChakraEntry = chakra(StreamEntry);
export default StreamChakraEntry;

Wyświetl plik

@ -1,5 +1,5 @@
import React from "react";
import { Skeleton, IconButton } from "@chakra-ui/react";
import { Skeleton, IconButton, Container } from "@chakra-ui/react";
import {
Table,
Th,
@ -12,6 +12,7 @@ import {
EditableInput,
Image,
EditablePreview,
Button,
} from "@chakra-ui/react";
import { DeleteIcon } from "@chakra-ui/icons";
import moment from "moment";
@ -20,7 +21,7 @@ import { useSubscriptions } from "../core/hooks";
import ConfirmationRequest from "./ConfirmationRequest";
import ColorSelector from "./ColorSelector";
const SubscriptionsList = () => {
const SubscriptionsList = ({ emptyCTA }) => {
const { subscriptionsCache, updateSubscription, deleteSubscription } =
useSubscriptions();
@ -31,7 +32,10 @@ const SubscriptionsList = () => {
updateSubscription.mutate(data);
};
if (subscriptionsCache.data) {
if (
subscriptionsCache.data &&
subscriptionsCache.data.subscriptions.length > 0
) {
return (
<Table
borderColor="gray.200"
@ -138,6 +142,16 @@ const SubscriptionsList = () => {
</Tbody>
</Table>
);
} else if (
subscriptionsCache.data &&
subscriptionsCache.data.subscriptions.length === 0
) {
return (
<Container>
{` You don't have any subscriptions at the moment.`}
{emptyCTA && <Button variant="suggested">Create one</Button>}
</Container>
);
} else if (subscriptionsCache.isLoading) {
return <Skeleton />;
} else {

Wyświetl plik

@ -16,7 +16,6 @@ const toEth = (wei) => {
const TxABI = (props) => {
const byteCode = props.byteCode;
const abi = props.abi;
console.log(abi.functions);
return (
<VStack spacing={3}>
<br />
@ -58,25 +57,29 @@ const TxInfo = (props) => {
return (
<Box boxShadow="xs" p="6" rounded="md">
<StatGroup>
<Stat>
<Stat px={2}>
<StatLabel>Value</StatLabel>
<StatNumber>{toEth(transaction.tx.value)} eth</StatNumber>
<StatNumber fontSize="md">
{toEth(transaction.tx.value)} eth
</StatNumber>
<StatHelpText>amount of ETH to transfer</StatHelpText>
</Stat>
<Stat>
<StatLabel>Gas limit</StatLabel>
<StatNumber>{transaction.tx.gas}</StatNumber>
<StatNumber fontSize="md">{transaction.tx.gas}</StatNumber>
<StatHelpText>Maximum amount of gas </StatHelpText>
<StatHelpText>provided for the transaction</StatHelpText>
</Stat>
<Stat>
<StatLabel>Gas price</StatLabel>
<StatNumber>{toEth(transaction.tx.gasPrice)} eth</StatNumber>
<StatNumber fontSize="md">
{toEth(transaction.tx.gasPrice)} eth
</StatNumber>
<StatHelpText>the fee the sender pays per unit of gas</StatHelpText>
</Stat>
</StatGroup>
<Table variant="simple">
<Table variant="simple" size="sm">
<Tbody>
{Object.keys(transaction.tx)
.filter(dont_display)
@ -85,7 +88,7 @@ const TxInfo = (props) => {
transaction.tx[key] != undefined && (
<Tr key={key}>
<Td>{key}</Td>
<Td>{transaction.tx[key]}</Td>
<Td wordBreak="break-all">{transaction.tx[key]}</Td>
</Tr>
)
)}

Wyświetl plik

@ -1,9 +1,11 @@
import { useEffect } from "react";
import { useContext } from "react";
import { useMutation } from "react-query";
import { AuthService } from "../services";
import { useUser, useToast, useInviteAccept, useRouter, useAnalytics } from ".";
import UIContext from "../providers/UIProvider/context";
const useSignUp = (source) => {
const ui = useContext(UIContext);
const router = useRouter();
const { getUser } = useUser();
const toast = useToast();
@ -30,28 +32,16 @@ const useSignUp = (source) => {
{ full_url: router.nextRouter.asPath, code: source }
);
}
getUser();
ui.setisOnboardingComplete(false);
ui.setOnboardingState({ welcome: 0, subscriptions: 0, stream: 0 });
router.push("/welcome", undefined, { shallow: false });
},
onError: (error) => {
toast(error, "error");
},
});
useEffect(() => {
if (!data) {
return;
}
getUser();
const requested_pricing = window.sessionStorage.getItem(
"requested_pricing_plan"
);
const redirectURL = requested_pricing ? "/subscriptions" : "/stream";
router.push(redirectURL);
window.sessionStorage.clear("requested_pricing");
}, [data, getUser, router]);
return {
signUp,
isLoading,

Wyświetl plik

@ -5,6 +5,7 @@ export const MIXPANEL_PROPS = {
PRICING_PLAN: "Pricing Plan",
TOGGLE_LEFT_SIDEBAR: "Toggle left sidebar",
BUTTON_NAME: "Button",
USER_SPECIALITY: "user speciality",
};
export const MIXPANEL_EVENTS = {

Wyświetl plik

@ -17,15 +17,7 @@ const UIProvider = ({ children }) => {
xl: false,
"2xl": false,
});
const currentBreakpoint = useBreakpointValue({
base: 0,
sm: 1,
md: 2,
lg: 3,
xl: 4,
"2xl": 5,
});
// const isMobileView = true;
const { modal, toggleModal } = useContext(ModalContext);
const [searchTerm, setSearchTerm] = useQuery("q", "", true, false);
@ -44,7 +36,8 @@ const UIProvider = ({ children }) => {
router.nextRouter.asPath.includes("/stream") ||
router.nextRouter.asPath.includes("/account") ||
router.nextRouter.asPath.includes("/subscriptions") ||
router.nextRouter.asPath.includes("/analytics")
router.nextRouter.asPath.includes("/analytics") ||
router.nextRouter.asPath.includes("/welcome")
);
useEffect(() => {
@ -81,7 +74,8 @@ const UIProvider = ({ children }) => {
router.nextRouter.asPath.includes("/stream") ||
router.nextRouter.asPath.includes("/account") ||
router.nextRouter.asPath.includes("/subscriptions") ||
router.nextRouter.asPath.includes("/analytics")
router.nextRouter.asPath.includes("/analytics") ||
router.nextRouter.asPath.includes("/welcome")
);
}, [router.nextRouter.asPath, user]);
@ -119,7 +113,7 @@ const UIProvider = ({ children }) => {
//Sidebar is visible at at breakpoint value less then 2
//Sidebar is visible always in appView
useEffect(() => {
if (currentBreakpoint < 2) {
if (isMobileView) {
setSidebarVisible(true);
setSidebarCollapsed(false);
} else {
@ -130,7 +124,7 @@ const UIProvider = ({ children }) => {
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentBreakpoint, isAppView]);
}, [isMobileView, isAppView]);
// *********** Entries layout states **********************
@ -159,6 +153,81 @@ const UIProvider = ({ children }) => {
);
}, [isEntryDetailView, isMobileView]);
// *********** Onboarding state **********************
const onboardingSteps = [
{
step: "welcome",
description: "Basics of how Moonstream works",
},
{
step: "subscriptions",
description: "Learn how to subscribe to blockchain events",
},
{
step: "stream",
description: "Learn how to use your Moonstream to analyze blah blah blah",
},
];
const [onboardingState, setOnboardingState] = useStorage(
window.localStorage,
"onboardingState",
{
welcome: 0,
subscriptions: 0,
stream: 0,
}
);
const [onboardingStep, setOnboardingStep] = useState(() => {
const step = onboardingSteps.findIndex(
(event) => onboardingState[event.step] === 0
);
if (step === -1 && isOnboardingComplete) return onboardingSteps.length - 1;
else if (step === -1 && !isOnboardingComplete) return 0;
else return step;
});
const [isOnboardingComplete, setisOnboardingComplete] = useStorage(
window.localStorage,
"isOnboardingComplete",
isLoggedIn ? true : false
);
useEffect(() => {
if (isLoggedIn && !isOnboardingComplete) {
router.replace("/welcome");
}
// eslint-disable-next-line
}, [isLoggedIn, isOnboardingComplete]);
useEffect(() => {
if (
onboardingSteps.findIndex(
(event) => onboardingState[event.step] === 0
) === -1
) {
setisOnboardingComplete(true);
}
//eslint-disable-next-line
}, [onboardingState]);
useEffect(() => {
if (router.nextRouter.pathname === "/welcome") {
const newOnboardingState = {
...onboardingState,
[`${onboardingSteps[onboardingStep].step}`]:
onboardingState[onboardingSteps[onboardingStep].step] + 1,
};
setOnboardingState(newOnboardingState);
}
// eslint-disable-next-line
}, [onboardingStep, router.nextRouter.pathname]);
// const ONBOARDING_STEP_NUM = steps.length;
// ********************************************************
return (
@ -188,6 +257,12 @@ const UIProvider = ({ children }) => {
currentTransaction,
setCurrentTransaction,
isEntryDetailView,
onboardingStep,
setOnboardingStep,
isOnboardingComplete,
setisOnboardingComplete,
onboardingSteps,
setOnboardingState,
}}
>
{children}