kopia lustrzana https://github.com/bugout-dev/moonstream
Merge branch 'main' into whale-watch
commit
828c6b1e61
|
@ -1,7 +1,9 @@
|
||||||
name: Lint Moonstream backend
|
name: Lint Moonstream backend
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- "main"
|
||||||
paths:
|
paths:
|
||||||
- "backend/**"
|
- "backend/**"
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
name: Lint Moonstream crawlers
|
name: Lint Moonstream crawlers
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- "main"
|
||||||
paths:
|
paths:
|
||||||
- "crawlers/**"
|
- "crawlers/**"
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
name: Lint Moonstream db
|
name: Lint Moonstream db
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- "main"
|
||||||
paths:
|
paths:
|
||||||
- "db/**"
|
- "db/**"
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
name: Build Moonstream frontend
|
name: Build Moonstream frontend
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
|
||||||
paths:
|
|
||||||
- "frontend/**"
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- "main"
|
- "main"
|
||||||
|
|
|
@ -160,3 +160,17 @@ class TxinfoEthereumBlockchainResponse(BaseModel):
|
||||||
smart_contract_address: Optional[str] = None
|
smart_contract_address: Optional[str] = None
|
||||||
abi: Optional[ContractABI] = None
|
abi: Optional[ContractABI] = None
|
||||||
errors: List[str] = Field(default_factory=list)
|
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)
|
||||||
|
|
|
@ -6,33 +6,26 @@ transactions, etc.) with side information and return objects that are better sui
|
||||||
end users.
|
end users.
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict
|
from typing import Dict, Optional
|
||||||
|
|
||||||
from fastapi import (
|
from fastapi import FastAPI, Depends, HTTPException, Query
|
||||||
FastAPI,
|
|
||||||
Depends,
|
|
||||||
)
|
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from moonstreamdb.db import yield_db_session
|
from moonstreamdb.db import yield_db_session
|
||||||
from moonstreamdb.models import EthereumAddress
|
from moonstreamdb.models import EthereumAddress
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from ..abi_decoder import decode_abi
|
from ..abi_decoder import decode_abi
|
||||||
from ..data import TxinfoEthereumBlockchainRequest, TxinfoEthereumBlockchainResponse
|
from .. import actions
|
||||||
|
from .. import data
|
||||||
from ..middleware import BroodAuthMiddleware
|
from ..middleware import BroodAuthMiddleware
|
||||||
from ..settings import (
|
from ..settings import DOCS_TARGET_PATH, ORIGINS, DOCS_PATHS
|
||||||
DOCS_TARGET_PATH,
|
|
||||||
ORIGINS,
|
|
||||||
DOCS_PATHS,
|
|
||||||
bugout_client as bc,
|
|
||||||
)
|
|
||||||
from ..version import MOONSTREAM_VERSION
|
from ..version import MOONSTREAM_VERSION
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
tags_metadata = [
|
tags_metadata = [
|
||||||
{"name": "users", "description": "Operations with users."},
|
{"name": "txinfo", "description": "Ethereum transactions info."},
|
||||||
{"name": "tokens", "description": "Operations with user tokens."},
|
{"name": "address info", "description": "Addresses public information."},
|
||||||
]
|
]
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
|
@ -63,13 +56,13 @@ app.add_middleware(BroodAuthMiddleware, whitelist=whitelist_paths)
|
||||||
@app.post(
|
@app.post(
|
||||||
"/ethereum_blockchain",
|
"/ethereum_blockchain",
|
||||||
tags=["txinfo"],
|
tags=["txinfo"],
|
||||||
response_model=TxinfoEthereumBlockchainResponse,
|
response_model=data.TxinfoEthereumBlockchainResponse,
|
||||||
)
|
)
|
||||||
async def txinfo_ethereum_blockchain_handler(
|
async def txinfo_ethereum_blockchain_handler(
|
||||||
txinfo_request: TxinfoEthereumBlockchainRequest,
|
txinfo_request: data.TxinfoEthereumBlockchainRequest,
|
||||||
db_session: Session = Depends(yield_db_session),
|
db_session: Session = Depends(yield_db_session),
|
||||||
) -> TxinfoEthereumBlockchainResponse:
|
) -> data.TxinfoEthereumBlockchainResponse:
|
||||||
response = TxinfoEthereumBlockchainResponse(tx=txinfo_request.tx)
|
response = data.TxinfoEthereumBlockchainResponse(tx=txinfo_request.tx)
|
||||||
if txinfo_request.tx.input is not None:
|
if txinfo_request.tx.input is not None:
|
||||||
try:
|
try:
|
||||||
response.abi = decode_abi(txinfo_request.tx.input, db_session)
|
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
|
response.is_smart_contract_call = True
|
||||||
|
|
||||||
return response
|
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
|
||||||
|
|
|
@ -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()
|
|
@ -1,12 +1,12 @@
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
from moonstreamdb.db import yield_db_session_ctx
|
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")
|
COINMARKETCAP_API_KEY = os.environ.get("COINMARKETCAP_API_KEY")
|
||||||
if COINMARKETCAP_API_KEY is None:
|
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.
|
Parse metadata for Ethereum tokens.
|
||||||
"""
|
"""
|
||||||
|
@ -37,7 +37,11 @@ def identities_cmc_handler(args: argparse.Namespace) -> None:
|
||||||
limit_n = 5000
|
limit_n = 5000
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
params = {"start": start_n, "limit": limit_n}
|
params = {
|
||||||
|
"start": start_n,
|
||||||
|
"limit": limit_n,
|
||||||
|
"listing_status": args.listing_status,
|
||||||
|
}
|
||||||
try:
|
try:
|
||||||
r = requests.get(url=url, headers=headers, params=params)
|
r = requests.get(url=url, headers=headers, params=params)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
|
@ -50,20 +54,36 @@ def identities_cmc_handler(args: argparse.Namespace) -> None:
|
||||||
break
|
break
|
||||||
|
|
||||||
with yield_db_session_ctx() as db_session:
|
with yield_db_session_ctx() as db_session:
|
||||||
for crypto in response["data"]:
|
latest_address = (
|
||||||
if crypto["platform"] is not None:
|
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 (
|
if (
|
||||||
crypto["platform"]["id"] == 1027
|
coin["platform"]["id"] == 1027
|
||||||
and crypto["platform"]["token_address"] is not None
|
and coin["platform"]["token_address"] is not None
|
||||||
):
|
):
|
||||||
|
latest_address += 1
|
||||||
|
eth_token_id = latest_address
|
||||||
eth_token = EthereumAddress(
|
eth_token = EthereumAddress(
|
||||||
address=crypto["platform"]["token_address"],
|
id=eth_token_id,
|
||||||
name=crypto["name"],
|
address=coin["platform"]["token_address"],
|
||||||
symbol=crypto["symbol"],
|
|
||||||
)
|
)
|
||||||
db_session.add(eth_token)
|
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()
|
db_session.commit()
|
||||||
start_n += limit_n
|
start_n += limit_n
|
||||||
|
@ -79,21 +99,31 @@ def main():
|
||||||
|
|
||||||
parser_cmc = subcommands.add_parser("cmc", description="Coinmarketcap commands")
|
parser_cmc = subcommands.add_parser("cmc", description="Coinmarketcap commands")
|
||||||
parser_cmc.set_defaults(func=lambda _: parser_cmc.print_help())
|
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(
|
parser_cmc.add_argument(
|
||||||
"-s",
|
"-s",
|
||||||
"--sandbox",
|
"--sandbox",
|
||||||
action="store_true",
|
action="store_true",
|
||||||
help="Target to sandbox API",
|
help="Target to sandbox API",
|
||||||
)
|
)
|
||||||
parser_cmc.set_defaults(func=identities_cmc_handler)
|
|
||||||
|
|
||||||
parser_label_cloud = subcommands.add_parser(
|
subcommands_parser_cmc = parser_cmc.add_subparsers(
|
||||||
"label_cloud", description="Etherscan label cloud commands"
|
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 = parser.parse_args()
|
||||||
args.func(args)
|
args.func(args)
|
||||||
|
|
|
@ -11,3 +11,6 @@ except:
|
||||||
raise Exception(
|
raise Exception(
|
||||||
f"Could not parse MOONSTREAM_CRAWL_WORKERS as int: {MOONSTREAM_CRAWL_WORKERS_RAW}"
|
f"Could not parse MOONSTREAM_CRAWL_WORKERS as int: {MOONSTREAM_CRAWL_WORKERS_RAW}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
MOONSTREAM_ETHERSCAN_TOKEN = os.environ.get("MOONSTREAM_ETHERSCAN_TOKEN")
|
||||||
|
|
|
@ -2,4 +2,4 @@
|
||||||
Moonstream crawlers version.
|
Moonstream crawlers version.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
MOONCRAWL_VERSION = "0.0.2"
|
MOONCRAWL_VERSION = "0.0.3"
|
||||||
|
|
|
@ -2,4 +2,8 @@
|
||||||
export MOONSTREAM_IPC_PATH=null
|
export MOONSTREAM_IPC_PATH=null
|
||||||
export MOONSTREAM_CRAWL_WORKERS=4
|
export MOONSTREAM_CRAWL_WORKERS=4
|
||||||
export MOONSTREAM_DB_URI="postgresql://<username>:<password>@<db_host>:<db_port>/<db_name>"
|
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 MOONSTREAM_HUMBUG_TOKEN="<Token for crawlers store data via Humbug>"
|
||||||
|
export COINMARKETCAP_API_KEY="<API key to parse conmarketcap>"
|
||||||
|
|
|
@ -38,6 +38,7 @@ setup(
|
||||||
"requests",
|
"requests",
|
||||||
"tqdm",
|
"tqdm",
|
||||||
"web3",
|
"web3",
|
||||||
|
"boto3",
|
||||||
],
|
],
|
||||||
extras_require={"dev": ["black", "mypy", "types-requests"]},
|
extras_require={"dev": ["black", "mypy", "types-requests"]},
|
||||||
entry_points={
|
entry_points={
|
||||||
|
@ -45,6 +46,7 @@ setup(
|
||||||
"ethcrawler=mooncrawl.ethcrawler:main",
|
"ethcrawler=mooncrawl.ethcrawler:main",
|
||||||
"esd=mooncrawl.esd:main",
|
"esd=mooncrawl.esd:main",
|
||||||
"identity=mooncrawl.identity:main",
|
"identity=mooncrawl.identity:main",
|
||||||
|
"etherscan=mooncrawl.etherscan:main",
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -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 ###
|
|
@ -113,7 +113,7 @@ class EthereumAddress(Base): # type: ignore
|
||||||
nullable=True,
|
nullable=True,
|
||||||
index=True,
|
index=True,
|
||||||
)
|
)
|
||||||
address = Column(VARCHAR(256), nullable=False, index=True)
|
address = Column(VARCHAR(256), nullable=False, unique=True, index=True)
|
||||||
created_at = Column(
|
created_at = Column(
|
||||||
DateTime(timezone=True), server_default=utcnow(), nullable=False
|
DateTime(timezone=True), server_default=utcnow(), nullable=False
|
||||||
)
|
)
|
||||||
|
|
|
@ -39,7 +39,6 @@ export default function CachingApp({ Component, pageProps }) {
|
||||||
router.events.on("routeChangeComplete", handleStop);
|
router.events.on("routeChangeComplete", handleStop);
|
||||||
router.events.on("routeChangeError", handleStop);
|
router.events.on("routeChangeError", handleStop);
|
||||||
|
|
||||||
console.log("_app", router.asPath);
|
|
||||||
return () => {
|
return () => {
|
||||||
router.events.off("routeChangeStart", handleStart);
|
router.events.off("routeChangeStart", handleStart);
|
||||||
router.events.off("routeChangeComplete", handleStop);
|
router.events.off("routeChangeComplete", handleStop);
|
||||||
|
@ -49,8 +48,6 @@ export default function CachingApp({ Component, pageProps }) {
|
||||||
const getLayout =
|
const getLayout =
|
||||||
Component.getLayout || ((page) => <DefaultLayout>{page}</DefaultLayout>);
|
Component.getLayout || ((page) => <DefaultLayout>{page}</DefaultLayout>);
|
||||||
|
|
||||||
console.log("_app loaded", router.asPath);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<style global jsx>{`
|
<style global jsx>{`
|
||||||
|
|
|
@ -10,7 +10,9 @@ import {
|
||||||
UnorderedList,
|
UnorderedList,
|
||||||
ListItem,
|
ListItem,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
|
import RouteButton from "../../src/components/RouteButton";
|
||||||
const Entry = () => {
|
const Entry = () => {
|
||||||
|
console.count("render stream!");
|
||||||
const ui = useContext(UIContext);
|
const ui = useContext(UIContext);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -49,6 +51,14 @@ const Entry = () => {
|
||||||
subscription screen
|
subscription screen
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</UnorderedList>
|
</UnorderedList>
|
||||||
|
<RouteButton
|
||||||
|
variant="solid"
|
||||||
|
size="md"
|
||||||
|
colorScheme="suggested"
|
||||||
|
href="/welcome"
|
||||||
|
>
|
||||||
|
Learn how to use moonstream
|
||||||
|
</RouteButton>
|
||||||
</Stack>
|
</Stack>
|
||||||
</>
|
</>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
@ -15,7 +15,7 @@ import {
|
||||||
ModalOverlay,
|
ModalOverlay,
|
||||||
ModalContent,
|
ModalContent,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import NewSubscription from "../src/components/NewSubscription";
|
import NewSubscription from "../src/components/NewModalSubscripton";
|
||||||
import { AiOutlinePlusCircle } from "react-icons/ai";
|
import { AiOutlinePlusCircle } from "react-icons/ai";
|
||||||
|
|
||||||
const Subscriptions = () => {
|
const Subscriptions = () => {
|
||||||
|
|
|
@ -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;
|
|
@ -1,13 +1,38 @@
|
||||||
const Spinner = {
|
const baseStyle = {
|
||||||
baseStyle: {
|
color: "primary.400",
|
||||||
color: "primary.400",
|
thickness: "4px",
|
||||||
thickness: "4px",
|
speed: "1.5s",
|
||||||
speed: "1.5s",
|
my: 8,
|
||||||
my: 8,
|
};
|
||||||
|
const variants = {
|
||||||
|
basic: { thickness: "4px", speed: "1.5s" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const sizes = {
|
||||||
|
xs: {
|
||||||
|
"--spinner-size": "0.75rem",
|
||||||
},
|
},
|
||||||
variants: {
|
sm: {
|
||||||
basic: { thickness: "4px", speed: "1.5s" },
|
"--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,
|
||||||
|
};
|
||||||
|
|
|
@ -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,
|
||||||
|
};
|
|
@ -8,6 +8,8 @@ import NumberInput from "./NumberInput";
|
||||||
import Badge from "./Badge";
|
import Badge from "./Badge";
|
||||||
import Checkbox from "./Checkbox";
|
import Checkbox from "./Checkbox";
|
||||||
import Table from "./Table";
|
import Table from "./Table";
|
||||||
|
import Tooltip from "./Tooltip";
|
||||||
|
import Spinner from "./Spinner";
|
||||||
import { createBreakpoints } from "@chakra-ui/theme-tools";
|
import { createBreakpoints } from "@chakra-ui/theme-tools";
|
||||||
|
|
||||||
const breakpointsCustom = createBreakpoints({
|
const breakpointsCustom = createBreakpoints({
|
||||||
|
@ -53,6 +55,8 @@ const theme = extendTheme({
|
||||||
Badge,
|
Badge,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Table,
|
Table,
|
||||||
|
Spinner,
|
||||||
|
Tooltip,
|
||||||
},
|
},
|
||||||
|
|
||||||
fonts: {
|
fonts: {
|
||||||
|
|
|
@ -15,6 +15,7 @@ import {
|
||||||
PopoverCloseButton,
|
PopoverCloseButton,
|
||||||
useBreakpointValue,
|
useBreakpointValue,
|
||||||
Spacer,
|
Spacer,
|
||||||
|
ButtonGroup,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import {
|
import {
|
||||||
HamburgerIcon,
|
HamburgerIcon,
|
||||||
|
@ -26,6 +27,7 @@ import { MdTimeline } from "react-icons/md";
|
||||||
import useRouter from "../core/hooks/useRouter";
|
import useRouter from "../core/hooks/useRouter";
|
||||||
import UIContext from "../core/providers/UIProvider/context";
|
import UIContext from "../core/providers/UIProvider/context";
|
||||||
import AccountIconButton from "./AccountIconButton";
|
import AccountIconButton from "./AccountIconButton";
|
||||||
|
import RouteButton from "./RouteButton";
|
||||||
|
|
||||||
const AppNavbar = () => {
|
const AppNavbar = () => {
|
||||||
const ui = useContext(UIContext);
|
const ui = useContext(UIContext);
|
||||||
|
@ -95,6 +97,14 @@ const AppNavbar = () => {
|
||||||
<Flex width="100%" px={2}>
|
<Flex width="100%" px={2}>
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<Flex placeSelf="flex-end">
|
<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 />
|
<SupportPopover />
|
||||||
<AccountIconButton
|
<AccountIconButton
|
||||||
colorScheme="primary"
|
colorScheme="primary"
|
||||||
|
|
|
@ -204,7 +204,6 @@ const EntriesNavigation = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const dropFilterArrayItem = (idx) => {
|
const dropFilterArrayItem = (idx) => {
|
||||||
console.log("dropFilterArrayItem", idx, filterState);
|
|
||||||
const oldArray = [...filterState];
|
const oldArray = [...filterState];
|
||||||
const newArray = oldArray.filter(function (ele) {
|
const newArray = oldArray.filter(function (ele) {
|
||||||
return ele != oldArray[idx];
|
return ele != oldArray[idx];
|
||||||
|
@ -242,7 +241,6 @@ const EntriesNavigation = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFilterStateCallback = (props) => {
|
const handleFilterStateCallback = (props) => {
|
||||||
console.log("handleFilterStateCallback", props);
|
|
||||||
const currentFilterState = [...filterState];
|
const currentFilterState = [...filterState];
|
||||||
currentFilterState.push({ ...props });
|
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
|
?.sort((a, b) => b.timestamp - a.timestamp) // TODO(Andrey) improve that for bi chunks of data sorting can take time
|
||||||
.map((entry, idx) => (
|
.map((entry, idx) => (
|
||||||
<StreamEntry
|
<StreamEntry
|
||||||
|
showOnboardingTooltips={false}
|
||||||
key={`entry-list-${idx}`}
|
key={`entry-list-${idx}`}
|
||||||
entry={entry}
|
entry={entry}
|
||||||
disableDelete={!canDelete}
|
disableDelete={!canDelete}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { HamburgerIcon } from "@chakra-ui/icons";
|
||||||
import useModals from "../core/hooks/useModals";
|
import useModals from "../core/hooks/useModals";
|
||||||
import UIContext from "../core/providers/UIProvider/context";
|
import UIContext from "../core/providers/UIProvider/context";
|
||||||
import ChakraAccountIconButton from "./AccountIconButton";
|
import ChakraAccountIconButton from "./AccountIconButton";
|
||||||
|
import RouteButton from "./RouteButton";
|
||||||
|
|
||||||
const LandingNavbar = () => {
|
const LandingNavbar = () => {
|
||||||
const ui = useContext(UIContext);
|
const ui = useContext(UIContext);
|
||||||
|
@ -52,6 +53,17 @@ const LandingNavbar = () => {
|
||||||
spacing={4}
|
spacing={4}
|
||||||
pr={16}
|
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 && (
|
{ui.isLoggedIn && (
|
||||||
<RouterLink href="/stream" passHref>
|
<RouterLink href="/stream" passHref>
|
||||||
<Button
|
<Button
|
||||||
|
|
|
@ -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;
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect, useCallback } from "react";
|
||||||
import { useSubscriptions } from "../core/hooks";
|
import { useSubscriptions } from "../core/hooks";
|
||||||
import {
|
import {
|
||||||
Input,
|
Input,
|
||||||
|
@ -8,23 +8,24 @@ import {
|
||||||
useRadioGroup,
|
useRadioGroup,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormErrorMessage,
|
FormErrorMessage,
|
||||||
ModalBody,
|
|
||||||
ModalCloseButton,
|
|
||||||
ModalHeader,
|
|
||||||
Button,
|
Button,
|
||||||
ModalFooter,
|
|
||||||
Spinner,
|
Spinner,
|
||||||
IconButton,
|
IconButton,
|
||||||
|
ButtonGroup,
|
||||||
|
Spacer,
|
||||||
|
Flex,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import RadioCard from "./RadioCard";
|
import RadioCard from "./RadioCard";
|
||||||
import { useForm } from "react-hook-form";
|
// import { useForm } from "react-hook-form";
|
||||||
import { GithubPicker } from "react-color";
|
import { CirclePicker } from "react-color";
|
||||||
import { BiRefresh } from "react-icons/bi";
|
import { BiRefresh } from "react-icons/bi";
|
||||||
import { makeColor } from "../core/utils/makeColor";
|
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 [color, setColor] = useState(makeColor());
|
||||||
const { typesCache, createSubscription } = useSubscriptions();
|
|
||||||
const { handleSubmit, errors, register } = useForm({});
|
const { handleSubmit, errors, register } = useForm({});
|
||||||
|
const { typesCache, createSubscription } = useSubscriptions();
|
||||||
|
// const { handleSubmit, errors, register } = useForm({});
|
||||||
const [radioState, setRadioState] = useState("ethereum_blockchain");
|
const [radioState, setRadioState] = useState("ethereum_blockchain");
|
||||||
let { getRootProps, getRadioProps } = useRadioGroup({
|
let { getRootProps, getRadioProps } = useRadioGroup({
|
||||||
name: "type",
|
name: "type",
|
||||||
|
@ -34,96 +35,105 @@ const NewSubscription = ({ isFreeOption, onClose }) => {
|
||||||
|
|
||||||
const group = getRootProps();
|
const group = getRootProps();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (setIsLoading) {
|
||||||
|
setIsLoading(createSubscription.isLoading);
|
||||||
|
}
|
||||||
|
}, [createSubscription.isLoading, setIsLoading]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (createSubscription.isSuccess) {
|
if (createSubscription.isSuccess) {
|
||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
}, [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) => {
|
if (typesCache.isLoading) return <Spinner />;
|
||||||
createSubscription.mutate({
|
|
||||||
...props,
|
|
||||||
color: color,
|
|
||||||
type: isFreeOption ? "free" : radioState,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleChangeColorComplete = (color) => {
|
const handleChangeColorComplete = (color) => {
|
||||||
setColor(color.hex);
|
setColor(color.hex);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
if (!errors) 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}>
|
return (
|
||||||
<HStack {...group} alignItems="stretch">
|
<form onSubmit={handleSubmit(createSubscriptionWrapper)}>
|
||||||
{typesCache.data.subscriptions.map((type) => {
|
<FormControl isInvalid={errors?.label}>
|
||||||
const radio = getRadioProps({
|
<Input
|
||||||
value: type.id,
|
my={2}
|
||||||
isDisabled:
|
type="text"
|
||||||
!type.active ||
|
autoComplete="off"
|
||||||
(isFreeOption &&
|
placeholder="Meaningful name of your subscription"
|
||||||
type.subscription_type !== "ethereum_blockchain"),
|
name="label"
|
||||||
});
|
ref={register({ required: "label is required!" })}
|
||||||
return (
|
></Input>
|
||||||
<RadioCard key={`subscription_type_${type.id}`} {...radio}>
|
<FormErrorMessage color="unsafe.400" pl="1">
|
||||||
{type.name}
|
{errors?.label && errors?.label.message}
|
||||||
</RadioCard>
|
</FormErrorMessage>
|
||||||
);
|
</FormControl>
|
||||||
})}
|
<FormControl isInvalid={errors?.address}>
|
||||||
</HStack>
|
<Input
|
||||||
<Input
|
type="text"
|
||||||
type="hidden"
|
autoComplete="off"
|
||||||
placeholder="subscription_type"
|
my={2}
|
||||||
name="subscription_type"
|
placeholder="Address to subscribe to"
|
||||||
ref={register({ required: "select type" })}
|
name="address"
|
||||||
value={radioState}
|
ref={register({ required: "address is required!" })}
|
||||||
onChange={() => null}
|
></Input>
|
||||||
></Input>
|
<FormErrorMessage color="unsafe.400" pl="1">
|
||||||
<FormErrorMessage color="unsafe.400" pl="1">
|
{errors?.address && errors?.address.message}
|
||||||
{errors.subscription_type && errors.subscription_type.message}
|
</FormErrorMessage>
|
||||||
</FormErrorMessage>
|
</FormControl>
|
||||||
</FormControl>
|
<Stack my={4} direction="column">
|
||||||
</Stack>
|
{/* <Text fontWeight="600">
|
||||||
<FormControl isInvalid={errors.color}>
|
{isFreeOption
|
||||||
<Stack direction="row" pb={2}>
|
? `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">
|
<Text fontWeight="600" alignSelf="center">
|
||||||
Label color
|
Label color
|
||||||
</Text>{" "}
|
</Text>{" "}
|
||||||
|
@ -147,18 +157,21 @@ const NewSubscription = ({ isFreeOption, onClose }) => {
|
||||||
w="200px"
|
w="200px"
|
||||||
></Input>
|
></Input>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
<Flex p={2}>
|
||||||
|
<CirclePicker
|
||||||
|
onChangeComplete={handleChangeColorComplete}
|
||||||
|
circleSpacing={1}
|
||||||
|
circleSize={24}
|
||||||
|
/>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
|
||||||
<GithubPicker
|
<FormErrorMessage color="unsafe.400" pl="1">
|
||||||
// color={this.state.background}
|
{errors?.color && errors?.color.message}
|
||||||
onChangeComplete={handleChangeColorComplete}
|
</FormErrorMessage>
|
||||||
/>
|
</FormControl>
|
||||||
|
|
||||||
<FormErrorMessage color="unsafe.400" pl="1">
|
<ButtonGroup direction="row" justifyContent="space-evenly">
|
||||||
{errors.color && errors.color.message}
|
|
||||||
</FormErrorMessage>
|
|
||||||
</FormControl>
|
|
||||||
</ModalBody>
|
|
||||||
<ModalFooter>
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
colorScheme="suggested"
|
colorScheme="suggested"
|
||||||
|
@ -166,12 +179,13 @@ const NewSubscription = ({ isFreeOption, onClose }) => {
|
||||||
>
|
>
|
||||||
Confirm
|
Confirm
|
||||||
</Button>
|
</Button>
|
||||||
|
<Spacer />
|
||||||
<Button colorScheme="gray" onClick={onClose}>
|
<Button colorScheme="gray" onClick={onClose}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</ButtonGroup>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default NewSubscription;
|
export default _NewSubscription;
|
||||||
|
|
|
@ -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;
|
|
@ -14,6 +14,7 @@ import React from "react";
|
||||||
import { HamburgerIcon, ArrowLeftIcon, ArrowRightIcon } from "@chakra-ui/icons";
|
import { HamburgerIcon, ArrowLeftIcon, ArrowRightIcon } from "@chakra-ui/icons";
|
||||||
import { MdTimeline, MdSettings } from "react-icons/md";
|
import { MdTimeline, MdSettings } from "react-icons/md";
|
||||||
import { ImStatsBars } from "react-icons/im";
|
import { ImStatsBars } from "react-icons/im";
|
||||||
|
import { HiAcademicCap } from "react-icons/hi";
|
||||||
|
|
||||||
const Sidebar = () => {
|
const Sidebar = () => {
|
||||||
const ui = useContext(UIContext);
|
const ui = useContext(UIContext);
|
||||||
|
@ -79,6 +80,13 @@ const Sidebar = () => {
|
||||||
<RouterLink href="/subscriptions">Subscriptions </RouterLink>
|
<RouterLink href="/subscriptions">Subscriptions </RouterLink>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
|
{ui.isMobileView && (
|
||||||
|
<Menu iconShape="square">
|
||||||
|
<MenuItem icon={<HiAcademicCap />}>
|
||||||
|
<RouterLink href="/welcome">Learn how to</RouterLink>
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
|
)}
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
)}
|
)}
|
||||||
{!ui.isLoggedIn && (
|
{!ui.isLoggedIn && (
|
||||||
|
|
|
@ -36,17 +36,16 @@ const SteamEntryDetails = () => {
|
||||||
>
|
>
|
||||||
<HStack id="EntryHeader" width="100%" m={0}>
|
<HStack id="EntryHeader" width="100%" m={0}>
|
||||||
<Heading
|
<Heading
|
||||||
overflow="hidden"
|
|
||||||
width={entry?.context_url ? "calc(100% - 28px)" : "100%"}
|
width={entry?.context_url ? "calc(100% - 28px)" : "100%"}
|
||||||
minH="36px"
|
minH="36px"
|
||||||
style={{ marginLeft: "0" }}
|
style={{ marginLeft: "0" }}
|
||||||
m={0}
|
m={0}
|
||||||
p={0}
|
p={0}
|
||||||
fontWeight="600"
|
fontWeight="600"
|
||||||
fontSize="1.5rem"
|
fontSize="md"
|
||||||
textAlign="left"
|
textAlign="left"
|
||||||
>
|
>
|
||||||
{entry && entry.tx.hash}
|
{entry && `Hash: ${entry.tx.hash}`}
|
||||||
</Heading>
|
</Heading>
|
||||||
</HStack>
|
</HStack>
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
|
|
|
@ -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;
|
|
@ -11,6 +11,7 @@ import {
|
||||||
useMediaQuery,
|
useMediaQuery,
|
||||||
Spacer,
|
Spacer,
|
||||||
Spinner,
|
Spinner,
|
||||||
|
chakra,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { ArrowRightIcon } from "@chakra-ui/icons";
|
import { ArrowRightIcon } from "@chakra-ui/icons";
|
||||||
|
@ -18,7 +19,7 @@ import UIContext from "../core/providers/UIProvider/context";
|
||||||
import { useToast } from "../core/hooks";
|
import { useToast } from "../core/hooks";
|
||||||
import { useSubscriptions } from "../core/hooks";
|
import { useSubscriptions } from "../core/hooks";
|
||||||
|
|
||||||
const StreamEntry = ({ entry }) => {
|
const StreamEntry = ({ entry, showOnboardingTooltips, className }) => {
|
||||||
const { subscriptionsCache } = useSubscriptions();
|
const { subscriptionsCache } = useSubscriptions();
|
||||||
const ui = useContext(UIContext);
|
const ui = useContext(UIContext);
|
||||||
const [copyString, setCopyString] = useState(false);
|
const [copyString, setCopyString] = useState(false);
|
||||||
|
@ -49,6 +50,7 @@ const StreamEntry = ({ entry }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
|
className={className}
|
||||||
p={0}
|
p={0}
|
||||||
m={1}
|
m={1}
|
||||||
mr={2}
|
mr={2}
|
||||||
|
@ -89,35 +91,44 @@ const StreamEntry = ({ entry }) => {
|
||||||
overflowX="hidden"
|
overflowX="hidden"
|
||||||
overflowY="visible"
|
overflowY="visible"
|
||||||
>
|
>
|
||||||
<Stack
|
<Tooltip
|
||||||
className="title"
|
hasArrow
|
||||||
direction="row"
|
isOpen={showOnboardingTooltips}
|
||||||
w="100%"
|
// shouldWrapChildren
|
||||||
h="1.6rem"
|
label="Top of card describes type of event. Ethereum blockchain in this case. It as unique hash shown here"
|
||||||
minH="1.6rem"
|
variant="onboarding"
|
||||||
textAlign="center"
|
placement="top"
|
||||||
spacing={0}
|
|
||||||
alignItems="center"
|
|
||||||
bgColor="gray.300"
|
|
||||||
>
|
>
|
||||||
<Image
|
<Stack
|
||||||
boxSize="16px"
|
className="title"
|
||||||
src={
|
direction="row"
|
||||||
"https://upload.wikimedia.org/wikipedia/commons/0/05/Ethereum_logo_2014.svg"
|
w="100%"
|
||||||
}
|
h="1.6rem"
|
||||||
/>
|
minH="1.6rem"
|
||||||
<Heading px={1} size="xs">
|
textAlign="center"
|
||||||
Hash
|
spacing={0}
|
||||||
</Heading>
|
alignItems="center"
|
||||||
<Spacer />
|
bgColor="gray.300"
|
||||||
<Text
|
|
||||||
isTruncated
|
|
||||||
onClick={() => setCopyString(entry.hash)}
|
|
||||||
pr={12}
|
|
||||||
>
|
>
|
||||||
{entry.hash}
|
<Image
|
||||||
</Text>
|
boxSize="16px"
|
||||||
</Stack>
|
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
|
<Stack
|
||||||
className="CardAddressesRow"
|
className="CardAddressesRow"
|
||||||
direction={showFullView ? "row" : "column"}
|
direction={showFullView ? "row" : "column"}
|
||||||
|
@ -143,16 +154,25 @@ const StreamEntry = ({ entry }) => {
|
||||||
placeContent="center"
|
placeContent="center"
|
||||||
spacing={0}
|
spacing={0}
|
||||||
>
|
>
|
||||||
<Text
|
<Tooltip
|
||||||
bgColor="gray.600"
|
hasArrow
|
||||||
h="100%"
|
isOpen={showOnboardingTooltips && !ui.isMobileView}
|
||||||
fontSize="sm"
|
label="From and to addresses, clicking names will copy address to clipboard!"
|
||||||
py="2px"
|
variant="onboarding"
|
||||||
px={2}
|
placement={ui.isMobileView ? "bottom" : "left"}
|
||||||
w={showFullView ? null : "120px"}
|
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:">
|
<Tooltip label={entry.from_address} aria-label="From:">
|
||||||
<Text
|
<Text
|
||||||
mx={0}
|
mx={0}
|
||||||
|
@ -168,6 +188,7 @@ const StreamEntry = ({ entry }) => {
|
||||||
</Text>
|
</Text>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Stack
|
<Stack
|
||||||
overflow="hidden"
|
overflow="hidden"
|
||||||
textOverflow="ellipsis"
|
textOverflow="ellipsis"
|
||||||
|
@ -322,22 +343,33 @@ const StreamEntry = ({ entry }) => {
|
||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
<Flex>
|
<Flex>
|
||||||
<IconButton
|
<Tooltip
|
||||||
m={0}
|
hasArrow
|
||||||
onClick={() => ui.setCurrentTransaction(entry)}
|
isOpen={showOnboardingTooltips}
|
||||||
h="full"
|
placement={ui.isMobileView ? "bottom" : "right"}
|
||||||
// minH="24px"
|
label="Clicking side arrow will bring up detailed view"
|
||||||
borderLeftRadius={0}
|
variant="onboarding"
|
||||||
variant="solid"
|
maxW="150px"
|
||||||
px={0}
|
>
|
||||||
minW="24px"
|
<IconButton
|
||||||
colorScheme="secondary"
|
m={0}
|
||||||
icon={<ArrowRightIcon w="24px" />}
|
onClick={() => ui.setCurrentTransaction(entry)}
|
||||||
/>
|
h="full"
|
||||||
|
// minH="24px"
|
||||||
|
borderLeftRadius={0}
|
||||||
|
variant="solid"
|
||||||
|
px={0}
|
||||||
|
minW="24px"
|
||||||
|
colorScheme="secondary"
|
||||||
|
icon={<ArrowRightIcon w="24px" />}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default StreamEntry;
|
const StreamChakraEntry = chakra(StreamEntry);
|
||||||
|
|
||||||
|
export default StreamChakraEntry;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Skeleton, IconButton } from "@chakra-ui/react";
|
import { Skeleton, IconButton, Container } from "@chakra-ui/react";
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
Th,
|
Th,
|
||||||
|
@ -12,6 +12,7 @@ import {
|
||||||
EditableInput,
|
EditableInput,
|
||||||
Image,
|
Image,
|
||||||
EditablePreview,
|
EditablePreview,
|
||||||
|
Button,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import { DeleteIcon } from "@chakra-ui/icons";
|
import { DeleteIcon } from "@chakra-ui/icons";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
|
@ -20,7 +21,7 @@ import { useSubscriptions } from "../core/hooks";
|
||||||
import ConfirmationRequest from "./ConfirmationRequest";
|
import ConfirmationRequest from "./ConfirmationRequest";
|
||||||
import ColorSelector from "./ColorSelector";
|
import ColorSelector from "./ColorSelector";
|
||||||
|
|
||||||
const SubscriptionsList = () => {
|
const SubscriptionsList = ({ emptyCTA }) => {
|
||||||
const { subscriptionsCache, updateSubscription, deleteSubscription } =
|
const { subscriptionsCache, updateSubscription, deleteSubscription } =
|
||||||
useSubscriptions();
|
useSubscriptions();
|
||||||
|
|
||||||
|
@ -31,7 +32,10 @@ const SubscriptionsList = () => {
|
||||||
updateSubscription.mutate(data);
|
updateSubscription.mutate(data);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (subscriptionsCache.data) {
|
if (
|
||||||
|
subscriptionsCache.data &&
|
||||||
|
subscriptionsCache.data.subscriptions.length > 0
|
||||||
|
) {
|
||||||
return (
|
return (
|
||||||
<Table
|
<Table
|
||||||
borderColor="gray.200"
|
borderColor="gray.200"
|
||||||
|
@ -138,6 +142,16 @@ const SubscriptionsList = () => {
|
||||||
</Tbody>
|
</Tbody>
|
||||||
</Table>
|
</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) {
|
} else if (subscriptionsCache.isLoading) {
|
||||||
return <Skeleton />;
|
return <Skeleton />;
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -16,7 +16,6 @@ const toEth = (wei) => {
|
||||||
const TxABI = (props) => {
|
const TxABI = (props) => {
|
||||||
const byteCode = props.byteCode;
|
const byteCode = props.byteCode;
|
||||||
const abi = props.abi;
|
const abi = props.abi;
|
||||||
console.log(abi.functions);
|
|
||||||
return (
|
return (
|
||||||
<VStack spacing={3}>
|
<VStack spacing={3}>
|
||||||
<br />
|
<br />
|
||||||
|
@ -58,25 +57,29 @@ const TxInfo = (props) => {
|
||||||
return (
|
return (
|
||||||
<Box boxShadow="xs" p="6" rounded="md">
|
<Box boxShadow="xs" p="6" rounded="md">
|
||||||
<StatGroup>
|
<StatGroup>
|
||||||
<Stat>
|
<Stat px={2}>
|
||||||
<StatLabel>Value</StatLabel>
|
<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>
|
<StatHelpText>amount of ETH to transfer</StatHelpText>
|
||||||
</Stat>
|
</Stat>
|
||||||
<Stat>
|
<Stat>
|
||||||
<StatLabel>Gas limit</StatLabel>
|
<StatLabel>Gas limit</StatLabel>
|
||||||
<StatNumber>{transaction.tx.gas}</StatNumber>
|
<StatNumber fontSize="md">{transaction.tx.gas}</StatNumber>
|
||||||
<StatHelpText>Maximum amount of gas </StatHelpText>
|
<StatHelpText>Maximum amount of gas </StatHelpText>
|
||||||
<StatHelpText>provided for the transaction</StatHelpText>
|
<StatHelpText>provided for the transaction</StatHelpText>
|
||||||
</Stat>
|
</Stat>
|
||||||
<Stat>
|
<Stat>
|
||||||
<StatLabel>Gas price</StatLabel>
|
<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>
|
<StatHelpText>the fee the sender pays per unit of gas</StatHelpText>
|
||||||
</Stat>
|
</Stat>
|
||||||
</StatGroup>
|
</StatGroup>
|
||||||
|
|
||||||
<Table variant="simple">
|
<Table variant="simple" size="sm">
|
||||||
<Tbody>
|
<Tbody>
|
||||||
{Object.keys(transaction.tx)
|
{Object.keys(transaction.tx)
|
||||||
.filter(dont_display)
|
.filter(dont_display)
|
||||||
|
@ -85,7 +88,7 @@ const TxInfo = (props) => {
|
||||||
transaction.tx[key] != undefined && (
|
transaction.tx[key] != undefined && (
|
||||||
<Tr key={key}>
|
<Tr key={key}>
|
||||||
<Td>{key}</Td>
|
<Td>{key}</Td>
|
||||||
<Td>{transaction.tx[key]}</Td>
|
<Td wordBreak="break-all">{transaction.tx[key]}</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import { useEffect } from "react";
|
import { useContext } from "react";
|
||||||
import { useMutation } from "react-query";
|
import { useMutation } from "react-query";
|
||||||
import { AuthService } from "../services";
|
import { AuthService } from "../services";
|
||||||
import { useUser, useToast, useInviteAccept, useRouter, useAnalytics } from ".";
|
import { useUser, useToast, useInviteAccept, useRouter, useAnalytics } from ".";
|
||||||
|
import UIContext from "../providers/UIProvider/context";
|
||||||
|
|
||||||
const useSignUp = (source) => {
|
const useSignUp = (source) => {
|
||||||
|
const ui = useContext(UIContext);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { getUser } = useUser();
|
const { getUser } = useUser();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
@ -30,28 +32,16 @@ const useSignUp = (source) => {
|
||||||
{ full_url: router.nextRouter.asPath, code: 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) => {
|
onError: (error) => {
|
||||||
toast(error, "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 {
|
return {
|
||||||
signUp,
|
signUp,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
|
|
@ -5,6 +5,7 @@ export const MIXPANEL_PROPS = {
|
||||||
PRICING_PLAN: "Pricing Plan",
|
PRICING_PLAN: "Pricing Plan",
|
||||||
TOGGLE_LEFT_SIDEBAR: "Toggle left sidebar",
|
TOGGLE_LEFT_SIDEBAR: "Toggle left sidebar",
|
||||||
BUTTON_NAME: "Button",
|
BUTTON_NAME: "Button",
|
||||||
|
USER_SPECIALITY: "user speciality",
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MIXPANEL_EVENTS = {
|
export const MIXPANEL_EVENTS = {
|
||||||
|
|
|
@ -17,15 +17,7 @@ const UIProvider = ({ children }) => {
|
||||||
xl: false,
|
xl: false,
|
||||||
"2xl": false,
|
"2xl": false,
|
||||||
});
|
});
|
||||||
|
// const isMobileView = true;
|
||||||
const currentBreakpoint = useBreakpointValue({
|
|
||||||
base: 0,
|
|
||||||
sm: 1,
|
|
||||||
md: 2,
|
|
||||||
lg: 3,
|
|
||||||
xl: 4,
|
|
||||||
"2xl": 5,
|
|
||||||
});
|
|
||||||
|
|
||||||
const { modal, toggleModal } = useContext(ModalContext);
|
const { modal, toggleModal } = useContext(ModalContext);
|
||||||
const [searchTerm, setSearchTerm] = useQuery("q", "", true, false);
|
const [searchTerm, setSearchTerm] = useQuery("q", "", true, false);
|
||||||
|
@ -44,7 +36,8 @@ const UIProvider = ({ children }) => {
|
||||||
router.nextRouter.asPath.includes("/stream") ||
|
router.nextRouter.asPath.includes("/stream") ||
|
||||||
router.nextRouter.asPath.includes("/account") ||
|
router.nextRouter.asPath.includes("/account") ||
|
||||||
router.nextRouter.asPath.includes("/subscriptions") ||
|
router.nextRouter.asPath.includes("/subscriptions") ||
|
||||||
router.nextRouter.asPath.includes("/analytics")
|
router.nextRouter.asPath.includes("/analytics") ||
|
||||||
|
router.nextRouter.asPath.includes("/welcome")
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -81,7 +74,8 @@ const UIProvider = ({ children }) => {
|
||||||
router.nextRouter.asPath.includes("/stream") ||
|
router.nextRouter.asPath.includes("/stream") ||
|
||||||
router.nextRouter.asPath.includes("/account") ||
|
router.nextRouter.asPath.includes("/account") ||
|
||||||
router.nextRouter.asPath.includes("/subscriptions") ||
|
router.nextRouter.asPath.includes("/subscriptions") ||
|
||||||
router.nextRouter.asPath.includes("/analytics")
|
router.nextRouter.asPath.includes("/analytics") ||
|
||||||
|
router.nextRouter.asPath.includes("/welcome")
|
||||||
);
|
);
|
||||||
}, [router.nextRouter.asPath, user]);
|
}, [router.nextRouter.asPath, user]);
|
||||||
|
|
||||||
|
@ -119,7 +113,7 @@ const UIProvider = ({ children }) => {
|
||||||
//Sidebar is visible at at breakpoint value less then 2
|
//Sidebar is visible at at breakpoint value less then 2
|
||||||
//Sidebar is visible always in appView
|
//Sidebar is visible always in appView
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentBreakpoint < 2) {
|
if (isMobileView) {
|
||||||
setSidebarVisible(true);
|
setSidebarVisible(true);
|
||||||
setSidebarCollapsed(false);
|
setSidebarCollapsed(false);
|
||||||
} else {
|
} else {
|
||||||
|
@ -130,7 +124,7 @@ const UIProvider = ({ children }) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [currentBreakpoint, isAppView]);
|
}, [isMobileView, isAppView]);
|
||||||
|
|
||||||
// *********** Entries layout states **********************
|
// *********** Entries layout states **********************
|
||||||
|
|
||||||
|
@ -159,6 +153,81 @@ const UIProvider = ({ children }) => {
|
||||||
);
|
);
|
||||||
}, [isEntryDetailView, isMobileView]);
|
}, [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 (
|
return (
|
||||||
|
@ -188,6 +257,12 @@ const UIProvider = ({ children }) => {
|
||||||
currentTransaction,
|
currentTransaction,
|
||||||
setCurrentTransaction,
|
setCurrentTransaction,
|
||||||
isEntryDetailView,
|
isEntryDetailView,
|
||||||
|
onboardingStep,
|
||||||
|
setOnboardingStep,
|
||||||
|
isOnboardingComplete,
|
||||||
|
setisOnboardingComplete,
|
||||||
|
onboardingSteps,
|
||||||
|
setOnboardingState,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
Ładowanie…
Reference in New Issue