kopia lustrzana https://github.com/bugout-dev/moonstream
Merge branch 'main' into whale-watch
commit
d55f25d5e3
|
@ -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/
|
||||
|
|
|
@ -4,6 +4,11 @@ on:
|
|||
push:
|
||||
paths:
|
||||
- "frontend/**"
|
||||
pull_request:
|
||||
branches:
|
||||
- "main"
|
||||
paths:
|
||||
- "frontend/**"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
|
61
README.md
61
README.md
|
@ -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).
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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()
|
||||
)
|
||||
|
||||
|
|
|
@ -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,10 +90,11 @@ async def get_transaction_in_blocks(
|
|||
text("timestamp desc")
|
||||
).limit(1)
|
||||
).one_or_none()
|
||||
boundaries.end_time = ethereum_transaction_start_point[-1]
|
||||
boundaries.start_time = (
|
||||
ethereum_transaction_start_point[-1] - DEFAULT_STREAM_TIMEINTERVAL
|
||||
)
|
||||
if ethereum_transaction_start_point:
|
||||
boundaries.end_time = ethereum_transaction_start_point[-1]
|
||||
boundaries.start_time = (
|
||||
ethereum_transaction_start_point[-1] - DEFAULT_STREAM_TIMEINTERVAL
|
||||
)
|
||||
|
||||
if boundaries.start_time != 0 and boundaries.end_time != 0:
|
||||
if boundaries.start_time > boundaries.end_time:
|
||||
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"],
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
|
|
@ -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'
|
|
@ -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
|
|
@ -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)
|
||||
|
|
@ -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,20 +122,31 @@ 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:
|
||||
block: BlockData = web3_client.eth.get_block(
|
||||
block_number, full_transactions=with_transactions
|
||||
)
|
||||
add_block(db_session, block)
|
||||
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
|
||||
)
|
||||
add_block(db_session, block)
|
||||
|
||||
if with_transactions:
|
||||
add_block_transactions(db_session, block)
|
||||
if with_transactions:
|
||||
add_block_transactions(db_session, block)
|
||||
|
||||
db_session.commit()
|
||||
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
|
|
@ -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()
|
|
@ -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()
|
|
@ -2,4 +2,4 @@
|
|||
Moonstream crawlers version.
|
||||
"""
|
||||
|
||||
MOONSTREAMCRAWLERS_VERSION = "0.0.1"
|
||||
MOONCRAWL_VERSION = "0.0.2"
|
|
@ -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>"
|
||||
|
|
|
@ -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",
|
||||
]
|
||||
},
|
||||
)
|
||||
|
|
|
@ -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__,
|
||||
|
|
|
@ -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 ###
|
|
@ -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()
|
|
@ -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
|
||||
|
|
|
@ -2,4 +2,4 @@
|
|||
Moonstream database version.
|
||||
"""
|
||||
|
||||
MOONSTREAMDB_VERSION = "0.0.1"
|
||||
MOONSTREAMDB_VERSION = "0.0.2"
|
||||
|
|
|
@ -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",
|
||||
]
|
||||
},
|
||||
)
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -1,49 +1,74 @@
|
|||
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 getLayout =
|
||||
Component.getLayout || ((page) => <DefaultLayout>{page}</DefaultLayout>);
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<>
|
||||
<style global jsx>{`
|
||||
html,
|
||||
body,
|
||||
body > div:first-child,
|
||||
div#__next,
|
||||
div#__next > div {
|
||||
height: 100% !important;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
`}</style>
|
||||
{pageProps.metaTags && <HeadSEO {...pageProps.metaTags} />}
|
||||
{pageProps.preloads && <HeadLinks links={pageProps.preloads} />}
|
||||
<QueryClientProvider client={this.state.queryClient}>
|
||||
<AppContext>{getLayout(<Component {...pageProps} />)}</AppContext>
|
||||
</QueryClientProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
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>{`
|
||||
html,
|
||||
body,
|
||||
body > div:first-child,
|
||||
div#__next,
|
||||
div#__next > div {
|
||||
height: 100% !important;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
`}</style>
|
||||
{pageProps.metaTags && <HeadSEO {...pageProps.metaTags} />}
|
||||
{pageProps.preloads && <HeadLinks links={pageProps.preloads} />}
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AppContext>{getLayout(<Component {...pageProps} />)}</AppContext>
|
||||
</QueryClientProvider>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -58,6 +58,12 @@ const Security = () => {
|
|||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
document.title = `Change password`;
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) router.push("/");
|
||||
}, [data, router]);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,11 +152,16 @@ 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]);
|
||||
|
||||
|
@ -155,351 +198,352 @@ const Homepage = () => {
|
|||
}, []);
|
||||
|
||||
return (
|
||||
<Fade in>
|
||||
<Box
|
||||
width="100%"
|
||||
flexDirection="column"
|
||||
sx={{ scrollBehavior: "smooth" }}
|
||||
bgSize="cover"
|
||||
>
|
||||
<Flex
|
||||
direction="column"
|
||||
h="auto"
|
||||
position="relative"
|
||||
w="100%"
|
||||
overflow="initial"
|
||||
pt={0}
|
||||
<Suspense fallback="">
|
||||
<Fade in>
|
||||
<Box
|
||||
width="100%"
|
||||
flexDirection="column"
|
||||
sx={{ scrollBehavior: "smooth" }}
|
||||
bgSize="cover"
|
||||
>
|
||||
<Suspense fallback={""}></Suspense>
|
||||
|
||||
<Grid
|
||||
templateColumns="repeat(12,1fr)"
|
||||
mt={0}
|
||||
border="none"
|
||||
boxSizing="content-box"
|
||||
<Flex
|
||||
direction="column"
|
||||
h="auto"
|
||||
position="relative"
|
||||
w="100%"
|
||||
overflow="initial"
|
||||
pt={0}
|
||||
>
|
||||
<GridItem
|
||||
<Suspense fallback={""}></Suspense>
|
||||
|
||||
<Grid
|
||||
templateColumns="repeat(12,1fr)"
|
||||
mt={0}
|
||||
px="0"
|
||||
colSpan="12"
|
||||
pb={[1, 2, null, 8]}
|
||||
minH="100vh"
|
||||
// bgColor="primary.1200"
|
||||
border="none"
|
||||
boxSizing="content-box"
|
||||
>
|
||||
<chakra.header boxSize="full" minH="100vh">
|
||||
<Box
|
||||
bgPos="bottom"
|
||||
bgColor="transparent"
|
||||
backgroundImage={`url(${assets[`${background}`]})`}
|
||||
bgSize="cover"
|
||||
boxSize="full"
|
||||
minH="100vh"
|
||||
>
|
||||
<Flex align="center" justify="center" boxSize="full">
|
||||
<Stack
|
||||
textAlign="center"
|
||||
alignItems="center"
|
||||
spacing={6}
|
||||
maxW="1620px"
|
||||
px="7%"
|
||||
h="100%"
|
||||
pt={["10vh", null, "30vh"]}
|
||||
>
|
||||
<Heading size="2xl" fontWeight="semibold" color="white">
|
||||
All the crypto data you care about in a single stream
|
||||
</Heading>
|
||||
<chakra.span
|
||||
my={12}
|
||||
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
|
||||
pool to Elon Musk’s latest tweets.
|
||||
</chakra.span>
|
||||
<chakra.span
|
||||
fontSize={["lg", null, "xl"]}
|
||||
display="inline-block"
|
||||
color="primary.300"
|
||||
textDecor="underline"
|
||||
>
|
||||
Access this data through the Moonstream dashboard or API
|
||||
</chakra.span>
|
||||
</Stack>
|
||||
</Flex>
|
||||
</Box>
|
||||
</chakra.header>
|
||||
</GridItem>
|
||||
|
||||
<GridItem
|
||||
px="7%"
|
||||
colSpan="12"
|
||||
pt={["20px", "20px", "100px", null, "120px"]}
|
||||
pb={["20px", "56px", null, "184px"]}
|
||||
minH="100vh"
|
||||
>
|
||||
<Heading
|
||||
{...HEADING_PROPS}
|
||||
textAlign="center"
|
||||
pb={[12, 12, 12, null, 48]}
|
||||
<GridItem
|
||||
mt={0}
|
||||
px="0"
|
||||
colSpan="12"
|
||||
pb={[1, 2, null, 8]}
|
||||
minH="100vh"
|
||||
// bgColor="primary.1200"
|
||||
>
|
||||
Data you can add to your stream:
|
||||
</Heading>
|
||||
<chakra.header boxSize="full" minH="100vh">
|
||||
<Box
|
||||
bgPos="bottom"
|
||||
bgColor="transparent"
|
||||
backgroundImage={`url(${assets[`${background}`]})`}
|
||||
bgSize="cover"
|
||||
boxSize="full"
|
||||
minH="100vh"
|
||||
>
|
||||
<Flex align="center" justify="center" boxSize="full">
|
||||
<Stack
|
||||
textAlign="center"
|
||||
alignItems="center"
|
||||
spacing={6}
|
||||
maxW="1620px"
|
||||
px="7%"
|
||||
h="100%"
|
||||
pt={["10vh", null, "20vh"]}
|
||||
>
|
||||
<Heading size="2xl" fontWeight="semibold" color="white">
|
||||
All the crypto data you care about in a single stream
|
||||
</Heading>
|
||||
<chakra.span
|
||||
my={12}
|
||||
fontSize={["lg", null, "xl"]}
|
||||
display="inline-block"
|
||||
color="primary.200"
|
||||
>
|
||||
Get all the crypto data you need in a single stream.
|
||||
From pending transactions in the Ethereum transaction
|
||||
pool to Elon Musk’s latest tweets.
|
||||
</chakra.span>
|
||||
<chakra.span
|
||||
fontSize={["lg", null, "xl"]}
|
||||
display="inline-block"
|
||||
color="primary.300"
|
||||
>
|
||||
Access this data through the Moonstream dashboard or
|
||||
API
|
||||
</chakra.span>
|
||||
</Stack>
|
||||
</Flex>
|
||||
</Box>
|
||||
</chakra.header>
|
||||
</GridItem>
|
||||
|
||||
<SimpleGrid columns={[1, 2, 2, 4, null, 4]}>
|
||||
<Stack spacing={1} px={1} alignItems="center">
|
||||
<ChakraImage
|
||||
boxSize={["220px", "220px", "xs", null, "xs"]}
|
||||
objectFit="contain"
|
||||
src={assets["minedTransactions"]}
|
||||
alt="mined transactions"
|
||||
/>
|
||||
<Heading textAlign="center ">
|
||||
Ethereum mined transactions
|
||||
</Heading>
|
||||
</Stack>
|
||||
<Stack spacing={1} px={1} alignItems="center">
|
||||
<ChakraImage
|
||||
boxSize={["220px", "220px", "xs", null, "xs"]}
|
||||
objectFit="contain"
|
||||
src={assets["pendingTransactions"]}
|
||||
alt="mined transactions"
|
||||
/>
|
||||
<Heading textAlign="center ">
|
||||
Ethereum pending transactions
|
||||
</Heading>
|
||||
</Stack>
|
||||
<Stack spacing={1} px={1} alignItems="center">
|
||||
<ChakraImage
|
||||
boxSize={["220px", "220px", "xs", null, "xs"]}
|
||||
objectFit="contain"
|
||||
src={assets["priceInformation"]}
|
||||
alt="mined transactions"
|
||||
/>
|
||||
<Heading textAlign="center ">Centralized exchanges</Heading>
|
||||
</Stack>
|
||||
<Stack spacing={1} px={1} alignItems="center">
|
||||
<ChakraImage
|
||||
boxSize={["220px", "220px", "xs", null, "xs"]}
|
||||
objectFit="contain"
|
||||
src={assets["socialMediaPosts"]}
|
||||
alt="mined transactions"
|
||||
/>
|
||||
<Heading textAlign="center ">Social media posts</Heading>
|
||||
</Stack>
|
||||
</SimpleGrid>
|
||||
<Center>
|
||||
<Heading pt="160px" pb="60px">
|
||||
Moonstream is ment for you if
|
||||
<GridItem
|
||||
px="7%"
|
||||
colSpan="12"
|
||||
pt={["20px", "20px", "100px", null, "120px"]}
|
||||
pb={["20px", "56px", null, "184px"]}
|
||||
minH="100vh"
|
||||
>
|
||||
<Heading
|
||||
{...HEADING_PROPS}
|
||||
textAlign="center"
|
||||
pb={[12, 12, 12, null, 48]}
|
||||
>
|
||||
Data you can add to your stream:
|
||||
</Heading>
|
||||
</Center>
|
||||
<Flex
|
||||
w="100%"
|
||||
direction={["column", "row", "column", null, "column"]}
|
||||
flexWrap={["nowrap", "nowrap", "nowrap", null, "nowrap"]}
|
||||
pb="66px"
|
||||
>
|
||||
<ConnectedButtons
|
||||
title={"You are..."}
|
||||
button1={{
|
||||
label: "Crypto trader",
|
||||
link: "/#cryptoTrader",
|
||||
onClick: () => {
|
||||
track(`${MIXPANEL_EVENTS.BUTTON_CLICKED}`, {
|
||||
[`${MIXPANEL_PROPS.BUTTON_CLICKED}`]: `scroll to CryptoTrader`,
|
||||
});
|
||||
},
|
||||
}}
|
||||
button2={{
|
||||
label: "Algorithmic Fund",
|
||||
link: "/#algoFund",
|
||||
onClick: () => {
|
||||
track(`${MIXPANEL_EVENTS.BUTTON_CLICKED}`, {
|
||||
[`${MIXPANEL_PROPS.BUTTON_CLICKED}`]: `scroll to AlgoFund`,
|
||||
});
|
||||
},
|
||||
}}
|
||||
button3={{
|
||||
label: "Developer",
|
||||
link: "/#smartDeveloper",
|
||||
onClick: () => {
|
||||
track(`${MIXPANEL_EVENTS.BUTTON_CLICKED}`, {
|
||||
[`${MIXPANEL_PROPS.BUTTON_CLICKED}`]: `scroll to Developer`,
|
||||
});
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</GridItem>
|
||||
<GridItem
|
||||
px="7%"
|
||||
colSpan="12"
|
||||
pt={["1rem", "1rem", "5.125rem", null, "5.125rem"]}
|
||||
pb={["0", "66px", null, "66px"]}
|
||||
id="cryptoTrader"
|
||||
minH={ui.isMobileView ? "100vh" : null}
|
||||
>
|
||||
<SplitWithImage
|
||||
cta={{
|
||||
label: "I want early access!",
|
||||
onClick: () => {
|
||||
track(`${MIXPANEL_EVENTS.BUTTON_CLICKED}`, {
|
||||
[`${MIXPANEL_PROPS.BUTTON_CLICKED}`]: `Early access CTA: Crypto trader`,
|
||||
});
|
||||
toggleModal("hubspot-trader");
|
||||
},
|
||||
}}
|
||||
elementName={"element1"}
|
||||
colorScheme="suggested"
|
||||
badge={`For crypto traders`}
|
||||
title={``}
|
||||
body={``}
|
||||
bullets={[
|
||||
{
|
||||
text: `Subscribe to the defi contracts you care about`,
|
||||
icon: FaFileContract,
|
||||
color: "suggested.50",
|
||||
bgColor: "suggested.900",
|
||||
},
|
||||
{
|
||||
text: `Make sense of how others are calling these contracts using Moonstream dashboards.
|
||||
`,
|
||||
icon: RiDashboardFill,
|
||||
color: "suggested.50",
|
||||
bgColor: "suggested.900",
|
||||
},
|
||||
{
|
||||
text: `Get data directly from the transaction pool through our global network of Ethereum nodes`,
|
||||
icon: GiMeshBall,
|
||||
color: "suggested.50",
|
||||
bgColor: "suggested.900",
|
||||
},
|
||||
]}
|
||||
imgURL={assets["cryptoTraders"]}
|
||||
/>
|
||||
</GridItem>
|
||||
<GridItem
|
||||
px="7%"
|
||||
colSpan="12"
|
||||
pt={["1rem", "1rem", "5.125rem", null, "5.125rem"]}
|
||||
pb={["0", "66px", null, "66px"]}
|
||||
id="algoFund"
|
||||
minH={ui.isMobileView ? "100vh" : null}
|
||||
>
|
||||
<SplitWithImage
|
||||
cta={{
|
||||
label: "I want early access!",
|
||||
onClick: () => {
|
||||
track(`${MIXPANEL_EVENTS.BUTTON_CLICKED}`, {
|
||||
[`${MIXPANEL_PROPS.BUTTON_CLICKED}`]: `Early access CTA: Algo fund`,
|
||||
});
|
||||
toggleModal("hubspot-fund");
|
||||
},
|
||||
}}
|
||||
elementName={"element2"}
|
||||
mirror={true}
|
||||
colorScheme="secondary"
|
||||
badge={`For algorithmic funds`}
|
||||
bullets={[
|
||||
{
|
||||
text: `Get API access to your stream`,
|
||||
icon: AiFillApi,
|
||||
color: "secondary.50",
|
||||
bgColor: "secondary.900",
|
||||
},
|
||||
{
|
||||
text: `Set conditions that trigger predefined actions`,
|
||||
icon: GiLogicGateXor,
|
||||
color: "secondary.50",
|
||||
bgColor: "secondary.900",
|
||||
},
|
||||
{
|
||||
text: `Execute transactions directly on Moonstream nodes`,
|
||||
icon: BiTransfer,
|
||||
color: "secondary.50",
|
||||
bgColor: "secondary.900",
|
||||
},
|
||||
]}
|
||||
imgURL={assets["algorithmicFunds"]}
|
||||
/>
|
||||
</GridItem>
|
||||
<GridItem
|
||||
px="7%"
|
||||
colSpan="12"
|
||||
pt={["1rem", "1rem", "5.125rem", null, "5.125rem"]}
|
||||
pb={["0", "66px", null, "66px"]}
|
||||
id="smartDeveloper"
|
||||
minH={ui.isMobileView ? "100vh" : null}
|
||||
>
|
||||
<SplitWithImage
|
||||
cta={{
|
||||
label: "I want early access!",
|
||||
onClick: () => {
|
||||
track(`${MIXPANEL_EVENTS.BUTTON_CLICKED}`, {
|
||||
[`${MIXPANEL_PROPS.BUTTON_CLICKED}`]: `Early access CTA: developer`,
|
||||
});
|
||||
toggleModal("hubspot-developer");
|
||||
},
|
||||
}}
|
||||
elementName={"element3"}
|
||||
colorScheme="primary"
|
||||
badge={`For smart contract developers`}
|
||||
bullets={[
|
||||
{
|
||||
text: `See how people use your smart contracts`,
|
||||
icon: IoTelescopeSharp,
|
||||
color: "primary.50",
|
||||
bgColor: "primary.900",
|
||||
},
|
||||
{
|
||||
text: `Set up alerts on suspicious activity`,
|
||||
icon: GiSuspicious,
|
||||
color: "primary.50",
|
||||
bgColor: "primary.900",
|
||||
},
|
||||
{
|
||||
text: `Register webhooks to connect your off-chain infrastructure`,
|
||||
icon: GiHook,
|
||||
color: "primary.50",
|
||||
bgColor: "primary.900",
|
||||
},
|
||||
]}
|
||||
imgURL={assets["smartDevelopers"]}
|
||||
/>
|
||||
</GridItem>
|
||||
<GridItem
|
||||
placeItems="center"
|
||||
w="100%"
|
||||
colSpan="12"
|
||||
pt={["0", "0", "5.125rem", null, "5.125rem"]}
|
||||
pb="120px"
|
||||
>
|
||||
<Center>
|
||||
<Button
|
||||
as={Link}
|
||||
isExternal
|
||||
href={"https://discord.gg/K56VNUQGvA"}
|
||||
size="lg"
|
||||
variant="solid"
|
||||
colorScheme="suggested"
|
||||
id="test"
|
||||
onClick={() => {
|
||||
track(`${MIXPANEL_EVENTS.BUTTON_CLICKED}`, {
|
||||
[`${MIXPANEL_PROPS.BUTTON_CLICKED}`]: `Join our discord`,
|
||||
});
|
||||
toggleModal("hubspot");
|
||||
}}
|
||||
|
||||
<SimpleGrid columns={[1, 2, 2, 4, null, 4]}>
|
||||
<Stack spacing={1} px={1} alignItems="center">
|
||||
<ChakraImage
|
||||
boxSize={["220px", "220px", "xs", null, "xs"]}
|
||||
objectFit="contain"
|
||||
src={assets["minedTransactions"]}
|
||||
alt="mined transactions"
|
||||
/>
|
||||
<Heading textAlign="center ">
|
||||
Ethereum mined transactions
|
||||
</Heading>
|
||||
</Stack>
|
||||
<Stack spacing={1} px={1} alignItems="center">
|
||||
<ChakraImage
|
||||
boxSize={["220px", "220px", "xs", null, "xs"]}
|
||||
objectFit="contain"
|
||||
src={assets["pendingTransactions"]}
|
||||
alt="mined transactions"
|
||||
/>
|
||||
<Heading textAlign="center ">
|
||||
Ethereum pending transactions
|
||||
</Heading>
|
||||
</Stack>
|
||||
<Stack spacing={1} px={1} alignItems="center">
|
||||
<ChakraImage
|
||||
boxSize={["220px", "220px", "xs", null, "xs"]}
|
||||
objectFit="contain"
|
||||
src={assets["priceInformation"]}
|
||||
alt="mined transactions"
|
||||
/>
|
||||
<Heading textAlign="center ">Centralized exchanges</Heading>
|
||||
</Stack>
|
||||
<Stack spacing={1} px={1} alignItems="center">
|
||||
<ChakraImage
|
||||
boxSize={["220px", "220px", "xs", null, "xs"]}
|
||||
objectFit="contain"
|
||||
src={assets["socialMediaPosts"]}
|
||||
alt="mined transactions"
|
||||
/>
|
||||
<Heading textAlign="center ">Social media posts</Heading>
|
||||
</Stack>
|
||||
</SimpleGrid>
|
||||
<Center>
|
||||
<Heading pt="160px" pb="60px">
|
||||
Moonstream is meant for you if
|
||||
</Heading>
|
||||
</Center>
|
||||
<Flex
|
||||
w="100%"
|
||||
direction={["column", "row", "column", null, "column"]}
|
||||
flexWrap={["nowrap", "nowrap", "nowrap", null, "nowrap"]}
|
||||
pb="66px"
|
||||
>
|
||||
Join our discord
|
||||
</Button>
|
||||
</Center>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Fade>
|
||||
<ConnectedButtons
|
||||
title={"You are..."}
|
||||
button1={{
|
||||
label: "Crypto trader",
|
||||
link: "/#cryptoTrader",
|
||||
onClick: () => {
|
||||
track(`${MIXPANEL_EVENTS.BUTTON_CLICKED}`, {
|
||||
[`${MIXPANEL_PROPS.BUTTON_CLICKED}`]: `scroll to CryptoTrader`,
|
||||
});
|
||||
},
|
||||
}}
|
||||
button2={{
|
||||
label: "Algorithmic Fund",
|
||||
link: "/#algoFund",
|
||||
onClick: () => {
|
||||
track(`${MIXPANEL_EVENTS.BUTTON_CLICKED}`, {
|
||||
[`${MIXPANEL_PROPS.BUTTON_CLICKED}`]: `scroll to AlgoFund`,
|
||||
});
|
||||
},
|
||||
}}
|
||||
button3={{
|
||||
label: "Developer",
|
||||
link: "/#smartDeveloper",
|
||||
onClick: () => {
|
||||
track(`${MIXPANEL_EVENTS.BUTTON_CLICKED}`, {
|
||||
[`${MIXPANEL_PROPS.BUTTON_CLICKED}`]: `scroll to Developer`,
|
||||
});
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</GridItem>
|
||||
<GridItem
|
||||
px="7%"
|
||||
colSpan="12"
|
||||
pt={["1rem", "1rem", "5.125rem", null, "5.125rem"]}
|
||||
pb={["0", "66px", null, "66px"]}
|
||||
id="cryptoTrader"
|
||||
minH={ui.isMobileView ? "100vh" : null}
|
||||
>
|
||||
<SplitWithImage
|
||||
cta={{
|
||||
label: "I want early access!",
|
||||
onClick: () => {
|
||||
track(`${MIXPANEL_EVENTS.BUTTON_CLICKED}`, {
|
||||
[`${MIXPANEL_PROPS.BUTTON_CLICKED}`]: `Early access CTA: Crypto trader`,
|
||||
});
|
||||
toggleModal("hubspot-trader");
|
||||
},
|
||||
}}
|
||||
elementName={"element1"}
|
||||
colorScheme="suggested"
|
||||
badge={`For crypto traders`}
|
||||
title={``}
|
||||
body={``}
|
||||
bullets={[
|
||||
{
|
||||
text: `Subscribe to the defi contracts you care about`,
|
||||
icon: FaFileContract,
|
||||
color: "suggested.50",
|
||||
bgColor: "suggested.900",
|
||||
},
|
||||
{
|
||||
text: `Make sense of how others are calling these contracts using Moonstream dashboards.
|
||||
`,
|
||||
icon: RiDashboardFill,
|
||||
color: "suggested.50",
|
||||
bgColor: "suggested.900",
|
||||
},
|
||||
{
|
||||
text: `Get data directly from the transaction pool through our global network of Ethereum nodes`,
|
||||
icon: GiMeshBall,
|
||||
color: "suggested.50",
|
||||
bgColor: "suggested.900",
|
||||
},
|
||||
]}
|
||||
imgURL={assets["cryptoTraders"]}
|
||||
/>
|
||||
</GridItem>
|
||||
<GridItem
|
||||
px="7%"
|
||||
colSpan="12"
|
||||
pt={["1rem", "1rem", "5.125rem", null, "5.125rem"]}
|
||||
pb={["0", "66px", null, "66px"]}
|
||||
id="algoFund"
|
||||
minH={ui.isMobileView ? "100vh" : null}
|
||||
>
|
||||
<SplitWithImage
|
||||
cta={{
|
||||
label: "I want early access!",
|
||||
onClick: () => {
|
||||
track(`${MIXPANEL_EVENTS.BUTTON_CLICKED}`, {
|
||||
[`${MIXPANEL_PROPS.BUTTON_CLICKED}`]: `Early access CTA: Algo fund`,
|
||||
});
|
||||
toggleModal("hubspot-fund");
|
||||
},
|
||||
}}
|
||||
elementName={"element2"}
|
||||
mirror={true}
|
||||
colorScheme="secondary"
|
||||
badge={`For algorithmic funds`}
|
||||
bullets={[
|
||||
{
|
||||
text: `Get API access to your stream`,
|
||||
icon: AiFillApi,
|
||||
color: "secondary.50",
|
||||
bgColor: "secondary.900",
|
||||
},
|
||||
{
|
||||
text: `Set conditions that trigger predefined actions`,
|
||||
icon: GiLogicGateXor,
|
||||
color: "secondary.50",
|
||||
bgColor: "secondary.900",
|
||||
},
|
||||
{
|
||||
text: `Execute transactions directly on Moonstream nodes`,
|
||||
icon: BiTransfer,
|
||||
color: "secondary.50",
|
||||
bgColor: "secondary.900",
|
||||
},
|
||||
]}
|
||||
imgURL={assets["algorithmicFunds"]}
|
||||
/>
|
||||
</GridItem>
|
||||
<GridItem
|
||||
px="7%"
|
||||
colSpan="12"
|
||||
pt={["1rem", "1rem", "5.125rem", null, "5.125rem"]}
|
||||
pb={["0", "66px", null, "66px"]}
|
||||
id="smartDeveloper"
|
||||
minH={ui.isMobileView ? "100vh" : null}
|
||||
>
|
||||
<SplitWithImage
|
||||
cta={{
|
||||
label: "I want early access!",
|
||||
onClick: () => {
|
||||
track(`${MIXPANEL_EVENTS.BUTTON_CLICKED}`, {
|
||||
[`${MIXPANEL_PROPS.BUTTON_CLICKED}`]: `Early access CTA: developer`,
|
||||
});
|
||||
toggleModal("hubspot-developer");
|
||||
},
|
||||
}}
|
||||
elementName={"element3"}
|
||||
colorScheme="primary"
|
||||
badge={`For smart contract developers`}
|
||||
bullets={[
|
||||
{
|
||||
text: `See how people use your smart contracts`,
|
||||
icon: IoTelescopeSharp,
|
||||
color: "primary.50",
|
||||
bgColor: "primary.900",
|
||||
},
|
||||
{
|
||||
text: `Set up alerts on suspicious activity`,
|
||||
icon: GiSuspicious,
|
||||
color: "primary.50",
|
||||
bgColor: "primary.900",
|
||||
},
|
||||
{
|
||||
text: `Register webhooks to connect your off-chain infrastructure`,
|
||||
icon: GiHook,
|
||||
color: "primary.50",
|
||||
bgColor: "primary.900",
|
||||
},
|
||||
]}
|
||||
imgURL={assets["smartDevelopers"]}
|
||||
/>
|
||||
</GridItem>
|
||||
<GridItem
|
||||
placeItems="center"
|
||||
w="100%"
|
||||
colSpan="12"
|
||||
pt={["0", "0", "5.125rem", null, "5.125rem"]}
|
||||
pb="120px"
|
||||
>
|
||||
<Center>
|
||||
<Button
|
||||
as={Link}
|
||||
isExternal
|
||||
href={"https://discord.gg/K56VNUQGvA"}
|
||||
size="lg"
|
||||
variant="solid"
|
||||
colorScheme="suggested"
|
||||
id="test"
|
||||
onClick={() => {
|
||||
track(`${MIXPANEL_EVENTS.BUTTON_CLICKED}`, {
|
||||
[`${MIXPANEL_PROPS.BUTTON_CLICKED}`]: `Join our discord`,
|
||||
});
|
||||
toggleModal("hubspot");
|
||||
}}
|
||||
>
|
||||
Join our discord
|
||||
</Button>
|
||||
</Center>
|
||||
</GridItem>
|
||||
</Grid>
|
||||
</Flex>
|
||||
</Box>
|
||||
</Fade>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -530,7 +574,4 @@ export async function getStaticProps() {
|
|||
};
|
||||
}
|
||||
|
||||
Homepage.layout = "default";
|
||||
Homepage.getLayout = getLayout;
|
||||
|
||||
export default Homepage;
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -62,7 +62,6 @@ const Subscriptions = () => {
|
|||
<Center>
|
||||
<Spinner
|
||||
hidden={false}
|
||||
// ref={loadMoreButtonRef}
|
||||
my={8}
|
||||
size="lg"
|
||||
color="primary.500"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
|
@ -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,34 +478,42 @@ const EntriesNavigation = () => {
|
|||
//onScroll={(e) => handleScroll(e)}
|
||||
>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setStreamBoundary({
|
||||
start_time: null,
|
||||
end_time: null,
|
||||
include_start: false,
|
||||
include_end: true,
|
||||
next_event_time: null,
|
||||
previous_event_time: null,
|
||||
update: true,
|
||||
});
|
||||
}}
|
||||
variant="outline"
|
||||
colorScheme="suggested"
|
||||
>
|
||||
Refresh to newest
|
||||
</Button>
|
||||
{!isFetching ? (
|
||||
<Button
|
||||
onClick={() => {
|
||||
remove();
|
||||
setStreamBoundary({
|
||||
start_time: null,
|
||||
end_time: null,
|
||||
include_start: false,
|
||||
include_end: true,
|
||||
next_event_time: null,
|
||||
previous_event_time: null,
|
||||
});
|
||||
}}
|
||||
variant="outline"
|
||||
colorScheme="suggested"
|
||||
>
|
||||
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,28 +525,28 @@ const EntriesNavigation = () => {
|
|||
"" // some strange behaivior without else condition return 0 wich can see on frontend page
|
||||
)}
|
||||
</Stack>
|
||||
{entries.map((entry, idx) => (
|
||||
<StreamEntry
|
||||
key={`entry-list-${idx}`}
|
||||
entry={entry}
|
||||
disableDelete={!canDelete}
|
||||
disableCopy={!canCreate}
|
||||
filterCallback={handleFilterStateCallback}
|
||||
filterConstants={{ DIRECTIONS, CONDITION, FILTER_TYPES }}
|
||||
/>
|
||||
))}
|
||||
{streamBoundary.previous_event_time && !isLoading && (
|
||||
{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}
|
||||
disableDelete={!canDelete}
|
||||
disableCopy={!canCreate}
|
||||
filterCallback={handleFilterStateCallback}
|
||||
filterConstants={{ DIRECTIONS, CONDITION, FILTER_TYPES }}
|
||||
/>
|
||||
))}
|
||||
{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>
|
||||
)}
|
||||
</Center>
|
||||
)}
|
||||
{streamBoundary.previous_event_time && isLoading && (
|
||||
{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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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;
|
|
@ -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"
|
||||
>
|
||||
<Text mx={0} py="2px" fontSize="sm" w="calc(100%)" h="100%">
|
||||
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"
|
||||
>
|
||||
{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>
|
||||
|
|
|
@ -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>
|
||||
<Image h="32px" src={iconLink} alt="pool icon" />
|
||||
<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}>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -50,12 +50,15 @@ const useSubscriptions = () => {
|
|||
}
|
||||
);
|
||||
|
||||
const changeNote = useMutation(SubscriptionsService.modifySubscription(), {
|
||||
onError: (error) => toast(error, "error"),
|
||||
onSuccess: () => {
|
||||
subscriptionsCache.refetch();
|
||||
},
|
||||
});
|
||||
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,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
class TxCashe {
|
||||
currentTransaction = undefined;
|
||||
getCurrentTransaction() {
|
||||
return this.currentTransaction;
|
||||
}
|
||||
setCurrentTransaction(transaction) {
|
||||
this.currentTransaction = transaction;
|
||||
}
|
||||
}
|
||||
const useTxCashe = new TxCashe();
|
||||
export default useTxCashe;
|
|
@ -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;
|
||||
|
|
|
@ -28,7 +28,7 @@ const AnalyticsProvider = ({ children }) => {
|
|||
},
|
||||
{ transport: "sendBeacon" }
|
||||
);
|
||||
}, 1000);
|
||||
}, 30000);
|
||||
|
||||
return () => clearInterval(intervalId);
|
||||
// eslint-disable-next-line
|
||||
|
|
|
@ -28,9 +28,7 @@ const UIProvider = ({ children }) => {
|
|||
});
|
||||
|
||||
const { modal, toggleModal } = useContext(ModalContext);
|
||||
const [searchTerm, setSearchTerm] = useQuery("q", " ", true, false);
|
||||
|
||||
const [entryId, setEntryId] = useState();
|
||||
const [searchTerm, setSearchTerm] = useQuery("q", "", true, false);
|
||||
|
||||
const [searchBarActive, setSearchBarActive] = useState(false);
|
||||
|
||||
|
@ -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}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -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"
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -46,6 +46,7 @@
|
|||
}
|
||||
.Resizer.disabled:hover {
|
||||
border-color: transparent;
|
||||
cursor: inherit;
|
||||
}
|
||||
|
||||
.triangle {
|
||||
|
@ -156,7 +157,7 @@
|
|||
word-break: break-word;
|
||||
}
|
||||
|
||||
.mde-preview * {
|
||||
.mde-preview * {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
|
@ -181,12 +182,10 @@
|
|||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
|
||||
code {
|
||||
white-space: pre-line !important;
|
||||
}
|
||||
|
||||
|
||||
.fade-in-section {
|
||||
opacity: 0;
|
||||
transform: translateY(5vh);
|
||||
|
@ -195,8 +194,7 @@ code {
|
|||
will-change: opacity, visibility;
|
||||
}
|
||||
.fade-in-section.is-visible {
|
||||
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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==
|
||||
|
|
Ładowanie…
Reference in New Issue