Commit f224803d authored by tom goriunov's avatar tom goriunov Committed by GitHub

Merge branch 'main' into address-qr-code

parents 5eaf61c4 9fc864f1
......@@ -9,6 +9,8 @@ NEXT_PUBLIC_AUTH_URL=__PLACEHOLDER_FOR_NEXT_PUBLIC_AUTH_URL__
# network config
NEXT_PUBLIC_NETWORK_NAME=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_NAME__
NEXT_PUBLIC_NETWORK_SHORT_NAME=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_SHORT_NAME__
NEXT_PUBLIC_NETWORK_LOGO=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_LOGO__
NEXT_PUBLIC_NETWORK_SMALL_LOGO=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_SMALL_LOGO__
NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME__
NEXT_PUBLIC_NETWORK_TYPE=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_TYPE__
NEXT_PUBLIC_NETWORK_ID=__PLACEHOLDER_FOR_NEXT_PUBLIC_NETWORK_ID__
......@@ -35,10 +37,13 @@ NEXT_PUBLIC_HOMEPAGE_CHARTS=__PLACEHOLDER_FOR_NEXT_PUBLIC_HOMEPAGE_CHARTS__
NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT=__PLACEHOLDER_FOR_NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT__
NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER=__PLACEHOLDER_FOR_NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER__
NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME=__PLACEHOLDER_FOR_NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME__
NEXT_PUBLIC_AD_DOMAIN_WITH_AD=__PLACEHOLDER_FOR_NEXT_PUBLIC_DOMAIN_WITH_AD__
NEXT_PUBLIC_AD_ADBUTLER_ON=__PLACEHOLDER_FORNEXT_PUBLIC_AD_ADBUTLER_ON__
# api config
NEXT_PUBLIC_API_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_HOST__
NEXT_PUBLIC_API_BASE_PATH=__PLACEHOLDER_FOR_NEXT_PUBLIC_API_BASE_PATH__
NEXT_PUBLIC_STATS_API_HOST=__PLACEHOLDER_FOR_NEXT_PUBLIC_STATS_API_HOST__
# external services config
NEXT_PUBLIC_SENTRY_DSN=__PLACEHOLDER_FOR_NEXT_PUBLIC_SENTRY_DSN__
......
......@@ -11,5 +11,6 @@ jobs:
cleanup:
uses: blockscout/blockscout-ci-cd/.github/workflows/cleanup.yaml@master
with:
appNamespace: review-front-$GITHUB_HEAD_REF_SLUG
appNamespace: review-front-$GITHUB_REF_NAME_SLUG
dockerImage: prerelease-$GITHUB_REF_NAME_SLUG
secrets: inherit
......@@ -59,7 +59,7 @@ jobs:
push: true
cache-from: type=gha
cache-to: type=gha,mode=max
tags: ghcr.io/blockscout/frontend:prerelease-${{ env.GITHUB_EVENT_PULL_REQUEST_HEAD_SHA_SHORT }}
tags: ghcr.io/blockscout/frontend:prerelease-${{ env.GITHUB_REF_NAME_SLUG }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
GIT_COMMIT_SHA=${{ env.GITHUB_EVENT_PULL_REQUEST_HEAD_SHA_SHORT }}
......@@ -69,12 +69,12 @@ jobs:
needs: push_to_registry
uses: blockscout/blockscout-ci-cd/.github/workflows/deploy.yaml@master
with:
env_vars: VALUES_DIR=deploy/values/review,APP_NAME=bs-stack,DOCKER_IMAGE=prerelease-$GITHUB_EVENT_PULL_REQUEST_HEAD_SHA_SHORT
env_vars: VALUES_DIR=deploy/values/review,APP_NAME=bs-stack,DOCKER_IMAGE=prerelease-$GITHUB_REF_NAME_SLUG
globalEnv: review
appNamespace: review-front-$GITHUB_HEAD_REF_SLUG
appNamespace: review-front-$GITHUB_REF_NAME_SLUG
blockscoutIngressHost: blockscout
frontendIngressHost: blockscout
frontendImage: ghcr.io/blockscout/frontend:prerelease-$GITHUB_EVENT_PULL_REQUEST_HEAD_SHA_SHORT
frontendImage: ghcr.io/blockscout/frontend:prerelease-$GITHUB_REF_NAME_SLUG
gethIngressHost: geth
scVerifierIngressHost: sc-verifier
secrets: inherit
......@@ -52,6 +52,7 @@ The app instance could be customized by passing following variables to NodeJS en
| NEXT_PUBLIC_NETWORK_TOKEN_ADDRESS | `string` | Address of network's native token | `0x029a799563238d0e75e20be2f4bda0ea68d00172` |
| NEXT_PUBLIC_NETWORK_ASSETS_PATHNAME | `string` *(optional)* | Network name for constructing url of token logos according to template `https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/${assetsNamePath}/assets/${tokenAddress}/logo.png`. It should match network name in TrustWallet assets repo, see the full list [here](https://github.com/trustwallet/assets/tree/master/blockchains) | `ethereum` |
| NEXT_PUBLIC_NETWORK_LOGO | `string` *(optional)* | Network logo; if not provided, will fallback to logo predefined in the project; if the project doesn't have logo for such network then the common placeholder will be shown; *Note* that logo height should be 20px and width less than 120px | `https://www.fillmurray.com/240/40` |
| NEXT_PUBLIC_NETWORK_SMALL_LOGO | `string` *(optional)* | Small version of network logo; if not provided, will fallback to logo predefined in the project; if the project doesn't have logo for such network then the common placeholder will be shown; *Note* that logo should have square format (e.g 60px by 60px) | `https://www.fillmurray.com/60/60` |
| NEXT_PUBLIC_IS_ACCOUNT_SUPPORTED | `boolean` *(optional)* | Set to true if network has account feature | `true` |
### UI configuration
......@@ -75,7 +76,8 @@ The app instance could be customized by passing following variables to NodeJS en
| NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT | `string` *(optional)* | Gradient value for hero plate on the homepage | `radial-gradient(at 15% 86%, hsla(350,65%,70%,1) 0px, transparent 50%), radial-gradient(at 72% 57%, hsla(14,95%,76%,1) 0px, transparent 50%)` |
| NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER | `boolean` *(optional)* | Set to false if network doesn't have gas tracker | `true` |
| NEXT_PUBLIC_HOMEPAGE_SHOW_AVG_BLOCK_TIME | `boolean` *(optional)* | Set to false if average block time is useless for the network | `true` |
| NEXT_PUBLIC_DOMAIN_WITH_AD | `string` *(optional)* | The domain on which we display ads | `blockscout.com` |
| NEXT_PUBLIC_AD_ADBUTLER_ON | `boolean` *(optional)* | Set to true to show Adbutler banner instead of Coinzilla banner | `false` |
### App configuration
| Variable | Type | Description | Default value
......@@ -92,6 +94,7 @@ The app instance could be customized by passing following variables to NodeJS en
| --- | --- | --- | --- |
| NEXT_PUBLIC_API_HOST | `string` *(optional)* | By default the API endpoint base URL will be set as `https://blockscout.com`. If it is not the case, pass the API host in this variable | `my-host.com` |
| NEXT_PUBLIC_API_BASE_PATH | `string` *(optional)* | Base path for API endpoint url | `/poa/core` |
| NEXT_PUBLIC_STATS_API_HOST | `string` *(optional)* | Pass the Stats API host in this variable | `https://my-host.com` |
### Featured network configuration properties
......
......@@ -13,7 +13,7 @@ const parseEnvJson = <DataType>(env: string | undefined): DataType | null => {
};
const stripTrailingSlash = (str: string) => str[str.length - 1] === '/' ? str.slice(0, -1) : str;
const env = process.env.VERCEL_ENV || process.env.NODE_ENV;
const env = process.env.NODE_ENV;
const isDev = env === 'development';
const appPort = getEnvValue(process.env.NEXT_PUBLIC_APP_PORT);
......@@ -22,7 +22,7 @@ const appHost = getEnvValue(process.env.NEXT_PUBLIC_APP_HOST);
const baseUrl = [
appSchema || 'https',
'://',
process.env.NEXT_PUBLIC_VERCEL_URL || appHost,
appHost,
appPort && ':' + appPort,
].filter(Boolean).join('');
const authUrl = getEnvValue(process.env.NEXT_PUBLIC_AUTH_URL) || baseUrl;
......@@ -54,6 +54,7 @@ const config = Object.freeze({
network: {
type: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_TYPE) as PreDefinedNetwork | undefined,
logo: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_LOGO),
smallLogo: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_SMALL_LOGO),
name: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_NAME),
id: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_ID),
shortName: getEnvValue(process.env.NEXT_PUBLIC_NETWORK_SHORT_NAME),
......@@ -84,11 +85,18 @@ const config = Object.freeze({
baseUrl,
authUrl,
logoutUrl,
ad: {
domainWithAd: getEnvValue(process.env.NEXT_PUBLIC_AD_DOMAIN_WITH_AD) || 'blockscout.com',
adButlerOn: getEnvValue(process.env.NEXT_PUBLIC_AD_ADBUTLER_ON) === 'true',
},
api: {
endpoint: apiHost ? `https://${ apiHost }` : 'https://blockscout.com',
socket: apiHost ? `wss://${ apiHost }` : 'wss://blockscout.com',
basePath: stripTrailingSlash(getEnvValue(process.env.NEXT_PUBLIC_API_BASE_PATH) || ''),
},
statsApi: {
endpoint: getEnvValue(process.env.NEXT_PUBLIC_STATS_API_HOST),
},
homepage: {
charts: parseEnvJson<Array<ChainIndicatorId>>(getEnvValue(process.env.NEXT_PUBLIC_HOMEPAGE_CHARTS)) || [],
plateGradient: getEnvValue(process.env.NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT) ||
......
......@@ -12,3 +12,4 @@ NEXT_PUBLIC_FOOTER_TWITTER_LINK=https://www.twitter.com/blockscoutcom
# api config
NEXT_PUBLIC_API_HOST=blockscout.com
NEXT_PUBLIC_STATS_API_HOST=https://stats-test.aws-k8s.blockscout.com
......@@ -20,3 +20,4 @@ NEXT_PUBLIC_MARKETPLACE_APP_LIST=[{'author': 'Blockscout','id':'token-approval-t
# api config
NEXT_PUBLIC_API_HOST=blockscout-main.test.aws-k8s.blockscout.com
NEXT_PUBLIC_API_BASE_PATH=/
NEXT_PUBLIC_STATS_API_HOST=https://stats-test.aws-k8s.blockscout.com
......@@ -10,3 +10,4 @@ NEXT_PUBLIC_FOOTER_TWITTER_LINK=https://www.twitter.com/blockscoutcom
# api config
NEXT_PUBLIC_API_HOST=blockscout.com
NEXT_PUBLIC_STATS_API_HOST=https://stats-test.aws-k8s.blockscout.com
......@@ -5,6 +5,7 @@ NEXT_PUBLIC_FEATURED_NETWORKS=[{'title':'Gnosis Chain','url':'https://blockscout
NEXT_PUBLIC_NETWORK_EXPLORERS=[{'title':'Anyblock','baseUrl':'https://explorer.anyblock.tools','paths':{'tx':'/ethereum/poa/core/transaction','address':'/ethereum/poa/core/address'}}]
NEXT_PUBLIC_HOMEPAGE_CHARTS=['daily_txs','coin_price','market_cup']
NEXT_PUBLIC_HOMEPAGE_PLATE_GRADIENT=radial-gradient(at 12% 37%, hsla(324,73%,67%,1) 0px, transparent 50%), radial-gradient(at 62% 14%, hsla(256,87%,73%,1) 0px, transparent 50%), radial-gradient(at 84% 80%, hsla(128,75%,73%,1) 0px, transparent 50%), radial-gradient(at 57% 46%, hsla(285,63%,72%,1) 0px, transparent 50%), radial-gradient(at 37% 30%, hsla(174,70%,61%,1) 0px, transparent 50%), radial-gradient(at 15% 86%, hsla(350,65%,70%,1) 0px, transparent 50%), radial-gradient(at 67% 57%, hsla(14,95%,76%,1) 0px, transparent 50%)
#NEXT_PUBLIC_NETWORK_SMALL_LOGO=https://placekitten.com/300/300
# network config
NEXT_PUBLIC_NETWORK_NAME=POA
......
......@@ -14,3 +14,4 @@ NEXT_PUBLIC_HOMEPAGE_SHOW_GAS_TRACKER=true
# api config
NEXT_PUBLIC_API_HOST=blockscout.com
NEXT_PUBLIC_STATS_API_HOST=https://stats-test.aws-k8s.blockscout.com
import type { NextjsOptions } from '@sentry/nextjs/types/utils/nextjsOptions';
const config: NextjsOptions = {
environment: process.env.VERCEL_ENV || process.env.NODE_ENV,
environment: process.env.NODE_ENV,
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
release: process.env.NEXT_PUBLIC_GIT_COMMIT_SHA || process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA,
release: process.env.NEXT_PUBLIC_GIT_COMMIT_SHA,
// We recommend adjusting this value in production, or using tracesSampler
// for finer control
tracesSampleRate: 1.0,
......
......@@ -2,9 +2,9 @@ import type * as Sentry from '@sentry/react';
import { BrowserTracing } from '@sentry/tracing';
export const config: Sentry.BrowserOptions = {
environment: process.env.VERCEL_ENV || process.env.NEXT_PUBLIC_APP_ENV || process.env.NODE_ENV,
environment: process.env.NEXT_PUBLIC_APP_ENV || process.env.NODE_ENV,
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
release: process.env.NEXT_PUBLIC_GIT_COMMIT_SHA || process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA,
release: process.env.NEXT_PUBLIC_GIT_COMMIT_SHA,
integrations: [ new BrowserTracing() ],
// We recommend adjusting this value in production, or using tracesSampler
// for finer control
......
......@@ -119,7 +119,7 @@ blockscout:
GAS_PRICE_ORACLE_CACHE_PERIOD:
_default: 300
POOL_SIZE:
_default: 20
_default: 100
DISPLAY_TOKEN_ICONS:
_default: 'true'
FETCH_REWARDS_WAY:
......@@ -454,6 +454,8 @@ frontend:
- "/tx"
- "/blocks"
- "/block"
- "/address"
- "/stats"
resources:
limits:
memory:
......@@ -517,6 +519,8 @@ frontend:
_default: unknown
NEXT_PUBLIC_API_HOST:
_default: blockscout.com/eth/goerli
NEXT_PUBLIC_STATS_API_HOST:
_default: https://stats-test.aws-k8s.blockscout.com/
NEXT_PUBLIC_APP_HOST:
_default: blockscout.com/eth/goerli
NEXT_PUBLIC_LOGOUT_URL:
......
......@@ -316,6 +316,9 @@ frontend:
- "/tx"
- "/blocks"
- "/block"
- "/login"
- "/address"
- "/stats"
resources:
limits:
memory:
......@@ -371,6 +374,8 @@ frontend:
NEXT_PUBLIC_API_HOST:
_default: blockscout.com
review: blockscout-main.test.aws-k8s.blockscout.com
NEXT_PUBLIC_STATS_API_HOST:
_default: https://stats-test.aws-k8s.blockscout.com/
NEXT_PUBLIC_AUTH_URL:
_default: https://blockscout-main.test.aws-k8s.blockscout.com
NEXT_PUBLIC_API_BASE_PATH:
......
<svg viewBox="0 0 143 26" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.513 1a1 1 0 0 0-1-1H6.068a1 1 0 0 0-1 1v3.417a1 1 0 0 1-1 1H1.49a1 1 0 0 0-1 1V25a1 1 0 0 0 1 1h3.445a1 1 0 0 0 1-1V6.417a1 1 0 0 1 1-1h2.578a1 1 0 0 0 1-1V1Zm10.926 0a1 1 0 0 0-1-1h-3.445a1 1 0 0 0-1 1v3.417a1 1 0 0 0 1 1h2.389a1 1 0 0 1 1 1V25a1 1 0 0 0 1 1h3.444a1 1 0 0 0 1-1V6.417a1 1 0 0 0-1-1H22.44a1 1 0 0 1-1-1V1Zm-5.52 10.369a1 1 0 0 0-1-1h-3.445a1 1 0 0 0-1 1v8.524a1 1 0 0 0 1 1h3.445a1 1 0 0 0 1-1v-8.524Z"/>
<path d="M35.113 5.893h6.228c2.638 0 4.065 1.434 4.065 3.498.02.645-.15 1.28-.486 1.825a3.12 3.12 0 0 1-1.395 1.221v.067a3.548 3.548 0 0 1 1.842 1.364c.453.652.687 1.44.666 2.242 0 2.308-1.544 4.122-4.346 4.122h-6.574V5.893Zm6.27 5.827c.996 0 1.71-.761 1.71-1.92 0-1.158-.714-1.92-1.71-1.92h-3.95v3.84h3.95Zm.282 6.54c1.19 0 2.054-.94 2.054-2.306 0-1.367-.865-2.285-2.054-2.285h-4.232v4.592h4.232ZM48.93 7.599h-1.303V5.436h3.59v14.786h-2.293L48.931 7.6Zm4.409 7.324c0-3.248 2.317-5.644 5.517-5.644s5.535 2.374 5.535 5.644-2.313 5.623-5.535 5.623-5.517-2.375-5.517-5.623Zm4.911 3.495h1.236c1.406 0 2.638-1.546 2.638-3.495 0-1.948-1.236-3.52-2.638-3.52H58.25c-1.405 0-2.638 1.6-2.638 3.52s1.233 3.495 2.638 3.495Zm7.63-3.518c0-3.314 2.21-5.621 5.388-5.621 2.552 0 4.562 1.366 4.887 4.16h-2.227c-.217-1.412-1.19-2.039-2.184-2.039h-1.082c-1.384 0-2.508 1.568-2.508 3.52s1.124 3.54 2.508 3.54h1.082a2.163 2.163 0 0 0 1.494-.597c.409-.386.662-.916.711-1.487h2.227c-.308 2.755-2.335 4.189-4.942 4.189-3.17-.02-5.353-2.349-5.353-5.664Zm12.348-9.464h2.27v8.156h1.647l3.07-4.01h2.595l-3.784 4.951 3.957 5.69h-2.725l-3.308-4.704h-1.452v4.703h-2.27V5.436ZM89.02 17.029h2.185c.108.94.67 1.546 1.853 1.546h1.43c1.017 0 1.493-.538 1.493-1.255 0-.717-.39-1.142-1.32-1.28l-2.403-.32c-2.032-.246-2.94-1.5-2.94-3.136 0-2.15 1.544-3.315 4.303-3.315 2.64 0 4.24 1.098 4.324 3.54h-2.184c-.108-.92-.475-1.569-1.6-1.569h-1.3c-.973 0-1.428.538-1.428 1.232 0 .695.433 1.165 1.363 1.3l2.425.32c1.968.246 2.876 1.343 2.876 3.046 0 2.195-1.406 3.404-4.52 3.404-3.012.004-4.473-1.184-4.556-3.513ZM99.524 14.9c0-3.314 2.205-5.621 5.387-5.621 2.552 0 4.563 1.366 4.887 4.16h-2.227c-.216-1.412-1.189-2.039-2.184-2.039h-1.081c-1.387 0-2.512 1.568-2.512 3.52s1.125 3.54 2.512 3.54h1.081a2.165 2.165 0 0 0 1.495-.597 2.32 2.32 0 0 0 .711-1.487h2.227c-.309 2.755-2.336 4.189-4.943 4.189-3.166-.02-5.353-2.349-5.353-5.664Zm11.738.023c0-3.248 2.314-5.644 5.514-5.644 3.201 0 5.539 2.374 5.539 5.644s-2.314 5.623-5.539 5.623c-3.225 0-5.514-2.375-5.514-5.623Zm4.909 3.495h1.236c1.405 0 2.641-1.546 2.641-3.495 0-1.948-1.236-3.52-2.641-3.52h-1.236c-1.406 0-2.638 1.6-2.638 3.52s1.239 3.495 2.644 3.495h-.006Zm8.029-2.128V9.595h2.27v6.295c0 1.545.757 2.464 1.752 2.464h1.235c1.06 0 2.011-1.008 2.011-2.464V9.595h2.274v10.64h-2.209V18.78c-.618 1.098-1.708 1.77-3.373 1.77-2.49-.003-3.96-1.504-3.96-4.26Zm13.32 1.456v-6.08h-1.99v-2.07h1.99V6.612h2.292v2.982h2.681v2.061h-2.681v5.42c0 .695.216 1.031.951 1.031h1.817v2.128h-2.472c-1.662-.003-2.588-.899-2.588-2.49Z"/>
</svg>
<svg viewBox="0 0 30 30" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.822 15.052h-6.9v2.731h3.19l-.01 3.433c-.123.132-.269.24-.43.319-.21.108-.43.197-.656.267a5.653 5.653 0 0 1-1.656.247 3.012 3.012 0 0 1-2.715-1.424 7.746 7.746 0 0 1-.9-4.064v-2.6a9.44 9.44 0 0 1 .265-2.36 5.76 5.76 0 0 1 .715-1.71c.267-.427.63-.785 1.06-1.047.4-.237.858-.36 1.323-.357a2.922 2.922 0 0 1 2.191.735c.526.615.84 1.383.895 2.191h3.627a8.472 8.472 0 0 0-.615-2.458 5.315 5.315 0 0 0-1.304-1.857 5.626 5.626 0 0 0-2.047-1.164 9.016 9.016 0 0 0-2.84-.4 6.71 6.71 0 0 0-2.774.572A6.354 6.354 0 0 0 2.016 7.77a7.927 7.927 0 0 0-1.483 2.659 10.954 10.954 0 0 0-.532 3.557v2.575a11.475 11.475 0 0 0 .51 3.557c.3.97.793 1.871 1.45 2.646a6.251 6.251 0 0 0 2.264 1.65 7.406 7.406 0 0 0 2.966.573c.765.005 1.529-.07 2.278-.221a10.497 10.497 0 0 0 1.914-.58 7.636 7.636 0 0 0 1.477-.8 5.2 5.2 0 0 0 .98-.87l-.018-7.464Zm2.66 2.783c-.01.97.143 1.935.451 2.855.281.837.73 1.61 1.317 2.269a5.985 5.985 0 0 0 2.133 1.5 7.278 7.278 0 0 0 2.88.54 7.194 7.194 0 0 0 2.86-.54 5.935 5.935 0 0 0 2.118-1.5 6.602 6.602 0 0 0 1.311-2.27c.308-.92.46-1.884.45-2.854v-.274a8.673 8.673 0 0 0-.45-2.84 6.532 6.532 0 0 0-1.317-2.27 6.078 6.078 0 0 0-2.125-1.508 7.812 7.812 0 0 0-5.74 0 6.082 6.082 0 0 0-2.12 1.508 6.516 6.516 0 0 0-1.317 2.27 8.647 8.647 0 0 0-.45 2.84v.274Zm3.681-.273a7.394 7.394 0 0 1 .174-1.625c.1-.478.284-.936.541-1.352a2.633 2.633 0 0 1 2.357-1.26c.495-.016.984.1 1.418.337.387.224.712.542.947.923.253.417.435.874.535 1.352a7.4 7.4 0 0 1 .173 1.625v.273a7.613 7.613 0 0 1-.172 1.659c-.1.478-.281.935-.537 1.352a2.622 2.622 0 0 1-2.337 1.255 2.818 2.818 0 0 1-1.424-.338 2.759 2.759 0 0 1-.96-.917 4.143 4.143 0 0 1-.541-1.352 7.6 7.6 0 0 1-.174-1.659v-.274.001Zm-1.747-10.3c.086.196.212.37.37.514.167.147.36.262.57.338.472.164.985.164 1.456 0 .21-.076.404-.19.57-.338.16-.143.285-.318.37-.514a1.594 1.594 0 0 0 0-1.274 1.562 1.562 0 0 0-.37-.52 1.7 1.7 0 0 0-.57-.344 2.206 2.206 0 0 0-1.456 0 1.7 1.7 0 0 0-.57.345 1.562 1.562 0 0 0-.502 1.157c0 .219.045.436.133.637v-.001Zm6.357.013c.086.197.212.374.37.52a1.7 1.7 0 0 0 .57.345c.47.165.984.165 1.456 0a1.7 1.7 0 0 0 .57-.345c.157-.146.283-.323.37-.52.089-.205.134-.427.132-.65a1.561 1.561 0 0 0-.503-1.151 1.761 1.761 0 0 0-.57-.338 2.206 2.206 0 0 0-1.456 0c-.21.075-.403.19-.57.338a1.546 1.546 0 0 0-.503 1.151c-.002.224.043.446.134.65Z" fill="currentColor"/>
</svg>
<svg viewBox="0 0 114 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.21 0a1 1 0 0 1 1 1v2.167a1 1 0 0 1-1 1H5.687a1 1 0 0 0-1 1V19a1 1 0 0 1-1 1H1.5a1 1 0 0 1-1-1V5.167a1 1 0 0 1 1-1h1.521a1 1 0 0 0 1-1V1a1 1 0 0 1 1-1H7.21Zm8.404 0a1 1 0 0 1 1 1v2.167a1 1 0 0 0 1 1h1.376a1 1 0 0 1 1 1V19a1 1 0 0 1-1 1H16.8a1 1 0 0 1-1-1V5.167a1 1 0 0 0-1-1h-1.375a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1h2.188Zm-4.246 7.976a1 1 0 0 1 1 1v6.095a1 1 0 0 1-1 1H9.18a1 1 0 0 1-1-1V8.976a1 1 0 0 1 1-1h2.188Zm26.776-2.131h1.035l-.005 9.71h1.821V4.181h-2.851v1.664ZM33.15 4.533h-4.948v11.03h5.223c2.225 0 3.452-1.396 3.452-3.17a2.825 2.825 0 0 0-.529-1.726 2.81 2.81 0 0 0-1.463-1.049v-.052a2.456 2.456 0 0 0 1.494-2.343c0-1.588-1.134-2.69-3.23-2.69Zm1.391 3.005c0 .891-.567 1.477-1.357 1.477h-3.139V6.061h3.139c.79 0 1.357.586 1.357 1.477Zm.498 4.734c0 1.05-.687 1.774-1.632 1.774h-3.362v-3.532h3.362c.945 0 1.632.707 1.632 1.758Zm7.642-.793c0-2.498 1.84-4.342 4.383-4.342s4.398 1.826 4.398 4.342c0 2.516-1.838 4.325-4.398 4.325-2.56 0-4.383-1.827-4.383-4.325Zm3.902 2.688h.982c1.117 0 2.096-1.189 2.096-2.688 0-1.5-.982-2.708-2.096-2.708h-.982c-1.116 0-2.095 1.231-2.095 2.708s.979 2.688 2.095 2.688Zm10.342-7.03c-2.525 0-4.28 1.775-4.28 4.325 0 2.55 1.735 4.342 4.253 4.357 2.071 0 3.681-1.103 3.927-3.223h-1.77a1.75 1.75 0 0 1-1.752 1.602h-.859c-1.1 0-1.993-1.22-1.993-2.722 0-1.501.893-2.707 1.993-2.707h.859c.79 0 1.563.482 1.735 1.568h1.77c-.258-2.149-1.856-3.2-3.883-3.2Zm5.53-2.956h1.803v6.274h1.308l2.44-3.084h2.06l-3.006 3.808 3.144 4.376H68.04l-2.629-3.618h-1.153v3.618h-1.804V4.181Zm10.309 8.918h-1.735c.066 1.792 1.227 2.705 3.62 2.702 2.473 0 3.59-.93 3.59-2.619 0-1.31-.722-2.154-2.285-2.343l-1.926-.246c-.74-.104-1.083-.465-1.083-1 0-.534.361-.947 1.134-.947h1.033c.894 0 1.186.5 1.272 1.206h1.735c-.067-1.878-1.338-2.722-3.436-2.722-2.192 0-3.419.896-3.419 2.55 0 1.257.722 2.222 2.337 2.412l1.909.246c.739.106 1.048.433 1.048.985 0 .551-.378.964-1.186.964h-1.136c-.94 0-1.386-.465-1.472-1.188Zm6.609-1.637c0-2.55 1.752-4.325 4.28-4.325 2.027 0 3.624 1.051 3.882 3.2h-1.77c-.171-1.086-.944-1.568-1.734-1.568h-.86c-1.101 0-1.995 1.206-1.995 2.707 0 1.502.894 2.723 1.996 2.723h.859a1.75 1.75 0 0 0 1.752-1.602h1.77c-.246 2.119-1.856 3.222-3.927 3.222-2.516-.015-4.253-1.807-4.253-4.357Zm13.706-4.325c-2.543 0-4.38 1.844-4.38 4.342 0 2.498 1.818 4.325 4.38 4.325s4.4-1.81 4.4-4.325c0-2.516-1.858-4.342-4.4-4.342Zm.5 7.03h-.976c-1.117 0-2.101-1.211-2.101-2.688s.98-2.708 2.096-2.708h.981c1.117 0 2.099 1.209 2.099 2.708s-.982 2.688-2.099 2.688Zm5.397-1.637V7.38h1.804v4.842c0 1.19.601 1.896 1.391 1.896h.982c.842 0 1.598-.776 1.598-1.896V7.381h1.806v8.184h-1.755v-1.12c-.491.844-1.357 1.361-2.68 1.361-1.978-.002-3.146-1.157-3.146-3.276Zm10.582-3.557v4.677c0 1.223.736 1.913 2.057 1.915h1.963v-1.637h-1.443c-.584 0-.756-.258-.756-.792v-4.17h2.13V7.38h-2.13V5.086h-1.821v2.295h-1.58v1.592h1.58Z" fill="#2B6CB0"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.21 0a1 1 0 0 1 1 1v2.167a1 1 0 0 1-1 1H5.687a1 1 0 0 0-1 1V19a1 1 0 0 1-1 1H1.5a1 1 0 0 1-1-1V5.167a1 1 0 0 1 1-1h1.521a1 1 0 0 0 1-1V1a1 1 0 0 1 1-1H7.21Zm8.404 0a1 1 0 0 1 1 1v2.167a1 1 0 0 0 1 1h1.376a1 1 0 0 1 1 1V19a1 1 0 0 1-1 1H16.8a1 1 0 0 1-1-1V5.167a1 1 0 0 0-1-1h-1.375a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1h2.188Zm-4.246 7.976a1 1 0 0 1 1 1v6.095a1 1 0 0 1-1 1H9.18a1 1 0 0 1-1-1V8.976a1 1 0 0 1 1-1h2.188Zm26.776-2.131h1.035l-.005 9.71h1.821V4.181h-2.851v1.664ZM33.15 4.533h-4.948v11.03h5.223c2.225 0 3.452-1.396 3.452-3.17a2.825 2.825 0 0 0-.529-1.726 2.81 2.81 0 0 0-1.463-1.049v-.052a2.456 2.456 0 0 0 1.494-2.343c0-1.588-1.134-2.69-3.23-2.69Zm1.391 3.005c0 .891-.567 1.477-1.357 1.477h-3.139V6.061h3.139c.79 0 1.357.586 1.357 1.477Zm.498 4.734c0 1.05-.687 1.774-1.632 1.774h-3.362v-3.532h3.362c.945 0 1.632.707 1.632 1.758Zm7.642-.793c0-2.498 1.84-4.342 4.383-4.342s4.398 1.826 4.398 4.342c0 2.516-1.838 4.325-4.398 4.325-2.56 0-4.383-1.827-4.383-4.325Zm3.902 2.688h.982c1.117 0 2.096-1.189 2.096-2.688 0-1.5-.982-2.708-2.096-2.708h-.982c-1.116 0-2.095 1.231-2.095 2.708s.979 2.688 2.095 2.688Zm10.342-7.03c-2.525 0-4.28 1.775-4.28 4.325 0 2.55 1.735 4.342 4.253 4.357 2.071 0 3.681-1.103 3.927-3.223h-1.77a1.75 1.75 0 0 1-1.752 1.602h-.859c-1.1 0-1.993-1.22-1.993-2.722 0-1.501.893-2.707 1.993-2.707h.859c.79 0 1.563.482 1.735 1.568h1.77c-.258-2.149-1.856-3.2-3.883-3.2Zm5.53-2.956h1.803v6.274h1.308l2.44-3.084h2.06l-3.006 3.808 3.144 4.376H68.04l-2.629-3.618h-1.153v3.618h-1.804V4.181Zm10.309 8.918h-1.735c.066 1.792 1.227 2.705 3.62 2.702 2.473 0 3.59-.93 3.59-2.619 0-1.31-.722-2.154-2.285-2.343l-1.926-.246c-.74-.104-1.083-.465-1.083-1 0-.534.361-.947 1.134-.947h1.033c.894 0 1.186.5 1.272 1.206h1.735c-.067-1.878-1.338-2.722-3.436-2.722-2.192 0-3.419.896-3.419 2.55 0 1.257.722 2.222 2.337 2.412l1.909.246c.739.106 1.048.433 1.048.985 0 .551-.378.964-1.186.964h-1.136c-.94 0-1.386-.465-1.472-1.188Zm6.609-1.637c0-2.55 1.752-4.325 4.28-4.325 2.027 0 3.624 1.051 3.882 3.2h-1.77c-.171-1.086-.944-1.568-1.734-1.568h-.86c-1.101 0-1.995 1.206-1.995 2.707 0 1.502.894 2.723 1.996 2.723h.859a1.75 1.75 0 0 0 1.752-1.602h1.77c-.246 2.119-1.856 3.222-3.927 3.222-2.516-.015-4.253-1.807-4.253-4.357Zm13.706-4.325c-2.543 0-4.38 1.844-4.38 4.342 0 2.498 1.818 4.325 4.38 4.325s4.4-1.81 4.4-4.325c0-2.516-1.858-4.342-4.4-4.342Zm.5 7.03h-.976c-1.117 0-2.101-1.211-2.101-2.688s.98-2.708 2.096-2.708h.981c1.117 0 2.099 1.209 2.099 2.708s-.982 2.688-2.099 2.688Zm5.397-1.637V7.38h1.804v4.842c0 1.19.601 1.896 1.391 1.896h.982c.842 0 1.598-.776 1.598-1.896V7.381h1.806v8.184h-1.755v-1.12c-.491.844-1.357 1.361-2.68 1.361-1.978-.002-3.146-1.157-3.146-3.276Zm10.582-3.557v4.677c0 1.223.736 1.913 2.057 1.915h1.963v-1.637h-1.443c-.584 0-.756-.258-.756-.792v-4.17h2.13V7.38h-2.13V5.086h-1.821v2.295h-1.58v1.592h1.58Z" fill="currentColor"/>
</svg>
<svg viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.485 7.012h1.446a.918.918 0 1 1 0 1.84H4.253a.919.919 0 0 1-.92-.92V4.254a.919.919 0 1 1 1.84 0v1.47l.505-.505a6.436 6.436 0 0 1 9.103 0 6.436 6.436 0 0 1 0 9.103 6.436 6.436 0 0 1-9.103 0 .92.92 0 0 1 1.302-1.301 4.597 4.597 0 1 0 0-6.503l-.495.494Z" fill="currentColor"/>
</svg>
<svg viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.615 5.192c0-.934.758-1.692 1.693-1.692h2.538c.935 0 1.692.758 1.692 1.692v18.616c0 .934-.757 1.692-1.692 1.692h-2.538a1.692 1.692 0 0 1-1.693-1.692V5.192Zm4.231.339a.339.339 0 0 0-.338-.339h-1.862a.339.339 0 0 0-.338.339v17.938c0 .187.151.339.338.339h1.862a.339.339 0 0 0 .338-.339V5.531ZM4 19.577c0-.935.758-1.692 1.692-1.692h2.539c.934 0 1.692.757 1.692 1.692v4.23c0 .935-.758 1.693-1.692 1.693H5.692A1.692 1.692 0 0 1 4 23.808v-4.231Zm4.23.338a.339.339 0 0 0-.338-.338H6.031a.339.339 0 0 0-.339.338v3.554a.34.34 0 0 0 .339.339h1.861a.338.338 0 0 0 .339-.339v-3.554Zm12.693-9.646c-.934 0-1.692.758-1.692 1.693v11.846c0 .934.757 1.692 1.692 1.692h2.539c.934 0 1.692-.758 1.692-1.692V11.962c0-.935-.758-1.693-1.692-1.693h-2.539Zm2.2 1.693c.187 0 .339.151.339.338v11.17a.338.338 0 0 1-.339.338h-1.861a.338.338 0 0 1-.339-.339V12.3c0-.187.152-.338.339-.338h1.861Z" fill="currentColor"/>
</svg>
......@@ -8,14 +8,17 @@ import * as cookies from 'lib/cookies';
// first arg can be only a string
// FIXME migrate to RequestInfo later if needed
export default function fetchFactory(_req: NextApiRequest) {
export default function fetchFactory(
_req: NextApiRequest,
apiEndpoint: string = appConfig.api.endpoint,
) {
return function fetch(path: string, init?: RequestInit): Promise<Response> {
const headers = {
accept: 'application/json',
'content-type': 'application/json',
cookie: `${ cookies.NAMES.API_TOKEN }=${ _req.cookies[cookies.NAMES.API_TOKEN] }`,
};
const url = new URL(path, appConfig.api.endpoint);
const url = new URL(path, apiEndpoint);
httpLogger.logger.info({
message: 'Trying to call API',
......
......@@ -6,7 +6,7 @@ import { httpLogger } from 'lib/api/logger';
type Methods = 'GET' | 'POST' | 'PUT' | 'DELETE';
export default function createHandler(getUrl: (_req: NextApiRequest) => string, allowedMethods: Array<Methods>) {
export default function createHandler(getUrl: (_req: NextApiRequest) => string, allowedMethods: Array<Methods>, apiEndpoint?: string) {
const handler = async(_req: NextApiRequest, res: NextApiResponse) => {
httpLogger(_req, res);
......@@ -18,8 +18,8 @@ export default function createHandler(getUrl: (_req: NextApiRequest) => string,
const isBodyDisallowed = _req.method === 'GET' || _req.method === 'HEAD';
const url = getUrlWithNetwork(_req, `/api${ getUrl(_req) }`);
const fetch = fetchFactory(_req);
const url = apiEndpoint ? `/api${ getUrl(_req) }` : getUrlWithNetwork(_req, `/api${ getUrl(_req) }`);
const fetch = fetchFactory(_req, apiEndpoint);
const response = await fetch(url, {
method: _req.method,
body: isBodyDisallowed ? undefined : _req.body,
......
......@@ -63,6 +63,9 @@ function makePolicyMap() {
'sentry.io', '*.sentry.io',
appConfig.api.socket,
// ad
'request-global.czilladx.com',
],
'script-src': [
......@@ -76,6 +79,12 @@ function makePolicyMap() {
// hash of ColorModeScript
'\'sha256-e7MRMmTzLsLQvIy1iizO1lXf7VWYoQ6ysj5fuUzvRwE=\'',
// ad
'coinzillatag.com',
'servedbyadbutler.com',
'\'sha256-wMOeDjJaOTjCfNjluteV+tSqHW547T89sgxd8W6tQJM=\'',
'\'sha256-FcyIn1h7zra8TVnnRhYrwrplxJW7dpD5TV7kP2AG/kI=\'',
],
'style-src': [
......@@ -113,6 +122,9 @@ function makePolicyMap() {
// marketplace apps logos
...getMarketplaceAppsLogosOrigins().map((url) => url.host),
// ad
'servedbyadbutler.com',
],
'font-src': [
......@@ -135,7 +147,12 @@ function makePolicyMap() {
KEY_WORDS.NONE,
],
'frame-src': getMarketplaceAppsOrigins(),
'frame-src': [
...getMarketplaceAppsOrigins(),
// ad
'request-global.czilladx.com',
],
...(REPORT_URI ? {
'report-uri': [
......
import BigNumber from 'bignumber.js';
interface Params {
value: string;
exchangeRate?: string | null;
accuracy?: number;
accuracyUsd?: number;
decimals?: string | null;
}
export default function getCurrencyValue({ value, accuracy, accuracyUsd, decimals, exchangeRate }: Params) {
const valueCurr = BigNumber(value).div(BigNumber(10 ** Number(decimals || '18')));
const valueResult = accuracy ? valueCurr.dp(accuracy).toFormat() : valueCurr.toFormat();
let usdResult: string | undefined;
if (exchangeRate) {
const exchangeRateBn = new BigNumber(exchangeRate);
const usdBn = valueCurr.times(exchangeRateBn);
if (accuracyUsd && !usdBn.isEqualTo(0)) {
const usdBnDp = usdBn.dp(accuracyUsd);
usdResult = usdBnDp.isEqualTo(0) ? usdBn.precision(accuracyUsd).toFormat() : usdBnDp.toFormat();
} else {
usdResult = usdBn.toFormat();
}
}
return { valueStr: valueResult, usd: usdResult };
}
import { useRouter } from 'next/router';
import link from 'lib/link/link';
export default function useLoginUrl() {
const router = useRouter();
return link('auth', {}, { path: router.asPath });
}
......@@ -9,6 +9,7 @@ import blocksIcon from 'icons/block.svg';
import privateTagIcon from 'icons/privattags.svg';
import profileIcon from 'icons/profile.svg';
import publicTagIcon from 'icons/publictags.svg';
import statsIcon from 'icons/stats.svg';
import tokensIcon from 'icons/token.svg';
import transactionsIcon from 'icons/transactions.svg';
import watchlistIcon from 'icons/watchlist.svg';
......@@ -28,6 +29,7 @@ export default function useNavItems() {
{ text: 'Tokens', url: link('tokens'), icon: tokensIcon, isActive: currentRoute === 'tokens', isNewUi: false },
isMarketplaceFilled ?
{ text: 'Apps', url: link('apps'), icon: appsIcon, isActive: currentRoute === 'apps', isNewUi: true } : null,
{ text: 'Charts & stats', url: link('stats'), icon: statsIcon, isActive: currentRoute === 'stats', isNewUi: true },
// there should be custom site sections like Stats, Faucet, More, etc but never an 'other'
// examples https://explorer-edgenet.polygon.technology/ and https://explorer.celo.org/
// at this stage custom menu items is under development, we will implement it later
......
......@@ -5,7 +5,7 @@ import React from 'react';
import { QueryKeys } from 'types/client/accountQueries';
import * as cookies from 'lib/cookies';
import link from 'lib/link/link';
import useLoginUrl from 'lib/hooks/useLoginUrl';
export interface ErrorType {
error?: {
......@@ -19,6 +19,7 @@ export default function useRedirectForInvalidAuthToken() {
const state = queryClient.getQueryState<unknown, ErrorType>([ QueryKeys.profile ]);
const errorStatus = state?.error?.error?.status;
const loginUrl = useLoginUrl();
React.useEffect(() => {
if (errorStatus === 401) {
......@@ -26,9 +27,8 @@ export default function useRedirectForInvalidAuthToken() {
if (apiToken) {
Sentry.captureException(new Error('Invalid api token'), { tags: { source: 'fetch' } });
const authURL = link('auth');
window.location.assign(authURL);
window.location.assign(loginUrl);
}
}
}, [ errorStatus ]);
}, [ errorStatus, loginUrl ]);
}
import appConfig from 'configs/app/config';
export default function isSelfHosted() {
return appConfig.host?.endsWith(appConfig.ad.domainWithAd) || appConfig.host === 'localhost';
}
......@@ -4,13 +4,18 @@ import React from 'react';
import type { RouteName } from 'lib/link/routes';
import { ROUTES } from 'lib/link/routes';
const PATH_PARAM_REGEXP = /\/:(\w+)/g;
export default function useCurrentRoute() {
const { route: nextRoute } = useRouter();
return React.useCallback((): RouteName => {
for (const routeName in ROUTES) {
const route = ROUTES[routeName as RouteName];
if (route.pattern === nextRoute) {
const formattedRoute = route.pattern.replace(PATH_PARAM_REGEXP, (_, paramName: string) => {
return `/[${ paramName }]`;
});
if (formattedRoute === nextRoute) {
return routeName as RouteName;
}
}
......
import type { FeaturedNetwork, PreDefinedNetwork } from 'types/networks';
import type { FeaturedNetwork } from 'types/networks';
import appConfig from 'configs/app/config';
import arbitrumIcon from 'icons/networks/icons/arbitrum.svg';
import artisIcon from 'icons/networks/icons/artis.svg';
import ethereumClassicIcon from 'icons/networks/icons/ethereum-classic.svg';
import ethereumIcon from 'icons/networks/icons/ethereum.svg';
import gnosisIcon from 'icons/networks/icons/gnosis.svg';
import optimismIcon from 'icons/networks/icons/optimism.svg';
import poaSokolIcon from 'icons/networks/icons/poa-sokol.svg';
import poaIcon from 'icons/networks/icons/poa.svg';
import rskIcon from 'icons/networks/icons/rsk.svg';
// predefined network icons
const ICONS: Partial<Record<PreDefinedNetwork, React.FunctionComponent<React.SVGAttributes<SVGElement>>>> = {
xdai_mainnet: gnosisIcon,
xdai_optimism: optimismIcon,
xdai_aox: arbitrumIcon,
eth_mainnet: ethereumIcon,
etc_mainnet: ethereumClassicIcon,
poa_core: poaIcon,
rsk_mainnet: rskIcon,
xdai_testnet: arbitrumIcon,
poa_sokol: poaSokolIcon,
artis_sigma1: artisIcon,
};
import ASSETS from 'lib/networks/networkAssets';
// for easy .env.example update
// const FEATURED_NETWORKS = JSON.stringify([
......@@ -136,7 +114,7 @@ const ICONS: Partial<Record<PreDefinedNetwork, React.FunctionComponent<React.SVG
const featuredNetworks: Array<FeaturedNetwork> = (() => {
return appConfig.featuredNetworks.map((network) => ({
...network,
icon: network.icon || (network.type ? ICONS[network.type] : undefined),
icon: network.icon || (network.type ? ASSETS[network.type]?.icon : undefined),
}));
})();
......
import type React from 'react';
import type { PreDefinedNetwork } from 'types/networks';
import arbitrumIcon from 'icons/networks/icons/arbitrum.svg';
import artisIcon from 'icons/networks/icons/artis.svg';
import ethereumClassicIcon from 'icons/networks/icons/ethereum-classic.svg';
import ethereumIcon from 'icons/networks/icons/ethereum.svg';
import gnosisIcon from 'icons/networks/icons/gnosis.svg';
import goerliIcon from 'icons/networks/icons/goerli.svg';
import optimismIcon from 'icons/networks/icons/optimism.svg';
import poaSokolIcon from 'icons/networks/icons/poa-sokol.svg';
import poaIcon from 'icons/networks/icons/poa.svg';
import rskIcon from 'icons/networks/icons/rsk.svg';
import artisLogo from 'icons/networks/logos/artis.svg';
import astarLogo from 'icons/networks/logos/astar.svg';
import etcLogo from 'icons/networks/logos/etc.svg';
import ethLogo from 'icons/networks/logos/eth.svg';
import gnosisLogo from 'icons/networks/logos/gnosis.svg';
import goerliLogo from 'icons/networks/logos/goerli.svg';
import luksoLogo from 'icons/networks/logos/lukso.svg';
import poaLogo from 'icons/networks/logos/poa.svg';
import rskLogo from 'icons/networks/logos/rsk.svg';
import shibuyaLogo from 'icons/networks/logos/shibuya.svg';
import shidenLogo from 'icons/networks/logos/shiden.svg';
import sokolLogo from 'icons/networks/logos/sokol.svg';
interface NetworkAssets {
icon?: React.FunctionComponent<React.SVGAttributes<SVGElement>>;
logo?: React.FunctionComponent<React.SVGAttributes<SVGElement>>;
smallLogo?: React.FunctionComponent<React.SVGAttributes<SVGElement>>;
}
const networkAssets: Partial<Record<PreDefinedNetwork, NetworkAssets>> = {
xdai_mainnet: {
icon: gnosisIcon,
logo: gnosisLogo,
},
xdai_optimism: {
icon: optimismIcon,
},
xdai_aox: {
icon: arbitrumIcon,
},
eth_mainnet: {
icon: ethereumIcon,
logo: ethLogo,
},
etc_mainnet: {
icon: ethereumClassicIcon,
logo: etcLogo,
},
poa_core: {
icon: poaIcon,
logo: poaLogo,
},
rsk_mainnet: {
icon: rskIcon,
logo: rskLogo,
},
xdai_testnet: {
icon: arbitrumIcon,
logo: gnosisLogo,
},
poa_sokol: {
icon: poaSokolIcon,
logo: sokolLogo,
},
artis_sigma1: {
icon: artisIcon,
logo: artisLogo,
},
lukso_l14: {
logo: luksoLogo,
},
astar: {
logo: astarLogo,
},
shiden: {
logo: shidenLogo,
},
shibuya: {
logo: shibuyaLogo,
},
goerli: {
logo: goerliLogo,
icon: goerliIcon,
},
};
export default networkAssets;
......@@ -7,6 +7,8 @@ SocketMessage.BlocksIndexStatus |
SocketMessage.TxStatusUpdate |
SocketMessage.NewTx |
SocketMessage.NewPendingTx |
SocketMessage.AddressTokenBalance |
SocketMessage.AddressCoinBalance |
SocketMessage.Unknown;
interface SocketMessageParamsGeneric<Event extends string | undefined, Payload extends object | unknown> {
......@@ -15,6 +17,16 @@ interface SocketMessageParamsGeneric<Event extends string | undefined, Payload e
handler: (payload: Payload) => void;
}
export interface AddressCoinBalancePayload {
coin_balance: {
block_number: number;
block_timestamp?: string;
delta?: string;
transaction_hash?: string | null;
value?: string;
};
}
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace SocketMessage {
export type NewBlock = SocketMessageParamsGeneric<'new_block', NewBlockSocketResponse>;
......@@ -22,5 +34,7 @@ export namespace SocketMessage {
export type TxStatusUpdate = SocketMessageParamsGeneric<'collated', NewBlockSocketResponse>;
export type NewTx = SocketMessageParamsGeneric<'transaction', { transaction: number }>;
export type NewPendingTx = SocketMessageParamsGeneric<'pending_transaction', { pending_transaction: number }>;
export type AddressTokenBalance = SocketMessageParamsGeneric<'token_balance', { block_number: number }>;
export type AddressCoinBalance = SocketMessageParamsGeneric<'coin_balance', AddressCoinBalancePayload>;
export type Unknown = SocketMessageParamsGeneric<undefined, unknown>;
}
......@@ -5,6 +5,8 @@ import notEmpty from 'lib/notEmpty';
import { useSocket } from './context';
const CHANNEL_REGISTRY: Record<string, Channel> = {};
interface Params {
topic: string | undefined;
params?: object;
......@@ -51,12 +53,21 @@ export default function useSocketChannel({ topic, params, isDisabled, onJoin, on
return;
}
const ch = socket.channel(topic, params);
ch.join().receive('ok', (message) => onJoinRef.current?.(ch, message));
let ch: Channel;
if (CHANNEL_REGISTRY[topic]) {
ch = CHANNEL_REGISTRY[topic];
onJoinRef.current?.(ch, '');
} else {
ch = socket.channel(topic);
CHANNEL_REGISTRY[topic] = ch;
ch.join().receive('ok', (message) => onJoinRef.current?.(ch, message));
}
setChannel(ch);
return () => {
ch.leave();
delete CHANNEL_REGISTRY[topic];
setChannel(undefined);
};
}, [ socket, topic, params, isDisabled ]);
......
......@@ -21,7 +21,7 @@ export function middleware(req: NextRequest) {
const apiToken = req.cookies.get(NAMES.API_TOKEN);
if ((isAccountRoute || isProfileRoute) && !apiToken && appConfig.isAccountSupported) {
const authUrl = link('auth');
const authUrl = link('auth', undefined, { path: req.nextUrl.pathname });
return NextResponse.redirect(authUrl);
}
......
import type { AddressTokenBalance } from 'types/api/address';
export const erc20a: AddressTokenBalance = {
token: {
address: '0xb2a90505dc6680a7a695f7975d0d32EeF610f456',
decimals: '18',
exchange_rate: null,
holders: '23',
name: 'hyfi.token',
symbol: 'HyFi',
total_supply: '369000000000000000000000000',
type: 'ERC-20',
},
token_id: null,
value: '1169320000000000000000000',
};
export const erc20b: AddressTokenBalance = {
token: {
address: '0xc1116c98ba622a6218433fF90a2E40DEa482d7A7',
decimals: '6',
exchange_rate: '0.982',
holders: '17',
name: 'USD Coin',
symbol: 'USDC',
total_supply: '900000000000000000000000000',
type: 'ERC-20',
},
token_id: null,
value: '872500000000',
};
export const erc20c: AddressTokenBalance = {
token: {
address: '0xc1116c98ba622a6218433fF90a2E40DEa482d7A7',
decimals: '18',
exchange_rate: '1328.89',
holders: '17',
name: 'Ethereum',
symbol: 'ETH',
total_supply: '1000000000000000000000000',
type: 'ERC-20',
},
token_id: null,
value: '9852000000000000000000',
};
export const erc20d: AddressTokenBalance = {
token: {
address: '0xCc7bb2D219A0FC08033E130629C2B854b7bA9195',
decimals: '18',
exchange_rate: null,
holders: '102625',
name: 'Zeta',
symbol: 'ZETA',
total_supply: '2100000000000000000000000000',
type: 'ERC-20',
},
token_id: null,
value: '39000000000000000000',
};
export const erc721a: AddressTokenBalance = {
token: {
address: '0xDe7cAc71E072FCBd4453E5FB3558C2684d1F88A0',
decimals: null,
exchange_rate: null,
holders: '7',
name: 'HyFi Athena',
symbol: 'HYFI_ATHENA',
total_supply: '105',
type: 'ERC-721',
},
token_id: null,
value: '51',
};
export const erc721b: AddressTokenBalance = {
token: {
address: '0xA8d5C7beEA8C9bB57f5fBa35fB638BF45550b11F',
decimals: null,
exchange_rate: null,
holders: '2',
name: 'World Of Women Galaxy',
symbol: 'WOWG',
total_supply: null,
type: 'ERC-721',
},
token_id: null,
value: '1',
};
export const erc721c: AddressTokenBalance = {
token: {
address: '0x47646F1d7dc4Dd2Db5a41D092e2Cf966e27A4992',
decimals: null,
exchange_rate: null,
holders: '12',
name: 'Puma',
symbol: 'PUMA',
total_supply: null,
type: 'ERC-721',
},
token_id: null,
value: '5',
};
export const erc1155a: AddressTokenBalance = {
token: {
address: '0x4b333DEd10c7ca855EA2C8D4D90A0a8b73788c8e',
decimals: null,
exchange_rate: null,
holders: '22',
name: 'HyFi Membership',
symbol: 'HYFI_MEMBERSHIP',
total_supply: '482',
type: 'ERC-1155',
},
token_id: '42',
value: '24',
};
export const erc1155b: AddressTokenBalance = {
token: {
address: '0xf4b71b179132ad457f6bcae2a55efa9e4b26eefc',
decimals: null,
exchange_rate: null,
holders: '100',
name: 'WinkyVerse Collections',
symbol: 'WVC',
total_supply: '4943',
type: 'ERC-1155',
},
token_id: '100010000000001',
value: '11',
};
export const erc1155withoutName: AddressTokenBalance = {
token: {
address: '0x4b333DEd10c7ca855EA2C8D4D90A0a8b73788c8e',
decimals: null,
exchange_rate: null,
holders: '22',
name: null,
symbol: null,
total_supply: '482',
type: 'ERC-1155',
},
token_id: '64532245',
value: '42',
};
export const baseList = [
erc20a,
erc20b,
erc20c,
erc721a,
erc721b,
erc721c,
erc1155withoutName,
erc1155a,
erc1155b,
];
......@@ -29,6 +29,7 @@ export const erc20: TokenTransfer = {
name: 'ARIANEE',
symbol: 'ARIA',
type: 'ERC-20',
total_supply: '0',
},
total: {
decimals: '18',
......@@ -67,6 +68,7 @@ export const erc721: TokenTransfer = {
name: 'Arianee Smart-Asset',
symbol: 'AriaSA',
type: 'ERC-721',
total_supply: '0',
},
total: {
token_id: '875879856',
......@@ -104,6 +106,7 @@ export const erc1155: TokenTransfer = {
name: null,
symbol: null,
type: 'ERC-1155',
total_supply: '0',
},
total: {
token_id: '123',
......
......@@ -12,7 +12,6 @@
"dev:poa_core": "./node_modules/.bin/dotenv -e ./configs/envs/.env.poa_core -e ./configs/envs/.env.common -e ./configs/envs/.env.secrets next dev | ./node_modules/.bin/pino-pretty",
"dev:goerli": "./node_modules/.bin/dotenv -e ./configs/envs/.env.goerli -e ./configs/envs/.env.common -e ./configs/envs/.env.secrets next dev | ./node_modules/.bin/pino-pretty",
"build": "next build",
"build:vercel": "./node_modules/.bin/dotenv -e ./configs/envs/.env.poa_core -e ./configs/envs/.env.common next build",
"build:docker": "docker build --build-arg GIT_COMMIT_SHA=$(git rev-parse HEAD) -t blockscout ./",
"start": "next start",
"start:docker:poa_core": "docker run -p 3000:3000 --env-file ./configs/envs/.env.common --env-file ./configs/envs/.env.poa_core --env-file ./configs/envs/.env.secrets blockscout",
......@@ -28,13 +27,13 @@
"test:jest:watch": "jest --watch"
},
"dependencies": {
"@chakra-ui/react": "2.3.1",
"@chakra-ui/theme-tools": "^2.0.2",
"@emotion/react": "^11",
"@emotion/styled": "^11",
"@chakra-ui/react": "2.4.3",
"@chakra-ui/theme-tools": "^2.0.14",
"@emotion/react": "^11.10.4",
"@emotion/styled": "^11.10.4",
"@sentry/nextjs": "^7.12.1",
"@sentry/react": "^7.13.0",
"@sentry/tracing": "^7.13.0",
"@sentry/react": "^7.24.0",
"@sentry/tracing": "^7.24.0",
"@tanstack/react-query": "^4.0.10",
"@tanstack/react-query-devtools": "^4.0.10",
"@types/react-scroll": "^1.8.4",
......@@ -42,7 +41,7 @@
"d3": "^7.6.1",
"dayjs": "^1.11.5",
"ethers": "^5.7.1",
"framer-motion": "^6",
"framer-motion": "^6.5.1",
"lodash": "^4.0.0",
"next": "12.2.5",
"node-fetch": "^3.2.9",
......@@ -50,8 +49,8 @@
"pino-http": "^8.2.1",
"pino-pretty": "^9.1.1",
"qrcode": "^1.5.1",
"react": "18.1.0",
"react-dom": "18.1.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-hook-form": "^7.33.1",
"react-identicons": "^1.2.5",
"react-jazzicon": "^1.0.4",
......
import type { NextApiRequest } from 'next';
import appConfig from 'configs/app/config';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
const { name, from, to } = req.query;
return `/v1/charts/line?name=${ name }${ from ? `&from=${ from }&to=${ to }` : '' }`;
};
const requestHandler = handler(getUrl, [ 'GET' ], appConfig.statsApi.endpoint);
export default requestHandler;
import appConfig from 'configs/app/config';
import handler from 'lib/api/handler';
const getUrl = () => '/v1/counters';
const requestHandler = handler(getUrl, [ 'GET' ], appConfig.statsApi.endpoint);
export default requestHandler;
......@@ -2,17 +2,17 @@ import type { NextPage } from 'next';
import Head from 'next/head';
import React from 'react';
import Vercel from 'ui/pages/Vercel';
import Login from 'ui/pages/Login';
const VercelPage: NextPage = () => {
const LoginPage: NextPage = () => {
return (
<>
<Head><title>Vercel Page</title></Head>
<Vercel/>
<Head><title>Login Page</title></Head>
<Login/>
</>
);
};
export default VercelPage;
export default LoginPage;
export { getServerSideProps } from 'lib/next/getServerSideProps';
......@@ -2,6 +2,7 @@ import type { TestFixture, Page } from '@playwright/test';
import type { WebSocket } from 'ws';
import { WebSocketServer } from 'ws';
import type { AddressCoinBalancePayload } from 'lib/socket/types';
import type { NewBlockSocketResponse } from 'types/api/block';
type ReturnType = () => Promise<WebSocket>;
......@@ -53,6 +54,8 @@ export const joinChannel = async(socket: WebSocket, channelName: string) => {
});
};
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'coin_balance', payload: AddressCoinBalancePayload): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'token_balance', payload: { block_number: number }): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'transaction', payload: { transaction: number }): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'pending_transaction', payload: { pending_transaction: number }): void;
export function sendMessage(socket: WebSocket, channel: Channel, msg: 'new_block', payload: NewBlockSocketResponse): void;
......
export default async function insertAdText() {
const ad = document.getElementsByClassName('coinzilla');
ad[0].textContent = 'coinzilla banner!';
}
......@@ -77,11 +77,17 @@ const variantOutline = defineStyle((props) => {
bg: props.isActive ? activeBg : 'transparent',
borderColor: props.isActive ? activeBg : 'blue.400',
color: props.isActive ? activeColor : 'blue.400',
p: {
color: 'blue.400',
},
},
_disabled: {
color,
borderColor,
},
p: {
color: 'blue.400',
},
},
_disabled: {
opacity: 0.2,
......@@ -94,6 +100,9 @@ const variantOutline = defineStyle((props) => {
color,
borderColor,
},
p: {
color: activeColor,
},
},
};
});
......
import { formAnatomy as parts } from '@chakra-ui/anatomy';
import { createMultiStyleConfigHelpers } from '@chakra-ui/styled-system';
import type { StyleFunctionProps } from '@chakra-ui/theme-tools';
import { getColor, mode } from '@chakra-ui/theme-tools';
import { getColor } from '@chakra-ui/theme-tools';
import getDefaultFormColors from '../utils/getDefaultFormColors';
import FormLabel from './FormLabel';
......@@ -13,7 +13,7 @@ const { definePartsStyle, defineMultiStyleConfig } =
function getFloatingVariantStylesForSize(size: 'md' | 'lg', props: StyleFunctionProps) {
const { theme } = props;
const { focusColor: fc, errorColor: ec } = getDefaultFormColors(props);
const { focusPlaceholderColor, errorColor } = getDefaultFormColors(props);
const activeLabelStyles = {
...FormLabel.variants?.floating?.(props)._focusWithin,
......@@ -62,7 +62,7 @@ function getFloatingVariantStylesForSize(size: 'md' | 'lg', props: StyleFunction
label: FormLabel.sizes?.[size](props) || {},
'input:not(:placeholder-shown) + label, textarea:not(:placeholder-shown) + label': activeLabelStyles,
'input[aria-invalid=true] + label, textarea[aria-invalid=true] + label': {
color: getColor(theme, ec),
color: getColor(theme, errorColor),
},
// input styles
......@@ -78,20 +78,20 @@ function getFloatingVariantStylesForSize(size: 'md' | 'lg', props: StyleFunction
// indicator styles
'input:not(:placeholder-shown) + label .chakra-form__required-indicator, textarea:not(:placeholder-shown) + label .chakra-form__required-indicator': {
color: getColor(theme, fc),
color: getColor(theme, focusPlaceholderColor),
},
'input[aria-invalid=true] + label .chakra-form__required-indicator, textarea[aria-invalid=true] + label .chakra-form__required-indicator': {
color: getColor(theme, ec),
color: getColor(theme, errorColor),
},
},
};
}
const baseStyle = definePartsStyle((props) => {
const baseStyle = definePartsStyle(() => {
return {
requiredIndicator: {
marginStart: 0,
color: mode('gray.500', 'whiteAlpha.400')(props),
color: 'gray.500',
},
};
});
......
......@@ -19,7 +19,7 @@ const baseStyle = defineStyle({
const variantFloating = defineStyle((props) => {
const { theme, backgroundColor } = props;
const { focusColor: fc } = getDefaultFormColors(props);
const { focusPlaceholderColor } = getDefaultFormColors(props);
const bc = backgroundColor || mode('white', 'black')(props);
return {
......@@ -40,7 +40,7 @@ const variantFloating = defineStyle((props) => {
textOverflow: 'ellipsis',
_focusWithin: {
backgroundColor: bc,
color: getColor(theme, fc),
color: getColor(theme, focusPlaceholderColor),
fontSize: 'xs',
lineHeight: '16px',
borderTopRightRadius: 'none',
......
......@@ -22,7 +22,7 @@ const baseStyleDialog = defineStyle((props) => {
const baseStyleDialogContainer = defineStyle({
'::-webkit-scrollbar': { display: 'none' },
'scrollbar-width': 'none',
'@supports (height: -webkit-fill-available)': { height: '100vh' },
'@supports (height: -webkit-fill-available)': { height: '-webkit-fill-available' },
});
const baseStyleHeader = defineStyle((props) => ({
......@@ -63,6 +63,7 @@ const baseStyleOverlay = defineStyle({
const baseStyle = definePartsStyle((props) => ({
dialog: runIfFn(baseStyleDialog, props),
dialogContainer: baseStyleDialogContainer,
header: runIfFn(baseStyleHeader, props),
body: baseStyleBody,
footer: baseStyleFooter,
......
......@@ -26,6 +26,10 @@ const baseStyleContent = defineStyle((props) => {
bg: $popperBg.reference,
[$arrowBg.variable]: $popperBg.reference,
[$arrowShadowColor.variable]: `colors.${ shadowColor }`,
_dark: {
[$popperBg.variable]: `colors.gray.900`,
[$arrowShadowColor.variable]: `colors.whiteAlpha.300`,
},
width: 'xs',
border: 'none',
borderColor: 'inherit',
......
......@@ -3,6 +3,7 @@ const zIndices = {
auto: 'auto',
base: 0,
docked: 10,
tooltip: 900,
dropdown: 1000,
sticky: 1100,
sticky1: 1101,
......@@ -13,7 +14,6 @@ const zIndices = {
popover: 1500,
skipLink: 1600,
toast: 1700,
tooltip: 1800,
};
export default zIndices;
......@@ -4,7 +4,8 @@ import { mode } from '@chakra-ui/theme-tools';
export default function getDefaultFormColors(props: StyleFunctionProps) {
const { focusBorderColor: fc, errorBorderColor: ec, filledBorderColor: flc } = props;
return {
focusColor: fc || mode('brand.700', 'brand.300')(props),
focusBorderColor: fc || mode('blue.500', 'blue.300')(props),
focusPlaceholderColor: fc || 'gray.500',
errorColor: ec || mode('red.400', 'red.300')(props),
filledColor: flc || mode('gray.300', 'gray.600')(props),
};
......
......@@ -6,7 +6,7 @@ import getDefaultTransitionProps from './getDefaultTransitionProps';
export default function getOutlinedFieldStyles(props: StyleFunctionProps) {
const { theme, borderColor } = props;
const { focusColor: fc, errorColor: ec } = getDefaultFormColors(props);
const { focusBorderColor, errorColor } = getDefaultFormColors(props);
const transitionProps = getDefaultTransitionProps();
return {
......@@ -32,12 +32,12 @@ export default function getOutlinedFieldStyles(props: StyleFunctionProps) {
},
},
_invalid: {
borderColor: getColor(theme, ec),
borderColor: getColor(theme, errorColor),
boxShadow: `none`,
},
_focusVisible: {
zIndex: 1,
borderColor: getColor(theme, fc),
borderColor: getColor(theme, focusBorderColor),
boxShadow: 'md',
},
_placeholder: {
......
......@@ -20,10 +20,10 @@ export interface Address {
}
export interface AddressCounters {
transaction_count: string;
token_transfer_count: string;
transactions_count: string;
token_transfers_count: string;
gas_usage_count: string;
validation_count: string | null;
validations_count: string | null;
}
export interface AddressTokenBalance {
......
......@@ -7,12 +7,25 @@ export type HomeStats = {
total_gas_used: string;
transactions_today: string;
gas_used_today: string;
gas_prices: {average: number; fast: number; slow: number};
gas_prices: GasPrices;
static_gas_price: string;
market_cap: string;
network_utilization_percentage: number;
}
export type GasPrices = {
average: number;
fast: number;
slow: number;
}
export type Stats = {
total_blocks: string;
totalBlocksAllTime: string;
}
export type Charts = {
'chart': Array<{
date: string;
value: string;
}>;
}
......@@ -8,6 +8,7 @@ export interface TokenInfo {
decimals: string | null;
holders: string | null;
exchange_rate: string | null;
total_supply: string | null;
}
export type TokenInfoGeneric<Type extends TokenType> = Omit<TokenInfo, 'type'> & { type: Type };
......@@ -5,6 +5,7 @@ export enum QueryKeys {
txsPending = 'txs-pending',
homeStats='homeStats',
stats='stats',
charts='stats',
tx = 'tx',
txInternals = 'tx-internals',
txLogs = 'tx-logs',
......
......@@ -23,10 +23,4 @@ export type StatsChart = {
id: string;
title: string;
description: string;
apiMethodURL: string;
}
export interface ModalChart {
id: string;
title: string;
}
import { Box, Flex, Text, Icon, Button, Grid, Select } from '@chakra-ui/react';
import { Box, Flex, Text, Icon, Button, Grid } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import { useQuery } from '@tanstack/react-query';
import BigNumber from 'bignumber.js';
......@@ -11,16 +11,18 @@ import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config';
import metamaskIcon from 'icons/metamask.svg';
import starOutlineIcon from 'icons/star_outline.svg';
import walletIcon from 'icons/wallet.svg';
import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile';
import AddressIcon from 'ui/shared/address/AddressIcon';
import CopyToClipboard from 'ui/shared/CopyToClipboard';
import DataFetchAlert from 'ui/shared/DataFetchAlert';
import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import ExternalLink from 'ui/shared/ExternalLink';
import HashStringShorten from 'ui/shared/HashStringShorten';
import AddressDetailsSkeleton from './details/AddressDetailsSkeleton';
import AddressQrCode from './details/AddressQrCode';
import TokenSelect from './tokenSelect/TokenSelect';
interface Props {
addressQuery: UseQueryResult<TAddress>;
......@@ -48,14 +50,15 @@ const AddressDetails = ({ addressQuery }: Props) => {
);
if (countersQuery.isLoading || addressQuery.isLoading || tokenBalancesQuery.isLoading) {
return <Box>loading</Box>;
return <AddressDetailsSkeleton/>;
}
if (countersQuery.isError || addressQuery.isError || tokenBalancesQuery.isError) {
return <Box>error</Box>;
return <DataFetchAlert/>;
}
const explorers = appConfig.network.explorers.filter(({ paths }) => paths.address);
const validationsCount = Number(countersQuery.data.validations_count);
return (
<Box>
......@@ -73,7 +76,7 @@ const AddressDetails = ({ addressQuery }: Props) => {
</Flex>
{ explorers.length > 0 && (
<Flex mt={ 8 } columnGap={ 4 } flexWrap="wrap">
<Text>Verify with other explorers</Text>
<Text fontSize="sm">Verify with other explorers</Text>
{ explorers.map((explorer) => {
const url = new URL(explorer.paths.tx + '/' + router.query.id, explorer.baseUrl);
return <ExternalLink key={ explorer.baseUrl } title={ explorer.title } href={ url.toString() }/>;
......@@ -90,39 +93,21 @@ const AddressDetails = ({ addressQuery }: Props) => {
title="Tokens"
hint="All tokens in the account and total value."
alignSelf="center"
py="2px"
>
{ tokenBalancesQuery.data.length > 0 ? (
<>
{ /* TODO will be fixed later when we implement select with custom menu */ }
<Select
size="sm"
borderRadius="base"
focusBorderColor="none"
display="inline-block"
w="auto"
>
{ tokenBalancesQuery.data.map((token) =>
<option key={ token.token.address } value={ token.token.address }>{ token.token.symbol }</option>) }
</Select>
<Button variant="outline" size="sm" ml={ 3 }>
<Icon as={ walletIcon } boxSize={ 5 }/>
</Button>
</>
) : (
'-'
) }
<TokenSelect/>
</DetailsInfoItem>
<DetailsInfoItem
title="Transactions"
hint="Number of transactions related to this address."
>
{ Number(countersQuery.data.transaction_count).toLocaleString() }
{ Number(countersQuery.data.transactions_count).toLocaleString() }
</DetailsInfoItem>
<DetailsInfoItem
title="Transfers"
hint="Number of transfers to/from this address."
>
{ Number(countersQuery.data.token_transfer_count).toLocaleString() }
{ Number(countersQuery.data.token_transfers_count).toLocaleString() }
</DetailsInfoItem>
<DetailsInfoItem
title="Gas used"
......@@ -130,12 +115,12 @@ const AddressDetails = ({ addressQuery }: Props) => {
>
{ BigNumber(countersQuery.data.gas_usage_count).toFormat() }
</DetailsInfoItem>
{ countersQuery.data.validation_count && (
{ !Object.is(validationsCount, NaN) && validationsCount > 0 && (
<DetailsInfoItem
title="Blocks validated"
hint="Number of blocks validated by this validator."
>
{ Number(countersQuery.data.validation_count).toLocaleString() }
{ validationsCount.toLocaleString() }
</DetailsInfoItem>
) }
</Grid>
......
import { Box, Flex, Grid, Skeleton, SkeletonCircle } from '@chakra-ui/react';
import React from 'react';
import DetailsSkeletonRow from 'ui/shared/skeletons/DetailsSkeletonRow';
const AddressDetailsSkeleton = () => {
return (
<Box>
<Flex align="center">
<SkeletonCircle boxSize={ 6 } flexShrink={ 0 }/>
<Skeleton h={ 6 } w="420px" ml={ 2 }/>
<Skeleton h={ 6 } w="24px" ml={ 2 } flexShrink={ 0 }/>
<Skeleton h={ 8 } w="48px" ml={ 3 } flexShrink={ 0 }/>
<Skeleton h={ 8 } w="48px" ml={ 3 } flexShrink={ 0 }/>
</Flex>
<Flex align="center" columnGap={ 4 } mt={ 8 }>
<Skeleton h={ 6 } w="200px"/>
<Skeleton h={ 6 } w="80px"/>
</Flex>
<Grid columnGap={ 8 } rowGap={{ base: 5, lg: 7 }} templateColumns={{ base: '1fr', lg: '150px 1fr' }} maxW="1000px" mt={ 8 }>
<DetailsSkeletonRow w="30%"/>
<DetailsSkeletonRow w="10%"/>
<DetailsSkeletonRow w="10%"/>
<DetailsSkeletonRow w="20%"/>
</Grid>
</Box>
);
};
export default AddressDetailsSkeleton;
import { useQuery } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import { QueryKeys } from 'types/client/queries';
import useFetch from 'lib/hooks/useFetch';
const MockAddressPage = ({ children }: { children: JSX.Element }): JSX.Element => {
const router = useRouter();
const fetch = useFetch();
const { data } = useQuery(
[ QueryKeys.address, router.query.id ],
async() => await fetch(`/node-api/addresses/${ router.query.id }`),
{
enabled: Boolean(router.query.id),
},
);
if (!data) {
return <div/>;
}
return children;
};
export default MockAddressPage;
import { Flex } from '@chakra-ui/react';
import { test as base, expect, devices } from '@playwright/experimental-ct-react';
import React from 'react';
import * as tokenBalanceMock from 'mocks/address/tokenBalance';
import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp';
import MockAddressPage from 'ui/address/testUtils/MockAddressPage';
import TokenSelect from './TokenSelect';
const ASSET_URL = 'https://raw.githubusercontent.com/trustwallet/assets/master/blockchains/poa/assets/0xb2a90505dc6680a7a695f7975d0d32EeF610f456/logo.png';
const TOKENS_API_URL = '/node-api/addresses/1/token-balances';
const ADDRESS_API_URL = '/node-api/addresses/1';
const hooksConfig = {
router: {
query: { id: '1' },
},
};
const CLIPPING_AREA = { x: 0, y: 0, width: 360, height: 500 };
const test = base.extend({
page: async({ page }, use) => {
await page.route(ASSET_URL, (route) => {
return route.fulfill({
status: 200,
path: './playwright/image_s.jpg',
});
});
await page.route(ADDRESS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ hash: '1' }),
}), { times: 1 });
await page.route(TOKENS_API_URL, async(route) => route.fulfill({
status: 200,
body: JSON.stringify(tokenBalanceMock.baseList),
}), { times: 1 });
use(page);
},
});
test('base view +@dark-mode', async({ mount, page }) => {
await mount(
<TestApp>
<MockAddressPage>
<Flex>
<TokenSelect/>
</Flex>
</MockAddressPage>
</TestApp>,
{ hooksConfig },
);
await page.getByRole('button', { name: /select/i }).click();
await page.getByText('USD Coin').hover();
await expect(page).toHaveScreenshot({ clip: CLIPPING_AREA });
await page.mouse.move(100, 200);
await page.mouse.wheel(0, 1000);
await expect(page).toHaveScreenshot({ clip: CLIPPING_AREA });
});
test.describe('mobile', () => {
test.use({ viewport: devices['iPhone 13 Pro'].viewport });
test('base view', async({ mount, page }) => {
await mount(
<TestApp>
<MockAddressPage>
<Flex>
<TokenSelect/>
</Flex>
</MockAddressPage>
</TestApp>,
{ hooksConfig },
);
await page.getByRole('button', { name: /select/i }).click();
await page.getByText('USD Coin').hover();
await expect(page).toHaveScreenshot();
});
});
test('sort', async({ mount, page }) => {
await mount(
<TestApp>
<MockAddressPage>
<Flex>
<TokenSelect/>
</Flex>
</MockAddressPage>
</TestApp>,
{ hooksConfig },
);
await page.getByRole('button', { name: /select/i }).click();
await page.locator('a[aria-label="Sort ERC-20 tokens"]').click();
await expect(page).toHaveScreenshot({ clip: CLIPPING_AREA });
await page.mouse.move(100, 200);
await page.mouse.wheel(0, 1000);
await page.locator('a[aria-label="Sort ERC-1155 tokens"]').click();
await expect(page).toHaveScreenshot({ clip: CLIPPING_AREA });
});
test('filter', async({ mount, page }) => {
await mount(
<TestApp>
<MockAddressPage>
<Flex>
<TokenSelect/>
</Flex>
</MockAddressPage>
</TestApp>,
{ hooksConfig },
);
await page.getByRole('button', { name: /select/i }).click();
await page.getByPlaceholder('Search by token name').type('c');
await expect(page).toHaveScreenshot({ clip: CLIPPING_AREA });
});
test.describe('socket', () => {
const testWithSocket = test.extend<socketServer.SocketServerFixture>({
createSocket: socketServer.createSocket,
});
testWithSocket.describe.configure({ mode: 'serial' });
testWithSocket('new item after token balance update', async({ page, mount, createSocket }) => {
await mount(
<TestApp withSocket>
<MockAddressPage>
<Flex>
<TokenSelect/>
</Flex>
</MockAddressPage>
</TestApp>,
{ hooksConfig },
);
await page.route(TOKENS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify([
...tokenBalanceMock.baseList,
tokenBalanceMock.erc20d,
]),
}));
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'addresses:1');
socketServer.sendMessage(socket, channel, 'token_balance', {
block_number: 1,
});
const button = page.getByRole('button', { name: /select/i });
const text = await button.innerText();
expect(text).toContain('10');
});
testWithSocket('new item after coin balance update', async({ page, mount, createSocket }) => {
await mount(
<TestApp withSocket>
<MockAddressPage>
<Flex>
<TokenSelect/>
</Flex>
</MockAddressPage>
</TestApp>,
{ hooksConfig },
);
await page.route(TOKENS_API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify([
...tokenBalanceMock.baseList,
tokenBalanceMock.erc20d,
]),
}));
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'addresses:1');
socketServer.sendMessage(socket, channel, 'coin_balance', {
coin_balance: {
block_number: 1,
},
});
const button = page.getByRole('button', { name: /select/i });
const text = await button.innerText();
expect(text).toContain('10');
});
});
import { Box, Icon, IconButton, Skeleton, Tooltip } from '@chakra-ui/react';
import { useQuery, useQueryClient, useIsFetching } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { Address, AddressTokenBalance } from 'types/api/address';
import { QueryKeys } from 'types/client/queries';
import walletIcon from 'icons/wallet.svg';
import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import TokenSelectDesktop from './TokenSelectDesktop';
import TokenSelectMobile from './TokenSelectMobile';
const TokenSelect = () => {
const router = useRouter();
const fetch = useFetch();
const isMobile = useIsMobile();
const queryClient = useQueryClient();
const [ blockNumber, setBlockNumber ] = React.useState<number>();
const addressQueryData = queryClient.getQueryData<Address>([ QueryKeys.address, router.query.id ]);
const { data, isError, isLoading, refetch } = useQuery<unknown, unknown, Array<AddressTokenBalance>>(
[ QueryKeys.addressTokenBalances, addressQueryData?.hash ],
async() => await fetch(`/node-api/addresses/${ addressQueryData?.hash }/token-balances`),
{
enabled: Boolean(addressQueryData),
},
);
const balancesIsFetching = useIsFetching({ queryKey: [ QueryKeys.addressTokenBalances, addressQueryData?.hash ] });
const handleTokenBalanceMessage: SocketMessage.AddressTokenBalance['handler'] = React.useCallback((payload) => {
if (payload.block_number !== blockNumber) {
refetch();
setBlockNumber(payload.block_number);
}
}, [ blockNumber, refetch ]);
const handleCoinBalanceMessage: SocketMessage.AddressCoinBalance['handler'] = React.useCallback((payload) => {
if (payload.coin_balance.block_number !== blockNumber) {
refetch();
setBlockNumber(payload.coin_balance.block_number);
}
}, [ blockNumber, refetch ]);
const channel = useSocketChannel({
topic: `addresses:${ addressQueryData?.hash.toLowerCase() }`,
isDisabled: !addressQueryData,
});
useSocketMessage({
channel,
event: 'coin_balance',
handler: handleCoinBalanceMessage,
});
useSocketMessage({
channel,
event: 'token_balance',
handler: handleTokenBalanceMessage,
});
if (isLoading) {
return <Skeleton h={ 8 } w="160px"/>;
}
if (isError || data.length === 0) {
return <Box py="6px">0</Box>;
}
return (
<>
{ isMobile ?
<TokenSelectMobile data={ data } isLoading={ balancesIsFetching === 1 }/> :
<TokenSelectDesktop data={ data } isLoading={ balancesIsFetching === 1 }/>
}
<Tooltip label="Show all tokens">
<IconButton
aria-label="Show all tokens"
variant="outline"
size="sm"
pl="6px"
pr="6px"
ml={ 3 }
icon={ <Icon as={ walletIcon } boxSize={ 5 }/> }
/>
</Tooltip>
</>
);
};
export default React.memo(TokenSelect);
import { Box, Button, Icon, Skeleton, Text, useColorModeValue } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import arrowIcon from 'icons/arrows/east-mini.svg';
import tokensIcon from 'icons/tokens.svg';
import { ZERO } from 'lib/consts';
import type { EnhancedData } from './utils';
interface Props {
isOpen: boolean;
isLoading: boolean;
onClick: () => void;
data: Array<EnhancedData>;
}
const TokenSelectButton = ({ isOpen, isLoading, onClick, data }: Props, ref: React.ForwardedRef<HTMLButtonElement>) => {
const totalBn = data.reduce((result, item) => !item.usd ? result : result.plus(BigNumber(item.usd)), ZERO);
const skeletonBgColor = useColorModeValue('white', 'black');
const handleClick = React.useCallback(() => {
if (isLoading && !isOpen) {
return;
}
onClick();
}, [ isLoading, isOpen, onClick ]);
return (
<Box position="relative">
<Button
ref={ ref }
size="sm"
variant="outline"
colorScheme="gray"
onClick={ handleClick }
aria-label="Token select"
>
<Icon as={ tokensIcon } boxSize={ 4 } mr={ 2 }/>
<Text fontWeight={ 600 }>{ data.length }</Text>
<Text whiteSpace="pre" variant="secondary" fontWeight={ 400 }> (${ totalBn.toFormat(2) })</Text>
<Icon as={ arrowIcon } transform={ isOpen ? 'rotate(90deg)' : 'rotate(-90deg)' } transitionDuration="faster" boxSize={ 5 } ml={ 3 }/>
</Button>
{ isLoading && !isOpen && <Skeleton h="100%" w="100%" position="absolute" top={ 0 } left={ 0 } bgColor={ skeletonBgColor }/> }
</Box>
);
};
export default React.forwardRef(TokenSelectButton);
import { useColorModeValue, Popover, PopoverTrigger, PopoverContent, PopoverBody, useDisclosure } from '@chakra-ui/react';
import React from 'react';
import type { AddressTokenBalance } from 'types/api/address';
import TokenSelectButton from './TokenSelectButton';
import TokenSelectMenu from './TokenSelectMenu';
import useTokenSelect from './useTokenSelect';
interface Props {
data: Array<AddressTokenBalance>;
isLoading: boolean;
}
const TokenSelectDesktop = ({ data, isLoading }: Props) => {
const { isOpen, onToggle, onClose } = useDisclosure();
const bgColor = useColorModeValue('white', 'gray.900');
const result = useTokenSelect(data);
return (
<Popover isOpen={ isOpen } onClose={ onClose } placement="bottom-start" isLazy>
<PopoverTrigger>
<TokenSelectButton isOpen={ isOpen } onClick={ onToggle } data={ result.modifiedData } isLoading={ isLoading }/>
</PopoverTrigger>
<PopoverContent w="355px" maxH="450px" overflowY="scroll">
<PopoverBody px={ 4 } py={ 6 } bgColor={ bgColor } boxShadow="2xl" >
<TokenSelectMenu { ...result }/>
</PopoverBody>
</PopoverContent>
</Popover>
);
};
export default React.memo(TokenSelectDesktop);
import { Flex, Text, useColorModeValue } from '@chakra-ui/react';
import BigNumber from 'bignumber.js';
import React from 'react';
import link from 'lib/link/link';
import HashStringShorten from 'ui/shared/HashStringShorten';
import TokenLogo from 'ui/shared/TokenLogo';
import type { EnhancedData } from './utils';
interface Props {
data: EnhancedData;
}
const TokenSelectItem = ({ data }: Props) => {
const secondRow = (() => {
switch (data.token.type) {
case 'ERC-20': {
const tokenDecimals = Number(data.token.decimals) || 18;
return (
<>
<Text >{ BigNumber(data.value).dividedBy(10 ** tokenDecimals).toFormat(2) } { data.token.symbol }</Text>
{ data.token.exchange_rate && <Text >@{ data.token.exchange_rate }</Text> }
</>
);
}
case 'ERC-721': {
return <Text >{ BigNumber(data.value).toFormat() } { data.token.symbol }</Text>;
}
case 'ERC-1155': {
return (
<>
<Text >#{ data.token_id || 0 }</Text>
<Text >{ BigNumber(data.value).toFormat() }</Text>
</>
);
}
}
})();
// TODO add filter param when token page is ready
const url = link('token_index', { hash: data.token.address });
return (
<Flex
px={ 1 }
py="10px"
display="flex"
flexDir="column"
rowGap={ 2 }
borderColor={ useColorModeValue('blackAlpha.200', 'whiteAlpha.200') }
borderBottomWidth="1px"
_hover={{
bgColor: useColorModeValue('blue.50', 'gray.800'),
}}
fontSize="sm"
cursor="pointer"
as="a"
href={ url }
>
<Flex alignItems="center" w="100%">
<TokenLogo hash={ data.token.address } name={ data.token.name } boxSize={ 6 }/>
<Text fontWeight={ 700 } ml={ 2 }>{ data.token.name || <HashStringShorten hash={ data.token.address }/> }</Text>
{ data.usd && <Text fontWeight={ 700 } ml="auto">${ data.usd.toFormat(2) }</Text> }
</Flex>
<Flex alignItems="center" justifyContent="space-between" w="100%">
{ secondRow }
</Flex>
</Flex>
);
};
export default React.memo(TokenSelectItem);
import { Icon, Text, Box, Input, InputGroup, InputLeftElement, useColorModeValue, Flex, Link } from '@chakra-ui/react';
import type { Dictionary } from 'lodash';
import type { ChangeEvent } from 'react';
import React from 'react';
import type { TokenType } from 'types/api/tokenInfo';
import arrowIcon from 'icons/arrows/east.svg';
import searchIcon from 'icons/search.svg';
import TokenSelectItem from './TokenSelectItem';
import type { Sort, EnhancedData } from './utils';
import { sortTokenGroups, sortingFns } from './utils';
interface Props {
searchTerm: string;
erc20sort: Sort;
erc1155sort: Sort;
modifiedData: Array<EnhancedData>;
groupedData: Dictionary<Array<EnhancedData>>;
onInputChange: (event: ChangeEvent<HTMLInputElement>) => void;
onSortClick: (event: React.SyntheticEvent) => void;
}
const TokenSelectMenu = ({ erc20sort, erc1155sort, modifiedData, groupedData, onInputChange, onSortClick, searchTerm }: Props) => {
const searchIconColor = useColorModeValue('blackAlpha.600', 'whiteAlpha.600');
const inputBorderColor = useColorModeValue('blackAlpha.100', 'whiteAlpha.200');
return (
<>
<InputGroup size="xs" mb={ 5 }>
<InputLeftElement >
<Icon as={ searchIcon } boxSize={ 4 } color={ searchIconColor }/>
</InputLeftElement>
<Input
paddingInlineStart="38px"
placeholder="Search by token name"
ml="1px"
onChange={ onInputChange }
borderColor={ inputBorderColor }
/>
</InputGroup>
<Flex flexDir="column" rowGap={ 6 }>
{ Object.entries(groupedData).sort(sortTokenGroups).map(([ tokenType, tokenInfo ]) => {
const type = tokenType as TokenType;
const arrowTransform = (type === 'ERC-1155' && erc1155sort === 'desc') || (type === 'ERC-20' && erc20sort === 'desc') ?
'rotate(90deg)' :
'rotate(-90deg)';
const sortDirection: Sort = (() => {
switch (type) {
case 'ERC-1155':
return erc1155sort;
case 'ERC-20':
return erc20sort;
default:
return 'desc';
}
})();
const hasSort = type === 'ERC-1155' || (type === 'ERC-20' && tokenInfo.some(({ usd }) => usd));
return (
<Box key={ type }>
<Flex justifyContent="space-between">
<Text mb={ 3 } color="gray.500" fontWeight={ 600 } fontSize="sm">{ type } tokens ({ tokenInfo.length })</Text>
{ hasSort && (
<Link data-type={ type } onClick={ onSortClick } aria-label={ `Sort ${ type } tokens` }>
<Icon as={ arrowIcon } boxSize={ 5 } transform={ arrowTransform } transitionDuration="faster"/>
</Link>
) }
</Flex>
{ tokenInfo.sort(sortingFns[type](sortDirection)).map((data) => <TokenSelectItem key={ data.token.address + data.token_id } data={ data }/>) }
</Box>
);
}) }
</Flex>
{ modifiedData.length === 0 && searchTerm && <Text fontSize="sm">Could not find any matches.</Text> }
</>
);
};
export default React.memo(TokenSelectMenu);
import { useDisclosure, Modal, ModalContent, ModalCloseButton } from '@chakra-ui/react';
import React from 'react';
import type { AddressTokenBalance } from 'types/api/address';
import TokenSelectButton from './TokenSelectButton';
import TokenSelectMenu from './TokenSelectMenu';
import useTokenSelect from './useTokenSelect';
interface Props {
data: Array<AddressTokenBalance>;
isLoading: boolean;
}
const TokenSelectMobile = ({ data, isLoading }: Props) => {
const { isOpen, onToggle, onClose } = useDisclosure();
const result = useTokenSelect(data);
return (
<>
<TokenSelectButton isOpen={ isOpen } onClick={ onToggle } data={ result.modifiedData } isLoading={ isLoading }/>
<Modal isOpen={ isOpen } onClose={ onClose } size="full">
<ModalContent>
<ModalCloseButton/>
<TokenSelectMenu { ...result }/>
</ModalContent>
</Modal>
</>
);
};
export default React.memo(TokenSelectMobile);
import _groupBy from 'lodash/groupBy';
import type { ChangeEvent } from 'react';
import React from 'react';
import type { AddressTokenBalance } from 'types/api/address';
import type { Sort } from './utils';
import { calculateUsdValue, filterTokens } from './utils';
export default function useData(data: Array<AddressTokenBalance>) {
const [ searchTerm, setSearchTerm ] = React.useState('');
const [ erc1155sort, setErc1155Sort ] = React.useState<Sort>('desc');
const [ erc20sort, setErc20Sort ] = React.useState<Sort>('desc');
const onInputChange = React.useCallback((event: ChangeEvent<HTMLInputElement>) => {
setSearchTerm(event.target.value);
}, []);
const onSortClick = React.useCallback((event: React.SyntheticEvent) => {
const tokenType = (event.currentTarget as HTMLAnchorElement).getAttribute('data-type');
if (tokenType === 'ERC-1155') {
setErc1155Sort((prevValue) => prevValue === 'desc' ? 'asc' : 'desc');
}
if (tokenType === 'ERC-20') {
setErc20Sort((prevValue) => prevValue === 'desc' ? 'asc' : 'desc');
}
}, []);
const modifiedData = React.useMemo(() => {
return data.filter(filterTokens(searchTerm.toLowerCase())).map(calculateUsdValue);
}, [ data, searchTerm ]);
const groupedData = React.useMemo(() => {
return _groupBy(modifiedData, 'token.type');
}, [ modifiedData ]);
return {
searchTerm,
erc20sort,
erc1155sort,
onInputChange,
onSortClick,
modifiedData,
groupedData,
};
}
import BigNumber from 'bignumber.js';
import type { AddressTokenBalance } from 'types/api/address';
import type { TokenType } from 'types/api/tokenInfo';
export type EnhancedData = AddressTokenBalance & {
usd?: BigNumber ;
}
export type Sort = 'desc' | 'asc';
const TOKEN_GROUPS_ORDER: Array<TokenType> = [ 'ERC-20', 'ERC-721', 'ERC-1155' ];
type TokenGroup = [string, Array<AddressTokenBalance>];
export const sortTokenGroups = (groupA: TokenGroup, groupB: TokenGroup) => {
return TOKEN_GROUPS_ORDER.indexOf(groupA[0] as TokenType) > TOKEN_GROUPS_ORDER.indexOf(groupB[0] as TokenType) ? 1 : -1;
};
const sortErc1155Tokens = (sort: Sort) => (dataA: AddressTokenBalance, dataB: AddressTokenBalance) => {
if (dataA.value === dataB.value) {
return 0;
}
if (sort === 'desc') {
return Number(dataA.value) > Number(dataB.value) ? -1 : 1;
}
return Number(dataA.value) > Number(dataB.value) ? 1 : -1;
};
const sortErc20Tokens = (sort: Sort) => (dataA: EnhancedData, dataB: EnhancedData) => {
if (!dataA.usd && !dataB.usd) {
return 0;
}
// keep tokens without usd value in the end of the group
if (!dataB.usd) {
return -1;
}
if (!dataA.usd) {
return 0;
}
if (sort === 'desc') {
return dataA.usd.gt(dataB.usd) ? -1 : 1;
}
return dataA.usd.gt(dataB.usd) ? 1 : -1;
};
const sortErc721Tokens = () => () => 0;
export const sortingFns = {
'ERC-20': sortErc20Tokens,
'ERC-721': sortErc721Tokens,
'ERC-1155': sortErc1155Tokens,
};
export const filterTokens = (searchTerm: string) => ({ token }: AddressTokenBalance) => {
if (!token.name) {
return !searchTerm ? true : token.address.toLowerCase().includes(searchTerm);
}
return token.name?.toLowerCase().includes(searchTerm);
};
export const calculateUsdValue = (data: AddressTokenBalance): EnhancedData => {
if (data.token.type !== 'ERC-20') {
return data;
}
const exchangeRate = data.token.exchange_rate;
if (!exchangeRate) {
return data;
}
const decimals = Number(data.token.decimals || '18');
return {
...data,
usd: BigNumber(data.value).div(BigNumber(10 ** decimals)).multipliedBy(BigNumber(exchangeRate)),
};
};
......@@ -34,7 +34,7 @@ const NAME_MAX_LENGTH = 255;
const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
const { control, handleSubmit, formState: { errors, isValid, isDirty }, setError } = useForm<Inputs>({
mode: 'all',
mode: 'onTouched',
defaultValues: {
token: data?.api_key || '',
name: data?.name || '',
......@@ -112,9 +112,7 @@ const ApiKeyForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
isInvalid={ Boolean(errors.name) }
maxLength={ NAME_MAX_LENGTH }
/>
<FormLabel>
<InputPlaceholder text="Application name for API key (e.g Web3 project)" error={ errors.name?.message }/>
</FormLabel>
<InputPlaceholder text="Application name for API key (e.g Web3 project)" error={ errors.name }/>
</FormControl>
);
}, [ errors, formBackgroundColor ]);
......
import { Grid, GridItem, Skeleton, SkeletonCircle } from '@chakra-ui/react';
import { Grid, GridItem, Skeleton } from '@chakra-ui/react';
import React from 'react';
const SkeletonRow = ({ w = '100%' }: { w?: string }) => (
<>
<GridItem display="flex" columnGap={ 2 } w={{ base: '50%', lg: 'auto' }} _notFirst={{ mt: { base: 3, lg: 0 } }}>
<SkeletonCircle h={ 5 } w={ 5 }/>
<Skeleton flexGrow={ 1 } h={ 5 } borderRadius="full"/>
</GridItem>
<GridItem pl={{ base: 7, lg: 0 }}>
<Skeleton h={ 5 } borderRadius="full" w={{ base: '100%', lg: w }}/>
</GridItem>
</>
);
import DetailsSkeletonRow from 'ui/shared/skeletons/DetailsSkeletonRow';
const BlockDetailsSkeleton = () => {
const sectionGap = <GridItem colSpan={{ base: undefined, lg: 2 }} mt={{ base: 1, lg: 4 }}/>;
return (
<Grid columnGap={ 8 } rowGap={{ base: 5, lg: 7 }} templateColumns={{ base: '1fr', lg: '210px 1fr' }} maxW="1000px">
<SkeletonRow w="25%"/>
<SkeletonRow w="25%"/>
<SkeletonRow w="65%"/>
<SkeletonRow w="25%"/>
<SkeletonRow/>
<SkeletonRow/>
<DetailsSkeletonRow w="25%"/>
<DetailsSkeletonRow w="25%"/>
<DetailsSkeletonRow w="65%"/>
<DetailsSkeletonRow w="25%"/>
<DetailsSkeletonRow/>
<DetailsSkeletonRow/>
{ sectionGap }
<SkeletonRow w="50%"/>
<SkeletonRow w="25%"/>
<SkeletonRow w="50%"/>
<SkeletonRow w="50%"/>
<SkeletonRow w="50%"/>
<DetailsSkeletonRow w="50%"/>
<DetailsSkeletonRow w="25%"/>
<DetailsSkeletonRow w="50%"/>
<DetailsSkeletonRow w="50%"/>
<DetailsSkeletonRow w="50%"/>
{ sectionGap }
<GridItem colSpan={{ base: undefined, lg: 2 }}>
<Skeleton h={ 5 } borderRadius="full" w="100px"/>
......
......@@ -21,7 +21,7 @@ const BlocksTabSlot = ({ pagination }: Props) => {
const statsQuery = useQuery<unknown, unknown, HomeStats>(
[ QueryKeys.homeStats ],
() => fetch('/node-api/stats'),
() => fetch('/node-api/home-stats'),
);
if (isMobile) {
......
......@@ -42,7 +42,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
name: data?.name || '',
abi: JSON.stringify(data?.abi) || '',
},
mode: 'all',
mode: 'onTouched',
});
const queryClient = useQueryClient();
......@@ -118,7 +118,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
isInvalid={ Boolean(errors.name) }
maxLength={ NAME_MAX_LENGTH }
/>
<InputPlaceholder text="Project name" error={ errors.name?.message }/>
<InputPlaceholder text="Project name" error={ errors.name }/>
</FormControl>
);
}, [ errors, formBackgroundColor ]);
......@@ -132,7 +132,7 @@ const CustomAbiForm: React.FC<Props> = ({ data, onClose, setAlertVisible }) => {
minH="300px"
isInvalid={ Boolean(errors.abi) }
/>
<InputPlaceholder text="Custom ABI [{...}] (JSON format)" error={ errors.abi?.message }/>
<InputPlaceholder text="Custom ABI [{...}] (JSON format)" error={ errors.abi }/>
</FormControl>
);
}, [ errors, formBackgroundColor ]);
......
......@@ -8,7 +8,7 @@ import TestApp from 'playwright/TestApp';
import LatestBlocks from './LatestBlocks';
const STATS_API_URL = '/node-api/stats';
const STATS_API_URL = '/node-api/home-stats';
const BLOCKS_API_URL = '/node-api/index/blocks';
export const test = base.extend<socketServer.SocketServerFixture>({
......
......@@ -33,7 +33,7 @@ const LatestBlocks = () => {
const queryClient = useQueryClient();
const statsQueryResult = useQuery<unknown, unknown, HomeStats>(
[ QueryKeys.homeStats ],
() => fetch('/node-api/stats'),
() => fetch('/node-api/home-stats'),
);
const handleNewBlockMessage: SocketMessage.NewBlock['handler'] = React.useCallback((payload) => {
......
......@@ -14,7 +14,7 @@ export const test = base.extend<socketServer.SocketServerFixture>({
});
test('default view +@mobile +@dark-mode', async({ mount, page }) => {
await page.route('/node-api/stats', (route) => route.fulfill({
await page.route('/node-api/home-stats', (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsMock.base),
}));
......@@ -47,7 +47,7 @@ test.describe('socket', () => {
};
test('new item', async({ mount, page, createSocket }) => {
await page.route('/node-api/stats', (route) => route.fulfill({
await page.route('/node-api/home-stats', (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsMock.base),
}));
......
......@@ -7,7 +7,7 @@ import TestApp from 'playwright/TestApp';
import Stats from './Stats';
const API_URL = '/node-api/stats';
const API_URL = '/node-api/home-stats';
test('all items +@mobile +@dark-mode +@desktop-xl', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
......
......@@ -13,6 +13,7 @@ import txIcon from 'icons/transactions.svg';
import walletIcon from 'icons/wallet.svg';
import useFetch from 'lib/hooks/useFetch';
import StatsGasPrices from './StatsGasPrices';
import StatsItem from './StatsItem';
import StatsItemSkeleton from './StatsItemSkeleton';
......@@ -28,7 +29,7 @@ const Stats = () => {
const { data, isLoading, isError } = useQuery<unknown, unknown, HomeStats>(
[ QueryKeys.homeStats ],
async() => await fetch(`/node-api/stats`),
async() => await fetch(`/node-api/home-stats`),
);
if (isError) {
......@@ -44,6 +45,7 @@ const Stats = () => {
const lastItemTouchStyle = { gridColumn: { base: 'span 2', lg: 'unset' } };
if (data) {
const gasLabel = hasGasTracker ? <StatsGasPrices gasPrices={ data.gas_prices }/> : null;
content = (
<>
<StatsItem
......@@ -75,6 +77,7 @@ const Stats = () => {
title="Gas tracker"
value={ `${ Number(data.gas_prices.average).toLocaleString() } Gwei` }
_last={ itemsCount % 2 ? lastItemTouchStyle : undefined }
tooltipLabel={ gasLabel }
/>
) }
</>
......
import { Grid, GridItem, useColorModeValue } from '@chakra-ui/react';
import React from 'react';
import type { GasPrices } from 'types/api/stats';
const StatsGasPrices = ({ gasPrices }: {gasPrices: GasPrices}) => {
const nameStyleProps = {
color: useColorModeValue('blue.100', 'blue.600'),
};
return (
<Grid templateColumns="repeat(2, max-content)" rowGap={ 2 } columnGap={ 4 } padding={ 4 } fontSize="xs">
<GridItem { ...nameStyleProps }>Slow</GridItem>
<GridItem>{ `${ gasPrices.slow } Gwei` }</GridItem>
<GridItem { ...nameStyleProps }>Average</GridItem>
<GridItem>{ `${ gasPrices.average } Gwei` }</GridItem>
<GridItem { ...nameStyleProps }>Fast</GridItem>
<GridItem>{ `${ gasPrices.fast } Gwei` }</GridItem>
</Grid>
);
};
export default StatsGasPrices;
import { Flex, Icon, Text, useColorModeValue, chakra } from '@chakra-ui/react';
import { Box, Flex, Icon, Text, useColorModeValue, chakra, Tooltip } from '@chakra-ui/react';
import React from 'react';
import infoIcon from 'icons/info.svg';
import breakpoints from 'theme/foundations/breakpoints';
type Props = {
icon: React.FC<React.SVGAttributes<SVGElement>>;
title: string;
value: string;
className?: string;
tooltipLabel?: React.ReactNode;
}
const StatsItem = ({ icon, title, value, className }: Props) => {
const LARGEST_BREAKPOINT = '1240px';
const StatsItem = ({ icon, title, value, className, tooltipLabel }: 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' };
// 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 infoColor = useColorModeValue('gray.600', 'gray.400');
return (
<Flex
backgroundColor={ useColorModeValue('blue.50', 'blue.800') }
padding={ 3 }
borderRadius="md"
flexDirection={{ base: 'row', lg: 'column', xl: 'row' }}
flexDirection="row"
sx={ sxContainer }
alignItems="center"
columnGap={ 3 }
rowGap={ 2 }
className={ className }
color={ useColorModeValue('black', 'white') }
position="relative"
>
<Icon as={ icon } boxSize={ 7 }/>
<Flex flexDirection="column" alignItems={{ base: 'start', lg: 'center', xl: 'start' }}>
<Flex
flexDirection="column"
alignItems="start"
sx={ sxText }
>
<Text variant="secondary" fontSize="xs" lineHeight="16px">{ title }</Text>
<Text fontWeight={ 500 } fontSize="md" color={ useColorModeValue('black', 'white') }>{ value }</Text>
</Flex>
{ tooltipLabel && (
<Tooltip label={ tooltipLabel } hasArrow={ false } borderRadius="12px" placement="bottom-end" offset={ [ 0, 0 ] }>
<Box
position="absolute"
top={{ base: 'calc(50% - 12px)', lg: '10px', xl: 'calc(50% - 12px)' }}
right="10px">
<Icon
as={ infoIcon }
boxSize={ 6 }
color={ infoColor }
/>
</Box>
</Tooltip>
) }
</Flex>
);
};
......
......@@ -7,8 +7,8 @@ import TestApp from 'playwright/TestApp';
import ChainIndicators from './ChainIndicators';
const STATS_API_URL = '/node-api/stats';
const TX_CHART_API_URL = '/node-api/stats/charts/transactions';
const STATS_API_URL = '/node-api/home-stats';
const TX_CHART_API_URL = '/node-api/home-stats/charts/transactions';
test('daily txs chart +@mobile +@dark-mode +@dark-mode-mobile', async({ mount, page }) => {
await page.route(STATS_API_URL, (route) => route.fulfill({
......
......@@ -37,7 +37,7 @@ const ChainIndicators = () => {
const fetch = useFetch();
const statsQueryResult = useQuery<unknown, unknown, HomeStats>(
[ QueryKeys.homeStats ],
() => fetch('/node-api/stats'),
() => fetch('/node-api/home-stats'),
);
const bgColorDesktop = useColorModeValue('white', 'gray.900');
......
......@@ -19,7 +19,7 @@ const dailyTxsIndicator: TChainIndicator<QueryKeys.chartsTxs> = {
hint: `The total daily number of transactions on the blockchain for the last month.`,
api: {
queryName: QueryKeys.chartsTxs,
path: '/node-api/stats/charts/transactions',
path: '/node-api/home-stats/charts/transactions',
dataFn: (response) => ([ {
items: response.chart_data
.map((item) => ({ date: new Date(item.date), value: item.tx_count }))
......@@ -38,7 +38,7 @@ const coinPriceIndicator: TChainIndicator<QueryKeys.chartsMarket> = {
hint: `${ appConfig.network.currency.symbol } token daily price in USD.`,
api: {
queryName: QueryKeys.chartsMarket,
path: '/node-api/stats/charts/market',
path: '/node-api/home-stats/charts/market',
dataFn: (response) => ([ {
items: response.chart_data
.map((item) => ({ date: new Date(item.date), value: Number(item.closing_price) }))
......@@ -58,7 +58,7 @@ const marketPriceIndicator: TChainIndicator<QueryKeys.chartsMarket> = {
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.',
api: {
queryName: QueryKeys.chartsMarket,
path: '/node-api/stats/charts/market',
path: '/node-api/home-stats/charts/market',
dataFn: (response) => ([ {
items: response.chart_data
.map((item) => ({ date: new Date(item.date), value: Number(item.closing_price) * Number(response.available_supply) }))
......
......@@ -5,11 +5,13 @@ import React from 'react';
import type { Address } from 'types/api/address';
import { QueryKeys } from 'types/client/queries';
import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import useFetch from 'lib/hooks/useFetch';
import AddressDetails from 'ui/address/AddressDetails';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
const AddressPageContent = () => {
const router = useRouter();
......@@ -29,6 +31,14 @@ const AddressPageContent = () => {
...(addressQuery.data?.watchlist_names || []),
].map((tag) => <Tag key={ tag.label }>{ tag.display_name }</Tag>);
const tabs: Array<RoutedTab> = [
{ id: 'txs', title: 'Transactions', component: null },
{ id: 'token_transfers', title: 'Token transfers', component: null },
{ id: 'tokens', title: 'Tokens', component: null },
{ id: 'internal_txn', title: 'Internal txn', component: null },
{ id: 'coin_balance_history', title: 'Coin balance history', component: null },
];
return (
<Page>
<Flex alignItems="center" columnGap={ 3 }>
......@@ -40,6 +50,7 @@ const AddressPageContent = () => {
) }
</Flex>
<AddressDetails addressQuery={ addressQuery }/>
<RoutedTabs tabs={ tabs } tabListProps={{ mt: 8 }}/>
</Page>
);
};
......
......@@ -9,7 +9,7 @@ import TestApp from 'playwright/TestApp';
import Blocks from './Blocks';
const BLOCKS_API_URL = '/node-api/blocks?type=block';
const STATS_API_URL = '/node-api/stats';
const STATS_API_URL = '/node-api/home-stats';
const hooksConfig = {
router: {
query: { tab: 'blocks' },
......@@ -17,7 +17,7 @@ const hooksConfig = {
},
};
export const test = base.extend<socketServer.SocketServerFixture>({
const test = base.extend<socketServer.SocketServerFixture>({
createSocket: socketServer.createSocket,
});
......
......@@ -5,12 +5,13 @@ import * as blockMock from 'mocks/blocks/block';
import * as dailyTxsMock from 'mocks/stats/daily_txs';
import * as statsMock from 'mocks/stats/index';
import * as txMock from 'mocks/txs/tx';
import insertAdText from 'playwright/scripts/insertAdText';
import TestApp from 'playwright/TestApp';
import Home from './Home';
test('default view -@default +@desktop-xl +@mobile +@dark-mode', async({ mount, page }) => {
await page.route('/node-api/stats', (route) => route.fulfill({
await page.route('/node-api/home-stats', (route) => route.fulfill({
status: 200,
body: JSON.stringify(statsMock.base),
}));
......@@ -29,7 +30,7 @@ test('default view -@default +@desktop-xl +@mobile +@dark-mode', async({ mount,
txMock.withTokenTransfer,
]),
}));
await page.route('/node-api/stats/charts/transactions', (route) => route.fulfill({
await page.route('/node-api/home-stats/charts/transactions', (route) => route.fulfill({
status: 200,
body: JSON.stringify(dailyTxsMock.base),
}));
......@@ -40,5 +41,7 @@ test('default view -@default +@desktop-xl +@mobile +@dark-mode', async({ mount,
</TestApp>,
);
await page.evaluate(insertAdText);
await expect(component).toHaveScreenshot();
});
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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