Commit 18c8581d authored by tom's avatar tom

Merge branch 'main' of github.com:blockscout/frontend into charts-fixes

parents 8dc1eca9 80bda612
......@@ -2,3 +2,4 @@ NEXT_PUBLIC_SENTRY_DSN=xxx
SENTRY_CSP_REPORT_URI=xxx
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=xxx
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=xxx
NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID=UA-XXXXXX-X
\ No newline at end of file
......@@ -56,6 +56,7 @@ NEXT_PUBLIC_SENTRY_DSN=__PLACEHOLDER_FOR_NEXT_PUBLIC_SENTRY_DSN__
NEXT_PUBLIC_AUTH0_CLIENT_ID=__PLACEHOLDER_FOR_NEXT_PUBLIC_AUTH0_CLIENT_ID__
NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID=__PLACEHOLDER_FOR_NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID__
NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY=__PLACEHOLDER_FOR_NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY__
NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID=__PLACEHOLDER_FOR_NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID__
# l2 config
NEXT_PUBLIC_IS_L2_NETWORK=__PLACEHOLDER_FOR_NEXT_PUBLIC_IS_L2_NETWORKL__
......
......@@ -30,6 +30,7 @@ module.exports = {
'jsx-a11y',
'eslint-plugin-import-helpers',
'jest',
'eslint-plugin-no-cyrillic-string',
],
parser: '@typescript-eslint/parser',
parserOptions: {
......@@ -117,7 +118,7 @@ module.exports = {
'@typescript-eslint/type-annotation-spacing': 'error',
'@typescript-eslint/no-explicit-any': [ 'error', { ignoreRestArgs: true } ],
// отключены в пользу @typescript-eslint
// disabled in favor of @typescript-eslint
'brace-style': 'off',
camelcase: 'off',
indent: 'off',
......@@ -269,7 +270,7 @@ module.exports = {
'regexp/no-empty-capturing-group': 'error',
'regexp/no-lazy-ends': 'error',
'regexp/no-obscure-range': [ 'error', {
allowed: [ 'alphanumeric', 'А-Я', 'а-я' ],
allowed: [ 'alphanumeric' ],
} ],
'regexp/no-optional-assertion': 'error',
'regexp/no-unused-capturing-group': [ 'error', {
......@@ -277,6 +278,8 @@ module.exports = {
} ],
'regexp/no-useless-character-class': 'error',
'regexp/no-useless-dollar-replacements': 'error',
'no-cyrillic-string/no-cyrillic-string': 'error',
},
overrides: [
{
......
......@@ -36,7 +36,7 @@ To perform testing locally you need to install docker and run `yarn test:pw:dock
The app instance could be customized by passing following variables to NodeJS environment at runtime.
**IMPORTANT NOTE!** For _production_ build purposes all json-like values should be single-quoted
**IMPORTANT NOTE!** For _production_ build purposes all json-like values should be single-quoted. If it contains a hash (`#`) or a dollar-sign (`$`) the whole value should be wrapped in single quotes as well (see `dotenv` [readme](https://github.com/bkeepers/dotenv#variable-substitution))
### Network configuration
......@@ -130,6 +130,7 @@ The app instance could be customized by passing following variables to NodeJS en
| NEXT_PUBLIC_AUTH0_CLIENT_ID | `string` | Client id for [Auth0](https://auth0.com/) provider | `<secret>` |
| NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID | `string` | Project id for [WalletConnect](https://docs.walletconnect.com/2.0/web3modal/react/installation#obtain-project-id) integration | `<secret>` |
| NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY | `string` | Site key for [reCAPTCHA](https://developers.google.com/recaptcha) service | `<secret>` |
| NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID | `string` | Property ID for [Google Analytics](https://analytics.google.com/) service | `UA-XXXXXX-X` |
### L2 configuration
| Variable | Type | Description | Default value
......
......@@ -6,7 +6,7 @@ import type { ChainIndicatorId } from 'ui/home/indicators/types';
const getEnvValue = (env: string | undefined) => env?.replaceAll('\'', '"');
const parseEnvJson = <DataType>(env: string | undefined): DataType | null => {
try {
return JSON.parse(env || 'null');
return JSON.parse(env || 'null') as DataType | null;
} catch (error) {
return null;
}
......@@ -133,6 +133,9 @@ const config = Object.freeze({
reCaptcha: {
siteKey: getEnvValue(process.env.NEXT_PUBLIC_RE_CAPTCHA_APP_SITE_KEY) || '',
},
googleAnalytics: {
propertyId: getEnvValue(process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS_PROPERTY_ID),
},
});
export default config;
......@@ -5,20 +5,14 @@ blockscout:
app: blockscout
enabled: true
image:
_default: &image blockscout/blockscout-optimism-l2-advanced:5.1.0-prerelease-3097e10c
_default: &image blockscout/blockscout-optimism-l2-advanced:5.1.0-prerelease-7a8c745e
replicas:
app: 1
docker:
port: 80
targetPort: 4000
# init container
init:
enabled: true
image:
_default: *image
service:
# ClusterIP, NodePort or LoadBalancer
type: ClusterIP
# enable ingress
ingress:
enabled: true
......@@ -64,17 +58,6 @@ blockscout:
_default: "1Gi"
cpu:
_default: "1"
# enable service to connect to RDS
rds:
enabled: false
endpoint:
_default: <endpoint>.<region>.rds.amazonaws.com
# node label
nodeSelector:
enabled: true
labels:
_default:
app: blockscout
# Blockscout environment variables
environment:
ENV:
......@@ -149,6 +132,12 @@ blockscout:
_default: '8299683'
INDEXER_OPTIMISM_OUTPUT_ORACLE_L1:
_default: 0xE6Dfba0953616Bacab0c9A8ecb3a9BBa77FC15c0
INDEXER_OPTIMISM_BATCH_START_BLOCK_L1:
_default: '8381594'
INDEXER_OPTIMISM_BATCH_INBOX:
_default: 0xff00000000000000000000000000000000000420
INDEXER_OPTIMISM_BATCH_SUBMITTER:
_default: 0x7431310e026b69bfc676c0013e12a1a11411eec9
postgres:
enabled: true
......@@ -188,30 +177,6 @@ scVerifier:
enabled: true
image:
_default: ghcr.io/blockscout/smart-contract-verifier:main
replicas:
app: 1
ports:
http:
number: 8050
protocol: TCP
host: 'http.'
path: "/"
pathType: Prefix
grpc:
number: 8051
protocol: TCP
host: 'grpc.'
path: "/"
pathType: Prefix
metrics:
number: 6060
protocol: TCP
host: "metrics."
path: "/metrics"
pathType: Exact
service:
# ClusterIP, NodePort or LoadBalancer
type: ClusterIP
# enable ingress
ingress:
enabled: true
......@@ -231,24 +196,6 @@ scVerifier:
_default: "0.5Gi"
cpu:
_default: "0.25"
# node label
nodeSelector:
enabled: true
labels:
app: blockscout
# probes
livenessProbe:
enabled: true
# path: /health
readinessProbe:
enabled: true
# path: /health
# enable Horizontal Pod Autoscaler
hpa:
enabled: true
minReplicas: 1
maxReplicas: 10
cpuTarget: 90
environment:
SMART_CONTRACT_VERIFIER__SERVER__HTTP__ADDR:
_default: 0.0.0.0:8050
......@@ -292,17 +239,6 @@ stats:
image:
_default: ghcr.io/blockscout/stats:main
replicas:
app: 1
docker:
port: 80
targetPort: 8050
metricsPort: 6060
service:
# ClusterIP, NodePort or LoadBalancer
type: ClusterIP
# enable ingress
ingress:
enabled: true
......@@ -329,11 +265,6 @@ stats:
cpu:
_default: "0.25"
# node label
nodeSelector:
enabled: true
app: blockscout
environment:
RUST_LOG:
_default: info
......@@ -349,11 +280,6 @@ frontend:
enabled: true
image:
_default: ghcr.io/blockscout/frontend:main
replicas:
app: 1
docker:
port: 80
targetPort: 3000
ingress:
enabled: true
# annotations:
......@@ -394,11 +320,6 @@ frontend:
_default: "0.3Gi"
cpu:
_default: "0.2"
# node label
nodeSelector:
enabled: true
labels:
app: blockscout
environment:
NEXT_PUBLIC_BLOCKSCOUT_VERSION:
_default: v5.1.0-beta
......
......@@ -258,126 +258,12 @@ geth:
enabled: false
files:
enabled: true
# geth:
# enabled: true
# image:
# _default: ethereum/client-go:stable
# replicas:
# app: 1
# portHttp: 8545
# portWs: 8546
# portAuth: 8551
# command:
# - /bin/sh
# - -c
# - |-
# /root/init.sh --fakepow --dev --dev.period=1 --datadir=/root/.ethereum/devnet --keystore=/root/.ethereum/devnet/keystore --password=/root/password.txt --unlock=0 --unlock=1 --mine --miner.threads=1 --miner.etherbase=0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 --ipcpath=/root/geth.ipc --http --http.vhosts=* --http.addr=0.0.0.0 --http.port=8545 --http.api=eth,net,web3,debug,txpool --ws --ws.origins=* --ws.addr=0.0.0.0 --ws.port=8546 --ws.api=eth,net,web3,debug,txpool --graphql --graphql.corsdomain=* --allow-insecure-unlock --rpc.allow-unprotected-txs --http.corsdomain=* --vmdebug --networkid=1337 --rpc.txfeecap=0
# # command: '["geth"]'
# # args: '["--goerli", "--datadir=/.ethereum", "--gcmode=archive", "--http", "--http.vhosts=*", "--http.addr=0.0.0.0", "--http.port=8545", "--http.api=eth,net,web3,debug,txpool", "--ws", "--ws.origins=*", "--ws.addr=0.0.0.0", "--ws.port=8546", "--ws.api=eth,net,web3,debug,txpool", "--allow-insecure-unlock", "--rpc.allow-unprotected-txs", "--http.corsdomain=*", "--vmdebug", "--rpc.txfeecap=0"]'
# environment: {}
# persistence:
# enabled: true
# mountPath: /geth
# storageClass: gp3-new
# storage: 1000Gi
# resources:
# limits:
# memory:
# _default: "8Gi"
# cpu:
# _default: "2"
# requests:
# memory:
# _default: "8Gi"
# cpu:
# _default: "2"
# # node label
# nodeSelector:
# enabled: true
# app: blockscout
# service:
# # ClusterIP, NodePort or LoadBalancer
# type: ClusterIP
# # enable ingress
# ingress:
# enabled: true
# host:
# _default: node.test.blockscout.aws-k8s.blockscout.com
# # enable https
# tls:
# enabled: false
# jwt:
# enabled: true
# mountPath: /geth/geth/jwtsecret
# files:
# enabled: false
# # enable client deploy (Prysm, lighthouse, nimbus, etc.)
# client:
# enabled: true
# image:
# _default: gcr.io/prysmaticlabs/prysm/beacon-chain:stable
# # command: '["sh","./root/init.sh"]'
# args: '["--goerli", "--datadir=/data", "--jwt-secret=/geth/geth/jwtsecret", "--rpc-host=0.0.0.0", "--grpc-gateway-host=0.0.0.0", "--monitoring-host=0.0.0.0", "--execution-endpoint=http://geth-svc:8545", "--checkpoint-sync-url=https://goerli.checkpoint-sync.ethdevops.io", "--genesis-beacon-api-url=https://goerli.checkpoint-sync.ethdevops.io"]'
# ports:
# port-tcp:
# number: 13000
# protocol: TCP
# port-udp:
# number: 12000
# protocol: UDP
# port-rpc:
# number: 4000
# protocol: TCP
# port-rpc-gtw:
# number: 3500
# protocol: TCP
# port-monitoring:
# number: 8080
# protocol: TCP
# persistence:
# enabled: false
# mountPath: path
# storageClass: gp3-new
# storage: 100Gi
# files:
# enabled: false
# list: {}
# service:
# # ClusterIP, NodePort or LoadBalancer
# type: ClusterIP
# # enable ingress
# ingress:
# enabled: false
# host:
# # enable https
# tls:
# enabled: false
# environment: {}
# resources:
# limits:
# memory:
# _default: "6Gi"
# cpu:
# _default: "3"
# requests:
# memory:
# _default: "6Gi"
# cpu:
# _default: "3"
# enable Smart-contract-verifier deploy
scVerifier:
enabled: true
image:
_default: ghcr.io/blockscout/smart-contract-verifier:main
replicas:
app: 1
# docker:
# port: 80
# targetPort: 8043
# metricsPort: 6060
service:
# ClusterIP, NodePort or LoadBalancer
type: ClusterIP
# enable ingress
ingress:
enabled: true
......@@ -397,23 +283,6 @@ scVerifier:
_default: "0.05Gi"
cpu:
_default: "0.01"
# node label
nodeSelector:
enabled: true
app: blockscout
# probes
livenessProbe:
enabled: true
# path: /health
readinessProbe:
enabled: true
# path: /health
# enable Horizontal Pod Autoscaler
hpa:
enabled: true
minReplicas: 1
maxReplicas: 10
cpuTarget: 90
environment:
SMART_CONTRACT_VERIFIER__SERVER__HTTP__ADDR:
_default: 0.0.0.0:8050
......@@ -505,12 +374,6 @@ frontend:
_default: "0.1Gi"
cpu:
_default: "0.1"
# node label
nodeSelector:
enabled: true
labels:
_default:
app: blockscout
environment:
# ui config
NEXT_PUBLIC_FEATURED_NETWORKS:
......
......@@ -5,15 +5,8 @@ frontend:
enabled: true
image:
_default: ghcr.io/blockscout/frontend:main
replicas:
app: 1
docker:
port: 80
targetPort: 3000
ingress:
enabled: true
# annotations:
# - 'nginx.ingress.kubernetes.io/use-regex: "true"'
host:
_default: frontend.test.blockscout.aws-k8s.blockscout.com
# enable https
......@@ -57,10 +50,6 @@ frontend:
_default: "0.1Gi"
cpu:
_default: "0.1"
# node label
nodeSelector:
enabled: true
app: blockscout
environment:
NEXT_PUBLIC_BLOCKSCOUT_VERSION:
_default: v5.1.0-beta
......
global:
env: review
# enable Blockscout deploy
blockscout:
app: blockscout
enabled: false
image:
_default: blockscout/blockscout:latest
replicas:
app: 1
docker:
port: 80
targetPort: 4000
# init container
init:
enabled: true
image:
_default: blockscout/blockscout:latest
service:
# ClusterIP, NodePort or LoadBalancer
type: ClusterIP
# enable ingress
ingress:
enabled: true
annotations: {}
# - 'nginx.ingress.kubernetes.io/rewrite-target: /$2'
host:
_default: blockscout.test.blockscout.aws-k8s.blockscout.com
# enable https
#
tls:
enabled: true
path:
prefix:
# - "/poa/sokol(/|$)(.*)"
- "/"
# probes
livenessProbe:
enabled: true
path: /
readinessProbe:
enabled: true
path: /
resources:
limits:
memory:
_default: "1Gi"
cpu:
_default: "2"
requests:
memory:
_default: "1Gi"
cpu:
_default: "2"
# enable service to connect to RDS
rds:
enabled: false
endpoint:
_default: <endpoint>.<region>.rds.amazonaws.com
# node label
nodeSelector:
enabled: true
app: blockscout
# Blockscout environment variables
environment:
ENV:
_default: test
RESOURCE_MODE:
_default: account
PUBLIC:
_default: 'false'
PORT:
_default: 4000
PORT_PG:
_default: 5432
PORT_NETWORK_HTTP:
_default: 8545
PORT_NETWORK_WS:
_default: 8546
ETHEREUM_JSONRPC_VARIANT:
_default: geth
ETHEREUM_JSONRPC_TRACE_URL:
_default: http://geth-svc:8545
ETHEREUM_JSONRPC_HTTP_URL:
_default: http://geth-svc:8545
ETHEREUM_JSONRPC_WS_URL:
_default: ws://geth-svc:8546
COIN:
_default: DAI
MIX_ENV:
_default: prod
ECTO_USE_SSL:
_default: 'false'
RUST_VERIFICATION_SERVICE_URL:
_default: http://sc-verifier-svc:8043
ACCOUNT_ENABLED:
_default: 'true'
DISABLE_REALTIME_INDEXER:
_default: 'false'
SOCKET_ROOT:
_default: "/"
NETWORK_PATH:
_default: "/"
API_PATH:
_default: "/"
ETHEREUM_JSONRPC_DISABLE_ARCHIVE_BALANCES:
_default: 'true'
API_BASE_PATH:
_default: "/"
APPS_MENU:
_default: 'true'
EXTERNAL_APPS:
_default: '[{"title": "Marketplace", "url": "/apps"}]'
JSON_RPC:
_default: https://sokol.poa.network
API_V2_ENABLED:
_default: 'true'
postgres:
enabled: false
image: postgres:13.8
port: 5432
command: '["docker-entrypoint.sh", "-c"]'
args: '["max_connections=300"]'
# persistence: true
resources:
limits:
memory:
_default: "4Gi"
cpu:
_default: "3"
requests:
memory:
_default: "4Gi"
cpu:
_default: "3"
environment:
POSTGRES_USER:
_default: 'postgres'
POSTGRES_HOST_AUTH_METHOD:
_default: 'trust'
# enable geth deploy
geth:
enabled: false
image:
_default: ethereum/client-go:stable
replicas:
app: 1
ports:
http:
number: 8545
protocol: TCP
ws:
number: 8546
protocol: TCP
auth:
number: 8551
protocol: TCP
command:
- /bin/sh
- -c
- |-
/root/init.sh --fakepow --dev --dev.period=1 --datadir=/root/.ethereum/devnet --keystore=/root/.ethereum/devnet/keystore --password=/root/password.txt --unlock=0 --unlock=1 --mine --miner.threads=1 --miner.etherbase=0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 --ipcpath=/root/geth.ipc --http --http.vhosts=* --http.addr=0.0.0.0 --http.port=8545 --http.api=eth,net,web3,debug,txpool --ws --ws.origins=* --ws.addr=0.0.0.0 --ws.port=8546 --ws.api=eth,net,web3,debug,txpool --graphql --graphql.corsdomain=* --allow-insecure-unlock --rpc.allow-unprotected-txs --http.corsdomain=* --vmdebug --networkid=1337 --rpc.txfeecap=0
environment: {}
persistence:
enabled: false
resources:
limits:
memory:
_default: "2Gi"
cpu:
_default: "0.2"
requests:
memory:
_default: "2Gi"
cpu:
_default: "0.2"
# node label
nodeSelector:
enabled: true
app: blockscout
service:
# ClusterIP, NodePort or LoadBalancer
type: ClusterIP
# enable ingress
ingress:
enabled: true
host:
_default: node.test.blockscout.aws-k8s.blockscout.com
# enable https
tls:
enabled: false
jwt:
enabled: false
files:
enabled: true
# enable Smart-contract-verifier deploy
scVerifier:
enabled: false
image:
_default: ghcr.io/blockscout/smart-contract-verifier:main
replicas:
app: 1
# docker:
# port: 80
# targetPort: 8043
# metricsPort: 6060
service:
# ClusterIP, NodePort or LoadBalancer
type: ClusterIP
# enable ingress
ingress:
enabled: true
host:
_default: verifier.test.blockscout.aws-k8s.blockscout.com
# enable https
tls:
enabled: true
resources:
limits:
memory:
_default: "0.5Gi"
cpu:
_default: "0.25"
requests:
memory:
_default: "0.5Gi"
cpu:
_default: "0.25"
# node label
nodeSelector:
enabled: true
app: blockscout
# probes
livenessProbe:
enabled: true
# path: /health
readinessProbe:
enabled: true
# path: /health
# enable Horizontal Pod Autoscaler
hpa:
enabled: true
minReplicas: 1
maxReplicas: 10
cpuTarget: 90
environment:
SMART_CONTRACT_VERIFIER__SERVER__HTTP__ADDR:
_default: 0.0.0.0:8050
SMART_CONTRACT_VERIFIER__SERVER__GRPC__ADDR:
_default: 0.0.0.0:8051
# SMART_CONTRACT_VERIFIER__SOLIDITY__ENABLED:
# _default: 'true'
SMART_CONTRACT_VERIFIER__SOLIDITY__COMPILERS_DIR:
_default: /tmp/solidity-compilers
SMART_CONTRACT_VERIFIER__SOLIDITY__REFRESH_VERSIONS_SCHEDULE:
_default: 0 0 * * * * *
# It depends on the OS you are running the service on
# SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__LIST__LIST_URL:
# _default: https://solc-bin.ethereum.org/linux-amd64/list.json
#SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__LIST__LIST_URL=https://solc-bin.ethereum.org/macosx-amd64/list.json
#SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__LIST__LIST_URL=https://solc-bin.ethereum.org/windows-amd64/list.json
SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__S3__REGION:
_default: ""
SMART_CONTRACT_VERIFIER__SOLIDITY__FETCHER__S3__ENDPOINT:
_default: https://storage.googleapis.com
SMART_CONTRACT_VERIFIER__SOURCIFY__ENABLED:
_default: 'true'
SMART_CONTRACT_VERIFIER__SOURCIFY__API_URL:
_default: https://sourcify.dev/server/
SMART_CONTRACT_VERIFIER__SOURCIFY__VERIFICATION_ATTEMPTS:
_default: 3
SMART_CONTRACT_VERIFIER__SOURCIFY__REQUEST_TIMEOUT:
_default: 10
SMART_CONTRACT_VERIFIER__METRICS__ENABLED:
_default: 'true'
SMART_CONTRACT_VERIFIER__METRICS__ADDR:
_default: 0.0.0.0:6060
SMART_CONTRACT_VERIFIER__METRICS__ROUTE:
_default: /metrics
SMART_CONTRACT_VERIFIER__JAEGER__ENABLED:
_default: 'false'
frontend:
app: blockscout
enabled: true
image:
_default: ghcr.io/blockscout/frontend:main
replicas:
app: 1
docker:
port: 80
targetPort: 3000
ingress:
enabled: true
# annotations:
# - 'nginx.ingress.kubernetes.io/use-regex: "true"'
host:
_default: frontend.test.blockscout.aws-k8s.blockscout.com
# enable https
......@@ -341,10 +50,6 @@ frontend:
_default: "0.1Gi"
cpu:
_default: "0.1"
# node label
nodeSelector:
enabled: true
app: blockscout
environment:
NEXT_PUBLIC_BLOCKSCOUT_VERSION:
_default: v4.1.8-beta
......
import type { MetaMaskInpageProvider } from '@metamask/providers';
type CPreferences = {
zone: string;
width: string;
height: string;
}
declare global {
interface Window {
ethereum: MetaMaskInpageProvider;
coinzilla_display: Array<CPreferences>;
}
}
......@@ -6,7 +6,7 @@ import type { Params as ApiFetchParams } from './useApiFetch';
import useApiFetch from './useApiFetch';
export interface Params<R extends ResourceName, E = unknown> extends ApiFetchParams<R> {
queryOptions?: Omit<UseQueryOptions<unknown, ResourceError<E>, ResourcePayload<R>>, 'queryKey' | 'queryFn'>;
queryOptions?: Omit<UseQueryOptions<ResourcePayload<R>, ResourceError<E>, ResourcePayload<R>>, 'queryKey' | 'queryFn'>;
}
export function getResourceKey<R extends ResourceName>(resource: R, { pathParams, queryParams }: Params<R> = {}) {
......@@ -23,9 +23,12 @@ export default function useApiQuery<R extends ResourceName, E = unknown>(
) {
const apiFetch = useApiFetch();
return useQuery<unknown, ResourceError<E>, ResourcePayload<R>>(
return useQuery<ResourcePayload<R>, ResourceError<E>, ResourcePayload<R>>(
getResourceKey(resource, { pathParams, queryParams }),
async() => {
return apiFetch<R, ResourcePayload<R>, ResourceError>(resource, { pathParams, queryParams, fetchParams });
// all errors and error typing is handled by react-query
// so error response will never go to the data
// that's why we are safe here to do type conversion "as Promise<ResourcePayload<R>>"
return apiFetch(resource, { pathParams, queryParams, fetchParams }) as Promise<ResourcePayload<R>>;
}, queryOptions);
}
......@@ -12,7 +12,11 @@ const KEY_WORDS = {
UNSAFE_EVAL: '\'unsafe-eval\'',
};
const MAIN_DOMAINS = [ `*.${ appConfig.host }`, appConfig.host ];
const MAIN_DOMAINS = [
`*.${ appConfig.host }`,
appConfig.host,
appConfig.visualizeApi.endpoint,
].filter(Boolean);
// eslint-disable-next-line no-restricted-properties
const REPORT_URI = process.env.SENTRY_CSP_REPORT_URI;
......@@ -49,7 +53,9 @@ function makePolicyMap() {
return {
'default-src': [
KEY_WORDS.NONE,
// KEY_WORDS.NONE,
// temporarily, see if warnings for "/_next/static/chunks/8861-ad3efb7f624b7bc1.js" go away
...MAIN_DOMAINS,
],
'connect-src': [
......@@ -78,8 +84,16 @@ function makePolicyMap() {
'wss://*.bridge.walletconnect.org',
'wss://www.walletlink.org',
// RPC providers
'https://infragrid.v.network',
// github (spec for api-docs page)
'raw.githubusercontent.com',
// google analytics
'https://www.googletagmanager.com',
'https://www.google-analytics.com',
'https://stats.g.doubleclick.net',
],
'script-src': [
......@@ -103,7 +117,13 @@ function makePolicyMap() {
// reCAPTCHA from google
'https://www.google.com/recaptcha/api.js',
'https://www.gstatic.com',
'https://translate.google.com',
'\'sha256-FDyPg8CqqIpPAfGVKx1YeKduyLs0ghNYWII21wL+7HM=\'',
// google analytics
'\'sha256-NTmEg2dBnojQfTYrYJEmp3nG7V66756qPbQMCIBrctk=\'',
'https://www.googletagmanager.com',
'https://www.google-analytics.com',
],
'style-src': [
......@@ -113,6 +133,9 @@ function makePolicyMap() {
// google fonts
'fonts.googleapis.com',
// reCAPTCHA from google
'https://www.gstatic.com',
// yes, it is unsafe as it stands, but
// - we cannot use hashes because all styles are generated dynamically
// - we cannot use nonces since we are not following along SSR path
......@@ -151,6 +174,13 @@ function makePolicyMap() {
// token's media
'ipfs.io',
// reCAPTCHA from google
'https://translate.google.com',
'https://www.gstatic.com',
// google analytics
'https://www.google-analytics.com',
],
'font-src': [
......
export default function getErrorStatusCode(error: Error | undefined): number | undefined {
return (
error && 'cause' in error &&
typeof error.cause === 'object' && error.cause !== null &&
'status' in error.cause && typeof error.cause.status === 'number' &&
error.cause.status
) ||
undefined;
}
export default function formatNumberToMetricPrefix(number: number) {
return Intl.NumberFormat('en-US', {
notation: 'compact',
maximumFractionDigits: 3,
}).format(number);
}
export function shortenNumberWithLetter(
x: number,
params?: {
unitSeparator: string;
},
_options?: Intl.NumberFormatOptions,
) {
const options = _options || { maximumFractionDigits: 2 };
const unitSeparator = params?.unitSeparator || '';
if (x > 1_000_000_000) {
return (x / 1_000_000_000).toLocaleString('en', options) + unitSeparator + 'B';
}
if (x > 1_000_000) {
return (x / 1_000_000).toLocaleString('en', options) + unitSeparator + 'M';
}
if (x > 1_000) {
return (x / 1_000).toLocaleString('en', options) + unitSeparator + 'K';
}
return x.toLocaleString('en', options);
}
export default function getFilterValue<FilterType>(filterValues: ReadonlyArray<FilterType>, val: string | Array<string> | undefined) {
if (typeof val === 'string' && filterValues.includes(val as unknown as FilterType)) {
return val as unknown as FilterType;
export default function getFilterValue<FilterType>(filterValues: ReadonlyArray<FilterType>, val: string | Array<string> | undefined): FilterType | undefined {
if (typeof val === 'string' && filterValues.includes(val as FilterType)) {
return val as FilterType;
}
}
import BigNumber from 'bignumber.js';
import type { Unit } from 'types/unit';
import { WEI, GWEI } from 'lib/consts';
export default function getValueWithUnit(value: string | number, unit: 'wei' | 'gwei' | 'ether' = 'wei') {
export default function getValueWithUnit(value: string | number, unit: Unit = 'wei') {
let unitBn: BigNumber.Value;
switch (unit) {
case 'wei':
......
......@@ -2,7 +2,6 @@ import React from 'react';
import type { Address } from 'types/api/address';
import notEmpty from 'lib/notEmpty';
import ContractCode from 'ui/address/contract/ContractCode';
import ContractRead from 'ui/address/contract/ContractRead';
import ContractWrite from 'ui/address/contract/ContractWrite';
......@@ -33,6 +32,6 @@ export default function useContractTabs(data: Address | undefined) {
data?.has_custom_methods_write ?
{ id: 'write_custom_methods', title: 'Write custom', component: <ContractWrite addressHash={ data?.hash } isCustomAbi/> } :
undefined,
].filter(notEmpty);
].filter(Boolean);
}, [ data ]);
}
......@@ -20,10 +20,9 @@ import topAccountsIcon from 'icons/top-accounts.svg';
import transactionsIcon from 'icons/transactions.svg';
// import depositsIcon from 'icons/arrows/south-east.svg';
// import txnBatchIcon from 'icons/txn_batches.svg';
// import verifiedIcon from 'icons/verified.svg';
import verifiedIcon from 'icons/verified.svg';
import watchlistIcon from 'icons/watchlist.svg';
// import { rightLineArrow } from 'lib/html-entities';
import notEmpty from 'lib/notEmpty';
export interface NavItem {
text: string;
......@@ -61,9 +60,9 @@ export default function useNavItems(): ReturnType {
{ text: 'Top accounts', nextRoute: { pathname: '/accounts' as const }, icon: topAccountsIcon, isActive: pathname === '/accounts', isNewUi: true };
const blocks = { text: 'Blocks', nextRoute: { pathname: '/blocks' as const }, icon: blocksIcon, isActive: pathname.startsWith('/block'), isNewUi: true };
const txs = { text: 'Transactions', nextRoute: { pathname: '/txs' as const }, icon: transactionsIcon, isActive: pathname.startsWith('/tx'), isNewUi: true };
// const verifiedContracts =
const verifiedContracts =
// eslint-disable-next-line max-len
// { text: 'Verified contracts', nextRoute: { pathname: '/verified_contracts' as const }, icon: verifiedIcon, isActive: pathname === '/verified_contracts', isNewUi: false },
{ text: 'Verified contracts', nextRoute: { pathname: '/verified-contracts' as const }, icon: verifiedIcon, isActive: false, isNewUi: false };
if (appConfig.L2.isL2Network) {
blockchainNavItems = [
......@@ -83,17 +82,15 @@ export default function useNavItems(): ReturnType {
],
[
topAccounts,
verifiedContracts,
],
// [
// verifiedContracts
// ],
];
} else {
blockchainNavItems = [
txs,
blocks,
topAccounts,
// verifiedContracts,
verifiedContracts,
];
}
......@@ -106,7 +103,9 @@ export default function useNavItems(): ReturnType {
isActive: pathname === '/api-docs',
isNewUi: true,
} : null,
].filter(notEmpty);
// FIXME: need icon for this item
{ text: 'GraphQL', nextRoute: { pathname: '/graphiql' as const }, icon: topAccountsIcon, isActive: false, isNewUi: false },
].filter(Boolean);
const mainNavItems = [
{
......@@ -125,7 +124,7 @@ export default function useNavItems(): ReturnType {
// at this stage custom menu items is under development, we will implement it later
otherNavItems.length > 0 ?
{ text: 'Other', icon: gearIcon, isActive: otherNavItems.some(item => item.isActive), subItems: otherNavItems } : null,
].filter(notEmpty) as Array<NavItem | NavGroupItem>;
].filter(Boolean);
const accountNavItems = [
{
......
// https://unicode-table.com
export const asymp = String.fromCharCode(8776); // приблизительно
export const hellip = String.fromCharCode(8230); // многоточие
export const nbsp = String.fromCharCode(160); // неразрывный пробел
export const thinsp = String.fromCharCode(8201); // короткий пробел
export const space = String.fromCharCode(32); // обычный пробел
export const nbdash = String.fromCharCode(8209); // неразрывное тире
export const mdash = String.fromCharCode(8212); // длинное тире
export const ndash = String.fromCharCode(8211); // среднее тире
export const laquo = String.fromCharCode(171); // кавычки-ёлочки (левые)
export const raquo = String.fromCharCode(187); // кавычки-ёлочки (правые)
export const middot = String.fromCharCode(183); // точка по центру строки (в вертикальном смысле)
export const blackCircle = String.fromCharCode(9679); // жирная точка по центру строки (в вертикальном смысле)
export const asymp = String.fromCharCode(8776); // ~
export const hellip = String.fromCharCode(8230); //
export const nbsp = String.fromCharCode(160); // no-break Space
export const thinsp = String.fromCharCode(8201); // thin Space
export const space = String.fromCharCode(32); // space
export const nbdash = String.fromCharCode(8209); // non-breaking hyphen
export const mdash = String.fromCharCode(8212); // em dash
export const ndash = String.fromCharCode(8211); // en dash
export const laquo = String.fromCharCode(171); // «
export const raquo = String.fromCharCode(187); // »
export const middot = String.fromCharCode(183); // ·
export const blackCircle = String.fromCharCode(9679); //
export const blackRightwardsArrowhead = String.fromCharCode(10148); // ➤
export const degree = String.fromCharCode(176); // градус °
export const times = String.fromCharCode(215); // мультипликатор ×
export const disk = String.fromCharCode(8226); // диск
export const minus = String.fromCharCode(8722); // минус
export const leftLineArrow = String.fromCharCode(8592); // стрелка
export const rightLineArrow = String.fromCharCode(8594); // стрелка
export const apos = String.fromCharCode(39); // апостроф '
export const degree = String.fromCharCode(176); // °
export const times = String.fromCharCode(215); // ×
export const disk = String.fromCharCode(8226); // •
export const minus = String.fromCharCode(8722); // −
export const leftLineArrow = String.fromCharCode(8592); // ←
export const rightLineArrow = String.fromCharCode(8594); // →
export const apos = String.fromCharCode(39); // apostrophe '
export default function notEmpty<TValue>(value: TValue | null | undefined): value is TValue {
return value !== null && value !== undefined;
}
import type { Channel } from 'phoenix';
import { useEffect, useRef, useState } from 'react';
import notEmpty from 'lib/notEmpty';
import { useSocket } from './context';
const CHANNEL_REGISTRY: Record<string, Channel> = {};
......@@ -27,7 +25,7 @@ export default function useSocketChannel({ topic, params, isDisabled, onJoin, on
useEffect(() => {
const cleanUpRefs = () => {
const refs = [ onCloseRef.current, onErrorRef.current ].filter(notEmpty);
const refs = [ onCloseRef.current, onErrorRef.current ].filter(Boolean);
refs.length > 0 && socket?.off(refs);
};
......
......@@ -49,7 +49,7 @@ export const base: Block = {
uncles_hashes: [],
};
export const genesis = {
export const genesis: Block = {
base_fee_per_gas: null,
burnt_fees: null,
burnt_fees_percentage: null,
......
......@@ -9,11 +9,13 @@ import type { ResourceError } from 'lib/api/resources';
import { AppContextProvider } from 'lib/appContext';
import { Chakra } from 'lib/Chakra';
import { ScrollDirectionProvider } from 'lib/contexts/scrollDirection';
import getErrorStatusCode from 'lib/errors/getErrorStatusCode';
import useConfigSentry from 'lib/hooks/useConfigSentry';
import { SocketProvider } from 'lib/socket/context';
import theme from 'theme';
import AppError from 'ui/shared/AppError/AppError';
import ErrorBoundary from 'ui/shared/ErrorBoundary';
import GoogleAnalytics from 'ui/shared/GoogleAnalytics';
function MyApp({ Component, pageProps }: AppProps) {
useConfigSentry();
......@@ -36,12 +38,11 @@ function MyApp({ Component, pageProps }: AppProps) {
}));
const renderErrorScreen = React.useCallback((error?: Error) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const statusCode = (error?.cause as any)?.status || 500;
const statusCode = getErrorStatusCode(error);
return (
<AppError
statusCode={ statusCode }
statusCode={ statusCode || 500 }
height="100vh"
display="flex"
flexDirection="column"
......@@ -68,6 +69,7 @@ function MyApp({ Component, pageProps }: AppProps) {
</SocketProvider>
</ScrollDirectionProvider>
<ReactQueryDevtools/>
<GoogleAnalytics/>
</QueryClientProvider>
</AppContextProvider>
</ErrorBoundary>
......
import type { NextPage } from 'next';
const GraphQLPage: NextPage = () => {
return null;
};
export default GraphQLPage;
export async function getServerSideProps() {
return {
notFound: true,
};
}
import type { NextPage } from 'next';
const VerifiedContractsPage: NextPage = () => {
return null;
};
export default VerifiedContractsPage;
export async function getServerSideProps() {
return {
notFound: true,
};
}
......@@ -35,7 +35,7 @@ export const joinChannel = async(socket: WebSocket, channelName: string) => {
return new Promise<[string, string, string]>((resolve, reject) => {
socket.on('message', (msg) => {
try {
const payload: Array<string> = JSON.parse(msg.toString());
const payload = JSON.parse(msg.toString()) as Array<string>;
if (channelName === payload[2] && payload[3] === 'phx_join') {
socket.send(JSON.stringify([
......
import '@total-typescript/ts-reset';
import type { AddressParam } from './addressParams';
export type TransactionReward = {
types: Array<string>;
emission_reward: string;
block_hash: string;
from: AddressParam;
to: AddressParam;
}
......@@ -25,6 +25,7 @@ declare module "nextjs-routes" {
| StaticRoute<"/blocks">
| StaticRoute<"/csv-export">
| StaticRoute<"/graph">
| StaticRoute<"/graphiql">
| StaticRoute<"/">
| StaticRoute<"/login">
| StaticRoute<"/search-results">
......@@ -34,6 +35,7 @@ declare module "nextjs-routes" {
| StaticRoute<"/tokens">
| DynamicRoute<"/tx/[hash]", { "hash": string }>
| StaticRoute<"/txs">
| StaticRoute<"/verified-contracts">
| StaticRoute<"/visualize/sol2uml">;
interface StaticRoute<Pathname> {
......
......@@ -137,7 +137,7 @@ const ContractMethodCallable = <T extends SmartContractMethod>({ data, onSubmit,
{ isWrite ? 'Write' : 'Query' }
</Button>
</chakra.form>
{ 'outputs' in data && data.outputs.length > 0 && (
{ 'outputs' in data && !isWrite && data.outputs.length > 0 && (
<Flex mt={ 3 }>
<Icon as={ arrowIcon } boxSize={ 5 } mr={ 1 }/>
<Text>{ data.outputs.map(({ type }) => type).join(', ') }</Text>
......
......@@ -7,7 +7,9 @@ import type { SmartContractMethodOutput } from 'types/api/contract';
import appConfig from 'configs/app/config';
import { WEI } from 'lib/consts';
import Address from 'ui/shared/address/Address';
import AddressLink from 'ui/shared/address/AddressLink';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
interface Props {
data: SmartContractMethodOutput;
......@@ -34,7 +36,12 @@ const ContractMethodStatic = ({ data }: Props) => {
const content = (() => {
if (data.type === 'address' && data.value) {
return <AddressLink type="address" hash={ data.value }/>;
return (
<Address>
<AddressLink type="address" hash={ data.value }/>
<CopyToClipboard text={ data.value }/>
</Address>
);
}
return <chakra.span wordBreak="break-all">({ data.type }): { value }</chakra.span>;
......
......@@ -7,7 +7,6 @@ import type { AppItemPreview } from 'types/client/apps';
import northEastIcon from 'icons/arrows/north-east.svg';
import starFilledIcon from 'icons/star_filled.svg';
import starOutlineIcon from 'icons/star_outline.svg';
import notEmpty from 'lib/notEmpty';
import AppCardLink from './AppCardLink';
import { APP_CATEGORIES } from './constants';
......@@ -30,7 +29,7 @@ const AppCard = ({
isFavorite,
onFavoriteClick,
}: Props) => {
const categoriesLabel = categories.map(c => APP_CATEGORIES[c]).filter(notEmpty).join(', ');
const categoriesLabel = categories.map(c => APP_CATEGORIES[c]).filter(Boolean).join(', ');
const handleInfoClick = useCallback((event: MouseEvent) => {
event.preventDefault();
......
......@@ -15,7 +15,6 @@ import starFilledIcon from 'icons/star_filled.svg';
import starOutlineIcon from 'icons/star_outline.svg';
import useIsMobile from 'lib/hooks/useIsMobile';
import { nbsp } from 'lib/html-entities';
import notEmpty from 'lib/notEmpty';
import AppModalLink from './AppModalLink';
import { APP_CATEGORIES } from './constants';
......@@ -60,7 +59,7 @@ const AppModal = ({
icon: ghIcon,
url: github,
} : null,
].filter(notEmpty);
].filter(Boolean);
const handleFavoriteClick = useCallback(() => {
onFavoriteClick(id, isFavorite);
......
......@@ -9,7 +9,7 @@ const favoriteAppsLocalStorageKey = 'favoriteApps';
function getFavoriteApps() {
try {
return JSON.parse(localStorage.getItem(favoriteAppsLocalStorageKey) || '[]');
return JSON.parse(localStorage.getItem(favoriteAppsLocalStorageKey) || '[]') as Array<string>;
} catch (e) {
return [];
}
......
import { test, expect } from '@playwright/experimental-ct-react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
import type { Block } from 'types/api/block';
import type { ResourceError } from 'lib/api/resources';
import * as blockMock from 'mocks/blocks/block';
import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl';
import BlockDetails from './BlockDetails';
const API_URL = buildApiUrl('block', { height: '1' });
const hooksConfig = {
router: {
query: { height: '1' },
......@@ -15,14 +17,14 @@ const hooksConfig = {
};
test('regular block +@mobile +@dark-mode', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(blockMock.base),
}));
const query = {
data: blockMock.base,
isLoading: false,
} as UseQueryResult<Block, ResourceError>;
const component = await mount(
<TestApp>
<BlockDetails/>
<BlockDetails query={ query }/>
</TestApp>,
{ hooksConfig },
);
......@@ -33,14 +35,14 @@ test('regular block +@mobile +@dark-mode', async({ mount, page }) => {
});
test('genesis block', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify(blockMock.genesis),
}));
const query = {
data: blockMock.genesis,
isLoading: false,
} as UseQueryResult<Block, ResourceError>;
const component = await mount(
<TestApp>
<BlockDetails/>
<BlockDetails query={ query }/>
</TestApp>,
{ hooksConfig },
);
......
import { Grid, GridItem, Text, Icon, Link, Box, Tooltip } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import BigNumber from 'bignumber.js';
import capitalize from 'lodash/capitalize';
import { useRouter } from 'next/router';
......@@ -6,10 +7,12 @@ import { route } from 'nextjs-routes';
import React from 'react';
import { scroller, Element } from 'react-scroll';
import type { Block } from 'types/api/block';
import appConfig from 'configs/app/config';
import clockIcon from 'icons/clock.svg';
import flameIcon from 'icons/flame.svg';
import useApiQuery from 'lib/api/useApiQuery';
import type { ResourceError } from 'lib/api/resources';
import getBlockReward from 'lib/block/getBlockReward';
import { WEI, WEI_IN_GWEI, ZERO } from 'lib/consts';
import dayjs from 'lib/date/dayjs';
......@@ -28,15 +31,16 @@ import PrevNext from 'ui/shared/PrevNext';
import TextSeparator from 'ui/shared/TextSeparator';
import Utilization from 'ui/shared/Utilization/Utilization';
const BlockDetails = () => {
interface Props {
query: UseQueryResult<Block, ResourceError>;
}
const BlockDetails = ({ query }: Props) => {
const [ isExpanded, setIsExpanded ] = React.useState(false);
const router = useRouter();
const height = getQueryParamString(router.query.height);
const heightOrHash = getQueryParamString(router.query.height);
const { data, isLoading, isError, error } = useApiQuery('block', {
pathParams: { height },
queryOptions: { enabled: Boolean(height) },
});
const { data, isLoading, isError, error } = query;
const handleCutClick = React.useCallback(() => {
setIsExpanded((flag) => !flag);
......@@ -47,11 +51,15 @@ const BlockDetails = () => {
}, []);
const handlePrevNextClick = React.useCallback((direction: 'prev' | 'next') => {
if (!data) {
return;
}
const increment = direction === 'next' ? +1 : -1;
const nextId = String(Number(height) + increment);
const nextId = String(data.height + increment);
router.push({ pathname: '/block/[height]', query: { height: nextId } }, undefined);
}, [ height, router ]);
}, [ data, router ]);
if (isLoading) {
return <BlockDetailsSkeleton/>;
......@@ -85,7 +93,7 @@ const BlockDetails = () => {
return (
<Grid columnGap={ 8 } rowGap={{ base: 3, lg: 3 }} templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }} overflow="hidden">
<DetailsInfoItem
title="Block height"
title={ `${ data.type === 'reorg' ? 'Reorg' : 'Block' } height` }
hint="The block height of a particular block is defined as the number of blocks preceding it in the blockchain"
>
{ data.height }
......@@ -117,7 +125,7 @@ const BlockDetails = () => {
title="Transactions"
hint="The number of transactions in the block"
>
<LinkInternal href={ route({ pathname: '/block/[height]', query: { height, tab: 'txs' } }) }>
<LinkInternal href={ route({ pathname: '/block/[height]', query: { height: heightOrHash, tab: 'txs' } }) }>
{ data.tx_count } transaction{ data.tx_count === 1 ? '' : 's' }
</LinkInternal>
</DetailsInfoItem>
......
......@@ -36,7 +36,7 @@ const BlocksListItem = ({ data, isPending, enableTimeIncrement }: Props) => {
{ isPending && <Spinner size="sm"/> }
<LinkInternal
fontWeight={ 600 }
href={ route({ pathname: '/block/[height]', query: { height: String(data.height) } }) }
href={ route({ pathname: '/block/[height]', query: { height: data.type === 'reorg' ? String(data.hash) : String(data.height) } }) }
>
{ data.height }
</LinkInternal>
......
......@@ -41,7 +41,7 @@ const BlocksTableItem = ({ data, isPending, enableTimeIncrement }: Props) => {
<Tooltip isDisabled={ data.type !== 'reorg' } label="Chain reorganizations">
<LinkInternal
fontWeight={ 600 }
href={ route({ pathname: '/block/[height]', query: { height: String(data.height) } }) }
href={ route({ pathname: '/block/[height]', query: { height: data.type === 'reorg' ? String(data.hash) : String(data.height) } }) }
>
{ data.height }
</LinkInternal>
......
......@@ -84,10 +84,9 @@ const ContractVerificationForm = ({ method: methodFromQuery, config, hash }: Pro
status: 'success',
variant: 'subtle',
isClosable: true,
onCloseComplete: () => {
router.push({ pathname: '/address/[hash]', query: { hash, tab: 'contract' } }, undefined, { shallow: true });
},
});
router.push({ pathname: '/address/[hash]', query: { hash, tab: 'contract' } }, undefined, { shallow: false });
}, [ hash, router, setError, toast ]);
const handleSocketError = React.useCallback(() => {
......
......@@ -12,7 +12,7 @@ import ContractVerificationFieldOptimization from '../fields/ContractVerificatio
const ContractVerificationFlattenSourceCode = () => {
return (
<ContractVerificationMethod title="Contract verification via Solidity (fattened source code)">
<ContractVerificationMethod title="Contract verification via Solidity (flattened source code)">
<ContractVerificationFieldName/>
<ContractVerificationFieldIsYul/>
<ContractVerificationFieldCompiler/>
......
......@@ -101,7 +101,7 @@ export const DEFAULT_VALUES = {
};
export function isValidVerificationMethod(method?: string): method is SmartContractVerificationMethod {
return method && SUPPORTED_VERIFICATION_METHODS.includes(method as SmartContractVerificationMethod) ? true : false;
return method && SUPPORTED_VERIFICATION_METHODS.includes(method) ? true : false;
}
export function sortVerificationMethods(methodA: SmartContractVerificationMethod, methodB: SmartContractVerificationMethod) {
......
import type { TooltipProps } from '@chakra-ui/react';
import type { SystemStyleObject, TooltipProps } from '@chakra-ui/react';
import { Flex, Icon, Text, useColorModeValue, chakra, LightMode } from '@chakra-ui/react';
import React from 'react';
......@@ -25,13 +25,13 @@ const TOOLTIP_PROPS: Partial<TooltipProps> = {
};
const StatsItem = ({ icon, title, value, className, tooltipLabel, url }: Props) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const sxContainer = {} as any;
sxContainer[`@media screen and (min-width: ${ breakpoints.lg }) and (max-width: ${ LARGEST_BREAKPOINT })`] = { flexDirection: 'column' };
const sxContainer: SystemStyleObject = {
[`@media screen and (min-width: ${ breakpoints.lg }) and (max-width: ${ LARGEST_BREAKPOINT })`]: { flexDirection: 'column' },
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const sxText = {} as any;
sxText[`@media screen and (min-width: ${ breakpoints.lg }) and (max-width: ${ LARGEST_BREAKPOINT })`] = { alignItems: 'center' };
const sxText: SystemStyleObject = {
[`@media screen and (min-width: ${ breakpoints.lg }) and (max-width: ${ LARGEST_BREAKPOINT })`]: { alignItems: 'center' },
};
const infoColor = useColorModeValue('gray.600', 'gray.400');
......
......@@ -6,14 +6,13 @@ import type { TChainIndicator } from '../types';
import appConfig from 'configs/app/config';
import globeIcon from 'icons/globe.svg';
import txIcon from 'icons/transactions.svg';
import { shortenNumberWithLetter } from 'lib/formatters';
import { sortByDateDesc } from 'ui/shared/chart/utils/sorts';
import TokenLogo from 'ui/shared/TokenLogo';
const dailyTxsIndicator: TChainIndicator<'homepage_chart_txs'> = {
id: 'daily_txs',
title: 'Daily transactions',
value: (stats) => shortenNumberWithLetter(Number(stats.transactions_today), undefined, { maximumFractionDigits: 2 }),
value: (stats) => Number(stats.transactions_today).toLocaleString('en', { maximumFractionDigits: 2, notation: 'compact' }),
icon: <Icon as={ txIcon } boxSize={ 6 } bgColor="#56ACD1" borderRadius="base" color="white"/>,
hint: `The total daily number of transactions on the blockchain for the last month.`,
api: {
......@@ -23,7 +22,7 @@ const dailyTxsIndicator: TChainIndicator<'homepage_chart_txs'> = {
.map((item) => ({ date: new Date(item.date), value: item.tx_count }))
.sort(sortByDateDesc),
name: 'Tx/day',
valueFormatter: (x: number) => shortenNumberWithLetter(x, undefined, { maximumFractionDigits: 2 }),
valueFormatter: (x: number) => x.toLocaleString('en', { maximumFractionDigits: 2, notation: 'compact' }),
} ]),
},
};
......@@ -49,7 +48,7 @@ const coinPriceIndicator: TChainIndicator<'homepage_chart_market'> = {
const marketPriceIndicator: TChainIndicator<'homepage_chart_market'> = {
id: 'market_cup',
title: 'Market cap',
value: (stats) => '$' + shortenNumberWithLetter(Number(stats.market_cap), undefined, { maximumFractionDigits: 0 }),
value: (stats) => '$' + Number(stats.market_cap).toLocaleString('en', { maximumFractionDigits: 0, notation: 'compact' }),
icon: <Icon as={ globeIcon } boxSize={ 6 } bgColor="#6A5DCC" borderRadius="base" color="white"/>,
// eslint-disable-next-line max-len
hint: 'The total market value of a cryptocurrency\'s circulating supply. It is analogous to the free-float capitalization in the stock market. Market Cap = Current Price x Circulating Supply.',
......@@ -60,7 +59,7 @@ const marketPriceIndicator: TChainIndicator<'homepage_chart_market'> = {
.map((item) => ({ date: new Date(item.date), value: Number(item.closing_price) * Number(response.available_supply) }))
.sort(sortByDateDesc),
name: 'Market cap',
valueFormatter: (x: number) => '$' + shortenNumberWithLetter(x, undefined, { maximumFractionDigits: 0 }),
valueFormatter: (x: number) => '$' + x.toLocaleString('en', { maximumFractionDigits: 0 }),
} ]),
},
};
......
......@@ -9,7 +9,6 @@ import iconSuccess from 'icons/status/success.svg';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/appContext';
import useContractTabs from 'lib/hooks/useContractTabs';
import notEmpty from 'lib/notEmpty';
import getQueryParamString from 'lib/router/getQueryParamString';
import AddressBlocksValidated from 'ui/address/AddressBlocksValidated';
import AddressCoinBalance from 'ui/address/AddressCoinBalance';
......@@ -57,7 +56,7 @@ const AddressPageContent = () => {
...(addressQuery.data?.public_tags || []),
...(addressQuery.data?.watchlist_names || []),
]
.filter(notEmpty)
.filter(Boolean)
.map((tag) => <Tag key={ tag.label }>{ tag.display_name }</Tag>);
const contractTabs = useContractTabs(addressQuery.data);
......@@ -92,7 +91,7 @@ const AddressPageContent = () => {
component: <AddressContract tabs={ contractTabs } addressHash={ hash }/>,
subTabs: contractTabs.map(tab => tab.id),
} : undefined,
].filter(notEmpty);
].filter(Boolean);
}, [ addressQuery.data, contractTabs, hash ]);
const tagsNode = tags.length > 0 ? <Flex columnGap={ 2 }>{ tags }</Flex> : null;
......
import { Skeleton } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/appContext';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
......@@ -28,6 +30,11 @@ const BlockPageContent = () => {
const height = getQueryParamString(router.query.height);
const tab = getQueryParamString(router.query.tab);
const blockQuery = useApiQuery('block', {
pathParams: { height },
queryOptions: { enabled: Boolean(height) },
});
const blockTxsQuery = useQueryWithPages({
resourceName: 'block_txs',
pathParams: { height },
......@@ -40,10 +47,10 @@ const BlockPageContent = () => {
throw new Error('Block not found', { cause: { status: 404 } });
}
const tabs: Array<RoutedTab> = [
{ id: 'index', title: 'Details', component: <BlockDetails/> },
const tabs: Array<RoutedTab> = React.useMemo(() => ([
{ id: 'index', title: 'Details', component: <BlockDetails query={ blockQuery }/> },
{ id: 'txs', title: 'Transactions', component: <TxsContent query={ blockTxsQuery } showBlockInfo={ false } showSocketInfo={ false }/> },
];
]), [ blockQuery, blockTxsQuery ]);
const hasPagination = !isMobile && tab === 'txs' && blockTxsQuery.isPaginationVisible;
......@@ -51,12 +58,16 @@ const BlockPageContent = () => {
return (
<Page>
<TextAd mb={ 6 }/>
{ blockQuery.isLoading ? <Skeleton h={{ base: 12, lg: 6 }} mb={ 6 } w="100%" maxW="680px"/> : <TextAd mb={ 6 }/> }
{ blockQuery.isLoading ? (
<Skeleton h={ 10 } w="300px" mb={ 6 }/>
) : (
<PageTitle
text={ `Block #${ height }` }
text={ `Block #${ blockQuery.data?.height }` }
backLinkUrl={ hasGoBackLink ? appProps.referrer : undefined }
backLinkLabel="Back to blocks list"
/>
) }
<RoutedTabs
tabs={ tabs }
tabListProps={ isMobile ? undefined : TAB_LIST_PROPS }
......
......@@ -2,7 +2,7 @@ import { Text } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
import type { SmartContractVerificationConfigRaw, SmartContractVerificationMethod } from 'types/api/contract';
import type { SmartContractVerificationMethod } from 'types/api/contract';
import useApiQuery from 'lib/api/useApiQuery';
import { useAppContext } from 'lib/appContext';
......@@ -39,11 +39,10 @@ const ContractVerification = () => {
const configQuery = useApiQuery('contract_verification_config', {
queryOptions: {
select: (data: unknown) => {
const _data = data as SmartContractVerificationConfigRaw;
select: (data) => {
return {
..._data,
verification_options: _data.verification_options.filter(isValidVerificationMethod).sort(sortVerificationMethods),
...data,
verification_options: data.verification_options.filter(isValidVerificationMethod).sort(sortVerificationMethods),
};
},
enabled: Boolean(hash),
......
......@@ -37,6 +37,7 @@ const Home = () => {
alignItems="center"
display={{ base: 'none', lg: 'flex' }}
columnGap={ 12 }
pl={ 4 }
>
<ColorModeToggler trackBg="whiteAlpha.500"/>
<ProfileMenuDesktop/>
......
......@@ -10,7 +10,6 @@ import { useAppContext } from 'lib/appContext';
import useContractTabs from 'lib/hooks/useContractTabs';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import notEmpty from 'lib/notEmpty';
import trimTokenSymbol from 'lib/token/trimTokenSymbol';
import AddressContract from 'ui/address/AddressContract';
import TextAd from 'ui/shared/ad/TextAd';
......@@ -118,7 +117,7 @@ const TokenPageContent = () => {
component: <AddressContract tabs={ contractTabs } addressHash={ hashString }/>,
subTabs: contractTabs.map(tab => tab.id),
} : undefined,
].filter(notEmpty);
].filter(Boolean);
let hasPagination;
let pagination;
......
import Script from 'next/script';
import React from 'react';
import appConfig from 'configs/app/config';
const GoogleAnalytics = () => {
if (!appConfig.googleAnalytics.propertyId) {
return null;
}
const id = appConfig.googleAnalytics.propertyId;
return (
<>
<Script src={ `https://www.googletagmanager.com/gtag/js?id=${ id }` }/>
<Script id="google-analytics">
{ `
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', '${ id }');
` }
</Script>
</>
);
};
export default React.memo(GoogleAnalytics);
import { Flex } from '@chakra-ui/react';
import React from 'react';
import getErrorStatusCode from 'lib/errors/getErrorStatusCode';
import useGetCsrfToken from 'lib/hooks/useGetCsrfToken';
import AppError from 'ui/shared/AppError/AppError';
import ErrorBoundary from 'ui/shared/ErrorBoundary';
......@@ -26,8 +27,7 @@ const Page = ({
useGetCsrfToken();
const renderErrorScreen = React.useCallback((error?: Error) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const statusCode = (error?.cause as any)?.status || 500;
const statusCode = getErrorStatusCode(error) || 500;
const isInvalidTxHash = error?.message.includes('Invalid tx hash');
if (wrapChildren) {
......
......@@ -4,29 +4,17 @@ import React from 'react';
import isBrowser from 'lib/isBrowser';
declare global {
interface Window {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
coinzilla_display: any;
}
}
type CPreferences = {
zone: string;
width: string;
height: string;
}
const CoinzillaBanner = ({ className }: { className?: string }) => {
const isInBrowser = isBrowser();
React.useEffect(() => {
if (isInBrowser) {
window.coinzilla_display = window.coinzilla_display || [];
const cDisplayPreferences = {} as CPreferences;
cDisplayPreferences.zone = '26660bf627543e46851';
cDisplayPreferences.width = '728';
cDisplayPreferences.height = '90';
const cDisplayPreferences = {
zone: '26660bf627543e46851',
width: '728',
height: '90',
};
window.coinzilla_display.push(cDisplayPreferences);
}
}, [ isInBrowser ]);
......
......@@ -31,7 +31,8 @@ const CoinzillaTextAd = ({ className }: {className?: string}) => {
useEffect(() => {
fetch('https://request-global.czilladx.com/serve/native.php?z=19260bf627546ab7242')
.then(res => res.status === 200 ? res.json() : null)
.then((data) => {
.then((_data) => {
const data = _data as AdData;
setAdData(data);
if (data?.ad?.impressionUrl) {
fetch(data.ad.impressionUrl);
......
......@@ -27,7 +27,7 @@ const AddressContractIcon = ({ className }: Props) => {
transitionDuration="normal"
transitionTimingFunction="ease"
>
С
C
</Box>
</Tooltip>
);
......
......@@ -4,7 +4,6 @@ import { useMemo } from 'react';
import type { TimeChartData } from 'ui/shared/chart/types';
import { WEEK, MONTH, YEAR } from 'lib/consts';
import formatNumberToMetricPrefix from 'lib/formatNumberToMetricPrefix';
interface Props {
data: TimeChartData;
......@@ -73,7 +72,7 @@ export default function useTimeChartController({ data, width, height }: Props) {
return format(d as Date);
};
const yTickFormat = () => (d: d3.AxisDomain) => formatNumberToMetricPrefix(Number(d));
const yTickFormat = () => (d: d3.AxisDomain) => Number(d).toLocaleString('en', { maximumFractionDigits: 3, notation: 'compact' });
return {
xTickFormat,
......
......@@ -7,7 +7,6 @@ import placeholderIcon from 'icons/files/placeholder.svg';
import solIcon from 'icons/files/sol.svg';
import yulIcon from 'icons/files/yul.svg';
import infoIcon from 'icons/info.svg';
import { shortenNumberWithLetter } from 'lib/formatters';
const FILE_ICONS: Record<string, React.FunctionComponent<React.SVGAttributes<SVGElement>>> = {
'.json': jsonIcon,
......@@ -101,7 +100,9 @@ const FileSnippet = ({ file, className, index, onRemove, isDisabled, error }: Pr
alignSelf="flex-start"
/>
</Flex>
<Text variant="secondary" mt={ 1 }>{ shortenNumberWithLetter(file.size) }B</Text>
<Text variant="secondary" mt={ 1 }>
{ file.size.toLocaleString('en', { notation: 'compact', maximumFractionDigits: 2, unit: 'byte', unitDisplay: 'narrow', style: 'unit' }) }
</Text>
</Box>
</Flex>
);
......
......@@ -6,7 +6,6 @@ import type { Log } from 'types/api/log';
// import searchIcon from 'icons/search.svg';
import { space } from 'lib/html-entities';
import notEmpty from 'lib/notEmpty';
import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
......@@ -83,7 +82,7 @@ const LogItem = ({ address, index, topics, data, decoded, type, tx_hash: txHash
) }
<RowHeader>Topics</RowHeader>
<GridItem>
{ topics.filter(notEmpty).map((item, index) => (
{ topics.filter(Boolean).map((item, index) => (
<LogTopic
key={ index }
hex={ item }
......
......@@ -25,7 +25,7 @@ test('base view', async({ mount, page }) => {
);
await component.locator('svg[aria-label="Menu button"]').click();
await expect(page).toHaveScreenshot();
await expect(page.locator('.chakra-modal__content-container')).toHaveScreenshot();
await page.locator('button[aria-label="Network menu"]').click();
await expect(page).toHaveScreenshot();
......@@ -50,15 +50,7 @@ test.describe('dark mode', () => {
});
});
test.describe('auth', () => {
const extendedTest = test.extend({
context: ({ context }, use) => {
authFixture(context);
use(context);
},
});
extendedTest('base view', async({ mount, page }) => {
test('submenu', async({ mount, page }) => {
const component = await mount(
<TestApp>
<Burger/>
......@@ -67,10 +59,21 @@ test.describe('auth', () => {
);
await component.locator('svg[aria-label="Menu button"]').click();
await page.locator('div[aria-label="Blockchain link group"]').click();
await expect(page).toHaveScreenshot();
});
test.describe('auth', () => {
const extendedTest = test.extend({
context: ({ context }, use) => {
authFixture(context);
use(context);
},
});
extendedTest('submenu', async({ mount, page }) => {
extendedTest.use({ viewport: { width: devices['iPhone 13 Pro'].viewport.width, height: 1000 } });
extendedTest('base view', async({ mount, page }) => {
const component = await mount(
<TestApp>
<Burger/>
......@@ -79,7 +82,6 @@ test.describe('auth', () => {
);
await component.locator('svg[aria-label="Menu button"]').click();
await page.locator('div[aria-label="Blockchain link group"]').click();
await expect(page).toHaveScreenshot();
});
});
......@@ -70,8 +70,9 @@ const NavLinkGroupDesktop = ({ text, subItems, icon, isCollapsed, isActive }: Pr
{ text }
</Text>
<VStack spacing={ 1 } alignItems="start">
{ subItems.map(item => Array.isArray(item) ? (
{ subItems.map((item, index) => Array.isArray(item) ? (
<Box
key={ index }
w="100%"
as="ul"
_notLast={{
......
......@@ -93,8 +93,9 @@ const NavigationMobile = () => {
as="ul"
>
{ isGroupItem(openedItem) && openedItem.subItems?.map(
item => Array.isArray(item) ? (
(item, index) => Array.isArray(item) ? (
<Box
key={ index }
w="100%"
as="ul"
_notLast={{
......
......@@ -2,7 +2,6 @@ import { Grid } from '@chakra-ui/react';
import React from 'react';
import useApiQuery from 'lib/api/useApiQuery';
import formatNumberToMetricPrefix from 'lib/formatNumberToMetricPrefix';
import DataFetchAlert from '../shared/DataFetchAlert';
import NumberWidget from './NumberWidget';
......@@ -32,7 +31,7 @@ const NumberWidgetsList = () => {
<NumberWidget
key={ id }
label={ title }
value={ `${ formatNumberToMetricPrefix(Number(value)) } ${ units ? units : '' }` }
value={ `${ Number(value).toLocaleString('en', { maximumFractionDigits: 3, notation: 'compact' }) } ${ units ? units : '' }` }
/>
);
}) }
......
......@@ -10,6 +10,7 @@ import {
Flex,
Tooltip,
chakra,
useColorModeValue,
} from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import { route } from 'nextjs-routes';
......@@ -59,6 +60,7 @@ const TxDetails = () => {
smooth: true,
});
}, []);
const executionSuccessIconColor = useColorModeValue('blackAlpha.800', 'whiteAlpha.800');
if (isLoading) {
return <TxDetailsSkeleton/>;
......@@ -94,7 +96,7 @@ const TxDetails = () => {
const executionSuccessBadge = toAddress.is_contract && data.result === 'success' ? (
<Tooltip label="Contract execution completed">
<chakra.span display="inline-flex" ml={ 2 } mr={ 1 }>
<Icon as={ successIcon } boxSize={ 4 } color="green.500" cursor="pointer"/>
<Icon as={ successIcon } boxSize={ 4 } color={ executionSuccessIconColor } cursor="pointer"/>
</chakra.span>
</Tooltip>
) : null;
......
......@@ -37,8 +37,9 @@ type Props = {
}
const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }: Props) => {
const dataTo = tx.to ? tx.to : tx.created_contract;
const isOut = Boolean(currentAddress && currentAddress === tx.from.hash);
const isIn = Boolean(currentAddress && currentAddress === tx.to?.hash);
const isIn = Boolean(currentAddress && currentAddress === dataTo.hash);
const timeAgo = useTimeAgoIncrement(tx.timestamp, enableTimeIncrement);
......@@ -49,8 +50,6 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }
</Address>
);
const dataTo = tx.to ? tx.to : tx.created_contract;
const addressTo = (
<Address>
<AddressIcon address={ dataTo }/>
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment