Merge branch 'main' into whale-watch

pull/105/head
Neeraj Kashyap 2021-08-13 12:37:55 -07:00
commit d55f25d5e3
63 zmienionych plików z 2144 dodań i 950 usunięć

Wyświetl plik

@ -19,7 +19,7 @@ jobs:
run: pip install -e .[dev]
# - name: Mypy type check
# working-directory: ./crawlers
# run: mypy moonstreamcrawlers/
# run: mypy mooncrawl/
- name: Black syntax check
working-directory: ./crawlers
run: black --check moonstreamcrawlers/
run: black --check mooncrawl/

Wyświetl plik

@ -4,6 +4,11 @@ on:
push:
paths:
- "frontend/**"
pull_request:
branches:
- "main"
paths:
- "frontend/**"
jobs:
build:

Wyświetl plik

@ -1,2 +1,59 @@
# moonstock
The Bugout blockchain inspector
# moonstream
\[[Live at https://moonstream.to/](https://moonstream.to)\] | \[[Join us on Discord](https://discord.gg/pYE65FuNSz)\]
## What is Moonstream?
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.
Moonstream users can subscribe to events from any blockchain - from the activity of specific accounts
or smart contracts to updates about general market movements. This information comes from the blockchains
themselves, from their mempools/transaction pools, and from centralized exchanges, social media, and
the news. This forms a stream of information tailored to their specific needs.
They can use this information to execute transactions directly from the Moonstream frontend or they
can set up programs which execute (on- or off-chain) when their stream meets certain conditions.
## Who uses Moonstream?
1. **Development teams deploying decentralized applications.** They use Moonstream to analyze how
users are calling their dapps, and set up alerts for suspicious activity.
2. **Algorithmic funds.** They use Moonstream to execute transactions directly on-chain under
prespecified conditions.
3. **Crypto traders.** They use Moonstream to evaluate trading strategies based on data from
centralized exchanges, the blockchain, and the transaction pool.
## Free software
Proprietary technologies are not inclusive technologies, and we believe in inclusion.
All of our technology is open source. This repository contains all the code that powers
https://moonstream.to. The code is licensed with the [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0).
You are and _will always be_ free to host your own instance of Moonstream.
## Architecture
This monorepo contains the following components:
1. [`frontend`](./frontend): A web frontend for Moonstream. Allows users to perform API operations
through a visual interface. The frontend also offers charting and analysis functionality. Built
in [React](https://reactjs.org/).
2. [`backend`'](./backend): The Moonstream API. This portion of the code base implements a REST API
through which users can manage the events that show up in their stream and actually consume their
stream data. Built in [Python](https://www.python.org/) using [Fast API](https://fastapi.tiangolo.com/).
3. [`crawlers`](./crawlers): This part of the code base contains workers which extract data from
blockchains, transaction pools, and other sources. Currently contains a single [Python](https://www.python.org/)
package but we will soon be addding crawlers implemented in other languages: [Go](https://golang.org/),
[Rust](https://www.rust-lang.org/)), and [Javascript](https://developer.mozilla.org/en-US/docs/Web/JavaScript).
4. [`db`](./db): Moonstream stores blockchain data in [Postgres](https://www.postgresql.org/). This
directory contains the code we use to manage the schema in our Postgres database. For sources that
send higher volumes of data, we use a separate Postgres database and interface with it using
[Bugout](https://bugout.dev). For more information on how that data is processed, check how the API
inserts events from those sources into a stream.
## Contributing
If you would like to contribute to Moonstream, please reach out to @zomglings on the [Moonstream Discord](https://discord.gg/pYE65FuNSz).

Wyświetl plik

@ -1,7 +1,7 @@
"""
Pydantic schemas for the Moonstream HTTP API
"""
from typing import List, Optional
from typing import List, Optional, Dict, Any
from pydantic import BaseModel, Field
@ -22,8 +22,8 @@ class SubscriptionTypesListResponse(BaseModel):
class SubscriptionResourceData(BaseModel):
id: str
address: str
color: str
label: str
color: Optional[str]
label: Optional[str]
user_id: str
subscription_type_id: str
@ -51,21 +51,9 @@ class VersionResponse(BaseModel):
version: str
class SubscriptionRequest(BaseModel):
"""
Schema for data retrieving from frontend about subscription.
"""
blockchain: str
class SubscriptionResponse(BaseModel):
"""
User subscription storing in Bugout resources.
"""
user_id: str
blockchain: str
class SubscriptionUpdate(BaseModel):
update: Dict[str, Any]
drop_keys: List[str] = Field(default_factory=list)
class SubscriptionsListResponse(BaseModel):

Wyświetl plik

@ -58,8 +58,8 @@ async def search_transactions(
q: str = Query(""),
start_time: Optional[int] = Query(0),
end_time: Optional[int] = Query(0),
include_start: bool = Query(False),
include_end: bool = Query(False),
include_start: Optional[bool] = Query(False),
include_end: Optional[bool] = Query(False),
db_session: Session = Depends(db.yield_db_session),
):
# get user subscriptions

Wyświetl plik

@ -2,7 +2,7 @@
The Moonstream subscriptions HTTP API
"""
import logging
from typing import Dict, List
from typing import Dict, List, Optional
from bugout.data import BugoutResource, BugoutResources
from bugout.exceptions import BugoutResponseException
@ -183,6 +183,54 @@ async def get_subscriptions_handler(request: Request) -> data.SubscriptionsListR
)
@app.put(
"/{subscription_id}",
tags=["subscriptions"],
response_model=data.SubscriptionResourceData,
)
async def update_subscriptions_handler(
request: Request,
subscription_id: str,
color: Optional[str] = Form(None),
label: Optional[str] = Form(None),
) -> data.SubscriptionResourceData:
"""
Get user's subscriptions.
"""
token = request.state.token
update = {}
if color:
update["color"] = color
if label:
update["label"] = label
try:
resource: BugoutResource = bc.update_resource(
token=token,
resource_id=subscription_id,
resource_data=data.SubscriptionUpdate(
update=update,
).dict(),
)
except BugoutResponseException as e:
raise HTTPException(status_code=e.status_code, detail=e.detail)
except Exception as e:
raise HTTPException(status_code=500)
return data.SubscriptionResourceData(
id=str(resource.id),
user_id=resource.resource_data["user_id"],
address=resource.resource_data["address"],
color=resource.resource_data["color"],
label=resource.resource_data["label"],
subscription_type_id=resource.resource_data["subscription_type_id"],
)
@app.get(
"/types", tags=["subscriptions"], response_model=data.SubscriptionTypesListResponse
)

Wyświetl plik

@ -14,7 +14,7 @@ from fastapi import (
)
from fastapi.middleware.cors import CORSMiddleware
from moonstreamdb.db import yield_db_session
from moonstreamdb.models import EthereumSmartContract
from moonstreamdb.models import EthereumAddress
from sqlalchemy.orm import Session
from ..abi_decoder import decode_abi
@ -79,8 +79,8 @@ async def txinfo_ethereum_blockchain_handler(
response.errors.append("Could not decode ABI from the given input")
smart_contract = (
db_session.query(EthereumSmartContract)
.filter(EthereumSmartContract.transaction_hash == txinfo_request.tx.hash)
db_session.query(EthereumAddress)
.filter(EthereumAddress.transaction_hash == txinfo_request.tx.hash)
.one_or_none()
)

Wyświetl plik

@ -80,6 +80,8 @@ async def get_transaction_in_blocks(
.filter(filters)
)
ethereum_transactions = ethereum_transactions_in_subscriptions
# If not start_time and end_time not present
# Get latest transaction
if boundaries.end_time == 0:
@ -88,6 +90,7 @@ async def get_transaction_in_blocks(
text("timestamp desc")
).limit(1)
).one_or_none()
if ethereum_transaction_start_point:
boundaries.end_time = ethereum_transaction_start_point[-1]
boundaries.start_time = (
ethereum_transaction_start_point[-1] - DEFAULT_STREAM_TIMEINTERVAL
@ -101,7 +104,7 @@ async def get_transaction_in_blocks(
)
if boundaries.end_time:
ethereum_transactions = ethereum_transactions_in_subscriptions.filter(
ethereum_transactions = ethereum_transactions.filter(
include_or_not_lower(
EthereumBlock.timestamp, boundaries.include_end, boundaries.end_time
)

Wyświetl plik

@ -3,7 +3,7 @@ asgiref==3.4.1
black==21.7b0
boto3==1.18.1
botocore==1.21.1
bugout==0.1.15
bugout==0.1.16
certifi==2021.5.30
charset-normalizer==2.0.3
click==8.0.1
@ -11,7 +11,7 @@ fastapi==0.66.0
h11==0.12.0
idna==3.2
jmespath==0.10.0
-e git+https://git@github.com/bugout-dev/moonstream.git@ec3278e192119d1e8a273cfaab6cb53890d2e8e9#egg=moonstreamdb&subdirectory=db
-e git+https://git@github.com/bugout-dev/moonstream.git@39d2b8e36a49958a9ae085ec2cc1be3fc732b9d0#egg=moonstreamdb&subdirectory=db
mypy==0.910
mypy-extensions==0.4.3
pathspec==0.9.0

Wyświetl plik

@ -10,7 +10,7 @@ setup(
name="moonstream",
version=MOONSTREAM_VERSION,
packages=find_packages(),
install_requires=["boto3", "bugout >= 0.1.15", "fastapi", "uvicorn"],
install_requires=["boto3", "bugout >= 0.1.16", "fastapi", "uvicorn"],
extras_require={
"dev": ["black", "mypy"],
"distribute": ["setuptools", "twine", "wheel"],

Wyświetl plik

@ -1,4 +1,4 @@
# moonstream crawlers
# Moonstream Crawlers
## Installation
@ -24,13 +24,13 @@ This crawler retrieves Ethereum function signatures from the Ethereum Signature
#### Crawling ESD function signatures
```bash
python -m moonstreamcrawlers.esd --interval 0.3 functions
python -m mooncrawl.esd --interval 0.3 functions
```
#### Crawling ESD event signatures
```bash
python -m moonstreamcrawlers.esd --interval 0.3 events
python -m mooncrawl.esd --interval 0.3 events
```
### Ethereum contract registrar
@ -41,17 +41,17 @@ addresses from transaction receipts.
To run this crawler:
```bash
python -m moonstreamcrawlers.cli ethcrawler contracts update
python -m mooncrawl.cli ethcrawler contracts update
```
Output is JSON list of pairs `[..., (<transaction_hash>, <contract_address>), ...]`, so you can pipe to `jq`:
```bash
python -m moonstreamcrawlers.cli ethcrawler contracts update | jq .
python -m mooncrawl.cli ethcrawler contracts update | jq .
```
You can also specify an output file:
```bash
python -m moonstreamcrawlers.cli ethcrawler contracts update -o new_contracts.json
python -m mooncrawl.cli ethcrawler contracts update -o new_contracts.json
```

Wyświetl plik

@ -0,0 +1,11 @@
[Unit]
Description=Load trending Ethereum addresses to the database
After=network.target
[Service]
Type=oneshot
User=ubuntu
Group=www-data
WorkingDirectory=/home/ubuntu/moonstream/crawlers
EnvironmentFile=/home/ubuntu/mooncrawl-secrets/app.env
ExecStart=/usr/bin/bash -c '/home/ubuntu/mooncrawl-env/bin/python -m mooncrawl.ethcrawler trending'

Wyświetl plik

@ -0,0 +1,9 @@
[Unit]
Description=Load trending Ethereum addresses to the database every 5 minutes
[Timer]
OnBootSec=10s
OnUnitActiveSec=5m
[Install]
WantedBy=timers.target

Wyświetl plik

@ -2,20 +2,28 @@
Moonstream crawlers CLI.
"""
import argparse
from datetime import datetime, timedelta, timezone
from enum import Enum
import json
import os
import sys
import time
from typing import Iterator, List
import dateutil.parser
from .ethereum import (
crawl_blocks_executor,
crawl_blocks,
check_missing_blocks,
get_latest_blocks,
process_contract_deployments,
DateRange,
trending,
)
from .publish import publish_json
from .settings import MOONSTREAM_CRAWL_WORKERS
from .version import MOONCRAWL_VERSION
class ProcessingOrder(Enum):
@ -82,9 +90,7 @@ def ethcrawler_blocks_sync_handler(args: argparse.Namespace) -> None:
"""
starting_block: int = args.start
while True:
bottom_block_number, top_block_number = get_latest_blocks(
with_transactions=not args.notransactions
)
bottom_block_number, top_block_number = get_latest_blocks(args.confirmations)
bottom_block_number = max(bottom_block_number + 1, starting_block)
if bottom_block_number >= top_block_number:
print(
@ -166,21 +172,41 @@ def ethcrawler_contracts_update_handler(args: argparse.Namespace) -> None:
json.dump(results, args.outfile)
def ethcrawler_trending_handler(args: argparse.Namespace) -> None:
date_range = DateRange(
start_time=args.start,
end_time=args.end,
include_start=args.include_start,
include_end=args.include_end,
)
results = trending(date_range)
humbug_token = args.humbug
if humbug_token is None:
humbug_token = os.environ.get("MOONSTREAM_HUMBUG_TOKEN")
if humbug_token:
opening_bracket = "[" if args.include_start else "("
closing_bracket = "]" if args.include_end else ")"
title = f"Ethereum trending addresses: {opening_bracket}{args.start}, {args.end}{closing_bracket}"
publish_json(
"ethereum_trending",
humbug_token,
title,
results,
tags=[f"crawler_version:{MOONCRAWL_VERSION}"],
)
with args.outfile as ofp:
json.dump(results, ofp)
def main() -> None:
parser = argparse.ArgumentParser(description="Moonstream crawlers CLI")
parser.set_defaults(func=lambda _: parser.print_help())
subcommands = parser.add_subparsers(description="Crawlers commands")
parser_ethcrawler = subcommands.add_parser(
"ethcrawler", description="Ethereum crawler"
)
parser_ethcrawler.set_defaults(func=lambda _: parser_ethcrawler.print_help())
subcommands_ethcrawler = parser_ethcrawler.add_subparsers(
description="Ethereum crawler commands"
)
time_now = datetime.now(timezone.utc)
# Ethereum blocks parser
parser_ethcrawler_blocks = subcommands_ethcrawler.add_parser(
parser_ethcrawler_blocks = subcommands.add_parser(
"blocks", description="Ethereum blocks commands"
)
parser_ethcrawler_blocks.set_defaults(
@ -218,6 +244,13 @@ def main() -> None:
default=0,
help="(Optional) Block to start synchronization from. Default: 0",
)
parser_ethcrawler_blocks_sync.add_argument(
"-c",
"--confirmations",
type=int,
default=0,
help="Number of confirmations we require before storing a block in the database. (Default: 0)",
)
parser_ethcrawler_blocks_sync.add_argument(
"--order",
type=processing_order,
@ -284,7 +317,7 @@ def main() -> None:
func=ethcrawler_blocks_missing_handler
)
parser_ethcrawler_contracts = subcommands_ethcrawler.add_parser(
parser_ethcrawler_contracts = subcommands.add_parser(
"contracts", description="Ethereum smart contract related crawlers"
)
parser_ethcrawler_contracts.set_defaults(
@ -309,6 +342,51 @@ def main() -> None:
func=ethcrawler_contracts_update_handler
)
parser_ethcrawler_trending = subcommands.add_parser(
"trending", description="Trending addresses on the Ethereum blockchain"
)
parser_ethcrawler_trending.add_argument(
"-s",
"--start",
type=dateutil.parser.parse,
default=(time_now - timedelta(hours=1, minutes=0)).isoformat(),
help=f"Start time for window to calculate trending addresses in (default: {(time_now - timedelta(hours=1,minutes=0)).isoformat()})",
)
parser_ethcrawler_trending.add_argument(
"--include-start",
action="store_true",
help="Set this flag if range should include start time",
)
parser_ethcrawler_trending.add_argument(
"-e",
"--end",
type=dateutil.parser.parse,
default=time_now.isoformat(),
help=f"End time for window to calculate trending addresses in (default: {time_now.isoformat()})",
)
parser_ethcrawler_trending.add_argument(
"--include-end",
action="store_true",
help="Set this flag if range should include end time",
)
parser_ethcrawler_trending.add_argument(
"--humbug",
default=None,
help=(
"If you would like to write this data to a Moonstream journal, please provide a Humbug "
"token for that here. (This argument overrides any value set in the "
"MOONSTREAM_HUMBUG_TOKEN environment variable)"
),
)
parser_ethcrawler_trending.add_argument(
"-o",
"--outfile",
type=argparse.FileType("w"),
default=sys.stdout,
help="Optional file to write output to. By default, prints to stdout.",
)
parser_ethcrawler_trending.set_defaults(func=ethcrawler_trending_handler)
args = parser.parse_args()
args.func(args)

Wyświetl plik

@ -1,19 +1,38 @@
from concurrent.futures import Future, ProcessPoolExecutor, wait
from typing import List, Optional, Tuple, Union
from dataclasses import dataclass
from datetime import datetime
from os import close
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
from sqlalchemy import desc
from sqlalchemy import desc, Column
from sqlalchemy import func
from sqlalchemy.orm import Session, Query
from web3 import Web3, IPCProvider, HTTPProvider
from web3.types import BlockData
from .settings import MOONSTREAM_IPC_PATH, MOONSTREAM_CRAWL_WORKERS
from moonstreamdb.db import yield_db_session_ctx
from moonstreamdb.db import yield_db_session, yield_db_session_ctx
from moonstreamdb.models import (
EthereumBlock,
EthereumSmartContract,
EthereumAddress,
EthereumTransaction,
)
class EthereumBlockCrawlError(Exception):
"""
Raised when there is a problem crawling Ethereum blocks.
"""
@dataclass
class DateRange:
start_time: datetime
end_time: datetime
include_start: bool
include_end: bool
def connect(web3_uri: Optional[str] = MOONSTREAM_IPC_PATH):
web3_provider: Union[IPCProvider, HTTPProvider] = Web3.IPCProvider()
if web3_uri is not None:
@ -71,24 +90,29 @@ def add_block_transactions(db_session, block: BlockData) -> None:
db_session.add(tx_obj)
def get_latest_blocks(with_transactions: bool = False) -> Tuple[Optional[int], int]:
def get_latest_blocks(confirmations: int = 0) -> Tuple[Optional[int], int]:
"""
Retrieve the latest block from the connected node (connection is created by the connect() method).
If confirmations > 0, and the latest block on the node has block number N, this returns the block
with block_number (N - confirmations)
"""
web3_client = connect()
block_latest: BlockData = web3_client.eth.get_block(
"latest", full_transactions=with_transactions
)
latest_block_number: int = web3_client.eth.block_number
if confirmations > 0:
latest_block_number -= confirmations
with yield_db_session_ctx() as db_session:
block_number_latest_exist_row = (
latest_stored_block_row = (
db_session.query(EthereumBlock.block_number)
.order_by(EthereumBlock.block_number.desc())
.first()
)
block_number_latest_exist = (
None
if block_number_latest_exist_row is None
else block_number_latest_exist_row[0]
latest_stored_block_number = (
None if latest_stored_block_row is None else latest_stored_block_row[0]
)
return block_number_latest_exist, block_latest.number
return latest_stored_block_number, latest_block_number
def crawl_blocks(
@ -98,8 +122,9 @@ def crawl_blocks(
Open database and geth sessions and fetch block data from blockchain.
"""
web3_client = connect()
for block_number in blocks_numbers:
with yield_db_session_ctx() as db_session:
for block_number in blocks_numbers:
try:
block: BlockData = web3_client.eth.get_block(
block_number, full_transactions=with_transactions
)
@ -109,9 +134,19 @@ def crawl_blocks(
add_block_transactions(db_session, block)
db_session.commit()
except Exception as err:
db_session.rollback()
message = f"Error adding block (number={block_number}) to database:\n{repr(err)}"
raise EthereumBlockCrawlError(message)
except:
db_session.rollback()
print(
f"Interrupted while adding block (number={block_number}) to database."
)
raise
if verbose:
print(f"Added {block_number} block")
print(f"Added block: {block_number}")
def check_missing_blocks(blocks_numbers: List[int]) -> List[int]:
@ -143,6 +178,15 @@ def crawl_blocks_executor(
) -> None:
"""
Execute crawler in processes.
Args:
block_numbers_list - List of block numbers to add to database.
with_transactions - If True, also adds transactions from those blocks to the ethereum_transactions table.
verbose - Print logs to stdout?
num_processes - Number of processes to use to feed blocks into database.
Returns nothing, but if there was an error processing the given blocks it raises an EthereumBlocksCrawlError.
The error message is a list of all the things that went wrong in the crawl.
"""
errors: List[Exception] = []
@ -173,12 +217,10 @@ def crawl_blocks_executor(
results.append(result)
wait(results)
# TODO(kompotkot): Return list of errors and colors responsible for
# handling errors
if len(errors) > 0:
print("Errors:")
for error in errors:
print(f"- {error}")
error_messages = "\n".join([f"- {error}" for error in errors])
message = f"Error processing blocks in list:\n{error_messages}"
raise EthereumBlockCrawlError(message)
def process_contract_deployments() -> List[Tuple[str, str]]:
@ -198,7 +240,7 @@ def process_contract_deployments() -> List[Tuple[str, str]]:
limit = 10
transactions_remaining = True
existing_contract_transaction_hashes = db_session.query(
EthereumSmartContract.transaction_hash
EthereumAddress.transaction_hash
)
while transactions_remaining:
@ -222,7 +264,7 @@ def process_contract_deployments() -> List[Tuple[str, str]]:
if contract_address is not None:
results.append((deployment.hash, contract_address))
db_session.add(
EthereumSmartContract(
EthereumAddress(
transaction_hash=deployment.hash,
address=contract_address,
)
@ -234,3 +276,104 @@ def process_contract_deployments() -> List[Tuple[str, str]]:
current_offset += limit
return results
def trending(
date_range: DateRange, db_session: Optional[Session] = None
) -> Dict[str, Any]:
close_db_session = False
if db_session is None:
close_db_session = True
db_session = next(yield_db_session())
start_timestamp = int(date_range.start_time.timestamp())
end_timestamp = int(date_range.end_time.timestamp())
def make_query(
identifying_column: Column,
statistic_column: Column,
aggregate_func: Callable,
aggregate_label: str,
) -> Query:
query = db_session.query(
identifying_column, aggregate_func(statistic_column).label(aggregate_label)
).join(
EthereumBlock,
EthereumTransaction.block_number == EthereumBlock.block_number,
)
if date_range.include_start:
query = query.filter(EthereumBlock.timestamp >= start_timestamp)
else:
query = query.filter(EthereumBlock.timestamp > start_timestamp)
if date_range.include_end:
query = query.filter(EthereumBlock.timestamp <= end_timestamp)
else:
query = query.filter(EthereumBlock.timestamp < end_timestamp)
query = (
query.group_by(identifying_column).order_by(desc(aggregate_label)).limit(10)
)
return query
results: Dict[str, Any] = {
"date_range": {
"start_time": date_range.start_time.isoformat(),
"end_time": date_range.end_time.isoformat(),
"include_start": date_range.include_start,
"include_end": date_range.include_end,
}
}
try:
transactions_out_query = make_query(
EthereumTransaction.from_address,
EthereumTransaction.hash,
func.count,
"transactions_out",
)
transactions_out = transactions_out_query.all()
results["transactions_out"] = [
{"address": row[0], "statistic": row[1]} for row in transactions_out
]
transactions_in_query = make_query(
EthereumTransaction.to_address,
EthereumTransaction.hash,
func.count,
"transactions_in",
)
transactions_in = transactions_in_query.all()
results["transactions_in"] = [
{"address": row[0], "statistic": row[1]} for row in transactions_in
]
value_out_query = make_query(
EthereumTransaction.from_address,
EthereumTransaction.value,
func.sum,
"value_out",
)
value_out = value_out_query.all()
results["value_out"] = [
{"address": row[0], "statistic": int(row[1])} for row in value_out
]
value_in_query = make_query(
EthereumTransaction.to_address,
EthereumTransaction.value,
func.sum,
"value_in",
)
value_in = value_in_query.all()
results["value_in"] = [
{"address": row[0], "statistic": int(row[1])} for row in value_in
]
pass
finally:
if close_db_session:
db_session.close()
return results

Wyświetl plik

@ -0,0 +1,103 @@
import argparse
import json
import os
import time
import requests
from moonstreamdb.db import yield_db_session_ctx
from moonstreamdb.models import EthereumAddress
COINMARKETCAP_API_KEY = os.environ.get("COINMARKETCAP_API_KEY")
if COINMARKETCAP_API_KEY is None:
raise ValueError("COINMARKETCAP_API_KEY environment variable must be set")
CRAWL_ORIGINS = {
"pro": "https://pro-api.coinmarketcap.com",
"sandbox": "https://sandbox-api.coinmarketcap.com",
}
def identities_cmc_handler(args: argparse.Namespace) -> None:
"""
Parse metadata for Ethereum tokens.
"""
headers = {
"X-CMC_PRO_API_KEY": COINMARKETCAP_API_KEY,
"Accept": "application/json",
"Accept-Encoding": "deflate, gzip",
}
if args.sandbox:
CRAWL_ORIGIN = CRAWL_ORIGINS["sandbox"]
else:
CRAWL_ORIGIN = CRAWL_ORIGINS["pro"]
url = f"{CRAWL_ORIGIN}/v1/cryptocurrency/map"
start_n = 1
limit_n = 5000
while True:
params = {"start": start_n, "limit": limit_n}
try:
r = requests.get(url=url, headers=headers, params=params)
r.raise_for_status()
response = r.json()
except Exception as err:
raise Exception(err)
if len(response["data"]) == 0:
print("No more data, crawling finished")
break
with yield_db_session_ctx() as db_session:
for crypto in response["data"]:
if crypto["platform"] is not None:
if (
crypto["platform"]["id"] == 1027
and crypto["platform"]["token_address"] is not None
):
eth_token = EthereumAddress(
address=crypto["platform"]["token_address"],
name=crypto["name"],
symbol=crypto["symbol"],
)
db_session.add(eth_token)
print(f"Added {crypto['name']} token")
db_session.commit()
start_n += limit_n
print(f"Loop ended, starting new from {start_n} to {start_n + limit_n - 1}")
time.sleep(1)
def main():
parser = argparse.ArgumentParser(description="Crawls address identities CLI")
parser.set_defaults(func=lambda _: parser.print_help())
subcommands = parser.add_subparsers(description="Crawlers commands")
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"
)
parser_label_cloud.set_defaults(func=identities_get_handler)
args = parser.parse_args()
args.func(args)
if __name__ == "__main__":
main()

Wyświetl plik

@ -0,0 +1,33 @@
import json
import os
from typing import Any, Dict, List, Optional
import requests
def publish_json(
crawl_type: str,
humbug_token: str,
title: str,
content: Dict[str, Any],
tags: Optional[List[str]] = None,
) -> None:
spire_api_url = os.environ.get(
"MOONSTREAM_SPIRE_API_URL", "https://spire.bugout.dev"
).rstrip("/")
report_url = f"{spire_api_url}/humbug/reports"
if tags is None:
tags = []
tags.append(f"crawl_type:{crawl_type}")
headers = {
"Authorization": f"Bearer {humbug_token}",
}
request_body = {"title": title, "content": json.dumps(content), "tags": tags}
query_parameters = {"sync": True}
response = requests.post(
report_url, headers=headers, json=request_body, params=query_parameters
)
response.raise_for_status()

Wyświetl plik

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

Wyświetl plik

@ -2,3 +2,4 @@
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_HUMBUG_TOKEN="<Token for crawlers store data via Humbug>"

Wyświetl plik

@ -1,14 +1,14 @@
from setuptools import find_packages, setup
from moonstreamcrawlers.version import MOONSTREAMCRAWLERS_VERSION
from mooncrawl.version import MOONCRAWL_VERSION
long_description = ""
with open("README.md") as ifp:
long_description = ifp.read()
setup(
name="moonstreamcrawlers",
version=MOONSTREAMCRAWLERS_VERSION,
name="mooncrawl",
version=MOONCRAWL_VERSION,
author="Bugout.dev",
author_email="engineers@bugout.dev",
license="Apache License 2.0",
@ -30,16 +30,21 @@ setup(
],
python_requires=">=3.6",
packages=find_packages(),
package_data={"moonstreamcrawlers": ["py.typed"]},
package_data={"mooncrawl": ["py.typed"]},
zip_safe=False,
install_requires=[
"moonstreamdb @ git+https://git@github.com/bugout-dev/moonstream.git@ec3278e192119d1e8a273cfaab6cb53890d2e8e9#egg=moonstreamdb&subdirectory=db",
"moonstreamdb @ git+https://git@github.com/bugout-dev/moonstream.git@39d2b8e36a49958a9ae085ec2cc1be3fc732b9d0#egg=moonstreamdb&subdirectory=db",
"python-dateutil",
"requests",
"tqdm",
"web3",
],
extras_require={"dev": ["black", "mypy", "types-requests"]},
entry_points={
"console_scripts": ["moonstreamcrawlers=moonstreamcrawlers.cli:main"]
"console_scripts": [
"ethcrawler=mooncrawl.ethcrawler:main",
"esd=mooncrawl.esd:main",
"identity=mooncrawl.identity:main",
]
},
)

Wyświetl plik

@ -17,22 +17,31 @@ fileConfig(config.config_file_name)
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
from moonstreamdb.models import Base as ExplorationBase
from moonstreamdb.models import Base as MoonstreamBase
target_metadata = ExplorationBase.metadata
target_metadata = MoonstreamBase.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
from moonstreamdb.models import EthereumBlock, EthereumTransaction, EthereumPendingTransaction, EthereumSmartContract, ESDEventSignature, ESDFunctionSignature
from moonstreamdb.models import (
EthereumBlock,
EthereumTransaction,
EthereumPendingTransaction,
EthereumAddress,
EthereumLabel,
ESDEventSignature,
ESDFunctionSignature,
)
def include_symbol(tablename, schema):
return tablename in {
EthereumBlock.__tablename__,
EthereumTransaction.__tablename__,
EthereumSmartContract.__tablename__,
EthereumAddress.__tablename__,
EthereumLabel.__tablename__,
EthereumPendingTransaction.__tablename__,
ESDEventSignature.__tablename__,
ESDFunctionSignature.__tablename__,

Wyświetl plik

@ -0,0 +1,62 @@
"""Labels for addresses
Revision ID: 40871a7807f6
Revises: 571f33ad7587
Create Date: 2021-08-09 14:50:46.163063
"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = '40871a7807f6'
down_revision = '571f33ad7587'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index('ix_ethereum_smart_contracts_address', table_name='ethereum_smart_contracts')
op.drop_index('ix_ethereum_smart_contracts_transaction_hash', table_name='ethereum_smart_contracts')
op.execute("ALTER TABLE ethereum_smart_contracts RENAME TO ethereum_addresses;")
op.alter_column("ethereum_addresses", "transaction_hash", nullable=True)
op.add_column('ethereum_addresses', sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text("TIMEZONE('utc', statement_timestamp())"), nullable=False))
op.create_index(op.f('ix_ethereum_addresses_address'), 'ethereum_addresses', ['address'], unique=False)
op.create_index(op.f('ix_ethereum_addresses_transaction_hash'), 'ethereum_addresses', ['transaction_hash'], unique=False)
op.create_table('ethereum_labels',
sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False),
sa.Column('label', sa.VARCHAR(length=256), nullable=False),
sa.Column('address_id', sa.Integer(), nullable=False),
sa.Column('label_data', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text("TIMEZONE('utc', statement_timestamp())"), nullable=False),
sa.ForeignKeyConstraint(['address_id'], ['ethereum_addresses.id'], name=op.f('fk_ethereum_labels_address_id_ethereum_addresses'), ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id', name=op.f('pk_ethereum_labels')),
sa.UniqueConstraint('id', name=op.f('uq_ethereum_labels_id')),
sa.UniqueConstraint('label', 'address_id', name=op.f('uq_ethereum_labels_label'))
)
op.create_index(op.f('ix_ethereum_labels_address_id'), 'ethereum_labels', ['address_id'], unique=False)
op.create_index(op.f('ix_ethereum_labels_label'), 'ethereum_labels', ['label'], unique=False)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_ethereum_addresses_transaction_hash'), table_name='ethereum_addresses')
op.drop_index(op.f('ix_ethereum_addresses_address'), table_name='ethereum_addresses')
op.execute("ALTER TABLE ethereum_addresses RENAME TO ethereum_smart_contracts;")
op.alter_column("ethereum_smart_contracts", "transaction_hash", nullable=False)
op.drop_column('ethereum_smart_contracts', 'created_at')
op.create_index('ix_ethereum_smart_contracts_transaction_hash', 'ethereum_smart_contracts', ['transaction_hash'], unique=False)
op.create_index('ix_ethereum_smart_contracts_address', 'ethereum_smart_contracts', ['address'], unique=False)
op.drop_index(op.f('ix_ethereum_labels_label'), table_name='ethereum_labels')
op.drop_index(op.f('ix_ethereum_labels_address_id'), table_name='ethereum_labels')
op.drop_table('ethereum_labels')
# ### end Alembic commands ###

Wyświetl plik

@ -0,0 +1,121 @@
import argparse
import json
from .db import yield_db_session_ctx
from .models import EthereumAddress, EthereumLabel
def labels_add_handler(args: argparse.Namespace) -> None:
"""
Add new label for ethereum address.
"""
try:
label_data = json.loads(args.data)
except ValueError as err:
print(str(err))
raise ValueError("Unable to parse data as dictionary")
with yield_db_session_ctx() as db_session:
address = (
db_session.query(EthereumAddress)
.filter(EthereumAddress.address == str(args.address))
.one_or_none()
)
if address is None:
print(f"There is no {args.address} address")
return
label = EthereumLabel(
label=args.label, address_id=address.id, label_data=label_data
)
db_session.add(label)
db_session.commit()
print(
json.dumps(
{
"id": str(label.id),
"label": str(label.label),
"address_id": str(label.address_id),
"label_data": str(label.label_data),
"created_at": str(label.created_at),
}
)
)
def labels_list_handler(args: argparse.Namespace) -> None:
"""
Return list of all labels.
"""
with yield_db_session_ctx() as db_session:
query = db_session.query(EthereumLabel).all()
if str(args.address) is not None:
query = query.filter(EthereumAddress.address == str(args.address))
labels = query.all()
print(
json.dumps(
[
{
"id": str(label.id),
"label": str(label.label),
"address_id": str(label.address_id),
"label_data": str(label.label_data),
"created_at": str(label.created_at),
}
for label in labels
]
)
)
def main():
parser = argparse.ArgumentParser(description="Crawls address identities CLI")
parser.set_defaults(func=lambda _: parser.print_help())
subcommands = parser.add_subparsers(description="Crawlers commands")
parser_labels = subcommands.add_parser("labels", description="Meta labels commands")
parser_labels.set_defaults(func=lambda _: parser_labels.print_help())
subcommands_labels = parser_labels.add_subparsers(
description="Database meta labels commands"
)
parser_labels_add = subcommands_labels.add_parser(
"add", description="Add new label command"
)
parser_labels_add.add_argument(
"-a",
"--address",
required=True,
help="Address attach to",
)
parser_labels_add.add_argument(
"-l",
"--label",
required=True,
help="New label name",
)
parser_labels_add.add_argument(
"-d",
"--data",
help="New label data",
)
parser_labels_add.set_defaults(func=labels_add_handler)
parser_labels_list = subcommands_labels.add_parser(
"list", description="List all meta labels command"
)
parser_labels_list.add_argument(
"-a",
"--address",
help="Filter address",
)
parser_labels_list.set_defaults(func=labels_list_handler)
args = parser.parse_args()
args.func(args)
if __name__ == "__main__":
main()

Wyświetl plik

@ -1,4 +1,5 @@
import sqlalchemy
import uuid
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import (
BigInteger,
@ -10,7 +11,9 @@ from sqlalchemy import (
Numeric,
Text,
VARCHAR,
UniqueConstraint,
)
from sqlalchemy.dialects.postgresql import JSONB, UUID
from sqlalchemy.sql import expression
from sqlalchemy.ext.compiler import compiles
@ -100,17 +103,59 @@ class EthereumTransaction(Base): # type: ignore
)
class EthereumSmartContract(Base): # type: ignore
__tablename__ = "ethereum_smart_contracts"
class EthereumAddress(Base): # type: ignore
__tablename__ = "ethereum_addresses"
id = Column(Integer, primary_key=True, autoincrement=True)
transaction_hash = Column(
VARCHAR(256),
ForeignKey("ethereum_transactions.hash", ondelete="CASCADE"),
nullable=False,
nullable=True,
index=True,
)
address = Column(VARCHAR(256), nullable=False, index=True)
created_at = Column(
DateTime(timezone=True), server_default=utcnow(), nullable=False
)
class EthereumLabel(Base): # type: ignore
"""
Example of label_data:
{
"label": "ERC20",
"label_data": {
"name": "Uniswap",
"symbol": "UNI"
}
},
{
"label": "Exchange"
"label_data": {...}
}
"""
__tablename__ = "ethereum_labels"
__table_args__ = (UniqueConstraint("label", "address_id"),)
id = Column(
UUID(as_uuid=True),
primary_key=True,
default=uuid.uuid4,
unique=True,
nullable=False,
)
label = Column(VARCHAR(256), nullable=False, index=True)
address_id = Column(
Integer,
ForeignKey("ethereum_addresses.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
label_data = Column(JSONB, nullable=True)
created_at = Column(
DateTime(timezone=True), server_default=utcnow(), nullable=False
)
class EthereumPendingTransaction(Base): # type: ignore

Wyświetl plik

@ -2,4 +2,4 @@
Moonstream database version.
"""
MOONSTREAMDB_VERSION = "0.0.1"
MOONSTREAMDB_VERSION = "0.0.2"

Wyświetl plik

@ -34,4 +34,9 @@ setup(
zip_safe=False,
install_requires=["alembic", "psycopg2-binary", "sqlalchemy"],
extras_require={"dev": ["black", "mypy"]},
entry_points={
"console_scripts": [
"moonstreamdb=moonstreamdb.cli:main",
]
},
)

Wyświetl plik

@ -16,12 +16,15 @@
"@emotion/styled": "^11.3.0",
"@stripe/stripe-js": "^1.16.0",
"axios": "^0.21.1",
"focus-visible": "^5.2.0",
"framer-motion": "^4.1.17",
"mixpanel-browser": "^2.41.0",
"moment": "^2.29.1",
"next": "11.0.1",
"nprogress": "^0.2.0",
"react": "^17.0.2",
"react-calendly": "^2.2.1",
"react-color": "^2.19.3",
"react-copy-to-clipboard": "^5.0.2",
"react-dom": "^17.0.2",
"react-hook-form": "^6.9.2",

Wyświetl plik

@ -1,30 +1,56 @@
import React from "react";
import { React, useEffect, useState } from "react";
import "/styles/styles.css";
import "/styles/nprogress.css";
import "/styles/sidebar.css";
import "highlight.js/styles/github.css";
import App from "next/app";
import "focus-visible/dist/focus-visible";
import dynamic from "next/dynamic";
import { QueryClient, QueryClientProvider } from "react-query";
import HeadSEO from "../src/components/HeadSEO";
import HeadLinks from "../src/components/HeadLinks";
const HeadSEO = dynamic(() => import("../src/components/HeadSEO"), {
ssr: false,
});
const HeadLinks = dynamic(() => import("../src/components/HeadLinks"), {
ssr: false,
});
const AppContext = dynamic(() => import("../src/AppContext"), {
ssr: false,
});
const DefaultLayout = dynamic(() => import("../src/layouts"), {
ssr: false,
});
import { useRouter } from "next/router";
import NProgress from "nprogress";
export default class CachingApp extends App {
constructor(props) {
super(props);
this.state = { queryClient: new QueryClient() };
}
export default function CachingApp({ Component, pageProps }) {
const [queryClient] = useState(new QueryClient());
render() {
const { Component, pageProps } = this.props;
const router = useRouter();
useEffect(() => {
const handleStart = () => {
NProgress.start();
};
const handleStop = () => {
NProgress.done();
};
router.events.on("routeChangeStart", handleStart);
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);
router.events.off("routeChangeError", handleStop);
};
}, [router]);
const getLayout =
Component.getLayout || ((page) => <DefaultLayout>{page}</DefaultLayout>);
console.log("_app loaded", router.asPath);
return (
<>
<style global jsx>{`
@ -40,10 +66,9 @@ export default class CachingApp extends App {
`}</style>
{pageProps.metaTags && <HeadSEO {...pageProps.metaTags} />}
{pageProps.preloads && <HeadLinks links={pageProps.preloads} />}
<QueryClientProvider client={this.state.queryClient}>
<QueryClientProvider client={queryClient}>
<AppContext>{getLayout(<Component {...pageProps} />)}</AppContext>
</QueryClientProvider>
</>
);
}
}

Wyświetl plik

@ -58,6 +58,12 @@ const Security = () => {
}
};
useEffect(() => {
if (typeof window !== "undefined") {
document.title = `Change password`;
}
}, []);
useEffect(() => {
if (data) router.push("/");
}, [data, router]);

Wyświetl plik

@ -1,10 +1,16 @@
import React from "react";
import React, { useEffect } from "react";
import HubspotForm from "react-hubspot-form";
import { getLayout } from "../src/layouts/AppLayout";
import { Spinner, Flex, Heading } from "@chakra-ui/react";
import Scrollable from "../src/components/Scrollable";
const Analytics = () => {
useEffect(() => {
if (typeof window !== "undefined") {
document.title = `Analytics: Page under construction`;
}
}, []);
return (
<Scrollable>
<Flex

Wyświetl plik

@ -1,42 +1,80 @@
import React, {
useLayoutEffect,
useEffect,
Suspense,
useContext,
useState,
useContext,
Suspense,
useEffect,
useLayoutEffect,
} from "react";
import {
Fade,
Flex,
Heading,
Box,
Image as ChakraImage,
Button,
Center,
Fade,
chakra,
Stack,
Link,
SimpleGrid,
useMediaQuery,
Grid,
GridItem,
} from "@chakra-ui/react";
import { Grid, GridItem } from "@chakra-ui/react";
import { useUser, useAnalytics, useModals, useRouter } from "../src/core/hooks";
import { getLayout } from "../src/layouts";
import SplitWithImage from "../src/components/SplitWithImage";
import ConnectedButtons from "../src/components/ConnectedButtons";
import UIContext from "../src/core/providers/UIProvider/context";
import dynamic from "next/dynamic";
import useUser from "../src/core/hooks/useUser";
import useAnalytics from "../src/core/hooks/useAnalytics";
import useModals from "../src/core/hooks/useModals";
import useRouter from "../src/core/hooks/useRouter";
import { MIXPANEL_PROPS } from "../src/core/providers/AnalyticsProvider/constants";
import { FaFileContract } from "react-icons/fa";
import { RiDashboardFill } from "react-icons/ri";
import {
GiMeshBall,
GiLogicGateXor,
GiSuspicious,
GiHook,
} from "react-icons/gi";
import { AiFillApi } from "react-icons/ai";
import { BiTransfer } from "react-icons/bi";
import { IoTelescopeSharp } from "react-icons/io5";
import UIContext from "../src/core/providers/UIProvider/context";
const SplitWithImage = dynamic(
() => import("../src/components/SplitWithImage"),
{
ssr: false,
}
);
const ConnectedButtons = dynamic(
() => import("../src/components/ConnectedButtons"),
{
ssr: false,
}
);
const RiDashboardFill = dynamic(() =>
import("react-icons/ri").then((mod) => mod.RiDashboardFill)
);
const FaFileContract = dynamic(() =>
import("react-icons/fa").then((mod) => mod.FaFileContract)
);
const GiMeshBall = dynamic(() =>
import("react-icons/gi").then((mod) => mod.GiMeshBall)
);
const GiLogicGateXor = dynamic(() =>
import("react-icons/gi").then((mod) => mod.GiLogicGateXor)
);
const GiSuspicious = dynamic(() =>
import("react-icons/gi").then((mod) => mod.GiSuspicious)
);
const GiHook = dynamic(() =>
import("react-icons/gi").then((mod) => mod.GiHook)
);
const AiFillApi = dynamic(() =>
import("react-icons/ai").then((mod) => mod.AiFillApi)
);
const BiTransfer = dynamic(() =>
import("react-icons/bi").then((mod) => mod.BiTransfer)
);
const IoTelescopeSharp = dynamic(() =>
import("react-icons/io5").then((mod) => mod.IoTelescopeSharp)
);
const HEADING_PROPS = {
fontWeight: "700",
@ -114,12 +152,17 @@ const Homepage = () => {
if (
router.nextRouter.asPath !== "/" &&
router.nextRouter.asPath.slice(0, 2) !== "/?" &&
router.nextRouter.asPath.slice(0, 2) !== "/#"
router.nextRouter.asPath.slice(0, 2) !== "/#" &&
router.nextRouter.asPath.slice(0, 11) !== "/index.html"
) {
router.replace(router.nextRouter.asPath, undefined, {
shallow: true,
console.warn("redirect attempt..");
if (typeof window !== "undefined") {
console.warn("window present:", window.location.pathname);
router.replace(router.nextRouter.asPath, router.nextRouter.asPath, {
shallow: false,
});
}
}
}, [isInit, router]);
useLayoutEffect(() => {
@ -155,6 +198,7 @@ const Homepage = () => {
}, []);
return (
<Suspense fallback="">
<Fade in>
<Box
width="100%"
@ -203,7 +247,7 @@ const Homepage = () => {
maxW="1620px"
px="7%"
h="100%"
pt={["10vh", null, "30vh"]}
pt={["10vh", null, "20vh"]}
>
<Heading size="2xl" fontWeight="semibold" color="white">
All the crypto data you care about in a single stream
@ -213,7 +257,6 @@ const Homepage = () => {
fontSize={["lg", null, "xl"]}
display="inline-block"
color="primary.200"
textDecor="underline"
>
Get all the crypto data you need in a single stream.
From pending transactions in the Ethereum transaction
@ -223,9 +266,9 @@ const Homepage = () => {
fontSize={["lg", null, "xl"]}
display="inline-block"
color="primary.300"
textDecor="underline"
>
Access this data through the Moonstream dashboard or API
Access this data through the Moonstream dashboard or
API
</chakra.span>
</Stack>
</Flex>
@ -292,7 +335,7 @@ const Homepage = () => {
</SimpleGrid>
<Center>
<Heading pt="160px" pb="60px">
Moonstream is ment for you if
Moonstream is meant for you if
</Heading>
</Center>
<Flex
@ -500,6 +543,7 @@ const Homepage = () => {
</Flex>
</Box>
</Fade>
</Suspense>
);
};
@ -530,7 +574,4 @@ export async function getStaticProps() {
};
}
Homepage.layout = "default";
Homepage.getLayout = getLayout;
export default Homepage;

Wyświetl plik

@ -0,0 +1,133 @@
import React, { useState } from "react";
import { useForm } from "react-hook-form";
import {
Heading,
Text,
Stack,
Box,
FormControl,
FormErrorMessage,
InputGroup,
Button,
Input,
InputRightElement,
} from "@chakra-ui/react";
import Icon from "../../src/components/CustomIcon";
import useSignUp from "../../src/core/hooks/useSignUp";
import useUser from "../../src/core/hooks/useSignUp";
import useRouter from "../../src/core/hooks/useSignUp";
import { DEFAULT_METATAGS } from "../../src/components/constants";
export async function getStaticProps() {
return {
props: { metaTags: { ...DEFAULT_METATAGS } },
};
}
const Register = () => {
const router = useRouter();
const { handleSubmit, errors, register } = useForm();
const [showPassword, togglePassword] = useState(false);
const { user } = useUser();
const loggedIn = user && user.username;
// const { email, code } = router.query;
const email = router.query?.email;
const code = router.query?.code;
const { signUp, isLoading } = useSignUp(code);
loggedIn && router.push("/stream");
return (
<Box minH="900px" w="100%" px={["7%", null, "25%"]} alignSelf="center">
<Heading mt={2} size="md">
Create an account
</Heading>
<Text color="gray.300" fontSize="md">
Sign up for free
</Text>
<form onSubmit={handleSubmit(signUp)}>
<Stack width="100%" pt={4} spacing={3}>
<FormControl isInvalid={errors.username}>
<InputGroup>
<Input
variant="filled"
colorScheme="primary"
placeholder="Your username here"
name="username"
ref={register({ required: "Username is required!" })}
/>
<InputRightElement>
<Icon icon="name" />
</InputRightElement>
</InputGroup>
<FormErrorMessage color="unsafe.400" pl="1">
{errors.username && errors.username.message}
</FormErrorMessage>
</FormControl>
<FormControl isInvalid={errors.email}>
<InputGroup>
{!email && (
<Input
variant="filled"
colorScheme="primary"
placeholder="Your email here"
name="email"
ref={register({ required: "Email is required!" })}
/>
)}
{email && (
<Input
variant="filled"
colorScheme="primary"
placeholder="Your email here"
defaultValue={email}
isReadOnly={true}
name="email"
ref={register({ required: "Email is required!" })}
/>
)}
<InputRightElement>
<Icon icon="name" />
</InputRightElement>
</InputGroup>
<FormErrorMessage color="unsafe.400" pl="1">
{errors.email && errors.email.message}
</FormErrorMessage>
</FormControl>
<FormControl isInvalid={errors.password}>
<InputGroup>
<Input
variant="filled"
colorScheme="primary"
autoComplete="new-password"
placeholder="Add password"
name="password"
type={showPassword ? "text" : "password"}
ref={register({ required: "Password is required!" })}
/>
<InputRightElement onClick={() => togglePassword(!showPassword)}>
<Icon icon="password" />
</InputRightElement>
</InputGroup>
<FormErrorMessage color="unsafe.400" pl="1">
{errors.password && errors.password.message}
</FormErrorMessage>
</FormControl>
</Stack>
<Button
my={8}
variant="solid"
colorScheme="primary"
width="100%"
type="submit"
isLoading={isLoading}
>
Register
</Button>
</form>
</Box>
);
};
export default Register;

Wyświetl plik

@ -1,117 +0,0 @@
import React, {useContext} from "react";
import { Flex, HStack, Skeleton, Box, Heading, Center, Spinner } from "@chakra-ui/react";
import { useTxInfo, useTxCashe, useRouter } from "../../src/core/hooks";
import FourOFour from "../../src/components/FourOFour";
import FourOThree from "../../src/components/FourOThree";
import Tags from "../../src/components/Tags";
import { getLayout } from "../../src/layouts/EntriesLayout";
import Scrollable from "../../src/components/Scrollable";
import TxInfo from "../../src/components/TxInfo"
import UIContext from "../../src/core/providers/UIProvider/context";
const Entry = () => {
const ui = useContext(UIContext);
const router = useRouter();
const { entryId } = router.params;
const txCache = useTxCashe;
const callReroute = () => {
ui.setEntriesViewMode("list");
router.push({
pathname: `/stream`,
query: router.query,
});
const LoadingSpinner = () => (
<Box px="12%" my={12} width="100%">
<Center>
<Spinner
hidden={false}
my={0}
size="lg"
color="primary.500"
thickness="4px"
speed="1.5s"
/>
</Center>
</Box>
);
return (
<LoadingSpinner/>
)
}
const transaction = txCache.getCurrentTransaction()
const {
data: entry,
isFetchedAfterMount,
isLoading,
isError,
error,
} = useTxInfo({tx:transaction})
if (isError) {return callReroute()}
if (isError && error.response.status === 404) return <FourOFour />;
if (isError && error.response.status === 403) return <FourOThree />;
// if (!entry || isLoading) return "";
return (
<Flex
id="Entry"
height="100%"
flexGrow="1"
flexDirection="column"
key={entryId}
>
<Skeleton
id="EntryNameSkeleton"
mx={2}
mt={2}
overflow="initial"
isLoaded={!isLoading}
>
<HStack id="EntryHeader" width="100%" m={0}>
<Heading
overflow="hidden"
width={entry?.context_url ? "calc(100% - 28px)" : "100%"}
// height="auto"
minH="36px"
style={{ marginLeft: "0" }}
m={0}
p={0}
fontWeight="600"
fontSize="1.5rem"
textAlign="left"
>
{entry && entry.hash}
</Heading>
</HStack>
</Skeleton>
<Skeleton
id="TagsSkeleton"
mx={2}
overflow="initial"
mt={1}
isLoaded={isFetchedAfterMount || entry}
>
<Tags entry={entry} />
</Skeleton>
<Skeleton
height="10px"
flexGrow={1}
id="EditorSkeleton"
mx={2}
mr={isFetchedAfterMount || entry ? 0 : 2}
mt={1}
isLoaded={isFetchedAfterMount || entry}
>
<Scrollable>
{!isLoading && (<TxInfo transaction = {entry}></TxInfo> )}
</Scrollable>
</Skeleton>
</Flex>
);
};
Entry.getLayout = getLayout;
export default Entry;

Wyświetl plik

@ -1,6 +1,58 @@
import React, { useContext, useEffect } from "react";
import { getLayout } from "../../src/layouts/EntriesLayout";
import StreamEntryDetails from "../../src/components/SteamEntryDetails";
import UIContext from "../../src/core/providers/UIProvider/context";
import {
Box,
Heading,
Text,
Stack,
UnorderedList,
ListItem,
} from "@chakra-ui/react";
const Entry = () => {
return "";
const ui = useContext(UIContext);
useEffect(() => {
if (typeof window !== "undefined") {
if (ui.currentTransaction) {
document.title = `Stream details: ${ui.currentTransaction.hash}`;
} else {
document.title = `Stream`;
}
}
}, [ui.currentTransaction]);
if (ui.currentTransaction) {
return <StreamEntryDetails />;
} else
return (
<Box px="7%" pt={12}>
<>
<Stack direction="column">
<Heading>Stream view</Heading>
<Text>
In this view you can follow events that happen on your subscribed
addresses
</Text>
<UnorderedList pl={4}>
<ListItem>
Click filter icon on right top corner to filter by specific
address across your subscriptions
</ListItem>
<ListItem>
On event cards you can click at right corner to see detailed
view!
</ListItem>
<ListItem>
For any adress of interest here you can copy it and subscribe at
subscription screen
</ListItem>
</UnorderedList>
</Stack>
</>
</Box>
);
};
Entry.getLayout = getLayout;
export default Entry;

Wyświetl plik

@ -62,7 +62,6 @@ const Subscriptions = () => {
<Center>
<Spinner
hidden={false}
// ref={loadMoreButtonRef}
my={8}
size="lg"
color="primary.500"

Wyświetl plik

@ -30,7 +30,6 @@ const AccountIconButton = (props) => {
zIndex="dropdown"
width={["100vw", "100vw", "18rem", "20rem", "22rem", "24rem"]}
borderRadius={0}
m={0}
>
<MenuGroup>
<RouterLink href="/account/security" passHref>

Wyświetl plik

@ -2,9 +2,7 @@ import React, { useState, useContext, useEffect } from "react";
import RouterLink from "next/link";
import {
Flex,
Button,
Image,
ButtonGroup,
Text,
IconButton,
Link,
@ -17,7 +15,6 @@ import {
PopoverCloseButton,
useBreakpointValue,
Spacer,
Fade,
} from "@chakra-ui/react";
import {
HamburgerIcon,
@ -96,66 +93,8 @@ const AppNavbar = () => {
{!ui.isMobileView && (
<>
<Flex width="100%" px={2}>
<Fade in={ui.entriesViewMode === "entry"}>
<Button
m={0}
alignSelf="center"
variant="outline"
justifyContent="space-evenly"
alignContent="center"
h="32px"
size="sm"
colorScheme="gray"
aria-label="App navigation"
leftIcon={<ArrowLeftIcon />}
onClick={() => {
router.push(
{
pathname: "/stream",
query: router.query,
},
undefined,
{ shallow: false }
);
// router.params?.entryId && ui.entriesViewMode === "entry"
// ?
ui.setEntriesViewMode("list");
// : router.nextRouter.back();
}}
>
Back to stream
</Button>
</Fade>
<Spacer />
<Flex placeSelf="flex-end">
<ButtonGroup
alignSelf="center"
// position="relative"
left={
isSearchBarActive
? "100%"
: ["64px", "30%", "50%", "55%", null, "60%"]
}
// hidden={ui.searchBarActive}
display={isSearchBarActive ? "hidden" : "block"}
variant="link"
colorScheme="secondary"
spacing={4}
px={2}
zIndex={ui.searchBarActive ? -10 : 0}
size={["xs", "xs", "xs", "lg", null, "lg"]}
>
<RouterLink href="/pricing" passHref>
<Button color="white" fontWeight="400">
Pricing
</Button>
</RouterLink>
<RouterLink href="/product" passHref>
<Button color="white" fontWeight="400">
Product
</Button>
</RouterLink>
</ButtonGroup>
<SupportPopover />
<AccountIconButton
colorScheme="primary"
@ -214,8 +153,9 @@ const AppNavbar = () => {
aria-label="App navigation"
icon={<ArrowLeftIcon />}
onClick={() => {
router.params?.entryId && ui.entriesViewMode === "entry"
? ui.setEntriesViewMode("list")
router.nextRouter.pathname === "/stream" &&
ui.isEntryDetailView
? ui.setEntryDetailView(false)
: router.nextRouter.back();
}}
/>
@ -245,8 +185,9 @@ const AppNavbar = () => {
aria-label="App navigation"
icon={<ArrowRightIcon />}
onClick={() => {
router.params?.entryId && ui.entriesViewMode === "list"
? ui.setEntriesViewMode("entry")
router.nextRouter.pathname === "/stream" &&
!ui.isEntryDetailView
? ui.setEntryDetailView(true)
: history.forward();
}}
/>
@ -256,7 +197,6 @@ const AppNavbar = () => {
{!isSearchBarActive && (
<AccountIconButton
variant="link"
mx={0}
justifyContent="space-evenly"
alignContent="center"
h="32px"

Wyświetl plik

@ -0,0 +1,102 @@
import { React, useEffect, useState } from "react";
import {
Box,
Popover,
PopoverTrigger,
PopoverContent,
PopoverHeader,
PopoverBody,
PopoverFooter,
PopoverArrow,
PopoverCloseButton,
Portal,
Stack,
IconButton,
Text,
Input,
useDisclosure,
Button,
} from "@chakra-ui/react";
import { makeColor } from "../core/utils/makeColor";
import { BiRefresh } from "react-icons/bi";
import { GithubPicker } from "react-color";
const ColorSelector = (props) => {
const { onOpen, onClose, isOpen } = useDisclosure();
const [color, setColor] = useState(props.initialColor ?? makeColor());
const [triggerColor, setTriggerColor] = useState(color);
useEffect(() => {
setTriggerColor(props.initialColor);
}, [props.initialColor]);
const handleChangeColorComplete = (color) => {
setColor(color.hex);
};
const handleChangeColor = (event) => setColor(event.target.value);
return (
<Popover isOpen={isOpen} onOpen={onOpen} onClose={onClose}>
<PopoverTrigger>
<Box
placeSelf="center"
boxSize="24px"
borderRadius="sm"
bgColor={triggerColor}
></Box>
</PopoverTrigger>
<Portal>
<PopoverContent bg={"white.100"}>
<PopoverArrow />
<PopoverHeader>Change color</PopoverHeader>
<PopoverCloseButton />
<PopoverBody>
<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"
value={color}
onChange={handleChangeColor}
w="200px"
onSubmit={handleChangeColorComplete}
></Input>
</Stack>
<GithubPicker
// color={this.state.background}
onChangeComplete={handleChangeColorComplete}
/>
</PopoverBody>
<PopoverFooter>
<Button
onClick={() => {
props.callback(color);
onClose();
}}
colorScheme="suggested"
variant="outline"
>
Apply
</Button>
</PopoverFooter>
</PopoverContent>
</Portal>
</Popover>
);
};
export default ColorSelector;

Wyświetl plik

@ -32,7 +32,6 @@ import {
TagCloseButton,
Stack,
Spacer,
useBoolean,
} from "@chakra-ui/react";
import { useSubscriptions } from "../core/hooks";
import StreamEntry from "./StreamEntry";
@ -40,9 +39,7 @@ import UIContext from "../core/providers/UIProvider/context";
import { FaFilter } from "react-icons/fa";
import useStream from "../core/hooks/useStream";
import { ImCancelCircle } from "react-icons/im";
import { IoStopCircleOutline, IoPlayCircleOutline } from "react-icons/io5";
const pageSize = 25;
const FILTER_TYPES = {
ADDRESS: 0,
GAS: 1,
@ -64,7 +61,6 @@ const CONDITION = {
const EntriesNavigation = () => {
const ui = useContext(UIContext);
const [isStreamOn, setStreamState] = useBoolean(true);
const { isOpen, onOpen, onClose } = useDisclosure();
const { subscriptionsCache } = useSubscriptions();
const [newFilterState, setNewFilterState] = useState([
@ -86,13 +82,9 @@ const EntriesNavigation = () => {
include_end: true,
next_event_time: null,
previous_event_time: null,
update: false,
});
const updateStreamBoundaryWith = (pageBoundary) => {
console.log("pageBoundary", pageBoundary);
console.log("streamBoundary", streamBoundary);
if (!pageBoundary) {
return streamBoundary;
}
@ -134,7 +126,7 @@ const EntriesNavigation = () => {
if (
!newBoundary.next_event_time ||
pageBoundary.next_event_time == 0 ||
!pageBoundary.next_event_time ||
(pageBoundary.next_event_time &&
pageBoundary.next_event_time > newBoundary.next_event_time)
) {
@ -143,50 +135,33 @@ const EntriesNavigation = () => {
if (
!newBoundary.previous_event_time ||
pageBoundary.previous_event_time == 0 ||
!pageBoundary.previous_event_time ||
(pageBoundary.previous_event_time &&
pageBoundary.previous_event_time < newBoundary.previous_event_time)
) {
newBoundary.previous_event_time = pageBoundary.previous_event_time;
}
newBoundary.update = pageBoundary.update;
setStreamBoundary(newBoundary);
return newBoundary;
};
const { EntriesPages, isLoading, refetch } = useStream({
refreshRate: 1500,
const { EntriesPages, isLoading, refetch, isFetching, remove } = useStream({
searchQuery: ui.searchTerm,
start_time: streamBoundary.start_time,
end_time: streamBoundary.end_time,
include_start: streamBoundary.include_start,
include_end: streamBoundary.include_end,
enabled: isStreamOn,
updateStreamBoundaryWith: updateStreamBoundaryWith,
streamBoundary: streamBoundary,
setStreamBoundary: setStreamBoundary,
isContent: false,
});
// const handleScroll = ({ currentTarget }) => {
// if (
// currentTarget.scrollTop + currentTarget.clientHeight >=
// 0.5 * currentTarget.scrollHeight
// ) {
// if (!isLoading && hasPreviousPage) {
// fetchPreviousPage();
// }
// }
// };
useEffect(() => {
if (EntriesPages && !isLoading && streamBoundary.update) {
console.log("streamBoundary.update", streamBoundary.update);
streamBoundary.update = false;
if (!streamBoundary.start_time && !streamBoundary.end_time) {
refetch();
}
}, [streamBoundary]);
}, [streamBoundary, refetch]);
const setFilterProps = useCallback(
(filterIdx, props) => {
@ -200,7 +175,7 @@ const EntriesNavigation = () => {
useEffect(() => {
if (
subscriptionsCache.data?.subscriptions[0]?.id &&
newFilterState[0].value === null
newFilterState[0]?.value === null
) {
setFilterProps(0, {
value: subscriptionsCache?.data?.subscriptions[0]?.address,
@ -225,8 +200,6 @@ const EntriesNavigation = () => {
const newArray = oldArray.filter(function (ele) {
return ele != oldArray[idx];
});
console.log(newFilterState);
console.log(newArray);
setNewFilterState(newArray);
};
@ -306,7 +279,6 @@ const EntriesNavigation = () => {
Source:
</Text>
{newFilterState.map((filter, idx) => {
console.log("197", newFilterState);
if (filter.type === FILTER_TYPES.DISABLED) return "";
return (
<Flex
@ -453,20 +425,6 @@ const EntriesNavigation = () => {
</Drawer>
<Flex h="3rem" w="100%" bgColor="gray.100" alignItems="center">
<Flex maxW="90%">
<Flex direction="column">
<IconButton
size="sm"
onClick={() => setStreamState.toggle()}
icon={
isStreamOn ? (
<IoStopCircleOutline size="32px" />
) : (
<IoPlayCircleOutline size="32px" />
)
}
colorScheme={isStreamOn ? "unsafe" : "suggested"}
/>
</Flex>
{filterState.map((filter, idx) => {
if (filter.type === FILTER_TYPES.DISABLED) return "";
return (
@ -520,8 +478,10 @@ const EntriesNavigation = () => {
//onScroll={(e) => handleScroll(e)}
>
<Stack direction="row" justifyContent="space-between">
{!isFetching ? (
<Button
onClick={() => {
remove();
setStreamBoundary({
start_time: null,
end_time: null,
@ -529,7 +489,6 @@ const EntriesNavigation = () => {
include_end: true,
next_event_time: null,
previous_event_time: null,
update: true,
});
}}
variant="outline"
@ -537,17 +496,24 @@ const EntriesNavigation = () => {
>
Refresh to newest
</Button>
) : (
<Button
isLoading
loadingText="Loading"
variant="outline"
colorScheme="suggested"
></Button>
)}
{streamBoundary.next_event_time &&
streamBoundary.end_time != 0 &&
!isLoading ? (
!isFetching ? (
<Button
onClick={() => {
updateStreamBoundaryWith({
end_time: streamBoundary.next_event_time + 5 * 60,
include_start: false,
include_end: true,
update: true,
});
}}
variant="outline"
@ -559,7 +525,9 @@ const EntriesNavigation = () => {
"" // some strange behaivior without else condition return 0 wich can see on frontend page
)}
</Stack>
{entries.map((entry, idx) => (
{entries
?.sort((a, b) => b.timestamp - a.timestamp) // TODO(Andrey) improve that for bi chunks of data sorting can take time
.map((entry, idx) => (
<StreamEntry
key={`entry-list-${idx}`}
entry={entry}
@ -569,18 +537,16 @@ const EntriesNavigation = () => {
filterConstants={{ DIRECTIONS, CONDITION, FILTER_TYPES }}
/>
))}
{streamBoundary.previous_event_time && !isLoading && (
{streamBoundary.previous_event_time && !isFetching ? (
<Center>
<Button
onClick={() => {
remove();
updateStreamBoundaryWith({
start_time: streamBoundary.previous_event_time - 5 * 60,
include_start: false,
include_end: true,
update: true,
});
//fetchPreviousPage();
}}
variant="outline"
colorScheme="suggested"
@ -588,11 +554,24 @@ const EntriesNavigation = () => {
Go to previous transaction
</Button>
</Center>
) : (
<Center>
{!isFetching ? (
"Тransactions not found. You can subscribe to more addresses in Subscriptions menu."
) : (
<Button
isLoading
loadingText="Loading"
variant="outline"
colorScheme="suggested"
></Button>
)}
{streamBoundary.previous_event_time && isLoading && (
</Center>
)}
{streamBoundary.previous_event_time && isLoading ? (
<Center>
<Spinner
hidden={!isFetchingMore}
//hidden={!isFetchingMore}
ref={loadMoreButtonRef}
my={8}
size="lg"
@ -601,6 +580,8 @@ const EntriesNavigation = () => {
speed="1.5s"
/>
</Center>
) : (
""
)}
</Flex>
</Flex>

Wyświetl plik

@ -14,13 +14,17 @@ import {
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 { handleSubmit, errors, register } = useForm({});
const [radioState, setRadioState] = useState("ethereum_blockchain");
let { getRootProps, getRadioProps } = useRadioGroup({
name: "type",
@ -41,10 +45,15 @@ const NewSubscription = ({ isFreeOption, onClose }) => {
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>
@ -83,7 +92,7 @@ const NewSubscription = ({ isFreeOption, onClose }) => {
: `On which source?`}
</Text>
<FormControl isInvalid={errors.type}>
<FormControl isInvalid={errors.subscription_type}>
<HStack {...group} alignItems="stretch">
{typesCache.data.subscriptions.map((type) => {
const radio = getRadioProps({
@ -100,9 +109,54 @@ const NewSubscription = ({ isFreeOption, onClose }) => {
);
})}
</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>
<Input placeholder="color" name="color" ref={register()}></Input>
<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
@ -112,7 +166,9 @@ const NewSubscription = ({ isFreeOption, onClose }) => {
>
Confirm
</Button>
<Button colorScheme="gray">Cancel</Button>
<Button colorScheme="gray" onClick={onClose}>
Cancel
</Button>
</ModalFooter>
</form>
);

Wyświetl plik

@ -0,0 +1,79 @@
import React, { useContext } from "react";
import { Flex, HStack, Skeleton, Heading } from "@chakra-ui/react";
import { useTxInfo } from "../core/hooks";
import FourOFour from "./FourOFour";
import FourOThree from "./FourOThree";
import Tags from "./Tags";
import Scrollable from "./Scrollable";
import TxInfo from "./TxInfo";
import UIContext from "../core/providers/UIProvider/context";
const SteamEntryDetails = () => {
const ui = useContext(UIContext);
const {
data: entry,
isFetchedAfterMount,
isLoading,
isFetching, //If transaction.tx is undefined, will not fetch
isError,
error,
} = useTxInfo({ tx: ui.currentTransaction });
if (!isFetching) {
return "";
}
if (isError && error.response.status === 404) return <FourOFour />;
if (isError && error.response.status === 403) return <FourOThree />;
return (
<Flex id="Entry" height="100%" flexGrow="1" flexDirection="column">
<Skeleton
id="EntryNameSkeleton"
mx={2}
mt={2}
overflow="initial"
isLoaded={!isLoading}
>
<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"
textAlign="left"
>
{entry && entry.tx.hash}
</Heading>
</HStack>
</Skeleton>
<Skeleton
id="TagsSkeleton"
mx={2}
overflow="initial"
mt={1}
isLoaded={isFetchedAfterMount || entry}
>
<Tags entry={entry} />
</Skeleton>
<Skeleton
height="10px"
flexGrow={1}
id="EditorSkeleton"
mx={2}
mr={isFetchedAfterMount || entry ? 0 : 2}
mt={1}
isLoaded={isFetchedAfterMount || entry}
>
<Scrollable>
{!isLoading && <TxInfo transaction={entry}></TxInfo>}
</Scrollable>
</Skeleton>
</Flex>
);
};
export default SteamEntryDetails;

Wyświetl plik

@ -9,16 +9,18 @@ import {
Heading,
Image,
useMediaQuery,
Spacer,
Spinner,
} from "@chakra-ui/react";
import moment from "moment";
import { ArrowRightIcon } from "@chakra-ui/icons";
import { useRouter } from "../core/hooks";
import UIContext from "../core/providers/UIProvider/context";
import { useToast, useTxCashe } from "../core/hooks";
import { useToast } from "../core/hooks";
import { useSubscriptions } from "../core/hooks";
const StreamEntry = ({ entry, filterCallback, filterConstants }) => {
const StreamEntry = ({ entry }) => {
const { subscriptionsCache } = useSubscriptions();
const ui = useContext(UIContext);
const router = useRouter();
const [copyString, setCopyString] = useState(false);
const { onCopy, hasCopied } = useClipboard(copyString, 1);
const toast = useToast();
@ -31,17 +33,19 @@ const StreamEntry = ({ entry, filterCallback, filterConstants }) => {
onCopy();
}
}, [copyString, onCopy, hasCopied, toast]);
const handleViewClicked = (entryId) => {
ui.setEntryId(entryId);
ui.setEntriesViewMode("entry");
useTxCashe.setCurrentTransaction(entry);
router.push({
pathname: `/stream/${entry.hash}`,
query: router.query,
});
};
const [showFullView] = useMediaQuery(["(min-width: 420px)"]);
if (subscriptionsCache.isLoading) return <Spinner />;
const from_color =
subscriptionsCache.data.subscriptions.find((obj) => {
return obj.address === entry.from_address;
})?.color ?? "gray.500";
const to_color =
subscriptionsCache.data.subscriptions.find((obj) => {
return obj.address === entry.to_address;
})?.color ?? "gray.500";
return (
<Flex
@ -53,7 +57,6 @@ const StreamEntry = ({ entry, filterCallback, filterConstants }) => {
bgColor="gray.100"
borderColor="white.300"
transition="0.1s"
_hover={{ bg: "secondary.200" }}
flexBasis="50px"
direction="row"
justifySelf="center"
@ -80,9 +83,11 @@ const StreamEntry = ({ entry, filterCallback, filterConstants }) => {
borderLeftRadius="md"
borderColor="gray.600"
spacing={0}
h="fit-content"
minH="fit-content"
h="auto"
// h="fit-content"
// minH="fit-content"
overflowX="hidden"
overflowY="visible"
>
<Stack
className="title"
@ -93,7 +98,7 @@ const StreamEntry = ({ entry, filterCallback, filterConstants }) => {
textAlign="center"
spacing={0}
alignItems="center"
bgColor="brand.300"
bgColor="gray.300"
>
<Image
boxSize="16px"
@ -101,7 +106,17 @@ const StreamEntry = ({ entry, filterCallback, filterConstants }) => {
"https://upload.wikimedia.org/wikipedia/commons/0/05/Ethereum_logo_2014.svg"
}
/>
<Heading size="xs">Ethereum blockhain</Heading>
<Heading px={1} size="xs">
Hash
</Heading>
<Spacer />
<Text
isTruncated
onClick={() => setCopyString(entry.hash)}
pr={12}
>
{entry.hash}
</Text>
</Stack>
<Stack
className="CardAddressesRow"
@ -129,7 +144,7 @@ const StreamEntry = ({ entry, filterCallback, filterConstants }) => {
spacing={0}
>
<Text
bgColor="secondary.500"
bgColor="gray.600"
h="100%"
fontSize="sm"
py="2px"
@ -143,7 +158,7 @@ const StreamEntry = ({ entry, filterCallback, filterConstants }) => {
mx={0}
py="2px"
fontSize="sm"
bgColor="secondary.200"
bgColor={from_color}
isTruncated
w="calc(100%)"
h="100%"
@ -166,9 +181,8 @@ const StreamEntry = ({ entry, filterCallback, filterConstants }) => {
spacing={0}
>
<Text
bgColor="primary.500"
bgColor="gray.600"
h="100%"
color="white"
py={1}
px={2}
w={showFullView ? null : "120px"}
@ -177,7 +191,7 @@ const StreamEntry = ({ entry, filterCallback, filterConstants }) => {
</Text>
<Tooltip label={entry.to_address} aria-label="From:">
<Text
bgColor="primary.200"
bgColor={to_color}
isTruncated
w="calc(100%)"
h="100%"
@ -188,31 +202,8 @@ const StreamEntry = ({ entry, filterCallback, filterConstants }) => {
</Tooltip>
</Stack>
</Stack>
<Stack
className="ValuesRow"
direction={showFullView ? "row" : "column"}
alignItems={showFullView ? "center" : "flex-start"}
placeContent="space-evenly"
// h="1rem"
w="100%"
// h="1.6rem"
minH="2rem"
textAlign="center"
spacing={0}
bgColor="primimary.50"
>
<Stack
direction="row"
fontSize="sm"
fontWeight="600"
borderColor="gray.1200"
borderRightWidth={showFullView ? "1px" : "0px"}
placeContent="center"
spacing={0}
flexBasis="10px"
flexGrow={1}
w="100%"
>
<Flex flexWrap="wrap" w="100%">
<Flex minH="2rem" minW="fit-content" flexGrow={1}>
<Text
h="100%"
fontSize="sm"
@ -236,19 +227,8 @@ const StreamEntry = ({ entry, filterCallback, filterConstants }) => {
{entry.gasPrice}
</Text>
</Tooltip>
</Stack>
<Stack
direction="row"
fontSize="sm"
fontWeight="600"
borderColor="gray.1200"
borderRightWidth={showFullView ? "1px" : "0px"}
placeContent="center"
spacing={0}
flexBasis="10px"
flexGrow={1}
w="100%"
>
</Flex>
<Flex h="2rem" minW="fit-content" flexGrow={1}>
<Text
w={showFullView ? null : "120px"}
h="100%"
@ -271,21 +251,8 @@ const StreamEntry = ({ entry, filterCallback, filterConstants }) => {
{entry.gas}
</Text>
</Tooltip>
</Stack>
<Stack
direction="row"
fontSize="sm"
fontWeight="600"
borderColor="gray.1200"
borderRightWidth={
entry.timestamp ? (showFullView ? "1px" : "0px") : "0px"
}
placeContent="center"
spacing={0}
flexBasis="10px"
flexGrow={1}
w="100%"
>
</Flex>
<Flex h="2rem" minW="fit-content" flexGrow={1}>
<Text
w={showFullView ? null : "120px"}
h="100%"
@ -308,38 +275,63 @@ const StreamEntry = ({ entry, filterCallback, filterConstants }) => {
{entry.value}
</Text>
</Tooltip>
</Stack>
{entry.timestamp && (
<Stack
direction="row"
</Flex>
<Flex h="2rem" minW="fit-content" flexGrow={1}>
<Text
w={showFullView ? null : "120px"}
h="100%"
fontSize="sm"
fontWeight="600"
placeContent="center"
spacing={0}
flexBasis="10px"
flexGrow={1}
py="2px"
px={2}
textAlign="justify"
>
Nonce:
</Text>
<Tooltip label={entry.value} aria-label="Value:">
<Text
mx={0}
py="2px"
fontSize="sm"
w="calc(100%)"
h="100%"
onClick={() => setCopyString(entry.value)}
>
{entry.nonce}
</Text>
</Tooltip>
</Flex>
{entry.timestamp && (
<Flex h="auto" minW="fit-content">
<Text
px={1}
mx={0}
py="2px"
fontSize="sm"
w="calc(100%)"
h="100%"
borderColor="gray.700"
>
<Text mx={0} py="2px" fontSize="sm" w="calc(100%)" h="100%">
{moment(entry.timestamp * 1000).format(
"DD MMM, YYYY, HH:mm:ss"
)}{" "}
</Text>
</Stack>
</Flex>
)}
</Stack>
</Flex>
</Stack>
)}
<Flex>
<IconButton
m={0}
onClick={() => handleViewClicked(entry)}
onClick={() => ui.setCurrentTransaction(entry)}
h="full"
// minH="24px"
borderLeftRadius={0}
variant="solid"
px={0}
minW="24px"
colorScheme="suggested"
colorScheme="secondary"
icon={<ArrowRightIcon w="24px" />}
/>
</Flex>

Wyświetl plik

@ -7,6 +7,7 @@ import {
Tr,
Thead,
Tbody,
Tooltip,
Editable,
EditableInput,
Image,
@ -17,13 +18,17 @@ import moment from "moment";
import CopyButton from "./CopyButton";
import { useSubscriptions } from "../core/hooks";
import ConfirmationRequest from "./ConfirmationRequest";
import ColorSelector from "./ColorSelector";
const SubscriptionsList = () => {
const { subscriptionsCache, changeNote, deleteSubscription } =
const { subscriptionsCache, updateSubscription, deleteSubscription } =
useSubscriptions();
const updateCallback = ({ id, note }) => {
changeNote.mutate({ id, note });
const updateCallback = ({ id, label, color }) => {
const data = { id: id };
label && (data.label = label);
color && (data.color = color);
updateSubscription.mutate(data);
};
if (subscriptionsCache.data) {
@ -45,6 +50,7 @@ const SubscriptionsList = () => {
<Th>Token</Th>
<Th>Label</Th>
<Th>Address</Th>
<Th>Color</Th>
<Th>Date Created</Th>
<Th>Actions</Th>
</Tr>
@ -52,8 +58,8 @@ const SubscriptionsList = () => {
<Tbody>
{subscriptionsCache.data.subscriptions.map((subscription) => {
let iconLink;
switch (subscription.subscription_type) {
case "ethereum_blockchain":
switch (subscription.subscription_type_id) {
case "0":
iconLink =
"https://ethereum.org/static/c48a5f760c34dfadcf05a208dab137cc/31987/eth-diamond-rainbow.png";
break;
@ -73,9 +79,11 @@ const SubscriptionsList = () => {
console.error("no icon found for this pool");
}
return (
<Tr key={`token-row-${subscription.address}`}>
<Tr key={`token-row-${subscription.id}`}>
<Td>
<Tooltip label="Ethereum blockchain" fontSize="md">
<Image h="32px" src={iconLink} alt="pool icon" />
</Tooltip>
</Td>
<Td py={0}>
<Editable
@ -99,6 +107,15 @@ const SubscriptionsList = () => {
<Td mr={4} p={0}>
<CopyButton>{subscription.address}</CopyButton>
</Td>
<Td>
<ColorSelector
// subscriptionId={subscription.id}
initialColor={subscription.color}
callback={(color) =>
updateCallback({ id: subscription.id, color: color })
}
/>
</Td>
<Td py={0}>{moment(subscription.created_at).format("L")}</Td>
<Td py={0}>

Wyświetl plik

@ -10,7 +10,9 @@ import {
VStack,
} from "@chakra-ui/react";
import { Table, Thead, Tbody, Tr, Th, Td } from "@chakra-ui/react";
const toEth = (wei) => {
return wei / Math.pow(10, 18);
};
const TxABI = (props) => {
const byteCode = props.byteCode;
const abi = props.abi;
@ -58,17 +60,18 @@ const TxInfo = (props) => {
<StatGroup>
<Stat>
<StatLabel>Value</StatLabel>
<StatNumber>{transaction.tx.value}</StatNumber>
<StatHelpText>amount of ETH to transfer in WEI</StatHelpText>
<StatNumber>{toEth(transaction.tx.value)} eth</StatNumber>
<StatHelpText>amount of ETH to transfer</StatHelpText>
</Stat>
<Stat>
<StatLabel>Gas</StatLabel>
<StatLabel>Gas limit</StatLabel>
<StatNumber>{transaction.tx.gas}</StatNumber>
<StatHelpText>gas limit for transaction</StatHelpText>
<StatHelpText>Maximum amount of gas </StatHelpText>
<StatHelpText>provided for the transaction</StatHelpText>
</Stat>
<Stat>
<StatLabel>Gas price</StatLabel>
<StatNumber>{transaction.tx.gasPrice}</StatNumber>
<StatNumber>{toEth(transaction.tx.gasPrice)} eth</StatNumber>
<StatHelpText>the fee the sender pays per unit of gas</StatHelpText>
</Stat>
</StatGroup>

Wyświetl plik

@ -20,5 +20,4 @@ export { default as useStripe } from "./useStripe";
export { default as useSubscriptions } from "./useSubscriptions";
export { default as useToast } from "./useToast";
export { default as useTxInfo } from "./useTxInfo";
export { default as useTxCashe } from "./useTxCache";
export { default as useUser } from "./useUser";

Wyświetl plik

@ -3,14 +3,12 @@ import { useQuery } from "react-query";
import { queryCacheProps } from "./hookCommon";
const useJournalEntries = ({
refreshRate,
searchQuery,
start_time,
end_time,
include_start,
include_end,
updateStreamBoundaryWith,
enabled,
}) => {
// set our get method
const getStream =
@ -36,18 +34,19 @@ const useJournalEntries = ({
};
};
const { data, isLoading, refetch } = useQuery(
["stream", searchQuery],
const { data, isLoading, refetch, isFetching, remove } = useQuery(
["stream", searchQuery, start_time, end_time],
getStream(searchQuery, start_time, end_time, include_start, include_end),
{
//refetchInterval: refreshRate,
...queryCacheProps,
keepPreviousData: true,
retry: 3,
onSuccess: (response) => {
// response is object which return condition in getStream
// TODO(andrey): Response should send page parameters inside "boundary" object (can be null).
updateStreamBoundaryWith(response.boundaries);
},
enabled: !!enabled,
}
);
@ -55,6 +54,8 @@ const useJournalEntries = ({
EntriesPages: data,
isLoading,
refetch,
isFetching,
remove,
};
};
export default useJournalEntries;

Wyświetl plik

@ -50,12 +50,15 @@ const useSubscriptions = () => {
}
);
const changeNote = useMutation(SubscriptionsService.modifySubscription(), {
const updateSubscription = useMutation(
SubscriptionsService.modifySubscription(),
{
onError: (error) => toast(error, "error"),
onSuccess: () => {
subscriptionsCache.refetch();
},
});
}
);
const deleteSubscription = useMutation(
SubscriptionsService.deleteSubscription(),
@ -71,7 +74,7 @@ const useSubscriptions = () => {
createSubscription,
subscriptionsCache,
typesCache,
changeNote,
updateSubscription,
deleteSubscription,
};
};

Wyświetl plik

@ -1,11 +0,0 @@
class TxCashe {
currentTransaction = undefined;
getCurrentTransaction() {
return this.currentTransaction;
}
setCurrentTransaction(transaction) {
this.currentTransaction = transaction;
}
}
const useTxCashe = new TxCashe();
export default useTxCashe;

Wyświetl plik

@ -4,27 +4,27 @@ import { queryCacheProps } from "./hookCommon";
import { useToast } from ".";
const useTxInfo = (transaction) => {
if (!transaction.tx)
return {
data: "undefined",
isLoading: false,
isFetchedAfterMount: true,
refetch: false,
isError: true,
error: "undefined",
};
const toast = useToast();
const getTxInfo = async () => {
const response = await TxInfoService.getTxInfo(transaction);
return response.data;
};
const { data, isLoading, isFetchedAfterMount, refetch, isError, error } =
useQuery(["txinfo", transaction.tx.hash], getTxInfo, {
useQuery(["txinfo", transaction.tx && transaction.tx.hash], getTxInfo, {
...queryCacheProps,
enabled: !!transaction.tx,
onError: (error) => toast(error, "error"),
});
return { data, isFetchedAfterMount, isLoading, refetch, isError, error };
const isFetching = !!transaction.tx;
return {
data,
isFetchedAfterMount,
isLoading,
refetch,
isFetching,
isError,
error,
};
};
export default useTxInfo;

Wyświetl plik

@ -28,7 +28,7 @@ const AnalyticsProvider = ({ children }) => {
},
{ transport: "sendBeacon" }
);
}, 1000);
}, 30000);
return () => clearInterval(intervalId);
// eslint-disable-next-line

Wyświetl plik

@ -30,8 +30,6 @@ const UIProvider = ({ children }) => {
const { modal, toggleModal } = useContext(ModalContext);
const [searchTerm, setSearchTerm] = useQuery("q", "", true, false);
const [entryId, setEntryId] = useState();
const [searchBarActive, setSearchBarActive] = useState(false);
// ****** Session state *****
@ -136,18 +134,30 @@ const UIProvider = ({ children }) => {
// *********** Entries layout states **********************
//
// const [entryId, setEntryId] = useState();
// Current transaction to show in sideview
const [currentTransaction, _setCurrentTransaction] = useState(undefined);
const [isEntryDetailView, setEntryDetailView] = useState(false);
const setCurrentTransaction = (tx) => {
_setCurrentTransaction(tx);
setEntryDetailView(!!tx);
};
/**
* States that entries list box should be expanded
* Default true in mobile mode and false in desktop mode
*/
const [entriesViewMode, setEntriesViewMode] = useState(
router.params?.entryId ? "entry" : "list"
isMobileView ? "list" : "split"
);
useEffect(() => {
setEntriesViewMode(router.params?.entryId ? "entry" : "list");
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [router.params?.id]);
setEntriesViewMode(
isMobileView ? (isEntryDetailView ? "entry" : "list") : "split"
);
}, [isEntryDetailView, isMobileView]);
// ********************************************************
@ -171,12 +181,13 @@ const UIProvider = ({ children }) => {
isLoggedIn,
isAppReady,
entriesViewMode,
setEntriesViewMode,
setEntryDetailView,
modal,
toggleModal,
entryId,
setEntryId,
sessionId,
currentTransaction,
setCurrentTransaction,
isEntryDetailView,
}}
>
{children}

Wyświetl plik

@ -10,7 +10,6 @@ import * as InvitesService from "./invites.service";
import * as SubscriptionsService from "./subscriptions.service";
import * as StreamService from "./stream.service";
import * as TxInfoService from "./txinfo.service";
console.log("StreamService", StreamService);
export {
SearchService,
AuthService,

Wyświetl plik

@ -9,15 +9,26 @@ export const getStream = ({
end_time,
include_start,
include_end,
}) =>
http({
}) => {
let params = {
q: searchTerm,
};
if (start_time || start_time === 0) {
params.start_time = start_time;
}
if (end_time || end_time === 0) {
params.end_time = end_time;
}
if (include_start) {
params.include_start = include_start;
}
if (include_end) {
params.include_end = include_end;
}
return http({
method: "GET",
url: `${API}/streams/`,
params: {
q: searchTerm,
start_time: start_time,
end_time: end_time,
include_start: include_start,
include_end: include_end,
},
params,
});
};

Wyświetl plik

@ -50,13 +50,13 @@ export const createSubscription =
export const modifySubscription =
() =>
({ id, note }) => {
({ id, label, color }) => {
const data = new FormData();
data.append("note", note);
data.append("id", id);
color && data.append("color", color);
label && data.append("label", label);
return http({
method: "POST",
url: `${API}/subscription/${id}`,
method: "PUT",
url: `${API}/subscriptions/${id}`,
data,
});
};

Wyświetl plik

@ -0,0 +1,9 @@
export const makeColor = () => {
var result = "#";
var characters = "0123456789ABCDEF";
var charactersLength = characters.length;
for (var i = 0; i < 6; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
};

Wyświetl plik

@ -20,6 +20,7 @@ const EntriesLayout = (props) => {
<>
<Flex id="Entries" flexGrow={1} maxW="100%">
<SplitPane
allowResize={false}
split="vertical"
defaultSize={defaultWidth}
primary="first"
@ -29,7 +30,11 @@ const EntriesLayout = (props) => {
? { transition: "1s", width: "100%" }
: ui.entriesViewMode === "entry"
? { transition: "1s", width: "0%" }
: { overflowX: "hidden", height: "100%" }
: {
overflowX: "hidden",
height: "100%",
width: ui.isMobileView ? "100%" : "55%",
}
}
pane2Style={
ui.entriesViewMode === "entry"

Wyświetl plik

@ -0,0 +1,81 @@
/* Make clicks pass-through */
#nprogress {
pointer-events: none;
}
#nprogress .bar {
background: #fd5602;
position: fixed;
z-index: 1031;
top: 0;
left: 0;
width: 100%;
height: 3px;
}
/* Fancy blur effect */
#nprogress .peg {
display: block;
position: absolute;
right: 0px;
width: 100px;
height: 100%;
box-shadow: 0 0 10px #fd5602, 0 0 5px #fd5602;
opacity: 1;
-webkit-transform: rotate(3deg) translate(0px, -4px);
-ms-transform: rotate(3deg) translate(0px, -4px);
transform: rotate(3deg) translate(0px, -4px);
}
/* Remove these to get rid of the spinner */
#nprogress .spinner {
display: none;
position: fixed;
z-index: 1031;
top: 15px;
right: 15px;
}
#nprogress .spinner-icon {
width: 18px;
height: 18px;
box-sizing: border-box;
border: solid 2px transparent;
border-top-color: #29d;
border-left-color: #29d;
border-radius: 50%;
-webkit-animation: nprogress-spinner 400ms linear infinite;
animation: nprogress-spinner 400ms linear infinite;
}
.nprogress-custom-parent {
overflow: hidden;
position: relative;
}
.nprogress-custom-parent #nprogress .spinner,
.nprogress-custom-parent #nprogress .bar {
position: absolute;
}
@-webkit-keyframes nprogress-spinner {
0% {
-webkit-transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
}
}
@keyframes nprogress-spinner {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

Wyświetl plik

@ -46,6 +46,7 @@
}
.Resizer.disabled:hover {
border-color: transparent;
cursor: inherit;
}
.triangle {
@ -181,12 +182,10 @@
padding: 0.5rem;
}
code {
white-space: pre-line !important;
}
.fade-in-section {
opacity: 0;
transform: translateY(5vh);
@ -195,7 +194,6 @@ code {
will-change: opacity, visibility;
}
.fade-in-section.is-visible {
opacity: 1;
transform: none;
visibility: visible;

Wyświetl plik

@ -977,6 +977,11 @@
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.2.0.tgz#f3933a44e365864f4dad5db94158106d511e8131"
integrity sha512-sqKVVVOe5ivCaXDWivIJYVSaEgdQK9ul7a4Kity5Iw7u9+wBAPbX1RMSnLLmp7O4Vzj0WOWwMAJsTL00xwaNug==
"@icons/material@^0.2.4":
version "0.2.4"
resolved "https://registry.yarnpkg.com/@icons/material/-/material-0.2.4.tgz#e90c9f71768b3736e76d7dd6783fc6c2afa88bc8"
integrity sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==
"@jest/types@^26.6.2":
version "26.6.2"
resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e"
@ -2565,6 +2570,11 @@ focus-lock@^0.8.1:
dependencies:
tslib "^1.9.3"
focus-visible@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/focus-visible/-/focus-visible-5.2.0.tgz#3a9e41fccf587bd25dcc2ef045508284f0a4d6b3"
integrity sha512-Rwix9pBtC1Nuy5wysTmKy+UjbDJpIfg8eHjw0rjZ1mX4GNLz1Bmd16uDpI3Gk1i70Fgcs8Csg2lPm8HULFg9DQ==
follow-redirects@^1.10.0:
version "1.14.1"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.1.tgz#d9114ded0a1cfdd334e164e6662ad02bfd91ff43"
@ -3197,6 +3207,11 @@ locate-path@^5.0.0:
dependencies:
p-locate "^4.1.0"
lodash-es@^4.17.15:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee"
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
lodash.clonedeep@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz#e23f3f9c4f8fbdde872529c1071857a086e5ccef"
@ -3222,7 +3237,7 @@ lodash.truncate@^4.4.2:
resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193"
integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=
lodash@^4.17.13, lodash@^4.17.21, lodash@^4.17.4:
lodash@^4.0.1, lodash@^4.17.13, lodash@^4.17.15, lodash@^4.17.21, lodash@^4.17.4:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@ -3261,6 +3276,11 @@ match-sorter@^6.0.2:
"@babel/runtime" "^7.12.5"
remove-accents "0.4.2"
material-colors@^1.2.1:
version "1.2.6"
resolved "https://registry.yarnpkg.com/material-colors/-/material-colors-1.2.6.tgz#6d1958871126992ceecc72f4bcc4d8f010865f46"
integrity sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==
md5.js@^1.3.4:
version "1.3.5"
resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f"
@ -3489,6 +3509,11 @@ normalize-path@^3.0.0, normalize-path@~3.0.0:
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
nprogress@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/nprogress/-/nprogress-0.2.0.tgz#cb8f34c53213d895723fcbab907e9422adbcafb1"
integrity sha1-y480xTIT2JVyP8urkH6UIq28r7E=
object-assign@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
@ -3834,7 +3859,7 @@ progress@^2.0.0:
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
prop-types@15.7.2, prop-types@^15.5.4, prop-types@^15.5.8, prop-types@^15.6.2, prop-types@^15.7.2:
prop-types@15.7.2, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.8, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.7.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@ -3934,6 +3959,19 @@ react-clientside-effect@^1.2.2:
dependencies:
"@babel/runtime" "^7.12.13"
react-color@^2.19.3:
version "2.19.3"
resolved "https://registry.yarnpkg.com/react-color/-/react-color-2.19.3.tgz#ec6c6b4568312a3c6a18420ab0472e146aa5683d"
integrity sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA==
dependencies:
"@icons/material" "^0.2.4"
lodash "^4.17.15"
lodash-es "^4.17.15"
material-colors "^1.2.1"
prop-types "^15.5.10"
reactcss "^1.2.0"
tinycolor2 "^1.4.1"
react-copy-to-clipboard@^5.0.2:
version "5.0.3"
resolved "https://registry.yarnpkg.com/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.3.tgz#2a0623b1115a1d8c84144e9434d3342b5af41ab4"
@ -4101,6 +4139,13 @@ react@^17.0.2:
loose-envify "^1.1.0"
object-assign "^4.1.1"
reactcss@^1.2.0:
version "1.2.3"
resolved "https://registry.yarnpkg.com/reactcss/-/reactcss-1.2.3.tgz#c00013875e557b1cf0dfd9a368a1c3dab3b548dd"
integrity sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==
dependencies:
lodash "^4.0.1"
read-pkg-up@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07"
@ -4673,7 +4718,7 @@ tiny-invariant@^1.0.6:
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
tinycolor2@1.4.2:
tinycolor2@1.4.2, tinycolor2@^1.4.1:
version "1.4.2"
resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.2.tgz#3f6a4d1071ad07676d7fa472e1fac40a719d8803"
integrity sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==